文件
hyb-oa/.trellis/tasks/08-05-contract-detail-workspace/design.md
T

7.1 KiB

设计说明

1. 总体结构

合同任务分为四个同批兼容层:

  1. Prisma 增量字段与关系。
  2. 合同详情/终止/历史 API 与事务审计。
  3. 合同列表、详情、新建/编辑和响应式动作 UI。
  4. 状态机、权限、迁移和端到端验证。

本任务不建设合同附件库。证据区只消费现有台账字段和 ConfidentialDocumentRef 的最小摘要。

2. 数据模型

在 ContractRegistry 增加:

terminated_at     DateTime?
terminated_reason String?   @db.VarChar(1000)
terminated_by     String?   @db.Uuid
terminator        Employee? @relation("ContractRegistryTerminator", fields: [terminated_by], references: [id], onDelete: Restrict)

在 Employee 增加反向关系 terminated_contract_registries。

迁移只增加三个可空列、外键和必要索引,不回填历史行,不修改 status 列类型。terminated_reason 请求 trim 后长度 10–1000。

数据库暂不增加跨列 CHECK;服务层与测试强制:

status = terminated  <=>  terminated_at/reason/by 全部非空
status != terminated <=>  terminated_at/reason/by 全部为空

terminated 是终态,本轮不允许恢复、续约或再归档,避免单一状态列丢失终止事实。

3. 状态值域与写入边界

  • CONTRACT_REGISTRY_PERSISTED_STATUS_VALUES 增加 terminated,筛选和派生状态自动识别。
  • resolveContractRegistryEffectiveStatus 继续只对持久状态 active 派生 expired;terminated 天然不被覆盖。
  • 到期提醒继续硬过滤 active,无需特殊排除逻辑,但增加回归断言。
  • 不把通用 create/PATCH schema 直接开放为可写 terminated。新增一般写状态集合 draft/active/archived,终止只能走专用接口,防止绕过三字段不变量。
  • 通用 PATCH 对 terminated 和 archived 主记录执行状态锁:terminated 不可编辑;archived 普通字段不可覆盖。

4. API 合同

4.1 详情

GET /api/contracts-registry/:id

  • 使用与列表相同的数据范围条件查询,未授权与不存在统一安全 404。
  • 返回合同摘要、owner、project、terminator、三个终止字段、最小涉密资料索引和白名单操作历史。
  • ConfidentialDocumentRef 不返回完整 storage_path、access rule raw JSON 或其他敏感原文;只返回 ID、标题、资料编号、敏感级别、存储类型、离线档案号、责任人和时间等必要摘要。
  • 历史从 AuditLog 按 target_type=contract_registry + target_id 查询,只映射允许展示的 action、actor、time 和安全摘要,不直出 raw extra。

4.2 终止

POST /api/contracts-registry/:id/terminate

请求:{ reason: string },trim 后 10–1000 字。

  • 仅当前合同管理权限可调用。
  • 只允许持久状态 active(包含页面派生 expired);draft 应删除/取消拟办,archived 与 terminated 拒绝。
  • 在一个 Prisma transaction 中锁定/读取当前行、校验状态、写 status + terminated_*、创建 CONTRACT_REGISTRY_TERMINATED 审计。
  • 审计创建失败时整个事务回滚;不调用现有会吞错的 best-effort helper。
  • 审计 extra 只记录合同编号、from/to 状态和必要原因摘要,不写合同正文或原始 payload。

4.3 归档、恢复与修正

  • 现有归档/恢复 UI 迁入详情。服务端至少阻止 archived 普通 PATCH 和 terminated 的任何一般状态写入。
  • 归档/恢复属于状态变更,改为事务内 required audit;不得状态成功而审计失败。
  • 归档后修正使用追加式 correction 记录,不覆盖主合同普通字段。可复用 AuditLog 新 action 与白名单修正 payload;详情把它显示为追加历史。
  • 续约保持 active/expired → 新 draft + 原合同 archived,并继续事务内审计。

5. 前端信息架构

5.1 路由与页面

  • 列表:/contracts-registry
  • 详情:/contracts-registry/:contractId
  • 新建可使用独立 /contracts-registry/new 或明确 Dialog;实施时优先选择复用现有表单且 dirty guard 最清晰的独立新建工作区。
  • 编辑在详情工作区展开或进入明确编辑态,不再使列表页面整体变形。

5.2 信息顺序

  1. 状态与下一步。
  2. 业务摘要:编号、类型、相对方、金额、日期、负责人、敏感级别。
  3. 证据区:归档位置、离线档案号、涉密资料索引;显示“涉密资料请登记线下归档位置”。
  4. 变更/操作历史。
  5. 动作面板:PC 右侧,窄屏/移动底部安全区,复用低风险子任务的共享动作容器。

不渲染空附件模块,不在列表增加附件完整性列。版式分区保持可扩展,但不显示六类空占位。

6. 动作矩阵

状态 编辑 续约 归档 恢复 终止 修正追加
draft 允许 禁止 按现有规则 - 禁止 禁止
active / expired 允许 允许 允许 - 允许 禁止
archived 禁止覆盖 禁止 - 允许 禁止 授权角色允许
terminated 禁止 禁止 禁止 禁止 禁止 只读

终止面板始终显示至少 10 字要求;不足时按钮禁用且邻近显示原因。确认弹窗展示合同编号、相对方、金额以及“合同将标记已终止、编号不释放、普通编辑/续约不可用”的后果。

7. 跨模块与权限

  • 本轮详情只稳定展示已有 project 关联并跳转项目详情。
  • 采购/付款没有现成合同外键或可靠查询时不显示空模块,不为页面完整性新增错误关联。
  • 合同审批和付款写动作不进入详情;未来只显示授权摘要与归属模块入口。
  • 所有详情数据由服务端范围查询裁剪,前端隐藏不替代权限。

8. 迁移、发布与回滚

  • 本地:先 prisma:validate,生成 client,运行聚焦 route 测试和迁移状态只读检查;不得 reset 数据库。
  • migration 是向后兼容的可空列。部署新 schema 后、首次产生 terminated 行前,旧应用可暂时忽略新列;不主动 drop。
  • 一旦生产产生 terminated 行,旧应用不认识该状态且无法展示终止事实,应用回滚应以 roll-forward 或兼容 hotfix 为主,不能简单回到旧客户端/服务端。
  • drop 三列会保留审计但使 terminated 行与字段不变量失配,因此不作为自动回滚。任何生产 schema drop、状态翻译或数据修复都需另行批准。
  • 部署后显式运行 migration status,并验证 /ready、合同列表/详情、终止负向路径和权限;init 日志不能替代迁移状态证据。

9. 只读 Review

实施后 reviewer 只读检查:

  • terminated 与三字段全有/全空不变量。
  • 通用 create/PATCH 无法绕过专用终止接口。
  • audit failure 能回滚状态写入。
  • expired/expiring/renew 行为未误含 terminated。
  • terminated_by Restrict 外键和 migration 兼容。
  • 详情 403/404、document refs 和 history 不泄露敏感字段。
  • archived 锁定与修正追加不覆盖主记录。

reviewer 不修改代码,发现问题由主会话修复后再复查。