非常好的选择!让我们来实现数据归档策略和自定义仪表板功能。这将帮助我们更好地管理数据并提供更灵活的分析体验。
第三十五部分:数据归档和自定义仪表板
- 实现数据归档策略
首先,我们需要创建历史表来存储归档的数据。更新 src/db/schema.ts:
import { sqliteTable, integer, text, real } from "drizzle-orm/sqlite-core";
import { sql } from "drizzle-orm";
// ... 保留之前的表定义
export const userBehaviorsArchive = sqliteTable("user_behaviors_archive", {
id: integer("id").primaryKey(),
userId: integer("user_id"),
data: text("data"),
timestamp: integer("timestamp", { mode: "timestamp" }),
date: text("date"),
});
export const conversionsArchive = sqliteTable("conversions_archive", {
id: integer("id").primaryKey(),
userId: integer("user_id"),
type: text("type"),
value: real("value"),
timestamp: integer("timestamp", { mode: "timestamp" }),
date: text("date"),
});
// 添加索引
db.run(`CREATE INDEX IF NOT EXISTS idx_user_behaviors_archive_date ON user_behaviors_archive (date)`);
db.run(`CREATE INDEX IF NOT EXISTS idx_conversions_archive_date ON conversions_archive (date)`);然后,创建一个新的文件 src/tasks/dataArchive.ts 来处理数据归档:
import { db } from "../db";
import { userBehaviors, conversions, userBehaviorsArchive, conversionsArchive } from "../db/schema";
import { sql } from "drizzle-orm";
export async function archiveOldData() {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
// 归档用户行为数据
await db.transaction(async (tx) => {
await tx.insert(userBehaviorsArchive).from(
db.select().from(userBehaviors).where(sql`${userBehaviors.timestamp} < ${thirtyDaysAgo}`)
);
await tx.delete(userBehaviors).where(sql`${userBehaviors.timestamp} < ${thirtyDaysAgo}`);
});
// 归档转化数据
await db.transaction(async (tx) => {
await tx.insert(conversionsArchive).from(
db.select().from(conversions).where(sql`${conversions.timestamp} < ${thirtyDaysAgo}`)
);
await tx.delete(conversions).where(sql`${conversions.timestamp} < ${thirtyDaysAgo}`);
});
console.log("Data archiving completed");
}更新 src/index.ts 以定期运行归档任务:
import { Elysia } from "elysia";
import { cron } from "@elysiajs/cron";
import { archiveOldData } from "./tasks/dataArchive";
const app = new Elysia()
// ... 其他配置
.use(cron({
name: 'archive-old-data',
pattern: '0 0 * * *', // 每天午夜运行
run: archiveOldData,
}))
// ... 其他路由和配置
app.listen(3000);- 实现自定义仪表板功能
首先,我们需要创建一个新的表来存储用户的仪表板配置。更新 src/db/schema.ts:
export const dashboardConfigs = sqliteTable("dashboard_configs", {
id: integer("id").primaryKey(),
userId: integer("user_id").references(() => users.id),
config: text("config"), // 存储 JSON 格式的配置
});然后,创建新的路由来处理仪表板配置。更新 src/routes/analytics.ts:
import { Elysia } from "elysia";
import { db } from "../db";
import { dashboardConfigs } from "../db/schema";
export const analyticsRoutes = new Elysia()
// ... 保留之前的路由
.get("/api/dashboard-config", async ({ cookie: { userId } }) => {
const config = await db
.select()
.from(dashboardConfigs)
.where(sql`${dashboardConfigs.userId} = ${userId}`)
.get();
return config ? JSON.parse(config.config) : null;
})
.post("/api/dashboard-config", async ({ body, cookie: { userId } }) => {
const existingConfig = await db
.select()
.from(dashboardConfigs)
.where(sql`${dashboardConfigs.userId} = ${userId}`)
.get();
if (existingConfig) {
await db
.update(dashboardConfigs)
.set({ config: JSON.stringify(body) })
.where(sql`${dashboardConfigs.userId} = ${userId}`);
} else {
await db.insert(dashboardConfigs).values({
userId,
config: JSON.stringify(body),
});
}
return { success: true };
});现在,更新 src/views/analytics.eta 以支持自定义仪表板:
<% layout('./layouts/main.eta', { title: 'Analytics' }) %>
<h1>Custom Analytics Dashboard</h1>
<div id="custom-dashboard"></div>
<button id="customize-dashboard">Customize Dashboard</button>
<div id="customize-modal" class="modal" style="display: none;">
<div class="modal-content">
<h2>Customize Your Dashboard</h2>
<div id="metric-options"></div>
<button id="save-dashboard">Save</button>
<button id="close-modal">Close</button>
</div>
</div>
<script>
const availableMetrics = [
{ id: 'active-users', name: 'Active Users' },
{ id: 'page-views', name: 'Page Views' },
{ id: 'conversions', name: 'Conversions' },
{ id: 'conversion-rate', name: 'Conversion Rate' },
{ id: 'revenue', name: 'Revenue' },
{ id: 'active-pages', name: 'Top Active Pages' },
];
let dashboardConfig = [];
async function loadDashboardConfig() {
const response = await fetch('/api/dashboard-config');
dashboardConfig = await response.json() || availableMetrics.map(m => m.id);
renderDashboard();
}
function renderDashboard() {
const dashboard = document.getElementById('custom-dashboard');
dashboard.innerHTML = '';
dashboardConfig.forEach(metricId => {
const metric = availableMetrics.find(m => m.id === metricId);
if (metric) {
const metricElement = document.createElement('div');
metricElement.className = 'stat-box';
metricElement.innerHTML = `<h2>${metric.name}</h2><p id="${metric.id}">Loading...</p>`;
dashboard.appendChild(metricElement);
}
});
updateRealTimeStats();
}
async function updateRealTimeStats() {
const response = await fetch('/api/real-time-stats');
const stats = await response.json();
if (dashboardConfig.includes('active-users')) {
document.getElementById('active-users').textContent = stats.activeUsers;
}
if (dashboardConfig.includes('page-views')) {
document.getElementById('page-views').textContent = stats.pageViews;
}
if (dashboardConfig.includes('conversions')) {
document.getElementById('conversions').textContent = stats.conversions;
}
if (dashboardConfig.includes('conversion-rate')) {
document.getElementById('conversion-rate').textContent = `${stats.conversionRate}%`;
}
if (dashboardConfig.includes('revenue')) {
document.getElementById('revenue').textContent = `$${stats.revenue.toFixed(2)}`;
}
if (dashboardConfig.includes('active-pages')) {
const activePagesList = document.getElementById('active-pages');
activePagesList.innerHTML = '';
stats.activePages.forEach(page => {
const li = document.createElement('li');
li.textContent = `${page.path}: ${page.count} views`;
activePagesList.appendChild(li);
});
}
}
document.getElementById('customize-dashboard').addEventListener('click', () => {
const modal = document.getElementById('customize-modal');
const optionsContainer = document.getElementById('metric-options');
optionsContainer.innerHTML = '';
availableMetrics.forEach(metric => {
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `option-${metric.id}`;
checkbox.checked = dashboardConfig.includes(metric.id);
const label = document.createElement('label');
label.htmlFor = `option-${metric.id}`;
label.textContent = metric.name;
optionsContainer.appendChild(checkbox);
optionsContainer.appendChild(label);
optionsContainer.appendChild(document.createElement('br'));
});
modal.style.display = 'block';
});
document.getElementById('save-dashboard').addEventListener('click', async () => {
dashboardConfig = availableMetrics
.filter(metric => document.getElementById(`option-${metric.id}`).checked)
.map(metric => metric.id);
await fetch('/api/dashboard-config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dashboardConfig),
});
renderDashboard();
document.getElementById('customize-modal').style.display = 'none';
});
document.getElementById('close-modal').addEventListener('click', () => {
document.getElementById('customize-modal').style.display = 'none';
});
// 初始加载
loadDashboardConfig();
setInterval(updateRealTimeStats, 5000); // 每5秒更新一次
</script>
<style>
.stat-box {
background-color: #f0f0f0;
border-radius: 8px;
padding: 1em;
margin: 0.5em;
text-align: center;
}
.modal {
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: #fefefe;
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
}
</style>这些改进实现了以下功能:
-
数据归档策略:
- 创建了历史表来存储归档的数据
- 实现了定期归档任务,将 30 天前的数据移动到历史表中
- 使用事务确保数据移动的一致性和安全性
-
自定义仪表板功能:
- 创建了新的表来存储用户的仪表板配置
- 实现了保存和加载用户仪表板配置的 API
- 添加了自定义仪表板的用户界面,允许用户选择他们想要显示的指标
- 实现了根据用户配置动态渲染仪表板的功能
主要改进包括:
- 通过数据归档策略,提高了系统对大规模数据的处理能力
- 提供了更灵活的分析体验,允许用户根据自己的需求定制仪表板
- 改进了用户界面,添加了模态框用于仪表板定制
接下来的步骤可以包括:
- 实现数据归档的管理界面,允许管理员查看和管理归档的数据
- 添加更多的可视化选项,如折线图、柱状图等
- 实现仪表板共享功能,允许用户与他人分享自己的仪表板配置
- 添加数据导出功能,允许用户导出选定时间范围内的数据
- 实现更高级的数据分析功能,如趋势分析、同比环比分析等
你希望继续深入哪个方向,或者有其他想法吗?