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:
2026-03-10 09:02:35 +08:00
parent 971e903acf
commit 097e7920ab
19 changed files with 693 additions and 87 deletions

View 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);
});