diff --git a/client/src/api/logs.ts b/client/src/api/logs.ts index cdcbe2d..d68dd95 100644 --- a/client/src/api/logs.ts +++ b/client/src/api/logs.ts @@ -24,6 +24,24 @@ export interface LogStats { }> } +/** 埋点统计 */ +export interface TrackStats { + today: Array<{ event: string; count: number }> + daily: Array<{ date: string; count: number }> + events: Array<{ event: string; count: number }> +} + +/** 埋点事件 */ +export interface TrackEvent { + id: number + event: string + page: string + data: any + user_id: number | null + nickname: string | null + created_at: string +} + /** 获取日志文件列表 */ export function getLogFiles(): Promise { return request({ url: '/logs/files' }) @@ -44,3 +62,17 @@ export function queryLogs(params: { }): Promise<{ lines: string[]; total: number; page: number; pageSize: number }> { return request({ url: `/logs/${params.date}`, data: { ...params, date: undefined } }) } + +/** 获取埋点统计 */ +export function getTrackStats(): Promise { + return request({ url: '/logs/track/stats' }) +} + +/** 查询埋点事件 */ +export function queryTrackEvents(params?: { + event?: string + page?: number + pageSize?: number +}): Promise<{ list: TrackEvent[]; total: number; page: number; pageSize: number }> { + return request({ url: '/logs/track/list', data: params }) +} diff --git a/client/src/pages.json b/client/src/pages.json index 814c8c8..810de98 100644 --- a/client/src/pages.json +++ b/client/src/pages.json @@ -139,6 +139,14 @@ "navigationBarTitleText": "系统日志", "enablePullDownRefresh": true } + }, + { + "path": "pages/admin/track", + "style": { + "navigationStyle": "custom", + "navigationBarTitleText": "埋点统计", + "enablePullDownRefresh": true + } } ], "globalStyle": { diff --git a/client/src/pages/admin/index.vue b/client/src/pages/admin/index.vue index cf06f88..2323fa9 100644 --- a/client/src/pages/admin/index.vue +++ b/client/src/pages/admin/index.vue @@ -87,6 +87,11 @@ 系统日志 + + + 埋点统计 + + @@ -153,6 +158,7 @@ function goNotificationManage() { uni.navigateTo({ url: '/pages/admin/notificati function goFeedbackManage() { uni.navigateTo({ url: '/pages/admin/feedback' }) } function goConfig() { uni.navigateTo({ url: '/pages/admin/config' }) } function goLogs() { uni.navigateTo({ url: '/pages/admin/logs' }) } +function goTrack() { uni.navigateTo({ url: '/pages/admin/track' }) } diff --git a/server/src/db/init.ts b/server/src/db/init.ts index f761298..21e0e44 100644 --- a/server/src/db/init.ts +++ b/server/src/db/init.ts @@ -224,6 +224,25 @@ async function runMigrations(conn: mysql.Connection) { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `) } + + // 埋点事件表 + const hasTrackEvents = await tableExists(conn, 'track_events') + if (!hasTrackEvents) { + console.log('[DB] Migrating: creating track_events table') + await conn.query(` + CREATE TABLE IF NOT EXISTS track_events ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + event VARCHAR(50) NOT NULL COMMENT '事件类型', + page VARCHAR(100) DEFAULT '' COMMENT '页面路径', + data JSON DEFAULT NULL COMMENT '事件数据', + user_id INT DEFAULT NULL COMMENT '用户ID', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_event (event), + INDEX idx_created (created_at), + INDEX idx_user (user_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + `) + } } export async function initDatabase() { diff --git a/server/src/db/schema.sql b/server/src/db/schema.sql index 0c4a59d..116dd32 100644 --- a/server/src/db/schema.sql +++ b/server/src/db/schema.sql @@ -137,3 +137,15 @@ CREATE TABLE IF NOT EXISTS sys_config ( config_value TEXT NOT NULL COMMENT '配置值', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS track_events ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + event VARCHAR(50) NOT NULL COMMENT '事件类型', + page VARCHAR(100) DEFAULT '' COMMENT '页面路径', + data JSON DEFAULT NULL COMMENT '事件数据', + user_id INT DEFAULT NULL COMMENT '用户ID', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_event (event), + INDEX idx_created (created_at), + INDEX idx_user (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/server/src/routes/logs.ts b/server/src/routes/logs.ts index bd92ad0..e285d6f 100644 --- a/server/src/routes/logs.ts +++ b/server/src/routes/logs.ts @@ -1,6 +1,7 @@ import { Router } from 'express' import fs from 'fs' import path from 'path' +import pool from '../db/connection' import { requireAdmin } from '../middleware/requireAdmin' const router = Router() @@ -143,4 +144,84 @@ router.get('/:date', requireAdmin, (req, res) => { } }) +/** 获取埋点统计 */ +router.get('/track/stats', requireAdmin, async (_req, res) => { + try { + // 今日统计 + const [todayRows] = await pool.execute( + `SELECT event, COUNT(*) as count FROM track_events + WHERE DATE(created_at) = CURDATE() + GROUP BY event ORDER BY count DESC` + ) + + // 最近7天每日统计 + const [dailyRows] = await pool.execute( + `SELECT DATE(created_at) as date, COUNT(*) as count FROM track_events + WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) + GROUP BY DATE(created_at) ORDER BY date DESC` + ) + + // 最近7天事件分布 + const [eventRows] = await pool.execute( + `SELECT event, COUNT(*) as count FROM track_events + WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) + GROUP BY event ORDER BY count DESC LIMIT 10` + ) + + res.json({ + code: 0, + data: { + today: todayRows, + daily: dailyRows, + events: eventRows + } + }) + } catch (err) { + console.error('[Track] Stats error:', err) + res.status(500).json({ code: 50000, message: '获取埋点统计失败' }) + } +}) + +/** 查询埋点事件 */ +router.get('/track/list', requireAdmin, async (req, res) => { + try { + const { event, page = '1', pageSize = '50' } = req.query + const pNum = Math.max(1, parseInt(page as string) || 1) + const pSize = Math.min(200, Math.max(1, parseInt(pageSize as string) || 50)) + const offset = (pNum - 1) * pSize + + let where = '1=1' + const params: any[] = [] + + if (event && typeof event === 'string') { + where += ' AND t.event = ?' + params.push(event) + } + + const [countResult] = await pool.execute( + `SELECT COUNT(*) as total FROM track_events t WHERE ${where}`, + params + ) + const total = (countResult as any[])[0].total + + const [rows] = await pool.execute( + `SELECT t.*, u.nickname + FROM track_events t + LEFT JOIN users u ON t.user_id = u.id + WHERE ${where} + ORDER BY t.created_at DESC + LIMIT ? OFFSET ?`, + [...params, pSize, offset] + ) + + res.json({ + code: 0, + data: { list: rows, total, page: pNum, pageSize: pSize } + }) + } catch (err) { + console.error('[Track] List error:', err) + res.status(500).json({ code: 50000, message: '查询埋点失败' }) + } +}) + export default router diff --git a/server/src/routes/track.ts b/server/src/routes/track.ts index a94fde2..7ad3eac 100644 --- a/server/src/routes/track.ts +++ b/server/src/routes/track.ts @@ -1,24 +1,24 @@ import { Router } from 'express' -import { logInfo } from '../middleware/logger' +import pool from '../db/connection' const router = Router() /** 接收前端埋点数据 */ -router.post('/', (req, res) => { +router.post('/', async (req, res) => { try { const { events } = req.body + const userId = (req as any).userId || null if (!Array.isArray(events)) { return res.status(400).json({ code: 40001, message: '无效的埋点数据' }) } - // 记录埋点事件 + // 批量插入埋点事件 for (const event of events) { - logInfo(`[Track] ${event.event}`, { - page: event.page, - data: event.data, - userId: (req as any).userId || null - }) + await pool.execute( + 'INSERT INTO track_events (event, page, data, user_id) VALUES (?, ?, ?, ?)', + [event.event, event.page || '', JSON.stringify(event.data || {}), userId] + ) } res.json({ code: 0 })