feat: 埋点统计系统 — 数据库存储 + 管理后台查询
后端: - track_events 表存储埋点数据(事件类型、页面、数据、用户) - track 路由改为写入数据库 - logs 路由新增 /track/stats 和 /track/list 查询接口 前端: - 新增埋点统计页面(今日事件、7天趋势、热门事件) - 支持按事件类型筛选 - 管理后台添加「埋点统计」入口
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user