比较提交
111
次代码提交
| 作者 | SHA1 | 提交日期 | |
|---|---|---|---|
|
|
70c1f44650 | ||
|
|
a7c44c0caa | ||
|
|
354d9083a7 | ||
|
|
7b9b60b9ac | ||
|
|
c1d7fed7b6 | ||
|
|
c5c69d10aa | ||
|
|
c5adab6346 | ||
|
|
c4968b5233 | ||
|
|
62c52b8024 | ||
|
|
713b15a640 | ||
|
|
293aa77f98 | ||
|
|
2f6a989fcf | ||
|
|
54b4d7a86e | ||
|
|
f3f11505d4 | ||
|
|
d1fe0c3bf0 | ||
|
|
4e52f26972 | ||
|
|
c399b3b0f1 | ||
|
|
0dec0584f9 | ||
|
|
a269c0bc97 | ||
|
|
be5afcf30a | ||
|
|
125014ac9b | ||
|
|
19c3abe79e | ||
|
|
a67e24b9b9 | ||
|
|
81139ab8c3 | ||
|
|
8973353356 | ||
|
|
17490fff88 | ||
|
|
a97809f105 | ||
|
|
0a324bb6dc | ||
|
|
8696bbb083 | ||
|
|
eede11917a | ||
|
|
e3aace57c5 | ||
|
|
4ff1480683 | ||
|
|
8c38fc6b89 | ||
|
|
a611948477 | ||
|
|
db6698061b | ||
|
|
2096a47340 | ||
|
|
e5356a65e0 | ||
|
|
71cc52c1a3 | ||
|
|
0df2c917bf | ||
|
|
cf78c344a1 | ||
|
|
a0daab2b90 | ||
|
|
5046bce500 | ||
|
|
c80d1975aa | ||
|
|
f4354d8c31 | ||
|
|
828d9ae2e4 | ||
|
|
d2c39b6c24 | ||
|
|
411a549548 | ||
|
|
f68b6d80d4 | ||
|
|
c579da759e | ||
|
|
c66a28b43f | ||
|
|
6c1d11d40f | ||
|
|
9428076c2c | ||
|
|
95394c35fc | ||
|
|
edfffdf063 | ||
|
|
06a86d488d | ||
|
|
082f32ad3f | ||
|
|
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 | ||
|
|
3d6f6245a0 | ||
|
|
ff16cfd94c | ||
|
|
2e18de27e1 |
+3
-3
@@ -2,7 +2,7 @@
|
||||
# changelog 发布门禁(推送前强制校验)
|
||||
# 启用(每台机一次): git config core.hooksPath .githooks
|
||||
# 拦截目标: 接口类 changelog 未部署测试服(backend_status != deployed)就推送给前端,
|
||||
# 以及文件名/frontmatter 结构违规。规则实现见 scripts/validate-changelog-*.mjs。
|
||||
# 以及文件名/frontmatter/CHANGELOG_TEMPLATE.md 结构违规。规则实现见 scripts/validate-changelog-*.mjs。
|
||||
zero=0000000000000000000000000000000000000000
|
||||
status=0
|
||||
while read local_ref local_sha remote_ref remote_sha; do
|
||||
@@ -19,7 +19,7 @@ while read local_ref local_sha remote_ref remote_sha; do
|
||||
done
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "" >&2
|
||||
echo "推送被 changelog 发布门禁拦截:接口类条目必须测试服已部署+实测(backend_status=deployed)后才能推送给前端。" >&2
|
||||
echo "修正文件后重试;规则详见 BACKEND_CHANGELOG_DELIVERY_GUIDE.md §2.1。" >&2
|
||||
echo "推送被 changelog 发布门禁拦截:接口类条目必须已部署实测,并完整仿照 CHANGELOG_TEMPLATE.md。" >&2
|
||||
echo "每个接口需自含入参、出参、请求/响应、错误和业务边界;修正规则详见 BACKEND_CHANGELOG_DELIVERY_GUIDE.md。" >&2
|
||||
fi
|
||||
exit $status
|
||||
|
||||
@@ -25,7 +25,8 @@ changelogs-v2/2026-07/24_5205_车务首页汇总状态补全-修改接口-管理
|
||||
|
||||
## 2. 写什么
|
||||
|
||||
可以复制仓库根目录的 `CHANGELOG_TEMPLATE.md`,至少写清:
|
||||
必须复制并按仓库根目录的 `CHANGELOG_TEMPLATE.md` 组织接口文档。接口类 Changelog 不能只写
|
||||
路径和变更摘要,至少写清:
|
||||
|
||||
- 关联的 Issue 和后端 PR;
|
||||
- 接口路径和 HTTP 方法;
|
||||
@@ -34,6 +35,11 @@ changelogs-v2/2026-07/24_5205_车务首页汇总状态补全-修改接口-管理
|
||||
- 前端需要做什么;
|
||||
- 后端测试、部署和网关验证结果。
|
||||
|
||||
多接口条目必须让每个接口小节独立包含入参、出参、请求示例、响应示例、错误响应和业务边界;
|
||||
“变更接口清单”的 `METHOD /path` 必须与逐接口详情一一对应。该结构由
|
||||
`check:frontmatter` 机器校验,缺失时分别报 `E_API_TEMPLATE`、`E_API_ENDPOINTS` 或
|
||||
`E_API_DETAIL`。存量接口文档一旦修改,也必须升级到当前模板标准。
|
||||
|
||||
元数据中:
|
||||
|
||||
```yaml
|
||||
|
||||
+12
-2
@@ -3,7 +3,8 @@ schema: "hl-changelog/v2"
|
||||
ticket: "{issue-no}"
|
||||
title: "{一句话概括变化}"
|
||||
consumer: "{admin|mp|internal|multiple}"
|
||||
author: "{推送者登录名}(GIT)" # 如 wx(GIT)/yst(GIT),谁 push 到 main 就写谁
|
||||
# author 示例: wx(GIT) / yst(GIT);谁 push 到 main 就写谁
|
||||
author: "{推送者登录名}(GIT)"
|
||||
change_type: "{新增接口|修改接口|删除接口}"
|
||||
backend_status: "pending"
|
||||
gateway_status: "pending"
|
||||
@@ -67,6 +68,10 @@ base: "{dev|dev-v3}"
|
||||
|
||||
**VO**: `{VO 类名}`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
说明前端在什么页面、什么时机调用;多接口条目中每个接口都必须自包含,不依赖其他章节补参数或错误语义。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
@@ -113,9 +118,14 @@ base: "{dev|dev-v3}"
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 写清本接口自己的鉴权、空值、状态、幂等/并发、失败零写入和兼容规则。
|
||||
- 不要只在全局章节写一次;消费方应能单独阅读本接口小节完成联调。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(选填,字段互斥/联动/切换场景必写)
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
|
||||
|
||||
|
||||
+18
@@ -75,6 +75,24 @@ pending → claimed → implemented → released → verified
|
||||
- `FRONTEND_CONSUMPTION_STATUS_GUIDE.md`
|
||||
- `BACKEND_CHANGELOG_DELIVERY_GUIDE.md`
|
||||
|
||||
## 接口文档模板门禁
|
||||
|
||||
新增、修改或删除接口的 Changelog 必须以仓库根目录
|
||||
[`CHANGELOG_TEMPLATE.md`](CHANGELOG_TEMPLATE.md) 为结构基线,不能只写接口路径和一段变更摘要。
|
||||
|
||||
机器校验要求:
|
||||
|
||||
- 使用“二、变更接口清单”的标准六列表格;
|
||||
- 清单中的每个 `METHOD /path` 都必须在“三、接口详情”中有且只有一个对应小节;
|
||||
- 每个接口小节必须自含入参字段表、出参字段表、请求示例、响应示例、错误响应和业务边界;
|
||||
- 含写接口时必须说明外部可观察的数据库行为,但不得泄露物理表结构;
|
||||
- 修改或删除接口必须补“修改前后对比”和“影响评估”;
|
||||
- 必须保留契约约束、边界行为、不影响范围、TEST 验证、相关文档及联系人章节。
|
||||
|
||||
缺少上述内容时 `check:frontmatter` 返回 `E_API_TEMPLATE`、`E_API_ENDPOINTS` 或
|
||||
`E_API_DETAIL`,PR/push 检测失败。存量文件不全量追责,但任何接口类文件一旦新增或修改,
|
||||
就必须补到当前模板标准。
|
||||
|
||||
已下发路径是消费契约的一部分,不通过重命名表达状态。历史路径已发生迁移时:
|
||||
|
||||
- `changelog-path-aliases.json` 是机器可识别的唯一映射源;
|
||||
|
||||
@@ -7,7 +7,7 @@ author: "wx"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
|
||||
@@ -7,7 +7,7 @@ author: "wx"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
|
||||
@@ -7,7 +7,7 @@ author: "wx"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
|
||||
@@ -7,67 +7,290 @@ author: "lc(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
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 已由 Deploy Panel 任务 52771074(hl-user-service)和 b1db24b2(hl-gateway)部署 TEST;真实管理员经 Gateway 完成未认证、读取、失败零写入、创建、CAS 更新、数据库和 Redis 一致性验收,临时数据、缓存快照及登录会话均已恢复。管理端尚待接入新增接口。"
|
||||
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"
|
||||
---
|
||||
|
||||
# 行政区划主数据与三级联动
|
||||
# User: 行政区划主数据与三级联动
|
||||
|
||||
管理后台新增可版本化的行政区划主数据接口,统一提供省、市、县三级联动、任意节点父链回显,以及受权限、有效期、层级和并发版本保护的创建与更新能力。
|
||||
> **服务**: `hl-user-service`(统一经 Gateway 访问)
|
||||
>
|
||||
> **PR**: #6359、#6387、#6393、#6398、#6402、#6412
|
||||
>
|
||||
> **Issue**: #6350、#6384、#6389、#6395、#6399、#6409、#6422、#6438
|
||||
>
|
||||
> **日期**: 2026-08-26
|
||||
>
|
||||
> **影响范围**: 管理后台行政区划维护、省/市/县三级选择器和历史值父链回显
|
||||
|
||||
> **服务**:`hl-user-service`,统一经 Gateway `/admin/region/**` 访问
|
||||
> **Issue**:#6350、#6384、#6389、#6395、#6399、#6409
|
||||
> **PR**:#6359、#6387、#6393、#6398、#6402、#6412
|
||||
> **影响范围**:管理后台行政区划维护、三级选择器和历史值父链回显
|
||||
---
|
||||
|
||||
## 关键约定
|
||||
## ⚠️ 关键变化
|
||||
|
||||
- 所有响应中的行政区划 ID 均为 JSON 字符串,包括 `data`、`parentId`、`defaultId`、`selectedId`、`provinceId`、`prefectureId`、`countyId` 和节点 `id`;没有父级或对应层级时保持 `null`。
|
||||
- 当前写入边界为省、市、县三级,层级代码依次为 `PROVINCE`、`PREFECTURE`、`COUNTY`;服务端根据父链计算 `depth`,请求不能直接指定深度。
|
||||
- `ADMINISTRATIVE` 表示可作为业务行政区的节点;`AGGREGATION` 只用于导航分组,必须使用 `HL_GROUP`、`navigable=true`、`selectable=false`。
|
||||
- 有效期使用左闭右开区间 `[validFrom, validTo)`;`validTo=null` 表示持续有效。
|
||||
- 读接口只要求真实管理员登录;创建还要求 `system:region:create`,更新要求 `system:region:update`。
|
||||
- 统一响应为 `Result<T>`。业务失败可能仍是 HTTP 200,调用方必须同时检查 `code`、`success`、`message` 和 `data`。
|
||||
- 本次补文档不改变 TEST 上已经部署的接口行为;它把原 Changelog 中分散的说明整理为可直接联调的完整契约。
|
||||
- 前端不得再把行政区划 ID 当 JavaScript number:响应中的所有行政区划 ID 均为 JSON string,缺失层级保持真正的 `null`。
|
||||
- 三级联动不允许通过行政区编码截位推断父子关系;必须逐级调用 `children`,编辑回显先调用 `path`。
|
||||
- `PREFECTURE` 是第二层导航层,不保证一定是可选择的法定地级行政区;直辖市等链路会返回 `AGGREGATION` 聚合节点,必须继续下钻且不得作为最终业务值。
|
||||
- 当前没有单节点详情接口,读响应也不返回 `extension`;更新接口省略或传 `null` 都会清空该字段,前端不能把它理解成“保持原值”。
|
||||
- 业务失败可能仍使用 HTTP 200,调用方必须同时判断响应体 `code`、`success`、`message` 和 `data`。
|
||||
|
||||
## 变更接口清单
|
||||
---
|
||||
|
||||
| 方法 | 路径 | 权限 | 用途 |
|
||||
|---|---|---|---|
|
||||
| POST | `/admin/region` | `system:region:create` | 创建行政区或导航聚合节点 |
|
||||
| PUT | `/admin/region/{id}` | `system:region:update` | 使用 `rowVersion` 更新可变字段 |
|
||||
| GET | `/admin/region/children` | 管理员登录 | 查询根层或指定父节点的当前有效直接子节点 |
|
||||
| GET | `/admin/region/{id}/path` | 管理员登录 | 返回任意节点从省级根开始的完整父链 |
|
||||
## 一、背景
|
||||
|
||||
## 1. 单级联动列表
|
||||
管理后台需要一套统一的行政区划主数据能力,同时支持:
|
||||
|
||||
- 省、市、县三级选择器逐级加载;
|
||||
- 编辑历史业务数据时,从任意节点恢复完整父链;
|
||||
- 具有专用权限的管理员创建自定义行政区、带来源证据的新法定版本或导航聚合节点;
|
||||
- 使用 `rowVersion` 防止两位管理员并发更新时互相覆盖。
|
||||
|
||||
接口当前只开放省、市、县三级写入,层级代码依次为 `PROVINCE`、`PREFECTURE`、`COUNTY`。第二层可能是 `AGGREGATION` 导航分组,不等同于可提交的法定地级行政区。下文读取示例使用 TEST migration 中的北京链路 `1 → 35 → 376`,仅用于说明响应结构,业务代码仍不得硬编码。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 单级联动列表 | GET | `/admin/region/children` | 新增接口 | 加载根层省级或指定父节点的当前有效直接子节点 |
|
||||
| 2 | 父链回显 | GET | `/admin/region/{id}/path` | 新增接口 | 从任意省/市/县节点恢复省级根到目标节点的完整路径 |
|
||||
| 3 | 创建行政区划节点 | POST | `/admin/region` | 新增接口 | 创建自定义行政区、新法定版本或不可选择的导航聚合节点 |
|
||||
| 4 | 更新行政区划节点 | PUT | `/admin/region/{id}` | 新增接口 | 使用完整可变字段和 `rowVersion` 执行 CAS 更新 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 单级联动列表 `GET /admin/region/children`
|
||||
|
||||
**VO**: `AdministrativeRegionChildrenVO / AdministrativeRegionItemVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
- 新建页面首次加载省级列表:省略 `parentId`。
|
||||
- 选择省后加载市、选择市后加载县:把当前选中节点 ID 作为下一次请求的 `parentId`。
|
||||
- 编辑页面逐层恢复默认值:把本层历史 ID 作为 `selectedId`;它仍在当前 `items` 中时会成为 `defaultId`。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `Authorization` | Header | String | ✅ | `Bearer <token>` | 真实管理端登录令牌 |
|
||||
| `parentId` | Query | String | ❌ | 十进制正整数;根层省级列表必须省略 | 指定要查询直接子节点的父节点 ID |
|
||||
| `selectedId` | Query | String | ❌ | 十进制正整数 | 编辑回显时希望优先选中的本级节点 ID |
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 出参 `Result<AdministrativeRegionChildrenVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `code` | Integer | `200` 表示成功;业务失败读取具体业务码 |
|
||||
| `message` | String | 响应说明 |
|
||||
| `success` | Boolean | 仅当 `code=200` 时为 `true` |
|
||||
| `traceId` | String / null | 链路追踪 ID;报错排查时提供给后端 |
|
||||
| `data.parentId` | String / null | 本次查询父节点;根层查询为 `null` |
|
||||
| `data.defaultId` | String / null | 合法 `selectedId`,否则为排序后首项 ID;空列表为 `null` |
|
||||
| `data.items` | Array | 当前有效且状态为 `ACTIVE` 的直接子节点,稳定排序 |
|
||||
| `data.items[].id` | String | 节点 ID,始终为字符串 |
|
||||
| `data.items[].parentId` | String / null | 父节点 ID;省级根节点为 `null` |
|
||||
| `data.items[].codeStandard` | String | 编码标准,例如 `GB/T 2260`、`HL_CUSTOM`、`HL_GROUP` |
|
||||
| `data.items[].regionCode` | String | 编码标准内的地区代码 |
|
||||
| `data.items[].levelCode` | String | `PROVINCE`、`PREFECTURE` 或 `COUNTY` |
|
||||
| `data.items[].depth` | Integer | 树深度,省/市/县分别为 1/2/3 |
|
||||
| `data.items[].name` | String | 行政区划完整名称 |
|
||||
| `data.items[].shortName` | String / null | 简称 |
|
||||
| `data.items[].nodeKind` | String | `ADMINISTRATIVE` 或 `AGGREGATION` |
|
||||
| `data.items[].navigable` | Boolean | 是否允许继续查询下一层 |
|
||||
| `data.items[].selectable` | Boolean | 是否允许作为最终业务行政区值 |
|
||||
| `data.items[].hasChildren` | Boolean | 是否存在当前有效、启用的直接子节点 |
|
||||
| `data.items[].currentlyEffective` | Boolean | 中国业务当日是否位于有效期且状态启用 |
|
||||
| `data.items[].status` | String | `ACTIVE` 或 `INACTIVE` |
|
||||
| `data.items[].validFrom` | String | 生效日期,格式 `yyyy-MM-dd` |
|
||||
| `data.items[].validTo` | String / null | 失效日期;`null` 表示持续有效 |
|
||||
| `data.items[].sortOrder` | Integer | 同级排序号 |
|
||||
| `data.items[].rowVersion` | Integer | 当前 CAS 版本,更新时原样提交 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/region/children?parentId=1&selectedId=35
|
||||
GET /admin/region/children?parentId=1&selectedId=35 HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <管理员令牌>
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
查询省级根列表时省略 `parentId`。`selectedId` 只有在本次 `items` 中存在时才作为 `defaultId`;否则回退为排序后的首项,空列表返回 `null`。
|
||||
根层省级列表:
|
||||
|
||||
```http
|
||||
GET /admin/region/children HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <管理员令牌>
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"parentId": "1",
|
||||
"items": [
|
||||
{
|
||||
"id": "35",
|
||||
"parentId": "1",
|
||||
"codeStandard": "GB/T 2260",
|
||||
"regionCode": "110100",
|
||||
"codeStandard": "HL_GROUP",
|
||||
"regionCode": "HL-GROUP-110000",
|
||||
"levelCode": "PREFECTURE",
|
||||
"depth": 2,
|
||||
"name": "北京市辖区",
|
||||
"shortName": null,
|
||||
"nodeKind": "AGGREGATION",
|
||||
"navigable": true,
|
||||
"selectable": false,
|
||||
"hasChildren": true,
|
||||
"currentlyEffective": true,
|
||||
"status": "ACTIVE",
|
||||
"validFrom": "2025-12-31",
|
||||
"validTo": null,
|
||||
"sortOrder": 2000000001,
|
||||
"rowVersion": 0
|
||||
}
|
||||
],
|
||||
"defaultId": "35"
|
||||
},
|
||||
"traceId": "a1b2c3d4-e5f6-7890",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
父节点存在但没有当前有效直接子节点时,返回成功空数组。Redis 读取失败时服务会降级查数据库,成功结构不变:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"parentId": "376",
|
||||
"items": [],
|
||||
"defaultId": null
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
父节点不存在或已删除:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 210801,
|
||||
"message": "行政区划节点不存在",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 只返回 `status=ACTIVE` 且当前有效的直接子节点,停用或历史节点不会混入当前选择项。
|
||||
- `selectedId` 不在本次 `items` 中时不会报错,而是稳定回退到排序后的第一项;空列表回退为 `null`。
|
||||
- 排序固定为 `sortOrder`、`regionCode`、`id`,前端不得另用行政区编码推断顺序或父子关系。
|
||||
- `navigable` 控制能否继续向下加载,`selectable` 控制能否作为最终值;两者不能相互替代。
|
||||
- `defaultId` 只表示本级默认导航项,不保证 `selectable=true`;例如北京第二层默认项 `"35"` 是不可提交的聚合节点。
|
||||
- 未登录返回业务码 `401`;`parentId=0` 等非正整数参数返回业务码 `400`。
|
||||
|
||||
---
|
||||
|
||||
### 2. 父链回显 `GET /admin/region/{id}/path`
|
||||
|
||||
**VO**: `AdministrativeRegionPathVO / AdministrativeRegionItemVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
编辑已有业务数据时,后端只保存了一个最终行政区 ID。前端先调用本接口得到完整三级父链,再按父链逐层调用 `children` 加载每一级导航项。父链的第二层可能是不可选择的 `AGGREGATION`,不能仅凭 `prefectureId` 字段名判断它一定是法定地级行政区。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `Authorization` | Header | String | ✅ | `Bearer <token>` | 真实管理端登录令牌 |
|
||||
| `id` | Path | String | ✅ | 十进制正整数 | 要回显的省、市或县节点 ID |
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 出参 `Result<AdministrativeRegionPathVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `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.countyId` | String / null | 父链中的县级 ID;目标高于县级时为 `null` |
|
||||
| `data.path` | Array | 从省级根到目标节点的完整有序父链 |
|
||||
| `data.path[].id` | String | 节点 ID,始终为字符串 |
|
||||
| `data.path[].parentId` | String / null | 父节点 ID;首个省级节点为 `null` |
|
||||
| `data.path[].codeStandard` | String | 编码标准 |
|
||||
| `data.path[].regionCode` | String | 地区代码 |
|
||||
| `data.path[].levelCode` | String | 省/市/县层级代码 |
|
||||
| `data.path[].depth` | Integer | 从 1 开始的连续深度 |
|
||||
| `data.path[].name` | String | 完整名称 |
|
||||
| `data.path[].shortName` | String / null | 简称 |
|
||||
| `data.path[].nodeKind` | String | 节点类型 |
|
||||
| `data.path[].navigable` | Boolean | 是否允许逐级导航 |
|
||||
| `data.path[].selectable` | Boolean | 是否允许作为最终值 |
|
||||
| `data.path[].hasChildren` | Boolean | 是否存在当前有效直接子节点 |
|
||||
| `data.path[].currentlyEffective` | Boolean | 当前是否有效;历史节点可能为 `false` |
|
||||
| `data.path[].status` | String | `ACTIVE` 或 `INACTIVE` |
|
||||
| `data.path[].validFrom` | String | 生效日期 |
|
||||
| `data.path[].validTo` | String / null | 失效日期 |
|
||||
| `data.path[].sortOrder` | Integer | 同级排序号 |
|
||||
| `data.path[].rowVersion` | Integer | 当前 CAS 版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/region/376/path HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <管理员令牌>
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"selectedId": "376",
|
||||
"provinceId": "1",
|
||||
"prefectureId": "35",
|
||||
"countyId": "376",
|
||||
"path": [
|
||||
{
|
||||
"id": "1",
|
||||
"parentId": null,
|
||||
"codeStandard": "GB/T 2260",
|
||||
"regionCode": "110000",
|
||||
"levelCode": "PROVINCE",
|
||||
"depth": 1,
|
||||
"name": "北京市",
|
||||
"shortName": null,
|
||||
"nodeKind": "ADMINISTRATIVE",
|
||||
@@ -78,80 +301,143 @@ Authorization: Bearer <管理员令牌>
|
||||
"status": "ACTIVE",
|
||||
"validFrom": "2025-12-31",
|
||||
"validTo": null,
|
||||
"sortOrder": 100,
|
||||
"sortOrder": 10,
|
||||
"rowVersion": 0
|
||||
}
|
||||
],
|
||||
"defaultId": "35"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
仅返回中国业务当日有效且状态为 `ACTIVE` 的直接子节点,排序稳定为 `sortOrder`、`regionCode`、`id`。停用或历史节点不会进入联动列表,但仍可通过父链接口回显。
|
||||
|
||||
## 2. 父链回显
|
||||
|
||||
```http
|
||||
GET /admin/region/376/path
|
||||
Authorization: Bearer <管理员令牌>
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"selectedId": "376",
|
||||
"provinceId": "1",
|
||||
"prefectureId": "35",
|
||||
"countyId": "376",
|
||||
"path": [
|
||||
{
|
||||
"id": "1",
|
||||
"parentId": null,
|
||||
"levelCode": "PROVINCE",
|
||||
"depth": 1,
|
||||
"name": "北京市",
|
||||
"nodeKind": "ADMINISTRATIVE",
|
||||
"currentlyEffective": true
|
||||
},
|
||||
{
|
||||
"id": "35",
|
||||
"parentId": "1",
|
||||
"codeStandard": "HL_GROUP",
|
||||
"regionCode": "HL-GROUP-110000",
|
||||
"levelCode": "PREFECTURE",
|
||||
"depth": 2,
|
||||
"name": "北京市",
|
||||
"nodeKind": "ADMINISTRATIVE",
|
||||
"currentlyEffective": true
|
||||
"name": "北京市辖区",
|
||||
"shortName": null,
|
||||
"nodeKind": "AGGREGATION",
|
||||
"navigable": true,
|
||||
"selectable": false,
|
||||
"hasChildren": true,
|
||||
"currentlyEffective": true,
|
||||
"status": "ACTIVE",
|
||||
"validFrom": "2025-12-31",
|
||||
"validTo": null,
|
||||
"sortOrder": 2000000001,
|
||||
"rowVersion": 0
|
||||
},
|
||||
{
|
||||
"id": "376",
|
||||
"parentId": "35",
|
||||
"codeStandard": "GB/T 2260",
|
||||
"regionCode": "110101",
|
||||
"levelCode": "COUNTY",
|
||||
"depth": 3,
|
||||
"name": "东城区",
|
||||
"shortName": null,
|
||||
"nodeKind": "ADMINISTRATIVE",
|
||||
"currentlyEffective": true
|
||||
"navigable": true,
|
||||
"selectable": true,
|
||||
"hasChildren": false,
|
||||
"currentlyEffective": true,
|
||||
"status": "ACTIVE",
|
||||
"validFrom": "2025-12-31",
|
||||
"validTo": null,
|
||||
"sortOrder": 20,
|
||||
"rowVersion": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"traceId": "a1b2c3d4-e5f6-7890",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
目标为省级或地级时,尚未到达的快捷层级字段返回 `null`。历史停用节点可返回,但节点自身的 `currentlyEffective=false`,调用方不能把它重新放入当前可选列表。父链存在孤儿、环、越级或超过当前安全深度时,接口失败且不返回部分路径。
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
## 3. 创建节点
|
||||
|
||||
```http
|
||||
POST /admin/region
|
||||
Authorization: Bearer <具有 system:region:create 的管理员令牌>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
创建一个县级业务行政区示例:
|
||||
本接口不返回“成功但空父链”。目标不存在、父链不完整或超过当前三级安全边界时使用错误响应,不返回部分路径。Redis 不可用时整条父链降级为同一数据库事务快照读取,不拼接缓存与数据库的半条路径。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 210801,
|
||||
"message": "行政区划节点不存在",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
父链存在孤儿、越级、循环或异常终止:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 210811,
|
||||
"message": "行政区划父链数据不完整",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `path` 必须从省级根开始,`parentId`、`depth` 和 `levelCode` 逐级连续,最后一个节点必须等于 `selectedId`。
|
||||
- 目标为省级时 `prefectureId`、`countyId` 均为 `null`;目标为地级时只有 `countyId=null`,绝不会返回字符串 `"null"`。
|
||||
- `prefectureId` 只是路径第二层快捷 ID;直辖市等链路可指向 `nodeKind=AGGREGATION && selectable=false` 的聚合节点。
|
||||
- 历史停用或过期节点允许回显,节点自身 `currentlyEffective=false`,且后端会把该节点的 `navigable`、`selectable` 强制返回 `false`;前端只能展示历史值,不能把它重新加入当前可选列表。
|
||||
- 父链异常时整体失败,不返回可被误用的部分路径。
|
||||
- 未登录返回业务码 `401`;不存在节点返回 `210801`。
|
||||
|
||||
---
|
||||
|
||||
### 3. 创建行政区划节点 `POST /admin/region`
|
||||
|
||||
**VO**: `AdministrativeRegionCreateReqVO / String`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
具有 `system:region:create` 权限的管理员可以创建自定义业务行政区、带完整来源证据且不与旧版本重叠的 `GB/T 2260` 新版本,或只用于层级导航、不可作为最终业务值的聚合节点。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `Authorization` | Header | String | ✅ | `Bearer <token>` 且具有 `system:region:create` | 管理端登录令牌 |
|
||||
| `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` |
|
||||
| `nodeKind` | Body | String | ✅ | `ADMINISTRATIVE` 或 `AGGREGATION` | 节点业务类型 |
|
||||
| `name` | Body | String | ✅ | 非空,最多 128 字符 | 完整名称 |
|
||||
| `shortName` | Body | String / null | ❌ | 最多 64 字符;空白归一为 `null` | 简称 |
|
||||
| `navigable` | Body | Boolean | ✅ | 聚合节点固定为 `true` | 是否允许继续向下导航 |
|
||||
| `selectable` | Body | Boolean | ✅ | 聚合节点固定为 `false` | 是否允许作为最终业务值 |
|
||||
| `validFrom` | Body | String | ✅ | `yyyy-MM-dd` | 生效日期 |
|
||||
| `validTo` | Body | String / null | ❌ | `yyyy-MM-dd`,必须严格晚于 `validFrom` | 失效日期;`null` 表示持续有效 |
|
||||
| `status` | Body | String | ✅ | `ACTIVE` 或 `INACTIVE` | 节点状态 |
|
||||
| `sortOrder` | Body | Integer | ✅ | 0~2,000,000,000 | 同级排序号 |
|
||||
| `sourceVersion` | Body | String | ✅ | 非空,最多 64 字符 | 可审计的数据来源版本 |
|
||||
| `sourceRef` | Body | String | ✅ | 非空,最多 512 字符 | 来源 URL、文件定位或管理批次标识 |
|
||||
| `extension` | Body | Object / null | ❌ | 必须是 JSON object,序列化后最多 4000 字符 | 结构化扩展;旧入参别名 `extJson` 仍兼容 |
|
||||
|
||||
`depth`、操作人和来源校验值由服务端派生,禁止由前端提交。
|
||||
|
||||
#### 出参 `Result<String>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `code` | Integer | `200` 表示创建成功 |
|
||||
| `message` | String | 响应说明 |
|
||||
| `success` | Boolean | 创建成功为 `true` |
|
||||
| `traceId` | String / null | 链路追踪 ID;报错排查时提供给后端 |
|
||||
| `data` | String | 新节点 ID,始终为 JSON string |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /admin/region HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <具有 system:region:create 的管理员令牌>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"codeStandard": "HL_CUSTOM",
|
||||
"regionCode": "DEMO-COUNTY-001",
|
||||
@@ -174,28 +460,94 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
成功响应的 `data` 是字符串 ID:
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": "2090000000000000001"
|
||||
"data": "2090000000000001001",
|
||||
"traceId": "a1b2c3d4-e5f6-7890",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
`extension` 只能是 JSON 对象;历史入参名 `extJson` 仅作为请求别名兼容,响应统一使用 `extension`。服务端会记录来源版本、来源定位、操作人和来源校验值,并在事务提交后刷新行政区划独立缓存。
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
## 4. 更新节点
|
||||
本接口成功时一定返回字符串 ID,不存在 `data=null` 的成功语义。失败时事务回滚且不创建节点;本接口没有“Redis 不可用仍受理写入”的公开降级承诺。
|
||||
|
||||
```http
|
||||
PUT /admin/region/2090000000000000001
|
||||
Authorization: Bearer <具有 system:region:update 的管理员令牌>
|
||||
Content-Type: application/json
|
||||
```
|
||||
#### 错误响应
|
||||
|
||||
层级代码与父链派生深度不一致:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 210813,
|
||||
"message": "行政区划层级代码与父链深度不一致",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 当前最大写入深度为 3;省、市、县层级必须分别使用 `PROVINCE`、`PREFECTURE`、`COUNTY`。
|
||||
- 父节点必须存在、可导航、有效期完整覆盖子节点,并且不会让写入深度超过三级。
|
||||
- `AGGREGATION` 必须同时满足 `codeStandard=HL_GROUP`、`navigable=true`、`selectable=false`;它只能导航,不能作为最终业务值。
|
||||
- `ADMINISTRATIVE` 不得占用 `HL_GROUP`;除 `HL_GROUP`、`HL_INTERNAL` 的限制外,`codeStandard` 不是封闭枚举,自定义命名空间可扩展,推荐使用 `HL_CUSTOM`。
|
||||
- 旧命名空间 `HL_INTERNAL` 不允许产生新写入;`GB/T 2260` 只允许六位数字 `regionCode`,并要求完整的 `sourceVersion`、`sourceRef`。
|
||||
- 同一 `codeStandard + regionCode` 的版本有效期不得重叠;有效期采用左闭右开 `[validFrom, validTo)`。
|
||||
- 缺少权限返回 `210802`;空请求或格式错误返回 `400`;全部失败路径零业务写入。
|
||||
|
||||
---
|
||||
|
||||
### 4. 更新行政区划节点 `PUT /admin/region/{id}`
|
||||
|
||||
**VO**: `AdministrativeRegionUpdateReqVO / Void`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
具有 `system:region:update` 权限的管理员更新节点名称、父级、层级、可导航/可选状态、有效期、排序和扩展信息。请求必须携带完整可变字段快照和最近读取到的 `rowVersion`;这不是局部 PATCH。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `Authorization` | Header | String | ✅ | `Bearer <token>` 且具有 `system:region:update` | 管理端登录令牌 |
|
||||
| `id` | Path | String | ✅ | 十进制正整数 | 要更新的节点 ID |
|
||||
| `parentId` | Body | String / null | ❌ | 十进制正整数;省级根节点为 `null` | 更新后的父节点 ID |
|
||||
| `levelCode` | Body | String | ✅ | 1~32 字符;必须匹配更新后父链深度 | 省/市/县层级代码 |
|
||||
| `name` | Body | String | ✅ | 非空,最多 128 字符 | 完整名称 |
|
||||
| `shortName` | Body | String / null | ❌ | 最多 64 字符;空白归一为 `null` | 简称 |
|
||||
| `navigable` | Body | Boolean | ✅ | 聚合节点必须保持 `true` | 是否允许继续向下导航 |
|
||||
| `selectable` | Body | Boolean | ✅ | 聚合节点必须保持 `false` | 是否允许作为最终业务值 |
|
||||
| `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 字符;省略与显式 `null` 都会清空 | 结构化扩展;旧别名 `extJson` 仍兼容 |
|
||||
| `rowVersion` | Body | Integer | ✅ | 大于等于 0,必须等于当前版本 | CAS 并发版本,成功后自动加 1 |
|
||||
|
||||
`codeStandard`、`regionCode`、`nodeKind`、`sourceVersion` 和 `sourceRef` 不属于更新接口,不能通过请求改写。
|
||||
|
||||
#### 出参 `Result<Void>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `code` | Integer | `200` 表示更新成功 |
|
||||
| `message` | String | 成功时为“行政区划更新成功” |
|
||||
| `success` | Boolean | 更新成功为 `true` |
|
||||
| `traceId` | String / null | 链路追踪 ID;报错排查时提供给后端 |
|
||||
| `data` | null | 更新接口不返回业务数据 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
PUT /admin/region/2090000000000001001 HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <具有 system:region:update 的管理员令牌>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentId": "35",
|
||||
"levelCode": "COUNTY",
|
||||
@@ -214,77 +566,312 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "行政区划更新成功",
|
||||
"success": true,
|
||||
"data": null
|
||||
"data": null,
|
||||
"traceId": "a1b2c3d4-e5f6-7890",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
更新采用完整可变字段加 `rowVersion` 的 CAS 语义。成功后版本号递增;并发版本过期返回 `210807`,不会覆盖另一位管理员的提交。编码标准、地区编码、节点类型和来源证据不可通过更新接口改写。
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
已到生效日的 `GB/T 2260` 法定节点不能原位修改历史属性;只能保持其他字段不变,将旧版本关闭,再创建不重叠的新版本。有子节点的分支不能直接移动或改变层级,父节点的状态、导航能力和有效期必须持续覆盖当前或未来仍会生效的子版本。
|
||||
本接口成功时 `data=null` 是固定契约,调用方以 `code=200 && success=true` 判断成功。本接口没有“Redis 不可用仍受理写入”的公开降级承诺,也不会返回更新后的节点快照。
|
||||
|
||||
## 字段说明
|
||||
#### 错误响应
|
||||
|
||||
| 字段 | JSON 类型 | 说明 |
|
||||
提交了已经过期的 `rowVersion`:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 210807,
|
||||
"message": "行政区划已被其他操作更新,请刷新后重试",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 更新采用完整可变字段快照,不是局部 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` 法定节点不能原位改写历史属性;代码只允许在其他历史字段(包含 `extension`)完全不变时关闭状态/结束时间。由于当前读接口不返回 `extension`,前端不能把“关闭已有法定版本”当成已具备的安全通用操作。
|
||||
- 成功后数据库中的 `rowVersion` 增加 1,但响应不返回新版本;再次编辑前需重新读取可见节点,且要考虑提交后缓存异步刷新的短暂窗口。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### 三级联动新建流程
|
||||
|
||||
```text
|
||||
GET /admin/region/children
|
||||
→ 用户选择 provinceId
|
||||
GET /admin/region/children?parentId={provinceId}
|
||||
→ 用户选择第二层导航项 prefectureId(可能是不可提交的 AGGREGATION)
|
||||
GET /admin/region/children?parentId={prefectureId}
|
||||
→ 用户选择 countyId
|
||||
→ 业务表单最终保存 countyId(或业务允许的上级 selectable 节点)
|
||||
```
|
||||
|
||||
### 编辑历史值回显流程
|
||||
|
||||
```text
|
||||
GET /admin/region/{savedRegionId}/path
|
||||
→ 读取 provinceId / prefectureId / countyId
|
||||
→ 按 path 顺序逐层调用 children
|
||||
→ 每层把对应历史 ID 作为 selectedId
|
||||
→ 历史节点 currentlyEffective=false 时只展示历史值,不重新作为当前选项提交
|
||||
```
|
||||
|
||||
### ✅ 正确 / ❌ 错误调用对照
|
||||
|
||||
| 场景 | 调用或 payload |
|
||||
|---|---|
|
||||
| ✅ 加载省级 | `GET /admin/region/children`,省略 `parentId` |
|
||||
| ✅ 加载市级 | `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` 时仍提交为业务行政区 |
|
||||
| ❌ 强转 number | `Number("2090000000000000376")` 会丢失精度 |
|
||||
|
||||
### 统一响应判断
|
||||
|
||||
```javascript
|
||||
if (response.code !== 200 || response.success !== true) {
|
||||
throw new Error(response.message || '行政区划接口调用失败');
|
||||
}
|
||||
```
|
||||
|
||||
禁止只判断 HTTP status;业务错误可能使用 HTTP 200 返回。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
| 前端操作 | 外部可观察的持久化行为 | 失败行为 |
|
||||
|---|---|---|
|
||||
| `id`、`parentId` | string / null | 行政区划节点与父节点 ID;响应始终为字符串 |
|
||||
| `codeStandard` | string | `GB/T 2260`、`HL_CUSTOM` 或聚合节点专用 `HL_GROUP` |
|
||||
| `regionCode` | string | 同一编码标准内的业务标识;`GB/T 2260` 必须为六位数字 |
|
||||
| `levelCode` | string | 当前三级固定为 `PROVINCE`、`PREFECTURE`、`COUNTY` |
|
||||
| `nodeKind` | string | `ADMINISTRATIVE` 或 `AGGREGATION` |
|
||||
| `navigable` | boolean | 是否可继续逐级导航 |
|
||||
| `selectable` | boolean | 是否可作为最终业务值 |
|
||||
| `hasChildren` | boolean | 是否存在当前有效、启用的直接子节点 |
|
||||
| `currentlyEffective` | boolean | 节点在中国业务当日是否处于有效时间窗且为 `ACTIVE` |
|
||||
| `validFrom`、`validTo` | string / null | `yyyy-MM-dd`;结束日为开区间 |
|
||||
| `status` | string | `ACTIVE` 或 `INACTIVE` |
|
||||
| `rowVersion` | integer | 更新 CAS 版本;创建后从 0 开始 |
|
||||
| 查询 `children` | 只读,不改变节点、版本或审计 | 不写入 |
|
||||
| 查询 `path` | 只读,不改变节点、版本或审计 | 不写入,不返回部分父链 |
|
||||
| 创建节点 | 成功新增一个节点,初始 `rowVersion=0`;数据库事务提交后异步刷新节点和父级列表缓存 | 参数、权限、父链、有效期或唯一性失败时不新增节点 |
|
||||
| 更新节点 | 成功更新一个节点并令 `rowVersion+1`;数据库事务提交后异步刷新节点、旧/新父级及上层列表缓存 | 版本冲突或业务 guard 失败时原节点保持不变 |
|
||||
|
||||
## 业务错误码
|
||||
写接口成功只证明数据库事务已经提交,不承诺紧随其后的第一个缓存读取已经反映新值。前端不得自行操作缓存;需要立即回显时应短暂重读,并始终以新的 `rowVersion` 为准。读取阶段 Redis 故障会降级数据库;写入口还受公共幂等保护,不能据此推导“Redis 故障时写入一定成功”。
|
||||
|
||||
| code | message | 典型场景 |
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录:业务码 `401`。
|
||||
- 已登录但缺少创建/更新专用权限:`210802`。
|
||||
- Query/Path ID 为 0、负数或非十进制正整数:`400`。
|
||||
- 父节点或目标节点不存在:`210801`。
|
||||
- 父级不可导航、有效期不能覆盖子级:`210803`。
|
||||
- 写入超过当前三级边界:`210804`。
|
||||
- 自引用或父链成环:`210805`。
|
||||
- 当前编码或版本冲突:`210806` / `210815`。
|
||||
- `rowVersion` 过期:`210807`,零写入。
|
||||
- `validTo` 不晚于 `validFrom`:`210808`。
|
||||
- 父链异常:`210811`,不返回部分路径。
|
||||
- `extension` 不是 JSON object 或过长:`210812`。
|
||||
- 层级代码与父链深度不一致:`210813`。
|
||||
- 写接口命中五秒参数幂等键:`100502`;这是拒绝重复提交,不会回放第一次成功响应。
|
||||
- 老数据兼容:停用历史节点可由 `path` 回显;旧缓存中的数字 ID 可由后端读取,但当前 API 响应始终输出 string ID。
|
||||
|
||||
### 业务错误码
|
||||
|
||||
| code | message | 典型接口与场景 |
|
||||
|---:|---|---|
|
||||
| `210801` | 行政区划节点不存在 | 查询不存在的父级或父链目标 |
|
||||
| `210802` | 无行政区划维护权限 | 创建或更新缺少细粒度权限 |
|
||||
| `210803` | 行政区划父节点不合法 | 父级不可导航或有效期不能覆盖子级 |
|
||||
| `210804` | 行政区划层级超过当前允许深度 | 写入超过当前三级边界 |
|
||||
| `210805` | 行政区划父链存在循环 | 自引用或移动后形成祖先环 |
|
||||
| `210806` | 行政区划当前编码已存在 | 当前唯一键冲突 |
|
||||
| `210807` | 行政区划已被其他操作更新,请刷新后重试 | `rowVersion` 过期 |
|
||||
| `400` | 参数校验失败文案 | 任一接口的 ID 非正整数、Body 缺少必填字段或格式不合法 |
|
||||
| `401` | 未认证文案 | 任一接口未携带有效管理端身份 |
|
||||
| `100502` | 请勿重复提交 | `create` / `update` 在五秒内命中相同参数幂等键;失败不会返回第一次结果 |
|
||||
| `210801` | 行政区划节点不存在 | `children` 父节点或 `path`/`update` 目标不存在 |
|
||||
| `210802` | 无行政区划维护权限 | `create` / `update` 缺少专用写权限 |
|
||||
| `210803` | 行政区划父节点不合法 | 父级不可导航、已失效或有效期不能覆盖子级 |
|
||||
| `210804` | 行政区划层级超过当前允许深度 | `create` / `update` 试图写入第四级 |
|
||||
| `210805` | 行政区划父链存在循环 | `update` 自引用或移动后形成祖先环 |
|
||||
| `210806` | 行政区划当前编码已存在 | `create` 唯一编码冲突 |
|
||||
| `210807` | 行政区划已被其他操作更新,请刷新后重试 | `update.rowVersion` 已过期 |
|
||||
| `210808` | 行政区划有效期不合法 | `validTo` 不晚于 `validFrom` |
|
||||
| `210809` | 该行政区划编码标准为系统保留值 | 新写入继续使用旧 `HL_INTERNAL` |
|
||||
| `210810` | 存在子节点的行政区划不能移动或变更层级 | 分支移动或改层级 |
|
||||
| `210811` | 行政区划父链数据不完整 | 孤儿、越级、环或异常终止 |
|
||||
| `210812` | 行政区划扩展字段必须是合法 JSON | `extension` 不是 JSON 对象 |
|
||||
| `210813` | 行政区划层级代码与父链深度不一致 | 省市县代码与派生深度不匹配 |
|
||||
| `210814` | 行政区划更新与现有子节点状态或有效期冲突 | 父级提前停用、取消导航或缩短窗口 |
|
||||
| `210815` | 行政区划编码的版本有效期与既有数据重叠 | 同编码版本时间窗重叠 |
|
||||
| `210816` | 行政区划节点类型或编码命名空间不合法 | 聚合节点类型、命名空间或可选性组合错误 |
|
||||
| `210817` | 已生效法定行政区划只能关闭旧版本后新增 | 原位改写生效中的法定节点 |
|
||||
| `210818` | 行政区划数据来源信息不完整 | 缺少来源版本或定位 |
|
||||
| `210819` | 法定行政区划代码必须为六位数字 | `GB/T 2260` 新版本编码格式错误 |
|
||||
| `210810` | 存在子节点的行政区划不能移动或变更层级 | `update` 移动分支或改变分支层级 |
|
||||
| `210811` | 行政区划父链数据不完整 | `path` 遇到孤儿、越级、循环或异常终止 |
|
||||
| `210812` | 行政区划扩展字段必须是合法 JSON | `extension` 不是 JSON object 或内容过长 |
|
||||
| `210813` | 行政区划层级代码与父链深度不一致 | 省/市/县代码与派生深度不匹配 |
|
||||
| `210814` | 行政区划更新与现有子节点状态或有效期冲突 | 父级提前停用、取消导航或缩短有效期 |
|
||||
| `210815` | 行政区划编码的版本有效期与既有数据重叠 | 同编码的两个版本时间窗重叠 |
|
||||
| `210816` | 行政区划节点类型或编码命名空间不合法 | `nodeKind`、`codeStandard`、可导航/可选组合错误 |
|
||||
| `210817` | 已生效法定行政区划只能关闭旧版本后新增 | 原位改写生效中的 `GB/T 2260` 节点 |
|
||||
| `210818` | 行政区划数据来源信息不完整 | `sourceVersion` 或 `sourceRef` 缺失/过长 |
|
||||
| `210819` | 法定行政区划代码必须为六位数字 | `GB/T 2260` 的 `regionCode` 格式错误 |
|
||||
|
||||
参数格式错误继续使用统一参数错误响应,例如未登录返回业务码 `401`,`parentId=0` 或空创建请求返回业务码 `400`。
|
||||
---
|
||||
|
||||
## 兼容与接入建议
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
- 三级选择器首次加载调用不带 `parentId` 的 `children`;选择省、市后分别以当前 ID 继续查询下一层。
|
||||
- 编辑历史数据时先调用 `/{id}/path` 得到完整选中链,再逐层加载 `children`;不要根据行政区编码截位推断父子关系。
|
||||
- `AGGREGATION` 节点可能可导航但不可选,页面应分别使用 `navigable` 和 `selectable`,不要只根据 `hasChildren` 判断。
|
||||
- 客户端必须把响应 ID 当字符串保存和比较;请求路径与查询参数可以继续发送十进制字符串。
|
||||
- 旧行政区划缓存中的数字 ID 与新缓存中的字符串 ID 均可被后端读取,滚动部署期间无需调用方切换缓存版本。
|
||||
### `levelCode`(行政区划层级)
|
||||
|
||||
## 验证证据
|
||||
**所属字段**: `AdministrativeRegionCreateReqVO.levelCode`、`AdministrativeRegionUpdateReqVO.levelCode`、`AdministrativeRegionItemVO.levelCode` | **类型**: `String`
|
||||
|
||||
- PR #6359、#6387、#6393、#6398、#6402、#6412 均已合并 `dev-v3`,六个合并提交都包含在 TEST 精确提交 `b62054035a139e3c689179ef75e2d052d08dde52` 中。
|
||||
- Deploy Panel API 任务 `52771074` 部署 `hl-user-service`,任务 `b1db24b2` 部署 `hl-gateway`;两项脚本均为 `/opt/hulalv/scripts/deploy-backend.sh`,终态 `success`、退出码 0,双实例、Nacos 健康注册、滚动采样与部署窗口日志检查通过。
|
||||
- 真实 TEST 管理员经 Gateway 验证未认证返回 401、根级/子级联动、父链回显、失败零写入、创建字符串 ID、数据库落行、Redis 回填、CAS 更新和更新后缓存一致性。
|
||||
- 验收创建的唯一测试行已精确删除;数据库总量与测试命名空间恢复基线,四个 Redis 快照按原值和绝对过期时间恢复,缓存锁释放,登录会话注销。操作审计按系统设计保留。
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `PROVINCE` | 省级 | 深度 1,父节点必须为 `null` |
|
||||
| `PREFECTURE` | 地级 | 深度 2,父节点必须是省级 |
|
||||
| `COUNTY` | 县级 | 深度 3,父节点必须是地级 |
|
||||
|
||||
### `nodeKind`(节点类型)
|
||||
|
||||
**所属字段**: `AdministrativeRegionCreateReqVO.nodeKind`、`AdministrativeRegionItemVO.nodeKind` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `ADMINISTRATIVE` | 行政区节点 | 可按 `selectable` 决定是否作为最终业务值;不得使用 `HL_GROUP` |
|
||||
| `AGGREGATION` | 导航聚合节点 | 必须使用 `HL_GROUP`、`navigable=true`、`selectable=false` |
|
||||
|
||||
### `status`(节点状态)
|
||||
|
||||
**所属字段**: 创建/更新请求及节点响应的 `status` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `ACTIVE` | 启用 | 位于有效期时可进入当前联动列表 |
|
||||
| `INACTIVE` | 停用 | 不进入当前联动列表,但历史父链仍可回显 |
|
||||
|
||||
### `codeStandard`(可扩展编码命名空间)
|
||||
|
||||
**所属字段**: `AdministrativeRegionCreateReqVO.codeStandard`、`AdministrativeRegionItemVO.codeStandard` | **类型**: `String`
|
||||
|
||||
`codeStandard` 不是封闭枚举。服务端会去除首尾空白并转成大写;下表仅列出内置/保留语义:
|
||||
|
||||
| 值或类别 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `GB/T 2260` | 法定行政区编码 | `regionCode` 必须为六位数字;生效历史受不可变保护 |
|
||||
| `HL_CUSTOM` | 自定义行政区编码 | 管理端创建普通自定义行政区的推荐命名空间 |
|
||||
| `HL_GROUP` | 导航聚合编码 | 只能与 `AGGREGATION` 搭配 |
|
||||
| `HL_INTERNAL` | 旧内部命名空间 | 仅兼容识别旧缓存,新写入固定拒绝并返回 `210809` |
|
||||
| 其他非空值 | 扩展命名空间 | 最多 32 字符;可供 `ADMINISTRATIVE` 使用,但不得冒充 `HL_GROUP` |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
本条为新增接口,没有需要兼容的旧 `/admin/region/**` 公开接口。补充工单 #6409 在正式前端接入前固定了 ID 输出类型:
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 初始实现风险 | 当前已部署契约 |
|
||||
|---|---|---|
|
||||
| 所有行政区划 Long ID | 小数值可能被全局序列化规则输出为 JSON number | 一律输出 JSON string |
|
||||
| 缺失父级/层级 | 可能被调用方误当字符串处理 | 保持真正的 JSON `null` |
|
||||
| `extension` 历史入参 | 旧调用方可能使用 `extJson` | 请求继续兼容 `extJson`,当前文档统一使用 `extension` |
|
||||
| TEST 北京第二层示例 | 曾被文档误写为可选择的 `GB/T 2260` 地级节点 | 实际为 `id="35"`、`HL_GROUP`、`AGGREGATION`、`selectable=false` 的导航分组 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 接入前 | 当前已部署行为 |
|
||||
|---|---|---|
|
||||
| 三级联动 | 无统一公开接口,容易截行政区编码推断 | 逐级调用 `children`,以后端父子关系为准 |
|
||||
| 编辑回显 | 调用方自行拼装父链 | 调用 `path` 返回完整有序父链和三级快捷 ID |
|
||||
| 并发更新 | 无本接口旧语义 | 必须提交 `rowVersion`,冲突返回 `210807` 且零写入 |
|
||||
| 写后读取 | 文档曾表述为提交后立即可见 | 代码为事务提交后异步刷新缓存,存在短暂最终一致窗口 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否;这是新增接口,且正式前端接入前已经固定字符串 ID 契约。
|
||||
- **前端接入状态**: `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 现有业务接口;
|
||||
- 已保存业务数据中的行政区 ID;
|
||||
- 登录、权限菜单和非行政区划缓存;
|
||||
- 生产环境(本 Changelog 只证明 TEST 已部署和验收)。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
```text
|
||||
GET /admin/region/children → 根级/子级列表、defaultId、string/null ID ✓
|
||||
GET /admin/region/{id}/path → 三级完整父链、缺失层级 null、string ID ✓
|
||||
POST /admin/region → 创建成功返回 string ID,数据库与缓存读取一致 ✓
|
||||
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 更新、持久化结果和缓存一致性验收。
|
||||
- 验收创建的临时节点已精确清理;数据库数量与测试命名空间恢复基线,四个缓存快照按原值和绝对过期时间恢复,登录会话已注销;操作审计按系统设计保留。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|---|---|---|---|
|
||||
| #6359 | #6350 | 行政区划主数据、三级写入与读接口基础实现 | ✅ 有效 |
|
||||
| #6387 | #6384 | 权限、来源和节点类型契约补强 | ✅ 有效 |
|
||||
| #6393 | #6389 | 有效期、父链和历史版本保护补强 | ✅ 有效 |
|
||||
| #6398 | #6395 | Gateway 路由与认证策略补齐 | ✅ 有效 |
|
||||
| #6402 | #6399 | 缓存一致性与滚动兼容补强 | ✅ 有效 |
|
||||
| #6412 | #6409 | 所有行政区划响应 ID 固定为 JSON string | ✅ 有效 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 主 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)
|
||||
|
||||
---
|
||||
|
||||
## 撤回
|
||||
|
||||
代码撤回需回退上述六个 PR 并按依赖逆序重新部署 `hl-gateway` 与 `hl-user-service`;调用方停止访问 `/admin/region/**`,并经 Gateway 验证新增路由已不可达、既有 User 接口正常。数据库迁移已经在 TEST 应用,回退代码时保留行政区划表和权限元数据,不执行降版 DDL;Redis 仅在确需重建时精确清理 `hl:user:region:v1:` 命名空间,禁止触碰登录及其他业务缓存。无 MQ 或跨服务写入需要补偿。
|
||||
本次 #6438 代码优先核对只修改 Changelog,不改变后端制品。若本次文档修正需要撤回,从最新 `hl-api-changelog/main` 创建独立分支,revert 本次文档合并提交并重新运行仓库全部校验;TEST 行政区划 API、数据库、配置、Redis 和 MQ 均不需要回退。撤回后前端必须以 TEST 对应提交的 Controller、VO、Service 与 migration 为准,不能恢复使用被本次指出的错误聚合节点示例或同步缓存假设。
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **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)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @lc
|
||||
|
||||
@@ -7,11 +7,11 @@ author: "lc(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "49b071b0"
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
verified_at: "2026-08-26"
|
||||
status_note: "PR #6403 已合并 dev-v3;两个状态命令已部署 TEST,ACTIVE→SUSPENDED、ACTIVE/SUSPENDED→BLACKLIST、原因审计、权限、并发、幂等和失败零写入均经真实 Gateway 验证。前端待接入原因弹窗与并发版本。"
|
||||
updated_at: "2026-08-26"
|
||||
base: "dev-v3"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6397"
|
||||
title: "供应商注册合同聚合信息"
|
||||
title: "供应商注册合同聚合信息(已废弃)"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
@@ -10,32 +10,44 @@ gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
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"
|
||||
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: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "c91163fc"
|
||||
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` | 配房完成(房)/ 配车完成(车) | 已完成(房/车分文案,#6921) |
|
||||
| `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,338 @@
|
||||
---
|
||||
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: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "c91163fc"
|
||||
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(契约修正)
|
||||
|
||||
## 十一、复审补充知会(2026-09-01)
|
||||
|
||||
> 复审轮对现行行为的口径确认,无新接口、无字段结构变更。
|
||||
|
||||
1. **排序口径不一致(暂行,待拍板)**:001 看板列表按 `create_time` **倒序**(既有行为未变),009 统计条 / 008 导出按 `depart_date` **升序**——「导出件顺序 ≠ 列表页顺序」当前属预期;001 是否改 `depart_date ASC` 待 wx 确认(决策点③),确认后另行通知对齐。
|
||||
2. **008 当前忽略 `opsStage`**:本期 008 只收 productId/month/keyword,契约卡的 opsStage 筛选由返工 #6926 补上。**#6926 上线后:桶筛选态点「导出表格」必须带上当前 `opsStage`**,否则导出为全量(当前行为:传了也被静默忽略)。
|
||||
3. 同随 #6926 上线:009 自校验日志修正(内部可观测性,前端无感)、month 非法值容错跳过(不再 500)。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#6904](https://git.1814.love:8443/wx/HL/issues/6904)
|
||||
- **PR**: #6917
|
||||
- **Merge commit**: 9f689354e
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx(GIT)
|
||||
@@ -0,0 +1,467 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6905"
|
||||
title: "团期看板 4 接口对齐补全 GB-ADM-000/001/002/003"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "f6b849eb"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-02"
|
||||
status_note: "regionText 由上游 product-v2 提供后透传,当前测试环境返回 null 属预期;001 排序保持 create_time DESC 未变(depart_date ASC 变更待 wx 确认);#6929 已实现:003 totalPrice 应收字段 + birthdayInTrip 跨年修正(详见 §十一)"
|
||||
updated_at: "2026-09-04"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期看板 4 接口对齐补全 GB-ADM-000/001/002/003
|
||||
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #6923(网关拦截 PUT 无法 API 合并,采用等价 squash 推送落地 dev-v3,PR 已 closed 留痕)
|
||||
> **Issue**: #6905
|
||||
> **日期**: 2026-09-01
|
||||
> **影响范围**: 管理后台团期看板 4 个查询接口(产品列表/团期分页/团期详情/团期订单列表)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
- 本单**只做查询接口的字段/筛选/性能对齐**,未新建任何表/列,未新增写操作。
|
||||
- 运营阶段映射为团期八态(opsStage 桶+八态并集),本单**不新增 ops_stage 字段/表**。
|
||||
- **证件号与游客手机号永不返回**(编译期字段裁剪 + 运行期实测零返回),前端不得依赖。
|
||||
- contactPhone 一期**全量掩码**(保前 3 后 4,如 138****0001)。
|
||||
- 001 排序**保持 create_time DESC**(将 depart_date 升序的变更留待 wx 确认,PR 与进度中已注明)。
|
||||
- regionText 由上游 product-v2 提供(order 侧仅透传),当前上游未提供时返回 null,**产品域交接后由前端组验收展示**。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
团期看板已实现 4 个查询接口(GB-ADM-000/001/002/003,见 #6902 共享件),本单按验收清单补齐全量字段、真实来源取值、批量聚合与筛选参数,消除 N+1。依赖 #6902(已合并 dev-v3)。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 产品团期看板(000) | GET | `/v3/admin/order/group-batch/products` | 新增响应字段+入参 | batchCount/regionText + productType 筛选 |
|
||||
| 2 | 团期分页列表(001) | GET | `/v3/admin/order/group-batch` | 新增筛选+响应字段 | opsStage/month/keyword + chips/金额/orderCount |
|
||||
| 3 | 团期详情(002) | GET | `/v3/admin/order/group-batch/<groupBatchId>` | 金额实时聚合+reporter | totalReceivable/totalReceived 实时、primaryReporter 批量 |
|
||||
| 4 | 团期订单列表(003) | GET | `/v3/admin/order/group-batch/<groupBatchId>/orders` | 新增批量派生字段 | 成本/tier/人数/房车需求/掩码手机/游客(证件零返回) |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 产品团期看板 `GET /v3/admin/order/group-batch/products`
|
||||
|
||||
**VO**: `GroupBatchProductItemRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台看板"产品列表"页:按产品展示其团期批量数。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| productType | Query | String | 否 | - | 产品类型筛选;不传=只看团期产品(上游恒定 GROUP 范围,详见「十一、复审补充知会」第 2 条勘误) |
|
||||
| keyword | Query | String | 否 | - | 产品名关键词(原有行为不变) |
|
||||
|
||||
#### 出参 `Result<GroupBatchProductItemRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| batchCount | Integer | 该产品下活跃团期数(一次性 in 投影 product_id 后内存分组计数,无逐产品 count) |
|
||||
| regionText | String | 产品地域文本(上游 product-v2 提供后透传;当前上游未提供时 null) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/products?productType=&keyword=
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
- 无数据 → 空数组,不报错。
|
||||
- 未登录 → 401(网关拦截)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "未登录或登录已过期",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 未传筛选 = 不过滤,等价旧行为;regionText 为 null 时不阻断列表。
|
||||
|
||||
### 2. 团期分页列表 `GET /v3/admin/order/group-batch`
|
||||
|
||||
**VO**: `GroupBatchPageItemRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台看板"团期列表"页:分页 + 筛选(阶段桶/月份/关键词)。
|
||||
|
||||
#### 入参(新增,均可选)
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| opsStage | Query | String | 否 | RECRUIT/FORMED/PENDING_TRIP/TRAVELLING/AUDITING/CHECKED/DISBANDED | 七桶折叠;FORMED=RRESOURCE_PREPARING+MATERIAL_PREPARING 复合桶;未知/空=不过滤 |
|
||||
| month | Query | String | 否 | yyyy-MM | 按出发日期所在自然月区间过滤 |
|
||||
| keyword | Query | String | 否 | - | 匹配团期编号/名称,服务端 trim+转义 % _ \\(防通配符扩匹配) |
|
||||
|
||||
#### 出参 `Result<PageResult<GroupBatchPageItemRespVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| receivableAmount | BigDecimal | 应收(批量 sum 实时聚合,非快照) |
|
||||
| receivedAmount | BigDecimal | 实收(批量 sum 实时聚合,非快照) |
|
||||
| chips | Object | 六芯片键:hotel/vehicle/guide/photo/contract/insurance(#6902 GroupBatchChipResolver 聚合) |
|
||||
| orderCount | Integer | 该团期子订单数(批量 in 投影计数,无 N+1) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch?pageNo=1&pageSize=5&opsStage=FORMED&month=2026-09&keyword=x
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"records": [],
|
||||
"total": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"records": [],
|
||||
"total": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 无匹配 → records 空数组 total=0。
|
||||
- 排序保持 create_time DESC(向后兼容,未做排序变更)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "未登录或登录已过期",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- opsStage 非法/未知 = 不过滤(保守向后兼容);month 格式非法 → 业务 400。
|
||||
|
||||
### 3. 团期详情 `GET /v3/admin/order/group-batch/<groupBatchId>`
|
||||
|
||||
**VO**: `GroupBatchDetailRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
看板点击团期看详情:实时金额 + 主报道人。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | - | 团期 ID(路径参数) |
|
||||
|
||||
#### 出参 `Result<GroupBatchDetailRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| totalReceivable | BigDecimal | 应收实时聚合(sumBatchAmountsByProductBatchIds),非恒 0 |
|
||||
| totalReceived | BigDecimal | 实收实时聚合,非恒 0 |
|
||||
| primaryReporterId/primaryReporterName | Long/String | order_batch_staff 中 reporter_rank=PRIMARY 的首个;无 PRIMARY 配置时 null |
|
||||
| secondaryReporterId/Name | Long/String | 副报道人(可 null) |
|
||||
| advanceAmount | BigDecimal | 预付款(原字段,语义不变) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/<groupBatchId>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
- 团期不存在 → 业务 404。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "团期不存在或已被删除",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- PRIMARY 报道人未配置时两 reporter 字段为 null,不降级不报错。
|
||||
|
||||
### 4. 团期订单列表 `GET /v3/admin/order/group-batch/<groupBatchId>/orders`
|
||||
|
||||
**VO**: `GroupBatchOrderItemRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
看板展开订单列表:成本/tier/人数/房车需求/游客(含旅行内生日、无证件号)。
|
||||
|
||||
#### 入参(新增,均可选)
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| includeTravelers | Query | Boolean | 否 | - | 是否加载游客数组(不传=不加载,零开销) |
|
||||
| includeNeeds | Query | Boolean | 否 | - | 是否加载房车需求做派生聚合 |
|
||||
| includeCancelled | Query | Boolean | 否 | - | 是否包含已取消子订单 |
|
||||
|
||||
#### 出参 `Result<List<GroupBatchOrderItemRespVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| estimatedCost | BigDecimal | 估算成本(无成本数据可 null) |
|
||||
| totalPrice | BigDecimal(字符串) | 本户应收 = orderAmount + surchargeAmount − discountAmount(下限 0,`OrderAmountUtil.payableForDisplay` 口径;**取消单返 "0.00"**,#7097 已补两位小数);预计毛利 = totalPrice − estimatedCost(#6929) |
|
||||
| tierCode/tierName | String | tier 组合(成人A/儿童C/幼童Y/婴儿B,如 2A1C→"2成人1儿童";全零→null,映射表待 wx 确认) |
|
||||
| participantCount | Integer | 人数聚合(adult+child+youngChild+baby) |
|
||||
| youngChildCount/babyCount | Integer | 幼童/婴儿数 |
|
||||
| roomCount | Integer | 房数=逐晚需求 segment 房间数最大值;无需求行缺省 ceil(人数/2) |
|
||||
| roomType | String | 最大房数晚的 roomCategory "、" 连接;无 → null |
|
||||
| specialNeeds | String | 需求 special_tags ";" 连接 → 需求行 remark → 缺需求行 customer_remark |
|
||||
| contactPhone | String | **全量掩码**(保前 3 后 4) |
|
||||
| travelerInfoComplete | Boolean | 游客信息完整(校验服务批量聚合) |
|
||||
| travelers | Array | name/type/age/birthdayInTrip;**不含 idNo/phone(编译期字段裁剪 + 运行期零返回)** |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/<groupBatchId>/orders?includeTravelers=true&includeNeeds=true&includeCancelled=true
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
- 无子订单 → 空数组。
|
||||
- 证件号/游客手机号永不返回(数据安全边界)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "未登录或登录已过期",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- include* 参数不传 = 不加载对应数据(零开销,向后兼容)。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- 全部新增参数可选;不传 = 旧行为(向后兼容)。
|
||||
- `opsStage=FORMED` 代表"资源筹备中+物料筹备中"两态并集(复合桶),不是单一状态值。
|
||||
- 金额一律 BigDecimal 字符串(ToStringSerializer 序列化),前端按字符串处理,避免精度丢失。
|
||||
- 001 keyword 的 `%_` 会被转义为字面匹配;需要模糊查询请用 `%` 以外的普通字符。
|
||||
- 003 的证件号/手机号**不存在于任何响应**,前端不得引用(编译期保证)。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 本单**零写操作、零表/列变更、零迁移**;全部为查询层字段/聚合/筛选对齐。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截)
|
||||
- 资源不存在 → 业务 404
|
||||
- 下游(order-stats/requirement)异常降级 → 金额/需求字段 null,不 500 不阻断页面
|
||||
- 老数据无新列 → 新增字段 null,不异常
|
||||
- 身份证号/手机号 → 永不返回(安全边界)
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### opsStage(GroupBatchStageBuckets 七桶)
|
||||
|
||||
| 值 | 中文 | 折叠状态 |
|
||||
|----|------|----------|
|
||||
| RECRUIT | 招募中 | (created) |
|
||||
| FORMED | 已成团(复合桶) | RESOURCE_PREPARING + MATERIAL_PREPARING |
|
||||
| PENDING_TRIP | 待出行 | - |
|
||||
| TRAVELLING | 出行中 | - |
|
||||
| AUDITING | 待审核 | - |
|
||||
| CHECKED | 已审核 | - |
|
||||
| DISBANDED | 已解散 | - |
|
||||
|
||||
### tierCode 组合(成人A/儿童C/幼童Y/婴儿B)
|
||||
|
||||
| 值 | 说明 |
|
||||
|----|------|
|
||||
| 2A1C | 2 成人 1 儿童(tierName="2成人1儿童") |
|
||||
| 全零 | null(不输出) |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 接口 | 字段 | 改前 | 改后 |
|
||||
|------|------|------|------|
|
||||
| 000 | batchCount | 无 | 批量 in 投影计数 |
|
||||
| 001 | receivableAmount/receivedAmount | 无(或空) | 实时聚合(2943.00/3270.00 测试实证) |
|
||||
| 001 | chips/orderCount | 无 | 六芯片聚合/批量计数 |
|
||||
| 002 | totalReceivable/totalReceived | 实体快照 | sumBatchAmounts 实时聚合(非恒 0) |
|
||||
| 002 | primaryReporter | 无 | reporter_rank=PRIMARY 批量取值 |
|
||||
| 003 | estimatedCost/tier/人数 | 无 | 批量派生 |
|
||||
| 003 | contactPhone | 原文 | 全量掩码 |
|
||||
| 003 | totalPrice | 无 | 本户应收(payableForDisplay 口径,取消单返 "0.00",#6929/#7097) |
|
||||
| 003 | travelers | 无 | name/type/age/birthdayInTrip(无证件号,跨年修正 #6929) |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否(全部新增可选字段/参数)
|
||||
- **前端是否必须同步上线**: 否
|
||||
- **前端 workaround 清理点**: 无
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台团期看板 4 个查询接口(响应新增字段 + 筛选参数)
|
||||
- **零影响**:
|
||||
- C 端 / MP 端接口
|
||||
- 下单/支付/退款链路
|
||||
- 数据库表结构(零迁移)
|
||||
- #6902/#6903/#6904 已合并接口(本单未改其文件,仅顺带合并 dev-v3 时解决 GroupBatchMapper 尾部冲突取并集)
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
网关: `https://api.test.1814.love:9443`(`/v3/admin/**` → hl-order-service-v3,dev-v3 @ 47aaff0be,双实例 8086/8186 UP,BUILD SUCCESS 24.6s)
|
||||
|
||||
- `GET /v3/admin/order/group-batch?pageNo=1&pageSize=5` → 200 records=5 total=14,receivableAmount=2943.00 receivedAmount=3270.00,chips=hotel/vehicle/guide/photo/contract/insurance,orderCount=1 ✓
|
||||
- 筛选实证: base_total=14 → keyword=0 / opsStage(FORMED)=13 / month(2026-09)=12 ✓
|
||||
- `GET /v3/admin/order/group-batch/products` → 200 list=6,batchCount>0 产品 4/6(3/3/3/5),regionText=null(上游未提供,预期)✓
|
||||
- `GET /v3/admin/order/group-batch/<groupBatchId>` → 200 keys(34),totalReceivable=2943.00 totalReceived=3270.00(非恒 0)✓,primaryReporterId/Name 字段在位(无 PRIMARY 配置时 null)✓
|
||||
- `GET /v3/admin/order/group-batch/<groupBatchId>/orders?includeTravelers=true&includeNeeds=true&includeCancelled=true` → 200 orders=1,tierCode=2A tierName=2成人,roomCount=1,specialNeeds=脚本造单·…,contactPhone=138****0001(掩码)✓,travelerInfoComplete=true ✓,travelers len=2 keys=age,birthdayInTrip,name,type(无 idNo/phone)✓,样本: name=张伟 type=ADULT age=41 birthdayInTrip=false ✓
|
||||
|
||||
本地: 定向单测 19+15+27+8+3 全绿;模块全量 7832 用例仅 3 个 refund 401 用例失败且为 dev-v3 基线固有(git stash 对照同结果),与本单无关。
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| #6902 | #6902 | 共享件(chip/stats/buckets) | ✅ 依赖 |
|
||||
| #6917 | #6904 | 看板统计条+导出(顺带合并冲突并集) | ✅ 有效 |
|
||||
| **#6923** | **#6905** | 本单 4 接口对齐(squash 等价落地 47aaff0be,PR 因网关禁 PUT 已 closed 留痕) | ✅ 最新 |
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#6905](https://git.1814.love:8443/wx/HL/issues/6905)
|
||||
- 关联 PR: [wx/HL#6923](https://git.1814.love:8443/wx/HL/pulls/6923)
|
||||
- 合并落点: [wx/HL commit 47aaff0be](https://git.1814.love:8443/wx/HL/commit/47aaff0be)(dev-v3)
|
||||
- 后续计划: regionText 上游 product-v2 补齐 + 前端展示验收(另行交接)
|
||||
|
||||
## 十一、复审补充知会(2026-09-01)
|
||||
|
||||
> 复审轮对 #6903/#6904/#6905 已上线行为的口径确认与勘误,无新接口、无字段结构变更;其中 2 项已开返工(见文末表)。
|
||||
|
||||
### 联调口径(现行行为,按此开发)
|
||||
|
||||
1. **【001】无活跃子订单的团期 `chips` 整体为 `null`**(不是六键全「待办」)。有单团期六键全在(聚合态四值:待办/处理中/已完成/异常;个别键可为 null)。渲染芯片前判 `chips != null`,null 时按「未开始」占位。
|
||||
2. **【000】`productType` 实际只能看团期产品**:上游产品域只返回 GROUP 产品,传非 GROUP 值得空列表;「不限类型查普通产品」是面向隐式团的规划能力,当前不可达。§三.1 入参表原「不传=不限」描述有误,已就地勘误,以本条为准。
|
||||
3. **【003】`demandStatus`(本户需求态 SUBMITTED/CONFIRMED/REJECTED)不下发**:契约卡 GB-ADM-003 有该字段但本期未实现,响应中不存在;原型名单表「打回 / 已重提」列暂无数据源,请先隐藏或恒占位,勿依赖。
|
||||
4. **【003】`totalPrice`(本户应收)已实现(#6929)**:「预计毛利 = totalPrice − estimatedCost」现可直接算(`estimatedCost` 已可用)。`totalPrice` 为 JSON 字符串(ToStringSerializer),活跃单 = orderAmount + surchargeAmount − discountAmount(下限 0),**取消单返 `"0.00"`**(#7097 已补两位小数;JSON 为字符串,勿数值化)。**请勿用 `paidAmount + balanceAmount` 自算应收**(含退款场景口径不对)。
|
||||
5. **【003】`include*=false` 时扩展字段「键在、值为 null」**:`travelers / roomCount / roomType / specialNeeds` 键仍存在、值为 `null`,判 `null` 即可,勿用 `key in obj` 判断。
|
||||
6. **【003】`birthdayInTrip` 跨年已修复(#6929)**:行程跨年(12 月~1 月)时,出团年与返团年分别年化比较,任一落在行程闭区间即 `true`(如 12-28~01-03 行程内 01-02 生日 → true);2-29 生日在非闰年落 2-28 不抛异常。
|
||||
|
||||
### 在途返工(上线后另行同步)
|
||||
|
||||
| 工单 | 内容 | 前端影响 |
|
||||
|------|------|----------|
|
||||
| #6926 | 008 导出补 `opsStage`;009 自校验修正;month 非法值容错 | 见 `01_6904_*` 第十一节 |
|
||||
| ~~#6929~~ | ~~003 补 `totalPrice`;`birthdayInTrip` 跨年修正;内部双包装收敛~~ | ✅ 已实现(PR #7057,部署验收后生效):`totalPrice − estimatedCost` 可直接算预计毛利;跨年生日不再漏报;其余无感 |
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#6905](https://git.1814.love:8443/wx/HL/issues/6905)
|
||||
- **PR**: [#6923](https://git.1814.love:8443/wx/HL/pulls/6923)(closed;网关拦截 PUT,采用等价 squash 推送落地)
|
||||
- **Merge commit**: [47aaff0be](https://git.1814.love:8443/wx/HL/commit/47aaff0be)(dev-v3 落点)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,662 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6921"
|
||||
title: "六芯片读端接入专用投影 + 车芯片车务文案修正"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "复审返工:#6903 六芯片读端由 order_main 整行实体查询改为 18 列专用投影(不再触发 customerPhone/emergencyContactPhone 解密),车芯片 statusText 由房务文案改为车务文案;2026-09-01 部署测试服 dev-v3,6 端点网关实测 200 全通过、团期不存在 589500,文案由单测锁定"
|
||||
updated_at: "2026-09-01"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 六芯片读端接入专用投影 + 车芯片车务文案修正
|
||||
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #6924(squash 合并落地 dev-v3 `35b7f2ed`)
|
||||
> **Issue**: #6921(#6903 复审返工)
|
||||
> **日期**: 2026-09-01
|
||||
> **影响范围**: 管理后台团期看板行右侧六芯片(配房/配车/配导游/配摄影/合同/保险)逐户下钻接口(GB-ADM-090~095)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
- **读端投影化(实现层,对外契约不变)**:六芯片逐户明细改为从 `order_main` 的 **18 列专用投影**读取,不再整行加载实体,因此**不再触发 `customerPhone` / `emergencyContactPhone` 解密逻辑**;返回体、字段名、枚举、免闸计数口径全部不变。
|
||||
- **车芯片文案修正(对外可观察)**:`items[].statusText` 车务文案为 `待车务配 / 配车中 / 配车完成`(房务文案 `待房务配 / 配房中 / 配房完成` 不变)。前版 #6903 文档误写"与房同文案",#6921 按车务实际口径修正。
|
||||
- 对外响应结构、错误码、聚合态、免闸口径与 #6903 完全一致,**无任何字段新增/删除/改名**,前端无需改动。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
#6903 交付的六芯片接口在代码走查中发现读端直接加载 `OrderInfo` 实体并触发手机号解密,存在无关数据暴露与无效解密开销;同时车芯片文案错误沿用房务文案。本单为复审返工:#6901/#6903 接口契约不变,仅修正读端实现与车芯片文案。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 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` | 修改接口 | 读端改专用投影 + statusText 车务文案 |
|
||||
| 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`,本单为复审返工:仅读端实现与车芯片文案变化,契约字段与 #6903 完全一致,正文各自自包含。
|
||||
|
||||
### 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` 完全一致(整团待办/进行中/已完成/异常) |
|
||||
| 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": "DONE",
|
||||
"totalCount": 3,
|
||||
"doneCount": 3,
|
||||
"items": [
|
||||
{ "orderId": "770153", "orderNo": "GT-26-0097", "contactName": "林婉清",
|
||||
"peopleCount": 3, "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 事务),无锁、无幂等、不改变任何状态。
|
||||
- 本单读端改为专用投影,返回体与 #6903 完全一致(契约不变)。
|
||||
|
||||
### 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 事务),无锁、无幂等、不改变任何状态。
|
||||
- **本单唯一文案变化**:statusText 使用车务文案(待车务配/配车中/配车完成),不再与房同文案。
|
||||
- 免闸户(`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 置灰。
|
||||
|
||||
### 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": "DOING",
|
||||
"totalCount": 3,
|
||||
"doneCount": 1,
|
||||
"items": [
|
||||
{ "orderId": "770180", "orderNo": "GT-26-0104", "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 为 `NONE`(非 null),前端按 NONE 置灰。
|
||||
|
||||
### 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` 完全一致(整团待办/进行中/已完成/异常) |
|
||||
| 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`),见「六.5」;无合同记录为 null |
|
||||
| items[].statusText | String | 状态中文名(服务端给出,见「六.5」);无合同为「无合同」 |
|
||||
| items[].needsIt | Boolean | 本户是否需要合同(`order_main.needs_contract`);false=免闸户不计入 |
|
||||
| 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": "整团待办",
|
||||
"totalCount": 1,
|
||||
"doneCount": 0,
|
||||
"items": [
|
||||
{ "orderId": "770190", "orderNo": "GT-26-0105", "contactName": "钱进",
|
||||
"peopleCount": 2, "status": "SIGNED", "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 事务),无锁、无幂等、不改变任何状态。
|
||||
- 老数据无合同记录:status=null、statusText=「无合同」,不异常不计入。
|
||||
|
||||
### 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` 完全一致(整团待办/进行中/已完成/异常) |
|
||||
| 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 | 本户保险状态,见「六.5」;无保险记录为 null |
|
||||
| items[].statusText | String | 状态中文名(服务端给出,见「六.5」);无保险为「无保险」 |
|
||||
| items[].needsIt | Boolean | 本户是否需要保险(`order_main.needs_insurance`);false=免闸户不计入 |
|
||||
| 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": "整团待办",
|
||||
"totalCount": 1,
|
||||
"doneCount": 0,
|
||||
"items": [
|
||||
{ "orderId": "770200", "orderNo": "GT-26-0106", "contactName": "孙丽",
|
||||
"peopleCount": 2, "status": "INSURED", "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 事务),无锁、无幂等、不改变任何状态。
|
||||
- 老数据无保险记录:status=null、statusText=「无保险」,不异常不计入。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- **只读**:六端点全部为 GET 查询,READ_ONLY 事务,不产生任何写操作;重复调用结果一致(无副作用)。
|
||||
- **授权**:必须携带管理端登录令牌,网关校验 `group-batch:view` 权限;未配置权限 → 589507。
|
||||
- **入参**:仅 Path 参数 `groupBatchId`(Snowflake 团期 ID),无查询参数、无请求体。
|
||||
- **安全**:读端 18 列专用投影不含任何手机号/证件列,**不会返回也不解密 `customerPhone` / `emergencyContactPhone`**;前端不得依赖此类字段。
|
||||
- **免闸口径**:`needsIt=false` 户不计入 totalCount/doneCount;房/车/约/保 status=null+「无需」/「无合同」/「无保险」,导/摄 status=`NONE`+「无需」。
|
||||
- **聚合态**:`aggregateStatus` 与看板 GB-ADM-001 `chips.X` 同源同算法(共享 `GroupBatchChipResolver`)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录/无有效 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),单次请求最多一次投影查询。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 维度 | 修改前(#6903) | 修改后(#6921) |
|
||||
|------|------|------|
|
||||
| 读端数据源 | 加载 `OrderInfo` 整行实体再取字段 | `order_main` **18 列专用投影**(`listChipProjectionByProductBatchIds`),不再整行加载 |
|
||||
| 手机号解密 | 读端会触发 `customerPhone` / `emergencyContactPhone` 解密(本单实测从未返回,但存在触发路径) | 投影列不含手机号列,**编译期零手机列、运行时零解密** |
|
||||
| 车芯片 statusText | 与房同文案(待房务配/配房中/配房完成) | **车务文案**:待车务配/配车中/配车完成(驳回文案与房一致) |
|
||||
| 对外响应结构 | `GroupBatchChipDetailVO` | 完全一致(无字段新增/删除/改名) |
|
||||
| 错误码/聚合/免闸口径 | 同 #6903 | 完全一致 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **调用方影响**:无。响应结构、字段名、枚举、错误码、聚合态、免闸计数口径全部不变,管理后台前端无需改动。
|
||||
- **性能**:读端从整行实体 + 可能触发解密,改为 18 列投影 + 内存聚合,减少列宽与解密开销(本单不涉及额外查询次数)。
|
||||
- **安全**:消除读端手机号解密触发路径,减少无关敏感字段暴露面。
|
||||
- **回归范围**:六芯片端点 + 共享 `GroupBatchChipResolver` 文案;已用模块全量 7874 用例回归,仅 3 个 dev-v3 既有 refund 401 基线用例失败(与本单无关)。
|
||||
- **其他服务**:不涉及 Feign/域事件/表结构变更,未触碰 #6905 GroupBatchQueryService 与 OrderInfoMapper 契约。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 写操作(配房/配车/配导/配摄/合同/保险状态流转)
|
||||
- 下单/支付/退款链路
|
||||
- 数据库表结构(零迁移)
|
||||
- #6905 团期看板 4 接口(GB-ADM-000/001/002/003,本单未改)
|
||||
- OrderInfoMapper 既有查询契约(仅新增只读投影方法,未改任何既有方法)
|
||||
- 前端展示逻辑(本次为纯后端返工,前端无需改动)
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
网关: `https://api.test.1814.love:9443`(`/v3/admin/**` → hl-order-service-v3,dev-v3 @ `0d5f8c568`,双实例 8086/8186 UP)
|
||||
|
||||
- 6 端点对真实团期 `2089713777065832450` 网关实测全通过:hotel/vehicle/guide/photo/contract/insurance → 200 code=200,chipLabel 正确,items 在位 ✓
|
||||
- 团期不存在 `999999999999999999` → 589500 团期不存在 ✓
|
||||
- 测试数据无 PROCESSING/DONE 态订单(status 全空或无需/无合同/无保险),车务文案(待车务配/配车中/配车完成)由 `GroupBatchChipResolverTest` 单测锁定 ✓
|
||||
- 本地:chip 定向 229 用例全绿;模块全量 7874 用例仅 3 个 dev-v3 refund 401 基线失败(与本单无关)✓
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| #6902 | #6902 | 共享件(chip/stats/buckets) | ✅ 依赖 |
|
||||
| #6903 | #6903 | 六芯片逐户明细新增接口(本单返工对象) | ✅ 有效 |
|
||||
| #6916 | #6915 | 共享地基 statusText 契约文案修正 + 测试补齐 | ✅ 依赖 |
|
||||
| #6924 | #6921 | 本单复审返工(读端投影 + 车务文案)squash 合并 `35b7f2ed` | ✅ 最新 |
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#6921](https://git.1814.love:8443/wx/HL/issues/6921)
|
||||
- 关联 PR: [wx/HL#6924](https://git.1814.love:8443/wx/HL/pulls/6924)
|
||||
- 合并落点: [wx/HL commit 35b7f2ed](https://git.1814.love:8443/wx/HL/commit/35b7f2ed)(dev-v3)
|
||||
- 原接口文档: #6903 `01_6903_团期看板六芯片逐户明细-GB-ADM-090~095-新增接口-管理后台.md`(本单已同步其车务文案描述)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#6921](https://git.1814.love:8443/wx/HL/issues/6921)
|
||||
- **PR**: [#6924](https://git.1814.love:8443/wx/HL/pulls/6924)(squash 合并)
|
||||
- **Merge commit**: [35b7f2ed](https://git.1814.love:8443/wx/HL/commit/35b7f2ed)(dev-v3 落点)
|
||||
|
||||
### 联系人
|
||||
|
||||
- 后端: wx(GIT)
|
||||
- 前端: 待定(本次无前端改动)
|
||||
@@ -0,0 +1,259 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6938"
|
||||
title: "供应商注册提交可选主表字段缺席保留"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: "2026-09-02"
|
||||
status_note: "PR #6941(含补充 #6945/#6946)已合并 dev-v3,合并链终态提交 697ae6f57 已滚动部署 TEST 双实例。注册提交接口可选主表字段语义由缺席清空改为缺席保留;TEST 真实网关验收 32 项断言通过。"
|
||||
updated_at: "2026-09-02"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商模块: 注册提交可选主表字段缺席保留
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-resource-service (端口 8082)
|
||||
> **PR**: #6941
|
||||
> **Issue**: #6938
|
||||
> **日期**: 2026-09-02
|
||||
> **影响范围**: 管理端供应商注册提交表单
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
|
||||
|
||||
- 本次变化:注册提交保存完整表单时,**缺席或空白的可选主表字段保留草稿现值,不再清空**。
|
||||
- 前端以前以为的:提交表单未回显的字段(如公司联系电话)提交后仍在。
|
||||
- 实际旧行为:提交瞬间被静默置空(本单修复前的缺陷);**新行为**:缺席=保留,与增量更新接口语义对齐。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景(选填)
|
||||
|
||||
工单 #6938:供应商提交注册时全量表单静默清空未回传字段(公司联系电话丢失)。根因是提交链路按"完整表单权威覆盖"语义把缺席/空白字段写成 NULL 并留下清空审计。修复后提交与增量更新共用"空白=保留"语义。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 提交供应商注册审批 | POST | `/admin/supplier/items/{supplierId}/submit` | 字段缺席语义变更 | 可选主表字段缺席/空白由"清空"改为"保留现值" |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 提交供应商注册审批 `POST /admin/supplier/items/{supplierId}/submit`
|
||||
|
||||
**VO**: `SupplierSubmitReqVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理端供应商编辑页点击"提交注册"时调用;提交前应用 `GET /admin/supplier/items/{supplierId}/basic-info/view` 回显完整表单。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| fullName | Body | String | ✅ | - | 全称,请求权威提供 |
|
||||
| taxNo | Body | String | ✅ | GB32100 校验位 | 税号,请求权威提供 |
|
||||
| mainCooperation | Body | String | ✅ | - | 主要合作内容 |
|
||||
| balance | Body | BigDecimal | ✅ | ≥0 | 余额 |
|
||||
| paymentType | Body | String | ✅ | - | 支付类型 |
|
||||
| licenseImageUrl | Body | String | ✅(提交时) | - | 执照影像,维持请求侧必填门禁,缺席拒绝 |
|
||||
| expectedUpdateTime | Body | LocalDateTime | ✅ | `yyyy-MM-dd HH:mm:ss` | 乐观锁,取视图 updateTime |
|
||||
| shortName / contactPhone / address / countyId / establishDate / registeredCapital / businessScope / staffScale / remark | Body | - | ❌ | - | **缺席或空白 = 保留草稿现值(本次变更)** |
|
||||
| legalRepresentative / legalRepresentativeIdNo / legalRepresentativeIdCardFrontUrl / legalRepresentativeIdCardBackUrl | Body | - | ❌ | 格式校验 | 法人四件套,同上缺席保留 |
|
||||
| types / contacts / qualifications / initialAccounts | Body | List | ❌ | - | 子项快照;`contacts`/`initialAccounts` 缺席 = 保留历史 |
|
||||
|
||||
#### 出参 `Result<SupplierApprovalCommandRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| approvalStatus | String | 受理后 `PENDING` |
|
||||
| spNo | String | 审批单号(TEST 现为真实企微单号) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "HL6938-TEST-20260902",
|
||||
"taxNo": "91150100HL6938T01W",
|
||||
"mainCooperation": "车队合作",
|
||||
"balance": "0.00",
|
||||
"paymentType": "1",
|
||||
"licenseImageUrl": "https://example.com/hl6938-license.png",
|
||||
"expectedUpdateTime": "2026-09-02 11:05:52",
|
||||
"types": [{"typeCode": "FLEET", "isPrimary": 1}],
|
||||
"qualifications": [{"qualType": "GZZRX_LICENSE", "certNo": "GZZRX2026HL6938A", "permanentValid": true}]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": { "approvalStatus": "PENDING", "spNo": "202609020001" },
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口无空数据场景;提交受理即返回审批单号,无降级分支。
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "approvalStatus": "PENDING" }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "expectedUpdateTime不能为空",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 鉴权:写权限 `FINANCE`/`SUPER_ADMIN`;未登录 → 业务码 `401`。
|
||||
- 乐观锁:`expectedUpdateTime` 与现值不符 → 业务码 `395014`,零写入。
|
||||
- 非草稿状态重复提交 → 状态机拒绝,零写入。
|
||||
- 表单与草稿一致时不产生"提交注册前保存完整表单"变更记录。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
| 场景 | payload |
|
||||
|------|---------|
|
||||
| ✅ 回显全量已填字段提交 | 视图字段全部带回,行为与旧版一致 |
|
||||
| ✅ 可选字段缺席 | `contactPhone` 等缺省 → 保留草稿现值,**不再清空** |
|
||||
| ✅ 显式修改可选字段 | `contactPhone: "04712227654"` → 覆盖并双向审计留痕 |
|
||||
| ❌ 缺席 `licenseImageUrl` | 提交必填门禁拒绝(业务码 3950xx 资质必填) |
|
||||
| ❌ 缺席 `expectedUpdateTime` | 400,`expectedUpdateTime不能为空` |
|
||||
|
||||
- 缺席保留意味着可选字段**不再支持以空白清空**;确需清空请联系后端评估独立入口。
|
||||
- `expectedUpdateTime` 必须取最近一次 `basic-info/view` 返回的 `updateTime`。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
| 前端提交 | `contact_phone` 等可选列 |
|
||||
|----------|--------------------------|
|
||||
| 字段缺席或空白 | **保留数据库现值(不再 SET NULL)** |
|
||||
| 字段显式提供新值 | 覆盖为新值,`supplier_change_log` 记录旧→新 |
|
||||
|
||||
- 无 migration、DDL 或 DML;历史已被清空的旧数据不回写。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 业务码 `401`(网关包装 HTTP 200)。
|
||||
- 乐观锁冲突 → 业务码 `395014`,不阻断页面,重新取视图重试即可。
|
||||
- 旧客户端继续全量回显提交 → 行为不变。
|
||||
- 历史已清空数据 → 保持 NULL,不异常、不回写。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 12 个可选文本字段(`contactPhone` 等)缺席/空白 | 清空为 NULL 并记清空审计 | 保留草稿现值,无审计 |
|
||||
| `countyId`/`establishDate` 缺席 | 清空为 NULL | 保留草稿现值 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 提交表单未回显可选字段 | 字段被静默清空 | 字段保留 |
|
||||
| 提交审计 delta | 出现非用户主动的清空留痕 | 只记录真实变化;一致时不落审计行 |
|
||||
|
||||
## 六.7、影响评估(修改/删除类必写)
|
||||
|
||||
- **是否破坏向后兼容**: 否。请求/响应结构不变;仅缺席字段行为由清空变保留,无合理调用方依赖清空语义。
|
||||
- **前端是否必须同步上线**: 否。建议跟进修复提交表单回显(工单验收②的前端分支),后端缺席保留已兜底。
|
||||
- **前端 workaround 清理点**: 无。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
|
||||
|
||||
- **仅影响**: 管理后台供应商注册提交接口的可选主表字段缺席行为。
|
||||
- **零影响**:
|
||||
- 供应商新增 `/add`、增量更新 `/update`(其 `licenseImageUrl` 既有空白清空分支保持原样,不在本单)
|
||||
- 必填字段(全称/税号/合作内容/余额/支付类型/执照影像)的请求权威语义
|
||||
- 供应商状态机、审批流、企微 Provider 交互、审批候选快照结构
|
||||
- C 端接口与历史存量数据
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
真实 Gateway(`api.test.1814.love`)+ TEST 数据库实证,带 ✓ 标记:
|
||||
|
||||
```
|
||||
POST /admin/supplier/items/add → 200 草稿含全量可选字段 ✓
|
||||
POST /admin/supplier/items/{id}/submit(缺席可选字段) → 200 PENDING + 真实单号 ✓
|
||||
GET /admin/supplier/items/{id}/basic-info/view → 九个可选字段全部保留 ✓
|
||||
(contactPhone=04711234567 座机形态保留,验收①②)
|
||||
DB supplier_change_log 提交审计 changedFields → 恰为 [contacts,qualifications],
|
||||
九字段均不在列,无 "contactPhone":null 清空形态 ✓(验收③)
|
||||
POST submit(显式改 contactPhone) → 新值生效,审计 before/after
|
||||
双向留痕 13847110001→04712227654 ✓(兼容性)
|
||||
```
|
||||
|
||||
验证夹具: `HL6938-TEST-20260902`(supplierId=2094985119537217538) / `HL6938B-TEST-20260902`(supplierId=2094987591035101186);验收产生的真实企微在途单已发起撤销。
|
||||
自动化: `SupplierAggregateWriterTest` 定向零失败;hl-resource-service 全量 2293 项零失败。部署: TEST 任务 `dcdfb70a`,部署提交 `697ae6f57` 与预期一致,双实例滚动健康。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR(纠错 / 功能演进时必写)
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| #6930 | #6927 | 注册提交接企微四级串行审批 | ✅ 有效 |
|
||||
| **本 PR #6941** | **#6938** | 可选主表字段缺席保留(补充 #6945/#6946) | ✅ 最新 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#6938](https://git.1814.love:8443/wx/HL/issues/6938)
|
||||
- 关联 PR: [wx/HL#6941](https://git.1814.love:8443/wx/HL/pulls/6941)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#6938](https://git.1814.love:8443/wx/HL/issues/6938)
|
||||
- **PR**: [#6941](https://git.1814.love:8443/wx/HL/pulls/6941)(补充 [#6945](https://git.1814.love:8443/wx/HL/pulls/6945)、[#6946](https://git.1814.love:8443/wx/HL/pulls/6946))
|
||||
- **Merge commit**: [697ae6f57](https://git.1814.love:8443/wx/HL/commit/697ae6f57aeea96b0567cfc8766e6ff1a091c1da)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,441 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6950"
|
||||
title: "团期人员配置候选列表(导游位并收 GUIDE/LEADER)"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "44f51c13"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-03"
|
||||
status_note: "PR #6962 已合入 dev-v3;2026-09-03 经测试环境网关实测,6 条正负向用例全部通过。前端尚未接入"
|
||||
updated_at: "2026-09-03"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期人员配置: 新增人员配置候选列表接口,并修正 staffRole 取值校验
|
||||
|
||||
> **存放目录**: 二期(order-v3)→ `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #6962
|
||||
> **Issue**: #6950
|
||||
> **日期**: 2026-09-02
|
||||
> **影响范围**: 管理后台「团期详情 → 配置导游 / 配置摄影」弹窗的人员资源库列表
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
本次除新增候选列表接口外,**另有一处行为变更**,调用方必须知道:
|
||||
|
||||
**保存团期人员配置 `PUT /v3/admin/group-batch/:groupBatchId/staff` 的 `staffList[].staffRole` 加了枚举白名单校验。**
|
||||
|
||||
- 前端以前以为:`staffRole` 只做非空校验,传什么都能存进去。
|
||||
- 实际现在是:白名单外的取值 **返回 400**,不再原样入库。
|
||||
- 白名单:`LEADER` / `GUIDE` / `DRIVER` / `PHOTOGRAPHER` / `OTHER`。
|
||||
|
||||
另外该字段的 Swagger 描述此前 **漏了 `GUIDE`**,本次补全。若前端此前按旧描述认为只有 4 个取值,现在是 5 个。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
「配置导游」「配置摄影」弹窗需要一份可选人员列表,此前 order-v3 侧没有这个接口,前端无从取候选池。
|
||||
|
||||
**资源域零改动**:`GET /internal/staff/list-available` 本就存在(其 Swagger 注释写明「供管理后台团批分配人员的下拉选择框使用」),order-v3 侧只补了 Feign 方法与降级处理。
|
||||
|
||||
**导游位为什么并收两类**(2026-09-02 jw 裁决):资源域字典里 `GUIDE` 是导游、`LEADER` 是领队,两者在团期现场都可能承担带团职责,由配置人按实际情况挑,**服务端不替业务做取舍**。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 查询团期人员配置候选列表 | GET | `/v3/admin/group-batch/:groupBatchId/staff/candidates` | **新增接口** | 导游位并收 GUIDE/LEADER,摄影位只收 PHOTOGRAPHER |
|
||||
| 2 | 保存团期人员配置 | PUT | `/v3/admin/group-batch/:groupBatchId/staff` | **请求体新增校验** | `staffList[].staffRole` 加枚举白名单,越界返 400 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 查询团期人员配置候选列表 `GET /v3/admin/group-batch/:groupBatchId/staff/candidates`
|
||||
|
||||
**VO**: `StaffCandidateRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台「团期详情 → 配置导游 / 配置摄影」弹窗打开时调用,用于渲染可选人员资源库列表。
|
||||
弹窗按配置位分别调用:导游弹窗传 `role=GUIDE`,摄影弹窗传 `role=PHOTOGRAPHER`。
|
||||
列表中 `assigned=true` 的项应预置为已勾选状态,供配置人在原有选择基础上增删。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `groupBatchId` | Path | Long | ✅ | — | 团期批次 ID。**这里是产品侧 `group_tour_batch.batch_id`**,不是订单侧运营团期主键,与 `order_batch_staff.group_batch_id` 同源 |
|
||||
| `role` | Query | String | ✅ | 只接受 `GUIDE` / `PHOTOGRAPHER` | 配置位。`GUIDE`=导游位(并收 GUIDE/LEADER,可选多人);`PHOTOGRAPHER`=摄影位。**其余取值(含 `DRIVER`)返回 582113** |
|
||||
|
||||
> `DRIVER` 被显式拒绝:司机由车务派车产生,不在团期人员配置里手工指定。
|
||||
|
||||
#### 出参 `Result<List<StaffCandidateRespVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `staffId` | Long | 人员 ID |
|
||||
| `staffName` | String | 姓名 |
|
||||
| `staffPhone` | String | 手机号,**前 3 后 4 脱敏**(与 `getConfig` 口径一致) |
|
||||
| `staffType` | String | 资源域人员类型:`GUIDE` / `LEADER` / `PHOTOGRAPHER` |
|
||||
| `avatarUrl` | String | 头像 URL;资源域无头像或取头像失败时为 `null` |
|
||||
| `assigned` | Boolean | 是否已被本团期选中。**`true` 时前端应显示为已勾选** |
|
||||
| `assignedRole` | String | 已选中时对应的 `order_batch_staff.staff_role`;未选中为 `null` |
|
||||
|
||||
> `assigned` 与 `assignedRole` 要分开判:存在「已选但角色为空」的历史数据,此时 `assigned=true` 而 `assignedRole=null`,前端**不能**用 `assignedRole != null` 判断勾选态。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/group-batch/1823456789012345678/staff/candidates?role=GUIDE
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"staffId": 1823456789012345678,
|
||||
"staffName": "张三",
|
||||
"staffPhone": "138****8000",
|
||||
"staffType": "GUIDE",
|
||||
"avatarUrl": "https://oss.example.com/avatar/1.jpg",
|
||||
"assigned": true,
|
||||
"assignedRole": "LEADER"
|
||||
},
|
||||
{
|
||||
"staffId": 1823456789012345679,
|
||||
"staffName": "李四",
|
||||
"staffPhone": "139****1234",
|
||||
"staffType": "LEADER",
|
||||
"avatarUrl": null,
|
||||
"assigned": false,
|
||||
"assignedRole": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
资源域无可用人员时返回空数组,不报错:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
头像服务取不到时**不阻断列表**,`avatarUrl` 降级为 `null`,其余字段照常返回。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
`role` 传了 `GUIDE` / `PHOTOGRAPHER` 之外的值(如 `DRIVER`):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 582113,
|
||||
"message": "人员配置位不合法,只支持导游位与摄影位",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
资源域人员查询失败:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 582103,
|
||||
"message": "员工信息查询失败,请稍后重试",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 只读接口,**不产生任何写入**,可安全重复调用
|
||||
- 同一人同时属于 `GUIDE` 与 `LEADER` 时按 `staffId` 去重,只返回一条
|
||||
- `assigned` 反映的是本团期当前配置状态,与 `role` 入参无关:传 `role=GUIDE` 时,
|
||||
已被配成摄影位的人不会出现在结果里,但导游位候选中若有人已被选中则 `assigned=true`
|
||||
- 资源域仅返回启用状态人员,停用人员不在候选池
|
||||
- 头像属展示增强,取不到时降级为 `null`,**不影响可选性**
|
||||
|
||||
---
|
||||
|
||||
### 2. 保存团期人员配置 `PUT /v3/admin/group-batch/:groupBatchId/staff`
|
||||
|
||||
**VO**: `BatchStaffConfigReqVO`
|
||||
|
||||
**本次只改请求体校验,路径、出参均不变。**
|
||||
|
||||
#### 使用场景
|
||||
|
||||
「配置导游 / 配置摄影」弹窗点击保存时调用,整批覆盖该团期的人员配置。
|
||||
本次变更后,前端必须保证 `staffRole` 取值落在白名单内,否则整个请求被拒、无一条生效。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `groupBatchId` | Path | Long | ✅ | — | 团期批次 ID,同接口 1 |
|
||||
| `staffList` | Body | Array | ✅ | — | 整批覆盖语义:传入列表即最终配置,未包含的人员被移除 |
|
||||
| `staffList[].staffId` | Body | Long | ✅ | `@NotNull` | 人员 ID |
|
||||
| `staffList[].staffRole` | Body | String | ✅ | **本次新增** `@Pattern`:`LEADER`\|`GUIDE`\|`DRIVER`\|`PHOTOGRAPHER`\|`OTHER` | **变更前**仅 `@NotBlank`,任意非空值原样入库;**变更后**越界返 400 |
|
||||
| `staffList[].sortOrder` | Body | Integer | ❌ | — | 展示排序 |
|
||||
| `staffList[].remark` | Body | String | ❌ | `@Size(max=500)` | 备注 |
|
||||
|
||||
#### 出参 `Result<BatchStaffConfigRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `staffList` | Array | 保存后的人员配置列表,字段同 `getConfig` |
|
||||
| `affectedOrderCount` | Integer | 本次保存扇出影响的子订单数 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"staffList": [
|
||||
{
|
||||
"staffId": 1002,
|
||||
"staffRole": "GUIDE",
|
||||
"sortOrder": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"staffList": [
|
||||
{
|
||||
"id": "2095403473712418817",
|
||||
"staffId": 1002,
|
||||
"staffRole": "GUIDE",
|
||||
"staffName": "李雪梅",
|
||||
"staffPhone": "138****1002",
|
||||
"avatarUrl": null,
|
||||
"sortOrder": 0,
|
||||
"remark": null
|
||||
}
|
||||
],
|
||||
"affectedOrderCount": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
传入空 `staffList` 数组表示清空该团期的人员配置,返回 200 且 `staffList` 为空数组,
|
||||
不报错。这也是回退误配置的正规手段。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
`staffRole` 越界(**本次新增行为**):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "员工角色只能是 LEADER / GUIDE / DRIVER / PHOTOGRAPHER / OTHER",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **整批覆盖**语义,不是增量追加;未包含在 `staffList` 里的既有人员会被移除
|
||||
- 校验在 `@Valid` 阶段完成,**任一条目越界则整个请求被拒,不会部分写入**
|
||||
- `@Pattern` **区分大小写**,`guide` 不等于 `GUIDE`
|
||||
- 空串会同时触发 `@Pattern` 与 `@NotBlank`,消息合并返回
|
||||
- 存量 `order_batch_staff` 数据不迁移,新校验只在下次保存时触发
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- **`role` 必传且只接受两个值**:`GUIDE`、`PHOTOGRAPHER`。不要传 `DRIVER`——司机由车务派车投影产生,
|
||||
不在团期人员配置里手工指定,传了会返回 582113。
|
||||
- **勾选态判断用 `assigned`,不要用 `assignedRole != null`**。存在「已选但角色为空」的历史数据,
|
||||
用后者会漏掉这批人。
|
||||
- **`groupBatchId` 是产品侧 `group_tour_batch.batch_id`**,不是订单侧运营团期主键。传错会返回空列表而非报错。
|
||||
- **保存前先过白名单**:`staffRole` 只能是 `LEADER` / `GUIDE` / `DRIVER` / `PHOTOGRAPHER` / `OTHER`,
|
||||
且区分大小写。整批中任一条越界会导致整个请求 400、无一条生效。
|
||||
- **保存是整批覆盖**:每次提交需带上该团期的完整人员列表,只传增量会导致其余人员被清除。
|
||||
- 错误码 `582113` 是业务码,HTTP 状态仍为 200,判断成败要读响应体的 `code` 字段。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
仅描述外部可观察行为:
|
||||
|
||||
- 接口 1(GET candidates)**只读,不产生任何写入**。
|
||||
- 接口 2(PUT staff)按整批覆盖语义重写该团期的人员配置:提交列表中的人员被保留或新增,
|
||||
未包含的既有人员被移除;响应的 `affectedOrderCount` 表示随之扇出更新的子订单数量。
|
||||
- 越界校验发生在写入之前,**校验失败时数据库无任何变更**。
|
||||
- 本次变更**不涉及表结构调整**,也不对存量数据做迁移或回填。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- `role` 非法 → **582113**,不是 400(业务码,HTTP 仍 200)
|
||||
- 资源域无可用人员 → 返回 `[]`,不报错、不阻断弹窗
|
||||
- 头像服务异常 → `avatarUrl` 为 `null`,列表照常返回(头像属展示增强)
|
||||
- 资源域只返回 `status=1` 的人员,按 `sortOrder`、`staffId` 排序
|
||||
- 同一人同时命中 `GUIDE` 与 `LEADER` 两类 → **按 `staffId` 去重,只出现一次**
|
||||
- 已选人员 `staffRole` 为 `null`(历史脏数据)→ `assigned=true`、`assignedRole=null`,**接口不 500**
|
||||
- 未登录 → 401(网关拦截)
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### `role`(Query 参数,配置位)
|
||||
|
||||
| 取值 | 含义 | 实际拉取的资源域 `staffType` |
|
||||
|------|------|------------------------------|
|
||||
| `GUIDE` | 导游位 | **`GUIDE` + `LEADER` 两类并收**,按 `staffId` 去重,可选多人 |
|
||||
| `PHOTOGRAPHER` | 摄影位 | 只收 `PHOTOGRAPHER` |
|
||||
| 其他(含 `DRIVER`) | — | 拒绝,返回 582113 |
|
||||
|
||||
### `staffType`(响应字段,资源域人员类型)
|
||||
|
||||
本接口可能返回 `GUIDE`(导游)、`LEADER`(领队)、`PHOTOGRAPHER`(摄影)三种。
|
||||
资源域完整字典还包含 `GUIDE_ASSISTANT` / `LIFE_TEACHER` / `STUDY_TEACHER` / `OTHER`,但**不会出现在本接口响应里**。
|
||||
|
||||
### `staffRole`(保存接口入参,取值域对齐 `SettlementStaffRoleEnum`)
|
||||
|
||||
| 取值 | 含义 |
|
||||
|------|------|
|
||||
| `LEADER` | 领队 |
|
||||
| `GUIDE` | 导游 |
|
||||
| `DRIVER` | 司机(**由车务派车投影产生,一般不由本接口写入**) |
|
||||
| `PHOTOGRAPHER` | 摄影 |
|
||||
| `OTHER` | 其他 |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、错误码
|
||||
|
||||
段位 `582100-582199`,owner `hl-order-service-v3`(`AssignmentErrorCode`)。
|
||||
|
||||
| 码 | 符号 | 消息 | 触发 |
|
||||
|---|---|---|---|
|
||||
| `582113` | `STAFF_CANDIDATE_ROLE_INVALID` | 人员配置位不合法,只支持导游位与摄影位 | **本次新增**。`role` 不是 `GUIDE` / `PHOTOGRAPHER` |
|
||||
| `582103` | `STAFF_INFO_FETCH_FAILED` | 员工信息查询失败,请稍后重试 | 既有码。资源域 `list-available` 返回失败或空结果对象 |
|
||||
| `400` | — | 员工角色只能是 LEADER / GUIDE / DRIVER / PHOTOGRAPHER / OTHER | **本次新增**。保存接口 `staffRole` 越界 |
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**:管理后台团期详情的「配置导游 / 配置摄影」弹窗
|
||||
- **零影响**:
|
||||
- 资源域 `hl-resource-service`(**结构与接口零改动**,只是被新调用方使用)
|
||||
- 团期人员保存后的子订单扇出逻辑(`order_staff_assignment`)
|
||||
- 车务派车产生的司机行
|
||||
- 已有的 `GET` / `PUT /v3/admin/group-batch/:groupBatchId/staff` 出参
|
||||
- 存量 `order_batch_staff` 数据(不迁移,`staffRole` 新校验只在下次保存时触发)
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
✅ **已验证。** 2026-09-03 于测试环境网关实测,真实鉴权(管理端 admin 账号)。
|
||||
|
||||
- 网关:`https://api.test.1814.love:9443`
|
||||
- 路由:`/v3/admin/**` 由网关直挂 hl-order-service-v3(**无 `/order-v3` 前缀**)
|
||||
- 分支:`dev-v3`
|
||||
|
||||
| # | 用例 | 期望 | 实测 |
|
||||
|---|---|---|---|
|
||||
| 1 | `?role=GUIDE` | 200,并收 GUIDE+LEADER | ✅ 200,7 条,staffType 去重 = `[GUIDE, LEADER]`,staffId 唯一 |
|
||||
| 2 | `?role=PHOTOGRAPHER` | 200,只含摄影 | ✅ 200,1 条,staffType 去重 = `[PHOTOGRAPHER]` |
|
||||
| 3 | `?role=DRIVER` | 582113 | ✅ `582113 人员配置位不合法,只支持导游位与摄影位` |
|
||||
| 4 | `?role=XXX` 非法值 | 582113 | ✅ 同上 |
|
||||
| 5 | 缺 `role` 参数 | 400 | ✅ `400 缺少必要参数: role` |
|
||||
| 6 | 无 Authorization | 401 | ✅ `401 缺少有效的 Authorization 头` |
|
||||
|
||||
**脱敏核对**:`138****1002` / `139****1011` / `139****1010`,前 3 后 4 生效。
|
||||
**头像降级核对**:用例 1 中多条 `avatarUrl` 为 `null` 未阻断返回;用例 2 中摄影师返回真实 OSS 地址。
|
||||
|
||||
本地单元与架构测试(提交信息记载):
|
||||
|
||||
```
|
||||
GroupBatchStaffConfigServiceTest 27 例全过 ✓
|
||||
LayerEnforcement / MapperBoundary / RedLine /
|
||||
ErrorCodeRegistry 45 例全过 ✓
|
||||
```
|
||||
|
||||
### 保存接口 `staffRole` 白名单实测(本次行为变更)
|
||||
|
||||
同日于同一网关实测 `PUT /v3/admin/group-batch/:groupBatchId/staff`:
|
||||
|
||||
| # | payload `staffList[0].staffRole` | 期望 | 实测 |
|
||||
|---|---|---|---|
|
||||
| 7 | `"SUPERVISOR"` 越界值 | 400 | ✅ `400 员工角色只能是 LEADER / GUIDE / DRIVER / PHOTOGRAPHER / OTHER` |
|
||||
| 8 | `"guide"` 小写 | 400 | ✅ 同上(正则区分大小写) |
|
||||
| 9 | `""` 空串 | 400 | ✅ `400 …; 该字段不能为空`(@Pattern 与 @NotBlank 同时触发) |
|
||||
| 10 | `"GUIDE"` 白名单内 | 放行 | ✅ 200,`affectedOrderCount: 0` |
|
||||
|
||||
用例 7–9 在 `@Valid` 阶段即被拒,**不产生任何写入**。用例 10 会落库,测试后已用空 `staffList` 数组 PUT 还原,
|
||||
复核 `GET .../staff` 返回 `data: []`,与测试前一致。
|
||||
|
||||
**仍待补**:空候选池分支未构造(需一个无可用人员的团期)。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| **本 PR #6962** | **#6950** | 新增候选列表接口 + `staffRole` 白名单校验 | ✅ 最新 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#6950](https://git.1814.love:8443/wx/HL/issues/6950)
|
||||
- 关联 PR: [wx/HL#6962](https://git.1814.love:8443/wx/HL/pulls/6962)
|
||||
- 合入提交: `809462321`(feat),merge `35d0a8f2c` → `dev-v3`
|
||||
- 需求与契约: `docs/group/团期模块接口文档-v2.0.html` GB-ADM-014
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: #6950
|
||||
- **PR**: #6962(合并提交 `35d0a8f2c`,落入 `dev-v3`)
|
||||
- **服务**: hl-order-service-v3
|
||||
- **后端**: jw
|
||||
- **前端**: 待认领(`frontend_status: pending`)
|
||||
- **口径裁决**: 2026-09-02 jw —— 导游位并收 `GUIDE` 与 `LEADER`,摄影位只收 `PHOTOGRAPHER`,`DRIVER` 显式拒绝
|
||||
@@ -0,0 +1,511 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6979"
|
||||
title: "统一供应商暂停状态与拉黑归档流转"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "a67bbd79"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-03"
|
||||
status_note: "PR #7007 已合并 dev-v3 并部署 TEST;当前状态统一使用 SUSPENDED,新增恢复合作和解除黑名单接口,前端需按状态矩阵调整菜单。"
|
||||
updated_at: "2026-09-03"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商模块:统一暂停状态与拉黑归档流转
|
||||
|
||||
> **服务**: `hl-resource-service`(8082)
|
||||
> **Issue**: #6979
|
||||
> **PR**: #7007
|
||||
> **影响范围**: 管理后台供应商列表、详情与状态操作菜单
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
- 当前供应商状态不再返回 `FROZEN`,暂停合作统一为 `SUSPENDED`。
|
||||
- 新增“恢复合作”和“解除黑名单”接口;拉黑只允许从 `SUSPENDED` 发起。
|
||||
- 归档只允许从 `SUSPENDED` 或 `BLACKLIST` 进入门禁;清账能力未交付时仍返回 `395032`,不会归档。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 分页查询供应商 | GET | `/admin/supplier/items/page` | 响应枚举修改 | 当前状态不再返回 `FROZEN` |
|
||||
| 2 | 有界查询供应商 | GET | `/admin/supplier/items/list` | 响应枚举修改 | 当前状态不再返回 `FROZEN` |
|
||||
| 3 | 查询供应商详情 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应枚举修改 | 暂停状态统一返回 `SUSPENDED` |
|
||||
| 4 | 暂停合作 | POST | `/admin/supplier/items/{supplierId}/suspend` | 行为修改 | `ACTIVE → SUSPENDED` |
|
||||
| 5 | 恢复合作 | POST | `/admin/supplier/items/{supplierId}/resume` | 新增接口 | `SUSPENDED → ACTIVE` |
|
||||
| 6 | 拉入黑名单 | POST | `/admin/supplier/items/{supplierId}/blacklist` | 行为修改 | 仅允许 `SUSPENDED → BLACKLIST` |
|
||||
| 7 | 解除黑名单 | POST | `/admin/supplier/items/{supplierId}/unblacklist` | 新增接口 | `BLACKLIST → SUSPENDED` |
|
||||
| 8 | 清账归档 | POST | `/admin/supplier/items/{supplierId}/archive` | 行为修改 | 仅 `SUSPENDED/BLACKLIST` 可进入清账门禁 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 分页查询供应商 `GET /admin/supplier/items/page`
|
||||
|
||||
**VO**: `SupplierPageReqVO / PageResult<SupplierListItemRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
供应商列表页分页查询,并依据每行 `status` 显示状态菜单。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| page / pageSize | Query | Integer | 否 | 页码 ≥ 1;每页 ≤ 100 | 分页参数 |
|
||||
| status | Query | String | 否 | 当前六状态之一 | 状态筛选 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| records[].status | String | 当前状态;不再返回 `FROZEN` |
|
||||
| records[].updateTime | LocalDateTime | 状态动作的乐观锁版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/page?page=1&pageSize=20&status=SUSPENDED
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"data":{"records":[{"supplierId":"2094672459314745346","status":"SUSPENDED","updateTime":"2026-09-03 08:03:00"}],"total":1,"page":1,"pageSize":20},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无匹配数据返回 `records: []`;本接口无业务降级分支。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":401,"message":"缺少有效的 Authorization 头","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `status=FROZEN` 不再是合法筛选值;请改用 `SUSPENDED`。
|
||||
|
||||
### 2. 有界查询供应商 `GET /admin/supplier/items/list`
|
||||
|
||||
**VO**: `SupplierListReqVO / List<SupplierListItemRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
供应商选择器或短列表查询,并依据 `status` 显示状态菜单。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| status | Query | String | 否 | 当前六状态之一 | 状态筛选 |
|
||||
| limit | Query | Integer | 否 | 1~200,默认 50 | 最大返回数 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| data[].status | String | 当前状态;暂停合作为 `SUSPENDED` |
|
||||
| data[].updateTime | LocalDateTime | 状态动作的乐观锁版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/list?status=BLACKLIST&limit=50
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"data":[{"supplierId":"2094672459314745346","status":"BLACKLIST","updateTime":"2026-09-03 08:04:00"}],"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无匹配数据返回 `data: []`;本接口无业务降级分支。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":400,"message":"供应商状态不合法","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 当前状态只接受 `DRAFT/VETTING/ACTIVE/SUSPENDED/BLACKLIST/ARCHIVED`。
|
||||
|
||||
### 3. 查询供应商详情 `GET /admin/supplier/items/{supplierId}/basic-info/view`
|
||||
|
||||
**VO**: `SupplierBasicInfoRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
进入供应商详情或执行状态动作前,读取当前状态和最新并发版本。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| supplierId | Path | String | 是 | 正整数雪花 ID | 供应商 ID |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| status | String | 当前六状态之一 |
|
||||
| updateTime | LocalDateTime | 后续状态动作原样回传 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/2094672459314745346/basic-info/view
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"data":{"supplierId":"2094672459314745346","status":"ACTIVE","updateTime":"2026-09-03 08:05:00"},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
详情无空对象或降级结果;目标不存在时返回业务错误。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395001,"message":"供应商不存在","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 暂停合作的当前主表状态统一返回 `SUSPENDED`,历史记录仍可能展示旧快照 `FROZEN`。
|
||||
|
||||
### 4. 暂停合作 `POST /admin/supplier/items/{supplierId}/suspend`
|
||||
|
||||
**VO**: `SupplierStatusChangeReqVO / SupplierStatusChangeRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
在 `ACTIVE` 行点击“暂停合作”。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| supplierId | Path | String | 是 | 正整数雪花 ID | 供应商 ID |
|
||||
| reason | Body | String | 是 | 1~500 字符 | 审计原因 |
|
||||
| expectedUpdateTime | Body | LocalDateTime | 是 | `yyyy-MM-dd HH:mm:ss` | 最近读取的版本 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| status | String | 固定为 `SUSPENDED` |
|
||||
| updateTime | LocalDateTime | 新并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"reason":"暂停业务合作","expectedUpdateTime":"2026-09-03 08:02:00"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"data":{"supplierId":"2094672459314745346","status":"SUSPENDED","updateTime":"2026-09-03 08:03:00"},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口无空成功结果,也不提供降级成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395005,"message":"当前状态不允许执行该操作","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 仅 `ACTIVE` 可调用;成功后继续通知车务停用关联车队。
|
||||
|
||||
### 5. 恢复合作 `POST /admin/supplier/items/{supplierId}/resume`
|
||||
|
||||
**VO**: `SupplierStatusChangeReqVO / SupplierStatusChangeRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
在 `SUSPENDED` 行点击“恢复合作”。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| supplierId | Path | String | 是 | 正整数雪花 ID | 供应商 ID |
|
||||
| reason | Body | String | 是 | 1~500 字符 | 审计原因 |
|
||||
| expectedUpdateTime | Body | LocalDateTime | 是 | `yyyy-MM-dd HH:mm:ss` | 最近读取的版本 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| status | String | 固定为 `ACTIVE` |
|
||||
| updateTime | LocalDateTime | 新并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"reason":"恢复业务合作","expectedUpdateTime":"2026-09-03 08:03:00"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"data":{"supplierId":"2094672459314745346","status":"ACTIVE","updateTime":"2026-09-03 08:04:00"},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口无空成功结果,也不提供降级成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395014,"message":"数据已被他人修改,请刷新后重试","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 仅 `SUSPENDED` 可调用;恢复供应商不会自动启用车队。
|
||||
|
||||
### 6. 拉入黑名单 `POST /admin/supplier/items/{supplierId}/blacklist`
|
||||
|
||||
**VO**: `SupplierStatusChangeReqVO / SupplierStatusChangeRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
在 `SUSPENDED` 行点击“拉入黑名单”。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| supplierId | Path | String | 是 | 正整数雪花 ID | 供应商 ID |
|
||||
| reason | Body | String | 是 | 1~500 字符 | 审计原因 |
|
||||
| expectedUpdateTime | Body | LocalDateTime | 是 | `yyyy-MM-dd HH:mm:ss` | 最近读取的版本 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| status | String | 固定为 `BLACKLIST` |
|
||||
| updateTime | LocalDateTime | 新并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"reason":"列入合作黑名单","expectedUpdateTime":"2026-09-03 08:03:00"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"data":{"supplierId":"2094672459314745346","status":"BLACKLIST","updateTime":"2026-09-03 08:04:00"},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口无空成功结果,也不提供降级成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395005,"message":"当前状态不允许执行该操作","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 仅 `SUSPENDED` 可调用;`ACTIVE` 直接拉黑返回 `395005`,成功后通知车务停用关联车队。
|
||||
|
||||
### 7. 解除黑名单 `POST /admin/supplier/items/{supplierId}/unblacklist`
|
||||
|
||||
**VO**: `SupplierStatusChangeReqVO / SupplierStatusChangeRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
在 `BLACKLIST` 行点击“解除黑名单”。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| supplierId | Path | String | 是 | 正整数雪花 ID | 供应商 ID |
|
||||
| reason | Body | String | 是 | 1~500 字符 | 审计原因 |
|
||||
| expectedUpdateTime | Body | LocalDateTime | 是 | `yyyy-MM-dd HH:mm:ss` | 最近读取的版本 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| status | String | 固定为 `SUSPENDED` |
|
||||
| updateTime | LocalDateTime | 新并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"reason":"解除合作黑名单","expectedUpdateTime":"2026-09-03 08:04:00"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"data":{"supplierId":"2094672459314745346","status":"SUSPENDED","updateTime":"2026-09-03 08:05:00"},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口无空成功结果,也不提供降级成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395005,"message":"当前状态不允许执行该操作","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 仅 `BLACKLIST` 可调用;成功后回到 `SUSPENDED`,不会直接恢复交易或自动启用车队。
|
||||
|
||||
### 8. 清账归档 `POST /admin/supplier/items/{supplierId}/archive`
|
||||
|
||||
**VO**: `SupplierArchiveRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
在 `SUSPENDED` 或 `BLACKLIST` 行点击“清账归档”。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| supplierId | Path | String | 是 | 正整数雪花 ID | 供应商 ID;无请求体 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| data | SupplierArchiveRespVO | 当前清账能力未交付,不会返回成功数据 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /admin/supplier/items/2094672459314745346/archive
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":395032,"message":"暂无法确认财务已清账,不能归档","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
当前固定失败关闭,不提供空数据或降级成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395005,"message":"当前状态不允许执行该操作","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `ACTIVE` 先返回 `395005`;`SUSPENDED/BLACKLIST` 进入清账门禁后返回 `395032`,两者均零写入。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
| 当前状态 | 仅显示以下操作 | 正确接口 |
|
||||
|---|---|---|
|
||||
| `ACTIVE` | 暂停合作 | `/suspend` |
|
||||
| `SUSPENDED` | 恢复合作、拉入黑名单、清账归档 | `/resume`、`/blacklist`、`/archive` |
|
||||
| `BLACKLIST` | 解除黑名单、清账归档 | `/unblacklist`、`/archive` |
|
||||
|
||||
- `expectedUpdateTime` 必须使用最近一次详情或列表返回值;成功后使用响应的新版本刷新页面。
|
||||
- 统一响应可能由 HTTP 200 承载业务失败,必须同时判断 `code` 和 `success`。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 当前主表中的旧 `FROZEN` 已转换为 `SUSPENDED`;历史审批与变更快照不改写。
|
||||
- 四个状态接口成功时更新当前状态和版本,并各写一条既有变更审计;失败路径不写主体、审批或审计。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录返回业务码 `401`。
|
||||
- 生命周期写操作要求 `SUPER_ADMIN` 和 `supplier:status:manage`;不满足返回 `395004`。
|
||||
- `395005` 表示来源状态错误;`395014` 表示版本过期;`395032` 表示清账能力不可用;`100502` 表示 5 秒内重复提交。
|
||||
- 暂停、拉黑继续通知车务停用关联车队;恢复、解除黑名单不会自动启用车队。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### `status`(供应商当前生命周期)
|
||||
|
||||
| 值 | 中文 | 菜单语义 |
|
||||
|---|---|---|
|
||||
| `ACTIVE` | 合作中 | 仅暂停合作 |
|
||||
| `SUSPENDED` | 暂停合作 | 恢复、拉黑、清账归档 |
|
||||
| `BLACKLIST` | 黑名单 | 解除黑名单、清账归档 |
|
||||
|
||||
`DRAFT/VETTING/ARCHIVED` 保持既有语义;当前接口不再产生或返回主表状态 `FROZEN`。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 场景 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 暂停合作 | 当前状态进入 `FROZEN` | 当前状态进入 `SUSPENDED` |
|
||||
| 恢复合作 | 无独立接口 | 新增 `/resume` |
|
||||
| `ACTIVE` 直接拉黑 | 可进入黑名单 | 返回 `395005`,零写入 |
|
||||
| 解除黑名单 | 无独立接口 | 新增 `/unblacklist`,返回 `SUSPENDED` |
|
||||
| 归档来源 | `ACTIVE/BLACKLIST` | `SUSPENDED/BLACKLIST` |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 是。依赖当前态 `FROZEN` 或 `ACTIVE` 直接拉黑的调用方式必须调整。
|
||||
- **前端是否必须同步上线**: 是。
|
||||
- **前端 workaround 清理点**: 删除当前态 `FROZEN` 的筛选、文案与按钮分支。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台供应商当前状态展示与生命周期操作。
|
||||
- **零影响**: 草稿建档、注册审批、收款账户、资源绑定、小程序与历史审批/变更快照展示。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- `ACTIVE → SUSPENDED → BLACKLIST → SUSPENDED → ACTIVE` 全链路通过,列表与详情状态一致。
|
||||
- `ACTIVE` 直接拉黑/归档返回 `395005`;两种可归档来源均返回 `395032` 且零写入。
|
||||
- 乐观锁、重复提交、未认证门禁通过;四次成功动作写四条变更审计,审批记录未增加。
|
||||
- 部署提交:[`8d38f69d2d6ebd99fb25fb6b0f5dc50ce4cfa1c0`](https://git.1814.love:8443/wx/HL/commit/8d38f69d2d6ebd99fb25fb6b0f5dc50ce4cfa1c0)。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- [Issue #6979](https://git.1814.love:8443/wx/HL/issues/6979)
|
||||
- [PR #7007](https://git.1814.love:8443/wx/HL/pulls/7007)
|
||||
|
||||
## 前端动作与当前状态
|
||||
|
||||
- 移除当前态 `FROZEN`,统一映射 `SUSPENDED` 为“暂停合作”。
|
||||
- 按状态矩阵接入两个新增接口并收紧按钮显隐;`395032` 不得更新页面为已归档。
|
||||
- **当前状态:待前端接入。**
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#6979](https://git.1814.love:8443/wx/HL/issues/6979)
|
||||
- **PR**: [#7007](https://git.1814.love:8443/wx/HL/pulls/7007)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,374 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6986"
|
||||
title: "团期物资支持从备品库选择配置"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "4d82bdb5"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-03"
|
||||
status_note: "PR #6987 已合入 dev-v3;2026-09-03 经测试环境网关实测,10 条正负向用例全部通过。前端尚未接入"
|
||||
updated_at: "2026-09-03"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期物资: 新增备品库候选列表接口,并放宽 suppliesName 必填约束
|
||||
|
||||
> **服务**: hl-order-service-v3 + hl-resource-service(**跨两个服务**)
|
||||
> **PR**: #6987
|
||||
> **Issue**: #6986
|
||||
> **影响范围**: 管理后台「团期详情 → 物资清单 → 从备品库选择」
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**新增物资行 `POST /v3/admin/order/group-batch/:groupBatchId/supplies` 的 `suppliesName` 不再无条件必填。**
|
||||
|
||||
- 前端以前以为:`suppliesName` 是 `@NotBlank`,任何情况下都得填。
|
||||
- 实际现在是:改为 `@AssertTrue` **二选一**——传了 `suppliesResourceId` 就可以不填名称
|
||||
(名称、分类、计费方式从备品库取,**入参传了也会被库值覆盖**);不传 `suppliesResourceId` 时名称仍必填。
|
||||
- 这解决的是原先「从备品库选一条加进来,却仍被迫手填名字」与「名称以库为准」自相矛盾的问题。
|
||||
|
||||
**部署要求**:本次跨 order-v3 与 resource-service 两个服务,**必须同时部署**。
|
||||
只发 order-v3 会调不到资源域新增的 `/internal/supplies/list-available`,候选列表直接报 589518。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
物资清单原先只能手工逐条录入,名称、分类、计费方式、单价全靠人填,既慢又容易与备品库口径不一致。
|
||||
本次接入资源域备品库:团期管理员从库里勾选,基础信息由库带出。
|
||||
|
||||
**资源域改动**:新增 `GET /internal/supplies/list-available`(内部接口,不对外)。
|
||||
order-v3 侧通过 Feign 调用,并带降级工厂。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 团期物资候选列表 | GET | `/v3/admin/order/group-batch/:groupBatchId/supplies/candidates` | **新增接口** | 从备品库拉可选备品,标记本团期已加入项 |
|
||||
| 2 | 新增团期物资行 | POST | `/v3/admin/order/group-batch/:groupBatchId/supplies` | **请求体校验放宽** | `suppliesName` 由必填改为与 `suppliesResourceId` 二选一 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 团期物资候选列表 `GET /v3/admin/order/group-batch/:groupBatchId/supplies/candidates`
|
||||
|
||||
**VO**: `SuppliesCandidateRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
「物资清单 → 从备品库选择」弹窗打开时调用,渲染可选备品列表。
|
||||
`added=true` 的项应显示为已勾选,其 `batchSuppliesId` 即清单中对应行的 ID,可直接用于删除。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `groupBatchId` | Path | Long | ✅ | — | 团期主订单 ID |
|
||||
| `categoryCode` | Query | String | ❌ | 字典 `supplies_category` | 分类筛选。传不存在的分类返回空数组,不报错 |
|
||||
| `keyword` | Query | String | ❌ | — | 关键字,匹配备品**名称或副标题**。中文需 URL 编码 |
|
||||
|
||||
#### 出参 `Result<List<SuppliesCandidateRespVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `suppliesResourceId` | Long | 备品资源 ID。**加入清单时回传本值** |
|
||||
| `suppliesName` | String | 备品名称(来自库) |
|
||||
| `subtitle` | String | 副标题 |
|
||||
| `category` | String | 分类编码 |
|
||||
| `billingType` | String | 计费方式,**已归一为产品域词表** `PER_PERSON` / `PER_QUANTITY` |
|
||||
| `hasCost` | Boolean | 是否计费(资源域 `isCharged=1` 时为 true) |
|
||||
| `unitPrice` | BigDecimal | 基础价,加入清单时作为 `unitPrice` 默认值 |
|
||||
| `unit` | String | 计量单位 |
|
||||
| `added` | Boolean | 是否已加入本团期清单,**true 时前端应显示为已勾选** |
|
||||
| `batchSuppliesId` | Long | 已加入时对应的清单行 ID;未加入为 `null` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/1/supplies/candidates?categoryCode=personal_gear
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"suppliesResourceId": "2023438566624866305",
|
||||
"suppliesName": "定制遮阳帽",
|
||||
"subtitle": "呼籁旅行专属防晒鸭舌帽",
|
||||
"category": "personal_gear",
|
||||
"billingType": "PER_PERSON",
|
||||
"hasCost": true,
|
||||
"unitPrice": 15.00,
|
||||
"unit": "顶",
|
||||
"added": false,
|
||||
"batchSuppliesId": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
备品库无匹配项(含分类不存在、关键字无命中)时返回空数组,不报错:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
资源域不可达时**不静默降级为空**,而是显式报 589518,避免前端误判为「库里没有备品」。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
备品库查询失败(资源域未部署或不可达):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589518,
|
||||
"message": "备品库查询失败,请稍后重试",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 只读接口,**不产生任何写入**,可安全重复调用
|
||||
- 资源域只返回可用备品,已下架的不在候选池
|
||||
- `billingType` 在 order-v3 侧做过归一,前端**不必**再兼容资源域原始词表
|
||||
- `added` 依据 `suppliesResourceId` 匹配;纯手填(无 resourceId)的清单行不会影响任何候选项的勾选态
|
||||
- 同一备品在清单中重复存在时,`batchSuppliesId` 取首条
|
||||
|
||||
---
|
||||
|
||||
### 2. 新增团期物资行 `POST /v3/admin/order/group-batch/:groupBatchId/supplies`
|
||||
|
||||
**VO**: `AddSuppliesReqVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
两种来源共用本接口:从备品库勾选加入(传 `suppliesResourceId`),或手工录入临时物资(传 `suppliesName`)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `groupBatchId` | Path | Long | ✅ | — | 团期主订单 ID |
|
||||
| `suppliesResourceId` | Body | Long | ❌ | 与 `suppliesName` **二选一** | 备品资源 ID。传了则名称/分类/计费方式从库取 |
|
||||
| `suppliesName` | Body | String | ❌ | 与 `suppliesResourceId` **二选一** | **变更前** `@NotBlank` 无条件必填;**变更后**传了 resourceId 时可空,且**传了也会被库值覆盖** |
|
||||
| `quantity` | Body | Integer | ✅ | `@NotNull` `@Min(1)` | 数量 |
|
||||
| `category` | Body | String | ❌ | — | 分类,传 resourceId 时以库为准 |
|
||||
| `hasCost` | Body | Boolean | ❌ | — | 是否计费 |
|
||||
| `billingType` | Body | String | ❌ | — | 计费方式 |
|
||||
| `unitPrice` | Body | BigDecimal | ❌ | — | 单价,缺省取库基础价 |
|
||||
| `sortOrder` | Body | Integer | ❌ | — | 排序 |
|
||||
| `remark` | Body | String | ❌ | — | 备注 |
|
||||
|
||||
#### 出参 `Result<Long>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data` | Long | 新建清单行 ID(`batchSuppliesId`),可直接用于删除或调整数量 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"suppliesResourceId": 2023438566624866305,
|
||||
"quantity": 2
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": "2095411540751519745"
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口必然返回行 ID 或错误,无空数据形态。校验失败时 `data` 为 `null` 且 `success=false`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
名称与备品库 ID 都没传(**本次新增的二选一校验**):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "备品名称不能为空(未指定备品库 ID 时必填)",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
所选备品不存在或已下架:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589519,
|
||||
"message": "所选备品不存在或已下架,请重新选择",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 二选一校验在 `@AssertTrue` 阶段完成,**失败时数据库无任何变更**
|
||||
- 传了 `suppliesResourceId` 时,入参里的 `suppliesName` / `category` / `billingType` **会被库值覆盖**,不是「以入参优先」
|
||||
- `quantity` 最小为 1,传 0 或负数返回 400
|
||||
- 同一备品可重复加入,接口不做去重
|
||||
- 纯手填行(无 `suppliesResourceId`)不会出现在候选列表的 `added` 判定中
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- **加入备品库物资时,只需传 `suppliesResourceId` 和 `quantity`**。不要再从候选列表把名称回填进请求体——
|
||||
传了也会被库值覆盖,徒增不一致风险。
|
||||
- **手工录入临时物资时,`suppliesName` 仍然必填**。二选一不等于两个都可以不传。
|
||||
- **勾选态用 `added` 判断,删除用 `batchSuppliesId`**。候选列表已经把行 ID 回填好,无需再查一次清单。
|
||||
- **`keyword` 含中文必须 URL 编码**,否则查询条件丢失(实测未编码时请求异常)。
|
||||
- **589518 与「空数组」含义不同**:前者是资源域挂了,后者是库里确实没有匹配项。前端不应把 589518 展示成「暂无备品」。
|
||||
- 589518 / 589519 是业务码,HTTP 状态仍为 200,判断成败要读响应体的 `code`。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
仅描述外部可观察行为:
|
||||
|
||||
- 接口 1(GET candidates)**只读,不产生任何写入**。
|
||||
- 接口 2(POST supplies)新增一条团期物资清单行并返回其 ID;传 `suppliesResourceId` 时,
|
||||
名称、分类、计费方式、单价默认值取自备品库快照,落库后不再随备品库变动。
|
||||
- 二选一校验与数量校验均发生在写入之前,**校验失败时数据库无任何变更**(已实测复核)。
|
||||
- 本次变更**不涉及表结构调整**,也不对存量物资行做迁移或回填。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 资源域未部署 / 不可达 → 589518,**不降级为空数组**
|
||||
- 备品库无匹配 → 返回 `[]`,不报错
|
||||
- `categoryCode` 传不存在的分类 → 返回 `[]`
|
||||
- 备品库 ID 不存在或已下架 → 589519
|
||||
- 既无 `suppliesName` 又无 `suppliesResourceId` → 400
|
||||
- `quantity` 为 0、负数或缺失 → 400
|
||||
- 未登录 → 401(网关拦截)
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### `billingType`(已归一为产品域词表)
|
||||
|
||||
| 取值 | 含义 |
|
||||
|------|------|
|
||||
| `PER_PERSON` | 按人计费 |
|
||||
| `PER_QUANTITY` | 按数量计费 |
|
||||
|
||||
> order-v3 侧已对资源域原始词表做过归一,前端直接消费这两个值即可。
|
||||
|
||||
### `category`(字典 `supplies_category`)
|
||||
|
||||
分类编码由字典维护,如 `personal_gear`(个人装备)、`camping_equipment`(露营装备)等。
|
||||
本接口不校验分类是否存在,传未知值返回空数组。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、错误码
|
||||
|
||||
段位 `589500-589599`,owner `hl-order-service-v3`(`GroupBatchErrorCode`)。
|
||||
|
||||
| 码 | 符号 | 消息 | 触发 |
|
||||
|---|---|---|---|
|
||||
| `589518` | `SUPPLIES_LIBRARY_FETCH_FAILED` | 备品库查询失败,请稍后重试 | **本次新增**。资源域 `list-available` 返回失败或空结果对象 |
|
||||
| `589519` | `SUPPLIES_RESOURCE_NOT_AVAILABLE` | 所选备品不存在或已下架,请重新选择 | **本次新增**。`suppliesResourceId` 在库中查不到 |
|
||||
| `400` | — | 备品名称不能为空(未指定备品库 ID 时必填) | **本次新增**。二选一校验未通过 |
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**:管理后台团期详情的「物资清单」模块
|
||||
- **零影响**:
|
||||
- 已有的物资清单查询、调整数量、删除行、确认物料四个接口的出参
|
||||
- 存量团期物资行数据(不迁移,新校验只在下次新增时触发)
|
||||
- 资源域备品库自身的管理接口(本次只新增一个内部只读端点)
|
||||
- 团期状态机与物料确认闸门
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
✅ **已验证。** 2026-09-03 于测试环境网关实测,真实鉴权(管理端 admin 账号)。
|
||||
|
||||
- 网关:`https://api.test.1814.love:9443`,分支 `dev-v3`
|
||||
- 前置确认:order-v3 与 resource-service **两侧均已部署**(候选列表能返回资源域真实数据即为证)
|
||||
|
||||
| # | 用例 | 期望 | 实测 |
|
||||
|---|---|---|---|
|
||||
| 1 | GET candidates 无筛选 | 200,返回候选 | ✅ 200,15 条 |
|
||||
| 2 | `?categoryCode=personal_gear` | 200,按分类收窄 | ✅ 200,5 条 |
|
||||
| 3 | `?keyword=帽`(URL 编码) | 200,按名称匹配 | ✅ 200,1 条「定制遮阳帽」 |
|
||||
| 4 | `?categoryCode` 传不存在分类 | 200 空数组 | ✅ 200,0 条 |
|
||||
| 5 | GET candidates 无 Authorization | 401 | ✅ `401 缺少有效的 Authorization 头` |
|
||||
| 6 | POST 既无名称也无 resourceId | 400 | ✅ `400 备品名称不能为空(未指定备品库 ID 时必填)` |
|
||||
| 7 | POST `quantity=0` | 400 | ✅ `400 数量至少为 1` |
|
||||
| 8 | POST 缺 `quantity` | 400 | ✅ `400 数量不能为空` |
|
||||
| 9 | POST `suppliesResourceId` 不存在 | 589519 | ✅ `589519 所选备品不存在或已下架,请重新选择` |
|
||||
| 10 | POST 只传 resourceId 不传名称 | 放行,名称取库值 | ✅ 200,落库 `suppliesName="定制遮阳帽"` 与库一致,`unitPrice=15.00`、`billingType=PER_PERSON` |
|
||||
|
||||
**行为变更核对**:用例 10 是本次核心——入参未传 `suppliesName` 仍成功建行,名称由备品库带出。
|
||||
|
||||
**联动核对**:加入后重查候选列表,该项 `added=true`、`batchSuppliesId` 回填为新建行 ID;删除后回到 `added=false`。
|
||||
|
||||
**未落库核对**:用例 6–9 执行后复查清单仍为 0 条,证明校验失败不产生写入。
|
||||
|
||||
**测试数据还原**:用例 10 建的行已 `DELETE` 删除,清单由测试前 0 条恢复为 0 条。
|
||||
|
||||
本地单元测试(提交信息记载):`GroupBatchSuppliesServiceTest` 新增 208 行用例。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 团期需求文档:`docs/group/`(dev-v3 分支)
|
||||
- 实施单 AC-TD-13(`ad747212d` 已标记 #6986 落地并勘误)
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: #6986
|
||||
- **PR**: #6987(合并提交 `bcf9332bd`,落入 `dev-v3`)
|
||||
- **后续修正**: `8806315e3`(备品去重,同时关联 #6950)
|
||||
- **服务**: hl-order-service-v3 + hl-resource-service
|
||||
- **后端**: jw
|
||||
- **前端**: 待认领(`frontend_status: pending`)
|
||||
@@ -0,0 +1,175 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7013"
|
||||
title: "供应商清账归档启用"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "f3134941"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-03"
|
||||
status_note: "归档接口已可成功归档;前端需移除旧 395032 禁用提示并接入归档请求。"
|
||||
updated_at: "2026-09-03"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商模块:清账归档启用
|
||||
|
||||
> **服务**: `hl-resource-service`(8082)
|
||||
> **Issue**: #7013
|
||||
> **PR**: #7017
|
||||
> **影响范围**: 管理后台供应商列表与详情的“归档”操作
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
此前“归档固定返回 `395032`、前端保持禁用”的约定已失效。符合条件的供应商现在可调用归档接口,成功后状态变为 `ARCHIVED`。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 归档供应商 | POST | `/admin/supplier/items/{supplierId}/archive` | 行为修改 | `SUSPENDED/BLACKLIST → ARCHIVED` |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 归档供应商 `POST /admin/supplier/items/{supplierId}/archive`
|
||||
|
||||
**VO**: `SupplierArchiveRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
在供应商列表或详情中归档当前状态为 `SUSPENDED` 或 `BLACKLIST` 的供应商。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| supplierId | Path | String | 是 | 正整数雪花 ID | 无请求体 |
|
||||
|
||||
#### 出参 `Result<SupplierArchiveRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| supplierId | String | 已归档供应商 ID |
|
||||
| status | String | 固定为 `ARCHIVED` |
|
||||
| clearanceRequestId | String / null | 当前固定为 `null` |
|
||||
| checkedAt | String / null | 当前固定为 `null` |
|
||||
| ledgerRevision | String / null | 当前固定为 `null` |
|
||||
| updateTime | String | 归档后的并发版本,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /admin/supplier/items/2094672459314745346/archive
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"supplierId": "2094672459314745346",
|
||||
"status": "ARCHIVED",
|
||||
"clearanceRequestId": null,
|
||||
"checkedAt": null,
|
||||
"ledgerRevision": null,
|
||||
"updateTime": "2026-09-03 10:20:00"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口无空成功结果,也不提供降级成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395005,
|
||||
"message": "当前状态不允许执行该操作",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
| code | 场景 | 前端处理 |
|
||||
|---|---|---|
|
||||
| `401` | 未登录 | 进入既有登录失效流程 |
|
||||
| `395004` | 无归档权限 | 不展示或禁止操作 |
|
||||
| `395005` | 当前状态不是 `SUSPENDED/BLACKLIST`,或已归档后重复请求 | 刷新列表或详情状态 |
|
||||
| `100502` | 短时间重复提交 | 保持当前页面并提示勿重复操作 |
|
||||
|
||||
- `395032` 仅为历史兼容错误码,当前归档入口不再主动返回。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- 当前状态为 `SUSPENDED` 或 `BLACKLIST` 且用户具备归档权限时,显示可点击的“归档”。
|
||||
- 点击后调用 `POST /admin/supplier/items/{supplierId}/archive`,不传请求体。
|
||||
- 必须同时判断 `success` 与 `code`;成功后使用响应中的 `ARCHIVED` 和 `updateTime` 刷新列表或详情。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
归档成功会写入审核记录与状态变更审计,并在同一事务中把供应商主表状态更新为 `ARCHIVED`。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 仅 `SUSPENDED` 和 `BLACKLIST` 可归档;其他状态返回 `395005`。
|
||||
- 归档成功后供应商只允许查看,前端不得继续显示写操作。
|
||||
- 接口路径、请求方式和响应字段均未改变。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
| 值 | 中文 | 菜单语义 |
|
||||
|---|---|---|
|
||||
| `SUSPENDED` | 暂停合作 | 可归档 |
|
||||
| `BLACKLIST` | 黑名单 | 可归档 |
|
||||
| `ARCHIVED` | 已归档 | 仅查看,不再显示归档操作 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 场景 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 合法来源状态归档 | 固定返回 `395032`,前端禁用 | 返回成功结果,状态更新为 `ARCHIVED` |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否;接口路径和响应字段未变,仅启用成功行为。
|
||||
- **前端是否必须同步上线**: 是。
|
||||
- **前端 workaround 清理点**: 删除“归档(暂无法确认财务已清账)”禁用项及 `395032` 固定失败判断。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台供应商归档操作。
|
||||
- **零影响**: 其他供应商状态操作、收款账户、合同、资源绑定及小程序接口。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
TEST Gateway 已验证合法来源状态归档返回 `ARCHIVED`,审核记录与主表状态同步更新。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- [Issue #7013](https://git.1814.love:8443/wx/HL/issues/7013)
|
||||
- [PR #7017](https://git.1814.love:8443/wx/HL/pulls/7017)
|
||||
- 本条覆盖 [#6979 旧归档约定](https://git.1814.love:8443/wx/hl-api-changelog/src/branch/main/changelogs-v2/2026-09/03_6979_%E7%BB%9F%E4%B8%80%E4%BE%9B%E5%BA%94%E5%95%86%E6%9A%82%E5%81%9C%E7%8A%B6%E6%80%81%E4%B8%8E%E6%8B%89%E9%BB%91%E5%BD%92%E6%A1%A3%E6%B5%81%E8%BD%AC-%E4%BF%AE%E6%94%B9%E6%8E%A5%E5%8F%A3-%E7%AE%A1%E7%90%86%E5%90%8E%E5%8F%B0.md) 中“固定返回 `395032`”的说明。
|
||||
|
||||
## 前端动作与当前状态
|
||||
|
||||
- 移除归档菜单的固定禁用状态和旧清账提示。
|
||||
- 对 `SUSPENDED/BLACKLIST` 接入归档请求;成功后刷新为 `ARCHIVED`。
|
||||
- **当前状态:待前端处理。**
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#7013](https://git.1814.love:8443/wx/HL/issues/7013)
|
||||
- **PR**: [#7017](https://git.1814.love:8443/wx/HL/pulls/7017)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,223 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7029"
|
||||
title: "供应商新增账户接入企微审批"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "908e691f"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-03"
|
||||
status_note: "后端已改为企微异步审批;前端需切换账户新增路径、展示待审批状态并移除本地人工审批入口。"
|
||||
updated_at: "2026-09-03"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商模块:新增账户接入企微审批
|
||||
|
||||
> **服务**: `hl-resource-service`(8082)、`hl-user-service`(8081)
|
||||
> **Issue**: #7029
|
||||
> **PR**: #7031
|
||||
> **影响范围**: 管理后台供应商列表的账户管理
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
已审核通过的供应商新增账户后不再立即生效,也不再由管理端调用人工审批接口;后端会在企微“账户”模板发起审批,账户先返回 `PENDING`,企微通过并同步后才变为 `ACTIVE`。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 批量新增并提交收款账户 | POST | `/admin/supplier/items/{supplierId}/bank-accounts/add` | 行为修改 | 每个账户独立发起企微审批并返回审批状态 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 批量新增并提交收款账户 `POST /admin/supplier/items/{supplierId}/bank-accounts/add`
|
||||
|
||||
**VO**: `SupplierBankAccountBatchCreateReqVO` → `List<BankAccountSubmitResultRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
供应商状态为 `ACTIVE` 后,在账户管理中新增一至五十个收款账户。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| supplierId | Path | String | 是 | 正整数雪花 ID | 已生效供应商 |
|
||||
| accounts | Body | Array | 是 | 1~50 项 | 收款账户列表 |
|
||||
| accounts[].accountType | Body | String | 是 | `CORPORATE` / `PERSONAL` | 账户类型 |
|
||||
| accounts[].bankName | Body | String | 是 | 最长 500 | 开户行 |
|
||||
| accounts[].bankBranch | Body | String | 否 | 最长 500 | 开户支行 |
|
||||
| accounts[].accountNo | Body | String | 是 | 8~128 位,仅数字、空格、`-` | 账号 |
|
||||
| accounts[].proofFileUrls | Body | Array | 否 | 最多 20 项 | 证明附件 |
|
||||
| accounts[].settleMode | Body | String | 否 | `PREPAY` / `MONTHLY` / `SINGLE` | 结算方式 |
|
||||
| accounts[].accountPeriod | Body | String | 条件必填 | `MONTHLY` 时必填,其他方式必须为空 | 账期 |
|
||||
| accounts[].invoiceType | Body | String | 否 | `SPECIAL` / `NORMAL` / `NONE` | 发票类型 |
|
||||
| accounts[].taxRate | Body | String | 条件必填 | 可开票时必填,`NONE` 时必须为空 | `0%`~`100%`,最多两位小数 |
|
||||
|
||||
#### 出参 `Result<List<BankAccountSubmitResultRespVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| accountId | String | 新账户 ID |
|
||||
| approvalLogId | String | 账户审批记录 ID |
|
||||
| requestNo | String | 同一请求重放使用的稳定请求号 |
|
||||
| provider | String | 新申请固定为 `WECOM` |
|
||||
| approvalStatus | String | 新申请为 `PENDING`,通过后为 `APPROVED` |
|
||||
| spNo | String | 企微审批单号 |
|
||||
| spStatus | String / null | 企微原始状态,完成同步前可为空 |
|
||||
| syncStatus | String | 正常建单为 `REQUESTING`,完成应用为 `APPLIED`;异常可能为 `APPLY_FAILED` / `RESULT_UNCERTAIN` |
|
||||
| accountStatus | String | 通过前 `PENDING`,通过后 `ACTIVE` |
|
||||
| isDefault | String | 新增账户固定为 `NO` |
|
||||
| submittedAt | String / null | 提交时间 |
|
||||
| finishedAt | String / null | 完成时间,审批中为空 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"accounts": [
|
||||
{
|
||||
"accountType": "PERSONAL",
|
||||
"bankName": "示例银行",
|
||||
"bankBranch": "示例支行",
|
||||
"accountNo": "6222 0000 1234 5678",
|
||||
"proofFileUrls": [],
|
||||
"settleMode": "SINGLE",
|
||||
"accountPeriod": null,
|
||||
"invoiceType": "NONE",
|
||||
"taxRate": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": [
|
||||
{
|
||||
"accountId": "353807302408683520",
|
||||
"approvalLogId": "353807302408683521",
|
||||
"requestNo": "SUP-ACC-REQ-example",
|
||||
"provider": "WECOM",
|
||||
"approvalStatus": "PENDING",
|
||||
"spNo": "202609030009",
|
||||
"spStatus": null,
|
||||
"syncStatus": "REQUESTING",
|
||||
"accountStatus": "PENDING",
|
||||
"isDefault": "NO",
|
||||
"submittedAt": "2026-09-03 15:45:00",
|
||||
"finishedAt": null
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口不返回空成功列表。企微结果不确定时仍保留账户和审批事实为 `PENDING`,对应项返回 `syncStatus=RESULT_UNCERTAIN`,前端不得自动重提。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395010,
|
||||
"message": "请先完成供应商注册",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
| code | 场景 | 前端处理 |
|
||||
|---|---|---|
|
||||
| `400` | 账户字段、枚举或组合规则不合法 | 保留表单并展示后端提示 |
|
||||
| `395001` | 供应商不存在 | 关闭弹窗并刷新列表 |
|
||||
| `395002` | 无账户管理或提交权限 | 禁止操作 |
|
||||
| `395010` | 供应商不是 `ACTIVE` | 先完成供应商注册审批 |
|
||||
| `395011` | 同一账号已有在途审批 | 不重复提交,刷新账户列表 |
|
||||
| `395027` | 账号已被占用 | 联系财务核实 |
|
||||
|
||||
- 每个账户生成独立审批;顶层 `success=true` 后仍须检查每项 `syncStatus` 和 `accountStatus`。
|
||||
- 企微模板固定展示供应商、账户类型、开户行和账号,审批人由企微模板配置决定,前端不传审批人。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
1. 新增账户只调用 `POST /admin/supplier/items/{supplierId}/bank-accounts/add`,请求体必须使用 `accounts` 数组。
|
||||
2. 成功建单后展示“待审批”,不得把 `PENDING` 当作可用账户,也不得调用旧的 `/supplier/{supplierId}/accounts/{accountId}/approve`。
|
||||
3. 通过 `GET /admin/supplier/items/{supplierId}/account-info/list` 刷新状态;只有 `status=ACTIVE` 才展示为生效中。
|
||||
4. 必须同时判断顶层 `success/code` 与每项 `syncStatus/accountStatus`;`RESULT_UNCERTAIN` 只提示对账,不自动重试。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
提交成功后先持久化 `PENDING`、非默认账户及对应审批记录;企微审批通过后异步更新为 `ACTIVE`,并追加审批关联的账户启用审计。外部提交失败或结果不确定时不会把账户标记为生效。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 供应商创建时携带的初始账户仍随供应商注册审批,通过前为 `PENDING`,通过后为 `ACTIVE`。
|
||||
- 后续新增账户与供应商创建时是否携带初始账户无关,只要求供应商当前为 `ACTIVE`。
|
||||
- 企微驳回或撤销不会激活账户;终态由后端回调或轮询同步。
|
||||
- 接口使用统一 `Result`;业务错误可能仍为 HTTP 200,必须读取响应体 `code/success`。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### accountStatus / 账户列表 status
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `PENDING` | 待审批 | 不可作为生效账户使用 |
|
||||
| `ACTIVE` | 生效中 | 企微审批通过且后端已完成应用 |
|
||||
| `DISABLED` | 已停用 | 本工单未修改停用流程 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 行为 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 新增账户 | 本地自动通过并立即 `ACTIVE` | 企微建单后先 `PENDING`,通过后才 `ACTIVE` |
|
||||
| 审批动作 | 管理端存在本地人工审批调用 | 仅在企微处理,管理端只刷新状态 |
|
||||
| 新增路径 | 旧页面调用 `/supplier/{supplierId}/accounts` | 调用 `/admin/supplier/items/{supplierId}/bank-accounts/add` |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 是;新增账户由同步生效改为异步审批。
|
||||
- **前端是否必须同步上线**: 是。
|
||||
- **前端 workaround 清理点**: 删除账户本地审批按钮及 `/supplier/{supplierId}/accounts/{accountId}/approve` 调用。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台供应商账户新增及状态展示。
|
||||
- **零影响**: 账户详情读取、默认账户切换、停用流程、合同、资源绑定和小程序接口。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
TEST Gateway 已验证:初始账户随注册审批由 `PENDING` 转为 `ACTIVE`;后续账户使用指定企微模板建单并在通过后由 `PENDING` 转为 `ACTIVE`,审批记录和启用审计均可查询;非法账户类型返回业务失败且零写入。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- [Issue #7029](https://git.1814.love:8443/wx/HL/issues/7029)
|
||||
- [PR #7031](https://git.1814.love:8443/wx/HL/pulls/7031)
|
||||
|
||||
## 前端动作与当前状态
|
||||
|
||||
- 将账户新增改为批量接口及 `accounts` 请求结构。
|
||||
- 新增后展示 `PENDING`,轮询或刷新账户列表,直到后端返回 `ACTIVE`。
|
||||
- 移除管理端账户审批按钮和旧人工审批请求。
|
||||
- **当前状态:待前端处理。**
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#7029](https://git.1814.love:8443/wx/HL/issues/7029)
|
||||
- **PR**: [#7031](https://git.1814.love:8443/wx/HL/pulls/7031)
|
||||
- **Merge commit**: [f5dfa7771a61948bb4d59e35fbedb67d61fcb3f3](https://git.1814.love:8443/wx/HL/commit/f5dfa7771a61948bb4d59e35fbedb67d61fcb3f3)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,368 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7036"
|
||||
title: "供应商主体状态变更接入企微审批"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "68ecb96b"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-03"
|
||||
status_note: "后端已部署并完成 TEST 验收;四类状态动作改为企微异步审批,前端需适配审批中与终态刷新。"
|
||||
updated_at: "2026-09-03"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商模块:主体状态变更接入企微审批
|
||||
|
||||
> **服务**: `hl-resource-service`、`hl-user-service`
|
||||
> **Issue**: #7036
|
||||
> **PR**: #7043、#7046
|
||||
> **影响范围**: 管理后台供应商状态操作及审批进度展示
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
列入黑名单、解除黑名单和两种清账归档不再同步改变状态:接口先返回企微审批单,审批通过后才异步迁移;解除黑名单需两个不同审批人依次通过,其余三项均为一级审批。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 列入黑名单 | POST | `/admin/supplier/items/{supplierId}/blacklist` | 行为及响应修改 | 先发起一级企微审批 |
|
||||
| 2 | 解除黑名单 | POST | `/admin/supplier/items/{supplierId}/unblacklist` | 行为及响应修改 | 两个不同审批人串行通过后生效 |
|
||||
| 3 | 清账归档 | POST | `/admin/supplier/items/{supplierId}/archive` | 行为及响应修改 | 按来源状态发起一级企微审批 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 列入黑名单 `POST /admin/supplier/items/{supplierId}/blacklist`
|
||||
|
||||
**VO**: `SupplierStatusChangeReqVO` → `SupplierStatusChangeRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
对 `SUSPENDED` 供应商发起“列入黑名单”一级审批。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| supplierId | Path | String | 是 | 正整数 | 供应商 ID |
|
||||
| reason | Body | String | 是 | 1~500 字符 | 变更原因 |
|
||||
| expectedUpdateTime | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 页面读取到的并发版本 |
|
||||
|
||||
#### 出参 `Result<SupplierStatusChangeRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| supplierId | String | 供应商 ID |
|
||||
| status | String | 审批中仍为 `SUSPENDED`,通过后为 `BLACKLIST` |
|
||||
| updateTime | String | 当前并发版本 |
|
||||
| approval.approvalLogId | String | 审批记录 ID |
|
||||
| approval.provider | String | 固定为 `WECOM` |
|
||||
| approval.approvalStatus | String | `PENDING` / `APPROVED` / `REJECTED` / `CANCELED` |
|
||||
| approval.spNo | String | 企微审批单号 |
|
||||
| approval.syncStatus | String | 本地同步状态 |
|
||||
| approval.submittedAt | String | 提交时间 |
|
||||
| approval.finishedAt | String / null | 审批完成时间 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"reason": "严重违约",
|
||||
"expectedUpdateTime": "2026-09-03 10:18:27"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"supplierId": "2094672459314745346",
|
||||
"status": "SUSPENDED",
|
||||
"updateTime": "2026-09-03 10:18:27",
|
||||
"approval": {
|
||||
"approvalLogId": "2095450331436515330",
|
||||
"provider": "WECOM",
|
||||
"approvalStatus": "PENDING",
|
||||
"spNo": "202609030012",
|
||||
"syncStatus": "REQUESTING",
|
||||
"submittedAt": "2026-09-03 18:05:57",
|
||||
"finishedAt": null
|
||||
}
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口不返回空成功数据;企微提交失败返回业务失败,供应商保持 `SUSPENDED`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395005,
|
||||
"message": "当前状态不允许执行该操作",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 需要供应商状态管理权限;`expectedUpdateTime` 不一致返回 `395014`。
|
||||
- 顶层成功仅表示企微建单成功,前端不得立即展示为黑名单。
|
||||
- 驳回或撤销不改变供应商状态。
|
||||
|
||||
### 2. 解除黑名单 `POST /admin/supplier/items/{supplierId}/unblacklist`
|
||||
|
||||
**VO**: `SupplierStatusChangeReqVO` → `SupplierStatusChangeRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
对 `BLACKLIST` 供应商发起“解除黑名单”两级串行审批。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| supplierId | Path | String | 是 | 正整数 | 供应商 ID |
|
||||
| reason | Body | String | 是 | 1~500 字符 | 解除原因 |
|
||||
| expectedUpdateTime | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 页面读取到的并发版本 |
|
||||
|
||||
#### 出参 `Result<SupplierStatusChangeRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| supplierId | String | 供应商 ID |
|
||||
| status | String | 两级审批完成前仍为 `BLACKLIST`,通过后为 `SUSPENDED` |
|
||||
| updateTime | String | 当前并发版本 |
|
||||
| approval.approvalLogId | String | 审批记录 ID |
|
||||
| approval.provider | String | 固定为 `WECOM` |
|
||||
| approval.approvalStatus | String | 审批状态 |
|
||||
| approval.spNo | String | 企微审批单号 |
|
||||
| approval.syncStatus | String | 本地同步状态 |
|
||||
| approval.submittedAt | String | 提交时间 |
|
||||
| approval.finishedAt | String / null | 审批完成时间 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"reason": "整改完成,申请解除",
|
||||
"expectedUpdateTime": "2026-09-02 15:49:06"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"supplierId": "2091381911266967553",
|
||||
"status": "BLACKLIST",
|
||||
"updateTime": "2026-09-02 15:49:06",
|
||||
"approval": {
|
||||
"approvalLogId": "2095450475109527553",
|
||||
"provider": "WECOM",
|
||||
"approvalStatus": "PENDING",
|
||||
"spNo": "202609030013",
|
||||
"syncStatus": "REQUESTING",
|
||||
"submittedAt": "2026-09-03 18:06:31",
|
||||
"finishedAt": null
|
||||
}
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口不返回空成功数据;企微提交失败返回业务失败,供应商保持 `BLACKLIST`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395014,
|
||||
"message": "数据已被他人修改,请刷新后重试",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 必须由企微模板配置的两个不同审批人按顺序通过,只完成一级时状态不变。
|
||||
- 通过后的目标为 `SUSPENDED`,不会直接恢复到 `ACTIVE`。
|
||||
- 驳回、撤销或审批链不完整均不改变供应商状态。
|
||||
|
||||
### 3. 清账归档 `POST /admin/supplier/items/{supplierId}/archive`
|
||||
|
||||
**VO**: `Void` → `SupplierArchiveRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
`SUSPENDED` 选择“终止且账清”,或 `BLACKLIST` 选择“拉黑且账清”,发起一级审批并在通过后归档。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| supplierId | Path | String | 是 | 正整数 | 供应商 ID |
|
||||
|
||||
#### 出参 `Result<SupplierArchiveRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| supplierId | String | 供应商 ID |
|
||||
| status | String | 审批中保持来源状态,通过后为 `ARCHIVED` |
|
||||
| clearanceRequestId | null | 本期不提供权威清账凭证 |
|
||||
| checkedAt | null | 本期不提供权威清账核验时间 |
|
||||
| ledgerRevision | null | 本期不提供账本版本 |
|
||||
| updateTime | String | 当前并发版本 |
|
||||
| approval.approvalLogId | String | 审批记录 ID |
|
||||
| approval.provider | String | 固定为 `WECOM` |
|
||||
| approval.approvalStatus | String | 审批状态 |
|
||||
| approval.spNo | String | 企微审批单号 |
|
||||
| approval.syncStatus | String | 本地同步状态 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /admin/supplier/items/2095047059194138625/archive
|
||||
Authorization: Bearer <管理端登录凭证>
|
||||
|
||||
无请求体
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"supplierId": "2095047059194138625",
|
||||
"status": "SUSPENDED",
|
||||
"clearanceRequestId": null,
|
||||
"checkedAt": null,
|
||||
"ledgerRevision": null,
|
||||
"updateTime": "2026-09-03 09:48:55",
|
||||
"approval": {
|
||||
"approvalLogId": "2095450500279549953",
|
||||
"provider": "WECOM",
|
||||
"approvalStatus": "PENDING",
|
||||
"spNo": "202609030014",
|
||||
"syncStatus": "REQUESTING"
|
||||
}
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口不返回空成功数据;企微提交失败返回业务失败,供应商保持来源状态。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395021,
|
||||
"message": "企业微信申请提交失败,请稍后重试",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `SUSPENDED` 和 `BLACKLIST` 分别映射为两个固定变更事项,但均只需一级审批。
|
||||
- 接口不接收清账证据;三个预留清账字段继续返回 `null`。
|
||||
- 驳回或撤销不归档,`ARCHIVED` 仍为只读终态。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
1. 提交动作后以 `approval.approvalStatus` 展示审批中,不以 HTTP 200 推断状态已经变化。
|
||||
2. 轮询 `GET /admin/supplier/items/{supplierId}/approval-history/page` 展示审批层级及终态,同时刷新 `GET /admin/supplier/items/{supplierId}/basic-info/view` 获取最终主体状态。
|
||||
3. `GET /admin/supplier/items/{supplierId}/change-records/page` 只展示审批通过后真正发生的状态变化;审批中、驳回和撤销没有对应变更记录。
|
||||
4. 企微表单的供应商、变更事项、当前状态、目标状态、申请原因、当前余额、最近拉黑记录、详情链接和申请人均由后端填写,前端不传审批人或模板控件值。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
提交成功后可立即查询到独立审批记录,但主体状态和变更记录不变;企微有效审批链全部通过后,状态迁移与新增关联变更记录同时完成。驳回或撤销只结束审批记录,不产生状态变更记录。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录或无供应商状态管理权限时拒绝写入。
|
||||
- 同一供应商已有在途状态审批时复用该审批,不重复建单。
|
||||
- 企微提交失败返回 `395021`;结果不确定返回 `395022`,前端不得自动重复提交。
|
||||
- 暂停合作和恢复合作仍沿用原直接状态迁移,不进入本审批模板。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
| 动作 | 来源状态 | 企微变更事项 | 审批层级 | 通过后状态 |
|
||||
|---|---|---|---:|---|
|
||||
| 列入黑名单 | `SUSPENDED` | 列入黑名单 | 1 | `BLACKLIST` |
|
||||
| 解除黑名单 | `BLACKLIST` | 解除黑名单 | 2 | `SUSPENDED` |
|
||||
| 清账归档 | `SUSPENDED` | 终止且账清 | 1 | `ARCHIVED` |
|
||||
| 清账归档 | `BLACKLIST` | 拉黑且账清 | 1 | `ARCHIVED` |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 行为 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 列入/解除黑名单 | 请求内直接迁移 | 企微审批通过后迁移 |
|
||||
| 清账归档 | 旧审批提供方语义 | 指定企微模板一级审批 |
|
||||
| 解除黑名单 | 无两级企微门禁 | 两个不同审批人串行通过 |
|
||||
| 接口响应 | 仅返回状态 | 追加 `approval` 审批受理信息 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 是;状态动作从同步完成改为异步审批。
|
||||
- **前端是否必须同步上线**: 是。
|
||||
- **前端 workaround 清理点**: 移除请求成功即展示目标状态的逻辑,改为展示审批中并刷新审批历史与主体状态。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台供应商主体的列入黑名单、解除黑名单和清账归档。
|
||||
- **零影响**: 暂停合作、恢复合作、供应商注册审批、后续新增账户审批、合同及资源绑定。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- 四类动作均通过指定企微模板建单;审批完成前保持来源状态,审批记录与状态变更记录分表保存。
|
||||
- “列入黑名单”“终止且账清”“拉黑且账清”一级通过后分别迁移为 `BLACKLIST`、`ARCHIVED`、`ARCHIVED`。
|
||||
- “解除黑名单”一级通过后仍为 `BLACKLIST`,由第二位不同审批人通过后迁移为 `SUSPENDED`。
|
||||
- 空申请原因返回业务 `400` 且未新增审批记录。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- [Issue #7036](https://git.1814.love:8443/wx/HL/issues/7036)
|
||||
- [PR #7043](https://git.1814.love:8443/wx/HL/pulls/7043)
|
||||
- [补充 PR #7046](https://git.1814.love:8443/wx/HL/pulls/7046)
|
||||
|
||||
## 前端动作与当前状态
|
||||
|
||||
- 三个写接口成功后展示“审批中”,并使用返回的 `approval` 信息追踪企微状态。
|
||||
- 解除黑名单展示两级进度;一级通过时仍保持黑名单态。
|
||||
- 仅以主体详情最终状态和变更记录确认动作生效。
|
||||
- **当前状态:待前端处理。**
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#7036](https://git.1814.love:8443/wx/HL/issues/7036)
|
||||
- **PR**: [#7043](https://git.1814.love:8443/wx/HL/pulls/7043)
|
||||
- **Merge commit**: [7c99e96a8f7c0977492a620483d86a7340c400ee](https://git.1814.love:8443/wx/HL/commit/7c99e96a8f7c0977492a620483d86a7340c400ee)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7042"
|
||||
title: "设置供应商列表按资源类型过滤"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "前端缺陷"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "原前端传入 typeCode 并维护类型映射的要求已作废;改由后端工单 #7059 处理。当前状态:无需前端处理。"
|
||||
updated_at: "2026-09-04"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 设置供应商列表按资源类型过滤(更正)
|
||||
|
||||
> **更正(2026-09-04)**:原记录要求前端传入 `typeCode` 并维护资源分类与供应商类型映射,该要求已作废。当前状态:**无需前端处理**。
|
||||
|
||||
## 更正结论
|
||||
|
||||
- 前端无需新增或修改代码,也无需维护资源分类与供应商类型映射。
|
||||
- 原“资源类型口径”和“前端处理要求”全部撤销,不再作为调用约束。
|
||||
- 设置供应商候选列表的类型解析与过滤改由后端工单 #7059 处理。
|
||||
|
||||
## 关联
|
||||
|
||||
- Issue:[#7042](https://git.1814.love:8443/wx/HL/issues/7042)
|
||||
- 后端更正工单:[#7059](https://git.1814.love:8443/wx/HL/issues/7059)
|
||||
- 后端联系人:@lc
|
||||
@@ -0,0 +1,173 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6926"
|
||||
title: "导出补 opsStage 筛选 + 统计条自校验死代码修正(修改接口)"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "ea1b9803"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-04"
|
||||
status_note: "2026-09-04 测试服部署 dev-v3@0c497649b;导出三态与 summary 非法 month/opsStage 忽略经网关 200 全通过"
|
||||
updated_at: "2026-09-04"
|
||||
base: "dev-v3"
|
||||
generated: "2026-09-04T10:34:46+08:00"
|
||||
---
|
||||
|
||||
# 团期看板:导出补 opsStage 筛选(GB-ADM-008)+ 统计条自校验死代码修正(GB-ADM-009)
|
||||
|
||||
> **存放目录**: 二期(order-v3)→ `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #7061(squash 合并 dev-v3@0c497649b)
|
||||
> **Issue**: [#6926](https://git.1814.love:8443/wx/HL/issues/6926)
|
||||
> **日期**: 2026-09-04
|
||||
> **影响范围**: 管理后台团期看板——导出端点新增可选 opsStage 筛选;统计条自校验死代码修正(使未知状态行排除可观测)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**修改接口,向后兼容、无破坏**:① GB-ADM-008 导出新增可选查询参数 `opsStage`(七桶筛选,`FORMED` 展开为 `RESOURCE_PREPARING+MATERIAL_PREPARING` 两态 in 过滤,与 productId/month/keyword 叠加取交集;非法值忽略不报错);② GB-ADM-009 统计条自校验条件 `bucketSum != counted`(永假死代码)改为 `rows.size() != counted`(未知 `batch_status` 行排除后真实触发 ERROR 日志);③ 两端点 month 非法值(如 `2026-13`)跳过月份筛选不 500(与 GB-ADM-001 `parseMonthRange` 同口径);④ `buildCsv` 补 capacity_delta 拟议注释。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景(选填)
|
||||
|
||||
PR #6917(#6904 统计条+导出)复审确认两处要修:008 契约卡查询参数含 `opsStage`(同 GB-ADM-001),实现未接收——前端在桶筛选态点导出时参数被 Spring 静默忽略,导出未按桶过滤的全量数据且无报错;009 的 total 自校验 `bucketSum != counted` 永假(counted 只在桶 merge 成功的同一分支自增,与桶和定义相等),契约要求的不一致记 ERROR 永不触发,未知状态行在 summary 路径被静默丢弃(export 路径已有 ERROR)。另顺手对齐两小口径(month 容错、capacity_delta 注释)。
|
||||
|
||||
---
|
||||
|
||||
## 变更接口
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | GB-ADM-008 团期导出 | GET | `/v3/admin/order/group-batch/export` | 修改接口 | 新增可选 `opsStage` 筛选参数 + month 容错 |
|
||||
| 2 | GB-ADM-009 团期统计条 | GET | `/v3/admin/order/group-batch/summary` | 修改接口(行为修正) | 自校验死代码修正(参数/响应不变);month 容错 |
|
||||
|
||||
---
|
||||
|
||||
## 接口详情
|
||||
|
||||
### 1. GB-ADM-008 团期导出 `GET /v3/admin/order/group-batch/export`(修改接口)
|
||||
|
||||
**请求参数**(★=本期新增,其余不变)
|
||||
|
||||
| 参数 | 位置 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| Authorization / X-Admin-Id | Header | String/Long | ✓ | 网关注入,客户端传值忽略 |
|
||||
| productId | Query | Long | | 产品 ID,缺省不限(既有) |
|
||||
| month | Query | String | | 出团月份 yyyy-MM(既有;本期改容错:非法值跳过筛选不 500,与 001 同口径) |
|
||||
| keyword | Query | String | | 团期编号/名称模糊关键词(既有) |
|
||||
| ★ opsStage | Query | String | | 七桶筛选(RECRUIT/FORMED/PENDING_TRIP/TRAVELLING/AUDITING/CHECKED/DISBANDED),FORMED 展开为两态 in 过滤;与 productId/month/keyword 叠加取交集;非法值忽略不报错 |
|
||||
|
||||
**响应**: 不变(`text/csv` 文件流,固定 10 列,UTF-8 BOM + CRLF,单次上限 2000 行)。
|
||||
|
||||
**错误码**: 不变(589507 无 `group-batch:export`、589517 超 2000 行)。
|
||||
|
||||
**内部实现**:opsStage → `GroupBatchStageBuckets.statusCodesOf()`(唯一解析口,001 的 `resolveOpsStatuses` 委托同源)→ `selectBoardRows(..., opsStatuses, ...)` 的 `inIfPresent(GroupBatchDO::getBatchStatus, opsStatuses)`。
|
||||
|
||||
### 2. GB-ADM-009 团期统计条 `GET /v3/admin/order/group-batch/summary`(行为修正)
|
||||
|
||||
**请求参数**: 不变(productId/month/keyword;**不接受** opsStage——契约原文要求忽略该参数)。
|
||||
|
||||
**响应**: 不变(`GroupBatchSummaryVO`:total + 7 桶 + subOrderCount)。
|
||||
|
||||
**行为修正**: 未知 batch_status 行排除后,`rows.size() != counted` 时记 ERROR 日志(原先 `bucketSum != counted` 永假不触发),total 仍以桶和为准。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
### ✅ 正确 / ❌ 错误调用对照
|
||||
|
||||
| 场景 | 说明 |
|
||||
|------|------|
|
||||
| ✅ export?opsStage=FORMED | 两态 in 过滤(RESOURCE_PREPARING+MATERIAL_PREPARING),与 productId/month/keyword 取交集 |
|
||||
| ✅ export?opsStage=非法值(如 NOT_BUCKET) | 忽略该筛选,不报错,等价不传 |
|
||||
| ✅ export?month=2026-13 | 跳过月份筛选不 500(2026-13 非法) |
|
||||
| ✅ summary 带 opsStage | 忽略(009 契约不接受),响应与不带一致 |
|
||||
| ❌ 导出命中 > 2000 行 | 589517 |
|
||||
| ❌ 无 group-batch:export 授权 | 589507 |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
无表变更、无列变更。唯一写操作:导出成功后逐团期写 `group_batch_status_log` BATCH_EXPORT 留痕(REQUIRES_NEW 独立写事务,导出失败不写留痕),与 #6904 一致。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 非法 opsStage(含未知桶)→ warn 日志「未知的 opsStage 筛选值,忽略该筛选」,返回 null 跳过筛选(与 001 口径一致)。
|
||||
- 非法 month → warn「非法的 month 参数,忽略月份筛选」,跳过月份条件(summary 与 export 均不 500)。
|
||||
- summary 遇未知 batch_status 行 → 不进入任何桶,total=桶和,ERROR「团期看板统计条口径不一致: rows=N bucketSum=M」真正触发。
|
||||
- 空结果:export 仅表头(BOM/CRLF 保持),summary 七桶全 0。
|
||||
- 导出「状态」列对未知状态原样透出(不报错)。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围(显式声明)
|
||||
|
||||
- 未改 CSV 列结构与列序(仍固定 10 列)。
|
||||
- 未建表/未改表/未加列。
|
||||
- 009 响应结构不变(7 桶 + total + subOrderCount)。
|
||||
- 未触碰 #6905(GB-ADM-000~003 看板列表/详情)任何接口与文件。
|
||||
- 网关路由不动(/v3/admin/** 通配已覆盖),无配置中心变更。
|
||||
|
||||
---
|
||||
|
||||
## 验证证据
|
||||
|
||||
**JUnit 定向测试**(dev-v3@0c497649b 合并后,分支内):
|
||||
|
||||
```
|
||||
mvn -pl hl-order-service-v3 -am test -Dtest=GroupBatchBoardStatsServiceTest,GroupBatchStageBucketsTest,GroupBatchBoardStatsControllerTest,GroupBatchMapperEscapeLikeTest -DfailIfNoTests=false
|
||||
Tests run: 34, Failures: 0, Errors: 0(含 opsStage 展开/非法忽略/month 容错/ERROR 日志触发 4 新断言)
|
||||
```
|
||||
|
||||
**全量**: `mvn -pl hl-order-service-v3 -am test` 7897 用例,仅基线 6 失败(3 refund 401-envelope + 3 adjustment 日期敏感,与 dev-v3 基线一致,非本单引入)。
|
||||
|
||||
**网关实测**(部署 dev-v3@0c497649b 后,经 api.test.1814.love:9443):
|
||||
|
||||
```
|
||||
GET /v3/admin/order/group-batch/export?month=2026-06 → 200 text/csv 附件(group-batch-2026-06.csv) ✓
|
||||
GET /v3/admin/order/group-batch/export?month=2026-06&opsStage=FORMED → 200 text/csv ✓
|
||||
GET /v3/admin/order/group-batch/export?month=2026-06&opsStage=NOT_BUCKET → 200 text/csv(非法忽略)✓
|
||||
GET /v3/admin/order/group-batch/summary?month=2026-13 → 200 code=200 total=14(容错跳过,非 500)✓
|
||||
GET /v3/admin/order/group-batch/summary?opsStage=FORMED → 200 code=200(忽略,与不带一致 total=14)✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
- PR #6917(#6904 统计条+导出)——本单为其实施复审返工。
|
||||
- PR #7061——本单(squash 合并 dev-v3@0c497649b)。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 契约卡 GB-ADM-008 / GB-ADM-009(#6902 地基,#6904 契约)。
|
||||
- `changelogs-v2/2026-09/01_6904_...`(新增接口原始文档)。
|
||||
- `changelogs-v2/2026-09/01_6905_...`(看板列表 4 接口,与本单互不覆盖)。
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#6926](https://git.1814.love:8443/wx/HL/issues/6926)
|
||||
- **PR**: [#7061](https://git.1814.love:8443/wx/HL/pulls/7061)
|
||||
- **Merge commit**: [0c497649b](https://git.1814.love:8443/wx/HL/commit/0c497649b)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,306 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7059"
|
||||
title: "设置供应商候选按资源上下文过滤"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "a1a78906"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-04"
|
||||
status_note: "后端已部署并验证;待前端删除本地映射,仅透传资源上下文。"
|
||||
updated_at: "2026-09-04"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 资源管理:设置供应商候选按资源上下文过滤
|
||||
|
||||
> **服务**: hl-resource-service (8082)
|
||||
> **PR**: #7064
|
||||
> **Issue**: #7059
|
||||
> **日期**: 2026-09-04
|
||||
> **影响范围**: 管理后台资源管理、车务管理的“设置供应商”候选与绑定
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
前端不再维护“资源分类 → 供应商类型”映射;候选查询传 `resourceModule`、`resourceId`,后端解析类型并只返回 `ACTIVE` 且匹配的供应商。
|
||||
|
||||
## 一、背景
|
||||
|
||||
#7042 要求前端传 `typeCode` 的结论已撤销。本次由后端统一解析资源上下文,避免景区混入车队供应商,并修正组合备品误用 `SUPPLIES` 的问题。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 有界查询供应商 | GET | `/admin/supplier/items/list` | 新增可选查询参数 | 完整资源上下文下由后端过滤候选 |
|
||||
| 2 | 设置或改绑资源供应商 | PUT | `/admin/supplier/resource-relations/{resourceModule}/{resourceId}/update` | 调整类型解析 | 固定映射模块可省略 `requiredTypeCode` |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 有界查询供应商 `GET /admin/supplier/items/list`
|
||||
|
||||
**VO**: `SupplierListReqVO / SupplierListItemRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
资源管理或车务管理打开“设置供应商”候选列表时调用;搜索时沿用 `keyword`。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `resourceModule` | Query | String | 场景必填 | 与 `resourceId` 同传 | 当前资源模块,不是供应商类型 |
|
||||
| `resourceId` | Query | String | 场景必填 | 正整数,与 `resourceModule` 同传 | 当前资源 ID |
|
||||
| `keyword` | Query | String | 否 | 最长 500 | 搜索关键字 |
|
||||
| `limit` | Query | Integer | 否 | 1~200,默认 50 | 候选列表建议传 200 |
|
||||
| `typeCode` | Query | String | 否 | 通用列表兼容参数 | 设置供应商场景不要传;资源上下文存在时后端忽略调用方值 |
|
||||
| `status` | Query | String | 否 | 通用列表兼容参数 | 设置供应商场景不要传;资源上下文存在时后端固定 `ACTIVE` |
|
||||
|
||||
#### 出参 `Result<List<SupplierListItemRespVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data[].supplierId` | String | 供应商 ID |
|
||||
| `data[].supplierNo` | String | 供应商编号 |
|
||||
| `data[].fullName` | String | 供应商全称 |
|
||||
| `data[].shortName` | String | 供应商简称 |
|
||||
| `data[].types` | Array | 类型列表;元素含 `typeCode`、`typeName`、`isPrimary` |
|
||||
| `data[].status` | String | 资源上下文场景恒为 `ACTIVE` |
|
||||
| 其他既有字段 | - | 响应结构未变化 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/list?resourceModule=SCENIC&resourceId=3001000000000000019&limit=200
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": [
|
||||
{
|
||||
"supplierId": "2091381643661983746",
|
||||
"supplierNo": "SUP2091381643661983746",
|
||||
"fullName": "示例景区供应商",
|
||||
"types": [{ "typeCode": "SCENIC", "typeName": "景区", "isPrimary": true }],
|
||||
"status": "ACTIVE"
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
没有符合条件的供应商时返回 `data: []`;资源不存在、上下文不完整或映射不可用时失败关闭,不降级为全量列表。
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": [], "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "resourceModule与resourceId必须同时提供或同时省略",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 完整资源上下文下,后端校验供应商列表权限、资源关系查看权限、资源存在性和数据范围。
|
||||
- 后端只返回状态为 `ACTIVE` 且包含映射类型的供应商;调用方传入的 `typeCode`、`status` 不会覆盖该规则。
|
||||
- 不传资源上下文时,原有 `typeCode`、`status`、`keyword`、`limit` 通用查询行为保持不变。
|
||||
|
||||
### 2. 设置或改绑资源供应商 `PUT /admin/supplier/resource-relations/{resourceModule}/{resourceId}/update`
|
||||
|
||||
**VO**: `SupplierResourceReassignReqVO / SupplierResourceRelationRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
用户确认候选供应商后首次设置或改绑当前资源的供应商。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `resourceModule` | Path | String | 是 | 见映射表 | 当前资源模块 |
|
||||
| `resourceId` | Path | String | 是 | 正整数 | 当前资源 ID |
|
||||
| `supplierId` | Body | String | 是 | 正整数 | 候选供应商 ID |
|
||||
| `requiredTypeCode` | Body | String | 否 | 最长 64 | 七个固定映射模块应省略,由后端解析 |
|
||||
| `remark` | Body | String | 否 | 最长 500 | 关系备注 |
|
||||
| `changeReason` | Body | String | 是 | 非空,最长 500 | 设置或改绑原因 |
|
||||
| `expectedCurrentSupplierId` | Body | String | 改绑时必填 | 与版本时间同传 | 当前关系供应商 ID |
|
||||
| `expectedRelationUpdateTime` | Body | String | 改绑时必填 | 与当前供应商 ID 同传 | 当前关系并发版本 |
|
||||
|
||||
#### 出参 `Result<SupplierResourceRelationRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data.supplierId` | String | 生效供应商 ID |
|
||||
| `data.resourceModule` | String | 资源模块 |
|
||||
| `data.resourceId` | String | 资源 ID |
|
||||
| `data.requiredTypeCode` | String | 后端解析并冻结的供应商类型 |
|
||||
| `data.updateTime` | String | 后续改绑或解绑使用的并发版本 |
|
||||
| 其他既有字段 | - | 响应结构未变化 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"supplierId": "2091381643661983746",
|
||||
"changeReason": "设置资源供应商"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"supplierId": "2091381643661983746",
|
||||
"resourceModule": "SUPPLIES_COMBO",
|
||||
"resourceId": "2046122595429806082",
|
||||
"requiredTypeCode": "SUPPLIES_COMBO",
|
||||
"updateTime": "2026-09-04 12:00:00"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口成功时返回完整关系;校验失败时返回业务错误和 `data: null`,不会创建或改写关系。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395037,
|
||||
"message": "供应商类型不满足资源关联要求",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 七个固定映射模块由后端确定 `requiredTypeCode`;前端不要提交本地映射值。
|
||||
- 供应商必须处于 `ACTIVE` 且包含所需类型;失败时关系保持不变。
|
||||
- 已有关系改绑仍必须传成对的并发版本字段,既有审计、权限和数据范围规则不变。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
| 场景 | 正确调用 | 错误调用 |
|
||||
|------|----------|----------|
|
||||
| 打开候选列表 | `resourceModule=SCENIC&resourceId={id}&limit=200` | 前端把 `SCENIC` 映射为 `typeCode` 后只按类型查询 |
|
||||
| 搜索候选 | 上述参数继续加 `keyword` | 搜索时丢失资源上下文 |
|
||||
| 设置/改绑 | Path 传资源上下文,Body 省略 `requiredTypeCode` | 前端根据字典或资源分类拼 `requiredTypeCode` |
|
||||
|
||||
前端应删除资源分类与供应商类型的本地映射。`supplier_type` 在“系统管理 → 字典管理”维护供应商类型的名称、状态和排序;它不是资源模块映射配置。资源模块映射由后端统一维护。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
设置或改绑时,关系中冻结后端解析出的 `requiredTypeCode`;本次无表结构、存量数据迁移或跨库写入变化,原有关系审计行为保持不变。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- `resourceModule` 与 `resourceId` 只传一个 → `400`,`resourceModule与resourceId必须同时提供或同时省略`。
|
||||
- 未知模块 → `395034`,`不支持的资源模块`;资源不存在或已删除 → `395035`。
|
||||
- `ACTIVITY`、`COST_ITEM`、`STAFF` 本工单没有默认映射,按资源上下文查询会以 `400 requiredTypeCode不能为空` 失败关闭。
|
||||
- 映射类型在 `supplier_type` 字典缺失或停用 → `400`,`供应商类型不合法或已停用`。
|
||||
- 未登录 → 应用响应 `401`;无权限或超出资源数据范围 → 拒绝访问。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### resourceModule 与 supplier_type
|
||||
|
||||
**所属字段**: `SupplierListReqVO.resourceModule / SupplierResourceRelationRespVO.requiredTypeCode` | **类型**: `String`
|
||||
|
||||
| `resourceModule` | 后端要求的 `supplier_type` | 页面 |
|
||||
|------------------|-------------------------------|------|
|
||||
| `SCENIC` | `SCENIC` | 景区管理 |
|
||||
| `RESTAURANT` | `RESTAURANT` | 餐厅管理 |
|
||||
| `SUPPLIES` | `SUPPLIES` | 备品管理 |
|
||||
| `SUPPLIES_COMBO` | `SUPPLIES_COMBO` | 组合配品 |
|
||||
| `HOTEL` | `HOTEL` | 酒店管理 |
|
||||
| `SERVICE` | `SERVICE` | 服务管理 |
|
||||
| `VEHICLE` | `FLEET` | 车务管理-车队管理 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 候选查询资源上下文 | 无 | 新增可选 `resourceModule`、`resourceId`,必须成对传入 |
|
||||
| `requiredTypeCode` | 组合备品可能由调用方误传 `SUPPLIES` | 固定映射模块可省略,组合备品由后端解析为 `SUPPLIES_COMBO` |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 设置供应商候选 | 依赖前端映射 `typeCode`,可能展示不对应类型 | 后端按真实资源上下文过滤 `ACTIVE` 且匹配的供应商 |
|
||||
| 无资源上下文的通用列表 | 按调用方筛选 | 保持不变 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否;无资源上下文的原调用保持兼容。
|
||||
- **前端是否必须同步上线**: 是。
|
||||
- **前端 workaround 清理点**: 删除资源分类与供应商类型本地映射;候选查询改传 `resourceModule`、`resourceId`,绑定请求不再拼 `requiredTypeCode`。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台资源管理、车务管理的供应商候选和设置/改绑类型解析。
|
||||
- **零影响**: 供应商分页管理、供应商注册审批、既有响应字段、数据库结构、Redis、MQ 和其他前端源码。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
```text
|
||||
SCENIC 候选:忽略错误 typeCode/status,只返回 ACTIVE + SCENIC,FLEET-only 为 0 ✓
|
||||
SUPPLIES_COMBO 候选及绑定:返回/冻结 SUPPLIES_COMBO,绑定后已解绑恢复原关系状态 ✓
|
||||
VEHICLE 候选:忽略错误 typeCode/status,只返回 ACTIVE + FLEET ✓
|
||||
通用列表 typeCode/status/keyword/limit:保持兼容 ✓
|
||||
上下文不完整、无默认映射、失效类型、资源不存在:均失败关闭 ✓
|
||||
```
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR / 提交 | Issue | 说明 | 是否仍有效 |
|
||||
|-----------|-------|------|------------|
|
||||
| changelog `5046bce` | #7042 | 原要求前端维护映射 | ❌ 已更正 |
|
||||
| changelog `a0daab2` | #7042 | 撤销前端映射要求,转后端工单 | ✅ 有效 |
|
||||
| **PR #7064** | **#7059** | 后端按资源上下文解析、过滤并修正组合备品映射 | ✅ 最新 |
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#7059](https://git.1814.love:8443/wx/HL/issues/7059)
|
||||
- 关联 PR: [wx/HL#7064](https://git.1814.love:8443/wx/HL/pulls/7064)
|
||||
- 被更正记录: [#7042 Changelog](./03_7042_设置供应商列表按资源类型过滤-前端缺陷-管理后台.md)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#7059](https://git.1814.love:8443/wx/HL/issues/7059)
|
||||
- **PR**: [#7064](https://git.1814.love:8443/wx/HL/pulls/7064)
|
||||
- **Merge commit**: [5999b532e9f9725ac3b36f859ee48f6f0b8aa43c](https://git.1814.love:8443/wx/HL/commit/5999b532e9f9725ac3b36f859ee48f6f0b8aa43c)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7062"
|
||||
title: "供应商清账归档后修改入口置灰"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "前端优化"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "管理端已实现 status=ARCHIVED 时置灰修改入口,前端无需修改代码或接收通知。"
|
||||
updated_at: "2026-09-04"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商模块:清账归档后修改入口置灰
|
||||
|
||||
> **Issue**: [#7062](https://git.1814.love:8443/wx/HL/issues/7062)
|
||||
> **影响范围**: 管理后台供应商列表、详情、账户和合同写入口
|
||||
|
||||
## 关键变化
|
||||
|
||||
管理端已实现:供应商 `status="ARCHIVED"` 时,详情查看入口保留,修改入口禁用并呈灰色。前端无需修改代码。
|
||||
|
||||
## 接口与字段
|
||||
|
||||
| 方法 | 路径 | 前端使用字段 | 请求 / 响应变化 |
|
||||
|---|---|---|---|
|
||||
| GET | `/admin/supplier/items/page` | `data.records[].status` | 无变化;`ARCHIVED` 表示已归档 |
|
||||
| GET | `/admin/supplier/items/list` | `data[].status` | 无变化;`ARCHIVED` 表示已归档 |
|
||||
| GET | `/admin/supplier/items/{supplierId}/basic-info/view` | `data.status` | 无变化;详情以该字段刷新置灰状态 |
|
||||
|
||||
- `status` 用于按钮门禁;`statusName` 只用于展示“已归档”。
|
||||
- 本条不变更请求、响应字段或前端调用方式。
|
||||
|
||||
## 正确调用方式与边界
|
||||
|
||||
- 已归档供应商保持现有置灰和只读展示行为。
|
||||
- 后端业务码 `395031` 继续作为绕过页面时的服务端兜底。
|
||||
- 本工单只处理后端归档不可写缺口,不要求前端联调。
|
||||
|
||||
## 前端动作与当前状态
|
||||
|
||||
- 未提交:前端无需修改代码或接收通知。
|
||||
- **当前状态:无需前端处理。**
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#7062](https://git.1814.love:8443/wx/HL/issues/7062)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,537 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7063"
|
||||
title: "团期导游位下游按配置位归组,GUIDE 与 LEADER 一视同仁"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "PR #7065 已合入 dev-v3(合并提交 f37b6ee0f);2026-09-04 部署测试环境并经网关实测 AC-2/AC-3/AC-4/AC-5/AC-9。第 2 波配置位字典化未做"
|
||||
updated_at: "2026-09-04"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期人员配置: 导游位下游判定由单一角色改为按配置位归组
|
||||
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #7065 | **Issue**: #7063 | **合并提交**: `f37b6ee0f`
|
||||
> **影响范围**: 管理后台「团期详情 → 配置导游/摄影」、「订单详情 → 人员」、行程单打印
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**「导游位并收导游与领队」这条裁决此前只落到了候选列表一层,保存之后的下游仍只认单一角色。**
|
||||
|
||||
结果是配置人怎么选都会踩一边:
|
||||
|
||||
| 存成 | 丢什么 |
|
||||
|---|---|
|
||||
| `GUIDE` | `guide_ready` 不置位、`guide_status` 不写、`GUIDE_DONE` 流水不写、订单资源就绪闸门推不动 |
|
||||
| `LEADER` | 行程单「导游」栏为空 |
|
||||
|
||||
**本机实测还暴露了第二处更深的缺陷**:团期人员配置保存时,`ready` 回填用的是**产品侧 batchId**,
|
||||
而 `order_group_batch` 的 ready 列按**主键**更新,两者不是同一个 ID 空间。
|
||||
Mapper `rows=0` 只打 WARN 不抛,所以这个洞一直没被发现——
|
||||
**改前不论配 GUIDE 还是 LEADER,`guide_ready` / `photographer_ready` 从来就没被置位过。**
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
人员类型 `staff_type` 在服务人员管理里是维护好的字典,团期现场谁带团由业务按实际安排:
|
||||
有的团派导游,有的团派领队,也有两人都上。2026-09-02 已裁决「导游位并收 GUIDE 与 LEADER,
|
||||
服务端不替业务做取舍」,本次把这条裁决贯彻到保存之后的全部下游链路。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 全量保存团期人员配置 | PUT | `/v3/admin/group-batch/:groupBatchId/staff` | **副作用变更** | 导游位配 `GUIDE` 也置 `guide_ready`;并修正 ready 回填的 ID 空间 |
|
||||
| 2 | 订单新增人员 | POST | `/v3/admin/order/:id/staff` | **副作用变更** | `staffRole=GUIDE` 也写 `guide_status` 与 `GUIDE_DONE` 流水 |
|
||||
| 3 | 订单删除人员 | DELETE | `/v3/admin/order/:id/staff/:staffAssignmentId` | **副作用变更** | 计数按导游位整组,删其一不再误置空 `guide_status` |
|
||||
| 4 | 打印行程单 | GET | `/v3/admin/order/:id/print-itinerary` | **出参取值口径变更** | 导游栏 `GUIDE` 优先、缺位回退 `LEADER` |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 全量保存团期人员配置 `PUT /v3/admin/group-batch/:groupBatchId/staff`
|
||||
|
||||
**VO**: `BatchStaffConfigReqVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情「配置导游 / 配置摄影」弹窗点保存时调用。全量覆盖语义:传入列表即为最终配置。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `groupBatchId` | Path | Long | ✅ | — | **产品侧团期 batchId**,非 `order_group_batch` 主键 |
|
||||
| `staffList` | Body | Array | ❌ | 传空则清空 | 人员配置列表,全量覆盖 |
|
||||
| `staffList[].staffId` | Body | Long | ✅ | — | 资源域人员 ID |
|
||||
| `staffList[].staffRole` | Body | String | ✅ | `LEADER` `GUIDE` `DRIVER` `PHOTOGRAPHER` `OTHER` | 团期角色 |
|
||||
| `staffList[].sortOrder` | Body | Integer | ❌ | 缺省 0 | 展示排序 |
|
||||
|
||||
#### 出参 `Result<BatchStaffConfigRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `staffList` | Array | 落库后的人员快照,**结构未变** |
|
||||
| `affectedOrderCount` | Integer | 扇出到的活跃子订单数,**未变** |
|
||||
|
||||
> **出参结构完全没有变化**,变的是写库之后的副作用。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"staffList": [
|
||||
{
|
||||
"staffId": 1002,
|
||||
"staffRole": "GUIDE",
|
||||
"sortOrder": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"staffList": [
|
||||
{
|
||||
"staffId": 1002,
|
||||
"staffRole": "GUIDE",
|
||||
"staffName": "李雪梅",
|
||||
"staffPhone": "138****1002",
|
||||
"sortOrder": 0
|
||||
}
|
||||
],
|
||||
"affectedOrderCount": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
`staffList` 传空数组即清空该团期全部人员配置,返回空列表:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"staffList": [],
|
||||
"affectedOrderCount": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
团期不存在时 ready 回填跳过并打 WARN,接口本身仍成功;未携带令牌时:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "缺少有效的 Authorization 头",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 导游位认 `GUIDE` 与 `LEADER` 两种角色,任一有人即置 `guide_ready`
|
||||
- 摄影位只认 `PHOTOGRAPHER`
|
||||
- 同时配了导游和领队时 `markGuideReady` 只调一次,幂等
|
||||
- 团期查不到(未成团 / 已解散)时 ready 回填跳过,不抛异常
|
||||
- `DRIVER` 不参与任何配置位判定,由车务派车投影产生
|
||||
|
||||
### 2. 订单新增人员 `POST /v3/admin/order/:id/staff`
|
||||
|
||||
**VO**: `StaffAssignReqVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
订单详情「人员」区手工新增一名服务人员时调用。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `id` | Path | Long | ✅ | — | 订单 ID |
|
||||
| `staffId` | Body | Long | ✅ | — | 资源域人员 ID |
|
||||
| `staffRole` | Body | String | ✅ | `LEADER` `GUIDE` `DRIVER` `PHOTOGRAPHER` `OTHER` | 团期角色 |
|
||||
|
||||
#### 出参 `Result<StaffAssignmentVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `assignmentId` | Long | 新建行 ID,**未变** |
|
||||
| `staffRole` | String | 角色,**未变** |
|
||||
| `staffPhone` | String | 脱敏手机号,**未变** |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"staffId": 1002,
|
||||
"staffRole": "GUIDE"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"assignmentId": 2095758914858471425,
|
||||
"staffId": 1002,
|
||||
"staffRole": "GUIDE",
|
||||
"staffName": "李雪梅",
|
||||
"staffPhone": "138****1002"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口为写入,无空数据形态;资源域人员查询失败时整批 fail-fast:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 582103,
|
||||
"message": "员工信息查询失败,请稍后重试",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
同一订单同一角色重复添加同一人:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 582101,
|
||||
"message": "该员工已分配此角色到此订单",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `staffRole=GUIDE` 现在与 `LEADER` 等价地触发 `guide_status=DONE` 与 `GUIDE_DONE` 流水
|
||||
- 流水姓名脱敏为首字符加星号,操作人由 `operatorResolver` 自动带出
|
||||
- 写完状态后触发 `maybeAdvanceToConfirm`,尝试推进订单资源就绪
|
||||
- `DRIVER` 与 `OTHER` 不落任何 staff 状态列
|
||||
- 团期来源行(`source=GROUP_BATCH`)不允许在订单侧增删改
|
||||
|
||||
### 3. 订单删除人员 `DELETE /v3/admin/order/:id/staff/:staffAssignmentId`
|
||||
|
||||
**VO**: `StaffAssignmentVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
订单详情「人员」区移除一名手工添加的服务人员。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `id` | Path | Long | ✅ | — | 订单 ID |
|
||||
| `staffAssignmentId` | Path | Long | ✅ | — | 人员配置行 ID |
|
||||
|
||||
#### 出参 `Result<Void>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data` | Null | 无返回体,**未变** |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
DELETE /v3/admin/order/990706300101/staff/2095758914858471425
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
行不存在或不属于该订单:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 582102,
|
||||
"message": "员工分配记录不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
团期共享行不可在订单侧删除:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 582109,
|
||||
"message": "团期共享员工不可在订单侧增删改,请到团期配置页操作",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 删除后按**导游位整组**重新计数,而不是只数被删的那个角色
|
||||
- 导游位同时有导游和领队时,删掉其一 `guide_status` 保持 `DONE`
|
||||
- 整组归零时才把 `guide_status` 置空,且不写状态流水
|
||||
- 摄影位口径不变,仍只数 `PHOTOGRAPHER`
|
||||
|
||||
### 4. 打印行程单 `GET /v3/admin/order/:id/print-itinerary`
|
||||
|
||||
**VO**: `PrintItineraryRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
订单详情点「打印行程单」,抬头区展示司机 / 导游 / 领队的姓名与电话。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `id` | Path | Long | ✅ | — | 订单 ID |
|
||||
|
||||
#### 出参 `Result<PrintItineraryRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `guideName` | String | 导游姓名。**取值口径变更**:导游位内 `GUIDE` 优先,缺位回退 `LEADER` |
|
||||
| `guidePhone` | String | 导游电话,同上口径 |
|
||||
| `leaderName` | String | 领队姓名,仍只取 `LEADER` 行,**未变** |
|
||||
| `leaderPhone` | String | 领队电话,**未变** |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/990706300101/print-itinerary
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
导游位只配了领队时,导游栏回退取到领队:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"header": {
|
||||
"guideName": "刘大山",
|
||||
"guidePhone": "13800001005",
|
||||
"leaderName": "刘大山",
|
||||
"leaderPhone": "13800001005"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
导游位无人时四个字段均为 null,接口仍成功:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"header": {
|
||||
"guideName": null,
|
||||
"guidePhone": null,
|
||||
"leaderName": null,
|
||||
"leaderPhone": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
非本人名下订单且无数据权限:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 581008,
|
||||
"message": "无权查看此订单",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 只读接口,不产生任何写入
|
||||
- 导游位同时有导游和领队时,导游栏取导游,不取领队
|
||||
- `leaderName` 与 `leaderPhone` 语义不变,仍是领队本身
|
||||
- 导游与领队是同一人时两栏显示同一姓名,属预期
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- **导游位的角色集合是 `GUIDE` 与 `LEADER` 两个**,前端配置弹窗可以把两类人员一起列出来,服务端不做取舍。
|
||||
- **`guideName` 不再等价于「`staffRole=GUIDE` 那一行」**,它是「导游位上的人」。要精确区分导游与领队请分别读 `guideName` 与 `leaderName`。
|
||||
- **`groupBatchId` 在人员配置接口里是产品侧 batchId**,不是 `order_group_batch` 主键,两者不可互换。
|
||||
- 判断「导游是否配齐」请读团期的 `guide_ready` 或订单的 `guide_status`,不要自行按角色字符串比对。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
无 DDL、无迁移、无新增列。写入行为的变化:
|
||||
|
||||
| 表 | 列 | 变化 |
|
||||
|---|---|---|
|
||||
| `order_group_batch` | `guide_ready` | 改前因 ID 空间用错**从未被置位**;改后按主键正确置 true |
|
||||
| `order_group_batch` | `photographer_ready` | 同上 |
|
||||
| `order_main` | `guide_status` | `staffRole=GUIDE` 的行现在也会写 `DONE`;删除时按导游位整组判空 |
|
||||
| `order_status_log` | — | `staffRole=GUIDE` 现在也会产生一条 `GUIDE_DONE` 数据流水 |
|
||||
| `order_batch_staff` / `order_staff_assignment` | — | 结构与写入内容均**未变** |
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 导游位配 `GUIDE` → `guide_ready=true`
|
||||
- 导游位配 `LEADER` → `guide_ready=true`
|
||||
- 导游位同时配两者 → `markGuideReady` 只调一次
|
||||
- 团期查不到 → ready 回填跳过并 WARN,接口不报错
|
||||
- 导游位删剩一人 → `guide_status` 保持 `DONE`
|
||||
- 导游位清空 → `guide_status` 置空且不写流水
|
||||
- 行程单导游位只有领队 → 导游栏取领队
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
本次无新增枚举。`staffRole` 取值域与 `SettlementStaffRoleEnum` 保持一致,未扩展。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 项 | 变更前 | 变更后 |
|
||||
|---|---|---|
|
||||
| `guide_ready` 回填 | **从未生效**(用产品侧 batchId 打主键,rows=0 只 WARN) | 反查真实主键后正确置位 |
|
||||
| 导游位判定 | 只认 `LEADER` | 认 `GUIDE` 与 `LEADER` |
|
||||
| `guide_status` 写入 | `GUIDE` 行不写 | `GUIDE` 行同样写 |
|
||||
| `GUIDE_DONE` 流水 | `GUIDE` 行不写 | `GUIDE` 行同样写 |
|
||||
| 删除后计数 | 只数被删角色,可能误置空 | 按导游位整组计数 |
|
||||
| 行程单导游栏 | 只取 `GUIDE` 行,配领队时为空 | `GUIDE` 优先,缺位回退 `LEADER` |
|
||||
| 接口路径 / 入参 / 出参结构 | — | **全部不变** |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
| 维度 | 评估 |
|
||||
|---|---|
|
||||
| 兼容性 | **只放宽不收紧**。原来不触发的现在会触发,存量 `staffRole=LEADER` 的配置行行为与改前完全一致 |
|
||||
| 前端 | **无需改动**。路径、入参、出参结构均未变;行程单导游栏由「可能为空」变为「有值」,是修复不是破坏 |
|
||||
| 数据 | 无 DDL、无迁移、无回填 |
|
||||
| 性能 | 删除路径的计数由单值等值查询改为 `IN` 两值;行程单导游栏由一次点查改为一次整组查询后按序挑选,**查询次数不增** |
|
||||
| 回滚 | `git revert`,单一 PR,无数据侧残留 |
|
||||
| 风险 | 低。`guide_ready` 修复后会让原本推不动的团期开始推进资源就绪闸门,属预期恢复 |
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **零影响**:所有接口的路径、HTTP 方法、入参、出参结构
|
||||
- **零影响**:摄影位口径,仍只收 `PHOTOGRAPHER`
|
||||
- **零影响**:`leaderName` / `leaderPhone` 的语义与取值
|
||||
- **零影响**:`DRIVER` 与 `OTHER` 角色的处理
|
||||
- **零影响**:staff-fees 导游费用录入分流逻辑(裁决 D-2,配在导游位的领队不录导游费用,属预期)
|
||||
- **未新建端点**,未改动任何路径与入参
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
✅ 2026-09-04 于测试环境网关实测,真实鉴权(管理端 `test_admin`,角色定制师)。
|
||||
|
||||
- 网关 `https://api.test.1814.love:9443`,分支 `dev-v3`,合并提交 `f37b6ee0f`
|
||||
- 部署方式:双实例滚动更新(8186 → 8086),各 11s 就绪,零停机
|
||||
- 部署内容经部署面板 `/api/git/backend` 交叉核对:构建时点 `f37b6ee0f` 已是 `dev-v3` 顶端
|
||||
|
||||
| # | 用例 | 期望 | 实测 |
|
||||
|---|---|---|---|
|
||||
| 1 | 配 `staffType=GUIDE` 到导游位 | `guide_ready` 置 1 | ✅ 0 → 1(AC-2) |
|
||||
| 2 | 配 `staffType=LEADER` 到导游位 | `guide_ready` 置 1 | ✅ 0 → 1(AC-4) |
|
||||
| 3 | 配摄影师 | `photographer_ready` 置 1 | ✅ 0 → 1(AC-9) |
|
||||
| 4 | 订单侧配 `staffRole=GUIDE` | `guide_status=DONE` + `GUIDE_DONE` 流水 | ✅ 流水操作人 `test_admin`,内容脱敏为首字加星 |
|
||||
| 5 | 导游位删剩领队 | `guide_status` 保持 `DONE` | ✅ 未被置空 |
|
||||
| 6 | 行程单,导游位只有领队 | 导游栏取到领队 | ✅ `guideName` 为领队姓名,电话一致 |
|
||||
| 7 | 行程单,导游位两者都有 | 导游栏取导游 | ✅ 导游优先生效 |
|
||||
| 8 | 扇出 | 团期配置扇出到活跃子订单 | ✅ 2 单,`source=GROUP_BATCH` |
|
||||
|
||||
**观察**:部署后双实例持续 running 无重启,服务日志最近 200 行 `ERROR` / `Exception` 命中 **0** 条。
|
||||
日志中可见 `Parameters: 990706300101(Long), GUIDE(String), LEADER(String)`,即按导游位整组计数的查询确已在测试环境执行。
|
||||
|
||||
**本地单测**:`AssignmentServiceTest` 65 例、`GroupBatchStaffConfigServiceTest` 31 例、
|
||||
`AssignmentServiceCallbackDriverTest` 42 例、`OrderServiceTest` 205 例、
|
||||
架构与错误码门禁 51 例,合计 **401 例全过**,其中新增 7 例覆盖本次全部行为变化。
|
||||
|
||||
**测试数据**:测试环境所造数据一律 `T7063-` 前缀,验证后已物理删除,残留合计 0 条。
|
||||
所用 `test_admin` 口令为一次性置换,取到令牌后立即按备份还原,与备份逐字节一致。
|
||||
|
||||
> ⚠️ **一处未做实测**:团期人员配置的扇出链路(`doFanOutForOrder`)**不调用** `syncStaffStatusToOrder`,
|
||||
> 因此经团期配置这条路进来时 `order_main.guide_status` 始终为 null。用例 4 走的是**订单侧**新增人员端点。
|
||||
> 这是先于本次变更就存在的第三处缺口,不在本波范围,已在工单中记录待另议。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
- #7065 本次变更(第 1 波 P1 止血)
|
||||
- #6962(Refs #6950)团期人员配置候选列表——本次修的正是它的下游遗漏
|
||||
- 裁决来源:2026-09-02「导游位并收 GUIDE 与 LEADER,服务端不替业务做取舍」
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 团期需求文档:`docs/group/`(dev-v3 分支)
|
||||
- 一期实施拆分详细设计 v1.0 §0.26.4(AC-TD-13~16)
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: #7063
|
||||
- **PR**: #7065(合并提交 `f37b6ee0f`)
|
||||
@@ -0,0 +1,269 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7066"
|
||||
title: "团期子订单列表补应收总额 totalPrice"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "8980e197"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-04"
|
||||
status_note: "PR #7068 已合入 dev-v3(合并提交 e1ad8550b);2026-09-04 部署测试环境,网关实测 8 组用例全部通过(含取消单归 0)。#7097 复审收敛:应收字段名 totalAmount 未上线即收敛为 totalPrice(#7113 合入 dev-v3 合并提交 11b677198),前端以 totalPrice 为准"
|
||||
updated_at: "2026-09-04"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期子订单: 出参新增应收总额 totalPrice
|
||||
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #7068 | **Issue**: #7066 | **合并提交**: `e1ad8550b`
|
||||
> **影响范围**: 管理后台「团期详情 → 子订单」页签
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**前端不要再用 `paidAmount + balanceAmount` 反推应收总额,改读新增的 `totalPrice`。**
|
||||
|
||||
> **字段名收敛说明(#7097)**:本单初版字段名 `totalAmount`,与 #6929 已在 003 接口下发的 `totalPrice` 构成同义双字段;复审 #7097 在未上线前收敛为 `totalPrice`,`totalAmount` 已移除。本 changelog 全文按收敛后字段名 `totalPrice` 表述,字段语义与两位小数格式不变。
|
||||
|
||||
旧反推在两种场景下**偏大**:
|
||||
|
||||
1. `balanceAmount` 被钳在 ≥0(`OrderAmountUtil.calcBalance` 末行),超付或退款后已付大于应收时反推值偏大
|
||||
2. 已取消子订单 `calcBalance` 直接返 0,反推得到的是已付而非应收
|
||||
|
||||
**这不是理论风险**:2026-09-04 测试环境实测,现存子订单 `paidAmount=3270.00`、`balanceAmount=0.00`,
|
||||
而真实应收 `totalPrice=2943.00`——**旧算法多显示 327.00**。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
团期详情「子订单」页签每张卡片显示「已付 / 应收总额」,但列表接口出参此前没有应收总额字段,
|
||||
前端只能自行反推。本次补齐该字段,口径与订单域既有展示口径统一。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 团期下子订单列表 | GET | `/v3/admin/order/group-batch/:groupBatchId/orders` | **出参新增字段** | 新增 `totalPrice` 应收总额 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 团期下子订单列表 `GET /v3/admin/order/group-batch/:groupBatchId/orders`
|
||||
|
||||
**VO**: `GroupBatchOrderItemRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情「子订单」页签加载时调用,渲染每户卡片。卡片右下角「已付 ¥X / ¥Y」中的 Y 即本次新增的 `totalPrice`。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `groupBatchId` | Path | Long | ✅ | — | 运营团期 ID(订单侧 `order_group_batch` 主键) |
|
||||
| `includeTravelers` | Query | Boolean | ❌ | 缺省 false | 附出行人明细,证件号一律不返回 |
|
||||
| `includeNeeds` | Query | Boolean | ❌ | 缺省 false | 附房数 / 房型 / 特殊需求 |
|
||||
| `includeCancelled` | Query | Boolean | ❌ | 缺省 false | 是否含已取消子订单,缺省只返活跃集 |
|
||||
|
||||
#### 出参 `Result<List<GroupBatchOrderItemRespVO>>`
|
||||
|
||||
本次仅新增一个字段,其余 28 个字段不变:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `totalPrice` | String | **本次新增**。应收总额 = `orderAmount + 增项 − 优惠`;**已取消子订单归 0**。2 位小数,字符串输出 |
|
||||
| `paidAmount` | String | 已支付金额(未变) |
|
||||
| `balanceAmount` | String | 待支付尾款,**钳在 ≥0**(未变) |
|
||||
|
||||
> 三个金额字段均带 `@JsonSerialize(ToStringSerializer)`,**JSON 里是字符串**,前端直接解析为数字会有精度风险,应按字符串处理或用高精度解析。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2089713777065832450/orders
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"orderId": 2094597053924487170,
|
||||
"customerName": "6915验收造单01",
|
||||
"orderStatus": "CUSTOMIZING",
|
||||
"orderStatusName": "定制中",
|
||||
"adultCount": 2,
|
||||
"childCount": 0,
|
||||
"participantCount": 2,
|
||||
"contactPhone": "138****0001",
|
||||
"paidAmount": "3270.00",
|
||||
"balanceAmount": "0.00",
|
||||
"totalPrice": "2943.00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
团期下无子订单时返回空数组,不报错:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
未携带管理端令牌:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "缺少有效的 Authorization 头",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 只读接口,不产生任何写入
|
||||
- `totalPrice` **不等于** `paidAmount + balanceAmount`,超付、退款、取消单三种情况下都会不等
|
||||
- 已取消子订单(`includeCancelled=true` 才可见)的 `totalPrice` 与 `balanceAmount` 同为 0,闭合财务勾稽
|
||||
- 金额一律 2 位小数(HALF_UP),与订单域其他接口格式一致
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- **应收总额一律读 `totalPrice`**,不要自行用 `paidAmount + balanceAmount` 计算。
|
||||
- **`totalPrice` 与 `balanceAmount` 是不同语义**:前者是「客户总共该付多少」,后者是「现在还差多少」。已付清时后者为 0,前者仍是原值。
|
||||
- **三个金额字段是 JSON 字符串**,不是数字。
|
||||
- 取消单场景下 `totalPrice` 归 0 是**有意设计**(对齐 `calcBalance` 的取消单归 0),用于闭合「应收 0 / 已付 0 / 已退 0 / 待收 0」的勾稽链,不是缺陷。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
本接口只读,**不产生任何写入**,也不涉及表结构调整。`totalPrice` 由既有字段实时计算,不新增存储列。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 超付单 → `totalPrice` 为真实应收,小于 `paidAmount`
|
||||
- 取消单 → `totalPrice` 归 0
|
||||
- 团期无子订单 → 返回 `[]`
|
||||
- 未登录 → 401(网关拦截)
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
本次无新增枚举。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 项 | 变更前 | 变更后 |
|
||||
|---|---|---|
|
||||
| 出参字段数 | 28 | **29** |
|
||||
| 应收总额 | **无字段**,前端用 `paidAmount + balanceAmount` 反推 | 新增 `totalPrice`,服务端按既有 `payableForDisplay` 口径给出 |
|
||||
| 超付单显示 | 反推值偏大(实测多 327.00) | `totalPrice` 为真实应收 |
|
||||
| 取消单显示 | 反推得到已付金额,非应收 | `totalPrice` 归 0,与 `balanceAmount` 一致 |
|
||||
| `paidAmount` / `balanceAmount` | — | **语义与取值均不变** |
|
||||
| 路径 / 入参 | — | **不变** |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
| 维度 | 评估 |
|
||||
|---|---|
|
||||
| 兼容性 | **纯 additive**,老调用方不读新字段即无感,无破坏性变更 |
|
||||
| 前端 | 需改为读 `totalPrice`;不改也不会报错,但超付 / 取消单场景显示值偏大 |
|
||||
| 数据 | 无 DDL、无迁移、无回填;`totalPrice` 实时计算不落库 |
|
||||
| 性能 | 无额外查询,复用同一 `OrderInfo` 实体计算,零新增 IO |
|
||||
| 回滚 | 移除字段即可,无数据侧残留 |
|
||||
| 风险 | 低。口径复用 `AdjustmentService` 已在生产使用的既有方法,未新造公式 |
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **零影响**:`paidAmount` / `balanceAmount` 的现有语义与取值
|
||||
- **零影响**:其余 28 个出参字段
|
||||
- **零影响**:老调用方——纯新增字段,不读即无感
|
||||
- **未新建端点**,未改动路径与入参
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
✅ 2026-09-04 于测试环境网关实测,真实鉴权(管理端 admin)。
|
||||
|
||||
- 网关 `https://api.test.1814.love`,分支 `dev-v3`,合并提交 `e1ad8550b`
|
||||
- 部署方式:双实例滚动更新(8186 → 8086),各 11s 就绪,零停机
|
||||
|
||||
| # | 用例 | 期望 | 实测 |
|
||||
|---|---|---|---|
|
||||
| 1 | 字段上线 | 出参含 `totalPrice` | ✅ 字段数 27 → 29 |
|
||||
| 2 | 真实数据口径 | `totalPrice` 为真实应收 | ✅ `2943.00`;旧反推得 `3270.00`,**偏差 327.00** |
|
||||
| 3 | 序列化格式 | 字符串、2 位小数 | ✅ `"totalPrice":"2943.00"` |
|
||||
| 4 | `includeCancelled=true` | 正常返回 | ✅ 返回 1 条 |
|
||||
| 5 | 无 Authorization | 401 | ✅ `401 缺少有效的 Authorization 头` |
|
||||
| 6 | 老字段回归 | 未受影响 | ✅ 8 个字段逐一核对一致 |
|
||||
| 7 | 缺省过滤取消单 | 取消单不出现 | ✅ `GET .../orders` 返回 0 条 |
|
||||
| 8 | **取消单归 0** | `totalPrice = 0` 且与 `balanceAmount` 一致 | ✅ `totalPrice="0.00"`、`balanceAmount="0.00"`、`paidAmount="3270.00"` |
|
||||
|
||||
**部署前基线对照**:同一接口部署前出参 27 字段、无 `totalPrice`,确认变更确实生效。
|
||||
|
||||
**本地单测**:`GroupBatchConverterTest` 新增 5 例(正常单 / 含增项优惠 / **超付单断言反推不等** / **取消单归 0** / 2 位小数);
|
||||
本地全量 **750 例全过**。
|
||||
|
||||
**取消单分支已实测**(用例 7–8):验收期间该子订单被退单,恰好提供了取消单样本。
|
||||
实测 `totalPrice="0.00"`,而 `paidAmount="3270.00"`——**旧算法 `paidAmount + balanceAmount` 会把一条
|
||||
已取消的子订单显示成「应收 3270」,真实应收应为 0**。本字段同时修正了这一场景。
|
||||
|
||||
全部 8 组用例通过,无遗留未验分支。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
- #7068 本次变更
|
||||
- 口径来源:`OrderAmountUtil.payableForDisplay`(#4456 展示用应收总额,取消单归 0)
|
||||
- 相关:#5733(`calcBalance` 改为退款冲减应收)、#2896 P1-3(金额统一 2 位小数)
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 团期需求文档:`docs/group/`(dev-v3 分支)
|
||||
- 3 天开发计划:`docs/group/团期模块3天开发计划.md`
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: #7066
|
||||
- **PR**: #7068(合并提交 `e1ad8550b`)
|
||||
- **服务**: hl-order-service-v3
|
||||
- **后端**: jw
|
||||
- **前端**: 待认领(`frontend_status: pending`)
|
||||
@@ -0,0 +1,514 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7069"
|
||||
title: "供应商企微在途状态统一审核中"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "f6ddd94c"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-04"
|
||||
status_note: "后端已部署并通过 TEST;前端需统一展示 statusName。"
|
||||
updated_at: "2026-09-04"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商模块:企微在途状态统一显示“审核中”
|
||||
|
||||
> **服务**: `hl-resource-service`
|
||||
> **Issue**: #7069
|
||||
> **PR**: #7072
|
||||
> **影响范围**: 管理后台供应商列表、详情和状态动作结果
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
前端统一展示新增的 `statusName`:已提交企业微信且仍在审批中的供应商显示“审核中”,不再把来源生命周期状态作为展示文案;原 `status` 字段继续用于筛选和业务判断。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 供应商分页 | GET | `/admin/supplier/items/page` | 响应新增字段 | 每行新增 `statusName` |
|
||||
| 2 | 供应商有界列表 | GET | `/admin/supplier/items/list` | 响应新增字段 | 每行新增 `statusName` |
|
||||
| 3 | 供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应新增字段 | 详情新增 `statusName` |
|
||||
| 4 | 暂停合作 | POST | `/admin/supplier/items/{supplierId}/suspend` | 响应新增字段 | 返回 `statusName=暂停合作` |
|
||||
| 5 | 列入黑名单 | POST | `/admin/supplier/items/{supplierId}/blacklist` | 响应新增字段 | 企微在途返回 `statusName=审核中` |
|
||||
| 6 | 恢复合作 | POST | `/admin/supplier/items/{supplierId}/resume` | 响应新增字段 | 返回 `statusName=合作中` |
|
||||
| 7 | 解除黑名单 | POST | `/admin/supplier/items/{supplierId}/unblacklist` | 响应新增字段 | 企微在途返回 `statusName=审核中` |
|
||||
| 8 | 清账归档 | POST | `/admin/supplier/items/{supplierId}/archive` | 响应新增字段 | 两类企微归档在途均返回“审核中” |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 供应商分页 `GET /admin/supplier/items/page`
|
||||
|
||||
**VO**: `SupplierPageReqVO` → `PageResult<SupplierListItemRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
供应商管理分页展示及按生命周期状态筛选。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| page / pageSize | Query | Integer | 否 | `page>=1`,`1<=pageSize<=100` | 默认 1 / 20 |
|
||||
| status | Query | String | 否 | 生命周期枚举 | 仍按原 `status` 筛选 |
|
||||
| keyword / typeCode / creditLevel / creatorId | Query | String | 否 | 沿用原约束 | 其他筛选条件不变 |
|
||||
|
||||
#### 出参 `Result<PageResult<SupplierListItemRespVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| data.records[].status | String | 原生命周期状态码,保持不变 |
|
||||
| data.records[].statusName | String | 对外展示文案;企微审批在途为“审核中” |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/page?page=1&pageSize=20
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","data":{"records":[{"supplierId":"2095701432631017473","status":"VETTING","statusName":"审核中"}],"total":1,"page":1,"pageSize":20},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无匹配供应商时 `data.records` 为空数组,分页元数据仍正常返回。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":401,"message":"未登录或登录已过期","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `status` 仍只接受既有生命周期状态码;“审核中”是展示文案,不作为筛选值。
|
||||
|
||||
### 2. 供应商有界列表 `GET /admin/supplier/items/list`
|
||||
|
||||
**VO**: `SupplierListReqVO` → `List<SupplierListItemRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
下拉选择及有界供应商列表展示。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| limit | Query | Integer | 否 | 1~200 | 默认 50 |
|
||||
| status | Query | String | 否 | 生命周期枚举 | 仍按原 `status` 筛选 |
|
||||
| keyword / typeCode / resourceModule / resourceId | Query | String | 否 | 沿用原约束 | 其他筛选条件不变 |
|
||||
|
||||
#### 出参 `Result<List<SupplierListItemRespVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| data[].status | String | 原生命周期状态码,保持不变 |
|
||||
| data[].statusName | String | 对外展示文案;企微审批在途为“审核中” |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/list?limit=50
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","data":[{"supplierId":"2095701432631017473","status":"VETTING","statusName":"审核中"}],"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无匹配供应商时返回 `data=[]`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":401,"message":"未登录或登录已过期","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 返回条数仍受 `limit` 限制;前端展示 `statusName`,业务判断继续使用 `status`。
|
||||
|
||||
### 3. 供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
|
||||
|
||||
**VO**: `Void` → `SupplierBasicInfoRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
供应商详情页展示当前对外状态。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| supplierId | Path | String | 是 | 正整数 | 供应商 ID |
|
||||
|
||||
#### 出参 `Result<SupplierBasicInfoRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| data.status | String | 原生命周期状态码,保持不变 |
|
||||
| data.statusName | String | 对外展示文案;企微审批在途为“审核中” |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/2095701432631017473/basic-info/view
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","data":{"supplierId":"2095701432631017473","status":"VETTING","statusName":"审核中"},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
供应商不存在时返回业务失败,不返回空成功详情。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395001,"message":"供应商不存在","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 企微审批结束后刷新本接口,即可获得对应终态展示文案。
|
||||
|
||||
### 4. 暂停合作 `POST /admin/supplier/items/{supplierId}/suspend`
|
||||
|
||||
**VO**: `SupplierStatusChangeReqVO` → `SupplierStatusChangeRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
将合作中供应商暂停合作。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| supplierId | Path | String | 是 | 正整数 | 供应商 ID |
|
||||
| reason | Body | String | 是 | 1~500 字符 | 状态变更原因 |
|
||||
| expectedUpdateTime | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 页面读取到的并发版本 |
|
||||
|
||||
#### 出参 `Result<SupplierStatusChangeRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| data.status | String | `SUSPENDED` |
|
||||
| data.statusName | String | “暂停合作” |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"reason":"暂停合作","expectedUpdateTime":"2026-09-04 10:00:00"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","data":{"supplierId":"2095000000000000001","status":"SUSPENDED","statusName":"暂停合作"},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口不返回空成功数据;执行失败时状态不变。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395014,"message":"数据已被他人修改,请刷新后重试","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 权限、允许状态、并发版本及幂等规则均保持不变。
|
||||
|
||||
### 5. 列入黑名单 `POST /admin/supplier/items/{supplierId}/blacklist`
|
||||
|
||||
**VO**: `SupplierStatusChangeReqVO` → `SupplierStatusChangeRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
对暂停合作供应商发起列入黑名单企微审批。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| supplierId | Path | String | 是 | 正整数 | 供应商 ID |
|
||||
| reason | Body | String | 是 | 1~500 字符 | 列入黑名单原因 |
|
||||
| expectedUpdateTime | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 页面读取到的并发版本 |
|
||||
|
||||
#### 出参 `Result<SupplierStatusChangeRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| data.status | String | 审批中仍为 `SUSPENDED` |
|
||||
| data.statusName | String | 审批中为“审核中” |
|
||||
| data.approval.approvalStatus | String | 审批中为 `PENDING` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"reason":"严重违约","expectedUpdateTime":"2026-09-04 10:00:00"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","data":{"supplierId":"2095000000000000002","status":"SUSPENDED","statusName":"审核中","approval":{"provider":"WECOM","approvalStatus":"PENDING","spNo":"202609040001"}},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口不返回空成功数据;企微提交失败时保持暂停合作。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395005,"message":"当前状态不允许执行该操作","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 审批通过后刷新查询接口显示“黑名单”,驳回后显示“暂停合作”。
|
||||
|
||||
### 6. 恢复合作 `POST /admin/supplier/items/{supplierId}/resume`
|
||||
|
||||
**VO**: `SupplierStatusChangeReqVO` → `SupplierStatusChangeRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
将暂停合作供应商恢复合作。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| supplierId | Path | String | 是 | 正整数 | 供应商 ID |
|
||||
| reason | Body | String | 是 | 1~500 字符 | 状态变更原因 |
|
||||
| expectedUpdateTime | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 页面读取到的并发版本 |
|
||||
|
||||
#### 出参 `Result<SupplierStatusChangeRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| data.status | String | `ACTIVE` |
|
||||
| data.statusName | String | “合作中” |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"reason":"恢复合作","expectedUpdateTime":"2026-09-04 10:00:00"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","data":{"supplierId":"2095000000000000003","status":"ACTIVE","statusName":"合作中"},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口不返回空成功数据;执行失败时状态不变。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395014,"message":"数据已被他人修改,请刷新后重试","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 权限、允许状态、并发版本及幂等规则均保持不变。
|
||||
|
||||
### 7. 解除黑名单 `POST /admin/supplier/items/{supplierId}/unblacklist`
|
||||
|
||||
**VO**: `SupplierStatusChangeReqVO` → `SupplierStatusChangeRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
对黑名单供应商发起解除黑名单企微审批。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| supplierId | Path | String | 是 | 正整数 | 供应商 ID |
|
||||
| reason | Body | String | 是 | 1~500 字符 | 解除原因 |
|
||||
| expectedUpdateTime | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 页面读取到的并发版本 |
|
||||
|
||||
#### 出参 `Result<SupplierStatusChangeRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| data.status | String | 审批中仍为 `BLACKLIST` |
|
||||
| data.statusName | String | 审批中为“审核中” |
|
||||
| data.approval.approvalStatus | String | 审批中为 `PENDING` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"reason":"整改完成","expectedUpdateTime":"2026-09-04 10:00:00"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","data":{"supplierId":"2095000000000000004","status":"BLACKLIST","statusName":"审核中","approval":{"provider":"WECOM","approvalStatus":"PENDING","spNo":"202609040002"}},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口不返回空成功数据;企微提交失败时保持黑名单。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395005,"message":"当前状态不允许执行该操作","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 审批通过后刷新查询接口显示“暂停合作”,驳回后显示“黑名单”。
|
||||
|
||||
### 8. 清账归档 `POST /admin/supplier/items/{supplierId}/archive`
|
||||
|
||||
**VO**: `Void` → `SupplierArchiveRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
暂停合作供应商发起“终止且账清”,或黑名单供应商发起“拉黑且账清”企微审批。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| supplierId | Path | String | 是 | 正整数 | 供应商 ID |
|
||||
|
||||
#### 出参 `Result<SupplierArchiveRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| data.status | String | 审批中保持 `SUSPENDED` 或 `BLACKLIST` |
|
||||
| data.statusName | String | 两类审批在途均为“审核中” |
|
||||
| data.approval.approvalStatus | String | 审批中为 `PENDING` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /admin/supplier/items/2095000000000000005/archive
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","data":{"supplierId":"2095000000000000005","status":"SUSPENDED","statusName":"审核中","approval":{"provider":"WECOM","approvalStatus":"PENDING","spNo":"202609040003"}},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口不返回空成功数据;企微提交失败时保持来源状态。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395021,"message":"企业微信申请提交失败,请稍后重试","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 两类审批通过后刷新查询接口均显示“已归档”;驳回后分别显示“暂停合作”或“黑名单”。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- 请求参数、鉴权、权限、幂等和错误码均不变。
|
||||
- `status` 是生命周期状态码,继续用于筛选、按钮门禁和业务判断;`statusName` 是中文展示值。
|
||||
- 状态动作成功后先使用响应中的 `statusName`,后续刷新分页、列表或详情获取审批终态。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
本次不修改数据结构和状态迁移规则;写接口原有业务写入、失败零写入及并发规则不变,仅在响应中增加展示字段。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 注册审批中:`status=VETTING`,`statusName=审核中`。
|
||||
- 企业微信状态审批仅在 `provider=WECOM`、`approvalStatus=PENDING` 且已有审批单号时覆盖展示为“审核中”。
|
||||
- 审批结束后按当前生命周期状态展示,不继续显示“审核中”。
|
||||
- 未登录时统一返回业务码 `401`;其他错误码保持不变。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
| 原状态或审批条件 | `statusName` |
|
||||
|---|---|
|
||||
| 注册审批中 `VETTING` | 审核中 |
|
||||
| 企微状态审批在途 | 审核中 |
|
||||
| `DRAFT` | 草稿 |
|
||||
| `ACTIVE` | 合作中 |
|
||||
| `SUSPENDED` 且无在途审批 | 暂停合作 |
|
||||
| `BLACKLIST` 且无在途审批 | 黑名单 |
|
||||
| `ARCHIVED` | 已归档 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| `statusName` | 不返回 | 列表、详情及五个状态动作响应返回中文展示值 |
|
||||
| `status` | 生命周期状态码 | 保持不变 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 企微审批在途展示 | 可能继续显示来源状态 | 统一显示“审核中” |
|
||||
| 审批完成展示 | 读取生命周期状态 | 保持按最终生命周期状态展示 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否,响应仅新增字段,原 `status` 保留。
|
||||
- **前端是否必须同步上线**: 是,需改为展示 `statusName`。
|
||||
- **前端 workaround 清理点**: 删除前端自行翻译 `status` 作为展示文案的逻辑。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台供应商状态文案展示。
|
||||
- **零影响**: 生命周期状态机、审批通过或驳回迁移、请求参数、权限、数据库结构、配置、Redis 和 MQ。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- 分页、列表和详情均返回 `statusName`;注册审批中返回“审核中”,稳定态分别返回草稿、合作中、暂停合作、黑名单和已归档。
|
||||
- 四类企微状态审批的完成态与来源状态一致;未登录请求返回业务码 `401`。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- [Issue #7069](https://git.1814.love:8443/wx/HL/issues/7069)
|
||||
- [PR #7072](https://git.1814.love:8443/wx/HL/pulls/7072)
|
||||
|
||||
## 前端动作与当前状态
|
||||
|
||||
- 列表、详情和动作结果统一展示 `statusName`;保留 `status` 做筛选和按钮门禁。
|
||||
- 状态动作后刷新列表或详情,以最新 `statusName` 展示审批终态。
|
||||
- **当前状态:待前端适配。**
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#7069](https://git.1814.love:8443/wx/HL/issues/7069)
|
||||
- **PR**: [#7072](https://git.1814.love:8443/wx/HL/pulls/7072)
|
||||
- **Merge commit**: [c95f1de495d4bcdd8579ccd1f0c895deca29e14a](https://git.1814.love:8443/wx/HL/commit/c95f1de495d4bcdd8579ccd1f0c895deca29e14a)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,556 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7070"
|
||||
title: "房务列表/详情补产品类型标签 productType"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "7476250d"
|
||||
target_release: "hl-ui@7476250d"
|
||||
verified_at: "2026-09-04"
|
||||
status_note: "2026-09-04 测试服部署 dev-v3@19ca3d702;详情/待办/抢单池 productType 网关实测与订单一致;前端 mmg 已 verified。#7098 复审:productType 筛选下推 SQL(抢单池/我的接单),我的接单 productType 入参此前被静默忽略现生效;PR #7117 合入 dev-v3(合并提交 cd21ae0)"
|
||||
updated_at: "2026-09-04"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 房务: 各订单列表与详情补产品类型标签 productType
|
||||
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: [#7077](https://git.1814.love:8443/wx/HL/pulls/7077) / [#7084](https://git.1814.love:8443/wx/HL/pulls/7084) / [#7117](https://git.1814.love:8443/wx/HL/pulls/7117)(#7098 复审)
|
||||
> **Issue**: [#7070](https://git.1814.love:8443/wx/HL/issues/7070) / [#7098](https://git.1814.love:8443/wx/HL/issues/7098)(复审)
|
||||
> **日期**: 2026-09-04
|
||||
> **影响范围**: 管理后台房务——抢单池、我的接单、订单详情、待办列表、日历下钻、月度对账订单明细、需求历史
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
🔧 房务各订单维度列表行/详情新增或接通 `productType` 字段,前端据此渲染「核心订单 / 线路订单 / 定制订单 / 团期订单」标签。取值口径统一、稳定性提升(此前部分场景在产品服务不可用时返回空,现已消除该空值)。`productType` 为 null 的订单前端不渲染标签。
|
||||
|
||||
> **复审收敛说明(#7098)**:初版 `productType` 筛选在查询返回后按当前页内存匹配,且「我的接单」接口的 `productType` 入参未透传(被静默忽略)。#7098 复审将筛选下推为 SQL 条件(MyBatis-Plus WrapperX 条件 eq),跨页结果精确;「我的接单」`productType` 入参现生效。内部同时收敛 `TodoEnrichment` 为单一构造器并补 5 处断言护栏,无对外 API 变更。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景(选填)
|
||||
|
||||
房务在抢单、配房、对账时需要直观区分订单产品类型。此前抢单池/我的接单/详情虽能取到产品类型,但在产品服务不可用时可能返回空;待办/日历/对账/需求历史则完全没有该字段。本次统一为订单主数据直读并补齐缺失场景。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 抢单池列表 | GET | `/v3/admin/order/grab-pool/hotel-requirements` | 修改接口 | `productType` 取值口径统一,稳定性提升 |
|
||||
| 2 | 我的接单列表 | GET | `/v3/admin/order/grab-pool/my-claims/hotel` | 修改接口 | `productType` 取值口径统一 |
|
||||
| 3 | 房务订单详情 | GET | `/admin/house/orders/{orderId}` | 修改接口 | `order.productType` 取值口径统一 |
|
||||
| 4 | 房务待办列表 | GET | `/v3/admin/order/todos` | 修改接口 | 行项新增出参 `productType` |
|
||||
| 5 | 日历某天下钻 | GET | `/admin/house/calendar/day` | 修改接口 | 单团项新增出参 `productType` |
|
||||
| 6 | 月度对账订单明细 | GET | `/v3/admin/house/reconciliation/monthly/hotel-orders` | 修改接口 | 订单明细行新增出参 `productType` |
|
||||
| 7 | 需求历史列表 | GET | `/admin/house/orders/{orderId}/requirement-history` | 修改接口 | 历史版本项新增出参 `productType` |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 抢单池列表 `GET /v3/admin/order/grab-pool/hotel-requirements`
|
||||
|
||||
**VO**: `HouseGrabPageItemRespVO`
|
||||
|
||||
#### 使用场景
|
||||
房务打开抢单池页,浏览待抢的用房需求卡片。本变更让每张卡片可稳定显示产品类型标签。
|
||||
|
||||
#### 入参
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| page | Query | Integer | 否 | 默认 1 | 页码 |
|
||||
| pageSize | Query | Integer | 否 | 默认 20 | 每页条数 |
|
||||
| keyword | Query | String | 否 | ≤32 字 | 订单号/团号/客人/产品名模糊 |
|
||||
| productType | Query | String | 否 | CORE/ROUTE/CUSTOM/GROUP | 产品类型筛选(既有入参,语义不变) |
|
||||
| productName | Query | String | 否 | - | 产品名精准 |
|
||||
| consultantId | Query | Long | 否 | - | 定制师筛选 |
|
||||
| guestName | Query | String | 否 | - | 客人模糊 |
|
||||
| departDateFrom | Query | Date | 否 | yyyy-MM-dd | 出发日起 |
|
||||
| departDateTo | Query | Date | 否 | yyyy-MM-dd | 出发日止 |
|
||||
| sortBy | Query | String | 否 | - | 排序 |
|
||||
|
||||
#### 出参
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| records[].productType | String | 产品类型枚举值 CORE/ROUTE/CUSTOM/GROUP;取值稳定(不再受产品服务可用性影响);历史数据为 null 时前端不渲染 |
|
||||
| records[].productNo | String | 产品编号(不受本变更影响) |
|
||||
|
||||
#### 请求示例
|
||||
```http
|
||||
GET /v3/admin/order/grab-pool/hotel-requirements?page=1&pageSize=10 HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <管理员token>
|
||||
```
|
||||
(GET 无请求体)
|
||||
|
||||
#### 响应示例
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"total": 10,
|
||||
"records": [
|
||||
{ "orderId": "30456", "orderNo": "HL20260901101825666", "productType": "CORE", "productNo": "C-2026-001" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
`productType` 为 null(历史数据)时该字段返回 null,前端不渲染标签;列表其它字段不受影响。
|
||||
|
||||
#### 错误响应
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "未登录或登录已过期",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
- `productType` 与订单一致,只读。
|
||||
- `productType` 筛选入参语义不变;#7098 起由「查询后当前页内存匹配」改为 **SQL 下推**(WrapperX 条件 eq),跨页筛选结果精确。
|
||||
|
||||
### 2. 我的接单列表 `GET /v3/admin/order/grab-pool/my-claims/hotel`
|
||||
|
||||
**VO**: `HouseMyOrderItemRespVO`
|
||||
|
||||
#### 使用场景
|
||||
房务查看「我的接单」列表。本变更让每行可稳定显示产品类型标签。
|
||||
|
||||
#### 入参
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| page | Query | Integer | 否 | 默认 1 | 页码 |
|
||||
| pageSize | Query | Integer | 否 | 默认 20 | 每页条数 |
|
||||
| keyword | Query | String | 否 | - | 模糊关键词 |
|
||||
| productType | Query | String | 否 | CORE/ROUTE/CUSTOM/GROUP | 产品类型筛选(既有入参,语义不变) |
|
||||
| status | Query | String | 否 | - | 状态筛选 |
|
||||
| productName | Query | String | 否 | - | 产品名筛选 |
|
||||
| consultantId | Query | Long | 否 | - | 定制师筛选 |
|
||||
| guestName | Query | String | 否 | - | 客人筛选 |
|
||||
| departDateFrom | Query | Date | 否 | yyyy-MM-dd | 出发日起 |
|
||||
| departDateTo | Query | Date | 否 | yyyy-MM-dd | 出发日止 |
|
||||
|
||||
#### 出参
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| list[].productType | String | 产品类型枚举值;取值稳定,与订单一致;null 时前端不渲染 |
|
||||
|
||||
#### 请求示例
|
||||
```http
|
||||
GET /v3/admin/order/grab-pool/my-claims/hotel?page=1&pageSize=10 HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <管理员token>
|
||||
```
|
||||
(GET 无请求体)
|
||||
|
||||
#### 响应示例
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"total": 3,
|
||||
"list": [
|
||||
{ "orderId": "30456", "orderNo": "HL2026...", "productType": "GROUP" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
订单信息取不到时 `productType` 落 null,列表照常返回,不阻断。
|
||||
|
||||
#### 错误响应
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "未登录或登录已过期",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
- 取值与订单一致,只读不改写。
|
||||
- `productType` 筛选入参 #7098 起**生效并 SQL 下推**(此前被静默忽略)。
|
||||
|
||||
### 3. 房务订单详情 `GET /admin/house/orders/{orderId}`
|
||||
|
||||
**VO**: `HouseOrderDetailRespVO`
|
||||
|
||||
#### 使用场景
|
||||
房务打开订单详情弹窗。本变更让详情头部订单块稳定显示产品类型标签。
|
||||
|
||||
#### 入参
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| orderId | Path | Long | 是 | - | 订单 ID |
|
||||
| requirementId | Query | Long | 否 | - | 指定需求版本(查看历史作废版本) |
|
||||
|
||||
#### 出参
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| order.productType | String | 产品类型枚举值;取值稳定,与订单一致;null 时前端不渲染 |
|
||||
|
||||
#### 请求示例
|
||||
```http
|
||||
GET /admin/house/orders/30456 HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <管理员token>
|
||||
```
|
||||
(GET 无请求体)
|
||||
|
||||
#### 响应示例
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"order": { "orderId": "30456", "orderNo": "HL2026...", "productType": "CUSTOM" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
订单不存在时订单块仅含 orderId,`productType` 为 null,不返回 500。
|
||||
|
||||
#### 错误响应
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "未登录或登录已过期",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
- 取值与订单一致,只读。
|
||||
|
||||
### 4. 房务待办列表 `GET /v3/admin/order/todos`
|
||||
|
||||
**VO**: `HouseTodoItemVO`
|
||||
|
||||
#### 使用场景
|
||||
房务待办列表(订单聚合行)。本变更**新增** `productType` 出参,每行可显示产品类型标签。
|
||||
|
||||
#### 入参
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| page | Query | Integer | 否 | 默认 1 | 页码 |
|
||||
| pageSize | Query | Integer | 否 | 默认 20 | 每页条数 |
|
||||
| scope | Query | String | 否 | mine/others/all | 范围,默认 all |
|
||||
| todoType | Query | String | 否 | 多选逗号分隔 | 待办类型筛选 |
|
||||
| status | Query | String | 否 | OPEN/RESOLVED | 状态筛选 |
|
||||
| urgency | Query | String | 否 | danger/warn/normal | 紧急度筛选 |
|
||||
| keyword | Query | String | 否 | ≤32 字 | 关键词 |
|
||||
| orderId | Query | Long | 否 | - | 订单筛选 |
|
||||
| ownerUserId | Query | Long | 否 | - | 归属房务筛选 |
|
||||
| hotelId | Query | Long | 否 | - | 酒店筛选 |
|
||||
|
||||
#### 出参
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| list[].productType | String | **新增**:产品类型枚举值;无关联订单(酒店维度待办)或取不到订单时为 null,前端不渲染 |
|
||||
|
||||
#### 请求示例
|
||||
```http
|
||||
GET /v3/admin/order/todos?page=1&pageSize=10 HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <管理员token>
|
||||
```
|
||||
(GET 无请求体)
|
||||
|
||||
#### 响应示例
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"total": 10,
|
||||
"list": [
|
||||
{ "id": "70500", "orderNo": "HL2026...", "productType": "CORE" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
订单信息取不到时 `productType` 落 null,列表照常返回。
|
||||
|
||||
#### 错误响应
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "未登录或登录已过期",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
- 订单维度待办行与订单一致;酒店维度待办(无订单)productType 为 null。
|
||||
|
||||
### 5. 日历某天下钻 `GET /admin/house/calendar/day`
|
||||
|
||||
**VO**: `DayTourItemVO`
|
||||
|
||||
#### 使用场景
|
||||
房务日历点击某天下钻查看当天各「团」明细。本变更**新增** `productType` 出参。
|
||||
|
||||
#### 入参
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| date | Query | Date | 是 | yyyy-MM-dd | 下钻日期 |
|
||||
| scope | Query | String | 否 | mine/all | 范围,默认 mine |
|
||||
| status | Query | String | 否 | 多选 | 状态筛选 |
|
||||
|
||||
#### 出参
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| [].productType | String | **新增**:产品类型枚举值;团期折叠时取代表子订单;null 时前端不渲染 |
|
||||
|
||||
#### 请求示例
|
||||
```http
|
||||
GET /admin/house/calendar/day?date=2026-05-22&scope=all HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <管理员token>
|
||||
```
|
||||
(GET 无请求体)
|
||||
|
||||
#### 响应示例
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "teamKey": "B:900", "isGroup": true, "orderNo": "HL100", "productType": "GROUP" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
当天无团返空列表;展示信息缺失时 `productType` 为 null。
|
||||
|
||||
#### 错误响应
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "scope 非法",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
- 团期折叠场景取代表子订单(订单号最小)的产品类型,与同团其它子订单一致。
|
||||
|
||||
### 6. 月度对账订单明细 `GET /v3/admin/house/reconciliation/monthly/hotel-orders`
|
||||
|
||||
**VO**: `HotelReconciliationOrderVO`
|
||||
|
||||
#### 使用场景
|
||||
房务月度对账,查看某酒店下各订单明细行。本变更**新增** `productType` 出参。
|
||||
|
||||
#### 入参
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| month | Query | String | 是 | yyyy-MM | 对账月份 |
|
||||
| hotelId | Query | Long | 否 | - | 酒店筛选 |
|
||||
| hotelName | Query | String | 否 | - | 酒店名筛选 |
|
||||
|
||||
#### 出参
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| [].productType | String | **新增**:产品类型枚举值;订单缺失时为 null,前端不渲染 |
|
||||
|
||||
#### 请求示例
|
||||
```http
|
||||
GET /v3/admin/house/reconciliation/monthly/hotel-orders?month=2026-08&hotelId=1001 HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <管理员token>
|
||||
```
|
||||
(GET 无请求体)
|
||||
|
||||
#### 响应示例
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "orderId": "30456", "orderNo": "HL2026...", "productName": "额吉的故乡", "productType": "CUSTOM" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
无配房订单返空列表;订单信息缺失时 `productType` 为 null。
|
||||
|
||||
#### 错误响应
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "month 格式非法",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
- 仅订单维度明细行新增;酒店汇总行无订单粒度,不含此字段。
|
||||
|
||||
### 7. 需求历史列表 `GET /admin/house/orders/{orderId}/requirement-history`
|
||||
|
||||
**VO**: `RequirementHistoryItem`
|
||||
|
||||
#### 使用场景
|
||||
房务查看订单的用房需求历史版本。本变更**新增** `productType` 出参。
|
||||
|
||||
#### 入参
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| orderId | Path | Long | 是 | - | 订单 ID |
|
||||
| includeDiff | Query | Boolean | 否 | - | 是否含字段级 diff |
|
||||
| onlyReturned | Query | Boolean | 否 | - | 仅看退回版本 |
|
||||
|
||||
#### 出参
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| list[].productType | String | **新增**:产品类型枚举值;订单缺失时为 null,前端不渲染 |
|
||||
|
||||
#### 请求示例
|
||||
```http
|
||||
GET /admin/house/orders/30456/requirement-history HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <管理员token>
|
||||
```
|
||||
(GET 无请求体)
|
||||
|
||||
#### 响应示例
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"list": [
|
||||
{ "requirementId": "70123", "version": 2, "productType": "CORE" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
无历史返空列表;订单缺失时 `productType` 为 null。
|
||||
|
||||
#### 错误响应
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "未登录或登录已过期",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
- 各历史版本项产品类型同源同值(同订单)。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- `productType` 为**只读出参**,无需也不应在请求体中提交;抢单池/我的接单原有的 `productType` **筛选入参**语义不变,#7098 起下推 SQL 且「我的接单」入参生效。
|
||||
- 前端按枚举值渲染标签,`null` 时不渲染。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截)。
|
||||
- 非法参数 → 400。
|
||||
- 订单/需求信息缺失 → `productType` 落 null,不 500 不阻断列表/详情。
|
||||
- 历史数据(无产品类型)→ 字段为 null,前端不渲染标签。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 抢单池/我的接单/详情 productType | 产品服务不可用时可能返回空 | 取值稳定,不受产品服务可用性影响 |
|
||||
| 待办/日历/对账/需求历史 productType | 无此字段 | 新增返回 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 产品类型取值稳定性 | 依赖产品服务实时可用 | 订单主数据直读,稳定 |
|
||||
| 列表/详情标签覆盖 | 仅抢单池/我的接单/详情 | 待办/日历/对账/需求历史全覆盖 |
|
||||
| `productType` 筛选实现 | 查询后按当前页内存匹配(「我的接单」入参被忽略) | SQL 下推条件 eq,跨页精确(「我的接单」入参生效) |
|
||||
|
||||
## 六.7、影响评估(修改/删除类必写)
|
||||
|
||||
- **是否破坏向后兼容**: 否(纯出参字段新增 + 已有字段取值口径不变)
|
||||
- **前端是否必须同步上线**: 否(不渲染则忽略新字段;旧前端不受影响)
|
||||
- **前端 workaround 清理点**: 无
|
||||
|
||||
## 七、不影响范围(显式声明)
|
||||
|
||||
- **仅影响**: 管理后台房务上述 7 个接口的出参。
|
||||
- **零影响**: C 端接口、订单创建/调整、车务、结算、`productType` 筛选入参语义、酒店维度视图(无订单行)。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
真实网关接口输出(api.test.1814.love),带 ✓ 标记:
|
||||
|
||||
```
|
||||
GET /admin/house/orders/... (3 个订单) → 200 + order.productType=CORE 与订单一致 ✓
|
||||
GET /v3/admin/order/todos → 200 + 10 行均含 productType(订单行=CORE) ✓
|
||||
GET /v3/admin/order/grab-pool/hotel-requirements → 200 + 10 行 productType=CORE ✓
|
||||
```
|
||||
|
||||
验证账号: 房务管理员(测试环境)。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#7070](https://git.1814.love:8443/wx/HL/issues/7070)
|
||||
- 关联 PR: [wx/HL#7077](https://git.1814.love:8443/wx/HL/pulls/7077) / [wx/HL#7084](https://git.1814.love:8443/wx/HL/pulls/7084)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#7070](https://git.1814.love:8443/wx/HL/issues/7070)
|
||||
- **PR**: [#7077](https://git.1814.love:8443/wx/HL/pulls/7077) / [#7084](https://git.1814.love:8443/wx/HL/pulls/7084)
|
||||
- **Merge commit**: [19ca3d702](https://git.1814.love:8443/wx/HL/commit/19ca3d702)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,370 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7078"
|
||||
title: "供应商企微审核中冻结资料修改"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "f4d3e1c9"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-04"
|
||||
status_note: '后端已部署并通过 TEST;前端需在 statusName="审核中" 时制灰供应商、账户、合同及冲突状态写操作。'
|
||||
updated_at: "2026-09-04"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商模块:企微审核中冻结资料修改
|
||||
|
||||
> **服务**: `hl-resource-service`
|
||||
> **Issue**: [#7078](https://git.1814.love:8443/wx/HL/issues/7078)
|
||||
> **PR**: [#7086](https://git.1814.love:8443/wx/HL/pulls/7086)
|
||||
> **日期**: 2026-09-04
|
||||
> **影响范围**: 管理后台供应商详情、账户、合同和状态操作
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
`statusName="审核中"` 时,前端统一制灰供应商资料、账户、合同及冲突状态写入口;详情和审批记录仍可查看,注册审批撤销等合法审批退出路径保持可用。`statusName` 是 #7069 已交付字段,本次未新增请求字段、响应字段或错误码。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 更新供应商 | PUT | `/admin/supplier/items/{supplierId}/update` | 行为修改 | 企微主体审批中返回 `395005` |
|
||||
| 2 | 新增合同 | POST | `/admin/supplier/items/{supplierId}/contracts/add` | 行为修改 | 企微主体审批中返回 `395005` |
|
||||
| 3 | 更新合同 | PUT | `/admin/supplier/items/{supplierId}/contracts/{contractId}/update` | 行为修改 | 企微主体审批中返回 `395005` |
|
||||
| 4 | 删除合同 | DELETE | `/admin/supplier/items/{supplierId}/contracts/{contractId}/del` | 行为修改 | 企微主体审批中返回 `395005` |
|
||||
| 5 | 恢复合作 | POST | `/admin/supplier/items/{supplierId}/resume` | 行为修改 | 状态审批中不得绕过审批恢复合作 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 更新供应商 `PUT /admin/supplier/items/{supplierId}/update`
|
||||
|
||||
**VO**: `SupplierUpdateReqVO → SupplierWriteRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
修改供应商主体、类型、联系人、资质、评价或其他档案资料。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 | 供应商 ID |
|
||||
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 当前供应商版本 |
|
||||
| `changeReason` | Body | String | 条件必填 | 最长 500 | 非草稿修改必填 |
|
||||
| 资料字段 | Body | 原类型 | 否 | 沿用原契约 | 至少提交一个可变更字段 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data` | `SupplierWriteRespVO` | 成功响应结构不变 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"remark":"更新备注","changeReason":"资料修正","expectedUpdateTime":"2026-09-04 10:00:00"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","data":{"supplierId":"2095000000000000001","status":"ACTIVE","updateTime":"2026-09-04 10:01:00"},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口无空成功结果,也不提供降级成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395005,"message":"当前状态不允许执行该操作","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 企微注册或主体状态审批为 `PENDING` 时拒绝,供应商主档及聚合子项均保持不变。
|
||||
- 其他权限、字段校验、并发和审计规则不变。
|
||||
|
||||
### 2. 新增合同 `POST /admin/supplier/items/{supplierId}/contracts/add`
|
||||
|
||||
**VO**: `SupplierContractCreateReqVO → SupplierContractRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
为供应商独立登记一份线下合同。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 | 供应商 ID |
|
||||
| `changeReason` | Body | String | 是 | 最长 500 | 登记原因 |
|
||||
| 合同资料字段 | Body | 原类型 | 否 | 沿用原契约 | 名称、编号、日期、金额等字段不变 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data` | `SupplierContractRespVO` | 成功响应结构不变 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"contractName":"年度框架合同","changeReason":"线下签署后登记"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","data":{"contractId":"2095000000000000002","contractName":"年度框架合同","updateTime":"2026-09-04 10:02:00"},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
除 `changeReason` 外的合同业务字段可空;成功仍返回合同对象,不提供降级成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395005,"message":"当前状态不允许执行该操作","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 企微注册或主体状态审批为 `PENDING` 时不新增合同或审计记录。
|
||||
- 合同字段、权限、锁和幂等规则不变。
|
||||
|
||||
### 3. 更新合同 `PUT /admin/supplier/items/{supplierId}/contracts/{contractId}/update`
|
||||
|
||||
**VO**: `SupplierContractUpdateReqVO → SupplierContractRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
完整替换一份已有合同的业务资料。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` / `contractId` | Path | String | 是 | 正整数 | 供应商和合同 ID |
|
||||
| `changeReason` | Body | String | 是 | 最长 500 | 修改原因 |
|
||||
| `expectedUpdateTime` | Body | String | 否 | `yyyy-MM-dd HH:mm:ss` | 合同并发版本 |
|
||||
| 合同资料字段 | Body | 原类型 | 否 | 沿用原契约 | 完整替换语义不变 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data` | `SupplierContractRespVO` | 成功响应结构不变 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"contractName":"年度框架合同(修订)","changeReason":"合同修订","expectedUpdateTime":"2026-09-04 10:02:00"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","data":{"contractId":"2095000000000000002","contractName":"年度框架合同(修订)","updateTime":"2026-09-04 10:03:00"},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
合同业务字段保持既有可空和完整替换语义;写接口不提供降级成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395005,"message":"当前状态不允许执行该操作","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 企微注册或主体状态审批为 `PENDING` 时,先于合同查询拒绝,不修改合同或审计记录。
|
||||
- 合同归属、可选版本及无变化校验不变。
|
||||
|
||||
### 4. 删除合同 `DELETE /admin/supplier/items/{supplierId}/contracts/{contractId}/del`
|
||||
|
||||
**VO**: `SupplierContractDeleteReqVO → Void`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
软删除一份已有合同。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` / `contractId` | Path | String | 是 | 正整数 | 供应商和合同 ID |
|
||||
| `changeReason` | Body | String | 是 | 最长 500 | 删除原因 |
|
||||
| `expectedUpdateTime` | Body | String | 否 | `yyyy-MM-dd HH:mm:ss` | 合同并发版本 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data` | null | 成功时为空 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"changeReason":"合同登记作废","expectedUpdateTime":"2026-09-04 10:03:00"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","data":null,"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
成功时 `data=null`;请求体不能省略,也不提供降级成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395005,"message":"当前状态不允许执行该操作","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 企微注册或主体状态审批为 `PENDING` 时,先于合同查询拒绝,不删除合同或新增审计记录。
|
||||
- 合同归属、可选版本、软删除及幂等规则不变。
|
||||
|
||||
### 5. 恢复合作 `POST /admin/supplier/items/{supplierId}/resume`
|
||||
|
||||
**VO**: `SupplierStatusChangeReqVO → SupplierStatusChangeRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
将没有在途主体审批的暂停合作供应商恢复合作。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 | 供应商 ID |
|
||||
| `reason` | Body | String | 是 | 1~500 字符 | 恢复原因 |
|
||||
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 当前供应商版本 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.status` / `data.statusName` | String | 成功后为 `ACTIVE` / “合作中” |
|
||||
| `data.updateTime` | String | 新并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"reason":"恢复合作","expectedUpdateTime":"2026-09-04 10:00:00"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","data":{"supplierId":"2095000000000000003","status":"ACTIVE","statusName":"合作中","updateTime":"2026-09-04 10:04:00"},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口无空成功结果,也不提供降级成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395005,"message":"当前状态不允许执行该操作","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 暂停合作供应商已有企微主体状态审批时不得恢复合作,来源状态保持不变。
|
||||
- 原有权限、来源状态、并发、审计和幂等规则不变。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- 后端以企微注册或主体状态审批 `PENDING` 为权威门禁;页面即使状态滞后,写请求仍可能返回 `395005`。
|
||||
- 前端展示和制灰统一读取列表或详情的 `statusName`;`status` 继续用于筛选和生命周期判断。
|
||||
- 账户新增和设默认接口的后端契约未变:注册审批中的 `VETTING`、状态审批来源的 `SUSPENDED` / `BLACKLIST` 均不满足账户写入所需的 `ACTIVE` 状态,仍返回既有 `395010`。
|
||||
- 详情、账户详情、审批记录和变更记录等读取入口保持可用;注册审批撤销 `POST /admin/supplier/items/{supplierId}/approval/revoke` 保持可用。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 本次没有数据库结构、初始化数据或迁移变化。
|
||||
- 审核中被拒绝的请求不修改供应商、账户、合同、状态或审计数据。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 五类展示场景:注册、列入黑名单、解除黑名单、“终止且账清”和“拉黑且账清”在企微审核中均返回 `statusName="审核中"`。
|
||||
- 审批结束后刷新列表或详情,再按最终生命周期状态恢复原有可用操作。
|
||||
- 请求参数、成功响应、认证、权限、配置、Redis 和 MQ 契约均未改变。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 行为 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 企微主体审批中修改供应商资料 | 部分入口可能继续写入 | 返回 `395005`,业务数据不变 |
|
||||
| 企微主体审批中新增、更新或删除合同 | 部分入口可能继续写入 | 返回 `395005`,业务数据不变 |
|
||||
| 状态审批中恢复合作 | 可能绕过审批候选来源状态 | 返回 `395005`,来源状态不变 |
|
||||
| 账户写入 | 既有规则要求供应商为 `ACTIVE` | 规则不变;前端在“审核中”时提前制灰 |
|
||||
| 详情与合法审批退出 | 可用 | 保持可用 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 是;审批中的资料和合同写请求由可能成功调整为业务拒绝。
|
||||
- **前端是否必须同步上线**: 是。
|
||||
- **前端 workaround 清理点**: 不再仅按 `status` 放开写按钮,增加 `statusName="审核中"` 的统一制灰条件。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 企微注册或主体状态审批中的供应商资料、账户、合同和冲突状态操作。
|
||||
- **零影响**: 详情及审批记录读取、注册审批撤销、企微回调与轮询、审批结束后的原有业务规则。
|
||||
- **无变化**: 请求/响应字段、错误码定义、数据库、配置、Redis、MQ;后端仓库未修改前端代码。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署提交:`6462397526ad61acfbcf503486d8507c9c1c7293`。
|
||||
|
||||
```text
|
||||
GET /admin/supplier/items/list?limit=200 → 200;注册审批返回 VETTING / 审核中 ✓
|
||||
GET /admin/supplier/items/{supplierId}/basic-info/view → 200;详情同为 VETTING / 审核中 ✓
|
||||
PUT /admin/supplier/items/{supplierId}/update → 395005 ✓
|
||||
POST /admin/supplier/items/{supplierId}/contracts/add → 395005 ✓
|
||||
PUT /admin/supplier/items/{supplierId}/contracts/{contractId}/update → 395005 ✓
|
||||
DELETE /admin/supplier/items/{supplierId}/contracts/{contractId}/del → 395005 ✓
|
||||
POST /admin/supplier/items/{supplierId}/bank-accounts/add → 395010 ✓
|
||||
```
|
||||
|
||||
上述拒绝请求前后,供应商版本、账户、合同、审批和审计的只读数据库快照哈希一致。TEST 当前没有四类状态审批的在途样本,因此没有发起企微审批造数;对应子类型、来源状态、“审核中”投影及全部写入口由合并提交中的精确自动化测试覆盖。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- [Issue #7078](https://git.1814.love:8443/wx/HL/issues/7078)
|
||||
- [PR #7086](https://git.1814.love:8443/wx/HL/pulls/7086)
|
||||
- [前置展示契约 #7069](https://git.1814.love:8443/wx/HL/issues/7069)
|
||||
|
||||
## 前端动作与当前状态
|
||||
|
||||
- `statusName="审核中"` 时,统一制灰供应商资料编辑/保存、账户新增/设默认、合同新增/编辑/删除,以及暂停、恢复、拉黑、解除拉黑和归档等冲突写入口。
|
||||
- 保留详情、审批记录、审批进度刷新和合法审批退出。
|
||||
- **当前状态:待前端处理。**
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#7078](https://git.1814.love:8443/wx/HL/issues/7078)
|
||||
- **PR**: [#7086](https://git.1814.love:8443/wx/HL/pulls/7086)
|
||||
- **Merge commit**: [6462397526ad61acfbcf503486d8507c9c1c7293](https://git.1814.love:8443/wx/HL/commit/6462397526ad61acfbcf503486d8507c9c1c7293)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,375 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7079"
|
||||
title: "团期人员配置位成员改由字典决定,取值域三处对齐"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "PR #7085 已合入 dev-v3(合并提交 d1b724d59);2026-09-04 部署测试环境并经网关实测。含字典初始化脚本 sql/dict_group_batch_staff_slot.sql,需手工执行"
|
||||
updated_at: "2026-09-04"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期人员配置: 配置位收哪些人员类型改由数据字典决定
|
||||
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #7085 | **Issue**: #7079 | **合并提交**: `d1b724d59`
|
||||
> **影响范围**: 管理后台「团期详情 → 配置导游/摄影」弹窗的候选池
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**「导游位收哪些人员类型」不再写死在代码里,改由数据字典决定。业务在字典管理页面加一行,最多 5 分钟后生效,不需要发版。**
|
||||
|
||||
```
|
||||
group_batch_staff_slot_guide → GUIDE、LEADER
|
||||
group_batch_staff_slot_photographer → PHOTOGRAPHER
|
||||
```
|
||||
|
||||
另有一处**落库取值变化**:导游位并收导游与领队,改前不论选的是谁,`order_batch_staff.staff_role` 落的都是前端传的配置位名(往往是 `GUIDE`),「这一位上站的到底是导游还是领队」在数据里就丢了。现在落该人员在资源域的真实 `staffType`。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
人员类型 `staff_type` 本来就是「服务人员管理」里维护的字典,实有 7 项。而团期人员配置位只认死了导游位与摄影位两个、成员也写死在 Java 字面量里,要让研学老师也能配进导游位就得改代码发版。本次把这件事交还给业务。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 团期人员候选列表 | GET | `/v3/admin/group-batch/:groupBatchId/staff/candidates` | **候选池来源变更** | 成员由字典决定;`582113` 文案变更 |
|
||||
| 2 | 全量保存团期人员配置 | PUT | `/v3/admin/group-batch/:groupBatchId/staff` | **落库取值变更** | `staffRole` 落真实 `staffType` |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 团期人员候选列表 `GET /v3/admin/group-batch/:groupBatchId/staff/candidates`
|
||||
|
||||
**VO**: `StaffCandidateRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
「配置导游 / 配置摄影」弹窗打开时调用,渲染可选人员列表。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `groupBatchId` | Path | Long | ✅ | — | 产品侧团期 batchId |
|
||||
| `role` | Query | String | ✅ | `GUIDE` / `PHOTOGRAPHER`,大小写敏感 | 配置位标识;未知值返回 `582113` |
|
||||
|
||||
#### 出参 `Result<List<StaffCandidateRespVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `staffId` | Long | 人员 ID,**未变** |
|
||||
| `staffName` | String | 姓名,**未变** |
|
||||
| `staffType` | String | 资源域人员类型,**未变** |
|
||||
| `staffPhone` | String | 脱敏手机号,**未变** |
|
||||
| `assigned` | Boolean | 是否已选,**未变** |
|
||||
|
||||
> **出参结构完全没有变化**,变的是这个列表里会出现哪些人——由字典 `group_batch_staff_slot_*` 决定。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/group-batch/990707900901/staff/candidates?role=GUIDE
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"staffId": 1002,
|
||||
"staffName": "李雪梅",
|
||||
"staffType": "GUIDE",
|
||||
"staffPhone": "138****1002",
|
||||
"assigned": false
|
||||
},
|
||||
{
|
||||
"staffId": 1005,
|
||||
"staffName": "刘大山",
|
||||
"staffType": "LEADER",
|
||||
"staffPhone": "138****1005",
|
||||
"assigned": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
字典读空或字典服务不可用时**不报错**,回落到内置默认值(导游位 `GUIDE`+`LEADER`,摄影位 `PHOTOGRAPHER`)并打 WARN,候选列表照常返回。资源域无可用人员时返回空数组:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
配置位标识未知(含小写 `guide`、`DRIVER`):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 582113,
|
||||
"message": "人员配置位不合法,请检查配置位标识",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 配置位成员来自字典,改字典后各实例最多 5 分钟内生效(本地缓存有界 TTL)
|
||||
- 字典里写了取值域外的人员类型会被忽略并 WARN,其余行照常生效
|
||||
- 字典整体读不到时回落内置默认值,行为与改前一致
|
||||
- `role` 大小写敏感,小写不认(沿用既有口径)
|
||||
- 司机不设配置位,由车务派车投影产生
|
||||
|
||||
### 2. 全量保存团期人员配置 `PUT /v3/admin/group-batch/:groupBatchId/staff`
|
||||
|
||||
**VO**: `BatchStaffConfigReqVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
「配置导游 / 配置摄影」弹窗点保存时调用,全量覆盖该团期人员配置。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `groupBatchId` | Path | Long | ✅ | — | 产品侧团期 batchId |
|
||||
| `staffList` | Body | Array | ❌ | 传空则清空 | 全量覆盖 |
|
||||
| `staffList[].staffId` | Body | Long | ✅ | — | 资源域人员 ID |
|
||||
| `staffList[].staffRole` | Body | String | ✅ | 取值域本次扩至 8 项 | 见下方取值域说明 |
|
||||
| `staffList[].sortOrder` | Body | Integer | ❌ | 缺省 0 | 展示排序 |
|
||||
|
||||
#### 出参 `Result<BatchStaffConfigRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `staffList` | Array | 落库后的人员快照;其中 `staffRole` **取值口径变更** |
|
||||
| `affectedOrderCount` | Integer | 扇出到的活跃子订单数,**未变** |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"staffList": [
|
||||
{
|
||||
"staffId": 1005,
|
||||
"staffRole": "GUIDE",
|
||||
"sortOrder": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
请求传的是配置位名 `GUIDE`,但 1005 在资源域是领队,出参与落库都归一为 `LEADER`:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"staffList": [
|
||||
{
|
||||
"staffId": 1005,
|
||||
"staffRole": "LEADER",
|
||||
"staffName": "刘大山",
|
||||
"staffPhone": "138****1005",
|
||||
"sortOrder": 0
|
||||
}
|
||||
],
|
||||
"affectedOrderCount": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
`staffList` 传空数组即清空该团期全部人员配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"staffList": [],
|
||||
"affectedOrderCount": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
`staffRole` 不在取值域内:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "员工角色不在取值域内,取值域对齐 SettlementStaffRoleEnum 与 staff_role 字典",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 落库 `staffRole` 只在**配置位内部**归一:请求角色能解析到配置位、且该人员真实类型也在这个配置位成员里时才替换
|
||||
- 跨位不匹配(领队被配到摄影位)沿用入参,本次不做拒绝
|
||||
- `DRIVER` / `OTHER` 不属于任何配置位,沿用入参
|
||||
- 资源域反查不到人员类型时沿用入参
|
||||
- 导游位有人(`GUIDE` 或 `LEADER`)即置 `guide_ready`,与改前一致
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- **候选池成员不再由代码保证**,前端不要硬编码「导游位只会出现导游和领队」;以接口返回为准。
|
||||
- **`staffRole` 落库值可能与提交值不同**:提交配置位名,落库是该人员的真实人员类型。前端若需回显角色,读出参里的 `staffRole`,不要沿用自己提交的值。
|
||||
- **`staffRole` 取值域本次由 5 项扩至 8 项**:新增 `GUIDE_ASSISTANT` / `STUDY_TEACHER` / `LIFE_TEACHER`。
|
||||
- **改字典不是立即全局生效**:各实例本地缓存 5 分钟有界 TTL,期间不同实例口径可能不同,只影响候选池多列/少列一类人员。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
无 DDL、无 Flyway 迁移。**但需手工执行一个字典初始化脚本**:
|
||||
|
||||
```
|
||||
sql/dict_group_batch_staff_slot.sql 新增,幂等,可重复执行
|
||||
```
|
||||
|
||||
| 表 | 变化 |
|
||||
|---|---|
|
||||
| `sys_dict_type` | 新增 2 行:`group_batch_staff_slot_guide`(8071) / `group_batch_staff_slot_photographer`(8072) |
|
||||
| `sys_dict_data` | 新增 3 行配置位成员;`staff_role` 补 `STUDY_TEACHER` / `LIFE_TEACHER` 两行 |
|
||||
| `order_batch_staff.staff_role` | 结构未变;**新写入的取值口径变了**,存量行不动 |
|
||||
|
||||
> `dict_type_id` 取 8071/8072 而非号段顺序的 8021/8022——后者在本机与测试环境**都已被** `contract_status` / `contract_platform` 占用(两处实测确认)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 字典未初始化 → 回落内置默认值,行为与改前完全一致
|
||||
- 字典含非法人员类型 → 忽略该行 + WARN,其余生效
|
||||
- 字典全部非法 → 视同读空,回落默认值
|
||||
- 字典行 `dictValue` 为空或空白 → 直接跳过,不刷告警
|
||||
- Feign 返回 null / 失败 / 抛异常 → 三种都回落默认值,不把异常透给调用方
|
||||
- `role` 小写 / 未知 → `582113`
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
**新增字典类型 2 个**:`group_batch_staff_slot_guide`、`group_batch_staff_slot_photographer`。取值域 = 资源域 `staff_type`。
|
||||
|
||||
**`staff_role` 字典扩至 8 项**,与 `SettlementStaffRoleEnum` 和接口 `@Pattern` 三处对齐(测试环境实测三处完全一致)。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 项 | 变更前 | 变更后 |
|
||||
|---|---|---|
|
||||
| 配置位成员 | 写死在 3 处 Java 字面量 | 由字典决定,加一行即生效 |
|
||||
| 扩一种人员类型 | 改代码 + 发版 | 改字典,最多 5 分钟生效 |
|
||||
| 落库 `staffRole` | 前端传的配置位名 | 该人员真实 `staffType` |
|
||||
| `staffRole` 取值域 | 5 项 | 8 项 |
|
||||
| `582113` 文案 | 「只支持导游位与摄影位」 | 「请检查配置位标识」 |
|
||||
| 字典不可用 | — | 回落内置默认值,不中断 |
|
||||
| 路径 / 入参 / 出参结构 | — | **全部不变** |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
| 维度 | 评估 |
|
||||
|---|---|
|
||||
| 兼容性 | 字典未初始化时回落默认值,与改前**完全一致**,不会坏 |
|
||||
| 前端 | **无需改动**。路径、入参、出参结构均未变;但不要再硬编码候选池成员,也不要沿用自己提交的 `staffRole` 回显 |
|
||||
| 数据 | 无 DDL;需手工执行一个幂等字典脚本;存量 `staff_role` 行不动 |
|
||||
| 性能 | 每个配置位一次字典 Feign,5 分钟缓存;候选池按类型逐类拉取,类型多一种多一次资源域调用 |
|
||||
| 回滚 | `git revert`;字典行留着无害(回滚后代码不读它) |
|
||||
| 风险 | 中低。主要风险是下游若有按 `staff_role == 'GUIDE'` 硬比对处会受落库取值变化影响;order-v3 内已全部改为按配置位归组 |
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **零影响**:两个接口的路径、HTTP 方法、入参字段、出参结构
|
||||
- **零影响**:摄影位口径,字典默认仍只收 `PHOTOGRAPHER`
|
||||
- **零影响**:staff-fees 费用录入分流——只加枚举取值域,未动 `SettlementStaffFeeDetailCodec` 的 `GUIDE`/`PHOTOGRAPHER` 两族判定
|
||||
- **零影响**:存量 `order_batch_staff` / `order_staff_assignment` 数据
|
||||
- **零影响**:`DRIVER` / `OTHER` 角色的处理
|
||||
- **未新建端点**
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
✅ 2026-09-04 于测试环境网关实测,真实鉴权(管理端 `test_admin`,角色定制师)。
|
||||
|
||||
- 网关 `https://api.test.1814.love:9443`,分支 `dev-v3`,合并提交 `d1b724d59`
|
||||
- 部署方式:双实例滚动更新(8186 → 8086),各 10s 就绪,零停机
|
||||
- 部署内容经部署面板 `/api/git/backend` 交叉核对:构建时点 `d1b724d59` 已是 `dev-v3` 顶端
|
||||
- 字典初始化脚本已在测试环境执行,幂等
|
||||
|
||||
| # | 用例 | 期望 | 实测 |
|
||||
|---|---|---|---|
|
||||
| 1 | 导游位候选(字典 GUIDE+LEADER) | 与改前一致 | ✅ 7 人 |
|
||||
| 2 | 摄影位候选 | 口径不变 | ✅ 1 人 |
|
||||
| 3 | `role=DRIVER` | `582113` 新文案 | ✅ 「请检查配置位标识」 |
|
||||
| 4 | 导游位选领队(提交 `GUIDE`) | 落库真实类型 | ✅ 出参与落库均为 `LEADER` |
|
||||
| 5 | 同场景 `guide_ready` | 仍置位 | ✅ 1 |
|
||||
| 6 | `staff_role` 取值域三处对齐 | 字典 = 枚举 = `@Pattern` | ✅ 8 项完全一致 |
|
||||
| 7 | **字典加一行 `GUIDE_ASSISTANT`** | 无需发版即生效 | ✅ **301 秒后候选 7 → 8 人,陈小燕出现;两个实例连打 8 次均为 8** |
|
||||
|
||||
**观察**:双实例持续 running,服务日志最近 200 行 `ERROR` / `Exception` 命中 **0** 条;配置位字典相关 WARN **0** 条,说明字典读取正常、未走降级。
|
||||
|
||||
用例 7 是本单的核心证据:15:30:33 往字典插一行,15:35:44(301 秒后)候选列表由 7 人变 8 人——**没有改代码、没有重启服务、没有重新部署**。延迟符合 5 分钟有界 TTL 的设计;随后连打 8 次全部返回 8 人,两个实例都已回源。演示用的那一行验证后已撤除,出厂默认仍是 `GUIDE` + `LEADER`。
|
||||
|
||||
**本地单测**:**431 例全过**。新增 `GroupBatchStaffSlotResolverTest` 14 例(正常读取、加行即生效、去重、TTL 命中缓存、读空/Feign null/Feign 异常三种降级、降级后判定仍工作、非法值过滤、全非法回落、空白值跳过、未知角色无配置位、枚举大小写敏感);`GroupBatchStaffConfigServiceTest` 新增 3 例覆盖落库取值三个分支。
|
||||
|
||||
**测试数据**:一律 `T7079-` 前缀,验证后物理删除。`test_admin` 口令一次性置换,取到令牌后立即按备份还原,逐字节一致;共享库写入全程持 `test-db` 租约。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
- #7065(Refs #7063 第 1 波)本 PR 承接它,`StaffSlotRoles` 由事实源退化为兜底默认值
|
||||
- 裁决来源:#7063 的 D-1 / D-3 / D-4(jw 2026-09-04)
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 团期需求文档:`docs/group/`(dev-v3 分支)
|
||||
- 字典初始化脚本:`sql/dict_group_batch_staff_slot.sql`
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: #7079
|
||||
- **PR**: #7085(合并提交 `d1b724d59`)
|
||||
@@ -0,0 +1,267 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7087"
|
||||
title: "供应商新建修改必填资料校验"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "72d63c8e"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-04"
|
||||
status_note: "后端已部署并通过 TEST;前端需为供应商新建和编辑表单补齐主体资料、联系人及结算账户必填校验。"
|
||||
updated_at: "2026-09-04"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商模块:新建与修改补齐必填资料校验
|
||||
|
||||
`POST /admin/supplier/items/add` 新建时必须一次提交完整资料;`PUT /admin/supplier/items/{supplierId}/update` 仍是增量接口,但保存后的供应商聚合必须完整。前端需同步补齐表单必填标识和提交前校验。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 新建供应商草稿 | POST | `/admin/supplier/items/add` | 必填校验收紧 | 主体资料完整,且至少一名联系人、一个初始账户 |
|
||||
| 2 | 修改供应商资料 | PUT | `/admin/supplier/items/{supplierId}/update` | 聚合完整性校验 | 省略字段保留现值;显式空类型、联系人或账户拒绝 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 新建供应商草稿 `POST /admin/supplier/items/add`
|
||||
|
||||
**VO**: `SupplierDraftSaveReqVO → SupplierWriteRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理端完成供应商新建表单后保存草稿。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `fullName` | Body | String | 是 | 非空,最长 500 | 供应商全称 |
|
||||
| `taxNo` | Body | String | 是 | 合法主体证件号 | 统一社会信用代码 |
|
||||
| `types` | Body | Array | 是 | 至少 1 项,最多 15 项 | `typeCode` 取 `supplier_type` 生效字典值 |
|
||||
| `legalRepresentative` | Body | String | 是 | 非空 | 法定代表人 |
|
||||
| `legalRepresentativeIdNo` | Body | String | 是 | 合法 18 位居民身份证号 | 法人身份证号 |
|
||||
| `legalRepresentativeIdCardFrontUrl` | Body | String | 是 | 公网 HTTPS 永久地址 | 身份证人像面 |
|
||||
| `legalRepresentativeIdCardBackUrl` | Body | String | 是 | 公网 HTTPS 永久地址 | 身份证国徽面 |
|
||||
| `contactPhone` | Body | String | 是 | 合法手机号、座机或 400/800 号码 | 法人联系电话 |
|
||||
| `establishDate` | Body | String | 是 | `yyyy-MM-dd`,不得晚于当天 | 成立日期 |
|
||||
| `balance` | Body | Number | 是 | 最多 16 位整数、2 位小数 | 余额,可为正数、0 或负数 |
|
||||
| `paymentType` | Body | String | 是 | `supplier_payment_type` 生效字典值 | 支付类型 |
|
||||
| `mainCooperation` | Body | String | 是 | 非空 | 主要合作内容 |
|
||||
| `licenseImageUrl` | Body | String | 是 | 非空 | 营业执照影像地址 |
|
||||
| `address` | Body | String | 是 | 非空,最长 500 | 注册地址 |
|
||||
| `contacts` | Body | Array | 是 | 至少 1 项,最多 100 项 | 每项填写姓名、电话及 `sup_content_role` 角色 |
|
||||
| `initialAccounts` | Body | Array | 是 | 当前必须且只能 1 项 | 每项填写账户类型、银行和账号 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.supplierId` | String | 供应商 ID |
|
||||
| `data.supplierNo` | String | 供应商编号 |
|
||||
| `data.status` | String | 新建成功为 `DRAFT` |
|
||||
| `data.initialAccounts` | Array | 初始账户摘要 |
|
||||
| `data.updateTime` | String | 后续修改使用的并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "示例供应商有限公司",
|
||||
"taxNo": "91350211M000100Y46",
|
||||
"types": [{"typeCode": "HOTEL"}],
|
||||
"legalRepresentative": "张三",
|
||||
"legalRepresentativeIdNo": "11010519491231002X",
|
||||
"legalRepresentativeIdCardFrontUrl": "https://example.com/supplier/id-front.jpg",
|
||||
"legalRepresentativeIdCardBackUrl": "https://example.com/supplier/id-back.jpg",
|
||||
"contactPhone": "13800138000",
|
||||
"establishDate": "2020-01-02",
|
||||
"balance": 0,
|
||||
"paymentType": "1",
|
||||
"mainCooperation": "酒店资源合作",
|
||||
"licenseImageUrl": "https://example.com/supplier/license.jpg",
|
||||
"address": "厦门市思明区示例路 1 号",
|
||||
"contacts": [{"contactName": "李四", "contactPhone": "13800138001", "contactRole": "contentBus", "isPrimary": true}],
|
||||
"initialAccounts": [{"accountType": "CORPORATE", "bankName": "示例银行", "accountNo": "6222000012345678"}]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2095000000000000001",
|
||||
"supplierNo": "SUP2095000000000000001",
|
||||
"status": "DRAFT",
|
||||
"initialAccounts": [{"accountId": "2095000000000000002", "status": "DRAFT"}],
|
||||
"updateTime": "2026-09-04 16:48:49"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口没有空数据成功或降级成功;任一必填项缺失时返回失败响应。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":400,"message":"联系人不能为空","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 未登录返回业务码 `401`;写入仍要求 `FINANCE` 或 `SUPER_ADMIN` 及 `supplier:create` 权限。
|
||||
- `types`、`contacts`、`initialAccounts` 传 `null`、省略或空数组均视为缺失。
|
||||
- 校验失败不创建供应商或子项;响应可能使用 HTTP 200,必须同时检查 `code` 与 `success`。
|
||||
|
||||
### 2. 修改供应商资料 `PUT /admin/supplier/items/{supplierId}/update`
|
||||
|
||||
**VO**: `SupplierUpdateReqVO → SupplierWriteRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理端编辑既有供应商资料并按最新版本保存。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID | 目标供应商 |
|
||||
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 取详情最新 `updateTime` |
|
||||
| 主体必填资料 | Body | 原类型 | 条件必填 | 与新建接口相同 | 省略表示保留现值;保存后的有效值必须完整 |
|
||||
| `types` | Body | Array | 条件必填 | 显式提交时至少 1 项 | 省略保留存量,`[]` 拒绝 |
|
||||
| `contacts` | Body | Array | 条件必填 | 显式提交时至少 1 项 | 省略保留存量,`[]` 拒绝 |
|
||||
| `initialAccounts` | Body | Array | 条件必填 | 显式提交时当前必须且只能 1 项 | 省略保留存量,`[]` 拒绝 |
|
||||
| `changeReason` | Body | String | 条件必填 | 最长 500 | `DRAFT` 可省略,其他可修改状态沿用既有规则 |
|
||||
|
||||
主体必填资料指:`fullName`、`taxNo`、`legalRepresentative`、`legalRepresentativeIdNo`、`legalRepresentativeIdCardFrontUrl`、`legalRepresentativeIdCardBackUrl`、`contactPhone`、`establishDate`、`balance`、`paymentType`、`mainCooperation`、`licenseImageUrl`、`address`。
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.supplierId` | String | 供应商 ID |
|
||||
| `data.status` | String | 保存后的状态 |
|
||||
| `data.initialAccounts` | Array | 当前初始账户摘要 |
|
||||
| `data.updateTime` | String | 保存后的新并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"shortName": "示例供应商",
|
||||
"expectedUpdateTime": "2026-09-04 16:48:49"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2095000000000000001",
|
||||
"status": "DRAFT",
|
||||
"initialAccounts": [{"accountId": "2095000000000000002", "status": "DRAFT"}],
|
||||
"updateTime": "2026-09-04 16:49:16"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
省略未修改字段会保留已存值;接口没有空数据成功或降级成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":400,"message":"供应商类型不能为空","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 写入仍要求 `FINANCE` 或 `SUPER_ADMIN` 及 `supplier:update` 权限。
|
||||
- 保存前按“请求值 + 已存值”校验完整聚合;存量资料缺项时,必须补齐后才能保存其他修改。
|
||||
- 非草稿账户继续走独立账户审批;本次不改变状态、并发、审批、幂等或审计规则。
|
||||
- `expectedUpdateTime` 过期返回既有业务码 `395014`,失败不更新任何聚合数据。
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
| 场景 | 正确调用 |
|
||||
|---|---|
|
||||
| 新建 | 一次提交全部主体必填资料、非空 `types`、非空 `contacts` 和一个 `initialAccounts` |
|
||||
| 修改普通字段 | 先查询详情;已存聚合完整时,只提交变化字段和最新 `expectedUpdateTime` |
|
||||
| 修改关系快照 | 提交完整非空快照;不修改关系时省略对应字段 |
|
||||
| 字典字段 | 提交字典接口返回的 `dictValue`,不要提交中文标签或前端硬编码默认值 |
|
||||
|
||||
支付类型当前字典值为 `1`(现付)、`2`(签单)、`3`(月付);供应商类型和联系人角色分别从 `supplier_type`、`sup_content_role` 动态字典读取。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 校验失败不新增或更新供应商聚合数据。
|
||||
- 本次没有表结构、字段非空约束、数据迁移或历史数据回填。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 业务失败可能仍为 HTTP 200;前端必须检查 `success=false` 和业务 `code/message`。
|
||||
- 必填文本为空白、必填值为 `null`、必填集合为空时均拒绝。
|
||||
- 更新省略字段表示保留,不等于清空;完整性按保存后的有效聚合判断。
|
||||
- 超过列表上限、字典值失效、身份证/电话/URL 格式不合法时继续使用既有校验错误。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 新建缺主体资料 | 部分字段可缺失并保存草稿 | 任一约定主体必填资料缺失即拒绝 |
|
||||
| 新建缺关系资料 | 类型、联系人或账户可能为空 | 三类均必须非空 |
|
||||
| 修改显式空关系快照 | 可清空部分关系 | `types: []`、`contacts: []`、`initialAccounts: []` 均拒绝 |
|
||||
| 修改省略未变化字段 | 保留现值 | 仍保留现值,并校验最终聚合完整性 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 是。依赖不完整草稿或显式清空类型、联系人、初始账户的旧请求会被拒绝。
|
||||
- **前端是否必须同步上线**: 是。新建和编辑表单均需补齐必填标识、校验提示和提交数据。
|
||||
- **前端 workaround 清理点**: 不再允许以空数组清空供应商类型、联系人或初始账户。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 响应结构和字段名不变。
|
||||
- 不改变供应商注册提交接口、权限矩阵、状态机、审批流、并发版本、幂等与审计语义。
|
||||
- 不改变非草稿账户独立审批入口,也不修改数据库约束。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- 新建缺注册地址、联系人或账户分别返回业务码 `400`,且无业务数据写入;完整资料创建成功。
|
||||
- 修改显式空类型、联系人、账户或清空营业执照分别返回业务码 `400`,资料与版本不变;完整存量上的增量修改成功。
|
||||
- 匿名新建返回业务码 `401`;验收草稿已通过应用删除接口清理,有效聚合数据为零。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- [后端 Issue #7087](https://git.1814.love:8443/wx/HL/issues/7087)
|
||||
- [后端 PR #7093](https://git.1814.love:8443/wx/HL/pulls/7093)
|
||||
- [历史契约 #6343](https://git.1814.love:8443/wx/HL/issues/6343):其中“新建可空类型、修改可显式清空类型”已被本工单取代。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#7087](https://git.1814.love:8443/wx/HL/issues/7087)
|
||||
- **PR**: [#7093](https://git.1814.love:8443/wx/HL/pulls/7093)
|
||||
- **Merge commit**: [e8b96e9a5](https://git.1814.love:8443/wx/HL/commit/e8b96e9a5dd0c75e65157bf889336ef740533da4)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,506 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7090"
|
||||
title: "供应商信用等级与公司资料字段"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "32a1a289"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-04"
|
||||
status_note: "后端已部署并通过 TEST;前端需在供应商新建和编辑表单接入信用等级、公开电子邮箱、公司类型和办公地址。"
|
||||
updated_at: "2026-09-04"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商模块:信用等级与公司资料字段
|
||||
|
||||
供应商新建、编辑和注册提交支持 `creditLevel`、`publicEmail`、`companyType`、`officeAddress`,详情新增后三个字段回显。公司类型使用系统字典 `supplier_company_type`。
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
- 新建省略 `creditLevel` 时后端默认值由 B 改为 A;可选值仍与供应商主列表筛选一致,为 A/B/C/D。
|
||||
- 只有 `SUPER_ADMIN` 可以显式提交 `creditLevel`。其他角色可以展示详情值,但请求体必须省略该字段。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应新增字段 | 回显公开邮箱、公司类型和办公地址 |
|
||||
| 2 | 查询供应商公司类型字典 | GET | `/admin/dict/data/supplier_company_type` | 新增字典数据 | 返回固定的两个生效选项 |
|
||||
| 3 | 新建供应商草稿 | POST | `/admin/supplier/items/add` | 请求新增字段 | 支持信用等级和三个可选公司资料字段 |
|
||||
| 4 | 修改供应商资料 | PUT | `/admin/supplier/items/{supplierId}/update` | 请求新增字段 | 支持增量维护并保留省略字段 |
|
||||
| 5 | 提交供应商注册 | POST | `/admin/supplier/items/{supplierId}/submit` | 请求新增字段 | 完整表单可携带新增字段进入审批 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
|
||||
|
||||
**VO**: `SupplierBasicInfoRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
打开供应商详情或编辑页时读取当前字段值和并发版本。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID | 供应商 ID |
|
||||
|
||||
#### 出参 `Result<SupplierBasicInfoRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.creditLevel` | String | 信用等级 A/B/C/D |
|
||||
| `data.publicEmail` | String/null | 公开电子邮箱 |
|
||||
| `data.companyType` | String/null | `supplier_company_type` 的 `dictValue` |
|
||||
| `data.officeAddress` | String/null | 办公地址 |
|
||||
| `data.updateTime` | String | 编辑请求使用的最新并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/2095000000000000001/basic-info/view
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2095000000000000001",
|
||||
"creditLevel": "A",
|
||||
"publicEmail": "service@example.com",
|
||||
"companyType": "1",
|
||||
"officeAddress": "呼和浩特市示例办公地址",
|
||||
"updateTime": "2026-09-04 20:04:19"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
存量数据或新建时未填写三个可选资料字段,分别返回 `null`;接口不使用默认文案代替空值。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395001,"message":"供应商不存在","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 沿用供应商详情查看权限;业务失败可能仍使用 HTTP 200,必须检查响应体。
|
||||
- `companyType` 返回字典值,不返回中文标签;前端用字典接口翻译。
|
||||
- `creditLevel` 可以展示给有详情权限的用户,是否可编辑按当前角色控制。
|
||||
|
||||
### 2. 查询供应商公司类型字典 `GET /admin/dict/data/supplier_company_type`
|
||||
|
||||
**VO**: `Result<List<SysDictDataRespVO>>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
供应商新建或编辑页加载公司类型下拉选项。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplier_company_type` | Path | String | 是 | 固定字典类型 | 不要改成中文名称 |
|
||||
|
||||
#### 出参 `Result<List<SysDictDataRespVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data[].dictValue` | String | 提交给供应商接口的值 |
|
||||
| `data[].dictLabel` | String | 下拉展示文案 |
|
||||
| `data[].sortOrder` | Number | 升序展示顺序 |
|
||||
| `data[].status` | String | 当前均为 `ACTIVE` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/dict/data/supplier_company_type
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": [
|
||||
{"dictValue":"1","dictLabel":"有限责任公司(自然人投资或控股)","sortOrder":10,"status":"ACTIVE"},
|
||||
{"dictValue":"2","dictLabel":"国企控股","sortOrder":20,"status":"ACTIVE"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
后端已初始化两个选项;若请求失败或返回空数组,表单不要用本地硬编码选项替代。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":401,"message":"未登录或登录已过期","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 下拉框展示 `dictLabel`,请求只提交对应的 `dictValue`。
|
||||
- 当前合法值严格为 `1`、`2`;不要提交中文标签。
|
||||
- 字典接口需要管理端真实登录态。
|
||||
|
||||
### 3. 新建供应商草稿 `POST /admin/supplier/items/add`
|
||||
|
||||
**VO**: `SupplierDraftSaveReqVO → SupplierWriteRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理端供应商新建表单保存完整草稿。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `creditLevel` | Body | String | 否 | A/B/C/D;仅 `SUPER_ADMIN` 可显式提交 | 省略时后端默认 A |
|
||||
| `publicEmail` | Body | String | 否 | 邮箱格式,最长 100 | 公开电子邮箱 |
|
||||
| `companyType` | Body | String | 否 | 生效的 `supplier_company_type` 值 | 公司类型 |
|
||||
| `officeAddress` | Body | String | 否 | 最长 500 | 录入方式与 `address` 一致 |
|
||||
| `fullName`、`taxNo` | Body | String | 是 | 沿用现有主体校验 | 供应商名称与主体证件号 |
|
||||
| `types` | Body | Array | 是 | 至少 1 项 | 供应商类型完整集合 |
|
||||
| 法人资料与 `contactPhone` | Body | String | 是 | 沿用现有格式校验 | 法人姓名、身份证三字段和联系电话 |
|
||||
| `establishDate`、`address` | Body | String | 是 | 日期不得晚于当天;地址非空 | 成立日期和注册地址 |
|
||||
| `balance`、`paymentType`、`mainCooperation` | Body | 混合 | 是 | 沿用现有规则 | 结算与合作资料 |
|
||||
| `licenseImageUrl` | Body | String | 是 | 非空 | 营业执照影像 |
|
||||
| `contacts`、`initialAccounts` | Body | Array | 是 | 至少一名联系人;当前一个初始账户 | 聚合子项 |
|
||||
|
||||
#### 出参 `Result<SupplierWriteRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.supplierId` | String | 新供应商 ID |
|
||||
| `data.status` | String | 新建成功为 `DRAFT` |
|
||||
| `data.updateTime` | String | 后续编辑使用的并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "示例供应商有限公司",
|
||||
"taxNo": "91350211M000100Y46",
|
||||
"types": [{"typeCode":"HOTEL"}],
|
||||
"legalRepresentative": "张三",
|
||||
"legalRepresentativeIdNo": "11010519491231002X",
|
||||
"legalRepresentativeIdCardFrontUrl": "https://example.com/id-front.jpg",
|
||||
"legalRepresentativeIdCardBackUrl": "https://example.com/id-back.jpg",
|
||||
"contactPhone": "13800138000",
|
||||
"establishDate": "2020-01-02",
|
||||
"address": "呼和浩特市示例注册地址",
|
||||
"creditLevel": "A",
|
||||
"publicEmail": "service@example.com",
|
||||
"companyType": "1",
|
||||
"officeAddress": "呼和浩特市示例办公地址",
|
||||
"balance": 0,
|
||||
"paymentType": "1",
|
||||
"mainCooperation": "酒店资源合作",
|
||||
"licenseImageUrl": "https://example.com/license.jpg",
|
||||
"contacts": [{"contactName":"李四","contactPhone":"13800138001","contactRole":"contentBus","isPrimary":true}],
|
||||
"initialAccounts": [{"accountType":"CORPORATE","bankName":"示例银行","accountNo":"6222000012345678"}]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code":200,"message":"成功","success":true,
|
||||
"data":{"supplierId":"2095000000000000001","status":"DRAFT","updateTime":"2026-09-04 20:04:19"}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
省略 `publicEmail`、`companyType`、`officeAddress` 时保存为未填写,详情返回 `null`;无降级成功响应。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395002,"message":"无权执行该供应商写操作","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `creditLevel` 省略时后端固定默认 A;不要再按旧行为假设默认 B。
|
||||
- 只有 `SUPER_ADMIN` 可显式提交 `creditLevel`;其他可创建供应商的角色必须省略该字段。
|
||||
- 三个公司资料字段均非必填;显式 `companyType` 必须命中当前生效字典。
|
||||
- 非法邮箱、信用等级、公司类型或超过 500 字的办公地址返回业务码 `400`,失败不创建草稿。
|
||||
|
||||
### 4. 修改供应商资料 `PUT /admin/supplier/items/{supplierId}/update`
|
||||
|
||||
**VO**: `SupplierUpdateReqVO → SupplierWriteRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
编辑页按详情中的最新版本增量保存供应商资料。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID | 目标供应商 |
|
||||
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 取详情最新 `updateTime` |
|
||||
| `creditLevel` | Body | String | 否 | A/B/C/D;仅 `SUPER_ADMIN` 可显式提交 | 省略保留现值 |
|
||||
| `publicEmail` | Body | String | 否 | 邮箱格式,最长 100 | 省略保留现值 |
|
||||
| `companyType` | Body | String | 否 | 生效的字典值 1/2 | 省略保留现值 |
|
||||
| `officeAddress` | Body | String | 否 | 最长 500 | 省略保留现值 |
|
||||
|
||||
#### 出参 `Result<SupplierWriteRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.supplierId` | String | 供应商 ID |
|
||||
| `data.status` | String | 保存后的状态 |
|
||||
| `data.updateTime` | String | 保存后的新并发版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"creditLevel": "B",
|
||||
"publicEmail": "service@example.com",
|
||||
"companyType": "2",
|
||||
"officeAddress": "呼和浩特市示例办公地址",
|
||||
"expectedUpdateTime": "2026-09-04 20:04:19"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code":200,"message":"成功","success":true,
|
||||
"data":{"supplierId":"2095000000000000001","status":"DRAFT","updateTime":"2026-09-04 20:05:01"}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
省略新增字段表示保留现值;接口没有空数据成功或降级成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395014,"message":"数据已被他人修改,请刷新后重试","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 保存前先读取详情并使用最新 `updateTime`;过期版本不写入任何字段。
|
||||
- 只有 `SUPER_ADMIN` 可显式提交 `creditLevel`。其他角色即使值未改变也必须省略,否则返回 `395002`。
|
||||
- 省略 `creditLevel` 不会重置为 A;A 只用于新建时的缺省值。
|
||||
- `companyType` 必须提交字典值;邮箱、公司类型和办公地址的失败校验均不产生部分更新。
|
||||
|
||||
### 5. 提交供应商注册 `POST /admin/supplier/items/{supplierId}/submit`
|
||||
|
||||
**VO**: `SupplierSubmitReqVO → SupplierApprovalCommandRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
供应商完整注册表单保存并提交审批。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID | 草稿供应商 |
|
||||
| `expectedUpdateTime` | Body | String | 是 | 最新并发版本 | 防止覆盖并发编辑 |
|
||||
| `creditLevel` | Body | String | 否 | A/B/C/D;仅 `SUPER_ADMIN` 可显式提交 | 省略保留草稿值 |
|
||||
| `publicEmail` | Body | String | 否 | 邮箱格式,最长 100 | 省略保留草稿值 |
|
||||
| `companyType` | Body | String | 否 | 生效的字典值 1/2 | 省略保留草稿值 |
|
||||
| `officeAddress` | Body | String | 否 | 最长 500 | 省略保留草稿值 |
|
||||
| 完整注册资料 | Body | 混合 | 是 | 沿用现有提交契约 | 主体、类型、联系人、账户及营业执照等 |
|
||||
|
||||
#### 出参 `Result<SupplierApprovalCommandRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.approvalLogId` | String | 审批记录 ID |
|
||||
| `data.provider` | String | 审批提供方 |
|
||||
| `data.approvalStatus` | String | 审批状态 |
|
||||
| `data.submittedAt` | String | 提交时间 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "示例供应商有限公司",
|
||||
"taxNo": "91350211M000100Y46",
|
||||
"types": [{"typeCode":"HOTEL"}],
|
||||
"legalRepresentative": "张三",
|
||||
"legalRepresentativeIdNo": "11010519491231002X",
|
||||
"legalRepresentativeIdCardFrontUrl": "https://example.com/id-front.jpg",
|
||||
"legalRepresentativeIdCardBackUrl": "https://example.com/id-back.jpg",
|
||||
"contactPhone": "13800138000",
|
||||
"establishDate": "2020-01-02",
|
||||
"address": "呼和浩特市示例注册地址",
|
||||
"creditLevel": "A",
|
||||
"publicEmail": "service@example.com",
|
||||
"companyType": "1",
|
||||
"officeAddress": "呼和浩特市示例办公地址",
|
||||
"balance": 0,
|
||||
"paymentType": "1",
|
||||
"mainCooperation": "酒店资源合作",
|
||||
"licenseImageUrl": "https://example.com/license.jpg",
|
||||
"contacts": [{"contactName":"李四","contactPhone":"13800138001","contactRole":"contentBus","isPrimary":true}],
|
||||
"initialAccounts": [{"accountType":"CORPORATE","bankName":"示例银行","accountNo":"6222000012345678"}],
|
||||
"expectedUpdateTime": "2026-09-04 20:05:01"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code":200,"message":"成功","success":true,
|
||||
"data":{"approvalLogId":"2095000000000000010","provider":"WECOM","approvalStatus":"PENDING","submittedAt":"2026-09-04 20:05:02"}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口成功时返回审批受理结果;失败时 `data` 为 `null`,不会以空对象表示成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395002,"message":"无权执行该供应商写操作","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 新增可选字段省略时沿用既有“保留草稿值”语义,不会清空已保存资料。
|
||||
- 显式 `creditLevel` 仍只允许 `SUPER_ADMIN`;其他提交角色必须省略该字段。
|
||||
- 公司类型按提交时生效字典校验;校验失败不会进入审批。
|
||||
- 审批、幂等、重复主体确认和状态规则保持不变。
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
| 场景 | 正确调用 |
|
||||
|---|---|
|
||||
| 新建页面 | 从主列表相同选项展示 A/B/C/D;初始选中 A |
|
||||
| 非超级管理员 | 可展示信用等级,但新建、编辑、提交请求均省略 `creditLevel` |
|
||||
| 公司类型 | 调字典接口,用 `dictLabel` 展示、用 `dictValue` 提交 |
|
||||
| 编辑保存 | 携带详情最新 `updateTime`;只提交实际允许修改的字段 |
|
||||
| 注册提交 | 三个公司资料字段省略时保留草稿值,不需重复拼接空值 |
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
| 前端提交 | 外部可观察结果 |
|
||||
|---|---|
|
||||
| 新建省略 `creditLevel` | 详情返回 `creditLevel: "A"` |
|
||||
| 编辑或提交省略 `creditLevel` | 保留已有等级 |
|
||||
| 新建省略三个公司资料字段 | 详情对应字段返回 `null` |
|
||||
| 填写三个公司资料字段 | 保存成功后详情原值回显 |
|
||||
| 校验或权限失败 | 不产生部分字段写入 |
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 业务失败可能仍为 HTTP 200,必须同时检查 `code`、`success` 和 `message`。
|
||||
- `creditLevel` 只接受大写 A/B/C/D;非法值返回业务码 `400`。
|
||||
- `publicEmail` 最长 100,`officeAddress` 最长 500;超限或邮箱格式非法返回业务码 `400`。
|
||||
- `companyType` 只接受当前生效字典值;字典不可用或值失效时失败关闭。
|
||||
- 存量供应商三个新增资料字段没有自动推断或回填,详情可返回 `null`。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### creditLevel
|
||||
|
||||
**所属字段**: `SupplierDraftSaveReqVO.creditLevel / SupplierUpdateReqVO.creditLevel / SupplierBasicInfoRespVO.creditLevel` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `A` | A 级 | 新建省略时的默认值 |
|
||||
| `B` | B 级 | 与主列表筛选共用 |
|
||||
| `C` | C 级 | 与主列表筛选共用 |
|
||||
| `D` | D 级 | 与主列表筛选共用 |
|
||||
|
||||
### companyType(supplier_company_type)
|
||||
|
||||
**所属字段**: `SupplierDraftSaveReqVO.companyType / SupplierUpdateReqVO.companyType / SupplierBasicInfoRespVO.companyType` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `1` | 有限责任公司(自然人投资或控股) | 生效字典值 |
|
||||
| `2` | 国企控股 | 生效字典值 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| `creditLevel` 请求 | 新建、编辑不能维护 | 新建、编辑、提交可选;仅 `SUPER_ADMIN` 可显式提交 |
|
||||
| `publicEmail` | 无请求或详情字段 | 可选保存并在详情回显 |
|
||||
| `companyType` | 无请求或详情字段 | 可选保存并按系统字典回显 |
|
||||
| `officeAddress` | 无请求或详情字段 | 可选保存并在详情回显 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 新建省略信用等级 | 默认 B | 默认 A |
|
||||
| 编辑省略新增字段 | 无对应字段 | 保留已有值 |
|
||||
| 公司类型选项 | 无集中契约 | 从 `supplier_company_type` 动态读取 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否。旧客户端省略新增字段仍可调用;新建信用缺省业务值由 B 调整为 A。
|
||||
- **前端是否必须同步上线**: 是。新建和编辑页需展示四个字段并接入公司类型字典。
|
||||
- **前端 workaround 清理点**: 不要硬编码公司类型;不要再把新建缺省信用等级当作 B;非 `SUPER_ADMIN` 不要序列化 `creditLevel`。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台供应商新建、编辑、详情和注册提交表单。
|
||||
- **零影响**: 供应商主列表信用等级筛选选项、生命周期状态机、审批流程、账户流程和其他前端入口。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- 公司类型字典真实返回且仅返回两个约定的生效选项。
|
||||
- 真实新建、编辑和详情调用已逐项保存并回读 A/B/C/D,以及公开邮箱、公司类型 1/2、办公地址。
|
||||
- 真实新建省略四个字段后,信用等级回读为 A,三个可选公司资料字段回读为 `null`。
|
||||
- 编辑省略 `creditLevel` 后保持既有等级;两条验收草稿均已通过业务删除接口清理。
|
||||
- 注册提交沿用同一请求字段、权限和保存逻辑,已通过合并提交的后端契约测试;TEST 未创建外部审批单。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#7090](https://git.1814.love:8443/wx/HL/issues/7090)
|
||||
- 关联 PR: [wx/HL#7107](https://git.1814.love:8443/wx/HL/pulls/7107)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#7090](https://git.1814.love:8443/wx/HL/issues/7090)
|
||||
- **PR**: [#7107](https://git.1814.love:8443/wx/HL/pulls/7107)
|
||||
- **Merge commit**: [c1102d62ecb81c24596fba91bba6e704da6e85ff](https://git.1814.love:8443/wx/HL/commit/c1102d62ecb81c24596fba91bba6e704da6e85ff)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,284 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7094"
|
||||
title: "供应商账户驳回终态"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "2e8ed57c"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-04"
|
||||
status_note: "后端已部署并通过 TEST;前端需将 REJECTED 映射为已驳回且不可用。"
|
||||
updated_at: "2026-09-04"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商模块:账户驳回返回明确不可用终态
|
||||
|
||||
企微驳回后,后续新增账户由 `PENDING` 改为 `REJECTED`;只有 `ACTIVE` 账户可用于收款或设为默认。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 账户列表 | GET | `/admin/supplier/items/{supplierId}/account-info/list` | 响应枚举扩展 | `bankAccounts[].status` 新增 `REJECTED` |
|
||||
| 2 | 账户详情 | GET | `/admin/supplier/bank-accounts/{accountId}/view` | 响应枚举扩展 | `status` 新增 `REJECTED` |
|
||||
| 3 | 新增账户 | POST | `/admin/supplier/items/{supplierId}/bank-accounts/add` | 重放结果修正 | 已驳回请求重放返回 `accountStatus=REJECTED` |
|
||||
| 4 | 审批变更记录 | GET | `/admin/supplier/items/{supplierId}/approval-records/page` | 查询枚举扩展 | 账户记录支持按 `REJECTED` 筛选并显示“已驳回” |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 账户列表 `GET /admin/supplier/items/{supplierId}/account-info/list`
|
||||
|
||||
**VO**: `SupplierAccountInfoRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
账户管理弹窗刷新企微审批后的账户状态。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 | 供应商 ID |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.bankAccounts[].status` | String | 新增 `REJECTED`,表示已驳回且不可用 |
|
||||
| `data.bankAccounts[].isDefault` | String | 驳回账户为 `NO` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/2095000000000000001/account-info/list
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","success":true,"data":{"supplierId":"2095000000000000001","bankAccounts":[{"accountId":"2095000000000000002","status":"REJECTED","isDefault":"NO"}]}}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
供应商没有可见账户时返回 `bankAccounts=[]`;接口不提供降级成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395001,"message":"供应商不存在","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 前端只有在 `status=ACTIVE` 时才允许选择账户或显示默认操作。
|
||||
|
||||
### 2. 账户详情 `GET /admin/supplier/bank-accounts/{accountId}/view`
|
||||
|
||||
**VO**: `SupplierBankAccountDetailRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
打开单个账户详情时展示当前终态。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `accountId` | Path | String | 是 | 正整数 | 账户 ID |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.status` | String | 新增 `REJECTED`,表示已驳回且不可用 |
|
||||
| `data.isDefault` | String | 驳回账户为 `NO` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/bank-accounts/2095000000000000002/view
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","success":true,"data":{"accountId":"2095000000000000002","status":"REJECTED","isDefault":"NO"}}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
账户不存在或不可见时返回业务失败,不返回空成功详情。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395001,"message":"供应商不存在","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `REJECTED` 只读可见,但不能设为默认账户。
|
||||
|
||||
### 3. 新增账户 `POST /admin/supplier/items/{supplierId}/bank-accounts/add`
|
||||
|
||||
**VO**: `SupplierBankAccountBatchCreateReqVO → List<BankAccountSubmitResultRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
新增账户提交企微审批,或以同一请求重放已完成结果。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 | 已生效供应商 ID |
|
||||
| `accounts` | Body | Array | 是 | 1~50 项 | 请求结构不变 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data[].approvalStatus` | String | 已驳回终态为 `REJECTED` |
|
||||
| `data[].accountStatus` | String | 已驳回终态由 `PENDING` 修正为 `REJECTED` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"accounts":[{"accountType":"CORPORATE","bankName":"示例银行","accountNo":"6222000012345678"}]}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","success":true,"data":[{"accountId":"2095000000000000002","approvalStatus":"REJECTED","accountStatus":"REJECTED","isDefault":"NO"}]}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口不返回空成功列表;新提交在企微完成前仍返回 `PENDING`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395010,"message":"请先完成供应商注册","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 仅已完成驳回的请求返回 `REJECTED`;企微撤销继续沿用既有 `PENDING` 语义。
|
||||
|
||||
### 4. 审批变更记录 `GET /admin/supplier/items/{supplierId}/approval-records/page`
|
||||
|
||||
**VO**: `SupplierApprovalRecordPageReqVO → PageResult<SupplierApprovalRecordRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
查询账户审批关联的状态变更记录。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 | 供应商 ID |
|
||||
| `targetType` | Query | String | 否 | 查询账户时传 `ACCOUNT` | 目标类型 |
|
||||
| `status` | Query | String | 否 | 新增 `REJECTED` | 变更后状态 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.records[].status` | String | 驳回记录为 `REJECTED` |
|
||||
| `data.records[].statusName` | String | 驳回记录为“已驳回” |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/2095000000000000001/approval-records/page?targetType=ACCOUNT&status=REJECTED&page=1&pageSize=20
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","success":true,"data":{"records":[{"targetType":"ACCOUNT","status":"REJECTED","statusName":"已驳回"}],"total":1}}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无匹配记录时返回 `records=[]`;接口不提供降级成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":400,"message":"变更目标状态不合法","data":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `status=REJECTED` 配合 `targetType=ACCOUNT` 使用,不作为供应商主体生命周期筛选值。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
1. 企微处理完成后刷新账户列表,以 `status` 判断账户是否可用。
|
||||
2. 将 `REJECTED` 映射为“已驳回”,按不可用样式展示;只有 `ACTIVE` 可设为默认。
|
||||
3. 必须检查统一响应的 `code` 和 `success`,不能只判断 HTTP 状态。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
企微驳回完成后,账户最终可观察状态为 `REJECTED`;历史同类异常会同步收敛。将 `REJECTED` 账户设为默认会失败且不改变账户或原默认账户。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- `PENDING`、`REJECTED`、`DISABLED` 均不可用,`ACTIVE` 才可用于收款。
|
||||
- 企微撤销仍保留 `PENDING`;本次未新增错误码,也未改变请求字段。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 场景 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 企微驳回 | 账户仍返回 `PENDING` | 账户返回 `REJECTED` |
|
||||
| 已驳回请求重放 | `accountStatus=PENDING` | `accountStatus=REJECTED` |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 响应枚举扩展;前端需增加未知值处理。
|
||||
- **前端是否必须同步上线**: 是。
|
||||
- **前端 workaround 清理点**: 删除把企微驳回账户继续显示为“待审批”的兜底逻辑。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 不改变供应商注册随单账户的驳回退草稿语义。
|
||||
- 不改变企微撤销、审批通过、默认账户或停用流程。
|
||||
- 不影响小程序接口;没有新增错误码。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- 列表和详情均返回业务成功,目标账户为 `status=REJECTED`、`isDefault=NO`。
|
||||
- 将该账户设为默认返回 `code=395005`、`success=false`,账户与原默认账户均未变化。
|
||||
- 企微已驳回且仍为 `PENDING` 的精确异常计数为 0。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- [Issue #7094](https://git.1814.love:8443/wx/HL/issues/7094)
|
||||
- [补充 Issue #7119](https://git.1814.love:8443/wx/HL/issues/7119)
|
||||
- [PR #7118](https://git.1814.love:8443/wx/HL/pulls/7118)
|
||||
- [补充 PR #7121](https://git.1814.love:8443/wx/HL/pulls/7121)
|
||||
|
||||
## 前端动作与当前状态
|
||||
|
||||
- 将 `REJECTED` 显示为“已驳回”且不可用,只为 `ACTIVE` 显示默认操作。
|
||||
- **当前状态:待前端处理。**
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **部署提交**: [f861074a84b50eb83fd67729b77f8208747a2095](https://git.1814.love:8443/wx/HL/commit/f861074a84b50eb83fd67729b77f8208747a2095)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,746 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7100"
|
||||
title: "团期退单户:提交退单申请 + 管理员审批(070 破坏性变更 + 072~075 新增)"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "4bc8dd4d"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-05"
|
||||
status_note: "已过网关真测(2026-09-05,admin 切 ADMIN 角色,5 端点全链路含提交/驳回/再提交/通过);含 #7126 预估口径修复后的复测"
|
||||
updated_at: "2026-09-05"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期: 退单户改为「提交申请 + 管理员审批」
|
||||
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #7106
|
||||
> **Issue**: #7100
|
||||
> **日期**: 2026-09-04
|
||||
> **影响范围**: 管理后台团期详情「退单户」弹窗 + 新增退单审批中心
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**`POST .../sub-order/{orderId}/withdraw` 是破坏性变更:调用它不再退款,只建一张待审申请单。**
|
||||
|
||||
三条前端必读:
|
||||
|
||||
1. **响应体由 `Result<Void>` 变成对象**。老前端只看 `code` 不读 `data` 的话不会崩,但**提示语必须改**——
|
||||
现在的语义是「已提交审核」,不是「已退款」。
|
||||
2. **金额要显示两个数**:`paidAmount`(已付)和 `estimatedRefundAmount`(预计退)。
|
||||
**两者可以不相等,而且早期阶段常常不等**——招募中 / 资源准备中退的是**订金应付额**,
|
||||
与已付多少无关(TEST 实测:已付 ¥0、订金 ¥2000 的单,预计退与实退都是 ¥2000)。
|
||||
**不要自己调 `/v3/admin/order/{id}/cancel-preview` 算**——那个接口恒按退改政策算,
|
||||
与早期阶段的订金口径对不上。
|
||||
3. **审批中心是新界面**:列表 → 详情 → 通过 / 取消退单(4 个新端点),入口按角色显隐,
|
||||
仅 `ADMIN` / `SUPER_ADMIN` 可见,其余角色调用返 589530。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
原来的退单户是「点一下就退款」:提交那一刻订单即 `CANCELLED`、团期名额当场释放、退款单自动建,
|
||||
招募中/资源准备中甚至免审直退。但业务要的是**可以取消退单,且取消后该户继续留在团期里走原流程**——
|
||||
订单都取消了、名额可能已被新客户占走,这事根本做不到。
|
||||
|
||||
故改为**先挂申请单、批了才执行**:`PENDING` 期间订单 / 名额 / 钱三项零变动,
|
||||
该户照常提需求、排房排车;管理员通过才真正退团,驳回则全程无痕、可再次提交。
|
||||
|
||||
退款口径**没有变**:仍按团期状态选模式(招募中 / 资源准备中全退定金,物资准备中 / 待出发按退改政策阶梯扣)。
|
||||
变的只是「什么时候执行」和「谁点头」。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 退单户·提交退单申请 | POST | `/v3/admin/order/group-batch/:groupBatchId/sub-order/:orderId/withdraw` | 修改 | **破坏性**:由「直接退款」改为「建待审申请单」,响应体 Void → 对象 |
|
||||
| 2 | 退单审批分页列表 | GET | `/v3/admin/order/group-batch/withdraw/page` | 新增 | 审批中心列表,缺省只返待审 |
|
||||
| 3 | 退单申请详情 | GET | `/v3/admin/order/group-batch/withdraw/:approvalId` | 新增 | 含「提交时预估」与「当前预估」两个金额 |
|
||||
| 4 | 退单审核通过 | POST | `/v3/admin/order/group-batch/withdraw/:approvalId/approve` | 新增 | 此刻才执行退团:订单取消 + 名额回落 + 退款 |
|
||||
| 5 | 取消退单(驳回) | POST | `/v3/admin/order/group-batch/withdraw/:approvalId/reject` | 新增 | 该户继续留在团期中走原流程 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 退单户·提交退单申请 `POST /v3/admin/order/group-batch/:groupBatchId/sub-order/:orderId/withdraw`
|
||||
|
||||
**VO**: `WithdrawSubOrderReqVO` / `WithdrawSubOrderRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情底部操作条点「退单户」→ 弹窗选一户(下拉数据仍用既有的
|
||||
`GET /v3/admin/order/group-batch/:groupBatchId/orders`,后端没有另开候选接口)→ 填退团原因 →
|
||||
点「提交退团审核」时调用。**调完钱不动**,等管理员在审批中心处理。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | - | 团期主订单 ID |
|
||||
| orderId | Path | Long | ✅ | - | 要退的子订单 ID(= 一户) |
|
||||
| reason | Body | String | ❌ | ≤512 字 | 退团原因;不传后端记「团期退团」。整个 body 可省略 |
|
||||
|
||||
#### 出参 `Result<WithdrawSubOrderRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| approvalId | String | 退单审批单 ID(雪花,序列化为字符串),审批中心用它取详情 |
|
||||
| paidAmount | BigDecimal | 该户已付款额,弹窗「已付 ¥5,000」取此列 |
|
||||
| estimatedRefundAmount | BigDecimal | 预计退款额,弹窗「预计退 ¥3,500」取此列。`FULL_DEPOSIT` 时 = **订金应付额**(与已付无关),`POLICY` 时 = 已付额按政策扣减后;**实退以审批通过时重算为准** |
|
||||
| refundMode | String | `FULL_DEPOSIT`=全退定金 / `POLICY`=按退改政策阶梯扣 |
|
||||
| refundPolicy | Object | 退改政策明细,仅 `POLICY` 模式给(用于展示「距出发 6 天,扣 30%」),否则为 null |
|
||||
| consultantName | String | 该户定制师姓名。**信息项,不是审批人**,前端不得暗示他要来点头 |
|
||||
| warnings | String[] | 非阻断提示(如当前已满团,退单通过后将空出 1 户);无则为空数组 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{ "reason": "客户临时有事无法参团" }
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"approvalId": "1955009900112233",
|
||||
"paidAmount": 5000.00,
|
||||
"estimatedRefundAmount": 3500.00,
|
||||
"refundMode": "POLICY",
|
||||
"refundPolicy": { "policyName": "标准退改政策", "policyId": 12 },
|
||||
"consultantName": "李雯",
|
||||
"warnings": []
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写操作,无空数据形态。`refundPolicy` 在 `FULL_DEPOSIT` 阶段恒为 null(退订金不看政策),
|
||||
此时 `estimatedRefundAmount` 是**订金应付额**,与 `paidAmount` 无关,两者不等属正常:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"approvalId": "1955009900112234",
|
||||
"paidAmount": 5000.00,
|
||||
"estimatedRefundAmount": 2000.00,
|
||||
"refundMode": "FULL_DEPOSIT",
|
||||
"refundPolicy": null,
|
||||
"warnings": []
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589529,
|
||||
"message": "该户已有退单审核在途,请勿重复提交",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
四类拒绝:
|
||||
|
||||
| code | 含义 |
|
||||
|------|------|
|
||||
| 589500 | 团期不存在 |
|
||||
| 589501 | 团期状态不允许退团(出行中及以后,走售后退款) |
|
||||
| 589512 | 子订单不属于该团期 |
|
||||
| 589529 | 该户已有在途退单申请 |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **零副作用**:本接口只 INSERT 一行审批单。订单状态、团期已报名人数/户数、退款单**三项分文未动**。
|
||||
- **未付分文也能提交**,但**不等于退 0**:
|
||||
- 招募中 / 资源准备中(`FULL_DEPOSIT`)退的是**订金应付额**,未付款的单照样会退订金并生成退款单——
|
||||
这是订单侧既有口径(D3 决策),不是本次引入;如果这不符合业务预期,需要在订单侧另开工单讨论。
|
||||
- 物资准备中 / 待出发(`POLICY`)以已付额为基数,未付则退 0、**不生成退款单**。
|
||||
- 订金为 0 时 `warnings` 会提示「该户订金为 0…」——这类单提交得进去但**审批会被订单侧拒绝**。
|
||||
- **出行后拒**:出行中 / 核算中 / 已结算 / 已取消一律 589501,且**在建单之前就拒**,不会留下批不掉的单。
|
||||
- **幂等**:同一 `groupBatchId + orderId` 5 秒窗口内重复提交只成功一次;顺序重复由 589529 兜底。
|
||||
- **金额会漂移**:`POLICY` 模式下「距出发天数」每天在变,本接口返回的是**提交时**的预估,仅供展示。
|
||||
- **兼容**:body 整体可省略;老前端不读 `data` 也不会报错,但提示文案必须改。
|
||||
|
||||
---
|
||||
|
||||
### 2. 退单审批分页列表 `GET /v3/admin/order/group-batch/withdraw/page`
|
||||
|
||||
**VO**: `WithdrawApprovalListReqVO` / `WithdrawApprovalItemRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
退单审批中心的列表页。缺省只返待审单,管理员进来就是「待我处理」的视图。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| approvalStatus | Query | String | ❌ | PENDING/APPROVED/REJECTED/ALL | 缺省 `PENDING`;查全部传 `ALL` |
|
||||
| groupBatchId | Query | Long | ❌ | - | 按团期筛选 |
|
||||
| keyword | Query | String | ❌ | - | 客户姓名 / 订单号模糊匹配(两者取并集) |
|
||||
| createdFrom | Query | String | ❌ | yyyy-MM-dd | 提交时间起;格式非法则忽略该条件 |
|
||||
| createdTo | Query | String | ❌ | yyyy-MM-dd | 提交时间止,含当日 |
|
||||
| pageNo | Query | Integer | ❌ | ≥1,默认 1 | 页码 |
|
||||
| pageSize | Query | Integer | ❌ | ≤100,默认 20 | 每页条数,超 100 截断 |
|
||||
|
||||
#### 出参 `Result<PageResult<WithdrawApprovalItemRespVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| approvalId | String | 退单审批单 ID |
|
||||
| groupBatchId | String | 团期 ID |
|
||||
| batchNo | String | 团期号 |
|
||||
| productName | String | 产品名称 |
|
||||
| departDate | String | 出发日期 yyyy-MM-dd |
|
||||
| orderId | String | 子订单 ID |
|
||||
| orderNo | String | 订单号 |
|
||||
| customerName | String | 客户姓名 |
|
||||
| participantCount | Integer | 该户人数(成人+儿童+小童+婴儿) |
|
||||
| paidAmount | BigDecimal | 已付款额(提交时快照) |
|
||||
| estimatedRefundAmount | BigDecimal | 预计退款额(提交时快照) |
|
||||
| refundMode | String | `FULL_DEPOSIT` / `POLICY` |
|
||||
| reason | String | 退团原因 |
|
||||
| applicantName | String | 提交人姓名 |
|
||||
| approvalStatus | String | `PENDING` / `APPROVED` / `REJECTED` |
|
||||
| createdAt | DateTime | 提交时间 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/withdraw/page?approvalStatus=PENDING&pageNo=1&pageSize=20
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"approvalId": "1955009900112233",
|
||||
"batchNo": "GT-26-0007",
|
||||
"productName": "小蒙马亲子团",
|
||||
"departDate": "2026-10-01",
|
||||
"orderNo": "GT-26-0099",
|
||||
"customerName": "林婉清",
|
||||
"participantCount": 2,
|
||||
"paidAmount": 5000.00,
|
||||
"estimatedRefundAmount": 3500.00,
|
||||
"refundMode": "POLICY",
|
||||
"reason": "临时有事无法参团",
|
||||
"applicantName": "王磊",
|
||||
"approvalStatus": "PENDING",
|
||||
"createdAt": "2026-09-04 17:20:11"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无待审单,或 keyword 未命中任何客户/订单号时返回空页(**不是 null**):
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "records": [], "total": 0, "page": 1, "pageSize": 20 }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589530,
|
||||
"message": "仅管理员可处理退单审核",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
| code | 含义 |
|
||||
|------|------|
|
||||
| 589530 | 当前登录人不是管理员角色 |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **鉴权**:仅 `ADMIN` / `SUPER_ADMIN` 放行;定制师 / 财务 / 房务 / 车务一律 589530。前端入口应按角色显隐,不要靠调用失败来判断。
|
||||
- **排序**:按提交时间倒序。
|
||||
- **性能**:整页的团期与子订单各批量取一次,不随行数放大查询。
|
||||
- **日期容错**:`createdFrom` / `createdTo` 格式非法时**忽略该条件**并继续查询,不报错。
|
||||
|
||||
---
|
||||
|
||||
### 3. 退单申请详情 `GET /v3/admin/order/group-batch/withdraw/:approvalId`
|
||||
|
||||
**VO**: `WithdrawApprovalDetailRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
审批中心点进某一单。管理员在这里看清楚「退谁、退多少、现在退多少」再决定通过还是取消。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| approvalId | Path | Long | ✅ | - | 退单审批单 ID |
|
||||
|
||||
#### 出参 `Result<WithdrawApprovalDetailRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| (列表行全部字段) | - | 见接口 2 的出参表 |
|
||||
| currentEstimatedRefundAmount | BigDecimal | **当前**预计退款额(实时重算)。审批通过时按它执行 |
|
||||
| currentRefundMode | String | 当前退款模式;团期状态推进后可能与提交时不同 |
|
||||
| refundPolicy | Object | 当前退改政策明细(`POLICY` 模式给) |
|
||||
| actualRefundAmount | BigDecimal | 实际退款额,审批通过后回填;未通过为 null |
|
||||
| consultantName | String | 该户定制师(信息项,非审批人) |
|
||||
| approvedByName | String | 批复人姓名;未批复为 null |
|
||||
| approvedAt | DateTime | 批复时间;未批复为 null |
|
||||
| approveRemark | String | 批复备注 |
|
||||
| refundApplicationId | String | 通过后生成的退款申请 ID |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/withdraw/1955009900112233
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"approvalId": "1955009900112233",
|
||||
"orderNo": "GT-26-0099",
|
||||
"customerName": "林婉清",
|
||||
"participantCount": 2,
|
||||
"paidAmount": 5000.00,
|
||||
"estimatedRefundAmount": 3500.00,
|
||||
"refundMode": "POLICY",
|
||||
"currentEstimatedRefundAmount": 3200.00,
|
||||
"currentRefundMode": "POLICY",
|
||||
"consultantName": "李雯",
|
||||
"approvalStatus": "PENDING",
|
||||
"approvedByName": null,
|
||||
"approvedAt": null,
|
||||
"actualRefundAmount": null,
|
||||
"refundApplicationId": null
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
团期已推进到不可退阶段(出行中及以后)时,**当前预估取不到**,`currentEstimatedRefundAmount` /
|
||||
`currentRefundMode` 降级为 null,单子仍可查看——但点通过会被 589501 拦住:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"approvalId": "1955009900112233",
|
||||
"estimatedRefundAmount": 3500.00,
|
||||
"currentEstimatedRefundAmount": null,
|
||||
"currentRefundMode": null
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
`refundApplicationId` 目前**恒为 null**——退款单由订单取消事件异步建,取消响应里不带该 ID。
|
||||
需要跳退款单请用 `orderId` 去退款工作台查。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589531,
|
||||
"message": "退单申请不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
| code | 含义 |
|
||||
|------|------|
|
||||
| 589530 | 非管理员角色 |
|
||||
| 589531 | 申请单不存在 / 已删除 |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **两个预估都要展示**:`estimatedRefundAmount` 是提交时快照,`currentEstimatedRefundAmount` 是当前值。
|
||||
`POLICY` 下距出发天数每天在变,**实退按当前值算**,只给前者会误导审批人。
|
||||
- **鉴权**:同列表,仅管理员。
|
||||
|
||||
---
|
||||
|
||||
### 4. 退单审核通过 `POST /v3/admin/order/group-batch/withdraw/:approvalId/approve`
|
||||
|
||||
**VO**: `ApproveWithdrawReqVO` / `WithdrawApprovalDetailRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
审批中心详情页点「通过」。**这一刻才真正退团**:订单取消、团期名额回落、退款进实退链路。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| approvalId | Path | Long | ✅ | - | 退单审批单 ID |
|
||||
| remark | Body | String | ❌ | ≤512 字 | 批复备注;body 可整体省略 |
|
||||
|
||||
#### 出参 `Result<WithdrawApprovalDetailRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| (详情全部字段) | - | 见接口 3 |
|
||||
| approvalStatus | String | 固定 `APPROVED` |
|
||||
| actualRefundAmount | BigDecimal | **实际**退款额(按批复时团期状态重算,可能与提交时快照不同) |
|
||||
| approvedByName | String | 批复人(当前登录人) |
|
||||
| approvedAt | DateTime | 批复时间 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{ "remark": "已与客户确认,同意退单" }
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"approvalId": "1955009900112233",
|
||||
"approvalStatus": "APPROVED",
|
||||
"refundMode": "POLICY",
|
||||
"estimatedRefundAmount": 3500.00,
|
||||
"actualRefundAmount": 3200.00,
|
||||
"approvedByName": "刘涛",
|
||||
"approvedAt": "2026-09-04 18:02:35"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写操作,无空数据形态。团期时间线写失败会降级(记 WARN)但**不影响退团成功**。
|
||||
`paidAmount = 0` 的零元退单:订单照常取消,`actualRefundAmount` 为 0,**不生成退款单**:
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "approvalStatus": "APPROVED", "actualRefundAmount": 0.00 }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589532,
|
||||
"message": "该退单申请已处理,不可重复操作",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
| code | 含义 |
|
||||
|------|------|
|
||||
| 589501 | 团期已推进到不可退阶段(出行中及以后) |
|
||||
| 589512 | 子订单不属于该团期 |
|
||||
| 589530 | 非管理员角色 |
|
||||
| 589531 | 申请单不存在 |
|
||||
| 589532 | 申请单已是终态(已通过 / 已取消) |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **钱以批复时为准**:审批通过时**重新**按当时团期状态选模式、重算金额,**不认提交时的快照**。
|
||||
典型场景:提交时团期还在资源准备中(全退 5000),批复时已进物资准备(按政策退 3200)→ 实退 3200。
|
||||
- **一次到位**:团期侧审批完即执行,退款**不再进退款审批中心二次审**。
|
||||
- **名额回落**:1 单 = 1 户 = 1 房,通过后该团期已报名 −1 户 / −N 人。
|
||||
- **并发**:按 `approvalId` 加分布式锁 + 状态 CAS 双保险,两名管理员同时点通过只有一个成功,另一个 589532。
|
||||
- **不可撤销**:钱一旦进实退链路就撤不回来,要撤走售后退款。
|
||||
|
||||
---
|
||||
|
||||
### 5. 取消退单(驳回) `POST /v3/admin/order/group-batch/withdraw/:approvalId/reject`
|
||||
|
||||
**VO**: `RejectWithdrawReqVO` / `WithdrawApprovalDetailRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
审批中心详情页点「取消退单」。**该户继续留在团期中走原流程**,可以继续提需求、住房用车、正常出行。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| approvalId | Path | Long | ✅ | - | 退单审批单 ID |
|
||||
| remark | Body | String | ✅ | 非空,≤512 字 | 驳回原因(合规要求必填),body 不可省略 |
|
||||
|
||||
#### 出参 `Result<WithdrawApprovalDetailRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| (详情全部字段) | - | 见接口 3 |
|
||||
| approvalStatus | String | 固定 `REJECTED` |
|
||||
| approveRemark | String | 驳回原因 |
|
||||
| approvedByName | String | 操作人 |
|
||||
| approvedAt | DateTime | 操作时间 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{ "remark": "客户已改口,继续参团" }
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"approvalId": "1955009900112233",
|
||||
"approvalStatus": "REJECTED",
|
||||
"approveRemark": "客户已改口,继续参团",
|
||||
"approvedByName": "刘涛",
|
||||
"approvedAt": "2026-09-04 18:05:12"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写操作,无空数据形态。时间线写失败降级不阻断驳回。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589532,
|
||||
"message": "该退单申请已处理,不可重复操作",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
| code | 含义 |
|
||||
|------|------|
|
||||
| 589530 | 非管理员角色 |
|
||||
| 589531 | 申请单不存在 |
|
||||
| 589532 | 申请单已是终态 |
|
||||
|
||||
remark 为空时走参数校验,返回校验失败提示(非业务码)。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **零副作用**:只改申请单状态。订单状态、团期名额、退款单**三项分文未动**。
|
||||
- **可再次提交**:驳回后该户可以再走一遍退单户流程(在途判重只拦 `PENDING` 单)。
|
||||
- **并发**:同 approve,锁 + CAS 双保险。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
```jsonc
|
||||
// ✅ 提交退单:body 可省略,也可只带 reason
|
||||
{ "reason": "客户临时有事无法参团" }
|
||||
|
||||
// ❌ 不要再期待「调完就退款了」——现在只是建了一张待审单
|
||||
// ❌ 不要传 refundMode:退款模式由后端按团期状态定,前端无权指定
|
||||
|
||||
// ✅ 取消退单:remark 必填
|
||||
{ "remark": "客户已改口,继续参团" }
|
||||
|
||||
// ❌ 取消退单不传 remark → 参数校验失败
|
||||
{}
|
||||
```
|
||||
|
||||
### 金额取哪个数
|
||||
|
||||
- 弹窗展示 → 提交接口返回的 `paidAmount` + `estimatedRefundAmount`
|
||||
- 审批详情展示 → `paidAmount` + `currentEstimatedRefundAmount`(当前值才是实退依据)
|
||||
- **都不要**调 `/v3/admin/order/{id}/cancel-preview` 自己算,它恒按退改政策算,团期早期会少显示
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 提交时新增一条退单审批记录(待审),**不改订单、不改团期计数、不建退款单**。
|
||||
- 审批通过时才发生:订单流转为已取消、团期已报名人数/户数回落、按金额生成退款申请并进实退链路
|
||||
(金额为 0 时不生成退款申请)。
|
||||
- 取消退单(驳回)只更新审批记录的状态与批复信息,其余数据零变动。
|
||||
- 提交 / 通过 / 驳回各写一条团期时间线;写失败降级为 WARN,不影响主流程。
|
||||
- 同一子订单同时只允许存在一条待审记录。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 出行中及以后不允许退单,提交与审批两处都拦(589501),走售后退款。
|
||||
- 未付分文可以退单;早期阶段(全退订金)仍会按**订金应付额**退并生成退款单,后期阶段(按政策)才退 0 且不生成退款单。
|
||||
- 已满团的团期提交退单会返回 `warnings` 提示,**不阻断**。
|
||||
- 审批期间该户完全正常:可提需求、可排房排车、可被派资源。
|
||||
- 审批期间若该户又付了尾款,通过时按**当前**已付款额重算退款。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### 审批状态(`approvalStatus`)
|
||||
|
||||
| 值 | 含义 | 可做的操作 |
|
||||
|----|------|------------|
|
||||
| PENDING | 待审 | 通过 / 取消退单 |
|
||||
| APPROVED | 已通过(已退团) | 无(终态) |
|
||||
| REJECTED | 已取消退单 | 无(终态);该户可再次提交新申请 |
|
||||
|
||||
### 退款模式(`refundMode`)
|
||||
|
||||
| 值 | 含义 | 出现阶段 |
|
||||
|----|------|----------|
|
||||
| FULL_DEPOSIT | 全额退**订金应付额**(与已付款额无关;订金为 0 时订单侧拒绝退款) | 招募中 / 资源准备中 |
|
||||
| POLICY | 以**已付款额**为基数按退改政策阶梯扣减 | 物资准备中 / 待出发 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
针对 `POST /v3/admin/order/group-batch/:groupBatchId/sub-order/:orderId/withdraw`:
|
||||
|
||||
| 维度 | 修改前 | 修改后 |
|
||||
|------|--------|--------|
|
||||
| 调用后果 | **立即退款**:订单取消、名额释放、退款单自动建 | **只建待审申请单**,三项零变动 |
|
||||
| 招募中 / 资源准备中 | 免审直退,钱当场退 | 同样要管理员审批 |
|
||||
| 物资准备中 / 待出发 | 退款单挂起等定制师审 | 团期侧审批通过后一次到位,不再二次审 |
|
||||
| 响应体 | `Result<Void>`,`data` 为 null | `Result<WithdrawSubOrderRespVO>`,含两个金额与审批单 ID |
|
||||
| 能否反悔 | 不能,订单已取消 | 能,管理员可「取消退单」,该户继续留在团期 |
|
||||
| 退款金额口径 | 按团期状态选模式 | **不变**,仍按团期状态选模式 |
|
||||
| 谁审批 | 定制师(且早期免审) | **管理员角色**(ADMIN / SUPER_ADMIN) |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **前端必改**:提交成功后的提示语(「已退款」→「已提交审核,待管理员确认后退款」);
|
||||
弹窗金额改为两行展示;新增审批中心三个页面(列表 / 详情 / 通过与取消)。
|
||||
- **不改也不会崩**:响应体从 null 变对象是**向后兼容**的(老前端不读 `data` 即可),
|
||||
但**业务语义会错**——用户以为钱退了,实际还在等审批。属必须跟进项。
|
||||
- **运营流程变化**:招募期退款不再是秒退,需要管理员点一下;换来的是可撤销。
|
||||
- **其他端**:C 端、小程序、财务侧退款工作台**均不受影响**——退款单的建立与实退链路完全没动。
|
||||
- **回滚**:回滚本次发布即恢复旧行为;已建的待审申请单不会自动执行,需人工处理。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 转订单 `POST .../transfer-in`(#7095)——两件事,各走各的,本次未动。
|
||||
- 团期成团 / 取消成团 / 流团 / 调整容量 / 预支等其余团期动作端点。
|
||||
- 子订单列表 `GET /v3/admin/order/group-batch/:groupBatchId/orders`——退单弹窗下拉仍用它,出参未变。
|
||||
- 订单侧散客退改政策与 `/v3/admin/order/{id}/cancel-preview`,口径与实现均未动。
|
||||
- 退款工作台的申请、审核、实退链路。
|
||||
- C 端 / 小程序全部接口。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**过网关真测已完成**(2026-09-05,`https://api.test.1814.love:9443`,
|
||||
admin 账号经 `POST /admin/auth/login` + `POST /admin/auth/switch-role` 切到 `ADMIN` 角色)。
|
||||
测试数据:在团期 `Q202610312089667212070612994`(资源准备中)代下两单并全程验证后作废。
|
||||
|
||||
| # | 验证项 | 结果 |
|
||||
|---|--------|------|
|
||||
| 1 | 角色门:`ROOM_MANAGER` 调审批列表 | 589530 拒绝 ✅ |
|
||||
| 2 | 072 列表(ADMIN,缺省 PENDING / `ALL`) | 200,分页结构 `records/total/page/pageSize` ✅ |
|
||||
| 3 | 073 详情:不存在的单 | 589531 ✅ |
|
||||
| 4 | 070 提交:不存在的团期 | 589500 ✅ |
|
||||
| 5 | 070 提交:真团期 + 不存在的子订单 | 581007 订单不存在 ✅ |
|
||||
| 6 | 070 提交:真实子订单 | 200,返 approvalId + 两个金额 + 模式 ✅ |
|
||||
| 7 | 提交后订单/名额零变动 | 订单仍 `PENDING_PAY`,enrolled 仍 2/1 ✅ |
|
||||
| 8 | 072 列表出现该待审单 | total=1,字段齐 ✅ |
|
||||
| 9 | 073 详情双预估 | 提交时预估与当前预估均返回 ✅ |
|
||||
| 10 | 070 重复提交 | 589529 ✅ |
|
||||
| 11 | **075 取消退单** | 200 → REJECTED ✅ |
|
||||
| 12 | **驳回后该户仍在团里** | 订单仍 `PENDING_PAY`,enrolled 仍 2/1,三项零变动 ✅ |
|
||||
| 13 | 已处理的单再驳 | 589532 ✅ |
|
||||
| 14 | 驳回后可再次提交 | 200,新 approvalId ✅ |
|
||||
| 15 | **074 审核通过** | 200 → APPROVED,回填批复人/实退额 ✅ |
|
||||
| 16 | 通过后订单取消 + 名额回落 | 订单 `CANCELLED`,enrolled 2/1 → **0/0** ✅ |
|
||||
| 17 | 已通过的单再批 | 589532 ✅ |
|
||||
| 18 | 075 remark 为空 | 400「驳回原因不能为空」✅ |
|
||||
| 19 | **预估 == 实退** | 预估 ¥2000 = 实退 ¥2000 ✅(见下方缺陷) |
|
||||
|
||||
### 实测暴露并已修复的缺陷
|
||||
|
||||
首轮实测发现:已付 ¥0 的单,弹窗显示「预计退 ¥0」,审批通过后**实退 ¥2000**——预估与实退不同源。
|
||||
根因是订单侧 `FULL_DEPOSIT` 退的是**订金应付额**而非已付额,本模块的预估函数错用了已付额。
|
||||
已由 **PR #7126** 修复(预估改用订金、订金为 0 时出 warning),重新部署后复测预估与实退一致。
|
||||
两轮验证产生的退款单(`2096027837650616321`、`2096029516789915650`)均已置 REJECTED,
|
||||
无实退记录,**测试环境资金零变动**。
|
||||
|
||||
### 部署记录
|
||||
|
||||
- 2026-09-04 19:39–19:40 首次部署(PR #7106)
|
||||
- 2026-09-05 08:14–08:15 修复后重新部署(PR #7126)
|
||||
|
||||
两次均从 `dev-v3` 重新构建 hl-order-service-v3 并滚动重启,8186 / 8086 双实例先后健康。
|
||||
|
||||
### 本地测试
|
||||
|
||||
`mvn -pl hl-order-service-v3 -am test` 共 8175 例,Failures: 0、Errors: 7——
|
||||
7 例全部是 Testcontainers 迁移测试因本机无 Docker 报 `IllegalState`,与本次改动无关。
|
||||
`GroupBatchWithdrawApprovalServiceTest` 18 例(含 3 例订金口径回归)、
|
||||
`GroupBatchFinanceServiceTest` 24 例、团期全域 553 例全绿;ArchUnit 架构规则全绿。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| **PR #7126** | **#7100** | 实测暴露的预估口径修复(FULL_DEPOSIT 按订金算) | ✅ 最新 |
|
||||
| PR #7106 | #7100 | 退单户改为申请 + 管理员审批,新增 072~075 | ✅ 有效 |
|
||||
| #7103 | #7095 | 团期转订单(与本次并行开发,错误码与迁移号已错开) | ✅ 有效 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#7100](https://git.1814.love:8443/wx/HL/issues/7100)
|
||||
- 关联 PR: [wx/HL#7106](https://git.1814.love:8443/wx/HL/pulls/7106)
|
||||
- 后续计划: 流团审批复用同一套审批表(本次只做退单);已通过后的撤销走售后,不在本次范围
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#7100](https://git.1814.love:8443/wx/HL/issues/7100)
|
||||
- **PR**: [#7106](https://git.1814.love:8443/wx/HL/pulls/7106) + [#7126](https://git.1814.love:8443/wx/HL/pulls/7126)(预估口径修复)
|
||||
- **Merge commit**: [46c261d84](https://git.1814.love:8443/wx/HL/commit/46c261d84)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
@@ -0,0 +1,299 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7101"
|
||||
title: "供应商合作中资料变更接入企微审批"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "fbed9101"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-05"
|
||||
status_note: "后端已部署并通过 TEST;前端需将合作中资料保存结果按企微异步审批处理,并展示审核中。"
|
||||
updated_at: "2026-09-05"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商模块:合作中资料变更接入企微审批
|
||||
|
||||
> **服务**: `hl-resource-service`、`hl-user-service`
|
||||
> **Issue**: #7101
|
||||
> **PR**: #7125、#7128
|
||||
> **日期**: 2026-09-05
|
||||
> **影响范围**: 管理后台供应商资料保存与新增收款账户结果展示
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
合作中(`ACTIVE`)供应商保存资料不再直接更新正式资料:接口先返回企微审批受理结果,待审批时统一显示“审核中”;驳回不更新,通过后才应用本次申请。新增账户仍独立审批,本次补充中文审批状态字段。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 更新供应商 | PUT | `/admin/supplier/items/{supplierId}/update` | 行为与响应修改 | `ACTIVE` 改为企微审批后生效,响应新增 `approval` |
|
||||
| 2 | 批量新增收款账户 | POST | `/admin/supplier/items/{supplierId}/bank-accounts/add` | 响应字段扩展 | 每项新增 `approvalStatusName` |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 更新供应商 `PUT /admin/supplier/items/{supplierId}/update`
|
||||
|
||||
**VO**: `SupplierUpdateReqVO → SupplierWriteRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
保存合作中供应商的主体资料、联系人或资质;草稿保存语义不变。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 | 供应商 ID |
|
||||
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 当前供应商版本 |
|
||||
| `changeReason` | Body | String | `ACTIVE` 必填 | 最长 500 | 变更原因 |
|
||||
| 资料字段 | Body | 原类型 | 否 | 沿用原契约 | 至少产生一项实际变化 |
|
||||
| `contacts` | Body | Array | 否 | 完整快照 | 已有联系人带 `contactId`、`expectedUpdateTime`;新增联系人省略 `contactId` |
|
||||
|
||||
#### 出参 `Result<SupplierWriteRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.status` | String | 审批期间仍为 `ACTIVE` |
|
||||
| `data.statusName` | String | 待审批时为“审核中” |
|
||||
| `data.approval.approvalLogId` | String | 资料变更审批记录 ID |
|
||||
| `data.approval.approvalStatus` | String | 初次受理为 `PENDING` |
|
||||
| `data.approval.approvalStatusName` | String | 初次受理为“审核中” |
|
||||
| `data.approval.spNo` | String | 企业微信审批单号 |
|
||||
| `data.approval.syncStatus` | String | 审批同步状态 |
|
||||
| `data.updateTime` | String | 审核中仍返回正式资料当前版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"remark": "更新合作备注",
|
||||
"changeReason": "业务资料更新",
|
||||
"expectedUpdateTime": "2026-09-05 09:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2095000000000000001",
|
||||
"status": "ACTIVE",
|
||||
"statusName": "审核中",
|
||||
"approval": {
|
||||
"approvalLogId": "2095000000000000002",
|
||||
"provider": "WECOM",
|
||||
"approvalStatus": "PENDING",
|
||||
"approvalStatusName": "审核中",
|
||||
"spNo": "202609050001",
|
||||
"syncStatus": "REQUESTING"
|
||||
},
|
||||
"updateTime": "2026-09-05 09:00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
成功受理时 `data.approval` 不为空;企微结果不确定时保留审批事实,前端不得自动重复提交。草稿直接保存时 `data.approval=null`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395014,
|
||||
"message": "数据已被他人修改,请刷新后重试",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
`400` 表示字段、联系人快照或变更原因不合法;`395005` 表示当前状态不允许;`395019`~`395022` 表示企微未配置、账号未绑定、提交失败或状态待对账。未新增错误码。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 审核中正式资料保持原值;驳回不更新,通过后一次性应用申请中的冻结值。
|
||||
- 新增联系人必须省略 `contactId`,不得发送 `contactId:null`;只修正内部审批快照兼容,不放宽外部显式空 ID 校验。
|
||||
- 企微表单的变更明细只列实际差异,身份证号和个人电话完整展示;相关附件和当前正式资料为选填。
|
||||
|
||||
### 2. 批量新增收款账户 `POST /admin/supplier/items/{supplierId}/bank-accounts/add`
|
||||
|
||||
**VO**: `SupplierBankAccountBatchCreateReqVO → List<BankAccountSubmitResultRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
为合作中供应商新增一至五十个收款账户,每个账户独立提交企微审批。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 | 合作中供应商 ID |
|
||||
| `accounts` | Body | Array | 是 | 1~50 项 | 账户列表,原请求结构不变 |
|
||||
|
||||
#### 出参 `Result<List<BankAccountSubmitResultRespVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data[].approvalStatus` | String | `PENDING` / `APPROVED` / `REJECTED` |
|
||||
| `data[].approvalStatusName` | String | “审核中” / “已通过” / “已驳回” |
|
||||
| `data[].accountStatus` | String | 审核中为 `PENDING`,通过为 `ACTIVE`,驳回为 `REJECTED` |
|
||||
| `data[].spNo` | String | 企业微信审批单号 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"accounts": [{
|
||||
"accountType": "PERSONAL",
|
||||
"bankName": "示例银行",
|
||||
"bankBranch": "示例支行",
|
||||
"accountNo": "6222000012345678",
|
||||
"proofFileUrls": [],
|
||||
"settleMode": "SINGLE",
|
||||
"invoiceType": "NONE"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": [{
|
||||
"accountId": "2095000000000000003",
|
||||
"approvalLogId": "2095000000000000004",
|
||||
"provider": "WECOM",
|
||||
"approvalStatus": "PENDING",
|
||||
"approvalStatusName": "审核中",
|
||||
"spNo": "202609050002",
|
||||
"syncStatus": "REQUESTING",
|
||||
"accountStatus": "PENDING",
|
||||
"isDefault": "NO"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口不返回空成功列表;结果不确定时账户保持不可用,前端不得自动重提。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395011,
|
||||
"message": "该账户正在审批中,请勿重复提交",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
`400` 表示账户字段或组合规则不合法;`395010` 表示供应商尚未生效;其他企微错误沿用现有 `395019`~`395022`。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 每个账户独立审批;审核中保持 `PENDING` 且不可收款、不可设为默认。
|
||||
- 企微驳回后为 `REJECTED`,通过后为 `ACTIVE`;前端只把 `ACTIVE` 当作生效账户。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
1. 更新接口返回成功后,若 `data.approval.approvalStatus=PENDING`,展示“审核中”并保留正式资料页面值,不做本地乐观覆盖。
|
||||
2. 通过详情、审批记录或账户列表刷新终态;审批只能在企业微信处理,管理端不发送通过或驳回命令。
|
||||
3. 联系人数组是完整快照;新增项省略 `contactId`,已有项同时传 `contactId` 与 `expectedUpdateTime`。
|
||||
4. 所有接口同时检查 HTTP 状态和响应体 `code/success`。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 本次无数据库结构或迁移变化。
|
||||
- 资料审批中不写正式资料;通过后应用冻结申请,驳回后正式版本和值均不变。
|
||||
- 新账户审批中为 `PENDING`,通过后为 `ACTIVE`,驳回后为 `REJECTED`。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 模板审批节点由企业微信配置决定,后端不校验“恰好一级”。
|
||||
- 表单固定使用已配置的八个 `Text` 控件;相关附件与当前正式资料允许为空。
|
||||
- 企微回调或轮询完成前,前端只展示受理状态,不推断最终结果。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
| 字段 | 值 | 中文 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `approvalStatus` | `PENDING` | 审核中 | 尚未终结 |
|
||||
| `approvalStatus` | `APPROVED` | 已通过 | 业务变更已应用 |
|
||||
| `approvalStatus` | `REJECTED` | 已驳回 | 业务变更未应用 |
|
||||
| `accountStatus` | `PENDING` | 待审批 | 账户不可用 |
|
||||
| `accountStatus` | `ACTIVE` | 生效中 | 账户可用 |
|
||||
| `accountStatus` | `REJECTED` | 已驳回 | 账户不可用 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 行为 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| `ACTIVE` 供应商保存资料 | 直接更新正式资料 | 返回企微审批;通过后更新,驳回不更新 |
|
||||
| 资料保存响应 | 无独立资料审批结果 | 返回 `data.approval` 与“审核中”状态 |
|
||||
| 新增账户响应 | 仅英文审批状态 | 增加 `approvalStatusName` 中文状态 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 是;合作中资料保存由同步生效改为异步审批。
|
||||
- **前端是否必须同步上线**: 是。
|
||||
- **前端 workaround 清理点**: 删除保存成功后立即以请求值覆盖正式资料的逻辑,改为展示“审核中”并刷新终态。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 草稿供应商直接保存、注册审批、主体状态审批、合同、默认账户和停用流程不变。
|
||||
- 新增账户的请求路径、请求字段和独立审批语义不变。
|
||||
- 未新增数据库、配置、Redis、MQ 或错误码。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
```text
|
||||
PUT /admin/supplier/items/{supplierId}/update → PENDING / 审核中,正式资料保持原值 ✓
|
||||
企微通过资料变更 → 正式资料更新,身份证号与电话完整值可见 ✓
|
||||
企微驳回资料变更 → 正式资料和值版本均不变 ✓
|
||||
POST /admin/supplier/items/{supplierId}/bank-accounts/add → PENDING / 审核中 ✓
|
||||
企微通过新增账户 → ACTIVE ✓
|
||||
企微驳回新增账户 → REJECTED,未激活 ✓
|
||||
```
|
||||
|
||||
资料审批使用模板 `3WNhaxns4i6kfVs74gz4qZQhFxy4iJY4anxMuvYw`;八个 `Text` 控件、实际差异格式、完整身份证号/电话和两个选填项已逐项验证。部署提交为 `6bc1c1262e5e4902a893f30fac875d35a65f8a76`。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- [Issue #7101](https://git.1814.love:8443/wx/HL/issues/7101)
|
||||
- [主 PR #7125](https://git.1814.love:8443/wx/HL/pulls/7125)
|
||||
- [补充 Issue #7127](https://git.1814.love:8443/wx/HL/issues/7127)
|
||||
- [补充 PR #7128](https://git.1814.love:8443/wx/HL/pulls/7128)
|
||||
|
||||
## 前端动作与当前状态
|
||||
|
||||
- 合作中资料保存成功后读取 `data.approval`,展示“审核中”,刷新终态后再更新正式资料。
|
||||
- 新增账户直接展示 `approvalStatusName`,并按 `accountStatus` 判断是否可用。
|
||||
- 新增联系人省略 `contactId`,已有联系人继续携带 ID 与版本。
|
||||
- **当前状态:待前端处理。**
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#7101](https://git.1814.love:8443/wx/HL/issues/7101)
|
||||
- **PR**: [#7125](https://git.1814.love:8443/wx/HL/pulls/7125)
|
||||
- **Merge commit**: [91a965f0b0f403a0f8c9d3990587f09ea780c9dd](https://git.1814.love:8443/wx/HL/commit/91a965f0b0f403a0f8c9d3990587f09ea780c9dd)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,300 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6842"
|
||||
title: "供应商合同修改与删除接口"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "汇总现有合同修改、删除契约;前端按完整替换、必填原因及可选合同版本接入。既有 TEST 业务实测及本次只读复核范围见正文。"
|
||||
updated_at: "2026-09-06"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商合同:修改与删除接口
|
||||
|
||||
> **影响范围**:管理后台供应商合同编辑、删除。当前状态:后端已部署;前端待核对接入。
|
||||
|
||||
更新为整份替换,15 个业务字段均可空;修改、删除都必须提交 `changeReason`。`expectedUpdateTime` 可省略,传入时必须使用合同自身版本。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 修改供应商合同 | PUT | `/admin/supplier/items/{supplierId}/contracts/{contractId}/update` | 现有契约说明 | 完整替换一份合同 |
|
||||
| 2 | 删除供应商合同 | DELETE | `/admin/supplier/items/{supplierId}/contracts/{contractId}/del` | 现有契约说明 | 携带 JSON 原因软删除一份合同 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 修改供应商合同 `PUT /admin/supplier/items/{supplierId}/contracts/{contractId}/update`
|
||||
|
||||
**VO**: `SupplierContractUpdateReqVO / SupplierContractRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
编辑供应商下已登记的单份合同。提交应保留的全部业务字段;未提交字段按空值覆盖。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID | 所属供应商 |
|
||||
| `contractId` | Path | String | 是 | 正整数 ID | 目标合同 |
|
||||
| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 字符 | 本次修改原因 |
|
||||
| `expectedUpdateTime` | Body | String/null | 否 | `yyyy-MM-dd HH:mm:ss` | 合同 `updateTime`,传入即校验 |
|
||||
| `contractName` | Body | String/null | 否 | 最长 500 字符 | 合同名称 |
|
||||
| `contractNo` | Body | String/null | 否 | 最长 100 字符 | 合同编号 |
|
||||
| `contractType` | Body | String/null | 否 | `FRAME` / `SINGLE_TRIP` / `PURCHASE` | 合同类型 |
|
||||
| `signDate` | Body | String/null | 否 | `yyyy-MM-dd` | 签署日期 |
|
||||
| `startDate` | Body | String/null | 否 | `yyyy-MM-dd` | 有效期开始 |
|
||||
| `endDate` | Body | String/null | 否 | 两端有值时不得早于 `startDate` | 有效期结束 |
|
||||
| `businessLine` | Body | String/null | 否 | 最长 100 字符 | 业务线自由文本 |
|
||||
| `relatedMainContract` | Body | String/null | 否 | 最长 100 字符 | 关联主合同引用 |
|
||||
| `autoRenew` | Body | Boolean/null | 否 | `true` / `false` / `null` | 自动续约 / 不续约 / 未登记 |
|
||||
| `amount` | Body | String/null | 否 | 非负,最多 10 位整数、2 位小数 | 金额,如 `"1200.50"` |
|
||||
| `pricingMode` | Body | String/null | 否 | 最长 100 字符 | 计价方式 |
|
||||
| `settleCycle` | Body | String/null | 否 | 最长 32 字符 | 结算周期自由文本 |
|
||||
| `status` | Body | String/null | 否 | `DRAFT` / `ACTIVE` / `SIGNED` / `EXPIRED` | 合同状态 |
|
||||
| `scanFileUrl` | Body | String/null | 否 | 最长 1000 字符 | 一个合同附件的永久地址 |
|
||||
| `remark` | Body | String/null | 否 | 最长 500 字符 | 合同备注 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `code` / `message` / `success` | Integer / String / Boolean | 成功为 `200`、`成功`、`true` |
|
||||
| `data.contractId` | String | 合同 ID |
|
||||
| `data.contractName` | String/null | 合同名称 |
|
||||
| `data.contractNo` | String/null | 合同编号 |
|
||||
| `data.contractType` | String/null | 合同类型 |
|
||||
| `data.signDate` | String/null | 签署日期 |
|
||||
| `data.startDate` / `data.endDate` | String/null | 有效期,`yyyy-MM-dd` |
|
||||
| `data.businessLine` | String/null | 业务线 |
|
||||
| `data.relatedMainContract` | String/null | 关联主合同 |
|
||||
| `data.autoRenew` | Boolean/null | 自动续约标记,`false` 与 `null` 含义不同 |
|
||||
| `data.amount` | String/null | 金额;无金额为 `null` |
|
||||
| `data.pricingMode` | String/null | 计价方式 |
|
||||
| `data.settleCycle` | String/null | 结算周期 |
|
||||
| `data.status` | String/null | 合同状态;历史读取可能出现 `TERMINATED` |
|
||||
| `data.scanFileUrl` | String/null | 合同附件地址 |
|
||||
| `data.remark` | String/null | 合同备注 |
|
||||
| `data.updateTime` | String | 合同新版本,`yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
以下 ID、附件地址和时间均为示例值。
|
||||
|
||||
```http
|
||||
PUT /admin/supplier/items/2095000000000000001/contracts/2095000000000000010/update
|
||||
Authorization: Bearer <当前有效凭证>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"changeReason": "调整合同有效期",
|
||||
"expectedUpdateTime": "2026-09-06 10:00:00",
|
||||
"contractName": "年度服务合同",
|
||||
"contractNo": "HT-2026-001",
|
||||
"contractType": "FRAME",
|
||||
"signDate": "2026-09-01",
|
||||
"startDate": "2026-09-01",
|
||||
"endDate": "2027-09-30",
|
||||
"businessLine": "旅行服务",
|
||||
"relatedMainContract": null,
|
||||
"autoRenew": false,
|
||||
"amount": "1200.50",
|
||||
"pricingMode": "按团结算",
|
||||
"settleCycle": "月结",
|
||||
"status": "SIGNED",
|
||||
"scanFileUrl": "https://files.example.com/contracts/demo.pdf",
|
||||
"remark": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"contractId": "2095000000000000010",
|
||||
"contractName": "年度服务合同",
|
||||
"contractNo": "HT-2026-001",
|
||||
"contractType": "FRAME",
|
||||
"signDate": "2026-09-01",
|
||||
"startDate": "2026-09-01",
|
||||
"endDate": "2027-09-30",
|
||||
"businessLine": "旅行服务",
|
||||
"relatedMainContract": null,
|
||||
"autoRenew": false,
|
||||
"amount": "1200.50",
|
||||
"pricingMode": "按团结算",
|
||||
"settleCycle": "月结",
|
||||
"status": "SIGNED",
|
||||
"scanFileUrl": "https://files.example.com/contracts/demo.pdf",
|
||||
"remark": null,
|
||||
"updateTime": "2026-09-06 10:00:01"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
可空业务字段省略或传 `null` 均清空;可选文本空白也规范化为空。只传 `changeReason` 会尝试清空全部业务字段,不表示仅改原因。成功返回合同对象,无变化时返回业务错误。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
合同版本过期:
|
||||
|
||||
```json
|
||||
{"code":395014,"message":"数据已被他人修改,请刷新后重试","success":false,"data":null}
|
||||
```
|
||||
|
||||
`400`:缺少原因或格式错误;`395002`:无写权限;`395051`:合同不存在、已删除或归属不符;`395054`:日期倒置;`395057`:无实际变化;`395031`:供应商已归档;`395005`:主体企微审批未结束或状态不允许。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 要求 `FINANCE` 或 `SUPER_ADMIN` 且具有 `supplier:update`;`ADMIN` 被拒绝。
|
||||
- 合同必须属于路径供应商,供应商不能已归档,主体企微审批必须已结束。
|
||||
- 完整替换包含页面隐藏字段;需保留的字段应原样提交。版本取合同自身,不使用供应商主体版本。
|
||||
|
||||
### 2. 删除供应商合同 `DELETE /admin/supplier/items/{supplierId}/contracts/{contractId}/del`
|
||||
|
||||
**VO**: `SupplierContractDeleteReqVO / Result<Void>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
删除一份已登记合同。DELETE 请求须携带 JSON 请求体。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID | 所属供应商 |
|
||||
| `contractId` | Path | String | 是 | 正整数 ID | 目标合同 |
|
||||
| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 字符 | 本次删除原因 |
|
||||
| `expectedUpdateTime` | Body | String/null | 否 | `yyyy-MM-dd HH:mm:ss` | 合同当前版本,提供时必须匹配 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `code` | Integer | 成功为 `200` |
|
||||
| `message` | String | 成功为 `成功` |
|
||||
| `success` | Boolean | 成功为 `true` |
|
||||
| `data` | null | 无合同对象 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
DELETE /admin/supplier/items/2095000000000000001/contracts/2095000000000000010/del
|
||||
Authorization: Bearer <当前有效凭证>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"changeReason": "合同重复登记",
|
||||
"expectedUpdateTime": "2026-09-06 10:00:01"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","success":true,"data":null}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
成功的 `data: null` 为正常结果,删除后从详情合同列表中移除该项。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
缺少删除原因:
|
||||
|
||||
```json
|
||||
{"code":400,"message":"变更原因不能为空","success":false,"data":null}
|
||||
```
|
||||
|
||||
`395002`:无写权限;`395051`:合同不存在、已删除或归属不符;`395014`:版本过期;`395031`:供应商已归档;`395005`:主体企微审批未结束或状态不允许。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 要求 `FINANCE` 或 `SUPER_ADMIN` 且具有 `supplier:update`;供应商已归档或主体企微审批未结束时拒绝。
|
||||
- 删除为软删除,不删除供应商或账户;没有合同恢复接口。
|
||||
- 前端请求库需将原因放进 DELETE 的 JSON body;不能只拼 Query 参数或只传 ID。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
从 `GET /admin/supplier/items/{supplierId}/basic-info/view` 的 `data.contracts[]` 读取合同、`contractId` 和 `updateTime`。修改时提交应保留的全部字段;修改成功用新版本继续操作,删除成功刷新列表。版本冲突重新读取后再编辑。日期按 GMT+8 日历日期传 `yyyy-MM-dd`。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
更新只替换目标合同并刷新合同版本;删除后有效详情不再返回该合同,历史记录保留。供应商主体版本不因合同写入改变。参数、权限、归属或版本校验失败时不产生合同变更。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
未登录或登录失效按认证失败处理;供应商不存在返回 `395001`。业务错误可能随 HTTP 200 返回,应检查 `code` 和 `success`。金额 `1200.5` 与 `1200.50` 视为相同值,仅改变金额格式不会构成实际变更。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### `contractType`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `FRAME` | 框架合同 | 可写 |
|
||||
| `SINGLE_TRIP` | 单团单合同 | 可写 |
|
||||
| `PURCHASE` | 采购合同 | 可写 |
|
||||
| `null` | 未登记 | 可写 |
|
||||
|
||||
### `status`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `DRAFT` | 草稿 | 可写 |
|
||||
| `ACTIVE` | 生效中 | 可写 |
|
||||
| `SIGNED` | 已签约 | 可写 |
|
||||
| `EXPIRED` | 已过期 | 可写 |
|
||||
| `TERMINATED` | 已终止 | 仅历史回显,不可提交 |
|
||||
| `null` | 未登记 | 可写 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
本次为既有契约汇总,后端无新增变更。
|
||||
|
||||
| 字段 / 行为 | 早期 #6544 说明 | 当前契约 |
|
||||
|---|---|---|
|
||||
| 合同业务字段 | 部分必填 | 全部可空;含业务线、关联主合同、自动续约 |
|
||||
| `expectedUpdateTime` | 必填 | 可选,提供时校验合同版本 |
|
||||
| `status` | 未列出 `SIGNED` | 支持已签约 `SIGNED` |
|
||||
| 修改 / 删除 | 独立接口 | 路径保持不变;原因仍必填 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
本次没有新增兼容性变化,无需与后端同步上线。前端接入现有修改、删除按钮时使用上述请求体,避免只传变化字段导致清空、丢弃 DELETE body 或误用主体版本。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
本次仅说明合同维护入口;供应商主体保存和账户维护继续使用各自接口。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- 既有业务实测:#6654 已覆盖合同补全、清空、删除、缺原因及版本失败零写入;#6842 已覆盖扩展字段保存回读、`SIGNED`、`null`、`false` 和日期错误零写入。
|
||||
- 本次于 2026-09-06 通过 TEST Gateway 只读核对 Resource Swagger:合同 PUT、DELETE 路径存在,更新的 15 个业务字段、可选版本与必填原因均已发布;同步核对最新 `dev-v3`。此次未重复执行共享环境业务写操作。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- [合同修改、删除实测 #6654](https://git.1814.love:8443/wx/HL/issues/6654#issuecomment-43820)
|
||||
- [补充字段与实测 #6842](https://git.1814.love:8443/wx/HL/issues/6842#issuecomment-44703)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#6842](https://git.1814.love:8443/wx/HL/issues/6842)
|
||||
- **PR**: [#6858](https://git.1814.love:8443/wx/HL/pulls/6858)、[#6665](https://git.1814.love:8443/wx/HL/pulls/6665)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,234 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7087"
|
||||
title: "供应商账户修改入口与删除限制"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "现有接口说明与历史口径纠正;前端按草稿账户修改入口接入,取消空数组删除账户。既有 TEST 业务实测及本次只读复核范围见正文。"
|
||||
updated_at: "2026-09-06"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商账户:修改入口与删除限制
|
||||
|
||||
> **影响范围**:管理后台供应商账户编辑、删除操作。当前状态:后端已部署;前端待核对接入。
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
草稿初始账户通过供应商更新接口修改,必须保留一项账户。**旧 #6669 文档的 `initialAccounts: []` 删除方式已被 #7087 收紧,当前返回 `400 / 账户不能为空`。** 草稿的 `changeReason` 现可省略。
|
||||
|
||||
| 操作 | 当前支持情况 |
|
||||
|---|---|
|
||||
| 修改草稿初始账户 | 支持,使用下文接口,供应商及其已有账户必须均为 `DRAFT` |
|
||||
| 修改已提交或已生效账户资料 | 未提供独立接口;不能通过供应商更新绕过状态限制 |
|
||||
| 单独删除账户 | 未提供接口;不能提交空数组清空最后一项账户 |
|
||||
| 设置默认账户 | 已有 `PUT /admin/supplier/bank-accounts/{accountId}/default/update`,仅改变默认标记 |
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 修改草稿初始账户 | PUT | `/admin/supplier/items/{supplierId}/update` | 现有契约说明 | 通过 `initialAccounts` 完整替换唯一草稿账户;禁止空数组删除 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 修改草稿初始账户 `PUT /admin/supplier/items/{supplierId}/update`
|
||||
|
||||
**VO**: `SupplierUpdateReqVO / SupplierBankAccountReqVO / SupplierWriteRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
在资料完整的草稿供应商下修改初始账户。本节列出账户编辑所需载荷;其余主体资料省略时保留现值。保存后主体必填资料、供应商类型、联系人、账户仍须完整。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `supplierId` | Path | String | 是 | 正整数 ID | 供应商 ID |
|
||||
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 供应商主体版本,不能使用账户版本 |
|
||||
| `initialAccounts` | Body | Array | 本场景是 | 恰好 1 项 | 省略或 `null` 保留现值;`[]` 拒绝 |
|
||||
| `initialAccounts[].accountType` | Body | String | 是 | `CORPORATE` / `PERSONAL` | 对公 / 对私 |
|
||||
| `initialAccounts[].bankName` | Body | String | 是 | 非空,最长 500 字符 | 开户银行 |
|
||||
| `initialAccounts[].accountNo` | Body | String | 是 | 去空白和连字符后 8~32 位数字 | 收款账号 |
|
||||
| `initialAccounts[].bankBranch` | Body | String/null | 否 | 最长 500 字符 | 开户支行,可清空 |
|
||||
| `initialAccounts[].proofFileUrls` | Body | Array/null | 否 | 最多 20 个不重复的公网 HTTPS 地址,每项最长 1000 字符;不带查询参数或片段 | 证明附件,省略或 `[]` 清空 |
|
||||
| `initialAccounts[].settleMode` | Body | String/null | 否 | `PREPAY` / `MONTHLY` / `SINGLE` | 结算方式 |
|
||||
| `initialAccounts[].accountPeriod` | Body | String/null | 条件必填 | 最长 50 字符;仅 `MONTHLY` 必填,其他方式须为空 | 月结账期 |
|
||||
| `initialAccounts[].invoiceType` | Body | String/null | 否 | `SPECIAL` / `NORMAL` / `NONE` | 发票类型 |
|
||||
| `initialAccounts[].taxRate` | Body | String/null | 条件必填 | 0%~100%,最多两位小数 | 可开票时必填;`NONE` 或未选发票类型时须为空 |
|
||||
| `changeReason` | Body | String | 否 | 最长 500 字符 | 此处为 `DRAFT`,允许省略 |
|
||||
|
||||
不提交 `accountId`、`accountName` 或账户状态;户名由供应商全称确定。
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `code` / `message` / `success` | Integer / String / Boolean | 业务结果;成功为 `200`、`成功`、`true` |
|
||||
| `data.supplierId` / `data.supplierNo` | String | 供应商 ID / 编号 |
|
||||
| `data.status` / `data.statusName` | String | 本场景为 `DRAFT` / `草稿` |
|
||||
| `data.onboardingStage` | String | 本场景为 `PROFILE_DRAFT` |
|
||||
| `data.initialAccounts` | Array | 保存后的初始账户摘要 |
|
||||
| `data.initialAccounts[].accountId` | String | 当前账户 ID,替换账号后应重新读取 |
|
||||
| `data.initialAccounts[].accountNo` | String | 完整账号 |
|
||||
| `data.initialAccounts[].accountNoMask` | String | 废弃兼容字段,实际同样为完整账号;使用 `accountNo` |
|
||||
| `data.initialAccounts[].status` | String | 本场景为 `DRAFT` |
|
||||
| `data.approval` | null | 草稿直接保存,不发起审批 |
|
||||
| `data.updateTime` | String | 保存后的供应商版本,供下次编辑使用 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
以下 ID、账号和时间均为示例值。
|
||||
|
||||
```http
|
||||
PUT /admin/supplier/items/2095000000000000001/update
|
||||
Authorization: Bearer <当前有效凭证>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"expectedUpdateTime": "2026-09-06 10:00:00",
|
||||
"initialAccounts": [{
|
||||
"accountType": "CORPORATE",
|
||||
"bankName": "示例银行",
|
||||
"accountNo": "6222000012345678",
|
||||
"bankBranch": "示例支行",
|
||||
"proofFileUrls": [],
|
||||
"settleMode": "MONTHLY",
|
||||
"accountPeriod": "月结30天",
|
||||
"invoiceType": "SPECIAL",
|
||||
"taxRate": "6%"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2095000000000000001",
|
||||
"supplierNo": "SUP2095000000000000001",
|
||||
"status": "DRAFT",
|
||||
"statusName": "草稿",
|
||||
"onboardingStage": "PROFILE_DRAFT",
|
||||
"initialAccounts": [{
|
||||
"accountId": "2095000000000000002",
|
||||
"accountNo": "6222000012345678",
|
||||
"accountNoMask": "6222000012345678",
|
||||
"status": "DRAFT"
|
||||
}],
|
||||
"approval": null,
|
||||
"updateTime": "2026-09-06 10:00:01"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
成功响应包含账户摘要。编辑表单完整回显使用 `GET /admin/supplier/items/{supplierId}/account-info/list` 的 `data.bankAccounts`,主体版本取该响应的 `data.updateTime`。摘要不包含银行、附件和结算字段,不能直接作为下次完整账户载荷。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
提交 `initialAccounts: []`:
|
||||
|
||||
```json
|
||||
{"code":400,"message":"账户不能为空","success":false,"data":null}
|
||||
```
|
||||
|
||||
其他常见业务码:`395002` 无写权限;`395005` 非草稿或主体企微审批未结束;`395009` 已有账户不是草稿;`395014` 主体版本过期;`395027` 账号已占用;`400` 缺版本、字段或结算组合不合法。完全未改变数据也返回 `400 / 未检测到实际变化`。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 要求 `FINANCE` 或 `SUPER_ADMIN` 且具有 `supplier:update`;`ADMIN` 被拒绝。
|
||||
- 仅可维护草稿初始账户;已提交、已生效、已驳回账户没有资料修改或删除入口。
|
||||
- 一项账户是完整快照:未提交的可选字段会被清空,需保留的字段必须一并带回。
|
||||
- 相同账号保留账户 ID;换成新账号会替换旧草稿账户,成功后刷新列表和版本。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
1. 按供应商读取账户列表和主体版本,草稿页面提交一项完整账户。
|
||||
2. 从月结切换为其他结算方式时同步清空 `accountPeriod`;选不开票时同步清空 `taxRate`。
|
||||
3. 保存成功刷新账户;版本冲突先重新读取。前端移除空数组删除逻辑,不生成不存在的账户删除路径。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
修改成功保留一项草稿账户并刷新供应商版本;更换账号时旧草稿账户不再出现在有效列表。校验失败不改变账户;空数组请求不会删除数据。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
未登录或登录失效按认证失败处理;必须检查响应体 `code`、`success`,不能仅凭 HTTP 200 判断保存成功。既有主体资料不完整时,账户编辑同样会被必填校验拒绝。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### `accountType`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `CORPORATE` | 对公 | 必填账户类型之一 |
|
||||
| `PERSONAL` | 对私 | 必填账户类型之一 |
|
||||
|
||||
### `settleMode`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `PREPAY` | 预付 | 账期须为空 |
|
||||
| `MONTHLY` | 月结 | 必填账期 |
|
||||
| `SINGLE` | 单次结算 | 账期须为空 |
|
||||
| `null` | 未登记 | 账期须为空 |
|
||||
|
||||
### `invoiceType`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `SPECIAL` | 专票 | 必填税率 |
|
||||
| `NORMAL` | 普票 | 必填税率 |
|
||||
| `NONE` | 不开票 | 税率须为空 |
|
||||
| `null` | 未登记 | 税率须为空 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
本次补充文档,后端无新增变更。
|
||||
|
||||
| 字段 / 行为 | 旧 #6669 说明 | 当前契约 |
|
||||
|---|---|---|
|
||||
| `initialAccounts: []` | 可清空账户 | #7087 起拒绝,必须保留账户 |
|
||||
| 草稿 `changeReason` | 必填 | #6684 起可省略 |
|
||||
| `expectedUpdateTime` | 必填 | 仍必填,使用供应商主体版本 |
|
||||
| 独立账户修改 / 删除 | 无独立接口 | 仍无独立接口;草稿修改走主体更新 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- 本次没有新增兼容性变化;前端须遵守已部署的账户非空约束。
|
||||
- 无需与后端同步上线;清理旧的 `[]` 删除调用,按上述状态控制编辑入口。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
本次说明覆盖草稿初始账户维护;新增账户审批、默认账户切换和合同契约保持现状。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- 既有业务实测:#6654 最终证据记录草稿账户修改、可选字段清空与版本失败零写入;#7087 记录显式空账户被拒绝、至少一项账户保存成功。旧证据中的整项清空已被 #7087 覆盖。
|
||||
- 本次于 2026-09-06 通过 TEST Gateway 只读核对 Resource Swagger:主体更新路径及账户请求/摘要字段存在,未发布独立账户修改、删除路径;同时核对最新 `dev-v3` 源码。此次未重复执行共享环境业务写操作。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- [账户修改既有实测 #6654](https://git.1814.love:8443/wx/HL/issues/6654#issuecomment-43820)
|
||||
- [账户非空约束与实测 #7087](https://git.1814.love:8443/wx/HL/issues/7087#issuecomment-46744)
|
||||
- [草稿免填变更原因 #6684](https://git.1814.love:8443/wx/HL/issues/6684)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#7087](https://git.1814.love:8443/wx/HL/issues/7087)
|
||||
- **PR**: [#7093](https://git.1814.love:8443/wx/HL/pulls/7093)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,351 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7135"
|
||||
title: "团期创单收紧 productBatchId 校验(班期归属产品 / 非 GROUP 拒收 / tierSeq 存在性 / 日期按班期)"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "服务端对 POST /v3/admin/order 新增三个校验错误码 581055/581056/581057,响应 departureDate/returnDate 改为落库值;前端新建订单向导须仅对 GROUP 产品传 productBatchId 且班期必须属于所选产品。"
|
||||
updated_at: "2026-09-06"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 订单模块:团期创单校验收紧(班期归属产品 / 非 GROUP 拒收 / tierSeq 存在性 / 日期按班期)
|
||||
|
||||
管理后台创单接口 `POST /v3/admin/order` 对 GROUP 产品的团期校验收紧,新增三个拒单错误码(581055/581056/581057),同时出发日期、返团日期改为班期的权威值。修复跨产品班期串号导致订单错误归团、CORE/CUSTOM 单被误命中团期逻辑等问题。
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
- **仅 GROUP 产品可传 productBatchId**:CORE/CUSTOM 请求含 productBatchId → 拒单 581056「非团期产品不能指定团期」(改前静默落库并误命中团期分支)。
|
||||
- **班期必须属于所选产品**:productBatchId 对应班期的 productId ≠ 请求 productId → 拒单 581055「所选团期不属于该产品」(改前会按别家班期计价并挂到别家的团)。
|
||||
- **tierSeq 必须在产品配置内**(全产品类型):不在 tierPrices ∪ tiers 并集中 → 拒单 581057「所选档位不存在」;产品未配档位时不拦(改前 tier_name 落 NULL)。
|
||||
- **响应 departureDate/returnDate 改为班期权威值**:GROUP 单出发日 = 班期出发日;返团日 = 班期 endDate,或 班期出发日 + 行程天数 − 1(班期无 endDate 时)。改前响应回显请求日期,落库日期与班期脱钩。
|
||||
- **GROUP 单响应 tierName 现有值**:来自产品 tiers 配置,改前恒 null。
|
||||
- **人数超班期剩余名额拒单(581034)**(#7159 并入本 PR):`成人+儿童+小童 > 班期剩余名额` → 581034;此前订单侧读的 `remainingSlots` 字段在产品侧「库存改造 Phase 2」后已无来源、恒 null,致该预查长期 no-op,现改读产品侧权威字段 `remainingParticipants`(null=人数不限,跳过校验)。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 创建订单(管理端) | POST | `/v3/admin/order` | 请求新增校验 / 响应日期改值 | productBatchId 仅 GROUP 可传;班期归属;tierSeq 存在性;日期按班期 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 创建订单(管理端) `POST /v3/admin/order`
|
||||
|
||||
**VO**: `OrderCreateReqVO → OrderCreateRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理端新建订单向导或团期看板新增子订单调用。创建跟团游、定制游、线路游订单,GROUP 产品必须指定班期,CORE/CUSTOM 产品不允许指定班期。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `productId` | Body | Long(String)| ✅ | 正整数 | 产品 ID |
|
||||
| `tierSeq` | Body | Integer | ✅ | ≥ 1;必须存在于产品配置档位 | 档位序号,不在产品 tierPrices ∪ tiers 配置内 → 581057 |
|
||||
| `departureDate` | Body | yyyy-MM-dd | ✅ | 不早于当天;日期格式 | 出发日期;出发日早于今天 → 581011 |
|
||||
| `adultCount` | Body | Integer | ✅ | ≥ 1 | 成人数 |
|
||||
| `childCount` | Body | Integer | 否 | ≥ 0,默认 0 | 儿童数(5-12 岁) |
|
||||
| `youngChildCount` | Body | Integer | 否 | ≥ 0,默认 0 | 小童数(3-4 岁) |
|
||||
| `babyCount` | Body | Integer | 否 | ≥ 0,默认 0 | 婴儿数(0-2 岁) |
|
||||
| `customerName` | Body | String | ✅ | @NotBlank | 客户姓名 |
|
||||
| `customerPhone` | Body | String | ✅ | ^1[3-9]\d{9}$ | 客户手机号(明文传,DB 层 AES 加密) |
|
||||
| `customerRemark` | Body | String | 否 | ≤ 500 | 客户备注 |
|
||||
| `createSource` | Body | String | 否 | ≤ 20 字;枚举:CUSTOMER / CONSULTANT / OTA / WALK_IN / B2B / VIP_REPURCHASE / REFERRAL / PROMOTION / INTERNAL;默认 CONSULTANT | 创建来源 |
|
||||
| `productBatchId` | Body | Long(String) | 条件必填 | 仅 GROUP 产品可传;CORE/CUSTOM 传了 → 581056;班期 productId 必须等于请求 productId,否则 → 581055 | 团期 ID(product 侧班期 batchId);**GROUP 产品必传,CORE/CUSTOM/ROUTE 禁传**;值须属于请求 productId 对应产品 |
|
||||
| `roomCount` | Body | Integer | 否 | ≥ 1 | 房间数 |
|
||||
| `tags` | Body | List<String> | 否 | 无约束 | 订单标签名列表 |
|
||||
| `sharerOpenid` | Body | String | 否 | 无约束 | 分享人 openid(C 端裂变追踪) |
|
||||
| `customizerId` | Body | Long | 否 | 无约束 | 分享归因定制师 ID |
|
||||
|
||||
#### 出参 `Result<OrderCreateRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data.id` | Long | 订单主键(雪花 ID 序列化为 String) |
|
||||
| `data.orderNo` | String | 订单号(创单瞬间生成,永不变) |
|
||||
| `data.orderStatus` | String | 粗状态枚举(创单后 = PENDING_PAY) |
|
||||
| `data.orderStatusName` | String | 粗状态中文名(创单后 = 待支付) |
|
||||
| `data.flowStatus` | String | 细状态枚举值(创单后 = AWAITING_PAY) |
|
||||
| `data.flowStatusName` | String | 细状态中文名 |
|
||||
| `data.flowStep` | Integer | 线性 6 步当前步序号(创单初态 = 0) |
|
||||
| `data.flowStepTotal` | Integer | 线性 6 步总步数(固定 6) |
|
||||
| `data.flowStepName` | String | 线性 6 步当前步中文名 |
|
||||
| `data.consultantId` | Long | 实际绑定定制师 ID(序列化为 String) |
|
||||
| `data.consultantSource` | String | 定制师来源(DEFAULT_ASSIGNED / LINK_BOUND / MANUAL / SHARED) |
|
||||
| `data.tags` | List<String> | 系统自动打的标签 |
|
||||
| `data.createdAt` | yyyy-MM-ddTHH:mm:ss | 创单时间 |
|
||||
| `data.productName` | String | 产品名称 |
|
||||
| `data.tierName` | String | 档位名(v5.18 新增,**GROUP 单现有值来自产品 tiers 配置,CORE/CUSTOM/ROUTE 仍为 null**) |
|
||||
| `data.groupBatchName` | String | 拼团批次名(仅供兼容,恒 null,勿读) |
|
||||
| `data.departureDate` | yyyy-MM-dd | **出发日期(v5.18;改后 = 班期权威出发日,与请求值可能不同)** |
|
||||
| `data.returnDate` | yyyy-MM-dd | **返团日期(v5.18;改后 = 班期 endDate 或班期出发日 + 行程天数 − 1,与请求值可能不同)** |
|
||||
| `data.totalAmount` | BigDecimal(String) | 订单总价(元) |
|
||||
| `data.depositAmount` | BigDecimal(String) | 建议定金金额(元) |
|
||||
| `data.depositRatio` | Integer | 定金比例百分比;RATIO 模式有值,FIXED 模式为 null,FULL 模式 = 100 |
|
||||
| `data.depositMode` | String | 定金计算模式(FIXED / RATIO / FULL) |
|
||||
| `data.paymentMode` | String | 支付模式(DEPOSIT / FULL) |
|
||||
| `data.expiryMinutes` | Integer | 支付时限分钟数(默认 1440 = 24h) |
|
||||
| `data.payUrl` | String | 支付页绝对 URL |
|
||||
| `data.customerName` | String | 客户姓名(回显) |
|
||||
| `data.groupBatchId` | Long(String) | **运营团期 ID(order 侧),非空 = 团订单,为空 = 普通订单,这是唯一判别** |
|
||||
| `data.productBatchId` | Long(String) | 团期产品排期 ID(product 侧 batchId,仅供溯源) |
|
||||
| `data.groupOrder` | Boolean | 是否团订单(= groupBatchId 非空的派生值) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"productId": "2044306857534636034",
|
||||
"tierSeq": 1,
|
||||
"departureDate": "2026-10-01",
|
||||
"adultCount": 1,
|
||||
"childCount": 1,
|
||||
"youngChildCount": 0,
|
||||
"babyCount": 0,
|
||||
"customerName": "张三",
|
||||
"customerPhone": "13800009601",
|
||||
"createSource": "CONSULTANT",
|
||||
"productBatchId": "2052935476557328386"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例(成功)
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"id": "2096414445365338113",
|
||||
"orderNo": "HL20260906094527867",
|
||||
"orderStatus": "PENDING_PAY",
|
||||
"orderStatusName": "待支付",
|
||||
"flowStatus": "AWAITING_PAY",
|
||||
"flowStatusName": "待支付",
|
||||
"flowStep": 0,
|
||||
"flowStepTotal": 6,
|
||||
"flowStepName": "待支付",
|
||||
"consultantId": "50001234567890",
|
||||
"consultantSource": "DEFAULT_ASSIGNED",
|
||||
"tags": [],
|
||||
"createdAt": "2026-09-06T09:45:27",
|
||||
"productName": "冻干粉发短信给",
|
||||
"tierName": "标准档",
|
||||
"groupBatchName": null,
|
||||
"departureDate": "2026-10-01",
|
||||
"returnDate": "2026-10-03",
|
||||
"totalAmount": "5850.00",
|
||||
"depositAmount": "1000.00",
|
||||
"depositRatio": null,
|
||||
"depositMode": "FIXED",
|
||||
"paymentMode": "DEPOSIT",
|
||||
"expiryMinutes": 1440,
|
||||
"payUrl": "https://pay.hulalv.com/pay/HL20260906094527867",
|
||||
"customerName": "张三",
|
||||
"groupBatchId": "2096412454643802114",
|
||||
"productBatchId": "2052935476557328386",
|
||||
"groupOrder": true
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
创建接口无「空数据」场景(成功即返回订单对象)。降级路径:所依赖的产品服务 Feign 不可用时按错误码降级而非返回空——拉班期失败返 581027、拉报价失败返 581032,前端据 `code` 提示重试,不会返回 `data=null` 的成功包。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
**出发日期早于今天**
|
||||
```json
|
||||
{
|
||||
"code": 581011,
|
||||
"message": "出发日期不能早于今天",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
**档位不存在(tierSeq 未在产品配置内)**
|
||||
```json
|
||||
{
|
||||
"code": 581057,
|
||||
"message": "所选档位不存在",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
**非团期产品不能指定团期**
|
||||
```json
|
||||
{
|
||||
"code": 581056,
|
||||
"message": "非团期产品不能指定团期",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
**所选团期不属于该产品**
|
||||
```json
|
||||
{
|
||||
"code": 581055,
|
||||
"message": "所选团期不属于该产品",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
**既有 GROUP 校验错误(团期缺失)**
|
||||
```json
|
||||
{
|
||||
"code": 581026,
|
||||
"message": "团期产品必须选择团期",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
**参数校验错误(如手机号格式错误)**
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "手机号格式错误",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **productBatchId 传值规则**:仅 GROUP 产品可传且必传(除非为空表示不下单);CORE/CUSTOM/ROUTE 产品绝不能带。
|
||||
- **班期归属校验**:productBatchId 对应班期的 productId 必须等于请求 productId;不同则拒单 581055,不会创建订单或部分落库。
|
||||
- **tierSeq 校验**:必须存在于产品的 tierPrices JSON(CORE/CUSTOM/ROUTE)或 tiers JSON(GROUP);产品未配任何档位时保持放行(存量产品兼容)。
|
||||
- **出发日期与返团日期**:响应值为班期或产品的权威日期,可能与请求值不同。前端展示及后续行程渲染必须以响应值为准,不要缓存请求值。
|
||||
- **错误码优先级顺序**:出发日期 581011 → 非 GROUP 拒收 581056 → tierSeq 581057 → GROUP 缺 productBatchId 581026 → 拉班期 581027 → 班期归属 581055 → 后续班期状态 / 截止 / 库存检查。
|
||||
- **鉴权**:网关注入 `X-Admin-Id` 头,consultantId 无法解析返 581013。
|
||||
- **幂等性**:客户端不得重试;同一 orderNo 存量幂等托管由 order-v3 内核保证。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
| 场景 | 正确调用 | 错误调用 | 结果 |
|
||||
|------|---------|---------|------|
|
||||
| GROUP 产品创单 | 传 productBatchId,值取价格日历 items[].batchId | 不传 productBatchId | 581026 团期产品必须选择团期 |
|
||||
| CORE 产品创单 | 不传 productBatchId / productBatchId = null | 传 productBatchId(任何值) | 581056 非团期产品不能指定团期 |
|
||||
| CUSTOM 产品创单 | 不传 productBatchId / productBatchId = null | 传 productBatchId(任何值) | 581056 非团期产品不能指定团期 |
|
||||
| 档位选择 | tierSeq 必须在产品已配档位内 | tierSeq 超过产品最大档位序号 | 581057 所选档位不存在 |
|
||||
| 班期串号防护 | 班期 batchId 必须属于所选 productId | 前端用不同产品的 batchId | 581055 所选团期不属于该产品 |
|
||||
| 日期展示 | 使用响应 departureDate / returnDate | 使用请求的出发日期 | 行程日期与班期脱钩,退款档位错位 |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
| 操作 | 落库字段 | 说明 |
|
||||
|------|---------|------|
|
||||
| GROUP 创单(班期出发 2026-10-01,endDate 2026-10-03,tripDays=3) | order_main.depart_date = 2026-10-01(班期值) | 请求可能为 2026-10-02,但落库为班期出发日 |
|
||||
| GROUP 创单(班期无 endDate) | order_main.return_date = 班期出发日 + tripDays - 1 | 如班期 2026-10-01,tripDays=3,则 return_date=2026-10-03 |
|
||||
| GROUP 创单(班期有 endDate) | order_main.return_date = 班期 endDate | endDate 为准,与 tripDays 无关 |
|
||||
| GROUP 创单 | order_main.tier_name = 产品 tiers JSON 对应 tierSeq 的值 | 改前恒 null;CORE/CUSTOM 仍为 null(无 tiers JSON) |
|
||||
| CORE 创单带 productBatchId | 不创建,拒单 581056 | 改前会落库 productBatchId,误命中团期逻辑 |
|
||||
| 班期串号 productBatchId 错 | 不创建,拒单 581055 | 改前会按班期产品计价,订单挂错团 |
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 业务失败仍 HTTP 200,**必须检查 `code` 字段判成败**。
|
||||
- 存量订单不回溯修正;仅新建订单应用新规则。
|
||||
- 产品未配档位时保持放行(向后兼容存量产品);即使 tierSeq 传 99 也不拦。
|
||||
- 班期 productId 缺失时仅告警日志,不拦单(product 侧老数据未回填,宁缺毋滥)。
|
||||
- 网关注入 `X-Admin-Id` 为 null 时返 581013,改前静默取 JWT;两者互斥。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
针对 `POST /v3/admin/order`(GROUP 产品):
|
||||
|
||||
| 维度 | 修改前 | 修改后 |
|
||||
|------|--------|--------|
|
||||
| CORE/CUSTOM 带 productBatchId | 静默落库 product_batch_id,误命中团期 staff 扇出分支 | 拒单 581056「非团期产品不能指定团期」,不落库 |
|
||||
| 跨产品班期(batchId 属别的产品) | 按别家班期计价,订单挂错团 | 拒单 581055「所选团期不属于该产品」(任一侧 productId 为空时仅告警放行) |
|
||||
| tierSeq 不在产品档位内 | 不校验,tier_name 落 NULL | 拒单 581057「所选档位不存在」(产品未配档位时不拦) |
|
||||
| GROUP 单响应/落库出发日 | 回显请求出发日,可能与班期脱钩 | = 班期出发日 |
|
||||
| GROUP 单响应/落库返团日 | 按请求出发日推算 | = 班期 endDate(缺则 班期出发日 + 行程天数 − 1) |
|
||||
| GROUP 单响应 tierName | 恒 null | 现有值(来自产品 tiers 配置) |
|
||||
| 人数超班期剩余名额(#7159) | 预查读 remainingSlots 恒 null,整段 no-op(不拦) | 读 remainingParticipants,超额拒单 581034(人数不限的班期跳过) |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **前端必改**:新建订单向导与团期看板「新增子订单」仅对 GROUP 产品传 `productBatchId`,且取该产品价格日历的 `items[].batchId`;CORE/CUSTOM 绝不能带,否则 581056。
|
||||
- **前端展示**:出发日期 / 返团日期以创单响应值为准(GROUP 单被班期覆盖),不要回显请求值。
|
||||
- **向后兼容**:正常 CORE/CUSTOM 创单(不带 productBatchId)行为完全不变;错误码新增不影响既有成功路径。
|
||||
- **其他端**:小程序 `POST /v3/mp/order` 共用内核,四个校验同样生效,请求契约不变。
|
||||
- **数据安全**:跨产品串号单此前会把订单挂到别家团、扣错名额,本次从源头拒绝。
|
||||
- **回滚**:回滚本次发布即恢复旧行为;已按新规则创建的订单不受影响。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**:管理后台「新建订单向导」和「团期看板新增子订单」流程。
|
||||
- **零影响**:
|
||||
- 小程序端 `POST /v3/mp/order` 共用内核,新校验同样生效但请求契约不变。
|
||||
- 表结构:无 Flyway 变更、无新列新表。
|
||||
- 订单列表、详情、订单编辑等读操作。
|
||||
- CORE/CUSTOM 正常创单(不带 productBatchId)完全不变。
|
||||
- 团期相关接口(看板、价格日历、班期详情)。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**测试服创单(产品「冻干粉发短信给」productId=2044306857534636034,班期 2026-10-01 productBatchId=2052935476557328386,1 成人 1 儿童,2026-09-06 测试)**
|
||||
|
||||
- ✓ GROUP 单正例:响应 departureDate/returnDate 与班期一致(班期 2026-10-01 出发、2026-10-03 返);groupBatchId / productBatchId / groupOrder 三字段已填值。
|
||||
- ✓ CORE 产品 2056944461216100353 带 productBatchId → HTTP 200,code 581056「非团期产品不能指定团期」。
|
||||
- ✓ productId=2044306857534636034 + productBatchId=2089667212070612995(属另一产品) → HTTP 200,code 581055「所选团期不属于该产品」。
|
||||
- ✓ tierSeq=9(产品只配 1-3 档) → HTTP 200,code 581057「所选档位不存在」。
|
||||
- ✓ GROUP 单不传 productBatchId → HTTP 200,code 581026「团期产品必须选择团期」。
|
||||
|
||||
**单测覆盖**(定向复测全绿):OrderServiceTest 193/193 ✓ | GroupOrderStrategyTest 30/30 ✓(含 581034 三例)| OrderCreateTransactionExecutorTest 21/21 ✓ | ProductTierResolverTest 11/11 ✓ | OrderMpCreateServiceTest 6/6 ✓ | BatchInfoVODeserializationTest 2/2 ✓ | OrderMpReadServiceTest 15/15 ✓ | InternalOrderSnapshotControllerTest 4/4 ✓ | E2eScopedOrderCreateServiceTest 19/19 ✓;ArchTest 全绿(LayerEnforcement 5 / RedLine 9 / MapperBoundary 26 / HouseModuleBoundary 4 / DashboardLayer 2 / LocalCacheVetting 1)。
|
||||
|
||||
**网关验证(已部署 dev-v3 复测,2026-09-06 15:00)**:合并提交 977be08e 部署测试服,双实例滚动重启完成(8086/8186 新 PID、jar 已更新)。网关实测 15/15 通过:S2 正例 200(日期=班期 10-01/10-03)、S10 跨产品班期 581055、S11 日期不一致回显班期日期、S13 tierSeq=9 581057、S14 CORE 带班期 581056、S16 GROUP tierName=轻奢;#7142 判团字段在创单响应/详情/列表三处透出、#7143 productId 在看板/分页/详情三接口透出,均已核。581034(#7159)判定逻辑经三条单测证实生效;线上端到端受测试账号对产品 schedule 无编辑权限(403)与限额班期 getBatchInfo 存量异常(581027)所限未实跑,详见 Issue #7159 评论。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#7135](https://git.1814.love:8443/wx/HL/issues/7135)
|
||||
- 关联 PR: [wx/HL#7169](https://git.1814.love:8443/wx/HL/pulls/7169)(Closes #7135、#7159;合并提交 977be08e)
|
||||
- 关联工单 #7142(判团字段 groupBatchId / productBatchId / groupOrder)、#7143(看板 productId 显示)。
|
||||
- 团期接口文档:GB-ADM-00B(OpenWiki)。
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#7135](https://git.1814.love:8443/wx/HL/issues/7135)
|
||||
- **PR**: [#7169](https://git.1814.love:8443/wx/HL/pulls/7169)(含 #7159)
|
||||
- **Merge commit**: 977be08eccd0a32e27c01dce41de210a53f13e5d
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7137"
|
||||
title: "供应商账户信息列表状态显示中文"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "前端缺陷"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "9d8115ed"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-06"
|
||||
status_note: "本单仅做后端验收;前端中文映射独立处理,不作为关单条件。"
|
||||
updated_at: "2026-09-06"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商:账户信息列表状态显示中文
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 账户信息列表 | GET | `/admin/supplier/items/{supplierId}/account-info/list` | 无接口变更 | 补充状态中文展示映射 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 账户信息列表 `GET /admin/supplier/items/{supplierId}/account-info/list`
|
||||
|
||||
- 打开供应商详情的“账户信息”时调用;`supplierId` 为路径参数,无请求体。
|
||||
- 冻结字段:`data.bankAccounts[].status`,类型 `String`,值为账户状态码。
|
||||
- 成功须同时满足 HTTP 200、`code=200`、`success=true`。请求、响应和错误码均无变更。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
| 状态码 | 中文展示 | 值语义 |
|
||||
|---|---|---|
|
||||
| `DRAFT` | 草稿 | 初始账户尚未提交审批 |
|
||||
| `PENDING` | 待审批 | 账户审批处理中 |
|
||||
| `REJECTED` | 已驳回 | 审批驳回,账户不可用 |
|
||||
| `ACTIVE` | 有效 | 账户已审批生效 |
|
||||
| `DISABLED` | 已停用 | 账户停用 |
|
||||
|
||||
## 前端动作与边界
|
||||
|
||||
状态列按上述状态码显示中文,补齐 `DRAFT → 草稿`。业务判断继续使用原状态码;不把中文写回 `status`,不依赖账户响应中不存在的 `statusName`。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
账户列表经 TEST Gateway 返回 HTTP 200、`code=200`、`success=true`。目标供应商当前账户为 `ACTIVE`;草稿账户按既有后端契约返回 `DRAFT`,中文语义为“草稿”。
|
||||
|
||||
## 当前状态
|
||||
|
||||
前端中文映射独立待处理。#7137 按后端接口和状态契约验收,不要求前端页面验收。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- 工单:[HL #7137](https://git.1814.love:8443/wx/HL/issues/7137)
|
||||
- 后端负责人:@lc
|
||||
@@ -0,0 +1,509 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7142"
|
||||
title: "订单详情/列表/创单响应透出 groupBatchId·productBatchId·groupOrder 判团字段"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: ""
|
||||
updated_at: "2026-09-06"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 订单模块:判团字段透出(详情·列表·创单)
|
||||
|
||||
三个订单读接口响应透出判团字段 `groupBatchId` / `productBatchId` / `groupOrder`,供前端判断订单是否为团订单。关键口径:**判团只读 `groupBatchId`(非空=团订单)或 `groupOrder`**;`productBatchId` 仅供溯源,不参与判团。
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
- **判团唯一口径** `groupBatchId` 非空或 `groupOrder = true` → 团订单;为空/false → 普通订单。
|
||||
- **不要拿 `productBatchId` 反推团单**,该字段仅供产品侧班期溯源展示,产品侧可能有多个班期映射同一团期。
|
||||
- 前一版 #7083 仅涉及订单主表冻结,本次全量透出给前端消费,与主表字段同源。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 订单详情 - 主单数据 | GET | `/v3/admin/order/{id}` | 响应新增字段 | data.main 新增 4 字段 |
|
||||
| 2 | 订单分页列表 | GET | `/v3/admin/order` | 响应新增字段 | data.list[] 新增 2 字段 |
|
||||
| 3 | 创建订单 | POST | `/v3/admin/order` | 响应新增字段 | data 新增 3 字段 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 订单详情 - 主单数据 `GET /v3/admin/order/{id}`
|
||||
|
||||
**VO**: `OrderMainVO`(响应位置:`data.main`)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
打开订单详情页面时,读取订单主单数据及其判团标记,供前端决定显示团期相关内容(如「团期编号」、「团期名称」等)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `id` | Path | String | 是 | 正整数 ID | 订单 ID |
|
||||
|
||||
#### 出参 `Result<OrderDetailRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data.main.groupBatchId` | String/null | 运营团期主键(order_group_batch.group_batch_id);非空=团订单,为空=普通订单 |
|
||||
| `data.main.productBatchId` | String/null | 产品侧班期 ID(product_v2.group_tour_batch.batch_id);仅供溯源,不参与判团 |
|
||||
| `data.main.groupOrder` | Boolean | 派生值,= groupBatchId != null;直接用于前端判团 |
|
||||
| `data.main.batchNo` | String/null | 团期编号(order_group_batch.batch_no);普通单为 null,团期软删也为 null |
|
||||
| `data.main.batchName` | String/null | 团期名称(order_group_batch.batch_name);普通单为 null,团期软删也为 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/2096414445365338113
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"main": {
|
||||
"id": "2096414445365338113",
|
||||
"orderNo": "HL20260906094527867",
|
||||
"orderStatus": "PENDING_PAY",
|
||||
"orderStatusName": "待支付",
|
||||
"flowStatus": "AWAITING_PAY",
|
||||
"flowStatusName": "待补全信息",
|
||||
"flowStep": 0,
|
||||
"flowStepTotal": 6,
|
||||
"totalAmount": "5850.00",
|
||||
"depositAmount": "1000.00",
|
||||
"departureDate": "2026-10-01",
|
||||
"returnDate": "2026-10-03",
|
||||
"groupBatchId": "2096412454643802114",
|
||||
"productBatchId": "2052935476557328386",
|
||||
"groupOrder": true,
|
||||
"batchNo": "Q202610012052935476548939777",
|
||||
"batchName": "10月1日长白山亲子团",
|
||||
"progressStepper": []
|
||||
},
|
||||
"profile": {},
|
||||
"resource": {},
|
||||
"contract": {},
|
||||
"insurance": {},
|
||||
"refund": {},
|
||||
"aftersale": {},
|
||||
"financial": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
普通订单时,`groupBatchId`、`productBatchId`、`batchNo`、`batchName` 均为 null;`groupOrder` 为 false。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 581007,
|
||||
"message": "订单不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
示例错误码:
|
||||
- 581007:订单不存在
|
||||
- 581045:房务角色无权查看订单详情(HTTP 200 code)
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 沿用订单详情权限校验;业务失败仍为 HTTP 200,需检查 code。
|
||||
- 普通订单与团单在出参结构上无区别,仅字段值不同(null vs 有值)。
|
||||
- 团期已软删时,`groupBatchId` 存在但对应记录不可查,`batchNo` / `batchName` 回退为 null。
|
||||
- `productBatchId` 与 `groupBatchId` 无必然对应,前端不做交叉校验。
|
||||
|
||||
---
|
||||
|
||||
### 2. 订单分页列表 `GET /v3/admin/order`
|
||||
|
||||
别名接口:`GET /v3/admin/order/list`
|
||||
|
||||
**VO**: `OrderListItemRespVO`(响应位置:`data.list[]`)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
订单列表页加载数据时,带上新增的判团标记,供前端快速判断每行是否为团订单,可用于条件展示「团期信息」列或其他团期特化功能。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `orderStatus` | Query | String | 否 | 枚举多值(逗号分隔) | 粗状态过滤(PENDING_PAY、CUSTOMIZING 等) |
|
||||
| `flowStatus` | Query | String | 否 | 枚举多值(逗号分隔) | 细状态过滤(AWAITING_PAY、RESOURCE_PREPARING 等) |
|
||||
| `tagNames` | Query | Array | 否 | - | 按标签过滤(多标签 OR 关系) |
|
||||
| `keyword` | Query | String | 否 | - | 搜索关键字(团号/客户姓名/产品名/订单号 LIKE) |
|
||||
| `departureDateFrom` | Query | String | 否 | yyyy-MM-dd | 出发日期范围起始 |
|
||||
| `departureDateTo` | Query | String | 否 | yyyy-MM-dd | 出发日期范围结束 |
|
||||
| `createSource` | Query | String | 否 | - | 来源过滤(CONSULTANT/MP/...) |
|
||||
| `cancelled` | Query | Boolean | 否 | - | 是否含已取消订单(默认 false) |
|
||||
| `consultantName` | Query | String | 否 | - | 定制师姓名 LIKE 模糊匹配 |
|
||||
| `statusGroup` | Query | String | 否 | 枚举单值 | 按 Tab 分组(ALL/BEFORE_TRIP/ON_TRIP/SETTLEMENT/ABNORMAL/AFTERSALE) |
|
||||
| `page` | Query | Integer | 是 | ≥1 | 页码 |
|
||||
| `pageSize` | Query | Integer | 是 | ≤100 | 每页条数 |
|
||||
|
||||
#### 出参 `Result<Page<OrderListItemRespVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data.list[].groupBatchId` | String/null | 运营团期主键(order_group_batch.group_batch_id);非空=团订单 |
|
||||
| `data.list[].groupOrder` | Boolean | 派生值,= groupBatchId != null;直接用于前端判团 |
|
||||
|
||||
其他字段详见现有订单列表接口文档。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order?page=1&pageSize=20
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"total": 150,
|
||||
"list": [
|
||||
{
|
||||
"id": "2096414445365338113",
|
||||
"orderNo": "HL20260906094527867",
|
||||
"productName": "冻干粉发短信给",
|
||||
"customerName": "张三",
|
||||
"departureDate": "2026-10-01",
|
||||
"orderStatus": "PENDING_PAY",
|
||||
"flowStatus": "AWAITING_PAY",
|
||||
"totalAmount": "5850.00",
|
||||
"groupBatchId": "2096412454643802114",
|
||||
"groupOrder": true
|
||||
},
|
||||
{
|
||||
"id": "2096414445365338114",
|
||||
"orderNo": "HL20260906094527868",
|
||||
"productName": "其他产品",
|
||||
"customerName": "李四",
|
||||
"departureDate": "2026-10-02",
|
||||
"orderStatus": "PENDING_DEPARTURE",
|
||||
"flowStatus": "PENDING_DEPARTURE",
|
||||
"totalAmount": "8000.00",
|
||||
"groupBatchId": null,
|
||||
"groupOrder": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"total": 0,
|
||||
"list": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "未登录或登录已过期",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 分页字段 `page` / `pageSize` 沿用现有约束。
|
||||
- 列表返回最新 50 条或 100 条时,两个新增字段保证同时返回,不存在部分返回的情况。
|
||||
- `groupOrder` 是 `groupBatchId != null` 的派生布尔值,前端可二选一使用。
|
||||
- 普通订单与团单混合返回,字段值直接对标订单属性。
|
||||
|
||||
---
|
||||
|
||||
### 3. 创建订单 `POST /v3/admin/order`
|
||||
|
||||
**VO**: `OrderCreateRespVO`(响应位置:`data`)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
创建订单后,管理端「订单已创建」弹窗或后续流程需判断该单是否为团单,及时显示团期相关信息(如「团期编号」、「出发日期」等)。
|
||||
|
||||
#### 入参
|
||||
|
||||
沿用现有 `OrderCreateReqVO`,**请求体不变**(本单只改响应)。字段表:
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `productId` | Body | String(Long) | ✅ | 正整数 | 产品 ID |
|
||||
| `tierSeq` | Body | Integer | ✅ | ≥1 | 档位序号 |
|
||||
| `departureDate` | Body | String(yyyy-MM-dd) | ✅ | 不早于今天 | 出发日期 |
|
||||
| `adultCount` | Body | Integer | ✅ | ≥1 | 成人数 |
|
||||
| `childCount` | Body | Integer | 否 | ≥0,默认 0 | 儿童数 |
|
||||
| `youngChildCount` | Body | Integer | 否 | ≥0,默认 0 | 小童数 |
|
||||
| `babyCount` | Body | Integer | 否 | ≥0,默认 0 | 婴儿数 |
|
||||
| `customerName` | Body | String | ✅ | @NotBlank | 客户姓名 |
|
||||
| `customerPhone` | Body | String | ✅ | ^1[3-9]\d{9}$ | 客户手机号 |
|
||||
| `customerRemark` | Body | String | 否 | ≤500 | 备注 |
|
||||
| `createSource` | Body | String | 否 | 默认 CONSULTANT | 创建来源 |
|
||||
| `productBatchId` | Body | String(Long) | 否 | GROUP 必传、非 GROUP 禁传 | 团期 ID(见 #7135) |
|
||||
| `roomCount` | Body | Integer | 否 | ≥1 | 房间数 |
|
||||
| `tags` | Body | Array<String> | 否 | — | 订单标签 |
|
||||
|
||||
#### 出参 `Result<OrderCreateRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data.id` | String | 订单主键 |
|
||||
| `data.orderNo` | String | 订单号 |
|
||||
| `data.orderStatus` | String | 粗状态(创单后为 PENDING_PAY) |
|
||||
| `data.flowStatus` | String | 细状态(创单后为 AWAITING_PAY) |
|
||||
| `data.flowStep` | Integer | 线性步序(创单初态为 0) |
|
||||
| `data.flowStepTotal` | Integer | 总步数(固定 6) |
|
||||
| `data.productName` | String | 产品名 |
|
||||
| `data.tierName` | String | 档位名 |
|
||||
| `data.departureDate` | String | 出发日期 |
|
||||
| `data.returnDate` | String | 返团日期 |
|
||||
| `data.totalAmount` | String | 订单总价 |
|
||||
| `data.depositAmount` | String | 建议定金金额 |
|
||||
| `data.depositRatio` | Integer/null | 定金比例百分比 |
|
||||
| `data.depositMode` | String | 定金计算模式(FIXED/RATIO/FULL) |
|
||||
| `data.paymentMode` | String | 支付模式(DEPOSIT/FULL) |
|
||||
| `data.groupBatchId` | String/null | 运营团期 ID(创单同事务回写);非空=团订单 |
|
||||
| `data.productBatchId` | String/null | 产品侧班期 ID(创单入参原样固化);仅供溯源 |
|
||||
| `data.groupOrder` | Boolean | 派生值,= groupBatchId != null;直接用于判团 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"productId": 2044306857534636034,
|
||||
"tierSeq": 1,
|
||||
"departureDate": "2026-10-01",
|
||||
"adultCount": 2,
|
||||
"childCount": 1,
|
||||
"customerName": "张三",
|
||||
"customerPhone": "13800138000",
|
||||
"productBatchId": 2052935476557328386,
|
||||
"createSource": "CONSULTANT"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "2096414445365338113",
|
||||
"orderNo": "HL20260906094527867",
|
||||
"orderStatus": "PENDING_PAY",
|
||||
"orderStatusName": "待支付",
|
||||
"flowStatus": "AWAITING_PAY",
|
||||
"flowStatusName": "待补全信息",
|
||||
"flowStep": 0,
|
||||
"flowStepTotal": 6,
|
||||
"flowStepName": "待支付",
|
||||
"productName": "冻干粉发短信给",
|
||||
"tierName": "经典档",
|
||||
"departureDate": "2026-10-01",
|
||||
"returnDate": "2026-10-03",
|
||||
"totalAmount": "5850.00",
|
||||
"depositAmount": "1000.00",
|
||||
"depositRatio": null,
|
||||
"depositMode": "FIXED",
|
||||
"paymentMode": "DEPOSIT",
|
||||
"expiryMinutes": 1440,
|
||||
"payUrl": "https://pay.hulalv.com/pay/HL20260906094527867",
|
||||
"customerName": "张三",
|
||||
"groupBatchId": "2096412454643802114",
|
||||
"productBatchId": "2052935476557328386",
|
||||
"groupOrder": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
实测数据示例(2026-09-06 测试服):
|
||||
- 产品「冻干粉发短信给」productId=2044306857534636034
|
||||
- 班期 2026-10-01 productBatchId=2052935476557328386
|
||||
- 团期主键 groupBatchId=2096412454643802114
|
||||
- 团期编号 batchNo=Q202610012052935476548939777
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
创建订单成功后无空数据响应。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "产品 ID 非法或产品已下架",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 创建普通订单时,`groupBatchId` / `productBatchId` 为 null,`groupOrder` 为 false。
|
||||
- 创建 GROUP 产品订单时(必须提交 `productBatchId`),后端在同事务内懒创建或命中已有团期,回写 `groupBatchId`。
|
||||
- `groupBatchName` 字段当前恒为 null(仅为兼容既有前端契约),**前端不要读它**。
|
||||
- 响应中 `groupBatchId` / `productBatchId` 为字符串(JSON 序列化后,避免 JS 精度丢失);前端若需数值运算应转换为字符串存储。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
| 场景 | 正确做法 |
|
||||
|------|---------|
|
||||
| 判断订单是否团单 | 读 `groupBatchId` 非空 或 `groupOrder == true`,两者等价 |
|
||||
| 不要用 productBatchId 判团 | `productBatchId` 仅供溯源,可能 null(普通单)或有值(团单/非团单均可能) |
|
||||
| 团单需显示团期名 | `groupBatchName` 恒为 null,读 `batchName`;团期软删时也为 null |
|
||||
| 普通单与团单混合渲染 | 按 `groupOrder` 条件渲染,普通单该字段为 false;两类订单出参结构一致,仅值不同 |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
| 订单类型 | groupBatchId | productBatchId | groupOrder | batchNo | batchName |
|
||||
|---------|-------------|----------------|-----------|---------|-----------|
|
||||
| 普通订单 | null | null | false | null | null |
|
||||
| 团单(命中或懒建) | 非空 | 非空 | true | 有值 | 有值 |
|
||||
| 团期已软删 | 非空 | 非空 | true | null | null |
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 业务失败可能仍为 HTTP 200,必须同时检查 `code`、`success` 和 `message`。
|
||||
- 详情接口 404/权限 403 时直接返回对应 HTTP 状态码;业务类失败(如订单状态不符)返回 HTTP 200 + code。
|
||||
- 列表空结果返回 `total=0, list=[]`,分页参数超界时返回空列表(无 5XX)。
|
||||
- 创单失败不落库,响应 HTTP 200 + code,data 为 null。
|
||||
- 新增判团字段与现有字段同源、同时刷新,无时间差。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `groupBatchId` | 无此字段 | 新增;运营团期主键 |
|
||||
| `productBatchId` | 无此字段 | 新增;产品班期 ID(仅溯源) |
|
||||
| `groupOrder` | 无此字段 | 新增;派生布尔,= groupBatchId != null |
|
||||
| `batchNo` | 无此字段 | 新增;团期编号(详情/列表) |
|
||||
| `batchName` | 无此字段 | 新增;团期名称(详情/列表) |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 前端判团依据 | 无判团字段,无法直接判别 | 读 groupBatchId 非空 或 groupOrder = true |
|
||||
| 团单信息展示 | 依赖联查或额外接口 | 直接在订单响应中获得 |
|
||||
| 产品班期溯源 | 不支持 | 新增 productBatchId(仅溯源展示) |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否。新增字段对旧客户端透明,非必需字段缺失时前端框架可靠 null 处理。
|
||||
- **前端是否必须同步上线**: 是。前端需接入新增四个字段至详情/列表/创建成功弹窗模板,判团逻辑改用 groupBatchId 或 groupOrder。
|
||||
- **前端 workaround 清理点**:
|
||||
- 删除旧的"通过产品 ID 推断团单"逻辑,改用 groupBatchId 判别
|
||||
- 不要硬编码团期编号/名称,改用响应中的 batchNo / batchName
|
||||
- groupBatchName 恒为 null,勿读之;用 batchName 替代
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台订单详情、列表和创建流程的前端渲染
|
||||
- **零影响**:
|
||||
- 订单创建/编辑/取消接口
|
||||
- 小程序端(MpOrderDetailVO 不变)
|
||||
- 数据库结构(新字段冻结在 order_main 表,无表改动)
|
||||
- 订单写操作和业务流程
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- **单测**: 559 条测试用例绿✓(新增判团字段相关的 UT 已覆盖普通单/团单双路径)
|
||||
- **ArchTest**: 45 条架构测试绿✓
|
||||
- **测试服网关实测**: 单测 + CR 通过;测试服网关实测见管理者补充
|
||||
|
||||
实测产品: `productId=2044306857534636034`(冻干粉发短信给),班期 `2026-10-01`(productBatchId=2052935476557328386),团期主键 `groupBatchId=2096412454643802114`,批号 `batchNo=Q202610012052935476548939777`。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR(功能演进)
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| #7083 | - | order_group_batch 一跳直连,订单主表冻结 groupBatchId/productBatchId | ✅ 有效 |
|
||||
| #7135 | - | GROUP 产品创单校验与重整 | ✅ 有效 |
|
||||
| #7143 | - | 团期看板 VO 补 productId(配合本 PR) | ✅ 有效 |
|
||||
| **本 PR #7155** | **#7142** | **订单详情/列表/创单响应透出判团字段** | ✅ 最新 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#7142](https://git.1814.love:8443/wx/HL/issues/7142)
|
||||
- 关联 PR: [wx/HL#7155](https://git.1814.love:8443/wx/HL/pulls/7155)
|
||||
- 团期接口文档: `docs/ARCHITECTURE.md` §0A.2.2(团单冻结口径)
|
||||
- 团单业务规范: 见 #7135 changelog 中的团期创建规则
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#7142](https://git.1814.love:8443/wx/HL/issues/7142)
|
||||
- **PR**: [#7155](https://git.1814.love:8443/wx/HL/pulls/7155)
|
||||
- **Merge commit**: [21d3d00de](https://git.1814.love:8443/wx/HL/commit/21d3d00de)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,573 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7143"
|
||||
title: "团期看板分页项/详情/看板 VO 补 productId(供新增子订单深链预填产品)"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: ""
|
||||
updated_at: "2026-09-06"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期模块:看板 VO 补 productId
|
||||
|
||||
三个团期看板读接口响应新增 `productId` 字段,供前端「新增子订单」深链到订单创建向导时预填产品和班期 ID,锁定出发日期。前端逻辑:**仅 GROUP 产品创单时才在 payload 中带 productBatchId**;非 GROUP 产品忽略 productBatchId。
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
- 新增 `productId` 是产品主键,用于深链向导 `/order-v2/new?productId={productId}&productBatchId={productBatchId}&departureDate={date}` 的预填参数。
|
||||
- 向导跳转后,**仅 GROUP 产品**应将 productBatchId 放入订单创建 POST payload;其他产品类型忽略该参数。
|
||||
- 前端缺陷单已发,说明旧建单向导漏传 productBatchId,新版本补全(见关联链接)。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 团期分页列表 | GET | `/v3/admin/order/group-batch` | 响应新增字段 | data.list[] 各项新增 productId |
|
||||
| 2 | 团期详情 | GET | `/v3/admin/order/group-batch/{groupBatchId}` | 响应新增字段 | data 新增 productId |
|
||||
| 3 | 团期看板列表 | GET | `/v3/admin/order/group-batch/board?productId=` | 响应新增字段 | data[] 各项新增 productId |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 团期分页列表 `GET /v3/admin/order/group-batch`
|
||||
|
||||
**VO**: `GroupBatchPageItemRespVO`(响应位置:`data.list[]`)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
加载团期分页列表时,每一行包含产品 ID,前端点击「新增子订单」时取该行的 productId / productBatchId / departureDate 拼接深链,跳转到订单创建向导。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `productId` | Query | String | 否 | 正整数 ID | 按产品筛选 |
|
||||
| `batchStatus` | Query | String | 否 | 枚举值 | 按团期状态码筛选(RECRUITING/RESOURCE_PREPARING/...) |
|
||||
| `deadlineFrom` | Query | String | 否 | yyyy-MM-dd | 报名截止日起 |
|
||||
| `deadlineTo` | Query | String | 否 | yyyy-MM-dd | 报名截止日止 |
|
||||
| `opsStage` | Query | String | 否 | 枚举值 | 按运营阶段筛选(RECRUIT/FORMED/PENDING_TRIP/TRAVELLING/AUDITING/CHECKED/DISBANDED) |
|
||||
| `month` | Query | String | 否 | yyyy-MM | 按出发月份筛选 |
|
||||
| `keyword` | Query | String | 否 | - | 班期编号/班期名称模糊关键词 |
|
||||
| `pageNo` | Query | Integer | 是 | ≥1 | 页码,从 1 开始 |
|
||||
| `pageSize` | Query | Integer | 是 | ≤100 | 每页条数,默认 20 |
|
||||
|
||||
#### 出参 `Result<Page<GroupBatchPageItemRespVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data.list[].groupBatchId` | String | 团期聚合主键 |
|
||||
| `data.list[].productBatchId` | String | product 侧班期 ID |
|
||||
| `data.list[].productId` | String | **【新增】** 产品 ID,供深链预填 |
|
||||
| `data.list[].productName` | String | 产品名称 |
|
||||
| `data.list[].batchNo` | String | 班期编号 |
|
||||
| `data.list[].batchName` | String | 班期名称 |
|
||||
| `data.list[].batchStatus` | String | 团期状态码 |
|
||||
| `data.list[].batchStatusName` | String | 状态中文名 |
|
||||
| `data.list[].minGroupPeople` | Integer | 最低成团人数 |
|
||||
| `data.list[].maxRooms` | Integer | 房间容量 |
|
||||
| `data.list[].maxParticipants` | Integer | 人数容量 |
|
||||
| `data.list[].enrolledPeople` | Integer | 已报名人数 |
|
||||
| `data.list[].enrolledRooms` | Integer | 已用房间数 |
|
||||
| `data.list[].remainRooms` | Integer | 剩余房间数 |
|
||||
| `data.list[].remainParticipants` | Integer | 剩余人数 |
|
||||
| `data.list[].orderCount` | Integer | 子订单数 |
|
||||
| `data.list[].enrollDeadline` | String | 报名截止日 |
|
||||
| `data.list[].departDate` | String | 出发日期 |
|
||||
| `data.list[].endDate` | String | 结束日期 |
|
||||
| `data.list[].receivableAmount` | String | 整团应收合计 |
|
||||
| `data.list[].receivedAmount` | String | 整团已收合计 |
|
||||
| `data.list[].chips` | Object | 六芯片整团聚合态(hotel/vehicle/guide/photo/contract/insurance,各为字符串状态:全部完成为 `DONE`,存在未完成项为待办态;完整取值见六芯片文档 GB-ADM-090~095) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch?pageNo=1&pageSize=20
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"total": 5,
|
||||
"list": [
|
||||
{
|
||||
"groupBatchId": "2096412454643802114",
|
||||
"productBatchId": "2052935476557328386",
|
||||
"productId": "2044306857534636034",
|
||||
"productName": "冻干粉发短信给",
|
||||
"batchNo": "Q202610012052935476548939777",
|
||||
"batchName": "10月1日长白山亲子团",
|
||||
"batchStatus": "RECRUITING",
|
||||
"batchStatusName": "招募中",
|
||||
"minGroupPeople": 6,
|
||||
"maxRooms": 4,
|
||||
"maxParticipants": 10,
|
||||
"enrolledPeople": 4,
|
||||
"enrolledRooms": 2,
|
||||
"remainRooms": 2,
|
||||
"remainParticipants": 6,
|
||||
"orderCount": 2,
|
||||
"enrollDeadline": "2026-09-25",
|
||||
"departDate": "2026-10-01",
|
||||
"endDate": "2026-10-03",
|
||||
"receivableAmount": "48000.00",
|
||||
"receivedAmount": "36000.00",
|
||||
"chips": {
|
||||
"hotel": "DONE",
|
||||
"vehicle": "DONE",
|
||||
"guide": "DONE",
|
||||
"photo": "DONE",
|
||||
"contract": "DONE",
|
||||
"insurance": "DONE"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"total": 0,
|
||||
"list": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 沿用团期列表权限校验(团期管理员/运营/定制师等);无权返回 589507。
|
||||
- `productId` 与其他字段同时生效,始终非空(命中行/未命中行/孤儿行均返回)。
|
||||
- 分页参数超界时返回空列表。
|
||||
- 业务失败仍为 HTTP 200,需检查 code。
|
||||
|
||||
---
|
||||
|
||||
### 2. 团期详情 `GET /v3/admin/order/group-batch/{groupBatchId}`
|
||||
|
||||
**VO**: `GroupBatchDetailRespVO`(响应位置:`data`)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
打开团期详情页时,读取产品 ID,供「新增子订单」按钮拼接深链,跳转订单创建向导并预填产品及班期。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `groupBatchId` | Path | String | 是 | 正整数 ID | 团期主键 |
|
||||
|
||||
#### 出参 `Result<GroupBatchDetailRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data.groupBatchId` | String | 团期聚合主键 |
|
||||
| `data.productBatchId` | String | product 侧班期 ID |
|
||||
| `data.productId` | String | **【新增】** 产品 ID,供深链预填 |
|
||||
| `data.productName` | String | 产品名称 |
|
||||
| `data.batchNo` | String | 班期编号 |
|
||||
| `data.batchName` | String | 班期名称 |
|
||||
| `data.batchLabel` | String/null | 班期标签快照 |
|
||||
| `data.batchStatus` | String | 团期状态码 |
|
||||
| `data.batchStatusName` | String | 状态中文名 |
|
||||
| `data.minGroupPeople` | Integer | 最低成团人数 |
|
||||
| `data.maxRooms` | Integer | 房间容量 |
|
||||
| `data.maxParticipants` | Integer | 人数容量 |
|
||||
| `data.enrolledPeople` | Integer | 已报名人数 |
|
||||
| `data.enrolledRooms` | Integer | 已用房间数 |
|
||||
| `data.remainRooms` | Integer | 剩余房间数 |
|
||||
| `data.remainParticipants` | Integer | 剩余人数 |
|
||||
| `data.hotelReady` | Boolean | 配房完成标志 |
|
||||
| `data.vehicleReady` | Boolean | 配车完成标志 |
|
||||
| `data.guideReady` | Boolean | 导游完成标志 |
|
||||
| `data.photographerReady` | Boolean | 摄影完成标志 |
|
||||
| `data.materialConfirmed` | Boolean | 物资确认标志 |
|
||||
| `data.requirementConfirmed` | Boolean | 需求整体确认标志 |
|
||||
| `data.departDate` | String | 出发日期 |
|
||||
| `data.endDate` | String | 结束日期 |
|
||||
| `data.enrollDeadline` | String | 报名截止日 |
|
||||
| `data.totalReceivable` | String | 整团应收合计 |
|
||||
| `data.totalReceived` | String | 整团已收合计 |
|
||||
| `data.remark` | String/null | 备注 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2096412454643802114
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"groupBatchId": "2096412454643802114",
|
||||
"productBatchId": "2052935476557328386",
|
||||
"productId": "2044306857534636034",
|
||||
"productName": "冻干粉发短信给",
|
||||
"batchNo": "Q202610012052935476548939777",
|
||||
"batchName": "10月1日长白山亲子团",
|
||||
"batchLabel": null,
|
||||
"batchStatus": "RESOURCE_PREPARING",
|
||||
"batchStatusName": "资源准备中",
|
||||
"minGroupPeople": 6,
|
||||
"maxRooms": 4,
|
||||
"maxParticipants": 10,
|
||||
"enrolledPeople": 10,
|
||||
"enrolledRooms": 4,
|
||||
"remainRooms": 0,
|
||||
"remainParticipants": 0,
|
||||
"hotelReady": false,
|
||||
"vehicleReady": false,
|
||||
"guideReady": false,
|
||||
"photographerReady": false,
|
||||
"materialConfirmed": false,
|
||||
"requirementConfirmed": false,
|
||||
"departDate": "2026-10-01",
|
||||
"endDate": "2026-10-03",
|
||||
"enrollDeadline": "2026-09-25",
|
||||
"totalReceivable": "48000.00",
|
||||
"totalReceived": "36000.00",
|
||||
"remark": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
详情接口不存在空数据响应。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589501,
|
||||
"message": "团期不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
业务失败错误码沿用 GroupBatchErrorCode(权限/不存在等)。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 沿用团期详情权限校验;无权返回 589507。
|
||||
- `productId` 与其他字段同时返回,始终非空。
|
||||
- 团期不存在返回业务码 589501(HTTP 200)。
|
||||
|
||||
---
|
||||
|
||||
### 3. 团期看板列表 `GET /v3/admin/order/group-batch/board?productId=`
|
||||
|
||||
**VO**: `GroupBatchBoardItemRespVO`(响应位置:`data[]`)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
打开团期看板时,按产品维度加载该产品下的所有班期(产品侧班期为基底,左连运营侧团期数据),每一行携带 productId,供「新增子订单」深链按行数据拼接预填参数。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `productId` | Query | String | 是 | 正整数 ID | 按产品筛选(必填;缺参返回 400) |
|
||||
|
||||
#### 出参 `Result<List<GroupBatchBoardItemRespVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data[].productBatchId` | String | product 侧班期 ID |
|
||||
| `data[].productId` | String | **【新增】** 产品 ID(= 请求入参;命中/未命中/孤儿行均非空) |
|
||||
| `data[].batchNo` | String | 班期编号 |
|
||||
| `data[].batchName` | String | 班期名称 |
|
||||
| `data[].departureDate` | String | 出发日期 |
|
||||
| `data[].endDate` | String | 结束日期 |
|
||||
| `data[].enrollmentDeadline` | String | 报名截止日 |
|
||||
| `data[].maxRooms` | Integer | 房间容量 |
|
||||
| `data[].maxParticipants` | Integer | 人数容量 |
|
||||
| `data[].enrolledRooms` | Integer | 已报名房数 |
|
||||
| `data[].enrolledPeople` | Integer | 已报名人数 |
|
||||
| `data[].remainRooms` | Integer | 剩余房间数 |
|
||||
| `data[].remainParticipants` | Integer | 剩余人数 |
|
||||
| `data[].batchStatus` | String | 团期状态码 |
|
||||
| `data[].batchStatusLabel` | String | 状态中文名 |
|
||||
| `data[].hotelReady` | Boolean | 配房完成标志 |
|
||||
| `data[].vehicleReady` | Boolean | 配车完成标志 |
|
||||
| `data[].guideReady` | Boolean | 导游完成标志 |
|
||||
| `data[].photographerReady` | Boolean | 摄影完成标志 |
|
||||
| `data[].needsGuide` | Boolean | 是否需领队 |
|
||||
| `data[].needsPhotographer` | Boolean | 是否需摄影 |
|
||||
| `data[].orderCount` | Integer | 活跃子订单数 |
|
||||
| `data[].groupBatchId` | String/null | 团期聚合主键(未成团时为 null) |
|
||||
| `data[].productBatchRemoved` | Boolean | 是否孤儿行(product 侧已删除该班期) |
|
||||
| `data[].contractSignedCount` | Integer | 合同已签子订单数 |
|
||||
| `data[].insuranceInsuredCount` | Integer | 保险已出子订单数 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/board?productId=2044306857534636034
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"productBatchId": "2052935476557328386",
|
||||
"productId": "2044306857534636034",
|
||||
"batchNo": "Q202610012052935476548939777",
|
||||
"batchName": "10月1日长白山亲子团",
|
||||
"departureDate": "2026-10-01",
|
||||
"endDate": "2026-10-03",
|
||||
"enrollmentDeadline": "2026-09-25",
|
||||
"maxRooms": 4,
|
||||
"maxParticipants": 10,
|
||||
"enrolledRooms": 2,
|
||||
"enrolledPeople": 4,
|
||||
"remainRooms": 2,
|
||||
"remainParticipants": 6,
|
||||
"batchStatus": "RECRUITING",
|
||||
"batchStatusLabel": "招募中",
|
||||
"hotelReady": false,
|
||||
"vehicleReady": false,
|
||||
"guideReady": false,
|
||||
"photographerReady": false,
|
||||
"needsGuide": true,
|
||||
"needsPhotographer": false,
|
||||
"orderCount": 2,
|
||||
"groupBatchId": "2096412454643802114",
|
||||
"productBatchRemoved": false,
|
||||
"contractSignedCount": 1,
|
||||
"insuranceInsuredCount": 1
|
||||
},
|
||||
{
|
||||
"productBatchId": "2052935476557328387",
|
||||
"productId": "2044306857534636034",
|
||||
"batchNo": "Q202610022052935476548939778",
|
||||
"batchName": "10月2日长白山亲子团",
|
||||
"departureDate": "2026-10-02",
|
||||
"endDate": "2026-10-04",
|
||||
"enrollmentDeadline": "2026-09-26",
|
||||
"maxRooms": 4,
|
||||
"maxParticipants": 10,
|
||||
"enrolledRooms": 0,
|
||||
"enrolledPeople": 0,
|
||||
"remainRooms": 4,
|
||||
"remainParticipants": 10,
|
||||
"batchStatus": "RECRUITING",
|
||||
"batchStatusLabel": "招募中",
|
||||
"hotelReady": false,
|
||||
"vehicleReady": false,
|
||||
"guideReady": false,
|
||||
"photographerReady": false,
|
||||
"needsGuide": true,
|
||||
"needsPhotographer": false,
|
||||
"orderCount": 0,
|
||||
"groupBatchId": null,
|
||||
"productBatchRemoved": false,
|
||||
"contractSignedCount": 0,
|
||||
"insuranceInsuredCount": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
缺参 productId:
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "productId 参数缺失",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `productId` 必填,缺参返回 HTTP 200 + code 400;不要用 GET 参数默认值。
|
||||
- 返回结构按产品侧班期为基底(左连团期数据):
|
||||
- **命中行**(班期有对应团期):团期状态/容量/ready 取订单侧值;batchStatus ≠ RECRUITING
|
||||
- **未命中行**(班期无团期):batchStatus 固定 RECRUITING;groupBatchId = null;容量取产品侧 maxRooms/maxParticipants
|
||||
- **孤儿行**(团期但班期已删):productBatchRemoved = true;仅 orderCount > 0 时出现
|
||||
- `productId` 在命中行、未命中行、孤儿行中均等于请求入参,始终非空。
|
||||
- 表合并按 productBatchId 左连 order_group_batch;无团期记录时新增一行(未命中)。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
| 场景 | 正确做法 |
|
||||
|------|---------|
|
||||
| 新增子订单深链 | 按行数据拼接 `/order-v2/new?productId={productId}&productBatchId={productBatchId}&departureDate={departureDate}` |
|
||||
| 向导内 GROUP 产品创单 | 检查产品类型,仅 GROUP 类型在 POST payload 中带 productBatchId;其他类型忽略 |
|
||||
| 非 GROUP 产品创单 | 忽略 productBatchId 参数,后端根据 productId 与 departureDate 自行逻辑 |
|
||||
| 孤儿行处理 | 若 productBatchRemoved = true,需向用户提示"班期已下架,不可创建子订单"或禁用按钮 |
|
||||
| 未成团行处理 | groupBatchId = null 时无团期记录;前端可选择隐藏"团期信息"列或显示"待成团" |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
| 班期状态 | productBatchId | groupBatchId | batchStatus | 数据来源 |
|
||||
|---------|----------------|-------------|------------|---------|
|
||||
| 命中(有团期) | 产品侧 | 订单侧 | 订单侧 | order_group_batch 存在 |
|
||||
| 未命中(无团期) | 产品侧 | null | RECRUITING | 无 order_group_batch 行 |
|
||||
| 孤儿(班期删)| 产品侧 | 订单侧 | 订单侧 | order_group_batch 存在但班期无 |
|
||||
|
||||
新增 productId 字段在 productBatchId 后(响应顺序一致)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 业务失败可能仍为 HTTP 200,必须同时检查 `code`、`success` 和 `message`。
|
||||
- 看板接口 `productId` 缺参返回 400(HTTP 200);不要依赖前端参数校验。
|
||||
- 分页/列表接口分页参数超界时返回空列表(无 5XX)。
|
||||
- `productId` 在所有三个接口的所有行中均非空,无特殊情况返回 null。
|
||||
- Long 型 ID 在 JSON 字符串化后,前端若需数值运算应保持字符串存储。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `productId`(分页项) | 无此字段 | 新增;产品 ID |
|
||||
| `productId`(详情) | 无此字段 | 新增;产品 ID |
|
||||
| `productId`(看板项) | 无此字段 | 新增;产品 ID |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 新增子订单跳转 | 无法从响应直接拼深链参数 | 新增 productId,与 productBatchId/departureDate 配合直接拼链 |
|
||||
| 向导内产品预填 | 依赖约定俗成或外部 context | 直接从深链 query string 传入 |
|
||||
| GROUP 产品识别 | 向导内自行判别 | 向导根据产品类型自动识别是否需 productBatchId |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否。新增字段对旧客户端透明。
|
||||
- **前端是否必须同步上线**: 否(从旧口径讲);但为完整支持团期看板「新增子订单」功能,前端应同步接入新增 productId 于深链拼接(仅一处改动)。
|
||||
- **前端 workaround 清理点**:
|
||||
- 新增子订单按钮处,改用响应中的 productId 拼深链,而非硬编码产品 ID
|
||||
- 向导内创建 GROUP 订单时,检查产品类型再决定是否传 productBatchId(无需前端重构,只需补一个类型判断)
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台团期看板的「新增子订单」功能入口
|
||||
- **零影响**:
|
||||
- 团期创建、编辑、审批、资源配置等写接口
|
||||
- 团期与订单间的关联关系和业务流程
|
||||
- 产品侧班期相关接口和定义
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- **单测**: 79 条测试用例绿✓(新增 productId 相关的 UT 已覆盖命中/未命中/孤儿行三路径)
|
||||
- **ArchTest**: 45 条架构测试绿✓
|
||||
- **测试服网关实测**: 单测 + CR 通过;测试服网关实测见管理者补充
|
||||
|
||||
实测产品: `productId=2044306857534636034`(冻干粉发短信给),班期 `2026-10-01`(productBatchId=2052935476557328386),团期主键 `groupBatchId=2096412454643802114`,团期名 `batchName=10月1日长白山亲子团`。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR(功能演进)
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| #7135 | - | GROUP 产品创单校验与重整(定义 productBatchId 入参) | ✅ 有效 |
|
||||
| #7142 | - | 订单详情/列表/创单响应透出判团字段(groupBatchId/productBatchId/groupOrder)| ✅ 有效 |
|
||||
| **本 PR #7157** | **#7143** | **团期看板 VO 补 productId(配合深链预填)** | ✅ 最新 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#7143](https://git.1814.love:8443/wx/HL/issues/7143)
|
||||
- 关联 PR: [wx/HL#7157](https://git.1814.love:8443/wx/HL/pulls/7157)
|
||||
- 相关工单: #7142(订单判团字段)、#7135(GROUP 产品规范)
|
||||
- 前端缺陷: 新建订单向导漏传 productBatchId(另发前端 changelog)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#7143](https://git.1814.love:8443/wx/HL/issues/7143)
|
||||
- **PR**: [#7157](https://git.1814.love:8443/wx/HL/pulls/7157)
|
||||
- **Merge commit**: [ce2809c15](https://git.1814.love:8443/wx/HL/commit/ce2809c15)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,133 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7148"
|
||||
title: "供应商账户停用审批及审批结果状态调整"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "7245780a"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-06"
|
||||
status_note: "后端已部署并通过 TEST;前端待接入停用审批入口与状态刷新"
|
||||
updated_at: "2026-09-06"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商:账户停用审批及审批结果状态调整
|
||||
|
||||
## 关键变化
|
||||
|
||||
停用审批通过后账户停用、驳回后启用;原账户新增审批通过后启用、驳回后改为停用。审批状态与账户状态分别展示。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 申请账户停用 | POST | `/admin/supplier/bank-accounts/{accountId}/disable` | 新增接口 | 复用现有账户审批模板与表单 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 申请账户停用 `POST /admin/supplier/bank-accounts/{accountId}/disable`
|
||||
|
||||
**VO**: `SupplierAccountDisableReqVO / BankAccountSubmitResultRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
供应商账户页面对非默认启用账户提交停用申请。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| accountId | Path | String | 是 | 正整数 | 目标账户 ID |
|
||||
| expectedUpdateTime | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 账户详情返回的 updateTime |
|
||||
|
||||
#### 出参 `Result<BankAccountSubmitResultVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| accountId / approvalLogId | String | 账户 / 本次审批记录 ID |
|
||||
| requestNo | String | 同次申请重试时不变 |
|
||||
| provider | String | 新停用申请为 WECOM |
|
||||
| approvalStatus / approvalStatusName | String | 审批状态 / 中文名称 |
|
||||
| spNo / spStatus | String 或 null | 企微单号 / 企微原始状态,尚未取得时为 null |
|
||||
| syncStatus | String | REQUESTING 待同步、APPLIED 已应用、APPLY_FAILED 应用失败、RESULT_UNCERTAIN 待对账 |
|
||||
| accountStatus | String | 当前账户状态 |
|
||||
| isDefault | String | NO 非默认;历史申请重试返回当前默认标记 |
|
||||
| submittedAt / finishedAt | String 或 null | 提交 / 审批完成时间,尚未发生时为 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
`POST /admin/supplier/bank-accounts/7148001/disable`,携带正常管理端登录认证。
|
||||
|
||||
```json
|
||||
{"expectedUpdateTime":"2026-09-06 12:00:00"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","success":true,"data":{"accountId":"7148001","approvalLogId":"7148002","requestNo":"SUP-ACC-DISABLE-example","provider":"WECOM","approvalStatus":"PENDING","approvalStatusName":"审核中","spNo":"202609060001","spStatus":null,"syncStatus":"REQUESTING","accountStatus":"ACTIVE","isDefault":"NO","submittedAt":"2026-09-06 12:01:00","finishedAt":null}}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code":395014,"message":"数据已被他人修改,请刷新后重试","success":false,"data":null}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
不存在返回 395001;外部提交失败时可能返回已保留的审批与 `APPLY_FAILED` 或 `RESULT_UNCERTAIN`,不会把账户直接停用。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 仅 SUPER_ADMIN 且具备 `supplier:account:manage`、`supplier:approval:submit` 可提交。主体须启用、账户须启用且非默认,主体与账户均不能有在途审批。门禁失败不新建申请;企微失败可能保留申请,须同时检查 `syncStatus`,不能把 `code=200` 当作已停用。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
从最新账户详情取版本;同次失败重试复用原版本。已结束申请后重新申请须刷新版本。`RESULT_UNCERTAIN` 停止自动重提并等待对账;`APPLY_FAILED` 沿用原申请重试。默认账户须先切换默认。前端新增停用动作并在提交后刷新账户和审批状态。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
审批中账户保持启用;只有停用审批通过才转为停用。驳回或撤销保留启用,重复请求不重复应用结果。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
未登录由网关拒绝;400 参数无效;395001 账户不存在;395004 无权限;395005 账户状态或默认标记不允许;395010 主体未启用;395011 账户已有在途审批;395014 版本过期。主体审批冻结返回 395005。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### accountStatus(账户状态)
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| ACTIVE | 启用 | 停用待审或驳回后保持可用 |
|
||||
| DISABLED | 停用 | 停用通过或新账户审批驳回 |
|
||||
| PENDING | 待审批 | 原新账户审批未结束 |
|
||||
| REJECTED | 驳回 | 仅兼容历史账户记录 |
|
||||
|
||||
### approvalStatus(审批状态)
|
||||
|
||||
PENDING 审核中;APPROVED 已通过;REJECTED 已驳回;CANCELED 已撤销。终态重试返回账户当前状态,不据旧审批推导账户状态。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
本单涉及供应商账户停用入口和审批结果状态;已有账户请求、查询返回结构保持兼容。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
真实企微审批已验证:新增账户通过后为 `ACTIVE`、驳回后为 `DISABLED`;停用审批待审期间保持 `ACTIVE`,驳回后仍为 `ACTIVE`,通过后为 `DISABLED`。同版本重试复用原审批单;未登录、缺参、非法账户 ID、旧版本、在途审批和状态不允许均按契约拒绝,失败请求未新增审批或改写账户。测试账户最终均为非默认 `DISABLED`,原默认账户未变化。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- **Issue**:[#7148](https://git.1814.love:8443/wx/HL/issues/7148)
|
||||
- **PR**:[#7161](https://git.1814.love:8443/wx/HL/pulls/7161)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **后端负责人**:@lc
|
||||
@@ -0,0 +1,418 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7149"
|
||||
title: "团期子订单支付后即可提房车需求、物资准备起冻结、团单房型与间数必填"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: "2026-09-06"
|
||||
status_note: "后端已合 dev-v3 并部署测试服、网关实测通过;前端需改团期子订单提需求弹窗:房型大类必填、两个新错误码直接展示 message、招募中即显示提需求入口、物料准备中入口置灰。"
|
||||
updated_at: "2026-09-06"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期模块:子订单支付后即可提房车需求、物资准备起冻结、团单房型与间数必填
|
||||
|
||||
> **服务**: `hl-order-service-v3`
|
||||
> **Issue**: #7149
|
||||
> **PR**: #7177(squash 合入 dev-v3 `a52365278`)
|
||||
> **日期**: 2026-09-06
|
||||
> **影响范围**: 管理后台团期子订单详情「住宿安排 / 用车安排」提需求弹窗与「订单调整」提交
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
三条行为改造,请求 / 响应结构一律不变:
|
||||
|
||||
1. **支付后即可提需求**:团期子订单(详情里 `groupOrder=true` / `productBatchId` 非空)在客户已支付(`orderStatus=CUSTOMIZING`)后即可提 / 改用房、用车需求,团期处于「招募中 `RECRUITING`」或「资源准备中 `RESOURCE_PREPARING`」都放行,不再等团期成团。改前团期非 `RESOURCE_PREPARING` 一律 589501「团期状态不允许当前操作」。
|
||||
2. **物资准备起冻结**:团期进入「物料准备中 `MATERIAL_PREPARING`」及之后(`PENDING_DEPARTURE / TRAVELLING / REVIEWING / SETTLED`)提 / 改需求被拒,新错误码 **589536「团期已进入物资准备,需求已冻结,请联系团期管理员」**(文案里的「物资准备」就是状态芯片的「物料准备中」,同义)。唯一例外:该户该资源最新一版需求被团期管理员打回(`REJECTED_TO_CONSULTANT`)时可以重提一次,重提后再改仍 589536。团期 `CANCELLED` / 查不到团期仍 589501。
|
||||
3. **团单房型大类与房间数必填**:团期子订单每个非自订晚(`customerSelfBooked` 非 `true`)的每段,按段首候选 `candidates[0].rooms[]` 逐行要求 `roomCategory` 非空且 `roomCount ≥ 1`(旧结构无 `rooms[]` 时按段级 `roomCategory` + `roomCount` 判),缺失返回新错误码 **582099「团期订单第{N}晚第{M}段需填写房型大类与房间数」**(M 从 1 起),拒绝时零副作用。核心订单不受影响,房型仍选填。**团单不再接受「加晚次空白占位」段**——每晚要么填齐房型行,要么标 `customerSelfBooked=true`。
|
||||
|
||||
前端要做:① 团期子订单提需求弹窗把「房型大类」改必填并提示;② 589536 / 582099 直接展示后端 `message`;③ 团期子订单在招募中即显示「提交房型需求 / 用车需求」入口;进入物料准备中后入口置灰或提示已冻结(被打回的户除外);④ 团单去掉「先提交空白占位」交互。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 提交 / 修改用房需求(兼容期 @Deprecated) | PUT | `/v3/admin/order/{id}/hotel-requirement` | 行为修改 | 团期闸门放宽 + 冻结 + 团单房型间数必填 |
|
||||
| 2 | 订单调整统一提交 | POST | `/v3/admin/order/{id}/adjustment/submit` | 行为修改 | `updates.hotelRequirement` / `updates.vehicleRequirement` 走同一闸门与必填校验 |
|
||||
| 3 | 提交 / 修改用车需求 | PUT | `/v3/admin/order/{id}/vehicle-requirement` | 行为修改 | 团期闸门放宽 + 冻结,不加必填 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 提交 / 修改用房需求 `PUT /v3/admin/order/{id}/hotel-requirement`
|
||||
|
||||
**VO**: `HotelRequirementReqVO → HotelRequirementRespVO`(`hl-order-service-v3/src/main/java/com/hulalv/order/requirement/controller/admin/vo/`)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
定制师在子订单详情「住宿安排」提交或修改用房需求。后端按当前生效版本自动分支:无生效版本 → `INIT_SUBMIT`(新版本);生效版本为 `PENDING / PENDING_REVIEW` → `PENDING_EDIT`(同版本覆盖);`DONE` 等 → `DONE_ADJUST`(版本 +1)。团期子订单新版本状态恒为 `PENDING_REVIEW`(等团期管理员确认),不进房务抢单池。本端点为兼容期入口(#4515 标 @Deprecated),新客户端走接口 2。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `id` | Path | String(Long) | 是 | 订单 ID | 团期子订单 `productBatchId` 非空 |
|
||||
| `days` | Body | Array | 是 | 长度 = `tripNights`,`dayNumber` 1..tripNights 不重复 | 逐晚安排 |
|
||||
| `days[].dayNumber` | Body | Integer | 是 | 1..tripNights | 第几晚 |
|
||||
| `days[].customerSelfBooked` | Body | Boolean | 否 | `true` = 客人自订 | 自订晚不校验房型,`segments` 可空 |
|
||||
| `days[].segments` | Body | Array | 非自订晚 ≥1 | 缺段 582098 | 当晚分住段 |
|
||||
| `days[].segments[].candidates` | Body | Array | 是(≥1) | 候选酒店,房控择一 | `hotelId / hotelName` 可空(无酒店候选) |
|
||||
| `days[].segments[].candidates[].rooms` | Body | Array | 新结构 | 房型行 | 团单按 `candidates[0].rooms` 逐行校验 |
|
||||
| `…rooms[].roomCategory` | Body | String | **团单是** | 字典 `room_category`(STANDARD/SINGLE/TWIN/QUEEN/KING/SUITE/FAMILY/YURT/SPECIAL) | 核心订单选填不变 |
|
||||
| `…rooms[].roomCount` | Body | Integer | **团单是,≥1** | 有行即 >0(582016) | 房数 |
|
||||
| `…rooms[].roomTypeId / roomTypeName / protocolPrice / remark` | Body | Long / String / Decimal / String | 否 | — | 真实房型与快照,可空 |
|
||||
| `days[].segments[].roomCategory / roomCount` | Body | String / Integer | 兼容 | 旧结构无 `rooms[]` 时团单必填 | 段级兼容字段 |
|
||||
| `days[].segments[].budget / remark` | Body | Decimal / String(≤200) | 否 | 预算由后端按协议价覆盖 | — |
|
||||
| `specialTags` | Body | String[] | 否 | 字典 `house_special_demand` | 特殊诉求 |
|
||||
| `remark` | Body | String(≤500) | 否 | — | 备注 |
|
||||
|
||||
#### 出参 `Result<HotelRequirementRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.requirementId` | String | 需求行 ID |
|
||||
| `data.version` | Integer | 版本号(INSERT-only 单调递增) |
|
||||
| `data.isActive` | Boolean | 是否当前生效版本 |
|
||||
| `data.status` | String | 团单恒 `PENDING_REVIEW`;核心为 `PENDING` |
|
||||
| `data.submittedAt` | String | 首提时间 |
|
||||
| `data.claimerId / claimerName / claimedAt` | String / String / String | 团单恒 null(不进抢单池) |
|
||||
| `data.branchTaken` | String | `INIT_SUBMIT / PENDING_EDIT / DONE_ADJUST` |
|
||||
| `data.previousVersion` | Integer | `DONE_ADJUST` 时上一版本号,否则 null |
|
||||
| `data.assignmentDeletedCount` | Integer | `DONE_ADJUST` 时软删配房行数,否则 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"days": [
|
||||
{ "dayNumber": 1, "segments": [ { "remark": "第1晚", "candidates": [ { "hotelName": "网关实测酒店", "rooms": [ { "roomTypeName": "测试房型", "roomCategory": "KING", "roomCount": 1 } ] } ] } ] },
|
||||
{ "dayNumber": 2, "segments": [ { "remark": "第2晚", "candidates": [ { "hotelName": "网关实测酒店", "rooms": [ { "roomTypeName": "测试房型", "roomCategory": "TWIN", "roomCount": 2 } ] } ] } ] }
|
||||
],
|
||||
"remark": "#7149 网关实测"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"requirementId": "2096504560393551873",
|
||||
"version": 1,
|
||||
"isActive": true,
|
||||
"status": "PENDING_REVIEW",
|
||||
"submittedAt": "2026-09-06 15:44:21",
|
||||
"claimerId": null,
|
||||
"claimerName": null,
|
||||
"claimedAt": null,
|
||||
"branchTaken": "INIT_SUBMIT",
|
||||
"previousVersion": null,
|
||||
"assignmentDeletedCount": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口无空列表语义;`days` 为空数组或长度与 `tripNights` 不符返回 582011「天数长度与订单住宿晚数不一致」。第 2 晚 `customerSelfBooked=true` 且无 `segments` 时正常返回 200(自订晚不进房务分母,团单房型校验跳过)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 582099, "message": "团期订单第2晚第1段需填写房型大类与房间数", "success": false, "data": null }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 589536, "message": "团期已进入物资准备,需求已冻结,请联系团期管理员", "success": false, "data": null }
|
||||
```
|
||||
|
||||
其它沿用:589501「团期状态不允许当前操作」(团期 `CANCELLED` / 查不到)、582098「第{N}晚缺少用房需求,请填写房间需求或标记为客户自订」、582016「房间数必须大于 0」、582019「候选方案必须至少含 1 个房型行」、582017「订单状态不允许提交需求」(未支付)。HTTP 始终 200,错误在 `code` / `message`。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 团期闸门(房车共用):`RECRUITING / RESOURCE_PREPARING` 放行;`MATERIAL_PREPARING / PENDING_DEPARTURE / TRAVELLING / REVIEWING / SETTLED` 及未知状态 589536;`CANCELLED` / 查不到团期 589501;核心订单不查团期。
|
||||
- 冻结期例外:该户用房需求最新一版为 `REJECTED_TO_CONSULTANT`(团期管理员打回)时放行重提,新版本回到 `PENDING_REVIEW`;之后再改仍 589536,要再改只能再次被打回。
|
||||
- 团单房型间数必填按「段首候选 rooms 行」判,与全团需求汇总 `requirement-summary` 同口径;旧结构(无 `rooms[]`)按段级 `roomCategory + roomCount` 合成一行判。
|
||||
- 校验顺序:结构校验(582098 / 582016 / 582019)→ 订单状态(582017)→ 团期闸门(589536 / 589501)→ 团单房型间数(582099);任一拒绝均在写库之前,无新版本、`room_control_status` 不变。
|
||||
- 成功后 `order_main.room_control_status=PENDING_REVIEW`(既有行为),团期成团时定制师不再产生「房型需求 · 待提交」待办。
|
||||
|
||||
### 2. 订单调整统一提交 `POST /v3/admin/order/{id}/adjustment/submit`
|
||||
|
||||
**VO**: `AdjustmentSubmitReqVO → AdjustmentSubmitRespVO`(`hl-order-service-v3/src/main/java/com/hulalv/order/adjustment/controller/admin/vo/`)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理端子订单详情「订单调整」一次性提交各子域改动;本次只涉及 `updates.hotelRequirement`(用房需求完整新版本)与 `updates.vehicleRequirement`(用车需求完整新版本),两者与接口 1 / 接口 3 走同一团期闸门与团单房型间数校验。其它子域(人数 / 日期 / 出行人等)本次不变、省略。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `id` | Path | String(Long) | 是 | 订单 ID | — |
|
||||
| `updates` | Body | Object | 是 | 至少一个子域非空 | 各子域修改内容 |
|
||||
| `updates.hotelRequirement` | Body | `HotelRequirementBodyVO` | 否 | 结构同接口 1 的 `days / specialTags / remark` | 用房需求完整新版本,团单房型间数必填规则同接口 1 |
|
||||
| `updates.vehicleRequirement` | Body | `VehicleRequirementBodyVO` | 否 | 结构同接口 3 的 `fleet / specialTags / pickupRequired / dropoffRequired / remark` | 用车需求完整新版本 |
|
||||
|
||||
#### 出参 `Result<AdjustmentSubmitRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.success` | Boolean | 提交成功恒 `true` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"updates": {
|
||||
"hotelRequirement": {
|
||||
"days": [
|
||||
{ "dayNumber": 1, "segments": [ { "candidates": [ { "hotelName": "网关实测酒店", "rooms": [ { "roomCategory": "KING", "roomCount": 1 } ] } ] } ] },
|
||||
{ "dayNumber": 2, "segments": [ { "candidates": [ { "hotelName": "网关实测酒店", "rooms": [ { "roomCategory": "TWIN", "roomCount": 1 } ] } ] } ] }
|
||||
],
|
||||
"remark": "#7149 网关实测 调整入口"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "success": true, "data": { "success": true } }
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
`updates` 各子域全空时沿用既有「无有效变更」拒绝;`hotelRequirement` 提交成功但版本号不变(`PENDING_EDIT` 覆盖同版本)属正常,前端以订单详情重新拉取为准。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 589536, "message": "团期已进入物资准备,需求已冻结,请联系团期管理员", "success": false, "data": null }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 582099, "message": "团期订单第2晚第1段需填写房型大类与房间数", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 调整入口绕过「订单须为定制中」的普通提交闸,但**不绕**团期闸门与团单房型间数校验,行为与接口 1 / 3 一致。
|
||||
- 团期冻结期内该入口同样 589536,被打回户例外同接口 1。
|
||||
- 拒绝发生在事务内任何写操作之前,其它子域改动一并回滚。
|
||||
|
||||
### 3. 提交 / 修改用车需求 `PUT /v3/admin/order/{id}/vehicle-requirement`
|
||||
|
||||
**VO**: `VehicleRequirementReqVO → VehicleRequirementRespVO`(`hl-order-service-v3/src/main/java/com/hulalv/order/requirement/controller/admin/vo/`)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
定制师在子订单详情「用车安排」提交或修改用车需求。本次只改团期闸门(放宽 + 冻结 + 打回例外),**不加任何必填**;车型 / 座位联动校验、容量校验、派单展开等既有逻辑不变。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `id` | Path | String(Long) | 是 | 订单 ID | — |
|
||||
| `fleet` | Body | Array | 是(≥1) | 缺 582021 | 用车明细 |
|
||||
| `fleet[].vehicleType` | Body | String | 是 | 车务车型大类 key(582022) | 车型大类 |
|
||||
| `fleet[].seats` | Body | Integer | 是 | 该车型可选座位数(582024) | 座位数 |
|
||||
| `fleet[].count` | Body | Integer | 是 | >0(582023) | 车辆数 |
|
||||
| `specialTags` | Body | String[] | 否 | 字典 `vehicle_special_demand`(582025) | 特殊诉求 |
|
||||
| `pickupRequired / dropoffRequired` | Body | Boolean | 否 | — | 接 / 送机 |
|
||||
| `remark` | Body | String | 否 | — | 备注 |
|
||||
|
||||
#### 出参 `Result<VehicleRequirementRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `data.requirementId` | String | 需求行 ID |
|
||||
| `data.version` | Integer | 版本号 |
|
||||
| `data.isActive` | Boolean | 是否当前生效版本 |
|
||||
| `data.status` | String | 团单恒 `PENDING_REVIEW` |
|
||||
| `data.submittedAt` | String | 首提时间 |
|
||||
| `data.branchTaken` | String | `INIT_SUBMIT / PENDING_EDIT / DONE_ADJUST` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{ "fleet": [ { "vehicleType": "SUV", "seats": 7, "count": 1 } ] }
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": { "requirementId": "2096504700000000001", "version": 1, "isActive": true, "status": "PENDING_REVIEW", "submittedAt": "2026-09-06 15:46:02", "branchTaken": "INIT_SUBMIT" }
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
`fleet` 为空返回 582021「用车需求数组不能为空」;车队车型库不可用返回 582091「车队车型库不可用,无法校验座位数选项」(既有降级,本次不变)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 589536, "message": "团期已进入物资准备,需求已冻结,请联系团期管理员", "success": false, "data": null }
|
||||
```
|
||||
|
||||
其它沿用:589501(团期 `CANCELLED` / 查不到)、582022 / 582024 / 582023 / 582025(车型 / 座位 / 数量 / 诉求校验)。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 团期闸门与接口 1 完全一致(同一方法),放行 / 冻结 / 打回例外按**用车需求**自身的最新一版判。
|
||||
- 车侧不加房型类必填;`fleet` 结构与既有校验不变。
|
||||
- 成功后 `order_main.vehicle_control_status=PENDING_REVIEW`(既有行为)。
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
| 场景 | payload 要点 | 结果 |
|
||||
|---|---|---|
|
||||
| ✅ 团单每晚房型行齐全 | `candidates[0].rooms[]` 每行 `roomCategory` + `roomCount ≥ 1` | 200,`status=PENDING_REVIEW` |
|
||||
| ✅ 团单某晚客人自订 | `{ "dayNumber": 2, "customerSelfBooked": true }`,无 `segments` | 200,该晚跳过校验 |
|
||||
| ✅ 旧结构团单 | 无 `rooms[]`,段级 `roomCategory` + `roomCount` 齐全 | 200 |
|
||||
| ❌ 团单房型行缺 `roomCategory` | `rooms: [ { "roomCount": 1 } ]` | 582099 |
|
||||
| ❌ 团单旧结构缺段级 `roomCategory` | 段级只有 `roomCount`,候选带 `roomTypeId` | 582099 |
|
||||
| ❌ 团单「加晚次空白占位」段 | 候选与段全空 | 582099(核心订单该形态仍放行) |
|
||||
| ❌ 团期物料准备中提交 / 修改 | 任何合法 payload | 589536(该户最新需求被打回除外) |
|
||||
| ❌ 子订单未支付 | 任何 payload | 582017「订单状态不允许提交需求」 |
|
||||
|
||||
### 切换状态时的必要动作
|
||||
|
||||
- 团期从招募中推进到物料准备中后,前端应把提需求入口置灰并以 589536 文案提示;管理员打回某户后该户入口恢复一次。
|
||||
- 团单弹窗保存前在前端做房型大类必填校验,减少 582099 往返;后端仍以 582099 兜底。
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
- 无表结构变更、无 Flyway。
|
||||
- 提交成功:`order_hotel_requirement` / `order_vehicle_requirement` 写入新版本(`PENDING_EDIT` 为同版本换行,其它版本 +1),`status=PENDING_REVIEW`;`order_main.room_control_status` / `vehicle_control_status` 回写 `PENDING_REVIEW`(既有行为)。
|
||||
- 被拒(589536 / 582099 / 589501 / 结构校验):无任何写入。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- **团单不再接受「加晚次空白占位」段**:核心订单允许先提交全空的候选 / 段占位,团期子订单的非自订晚一律按缺房型拒绝(582099「第 N 晚第 M 段」);团单每晚要么填齐 `rooms[]`,要么标 `customerSelfBooked=true`。
|
||||
- **文案「物资准备」= 团期状态芯片「物料准备中」(`MATERIAL_PREPARING`)**:589536 文案沿用代码里的「物资准备」叫法,与状态枚举文案同义,前端直接展示 `message` 即可。
|
||||
- **冻结期打回只给一次重提机会**:重提后最新版变 `PENDING_REVIEW`,再改回到 589536;这是有意设计(改需求须经管理员再次打回)。
|
||||
- **自订晚**:`customerSelfBooked=true` 的晚不做房型校验;前端标记自订后应清空该晚 `segments`,避免残留段进入全团汇总。
|
||||
- **未知团期状态**按冻结处理(fail closed),不会误放行。
|
||||
|
||||
## 六.5、枚举 / 数据字典(接口出现枚举时必写)
|
||||
|
||||
### roomCategory(字典 `room_category`)
|
||||
|
||||
| 值 | 含义 |
|
||||
|---|---|
|
||||
| STANDARD | 标准间 |
|
||||
| SINGLE | 单人间 |
|
||||
| TWIN | 双床房 |
|
||||
| QUEEN | 大床房 |
|
||||
| KING | 特大床房 |
|
||||
| SUITE | 套房 |
|
||||
| FAMILY | 家庭房 |
|
||||
| YURT | 帐篷 / 毡房 |
|
||||
| SPECIAL | 特色房 |
|
||||
|
||||
### 团期状态(`com.hulalv.order.groupbatch.enums.GroupBatchStatus`)
|
||||
|
||||
| 值 | 芯片文案 | 提 / 改需求 |
|
||||
|---|---|---|
|
||||
| RECRUITING | 招募中 | 放行 |
|
||||
| RESOURCE_PREPARING | 资源准备中 | 放行 |
|
||||
| MATERIAL_PREPARING | 物料准备中 | 589536(打回户例外) |
|
||||
| PENDING_DEPARTURE | 待出发 | 589536(打回户例外) |
|
||||
| TRAVELLING | 出行中 | 589536(打回户例外) |
|
||||
| REVIEWING | 核单中 | 589536(打回户例外) |
|
||||
| SETTLED | 已结算 | 589536(打回户例外) |
|
||||
| CANCELLED | 已取消 | 589501 |
|
||||
|
||||
### 需求状态(`com.hulalv.order.requirement.enums.RequirementStatus`,`data.status` / `room_control_status`)
|
||||
|
||||
| 值 | 含义 |
|
||||
|---|---|
|
||||
| PENDING_REVIEW | 待团期管理员审核(团单提交后) |
|
||||
| REJECTED_TO_CONSULTANT | 已打回定制师(冻结期可重提一次) |
|
||||
| PENDING / PROCESSING / DONE | 核心订单 / 管理员提交房务后的房务侧状态,本次不变 |
|
||||
|
||||
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
|
||||
|
||||
### 字段级对比
|
||||
|
||||
无字段增删;`rooms[].roomCategory`、`rooms[].roomCount`(及旧结构段级同名字段)由「选填」改为「团期子订单必填」,核心订单不变。
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 团期子订单提 / 改需求时机 | 团期须为 `RESOURCE_PREPARING`(成团后),否则 589501 | 支付后即可:`RECRUITING / RESOURCE_PREPARING` 放行 |
|
||||
| 团期物料准备及之后 | 589501 | 589536(新码,文案明确「已冻结」),最新需求被打回的户可重提一次 |
|
||||
| 团单缺房型大类 / 房数 | 放行,全团汇总出现「未知」房型 | 582099 拒绝,零副作用 |
|
||||
| 团单空白占位段 | 放行 | 582099 拒绝 |
|
||||
| 用车需求 | 同 589501 闸 | 同新闸门,不加必填 |
|
||||
|
||||
## 六.7、影响评估(修改/删除类必写)
|
||||
|
||||
- 破坏向后兼容:**部分**——请求 / 响应结构不变,但团单缺房型或空白占位段的旧调用会从 200 变 582099,招募中的调用从 589501 变 200,物料准备中从 589501 变 589536。
|
||||
- 前端是否必须同步上线:**建议同步**——不同步时功能可用但用户会收到 582099 / 589536 提示且入口显示时机不准。
|
||||
- 前端需清理的分支:「未成团不显示提需求入口」「物料准备中仍允许提交」「团单空白占位先提交」。
|
||||
|
||||
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
|
||||
|
||||
- 核心订单(`productBatchId` 为空)的提需求、房型选填、抢单池、房务配房全部不变。
|
||||
- 团期管理员确认 / 打回 / 提交房务(`…/hotel-requirement/reject`、`…/dispatch`、`group-batch/{groupBatchId}/requirement/*`)本次不改;打回后重提的版本号规则不变。
|
||||
- `GET /v3/admin/order/group-batch/{groupBatchId}/requirement-summary` 结构不变,只是按本次提交的团单不再出现 `roomCategory="未知"` 项(历史数据仍可能出现)。
|
||||
- 网关路由、权限码、表结构均无改动。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**环境**:TEST 网关 `https://api.test.1814.love:9443`,order-v3 dev-v3 `a52365278` 两实例 2026-09-06 15:43 滚动部署,登录 `admin`(adminId 1001,SUPER_ADMIN,且为两张测试单的定制师),实测 15:44–15:47。
|
||||
|
||||
| 场景 | 请求 | 结果 |
|
||||
|---|---|---|
|
||||
| 招募中(`RECRUITING`)子订单 2096412454488612866 提完整房型 | `PUT …/hotel-requirement` | 200,`status=PENDING_REVIEW` |
|
||||
| 招募中,第 2 晚房型行缺 `roomCategory` | `PUT …/hotel-requirement` | 582099「团期订单第2晚第1段需填写房型大类与房间数」,DB 无新版本 |
|
||||
| 招募中,旧结构段级缺 `roomCategory`(候选带 `roomTypeId`) | `PUT …/hotel-requirement` | 582099,同上 |
|
||||
| 资源准备中完整提交 | `PUT …/hotel-requirement` | 200,`version=1 INIT_SUBMIT PENDING_REVIEW`;`order_main.room_control_status=PENDING_REVIEW` |
|
||||
| 资源准备中经调整入口改需求 | `POST …/adjustment/submit` | 200,`data.success=true`(同版本 `PENDING_EDIT` 换行) |
|
||||
| 第 2 晚 `customerSelfBooked=true` 无段 | `PUT …/hotel-requirement` | 200 |
|
||||
| 团期 2089713777065832450 子订单 2096029450184347649 资源准备中提交 | `PUT …/hotel-requirement` | 200 |
|
||||
| 团期管理员打回该户 | `POST …/hotel-requirement/reject` | 200,需求行 `REJECTED_TO_CONSULTANT`、`is_active=0` |
|
||||
| 团期改为 `MATERIAL_PREPARING` 后被打回户重提 | `PUT …/hotel-requirement` | 200,`version=2 PENDING_REVIEW`(冻结期例外) |
|
||||
| 重提后再改 | `PUT …/hotel-requirement` | 589536「团期已进入物资准备,需求已冻结,请联系团期管理员」 |
|
||||
| 物料准备中经调整入口改需求 | `POST …/adjustment/submit` | 589536 |
|
||||
| 物料准备中提用车需求 | `PUT …/vehicle-requirement` | 589536 |
|
||||
|
||||
**单测**:`RequirementServiceTest` 244 / `OrderTodoServiceTest` 15 / `RequirementGroupBatchErrorCodeRangeTest` 3 + 5 个 ArchTest(RedLine / MapperBoundary / HouseModuleBoundary / DashboardLayer / LocalCacheVetting)全绿,`BUILD SUCCESS` 396 用例。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 团期模块接口文档 `docs/group/团期模块接口文档-v2.0.html` §0C.11.1(提需求时机)/ §0C.11.2(房型间数必填)/ GB-ADM-011:<https://web.test.1814.love:9443/hl-docs/group/>
|
||||
- 团期房务实现方案 `docs/group/团期房务实现方案-v1.0.html` §3.12.1 / §3.12.2
|
||||
- 后续工单(本次不做):管理员确认 / 打回改造(M1 / M2)、房务团期看板与按日订房、定制师 ↔ 团期管理员站内会话
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- Issue: <https://git.1814.love:8443/wx/HL/issues/7149>
|
||||
- PR: <https://git.1814.love:8443/wx/HL/pulls/7177>
|
||||
- 合并提交: `a52365278`(dev-v3)
|
||||
|
||||
### 联系人
|
||||
|
||||
- 后端:wx
|
||||
- 前端(hl-ui):mmg
|
||||
@@ -0,0 +1,644 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7154"
|
||||
title: "团期财务总览与预支复用订单预支:财务 Tab 三个只读端点 + 预支创建语义改造"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "pending"
|
||||
gateway_status: "pending"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "待部署测试服并过网关实测后改 backend_status=deployed 再推送"
|
||||
updated_at: "2026-09-06"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期: 财务总览与预支复用订单预支
|
||||
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #7170
|
||||
> **Issue**: #7154
|
||||
> **日期**: 2026-09-06
|
||||
> **影响范围**: 管理后台团期详情「财务」Tab、底部操作条「预支」弹窗、财务管理→预支审批列表
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
团期财务 Tab 与团期预支弹窗此前**在 hl-ui 里完全不存在**(全仓零调用点),本次补齐后端。
|
||||
|
||||
四条前端必读:
|
||||
|
||||
1. **`POST .../group-batch/{id}/advance` 入参整体更换**。旧契约 `{amount, remark}` 是后端造了、前端从没接过的端点(已核 hl-ui v2.1 全仓无调用点),故**不是破坏性变更**;新契约与订单级预支**逐字一致**,预支弹窗组件可整体复用。
|
||||
2. **「已预支」拆成三个数,旧 `advanceTotal` 已废**。旧语义是「提交即计入」(无审批),与新口径不等价:展示用 `advanceApproved`(只计已通过),额度用 `advanceAvailable`。**不要再用一个数**。
|
||||
3. **预支上限一律读后端 `advanceAvailable`,不要前端自算**。订单级弹窗现在是本地 `balanceAmount − Σ记录` 算的,依赖记录列表已拉全;团期场景该算法不可靠。
|
||||
4. **「设置报账人」传的是产品侧 `productBatchId`**,从团期详情接口取,**不是**财务 Tab 路径上的 `groupBatchId`,两者不同值。该端点后端零改动。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 团期财务总览 | GET | `/v3/admin/order/group-batch/:id/finance` | 新增 | 四张金额卡 + 逐户付款 + 整团合计 |
|
||||
| 2 | 团期预支记录 | GET | `/v3/admin/order/group-batch/:id/advances` | 新增 | 团期级 + 各子订单级逐笔,不分页 |
|
||||
| 3 | 领款人候选 | GET | `/v3/admin/order/group-batch/:id/advance/payee-candidates` | 新增 | 预支弹窗「借款对象」下拉 |
|
||||
| 4 | 发起团期预支 | POST | `/v3/admin/order/group-batch/:id/advance` | 修改 | 路径不变,入参与返回体更换;创建即进待审批 |
|
||||
| 5 | 预支审批列表 | GET | `/v3/admin/order/advance-approvals/page` | 修改 | 容忍团期级行 + `scope` 筛选 + keyword 六路 |
|
||||
| 6 | 整团核算汇总 | GET | `/v3/admin/order/group-batch/:id/settlement/summary` | 修改 | 出参新增两个整团预支只读字段 |
|
||||
|
||||
**零改动复用**(同一套审批流程,前端无需改):`PUT /v3/admin/order/advance/:id/approve`、
|
||||
`PUT /v3/admin/order/advance/:id/reject`、`DELETE /v3/admin/order/advance/:id`、
|
||||
`PUT /v3/admin/group-batch/:id/staff/:id/reporter-rank`。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 团期财务总览 `GET /v3/admin/order/group-batch/:id/finance`
|
||||
|
||||
**VO**: `GroupBatchFinanceRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情切到「财务」Tab 时加载。一次返回顶部四张金额卡、逐户付款表与整团合计、已退团户数、主/次报账人,Tab 全部内容一个请求搞定。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `id` | path | Long | ✓ | 正整数 | 团期 ID(订单侧 `order_group_batch.group_batch_id`) |
|
||||
|
||||
无 query 参数,无请求体。需 `group-batch:finance:view` 权限码(比 `group-batch:view` 更严)。
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `receivableAmount` | BigDecimal | 整团应收 = Σ 活跃子订单(订单金额 + 附加费 − 优惠) |
|
||||
| `receivedAmount` | BigDecimal | 整团已收 = Σ 已付(累计毛额,不冲抵退款) |
|
||||
| `unpaidAmount` | BigDecimal | 整团待收 = 应收 − 已收,下限 0,即本期总尾款 |
|
||||
| `advanceApproved` | BigDecimal | 已预支(只计已通过)。**第四张卡取此值,不是三个数之和** |
|
||||
| `advancePending` | BigDecimal | 待审批预支,占额度但未出账,不计入「已预支」卡 |
|
||||
| `advanceAvailable` | BigDecimal | 可支取余额 = 尾款池 − (已通过 + 待审批),下限 0,即预支上限 |
|
||||
| `withdrawnCount` | Integer | 已退团户数(前端只拉活跃集,算不出这个数) |
|
||||
| `primaryPayeeName` | String | 主报账人姓名,未设置为 `null`(后端不兜底默认导游) |
|
||||
| `secondaryPayeeName` | String | 次报账人姓名,未设置为 `null` |
|
||||
| `totals.totalPrice` / `.paidAmount` / `.unpaidAmount` | BigDecimal | 逐户表末行「整团合计」,**服务端算**,与顶部卡同源 |
|
||||
| `items[].orderId` | String | 子订单 ID(字符串回传防精度丢失) |
|
||||
| `items[].orderNo` | String | 子订单号 |
|
||||
| `items[].customerName` | String | 客户姓名(不返回手机号与证件号) |
|
||||
| `items[].consultantName` | String | 定制师姓名 |
|
||||
| `items[].totalPrice` | BigDecimal | 本户应收 |
|
||||
| `items[].paidAmount` | BigDecimal | 本户已付。⚠️ 是**全额已付**,不是真定金拆分 |
|
||||
| `items[].unpaidAmount` | BigDecimal | 本户待收尾款 |
|
||||
| `items[].payStatus` | String | 仅 `UNPAID` / `DEPOSIT_PAID` / `FULLY_PAID` **三值** |
|
||||
| `items[].settleStatus` | String | `SETTLED` / `PENDING_BALANCE` / `WITHDRAWN`,**状态胶囊取此值** |
|
||||
| `items[].settleStatusText` | String | 结清状态中文,可直接渲染 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/90211/finance
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"receivableAmount": 40200.00,
|
||||
"receivedAmount": 32900.00,
|
||||
"unpaidAmount": 7300.00,
|
||||
"advanceApproved": 0.00,
|
||||
"advancePending": 0.00,
|
||||
"advanceAvailable": 7300.00,
|
||||
"withdrawnCount": 0,
|
||||
"primaryPayeeName": "张领队",
|
||||
"secondaryPayeeName": null,
|
||||
"totals": { "totalPrice": 40200.00, "paidAmount": 32900.00, "unpaidAmount": 7300.00 },
|
||||
"items": [
|
||||
{ "orderId": "770145", "orderNo": "GT-26-0081", "customerName": "罗敏", "consultantName": "李雯",
|
||||
"totalPrice": 12300.00, "paidAmount": 12300.00, "unpaidAmount": 0.00,
|
||||
"payStatus": "FULLY_PAID", "settleStatus": "SETTLED", "settleStatusText": "已结清" },
|
||||
{ "orderId": "770147", "orderNo": "GT-26-0083", "customerName": "汪洋", "consultantName": "陈璐",
|
||||
"totalPrice": 12300.00, "paidAmount": 5000.00, "unpaidAmount": 7300.00,
|
||||
"payStatus": "DEPOSIT_PAID", "settleStatus": "PENDING_BALANCE", "settleStatusText": "待收尾款" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无活跃子订单时,六个金额字段均为 `0.00`(**不是 null**),`items` 为空数组,`withdrawnCount` 为 `0`,两个报账人为 `null`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
| 码 | 含义 |
|
||||
|---|---|
|
||||
| `589500` | 团期不存在或已软删 |
|
||||
| `589507` | 缺 `group-batch:finance:view` 权限 |
|
||||
|
||||
```json
|
||||
{ "code": 589500, "msg": "团期不存在", "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 已取消与已软删子订单**不进任何金额、不进 `items`**,只贡献 `withdrawnCount`。
|
||||
- 隐式团(`IMPLICIT_SINGLE`)按一单退化开放,字段结构不变,**前端不得因团类型走两套渲染分支**。
|
||||
- 顶部「已收(定金)」卡与逐户「已付定金」列,语义都是**全额已付**,不区分定金/尾款;已结清户该列等于应收。
|
||||
- 整团应收与团期详情 `totalReceivable`、看板列表该行应收**三处同源**,数值必然一致。
|
||||
|
||||
---
|
||||
|
||||
### 2. 团期预支记录 `GET /v3/admin/order/group-batch/:id/advances`
|
||||
|
||||
**VO**: `List<GroupBatchAdvanceItemVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
财务 Tab 下半部「预支记录」区块,以及预支弹窗内的记录列表。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `id` | path | Long | ✓ | 正整数 | 团期 ID |
|
||||
|
||||
无 query 参数,**不分页**,一次返回本期全部。
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `id` | String | 预支单 ID |
|
||||
| `payeeStaffId` | String | 领款人主键快照 |
|
||||
| `payeeName` / `payeeRole` / `payeeRoleText` | String | 领款人姓名 / 角色码 / 角色中文 |
|
||||
| `advanceType` | String | 借款类型 |
|
||||
| `amount` | BigDecimal | 预支金额 |
|
||||
| `purpose` / `voucherUrl` | String | 用途说明 / 凭证 URL |
|
||||
| `status` / `statusText` | String | `SUBMITTED` / `APPROVED` / `REJECTED` 及其中文 |
|
||||
| `rejectReason` | String | 驳回原因 |
|
||||
| `createdByName` | String | 申请人姓名 |
|
||||
| `createTime` / `submittedAt` / `approvedAt` | String | 创建 / 提交 / 审批时间 |
|
||||
| `approvedBy` | String | 审批人姓名 |
|
||||
| `scope` | String | `ORDER` 订单级 / `GROUP_BATCH` 团期级(**本次新增**) |
|
||||
| `scopeName` | String | 归属维度中文,可直接渲染标签(**本次新增**) |
|
||||
| `orderNo` | String | 子订单号,`scope=ORDER` 时非空;团期级为 `null`(**本次新增**) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/90211/advances
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": [
|
||||
{ "id": "31005", "scope": "GROUP_BATCH", "scopeName": "团期预支", "orderNo": null,
|
||||
"payeeName": "朝鲁门", "payeeRole": "DRIVER", "payeeRoleText": "司机",
|
||||
"advanceType": "住宿押金", "amount": 5000.00, "purpose": "沿途住宿押金",
|
||||
"status": "APPROVED", "statusText": "已通过", "createdByName": "李雯",
|
||||
"submittedAt": "2026-08-18 22:58:26", "approvedAt": "2026-08-19 09:12:03" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无预支时返回空数组 `[]`,前端显示空态文案「暂无预支 · 在底部「预支」发起,记录将在此显示」(文案由前端提供,后端不返回)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
| 码 | 含义 |
|
||||
|---|---|
|
||||
| `589500` | 团期不存在 |
|
||||
| `589507` | 缺 `group-batch:finance:view` 权限 |
|
||||
|
||||
```json
|
||||
{ "code": 589500, "msg": "团期不存在", "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 返回**团期级 + 各子订单级**全部预支,按创建时间倒序。
|
||||
- **本接口不返回累计金额**,累计取接口 1 的 `advanceApproved` / `advancePending` / `advanceAvailable`。
|
||||
|
||||
---
|
||||
|
||||
### 3. 领款人候选 `GET /v3/admin/order/group-batch/:id/advance/payee-candidates`
|
||||
|
||||
**VO**: `List<AdvancePayeeCandidateVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
预支弹窗「借款对象」下拉。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `id` | path | Long | ✓ | 正整数 | 团期 ID(订单侧),服务端内部换算成产品侧,前端不感知 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `id` | Long | 候选 ID,即创建预支的 `payeeStaffId`(资源域 `staff.staff_id`) |
|
||||
| `staffName` | String | 姓名 |
|
||||
| `staffRole` | String | 角色代码 |
|
||||
| `staffRoleText` | String | 角色中文 |
|
||||
| `reporterRank` | String | `PRIMARY` / `SECONDARY` / `NONE` |
|
||||
| `isDefault` | Boolean | 主报账人为 `true`,前端默认选中 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/90211/advance/payee-candidates
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0, "msg": "success",
|
||||
"data": [
|
||||
{ "id": 88101, "staffName": "张领队", "staffRole": "GUIDE", "staffRoleText": "导游",
|
||||
"reporterRank": "PRIMARY", "isDefault": true },
|
||||
{ "id": 88102, "staffName": "朝鲁门", "staffRole": "DRIVER", "staffRoleText": "司机",
|
||||
"reporterRank": "NONE", "isDefault": false }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
团期未配人员时返回空数组 `[]`;此时预支无法提交(借款对象必填)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
| 码 | 含义 |
|
||||
|---|---|
|
||||
| `589500` | 团期不存在 |
|
||||
| `589507` | 缺 `group-batch:finance:view` 权限 |
|
||||
|
||||
```json
|
||||
{ "code": 589500, "msg": "团期不存在", "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 候选 = 本团**全部**人员(与订单级「可选本单任一人员」口径一致),排序:主报账人 → 次报账人 → 其余。
|
||||
- **不返回手机号**,与订单级候选保持一致。
|
||||
|
||||
---
|
||||
|
||||
### 4. 发起团期预支 `POST /v3/admin/order/group-batch/:id/advance`
|
||||
|
||||
**VO**: `OrderAdvanceRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
财务 Tab 底部操作条「预支」→ 弹窗填写 → 「申请预支」。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `id` | path | Long | ✓ | 正整数 | 团期 ID |
|
||||
| `payeeStaffId` | body | Long | ✓ | 须属本团人员 | 借款对象,取候选下拉项的 `id` |
|
||||
| `advanceType` | body | String | ✓ | 字典 `advance_type` 的 `dictValue` | 借款类型 |
|
||||
| `amount` | body | BigDecimal | ✓ | > 0 且 ≤ `advanceAvailable` | 预支金额 |
|
||||
| `purpose` | body | String | — | ≤ 255 | 用途说明 |
|
||||
| `voucherUrl` | body | String | — | ≤ 512 | 凭证 URL |
|
||||
|
||||
> ⚠️ 旧入参 `{amount, remark}` **已废除**。`remark` 无对应字段,改用 `purpose`。
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `id` | String | 新建的预支单 ID(旧实现返回 `Void`,**现在回传**) |
|
||||
| `payeeStaffId` | String | 领款人主键快照(团期级存资源域 `staff.staff_id`) |
|
||||
| `payeeName` / `payeeRole` / `payeeRoleText` | String | 领款人姓名 / 角色码 / 角色中文 |
|
||||
| `advanceType` | String | 借款类型 |
|
||||
| `amount` | BigDecimal | 预支金额 |
|
||||
| `purpose` / `voucherUrl` | String | 用途说明 / 凭证 URL |
|
||||
| `status` / `statusText` | String | 创建后恒为 `SUBMITTED` / 「待审批」 |
|
||||
| `createdByName` | String | 申请人姓名 |
|
||||
| `createTime` / `submittedAt` | String | 创建 / 提交时间 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/90211/advance
|
||||
{
|
||||
"payeeStaffId": 88102,
|
||||
"advanceType": "住宿押金",
|
||||
"amount": "3000.00",
|
||||
"purpose": "沿途住宿押金",
|
||||
"voucherUrl": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0, "msg": "success",
|
||||
"data": {
|
||||
"id": "31006", "payeeStaffId": "88102", "payeeName": "朝鲁门",
|
||||
"payeeRole": "DRIVER", "payeeRoleText": "司机",
|
||||
"advanceType": "住宿押金", "amount": 3000.00, "purpose": "沿途住宿押金",
|
||||
"status": "SUBMITTED", "statusText": "待审批",
|
||||
"createdByName": "李雯", "submittedAt": "2026-09-06 15:20:11"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
数据字典服务不可用时,借款类型校验降级为服务端内置集合,**不影响正常类型提交**。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
| 码 | 含义 |
|
||||
|---|---|
|
||||
| `589500` | 团期不存在 |
|
||||
| `589507` | 缺 `group-batch:finance:advance` 权限 |
|
||||
| `589538` | 团期状态不可发起预支(须为物料准备中 / 待出发 / 出行中) |
|
||||
| `589539` | 房 / 车 / 导 / 摄四项未配齐 |
|
||||
| `585003` | 预支金额必须大于 0 |
|
||||
| `585004` | 预支金额超过可用余额上限 |
|
||||
| `585006` | 借款类型非法 |
|
||||
| `585007` | 借款对象不属于本团期人员 |
|
||||
|
||||
```json
|
||||
{ "code": 585004, "msg": "预支金额超过可用余额上限", "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **双前置闸门**:团期状态 + 四项资源全就绪,缺一即拒(此前服务端两道都没有,只有前端做了置灰)。
|
||||
- **上限走团期统一池**:团期级与各子订单级**共扣一池**。团期把整团尾款预支满后,该团任一子订单再发起订单级预支同样会被 `585004` 拒——这是本次修复的超支漏洞。
|
||||
- 创建后进入**站内财务审批**,走既有 `advance-approvals` 列表与 approve / reject / 撤回三端点,与订单级完全一致。
|
||||
- 一期仍**只记账不出款**,实际出款走财务既有付款流程。
|
||||
- 同一团期 + 同一金额 5 秒内重复提交只成功一次(幂等)。
|
||||
|
||||
---
|
||||
|
||||
### 5. 预支审批列表 `GET /v3/admin/order/advance-approvals/page`
|
||||
|
||||
**VO**: `PageResult<AdvanceApprovalPageItemRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
财务管理 → 预支审批。团期级预支与订单级预支**混排在同一列表、走同一审批流程**。
|
||||
|
||||
#### 入参
|
||||
|
||||
新增一个可选 query 参数,其余入参不变:
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `scope` | query | String | — | `ALL` / `ORDER` / `GROUP_BATCH` | 归属维度筛选,缺省 `ALL`(两类混排,与改造前行为一致) |
|
||||
|
||||
`keyword` 语义扩展:由「订单号 / 团号 / 产品名」三路扩为**六路**,另加「团期号 / 团期名 / 团期产品名」。改造前搜索框标着「团号 / 产品」却搜不出团期级预支。
|
||||
|
||||
#### 出参
|
||||
|
||||
每行新增四个字段,其余不变:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `scope` | String | `ORDER` / `GROUP_BATCH` |
|
||||
| `scopeName` | String | 归属维度中文 |
|
||||
| `batchStatus` | String | 团期状态编码,仅 `scope=GROUP_BATCH` 时非空 |
|
||||
| `batchStatusName` | String | 团期状态中文 |
|
||||
|
||||
**既有字段按 `scope` 择一填充,前端零改动即可显示**:
|
||||
|
||||
| 字段 | `scope=ORDER` | `scope=GROUP_BATCH` |
|
||||
|---|---|---|
|
||||
| `teamNo`(团号列) | 订单团号 | **团期号** |
|
||||
| `productName` | 订单产品名 | 团期产品名 |
|
||||
| `departDate` / `returnDate`(行程列) | 订单出行日期 | 团期出团 / 返程日 |
|
||||
| `orderAmount` / `paidAmount` | 本单应收 / 已收 | **整团应收 / 已收** |
|
||||
| `orderNo` / `consultantName` | 有值 | `null`(前端显「—」) |
|
||||
| `orderStatus` / `flowStatus` / `payStatus` / `settlementStatus` | 有值 | `null`,改看 `batchStatus` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/advance-approvals/page?status=SUBMITTED&scope=GROUP_BATCH&page=1&pageSize=20
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0, "msg": "success",
|
||||
"data": {
|
||||
"records": [
|
||||
{ "id": "31006", "scope": "GROUP_BATCH", "scopeName": "团期预支",
|
||||
"orderNo": null, "teamNo": "GT-26-05", "productName": "呼伦贝尔草原 5 日游",
|
||||
"departDate": "2026-09-20", "returnDate": "2026-09-24", "consultantName": null,
|
||||
"orderStatus": null, "batchStatus": "PENDING_DEPARTURE", "batchStatusName": "待出发",
|
||||
"orderAmount": 40200.00, "paidAmount": 32900.00,
|
||||
"payeeName": "朝鲁门", "advanceType": "住宿押金", "amount": 3000.00,
|
||||
"status": "SUBMITTED", "statusText": "待审批" }
|
||||
],
|
||||
"total": 1, "page": 1, "pageSize": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无匹配记录时 `records` 为空数组,`total` 为 `0`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
沿用改造前,本次未新增错误码。
|
||||
|
||||
```json
|
||||
{ "code": 589507, "msg": "无操作权限", "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **前端必须容忍 `orderId` / `orderNo` 为 `null`**(团期级行不挂订单)。
|
||||
- 审批通过 / 驳回 / 撤回三个端点**零改动**,对团期级行行为与订单级完全一致。
|
||||
- 团期级预支走**站内财务审批(资金审批)**,与流团 / 退单户的企微 OA **业务审批**是两条线,不合并。
|
||||
|
||||
---
|
||||
|
||||
### 6. 整团核算汇总 `GET /v3/admin/order/group-batch/:id/settlement/summary`
|
||||
|
||||
**VO**: `GroupBatchSettlementSummaryRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
整团核算页。本次新增两个只读字段,用于**核单时把整团预支从主报账人代收的尾款中扣回**。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `id` | path | Long | ✓ | 正整数 | 团期 ID。入参未变 |
|
||||
|
||||
#### 出参
|
||||
|
||||
新增两个字段,其余不变:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `groupAdvanceApproved` | BigDecimal | 整团已拨付预支(**只含团期级**),核算时单独成一行扣回 |
|
||||
| `groupAdvancePending` | BigDecimal | 整团待审批预支,占额度未出账,仅提示 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/90211/settlement/summary
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0, "msg": "success",
|
||||
"data": {
|
||||
"settledOrderCount": 3, "totalActiveOrderCount": 3,
|
||||
"subOrderTotalActualCost": 21000.00, "sharedCostTotal": 6000.00,
|
||||
"grandTotalCost": 27000.00,
|
||||
"groupAdvanceApproved": 5000.00,
|
||||
"groupAdvancePending": 0.00
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无团期级预支时两个字段均为 `0.00`(不是 null)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
沿用改造前。
|
||||
|
||||
```json
|
||||
{ "code": 589500, "msg": "团期不存在", "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 🚫 **只含团期级预支,不含子订单级**。子订单级预支已进各户的核单报销单与对账,整团层再算一次会**同一笔钱扣两遍**。
|
||||
- 🚫 **不计入 `grandTotalCost`**。预支是**资金拨付**不是成本,计入会与核销后的真实成本科目重复计成本。前端渲染为「整团成本 X / 其中已拨付预支 Y」的独立一行,**不参与任何成本或毛利公式**。
|
||||
- 逐户报销单与逐户对账**一个字未改**。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
1. **财务 Tab 一次请求拿全**:接口 1 已含四张卡、逐户表、整团合计、退团户数、报账人,不要再拼 `/orders`。
|
||||
2. **金额一律用服务端给的值**。整团合计由服务端算并与四张卡同源;前端自行 sum 会与卡片对不上。
|
||||
3. **预支上限读 `advanceAvailable`**,不要用「待收尾款 − 记录之和」本地推算。
|
||||
4. **状态胶囊读 `settleStatus`**,不要用 `payStatus` 推——后者只有三值,表达的是支付事件不是结清与否,已退团户用它渲染会显示成「已付定金」。
|
||||
5. **设置报账人用产品侧 `productBatchId`**(团期详情接口取),不是财务 Tab 路径上的 `groupBatchId`。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
> 只写外部可观察行为。
|
||||
|
||||
| 动作 | 可观察结果 |
|
||||
|---|---|
|
||||
| 发起团期预支 | 新增一条待审批预支记录,立即出现在本团期预支记录列表与预支审批列表;团期与订单的任何金额字段**均不变动**(预支不改应收/已收/待收) |
|
||||
| 审批通过 | 该记录转为「已通过」,财务 Tab 的「已预支」增加、「待审批预支」减少、「可支取余额」不变(原本就已占额度) |
|
||||
| 审批驳回 / 撤回 | 该记录转为「已驳回」或从列表消失,占用的额度**当场释放**,「可支取余额」回升 |
|
||||
| 上线迁移(一次性) | 存量团期原有的累计预支金额,会转成一条「已通过」的**期初结转**记录出现在预支记录列表,金额与迁移前的累计值相等;该记录无领款人与凭证,按借款类型「期初结转」可识别 |
|
||||
|
||||
**幂等**:同一团期 + 同一金额 5 秒内重复提交只成功一次。
|
||||
|
||||
**并发**:同一团期的预支申请串行处理,两个管理员同时提交不会双双突破可支取余额。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
| 场景 | 行为 |
|
||||
|---|---|
|
||||
| 团期无活跃子订单 | 六个金额字段 `0.00`,`items` 空数组 |
|
||||
| 子订单已取消 | 不进金额、不进 `items`,只计入 `withdrawnCount` |
|
||||
| 未设置报账人 | `primaryPayeeName` 为 `null`,后端不兜底默认导游 |
|
||||
| 团期状态为招募中 / 核单中 | 发起预支返回 `589538` |
|
||||
| 四项资源缺任一 | 发起预支返回 `589539` |
|
||||
| 团期尾款池已被预支占满 | 团期级与该团任一子订单级预支**均**返回 `585004` |
|
||||
| 数据字典服务不可用 | 借款类型降级到内置集合校验,正常类型仍可提交 |
|
||||
| 审批列表出现团期级行 | `orderId` / `orderNo` / `consultantName` / 订单四态均为 `null` |
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **订单级预支全链路行为零变化**:创建 / 审批 / 驳回 / 撤回 / 本单列表五个端点与改造前逐字一致(底表 `order_id` 由非空放宽为可空,但既有查询全是等值匹配,空值行天然不命中)。
|
||||
- **逐户核单报销单与逐户对账未改**:仍只含本户预支。
|
||||
- **审批通过 / 驳回 / 撤回三端点未改**。
|
||||
- **设置报账人端点未改**。
|
||||
- 前端页面、打印导出、发票区、预支实际出款均不在本次范围。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
> ⏳ **尚未验证**。本文档随 PR #7170 提交,待合并并部署测试服后过网关实测,届时更新
|
||||
> `backend_status` / `gateway_status` / `verified_at` 并补本节实测结果。
|
||||
|
||||
计划实测要点:
|
||||
|
||||
1. 财务 Tab 四张卡与逐户表末行合计一致,且与团期详情、看板列表三处应收同源。
|
||||
2. **超支被堵死**:团期级把整团尾款预支满 → 该团任一子订单再发起订单级预支被 `585004` 拒。
|
||||
3. **核单零重复扣**:团期级预支 5,000 通过 + 某户订单级预支 2,000 通过 → 该户报销单含 2,000,整团 `groupAdvanceApproved` 仍是 5,000;`grandTotalCost` 不变。
|
||||
4. 两道闸门各拒一次(`589538` / `589539`)。
|
||||
5. 团期级预支出现在预支审批列表,「团号 / 产品」「行程」两列有值,按团期号搜得到,就地通过 / 驳回 / 撤回正常。
|
||||
6. 订单级预支五端点回归无变化。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 团期模块接口文档 v2.0 §0B(预支权威契约)、§0A.3(金额口径)、卡片 GB-ADM-040 / 041 / 042 / 043
|
||||
- 团期模块数据模型 §A.11(预支复用订单预支)、§A.11.10(团期层单独对账)
|
||||
- 团期模块表结构 v1.2 §4-5(`order_advance` 改造 DDL)
|
||||
|
||||
**待回写正式稿**(本次实现与文档不一致处):
|
||||
|
||||
| 处 | 文档现状 | 实际 |
|
||||
|---|---|---|
|
||||
| §0B.9 错误码 | 「统一用 `AdvanceErrorCode`(585 段)」 | 改落 `589538` / `589539`;585 段被 v2/v3 整段重叠声明且 585001-585010 已被 order-v2 实占 |
|
||||
| GB-ADM-040 出参 | `payStatus` 写五值含 `REFUNDING` / `REFUNDED` | 代码只有三值;结清状态另出 `settleStatus` 字段 |
|
||||
| GB-ADM-040 出参 | 含 `advanceTotal` | 已废,改三个数 |
|
||||
| GB-ADM-042 | 返回 `GroupBatchWriteResultVO`、不回传 `advanceId` | 改返 `OrderAdvanceRespVO` 并回传 `advanceId` |
|
||||
| §0A.3 | 「金额缺口是本期 P0,详情恒返回 0.00」 | 已被 #6905 / #6902 实时聚合关闭 |
|
||||
| §0B.6 | 「查询键列名骗人,必须进 CR checklist」 | `V20260904_001` 改名后该坑消失 |
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: #7154
|
||||
- **PR**: #7170(base `dev-v3`)
|
||||
- **后端**: jw
|
||||
- **前端**: 待认领(三个新端点 + 预支弹窗入参更换 + 审批列表 `scope` 标签)
|
||||
@@ -0,0 +1,424 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7158"
|
||||
title: "团期手动成团门槛与提前成团留痕"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "后端已部署 TEST 并实测:成团带理由成功、重复成团 589537、不传 body 兼容路径均通过。前端待接:成团弹窗三格读 minToForm、产品排期表单加最低成团户数输入框。"
|
||||
updated_at: "2026-09-06"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期手动成团:成团门槛 min_to_form 与提前成团留痕
|
||||
|
||||
> **影响范围**:管理后台「团期详情 → 整团总览 → 成团」弹窗,以及产品「Step4 班期」维护表单。
|
||||
> 当前状态:后端已部署 TEST 并实测;前端待接入。
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **成团接口新增可选请求体**。`POST .../group` 原来无请求体,现在可传 `formedNote`(提前成团理由)。
|
||||
**不传 body 时行为与之前完全一致**,前端不改也不会坏。
|
||||
2. **成团新增权限校验** `group-batch:manage`。此前该接口无任何权限校验。
|
||||
3. **重复成团改为语义化错误码 589537**,此前返回笼统的 589501「团期状态不允许当前操作」。
|
||||
4. **新增成团门槛字段 `minToForm`**(最低成团**户数**,户 = 订单 = 房)。
|
||||
团期详情与产品班期两侧都新增该字段,供成团弹窗三格渲染。
|
||||
|
||||
## 一、背景
|
||||
|
||||
招募中的团期需管理员手动点「成团」才进入成团状态,进入后才能开始需求审核 / 配置资源。
|
||||
成团弹窗顶部要显示三格「已售 5/9 户 · 成团标准 满 6 户 · 距标准差 1 户」,
|
||||
但此前**成团标准没有任何数据源**:团期侧的 `minGroupPeople` 是「人数」口径且全局无写入方恒为 null,
|
||||
产品侧只有按人数的最低成团人数。本次新增按**户数**的 `minToForm` 补齐这条链路。
|
||||
|
||||
**门槛只提示不拦截**:未达标也可提前成团,这是业务要求。系统在任何情况下都不自动成团。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 1 | 团期手动成团 | POST | `/v3/admin/order/group-batch/{groupBatchId}/group` | 修改接口 | 新增可选请求体 formedNote;新增权限校验;重复成团改 589537 |
|
||||
| 2 | 团期详情 | GET | `/v3/admin/order/group-batch/{groupBatchId}` | 修改接口 | 出参新增 minToForm(成团门槛户数) |
|
||||
| 3 | 产品班期修改 | PUT | `/admin/product/item/{id}/schedule` | 修改接口 | 入参新增 minToForm(最低成团户数) |
|
||||
| 4 | 产品班期列表 | GET | `/admin/product/item/{id}/schedule/list` | 修改接口 | 出参新增 minToForm |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 团期手动成团 `POST /v3/admin/order/group-batch/{groupBatchId}/group`
|
||||
|
||||
**VO**: `GroupBatchFormReqVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理员在「团期详情 → 整团总览」底部操作条点「成团」,弹窗确认后调用。
|
||||
仅招募中(`RECRUITING`)的团期可成团;成团后团期进入资源准备中,才能开始需求审核与配置资源。
|
||||
已售户数未达成团门槛时也可以成团(提前成团),此时建议在弹窗里填写理由。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| groupBatchId | path | Long | 是 | 正整数 | 团期 ID |
|
||||
| formedNote | body | String | 否 | 最长 256 字 | 提前成团理由。整个 body 可以不传;不传等同于不带理由,行为与本次改动前一致 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| code | Integer | 200 表示成团成功 |
|
||||
| message | String | 提示文案 |
|
||||
| data | Object | 固定为 null,本接口无返回体 |
|
||||
| success | Boolean | true 表示成功 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"formedNote": "客户催促,线下已谈妥另外 1 户"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口成功时 `data` 恒为 null,不存在空数据形态。
|
||||
成团流水文案里的「已售 N/M 户」依赖实时聚合,聚合失败时降级为不带户数的文案,
|
||||
**不影响成团本身成功**,前端无需为此做特殊处理。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589537,
|
||||
"message": "团期已成团,不可重复成团",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 仅 `RECRUITING` 状态可成团;已成团(含之后各状态)返回 589537,已流团返回 589501。
|
||||
- 重复成团**零副作用**:不写状态、不写流水、不触发任何下游动作。
|
||||
- 未达成团门槛**不拦截**,照常成团,仅在成团流水里标注未达标与理由。
|
||||
- 需要 `group-batch:manage` 权限,无权限返回 589507。
|
||||
- 系统在任何情况下都不自动成团;达标只代表「允许成团」。
|
||||
|
||||
### 2. 团期详情 `GET /v3/admin/order/group-batch/{groupBatchId}`
|
||||
|
||||
**VO**: `GroupBatchDetailRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
打开团期详情页时调用。本次新增的 `minToForm` 与既有 `enrolledRooms` / `maxRooms`
|
||||
一起,足以渲染成团弹窗顶部三格,前端不需要再调其他接口。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| groupBatchId | path | Long | 是 | 正整数 | 团期 ID |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| minToForm | Integer | **本次新增**。最低成团户数,户 = 订单 = 房。null 或 0 表示未设门槛 |
|
||||
| enrolledRooms | Integer | 已售户数(成团弹窗第一格分子) |
|
||||
| maxRooms | Integer | 满团户数(第一格分母),0 表示不限 |
|
||||
| remainRooms | Integer | 剩余户数,不限时为 null |
|
||||
| minGroupPeople | Integer | 历史字段,最低成团**人数**,全局无写入方恒为 null。成团门槛请改用 minToForm |
|
||||
| batchStatus | String | 团期状态码 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2096412454643802114
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2096412454643802114",
|
||||
"batchStatus": "RECRUITING",
|
||||
"enrolledRooms": 1,
|
||||
"maxRooms": 8,
|
||||
"remainRooms": 7,
|
||||
"minToForm": 6,
|
||||
"minGroupPeople": null
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
`minToForm` 为 null 或 0 表示该团期未设成团门槛。
|
||||
此时前端**只显示第一格「已售 X/Y 户」**,不显示「成团标准」与「距标准差」两格。
|
||||
本次上线前建出的存量团期一律是这种情况,不做数据回填。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589500,
|
||||
"message": "团期不存在",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 三格渲染口径:已售 `enrolledRooms`/`maxRooms` 户;成团标准 满 `minToForm` 户;距标准差 `max(0, minToForm - enrolledRooms)` 户。
|
||||
- `minToForm` 是**建团时从产品班期快照**下来的,之后改产品班期不会回改已建出的团期。
|
||||
- `minGroupPeople` 与 `minToForm` 是两个不同口径的字段,并存且互不换算,不要混用。
|
||||
|
||||
### 3. 产品班期修改 `PUT /admin/product/item/{id}/schedule`
|
||||
|
||||
**VO**: `ScheduleSaveReqVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
产品维护 Step4「班期」表单保存时调用。本次新增 `minToForm`,
|
||||
即该班期的最低成团户数,与既有 `maxRooms`(满团户数)成对维护。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| id | path | Long | 是 | 正整数 | 产品 ID |
|
||||
| batchId | body | Long | 是 | 修改时必传 | 班期 ID |
|
||||
| minToForm | body | Integer | 否 | 非负整数 | **本次新增**。最低成团户数,空或 0 表示不限 |
|
||||
| maxRooms | body | Integer | 否 | 非负整数 | 满团户数,0 表示不限 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| data | Long | 班期 ID |
|
||||
| success | Boolean | 是否成功 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"batchId": 2052935476557328386,
|
||||
"departureDate": "2026-10-01",
|
||||
"adultPrice": 2925.00,
|
||||
"childPrice": 2425.00,
|
||||
"singleRoomDiff": 500.00,
|
||||
"maxRooms": 8,
|
||||
"minToForm": 6
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": 2052935476557328386,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
`minToForm` 不传时服务端兜底为 0(不限),与 `maxRooms` / `maxParticipants` 的兜底口径一致。
|
||||
存量班期在本次 DDL 后取默认值 0,不做回填。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 403,
|
||||
"message": "无操作权限",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `minToForm` **只服务团期运营台的成团弹窗与成团留痕**,不参与任何下单与库存判定。
|
||||
- 库存与满团判定仍然只看 `maxRooms` / `maxParticipants`,本次不受影响。
|
||||
- 与既有的按人数的最低成团人数字段并存、互不覆盖、互不换算。
|
||||
- 改这里**不会**回改已经建出的团期,团期侧是建团时的快照。
|
||||
|
||||
### 4. 产品班期列表 `GET /admin/product/item/{id}/schedule/list`
|
||||
|
||||
**VO**: `ScheduleRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
产品维护 Step4「班期」列表回显,以及班期编辑弹窗打开时取当前值。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| id | path | Long | 是 | 正整数 | 产品 ID |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| minToForm | Integer | **本次新增**。最低成团户数,0 表示不限 |
|
||||
| maxRooms | Integer | 满团户数,0 表示不限 |
|
||||
| batchId | Long | 班期 ID |
|
||||
| departureDate | String | 出发日期 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/product/item/2044306857534636034/schedule/list
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": [
|
||||
{
|
||||
"batchId": 2052935476557328386,
|
||||
"batchNo": "Q202610012052935476548939777",
|
||||
"departureDate": "2026-10-01",
|
||||
"maxRooms": 8,
|
||||
"minToForm": 0
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
产品下无班期时 `data` 为空数组。
|
||||
存量班期的 `minToForm` 一律回显 0,表示尚未设置成团门槛,属正常值而非异常。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "产品不存在",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 列表回显的 `minToForm` 是产品侧权威值;团期侧显示的是建团时的快照,两者可能不同,属预期。
|
||||
- 该字段不影响列表排序与筛选。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- **成团请求体整体可选**。前端可以继续用无 body 的旧调用,也可以传 `{"formedNote": "..."}`。
|
||||
建议:已售户数未达 `minToForm` 时在弹窗里出现理由输入框并把值传上来。
|
||||
- **理由本期不强制必填**。服务端不会因为没填理由而拒绝成团,前端可自行决定是否做前端必填。
|
||||
- **成团弹窗三格全部来自团期详情接口**,不需要额外接口。`minToForm` 为 null 或 0 时只显示第一格。
|
||||
- **同一份 `ScheduleSaveReqVO` 也被新建班期 `POST /admin/product/item/{id}/schedule` 使用**,
|
||||
该接口同样接受 `minToForm`,语义与修改班期完全一致;批量创建
|
||||
`POST /admin/product/item/{id}/schedule/batch-create` 使用 `ScheduleBatchCreateReqVO`,
|
||||
同样新增了 `minToForm` 并逐个透传给每个新建班期。这三个入口共用一套口径,前端按同一字段接入即可。
|
||||
- **权限**:成团需要 `group-batch:manage`。该权限已随本次发布授予 `ADMIN` 与 `SUPER_ADMIN`。
|
||||
若线上还有其他角色需要点成团,需要单独补授,否则会被拦为 589507。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 成团成功时推进团期状态,并写入一条**成团操作流水**,内容形如
|
||||
「手动成团 · 已售 1/8 户 · 未达标(满6户) · 理由:客户催促」,含操作人与时间。
|
||||
未设门槛时省略达标判定段,未填理由时省略理由段。
|
||||
- 重复成团、无权限、团期不存在三种情况**均不产生任何写入**。
|
||||
- 成团门槛在产品侧维护、在建团时快照到团期侧,之后两侧独立,不做双向同步。
|
||||
- 存量数据不回填:本次上线前已存在的班期与团期,成团门槛一律为默认值(0 或空)。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未达成团门槛照常可以成团,门槛只影响提示与流水文案。
|
||||
- 系统在任何情况下都不自动成团,达标只代表「允许成团」。
|
||||
- 成团后的既有下游行为不变:派生导游 / 摄影需求标志、免闸置位、尝试推进物料准备、驱动子订单进需求态并派发配房配车待办。
|
||||
- 成团流水文案里的已售户数与团期详情同源(实时聚合),不读持久计数列,两处显示不会打架。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 项 | 修改前 | 修改后 |
|
||||
| --- | --- | --- |
|
||||
| 成团请求体 | 无请求体 | 可选 body,含 `formedNote`;不传时行为不变 |
|
||||
| 成团权限 | **无任何权限校验** | 需要 `group-batch:manage`,无权限 589507 |
|
||||
| 重复成团 | 589501「团期状态不允许当前操作」 | 589537「团期已成团,不可重复成团」 |
|
||||
| 成团流水 | 固定文案「成团」 | 带已售 / 满团 / 达标情况 / 提前成团理由 |
|
||||
| 团期详情成团门槛 | 只有恒为 null 的 `minGroupPeople`(人数口径) | 新增 `minToForm`(户数口径),弹窗三格可渲染 |
|
||||
| 产品班期成团门槛 | 只有按人数的最低成团人数 | 新增按户数的 `minToForm`,可在班期表单维护 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **对现有前端调用零破坏**:成团请求体可选,不传 body 的老调用行为与改动前完全一致,已在 TEST 实测确认。
|
||||
- **权限是行为变更**:`group-batch:manage` 已随发布授予 `ADMIN` / `SUPER_ADMIN`;
|
||||
若还有其他角色在点成团,上线后会被拦住,需要补授权限。这是本次唯一需要运维配合的点。
|
||||
- **错误码语义变更**:重复成团由 589501 变为 589537。若前端对 589501 做过特殊处理,需要同步识别 589537。
|
||||
- **新增字段均为增量**,不改动任何既有字段的类型与含义;`minGroupPeople` 保持原样不动。
|
||||
- 团期成团的下游链路(子订单推进、待办派发)本次未做任何改动。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 取消成团、流团两个接口本次未改动,仍然没有权限校验,将另行统一处理。
|
||||
- 下单、库存扣减、满团判定完全不受影响,仍只看 `maxRooms` / `maxParticipants`。
|
||||
- 小程序端不受影响,本次改动全部在管理后台侧。
|
||||
- 成团通知(公众号 / 短信)本期不做,弹窗上的通知勾选框暂无后端对应入参,成团后仍需人工通知客户。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
TEST 环境已部署 product-v2、user-service、order-v3 三个服务并实测通过:
|
||||
|
||||
- 团期详情返回 `minToForm` / `maxRooms` / `enrolledRooms` / `remainRooms`,`minGroupPeople` 保持并存。
|
||||
- 带 `formedNote` 成团成功,团期由招募中进入资源准备中。
|
||||
- 重复成团返回 589537「团期已成团,不可重复成团」。
|
||||
- 不传请求体再次调用同样走到业务校验而非参数错误,确认请求体确实可选。
|
||||
- 产品班期列表返回 `minToForm`,存量班期回显 0。
|
||||
|
||||
未在测试环境覆盖的三项:成团流水文案(团期流水目前无对外查询接口,由单元测试保证)、
|
||||
产品班期写入 `minToForm`(受产品模块数据权限限制未能实调)、
|
||||
建团快照(需在设置门槛后新建团期订单才能观察)。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 工单:HL#7158
|
||||
- 合并:HL PR#7168(已合入 dev-v3)
|
||||
- 前置工单:HL#7104(成团驱动子订单流程推进),本次改成团入口,与其下游改动互不冲突
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- 后端:jw
|
||||
- 前端待接两项:成团弹窗三格读取 `minToForm` 并在未达标时出现提前成团理由输入框;
|
||||
产品 Step4 班期表单新增「最低成团户数」输入框。
|
||||
@@ -0,0 +1,225 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7178"
|
||||
title: "团期调整满团名额同步产品域库存"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "修复「调了不生效」:调名额此前只写订单域,而库存权威在产品域。后端已部署 TEST 并实测:加减名额驱动产品域满团即停/放开继续招募、低于已报名拒绝且零写入、物料准备起阶段门。请求体形状已变更(净增量),前端待接。"
|
||||
updated_at: "2026-09-06"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期调整满团名额:同步产品域库存 + 阶段门 + 净增量入参
|
||||
|
||||
> **影响范围**:管理后台「团期详情 → 整团总览 → 调整满团名额」弹窗。
|
||||
> 当前状态:后端已部署 TEST 并实测;前端待接入(**请求体形状已变更**)。
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **修复「调了不生效」**。此前调名额只写订单域,而下单侧的剩余名额由**产品域**算出,
|
||||
于是加名额放不出空位、减名额停不了售,但看板数字会变。现在调整会真正驱动售卖状态。
|
||||
2. **请求体不兼容变更**:由 `{maxParticipants, maxRooms}` 两个绝对值,
|
||||
改为 `{capacityDelta, reason}` —— **净增量、只调户数**。
|
||||
3. **新增阶段门**:仅「招募中」与「资源准备中」可调,物料准备中及之后返回 589538。
|
||||
4. **新增权限校验** `group-batch:manage`(此前该接口无任何权限校验)。
|
||||
5. **响应由空改为返回结果对象**,直接给出弹窗三格所需数据。
|
||||
|
||||
## 一、背景
|
||||
|
||||
调整满团名额是给运营临时增减本期可报名户数用的。此前的实现只更新了团期侧的展示值,
|
||||
没有同步到真正决定「还能不能报名」的那一侧,导致这个功能实际上不起作用——
|
||||
**页面上数字变了,但客户仍按老上限被卡住**。本次修复把调整落到库存权威侧,
|
||||
并按新库存重算班期是否已满额。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 1 | 调整团期满团名额 | PUT | `/v3/admin/order/group-batch/{groupBatchId}/capacity` | 修改接口 | 入参改为净增量并只调户数;新增阶段门与权限校验;同步产品域库存;响应改为结果对象 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 调整团期满团名额 `PUT /v3/admin/order/group-batch/{groupBatchId}/capacity`
|
||||
|
||||
**VO**: `AdjustCapacityReqVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理员在「团期详情 → 整团总览」底部操作条点「调整满团名额」,
|
||||
弹窗用 −/+ 步进器改满团户数,点「保存名额」时把**净变化量**提交给本接口。
|
||||
|
||||
典型用法:某期已满员但还有客户想报,加 2 个名额把空位放出来继续招募;
|
||||
或临近出团减名额提前收口。**已成团的团期加名额后仍保持成团,不会退回招募中。**
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| groupBatchId | path | Long | 是 | 正整数 | 团期 ID |
|
||||
| capacityDelta | body | Integer | 是 | 不能为 0 | 满团名额净增量(户),可正可负。新满团户数 = 当前满团户数 + 该值 |
|
||||
| reason | body | String | 否 | 最长 256 字 | 调整原因,填了写进团期操作记录。**不强制必填** |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| batchId | String | 团期 ID |
|
||||
| beforeMaxRooms | Integer | 调整前满团户数 |
|
||||
| maxRooms | Integer | 调整后满团户数,0 表示不限 |
|
||||
| capacityDelta | Integer | 本次净增量 |
|
||||
| enrolledRooms | Integer | 已报名户数(户 = 订单 = 房) |
|
||||
| remainRooms | Integer | 调整后余量 = max(0, maxRooms − enrolledRooms);满团户数为 0(不限)时为 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"capacityDelta": 3,
|
||||
"reason": "客户加订,放三个空位继续招募"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"batchId": "2096510069465088002",
|
||||
"beforeMaxRooms": 2,
|
||||
"maxRooms": 5,
|
||||
"capacityDelta": 3,
|
||||
"enrolledRooms": 1,
|
||||
"remainRooms": 4
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
调整后满团户数为 0 时表示**不限名额**,此时 `remainRooms` 返回 null,
|
||||
前端不显示余量。这是正常语义而非异常。
|
||||
|
||||
本接口没有空列表形态;任何失败都以错误码返回,且**失败一律零写入**——
|
||||
产品侧与团期侧都不会留下半改状态。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589538,
|
||||
"message": "物料准备开始后不可再调整满团名额(仅招募中、资源准备中可调)",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **仅「招募中」与「资源准备中」可调**;物料准备中及之后返回 589538。
|
||||
- **调整后不得低于已报名户数**,否则返回 589509;新值也不得为负。
|
||||
- **新值为 0 表示不限名额**,此时不受已报名数约束。
|
||||
- `capacityDelta` 为 0 返回 589539(等于没调)。
|
||||
- 需要 `group-batch:manage` 权限,无权限返回 589507。
|
||||
- **调整不改团期状态**:已成团的仍保持成团,不会退回招募中。
|
||||
- 产品侧同步失败时整笔失败并返回 589540,团期侧零写入。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- **提交净增量,不要提交总数**。前端步进器算出的总数只用于展示,
|
||||
提交时给差值即可。这样两人同时调也不会互相覆盖成对方的总数。
|
||||
- **不再接收人数容量**。此前的 `maxParticipants` 字段已移除,人数容量由产品侧维护。
|
||||
- **调整前先看能不能调**:团期进入物料准备后按钮应置灰,避免用户点了才报错。
|
||||
- **弹窗三格数据**:调整前可从团期详情取「当前名额 / 已报名」;
|
||||
调整成功后直接用本接口返回的 `maxRooms` / `enrolledRooms` / `remainRooms` 刷新,
|
||||
不必再拉一次详情。
|
||||
- **重试是安全的**。若前端超时后重试同一请求,服务端以团期侧当前值为基数重算,
|
||||
不会把同一个增量叠加两次。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 调整会**同时更新产品侧与团期侧的满团户数**,并按新库存重算班期是否已满额:
|
||||
售罄时停止继续接单,库存恢复时重新开放报名。
|
||||
- **先写产品侧,成功后才写团期侧**;产品侧写失败即整笔失败、团期侧不留任何痕迹。
|
||||
- 每次成功调整写一条团期操作记录,内容形如
|
||||
「满团名额:9→10(+1) · 理由:客户加订一间」,含操作人与时间;未填理由时省略后半段。
|
||||
- 拒绝的三种情况(阶段不允许 / 低于已报名 / 增量为 0)**均不产生任何写入**。
|
||||
- 历史上两侧数值已经不一致的团期,本次不做批量订正;下一次调整会自动把两侧拉齐。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 已成团的团期加名额后**仍保持成团**,只是把空位放出来继续招募,满团即停。
|
||||
- 减名额减到正好等于已报名户数是允许的(余量为 0),再减一户即被拒绝。
|
||||
- 满团户数为 0 表示不限,此时无论已报名多少都不触发下限校验。
|
||||
- 团期未绑定产品班期时返回 589540(无处可写,不静默放过)。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 项 | 修改前 | 修改后 |
|
||||
| --- | --- | --- |
|
||||
| 是否真正影响售卖 | ❌ 只改团期侧展示值,**加名额放不出空位、减名额停不了售** | ✅ 同步到库存权威侧,满团即停 / 放开继续招募都生效 |
|
||||
| 请求体 | `{maxParticipants, maxRooms}` 两个绝对值,均必填 | `{capacityDelta, reason}`,净增量、只调户数 |
|
||||
| 响应 | 空(调完要再拉一次详情) | 返回调整前后值、已报名与余量 |
|
||||
| 阶段限制 | **无**,任何状态都能调 | 仅招募中与资源准备中,其余 589538 |
|
||||
| 权限 | **无任何校验** | `group-batch:manage` |
|
||||
| 调整原因 | 无此入参 | `reason` 选填,写进操作记录 |
|
||||
| 操作记录 | 只有「最大人数:20→24,最大房间:8→9」 | 「满团名额:9→10(+1) · 理由:…」 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **请求体不兼容**:字段整体更换。上线前已核对管理后台前端仓库,
|
||||
**没有任何页面在调用本接口**(原型侧本就是提交净增量,只是此前只在前端本地累加),
|
||||
因此未设兼容期。若有未知调用方,需同步改造。
|
||||
- **阶段门是行为变更**:此前任何状态都能调,现在物料准备后会被拒。
|
||||
这是有意收紧——那之后房车已按户数配好,改名额会让资源计划失真。
|
||||
- **权限是行为变更**:`group-batch:manage` 已随上一单建好并授予管理员与超级管理员,
|
||||
本次直接复用,无需额外配置。
|
||||
- **资源准备中调整的已知风险**:该阶段房务/车务可能已按当前户数派单订房订车,
|
||||
此时加减名额**不会回头改动已派资源**,需人工复核资源计划。
|
||||
- 新增字段与新响应均为增量,不改动任何既有字段的类型与含义。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 下单与库存扣减的判定口径不变,仍按满团户数与人数上限;本次只是让调整真正作用到它。
|
||||
- 人数容量不受影响,仍由产品侧维护。
|
||||
- 团期状态机不变:调整不推进也不回退任何状态。
|
||||
- 成团、取消成团、流团三个动作本次未改动。
|
||||
- 小程序端不受影响,改动全部在管理后台侧。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
TEST 环境已部署产品服务与订单服务并实测通过:
|
||||
|
||||
- 加名额 +1:产品侧满团户数随之变化,团期侧同步,返回三格数据。
|
||||
- 减名额 −2:两侧同步。
|
||||
- 减到正好等于已报名户数:班期转为**已满额、停止接单**。
|
||||
- 再加 1 户:班期**恢复报名中**,空位重新放出。
|
||||
- 减到低于已报名户数:拒绝且**零写入**(产品侧数值未变)。
|
||||
- 增量为 0:拒绝。
|
||||
- 物料准备中及之后调整:拒绝且零写入。
|
||||
- **已成团(资源准备中)加名额:成功,班期继续招募,团期仍保持成团不回退**。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 工单:HL#7178
|
||||
- 合并:HL PR#7181(已合入 dev-v3)
|
||||
- 关联工单:HL#7158(团期手动成团),本单复用其建立的 `group-batch:manage` 权限码;
|
||||
弹窗顶部「满 6 户成团 · 满 9 户满团」的成团标准数据亦由该单交付
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- 后端:jw
|
||||
- 前端待接:弹窗步进器改为提交净增量、接收新的响应结构刷新三格;
|
||||
团期进入物料准备后按钮置灰;未达标提示与「不得低于已报名数」的前端拦截保持不变
|
||||
@@ -0,0 +1,249 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "frontend"
|
||||
title: "团期产品新建订单向导创单漏传 productBatchId"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "前端缺陷"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "后端已随 #7135/#7159 加固并部署测试服 dev-v3、网关复测 15/15 通过(新增 581055/581056/581057、修复 581034 人数预查,合并提交 977be08e);前端可据此联调:在 order-v2/new 向导创单 payload 补 productBatchId(仅 GROUP 产品,且必须是该产品的班期),并修团期看板「新增子订单」入口带团期上下文。"
|
||||
updated_at: "2026-09-06"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期订单:新建订单向导对 GROUP 产品创单漏传 productBatchId(前端缺陷)
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**现象**:团期(GROUP)产品在管理后台新建订单向导完成表单、点"确认创建"后报 HTTP 200 `code=581026 message='团期产品必须选择团期'`,无法创建。
|
||||
|
||||
**根因**:前端 `src/views/order-v2/new/index.vue` `handleCreate()` 组装的创单 payload 漏传 `productBatchId` 字段。而向导其实已在报价阶段成功拿到班期 ID(存于 `pricingContext.batchId`),报价后端响应 ¥5,850 正确,但创建时没把它放入请求体。
|
||||
|
||||
**结论**:前端补 `productBatchId` 是主修复。**后端已随 #7135/#7159 同步加固**(不再是「零改动」):现在仅 GROUP 产品可传 `productBatchId`,CORE/CUSTOM 传了会被拒(581056);班期必须属于所选产品(否则 581055);`tierSeq` 必须在产品配置内(否则 581057);人数超班期剩余名额会被拒(581034,此前因字段名漂移长期失效,#7159 修复)。因此前端务必**只对 GROUP 产品**传 `productBatchId`,且取自该产品价格日历的 `items[].batchId`。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
### 复现步骤
|
||||
|
||||
页面:管理后台 `192.168.100.219:9527/order-v2/new`(hl-ui 路由 `orderV2NewRoute`,`src/router/routes.js:166-175`,四步向导:选主题 → 选产品/档位 → 基本信息 → 确认创建)。
|
||||
|
||||
1. **Step 0 选主题**:「阿斯蒂芬撒点」(GROUP 产品线 `lineId=2044248925572919297`)
|
||||
2. **Step 1 选产品**:「冻干粉发短信给」(`productId=2044306857534636034`,GROUP,`tierSeq=1` 档位「轻奢」)
|
||||
3. **Step 2 基本信息**:出发日期 2026-10-01,成人 1 名、儿童 1 名 → 系统报价 ¥5,850.00(其中单房差 +¥500.00)
|
||||
4. **Step 3 确认创建**:填客户姓名、手机号 → 点「确认创建订单」
|
||||
5. **结果**:Toast 弹窗「团期产品必须选择团期」,订单创建失败
|
||||
|
||||
### 调用链
|
||||
|
||||
1. hl-ui `src/views/order-v2/new/index.vue:405-450` `handleCreate()` 组装 payload → `createOrder(payload)`
|
||||
2. `src/api/orderV2.js:79-81` `createOrder(data)` = `http.post('/v3/admin/order', data, V3)`
|
||||
3. hl-gateway `application.yml:246-249` 路由 `/v3/admin/**` → `lb://hl-order-service-v3`
|
||||
4. hl-order-service-v3 `OrderController.java:86-91` 接收 → `orderService.createOrder(req, ...)`
|
||||
5. `OrderService.java:630` 直接 `ctx.setProductBatchId(req.getProductBatchId())`,无兜底反查
|
||||
6. `OrderService.java:635` `strategy.validate(ctx, productDetail)`
|
||||
7. `GroupOrderStrategy.java:46-49` 校验失败:`if (ctx.getProductBatchId() == null) throw new BusinessException(OrderCoreErrorCode.GROUP_BATCH_REQUIRED)`
|
||||
8. `OrderCoreErrorCode.java:102-103` 返回 `581026`「团期产品必须选择团期」
|
||||
|
||||
### 地面真相(测试库验证)
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 主题(product_line) | 阿斯蒂芬撒点 `2044248925572919297` |
|
||||
| 产品(product) | 冻干粉发短信给 `2044306857534636034`,`product_type=GROUP` |
|
||||
| 档位(tier) | `tierSeq=1` 轻奢 |
|
||||
| 班期(group_tour_batch) | `batch_id=2052935476557328386`,`batch_no=Q202610012052935476548939777`,`departure_date=2026-10-01`,`batch_status=ENROLLING`,`end_date=2026-10-03`,`adult_price=2925`,`child_price=2425`,`single_room_diff=500` |
|
||||
| 预计金额 | 2925 + 2425 = 5,350,加单房差 500 = **5,850** ✓ (与前端报价一致,证明向导已成功拿到班期信息计价) |
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口名 | 方法 | 网关路径 | 前端函数 | 变更 | 说明 |
|
||||
|---|--------|------|---------|---------|------|------|
|
||||
| 1 | 创建订单 | POST | `/v3/admin/order` | `createOrder()` | **调用方式修正** | GROUP 产品**必传** `productBatchId`;CORE/CUSTOM 必须**不传** |
|
||||
| 2 | 统一价格日历 | GET | `/admin/product/item/{productId}/pricing-calendar` | `getUnifiedPricingCalendar()` | 无变更 | GROUP 时返回班期列表,`items[].batchId` 即为所需班期 ID |
|
||||
| 3 | 报价 | POST | `/admin/product/item/{productId}/quote` | `quoteProduct()` | 无变更 | GROUP 时现已传 `batchId`,保持即可 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 创建订单 `POST /v3/admin/order`
|
||||
|
||||
**VO**: `OrderCreateReqVO → Result<OrderCreateRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
新建订单向导 Step 3 确认创建时调用,后端落库并触发团期聚合等副作用。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|
|
||||
| productId | String(Long 值) | ✓ | 正整数 | 产品 ID |
|
||||
| tierSeq | Integer | ✓ | 1~N,且必须在产品已配档位内 | 档位序号;不在产品 tierPrices ∪ tiers 配置内 → 581057(#7135 起全类型校验;产品未配任何档位时不拦) |
|
||||
| departureDate | String (yyyy-MM-dd) | ✓ | 不早于今天 | 出发日期;GROUP 下服务端会用班期权威出发日落库,但仍必填;CORE/CUSTOM 按字面值 |
|
||||
| adultCount | Integer | ✓ | ≥1 | 成人数 |
|
||||
| childCount | Integer | — | ≥0,默认 0 | 儿童数(6~12 岁) |
|
||||
| youngChildCount | Integer | — | ≥0,默认 0 | 小童数(2~5 岁) |
|
||||
| babyCount | Integer | — | ≥0,默认 0 | 婴儿数;GROUP 下服务端强制置为 0,不计入名额和价格 |
|
||||
| customerName | String | ✓ | 非空,≤50 | 客户姓名 |
|
||||
| customerPhone | String | ✓ | 格式 `^1[3-9]\d{9}$` | 手机号明文 |
|
||||
| customerRemark | String | — | ≤500 | 订单备注 |
|
||||
| createSource | String | — | ≤20,默认 CONSULTANT | 创建来源标记 |
|
||||
| **productBatchId** | **String(Long 值)** | **GROUP ✓ / CORE、CUSTOM ✗** | 仅 GROUP 可传且必传;班期须属于本 productId | **GROUP 产品必传班期 ID**(来自价格日历 `items[].batchId`),**非 GROUP 产品禁止传递**(CORE/CUSTOM 带了 → 581056);班期 productId 须等于请求 productId(否则 581055);建议用字符串如 `"2052935476557328386"`(JSON Number 亦可,后端接受,但字符串防前端精度丢失) |
|
||||
| roomCount | Integer | — | ≥1,默认 1 | 房间数;超过班期剩余房间数报 `581031` |
|
||||
| tags | Array<String> | — | — | 订单标签 |
|
||||
|
||||
#### 出参 `Result<OrderCreateRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| code | Integer | 200 = 成功,否则为业务错误码 |
|
||||
| data.id | String | 订单 ID(雪花 Long)|
|
||||
| data.orderNo | String | 订单编号(HL+时间+序号,如 HL20260906093733163) |
|
||||
| data.orderStatus | String | 订单状态(创建初态 = PENDING_PAY) |
|
||||
| data.totalAmount | String | 订单总金额,格式 decimal(12,2) |
|
||||
| data.depositAmount | String | 订金额 |
|
||||
| data.departureDate | String | 出发日期(yyyy-MM-dd);GROUP 以班期为准,回显班期出发日 |
|
||||
| data.returnDate | String | 归程日期(yyyy-MM-dd);根据 tripDays = endDate - departureDate + 1 推算 |
|
||||
| data.groupBatchName | String/null | 团批次名称(当前实现为 null) |
|
||||
| data.tierName | String/null | 档位名称(#7135 起 GROUP 也回显来自 tiers 配置的档位名;产品未配档位时为 null) |
|
||||
| data.consultantId | String | 发单定制师 ID(创建时落 1001) |
|
||||
|
||||
#### 请求示例(GROUP 产品,正例)
|
||||
|
||||
```json
|
||||
{
|
||||
"productId": "2044306857534636034",
|
||||
"tierSeq": 1,
|
||||
"departureDate": "2026-10-01",
|
||||
"adultCount": 1,
|
||||
"childCount": 1,
|
||||
"youngChildCount": 0,
|
||||
"babyCount": 0,
|
||||
"customerName": "张三",
|
||||
"customerPhone": "13800009601",
|
||||
"createSource": "CONSULTANT",
|
||||
"customerRemark": "团期测试订单",
|
||||
"productBatchId": "2052935476557328386",
|
||||
"roomCount": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"id": "2096412454488612866",
|
||||
"orderNo": "HL20260906093733163",
|
||||
"orderStatus": "PENDING_PAY",
|
||||
"totalAmount": "5850.00",
|
||||
"depositAmount": "1000.00",
|
||||
"departureDate": "2026-10-01",
|
||||
"returnDate": "2026-10-03",
|
||||
"groupBatchName": null,
|
||||
"tierName": null,
|
||||
"consultantId": "1001"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
| code | message | 触发条件 |
|
||||
|------|---------|---------|
|
||||
| 581026 | 团期产品必须选择团期 | productBatchId 为 null 且 productType = GROUP |
|
||||
| 581027 | 团期信息获取失败 | Feign 调班期服务异常 |
|
||||
| 581028 | 团期状态不允许报名 | 班期状态不在可订范围 |
|
||||
| 581029 | 团期已过报名截止日 | 当前日期 > enrollment_deadline |
|
||||
| 581031 | 团期剩余房间不足 | roomCount > 班期剩余房间数 |
|
||||
| 581034 | 团期剩余名额不足 | 成人+儿童+小童 > 班期剩余名额(#7159 修复:此前字段名漂移致此校验长期失效;人数不限的班期不拦) |
|
||||
| 581055 | 所选团期不属于该产品 | 班期 productId ≠ 请求 productId(跨产品串号;#7135 新增) |
|
||||
| 581056 | 非团期产品不能指定团期 | CORE/CUSTOM 请求带了 productBatchId(#7135 新增) |
|
||||
| 581057 | 所选档位不存在 | tierSeq 不在产品 tierPrices ∪ tiers 配置内(#7135 新增;产品未配档位时不拦) |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 后端已校验 tierSeq 存在性(581057)与班期归属产品(581055);GROUP 出发日/返团日以班期为准回显(#7135)
|
||||
- babyCount 强制 0,不参与计价
|
||||
- returnDate 按班期 tripDays 计算
|
||||
- 创建后触发 staff 分配、group_batch 懒建
|
||||
|
||||
---
|
||||
|
||||
### 2. 统一价格日历 `GET /admin/product/item/{productId}/pricing-calendar`
|
||||
|
||||
GROUP 时返回班期列表,items[] 每条含:
|
||||
- date、batchId(JSON Number,**前端必须 String() 转换**)
|
||||
- batchNo、batchName、batchStatus、endDate、enrollmentDeadline
|
||||
- maxRooms、bookedRooms、remainParticipants(null 表不限)
|
||||
- 价格字段、sellable(仅 ENROLLING 为 true)
|
||||
|
||||
---
|
||||
|
||||
### 3. 报价 `POST /admin/product/item/{productId}/quote`
|
||||
|
||||
GROUP 时入参需 batchId,出参 grandTotal、singleRoomSurcharge 等。
|
||||
|
||||
---
|
||||
|
||||
## 四、前端修复要点与自测清单
|
||||
|
||||
### 修复要点
|
||||
|
||||
1. `index.vue handleCreate()`:GROUP 时 `payload.productBatchId = String(pricingContext.batchId)`
|
||||
2. Step3 确认前二次校验 pricingContext 非空且与当前表单一致
|
||||
3. Step3 展示 batchNo/batchName
|
||||
4. 团期看板 onAddSub() 带深链 ?productBatchId&departureDate
|
||||
|
||||
### 自测清单
|
||||
|
||||
- ✓ GROUP 选日期 → 报价 → 创建成功
|
||||
- ✓ CORE 创建不含 productBatchId
|
||||
- ✓ 日期未选时按钮拦截
|
||||
|
||||
---
|
||||
|
||||
## 五、验证证据
|
||||
|
||||
### 场景矩阵(测试服 2026-09-06)
|
||||
|
||||
14 场景通过,1 场景失败(S8),10 单已清理。
|
||||
|
||||
### DB 落库
|
||||
|
||||
orderNo HL20260906093733163,product_batch_id=2052935476557328386
|
||||
|
||||
---
|
||||
|
||||
## 六、影响与不影响范围
|
||||
|
||||
**不影响**:
|
||||
- 正常 CORE/CUSTOM 创单(不带 productBatchId):完全不变
|
||||
- 订单读接口(列表 / 详情 / 编辑):不变
|
||||
- 前端改动只涉及 `order-v2/new` 向导创单 payload 与团期看板「新增子订单」入口
|
||||
|
||||
**受后端加固影响(前端需知晓)**:
|
||||
- 小程序端 `POST /v3/mp/order` 与管理端共用内核,581055/581056/581057/581034 同样生效,但小程序请求契约不变(`groupBatchId` 语义不变)
|
||||
- CORE/CUSTOM 若误带 productBatchId:此前静默落库,现在直接 581056 拒单
|
||||
|
||||
---
|
||||
|
||||
## 七、关联
|
||||
|
||||
- 前端向导既有工单 #7083(已合);后端判团字段透出 #7142、看板 productId 深链 #7143(均已合 dev-v3)
|
||||
- 后端加固 #7135(收紧 productBatchId 校验:581055/581056/581057、日期按班期)+ #7159(人数预查改读 remainingParticipants 使 581034 生效),随 PR #7169 合入 dev-v3
|
||||
- 证据:gateway-verify.txt、gateway-matrix.txt
|
||||
@@ -30,6 +30,7 @@ const REQUIRED_KEYS = [
|
||||
'ticket',
|
||||
'title',
|
||||
'consumer',
|
||||
'author',
|
||||
'change_type',
|
||||
'backend_status',
|
||||
'gateway_status',
|
||||
@@ -38,9 +39,31 @@ const REQUIRED_KEYS = [
|
||||
'frontend_ref',
|
||||
'target_release',
|
||||
'verified_at',
|
||||
'status_note',
|
||||
'updated_at',
|
||||
'base',
|
||||
];
|
||||
const HTTP_METHOD_PATTERN = '(?:GET|POST|PUT|PATCH|DELETE)';
|
||||
const API_TEMPLATE_SECTION_PREFIXES = [
|
||||
'二、变更接口清单',
|
||||
'三、接口详情',
|
||||
'四、契约约束与正确调用方式',
|
||||
'六、边界行为',
|
||||
'七、不影响范围',
|
||||
'八、测试环境已验证',
|
||||
'十、相关文档',
|
||||
'关联 / 联系人',
|
||||
];
|
||||
const API_DETAIL_SUBSECTIONS = [
|
||||
'使用场景',
|
||||
'入参',
|
||||
'出参',
|
||||
'请求示例',
|
||||
'响应示例',
|
||||
'空数据 / 降级响应',
|
||||
'错误响应',
|
||||
'业务边界',
|
||||
];
|
||||
|
||||
function ruleError(code, file, message) {
|
||||
return { code, path: file, message };
|
||||
@@ -68,6 +91,181 @@ function isIsoDateOrTime(value) {
|
||||
&& Number.isFinite(Date.parse(value));
|
||||
}
|
||||
|
||||
function sectionByPrefix(body, prefix, level = 2) {
|
||||
const marker = `${'#'.repeat(level)} ${prefix}`;
|
||||
const lines = String(body ?? '').replaceAll('\r\n', '\n').split('\n');
|
||||
const start = lines.findIndex((line) => line.trim().startsWith(marker));
|
||||
if (start < 0) {
|
||||
return undefined;
|
||||
}
|
||||
const nextMarker = '#'.repeat(level);
|
||||
let end = lines.length;
|
||||
for (let index = start + 1; index < lines.length; index += 1) {
|
||||
const value = lines[index].trim();
|
||||
if (value.startsWith(`${nextMarker} `) && !value.startsWith(`${nextMarker}#`)) {
|
||||
end = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return lines.slice(start + 1, end).join('\n');
|
||||
}
|
||||
|
||||
function apiEndpointKey(method, endpointPath) {
|
||||
return `${method.trim().toUpperCase()} ${endpointPath.trim()}`;
|
||||
}
|
||||
|
||||
function validateApiTemplate(file, metadata, body) {
|
||||
const errors = [];
|
||||
const missingSections = API_TEMPLATE_SECTION_PREFIXES.filter(
|
||||
(prefix) => sectionByPrefix(body, prefix) === undefined,
|
||||
);
|
||||
if (missingSections.length > 0) {
|
||||
errors.push(ruleError(
|
||||
'E_API_TEMPLATE',
|
||||
file,
|
||||
`接口类正文必须仿照 CHANGELOG_TEMPLATE.md,缺少章节: ${missingSections.join('、')}`,
|
||||
));
|
||||
}
|
||||
|
||||
const listSection = sectionByPrefix(body, '二、变更接口清单');
|
||||
const detailSection = sectionByPrefix(body, '三、接口详情');
|
||||
if (listSection === undefined || detailSection === undefined) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
const requiredHeader = /^\|\s*#\s*\|\s*接口\s*\|\s*方法\s*\|\s*路径\s*\|\s*变更类型\s*\|\s*说明\s*\|\s*$/m;
|
||||
if (!requiredHeader.test(listSection)) {
|
||||
errors.push(ruleError(
|
||||
'E_API_TEMPLATE',
|
||||
file,
|
||||
'“二、变更接口清单”必须使用模板列: #、接口、方法、路径、变更类型、说明',
|
||||
));
|
||||
}
|
||||
|
||||
const listPattern = new RegExp(
|
||||
'^\\|\\s*\\d+\\s*\\|\\s*[^|]+\\|\\s*('
|
||||
+ HTTP_METHOD_PATTERN
|
||||
+ ')\\s*\\|\\s*`([^`]+)`\\s*\\|\\s*[^|]+\\|\\s*[^|]+\\|\\s*$',
|
||||
'gm',
|
||||
);
|
||||
const listed = [...listSection.matchAll(listPattern)]
|
||||
.map((match) => apiEndpointKey(match[1], match[2]));
|
||||
if (listed.length === 0) {
|
||||
errors.push(ruleError(
|
||||
'E_API_TEMPLATE',
|
||||
file,
|
||||
'“二、变更接口清单”至少需要一条带 METHOD 和反引号路径的接口记录',
|
||||
));
|
||||
}
|
||||
|
||||
const detailPattern = new RegExp(
|
||||
'^###\\s+\\d+\\.\\s+.+?\\s+`('
|
||||
+ HTTP_METHOD_PATTERN
|
||||
+ ')\\s+([^`]+)`\\s*$',
|
||||
'gm',
|
||||
);
|
||||
const detailMatches = [...detailSection.matchAll(detailPattern)];
|
||||
const detailed = detailMatches.map((match) => apiEndpointKey(match[1], match[2]));
|
||||
if (detailMatches.length === 0) {
|
||||
errors.push(ruleError(
|
||||
'E_API_TEMPLATE',
|
||||
file,
|
||||
'“三、接口详情”至少需要一个“### N. 接口名 `METHOD /path`”子节',
|
||||
));
|
||||
}
|
||||
|
||||
const listedSet = [...new Set(listed)].sort();
|
||||
const detailedSet = [...new Set(detailed)].sort();
|
||||
if (
|
||||
listed.length !== listedSet.length
|
||||
|| detailed.length !== detailedSet.length
|
||||
|| listedSet.join('\n') !== detailedSet.join('\n')
|
||||
) {
|
||||
errors.push(ruleError(
|
||||
'E_API_ENDPOINTS',
|
||||
file,
|
||||
'接口清单与逐接口详情的 METHOD/path 必须去重且一一对应',
|
||||
));
|
||||
}
|
||||
|
||||
for (let index = 0; index < detailMatches.length; index += 1) {
|
||||
const match = detailMatches[index];
|
||||
const next = detailMatches[index + 1];
|
||||
const block = detailSection.slice(
|
||||
match.index + match[0].length,
|
||||
next ? next.index : detailSection.length,
|
||||
);
|
||||
const missing = API_DETAIL_SUBSECTIONS.filter(
|
||||
(prefix) => sectionByPrefix(block, prefix, 4) === undefined,
|
||||
);
|
||||
const usage = sectionByPrefix(block, '使用场景', 4) ?? '';
|
||||
const input = sectionByPrefix(block, '入参', 4) ?? '';
|
||||
const output = sectionByPrefix(block, '出参', 4) ?? '';
|
||||
const requestExample = sectionByPrefix(block, '请求示例', 4) ?? '';
|
||||
const responseExample = sectionByPrefix(block, '响应示例', 4) ?? '';
|
||||
const emptyResponse = sectionByPrefix(block, '空数据 / 降级响应', 4) ?? '';
|
||||
const errorExample = sectionByPrefix(block, '错误响应', 4) ?? '';
|
||||
const boundary = sectionByPrefix(block, '业务边界', 4) ?? '';
|
||||
if (!/^\*\*VO\*\*:\s*`[^`]+`/m.test(block)) {
|
||||
missing.push('VO 契约');
|
||||
}
|
||||
if (!usage.trim()) {
|
||||
missing.push('使用场景说明');
|
||||
}
|
||||
if (!/^\|\s*字段\s*\|\s*位置\s*\|\s*类型\s*\|\s*必填\s*\|\s*约束\s*\|\s*说明\s*\|\s*$/m.test(input)) {
|
||||
missing.push('入参字段表');
|
||||
}
|
||||
if (!/^\|\s*字段\s*\|\s*类型\s*\|\s*说明\s*\|\s*$/m.test(output)) {
|
||||
missing.push('出参字段表');
|
||||
}
|
||||
if (!/```(?:json|http)\s*\n[\s\S]+?\n```/.test(requestExample)) {
|
||||
missing.push('请求示例代码块');
|
||||
}
|
||||
if (!/```json\s*\n[\s\S]+?\n```/.test(responseExample)) {
|
||||
missing.push('响应示例 JSON');
|
||||
}
|
||||
if (!emptyResponse.trim()) {
|
||||
missing.push('空数据 / 降级说明');
|
||||
}
|
||||
if (!/```json\s*\n[\s\S]+?\n```/.test(errorExample)) {
|
||||
missing.push('错误响应 JSON');
|
||||
}
|
||||
if (!/^\s*[-*]\s+\S/m.test(boundary)) {
|
||||
missing.push('业务边界条目');
|
||||
}
|
||||
if (missing.length > 0) {
|
||||
errors.push(ruleError(
|
||||
'E_API_DETAIL',
|
||||
file,
|
||||
`${detailed[index]} 缺少逐接口自包含内容: ${[...new Set(missing)].join('、')}`,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
const writeMethods = listed.some((entry) => /^(?:POST|PUT|PATCH|DELETE) /.test(entry));
|
||||
if (writeMethods && sectionByPrefix(body, '五、数据库行为') === undefined) {
|
||||
errors.push(ruleError(
|
||||
'E_API_TEMPLATE',
|
||||
file,
|
||||
'包含写接口时必须按模板提供“五、数据库行为”章节(只写外部可观察行为,不泄露表结构)',
|
||||
));
|
||||
}
|
||||
if (
|
||||
['修改接口', '删除接口'].includes(metadata.change_type)
|
||||
&& (
|
||||
sectionByPrefix(body, '六.6、修改前后对比') === undefined
|
||||
|| sectionByPrefix(body, '六.7、影响评估') === undefined
|
||||
)
|
||||
) {
|
||||
errors.push(ruleError(
|
||||
'E_API_TEMPLATE',
|
||||
file,
|
||||
'修改/删除接口必须提供“六.6、修改前后对比”和“六.7、影响评估”章节',
|
||||
));
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function parseFrontmatter(text) {
|
||||
const value = String(text ?? '').replaceAll('\r\n', '\n');
|
||||
if (!value.startsWith('---\n')) {
|
||||
@@ -165,11 +363,14 @@ export function validateV2Document(file, text, { requireV2 = false } = {}) {
|
||||
errors.push(ruleError('E_REQUIRED', file, `frontmatter 缺少 ${key}`));
|
||||
}
|
||||
}
|
||||
for (const key of ['ticket', 'title', 'consumer', 'change_type', 'backend_status', 'gateway_status', 'frontend_status', 'updated_at', 'base']) {
|
||||
for (const key of ['ticket', 'title', 'consumer', 'author', 'change_type', 'backend_status', 'gateway_status', 'frontend_status', 'updated_at', 'base']) {
|
||||
if (!metadata[key]?.trim()) {
|
||||
errors.push(ruleError('E_REQUIRED', file, `${key} 不能为空`));
|
||||
}
|
||||
}
|
||||
if (metadata.author && !/^\S+\(GIT\)$/.test(metadata.author)) {
|
||||
errors.push(ruleError('E_AUTHOR', file, 'author 必须使用“登录名(GIT)”格式'));
|
||||
}
|
||||
if (!CHANGE_TYPES.has(metadata.change_type)) {
|
||||
errors.push(ruleError('E_CHANGE_TYPE', file, `change_type 非法: ${metadata.change_type || '(空)'}`));
|
||||
}
|
||||
@@ -213,13 +414,9 @@ export function validateV2Document(file, text, { requireV2 = false } = {}) {
|
||||
if (/\{\{[^{}\n]+\}\}|\bTODO\b|待补充/i.test(body)) {
|
||||
errors.push(ruleError('E_PLACEHOLDER', file, '正文仍有 TODO、待补充或模板占位符'));
|
||||
}
|
||||
// 接口类条目必须有接口清单章节与验证章节;修复/前端类条目结构自由,不强制
|
||||
// 接口类条目必须逐项遵循根模板,保证前端不依赖 Swagger 或口头补充也能联调。
|
||||
if (API_CHANGE_TYPES.has(metadata.change_type)) {
|
||||
const hasApiSection = /^##[^\n]*(变更接口|变更清单|变更内容|接口详情|变更点|接口变化|行为变化)/m.test(body);
|
||||
const hasEvidenceSection = /^##[^\n]*(验证|测试|复现|证据)/m.test(body);
|
||||
if (!hasApiSection || !hasEvidenceSection) {
|
||||
errors.push(ruleError('E_SECTIONS', file, '接口类正文缺少接口清单(变更接口/变更清单)或验证(验证证据/测试)章节'));
|
||||
}
|
||||
errors.push(...validateApiTemplate(file, metadata, body));
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
@@ -250,8 +447,51 @@ export function runFrontmatterValidation(records, root = process.cwd()) {
|
||||
return { checkedCount: documents.length, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交前自检模式:`--files a.md b.md`(或逗号分隔)按「新增文件」口径校验工作区里的 changelog,
|
||||
* 让作者/Agent 不用先 commit 就能跑到与 pre-push 门禁完全相同的规则(hl-workflow changelog_workflow.py lint 也走这里)。
|
||||
*/
|
||||
export function runFilesValidation(files, root = process.cwd()) {
|
||||
const errors = [];
|
||||
let checkedCount = 0;
|
||||
for (const raw of files) {
|
||||
const absolute = path.resolve(root, raw);
|
||||
const relative = path.relative(root, absolute).split(path.sep).join('/');
|
||||
if (!controlledRootForPath(relative) || !relative.endsWith('.md')) {
|
||||
errors.push(ruleError('E_PATH', relative, '不在受控 changelog 目录内(changelogs/ 或 changelogs-v2/)'));
|
||||
continue;
|
||||
}
|
||||
let text;
|
||||
try {
|
||||
text = readFileSync(absolute, 'utf8');
|
||||
} catch (error) {
|
||||
errors.push(ruleError('E_READ', relative, `无法读取文件: ${error.message}`));
|
||||
continue;
|
||||
}
|
||||
checkedCount += 1;
|
||||
errors.push(...validateV2Document(relative, text, { requireV2: true }));
|
||||
}
|
||||
return { checkedCount, errors };
|
||||
}
|
||||
|
||||
export function main(argv = process.argv.slice(2)) {
|
||||
try {
|
||||
if (argv[0] === '--files') {
|
||||
const files = argv.slice(1).flatMap((item) => item.split(',')).filter(Boolean);
|
||||
if (files.length === 0) {
|
||||
throw new Error('--files 需要至少一个文件路径');
|
||||
}
|
||||
const result = runFilesValidation(files);
|
||||
if (result.errors.length > 0) {
|
||||
for (const error of result.errors) {
|
||||
console.error(`[${error.code}] ${error.path}: ${error.message}`);
|
||||
}
|
||||
console.error(`FAIL: ${result.errors.length} frontmatter error(s) in ${result.checkedCount} changelog file(s).`);
|
||||
return 1;
|
||||
}
|
||||
console.log(`PASS: validated frontmatter for ${result.checkedCount} changelog file(s) (--files).`);
|
||||
return 0;
|
||||
}
|
||||
const options = parseArguments(argv);
|
||||
const records = parseNameStatusZ(diffFromOptions(options));
|
||||
const result = runFrontmatterValidation(records);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
@@ -21,6 +21,7 @@ function metadata(overrides = {}) {
|
||||
ticket: '5218',
|
||||
title: '工作流治理',
|
||||
consumer: 'admin',
|
||||
author: 'lc(GIT)',
|
||||
change_type: '修改接口',
|
||||
backend_status: 'deployed',
|
||||
gateway_status: 'verified',
|
||||
@@ -41,7 +42,15 @@ function document(overrides = {}) {
|
||||
const frontmatter = Object.entries(fields)
|
||||
.map(([key, value]) => `${key}: "${value}"`)
|
||||
.join('\n');
|
||||
return `---\n${frontmatter}\n---\n\n# 工作流治理\n\n## 变更接口\n\n- 无业务接口变化。\n\n## 验证证据\n\n- 自动化测试通过。\n`;
|
||||
return `---\n${frontmatter}\n---\n\n# 工作流治理\n\n## 二、变更接口清单\n\n| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |\n|---|---|---|---|---|---|\n| 1 | 保存配置 | POST | \`/admin/workflow/config\` | 修改请求 | 保存工作流配置 |\n\n## 三、接口详情\n\n### 1. 保存配置 \`POST /admin/workflow/config\`\n\n**VO**: \`WorkflowConfigReqVO / String\`\n\n#### 使用场景\n\n- 管理员保存工作流配置。\n\n#### 入参\n\n| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |\n|---|---|---|---|---|---|\n| name | Body | String | ✅ | 非空 | 配置名称 |\n\n#### 出参 \`Result<String>\`\n\n| 字段 | 类型 | 说明 |\n|---|---|---|\n| data | String | 配置 ID |\n\n#### 请求示例\n\n\`\`\`json\n{ "name": "审批流" }\n\`\`\`\n\n#### 响应示例\n\n\`\`\`json\n{ "code": 200, "message": "成功", "data": "1", "success": true }\n\`\`\`\n\n#### 空数据 / 降级响应\n\n成功时一定返回字符串 ID。\n\n#### 错误响应\n\n\`\`\`json\n{ "code": 400, "message": "name 不能为空", "data": null, "success": false }\n\`\`\`\n\n#### 业务边界\n\n- 业务失败也必须检查响应体 code。\n\n## 四、契约约束与正确调用方式\n\n- 请求体必须使用 JSON。\n\n## 五、数据库行为\n\n- 成功请求保存一份配置,失败请求不产生业务写入。\n\n## 六、边界行为\n\n- 未登录返回 401。\n\n## 六.6、修改前后对比\n\n| 字段 | 改前 | 改后 |\n|---|---|---|\n| name | 可为空 | 必填 |\n\n## 六.7、影响评估\n\n- **是否破坏向后兼容**: 否\n- **前端是否必须同步上线**: 是\n- **前端 workaround 清理点**: 无\n\n## 七、不影响范围\n\n- 查询接口不变。\n\n## 八、测试环境已验证\n\n- POST /admin/workflow/config → 200 ✓\n\n## 十、相关文档\n\n- Issue: #5218\n\n## 关联 / 联系人\n\n- **后端负责人**: @lc\n`;
|
||||
}
|
||||
|
||||
function incompleteApiDocument() {
|
||||
const fields = metadata();
|
||||
const frontmatter = Object.entries(fields)
|
||||
.map(([key, value]) => `${key}: "${value}"`)
|
||||
.join('\n');
|
||||
return `---\n${frontmatter}\n---\n\n# 工作流治理\n\n## 变更接口\n\n- POST /admin/workflow/config。\n\n## 验证证据\n\n- 自动化测试通过。\n`;
|
||||
}
|
||||
|
||||
test('parses quoted flat YAML frontmatter', () => {
|
||||
@@ -50,10 +59,89 @@ test('parses quoted flat YAML frontmatter', () => {
|
||||
assert.equal(parsed.metadata.frontend_status, 'pending');
|
||||
});
|
||||
|
||||
test('CHANGELOG_TEMPLATE.md contains every section enforced for API handoffs', () => {
|
||||
const template = readFileSync(
|
||||
new URL('../CHANGELOG_TEMPLATE.md', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
for (const value of [
|
||||
'author: "{推送者登录名}(GIT)"',
|
||||
'## 二、变更接口清单',
|
||||
'## 三、接口详情',
|
||||
'#### 使用场景',
|
||||
'#### 入参',
|
||||
'#### 出参',
|
||||
'#### 请求示例',
|
||||
'#### 响应示例',
|
||||
'#### 空数据 / 降级响应',
|
||||
'#### 错误响应',
|
||||
'#### 业务边界',
|
||||
'## 四、契约约束与正确调用方式',
|
||||
'## 五、数据库行为',
|
||||
'## 六、边界行为',
|
||||
'## 六.6、修改前后对比',
|
||||
'## 六.7、影响评估',
|
||||
'## 七、不影响范围',
|
||||
'## 八、测试环境已验证',
|
||||
'## 十、相关文档',
|
||||
'## 关联 / 联系人',
|
||||
]) {
|
||||
assert.ok(template.includes(value), `template missing ${value}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('accepts a complete v2 handoff with pending frontend consumption', () => {
|
||||
assert.deepEqual(validateV2Document(FILE, document(), { requireV2: true }), []);
|
||||
});
|
||||
|
||||
test('rejects an API handoff that does not follow CHANGELOG_TEMPLATE.md', () => {
|
||||
const errors = validateV2Document(FILE, incompleteApiDocument(), { requireV2: true });
|
||||
assert.ok(errors.some(({ code }) => code === 'E_API_TEMPLATE'));
|
||||
});
|
||||
|
||||
test('rejects an endpoint detail without a required self-contained contract block', () => {
|
||||
const value = document().replace('#### 错误响应', '#### 异常说明');
|
||||
const errors = validateV2Document(FILE, value, { requireV2: true });
|
||||
assert.ok(errors.some(({ code }) => code === 'E_API_DETAIL'));
|
||||
});
|
||||
|
||||
test('requires a use case and explicit empty/degraded behavior for every endpoint', () => {
|
||||
const value = document()
|
||||
.replace('#### 使用场景', '#### 调用时机')
|
||||
.replace('#### 空数据 / 降级响应', '#### 无数据');
|
||||
const errors = validateV2Document(FILE, value, { requireV2: true });
|
||||
assert.ok(errors.some(({ code, message }) => (
|
||||
code === 'E_API_DETAIL'
|
||||
&& message.includes('使用场景')
|
||||
&& message.includes('空数据 / 降级响应')
|
||||
)));
|
||||
});
|
||||
|
||||
test('rejects a mismatch between the API list and endpoint detail headings', () => {
|
||||
const value = document().replace(
|
||||
'### 1. 保存配置 `POST /admin/workflow/config`',
|
||||
'### 1. 保存配置 `POST /admin/workflow/other`',
|
||||
);
|
||||
const errors = validateV2Document(FILE, value, { requireV2: true });
|
||||
assert.ok(errors.some(({ code }) => code === 'E_API_ENDPOINTS'));
|
||||
});
|
||||
|
||||
test('requires the template author format and write-operation database section', () => {
|
||||
const invalidAuthor = validateV2Document(
|
||||
FILE,
|
||||
document({ author: 'lc' }),
|
||||
{ requireV2: true },
|
||||
);
|
||||
assert.ok(invalidAuthor.some(({ code }) => code === 'E_AUTHOR'));
|
||||
|
||||
const withoutDatabaseBehavior = document().replace(
|
||||
'## 五、数据库行为',
|
||||
'## 五、持久化说明',
|
||||
);
|
||||
const errors = validateV2Document(FILE, withoutDatabaseBehavior, { requireV2: true });
|
||||
assert.ok(errors.some(({ code }) => code === 'E_API_TEMPLATE'));
|
||||
});
|
||||
|
||||
test('rejects backend and gateway pending at publication', () => {
|
||||
const errors = validateV2Document(
|
||||
FILE,
|
||||
|
||||
在新工单中引用
屏蔽一个用户