比较提交

...
作者 SHA1 备注 提交日期
lc 1ba3299afa docs(changelog): 补充草稿结算编辑契约 #6669
changelog-filename-gate / validate (pull_request) Successful in 2s
2026-08-29 17:12:25 +08:00
Mimingguang bd235c2e1d chore(changelog): 回写 #6654 前端消费状态 verified(frontend_ref 5fe4e6fa)
changelog-filename-gate / validate (push) Successful in 3s
2026-08-29 16:32:05 +08:00
lc eb628f56b0 docs(changelog): 补充 #6654 供应商合同与结算契约
changelog-filename-gate / validate (push) Successful in 2s
2026-08-29 16:15:01 +08:00
Mimingguang b54c4cc8d7 chore(changelog): 回写 #6544/#6620/#6643 前端消费状态 verified
changelog-filename-gate / validate (push) Successful in 2s
修改原因:三条 changelog 前端已交付并推送 hl-admin v2.1,回写 frontmatter 消费证据。
- #6544 供应商合同独立登记 → frontend_ref 55a3a056
- #6620 主体证件号 taxNo 回显(DRAFT 可改非 DRAFT 锁定) → frontend_ref 9d5dcbab
- #6643 营业执照 licenseImageUrl 顶层权威回显 → frontend_ref bab3673b
2026-08-29 15:07:39 +08:00
lc 4621f11e36 交接供应商编辑字段回显契约(#6643)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-29 14:36:10 +08:00
lc 1f608928b7 Merge pull request 'docs: 交接供应商 tax_no 表单绑定缺口 (#6620)' (#81) from changelog/6620-supplier-taxno-frontend into main
changelog-filename-gate / validate (push) Successful in 2s
2026-08-29 12:01:09 +08:00
lc a4aba37568 docs: hand off supplier tax_no binding #6620
changelog-filename-gate / validate (pull_request) Successful in 2s
2026-08-29 11:53:48 +08:00
lc 60a28339e0 Merge pull request 'docs: 纠正供应商合同独立登记交付状态 (#6544)' (#80) from changelog/6544-supplier-contract-boundary into main
changelog-filename-gate / validate (push) Successful in 2s
2026-08-29 09:40:44 +08:00
共修改 5 个文件,包含 1194 行新增和 2 行删除
@@ -7,9 +7,9 @@ author: "lc(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "pending"
frontend_ref: "55a3a056"
target_release: "v2.1"
verified_at: "2026-08-29"
status_note: "PR #6559、#6591、#6606 已合并 dev-v3;hl-resource-service 已通过 Deploy Panel 任务 ca2fe4d7 部署最终提交 c579c87014f56452fea2fce5075b31ae6fd12a35。#6591 的写后回读、395052–395058、金额 scale 等值与草稿合同存在门禁均已进入 TEST。#6544 基线曾完成独立合同写链路实证;本次最终提交因运行时身份缺少 supplier:update,按用户确认冻结权限依赖的真实写复测,未配置权限、未发业务写请求,不把缺失权限记为通过。"
@@ -0,0 +1,55 @@
---
schema: "hl-changelog/v2"
ticket: "6620"
title: "供应商编辑页主体证件号未绑定 tax_no"
consumer: "admin"
author: "lc(GIT)"
change_type: "前端缺陷"
backend_status: "not_required"
gateway_status: "not_required"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "9d5dcbab"
target_release: "v2.1"
verified_at: "2026-08-29"
status_note: "hl-admin 端实际绑定 formData.taxNo(非 socialCreditCode,该定位对端不符);经用户拍板采用「tax_no 回显、DRAFT 可改、非 DRAFT 锁定」折中,非 DRAFT 恒不携带防 395033,未按字面「保持可编辑」。契约库记 002 tax_no 脱敏与本条称完整值不一致,回填已加脱敏守卫兼容。"
updated_at: "2026-08-29"
base: "dev-v3"
---
# 供应商编辑页主体证件号未绑定 `tax_no`
后端契约没有变化。打开已有供应商时,详情响应已经返回完整 `tax_no`,管理端需在表单适配层消费该字段。
## 接口与字段
- 接口:`GET /admin/supplier/items/{supplierId}/basic-info/view`
- 响应字段:`data.tax_no`,类型为 `string`,值为完整主体证件号。
- 创建、更新请求字段仍为 `taxNo`。响应与请求字段命名不同,前端不得改读 `data.taxNo` 或用 `socialCreditCode` 代替响应字段。
## 前端处理要求
1. 详情加载后,将非空 `data.tax_no` 映射为“主体证件号”输入框的实际值,并保持可编辑。
2. 详情值必须覆盖表单初始空值;初始化或重置逻辑不得在映射后再次清空。
3. 只有接口实际返回空值时才显示空输入框;占位提示不能代替已有证件号。
4. 保存时继续按既有写契约提交 `taxNo`,不要新增或要求后端返回重复字段。
## 后端核验结论
- 响应 VO 将 Java 属性 `taxNo` 固定序列化为 JSON 字段 `tax_no`。
- 查询服务直接取供应商主体的 `taxNo` 写入响应,持久化读取兼容密文和历史明文。
- 供应商回显、查询、Controller 契约与聚合写入相关测试共 60 项通过,失败、错误和跳过均为 0。
- #6499 / PR #6505 已在 TEST 验证详情返回的 `tax_no` 与数据库原值一致;当前 `dev-v3` 已包含该实现。本条目不引入后端代码、接口、数据库、配置、Redis 或 MQ 变更。
## 只读前端定位
现有管理端表单显示字段绑定为 `socialCreditCode`,详情适配逻辑没有把后端 `tax_no` 映射到该输入框,因此页面会保留初始空值。该定位仅用于联调交接,本工单未修改前端源码。
## 撤回
如需撤回本联调通知,只需回退本 Changelog 文件;既有后端 `tax_no` 契约和运行数据不受影响。
## 关联 / 联系人
- Issue:[#6620](https://git.1814.love:8443/wx/HL/issues/6620)
- 后端联系人:@lc
@@ -0,0 +1,389 @@
---
schema: "hl-changelog/v2"
ticket: "6643"
title: "供应商编辑页地址营业执照与备注回显"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "bab3673b"
target_release: "v2.1"
verified_at: "2026-08-29"
status_note: "PR #6645 已合并 dev-v3;hl-resource-service 已通过 Deploy Panel 任务 954419f9 部署提交 d206d1d577a3e3f8cec228e73e139fcb36c00fd5。真实 TEST Gateway 已验证 address、licenseImageUrl、remark 原值回显、更新后回读、空地址保留、营业执照顶层字段权威读取、395014 零写入及测试草稿清理。"
updated_at: "2026-08-29"
base: "dev-v3"
---
# 🔧 供应商编辑页地址、营业执照与备注回显
供应商编辑页应直接使用详情根对象的 `address`、`licenseImageUrl`、`remark` 初始化表单。其中 `licenseImageUrl` 是营业执照上传控件的权威字段,不得再从 `qualifications[].imageUrl` 推导或覆盖。
本次没有新增接口路径、请求必填项、权限点或业务错误码。
## 一、关键变化
| 字段 | 修改前 | 修改后 |
|---|---|---|
| `data.address` | 已有详情字段,但编辑和空值保留规则缺少本次回归锁定 | 返回已保存地址;提交非空新值后可回读,更新时留空保持原值 |
| `data.licenseImageUrl` | 详情根对象没有稳定的营业执照权威回显,调用方可能从资质数组取值 | 详情根对象稳定返回营业执照;只读取顶层权威值,不再从资质数组反向派生 |
| `data.remark` | 已有详情字段,但编辑回显和更新规则缺少本次回归锁定 | 返回已保存内部备注;提交非空新值后可回读 |
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---:|---|---|---|---|---|
| 1 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应修改 | 根对象稳定返回 `address`、`licenseImageUrl`、`remark` |
| 2 | 更新供应商资料 | PUT | `/admin/supplier/items/{supplierId}/update` | 行为修改 | 三字段按增量更新和并发版本规则保存;写后重新查询可得到新值 |
## 三、接口详情
### 1. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
**VO**: `SupplierBasicInfoRespVO`
#### 使用场景
管理端进入供应商详情或编辑页时获取完整表单快照。营业执照上传控件必须绑定根对象 `data.licenseImageUrl`;`qualifications[]` 继续用于独立资质列表,不是该控件的取值来源。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商;不得转为 JavaScript Number |
无 Query 参数,无请求体。
#### 出参 `Result<SupplierBasicInfoRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.address` | String/null | 注册地址或经营地址;未填写时为 `null` |
| `data.licenseImageUrl` | String/null | 营业执照影像权威地址;未填写时为 `null` |
| `data.remark` | String/null | 供应商主体内部备注;不等同于审批意见或变更原因 |
| `data.qualifications[].imageUrl` | String/null | 单项资质影像;与根对象营业执照字段分别读取 |
| `data.updateTime` | String | 主体并发版本,格式 `yyyy-MM-dd HH:mm:ss` |
#### 请求示例
```http
GET /admin/supplier/items/2092800000000000001/basic-info/view
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2092800000000000001",
"supplierNo": "SUP2092800000000000001",
"fullName": "示例旅行服务有限公司",
"shortName": "示例旅行",
"tax_no": "91350211M000100Y46",
"legalRepresentative": "示例法人",
"legalRepresentativeIdNo": "11010519491231002X",
"legalRepresentativeIdNoMask": "11010519491231002X",
"legalRepresentativeIdCardFrontUrl": null,
"legalRepresentativeIdCardBackUrl": null,
"contactPhone": "13800138000",
"contactPhoneMask": "13800138000",
"establishDate": "2020-01-01",
"registeredCapital": "100万元",
"businessScope": "境内旅游服务",
"address": "福建省厦门市示例路 2 号",
"staffScale": "LT50",
"mainCooperation": "酒店与景区资源合作",
"licenseImageUrl": "https://files.example.com/supplier/business-license-new.jpg",
"remark": "地址与营业执照已复核",
"status": "DRAFT",
"creditLevel": "B",
"totalScore": null,
"types": [],
"primaryTypeCode": null,
"primaryTypeName": null,
"contacts": [],
"qualifications": [
{
"qualificationId": "2092800000000000011",
"qualType": "BUSINESS_LICENSE",
"qualTypeName": "营业执照",
"certNo": null,
"certNoMask": null,
"imageUrl": "https://files.example.com/supplier/business-license-new.jpg",
"expiryDate": null,
"permanentValid": true,
"daysUntilExpiry": null,
"validityStatus": "VALID",
"validityStatusName": "有效",
"isRequired": false,
"expired": false,
"updateTime": "2026-08-29 14:40:01"
}
],
"contracts": [],
"updateTime": "2026-08-29 14:40:02"
}
}
```
#### 空数据 / 降级响应
未填写三字段时返回明确的 `null`,前端显示空控件即可,不要用资质数组补写营业执照:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2092800000000000001",
"address": null,
"licenseImageUrl": null,
"remark": null,
"qualifications": [],
"updateTime": "2026-08-29 14:40:02"
}
}
```
#### 错误响应
供应商不存在、已删除或超出可见范围:
```json
{
"code": 395001,
"message": "供应商不存在",
"success": false,
"data": null
}
```
#### 业务边界
- 要求可信管理身份、可读角色、`supplier:view` 平台权限和既有数据范围。
- 根对象 `licenseImageUrl` 是编辑页营业执照的唯一权威回显;即使 `qualifications[]` 中同类型影像不同,也不得覆盖根对象值。
- 历史记录没有可用营业执照时返回 `null`,不猜测默认图片。
- 本接口只读,不推进版本、不触发审批或其他业务副作用。
### 2. 更新供应商资料 `PUT /admin/supplier/items/{supplierId}/update`
**VO**: `SupplierUpdateReqVO / SupplierWriteRespVO`
#### 使用场景
管理端保存供应商编辑表单。请求只发送实际修改字段,并携带详情中的最新 `updateTime` 和本次 `changeReason`。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
| `address` | Body | String | 否 | 最长 500 字符 | 非空时更新;省略、`null`、空串或纯空格时保留原值 |
| `licenseImageUrl` | Body | String | 否 | 最长 500 字符 | 省略时保留;传非空值时更新权威营业执照;传空串时清空 |
| `remark` | Body | String | 否 | - | 非空时更新;省略、`null`、空串或纯空格时保留原值 |
| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 字符 | 本次资料变更原因 |
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 详情最新主体并发版本 |
其他既有可选字段和集合快照规则保持不变。
#### 出参 `Result<SupplierWriteRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.supplierId` | String | 供应商 ID |
| `data.supplierNo` | String | 供应商业务编号 |
| `data.status` | String | 当前生命周期状态 |
| `data.onboardingStage` | String | 当前注册阶段 |
| `data.initialAccounts` | Array | 初始收款账户摘要 |
| `data.updateTime` | String | 写入后的新主体并发版本 |
#### 请求示例
```http
PUT /admin/supplier/items/2092800000000000001/update
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"address": "福建省厦门市示例路 2 号",
"licenseImageUrl": "https://files.example.com/supplier/business-license-new.jpg",
"remark": "地址与营业执照已复核",
"changeReason": "更新供应商编辑资料",
"expectedUpdateTime": "2026-08-29 14:40:00"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2092800000000000001",
"supplierNo": "SUP2092800000000000001",
"status": "DRAFT",
"onboardingStage": "PROFILE_DRAFT",
"initialAccounts": [],
"updateTime": "2026-08-29 14:40:02"
}
}
```
写响应不重复返回 `address`、`licenseImageUrl`、`remark`。前端需要完整表单时,应使用新 `updateTime` 重新查询详情。
#### 空数据 / 降级响应
成功更新时 `data` 恒为写入结果对象,不返回 `null`。当前请求没有实际变化时不会伪造成功或推进版本,而是返回业务失败:
```json
{
"code": 400,
"message": "未检测到实际变化",
"success": false,
"data": null
}
```
#### 错误响应
```json
{
"code": 395014,
"message": "数据已被他人修改,请刷新后重试",
"success": false,
"data": null
}
```
收到 `395014` 后先重新获取详情,由用户确认后再提交;不得用旧表单自动覆盖。
#### 业务边界
- 要求可信管理身份、`FINANCE` 或 `SUPER_ADMIN` 写角色及 `supplier:update` 平台权限。
- 成功写入后推进主体 `updateTime`;权限、参数、状态或并发失败时三字段及版本均不变化。
- 顶层 `licenseImageUrl` 更新时会继续兼容同步营业执照资质影像;反向只修改 `qualifications[].imageUrl` 不会改写顶层权威字段。
- `remark` 是主体内部备注;`changeReason` 是本次变更审计原因,两者不得互相替代。
- 统一响应可能以 HTTP 200 承载业务失败,调用方必须同时判断 `code`、`success`、`message` 和 `data`。
## 四、契约约束与正确调用方式
### 编辑页初始化
```text
地址输入框 ← data.address
营业执照上传控件 ← data.licenseImageUrl
内部备注输入框 ← data.remark
并发版本 ← data.updateTime
```
不得把 `qualifications.find(item => item.qualType === "BUSINESS_LICENSE").imageUrl` 作为营业执照上传控件的回显值或顶层字段兜底。
### 更新规则对照
| 场景 | 请求 | 结果 |
|---|---|---|
| 更新三个字段 | 发送三个非空新值 + 原因 + 最新版本 | 更新成功;重新查询返回三个新值 |
| 地址留空,修改备注 | `address: " "`,`remark` 为新值 | 地址保持原值,备注更新 |
| 省略营业执照 | 不发送 `licenseImageUrl` | 顶层营业执照保持原值 |
| 清空营业执照 | `licenseImageUrl: ""` | 顶层营业执照清空,兼容影像同步清空 |
| 只改资质数组影像 | 发送 `qualifications[]`,不发送顶层字段 | 资质影像独立变化,顶层营业执照保持原值 |
| 使用旧版本 | 发送过期 `expectedUpdateTime` | 返回 `395014`,零写入 |
## 五、数据库行为
本节只描述接口可观察结果,不要求前端感知存储结构:
- 三字段更新成功后,再次查询详情返回新值,且主体 `updateTime` 推进。
- `address` 或 `remark` 留空时保存其他字段,重新查询仍返回原地址或原备注。
- 顶层 `licenseImageUrl` 更新成功后,详情根对象与兼容营业执照资质影像均返回新值。
- 只更新资质数组影像时,详情根对象 `licenseImageUrl` 保持原值。
- 权限、参数、状态或 `395014` 并发失败时,三字段和主体版本均不变化。
- 部署时已对可识别的历史营业执照影像完成一次性兼容初始化;之后详情根对象不再从资质数组动态派生。
## 六、边界行为
- 未登录或 Token 无效由 Gateway 拒绝,不产生业务写入。
- 无 `supplier:view` 时详情读取失败;无 `supplier:update` 或不属于允许写角色时更新返回 `395002`,零写入。
- 供应商不存在、已删除或不可见时返回 `395001`。
- `address` 或 `licenseImageUrl` 超过各自长度上限时返回 `400`,零写入。
- 缺少 `changeReason`、缺少 `expectedUpdateTime` 或没有实际变化时返回 `400`,零写入。
- 版本过期返回 `395014`;客户端必须刷新详情,不能自动覆盖。
- `ARCHIVED` 等不可修改状态继续由既有状态门禁拒绝。
- 业务失败可能仍使用 HTTP 200,客户端必须检查统一响应体。
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|---|---|---|
| `data.address` | 字段已存在,但本次编辑回归未锁定 | 原值、更新值及留空保留规则均已锁定 |
| `data.licenseImageUrl` | 根对象没有稳定权威回显 | 根对象返回可空权威值,不从 `qualifications[]` 反向派生 |
| `data.remark` | 字段已存在,但本次编辑回归未锁定 | 原值和更新值均可稳定回读 |
### 行为级对比
| 行为 | 改前 | 改后 |
|---|---|---|
| 编辑页初始化营业执照 | 可能从资质数组搜索影像 | 固定读取根对象 `licenseImageUrl` |
| 更新营业执照 | 根对象回读来源不稳定 | 提交顶层字段后,写后查询返回同一新值 |
| 单独修改资质影像 | 可能被误当成主体营业执照 | 不改变根对象营业执照权威值 |
## 六.7、影响评估
- **是否破坏向后兼容**:否。详情根对象增加可空字段并固定既有字段行为;旧客户端可忽略未知字段。
- **前端是否必须同步上线**:需要。营业执照上传控件必须改为读取并提交顶层 `licenseImageUrl`;地址和备注继续直接绑定根对象字段。
- **前端 workaround 清理点**:删除从 `qualifications[]` 搜索营业执照影像并覆盖表单值的逻辑。
- **QA 重点**:已有值回显、三字段更新后刷新、地址留空保留、资质影像与顶层营业执照相互独立、旧版本失败零写入。
## 七、不影响范围
- **仅影响**:管理后台供应商详情与编辑表单的地址、营业执照和内部备注。
- **零影响**:
- 接口路径、Gateway 路由和认证级别。
- 供应商既有角色、平台权限和数据范围。
- 生命周期状态机、审批、合同、收款账户和资源关系。
- 既有业务错误码、Redis、MQ、Feign 和其他服务。
- 小程序、C 端、Web、H5 和桌面端接口。
## 八、测试环境已验证
- 本地:供应商定向 62 项通过;`hl-resource-service` Reactor 全量 2,186 项通过,0 失败、0 错误,38 项既有条件跳过。
- 合并:后端 PR [#6645](https://git.1814.love:8443/wx/HL/pulls/6645) 已合并,合并提交为 `d206d1d577a3e3f8cec228e73e139fcb36c00fd5`。
- 部署:Deploy Panel API 任务 `954419f9` 成功,TEST 目标与实际提交均为上述合并提交;任务期 6 个采样均至少保持 2 个进程与 2 个健康启用实例匹配,0 个不可用采样。
- 真实 Gateway:唯一测试草稿依次验证原值回显、资质影像独立变化时顶层营业执照不变、三字段更新后回读、空地址保留、旧版本 `395014` 零写入及未认证拒绝。
- 清理:测试草稿已通过业务删除接口软删除并确认详情不可查询;仅保留系统规定的脱敏业务审计事实。
## 九、撤回
如后端撤回本次契约,前端在回退部署前停止依赖顶层 `licenseImageUrl`,并以同步发布的撤回 Changelog 为准;不得自行恢复未冻结的资质数组反向覆盖逻辑。地址与备注仍按既有 #6476 契约处理。
## 十、相关文档
- 关联 Issue:[#6643](https://git.1814.love:8443/wx/HL/issues/6643)
- 关联 PR:[#6645](https://git.1814.love:8443/wx/HL/pulls/6645)
- 既有地址与备注契约:[#6476](./27_6476_供应商详情补齐地址备注并统一表单校验-修改接口-管理后台.md)
- 当前前端状态:已消费(`frontend_status: verified`)。前端本就从顶层控件读执照、从未从资质数组推导;本次补齐编辑态 `licenseImageUrl` 顶层权威回显(快照比对:改携带新值/清空携带空串/未动不携带)。address/remark 沿用既有 #6476 语义(remark 回填快照比对、address 级联不回显),与本条一致未改动。
## 关联 / 联系人
### 链接
- **Issue**: [#6643](https://git.1814.love:8443/wx/HL/issues/6643)
- **PR**: [#6645](https://git.1814.love:8443/wx/HL/pulls/6645)
- **Merge commit**: [`d206d1d5`](https://git.1814.love:8443/wx/HL/commit/d206d1d577a3e3f8cec228e73e139fcb36c00fd5)
- **既有地址/备注契约**: [#6476](./27_6476_供应商详情补齐地址备注并统一表单校验-修改接口-管理后台.md)
### 联系人
- **后端负责人**: @lc
- **管理端负责人**: 待认领(`frontend_status: pending`)
@@ -0,0 +1,389 @@
---
schema: "hl-changelog/v2"
ticket: "6654"
title: "供应商合同字段可空与编辑信息入口"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "5fe4e6fa"
target_release: "v2.1"
verified_at: "2026-08-29"
status_note: "PR #6665 已合并 dev-v3;hl-resource-service 已通过 Deploy Panel 任务 3dde08b9 部署提交 65ac86e9312b22dd2fc7313049a693fdb0ccad59。真实 TEST Gateway 已验证合同业务字段全空登记、补全、清空、删除,changeReason 必填失败零写入,expectedUpdateTime 省略成功及过期值拒绝;测试草稿已清理。合同与结算 table 的页面顺序和消费映射待前端处理。"
updated_at: "2026-08-29"
base: "dev-v3"
---
# 供应商合同字段可空与编辑信息入口
供应商合同继续走独立登记接口,不并入供应商档案聚合。合同的 12 个业务字段现在全部可空,`changeReason` 继续必填;合同更新、删除的 `expectedUpdateTime` 改为可选,但一旦提供,过期版本仍会被拒绝。
管理端新建供应商时可完全不登记合同,取得 `supplierId` 后再补录;编辑页通过基本信息详情读取合同,通过既有账户接口读取和维护结算信息。页面按“资质证照 → 合同信息 → 结算信息”排列属于前端消费工作,当前状态为待前端处理。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---:|---|---|---|---|---|
| 1 | 独立登记供应商合同 | POST | `/admin/supplier/items/{supplierId}/contracts/add` | 修改请求 | 12 个合同业务字段全部可空,`changeReason` 仍必填 |
| 2 | 独立更新供应商合同 | PUT | `/admin/supplier/items/{supplierId}/contracts/{contractId}/update` | 修改请求 | 合同业务字段与新增一致且可清空,`expectedUpdateTime` 可选 |
| 3 | 独立删除供应商合同 | DELETE | `/admin/supplier/items/{supplierId}/contracts/{contractId}/del` | 修改请求 | `expectedUpdateTime` 可选,`changeReason` 仍必填 |
| 4 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 修改响应语义 | `contracts[]` 的全部业务字段允许返回 `null` |
## 三、接口详情
### 1. 独立登记供应商合同 `POST /admin/supplier/items/{supplierId}/contracts/add`
**VO**: `SupplierContractCreateReqVO / SupplierContractRespVO`
#### 使用场景
供应商已经创建但合同资料尚未齐全时,可先登记一条纯合同记录,之后再补充。若暂时不需要合同记录,供应商新建请求直接省略废弃的 `contracts` 字段即可。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
| `contractName` | Body | String | 否 | 最长 500 字符 | 合同名称 |
| `contractNo` | Body | String | 否 | 最长 100 字符 | 合同编号 |
| `contractType` | Body | String | 否 | `FRAME` / `SINGLE_TRIP` / `PURCHASE` | 合同类型 |
| `signDate` | Body | String | 否 | `yyyy-MM-dd` | 签署日期 |
| `startDate` | Body | String | 否 | `yyyy-MM-dd` | 有效期开始;仅与同时提供的结束日期做区间校验 |
| `endDate` | Body | String | 否 | `yyyy-MM-dd` | 有效期结束;两端都提供时不得早于开始日期 |
| `amount` | Body | String | 否 | 非负,最多 10 位整数和 2 位小数 | 合同金额 |
| `pricingMode` | Body | String | 否 | 最长 100 字符 | 计价方式 |
| `settleCycle` | Body | String | 否 | 最长 32 字符 | 结算周期说明 |
| `status` | Body | String | 否 | `DRAFT` / `ACTIVE` / `EXPIRED` | 合同状态 |
| `scanFileUrl` | Body | String | 否 | 最长 1000 字符 | 扫描件永久地址 |
| `remark` | Body | String | 否 | 最长 500 字符 | 合同备注 |
| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 字符 | 审计原因,不属于可空业务字段 |
#### 出参 `Result<SupplierContractRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.contractId` | String | 新合同雪花 ID |
| `data.contractName` 等 12 个业务字段 | 对应类型/null | 未填写的字段返回 `null` |
| `data.updateTime` | String | 服务端合同版本,格式 `yyyy-MM-dd HH:mm:ss` |
#### 请求示例
```json
{
"changeReason": "合同资料暂缺,先登记记录"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"contractId": "2094000000000000001",
"contractName": null,
"contractNo": null,
"contractType": null,
"signDate": null,
"startDate": null,
"endDate": null,
"amount": null,
"pricingMode": null,
"settleCycle": null,
"status": null,
"scanFileUrl": null,
"remark": null,
"updateTime": "2026-08-29 16:10:00"
}
}
```
#### 空数据 / 降级响应
请求体除 `changeReason` 外可以不包含任何字段;成功后返回合同对象,业务字段全部为 `null`。不要把 `null` 自动替换为默认合同类型、状态、日期或金额。
#### 错误响应
缺少审计原因时失败且不新增合同:
```json
{ "code": 400, "message": "变更原因不能为空", "success": false, "data": null }
```
#### 业务边界
- 要求可信管理员、`FINANCE`/`SUPER_ADMIN` 写角色和 `supplier:update` 平台权限。
- 合同登记不进入供应商审批,不推进供应商主体 `updateTime`。
- `POST /admin/supplier/items/add` 和供应商资料更新中的废弃 `contracts` 字段仍被忽略;前端必须先获得 `supplierId`,再按需调用本接口。
### 2. 独立更新供应商合同 `PUT /admin/supplier/items/{supplierId}/contracts/{contractId}/update`
**VO**: `SupplierContractUpdateReqVO / SupplierContractRespVO`
#### 使用场景
补齐、修改或清空一条已登记合同。该接口执行完整替换:未传或传 `null` 的业务字段会保存为 `null`,不是“保持原值”。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
| `contractId` | Path | String | 是 | 正整数 ID 字符串 | 目标合同 |
| 新增接口的 12 个业务字段 | Body | 对应类型 | 否 | 与新增一致 | 完整替换;省略即清空对应字段 |
| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 字符 | 审计原因 |
| `expectedUpdateTime` | Body | String | 否 | `yyyy-MM-dd HH:mm:ss` | 提供时必须等于当前合同版本;省略时由服务端锁串行更新 |
#### 出参 `Result<SupplierContractRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.contractId` | String | 合同雪花 ID |
| `data.contractName` 等 12 个业务字段 | 对应类型/null | 完整替换后的值,允许为 `null` |
| `data.updateTime` | String | 更新后的合同版本 |
#### 请求示例
```json
{
"contractName": "2026 年度框架合同",
"contractType": "FRAME",
"startDate": "2026-09-01",
"endDate": "2027-08-31",
"status": "ACTIVE",
"changeReason": "补齐已签署合同"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"contractId": "2094000000000000001",
"contractName": "2026 年度框架合同",
"contractType": "FRAME",
"startDate": "2026-09-01",
"endDate": "2027-08-31",
"status": "ACTIVE",
"updateTime": "2026-08-29 16:10:01"
}
}
```
#### 空数据 / 降级响应
仅发送 `changeReason` 可把 12 个业务字段全部清空。若替换后的业务载荷与当前记录完全相同,返回“未检测到合同实际变化”,不会伪造新版本。
#### 错误响应
提供过期版本时继续失败且零写入:
```json
{ "code": 395014, "message": "数据已被修改,请刷新后重试", "success": false, "data": null }
```
#### 业务边界
- `expectedUpdateTime` 省略不等于关闭并发保护;分布式合同锁和数据库行锁仍串行化同一合同写入。
- 客户端若选择发送版本,必须使用最近一次详情或写响应中的 `updateTime`。
- `changeReason` 始终必填,失败时合同和审计均不写入。
### 3. 独立删除供应商合同 `DELETE /admin/supplier/items/{supplierId}/contracts/{contractId}/del`
**VO**: `SupplierContractDeleteReqVO / Void`
#### 使用场景
删除不再保留的合同登记。删除为软删除,并记录完整审计原因。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
| `contractId` | Path | String | 是 | 正整数 ID 字符串 | 目标合同 |
| `expectedUpdateTime` | Body | String | 否 | `yyyy-MM-dd HH:mm:ss` | 提供时执行版本匹配 |
| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 字符 | 删除审计原因 |
#### 出参 `Result<Void>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | Integer | 成功时为 `200` |
| `success` | Boolean | 成功时为 `true` |
| `data` | null | 删除成功不返回业务对象 |
#### 请求示例
```json
{
"changeReason": "合同登记作废"
}
```
#### 响应示例
```json
{ "code": 200, "message": "成功", "success": true, "data": null }
```
#### 空数据 / 降级响应
`expectedUpdateTime` 可省略,但请求体不能省略,且必须包含非空 `changeReason`。
#### 错误响应
```json
{ "code": 400, "message": "变更原因不能为空", "success": false, "data": null }
```
#### 业务边界
- 提供的过期 `expectedUpdateTime` 仍返回 `395014`,不删除、不写审计。
- 删除成功后基本信息详情的 `contracts[]` 不再返回该记录。
- 权限、锁、幂等、审计和软删除边界均保持原有实现。
### 4. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
**VO**: `SupplierBasicInfoRespVO`
#### 使用场景
进入供应商编辑页时读取主体、资质和合同。前端将 `data.contracts` 绑定到“合同信息”table。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
无 Query 参数,无请求体。
#### 出参 `Result<SupplierBasicInfoRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.contracts` | Array | 当前未软删除合同,按 `contractId` 升序 |
| `data.contracts[].contractId` | String | 合同雪花 ID |
| `data.contracts[]` 的 12 个业务字段 | 对应类型/null | 合同登记未填写时返回 `null` |
| `data.contracts[].updateTime` | String | 合同当前版本 |
#### 请求示例
```http
GET /admin/supplier/items/2094000000000000000/basic-info/view
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2094000000000000000",
"contracts": [
{
"contractId": "2094000000000000001",
"contractName": null,
"contractType": null,
"startDate": null,
"endDate": null,
"status": null,
"updateTime": "2026-08-29 16:10:00"
}
]
}
}
```
#### 空数据 / 降级响应
没有合同登记时 `data.contracts` 返回空数组;单份合同没有填写的业务字段返回 `null`,二者含义不同。
#### 错误响应
```json
{ "code": 395001, "message": "供应商不存在", "success": false, "data": null }
```
#### 业务边界
- 只读接口要求可信管理员、可读角色和 `supplier:view` 平台权限。
- 查询不推进主体或合同版本,不触发审批和写副作用。
- 合同 table 的新增、编辑、删除分别调用前三个独立写接口,不把 `contracts` 回传给供应商聚合更新接口。
## 四、契约约束与正确调用方式
- 新建供应商可完全省略合同;若需登记,先调用 `POST /admin/supplier/items/add` 获取字符串 `supplierId`,再调用合同新增接口。
- 编辑页合同读取使用 `GET /admin/supplier/items/{supplierId}/basic-info/view` 的 `data.contracts`。
- 编辑页结算读取继续使用 `GET /admin/supplier/items/{supplierId}/account-info/list`;新增账户继续使用 `POST /admin/supplier/items/{supplierId}/bank-accounts/add`,其审批和主体状态门禁不变。
- 新建的 `initialAccounts[]` 与独立账户新增的 `accounts[]` 继续复用同一 `SupplierBankAccountReqVO`,可填写项一致:`accountType`、`bankName`、`bankBranch`、`accountNo`、`proofFileUrls`、`settleMode`、`accountPeriod`、`invoiceType`、`taxRate`。新建供应商最多携带 1 项初始账户。
- `supplierId`、`contractId`、`accountId` 和 `amount` 按字符串处理,禁止转为 JavaScript Number。
## 五、数据库行为
- Flyway migration `V20260829_002` 将 `supplier_contract.contract_name`、`contract_type`、`start_date`、`end_date`、`status` 从 `NOT NULL` 调整为可空。
- 合同创建、更新、删除继续在本服务 schema 内完成;审计与业务写同事务提交或回滚。
- 没有跨 schema 写入,没有新增 Redis、MQ、配置或路由变更。
## 六、边界行为
- 业务字段为空不等于审计字段可空:合同新增、更新、删除均必须有 `changeReason`。
- `expectedUpdateTime` 仅在合同更新和删除中可省略;提供时仍执行秒级版本比较。
- 合同日期只在 `startDate` 与 `endDate` 同时存在时校验先后顺序。
- 合同枚举字段为空时不校验;非空时仍只接受既有枚举值。
- 账户查询和新增的既有状态、审批、权限及敏感附件读取规则未改变。
## 六.6、修改前后对比
| 项目 | 修改前 | 修改后 |
|---|---|---|
| 合同名称、类型、开始日、结束日、状态 | 必填 | 可空 |
| 其余 7 个合同业务字段 | 可空 | 仍可空 |
| `changeReason` | 新增、更新、删除均必填 | 继续必填 |
| `expectedUpdateTime` | 更新、删除必填 | 更新、删除可选;提供过期值仍拒绝 |
| 结算信息字段模型 | 新建与独立账户新增复用同一 VO | 不变,编辑页继续复用既有账户接口 |
## 六.7、影响评估
- **是否破坏向后兼容**: 否;原先完整载荷继续有效,旧客户端继续发送版本也有效。
- **前端是否必须同步上线**: 是;需要增加合同/结算 table、允许合同业务控件为空,并保留 `changeReason` 必填。
- **前端 workaround 清理点**: 删除合同业务字段的前端强制必填;不要删除 `changeReason` 校验。
## 七、不影响范围
- 不修改供应商新建、更新、提交请求中的废弃 `contracts` 聚合字段语义。
- 不修改结算账户字段、审批、状态机、默认账户、权限或接口路径。
- 不修改供应商主体的 `changeReason`、`expectedUpdateTime` 必填规则。
- 不新增业务错误码、Gateway 路由、Redis、MQ 或 Nacos 配置。
## 八、测试环境已验证
- 合同信息整体省略不影响供应商草稿创建;空业务字段合同可登记并由详情读回。
- 合同可在不传 `expectedUpdateTime` 时补全、清空和删除;传入过期版本返回并发失败且零写入。
- 新增、更新、删除缺少 `changeReason` 均失败且零写入。
- 编辑供应商后合同和初始结算账户摘要保持;结算读取入口可正常访问。
- TEST 验收产生的供应商草稿、合同和初始账户已软删除,并通过详情与结算入口读回确认不可见。
## 十、相关文档
- Issue: [#6654](https://git.1814.love:8443/wx/HL/issues/6654)
- PR: [#6665](https://git.1814.love:8443/wx/HL/pulls/6665)
- 合并提交: `65ac86e9312b22dd2fc7313049a693fdb0ccad59`
## 关联 / 联系人
- **后端负责人**: @lc
- **前端状态**: 已消费(`frontend_status: verified`)。页面「资质→合同→结算」排列现状已符(资质/合同在基本信息区、结算独立账户 Tab),DetailModal 合同表格对全 null 已 EMPTY/renderStatusTag 兜底安全;实质改动在 SupplierContractEditModal——12 业务字段全可空(去 contractName/contractType/status/startDate 必填、status 不再默认 DRAFT)、日期仅两端同填校验、expectedUpdateTime 编辑仅拿到版本才携带,changeReason 仍必填。
@@ -0,0 +1,359 @@
---
schema: "hl-changelog/v2"
ticket: "6669"
title: "供应商草稿结算信息编辑与可选字段清空"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: "v2.1"
verified_at: "2026-08-29"
status_note: "#6669 与补充工单 #6676 已合并 dev-v3;最终提交 af5ea05df3d7490c97e6f4329c145a4014396bee 已由 Deploy Panel 任务 9be3a694 部署 TEST。真实 Gateway 已验证草稿结算创建、回读、修改、六个可选字段清空、整项清空、并发失败零写入和数据清理。当前状态:待前端处理。"
updated_at: "2026-08-29"
base: "dev-v3"
---
# 供应商草稿结算信息编辑与可选字段清空
供应商编辑接口新增 `initialAccounts` 完整快照。新建时登记的初始结算信息现在可在草稿编辑页读回、修改或清空;`changeReason` 和 `expectedUpdateTime` 继续必填。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---:|---|---|---|---|---|
| 1 | 编辑供应商 | PUT | `/admin/supplier/items/{supplierId}/update` | 修改请求与响应 | 新增可选 `initialAccounts` 完整快照,仅草稿态可维护 |
| 2 | 查询供应商账户列表 | GET | `/admin/supplier/items/{supplierId}/account-info/list` | 修改响应语义 | 草稿供应商可读回其 `DRAFT` 初始账户 |
| 3 | 查询收款账户详情 | GET | `/admin/supplier/bank-accounts/{accountId}/view` | 修改响应语义 | 所属供应商为草稿时可读回 `DRAFT` 账户详情 |
## 三、接口详情
### 1. 编辑供应商 `PUT /admin/supplier/items/{supplierId}/update`
**VO**: `SupplierUpdateReqVO / SupplierWriteRespVO`
#### 使用场景
在供应商草稿编辑页维护与新建供应商相同的一项初始结算信息。省略 `initialAccounts` 不修改结算信息,空数组清空,1 项执行完整替换。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
| `initialAccounts` | Body | Array | 否 | 最多 1 项 | 省略不修改;`[]` 清空;1 项完整替换 |
| `initialAccounts[].accountType` | Body | String | 项内是 | `CORPORATE` / `PERSONAL` | 账户类型 |
| `initialAccounts[].bankName` | Body | String | 项内是 | 最长 500 字符 | 开户银行 |
| `initialAccounts[].accountNo` | Body | String | 项内是 | 规范化后 8 至 32 位数字 | 收款账号 |
| `initialAccounts[].bankBranch` | Body | String/null | 否 | 最长 500 字符 | 省略或 `null` 可清空 |
| `initialAccounts[].proofFileUrls` | Body | Array/null | 否 | 最多 20 项 HTTPS 地址 | 省略、`null` 或 `[]` 可清空 |
| `initialAccounts[].settleMode` | Body | String/null | 否 | `PREPAY` / `MONTHLY` / `SINGLE` | 可清空 |
| `initialAccounts[].accountPeriod` | Body | String/null | 否 | 最长 50 字符 | 仅月结时填写,可清空 |
| `initialAccounts[].invoiceType` | Body | String/null | 否 | `SPECIAL` / `NORMAL` / `NONE` | 可清空 |
| `initialAccounts[].taxRate` | Body | String/null | 否 | 0% 至 100%,最多两位小数 | 可清空 |
| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 字符 | 审计原因,继续必填 |
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 供应商主体并发版本,继续必填 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.supplierId` | String | 供应商雪花 ID |
| `data.status` | String | 当前为 `DRAFT` |
| `data.initialAccounts` | Array | 保存后的 0 或 1 项账户摘要 |
| `data.updateTime` | String | 新的供应商并发版本 |
雪花 ID 均按字符串处理。
#### 请求示例
```json
{
"changeReason": "维护草稿结算信息",
"expectedUpdateTime": "2026-08-29 17:01:00",
"initialAccounts": [
{
"accountType": "PERSONAL",
"bankName": "示例银行",
"accountNo": "6222000012345678"
}
]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2094000000000000000",
"status": "DRAFT",
"initialAccounts": [
{
"accountId": "2094000000000000001",
"accountType": "PERSONAL",
"bankName": "示例银行",
"bankBranch": null,
"accountNo": "6222000012345678",
"settleMode": null,
"accountPeriod": null,
"invoiceType": null,
"taxRate": null
}
],
"updateTime": "2026-08-29 17:01:01"
}
}
```
#### 空数据 / 降级响应
- 省略 `initialAccounts`:不修改现有初始账户。
- 传 `initialAccounts: []`:软删除草稿初始账户,写响应返回空数组。
- 1 项中省略 6 个可选字段:`bankBranch`、`proofFileUrls`、`settleMode`、`accountPeriod`、`invoiceType`、`taxRate` 均清空;附件在读取响应中表现为空数组。
#### 错误响应
缺少审计或并发字段时失败且账户零写入:
```json
{ "code": 400, "message": "变更原因不能为空", "success": false, "data": null }
```
```json
{ "code": 400, "message": "expectedUpdateTime不能为空", "success": false, "data": null }
```
过期版本继续返回既有并发错误:
```json
{ "code": 395014, "message": "数据已被他人修改,请刷新后重试", "success": false, "data": null }
```
#### 业务边界
- `initialAccounts` 仅允许供应商为 `DRAFT` 时维护;否则返回既有状态错误 `395005`,账户零写入。
- 同账号完整替换保留原 `accountId`;账号全局唯一、事务、行锁、幂等和账户审计保持不变。
- 生效后的账户继续走独立新增与审批接口,供应商编辑不得绕过账户状态机。
### 2. 查询供应商账户列表 `GET /admin/supplier/items/{supplierId}/account-info/list`
**VO**: `SupplierAccountInfoRespVO / SupplierBankAccountRespVO`
#### 使用场景
进入供应商编辑页时加载“结算信息”table。供应商为草稿时,列表包含其初始 `DRAFT` 账户。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
无 Query 参数,无请求体。
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.supplierId` | String | 供应商雪花 ID |
| `data.bankAccounts` | Array | 当前可读账户列表 |
| `data.bankAccounts[].status` | String | 草稿初始账户为 `DRAFT` |
| `data.bankAccounts[].isDefault` | String | 草稿初始账户为 `NO` |
| `data.updateTime` | String | 供应商主体并发版本 |
#### 请求示例
```http
GET /admin/supplier/items/2094000000000000000/account-info/list
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2094000000000000000",
"bankAccounts": [
{
"accountId": "2094000000000000001",
"accountType": "PERSONAL",
"bankName": "示例银行",
"bankBranch": null,
"accountNo": "6222000012345678",
"proofFileUrls": [],
"settleMode": null,
"accountPeriod": null,
"invoiceType": null,
"taxRate": null,
"status": "DRAFT",
"isDefault": "NO"
}
],
"updateTime": "2026-08-29 17:01:01"
}
}
```
#### 空数据 / 降级响应
没有可读账户时 `data.bankAccounts` 返回空数组。可选文本字段为空时返回 `null`;已获附件读取权限且附件为空时 `proofFileUrls` 返回空数组。
#### 错误响应
```json
{ "code": 395001, "message": "供应商不存在", "success": false, "data": null }
```
#### 业务边界
- 仅当供应商当前为 `DRAFT` 时才扩展读取草稿账户;其他状态的既有可见范围不变。
- 证明附件继续受独立权限和同步敏感读取审计约束;无权时不序列化附件原值。
- 查询不推进供应商或账户版本,不产生业务写副作用。
### 3. 查询收款账户详情 `GET /admin/supplier/bank-accounts/{accountId}/view`
**VO**: `SupplierBankAccountDetailRespVO`
#### 使用场景
草稿编辑页需要查看单个初始账户完整字段时,按列表返回的字符串 `accountId` 查询详情。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `accountId` | Path | String | 是 | 正整数 ID 字符串 | 目标账户 |
无 Query 参数,无请求体。
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.accountId` | String | 账户雪花 ID |
| `data.accountType` 等账户字段 | 对应类型/null | 完整账户业务值,6 个可选字段允许为空 |
| `data.status` | String | 草稿初始账户为 `DRAFT` |
| `data.isDefault` | String | 草稿初始账户为 `NO` |
| `data.updateTime` | String | 账户当前版本 |
#### 请求示例
```http
GET /admin/supplier/bank-accounts/2094000000000000001/view
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"accountId": "2094000000000000001",
"accountType": "PERSONAL",
"bankName": "示例银行",
"bankBranch": null,
"accountNo": "6222000012345678",
"proofFileUrls": [],
"settleMode": null,
"accountPeriod": null,
"invoiceType": null,
"taxRate": null,
"status": "DRAFT",
"isDefault": "NO",
"updateTime": "2026-08-29 17:01:01"
}
}
```
#### 空数据 / 降级响应
目标账户不存在、已软删除或不在当前供应商状态允许的读取范围时,不返回部分对象;统一按不存在失败关闭。
#### 错误响应
```json
{ "code": 395001, "message": "供应商不存在", "success": false, "data": null }
```
#### 业务边界
- 草稿详情可见性由所属供应商当前状态决定,不能仅凭 `accountId` 绕过主体边界。
- 完整账号沿用当前管理端授权语义;证明附件仍需独立权限与同步审计。
- 详情查询不改变账户、供应商、审批或默认账户状态。
## 四、契约约束与正确调用方式
- 编辑页先调用账户列表接口加载结算 table,再把用户实际编辑后的 0 或 1 项完整快照放入供应商更新请求的 `initialAccounts`。
- 用户未操作结算区域时可以省略 `initialAccounts`,避免无意义改写;明确清空时必须发送空数组。
- 保存必须同时发送非空 `changeReason` 与最近读取到的供应商 `expectedUpdateTime`。
- 账户业务字段使用与新建供应商一致的字段名;`supplierId`、`accountId` 按字符串处理,禁止转为 JavaScript Number。
## 五、数据库行为
- 草稿账户完整替换会把省略的 6 个可选字段持久化为空;重新查询不再回读旧值。
- 空数组对草稿初始账户执行软删除;供应商、账户和审计仍在同一服务事务内提交或回滚。
- 本次没有新增 migration、跨 schema 写入、Redis、MQ 或配置行为。
## 六、边界行为
- `accountType`、`bankName`、`accountNo` 仍为账户项必填字段;“可清空”只适用于其余 6 个可选字段。
- `MONTHLY` 与 `accountPeriod`、开票类型与税率的既有组合校验继续生效。
- 更新完整快照时未提供的可选字段保存为空,不是保持数据库旧值。
- 写失败时供应商、账户和审计均在同一事务回滚;过期版本和非草稿状态均零写入。
## 六.6、修改前后对比
| 项目 | 修改前 | 修改后 |
|---|---|---|
| 供应商编辑请求 | 无 `initialAccounts`,不能维护新建时的初始结算信息 | 可选完整快照:省略不改、空数组清空、1 项替换 |
| 草稿账户列表/详情 | `DRAFT` 初始账户不在管理端账户读取范围 | 所属供应商为 `DRAFT` 时可读回 |
| 可选字段清空 | 实体值虽置空,但默认更新策略可能保留数据库旧值 | 6 个可选字段显式持久化为空 |
| 审计与并发字段 | `changeReason`、`expectedUpdateTime` 必填 | 继续必填 |
## 六.7、影响评估
- **是否向后兼容**:是;不发送 `initialAccounts` 的原调用方保持原更新语义。
- **前端是否需要接入**:是;编辑页结算 table 需调用账户列表并按完整快照保存。
- **状态机影响**:无;仅草稿态开放聚合维护,其他状态继续走独立账户审批。
- **撤回影响**:撤回后编辑页应停止发送 `initialAccounts`,草稿账户也不再通过账户列表/详情读回。
## 七、不影响范围
- 不修改非草稿供应商的独立账户新增、审批、默认账户和账户状态机。
- 不修改合同独立登记接口、供应商提交审批流程或合同字段可空契约。
- 不新增错误码、数据库 migration、Gateway 路由、Redis、MQ、Nacos 或配置变更。
- 不修改任何前端源码;页面 table 排列和字段消费由前端按本契约处理。
## 八、测试环境已验证
- 新建草稿携带完整初始结算信息后,列表可读回相同字段。
- 草稿编辑把完整账户改为仅保留三个必填字段后,6 个可选字段保存并重新回读为空,账户 ID 保持不变。
- 缺少 `changeReason`、缺少 `expectedUpdateTime` 和使用过期版本均失败且账户零写入。
- 发送空数组后写响应和列表均为空;测试供应商删除后详情不可读,所有可恢复测试数据已清理。
## 十、相关文档
- Issue [#6669](https://git.1814.love:8443/wx/HL/issues/6669)
- 补充 Issue [#6676](https://git.1814.love:8443/wx/HL/issues/6676)
- PR [#6674](https://git.1814.love:8443/wx/HL/pulls/6674),合并提交 `35dba63d6a92159d933b38385fee057f617bdfed`
- PR [#6677](https://git.1814.love:8443/wx/HL/pulls/6677),合并提交 `af5ea05df3d7490c97e6f4329c145a4014396bee`
## 关联 / 联系人
- **后端负责人**:@lc
- **前端负责人**:@mmg
- **当前状态**:待前端处理