diff --git a/changelogs/2026-04/30_feat_contract-scheme_dispute-fields.md b/changelogs/2026-04/30_feat_contract-scheme_dispute-fields.md new file mode 100644 index 0000000..32a1721 --- /dev/null +++ b/changelogs/2026-04/30_feat_contract-scheme_dispute-fields.md @@ -0,0 +1,210 @@ +# 新增:合同方案补充仲裁机构/管辖法院字段 + 12301 dispute 节点完整化 + +**类型**: 后端 FEAT(新增字段+老 BUG 修复) +**关联**: 工单 #1561 / PR #1565 (合并到 dev sha `d3190de5`) +**日期**: 2026-04-30 +**前端处理者**: yst / mmg +**影响范围**: 管理后台 → 合同方案管理 → 编辑合同方案弹窗 → 争议解决方式 + +--- + +## 业务背景 + +管理后台合同方案编辑页(`web.test.1814.love:9443/contract/scheme`),用户截图反馈:**选择"仲裁/诉讼"时必须填对应地点,合同 PDF 模板里要用**。 + +排查发现: +- 12301 平台 `dispute.tribunalName` 字段已支持,但 `ContractCreateService.assembleParam` **从未把 scheme.tribunalName 透传过去** → 合同 PDF 仲裁条款常年是空模板(老 BUG) +- 合同方案表 `contract_scheme` 没有诉讼地点字段 +- `disputeResolution` 注释在不同代码处自相矛盾(`ContractSchemeRequest` 1=仲裁/2=诉讼 vs `ContractApplyParam` 1=诉讼/2=仲裁) + +经过 4 轮 PM 整改 + 4 轮评审委员会评审(综合 9.925/10)+ 架构师勘误 + dev 编码,本次 PR 一并解决。 + +--- + +## ⚠️ 关键:disputeResolution 语义统一为 1=诉讼 / 2=仲裁 + +**原 `ContractSchemeRequest` 注释 "1=仲裁 2=诉讼" 写反了**,本次后端统一以 12301 平台真值为准: + +| 值 | 含义 | +|---|---| +| 1 | 诉讼 (litigation) | +| 2 | 仲裁 (arbitration) | + +**前端 hl-ui 字典 `dispute_resolution` 渲染如原按 "1=仲裁" 请翻转**,否则用户选错语义。 + +测试服 DB 现存方案数据已经按 1=诉讼/2=仲裁 自洽(scheme_id=3 dispute=1 国内冬季诉讼;scheme_id=2046840448906940417 dispute=2 仲裁,均合理)— 确认 ContractApplyParam 注释才是真值。 + +--- + +## 后端改动概要 + +### 1. 字段 +`contract_scheme` 表增量加 2 列(已在测试服 DDL 落地): + +```sql +ALTER TABLE contract_scheme + ADD COLUMN tribunal_name VARCHAR(100) NULL COMMENT '仲裁机构名称(disputeResolution=2 仲裁时必填)', + ADD COLUMN litigation_court VARCHAR(100) NULL COMMENT '管辖法院(disputeResolution=1 诉讼时必填)'; +``` + +### 2. ReqVO / RespVO + +`ContractSchemeRequest` / `ContractSchemeVO` 加 2 字段: + +```java +@ApiModelProperty(value = "仲裁机构名称(disputeResolution=2 时必填)", example = "北京仲裁委员会") +@Size(max = 100, message = "仲裁机构名称长度不能超过 100 个字符") +private String tribunalName; + +@ApiModelProperty(value = "管辖法院(disputeResolution=1 时必填)", example = "北京市朝阳区人民法院") +@Size(max = 100, message = "管辖法院长度不能超过 100 个字符") +private String litigationCourt; +``` + +### 3. 校验 +联合校验在 Service 层(VO 层不加 `@NotBlank`,因为依赖 disputeResolution 值组合): + +| 场景 | 校验 | 错误码 | 业务码 | +|---|---|---|---| +| disputeResolution=2(仲裁)缺 tribunalName | 拦截 | `SCHEME_TRIBUNAL_NAME_REQUIRED` | **510216** | +| disputeResolution=1(诉讼)缺 litigationCourt | 拦截 | `SCHEME_LITIGATION_COURT_REQUIRED` | **510217** | +| 反向字段(选仲裁但传了诉讼地点等)| 不强制清空 | - | - | + +### 4. 合同条款渲染 +`supplementaryClause` 拼接顺序改为: + +``` +[1] safetyNotice (安全告知书,平台标准条款) +[2] disputeClause (争议解决条款,本次新增) +[3] userClause (用户自定义补充约定) +``` + +`disputeClause` 文本格式(本期固定模板): +- 仲裁: "双方因履行本合同发生争议的,提交{tribunalName}仲裁解决。" +- 诉讼: "双方因履行本合同发生争议的,提交{litigationCourt}诉讼解决。" + +12301 dispute 节点也会按 resolution 路由对应字段: +- 2=仲裁 → 输出 `dispute.tribunalName` +- 1=诉讼 → 输出 `dispute.litigationCourt` + +腾讯电子签链路通过共享 `param.supplementaryClause` 自动同步获得 disputeClause 文本(后端零改动)。 + +### 5. 字符净化 +`tribunalName` / `litigationCourt` 拼入条款文本时净化 `\n / \r / \t` 为空格(防 12301 errcode=301025,经验来自 PR #1551 reason 字段事故)。 + +--- + +## 错误响应示例 + +创建/修改合同方案时,选择仲裁但未填仲裁机构: + +```jsonc +HTTP 200 (走业务错误码,非协议错误) +{ + "code": 510216, + "message": "争议解决方式为仲裁时,仲裁机构名称不能为空", + "data": null +} +``` + +诉讼缺管辖法院: + +```jsonc +{ + "code": 510217, + "message": "争议解决方式为诉讼时,管辖法院不能为空", + "data": null +} +``` + +--- + +## 前端要做的改动 ⚠️ + +### 1. 字典 `dispute_resolution` 翻转(关键) +原可能渲染: `1=仲裁, 2=诉讼` → 改为 **`1=诉讼, 2=仲裁`**(与后端一致)。 + +如不翻转,用户选"仲裁" UI 传 1,后端按"1=诉讼"处理 → 数据完全反向。 + +### 2. 编辑合同方案弹窗 — 条件输入框 +争议解决方式单选(仲裁/诉讼)下方,**动态显示**对应输入框: + +| 选中 | 显示输入框 | 必填 | maxlength | placeholder | +|---|---|---|---|---| +| 仲裁(value=2)| 「仲裁机构名称」 | ✅ 必填 | 100 | 请填写仲裁委员会全称,例:北京仲裁委员会 | +| 诉讼(value=1)| 「管辖法院」 | ✅ 必填 | 100 | 请填写约定管辖法院,例:北京市朝阳区人民法院 | + +### 3. 切换 resolution 不清空已填值 +后端两列独立存,允许保留(用户切换"仲裁→诉讼→仲裁"时,仲裁地点会保留)。 + +### 4. 提交字段名 +- `disputeResolution` (Integer) +- `tribunalName` (String) +- `litigationCourt` (String) + +### 5. 编辑旧方案兼容 +旧方案 `tribunalName/litigationCourt` 都为 null: +- 加载时不拦截,允许列表正常显示 +- 用户**点击保存时**按新规校验拦截(后端会返 510216/510217) + +### 6. 错误码字典 +| 业务码 | 含义 | +|---|---| +| 510216 | 仲裁机构名称必填 | +| 510217 | 管辖法院必填 | + +--- + +## 部署状态 + +- ✅ PR #1565 合并到 dev (sha `d3190de5`,2026-04-30 15:46) +- ✅ 测试服 DDL 已落 (V20260501 ALTER TABLE 添加 2 列) +- ⏳ Deploy Panel 部署 hl-order-service-v2 测试服(进行中,管理者跟踪) +- ⏳ 测试服 12301 + 腾讯电子签 round-trip 验证待执行 +- ⏳ Release dev → main 待用户拍板 + +--- + +## 测试服 round-trip 计划 + +**经网关 https://api.test.1814.love:9443**: + +1. POST `/admin/contract/scheme` `disputeResolution=2` 不带 `tribunalName` → 期望 510216 +2. POST `/admin/contract/scheme` `disputeResolution=1` 不带 `litigationCourt` → 期望 510217 +3. POST `/admin/contract/scheme` 仲裁带 tribunalName → 200 success,GET 详情验证字段回显 +4. PUT 同一方案改诉讼带 litigationCourt → 200,字段独立保留 +5. 真实下单走 12301 报送(仲裁场景)→ 验 dispute.tribunalName 输出 + supplementaryClause 含仲裁文本 +6. 真实下单走腾讯电子签(仲裁场景)→ 验 supplementaryClause form 字段含仲裁文本(跨平台一致性) + +--- + +## 单测 + +`mvn -pl hl-order-service-v2 -Dtest='ContractCreateServiceTest,TencentEsignRequestBuilderTest' test`: +- 84 tests,0 failures,0 errors +- 17 个新单测 (4 校验 + 8 corner case + 4 边界净化顺序隐藏改点 + 1 跨平台) + +--- + +## 不动 + +- ❌ 腾讯电子签模板 widget(运营在腾讯控制台改模板时联动) +- ❌ disputeResolution `int → Integer` 重构(本期不动) +- ❌ MP / 字典服务 / Feign / MQ / 状态机 / 缓存(本期纯 admin 配置项) +- ❌ 历史合同回填(运营 TODO SQL 附 PR body) + +--- + +## 运营 TODO(可选) + +历史方案补齐(运营评估后由运维管理员执行,**本 PR 不自动跑**): + +```sql +-- 仲裁方案缺仲裁委员会 +SELECT scheme_id, name FROM contract_scheme + WHERE deleted_at IS NULL AND dispute_resolution = 2 AND (tribunal_name IS NULL OR tribunal_name = ''); + +-- 诉讼方案缺管辖法院 +SELECT scheme_id, name FROM contract_scheme + WHERE deleted_at IS NULL AND dispute_resolution = 1 AND (litigation_court IS NULL OR litigation_court = ''); +```