补充供应商合同字段与附件(#6842)
changelog-filename-gate / validate (push) Successful in 2s

这个提交包含在:
lc
2026-08-31 12:20:58 +08:00
父节点 070c937a08
当前提交 2b60fc3323
@@ -0,0 +1,373 @@
---
schema: "hl-changelog/v2"
ticket: "6842"
title: "供应商补充合同字段与附件"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "PR #6858 已合并 dev-v3,hl-resource-service 已部署提交 5546d7907c6eda9b372b4639094f84e568de6496。TEST Gateway 已验证完整字段与全空业务字段合同的新增、更新、详情回读,以及单个 PDF 上传、预览、下载和清理。当前状态:后端已就绪,前端待处理。"
updated_at: "2026-08-31"
base: "dev-v3"
---
# 供应商补充合同字段与附件
供应商补充合同新增 `businessLine`、`relatedMainContract`、`autoRenew`,并允许写入 `SIGNED`(已签约)。所有合同业务字段均可为空,只有 `changeReason` 继续必填。
管理端登记页仅展示补合同编号、状态、合同名称、业务线、有效期、签署日期、关联主合同、自动续约和单个 Word/PDF 附件;其他兼容字段隐藏。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---:|---|---|---|---|---|
| 1 | 独立登记供应商合同 | POST | `/admin/supplier/items/{supplierId}/contracts/add` | 扩展请求与响应 | 增加三个字段,状态支持 `SIGNED` |
| 2 | 独立更新供应商合同 | PUT | `/admin/supplier/items/{supplierId}/contracts/{contractId}/update` | 扩展请求与响应 | 新字段支持补录、清空和 `false` |
| 3 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 扩展响应 | `contracts[]` 完整回读补充合同字段 |
## 三、接口详情
### 1. 独立登记供应商合同 `POST /admin/supplier/items/{supplierId}/contracts/add`
**VO**: `SupplierContractCreateReqVO / SupplierContractRespVO`
#### 使用场景
供应商已创建后登记一份补充合同;业务资料未齐时也可只提交审计原因,稍后再补录。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
| `contractNo` | Body | String/null | 否 | 最长 100 | 补合同编号 |
| `status` | Body | String/null | 否 | `DRAFT` / `ACTIVE` / `SIGNED` / `EXPIRED` | `SIGNED` 显示为“已签约” |
| `contractName` | Body | String/null | 否 | 最长 500 | 合同名称 |
| `businessLine` | Body | String/null | 否 | 最长 100 | 业务线,自由文本 |
| `startDate` | Body | String/null | 否 | `yyyy-MM-dd` | 有效期开始日 |
| `endDate` | Body | String/null | 否 | `yyyy-MM-dd` | 有效期结束日;两端都有值时不得早于开始日 |
| `signDate` | Body | String/null | 否 | `yyyy-MM-dd` | 合同签署日期 |
| `relatedMainContract` | Body | String/null | 否 | 最长 100 | 关联主合同外部引用 |
| `autoRenew` | Body | Boolean/null | 否 | `true` / `false` / `null` | 是 / 否 / 未登记 |
| `scanFileUrl` | Body | String/null | 否 | 最长 1000 | 文件中心确认上传后返回的永久 `ossUrl` |
| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 | 登记原因,写入审计 |
`contractType`、`amount`、`pricingMode`、`settleCycle`、`remark` 仍兼容但本页面隐藏,也均可为空。
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.contractId` | String | 合同 ID |
| `data.contractNo` | String/null | 补合同编号 |
| `data.status` | String/null | 包含新增可写值 `SIGNED` |
| `data.contractName` | String/null | 合同名称 |
| `data.businessLine` | String/null | 业务线 |
| `data.startDate` / `data.endDate` | String/null | 有效期日期 |
| `data.signDate` | String/null | 签署日期 |
| `data.relatedMainContract` | String/null | 关联主合同 |
| `data.autoRenew` | Boolean/null | 自动续约三态值 |
| `data.scanFileUrl` | String/null | 合同附件永久地址 |
| `data.updateTime` | String | 当前合同版本,格式 `yyyy-MM-dd HH:mm:ss` |
#### 请求示例
```json
{
"contractNo": "SHCT2025070102599446",
"status": "SIGNED",
"contractName": "内蒙古牵手草原国际旅行社有限公司额外后返",
"businessLine": null,
"startDate": "2010-01-01",
"endDate": "2099-12-31",
"signDate": "2025-07-01",
"relatedMainContract": "2185933",
"autoRenew": true,
"scanFileUrl": "https://files.example.com/supplier-contract/example.pdf",
"changeReason": "补录已签约合同"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"contractId": "2095000000000000001",
"contractNo": "SHCT2025070102599446",
"status": "SIGNED",
"contractName": "内蒙古牵手草原国际旅行社有限公司额外后返",
"businessLine": null,
"startDate": "2010-01-01",
"endDate": "2099-12-31",
"signDate": "2025-07-01",
"relatedMainContract": "2185933",
"autoRenew": true,
"scanFileUrl": "https://files.example.com/supplier-contract/example.pdf",
"updateTime": "2026-08-31 12:00:00"
}
}
```
#### 空数据 / 降级响应
仅提交 `changeReason` 也可成功;所有合同业务字段返回 `null`,前端不得补默认状态或日期。
#### 错误响应
```json
{
"code": 400,
"message": "变更原因不能为空",
"success": false,
"data": null
}
```
#### 业务边界
- 所有合同业务字段可空;`changeReason` 不属于业务展示字段,继续必填。
- `startDate`、`endDate` 同时有值时才校验日期先后。
- `scanFileUrl` 只保存一个附件永久地址;上传、预览、下载复用文件中心。
### 2. 独立更新供应商合同 `PUT /admin/supplier/items/{supplierId}/contracts/{contractId}/update`
**VO**: `SupplierContractUpdateReqVO / SupplierContractRespVO`
#### 使用场景
补齐、修改或清空已登记合同。该接口为完整替换,省略或传 `null` 会清空对应字段。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
| `contractId` | Path | String | 是 | 正整数 ID 字符串 | 目标合同 |
| 新增接口全部业务字段 | Body | 对应类型/null | 否 | 与新增一致 | 完整替换;`autoRenew=false` 会保留为否 |
| `expectedUpdateTime` | Body | String/null | 否 | `yyyy-MM-dd HH:mm:ss` | 提供时必须匹配当前版本 |
| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 | 更新原因 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.contractId` | String | 原合同 ID |
| `data.businessLine` | String/null | 更新后的业务线,空白规范为 `null` |
| `data.relatedMainContract` | String/null | 更新后的关联主合同 |
| `data.autoRenew` | Boolean/null | 精确保留 `true`、`false` 或 `null` |
| `data.status` | String/null | 可返回 `SIGNED` |
| 其余合同业务字段 | 对应类型/null | 完整替换后的值 |
| `data.updateTime` | String | 更新后的合同版本 |
#### 请求示例
```json
{
"contractNo": "SHCT2025070102599446",
"status": "SIGNED",
"contractName": "内蒙古牵手草原国际旅行社有限公司额外后返",
"businessLine": null,
"startDate": "2010-01-01",
"endDate": "2099-12-31",
"signDate": "2025-07-01",
"relatedMainContract": null,
"autoRenew": false,
"scanFileUrl": "https://files.example.com/supplier-contract/example.pdf",
"changeReason": "调整自动续约信息"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"contractId": "2095000000000000001",
"contractNo": "SHCT2025070102599446",
"status": "SIGNED",
"businessLine": null,
"relatedMainContract": null,
"autoRenew": false,
"updateTime": "2026-08-31 12:00:01"
}
}
```
#### 空数据 / 降级响应
仅提交 `changeReason` 可把全部业务字段清空;与当前内容完全相同时拒绝空更新,不生成新版本。
#### 错误响应
```json
{
"code": 395054,
"message": "合同有效期开始日期不能晚于结束日期",
"success": false,
"data": null
}
```
#### 业务边界
- 页面隐藏兼容字段时,编辑既有合同应从详情原样回传其 `contractType`、`amount`、`pricingMode`、`settleCycle`、`remark`,避免完整替换误清历史值。
- `autoRenew=false` 是有效业务值,不能按空值过滤。
- `expectedUpdateTime` 可省略;提供过期值时返回 `395014`,合同保持不变。
### 3. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
**VO**: `SupplierBasicInfoRespVO / SupplierContractRespVO`
#### 使用场景
进入供应商编辑或详情页时,从 `data.contracts[]` 回显合同表格和附件操作。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.contracts` | Array | 当前有效合同,按合同 ID 升序 |
| `data.contracts[].businessLine` | String/null | 业务线 |
| `data.contracts[].relatedMainContract` | String/null | 关联主合同 |
| `data.contracts[].autoRenew` | Boolean/null | 自动续约三态值 |
| `data.contracts[].status` | String/null | `SIGNED` 显示为“已签约” |
| `data.contracts[].scanFileUrl` | String/null | 单个合同附件永久地址 |
| 其余合同字段 | 对应类型/null | 与新增、更新响应一致 |
#### 请求示例
```http
GET /admin/supplier/items/2095000000000000000/basic-info/view
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2095000000000000000",
"contracts": [
{
"contractId": "2095000000000000001",
"contractNo": "SHCT2025070102599446",
"status": "SIGNED",
"contractName": "内蒙古牵手草原国际旅行社有限公司额外后返",
"businessLine": null,
"startDate": "2010-01-01",
"endDate": "2099-12-31",
"signDate": "2025-07-01",
"relatedMainContract": "2185933",
"autoRenew": true,
"scanFileUrl": "https://files.example.com/supplier-contract/example.pdf"
}
]
}
}
```
#### 空数据 / 降级响应
没有合同记录时 `contracts=[]`;合同存在但某项未登记时,对应字段为 `null`。
#### 错误响应
```json
{
"code": 395001,
"message": "供应商不存在",
"success": false,
"data": null
}
```
#### 业务边界
- 日期均为上海业务日字符串,前端按 `yyyy-MM-dd` 原样展示,不做 UTC 日期转换。
- 查询只返回未删除合同;附件为空时隐藏预览、下载操作。
- 未登录请求由 Gateway 返回业务码 `401`。
## 四、契约约束与正确调用方式
1. 表格列固定映射为 `contractNo`、`status`、`contractName`、`businessLine`、`startDate/endDate`、`signDate`、`relatedMainContract`、`autoRenew`、`scanFileUrl`;`SIGNED` 展示“已签约”。
2. `startDate/endDate` 直接按上海日历日拼成有效期;`autoRenew=true` 可显示“到期自动续约”,`false` 显示“否”,`null` 显示未登记占位。
3. 附件复用文件中心:申请上传凭证 `POST /admin/file/upload/token`,按返回模式上传后调用 `POST /admin/file/upload/confirm`,把确认响应的 `data.ossUrl` 写入 `scanFileUrl`。组件仅保留一个 `.doc`、`.docx` 或 `.pdf` 文件。
4. 预览使用带登录态的 `GET /admin/file/preview-by-url?url={encodeURIComponent(scanFileUrl)}`;下载直接访问 `scanFileUrl`。
5. 本页面隐藏 `contractType`、`amount`、`pricingMode`、`settleCycle`、`remark`;编辑历史合同时仍从详情原样回传,避免完整替换清空旧值。
## 五、数据库行为
这里只约定外部可观察结果:新增或更新成功后,再次请求基本信息详情可读到相同字段;未填写值返回 `null`,`autoRenew=false` 不会丢失。失败响应不产生部分合同变更。
## 六、边界行为
- 所有合同业务字段可空,只有新增、更新、删除操作的 `changeReason` 必填。
- `SIGNED` 仅是新增可写状态,不改变既有 `DRAFT`、`ACTIVE`、`EXPIRED` 和历史 `TERMINATED` 的兼容读取。
- 日期两端都有值且开始日晚于结束日时返回 `395054`,整次更新零写入。
- 附件字段为单值;前端负责限制为一个 Word/PDF,并在替换附件后提交新的永久地址。
- 写接口沿用既有角色、权限、幂等、锁和审计规则,无新增错误码。
## 六.6、修改前后对比
| 项目 | 修改前 | 修改后 |
|---|---|---|
| 业务线、关联主合同、自动续约 | 无字段 | 新增 `businessLine`、`relatedMainContract`、`autoRenew`,均可空 |
| 合同状态 | 可写 `DRAFT` / `ACTIVE` / `EXPIRED` | 追加可写 `SIGNED`(已签约) |
| 详情回读 | 无新增三字段 | `contracts[]` 完整回读新增三字段 |
| 其他合同业务字段 | 可空 | 继续可空,语义不变 |
## 六.7、影响评估
- **是否破坏向后兼容**:否,均为可选字段和状态枚举扩展。
- **前端是否必须同步上线**:是,需要新增列、附件控件和 `SIGNED` 中文映射,并隐藏非目标字段。
- **前端 workaround 清理点**:不要在本地拼装缺失字段;以详情 `contracts[]` 为回显真相。
## 七、不影响范围
- 不修改供应商主体、联系人、资质、结算账户、审批和归档接口。
- 不把合同并回供应商注册聚合;仍先取得 `supplierId`,再调用独立合同接口。
- 不新增 Gateway 路由、权限点、配置、Redis 或 MQ 契约。
## 八、测试环境已验证
- 已在 TEST Gateway 验证完整字段和仅含 `changeReason` 的合同均可新增并跨请求回读;`SIGNED`、`null`、`false`、日期错误零写入均符合本契约。
- 已验证一个 PDF 经文件中心上传后可预览、下载,验收产生的合同、附件和会话均已清理。
## 当前状态
- 后端:已部署并已验证。
- 前端:待处理。
## 十、相关文档
- Issue:[#6842](https://git.1814.love:8443/wx/HL/issues/6842)
- 后端 PR:[#6858](https://git.1814.love:8443/wx/HL/pulls/6858)
- 合并提交:[5546d790](https://git.1814.love:8443/wx/HL/commit/5546d7907c6eda9b372b4639094f84e568de6496)
## 关联 / 联系人
- **Issue**: [#6842](https://git.1814.love:8443/wx/HL/issues/6842)
- **PR**: [#6858](https://git.1814.love:8443/wx/HL/pulls/6858)
- **后端负责人**: @lc