方案关联关系功能.md 8.0 KB

方案关联关系功能

状态:已完成(2026-08-22)

测试结果:

  • 后端 pytest:171 passed,3 failed 均为既有脱节(test_http.py 路由白名单/统一 404 格式、test_b5_cors_headers 同一路由 404 requestId,与本次无关),385 skipped(无 MySQL 时集成用例自动跳过);其中新增 test_document_relations.py 55 条用例全绿(集成 8 条跳过)。
  • 前端 vitest:54 文件 / 358 用例全绿(基线 343 + 新增 15:documentRelations.test.ts 守卫/API 11 条、relationScope.test.ts scope 断言 4 条);tsc --noEmit 通过。
  • 既有测试同步更新:passwordScope.test.ts 的 withDocumentPassword 计数 2→3(关联保存走 EDIT 密码包装)、test_models.py 模型集合 +doc_document_relation(10→11 表)、三处 OpenAPI 版本断言 1.8.0→1.9.0、test_openapi_storage.py 路径与版本、test_migration.py 迁移 head 锚定 0014。

1. 需求

在"编辑"功能里为方案增加关联关系:

  • 某方案可设置 0/1/多个关联方案,并指出其关系(如 A 方案指定 B 方案为其"依据"、C 方案为其"支撑");
  • 关系类型提供预置下拉(配置文件),同时保留用户自定义输入的口子;
  • "查看"弹窗一并展示关联清单,且点击后可跳转至对应方案的"查看"窗口。

已确认的三点决策:

  1. 预置关系初始清单:默认三项——依据(BASIS)/ 支撑(SUPPORT)/ 参照(REFERENCE);
  2. 关联对象范围:仅主体方案(MAIN)可作为被关联对象;
  3. 展示:查看弹窗一并展示,且点击可跳转打开对应方案的查看窗口。

补充约定(未显式询问,按"方案"语义推定):

  • 发起方(声明关联的文档)允许主案(MAIN)与子方案(SUB_PLAN),附件(ATTACHMENT)不参与——与 update_document 的类型约束一致;
  • 关系为单向声明(A 关联 B 不等于 B 关联 A),查看弹窗只展示本方案声明的关联;
  • side 模式右侧预览栏不展示关联(宽度受限,与提取文本同策略),仅 modal(查看窗口)展示并支持跳转。

2. 关键设计决策

2.1 预置关系放独立配置文件,不进 ui-dictionaries

ui-dictionaries 是强枚举绑定体系:字典 key 必须对应后端 StrEnum(DICTIONARY_ENUMS),JSON 集合与枚举成员必须完全一致,否则启动报错。而关系类型是开放集合(预置 + 用户自定义文本),不适合枚举绑定。

新建 backend/dms/resources/document-relation-presets.zh-CN.json:

{
  "presets": [
    { "code": "BASIS", "label": "依据", "sortNo": 10 },
    { "code": "SUPPORT", "label": "支撑", "sortNo": 20 },
    { "code": "REFERENCE", "label": "参照", "sortNo": 30 }
  ]
}

2.2 relation_type 存文本

doc_document_relation.relation_type 直接存最终文本(预置 label 或用户自定义文字):

  • 展示免翻译(预置清单变更不影响历史数据展示);
  • 代价:预置 label 改名后旧数据仍是旧文字(可接受,属数据治理范畴);
  • code 仅用于下拉匹配与排序,不落库。

2.3 新表 doc_document_relation(仿 doc_attachment_binding)

沿用挂载关系表的成熟模式(软删 + 生成列唯一约束):

列 类型 说明
id / created_by / updated_by / created_at / updated_at / deleted_at / is_deleted / row_version BusinessTableMixin 通用字段
document_id BigInteger FK doc_document.id RESTRICT 发起方
related_document_id BigInteger FK doc_document.id RESTRICT 被关联方(须为 MAIN)
relation_type String(64) NOT NULL 关系文本
sort_no Integer NOT NULL default 0 行序(10 步长)
active_marker Computed IF(is_deleted=0,1,NULL) 软删唯一约束载体

