比较提交

...
作者 SHA1 备注 提交日期
API Changelog Bot 4ff1480683 补全 7070 房务 productType 标签接口文档(完整模板+网关实测)
changelog-filename-gate / validate (push) Successful in 2s
2026-09-04 15:50:54 +08:00
API Changelog Bot 8c38fc6b89 docs(changelog): 7070 房务列表/详情补产品类型标签 productType(管理后台) 2026-09-04 15:50:54 +08:00
jw a611948477 团期人员配置位成员改由字典决定,取值域三处对齐(#7079)
changelog-filename-gate / validate (push) Successful in 3s
PR #7085 合入 dev-v3(合并提交 d1b724d59),2026-09-04 部署测试环境
并经真实网关鉴权实测 7 条用例。

核心证据:15:30:33 往 group_batch_staff_slot_guide 插一行 GUIDE_ASSISTANT,
15:35:44(301 秒后)导游位候选由 7 人变 8 人——未改代码、未重启、未重新部署。
随后连打 8 次全部返回 8 人,两个实例都已回源。演示行验证后已撤除。

2 个接口的候选池来源与落库取值变更,路径/入参/出参结构全部不变:
- GET /v3/admin/group-batch/:groupBatchId/staff/candidates  候选池成员由字典决定,582113 文案变更
- PUT /v3/admin/group-batch/:groupBatchId/staff             staffRole 落真实 staffType

含字典初始化脚本 sql/dict_group_batch_staff_slot.sql(幂等,需手工执行)。
dict_type_id 取 8071/8072——8021/8022 在本机与测试环境都已被
contract_status / contract_platform 占用。

staff_role 取值域测试环境实测三处一致(字典 = 枚举 = @Pattern,8 项)。

frontend_status=not_required:路径、入参、出参结构均未变,前端无需改动;
但不要再硬编码候选池成员,也不要沿用自己提交的 staffRole 回显。
2026-09-04 15:37:23 +08:00
jw和Claude Opus 5 db6698061b docs(changelog): #7066 团期子订单列表补应收总额 totalAmount
changelog-filename-gate / validate (push) Successful in 2s
GET /v3/admin/order/group-batch/:groupBatchId/orders 出参新增 totalAmount
应收总额,口径复用既有 OrderAmountUtil.payableForDisplay(取消单归 0),
与 calcBalance 口径一致。纯 additive,不新建端点、不改老字段语义。

关键变化:前端不要再用 paidAmount + balanceAmount 反推应收总额。
balanceAmount 被钳在 ≥0,超付/退款/取消单三种情况反推值都偏大。

2026-09-04 于测试环境网关实测 8 组用例全部通过,两处在真实数据上
命中本单要修的缺陷:
- 超付单 totalAmount=2943.00,旧反推得 3270.00,偏差 327.00
- 取消单 totalAmount=0.00 而 paidAmount=3270.00,旧算法会把已取消
  子订单显示成有应收

backend_status=deployed / gateway_status=verified。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 15:01:20 +08:00
jw 2096a47340 团期导游位下游按配置位归组,GUIDE 与 LEADER 一视同仁(#7063)
changelog-filename-gate / validate (push) Successful in 2s
PR #7065 合入 dev-v3(合并提交 f37b6ee0f),2026-09-04 部署测试环境
并经真实网关鉴权实测 AC-2/AC-3/AC-4/AC-5/AC-9。

4 个接口的副作用与取值口径变更,路径/入参/出参结构全部不变:
- PUT  /v3/admin/group-batch/:groupBatchId/staff      导游位配 GUIDE 也置 guide_ready
- POST /v3/admin/order/:id/staff                      GUIDE 也写 guide_status 与 GUIDE_DONE
- DELETE /v3/admin/order/:id/staff/:staffAssignmentId 按导游位整组计数,不再误置空
- GET  /v3/admin/order/:id/print-itinerary            导游栏 GUIDE 优先、缺位回退 LEADER

顺带修一处更深的缺陷:ready 回填此前用产品侧 batchId 打 order_group_batch 主键,
rows=0 只 WARN 不抛,改前不论配 GUIDE 还是 LEADER,guide_ready 从未被置位过。

frontend_status=not_required:路径、入参、出参结构均未变,前端无需改动。
2026-09-04 14:25:48 +08:00
lc e5356a65e0 供应商企微在途状态统一审核中(#7069)
changelog-filename-gate / validate (push) Successful in 2s
2026-09-04 12:55:57 +08:00
wx 71cc52c1a3 changelog(6926): 导出补 opsStage 筛选 + 统计条自校验死代码修正(修改接口) Refs #6926
changelog-filename-gate / validate (push) Failing after 2s
2026-09-04 11:26:36 +08:00
lc 0df2c917bf docs: add supplier context handoff (#7059)
changelog-filename-gate / validate (push) Successful in 2s
2026-09-04 11:11:05 +08:00
API Changelog Bot cf78c344a1 changelog(6905): 003 totalPrice 应收字段 + birthdayInTrip 跨年修正已实现(#6929)
changelog-filename-gate / validate (push) Successful in 2s
2026-09-04 10:31:34 +08:00
lc a0daab2b90 更正供应商类型过滤前端状态(#7042)
changelog-filename-gate / validate (push) Successful in 2s
2026-09-04 09:51:13 +08:00
lc 5046bce500 docs: add supplier type filter handoff for #7042
changelog-filename-gate / validate (push) Successful in 2s
2026-09-03 21:48:26 +08:00
lc c80d1975aa docs(changelog): #7036 供应商状态变更企微审批
changelog-filename-gate / validate (push) Successful in 2s
2026-09-03 20:03:18 +08:00
lc f4354d8c31 docs(changelog): #7029 供应商账户企微审批
changelog-filename-gate / validate (push) Successful in 3s
2026-09-03 15:51:47 +08:00
jw和Claude Opus 5 828d9ae2e4 docs(changelog): #6986 团期物资从备品库选择 + suppliesName 二选一校验
changelog-filename-gate / validate (push) Successful in 2s
新增 GET /v3/admin/order/group-batch/:groupBatchId/supplies/candidates,
从资源域备品库拉候选并标记本团期已加入项;资源域新增内部接口
GET /internal/supplies/list-available,两个服务需同时部署。

记录本次行为变更:POST .../supplies 的 suppliesName 由 @NotBlank
改为与 suppliesResourceId 二选一,传 resourceId 时名称从库取并覆盖入参。

新增错误码 589518 备品库查询失败、589519 所选备品不存在或已下架。

2026-09-03 于测试环境网关实测 10 条正负向用例全部通过,
写入用例已删除还原;backend_status=deployed / gateway_status=verified。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 15:22:10 +08:00
jw和Claude Opus 5 d2c39b6c24 docs(changelog): #6950 团期人员配置候选列表新增接口 + staffRole 白名单校验
changelog-filename-gate / validate (push) Successful in 2s
新增 GET /v3/admin/group-batch/:groupBatchId/staff/candidates,导游位并收
GUIDE/LEADER 并按 staffId 去重,摄影位只收 PHOTOGRAPHER,其余位返 582113。

同时记录本次行为变更:PUT .../staff 的 staffList[].staffRole 新增
@Pattern 白名单校验,越界值由原先原样入库改为返回 400。

2026-09-03 于测试环境网关实测 10 条正负向用例全部通过,
backend_status=deployed / gateway_status=verified。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 14:57:42 +08:00
lc 411a549548 docs(changelog): #7013 启用供应商清账归档
changelog-filename-gate / validate (push) Successful in 3s
2026-09-03 10:42:52 +08:00
lc f68b6d80d4 docs(changelog): #6979 供应商状态流转契约
changelog-filename-gate / validate (push) Successful in 2s
2026-09-03 08:10:43 +08:00
lc c579da759e docs(changelog): #6938 供应商注册提交可选主表字段缺席保留(管理后台)
changelog-filename-gate / validate (push) Successful in 3s
2026-09-02 11:30:39 +08:00
API Changelog Bot c66a28b43f changelog(团期看板): 6904/6905 复审补充知会-排序口径/008 opsStage预告/chips空判/demandStatus与totalPrice待补(管理后台)
changelog-filename-gate / validate (push) Successful in 2s
2026-09-01 10:01:41 +00:00
API Changelog Bot 6c1d11d40f changelog(6921): 六芯片读端接入专用投影+车芯片车务文案修正(管理后台/修改接口)
changelog-filename-gate / validate (push) Successful in 2s
2026-09-01 16:13:30 +08:00
API Changelog Bot 9428076c2c Merge remote-tracking branch 'origin/main'
changelog-filename-gate / validate (push) Successful in 2s
2026-09-01 15:59:00 +08:00
API Changelog Bot 95394c35fc docs(changelog): 6905 团期看板 4 接口对齐补全-补齐模板章节(使用场景/请求响应错误示例/VO 契约/清单对应) 2026-09-01 15:51:27 +08:00
API Changelog Bot edfffdf063 docs(changelog): 6905 团期看板 4 接口对齐补全-逐接口规范化 请求/响应/空数据/错误/业务边界 示例顺序 2026-09-01 15:15:47 +08:00
API Changelog Bot 06a86d488d docs(changelog): 6905 团期看板 4 接口对齐补全-逐接口补请求/响应/错误示例并规范化模板标题 2026-09-01 15:11:03 +08:00
API Changelog Bot 082f32ad3f changelog(6905): 团期看板 4 接口对齐补全 GB-ADM-000/001/002/003(管理后台/修改接口) 2026-09-01 14:56:52 +08:00
Mimingguang cfc3aa8b1e docs(changelog): #6904 补前端评估(待看板列表契约)
changelog-filename-gate / validate (push) Successful in 2s
status_note 增前端评估:看板纯 mock,统计条/导出依赖真实看板页;看板列表
契约 #6905/GB-ADM-000~003 未入仓,等契约后统一接真。frontend_status 保持 pending。
2026-09-01 14:24:09 +08:00
Mimingguang 72949d869c docs(changelog): #6903 补前端评估(待看板接真)
changelog-filename-gate / validate (push) Successful in 2s
status_note 增前端评估:团期看板纯 mock、chips 需真实 groupBatchId,先接真
看板再接下钻,形态定弹层明细。frontend_status 保持 pending。
2026-09-01 14:18:43 +08:00
API Changelog Bot a031039140 chore(6904): changelog 文件名归档 01 前缀 (2026-09-01)
changelog-filename-gate / validate (push) Successful in 2s
2026-09-01 13:26:58 +08:00
API Changelog Bot c9efecebc5 chore(6904): 团期看板统计条与导出 GB-ADM-009/008 变更归档 (dev-v3) 2026-09-01 13:26:58 +08:00
API Changelog Bot 4c12c1db01 chore(changelog): 6903 团期看板六芯片逐户明细 GB-ADM-090~095 接口交接
changelog-filename-gate / validate (push) Successful in 2s
2026-09-01 10:16:46 +08:00
Mimingguang 937f353b41 docs(changelog): 回写 #6842 前端已交付(hl-admin f5414b12)
changelog-filename-gate / validate (push) Successful in 2s
frontend_status verified,frontend_ref f5414b12,target_release v2.1。
2026-09-01 09:52:26 +08:00
Mimingguang 43bf3936d1 docs(changelog): 回写 #6834 前端已交付(hl-admin 848cca7b)
changelog-filename-gate / validate (push) Successful in 2s
frontend_status verified,frontend_ref 848cca7b,target_release v2.1。
2026-09-01 09:31:20 +08:00
Mimingguang 33a8f7cb5b docs(changelog): 回写派单 605074 车队停用前端已消费(frontend_ref f1b861c6)
changelog-filename-gate / validate (push) Successful in 2s
2026-09-01 09:13:41 +08:00
wx 5ce53bcd47 docs: 供应商暂停合作联动停用车队与派单拦截(#6843) (#84)
changelog-filename-gate / validate (push) Successful in 1s
docs: 供应商暂停合作联动停用车队与派单拦截(#6843) (#84)
2026-08-31 14:36:14 +08:00
lc 430d825904 补充供应商余额与支付类型接口说明(#6834)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-31 12:24:30 +08:00
lc 2b60fc3323 补充供应商合同字段与附件(#6842)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-31 12:20:58 +08:00
Mimingguang 070c937a08 docs(changelog): 回写车队 601112 在役车辆守卫前端已消费(frontend_ref 6985f0cf)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-31 10:55:41 +08:00
API Changelog Bot e28f675c8f 车队保存无供应商时自动停用与在役车辆守卫(#6811)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-31 10:46:03 +08:00
Mimingguang 787b9a007b docs(changelog): 回写车队排序校验缺陷前端已修复(frontend_ref e2b2482e)
changelog-filename-gate / validate (push) Successful in 3s
2026-08-31 09:56:51 +08:00
API Changelog Bot d81fea420a docs(changelog): 车队编辑弹窗排序已填仍提示请输入排序-前端缺陷通知
changelog-filename-gate / validate (push) Successful in 2s
2026-08-31 09:45:39 +08:00
Mimingguang 0e1ab992e8 chore(changelog): 回写 #6717 前端消费状态
changelog-filename-gate / validate (push) Successful in 2s
2026-08-30 18:10:46 +08:00
wx 95212ad9a5 docs: 车队关联供应商并展示供应商全名(#6717) (#83)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-30 17:54:25 +08:00
Mimingguang 2ebb4644a6 chore(changelog): 回写 #6684/#6710/#6739 前端消费状态
changelog-filename-gate / validate (push) Successful in 2s
2026-08-30 17:44:59 +08:00
lc 17bb0ef2bf 新增供应商三级地址保存与回显契约(#6739)
changelog-filename-gate / validate (push) Successful in 3s
2026-08-30 15:44:39 +08:00
lc 5c52926269 docs: 精简6710前端变更记录
changelog-filename-gate / validate (push) Successful in 2s
2026-08-30 10:24:09 +08:00
lc a45b51d8a8 docs: 更正6710为无需前端处理
changelog-filename-gate / validate (push) Successful in 2s
2026-08-30 10:16:40 +08:00
lc 73a4518181 docs: 交付供应商联系人快照版本兼容 (#6710)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-30 10:08:10 +08:00
lc 8821d1df6f docs: 交付供应商草稿变更原因契约 (#6684)
changelog-filename-gate / validate (push) Successful in 1s
2026-08-30 08:18:42 +08:00
Mimingguang 51708dc7a6 chore(changelog): 回写 #6669 前端消费状态 verified(frontend_ref 6678b97f)
changelog-filename-gate / validate (push) Successful in 3s
2026-08-29 17:34:50 +08:00
lc b28bd34ceb Merge pull request 'docs(changelog): 补充草稿结算编辑契约 #6669' (#82) from chore/6669-6676-supplier-draft-settlement-changelog into main
changelog-filename-gate / validate (push) Successful in 2s
2026-08-29 17:12:56 +08:00
lc 1ba3299afa docs(changelog): 补充草稿结算编辑契约 #6669
changelog-filename-gate / validate (pull_request) Successful in 2s
2026-08-29 17:12:25 +08:00
Mimingguang bd235c2e1d chore(changelog): 回写 #6654 前端消费状态 verified(frontend_ref 5fe4e6fa)
changelog-filename-gate / validate (push) Successful in 3s
2026-08-29 16:32:05 +08:00
lc eb628f56b0 docs(changelog): 补充 #6654 供应商合同与结算契约
changelog-filename-gate / validate (push) Successful in 2s
2026-08-29 16:15:01 +08:00
Mimingguang b54c4cc8d7 chore(changelog): 回写 #6544/#6620/#6643 前端消费状态 verified
changelog-filename-gate / validate (push) Successful in 2s
修改原因:三条 changelog 前端已交付并推送 hl-admin v2.1,回写 frontmatter 消费证据。
- #6544 供应商合同独立登记 → frontend_ref 55a3a056
- #6620 主体证件号 taxNo 回显(DRAFT 可改非 DRAFT 锁定) → frontend_ref 9d5dcbab
- #6643 营业执照 licenseImageUrl 顶层权威回显 → frontend_ref bab3673b
2026-08-29 15:07:39 +08:00
lc 4621f11e36 交接供应商编辑字段回显契约(#6643)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-29 14:36:10 +08:00
lc 1f608928b7 Merge pull request 'docs: 交接供应商 tax_no 表单绑定缺口 (#6620)' (#81) from changelog/6620-supplier-taxno-frontend into main
changelog-filename-gate / validate (push) Successful in 2s
2026-08-29 12:01:09 +08:00
lc a4aba37568 docs: hand off supplier tax_no binding #6620
changelog-filename-gate / validate (pull_request) Successful in 2s
2026-08-29 11:53:48 +08:00
lc 60a28339e0 Merge pull request 'docs: 纠正供应商合同独立登记交付状态 (#6544)' (#80) from changelog/6544-supplier-contract-boundary into main
changelog-filename-gate / validate (push) Successful in 2s
2026-08-29 09:40:44 +08:00
共修改 33 个文件,包含 12065 行新增和 2 行删除
@@ -7,9 +7,9 @@ author: "lc(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "pending"
frontend_ref: "55a3a056"
target_release: "v2.1"
verified_at: "2026-08-29"
status_note: "PR #6559、#6591、#6606 已合并 dev-v3;hl-resource-service 已通过 Deploy Panel 任务 ca2fe4d7 部署最终提交 c579c87014f56452fea2fce5075b31ae6fd12a35。#6591 的写后回读、395052–395058、金额 scale 等值与草稿合同存在门禁均已进入 TEST。#6544 基线曾完成独立合同写链路实证;本次最终提交因运行时身份缺少 supplier:update,按用户确认冻结权限依赖的真实写复测,未配置权限、未发业务写请求,不把缺失权限记为通过。"
@@ -0,0 +1,55 @@
---
schema: "hl-changelog/v2"
ticket: "6620"
title: "供应商编辑页主体证件号未绑定 tax_no"
consumer: "admin"
author: "lc(GIT)"
change_type: "前端缺陷"
backend_status: "not_required"
gateway_status: "not_required"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "9d5dcbab"
target_release: "v2.1"
verified_at: "2026-08-29"
status_note: "hl-admin 端实际绑定 formData.taxNo(非 socialCreditCode,该定位对端不符);经用户拍板采用「tax_no 回显、DRAFT 可改、非 DRAFT 锁定」折中,非 DRAFT 恒不携带防 395033,未按字面「保持可编辑」。契约库记 002 tax_no 脱敏与本条称完整值不一致,回填已加脱敏守卫兼容。"
updated_at: "2026-08-29"
base: "dev-v3"
---
# 供应商编辑页主体证件号未绑定 `tax_no`
后端契约没有变化。打开已有供应商时,详情响应已经返回完整 `tax_no`,管理端需在表单适配层消费该字段。
## 接口与字段
- 接口:`GET /admin/supplier/items/{supplierId}/basic-info/view`
- 响应字段:`data.tax_no`,类型为 `string`,值为完整主体证件号。
- 创建、更新请求字段仍为 `taxNo`。响应与请求字段命名不同,前端不得改读 `data.taxNo` 或用 `socialCreditCode` 代替响应字段。
## 前端处理要求
1. 详情加载后,将非空 `data.tax_no` 映射为“主体证件号”输入框的实际值,并保持可编辑。
2. 详情值必须覆盖表单初始空值;初始化或重置逻辑不得在映射后再次清空。
3. 只有接口实际返回空值时才显示空输入框;占位提示不能代替已有证件号。
4. 保存时继续按既有写契约提交 `taxNo`,不要新增或要求后端返回重复字段。
## 后端核验结论
- 响应 VO 将 Java 属性 `taxNo` 固定序列化为 JSON 字段 `tax_no`。
- 查询服务直接取供应商主体的 `taxNo` 写入响应,持久化读取兼容密文和历史明文。
- 供应商回显、查询、Controller 契约与聚合写入相关测试共 60 项通过,失败、错误和跳过均为 0。
- #6499 / PR #6505 已在 TEST 验证详情返回的 `tax_no` 与数据库原值一致;当前 `dev-v3` 已包含该实现。本条目不引入后端代码、接口、数据库、配置、Redis 或 MQ 变更。
## 只读前端定位
现有管理端表单显示字段绑定为 `socialCreditCode`,详情适配逻辑没有把后端 `tax_no` 映射到该输入框,因此页面会保留初始空值。该定位仅用于联调交接,本工单未修改前端源码。
## 撤回
如需撤回本联调通知,只需回退本 Changelog 文件;既有后端 `tax_no` 契约和运行数据不受影响。
## 关联 / 联系人
- Issue:[#6620](https://git.1814.love:8443/wx/HL/issues/6620)
- 后端联系人:@lc
@@ -0,0 +1,389 @@
---
schema: "hl-changelog/v2"
ticket: "6643"
title: "供应商编辑页地址营业执照与备注回显"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "bab3673b"
target_release: "v2.1"
verified_at: "2026-08-29"
status_note: "PR #6645 已合并 dev-v3;hl-resource-service 已通过 Deploy Panel 任务 954419f9 部署提交 d206d1d577a3e3f8cec228e73e139fcb36c00fd5。真实 TEST Gateway 已验证 address、licenseImageUrl、remark 原值回显、更新后回读、空地址保留、营业执照顶层字段权威读取、395014 零写入及测试草稿清理。"
updated_at: "2026-08-29"
base: "dev-v3"
---
# 🔧 供应商编辑页地址、营业执照与备注回显
供应商编辑页应直接使用详情根对象的 `address`、`licenseImageUrl`、`remark` 初始化表单。其中 `licenseImageUrl` 是营业执照上传控件的权威字段,不得再从 `qualifications[].imageUrl` 推导或覆盖。
本次没有新增接口路径、请求必填项、权限点或业务错误码。
## 一、关键变化
| 字段 | 修改前 | 修改后 |
|---|---|---|
| `data.address` | 已有详情字段,但编辑和空值保留规则缺少本次回归锁定 | 返回已保存地址;提交非空新值后可回读,更新时留空保持原值 |
| `data.licenseImageUrl` | 详情根对象没有稳定的营业执照权威回显,调用方可能从资质数组取值 | 详情根对象稳定返回营业执照;只读取顶层权威值,不再从资质数组反向派生 |
| `data.remark` | 已有详情字段,但编辑回显和更新规则缺少本次回归锁定 | 返回已保存内部备注;提交非空新值后可回读 |
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---:|---|---|---|---|---|
| 1 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应修改 | 根对象稳定返回 `address`、`licenseImageUrl`、`remark` |
| 2 | 更新供应商资料 | PUT | `/admin/supplier/items/{supplierId}/update` | 行为修改 | 三字段按增量更新和并发版本规则保存;写后重新查询可得到新值 |
## 三、接口详情
### 1. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
**VO**: `SupplierBasicInfoRespVO`
#### 使用场景
管理端进入供应商详情或编辑页时获取完整表单快照。营业执照上传控件必须绑定根对象 `data.licenseImageUrl`;`qualifications[]` 继续用于独立资质列表,不是该控件的取值来源。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商;不得转为 JavaScript Number |
无 Query 参数,无请求体。
#### 出参 `Result<SupplierBasicInfoRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.address` | String/null | 注册地址或经营地址;未填写时为 `null` |
| `data.licenseImageUrl` | String/null | 营业执照影像权威地址;未填写时为 `null` |
| `data.remark` | String/null | 供应商主体内部备注;不等同于审批意见或变更原因 |
| `data.qualifications[].imageUrl` | String/null | 单项资质影像;与根对象营业执照字段分别读取 |
| `data.updateTime` | String | 主体并发版本,格式 `yyyy-MM-dd HH:mm:ss` |
#### 请求示例
```http
GET /admin/supplier/items/2092800000000000001/basic-info/view
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2092800000000000001",
"supplierNo": "SUP2092800000000000001",
"fullName": "示例旅行服务有限公司",
"shortName": "示例旅行",
"tax_no": "91350211M000100Y46",
"legalRepresentative": "示例法人",
"legalRepresentativeIdNo": "11010519491231002X",
"legalRepresentativeIdNoMask": "11010519491231002X",
"legalRepresentativeIdCardFrontUrl": null,
"legalRepresentativeIdCardBackUrl": null,
"contactPhone": "13800138000",
"contactPhoneMask": "13800138000",
"establishDate": "2020-01-01",
"registeredCapital": "100万元",
"businessScope": "境内旅游服务",
"address": "福建省厦门市示例路 2 号",
"staffScale": "LT50",
"mainCooperation": "酒店与景区资源合作",
"licenseImageUrl": "https://files.example.com/supplier/business-license-new.jpg",
"remark": "地址与营业执照已复核",
"status": "DRAFT",
"creditLevel": "B",
"totalScore": null,
"types": [],
"primaryTypeCode": null,
"primaryTypeName": null,
"contacts": [],
"qualifications": [
{
"qualificationId": "2092800000000000011",
"qualType": "BUSINESS_LICENSE",
"qualTypeName": "营业执照",
"certNo": null,
"certNoMask": null,
"imageUrl": "https://files.example.com/supplier/business-license-new.jpg",
"expiryDate": null,
"permanentValid": true,
"daysUntilExpiry": null,
"validityStatus": "VALID",
"validityStatusName": "有效",
"isRequired": false,
"expired": false,
"updateTime": "2026-08-29 14:40:01"
}
],
"contracts": [],
"updateTime": "2026-08-29 14:40:02"
}
}
```
#### 空数据 / 降级响应
未填写三字段时返回明确的 `null`,前端显示空控件即可,不要用资质数组补写营业执照:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2092800000000000001",
"address": null,
"licenseImageUrl": null,
"remark": null,
"qualifications": [],
"updateTime": "2026-08-29 14:40:02"
}
}
```
#### 错误响应
供应商不存在、已删除或超出可见范围:
```json
{
"code": 395001,
"message": "供应商不存在",
"success": false,
"data": null
}
```
#### 业务边界
- 要求可信管理身份、可读角色、`supplier:view` 平台权限和既有数据范围。
- 根对象 `licenseImageUrl` 是编辑页营业执照的唯一权威回显;即使 `qualifications[]` 中同类型影像不同,也不得覆盖根对象值。
- 历史记录没有可用营业执照时返回 `null`,不猜测默认图片。
- 本接口只读,不推进版本、不触发审批或其他业务副作用。
### 2. 更新供应商资料 `PUT /admin/supplier/items/{supplierId}/update`
**VO**: `SupplierUpdateReqVO / SupplierWriteRespVO`
#### 使用场景
管理端保存供应商编辑表单。请求只发送实际修改字段,并携带详情中的最新 `updateTime` 和本次 `changeReason`。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
| `address` | Body | String | 否 | 最长 500 字符 | 非空时更新;省略、`null`、空串或纯空格时保留原值 |
| `licenseImageUrl` | Body | String | 否 | 最长 500 字符 | 省略时保留;传非空值时更新权威营业执照;传空串时清空 |
| `remark` | Body | String | 否 | - | 非空时更新;省略、`null`、空串或纯空格时保留原值 |
| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 字符 | 本次资料变更原因 |
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 详情最新主体并发版本 |
其他既有可选字段和集合快照规则保持不变。
#### 出参 `Result<SupplierWriteRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.supplierId` | String | 供应商 ID |
| `data.supplierNo` | String | 供应商业务编号 |
| `data.status` | String | 当前生命周期状态 |
| `data.onboardingStage` | String | 当前注册阶段 |
| `data.initialAccounts` | Array | 初始收款账户摘要 |
| `data.updateTime` | String | 写入后的新主体并发版本 |
#### 请求示例
```http
PUT /admin/supplier/items/2092800000000000001/update
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"address": "福建省厦门市示例路 2 号",
"licenseImageUrl": "https://files.example.com/supplier/business-license-new.jpg",
"remark": "地址与营业执照已复核",
"changeReason": "更新供应商编辑资料",
"expectedUpdateTime": "2026-08-29 14:40:00"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2092800000000000001",
"supplierNo": "SUP2092800000000000001",
"status": "DRAFT",
"onboardingStage": "PROFILE_DRAFT",
"initialAccounts": [],
"updateTime": "2026-08-29 14:40:02"
}
}
```
写响应不重复返回 `address`、`licenseImageUrl`、`remark`。前端需要完整表单时,应使用新 `updateTime` 重新查询详情。
#### 空数据 / 降级响应
成功更新时 `data` 恒为写入结果对象,不返回 `null`。当前请求没有实际变化时不会伪造成功或推进版本,而是返回业务失败:
```json
{
"code": 400,
"message": "未检测到实际变化",
"success": false,
"data": null
}
```
#### 错误响应
```json
{
"code": 395014,
"message": "数据已被他人修改,请刷新后重试",
"success": false,
"data": null
}
```
收到 `395014` 后先重新获取详情,由用户确认后再提交;不得用旧表单自动覆盖。
#### 业务边界
- 要求可信管理身份、`FINANCE` 或 `SUPER_ADMIN` 写角色及 `supplier:update` 平台权限。
- 成功写入后推进主体 `updateTime`;权限、参数、状态或并发失败时三字段及版本均不变化。
- 顶层 `licenseImageUrl` 更新时会继续兼容同步营业执照资质影像;反向只修改 `qualifications[].imageUrl` 不会改写顶层权威字段。
- `remark` 是主体内部备注;`changeReason` 是本次变更审计原因,两者不得互相替代。
- 统一响应可能以 HTTP 200 承载业务失败,调用方必须同时判断 `code`、`success`、`message` 和 `data`。
## 四、契约约束与正确调用方式
### 编辑页初始化
```text
地址输入框 ← data.address
营业执照上传控件 ← data.licenseImageUrl
内部备注输入框 ← data.remark
并发版本 ← data.updateTime
```
不得把 `qualifications.find(item => item.qualType === "BUSINESS_LICENSE").imageUrl` 作为营业执照上传控件的回显值或顶层字段兜底。
### 更新规则对照
| 场景 | 请求 | 结果 |
|---|---|---|
| 更新三个字段 | 发送三个非空新值 + 原因 + 最新版本 | 更新成功;重新查询返回三个新值 |
| 地址留空,修改备注 | `address: " "`,`remark` 为新值 | 地址保持原值,备注更新 |
| 省略营业执照 | 不发送 `licenseImageUrl` | 顶层营业执照保持原值 |
| 清空营业执照 | `licenseImageUrl: ""` | 顶层营业执照清空,兼容影像同步清空 |
| 只改资质数组影像 | 发送 `qualifications[]`,不发送顶层字段 | 资质影像独立变化,顶层营业执照保持原值 |
| 使用旧版本 | 发送过期 `expectedUpdateTime` | 返回 `395014`,零写入 |
## 五、数据库行为
本节只描述接口可观察结果,不要求前端感知存储结构:
- 三字段更新成功后,再次查询详情返回新值,且主体 `updateTime` 推进。
- `address` 或 `remark` 留空时保存其他字段,重新查询仍返回原地址或原备注。
- 顶层 `licenseImageUrl` 更新成功后,详情根对象与兼容营业执照资质影像均返回新值。
- 只更新资质数组影像时,详情根对象 `licenseImageUrl` 保持原值。
- 权限、参数、状态或 `395014` 并发失败时,三字段和主体版本均不变化。
- 部署时已对可识别的历史营业执照影像完成一次性兼容初始化;之后详情根对象不再从资质数组动态派生。
## 六、边界行为
- 未登录或 Token 无效由 Gateway 拒绝,不产生业务写入。
- 无 `supplier:view` 时详情读取失败;无 `supplier:update` 或不属于允许写角色时更新返回 `395002`,零写入。
- 供应商不存在、已删除或不可见时返回 `395001`。
- `address` 或 `licenseImageUrl` 超过各自长度上限时返回 `400`,零写入。
- 缺少 `changeReason`、缺少 `expectedUpdateTime` 或没有实际变化时返回 `400`,零写入。
- 版本过期返回 `395014`;客户端必须刷新详情,不能自动覆盖。
- `ARCHIVED` 等不可修改状态继续由既有状态门禁拒绝。
- 业务失败可能仍使用 HTTP 200,客户端必须检查统一响应体。
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|---|---|---|
| `data.address` | 字段已存在,但本次编辑回归未锁定 | 原值、更新值及留空保留规则均已锁定 |
| `data.licenseImageUrl` | 根对象没有稳定权威回显 | 根对象返回可空权威值,不从 `qualifications[]` 反向派生 |
| `data.remark` | 字段已存在,但本次编辑回归未锁定 | 原值和更新值均可稳定回读 |
### 行为级对比
| 行为 | 改前 | 改后 |
|---|---|---|
| 编辑页初始化营业执照 | 可能从资质数组搜索影像 | 固定读取根对象 `licenseImageUrl` |
| 更新营业执照 | 根对象回读来源不稳定 | 提交顶层字段后,写后查询返回同一新值 |
| 单独修改资质影像 | 可能被误当成主体营业执照 | 不改变根对象营业执照权威值 |
## 六.7、影响评估
- **是否破坏向后兼容**:否。详情根对象增加可空字段并固定既有字段行为;旧客户端可忽略未知字段。
- **前端是否必须同步上线**:需要。营业执照上传控件必须改为读取并提交顶层 `licenseImageUrl`;地址和备注继续直接绑定根对象字段。
- **前端 workaround 清理点**:删除从 `qualifications[]` 搜索营业执照影像并覆盖表单值的逻辑。
- **QA 重点**:已有值回显、三字段更新后刷新、地址留空保留、资质影像与顶层营业执照相互独立、旧版本失败零写入。
## 七、不影响范围
- **仅影响**:管理后台供应商详情与编辑表单的地址、营业执照和内部备注。
- **零影响**:
- 接口路径、Gateway 路由和认证级别。
- 供应商既有角色、平台权限和数据范围。
- 生命周期状态机、审批、合同、收款账户和资源关系。
- 既有业务错误码、Redis、MQ、Feign 和其他服务。
- 小程序、C 端、Web、H5 和桌面端接口。
## 八、测试环境已验证
- 本地:供应商定向 62 项通过;`hl-resource-service` Reactor 全量 2,186 项通过,0 失败、0 错误,38 项既有条件跳过。
- 合并:后端 PR [#6645](https://git.1814.love:8443/wx/HL/pulls/6645) 已合并,合并提交为 `d206d1d577a3e3f8cec228e73e139fcb36c00fd5`。
- 部署:Deploy Panel API 任务 `954419f9` 成功,TEST 目标与实际提交均为上述合并提交;任务期 6 个采样均至少保持 2 个进程与 2 个健康启用实例匹配,0 个不可用采样。
- 真实 Gateway:唯一测试草稿依次验证原值回显、资质影像独立变化时顶层营业执照不变、三字段更新后回读、空地址保留、旧版本 `395014` 零写入及未认证拒绝。
- 清理:测试草稿已通过业务删除接口软删除并确认详情不可查询;仅保留系统规定的脱敏业务审计事实。
## 九、撤回
如后端撤回本次契约,前端在回退部署前停止依赖顶层 `licenseImageUrl`,并以同步发布的撤回 Changelog 为准;不得自行恢复未冻结的资质数组反向覆盖逻辑。地址与备注仍按既有 #6476 契约处理。
## 十、相关文档
- 关联 Issue:[#6643](https://git.1814.love:8443/wx/HL/issues/6643)
- 关联 PR:[#6645](https://git.1814.love:8443/wx/HL/pulls/6645)
- 既有地址与备注契约:[#6476](./27_6476_供应商详情补齐地址备注并统一表单校验-修改接口-管理后台.md)
- 当前前端状态:已消费(`frontend_status: verified`)。前端本就从顶层控件读执照、从未从资质数组推导;本次补齐编辑态 `licenseImageUrl` 顶层权威回显(快照比对:改携带新值/清空携带空串/未动不携带)。address/remark 沿用既有 #6476 语义(remark 回填快照比对、address 级联不回显),与本条一致未改动。
## 关联 / 联系人
### 链接
- **Issue**: [#6643](https://git.1814.love:8443/wx/HL/issues/6643)
- **PR**: [#6645](https://git.1814.love:8443/wx/HL/pulls/6645)
- **Merge commit**: [`d206d1d5`](https://git.1814.love:8443/wx/HL/commit/d206d1d577a3e3f8cec228e73e139fcb36c00fd5)
- **既有地址/备注契约**: [#6476](./27_6476_供应商详情补齐地址备注并统一表单校验-修改接口-管理后台.md)
### 联系人
- **后端负责人**: @lc
- **管理端负责人**: 待认领(`frontend_status: pending`)
@@ -0,0 +1,389 @@
---
schema: "hl-changelog/v2"
ticket: "6654"
title: "供应商合同字段可空与编辑信息入口"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "5fe4e6fa"
target_release: "v2.1"
verified_at: "2026-08-29"
status_note: "PR #6665 已合并 dev-v3;hl-resource-service 已通过 Deploy Panel 任务 3dde08b9 部署提交 65ac86e9312b22dd2fc7313049a693fdb0ccad59。真实 TEST Gateway 已验证合同业务字段全空登记、补全、清空、删除,changeReason 必填失败零写入,expectedUpdateTime 省略成功及过期值拒绝;测试草稿已清理。合同与结算 table 的页面顺序和消费映射待前端处理。"
updated_at: "2026-08-29"
base: "dev-v3"
---
# 供应商合同字段可空与编辑信息入口
供应商合同继续走独立登记接口,不并入供应商档案聚合。合同的 12 个业务字段现在全部可空,`changeReason` 继续必填;合同更新、删除的 `expectedUpdateTime` 改为可选,但一旦提供,过期版本仍会被拒绝。
管理端新建供应商时可完全不登记合同,取得 `supplierId` 后再补录;编辑页通过基本信息详情读取合同,通过既有账户接口读取和维护结算信息。页面按“资质证照 → 合同信息 → 结算信息”排列属于前端消费工作,当前状态为待前端处理。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---:|---|---|---|---|---|
| 1 | 独立登记供应商合同 | POST | `/admin/supplier/items/{supplierId}/contracts/add` | 修改请求 | 12 个合同业务字段全部可空,`changeReason` 仍必填 |
| 2 | 独立更新供应商合同 | PUT | `/admin/supplier/items/{supplierId}/contracts/{contractId}/update` | 修改请求 | 合同业务字段与新增一致且可清空,`expectedUpdateTime` 可选 |
| 3 | 独立删除供应商合同 | DELETE | `/admin/supplier/items/{supplierId}/contracts/{contractId}/del` | 修改请求 | `expectedUpdateTime` 可选,`changeReason` 仍必填 |
| 4 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 修改响应语义 | `contracts[]` 的全部业务字段允许返回 `null` |
## 三、接口详情
### 1. 独立登记供应商合同 `POST /admin/supplier/items/{supplierId}/contracts/add`
**VO**: `SupplierContractCreateReqVO / SupplierContractRespVO`
#### 使用场景
供应商已经创建但合同资料尚未齐全时,可先登记一条纯合同记录,之后再补充。若暂时不需要合同记录,供应商新建请求直接省略废弃的 `contracts` 字段即可。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
| `contractName` | Body | String | 否 | 最长 500 字符 | 合同名称 |
| `contractNo` | Body | String | 否 | 最长 100 字符 | 合同编号 |
| `contractType` | Body | String | 否 | `FRAME` / `SINGLE_TRIP` / `PURCHASE` | 合同类型 |
| `signDate` | Body | String | 否 | `yyyy-MM-dd` | 签署日期 |
| `startDate` | Body | String | 否 | `yyyy-MM-dd` | 有效期开始;仅与同时提供的结束日期做区间校验 |
| `endDate` | Body | String | 否 | `yyyy-MM-dd` | 有效期结束;两端都提供时不得早于开始日期 |
| `amount` | Body | String | 否 | 非负,最多 10 位整数和 2 位小数 | 合同金额 |
| `pricingMode` | Body | String | 否 | 最长 100 字符 | 计价方式 |
| `settleCycle` | Body | String | 否 | 最长 32 字符 | 结算周期说明 |
| `status` | Body | String | 否 | `DRAFT` / `ACTIVE` / `EXPIRED` | 合同状态 |
| `scanFileUrl` | Body | String | 否 | 最长 1000 字符 | 扫描件永久地址 |
| `remark` | Body | String | 否 | 最长 500 字符 | 合同备注 |
| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 字符 | 审计原因,不属于可空业务字段 |
#### 出参 `Result<SupplierContractRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.contractId` | String | 新合同雪花 ID |
| `data.contractName` 等 12 个业务字段 | 对应类型/null | 未填写的字段返回 `null` |
| `data.updateTime` | String | 服务端合同版本,格式 `yyyy-MM-dd HH:mm:ss` |
#### 请求示例
```json
{
"changeReason": "合同资料暂缺,先登记记录"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"contractId": "2094000000000000001",
"contractName": null,
"contractNo": null,
"contractType": null,
"signDate": null,
"startDate": null,
"endDate": null,
"amount": null,
"pricingMode": null,
"settleCycle": null,
"status": null,
"scanFileUrl": null,
"remark": null,
"updateTime": "2026-08-29 16:10:00"
}
}
```
#### 空数据 / 降级响应
请求体除 `changeReason` 外可以不包含任何字段;成功后返回合同对象,业务字段全部为 `null`。不要把 `null` 自动替换为默认合同类型、状态、日期或金额。
#### 错误响应
缺少审计原因时失败且不新增合同:
```json
{ "code": 400, "message": "变更原因不能为空", "success": false, "data": null }
```
#### 业务边界
- 要求可信管理员、`FINANCE`/`SUPER_ADMIN` 写角色和 `supplier:update` 平台权限。
- 合同登记不进入供应商审批,不推进供应商主体 `updateTime`。
- `POST /admin/supplier/items/add` 和供应商资料更新中的废弃 `contracts` 字段仍被忽略;前端必须先获得 `supplierId`,再按需调用本接口。
### 2. 独立更新供应商合同 `PUT /admin/supplier/items/{supplierId}/contracts/{contractId}/update`
**VO**: `SupplierContractUpdateReqVO / SupplierContractRespVO`
#### 使用场景
补齐、修改或清空一条已登记合同。该接口执行完整替换:未传或传 `null` 的业务字段会保存为 `null`,不是“保持原值”。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
| `contractId` | Path | String | 是 | 正整数 ID 字符串 | 目标合同 |
| 新增接口的 12 个业务字段 | Body | 对应类型 | 否 | 与新增一致 | 完整替换;省略即清空对应字段 |
| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 字符 | 审计原因 |
| `expectedUpdateTime` | Body | String | 否 | `yyyy-MM-dd HH:mm:ss` | 提供时必须等于当前合同版本;省略时由服务端锁串行更新 |
#### 出参 `Result<SupplierContractRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.contractId` | String | 合同雪花 ID |
| `data.contractName` 等 12 个业务字段 | 对应类型/null | 完整替换后的值,允许为 `null` |
| `data.updateTime` | String | 更新后的合同版本 |
#### 请求示例
```json
{
"contractName": "2026 年度框架合同",
"contractType": "FRAME",
"startDate": "2026-09-01",
"endDate": "2027-08-31",
"status": "ACTIVE",
"changeReason": "补齐已签署合同"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"contractId": "2094000000000000001",
"contractName": "2026 年度框架合同",
"contractType": "FRAME",
"startDate": "2026-09-01",
"endDate": "2027-08-31",
"status": "ACTIVE",
"updateTime": "2026-08-29 16:10:01"
}
}
```
#### 空数据 / 降级响应
仅发送 `changeReason` 可把 12 个业务字段全部清空。若替换后的业务载荷与当前记录完全相同,返回“未检测到合同实际变化”,不会伪造新版本。
#### 错误响应
提供过期版本时继续失败且零写入:
```json
{ "code": 395014, "message": "数据已被修改,请刷新后重试", "success": false, "data": null }
```
#### 业务边界
- `expectedUpdateTime` 省略不等于关闭并发保护;分布式合同锁和数据库行锁仍串行化同一合同写入。
- 客户端若选择发送版本,必须使用最近一次详情或写响应中的 `updateTime`。
- `changeReason` 始终必填,失败时合同和审计均不写入。
### 3. 独立删除供应商合同 `DELETE /admin/supplier/items/{supplierId}/contracts/{contractId}/del`
**VO**: `SupplierContractDeleteReqVO / Void`
#### 使用场景
删除不再保留的合同登记。删除为软删除,并记录完整审计原因。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
| `contractId` | Path | String | 是 | 正整数 ID 字符串 | 目标合同 |
| `expectedUpdateTime` | Body | String | 否 | `yyyy-MM-dd HH:mm:ss` | 提供时执行版本匹配 |
| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 字符 | 删除审计原因 |
#### 出参 `Result<Void>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | Integer | 成功时为 `200` |
| `success` | Boolean | 成功时为 `true` |
| `data` | null | 删除成功不返回业务对象 |
#### 请求示例
```json
{
"changeReason": "合同登记作废"
}
```
#### 响应示例
```json
{ "code": 200, "message": "成功", "success": true, "data": null }
```
#### 空数据 / 降级响应
`expectedUpdateTime` 可省略,但请求体不能省略,且必须包含非空 `changeReason`。
#### 错误响应
```json
{ "code": 400, "message": "变更原因不能为空", "success": false, "data": null }
```
#### 业务边界
- 提供的过期 `expectedUpdateTime` 仍返回 `395014`,不删除、不写审计。
- 删除成功后基本信息详情的 `contracts[]` 不再返回该记录。
- 权限、锁、幂等、审计和软删除边界均保持原有实现。
### 4. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
**VO**: `SupplierBasicInfoRespVO`
#### 使用场景
进入供应商编辑页时读取主体、资质和合同。前端将 `data.contracts` 绑定到“合同信息”table。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
无 Query 参数,无请求体。
#### 出参 `Result<SupplierBasicInfoRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.contracts` | Array | 当前未软删除合同,按 `contractId` 升序 |
| `data.contracts[].contractId` | String | 合同雪花 ID |
| `data.contracts[]` 的 12 个业务字段 | 对应类型/null | 合同登记未填写时返回 `null` |
| `data.contracts[].updateTime` | String | 合同当前版本 |
#### 请求示例
```http
GET /admin/supplier/items/2094000000000000000/basic-info/view
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2094000000000000000",
"contracts": [
{
"contractId": "2094000000000000001",
"contractName": null,
"contractType": null,
"startDate": null,
"endDate": null,
"status": null,
"updateTime": "2026-08-29 16:10:00"
}
]
}
}
```
#### 空数据 / 降级响应
没有合同登记时 `data.contracts` 返回空数组;单份合同没有填写的业务字段返回 `null`,二者含义不同。
#### 错误响应
```json
{ "code": 395001, "message": "供应商不存在", "success": false, "data": null }
```
#### 业务边界
- 只读接口要求可信管理员、可读角色和 `supplier:view` 平台权限。
- 查询不推进主体或合同版本,不触发审批和写副作用。
- 合同 table 的新增、编辑、删除分别调用前三个独立写接口,不把 `contracts` 回传给供应商聚合更新接口。
## 四、契约约束与正确调用方式
- 新建供应商可完全省略合同;若需登记,先调用 `POST /admin/supplier/items/add` 获取字符串 `supplierId`,再调用合同新增接口。
- 编辑页合同读取使用 `GET /admin/supplier/items/{supplierId}/basic-info/view` 的 `data.contracts`。
- 编辑页结算读取继续使用 `GET /admin/supplier/items/{supplierId}/account-info/list`;新增账户继续使用 `POST /admin/supplier/items/{supplierId}/bank-accounts/add`,其审批和主体状态门禁不变。
- 新建的 `initialAccounts[]` 与独立账户新增的 `accounts[]` 继续复用同一 `SupplierBankAccountReqVO`,可填写项一致:`accountType`、`bankName`、`bankBranch`、`accountNo`、`proofFileUrls`、`settleMode`、`accountPeriod`、`invoiceType`、`taxRate`。新建供应商最多携带 1 项初始账户。
- `supplierId`、`contractId`、`accountId` 和 `amount` 按字符串处理,禁止转为 JavaScript Number。
## 五、数据库行为
- Flyway migration `V20260829_002` 将 `supplier_contract.contract_name`、`contract_type`、`start_date`、`end_date`、`status` 从 `NOT NULL` 调整为可空。
- 合同创建、更新、删除继续在本服务 schema 内完成;审计与业务写同事务提交或回滚。
- 没有跨 schema 写入,没有新增 Redis、MQ、配置或路由变更。
## 六、边界行为
- 业务字段为空不等于审计字段可空:合同新增、更新、删除均必须有 `changeReason`。
- `expectedUpdateTime` 仅在合同更新和删除中可省略;提供时仍执行秒级版本比较。
- 合同日期只在 `startDate` 与 `endDate` 同时存在时校验先后顺序。
- 合同枚举字段为空时不校验;非空时仍只接受既有枚举值。
- 账户查询和新增的既有状态、审批、权限及敏感附件读取规则未改变。
## 六.6、修改前后对比
| 项目 | 修改前 | 修改后 |
|---|---|---|
| 合同名称、类型、开始日、结束日、状态 | 必填 | 可空 |
| 其余 7 个合同业务字段 | 可空 | 仍可空 |
| `changeReason` | 新增、更新、删除均必填 | 继续必填 |
| `expectedUpdateTime` | 更新、删除必填 | 更新、删除可选;提供过期值仍拒绝 |
| 结算信息字段模型 | 新建与独立账户新增复用同一 VO | 不变,编辑页继续复用既有账户接口 |
## 六.7、影响评估
- **是否破坏向后兼容**: 否;原先完整载荷继续有效,旧客户端继续发送版本也有效。
- **前端是否必须同步上线**: 是;需要增加合同/结算 table、允许合同业务控件为空,并保留 `changeReason` 必填。
- **前端 workaround 清理点**: 删除合同业务字段的前端强制必填;不要删除 `changeReason` 校验。
## 七、不影响范围
- 不修改供应商新建、更新、提交请求中的废弃 `contracts` 聚合字段语义。
- 不修改结算账户字段、审批、状态机、默认账户、权限或接口路径。
- 不修改供应商主体的 `changeReason`、`expectedUpdateTime` 必填规则。
- 不新增业务错误码、Gateway 路由、Redis、MQ 或 Nacos 配置。
## 八、测试环境已验证
- 合同信息整体省略不影响供应商草稿创建;空业务字段合同可登记并由详情读回。
- 合同可在不传 `expectedUpdateTime` 时补全、清空和删除;传入过期版本返回并发失败且零写入。
- 新增、更新、删除缺少 `changeReason` 均失败且零写入。
- 编辑供应商后合同和初始结算账户摘要保持;结算读取入口可正常访问。
- TEST 验收产生的供应商草稿、合同和初始账户已软删除,并通过详情与结算入口读回确认不可见。
## 十、相关文档
- Issue: [#6654](https://git.1814.love:8443/wx/HL/issues/6654)
- PR: [#6665](https://git.1814.love:8443/wx/HL/pulls/6665)
- 合并提交: `65ac86e9312b22dd2fc7313049a693fdb0ccad59`
## 关联 / 联系人
- **后端负责人**: @lc
- **前端状态**: 已消费(`frontend_status: verified`)。页面「资质→合同→结算」排列现状已符(资质/合同在基本信息区、结算独立账户 Tab),DetailModal 合同表格对全 null 已 EMPTY/renderStatusTag 兜底安全;实质改动在 SupplierContractEditModal——12 业务字段全可空(去 contractName/contractType/status/startDate 必填、status 不再默认 DRAFT)、日期仅两端同填校验、expectedUpdateTime 编辑仅拿到版本才携带,changeReason 仍必填。
@@ -0,0 +1,359 @@
---
schema: "hl-changelog/v2"
ticket: "6669"
title: "供应商草稿结算信息编辑与可选字段清空"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "6678b97f"
target_release: "v2.1"
verified_at: "2026-08-29"
status_note: "#6669 与补充工单 #6676 已合并 dev-v3;最终提交 af5ea05df3d7490c97e6f4329c145a4014396bee 已由 Deploy Panel 任务 9be3a694 部署 TEST。真实 Gateway 已验证草稿结算创建、回读、修改、六个可选字段清空、整项清空、并发失败零写入和数据清理。当前状态:待前端处理。"
updated_at: "2026-08-29"
base: "dev-v3"
---
# 供应商草稿结算信息编辑与可选字段清空
供应商编辑接口新增 `initialAccounts` 完整快照。新建时登记的初始结算信息现在可在草稿编辑页读回、修改或清空;`changeReason` 和 `expectedUpdateTime` 继续必填。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---:|---|---|---|---|---|
| 1 | 编辑供应商 | PUT | `/admin/supplier/items/{supplierId}/update` | 修改请求与响应 | 新增可选 `initialAccounts` 完整快照,仅草稿态可维护 |
| 2 | 查询供应商账户列表 | GET | `/admin/supplier/items/{supplierId}/account-info/list` | 修改响应语义 | 草稿供应商可读回其 `DRAFT` 初始账户 |
| 3 | 查询收款账户详情 | GET | `/admin/supplier/bank-accounts/{accountId}/view` | 修改响应语义 | 所属供应商为草稿时可读回 `DRAFT` 账户详情 |
## 三、接口详情
### 1. 编辑供应商 `PUT /admin/supplier/items/{supplierId}/update`
**VO**: `SupplierUpdateReqVO / SupplierWriteRespVO`
#### 使用场景
在供应商草稿编辑页维护与新建供应商相同的一项初始结算信息。省略 `initialAccounts` 不修改结算信息,空数组清空,1 项执行完整替换。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
| `initialAccounts` | Body | Array | 否 | 最多 1 项 | 省略不修改;`[]` 清空;1 项完整替换 |
| `initialAccounts[].accountType` | Body | String | 项内是 | `CORPORATE` / `PERSONAL` | 账户类型 |
| `initialAccounts[].bankName` | Body | String | 项内是 | 最长 500 字符 | 开户银行 |
| `initialAccounts[].accountNo` | Body | String | 项内是 | 规范化后 8 至 32 位数字 | 收款账号 |
| `initialAccounts[].bankBranch` | Body | String/null | 否 | 最长 500 字符 | 省略或 `null` 可清空 |
| `initialAccounts[].proofFileUrls` | Body | Array/null | 否 | 最多 20 项 HTTPS 地址 | 省略、`null` 或 `[]` 可清空 |
| `initialAccounts[].settleMode` | Body | String/null | 否 | `PREPAY` / `MONTHLY` / `SINGLE` | 可清空 |
| `initialAccounts[].accountPeriod` | Body | String/null | 否 | 最长 50 字符 | 仅月结时填写,可清空 |
| `initialAccounts[].invoiceType` | Body | String/null | 否 | `SPECIAL` / `NORMAL` / `NONE` | 可清空 |
| `initialAccounts[].taxRate` | Body | String/null | 否 | 0% 至 100%,最多两位小数 | 可清空 |
| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 字符 | 审计原因,继续必填 |
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 供应商主体并发版本,继续必填 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.supplierId` | String | 供应商雪花 ID |
| `data.status` | String | 当前为 `DRAFT` |
| `data.initialAccounts` | Array | 保存后的 0 或 1 项账户摘要 |
| `data.updateTime` | String | 新的供应商并发版本 |
雪花 ID 均按字符串处理。
#### 请求示例
```json
{
"changeReason": "维护草稿结算信息",
"expectedUpdateTime": "2026-08-29 17:01:00",
"initialAccounts": [
{
"accountType": "PERSONAL",
"bankName": "示例银行",
"accountNo": "6222000012345678"
}
]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2094000000000000000",
"status": "DRAFT",
"initialAccounts": [
{
"accountId": "2094000000000000001",
"accountType": "PERSONAL",
"bankName": "示例银行",
"bankBranch": null,
"accountNo": "6222000012345678",
"settleMode": null,
"accountPeriod": null,
"invoiceType": null,
"taxRate": null
}
],
"updateTime": "2026-08-29 17:01:01"
}
}
```
#### 空数据 / 降级响应
- 省略 `initialAccounts`:不修改现有初始账户。
- 传 `initialAccounts: []`:软删除草稿初始账户,写响应返回空数组。
- 1 项中省略 6 个可选字段:`bankBranch`、`proofFileUrls`、`settleMode`、`accountPeriod`、`invoiceType`、`taxRate` 均清空;附件在读取响应中表现为空数组。
#### 错误响应
缺少审计或并发字段时失败且账户零写入:
```json
{ "code": 400, "message": "变更原因不能为空", "success": false, "data": null }
```
```json
{ "code": 400, "message": "expectedUpdateTime不能为空", "success": false, "data": null }
```
过期版本继续返回既有并发错误:
```json
{ "code": 395014, "message": "数据已被他人修改,请刷新后重试", "success": false, "data": null }
```
#### 业务边界
- `initialAccounts` 仅允许供应商为 `DRAFT` 时维护;否则返回既有状态错误 `395005`,账户零写入。
- 同账号完整替换保留原 `accountId`;账号全局唯一、事务、行锁、幂等和账户审计保持不变。
- 生效后的账户继续走独立新增与审批接口,供应商编辑不得绕过账户状态机。
### 2. 查询供应商账户列表 `GET /admin/supplier/items/{supplierId}/account-info/list`
**VO**: `SupplierAccountInfoRespVO / SupplierBankAccountRespVO`
#### 使用场景
进入供应商编辑页时加载“结算信息”table。供应商为草稿时,列表包含其初始 `DRAFT` 账户。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
无 Query 参数,无请求体。
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.supplierId` | String | 供应商雪花 ID |
| `data.bankAccounts` | Array | 当前可读账户列表 |
| `data.bankAccounts[].status` | String | 草稿初始账户为 `DRAFT` |
| `data.bankAccounts[].isDefault` | String | 草稿初始账户为 `NO` |
| `data.updateTime` | String | 供应商主体并发版本 |
#### 请求示例
```http
GET /admin/supplier/items/2094000000000000000/account-info/list
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2094000000000000000",
"bankAccounts": [
{
"accountId": "2094000000000000001",
"accountType": "PERSONAL",
"bankName": "示例银行",
"bankBranch": null,
"accountNo": "6222000012345678",
"proofFileUrls": [],
"settleMode": null,
"accountPeriod": null,
"invoiceType": null,
"taxRate": null,
"status": "DRAFT",
"isDefault": "NO"
}
],
"updateTime": "2026-08-29 17:01:01"
}
}
```
#### 空数据 / 降级响应
没有可读账户时 `data.bankAccounts` 返回空数组。可选文本字段为空时返回 `null`;已获附件读取权限且附件为空时 `proofFileUrls` 返回空数组。
#### 错误响应
```json
{ "code": 395001, "message": "供应商不存在", "success": false, "data": null }
```
#### 业务边界
- 仅当供应商当前为 `DRAFT` 时才扩展读取草稿账户;其他状态的既有可见范围不变。
- 证明附件继续受独立权限和同步敏感读取审计约束;无权时不序列化附件原值。
- 查询不推进供应商或账户版本,不产生业务写副作用。
### 3. 查询收款账户详情 `GET /admin/supplier/bank-accounts/{accountId}/view`
**VO**: `SupplierBankAccountDetailRespVO`
#### 使用场景
草稿编辑页需要查看单个初始账户完整字段时,按列表返回的字符串 `accountId` 查询详情。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `accountId` | Path | String | 是 | 正整数 ID 字符串 | 目标账户 |
无 Query 参数,无请求体。
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.accountId` | String | 账户雪花 ID |
| `data.accountType` 等账户字段 | 对应类型/null | 完整账户业务值,6 个可选字段允许为空 |
| `data.status` | String | 草稿初始账户为 `DRAFT` |
| `data.isDefault` | String | 草稿初始账户为 `NO` |
| `data.updateTime` | String | 账户当前版本 |
#### 请求示例
```http
GET /admin/supplier/bank-accounts/2094000000000000001/view
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"accountId": "2094000000000000001",
"accountType": "PERSONAL",
"bankName": "示例银行",
"bankBranch": null,
"accountNo": "6222000012345678",
"proofFileUrls": [],
"settleMode": null,
"accountPeriod": null,
"invoiceType": null,
"taxRate": null,
"status": "DRAFT",
"isDefault": "NO",
"updateTime": "2026-08-29 17:01:01"
}
}
```
#### 空数据 / 降级响应
目标账户不存在、已软删除或不在当前供应商状态允许的读取范围时,不返回部分对象;统一按不存在失败关闭。
#### 错误响应
```json
{ "code": 395001, "message": "供应商不存在", "success": false, "data": null }
```
#### 业务边界
- 草稿详情可见性由所属供应商当前状态决定,不能仅凭 `accountId` 绕过主体边界。
- 完整账号沿用当前管理端授权语义;证明附件仍需独立权限与同步审计。
- 详情查询不改变账户、供应商、审批或默认账户状态。
## 四、契约约束与正确调用方式
- 编辑页先调用账户列表接口加载结算 table,再把用户实际编辑后的 0 或 1 项完整快照放入供应商更新请求的 `initialAccounts`。
- 用户未操作结算区域时可以省略 `initialAccounts`,避免无意义改写;明确清空时必须发送空数组。
- 保存必须同时发送非空 `changeReason` 与最近读取到的供应商 `expectedUpdateTime`。
- 账户业务字段使用与新建供应商一致的字段名;`supplierId`、`accountId` 按字符串处理,禁止转为 JavaScript Number。
## 五、数据库行为
- 草稿账户完整替换会把省略的 6 个可选字段持久化为空;重新查询不再回读旧值。
- 空数组对草稿初始账户执行软删除;供应商、账户和审计仍在同一服务事务内提交或回滚。
- 本次没有新增 migration、跨 schema 写入、Redis、MQ 或配置行为。
## 六、边界行为
- `accountType`、`bankName`、`accountNo` 仍为账户项必填字段;“可清空”只适用于其余 6 个可选字段。
- `MONTHLY` 与 `accountPeriod`、开票类型与税率的既有组合校验继续生效。
- 更新完整快照时未提供的可选字段保存为空,不是保持数据库旧值。
- 写失败时供应商、账户和审计均在同一事务回滚;过期版本和非草稿状态均零写入。
## 六.6、修改前后对比
| 项目 | 修改前 | 修改后 |
|---|---|---|
| 供应商编辑请求 | 无 `initialAccounts`,不能维护新建时的初始结算信息 | 可选完整快照:省略不改、空数组清空、1 项替换 |
| 草稿账户列表/详情 | `DRAFT` 初始账户不在管理端账户读取范围 | 所属供应商为 `DRAFT` 时可读回 |
| 可选字段清空 | 实体值虽置空,但默认更新策略可能保留数据库旧值 | 6 个可选字段显式持久化为空 |
| 审计与并发字段 | `changeReason`、`expectedUpdateTime` 必填 | 继续必填 |
## 六.7、影响评估
- **是否向后兼容**:是;不发送 `initialAccounts` 的原调用方保持原更新语义。
- **前端是否需要接入**:是;编辑页结算 table 需调用账户列表并按完整快照保存。
- **状态机影响**:无;仅草稿态开放聚合维护,其他状态继续走独立账户审批。
- **撤回影响**:撤回后编辑页应停止发送 `initialAccounts`,草稿账户也不再通过账户列表/详情读回。
## 七、不影响范围
- 不修改非草稿供应商的独立账户新增、审批、默认账户和账户状态机。
- 不修改合同独立登记接口、供应商提交审批流程或合同字段可空契约。
- 不新增错误码、数据库 migration、Gateway 路由、Redis、MQ、Nacos 或配置变更。
- 不修改任何前端源码;页面 table 排列和字段消费由前端按本契约处理。
## 八、测试环境已验证
- 新建草稿携带完整初始结算信息后,列表可读回相同字段。
- 草稿编辑把完整账户改为仅保留三个必填字段后,6 个可选字段保存并重新回读为空,账户 ID 保持不变。
- 缺少 `changeReason`、缺少 `expectedUpdateTime` 和使用过期版本均失败且账户零写入。
- 发送空数组后写响应和列表均为空;测试供应商删除后详情不可读,所有可恢复测试数据已清理。
## 十、相关文档
- Issue [#6669](https://git.1814.love:8443/wx/HL/issues/6669)
- 补充 Issue [#6676](https://git.1814.love:8443/wx/HL/issues/6676)
- PR [#6674](https://git.1814.love:8443/wx/HL/pulls/6674),合并提交 `35dba63d6a92159d933b38385fee057f617bdfed`
- PR [#6677](https://git.1814.love:8443/wx/HL/pulls/6677),合并提交 `af5ea05df3d7490c97e6f4329c145a4014396bee`
## 关联 / 联系人
- **后端负责人**:@lc
- **前端负责人**:@mmg
- **当前状态**:已消费(`frontend_status: verified`)。SupplierEditModal 结算 Tab 放开为「新建 || (编辑 && DRAFT)」,草稿编辑拉账户列表回读初始账户(含 accountId,accountNo 完整值可回填),buildEditBody 仅 DRAFT 按快照比对携带 initialAccounts(未动省略/清空 `[]`/1 项整份替换保留 accountId,6 可选字段+proofFileUrls 省略即显式清空),非 DRAFT 不开放不携带。
@@ -0,0 +1,208 @@
---
schema: "hl-changelog/v2"
ticket: "6684"
title: "供应商草稿编辑变更原因可选且不写变更记录"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "646a1565"
target_release: "v2.1"
verified_at: "2026-08-30"
status_note: "PR #6686 已合并 dev-v3,合并提交 ae7226ac8e2a23f0ce79ddda1a3a892542b81996 已由 Deploy Panel 任务 da7ffb3a 精确部署 TEST。真实 Gateway 已验证 DRAFT 草稿不传 changeReason 可成功保存、详情回读状态与修改值正确、主体变更记录仍为 0,并已删除合成草稿。当前状态:待前端处理。"
updated_at: "2026-08-30"
base: "dev-v3"
---
# 🔧 供应商草稿编辑变更原因可选且不写变更记录
供应商当前状态为 `DRAFT` 时,编辑接口不再要求填写 `changeReason`,并且本次草稿编辑不会新增主体变更记录。其他状态继续要求非空变更原因并保留原审计行为。
管理端需要按详情返回的当前状态调整表单校验:草稿隐藏或取消“变更原因”必填,非草稿继续必填。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---:|---|---|---|---|---|
| 1 | 编辑供应商 | PUT | `/admin/supplier/items/{supplierId}/update` | 修改请求校验与写入行为 | `DRAFT` 可省略 `changeReason` 且不新增主体变更记录;其他状态不变 |
## 三、接口详情
### 1. 编辑供应商 `PUT /admin/supplier/items/{supplierId}/update`
**VO**: `SupplierUpdateReqVO / SupplierWriteRespVO`
#### 使用场景
在供应商编辑页继续完善尚未提交审批的草稿资料。调用方先读取供应商详情取得当前 `status` 和 `updateTime`,再按当前状态决定是否发送变更原因。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
| `changeReason` | Body | String | 条件必填 | 最长 500 字符 | 服务端当前状态为 `DRAFT` 时可省略或为空;其他状态去空白后必须非空 |
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 最近一次详情或写响应返回的并发版本 |
| `fullName`、`shortName` 等资料字段 | Body | 对应类型 | 否 | 沿用既有字段规则 | 仅发送本次需要修改的字段;示例使用 `remark` |
请求体不接收客户端自报的供应商状态;是否为草稿由服务端锁定并读取当前供应商后判定。
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | Number | 业务码;成功为 `200` |
| `success` | Boolean | 业务是否成功 |
| `data.supplierId` | String | 供应商 ID |
| `data.supplierNo` | String | 供应商业务编号 |
| `data.status` | String | 保存后的当前状态;本场景为 `DRAFT` |
| `data.onboardingStage` | String | 草稿为 `PROFILE_DRAFT` |
| `data.initialAccounts` | Array | 当前初始结算账户摘要,未登记时为空数组 |
| `data.updateTime` | String | 保存后的新并发版本 |
#### 请求示例
`DRAFT` 草稿请求体完全不发送 `changeReason`:
```json
{
"remark": "补充草稿内部备注",
"expectedUpdateTime": "2026-08-30 08:15:29"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2093855088501035010",
"supplierNo": "SUP2093855088501035010",
"status": "DRAFT",
"onboardingStage": "PROFILE_DRAFT",
"initialAccounts": [],
"updateTime": "2026-08-30 08:15:45"
}
}
```
#### 空数据 / 降级响应
- `DRAFT` 时省略 `changeReason`、传 `null` 或只传空白,不会因变更原因被拒绝。
- 省略其他可选资料字段仍表示“不修改该字段”,不自动清空既有值。
- 依赖服务异常继续按既有失败关闭语义返回,不把未完成写入伪装成成功。
#### 错误响应
非 `DRAFT` 供应商省略或传空白 `changeReason`,继续返回原业务错误且零写入:
```json
{
"code": 400,
"message": "变更原因不能为空",
"success": false,
"data": null
}
```
并发版本过期继续返回既有错误:
```json
{
"code": 395014,
"message": "数据已被他人修改,请刷新后重试",
"success": false,
"data": null
}
```
#### 业务边界
- 接口继续要求有效管理端身份、既有可信角色及 `supplier:update` 服务端权限。
- 草稿判定使用服务端锁定后读取到的持久状态,不能通过请求体伪造 `DRAFT` 绕过非草稿审计。
- `DRAFT` 成功编辑只更新草稿资料与并发版本,不新增主体变更记录。
- 非 `DRAFT` 的原因必填、主体变更记录、事务、聚合锁、幂等和乐观并发规则均不变。
- 参数、权限、状态或并发校验失败时不产生部分资料写入或变更记录。
## 四、契约约束与正确调用方式
1. 进入编辑页先调用 `GET /admin/supplier/items/{supplierId}/basic-info/view`,使用返回的 `status` 和 `updateTime`。
2. `status=DRAFT` 时可以不渲染变更原因输入框,或取消其必填校验;保存请求可完全省略 `changeReason`。
3. `status` 不是 `DRAFT` 时继续收集非空 `changeReason`,不要把草稿规则扩展到其他状态。
4. 每次成功保存后使用响应中的新 `updateTime` 作为下一次编辑的 `expectedUpdateTime`。
| 场景 | payload | 结果 |
|---|---|---|
| `DRAFT` 省略原因 | `{ "remark": "补充资料", "expectedUpdateTime": "2026-08-30 08:15:29" }` | 成功,不新增主体变更记录 |
| `DRAFT` 显式空原因 | `{ "changeReason": "", "expectedUpdateTime": "2026-08-30 08:15:29" }` | 成功,不新增主体变更记录 |
| 非 `DRAFT` 省略原因 | `{ "remark": "调整资料", "expectedUpdateTime": "2026-08-30 08:15:29" }` | `400`,变更原因不能为空 |
| 非 `DRAFT` 提供原因 | `{ "changeReason": "更新登记资料", "expectedUpdateTime": "2026-08-30 08:15:29" }` | 按既有编辑与审计规则处理 |
## 五、数据库行为
| 服务端当前状态 | 资料保存 | 主体变更记录 |
|---|---|---|
| `DRAFT` | 按既有增量编辑和并发规则保存 | 不新增本次编辑记录 |
| 非 `DRAFT` 且原因有效 | 按既有编辑规则保存 | 继续新增完整变更记录并保留原因 |
| 非 `DRAFT` 且原因为空 | 不保存 | 不新增记录 |
本次不新增数据库 migration,不改历史记录,不执行跨服务写入,也不引入 Redis、MQ 或配置写行为。
## 六、边界行为
- 未登录请求继续由 Gateway 拒绝;本次不改变认证级别。
- 无供应商写权限、供应商不存在、状态不允许或并发版本过期时继续按既有错误语义失败。
- `changeReason` 长度上限仍为 500 字符;非草稿只传空白等同未填写。
- 草稿成功编辑后,详情回读应保持 `status=DRAFT` 并返回本次修改值与新版本。
- 草稿的主体变更记录分页在本次保存前后数量保持不变。
## 六.6、修改前后对比
| 项目 | 修改前 | 修改后 |
|---|---|---|
| `DRAFT` 的 `changeReason` | 请求模型无条件必填,省略即返回“变更原因不能为空” | 可省略、为 `null` 或空白 |
| `DRAFT` 编辑审计 | 每次成功编辑都会新增主体变更记录 | 不新增主体变更记录 |
| 非 `DRAFT` 的 `changeReason` | 必填 | 继续必填 |
| 非 `DRAFT` 编辑审计 | 成功后记录完整变化与原因 | 保持不变 |
## 六.7、影响评估
- **是否破坏向后兼容**:否;原来已发送 `changeReason` 的草稿请求继续可用,非草稿契约不变。
- **前端是否必须同步上线**:是;管理端当前草稿编辑表单仍把“变更原因”显示为必填,需要按详情 `status` 取消草稿必填。
- **前端 workaround 清理点**:删除 `DRAFT` 草稿保存前对 `changeReason` 的必填拦截;不得删除非草稿校验。
- **响应与错误码影响**:成功响应字段不变;只移除草稿缺少原因时的 `400`,不新增错误码。
## 七、不影响范围
- 不修改供应商创建、注册提交、草稿删除、暂停、拉黑、归档、合同、账户或资源关系接口。
- 不修改非草稿资料编辑的变更原因与主体变更记录语义。
- 不修改 Gateway 路由、认证策略、角色或权限点。
- 不新增数据库结构、历史数据回填、Redis、MQ、Nacos 或配置变更。
- 本工单仅交付后端和接口 Changelog,不修改任何前端源码或资源。
## 八、测试环境已验证
- 合并提交 `ae7226ac8e2a23f0ce79ddda1a3a892542b81996` 已由 Deploy Panel 任务 `da7ffb3a` 精确部署到 TEST,Resource 双实例与 Nacos 注册均健康。
- 通过真实 TEST Gateway 创建合成 `DRAFT` 草稿,并用完全不含 `changeReason` 的请求修改 `remark`;返回 `code=200`、`success=true`、`status=DRAFT` 和新的 `updateTime`。
- 保存后再次查询详情,`remark` 为本次修改值且状态仍为 `DRAFT`。
- 保存前后查询该供应商的 `UPDATE` 主体变更记录均返回 `total=0`、`records=[]`。
- 验收后通过草稿删除接口清理合成数据;详情返回“供应商不存在”,按供应商编号查询列表为空。
## 十、相关文档
- Issue [#6684](https://git.1814.love:8443/wx/HL/issues/6684)
- PR [#6686](https://git.1814.love:8443/wx/HL/pulls/6686)
- 合并提交 [ae7226ac8e2a23f0ce79ddda1a3a892542b81996](https://git.1814.love:8443/wx/HL/commit/ae7226ac8e2a23f0ce79ddda1a3a892542b81996)
## 关联 / 联系人
### 联系人
- **后端负责人**:@lc
- **当前状态**:已消费(`frontend_status: verified`)。SupplierEditModal 变更原因必填改仅非 DRAFT(mainLocked)挂载,DRAFT 取消必填、留空省略 changeReason 不写变更记录;buildEditBody 按「非 DRAFT 恒携带/DRAFT 填了才携带」;「至少一个实际变化字段」判定剔除元字段后看业务字段。非 DRAFT 继续必填拦截。
@@ -0,0 +1,141 @@
---
schema: "hl-changelog/v2"
ticket: "6710"
title: "供应商联系人快照版本字段兼容"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "95e1f917"
target_release: "v2.1"
verified_at: "2026-08-30"
status_note: "PR #6712 已合并 dev-v3,合并提交 7f3f944bf361b7ae41424c43b29262fb634fe791 已精确部署 TEST。后端后改判「当前前端已回传 updateTime 无需修改代码」,但前端实证:修复前 cleanContactRow 对已有联系人仅发 contactId 不带任何版本,必触发 400「联系人快照项不合法」;本次前端补回传条目级版本(commit 95e1f917),修复后方可正常保存。前端按实际修复记 verified。"
updated_at: "2026-08-30"
base: "dev-v3"
---
# 供应商联系人快照版本字段兼容
后端现已兼容详情联系人字段 `contacts[].updateTime`。当前前端已经回传该字段,无需修改代码;本记录仅保留历史审计,不构成前端任务。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---:|---|---|---|---|---|
| 1 | 编辑供应商 | PUT | `/admin/supplier/items/{supplierId}/update` | 请求兼容 | 已有联系人可用 `updateTime` 作为条目级并发版本 |
## 三、接口详情
### 1. 编辑供应商 `PUT /admin/supplier/items/{supplierId}/update`
**VO**: `SupplierUpdateReqVO / SupplierContactMergeReqVO / SupplierWriteRespVO`
#### 使用场景
保留已有联系人并在一次编辑中新增多个联系人。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 主体并发版本 |
| `contacts` | Body | Array | 否 | 最多 100 项 | 联系人完整快照 |
| `contacts[].contactId` | Body | String | 已有项是 | 正整数 | 为空表示新增 |
| `contacts[].expectedUpdateTime` / `updateTime` | Body | String | 已有项二选一 | 时间格式同上 | 条目级并发版本 |
姓名和联系电话允许与已有联系人或同批新增联系人重复。新增项仍按既有规则提供姓名、电话和角色,不传 ID 与版本。
#### 出参 `Result<SupplierWriteRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.updateTime` | String | 保存后的主体并发版本 |
#### 请求示例
```json
{"expectedUpdateTime":"2026-08-30 10:04:18","contacts":[{"contactId":"2093000000000000001","contactName":"张三","contactPhone":"18501941408","contactRole":"contentBus","updateTime":"2026-08-30 10:04:19"},{"contactName":"张三","contactPhone":"18501941408","contactRole":"contentMoney"}]}
```
#### 响应示例
```json
{"code":200,"message":"成功","success":true,"data":{"updateTime":"2026-08-30 10:04:29"}}
```
#### 空数据 / 降级响应
- 省略 `contacts` 或传 `null`:联系人不变。
- 依赖异常:沿用既有失败关闭语义,不产生部分写入。
#### 错误响应
版本过期时:
```json
{"code":395014,"message":"数据已被他人修改,请刷新后重试","success":false,"data":null}
```
已有联系人未携带任一版本字段时,仍返回 `400`、`联系人快照项不合法`。
#### 业务边界
- 已有联系人仍须提供 `contactId` 和有效版本;本次没有取消并发校验。
- 姓名或联系电话重复本身不是错误条件。
- 权限、状态、默认联系人及角色字典规则均不变。
## 四、契约约束与正确调用方式
1. 主体详情 `updateTime` 放入请求顶层 `expectedUpdateTime`。
2. 已有联系人的 `updateTime` 可原样回传;新增联系人不传 ID 和版本。
3. 保存后重新读取详情,使用最新版本继续编辑。
## 五、数据库行为
- 不新增数据库结构或姓名、电话唯一约束。
- 校验失败时联系人和主体均不写入。
## 六、边界行为
- 未登录、无权限、供应商不存在或版本过期时沿用既有错误。
- `contacts=[]` 沿用既有清空语义。
- 业务失败可能仍为 HTTP 200,调用方同时判断 `code` 和 `success`。
## 六.6、修改前后对比
| 项目 | 修改前 | 修改后 |
|---|---|---|
| 联系人版本字段 | 仅识别 `expectedUpdateTime` | 同时识别 `expectedUpdateTime`、`updateTime` |
| 姓名、电话重复 | 允许 | 继续允许 |
## 六.7、影响评估
- **是否破坏向后兼容**:否。
- **前端是否必须同步上线**:否,当前前端已回传 `updateTime`。
- **前端 workaround 清理点**:无。
- **响应与错误码影响**:无。
## 七、不影响范围
- 不修改其他供应商接口、权限或状态机。
- 不修改数据库结构、配置、Redis 或 MQ。
- 不修改任何前端代码。
## 八、测试环境已验证
- 详情 `updateTime` 原样回传后,可保留已有联系人并新增多个联系人。
- 多个联系人姓名、电话重复时保存成功。
- 过期版本返回 `395014` 且零写入;验收草稿已清理。
## 十、相关文档
- [Issue #6710](https://git.1814.love:8443/wx/HL/issues/6710)
- [PR #6712](https://git.1814.love:8443/wx/HL/pulls/6712)
## 关联 / 联系人
- **后端负责人**:@lc
- **当前状态**:已消费(`frontend_status: verified`)。SupplierEditModal fillForm 回填详情联系人条目级 updateTime、createContactRow 新增行置 null;cleanContactRow 已有项(contactId 存在)加发 expectedUpdateTime=row.updateTime 原样回传,新增行仍不发版本;快照比对两侧同经 cleanContactRow,未改动不误携带。spec +3 例累计 71 例绿,commit 95e1f917 已推 v2.1。注:后端后改判「无需前端处理」系基于修复后前端,修复前旧前端仅发 contactId 必 400,本修复为必要。
@@ -0,0 +1,805 @@
---
schema: "hl-changelog/v2"
ticket: "6717"
title: "车队关联供应商并展示供应商全名"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "4faf23bb"
target_release: "v2.1"
verified_at: "2026-08-30"
status_note: "PR #6753 已合并 dev-v3(merge commit 447b92f5);Deploy Panel 任务 2fe21f84(hl-fleet-service,2026-08-30 17:20)与 f29a293f(hl-resource-service,2026-08-30 17:22)均已部署测试服成功。真实 TEST 身份已验证 5 条负向链路全绿 + 列表字段结构正确,测试数据已清理。车队列表操作列的「供应商」按钮需下线,改为在车队新增/编辑弹窗选择供应商。前端已消费(frontend_status: verified):车队列表下线操作列「供应商」入口并新增供应商列透传 supplierName 快照;新增/编辑弹窗内嵌供应商 NSelect(远程搜索 FLEET+ACTIVE、可清空、编辑回填并补快照选项防显原始 ID),提交体携带 supplierId 字符串;601108/601109/601110/601111 按 bizCode 精确提示,新建不选供应商落 DISABLED 并提示。前端 commit 4faf23bb 已推 v2.1。"
updated_at: "2026-08-30"
base: "dev-v3"
---
# 车队管理:车队关联供应商并展示供应商全名
> **存放目录**:
> - 二期(v3,`order-v3` 标签的工单)→ `changelogs-v2/2026-08/`
>
> **服务**: hl-fleet-service(端口 8094)/ hl-resource-service(内部依赖)
> **PR**: #6753
> **Issue**: #6717
> **日期**: 2026-08-30
> **影响范围**: 管理后台「车辆管理 - 车队管理」列表/新增/编辑/启用
车队管理新增供应商归属字段(可空),供应商只能随车队新增/编辑一起提交,不再提供单独配置入口。
---
## ⚠️ 关键变化
- 车队列表操作列的「供应商」按钮(`views/fleet/teams/index.vue` 弹窗 `SupplierResourceRelModal`)**需要下线**;供应商关联改为在车队新增/编辑弹窗里选择。
- 「不选供应商不能启动,只能停用」:未关联供应商的车队调用启用接口会被 601108 拦截;新建时不选供应商则车队落停用态。
- 已有订单(名下车辆存在非取消派单)的车队,**不允许更换或清除供应商**;允许首次绑定。
---
## 一、背景
车队此前没有供应商归属字段;供应商域(`supplier_main`,类型字典 `supplier_type`)已支持车队类型供应商(FLEET),但车队主数据与供应商档案之间未建立关联。工单 #6717 要求车队管理列表展示供应商全名,并在车队新增/编辑时明确供应商归属。
**最终口径(2026-08-30 用户确认,覆盖工单原文「必选」语义)**:
1. 供应商字段**不必填**(可选);不选 → 不能启用,只能停用。
2. 车队已有订单时,不允许**更换**或**清除**供应商(含已有订单的历史车队豁免存量,不动存量数据)。
3. 去掉单独配置供应商接口入口:前端车队列表的「供应商」按钮下线;供应商关联只随车队新增/编辑一起提交。
4. 供应商全名以**写时快照**存于 `fleet_team.supplier_name`;列表/详情零 Feign 读,供应商改名后陈旧、重新编辑可刷新。
5. 供应商资格校验走跨服务 Feign(fleet → resource `GET /internal/supplier/{supplierId}/fleet-eligibility`):主体存在 + 状态 ACTIVE + 类型关联含 FLEET。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 分页查询车队 | GET | `/admin/fleet/teams/page` | 响应新增字段 | `records[]` 增加 `supplierId`(String/null) + `supplierName`(String/null) |
| 2 | 查询车队详情 | GET | `/admin/fleet/teams/{fleetTeamId}` | 响应新增字段 | 同 #1 |
| 3 | 新增车队 | POST | `/admin/fleet/teams` | 请求体新增可选字段 + 响应新增字段 | 请求体加 `supplierId`(Long 字符串/可空);响应同 #2 |
| 4 | 编辑车队 | PUT | `/admin/fleet/teams/{fleetTeamId}` | 请求体新增可选字段 + 响应新增字段 + 失败语义 | 请求体加 `supplierId`;更换/清除时已有订单被 601111 拦截;清除且当前 ACTIVE 会强制落 DISABLED |
| 5 | 启用车队 | POST | `/admin/fleet/teams/{fleetTeamId}/enable` | 失败语义新增 | `supplierId==null` 时抛 601108「车队未关联供应商,不能启用」 |
---
## 三、接口详情
### 1. 分页查询车队 `GET /admin/fleet/teams/page`
**VO**: `FleetTeamPageReqVO` / `Result<PageResult<FleetTeamRespVO>>`
#### 使用场景
管理后台「车辆管理 - 车队管理」列表页加载时调用。供应商列新增展示。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `page` | Query | Integer | 否 | ≥1,默认 1 | 页码 |
| `pageSize` | Query | Integer | 否 | 1..100,默认 10 | 每页大小 |
| `keyword` | Query | String | 否 | ≤64 | 车队名称/负责人姓名模糊搜索 |
| `teamType` | Query | String | 否 | `SELF_OPERATED`/`COOPERATIVE` | 车队类型筛选 |
| `status` | Query | String | 否 | `ACTIVE`/`DISABLED` | 状态筛选 |
| `settleType` | Query | String | 否 | `cash`/`sign`/`company` | 付款方式筛选 |
#### 出参 `Result<PageResult<FleetTeamRespVO>>`
`records[]` 每一项 `FleetTeamRespVO` 字段(与改前相比只多两列,其余字段语义不变):
| 字段 | 类型 | 说明 |
|------|------|------|
| `fleetTeamId` | String | 车队 ID(雪花序列化为字符串,禁止转 Number) |
| `teamCode` | String | 内部稳定编码(`ft_xxx`) |
| `teamName` | String | 车队名称 |
| `teamType` | String | `SELF_OPERATED`/`COOPERATIVE` |
| `leaderName` | String | 负责人姓名 |
| `leaderPhone` | String | 负责人电话(**分页脱敏**:`138****8000`) |
| `settleType` | String | `cash`/`sign`/`company` |
| `status` | String | `ACTIVE`/`DISABLED` |
| `sortOrder` | Integer | 排序值 |
| `remark` | String | 备注 |
| `vehicleCount` | Integer | 名下车辆总数(含停用) |
| `activeVehicleCount` | Integer | 名下在役车辆数 |
| **`supplierId`** | **String/null** | **关联供应商 ID(雪花字符串;未关联为 null)** |
| **`supplierName`** | **String/null** | **关联供应商全称快照(写时同步,未关联为 null)** |
| `createTime` | String | 创建时间 `yyyy-MM-dd HH:mm:ss` |
| `updateTime` | String | 最后更新时间 `yyyy-MM-dd HH:mm:ss` |
#### 请求示例
```http
GET /admin/fleet/teams/page?page=1&pageSize=10&status=ACTIVE
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"total": 2,
"page": 1,
"pageSize": 10,
"records": [
{
"fleetTeamId": "2102345678901234567",
"teamCode": "ft_2x9k3m",
"teamName": "合作车队A",
"teamType": "COOPERATIVE",
"leaderName": "测试负责人甲",
"leaderPhone": "138****0001",
"settleType": "cash",
"status": "ACTIVE",
"sortOrder": 10,
"remark": "由 fleet_attribution/历史业务数据迁移",
"vehicleCount": 12,
"activeVehicleCount": 9,
"supplierId": "2091381911266967553",
"supplierName": "内蒙古呼籁旅游服务有限公司",
"createTime": "2026-06-01 10:00:00",
"updateTime": "2026-08-30 15:30:00"
},
{
"fleetTeamId": "2102345678901234568",
"teamCode": "ft_2x9k3n",
"teamName": "自有车队",
"teamType": "SELF_OPERATED",
"leaderName": "自有负责人",
"leaderPhone": "139****0002",
"settleType": "company",
"status": "DISABLED",
"sortOrder": 20,
"remark": "",
"vehicleCount": 0,
"activeVehicleCount": 0,
"supplierId": null,
"supplierName": null,
"createTime": "2026-08-01 09:00:00",
"updateTime": "2026-08-30 15:30:00"
}
]
}
}
```
#### 空数据 / 降级响应
无车队时 `records=[]` `total=0`,`code=200`。供应商字段未关联时为 `null`(不是空串),前端按未关联渲染。
#### 错误响应
分页查询无业务错误分支。网关/框架错误:
```json
{ "code": 401, "message": "未登录", "success": false, "data": null }
```
#### 业务边界
- 只读查询;返回的 `supplierId`/`supplierName` 来自 `fleet_team.supplier_id/supplier_name` 列快照,不做实时跨服务取数。
- 供应商改名后列表展示旧名(快照口径);如需最新名称,让用户重新编辑车队保存触发刷新。
- 车队停用车队(`status=DISABLED`)也可被列表查到(不带状态筛选时默认返回全部状态)。
---
### 2. 查询车队详情 `GET /admin/fleet/teams/{fleetTeamId}`
**VO**: `Result<FleetTeamRespVO>`
#### 使用场景
管理后台编辑车队弹窗初始化时调用,回填 `supplierId`/`supplierName` 到供应商选择器。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `fleetTeamId` | Path | String(Long) | 是 | 正整数 ID 字符串 | 目标车队 ID(雪花,不得转 Number) |
#### 出参 `Result<FleetTeamRespVO>`
字段与 #1 `records[]` 项结构一致(含新增 `supplierId`/`supplierName`):
| 字段 | 类型 | 说明 |
|------|------|------|
| `fleetTeamId` | String | 车队 ID(雪花序列化为字符串,禁止转 Number) |
| `teamCode` | String | 内部稳定编码(`ft_xxx`) |
| `teamName` | String | 车队名称 |
| `teamType` | String | `SELF_OPERATED`/`COOPERATIVE` |
| `leaderName` | String | 负责人姓名 |
| `leaderPhone` | String | 负责人电话(**详情返回原值**,编辑场景回填用) |
| `settleType` | String | `cash`/`sign`/`company` |
| `status` | String | `ACTIVE`/`DISABLED` |
| `sortOrder` | Integer | 排序值 |
| `remark` | String | 备注 |
| `vehicleCount` | Integer | 名下车辆总数(含停用) |
| `activeVehicleCount` | Integer | 名下在役车辆数 |
| `supplierId` | String/null | 关联供应商 ID(雪花字符串;未关联为 null) |
| `supplierName` | String/null | 关联供应商全称快照(写时同步) |
| `createTime` | String | 创建时间 `yyyy-MM-dd HH:mm:ss` |
| `updateTime` | String | 最后更新时间 `yyyy-MM-dd HH:mm:ss` |
#### 请求示例
```http
GET /admin/fleet/teams/2102345678901234567
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"fleetTeamId": "2102345678901234567",
"teamCode": "ft_2x9k3m",
"teamName": "合作车队A",
"teamType": "COOPERATIVE",
"leaderName": "测试负责人甲",
"leaderPhone": "13800000001",
"settleType": "cash",
"status": "ACTIVE",
"sortOrder": 10,
"remark": "由 fleet_attribution/历史业务数据迁移",
"vehicleCount": 12,
"activeVehicleCount": 9,
"supplierId": "2091381911266967553",
"supplierName": "内蒙古呼籁旅游服务有限公司",
"createTime": "2026-06-01 10:00:00",
"updateTime": "2026-08-30 15:30:00"
}
}
```
#### 空数据 / 降级响应
车队不存在或已软删除(业务错误,见下方错误响应);无空数据分支。
#### 错误响应
车队不存在或已软删除:
```json
{ "code": 601100, "message": "车队不存在", "success": false, "data": null }
```
未登录(网关拦截):
```json
{ "code": 401, "message": "未登录", "success": false, "data": null }
```
#### 业务边界
- 详情接口返回 `leaderPhone` 为原值(编辑场景需要回填),分页接口脱敏。
- `supplierId` 必须按字符串处理,禁止转 JavaScript Number。
---
### 3. 新增车队 `POST /admin/fleet/teams`
**VO**: `FleetTeamSaveReqVO` / `Result<FleetTeamRespVO>`
#### 使用场景
管理后台「车辆管理 - 车队管理」点击「新增车队」,在表单里可选填供应商。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `teamName` | Body | String | 是 | ≤64,唯一 | 车队名称 |
| `teamType` | Body | String | 是 | `SELF_OPERATED`/`COOPERATIVE` | 车队类型 |
| `leaderName` | Body | String | 是 | ≤64 | 负责人姓名 |
| `leaderPhone` | Body | String | 是 | 电话格式正则 | 负责人电话 |
| `settleType` | Body | String | 是 | `cash`/`sign`/`company` | 付款方式 |
| `sortOrder` | Body | Integer | 是 | ≥0 | 排序 |
| `remark` | Body | String | 否 | ≤256 | 备注 |
| **`supplierId`** | Body | **String(Long)/null** | **否** | **`@Positive`** | **关联供应商 ID(雪花字符串;不填=不选供应商,新建车队落 DISABLED)** |
#### 出参 `Result<FleetTeamRespVO>`
响应结构同 #2;`supplierId`/`supplierName` 按请求回填(选了供应商且校验通过则写入快照,否则为 null)。
| 字段 | 类型 | 说明 |
|------|------|------|
| `fleetTeamId` | String | 新车队 ID(雪花序列化为字符串) |
| `supplierId` | String/null | 按请求回填(选了且校验通过) |
| `supplierName` | String/null | 写时同步的供应商全称快照;未选为 null |
| `status` | String | 选供应商=`ACTIVE`;不选=`DISABLED` |
| `createTime` | String | 创建时间 `yyyy-MM-dd HH:mm:ss` |
| `updateTime` | String | 创建时间(与 createTime 相同) |
| (其余字段) | - | 与详情 #2 同构 |
#### 请求示例
```http
POST /admin/fleet/teams
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"teamName": "新合作车队B",
"teamType": "COOPERATIVE",
"leaderName": "王队长",
"leaderPhone": "13800138000",
"settleType": "sign",
"sortOrder": 20,
"remark": "新签约车队",
"supplierId": "2091381911266967553"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"fleetTeamId": "2103456789012345678",
"teamCode": "ft_3a1b2c",
"teamName": "新合作车队B",
"teamType": "COOPERATIVE",
"leaderName": "王队长",
"leaderPhone": "13800138000",
"settleType": "sign",
"status": "ACTIVE",
"sortOrder": 20,
"remark": "新签约车队",
"vehicleCount": 0,
"activeVehicleCount": 0,
"supplierId": "2091381911266967553",
"supplierName": "内蒙古呼籁旅游服务有限公司",
"createTime": "2026-08-30 16:00:00",
"updateTime": "2026-08-30 16:00:00"
}
}
```
#### 空数据 / 降级响应
未选供应商创建(落 DISABLED,供应商字段为 null):
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"fleetTeamId": "2103456789012345679",
"teamCode": "ft_3a1b2d",
"teamName": "临时车队",
"teamType": "COOPERATIVE",
"leaderName": "临时负责人",
"leaderPhone": "13900139000",
"settleType": "cash",
"status": "DISABLED",
"sortOrder": 30,
"remark": "",
"vehicleCount": 0,
"activeVehicleCount": 0,
"supplierId": null,
"supplierName": null,
"createTime": "2026-08-30 16:00:00",
"updateTime": "2026-08-30 16:00:00"
}
}
```
#### 错误响应
供应商不存在/未生效/不含车队类型(不写库):
```json
{ "code": 601109, "message": "供应商不存在、未生效或不包含车队类型", "success": false, "data": null }
```
供应商校验依赖故障(Feign 不可用,不写库):
```json
{ "code": 601110, "message": "暂时无法校验供应商,请稍后重试", "success": false, "data": null }
```
车队名称重复(`uk_fleet_team_name` 唯一索引兜底):
```json
{ "code": 601101, "message": "车队名称已存在", "success": false, "data": null }
```
Bean Validation 校验失败:
```json
{ "code": 400, "message": "供应商ID必须为正数", "success": false, "data": null }
```
#### 业务边界
- 幂等:同一 teamName + 请求摘要 10 秒窗口内重复提交只生效一次(@Idempotent key=`fleet:team:create:{teamName}`)。
- 分布式锁:同 teamName 创建串行化(@Lock4j)。
- 选供应商时先 Feign 校验(事务外),通过后写库(事务内),Feign 失败/不合格一律不写库(失败关闭)。
- 不写供应商时新建车队 status=DISABLED,需后续编辑绑定供应商后才能启用。
---
### 4. 编辑车队 `PUT /admin/fleet/teams/{fleetTeamId}`
**VO**: `FleetTeamSaveReqVO` / `Result<FleetTeamRespVO>`
#### 使用场景
管理后台编辑车队弹窗提交。可修改基础字段 + 供应商;**不允许修改 status**(启停用走独立接口)。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `fleetTeamId` | Path | String(Long) | 是 | 正整数 ID 字符串 | 目标车队 ID(不得转 Number) |
| (其余字段) | Body | String | 同新增 | 同新增 | 全字段提交(SaveReqVO 整体语义) |
| `supplierId` | Body | String(Long)/null | 否 | `@Positive` | 供应商 ID;更换/清除受订单围栏 |
#### 出参 `Result<FleetTeamRespVO>`
同 #2;`supplierId`/`supplierName` 反映最新写入值。
| 字段 | 类型 | 说明 |
|------|------|------|
| `fleetTeamId` | String | 车队 ID(路径回显) |
| `supplierId` | String/null | 最新写入值(更换/清除后刷新) |
| `supplierName` | String/null | 最新写入快照(更换时取新供应商 full_name;清除时为 null) |
| `status` | String | 清除供应商且当前 ACTIVE 时强制落 `DISABLED`;其余场景不变 |
| `updateTime` | String | 本次写入时间(秒级严格递增) |
| (其余字段) | - | 与详情 #2 同构 |
#### 请求示例
```http
PUT /admin/fleet/teams/2102345678901234567
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"teamName": "合作车队A",
"teamType": "COOPERATIVE",
"leaderName": "测试负责人甲",
"leaderPhone": "13800000001",
"settleType": "cash",
"sortOrder": 10,
"remark": "",
"supplierId": "2091381911266967553"
}
```
#### 响应示例
成功(更换供应商且无订单):
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"fleetTeamId": "2102345678901234567",
"teamCode": "ft_2x9k3m",
"teamName": "合作车队A",
"teamType": "COOPERATIVE",
"leaderName": "测试负责人甲",
"leaderPhone": "13800000001",
"settleType": "cash",
"status": "ACTIVE",
"sortOrder": 10,
"remark": "",
"vehicleCount": 12,
"activeVehicleCount": 9,
"supplierId": "2091381911266967553",
"supplierName": "内蒙古呼籁旅游服务有限公司",
"createTime": "2026-06-01 10:00:00",
"updateTime": "2026-08-30 16:30:00"
}
}
```
#### 空数据 / 降级响应
清除供应商(原值 → null)且车队无订单:快照清空,若当前 ACTIVE 强制落 DISABLED(保持不变量:ACTIVE ⇒ 已绑供应商)。
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"fleetTeamId": "2102345678901234567",
"teamCode": "ft_2x9k3m",
"teamName": "合作车队A",
"teamType": "COOPERATIVE",
"status": "DISABLED",
"supplierId": null,
"supplierName": null,
"updateTime": "2026-08-30 16:30:00"
}
}
```
#### 错误响应
车队已有订单时更换/清除供应商(含从原供应商换到新供应商、从原供应商清为空):
```json
{ "code": 601111, "message": "车队已关联订单,不能更换供应商", "success": false, "data": null }
```
供应商校验失败(同新增 #3 的 601109/601110);车队不存在:
```json
{ "code": 601100, "message": "车队不存在", "success": false, "data": null }
```
#### 业务边界
- 幂等:同 fleetTeamId + 请求摘要(含 supplierId 参与摘要)10 秒窗口内重复提交只生效一次(@Idempotent key=`fleet:team:update:{fleetTeamId}:{sha256}`)。
- 分布式锁:同 fleetTeamId 编辑串行化(@Lock4j)。
- 更换供应商校验顺序:订单围栏(601111)→ Feign 资格校验(601109/601110)→ 写库。
- 供应商不变时(含 null→null)不触发订单围栏,也不调 Feign。
- 首次绑定(null → 新值)即使已有订单也允许。
- `status` 不在 SaveReqVO,本接口不修改启停状态;清除供应商且当前 ACTIVE 时强制落 DISABLED 是唯一例外。
---
### 5. 启用车队 `POST /admin/fleet/teams/{fleetTeamId}/enable`
**VO**: `Result<Void>`
#### 使用场景
管理后台对已停用车队点击「启用」。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `fleetTeamId` | Path | String(Long) | 是 | 正整数 ID 字符串 | 目标车队 ID |
#### 出参 `Result<Void>`
成功时 `data=null`:
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 恒为 `200` 表示成功 |
| `data` | null | 启用接口无返回体 |
#### 空数据 / 降级响应
启用成功恒返回 `{"code":200,"success":true,"data":null}`;无空数据分支。已是启用态时幂等放行不重复写(返回同样结构)。
#### 请求示例
```http
POST /admin/fleet/teams/2102345678901234567/enable
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{ "code": 200, "message": "成功", "success": true, "data": null }
```
#### 错误响应
未关联供应商(新增语义):
```json
{ "code": 601108, "message": "车队未关联供应商,不能启用", "success": false, "data": null }
```
已是启用态(幂等放行,不重复写):
```json
{ "code": 200, "message": "成功", "success": true, "data": null }
```
车队不存在:`601100`。
#### 业务边界
- 幂等:同 fleetTeamId 重复调用只生效一次(@Idempotent key=`fleet:team:enable:{fleetTeamId}`)。
- 分布式锁:同 fleetTeamId 启停用串行化(@Lock4j key=`fleet:team:status:{fleetTeamId}`)。
- 存量迁移车队(own/coopA/coopB)当前 ACTIVE 且无供应商:enable 若已被置 DISABLED 后会被 601108 拦截,需先编辑绑定供应商再启用。
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | 调用 / 结果 |
|---|---|
| ✅ 新增时不选供应商 | `{ "supplierId": null }`(或不传)→ 车队落 DISABLED,待绑定后启用 |
| ✅ 新增时选供应商 | `{ "supplierId": "2091381911266967553" }` → Feign 校验通过后落 ACTIVE |
| ✅ 编辑时供应商不变 | 原 supplierId 原样传回(或省略由后端按原值处理?——必须原样传,SaveReqVO 整份语义) |
| ✅ 首次绑定 | 原 supplierId=null,新 supplierId=有效 FLEET 供应商 → 允许(不受订单围栏) |
| ✅ 供应商改名后刷新快照 | 重新编辑车队并保存(supplierId 原样),快照重新取最新 full_name |
| ❌ 供应商 ID 转 Number | 雪花精度丢失;必须按字符串传输 |
| ❌ 已有订单车队换供应商 | 601111,不写库 |
| ❌ 已有订单车队清供应商 | 601111,不写库 |
| ❌ 清除供应商后期望仍 ACTIVE | 无订单时快照清空+强制 DISABLED;前端收到 200 但 status=DISABLED 属于预期行为 |
### 关键提示(当前 TEST 构建)
- `supplierId`/`supplierName` 在所有响应中均按字符串序列化(`@JsonSerialize(ToStringSerializer)`);**禁止**前端用 `Number()`/`parseInt()`/一元 `+` 转换。
- 供应商全名快照为**写时取数**:`supplier_main.full_name` 改名后列表展示旧值,重新编辑车队保存触发刷新。
- 供应商选择器数据源:可复用 `GET /admin/supplier/items/list?typeCode=FLEET&status=ACTIVE`(mmg 自查前端是否已有该接口封装;无则后端再补)。
---
## 五、数据库行为
- **写操作只影响 `fleet_team` 一行**(新增 insert / 编辑 update / 启停用 update),不跨服务写 `supplier_resource_rel` 或 `supplier_main`。
- 快照列:`supplier_id`(关联 ID)+ `supplier_name`(全称快照)同列写入;清除时同列置 NULL。
- 幂等窗口内重复请求只写一次;锁键串行化同车队写。
- 迁移:`V20260830_001__add_supplier_to_fleet_team.sql` 对 `fleet_team` 加 `supplier_id BIGINT NULL` + `supplier_name VARCHAR(500) NULL` + 索引 `idx_fleet_team_supplier(supplier_id)`;存量车队两列均为 NULL。
- 不动 `supplier_resource_rel`(车队供应商不走资源关系表;关系表仅用于九大资源模块)。
---
## 六、边界行为
- 未登录/登录失效:业务码 `401`(网关拦截)。
- 车队不存在:`601100`。
- 车队名称重复:`601101`(含 DuplicateKeyException 翻译)。
- 车队已停用仍选该车:`601102`(既有口径,本工单不改)。
- 车队下有在役车辆时禁止停用:`601103`(既有口径)。
- 车队已关联车辆时禁止改自有/合作类型:`601104`(既有口径)。
- 车队名下仍有车辆/司机时禁止删除:`601107`(既有口径)。
- 车队未关联供应商禁止启用:`601108`(新增)。
- 供应商不存在/未生效/不含车队类型:`601109`(新增)。
- 供应商校验依赖故障:`601110`(新增,失败关闭不写库)。
- 车队已有订单禁止更换/清除供应商:`601111`(新增)。
- Bean Validation 校验失败:`400`。
- 跨服务 Feign 不可用:写接口一律失败关闭(601110);读接口(列表/详情)读快照列,不受影响。
---
## 六.5、枚举 / 数据字典
### `status`(车队启停状态)
**所属字段**: `FleetTeamRespVO.status` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `ACTIVE` | 启用 | 可被车辆选择;**前提:已关联供应商** |
| `DISABLED` | 停用 | 不可被车辆选择;新建未选供应商时默认落此态 |
### `teamType`(车队类型)
**所属字段**: `FleetTeamSaveReqVO.teamType` / `FleetTeamRespVO.teamType` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `SELF_OPERATED` | 自有 | 自有车队 |
| `COOPERATIVE` | 合作 | 合作车队 |
### `settleType`(付款方式,字典 `resource_settle_type`)
**所属字段**: `FleetTeamSaveReqVO.settleType` / `FleetTeamRespVO.settleType` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `cash` | 现付 | - |
| `sign` | 挂账签单 | - |
| `company` | 公司月结 | - |
---
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `FleetTeamSaveReqVO.supplierId` | 无 | 新增可选字段,@Positive,参与幂等摘要 |
| `FleetTeamRespVO.supplierId` | 无 | 新增,String/null(ToStringSerializer) |
| `FleetTeamRespVO.supplierName` | 无 | 新增,String/null(快照) |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 新建车队默认状态 | ACTIVE | 选供应商=ACTIVE;不选=DISABLED |
| 启用校验 | 只查当前状态 | 增加 supplierId==null → 601108 |
| 编辑车队供应商 | 无此字段 | 有订单禁换/禁清(601111);清除+ACTIVE→强制 DISABLED |
| 供应商配置入口 | 前端有独立「供应商」按钮(调通用资源关系接口) | 下线;改为车队新增/编辑内嵌选择 |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 否(新增可选字段;响应只多两列,前端旧版忽略即兼容)
- **前端是否必须同步上线**: 是(车队列表「供应商」按钮需下线,否则用户仍能从旧入口调通用接口——但通用接口对车队模块本工单起后端侧保留不拦,是前端入口下线)
- **前端 workaround 清理点**: 车队列表的「供应商」操作入口(`views/fleet/teams/index.vue` 中 `supplierRelaShow`/`supplierRelaRow`/`openSupplierRelation` 相关代码)整体删除
---
## 七、不影响范围
- **仅影响**: 管理后台「车辆管理 - 车队管理」列表/新增/编辑/启用 4 个端点 + 详情 1 个端点。
- **零影响**:
- 车辆档案(`fleet_vehicle`)/ 司机档案(`fleet_driver`)/ 派单(`fleet_assignment`)等车队下游域——它们继续经 `FleetTeamService.resolveForVehicle` 解析车队,供应商字段不影响车辆选车队。
- 供应商域九大资源模块(景区/餐厅/备品/组合/游玩项目/酒店/服务/额外成本/服务人员/车辆)的独立供应商关系维护(`/admin/supplier/resource-relations/{module}/{id}/update` 等通用接口保留不动)。
- 订单/对账/看板读路径(只读 fleet_team 既有字段,新增两列不影响)。
- Gateway 路由(`/admin/fleet/**` 通配已覆盖;`/internal/**` 不走网关)。
- Redis/MQ(无新增 key/消息)。
---
## 八、测试环境已验证
真实 TEST 环境实测(2026-08-30,网关 `https://api.test.1814.love:9443`,admin token 走 `/admin/auth/login`):
```
POST /admin/fleet/teams 无供应商新建 → 200, status=DISABLED, supplierId=null, supplierName=null ✓
POST /admin/fleet/teams/{id}/enable 无供应商启用 → 601108「车队未关联供应商,不能启用」 ✓
PUT /admin/fleet/teams/{id} 绑定 DRAFT 供应商 → 601109 ✓
PUT /admin/fleet/teams/{id} 绑定不存在供应商 → 601109 ✓
PUT /admin/fleet/teams/{id} 绑定非 FLEET 类型 ACTIVE 供应商(RESTAURANT) → 601109 ✓
GET /admin/fleet/teams/page → records[] 含 supplierId/supplierName 键(未关联为 null) ✓
```
- 部署:Deploy Panel 任务 `2fe21f84`(hl-fleet-service,2026-08-30 17:20 success)+ `f29a293f`(hl-resource-service,2026-08-30 17:22 success),预期/实际提交均为 `447b92f5143e9ca4381d590d4f868d518cf66e0a`(dev-v3 HEAD)。
- 正向链路(绑定合格 FLEET ACTIVE 供应商 → ACTIVE + 快照写 supplierName):测试服当前无 ACTIVE 状态的 FLEET 类型供应商(供应商审批链要求必备资质,`supplier_type_qualification_rule` 规则表为空,无法造出合格供应商);该路径本地单测已覆盖(`FleetTeamServiceTest#create_withEligibleSupplier_activeAndSnapshot` 等 24 用例全绿),建议 mmg 联调时在真实数据上补验。
- 有订单换供应商(601111)链路:测试服车队均无订单派单可安全构造验证数据,本地单测覆盖(`FleetTeamServiceTest#update_changeSupplierWithOrders_rejected` / `update_clearSupplierWithOrders_rejected`)。
- 本地自动化:fleet 全量 3883 项 0 失败 0 错误(含 FleetRedLineArchTest 13 项门禁);resource 全量 0 失败 0 错误;spotless:check 绿。
- 数据清理:测试车队(352385968453586944)与测试供应商(2093993745543299074)均已删除;未触碰同事真实订单;admin token 已登出。
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #6753 | #6717 | 车队关联供应商并展示供应商全名(本次) | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#6717](https://git.1814.love:8443/wx/HL/issues/6717)
- 关联 PR: [wx/HL#6753](https://git.1814.love:8443/wx/HL/pulls/6753)
- 任务设计文档: `docs/tasks/6717-fleet-team-supplier.md`(worktree `D:\work2\HL-v3-0830-fleetsup`)
## 撤回
1. 管理端先恢复车队列表「供应商」操作入口(参照改前版本),保持线上可用。
2. 从最新 `dev-v3` 建独立回退分支,回退 PR #6753 的合并 commit(`72c9018ab` 及其后续如有),验证后经独立 PR 合入。
3. 仅需下线新供应商字段时可先保留两列(快照保留无副作用),只回退 Controller/Service 逻辑与 Feign 校验。
4. 使用 Deploy Panel 滚动部署 `hl-fleet-service` 与 `hl-resource-service`;数据库列保留不删(`supplier_id`/`supplier_name` 允许 NULL,回退后不影响)。
5. 撤回后经 Gateway 验证新增/编辑/启用接口按改前口径通过;车队列表的供应商列展示空白或下线列头。
## 关联 / 联系人
### 链接
- **Issue**: [#6717](https://git.1814.love:8443/wx/HL/issues/6717)
- **PR**: [#6753](https://git.1814.love:8443/wx/HL/pulls/6753)
- **Merge commit**: 待合并后回填
### 联系人
- **后端负责人**: @wx
- **前端联动**: @mmg(下线车队列表「供应商」按钮 + 车队新增/编辑弹窗加供应商选择器)
@@ -0,0 +1,288 @@
---
schema: "hl-changelog/v2"
ticket: "6739"
title: "供应商三级地址保存与回显"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "5f35aafd"
target_release: "v2.1"
verified_at: "2026-08-30"
status_note: "PR #6744 已合并 dev-v3,合并提交 2d24b329 已部署 TEST。前端实证 #6350 拼接单字符串 address 且 002 不回显与新契约相反,属 required。已实现:三级联动与详细地址独立成字段(countyId+address),RegionCascader 新增 getRegionPath 受控回显,编辑表单回填入快照增量比对、详情页 countyId 反查省市区名。前端 commit 5f35aafd 已推 v2.1。当前状态:已消费(frontend_status: verified)。"
updated_at: "2026-08-30"
base: "dev-v3"
---
# 供应商三级地址保存与回显
`countyId` 保存三级联动的最下级区县 ID,`address` 只保存详细地址,例如“某某路 1 号”。前端不要再使用 `addressId`。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---:|---|---|---|---|---|
| 1 | 新增供应商草稿 | POST | `/admin/supplier/items/add` | 请求字段 | 可传 `countyId`、`address` |
| 2 | 修改供应商 | PUT | `/admin/supplier/items/{supplierId}/update` | 请求字段 | 可更新 `countyId`、`address` |
| 3 | 提交供应商 | POST | `/admin/supplier/items/{supplierId}/submit` | 请求字段 | 完整表单可传 `countyId`、`address` |
| 4 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应字段 | 顶层返回三级 ID 与 `address` |
## 三、接口详情
### 1. 新增供应商草稿 `POST /admin/supplier/items/add`
**VO**: `SupplierDraftSaveReqVO / SupplierWriteRespVO`
#### 使用场景
新增草稿时同时保存区县选择和详细地址。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| `countyId` | Body | String | 否 | 正整数 | 三级联动最后一级区县 ID |
| `address` | Body | String | 否 | 最长 500 字符 | 详细地址,不拼接省市区名称 |
| `fullName` / `taxNo` / `mainCooperation` | Body | String | 是 | 沿用既有规则 | 其他新增必填字段 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.supplierId` | String | 新供应商 ID |
| `data.updateTime` | String | 后续修改使用的并发版本 |
#### 请求示例
```json
{"fullName":"示例旅行服务有限公司","taxNo":"91350211M000100Y46","countyId":"376","address":"某某路1号","mainCooperation":"景区合作"}
```
#### 响应示例
```json
{"code":200,"message":"成功","success":true,"data":{"supplierId":"2093966831894056961","status":"DRAFT","updateTime":"2026-08-30 15:39:31"}}
```
#### 空数据 / 降级响应
省略 `countyId` 或 `address` 时按 `null` 保存;依赖或业务校验失败时不产生部分写入。
#### 错误响应
```json
{"code":400,"message":"区县ID必须为正数","success":false,"data":null}
```
#### 业务边界
- 有三级联动选择结果时只传最后一级 `countyId`。
- `addressId` 不是本接口字段。
### 2. 修改供应商 `PUT /admin/supplier/items/{supplierId}/update`
**VO**: `SupplierUpdateReqVO / SupplierWriteRespVO`
#### 使用场景
编辑页修改区县或详细地址。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| `supplierId` | Path | String | 是 | 正整数 | 供应商 ID |
| `countyId` | Body | String | 否 | 正整数 | 新的最下级区县 ID |
| `address` | Body | String | 否 | 最长 500 字符 | 新的详细地址 |
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 详情最新并发版本 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.supplierId` | String | 供应商 ID |
| `data.updateTime` | String | 保存后的新并发版本 |
#### 请求示例
```json
{"countyId":"377","address":"某某路2号","expectedUpdateTime":"2026-08-30 15:39:31"}
```
#### 响应示例
```json
{"code":200,"message":"成功","success":true,"data":{"supplierId":"2093966831894056961","status":"DRAFT","updateTime":"2026-08-30 15:39:42"}}
```
#### 空数据 / 降级响应
省略 `countyId` 或 `address` 表示该字段不修改;失败时原值和版本保持不变。
#### 错误响应
```json
{"code":400,"message":"区县ID必须为正数","success":false,"data":null}
```
#### 业务边界
- 修改成功后使用响应中的新 `updateTime` 重新查询详情。
- 非正数 `countyId` 在写入前拒绝。
### 3. 提交供应商 `POST /admin/supplier/items/{supplierId}/submit`
**VO**: `SupplierSubmitReqVO / SupplierApprovalCommandRespVO`
#### 使用场景
提交完整供应商表单进入审批时一并保存区县和详细地址。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| `supplierId` | Path | String | 是 | 正整数 | 草稿供应商 ID |
| `countyId` | Body | String | 否 | 正整数 | 最下级区县 ID |
| `address` | Body | String | 否 | 最长 500 字符 | 详细地址 |
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 当前并发版本 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.approvalLogId` | String | 审批记录 ID |
| `data.approvalStatus` | String | 审批状态 |
#### 请求示例
```json
{"fullName":"示例旅行服务有限公司","taxNo":"91350211M000100Y46","countyId":"377","address":"某某路2号","mainCooperation":"景区合作","expectedUpdateTime":"2026-08-30 15:39:42"}
```
#### 响应示例
```json
{"code":200,"message":"成功","success":true,"data":{"approvalLogId":"2093967000000000001","approvalStatus":"APPROVED"}}
```
#### 空数据 / 降级响应
`countyId`、`address` 为空时按完整表单既有规则处理;提交失败不推进审批状态。
#### 错误响应
```json
{"code":400,"message":"区县ID必须为正数","success":false,"data":null}
```
#### 业务边界
- 仍须提交完整表单和最新 `expectedUpdateTime`。
- 本次不改变既有审批、幂等和状态门禁。
### 4. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
**VO**: `SupplierBasicInfoRespVO`
#### 使用场景
编辑页读取三级联动默认值和详细地址。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| `supplierId` | Path | String | 是 | 正整数 | 供应商 ID |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.provinceId` | String/null | 省级 ID |
| `data.prefectureId` | String/null | 市级 ID |
| `data.countyId` | String/null | 已保存的最下级区县 ID |
| `data.address` | String/null | 已保存的详细地址原值 |
#### 请求示例
```http
GET /admin/supplier/items/2093966831894056961/basic-info/view
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{"code":200,"message":"成功","success":true,"data":{"supplierId":"2093966831894056961","provinceId":"1","prefectureId":"35","countyId":"377","address":"某某路2号","updateTime":"2026-08-30 15:39:42"}}
```
#### 空数据 / 降级响应
历史供应商没有 `countyId` 时三个行政区划 ID 均为 `null`,`address` 仍按历史值返回。
#### 错误响应
```json
{"code":395001,"message":"供应商不存在","success":false,"data":null}
```
#### 业务边界
- 三个行政区划 ID 均按 JSON 字符串返回。
- 行政区划父链不可用或不完整时失败关闭,不伪造 ID。
## 四、契约约束与正确调用方式
1. 用 `/admin/region/children` 逐级选择,将最后一级 ID 作为 `countyId`,门牌内容单独放入 `address`。
2. 编辑页按 `provinceId → prefectureId → countyId` 设置默认选中,并用 `address` 初始化详细地址输入框。
3. 前端字段统一命名为 `countyId`;保存后使用新版本重新查询详情。
## 五、数据库行为
- 写接口成功后,后续详情查询返回已保存的 `countyId` 和 `address`。
- 非正数 `countyId` 校验失败时不修改原值或并发版本。
- 历史供应商不自动补行政区划 ID。
## 六、边界行为
- 未认证返回业务码 `401`;供应商不存在或已删除返回 `395001`。
- 业务失败可能仍使用 HTTP 200,前端须同时判断 `code` 与 `success`。
- `address` 不包含省市区中文文本;省市区展示由三级 ID 对应的选项生成。
## 六.6、修改前后对比
| 项目 | 修改前 | 修改后 |
|---|---|---|
| 写请求 | 仅保存详细地址 | 可同时保存最下级 `countyId` 与详细地址 |
| 详情回显 | 仅有 `address` | 顶层增加三个行政区划 ID,继续返回 `address` |
## 六.7、影响评估
- **是否破坏向后兼容**:否,历史空值继续返回 `null`。
- **前端是否必须同步上线**:是,需要传 `countyId` 并消费三级回显字段。
- **前端 workaround 清理点**:移除 `addressId` 映射和自行解析完整中文地址的逻辑。
## 七、不影响范围
- 不改变供应商权限、状态机、审批、并发和错误码。
- 不改变行政区划联动接口契约,也不影响其他资源模块。
## 八、测试环境已验证
- 新增与修改后可回读对应 `countyId`、`address`,详情三个行政区划 ID 均为字符串。
- 非正数 `countyId` 返回 `400` 且原数据不变;未认证返回 `401`;验收草稿已清理。
## 十、相关文档
- [Issue #6739](https://git.1814.love:8443/wx/HL/issues/6739)
- [PR #6744](https://git.1814.love:8443/wx/HL/pulls/6744)
## 关联 / 联系人
- **合并提交**: [2d24b3290](https://git.1814.love:8443/wx/HL/commit/2d24b32908bffb0e7ac95fd31b33cdfe56ae69f5)
- **后端负责人**: @lc
- **当前状态**: 已消费(`frontend_status: verified`)。RegionCascader 新增受控回显(watch value→getRegionPath 反查父链逐级预载,零 emit,停用节点占位);EditModal 提交体改 countyId+address 独立字段、fillForm 回填+快照增量比对(改动才携带、清空都不携带),删拼接逻辑;DetailModal 地址改 countyId 反查省市区名+address。spec 84 例绿,commit 5f35aafd 已推 v2.1。
@@ -0,0 +1,293 @@
---
schema: "hl-changelog/v2"
ticket: "6811"
title: "车队保存(无供应商)自动落停用;仍有在役车辆时以 601112 拒绝保存"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "6985f0cf"
target_release: "v2.1"
verified_at: "2026-08-31"
status_note: "#6717 只在「本次保存清除供应商」时强制停用,存量无供应商车队原样再保存仍停留启用。本单把守卫改为按保存结果判定,并对齐 disable 的在役车辆不变量新增 601112。TEST 已实测两条分支。前端已消费:submitForm 补 601112 精确提示,保存后以响应/列表刷新状态展示。"
updated_at: "2026-08-31"
base: "dev-v3"
generated: "2026-08-31T10:45:00+08:00"
---
# 车队编辑保存:无供应商时自动落停用,仍有在役车辆则以 601112 拒绝
> **服务**: hl-fleet-service (端口 8087/8187)
> **PR**: #6827
> **Issue**: #6811
> **日期**: 2026-08-31
> **影响范围**: 管理后台车务 → 车队管理 → 编辑车队保存
---
## ⚠️ 关键变化
- 保存车队时若**结果为无供应商**(`supplierId` 传 `null` 或不传),后端不再允许车队停留在「启用」:无在役车辆时自动把状态落为 `DISABLED`,前端保存成功后需按响应/列表刷新后的状态展示,不能沿用提交前的「启用」。
- 上一版(#6717)只在「本次保存把已绑供应商清空」时才强制停用,存量无供应商车队原样再保存不变状态。**该行为已收口**:现在按保存结果判定,`null → null` 同样强制停用。
- 新增错误码 **601112**:无供应商需自动停用但车队仍有在役车辆时,保存整体失败、零写入。
---
## 一、背景
`fleet_team` 的不变量是 **ACTIVE ⇒ 已绑供应商**。此前新建(无供应商落停用)和启用(无供应商 601108 拒绝)都有守卫,唯独编辑保存漏了「结果无供应商」这一路,导致管理后台车队列表出现「供应商列为 —(无供应商)+ 状态启用」的行,且反复保存也不收敛。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 修改车队 | PUT | `/admin/fleet/teams/{fleetTeamId}` | 状态副作用 + 新增错误码 | 保存结果无供应商时自动落停用;仍有在役车辆则 601112 拒绝 |
---
## 三、接口详情
### 1. 修改车队 `PUT /admin/fleet/teams/{fleetTeamId}`
**VO**: `FleetTeamSaveReqVO` → `Result<FleetTeamRespVO>`
#### 使用场景
车务 → 车队管理 → 列表行「编辑」→ 弹窗改车队名称/类型/负责人/电话/付款方式/排序/备注/供应商后点「保存」。本次仅状态副作用与错误码变化,路径、请求字段、字段类型与必填性都不变。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| fleetTeamId | Path | String(雪花 ID) | ✅ | - | 车队 ID,字符串传递,禁止数值化 |
| teamName | Body | String | ✅ | 非空、全局唯一 | 车队名称 |
| teamType | Body | String | ✅ | `SELF_OPERATED` / `COOPERATIVE` | 已关联车辆时不可切换 |
| leaderName | Body | String | ✅ | 非空 | 负责人 |
| leaderPhone | Body | String | ✅ | 手机号 | 负责人电话 |
| settleType | Body | String | ✅ | `cash` / `sign` / `company` | 付款方式(资源付款方式字典) |
| sortOrder | Body | Integer | ✅ | ≥ 0 | 排序 |
| remark | Body | String | ❌ | ≤ 256 | 备注,空串按 null 落库 |
| supplierId | Body | String(雪花 ID) | ❌ | 供应商需生效且含 FLEET 类型 | **不传或传 null = 不选供应商**,触发本次自动停用规则 |
#### 出参 `Result<FleetTeamRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| fleetTeamId | String | 车队 ID |
| teamName | String | 车队名称 |
| teamType | String | `SELF_OPERATED` / `COOPERATIVE` |
| status | String | **`ACTIVE` / `DISABLED`;保存结果无供应商时返回 `DISABLED`** |
| settleType | String | 付款方式 |
| sortOrder | Integer | 排序 |
| supplierId | String / null | 关联供应商 ID,null = 未关联 |
| supplierName | String / null | 供应商全称快照,供应商为 null 时同步清空 |
| vehicleCount | Number | 名下车辆总数 |
| activeVehicleCount | Number | 在役车辆数 |
#### 请求示例
无供应商的车队原样保存(触发自动停用):
```json
{
"teamName": "测试车队P4-1787131721",
"teamType": "COOPERATIVE",
"leaderName": "测试负责人",
"leaderPhone": "13900000001",
"settleType": "cash",
"sortOrder": 99,
"remark": "由 fleet_attribution/历史业务数据迁移,负责人待完善",
"supplierId": null
}
```
#### 响应示例
无供应商 + 无在役车辆 → 200,且 `status` 已变 `DISABLED`:
```json
{
"code": 200,
"message": "成功",
"data": {
"fleetTeamId": "348397854705979392",
"teamName": "测试车队P4-1787131721",
"teamType": "COOPERATIVE",
"status": "DISABLED",
"settleType": "cash",
"sortOrder": 99,
"supplierId": null,
"supplierName": null,
"vehicleCount": 0,
"activeVehicleCount": 0
},
"success": true
}
```
#### 空数据 / 降级响应
本接口为写接口,不返回空集。供应商资格校验依赖(resource 侧)不可用时**失败关闭**,不降级放行:
```json
{
"code": 601110,
"message": "暂时无法校验供应商,请稍后重试",
"success": false,
"data": null
}
```
#### 错误响应
新增(本次)——无供应商需自动停用但仍有在役车辆:
```json
{
"code": 601112,
"message": "车队未关联供应商需自动停用,但仍有在役车辆,请先关联供应商或转移在役车辆",
"success": false,
"data": null
}
```
本接口其他错误码(本次未变,供自包含联调):
| code | message | 触发条件 |
|------|---------|----------|
| 601100 | 车队不存在 | `fleetTeamId` 无对应车队 |
| 601101 | 车队名称已存在 | `teamName` 与其他车队重名 |
| 601104 | 车队已关联车辆,不能修改自有/合作类型 | 名下有车辆时改 `teamType` |
| 601106 | 付款方式不是有效的资源付款方式 | `settleType` 不在 `cash`/`sign`/`company` |
| 601109 | 供应商不存在、未生效或不包含车队类型 | 绑定的 `supplierId` 不合格 |
| 601110 | 暂时无法校验供应商,请稍后重试 | 供应商资格校验依赖不可用 |
| 601111 | 车队已关联订单,不能更换供应商 | 已绑供应商的车队换绑/清除且名下车辆有非取消派单 |
#### 业务边界
- 鉴权:需登录且具备车务车队管理权限;未登录网关返 401。
- **状态不由前端入参驱动**:请求体没有 `status` 字段,启停仍由 `/disable`、`/enable` 切;唯一例外是本次的自动停用副作用。
- 供应商未变时不调供应商资格校验,也不查订单围栏;仅当 `supplierId` 发生变化才校验。
- 已绑供应商的车队换绑或清除时,名下车辆若有非取消派单一律 601111(该守卫先于 601112 判定)。
- 失败零写入:601112、601111、601109、601110 均在写库前抛出,名称、排序、备注等字段都不会部分保存。
- 并发:同一 `fleetTeamId` 的保存互斥(分布式锁),重复提交不会产生中间态。
- 已停用且无供应商的车队保存时保持停用,不报错。
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload 与结果 |
|------|---------------|
| ✅ 绑定合法供应商保存 | `{"supplierId": "2094238127517319170", ...}` → 200,`status` 保持原值 |
| ✅ 无供应商 + 无在役车辆 | `{"supplierId": null, ...}` → 200,`status=DISABLED` |
| ✅ 已停用 + 无供应商 | `{"supplierId": null, ...}` → 200,`status` 保持 `DISABLED` |
| ❌ 无供应商 + 有在役车辆 | `{"supplierId": null, ...}` → 601112,零写入 |
| ❌ 靠不传 `supplierId` 保留原供应商 | 不传等同传 `null`,会被当成「清除供应商」 |
### 切换状态时的必要动作
- 编辑回显后提交必须**原样回传详情里的 `supplierId`**;字段缺失就是清除语义,不存在「不传 = 不改」。
- 保存成功后不能沿用提交前的状态展示:用响应体 `data.status` 或重拉列表/详情。
- 收到 601112 时弹后端 message 即可;修复路径是「选一个合法供应商」或「先把在役车辆转走/停用」。
---
## 五、数据库行为
| 前端提交 | 保存后外部可观察结果 |
|----------|--------------------|
| `supplierId` 合法值 | 车队关联该供应商,`supplierName` 刷为当前全称快照,状态不变 |
| `supplierId=null`,无在役车辆 | 供应商关联与全称快照同时置空,状态变 `DISABLED` |
| `supplierId=null`,有在役车辆 | 零写入(名称/排序/备注等也不保存) |
本次无表结构变更、无 Flyway 迁移,存量脏数据不做批量刷状态(下次编辑保存时自然收口)。
---
## 六、边界行为
- 未登录 → 401(网关拦截)。
- 车队不存在 → 601100。
- 供应商资格依赖降级 → 601110 失败关闭,不静默放行。
- 存量无供应商且仍启用的车队:不会被后台任务批量改状态,只在下次编辑保存时收口。
- 已绑供应商的车队保存行为与之前一致,无新增拦截。
- 供应商改名后快照陈旧:仍需重新编辑车队刷新(行为未变)。
---
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| 请求体字段 | 无变化 | 无变化(路径、字段名、类型、必填性全部不变) |
| 响应 `data.status` | 仅当本次清除已绑供应商时可能返回 `DISABLED` | **只要保存结果无供应商且原为启用,就返回 `DISABLED`** |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 存量无供应商车队(`null → null`)原样保存 | 保持「启用」 | 无在役车辆 → 自动「停用」;有在役车辆 → 601112 |
| 清除已绑供应商(`X → null`) | 启用→停用(不查在役车辆) | 有在役车辆时改为 601112 拒绝,不再产出「停用车队挂在役车辆」脏态 |
| 绑定/换绑合法供应商 | 200,状态不变 | 不变 |
## 六.7、影响评估
- **是否破坏向后兼容**:否(请求/响应结构不变,新增一个错误码与一个状态副作用)。
- **前端是否必须同步上线**:否。旧前端不报错,但若保存后不刷新列表,页面会短暂显示陈旧的「启用」。
- **前端 workaround 清理点**:若前端曾为「无供应商仍显示启用」做过本地推断或提示,可改为直读后端 `status`。
---
## 七、不影响范围
- **仅影响**:管理后台车队编辑保存(`PUT /admin/fleet/teams/{fleetTeamId}`)。
- **零影响**:
- 新建车队 `POST /admin/fleet/teams`(无供应商本来就落停用)
- 启用/停用 `POST /admin/fleet/teams/{id}/enable`、`/disable`
- 车队列表、详情、options 读接口字段
- 车辆、司机、派单、看板、对账等其他车务接口
- 存量数据(不批量刷状态)
---
## 八、测试环境已验证
部署:`dev-v3` 合入 PR #6827(merge `740fdb834`)后部署 hl-fleet-service(8087/8187 滚动,两实例健康)。
```
PUT /admin/fleet/teams/348397854705979392 (无供应商 + 启用 + activeVehicleCount=0)
→ 200 成功,保存后 GET 详情 status=DISABLED, supplierId=null ✓
PUT /admin/fleet/teams/348398422098841600 (无供应商 + 启用 + activeVehicleCount=1)
→ 601112 「车队未关联供应商需自动停用,但仍有在役车辆…」
保存后 GET 详情 status=ACTIVE(零写入) ✓
PUT /admin/fleet/teams/2026072200010000001 (已绑供应商 + 有订单, 清除供应商)
→ 601111 既有订单围栏优先,行为未变 ✓
```
验证用车队:`348397854705979392`(测试车队P4-1787131721)、`348398422098841600`(测试车队P4-1787131856,验证用临时车辆已删除并回到 0 车)。
单测与构建:`mvn -pl hl-fleet-service -am verify` → Tests run 3887 / Failures 0 / Errors 0(Skipped 5 为既有 Release-E 跳过项);`mvn -pl hl-fleet-service spotless:check` → 799 文件 0 违规。
---
## 十、相关文档
- 前置契约:`changelogs-v2/2026-08/30_6717_车队关联供应商并展示供应商全名-修改接口-管理后台.md`
- 本次 PR:[#6827](https://git.1814.love:8443/wx/HL/pulls/6827)
## 关联 / 联系人
- Issue:[#6811](https://git.1814.love:8443/wx/HL/issues/6811)
- 后端联系人:@wx
@@ -0,0 +1,488 @@
---
schema: "hl-changelog/v2"
ticket: "6834"
title: "供应商余额支付类型与草稿编辑项"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "hl-admin"
frontend_ref: "848cca7b"
target_release: "v2.1"
verified_at: "2026-09-01"
status_note: "PR #6860 已合并 dev-v3,合并提交 6ed8e24c0f04051c9c4385c79de67f255abb4952 已部署 TEST。真实 Gateway 已验证支付类型字典、新增必填与失败零写入、余额和支付类型保存回显、草稿省略 changeReason 更新及验收数据清理。前端 hl-admin v2.1 提交 848cca7b 已交付:SupplierEditModal 新增余额 NInputNumber(数值允许负数,提交 number)+支付类型 NSelect(接 supplier_payment_type 字典存 dictValue 显 dictLabel,字典未就绪禁用提交不硬编码),balance 必填用 type:'number' 修 async-validator 误判,编辑按快照增量仅改动携带,DetailModal 回显两字段;spec 12 例绿。当前状态:后端已就绪,前端已交付。"
updated_at: "2026-09-01"
base: "dev-v3"
---
# 供应商余额支付类型与草稿编辑项
供应商新增、提交、编辑和详情增加 `balance`、`paymentType`。新增与提交时两项必填;编辑时可按增量提交,详情用于回显。支付类型从 `supplier_payment_type` 字典读取。
供应商详情 `status=DRAFT` 时,编辑页隐藏“变更原因”;其他状态继续显示并必填。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---:|---|---|---|---|---|
| 1 | 新增供应商草稿 | POST | `/admin/supplier/items/add` | 请求字段 | `balance`、`paymentType` 必填 |
| 2 | 提交供应商审批 | POST | `/admin/supplier/items/{supplierId}/submit` | 请求字段 | 完整表单中的两个字段必填 |
| 3 | 编辑供应商 | PUT | `/admin/supplier/items/{supplierId}/update` | 请求字段与状态规则 | 可更新两个字段;草稿可省略 `changeReason` |
| 4 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应字段 | 返回 `balance`、`paymentType`、`status` |
| 5 | 查询支付类型字典类型 | GET | `/admin/dict/type` | 新增字典数据 | 可按 `supplier_payment_type` 查询字典类型 |
| 6 | 查询支付类型字典项 | GET | `/admin/dict/data/supplier_payment_type` | 新增字典数据 | 返回现付、签单、月付 |
## 三、接口详情
### 1. 新增供应商草稿 `POST /admin/supplier/items/add`
**VO**: `SupplierDraftSaveReqVO / SupplierWriteRespVO`
#### 使用场景
新增供应商时录入初始余额并选择支付类型。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `balance` | Body | Number | 是 | 最多 16 位整数、2 位小数,可为正数、0 或负数 | 供应商当前余额 |
| `paymentType` | Body | String | 是 | 必须命中当前生效的 `supplier_payment_type` 字典项 | 支付类型值,传 `1`、`2` 或 `3` |
| `fullName` / `taxNo` / `mainCooperation` | Body | String | 是 | 沿用既有规则 | 其他新增必填字段 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.supplierId` | String | 新供应商 ID |
| `data.status` | String | 新增草稿为 `DRAFT` |
| `data.updateTime` | String | 后续编辑使用的并发版本 |
#### 请求示例
```json
{
"fullName": "示例供应商有限公司",
"taxNo": "91350211M000100Y46",
"mainCooperation": "旅游资源合作",
"balance": 1200.50,
"paymentType": "1"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2094278854020399106",
"status": "DRAFT",
"updateTime": "2026-08-31 12:19:23"
}
}
```
#### 空数据 / 降级响应
`balance` 或 `paymentType` 省略、为 `null` 或支付类型为空白时返回业务码 `400`,不创建草稿。
#### 错误响应
```json
{"code":400,"message":"余额不能为空","success":false,"data":null}
```
```json
{"code":400,"message":"供应商支付类型不合法或已停用","success":false,"data":null}
```
#### 业务边界
- 前端必须传数值型 `balance`,不要传格式化金额字符串。
- `paymentType` 传字典 `dictValue`,不要传“现付”等展示文本。
- 校验失败时不产生供应商记录。
### 2. 提交供应商审批 `POST /admin/supplier/items/{supplierId}/submit`
**VO**: `SupplierSubmitReqVO / SupplierApprovalCommandRespVO`
#### 使用场景
把完整供应商表单提交审批时,一并提交余额和支付类型。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 草稿供应商 |
| `balance` | Body | Number | 是 | 最多 16 位整数、2 位小数 | 当前余额 |
| `paymentType` | Body | String | 是 | 当前生效字典值 | 支付类型 |
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 当前供应商并发版本 |
| 其他完整表单字段 | Body | 对应类型 | 按既有规则 | 沿用现有提交契约 | 不得只提交本次新增字段 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.approvalLogId` | String | 审批记录 ID |
| `data.approvalStatus` | String | 审批状态 |
#### 请求示例
```json
{
"fullName": "示例供应商有限公司",
"taxNo": "91350211M000100Y46",
"mainCooperation": "旅游资源合作",
"balance": 1200.50,
"paymentType": "1",
"expectedUpdateTime": "2026-08-31 12:19:23"
}
```
#### 响应示例
```json
{"code":200,"message":"成功","success":true,"data":{"approvalLogId":"2094279000000000001","approvalStatus":"PENDING"}}
```
#### 空数据 / 降级响应
两个新增字段任一为空即拒绝提交,审批状态不推进。
#### 错误响应
```json
{"code":400,"message":"支付类型不能为空","success":false,"data":null}
```
#### 业务边界
- 提交仍是完整表单命令,并继续执行既有状态、并发、审批和幂等门禁。
- 支付类型失效或不存在时失败关闭,不使用本地默认值代替。
### 3. 编辑供应商 `PUT /admin/supplier/items/{supplierId}/update`
**VO**: `SupplierUpdateReqVO / SupplierWriteRespVO`
#### 使用场景
编辑页修改余额或支付类型;根据详情的服务端状态决定是否显示变更原因。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
| `balance` | Body | Number | 否 | 最多 16 位整数、2 位小数 | 省略表示不修改 |
| `paymentType` | Body | String | 否 | 非空时必须命中当前生效字典项 | 省略表示不修改 |
| `changeReason` | Body | String | 条件必填 | 最长 500 | `DRAFT` 可省略;其他状态去空白后必须非空 |
| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 详情最新并发版本 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.supplierId` | String | 供应商 ID |
| `data.status` | String | 保存后的服务端状态 |
| `data.updateTime` | String | 保存后的新并发版本 |
#### 请求示例
`DRAFT` 草稿完全省略 `changeReason`:
```json
{
"balance": -12.34,
"paymentType": "3",
"expectedUpdateTime": "2026-08-31 12:19:23"
}
```
#### 响应示例
```json
{"code":200,"message":"成功","success":true,"data":{"supplierId":"2094278854020399106","status":"DRAFT","updateTime":"2026-08-31 12:19:33"}}
```
#### 空数据 / 降级响应
省略 `balance` 或 `paymentType` 表示该字段不修改;历史供应商尚未补录时,详情可返回 `null`。
#### 错误响应
非草稿缺少变更原因:
```json
{"code":400,"message":"变更原因不能为空","success":false,"data":null}
```
#### 业务边界
- 是否为草稿只取服务端锁定后的当前状态,不接收客户端自报状态。
- 草稿隐藏“变更原因”并省略 `changeReason`;非草稿继续显示且必填。
- 更新成功后使用新 `updateTime` 重新查询详情;并发失败返回既有 `395014`。
### 4. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
**VO**: `SupplierBasicInfoRespVO`
#### 使用场景
新增后或进入编辑页时回显余额、支付类型和当前状态。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.balance` | Number/null | 当前余额;历史未补录可为 `null` |
| `data.paymentType` | String/null | `supplier_payment_type` 字典值;历史未补录可为 `null` |
| `data.status` | String | 用于控制变更原因框 |
| `data.updateTime` | String | 编辑请求的并发版本 |
#### 请求示例
```http
GET /admin/supplier/items/2094278854020399106/basic-info/view
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2094278854020399106",
"balance": -12.34,
"paymentType": "3",
"status": "DRAFT",
"updateTime": "2026-08-31 12:19:33"
}
}
```
#### 空数据 / 降级响应
历史供应商没有新字段时返回 `null`;前端显示未补录,不自行写入默认值。
#### 错误响应
```json
{"code":395001,"message":"供应商不存在","success":false,"data":null}
```
#### 业务边界
- `paymentType` 原样返回字典值,中文标签由字典项映射。
- 业务失败可能仍是 HTTP 200,必须同时判断 `code` 和 `success`。
### 5. 查询支付类型字典类型 `GET /admin/dict/type`
**VO**: `PageResult<SysDictTypeRespVO>`
#### 使用场景
字典管理页或联调时确认 `supplier_payment_type` 类型已经存在。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `page` | Query | Number | 否 | 默认 1 | 页码 |
| `pageSize` | Query | Number | 否 | 默认 20 | 每页条数 |
| `keyword` | Query | String | 否 | 传 `supplier_payment_type` | 按编码或名称搜索 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.records[].dictType` | String | `supplier_payment_type` |
| `data.records[].dictName` | String | `供应商支付类型` |
| `data.records[].status` | String | `ACTIVE` |
#### 请求示例
```http
GET /admin/dict/type?page=1&pageSize=20&keyword=supplier_payment_type
```
#### 响应示例
```json
{"code":200,"message":"成功","success":true,"data":{"records":[{"dictType":"supplier_payment_type","dictName":"供应商支付类型","status":"ACTIVE"}],"total":1,"page":1,"pageSize":20}}
```
#### 空数据 / 降级响应
未命中时 `records=[]`;这表示字典未就绪,前端不要回退到其他结算字典。
#### 错误响应
```json
{"code":401,"message":"Token无效或已过期","success":false,"data":null}
```
#### 业务边界
- 该接口只查询字典类型,不直接提供下拉项。
- 下拉选项必须继续调用下一接口。
### 6. 查询支付类型字典项 `GET /admin/dict/data/supplier_payment_type`
**VO**: `List<SysDictDataRespVO>`
#### 使用场景
新增、编辑供应商页面加载支付类型下拉选项。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| 无 | - | - | 否 | 字典编码固定在路径中 | 无请求体、查询参数或动态路径参数 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data[].dictValue` | String | 请求保存的值 |
| `data[].dictLabel` | String | 下拉展示文案 |
| `data[].sortOrder` | Number | 排序号 |
| `data[].status` | String | 当前均为 `ACTIVE` |
#### 请求示例
```http
GET /admin/dict/data/supplier_payment_type
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": [
{"dictValue":"1","dictLabel":"现付","sortOrder":10,"status":"ACTIVE"},
{"dictValue":"2","dictLabel":"签单","sortOrder":20,"status":"ACTIVE"},
{"dictValue":"3","dictLabel":"月付","sortOrder":30,"status":"ACTIVE"}
]
}
```
#### 空数据 / 降级响应
返回空数组或调用失败时禁用提交并提示字典加载失败,不硬编码替代值。
#### 错误响应
```json
{"code":401,"message":"Token无效或已过期","success":false,"data":null}
```
#### 业务边界
- 请求保存 `dictValue`,界面展示 `dictLabel`。
- 后端每次写入都动态校验当前生效项;已停用或未知值返回业务码 `400`。
## 四、契约约束与正确调用方式
1. 页面初始化先查询详情与支付类型字典项,用 `paymentType` 匹配 `dictValue`。
2. 新增和提交时 `balance`、`paymentType` 都必填;编辑时只发送实际修改字段和最新 `expectedUpdateTime`。
3. `status=DRAFT` 隐藏“变更原因”并可省略 `changeReason`;其他状态必须收集非空原因。
4. 金额输入保留最多两位小数,允许负数;不要把千分位格式化文本发给后端。
| 场景 | payload | 结果 |
|---|---|---|
| 新增完整 | `{ "balance": 100.00, "paymentType": "1", ... }` | 成功 |
| 新增缺余额 | `{ "paymentType": "1", ... }` | `400`,余额不能为空 |
| 新增非法支付类型 | `{ "balance": 100.00, "paymentType": "9", ... }` | `400`,支付类型不合法或已停用 |
| 草稿编辑 | `{ "balance": -12.34, "paymentType": "3", "expectedUpdateTime": "..." }` | 成功,可省略 `changeReason` |
| 非草稿编辑缺原因 | `{ "balance": 0, "expectedUpdateTime": "..." }` | `400`,变更原因不能为空 |
## 五、数据库行为
- 新增或更新成功后,详情回读相同的 `balance`、`paymentType`。
- 历史供应商不强制回填,新字段可返回 `null`;创建和提交的新数据仍由接口强制必填。
- 审批与非草稿变更继续保留两个字段的业务快照;失败时不产生部分写入。
## 六、边界行为
- `balance` 最多 16 位整数和 2 位小数,正数、0、负数均有效。
- `paymentType` 必须是当前生效字典值;未知、空白或停用值不保存。
- 未登录返回业务码 `401`;不存在返回 `395001`;并发版本过期返回 `395014`。
- 不新增权限、Gateway 路由、Redis、MQ 或配置行为。
## 六.5、枚举 / 数据字典
### `paymentType`(字典 `supplier_payment_type`)
**所属字段**: `SupplierDraftSaveReqVO.paymentType / SupplierUpdateReqVO.paymentType / SupplierBasicInfoRespVO.paymentType` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `1` | 现付 | 按现付方式处理 |
| `2` | 签单 | 按签单方式处理 |
| `3` | 月付 | 按月付方式处理 |
## 六.6、修改前后对比
| 项目 | 修改前 | 修改后 |
|---|---|---|
| 新增与提交 | 无余额、支付类型字段 | 两字段必填并校验 |
| 编辑与详情 | 无法录入和回显两字段 | 支持增量修改并原值回显 |
| 支付类型选项 | 无专用字典 | 使用 `supplier_payment_type` 三个生效项 |
| 草稿变更原因 | 页面仍可能显示 | `DRAFT` 隐藏;非草稿继续必填 |
## 六.7、影响评估
- **是否破坏向后兼容**:新增、提交请求增加必填字段,前端必须同步;编辑与详情为兼容扩展。
- **前端是否必须同步上线**:是,需要新增两个控件、接入字典并按状态调整变更原因框。
- **前端 workaround 清理点**:删除支付类型硬编码和草稿 `changeReason` 必填/展示逻辑;非草稿校验必须保留。
## 七、不影响范围
- 不修改供应商权限、状态机、审批流程、并发、软删除和既有错误码。
- 不复用 `resource_settle_type`,也不影响供应商账户、合同和资源关系接口。
- 本次仅交付后端接口 Changelog,不修改任何前端源码或资源。
## 八、测试环境已验证
- TEST Gateway 查询到字典类型及精确字典项 `1-现付`、`2-签单`、`3-月付`。
- 缺余额、缺支付类型、非法支付类型均返回 `400`,验证前后列表为空。
- 有效新增返回 `DRAFT`;详情回显 `balance=12.34`、`paymentType=1`。
- 草稿省略 `changeReason` 更新成功;详情回显 `balance=-12.34`、`paymentType=3`。
- 验收草稿已通过删除接口清理,列表为空且详情返回“供应商不存在”。
## 当前状态
- 后端:已部署并已验证。
- 前端:待处理。
## 十、相关文档
- Issue:[#6834](https://git.1814.love:8443/wx/HL/issues/6834)
- 后端 PR:[#6860](https://git.1814.love:8443/wx/HL/pulls/6860)
- 合并提交:[6ed8e24c](https://git.1814.love:8443/wx/HL/commit/6ed8e24c0f04051c9c4385c79de67f255abb4952)
## 关联 / 联系人
- **Issue**: [#6834](https://git.1814.love:8443/wx/HL/issues/6834)
- **PR**: [#6860](https://git.1814.love:8443/wx/HL/pulls/6860)
- **后端负责人**: @lc
- **当前状态**: 后端已就绪,前端待处理。
@@ -0,0 +1,373 @@
---
schema: "hl-changelog/v2"
ticket: "6842"
title: "供应商补充合同字段与附件"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "hl-admin"
frontend_ref: "f5414b12"
target_release: "v2.1"
verified_at: "2026-09-01"
status_note: "PR #6858 已合并 dev-v3,hl-resource-service 已部署提交 5546d7907c6eda9b372b4639094f84e568de6496。TEST Gateway 已验证完整字段与全空业务字段合同的新增、更新、详情回读,以及单个 PDF 上传、预览、下载和清理。前端 hl-admin v2.1 提交 f5414b12 已交付:ContractEditModal 增 businessLine/relatedMainContract/autoRenew(三态,false 不按空过滤)+SIGNED 选项,scanFileUrl 改 FileUpload 文件中心单附件(upload/token→confirm 写 ossUrl),隐藏 contractType/amount/pricingMode/settleCycle/remark 但编辑原样回传防整份替换误清;ManageModal 列重排契约固定集、SIGNED→已签约、有效期直拼上海日历日、附件预览带登录态 preview-by-url/下载直开永久地址;api/file.js 加 getContractPreviewBlobUrl。spec 编辑 16 例+管理 6 例绿。当前状态:后端已就绪,前端已交付。"
updated_at: "2026-09-01"
base: "dev-v3"
---
# 供应商补充合同字段与附件
供应商补充合同新增 `businessLine`、`relatedMainContract`、`autoRenew`,并允许写入 `SIGNED`(已签约)。所有合同业务字段均可为空,只有 `changeReason` 继续必填。
管理端登记页仅展示补合同编号、状态、合同名称、业务线、有效期、签署日期、关联主合同、自动续约和单个 Word/PDF 附件;其他兼容字段隐藏。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---:|---|---|---|---|---|
| 1 | 独立登记供应商合同 | POST | `/admin/supplier/items/{supplierId}/contracts/add` | 扩展请求与响应 | 增加三个字段,状态支持 `SIGNED` |
| 2 | 独立更新供应商合同 | PUT | `/admin/supplier/items/{supplierId}/contracts/{contractId}/update` | 扩展请求与响应 | 新字段支持补录、清空和 `false` |
| 3 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 扩展响应 | `contracts[]` 完整回读补充合同字段 |
## 三、接口详情
### 1. 独立登记供应商合同 `POST /admin/supplier/items/{supplierId}/contracts/add`
**VO**: `SupplierContractCreateReqVO / SupplierContractRespVO`
#### 使用场景
供应商已创建后登记一份补充合同;业务资料未齐时也可只提交审计原因,稍后再补录。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
| `contractNo` | Body | String/null | 否 | 最长 100 | 补合同编号 |
| `status` | Body | String/null | 否 | `DRAFT` / `ACTIVE` / `SIGNED` / `EXPIRED` | `SIGNED` 显示为“已签约” |
| `contractName` | Body | String/null | 否 | 最长 500 | 合同名称 |
| `businessLine` | Body | String/null | 否 | 最长 100 | 业务线,自由文本 |
| `startDate` | Body | String/null | 否 | `yyyy-MM-dd` | 有效期开始日 |
| `endDate` | Body | String/null | 否 | `yyyy-MM-dd` | 有效期结束日;两端都有值时不得早于开始日 |
| `signDate` | Body | String/null | 否 | `yyyy-MM-dd` | 合同签署日期 |
| `relatedMainContract` | Body | String/null | 否 | 最长 100 | 关联主合同外部引用 |
| `autoRenew` | Body | Boolean/null | 否 | `true` / `false` / `null` | 是 / 否 / 未登记 |
| `scanFileUrl` | Body | String/null | 否 | 最长 1000 | 文件中心确认上传后返回的永久 `ossUrl` |
| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 | 登记原因,写入审计 |
`contractType`、`amount`、`pricingMode`、`settleCycle`、`remark` 仍兼容但本页面隐藏,也均可为空。
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.contractId` | String | 合同 ID |
| `data.contractNo` | String/null | 补合同编号 |
| `data.status` | String/null | 包含新增可写值 `SIGNED` |
| `data.contractName` | String/null | 合同名称 |
| `data.businessLine` | String/null | 业务线 |
| `data.startDate` / `data.endDate` | String/null | 有效期日期 |
| `data.signDate` | String/null | 签署日期 |
| `data.relatedMainContract` | String/null | 关联主合同 |
| `data.autoRenew` | Boolean/null | 自动续约三态值 |
| `data.scanFileUrl` | String/null | 合同附件永久地址 |
| `data.updateTime` | String | 当前合同版本,格式 `yyyy-MM-dd HH:mm:ss` |
#### 请求示例
```json
{
"contractNo": "SHCT2025070102599446",
"status": "SIGNED",
"contractName": "内蒙古牵手草原国际旅行社有限公司额外后返",
"businessLine": null,
"startDate": "2010-01-01",
"endDate": "2099-12-31",
"signDate": "2025-07-01",
"relatedMainContract": "2185933",
"autoRenew": true,
"scanFileUrl": "https://files.example.com/supplier-contract/example.pdf",
"changeReason": "补录已签约合同"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"contractId": "2095000000000000001",
"contractNo": "SHCT2025070102599446",
"status": "SIGNED",
"contractName": "内蒙古牵手草原国际旅行社有限公司额外后返",
"businessLine": null,
"startDate": "2010-01-01",
"endDate": "2099-12-31",
"signDate": "2025-07-01",
"relatedMainContract": "2185933",
"autoRenew": true,
"scanFileUrl": "https://files.example.com/supplier-contract/example.pdf",
"updateTime": "2026-08-31 12:00:00"
}
}
```
#### 空数据 / 降级响应
仅提交 `changeReason` 也可成功;所有合同业务字段返回 `null`,前端不得补默认状态或日期。
#### 错误响应
```json
{
"code": 400,
"message": "变更原因不能为空",
"success": false,
"data": null
}
```
#### 业务边界
- 所有合同业务字段可空;`changeReason` 不属于业务展示字段,继续必填。
- `startDate`、`endDate` 同时有值时才校验日期先后。
- `scanFileUrl` 只保存一个附件永久地址;上传、预览、下载复用文件中心。
### 2. 独立更新供应商合同 `PUT /admin/supplier/items/{supplierId}/contracts/{contractId}/update`
**VO**: `SupplierContractUpdateReqVO / SupplierContractRespVO`
#### 使用场景
补齐、修改或清空已登记合同。该接口为完整替换,省略或传 `null` 会清空对应字段。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
| `contractId` | Path | String | 是 | 正整数 ID 字符串 | 目标合同 |
| 新增接口全部业务字段 | Body | 对应类型/null | 否 | 与新增一致 | 完整替换;`autoRenew=false` 会保留为否 |
| `expectedUpdateTime` | Body | String/null | 否 | `yyyy-MM-dd HH:mm:ss` | 提供时必须匹配当前版本 |
| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 | 更新原因 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.contractId` | String | 原合同 ID |
| `data.businessLine` | String/null | 更新后的业务线,空白规范为 `null` |
| `data.relatedMainContract` | String/null | 更新后的关联主合同 |
| `data.autoRenew` | Boolean/null | 精确保留 `true`、`false` 或 `null` |
| `data.status` | String/null | 可返回 `SIGNED` |
| 其余合同业务字段 | 对应类型/null | 完整替换后的值 |
| `data.updateTime` | String | 更新后的合同版本 |
#### 请求示例
```json
{
"contractNo": "SHCT2025070102599446",
"status": "SIGNED",
"contractName": "内蒙古牵手草原国际旅行社有限公司额外后返",
"businessLine": null,
"startDate": "2010-01-01",
"endDate": "2099-12-31",
"signDate": "2025-07-01",
"relatedMainContract": null,
"autoRenew": false,
"scanFileUrl": "https://files.example.com/supplier-contract/example.pdf",
"changeReason": "调整自动续约信息"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"contractId": "2095000000000000001",
"contractNo": "SHCT2025070102599446",
"status": "SIGNED",
"businessLine": null,
"relatedMainContract": null,
"autoRenew": false,
"updateTime": "2026-08-31 12:00:01"
}
}
```
#### 空数据 / 降级响应
仅提交 `changeReason` 可把全部业务字段清空;与当前内容完全相同时拒绝空更新,不生成新版本。
#### 错误响应
```json
{
"code": 395054,
"message": "合同有效期开始日期不能晚于结束日期",
"success": false,
"data": null
}
```
#### 业务边界
- 页面隐藏兼容字段时,编辑既有合同应从详情原样回传其 `contractType`、`amount`、`pricingMode`、`settleCycle`、`remark`,避免完整替换误清历史值。
- `autoRenew=false` 是有效业务值,不能按空值过滤。
- `expectedUpdateTime` 可省略;提供过期值时返回 `395014`,合同保持不变。
### 3. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
**VO**: `SupplierBasicInfoRespVO / SupplierContractRespVO`
#### 使用场景
进入供应商编辑或详情页时,从 `data.contracts[]` 回显合同表格和附件操作。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.contracts` | Array | 当前有效合同,按合同 ID 升序 |
| `data.contracts[].businessLine` | String/null | 业务线 |
| `data.contracts[].relatedMainContract` | String/null | 关联主合同 |
| `data.contracts[].autoRenew` | Boolean/null | 自动续约三态值 |
| `data.contracts[].status` | String/null | `SIGNED` 显示为“已签约” |
| `data.contracts[].scanFileUrl` | String/null | 单个合同附件永久地址 |
| 其余合同字段 | 对应类型/null | 与新增、更新响应一致 |
#### 请求示例
```http
GET /admin/supplier/items/2095000000000000000/basic-info/view
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2095000000000000000",
"contracts": [
{
"contractId": "2095000000000000001",
"contractNo": "SHCT2025070102599446",
"status": "SIGNED",
"contractName": "内蒙古牵手草原国际旅行社有限公司额外后返",
"businessLine": null,
"startDate": "2010-01-01",
"endDate": "2099-12-31",
"signDate": "2025-07-01",
"relatedMainContract": "2185933",
"autoRenew": true,
"scanFileUrl": "https://files.example.com/supplier-contract/example.pdf"
}
]
}
}
```
#### 空数据 / 降级响应
没有合同记录时 `contracts=[]`;合同存在但某项未登记时,对应字段为 `null`。
#### 错误响应
```json
{
"code": 395001,
"message": "供应商不存在",
"success": false,
"data": null
}
```
#### 业务边界
- 日期均为上海业务日字符串,前端按 `yyyy-MM-dd` 原样展示,不做 UTC 日期转换。
- 查询只返回未删除合同;附件为空时隐藏预览、下载操作。
- 未登录请求由 Gateway 返回业务码 `401`。
## 四、契约约束与正确调用方式
1. 表格列固定映射为 `contractNo`、`status`、`contractName`、`businessLine`、`startDate/endDate`、`signDate`、`relatedMainContract`、`autoRenew`、`scanFileUrl`;`SIGNED` 展示“已签约”。
2. `startDate/endDate` 直接按上海日历日拼成有效期;`autoRenew=true` 可显示“到期自动续约”,`false` 显示“否”,`null` 显示未登记占位。
3. 附件复用文件中心:申请上传凭证 `POST /admin/file/upload/token`,按返回模式上传后调用 `POST /admin/file/upload/confirm`,把确认响应的 `data.ossUrl` 写入 `scanFileUrl`。组件仅保留一个 `.doc`、`.docx` 或 `.pdf` 文件。
4. 预览使用带登录态的 `GET /admin/file/preview-by-url?url={encodeURIComponent(scanFileUrl)}`;下载直接访问 `scanFileUrl`。
5. 本页面隐藏 `contractType`、`amount`、`pricingMode`、`settleCycle`、`remark`;编辑历史合同时仍从详情原样回传,避免完整替换清空旧值。
## 五、数据库行为
这里只约定外部可观察结果:新增或更新成功后,再次请求基本信息详情可读到相同字段;未填写值返回 `null`,`autoRenew=false` 不会丢失。失败响应不产生部分合同变更。
## 六、边界行为
- 所有合同业务字段可空,只有新增、更新、删除操作的 `changeReason` 必填。
- `SIGNED` 仅是新增可写状态,不改变既有 `DRAFT`、`ACTIVE`、`EXPIRED` 和历史 `TERMINATED` 的兼容读取。
- 日期两端都有值且开始日晚于结束日时返回 `395054`,整次更新零写入。
- 附件字段为单值;前端负责限制为一个 Word/PDF,并在替换附件后提交新的永久地址。
- 写接口沿用既有角色、权限、幂等、锁和审计规则,无新增错误码。
## 六.6、修改前后对比
| 项目 | 修改前 | 修改后 |
|---|---|---|
| 业务线、关联主合同、自动续约 | 无字段 | 新增 `businessLine`、`relatedMainContract`、`autoRenew`,均可空 |
| 合同状态 | 可写 `DRAFT` / `ACTIVE` / `EXPIRED` | 追加可写 `SIGNED`(已签约) |
| 详情回读 | 无新增三字段 | `contracts[]` 完整回读新增三字段 |
| 其他合同业务字段 | 可空 | 继续可空,语义不变 |
## 六.7、影响评估
- **是否破坏向后兼容**:否,均为可选字段和状态枚举扩展。
- **前端是否必须同步上线**:是,需要新增列、附件控件和 `SIGNED` 中文映射,并隐藏非目标字段。
- **前端 workaround 清理点**:不要在本地拼装缺失字段;以详情 `contracts[]` 为回显真相。
## 七、不影响范围
- 不修改供应商主体、联系人、资质、结算账户、审批和归档接口。
- 不把合同并回供应商注册聚合;仍先取得 `supplierId`,再调用独立合同接口。
- 不新增 Gateway 路由、权限点、配置、Redis 或 MQ 契约。
## 八、测试环境已验证
- 已在 TEST Gateway 验证完整字段和仅含 `changeReason` 的合同均可新增并跨请求回读;`SIGNED`、`null`、`false`、日期错误零写入均符合本契约。
- 已验证一个 PDF 经文件中心上传后可预览、下载,验收产生的合同、附件和会话均已清理。
## 当前状态
- 后端:已部署并已验证。
- 前端:待处理。
## 十、相关文档
- Issue:[#6842](https://git.1814.love:8443/wx/HL/issues/6842)
- 后端 PR:[#6858](https://git.1814.love:8443/wx/HL/pulls/6858)
- 合并提交:[5546d790](https://git.1814.love:8443/wx/HL/commit/5546d7907c6eda9b372b4639094f84e568de6496)
## 关联 / 联系人
- **Issue**: [#6842](https://git.1814.love:8443/wx/HL/issues/6842)
- **PR**: [#6858](https://git.1814.love:8443/wx/HL/pulls/6858)
- **后端负责人**: @lc
@@ -0,0 +1,877 @@
---
schema: "hl-changelog/v2"
ticket: "6843"
title: "供应商暂停合作联动停用车队与派单拦截"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "f1b861c6"
target_release: "v2.1"
verified_at: "2026-08-31"
status_note: "PR #6868 已合并 dev-v3(merge commit df78cce5)已部署测试服。前端已消费:useAssignFlow 补 605074 专用分支「车队已停用不可派新单,请先启用车队或换车」;precheck 前端无调用方白名单属 no-op;司机候选静默排除无适配量。"
updated_at: "2026-08-31"
base: "dev-v3"
---
# 车务派单:供应商暂停合作联动停用车队,派单全链路拦截停用车队资源
> **存放目录**:
> - 二期(v3,`order-v3` 标签的工单)→ `changelogs-v2/2026-08/`
>
> **服务**: hl-fleet-service(端口 8087/8187,双实例滚动)
> **PR**: #6868
> **Issue**: #6843
> **作者**: wx
> **更新时间**: 2026-08-31
> **影响范围**: 管理后台「车务 - 派单」候选查询 / 预校验 / 创建(批量)/ 改派;新增服务间 internal 联动端点(前端不直接调用)
供应商暂停合作(SUSPENDED)/拉黑(BLACKLIST)后,其名下在启车队被批量联动停用;停用车队的车辆与常驻司机不再可派新单。**已派订单不受影响**(存量派单的最终确认、撤销取消不挂新校验)。
---
## ⚠️ 关键变化
- **precheck 新增 warning type `fleet_team_disabled`**:`POST /admin/fleet/assignments/precheck` 的 `warnings[]` 新增该类型(文案「车队已停用不可派新单,提交派单将被拦截,请先启用车队或换车」)。前端若按 type 白名单渲染提示,需把新 type 加入渲染映射,否则该提示会被静默丢弃。
- **新错误码 605074**:「车队已停用不可派新单(请先启用车队或换车)」。派单创建(批量,单派同路径)与改派在写库前硬拦截;前端错误码字典需补 605074 文案映射,建议引导动作 = 启用车队或更换车辆/司机。
- **司机候选静默排除**:`POST /admin/fleet/assignments/candidates` 的司机候选不再返回「常驻车所属车队已停用」的司机(无新增/删除字段,结果集合缩小;车辆侧自 #6717 起已排除)。
- **新增 internal 端点** `POST /internal/fleet-teams/supplier-status-sync`:仅供供应商域(resource 侧,工单 #6844)Feign 调用,**前端不直接调用、网关不路由**;本文按模板写全契约供服务间对齐。
---
## 一、背景
工单 #6843:供应商生命周期进入「停止合作」态(SUSPENDED=暂停合作 / BLACKLIST=拉黑)时,其名下关联车队应联动停用,且停用车队的司机/车辆不得再派新订单。
前置条件已由 #6717 建好:车队主数据持有 `supplierId`/`supplierName` 快照,车队停用会合并车辆可派状态(车辆侧候选过滤与 605037 拦截已存在)。本单补齐两环:
1. **联动入口(fleet 侧接收端)**:新增 internal 端点接收供应商域的状态同步事件,CAS 批量停用该供应商名下 `status=ACTIVE` 的车队。与人工停用的差异:供应商侧停用是**强制联动**,不做「在役车辆」守卫(车队不能再接新单与名下车辆是否仍在役无关)。
2. **派单链路拦截**:候选查询排除(司机侧新增)+ precheck 预警(新增 type)+ 创建/改派锁内硬校验(新错误码 605074,先于车辆/司机自身状态校验 605037/605038 执行)。
供应商侧通知发出端由工单 #6844 实现(本 PR 合并后指派),届时按本文 §三.1 契约 Feign 投递。车队域暂无独立操作日志表,停用结果经响应回执 + 服务端日志留痕。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 供应商状态同步(内部) | POST | `/internal/fleet-teams/supplier-status-sync` | 新增接口 | SUSPENDED/BLACKLIST 批量停用名下在启车队;同 eventId 重投返 duplicate=true 成功回执 |
| 2 | 查询派单候选资源 | POST | `/admin/fleet/assignments/candidates` | 行为变化 | 司机候选排除常驻车所属车队已停用的司机(无字段变化,集合缩小) |
| 3 | 派单预校验冲突 | POST | `/admin/fleet/assignments/precheck` | 响应新增枚举值 | `warnings[]` 新增 type `fleet_team_disabled`(只提示不阻断) |
| 4 | 批量创建派单 | POST | `/admin/fleet/assignments/batch` | 新增错误码 | 车队停用硬校验 605074,先于 605037/605038;单派/批量同路径 |
| 5 | 修改派单(改派) | POST | `/admin/fleet/assignments/{assignmentId}/change` | 新增错误码 | 换车/换司机到停用车队资源被 605074 拦截;纯费用调整司机侧不查 |
---
## 三、接口详情
### 1. 供应商状态同步(内部) `POST /internal/fleet-teams/supplier-status-sync`
**VO**: `SupplierStatusSyncReqVO` / `Result<SupplierStatusSyncRespVO>`
#### 使用场景
供应商域(resource 侧,工单 #6844)在供应商生命周期变更为 SUSPENDED/BLACKLIST 后,经 Feign LB 直连投递本端点;fleet 侧批量停用该供应商名下 `status=ACTIVE` 的车队,让派单链路(候选排除 + 创建/改派硬校验 605074)不再放行其资源。
`/internal` 前缀**不经网关路由**(网关实测返回业务码 404「接口不存在」),`X-Internal-Token` 由 InternalAuthFilter 统一校验。前端不直接调用本端点。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `supplierId` | Body | String(Long) | 是 | `@Positive` | 供应商 ID(雪花,建议按字符串传输,禁止转 JavaScript Number) |
| `targetStatus` | Body | String | 是 | ≤32,大小写不敏感 | 供应商目标状态;仅 `SUSPENDED`/`BLACKLIST` 触发联动停用,其余状态忽略 |
| `eventId` | Body | String | 是 | ≤64 | 事件 ID(幂等键,同一事件重复投递不产生二次变更) |
| `occurredAt` | Body | String/null | 否 | ISO 日期时间 | 事件发生时间(观测/日志用;本端不落供应商状态镜像,不做乱序围栏) |
#### 出参 `Result<SupplierStatusSyncRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `supplierId` | String | 供应商 ID(回显,雪花序列化为字符串) |
| `targetStatus` | String | 供应商目标状态(回显调用方传值) |
| `ignored` | Boolean | 是否忽略(目标状态非 SUSPENDED/BLACKLIST,未做联动) |
| `duplicate` | Boolean | 是否重复投递(true=同 eventId 幂等窗口内已消费,不产生二次变更;重复回执 `disabledCount` 恒 0、清单恒空) |
| `disabledCount` | Integer | 本次实际停用的车队数(CAS 实际影响行数;并发人工停用时可能小于快照大小;重复投递为 0) |
| `disabledTeamIds` | String[] | 本次停用的车队 ID 列表(停用前快照,雪花字符串,供调用方核对) |
#### 请求示例
```json
{
"supplierId": "8996843000000000001",
"targetStatus": "SUSPENDED",
"eventId": "supplier-status-8996843000000000001-SUSPENDED-20260831142000",
"occurredAt": "2026-08-31T14:20:00"
}
```
(传输头:`POST /internal/fleet-teams/supplier-status-sync`,`Content-Type: application/json`,`X-Internal-Token: <服务间令牌>`,Feign LB 直连不经网关。)
#### 响应示例
首次投递(TEST 实测:该供应商名下 1 个 ACTIVE 车队被停用,名下有在役车辆仍强制停用——供应商联动不做在役车辆守卫):
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "8996843000000000001",
"targetStatus": "SUSPENDED",
"ignored": false,
"duplicate": false,
"disabledCount": 1,
"disabledTeamIds": ["352696323818000384"]
}
}
```
#### 空数据 / 降级响应
- 目标状态非停止合作类(如恢复 ACTIVE):不联动、零写入,正常返回 `ignored=true`、`disabledCount=0`、`disabledTeamIds=[]`(恢复合作不自动启用是本单边界,车队需人工启用,安全侧)。
- 供应商名下无 ACTIVE 车队(含窗口外重投、车队已被人工停用):CAS 守卫自然命中 0 行,返回 `ignored=false`、`disabledCount=0`、`disabledTeamIds=[]`,仍属成功回执,调用方不应告警。
- 同 `eventId` 幂等窗口(24h)内重复投递:返回 `duplicate=true` 的成功回执(「已消费」语义),`disabledCount=0`、`disabledTeamIds=[]`(首次投递的实际停用明细不随幂等键留存,重复回执只承诺「已处理」);供应商侧「失败即重试告警」不应被触发。
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "8996843000000000001",
"targetStatus": "SUSPENDED",
"ignored": false,
"duplicate": true,
"disabledCount": 0,
"disabledTeamIds": []
}
}
```
#### 错误响应
缺少必填字段(Bean Validation,TEST 实测缺 `eventId`):
```json
{ "code": 400, "message": "事件ID不能为空", "success": false, "data": null }
```
`X-Internal-Token` 缺失或错误(InternalAuthFilter 拦截,原始响应为过滤器直出、字段名为 `msg`,TEST 实测):
```json
{ "code": 403, "msg": "内部接口禁止外部访问" }
```
经网关误调(`/internal/**` 无网关路由,TEST 实测 HTTP 200 传输层 + 业务码 404):
```json
{ "code": 404, "message": "接口不存在: /internal/fleet-teams/supplier-status-sync", "success": false, "data": null }
```
#### 业务边界
- 幂等双保险:① `@Idempotent` 按 `eventId` 开窗 24h(覆盖 MQ/Feign 延迟重投),窗口内重投由 Controller 转 `duplicate=true` 成功回执;② 窗口外重投由 Mapper CAS 守卫(仅停用 `status=ACTIVE` 行)兜底,自然命中 0 行。
- 状态匹配大小写不敏感(防调用方传小写被静默忽略);回执原样回显调用方传值。
- 与人工停用的差异:供应商联动是强制停用,**不做在役车辆守卫**;存量派单/槽位/占用不在联动范围。
- 本端不落供应商状态镜像、不做乱序围栏——迟到旧事件依赖调用方按事件流顺序投递。
- `supplierId`/`disabledTeamIds[]` 均按字符串序列化(`@JsonSerialize(ToStringSerializer)`),禁止转 JavaScript Number。
---
### 2. 查询派单候选资源 `POST /admin/fleet/assignments/candidates`
**VO**: `AssignmentCandidateReqVO` / `Result<AssignmentCandidateRespVO>`
#### 使用场景
派单 Step2 选车/选司机时查询候选资源。**本单行为变化**:司机候选排除「常驻车所属车队已停用」的司机(TEST 实测:司机斯琴常驻车挂到停用车队后,候选从 1 条变为 0 条;重新启用车队后恢复 1 条)。车辆侧排除自 #6717 起已存在(车队停用合并车辆可派状态),本单未改。
请求/响应**结构零字段变化**——排除是静默的,不返回被排除司机及其原因。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `startDate` | Body | String(日期) | 是 | `yyyy-MM-dd` | 用车开始日期 |
| `endDate` | Body | String(日期) | 是 | `yyyy-MM-dd`,≥startDate | 用车结束日期 |
| `orderId` | Body | String(Long)/null | 否 | - | 当前订单 ID;改派排除自身时必填 |
| `requirementId` | Body | String(Long)/null | 否 | - | 当前用车需求 ID;改派排除自身时必填 |
| `fleetItemIndex` | Body | Integer/null | 否 | ≥0 | 需求车型项序号 |
| `vehicleKeyword` | Body | String/null | 否 | - | 车辆关键词(车牌/车型/常驻司机姓名或完整手机号) |
| `driverKeyword` | Body | String/null | 否 | - | 司机关键词(姓名/完整手机号/常驻车牌) |
| `fleetTeamId` | Body | String(Long)/null | 否 | - | 按车队过滤车辆候选(雪花字符串) |
| `vehicleTypeId` | Body | String(Long)/null | 否 | - | 按车型大类过滤 |
| `driverAvailability` | Body | String | 否 | `AVAILABLE`/`ALL`,默认 `ALL` | 司机可用性筛选 |
| `selectedVehicleId` | Body | String(Long)/null | 否 | - | 已选车辆 ID(先选车后选司机场景) |
| `selectedDriverId` | Body | String(Long)/null | 否 | - | 已选司机 ID(先选司机后选车场景) |
| `excludeAssignmentId` | Body | String(Long)/null | 否 | 须属当前订单与需求 | 改派时排除的当前派单 ID |
#### 出参 `Result<AssignmentCandidateRespVO>`
顶层字段(与改前完全一致,无新增/删除):
| 字段 | 类型 | 说明 |
|------|------|------|
| `vehicles` | Object | 车辆候选分页(`records[]` + `total`);停用车队名下车辆继续被排除(#6717 既有口径) |
| `drivers` | Object | 司机候选分页(`records[]` + `total`);**本单起追加排除常驻车所属车队已停用的司机** |
| `fleetTeamFacets` | Array | 车队聚合筛选项 |
| `vehicleTypeFacets` | Array | 车型大类聚合筛选项 |
| `selectedDriverResidentVehicle` | Object/null | 已选司机的常驻车信息 |
| `selectedDriverResidentVehicles` | Array | 已选司机的全部常驻车 |
| `selectedVehicleResidentDriver` | Object/null | 已选车辆的常驻司机 |
| `selectedRelation` | Object/null | 已选车+司机的常驻关系 |
| `suggestedDriverId` | String(Long)/null | 自动代入司机 ID(雪花字符串) |
| `suggestedDriverReason` | String | 自动代入原因码 |
| `suggestedDriverMessage` | String | 自动代入原因展示文案 |
| `canonicalSnapshot` | Object | Step2 canonical 快照(代际/版本/槽位集合) |
`drivers.records[]` 项关键字段(结构未变,列与联调相关者):
| 字段 | 类型 | 说明 |
|------|------|------|
| `driverId` | String | 司机 ID(雪花字符串,禁止转 Number) |
| `name` | String | 司机姓名 |
| `maskedPhone` | String | 司机手机(脱敏,如 `135****5001`) |
| `driverStatus` | String | 占用态(`idle`/`busy`/`rest`/`pending`) |
| `season` | String | 赛季态(候选恒 `active`,其余赛季已过滤) |
| `residentVehicleId` | String/null | 常驻车辆 ID(常驻车所属车队停用时,该司机整体不出现在候选) |
| `residentVehiclePlate` | String/null | 常驻车辆车牌 |
| `available` | Boolean | 是否覆盖整个请求日期范围可用 |
| `selectable` | Boolean | 本次选车场景下是否可被选中 |
#### 请求示例
```http
POST /admin/fleet/assignments/candidates
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"startDate": "2026-09-10",
"endDate": "2026-09-12",
"driverKeyword": "斯琴"
}
```
#### 响应示例
司机常驻车所属车队**启用**时(TEST 实测基线,斯琴在候选内):
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"drivers": {
"total": 1,
"records": [
{
"driverId": "2065272145289658370",
"name": "斯琴",
"maskedPhone": "135****5001",
"years": 17,
"driverStatus": "idle",
"season": "active",
"licenseType": "A3",
"licenseExpired": false,
"available": true,
"selectable": true
}
]
}
}
}
```
#### 空数据 / 降级响应
车队停用后同一查询(TEST 实测:`records=[]`、`total=0`、`code=200`)——排除是静默的,响应结构不变、不附带被排除原因;前端按「无匹配司机」渲染即可,无需特殊处理。车辆候选同理(车队停用后名下车辆不再返回)。
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"drivers": { "total": 0, "records": [] }
}
}
```
#### 错误响应
缺少必填日期(Bean Validation):
```json
{ "code": 400, "message": "用车开始日期不能为空", "success": false, "data": null }
```
未登录(网关拦截):
```json
{ "code": 401, "message": "未登录", "success": false, "data": null }
```
#### 业务边界
- 司机侧排除口径 = 常驻关系(`fleet_vehicle.primary_driver_id`)反查:司机任一常驻车所属车队为 DISABLED 即被排除,与司机列表「所属车队」筛选同源(同一反查端口,防口径漂移)。
- **无常驻车的司机不受车队联动约束**(司机档案无车队列,本期不加列)。
- 排除只读过滤,不写任何表;被排除司机在司机档案/列表页不受影响,仅派单候选场景不可见。
- 车队长停用后重新启用,候选即时恢复(无缓存延迟,TEST 实测启用后同查询恢复 1 条)。
---
### 3. 派单预校验冲突 `POST /admin/fleet/assignments/precheck`
**VO**: `PrecheckReqVO` / `Result<PrecheckRespVO>`
#### 使用场景
Step2 选定车辆 + 司机后、保存前预探冲突。**本单变化**:`warnings[]` 新增 type `fleet_team_disabled`——车辆所属车队停用、或司机常驻车所属车队停用时给出预警(对齐创建/改派锁内 605074 硬校验,precheck 只提示不抛)。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `vehicleId` | Body | String(Long) | 是 | 雪花字符串 | 车辆 ID |
| `driverId` | Body | String(Long) | 是 | 雪花字符串 | 司机 ID |
| `startDate` | Body | String(日期) | 是 | `yyyy-MM-dd` | 用车开始日期(闭区间起点) |
| `endDate` | Body | String(日期) | 是 | `yyyy-MM-dd` | 用车结束日期(闭区间终点) |
| `orderId` | Body | String(Long)/null | 否 | - | 订单 ID(仅日志/上下文) |
| `pickupAt` | Body | String/null | 否 | - | 接客地(城市衔接判定用) |
| `dropoffAt` | Body | String/null | 否 | - | 送客地(城市衔接判定用) |
| `headcount` | Body | Integer/null | 否 | ≥0 | 人数(座位不足 warning 判定用) |
| `excludeAssignmentId` | Body | String(Long)/null | 否 | - | 改派预校验排除自身 |
#### 出参 `Result<PrecheckRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `conflict` | Boolean | 是否存在阻断性冲突;资源不可用(含车队停用)时为 true,此时阻断原因在 `warnings[]` 体现、`conflicts[]` 可为空 |
| `conflicts` | Array | 冲突明细(`type`/`conflictAssignmentId`/`conflictOrderNo`/`conflictDateRange`/`cityJunctionShareCandidate`/`msg`),无冲突为空数组 |
| `warnings` | Array | 非阻断提示(`type` + `msg`);**本单新增 type 值 `fleet_team_disabled`** |
`warnings[]` 项字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `type` | String | 提示类型;新增 `fleet_team_disabled`=车队已停用(全集见 §六.5) |
| `msg` | String | 提示文案,新 type 恒为「车队已停用不可派新单,提交派单将被拦截,请先启用车队或换车」 |
#### 请求示例
```http
POST /admin/fleet/assignments/precheck
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"vehicleId": "2094307514693648385",
"driverId": "2065272145289658370",
"startDate": "2026-09-10",
"endDate": "2026-09-12"
}
```
#### 响应示例
车队停用后(TEST 实测:车辆侧 `vehicle_unavailable` 与新增 `fleet_team_disabled` 同时出现,资源不可用使 `conflict=true` 但 `conflicts=[]`):
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"conflict": true,
"conflicts": [],
"warnings": [
{ "type": "vehicle_unavailable", "msg": "车辆处于维保或停用状态" },
{ "type": "fleet_team_disabled", "msg": "车队已停用不可派新单,提交派单将被拦截,请先启用车队或换车" }
]
}
}
```
#### 空数据 / 降级响应
无冲突无提示(车队启用基线,TEST 实测):`conflict=false`、`conflicts=[]`、`warnings=[]`、`code=200`。precheck 为只读咨询,恒 code=200 不抛业务异常;车辆/司机不存在时也不抛错,而是落 `vehicle_unavailable`/`driver_unavailable` warning + `vehicle_not_found`/`driver_not_found` conflict 明细(既有 #5629 口径)。
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": { "conflict": false, "conflicts": [], "warnings": [] }
}
```
#### 错误响应
缺少必填车辆 ID(Bean Validation):
```json
{ "code": 400, "message": "车辆 ID 不能为空", "success": false, "data": null }
```
未登录(网关拦截):
```json
{ "code": 401, "message": "未登录", "success": false, "data": null }
```
#### 业务边界
- precheck 只提示不阻断提交动作本身;最终一致以 create/change 锁内重校验(605074)为准——车务忽略预警强行提交会被 605074 拦截。
- 车辆所属车队行缺失(历史数据无车队)时 `fleet_team_disabled` 不触发,仍走 `vehicle_unavailable` 既有口径。
- 司机侧判定 = 常驻车所属车队停用;无常驻车司机不产生本 warning。
- 前端若按 type 白名单渲染 warning,需把 `fleet_team_disabled` 加入映射(建议样式与 `vehicle_unavailable` 同级:醒目提示 + 引导换车/启用车队)。Swagger 注解中的 type 枚举列举未同步新值,以本文 §六.5 全集为准。
---
### 4. 批量创建派单 `POST /admin/fleet/assignments/batch`
**VO**: `BatchCreateAssignmentReqVO` / `Result<BatchAssignmentWriteRespVO>`
#### 使用场景
派单 Step2 提交(单派/批量同路径,经同一锁内创建逻辑)。**本单变化**:新增车队状态硬校验——所选车辆所属车队、或所选司机常驻车所属车队非 ACTIVE 时,写库前抛 **605074**,先于车辆/司机自身状态校验(605037/605038)执行,避免车队停用被误报成「车辆维保/停用」。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `orderId` | Body | String(Long) | 是 | 雪花字符串 | 订单 ID |
| `requirementId` | Body | String(Long) | 是 | 雪花字符串 | 用车需求 ID |
| `startDate` | Body | String(日期) | 是 | `yyyy-MM-dd` | 用车开始日期 |
| `endDate` | Body | String(日期) | 是 | `yyyy-MM-dd` | 用车结束日期 |
| `requestId` | Body | String | 是 | 非空,幂等键 | 批次幂等请求标识(重复提交同 requestId + 同载荷幂等放行) |
| `items` | Body | Array | 是 | 每项含 `fleetItemIndex`/`vehicleId`/`driverId`(均必填) | 派车明细;`vehicleId`/`driverId` 为雪花字符串 |
| `pickupAt` | Body | String/null | 否 | - | 接客地 |
| `dropoffAt` | Body | String/null | 否 | - | 送客地 |
| `headcount` | Body | Integer/null | 否 | ≥0 | 乘客人数(不含司机) |
| `chargeableServiceDates` | Body | Array/null | 否 | - | 收取车费的服务日期 |
| `confirmCrossResident` | Body | Boolean/null | 否 | - | 跨常驻车派单显式确认 |
#### 出参 `Result<BatchAssignmentWriteRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `assignments` | Array | 创建结果列表(`fleetItemIndex` + `assignment` 写入结果,含 `assignmentId` 雪花字符串) |
| `failedFleetItemIndex` | Integer/null | 失败的需求项序号(整批事务回滚,任一失败全部不生效) |
| `dailyDifferences` | Array/null | 逐日差异提示 |
#### 请求示例
```http
POST /admin/fleet/assignments/batch
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"orderId": "2087157006417657857",
"requirementId": "2087157006845476865",
"startDate": "2026-09-10",
"endDate": "2026-09-12",
"headcount": 4,
"requestId": "web-step2-<uuid>",
"items": [
{ "fleetItemIndex": 0, "vehicleId": "2094307514693648385", "driverId": "2065272145289658370" }
]
}
```
#### 响应示例
成功(结构示例;本次 TEST 未跑通正向创建,原因见 §八):
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"assignments": [
{
"fleetItemIndex": 0,
"assignment": { "assignmentId": "2094310000000000001", "assignmentStatus": "assigned" }
}
],
"failedFleetItemIndex": null,
"dailyDifferences": null
}
}
```
#### 空数据 / 降级响应
本接口为写接口,无空数据分支;整批任一 item 失败即同事务回滚,不产生半批状态。幂等重试(同 `requestId` + 同载荷)返回首个成功结果,不重复建单。
#### 错误响应
车队已停用(新增;车辆所属车队或司机常驻车所属车队非 ACTIVE,写库前拦截、零写入):
```json
{ "code": 605074, "message": "车队已停用不可派新单(请先启用车队或换车)", "success": false, "data": null }
```
行程已结束的需求不可再派(既有口径,TEST 实测优先级高于 605074——过期需求先撞本码):
```json
{ "code": 605047, "message": "行程已结束,派车信息只读,不能修改或改派", "success": false, "data": null }
```
车辆自身维保/停用(既有;车队校验通过后才轮到本码):
```json
{ "code": 605037, "message": "车辆处于维保或停用状态,不能派车", "success": false, "data": null }
```
#### 业务边界
- 校验顺序(锁内):需求项占用守卫 → **车队状态(605074)** → 车辆状态(605037)→ 司机状态(605038)→ 司机赛季(605006/605013)→ 跨常驻确认 → 档期重叠。
- 只拦「产生新占用」的创建入口;存量派单的最终确认/撤销取消不挂本校验——供应商事后停用不卡死在途单。
- 车队行缺失(历史车辆无车队)不归 605074,仍由合并后车辆状态走 605037 老口径。
- 幂等:同 `requestId` 重放返回首个结果;分布式锁串行化同需求写入。
- 行程已结束(605047)、需求版本过期(605905)等前置守卫先于车队校验命中。
---
### 5. 修改派单(改派) `POST /admin/fleet/assignments/{assignmentId}/change`
**VO**: `ChangeAssignmentReqVO` / `Result<ChangeAssignmentRespVO>`
#### 使用场景
车务对在途派单换车/换司机/调整费用。**本单变化**:改派目标为停用车队资源时写库前抛 **605074**(TEST 实测:把在途派单改到停用车队的车辆+司机,返回 605074 且原派单零副作用)。纯费用调整(车人未换)不查司机常驻车队——在途单不因司机另一辆常驻车挂的车队被供应商事后停用而卡死费用修订。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `assignmentId` | Path | String(Long) | 是 | 雪花字符串 | 目标派单 ID |
| `effectiveDate` | Body | String(日期) | 是 | 须在派单服务日期范围内 | 改派生效日期 |
| `newVehicleId` | Body | String(Long)/null | 否 | 与 `newDriverId` 至少其一 | 新车辆 ID(雪花字符串) |
| `newDriverId` | Body | String(Long)/null | 否 | 与 `newVehicleId` 至少其一 | 新司机 ID(雪花字符串) |
| `reason` | Body | String | 是 | 非空 | 修改原因 |
| `requestId` | Body | String | 是 | 非空,幂等键 | 请求幂等 ID |
| `serviceDates` | Body | Array/null | 否 | - | 指定改派的服务日期子集 |
| `vehicleFeeTotal` | Body | Number/null | 否 | ≥0 | 手工总车费(需搭配调整原因) |
| `confirmCrossResident` | Body | Boolean/null | 否 | - | 跨常驻车派单显式确认 |
#### 出参 `Result<ChangeAssignmentRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `assignmentId` | String | 新派单 ID(雪花字符串) |
| `assignmentSlotId` | String | 槽位 ID(雪花字符串) |
| `previousAssignmentGroupId` | String/null | 原派车组 ID |
| `newAssignmentGroupId` | String/null | 新派车组 ID |
| `assignmentStatus` | String | 新派单状态 |
| `effectiveDate` | String(日期) | 改派生效日期 |
| `affectedDays` | Integer | 影响天数 |
| `vehicleFeeTotal` | String(Number)/null | 最终总车费 |
| `dailyVehicleFees` | Array/null | 逐日车费 |
| `warningCode` | String/null | 预警码 |
| `warningMessage` | String/null | 预警文案 |
#### 请求示例
```http
POST /admin/fleet/assignments/2091068072742756354/change
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"effectiveDate": "2026-09-02",
"newVehicleId": "2094307514693648385",
"newDriverId": "2065272145289658370",
"reason": "车队停用前换车",
"requestId": "web-change-<uuid>"
}
```
#### 响应示例
成功(结构示例;本次 TEST 的改派请求按预期被 605074 拦截,未产生成功样本):
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"assignmentId": "2094310000000000002",
"assignmentSlotId": "2094310000000000003",
"assignmentStatus": "assigned",
"effectiveDate": "2026-09-02",
"affectedDays": 1
}
}
```
#### 空数据 / 降级响应
本接口为写接口,无空数据分支;幂等重放(同 `requestId` + 同载荷)返回首个成功回执,不重复改派。改派目标派单自身已取消/已完成时不归本单变化范围,走既有 605066「派单已取消,无法修改,请重新派车」。
#### 错误响应
改派目标资源所属车队已停用(新增,TEST 实测原文;写库前拦截、原派单零副作用):
```json
{ "code": 605074, "message": "车队已停用不可派新单(请先启用车队或换车)", "success": false, "data": null }
```
生效日期不在派单服务日期范围内(既有):
```json
{ "code": 605028, "message": "生效日期不在派单服务日期范围内", "success": false, "data": null }
```
派单已取消(既有):
```json
{ "code": 605066, "message": "派单已取消,无法修改,请重新派车", "success": false, "data": null }
```
#### 业务边界
- 车队校验先于 605037/605038 执行(与创建同口径);车辆侧车队停用必拦(含纯费用调整场景——该场景 #6717 起本就被合并状态 605037 拦截,此处只是把报错换成更准的 605074)。
- 纯费用调整(`identityUnchanged`,车人未换)时**司机侧不查常驻车队**:在途单司机的另一辆常驻车被联动停用,不影响本单费用修订(「已派订单不受影响」边界)。
- 换车/换司机(非纯费用调整)时司机常驻车队停用 → 605074。
- 幂等:同 `requestId` 重放不重复改派;车锁/司机锁串行化。
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | 调用 / 结果 |
|---|---|
| ✅ 供应商域投递停止合作事件 | `{"supplierId":"...","targetStatus":"SUSPENDED","eventId":"<全局唯一>","occurredAt":"..."}` + `X-Internal-Token` 头,Feign LB 直连 |
| ✅ `targetStatus` 传小写 `suspended` | 大小写不敏感,正常触发联动停用 |
| ✅ 同 `eventId` 网络重试重投 | 返 `duplicate=true` 成功回执,按已消费处理,**不要告警重试** |
| ✅ 非停止合作态(如 ACTIVE 恢复) | 返 `ignored=true` 成功,不联动(恢复合作需人工启用车队) |
| ✅ 前端收到 605074 | 按文案引导:启用车队(车队管理页)或更换车辆/司机后重试 |
| ✅ precheck 渲染 `fleet_team_disabled` | 与 `vehicle_unavailable` 同级醒目提示;加入 type 白名单映射 |
| ❌ 前端直接调 `/internal/fleet-teams/supplier-status-sync` | 网关不路由(业务码 404);该端点仅供服务间 Feign 调用 |
| ❌ 调用方对 `duplicate=true` 回执告警重试 | 重复投递是「已消费」而非失败,告警重试会造成误报风暴 |
| ❌ 复用 `eventId` 投递不同事件 | 幂等键冲突会让新事件被当成已消费吞掉;`eventId` 必须全局唯一(建议含 supplierId+状态+时间戳) |
| ❌ 雪花 ID 转 JavaScript Number | `supplierId`/`disabledTeamIds[]`/`driverId`/`vehicleId`/`assignmentId` 均为雪花字符串,`Number()`/`parseInt()`/一元 `+` 会精度丢失 |
| ❌ 车务忽略 precheck 预警强行提交 | 创建/改派锁内 605074 硬拦截,零写入 |
### 关键提示(当前 TEST 构建)
- 605074 与 605037 的分工:605074 专指「车队级」停用(多因供应商 SUSPENDED/BLACKLIST 联动);605037 指车辆自身维保/停用。前端错误码字典两个都要保留。
- 候选排除是静默的:前端不需要也不可能在候选接口里展示「被排除的司机」;如业务需要解释「为什么某司机不可派」,引导至司机档案查看常驻车所属车队状态。
- 车队重新启用后(车队管理 → 启用,前提是已绑供应商),候选/预校验/创建/改派即时恢复,无缓存延迟。
---
## 五、数据库行为
- **internal 联动端点只写 `fleet_team` 行**:CAS 批量 update(`status` ACTIVE→DISABLED,`where` 带 `status=ACTIVE` 守卫),重复/并发自然命中 0 行;不跨服务写 `supplier_main`/`supplier_resource_rel`。
- **幂等键在 Redis**:`fleet:team:supplier-status-sync:{eventId}`,TTL 24h(@Idempotent 开窗),窗口内重投不再触库。
- **派单侧零表结构变更**:候选排除 = 查询过滤(只读);创建/改派 605074 = 锁内读校验(只读),不写任何额外表。
- **本工单无 Flyway 迁移**:不加表、不加列(车队-供应商关联列由 #6717 迁移建好)。
- 联动停用不影响存量 `fleet_assignment` 行(本单边界:不动在途派单/槽位/占用)。
---
## 六、边界行为
- 未登录/登录失效:业务码 `401`(网关拦截)。
- internal 端点 `X-Internal-Token` 缺失/错误:`403`(过滤器直出,字段名 `msg`)。
- 经网关误调 `/internal/**`:业务码 `404`「接口不存在」(无路由,符合预期)。
- Bean Validation 失败:`400`(如缺 `eventId`/`vehicleId`/`startDate`)。
- 车队已停用不可派新单:`605074`(新增;创建/改派写库前硬拦截)。
- 车辆维保/停用:`605037`(既有;车队校验通过后才会命中)。
- 司机休假/待激活:`605038`(既有;同上)。
- 行程已结束只读:`605047`(既有;过期需求/派单先于 605074 命中,TEST 实测)。
- 派单已取消不可改派:`605066`(既有)。
- 生效日期越出派单服务日期:`605028`(既有)。
- 车队已停用仍选该车(车队管理写口径):`601102`(既有,本单不改)。
- 跨服务 Feign 不可用:本单 fleet 侧链路不新增 Feign 调用(联动方向是 resource → fleet);候选/预校验/创建/改派均为库内读写,不受 resource 侧可用性影响。
---
## 六.5、枚举 / 数据字典
### `targetStatus`(供应商目标状态,internal 端点入参)
**所属字段**: `SupplierStatusSyncReqVO.targetStatus` | **类型**: `String`(大小写不敏感)
| 值 | 中文 | 说明 |
|----|------|------|
| `SUSPENDED` | 暂停合作 | **触发**联动停用名下在启车队 |
| `BLACKLIST` | 拉黑 | **触发**联动停用名下在启车队 |
| `DRAFT`/`VETTING`/`ACTIVE`/`FROZEN`/`ARCHIVED` | 草稿/审核中/合作中/冻结/归档 | 忽略(`ignored=true`),不联动 |
### `warnings[].type`(precheck 非阻断提示类型)
**所属字段**: `PrecheckRespVO.warnings[].type` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `seats_short` | 座位不足 | 既有 |
| `vehicle_unavailable` | 车不可用 | 既有(车辆不存在/维保/停用;车队停用合并车辆状态后同现) |
| `driver_unavailable` | 司机不可用 | 既有 |
| `driver_blacklisted` | 司机已黑名单 | 既有(提交将被 605006 拦截) |
| `driver_season_not_registered` | 司机非在册赛季 | 既有(提交将被 605013 拦截) |
| **`fleet_team_disabled`** | **车队已停用** | **本单新增**(提交将被 605074 拦截,引导启用车队或换车) |
| `cross_resident` | 跨常驻 | 既有 |
| `license_expired` | 驾照过期 | 既有 |
| `veh_inspect_expired` | 车辆年检过期 | 既有 |
| `veh_insure_expired` | 车辆保险过期 | 既有 |
### 车队状态(`fleet_team.status`)
**所属字段**: 车队管理相关响应 `status` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `ACTIVE` | 启用 | 名下资源可派;前提:已关联供应商(#6717/#6811 不变量) |
| `DISABLED` | 停用 | 名下车辆/常驻司机不可派新单(本单贯通到派单全链路) |
---
## 六.6、修改前后对比
### 字段/枚举级对比
| 项 | 改前 | 改后 |
|------|------|------|
| `POST /internal/fleet-teams/supplier-status-sync` | 不存在 | 新增 internal 端点(ReqVO 4 字段 / RespVO 6 字段) |
| `PrecheckRespVO.warnings[].type` 值域 | 9 种既有 type | 新增 `fleet_team_disabled` |
| 派单错误码 | 无车队级码(车队停用被合并状态误报 605037) | 新增 605074「车队已停用不可派新单(请先启用车队或换车)」 |
| `candidates` 请求/响应字段 | 现有结构 | 零字段变化(行为变化:司机候选集合缩小) |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 供应商暂停/拉黑 | 车队无感知,名下资源照常可派 | 联动停用名下在启车队(强制,不做在役车辆守卫) |
| 司机候选(常驻车挂停用车队) | 照常返回 | 静默排除 |
| precheck 车队停用提示 | 仅 `vehicle_unavailable`(车辆侧) | 追加 `fleet_team_disabled`(车+司机两侧) |
| 创建/改派选到停用车队资源 | 报 605037「车辆维保/停用」(语义不准);司机侧可漏过 | 统一报 605074(先于 605037/605038) |
| 在途单纯费用调整(司机另一常驻车车队被停用) | 司机侧无校验 | 仍不校验(刻意豁免,不卡死在途单) |
| 供应商恢复合作 | - | 不自动启用车队(人工启用,安全侧) |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 否。响应结构无字段删改;候选集合缩小属业务收口;新增 warning type 与错误码对旧前端为未知值——旧前端按通用兜底渲染即可,不崩。
- **前端是否必须同步上线**: 建议同批但不强阻断。两项适配:① precheck warning type 白名单加 `fleet_team_disabled`(不加则该预警被静默丢弃,车务要到提交时才知道被拦);② 错误码字典加 605074 文案与引导动作(不加则按通用错误提示展示)。
- **前端 workaround 清理点**: 无(此前无对应前端绕行逻辑)。
- **服务间依赖**: resource 侧 #6844 未上线前,internal 端点无生产流量,派单拦截仅对人工停用/无供应商自动停用的车队生效——同样符合预期。
---
## 七、不影响范围
- **仅影响**: 派单候选查询 / 派单预校验 / 派单创建(批量,单派同路径)/ 派单改派 4 个管理后台端点 + 1 个 internal 端点(fleet 侧)。
- **零影响**:
- 团批组派(GroupDispatch 独立体系,不写 `fleet_assignment`,PR 明示不在拦截范围)。
- 存量派单的最终确认 / 撤销取消 / 行程短信 / 软清等推进类写口(不挂车队状态新校验,供应商事后停用不卡死在途单)。
- 车队管理 CRUD 端点(`/admin/fleet/teams/**` 本单未改;启停用/供应商守卫口径仍属 #6717/#6811)。
- 司机档案 / 车辆档案 / 司机列表「所属车队」筛选(排除逻辑复用同一反查端口,档案域零改动感知)。
- 供应商域九大资源模块与供应商管理端点(联动发出端 #6844 另行落地)。
- 小程序端(派单链路为管理后台车务功能)。
- Gateway 路由(`/admin/fleet/assignments/**` 通配已覆盖;`/internal/**` 本就不经网关,无需新增配置)。
- Redis/MQ(仅新增幂等键 `fleet:team:supplier-status-sync:{eventId}`,无新消息)。
---
## 八、测试环境已验证
真实 TEST 环境实测(2026-08-31,网关 `https://api.test.1814.love:9443`,admin token 走 `/admin/auth/login`;internal 端点经 SSH 在测试服本机 curl `127.0.0.1:8087` 直连,模拟 Feign 调用方):
```
[internal 链路]
POST /internal/fleet-teams/supplier-status-sync SUSPENDED 首投
→ 200, ignored=false, duplicate=false, disabledCount=1, disabledTeamIds=["352696323818000384"] ✓
POST /internal/fleet-teams/supplier-status-sync 同 eventId 重投(幂等)
→ 200, duplicate=true, disabledCount=0, disabledTeamIds=[] ✓
POST /internal/fleet-teams/supplier-status-sync targetStatus=ACTIVE(非停合作态)
→ 200, ignored=true, disabledCount=0 ✓
POST /internal/fleet-teams/supplier-status-sync 缺 eventId
→ 400「事件ID不能为空」 ✓
POST /internal/fleet-teams/supplier-status-sync 错误 X-Internal-Token
→ 403「内部接口禁止外部访问」 ✓
POST 网关 /internal/fleet-teams/supplier-status-sync
→ 业务码 404「接口不存在」(/internal 不经网关路由,符合预期) ✓
[管理后台链路(admin token)]
GET /admin/fleet/teams/352696323818000384
→ status=DISABLED(联动生效,全栈贯通) ✓
POST /admin/fleet/assignments/candidates driverKeyword=斯琴
→ 停用前 records=1 条 → 联动停用后 records=[] total=0(新增排除生效) ✓
POST /admin/fleet/assignments/candidates vehicleKeyword=T6843
→ records=[](车辆侧 #6717 既有排除仍生效) ✓
POST /admin/fleet/assignments/precheck 停用车队车辆+常驻司机
→ warnings 含 {"type":"fleet_team_disabled","msg":"车队已停用不可派新单,提交派单将被拦截,请先启用车队或换车"} ✓
POST /admin/fleet/assignments/2091068072742756354/change 改派到停用车队车+司机
→ 605074「车队已停用不可派新单(请先启用车队或换车)」,原派单零副作用(事后核 update_time 未变) ✓
POST /admin/fleet/assignments/batch 停用车队车+司机(过期行程占位)
→ 605047「行程已结束」优先命中(前置守卫先于 605074,符合校验顺序) ✓
POST /admin/fleet/teams/352696323818000384/enable 重新启用
→ 200;同条件 candidates 斯琴恢复 1 条(恢复路径即时生效) ✓
```
- 部署:Deploy Panel 任务 `a7300401`(hl-fleet-service,2026-08-31 14:02 success,8087/8187 双实例滚动均 UP),部署 dev-v3 HEAD `2543febee`(含本 PR merge commit `df78cce550b4f4ea6a9f40352c1dd176b8d8fba6`,2026-08-31 13:50 合入)。
- **batch 创建 605074 正向链未能在 TEST 实跑**:测试服全部待派(unassigned)占位的行程均已结束,创建请求先撞 605047;构造可派需求需完整订单+需求展开链,超出本次验证范围。该路径由单测覆盖(`AssignmentServiceTest#create_fleetTeamDisabledVehicle_throws605074` / `create_driverOnDisabledTeam_throws605074`,与已实测的改派路径 `change_fleetTeamDisabledVehicle_throws605074` 共用同一锁内校验方法),建议 mmg 联调时在真实新订单上补验一次。
- 前置态构造说明:「停用车队 + 在役车辆/常驻司机」状态被人工停用守卫(601103)与车队选择守卫(601102)封锁,只有供应商联动能合法产生——实测经 internal 端点真实产生(正是本端点设计语义);测试车队的「ACTIVE + 已绑供应商」前置态由 DB 直改构造(测试服 fixture)。
- 本地自动化(PR 自报):目标单测 730 个全绿(AssignmentServiceTest 522 / FleetTeamServiceTest 36 / DriverServiceTest 157 / 新增 Mapper+Controller 测试);全量 `mvn -pl hl-fleet-service test` 3913 tests 0 失败(含 IT);`FleetRedLineArchTest` 13/13 绿;spotless:check 通过。
- 数据清理:测试车队(352696323818000384)、测试车辆(2094307514693648385,蒙A-T6843)均已删除(DB 复核无残留);测试用模拟供应商 ID(8996843000000000001)未在 `supplier_main` 落库,随车队删除清除;司机斯琴档案未改动(常驻绑定随车辆删除解除);被改派实测的真实派单(2091068072742756354)经核零副作用;admin token 已登出。
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #6868 | #6843 | 供应商暂停合作联动停用车队 + 派单拦截停用车队司机车辆(本次) | ✅ 最新 |
| #6753 | #6717 | 车队关联供应商并展示供应商全名(车队-供应商关联与车辆侧排除的前置) | ✅ 已被本单扩展 |
| #6827 | #6811 | 车队保存无供应商自动停用 + 601112 在役车辆守卫(车队停用来源之二) | ✅ 并存 |
| - | #6844 | 供应商侧通知发出端(resource → fleet 联动调用方),待指派实现 | ⏳ 未开始 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#6843](https://git.1814.love:8443/wx/HL/issues/6843)
- 关联 PR: [wx/HL#6868](https://git.1814.love:8443/wx/HL/pulls/6868)
- 前置契约: `changelogs-v2/2026-08/30_6717_车队关联供应商并展示供应商全名-修改接口-管理后台.md`
- 前置契约: `changelogs-v2/2026-08/31_6811_车队保存无供应商时自动停用与在役车辆守卫-修改接口-管理后台.md`
## 撤回
1. 前端先摘除对 605074 与 `fleet_team_disabled` 的特化渲染(按通用兜底展示),保持线上可用。
2. 从最新 `dev-v3` 建独立回退分支,回退 PR #6868 的合并 commit(`df78cce5`),验证后经独立 PR 合入。
3. ⚠️ 已被联动停用的车队**不会**因回退自动恢复:回退代码只撤联动与拦截逻辑,已落 DISABLED 的车队需在车队管理人工启用(启用前置 = 已绑供应商)。
4. 仅需止血时也可不回退整单:internal 端点无生产流量前(#6844 未上线)本单对线上唯一可感知影响是人工停用/无供应商车队在派单侧的拦截收口,属预期行为。
5. 使用 Deploy Panel 滚动部署 `hl-fleet-service`;本单无数据库迁移,无需回滚脚本。
6. 撤回后经 Gateway 验证:候选恢复返回停用车队常驻司机、precheck 不再出现 `fleet_team_disabled`、改派停用车队资源回报 605037 老口径。
## 关联 / 联系人
### 链接
- **Issue**: [#6843](https://git.1814.love:8443/wx/HL/issues/6843)
- **PR**: [#6868](https://git.1814.love:8443/wx/HL/pulls/6868)
- **Merge commit**: `df78cce550b4f4ea6a9f40352c1dd176b8d8fba6`
### 联系人
- **后端负责人**: @wx
- **前端联动**: @mmg(① precheck `warnings[]` type 白名单加 `fleet_team_disabled`;② 错误码字典加 605074「车队已停用不可派新单(请先启用车队或换车)」+ 引导动作;③ 司机候选静默排除无前端适配量)
- **服务间联动**: resource 侧 #6844 实现方按本文 §三.1 契约投递(supplierId/targetStatus/eventId/occurredAt + `X-Internal-Token`,重复投递认 `duplicate=true` 成功回执)
@@ -0,0 +1,74 @@
---
schema: "hl-changelog/v2"
ticket: "frontend-fleet-team-sort-validation"
title: "车队编辑弹窗「排序」已填值仍提示「请输入排序」,无法保存"
consumer: "admin"
author: "wx(GIT)"
change_type: "前端缺陷"
backend_status: "not_required"
gateway_status: "not_required"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "e2b2482e"
target_release: "v2.1"
verified_at: "2026-08-31"
status_note: "纯前端校验缺陷,前端已修复。rules.sortOrder 的 required 规则补 type:'number',number 值(含 0)不再误判为空,空值仍正确提示必填。"
updated_at: "2026-08-31"
base: "dev-v3"
generated: "2026-08-31T01:43:00+08:00"
---
# 车队编辑弹窗「排序」已填值仍提示「请输入排序」,无法保存
> 前端缺陷,待前端修复。后端契约无变化。
## 现象(TEST 实测)
车务 → 车队管理 → 编辑车队(如「合作车队A」):
- 「排序」输入框中已显示值 `1`;
- 点击「保存」时表单项仍报红色校验错误「请输入排序」,保存被拦截,无法提交。
## 只读前端定位(hl-ui 未修改)
`src/views/fleet/teams/index.vue`:
- 表单绑定:`NInputNumber v-model:value="formData.sortOrder"`,编辑时 `fillForm` 正确回填 `row?.sortOrder ?? null`,输入框显示的 `1` 说明 model 已有值;
- 校验规则(约 391 行):
```js
sortOrder: [{ required: true, message: '请输入排序', trigger: ['blur', 'change'] }],
```
规则缺少 `type: 'number'`。Naive UI 底层 async-validator 的必填规则默认按 string 类型校验,对 number 类型的值会误判为空,导致「值已填但 required 校验不通过」。同页面其他字段均为字符串/枚举值所以未暴露此问题。
## 修复建议
规则补充 `type: 'number'`,例如:
```js
sortOrder: [{ required: true, type: 'number', message: '请输入排序', trigger: ['blur', 'change'] }],
```
同时建议回归以下场景:
1. 编辑已有车队(详情回填 sortOrder)直接保存,不再误报;
2. 新建车队填写排序后保存正常;
3. 排序为空(清空输入框)时仍正确提示「请输入排序」;
4. 排序为 `0` 时可通过校验(`min: 0`,0 是合法值,不得被误判为空)。
## 后端契约(无变化,供联调核对)
- 详情:`GET /fleet/teams/{fleetTeamId}`,响应含 `sortOrder`(整数);
- 更新:`PUT /fleet/teams/{fleetTeamId}`,请求体 `sortOrder` 为整数;
- 新增:`POST /fleet/teams`,请求体 `sortOrder` 为整数。
本条目不引入后端代码、接口、数据库、配置、Redis 或 MQ 变更。
## 复现路径
车务(fleet 管理后台)→ 车队管理 → 任一有排序值的车队 → 编辑 → 直接点「保存」。
## 撤回
如需撤回本联调通知,只需回退本 Changelog 文件;后端契约和运行数据不受影响。
@@ -0,0 +1,748 @@
---
schema: "hl-changelog/v2"
ticket: "6903"
title: "团期看板六芯片逐户明细 GB-ADM-090~095"
consumer: "admin"
author: "wx(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: "2026-09-01"
status_note: "2026-09-01 部署测试服 dev-v3@1159c8949,6 端点网关实测 200 全通过,团期不存在 589500 / 参数错误 400 已核验。前端评估(2026-09-01):团期看板 src/views/order-v2/batch 当前为纯本地 mock 阶段(period 用 mock id,全仓无真实 group-batch 调用),chips 端点需真实雪花 groupBatchId,下钻暂无真实落脚页。决策:先接真团期看板(需 GB-ADM-001 看板列表契约+真实 groupBatchId,不在本单)再接六芯片下钻,下钻形态已定=弹层明细。待看板列表契约后接入,跟踪见 hl-admin 任务 #104。"
updated_at: "2026-09-01"
base: "dev-v3"
---
# 团期看板:六芯片逐户明细(GB-ADM-090~095,房/车/导/摄/约/保)
> **存放目录**: 二期(order-v3)→ `changelogs-v2/2026-09/`
>
> **服务**: hl-order-service-v3
> **Issue**: [#6903](https://git.1814.love:8443/wx/HL/issues/6903)
> **日期**: 2026-09-01
> **影响范围**: 管理后台团期看板行右侧六芯片(配房/配车/配导游/配摄影/合同/保险)点击后的逐户下钻
---
## ⚠️ 关键变化
**新增能力,无破坏性变化**:先前看板芯片只有整团聚合色块(GB-ADM-001 `chips.X`)、没有逐户下钻;本次把「点芯片看每户到哪一步」补成可调用接口。数据(order_main 六态列)早已落库并被房务/车队/地接等 Service 消费,本组接口只读透出,不改变任何业务状态。
---
## 一、背景(选填)
团期看板行右侧有「房/车/导/摄/约/保」六个芯片,点击后按需展开该项的**逐户明细**。逐户口径与整团聚合、看板芯片(GB-ADM-001 `chips.X`)同源同算法(共享地基 `GroupBatchChipResolver`,#6902/#6916)。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | GB-ADM-090 配房逐户明细 | GET | `/v3/admin/order/group-batch/{groupBatchId}/chips/hotel` | 新增接口 | 房芯片逐户下钻 |
| 2 | GB-ADM-091 配车逐户明细 | GET | `/v3/admin/order/group-batch/{groupBatchId}/chips/vehicle` | 新增接口 | 车芯片逐户下钻 |
| 3 | GB-ADM-092 配导游逐户明细 | GET | `/v3/admin/order/group-batch/{groupBatchId}/chips/guide` | 新增接口 | 导芯片逐户下钻 |
| 4 | GB-ADM-093 配摄影逐户明细 | GET | `/v3/admin/order/group-batch/{groupBatchId}/chips/photo` | 新增接口 | 摄芯片逐户下钻 |
| 5 | GB-ADM-094 合同逐户明细 | GET | `/v3/admin/order/group-batch/{groupBatchId}/chips/contract` | 新增接口 | 约芯片逐户下钻 |
| 6 | GB-ADM-095 保险逐户明细 | GET | `/v3/admin/order/group-batch/{groupBatchId}/chips/insurance` | 新增接口 | 保芯片逐户下钻 |
---
## 三、接口详情
六个接口共用 `GroupBatchChipDetailVO` / `GroupBatchChipItemRespVO`,正文各自自包含。
### 1. GB-ADM-090 配房逐户明细 `GET /v3/admin/order/group-batch/{groupBatchId}/chips/hotel`
**VO**: `GroupBatchChipDetailVO`
#### 使用场景
团期看板行右侧「房」芯片点击展开:返回该团期每户的配房进度(哪一户到哪一步),前端展开列表展示。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
| X-Admin-Id | Header | Long | ✅ | 网关注入 | 受信管理员 ID;客户端传值一律忽略(不校验客户端值) |
| groupBatchId | Path | String | ✅ | 团期 ID(Snowflake) | 不存在/非团期/已软删 → 589500 |
无查询参数、无请求体。
#### 出参 `Result<GroupBatchChipDetailVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| batchId | String | 团期 ID |
| chipLabel | String | 固定「配房」 |
| aggregateStatus | String | 整团聚合态 四态取值与看板 GB-ADM-001 `chips.X` 的 `aggregateStatus` 完全一致(整团待办/进行中/已完成/异常),与 GB-ADM-001 `chips.hotel` 同源同算法 |
| totalCount | Integer | 计入统计的子订单数(免闸户不计入分母) |
| doneCount | Integer | 已完成户数(`status==DONE`) |
| items[].orderId | String | 子订单 ID(JSON String 化) |
| items[].orderNo | String | 子订单编号 |
| items[].contactName | String | 联系人/客户姓名 |
| items[].peopleCount | Integer | 本户人数 = adult+child+youngChild+baby |
| items[].status | String | 本户配房状态,`RequirementStatus` 6 值,见「六.5」;无需/未开始为 null |
| items[].statusText | String | 状态中文名(服务端给出,见「六.5」) |
| items[].needsIt | Boolean | 本户是否需要配房(`order_main.needs_hotel`);false=免闸户,status=null、置灰、不计入计数 |
| items[].updateTime | String | 配房最后变更时间;六状态列无独立时间戳时为 null |
#### 请求示例
```http
GET /v3/admin/order/group-batch/90211/chips/hotel
Authorization: Bearer ****
X-Admin-Id: 3301
```
#### 响应示例
```json
{
"code": 200,
"data": {
"batchId": "90211",
"chipLabel": "配房",
"aggregateStatus": "DOING",
"totalCount": 7,
"doneCount": 5,
"items": [
{ "orderId": "770152", "orderNo": "GT-26-0096", "contactName": "陈昊",
"peopleCount": 2, "status": "DONE", "statusText": "配房完成",
"needsIt": true, "updateTime": null },
{ "orderId": "770153", "orderNo": "GT-26-0097", "contactName": "林婉清",
"peopleCount": 3, "status": null, "statusText": "无需",
"needsIt": false, "updateTime": null }
]
},
"success": true
}
```
#### 空数据 / 降级响应
```json
{ "code": 200, "data": { "batchId": "90211", "chipLabel": "配房",
"aggregateStatus": "整团待办", "totalCount": 0, "doneCount": 0, "items": [] },
"success": true }
```
#### 错误响应
```json
{ "code": 589500, "message": "团期不存在", "success": false, "data": null }
```
#### 业务边界
- 授权码 `group-batch:view`;未配置权限 → 589507。
- 只读(READ_ONLY 事务),无锁、无幂等、不改变任何状态。
- 免闸户(`needsIt=false`):status=null、statusText=「无需」、前端置灰、不计入 totalCount/doneCount 分子分母。
- 已取消子订单不计入(活跃口径与共享地基一致)。
- 团期已流团(CANCELLED)→ aggregateStatus 恒整团待办(wire 值同看板 chips.X),但逐户明细仍如实返回(不造假计数)。
---
### 2. GB-ADM-091 配车逐户明细 `GET /v3/admin/order/group-batch/{groupBatchId}/chips/vehicle`
**VO**: `GroupBatchChipDetailVO`
#### 使用场景
团期看板行右侧「车」芯片点击展开:返回每户配车进度。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
| X-Admin-Id | Header | Long | ✅ | 网关注入 | 受信管理员 ID;客户端传值忽略 |
| groupBatchId | Path | String | ✅ | 团期 ID(Snowflake) | 不存在/非团期/已软删 → 589500 |
无查询参数、无请求体。
#### 出参 `Result<GroupBatchChipDetailVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| batchId | String | 团期 ID |
| chipLabel | String | 固定「配车」 |
| aggregateStatus | String | 整团聚合态 四态取值与看板 GB-ADM-001 `chips.X` 的 `aggregateStatus` 完全一致(整团待办/进行中/已完成/异常),与 GB-ADM-001 `chips.vehicle` 同源 |
| totalCount | Integer | 计入统计的子订单数(免闸户不计入) |
| doneCount | Integer | 已完成户数(`status==DONE`) |
| items[].orderId | String | 子订单 ID(JSON String 化) |
| items[].orderNo | String | 子订单编号 |
| items[].contactName | String | 联系人/客户姓名 |
| items[].peopleCount | Integer | 本户人数 = adult+child+youngChild+baby |
| items[].status | String | 本户配车状态,`RequirementStatus` 6 值,见「六.5」;无需/未开始为 null |
| items[].statusText | String | 状态中文名(服务端给出,车务文案:待车务配/配车中/配车完成/待审核/驳回给定制师/驳回给管理员,见「六.5」) |
| items[].needsIt | Boolean | 本户是否需要配车(`order_main.needs_vehicle`);false=免闸户置灰不计入 |
| items[].updateTime | String | 配车最后变更时间;无独立时间戳时为 null |
#### 请求示例
```http
GET /v3/admin/order/group-batch/90211/chips/vehicle
Authorization: Bearer ****
X-Admin-Id: 3301
```
#### 响应示例
```json
{
"code": 200,
"data": {
"batchId": "90211",
"chipLabel": "配车",
"aggregateStatus": "DONE",
"totalCount": 3,
"doneCount": 3,
"items": [
{ "orderId": "770160", "orderNo": "GT-26-0101", "contactName": "王强",
"peopleCount": 2, "status": "DONE", "statusText": "配车完成",
"needsIt": true, "updateTime": null }
]
},
"success": true
}
```
#### 空数据 / 降级响应
```json
{ "code": 200, "data": { "batchId": "90211", "chipLabel": "配车",
"aggregateStatus": "整团待办", "totalCount": 0, "doneCount": 0, "items": [] },
"success": true }
```
#### 错误响应
```json
{ "code": 589500, "message": "团期不存在", "success": false, "data": null }
```
#### 业务边界
- 授权码 `group-batch:view`;未配置权限 → 589507。
- 只读(READ_ONLY 事务),无锁、无幂等、不改变任何状态。
- 免闸户(`needsIt=false`):status=null、statusText=「无需」、前端置灰、不计入计数。
- 已取消子订单不计入;流团团期聚合恒 整团待办、明细如实返回。
---
### 3. GB-ADM-092 配导游逐户明细 `GET /v3/admin/order/group-batch/{groupBatchId}/chips/guide`
**VO**: `GroupBatchChipDetailVO`
#### 使用场景
团期看板行右侧「导」芯片点击展开:返回每户配导游进度。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
| X-Admin-Id | Header | Long | ✅ | 网关注入 | 受信管理员 ID;客户端传值忽略 |
| groupBatchId | Path | String | ✅ | 团期 ID(Snowflake) | 不存在/非团期/已软删 → 589500 |
无查询参数、无请求体。
#### 出参 `Result<GroupBatchChipDetailVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| batchId | String | 团期 ID |
| chipLabel | String | 固定「配导游」 |
| aggregateStatus | String | 整团聚合态 四态取值与看板 GB-ADM-001 `chips.X` 的 `aggregateStatus` 完全一致(整团待办/进行中/已完成/异常) |
| totalCount | Integer | 计入统计的子订单数(免闸户不计入) |
| doneCount | Integer | 已完成户数(`status==DONE`) |
| items[].orderId | String | 子订单 ID(JSON String 化) |
| items[].orderNo | String | 子订单编号 |
| items[].contactName | String | 联系人/客户姓名 |
| items[].peopleCount | Integer | 本户人数 = adult+child+youngChild+baby |
| items[].status | String | 本户配导游状态,3 值:`NONE` 无需/未开始 / `PENDING` 待指派 / `DONE` 已指派,见「六.5」 |
| items[].statusText | String | 状态中文名(服务端给出,见「六.5」) |
| items[].needsIt | Boolean | 本户是否需要配导游(`order_main.needs_guide`);false=免闸户 status 恒 `NONE`、不计入计数 |
| items[].updateTime | String | 配导游最后变更时间;无独立时间戳时为 null |
#### 请求示例
```http
GET /v3/admin/order/group-batch/90211/chips/guide
Authorization: Bearer ****
X-Admin-Id: 3301
```
#### 响应示例
```json
{
"code": 200,
"data": {
"batchId": "90211",
"chipLabel": "配导游",
"aggregateStatus": "DOING",
"totalCount": 4,
"doneCount": 2,
"items": [
{ "orderId": "770170", "orderNo": "GT-26-0102", "contactName": "周磊",
"peopleCount": 2, "status": "DONE", "statusText": "已指派", "needsIt": true, "updateTime": null },
{ "orderId": "770171", "orderNo": "GT-26-0103", "contactName": "吴芳",
"peopleCount": 1, "status": "NONE", "statusText": "无需", "needsIt": false, "updateTime": null }
]
},
"success": true
}
```
#### 空数据 / 降级响应
```json
{ "code": 200, "data": { "batchId": "90211", "chipLabel": "配导游",
"aggregateStatus": "整团待办", "totalCount": 0, "doneCount": 0, "items": [] },
"success": true }
```
#### 错误响应
```json
{ "code": 589500, "message": "团期不存在", "success": false, "data": null }
```
#### 业务边界
- 授权码 `group-batch:view`;未配置权限 → 589507。
- 只读(READ_ONLY 事务),无锁、无幂等、不改变任何状态。
- **免闸户(needsIt=false)status 为 `NONE`(非 null)**,与房/车(null)区分;前端按 NONE 置灰。
- 读侧只认 `DONE`,其余非免闸状态一律 `PENDING`;已取消子订单不计入;流团团期聚合恒 整团待办。
---
### 4. GB-ADM-093 配摄影逐户明细 `GET /v3/admin/order/group-batch/{groupBatchId}/chips/photo`
**VO**: `GroupBatchChipDetailVO`
#### 使用场景
团期看板行右侧「摄」芯片点击展开:返回每户配摄影进度。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
| X-Admin-Id | Header | Long | ✅ | 网关注入 | 受信管理员 ID;客户端传值忽略 |
| groupBatchId | Path | String | ✅ | 团期 ID(Snowflake) | 不存在/非团期/已软删 → 589500 |
无查询参数、无请求体。
#### 出参 `Result<GroupBatchChipDetailVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| batchId | String | 团期 ID |
| chipLabel | String | 固定「配摄影」 |
| aggregateStatus | String | 整团聚合态 四态取值与看板 GB-ADM-001 `chips.X` 的 `aggregateStatus` 完全一致(整团待办/进行中/已完成/异常) |
| totalCount | Integer | 计入统计的子订单数(免闸户不计入) |
| doneCount | Integer | 已完成户数(`status==DONE`) |
| items[].orderId | String | 子订单 ID(JSON String 化) |
| items[].orderNo | String | 子订单编号 |
| items[].contactName | String | 联系人/客户姓名 |
| items[].peopleCount | Integer | 本户人数 = adult+child+youngChild+baby |
| items[].status | String | 本户配摄影状态,3 值:`NONE` 无需/未开始 / `PENDING` 待指派 / `DONE` 已指派,见「六.5」 |
| items[].statusText | String | 状态中文名(服务端给出,见「六.5」) |
| items[].needsIt | Boolean | 本户是否需要配摄影(`order_main.needs_photographer`);false=免闸户 status 恒 `NONE`、不计入计数 |
| items[].updateTime | String | 配摄影最后变更时间;无独立时间戳时为 null |
#### 请求示例
```http
GET /v3/admin/order/group-batch/90211/chips/photo
Authorization: Bearer ****
X-Admin-Id: 3301
```
#### 响应示例
```json
{
"code": 200,
"data": {
"batchId": "90211",
"chipLabel": "配摄影",
"aggregateStatus": "整团待办",
"totalCount": 1,
"doneCount": 0,
"items": [
{ "orderId": "770180", "orderNo": "GT-26-0104", "contactName": "郑浩",
"peopleCount": 2, "status": "PENDING", "statusText": "待指派", "needsIt": true, "updateTime": null }
]
},
"success": true
}
```
#### 空数据 / 降级响应
```json
{ "code": 200, "data": { "batchId": "90211", "chipLabel": "配摄影",
"aggregateStatus": "整团待办", "totalCount": 0, "doneCount": 0, "items": [] },
"success": true }
```
#### 错误响应
```json
{ "code": 589500, "message": "团期不存在", "success": false, "data": null }
```
#### 业务边界
- 授权码 `group-batch:view`;未配置权限 → 589507。
- 只读(READ_ONLY 事务),无锁、无幂等、不改变任何状态。
- 免闸户(needsIt=false)status 为 `NONE`(非 null),与导芯片一致。
- 读侧只认 `DONE`,其余非免闸状态一律 `PENDING`;已取消子订单不计入;流团团期聚合恒 整团待办。
---
### 5. GB-ADM-094 合同逐户明细 `GET /v3/admin/order/group-batch/{groupBatchId}/chips/contract`
**VO**: `GroupBatchChipDetailVO`
#### 使用场景
团期看板行右侧「约」芯片点击展开:返回每户合同(签约)进度。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
| X-Admin-Id | Header | Long | ✅ | 网关注入 | 受信管理员 ID;客户端传值忽略 |
| groupBatchId | Path | String | ✅ | 团期 ID(Snowflake) | 不存在/非团期/已软删 → 589500 |
无查询参数、无请求体。
#### 出参 `Result<GroupBatchChipDetailVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| batchId | String | 团期 ID |
| chipLabel | String | 固定「合同」 |
| aggregateStatus | String | 整团聚合态 四态取值与看板 GB-ADM-001 `chips.X` 的 `aggregateStatus` 完全一致(整团待办/进行中/已完成/异常);房车导摄四项未全 DONE 前恒整团待办(硬规则;wire 值同看板 chips.X) |
| totalCount | Integer | 计全部活跃子订单(约/保无免闸户) |
| doneCount | Integer | 已完成户数(`status==SIGNED`) |
| items[].orderId | String | 子订单 ID(JSON String 化) |
| items[].orderNo | String | 子订单编号 |
| items[].contactName | String | 联系人/客户姓名 |
| items[].peopleCount | Integer | 本户人数 = adult+child+youngChild+baby |
| items[].status | String | 本户合同状态,`ContractStatus` 8 值,见「六.5」;无合同为 null |
| items[].statusText | String | 状态中文名(服务端给出,见「六.5」) |
| items[].needsIt | Boolean | 恒 true(约/保无免闸) |
| items[].updateTime | String | 合同最后变更时间;无独立时间戳时为 null |
#### 请求示例
```http
GET /v3/admin/order/group-batch/90211/chips/contract
Authorization: Bearer ****
X-Admin-Id: 3301
```
#### 响应示例
```json
{
"code": 200,
"data": {
"batchId": "90211",
"chipLabel": "合同",
"aggregateStatus": "DOING",
"totalCount": 2,
"doneCount": 1,
"items": [
{ "orderId": "770190", "orderNo": "GT-26-0105", "contactName": "钱进",
"peopleCount": 2, "status": "SIGNED", "statusText": "已签署", "needsIt": true, "updateTime": null },
{ "orderId": "770191", "orderNo": "GT-26-0106", "contactName": "孙丽",
"peopleCount": 2, "status": "PENDING", "statusText": "待出具", "needsIt": true, "updateTime": null }
]
},
"success": true
}
```
#### 空数据 / 降级响应
```json
{ "code": 200, "data": { "batchId": "90211", "chipLabel": "合同",
"aggregateStatus": "整团待办", "totalCount": 0, "doneCount": 0, "items": [] },
"success": true }
```
#### 错误响应
```json
{ "code": 589500, "message": "团期不存在", "success": false, "data": null }
```
#### 业务边界
- 授权码 `group-batch:view`;未配置权限 → 589507。
- 只读(READ_ONLY 事务),无锁、无幂等、不改变任何状态。
- needsIt 恒 true(约/保无免闸户);doneCount 只按 `SIGNED`;作废中/已作废计入整团 ERROR。
- 约/保整团聚合在房车导摄四芯片未全部 DONE 前恒 整团待办(硬规则);已取消子订单不计入;流团团期聚合恒 整团待办。
---
### 6. GB-ADM-095 保险逐户明细 `GET /v3/admin/order/group-batch/{groupBatchId}/chips/insurance`
**VO**: `GroupBatchChipDetailVO`
#### 使用场景
团期看板行右侧「保」芯片点击展开:返回每户保险进度。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
| X-Admin-Id | Header | Long | ✅ | 网关注入 | 受信管理员 ID;客户端传值忽略 |
| groupBatchId | Path | String | ✅ | 团期 ID(Snowflake) | 不存在/非团期/已软删 → 589500 |
无查询参数、无请求体。
#### 出参 `Result<GroupBatchChipDetailVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| batchId | String | 团期 ID |
| chipLabel | String | 固定「保险」 |
| aggregateStatus | String | 整团聚合态 四态取值与看板 GB-ADM-001 `chips.X` 的 `aggregateStatus` 完全一致(整团待办/进行中/已完成/异常);房车导摄未全 DONE 前恒整团待办(硬规则;wire 值同看板 chips.X) |
| totalCount | Integer | 计全部活跃子订单(约/保无免闸户) |
| doneCount | Integer | 已完成户数(`status==INSURED`) |
| items[].orderId | String | 子订单 ID(JSON String 化) |
| items[].orderNo | String | 子订单编号 |
| items[].contactName | String | 联系人/客户姓名 |
| items[].peopleCount | Integer | 本户人数 = adult+child+youngChild+baby |
| items[].status | String | 本户保险状态,4 值:`INSURING` 投保中 / `INSURED` 已出单 / `CANCELLED` 已取消 / `FAILED` 出单失败;无保险为 null |
| items[].statusText | String | 状态中文名(服务端给出,见「六.5」) |
| items[].needsIt | Boolean | 恒 true(约/保无免闸) |
| items[].updateTime | String | 保险最后变更时间;无独立时间戳时为 null |
#### 请求示例
```http
GET /v3/admin/order/group-batch/90211/chips/insurance
Authorization: Bearer ****
X-Admin-Id: 3301
```
#### 响应示例
```json
{
"code": 200,
"data": {
"batchId": "90211",
"chipLabel": "保险",
"aggregateStatus": "DOING",
"totalCount": 2,
"doneCount": 1,
"items": [
{ "orderId": "770200", "orderNo": "GT-26-0107", "contactName": "李娜",
"peopleCount": 2, "status": "INSURED", "statusText": "已出单", "needsIt": true, "updateTime": null },
{ "orderId": "770201", "orderNo": "GT-26-0108", "contactName": "赵敏",
"peopleCount": 3, "status": "INSURING", "statusText": "出单中", "needsIt": true, "updateTime": null }
]
},
"success": true
}
```
#### 空数据 / 降级响应
```json
{ "code": 200, "data": { "batchId": "90211", "chipLabel": "保险",
"aggregateStatus": "整团待办", "totalCount": 0, "doneCount": 0, "items": [] },
"success": true }
```
#### 错误响应
```json
{ "code": 589500, "message": "团期不存在", "success": false, "data": null }
```
#### 业务边界
- 授权码 `group-batch:view`;未配置权限 → 589507。
- 只读(READ_ONLY 事务),无锁、无幂等、不改变任何状态。
- needsIt 恒 true;doneCount 只按 `INSURED`;已取消/出单失败计入整团 ERROR。
- 约/保整团聚合在房车导摄未全 DONE 前恒 整团待办(硬规则);已取消子订单不计入;流团团期聚合恒 整团待办。
---
## 四、契约约束与正确调用方式(接口类必写)
> 本节只写后端接受/拒绝请求的规则,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误调用对照
| 场景 | 说明 |
|------|------|
| ✅ 网关已注入 X-Admin-Id,调任一芯片端点 | 返回 `Result<GroupBatchChipDetailVO>`,code=200 |
| ✅ 免闸户(needsIt=false) | 房/车 status=null;导/摄 status=NONE;不计入 totalCount/doneCount |
| ❌ 未配置 `group-batch:view` 权限 | 589507 无操作权限 |
| ❌ groupBatchId 不存在/非团期/已软删 | 589500 团期不存在 |
| ❌ 客户端自行传 X-Admin-Id | 以网关注入值为准,客户端值一律忽略(不校验客户端值) |
### 切换状态时的必要动作
无状态切换——六个端点均为只读 GET、无请求体;前端点击芯片即查询,请求方不需要携带任何业务状态字段,也不修改任何状态。
---
## 五、数据库行为(涉及写操作时必写)
本组接口**无写操作**(READ_ONLY 事务),无表变更、无字段变更、无数据迁移。数据来源即为现有 order_main 六态列(needs_hotel/needs_vehicle/needs_guide/needs_photographer/合同状态/保险状态),由共享地基读取投影。
---
## 六、边界行为
- 未登录/无有效 token → 401(网关拦截,本组接口不允许匿名访问)。
- 未配置 `group-batch:view` 权限 → 589507 无操作权限。
- 团期不存在/非团期/已软删 → 589500 团期不存在。
- 任意芯片端点输入非法(groupBatchId 非数字)→ 400 参数错误(框架级)。
- 空团期/无活跃子订单 → data 正常返回:totalCount=0、doneCount=0、items=[],不 500 不降级。
- 下游数据缺失(老数据无对应需求/合同/保险记录)→ 对应 status 为 null(房/车/约/保)或 NONE(导/摄),不异常。
- 团期流团(CANCELLED)→ aggregateStatus 恒整团待办(wire 值同看板 chips.X),明细仍如实返回。
- 已返团(审核/结算)→ 房车导摄恒 DONE 聚合。
- 列表查询与整团伙计数一次性内存聚合(防 N+1),单次请求最多一次 `listByProductBatchId` 查询。
---
## 六.5、枚举 / 数据字典(接口出现枚举时必写)
### items[].status(配房/配车,`com.hulalv.order.core.enums.RequirementStatus`)
**所属字段**: `GroupBatchChipItemRespVO.status` | **类型**: `String`
| 值 | 中文(statusText) | 说明 |
|----|------|------|
| `PENDING` | 待房务配(房)/ 待车务配(车) | 待房务/车队配 |
| `PROCESSING` | 配房中(房)/ 配车中(车) | 配置进行中 |
| `DONE` | 配房完成(房)/ 配车完成(车) | 已完成(房/车分文案,#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: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: "2026-09-01"
status_note: "2026-09-01 部署测试服 dev-v3@9f689354e,/summary 与 /export 经测试网关实测 200 全通过;CSV BOM/CRLF/10 列、7 桶口径、589517 上限均核验。前端评估(2026-09-01):团期看板 src/views/order-v2/batch 当前为纯本地 mock 阶段(无真实 group-batch 调用),统计条/导出依赖真实看板页;且看板列表契约 #6905/GB-ADM-000~003 未推送进 changelog 仓(仅被本单与 #6903 引用,无契约本体)。决策:等后端推送看板列表契约后,统一接真看板再一并接统计条+导出+六芯片下钻(#6903)。跟踪见 hl-admin 任务 #104。"
updated_at: "2026-09-01"
base: "dev-v3"
---
# 团期看板:统计条(GB-ADM-009)+ 团期导出(GB-ADM-008)
> **存放目录**: 二期(order-v3)→ `changelogs-v2/2026-09/`
>
> **服务**: hl-order-service-v3
> **PR**: #6917
> **Issue**: [#6904](https://git.1814.love:8443/wx/HL/issues/6904)
> **日期**: 2026-09-01
> **影响范围**: 管理后台团期看板顶部统计条(7 桶计数 + 活跃子订单数)与整表 CSV 导出
---
## ⚠️ 关键变化
**新增能力,无破坏性变化**:团期看板新增两个只读端点——统计条(`/summary`)返回七大状态桶与跨团期活跃子订单数;导出(`/export`)按当前筛选整表导出 CSV(UTF-8 BOM、CRLF、固定 10 列)。与 #6905 交付的列表/详情(GB-ADM-000~003)同地基(#6902),本单不触碰 #6905 任何接口与文件。
---
## 一、背景(选填)
管理后台团期看板页顶部需要一条统计条(七桶计数 + 活跃子订单数),并把当前筛选下的团期整表导出为 CSV。口径与看板列表(GB-ADM-000/001/002/003)完全一致:同筛选(productId 精确、month 落出团月、keyword 模糊 OR)、同状态源(`order_group_batch.batch_status` 八态)、同聚合(跨命中团期统计活跃子订单)。FORMED 为 RESOURCE_PREPARING+MATERIAL_PREPARING 复合桶;AUDITING/CHECKED 在接口层保持独立桶,前端展示可折叠合并。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | GB-ADM-009 团期看板统计条 | GET | `/v3/admin/order/group-batch/summary` | 新增接口 | 七桶计数 + total + subOrderCount |
| 2 | GB-ADM-008 团期导出 | GET | `/v3/admin/order/group-batch/export` | 新增接口 | 看板整表 CSV 导出(10 列) |
---
## 三、接口详情
两个接口共用筛选口径与状态映射(#6902 `GroupBatchStageBuckets` 七桶 / `GroupBatchStatus` 八态),正文各自自包含。
### 1. GB-ADM-009 团期看板统计条 `GET /v3/admin/order/group-batch/summary`
**VO**: `GroupBatchSummaryVO`
#### 使用场景
团期看板页顶部统计条:返回当前筛选口径下七大状态桶计数、桶合计与跨团期活跃子订单数(任一桶非零即有数据)。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
| X-Admin-Id | Header | Long | ✅ | 网关注入 | 受信管理员 ID;客户端传值一律忽略(不校验客户端值) |
| productId | Query | Long | ❌ | - | 精确匹配团期所属产品 |
| month | Query | String | ❌ | `yyyy-MM` | 出团月筛选:`depart_date >= 月初 && < 次月`;空/缺省 = 全部 |
| keyword | Query | String | ❌ | - | 团期号/团期名模糊 OR(trim 后;`%` `_` `\` 转义,与 GB-ADM-001 同义) |
无请求体。
#### 出参 `Result<GroupBatchSummaryVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| total | Integer | 七桶计数之和(接口自校验桶和,恒等于各桶值加总) |
| buckets | Map<String,Integer> | 七键恒全:RECRUIT/FORMED/PENDING_TRIP/TRAVELLING/AUDITING/CHECKED/DISBANDED;FORMED=RESOURCE_PREPARING+MATERIAL_PREPARING 复合 |
| subOrderCount | Integer | 跨全部命中团期的活跃子订单数(order_status != CANCELLED 且未软删;1 团期 1 房) |
#### 请求示例
```http
GET /v3/admin/order/group-batch/summary?month=2026-06&keyword=%E6%B5%8B%E8%AF%95
Authorization: Bearer ****
X-Admin-Id: 3301
```
#### 响应示例
```json
{
"code": 200,
"data": {
"total": 14,
"buckets": {
"RECRUIT": 0, "FORMED": 13, "PENDING_TRIP": 0, "TRAVELLING": 0,
"AUDITING": 0, "CHECKED": 0, "DISBANDED": 1
},
"subOrderCount": 1
},
"success": true
}
```
#### 空数据 / 降级响应
```json
{ "code": 200, "data": { "total": 0,
"buckets": { "RECRUIT": 0, "FORMED": 0, "PENDING_TRIP": 0, "TRAVELLING": 0,
"AUDITING": 0, "CHECKED": 0, "DISBANDED": 0 },
"subOrderCount": 0 }, "success": true }
```
#### 错误响应
```json
{ "code": 589507, "message": "无操作权限", "success": false, "data": null }
```
#### 业务边界
- 授权码 `group-batch:list`;未配置权限 → 589507。
- 只读(READ_ONLY 事务),无锁、无幂等、不改变任何状态。
- 非法 month(非 `yyyy-MM`)→ 400 参数错误(框架级)。
- 空数据时七键仍全返回(各桶 0),total=0、subOrderCount=0,不 500 不降级。
---
### 2. GB-ADM-008 团期导出 `GET /v3/admin/order/group-batch/export`
**VO**: `text/csv`(流式附件,非 JSON 信封)
#### 使用场景
团期看板页「导出」按钮:按当前筛选口径整表导出 CSV 文件(浏览器附件下载)。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
| X-Admin-Id | Header | Long | ✅ | 网关注入 | 受信管理员 ID;客户端传值忽略 |
| productId | Query | Long | ❌ | - | 同 summary |
| month | Query | String | ❌ | `yyyy-MM` | 同 summary;文件名 `group-batch-{month}.csv` |
| keyword | Query | String | ❌ | - | 同 summary |
无请求体。
#### 出参 `text/csv`(附件流)
| 字段 | 类型 | 说明 |
|------|------|------|
| Content-Type | String | `text/csv; charset=utf-8` |
| Content-Disposition | String | `attachment; filename=group-batch-{month|all}.csv` |
| body | String | UTF-8 BOM 开头的 CSV 文本:CRLF 换行、固定 10 列(表头见下) |
**表头 10 列**: 团期号、期号、日期、出团日、满团名额、已售、剩余、状态、子订单数、整团应收。
#### 请求示例
```http
GET /v3/admin/order/group-batch/export?month=2026-06
Authorization: Bearer ****
X-Admin-Id: 3301
```
#### 响应示例
(响应体为原生 CSV 文本,非 JSON 信封;下表以 JSON 字符串形式呈现字节内容示例)
```json
"团期号,期号,日期,出团日,满团名额,已售,剩余,状态,子订单数,整团应收\r\nQ2026...,测试期,2026-06-10,2026-06-10,10,7,3,资源准备,1,15900.00\r\n"
```
#### 空数据 / 降级响应
0 命中仅输出表头 10 列(仍带 UTF-8 BOM 与 CRLF),HTTP 200。
#### 错误响应
```json
{ "code": 589517, "message": "导出数据超限", "success": false, "data": null }
```
#### 业务边界
- 授权码 `group-batch:export`;未配置权限 → 589507。
- 命中行数 > 2000 → 589517(上限 2000 行),前端提示收窄筛选。
- 每次成功导出写入 `group_batch_status_log` 导出留痕(BATCH_EXPORT,data 类变更,记录操作管理员与导出档头内容)。
- 只读(READ_ONLY 事务);0 命中仅表头(BOM/CRLF 保持)。
- 非法 month → 400 参数错误;未登录 → 401(网关拦截)。
- 期号含逗号/引号/换行 → CSV 引号转义(内部引号翻倍);金额两位小数不加千分位;剩余=max_rooms-已售且下限 0。
---
## 四、契约约束与正确调用方式(接口类必写)
> 本节只写后端接受/拒绝请求的规则,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误调用对照
| 场景 | 说明 |
|------|------|
| ✅ 仅带 Authorization 调 summary/export | 全量口径:7 桶恒全、export 全表(受 2000 行上限约束) |
| ✅ productId+month+keyword 组合筛选 | 与 GB-ADM-001 同义:productId 精确 / month 落出团月 / keyword trim 后模糊 OR(`%` `_` `\` 转义) |
| ❌ 未配置 `group-batch:list` / `group-batch:export` | 589507 无操作权限(两个授权点独立) |
| ❌ 命中 > 2000 行导出 | 589517 导出数据超限 |
| ❌ 客户端自行传 X-Admin-Id | 以网关注入值为准,客户端值一律忽略 |
### 切换状态时的必要动作
无状态切换——两端点均只读 GET、无请求体;导出需前端先具备 `group-batch:export` 授权点(看板查看仅需 `group-batch:list`)。
---
## 五、数据库行为(涉及写操作时必写)
两端点主体为只读(READ_ONLY 事务):聚合 `countActiveByProductBatchIds`(活跃子订单)、`sumBatchAmountsByProductBatchIds`(整团应收,共享 #6902 地基),无表变更、无字段变更。唯一写操作:成功导出后向 `group_batch_status_log` 追加一条 BATCH_EXPORT 留痕记录(`REQUIRES_NEW` 独立事务,导出失败不写留痕)。
---
## 六、边界行为
- 未登录/无有效 token → 401(网关拦截,两端点不允许匿名访问)。
- 未配置对应授权点 → 589507。(`/summary` 用 `group-batch:list`,`/export` 用 `group-batch:export`)
- 导出命中 > 2000 行 → 589517;前端应引导收窄 month/productId/keyword。
- 非法 month 格式 → 400 参数错误(框架级)。
- 空数据:summary 七键全 0;export 仅表头 10 列(BOM/CRLF 保持),不 500 不降级。
- 状态未知/历史脏数据:`safeStatus` 原样透出(导出的状态列不报错)。
- 剩余列恒 `max_rooms - 已售` 且下限 0;金额列两位小数、不加千分位。
- 期号含 `,` `"` `\r` `\n` → CSV 引号转义(内部引号翻倍)。
---
## 六.5、枚举 / 数据字典(接口类必写)
### buckets 键(7 桶)
**所属字段**: `GroupBatchSummaryVO.buckets` | **类型**: `Map<String,Integer>`
| 桶键 | 来源 batch_status | 说明 |
|------|------------------|------|
| RECRUIT | RECRUITING | 招募中 |
| FORMED | RESOURCE_PREPARING + MATERIAL_PREPARING | 复合桶(成团准备) |
| PENDING_TRIP | PENDING_DEPARTURE | 待出发 |
| TRAVELLING | TRAVELLING | 出游中 |
| AUDITING | REVIEWING | 审核 |
| CHECKED | SETTLED | 结算 |
| DISBANDED | CANCELLED | 流团/取消 |
> 说明:FORMED 复合桶、AUDITING/CHECKED 独立桶为接口层口径;前端展示可将 AUDITING+CHECKED 折叠为「返团核账」类目(display 层合并,接口不并)。
### 导出「状态」列中文名
**所属字段**: CSV 第 8 列 | **类型**: `String`
| batch_status | 中文 |
|--------------|------|
| RECRUITING | 招募中 |
| RESOURCE_PREPARING | 资源准备 |
| MATERIAL_PREPARING | 物料准备 |
| PENDING_DEPARTURE | 待出发 |
| TRAVELLING | 出游中 |
| REVIEWING | 审核中 |
| SETTLED | 已结算 |
| CANCELLED | 已流团 |
---
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
- **仅影响**: 团期看板统计条(GB-ADM-009)与导出(GB-ADM-008)两个新增端点。
- **零影响**:
- #6905 交付的看板列表/详情/条件接口(GB-ADM-000~003)与文件(本单未改动)
- 共享地基 #6902 的 `OrderService`/`GroupBatchStageBuckets`(本单仅新增导出留痕方法与本枚举项,未改既有方法与接口)
- 一期(v2)所有接口;数据库结构、网关路由、权限点(复用既有 `group-batch:list`/`group-batch:export`)
---
## 八、测试环境已验证
**JUnit 定向测试**: `GroupBatchBoardStatsServiceTest`(11 用例)+ `GroupBatchBoardStatsControllerTest`(6 用例)+ `GroupBatchMapperEscapeLikeTest`(3 用例)全绿;覆盖七桶折叠、total 自校验、subOrderCount、筛选转义、CSV 格式、589507/589517、留痕写入。
**全模块** `mvn -pl hl-order-service-v3 -am test`(2026-09-01,分支合并至 dev-v3 后):Tests run 7872 / Failures 3 / Errors 0 / Skipped 7——3 个失败均为 refund 模块**既有**切片用例(旧契约期望 200+code401 信封,当前拦截器直返 401),已在独立基线 worktree(dev-v3 不含本次改动)复现,与本次 9 个文件变更无关。
真实接口网关验证(2026-09-01 部署 dev-v3@9f689354e 后实测):
```
GET /v3/admin/order/group-batch/summary → 200 code=200 buckets={RECRUIT:0,FORMED:13,PENDING_TRIP:0,TRAVELLING:0,AUDITING:0,CHECKED:0,DISBANDED:1} total=14 subOrderCount=1 ✓
GET /v3/admin/order/group-batch/summary?month=2026-06&keyword=测试 → 200 code=200 ✓
GET /v3/admin/order/group-batch/export → 200 size=1739 BOM ✓ CRLF ✓ 10 列 ✓
GET /v3/admin/order/group-batch/export?month=2026-06 → 200 size=98(0 命中仅表头) ✓
```
验证通道: 统一网关 `https://api.test.1814.love:9443`,管理员登录 token + 网关注入 `X-Admin-Id`。
---
## 九、相关历史 PR(纠错 / 功能演进时必写)
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #6917 | #6904 | 团期看板统计条 + 导出交付(本单) | ✅ 最新 |
| #6914 | #6902 | 共享地基(七桶枚举/聚合/权限/错误码) | ✅ 有效 |
| #6916 | #6915 | 共享地基 statusText 契约修正 | ✅ 有效 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#6904](https://git.1814.love:8443/wx/HL/issues/6904)
- 关联 PR: [wx/HL#6917](https://git.1814.love:8443/wx/HL/pulls/6917)(合并至 dev-v3 @9f689354e)
- 共享地基: #6902(七桶/聚合/权限)、#6915/#6916(契约修正)
## 十一、复审补充知会(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: "pending"
frontend_owner: ""
frontend_ref: "regionText 待上游 product-v2 补齐后由前端组另行验收(本单不验收前端展示)"
target_release: ""
verified_at: "2026-09-01"
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"**);预计毛利 = 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",#6929) |
| 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"`**。**请勿用 `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: "pending"
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: "pending"
frontend_owner: ""
frontend_ref: ""
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: "pending"
frontend_owner: ""
frontend_ref: ""
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: "pending"
frontend_owner: ""
frontend_ref: ""
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: "pending"
frontend_owner: ""
frontend_ref: ""
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: "pending"
frontend_owner: ""
frontend_ref: ""
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: "pending"
frontend_owner: ""
frontend_ref: ""
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: "pending"
frontend_owner: ""
frontend_ref: ""
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: "pending"
frontend_owner: ""
frontend_ref: ""
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,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,267 @@
---
schema: "hl-changelog/v2"
ticket: "7066"
title: "团期子订单列表补应收总额 totalAmount"
consumer: "admin"
author: "jw(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: "2026-09-04"
status_note: "PR #7068 已合入 dev-v3(合并提交 e1ad8550b);2026-09-04 部署测试环境,网关实测 8 组用例全部通过(含取消单归 0)"
updated_at: "2026-09-04"
base: "dev-v3"
---
# 团期子订单: 出参新增应收总额 totalAmount
> **服务**: hl-order-service-v3
> **PR**: #7068 | **Issue**: #7066 | **合并提交**: `e1ad8550b`
> **影响范围**: 管理后台「团期详情 → 子订单」页签
---
## ⚠️ 关键变化
**前端不要再用 `paidAmount + balanceAmount` 反推应收总额,改读新增的 `totalAmount`。**
旧反推在两种场景下**偏大**:
1. `balanceAmount` 被钳在 ≥0(`OrderAmountUtil.calcBalance` 末行),超付或退款后已付大于应收时反推值偏大
2. 已取消子订单 `calcBalance` 直接返 0,反推得到的是已付而非应收
**这不是理论风险**:2026-09-04 测试环境实测,现存子订单 `paidAmount=3270.00`、`balanceAmount=0.00`,
而真实应收 `totalAmount=2943.00`——**旧算法多显示 327.00**。
---
## 一、背景
团期详情「子订单」页签每张卡片显示「已付 / 应收总额」,但列表接口出参此前没有应收总额字段,
前端只能自行反推。本次补齐该字段,口径与订单域既有展示口径统一。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 团期下子订单列表 | GET | `/v3/admin/order/group-batch/:groupBatchId/orders` | **出参新增字段** | 新增 `totalAmount` 应收总额 |
---
## 三、接口详情
### 1. 团期下子订单列表 `GET /v3/admin/order/group-batch/:groupBatchId/orders`
**VO**: `GroupBatchOrderItemRespVO`
#### 使用场景
团期详情「子订单」页签加载时调用,渲染每户卡片。卡片右下角「已付 ¥X / ¥Y」中的 Y 即本次新增的 `totalAmount`。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `groupBatchId` | Path | Long | ✅ | — | 运营团期 ID(订单侧 `order_group_batch` 主键) |
| `includeTravelers` | Query | Boolean | ❌ | 缺省 false | 附出行人明细,证件号一律不返回 |
| `includeNeeds` | Query | Boolean | ❌ | 缺省 false | 附房数 / 房型 / 特殊需求 |
| `includeCancelled` | Query | Boolean | ❌ | 缺省 false | 是否含已取消子订单,缺省只返活跃集 |
#### 出参 `Result<List<GroupBatchOrderItemRespVO>>`
本次仅新增一个字段,其余 28 个字段不变:
| 字段 | 类型 | 说明 |
|------|------|------|
| `totalAmount` | 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",
"totalAmount": "2943.00"
}
]
}
```
#### 空数据 / 降级响应
团期下无子订单时返回空数组,不报错:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": []
}
```
#### 错误响应
未携带管理端令牌:
```json
{
"code": 401,
"message": "缺少有效的 Authorization 头",
"success": false,
"data": null
}
```
#### 业务边界
- 只读接口,不产生任何写入
- `totalAmount` **不等于** `paidAmount + balanceAmount`,超付、退款、取消单三种情况下都会不等
- 已取消子订单(`includeCancelled=true` 才可见)的 `totalAmount` 与 `balanceAmount` 同为 0,闭合财务勾稽
- 金额一律 2 位小数(HALF_UP),与订单域其他接口格式一致
---
## 四、契约约束与正确调用方式
- **应收总额一律读 `totalAmount`**,不要自行用 `paidAmount + balanceAmount` 计算。
- **`totalAmount` 与 `balanceAmount` 是不同语义**:前者是「客户总共该付多少」,后者是「现在还差多少」。已付清时后者为 0,前者仍是原值。
- **三个金额字段是 JSON 字符串**,不是数字。
- 取消单场景下 `totalAmount` 归 0 是**有意设计**(对齐 `calcBalance` 的取消单归 0),用于闭合「应收 0 / 已付 0 / 已退 0 / 待收 0」的勾稽链,不是缺陷。
---
## 五、数据库行为
本接口只读,**不产生任何写入**,也不涉及表结构调整。`totalAmount` 由既有字段实时计算,不新增存储列。
---
## 六、边界行为
- 超付单 → `totalAmount` 为真实应收,小于 `paidAmount`
- 取消单 → `totalAmount` 归 0
- 团期无子订单 → 返回 `[]`
- 未登录 → 401(网关拦截)
---
## 六.5、枚举 / 数据字典
本次无新增枚举。
---
## 六.6、修改前后对比
| 项 | 变更前 | 变更后 |
|---|---|---|
| 出参字段数 | 28 | **29** |
| 应收总额 | **无字段**,前端用 `paidAmount + balanceAmount` 反推 | 新增 `totalAmount`,服务端按既有 `payableForDisplay` 口径给出 |
| 超付单显示 | 反推值偏大(实测多 327.00) | `totalAmount` 为真实应收 |
| 取消单显示 | 反推得到已付金额,非应收 | `totalAmount` 归 0,与 `balanceAmount` 一致 |
| `paidAmount` / `balanceAmount` | — | **语义与取值均不变** |
| 路径 / 入参 | — | **不变** |
## 六.7、影响评估
| 维度 | 评估 |
|---|---|
| 兼容性 | **纯 additive**,老调用方不读新字段即无感,无破坏性变更 |
| 前端 | 需改为读 `totalAmount`;不改也不会报错,但超付 / 取消单场景显示值偏大 |
| 数据 | 无 DDL、无迁移、无回填;`totalAmount` 实时计算不落库 |
| 性能 | 无额外查询,复用同一 `OrderInfo` 实体计算,零新增 IO |
| 回滚 | 移除字段即可,无数据侧残留 |
| 风险 | 低。口径复用 `AdjustmentService` 已在生产使用的既有方法,未新造公式 |
## 七、不影响范围
- **零影响**:`paidAmount` / `balanceAmount` 的现有语义与取值
- **零影响**:其余 28 个出参字段
- **零影响**:老调用方——纯新增字段,不读即无感
- **未新建端点**,未改动路径与入参
---
## 八、测试环境已验证
✅ 2026-09-04 于测试环境网关实测,真实鉴权(管理端 admin)。
- 网关 `https://api.test.1814.love`,分支 `dev-v3`,合并提交 `e1ad8550b`
- 部署方式:双实例滚动更新(8186 → 8086),各 11s 就绪,零停机
| # | 用例 | 期望 | 实测 |
|---|---|---|---|
| 1 | 字段上线 | 出参含 `totalAmount` | ✅ 字段数 27 → 29 |
| 2 | 真实数据口径 | `totalAmount` 为真实应收 | ✅ `2943.00`;旧反推得 `3270.00`,**偏差 327.00** |
| 3 | 序列化格式 | 字符串、2 位小数 | ✅ `"totalAmount":"2943.00"` |
| 4 | `includeCancelled=true` | 正常返回 | ✅ 返回 1 条 |
| 5 | 无 Authorization | 401 | ✅ `401 缺少有效的 Authorization 头` |
| 6 | 老字段回归 | 未受影响 | ✅ 8 个字段逐一核对一致 |
| 7 | 缺省过滤取消单 | 取消单不出现 | ✅ `GET .../orders` 返回 0 条 |
| 8 | **取消单归 0** | `totalAmount = 0` 且与 `balanceAmount` 一致 | ✅ `totalAmount="0.00"`、`balanceAmount="0.00"`、`paidAmount="3270.00"` |
**部署前基线对照**:同一接口部署前出参 27 字段、无 `totalAmount`,确认变更确实生效。
**本地单测**:`GroupBatchConverterTest` 新增 5 例(正常单 / 含增项优惠 / **超付单断言反推不等** / **取消单归 0** / 2 位小数);
本地全量 **750 例全过**。
**取消单分支已实测**(用例 7–8):验收期间该子订单被退单,恰好提供了取消单样本。
实测 `totalAmount="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: "pending"
frontend_owner: ""
frontend_ref: ""
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,552 @@
---
schema: "hl-changelog/v2"
ticket: "7070"
title: "房务列表/详情补产品类型标签 productType"
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: "2026-09-04 测试服部署 dev-v3@19ca3d702;详情/待办/抢单池 productType 网关实测与订单一致"
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)
> **Issue**: [#7070](https://git.1814.love:8443/wx/HL/issues/7070)
> **日期**: 2026-09-04
> **影响范围**: 管理后台房务——抢单池、我的接单、订单详情、待办列表、日历下钻、月度对账订单明细、需求历史
---
## ⚠️ 关键变化
🔧 房务各订单维度列表行/详情新增或接通 `productType` 字段,前端据此渲染「核心订单 / 线路订单 / 定制订单 / 团期订单」标签。取值口径统一、稳定性提升(此前部分场景在产品服务不可用时返回空,现已消除该空值)。`productType` 为 null 的订单前端不渲染标签。
---
## 一、背景(选填)
房务在抢单、配房、对账时需要直观区分订单产品类型。此前抢单池/我的接单/详情虽能取到产品类型,但在产品服务不可用时可能返回空;待办/日历/对账/需求历史则完全没有该字段。本次统一为订单主数据直读并补齐缺失场景。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 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` 筛选入参语义不变(当前页匹配过滤)。
### 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
}
```
#### 业务边界
- 取值与订单一致,只读不改写。
### 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` **筛选入参**语义不变。
- 前端按枚举值渲染标签,`null` 时不渲染。
---
## 六、边界行为
- 未登录 → 401(网关拦截)。
- 非法参数 → 400。
- 订单/需求信息缺失 → `productType` 落 null,不 500 不阻断列表/详情。
- 历史数据(无产品类型)→ 字段为 null,前端不渲染标签。
---
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| 抢单池/我的接单/详情 productType | 产品服务不可用时可能返回空 | 取值稳定,不受产品服务可用性影响 |
| 待办/日历/对账/需求历史 productType | 无此字段 | 新增返回 |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 产品类型取值稳定性 | 依赖产品服务实时可用 | 订单主数据直读,稳定 |
| 列表/详情标签覆盖 | 仅抢单池/我的接单/详情 | 待办/日历/对账/需求历史全覆盖 |
## 六.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,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`)