Backend fixes: - Fix PgBoss task infinite loop on SAE (root cause: missing queue table constraints) - Add singletonKey to prevent duplicate job enqueueing - Add idempotency check in reviewWorker (skip completed tasks) - Add optimistic locking in reviewService (atomic status update) Frontend fixes: - Add isSubmitting state to prevent duplicate submissions in RVW Dashboard - Fix API baseURL in knowledgeBaseApi (relative path) Cleanup (removed): - Old frontend/ directory (migrated to frontend-v2) - python-microservice/ (unused, replaced by extraction_service) - Root package.json and node_modules (accidentally created) - redcap-docker-dev/ (external dependency) - Various temporary files and outdated docs in root New documentation: - docs/07-运维文档/01-PgBoss队列监控与维护.md - docs/07-运维文档/02-故障预防检查清单.md - docs/07-运维文档/03-数据库迁移注意事项.md Database fix applied to RDS: - Added PRIMARY KEY to platform_schema.queue - Added 3 missing foreign key constraints Tested: Local build passed, RDS constraints verified
22 KiB
22 KiB
运营监控系统 MVP 开发计划
文档版本:V3.1 (完整版)
创建日期:2026-01-25
基于文档:运营体系设计方案-MVP-V3.0.md
预计工时:4-5 小时
📋 修订说明
本计划基于 V3.0 方案进行审查修订,主要解决以下 8 个问题:
| # | 问题 | 严重程度 | 修订内容 |
|---|---|---|---|
| 1 | 模块覆盖不完整 | 🔴 严重 | 补充 RVW、IIT、Protocol Agent、SSA/ST 预留 |
| 2 | 缺少 tenantName 字段 | 🔴 严重 | 添加冗余字段避免 JOIN |
| 3 | RVW 埋点清单缺失 | 🔴 严重 | 新增 RVW 模块埋点清单 |
| 4 | 用户360画像缺少 RVW | 🔴 严重 | 补充 RVW 资产统计 |
| 5 | action 类型不够全面 | 🟡 中等 | 扩展 CREATE/DELETE 类型 |
| 6 | 缺少 API 路由设计 | 🟡 中等 | 新增完整 API 端点设计 |
| 7 | 数据保留策略缺失 | 🟡 中等 | 补充 180 天数据清理 |
| 8 | 权限控制未说明 | 🟡 中等 | 明确角色权限矩阵 |
1. 核心指标定义(保持 V3.0)
| 优先级 | 指标名称 | 定义 | 价值 |
|---|---|---|---|
| P0+ | 活跃医生数 (DAU) | 今日有行为的去重 user_id 数 | 真实价值线 |
| P0 | 活跃租户数 (DAT) | 今日有行为的去重 tenant_id 数 | 商务生死线 |
| P1 | 功能渗透率 | 各模块/功能使用次数分布 | 产品迭代指引 |
| P2 | 价值交付次数 | 导出/下载次数 | 北极星指标 |
2. 数据库设计(V3.1 修订版)
2.1 SimpleLog 表(admin_schema)
/// 运营日志表 (MVP V3.1)
model SimpleLog {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
createdAt DateTime @default(now()) @map("created_at")
// === 租户和用户信息 ===
tenantId String @map("tenant_id") @db.VarChar(50)
tenantName String? @map("tenant_name") @db.VarChar(100) // 🆕 冗余字段,避免JOIN
userId String @map("user_id") @db.Uuid
userName String? @map("user_name") @db.VarChar(50)
// === 行为记录 ===
module String @db.VarChar(20) // 模块代码
feature String @db.VarChar(50) // 细分功能
action String @db.VarChar(20) // 动作类型
// === 详情信息 ===
info String? @db.Text // JSON或文本详情
// === 索引 ===
@@index([createdAt])
@@index([tenantId])
@@index([userId])
@@index([module, feature])
@@index([action]) // 🆕 支持按动作筛选
@@map("simple_logs")
@@schema("admin_schema")
}
2.2 字段说明
module(模块代码)- 完整列表
type ModuleCode =
| 'AIA' // AI智能问答 (12智能体 + Protocol Agent)
| 'PKB' // 个人知识库
| 'ASL' // AI智能文献
| 'DC' // 数据清洗整理
| 'RVW' // 稿件审查系统 🆕
| 'IIT' // IIT Manager Agent 🆕
| 'SSA' // 智能统计分析 (预留) 🆕
| 'ST' // 统计分析工具 (预留) 🆕
| 'SYSTEM'; // 系统级行为 (登录/登出)
action(动作类型)
type ActionType =
| 'LOGIN' // 登录系统
| 'USE' // 使用功能
| 'EXPORT' // 导出/下载
| 'CREATE' // 创建资源 🆕
| 'DELETE' // 删除资源 🆕
| 'ERROR'; // 错误记录
3. 完整埋点清单(按模块)
3.1 🤖 AIA 模块(AI智能问答)
12 个智能体
| agentId | feature (中文) | 埋点位置 |
|---|---|---|
| topic-scoping | 科学问题梳理 | conversationService.complete() |
| pico-analysis | PICO梳理 | conversationService.complete() |
| topic-eval | 选题评价 | conversationService.complete() |
| outcome-design | 观察指标设计 | conversationService.complete() |
| crf-design | CRF设计 | conversationService.complete() |
| sample-size | 样本量计算 | conversationService.complete() |
| protocol-writing | 方案撰写 | conversationService.complete() |
| methodology-review | 方法学评审 | conversationService.complete() |
| paper-polish | 论文润色 | conversationService.complete() |
| paper-translate | 论文翻译 | conversationService.complete() |
| data-preprocess | 数据预处理 | 跳转DC,记录来源 |
| stat-analysis | 统计分析 | 跳转DC,记录来源 |
Protocol Agent(🆕 2026-01-25 新功能)
| feature | action | 埋点位置 | info 示例 |
|---|---|---|---|
| Protocol要素收集 | USE | ProtocolOrchestrator.collectPhase() | "阶段1完成" |
| Protocol方案生成 | USE | ProtocolOrchestrator.generateProtocol() | "生成12章节方案" |
| Protocol Word导出 | EXPORT | ProtocolAgentController.exportWord() | "导出Word文档" |
埋点代码位置:
backend/src/modules/agent/protocol/services/ProtocolOrchestrator.tsbackend/src/modules/agent/protocol/controllers/ProtocolAgentController.ts
3.2 📚 PKB 模块(个人知识库)
| feature | action | 埋点位置 | info 示例 |
|---|---|---|---|
| 知识库创建 | CREATE | knowledgeBaseController.create() | "创建: 肺癌研究库" |
| 文档上传 | USE | documentController.upload() | "上传: 5篇PDF" |
| RAG问答 | USE | ragController.chat() | "提问: 入排标准是什么?" |
| 批处理提取 | USE | batchController.process() | "批量提取: 10篇" |
| 结果导出 | EXPORT | batchController.export() | "导出CSV" |
埋点代码位置:
backend/src/modules/pkb/controllers/
3.3 📖 ASL 模块(AI智能文献)
| feature | action | 埋点位置 | info 示例 |
|---|---|---|---|
| DeepSearch检索 | USE | researchController.stream() | "关键词: 肺癌治疗" |
| 标题摘要筛选 | USE | screeningController.start() | "筛选: 500篇" |
| 全文复筛 | USE | fullTextController.start() | "复筛: 100篇" |
| 筛选结果导出 | EXPORT | screeningController.export() | "导出Excel" |
埋点代码位置:
backend/src/modules/asl/controllers/
3.4 🧹 DC 模块(数据清洗整理)
| feature | action | 埋点位置 | info 示例 |
|---|---|---|---|
| Tool B 健康检查 | USE | toolBController.healthCheck() | "检查: 1000行数据" |
| Tool B 自动提取 | USE | toolBController.extract() | "提取任务: 50条" |
| Tool C 数据清洗 | USE | toolCController.process() | "执行: 筛选操作" |
| Tool C Pivot | USE | toolCController.pivot() | "Pivot转换" |
| 结果导出 | EXPORT | toolCController.export() | "导出Excel" |
埋点代码位置:
backend/src/modules/dc/controllers/
3.5 📝 RVW 模块(稿件审查系统)🆕
| feature | action | 埋点位置 | info 示例 |
|---|---|---|---|
| 稿件上传 | USE | reviewController.upload() | "上传: xxx.pdf" |
| 稿约规范性审查 | USE | reviewWorker (editorial) | "审查开始" |
| 方法学审查 | USE | reviewWorker (methodology) | "方法学审查开始" |
| 审查完成 | USE | reviewWorker.complete() | "评分: 规范85/方法78" |
| 报告导出 | EXPORT | TaskDetail.exportWord() | "导出Word报告" |
埋点代码位置:
backend/src/modules/rvw/services/reviewWorker.tsbackend/src/modules/rvw/controllers/reviewController.ts
3.6 🏥 IIT 模块(IIT Manager Agent)🆕
| feature | action | 埋点位置 | info 示例 |
|---|---|---|---|
| REDCap数据同步 | USE | redcapAdapter.sync() | "同步: 10条记录" |
| AI质控检查 | USE | qualityCheckService.check() | "检查患者ID 7" |
| 企微通知推送 | USE | wechatService.notify() | "推送预警通知" |
| 对话查询 | USE | chatService.query() | "查询患者统计" |
| 人工确权 | USE | actionController.approve() | "确权: 排除患者" |
埋点代码位置:
backend/src/modules/iit-manager/services/backend/src/modules/iit-manager/controllers/
3.7 🔐 SYSTEM(系统级)
| feature | action | 埋点位置 | info 示例 |
|---|---|---|---|
| 用户登录 | LOGIN | authController.login() | "密码登录" |
| 用户登出 | USE | authController.logout() | - |
埋点代码位置:
backend/src/common/auth/auth.controller.ts
4. 后端 API 设计
4.1 运营统计 API
| 方法 | 路径 | 说明 | 权限 |
|---|---|---|---|
| GET | /api/admin/stats/overview |
今日大盘(DAU/DAT/导出数) | SUPER_ADMIN |
| GET | /api/admin/stats/live-feed |
实时流水账(最近100条) | SUPER_ADMIN |
| GET | /api/admin/stats/module/:code |
模块使用统计 | SUPER_ADMIN |
| GET | /api/admin/users/:id/overview |
用户360画像 | SUPER_ADMIN |
4.2 API 响应示例
今日大盘 /api/admin/stats/overview
{
"success": true,
"data": {
"dau": 12, // 今日活跃医生数
"dat": 3, // 今日活跃租户数
"exportCount": 5, // 今日导出次数
"moduleStats": {
"AIA": 45,
"PKB": 23,
"DC": 12,
"RVW": 8,
"ASL": 5,
"IIT": 3
}
}
}
实时流水账 /api/admin/stats/live-feed
{
"success": true,
"data": [
{
"id": "uuid",
"createdAt": "2026-01-25T10:05:00Z",
"tenantName": "协和医院",
"userName": "张主任",
"module": "AIA",
"feature": "选题评价",
"action": "USE",
"info": "评价得分: 85分"
}
]
}
用户360画像 /api/admin/users/:id/overview
{
"success": true,
"data": {
"profile": {
"id": "uuid",
"name": "张主任",
"phone": "138****1234",
"tenantName": "协和医院"
},
"assets": {
"aia": { "conversationCount": 158 },
"pkb": { "kbCount": 3, "docCount": 450 },
"dc": { "taskCount": 12 },
"rvw": { "reviewTaskCount": 25, "completedCount": 20 } // 🆕
},
"activities": [
{
"createdAt": "2026-01-25T10:30:00Z",
"module": "AIA",
"feature": "选题评价",
"action": "USE",
"info": "生成结果: 85分"
}
]
}
}
5. 后端服务实现
5.1 ActivityService(埋点服务)
文件路径:backend/src/common/services/activity.service.ts
import { prisma } from '../../config/database.js';
import { logger } from '../logging/index.js';
type ModuleCode = 'AIA' | 'PKB' | 'ASL' | 'DC' | 'RVW' | 'IIT' | 'SSA' | 'ST' | 'SYSTEM';
type ActionType = 'LOGIN' | 'USE' | 'EXPORT' | 'CREATE' | 'DELETE' | 'ERROR';
export const activityService = {
/**
* 核心埋点方法 (Fire-and-Forget 模式)
* 异步执行,不阻塞主业务
*/
log(
tenantId: string,
tenantName: string, // 🆕 新增
userId: string,
userName: string,
module: ModuleCode,
feature: string,
action: ActionType,
info?: any
) {
// 异步执行,不要 await
prisma.simpleLog.create({
data: {
tenantId,
tenantName, // 🆕
userId,
userName,
module,
feature,
action,
info: typeof info === 'object' ? JSON.stringify(info) : String(info || ''),
}
}).catch(e => {
logger.warn('埋点写入失败(可忽略)', { error: e.message });
});
},
/**
* 获取今日核心大盘数据
*/
async getTodayOverview() {
const todayStart = new Date();
todayStart.setHours(0, 0, 0, 0);
const stats = await prisma.$queryRaw`
SELECT
COUNT(DISTINCT user_id) as dau,
COUNT(DISTINCT tenant_id) as dat,
COUNT(CASE WHEN action = 'EXPORT' THEN 1 END) as export_count
FROM admin_schema.simple_logs
WHERE created_at >= ${todayStart}
` as any[];
// 模块使用统计
const moduleStats = await prisma.$queryRaw`
SELECT module, COUNT(*) as count
FROM admin_schema.simple_logs
WHERE created_at >= ${todayStart}
GROUP BY module
` as any[];
const moduleMap: Record<string, number> = {};
moduleStats.forEach((m: any) => {
moduleMap[m.module] = Number(m.count);
});
return {
dau: Number(stats[0]?.dau || 0),
dat: Number(stats[0]?.dat || 0),
exportCount: Number(stats[0]?.export_count || 0),
moduleStats: moduleMap,
};
},
/**
* 获取实时流水账
*/
async getLiveFeed(limit = 100) {
return prisma.simpleLog.findMany({
orderBy: { createdAt: 'desc' },
take: limit,
select: {
id: true,
createdAt: true,
tenantName: true,
userName: true,
module: true,
feature: true,
action: true,
info: true,
}
});
},
/**
* 获取用户360画像
*/
async getUserOverview(userId: string) {
const [user, aiaStats, kbs, dcStats, rvwStats, logs] = await Promise.all([
// 基础信息
prisma.user.findUnique({
where: { id: userId },
include: { tenants: true }
}),
// AIA 资产 (会话数)
prisma.conversation.count({
where: { userId, deletedAt: null }
}),
// PKB 资产 (知识库数 + 文档数)
prisma.knowledgeBase.findMany({
where: { userId, deletedAt: null },
include: { _count: { select: { documents: true } } }
}),
// DC 资产 (任务数)
prisma.extractionTask.count({ where: { userId } }),
// RVW 资产 (审稿任务数) 🆕
prisma.reviewTask.groupBy({
by: ['status'],
where: { userId },
_count: true,
}),
// 最近行为 (从 SimpleLog 查最近 20 条)
prisma.simpleLog.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
take: 20,
select: {
createdAt: true,
module: true,
feature: true,
action: true,
info: true,
}
})
]);
const totalDocs = kbs.reduce((sum, kb) => sum + kb._count.documents, 0);
// 计算 RVW 统计
const rvwTotal = rvwStats.reduce((sum, s) => sum + s._count, 0);
const rvwCompleted = rvwStats.find(s => s.status === 'completed')?._count || 0;
return {
profile: user ? {
id: user.id,
name: user.name,
phone: user.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'),
tenantName: user.tenants?.name,
} : null,
assets: {
aia: { conversationCount: aiaStats },
pkb: { kbCount: kbs.length, docCount: totalDocs },
dc: { taskCount: dcStats },
rvw: { reviewTaskCount: rvwTotal, completedCount: rvwCompleted }, // 🆕
},
activities: logs,
};
}
};
5.2 StatsController(统计控制器)
文件路径:backend/src/modules/admin/controllers/statsController.ts
import type { FastifyRequest, FastifyReply } from 'fastify';
import { activityService } from '../../../common/services/activity.service.js';
/**
* 获取今日大盘
* GET /api/admin/stats/overview
*/
export async function getOverview(request: FastifyRequest, reply: FastifyReply) {
const data = await activityService.getTodayOverview();
return reply.send({ success: true, data });
}
/**
* 获取实时流水账
* GET /api/admin/stats/live-feed
*/
export async function getLiveFeed(
request: FastifyRequest<{ Querystring: { limit?: number } }>,
reply: FastifyReply
) {
const limit = request.query.limit || 100;
const data = await activityService.getLiveFeed(limit);
return reply.send({ success: true, data });
}
/**
* 获取用户360画像
* GET /api/admin/users/:id/overview
*/
export async function getUserOverview(
request: FastifyRequest<{ Params: { id: string } }>,
reply: FastifyReply
) {
const { id } = request.params;
const data = await activityService.getUserOverview(id);
return reply.send({ success: true, data });
}
6. 前端页面设计
6.1 Admin 首页改造
位置:frontend-v2/src/pages/admin/AdminDashboard.tsx
顶部卡片区域
┌─────────────────┬─────────────────┬─────────────────┐
│ 今日活跃医生 │ 今日活跃医院 │ 今日价值交付 │
│ 12 👨⚕️ │ 3 🏥 │ 5 🟢 │
│ (DAU) │ (DAT) │ (导出次数) │
└─────────────────┴─────────────────┴─────────────────┘
实时流水账区域
┌──────┬──────┬──────┬──────┬────────────┬──────┬────────────┐
│ 时间 │ 医院 │ 医生 │ 模块 │ 具体功能 │ 动作 │ 详情 │
├──────┼──────┼──────┼──────┼────────────┼──────┼────────────┤
│10:05 │ 协和 │张主任│ AIA │ 选题评价 │🔵USE │评分: 85分 │
│10:03 │ 协和 │张主任│ RVW │ 稿约规范 │🔵USE │审查开始 │
│09:55 │ 华西 │李医生│ DC │ Tool C │🟢EXP │导出 Excel │
└──────┴──────┴──────┴──────┴────────────┴──────┴────────────┘
6.2 用户详情页增强
位置:frontend-v2/src/modules/admin/pages/UserDetailPage.tsx
资产统计区域
┌─────────────┬─────────────┬─────────────┬─────────────┬─────────────┐
│ 💬 AIA对话 │ 📚 PKB知识库 │ 📄 上传文献 │ 🧹 DC清洗 │ 📝 RVW审稿 │
│ 158 次 │ 3 个 │ 450 篇 │ 12 次 │ 25 篇 │
└─────────────┴─────────────┴─────────────┴─────────────┴─────────────┘
行为时间轴
• 10:30 [AIA] 使用了 "选题评价" (生成结果: 85分)
• 10:15 [RVW] 完成了 "稿约规范性审查" (评分: 82分) 🆕
• 09:50 [DC] 导出了 Tool C 清洗结果 (Excel)
• 09:48 [SYSTEM] 登录系统
7. 权限控制
7.1 角色权限矩阵
| 功能 | SUPER_ADMIN | PROMPT_ENGINEER | HOSPITAL_ADMIN |
|---|---|---|---|
| 查看全局大盘 | ✅ | ✅(只读) | ❌ |
| 查看实时流水 | ✅ | ✅(只读) | ❌ |
| 查看用户画像 | ✅ | ❌ | 本租户用户 |
| 数据导出 | ✅ | ❌ | ❌ |
7.2 数据隔离规则
- SUPER_ADMIN:可查看全部租户数据
- HOSPITAL_ADMIN:只能查看本租户的用户活动
- 普通用户:无运营数据访问权限
8. 数据保留策略
8.1 清理规则
- 保留期限:180 天
- 清理方式:pg-boss 定时任务,每日 03:00 执行
- 清理脚本:
-- 清理180天前的日志
DELETE FROM admin_schema.simple_logs
WHERE created_at < NOW() - INTERVAL '180 days';
8.2 定时任务配置
文件路径:backend/src/common/jobs/cleanupWorker.ts
import { jobQueue } from './jobQueue.js';
import { prisma } from '../../config/database.js';
import { logger } from '../logging/index.js';
// 注册清理任务
export async function registerCleanupJobs() {
await jobQueue.schedule('cleanup-simple-logs', '0 3 * * *', async () => {
const result = await prisma.$executeRaw`
DELETE FROM admin_schema.simple_logs
WHERE created_at < NOW() - INTERVAL '180 days'
`;
logger.info('运营日志清理完成', { deletedCount: result });
});
}
9. 开发任务清单
Phase 1: 数据库(15分钟)
- 更新
prisma/schema.prisma,添加 SimpleLog 模型 - 执行
npx prisma db push同步数据库 - 验证表结构和索引
Phase 2: 后端服务(60分钟)
- 创建
common/services/activity.service.ts - 创建
modules/admin/controllers/statsController.ts - 创建
modules/admin/routes/statsRoutes.ts - 在
index.ts注册路由
Phase 3: 埋点集成(90分钟)
系统级
auth.controller.ts- 登录埋点
AIA 模块
conversationService.ts- 12个智能体埋点ProtocolOrchestrator.ts- Protocol Agent 埋点
PKB 模块
knowledgeBaseController.ts- 知识库创建埋点documentController.ts- 文档上传埋点ragController.ts- RAG问答埋点
DC 模块
toolBController.ts- Tool B 埋点toolCController.ts- Tool C 埋点
RVW 模块 🆕
reviewController.ts- 上传埋点reviewWorker.ts- 审查完成埋点
IIT 模块 🆕
chatService.ts- 对话查询埋点redcapAdapter.ts- 同步埋点
Phase 4: 前端页面(90分钟)
- 改造
AdminDashboard.tsx- 添加大盘卡片和流水账 - 改造
UserDetailPage.tsx- 添加资产统计和时间轴 - 创建
StatsCard.tsx组件 - 创建
LiveFeed.tsx组件 - 创建
ActivityTimeline.tsx组件
Phase 5: 数据清理(15分钟)
- 创建
cleanupWorker.ts - 注册定时任务
- 测试清理逻辑
10. 测试验证
10.1 单元测试
# 埋点服务测试
npm test -- --grep "ActivityService"
# API 测试
npm test -- --grep "Stats API"
10.2 端到端验证
- 登录系统,检查是否记录 LOGIN
- 使用 AIA 智能体,检查是否记录 USE
- 导出文件,检查是否记录 EXPORT
- 访问 Admin 首页,验证大盘数据
- 访问用户详情,验证 360 画像
📊 总结
| 项目 | V3.0 原方案 | V3.1 修订版 |
|---|---|---|
| 模块覆盖 | 4 个 | 8 个 |
| 字段设计 | 缺少 tenantName | ✅ 完整 |
| API 设计 | 缺失 | ✅ 4 个端点 |
| 数据保留 | 缺失 | ✅ 180 天 |
| 权限控制 | 缺失 | ✅ 角色矩阵 |
| 预计工时 | 3-4 小时 | 4-5 小时 |
下一步:按照任务清单执行开发,优先完成 Phase 1-2(基础设施),再逐步完成埋点集成和前端页面。