比较提交
52
次代码提交
| 作者 | SHA1 | 提交日期 | |
|---|---|---|---|
|
|
cfc3aa8b1e | ||
|
|
72949d869c | ||
|
|
a031039140 | ||
|
|
c9efecebc5 | ||
|
|
4c12c1db01 | ||
|
|
937f353b41 | ||
|
|
43bf3936d1 | ||
|
|
33a8f7cb5b | ||
|
|
5ce53bcd47 | ||
|
|
430d825904 | ||
|
|
2b60fc3323 | ||
|
|
070c937a08 | ||
|
|
e28f675c8f | ||
|
|
787b9a007b | ||
|
|
d81fea420a | ||
|
|
0e1ab992e8 | ||
|
|
95212ad9a5 | ||
|
|
2ebb4644a6 | ||
|
|
17bb0ef2bf | ||
|
|
5c52926269 | ||
|
|
a45b51d8a8 | ||
|
|
73a4518181 | ||
|
|
8821d1df6f | ||
|
|
51708dc7a6 | ||
|
|
b28bd34ceb | ||
|
|
1ba3299afa | ||
|
|
bd235c2e1d | ||
|
|
eb628f56b0 | ||
|
|
b54c4cc8d7 | ||
|
|
4621f11e36 | ||
|
|
1f608928b7 | ||
|
|
a4aba37568 | ||
|
|
60a28339e0 | ||
|
|
d3b5c0e8c9 | ||
|
|
0474e6ff78 | ||
|
|
9aace47441 | ||
|
|
605859b12f | ||
|
|
734a9b1112 | ||
|
|
20272bb706 | ||
|
|
59090c889b | ||
|
|
07387e506f | ||
|
|
e4f7c5cb0a | ||
|
|
cf3fe30a0f | ||
|
|
e91101c037 | ||
|
|
ebc2fecbfd | ||
|
|
e362441e9c | ||
|
|
81aab7abb0 | ||
|
|
4f81ef1cf9 | ||
|
|
ca11783f72 | ||
|
|
3a2c1ae38d | ||
|
|
7b37f31df6 | ||
|
|
49f8b6d0a0 |
@@ -7,12 +7,12 @@ author: "lc(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "主工单 #6350 及补充工单 #6384、#6389、#6395、#6399、#6409 的六个 PR 均已合并 dev-v3。精确提交 b62054035a139e3c689179ef75e2d052d08dde52 已部署 TEST;真实管理员经 Gateway 完成未认证、读取、失败零写入、创建、CAS 更新、数据库和 Redis 一致性验收。#6422 已按 CHANGELOG_TEMPLATE.md 补齐四个接口的前端联调契约。"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "7acd1cb0"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-08-26"
|
||||
status_note: "主工单 #6350 及补充工单 #6384、#6389、#6395、#6399、#6409 的六个 PR 均已合并 dev-v3。精确提交 b62054035a139e3c689179ef75e2d052d08dde52 已部署 TEST;真实管理员经 Gateway 完成未认证、读取、失败零写入、创建、CAS 更新、数据库和 Redis 一致性验收。#6422 已按 CHANGELOG_TEMPLATE.md 补齐四个接口的前端联调契约,三级联动由 mmg 以 ref 7acd1cb0 完成验证;#6438 再按 TEST 代码修正种子父链、聚合节点、可扩展编码命名空间、异步缓存刷新和 extension 更新限制。"
|
||||
updated_at: "2026-08-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
@@ -23,7 +23,7 @@ base: "dev-v3"
|
||||
>
|
||||
> **PR**: #6359、#6387、#6393、#6398、#6402、#6412
|
||||
>
|
||||
> **Issue**: #6350、#6384、#6389、#6395、#6399、#6409
|
||||
> **Issue**: #6350、#6384、#6389、#6395、#6399、#6409、#6422、#6438
|
||||
>
|
||||
> **日期**: 2026-08-26
|
||||
>
|
||||
@@ -36,6 +36,8 @@ base: "dev-v3"
|
||||
- 本次补文档不改变 TEST 上已经部署的接口行为;它把原 Changelog 中分散的说明整理为可直接联调的完整契约。
|
||||
- 前端不得再把行政区划 ID 当 JavaScript number:响应中的所有行政区划 ID 均为 JSON string,缺失层级保持真正的 `null`。
|
||||
- 三级联动不允许通过行政区编码截位推断父子关系;必须逐级调用 `children`,编辑回显先调用 `path`。
|
||||
- `PREFECTURE` 是第二层导航层,不保证一定是可选择的法定地级行政区;直辖市等链路会返回 `AGGREGATION` 聚合节点,必须继续下钻且不得作为最终业务值。
|
||||
- 当前没有单节点详情接口,读响应也不返回 `extension`;更新接口省略或传 `null` 都会清空该字段,前端不能把它理解成“保持原值”。
|
||||
- 业务失败可能仍使用 HTTP 200,调用方必须同时判断响应体 `code`、`success`、`message` 和 `data`。
|
||||
|
||||
---
|
||||
@@ -46,10 +48,10 @@ base: "dev-v3"
|
||||
|
||||
- 省、市、县三级选择器逐级加载;
|
||||
- 编辑历史业务数据时,从任意节点恢复完整父链;
|
||||
- 具有专用权限的管理员创建自定义行政区或导航聚合节点;
|
||||
- 具有专用权限的管理员创建自定义行政区、带来源证据的新法定版本或导航聚合节点;
|
||||
- 使用 `rowVersion` 防止两位管理员并发更新时互相覆盖。
|
||||
|
||||
接口当前只开放省、市、县三级写入,层级代码依次为 `PROVINCE`、`PREFECTURE`、`COUNTY`。接口返回的示例 ID 仅用于展示字符串类型,前端不得硬编码。
|
||||
接口当前只开放省、市、县三级写入,层级代码依次为 `PROVINCE`、`PREFECTURE`、`COUNTY`。第二层可能是 `AGGREGATION` 导航分组,不等同于可提交的法定地级行政区。下文读取示例使用 TEST migration 中的北京链路 `1 → 35 → 376`,仅用于说明响应结构,业务代码仍不得硬编码。
|
||||
|
||||
---
|
||||
|
||||
@@ -59,7 +61,7 @@ base: "dev-v3"
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 单级联动列表 | GET | `/admin/region/children` | 新增接口 | 加载根层省级或指定父节点的当前有效直接子节点 |
|
||||
| 2 | 父链回显 | GET | `/admin/region/{id}/path` | 新增接口 | 从任意省/市/县节点恢复省级根到目标节点的完整路径 |
|
||||
| 3 | 创建行政区划节点 | POST | `/admin/region` | 新增接口 | 创建自定义行政区或不可选择的导航聚合节点 |
|
||||
| 3 | 创建行政区划节点 | POST | `/admin/region` | 新增接口 | 创建自定义行政区、新法定版本或不可选择的导航聚合节点 |
|
||||
| 4 | 更新行政区划节点 | PUT | `/admin/region/{id}` | 新增接口 | 使用完整可变字段和 `rowVersion` 执行 CAS 更新 |
|
||||
|
||||
---
|
||||
@@ -119,7 +121,7 @@ base: "dev-v3"
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/region/children?parentId=2090000000000000001&selectedId=2090000000000000035 HTTP/1.1
|
||||
GET /admin/region/children?parentId=1&selectedId=35 HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <管理员令牌>
|
||||
Accept: application/json
|
||||
@@ -141,30 +143,30 @@ Accept: application/json
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"parentId": "2090000000000000001",
|
||||
"parentId": "1",
|
||||
"items": [
|
||||
{
|
||||
"id": "2090000000000000035",
|
||||
"parentId": "2090000000000000001",
|
||||
"codeStandard": "GB/T 2260",
|
||||
"regionCode": "110100",
|
||||
"id": "35",
|
||||
"parentId": "1",
|
||||
"codeStandard": "HL_GROUP",
|
||||
"regionCode": "HL-GROUP-110000",
|
||||
"levelCode": "PREFECTURE",
|
||||
"depth": 2,
|
||||
"name": "北京市",
|
||||
"name": "北京市辖区",
|
||||
"shortName": null,
|
||||
"nodeKind": "ADMINISTRATIVE",
|
||||
"nodeKind": "AGGREGATION",
|
||||
"navigable": true,
|
||||
"selectable": true,
|
||||
"selectable": false,
|
||||
"hasChildren": true,
|
||||
"currentlyEffective": true,
|
||||
"status": "ACTIVE",
|
||||
"validFrom": "2025-12-31",
|
||||
"validTo": null,
|
||||
"sortOrder": 100,
|
||||
"sortOrder": 2000000001,
|
||||
"rowVersion": 0
|
||||
}
|
||||
],
|
||||
"defaultId": "2090000000000000035"
|
||||
"defaultId": "35"
|
||||
},
|
||||
"traceId": "a1b2c3d4-e5f6-7890",
|
||||
"success": true
|
||||
@@ -173,14 +175,14 @@ Accept: application/json
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
父节点存在但没有当前有效直接子节点时,返回成功空数组:
|
||||
父节点存在但没有当前有效直接子节点时,返回成功空数组。Redis 读取失败时服务会降级查数据库,成功结构不变:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"parentId": "2090000000000000035",
|
||||
"parentId": "376",
|
||||
"items": [],
|
||||
"defaultId": null
|
||||
},
|
||||
@@ -207,6 +209,7 @@ Accept: application/json
|
||||
- `selectedId` 不在本次 `items` 中时不会报错,而是稳定回退到排序后的第一项;空列表回退为 `null`。
|
||||
- 排序固定为 `sortOrder`、`regionCode`、`id`,前端不得另用行政区编码推断顺序或父子关系。
|
||||
- `navigable` 控制能否继续向下加载,`selectable` 控制能否作为最终值;两者不能相互替代。
|
||||
- `defaultId` 只表示本级默认导航项,不保证 `selectable=true`;例如北京第二层默认项 `"35"` 是不可提交的聚合节点。
|
||||
- 未登录返回业务码 `401`;`parentId=0` 等非正整数参数返回业务码 `400`。
|
||||
|
||||
---
|
||||
@@ -217,7 +220,7 @@ Accept: application/json
|
||||
|
||||
#### 使用场景
|
||||
|
||||
编辑已有业务数据时,后端只保存了一个最终行政区 ID。前端先调用本接口得到完整省/市/县父链,再按父链逐层调用 `children` 加载每一级可选项。
|
||||
编辑已有业务数据时,后端只保存了一个最终行政区 ID。前端先调用本接口得到完整三级父链,再按父链逐层调用 `children` 加载每一级导航项。父链的第二层可能是不可选择的 `AGGREGATION`,不能仅凭 `prefectureId` 字段名判断它一定是法定地级行政区。
|
||||
|
||||
#### 入参
|
||||
|
||||
@@ -235,9 +238,10 @@ Accept: application/json
|
||||
| `code` | Integer | `200` 表示成功;业务失败读取具体业务码 |
|
||||
| `message` | String | 响应说明 |
|
||||
| `success` | Boolean | 仅当 `code=200` 时为 `true` |
|
||||
| `traceId` | String / null | 链路追踪 ID;报错排查时提供给后端 |
|
||||
| `data.selectedId` | String | 调用方传入并成功解析的目标节点 ID |
|
||||
| `data.provinceId` | String | 父链中的省级 ID |
|
||||
| `data.prefectureId` | String / null | 父链中的地级 ID;目标为省级时为 `null` |
|
||||
| `data.prefectureId` | String / null | 父链第二层 ID,可能是地级行政区或聚合导航节点;目标为省级时为 `null` |
|
||||
| `data.countyId` | String / null | 父链中的县级 ID;目标高于县级时为 `null` |
|
||||
| `data.path` | Array | 从省级根到目标节点的完整有序父链 |
|
||||
| `data.path[].id` | String | 节点 ID,始终为字符串 |
|
||||
@@ -262,7 +266,7 @@ Accept: application/json
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/region/2090000000000000376/path HTTP/1.1
|
||||
GET /admin/region/376/path HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <管理员令牌>
|
||||
Accept: application/json
|
||||
@@ -275,13 +279,13 @@ Accept: application/json
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"selectedId": "2090000000000000376",
|
||||
"provinceId": "2090000000000000001",
|
||||
"prefectureId": "2090000000000000035",
|
||||
"countyId": "2090000000000000376",
|
||||
"selectedId": "376",
|
||||
"provinceId": "1",
|
||||
"prefectureId": "35",
|
||||
"countyId": "376",
|
||||
"path": [
|
||||
{
|
||||
"id": "2090000000000000001",
|
||||
"id": "1",
|
||||
"parentId": null,
|
||||
"codeStandard": "GB/T 2260",
|
||||
"regionCode": "110000",
|
||||
@@ -297,32 +301,32 @@ Accept: application/json
|
||||
"status": "ACTIVE",
|
||||
"validFrom": "2025-12-31",
|
||||
"validTo": null,
|
||||
"sortOrder": 100,
|
||||
"sortOrder": 10,
|
||||
"rowVersion": 0
|
||||
},
|
||||
{
|
||||
"id": "2090000000000000035",
|
||||
"parentId": "2090000000000000001",
|
||||
"codeStandard": "GB/T 2260",
|
||||
"regionCode": "110100",
|
||||
"id": "35",
|
||||
"parentId": "1",
|
||||
"codeStandard": "HL_GROUP",
|
||||
"regionCode": "HL-GROUP-110000",
|
||||
"levelCode": "PREFECTURE",
|
||||
"depth": 2,
|
||||
"name": "北京市",
|
||||
"name": "北京市辖区",
|
||||
"shortName": null,
|
||||
"nodeKind": "ADMINISTRATIVE",
|
||||
"nodeKind": "AGGREGATION",
|
||||
"navigable": true,
|
||||
"selectable": true,
|
||||
"selectable": false,
|
||||
"hasChildren": true,
|
||||
"currentlyEffective": true,
|
||||
"status": "ACTIVE",
|
||||
"validFrom": "2025-12-31",
|
||||
"validTo": null,
|
||||
"sortOrder": 100,
|
||||
"sortOrder": 2000000001,
|
||||
"rowVersion": 0
|
||||
},
|
||||
{
|
||||
"id": "2090000000000000376",
|
||||
"parentId": "2090000000000000035",
|
||||
"id": "376",
|
||||
"parentId": "35",
|
||||
"codeStandard": "GB/T 2260",
|
||||
"regionCode": "110101",
|
||||
"levelCode": "COUNTY",
|
||||
@@ -337,18 +341,19 @@ Accept: application/json
|
||||
"status": "ACTIVE",
|
||||
"validFrom": "2025-12-31",
|
||||
"validTo": null,
|
||||
"sortOrder": 100,
|
||||
"sortOrder": 20,
|
||||
"rowVersion": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"traceId": "a1b2c3d4-e5f6-7890",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口不返回“成功但空父链”。目标不存在、父链不完整或超过安全深度时使用错误响应,不返回部分路径。
|
||||
本接口不返回“成功但空父链”。目标不存在、父链不完整或超过当前三级安全边界时使用错误响应,不返回部分路径。Redis 不可用时整条父链降级为同一数据库事务快照读取,不拼接缓存与数据库的半条路径。
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -376,7 +381,8 @@ Accept: application/json
|
||||
|
||||
- `path` 必须从省级根开始,`parentId`、`depth` 和 `levelCode` 逐级连续,最后一个节点必须等于 `selectedId`。
|
||||
- 目标为省级时 `prefectureId`、`countyId` 均为 `null`;目标为地级时只有 `countyId=null`,绝不会返回字符串 `"null"`。
|
||||
- 历史停用节点允许回显,节点自身 `currentlyEffective=false`;前端只能展示历史值,不能把它重新加入当前可选列表。
|
||||
- `prefectureId` 只是路径第二层快捷 ID;直辖市等链路可指向 `nodeKind=AGGREGATION && selectable=false` 的聚合节点。
|
||||
- 历史停用或过期节点允许回显,节点自身 `currentlyEffective=false`,且后端会把该节点的 `navigable`、`selectable` 强制返回 `false`;前端只能展示历史值,不能把它重新加入当前可选列表。
|
||||
- 父链异常时整体失败,不返回可被误用的部分路径。
|
||||
- 未登录返回业务码 `401`;不存在节点返回 `210801`。
|
||||
|
||||
@@ -388,14 +394,14 @@ Accept: application/json
|
||||
|
||||
#### 使用场景
|
||||
|
||||
具有 `system:region:create` 权限的管理员创建自定义业务行政区,或创建只用于层级导航、不可作为最终业务值的聚合节点。
|
||||
具有 `system:region:create` 权限的管理员可以创建自定义业务行政区、带完整来源证据且不与旧版本重叠的 `GB/T 2260` 新版本,或只用于层级导航、不可作为最终业务值的聚合节点。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `Authorization` | Header | String | ✅ | `Bearer <token>` 且具有 `system:region:create` | 管理端登录令牌 |
|
||||
| `codeStandard` | Body | String | ✅ | 1~32 字符;聚合节点必须为 `HL_GROUP` | 编码命名空间,自定义行政区推荐 `HL_CUSTOM` |
|
||||
| `codeStandard` | Body | String | ✅ | 1~32 字符;服务端去空白并转大写;聚合节点必须为 `HL_GROUP` | 可扩展编码命名空间,自定义行政区推荐 `HL_CUSTOM` |
|
||||
| `regionCode` | Body | String | ✅ | 1~64 字符;首字符为字母或数字;仅含字母、数字、`.`、`_`、`:`、`-` | 编码标准内的唯一地区代码;服务端转大写 |
|
||||
| `parentId` | Body | String / null | ❌ | 十进制正整数;创建省级根节点时为 `null` | 父节点 ID |
|
||||
| `levelCode` | Body | String | ✅ | 1~32 字符;省/市/县必须匹配派生深度 | `PROVINCE`、`PREFECTURE`、`COUNTY` |
|
||||
@@ -421,6 +427,7 @@ Accept: application/json
|
||||
| `code` | Integer | `200` 表示创建成功 |
|
||||
| `message` | String | 响应说明 |
|
||||
| `success` | Boolean | 创建成功为 `true` |
|
||||
| `traceId` | String / null | 链路追踪 ID;报错排查时提供给后端 |
|
||||
| `data` | String | 新节点 ID,始终为 JSON string |
|
||||
|
||||
#### 请求示例
|
||||
@@ -434,7 +441,7 @@ Content-Type: application/json
|
||||
{
|
||||
"codeStandard": "HL_CUSTOM",
|
||||
"regionCode": "DEMO-COUNTY-001",
|
||||
"parentId": "2090000000000000035",
|
||||
"parentId": "35",
|
||||
"levelCode": "COUNTY",
|
||||
"nodeKind": "ADMINISTRATIVE",
|
||||
"name": "示例业务区",
|
||||
@@ -460,13 +467,14 @@ Content-Type: application/json
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": "2090000000000001001",
|
||||
"traceId": "a1b2c3d4-e5f6-7890",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口成功时一定返回字符串 ID,不存在 `data=null` 的成功语义。失败时不创建节点。
|
||||
本接口成功时一定返回字符串 ID,不存在 `data=null` 的成功语义。失败时事务回滚且不创建节点;本接口没有“Redis 不可用仍受理写入”的公开降级承诺。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
@@ -486,7 +494,8 @@ Content-Type: application/json
|
||||
- 当前最大写入深度为 3;省、市、县层级必须分别使用 `PROVINCE`、`PREFECTURE`、`COUNTY`。
|
||||
- 父节点必须存在、可导航、有效期完整覆盖子节点,并且不会让写入深度超过三级。
|
||||
- `AGGREGATION` 必须同时满足 `codeStandard=HL_GROUP`、`navigable=true`、`selectable=false`;它只能导航,不能作为最终业务值。
|
||||
- `ADMINISTRATIVE` 不得占用 `HL_GROUP`;旧命名空间 `HL_INTERNAL` 不允许产生新写入。
|
||||
- `ADMINISTRATIVE` 不得占用 `HL_GROUP`;除 `HL_GROUP`、`HL_INTERNAL` 的限制外,`codeStandard` 不是封闭枚举,自定义命名空间可扩展,推荐使用 `HL_CUSTOM`。
|
||||
- 旧命名空间 `HL_INTERNAL` 不允许产生新写入;`GB/T 2260` 只允许六位数字 `regionCode`,并要求完整的 `sourceVersion`、`sourceRef`。
|
||||
- 同一 `codeStandard + regionCode` 的版本有效期不得重叠;有效期采用左闭右开 `[validFrom, validTo)`。
|
||||
- 缺少权限返回 `210802`;空请求或格式错误返回 `400`;全部失败路径零业务写入。
|
||||
|
||||
@@ -498,7 +507,7 @@ Content-Type: application/json
|
||||
|
||||
#### 使用场景
|
||||
|
||||
具有 `system:region:update` 权限的管理员更新节点名称、父级、层级、可导航/可选状态、有效期、排序和扩展信息。请求必须携带最近读取到的完整可变字段和 `rowVersion`。
|
||||
具有 `system:region:update` 权限的管理员更新节点名称、父级、层级、可导航/可选状态、有效期、排序和扩展信息。请求必须携带完整可变字段快照和最近读取到的 `rowVersion`;这不是局部 PATCH。
|
||||
|
||||
#### 入参
|
||||
|
||||
@@ -515,8 +524,8 @@ Content-Type: application/json
|
||||
| `validFrom` | Body | String | ✅ | `yyyy-MM-dd` | 生效日期 |
|
||||
| `validTo` | Body | String / null | ❌ | 必须严格晚于 `validFrom` | 失效日期 |
|
||||
| `status` | Body | String | ✅ | `ACTIVE` 或 `INACTIVE` | 节点状态 |
|
||||
| `sortOrder` | Body | Integer | ✅ | 0~2,000,000,000 | 同级排序号 |
|
||||
| `extension` | Body | Object / null | ❌ | 必须是 JSON object,最多 4000 字符 | 结构化扩展;旧别名 `extJson` 仍兼容 |
|
||||
| `sortOrder` | Body | Integer | ✅ | 0~2,000,000,000;部分初始化聚合节点的只读返回值大于此上限 | 同级排序号;不能无脑回填超限旧值 |
|
||||
| `extension` | Body | Object / null | ❌ | 必须是 JSON object,最多 4000 字符;省略与显式 `null` 都会清空 | 结构化扩展;旧别名 `extJson` 仍兼容 |
|
||||
| `rowVersion` | Body | Integer | ✅ | 大于等于 0,必须等于当前版本 | CAS 并发版本,成功后自动加 1 |
|
||||
|
||||
`codeStandard`、`regionCode`、`nodeKind`、`sourceVersion` 和 `sourceRef` 不属于更新接口,不能通过请求改写。
|
||||
@@ -528,6 +537,7 @@ Content-Type: application/json
|
||||
| `code` | Integer | `200` 表示更新成功 |
|
||||
| `message` | String | 成功时为“行政区划更新成功” |
|
||||
| `success` | Boolean | 更新成功为 `true` |
|
||||
| `traceId` | String / null | 链路追踪 ID;报错排查时提供给后端 |
|
||||
| `data` | null | 更新接口不返回业务数据 |
|
||||
|
||||
#### 请求示例
|
||||
@@ -539,7 +549,7 @@ Authorization: Bearer <具有 system:region:update 的管理员令牌>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentId": "2090000000000000035",
|
||||
"parentId": "35",
|
||||
"levelCode": "COUNTY",
|
||||
"name": "示例业务区(更新)",
|
||||
"shortName": "示例区",
|
||||
@@ -563,13 +573,14 @@ Content-Type: application/json
|
||||
"code": 200,
|
||||
"message": "行政区划更新成功",
|
||||
"data": null,
|
||||
"traceId": "a1b2c3d4-e5f6-7890",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口成功时 `data=null` 是固定契约,调用方以 `code=200 && success=true` 判断成功。
|
||||
本接口成功时 `data=null` 是固定契约,调用方以 `code=200 && success=true` 判断成功。本接口没有“Redis 不可用仍受理写入”的公开降级承诺,也不会返回更新后的节点快照。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
@@ -586,12 +597,14 @@ Content-Type: application/json
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 更新采用完整可变字段快照,不是局部 PATCH;前端必须先读取最新节点信息再提交。
|
||||
- 更新采用完整可变字段快照,不是局部 PATCH;省略 `extension` 不表示保持原值,而是写成 `null`。
|
||||
- 当前 `children` / `path` 响应不包含 `extension`,也没有单节点详情接口。前端无法仅靠这四个公开接口保留未知的非空扩展值;在补充详情契约前,不应开放此类节点的通用编辑入口。
|
||||
- TEST 初始化聚合节点可返回大于 `2,000,000,000` 的 `sortOrder`(例如北京第二层为 `2,000,000,001`),而更新请求上限是 `2,000,000,000`;直接回填会返回 `400`,这类节点也不能按无损通用编辑处理。
|
||||
- `rowVersion` 冲突返回 `210807` 且零写入;前端应重新加载,而不是自动覆盖重试。
|
||||
- 有子节点的分支禁止移动父级或改变层级,返回 `210810`。
|
||||
- 父节点提前停用、取消导航或缩短有效期,导致现有子节点失去合法父级时返回 `210814`。
|
||||
- 已到生效日的 `GB/T 2260` 法定节点不能原位改写历史属性;应先关闭旧版本,再创建有效期不重叠的新版本。
|
||||
- 成功后 `rowVersion` 增加 1;再次编辑必须使用新版本号。
|
||||
- 已到生效日的 `GB/T 2260` 法定节点不能原位改写历史属性;代码只允许在其他历史字段(包含 `extension`)完全不变时关闭状态/结束时间。由于当前读接口不返回 `extension`,前端不能把“关闭已有法定版本”当成已具备的安全通用操作。
|
||||
- 成功后数据库中的 `rowVersion` 增加 1,但响应不返回新版本;再次编辑前需重新读取可见节点,且要考虑提交后缓存异步刷新的短暂窗口。
|
||||
|
||||
---
|
||||
|
||||
@@ -603,7 +616,7 @@ Content-Type: application/json
|
||||
GET /admin/region/children
|
||||
→ 用户选择 provinceId
|
||||
GET /admin/region/children?parentId={provinceId}
|
||||
→ 用户选择 prefectureId
|
||||
→ 用户选择第二层导航项 prefectureId(可能是不可提交的 AGGREGATION)
|
||||
GET /admin/region/children?parentId={prefectureId}
|
||||
→ 用户选择 countyId
|
||||
→ 业务表单最终保存 countyId(或业务允许的上级 selectable 节点)
|
||||
@@ -627,6 +640,7 @@ GET /admin/region/{savedRegionId}/path
|
||||
| ✅ 加载市级 | `GET /admin/region/children?parentId={provinceId}` |
|
||||
| ✅ 编辑回显 | 先 `GET /admin/region/{savedId}/path`,再逐层加载 `children` |
|
||||
| ✅ 比较 ID | 全程以 string 比较,例如 `item.id === selectedId` |
|
||||
| ✅ 判断最终值 | 只提交 `selectable=true` 的节点;`defaultId`、`prefectureId` 本身不等于可提交 |
|
||||
| ❌ 根层传 0 | `GET /admin/region/children?parentId=0` → 400 |
|
||||
| ❌ 截行政区编码推父级 | 根据 `110101` 截取 `110100`;父子关系只能以接口返回的 ID 为准 |
|
||||
| ❌ 把聚合节点当最终值 | `nodeKind=AGGREGATION && selectable=false` 时仍提交为业务行政区 |
|
||||
@@ -650,10 +664,10 @@ if (response.code !== 200 || response.success !== true) {
|
||||
|---|---|---|
|
||||
| 查询 `children` | 只读,不改变节点、版本或审计 | 不写入 |
|
||||
| 查询 `path` | 只读,不改变节点、版本或审计 | 不写入,不返回部分父链 |
|
||||
| 创建节点 | 成功新增一个节点,初始 `rowVersion=0`;事务提交后联动和父链读取可见 | 参数、权限、父链、有效期或唯一性失败时不新增节点 |
|
||||
| 更新节点 | 成功更新一个节点并令 `rowVersion+1`;事务提交后联动和父链返回新值 | 版本冲突或业务 guard 失败时原节点保持不变 |
|
||||
| 创建节点 | 成功新增一个节点,初始 `rowVersion=0`;数据库事务提交后异步刷新节点和父级列表缓存 | 参数、权限、父链、有效期或唯一性失败时不新增节点 |
|
||||
| 更新节点 | 成功更新一个节点并令 `rowVersion+1`;数据库事务提交后异步刷新节点、旧/新父级及上层列表缓存 | 版本冲突或业务 guard 失败时原节点保持不变 |
|
||||
|
||||
前端无需理解或操作物理表、缓存键和审计表,也不得通过其他接口绕过创建/更新权限和版本检查。
|
||||
写接口成功只证明数据库事务已经提交,不承诺紧随其后的第一个缓存读取已经反映新值。前端不得自行操作缓存;需要立即回显时应短暂重读,并始终以新的 `rowVersion` 为准。读取阶段 Redis 故障会降级数据库;写入口还受公共幂等保护,不能据此推导“Redis 故障时写入一定成功”。
|
||||
|
||||
---
|
||||
|
||||
@@ -672,6 +686,7 @@ if (response.code !== 200 || response.success !== true) {
|
||||
- 父链异常:`210811`,不返回部分路径。
|
||||
- `extension` 不是 JSON object 或过长:`210812`。
|
||||
- 层级代码与父链深度不一致:`210813`。
|
||||
- 写接口命中五秒参数幂等键:`100502`;这是拒绝重复提交,不会回放第一次成功响应。
|
||||
- 老数据兼容:停用历史节点可由 `path` 回显;旧缓存中的数字 ID 可由后端读取,但当前 API 响应始终输出 string ID。
|
||||
|
||||
### 业务错误码
|
||||
@@ -680,6 +695,7 @@ if (response.code !== 200 || response.success !== true) {
|
||||
|---:|---|---|
|
||||
| `400` | 参数校验失败文案 | 任一接口的 ID 非正整数、Body 缺少必填字段或格式不合法 |
|
||||
| `401` | 未认证文案 | 任一接口未携带有效管理端身份 |
|
||||
| `100502` | 请勿重复提交 | `create` / `update` 在五秒内命中相同参数幂等键;失败不会返回第一次结果 |
|
||||
| `210801` | 行政区划节点不存在 | `children` 父节点或 `path`/`update` 目标不存在 |
|
||||
| `210802` | 无行政区划维护权限 | `create` / `update` 缺少专用写权限 |
|
||||
| `210803` | 行政区划父节点不合法 | 父级不可导航、已失效或有效期不能覆盖子级 |
|
||||
@@ -732,15 +748,19 @@ if (response.code !== 200 || response.success !== true) {
|
||||
| `ACTIVE` | 启用 | 位于有效期时可进入当前联动列表 |
|
||||
| `INACTIVE` | 停用 | 不进入当前联动列表,但历史父链仍可回显 |
|
||||
|
||||
### `codeStandard`(编码命名空间)
|
||||
### `codeStandard`(可扩展编码命名空间)
|
||||
|
||||
**所属字段**: `AdministrativeRegionCreateReqVO.codeStandard`、`AdministrativeRegionItemVO.codeStandard` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
`codeStandard` 不是封闭枚举。服务端会去除首尾空白并转成大写;下表仅列出内置/保留语义:
|
||||
|
||||
| 值或类别 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `GB/T 2260` | 法定行政区编码 | `regionCode` 必须为六位数字;生效历史受不可变保护 |
|
||||
| `HL_CUSTOM` | 自定义行政区编码 | 管理端创建普通自定义行政区的推荐命名空间 |
|
||||
| `HL_GROUP` | 导航聚合编码 | 只能与 `AGGREGATION` 搭配 |
|
||||
| `HL_INTERNAL` | 旧内部命名空间 | 仅兼容识别旧缓存,新写入固定拒绝并返回 `210809` |
|
||||
| 其他非空值 | 扩展命名空间 | 最多 32 字符;可供 `ADMINISTRATIVE` 使用,但不得冒充 `HL_GROUP` |
|
||||
|
||||
---
|
||||
|
||||
@@ -755,6 +775,7 @@ if (response.code !== 200 || response.success !== true) {
|
||||
| 所有行政区划 Long ID | 小数值可能被全局序列化规则输出为 JSON number | 一律输出 JSON string |
|
||||
| 缺失父级/层级 | 可能被调用方误当字符串处理 | 保持真正的 JSON `null` |
|
||||
| `extension` 历史入参 | 旧调用方可能使用 `extJson` | 请求继续兼容 `extJson`,当前文档统一使用 `extension` |
|
||||
| TEST 北京第二层示例 | 曾被文档误写为可选择的 `GB/T 2260` 地级节点 | 实际为 `id="35"`、`HL_GROUP`、`AGGREGATION`、`selectable=false` 的导航分组 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
@@ -763,20 +784,23 @@ if (response.code !== 200 || response.success !== true) {
|
||||
| 三级联动 | 无统一公开接口,容易截行政区编码推断 | 逐级调用 `children`,以后端父子关系为准 |
|
||||
| 编辑回显 | 调用方自行拼装父链 | 调用 `path` 返回完整有序父链和三级快捷 ID |
|
||||
| 并发更新 | 无本接口旧语义 | 必须提交 `rowVersion`,冲突返回 `210807` 且零写入 |
|
||||
| 写后读取 | 文档曾表述为提交后立即可见 | 代码为事务提交后异步刷新缓存,存在短暂最终一致窗口 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否;这是新增接口,且正式前端接入前已经固定字符串 ID 契约。
|
||||
- **前端是否必须同步上线**: 是;`frontend_status=pending`,管理端需接入三级选择器、历史回显和主数据维护。
|
||||
- **前端 workaround 清理点**: 不得保留行政区编码截位、Long ID 转 number、只判断 HTTP status 或把 `hasChildren` 等同于 `selectable` 的临时逻辑。
|
||||
- **前端接入状态**: `frontend_status=verified`;mmg 已完成三级联动接入验证(`frontend_ref=7acd1cb0`)。该验证不扩大为通用主数据编辑能力,后者仍受 `extension` 不可回读等限制,不能按“完整 CRUD 已具备”上线。
|
||||
- **前端 workaround 清理点**: 不得保留行政区编码截位、Long ID 转 number、只判断 HTTP status、把 `hasChildren` / `defaultId` / `prefectureId` 等同于 `selectable`,或把省略 `extension` 理解成保持原值的临时逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台行政区划维护、省/市/县三级选择器及历史行政区值回显。
|
||||
- **当前未提供**: 单节点详情、历史版本列表、按编码查询、删除、批量导入/导出接口;四个已发布接口不能组成无损的完整 CRUD。
|
||||
- **前端接入边界**: 三级选择与父链回显可直接接入;对已有非空 `extension`、超出更新上限的初始化 `sortOrder`、已生效法定节点关闭等需要无损回读/回填的场景暂不具备完整公开契约。
|
||||
- **零影响**:
|
||||
- 小程序端现有行政区接口;
|
||||
- Product、Order、Resource 和 Fleet 现有业务接口;
|
||||
@@ -790,15 +814,17 @@ if (response.code !== 200 || response.success !== true) {
|
||||
|
||||
```text
|
||||
GET /admin/region/children → 根级/子级列表、defaultId、string/null ID ✓
|
||||
GET /admin/region/{id}/path → 省市县完整父链、缺失层级 null、string ID ✓
|
||||
GET /admin/region/{id}/path → 三级完整父链、缺失层级 null、string ID ✓
|
||||
POST /admin/region → 创建成功返回 string ID,数据库与缓存读取一致 ✓
|
||||
PUT /admin/region/{id} → CAS 更新成功,rowVersion 0→1,联动与父链同步 ✓
|
||||
PUT /admin/region/{id} → CAS 更新成功,rowVersion 0→1,提交后缓存刷新完成再读取一致 ✓
|
||||
GET /admin/region/children(未认证) → 401 ✓
|
||||
POST /admin/region(缺参/非法节点类型) → 400 且零业务写入 ✓
|
||||
GET /admin/region/{missingId}/path → 210801 ✓
|
||||
```
|
||||
|
||||
- TEST 精确提交:`b62054035a139e3c689179ef75e2d052d08dde52`,包含六个相关 PR 的合并提交。
|
||||
- 本次代码优先复核确认:该提交的 `AdminRegionController` 只有本文四个公开端点;`AdministrativeRegionItemVO` 不含 `extension`;缓存刷新注册在事务 `afterCommit` 后交给独立执行器;migration 中北京链路为 `1 → 35 → 376`,其中 `35` 规范化后为不可选择的 `HL_GROUP/AGGREGATION`。
|
||||
- 2026-08-26 本次复核还通过 TEST Gateway 匿名请求确认 `/admin/region/children` 当前可达且返回业务码 `401`、`success=false`;未使用伪造身份,也未产生业务写入。
|
||||
- Deploy Panel API 任务:`52771074`(`hl-user-service`)和 `b1db24b2`(`hl-gateway`),终态均为 `success`,双实例、Nacos、滚动采样及部署窗口日志检查通过。
|
||||
- 真实 TEST 管理员经 Gateway 完成未认证、读取、失败零写入、创建、CAS 更新、持久化结果和缓存一致性验收。
|
||||
- 验收创建的临时节点已精确清理;数据库数量与测试命名空间恢复基线,四个缓存快照按原值和绝对过期时间恢复,登录会话已注销;操作审计按系统设计保留。
|
||||
@@ -822,6 +848,7 @@ GET /admin/region/{missingId}/path → 210801 ✓
|
||||
|
||||
- 主 Issue: [wx/HL#6350](https://git.1814.love:8443/wx/HL/issues/6350)
|
||||
- ID 字符串补充 Issue: [wx/HL#6409](https://git.1814.love:8443/wx/HL/issues/6409)
|
||||
- 代码优先核对修正 Issue: [wx/HL#6438](https://git.1814.love:8443/wx/HL/issues/6438)
|
||||
- 主 PR: [wx/HL#6359](https://git.1814.love:8443/wx/HL/pulls/6359)
|
||||
- ID 字符串 PR: [wx/HL#6412](https://git.1814.love:8443/wx/HL/pulls/6412)
|
||||
- 本次文档补充 Issue: [wx/HL#6422](https://git.1814.love:8443/wx/HL/issues/6422)
|
||||
@@ -830,7 +857,7 @@ GET /admin/region/{missingId}/path → 210801 ✓
|
||||
|
||||
## 撤回
|
||||
|
||||
本次 #6422 仅补充 Changelog 和模板校验,不改变后端制品。若文档补充需要撤回,从最新 `hl-api-changelog/main` 创建独立分支,revert #6422 对应合并提交并重新运行仓库全部校验;TEST 行政区划 API、数据、配置、Redis 和 MQ 均不需要回退。撤回后前端暂以当前 Controller/VO 契约为准,并重新提交一份通过模板门禁的自包含文档。
|
||||
本次 #6438 代码优先核对只修改 Changelog,不改变后端制品。若本次文档修正需要撤回,从最新 `hl-api-changelog/main` 创建独立分支,revert 本次文档合并提交并重新运行仓库全部校验;TEST 行政区划 API、数据库、配置、Redis 和 MQ 均不需要回退。撤回后前端必须以 TEST 对应提交的 Controller、VO、Service 与 migration 为准,不能恢复使用被本次指出的错误聚合节点示例或同步缓存假设。
|
||||
|
||||
---
|
||||
|
||||
@@ -840,6 +867,7 @@ GET /admin/region/{missingId}/path → 210801 ✓
|
||||
|
||||
- **Issue**: [#6350](https://git.1814.love:8443/wx/HL/issues/6350)
|
||||
- **补充文档 Issue**: [#6422](https://git.1814.love:8443/wx/HL/issues/6422)
|
||||
- **代码核对修正 Issue**: [#6438](https://git.1814.love:8443/wx/HL/issues/6438)
|
||||
- **PR**: [#6359](https://git.1814.love:8443/wx/HL/pulls/6359)
|
||||
- **ID 契约 PR**: [#6412](https://git.1814.love:8443/wx/HL/pulls/6412)
|
||||
- **最终后端提交**: [b62054035](https://git.1814.love:8443/wx/HL/commit/b62054035a139e3c689179ef75e2d052d08dde52)
|
||||
|
||||
@@ -1,41 +1,53 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6397"
|
||||
title: "供应商注册合同聚合信息"
|
||||
title: "供应商注册合同聚合信息(已废弃)"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "f2a4200d"
|
||||
target_release: ""
|
||||
verified_at: "2026-08-26"
|
||||
status_note: "PR #6405 已合并 dev-v3;Deploy Panel 任务 af3f205b 成功发布 Resource 双实例,合同接口已完成 23 项真实 Gateway 验收。TEST 工作副本存在服务器本地提交导致精确提交回读仍被阻塞,后端工单保持开启;本条只交接已经实测存在的接口契约。"
|
||||
updated_at: "2026-08-26"
|
||||
frontend_ref: "pending"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-08-29"
|
||||
status_note: "本条记录的 #6405 合同随供应商草稿/注册聚合写入语义已被业务纠正并废弃,不再是当前可联调契约。当前后端以 #6544 为准:add/update/submit 对旧 contracts 输入兼容忽略,合同只在已有 supplierId 后通过三个独立接口维护,不进入建档审批;详情继续只读返回 contracts。最终 Resource 已通过任务 ca2fe4d7 部署提交 c579c87014f56452fea2fce5075b31ae6fd12a35。旧前端 002475b2 仅作为历史记录,当前消费状态恢复 pending。"
|
||||
updated_at: "2026-08-29"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 🔧 供应商注册合同聚合信息
|
||||
# ⚠️ 供应商注册合同聚合信息(已废弃)
|
||||
|
||||
供应商创建草稿、提交注册和基础信息详情现统一支持合同完整快照。管理端应把“合同信息”放在“资质证照”之后、现有账户区域之前,并把原“初始账户”展示标题改为“结算信息”。
|
||||
> **2026-08-29 纠正:本文以下聚合写入说明仅保留为历史,不得继续用于联调或实现。** 当前契约见 [#6544 供应商合同独立登记](../2026-08/29_6544_供应商合同独立登记-新增接口-管理后台.md):供应商草稿、资料更新和注册提交对旧 `contracts` 输入兼容接收但完全忽略;合同必须在取得 `supplierId` 后,通过独立 add/update/del 接口和独立事务维护,且不进入建档审批。`basic-info/view` 仍只读返回未软删合同,`initialAccounts` 字段保持不变。
|
||||
|
||||
原前端提交 `002475b2` 消费的是已撤销的聚合写语义,只作为历史事实保留,当前状态为待按独立接口重新接入。
|
||||
|
||||
展示名调整不改变接口字段:原请求字段仍为 `initialAccounts`,不得改成 `settlementInfo` 或其他名称。
|
||||
|
||||
## 变更接口清单
|
||||
## 二、变更接口清单(历史,禁止用于当前联调)
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变化 |
|
||||
|---:|---|---|---|---|
|
||||
| 1 | 创建供应商注册草稿 | POST | `/admin/supplier/items/add` | 请求可选增加 `contracts` 完整集合 |
|
||||
| 2 | 提交供应商注册 | POST | `/admin/supplier/items/{supplierId}/submit` | 请求可选增加 `contracts` 完整快照 |
|
||||
| 3 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应增加 `contracts` 列表 |
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---:|---|---|---|---|---|
|
||||
| 1 | 创建供应商注册草稿 | POST | `/admin/supplier/items/add` | 请求体新增可选字段 | 可选增加 `contracts` 完整集合 |
|
||||
| 2 | 提交供应商注册 | POST | `/admin/supplier/items/{supplierId}/submit` | 请求体新增可选字段 | 可选增加 `contracts` 完整快照 |
|
||||
| 3 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应新增字段 | 响应增加 `contracts` 列表 |
|
||||
|
||||
统一响应均为 `Result<T>`。业务失败可能仍为 HTTP 200,调用方必须同时判断 `code`、`success`、`message` 和 `data`。
|
||||
|
||||
## 公共合同字段
|
||||
## ⚠️ 关键变化(2026-08-26 晚,PR #6450 同日修正)
|
||||
|
||||
### 请求字段 `contracts[]`
|
||||
- **响应 `contracts[].amount` 由 Number 改为 String**:此前(f2a4200d 验收时)响应示例为 `"amount": 1200.50`,现实际输出 `"amount": "1200.50"`,与全站金额字段(`contractId` 之外的金额一律字符串)对齐,防 JavaScript 浮点精度丢失。
|
||||
- **请求侧不变**:`contracts[].amount` 请求仍按 Number 传(字符串同值也可被兼容解析),无需改表单提交。
|
||||
- 前端处理:详情/列表展示处把 amount 当字符串渲染即可,参与运算前 `Number(...)` 转换。
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
三个接口的合同字段完全一致,先在「公共合同字段」统一约定;各接口再分节给出自包含的使用场景、入参、出参、示例与错误。
|
||||
|
||||
### 公共合同字段
|
||||
|
||||
#### 请求字段 `contracts[]`
|
||||
|
||||
| 字段 | 类型 | 创建必填 | 提交既有项必填 | 约束与说明 |
|
||||
|---|---|---:|---:|---|
|
||||
@@ -53,7 +65,7 @@ base: "dev-v3"
|
||||
| `scanFileUrl` | String | 否 | 否 | 合同扫描件永久地址,最长 1000 字符 |
|
||||
| `remark` | String | 否 | 否 | 最长 500 字符 |
|
||||
|
||||
### 响应字段 `contracts[]`
|
||||
#### 响应字段 `contracts[]`
|
||||
|
||||
详情返回上述全部业务字段,并额外返回:
|
||||
|
||||
@@ -62,20 +74,40 @@ base: "dev-v3"
|
||||
| `contractId` | String | 合同 ID,始终按字符串处理,不得转 JavaScript Number |
|
||||
| `updateTime` | String | 合同当前版本,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
响应中的 `amount` 为 String(如 `"1200.50"`,PR #6450 起),请求中的 `amount` 仍按 Number 传。
|
||||
|
||||
历史数据可能返回只读状态 `TERMINATED`;创建和提交请求不得发送该状态。
|
||||
|
||||
## 1. 创建供应商注册草稿
|
||||
### 1. 创建供应商注册草稿 `POST /admin/supplier/items/add`
|
||||
|
||||
`POST /admin/supplier/items/add`
|
||||
**VO**: `SupplierDraftSaveReqVO`(请求;响应 data 字段见下表)
|
||||
|
||||
### 使用场景与边界
|
||||
#### 使用场景
|
||||
|
||||
- `contracts` 可省略、为 `null` 或空数组,旧客户端行为不变。
|
||||
- 非空时最多 100 项,合同与供应商主体、资质和 `initialAccounts` 一起成功或一起失败。
|
||||
- 创建请求中的每个合同都是新合同,禁止携带 `contractId`。
|
||||
- 写入仍要求现有供应商创建权限;仅可信 `FINANCE`、`SUPER_ADMIN` 且拥有对应平台权限的身份可执行。
|
||||
供应商注册第一步:创建草稿。`contracts` 为本次新增的可选完整集合,随草稿与供应商主体、资质、`initialAccounts` 一起成功或一起失败;旧客户端省略该字段时行为完全不变。
|
||||
|
||||
### 典型请求
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `fullName` | Body | String | 是 | 非空白 | 供应商全称(本次未变更,列此定位) |
|
||||
| `taxNo` | Body | String | 是 | 统一社会信用代码 | 本次未变更,列此定位 |
|
||||
| `qualifications` | Body | Array | 否 | - | 资质证照集合(本次未变更) |
|
||||
| `contracts` | Body | Array | 否 | 非空时最多 100 项 | 本次新增:合同完整集合,单项字段见「公共合同字段」;创建时每项禁止携带 `contractId` |
|
||||
| `initialAccounts` | Body | Array | 否 | - | 结算账户集合,字段名不得改(本次未变更) |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `supplierId` | String | 供应商 ID,字符串 |
|
||||
| `supplierNo` | String/null | 供应商编号,草稿期可为 null |
|
||||
| `status` | String | 固定 `DRAFT` |
|
||||
| `onboardingStage` | String | 入驻阶段,草稿为 `PROFILE_DRAFT` |
|
||||
| `initialAccounts` | Array | 结算账户回显 |
|
||||
| `updateTime` | String | 聚合版本时间,`yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /admin/supplier/items/add
|
||||
@@ -123,7 +155,7 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
### 成功响应
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -141,7 +173,13 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
### 失败响应:创建携带合同 ID
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
`contracts` 省略、为 `null` 或空数组均可正常创建,旧客户端行为不变;创建响应不返回合同明细(合同通过详情接口回显)。草稿无结算账户时 `initialAccounts` 返回空数组 `[]`,不返回 `null`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
创建请求携带合同 ID(创建时每个合同都是新项,禁止 `contractId`):
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -154,20 +192,44 @@ Content-Type: application/json
|
||||
|
||||
失败时不会留下供应商主体或部分合同。
|
||||
|
||||
## 2. 提交供应商注册
|
||||
#### 业务边界
|
||||
|
||||
`POST /admin/supplier/items/{supplierId}/submit`
|
||||
- 非空时最多 100 项,合同与供应商主体、资质和 `initialAccounts` 一起成功或一起失败。
|
||||
- 创建请求中的每个合同都是新合同,禁止携带 `contractId`。
|
||||
- 写入仍要求现有供应商创建权限;仅可信 `FINANCE`、`SUPER_ADMIN` 且拥有对应平台权限的身份可执行。
|
||||
- `endDate` 早于 `startDate` 直接校验失败,零写入。
|
||||
|
||||
### 使用场景与快照语义
|
||||
### 2. 提交供应商注册 `POST /admin/supplier/items/{supplierId}/submit`
|
||||
|
||||
- `contracts` 省略或为 `null`:本次不处理合同,保留草稿当前合同。
|
||||
- `contracts: []`:明确清空当前全部合同。
|
||||
- 非空数组:作为完整快照;带 `contractId` 的项覆盖当前合同,不带 ID 的项新增,当前已有但数组中遗漏的合同删除。
|
||||
- 带 ID 的合同必须属于路径中的供应商;不属于当前供应商、重复 ID、非法枚举、负金额或日期逆序均失败。
|
||||
- `expectedUpdateTime` 仍是供应商聚合并发版本;发生并发修改时调用方应刷新详情后重新组装完整表单。
|
||||
- 合同快照会进入本次审批资料,但提交注册不会自动改写合同自身的 `status`。
|
||||
**VO**: `SupplierDraftSaveReqVO`(请求,含 `expectedUpdateTime` 版本;响应 data 字段见下表)
|
||||
|
||||
### 典型请求
|
||||
#### 使用场景
|
||||
|
||||
注册第二步:把草稿完整表单(含合同快照)提交审批。`contracts` 按完整快照语义处理:省略/`null` = 本次不处理合同;`[]` = 明确清空;非空数组 = 全量覆盖(带 ID 覆盖、无 ID 新增、遗漏删除)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 必须存在且未删除 | 目标供应商 |
|
||||
| `fullName` | Body | String | 是 | 非空白 | 完整表单字段(本次未变更,列此定位) |
|
||||
| `contracts` | Body | Array | 否 | 非空时最多 100 项 | 本次新增:合同完整快照,单项字段见「公共合同字段」;既有项必须带回字符串 `contractId` |
|
||||
| `initialAccounts` | Body | Array | 否 | - | 结算账户集合(本次未变更) |
|
||||
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 聚合并发版本,须取详情最新值 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `approvalLogId` | String | 审批日志 ID,字符串 |
|
||||
| `requestNo` | String | 审批请求号 |
|
||||
| `provider` | String | 审批通道,如 `LOCAL_AUTO` |
|
||||
| `approvalStatus` | String | 审批结果,如 `APPROVED` |
|
||||
| `spNo` / `spStatus` | String/null | 外部审批单号/状态,本地通道为 null |
|
||||
| `syncStatus` | String | 结果应用状态,如 `APPLIED` |
|
||||
| `submittedAt` / `finishedAt` | String | 提交/完成时间 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /admin/supplier/items/2090300000000063970/submit
|
||||
@@ -204,7 +266,7 @@ Content-Type: application/json
|
||||
"signDate": "2026-08-26",
|
||||
"startDate": "2026-09-01",
|
||||
"endDate": "2027-08-31",
|
||||
"amount": 1200.50,
|
||||
"amount": "1200.50",
|
||||
"pricingMode": "按团结算",
|
||||
"settleCycle": "MONTHLY",
|
||||
"status": "ACTIVE",
|
||||
@@ -217,7 +279,7 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
### 成功响应
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -238,7 +300,13 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
### 失败响应:合同不属于当前供应商
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
`contracts` 省略或为 `null` 时保留草稿当前合同,走既有审批流程,无降级差异;`contracts: []` 为明确清空(不是省略),会删除当前全部合同。草稿本身无合同时按无合同提交,不报错。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
合同不属于当前供应商(或重复 ID、非法枚举、负金额、日期逆序等同族校验失败):
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -251,20 +319,50 @@ Content-Type: application/json
|
||||
|
||||
该失败会回滚本次提交表单中的主体、资质、合同和审计变化,供应商仍保持原状态和原版本。
|
||||
|
||||
## 3. 查询供应商基本信息
|
||||
#### 业务边界
|
||||
|
||||
`GET /admin/supplier/items/{supplierId}/basic-info/view`
|
||||
- 带 ID 的合同必须属于路径中的供应商;不属于当前供应商、重复 ID、非法枚举、负金额或日期逆序均失败。
|
||||
- `expectedUpdateTime` 仍是供应商聚合并发版本;发生并发修改时调用方应刷新详情后重新组装完整表单。
|
||||
- 合同快照会进入本次审批资料,但提交注册不会自动改写合同自身的 `status`。
|
||||
- 快照语义易错点:用户明确删除全部合同时发送 `contracts: []`;未加载合同或不处理合同时省略字段,不要误发空数组。
|
||||
|
||||
### 请求
|
||||
### 3. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
|
||||
|
||||
**VO**: `SupplierBasicInfoRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
供应商详情首屏:返回主体信息、资质、合同列表(本次新增 `contracts`)与聚合版本。管理端据此渲染「资质证照 → 合同信息 → 结算信息」区块。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 必须存在且未删除 | 目标供应商 |
|
||||
|
||||
无请求体、无查询参数。读取继续要求可信读角色和 `supplier:view` 平台权限。
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `supplierId` | String | 供应商 ID,字符串 |
|
||||
| `fullName` / `shortName` | String | 供应商名称 |
|
||||
| `tax_no` | String | 脱敏税号 |
|
||||
| `types` | Array | 供应商类型,`typeCode`/`typeName` |
|
||||
| `qualifications` | Array | 资质证照列表(本次未变更) |
|
||||
| `contracts` | Array | 本次新增:合同列表,字段见「公共合同字段」;`amount` 为 String |
|
||||
| `status` | String | 供应商状态 |
|
||||
| `updateTime` | String | 聚合版本时间,提交表单时回传 `expectedUpdateTime` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/2090300000000063970/basic-info/view
|
||||
Authorization: Bearer <admin-token>
|
||||
```
|
||||
|
||||
无请求体。读取继续要求可信读角色和 `supplier:view` 平台权限。
|
||||
|
||||
### 成功响应
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -322,7 +420,7 @@ Authorization: Bearer <admin-token>
|
||||
"signDate": "2026-08-26",
|
||||
"startDate": "2026-09-01",
|
||||
"endDate": "2027-08-31",
|
||||
"amount": 1200.50,
|
||||
"amount": "1200.50",
|
||||
"pricingMode": "按团结算",
|
||||
"settleCycle": "MONTHLY",
|
||||
"status": "DRAFT",
|
||||
@@ -337,9 +435,22 @@ Authorization: Bearer <admin-token>
|
||||
}
|
||||
```
|
||||
|
||||
合同按 `contractId` 升序返回,只包含当前有效合同。没有合同时返回空数组 `[]`,不返回 `null`。
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
### 常见失败
|
||||
没有合同时返回空数组 `"contracts": []`,不返回 `null`;合同按 `contractId` 升序返回,只包含当前有效合同。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
常见失败(统一 `Result` 包装,HTTP 200):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395001,
|
||||
"message": "供应商不存在",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
| 场景 | `code` | 前端处理 |
|
||||
|---|---:|---|
|
||||
@@ -347,17 +458,13 @@ Authorization: Bearer <admin-token>
|
||||
| 可信角色或 `supplier:view` 平台权限不足 | `403` | 展示无权限状态 |
|
||||
| 供应商不存在或已删除 | `395001` | 返回列表并刷新 |
|
||||
|
||||
## 修改前后对比
|
||||
#### 业务边界
|
||||
|
||||
| 场景 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 创建草稿 | 请求不能携带合同 | 可选携带完整 `contracts`,与草稿一起成功或失败 |
|
||||
| 提交注册 | 提交表单不能维护合同 | 可省略保留、空数组清空或提交完整合同快照 |
|
||||
| 基础信息 | 不返回合同列表 | 返回完整 `contracts[]` 及字符串 ID、版本 |
|
||||
| 账户区域标题 | 页面显示“初始账户” | 页面应显示“结算信息”,接口字段仍为 `initialAccounts` |
|
||||
| 页面区块顺序 | 资质后直接进入账户区域 | 资质证照 → 合同信息 → 结算信息 |
|
||||
- 读取要求可信读角色(`ADMIN`/`FINANCE`/`SUPER_ADMIN`)和 `supplier:view` 平台权限。
|
||||
- 敏感字段(税号、证件号、手机号)一律脱敏返回,前端不得期待明文。
|
||||
- `updateTime` 是后续提交表单的并发版本,必须原样缓存回传。
|
||||
|
||||
## 兼容性与管理端接入事项
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
1. 在“资质证照”区域之后新增“合同信息”表格,在合同之后显示原账户表格。
|
||||
2. 原账户表格标题改为“结算信息”;所有请求和响应继续使用 `initialAccounts`,不要改字段名。
|
||||
@@ -367,15 +474,65 @@ Authorization: Bearer <admin-token>
|
||||
6. 本次不新增接口路径、权限点或业务错误码;旧客户端省略 `contracts` 时继续可用。
|
||||
7. 管理端源码不在本后端工单中修改,前端状态保持 `pending`,直至完成页签、标题和表格接入并提供前端引用。
|
||||
|
||||
## TEST 验证证据
|
||||
## 五、数据库行为
|
||||
|
||||
- 创建草稿与提交注册均为聚合级单事务写入:供应商主体、资质、合同快照、结算账户与审计记录一起成功或一起失败,任一校验失败零写入。
|
||||
- 合同快照随供应商聚合版本(`expectedUpdateTime` 乐观并发控制)持久化;并发修改时提交失败,调用方须刷新详情后重试。
|
||||
- 审计留痕:创建/删除等不可逆操作保留审计记录;测试环境临时数据已通过业务删除接口软删除。
|
||||
- 查询供应商基本信息为只读,无写库行为。
|
||||
- 本次无 DDL、无 Flyway 迁移、无 Redis/MQ 行为变化。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- `contracts` 省略、`null` 与空数组语义不同:省略/传 `null` = 不处理合同(旧客户端兼容);`[]` = 明确清空全部合同。
|
||||
- 合同非空时单请求最多 100 项;合同与供应商主体、资质、`initialAccounts` 同事务,一起成功或一起失败。
|
||||
- 创建草稿禁止携带 `contractId`(每个合同都是新项);提交既有合同必须原样带回字符串 `contractId`,外部/他人合同 ID 触发整体回滚零写入。
|
||||
- `endDate` 早于 `startDate` 直接校验失败,零写入。
|
||||
- `amount` 边界:大于等于 0,最多 10 位整数和 2 位小数;响应按字符串输出(PR #6450 起)。
|
||||
- 历史只读状态 `TERMINATED` 仅可返回,创建/提交发送该状态会被拒绝。
|
||||
- 越权:仅可信 `FINANCE`、`SUPER_ADMIN` 且拥有对应平台权限可写;普通 ADMIN 调用写接口返回越权错误。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 场景 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 创建草稿 | 请求不能携带合同 | 可选携带完整 `contracts`,与草稿一起成功或失败 |
|
||||
| 提交注册 | 提交表单不能维护合同 | 可省略保留、空数组清空或提交完整合同快照 |
|
||||
| 基础信息 | 不返回合同列表 | 返回完整 `contracts[]` 及字符串 ID、版本 |
|
||||
| 账户区域标题 | 页面显示“初始账户” | 页面应显示“结算信息”,接口字段仍为 `initialAccounts` |
|
||||
| 页面区块顺序 | 资质后直接进入账户区域 | 资质证照 → 合同信息 → 结算信息 |
|
||||
| 响应 `contracts[].amount`(PR #6450) | Number(如 `1200.50`) | String(如 `"1200.50"`) |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **管理端(admin)**:供应商注册/编辑页需新增「合同信息」表格并调整区块顺序;既有合同必须缓存并原样回传字符串 `contractId`,否则提交会整单回滚。
|
||||
- **同日修正(PR #6450)**:响应 `contracts[].amount` 由 Number 改为 String,前端 f2a4200d 按 Number 集成的解析处需改为字符串处理(展示直接渲染、运算前 `Number(...)`),影响面限于合同金额展示/计算处,请求提交不受影响。
|
||||
- **C 端(mp)**:不涉及,无影响。
|
||||
- **QA 排查面**:问题定位优先看「契约约束与正确调用方式」第 3、4 条(快照回传与空数组语义)与「关键变化」(amount 字符串化)。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 不新增接口路径、权限点或业务错误码;旧客户端省略 `contracts` 时行为完全不变。
|
||||
- `initialAccounts` 字段名、类型与语义不变(仅页面展示标题由「初始账户」改「结算信息」,属前端文案)。
|
||||
- 请求侧 `contracts[].amount` 仍按 Number 传,PR #6450 只改响应输出,不改请求解析。
|
||||
- 供应商其余模块(资源信息、审批记录、账户证明)的接口与字段不受影响。
|
||||
- 数据库结构无变更(复用既有快照列),无 Redis/MQ 行为变化。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- 自动化:供应商定向测试 115 项通过;`hl-resource-service` 全量 2,111 项,0 失败、0 错误,38 项条件跳过;`hl-verify` 与差异检查通过。
|
||||
- 部署:Deploy Panel API 任务 `af3f205b` 终态 `success`、退出码 0、`has_build_error=false`,未发现 Maven、编译或滚动发布错误。
|
||||
- 健康:`hl-resource-service` 的 8082、8182 双实例运行;Nacos `test` 命名空间两实例均 `healthy=true`、`enabled=true`。
|
||||
- 真实 Gateway:23 项断言通过,覆盖合同随草稿创建、详情完整回显、字符串 ID、创建携带 ID 失败、普通 ADMIN 越权、提交外部合同 ID 完整回滚、日期逆序零写入和旧客户端省略 `contracts`。
|
||||
- 同日修正复验(PR #6450):`hl-resource-service` 于 2026-08-26 23:44 滚动发布双实例 UP,jar 构建时间戳与合并提交一致;供应商定向测试 330 项通过;空原因/超长原因的状态变更请求仍由请求校验层返回 400(对外契约不变)。
|
||||
- 清理:两个临时 DRAFT 均通过业务删除接口软删除并回读为不存在;仅保留不可逆的 CREATE/DELETE 操作审计。
|
||||
- 环境限制:部署前锁定 `origin/dev-v3=1a16a5aec7d0b95ec87e6fb222581060a3135984`,但面板 Git API 回读到服务器本地短提交 `6f7d3ca78`,Gitea 无法解析该对象。接口行为已真实验证,后端工单仍等待测试环境恢复精确远端提交后复验,不能据此宣称最终交付完成。
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
- [#6405](https://git.1814.love:8443/wx/HL/pulls/6405):本功能原始落地(合同聚合信息)。
|
||||
- [#6450](https://git.1814.love:8443/wx/HL/pulls/6450):同日每日审查修正——响应 `contracts[].amount` Number→String(金额序列化红线)、状态变更原因错误码段位化(395043/395044,仅内部防御路径,对外契约不变)。
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit 1a16a5aec7d0b95ec87e6fb222581060a3135984`,经独立 PR 合入。
|
||||
@@ -384,6 +541,12 @@ Authorization: Bearer <admin-token>
|
||||
4. 已保存的合同资料保留,不做破坏性批量清理;回退后旧客户端继续按省略 `contracts` 的路径工作。
|
||||
5. 经 Gateway 复测创建、提交、基础信息、未认证、越权、非法合同 ID、失败零写入和旧客户端兼容,并确认双实例与 Nacos 健康。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 设计/API 说明:仓库 `docs/supplier/API-CHANGE-6397.html`
|
||||
- 同日修正 PR:[#6450](https://git.1814.love:8443/wx/HL/pulls/6450)(响应 `contracts[].amount` Number→String,每日审查红线修复)
|
||||
- 供应商暂停/拉黑原因必填(同属供应商状态域):`changelogs-v2/2026-08/26_6392_供应商暂停合作与拉黑原因必填接口-新增接口-管理后台.md`
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#6397](https://git.1814.love:8443/wx/HL/issues/6397)
|
||||
|
||||
@@ -0,0 +1,777 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6436"
|
||||
title: "供应商敏感字段完整回显与主类型"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "5957c58c"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-08-27"
|
||||
status_note: "PR #6478、补充 PR #6483/#6486 已合并 dev-v3,最终提交 aa735fb8 已由 Deploy Panel 任务 7a48028c 发布 TEST;真实 TEST 身份已验证完整值、权限投影、失败零写入及测试数据清理。"
|
||||
updated_at: "2026-08-27"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 🔧 供应商敏感字段完整回显与主类型
|
||||
|
||||
供应商管理接口不再用掩码替代已授权管理员需要处理的业务原值,并补齐唯一主类型和注册账户摘要。证明附件仍受独立权限保护,不能因为本次完整值调整而绕过授权或同步读取审计。
|
||||
|
||||
## 一、背景
|
||||
|
||||
此前详情、账户和审批记录混用了原值、掩码与历史密文回退,草稿类型也缺少稳定的主类型回显,导致管理端无法可靠编辑、审核或回填表单。本次统一以下消费口径:
|
||||
|
||||
- 通过既有供应商读取权限后,税号、法人证件、电话、证照号和银行账号返回完整业务值。
|
||||
- `proofFileUrls` 继续要求 `supplier:account:proof:read`,且只有同步读取审计成功后才返回;无权限时字段不序列化。
|
||||
- `types[].isPrimary` 与根对象 `primaryTypeCode`、`primaryTypeName` 成为主类型权威回显。
|
||||
- `*Mask` 字段保留兼容但已废弃,兼容期内与对应完整值字段同值;新代码应使用无 `Mask` 的字段。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---:|---|---|---|---|---|
|
||||
| 1 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应修改 | 返回完整主体、联系人、资质字段及主类型 |
|
||||
| 2 | 查询供应商账户信息 | GET | `/admin/supplier/items/{supplierId}/account-info/list` | 响应修改 | 返回可读账户完整账号,按权限决定附件字段 |
|
||||
| 3 | 查询收款账户详情 | GET | `/admin/supplier/bank-accounts/{accountId}/view` | 响应修改 | 返回完整账号,按权限决定附件字段 |
|
||||
| 4 | 查询供应商审批记录 | GET | `/admin/supplier/items/{supplierId}/approval-records/page` | 请求与响应修改 | 增加目标筛选及完整前后值可用性 |
|
||||
| 5 | 创建供应商注册草稿 | POST | `/admin/supplier/items/add` | 响应修改 | `initialAccounts` 回显完整账号摘要 |
|
||||
| 6 | 更新供应商 | PUT | `/admin/supplier/items/{supplierId}/update` | 响应修改 | 稳定回显主类型及完整初始账号摘要 |
|
||||
| 7 | 提交供应商注册 | POST | `/admin/supplier/items/{supplierId}/submit` | 响应修改 | 审批结果增加完整初始账号摘要 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
|
||||
|
||||
**VO**: `SupplierBasicInfoRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
供应商详情和编辑表单初始化。管理端可直接使用完整字段,并用主类型字段初始化单选控件。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `supplierId` | String | 供应商 ID |
|
||||
| `tax_no` | String | 完整主体证件号,JSON 名保持既有口径 |
|
||||
| `legalRepresentativeIdNo` | String/null | 完整法人居民身份证号 |
|
||||
| `legalRepresentativeIdCardFrontUrl` / `legalRepresentativeIdCardBackUrl` | String/null | 完整永久文件地址 |
|
||||
| `contactPhone` | String/null | 完整公司联系电话 |
|
||||
| `contacts[].contactPhone` | String | 完整联系人电话 |
|
||||
| `qualifications[].certNo` | String/null | 完整证照编号 |
|
||||
| `types[].isPrimary` | Boolean | 当前类型是否为主类型 |
|
||||
| `primaryTypeCode` / `primaryTypeName` | String/null | 主类型值和展示名称;无类型草稿为 null |
|
||||
| `legalRepresentativeIdNoMask` / `contactPhoneMask` | String/null | 废弃兼容别名,与完整值字段同值 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/2092800000000000001/basic-info/view
|
||||
Authorization: Bearer <admin-token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2092800000000000001",
|
||||
"fullName": "示例旅行服务有限公司",
|
||||
"tax_no": "91350211M000100Y46",
|
||||
"legalRepresentativeIdNo": "11010519491231002X",
|
||||
"legalRepresentativeIdNoMask": "11010519491231002X",
|
||||
"legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/front.jpg",
|
||||
"legalRepresentativeIdCardBackUrl": "https://files.example.com/supplier/back.jpg",
|
||||
"contactPhone": "13800138000",
|
||||
"contactPhoneMask": "13800138000",
|
||||
"types": [{"typeCode": "HOTEL", "typeName": "酒店", "isPrimary": true}],
|
||||
"primaryTypeCode": "HOTEL",
|
||||
"primaryTypeName": "酒店",
|
||||
"contacts": [{"contactName": "示例联系人", "contactPhone": "13900139000", "contactPhoneMask": "13900139000"}],
|
||||
"qualifications": [{"qualType": "BUSINESS_LICENSE", "certNo": "LIC-2026-001", "certNoMask": "LIC-2026-001"}],
|
||||
"updateTime": "2026-08-27 11:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无类型草稿返回 `types: []`、`primaryTypeCode: null`、`primaryTypeName: null`。联系人或资质为空时返回空数组;历史停用类型的名称无法从字典解析时,名称回退为类型值,不阻断详情。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395001,
|
||||
"message": "供应商不存在",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 要求既有 `supplier:view` 和数据范围校验,登录态不能替代业务权限。
|
||||
- 普通应用日志、异常和 Trace 不记录上述完整值。
|
||||
- `*Mask` 仅用于旧客户端兼容,新代码不得继续依赖掩码语义。
|
||||
|
||||
### 2. 查询供应商账户信息 `GET /admin/supplier/items/{supplierId}/account-info/list`
|
||||
|
||||
**VO**: `SupplierAccountInfoRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
在供应商结算信息区域展示已进入 PENDING、ACTIVE 或 DISABLED 的管理端可读账户。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `supplierId` | String | 供应商 ID |
|
||||
| `bankAccounts` | Array | 可读账户,默认账户优先 |
|
||||
| `bankAccounts[].accountNo` | String | 完整银行账号 |
|
||||
| `bankAccounts[].accountNoMask` | String | 废弃兼容别名,与 `accountNo` 同值 |
|
||||
| `bankAccounts[].proofFileUrls` | Array | 有独立权限且审计成功时出现;无权限时整个字段省略 |
|
||||
| `bankAccounts[].status` | String | `PENDING`、`ACTIVE` 或 `DISABLED` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/2092800000000000001/account-info/list
|
||||
Authorization: Bearer <admin-token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2092800000000000001",
|
||||
"bankAccounts": [{
|
||||
"accountId": "2092800000000000011",
|
||||
"accountName": "示例旅行服务有限公司",
|
||||
"bankName": "示例银行",
|
||||
"accountNo": "6222021234567890",
|
||||
"accountNoMask": "6222021234567890",
|
||||
"proofFileUrls": ["https://files.example.com/supplier/account-proof.pdf"],
|
||||
"status": "ACTIVE",
|
||||
"isDefault": "YES"
|
||||
}],
|
||||
"updateTime": "2026-08-27 11:31:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
仅有 DRAFT 账户或没有账户时返回 `bankAccounts: []`。无 `supplier:account:proof:read` 时账号仍为完整值,但每个账户均省略 `proofFileUrls`,不是返回空数组。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
证明附件同步审计不可用时失败关闭,不返回任何附件:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395039,
|
||||
"message": "暂时无法校验资源,请稍后重试",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 要求 `supplier:view`;附件另需 `supplier:account:proof:read`。
|
||||
- 每次获准的附件读取都会先同步写安全审计,失败时整个请求失败。
|
||||
- DRAFT 初始账户不会出现在该读取接口中,应使用创建/更新响应的 `initialAccounts` 展示草稿摘要。
|
||||
|
||||
### 3. 查询收款账户详情 `GET /admin/supplier/bank-accounts/{accountId}/view`
|
||||
|
||||
**VO**: `SupplierBankAccountDetailRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
查看单个已进入可读状态的收款账户完整信息。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `accountId` | Path | String | 是 | 正整数 ID 字符串 | 账户 ID |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `accountId` | String | 账户 ID |
|
||||
| `accountName` | String | 收款户名 |
|
||||
| `accountNo` | String | 完整银行账号 |
|
||||
| `accountNoMask` | String | 废弃兼容别名,与完整账号同值 |
|
||||
| `proofFileUrls` | Array | 有独立权限且同步审计成功时出现,否则省略 |
|
||||
| `status` / `isDefault` | String | 账户状态与默认标记 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/bank-accounts/2092800000000000011/view
|
||||
Authorization: Bearer <admin-token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"accountId": "2092800000000000011",
|
||||
"accountName": "示例旅行服务有限公司",
|
||||
"accountType": "CORPORATE",
|
||||
"bankName": "示例银行",
|
||||
"accountNo": "6222021234567890",
|
||||
"accountNoMask": "6222021234567890",
|
||||
"proofFileUrls": ["https://files.example.com/supplier/account-proof.pdf"],
|
||||
"settleMode": "PREPAY",
|
||||
"invoiceType": "NONE",
|
||||
"status": "ACTIVE",
|
||||
"isDefault": "YES",
|
||||
"updateTime": "2026-08-27 11:31:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无附件权限时响应省略 `proofFileUrls`;DRAFT、已删除或不存在的账户按不可读处理,不返回草稿详情。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395001,
|
||||
"message": "供应商不存在",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 完整账号沿用基础查看权限,证明附件使用独立权限和同步审计。
|
||||
- 账户必须属于未删除供应商且状态为 PENDING、ACTIVE 或 DISABLED。
|
||||
- 无附件权限时后端读取阶段即排除附件字段,不是先读取后隐藏。
|
||||
|
||||
### 4. 查询供应商审批记录 `GET /admin/supplier/items/{supplierId}/approval-records/page`
|
||||
|
||||
**VO**: `SupplierApprovalRecordPageReqVO / PageResult<SupplierApprovalRecordRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
统一查看供应商主体与收款账户的变更历史,并区分完整新记录和不可恢复的历史掩码记录。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
|
||||
| `page` / `limit` | Query | Integer | 否 | 正整数,`limit` 不超过 200 | 分页参数 |
|
||||
| `approvalLogId` | Query | String | 否 | 正整数 ID 字符串 | 精确筛选审批 |
|
||||
| `operationType` | Query | String | 否 | `CREATE/UPDATE/ENABLE/DISABLE/DELETE` | 操作类型 |
|
||||
| `targetType` | Query | String | 否 | `SUPPLIER/ACCOUNT` | 本次增加的目标筛选 |
|
||||
| `fieldName` / `status` | Query | String | 否 | 当前接口枚举 | 字段或状态筛选 |
|
||||
| `from` / `to` | Query | String | 否 | 必须成对,`yyyy-MM-dd HH:mm:ss` | 时间范围 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `records[].targetType` | String | `SUPPLIER` 或 `ACCOUNT` |
|
||||
| `records[].targetId` | String/null | 主体记录为 null,账户记录为账户 ID |
|
||||
| `records[].oldValue` / `newValue` | String/null | 完整中文业务摘要,附件仍按独立权限投影 |
|
||||
| `records[].valueAvailability` | String | `FULL` 或 `LEGACY_MASKED_UNRECOVERABLE` |
|
||||
| `records[].oldValueMasked` / `newValueMasked` | String/null | 废弃兼容别名 |
|
||||
| `total` / `page` / `pageSize` | Integer | 分页元数据 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/2092800000000000001/approval-records/page?page=1&limit=20&targetType=ACCOUNT
|
||||
Authorization: Bearer <admin-token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"records": [{
|
||||
"supplierId": "2092800000000000001",
|
||||
"operationType": "ENABLE",
|
||||
"targetType": "ACCOUNT",
|
||||
"targetId": "2092800000000000011",
|
||||
"oldValue": "账户状态:待审批;收款账号:6222021234567890",
|
||||
"newValue": "账户状态:已生效;收款账号:6222021234567890",
|
||||
"valueAvailability": "FULL",
|
||||
"status": "已生效",
|
||||
"operatorName": "测试管理员",
|
||||
"operatorRole": "超级管理员",
|
||||
"createTime": "2026-08-27 11:32:00"
|
||||
}],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无匹配记录返回 `records: []` 和 `total: 0`。历史记录只保存掩码且无法恢复原值时,`valueAvailability` 返回 `LEGACY_MASKED_UNRECOVERABLE`,不会猜测或拼造完整值。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "目标类型仅支持SUPPLIER或ACCOUNT",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 要求 `supplier:approval:read` 及供应商数据范围权限。
|
||||
- 证明附件只有独立权限存在且同步审计成功时才进入完整摘要。
|
||||
- 分页内主体记录和账户记录按创建时间、记录 ID 稳定排序。
|
||||
|
||||
### 5. 创建供应商注册草稿 `POST /admin/supplier/items/add`
|
||||
|
||||
**VO**: `SupplierDraftSaveReqVO / SupplierWriteRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
创建供应商草稿并可同时保存一个初始收款账户;成功响应直接回显该账户的完整账号摘要。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `fullName` | Body | String | 是 | 非空白,最长 500 字符 | 供应商全称 |
|
||||
| `taxNo` | Body | String | 是 | 6 至 64 字符 | 主体证件号 |
|
||||
| `types` | Body | Array | 否 | 草稿可为空,非空不得重复 | 供应商类型,首项成为主类型 |
|
||||
| `mainCooperation` | Body | String | 是 | 非空白 | 主要合作内容 |
|
||||
| `initialAccounts` | Body | Array | 否 | 最多 1 项 | 初始收款账户完整输入 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `supplierId` | String | 新供应商 ID |
|
||||
| `status` | String | `DRAFT` |
|
||||
| `initialAccounts[].accountId` | String | 初始账户 ID |
|
||||
| `initialAccounts[].accountNo` | String | 完整银行账号 |
|
||||
| `initialAccounts[].accountNoMask` | String | 废弃兼容别名,与完整账号同值 |
|
||||
| `initialAccounts[].status` | String | 创建时为 `DRAFT` |
|
||||
| `updateTime` | String | 并发版本时间 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "示例旅行服务有限公司",
|
||||
"taxNo": "91350211M000100Y46",
|
||||
"types": [{"typeCode": "HOTEL"}],
|
||||
"mainCooperation": "酒店资源合作",
|
||||
"initialAccounts": [{
|
||||
"accountType": "CORPORATE",
|
||||
"bankName": "示例银行",
|
||||
"accountNo": "6222021234567890",
|
||||
"proofFileUrls": ["https://files.example.com/supplier/account-proof.pdf"],
|
||||
"settleMode": "PREPAY",
|
||||
"invoiceType": "NONE"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2092800000000000001",
|
||||
"supplierNo": null,
|
||||
"status": "DRAFT",
|
||||
"onboardingStage": "PROFILE_DRAFT",
|
||||
"initialAccounts": [{
|
||||
"accountId": "2092800000000000011",
|
||||
"accountNo": "6222021234567890",
|
||||
"accountNoMask": "6222021234567890",
|
||||
"status": "DRAFT"
|
||||
}],
|
||||
"updateTime": "2026-08-27 11:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
省略 `initialAccounts` 或传空数组时成功创建并返回 `initialAccounts: []`。草稿可传 `types: []`,此时详情的主类型字段为 null。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395002,
|
||||
"message": "无权执行该供应商写操作",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 仅可信 `FINANCE`、`SUPER_ADMIN` 且拥有 `supplier:create` 的身份可写;`ADMIN` 明确拒绝。
|
||||
- 初始账户最多一项,完整请求原子成功或失败,失败不留下主体或子项。
|
||||
- 新写入和新审计使用完整业务值,但普通日志、异常、Trace 和跨服务消息不得携带这些值。
|
||||
|
||||
### 6. 更新供应商 `PUT /admin/supplier/items/{supplierId}/update`
|
||||
|
||||
**VO**: `SupplierUpdateReqVO / SupplierWriteRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
增量更新供应商草稿或可变字段,并获得稳定的主类型与初始账户摘要回显。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
|
||||
| `types` | Body | Array | 否 | 省略表示不改;空数组表示清空草稿类型 | 类型完整快照 |
|
||||
| `changeReason` | Body | String | 是 | 非空白 | 变更原因 |
|
||||
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 乐观并发版本 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `supplierId` / `status` | String | 供应商 ID 和当前状态 |
|
||||
| `initialAccounts[].accountNo` | String | 既有初始账户完整账号 |
|
||||
| `initialAccounts[].accountNoMask` | String | 废弃兼容别名 |
|
||||
| `updateTime` | String | 更新后的并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"types": [{"typeCode": "HOTEL", "isPrimary": true}],
|
||||
"changeReason": "调整主合作类型",
|
||||
"expectedUpdateTime": "2026-08-27 11:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2092800000000000001",
|
||||
"status": "DRAFT",
|
||||
"onboardingStage": "PROFILE_DRAFT",
|
||||
"initialAccounts": [{
|
||||
"accountId": "2092800000000000011",
|
||||
"accountNo": "6222021234567890",
|
||||
"accountNoMask": "6222021234567890",
|
||||
"status": "DRAFT"
|
||||
}],
|
||||
"updateTime": "2026-08-27 11:35:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
省略 `types` 保持现有类型;草稿传 `types: []` 会清空类型并在详情返回 null 主类型。响应没有初始账户时固定返回空数组。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395014,
|
||||
"message": "数据已被他人修改,请刷新后重试",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 仅可信 `FINANCE`、`SUPER_ADMIN` 且拥有 `supplier:update` 的身份可写。
|
||||
- 非空类型集合稳定保留且恰有一个主类型;提交审批时仍要求至少一个类型。
|
||||
- 并发版本、状态或权限不满足时失败且零写入。
|
||||
|
||||
### 7. 提交供应商注册 `POST /admin/supplier/items/{supplierId}/submit`
|
||||
|
||||
**VO**: `SupplierSubmitReqVO / SupplierApprovalCommandRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
提交完整注册表单并进入审批;审批响应直接携带本次注册初始账户的完整摘要。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 草稿供应商 |
|
||||
| `fullName` / `taxNo` | Body | String | 是 | 完整注册表单约束 | 主体信息 |
|
||||
| `types` | Body | Array | 是 | 至少一项且不得重复 | 第一项为主类型 |
|
||||
| `initialAccounts` | Body | Array | 否 | 最多一项;省略可保留既有草稿账户 | 初始账户完整快照 |
|
||||
| `expectedUpdateTime` | Body | String | 是 | 必须等于当前版本 | 并发围栏 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `approvalLogId` / `requestNo` | String | 审批记录与幂等请求号 |
|
||||
| `provider` / `approvalStatus` / `syncStatus` | String | 审批通道、结果与本地应用状态 |
|
||||
| `initialAccounts[].accountId` | String | 本次注册关联账户 ID |
|
||||
| `initialAccounts[].accountNo` | String | 完整银行账号 |
|
||||
| `initialAccounts[].accountNoMask` | String | 废弃兼容别名 |
|
||||
| `initialAccounts[].status` | String | 审批通过后为 `ACTIVE` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "示例旅行服务有限公司",
|
||||
"taxNo": "91350211M000100Y46",
|
||||
"types": [{"typeCode": "HOTEL"}],
|
||||
"mainCooperation": "酒店资源合作",
|
||||
"licenseImageUrl": "https://files.example.com/supplier/license.jpg",
|
||||
"qualifications": [{
|
||||
"qualType": "BUSINESS_LICENSE",
|
||||
"certNo": "LIC-2026-001",
|
||||
"imageUrl": "https://files.example.com/supplier/license.jpg",
|
||||
"permanentValid": true
|
||||
}],
|
||||
"expectedUpdateTime": "2026-08-27 11:35:00"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"approvalLogId": "2092800000000000021",
|
||||
"requestNo": "SUP-REQ-EXAMPLE",
|
||||
"provider": "LOCAL_AUTO",
|
||||
"approvalStatus": "APPROVED",
|
||||
"syncStatus": "APPLIED",
|
||||
"initialAccounts": [{
|
||||
"accountId": "2092800000000000011",
|
||||
"accountNo": "6222021234567890",
|
||||
"accountNoMask": "6222021234567890",
|
||||
"status": "ACTIVE"
|
||||
}],
|
||||
"submittedAt": "2026-08-27 11:36:00",
|
||||
"finishedAt": "2026-08-27 11:36:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
没有初始账户时返回 `initialAccounts: []`。提交仍必须包含非空类型和当前完整注册资料,不因草稿阶段允许空类型而放宽。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395008,
|
||||
"message": "请至少选择一个类型并设置唯一主类型",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 仅可信 `FINANCE`、`SUPER_ADMIN` 且同时拥有更新和提交权限的身份可执行。
|
||||
- 审批准备、结果应用、主体状态、账户状态和完整审计保持原有事务与幂等语义。
|
||||
- 权限、状态、摘要或并发围栏失败时不允许部分写入。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
| 场景 | 正确处理 |
|
||||
|---|---|
|
||||
| 主类型绑定 | 使用 `primaryTypeCode`;类型列表使用 `types[].isPrimary`,不要自行取第一项猜测 |
|
||||
| 完整字段 | 使用 `tax_no`、`legalRepresentativeIdNo`、`contactPhone`、`certNo`、`accountNo` |
|
||||
| 废弃别名 | `legalRepresentativeIdNoMask`、`contactPhoneMask`、`certNoMask`、`accountNoMask` 仅作旧客户端兼容 |
|
||||
| 证明附件无权限 | `proofFileUrls` 字段不存在;不要把缺字段当接口异常或空附件 |
|
||||
| 草稿账户 | 从写响应 `initialAccounts` 获取;账户读取接口只返回 PENDING/ACTIVE/DISABLED |
|
||||
| 业务失败 | HTTP 状态之外必须检查 `code`、`success`、`message` 和 `data` |
|
||||
|
||||
管理端不得把业务请求体中的身份或角色作为授权依据,也不得缓存其他管理员读取到的完整敏感值供当前会话复用。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
| 外部动作 | 可观察结果 |
|
||||
|---|---|
|
||||
| 创建/更新供应商 | 主体、联系人、资质、类型和初始账户在同一业务事务中成功或失败 |
|
||||
| 注册提交 | 审批结果成功应用后主体和初始账户进入可读生效状态,响应返回完整账户摘要 |
|
||||
| 新增或变更敏感值 | 后续授权读取返回与提交一致的完整业务值,唯一冲突或非法值整次失败 |
|
||||
| 失败请求 | 未认证、无权、缺参、非法状态、并发冲突和审计失败均不留下部分业务写入 |
|
||||
| 历史数据 | 可恢复历史值继续读取;只剩不可逆掩码的审批历史显式标记为不可恢复 |
|
||||
|
||||
这些是接口可观察行为;调用方不依赖具体存储结构,也不应自行维护明文/密文兼容状态。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录经 Gateway 返回 `401`。
|
||||
- `ADMIN` 可在既有查看权限与数据范围内读取完整业务值,但写入返回 `395002`。
|
||||
- `FINANCE`、`SUPER_ADMIN` 仍需同时拥有对应平台权限才能写,角色名称本身不是唯一授权条件。
|
||||
- 附件独立权限不足时字段省略;同步审计失败时请求失败,不降级泄露附件。
|
||||
- 列表 `limit=201`、缺少必填字段或非法状态返回业务失败,并保持零写入。
|
||||
- Snowflake ID 继续按字符串处理;时间格式继续为 `yyyy-MM-dd HH:mm:ss`。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### `targetType` / `valueAvailability`
|
||||
|
||||
| 字段 | 值 | 中文与说明 |
|
||||
|---|---|---|
|
||||
| `targetType` | `SUPPLIER` | 供应商主体变更,`targetId` 为 null |
|
||||
| `targetType` | `ACCOUNT` | 收款账户变更,`targetId` 为账户 ID |
|
||||
| `valueAvailability` | `FULL` | 完整业务前后值可用 |
|
||||
| `valueAvailability` | `LEGACY_MASKED_UNRECOVERABLE` | 历史只剩不可逆掩码,不推测原值 |
|
||||
|
||||
### 账户可读状态
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `PENDING` | 待审批 | 账户可在管理端读取 |
|
||||
| `ACTIVE` | 已生效 | 正常可用账户 |
|
||||
| `DISABLED` | 已停用 | 保留只读历史信息 |
|
||||
| `DRAFT` | 草稿 | 不进入账户列表和账户详情 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 税号、法人证件号、电话、证照号 | 主要返回掩码或混合口径 | 授权读取返回完整值 |
|
||||
| `accountNo` | 可能为空或仅依赖 `accountNoMask` | 返回完整账号 |
|
||||
| `*Mask` | 表示掩码 | 废弃兼容别名,暂与完整值同值 |
|
||||
| `proofFileUrls` | 权限语义分散 | 独立权限 + 同步审计;无权时字段省略 |
|
||||
| `types[].isPrimary` | 主类型回显不稳定 | 明确 Boolean 标记 |
|
||||
| `primaryTypeCode/Name` | 不存在 | 根对象直接返回,空类型草稿为 null |
|
||||
| `initialAccounts` | 写/提交响应摘要不完整 | 返回账户 ID、完整账号和状态 |
|
||||
| 审批记录 | 主体与账户口径分散 | 统一目标类型、目标 ID、完整值和可用性 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 编辑页初始化 | 可能需要用掩码字段或猜测主类型 | 可直接绑定完整值与权威主类型 |
|
||||
| 附件读取 | 容易把空值与无权混淆 | 无权省略字段,审计失败整体失败 |
|
||||
| 草稿账户展示 | 读取接口与草稿状态语义不清 | 写响应展示草稿摘要,读取接口只展示可读状态 |
|
||||
| 历史审批 | 无法区分完整值与不可逆掩码 | 通过 `valueAvailability` 明确区分 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否。既有字段名保留,废弃别名在兼容窗口内继续返回。
|
||||
- **前端是否必须同步上线**: 建议尽快。应切换到无 `Mask` 字段、接入主类型字段,并正确处理 `proofFileUrls` 缺失。
|
||||
- **前端 workaround 清理点**: 删除自行猜主类型、对 `accountNoMask` 二次掩码、把附件缺字段强制转空数组等兼容逻辑。
|
||||
- **敏感展示责任**: 完整值只在已授权业务页面按最小必要范围展示,不写入前端日志、埋点、错误上报或持久缓存。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台供应商基本信息、结算账户、注册提交与审批记录消费契约。
|
||||
- **零影响**:
|
||||
- 不新增或修改 Gateway 路由。
|
||||
- 不修改角色、菜单、按钮或数据范围定义。
|
||||
- 不修改小程序、C 端、订单、产品、车队接口。
|
||||
- 不修改文件上传接口、Redis、MQ 或跨服务 DTO。
|
||||
- 不允许 DRAFT 账户通过账户查询接口提前暴露。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
最终后端提交 `aa735fb8531a3463b2b460965245e07ed8697775` 已通过 Deploy Panel 任务 `7a48028c` 发布 `hl-resource-service` 双实例,真实 TEST 身份经 Gateway 验证:
|
||||
|
||||
```text
|
||||
历史兼容:完整值与数据库一致,税号关键字搜索命中 ✓
|
||||
SUPER_ADMIN:创建草稿、详情完整值、注册审批、账号及附件审计读取 ✓
|
||||
ADMIN:详情与完整账号可读,proofFileUrls 省略,写入拒绝 395002 ✓
|
||||
未认证 401、缺参 400、limit=201 为 400、非法状态 400 ✓
|
||||
新写主体/联系人/资质/账户及新审计均为完整值,失败请求零写入 ✓
|
||||
合成供应商完成精确清理:17 张相关表检查、业务残留 0、失败标记残留 0 ✓
|
||||
验收会话主动失效,敏感读取操作审计按审计规则保留 ✓
|
||||
```
|
||||
|
||||
获批真实账号没有 `FINANCE` 角色,因此没有伪造该身份;`FINANCE` 与 `SUPER_ADMIN` 的服务端写权限同构由后端自动化测试覆盖,真实 TEST 写入使用 `SUPER_ADMIN`,并用真实 `ADMIN` 验证拒绝路径。
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|---|---|---|---|
|
||||
| #6478 | #6436 | 完整字段、主类型、权限、迁移与兼容读写 | ✅ 主交付 |
|
||||
| #6483 | #6481 | 修复 TEST 排序规则下回填精确比较 | ✅ 补充修复 |
|
||||
| #6486 | #6484 | 支持历史 Unicode 税号回填 | ✅ 补充修复 |
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- [主工单 #6436](https://git.1814.love:8443/wx/HL/issues/6436)
|
||||
- [主 PR #6478](https://git.1814.love:8443/wx/HL/pulls/6478)
|
||||
- [补充工单 #6481](https://git.1814.love:8443/wx/HL/issues/6481) / [PR #6483](https://git.1814.love:8443/wx/HL/pulls/6483)
|
||||
- [补充工单 #6484](https://git.1814.love:8443/wx/HL/issues/6484) / [PR #6486](https://git.1814.love:8443/wx/HL/pulls/6486)
|
||||
- 后端详细 API 说明:`docs/supplier/API-CHANGE-6436.html`
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#6436](https://git.1814.love:8443/wx/HL/issues/6436)
|
||||
- **PR**: [#6478](https://git.1814.love:8443/wx/HL/pulls/6478)
|
||||
- **最终 TEST 提交**: [aa735fb8](https://git.1814.love:8443/wx/HL/commit/aa735fb8531a3463b2b460965245e07ed8697775)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,499 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6474"
|
||||
title: "供应商详情分离变更记录与审批流水"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "199147de"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-08-27"
|
||||
status_note: "PR #6519 已合并 dev-v3;补充缺陷 PR #6534 已恢复旧兼容查询。hl-resource-service 已由 Deploy Panel 任务 9e69fd2c 精确发布提交 aec1de2d 至 TEST,并以真实 SUPER_ADMIN、ADMIN 和受限 CUSTOMIZER 身份完成 Gateway 只读验收。"
|
||||
updated_at: "2026-08-27"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商管理:详情分离变更记录与审批流水
|
||||
|
||||
供应商详情新增两个相互独立的只读分页接口:“变更记录”只返回供应商主体发生过的业务变化,“审批记录”只返回供应商主体审批事实。两个列表独立计数、独立筛选、独立排序,收款账户记录不会混入。
|
||||
|
||||
旧 `/admin/supplier/items/{supplierId}/approval-records/page` 继续保留,用于仍需主体与账户变更混合列表的兼容场景;本次不删除、不改名,也不要求现有调用方同步切换。
|
||||
|
||||
## 一、背景
|
||||
|
||||
旧审批记录接口承载的是主体与收款账户变更的兼容混合列表,不能同时满足详情页“业务变更”和“审批过程”两种独立展示语义。若管理端在本地拆分或二次计数,会出现分页总数不准确、账户记录混入主体历史、审批技术字段误展示等问题。
|
||||
|
||||
本次由后端直接提供两个稳定的业务白名单视图:
|
||||
|
||||
- “变更记录”展示操作类型、变更前后业务摘要、原因、生命周期状态、操作人、历史角色和发生时间。
|
||||
- “审批记录”展示审批业务、申请人、状态、动作、审批人安全展示值、意见、提交时间和完成时间。
|
||||
- 两个接口都在查询前校验可信管理身份、角色和 `supplier:approval:read` 权限。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---:|---|---|---|---|---|
|
||||
| 1 | 查询供应商主体变更记录 | GET | `/admin/supplier/items/{supplierId}/change-records/page` | 新增只读接口 | 独立分页返回主体变更业务摘要 |
|
||||
| 2 | 查询供应商主体审批流水 | GET | `/admin/supplier/items/{supplierId}/approval-history/page` | 新增只读接口 | 独立分页返回主体审批过程与结果 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 查询供应商主体变更记录 `GET /admin/supplier/items/{supplierId}/change-records/page`
|
||||
|
||||
**VO**: `SupplierChangeRecordPageReqVO` / `SupplierChangeRecordRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理端进入供应商详情的“变更记录”页签时调用。服务端完成主体记录筛选、分页和业务摘要投影;前端不要从旧混合列表中再次筛选主体记录,也不要自行拼接前后值摘要。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商;不得转为 JavaScript Number |
|
||||
| `page` | Query | Integer | 否 | 默认 `1`,最小 `1` | 当前页;兼容别名 `pageNo` |
|
||||
| `pageSize` | Query | Integer | 否 | 默认 `20`,范围 `1..100` | 每页条数 |
|
||||
| `operationType` | Query | String | 否 | `CREATE`、`UPDATE`、`ENABLE`、`DISABLE`、`DELETE` | 操作类型精确筛选 |
|
||||
| `fieldName` | Query | String | 否 | 非空白,最长 64 字符 | 发生变化的业务字段精确筛选 |
|
||||
| `status` | Query | String | 否 | 生命周期编码 | 按变更后的供应商状态筛选 |
|
||||
| `from` | Query | String | 条件必填 | `yyyy-MM-dd HH:mm:ss` | 与 `to` 成对传入,按发生时间筛选,含边界 |
|
||||
| `to` | Query | String | 条件必填 | `yyyy-MM-dd HH:mm:ss`,不得早于 `from` | 与 `from` 成对传入,含边界 |
|
||||
| `sortBy` | Query | String | 否 | `occurredAt` 或 `changeLogId`,默认 `occurredAt` | 服务端白名单排序字段 |
|
||||
| `sortDirection` | Query | String | 否 | `ASC` 或 `DESC`,不区分大小写,默认 `DESC` | 排序方向 |
|
||||
|
||||
#### 出参 `Result<PageResult<SupplierChangeRecordRespVO>>`
|
||||
|
||||
分页对象固定包含 `records`、`total`、`page`、`pageSize`。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `records` | Array | 当前页主体变更记录;无数据时为 `[]` |
|
||||
| `total` | Integer | 符合筛选条件的主体变更总数,不包含账户记录 |
|
||||
| `page` | Integer | 当前页码 |
|
||||
| `pageSize` | Integer | 当前页容量 |
|
||||
| `records[].operationType` | String | 操作类型编码 |
|
||||
| `records[].oldValue` | String | 变更前中文业务摘要;空值使用明确占位 |
|
||||
| `records[].newValue` | String | 变更后中文业务摘要;空值使用明确占位 |
|
||||
| `records[].valueAvailability` | String | `FULL` 或 `LEGACY_MASKED_UNRECOVERABLE` |
|
||||
| `records[].changeReason` | String | 业务变更原因或稳定占位 |
|
||||
| `records[].status` | String/null | 变更后的供应商生命周期中文名 |
|
||||
| `records[].operatorName` | String | 操作人展示名;系统操作为“系统”,依赖降级为“未知管理员” |
|
||||
| `records[].operatorRole` | String | 操作发生时的角色快照中文名 |
|
||||
| `records[].occurredAt` | String | 变更发生时间,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
响应不会返回变更日志 ID、管理员内部 ID、原始审计 JSON、TraceId、账户 ID、证明附件或其他主体敏感快照字段。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/2092800000000000001/change-records/page?page=1&pageSize=20&operationType=UPDATE&sortBy=occurredAt&sortDirection=DESC
|
||||
Authorization: Bearer <admin-token>
|
||||
```
|
||||
|
||||
GET 请求无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"operationType": "UPDATE",
|
||||
"oldValue": "供应商全称:示例旅行服务公司",
|
||||
"newValue": "供应商全称:示例旅行服务有限公司",
|
||||
"valueAvailability": "FULL",
|
||||
"changeReason": "修正供应商主体名称",
|
||||
"status": "合作中",
|
||||
"operatorName": "示例管理员",
|
||||
"operatorRole": "超级管理员",
|
||||
"occurredAt": "2026-08-27 15:20:00"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
筛选结果为空仍返回成功分页,不回退到旧混合列表:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"records": [],
|
||||
"total": 0,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
历史记录无法恢复完整原值时,仍返回已有安全摘要,并明确标记:
|
||||
|
||||
```json
|
||||
{
|
||||
"operationType": "UPDATE",
|
||||
"oldValue": "138****8000",
|
||||
"newValue": "139****9000",
|
||||
"valueAvailability": "LEGACY_MASKED_UNRECOVERABLE",
|
||||
"changeReason": "历史记录",
|
||||
"status": "合作中",
|
||||
"operatorName": "未知管理员",
|
||||
"operatorRole": "管理员",
|
||||
"occurredAt": "2026-07-01 10:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
非法筛选、分页、排序或时间范围返回参数错误,例如只传 `from`:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "from和to必须同时传入",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
供应商不存在或已删除:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395001,
|
||||
"message": "供应商不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 要求 Gateway 登录态、可信管理员身份、允许的读角色以及 `supplier:approval:read` 平台权限;任一门禁失败时不查询历史数据。
|
||||
- 代码角色矩阵沿用 `ADMIN`、`FINANCE`、`SUPER_ADMIN`;角色允许不等于拥有平台权限,两项必须同时满足。
|
||||
- 只读取供应商主体变更,不包含收款账户变更或审批流水。
|
||||
- `oldValue`、`newValue` 是后端形成的中文业务摘要,不是可回填编辑表单的结构化快照。
|
||||
- User 姓名依赖异常只影响 `operatorName`,分页仍成功且不会以管理员内部 ID 降级。
|
||||
- 接口只读,成功、空数据和失败场景均不修改供应商、审批、缓存或消息状态。
|
||||
|
||||
### 2. 查询供应商主体审批流水 `GET /admin/supplier/items/{supplierId}/approval-history/page`
|
||||
|
||||
**VO**: `SupplierApprovalHistoryPageReqVO` / `SupplierApprovalHistoryRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理端进入供应商详情的“审批记录”页签时调用。该列表只表达主体审批申请、过程和结果,不包含主体字段变更摘要,也不包含收款账户审批。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商;不得转为 JavaScript Number |
|
||||
| `page` | Query | Integer | 否 | 默认 `1`,最小 `1` | 当前页;兼容别名 `pageNo` |
|
||||
| `pageSize` | Query | Integer | 否 | 默认 `20`,范围 `1..100` | 每页条数 |
|
||||
| `bizType` | Query | String | 否 | 大写字母开头,仅大写字母、数字、下划线,最长 32 字符 | 审批业务类型精确筛选 |
|
||||
| `approvalStatus` | Query | String | 否 | 大写字母开头,仅大写字母、数字、下划线,最长 20 字符 | 审批状态精确筛选 |
|
||||
| `action` | Query | String | 否 | 大写字母开头,仅大写字母、数字、下划线,最长 32 字符 | 审批动作或结果精确筛选 |
|
||||
| `from` | Query | String | 条件必填 | `yyyy-MM-dd HH:mm:ss` | 与 `to` 成对传入,按提交时间筛选,含边界 |
|
||||
| `to` | Query | String | 条件必填 | `yyyy-MM-dd HH:mm:ss`,不得早于 `from` | 与 `from` 成对传入,含边界 |
|
||||
| `sortBy` | Query | String | 否 | `submittedAt` 或 `finishedAt`,默认 `submittedAt` | 服务端白名单排序字段 |
|
||||
| `sortDirection` | Query | String | 否 | `ASC` 或 `DESC`,不区分大小写,默认 `DESC` | 排序方向 |
|
||||
|
||||
#### 出参 `Result<PageResult<SupplierApprovalHistoryRespVO>>`
|
||||
|
||||
分页对象固定包含 `records`、`total`、`page`、`pageSize`。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `records` | Array | 当前页主体审批记录;无数据时为 `[]` |
|
||||
| `total` | Integer | 符合筛选条件的主体审批总数,不包含账户审批 |
|
||||
| `page` | Integer | 当前页码 |
|
||||
| `pageSize` | Integer | 当前页容量 |
|
||||
| `records[].bizType` | String | 审批业务类型编码 |
|
||||
| `records[].bizTypeName` | String | 审批业务类型中文名;未知编码显示“其他供应商审批” |
|
||||
| `records[].applicantName` | String | 申请人展示名;系统申请为“系统”,依赖降级为“未知申请人” |
|
||||
| `records[].approvalStatus` | String | 审批状态编码 |
|
||||
| `records[].approvalStatusName` | String | 审批状态中文名;未知编码显示“未知状态” |
|
||||
| `records[].action` | String/null | 审批动作或结果编码 |
|
||||
| `records[].actionName` | String | 审批动作中文名;未知编码显示“其他动作” |
|
||||
| `records[].approverName` | String | 安全展示值:“系统”“待审批”或“外部审批人” |
|
||||
| `records[].opinion` | String | 审批意见;无意见时为 `—` |
|
||||
| `records[].submittedAt` | String | 提交时间;历史缺失时使用该审批事实的创建时间,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||
| `records[].finishedAt` | String/null | 完成时间;审批中为 `null`,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
响应不会返回审批日志 ID、申请人内部 ID、requestNo、spNo、审批模板 ID、企微用户 ID、候选或详情摘要、候选或详情 JSON/密文、apply/sync 技术状态。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/2092800000000000001/approval-history/page?page=1&pageSize=20&approvalStatus=APPROVED&sortBy=submittedAt&sortDirection=DESC
|
||||
Authorization: Bearer <admin-token>
|
||||
```
|
||||
|
||||
GET 请求无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"bizType": "PROFILE_CREATE",
|
||||
"bizTypeName": "供应商建档审批",
|
||||
"applicantName": "示例管理员",
|
||||
"approvalStatus": "APPROVED",
|
||||
"approvalStatusName": "已通过",
|
||||
"action": "SYSTEM_AUTO_APPROVE",
|
||||
"actionName": "系统自动通过",
|
||||
"approverName": "系统",
|
||||
"opinion": "—",
|
||||
"submittedAt": "2026-08-27 14:00:00",
|
||||
"finishedAt": "2026-08-27 14:00:01"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
不存在符合条件的审批事实时返回成功空分页:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"records": [],
|
||||
"total": 0,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
申请人姓名依赖不可用时,该记录仍正常返回,`applicantName` 为 `未知申请人`;不会回退为内部管理员 ID。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
非法枚举、分页、排序或反向时间范围返回参数错误,例如:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "from不能晚于to",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
无业务读取权限时返回拒绝结果,且不查询审批历史:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 403,
|
||||
"message": "无权访问供应商数据",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 权限条件与变更记录接口相同:可信管理身份、`ADMIN`/`FINANCE`/`SUPER_ADMIN` 读角色和 `supplier:approval:read` 平台权限缺一不可。
|
||||
- 只读取供应商主体审批,不包含收款账户审批或主体字段变更记录。
|
||||
- 时间筛选基于提交时间;历史提交时间为空时使用该审批事实的创建时间。
|
||||
- 申请人名称每页最多批量补全一次;User 服务异常、空响应或缺失用户时安全降级,整页不返回 500。
|
||||
- 审批人只返回业务安全展示值,不暴露企微或外部审批系统标识。
|
||||
- 接口只读,不推进审批状态,不触发审批回调,也不产生缓存、消息或配置副作用。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### 详情页接入映射
|
||||
|
||||
| 页面区域 | 正确接口 | 数据范围 |
|
||||
|---|---|---|
|
||||
| 变更记录 | `GET /admin/supplier/items/{supplierId}/change-records/page` | 仅主体变更 |
|
||||
| 审批记录 | `GET /admin/supplier/items/{supplierId}/approval-history/page` | 仅主体审批 |
|
||||
| 旧兼容混合列表 | `GET /admin/supplier/items/{supplierId}/approval-records/page` | 主体与账户变更,契约不变 |
|
||||
|
||||
### ✅ 正确 / ❌ 错误调用对照
|
||||
|
||||
| 场景 | 调用 / 结果 |
|
||||
|---|---|
|
||||
| ✅ 分别加载两个页签 | 两条新接口分别维护自己的 `page`、`pageSize`、筛选和 `total` |
|
||||
| ✅ 查询完整时间区间 | 同时传 `from=2026-08-01 00:00:00` 与 `to=2026-08-31 23:59:59` |
|
||||
| ✅ 兼容旧页面 | 继续调用旧 `approval-records/page`,无需因本次新增接口修改 |
|
||||
| ❌ 在前端拆旧混合列表 | 分页后再过滤会得到错误总数,也不能生成审批流水 |
|
||||
| ❌ 只传一侧时间 | 返回 `400`,不执行历史查询 |
|
||||
| ❌ 把 `supplierId` 转为 Number | 可能丢失精度;ID 必须始终按 String 传输和比较 |
|
||||
|
||||
调用方必须同时检查 `code`、`message`、`success` 和 `data`,不能只用 HTTP 状态判断业务成功。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录或登录态失效:统一结果业务码 `401`,不进入供应商查询。
|
||||
- 已登录但角色或平台权限不满足:业务码 `403`,不读取历史。
|
||||
- `supplierId<=0`、`page<1`、`pageSize` 不在 `1..100`、非法筛选或排序:业务码 `400`。
|
||||
- 供应商不存在或已软删除:业务码 `395001`。
|
||||
- `from`、`to` 必须同时传入,且 `from<=to`;时间边界包含起止时刻。
|
||||
- 请求超过最后一页:返回原请求页码、空 `records` 和真实 `total`,不自动改页。
|
||||
- 两个新接口的 `total` 分别统计自己的数据集,互不借用;账户变更和账户审批都不进入。
|
||||
- 旧 `approval-records/page` 的路径、请求、响应及主体/账户混合语义保持兼容。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### `operationType`(变更记录操作类型)
|
||||
|
||||
**所属字段**: `SupplierChangeRecordPageReqVO.operationType` / `SupplierChangeRecordRespVO.operationType` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `CREATE` | 创建 | 创建供应商主体 |
|
||||
| `UPDATE` | 更新 | 更新主体业务字段 |
|
||||
| `ENABLE` | 启用 | 恢复可用状态 |
|
||||
| `DISABLE` | 停用 | 暂停或禁用合作 |
|
||||
| `DELETE` | 删除 | 软删除业务事实 |
|
||||
|
||||
### `status`(变更后的供应商生命周期)
|
||||
|
||||
**所属字段**: `SupplierChangeRecordPageReqVO.status` | **类型**: `String`
|
||||
|
||||
| 值 | 中文展示 |
|
||||
|---|---|
|
||||
| `DRAFT` | 草稿 |
|
||||
| `VETTING` | 注册审核中 |
|
||||
| `ACTIVE` | 合作中 |
|
||||
| `SUSPENDED` | 暂停合作 |
|
||||
| `FROZEN` | 已冻结 |
|
||||
| `BLACKLIST` | 黑名单 |
|
||||
| `ARCHIVED` | 已归档 |
|
||||
|
||||
### `valueAvailability`(变更摘要可用性)
|
||||
|
||||
**所属字段**: `SupplierChangeRecordRespVO.valueAvailability` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 调用方处理 |
|
||||
|---|---|---|
|
||||
| `FULL` | 完整业务摘要可用 | 正常展示 `oldValue` / `newValue` |
|
||||
| `LEGACY_MASKED_UNRECOVERABLE` | 历史原值不可恢复 | 展示现有摘要,并标注其为历史脱敏值;不要提示用户重试 |
|
||||
|
||||
### `bizType`(主体审批业务类型)
|
||||
|
||||
**所属字段**: `SupplierApprovalHistoryPageReqVO.bizType` / `SupplierApprovalHistoryRespVO.bizType` | **类型**: `String`
|
||||
|
||||
| 值 | 中文展示 | 说明 |
|
||||
|---|---|---|
|
||||
| `PROFILE_CREATE` | 供应商建档审批 | 注册或建档审批 |
|
||||
| `STATUS_CHANGE` | 供应商状态变更审批 | 生命周期状态变更审批 |
|
||||
| 其他合法大写编码 | 其他供应商审批 | 为历史及后续业务保留兼容 |
|
||||
|
||||
### `approvalStatus`(主体审批状态)
|
||||
|
||||
**所属字段**: `SupplierApprovalHistoryPageReqVO.approvalStatus` / `SupplierApprovalHistoryRespVO.approvalStatus` | **类型**: `String`
|
||||
|
||||
| 值 | 中文展示 |
|
||||
|---|---|
|
||||
| `PENDING` | 审批中 |
|
||||
| `APPROVED` | 已通过 |
|
||||
| `REJECTED` | 已驳回 |
|
||||
| `CANCELED` / `CANCELLED` | 已撤销 |
|
||||
| `FAILED` | 失败 |
|
||||
| 其他合法大写编码 | 未知状态 |
|
||||
|
||||
### `action`(主体审批动作或结果)
|
||||
|
||||
**所属字段**: `SupplierApprovalHistoryPageReqVO.action` / `SupplierApprovalHistoryRespVO.action` | **类型**: `String`
|
||||
|
||||
| 值 | 中文展示 |
|
||||
|---|---|
|
||||
| `SUBMIT` | 已提交 |
|
||||
| `SYSTEM_AUTO_APPROVE` | 系统自动通过 |
|
||||
| `APPLY_FAIL` | 结果应用失败 |
|
||||
| `APPROVE` | 通过 |
|
||||
| `REJECT` | 驳回 |
|
||||
| `REVOKE` | 撤销 |
|
||||
| `CANCEL` | 取消 |
|
||||
| 其他合法大写编码 | 其他动作 |
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅新增**:供应商详情的两个管理端只读分页契约。
|
||||
- **保持兼容**:旧 `approval-records/page` 继续返回主体与账户变更混合列表。
|
||||
- **零影响**:供应商详情、创建、更新、提交、归档、暂停合作、拉黑、删除及收款账户管理接口。
|
||||
- **零影响**:供应商写权限、数据范围、状态机、审批提交/结果应用、事务、锁、幂等、审计与软删除语义。
|
||||
- **零影响**:数据库结构与历史数据;本次无 migration、数据回填或破坏性数据操作。
|
||||
- **零影响**:Gateway 顶级路由、Nacos 配置、Redis、MQ 和跨服务写链路。
|
||||
- 后端仓库未修改任何管理端前端源码;管理端接入状态保持 `pending`。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- 本地自动化:主功能定向测试 79 项零失败;补充空上下文兼容测试套件 51 项零失败;最终 `hl-resource-service` 全量 2158 项零失败、零错误(38 项条件跳过);`GatewayRouteAuditTest` 4 项零失败。
|
||||
- 合并:主 PR #6519 合并提交 `3dc80ad69630127d91fa973a682051b2ef5c41d8`;验收发现旧兼容接口空上下文缺陷后,补充工单 #6532 / PR #6534 以最小修复合入,最新合并提交为 `aec1de2db2b4ed07d78877c1ccd4e532919bdaf3`。
|
||||
- TEST 精确部署:Deploy Panel 任务 `9e69fd2c` 成功发布 `aec1de2db2b4ed07d78877c1ccd4e532919bdaf3`;构建退出码 0,任务期 10 次有效采样均至少 2 个运行进程、2 个健康启用 Nacos 实例,零不可用采样,部署窗日志无失败标记。
|
||||
- 真实 Gateway:使用现有真实 `SUPER_ADMIN` 与同账号可切换的 `ADMIN` 身份,两条新接口的成功分页、字段白名单、独立总数、筛选、排序、边界参数和旧接口兼容均通过;`CUSTOMIZER` 返回 `403`,未认证返回 `401`。
|
||||
- TEST 数据覆盖:目标供应商存在 18 条主体变更事实和 1 条主体审批事实;对应账户变更、账户审批均为 0,验证两个新接口没有跨域混页。审批样本为 `APPROVED` / `SYSTEM_AUTO_APPROVE`;环境没有外部审批人样本,未伪造业务数据。
|
||||
- 身份说明:代码角色矩阵仍包含 `ADMIN`、`FINANCE`、`SUPER_ADMIN`。TEST 角色目录存在 `FINANCE`,但当前获批真实账号不能切换到该角色;按工单确认使用现有 `SUPER_ADMIN`(并覆盖 `ADMIN`)替代 FINANCE 实测,不创建、不修改、不伪造财务账号。
|
||||
- 清理:验收全程只读,供应商数据指纹前后一致,业务测试数据创建数为 0;真实会话均已失效处理,无数据库、Redis、MQ 或临时配置需要清理。
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|---|---|---|---|
|
||||
| #6486 | #6436 | 冻结供应商授权完整值与历史摘要语义 | ✅ 有效,本次沿用 |
|
||||
| #6519 | #6474 | 新增主体变更记录与主体审批流水独立分页 | ✅ 本次主功能 |
|
||||
| #6534 | #6532 | 修复旧兼容接口投影空上下文时的空指针 | ✅ 有效,保障历史兼容 |
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue:[#6474](https://git.1814.love:8443/wx/HL/issues/6474)
|
||||
- 关联 PR:[#6519](https://git.1814.love:8443/wx/HL/pulls/6519)
|
||||
- 补充缺陷 Issue:[#6532](https://git.1814.love:8443/wx/HL/issues/6532)
|
||||
- 补充缺陷 PR:[#6534](https://git.1814.love:8443/wx/HL/pulls/6534)
|
||||
- 管理端接入:将“变更记录”“审批记录”分别切换到两条新接口;前端引用待回填。
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 管理端先停止请求两个新路径,并恢复使用旧 `approval-records/page`,避免代码回退窗口产生请求失败。
|
||||
2. 从最新 `dev-v3` 创建独立回退分支,对主功能合并执行 `git revert -m 1 --no-edit 3dc80ad69630127d91fa973a682051b2ef5c41d8`,验证后经独立 PR 合入。
|
||||
3. #6532 的空上下文修复可独立保留,它修复的是旧兼容接口且不依赖两个新路径。若明确要求连同该修复一起撤回,再按从新到旧顺序执行 `git revert -m 1 --no-edit aec1de2db2b4ed07d78877c1ccd4e532919bdaf3`;这样会重新引入旧兼容接口在特定记录上的 500 风险,不作为推荐方案。
|
||||
4. 使用 Deploy Panel 两阶段客户端,仅滚动部署 `hl-resource-service`;本次无数据库、配置、Redis 或 MQ 恢复步骤,也无不可逆数据影响。
|
||||
5. 撤回后经 Gateway 验证两个新路径不可用、旧兼容接口按选定回退范围正常,复测未认证和越权,并确认 Resource 双实例、Nacos、日志及零写入。
|
||||
6. 同步发布本 Changelog 的撤回说明;不得仅改文件名表达状态。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#6474](https://git.1814.love:8443/wx/HL/issues/6474)
|
||||
- **PR**: [#6519](https://git.1814.love:8443/wx/HL/pulls/6519)
|
||||
- **Merge commit**: [`3dc80ad6`](https://git.1814.love:8443/wx/HL/commit/3dc80ad69630127d91fa973a682051b2ef5c41d8)
|
||||
- **补充 PR**: [#6534](https://git.1814.love:8443/wx/HL/pulls/6534)
|
||||
- **TEST 目标提交**: [`aec1de2d`](https://git.1814.love:8443/wx/HL/commit/aec1de2db2b4ed07d78877c1ccd4e532919bdaf3)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,347 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6476"
|
||||
title: "供应商详情补齐地址备注并统一表单校验"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "6635a2ae"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-08-27"
|
||||
status_note: "PR #6517 已合并 dev-v3(合并提交 a952a04c);hl-resource-service 已随 dev-v3 精确提交 b269e5fd 由 Deploy Panel 任务 938ca09e 发布 TEST。真实 TEST 身份已验证 address/remark 创建、详情、更新回显,创建/更新同口径校验、权限失败零写入及测试数据清理。"
|
||||
updated_at: "2026-08-27"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商管理:详情补齐地址备注并统一表单校验
|
||||
|
||||
供应商详情接口现在完整返回主体的 `address` 和 `remark`,管理端可用同一份详情快照初始化查看页与编辑表单。创建、提交和更新原本已有的业务字段与校验未增加新的必填项;本工单用契约测试和真实 TEST 验收固定三条写链路的一致口径。
|
||||
|
||||
本次没有新增接口路径、权限点或业务错误码。
|
||||
|
||||
## 一、背景
|
||||
|
||||
创建和更新请求已经接受地址、备注等主体字段,但基础信息详情此前没有回传 `address`、`remark`,导致管理端打开编辑页时无法完整还原已保存表单。同时,创建、提交和更新分属不同请求模型,消费方需要一个明确、可验证的共同校验口径。
|
||||
|
||||
本次处理后:
|
||||
|
||||
- 详情响应补齐主体地址与内部备注。
|
||||
- 创建、提交、更新对共同主体字段、类型、联系人、资质和合同继续执行同一业务规则。
|
||||
- 更新只额外要求变更原因、主体并发版本,以及被修改子项的 ID/版本。
|
||||
- #6436/#6499 已交付的授权完整值语义保持不变,不重新引入脱敏值或“留空表示保留旧敏感值”的旧约定。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---:|---|---|---|---|---|
|
||||
| 1 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应新增字段 | 根对象新增 `address`、`remark`,用于完整初始化查看与编辑表单 |
|
||||
|
||||
创建草稿、提交注册和更新资料的路径、请求字段及错误码没有结构性变化,因此不作为新增接口列入上表;其稳定调用规则在第四节集中说明。
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
|
||||
|
||||
**VO**: `SupplierBasicInfoRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理端进入供应商详情或编辑页时调用。响应是主体、类型、联系人、资质、合同及并发版本的完整快照;编辑页应直接使用其中的 `address`、`remark` 和 `updateTime`,不得用本地空值覆盖。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商;不得转为 JavaScript Number |
|
||||
|
||||
#### 出参 `Result<SupplierBasicInfoRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `supplierId` | String | 供应商 ID |
|
||||
| `fullName` / `shortName` | String/null | 供应商全称与简称 |
|
||||
| `tax_no` | String | 完整主体证件号;JSON 字段名保持 `tax_no` |
|
||||
| `legalRepresentative` | String/null | 法定代表人姓名 |
|
||||
| `legalRepresentativeIdNo` | String/null | 完整法人居民身份证号 |
|
||||
| `legalRepresentativeIdCardFrontUrl` / `legalRepresentativeIdCardBackUrl` | String/null | 法人证件正反面永久文件地址 |
|
||||
| `contactPhone` | String/null | 完整公司联系电话 |
|
||||
| `establishDate` | String/null | 成立日期,格式 `yyyy-MM-dd` |
|
||||
| `registeredCapital` / `businessScope` | String/null | 注册资本与经营范围 |
|
||||
| `address` | String/null | 本次新增回显:注册地址或经营地址 |
|
||||
| `staffScale` | String/null | 人员规模字典值 |
|
||||
| `mainCooperation` | String | 主要合作内容 |
|
||||
| `remark` | String/null | 本次新增回显:供应商主体内部备注 |
|
||||
| `types` | Array | 类型完整集合;每项包含 `isPrimary` |
|
||||
| `primaryTypeCode` / `primaryTypeName` | String/null | 当前主类型 |
|
||||
| `contacts` / `qualifications` / `contracts` | Array | 联系人、资质和合同完整集合;现有项包含字符串 ID 与 `updateTime` |
|
||||
| `status` | String | 当前供应商状态 |
|
||||
| `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",
|
||||
"fullName": "示例旅行服务有限公司",
|
||||
"shortName": "示例旅行",
|
||||
"tax_no": "91350211M000100Y46",
|
||||
"legalRepresentative": "示例法人",
|
||||
"legalRepresentativeIdNo": "11010519491231002X",
|
||||
"legalRepresentativeIdNoMask": "11010519491231002X",
|
||||
"legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/id-front.jpg",
|
||||
"legalRepresentativeIdCardBackUrl": "https://files.example.com/supplier/id-back.jpg",
|
||||
"contactPhone": "13800138000",
|
||||
"contactPhoneMask": "13800138000",
|
||||
"establishDate": "2020-01-01",
|
||||
"registeredCapital": "100万元",
|
||||
"businessScope": "境内旅游服务",
|
||||
"address": "福建省厦门市示例路 1 号",
|
||||
"staffScale": "LT50",
|
||||
"mainCooperation": "酒店与景区资源合作",
|
||||
"remark": "重点合作供应商",
|
||||
"types": [{"typeCode": "HOTEL", "typeName": "酒店", "isPrimary": true}],
|
||||
"primaryTypeCode": "HOTEL",
|
||||
"primaryTypeName": "酒店",
|
||||
"contacts": [],
|
||||
"qualifications": [],
|
||||
"contracts": [],
|
||||
"status": "DRAFT",
|
||||
"updateTime": "2026-08-27 15:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
历史供应商未填写地址或备注时,字段明确返回 `null`;无类型、联系人、资质或合同时,相应集合返回空数组,不把缺失数据当成接口异常:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2092800000000000001",
|
||||
"address": null,
|
||||
"remark": null,
|
||||
"types": [],
|
||||
"primaryTypeCode": null,
|
||||
"primaryTypeName": null,
|
||||
"contacts": [],
|
||||
"qualifications": [],
|
||||
"contracts": [],
|
||||
"updateTime": "2026-08-27 15:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
供应商不存在、已删除或超出数据范围时不返回空详情:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395001,
|
||||
"message": "供应商不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 要求可信管理身份、`supplier:view` 平台权限和供应商数据范围;登录态不能代替业务权限。
|
||||
- `address`、`remark` 属于响应的向后兼容增加;旧客户端忽略未知字段即可继续运行。
|
||||
- `legalRepresentativeIdNoMask`、`contactPhoneMask` 等废弃兼容别名继续与完整值字段同值;新代码使用无 `Mask` 字段。
|
||||
- Long ID 一律按字符串保存、比较和传输。
|
||||
- 本接口只读,不修改主体版本、缓存或审批状态。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
以下三个既有写接口没有新增路径或字段,但共同业务口径由本工单固定:
|
||||
|
||||
- 创建草稿:`POST /admin/supplier/items/add`
|
||||
- 提交注册:`POST /admin/supplier/items/{supplierId}/submit`
|
||||
- 更新资料:`PUT /admin/supplier/items/{supplierId}/update`
|
||||
|
||||
### 共同主体字段规则
|
||||
|
||||
| 字段 | 创建/提交 | 更新 | 统一约束 |
|
||||
|---|---|---|---|
|
||||
| `fullName` | 必填 | 可选 | 最长 500 字符 |
|
||||
| `shortName` | 可选 | 可选 | 最长 300 字符 |
|
||||
| `taxNo` | 必填 | 可选,且仅允许在状态规则内修改 | 6 至 64 位字母、数字或展示分隔符;身份证/统一社会信用代码还校验日期或校验位 |
|
||||
| `legalRepresentative` | 可选 | 可选 | 最长 500 字符 |
|
||||
| `legalRepresentativeIdNo` | 可选 | 可选 | 18 位合法居民身份证号;与正反面地址按完整三字段组合校验 |
|
||||
| `legalRepresentativeIdCardFrontUrl` / `legalRepresentativeIdCardBackUrl` | 可选 | 可选 | 公网 HTTPS 永久文件地址,单项最长 1000 字符 |
|
||||
| `contactPhone` | 可选 | 可选 | 7 至 20 位合法电话字符 |
|
||||
| `establishDate` | 可选 | 可选 | `yyyy-MM-dd`,不得晚于当前日期 |
|
||||
| `registeredCapital` | 可选 | 可选 | 最长 50 字符 |
|
||||
| `businessScope` | 可选 | 可选 | 最长 500 字符 |
|
||||
| `address` | 可选 | 可选 | 最长 500 字符;创建和更新同口径 |
|
||||
| `staffScale` | 可选 | 可选 | `LT50`、`R50_200`、`R200_500`、`GT500` |
|
||||
| `mainCooperation` | 必填 | 可选 | 创建/提交不能为空白;更新传值时按同一业务含义保存 |
|
||||
| `licenseImageUrl` | 可选 | 可选 | 最长 500 字符,并与营业执照资质归一化 |
|
||||
| `remark` | 可选 | 可选 | 供应商主体内部备注,不等同于审批意见或 `changeReason` |
|
||||
|
||||
集合上限与语义保持不变:`types` 最多 15 项;`contacts`、`qualifications`、`contracts` 各最多 100 项;`initialAccounts` 最多 1 项。非空类型必须来自生效字典且不得重复,非空联系人集合必须形成唯一默认联系人,资质和合同继续执行类型、日期、金额、归属和完整快照校验。
|
||||
|
||||
更新场景额外要求:
|
||||
|
||||
- `changeReason` 必填、非空白、最长 500 字符,并进入业务审计。
|
||||
- `expectedUpdateTime` 必须等于详情最新 `updateTime`。
|
||||
- 已有联系人、资质、合同随完整快照更新时,必须带回对应字符串 ID 和子项 `updateTime`。
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
| 场景 | payload / 结果 |
|
||||
|---|---|
|
||||
| ✅ 更新地址和备注 | `{"address":"福建省厦门市示例路 2 号","remark":"地址已确认","changeReason":"更新地址和备注","expectedUpdateTime":"2026-08-27 15:30:00"}` |
|
||||
| ✅ 只更新备注 | `{"remark":"新的内部备注","changeReason":"补充内部备注","expectedUpdateTime":"2026-08-27 15:30:00"}` |
|
||||
| ❌ 地址超过 500 字符 | 创建、更新均返回 `400`,零写入 |
|
||||
| ❌ 成立日期晚于当前日期 | 创建、更新均返回 `400`,零写入 |
|
||||
| ❌ 更新缺少 `changeReason` | 返回 `400`,零写入 |
|
||||
| ❌ 使用旧 `expectedUpdateTime` | 返回 `395014`,零写入;必须刷新详情后由用户确认 |
|
||||
|
||||
### 正确更新顺序
|
||||
|
||||
1. 先查询详情,保存字符串 ID、子项版本和根对象 `updateTime`。
|
||||
2. 用户提交时只发送实际变更字段,并附非空 `changeReason`、最新 `expectedUpdateTime`。
|
||||
3. 更新成功后采用响应里的新版本;需要完整表单时重新查询详情。
|
||||
4. 遇到 `395014` 先刷新,不得自动用旧表单覆盖他人修改。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 成功创建、提交或更新时,主体与本次携带的类型、联系人、资质、合同及业务审计按聚合事务一起成功。
|
||||
- 地址和备注保存后,详情读取与变更审计使用同一业务值;`remark` 不会被提升为审批意见或变更原因。
|
||||
- 请求校验、权限、状态、归属或并发版本失败时零写入,主体 `updateTime` 不变化。
|
||||
- 本次没有数据库结构变更、Flyway migration 或历史数据回填;旧记录的地址/备注原值保持不变。
|
||||
- 不新增 Redis、MQ、Feign 或跨服务副作用。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录或 Token 失效:`401`,不把无认证响应当作正向验收。
|
||||
- ADMIN 等无写权限角色调用创建、提交或更新:`395002`,零写入;已有详情仍按读取权限返回。
|
||||
- 供应商不存在或已删除:`395001`。
|
||||
- 并发版本过期:`395014`,调用方刷新详情后重提。
|
||||
- 地址超过 500 字符、未来成立日期、必填字段缺失或组合不完整:`400`,零写入。
|
||||
- 历史 `address`、`remark` 为空:详情返回 `null`,不报错、不猜测默认值。
|
||||
- `ARCHIVED` 等不可修改状态继续由既有状态门禁拒绝更新。
|
||||
- 业务失败可能仍使用 HTTP 200,调用方必须同时判断响应体 `code`、`success`、`data`。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### `staffScale`(供应商人员规模稳定值)
|
||||
|
||||
**所属字段**: `SupplierDraftSaveReqVO.staffScale / SupplierUpdateReqVO.staffScale / SupplierBasicInfoRespVO.staffScale` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `LT50` | 50 人以下 | 小于 50 人 |
|
||||
| `R50_200` | 50 至 200 人 | 50 至 200 人档 |
|
||||
| `R200_500` | 200 至 500 人 | 200 至 500 人档 |
|
||||
| `GT500` | 500 人以上 | 大于 500 人 |
|
||||
|
||||
`types[].typeCode`、`contacts[].contactRole`、`qualifications[].qualType` 继续从对应生效数据字典读取,不在客户端硬编码展示名。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 详情根对象 `address` | 未返回,已保存值无法用于表单回填 | 返回保存的地址;未填写为 `null` |
|
||||
| 详情根对象 `remark` | 未返回,已保存值无法用于表单回填 | 返回主体内部备注;未填写为 `null` |
|
||||
| 创建/提交/更新请求字段 | 已存在 | 无新增字段、无新增必填项 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 打开编辑页 | 详情缺少地址和备注,表单初始化不完整 | 一次详情调用可完整初始化主体字段 |
|
||||
| 共同字段校验 | 实现存在,但缺少跨创建/更新契约锁定 | 自动化和 TEST 验收固定创建/更新同口径 |
|
||||
| 完整敏感值回显 | 按 #6436/#6499 返回授权完整值 | 保持不变 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否。响应新增两个可空字段,旧客户端可忽略;请求无新增必填项。
|
||||
- **前端是否必须同步上线**: 为闭合编辑体验需要接入 `address`、`remark`,但不构成旧客户端继续运行的硬门禁。
|
||||
- **前端 workaround 清理点**: 删除编辑页对地址、备注的本地空值兜底,改为使用详情真实值。
|
||||
- **QA 重点**: 新建后打开详情、更新后刷新详情、地址长度 500/501、今天/未来成立日期、ADMIN 越权、旧版本并发冲突。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台供应商详情/编辑表单的主体地址、内部备注回填,以及已有写接口校验口径的契约固定。
|
||||
- **零影响**:
|
||||
- Gateway 路由和认证级别。
|
||||
- 供应商生命周期状态机、审批、收款账户、证明附件、资源关系及归档语义。
|
||||
- 数据库结构、历史数据、Nacos 配置、Redis、MQ、Feign 和其他服务。
|
||||
- 小程序、C 端、Web、H5、桌面端接口。
|
||||
- #6436/#6499 的授权完整值和废弃 `*Mask` 兼容语义。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- 本地自动化:供应商定向测试 49 项通过;最新 `dev-v3` 上 `hl-resource-service` Reactor 全量 2,153 项,0 失败、0 错误、38 项条件跳过。
|
||||
- 部署:Deploy Panel API 任务 `938ca09e` 终态 `success`;目标与实际提交均为 `b269e5fdca2e1d29e0336314a909885e9d1f9d16`,包含本工单合并提交 `a952a04c`。
|
||||
- 采样:部署任务期 8 个有效采样;每个采样至少 2 个运行进程、2 个健康启用 Nacos 实例,零观测不可用采样。该证据只证明采样点,不代表采样间绝对连续。
|
||||
- 真实 Gateway:SUPER_ADMIN 创建完整表单后,详情精确回显 `address`、`remark`、完整授权字段、主类型及联系人/资质/合同 ID 和版本;更新后 API、数据库主体与更新审计一致。
|
||||
- 失败路径:创建/更新的地址超长均返回 `400`;创建/更新的未来成立日期均返回 `400`;ADMIN 更新返回 `395002`;所有失败均验证零写入、版本不变。
|
||||
- 清理:临时供应商先经业务删除接口软删除,确认主体已删除且活动子项为 0;随后按本次唯一标记精确清理 8 行测试数据,17 张关联表回读,剩余供应商 0 行;测试会话已注销并验证失效。
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|---|---|---|---|
|
||||
| #6478 / #6483 / #6486 | #6436 | 供应商授权完整值、主类型及同日修正 | ✅ 有效,本次保持 |
|
||||
| #6517 | #6476 | 详情补齐地址备注并固定表单校验契约 | ✅ 最新 |
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue:[#6476](https://git.1814.love:8443/wx/HL/issues/6476)
|
||||
- 关联 PR:[#6517](https://git.1814.love:8443/wx/HL/pulls/6517)
|
||||
- 相关完整值契约:`changelogs-v2/2026-08/27_6436_供应商敏感字段完整回显与主类型-修改接口-管理后台.md`
|
||||
- 管理端接入:读取详情 `address`、`remark`,更新时保留最新 `expectedUpdateTime`;前端引用待回填。
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 从最新 `dev-v3` 创建独立回退分支,执行:
|
||||
|
||||
```bash
|
||||
git revert -m 1 --no-edit a952a04cf80f24d6c50cbec4b992c82f8580beba
|
||||
```
|
||||
|
||||
2. 运行供应商定向测试、`hl-resource-service` 全量测试和差异检查,经独立 PR 合入。
|
||||
3. 使用同一 Deploy Panel 两阶段客户端,仅滚动部署 `hl-resource-service`,并绑定批准回退分支的精确提交。
|
||||
4. 管理端停止依赖详情响应中的 `address`、`remark`,再撤回本 Changelog;既有请求字段和完整值契约保持不变。
|
||||
5. 无数据库、配置、Redis 或 MQ 恢复步骤;已保存的地址和备注数据保留,不做破坏性回填或删除。
|
||||
6. 撤回后经 Gateway 复测详情、创建、更新、未认证、越权、并发冲突和失败零写入,并确认 Resource 双实例与 Nacos 健康。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#6476](https://git.1814.love:8443/wx/HL/issues/6476)
|
||||
- **PR**: [#6517](https://git.1814.love:8443/wx/HL/pulls/6517)
|
||||
- **实现提交**: [`3cd07d27`](https://git.1814.love:8443/wx/HL/commit/3cd07d27dc876f17af1751ec7ef9de2ddbd71949)
|
||||
- **Merge commit**: [`a952a04c`](https://git.1814.love:8443/wx/HL/commit/a952a04cf80f24d6c50cbec4b992c82f8580beba)
|
||||
- **TEST 目标提交**: [`b269e5fd`](https://git.1814.love:8443/wx/HL/commit/b269e5fdca2e1d29e0336314a909885e9d1f9d16)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @lc
|
||||
- **管理端负责人**: @mmg(`frontend_status: pending`)
|
||||
@@ -0,0 +1,308 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6518"
|
||||
title: "供应商草稿创建即生成编号"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "d546912c"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-08-27"
|
||||
status_note: "PR #6531 已合并 dev-v3(合并提交 66aed70a);hl-resource-service 已由 Deploy Panel 任务 e977bf7b 精确发布该提交。真实 TEST 身份完成 47 项 Gateway 断言,验证创建即返回 SUP{supplierId}、列表/详情/更新稳定回显、权限失败零写入、审计事实和业务清理。管理端需停止把新建草稿 supplierNo 当作空值或等待审批后再取。"
|
||||
updated_at: "2026-08-27"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 🔧 供应商管理:草稿创建即生成编号
|
||||
|
||||
`POST /admin/supplier/items/add` 成功后,响应中的 `supplierNo` 现在立即为非空字符串,格式固定为 `SUP{supplierId}`。该编号与本次创建的供应商绑定,后续列表、详情、更新、提交及审批流程均保持同一值。
|
||||
|
||||
请求结构、权限点、状态机和业务错误码没有新增或删除。
|
||||
|
||||
## 一、背景
|
||||
|
||||
此前供应商编号在审批通过时才补齐,因此新建草稿阶段的创建响应、列表和详情可能返回 `supplierNo: null`。管理端若需要展示或引用编号,只能等待审批或使用临时占位。
|
||||
|
||||
本次发布后:
|
||||
|
||||
- 新建草稿成功即取得正式编号,不再存在“先创建、后编号”的等待窗口。
|
||||
- 编号格式为字符串 `SUP{supplierId}`;例如 `supplierId="2094000000000000001"` 对应 `supplierNo="SUP2094000000000000001"`。
|
||||
- 编号在更新、提交、审批和状态变化中不重算、不覆盖。
|
||||
- 草稿删除后原编号不再分配给其他供应商。
|
||||
- 发布前已经存在且编号为空的历史记录仍可能返回 `null`;其审批通过时继续兼容补齐。调用方不能自行推导或写入编号。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---:|---|---|---|---|---|
|
||||
| 1 | 创建供应商注册草稿 | POST | `/admin/supplier/items/add` | 响应行为修改 | 成功响应的 `supplierNo` 从草稿期可空改为立即返回 `SUP{supplierId}` |
|
||||
|
||||
列表、详情、更新和审批相关响应中的 `supplierNo` 字段结构未变化;对于本次发布后创建的供应商,它们会稳定返回创建响应中的同一编号。
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 创建供应商注册草稿 `POST /admin/supplier/items/add`
|
||||
|
||||
**VO**: `SupplierDraftSaveReqVO` → `Result<SupplierWriteRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理端首次保存供应商注册表单。创建成功后可立即展示、复制或继续传递后端返回的正式 `supplierNo`,无需等待提交审批。
|
||||
|
||||
#### 入参
|
||||
|
||||
本次没有新增请求字段,`supplierNo` 也不允许由客户端提交。
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `fullName` | Body | String | 是 | 非空,最长 500 | 供应商法定全称 |
|
||||
| `shortName` | Body | String/null | 否 | 最长 300 | 供应商简称 |
|
||||
| `taxNo` | Body | String | 是 | 6 至 64 位字母、数字或展示分隔符 | 主体证件号 |
|
||||
| `types` | Body | Array/null | 否 | 最多 15 项;非空时类型值有效且不得重复 | 供应商类型完整集合 |
|
||||
| `legalRepresentative` | Body | String/null | 否 | 最长 500 | 法定代表人 |
|
||||
| `legalRepresentativeIdNo` | Body | String/null | 否 | 为空或合法 18 位居民身份证号 | 法人证件号 |
|
||||
| `legalRepresentativeIdCardFrontUrl` / `legalRepresentativeIdCardBackUrl` | Body | String/null | 否 | 单项最长 1000 | 法人证件正反面永久地址 |
|
||||
| `contactPhone` | Body | String/null | 否 | 7 至 20 位合法电话字符 | 公司联系电话 |
|
||||
| `establishDate` | Body | String/null | 否 | `yyyy-MM-dd`,不得晚于当天 | 成立日期 |
|
||||
| `registeredCapital` | Body | String/null | 否 | 最长 50 | 注册资本文本 |
|
||||
| `businessScope` / `address` | Body | String/null | 否 | 单项最长 500 | 经营范围与地址 |
|
||||
| `staffScale` | Body | String/null | 否 | `LT50`、`R50_200`、`R200_500`、`GT500` | 人员规模 |
|
||||
| `mainCooperation` | Body | String | 是 | 非空 | 主要合作内容 |
|
||||
| `licenseImageUrl` | Body | String/null | 否 | 最长 500 | 营业执照影像快捷字段 |
|
||||
| `remark` | Body | String/null | 否 | - | 供应商内部备注 |
|
||||
| `contacts` | Body | Array/null | 否 | 最多 100 项 | 联系人完整集合;非空时继续执行角色和唯一默认联系人规则 |
|
||||
| `qualifications` | Body | Array/null | 否 | 最多 100 项 | 资质完整集合;继续执行类型、证号和有效期规则 |
|
||||
| `contracts` | Body | Array/null | 否 | 最多 100 项 | 合同完整集合;继续执行日期、金额和状态规则 |
|
||||
| `initialAccounts` | Body | Array/null | 否 | 最多 1 项 | 初始收款账户集合 |
|
||||
| `duplicateConfirmToken` | Body | String/null | 否 | 最长 256,一次性使用 | 存在近似主体时按后端返回值二次确认 |
|
||||
| `expectedUpdateTime` | Body | String/null | 否 | 创建草稿时不传 | 仅已有草稿提交审批时使用 |
|
||||
|
||||
#### 出参 `Result<SupplierWriteRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `supplierId` | String | 新建供应商 ID;必须按字符串保存和传输 |
|
||||
| `supplierNo` | String | 本次行为变化:创建成功立即返回 `SUP{supplierId}`,非空且全生命周期稳定 |
|
||||
| `status` | String | 新建草稿固定为 `DRAFT` |
|
||||
| `onboardingStage` | String | 新建草稿固定为 `PROFILE_DRAFT` |
|
||||
| `initialAccounts` | Array | 初始收款账户摘要;未提交时通常为空数组 |
|
||||
| `updateTime` | String | 当前并发版本,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /admin/supplier/items/add
|
||||
Authorization: Bearer <admin-token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "示例草原旅行服务有限公司",
|
||||
"shortName": "示例草原旅行",
|
||||
"taxNo": "91350211M000100Y46",
|
||||
"mainCooperation": "酒店与景区资源合作",
|
||||
"types": [],
|
||||
"contacts": [],
|
||||
"qualifications": [],
|
||||
"contracts": [],
|
||||
"initialAccounts": []
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2094000000000000001",
|
||||
"supplierNo": "SUP2094000000000000001",
|
||||
"status": "DRAFT",
|
||||
"onboardingStage": "PROFILE_DRAFT",
|
||||
"initialAccounts": [],
|
||||
"updateTime": "2026-08-27 16:24:30"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
创建接口没有“成功但 data 为空”的降级分支:创建成功必须返回完整 `SupplierWriteRespVO`,失败则返回明确业务错误。编号的历史空数据兼容只出现在发布前遗留记录的列表或详情读取中;这类尚未补齐的记录仍可能出现:
|
||||
|
||||
```json
|
||||
{
|
||||
"supplierId": "2089000000000000001",
|
||||
"supplierNo": null,
|
||||
"status": "DRAFT"
|
||||
}
|
||||
```
|
||||
|
||||
管理端可为这种历史空值显示 `—`,但不得本地生成或回写编号。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
请求缺少必填字段或字段不合法时,业务失败且不创建草稿:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "统一社会信用代码不能为空",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
真实身份没有 `supplier:create` 写权限时:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395002,
|
||||
"message": "无供应商写入权限",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 外部请求必须经过 Gateway;未认证返回业务码 `401`,不能把该负向结果当作正向验收。
|
||||
- 写操作仅允许既有授权角色并再次校验 `supplier:create`;普通 `ADMIN` 被拒绝且零写入。
|
||||
- `supplierNo` 只在整个创建成功后返回;任何字段校验、权限、重复主体确认或持久化失败都不会留下可见的半成品编号。
|
||||
- 创建成功后,列表、详情和更新响应必须返回同一 `supplierNo`。
|
||||
- 客户端只消费后端响应,不能提交、覆盖、重算、截断或把编号转成数字。
|
||||
- 业务失败可能仍使用 HTTP 200,必须同时判断响应体 `code`、`success` 和 `data`。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### 正确消费顺序
|
||||
|
||||
1. 提交创建请求并确认 `code=200`、`success=true`。
|
||||
2. 将 `data.supplierId`、`data.supplierNo`、`data.updateTime` 全部按字符串保存。
|
||||
3. 页面立即展示 `data.supplierNo`;刷新列表或详情时核对同一字段,不再等待审批。
|
||||
4. 后续更新继续传最新 `expectedUpdateTime`,但不得把 `supplierNo` 放进请求体。
|
||||
|
||||
### ✅ 正确 / ❌ 错误行为对照
|
||||
|
||||
| 场景 | 正确行为 |
|
||||
|---|---|
|
||||
| ✅ 新草稿创建成功 | 直接显示响应 `supplierNo`,例如 `SUP2094000000000000001` |
|
||||
| ✅ 列表或详情刷新 | 使用接口返回的同一编号,不做本地拼接 |
|
||||
| ✅ 历史草稿仍为空 | 显示 `—` 并等待后端兼容补齐,不写回猜测值 |
|
||||
| ❌ 本地生成编号 | 禁止用时间、计数器或客户端缓存生成 |
|
||||
| ❌ 把编号当数字 | 禁止移除 `SUP`、转数值或做算术运算 |
|
||||
| ❌ 删除后复用 | 禁止把已删除供应商的旧编号分配给新供应商 |
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 创建成功时,供应商草稿、正式编号、可选子项和创建审计作为一个业务整体生效;响应中的编号与后续读取一致。
|
||||
- 创建失败时,供应商和编号都不产生可见结果,不返回部分成功。
|
||||
- 更新、提交或审批不改变已生成编号。
|
||||
- 草稿删除后不再出现在正常列表和详情中,但其编号不会重新分配。
|
||||
- 本次不要求调用方执行数据迁移,也不改变现有请求字段。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未认证:业务码 `401`,零写入。
|
||||
- 无写权限:业务码 `395002`,零写入。
|
||||
- 必填字段缺失、格式或组合不合法:业务码 `400`,零写入。
|
||||
- 发现近似主体且未完成二次确认:业务码 `395007`,按响应中的一次性确认信息继续既有流程。
|
||||
- 发布后新草稿:`supplierNo` 必为非空 `SUP{supplierId}`。
|
||||
- 发布前历史空编号:读取时仍允许 `null`;审批通过后按兼容规则补齐。
|
||||
- 更新、提交、审批、暂停、拉黑等后续操作:编号保持不变。
|
||||
- 软删除后再创建:新供应商获得新的 `supplierId` 和新的 `supplierNo`,不复用旧编号。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
本次没有新增枚举或数据字典。`status`、`onboardingStage`、供应商类型、联系人角色、资质类型等继续沿用现有取值;编号格式不是字典值。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 创建响应 `data.supplierNo` | 新草稿通常为 `null`,审批通过后才补齐 | 创建成功立即返回非空 `SUP{supplierId}` |
|
||||
| `supplierId`、`status`、`onboardingStage`、`initialAccounts`、`updateTime` | 已存在 | 字段和类型不变 |
|
||||
| 创建请求 | 不接受 `supplierNo` | 仍不接受 `supplierNo`,请求无新增字段 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 新建后展示编号 | 需要空值占位或等待审批 | 创建成功即可展示正式编号 |
|
||||
| 列表、详情、更新回显 | 草稿期可能为空 | 对发布后新建草稿稳定返回创建时编号 |
|
||||
| 提交与审批 | 审批通过时生成编号 | 保留创建时编号;只为历史空值兼容补齐 |
|
||||
| 删除后再次创建 | 无明确前端口径 | 新供应商取得新编号,旧编号不复用 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否。字段已存在,只把发布后新草稿的值从 `null` 收紧为稳定非空字符串。
|
||||
- **前端是否必须同步上线**: 需要适配体验。旧客户端仍可运行,但会继续把已经可用的编号显示为空或等待审批。
|
||||
- **前端 workaround 清理点**: 删除“草稿编号为空”“审批通过后再刷新编号”的新建流程兜底,创建成功后直接使用 `data.supplierNo`。
|
||||
- **历史兼容**: 仍保留对发布前空编号记录的 `null` 展示兼容,前端不要把所有历史数据强断言为非空。
|
||||
- **QA 重点**: 创建响应、立即列表/详情、更新后详情、未认证、ADMIN 越权、非法请求零写入、软删除后新编号不复用。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台供应商草稿创建成功后的编号生成时点和后续稳定回显。
|
||||
- **零影响**:
|
||||
- 创建请求字段、供应商类型可选语义、联系人/资质/合同/初始账户规则。
|
||||
- Gateway 路由、认证方式、权限码和数据范围。
|
||||
- 供应商生命周期状态机、审批级数、暂停、拉黑、归档和资源关系行为。
|
||||
- 小程序、C 端、Web、H5、桌面端接口。
|
||||
- 其他服务接口、缓存和消息契约。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- 本地自动化:供应商编号、审批兼容、完整回显、事务边界、权限和迁移契约定向测试 61 项通过;`hl-resource-service` 全量 2,157 项零失败、零错误,38 项仓库既有条件跳过;Gateway 路由审计 4 项通过。
|
||||
- 部署:Deploy Panel API 任务 `e977bf7b` 终态 `success`;目标与实际提交均为 `66aed70a2c9919046b9e4362b9db3219858ec6e1`。
|
||||
- 可用性采样:任务期 6 个有效采样均至少有 2 个运行进程、2 个健康启用实例,零观测不可用采样;该证据只证明采样点,不代表采样间绝对连续。
|
||||
- 真实 Gateway:SUPER_ADMIN 创建草稿后,响应立即满足 `supplierNo=SUP{supplierId}`;列表、详情、更新响应保持同一编号;独立变更记录同时存在 CREATE/UPDATE 事实。
|
||||
- 失败路径:未认证请求被 Gateway 拒绝;真实 ADMIN 创建返回 `395002`;非法税号返回 `400`;两类失败均验证零可见写入。TEST 未配置可用 FINANCE 身份,其角色等价性由服务端权限测试覆盖,未伪造身份。
|
||||
- 删除与不复用:首个草稿经业务 API 软删除并确认列表、详情不可见;随后创建的新草稿获得不同编号。
|
||||
- 清理:两条临时草稿均经业务 API 软删除并确认不可见,测试角色已恢复,会话已注销;未执行物理删除、缓存或 MQ 操作,仅保留正常的创建、更新、删除审计事实。
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|---|---|---|---|
|
||||
| #6274 | #6273 | 审批记录响应增加 `supplierNo` 字段 | ✅ 有效,本次收紧新草稿取值时点 |
|
||||
| #6344 | #6343 | 新建和修改允许供应商类型为空 | ✅ 有效,请求语义不变 |
|
||||
| #6517 | #6476 | 详情补齐地址备注并统一表单校验 | ✅ 有效,详情继续稳定回显 |
|
||||
| #6531 | #6518 | 草稿创建即生成唯一供应商编号 | ✅ 最新 |
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue:[#6518](https://git.1814.love:8443/wx/HL/issues/6518)
|
||||
- 关联 PR:[#6531](https://git.1814.love:8443/wx/HL/pulls/6531)
|
||||
- 历史编号响应契约:`changelogs-v2/2026-08/24_6273_供应商审批记录补充供应商编号-修改接口-管理后台.md`
|
||||
- 管理端接入:创建成功立即读取并展示 `data.supplierNo`;历史空值仍保留 `—` 兼容,前端引用待回填。
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 管理端先恢复“新草稿编号可能为空”的兼容展示,不再依赖创建响应立即有值。
|
||||
2. 后端从最新 `dev-v3` 创建独立回退分支,revert #6518 合并提交 `66aed70a2c9919046b9e4362b9db3219858ec6e1`,验证后经独立 PR 合入并重新发布 `hl-resource-service`。
|
||||
3. 无接口字段删除或请求结构回退;发布窗口内已生成的编号默认保留,不执行批量清空或复用。
|
||||
4. 撤回后经 Gateway 复测创建响应可空、历史审批补齐、列表/详情/更新兼容、未认证与越权零写入。
|
||||
5. 以新的 Changelog 提交标记本契约撤回,不改写已发布提交历史。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#6518](https://git.1814.love:8443/wx/HL/issues/6518)
|
||||
- **PR**: [#6531](https://git.1814.love:8443/wx/HL/pulls/6531)
|
||||
- **实现提交**: [`69665d72`](https://git.1814.love:8443/wx/HL/commit/69665d7249b5d269e56c36abbfeafbdbbbe72d45)
|
||||
- **Merge commit**: [`66aed70a`](https://git.1814.love:8443/wx/HL/commit/66aed70a2c9919046b9e4362b9db3219858ec6e1)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @lc
|
||||
- **管理端负责人**: 待认领(`frontend_status: pending`)
|
||||
@@ -0,0 +1,524 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6544"
|
||||
title: "供应商合同独立登记"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
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,按用户确认冻结权限依赖的真实写复测,未配置权限、未发业务写请求,不把缺失权限记为通过。"
|
||||
updated_at: "2026-08-29"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商管理:合同独立登记
|
||||
|
||||
供应商合同从注册聚合中拆出,改为独立登记、独立更新、独立软删除的维护边界。合同不再随供应商建档审批提交,审批通过与否都不影响合同登记;已登记合同在详情回显中继续随供应商返回。
|
||||
|
||||
## 一、背景
|
||||
|
||||
旧版把合同作为供应商注册聚合的一部分随审批走(#6397),审批候选 v2 起不再携带 contracts。为支持签约后可随时登记线下合同、按业务状态独立更新/作废,本次新增三个独立合同写接口:登记(add)、完整替换更新(update)、软删除(del)。合同不参与供应商生命周期状态机,不受建档/审批/归档状态推进约束;删除草稿供应商时若仍有未删除合同会被拒绝(需先独立删合同)。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---:|---|---|---|---|---|
|
||||
| 1 | 独立登记供应商合同 | POST | `/admin/supplier/items/{supplierId}/contracts/add` | 新增写接口 | 在既有供应商下登记一份线下合同,不进入建档审批 |
|
||||
| 2 | 独立更新供应商合同 | PUT | `/admin/supplier/items/{supplierId}/contracts/{contractId}/update` | 新增写接口 | 带乐观版本整份替换合同全部业务字段 |
|
||||
| 3 | 独立删除供应商合同 | DELETE | `/admin/supplier/items/{supplierId}/contracts/{contractId}/del` | 新增写接口 | 带乐观版本软删除,审计原因必填 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 独立登记供应商合同 `POST /admin/supplier/items/{supplierId}/contracts/add`
|
||||
|
||||
**VO**: `SupplierContractCreateReqVO` / `SupplierContractRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理端在供应商详情「合同」区域新增一条线下合同。合同登记与供应商审批解耦,登记成功立即在详情合同列表随时间返回 `updateTime` 并发版本。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商;不得转为 JavaScript Number |
|
||||
| `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`,不得早于 `startDate` | 有效期结束日期 |
|
||||
| `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>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `contractId` | String | 合同 ID(雪花,保证转字符串传输) |
|
||||
| `contractName` | String | 合同名称 |
|
||||
| `contractNo` | String/null | 合同编号 |
|
||||
| `contractType` | String | 合同类型 |
|
||||
| `signDate` | String/null | 合同签署日期 |
|
||||
| `startDate` | String | 有效期开始日期 |
|
||||
| `endDate` | String | 有效期结束日期 |
|
||||
| `amount` | String | 合同金额(保证转字符串传输,保留两位小数场景由后端规范) |
|
||||
| `pricingMode` | String/null | 计价方式 |
|
||||
| `settleCycle` | String/null | 结算周期 |
|
||||
| `status` | String | 合同状态;历史终止合同可能返回 `TERMINATED` |
|
||||
| `scanFileUrl` | String/null | 合同扫描件地址 |
|
||||
| `remark` | String/null | 合同备注 |
|
||||
| `updateTime` | String | 当前并发版本,格式 `yyyy-MM-dd HH:mm:ss`;请原样用于下一次 update/del |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /admin/supplier/items/2091381911266967553/contracts/add
|
||||
Authorization: Bearer <admin-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"contractName": "2026年度框架合同",
|
||||
"contractNo": "HT-2026-001",
|
||||
"contractType": "FRAME",
|
||||
"signDate": "2026-08-29",
|
||||
"startDate": "2026-09-01",
|
||||
"endDate": "2027-08-31",
|
||||
"amount": "1200.50",
|
||||
"pricingMode": "按团结算",
|
||||
"settleCycle": "MONTHLY",
|
||||
"status": "ACTIVE",
|
||||
"scanFileUrl": "https://files.example.com/contracts/1.pdf",
|
||||
"remark": "线下签署后登记",
|
||||
"changeReason": "线下签署后登记"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"contractId": "2093378327078154241",
|
||||
"contractName": "2026年度框架合同",
|
||||
"contractNo": "HT-2026-001",
|
||||
"contractType": "FRAME",
|
||||
"signDate": "2026-08-29",
|
||||
"startDate": "2026-09-01",
|
||||
"endDate": "2027-08-31",
|
||||
"amount": "1200.50",
|
||||
"pricingMode": "按团结算",
|
||||
"settleCycle": "MONTHLY",
|
||||
"status": "ACTIVE",
|
||||
"scanFileUrl": "https://files.example.com/contracts/1.pdf",
|
||||
"remark": "线下签署后登记",
|
||||
"updateTime": "2026-08-29 00:41:00"
|
||||
},
|
||||
"traceId": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
登记成功后 `data` 恒为完整合同对象(不会返回 null)。金额为空时 `amount` 返回 null,前端按无金额展示,不要把 null 当 `"0.00"`:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"contractId": "2093378327078154241",
|
||||
"contractName": "无金额合同",
|
||||
"contractNo": null,
|
||||
"contractType": "PURCHASE",
|
||||
"signDate": null,
|
||||
"startDate": "2026-09-01",
|
||||
"endDate": "2027-08-31",
|
||||
"amount": null,
|
||||
"pricingMode": null,
|
||||
"settleCycle": null,
|
||||
"status": "DRAFT",
|
||||
"scanFileUrl": null,
|
||||
"remark": null,
|
||||
"updateTime": "2026-08-29 00:41:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
供应商不存在或已软删除:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395001,
|
||||
"message": "供应商不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
已归档供应商拒绝合同写入(不查询、不锁行、不落审计):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395031,
|
||||
"message": "已归档供应商仅允许查看",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
正常 HTTP 请求先经过 Bean Validation;缺必填字段或长度超限仍返回 `400` 和中文字段文案。`395052`–`395056` 是 Service/事务层防旁路校验的冻结错误码,不替代 Controller 的 `400`:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "合同名称不能为空",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 权限:可信管理员身份 + `FINANCE`/`SUPER_ADMIN` 写角色 + `supplier:update` 平台权限;`ADMIN` 或任一平台权限门禁失败均不产生数据库写。
|
||||
- 合同登记只写 `supplier_contract` 单表与一条 `CREATE` 审计事实,不推进供应商主体版本,不进审批。
|
||||
- 幂等:同一管理员 + 同一供应商 + 同一规范化载荷 5 秒窗口内重复提交只执行一次;锁键按供应商分区。
|
||||
- 事务内先锁供应商行(FOR UPDATE),改合同必在事务中完成,失败整体回滚。
|
||||
- `supplierId`、`contractId`、`amount` 必须按字符串处理,禁止转 JavaScript Number。
|
||||
|
||||
### 2. 独立更新供应商合同 `PUT /admin/supplier/items/{supplierId}/contracts/{contractId}/update`
|
||||
|
||||
**VO**: `SupplierContractUpdateReqVO`(继承 Create 全字段 + `expectedUpdateTime`) / `SupplierContractRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理端对已登记合同做整份替换更新。请求体必须携带目标合同当前 `updateTime` 作为乐观版本围栏;服务端版本不匹配时整体拒绝且不产生任何写。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商;不得转为 Number |
|
||||
| `contractId` | Path | String | 是 | 正整数 ID 字符串 | 目标合同;不得转为 Number |
|
||||
| *(Create 全部字段)* | Body | String | 同新增 | 同新增 | 整份替换所需全字段 |
|
||||
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 目标合同当前并发版本,来自详情或上次写响应 |
|
||||
|
||||
#### 出参 `Result<SupplierContractRespVO>`
|
||||
|
||||
与新增相同,`updateTime` 返回新版本(严格晚于旧版本),请用该值继续后续围栏。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `contractId` | String | 合同 ID(字符串) |
|
||||
| (其余字段) | - | 同新增出参,`updateTime` 为新并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
PUT /admin/supplier/items/2091381911266967553/contracts/2093378327078154241/update
|
||||
Authorization: Bearer <admin-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"contractName": "2026年度框架合同(变更)",
|
||||
"contractNo": "HT-2026-001",
|
||||
"contractType": "FRAME",
|
||||
"signDate": "2026-08-29",
|
||||
"startDate": "2026-10-01",
|
||||
"endDate": "2027-08-31",
|
||||
"amount": "1500.00",
|
||||
"pricingMode": "按团结算",
|
||||
"settleCycle": "MONTHLY",
|
||||
"status": "ACTIVE",
|
||||
"scanFileUrl": "https://files.example.com/contracts/1.pdf",
|
||||
"remark": "续约调整",
|
||||
"changeReason": "续约调整",
|
||||
"expectedUpdateTime": "2026-08-29 00:41:00"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"contractId": "2093378327078154241",
|
||||
"contractName": "2026年度框架合同(变更)",
|
||||
"contractNo": "HT-2026-001",
|
||||
"contractType": "FRAME",
|
||||
"signDate": "2026-08-29",
|
||||
"startDate": "2026-10-01",
|
||||
"endDate": "2027-08-31",
|
||||
"amount": "1500.00",
|
||||
"pricingMode": "按团结算",
|
||||
"settleCycle": "MONTHLY",
|
||||
"status": "ACTIVE",
|
||||
"scanFileUrl": "https://files.example.com/contracts/1.pdf",
|
||||
"remark": "续约调整",
|
||||
"updateTime": "2026-08-29 00:41:01"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
更新接口无空数据场景。请求体携带的整份字段(含空值)会覆盖原值:`contractNo`、`signDate`、`pricingMode`、`settleCycle`、`scanFileUrl`、`remark` 传 null 或空串会被清空;`amount` 传 null 会清除金额。清空能力只在更新接口生效,新增不触发。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
版本不匹配/已过期(前端收到后请重新拉详情取最新 `updateTime` 再重试):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395014,
|
||||
"message": "数据已被他人修改,请刷新后重试",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
合同不存在、已删除或不属于路径供应商(跨供应商同码隐藏):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395051,
|
||||
"message": "供应商合同不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
请求与当前内容完全一致(无实际变化,不推进版本):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "未检测到实际变化",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 同一供应商下同一合同的 update 与 del 共享锁键,串行化后版本围栏在事务内校验。
|
||||
- `payloadChanged` 用值比较(金额忽略小数位 scale),纯金额 `1200.5` 与 `1200.50` 等价,不会误判为变更。
|
||||
- 更新推进合同自身 `updateTime`(秒级严格递增),不推进供应商主体版本。
|
||||
- 5 秒幂等窗口按双 ID + 载荷摘要去重;锁键串行化同行写。
|
||||
- 审计记录完整前后快照差异(`contracts` 字段组)。
|
||||
|
||||
### 3. 独立删除供应商合同 `DELETE /admin/supplier/items/{supplierId}/contracts/{contractId}/del`
|
||||
|
||||
**VO**: `SupplierContractDeleteReqVO` / 无返回体
|
||||
|
||||
#### 使用场景
|
||||
|
||||
对已登记合同作废软删除。删除后 `deletedAt` 写入当前时间,查询与详情不再返回;与同一合同 update 共享锁键和版本围栏。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
|
||||
| `contractId` | Path | String | 是 | 正整数 ID 字符串 | 目标合同 |
|
||||
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 目标合同当前并发版本 |
|
||||
| `changeReason` | Body | String | 是 | 最长 500 字符 | 本次删除原因,写入审计 |
|
||||
|
||||
#### 出参 `Result<Void>`
|
||||
|
||||
成功时 `data` 为 null:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `code` | Integer | 恒为 `200` 表示成功 |
|
||||
| `data` | null | 删除接口无返回体 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
DELETE /admin/supplier/items/2091381911266967553/contracts/2093378327078154241/del
|
||||
Authorization: Bearer <admin-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"expectedUpdateTime": "2026-08-29 00:41:01",
|
||||
"changeReason": "线下合同作废"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
删除成功恒返回上述结构;无空数据分支。删除后合同立即从查询/详情消失,如需保留展示请走更新改状态而非删除。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
版本不匹配:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395014,
|
||||
"message": "数据已被他人修改,请刷新后重试",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
合同不存在或已删除:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395051,
|
||||
"message": "供应商合同不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 删除是软删除,不物理清理业务合同数据;删除与完整删除审计同事务提交或回滚。
|
||||
- 删除草稿供应商时若存在未删除合同,档案删除会被拒绝(提示先独立删合同,错误码见「六、边界行为」)。
|
||||
- 已删除合同的 `expectedUpdateTime` 不再有效,误传原版本返回合同不存在。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
| 场景 | 调用 / 结果 |
|
||||
|---|---|
|
||||
| ✅ 新增后立即保存返回值 | 保存 `updateTime` 用于后续 update/del 版本围栏 |
|
||||
| ✅ 更新时整份提交 | 传全字段;想清空的字段显式传 null 或空串 |
|
||||
| ✅ 金额按字符串 | `"amount": "1200.50"`,`contractId`/`supplierId` 恒为字符串 |
|
||||
| ✅ 并发被拒后重试 | 重新 GET 详情取最新 `updateTime`,再带新版本重试 |
|
||||
| ❌ 把 ID/金额转 Number | 可能丢精度;必须按字符串传输与比较 |
|
||||
| ❌ 只传变更字段 | update 是整份替换语义;缺字段会被清空 |
|
||||
| ❌ 依赖审批候选带合同 | 审批候选 v2 起不携带 `contracts`;合同一律走独立接口维护 |
|
||||
|
||||
### 关键提示(当前 TEST 构建)
|
||||
|
||||
- 已登记的合同通过详情接口随供应商返回的 `contracts` 数组回显,前端不必重复维护列表状态。
|
||||
- **B1 已修复并部署**:登记(add)插入后按合同 ID 回读数据库持久化实体,响应与 CREATE 审计使用同一真实秒级 `updateTime`;该值可直接用于下一次 update/del 版本围栏。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 写操作仅影响 `supplier_contract` 一行(新增 insert / 更新 update / 删除软删),以及 `supplier_change_log` 一条对应合同审计事实(`fieldName=contract`,前后完整快照差异)。
|
||||
- 三个写接口均不修改 `supplier_main`(供应商主体版本不变),不进审批流,不写缓存、MQ 或跨服务数据。
|
||||
- 更新与删除仅在版本围栏通过后产生写;版本不匹配时不产生任何数据库副作用。
|
||||
- 软删除使用统一 `deleted_at` 逻辑删除,物理行保留;查询与详情自动过滤。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录/登录失效:业务码 `401`;角色或平台权限不足:`403`,不查询不写入。
|
||||
- `supplierId`/`contractId` 非法(非数字、超长、<=0):`400`;供应商不存在或已软删除:`395001`。
|
||||
- 已归档供应商:`395031`(写入一律拒绝)。
|
||||
- 未知或非生命周期状态供应商:`395005`(状态机门禁先行失败)。
|
||||
- 版本不匹配:`395014`;请求无实际变化:`395057`。
|
||||
- 删除草稿供应商仍有未删除合同:`395058`「已有合同登记,请先独立删除合同」。
|
||||
- Controller 的 Bean Validation/绑定失败仍返回 `400`;Service/事务层防旁路校验使用 `395052`–`395056`,其中缺失合同并发版本文案为「预期更新时间不能为空」。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### `contractType`(合同类型)
|
||||
|
||||
**所属字段**: `SupplierContractCreateReqVO.contractType` / `SupplierContractRespVO.contractType` | **类型**: `String`
|
||||
|
||||
| 值 | 说明 |
|
||||
|---|---|
|
||||
| `FRAME` | 框架合同 |
|
||||
| `SINGLE_TRIP` | 单团单合同 |
|
||||
| `PURCHASE` | 采购合同 |
|
||||
|
||||
### `status`(合同状态)
|
||||
|
||||
**所属字段**: `SupplierContractCreateReqVO.status` / `SupplierContractRespVO.status` | **类型**: `String`
|
||||
|
||||
| 值 | 说明 |
|
||||
|---|---|
|
||||
| `DRAFT` | 草稿 |
|
||||
| `ACTIVE` | 生效中(可维护) |
|
||||
| `EXPIRED` | 已过期(可维护) |
|
||||
| `TERMINATED` | 已终止(仅历史回显旧值,写接口不允许设置) |
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅新增**:三个独立合同写接口;合同登记与审批、状态机、归档完全解耦。
|
||||
- **保持兼容**:供应商建档/更新/提交/审批/归档/暂停/拉黑/删除接口契约不变;详情回显 `contracts` 数组不变。
|
||||
- **零影响**:审批候选 v2 起不含 `contracts`(v1 候选中的 contracts 字段被兼容忽略);不阻断既有审批流。
|
||||
- **零影响**:收款账户、资质、资源关联、历史/审批流水等供应商子域。
|
||||
- **零影响**:Gateway 路由(沿用 `/admin/supplier/**`)、数据库结构(无迁移)、Redis、MQ、Feign 契约。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- 本地自动化:合同命令/事务/校验器定向测试(SupplierContractTransactionServiceTest、SupplierContractValidatorTest、SupplierContractServiceTest、SupplierAggregateWriterTest、SupplierErrorCodeContractTest)51 项零失败;每日审查修复后 `hl-resource-service` 全量 2181 项零失败、零错误(38 项条件跳过)。
|
||||
- 主 PR #6559、审查修复 PR #6591 与中文版本文案补充 PR #6606 均已合入 `dev-v3`;最终 Resource 合并提交为 `d7e455492648b085445691ccde4a13198d4cd6c6`。
|
||||
- TEST 部署:Deploy Panel 任务 `ca2fe4d7` 成功,预期/实际提交均为 `c579c87014f56452fea2fce5075b31ae6fd12a35`;双进程、Nacos 两实例、6 个任务期采样和两类日志门禁通过,零观测不可用采样。
|
||||
- 最终自动化:合同定向 73 项、供应商真实 MySQL Testcontainers 398 项、Resource 全量 2181 项均为 0 失败/0 错误;全量保留 38 项既有条件跳过。
|
||||
- Gateway 边界:#6544 基线写链路已有真实 TEST 实证;最终部署回读身份为 `SUPER_ADMIN`,但当前平台权限不含 `supplier:update`。用户明确冻结该权限依赖的写复测,本轮没有配置/绕过权限,也没有发合同业务写请求;权限与事务成功/拒绝路径以最终自动化证据补充,不宣称运行时缺失权限已通过。
|
||||
- 数据清理:本轮最终部署和冻结验收未创建供应商、合同、缓存、MQ 或审批合成数据;TEST 身份已登出。
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|---|---|---|---|
|
||||
| #6559 | #6544 | 独立合同登记三写接口 + 边界拆分 | ✅ 本次主功能 |
|
||||
| #6591 | #6604 | 合同模块每日审查修复 7 项(写后读回/错误码/VO 文档/金额比较) | ✅ 已部署 |
|
||||
| #6606 | #6604 | 合同并发版本校验中文文案与全局错误码审计修复 | ✅ 已部署 |
|
||||
| #6519 | #6474 | 供应商变更/审批分离(前置演进) | ✅ 不受影响 |
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue:[#6544](https://git.1814.love:8443/wx/HL/issues/6544)
|
||||
- 关联 PR:[#6559](https://git.1814.love:8443/wx/HL/pulls/6559) / [#6591](https://git.1814.love:8443/wx/HL/pulls/6591) / [#6606](https://git.1814.love:8443/wx/HL/pulls/6606)
|
||||
- 管理端接入:详情页「合同」区改为直接调 add/update/del 三接口;提交审批的候选不再携带 contracts。
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 管理端停止请求三个 contract 写路径,并清除详情页合同编辑入口。
|
||||
2. 从最新 `dev-v3` 建独立回退分支,按依赖逆序回退 `d7e455492648b085445691ccde4a13198d4cd6c6`、`9eb9c96c09975a3ccf4966a257cc77bdf9f743b9` 与 `be0fe8125197b759459c975d211319e10cad150c`,验证后经独立 PR 合入。
|
||||
3. 仅需回退写接口时可先保留回显逻辑,只移除 add/update/del 路由与前端入口。
|
||||
4. 使用 Deploy Panel 两阶段客户端仅滚动部署 `hl-resource-service`;本次无数据库、配置、Redis 或 MQ 恢复步骤。
|
||||
5. 撤回后经 Gateway 验证三个写路径不可用、详情 `contracts` 回显按选定回退范围正常,并确认零写入。
|
||||
6. 同步发布本 Changelog 的撤回说明;不得仅改文件名表达状态。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#6544](https://git.1814.love:8443/wx/HL/issues/6544)
|
||||
- **PR**: [#6559](https://git.1814.love:8443/wx/HL/pulls/6559) / [#6591](https://git.1814.love:8443/wx/HL/pulls/6591) / [#6606](https://git.1814.love:8443/wx/HL/pulls/6606)
|
||||
- **Merge commit**: [`be0fe8125`](https://git.1814.love:8443/wx/HL/commit/be0fe8125197b759459c975d211319e10cad150c) / [`9eb9c96c0`](https://git.1814.love:8443/wx/HL/commit/9eb9c96c09975a3ccf4966a257cc77bdf9f743b9) / [`d7e455492`](https://git.1814.love:8443/wx/HL/commit/d7e455492648b085445691ccde4a13198d4cd6c6)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @lc
|
||||
@@ -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: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "6678b97f"
|
||||
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
|
||||
- **当前状态**:已消费(`frontend_status: verified`)。SupplierEditModal 结算 Tab 放开为「新建 || (编辑 && DRAFT)」,草稿编辑拉账户列表回读初始账户(含 accountId,accountNo 完整值可回填),buildEditBody 仅 DRAFT 按快照比对携带 initialAccounts(未动省略/清空 `[]`/1 项整份替换保留 accountId,6 可选字段+proofFileUrls 省略即显式清空),非 DRAFT 不开放不携带。
|
||||
@@ -0,0 +1,208 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6684"
|
||||
title: "供应商草稿编辑变更原因可选且不写变更记录"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "646a1565"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-08-30"
|
||||
status_note: "PR #6686 已合并 dev-v3,合并提交 ae7226ac8e2a23f0ce79ddda1a3a892542b81996 已由 Deploy Panel 任务 da7ffb3a 精确部署 TEST。真实 Gateway 已验证 DRAFT 草稿不传 changeReason 可成功保存、详情回读状态与修改值正确、主体变更记录仍为 0,并已删除合成草稿。当前状态:待前端处理。"
|
||||
updated_at: "2026-08-30"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 🔧 供应商草稿编辑变更原因可选且不写变更记录
|
||||
|
||||
供应商当前状态为 `DRAFT` 时,编辑接口不再要求填写 `changeReason`,并且本次草稿编辑不会新增主体变更记录。其他状态继续要求非空变更原因并保留原审计行为。
|
||||
|
||||
管理端需要按详情返回的当前状态调整表单校验:草稿隐藏或取消“变更原因”必填,非草稿继续必填。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---:|---|---|---|---|---|
|
||||
| 1 | 编辑供应商 | PUT | `/admin/supplier/items/{supplierId}/update` | 修改请求校验与写入行为 | `DRAFT` 可省略 `changeReason` 且不新增主体变更记录;其他状态不变 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 编辑供应商 `PUT /admin/supplier/items/{supplierId}/update`
|
||||
|
||||
**VO**: `SupplierUpdateReqVO / SupplierWriteRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
在供应商编辑页继续完善尚未提交审批的草稿资料。调用方先读取供应商详情取得当前 `status` 和 `updateTime`,再按当前状态决定是否发送变更原因。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
|
||||
| `changeReason` | Body | String | 条件必填 | 最长 500 字符 | 服务端当前状态为 `DRAFT` 时可省略或为空;其他状态去空白后必须非空 |
|
||||
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 最近一次详情或写响应返回的并发版本 |
|
||||
| `fullName`、`shortName` 等资料字段 | Body | 对应类型 | 否 | 沿用既有字段规则 | 仅发送本次需要修改的字段;示例使用 `remark` |
|
||||
|
||||
请求体不接收客户端自报的供应商状态;是否为草稿由服务端锁定并读取当前供应商后判定。
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `code` | Number | 业务码;成功为 `200` |
|
||||
| `success` | Boolean | 业务是否成功 |
|
||||
| `data.supplierId` | String | 供应商 ID |
|
||||
| `data.supplierNo` | String | 供应商业务编号 |
|
||||
| `data.status` | String | 保存后的当前状态;本场景为 `DRAFT` |
|
||||
| `data.onboardingStage` | String | 草稿为 `PROFILE_DRAFT` |
|
||||
| `data.initialAccounts` | Array | 当前初始结算账户摘要,未登记时为空数组 |
|
||||
| `data.updateTime` | String | 保存后的新并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
`DRAFT` 草稿请求体完全不发送 `changeReason`:
|
||||
|
||||
```json
|
||||
{
|
||||
"remark": "补充草稿内部备注",
|
||||
"expectedUpdateTime": "2026-08-30 08:15:29"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2093855088501035010",
|
||||
"supplierNo": "SUP2093855088501035010",
|
||||
"status": "DRAFT",
|
||||
"onboardingStage": "PROFILE_DRAFT",
|
||||
"initialAccounts": [],
|
||||
"updateTime": "2026-08-30 08:15:45"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- `DRAFT` 时省略 `changeReason`、传 `null` 或只传空白,不会因变更原因被拒绝。
|
||||
- 省略其他可选资料字段仍表示“不修改该字段”,不自动清空既有值。
|
||||
- 依赖服务异常继续按既有失败关闭语义返回,不把未完成写入伪装成成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
非 `DRAFT` 供应商省略或传空白 `changeReason`,继续返回原业务错误且零写入:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "变更原因不能为空",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
并发版本过期继续返回既有错误:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395014,
|
||||
"message": "数据已被他人修改,请刷新后重试",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 接口继续要求有效管理端身份、既有可信角色及 `supplier:update` 服务端权限。
|
||||
- 草稿判定使用服务端锁定后读取到的持久状态,不能通过请求体伪造 `DRAFT` 绕过非草稿审计。
|
||||
- `DRAFT` 成功编辑只更新草稿资料与并发版本,不新增主体变更记录。
|
||||
- 非 `DRAFT` 的原因必填、主体变更记录、事务、聚合锁、幂等和乐观并发规则均不变。
|
||||
- 参数、权限、状态或并发校验失败时不产生部分资料写入或变更记录。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
1. 进入编辑页先调用 `GET /admin/supplier/items/{supplierId}/basic-info/view`,使用返回的 `status` 和 `updateTime`。
|
||||
2. `status=DRAFT` 时可以不渲染变更原因输入框,或取消其必填校验;保存请求可完全省略 `changeReason`。
|
||||
3. `status` 不是 `DRAFT` 时继续收集非空 `changeReason`,不要把草稿规则扩展到其他状态。
|
||||
4. 每次成功保存后使用响应中的新 `updateTime` 作为下一次编辑的 `expectedUpdateTime`。
|
||||
|
||||
| 场景 | payload | 结果 |
|
||||
|---|---|---|
|
||||
| `DRAFT` 省略原因 | `{ "remark": "补充资料", "expectedUpdateTime": "2026-08-30 08:15:29" }` | 成功,不新增主体变更记录 |
|
||||
| `DRAFT` 显式空原因 | `{ "changeReason": "", "expectedUpdateTime": "2026-08-30 08:15:29" }` | 成功,不新增主体变更记录 |
|
||||
| 非 `DRAFT` 省略原因 | `{ "remark": "调整资料", "expectedUpdateTime": "2026-08-30 08:15:29" }` | `400`,变更原因不能为空 |
|
||||
| 非 `DRAFT` 提供原因 | `{ "changeReason": "更新登记资料", "expectedUpdateTime": "2026-08-30 08:15:29" }` | 按既有编辑与审计规则处理 |
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
| 服务端当前状态 | 资料保存 | 主体变更记录 |
|
||||
|---|---|---|
|
||||
| `DRAFT` | 按既有增量编辑和并发规则保存 | 不新增本次编辑记录 |
|
||||
| 非 `DRAFT` 且原因有效 | 按既有编辑规则保存 | 继续新增完整变更记录并保留原因 |
|
||||
| 非 `DRAFT` 且原因为空 | 不保存 | 不新增记录 |
|
||||
|
||||
本次不新增数据库 migration,不改历史记录,不执行跨服务写入,也不引入 Redis、MQ 或配置写行为。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录请求继续由 Gateway 拒绝;本次不改变认证级别。
|
||||
- 无供应商写权限、供应商不存在、状态不允许或并发版本过期时继续按既有错误语义失败。
|
||||
- `changeReason` 长度上限仍为 500 字符;非草稿只传空白等同未填写。
|
||||
- 草稿成功编辑后,详情回读应保持 `status=DRAFT` 并返回本次修改值与新版本。
|
||||
- 草稿的主体变更记录分页在本次保存前后数量保持不变。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 项目 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| `DRAFT` 的 `changeReason` | 请求模型无条件必填,省略即返回“变更原因不能为空” | 可省略、为 `null` 或空白 |
|
||||
| `DRAFT` 编辑审计 | 每次成功编辑都会新增主体变更记录 | 不新增主体变更记录 |
|
||||
| 非 `DRAFT` 的 `changeReason` | 必填 | 继续必填 |
|
||||
| 非 `DRAFT` 编辑审计 | 成功后记录完整变化与原因 | 保持不变 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**:否;原来已发送 `changeReason` 的草稿请求继续可用,非草稿契约不变。
|
||||
- **前端是否必须同步上线**:是;管理端当前草稿编辑表单仍把“变更原因”显示为必填,需要按详情 `status` 取消草稿必填。
|
||||
- **前端 workaround 清理点**:删除 `DRAFT` 草稿保存前对 `changeReason` 的必填拦截;不得删除非草稿校验。
|
||||
- **响应与错误码影响**:成功响应字段不变;只移除草稿缺少原因时的 `400`,不新增错误码。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 不修改供应商创建、注册提交、草稿删除、暂停、拉黑、归档、合同、账户或资源关系接口。
|
||||
- 不修改非草稿资料编辑的变更原因与主体变更记录语义。
|
||||
- 不修改 Gateway 路由、认证策略、角色或权限点。
|
||||
- 不新增数据库结构、历史数据回填、Redis、MQ、Nacos 或配置变更。
|
||||
- 本工单仅交付后端和接口 Changelog,不修改任何前端源码或资源。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- 合并提交 `ae7226ac8e2a23f0ce79ddda1a3a892542b81996` 已由 Deploy Panel 任务 `da7ffb3a` 精确部署到 TEST,Resource 双实例与 Nacos 注册均健康。
|
||||
- 通过真实 TEST Gateway 创建合成 `DRAFT` 草稿,并用完全不含 `changeReason` 的请求修改 `remark`;返回 `code=200`、`success=true`、`status=DRAFT` 和新的 `updateTime`。
|
||||
- 保存后再次查询详情,`remark` 为本次修改值且状态仍为 `DRAFT`。
|
||||
- 保存前后查询该供应商的 `UPDATE` 主体变更记录均返回 `total=0`、`records=[]`。
|
||||
- 验收后通过草稿删除接口清理合成数据;详情返回“供应商不存在”,按供应商编号查询列表为空。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue [#6684](https://git.1814.love:8443/wx/HL/issues/6684)
|
||||
- PR [#6686](https://git.1814.love:8443/wx/HL/pulls/6686)
|
||||
- 合并提交 [ae7226ac8e2a23f0ce79ddda1a3a892542b81996](https://git.1814.love:8443/wx/HL/commit/ae7226ac8e2a23f0ce79ddda1a3a892542b81996)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**:@lc
|
||||
- **当前状态**:已消费(`frontend_status: verified`)。SupplierEditModal 变更原因必填改仅非 DRAFT(mainLocked)挂载,DRAFT 取消必填、留空省略 changeReason 不写变更记录;buildEditBody 按「非 DRAFT 恒携带/DRAFT 填了才携带」;「至少一个实际变化字段」判定剔除元字段后看业务字段。非 DRAFT 继续必填拦截。
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6710"
|
||||
title: "供应商联系人快照版本字段兼容"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "95e1f917"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-08-30"
|
||||
status_note: "PR #6712 已合并 dev-v3,合并提交 7f3f944bf361b7ae41424c43b29262fb634fe791 已精确部署 TEST。后端后改判「当前前端已回传 updateTime 无需修改代码」,但前端实证:修复前 cleanContactRow 对已有联系人仅发 contactId 不带任何版本,必触发 400「联系人快照项不合法」;本次前端补回传条目级版本(commit 95e1f917),修复后方可正常保存。前端按实际修复记 verified。"
|
||||
updated_at: "2026-08-30"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商联系人快照版本字段兼容
|
||||
|
||||
后端现已兼容详情联系人字段 `contacts[].updateTime`。当前前端已经回传该字段,无需修改代码;本记录仅保留历史审计,不构成前端任务。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---:|---|---|---|---|---|
|
||||
| 1 | 编辑供应商 | PUT | `/admin/supplier/items/{supplierId}/update` | 请求兼容 | 已有联系人可用 `updateTime` 作为条目级并发版本 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 编辑供应商 `PUT /admin/supplier/items/{supplierId}/update`
|
||||
|
||||
**VO**: `SupplierUpdateReqVO / SupplierContactMergeReqVO / SupplierWriteRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
保留已有联系人并在一次编辑中新增多个联系人。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 主体并发版本 |
|
||||
| `contacts` | Body | Array | 否 | 最多 100 项 | 联系人完整快照 |
|
||||
| `contacts[].contactId` | Body | String | 已有项是 | 正整数 | 为空表示新增 |
|
||||
| `contacts[].expectedUpdateTime` / `updateTime` | Body | String | 已有项二选一 | 时间格式同上 | 条目级并发版本 |
|
||||
|
||||
姓名和联系电话允许与已有联系人或同批新增联系人重复。新增项仍按既有规则提供姓名、电话和角色,不传 ID 与版本。
|
||||
|
||||
#### 出参 `Result<SupplierWriteRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.updateTime` | String | 保存后的主体并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"expectedUpdateTime":"2026-08-30 10:04:18","contacts":[{"contactId":"2093000000000000001","contactName":"张三","contactPhone":"18501941408","contactRole":"contentBus","updateTime":"2026-08-30 10:04:19"},{"contactName":"张三","contactPhone":"18501941408","contactRole":"contentMoney"}]}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","success":true,"data":{"updateTime":"2026-08-30 10:04:29"}}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 省略 `contacts` 或传 `null`:联系人不变。
|
||||
- 依赖异常:沿用既有失败关闭语义,不产生部分写入。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
版本过期时:
|
||||
|
||||
```json
|
||||
{"code":395014,"message":"数据已被他人修改,请刷新后重试","success":false,"data":null}
|
||||
```
|
||||
|
||||
已有联系人未携带任一版本字段时,仍返回 `400`、`联系人快照项不合法`。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 已有联系人仍须提供 `contactId` 和有效版本;本次没有取消并发校验。
|
||||
- 姓名或联系电话重复本身不是错误条件。
|
||||
- 权限、状态、默认联系人及角色字典规则均不变。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
1. 主体详情 `updateTime` 放入请求顶层 `expectedUpdateTime`。
|
||||
2. 已有联系人的 `updateTime` 可原样回传;新增联系人不传 ID 和版本。
|
||||
3. 保存后重新读取详情,使用最新版本继续编辑。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 不新增数据库结构或姓名、电话唯一约束。
|
||||
- 校验失败时联系人和主体均不写入。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录、无权限、供应商不存在或版本过期时沿用既有错误。
|
||||
- `contacts=[]` 沿用既有清空语义。
|
||||
- 业务失败可能仍为 HTTP 200,调用方同时判断 `code` 和 `success`。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 项目 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 联系人版本字段 | 仅识别 `expectedUpdateTime` | 同时识别 `expectedUpdateTime`、`updateTime` |
|
||||
| 姓名、电话重复 | 允许 | 继续允许 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**:否。
|
||||
- **前端是否必须同步上线**:否,当前前端已回传 `updateTime`。
|
||||
- **前端 workaround 清理点**:无。
|
||||
- **响应与错误码影响**:无。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 不修改其他供应商接口、权限或状态机。
|
||||
- 不修改数据库结构、配置、Redis 或 MQ。
|
||||
- 不修改任何前端代码。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- 详情 `updateTime` 原样回传后,可保留已有联系人并新增多个联系人。
|
||||
- 多个联系人姓名、电话重复时保存成功。
|
||||
- 过期版本返回 `395014` 且零写入;验收草稿已清理。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- [Issue #6710](https://git.1814.love:8443/wx/HL/issues/6710)
|
||||
- [PR #6712](https://git.1814.love:8443/wx/HL/pulls/6712)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **后端负责人**:@lc
|
||||
- **当前状态**:已消费(`frontend_status: verified`)。SupplierEditModal fillForm 回填详情联系人条目级 updateTime、createContactRow 新增行置 null;cleanContactRow 已有项(contactId 存在)加发 expectedUpdateTime=row.updateTime 原样回传,新增行仍不发版本;快照比对两侧同经 cleanContactRow,未改动不误携带。spec +3 例累计 71 例绿,commit 95e1f917 已推 v2.1。注:后端后改判「无需前端处理」系基于修复后前端,修复前旧前端仅发 contactId 必 400,本修复为必要。
|
||||
@@ -0,0 +1,805 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6717"
|
||||
title: "车队关联供应商并展示供应商全名"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "4faf23bb"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-08-30"
|
||||
status_note: "PR #6753 已合并 dev-v3(merge commit 447b92f5);Deploy Panel 任务 2fe21f84(hl-fleet-service,2026-08-30 17:20)与 f29a293f(hl-resource-service,2026-08-30 17:22)均已部署测试服成功。真实 TEST 身份已验证 5 条负向链路全绿 + 列表字段结构正确,测试数据已清理。车队列表操作列的「供应商」按钮需下线,改为在车队新增/编辑弹窗选择供应商。前端已消费(frontend_status: verified):车队列表下线操作列「供应商」入口并新增供应商列透传 supplierName 快照;新增/编辑弹窗内嵌供应商 NSelect(远程搜索 FLEET+ACTIVE、可清空、编辑回填并补快照选项防显原始 ID),提交体携带 supplierId 字符串;601108/601109/601110/601111 按 bizCode 精确提示,新建不选供应商落 DISABLED 并提示。前端 commit 4faf23bb 已推 v2.1。"
|
||||
updated_at: "2026-08-30"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 车队管理:车队关联供应商并展示供应商全名
|
||||
|
||||
> **存放目录**:
|
||||
> - 二期(v3,`order-v3` 标签的工单)→ `changelogs-v2/2026-08/`
|
||||
>
|
||||
> **服务**: hl-fleet-service(端口 8094)/ hl-resource-service(内部依赖)
|
||||
> **PR**: #6753
|
||||
> **Issue**: #6717
|
||||
> **日期**: 2026-08-30
|
||||
> **影响范围**: 管理后台「车辆管理 - 车队管理」列表/新增/编辑/启用
|
||||
|
||||
车队管理新增供应商归属字段(可空),供应商只能随车队新增/编辑一起提交,不再提供单独配置入口。
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
- 车队列表操作列的「供应商」按钮(`views/fleet/teams/index.vue` 弹窗 `SupplierResourceRelModal`)**需要下线**;供应商关联改为在车队新增/编辑弹窗里选择。
|
||||
- 「不选供应商不能启动,只能停用」:未关联供应商的车队调用启用接口会被 601108 拦截;新建时不选供应商则车队落停用态。
|
||||
- 已有订单(名下车辆存在非取消派单)的车队,**不允许更换或清除供应商**;允许首次绑定。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
车队此前没有供应商归属字段;供应商域(`supplier_main`,类型字典 `supplier_type`)已支持车队类型供应商(FLEET),但车队主数据与供应商档案之间未建立关联。工单 #6717 要求车队管理列表展示供应商全名,并在车队新增/编辑时明确供应商归属。
|
||||
|
||||
**最终口径(2026-08-30 用户确认,覆盖工单原文「必选」语义)**:
|
||||
|
||||
1. 供应商字段**不必填**(可选);不选 → 不能启用,只能停用。
|
||||
2. 车队已有订单时,不允许**更换**或**清除**供应商(含已有订单的历史车队豁免存量,不动存量数据)。
|
||||
3. 去掉单独配置供应商接口入口:前端车队列表的「供应商」按钮下线;供应商关联只随车队新增/编辑一起提交。
|
||||
4. 供应商全名以**写时快照**存于 `fleet_team.supplier_name`;列表/详情零 Feign 读,供应商改名后陈旧、重新编辑可刷新。
|
||||
5. 供应商资格校验走跨服务 Feign(fleet → resource `GET /internal/supplier/{supplierId}/fleet-eligibility`):主体存在 + 状态 ACTIVE + 类型关联含 FLEET。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 分页查询车队 | GET | `/admin/fleet/teams/page` | 响应新增字段 | `records[]` 增加 `supplierId`(String/null) + `supplierName`(String/null) |
|
||||
| 2 | 查询车队详情 | GET | `/admin/fleet/teams/{fleetTeamId}` | 响应新增字段 | 同 #1 |
|
||||
| 3 | 新增车队 | POST | `/admin/fleet/teams` | 请求体新增可选字段 + 响应新增字段 | 请求体加 `supplierId`(Long 字符串/可空);响应同 #2 |
|
||||
| 4 | 编辑车队 | PUT | `/admin/fleet/teams/{fleetTeamId}` | 请求体新增可选字段 + 响应新增字段 + 失败语义 | 请求体加 `supplierId`;更换/清除时已有订单被 601111 拦截;清除且当前 ACTIVE 会强制落 DISABLED |
|
||||
| 5 | 启用车队 | POST | `/admin/fleet/teams/{fleetTeamId}/enable` | 失败语义新增 | `supplierId==null` 时抛 601108「车队未关联供应商,不能启用」 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 分页查询车队 `GET /admin/fleet/teams/page`
|
||||
|
||||
**VO**: `FleetTeamPageReqVO` / `Result<PageResult<FleetTeamRespVO>>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台「车辆管理 - 车队管理」列表页加载时调用。供应商列新增展示。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `page` | Query | Integer | 否 | ≥1,默认 1 | 页码 |
|
||||
| `pageSize` | Query | Integer | 否 | 1..100,默认 10 | 每页大小 |
|
||||
| `keyword` | Query | String | 否 | ≤64 | 车队名称/负责人姓名模糊搜索 |
|
||||
| `teamType` | Query | String | 否 | `SELF_OPERATED`/`COOPERATIVE` | 车队类型筛选 |
|
||||
| `status` | Query | String | 否 | `ACTIVE`/`DISABLED` | 状态筛选 |
|
||||
| `settleType` | Query | String | 否 | `cash`/`sign`/`company` | 付款方式筛选 |
|
||||
|
||||
#### 出参 `Result<PageResult<FleetTeamRespVO>>`
|
||||
|
||||
`records[]` 每一项 `FleetTeamRespVO` 字段(与改前相比只多两列,其余字段语义不变):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `fleetTeamId` | String | 车队 ID(雪花序列化为字符串,禁止转 Number) |
|
||||
| `teamCode` | String | 内部稳定编码(`ft_xxx`) |
|
||||
| `teamName` | String | 车队名称 |
|
||||
| `teamType` | String | `SELF_OPERATED`/`COOPERATIVE` |
|
||||
| `leaderName` | String | 负责人姓名 |
|
||||
| `leaderPhone` | String | 负责人电话(**分页脱敏**:`138****8000`) |
|
||||
| `settleType` | String | `cash`/`sign`/`company` |
|
||||
| `status` | String | `ACTIVE`/`DISABLED` |
|
||||
| `sortOrder` | Integer | 排序值 |
|
||||
| `remark` | String | 备注 |
|
||||
| `vehicleCount` | Integer | 名下车辆总数(含停用) |
|
||||
| `activeVehicleCount` | Integer | 名下在役车辆数 |
|
||||
| **`supplierId`** | **String/null** | **关联供应商 ID(雪花字符串;未关联为 null)** |
|
||||
| **`supplierName`** | **String/null** | **关联供应商全称快照(写时同步,未关联为 null)** |
|
||||
| `createTime` | String | 创建时间 `yyyy-MM-dd HH:mm:ss` |
|
||||
| `updateTime` | String | 最后更新时间 `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/fleet/teams/page?page=1&pageSize=10&status=ACTIVE
|
||||
Authorization: Bearer <admin-token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"total": 2,
|
||||
"page": 1,
|
||||
"pageSize": 10,
|
||||
"records": [
|
||||
{
|
||||
"fleetTeamId": "2102345678901234567",
|
||||
"teamCode": "ft_2x9k3m",
|
||||
"teamName": "合作车队A",
|
||||
"teamType": "COOPERATIVE",
|
||||
"leaderName": "测试负责人甲",
|
||||
"leaderPhone": "138****0001",
|
||||
"settleType": "cash",
|
||||
"status": "ACTIVE",
|
||||
"sortOrder": 10,
|
||||
"remark": "由 fleet_attribution/历史业务数据迁移",
|
||||
"vehicleCount": 12,
|
||||
"activeVehicleCount": 9,
|
||||
"supplierId": "2091381911266967553",
|
||||
"supplierName": "内蒙古呼籁旅游服务有限公司",
|
||||
"createTime": "2026-06-01 10:00:00",
|
||||
"updateTime": "2026-08-30 15:30:00"
|
||||
},
|
||||
{
|
||||
"fleetTeamId": "2102345678901234568",
|
||||
"teamCode": "ft_2x9k3n",
|
||||
"teamName": "自有车队",
|
||||
"teamType": "SELF_OPERATED",
|
||||
"leaderName": "自有负责人",
|
||||
"leaderPhone": "139****0002",
|
||||
"settleType": "company",
|
||||
"status": "DISABLED",
|
||||
"sortOrder": 20,
|
||||
"remark": "",
|
||||
"vehicleCount": 0,
|
||||
"activeVehicleCount": 0,
|
||||
"supplierId": null,
|
||||
"supplierName": null,
|
||||
"createTime": "2026-08-01 09:00:00",
|
||||
"updateTime": "2026-08-30 15:30:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无车队时 `records=[]` `total=0`,`code=200`。供应商字段未关联时为 `null`(不是空串),前端按未关联渲染。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
分页查询无业务错误分支。网关/框架错误:
|
||||
|
||||
```json
|
||||
{ "code": 401, "message": "未登录", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 只读查询;返回的 `supplierId`/`supplierName` 来自 `fleet_team.supplier_id/supplier_name` 列快照,不做实时跨服务取数。
|
||||
- 供应商改名后列表展示旧名(快照口径);如需最新名称,让用户重新编辑车队保存触发刷新。
|
||||
- 车队停用车队(`status=DISABLED`)也可被列表查到(不带状态筛选时默认返回全部状态)。
|
||||
|
||||
---
|
||||
|
||||
### 2. 查询车队详情 `GET /admin/fleet/teams/{fleetTeamId}`
|
||||
|
||||
**VO**: `Result<FleetTeamRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台编辑车队弹窗初始化时调用,回填 `supplierId`/`supplierName` 到供应商选择器。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `fleetTeamId` | Path | String(Long) | 是 | 正整数 ID 字符串 | 目标车队 ID(雪花,不得转 Number) |
|
||||
|
||||
#### 出参 `Result<FleetTeamRespVO>`
|
||||
|
||||
字段与 #1 `records[]` 项结构一致(含新增 `supplierId`/`supplierName`):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `fleetTeamId` | String | 车队 ID(雪花序列化为字符串,禁止转 Number) |
|
||||
| `teamCode` | String | 内部稳定编码(`ft_xxx`) |
|
||||
| `teamName` | String | 车队名称 |
|
||||
| `teamType` | String | `SELF_OPERATED`/`COOPERATIVE` |
|
||||
| `leaderName` | String | 负责人姓名 |
|
||||
| `leaderPhone` | String | 负责人电话(**详情返回原值**,编辑场景回填用) |
|
||||
| `settleType` | String | `cash`/`sign`/`company` |
|
||||
| `status` | String | `ACTIVE`/`DISABLED` |
|
||||
| `sortOrder` | Integer | 排序值 |
|
||||
| `remark` | String | 备注 |
|
||||
| `vehicleCount` | Integer | 名下车辆总数(含停用) |
|
||||
| `activeVehicleCount` | Integer | 名下在役车辆数 |
|
||||
| `supplierId` | String/null | 关联供应商 ID(雪花字符串;未关联为 null) |
|
||||
| `supplierName` | String/null | 关联供应商全称快照(写时同步) |
|
||||
| `createTime` | String | 创建时间 `yyyy-MM-dd HH:mm:ss` |
|
||||
| `updateTime` | String | 最后更新时间 `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/fleet/teams/2102345678901234567
|
||||
Authorization: Bearer <admin-token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"fleetTeamId": "2102345678901234567",
|
||||
"teamCode": "ft_2x9k3m",
|
||||
"teamName": "合作车队A",
|
||||
"teamType": "COOPERATIVE",
|
||||
"leaderName": "测试负责人甲",
|
||||
"leaderPhone": "13800000001",
|
||||
"settleType": "cash",
|
||||
"status": "ACTIVE",
|
||||
"sortOrder": 10,
|
||||
"remark": "由 fleet_attribution/历史业务数据迁移",
|
||||
"vehicleCount": 12,
|
||||
"activeVehicleCount": 9,
|
||||
"supplierId": "2091381911266967553",
|
||||
"supplierName": "内蒙古呼籁旅游服务有限公司",
|
||||
"createTime": "2026-06-01 10:00:00",
|
||||
"updateTime": "2026-08-30 15:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
车队不存在或已软删除(业务错误,见下方错误响应);无空数据分支。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
车队不存在或已软删除:
|
||||
|
||||
```json
|
||||
{ "code": 601100, "message": "车队不存在", "success": false, "data": null }
|
||||
```
|
||||
|
||||
未登录(网关拦截):
|
||||
|
||||
```json
|
||||
{ "code": 401, "message": "未登录", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 详情接口返回 `leaderPhone` 为原值(编辑场景需要回填),分页接口脱敏。
|
||||
- `supplierId` 必须按字符串处理,禁止转 JavaScript Number。
|
||||
|
||||
---
|
||||
|
||||
### 3. 新增车队 `POST /admin/fleet/teams`
|
||||
|
||||
**VO**: `FleetTeamSaveReqVO` / `Result<FleetTeamRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台「车辆管理 - 车队管理」点击「新增车队」,在表单里可选填供应商。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `teamName` | Body | String | 是 | ≤64,唯一 | 车队名称 |
|
||||
| `teamType` | Body | String | 是 | `SELF_OPERATED`/`COOPERATIVE` | 车队类型 |
|
||||
| `leaderName` | Body | String | 是 | ≤64 | 负责人姓名 |
|
||||
| `leaderPhone` | Body | String | 是 | 电话格式正则 | 负责人电话 |
|
||||
| `settleType` | Body | String | 是 | `cash`/`sign`/`company` | 付款方式 |
|
||||
| `sortOrder` | Body | Integer | 是 | ≥0 | 排序 |
|
||||
| `remark` | Body | String | 否 | ≤256 | 备注 |
|
||||
| **`supplierId`** | Body | **String(Long)/null** | **否** | **`@Positive`** | **关联供应商 ID(雪花字符串;不填=不选供应商,新建车队落 DISABLED)** |
|
||||
|
||||
#### 出参 `Result<FleetTeamRespVO>`
|
||||
|
||||
响应结构同 #2;`supplierId`/`supplierName` 按请求回填(选了供应商且校验通过则写入快照,否则为 null)。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `fleetTeamId` | String | 新车队 ID(雪花序列化为字符串) |
|
||||
| `supplierId` | String/null | 按请求回填(选了且校验通过) |
|
||||
| `supplierName` | String/null | 写时同步的供应商全称快照;未选为 null |
|
||||
| `status` | String | 选供应商=`ACTIVE`;不选=`DISABLED` |
|
||||
| `createTime` | String | 创建时间 `yyyy-MM-dd HH:mm:ss` |
|
||||
| `updateTime` | String | 创建时间(与 createTime 相同) |
|
||||
| (其余字段) | - | 与详情 #2 同构 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /admin/fleet/teams
|
||||
Authorization: Bearer <admin-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"teamName": "新合作车队B",
|
||||
"teamType": "COOPERATIVE",
|
||||
"leaderName": "王队长",
|
||||
"leaderPhone": "13800138000",
|
||||
"settleType": "sign",
|
||||
"sortOrder": 20,
|
||||
"remark": "新签约车队",
|
||||
"supplierId": "2091381911266967553"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"fleetTeamId": "2103456789012345678",
|
||||
"teamCode": "ft_3a1b2c",
|
||||
"teamName": "新合作车队B",
|
||||
"teamType": "COOPERATIVE",
|
||||
"leaderName": "王队长",
|
||||
"leaderPhone": "13800138000",
|
||||
"settleType": "sign",
|
||||
"status": "ACTIVE",
|
||||
"sortOrder": 20,
|
||||
"remark": "新签约车队",
|
||||
"vehicleCount": 0,
|
||||
"activeVehicleCount": 0,
|
||||
"supplierId": "2091381911266967553",
|
||||
"supplierName": "内蒙古呼籁旅游服务有限公司",
|
||||
"createTime": "2026-08-30 16:00:00",
|
||||
"updateTime": "2026-08-30 16:00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
未选供应商创建(落 DISABLED,供应商字段为 null):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"fleetTeamId": "2103456789012345679",
|
||||
"teamCode": "ft_3a1b2d",
|
||||
"teamName": "临时车队",
|
||||
"teamType": "COOPERATIVE",
|
||||
"leaderName": "临时负责人",
|
||||
"leaderPhone": "13900139000",
|
||||
"settleType": "cash",
|
||||
"status": "DISABLED",
|
||||
"sortOrder": 30,
|
||||
"remark": "",
|
||||
"vehicleCount": 0,
|
||||
"activeVehicleCount": 0,
|
||||
"supplierId": null,
|
||||
"supplierName": null,
|
||||
"createTime": "2026-08-30 16:00:00",
|
||||
"updateTime": "2026-08-30 16:00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
供应商不存在/未生效/不含车队类型(不写库):
|
||||
|
||||
```json
|
||||
{ "code": 601109, "message": "供应商不存在、未生效或不包含车队类型", "success": false, "data": null }
|
||||
```
|
||||
|
||||
供应商校验依赖故障(Feign 不可用,不写库):
|
||||
|
||||
```json
|
||||
{ "code": 601110, "message": "暂时无法校验供应商,请稍后重试", "success": false, "data": null }
|
||||
```
|
||||
|
||||
车队名称重复(`uk_fleet_team_name` 唯一索引兜底):
|
||||
|
||||
```json
|
||||
{ "code": 601101, "message": "车队名称已存在", "success": false, "data": null }
|
||||
```
|
||||
|
||||
Bean Validation 校验失败:
|
||||
|
||||
```json
|
||||
{ "code": 400, "message": "供应商ID必须为正数", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 幂等:同一 teamName + 请求摘要 10 秒窗口内重复提交只生效一次(@Idempotent key=`fleet:team:create:{teamName}`)。
|
||||
- 分布式锁:同 teamName 创建串行化(@Lock4j)。
|
||||
- 选供应商时先 Feign 校验(事务外),通过后写库(事务内),Feign 失败/不合格一律不写库(失败关闭)。
|
||||
- 不写供应商时新建车队 status=DISABLED,需后续编辑绑定供应商后才能启用。
|
||||
|
||||
---
|
||||
|
||||
### 4. 编辑车队 `PUT /admin/fleet/teams/{fleetTeamId}`
|
||||
|
||||
**VO**: `FleetTeamSaveReqVO` / `Result<FleetTeamRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台编辑车队弹窗提交。可修改基础字段 + 供应商;**不允许修改 status**(启停用走独立接口)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `fleetTeamId` | Path | String(Long) | 是 | 正整数 ID 字符串 | 目标车队 ID(不得转 Number) |
|
||||
| (其余字段) | Body | String | 同新增 | 同新增 | 全字段提交(SaveReqVO 整体语义) |
|
||||
| `supplierId` | Body | String(Long)/null | 否 | `@Positive` | 供应商 ID;更换/清除受订单围栏 |
|
||||
|
||||
#### 出参 `Result<FleetTeamRespVO>`
|
||||
|
||||
同 #2;`supplierId`/`supplierName` 反映最新写入值。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `fleetTeamId` | String | 车队 ID(路径回显) |
|
||||
| `supplierId` | String/null | 最新写入值(更换/清除后刷新) |
|
||||
| `supplierName` | String/null | 最新写入快照(更换时取新供应商 full_name;清除时为 null) |
|
||||
| `status` | String | 清除供应商且当前 ACTIVE 时强制落 `DISABLED`;其余场景不变 |
|
||||
| `updateTime` | String | 本次写入时间(秒级严格递增) |
|
||||
| (其余字段) | - | 与详情 #2 同构 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
PUT /admin/fleet/teams/2102345678901234567
|
||||
Authorization: Bearer <admin-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"teamName": "合作车队A",
|
||||
"teamType": "COOPERATIVE",
|
||||
"leaderName": "测试负责人甲",
|
||||
"leaderPhone": "13800000001",
|
||||
"settleType": "cash",
|
||||
"sortOrder": 10,
|
||||
"remark": "",
|
||||
"supplierId": "2091381911266967553"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
成功(更换供应商且无订单):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"fleetTeamId": "2102345678901234567",
|
||||
"teamCode": "ft_2x9k3m",
|
||||
"teamName": "合作车队A",
|
||||
"teamType": "COOPERATIVE",
|
||||
"leaderName": "测试负责人甲",
|
||||
"leaderPhone": "13800000001",
|
||||
"settleType": "cash",
|
||||
"status": "ACTIVE",
|
||||
"sortOrder": 10,
|
||||
"remark": "",
|
||||
"vehicleCount": 12,
|
||||
"activeVehicleCount": 9,
|
||||
"supplierId": "2091381911266967553",
|
||||
"supplierName": "内蒙古呼籁旅游服务有限公司",
|
||||
"createTime": "2026-06-01 10:00:00",
|
||||
"updateTime": "2026-08-30 16:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
清除供应商(原值 → null)且车队无订单:快照清空,若当前 ACTIVE 强制落 DISABLED(保持不变量:ACTIVE ⇒ 已绑供应商)。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"fleetTeamId": "2102345678901234567",
|
||||
"teamCode": "ft_2x9k3m",
|
||||
"teamName": "合作车队A",
|
||||
"teamType": "COOPERATIVE",
|
||||
"status": "DISABLED",
|
||||
"supplierId": null,
|
||||
"supplierName": null,
|
||||
"updateTime": "2026-08-30 16:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
车队已有订单时更换/清除供应商(含从原供应商换到新供应商、从原供应商清为空):
|
||||
|
||||
```json
|
||||
{ "code": 601111, "message": "车队已关联订单,不能更换供应商", "success": false, "data": null }
|
||||
```
|
||||
|
||||
供应商校验失败(同新增 #3 的 601109/601110);车队不存在:
|
||||
|
||||
```json
|
||||
{ "code": 601100, "message": "车队不存在", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 幂等:同 fleetTeamId + 请求摘要(含 supplierId 参与摘要)10 秒窗口内重复提交只生效一次(@Idempotent key=`fleet:team:update:{fleetTeamId}:{sha256}`)。
|
||||
- 分布式锁:同 fleetTeamId 编辑串行化(@Lock4j)。
|
||||
- 更换供应商校验顺序:订单围栏(601111)→ Feign 资格校验(601109/601110)→ 写库。
|
||||
- 供应商不变时(含 null→null)不触发订单围栏,也不调 Feign。
|
||||
- 首次绑定(null → 新值)即使已有订单也允许。
|
||||
- `status` 不在 SaveReqVO,本接口不修改启停状态;清除供应商且当前 ACTIVE 时强制落 DISABLED 是唯一例外。
|
||||
|
||||
---
|
||||
|
||||
### 5. 启用车队 `POST /admin/fleet/teams/{fleetTeamId}/enable`
|
||||
|
||||
**VO**: `Result<Void>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台对已停用车队点击「启用」。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `fleetTeamId` | Path | String(Long) | 是 | 正整数 ID 字符串 | 目标车队 ID |
|
||||
|
||||
#### 出参 `Result<Void>`
|
||||
|
||||
成功时 `data=null`:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `code` | Integer | 恒为 `200` 表示成功 |
|
||||
| `data` | null | 启用接口无返回体 |
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
启用成功恒返回 `{"code":200,"success":true,"data":null}`;无空数据分支。已是启用态时幂等放行不重复写(返回同样结构)。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /admin/fleet/teams/2102345678901234567/enable
|
||||
Authorization: Bearer <admin-token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "success": true, "data": null }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
未关联供应商(新增语义):
|
||||
|
||||
```json
|
||||
{ "code": 601108, "message": "车队未关联供应商,不能启用", "success": false, "data": null }
|
||||
```
|
||||
|
||||
已是启用态(幂等放行,不重复写):
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "success": true, "data": null }
|
||||
```
|
||||
|
||||
车队不存在:`601100`。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 幂等:同 fleetTeamId 重复调用只生效一次(@Idempotent key=`fleet:team:enable:{fleetTeamId}`)。
|
||||
- 分布式锁:同 fleetTeamId 启停用串行化(@Lock4j key=`fleet:team:status:{fleetTeamId}`)。
|
||||
- 存量迁移车队(own/coopA/coopB)当前 ACTIVE 且无供应商:enable 若已被置 DISABLED 后会被 601108 拦截,需先编辑绑定供应商再启用。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
| 场景 | 调用 / 结果 |
|
||||
|---|---|
|
||||
| ✅ 新增时不选供应商 | `{ "supplierId": null }`(或不传)→ 车队落 DISABLED,待绑定后启用 |
|
||||
| ✅ 新增时选供应商 | `{ "supplierId": "2091381911266967553" }` → Feign 校验通过后落 ACTIVE |
|
||||
| ✅ 编辑时供应商不变 | 原 supplierId 原样传回(或省略由后端按原值处理?——必须原样传,SaveReqVO 整份语义) |
|
||||
| ✅ 首次绑定 | 原 supplierId=null,新 supplierId=有效 FLEET 供应商 → 允许(不受订单围栏) |
|
||||
| ✅ 供应商改名后刷新快照 | 重新编辑车队并保存(supplierId 原样),快照重新取最新 full_name |
|
||||
| ❌ 供应商 ID 转 Number | 雪花精度丢失;必须按字符串传输 |
|
||||
| ❌ 已有订单车队换供应商 | 601111,不写库 |
|
||||
| ❌ 已有订单车队清供应商 | 601111,不写库 |
|
||||
| ❌ 清除供应商后期望仍 ACTIVE | 无订单时快照清空+强制 DISABLED;前端收到 200 但 status=DISABLED 属于预期行为 |
|
||||
|
||||
### 关键提示(当前 TEST 构建)
|
||||
|
||||
- `supplierId`/`supplierName` 在所有响应中均按字符串序列化(`@JsonSerialize(ToStringSerializer)`);**禁止**前端用 `Number()`/`parseInt()`/一元 `+` 转换。
|
||||
- 供应商全名快照为**写时取数**:`supplier_main.full_name` 改名后列表展示旧值,重新编辑车队保存触发刷新。
|
||||
- 供应商选择器数据源:可复用 `GET /admin/supplier/items/list?typeCode=FLEET&status=ACTIVE`(mmg 自查前端是否已有该接口封装;无则后端再补)。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- **写操作只影响 `fleet_team` 一行**(新增 insert / 编辑 update / 启停用 update),不跨服务写 `supplier_resource_rel` 或 `supplier_main`。
|
||||
- 快照列:`supplier_id`(关联 ID)+ `supplier_name`(全称快照)同列写入;清除时同列置 NULL。
|
||||
- 幂等窗口内重复请求只写一次;锁键串行化同车队写。
|
||||
- 迁移:`V20260830_001__add_supplier_to_fleet_team.sql` 对 `fleet_team` 加 `supplier_id BIGINT NULL` + `supplier_name VARCHAR(500) NULL` + 索引 `idx_fleet_team_supplier(supplier_id)`;存量车队两列均为 NULL。
|
||||
- 不动 `supplier_resource_rel`(车队供应商不走资源关系表;关系表仅用于九大资源模块)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录/登录失效:业务码 `401`(网关拦截)。
|
||||
- 车队不存在:`601100`。
|
||||
- 车队名称重复:`601101`(含 DuplicateKeyException 翻译)。
|
||||
- 车队已停用仍选该车:`601102`(既有口径,本工单不改)。
|
||||
- 车队下有在役车辆时禁止停用:`601103`(既有口径)。
|
||||
- 车队已关联车辆时禁止改自有/合作类型:`601104`(既有口径)。
|
||||
- 车队名下仍有车辆/司机时禁止删除:`601107`(既有口径)。
|
||||
- 车队未关联供应商禁止启用:`601108`(新增)。
|
||||
- 供应商不存在/未生效/不含车队类型:`601109`(新增)。
|
||||
- 供应商校验依赖故障:`601110`(新增,失败关闭不写库)。
|
||||
- 车队已有订单禁止更换/清除供应商:`601111`(新增)。
|
||||
- Bean Validation 校验失败:`400`。
|
||||
- 跨服务 Feign 不可用:写接口一律失败关闭(601110);读接口(列表/详情)读快照列,不受影响。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### `status`(车队启停状态)
|
||||
|
||||
**所属字段**: `FleetTeamRespVO.status` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `ACTIVE` | 启用 | 可被车辆选择;**前提:已关联供应商** |
|
||||
| `DISABLED` | 停用 | 不可被车辆选择;新建未选供应商时默认落此态 |
|
||||
|
||||
### `teamType`(车队类型)
|
||||
|
||||
**所属字段**: `FleetTeamSaveReqVO.teamType` / `FleetTeamRespVO.teamType` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `SELF_OPERATED` | 自有 | 自有车队 |
|
||||
| `COOPERATIVE` | 合作 | 合作车队 |
|
||||
|
||||
### `settleType`(付款方式,字典 `resource_settle_type`)
|
||||
|
||||
**所属字段**: `FleetTeamSaveReqVO.settleType` / `FleetTeamRespVO.settleType` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `cash` | 现付 | - |
|
||||
| `sign` | 挂账签单 | - |
|
||||
| `company` | 公司月结 | - |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `FleetTeamSaveReqVO.supplierId` | 无 | 新增可选字段,@Positive,参与幂等摘要 |
|
||||
| `FleetTeamRespVO.supplierId` | 无 | 新增,String/null(ToStringSerializer) |
|
||||
| `FleetTeamRespVO.supplierName` | 无 | 新增,String/null(快照) |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 新建车队默认状态 | ACTIVE | 选供应商=ACTIVE;不选=DISABLED |
|
||||
| 启用校验 | 只查当前状态 | 增加 supplierId==null → 601108 |
|
||||
| 编辑车队供应商 | 无此字段 | 有订单禁换/禁清(601111);清除+ACTIVE→强制 DISABLED |
|
||||
| 供应商配置入口 | 前端有独立「供应商」按钮(调通用资源关系接口) | 下线;改为车队新增/编辑内嵌选择 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否(新增可选字段;响应只多两列,前端旧版忽略即兼容)
|
||||
- **前端是否必须同步上线**: 是(车队列表「供应商」按钮需下线,否则用户仍能从旧入口调通用接口——但通用接口对车队模块本工单起后端侧保留不拦,是前端入口下线)
|
||||
- **前端 workaround 清理点**: 车队列表的「供应商」操作入口(`views/fleet/teams/index.vue` 中 `supplierRelaShow`/`supplierRelaRow`/`openSupplierRelation` 相关代码)整体删除
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台「车辆管理 - 车队管理」列表/新增/编辑/启用 4 个端点 + 详情 1 个端点。
|
||||
- **零影响**:
|
||||
- 车辆档案(`fleet_vehicle`)/ 司机档案(`fleet_driver`)/ 派单(`fleet_assignment`)等车队下游域——它们继续经 `FleetTeamService.resolveForVehicle` 解析车队,供应商字段不影响车辆选车队。
|
||||
- 供应商域九大资源模块(景区/餐厅/备品/组合/游玩项目/酒店/服务/额外成本/服务人员/车辆)的独立供应商关系维护(`/admin/supplier/resource-relations/{module}/{id}/update` 等通用接口保留不动)。
|
||||
- 订单/对账/看板读路径(只读 fleet_team 既有字段,新增两列不影响)。
|
||||
- Gateway 路由(`/admin/fleet/**` 通配已覆盖;`/internal/**` 不走网关)。
|
||||
- Redis/MQ(无新增 key/消息)。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
真实 TEST 环境实测(2026-08-30,网关 `https://api.test.1814.love:9443`,admin token 走 `/admin/auth/login`):
|
||||
|
||||
```
|
||||
POST /admin/fleet/teams 无供应商新建 → 200, status=DISABLED, supplierId=null, supplierName=null ✓
|
||||
POST /admin/fleet/teams/{id}/enable 无供应商启用 → 601108「车队未关联供应商,不能启用」 ✓
|
||||
PUT /admin/fleet/teams/{id} 绑定 DRAFT 供应商 → 601109 ✓
|
||||
PUT /admin/fleet/teams/{id} 绑定不存在供应商 → 601109 ✓
|
||||
PUT /admin/fleet/teams/{id} 绑定非 FLEET 类型 ACTIVE 供应商(RESTAURANT) → 601109 ✓
|
||||
GET /admin/fleet/teams/page → records[] 含 supplierId/supplierName 键(未关联为 null) ✓
|
||||
```
|
||||
|
||||
- 部署:Deploy Panel 任务 `2fe21f84`(hl-fleet-service,2026-08-30 17:20 success)+ `f29a293f`(hl-resource-service,2026-08-30 17:22 success),预期/实际提交均为 `447b92f5143e9ca4381d590d4f868d518cf66e0a`(dev-v3 HEAD)。
|
||||
- 正向链路(绑定合格 FLEET ACTIVE 供应商 → ACTIVE + 快照写 supplierName):测试服当前无 ACTIVE 状态的 FLEET 类型供应商(供应商审批链要求必备资质,`supplier_type_qualification_rule` 规则表为空,无法造出合格供应商);该路径本地单测已覆盖(`FleetTeamServiceTest#create_withEligibleSupplier_activeAndSnapshot` 等 24 用例全绿),建议 mmg 联调时在真实数据上补验。
|
||||
- 有订单换供应商(601111)链路:测试服车队均无订单派单可安全构造验证数据,本地单测覆盖(`FleetTeamServiceTest#update_changeSupplierWithOrders_rejected` / `update_clearSupplierWithOrders_rejected`)。
|
||||
- 本地自动化:fleet 全量 3883 项 0 失败 0 错误(含 FleetRedLineArchTest 13 项门禁);resource 全量 0 失败 0 错误;spotless:check 绿。
|
||||
- 数据清理:测试车队(352385968453586944)与测试供应商(2093993745543299074)均已删除;未触碰同事真实订单;admin token 已登出。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| #6753 | #6717 | 车队关联供应商并展示供应商全名(本次) | ✅ 最新 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#6717](https://git.1814.love:8443/wx/HL/issues/6717)
|
||||
- 关联 PR: [wx/HL#6753](https://git.1814.love:8443/wx/HL/pulls/6753)
|
||||
- 任务设计文档: `docs/tasks/6717-fleet-team-supplier.md`(worktree `D:\work2\HL-v3-0830-fleetsup`)
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 管理端先恢复车队列表「供应商」操作入口(参照改前版本),保持线上可用。
|
||||
2. 从最新 `dev-v3` 建独立回退分支,回退 PR #6753 的合并 commit(`72c9018ab` 及其后续如有),验证后经独立 PR 合入。
|
||||
3. 仅需下线新供应商字段时可先保留两列(快照保留无副作用),只回退 Controller/Service 逻辑与 Feign 校验。
|
||||
4. 使用 Deploy Panel 滚动部署 `hl-fleet-service` 与 `hl-resource-service`;数据库列保留不删(`supplier_id`/`supplier_name` 允许 NULL,回退后不影响)。
|
||||
5. 撤回后经 Gateway 验证新增/编辑/启用接口按改前口径通过;车队列表的供应商列展示空白或下线列头。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#6717](https://git.1814.love:8443/wx/HL/issues/6717)
|
||||
- **PR**: [#6753](https://git.1814.love:8443/wx/HL/pulls/6753)
|
||||
- **Merge commit**: 待合并后回填
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
- **前端联动**: @mmg(下线车队列表「供应商」按钮 + 车队新增/编辑弹窗加供应商选择器)
|
||||
@@ -0,0 +1,288 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6739"
|
||||
title: "供应商三级地址保存与回显"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "5f35aafd"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-08-30"
|
||||
status_note: "PR #6744 已合并 dev-v3,合并提交 2d24b329 已部署 TEST。前端实证 #6350 拼接单字符串 address 且 002 不回显与新契约相反,属 required。已实现:三级联动与详细地址独立成字段(countyId+address),RegionCascader 新增 getRegionPath 受控回显,编辑表单回填入快照增量比对、详情页 countyId 反查省市区名。前端 commit 5f35aafd 已推 v2.1。当前状态:已消费(frontend_status: verified)。"
|
||||
updated_at: "2026-08-30"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商三级地址保存与回显
|
||||
|
||||
`countyId` 保存三级联动的最下级区县 ID,`address` 只保存详细地址,例如“某某路 1 号”。前端不要再使用 `addressId`。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---:|---|---|---|---|---|
|
||||
| 1 | 新增供应商草稿 | POST | `/admin/supplier/items/add` | 请求字段 | 可传 `countyId`、`address` |
|
||||
| 2 | 修改供应商 | PUT | `/admin/supplier/items/{supplierId}/update` | 请求字段 | 可更新 `countyId`、`address` |
|
||||
| 3 | 提交供应商 | POST | `/admin/supplier/items/{supplierId}/submit` | 请求字段 | 完整表单可传 `countyId`、`address` |
|
||||
| 4 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应字段 | 顶层返回三级 ID 与 `address` |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 新增供应商草稿 `POST /admin/supplier/items/add`
|
||||
|
||||
**VO**: `SupplierDraftSaveReqVO / SupplierWriteRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
新增草稿时同时保存区县选择和详细地址。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `countyId` | Body | String | 否 | 正整数 | 三级联动最后一级区县 ID |
|
||||
| `address` | Body | String | 否 | 最长 500 字符 | 详细地址,不拼接省市区名称 |
|
||||
| `fullName` / `taxNo` / `mainCooperation` | Body | String | 是 | 沿用既有规则 | 其他新增必填字段 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.supplierId` | String | 新供应商 ID |
|
||||
| `data.updateTime` | String | 后续修改使用的并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"fullName":"示例旅行服务有限公司","taxNo":"91350211M000100Y46","countyId":"376","address":"某某路1号","mainCooperation":"景区合作"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","success":true,"data":{"supplierId":"2093966831894056961","status":"DRAFT","updateTime":"2026-08-30 15:39:31"}}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
省略 `countyId` 或 `address` 时按 `null` 保存;依赖或业务校验失败时不产生部分写入。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":400,"message":"区县ID必须为正数","success":false,"data":null}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 有三级联动选择结果时只传最后一级 `countyId`。
|
||||
- `addressId` 不是本接口字段。
|
||||
|
||||
### 2. 修改供应商 `PUT /admin/supplier/items/{supplierId}/update`
|
||||
|
||||
**VO**: `SupplierUpdateReqVO / SupplierWriteRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
编辑页修改区县或详细地址。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 | 供应商 ID |
|
||||
| `countyId` | Body | String | 否 | 正整数 | 新的最下级区县 ID |
|
||||
| `address` | Body | String | 否 | 最长 500 字符 | 新的详细地址 |
|
||||
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 详情最新并发版本 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.supplierId` | String | 供应商 ID |
|
||||
| `data.updateTime` | String | 保存后的新并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"countyId":"377","address":"某某路2号","expectedUpdateTime":"2026-08-30 15:39:31"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","success":true,"data":{"supplierId":"2093966831894056961","status":"DRAFT","updateTime":"2026-08-30 15:39:42"}}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
省略 `countyId` 或 `address` 表示该字段不修改;失败时原值和版本保持不变。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":400,"message":"区县ID必须为正数","success":false,"data":null}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 修改成功后使用响应中的新 `updateTime` 重新查询详情。
|
||||
- 非正数 `countyId` 在写入前拒绝。
|
||||
|
||||
### 3. 提交供应商 `POST /admin/supplier/items/{supplierId}/submit`
|
||||
|
||||
**VO**: `SupplierSubmitReqVO / SupplierApprovalCommandRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
提交完整供应商表单进入审批时一并保存区县和详细地址。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 | 草稿供应商 ID |
|
||||
| `countyId` | Body | String | 否 | 正整数 | 最下级区县 ID |
|
||||
| `address` | Body | String | 否 | 最长 500 字符 | 详细地址 |
|
||||
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 当前并发版本 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.approvalLogId` | String | 审批记录 ID |
|
||||
| `data.approvalStatus` | String | 审批状态 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"fullName":"示例旅行服务有限公司","taxNo":"91350211M000100Y46","countyId":"377","address":"某某路2号","mainCooperation":"景区合作","expectedUpdateTime":"2026-08-30 15:39:42"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","success":true,"data":{"approvalLogId":"2093967000000000001","approvalStatus":"APPROVED"}}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
`countyId`、`address` 为空时按完整表单既有规则处理;提交失败不推进审批状态。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":400,"message":"区县ID必须为正数","success":false,"data":null}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 仍须提交完整表单和最新 `expectedUpdateTime`。
|
||||
- 本次不改变既有审批、幂等和状态门禁。
|
||||
|
||||
### 4. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
|
||||
|
||||
**VO**: `SupplierBasicInfoRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
编辑页读取三级联动默认值和详细地址。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 | 供应商 ID |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.provinceId` | String/null | 省级 ID |
|
||||
| `data.prefectureId` | String/null | 市级 ID |
|
||||
| `data.countyId` | String/null | 已保存的最下级区县 ID |
|
||||
| `data.address` | String/null | 已保存的详细地址原值 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/2093966831894056961/basic-info/view
|
||||
Authorization: Bearer <admin-token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","success":true,"data":{"supplierId":"2093966831894056961","provinceId":"1","prefectureId":"35","countyId":"377","address":"某某路2号","updateTime":"2026-08-30 15:39:42"}}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
历史供应商没有 `countyId` 时三个行政区划 ID 均为 `null`,`address` 仍按历史值返回。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395001,"message":"供应商不存在","success":false,"data":null}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 三个行政区划 ID 均按 JSON 字符串返回。
|
||||
- 行政区划父链不可用或不完整时失败关闭,不伪造 ID。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
1. 用 `/admin/region/children` 逐级选择,将最后一级 ID 作为 `countyId`,门牌内容单独放入 `address`。
|
||||
2. 编辑页按 `provinceId → prefectureId → countyId` 设置默认选中,并用 `address` 初始化详细地址输入框。
|
||||
3. 前端字段统一命名为 `countyId`;保存后使用新版本重新查询详情。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 写接口成功后,后续详情查询返回已保存的 `countyId` 和 `address`。
|
||||
- 非正数 `countyId` 校验失败时不修改原值或并发版本。
|
||||
- 历史供应商不自动补行政区划 ID。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未认证返回业务码 `401`;供应商不存在或已删除返回 `395001`。
|
||||
- 业务失败可能仍使用 HTTP 200,前端须同时判断 `code` 与 `success`。
|
||||
- `address` 不包含省市区中文文本;省市区展示由三级 ID 对应的选项生成。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 项目 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 写请求 | 仅保存详细地址 | 可同时保存最下级 `countyId` 与详细地址 |
|
||||
| 详情回显 | 仅有 `address` | 顶层增加三个行政区划 ID,继续返回 `address` |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**:否,历史空值继续返回 `null`。
|
||||
- **前端是否必须同步上线**:是,需要传 `countyId` 并消费三级回显字段。
|
||||
- **前端 workaround 清理点**:移除 `addressId` 映射和自行解析完整中文地址的逻辑。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 不改变供应商权限、状态机、审批、并发和错误码。
|
||||
- 不改变行政区划联动接口契约,也不影响其他资源模块。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- 新增与修改后可回读对应 `countyId`、`address`,详情三个行政区划 ID 均为字符串。
|
||||
- 非正数 `countyId` 返回 `400` 且原数据不变;未认证返回 `401`;验收草稿已清理。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- [Issue #6739](https://git.1814.love:8443/wx/HL/issues/6739)
|
||||
- [PR #6744](https://git.1814.love:8443/wx/HL/pulls/6744)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **合并提交**: [2d24b3290](https://git.1814.love:8443/wx/HL/commit/2d24b32908bffb0e7ac95fd31b33cdfe56ae69f5)
|
||||
- **后端负责人**: @lc
|
||||
- **当前状态**: 已消费(`frontend_status: verified`)。RegionCascader 新增受控回显(watch value→getRegionPath 反查父链逐级预载,零 emit,停用节点占位);EditModal 提交体改 countyId+address 独立字段、fillForm 回填+快照增量比对(改动才携带、清空都不携带),删拼接逻辑;DetailModal 地址改 countyId 反查省市区名+address。spec 84 例绿,commit 5f35aafd 已推 v2.1。
|
||||
@@ -0,0 +1,293 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6811"
|
||||
title: "车队保存(无供应商)自动落停用;仍有在役车辆时以 601112 拒绝保存"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "6985f0cf"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-08-31"
|
||||
status_note: "#6717 只在「本次保存清除供应商」时强制停用,存量无供应商车队原样再保存仍停留启用。本单把守卫改为按保存结果判定,并对齐 disable 的在役车辆不变量新增 601112。TEST 已实测两条分支。前端已消费:submitForm 补 601112 精确提示,保存后以响应/列表刷新状态展示。"
|
||||
updated_at: "2026-08-31"
|
||||
base: "dev-v3"
|
||||
generated: "2026-08-31T10:45:00+08:00"
|
||||
---
|
||||
|
||||
# 车队编辑保存:无供应商时自动落停用,仍有在役车辆则以 601112 拒绝
|
||||
|
||||
> **服务**: hl-fleet-service (端口 8087/8187)
|
||||
> **PR**: #6827
|
||||
> **Issue**: #6811
|
||||
> **日期**: 2026-08-31
|
||||
> **影响范围**: 管理后台车务 → 车队管理 → 编辑车队保存
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
- 保存车队时若**结果为无供应商**(`supplierId` 传 `null` 或不传),后端不再允许车队停留在「启用」:无在役车辆时自动把状态落为 `DISABLED`,前端保存成功后需按响应/列表刷新后的状态展示,不能沿用提交前的「启用」。
|
||||
- 上一版(#6717)只在「本次保存把已绑供应商清空」时才强制停用,存量无供应商车队原样再保存不变状态。**该行为已收口**:现在按保存结果判定,`null → null` 同样强制停用。
|
||||
- 新增错误码 **601112**:无供应商需自动停用但车队仍有在役车辆时,保存整体失败、零写入。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
`fleet_team` 的不变量是 **ACTIVE ⇒ 已绑供应商**。此前新建(无供应商落停用)和启用(无供应商 601108 拒绝)都有守卫,唯独编辑保存漏了「结果无供应商」这一路,导致管理后台车队列表出现「供应商列为 —(无供应商)+ 状态启用」的行,且反复保存也不收敛。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 修改车队 | PUT | `/admin/fleet/teams/{fleetTeamId}` | 状态副作用 + 新增错误码 | 保存结果无供应商时自动落停用;仍有在役车辆则 601112 拒绝 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 修改车队 `PUT /admin/fleet/teams/{fleetTeamId}`
|
||||
|
||||
**VO**: `FleetTeamSaveReqVO` → `Result<FleetTeamRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
车务 → 车队管理 → 列表行「编辑」→ 弹窗改车队名称/类型/负责人/电话/付款方式/排序/备注/供应商后点「保存」。本次仅状态副作用与错误码变化,路径、请求字段、字段类型与必填性都不变。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| fleetTeamId | Path | String(雪花 ID) | ✅ | - | 车队 ID,字符串传递,禁止数值化 |
|
||||
| teamName | Body | String | ✅ | 非空、全局唯一 | 车队名称 |
|
||||
| teamType | Body | String | ✅ | `SELF_OPERATED` / `COOPERATIVE` | 已关联车辆时不可切换 |
|
||||
| leaderName | Body | String | ✅ | 非空 | 负责人 |
|
||||
| leaderPhone | Body | String | ✅ | 手机号 | 负责人电话 |
|
||||
| settleType | Body | String | ✅ | `cash` / `sign` / `company` | 付款方式(资源付款方式字典) |
|
||||
| sortOrder | Body | Integer | ✅ | ≥ 0 | 排序 |
|
||||
| remark | Body | String | ❌ | ≤ 256 | 备注,空串按 null 落库 |
|
||||
| supplierId | Body | String(雪花 ID) | ❌ | 供应商需生效且含 FLEET 类型 | **不传或传 null = 不选供应商**,触发本次自动停用规则 |
|
||||
|
||||
#### 出参 `Result<FleetTeamRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| fleetTeamId | String | 车队 ID |
|
||||
| teamName | String | 车队名称 |
|
||||
| teamType | String | `SELF_OPERATED` / `COOPERATIVE` |
|
||||
| status | String | **`ACTIVE` / `DISABLED`;保存结果无供应商时返回 `DISABLED`** |
|
||||
| settleType | String | 付款方式 |
|
||||
| sortOrder | Integer | 排序 |
|
||||
| supplierId | String / null | 关联供应商 ID,null = 未关联 |
|
||||
| supplierName | String / null | 供应商全称快照,供应商为 null 时同步清空 |
|
||||
| vehicleCount | Number | 名下车辆总数 |
|
||||
| activeVehicleCount | Number | 在役车辆数 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
无供应商的车队原样保存(触发自动停用):
|
||||
|
||||
```json
|
||||
{
|
||||
"teamName": "测试车队P4-1787131721",
|
||||
"teamType": "COOPERATIVE",
|
||||
"leaderName": "测试负责人",
|
||||
"leaderPhone": "13900000001",
|
||||
"settleType": "cash",
|
||||
"sortOrder": 99,
|
||||
"remark": "由 fleet_attribution/历史业务数据迁移,负责人待完善",
|
||||
"supplierId": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
无供应商 + 无在役车辆 → 200,且 `status` 已变 `DISABLED`:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"fleetTeamId": "348397854705979392",
|
||||
"teamName": "测试车队P4-1787131721",
|
||||
"teamType": "COOPERATIVE",
|
||||
"status": "DISABLED",
|
||||
"settleType": "cash",
|
||||
"sortOrder": 99,
|
||||
"supplierId": null,
|
||||
"supplierName": null,
|
||||
"vehicleCount": 0,
|
||||
"activeVehicleCount": 0
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口为写接口,不返回空集。供应商资格校验依赖(resource 侧)不可用时**失败关闭**,不降级放行:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 601110,
|
||||
"message": "暂时无法校验供应商,请稍后重试",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
新增(本次)——无供应商需自动停用但仍有在役车辆:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 601112,
|
||||
"message": "车队未关联供应商需自动停用,但仍有在役车辆,请先关联供应商或转移在役车辆",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
本接口其他错误码(本次未变,供自包含联调):
|
||||
|
||||
| code | message | 触发条件 |
|
||||
|------|---------|----------|
|
||||
| 601100 | 车队不存在 | `fleetTeamId` 无对应车队 |
|
||||
| 601101 | 车队名称已存在 | `teamName` 与其他车队重名 |
|
||||
| 601104 | 车队已关联车辆,不能修改自有/合作类型 | 名下有车辆时改 `teamType` |
|
||||
| 601106 | 付款方式不是有效的资源付款方式 | `settleType` 不在 `cash`/`sign`/`company` |
|
||||
| 601109 | 供应商不存在、未生效或不包含车队类型 | 绑定的 `supplierId` 不合格 |
|
||||
| 601110 | 暂时无法校验供应商,请稍后重试 | 供应商资格校验依赖不可用 |
|
||||
| 601111 | 车队已关联订单,不能更换供应商 | 已绑供应商的车队换绑/清除且名下车辆有非取消派单 |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 鉴权:需登录且具备车务车队管理权限;未登录网关返 401。
|
||||
- **状态不由前端入参驱动**:请求体没有 `status` 字段,启停仍由 `/disable`、`/enable` 切;唯一例外是本次的自动停用副作用。
|
||||
- 供应商未变时不调供应商资格校验,也不查订单围栏;仅当 `supplierId` 发生变化才校验。
|
||||
- 已绑供应商的车队换绑或清除时,名下车辆若有非取消派单一律 601111(该守卫先于 601112 判定)。
|
||||
- 失败零写入:601112、601111、601109、601110 均在写库前抛出,名称、排序、备注等字段都不会部分保存。
|
||||
- 并发:同一 `fleetTeamId` 的保存互斥(分布式锁),重复提交不会产生中间态。
|
||||
- 已停用且无供应商的车队保存时保持停用,不报错。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
| 场景 | payload 与结果 |
|
||||
|------|---------------|
|
||||
| ✅ 绑定合法供应商保存 | `{"supplierId": "2094238127517319170", ...}` → 200,`status` 保持原值 |
|
||||
| ✅ 无供应商 + 无在役车辆 | `{"supplierId": null, ...}` → 200,`status=DISABLED` |
|
||||
| ✅ 已停用 + 无供应商 | `{"supplierId": null, ...}` → 200,`status` 保持 `DISABLED` |
|
||||
| ❌ 无供应商 + 有在役车辆 | `{"supplierId": null, ...}` → 601112,零写入 |
|
||||
| ❌ 靠不传 `supplierId` 保留原供应商 | 不传等同传 `null`,会被当成「清除供应商」 |
|
||||
|
||||
### 切换状态时的必要动作
|
||||
|
||||
- 编辑回显后提交必须**原样回传详情里的 `supplierId`**;字段缺失就是清除语义,不存在「不传 = 不改」。
|
||||
- 保存成功后不能沿用提交前的状态展示:用响应体 `data.status` 或重拉列表/详情。
|
||||
- 收到 601112 时弹后端 message 即可;修复路径是「选一个合法供应商」或「先把在役车辆转走/停用」。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
| 前端提交 | 保存后外部可观察结果 |
|
||||
|----------|--------------------|
|
||||
| `supplierId` 合法值 | 车队关联该供应商,`supplierName` 刷为当前全称快照,状态不变 |
|
||||
| `supplierId=null`,无在役车辆 | 供应商关联与全称快照同时置空,状态变 `DISABLED` |
|
||||
| `supplierId=null`,有在役车辆 | 零写入(名称/排序/备注等也不保存) |
|
||||
|
||||
本次无表结构变更、无 Flyway 迁移,存量脏数据不做批量刷状态(下次编辑保存时自然收口)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截)。
|
||||
- 车队不存在 → 601100。
|
||||
- 供应商资格依赖降级 → 601110 失败关闭,不静默放行。
|
||||
- 存量无供应商且仍启用的车队:不会被后台任务批量改状态,只在下次编辑保存时收口。
|
||||
- 已绑供应商的车队保存行为与之前一致,无新增拦截。
|
||||
- 供应商改名后快照陈旧:仍需重新编辑车队刷新(行为未变)。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 请求体字段 | 无变化 | 无变化(路径、字段名、类型、必填性全部不变) |
|
||||
| 响应 `data.status` | 仅当本次清除已绑供应商时可能返回 `DISABLED` | **只要保存结果无供应商且原为启用,就返回 `DISABLED`** |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 存量无供应商车队(`null → null`)原样保存 | 保持「启用」 | 无在役车辆 → 自动「停用」;有在役车辆 → 601112 |
|
||||
| 清除已绑供应商(`X → null`) | 启用→停用(不查在役车辆) | 有在役车辆时改为 601112 拒绝,不再产出「停用车队挂在役车辆」脏态 |
|
||||
| 绑定/换绑合法供应商 | 200,状态不变 | 不变 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**:否(请求/响应结构不变,新增一个错误码与一个状态副作用)。
|
||||
- **前端是否必须同步上线**:否。旧前端不报错,但若保存后不刷新列表,页面会短暂显示陈旧的「启用」。
|
||||
- **前端 workaround 清理点**:若前端曾为「无供应商仍显示启用」做过本地推断或提示,可改为直读后端 `status`。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**:管理后台车队编辑保存(`PUT /admin/fleet/teams/{fleetTeamId}`)。
|
||||
- **零影响**:
|
||||
- 新建车队 `POST /admin/fleet/teams`(无供应商本来就落停用)
|
||||
- 启用/停用 `POST /admin/fleet/teams/{id}/enable`、`/disable`
|
||||
- 车队列表、详情、options 读接口字段
|
||||
- 车辆、司机、派单、看板、对账等其他车务接口
|
||||
- 存量数据(不批量刷状态)
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:`dev-v3` 合入 PR #6827(merge `740fdb834`)后部署 hl-fleet-service(8087/8187 滚动,两实例健康)。
|
||||
|
||||
```
|
||||
PUT /admin/fleet/teams/348397854705979392 (无供应商 + 启用 + activeVehicleCount=0)
|
||||
→ 200 成功,保存后 GET 详情 status=DISABLED, supplierId=null ✓
|
||||
|
||||
PUT /admin/fleet/teams/348398422098841600 (无供应商 + 启用 + activeVehicleCount=1)
|
||||
→ 601112 「车队未关联供应商需自动停用,但仍有在役车辆…」
|
||||
保存后 GET 详情 status=ACTIVE(零写入) ✓
|
||||
|
||||
PUT /admin/fleet/teams/2026072200010000001 (已绑供应商 + 有订单, 清除供应商)
|
||||
→ 601111 既有订单围栏优先,行为未变 ✓
|
||||
```
|
||||
|
||||
验证用车队:`348397854705979392`(测试车队P4-1787131721)、`348398422098841600`(测试车队P4-1787131856,验证用临时车辆已删除并回到 0 车)。
|
||||
|
||||
单测与构建:`mvn -pl hl-fleet-service -am verify` → Tests run 3887 / Failures 0 / Errors 0(Skipped 5 为既有 Release-E 跳过项);`mvn -pl hl-fleet-service spotless:check` → 799 文件 0 违规。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 前置契约:`changelogs-v2/2026-08/30_6717_车队关联供应商并展示供应商全名-修改接口-管理后台.md`
|
||||
- 本次 PR:[#6827](https://git.1814.love:8443/wx/HL/pulls/6827)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- Issue:[#6811](https://git.1814.love:8443/wx/HL/issues/6811)
|
||||
- 后端联系人:@wx
|
||||
@@ -0,0 +1,488 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6834"
|
||||
title: "供应商余额支付类型与草稿编辑项"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "hl-admin"
|
||||
frontend_ref: "848cca7b"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-01"
|
||||
status_note: "PR #6860 已合并 dev-v3,合并提交 6ed8e24c0f04051c9c4385c79de67f255abb4952 已部署 TEST。真实 Gateway 已验证支付类型字典、新增必填与失败零写入、余额和支付类型保存回显、草稿省略 changeReason 更新及验收数据清理。前端 hl-admin v2.1 提交 848cca7b 已交付:SupplierEditModal 新增余额 NInputNumber(数值允许负数,提交 number)+支付类型 NSelect(接 supplier_payment_type 字典存 dictValue 显 dictLabel,字典未就绪禁用提交不硬编码),balance 必填用 type:'number' 修 async-validator 误判,编辑按快照增量仅改动携带,DetailModal 回显两字段;spec 12 例绿。当前状态:后端已就绪,前端已交付。"
|
||||
updated_at: "2026-09-01"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商余额支付类型与草稿编辑项
|
||||
|
||||
供应商新增、提交、编辑和详情增加 `balance`、`paymentType`。新增与提交时两项必填;编辑时可按增量提交,详情用于回显。支付类型从 `supplier_payment_type` 字典读取。
|
||||
|
||||
供应商详情 `status=DRAFT` 时,编辑页隐藏“变更原因”;其他状态继续显示并必填。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---:|---|---|---|---|---|
|
||||
| 1 | 新增供应商草稿 | POST | `/admin/supplier/items/add` | 请求字段 | `balance`、`paymentType` 必填 |
|
||||
| 2 | 提交供应商审批 | POST | `/admin/supplier/items/{supplierId}/submit` | 请求字段 | 完整表单中的两个字段必填 |
|
||||
| 3 | 编辑供应商 | PUT | `/admin/supplier/items/{supplierId}/update` | 请求字段与状态规则 | 可更新两个字段;草稿可省略 `changeReason` |
|
||||
| 4 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应字段 | 返回 `balance`、`paymentType`、`status` |
|
||||
| 5 | 查询支付类型字典类型 | GET | `/admin/dict/type` | 新增字典数据 | 可按 `supplier_payment_type` 查询字典类型 |
|
||||
| 6 | 查询支付类型字典项 | GET | `/admin/dict/data/supplier_payment_type` | 新增字典数据 | 返回现付、签单、月付 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 新增供应商草稿 `POST /admin/supplier/items/add`
|
||||
|
||||
**VO**: `SupplierDraftSaveReqVO / SupplierWriteRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
新增供应商时录入初始余额并选择支付类型。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `balance` | Body | Number | 是 | 最多 16 位整数、2 位小数,可为正数、0 或负数 | 供应商当前余额 |
|
||||
| `paymentType` | Body | String | 是 | 必须命中当前生效的 `supplier_payment_type` 字典项 | 支付类型值,传 `1`、`2` 或 `3` |
|
||||
| `fullName` / `taxNo` / `mainCooperation` | Body | String | 是 | 沿用既有规则 | 其他新增必填字段 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.supplierId` | String | 新供应商 ID |
|
||||
| `data.status` | String | 新增草稿为 `DRAFT` |
|
||||
| `data.updateTime` | String | 后续编辑使用的并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "示例供应商有限公司",
|
||||
"taxNo": "91350211M000100Y46",
|
||||
"mainCooperation": "旅游资源合作",
|
||||
"balance": 1200.50,
|
||||
"paymentType": "1"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2094278854020399106",
|
||||
"status": "DRAFT",
|
||||
"updateTime": "2026-08-31 12:19:23"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
`balance` 或 `paymentType` 省略、为 `null` 或支付类型为空白时返回业务码 `400`,不创建草稿。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":400,"message":"余额不能为空","success":false,"data":null}
|
||||
```
|
||||
|
||||
```json
|
||||
{"code":400,"message":"供应商支付类型不合法或已停用","success":false,"data":null}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 前端必须传数值型 `balance`,不要传格式化金额字符串。
|
||||
- `paymentType` 传字典 `dictValue`,不要传“现付”等展示文本。
|
||||
- 校验失败时不产生供应商记录。
|
||||
|
||||
### 2. 提交供应商审批 `POST /admin/supplier/items/{supplierId}/submit`
|
||||
|
||||
**VO**: `SupplierSubmitReqVO / SupplierApprovalCommandRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
把完整供应商表单提交审批时,一并提交余额和支付类型。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 草稿供应商 |
|
||||
| `balance` | Body | Number | 是 | 最多 16 位整数、2 位小数 | 当前余额 |
|
||||
| `paymentType` | Body | String | 是 | 当前生效字典值 | 支付类型 |
|
||||
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 当前供应商并发版本 |
|
||||
| 其他完整表单字段 | Body | 对应类型 | 按既有规则 | 沿用现有提交契约 | 不得只提交本次新增字段 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.approvalLogId` | String | 审批记录 ID |
|
||||
| `data.approvalStatus` | String | 审批状态 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "示例供应商有限公司",
|
||||
"taxNo": "91350211M000100Y46",
|
||||
"mainCooperation": "旅游资源合作",
|
||||
"balance": 1200.50,
|
||||
"paymentType": "1",
|
||||
"expectedUpdateTime": "2026-08-31 12:19:23"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","success":true,"data":{"approvalLogId":"2094279000000000001","approvalStatus":"PENDING"}}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
两个新增字段任一为空即拒绝提交,审批状态不推进。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":400,"message":"支付类型不能为空","success":false,"data":null}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 提交仍是完整表单命令,并继续执行既有状态、并发、审批和幂等门禁。
|
||||
- 支付类型失效或不存在时失败关闭,不使用本地默认值代替。
|
||||
|
||||
### 3. 编辑供应商 `PUT /admin/supplier/items/{supplierId}/update`
|
||||
|
||||
**VO**: `SupplierUpdateReqVO / SupplierWriteRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
编辑页修改余额或支付类型;根据详情的服务端状态决定是否显示变更原因。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
|
||||
| `balance` | Body | Number | 否 | 最多 16 位整数、2 位小数 | 省略表示不修改 |
|
||||
| `paymentType` | Body | String | 否 | 非空时必须命中当前生效字典项 | 省略表示不修改 |
|
||||
| `changeReason` | Body | String | 条件必填 | 最长 500 | `DRAFT` 可省略;其他状态去空白后必须非空 |
|
||||
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 详情最新并发版本 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.supplierId` | String | 供应商 ID |
|
||||
| `data.status` | String | 保存后的服务端状态 |
|
||||
| `data.updateTime` | String | 保存后的新并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
`DRAFT` 草稿完全省略 `changeReason`:
|
||||
|
||||
```json
|
||||
{
|
||||
"balance": -12.34,
|
||||
"paymentType": "3",
|
||||
"expectedUpdateTime": "2026-08-31 12:19:23"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","success":true,"data":{"supplierId":"2094278854020399106","status":"DRAFT","updateTime":"2026-08-31 12:19:33"}}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
省略 `balance` 或 `paymentType` 表示该字段不修改;历史供应商尚未补录时,详情可返回 `null`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
非草稿缺少变更原因:
|
||||
|
||||
```json
|
||||
{"code":400,"message":"变更原因不能为空","success":false,"data":null}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 是否为草稿只取服务端锁定后的当前状态,不接收客户端自报状态。
|
||||
- 草稿隐藏“变更原因”并省略 `changeReason`;非草稿继续显示且必填。
|
||||
- 更新成功后使用新 `updateTime` 重新查询详情;并发失败返回既有 `395014`。
|
||||
|
||||
### 4. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
|
||||
|
||||
**VO**: `SupplierBasicInfoRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
新增后或进入编辑页时回显余额、支付类型和当前状态。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.balance` | Number/null | 当前余额;历史未补录可为 `null` |
|
||||
| `data.paymentType` | String/null | `supplier_payment_type` 字典值;历史未补录可为 `null` |
|
||||
| `data.status` | String | 用于控制变更原因框 |
|
||||
| `data.updateTime` | String | 编辑请求的并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/2094278854020399106/basic-info/view
|
||||
Authorization: Bearer <admin-token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2094278854020399106",
|
||||
"balance": -12.34,
|
||||
"paymentType": "3",
|
||||
"status": "DRAFT",
|
||||
"updateTime": "2026-08-31 12:19:33"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
历史供应商没有新字段时返回 `null`;前端显示未补录,不自行写入默认值。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395001,"message":"供应商不存在","success":false,"data":null}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `paymentType` 原样返回字典值,中文标签由字典项映射。
|
||||
- 业务失败可能仍是 HTTP 200,必须同时判断 `code` 和 `success`。
|
||||
|
||||
### 5. 查询支付类型字典类型 `GET /admin/dict/type`
|
||||
|
||||
**VO**: `PageResult<SysDictTypeRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
字典管理页或联调时确认 `supplier_payment_type` 类型已经存在。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| `page` | Query | Number | 否 | 默认 1 | 页码 |
|
||||
| `pageSize` | Query | Number | 否 | 默认 20 | 每页条数 |
|
||||
| `keyword` | Query | String | 否 | 传 `supplier_payment_type` | 按编码或名称搜索 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.records[].dictType` | String | `supplier_payment_type` |
|
||||
| `data.records[].dictName` | String | `供应商支付类型` |
|
||||
| `data.records[].status` | String | `ACTIVE` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/dict/type?page=1&pageSize=20&keyword=supplier_payment_type
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","success":true,"data":{"records":[{"dictType":"supplier_payment_type","dictName":"供应商支付类型","status":"ACTIVE"}],"total":1,"page":1,"pageSize":20}}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
未命中时 `records=[]`;这表示字典未就绪,前端不要回退到其他结算字典。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":401,"message":"Token无效或已过期","success":false,"data":null}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 该接口只查询字典类型,不直接提供下拉项。
|
||||
- 下拉选项必须继续调用下一接口。
|
||||
|
||||
### 6. 查询支付类型字典项 `GET /admin/dict/data/supplier_payment_type`
|
||||
|
||||
**VO**: `List<SysDictDataRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
新增、编辑供应商页面加载支付类型下拉选项。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---:|---|---|
|
||||
| 无 | - | - | 否 | 字典编码固定在路径中 | 无请求体、查询参数或动态路径参数 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data[].dictValue` | String | 请求保存的值 |
|
||||
| `data[].dictLabel` | String | 下拉展示文案 |
|
||||
| `data[].sortOrder` | Number | 排序号 |
|
||||
| `data[].status` | String | 当前均为 `ACTIVE` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/dict/data/supplier_payment_type
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": [
|
||||
{"dictValue":"1","dictLabel":"现付","sortOrder":10,"status":"ACTIVE"},
|
||||
{"dictValue":"2","dictLabel":"签单","sortOrder":20,"status":"ACTIVE"},
|
||||
{"dictValue":"3","dictLabel":"月付","sortOrder":30,"status":"ACTIVE"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
返回空数组或调用失败时禁用提交并提示字典加载失败,不硬编码替代值。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":401,"message":"Token无效或已过期","success":false,"data":null}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 请求保存 `dictValue`,界面展示 `dictLabel`。
|
||||
- 后端每次写入都动态校验当前生效项;已停用或未知值返回业务码 `400`。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
1. 页面初始化先查询详情与支付类型字典项,用 `paymentType` 匹配 `dictValue`。
|
||||
2. 新增和提交时 `balance`、`paymentType` 都必填;编辑时只发送实际修改字段和最新 `expectedUpdateTime`。
|
||||
3. `status=DRAFT` 隐藏“变更原因”并可省略 `changeReason`;其他状态必须收集非空原因。
|
||||
4. 金额输入保留最多两位小数,允许负数;不要把千分位格式化文本发给后端。
|
||||
|
||||
| 场景 | payload | 结果 |
|
||||
|---|---|---|
|
||||
| 新增完整 | `{ "balance": 100.00, "paymentType": "1", ... }` | 成功 |
|
||||
| 新增缺余额 | `{ "paymentType": "1", ... }` | `400`,余额不能为空 |
|
||||
| 新增非法支付类型 | `{ "balance": 100.00, "paymentType": "9", ... }` | `400`,支付类型不合法或已停用 |
|
||||
| 草稿编辑 | `{ "balance": -12.34, "paymentType": "3", "expectedUpdateTime": "..." }` | 成功,可省略 `changeReason` |
|
||||
| 非草稿编辑缺原因 | `{ "balance": 0, "expectedUpdateTime": "..." }` | `400`,变更原因不能为空 |
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 新增或更新成功后,详情回读相同的 `balance`、`paymentType`。
|
||||
- 历史供应商不强制回填,新字段可返回 `null`;创建和提交的新数据仍由接口强制必填。
|
||||
- 审批与非草稿变更继续保留两个字段的业务快照;失败时不产生部分写入。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- `balance` 最多 16 位整数和 2 位小数,正数、0、负数均有效。
|
||||
- `paymentType` 必须是当前生效字典值;未知、空白或停用值不保存。
|
||||
- 未登录返回业务码 `401`;不存在返回 `395001`;并发版本过期返回 `395014`。
|
||||
- 不新增权限、Gateway 路由、Redis、MQ 或配置行为。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### `paymentType`(字典 `supplier_payment_type`)
|
||||
|
||||
**所属字段**: `SupplierDraftSaveReqVO.paymentType / SupplierUpdateReqVO.paymentType / SupplierBasicInfoRespVO.paymentType` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `1` | 现付 | 按现付方式处理 |
|
||||
| `2` | 签单 | 按签单方式处理 |
|
||||
| `3` | 月付 | 按月付方式处理 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 项目 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 新增与提交 | 无余额、支付类型字段 | 两字段必填并校验 |
|
||||
| 编辑与详情 | 无法录入和回显两字段 | 支持增量修改并原值回显 |
|
||||
| 支付类型选项 | 无专用字典 | 使用 `supplier_payment_type` 三个生效项 |
|
||||
| 草稿变更原因 | 页面仍可能显示 | `DRAFT` 隐藏;非草稿继续必填 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**:新增、提交请求增加必填字段,前端必须同步;编辑与详情为兼容扩展。
|
||||
- **前端是否必须同步上线**:是,需要新增两个控件、接入字典并按状态调整变更原因框。
|
||||
- **前端 workaround 清理点**:删除支付类型硬编码和草稿 `changeReason` 必填/展示逻辑;非草稿校验必须保留。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 不修改供应商权限、状态机、审批流程、并发、软删除和既有错误码。
|
||||
- 不复用 `resource_settle_type`,也不影响供应商账户、合同和资源关系接口。
|
||||
- 本次仅交付后端接口 Changelog,不修改任何前端源码或资源。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- TEST Gateway 查询到字典类型及精确字典项 `1-现付`、`2-签单`、`3-月付`。
|
||||
- 缺余额、缺支付类型、非法支付类型均返回 `400`,验证前后列表为空。
|
||||
- 有效新增返回 `DRAFT`;详情回显 `balance=12.34`、`paymentType=1`。
|
||||
- 草稿省略 `changeReason` 更新成功;详情回显 `balance=-12.34`、`paymentType=3`。
|
||||
- 验收草稿已通过删除接口清理,列表为空且详情返回“供应商不存在”。
|
||||
|
||||
## 当前状态
|
||||
|
||||
- 后端:已部署并已验证。
|
||||
- 前端:待处理。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue:[#6834](https://git.1814.love:8443/wx/HL/issues/6834)
|
||||
- 后端 PR:[#6860](https://git.1814.love:8443/wx/HL/pulls/6860)
|
||||
- 合并提交:[6ed8e24c](https://git.1814.love:8443/wx/HL/commit/6ed8e24c0f04051c9c4385c79de67f255abb4952)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#6834](https://git.1814.love:8443/wx/HL/issues/6834)
|
||||
- **PR**: [#6860](https://git.1814.love:8443/wx/HL/pulls/6860)
|
||||
- **后端负责人**: @lc
|
||||
- **当前状态**: 后端已就绪,前端待处理。
|
||||
@@ -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: "verified"
|
||||
frontend_owner: "hl-admin"
|
||||
frontend_ref: "f5414b12"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-01"
|
||||
status_note: "PR #6858 已合并 dev-v3,hl-resource-service 已部署提交 5546d7907c6eda9b372b4639094f84e568de6496。TEST Gateway 已验证完整字段与全空业务字段合同的新增、更新、详情回读,以及单个 PDF 上传、预览、下载和清理。前端 hl-admin v2.1 提交 f5414b12 已交付:ContractEditModal 增 businessLine/relatedMainContract/autoRenew(三态,false 不按空过滤)+SIGNED 选项,scanFileUrl 改 FileUpload 文件中心单附件(upload/token→confirm 写 ossUrl),隐藏 contractType/amount/pricingMode/settleCycle/remark 但编辑原样回传防整份替换误清;ManageModal 列重排契约固定集、SIGNED→已签约、有效期直拼上海日历日、附件预览带登录态 preview-by-url/下载直开永久地址;api/file.js 加 getContractPreviewBlobUrl。spec 编辑 16 例+管理 6 例绿。当前状态:后端已就绪,前端已交付。"
|
||||
updated_at: "2026-09-01"
|
||||
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
|
||||
@@ -0,0 +1,877 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6843"
|
||||
title: "供应商暂停合作联动停用车队与派单拦截"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "f1b861c6"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-08-31"
|
||||
status_note: "PR #6868 已合并 dev-v3(merge commit df78cce5)已部署测试服。前端已消费:useAssignFlow 补 605074 专用分支「车队已停用不可派新单,请先启用车队或换车」;precheck 前端无调用方白名单属 no-op;司机候选静默排除无适配量。"
|
||||
updated_at: "2026-08-31"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 车务派单:供应商暂停合作联动停用车队,派单全链路拦截停用车队资源
|
||||
|
||||
> **存放目录**:
|
||||
> - 二期(v3,`order-v3` 标签的工单)→ `changelogs-v2/2026-08/`
|
||||
>
|
||||
> **服务**: hl-fleet-service(端口 8087/8187,双实例滚动)
|
||||
> **PR**: #6868
|
||||
> **Issue**: #6843
|
||||
> **作者**: wx
|
||||
> **更新时间**: 2026-08-31
|
||||
> **影响范围**: 管理后台「车务 - 派单」候选查询 / 预校验 / 创建(批量)/ 改派;新增服务间 internal 联动端点(前端不直接调用)
|
||||
|
||||
供应商暂停合作(SUSPENDED)/拉黑(BLACKLIST)后,其名下在启车队被批量联动停用;停用车队的车辆与常驻司机不再可派新单。**已派订单不受影响**(存量派单的最终确认、撤销取消不挂新校验)。
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
- **precheck 新增 warning type `fleet_team_disabled`**:`POST /admin/fleet/assignments/precheck` 的 `warnings[]` 新增该类型(文案「车队已停用不可派新单,提交派单将被拦截,请先启用车队或换车」)。前端若按 type 白名单渲染提示,需把新 type 加入渲染映射,否则该提示会被静默丢弃。
|
||||
- **新错误码 605074**:「车队已停用不可派新单(请先启用车队或换车)」。派单创建(批量,单派同路径)与改派在写库前硬拦截;前端错误码字典需补 605074 文案映射,建议引导动作 = 启用车队或更换车辆/司机。
|
||||
- **司机候选静默排除**:`POST /admin/fleet/assignments/candidates` 的司机候选不再返回「常驻车所属车队已停用」的司机(无新增/删除字段,结果集合缩小;车辆侧自 #6717 起已排除)。
|
||||
- **新增 internal 端点** `POST /internal/fleet-teams/supplier-status-sync`:仅供供应商域(resource 侧,工单 #6844)Feign 调用,**前端不直接调用、网关不路由**;本文按模板写全契约供服务间对齐。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
工单 #6843:供应商生命周期进入「停止合作」态(SUSPENDED=暂停合作 / BLACKLIST=拉黑)时,其名下关联车队应联动停用,且停用车队的司机/车辆不得再派新订单。
|
||||
|
||||
前置条件已由 #6717 建好:车队主数据持有 `supplierId`/`supplierName` 快照,车队停用会合并车辆可派状态(车辆侧候选过滤与 605037 拦截已存在)。本单补齐两环:
|
||||
|
||||
1. **联动入口(fleet 侧接收端)**:新增 internal 端点接收供应商域的状态同步事件,CAS 批量停用该供应商名下 `status=ACTIVE` 的车队。与人工停用的差异:供应商侧停用是**强制联动**,不做「在役车辆」守卫(车队不能再接新单与名下车辆是否仍在役无关)。
|
||||
2. **派单链路拦截**:候选查询排除(司机侧新增)+ precheck 预警(新增 type)+ 创建/改派锁内硬校验(新错误码 605074,先于车辆/司机自身状态校验 605037/605038 执行)。
|
||||
|
||||
供应商侧通知发出端由工单 #6844 实现(本 PR 合并后指派),届时按本文 §三.1 契约 Feign 投递。车队域暂无独立操作日志表,停用结果经响应回执 + 服务端日志留痕。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 供应商状态同步(内部) | POST | `/internal/fleet-teams/supplier-status-sync` | 新增接口 | SUSPENDED/BLACKLIST 批量停用名下在启车队;同 eventId 重投返 duplicate=true 成功回执 |
|
||||
| 2 | 查询派单候选资源 | POST | `/admin/fleet/assignments/candidates` | 行为变化 | 司机候选排除常驻车所属车队已停用的司机(无字段变化,集合缩小) |
|
||||
| 3 | 派单预校验冲突 | POST | `/admin/fleet/assignments/precheck` | 响应新增枚举值 | `warnings[]` 新增 type `fleet_team_disabled`(只提示不阻断) |
|
||||
| 4 | 批量创建派单 | POST | `/admin/fleet/assignments/batch` | 新增错误码 | 车队停用硬校验 605074,先于 605037/605038;单派/批量同路径 |
|
||||
| 5 | 修改派单(改派) | POST | `/admin/fleet/assignments/{assignmentId}/change` | 新增错误码 | 换车/换司机到停用车队资源被 605074 拦截;纯费用调整司机侧不查 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 供应商状态同步(内部) `POST /internal/fleet-teams/supplier-status-sync`
|
||||
|
||||
**VO**: `SupplierStatusSyncReqVO` / `Result<SupplierStatusSyncRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
供应商域(resource 侧,工单 #6844)在供应商生命周期变更为 SUSPENDED/BLACKLIST 后,经 Feign LB 直连投递本端点;fleet 侧批量停用该供应商名下 `status=ACTIVE` 的车队,让派单链路(候选排除 + 创建/改派硬校验 605074)不再放行其资源。
|
||||
|
||||
`/internal` 前缀**不经网关路由**(网关实测返回业务码 404「接口不存在」),`X-Internal-Token` 由 InternalAuthFilter 统一校验。前端不直接调用本端点。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `supplierId` | Body | String(Long) | 是 | `@Positive` | 供应商 ID(雪花,建议按字符串传输,禁止转 JavaScript Number) |
|
||||
| `targetStatus` | Body | String | 是 | ≤32,大小写不敏感 | 供应商目标状态;仅 `SUSPENDED`/`BLACKLIST` 触发联动停用,其余状态忽略 |
|
||||
| `eventId` | Body | String | 是 | ≤64 | 事件 ID(幂等键,同一事件重复投递不产生二次变更) |
|
||||
| `occurredAt` | Body | String/null | 否 | ISO 日期时间 | 事件发生时间(观测/日志用;本端不落供应商状态镜像,不做乱序围栏) |
|
||||
|
||||
#### 出参 `Result<SupplierStatusSyncRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `supplierId` | String | 供应商 ID(回显,雪花序列化为字符串) |
|
||||
| `targetStatus` | String | 供应商目标状态(回显调用方传值) |
|
||||
| `ignored` | Boolean | 是否忽略(目标状态非 SUSPENDED/BLACKLIST,未做联动) |
|
||||
| `duplicate` | Boolean | 是否重复投递(true=同 eventId 幂等窗口内已消费,不产生二次变更;重复回执 `disabledCount` 恒 0、清单恒空) |
|
||||
| `disabledCount` | Integer | 本次实际停用的车队数(CAS 实际影响行数;并发人工停用时可能小于快照大小;重复投递为 0) |
|
||||
| `disabledTeamIds` | String[] | 本次停用的车队 ID 列表(停用前快照,雪花字符串,供调用方核对) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"supplierId": "8996843000000000001",
|
||||
"targetStatus": "SUSPENDED",
|
||||
"eventId": "supplier-status-8996843000000000001-SUSPENDED-20260831142000",
|
||||
"occurredAt": "2026-08-31T14:20:00"
|
||||
}
|
||||
```
|
||||
|
||||
(传输头:`POST /internal/fleet-teams/supplier-status-sync`,`Content-Type: application/json`,`X-Internal-Token: <服务间令牌>`,Feign LB 直连不经网关。)
|
||||
|
||||
#### 响应示例
|
||||
|
||||
首次投递(TEST 实测:该供应商名下 1 个 ACTIVE 车队被停用,名下有在役车辆仍强制停用——供应商联动不做在役车辆守卫):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "8996843000000000001",
|
||||
"targetStatus": "SUSPENDED",
|
||||
"ignored": false,
|
||||
"duplicate": false,
|
||||
"disabledCount": 1,
|
||||
"disabledTeamIds": ["352696323818000384"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 目标状态非停止合作类(如恢复 ACTIVE):不联动、零写入,正常返回 `ignored=true`、`disabledCount=0`、`disabledTeamIds=[]`(恢复合作不自动启用是本单边界,车队需人工启用,安全侧)。
|
||||
- 供应商名下无 ACTIVE 车队(含窗口外重投、车队已被人工停用):CAS 守卫自然命中 0 行,返回 `ignored=false`、`disabledCount=0`、`disabledTeamIds=[]`,仍属成功回执,调用方不应告警。
|
||||
- 同 `eventId` 幂等窗口(24h)内重复投递:返回 `duplicate=true` 的成功回执(「已消费」语义),`disabledCount=0`、`disabledTeamIds=[]`(首次投递的实际停用明细不随幂等键留存,重复回执只承诺「已处理」);供应商侧「失败即重试告警」不应被触发。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "8996843000000000001",
|
||||
"targetStatus": "SUSPENDED",
|
||||
"ignored": false,
|
||||
"duplicate": true,
|
||||
"disabledCount": 0,
|
||||
"disabledTeamIds": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
缺少必填字段(Bean Validation,TEST 实测缺 `eventId`):
|
||||
|
||||
```json
|
||||
{ "code": 400, "message": "事件ID不能为空", "success": false, "data": null }
|
||||
```
|
||||
|
||||
`X-Internal-Token` 缺失或错误(InternalAuthFilter 拦截,原始响应为过滤器直出、字段名为 `msg`,TEST 实测):
|
||||
|
||||
```json
|
||||
{ "code": 403, "msg": "内部接口禁止外部访问" }
|
||||
```
|
||||
|
||||
经网关误调(`/internal/**` 无网关路由,TEST 实测 HTTP 200 传输层 + 业务码 404):
|
||||
|
||||
```json
|
||||
{ "code": 404, "message": "接口不存在: /internal/fleet-teams/supplier-status-sync", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 幂等双保险:① `@Idempotent` 按 `eventId` 开窗 24h(覆盖 MQ/Feign 延迟重投),窗口内重投由 Controller 转 `duplicate=true` 成功回执;② 窗口外重投由 Mapper CAS 守卫(仅停用 `status=ACTIVE` 行)兜底,自然命中 0 行。
|
||||
- 状态匹配大小写不敏感(防调用方传小写被静默忽略);回执原样回显调用方传值。
|
||||
- 与人工停用的差异:供应商联动是强制停用,**不做在役车辆守卫**;存量派单/槽位/占用不在联动范围。
|
||||
- 本端不落供应商状态镜像、不做乱序围栏——迟到旧事件依赖调用方按事件流顺序投递。
|
||||
- `supplierId`/`disabledTeamIds[]` 均按字符串序列化(`@JsonSerialize(ToStringSerializer)`),禁止转 JavaScript Number。
|
||||
|
||||
---
|
||||
|
||||
### 2. 查询派单候选资源 `POST /admin/fleet/assignments/candidates`
|
||||
|
||||
**VO**: `AssignmentCandidateReqVO` / `Result<AssignmentCandidateRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
派单 Step2 选车/选司机时查询候选资源。**本单行为变化**:司机候选排除「常驻车所属车队已停用」的司机(TEST 实测:司机斯琴常驻车挂到停用车队后,候选从 1 条变为 0 条;重新启用车队后恢复 1 条)。车辆侧排除自 #6717 起已存在(车队停用合并车辆可派状态),本单未改。
|
||||
|
||||
请求/响应**结构零字段变化**——排除是静默的,不返回被排除司机及其原因。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `startDate` | Body | String(日期) | 是 | `yyyy-MM-dd` | 用车开始日期 |
|
||||
| `endDate` | Body | String(日期) | 是 | `yyyy-MM-dd`,≥startDate | 用车结束日期 |
|
||||
| `orderId` | Body | String(Long)/null | 否 | - | 当前订单 ID;改派排除自身时必填 |
|
||||
| `requirementId` | Body | String(Long)/null | 否 | - | 当前用车需求 ID;改派排除自身时必填 |
|
||||
| `fleetItemIndex` | Body | Integer/null | 否 | ≥0 | 需求车型项序号 |
|
||||
| `vehicleKeyword` | Body | String/null | 否 | - | 车辆关键词(车牌/车型/常驻司机姓名或完整手机号) |
|
||||
| `driverKeyword` | Body | String/null | 否 | - | 司机关键词(姓名/完整手机号/常驻车牌) |
|
||||
| `fleetTeamId` | Body | String(Long)/null | 否 | - | 按车队过滤车辆候选(雪花字符串) |
|
||||
| `vehicleTypeId` | Body | String(Long)/null | 否 | - | 按车型大类过滤 |
|
||||
| `driverAvailability` | Body | String | 否 | `AVAILABLE`/`ALL`,默认 `ALL` | 司机可用性筛选 |
|
||||
| `selectedVehicleId` | Body | String(Long)/null | 否 | - | 已选车辆 ID(先选车后选司机场景) |
|
||||
| `selectedDriverId` | Body | String(Long)/null | 否 | - | 已选司机 ID(先选司机后选车场景) |
|
||||
| `excludeAssignmentId` | Body | String(Long)/null | 否 | 须属当前订单与需求 | 改派时排除的当前派单 ID |
|
||||
|
||||
#### 出参 `Result<AssignmentCandidateRespVO>`
|
||||
|
||||
顶层字段(与改前完全一致,无新增/删除):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `vehicles` | Object | 车辆候选分页(`records[]` + `total`);停用车队名下车辆继续被排除(#6717 既有口径) |
|
||||
| `drivers` | Object | 司机候选分页(`records[]` + `total`);**本单起追加排除常驻车所属车队已停用的司机** |
|
||||
| `fleetTeamFacets` | Array | 车队聚合筛选项 |
|
||||
| `vehicleTypeFacets` | Array | 车型大类聚合筛选项 |
|
||||
| `selectedDriverResidentVehicle` | Object/null | 已选司机的常驻车信息 |
|
||||
| `selectedDriverResidentVehicles` | Array | 已选司机的全部常驻车 |
|
||||
| `selectedVehicleResidentDriver` | Object/null | 已选车辆的常驻司机 |
|
||||
| `selectedRelation` | Object/null | 已选车+司机的常驻关系 |
|
||||
| `suggestedDriverId` | String(Long)/null | 自动代入司机 ID(雪花字符串) |
|
||||
| `suggestedDriverReason` | String | 自动代入原因码 |
|
||||
| `suggestedDriverMessage` | String | 自动代入原因展示文案 |
|
||||
| `canonicalSnapshot` | Object | Step2 canonical 快照(代际/版本/槽位集合) |
|
||||
|
||||
`drivers.records[]` 项关键字段(结构未变,列与联调相关者):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `driverId` | String | 司机 ID(雪花字符串,禁止转 Number) |
|
||||
| `name` | String | 司机姓名 |
|
||||
| `maskedPhone` | String | 司机手机(脱敏,如 `135****5001`) |
|
||||
| `driverStatus` | String | 占用态(`idle`/`busy`/`rest`/`pending`) |
|
||||
| `season` | String | 赛季态(候选恒 `active`,其余赛季已过滤) |
|
||||
| `residentVehicleId` | String/null | 常驻车辆 ID(常驻车所属车队停用时,该司机整体不出现在候选) |
|
||||
| `residentVehiclePlate` | String/null | 常驻车辆车牌 |
|
||||
| `available` | Boolean | 是否覆盖整个请求日期范围可用 |
|
||||
| `selectable` | Boolean | 本次选车场景下是否可被选中 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /admin/fleet/assignments/candidates
|
||||
Authorization: Bearer <admin-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"startDate": "2026-09-10",
|
||||
"endDate": "2026-09-12",
|
||||
"driverKeyword": "斯琴"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
司机常驻车所属车队**启用**时(TEST 实测基线,斯琴在候选内):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"drivers": {
|
||||
"total": 1,
|
||||
"records": [
|
||||
{
|
||||
"driverId": "2065272145289658370",
|
||||
"name": "斯琴",
|
||||
"maskedPhone": "135****5001",
|
||||
"years": 17,
|
||||
"driverStatus": "idle",
|
||||
"season": "active",
|
||||
"licenseType": "A3",
|
||||
"licenseExpired": false,
|
||||
"available": true,
|
||||
"selectable": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
车队停用后同一查询(TEST 实测:`records=[]`、`total=0`、`code=200`)——排除是静默的,响应结构不变、不附带被排除原因;前端按「无匹配司机」渲染即可,无需特殊处理。车辆候选同理(车队停用后名下车辆不再返回)。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"drivers": { "total": 0, "records": [] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
缺少必填日期(Bean Validation):
|
||||
|
||||
```json
|
||||
{ "code": 400, "message": "用车开始日期不能为空", "success": false, "data": null }
|
||||
```
|
||||
|
||||
未登录(网关拦截):
|
||||
|
||||
```json
|
||||
{ "code": 401, "message": "未登录", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 司机侧排除口径 = 常驻关系(`fleet_vehicle.primary_driver_id`)反查:司机任一常驻车所属车队为 DISABLED 即被排除,与司机列表「所属车队」筛选同源(同一反查端口,防口径漂移)。
|
||||
- **无常驻车的司机不受车队联动约束**(司机档案无车队列,本期不加列)。
|
||||
- 排除只读过滤,不写任何表;被排除司机在司机档案/列表页不受影响,仅派单候选场景不可见。
|
||||
- 车队长停用后重新启用,候选即时恢复(无缓存延迟,TEST 实测启用后同查询恢复 1 条)。
|
||||
|
||||
---
|
||||
|
||||
### 3. 派单预校验冲突 `POST /admin/fleet/assignments/precheck`
|
||||
|
||||
**VO**: `PrecheckReqVO` / `Result<PrecheckRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
Step2 选定车辆 + 司机后、保存前预探冲突。**本单变化**:`warnings[]` 新增 type `fleet_team_disabled`——车辆所属车队停用、或司机常驻车所属车队停用时给出预警(对齐创建/改派锁内 605074 硬校验,precheck 只提示不抛)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `vehicleId` | Body | String(Long) | 是 | 雪花字符串 | 车辆 ID |
|
||||
| `driverId` | Body | String(Long) | 是 | 雪花字符串 | 司机 ID |
|
||||
| `startDate` | Body | String(日期) | 是 | `yyyy-MM-dd` | 用车开始日期(闭区间起点) |
|
||||
| `endDate` | Body | String(日期) | 是 | `yyyy-MM-dd` | 用车结束日期(闭区间终点) |
|
||||
| `orderId` | Body | String(Long)/null | 否 | - | 订单 ID(仅日志/上下文) |
|
||||
| `pickupAt` | Body | String/null | 否 | - | 接客地(城市衔接判定用) |
|
||||
| `dropoffAt` | Body | String/null | 否 | - | 送客地(城市衔接判定用) |
|
||||
| `headcount` | Body | Integer/null | 否 | ≥0 | 人数(座位不足 warning 判定用) |
|
||||
| `excludeAssignmentId` | Body | String(Long)/null | 否 | - | 改派预校验排除自身 |
|
||||
|
||||
#### 出参 `Result<PrecheckRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `conflict` | Boolean | 是否存在阻断性冲突;资源不可用(含车队停用)时为 true,此时阻断原因在 `warnings[]` 体现、`conflicts[]` 可为空 |
|
||||
| `conflicts` | Array | 冲突明细(`type`/`conflictAssignmentId`/`conflictOrderNo`/`conflictDateRange`/`cityJunctionShareCandidate`/`msg`),无冲突为空数组 |
|
||||
| `warnings` | Array | 非阻断提示(`type` + `msg`);**本单新增 type 值 `fleet_team_disabled`** |
|
||||
|
||||
`warnings[]` 项字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `type` | String | 提示类型;新增 `fleet_team_disabled`=车队已停用(全集见 §六.5) |
|
||||
| `msg` | String | 提示文案,新 type 恒为「车队已停用不可派新单,提交派单将被拦截,请先启用车队或换车」 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /admin/fleet/assignments/precheck
|
||||
Authorization: Bearer <admin-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"vehicleId": "2094307514693648385",
|
||||
"driverId": "2065272145289658370",
|
||||
"startDate": "2026-09-10",
|
||||
"endDate": "2026-09-12"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
车队停用后(TEST 实测:车辆侧 `vehicle_unavailable` 与新增 `fleet_team_disabled` 同时出现,资源不可用使 `conflict=true` 但 `conflicts=[]`):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"conflict": true,
|
||||
"conflicts": [],
|
||||
"warnings": [
|
||||
{ "type": "vehicle_unavailable", "msg": "车辆处于维保或停用状态" },
|
||||
{ "type": "fleet_team_disabled", "msg": "车队已停用不可派新单,提交派单将被拦截,请先启用车队或换车" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无冲突无提示(车队启用基线,TEST 实测):`conflict=false`、`conflicts=[]`、`warnings=[]`、`code=200`。precheck 为只读咨询,恒 code=200 不抛业务异常;车辆/司机不存在时也不抛错,而是落 `vehicle_unavailable`/`driver_unavailable` warning + `vehicle_not_found`/`driver_not_found` conflict 明细(既有 #5629 口径)。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": { "conflict": false, "conflicts": [], "warnings": [] }
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
缺少必填车辆 ID(Bean Validation):
|
||||
|
||||
```json
|
||||
{ "code": 400, "message": "车辆 ID 不能为空", "success": false, "data": null }
|
||||
```
|
||||
|
||||
未登录(网关拦截):
|
||||
|
||||
```json
|
||||
{ "code": 401, "message": "未登录", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- precheck 只提示不阻断提交动作本身;最终一致以 create/change 锁内重校验(605074)为准——车务忽略预警强行提交会被 605074 拦截。
|
||||
- 车辆所属车队行缺失(历史数据无车队)时 `fleet_team_disabled` 不触发,仍走 `vehicle_unavailable` 既有口径。
|
||||
- 司机侧判定 = 常驻车所属车队停用;无常驻车司机不产生本 warning。
|
||||
- 前端若按 type 白名单渲染 warning,需把 `fleet_team_disabled` 加入映射(建议样式与 `vehicle_unavailable` 同级:醒目提示 + 引导换车/启用车队)。Swagger 注解中的 type 枚举列举未同步新值,以本文 §六.5 全集为准。
|
||||
|
||||
---
|
||||
|
||||
### 4. 批量创建派单 `POST /admin/fleet/assignments/batch`
|
||||
|
||||
**VO**: `BatchCreateAssignmentReqVO` / `Result<BatchAssignmentWriteRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
派单 Step2 提交(单派/批量同路径,经同一锁内创建逻辑)。**本单变化**:新增车队状态硬校验——所选车辆所属车队、或所选司机常驻车所属车队非 ACTIVE 时,写库前抛 **605074**,先于车辆/司机自身状态校验(605037/605038)执行,避免车队停用被误报成「车辆维保/停用」。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `orderId` | Body | String(Long) | 是 | 雪花字符串 | 订单 ID |
|
||||
| `requirementId` | Body | String(Long) | 是 | 雪花字符串 | 用车需求 ID |
|
||||
| `startDate` | Body | String(日期) | 是 | `yyyy-MM-dd` | 用车开始日期 |
|
||||
| `endDate` | Body | String(日期) | 是 | `yyyy-MM-dd` | 用车结束日期 |
|
||||
| `requestId` | Body | String | 是 | 非空,幂等键 | 批次幂等请求标识(重复提交同 requestId + 同载荷幂等放行) |
|
||||
| `items` | Body | Array | 是 | 每项含 `fleetItemIndex`/`vehicleId`/`driverId`(均必填) | 派车明细;`vehicleId`/`driverId` 为雪花字符串 |
|
||||
| `pickupAt` | Body | String/null | 否 | - | 接客地 |
|
||||
| `dropoffAt` | Body | String/null | 否 | - | 送客地 |
|
||||
| `headcount` | Body | Integer/null | 否 | ≥0 | 乘客人数(不含司机) |
|
||||
| `chargeableServiceDates` | Body | Array/null | 否 | - | 收取车费的服务日期 |
|
||||
| `confirmCrossResident` | Body | Boolean/null | 否 | - | 跨常驻车派单显式确认 |
|
||||
|
||||
#### 出参 `Result<BatchAssignmentWriteRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `assignments` | Array | 创建结果列表(`fleetItemIndex` + `assignment` 写入结果,含 `assignmentId` 雪花字符串) |
|
||||
| `failedFleetItemIndex` | Integer/null | 失败的需求项序号(整批事务回滚,任一失败全部不生效) |
|
||||
| `dailyDifferences` | Array/null | 逐日差异提示 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /admin/fleet/assignments/batch
|
||||
Authorization: Bearer <admin-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"orderId": "2087157006417657857",
|
||||
"requirementId": "2087157006845476865",
|
||||
"startDate": "2026-09-10",
|
||||
"endDate": "2026-09-12",
|
||||
"headcount": 4,
|
||||
"requestId": "web-step2-<uuid>",
|
||||
"items": [
|
||||
{ "fleetItemIndex": 0, "vehicleId": "2094307514693648385", "driverId": "2065272145289658370" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
成功(结构示例;本次 TEST 未跑通正向创建,原因见 §八):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"assignments": [
|
||||
{
|
||||
"fleetItemIndex": 0,
|
||||
"assignment": { "assignmentId": "2094310000000000001", "assignmentStatus": "assigned" }
|
||||
}
|
||||
],
|
||||
"failedFleetItemIndex": null,
|
||||
"dailyDifferences": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口为写接口,无空数据分支;整批任一 item 失败即同事务回滚,不产生半批状态。幂等重试(同 `requestId` + 同载荷)返回首个成功结果,不重复建单。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
车队已停用(新增;车辆所属车队或司机常驻车所属车队非 ACTIVE,写库前拦截、零写入):
|
||||
|
||||
```json
|
||||
{ "code": 605074, "message": "车队已停用不可派新单(请先启用车队或换车)", "success": false, "data": null }
|
||||
```
|
||||
|
||||
行程已结束的需求不可再派(既有口径,TEST 实测优先级高于 605074——过期需求先撞本码):
|
||||
|
||||
```json
|
||||
{ "code": 605047, "message": "行程已结束,派车信息只读,不能修改或改派", "success": false, "data": null }
|
||||
```
|
||||
|
||||
车辆自身维保/停用(既有;车队校验通过后才轮到本码):
|
||||
|
||||
```json
|
||||
{ "code": 605037, "message": "车辆处于维保或停用状态,不能派车", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 校验顺序(锁内):需求项占用守卫 → **车队状态(605074)** → 车辆状态(605037)→ 司机状态(605038)→ 司机赛季(605006/605013)→ 跨常驻确认 → 档期重叠。
|
||||
- 只拦「产生新占用」的创建入口;存量派单的最终确认/撤销取消不挂本校验——供应商事后停用不卡死在途单。
|
||||
- 车队行缺失(历史车辆无车队)不归 605074,仍由合并后车辆状态走 605037 老口径。
|
||||
- 幂等:同 `requestId` 重放返回首个结果;分布式锁串行化同需求写入。
|
||||
- 行程已结束(605047)、需求版本过期(605905)等前置守卫先于车队校验命中。
|
||||
|
||||
---
|
||||
|
||||
### 5. 修改派单(改派) `POST /admin/fleet/assignments/{assignmentId}/change`
|
||||
|
||||
**VO**: `ChangeAssignmentReqVO` / `Result<ChangeAssignmentRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
车务对在途派单换车/换司机/调整费用。**本单变化**:改派目标为停用车队资源时写库前抛 **605074**(TEST 实测:把在途派单改到停用车队的车辆+司机,返回 605074 且原派单零副作用)。纯费用调整(车人未换)不查司机常驻车队——在途单不因司机另一辆常驻车挂的车队被供应商事后停用而卡死费用修订。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `assignmentId` | Path | String(Long) | 是 | 雪花字符串 | 目标派单 ID |
|
||||
| `effectiveDate` | Body | String(日期) | 是 | 须在派单服务日期范围内 | 改派生效日期 |
|
||||
| `newVehicleId` | Body | String(Long)/null | 否 | 与 `newDriverId` 至少其一 | 新车辆 ID(雪花字符串) |
|
||||
| `newDriverId` | Body | String(Long)/null | 否 | 与 `newVehicleId` 至少其一 | 新司机 ID(雪花字符串) |
|
||||
| `reason` | Body | String | 是 | 非空 | 修改原因 |
|
||||
| `requestId` | Body | String | 是 | 非空,幂等键 | 请求幂等 ID |
|
||||
| `serviceDates` | Body | Array/null | 否 | - | 指定改派的服务日期子集 |
|
||||
| `vehicleFeeTotal` | Body | Number/null | 否 | ≥0 | 手工总车费(需搭配调整原因) |
|
||||
| `confirmCrossResident` | Body | Boolean/null | 否 | - | 跨常驻车派单显式确认 |
|
||||
|
||||
#### 出参 `Result<ChangeAssignmentRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `assignmentId` | String | 新派单 ID(雪花字符串) |
|
||||
| `assignmentSlotId` | String | 槽位 ID(雪花字符串) |
|
||||
| `previousAssignmentGroupId` | String/null | 原派车组 ID |
|
||||
| `newAssignmentGroupId` | String/null | 新派车组 ID |
|
||||
| `assignmentStatus` | String | 新派单状态 |
|
||||
| `effectiveDate` | String(日期) | 改派生效日期 |
|
||||
| `affectedDays` | Integer | 影响天数 |
|
||||
| `vehicleFeeTotal` | String(Number)/null | 最终总车费 |
|
||||
| `dailyVehicleFees` | Array/null | 逐日车费 |
|
||||
| `warningCode` | String/null | 预警码 |
|
||||
| `warningMessage` | String/null | 预警文案 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /admin/fleet/assignments/2091068072742756354/change
|
||||
Authorization: Bearer <admin-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"effectiveDate": "2026-09-02",
|
||||
"newVehicleId": "2094307514693648385",
|
||||
"newDriverId": "2065272145289658370",
|
||||
"reason": "车队停用前换车",
|
||||
"requestId": "web-change-<uuid>"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
成功(结构示例;本次 TEST 的改派请求按预期被 605074 拦截,未产生成功样本):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"assignmentId": "2094310000000000002",
|
||||
"assignmentSlotId": "2094310000000000003",
|
||||
"assignmentStatus": "assigned",
|
||||
"effectiveDate": "2026-09-02",
|
||||
"affectedDays": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口为写接口,无空数据分支;幂等重放(同 `requestId` + 同载荷)返回首个成功回执,不重复改派。改派目标派单自身已取消/已完成时不归本单变化范围,走既有 605066「派单已取消,无法修改,请重新派车」。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
改派目标资源所属车队已停用(新增,TEST 实测原文;写库前拦截、原派单零副作用):
|
||||
|
||||
```json
|
||||
{ "code": 605074, "message": "车队已停用不可派新单(请先启用车队或换车)", "success": false, "data": null }
|
||||
```
|
||||
|
||||
生效日期不在派单服务日期范围内(既有):
|
||||
|
||||
```json
|
||||
{ "code": 605028, "message": "生效日期不在派单服务日期范围内", "success": false, "data": null }
|
||||
```
|
||||
|
||||
派单已取消(既有):
|
||||
|
||||
```json
|
||||
{ "code": 605066, "message": "派单已取消,无法修改,请重新派车", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 车队校验先于 605037/605038 执行(与创建同口径);车辆侧车队停用必拦(含纯费用调整场景——该场景 #6717 起本就被合并状态 605037 拦截,此处只是把报错换成更准的 605074)。
|
||||
- 纯费用调整(`identityUnchanged`,车人未换)时**司机侧不查常驻车队**:在途单司机的另一辆常驻车被联动停用,不影响本单费用修订(「已派订单不受影响」边界)。
|
||||
- 换车/换司机(非纯费用调整)时司机常驻车队停用 → 605074。
|
||||
- 幂等:同 `requestId` 重放不重复改派;车锁/司机锁串行化。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
| 场景 | 调用 / 结果 |
|
||||
|---|---|
|
||||
| ✅ 供应商域投递停止合作事件 | `{"supplierId":"...","targetStatus":"SUSPENDED","eventId":"<全局唯一>","occurredAt":"..."}` + `X-Internal-Token` 头,Feign LB 直连 |
|
||||
| ✅ `targetStatus` 传小写 `suspended` | 大小写不敏感,正常触发联动停用 |
|
||||
| ✅ 同 `eventId` 网络重试重投 | 返 `duplicate=true` 成功回执,按已消费处理,**不要告警重试** |
|
||||
| ✅ 非停止合作态(如 ACTIVE 恢复) | 返 `ignored=true` 成功,不联动(恢复合作需人工启用车队) |
|
||||
| ✅ 前端收到 605074 | 按文案引导:启用车队(车队管理页)或更换车辆/司机后重试 |
|
||||
| ✅ precheck 渲染 `fleet_team_disabled` | 与 `vehicle_unavailable` 同级醒目提示;加入 type 白名单映射 |
|
||||
| ❌ 前端直接调 `/internal/fleet-teams/supplier-status-sync` | 网关不路由(业务码 404);该端点仅供服务间 Feign 调用 |
|
||||
| ❌ 调用方对 `duplicate=true` 回执告警重试 | 重复投递是「已消费」而非失败,告警重试会造成误报风暴 |
|
||||
| ❌ 复用 `eventId` 投递不同事件 | 幂等键冲突会让新事件被当成已消费吞掉;`eventId` 必须全局唯一(建议含 supplierId+状态+时间戳) |
|
||||
| ❌ 雪花 ID 转 JavaScript Number | `supplierId`/`disabledTeamIds[]`/`driverId`/`vehicleId`/`assignmentId` 均为雪花字符串,`Number()`/`parseInt()`/一元 `+` 会精度丢失 |
|
||||
| ❌ 车务忽略 precheck 预警强行提交 | 创建/改派锁内 605074 硬拦截,零写入 |
|
||||
|
||||
### 关键提示(当前 TEST 构建)
|
||||
|
||||
- 605074 与 605037 的分工:605074 专指「车队级」停用(多因供应商 SUSPENDED/BLACKLIST 联动);605037 指车辆自身维保/停用。前端错误码字典两个都要保留。
|
||||
- 候选排除是静默的:前端不需要也不可能在候选接口里展示「被排除的司机」;如业务需要解释「为什么某司机不可派」,引导至司机档案查看常驻车所属车队状态。
|
||||
- 车队重新启用后(车队管理 → 启用,前提是已绑供应商),候选/预校验/创建/改派即时恢复,无缓存延迟。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- **internal 联动端点只写 `fleet_team` 行**:CAS 批量 update(`status` ACTIVE→DISABLED,`where` 带 `status=ACTIVE` 守卫),重复/并发自然命中 0 行;不跨服务写 `supplier_main`/`supplier_resource_rel`。
|
||||
- **幂等键在 Redis**:`fleet:team:supplier-status-sync:{eventId}`,TTL 24h(@Idempotent 开窗),窗口内重投不再触库。
|
||||
- **派单侧零表结构变更**:候选排除 = 查询过滤(只读);创建/改派 605074 = 锁内读校验(只读),不写任何额外表。
|
||||
- **本工单无 Flyway 迁移**:不加表、不加列(车队-供应商关联列由 #6717 迁移建好)。
|
||||
- 联动停用不影响存量 `fleet_assignment` 行(本单边界:不动在途派单/槽位/占用)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录/登录失效:业务码 `401`(网关拦截)。
|
||||
- internal 端点 `X-Internal-Token` 缺失/错误:`403`(过滤器直出,字段名 `msg`)。
|
||||
- 经网关误调 `/internal/**`:业务码 `404`「接口不存在」(无路由,符合预期)。
|
||||
- Bean Validation 失败:`400`(如缺 `eventId`/`vehicleId`/`startDate`)。
|
||||
- 车队已停用不可派新单:`605074`(新增;创建/改派写库前硬拦截)。
|
||||
- 车辆维保/停用:`605037`(既有;车队校验通过后才会命中)。
|
||||
- 司机休假/待激活:`605038`(既有;同上)。
|
||||
- 行程已结束只读:`605047`(既有;过期需求/派单先于 605074 命中,TEST 实测)。
|
||||
- 派单已取消不可改派:`605066`(既有)。
|
||||
- 生效日期越出派单服务日期:`605028`(既有)。
|
||||
- 车队已停用仍选该车(车队管理写口径):`601102`(既有,本单不改)。
|
||||
- 跨服务 Feign 不可用:本单 fleet 侧链路不新增 Feign 调用(联动方向是 resource → fleet);候选/预校验/创建/改派均为库内读写,不受 resource 侧可用性影响。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### `targetStatus`(供应商目标状态,internal 端点入参)
|
||||
|
||||
**所属字段**: `SupplierStatusSyncReqVO.targetStatus` | **类型**: `String`(大小写不敏感)
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `SUSPENDED` | 暂停合作 | **触发**联动停用名下在启车队 |
|
||||
| `BLACKLIST` | 拉黑 | **触发**联动停用名下在启车队 |
|
||||
| `DRAFT`/`VETTING`/`ACTIVE`/`FROZEN`/`ARCHIVED` | 草稿/审核中/合作中/冻结/归档 | 忽略(`ignored=true`),不联动 |
|
||||
|
||||
### `warnings[].type`(precheck 非阻断提示类型)
|
||||
|
||||
**所属字段**: `PrecheckRespVO.warnings[].type` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `seats_short` | 座位不足 | 既有 |
|
||||
| `vehicle_unavailable` | 车不可用 | 既有(车辆不存在/维保/停用;车队停用合并车辆状态后同现) |
|
||||
| `driver_unavailable` | 司机不可用 | 既有 |
|
||||
| `driver_blacklisted` | 司机已黑名单 | 既有(提交将被 605006 拦截) |
|
||||
| `driver_season_not_registered` | 司机非在册赛季 | 既有(提交将被 605013 拦截) |
|
||||
| **`fleet_team_disabled`** | **车队已停用** | **本单新增**(提交将被 605074 拦截,引导启用车队或换车) |
|
||||
| `cross_resident` | 跨常驻 | 既有 |
|
||||
| `license_expired` | 驾照过期 | 既有 |
|
||||
| `veh_inspect_expired` | 车辆年检过期 | 既有 |
|
||||
| `veh_insure_expired` | 车辆保险过期 | 既有 |
|
||||
|
||||
### 车队状态(`fleet_team.status`)
|
||||
|
||||
**所属字段**: 车队管理相关响应 `status` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `ACTIVE` | 启用 | 名下资源可派;前提:已关联供应商(#6717/#6811 不变量) |
|
||||
| `DISABLED` | 停用 | 名下车辆/常驻司机不可派新单(本单贯通到派单全链路) |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段/枚举级对比
|
||||
|
||||
| 项 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `POST /internal/fleet-teams/supplier-status-sync` | 不存在 | 新增 internal 端点(ReqVO 4 字段 / RespVO 6 字段) |
|
||||
| `PrecheckRespVO.warnings[].type` 值域 | 9 种既有 type | 新增 `fleet_team_disabled` |
|
||||
| 派单错误码 | 无车队级码(车队停用被合并状态误报 605037) | 新增 605074「车队已停用不可派新单(请先启用车队或换车)」 |
|
||||
| `candidates` 请求/响应字段 | 现有结构 | 零字段变化(行为变化:司机候选集合缩小) |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 供应商暂停/拉黑 | 车队无感知,名下资源照常可派 | 联动停用名下在启车队(强制,不做在役车辆守卫) |
|
||||
| 司机候选(常驻车挂停用车队) | 照常返回 | 静默排除 |
|
||||
| precheck 车队停用提示 | 仅 `vehicle_unavailable`(车辆侧) | 追加 `fleet_team_disabled`(车+司机两侧) |
|
||||
| 创建/改派选到停用车队资源 | 报 605037「车辆维保/停用」(语义不准);司机侧可漏过 | 统一报 605074(先于 605037/605038) |
|
||||
| 在途单纯费用调整(司机另一常驻车车队被停用) | 司机侧无校验 | 仍不校验(刻意豁免,不卡死在途单) |
|
||||
| 供应商恢复合作 | - | 不自动启用车队(人工启用,安全侧) |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否。响应结构无字段删改;候选集合缩小属业务收口;新增 warning type 与错误码对旧前端为未知值——旧前端按通用兜底渲染即可,不崩。
|
||||
- **前端是否必须同步上线**: 建议同批但不强阻断。两项适配:① precheck warning type 白名单加 `fleet_team_disabled`(不加则该预警被静默丢弃,车务要到提交时才知道被拦);② 错误码字典加 605074 文案与引导动作(不加则按通用错误提示展示)。
|
||||
- **前端 workaround 清理点**: 无(此前无对应前端绕行逻辑)。
|
||||
- **服务间依赖**: resource 侧 #6844 未上线前,internal 端点无生产流量,派单拦截仅对人工停用/无供应商自动停用的车队生效——同样符合预期。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 派单候选查询 / 派单预校验 / 派单创建(批量,单派同路径)/ 派单改派 4 个管理后台端点 + 1 个 internal 端点(fleet 侧)。
|
||||
- **零影响**:
|
||||
- 团批组派(GroupDispatch 独立体系,不写 `fleet_assignment`,PR 明示不在拦截范围)。
|
||||
- 存量派单的最终确认 / 撤销取消 / 行程短信 / 软清等推进类写口(不挂车队状态新校验,供应商事后停用不卡死在途单)。
|
||||
- 车队管理 CRUD 端点(`/admin/fleet/teams/**` 本单未改;启停用/供应商守卫口径仍属 #6717/#6811)。
|
||||
- 司机档案 / 车辆档案 / 司机列表「所属车队」筛选(排除逻辑复用同一反查端口,档案域零改动感知)。
|
||||
- 供应商域九大资源模块与供应商管理端点(联动发出端 #6844 另行落地)。
|
||||
- 小程序端(派单链路为管理后台车务功能)。
|
||||
- Gateway 路由(`/admin/fleet/assignments/**` 通配已覆盖;`/internal/**` 本就不经网关,无需新增配置)。
|
||||
- Redis/MQ(仅新增幂等键 `fleet:team:supplier-status-sync:{eventId}`,无新消息)。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
真实 TEST 环境实测(2026-08-31,网关 `https://api.test.1814.love:9443`,admin token 走 `/admin/auth/login`;internal 端点经 SSH 在测试服本机 curl `127.0.0.1:8087` 直连,模拟 Feign 调用方):
|
||||
|
||||
```
|
||||
[internal 链路]
|
||||
POST /internal/fleet-teams/supplier-status-sync SUSPENDED 首投
|
||||
→ 200, ignored=false, duplicate=false, disabledCount=1, disabledTeamIds=["352696323818000384"] ✓
|
||||
POST /internal/fleet-teams/supplier-status-sync 同 eventId 重投(幂等)
|
||||
→ 200, duplicate=true, disabledCount=0, disabledTeamIds=[] ✓
|
||||
POST /internal/fleet-teams/supplier-status-sync targetStatus=ACTIVE(非停合作态)
|
||||
→ 200, ignored=true, disabledCount=0 ✓
|
||||
POST /internal/fleet-teams/supplier-status-sync 缺 eventId
|
||||
→ 400「事件ID不能为空」 ✓
|
||||
POST /internal/fleet-teams/supplier-status-sync 错误 X-Internal-Token
|
||||
→ 403「内部接口禁止外部访问」 ✓
|
||||
POST 网关 /internal/fleet-teams/supplier-status-sync
|
||||
→ 业务码 404「接口不存在」(/internal 不经网关路由,符合预期) ✓
|
||||
|
||||
[管理后台链路(admin token)]
|
||||
GET /admin/fleet/teams/352696323818000384
|
||||
→ status=DISABLED(联动生效,全栈贯通) ✓
|
||||
POST /admin/fleet/assignments/candidates driverKeyword=斯琴
|
||||
→ 停用前 records=1 条 → 联动停用后 records=[] total=0(新增排除生效) ✓
|
||||
POST /admin/fleet/assignments/candidates vehicleKeyword=T6843
|
||||
→ records=[](车辆侧 #6717 既有排除仍生效) ✓
|
||||
POST /admin/fleet/assignments/precheck 停用车队车辆+常驻司机
|
||||
→ warnings 含 {"type":"fleet_team_disabled","msg":"车队已停用不可派新单,提交派单将被拦截,请先启用车队或换车"} ✓
|
||||
POST /admin/fleet/assignments/2091068072742756354/change 改派到停用车队车+司机
|
||||
→ 605074「车队已停用不可派新单(请先启用车队或换车)」,原派单零副作用(事后核 update_time 未变) ✓
|
||||
POST /admin/fleet/assignments/batch 停用车队车+司机(过期行程占位)
|
||||
→ 605047「行程已结束」优先命中(前置守卫先于 605074,符合校验顺序) ✓
|
||||
POST /admin/fleet/teams/352696323818000384/enable 重新启用
|
||||
→ 200;同条件 candidates 斯琴恢复 1 条(恢复路径即时生效) ✓
|
||||
```
|
||||
|
||||
- 部署:Deploy Panel 任务 `a7300401`(hl-fleet-service,2026-08-31 14:02 success,8087/8187 双实例滚动均 UP),部署 dev-v3 HEAD `2543febee`(含本 PR merge commit `df78cce550b4f4ea6a9f40352c1dd176b8d8fba6`,2026-08-31 13:50 合入)。
|
||||
- **batch 创建 605074 正向链未能在 TEST 实跑**:测试服全部待派(unassigned)占位的行程均已结束,创建请求先撞 605047;构造可派需求需完整订单+需求展开链,超出本次验证范围。该路径由单测覆盖(`AssignmentServiceTest#create_fleetTeamDisabledVehicle_throws605074` / `create_driverOnDisabledTeam_throws605074`,与已实测的改派路径 `change_fleetTeamDisabledVehicle_throws605074` 共用同一锁内校验方法),建议 mmg 联调时在真实新订单上补验一次。
|
||||
- 前置态构造说明:「停用车队 + 在役车辆/常驻司机」状态被人工停用守卫(601103)与车队选择守卫(601102)封锁,只有供应商联动能合法产生——实测经 internal 端点真实产生(正是本端点设计语义);测试车队的「ACTIVE + 已绑供应商」前置态由 DB 直改构造(测试服 fixture)。
|
||||
- 本地自动化(PR 自报):目标单测 730 个全绿(AssignmentServiceTest 522 / FleetTeamServiceTest 36 / DriverServiceTest 157 / 新增 Mapper+Controller 测试);全量 `mvn -pl hl-fleet-service test` 3913 tests 0 失败(含 IT);`FleetRedLineArchTest` 13/13 绿;spotless:check 通过。
|
||||
- 数据清理:测试车队(352696323818000384)、测试车辆(2094307514693648385,蒙A-T6843)均已删除(DB 复核无残留);测试用模拟供应商 ID(8996843000000000001)未在 `supplier_main` 落库,随车队删除清除;司机斯琴档案未改动(常驻绑定随车辆删除解除);被改派实测的真实派单(2091068072742756354)经核零副作用;admin token 已登出。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| #6868 | #6843 | 供应商暂停合作联动停用车队 + 派单拦截停用车队司机车辆(本次) | ✅ 最新 |
|
||||
| #6753 | #6717 | 车队关联供应商并展示供应商全名(车队-供应商关联与车辆侧排除的前置) | ✅ 已被本单扩展 |
|
||||
| #6827 | #6811 | 车队保存无供应商自动停用 + 601112 在役车辆守卫(车队停用来源之二) | ✅ 并存 |
|
||||
| - | #6844 | 供应商侧通知发出端(resource → fleet 联动调用方),待指派实现 | ⏳ 未开始 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#6843](https://git.1814.love:8443/wx/HL/issues/6843)
|
||||
- 关联 PR: [wx/HL#6868](https://git.1814.love:8443/wx/HL/pulls/6868)
|
||||
- 前置契约: `changelogs-v2/2026-08/30_6717_车队关联供应商并展示供应商全名-修改接口-管理后台.md`
|
||||
- 前置契约: `changelogs-v2/2026-08/31_6811_车队保存无供应商时自动停用与在役车辆守卫-修改接口-管理后台.md`
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 前端先摘除对 605074 与 `fleet_team_disabled` 的特化渲染(按通用兜底展示),保持线上可用。
|
||||
2. 从最新 `dev-v3` 建独立回退分支,回退 PR #6868 的合并 commit(`df78cce5`),验证后经独立 PR 合入。
|
||||
3. ⚠️ 已被联动停用的车队**不会**因回退自动恢复:回退代码只撤联动与拦截逻辑,已落 DISABLED 的车队需在车队管理人工启用(启用前置 = 已绑供应商)。
|
||||
4. 仅需止血时也可不回退整单:internal 端点无生产流量前(#6844 未上线)本单对线上唯一可感知影响是人工停用/无供应商车队在派单侧的拦截收口,属预期行为。
|
||||
5. 使用 Deploy Panel 滚动部署 `hl-fleet-service`;本单无数据库迁移,无需回滚脚本。
|
||||
6. 撤回后经 Gateway 验证:候选恢复返回停用车队常驻司机、precheck 不再出现 `fleet_team_disabled`、改派停用车队资源回报 605037 老口径。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#6843](https://git.1814.love:8443/wx/HL/issues/6843)
|
||||
- **PR**: [#6868](https://git.1814.love:8443/wx/HL/pulls/6868)
|
||||
- **Merge commit**: `df78cce550b4f4ea6a9f40352c1dd176b8d8fba6`
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
- **前端联动**: @mmg(① precheck `warnings[]` type 白名单加 `fleet_team_disabled`;② 错误码字典加 605074「车队已停用不可派新单(请先启用车队或换车)」+ 引导动作;③ 司机候选静默排除无前端适配量)
|
||||
- **服务间联动**: resource 侧 #6844 实现方按本文 §三.1 契约投递(supplierId/targetStatus/eventId/occurredAt + `X-Internal-Token`,重复投递认 `duplicate=true` 成功回执)
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "frontend-fleet-team-sort-validation"
|
||||
title: "车队编辑弹窗「排序」已填值仍提示「请输入排序」,无法保存"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "前端缺陷"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "e2b2482e"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-08-31"
|
||||
status_note: "纯前端校验缺陷,前端已修复。rules.sortOrder 的 required 规则补 type:'number',number 值(含 0)不再误判为空,空值仍正确提示必填。"
|
||||
updated_at: "2026-08-31"
|
||||
base: "dev-v3"
|
||||
generated: "2026-08-31T01:43:00+08:00"
|
||||
---
|
||||
|
||||
# 车队编辑弹窗「排序」已填值仍提示「请输入排序」,无法保存
|
||||
|
||||
> 前端缺陷,待前端修复。后端契约无变化。
|
||||
|
||||
## 现象(TEST 实测)
|
||||
|
||||
车务 → 车队管理 → 编辑车队(如「合作车队A」):
|
||||
|
||||
- 「排序」输入框中已显示值 `1`;
|
||||
- 点击「保存」时表单项仍报红色校验错误「请输入排序」,保存被拦截,无法提交。
|
||||
|
||||
## 只读前端定位(hl-ui 未修改)
|
||||
|
||||
`src/views/fleet/teams/index.vue`:
|
||||
|
||||
- 表单绑定:`NInputNumber v-model:value="formData.sortOrder"`,编辑时 `fillForm` 正确回填 `row?.sortOrder ?? null`,输入框显示的 `1` 说明 model 已有值;
|
||||
- 校验规则(约 391 行):
|
||||
|
||||
```js
|
||||
sortOrder: [{ required: true, message: '请输入排序', trigger: ['blur', 'change'] }],
|
||||
```
|
||||
|
||||
规则缺少 `type: 'number'`。Naive UI 底层 async-validator 的必填规则默认按 string 类型校验,对 number 类型的值会误判为空,导致「值已填但 required 校验不通过」。同页面其他字段均为字符串/枚举值所以未暴露此问题。
|
||||
|
||||
## 修复建议
|
||||
|
||||
规则补充 `type: 'number'`,例如:
|
||||
|
||||
```js
|
||||
sortOrder: [{ required: true, type: 'number', message: '请输入排序', trigger: ['blur', 'change'] }],
|
||||
```
|
||||
|
||||
同时建议回归以下场景:
|
||||
|
||||
1. 编辑已有车队(详情回填 sortOrder)直接保存,不再误报;
|
||||
2. 新建车队填写排序后保存正常;
|
||||
3. 排序为空(清空输入框)时仍正确提示「请输入排序」;
|
||||
4. 排序为 `0` 时可通过校验(`min: 0`,0 是合法值,不得被误判为空)。
|
||||
|
||||
## 后端契约(无变化,供联调核对)
|
||||
|
||||
- 详情:`GET /fleet/teams/{fleetTeamId}`,响应含 `sortOrder`(整数);
|
||||
- 更新:`PUT /fleet/teams/{fleetTeamId}`,请求体 `sortOrder` 为整数;
|
||||
- 新增:`POST /fleet/teams`,请求体 `sortOrder` 为整数。
|
||||
|
||||
本条目不引入后端代码、接口、数据库、配置、Redis 或 MQ 变更。
|
||||
|
||||
## 复现路径
|
||||
|
||||
车务(fleet 管理后台)→ 车队管理 → 任一有排序值的车队 → 编辑 → 直接点「保存」。
|
||||
|
||||
## 撤回
|
||||
|
||||
如需撤回本联调通知,只需回退本 Changelog 文件;后端契约和运行数据不受影响。
|
||||
@@ -0,0 +1,748 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6903"
|
||||
title: "团期看板六芯片逐户明细 GB-ADM-090~095"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: "2026-09-01"
|
||||
status_note: "2026-09-01 部署测试服 dev-v3@1159c8949,6 端点网关实测 200 全通过,团期不存在 589500 / 参数错误 400 已核验。前端评估(2026-09-01):团期看板 src/views/order-v2/batch 当前为纯本地 mock 阶段(period 用 mock id,全仓无真实 group-batch 调用),chips 端点需真实雪花 groupBatchId,下钻暂无真实落脚页。决策:先接真团期看板(需 GB-ADM-001 看板列表契约+真实 groupBatchId,不在本单)再接六芯片下钻,下钻形态已定=弹层明细。待看板列表契约后接入,跟踪见 hl-admin 任务 #104。"
|
||||
updated_at: "2026-09-01"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期看板:六芯片逐户明细(GB-ADM-090~095,房/车/导/摄/约/保)
|
||||
|
||||
> **存放目录**: 二期(order-v3)→ `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3
|
||||
> **Issue**: [#6903](https://git.1814.love:8443/wx/HL/issues/6903)
|
||||
> **日期**: 2026-09-01
|
||||
> **影响范围**: 管理后台团期看板行右侧六芯片(配房/配车/配导游/配摄影/合同/保险)点击后的逐户下钻
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**新增能力,无破坏性变化**:先前看板芯片只有整团聚合色块(GB-ADM-001 `chips.X`)、没有逐户下钻;本次把「点芯片看每户到哪一步」补成可调用接口。数据(order_main 六态列)早已落库并被房务/车队/地接等 Service 消费,本组接口只读透出,不改变任何业务状态。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景(选填)
|
||||
|
||||
团期看板行右侧有「房/车/导/摄/约/保」六个芯片,点击后按需展开该项的**逐户明细**。逐户口径与整团聚合、看板芯片(GB-ADM-001 `chips.X`)同源同算法(共享地基 `GroupBatchChipResolver`,#6902/#6916)。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | GB-ADM-090 配房逐户明细 | GET | `/v3/admin/order/group-batch/{groupBatchId}/chips/hotel` | 新增接口 | 房芯片逐户下钻 |
|
||||
| 2 | GB-ADM-091 配车逐户明细 | GET | `/v3/admin/order/group-batch/{groupBatchId}/chips/vehicle` | 新增接口 | 车芯片逐户下钻 |
|
||||
| 3 | GB-ADM-092 配导游逐户明细 | GET | `/v3/admin/order/group-batch/{groupBatchId}/chips/guide` | 新增接口 | 导芯片逐户下钻 |
|
||||
| 4 | GB-ADM-093 配摄影逐户明细 | GET | `/v3/admin/order/group-batch/{groupBatchId}/chips/photo` | 新增接口 | 摄芯片逐户下钻 |
|
||||
| 5 | GB-ADM-094 合同逐户明细 | GET | `/v3/admin/order/group-batch/{groupBatchId}/chips/contract` | 新增接口 | 约芯片逐户下钻 |
|
||||
| 6 | GB-ADM-095 保险逐户明细 | GET | `/v3/admin/order/group-batch/{groupBatchId}/chips/insurance` | 新增接口 | 保芯片逐户下钻 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
六个接口共用 `GroupBatchChipDetailVO` / `GroupBatchChipItemRespVO`,正文各自自包含。
|
||||
|
||||
### 1. GB-ADM-090 配房逐户明细 `GET /v3/admin/order/group-batch/{groupBatchId}/chips/hotel`
|
||||
|
||||
**VO**: `GroupBatchChipDetailVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期看板行右侧「房」芯片点击展开:返回该团期每户的配房进度(哪一户到哪一步),前端展开列表展示。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| X-Admin-Id | Header | Long | ✅ | 网关注入 | 受信管理员 ID;客户端传值一律忽略(不校验客户端值) |
|
||||
| groupBatchId | Path | String | ✅ | 团期 ID(Snowflake) | 不存在/非团期/已软删 → 589500 |
|
||||
|
||||
无查询参数、无请求体。
|
||||
|
||||
#### 出参 `Result<GroupBatchChipDetailVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| batchId | String | 团期 ID |
|
||||
| chipLabel | String | 固定「配房」 |
|
||||
| aggregateStatus | String | 整团聚合态 四态取值与看板 GB-ADM-001 `chips.X` 的 `aggregateStatus` 完全一致(整团待办/进行中/已完成/异常),与 GB-ADM-001 `chips.hotel` 同源同算法 |
|
||||
| totalCount | Integer | 计入统计的子订单数(免闸户不计入分母) |
|
||||
| doneCount | Integer | 已完成户数(`status==DONE`) |
|
||||
| items[].orderId | String | 子订单 ID(JSON String 化) |
|
||||
| items[].orderNo | String | 子订单编号 |
|
||||
| items[].contactName | String | 联系人/客户姓名 |
|
||||
| items[].peopleCount | Integer | 本户人数 = adult+child+youngChild+baby |
|
||||
| items[].status | String | 本户配房状态,`RequirementStatus` 6 值,见「六.5」;无需/未开始为 null |
|
||||
| items[].statusText | String | 状态中文名(服务端给出,见「六.5」) |
|
||||
| items[].needsIt | Boolean | 本户是否需要配房(`order_main.needs_hotel`);false=免闸户,status=null、置灰、不计入计数 |
|
||||
| items[].updateTime | String | 配房最后变更时间;六状态列无独立时间戳时为 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/90211/chips/hotel
|
||||
Authorization: Bearer ****
|
||||
X-Admin-Id: 3301
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"batchId": "90211",
|
||||
"chipLabel": "配房",
|
||||
"aggregateStatus": "DOING",
|
||||
"totalCount": 7,
|
||||
"doneCount": 5,
|
||||
"items": [
|
||||
{ "orderId": "770152", "orderNo": "GT-26-0096", "contactName": "陈昊",
|
||||
"peopleCount": 2, "status": "DONE", "statusText": "配房完成",
|
||||
"needsIt": true, "updateTime": null },
|
||||
{ "orderId": "770153", "orderNo": "GT-26-0097", "contactName": "林婉清",
|
||||
"peopleCount": 3, "status": null, "statusText": "无需",
|
||||
"needsIt": false, "updateTime": null }
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "batchId": "90211", "chipLabel": "配房",
|
||||
"aggregateStatus": "整团待办", "totalCount": 0, "doneCount": 0, "items": [] },
|
||||
"success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 589500, "message": "团期不存在", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 授权码 `group-batch:view`;未配置权限 → 589507。
|
||||
- 只读(READ_ONLY 事务),无锁、无幂等、不改变任何状态。
|
||||
- 免闸户(`needsIt=false`):status=null、statusText=「无需」、前端置灰、不计入 totalCount/doneCount 分子分母。
|
||||
- 已取消子订单不计入(活跃口径与共享地基一致)。
|
||||
- 团期已流团(CANCELLED)→ aggregateStatus 恒整团待办(wire 值同看板 chips.X),但逐户明细仍如实返回(不造假计数)。
|
||||
|
||||
---
|
||||
|
||||
### 2. GB-ADM-091 配车逐户明细 `GET /v3/admin/order/group-batch/{groupBatchId}/chips/vehicle`
|
||||
|
||||
**VO**: `GroupBatchChipDetailVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期看板行右侧「车」芯片点击展开:返回每户配车进度。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| X-Admin-Id | Header | Long | ✅ | 网关注入 | 受信管理员 ID;客户端传值忽略 |
|
||||
| groupBatchId | Path | String | ✅ | 团期 ID(Snowflake) | 不存在/非团期/已软删 → 589500 |
|
||||
|
||||
无查询参数、无请求体。
|
||||
|
||||
#### 出参 `Result<GroupBatchChipDetailVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| batchId | String | 团期 ID |
|
||||
| chipLabel | String | 固定「配车」 |
|
||||
| aggregateStatus | String | 整团聚合态 四态取值与看板 GB-ADM-001 `chips.X` 的 `aggregateStatus` 完全一致(整团待办/进行中/已完成/异常),与 GB-ADM-001 `chips.vehicle` 同源 |
|
||||
| totalCount | Integer | 计入统计的子订单数(免闸户不计入) |
|
||||
| doneCount | Integer | 已完成户数(`status==DONE`) |
|
||||
| items[].orderId | String | 子订单 ID(JSON String 化) |
|
||||
| items[].orderNo | String | 子订单编号 |
|
||||
| items[].contactName | String | 联系人/客户姓名 |
|
||||
| items[].peopleCount | Integer | 本户人数 = adult+child+youngChild+baby |
|
||||
| items[].status | String | 本户配车状态,`RequirementStatus` 6 值,见「六.5」;无需/未开始为 null |
|
||||
| items[].statusText | String | 状态中文名(服务端给出,与房同文案,见「六.5」) |
|
||||
| items[].needsIt | Boolean | 本户是否需要配车(`order_main.needs_vehicle`);false=免闸户置灰不计入 |
|
||||
| items[].updateTime | String | 配车最后变更时间;无独立时间戳时为 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/90211/chips/vehicle
|
||||
Authorization: Bearer ****
|
||||
X-Admin-Id: 3301
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"batchId": "90211",
|
||||
"chipLabel": "配车",
|
||||
"aggregateStatus": "DONE",
|
||||
"totalCount": 3,
|
||||
"doneCount": 3,
|
||||
"items": [
|
||||
{ "orderId": "770160", "orderNo": "GT-26-0101", "contactName": "王强",
|
||||
"peopleCount": 2, "status": "DONE", "statusText": "配房完成",
|
||||
"needsIt": true, "updateTime": null }
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "batchId": "90211", "chipLabel": "配车",
|
||||
"aggregateStatus": "整团待办", "totalCount": 0, "doneCount": 0, "items": [] },
|
||||
"success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 589500, "message": "团期不存在", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 授权码 `group-batch:view`;未配置权限 → 589507。
|
||||
- 只读(READ_ONLY 事务),无锁、无幂等、不改变任何状态。
|
||||
- 免闸户(`needsIt=false`):status=null、statusText=「无需」、前端置灰、不计入计数。
|
||||
- 已取消子订单不计入;流团团期聚合恒 整团待办、明细如实返回。
|
||||
|
||||
---
|
||||
|
||||
### 3. GB-ADM-092 配导游逐户明细 `GET /v3/admin/order/group-batch/{groupBatchId}/chips/guide`
|
||||
|
||||
**VO**: `GroupBatchChipDetailVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期看板行右侧「导」芯片点击展开:返回每户配导游进度。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| X-Admin-Id | Header | Long | ✅ | 网关注入 | 受信管理员 ID;客户端传值忽略 |
|
||||
| groupBatchId | Path | String | ✅ | 团期 ID(Snowflake) | 不存在/非团期/已软删 → 589500 |
|
||||
|
||||
无查询参数、无请求体。
|
||||
|
||||
#### 出参 `Result<GroupBatchChipDetailVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| batchId | String | 团期 ID |
|
||||
| chipLabel | String | 固定「配导游」 |
|
||||
| aggregateStatus | String | 整团聚合态 四态取值与看板 GB-ADM-001 `chips.X` 的 `aggregateStatus` 完全一致(整团待办/进行中/已完成/异常) |
|
||||
| totalCount | Integer | 计入统计的子订单数(免闸户不计入) |
|
||||
| doneCount | Integer | 已完成户数(`status==DONE`) |
|
||||
| items[].orderId | String | 子订单 ID(JSON String 化) |
|
||||
| items[].orderNo | String | 子订单编号 |
|
||||
| items[].contactName | String | 联系人/客户姓名 |
|
||||
| items[].peopleCount | Integer | 本户人数 = adult+child+youngChild+baby |
|
||||
| items[].status | String | 本户配导游状态,3 值:`NONE` 无需/未开始 / `PENDING` 待指派 / `DONE` 已指派,见「六.5」 |
|
||||
| items[].statusText | String | 状态中文名(服务端给出,见「六.5」) |
|
||||
| items[].needsIt | Boolean | 本户是否需要配导游(`order_main.needs_guide`);false=免闸户 status 恒 `NONE`、不计入计数 |
|
||||
| items[].updateTime | String | 配导游最后变更时间;无独立时间戳时为 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/90211/chips/guide
|
||||
Authorization: Bearer ****
|
||||
X-Admin-Id: 3301
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"batchId": "90211",
|
||||
"chipLabel": "配导游",
|
||||
"aggregateStatus": "DOING",
|
||||
"totalCount": 4,
|
||||
"doneCount": 2,
|
||||
"items": [
|
||||
{ "orderId": "770170", "orderNo": "GT-26-0102", "contactName": "周磊",
|
||||
"peopleCount": 2, "status": "DONE", "statusText": "已指派", "needsIt": true, "updateTime": null },
|
||||
{ "orderId": "770171", "orderNo": "GT-26-0103", "contactName": "吴芳",
|
||||
"peopleCount": 1, "status": "NONE", "statusText": "无需", "needsIt": false, "updateTime": null }
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "batchId": "90211", "chipLabel": "配导游",
|
||||
"aggregateStatus": "整团待办", "totalCount": 0, "doneCount": 0, "items": [] },
|
||||
"success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 589500, "message": "团期不存在", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 授权码 `group-batch:view`;未配置权限 → 589507。
|
||||
- 只读(READ_ONLY 事务),无锁、无幂等、不改变任何状态。
|
||||
- **免闸户(needsIt=false)status 为 `NONE`(非 null)**,与房/车(null)区分;前端按 NONE 置灰。
|
||||
- 读侧只认 `DONE`,其余非免闸状态一律 `PENDING`;已取消子订单不计入;流团团期聚合恒 整团待办。
|
||||
|
||||
---
|
||||
|
||||
### 4. GB-ADM-093 配摄影逐户明细 `GET /v3/admin/order/group-batch/{groupBatchId}/chips/photo`
|
||||
|
||||
**VO**: `GroupBatchChipDetailVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期看板行右侧「摄」芯片点击展开:返回每户配摄影进度。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| X-Admin-Id | Header | Long | ✅ | 网关注入 | 受信管理员 ID;客户端传值忽略 |
|
||||
| groupBatchId | Path | String | ✅ | 团期 ID(Snowflake) | 不存在/非团期/已软删 → 589500 |
|
||||
|
||||
无查询参数、无请求体。
|
||||
|
||||
#### 出参 `Result<GroupBatchChipDetailVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| batchId | String | 团期 ID |
|
||||
| chipLabel | String | 固定「配摄影」 |
|
||||
| aggregateStatus | String | 整团聚合态 四态取值与看板 GB-ADM-001 `chips.X` 的 `aggregateStatus` 完全一致(整团待办/进行中/已完成/异常) |
|
||||
| totalCount | Integer | 计入统计的子订单数(免闸户不计入) |
|
||||
| doneCount | Integer | 已完成户数(`status==DONE`) |
|
||||
| items[].orderId | String | 子订单 ID(JSON String 化) |
|
||||
| items[].orderNo | String | 子订单编号 |
|
||||
| items[].contactName | String | 联系人/客户姓名 |
|
||||
| items[].peopleCount | Integer | 本户人数 = adult+child+youngChild+baby |
|
||||
| items[].status | String | 本户配摄影状态,3 值:`NONE` 无需/未开始 / `PENDING` 待指派 / `DONE` 已指派,见「六.5」 |
|
||||
| items[].statusText | String | 状态中文名(服务端给出,见「六.5」) |
|
||||
| items[].needsIt | Boolean | 本户是否需要配摄影(`order_main.needs_photographer`);false=免闸户 status 恒 `NONE`、不计入计数 |
|
||||
| items[].updateTime | String | 配摄影最后变更时间;无独立时间戳时为 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/90211/chips/photo
|
||||
Authorization: Bearer ****
|
||||
X-Admin-Id: 3301
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"batchId": "90211",
|
||||
"chipLabel": "配摄影",
|
||||
"aggregateStatus": "整团待办",
|
||||
"totalCount": 1,
|
||||
"doneCount": 0,
|
||||
"items": [
|
||||
{ "orderId": "770180", "orderNo": "GT-26-0104", "contactName": "郑浩",
|
||||
"peopleCount": 2, "status": "PENDING", "statusText": "待指派", "needsIt": true, "updateTime": null }
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "batchId": "90211", "chipLabel": "配摄影",
|
||||
"aggregateStatus": "整团待办", "totalCount": 0, "doneCount": 0, "items": [] },
|
||||
"success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 589500, "message": "团期不存在", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 授权码 `group-batch:view`;未配置权限 → 589507。
|
||||
- 只读(READ_ONLY 事务),无锁、无幂等、不改变任何状态。
|
||||
- 免闸户(needsIt=false)status 为 `NONE`(非 null),与导芯片一致。
|
||||
- 读侧只认 `DONE`,其余非免闸状态一律 `PENDING`;已取消子订单不计入;流团团期聚合恒 整团待办。
|
||||
|
||||
---
|
||||
|
||||
### 5. GB-ADM-094 合同逐户明细 `GET /v3/admin/order/group-batch/{groupBatchId}/chips/contract`
|
||||
|
||||
**VO**: `GroupBatchChipDetailVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期看板行右侧「约」芯片点击展开:返回每户合同(签约)进度。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| X-Admin-Id | Header | Long | ✅ | 网关注入 | 受信管理员 ID;客户端传值忽略 |
|
||||
| groupBatchId | Path | String | ✅ | 团期 ID(Snowflake) | 不存在/非团期/已软删 → 589500 |
|
||||
|
||||
无查询参数、无请求体。
|
||||
|
||||
#### 出参 `Result<GroupBatchChipDetailVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| batchId | String | 团期 ID |
|
||||
| chipLabel | String | 固定「合同」 |
|
||||
| aggregateStatus | String | 整团聚合态 四态取值与看板 GB-ADM-001 `chips.X` 的 `aggregateStatus` 完全一致(整团待办/进行中/已完成/异常);房车导摄四项未全 DONE 前恒整团待办(硬规则;wire 值同看板 chips.X) |
|
||||
| totalCount | Integer | 计全部活跃子订单(约/保无免闸户) |
|
||||
| doneCount | Integer | 已完成户数(`status==SIGNED`) |
|
||||
| items[].orderId | String | 子订单 ID(JSON String 化) |
|
||||
| items[].orderNo | String | 子订单编号 |
|
||||
| items[].contactName | String | 联系人/客户姓名 |
|
||||
| items[].peopleCount | Integer | 本户人数 = adult+child+youngChild+baby |
|
||||
| items[].status | String | 本户合同状态,`ContractStatus` 8 值,见「六.5」;无合同为 null |
|
||||
| items[].statusText | String | 状态中文名(服务端给出,见「六.5」) |
|
||||
| items[].needsIt | Boolean | 恒 true(约/保无免闸) |
|
||||
| items[].updateTime | String | 合同最后变更时间;无独立时间戳时为 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/90211/chips/contract
|
||||
Authorization: Bearer ****
|
||||
X-Admin-Id: 3301
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"batchId": "90211",
|
||||
"chipLabel": "合同",
|
||||
"aggregateStatus": "DOING",
|
||||
"totalCount": 2,
|
||||
"doneCount": 1,
|
||||
"items": [
|
||||
{ "orderId": "770190", "orderNo": "GT-26-0105", "contactName": "钱进",
|
||||
"peopleCount": 2, "status": "SIGNED", "statusText": "已签署", "needsIt": true, "updateTime": null },
|
||||
{ "orderId": "770191", "orderNo": "GT-26-0106", "contactName": "孙丽",
|
||||
"peopleCount": 2, "status": "PENDING", "statusText": "待出具", "needsIt": true, "updateTime": null }
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "batchId": "90211", "chipLabel": "合同",
|
||||
"aggregateStatus": "整团待办", "totalCount": 0, "doneCount": 0, "items": [] },
|
||||
"success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 589500, "message": "团期不存在", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 授权码 `group-batch:view`;未配置权限 → 589507。
|
||||
- 只读(READ_ONLY 事务),无锁、无幂等、不改变任何状态。
|
||||
- needsIt 恒 true(约/保无免闸户);doneCount 只按 `SIGNED`;作废中/已作废计入整团 ERROR。
|
||||
- 约/保整团聚合在房车导摄四芯片未全部 DONE 前恒 整团待办(硬规则);已取消子订单不计入;流团团期聚合恒 整团待办。
|
||||
|
||||
---
|
||||
|
||||
### 6. GB-ADM-095 保险逐户明细 `GET /v3/admin/order/group-batch/{groupBatchId}/chips/insurance`
|
||||
|
||||
**VO**: `GroupBatchChipDetailVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期看板行右侧「保」芯片点击展开:返回每户保险进度。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| X-Admin-Id | Header | Long | ✅ | 网关注入 | 受信管理员 ID;客户端传值忽略 |
|
||||
| groupBatchId | Path | String | ✅ | 团期 ID(Snowflake) | 不存在/非团期/已软删 → 589500 |
|
||||
|
||||
无查询参数、无请求体。
|
||||
|
||||
#### 出参 `Result<GroupBatchChipDetailVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| batchId | String | 团期 ID |
|
||||
| chipLabel | String | 固定「保险」 |
|
||||
| aggregateStatus | String | 整团聚合态 四态取值与看板 GB-ADM-001 `chips.X` 的 `aggregateStatus` 完全一致(整团待办/进行中/已完成/异常);房车导摄未全 DONE 前恒整团待办(硬规则;wire 值同看板 chips.X) |
|
||||
| totalCount | Integer | 计全部活跃子订单(约/保无免闸户) |
|
||||
| doneCount | Integer | 已完成户数(`status==INSURED`) |
|
||||
| items[].orderId | String | 子订单 ID(JSON String 化) |
|
||||
| items[].orderNo | String | 子订单编号 |
|
||||
| items[].contactName | String | 联系人/客户姓名 |
|
||||
| items[].peopleCount | Integer | 本户人数 = adult+child+youngChild+baby |
|
||||
| items[].status | String | 本户保险状态,4 值:`INSURING` 投保中 / `INSURED` 已出单 / `CANCELLED` 已取消 / `FAILED` 出单失败;无保险为 null |
|
||||
| items[].statusText | String | 状态中文名(服务端给出,见「六.5」) |
|
||||
| items[].needsIt | Boolean | 恒 true(约/保无免闸) |
|
||||
| items[].updateTime | String | 保险最后变更时间;无独立时间戳时为 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/90211/chips/insurance
|
||||
Authorization: Bearer ****
|
||||
X-Admin-Id: 3301
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"batchId": "90211",
|
||||
"chipLabel": "保险",
|
||||
"aggregateStatus": "DOING",
|
||||
"totalCount": 2,
|
||||
"doneCount": 1,
|
||||
"items": [
|
||||
{ "orderId": "770200", "orderNo": "GT-26-0107", "contactName": "李娜",
|
||||
"peopleCount": 2, "status": "INSURED", "statusText": "已出单", "needsIt": true, "updateTime": null },
|
||||
{ "orderId": "770201", "orderNo": "GT-26-0108", "contactName": "赵敏",
|
||||
"peopleCount": 3, "status": "INSURING", "statusText": "出单中", "needsIt": true, "updateTime": null }
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "batchId": "90211", "chipLabel": "保险",
|
||||
"aggregateStatus": "整团待办", "totalCount": 0, "doneCount": 0, "items": [] },
|
||||
"success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 589500, "message": "团期不存在", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 授权码 `group-batch:view`;未配置权限 → 589507。
|
||||
- 只读(READ_ONLY 事务),无锁、无幂等、不改变任何状态。
|
||||
- needsIt 恒 true;doneCount 只按 `INSURED`;已取消/出单失败计入整团 ERROR。
|
||||
- 约/保整团聚合在房车导摄未全 DONE 前恒 整团待办(硬规则);已取消子订单不计入;流团团期聚合恒 整团待办。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
> 本节只写后端接受/拒绝请求的规则,不写 UI 渲染建议。
|
||||
|
||||
### ✅ 正确 / ❌ 错误调用对照
|
||||
|
||||
| 场景 | 说明 |
|
||||
|------|------|
|
||||
| ✅ 网关已注入 X-Admin-Id,调任一芯片端点 | 返回 `Result<GroupBatchChipDetailVO>`,code=200 |
|
||||
| ✅ 免闸户(needsIt=false) | 房/车 status=null;导/摄 status=NONE;不计入 totalCount/doneCount |
|
||||
| ❌ 未配置 `group-batch:view` 权限 | 589507 无操作权限 |
|
||||
| ❌ groupBatchId 不存在/非团期/已软删 | 589500 团期不存在 |
|
||||
| ❌ 客户端自行传 X-Admin-Id | 以网关注入值为准,客户端值一律忽略(不校验客户端值) |
|
||||
|
||||
### 切换状态时的必要动作
|
||||
|
||||
无状态切换——六个端点均为只读 GET、无请求体;前端点击芯片即查询,请求方不需要携带任何业务状态字段,也不修改任何状态。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
本组接口**无写操作**(READ_ONLY 事务),无表变更、无字段变更、无数据迁移。数据来源即为现有 order_main 六态列(needs_hotel/needs_vehicle/needs_guide/needs_photographer/合同状态/保险状态),由共享地基读取投影。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录/无有效 token → 401(网关拦截,本组接口不允许匿名访问)。
|
||||
- 未配置 `group-batch:view` 权限 → 589507 无操作权限。
|
||||
- 团期不存在/非团期/已软删 → 589500 团期不存在。
|
||||
- 任意芯片端点输入非法(groupBatchId 非数字)→ 400 参数错误(框架级)。
|
||||
- 空团期/无活跃子订单 → data 正常返回:totalCount=0、doneCount=0、items=[],不 500 不降级。
|
||||
- 下游数据缺失(老数据无对应需求/合同/保险记录)→ 对应 status 为 null(房/车/约/保)或 NONE(导/摄),不异常。
|
||||
- 团期流团(CANCELLED)→ aggregateStatus 恒整团待办(wire 值同看板 chips.X),明细仍如实返回。
|
||||
- 已返团(审核/结算)→ 房车导摄恒 DONE 聚合。
|
||||
- 列表查询与整团伙计数一次性内存聚合(防 N+1),单次请求最多一次 `listByProductBatchId` 查询。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典(接口出现枚举时必写)
|
||||
|
||||
### items[].status(配房/配车,`com.hulalv.order.core.enums.RequirementStatus`)
|
||||
|
||||
**所属字段**: `GroupBatchChipItemRespVO.status` | **类型**: `String`
|
||||
|
||||
| 值 | 中文(statusText) | 说明 |
|
||||
|----|------|------|
|
||||
| `PENDING` | 待房务配 | 待房务/车队配 |
|
||||
| `PROCESSING` | 配房中 | 配置进行中 |
|
||||
| `DONE` | 配房完成 | 已完成(房/车同文案) |
|
||||
| `PENDING_REVIEW` | 待审核 | 待复核 |
|
||||
| `REJECTED_TO_CONSULTANT` | 驳回给定制师 | 失败态(计入整团 ERROR) |
|
||||
| `REJECTED_TO_ADMIN` | 驳回给管理员 | 失败态(计入整团 ERROR) |
|
||||
|
||||
### items[].status(配导游/配摄影)
|
||||
|
||||
**所属字段**: `GroupBatchChipItemRespVO.status` | **类型**: `String`
|
||||
|
||||
| 值 | 中文(statusText) | 说明 |
|
||||
|----|------|------|
|
||||
| `NONE` | 无需 | 免闸户/未开始(免闸户恒 NONE) |
|
||||
| `PENDING` | 待指派 | 读侧非免闸非 DONE 一律 PENDING |
|
||||
| `DONE` | 已指派 | 已完成 |
|
||||
|
||||
### items[].status(合同,`com.hulalv.order.*.ContractStatus`)
|
||||
|
||||
**所属字段**: `GroupBatchChipItemRespVO.status` | **类型**: `String`
|
||||
|
||||
| 值 | 中文(statusText) | 说明 |
|
||||
|----|------|------|
|
||||
| `PENDING` | 待出具 | 待生成 |
|
||||
| `GENERATED` | 已生成 | - |
|
||||
| `REPORTED` | 已报备 | - |
|
||||
| `UPLOADED` | 已上传 | - |
|
||||
| `SIGNING` | 签署中 | - |
|
||||
| `SIGNED` | 已签署 | doneCount 判定值 |
|
||||
| `VOIDING` | 作废中 | 失败态(计入整团 ERROR) |
|
||||
| `VOIDED` | 已作废 | 失败态(计入整团 ERROR) |
|
||||
|
||||
### items[].status(保险)
|
||||
|
||||
**所属字段**: `GroupBatchChipItemRespVO.status` | **类型**: `String`
|
||||
|
||||
| 值 | 中文(statusText) | 说明 |
|
||||
|----|------|------|
|
||||
| `INSURING` | 出单中 | 投保中 |
|
||||
| `INSURED` | 已出单 | doneCount 判定值 |
|
||||
| `CANCELLED` | 已取消 | 失败态(计入整团 ERROR) |
|
||||
| `FAILED` | 出单失败 | 失败态(计入整团 ERROR) |
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
|
||||
|
||||
- **仅影响**: 管理后台团期看板六芯片的逐户明细展示层接口(GB-ADM-090~095)。
|
||||
- **零影响**:
|
||||
- 看板整团聚合接口(GB-ADM-001 `chips.X` 结构不变)
|
||||
- 房务/车队/地接/合同/保险的任何写接口与业务流程(本组只读)
|
||||
- 一期(v2)所有接口
|
||||
- 数据库结构、网关路由、权限点(复用既有 `group-batch:view`)
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**JUnit 定向测试**: `GroupBatchChipServiceTest`(12 用例)+ `GroupBatchChipControllerTest`(7 用例)+ 共享地基 `GroupBatchChipResolverTest`(18 用例)全绿,覆盖六端点、聚合态、免闸户、错误码 589500/589507、权限校验。
|
||||
|
||||
真实接口网关验证(2026-09-01 部署 dev-v3 @1159c8949 后实测):
|
||||
|
||||
测试团期: `groupBatchId=2089713777065832450`(batch Q202610312089667212070612994,RESOURCE_PREPARING,含 1 活跃子订单)
|
||||
|
||||
> 聚合态 `aggregateStatus` wire 英文四态值与看板 GB-ADM-001 `chips.X` 完全一致(读侧同源同算法);下表按中文态名展示实测结果。
|
||||
|
||||
```
|
||||
GET /v3/admin/order/group-batch/2089713777065832450/chips/hotel → 200 code=200 aggregateStatus=整团待办 total=1 done=0 items=1 ✓
|
||||
GET /v3/admin/order/group-batch/2089713777065832450/chips/vehicle → 200 code=200 aggregateStatus=整团待办 total=1 done=0 items=1 ✓
|
||||
GET /v3/admin/order/group-batch/2089713777065832450/chips/guide → 200 code=200 aggregateStatus=整团待办 total=0 items=1(needsIt=false 免闸不计 total,"无需")✓
|
||||
GET /v3/admin/order/group-batch/2089713777065832450/chips/photo → 200 code=200 aggregateStatus=整团待办 total=0 items=1(免闸,"无需")✓
|
||||
GET /v3/admin/order/group-batch/2089713777065832450/chips/contract → 200 code=200 aggregateStatus=整团待办 total=1 items=1(无合同 → statusText="无合同")✓
|
||||
GET /v3/admin/order/group-batch/2089713777065832450/chips/insurance→ 200 code=200 aggregateStatus=整团待办 total=1 items=1(无保险 → statusText="无保险")✓
|
||||
GET /v3/admin/order/group-batch/999999999999999999/chips/hotel → 200 code=589500 message="团期不存在" ✓
|
||||
GET /v3/admin/order/group-batch/abc/chips/hotel → 200 code=400 message="参数 groupBatchId 格式错误,请检查后重试" ✓
|
||||
```
|
||||
|
||||
验证通道: 统一网关 `https://api.test.1814.love:9443`,管理员登录 token + 网关注入 `X-Admin-Id`。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR(纠错 / 功能演进时必写)
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| #6916 | #6915 | 共享地基 statusText 契约文案修正 + 测试补齐(本单契约基础) | ✅ 有效 |
|
||||
| 本 PR(#6903 分支合并) | #6903 | 六芯片逐户明细接口交付 | ✅ 最新 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#6903](https://git.1814.love:8443/wx/HL/issues/6903)
|
||||
- 关联 PR: [wx/HL#6918](https://git.1814.love:8443/wx/HL/pulls/6918)(squash 合并至 dev-v3 @1159c8949)
|
||||
- 共享地基: #6902(Resolver/看板聚合)、#6916(statusText 契约修正)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#6903](https://git.1814.love:8443/wx/HL/issues/6903)
|
||||
- **PR**: 合并后回填
|
||||
- **Merge commit**: 合并后回填
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx(GIT)
|
||||
@@ -0,0 +1,330 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6904"
|
||||
title: "团期看板统计条 GB-ADM-009 + 团期导出 GB-ADM-008"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: "2026-09-01"
|
||||
status_note: "2026-09-01 部署测试服 dev-v3@9f689354e,/summary 与 /export 经测试网关实测 200 全通过;CSV BOM/CRLF/10 列、7 桶口径、589517 上限均核验。前端评估(2026-09-01):团期看板 src/views/order-v2/batch 当前为纯本地 mock 阶段(无真实 group-batch 调用),统计条/导出依赖真实看板页;且看板列表契约 #6905/GB-ADM-000~003 未推送进 changelog 仓(仅被本单与 #6903 引用,无契约本体)。决策:等后端推送看板列表契约后,统一接真看板再一并接统计条+导出+六芯片下钻(#6903)。跟踪见 hl-admin 任务 #104。"
|
||||
updated_at: "2026-09-01"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期看板:统计条(GB-ADM-009)+ 团期导出(GB-ADM-008)
|
||||
|
||||
> **存放目录**: 二期(order-v3)→ `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #6917
|
||||
> **Issue**: [#6904](https://git.1814.love:8443/wx/HL/issues/6904)
|
||||
> **日期**: 2026-09-01
|
||||
> **影响范围**: 管理后台团期看板顶部统计条(7 桶计数 + 活跃子订单数)与整表 CSV 导出
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**新增能力,无破坏性变化**:团期看板新增两个只读端点——统计条(`/summary`)返回七大状态桶与跨团期活跃子订单数;导出(`/export`)按当前筛选整表导出 CSV(UTF-8 BOM、CRLF、固定 10 列)。与 #6905 交付的列表/详情(GB-ADM-000~003)同地基(#6902),本单不触碰 #6905 任何接口与文件。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景(选填)
|
||||
|
||||
管理后台团期看板页顶部需要一条统计条(七桶计数 + 活跃子订单数),并把当前筛选下的团期整表导出为 CSV。口径与看板列表(GB-ADM-000/001/002/003)完全一致:同筛选(productId 精确、month 落出团月、keyword 模糊 OR)、同状态源(`order_group_batch.batch_status` 八态)、同聚合(跨命中团期统计活跃子订单)。FORMED 为 RESOURCE_PREPARING+MATERIAL_PREPARING 复合桶;AUDITING/CHECKED 在接口层保持独立桶,前端展示可折叠合并。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | GB-ADM-009 团期看板统计条 | GET | `/v3/admin/order/group-batch/summary` | 新增接口 | 七桶计数 + total + subOrderCount |
|
||||
| 2 | GB-ADM-008 团期导出 | GET | `/v3/admin/order/group-batch/export` | 新增接口 | 看板整表 CSV 导出(10 列) |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
两个接口共用筛选口径与状态映射(#6902 `GroupBatchStageBuckets` 七桶 / `GroupBatchStatus` 八态),正文各自自包含。
|
||||
|
||||
### 1. GB-ADM-009 团期看板统计条 `GET /v3/admin/order/group-batch/summary`
|
||||
|
||||
**VO**: `GroupBatchSummaryVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期看板页顶部统计条:返回当前筛选口径下七大状态桶计数、桶合计与跨团期活跃子订单数(任一桶非零即有数据)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| X-Admin-Id | Header | Long | ✅ | 网关注入 | 受信管理员 ID;客户端传值一律忽略(不校验客户端值) |
|
||||
| productId | Query | Long | ❌ | - | 精确匹配团期所属产品 |
|
||||
| month | Query | String | ❌ | `yyyy-MM` | 出团月筛选:`depart_date >= 月初 && < 次月`;空/缺省 = 全部 |
|
||||
| keyword | Query | String | ❌ | - | 团期号/团期名模糊 OR(trim 后;`%` `_` `\` 转义,与 GB-ADM-001 同义) |
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 出参 `Result<GroupBatchSummaryVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| total | Integer | 七桶计数之和(接口自校验桶和,恒等于各桶值加总) |
|
||||
| buckets | Map<String,Integer> | 七键恒全:RECRUIT/FORMED/PENDING_TRIP/TRAVELLING/AUDITING/CHECKED/DISBANDED;FORMED=RESOURCE_PREPARING+MATERIAL_PREPARING 复合 |
|
||||
| subOrderCount | Integer | 跨全部命中团期的活跃子订单数(order_status != CANCELLED 且未软删;1 团期 1 房) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/summary?month=2026-06&keyword=%E6%B5%8B%E8%AF%95
|
||||
Authorization: Bearer ****
|
||||
X-Admin-Id: 3301
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"total": 14,
|
||||
"buckets": {
|
||||
"RECRUIT": 0, "FORMED": 13, "PENDING_TRIP": 0, "TRAVELLING": 0,
|
||||
"AUDITING": 0, "CHECKED": 0, "DISBANDED": 1
|
||||
},
|
||||
"subOrderCount": 1
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "total": 0,
|
||||
"buckets": { "RECRUIT": 0, "FORMED": 0, "PENDING_TRIP": 0, "TRAVELLING": 0,
|
||||
"AUDITING": 0, "CHECKED": 0, "DISBANDED": 0 },
|
||||
"subOrderCount": 0 }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 589507, "message": "无操作权限", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 授权码 `group-batch:list`;未配置权限 → 589507。
|
||||
- 只读(READ_ONLY 事务),无锁、无幂等、不改变任何状态。
|
||||
- 非法 month(非 `yyyy-MM`)→ 400 参数错误(框架级)。
|
||||
- 空数据时七键仍全返回(各桶 0),total=0、subOrderCount=0,不 500 不降级。
|
||||
|
||||
---
|
||||
|
||||
### 2. GB-ADM-008 团期导出 `GET /v3/admin/order/group-batch/export`
|
||||
|
||||
**VO**: `text/csv`(流式附件,非 JSON 信封)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期看板页「导出」按钮:按当前筛选口径整表导出 CSV 文件(浏览器附件下载)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| X-Admin-Id | Header | Long | ✅ | 网关注入 | 受信管理员 ID;客户端传值忽略 |
|
||||
| productId | Query | Long | ❌ | - | 同 summary |
|
||||
| month | Query | String | ❌ | `yyyy-MM` | 同 summary;文件名 `group-batch-{month}.csv` |
|
||||
| keyword | Query | String | ❌ | - | 同 summary |
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 出参 `text/csv`(附件流)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| Content-Type | String | `text/csv; charset=utf-8` |
|
||||
| Content-Disposition | String | `attachment; filename=group-batch-{month|all}.csv` |
|
||||
| body | String | UTF-8 BOM 开头的 CSV 文本:CRLF 换行、固定 10 列(表头见下) |
|
||||
|
||||
**表头 10 列**: 团期号、期号、日期、出团日、满团名额、已售、剩余、状态、子订单数、整团应收。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/export?month=2026-06
|
||||
Authorization: Bearer ****
|
||||
X-Admin-Id: 3301
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
(响应体为原生 CSV 文本,非 JSON 信封;下表以 JSON 字符串形式呈现字节内容示例)
|
||||
|
||||
```json
|
||||
"团期号,期号,日期,出团日,满团名额,已售,剩余,状态,子订单数,整团应收\r\nQ2026...,测试期,2026-06-10,2026-06-10,10,7,3,资源准备,1,15900.00\r\n"
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
0 命中仅输出表头 10 列(仍带 UTF-8 BOM 与 CRLF),HTTP 200。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 589517, "message": "导出数据超限", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 授权码 `group-batch:export`;未配置权限 → 589507。
|
||||
- 命中行数 > 2000 → 589517(上限 2000 行),前端提示收窄筛选。
|
||||
- 每次成功导出写入 `group_batch_status_log` 导出留痕(BATCH_EXPORT,data 类变更,记录操作管理员与导出档头内容)。
|
||||
- 只读(READ_ONLY 事务);0 命中仅表头(BOM/CRLF 保持)。
|
||||
- 非法 month → 400 参数错误;未登录 → 401(网关拦截)。
|
||||
- 期号含逗号/引号/换行 → CSV 引号转义(内部引号翻倍);金额两位小数不加千分位;剩余=max_rooms-已售且下限 0。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
> 本节只写后端接受/拒绝请求的规则,不写 UI 渲染建议。
|
||||
|
||||
### ✅ 正确 / ❌ 错误调用对照
|
||||
|
||||
| 场景 | 说明 |
|
||||
|------|------|
|
||||
| ✅ 仅带 Authorization 调 summary/export | 全量口径:7 桶恒全、export 全表(受 2000 行上限约束) |
|
||||
| ✅ productId+month+keyword 组合筛选 | 与 GB-ADM-001 同义:productId 精确 / month 落出团月 / keyword trim 后模糊 OR(`%` `_` `\` 转义) |
|
||||
| ❌ 未配置 `group-batch:list` / `group-batch:export` | 589507 无操作权限(两个授权点独立) |
|
||||
| ❌ 命中 > 2000 行导出 | 589517 导出数据超限 |
|
||||
| ❌ 客户端自行传 X-Admin-Id | 以网关注入值为准,客户端值一律忽略 |
|
||||
|
||||
### 切换状态时的必要动作
|
||||
|
||||
无状态切换——两端点均只读 GET、无请求体;导出需前端先具备 `group-batch:export` 授权点(看板查看仅需 `group-batch:list`)。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
两端点主体为只读(READ_ONLY 事务):聚合 `countActiveByProductBatchIds`(活跃子订单)、`sumBatchAmountsByProductBatchIds`(整团应收,共享 #6902 地基),无表变更、无字段变更。唯一写操作:成功导出后向 `group_batch_status_log` 追加一条 BATCH_EXPORT 留痕记录(`REQUIRES_NEW` 独立事务,导出失败不写留痕)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录/无有效 token → 401(网关拦截,两端点不允许匿名访问)。
|
||||
- 未配置对应授权点 → 589507。(`/summary` 用 `group-batch:list`,`/export` 用 `group-batch:export`)
|
||||
- 导出命中 > 2000 行 → 589517;前端应引导收窄 month/productId/keyword。
|
||||
- 非法 month 格式 → 400 参数错误(框架级)。
|
||||
- 空数据:summary 七键全 0;export 仅表头 10 列(BOM/CRLF 保持),不 500 不降级。
|
||||
- 状态未知/历史脏数据:`safeStatus` 原样透出(导出的状态列不报错)。
|
||||
- 剩余列恒 `max_rooms - 已售` 且下限 0;金额列两位小数、不加千分位。
|
||||
- 期号含 `,` `"` `\r` `\n` → CSV 引号转义(内部引号翻倍)。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典(接口类必写)
|
||||
|
||||
### buckets 键(7 桶)
|
||||
|
||||
**所属字段**: `GroupBatchSummaryVO.buckets` | **类型**: `Map<String,Integer>`
|
||||
|
||||
| 桶键 | 来源 batch_status | 说明 |
|
||||
|------|------------------|------|
|
||||
| RECRUIT | RECRUITING | 招募中 |
|
||||
| FORMED | RESOURCE_PREPARING + MATERIAL_PREPARING | 复合桶(成团准备) |
|
||||
| PENDING_TRIP | PENDING_DEPARTURE | 待出发 |
|
||||
| TRAVELLING | TRAVELLING | 出游中 |
|
||||
| AUDITING | REVIEWING | 审核 |
|
||||
| CHECKED | SETTLED | 结算 |
|
||||
| DISBANDED | CANCELLED | 流团/取消 |
|
||||
|
||||
> 说明:FORMED 复合桶、AUDITING/CHECKED 独立桶为接口层口径;前端展示可将 AUDITING+CHECKED 折叠为「返团核账」类目(display 层合并,接口不并)。
|
||||
|
||||
### 导出「状态」列中文名
|
||||
|
||||
**所属字段**: CSV 第 8 列 | **类型**: `String`
|
||||
|
||||
| batch_status | 中文 |
|
||||
|--------------|------|
|
||||
| RECRUITING | 招募中 |
|
||||
| RESOURCE_PREPARING | 资源准备 |
|
||||
| MATERIAL_PREPARING | 物料准备 |
|
||||
| PENDING_DEPARTURE | 待出发 |
|
||||
| TRAVELLING | 出游中 |
|
||||
| REVIEWING | 审核中 |
|
||||
| SETTLED | 已结算 |
|
||||
| CANCELLED | 已流团 |
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
|
||||
|
||||
- **仅影响**: 团期看板统计条(GB-ADM-009)与导出(GB-ADM-008)两个新增端点。
|
||||
- **零影响**:
|
||||
- #6905 交付的看板列表/详情/条件接口(GB-ADM-000~003)与文件(本单未改动)
|
||||
- 共享地基 #6902 的 `OrderService`/`GroupBatchStageBuckets`(本单仅新增导出留痕方法与本枚举项,未改既有方法与接口)
|
||||
- 一期(v2)所有接口;数据库结构、网关路由、权限点(复用既有 `group-batch:list`/`group-batch:export`)
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**JUnit 定向测试**: `GroupBatchBoardStatsServiceTest`(11 用例)+ `GroupBatchBoardStatsControllerTest`(6 用例)+ `GroupBatchMapperEscapeLikeTest`(3 用例)全绿;覆盖七桶折叠、total 自校验、subOrderCount、筛选转义、CSV 格式、589507/589517、留痕写入。
|
||||
|
||||
**全模块** `mvn -pl hl-order-service-v3 -am test`(2026-09-01,分支合并至 dev-v3 后):Tests run 7872 / Failures 3 / Errors 0 / Skipped 7——3 个失败均为 refund 模块**既有**切片用例(旧契约期望 200+code401 信封,当前拦截器直返 401),已在独立基线 worktree(dev-v3 不含本次改动)复现,与本次 9 个文件变更无关。
|
||||
|
||||
真实接口网关验证(2026-09-01 部署 dev-v3@9f689354e 后实测):
|
||||
|
||||
```
|
||||
GET /v3/admin/order/group-batch/summary → 200 code=200 buckets={RECRUIT:0,FORMED:13,PENDING_TRIP:0,TRAVELLING:0,AUDITING:0,CHECKED:0,DISBANDED:1} total=14 subOrderCount=1 ✓
|
||||
GET /v3/admin/order/group-batch/summary?month=2026-06&keyword=测试 → 200 code=200 ✓
|
||||
GET /v3/admin/order/group-batch/export → 200 size=1739 BOM ✓ CRLF ✓ 10 列 ✓
|
||||
GET /v3/admin/order/group-batch/export?month=2026-06 → 200 size=98(0 命中仅表头) ✓
|
||||
```
|
||||
|
||||
验证通道: 统一网关 `https://api.test.1814.love:9443`,管理员登录 token + 网关注入 `X-Admin-Id`。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR(纠错 / 功能演进时必写)
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| #6917 | #6904 | 团期看板统计条 + 导出交付(本单) | ✅ 最新 |
|
||||
| #6914 | #6902 | 共享地基(七桶枚举/聚合/权限/错误码) | ✅ 有效 |
|
||||
| #6916 | #6915 | 共享地基 statusText 契约修正 | ✅ 有效 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#6904](https://git.1814.love:8443/wx/HL/issues/6904)
|
||||
- 关联 PR: [wx/HL#6917](https://git.1814.love:8443/wx/HL/pulls/6917)(合并至 dev-v3 @9f689354e)
|
||||
- 共享地基: #6902(七桶/聚合/权限)、#6915/#6916(契约修正)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#6904](https://git.1814.love:8443/wx/HL/issues/6904)
|
||||
- **PR**: #6917
|
||||
- **Merge commit**: 9f689354e
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx(GIT)
|
||||
在新工单中引用
屏蔽一个用户