feat(iit): QC deep fix + V3.1 architecture plan + project member management

QC System Deep Fix:
- HardRuleEngine: add null tolerance + field availability pre-check (skipped status)
- SkillRunner: baseline data merge for follow-up events + field availability check
- QcReportService: record-level pass rate calculation + accurate LLM XML report
- iitBatchController: legacy log cleanup (eventId=null) + upsert RecordSummary
- seed-iit-qc-rules: null/empty string tolerance + applicableEvents config

V3.1 Architecture Design (docs only, no code changes):
- QC engine V3.1 plan: 5-level data structure (CDISC ODM) + D1-D7 dimensions
- Three-batch implementation strategy (A: foundation, B: bubbling, C: new engines)
- Architecture team review: 4 whitepapers reviewed + feedback doc + 4 critical suggestions
- CRA Agent strategy roadmap + CRA 4-tool explanation doc for clinical experts

Project Member Management:
- Cross-tenant member search and assignment (remove tenant restriction)
- IIT project detail page enhancement with tabbed layout (KB + members)
- IitProjectContext for business-side project selection
- System-KB route access control adjustment for project operators

Frontend:
- AdminLayout sidebar menu restructure
- IitLayout with project context provider
- IitMemberManagePage new component
- Business-side pages adapt to project context

Prisma:
- 2 new migrations (user-project RBAC + is_demo flag)
- Schema updates for project member management

Made-with: Cursor
This commit is contained in:
2026-03-01 15:27:05 +08:00
parent c3f7d54fdf
commit 0b29fe88b5
61 changed files with 6877 additions and 524 deletions

View File

@@ -67,79 +67,86 @@ export class IitBatchController {
});
}
// 3. 统计(按 record+event 组合
let passCount = 0;
let failCount = 0;
let warningCount = 0;
let uncertainCount = 0;
const uniqueRecords = new Set<string>();
// 3. V3.2: 按 record 级别聚合状态(每个 record 取最严重状态
const statusPriority: Record<string, number> = { 'FAIL': 3, 'WARNING': 2, 'UNCERTAIN': 1, 'PASS': 0 };
const recordWorstStatus = new Map<string, string>();
for (const result of results) {
uniqueRecords.add(result.recordId);
if (result.overallStatus === 'PASS') passCount++;
else if (result.overallStatus === 'FAIL') failCount++;
else if (result.overallStatus === 'WARNING') warningCount++;
else uncertainCount++;
// 更新录入汇总表(取最差状态)
const existingSummary = await prisma.iitRecordSummary.findUnique({
where: { projectId_recordId: { projectId, recordId: result.recordId } }
});
const statusPriority: Record<string, number> = { 'FAIL': 3, 'WARNING': 2, 'UNCERTAIN': 1, 'PASS': 0 };
const currentPriority = statusPriority[result.overallStatus] || 0;
const existingPriority = statusPriority[existingSummary?.latestQcStatus || 'PASS'] || 0;
// 只更新为更严重的状态
if (!existingSummary || currentPriority > existingPriority) {
await prisma.iitRecordSummary.upsert({
where: { projectId_recordId: { projectId, recordId: result.recordId } },
create: {
projectId,
recordId: result.recordId,
lastUpdatedAt: new Date(),
latestQcStatus: result.overallStatus,
latestQcAt: new Date(),
formStatus: {},
updateCount: 1
},
update: {
latestQcStatus: result.overallStatus,
latestQcAt: new Date()
}
});
const existing = recordWorstStatus.get(result.recordId);
const currentPrio = statusPriority[result.overallStatus] ?? 0;
const existingPrio = existing ? (statusPriority[existing] ?? 0) : -1;
if (currentPrio > existingPrio) {
recordWorstStatus.set(result.recordId, result.overallStatus);
}
}
// 4. 更新项目统计表
// V3.2: 用本次批量质控结果更新 record_summary覆盖旧状态
for (const [recordId, worstStatus] of recordWorstStatus) {
await prisma.iitRecordSummary.upsert({
where: { projectId_recordId: { projectId, recordId } },
create: {
projectId,
recordId,
lastUpdatedAt: new Date(),
latestQcStatus: worstStatus,
latestQcAt: new Date(),
formStatus: {},
updateCount: 1
},
update: {
latestQcStatus: worstStatus,
latestQcAt: new Date()
}
});
}
// V3.2: 清理该项目旧版本日志event_id 为 NULL 的遗留数据)
const deletedLegacy = await prisma.iitQcLog.deleteMany({
where: { projectId, eventId: null }
});
if (deletedLegacy.count > 0) {
logger.info('🧹 清理旧版质控日志', { projectId, deletedCount: deletedLegacy.count });
}
// V3.2: record 级别统计
let passCount = 0;
let failCount = 0;
let warningCount = 0;
for (const status of recordWorstStatus.values()) {
if (status === 'PASS') passCount++;
else if (status === 'FAIL') failCount++;
else warningCount++;
}
const totalRecords = recordWorstStatus.size;
// 4. 更新项目统计表record 级别)
await prisma.iitQcProjectStats.upsert({
where: { projectId },
create: {
projectId,
totalRecords: uniqueRecords.size,
totalRecords,
passedRecords: passCount,
failedRecords: failCount,
warningRecords: warningCount + uncertainCount
warningRecords: warningCount
},
update: {
totalRecords: uniqueRecords.size,
totalRecords,
passedRecords: passCount,
failedRecords: failCount,
warningRecords: warningCount + uncertainCount
warningRecords: warningCount
}
});
const durationMs = Date.now() - startTime;
logger.info('✅ 事件级全量质控完成', {
projectId,
uniqueRecords: uniqueRecords.size,
totalRecords,
totalEventCombinations: results.length,
passCount,
failCount,
warningCount,
uncertainCount,
durationMs
});
@@ -147,13 +154,14 @@ export class IitBatchController {
success: true,
message: '事件级全量质控完成',
stats: {
totalRecords: uniqueRecords.size,
totalRecords,
totalEventCombinations: results.length,
passed: passCount,
failed: failCount,
warnings: warningCount,
uncertain: uncertainCount,
passRate: `${((passCount / results.length) * 100).toFixed(1)}%`
passRate: totalRecords > 0
? `${((passCount / totalRecords) * 100).toFixed(1)}%`
: '0%'
},
durationMs
});