feat(admin): add user-level direct permission system and enhance activity tracking
Features: - Add user_permissions table for direct user-to-permission grants (ops:user-ops) - Merge role_permissions + user_permissions in auth chain (login, middleware, getCurrentUser) - Add getUserQueryScope support for USER role with ops:user-ops (cross-tenant access) - Unify cross-tenant operation checks via getUserQueryScope (remove hardcoded SUPER_ADMIN checks) - Add 3 new API endpoints: GET/PUT /:id/permissions, GET /options/permissions - Support ops:user-ops as alternative permission on all user/tenant management routes - Frontend: add user-ops permission toggle on UserFormPage and UserDetailPage - Enhance DC module activity tracking (StreamAIController, SessionController, QuickActionController) - Fix DC AIController user ID extraction and feature name consistency - Add verify-activity-tracking.ts validation script - Update deployment checklist and admin module documentation DB Migration: 20260309_add_user_permissions_table Made-with: Cursor
This commit is contained in:
137
backend/scripts/verify-activity-tracking.ts
Normal file
137
backend/scripts/verify-activity-tracking.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 埋点验证脚本
|
||||
*
|
||||
* 检查 simple_logs 表中是否存在各关键埋点,
|
||||
* 并汇总每个模块/功能的记录数量。
|
||||
*
|
||||
* 用法: npx tsx scripts/verify-activity-tracking.ts [--days N]
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
interface TrackingPoint {
|
||||
module: string;
|
||||
feature: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const EXPECTED_TRACKING_POINTS: TrackingPoint[] = [
|
||||
// 系统级
|
||||
{ module: 'SYSTEM', feature: '用户登录', label: '用户登录' },
|
||||
{ module: 'SYSTEM', feature: '顶部导航点击', label: '顶部导航点击' },
|
||||
// ASL
|
||||
{ module: 'ASL', feature: '意图识别', label: 'ASL 意图识别(需求扩写)' },
|
||||
{ module: 'ASL', feature: 'Deep Research', label: 'ASL 启动 Deep Research' },
|
||||
// AIA (各智能体名称作为 feature)
|
||||
{ module: 'AIA', feature: '科学问题梳理', label: 'AIA 智能体 - 科学问题梳理(示例)' },
|
||||
// PKB
|
||||
{ module: 'PKB', feature: '创建知识库', label: 'PKB 创建知识库' },
|
||||
// DC
|
||||
{ module: 'DC', feature: '智能数据清洗', label: 'DC 智能数据清洗' },
|
||||
// IIT/CRA
|
||||
{ module: 'IIT', feature: 'CRA质控', label: 'IIT CRA质控' },
|
||||
// RVW
|
||||
{ module: 'RVW', feature: '稿', label: 'RVW 稿件审查相关' },
|
||||
];
|
||||
|
||||
async function main() {
|
||||
const daysArg = process.argv.findIndex(a => a === '--days');
|
||||
const days = daysArg >= 0 ? parseInt(process.argv[daysArg + 1], 10) || 7 : 7;
|
||||
|
||||
const since = new Date();
|
||||
since.setDate(since.getDate() - days);
|
||||
|
||||
console.log(`\n📊 埋点验证报告 (最近 ${days} 天,since ${since.toISOString().slice(0, 10)})\n`);
|
||||
console.log('='.repeat(80));
|
||||
|
||||
// 1. 总记录数
|
||||
const totalCount = await prisma.simple_logs.count({
|
||||
where: { created_at: { gte: since } },
|
||||
});
|
||||
console.log(`\n📈 总记录数: ${totalCount}\n`);
|
||||
|
||||
// 2. 按模块统计
|
||||
const moduleStats = await prisma.$queryRaw`
|
||||
SELECT module, COUNT(*) as count
|
||||
FROM admin_schema.simple_logs
|
||||
WHERE created_at >= ${since}
|
||||
GROUP BY module
|
||||
ORDER BY count DESC
|
||||
` as Array<{ module: string; count: bigint }>;
|
||||
|
||||
console.log('📦 模块统计:');
|
||||
console.log('-'.repeat(40));
|
||||
for (const row of moduleStats) {
|
||||
console.log(` ${row.module.padEnd(12)} ${Number(row.count).toString().padStart(6)} 条`);
|
||||
}
|
||||
|
||||
// 3. 检查每个关键埋点是否存在
|
||||
console.log('\n🔍 关键埋点覆盖检查:');
|
||||
console.log('-'.repeat(80));
|
||||
|
||||
let coveredCount = 0;
|
||||
let missingCount = 0;
|
||||
|
||||
for (const tp of EXPECTED_TRACKING_POINTS) {
|
||||
const count = await prisma.simple_logs.count({
|
||||
where: {
|
||||
module: tp.module,
|
||||
feature: { contains: tp.feature },
|
||||
created_at: { gte: since },
|
||||
},
|
||||
});
|
||||
|
||||
const status = count > 0 ? '✅' : '❌';
|
||||
if (count > 0) coveredCount++;
|
||||
else missingCount++;
|
||||
|
||||
console.log(` ${status} ${tp.label.padEnd(35)} ${count > 0 ? `${count} 条` : '缺失'}`);
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(80));
|
||||
console.log(`\n📋 结果: ${coveredCount}/${EXPECTED_TRACKING_POINTS.length} 已覆盖, ${missingCount} 缺失\n`);
|
||||
|
||||
// 4. 按 feature 统计 Top 20
|
||||
console.log('🏆 Top 20 Feature (按记录数):');
|
||||
console.log('-'.repeat(80));
|
||||
const topFeatures = await prisma.$queryRaw`
|
||||
SELECT module, feature, action, COUNT(*) as count
|
||||
FROM admin_schema.simple_logs
|
||||
WHERE created_at >= ${since}
|
||||
GROUP BY module, feature, action
|
||||
ORDER BY count DESC
|
||||
LIMIT 20
|
||||
` as Array<{ module: string; feature: string; action: string; count: bigint }>;
|
||||
|
||||
for (const row of topFeatures) {
|
||||
console.log(` ${row.module.padEnd(10)} ${row.feature.padEnd(25)} ${row.action.padEnd(10)} ${Number(row.count).toString().padStart(6)} 条`);
|
||||
}
|
||||
|
||||
// 5. DAU/MAU
|
||||
const dauResult = await prisma.$queryRaw`
|
||||
SELECT COUNT(DISTINCT user_id) as dau
|
||||
FROM admin_schema.simple_logs
|
||||
WHERE created_at >= CURRENT_DATE
|
||||
` as Array<{ dau: bigint }>;
|
||||
|
||||
const mauResult = await prisma.$queryRaw`
|
||||
SELECT COUNT(DISTINCT user_id) as mau
|
||||
FROM admin_schema.simple_logs
|
||||
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
|
||||
` as Array<{ mau: bigint }>;
|
||||
|
||||
console.log(`\n👤 DAU (今日活跃用户): ${Number(dauResult[0]?.dau || 0)}`);
|
||||
console.log(`👥 MAU (30日活跃用户): ${Number(mauResult[0]?.mau || 0)}`);
|
||||
|
||||
console.log('\n✅ 验证完成\n');
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => prisma.$disconnect())
|
||||
.catch(async (e) => {
|
||||
console.error('❌ 验证失败:', e);
|
||||
await prisma.$disconnect();
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user