docs(platform): Add database documentation system and restructure deployment docs

Completed:
- Add 6 core database documents (docs/01-平台基础层/07-数据库/)
  Architecture overview, migration history, environment comparison,
  tech debt tracking, seed data management, PostgreSQL extensions
- Restructure deployment docs: archive 20 legacy files to _archive-2025/
- Create unified daily operations manual (01-日常更新操作手册.md)
- Add pending deployment change tracker (03-待部署变更清单.md)
- Update database development standard to v3.0 (three iron rules)
- Fix Prisma schema type drift: align @db.* annotations with actual DB
  IIT: UUID/Timestamptz(6), SSA: Timestamp(6)/VarChar(20/50/100)
- Add migration: 20260227_align_schema_with_db_types (idempotent ALTER)
- Add Cursor Rule for auto-reminding deployment change documentation
- Update system status guide v6.4 with deployment and DB doc references
- Add architecture consultation docs (Prisma guide, SAE deployment guide)

Technical details:
- Manual migration due to shadow DB limitation (TD-001 in tech debt)
- Deployment docs reduced from 20+ scattered files to 3 core documents
- Cursor Rule triggers on schema.prisma, package.json, Dockerfile changes

Made-with: Cursor
This commit is contained in:
2026-02-27 14:35:25 +08:00
parent 9b8490b4d0
commit 6124c7abc6
48 changed files with 3009 additions and 582 deletions

View File

