feat(server): 工程骨架后端——上传 → 解析 → 清单最小链路
- core:配置(prod 拒绝默认密钥 / dev 短信 / 假模型)、租户事务 + 行级安全策略、 problem+json 错误码、信封加密与签名下载、对象存储、事务内入队、Valkey stream 事件流、权限点 - identity:手机号 HMAC 存储、注册、登录(挤下线需确认,API3)、服务端会话 - billing:额度流水只追加,预扣 / 结算 / 退回,一笔预扣只有一个结局 - document:分片上传、按文件头识别格式、Gotenberg 转换、文字层与稳定句子编号 - checklist:标注规则识别、模型抽取、三道校验、标注类条款规则兜底、重跑对齐 - ai:提示词注册表(extract.substantive v1 来自 S4 v2)、假模型、OpenAI 兼容供应商、ai_call 流水 - 迁移:data-model 基线、套餐、Procrastinate、会话状态函数 - 测试 30 个:租户隔离、额度、失败退回、对象加密、契约一致性 - Compose(含 migrate)、镜像、CI Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
119
server/migrations/sql/0001_baseline/06_check.sql
Normal file
119
server/migrations/sql/0001_baseline/06_check.sql
Normal file
@@ -0,0 +1,119 @@
|
||||
-- 6. check:检查批次、问题、人工核对(FR-M3-02~08)
|
||||
-- 来源:docs/architecture/schema-draft.sql(2026-11-03 验证过的草案)
|
||||
CREATE TABLE check_run (
|
||||
id uuid PRIMARY KEY,
|
||||
tenant_id uuid NOT NULL,
|
||||
project_id uuid NOT NULL,
|
||||
run_no int NOT NULL, -- 项目内第几次检查
|
||||
checklist_version_id uuid NOT NULL,
|
||||
checklist_confirmed boolean NOT NULL, -- false 时报告和界面提示「清单未确认」(G7)
|
||||
rule_set_version text NOT NULL,
|
||||
legal_regime text NOT NULL CHECK (legal_regime IN ('gov_procurement', 'tendering', 'unknown')),
|
||||
lot_ids uuid[] NOT NULL DEFAULT '{}', -- 本次检查的包
|
||||
previous_run_id uuid, -- 状态沿用的来源
|
||||
status text NOT NULL DEFAULT 'queued'
|
||||
CHECK (status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled')),
|
||||
stage text CHECK (stage IN ('prepare_bid', 'match', 'rules', 'verify', 'carry_over', 'summarize')),
|
||||
checkpoints jsonb NOT NULL DEFAULT '{}',
|
||||
progress int NOT NULL DEFAULT 0 CHECK (progress BETWEEN 0 AND 100),
|
||||
cancel_requested_at timestamptz,
|
||||
error_class text CHECK (error_class IN ('infra', 'input', 'content', 'cancelled')),
|
||||
error_code text,
|
||||
error_detail text,
|
||||
billing text NOT NULL CHECK (billing IN ('charged', 'waived_recheck', 'free_plan')),
|
||||
quota_hold_id uuid,
|
||||
prompt_versions jsonb,
|
||||
coverage jsonb, -- 查了什么、没查什么(报告首页)
|
||||
counts jsonb, -- 各级别问题数,结束时写入
|
||||
queue_job_id bigint,
|
||||
created_by uuid NOT NULL REFERENCES user_account (id),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
started_at timestamptz,
|
||||
finished_at timestamptz,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (tenant_id, id),
|
||||
UNIQUE (tenant_id, project_id, run_no),
|
||||
CHECK (billing <> 'charged' OR quota_hold_id IS NOT NULL),
|
||||
FOREIGN KEY (tenant_id, project_id) REFERENCES project (tenant_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tenant_id, checklist_version_id) REFERENCES checklist_version (tenant_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tenant_id, previous_run_id) REFERENCES check_run (tenant_id, id) ON DELETE SET NULL (previous_run_id),
|
||||
FOREIGN KEY (tenant_id, quota_hold_id) REFERENCES quota_entry (tenant_id, id)
|
||||
);
|
||||
CREATE UNIQUE INDEX ON check_run (tenant_id, project_id) WHERE status IN ('queued', 'running');
|
||||
ALTER TABLE project
|
||||
ADD FOREIGN KEY (tenant_id, latest_check_run_id) REFERENCES check_run (tenant_id, id)
|
||||
ON DELETE SET NULL (latest_check_run_id);
|
||||
|
||||
CREATE TABLE check_run_document ( -- 本次检查用了哪些投标文件版本
|
||||
tenant_id uuid NOT NULL,
|
||||
check_run_id uuid NOT NULL,
|
||||
document_id uuid NOT NULL,
|
||||
PRIMARY KEY (tenant_id, check_run_id, document_id),
|
||||
FOREIGN KEY (tenant_id, check_run_id) REFERENCES check_run (tenant_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tenant_id, document_id) REFERENCES source_document (tenant_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE finding (
|
||||
id uuid PRIMARY KEY,
|
||||
tenant_id uuid NOT NULL,
|
||||
project_id uuid NOT NULL,
|
||||
check_run_id uuid NOT NULL,
|
||||
checklist_item_id uuid, -- AI 判断的问题必填;规则问题有对应条目时填(DM6)
|
||||
rule_code text, -- 规则问题的规则编号,如 PRICE.CAPITAL_MISMATCH
|
||||
match_key text NOT NULL, -- item:<id> 或 rule:<code>[:<目标>];复查沿用按它 + type 匹配
|
||||
type text NOT NULL
|
||||
CHECK (type IN ('unresponded', 'negative_deviation', 'suspected_unmet',
|
||||
'formality', 'qualification', 'price_form', 'manual_review')),
|
||||
severity text NOT NULL CHECK (severity IN ('fatal', 'important', 'hint')),
|
||||
title text NOT NULL,
|
||||
explanation text NOT NULL,
|
||||
detector text NOT NULL CHECK (detector IN ('ai', 'rule')),
|
||||
detail jsonb NOT NULL DEFAULT '{}', -- 按类型:参数的要求值 / 响应值、资质预警种类、读到的报价金额
|
||||
search_scope jsonb, -- 「未找到」时找过哪些分册、哪些章节
|
||||
checks_passed text[] NOT NULL DEFAULT '{}',
|
||||
presence text NOT NULL CHECK (presence IN ('new', 'carried', 'resolved')), -- 新增 / 沿用 / 本次已解决
|
||||
carried_from_id uuid,
|
||||
status text NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'confirmed_ok', 'modified', 'ignored')),
|
||||
resolution_note text, -- 忽略原因;致命问题「确认无误」的说明(U4)
|
||||
resolved_by uuid REFERENCES user_account (id),
|
||||
resolved_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (tenant_id, id),
|
||||
UNIQUE (tenant_id, check_run_id, match_key, type),
|
||||
CHECK (checklist_item_id IS NOT NULL OR rule_code IS NOT NULL),
|
||||
CHECK (detector <> 'ai' OR checklist_item_id IS NOT NULL),
|
||||
CHECK (status <> 'ignored' OR resolution_note IS NOT NULL),
|
||||
CHECK (NOT (severity = 'fatal' AND status = 'confirmed_ok') OR resolution_note IS NOT NULL),
|
||||
CHECK (presence <> 'carried' OR carried_from_id IS NOT NULL),
|
||||
FOREIGN KEY (tenant_id, project_id) REFERENCES project (tenant_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tenant_id, check_run_id) REFERENCES check_run (tenant_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tenant_id, checklist_item_id) REFERENCES checklist_item (tenant_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tenant_id, carried_from_id) REFERENCES finding (tenant_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX ON finding (tenant_id, check_run_id, severity, status);
|
||||
|
||||
CREATE TABLE finding_citation (
|
||||
tenant_id uuid NOT NULL,
|
||||
finding_id uuid NOT NULL,
|
||||
citation_id uuid NOT NULL,
|
||||
side text NOT NULL CHECK (side IN ('tender', 'bid')),
|
||||
position int NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (tenant_id, finding_id, citation_id),
|
||||
FOREIGN KEY (tenant_id, finding_id) REFERENCES finding (tenant_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tenant_id, citation_id) REFERENCES citation (tenant_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 需人工核对的勾选:按项目存,复查沿用(P2)
|
||||
CREATE TABLE manual_check (
|
||||
tenant_id uuid NOT NULL,
|
||||
project_id uuid NOT NULL,
|
||||
check_key text NOT NULL, -- item:<清单项编号> 或 rule:<规则编号>
|
||||
checked boolean NOT NULL,
|
||||
note text,
|
||||
checked_by uuid NOT NULL REFERENCES user_account (id),
|
||||
checked_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (tenant_id, project_id, check_key),
|
||||
FOREIGN KEY (tenant_id, project_id) REFERENCES project (tenant_id, id) ON DELETE CASCADE
|
||||
);
|
||||
Reference in New Issue
Block a user