P0-1: Variable list sync from REDCap metadata P0-2: QC rule configuration with JSON Logic + AI suggestion P0-3: Scheduled QC + report generation + eQuery closed loop P0-4: Unified dashboard + AI stream timeline + critical events Backend: - Add IitEquery, IitCriticalEvent Prisma models + migration - Add cronEnabled/cronExpression to IitProject - Implement eQuery service/controller/routes (CRUD + respond/review/close) - Implement DailyQcOrchestrator (report -> eQuery -> critical events -> notify) - Add AI rule suggestion service - Register daily QC cron worker and eQuery auto-review worker - Extend QC cockpit with timeline, trend, critical events APIs - Fix timeline issues field compat (object vs array format) Frontend: - Create IIT business module with 6 pages (Dashboard, AI Stream, eQuery, Reports, Variable List + project config pages) - Migrate IIT config from admin panel to business module - Implement health score, risk heatmap, trend chart, critical event alerts - Register IIT module in App router and top navigation Testing: - Add E2E API test script covering 7 modules (46 assertions, all passing) Tested: E2E API tests 46/46 passed, backend and frontend verified Made-with: Cursor
255 lines
8.7 KiB
TypeScript
255 lines
8.7 KiB
TypeScript
/**
|
||
* IIT 质控驾驶舱路由
|
||
*
|
||
* API:
|
||
* - GET /api/v1/admin/iit-projects/:projectId/qc-cockpit 获取驾驶舱数据
|
||
* - GET /api/v1/admin/iit-projects/:projectId/qc-cockpit/records/:recordId 获取记录详情
|
||
* - GET /api/v1/admin/iit-projects/:projectId/qc-cockpit/report 获取质控报告
|
||
* - POST /api/v1/admin/iit-projects/:projectId/qc-cockpit/report/refresh 刷新质控报告
|
||
*/
|
||
|
||
import { FastifyInstance } from 'fastify';
|
||
import { iitQcCockpitController } from './iitQcCockpitController.js';
|
||
|
||
export async function iitQcCockpitRoutes(fastify: FastifyInstance) {
|
||
// 获取质控驾驶舱数据
|
||
fastify.get('/:projectId/qc-cockpit', {
|
||
schema: {
|
||
description: '获取质控驾驶舱数据(统计 + 热力图)',
|
||
tags: ['IIT Admin - 质控驾驶舱'],
|
||
params: {
|
||
type: 'object',
|
||
properties: {
|
||
projectId: { type: 'string', description: 'IIT 项目 ID' },
|
||
},
|
||
required: ['projectId'],
|
||
},
|
||
response: {
|
||
200: {
|
||
type: 'object',
|
||
properties: {
|
||
success: { type: 'boolean' },
|
||
data: {
|
||
type: 'object',
|
||
properties: {
|
||
stats: {
|
||
type: 'object',
|
||
properties: {
|
||
qualityScore: { type: 'number' },
|
||
totalRecords: { type: 'number' },
|
||
passedRecords: { type: 'number' },
|
||
failedRecords: { type: 'number' },
|
||
warningRecords: { type: 'number' },
|
||
pendingRecords: { type: 'number' },
|
||
criticalCount: { type: 'number' },
|
||
queryCount: { type: 'number' },
|
||
deviationCount: { type: 'number' },
|
||
passRate: { type: 'number' },
|
||
topIssues: {
|
||
type: 'array',
|
||
items: {
|
||
type: 'object',
|
||
properties: {
|
||
issue: { type: 'string' },
|
||
count: { type: 'number' },
|
||
severity: { type: 'string' },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
heatmap: {
|
||
type: 'object',
|
||
properties: {
|
||
columns: { type: 'array', items: { type: 'string' } },
|
||
rows: {
|
||
type: 'array',
|
||
items: {
|
||
type: 'object',
|
||
properties: {
|
||
recordId: { type: 'string' },
|
||
status: { type: 'string' },
|
||
cells: {
|
||
type: 'array',
|
||
items: {
|
||
type: 'object',
|
||
properties: {
|
||
formName: { type: 'string' },
|
||
status: { type: 'string' },
|
||
issueCount: { type: 'number' },
|
||
recordId: { type: 'string' },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
lastUpdatedAt: { type: 'string' },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}, iitQcCockpitController.getCockpitData.bind(iitQcCockpitController));
|
||
|
||
// 获取记录质控详情
|
||
fastify.get('/:projectId/qc-cockpit/records/:recordId', {
|
||
schema: {
|
||
description: '获取单条记录的质控详情(含 LLM Trace)',
|
||
tags: ['IIT Admin - 质控驾驶舱'],
|
||
params: {
|
||
type: 'object',
|
||
properties: {
|
||
projectId: { type: 'string', description: 'IIT 项目 ID' },
|
||
recordId: { type: 'string', description: '记录 ID' },
|
||
},
|
||
required: ['projectId', 'recordId'],
|
||
},
|
||
querystring: {
|
||
type: 'object',
|
||
properties: {
|
||
formName: { type: 'string', description: '表单名称' },
|
||
},
|
||
},
|
||
response: {
|
||
200: {
|
||
type: 'object',
|
||
properties: {
|
||
success: { type: 'boolean' },
|
||
data: {
|
||
type: 'object',
|
||
properties: {
|
||
recordId: { type: 'string' },
|
||
formName: { type: 'string' },
|
||
status: { type: 'string' },
|
||
data: { type: 'object', additionalProperties: true },
|
||
fieldMetadata: { type: 'object', additionalProperties: true },
|
||
issues: {
|
||
type: 'array',
|
||
items: {
|
||
type: 'object',
|
||
properties: {
|
||
field: { type: 'string' },
|
||
ruleName: { type: 'string' },
|
||
message: { type: 'string' },
|
||
severity: { type: 'string' },
|
||
actualValue: {},
|
||
expectedValue: { type: 'string' },
|
||
confidence: { type: 'string' },
|
||
},
|
||
},
|
||
},
|
||
llmTrace: {
|
||
type: 'object',
|
||
properties: {
|
||
promptSent: { type: 'string' },
|
||
responseReceived: { type: 'string' },
|
||
model: { type: 'string' },
|
||
latencyMs: { type: 'number' },
|
||
},
|
||
},
|
||
entryTime: { type: 'string' },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}, iitQcCockpitController.getRecordDetail.bind(iitQcCockpitController));
|
||
|
||
// 获取质控报告
|
||
fastify.get('/:projectId/qc-cockpit/report', {
|
||
schema: {
|
||
description: '获取质控报告(支持 JSON 和 XML 格式)',
|
||
tags: ['IIT Admin - 质控驾驶舱'],
|
||
params: {
|
||
type: 'object',
|
||
properties: {
|
||
projectId: { type: 'string', description: 'IIT 项目 ID' },
|
||
},
|
||
required: ['projectId'],
|
||
},
|
||
querystring: {
|
||
type: 'object',
|
||
properties: {
|
||
format: {
|
||
type: 'string',
|
||
enum: ['json', 'xml'],
|
||
default: 'json',
|
||
description: '响应格式:json(默认)或 xml(LLM 友好格式)',
|
||
},
|
||
},
|
||
},
|
||
response: {
|
||
200: {
|
||
type: 'object',
|
||
properties: {
|
||
success: { type: 'boolean' },
|
||
data: {
|
||
type: 'object',
|
||
properties: {
|
||
projectId: { type: 'string' },
|
||
reportType: { type: 'string' },
|
||
generatedAt: { type: 'string' },
|
||
expiresAt: { type: 'string' },
|
||
summary: {
|
||
type: 'object',
|
||
properties: {
|
||
totalRecords: { type: 'number' },
|
||
completedRecords: { type: 'number' },
|
||
criticalIssues: { type: 'number' },
|
||
warningIssues: { type: 'number' },
|
||
pendingQueries: { type: 'number' },
|
||
passRate: { type: 'number' },
|
||
lastQcTime: { type: 'string' },
|
||
},
|
||
},
|
||
criticalIssues: { type: 'array' },
|
||
warningIssues: { type: 'array' },
|
||
formStats: { type: 'array' },
|
||
llmFriendlyXml: { type: 'string' },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}, iitQcCockpitController.getReport.bind(iitQcCockpitController));
|
||
|
||
// 刷新质控报告
|
||
fastify.post('/:projectId/qc-cockpit/report/refresh', {
|
||
schema: {
|
||
description: '强制刷新质控报告(忽略缓存)',
|
||
tags: ['IIT Admin - 质控驾驶舱'],
|
||
params: {
|
||
type: 'object',
|
||
properties: {
|
||
projectId: { type: 'string', description: 'IIT 项目 ID' },
|
||
},
|
||
required: ['projectId'],
|
||
},
|
||
response: {
|
||
200: {
|
||
type: 'object',
|
||
properties: {
|
||
success: { type: 'boolean' },
|
||
data: { type: 'object' },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}, iitQcCockpitController.refreshReport.bind(iitQcCockpitController));
|
||
|
||
// AI 工作时间线
|
||
fastify.get('/:projectId/qc-cockpit/timeline', iitQcCockpitController.getTimeline.bind(iitQcCockpitController));
|
||
|
||
// 重大事件列表
|
||
fastify.get('/:projectId/qc-cockpit/critical-events', iitQcCockpitController.getCriticalEvents.bind(iitQcCockpitController));
|
||
|
||
// 质控趋势(近N天通过率折线)
|
||
fastify.get('/:projectId/qc-cockpit/trend', iitQcCockpitController.getTrend.bind(iitQcCockpitController));
|
||
}
|