feat(contract): 合同方案补充仲裁机构/管辖法院字段 + 12301 dispute 节点完整化

PR #1565 (hotfix #1571) 已合并 dev。
关键: 前端字典 dispute_resolution 需翻转为 1=诉讼 2=仲裁。
通知 yst / mmg。
这个提交包含在:
API Changelog Bot 2026-04-30 16:14:42 +08:00
父节点 85f2f9e946
当前提交 cf5f6df4c8

查看文件

@ -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 = '');
```