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:
@@ -0,0 +1,21 @@
|
||||
-- Phase 2: User-Project Association + RBAC
|
||||
-- 1. IitUserMapping: add userId (nullable FK to platform_schema.users)
|
||||
-- 2. IitProject: add tenantId (nullable FK to platform_schema.tenants, for Phase 3)
|
||||
-- 3. UserRole enum: add IIT_OPERATOR role
|
||||
|
||||
-- 1. Add user_id to user_mappings (nullable, gradual migration)
|
||||
ALTER TABLE "iit_schema"."user_mappings" ADD COLUMN IF NOT EXISTS "user_id" VARCHAR(255);
|
||||
ALTER TABLE "iit_schema"."user_mappings" ADD CONSTRAINT "user_mappings_user_id_fkey"
|
||||
FOREIGN KEY ("user_id") REFERENCES "platform_schema"."users"("id")
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
CREATE INDEX IF NOT EXISTS "idx_iit_user_mappings_user_id" ON "iit_schema"."user_mappings"("user_id");
|
||||
|
||||
-- 2. Add tenant_id to projects (nullable, Phase 3 will make it required)
|
||||
ALTER TABLE "iit_schema"."projects" ADD COLUMN IF NOT EXISTS "tenant_id" VARCHAR(255);
|
||||
ALTER TABLE "iit_schema"."projects" ADD CONSTRAINT "projects_tenant_id_fkey"
|
||||
FOREIGN KEY ("tenant_id") REFERENCES "platform_schema"."tenants"("id")
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
CREATE INDEX IF NOT EXISTS "idx_iit_projects_tenant_id" ON "iit_schema"."projects"("tenant_id");
|
||||
|
||||
-- 3. Add IIT_OPERATOR to UserRole enum
|
||||
ALTER TYPE "platform_schema"."UserRole" ADD VALUE IF NOT EXISTS 'IIT_OPERATOR' BEFORE 'USER';
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add is_demo flag to IIT projects (体验项目标记)
|
||||
ALTER TABLE iit_schema.projects ADD COLUMN IF NOT EXISTS is_demo BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -46,6 +46,7 @@ model User {
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
tenant_members tenant_members[]
|
||||
user_modules user_modules[]
|
||||
iitUserMappings IitUserMapping[]
|
||||
departments departments? @relation(fields: [department_id], references: [id])
|
||||
tenants tenants @relation(fields: [tenant_id], references: [id])
|
||||
|
||||
@@ -950,17 +951,21 @@ model IitProject {
|
||||
lastSyncAt DateTime? @map("last_sync_at")
|
||||
cronEnabled Boolean @default(false) @map("cron_enabled")
|
||||
cronExpression String? @map("cron_expression")
|
||||
isDemo Boolean @default(false) @map("is_demo")
|
||||
status String @default("active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
tenantId String? @map("tenant_id")
|
||||
auditLogs IitAuditLog[]
|
||||
pendingActions IitPendingAction[]
|
||||
taskRuns IitTaskRun[]
|
||||
userMappings IitUserMapping[]
|
||||
tenant tenants? @relation(fields: [tenantId], references: [id])
|
||||
|
||||
@@index([status, deletedAt])
|
||||
@@index([knowledgeBaseId], map: "idx_iit_project_kb")
|
||||
@@index([tenantId])
|
||||
@@map("projects")
|
||||
@@schema("iit_schema")
|
||||
}
|
||||
@@ -1022,6 +1027,7 @@ model IitUserMapping {
|
||||
id String @id @default(uuid())
|
||||
projectId String @map("project_id")
|
||||
systemUserId String @map("system_user_id")
|
||||
userId String? @map("user_id")
|
||||
redcapUsername String @map("redcap_username")
|
||||
wecomUserId String? @map("wecom_user_id")
|
||||
miniProgramOpenId String? @unique @map("mini_program_open_id")
|
||||
@@ -1030,11 +1036,13 @@ model IitUserMapping {
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
project IitProject @relation(fields: [projectId], references: [id])
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
|
||||
@@unique([projectId, systemUserId])
|
||||
@@unique([projectId, redcapUsername])
|
||||
@@index([wecomUserId])
|
||||
@@index([miniProgramOpenId])
|
||||
@@index([userId])
|
||||
@@map("user_mappings")
|
||||
@@schema("iit_schema")
|
||||
}
|
||||
@@ -1763,6 +1771,7 @@ model tenants {
|
||||
tenant_quotas tenant_quotas[]
|
||||
users User[]
|
||||
user_modules user_modules[]
|
||||
iitProjects IitProject[]
|
||||
|
||||
@@index([code], map: "idx_tenants_code")
|
||||
@@index([status], map: "idx_tenants_status")
|
||||
@@ -1819,6 +1828,7 @@ enum UserRole {
|
||||
HOSPITAL_ADMIN
|
||||
PHARMA_ADMIN
|
||||
DEPARTMENT_ADMIN
|
||||
IIT_OPERATOR
|
||||
USER
|
||||
|
||||
@@schema("platform_schema")
|
||||
|
||||
@@ -27,60 +27,95 @@ const INCLUSION_RULES = [
|
||||
name: '年龄范围检查',
|
||||
field: 'age',
|
||||
logic: {
|
||||
and: [
|
||||
{ '>=': [{ var: 'age' }, 16] },
|
||||
{ '<=': [{ var: 'age' }, 35] }
|
||||
or: [
|
||||
{ '==': [{ var: 'age' }, null] },
|
||||
{ '==': [{ var: 'age' }, ''] },
|
||||
{
|
||||
and: [
|
||||
{ '>=': [{ var: 'age' }, 16] },
|
||||
{ '<=': [{ var: 'age' }, 35] }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
message: '年龄不在 16-35 岁范围内',
|
||||
severity: 'error',
|
||||
category: 'inclusion'
|
||||
category: 'inclusion',
|
||||
applicableEvents: [] as string[],
|
||||
},
|
||||
{
|
||||
id: 'inc_002',
|
||||
name: '出生日期范围检查',
|
||||
field: 'birth_date',
|
||||
logic: {
|
||||
and: [
|
||||
{ '>=': [{ var: 'birth_date' }, '1989-01-01'] },
|
||||
{ '<=': [{ var: 'birth_date' }, '2008-01-01'] }
|
||||
or: [
|
||||
{ '==': [{ var: 'birth_date' }, null] },
|
||||
{ '==': [{ var: 'birth_date' }, ''] },
|
||||
{
|
||||
and: [
|
||||
{ '>=': [{ var: 'birth_date' }, '1989-01-01'] },
|
||||
{ '<=': [{ var: 'birth_date' }, '2008-01-01'] }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
message: '出生日期不在 1989-01-01 至 2008-01-01 范围内',
|
||||
severity: 'error',
|
||||
category: 'inclusion'
|
||||
category: 'inclusion',
|
||||
applicableEvents: [] as string[],
|
||||
},
|
||||
{
|
||||
id: 'inc_003',
|
||||
name: '月经周期规律性检查',
|
||||
field: 'menstrual_cycle',
|
||||
logic: {
|
||||
and: [
|
||||
{ '>=': [{ var: 'menstrual_cycle' }, 21] },
|
||||
{ '<=': [{ var: 'menstrual_cycle' }, 35] }
|
||||
or: [
|
||||
{ '==': [{ var: 'menstrual_cycle' }, null] },
|
||||
{ '==': [{ var: 'menstrual_cycle' }, ''] },
|
||||
{
|
||||
and: [
|
||||
{ '>=': [{ var: 'menstrual_cycle' }, 21] },
|
||||
{ '<=': [{ var: 'menstrual_cycle' }, 35] }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
message: '月经周期不在 21-35 天范围内(28±7天)',
|
||||
severity: 'error',
|
||||
category: 'inclusion'
|
||||
category: 'inclusion',
|
||||
applicableEvents: [] as string[],
|
||||
},
|
||||
{
|
||||
id: 'inc_004',
|
||||
name: 'VAS 评分检查',
|
||||
field: 'vas_score',
|
||||
logic: { '>=': [{ var: 'vas_score' }, 4] },
|
||||
logic: {
|
||||
or: [
|
||||
{ '==': [{ var: 'vas_score' }, null] },
|
||||
{ '==': [{ var: 'vas_score' }, ''] },
|
||||
{ '>=': [{ var: 'vas_score' }, 4] }
|
||||
]
|
||||
},
|
||||
message: 'VAS 疼痛评分 < 4 分,不符合入组条件',
|
||||
severity: 'error',
|
||||
category: 'inclusion'
|
||||
category: 'inclusion',
|
||||
applicableEvents: [] as string[],
|
||||
},
|
||||
{
|
||||
id: 'inc_005',
|
||||
name: '知情同意书签署检查',
|
||||
field: 'informed_consent',
|
||||
logic: { '==': [{ var: 'informed_consent' }, 1] },
|
||||
logic: {
|
||||
or: [
|
||||
{ '==': [{ var: 'informed_consent' }, null] },
|
||||
{ '==': [{ var: 'informed_consent' }, ''] },
|
||||
{ '==': [{ var: 'informed_consent' }, 1] }
|
||||
]
|
||||
},
|
||||
message: '未签署知情同意书',
|
||||
severity: 'error',
|
||||
category: 'inclusion'
|
||||
category: 'inclusion',
|
||||
applicableEvents: [] as string[],
|
||||
}
|
||||
];
|
||||
|
||||
@@ -92,37 +127,65 @@ const EXCLUSION_RULES = [
|
||||
id: 'exc_001',
|
||||
name: '继发性痛经排除',
|
||||
field: 'secondary_dysmenorrhea',
|
||||
logic: { '!=': [{ var: 'secondary_dysmenorrhea' }, 1] },
|
||||
logic: {
|
||||
or: [
|
||||
{ '==': [{ var: 'secondary_dysmenorrhea' }, null] },
|
||||
{ '==': [{ var: 'secondary_dysmenorrhea' }, ''] },
|
||||
{ '!=': [{ var: 'secondary_dysmenorrhea' }, 1] }
|
||||
]
|
||||
},
|
||||
message: '存在继发性痛经(盆腔炎、子宫内膜异位症、子宫腺肌病等)',
|
||||
severity: 'error',
|
||||
category: 'exclusion'
|
||||
category: 'exclusion',
|
||||
applicableEvents: [] as string[],
|
||||
},
|
||||
{
|
||||
id: 'exc_002',
|
||||
name: '妊娠哺乳期排除',
|
||||
field: 'pregnancy_lactation',
|
||||
logic: { '!=': [{ var: 'pregnancy_lactation' }, 1] },
|
||||
logic: {
|
||||
or: [
|
||||
{ '==': [{ var: 'pregnancy_lactation' }, null] },
|
||||
{ '==': [{ var: 'pregnancy_lactation' }, ''] },
|
||||
{ '!=': [{ var: 'pregnancy_lactation' }, 1] }
|
||||
]
|
||||
},
|
||||
message: '妊娠或哺乳期妇女,不符合入组条件',
|
||||
severity: 'error',
|
||||
category: 'exclusion'
|
||||
category: 'exclusion',
|
||||
applicableEvents: [] as string[],
|
||||
},
|
||||
{
|
||||
id: 'exc_003',
|
||||
name: '严重疾病排除',
|
||||
field: 'severe_disease',
|
||||
logic: { '!=': [{ var: 'severe_disease' }, 1] },
|
||||
logic: {
|
||||
or: [
|
||||
{ '==': [{ var: 'severe_disease' }, null] },
|
||||
{ '==': [{ var: 'severe_disease' }, ''] },
|
||||
{ '!=': [{ var: 'severe_disease' }, 1] }
|
||||
]
|
||||
},
|
||||
message: '合并有心脑血管、肝、肾、造血系统等严重疾病或精神病',
|
||||
severity: 'error',
|
||||
category: 'exclusion'
|
||||
category: 'exclusion',
|
||||
applicableEvents: [] as string[],
|
||||
},
|
||||
{
|
||||
id: 'exc_004',
|
||||
name: '月经周期不规律排除',
|
||||
field: 'irregular_menstruation',
|
||||
logic: { '!=': [{ var: 'irregular_menstruation' }, 1] },
|
||||
logic: {
|
||||
or: [
|
||||
{ '==': [{ var: 'irregular_menstruation' }, null] },
|
||||
{ '==': [{ var: 'irregular_menstruation' }, ''] },
|
||||
{ '!=': [{ var: 'irregular_menstruation' }, 1] }
|
||||
]
|
||||
},
|
||||
message: '月经周期不规律或间歇性痛经发作',
|
||||
severity: 'error',
|
||||
category: 'exclusion'
|
||||
category: 'exclusion',
|
||||
applicableEvents: [] as string[],
|
||||
}
|
||||
];
|
||||
|
||||
@@ -242,7 +305,7 @@ async function main() {
|
||||
|
||||
// 1. 先获取项目 ID
|
||||
const project = await prisma.iitProject.findFirst({
|
||||
where: { name: 'test0102' }
|
||||
where: { name: { in: ['test0207', 'test0102'] } }
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
|
||||
Reference in New Issue
Block a user