非常好的选择!让我们来实现数据归档策略和自定义仪表板功能。这将帮助我们更好地管理数据并提供更灵活的分析体验。

第三十五部分:数据归档和自定义仪表板

  1. 实现数据归档策略

首先,我们需要创建历史表来存储归档的数据。更新 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);
  1. 实现自定义仪表板功能

首先,我们需要创建一个新的表来存储用户的仪表板配置。更新 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>

这些改进实现了以下功能:

  1. 数据归档策略:

    • 创建了历史表来存储归档的数据
    • 实现了定期归档任务,将 30 天前的数据移动到历史表中
    • 使用事务确保数据移动的一致性和安全性
  2. 自定义仪表板功能:

    • 创建了新的表来存储用户的仪表板配置
    • 实现了保存和加载用户仪表板配置的 API
    • 添加了自定义仪表板的用户界面,允许用户选择他们想要显示的指标
    • 实现了根据用户配置动态渲染仪表板的功能

主要改进包括:

  • 通过数据归档策略,提高了系统对大规模数据的处理能力
  • 提供了更灵活的分析体验,允许用户根据自己的需求定制仪表板
  • 改进了用户界面,添加了模态框用于仪表板定制

接下来的步骤可以包括:

  1. 实现数据归档的管理界面,允许管理员查看和管理归档的数据
  2. 添加更多的可视化选项,如折线图、柱状图等
  3. 实现仪表板共享功能,允许用户与他人分享自己的仪表板配置
  4. 添加数据导出功能,允许用户导出选定时间范围内的数据
  5. 实现更高级的数据分析功能,如趋势分析、同比环比分析等

你希望继续深入哪个方向,或者有其他想法吗?