7.1 KiB
7.1 KiB
设计说明
1. 总体结构
合同任务分为四个同批兼容层:
- Prisma 增量字段与关系。
- 合同详情/终止/历史 API 与事务审计。
- 合同列表、详情、新建/编辑和响应式动作 UI。
- 状态机、权限、迁移和端到端验证。
本任务不建设合同附件库。证据区只消费现有台账字段和 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 和安全摘要,不直出 rawextra。
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 信息顺序
- 状态与下一步。
- 业务摘要:编号、类型、相对方、金额、日期、负责人、敏感级别。
- 证据区:归档位置、离线档案号、涉密资料索引;显示“涉密资料请登记线下归档位置”。
- 变更/操作历史。
- 动作面板: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_byRestrict 外键和 migration 兼容。- 详情 403/404、document refs 和 history 不泄露敏感字段。
- archived 锁定与修正追加不覆盖主记录。
reviewer 不修改代码,发现问题由主会话修复后再复查。