约束与索引:

  • uq_doc_document_relation_active (document_id, related_document_id, active_marker)——同一发起方对同一目标至多一条有效关联;
  • ck document_relation_no_self: document_id <> related_document_id;
  • ix_doc_document_relation_document_active (document_id, is_deleted, sort_no);
  • ix_doc_document_relation_related_active (related_document_id, is_deleted)。

2.4 API(整体替换语义)

方法 路径 权限 说明
GET /documents/relation-presets 登录 预置关系清单(启动期加载校验)
GET /documents/{id}/relations 登录 查看关联(走与 get_document 相同的访问控制)
PUT /documents/{id}/relations 管理员 整体替换保存

PUT 请求体:{ "relations": [{ "relatedDocumentId": "…", "relationType": "…" }], "rowVersion": n };空数组 = 清空全部关联。

服务层要点(仿 bind_attachments / update_document):

  • with transaction() + 发起方 with_for_update() 行锁;类型限 MAIN/SUB_PLAN;
  • 密码锁:require_document_password(document, EDIT, …)(设密主案编辑关联同样需密码);
  • 乐观锁:校验 rowVersion,成功后 row_version += 1(与挂载/解挂一致);
  • 校验:不可自关联;被关联方必须存在、未删且为 MAIN;relationType 1~64 非空文本;去重(relatedDocumentId 不可重复);上限 50 条;
  • 保存:软删旧有效行 → 插入新行(sort_no 10,20,…);
  • 审计:复用 EDIT_DOCUMENT 动作(detail 标注 scope=relations 与前后清单),不扩展 sys_audit_log 的 CHECK 约束(避免迁移面扩大);
  • GET 查询联表过滤被关联方 is_deleted=False(目标进回收站后隐藏,恢复后自动重现)。

2.5 前端

编辑弹窗(EditMetadataModal):

  • MAIN/SUB_PLAN 显示"关联方案"区块(附件不显示);
  • 行结构:[关系下拉(预置 + "自定义…")] [自定义文本输入(选中自定义时显示)] [方案下拉(复用 useReferenceData 的 mainPlans,排除自身)] [删除];
  • 关系下拉 value 存文本(选中预置即取其 label),matchPresetCode(text, presets) 派生是否自定义;
  • 独立"保存关联"按钮:不混入元数据提交流程;成功后用响应中的 rowVersion 同步本地 detail(与密码锁保存同模式),避免后续保存元数据撞版本;
  • DATA_VERSION_CONFLICT 处理复用现有模式。

查看窗口(FilePreviewModal,modal 模式):

  • textPanel 顶部新增"关联方案"区块:每行 关系 · 方案名,点击即跳转;
  • 跳转实现:内部 navigated: DocumentSummary | null 状态,current = navigated ?? item;点击关联项 → documentApi.detail(targetId) → setNavigated(detail),预览/正文/说明/关联全部以 current 驱动重新加载;
  • navigated 非空时区块头部显示"返回原方案"按钮;
  • 跳转失败(无权限/已删除)在区块内提示,不影响当前展示。

类型与 API client:

  • 新文件 frontend/src/features/documents/documentRelationTypes.ts:DocumentRelationItem / DocumentRelationListResult / DocumentRelationPreset(s) / UpdateDocumentRelationsRequest + 运行时守卫 + matchPresetCode 纯函数;
  • documentApi 增 listRelations / relationPresets;documentMutationApi 增 updateRelations(带密码头)。

3. 实施阶段

  1. 后端配置与模型:presets JSON + 加载校验服务 + DocumentRelation 模型 + 迁移 0014 + models/init 注册;
  2. 后端服务与接口:document_relation_service(查询/整体替换)+ documents.py 三条路由 + OpenAPI(paths/documents.yaml、schemas.yaml、openapi.yaml 版本说明);
  3. 前端:类型/守卫/纯函数 + API client + EditMetadataModal 关联区块 + FilePreviewModal 展示与跳转;
  4. 测试与回归:后端 pytest(迁移锚定 0013→0014、presets 校验、relations API 用例)+ 前端 vitest(类型守卫、matchPresetCode、scope 断言)+ tsc。

4. 测试基线

  • 前端 vitest:343 全绿(新增用例后相应增加);
  • 后端 pytest:120 passed + 1 failed(langchain 既有)+ 12 errors(既有)+ 377 skipped,运行需 export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1。