Summary: - Refactor timeline API to read from qc_field_status (SSOT) instead of qc_logs - Add field-issues paginated API with severity/dimension/recordId filters - Add LEFT JOIN field_metadata + qc_event_status for Chinese display names - Implement per-project ChatOrchestrator cache and SessionMemory isolation - Redesign admin IIT config tabs (REDCap -> Fields -> KB -> Rules -> Members) - Add AI-powered QC rule generation (D3 programmatic + D1/D5/D6 LLM-based) - Add clickable warning/critical detail Modal in ReportsPage - Auto-dispatch eQuery after batch QC via DailyQcOrchestrator - Update module status documentation to v3.2 Backend changes: - iitQcCockpitController: rewrite getTimeline from qc_field_status, add getFieldIssues - iitQcCockpitRoutes: add field-issues route - ChatOrchestrator: per-projectId cached instances - SessionMemory: keyed by userId::projectId - WechatCallbackController: resolve projectId from iitUserMapping - iitRuleSuggestionService: dimension-based suggest + generateD3Rules - iitBatchController: call DailyQcOrchestrator after batch QC Frontend changes: - AiStreamPage: adapt to new timeline structure with dimension tags - ReportsPage: clickable stats cards with issue detail Modal - IitProjectDetailPage: reorder tabs, add AI rule generation UI - iitProjectApi: add TimelineIssue, FieldIssueItem types and APIs Status: TypeScript compilation verified, no new lint errors Made-with: Cursor
366 lines
13 KiB
TypeScript
366 lines
13 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));
|
||
|
||
// V3.1: D1-D7 维度统计
|
||
fastify.get('/:projectId/qc-cockpit/dimensions', {
|
||
schema: {
|
||
description: 'D1-D7 各维度详细统计',
|
||
tags: ['IIT Admin - 质控驾驶舱'],
|
||
params: { type: 'object', properties: { projectId: { type: 'string' } }, required: ['projectId'] },
|
||
},
|
||
}, iitQcCockpitController.getDimensions.bind(iitQcCockpitController));
|
||
|
||
// V3.1: 按受试者缺失率
|
||
fastify.get('/:projectId/qc-cockpit/completeness', {
|
||
schema: {
|
||
description: '按受试者返回缺失率',
|
||
tags: ['IIT Admin - 质控驾驶舱'],
|
||
params: { type: 'object', properties: { projectId: { type: 'string' } }, required: ['projectId'] },
|
||
},
|
||
}, iitQcCockpitController.getCompleteness.bind(iitQcCockpitController));
|
||
|
||
// V3.1: 字段级质控结果(分页)
|
||
fastify.get('/:projectId/qc-cockpit/field-status', {
|
||
schema: {
|
||
description: '字段级质控结果(分页,支持 recordId/eventId/status 筛选)',
|
||
tags: ['IIT Admin - 质控驾驶舱'],
|
||
params: { type: 'object', properties: { projectId: { type: 'string' } }, required: ['projectId'] },
|
||
querystring: {
|
||
type: 'object',
|
||
properties: {
|
||
recordId: { type: 'string' },
|
||
eventId: { type: 'string' },
|
||
status: { type: 'string', enum: ['PASS', 'FAIL', 'WARNING'] },
|
||
page: { type: 'string', default: '1' },
|
||
pageSize: { type: 'string', default: '50' },
|
||
},
|
||
},
|
||
},
|
||
}, iitQcCockpitController.getFieldStatus.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));
|
||
|
||
// V3.1: D6 方案偏离列表
|
||
fastify.get('/:projectId/qc-cockpit/deviations', iitQcCockpitController.getDeviations.bind(iitQcCockpitController));
|
||
|
||
// 字段级问题分页查询(支持按维度/严重程度筛选)
|
||
fastify.get('/:projectId/qc-cockpit/field-issues', {
|
||
schema: {
|
||
description: '从 qc_field_status 分页查询所有问题字段',
|
||
tags: ['IIT Admin - QC 驾驶舱'],
|
||
params: { type: 'object', properties: { projectId: { type: 'string' } }, required: ['projectId'] },
|
||
querystring: {
|
||
type: 'object',
|
||
properties: {
|
||
page: { type: 'string' },
|
||
pageSize: { type: 'string' },
|
||
severity: { type: 'string' },
|
||
dimension: { type: 'string' },
|
||
recordId: { type: 'string' },
|
||
},
|
||
},
|
||
},
|
||
}, iitQcCockpitController.getFieldIssues.bind(iitQcCockpitController));
|
||
|
||
// ============================================================
|
||
// GCP 业务报表路由
|
||
// ============================================================
|
||
|
||
fastify.get('/:projectId/qc-cockpit/report/eligibility', {
|
||
schema: {
|
||
description: 'D1 筛选入选表 — 入排合规性评估',
|
||
tags: ['IIT Admin - GCP 报表'],
|
||
params: { type: 'object', properties: { projectId: { type: 'string' } }, required: ['projectId'] },
|
||
},
|
||
}, iitQcCockpitController.getEligibilityReport.bind(iitQcCockpitController));
|
||
|
||
fastify.get('/:projectId/qc-cockpit/report/completeness', {
|
||
schema: {
|
||
description: 'D2 数据完整性总览 — L1 项目 + L2 受试者 + L3 事件级统计',
|
||
tags: ['IIT Admin - GCP 报表'],
|
||
params: { type: 'object', properties: { projectId: { type: 'string' } }, required: ['projectId'] },
|
||
},
|
||
}, iitQcCockpitController.getCompletenessReport.bind(iitQcCockpitController));
|
||
|
||
fastify.get('/:projectId/qc-cockpit/report/completeness/fields', {
|
||
schema: {
|
||
description: 'D2 字段级懒加载 — 按 recordId + eventId 返回缺失字段清单',
|
||
tags: ['IIT Admin - GCP 报表'],
|
||
params: { type: 'object', properties: { projectId: { type: 'string' } }, required: ['projectId'] },
|
||
querystring: {
|
||
type: 'object',
|
||
properties: {
|
||
recordId: { type: 'string' },
|
||
eventId: { type: 'string' },
|
||
},
|
||
required: ['recordId', 'eventId'],
|
||
},
|
||
},
|
||
}, iitQcCockpitController.getCompletenessFields.bind(iitQcCockpitController));
|
||
|
||
fastify.get('/:projectId/qc-cockpit/report/equery-log', {
|
||
schema: {
|
||
description: 'D3/D4 eQuery 全生命周期跟踪 — 统计 + 分组 + 全量明细',
|
||
tags: ['IIT Admin - GCP 报表'],
|
||
params: { type: 'object', properties: { projectId: { type: 'string' } }, required: ['projectId'] },
|
||
},
|
||
}, iitQcCockpitController.getEqueryLogReport.bind(iitQcCockpitController));
|
||
|
||
fastify.get('/:projectId/qc-cockpit/report/deviations', {
|
||
schema: {
|
||
description: 'D6 方案偏离报表 — 结构化超窗数据 + 汇总统计',
|
||
tags: ['IIT Admin - GCP 报表'],
|
||
params: { type: 'object', properties: { projectId: { type: 'string' } }, required: ['projectId'] },
|
||
},
|
||
}, iitQcCockpitController.getDeviationReport.bind(iitQcCockpitController));
|
||
}
|