@@ -28,6 +28,10 @@ RUN npx prisma generate
# 5. 复制本地已编译好的 dist 文件夹跳过TypeScript编译
COPY dist ./dist
# 5a. tsc 不会复制 .json 文件,手动补充 src 中的 JSON 配置到 dist
COPY src/modules/ssa/config/*.json ./dist/modules/ssa/config/
COPY src/modules/asl/fulltext-screening/prompts/*.json ./dist/modules/asl/fulltext-screening/prompts/
# 6. 复制配置文件agents.yaml等
COPY config ./config

View File

@@ -0,0 +1,27 @@
-- ================================================================
-- 对齐迁移:修正 prisma db push 造成的类型漂移
-- 使 Schema 的 @db.* 注解与数据库实际类型一致
-- 所有 ALTER 均为幂等操作DB 已是目标类型,仅记录逻辑变更)
-- 日期2026-02-27
-- ================================================================
-- 1. ssa_workflows: 精度/长度对齐
ALTER TABLE "ssa_schema"."ssa_workflows"
ALTER COLUMN "status" SET DATA TYPE VARCHAR(20),
ALTER COLUMN "created_at" SET DATA TYPE TIMESTAMP(6),
ALTER COLUMN "started_at" SET DATA TYPE TIMESTAMP(6),
ALTER COLUMN "completed_at" SET DATA TYPE TIMESTAMP(6);
-- 2. ssa_workflow_steps: 精度/长度对齐
ALTER TABLE "ssa_schema"."ssa_workflow_steps"
ALTER COLUMN "tool_code" SET DATA TYPE VARCHAR(50),
ALTER COLUMN "tool_name" SET DATA TYPE VARCHAR(100),
ALTER COLUMN "status" SET DATA TYPE VARCHAR(20),
ALTER COLUMN "started_at" SET DATA TYPE TIMESTAMP(6),
ALTER COLUMN "completed_at" SET DATA TYPE TIMESTAMP(6);
-- 3. 清理重复外键prisma db push 用 fk_ 前缀创建了额外 FK
ALTER TABLE "ssa_schema"."ssa_workflow_steps"
DROP CONSTRAINT IF EXISTS "fk_ssa_workflow_step_workflow";
ALTER TABLE "ssa_schema"."ssa_workflows"
DROP CONSTRAINT IF EXISTS "fk_ssa_workflow_session";

View File

@@ -0,0 +1,229 @@
-- ================================================================
-- 补丁迁移:覆盖 db push 创建的 6 张表 + 3 列
-- 所有语句使用 IF NOT EXISTS 保证幂等(多次执行不报错)
-- 日期2026-02-27
-- ================================================================
-- ============ iit_schema: 4 张新表 + 1 列 + 1 索引 ============
-- 1. IIT 字段元数据表REDCap 字段定义缓存)
CREATE TABLE IF NOT EXISTS "iit_schema"."field_metadata" (
"id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"field_name" TEXT NOT NULL,
"field_label" TEXT NOT NULL,
"field_type" TEXT NOT NULL,
"form_name" TEXT NOT NULL,
"section_header" TEXT,
"validation" TEXT,
"validation_min" TEXT,
"validation_max" TEXT,
"choices" TEXT,
"required" BOOLEAN NOT NULL DEFAULT false,
"branching" TEXT,
"alias" TEXT,
"rule_source" TEXT,
"synced_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "field_metadata_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "unique_iit_field_metadata"
ON "iit_schema"."field_metadata"("project_id", "field_name");
CREATE INDEX IF NOT EXISTS "idx_iit_field_metadata_project"
ON "iit_schema"."field_metadata"("project_id");
CREATE INDEX IF NOT EXISTS "idx_iit_field_metadata_form"
ON "iit_schema"."field_metadata"("project_id", "form_name");
-- 2. IIT 质控日志表
CREATE TABLE IF NOT EXISTS "iit_schema"."qc_logs" (
"id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"record_id" TEXT NOT NULL,
"event_id" TEXT,
"qc_type" TEXT NOT NULL,
"form_name" TEXT,
"status" TEXT NOT NULL,
"issues" JSONB NOT NULL DEFAULT '[]',
"rules_evaluated" INTEGER NOT NULL DEFAULT 0,
"rules_skipped" INTEGER NOT NULL DEFAULT 0,
"rules_passed" INTEGER NOT NULL DEFAULT 0,
"rules_failed" INTEGER NOT NULL DEFAULT 0,
"rule_version" TEXT NOT NULL,
"inclusion_passed" BOOLEAN,
"exclusion_passed" BOOLEAN,
"triggered_by" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "qc_logs_pkey" PRIMARY KEY ("id")
);
CREATE INDEX IF NOT EXISTS "idx_iit_qc_log_record_time"
ON "iit_schema"."qc_logs"("project_id", "record_id", "created_at");
CREATE INDEX IF NOT EXISTS "idx_iit_qc_log_status_time"
ON "iit_schema"."qc_logs"("project_id", "status", "created_at");
CREATE INDEX IF NOT EXISTS "idx_iit_qc_log_type_time"
ON "iit_schema"."qc_logs"("project_id", "qc_type", "created_at");
-- 3. IIT 受试者记录汇总表
CREATE TABLE IF NOT EXISTS "iit_schema"."record_summary" (
"id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"record_id" TEXT NOT NULL,
"enrolled_at" TIMESTAMP(3),
"enrolled_by" TEXT,
"last_updated_at" TIMESTAMP(3) NOT NULL,
"last_updated_by" TEXT,
"last_form_name" TEXT,
"form_status" JSONB NOT NULL DEFAULT '{}',
"total_forms" INTEGER NOT NULL DEFAULT 0,
"completed_forms" INTEGER NOT NULL DEFAULT 0,
"completion_rate" DOUBLE PRECISION NOT NULL DEFAULT 0,
"latest_qc_status" TEXT,
"latest_qc_at" TIMESTAMP(3),
"update_count" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "record_summary_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "unique_iit_record_summary"
ON "iit_schema"."record_summary"("project_id", "record_id");
CREATE INDEX IF NOT EXISTS "idx_iit_record_summary_enrolled"
ON "iit_schema"."record_summary"("project_id", "enrolled_at");
CREATE INDEX IF NOT EXISTS "idx_iit_record_summary_qc_status"
ON "iit_schema"."record_summary"("project_id", "latest_qc_status");
CREATE INDEX IF NOT EXISTS "idx_iit_record_summary_completion"
ON "iit_schema"."record_summary"("project_id", "completion_rate");
-- 4. IIT 项目级质控统计汇总表
CREATE TABLE IF NOT EXISTS "iit_schema"."qc_project_stats" (
"id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"total_records" INTEGER NOT NULL DEFAULT 0,
"passed_records" INTEGER NOT NULL DEFAULT 0,
"failed_records" INTEGER NOT NULL DEFAULT 0,
"warning_records" INTEGER NOT NULL DEFAULT 0,
"inclusion_met" INTEGER NOT NULL DEFAULT 0,
"exclusion_met" INTEGER NOT NULL DEFAULT 0,
"avg_completion_rate" DOUBLE PRECISION NOT NULL DEFAULT 0,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "qc_project_stats_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "qc_project_stats_project_id_key"
ON "iit_schema"."qc_project_stats"("project_id");
-- 5. IIT projects 表新增 knowledge_base_id 列
ALTER TABLE "iit_schema"."projects"
ADD COLUMN IF NOT EXISTS "knowledge_base_id" TEXT;
CREATE INDEX IF NOT EXISTS "idx_iit_project_kb"
ON "iit_schema"."projects"("knowledge_base_id");
-- ============ ssa_schema: 1 列 + 2 张新表 ============
-- 6a. SSA Session 新增 data_profile 列Phase 2A
ALTER TABLE "ssa_schema"."ssa_sessions"
ADD COLUMN IF NOT EXISTS "data_profile" JSONB;
-- 6. SSA 工作流表
CREATE TABLE IF NOT EXISTS "ssa_schema"."ssa_workflows" (
"id" TEXT NOT NULL DEFAULT gen_random_uuid()::text,
"session_id" TEXT NOT NULL,
"message_id" TEXT,
"status" VARCHAR NOT NULL DEFAULT 'pending',
"total_steps" INTEGER NOT NULL,
"completed_steps" INTEGER NOT NULL DEFAULT 0,
"workflow_plan" JSONB NOT NULL,
"reasoning" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT now(),
"started_at" TIMESTAMP(3),
"completed_at" TIMESTAMP(3),
CONSTRAINT "ssa_workflows_pkey" PRIMARY KEY ("id")
);
CREATE INDEX IF NOT EXISTS "idx_ssa_workflow_session"
ON "ssa_schema"."ssa_workflows"("session_id");
CREATE INDEX IF NOT EXISTS "idx_ssa_workflow_status"
ON "ssa_schema"."ssa_workflows"("status");
-- 外键ssa_workflows.session_id → ssa_sessions.id
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'ssa_workflows_session_id_fkey'
) THEN
ALTER TABLE "ssa_schema"."ssa_workflows"
ADD CONSTRAINT "ssa_workflows_session_id_fkey"
FOREIGN KEY ("session_id")
REFERENCES "ssa_schema"."ssa_sessions"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END $$;
-- 7. SSA 工作流步骤表
CREATE TABLE IF NOT EXISTS "ssa_schema"."ssa_workflow_steps" (
"id" TEXT NOT NULL DEFAULT gen_random_uuid()::text,
"workflow_id" TEXT NOT NULL,
"step_order" INTEGER NOT NULL,
"tool_code" VARCHAR NOT NULL,
"tool_name" VARCHAR NOT NULL,
"status" VARCHAR NOT NULL DEFAULT 'pending',
"input_params" JSONB,
"guardrail_checks" JSONB,
"output_result" JSONB,
"error_info" JSONB,
"execution_ms" INTEGER,
"started_at" TIMESTAMP(3),
"completed_at" TIMESTAMP(3),
CONSTRAINT "ssa_workflow_steps_pkey" PRIMARY KEY ("id")
);
CREATE INDEX IF NOT EXISTS "idx_ssa_workflow_step_workflow"
ON "ssa_schema"."ssa_workflow_steps"("workflow_id");
CREATE INDEX IF NOT EXISTS "idx_ssa_workflow_step_status"
ON "ssa_schema"."ssa_workflow_steps"("status");
-- 外键ssa_workflow_steps.workflow_id → ssa_workflows.id
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'ssa_workflow_steps_workflow_id_fkey'
) THEN
ALTER TABLE "ssa_schema"."ssa_workflow_steps"
ADD CONSTRAINT "ssa_workflow_steps_workflow_id_fkey"
FOREIGN KEY ("workflow_id")
REFERENCES "ssa_schema"."ssa_workflows"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END $$;
-- ============ rvw_schema: review_tasks 缺失列 ============
-- 8. review_tasks 后续 prisma db push 新增的 8 个列
ALTER TABLE "rvw_schema"."review_tasks"
ADD COLUMN IF NOT EXISTS "selected_agents" TEXT[] DEFAULT ARRAY['editorial','methodology'];
ALTER TABLE "rvw_schema"."review_tasks"
ADD COLUMN IF NOT EXISTS "editorial_score" DOUBLE PRECISION;
ALTER TABLE "rvw_schema"."review_tasks"
ADD COLUMN IF NOT EXISTS "methodology_score" DOUBLE PRECISION;
ALTER TABLE "rvw_schema"."review_tasks"
ADD COLUMN IF NOT EXISTS "methodology_status" TEXT;
ALTER TABLE "rvw_schema"."review_tasks"
ADD COLUMN IF NOT EXISTS "pico_extract" JSONB;
ALTER TABLE "rvw_schema"."review_tasks"
ADD COLUMN IF NOT EXISTS "context_data" JSONB;
ALTER TABLE "rvw_schema"."review_tasks"
ADD COLUMN IF NOT EXISTS "is_archived" BOOLEAN DEFAULT false;
ALTER TABLE "rvw_schema"."review_tasks"
ADD COLUMN IF NOT EXISTS "archived_at" TIMESTAMP(3);
CREATE INDEX IF NOT EXISTS "review_tasks_is_archived_idx"
ON "rvw_schema"."review_tasks"("is_archived");

View File

@@ -1430,7 +1430,8 @@ model IitQcProjectStats {
/// eQuery 表 - AI 自动生成的电子质疑,具有完整生命周期
model IitEquery {
id String @id @default(uuid())
// TODO: Tech Debt - DB 由 prisma db push 创建为 UUID 类型,未来大版本重构时统一为 String/TEXT
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
projectId String @map("project_id")
// 来源
@@ -1452,23 +1453,24 @@ model IitEquery {
// CRC 回复
assignedTo String? @map("assigned_to")
respondedAt DateTime? @map("responded_at")
// TODO: Tech Debt - DB 由 prisma db push 创建为 TIMESTAMPTZ(6),与 Prisma 默认的 TIMESTAMP(3) 不同
respondedAt DateTime? @map("responded_at") @db.Timestamptz(6)
responseText String? @map("response_text") @db.Text
responseData Json? @map("response_data") @db.JsonB
// AI 复核
reviewResult String? @map("review_result")
reviewNote String? @map("review_note") @db.Text
reviewedAt DateTime? @map("reviewed_at")
reviewedAt DateTime? @map("reviewed_at") @db.Timestamptz(6)
// 关闭
closedAt DateTime? @map("closed_at")
closedAt DateTime? @map("closed_at") @db.Timestamptz(6)
closedBy String? @map("closed_by")
resolution String? @db.Text
// 时间线
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
@@index([projectId], map: "idx_iit_equery_project")
@@index([projectId, status], map: "idx_iit_equery_project_status")
@@ -1480,7 +1482,8 @@ model IitEquery {
/// 重大事件归档表 - SAE、重大方案偏离等长期临床资产
model IitCriticalEvent {
id String @id @default(uuid())
// TODO: Tech Debt - DB 由 prisma db push 创建为 UUID 类型,未来大版本重构时统一为 String/TEXT
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
projectId String @map("project_id")
recordId String @map("record_id")
@@ -1491,7 +1494,8 @@ model IitCriticalEvent {
// 事件内容
title String
description String @db.Text
detectedAt DateTime @map("detected_at")
// TODO: Tech Debt - DB 由 prisma db push 创建为 TIMESTAMPTZ(6)
detectedAt DateTime @map("detected_at") @db.Timestamptz(6)
detectedBy String @default("ai") @map("detected_by")
// 来源追溯
@@ -1502,15 +1506,15 @@ model IitCriticalEvent {
// 处理状态
status String @default("open")
handledBy String? @map("handled_by")
handledAt DateTime? @map("handled_at")
handledAt DateTime? @map("handled_at") @db.Timestamptz(6)
handlingNote String? @map("handling_note") @db.Text
// 上报追踪
reportedToEc Boolean @default(false) @map("reported_to_ec")
reportedAt DateTime? @map("reported_at")
reportedAt DateTime? @map("reported_at") @db.Timestamptz(6)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
@@index([projectId], map: "idx_iit_critical_event_project")
@@index([projectId, eventType], map: "idx_iit_critical_event_type")
@@ -2513,17 +2517,18 @@ model SsaInterpretation {
/// SSA 多步骤流程
model SsaWorkflow {
id String @id @default(uuid())
// TODO: Tech Debt - DB 由 prisma db push 创建,类型与 Prisma 默认不同,已手动对齐
id String @id @default(dbgenerated("(gen_random_uuid())::text"))
sessionId String @map("session_id")
messageId String? @map("message_id") /// 关联的计划消息
status String @default("pending") /// pending | running | completed | partial | error
status String @default("pending") @db.VarChar(20) /// pending | running | completed | partial | error
totalSteps Int @map("total_steps")
completedSteps Int @default(0) @map("completed_steps")
workflowPlan Json @map("workflow_plan") /// 原始计划 JSON
reasoning String? @db.Text /// LLM 规划理由
createdAt DateTime @default(now()) @map("created_at")
startedAt DateTime? @map("started_at")
completedAt DateTime? @map("completed_at")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6)
startedAt DateTime? @map("started_at") @db.Timestamp(6)
completedAt DateTime? @map("completed_at") @db.Timestamp(6)
session SsaSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
steps SsaWorkflowStep[]
@@ -2536,19 +2541,20 @@ model SsaWorkflow {
/// SSA 流程步骤
model SsaWorkflowStep {
id String @id @default(uuid())
// TODO: Tech Debt - DB 由 prisma db push 创建,类型与 Prisma 默认不同,已手动对齐
id String @id @default(dbgenerated("(gen_random_uuid())::text"))
workflowId String @map("workflow_id")
stepOrder Int @map("step_order") /// 步骤顺序1, 2, 3...
toolCode String @map("tool_code")
toolName String @map("tool_name")
status String @default("pending") /// pending | running | success | warning | error | skipped
toolCode String @map("tool_code") @db.VarChar(50)
toolName String @map("tool_name") @db.VarChar(100)
status String @default("pending") @db.VarChar(20) /// pending | running | success | warning | error | skipped
inputParams Json? @map("input_params") /// 输入参数
guardrailChecks Json? @map("guardrail_checks") /// JIT 护栏检验结果
outputResult Json? @map("output_result") /// 执行结果
errorInfo Json? @map("error_info") /// 错误信息
executionMs Int? @map("execution_ms") /// 执行耗时(毫秒)
startedAt DateTime? @map("started_at")
completedAt DateTime? @map("completed_at")
startedAt DateTime? @map("started_at") @db.Timestamp(6)
completedAt DateTime? @map("completed_at") @db.Timestamp(6)
workflow SsaWorkflow @relation(fields: [workflowId], references: [id], onDelete: Cascade)