比较提交

..
160 次代码提交
作者 SHA1 备注 提交日期
Mimingguang c7d7b4c61e docs(changelog): #8813 前端标记 implemented
changelog-filename-gate / validate (push) Failing after 2s
2026-10-10 14:35:29 +08:00
yaosutu aa11cd2610 docs(changelog): 团期核单明细带出/暂存合并 + bizKey 溯源(#8813)
changelog-filename-gate / validate (push) Failing after 2s
- GET/PUT/POST settlement 三端点行为变化:已暂存 tab 持续带出未暂存预览行
- 行出参加 bizKey,暂存入参可透传 sourceType/bizKey
- PUT 响应新增 warnMessage/missedCarryOverCount,新错误码 589756/589757
- 前端必须同步上线(否则双显双计)
2026-10-10 12:44:09 +08:00
Mimingguang effa929648 docs(changelog): #8818 前端标记 implemented
changelog-filename-gate / validate (push) Failing after 2s
2026-10-10 10:22:00 +08:00
Mimingguang a6dfbd4719 docs(changelog): #8818 前端标记 implemented
changelog-filename-gate / validate (push) Failing after 1s
2026-10-10 10:21:45 +08:00
Mimingguang dcd914c8a5 docs(changelog): #8818 前端标记 implemented
changelog-filename-gate / validate (push) Failing after 3s
2026-10-10 10:14:05 +08:00
yaosutu 5409e499cd Merge branch 'main' of https://git.1814.love/wx/hl-api-changelog
changelog-filename-gate / validate (push) Failing after 1s
2026-10-10 10:04:29 +08:00
yaosutu 12d25a7f35 补充核单付款方式取值约定(settlement_payment_method 字典,单体/团期一致)及与 settleType 区分说明(#8818) 2026-10-10 10:04:11 +08:00
Mimingguang c55453fb89 docs(changelog): #8815 前端标记 implemented
changelog-filename-gate / validate (push) Failing after 2s
2026-10-10 09:54:08 +08:00
yaosutu aeb5106d90 新增团期核单酒店候选查询接口(#8818)
changelog-filename-gate / validate (push) Failing after 1s
2026-10-10 09:45:47 +08:00
yaosutu c87e079286 docs(changelog): 账户流水与微信商户对账双向查询关联(#8815,PR #8816)
changelog-filename-gate / validate (push) Failing after 2s
新增 2 个只读接口:对账→流水(/v3/admin/payment/wx-bill/records/{billRecordId}/fund-flow)
与流水→对账(/admin/finance/fund-flows/{flowId}/wx-bill-records),管理后台目录 changelogs-v2。
2026-10-09 17:55:39 +08:00
Mimingguang 90b18a1594 docs(changelog): 7 月 11 个历史文件 frontmatter 迁移 hl-changelog/v2(过远端门禁,补 change_type/backend_status/gateway_status/target_release)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-09 16:28:06 +08:00
Mimingguang 3bf207769a docs(changelog): #5205 前端标记 implemented 2026-10-09 16:24:03 +08:00
Mimingguang d2b34953bc docs(changelog): #5200 前端标记 implemented 2026-10-09 16:23:48 +08:00
Mimingguang dd6794e4b8 docs(changelog): #5199 前端标记 implemented 2026-10-09 16:23:23 +08:00
Mimingguang c36c051667 docs(changelog): #5194 前端标记 implemented 2026-10-09 16:23:09 +08:00
Mimingguang a0dde3e6ad docs(changelog): #5186 前端标记 implemented 2026-10-09 16:22:55 +08:00
Mimingguang ed192a98a4 docs(changelog): #5149 前端标记 implemented 2026-10-09 16:22:29 +08:00
Mimingguang fe4fa3b70b docs(changelog): #5146 前端标记 implemented 2026-10-09 16:22:06 +08:00
Mimingguang a785dcbae6 docs(changelog): #5145 前端标记 implemented 2026-10-09 16:21:49 +08:00
Mimingguang 9ab4794549 docs(changelog): #5141 前端标记 implemented 2026-10-09 16:21:32 +08:00
Mimingguang f8fb2f34b7 docs(changelog): #5139 前端标记 implemented 2026-10-09 16:21:18 +08:00
Mimingguang 575292337b docs(changelog): #5132 前端标记 implemented 2026-10-09 16:20:53 +08:00
Mimingguang c96526f098 docs(changelog): 8809 回写前端交付(implemented,ref 1f96d87b5)
changelog-filename-gate / validate (push) Failing after 1s
2026-10-09 15:47:24 +08:00
yaosutu 0b6467b2c3 docs(changelog): 出纳付款联动司导收款账户(报账款线选/手填建档,#8809)
changelog-filename-gate / validate (push) Failing after 2s
- 修改 POST /admin/finance/cashier/reimburse/pay:入参加 payeeAccountId/newPayeeAccount,出参加收款账户留痕
- 新增 GET /admin/finance/payee-accounts/list-by-payee 选账户下拉数据源
- 队列/台账/报账详情出参加报账人档案与收款账户字段
- 新增错误码 598612-598615
2026-10-09 14:07:38 +08:00
Mimingguang 3ce7ef580a docs(changelog): 8807 回写前端交付(implemented,ref cb0d22fa3)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-09 11:21:33 +08:00
yaosutu 1029581523 docs(changelog): 8807 微信商户对账逐笔账单记录出参新增 appid 字段(PR #8808)
changelog-filename-gate / validate (push) Failing after 1s
2026-10-09 11:01:46 +08:00
yaosutu 068e6ef2d7 docs(changelog): 8807 微信商户对账逐笔账单记录出参新增 appid 字段(PR #8808) 2026-10-09 11:01:06 +08:00
Mimingguang 6005b617a7 docs(v2): #8801 回写订正——已结算老团期首读建行出 DRAFT 核单,确认核单反向置灰撤回
changelog-filename-gate / validate (push) Failing after 2s
2026-10-08 17:05:20 +08:00
Mimingguang 9571ad0ceb docs(v2): #8801 回写确认核单/完成核单互斥与弹窗改名
changelog-filename-gate / validate (push) Failing after 2s
2026-10-08 16:56:29 +08:00
Mimingguang 8835683a40 docs(v2): #8801 回写行内化交付(明细行编辑改行内,SettlementLineModal 精简为 SettlementAllocModal)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-08 16:41:24 +08:00
Mimingguang 060d71bb90 docs(changelog): #8801 回写 ref 更新为 945e2a637(8 类合计表下线)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-08 16:14:45 +08:00
Mimingguang ed0e5bda03 docs(changelog): #8801 回写 ref 更新为最终提交 ee8182dcf(补清空全量查口径)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-08 16:05:46 +08:00
Mimingguang 22e05ddeaf docs(changelog): #8801 回写前端已交付(implemented,ref f18b3bda0)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-08 15:57:03 +08:00
yaosutu d35be59c3b docs(changelog): 8801 团期核单页面前端对接指引(列表默认 opsStage=REVIEW + 核算明细按 8714 重写)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-08 15:25:08 +08:00
Mimingguang和Claude Opus 4.8 f8ac9edf78 chore(#8796): 前端交付回写 implemented(发票列表 stats 卡片下线,提交 86db824e2)
changelog-filename-gate / validate (push) Failing after 1s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-05 22:57:43 +08:00
yaosutu 053276cfdd docs(changelog): 8796 发票管理列表出参移除 stats 统计块(顶部4张统计卡片下线,tabCounts 保留)
changelog-filename-gate / validate (push) Failing after 3s
2026-10-05 22:45:55 +08:00
Mimingguang和Claude Opus 4.8 86da96e23e chore(#8746): 前端交付回写 implemented(缺项展示随 #8767 同提交 130a49ebf 落地,补漏回写)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-05 10:57:58 +08:00
Mimingguang b2ca08e95e docs(changelog): #8767 回写前端 implemented(mmg,ref 130a49ebf)
changelog-filename-gate / validate (push) Failing after 1s
2026-10-04 17:25:48 +08:00
jw和Claude Opus 5.5 e08991a588 docs(changelog): #8767 出团通知书默认车辆补团车、下发缺项新增 DRIVER / DRIVER_UNAVAILABLE(修改接口·管理后台)+ 车务团车活跃派车行内部读口(新增接口·internal)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 17:16:16 +08:00
Mimingguang b4f2e3da14 docs(changelog): #8755 回写前端 implemented(mmg,ref bfe3c4537)
changelog-filename-gate / validate (push) Failing after 1s
2026-10-04 16:07:12 +08:00
jw和Claude Opus 5.5 58a9fc7211 docs(changelog): #8755 发票推送接短信(按下单手机号直发)推送日志新增 SKIP + 新增发票短码免登录下载端点(新增接口 / 修改接口·管理后台)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:58:24 +08:00
Mimingguang 9537a84303 docs(changelog): #8786 回写前端 not_required(未消费回执 teamNo,功能零适配)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-04 15:14:45 +08:00
yaosutu d481025d6b docs(changelog): 订正 04_8714 团期核单 alloc-preview 试算回执 splits.teamNo 由恒 null 改为已填充真实团号(#8786 / PR #8787)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-04 15:02:35 +08:00
Mimingguang 8b683169ea docs(changelog): #8751 回写前端 implemented(mmg,ref 7ed2db889)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-04 14:58:10 +08:00
Mimingguang f9b49d2852 docs(changelog): #8783 回写前端 implemented(589568 确认回执处理已交付,ref aef8ddd60)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-04 14:39:43 +08:00
Mimingguang 737e20824a docs(changelog): #8714 回写前端 implemented(整页重对接已交付,ref 4a9a34637)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-04 14:35:39 +08:00
yaosutu bb6df1469a docs(changelog): 订正 04_8714 团期核单确认门禁契约(#8783 PR #8784):confirm blocking 非空服务端硬拦 589568、负金额 400 改抛 589753、零值金额统一 0.00(修改接口·管理后台)
changelog-filename-gate / validate (push) Failing after 1s
2026-10-04 14:23:28 +08:00
Mimingguang和Claude Opus 4.8 21b7836c2b docs(changelog-v2): #8768 前端已同步下线(入口/筛选移除,837eb9659)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-04 12:00:10 +08:00
Mimingguang和Claude Opus 4.8 8b56ce67c6 docs(changelog-v2): #8754 前端已交付(补发提示改如实计数,c5f4af363)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-04 11:44:02 +08:00
Mimingguang和Claude Opus 4.8 2719ef1839 docs(changelog-v2): #8753 前端已交付(建单入口放开+归属定制师下拉,cf5af6d6b)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-04 11:36:53 +08:00
Mimingguang和Claude Opus 4.8 0c0d1ae396 docs(changelog-v2): #8752 前端已交付(催需求按钮+看板铃铛,e4cd749bd)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-04 11:24:40 +08:00
lc 2f5013a76a docs(changelog): #8741 供应商注册提交校验证件图片地址并去掉建单失败
changelog-filename-gate / validate (push) Failing after 2s
Refs wx/HL#8741
2026-10-04 10:59:44 +08:00
Mimingguang和Claude Opus 4.8 944c09775e docs(changelog-v2): #8750 前端判 not_required(4 写口前端零消费,下钻无跳去改,调整弹窗已双覆盖锁)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-04 10:47:21 +08:00
yaosutu d47e68a451 docs(changelog): 团期核单重做8类tab明细+公摊/指定报名拆账前端契约(#8714 整批6PR + teamNo #8779)
changelog-filename-gate / validate (push) Failing after 2s
- 8 类费用 tab GET 换实现 + 新增 8 个 PUT 整 tab 暂存(全量替换 + expectedVersion CAS)
- 新增面板族 7 端点:panel / sub-orders / lines 增删 / panel confirm / alloc-preview / invoice
- 旧 /audit 6 端点下线 404,旧四表 DROP
- sharedCostByType 键值 BatchCostType 4 值切 settlement_category 8 值
- 错误码新增 589750-589755 拆账段,589569/589570 废弃
2026-10-04 10:43:41 +08:00
jw和Claude Opus 5.5 194f5df83d docs(changelog): #8754 补发成团通知回执如实计数并新增 smsCount/inappCount/skippedCount/smsReady,成团通知加客户短信(修改接口·管理后台);通知事件配置内部接口新增 smsTemplateReady(修改接口·内部)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 10:31:14 +08:00
yaosutu 97056b0370 docs(changelog): 8768 资金账户盘盈盘亏整功能下线(inventory-adjust 删除 + INVENTORY 枚举删除,管理后台)
changelog-filename-gate / validate (push) Failing after 1s
- POST /admin/finance/fund-accounts/{id}/inventory-adjust 已删,调用一律 404
- 资金流水 bizType 枚举删 INVENTORY(历史残留行 bizTypeName 返 null)
- 错误码 595106 废弃(码位保留不重发)
- 关联:Issue #8768 / PR #8773(代码)/ PR #8781(原型+文档)
2026-10-04 10:23:13 +08:00
Mimingguang 441a01d6ea docs(changelog): #8747 前端已交付回写 implemented(天头终止提示+下钻终止标记,ref 17c98e50)
changelog-filename-gate / validate (push) Failing after 1s
2026-10-04 10:08:13 +08:00
yaosutu c200920a76 docs(changelog): 8751 往来台账净额视图破坏性契约 partyType(反转 #8719)
changelog-filename-gate / validate (push) Failing after 1s
2026-10-04 09:48:42 +08:00
Mimingguang 096398a6f8 docs(changelog): #8749 前端已交付回写 implemented(待办列表团期号+行程人数房数,ref a958e251)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-04 09:26:44 +08:00
jw和Claude Opus 5.5 cc8292ea1e docs(changelog): #8752 团期管理员一键催办未提交房 / 车需求的定制师(新增接口·管理后台)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 23:18:14 +08:00
jw和Claude Opus 5.5 93027c27eb docs(changelog): #8753 团期管理员可为团期产品新增子订单,建单入参新增归属定制师 consultantId(修改接口·管理后台)
changelog-filename-gate / validate (push) Failing after 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 20:05:59 +08:00
jw和Claude Opus 5.5 5e25e03897 docs(changelog): #8746 出团通知书可下发改为「阶段 + 四项资源」并新增 releaseBlockers,默认车辆带司机,保存留痕(修改接口·管理后台)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 18:59:12 +08:00
jw和Claude Opus 5.5 77ab7a9a67 docs(changelog): #8750 团期子订单封掉订单级行程写口,调整快照不再提示行程与出行日期可编辑(修改接口·管理后台)
changelog-filename-gate / validate (push) Failing after 1s
Refs wx/HL#8750

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 17:30:09 +08:00
jw和Claude Opus 5.5 5d34813960 docs(changelog): #8747 团期行程汇总与下钻按出行中终止日截断(修改接口·管理后台)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 17:18:12 +08:00
jw和Claude Opus 5.5 b221b2317a docs(changelog): #8749 定制师待办 5 个接口出参新增团期号、返团日期、行程天数、本户人数房数
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 17:13:04 +08:00
yaosutu 5203c4a599 docs(changelog): 标注 #8719 台账 changelog 已被 #8751 净额视图反转(frontend_status→superseded)
changelog-filename-gate / validate (push) Failing after 1s
(SUPPLIER=供应商应付+应收轧差/CUSTOMER=客户应收),ledgerType 入参作废。
避免前端按旧文档开发,标注 superseded 指向 #8751 最新契约。
2026-10-03 15:40:33 +08:00
API Changelog Bot和Claude Opus 5.5 7010d52c60 docs(changelog): #8578 接送机派车行按航班日自动置接送标志(修复,契约不变)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 10:41:51 +08:00
API Changelog Bot和Claude Opus 5.5 5995966e1d docs(mp): 小程序团期下单撞改期联动时等锁 5 秒并返回 100503(#8739)
changelog-filename-gate / validate (push) Failing after 2s
Refs #8739

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 03:33:44 +08:00
Mimingguang 5b3764085a docs(changelog): #8666 frontmatter 回写 not_required(前端零适配,新错误码走拦截器透 message)
changelog-filename-gate / validate (push) Failing after 1s
2026-10-03 02:35:06 +08:00
API Changelog Bot和Claude Opus 5.5 a7c93679f8 docs(changelog): #8666 产品班期改出发日联动团期与子单、新增改期拒绝码(管理后台)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 02:21:30 +08:00
Mimingguang 11195659e1 docs(changelog): #8735 前端判 not_required(预检 blocked+写口透 message 已覆盖,字段零变更)
changelog-filename-gate / validate (push) Failing after 1s
2026-10-02 21:30:59 +08:00
API Changelog Bot和Claude Opus 5.5 1c59fb716a docs(changelog): #8735 整团确认订房无需房户但有残留订房计划改为拒绝(808607)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 21:13:25 +08:00
jw和Claude Opus 5.5 358be6ddb2 docs(changelog): #8246 流团审批放开团期管理员,批后各户退款进退款审批中心二审(修改接口·管理后台)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:26:46 +08:00
API Changelog Bot和Claude Opus 5.5 5442f80523 docs(changelog): #8708 二期未支付订单自动取消时刻由创建后 2h 恢复为 24h,接口契约不变
changelog-filename-gate / validate (push) Failing after 1s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 18:08:31 +08:00
Mimingguang 27ba364e60 docs(changelog): #8719 前端已交付回写 implemented(往来台账三账套页签,ref b7bca850)
changelog-filename-gate / validate (push) Failing after 1s
2026-10-02 17:49:31 +08:00
API Changelog Bot和Claude Opus 5.5 b54834b3f5 docs(changelog): #8717 团期 21 个读端点补定制师归属校验,非本人团期返回 589507
changelog-filename-gate / validate (push) Failing after 1s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 17:36:28 +08:00
API Changelog Bot和Claude Opus 5.5 e66936fc5a docs(changelog): #8711 房务转房招募中放开退给酒店,源/目标团期阶段栅栏分码 808327/808323,列表新增 transferAllowed/transferBlockedReason
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 17:14:32 +08:00
yaosutu 3d0a3d7757 feat(finance): 应收侧往来台账放开 ledgerType 扩域 changelog(#8719 / PR #8727)
changelog-filename-gate / validate (push) Failing after 2s
statements/page 与 entries/page 入参 ledgerType 由仅 SUPPLIER 扩为
SUPPLIER / SUPPLIER_RECV / CUSTOMER(STAFF 未开放报 596005);
netAmount/openingAmount 符号方向按账套分化,应收账套本期无流水空页属正常。
2026-10-02 17:09:34 +08:00
Mimingguang 1e71734160 chore(changelog): #8684 回写前端 implemented(撤回按钮读 canRevoke,41468f52)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-02 15:26:44 +08:00
Mimingguang e3fe98b1f2 chore(changelog): #8690 回写前端 implemented(微信商户对账页,8175316c)
changelog-filename-gate / validate (push) Failing after 1s
2026-10-02 14:49:36 +08:00
jw 92c9bb51c1 docs(changelog): #8684 预支相关 8 个接口出参新增 canRevoke(修改接口·管理后台)
changelog-filename-gate / validate (push) Failing after 2s
Refs wx/HL#8684 · PR #8723 · merge 806058c66
2026-10-02 14:43:23 +08:00
API Changelog Bot和Claude Opus 5.5 ccc6c90e6f docs(changelog): #8687 删除团期抢单池旧列表接口 GET grab-pool/group-batches
changelog-filename-gate / validate (push) Failing after 1s
替代接口 GET /v3/admin/order/house-allocation/group-batches(status=pendingClaim 复刻旧口径)。

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:22:02 +08:00
Mimingguang d221505375 chore(changelog): #8699 回写前端 implemented(调账页+审批闭环,427e3080)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-02 12:21:50 +08:00
Mimingguang 48ddf1194e chore(changelog): #8693 回写前端 implemented(抽屉归属行反转三件套,3dddf2de)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-02 11:40:23 +08:00
yaosutu 26c43ee57c feat(order-v3): 微信商户对账管理端 6 端点 changelog(#8690 / PR #8703)
changelog-filename-gate / validate (push) Failing after 1s
对账汇总/逐笔记录/差异列表/差异详情/处理差异/手动补跑,5824 段错误码 + 4 组枚举全量内联
2026-10-02 11:22:55 +08:00
Mimingguang cb0ec7f47d docs(changelog): #8689 前端回写 implemented(核销管理页+发起/审批闭环已交付)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-02 10:34:34 +08:00
yaosutu b41e3df62d feat(finance): 往来账财务调账5端点 changelog(#8699)
changelog-filename-gate / validate (push) Failing after 2s
新增 /admin/finance/adjusts 5 端点:TZ调账单+页内审批+应收/应付入账联动。
2026-10-02 09:57:09 +08:00
API Changelog Bot和Claude Opus 5.5 9fe70242a6 changelog(#8665): 配房删除/修改/单日确认并发冲突新增 808932
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 09:42:38 +08:00
Mimingguang 9ff97c8e4c docs(changelog): #8679 前端回写 implemented(收款账户个人类档案选人已交付)
changelog-filename-gate / validate (push) Failing after 1s
2026-10-02 09:02:01 +08:00
API Changelog Bot和Claude Opus 5.5 cdcd07d8e5 docs(changelog): #8659 房务价格日历与库存口径统一 / #8662 删除旧住宿需求提交口与询房预览补权限
changelog-filename-gate / validate (push) Failing after 1s
- #8659:候选页 inventoryStatus 按日历状态取值;控房表新增 calendarStatus / calendarStatusName(前端加一列展示);扣减拒绝分 808906 / 808907 / 808901。
- #8662:删除 PUT /v3/admin/order/{id}/hotel-requirement;询房预览补房务读守卫,非房务角色返回 808090。

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 01:15:08 +08:00
yaosutu 5c13bfd8b4 docs(changelog): fin_advance 抽象化接通团期级预支,反转 #8680 详情出参(#8693)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-01 23:03:54 +08:00
yaosutu 4123a6a59c 核销管理后端落地:坏账核销/债务豁免 5 端点(#8689)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-01 17:42:29 +08:00
Mimingguang 7e0cdffd7d chore(8680): 追记批次 2 收官(7 域全接通,ref 更新 75b56e2f)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-01 17:03:49 +08:00
yaosutu fbd759f003 feat(finance): 收款账户关联服务人员档案 changelog(#8679)
changelog-filename-gate / validate (push) Failing after 2s
个人类收款方从手填改为档案下拉选人:新增 staff-candidates 候选接口 +
create 个人类 payeeRefId 必填/payeeName 档案真名覆盖/档案校验。财务域推
changelogs-v2/ 管理后台。
2026-10-01 16:35:55 +08:00
Mimingguang df02512101 chore(8680): 回写前端 implemented(台账明细抽屉批次 1 骨架+ADVANCE,fae23750)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-01 15:41:31 +08:00
jw和Claude Opus 5.5 a8147a3535 docs(changelog): #8677 团期预支可支取上限改为整团已收(修改接口·管理后台)
changelog-filename-gate / validate (push) Failing after 2s
Refs wx/HL#8677

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 15:25:51 +08:00
yaosutu ba729d6623 feat(finance): ADVANCE 已付台账补预支详情接口 changelog(#8680)
changelog-filename-gate / validate (push) Failing after 2s
新增 GET /admin/finance/advances/{id},补齐已付台账 8 页签明细抽屉最后一环。
2026-10-01 14:23:52 +08:00
Mimingguang 9c6c034e6f docs(changelog): #8673 应付款默认只看欠款+明细全景前端已交付(1be140db)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-01 13:32:32 +08:00
Mimingguang e1c529531c docs(changelog): #8671 核单页签 scope=ALL 前端已交付(04b5cca6)
changelog-filename-gate / validate (push) Failing after 1s
2026-10-01 12:40:45 +08:00
Mimingguang 8d426a4dd1 docs(changelog): #8664 公司借款单笔收回前端已交付(c32b35c9)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-01 12:25:40 +08:00
Mimingguang和Claude Opus 4.8 3b93fa2606 chore(changelog): #8663 回写 implemented(hl-admin 305ba037)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-01 11:25:23 +08:00
Mimingguang和Claude Opus 4.8 62e9df3879 chore(changelog): #8654 回写 implemented(hl-admin 3c153a32)
changelog-filename-gate / validate (push) Failing after 1s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-01 10:05:53 +08:00
yaosutu bed946723f docs(changelog): #8673 应付款列表默认只看欠款 + 付款明细补全量支付状态
changelog-filename-gate / validate (push) Failing after 1s
2026-10-01 09:58:57 +08:00
Mimingguang和Claude Opus 4.8 eb2dc899f2 chore(changelog): #8516 回写 implemented(hl-admin d8de94a8)
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-01 09:36:34 +08:00
yaosutu 8b8c147a1c 团期核单页签 changelog 示例数据修正为部署后实测(#8671)
changelog-filename-gate / validate (push) Failing after 1s
§8.1 示例改为真实响应:T26-2325 核单中(REVIEWING/stage=核单)、T26-5936 已结算(SETTLED/stage=结算),
当前库无待核单团故 total=2;补充说明待核单团仍显示出行节点。
2026-10-01 09:32:19 +08:00
yaosutu b82d4f4e6e 团期核单页签 opsStage=REVIEW 筛选口径扩为核单三态(#8671)
changelog-filename-gate / validate (push) Failing after 2s
changelogs-v2/2026-10/01_8671_团期核单页签扩为核单三态-修改接口-管理后台.md
2026-10-01 09:18:32 +08:00
yaosutu 86c8752549 新增 公司借款单笔收回端点 changelog(管理后台)(#8664)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-01 07:20:18 +08:00
Mimingguang b5c37d3724 chore(changelog): #8629/#8630 回写 not_required(hl-admin)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-01 00:33:03 +08:00
Mimingguang 767532e2ea chore(changelog): #8619 回写 implemented;#8544 文件补记 #8545/#8619 交付(hl-admin bc9c13aa)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-01 00:31:15 +08:00
Mimingguang 79aff0aa3d chore(changelog): #8544 回写 implemented(hl-admin 8926ebff)
changelog-filename-gate / validate (push) Failing after 2s
2026-10-01 00:22:09 +08:00
Mimingguang c98b11fa44 chore(changelog): #8562 回写 implemented(hl-admin e3f3d552)
changelog-filename-gate / validate (push) Failing after 1s
2026-10-01 00:09:34 +08:00
Mimingguang 95fdc98cdc chore(changelog): #8598 回写 not_required(hl-admin)
changelog-filename-gate / validate (push) Failing after 1s
2026-10-01 00:00:19 +08:00
Mimingguang b458800218 chore(changelog): #8601 回写 implemented(hl-admin 527842c1)
changelog-filename-gate / validate (push) Failing after 2s
2026-09-30 23:56:54 +08:00
Mimingguang fa3213d309 chore(changelog): #8543 回写 implemented(hl-admin bcd073be)
changelog-filename-gate / validate (push) Failing after 2s
2026-09-30 23:51:05 +08:00
Mimingguang 8f3526b357 chore(changelog): #8548 回写 implemented(hl-admin 043232cc)
changelog-filename-gate / validate (push) Failing after 2s
2026-09-30 23:45:22 +08:00
Mimingguang d3b6d38f39 chore(changelog): #8560 回写 implemented(hl-admin 8d23a381)
changelog-filename-gate / validate (push) Failing after 2s
2026-09-30 23:24:02 +08:00
Mimingguang ca207b14dc chore(changelog): #8556 回写 implemented(hl-admin 585a59b0)
changelog-filename-gate / validate (push) Failing after 1s
2026-09-30 23:11:44 +08:00
Mimingguang 38164c6439 chore(changelog): #8530 回写 implemented(hl-admin 38bcab54)
changelog-filename-gate / validate (push) Failing after 2s
2026-09-30 22:59:32 +08:00
jw和Claude Opus 5.5 5dc79ef2e6 docs(changelog): #8516 团期核单、结算拆成两个独立字段,出行完毕改名待核单
changelog-filename-gate / validate (push) Failing after 2s
两份:
- 新增接口(internal):POST /v3/internal/group-batch/:groupBatchId/review-status、
  /settlement-status,供财务回写整团核单、结算状态;主状态由两列推导,子订单同事务同步,
  任何一步都不推报账单。
- 修改接口(admin):团期主状态 TRIP_FINISHED 改为 PENDING_REVIEW「待核单」;详情 / 分页
  新增 reviewStatus / settlementStatus 及中文名;进度条核单、结算节点新增分支;三处入参
  旧值兼容;/settle 不再逐户推 ORDER 报账单。hl-ui 须同批改,TEST 先行、正式环境同批发布。

PR #8650 已合入 dev-v3(merge commit 54e64c50f),TEST 部署 dev-v3 dd0452916 并按
AC-01~15 验收通过,工单已关。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:38:06 +08:00
API Changelog Bot和Claude Opus 5 a29c0873e3 docs(changelog): 团期正式派车司机投影到人员配置表,DRIVER 改系统托管(#8653 #8654)
changelog-filename-gate / validate (push) Failing after 2s
PR #8669 已合入 dev-v3(merge commit 5c50782717d6),order-v3 已部署测试服
(jar 5c5078271)并逐条取证通过。

三条对外契约变化:
1. 同一批数据两个读口口径不同——saveConfig 的响应回显与 GROUP_BATCH 扇出副本
   不含司机行(走 selectManualConfigByProductBatchId,带 ne(staff_role, DRIVER)),
   而 getConfig / getConfigWithLiveStaffInfo 含司机行(走 selectByProductBatchId,
   无该谓词)。前端不要假设保存响应即全量。
2. order_batch_staff 现在会出现系统写入的 DRIVER 行,由车务派车回调投影维护,
   不提供人工编辑/删除入口。
3. 新错误码 582120:saveConfig 的 staffList 含 staffRole=DRIVER,或 scopeRoles
   声明 DRIVER,均拒绝整批保存且零写入(守卫排在软删与插入之前)。

Refs #8653
Refs #8654

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 22:34:15 +08:00
yaosutu 0e8500103a docs(changelog): #8663 公司借款域往来单位放开员工类型 + 归还归支付管理 + 双 tab
changelog-filename-gate / validate (push) Failing after 1s
2026-09-30 21:14:16 +08:00
Mimingguang be4e9e76fa chore(changelog): #8657 回写 implemented(hl-admin adfd6815)
changelog-filename-gate / validate (push) Failing after 2s
2026-09-30 18:24:34 +08:00
Mimingguang 6be4c04bea chore(changelog): #8655 回写 implemented(hl-admin e4de4c1f)
changelog-filename-gate / validate (push) Failing after 1s
2026-09-30 18:09:36 +08:00
Mimingguang 653936db83 chore(changelog): #8632/#8641 回写 implemented(hl-admin d5a372be)
changelog-filename-gate / validate (push) Failing after 2s
2026-09-30 17:59:13 +08:00
Mimingguang 514303e28e docs(changelog): #8626 司导往来账回写 implemented(hl-admin 1a337d86)
changelog-filename-gate / validate (push) Failing after 2s
2026-09-30 17:08:47 +08:00
Mimingguang 78361b8cde docs(changelog): #8507 财务初始化三 tab 纠偏回写 implemented(hl-admin 61f92cd8)
changelog-filename-gate / validate (push) Failing after 2s
2026-09-30 16:33:30 +08:00
lc 69c0b76bb9 docs(changelog): #8579 接送机配置补发条件放宽
changelog-filename-gate / validate (push) Failing after 2s
Refs wx/HL#8579
2026-09-30 16:21:51 +08:00
API Changelog Bot和Claude Opus 5 903e30b4d1 docs(changelog): 订正 #8576 的 frontend_status 误判,并给 #8597/#8603 补 not_required 的限定
changelog-filename-gate / validate (push) Failing after 1s
#8576:我在 2026-09-30 把它从 not_required 改成 pending 是错的,本次改回。
错因是读了落后 693 个提交的 hl-ui 本地工作树——#8464(提交 d7e932ac,2026-09-28)
已整体删除 src/views/fleet/group-dispatch/,src/api/fleet/group-dispatch.js 随之
收缩到只剩 getGroupDispatchPendingBatches,reconfigure / confirm 在前端已无消费方。
对 origin/v2.1 第三次复核:specWarnings 在 src/ 下 0 命中(唯一命中在 .claude 备忘文件),
reconfigure 的 src/ 命中全是注释或 order-v2 同词异义;阳性对照 13 个文件有 export function、
matrix.js 有活跃消费方,证明检索本身有分辨力。

#8597:点明「房务控制台」整个域在前端尚不存在(9 个关键词 0 命中,阳性对照 house-allocation
活跃),故此处的 not_required 是「没有可改的代码」而非「对现有页面透明」,需与 #8491 一并核对。

#8603:点名前端真实渲染点 Step3PickupDropoff.vue:178-179 读的是 props.order.pickupDropoffGate
(未动的那个对象),与本次删字段的 ConfirmRequirementRespVO 不是同一载体,避免下一个人
按字段名 grep 得出相反结论。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 15:55:39 +08:00
yaosutu 7242ab4108 收款方类型 payeeType 删 STAFF 拆为司机/导游/摄影/领队四类个人,保留 FLEET/GUIDE_CO/SUPPLIER(#8657)
changelog-filename-gate / validate (push) Failing after 2s
2026-09-30 15:55:25 +08:00
API Changelog Bot和Claude Opus 5 d25ff94370 docs(changelog): #8601 交接件点名前端两处已失效的 JSDoc 与 809012 无人接住
changelog-filename-gate / validate (push) Failing after 2s
「前端必须做的改动」节补一条落点:hl-ui origin/v2.1 的 src/api/orderV2.js 里
rejectVehicleRequirement 与 dispatchVehicleRequirement 的 JSDoc 仍写着
「不传保持旧行为(按 TRAVEL)」「不传=后端缺省 TRAVEL」,两句现已失效;
且全仓 809012 命中数为 0,该码今天无人接住。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 15:28:17 +08:00
Mimingguang f40c50f1bc chore(8510): 更新交付形态为独立详情页(hl-admin 8f1c982b)
changelog-filename-gate / validate (push) Failing after 2s
2026-09-30 15:24:05 +08:00
API Changelog Bot和Claude Opus 5 e1ae777695 docs(changelog): 订正 #8576/#8601/#8577 三份交接件的编造内容与 frontend_status
changelog-filename-gate / validate (push) Failing after 2s
三份都是既有条目(#8576/#8601 由 da562f3 批次产出,#8577 单独产出),本次逐条对源码与
测试服实测记录核对后订正,不新增条目。

#8576
- frontend_status 由 not_required 改回 pending(ca26be4 批量置位)。依据:hl-ui
  origin/v2.1 确有 src/api/fleet/group-dispatch.js 消费 reconfigure / confirm 两个端点,
  而全仓 specWarnings 命中数为 0 —— 新字段目前无人渲染,车辆规格提醒对车务不可见。
  改判原因已按 FRONTEND_CONSUMPTION_STATUS_GUIDE 要求写进 status_note。
- 两处错误响应示例的 message 是编造的,换成 GroupDispatchAdminErrorCode 的真实模板。
- planVersion 出参类型 Integer/Long 统一为 Long(两张表)。

#8601
- 删掉四处「589535 适用于本端点」的错误断言。实证:RequirementService.java:4749 对
  resourceType=VEHICLE 硬编码 hasActiveAssignments=false,该码在车需求打回上结构性不可达。
- 编造的订单 ID 2099459272533323777 换成实测的 2105173274755534850(dispatch)与
  2105173313083080706(reject)。
- 错误码集合订正为 809000 / 809007(仅 dispatch)/ 582031 / 582083。

#8577
- 订正一处「POST requirement/confirm 零影响」的错误断言:doConfirm 与 confirm-check 共用
  已收窄的 classifyVehicleSubmission,该端点的 809122 触发条件同步收窄。
- 809121 / 809123 的错误响应示例换成测试服实测原文。
- frontend_status 保留 not_required,但把判定依据写进 status_note:三个码一律走拦截器透
  message、生产代码无一处按报文匹配、前端也无纯接送机户的规避需要撤除;同时列出五处现已
  陈旧的前端注释与一处 mock 报文,供 mmg 顺手清理。

门禁:validate-changelog-frontmatter.mjs --files 三个文件一次通过(PASS: 3 files)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 15:17:17 +08:00
yaosutu dc080e389e docs(changelog): #8655 资金明细页补筛选控件对接指引(科目/账户/收支,接口已支持零改动)
changelog-filename-gate / validate (push) Failing after 2s
2026-09-30 14:58:19 +08:00
jw和Claude Opus 5.5 78eeac7a4f docs(changelog): #8642 团期详情进度条导摄分支按名册显示已完成
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:35:33 +08:00
Mimingguang 5933096d07 chore(8493): 回写前端交付 implemented(hl-admin 8c9f7bc5)
changelog-filename-gate / validate (push) Failing after 2s
2026-09-30 14:30:36 +08:00
API Changelog Bot和Claude Opus 5 b3de329869 docs(changelog): 8548 出参表标明待审户数的计数单位是户不是需求行
changelog-filename-gate / validate (push) Failing after 2s
同一户同时报行程用车与接送机用车只计 1 户。字段名 requirementReopenPendingHouseholds 与本单主题(两类用车需求并存)放在一起时,前端按需求行数理解会得到偏大的数。四个接口块各补一处,保持自包含。

Refs #8548

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 14:27:54 +08:00
API Changelog Bot和Claude Opus 5 877d69e651 docs(changelog): 8560 订正 requirementId 两卡是否同值按路径分档
changelog-filename-gate / validate (push) Failing after 2s
原文按真实行路径过度概括成「两张卡 requirementId 恒相同」。实测代码:MatrixService.java:497-498 真实行优先取订单侧单值(两卡相同),:637-638 虚拟待派条目优先取候选自身需求 ID(两卡不同),而未派订单最常见的形态正是后者。四处(正文口径、出参表、业务边界、测试说明)一并按路径分档,判类别只认 requirementKind 的结论不变、且更必要。

Refs #8560

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 14:24:30 +08:00
yaosutu和Claude Opus 4.8 687999963b docs(changelog): 团期核团详情统一接口 return-detail 上线,reports/group 旧路径已删 404(#8641)
changelog-filename-gate / validate (push) Failing after 2s
- 新增 GET /v3/admin/order/group-batch/{gid}/settlement/return-detail 团期核团详情统一总览
- 出参新增 driverVehicles 司机车辆连续区间(本地快照按司机+车辆合并,driverPhone 脱敏)
- 原 /settlement/reports/group 已下线,调用返回业务码 404(HTTP 200 包装)
- PR #8651 / Issue #8641

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-30 14:18:12 +08:00
Mimingguang 92fb65fe68 docs(changelog): #8510 团期核单 21 字段回写 implemented(hl-admin@a997595a)
changelog-filename-gate / validate (push) Failing after 1s
2026-09-30 13:51:42 +08:00
Mimingguang 42754367de docs(changelog): 29_frontend 审核弹窗 Epic 回写 implemented(hl-admin@80d7ad88)
changelog-filename-gate / validate (push) Failing after 1s
2026-09-30 13:18:00 +08:00
API Changelog Bot和Claude Opus 5 4445618696 docs(changelog): #8629 幂等键补对象身份段、#8630 团期活跃子订单清零自动复位
changelog-filename-gate / validate (push) Failing after 1s
- #8629 出行人/大交通新增的幂等键补上对象身份段,同订单录入第二个对象不再被误拒
- #8630 团期最后一户取消后自动复位 requirement_confirmed 与整团用车需求(CONFIRMED→DRAFT),
  并写 BATCH_REQUIREMENT_REOPENED 时间线(trigger=ALL_SUB_ORDERS_CANCELLED)

Refs #8629
Refs #8630

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 12:30:56 +08:00
API Changelog Bot和Claude Opus 5 77092f1ae4 docs(changelog): #8619 团期子订单列表——只报接送机的户不再判未提交
changelog-filename-gate / validate (push) Failing after 2s
GET /v3/admin/order/group-batch/{groupBatchId}/orders:
- requirementStatus/requirementStatusName 放宽取值范围(纯接送机户不再恒为「未提交」)
- 新增 vehicleRequirements[] 逐类用车需求清单(TRAVEL 在前、TRANSFER 在后)

Refs #8619

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 12:24:07 +08:00
API Changelog Bot和Claude Opus 5.5 d1ab03e7c0 docs(changelog): #8615 转房记录作废原因与退房给酒店口径调整
changelog-filename-gate / validate (push) Failing after 2s
- 转房列表 cancelReason 新增三个系统作废取值(本团已再分配 / 本团已撤销该晚计划 / 团期已解散)
- 退房给酒店(I-21)对团期来源行补批次门禁与超量码 808324
- 附测试环境实测结论

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 11:05:20 +08:00
yaosutu 3f3f5e7558 docs(changelog): 团期核单分类科目明细 tab——8 个新增读端点(#8632)
changelog-filename-gate / validate (push) Failing after 2s
团期核单新增 8 个分类明细只读端点(/v3/admin/order/group-batch/{id}/settlement/{hotels,activities,vehicles,guide-fees,photographer-fees,meals,other-expenses,other-incomes}),
数据来自团维度 order_batch_audit_item 按 category 过滤,命名对齐核心订单核单 tab。
管理后台目录 changelogs-v2/,关联 PR #8634。
2026-09-30 11:04:46 +08:00
yaosutu 315e9dbb2e 财务初始化三tab对齐原型:纠偏前端4tab为3tab(应收初始化/应付初始化/现金银行初始化),删员工往来tab(#8507 #8511)
changelog-filename-gate / validate (push) Failing after 2s
2026-09-30 10:32:47 +08:00
Mimingguang ca26be400d docs(changelog): 16 份前端判 not_required(#8621/#8603/#8559/#8577/#8593/#8576/#8536/#8528/#8571/#8561/#8597/#8491旧列表/#8533/#8508/#8515/#8613)
changelog-filename-gate / validate (push) Failing after 1s
2026-09-30 10:30:14 +08:00
API Changelog Bot和Claude Opus 5 0f9b98f084 docs(changelog): 团期用车需求读口补中文名 / 汇总草稿换读侧 VO / 并列下发团级已确认座位(#8544 #8545 #8619)
changelog-filename-gate / validate (push) Failing after 2s
四个读口的交接件:GET /vehicle-requirement 新增 4 个 *Name 字段、
GET /vehicle-requirement/aggregate-draft 的 draft 换成只读读侧 VO、
GET /requirement-summary 并列下发 confirmedSeats/confirmedCount、
GET /orders 的 vehicleRequirements[] 逐类下发用车需求行。

四个端点的响应示例全部为 2026-09-30 测试环境实测采集(团期批次
2104839654727618562 / 2104840641651556353),并标注了采集批次与时刻。
未知编码回落 null 这一支在当前环境无自然样本(三列库内全为 NULL),
已在第八节写明由源码与单测两侧钉住,不留悬念给前端。

三条给前端的硬约束:
- groups[].remark 不要按格式解析:本单只改新生成的汇总草稿,
  存量已确认团期仍是改前的订单号前缀格式,两种格式长期并存。
- 同一个 status 编码在不同 kind 上中文名不同(PENDING_REVIEW 在 TRAVEL
  下是「待提交车务」、在 TRANSFER 下是「待审核」,#8218 刻意分叉),
  前端不得自建 code 到中文的映射表,一律渲染后端下发的 statusName。
- 按类别判状态一律遍历 vehicleRequirements[],单值字段只表达展示序首条。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 10:14:49 +08:00
yaosutu a3dcb89d6e docs(finance): 司导往来账账页+明细两接口上线 changelog(#8626)
changelog-filename-gate / validate (push) Failing after 2s
新增 /admin/finance/statements/guide-ledger/page + /entries 两接口,
按带团服务人员(导游/司机/摄影/领队)聚合报账+预支现算净往来。
关联 PR #8574(初版)+ #8627(口径扩摄影/领队),Issue #8555 + #8626。
2026-09-30 10:04:41 +08:00
Mimingguang febdcfe063 docs(changelog): #8518 前端已交付(派单看板类别列+类别筛选,hl-admin@2f65d1f7)
changelog-filename-gate / validate (push) Failing after 2s
2026-09-30 09:48:14 +08:00
API Changelog Bot和Claude Opus 5 0491f00fa6 docs(changelog): 补 #8562 逐户用车提交态区分与 #8620/#8621 派车读口中文名交接件
changelog-filename-gate / validate (push) Failing after 2s
30_8562 覆盖 GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households:
新增 submitState 区分「从未提交」与「已被打回待重提」。四条限定写进关键变化与业务边界——
status 规则一字未改(判提交要读 submitState 不是 status)、requirements 内容零变化
(打回信息走 rejectedRequirements)、rejectedRequirements 与需求行刻意不同构不可当行渲染、
householdCount 不再恒等于 needs_vehicle=true 的户数。

30_8621 覆盖派车三个读口(board/orders、group-dispatch/pending-batches、
group-dispatch/batches/{groupBatchId}/overview):#8621 新增 4 个 *Label 字段,枚举码不再
裸下发;#8620 是 vehicleControlStatus 的读法澄清——字段名与取值域一字未改,改的是
「按哪套枚举去读」(真源是 order-v3 的 RequirementStatus,不是建表 SQL 里那条已陈旧的
COMMENT)。

两份 --files 校验全绿(校验 2 个对象,EXIT=0),正文零「等部署/另发/待补充」类前向引用。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 09:04:41 +08:00
API Changelog Bot和Claude Opus 5 c2f04d7b32 docs(changelog): 补 #8597 退团房期限清空与退团房待办交接件,并订正 #8491 里「三列一并置空」的错述
changelog-filename-gate / validate (push) Failing after 2s
新增 30_8597 覆盖两个端点:
- PUT /v3/admin/order/house-console/room-transfers/{id}/deadline
- GET /v3/admin/order/house-console/audit

同时就地订正已交付的 30_8491(4 行 / 3 处):原文写「cancelDays 传 null 时
cancelCutoff / remindDays 一并清空」,与源码相反。HouseRoomTransferManager#doUpdateDeadline
对 cancelDays == null 的分支是「cutoff / remind 沿用行上原值」——两列 NOT NULL,
传 null 会被 Mapper 守卫当失败返 0、进而被误报成并发修改(正是 #8597 的根因)。
30_8491 自己第 1847 行记录的实测读数也是「cancelCutoff 保留」,即该文件内部自相矛盾。

订正不是措辞问题:前端若按原文写 `cancelCutoff === null` 去判「有没有设期限」,
那个判断恒为 false。订正后同时写明正确判据是看 cancelDays 或 risk === "NO_DEADLINE"。

两文件 validate-changelog-frontmatter.mjs --files 全绿(校验 2 个对象,EXIT=0)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 07:46:23 +08:00
API Changelog Bot和Claude Opus 5.5 a60a791132 docs(changelog): #8491 房务控制台接口新增、旧列表下线、配房接口口径调整
changelog-filename-gate / validate (push) Failing after 1s
三份交接件:新增房务控制台接口,下线旧的房务列表接口,调整配房接口的字段与口径。每份的「测试环境已验证」一节填测试服实测读数。

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 06:42:04 +08:00
API Changelog Bot和Claude Opus 5 94fb72d79f docs(changelog): 补 #8598 #8613+#8614 #8593 三份交接件
changelog-filename-gate / validate (push) Failing after 1s
- 8598 派单保险隔离事件新增人工终结出口 DISCARDED(新增接口)
- 8613+8614 605072 恢复动作文案订正 + 605002 座位强禁码下线口径澄清(修复)
- 8593 待配车团期清单 transferPendingCount 由硬编码 0 改真值,取不到给 null(修改接口)

三份均经 validate-changelog-frontmatter.mjs 与 changelog_workflow.py lint 双门禁 PASS,
并做过双向串味自检(他域关键词命中 0、本域关键词命中非 0)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 06:29:16 +08:00
API Changelog Bot和Claude Opus 5 da562f36d4 docs(changelog): 补 5 张已合并工单的前端交接件(#8559 #8576 #8577 #8601 #8603)
changelog-filename-gate / validate (push) Failing after 1s
- #8559 团期用车户数计入汇总读数不再随 kind 筛选变化
- #8576 团期配车提交与确认响应新增车辆规格提醒清单
- #8577 只提交接送机的户不再被判「未提交用车需求」
- #8601 逐户提交车务与打回的 kind 参数取消默认值,新增 809012
- #8603 派单确认响应删除恒空的接送机缺失日期字段

后端均已部署测试服(order-v3 / fleet @ d57498d381),网关实测通过。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 05:51:38 +08:00
API Changelog Bot和Claude Opus 5.5 487f5fb399 docs(changelog): #8508 调整已确认配房行时处理应付台账(在途付款申请拒绝 599602)
changelog-filename-gate / validate (push) Failing after 1s
Refs wx/HL#8508

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 04:20:42 +08:00
API Changelog Bot和Claude Opus 5 52548a02a1 docs(changelog): 团期配车三项契约变更交接件(#8543 #8548 #8549 #8560)
changelog-filename-gate / validate (push) Failing after 2s
- #8543 团期订单 Tab 用车状态三处读口按需求类别拆分并统一文案(PR #8592)
- #8548/#8549 整团免车放行户级接送机需求,换组重开团期补待审户读数(PR #8586)
- #8560 矩阵未派订单卡下发 requirementKind,可区分行程用车与接送机(PR #8589)

三份均已在测试网关实测取证,两道门禁(frontmatter 校验 + changelog_workflow lint)全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 03:57:16 +08:00
API Changelog Bot和Claude Opus 5 14d84237e7 docs(changelog): 派单看板矩阵年月校验与团期配车详情三条交接件(#8561 #8571 #8556)
changelog-filename-gate / validate (push) Failing after 2s
- #8561 派单矩阵三入口补年份区间校验,越界返新错误码 605076
- #8571 未派订单清单月份越界改由 Service 判定,与 matrix 另两个入口同构返 605010
- #8556 派单看板订单详情识别团期配车,排车步与实派车数不再只认逐户派车行

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 02:22:05 +08:00
API Changelog Bot和Claude Opus 5.5 2bce6cb8ea docs(changelog): #8493 下线房务组长会话入口 open-house-lead
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 00:44:55 +08:00
yaosutu aeedf41639 docs(changelog): 团期核单详情 frontmatter 补 hl-changelog/v2 必需 key(#8510)
changelog-filename-gate / validate (push) Failing after 1s
补 base / updated_at,修正为完整 v2 schema 以过 hl-changelog-gate。
2026-09-30 00:17:05 +08:00
yaosutu 447b3294a9 docs(changelog): 团期核单详情对齐常规订单核单——reports/group 出参新增 21 字段(#8510)
聚合复核 GET /v3/admin/order/group-batch/{id}/settlement/reports/group 出参对齐常规订单核单财务总览:
新增 11 金额字段 + 4 个 mirrorMatched + customers 客户合并列表 + 6 个人数汇总字段,
数据从单订单换成全团在团子订单合并(排除已取消),待收尾款与 /finance 同源。

管理后台目录 changelogs-v2/,关联 PR #8546。
2026-09-30 00:12:44 +08:00
共修改 108 个文件,包含 35773 行新增和 69 行删除
@@ -0,0 +1,278 @@
---
schema: hl-changelog/v2
ticket: "8739"
title: "小程序团期下单撞上改期联动时最多等 5 秒,超时返回 100503,不再误报请勿重复提交"
consumer: mp
author: wx(GIT)
change_type: 修改接口
backend_status: deployed
gateway_status: not_required
frontend_status: pending
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: ""
updated_at: "2026-10-03"
base: dev-v3
---
# 小程序团期下单撞上改期联动时最多等 5 秒,超时返回 100503,不再误报请勿重复提交
## ⚠️ 关键变化
- 场景:`POST /mp/order/create` 带 `groupBatchId` 下团期单,而管理端此刻正在给该团期「改出发日」并联动订单(#8666 引入的联动)。
- 下单会先等联动结束,最多等 5 秒。
- 5 秒内联动结束:按改后的日期正常成单,响应比平时慢几秒。
- 5 秒后联动仍未结束:返回 `100503`「资源被占用,请稍后重试」,不成单,可以直接重提。
- 改前(#8666 上线后、本次之前),同一场景下小程序约 3.5 秒就收到 `100502`「请勿重复提交订单」。测试服实测:
- 联动在 5 秒内结束时,后台其实**已经成单**;
- 联动占用超过 5 秒时,**没有成单**。
也就是说,这条路径上的 `100502` 既可能是「成了」,也可能是「没成」。本次起这条路径不再返回 `100502`。
- 小程序服务等订单服务的上限由 2 秒放宽到 10 秒,超时后也不再自动重发下单请求。
- 超过 10 秒仍无结果时,返回 `500`「服务暂时不可用,请稍后重试」。
- 此时订单服务端的那次创单不会因此中止,订单**可能已经生成**。
- 不带 `groupBatchId` 的下单不等联动、不会多等。10 秒上限和「不自动重发」对它同样适用。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 创建订单 | POST | `/mp/order/create` | 错误响应语义变更 | 团期下单撞上改期联动时最多等 5s,超时返 100503,该路径不再返 100502;服务端超过 10s 返 500,不再自动重发 |
## 三、接口详情
### 1. 创建订单 `POST /mp/order/create`
**VO**: `MpCreateOrderRequest → MpOrderDetailVO`
#### 使用场景
用户在产品详情页选好档位、人数并填好联系人后提交下单。
- 本次只改变下单失败和变慢时返回什么。
- 对带 `groupBatchId` 的团期单影响最大:该团期正被管理端改出发日时,下单会先等联动结束。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| productId | Body | String | 是 | 不能为空;必须是数字字符串,否则返回 600002「产品ID格式错误」 | 产品 ID(对外收字符串,防止 JS 精度丢失) |
| groupBatchId | Body | String | 否 | GROUP 产品传;null 或空串视为未指定;非数字返回 600003「团期ID格式错误」;≤0 视为未指定 | 团期 ID。有效时下单与该团期的改期联动互斥 |
| departureDate | Body | LocalDate | 否 | `yyyy-MM-dd` | 出发日期 |
| adultCount | Body | Integer | 否 | ≥1,缺省 1 | 成人数 |
| childCount | Body | Integer | 否 | ≥0,缺省 0 | 儿童数 |
| youngChildCount | Body | Integer | 否 | ≥0,缺省 0 | 小童数 |
| babyCount | Body | Integer | 否 | ≥0,缺省 0 | 幼童数 |
| childNeedBed | Body | Boolean | 否 | 缺省 false | 儿童是否需要床位 |
| tierSeq | Body | Integer | 是 | ≥1 | 档位序号 |
| roomCount | Body | Integer | 否 | 传则 ≥1 | 房间数 |
| sharerOpenid | Body | String | 否 | ≤64;可传空串;非空时只能含字母、数字、下划线、短横线 | 分享人 OpenID |
| customizerId | Body | Long | 否 | ≥1 | 分享人定制师 ID |
| contactName | Body | String | 是 | 非空白,≤50 | 联系人姓名 |
| contactPhone | Body | String | 是 | 非空白,≤20 | 联系人电话 |
| remark | Body | String | 否 | ≤500 | 订单备注 |
本次请求字段零变更。上表为完整字段表,与改前一致。
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| orderId | String | 订单 ID。后端为 Long,超出 JS 安全整数范围时序列化为字符串,一律按字符串处理 |
| orderNo | String | 订单编号,格式为 `HL` + 年月日时分秒 + 3 位毫秒 |
| groupCode | String | 4 位团号,订金支付成功时生成;未生成时该字段不出现 |
| groupBatchId | String | 团期 ID(序列化同 orderId),只有团期订单才出现 |
| productId | String | 产品 ID(序列化同 orderId) |
| departureDate | LocalDate | 出发日期 |
| returnDate | LocalDate | 返程日期 |
- 响应对象带 `@JsonInclude(NON_NULL)`:值为空的字段(如订金支付前的 `groupCode`)直接不出现,不会以 `null` 返回。前端按「字段缺失」判空。
- 本次响应字段零变更。出参与 `GET /mp/order/{orderId}` 详情接口同构;上表只列标识字段,完整字段见既有详情接口文档。
#### 请求示例
```json
{
"productId": "1900123456789000001",
"groupBatchId": "1900123456789000002",
"departureDate": "2027-01-27",
"adultCount": 2,
"childCount": 1,
"tierSeq": 1,
"contactName": "林晓梅",
"contactPhone": "13900001234",
"remark": "老人同行,希望安排低楼层"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "1900123456789000003",
"orderNo": "HL20270127143025001",
"groupBatchId": "1900123456789000002",
"productId": "1900123456789000001",
"departureDate": "2027-01-27",
"returnDate": "2027-02-01"
},
"traceId": null,
"success": true
}
```
#### 空数据 / 降级响应
本接口没有空数据形态。
- 订单服务等待超过 10 秒,或暂时不可达时,返回下方错误响应里的 5xx(文案「服务暂时不可用,请稍后重试」),不会返回半成品数据。
- 上述错误响应的 HTTP 状态码都是 200,结果看 `code`。
#### 错误响应
团期下单撞上改期联动,等满 5 秒联动仍未结束(不成单,可以直接重提):
```json
{
"code": 100503,
"message": "资源被占用,请稍后重试",
"data": null,
"traceId": null,
"success": false
}
```
订单服务超过 10 秒没给出结果(订单可能已经生成,先查订单列表再决定是否重提):
```json
{
"code": 500,
"message": "服务暂时不可用,请稍后重试",
"data": null,
"traceId": null,
"success": false
}
```
真的重复提交(上一次下单请求已被受理或仍在处理,不要自动重提):
```json
{
"code": 100502,
"message": "请勿重复提交订单",
"data": null,
"traceId": null,
"success": false
}
```
#### 业务边界
- **什么时候会等**:只有 `groupBatchId` 解析为正数时,下单才与该团期的改期联动互斥。不带团期的下单不等。
- **等多久**:最多 5 秒,后端常量,前端不可调整。测试服实测一次改期联动占用 41–688 毫秒(12 次),所以多数情况是「慢一点但成单」,`100503` 只在联动占用超过 5 秒时出现。
- **`100503`**:不成单,因为等锁失败时创单业务还没开始执行。可以直接重提,不需要任何清理。
- 重提不会撞 `100502`。小程序层的防重窗口 5 秒,`100503` 返回时已过;订单服务层的防重键在 `100503` 时随失败释放。
- **`500` 等 5xx(等待超过 10 秒或订单服务不可达)**:小程序服务不再自动重发,但订单服务端已开始的那次创单不会中止,**可能已经成单**。
- 此时直接重提可能产生两张订单:订单服务层同用户同产品的防重窗口是 10 秒,从原请求开始计时,到 `500` 返回时已基本过期。(按代码机制推导,测试服未构造过超过 10 秒的场景。)
- 建议提示「网络繁忙,请到我的订单查看」,先刷新订单列表,确认没有新订单再让用户重提。
- **`100502` 现在只表示真的重复提交**,有两层窗口:
- 小程序层:同一用户 5 秒内第二次下单,不分产品。成功下单后 5 秒内再下也算。
- 订单服务层:同一用户同一产品 10 秒内第二次下单。
收到时说明前一次请求已被受理或仍在处理,引导用户到订单列表查看,不要自动重提。
- **`100501`「下单过于频繁,请稍后再试」**:订单服务层按用户限流,每个用户 60 秒内最多 5 次下单请求。既有行为,未变。
## 四、契约约束与正确调用方式
1. 按 `code` 区分错误,不要按 `message` 判断。`100502`、`100503` 和 5xx 的文案相近,处理方式完全不同。
2. 下单请求的前端超时不要短于 15 秒:服务端最长 10 秒给出结果,另有网关与网络开销。前端先放弃时,用户看不到 `100503`,后台却可能已成单。
3. 提交期间禁用提交按钮,直到收到响应。团期下单撞上改期联动时,正常响应也可能慢约 5 秒。
4. 收到 `100503` 可以直接重提原请求。
5. 收到 5xx(`code` ≥ 500)先刷新订单列表,确认没成单再重提。
6. 收到 `100502` 不要自动重提,引导用户到订单列表查看。
## 五、数据库行为
| 结果 | 数据库写入 |
|------|------------|
| `100503` | 零写入:等锁失败发生在创单业务开始之前 |
| `100502` / `100501` | 零写入:在进入创单业务前就被拦截 |
| 5xx(等待超过 10 秒) | 订单服务端那次创单照常提交或失败,与小程序有没有收到结果无关 |
| 成功 | 写入与改前完全一致;本次不涉及任何表结构变更 |
## 六、边界行为
- 两个时限(等锁 5 秒、小程序服务调订单服务超时 10 秒)都是服务端固定值,请求参数无法调整。
- 本接口与管理端创单复用同一套创单内核,锁判断完全一致。
- 管理端用 `productBatchId` 数字。
- 小程序用 `groupBatchId` 字符串,两者是同一个 ID。
## 六.6、修改前后对比
### 字段级对比
本接口请求、响应字段均**零变更**,变的只是失败和变慢时的返回。
### 行为级对比
| 场景 | 改前(#8666 上线后、本次之前) | 改后 |
|------|------|------|
| 团期下单,没撞上改期联动 | 正常成单 | 正常成单,无差异 |
| 团期下单,撞上改期联动且联动在 5 秒内结束 | 约 3.5 秒返回 `100502`,后台其实已成单 | 等联动结束后正常成单,响应最多慢约 5 秒 |
| 团期下单,联动占用超过 5 秒 | 约 3.5 秒返回 `100502`,没有成单 | 约 5 秒返回 `100503`,没有成单,可直接重提 |
| 任意下单,订单服务处理超过 2 秒 | 小程序服务约 2 秒超时后自动重发同一请求,用户可能收到 `100502` 而后台已成单 | 等到 10 秒;超过 10 秒返回 `500`,不重发 |
在 #8666 之前,下单完全不与改期联动互斥。撞上改期时可能按旧出发日成单,且日期与团期永久错位、无人察觉。#8666 堵住了这个窗口,本次修正的是它在小程序链路上的错误码表现。
## 六.7、影响评估
- **是否破坏向后兼容**:否。请求、响应字段零变更,正常成单路径不变。
- **前端是否必须同步上线**:否。未识别 `100503` 时展示原始 `message` 不会白屏。但若前端下单请求的超时短于 15 秒,需要调整,见第四节第 2 条。
- **前端 workaround 清理点**:无。
## 七、不影响范围
- 小程序其他订单接口(列表、详情、取消、改单等)调订单服务的等待上限与重试策略**未变**。
- 请求、响应字段零变更。
- 网关路由零变更,无数据库结构变更。
## 八、测试环境已验证
2026-10-03 测试服部署 hl-mp-service(dev-v3 `f4e45c2f1`)后实测。做法:用测试 C 端用户直连小程序服务,在自建团期上人为占住改期联动锁。
| 场景 | 响应 | 耗时 | 成单 | 订单服务收到的请求 |
|------|------|------|------|------|
| 联动占用约 3.5 秒 | 成功,`data` 为完整订单详情 | 约 3.5 秒 | 1 单 | 1 次 |
| 联动占用约 6 秒 | `100503`「资源被占用,请稍后重试」 | 约 5 秒 | 0 | 1 次 |
| 上一行锁释放后间隔 ≥5 秒重提 | 成功 | 正常 | 1 单 | 1 次 |
| 收到 `100503` 后 0.3 秒内立即重提(锁剩余不足 1 秒) | 未返回 `100502`,等联动结束后成功 | 未单独计时 | 1 单 | 2 次,即两次真实提交,无自动重发 |
| 不占锁的常规下单 | 成功 | 正常 | 1 单 | 1 次 |
- 常规下单的响应 `data` 与 `GET /mp/order/{orderId}` 的 `data` 做了字段集比对:61 个字段路径,零差异,二者同为 `MpOrderDetailVO`。
- 改前基线是部署前用同一夹具、同一方法测的:
- 联动占用约 3.5 秒:约 3.5 秒返回 `100502`,但后台已成单,订单服务收到 2 次。
- 联动占用约 6 秒:同样返回 `100502`,未成单。
- 本轮未构造订单服务超过 10 秒才返回的场景,所以 `500` 之后可能已成单这一点仍是按代码推导,见业务边界。
## 十、相关文档
- 管理端同源改动:`changelogs-v2/2026-10/03_8666_产品班期改出发日联动团期与子单日期并新增改期拒绝码-修改接口-管理后台.md`(#8666)。改期联动本身的事件与拒绝码以该条为准。
- 内部转发路径:`POST /mp/order/create`(hl-mp-service)→ `POST /v3/internal/mp/order/create`(order-v3)。`/v3/internal/**` 只供服务间调用,不经网关,前端不可达。
- `100503` 是全仓统一的锁竞争可重试错误码。
## 关联 / 联系人
### 链接
- **Issue**: [#8739](https://git.1814.love/wx/HL/issues/8739)(本次)、[#8666](https://git.1814.love/wx/HL/issues/8666)(引入改期联动锁)
- **PR**: [#8740](https://git.1814.love/wx/HL/pulls/8740)、[#8738](https://git.1814.love/wx/HL/pulls/8738)
- **Merge commit**: `f4e45c2f147eb5e0deec4dbd920e6c82c496ec7b`(#8740)、`fad5d7814b2d1fb940d740457dbed754e46d03f9`(#8738)
### 联系人
- **后端负责人**: @wx
@@ -1,11 +1,18 @@
---
schema: "hl-changelog/v1"
schema: "hl-changelog/v2"
ticket: "5186"
title: "排车中订单恢复派车派人入口并补齐改派上下文"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
change_type: "修改接口"
backend_status: "verified"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude)"
frontend_ref: "158f728ce9110ca6a9e0185c137969f84b522c6f"
target_release: "v2.1"
verified_at: "2026-10-09"
status_note: "7 月历史补标:已于 2026-07-23 交付(恢复排车中订单派车派人入口,158f728ce 起含 2f4d278a6/54c007856 后续修正),证据为交付 commit 正文标注 #5186"
updated_at: "2026-10-09"
base: "dev-v3"
generated: "2026-07-23T16:10:00+08:00"
---
@@ -1,11 +1,18 @@
---
schema: "hl-changelog/v1"
schema: "hl-changelog/v2"
ticket: "5194"
title: "待确认详情补全多车多司机与按槽位改派"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
change_type: "修改接口"
backend_status: "verified"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude)"
frontend_ref: "bfb4e3cde018d14b2b5dedaacacb83128cc7714d"
target_release: "v2.1"
verified_at: "2026-10-09"
status_note: "7 月历史补标:已于 2026-07-23 交付(多槽位改派与司机确认,bfb4e3cde),证据为交付 commit 正文标注 #5194"
updated_at: "2026-10-09"
base: "dev-v3"
generated: "2026-07-23T18:02:00+08:00"
---
@@ -1,11 +1,18 @@
---
schema: "hl-changelog/v1"
schema: "hl-changelog/v2"
ticket: "5199"
title: "车务首页订单去重与字段补全"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
change_type: "修改接口"
backend_status: "verified"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude)"
frontend_ref: "3c4f8619bbc569d72e73cedba9a74b2cefc67402"
target_release: "v2.1"
verified_at: "2026-10-09"
status_note: "7 月历史补标:已于 2026-07-24 交付(车务首页订单聚合契约对齐,3c4f8619b),证据为交付 commit 正文标注 #5199"
updated_at: "2026-10-09"
base: "dev-v3"
generated: "2026-07-24T09:46:00+08:00"
---
@@ -1,11 +1,18 @@
---
schema: "hl-changelog/v1"
schema: "hl-changelog/v2"
ticket: "5200"
title: "用车需求驳回历史与重新提交"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
change_type: "修改接口"
backend_status: "verified"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude)"
frontend_ref: "d7afc25146ed1bceea3e3722c521babd85c1345e"
target_release: "v2.1"
verified_at: "2026-10-09"
status_note: "7 月历史补标:已于 2026-07-24 交付(用车需求驳回历史与重新提交,d7afc2514),证据为交付 commit 正文标注 #5200"
updated_at: "2026-10-09"
base: "dev-v3"
generated: "2026-07-24T11:05:00+08:00"
---
@@ -1,11 +1,18 @@
---
schema: "hl-changelog/v1"
schema: "hl-changelog/v2"
ticket: "5205"
title: "车务首页汇总与看板人员类型展示"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
change_type: "修改接口"
backend_status: "verified"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude)"
frontend_ref: "78cbb1565b6555788607b34f591f0291a87af96d"
target_release: "v2.1"
verified_at: "2026-10-09"
status_note: "7 月历史补标:已于 2026-07-24 交付(车务汇总与人员类型展示对齐,78cbb1565),证据为交付 commit 正文标注 #5205"
updated_at: "2026-10-09"
base: "dev-v3"
generated: "2026-07-24T10:45:00+08:00"
---
@@ -1,11 +1,18 @@
---
schema: "hl-changelog/v1"
schema: "hl-changelog/v2"
ticket: "5132"
title: "车务派单详情补全产品、行程节点、出行人与大交通"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
change_type: "修改接口"
backend_status: "verified"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude)"
frontend_ref: "524cfb21d24074e8e22c5ca7a89cbbd693f59abb"
target_release: "v2.1"
verified_at: "2026-10-09"
status_note: "7 月历史补标:已于 2026-07-22 交付(派单详情新增字段展示/出行人类型区分,524cfb21d 起 3 个 commit),证据为交付 commit 正文标注 #5132"
updated_at: "2026-10-09"
base: "dev-v3"
generated: "2026-07-22T10:35:00+08:00"
---
@@ -1,11 +1,18 @@
---
schema: "hl-changelog/v1"
schema: "hl-changelog/v2"
ticket: "5139"
title: "车务派单候选筛选、分页与任意车辆选择"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
change_type: "修改接口"
backend_status: "verified"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude)"
frontend_ref: "a0e8a6ef15a5a1983ddab3ca2958444e7971ae77"
target_release: "v2.1"
verified_at: "2026-10-09"
status_note: "7 月历史补标:已于 2026-07-22 交付(派单候选筛选与任意车辆选择接入,a0e8a6ef1),证据为交付 commit 正文标注 #5139"
updated_at: "2026-10-09"
base: "dev-v3"
generated: "2026-07-22T13:25:00+08:00"
---
@@ -1,11 +1,18 @@
---
schema: "hl-changelog/v1"
schema: "hl-changelog/v2"
ticket: "5141"
title: "车务派单司机保险类型与行程保障状态"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
change_type: "修改接口"
backend_status: "verified"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude)"
frontend_ref: "29ab0d6562413be6ab715d8b1e1eb2d4521a9eff"
target_release: "v2.1"
verified_at: "2026-10-09"
status_note: "7 月历史补标:已于 2026-07-22 交付(司机保险保障状态与线上投保接入,29ab0d656),证据为交付 commit 正文标注 #5141"
updated_at: "2026-10-09"
base: "dev-v3"
generated: "2026-07-22T14:07:00+08:00"
---
@@ -1,11 +1,18 @@
---
schema: "hl-changelog/v1"
schema: "hl-changelog/v2"
ticket: "5145"
title: "选车后常驻司机默认配对"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
change_type: "修改接口"
backend_status: "verified"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude)"
frontend_ref: "b9994da9308ae84d361e79e86f74200813ec06cc"
target_release: "v2.1"
verified_at: "2026-10-09"
status_note: "7 月历史补标:已于 2026-07-22 交付(选车后常驻司机默认配对接入,b9994da93),证据为交付 commit 正文标注 #5145"
updated_at: "2026-10-09"
base: "dev-v3"
generated: "2026-07-22T15:30:00+08:00"
---
@@ -1,11 +1,18 @@
---
schema: "hl-changelog/v1"
schema: "hl-changelog/v2"
ticket: "5146"
title: "司机待确认通知与可选确认凭证"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
change_type: "修改接口"
backend_status: "verified"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude)"
frontend_ref: "089e55ffb6f4336c19ffd2ed2eade131bdb35bf7"
target_release: "v2.1"
verified_at: "2026-10-09"
status_note: "7 月历史补标:已于 2026-07-22 交付(司机确认登记与可选凭证接入,089e55ffb),证据为交付 commit 正文标注 #5146"
updated_at: "2026-10-09"
base: "dev-v3"
generated: "2026-07-22T15:20:00+08:00"
---
@@ -1,11 +1,18 @@
---
schema: "hl-changelog/v1"
schema: "hl-changelog/v2"
ticket: "5149"
title: "派单详情补充团号与产品类型"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
change_type: "修改接口"
backend_status: "verified"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude)"
frontend_ref: "ccbdd0bd1b7c903846c8ae9e4e9e15d80266b9a4"
target_release: "v2.1"
verified_at: "2026-10-09"
status_note: "7 月历史补标:已于 2026-07-22 交付(派单详情补充团号与产品类型,ccbdd0bd1),证据为交付 commit 正文标注 #5149"
updated_at: "2026-10-09"
base: "dev-v3"
generated: "2026-07-22T16:22:00+08:00"
---
@@ -0,0 +1,244 @@
---
schema: "hl-changelog/v2"
ticket: "8510"
title: "团期核单聚合复核 reports/group 出参新增 21 字段,对齐常规订单核单财务总览"
consumer: "admin"
author: "yst(GIT)"
change_type: "修改接口"
backend_status: "required"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "hl-admin"
frontend_ref: "8f1c982b831ae11b0bd9783f812ca0ecf4dcb90d"
target_release: "v2.1"
verified_at: "2026-09-30"
base: "dev-v3"
updated_at: "2026-09-30"
status_note: "团期核单「聚合复核」GET /v3/admin/order/group-batch/{groupBatchId}/settlement/reports/group 出参 GroupSettlementRespVO 新增 21 个字段(11 金额字段 + 4 个 mirrorMatched + customers 客户合并列表 + 6 个人数汇总字段),结构与常规订单核单财务总览完全对齐,唯一区别是数据从单订单换成全团在团子订单合并(排除已取消)。关键:待收尾款 outstandingAmount 与 /finance 应收台账同源,前端不要再自行用「应收−已付」硬减;primaryReporterCollectedAmount 已含在 offlinePaidAmount 内勿重复加总。后端已合并待部署,部署后行为验证。前端可按 §5 字段表接入团期核单详情财务总览区(与常规订单核单同一套渲染)。前端已交付:收款总览(实时)11 金额+客户与人数 6 档+customers 一户一行,未部署 key 缺失整块隐藏;2026-09-30 拍板改独立详情页 /finance/settlement/group/:id(骨架对齐常规核单详情两步流程,核单录入 AuditTab+聚合复核 finalize/confirm),行弹层与 GroupSettlementPanel 已删。"
---
# 团期核单详情对齐常规订单核单 —— 修改接口(管理后台)
> Issue: https://git.1814.love/wx/HL/issues/8510
> PR: https://git.1814.love/wx/HL/pulls/8546
> Commit: https://git.1814.love/wx/HL/commit/306f9873d30a567331afed12b91c45af176418bf
> 负责人:腰苏图
---
## 1. 接口背景
团期核单(一团一核单)的「聚合复核」接口此前只返回团级汇总快照(订单总额/已收/欠收),与常规订单核单详情的财务总览结构不一致,缺线上/线下拆分、主报账人代收、客户合并信息、人数分档等字段,前端无法像常规订单核单一样渲染完整详情。
本次把团期核单的财务总览**完全对齐常规订单核单**:字段平铺结构与常规订单 `SettlementFinancialOverviewRespVO` 一致,唯一区别是**数据来源从单个订单换成全团所有在团子订单合并**(排除已取消子订单)。
---
## 2. 变更清单
| 类型 | 接口 | 说明 |
|---|---|---|
| 修改 | `GET /v3/admin/order/group-batch/{groupBatchId}/settlement/reports/group` | 出参 `GroupSettlementRespVO` 新增 21 个字段(11 金额字段 + 4 mirrorMatched + customers 客户合并列表 + 6 人数汇总字段) |
入参无变化;无字段删除、无类型变更、无枚举变更。纯**出参新增字段**,前端旧逻辑可继续按原字段渲染,新字段为增强。
---
## 3. 接口详情
- **方法/路径**:`GET /v3/admin/order/group-batch/{groupBatchId}/settlement/reports/group`
- **鉴权**:管理后台登录(与团期核单录入同权限)
- **说明**:团期核单聚合复核详情。`finalized=false`(未结算)时同样实时装配财务总览,字段照常返回。
---
## 4. 入参
无变化。路径参数 `groupBatchId`(团期 ID,Long)。
---
## 5. 出参
在原有团级汇总字段基础上,**新增**以下字段(与常规订单核单财务总览同口径、同平铺风格)。所有金额为 `BigDecimal`,货币单位元;缺省时返回 `0`(不返回 null)。
### 5.1 金额字段(11 个,全团在团子订单合并求和)
| 字段 | 类型 | 含义 |
|---|---|---|
| `baseOrderAmount` | BigDecimal | 订单基础金额合计(Σ 各户 order_amount) |
| `otherIncomeAmount` | BigDecimal | 有效增费合计(Σ 各户 surcharge_amount) |
| `discountAmount` | BigDecimal | 有效优惠合计(Σ 各户 discount_amount) |
| `adjustedReceivableAmount` | BigDecimal | 调整后应收 = Σ 各户 calcPayable = order + surcharge − discount |
| `onlinePaidAmount` | BigDecimal | 成功线上支付合计(权威源,Σ 各户成功线上交易) |
| `offlinePaidAmount` | BigDecimal | 有效线下收款合计(权威源,Σ 各户有效手工收款) |
| `primaryReporterCollectedAmount` | BigDecimal | 各户主报账人(DRIVER_CASH 渠道)代收合计;仅展示,已包含在 `offlinePaidAmount` 内 |
| `paidAmount` | BigDecimal | 已收合计(镜像口径 Σ 各户 paid_amount,与 outstanding 同源) |
| `actualRefundedAmount` | BigDecimal | 实际退款合计(镜像口径 Σ 各户 refunded_amount) |
| `netPaidAmount` | BigDecimal | 净已收 = paidAmount − actualRefundedAmount |
| `outstandingAmount` | BigDecimal | **待收尾款** = Σ 各户 calcBalance(= calcPayable − refunded − paid,已取消恒 0、下限 0),与 `/finance` 应收台账同源 |
### 5.2 镜像校验位(4 个)
| 字段 | 类型 | 含义 |
|---|---|---|
| `surchargeMirrorMatched` | Boolean | 增费镜像是否匹配权威明细(数据质量信号,仅供内部核查) |
| `discountMirrorMatched` | Boolean | 优惠镜像是否匹配权威明细 |
| `paidMirrorMatched` | Boolean | 已收镜像是否匹配权威明细 |
| `refundedMirrorMatched` | Boolean | 退款镜像是否匹配权威明细(**仅对拍已退完成 REFUNDED 口径**,在途退款不计,避免在途期间恒 false) |
> 前端一般不需要展示这 4 个字段,它们是给后端/排查用的数据质量信号。
### 5.3 客户合并信息 `customers`
| 字段 | 类型 | 含义 |
|---|---|---|
| `customers` | `List<GroupSettlementCustomerItemVO>` | 全团在团子订单的客户信息,一户一行 |
**GroupSettlementCustomerItemVO**(一户一行):
| 字段 | 类型 | 含义 |
|---|---|---|
| `orderId` | String | 子订单 ID(Long 序列化为字符串,防 JS 精度丢失) |
| `orderNo` | String | 子订单号 |
| `teamNo` | String | 子订单团内编号(如有) |
| `customerName` | String | 联系人姓名(**明文**,与团期财务列表现状一致) |
| `customerPhone` | String | 联系人手机号(**已脱敏**,如 `138****5678`) |
| `travelerCount` | Integer | 该户人数(含婴儿) |
### 5.4 人数汇总(6 个,含婴儿统一口径)
| 字段 | 类型 | 含义 |
|---|---|---|
| `householdCount` | Integer | 在团户数(排除已取消子订单) |
| `travelerCount` | Integer | 总人数 = adult + child + youngChild + **baby** |
| `adultCount` | Integer | 成人数 |
| `childCount` | Integer | 儿童数 |
| `youngChildCount` | Integer | 幼儿数 |
| `babyCount` | Integer | 婴儿数 |
---
## 6. 枚举 / 数据字典
无新增枚举。本接口不复用订单状态枚举,全部为金额/客户/人数字段。
---
## 7. 错误码
无新增错误码。复用团期域既有错误码(如团期不存在返回对应业务错误)。
---
## 8. 示例
### 8.1 典型(已结算团,finalized=true)
```json
{
"code": 0,
"data": {
"groupBatchId": "123",
"finalized": true,
"baseOrderAmount": 12800.00,
"otherIncomeAmount": 600.00,
"discountAmount": 300.00,
"adjustedReceivableAmount": 13100.00,
"onlinePaidAmount": 8000.00,
"offlinePaidAmount": 2600.00,
"primaryReporterCollectedAmount": 1200.00,
"paidAmount": 10600.00,
"actualRefundedAmount": 0.00,
"netPaidAmount": 10600.00,
"outstandingAmount": 2500.00,
"surchargeMirrorMatched": true,
"discountMirrorMatched": true,
"paidMirrorMatched": true,
"refundedMirrorMatched": true,
"householdCount": 2,
"travelerCount": 6,
"adultCount": 4,
"childCount": 1,
"youngChildCount": 0,
"babyCount": 1,
"customers": [
{
"orderId": "9001",
"orderNo": "HL20260901001",
"teamNo": "A1",
"customerName": "张三",
"customerPhone": "138****5678",
"travelerCount": 3
},
{
"orderId": "9002",
"orderNo": "HL20260901002",
"teamNo": "A2",
"customerName": "李四",
"customerPhone": "139****1234",
"travelerCount": 3
}
]
}
}
```
### 8.2 边界(未结算团,finalized=false,字段照常返回)
未结算(团期未 finalize)时,财务总览字段仍**实时装配**返回(金额字段/客户列表/人数照常),`finalized=false`,无快照段。前端可直接用同一套字段渲染,无需区分。
### 8.3 异常(团期不存在)
```json
{
"code": 5xxxxx,
"msg": "团期不存在"
}
```
---
## 9. 业务边界
- **数据范围**:金额与客户合并**只统计在团子订单**,**排除已取消(CANCELLED)子订单**。已取消订单的金额不计入任何合计。
- **待收尾款口径**:`outstandingAmount` 与 `/finance` 应收台账 `totalPendingBalance` 同源(都是 Σ calcBalance,含实退、下限 0),两端数值可对拍一致。
- **主报账人代收**:`primaryReporterCollectedAmount` 仅展示用,其金额已包含在 `offlinePaidAmount` 中,**不要重复加总**。
- **手机号**:`customerPhone` 已脱敏;`customerName` 明文(管理后台财务查看权限内,与团期财务列表现状一致)。
---
## 10. 修改前后对比(修改类)
| 维度 | 修改前 | 修改后 |
|---|---|---|
| 出参结构 | 仅团级汇总快照(订单总额/已收/欠收等粗粒度) | 新增 21 字段:11 金额 + 4 mirrorMatched + customers + 6 人数,与常规订单核单财务总览同结构 |
| 线上/线下拆分 | 无 | 有(onlinePaidAmount / offlinePaidAmount / primaryReporterCollectedAmount) |
| 待收尾款 | 无(前端需自行用「应收−已付」硬减,未扣实退口径错误) | 有(outstandingAmount,与 /finance 同源,口径正确) |
| 客户信息 | 无 | 有(customers 一户一行,含脱敏手机号) |
| 人数 | 无 | 有(含婴儿 6 个分档汇总) |
---
## 11. 影响评估 / 回滚(修改类)
- **兼容性**:纯出参新增字段,前端按原字段渲染不受影响;新字段为增强,可渐进接入。
- **性能**:单团装配固定 9 次查询封顶(无 N+1),空团短路;金额内存聚合,性能可控。
- **回滚**:回退本次 merge commit 即可恢复原出参;新字段无持久化、无 DDL,回滚无数据迁移成本。
---
## 12. 注意事项
1. 新字段**实时装配**,不读快照表;`finalized=false` 时也照常返回。
2. `paidAmount` / `actualRefundedAmount` 是 order_main 镜像口径(用于与 outstanding 同源自洽),线上/线下拆分走权威源——两者求和理论上应等于 `netPaidAmount` 相关口径,差异会体现在 `paidMirrorMatched` 校验位。
3. 本 PR 是团期核单详情对齐的 **PR-1(财务总览 + 客户合并 + 人数口径)**;后续 **PR-2** 还会补多 tab 明细(step1 住宿/step2 门票/step3 车辆/导游/摄影/餐食/其他收入/其他支出/返还),届时另行推 changelog。
---
## 13. 关联 / 联系人
- Issue: https://git.1814.love/wx/HL/issues/8510
- PR: https://git.1814.love/wx/HL/pulls/8546
- Commit: https://git.1814.love/wx/HL/commit/306f9873d30a567331afed12b91c45af176418bf
- 负责人:腰苏图
@@ -13,7 +13,7 @@ frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "只新增一种失败返回,前端按现有方式展示 message 即可"
updated_at: "2026-09-29"
updated_at: "2026-09-30"
base: "dev-v3"
---
@@ -7,13 +7,13 @@ author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "PR #8534(fix(fleet): 派单看板列表下发用车需求类别并支持按类别筛,关联 #8518)已合并 dev-v3,滚动部署测试服 hl-fleet-service @ dfb5db832(2026-09-29 17:48:30)。requirementKind/requirementKindLabel 两字段与同名可选筛选参数已实测:基线 47 条,TRAVEL 37/TRANSFER 10,两档相加等于基线且交集为空,requirementId 集合与基线一致;非法值返 100001。"
updated_at: "2026-09-29"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "2f65d1f756f98453acf91cd32d1866eed3f01f5c"
target_release: "v2.1"
verified_at: "2026-09-30"
status_note: "PR #8534(fix(fleet): 派单看板列表下发用车需求类别并支持按类别筛,关联 #8518)已合并 dev-v3,滚动部署测试服 hl-fleet-service @ dfb5db832(2026-09-29 17:48:30)。requirementKind/requirementKindLabel 两字段与同名可选筛选参数已实测:基线 47 条,TRAVEL 37/TRANSFER 10,两档相加等于基线且交集为空,requirementId 集合与基线一致;非法值返 100001。前端 2026-09-30 已交付:看板列表加「类别」列直显 requirementKindLabel(NTag info/warning 读英文码,不自译),orderKind 页签旁加类别页签且列表+汇总两接口同传(空=不过滤),与 orderKind 可同传 AND;586 例全绿。"
updated_at: "2026-09-30"
base: "dev-v3"
---
@@ -7,13 +7,13 @@ author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "PR #8553 合并 dev-v3(7b702f5c3f);hl-fleet-service dev-v3 分支部署测试网关 @ e2982739e8(含 7b702f5c3f)并实测:排入 rest 司机返 605038、排入 DISABLED 车辆返 605037(均一行未落库);正常 ACTIVE 车辆 7 天全排返 addedCount=7;clearAll=true 场景返 ignoredDemandDays 回填生效。"
updated_at: "2026-09-29"
updated_at: "2026-09-30"
base: "dev-v3"
---
@@ -7,12 +7,12 @@ author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "38bcab548c5d71317d0f56c6d0051c7fe7a0d4fd"
target_release: ""
verified_at: ""
status_note: "PR #8563 合并 dev-v3(cb876bf780);hl-order-service-v3 + hl-fleet-service 已随该提交部署测试网关;order-v3 新增单测 6 条(RequirementServiceTest)+ fleet 新增单测 6 条(AssignmentServiceTest)+ mapper 层 2 条 + 跨服务常量 1 条,共 15 条,均为换组迁移/无配车迁移/同内容重放不迁移/非团期恒 0/订单终态不迁移等场景的定向用例。"
verified_at: "2026-09-30"
status_note: "PR #8563 合并 dev-v3(cb876bf780);hl-order-service-v3 + hl-fleet-service 已随该提交部署测试网关;order-v3 新增单测 6 条(RequirementServiceTest)+ fleet 新增单测 6 条(AssignmentServiceTest)+ mapper 层 2 条 + 跨服务常量 1 条,共 15 条,均为换组迁移/无配车迁移/同内容重放不迁移/非团期恒 0/订单终态不迁移等场景的定向用例。;前端已交付:FunItemAdjustModal 成功提示附换组平移基数(>0 显「旧版 N 行配车将平移至新需求」,0/统一提交路径原提示),spec 3 例,checkpoint 全绿(hl-admin 38bcab54)"
updated_at: "2026-09-29"
base: "dev-v3"
---
@@ -13,7 +13,7 @@ frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "A2 团期详情的 advanceAmount(已预支金额·累计)原先直读 order_group_batch.advance_amount。该列自 #7154(团期预支改为 order_advance 逐笔台账 + 实时聚合)起已无写入点:新团期永远 0;#7154 之前就有预支的老团期停在 V20260906_004 期初结转那一刻;且旧列只累加团期级、从不含子订单级。于是同一团期 A2 与财务 Tab(GB-ADM-040)advanceApproved 可以是两个数。本次 A2 改为与财务 Tab 调同一对方法(GroupBatchAdvanceQueryService.listActiveSubOrderIds + sumApproved),两位小数,字符串逐字一致。路径、入参、出参字段名与类型零变化;变的是 advanceAmount 的取值口径(旧列快照 → 团期级 ∪ 子订单级 APPROVED 实时聚合),属「语义取值」变化,按 jw 2026-09-18 口径推送。已合并 dev-v3(PR #8539,merge commit 27409a3f1)并部署测试服。hl-ui v2.1 团期详情页未渲染 advanceAmount(已预支卡在 FinanceTab 读 advanceApproved),前端零改动,frontend_status 记 not_required。PAID 不计入是 g-081 的既有口径(09-28 定暂不处理),本次只对齐来源、不改口径。"
updated_at: "2026-09-29"
updated_at: "2026-09-30"
base: "dev-v3"
---
@@ -7,13 +7,13 @@ author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "PR #8554 合并 dev-v3(e2982739e8);hl-fleet-service dev-v3 分支部署测试网关 @ e2982739e8 并实测:四个读口对不存在团期均返回 600015;有效团期零共用关系仍返回 200+data=[];就绪判定零配车行场景文案已改为「本团尚未创建任何配车行」。"
updated_at: "2026-09-29"
updated_at: "2026-09-30"
base: "dev-v3"
---
@@ -7,13 +7,13 @@ author: "wx(GIT)"
change_type: "前端优化"
backend_status: "not_required"
gateway_status: "not_required"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "后端零改动,17 个既有接口;三种提交模式的调用顺序、确认后接送机对账补放行、已确认态只可通过不可打回见正文四;4 项字段缺口本期不展示"
updated_at: "2026-09-29"
frontend_status: "implemented"
frontend_owner: "hl-admin"
frontend_ref: "80d7ad8872b2938e4849d71f32007ff07c6f465d"
target_release: "v2.1"
verified_at: "2026-09-30"
status_note: "后端零改动,17 个既有接口;三种提交模式的调用顺序、确认后接送机对账补放行、已确认态只可通过不可打回见正文四;4 项字段缺口本期不展示;前端已交付:页签只读总览+三步审核弹窗+对账补放行+审核变更只通过"
updated_at: "2026-09-30"
base: "dev-v3"
---
@@ -0,0 +1,614 @@
---
schema: "hl-changelog/v2"
ticket: "8491"
title: "房务旧列表接口下线:抢单池列表、我的接单、组长全部已抢订单、我的团"
consumer: "admin"
author: "wx(GIT)"
change_type: "删除接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: ""
updated_at: "2026-09-30"
base: "dev-v3"
---
# 房务旧列表: 下线 4 个已废弃的管理端列表接口(抢单池列表 / 我的接单 / 组长全部已抢订单 / 我的团)
> **存放目录**: 二期 → `changelogs-v2/2026-09/`
>
> **服务**: hl-order-service-v3
> **Issue**: #8491
> **日期**: 2026-09-29
> **影响范围**: 管理后台房务旧列表页(抢单池、我的接单、组长监督视图、我的团)的数据来源
---
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
- 以下 4 个 GET 接口从服务端删除,服务端已无这些路由映射,删除后返回形态本文不作约定,调用点一律移除:
- `GET /v3/admin/order/grab-pool/hotel-requirements`
- `GET /v3/admin/order/grab-pool/my-claims/hotel`
- `GET /v3/admin/order/grab-pool/all-claims/hotel`
- `GET /v3/admin/order/grab-pool/my-claims/group-batches`
- 替代接口:常规单统一走 `GET /v3/admin/order/house-allocation/households`,团期统一走 `GET /v3/admin/order/house-allocation/group-batches`(两者本次的字段调整见同目录修改接口文件)。
- 「房务组长」角色同步取消:原「全部已抢订单」监督视图与「我的团 scope=all」不再存在,全体房务改用替代接口的 `scope=all` 查看全部。
---
## 一、背景(选填)
这 4 个接口在 #8491 之前已标注「已废弃,改用 house-allocation 列表」,房务控制台上线两条统一列表后删除。它们的请求 / 响应 VO(`HouseMyOrderPageReqVO`、`HouseMyOrderPageRespVO`、`HouseMyOrderItemRespVO`、`HouseMyOrderStatsVO`、`HouseMyGroupPageReqVO`、`HouseMyGroupPageRespVO`、`HouseMyGroupItemRespVO`、`HouseMyGroupStatsVO`)随之删除。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 抢单池列表(常规单) | GET | `/v3/admin/order/grab-pool/hotel-requirements` | 删除 | 改用 households 列表 status=pendingClaim |
| 2 | 我的接单(常规单) | GET | `/v3/admin/order/grab-pool/my-claims/hotel` | 删除 | 改用 households 列表 scope=mine |
| 3 | 组长全部已抢订单(常规单) | GET | `/v3/admin/order/grab-pool/all-claims/hotel` | 删除 | 改用 households 列表 scope=all |
| 4 | 我的团(团期) | GET | `/v3/admin/order/grab-pool/my-claims/group-batches` | 删除 | 改用 group-batches 列表 status=claimed |
---
## 三、接口详情
本节 4 个接口均已删除。入参 / 出参表与示例记录的是**删除前**的契约,仅供前端定位与清理调用点;字段名、类型、校验文案逐一取自删除前源码,ID、日期、姓名等取值为说明用的构造值。服务端已无这些路由映射,删除后返回形态本文不作约定。
### 1. 抢单池列表(常规单) `GET /v3/admin/order/grab-pool/hotel-requirements`
**VO**: `HouseGrabPageReqVO → PageResult<HouseGrabPageItemRespVO>`(已删除接口,VO 类仍在代码中但无接口引用)
#### 使用场景
删除前:房务在抢单池页查看待认领的常规单需求。现在改为 `GET /v3/admin/order/house-allocation/households?status=pendingClaim`。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| keyword | Query | String | ❌ | ≤32 字 | 删除前:关键词 |
| productType | Query | String | ❌ | - | 删除前:产品类型 |
| productName | Query | String | ❌ | - | 删除前:产品名 |
| consultantId | Query | Long | ❌ | - | 删除前:定制师 |
| guestName | Query | String | ❌ | - | 删除前:客人姓名 |
| departDateFrom / departDateTo | Query | LocalDate | ❌ | - | 删除前:出行日期区间 |
| page | Query | Integer | ❌ | ≥1,默认 1 | 删除前:页码 |
| pageSize | Query | Integer | ❌ | 1~100,默认 20 | 删除前:每页条数 |
| sortBy | Query | String | ❌ | 默认 createTime,desc | 删除前:排序 |
#### 出参 `Result<PageResult<HouseGrabPageItemRespVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| records | List<HouseGrabPageItemRespVO> | 删除前:行列表 |
| total | int | 删除前:总数 |
| records[].id / orderId | Long(String) | 删除前:需求 ID / 订单 ID |
| records[].orderNo / teamNo / guestName / personsDesc | String | 删除前:订单号 / 团号 / 客人 / 人数描述 |
| records[].productType / productName / productNo / route | String | 删除前:产品信息 |
| records[].departDate / nights / cities | LocalDate / Integer / List<String> | 删除前:出行日期 / 夜数 / 城市 |
| records[].totalAmount | BigDecimal(String) | 删除前:订单总额 |
| records[].consultantName / consultantId / consultantRemark | String / Long(String) / String | 删除前:定制师信息 |
| records[].requirementNote / dispatchRemark / special | String / String / List<String> | 删除前:需求备注 / 派单备注 / 特殊要求 |
| records[].requirementVersion | Integer | 删除前:需求版本 |
| records[].urgencyLevel / urgencyLabel / daysToDepart / manualUrgent | String / String / Integer / Boolean | 删除前:紧急度 |
| records[].createTime | LocalDateTime | 删除前:入池时间 |
| records[].isRework / reworkPrevClaimerName | Boolean / String | 删除前:返工标识 / 上一任认领人 |
#### 请求示例
```http
GET /v3/admin/order/grab-pool/hotel-requirements?page=1&pageSize=20
```
#### 响应示例
删除前(仅供清理对照):
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"id": "1940000000000000011",
"orderId": "1930000000000000021",
"orderNo": "26-0915",
"teamNo": "26-0920",
"guestName": "李女士一家",
"productName": "呼伦贝尔秋色 6 日",
"departDate": "2026-10-05",
"isRework": false
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"success": true
}
```
#### 空数据 / 降级响应
接口已删除,无空数据或降级形态可约定;前端移除调用点,空列表展示改由替代接口 `records` 为空时处理。
#### 错误响应
删除前(仅供清理对照):
```json
{
"code": 400,
"message": "keyword 长度不能超过 32 字",
"data": null,
"success": false
}
```
| code | message | 触发 |
|------|---------|------|
| 400 | keyword 长度不能超过 32 字 / page 必须 ≥ 1 / pageSize 最大 100 | 删除前的入参校验 |
#### 业务边界
- 服务端已无该路由映射,删除后返回形态本文不作约定,调用点一律移除。
- 替代:`GET /v3/admin/order/house-allocation/households?status=pendingClaim`,排序可传 `sortBy=createTime,desc` 保持原默认顺序;替代接口的 `list` 字段名与本接口的 `records` 不同。
### 2. 我的接单(常规单) `GET /v3/admin/order/grab-pool/my-claims/hotel`
**VO**: `HouseMyOrderPageReqVO → HouseMyOrderPageRespVO`(均已删除)
#### 使用场景
删除前:房务查看本人已认领的常规单及状态统计。现在改为 `GET /v3/admin/order/house-allocation/households?scope=mine`。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| keyword | Query | String | ❌ | ≤32 字 | 删除前:关键词 |
| status | Query | String | ❌ | `unfinished` / `allUnfinished` / `todo` / `inProgress` / `claiming` / `pendingConfirm` / `confirmed` / `exception` / `voided` / `inInquiry` | 删除前:跟单状态(前三个都表示全部未完成) |
| productType | Query | String | ❌ | CORE / ROUTE / CUSTOM / GROUP | 删除前:产品类型 |
| productName / guestName | Query | String | ❌ | - | 删除前:模糊搜 |
| consultantId | Query | Long | ❌ | - | 删除前:定制师 |
| departDateFrom / departDateTo | Query | LocalDate | ❌ | - | 删除前:出行日期区间 |
| stayDate | Query | LocalDate | ❌ | - | 删除前:入住晚下钻 |
| claimedAtFrom / claimedAtTo | Query | LocalDateTime | ❌ | - | 删除前:认领时间区间 |
| city / hasException / hasTodo / hasUnreadMessage | Query | String / Boolean | ❌ | - | 删除前:预留字段,未实现 |
| page | Query | Integer | ❌ | ≥1,默认 1 | 删除前:页码 |
| pageSize | Query | Integer | ❌ | 1~100,默认 20 | 删除前:每页条数 |
| sortBy | Query | String | ❌ | 默认 claimedAt,desc | 删除前:排序 |
#### 出参 `Result<HouseMyOrderPageRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| list | List<HouseMyOrderItemRespVO> | 删除前:行列表 |
| total | Long | 删除前:总数 |
| stats | HouseMyOrderStatsVO | 删除前:inProgress / claiming / inInquiry(恒 0)/ pendingConfirm / confirmed / exception / voided |
| list[].id / orderId / consultantId / claimerId | Long(String) | 删除前:需求 / 订单 / 定制师 / 持有人 ID |
| list[].orderNo / teamNo / guestName / personsDesc / productType / productName / route | String | 删除前:订单与产品信息 |
| list[].departDate / nights / cities | LocalDate / Integer / List<String> | 删除前:出行信息 |
| list[].totalAmount | BigDecimal(String) | 删除前:订单总额 |
| list[].consultantName / claimerName / claimedAt | String / String / LocalDateTime | 删除前:定制师 / 持有人 / 认领时间 |
| list[].houseStatus | String | 删除前:**中文状态**(与 houseStatusLabel 同值) |
| list[].houseStatusLabel | String | 删除前:中文状态 |
| list[].progressDesc / hotelSummary / lastAction | String | 删除前:进度文字 / 已配酒店摘要 / 最近动作 |
| list[].exceptionCount / todoCount / unreadMessageCount | Integer | 删除前:异常数 / 待办数 / 未读留言数 |
| list[].primaryAction | HousePrimaryActionVO | 删除前:code / label / url |
| list[].voided / requirementVersion / voidReason / voidedAt | Boolean / Integer / String / LocalDateTime | 删除前:作废信息 |
#### 请求示例
```http
GET /v3/admin/order/grab-pool/my-claims/hotel?status=unfinished&page=1&pageSize=20
```
#### 响应示例
删除前(仅供清理对照):
```json
{
"code": 200,
"message": "成功",
"data": {
"list": [
{
"id": "1940000000000000011",
"orderId": "1930000000000000021",
"orderNo": "26-0915",
"guestName": "李女士一家",
"claimerName": "张敏",
"houseStatus": "配房中",
"houseStatusLabel": "配房中",
"progressDesc": "5晚已配3晚",
"unreadMessageCount": 2,
"voided": false
}
],
"total": 1,
"stats": {
"inProgress": 1,
"claiming": 1,
"inInquiry": 0,
"pendingConfirm": 0,
"confirmed": 0,
"exception": 0,
"voided": 0
}
},
"success": true
}
```
#### 空数据 / 降级响应
接口已删除,无空数据或降级形态可约定;前端移除调用点。
#### 错误响应
删除前(仅供清理对照):
```json
{
"code": 400,
"message": "pageSize 最大 100",
"data": null,
"success": false
}
```
| code | message | 触发 |
|------|---------|------|
| 400 | keyword 长度不能超过 32 字 / page 必须 ≥ 1 / pageSize 最大 100 | 删除前的入参校验 |
#### 业务边界
- 服务端已无该路由映射,删除后返回形态本文不作约定,调用点一律移除。
- 替代接口的 `houseStatus` 是枚举码(PENDING_CLAIM / CLAIMING / PENDING_FINALIZE / CONFIRMED / EXCEPTION),中文取 `houseStatusLabel`;按本接口 `houseStatus` 中文做判断的代码要改。
- 替代接口 `scope` 默认 `all`,查本人认领的单必须显式传 `scope=mine`。
- 本接口的 `voided` / `voidReason` / `voidedAt` / `progressDesc` / `hotelSummary` / `lastAction` 在替代列表中没有对应字段。
### 3. 组长全部已抢订单(常规单) `GET /v3/admin/order/grab-pool/all-claims/hotel`
**VO**: `HouseMyOrderPageReqVO → HouseMyOrderPageRespVO`(均已删除)
#### 使用场景
删除前:房务组长 / 超管的只读监督视图,查看全部已认领常规单。「房务组长」角色已取消,全体房务改用 `GET /v3/admin/order/house-allocation/households?scope=all`。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| keyword | Query | String | ❌ | ≤32 字 | 删除前:同接口 2 |
| status | Query | String | ❌ | 同接口 2 | 删除前:跟单状态 |
| page | Query | Integer | ❌ | ≥1,默认 1 | 删除前:页码 |
| pageSize | Query | Integer | ❌ | 1~100,默认 20 | 删除前:每页条数 |
| 其余筛选 | Query | - | ❌ | 同接口 2 | 删除前:与接口 2 共用同一请求 VO |
#### 出参 `Result<HouseMyOrderPageRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| list | List<HouseMyOrderItemRespVO> | 删除前:同接口 2,行内 claimerId / claimerName 标该单属哪个房务 |
| total | Long | 删除前:总数 |
| stats | HouseMyOrderStatsVO | 删除前:同接口 2 |
#### 请求示例
```http
GET /v3/admin/order/grab-pool/all-claims/hotel?page=1&pageSize=20
```
#### 响应示例
删除前(仅供清理对照):
```json
{
"code": 200,
"message": "成功",
"data": {
"list": [
{
"id": "1940000000000000012",
"orderId": "1930000000000000022",
"orderNo": "26-0916",
"claimerId": "30002",
"claimerName": "王芳",
"houseStatus": "待最终确认",
"houseStatusLabel": "待最终确认"
}
],
"total": 1,
"stats": {
"inProgress": 0,
"claiming": 0,
"inInquiry": 0,
"pendingConfirm": 1,
"confirmed": 0,
"exception": 0,
"voided": 0
}
},
"success": true
}
```
#### 空数据 / 降级响应
接口已删除,无空数据或降级形态可约定;前端移除调用点。
#### 错误响应
删除前(仅供清理对照):
```json
{
"code": 808092,
"message": "无权查看全部房务订单(仅房务组长或超管可查看)",
"data": null,
"success": false
}
```
| code | message | 触发 |
|------|---------|------|
| 808092 | 无权查看全部房务订单(仅房务组长或超管可查看) | 删除前:非组长 / 超管访问;该码已删除,不复用 |
| 400 | keyword 长度不能超过 32 字 / page 必须 ≥ 1 / pageSize 最大 100 | 删除前的入参校验 |
#### 业务边界
- 服务端已无该路由映射,删除后返回形态本文不作约定,调用点一律移除。
- 替代接口 `scope=all` 对全体房务开放;行内 `claimerName` 与 `readOnly` / `readOnlyReason` 标出该单由谁处理。
### 4. 我的团(团期) `GET /v3/admin/order/grab-pool/my-claims/group-batches`
**VO**: `HouseMyGroupPageReqVO → HouseMyGroupPageRespVO`(均已删除)
#### 使用场景
删除前:房务查看本人整团认领的团期(scope=mine),组长 / 超管可看全部已认领团(scope=all)。现在改为 `GET /v3/admin/order/house-allocation/group-batches?status=claimed`,配合 `scope=mine` 或 `scope=all`。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| scope | Query | String | ❌ | mine / all,默认 mine | 删除前:all 仅组长 / 超管 |
| keyword | Query | String | ❌ | ≤32 字 | 删除前:团期号 / 产品名 |
| batchStatus | Query | String | ❌ | 团期九态之一 | 删除前:团期状态 |
| needsReconfirm | Query | Boolean | ❌ | - | 删除前:只看需求待重新确认的团 |
| departDateFrom / departDateTo | Query | LocalDate | ❌ | - | 删除前:出发日区间 |
| claimedAtFrom / claimedAtTo | Query | LocalDateTime | ❌ | - | 删除前:认领时间区间 |
| page | Query | Integer | ❌ | ≥1,默认 1 | 删除前:页码 |
| pageSize | Query | Integer | ❌ | 1~100,默认 20 | 删除前:每页条数 |
| sortBy | Query | String | ❌ | 默认 claimedAt,desc,可切 departDate,asc | 删除前:排序 |
#### 出参 `Result<HouseMyGroupPageRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| total | Long | 删除前:总数 |
| stats | HouseMyGroupStatsVO | 删除前:total / needsReconfirm / cancelled(后两项在 total 超过 500 时为 null) |
| list | List<HouseMyGroupItemRespVO> | 删除前:行列表 |
| list[].groupBatchId / productId | Long(String) | 删除前:团期 / 产品 ID |
| list[].batchNo / productName / batchName / batchLabel / batchStatus / batchStatusLabel | String | 删除前:团期信息 |
| list[].departDate / endDate / enrollDeadline | LocalDate | 删除前:日期 |
| list[].enrolledRooms / enrolledPeople / activeOrderCount / hotelOrderCount / daysToDepart | Integer | 删除前:计数 |
| list[].hotelReady | Boolean | 删除前:酒店是否就绪 |
| list[].urgencyLevel / urgencyLabel | String | 删除前:紧急度 |
| list[].createTime | LocalDateTime | 删除前:创建时间 |
| list[].houseClaimerId / houseClaimerName / houseClaimedAt | Long(String) / String / LocalDateTime | 删除前:整团认领人与时间 |
| list[].requirementConfirmed | Boolean | 删除前:需求整体确认标记 |
#### 请求示例
```http
GET /v3/admin/order/grab-pool/my-claims/group-batches?scope=mine&page=1&pageSize=20
```
#### 响应示例
删除前(仅供清理对照):
```json
{
"code": 200,
"message": "成功",
"data": {
"total": 1,
"stats": {
"total": 1,
"needsReconfirm": 0,
"cancelled": 0
},
"list": [
{
"groupBatchId": "1950000000000000031",
"batchNo": "GB261005",
"batchName": "呼伦贝尔秋色 6 日",
"batchStatus": "PENDING_DEPARTURE",
"departDate": "2026-10-05",
"houseClaimerId": "30001",
"houseClaimerName": "张敏",
"houseClaimedAt": "2026-09-20 10:00:00",
"requirementConfirmed": true
}
]
},
"success": true
}
```
#### 空数据 / 降级响应
接口已删除,无空数据或降级形态可约定;前端移除调用点。
#### 错误响应
删除前(仅供清理对照):
```json
{
"code": 808092,
"message": "无权查看全部房务订单(仅房务组长或超管可查看)",
"data": null,
"success": false
}
```
| code | message | 触发 |
|------|---------|------|
| 808092 | 无权查看全部房务订单(仅房务组长或超管可查看) | 删除前:普通房务传 scope=all;该码已删除,不复用 |
| 400 | keyword 长度不能超过 32 字 / page 必须大于等于 1 / pageSize 最大 100 | 删除前的入参校验 |
#### 业务边界
- 服务端已无该路由映射,删除后返回形态本文不作约定,调用点一律移除。
- 替代接口没有 `needsReconfirm` 筛选;行字段 `requirementConfirmed` 仍在,可在前端按它标记。
- 替代接口的统计为 `pendingClaim` / `claimed` / `all`,没有 needsReconfirm / cancelled 计数。
- 替代接口 `scope` 默认 `all`,查本人整团认领的团必须显式传 `scope=mine`。
---
## 四、契约约束与正确调用方式(接口类必写)
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload |
|------|---------|
| ❌ 抢单池列表 | `GET /v3/admin/order/grab-pool/hotel-requirements` → 路由已删除 |
| ✅ 抢单池列表 | `GET /v3/admin/order/house-allocation/households?status=pendingClaim&sortBy=createTime,desc` |
| ❌ 我的接单 | `GET /v3/admin/order/grab-pool/my-claims/hotel` → 路由已删除 |
| ✅ 我的接单 | `GET /v3/admin/order/house-allocation/households?scope=mine&status=unfinished` |
| ❌ 全部已抢订单 | `GET /v3/admin/order/grab-pool/all-claims/hotel` → 路由已删除 |
| ✅ 全部已抢订单 | `GET /v3/admin/order/house-allocation/households?scope=all&status=unfinished` |
| ❌ 我的团 | `GET /v3/admin/order/grab-pool/my-claims/group-batches` → 路由已删除 |
| ✅ 我的团 | `GET /v3/admin/order/house-allocation/group-batches?scope=mine&status=claimed` |
### 切换状态时的必要动作
- 替代接口的状态筛选取值与旧接口不同:常规单 `status` 为 pendingClaim / unfinished(默认)/ claiming / pendingConfirm / confirmed / exception,空串=全部;团期 `status` 为 pendingClaim / claimed,不传=两者都要。旧值(`allUnfinished` / `todo` / `inProgress` / `voided` / `inInquiry`)传给替代接口会被入参校验拒绝(400)。
- 两个替代接口的 `scope` 默认都是 `all`;原「我的接单」「我的团」对应的调用必须显式带 `scope=mine`。
- 常规单替代接口 `sortBy` 只接受 departDate,asc(默认)/ createTime,desc / claimedAt,desc;团期替代接口只接受 departDate,asc(默认)/ claimedAt,desc / createTime,desc。
---
## 五、数据库行为(涉及写操作时必写)
| 前端提交 | 写入位置 | 行为 |
|----------|----------|------|
| 4 个已删除接口均为只读 GET | 无 | 不涉及写入 |
**显式 SET NULL 说明**: 不涉及。
---
## 六、边界行为
- 4 条路由已从服务端删除,删除后返回形态本文不作约定,前端不得依赖任何返回来判断,调用点一律移除。
- 替代接口的读权限:全体房务(ROOM_MANAGER / SUPER_ADMIN)可用 `scope=all`;非房务角色返回 808090「未登录或非房务角色,无权操作」。
- 808091、808092 随组长角色一并删除,不复用。
---
## 六.5、枚举 / 数据字典(接口出现枚举时必写)
### 替代接口 houseStatus(HouseStateEnum)
**所属字段**: `HouseAllocationHouseholdRespVO.houseStatus`(替代旧接口 `HouseMyOrderItemRespVO.houseStatus` 的中文值) / **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `PENDING_CLAIM` | 待配房 | 尚未认领 |
| `CLAIMING` | 配房中 | 已认领、配房进行中 |
| `PENDING_FINALIZE` | 待最终确认 | 配房待最终确认 |
| `CONFIRMED` | 已完成 | 配房完成 |
| `EXCEPTION` | 异常 | 异常 |
---
## 六.6、修改前后对比(修改/删除接口必写)
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| 我的接单 `list[].houseStatus` | 中文状态 | 替代接口为枚举码,中文取 `houseStatusLabel` |
| 我的接单 `list[].unreadMessageCount` | 未读留言数 | 替代接口 `unreadCount`(房务会话未读数) |
| 我的接单 `stats` | inProgress / claiming / inInquiry / pendingConfirm / confirmed / exception / voided | 替代接口 pendingClaim / claiming / pendingConfirm / confirmed / exception / unfinished / all |
| 我的接单 `progressDesc` / `hotelSummary` / `lastAction` / `voided` / `voidReason` / `voidedAt` | 有 | 替代列表无对应字段 |
| 抢单池列表 `records` | 行列表字段名 | 替代接口为 `list` |
| 我的团 `stats` | total / needsReconfirm / cancelled | 替代接口 pendingClaim / claimed / all |
| 我的团入参 `needsReconfirm` | 有 | 替代接口无;行字段 `requirementConfirmed` 保留 |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 调用 4 个旧列表接口 | 返回列表 | 路由已删除,返回形态本文不作约定 |
| 普通房务查看全部已认领常规单 | 808091 | 替代接口 scope=all 放行 |
| 普通房务查看全部已认领团期 | 808092 | 替代接口 scope=all 放行 |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 是。4 条路由删除,仍调用的页面拿不到数据。
- **前端是否必须同步上线**: 是。服务端已无这 4 条路由,仍在调用的页面需改为替代接口。
- **前端 workaround 清理点**: 旧抢单池页、我的接单页、组长监督视图、我的团页对这 4 个路径的调用;按 `houseStatus` 中文值、`unreadMessageCount`、`inInquiry` / `voided` 统计、808091 / 808092 做的分支。
---
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
- **仅影响**: 管理后台调用上述 4 个路径的页面。
- **零影响**:
- 团期抢单池列表 `GET /v3/admin/order/grab-pool/group-batches` 保留(仍为废弃标注)
- 同一控制器的转单 `POST /v3/admin/order/hotel-requirements/{requirementId}/transfer` 保留(本次行为变化见同目录修改接口文件)
- 小程序与 H5
---
## 八、测试环境已验证
四个旧端点都用房务 A 身份经测试服网关调用,共三轮(00:30、00:31、01:18)。HTTP 状态恒为 200,下面的 `code` 指响应 body 里的 `code`,带 ✓ 标记。行尾 `@` 后面是当时测试服 order-v3 的部署提交,两个提交都包含本单合并提交 `7c21cf0e40`。
```
GET /v3/admin/order/grab-pool/hotel-requirements 抢单池列表(常规单) → code=404 ✓ @bdde64a3a,复测 ✓ @9c7ac9382
GET /v3/admin/order/grab-pool/my-claims/hotel 我的接单(常规单) → code=404 ✓ @bdde64a3a,复测 ✓ @9c7ac9382
GET /v3/admin/order/grab-pool/all-claims/hotel 组长全部已抢订单(常规单) → code=404 ✓ @bdde64a3a,复测 ✓ @9c7ac9382
GET /v3/admin/order/grab-pool/my-claims/group-batches 我的团(团期) → code=404 ✓ @bdde64a3a,复测 ✓ @9c7ac9382
```
验证身份:房务 A,测试专用账号。
---
## 十、相关文档
- 关联 Issue: [wx/HL#8491](https://git.1814.love:8443/wx/HL/issues/8491)
- 契约文档: `docs/order-v3/api/API-SPEC-HOUSE-V1.1.html` §12 房务控制台
- 同批变更: 同目录 `30_8491_房务控制台接口-新增接口-管理后台.md`、`30_8491_房务配房接口字段与口径调整-修改接口-管理后台.md`
## 关联 / 联系人
### 链接
- **Issue**: [#8491](https://git.1814.love:8443/wx/HL/issues/8491)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,245 @@
---
schema: "hl-changelog/v2"
ticket: "8493"
title: "下线房务组长会话入口 POST /admin/message/chat/open-house-lead"
consumer: "admin"
author: "wx(GIT)"
change_type: "删除接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin"
frontend_ref: "8c9f7bc5174f379f630b4b7530ffd82a4bc1a80c"
target_release: "v2.1"
verified_at: "2026-09-30"
status_note: "测试服 hl-user-service 已部署 eee6d17ef4(PR #8569)。网关实测:open-house-lead 返回 code=404,open-house 与 conversations 均返回 code=200。历史 HOUSE_LEAD 会话数据只读保留,会话列表、消息分页、标记已读三个既有接口按通用逻辑处理,不对 HOUSE_LEAD 做特殊拦截。前端已交付:chat.js 删 openHouseLead 封装;ChatDrawer HOUSE_LEAD 分支改 conversationKey 直连 messages/read/发消息,对端名片由 conversations 行 peerName/peerRoleLabel 补;housekeeper/orders 移除「联系房务」入口,历史会话从「我的消息」行直读。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# hl-user-service:下线房务组长会话入口 `POST /admin/message/chat/open-house-lead`
> **服务**: hl-user-service (端口 8081)
> **PR**: #8569(dev-v3 `eee6d17ef4`)
> **Issue**: #8493
> **日期**: 2026-09-30
> **影响范围**: 管理后台「房务组长联系房务」会话入口
---
## ⚠️ 关键变化
🔴 **`POST /admin/message/chat/open-house-lead` 已整体下线**,不再接受任何调用,经网关返回业务码 `404`。这不是临时故障,是本单的预期结果。
🔴 **历史 HOUSE_LEAD 会话不受影响,仍可正常读写。** 只是下线了"新开一条组长会话"的入口;已存在的 `HOUSE_LEAD:{orderId}` 会话在会话列表(`GET conversations`)、消息分页(`GET {conversationKey}/messages`)、标记已读(`POST {conversationKey}/read`)、发消息(`POST {conversationKey}/messages`)四个既有接口上都按通用逻辑处理,后端不做任何额外拦截。前端对这类历史行不要再调 `open-house-lead`(也不要改调其它 `open-*`,那会新建一条会话、找不回历史那条),直接用行上的 `conversationKey` 调上述四个既有接口即可。
---
## 一、背景
`open-house-lead` 原本的用法:房务组长在 `all-claims` 列表里选中一个订单,把该单房务(claimer)的 `adminId` 作为 `peerAdminId` 传入,开一个键为 `HOUSE_LEAD:{orderId}` 的组长↔房务独立会话。#8491 取消了"房务组长"角色、同时删除了 `all-claims` 读口,这个入口从此既没有使用者也没有数据来源,本单据此下线 Controller 方法、Manager 方法、专用请求 VO(`ChatOpenHouseLeadReqVO`)与专用常量(`ChatConstants.BIZ_MODULE_HOUSE_LEAD`、`ROLE_HOUSE_LEAD`、`ConversationKeyUtil.buildHouseOrderLead`)。
`ChatRoleEnum.HOUSE_LEAD`(值「房务组长」)本单**没有**删除:测试服 `peer_role = 'HOUSE_LEAD'` 的历史成员行不为 0,删掉枚举值会让这些历史行的角色徽章从「房务组长」退化成裸码 `HOUSE_LEAD`,故保留该枚举值只用于历史数据回显。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 打开/找回房务组长会话 | POST | `/admin/message/chat/open-house-lead` | 删除 | 接口整体下线;下线前用于开一条组长↔房务订单维度会话 |
---
## 三、接口详情
### 1. 打开/找回房务组长会话(已删除) `POST /admin/message/chat/open-house-lead`
**VO**: `ChatOpenHouseLeadReqVO → Result<ChatOpenFullRespVO>`(请求 VO 已随本单删除;响应类型下线前复用现仍在用的 `ChatOpenFullRespVO`——该类当前的类头注释已注明"组长会话入口 open-house-lead 已由 #8493 下线")
#### 使用场景
**已下线,不再有使用场景。** 下线前用于「房务组长在 all-claims 列表选中一个订单、为该单房务(claimer)开一条独立组长会话」。#8491 取消组长角色并删除 all-claims 读口后,这个场景已不存在。
#### 入参
**已下线,不再接受任何入参。** 以下是下线前的参数语义(工单 #8493「现状事实」节口径;字段命名与序列化方式对照同结构的 `open-house` 请求 VO——两者均为"雪花 id、JSON 按 String 透传",仅供核对旧调用代码,不代表下线前 VO 的逐字段校验注解):
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Body | String | 是 | 雪花id,JSON 按 String 透传 | **已下线**。会话维度键 `HOUSE_LEAD:{orderId}` |
| peerAdminId | Body | String | 是 | 雪花id,JSON 按 String 透传 | **已下线**。该订单房务(claimer)员工id,原由前端从 all-claims 列表传入 |
#### 出参
**已下线,不再有响应体。** 下线前复用现仍在用的 `ChatOpenFullRespVO`(继承 `ChatOpenRespVO` 基类字段):
| 字段 | 类型 | 说明 |
|------|------|------|
| conversationKey | String | **已下线**。规范化会话键,形如 `HOUSE_LEAD:{orderId}` |
| peerAdminId | Long | **已下线**。对方(房务)员工id |
| peerName | String | **已下线**。对方姓名快照 |
| peerRole | String | **已下线**。对方角色,值为 `HOUSE_LEAD` |
| peerRoleLabel | String | **已下线**。对方角色中文 label,`HOUSE_LEAD` 对应「房务组长」 |
| order | Object | **已下线**。订单卡 |
| thread | Object | **已下线**。首屏消息(最新一页 20 条) |
| unreadTotal | Integer | **已下线**。标记已读后的合并未读总数 |
#### 请求示例
已下线,以下是下线前的请求形态,用来识别调用点:
```json
{
"orderId": "70123",
"peerAdminId": "205"
}
```
#### 响应示例
**接口已下线,现在的实际响应(测试服经网关实测;HTTP 状态码 200,业务码 `code=404`):**
```json
{
"code": 404,
"message": "接口不存在: POST /admin/message/chat/open-house-lead",
"data": null,
"traceId": null,
"success": false
}
```
#### 空数据 / 降级响应
**不适用。** 接口已不存在,不论带什么参数、用哪个账号,响应都与上面相同,没有空数据或降级分支。
#### 错误响应
下线前这个接口没有专属错误码(越权、参数缺失都走通用的 `281005`/`281010` 等,与 `open-house` 共用一套)。现在唯一的响应就是路由未命中:
```json
{
"code": 404,
"message": "接口不存在: POST /admin/message/chat/open-house-lead",
"data": null,
"traceId": null,
"success": false
}
```
#### 业务边界
- 下线是纯删除:只删了这一个接口,以及只为它存在的请求 VO、Manager 方法、常量、单测;`ChatOpenFullRespVO`、`ChatOpenRespVO` 等公共响应类型未动。
- 同控制器下的 `open`、`open-house`、`open-fleet`、`open-group`、`open-group-house`、`open-group-fleet` 六个接口本单没有改动,仍可正常调用。
- 历史 `HOUSE_LEAD:{orderId}` 会话不受影响:数据不删、不迁移,会话列表、消息分页、标记已读、发消息四个既有接口对它们照常放行(详见四、六)。
- 不要用别的 `open-*` 接口去"找回"历史 HOUSE_LEAD 会话——键前缀不同(`HOUSE_LEAD:` vs `HOUSE:`),会新建一条会话而不是复用旧会话。
---
## 四、契约约束与正确调用方式
- 不要调用 `POST /admin/message/chat/open-house-lead`,也不要重试或做降级兜底:它现在固定返回上面那个 404 响应。
- 历史 HOUSE_LEAD 会话的正确访问方式:直接用会话列表行上的 `conversationKey`(形如 `HOUSE_LEAD:70123`)去调既有的 `GET /admin/message/chat/{conversationKey}/messages`(分页)、`POST /admin/message/chat/{conversationKey}/read`(已读)、`POST /admin/message/chat/{conversationKey}/messages`(发消息)——这三个端点路径、参数、响应结构本单均未改动。
- 这三个端点对 HOUSE_LEAD 键走的是**普通会话成员校验**(`ChatMessageService.requireActiveMember`),不是团队会话的 `TeamChatAuthorizationService` 授权分支——因为 `TeamChatModule` 只有 `FLEET`/`GROUP`/`GROUP_HOUSE`/`GROUP_FLEET` 四个团队模块,`HOUSE_LEAD` 不在其中;`assertConversationAccess` 对非团队键统一返回 `null`,放行给调用方原有的成员校验,不会因为组长角色已取消就把这些历史成员判成越权。
- `GET /admin/message/chat/conversations` 按 `bizModule=HOUSE_LEAD` 过滤仍然可用(服务端不校验 `bizModule` 取值是否在文档列出的枚举里,透传给 Mapper),可用于单独拉出历史组长会话列表。
- 会话列表返回的 `peerRoleLabel` 字段对 HOUSE_LEAD 行固定是「房务组长」(`ChatRoleEnum.resolveLabel("HOUSE_LEAD")`),前端可直接展示,不需要自己再映射。
- 会话列表返回的 `orderNo` 字段对 HOUSE_LEAD 行恒为 `null`(不做 order-v3 订单摘要 Feign 富化,与 HOUSE/FLEET 订单维度会话不同),不要用它判断订单归属或做展示兜底。
---
## 五、数据库行为
本单零数据库变更:没有新增 Flyway 脚本,`ai_admin_conversation`(会话)、`ai_admin_conversation_member`(成员)、`ai_admin_message`(消息)三张表结构不动。历史 `HOUSE_LEAD:*` 的会话、成员行、消息行只读保留,不删除、不迁移。接口下线只是不再产生**新的** HOUSE_LEAD 会话,不影响任何已有数据。
---
## 六、边界行为
| 场景 | 行为 |
|---|---|
| 调 `POST .../open-house-lead`,不带参数或带任意参数 | 路由未命中,返回上面的 404 响应 |
| 历史 HOUSE_LEAD 会话调 `GET conversations`(不加 bizModule 过滤) | 正常返回,与其它会话混排,`peerRoleLabel`=「房务组长」 |
| 历史 HOUSE_LEAD 会话调 `GET conversations?bizModule=HOUSE_LEAD` | 正常返回,只含 HOUSE_LEAD 会话 |
| 历史 HOUSE_LEAD 会话调 `GET {conversationKey}/messages` | 正常返回,走普通成员校验,不走团队授权 |
| 历史 HOUSE_LEAD 会话调 `POST {conversationKey}/read` | 正常返回,标记已读到最新 |
| 历史 HOUSE_LEAD 会话调 `POST {conversationKey}/messages`(发消息) | 正常发送,后端不拦截 |
| 非该会话成员访问 HOUSE_LEAD 会话 | 返回 `281002`(无权访问该会话),与其它会话一致 |
## 六.5、枚举 / 数据字典
### peerRole / peerRoleLabel(`ChatRoleEnum`)
**所属字段**: `ChatConversationRespVO.peerRole` / `peerRoleLabel`(会话列表出参,`GET /admin/message/chat/conversations`) | **类型**: `String`
本单只涉及 `HOUSE_LEAD` 这一个值——它在下线前是 `open-house-lead` 专属对端角色,下线后仅作为历史数据的回显值继续存在:
| 值 | 中文 | 说明 |
|----|------|------|
| `HOUSE_LEAD` | 房务组长 | 历史会话专属,本单起不会再新产生带这个值的会话;`ChatRoleEnum.resolveLabel` 未知码回退原码,故枚举值一旦被删,历史行会退化成显示裸码 `HOUSE_LEAD` 而非中文——本单保留了该枚举值,不会发生这种退化 |
## 六.6、修改前后对比
| 维度 | 改前 | 改后 |
|---|---|---|
| `POST /admin/message/chat/open-house-lead` | 路由存在,可新开组长↔房务会话 | 路由不存在,返回 404 |
| `ChatOpenHouseLeadReqVO` | 存在 | 已删除 |
| `ChatConstants.BIZ_MODULE_HOUSE_LEAD` / `ROLE_HOUSE_LEAD` | 存在 | 已删除 |
| `ConversationKeyUtil.buildHouseOrderLead` | 存在 | 已删除 |
| `ChatRoleEnum.HOUSE_LEAD` 枚举值 | 存在,用于新建会话的对端角色 | 保留,仅用于历史会话回显 |
| 历史 `HOUSE_LEAD:*` 会话的 conversations/messages/read/发消息 | 可用 | 不变,仍可用 |
| 网关路由配置 | — | 未改动(`/admin/message/chat/**` 整段转发) |
## 六.7、影响评估
- **是否破坏向后兼容**:对历史数据不破坏(只读保留、既有接口照常可用);对"新开组长会话"这一个操作是破坏性下线,因为它的前置角色和数据来源(#8491)已经不存在。
- **前端是否必须同步上线**:是——继续调用已下线的 `open-house-lead` 会拿到 404。需要移除的前端调用点见「关联 / 联系人」下方备注。
- **前端 workaround 清理点**:历史 HOUSE_LEAD 会话若在前端有专属的"打开会话"分支(调 `open-house-lead`),需要改为直接用行上的 `conversationKey` 调 `messages`/`read`;组长发起新会话的入口(原「联系房务」按钮)直接移除,不需要替换成别的接口。
---
## 七、不影响范围
- **仅影响**:`POST /admin/message/chat/open-house-lead` 这一个接口的可用性。
- **零影响**:
- 同控制器下 `open`、`open-house`、`open-fleet`、`open-group`、`open-group-house`、`open-group-fleet`、`conversations`、`{conversationKey}/messages`(GET/POST)、`{conversationKey}/read`、`unread-total` 十个既有接口,均未改动。
- 历史 HOUSE_LEAD 会话、成员、消息数据本身:不删除、不迁移。
- 网关路由:`/admin/message/chat/**` 整段转发未改动,无需新增或删除路由配置。
- 通知收件方配置:`HOUSE_LEAD` 收件方已由 `V20260922_211` 单独清理,与本单无关。
---
## 八、测试环境已验证
- **部署**:hl-user-service 已部署合并提交 `eee6d17ef4`(PR #8569)。
- **改后实测(经网关)**:
- `POST /admin/message/chat/open-house-lead` → HTTP 200,业务码 `code=404`。
- `POST /admin/message/chat/open-house` → HTTP 200,业务码 `code=200`(同域保留接口,未受影响)。
- `GET /admin/message/chat/conversations` → HTTP 200,业务码 `code=200`。
---
## 十、相关文档
- Issue:https://git.1814.love/wx/HL/issues/8493
- PR:https://git.1814.love/wx/HL/pulls/8569
- 前置工单:#8491(取消房务组长角色、删除抢单池读口 `all-claims`)
## 关联 / 联系人
### 链接
- **Issue**: [#8493](https://git.1814.love/wx/HL/issues/8493)
- **PR**: [#8569](https://git.1814.love/wx/HL/pulls/8569)
- **Merge commit**: [eee6d17ef4](https://git.1814.love/wx/HL/commit/eee6d17ef4)
### 联系人
- **后端负责人**: @wx
### 前端需要移除的调用点(hl-ui,`origin/v2.1` 已核实)
- `src/api/chat.js`:`openHouseLead` 接口封装。
- `src/components/chat/ChatDrawer.vue`:HOUSE_LEAD 分支里调 `open-house-lead` 打开会话的逻辑——改为对 HOUSE_LEAD 行直接用 `conversationKey` 走 `messages`/`read`。
- `src/views/notification/MyMessages/index.vue`:消息中心按 `row.bizModule` 打开 `ChatDrawer` 时,HOUSE_LEAD 行会走到上面这条分支,需同步调整。
- `src/views/housekeeper/orders/HouseholdTable.vue`、`src/views/housekeeper/orders/index.vue`:组长「联系房务」入口按钮,直接移除。
@@ -0,0 +1,303 @@
---
schema: "hl-changelog/v2"
ticket: "8507"
title: "财务初始化页面 tab 结构纠偏:应为 3 tab(应收初始化/应付初始化/现金银行初始化),删「员工往来」tab,应收初始化内按往来对象选 CUSTOMER/SUPPLIER_RECV(#8507 #8511)"
consumer: "admin"
author: "yst(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "hl-admin"
frontend_ref: "61f92cd88eb53dbf017a776038cdceb1eb6774f8"
target_release: "v2.1"
verified_at: "2026-09-30"
status_note: "财务初始化(菜单:参数设置/财务初始化)前端页面 tab 结构与原型不符的纠偏单。原型只有 3 个 tab(应收初始化/应付初始化/现金银行初始化),按「初始化场景」划分,初始化里**没有「员工往来」期初**(员工借款/备用金属付款管理业务流程,不在初始化)。当前前端做成了 4 个 tab(客户应收/供应商应付/供应商应收/员工往来),按「往来对象类型」划分,需重构。后端接口零改动、已全部部署测试服并验证通过:应收/应付走 /admin/finance/opening-balances(按 ledgerType 区分 CUSTOMER/SUPPLIER_RECV/SUPPLIER),现金银行走 /admin/finance/fund-accounts(账户期初结存),两套接口互相独立。本文自包含全部入参/出参/枚举/错误码/示例。前端已交付:3 tab 按场景重构——应收初始化(CUSTOMER 手录+SUPPLIER_RECV 供应商下拉并入「往来对象」开关,客户分类/应收性质字典必填+所属公司必填,列表「全部」两账套各查一次合并)/应付初始化(固定 SUPPLIER)/现金银行初始化(fund-accounts,复用 AccountFormDrawer+OpeningAdjustModal);删「员工往来」tab 与 STAFF 账套;期初调整改一方向单框+佐证选填;OpeningAdjustModal 加 595105 差额为 0 幂等分流。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# finance:财务初始化三 tab 对齐原型(管理后台)
> **性质**:前端实现纠偏。后端接口无新增/无变更,已在测试服就绪。本文告诉前端「正确的 tab 结构 + 每个 tab 怎么调既有接口」。
## 1. 接口背景
财务初始化是账套启用前录入期初数据的入口(菜单:**参数设置 / 财务初始化**)。
原型 `finance-prototype.html`(页面 id `cfg-fininit`)规定财务初始化**只有 3 个 tab**,按「初始化场景」划分:
```
财务初始化
├─ 应收初始化 启用前欠我们的款(客户欠款 + 供应商杂项应收)
├─ 应付初始化 启用前我们欠供应商的款
└─ 现金银行初始化 各资金账户启用前已有结存
```
**当前前端实现错误**:做成了 4 个 tab(客户应收 / 供应商应付 / 供应商应收 / 员工往来),按「往来对象类型」划分。两处偏差:
1. tab 划分维度错了——应按「初始化场景」(应收/应付/现金银行),不是按「往来对象类型」(客户/供应商/员工)。
2. 多出了「员工往来」tab——原型财务初始化**没有员工往来期初**。员工借款/备用金是「付款管理 / 员工借款」的业务单据流(页面 `loan-stf` / `payex-stfloan`),不属于财务初始化。
**「供应商应收」不是独立 tab**:它是「应收初始化」tab 内部、往来对象选「供应商」时的一种(供应商欠我们的杂项应收:押金退还/赔偿款/口车费/其他应收)。
## 2. 变更清单(前端 tab 结构改动)
| # | 改动 | 说明 |
|---|------|------|
| 1 | 删除「员工往来」tab | 初始化无此场景 |
| 2 | tab 改 3 个并改名 | `应收初始化` / `应付初始化` / `现金银行初始化` |
| 3 | 「客户应收」+「供应商应收」合并进「应收初始化」一个 tab | tab 内用「往来对象」下拉(客户/供应商)切换 ledgerType |
| 4 | 「供应商应付」改名「应付初始化」 | 固定 ledgerType=SUPPLIER,去掉对象细分字段 |
| 5 | 新增「现金银行初始化」tab | 接 `/admin/finance/fund-accounts` 系列接口(账户期初结存) |
## 3. tab ↔ 接口 / 账套映射(核心)
| 前端 tab | tab 内「往来对象」 | 调接口 | 传 `ledgerType` |
|---|---|---|---|
| 应收初始化 | 客户 | `POST /admin/finance/opening-balances` 等 | `CUSTOMER` |
| 应收初始化 | 供应商 | 同上 | `SUPPLIER_RECV` |
| 应付初始化 | (固定供应商,无此下拉) | 同上 | `SUPPLIER` |
| 现金银行初始化 | — | `/admin/finance/fund-accounts` 系列 | —(无 ledgerType 概念) |
> 应收初始化一个 tab 对应两个 ledgerType,按用户选的「往来对象」决定传哪个;金额字段填 `openingReceivable`。应付初始化固定 `SUPPLIER`,金额填 `openingPayable`。
---
## 4. 应收初始化 tab(ledgerType = CUSTOMER / SUPPLIER_RECV)
### 4.1 列表(分页)
`GET /admin/finance/opening-balances/page`
**入参(query)**:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| pageNo | int | 是 | 页码 |
| pageSize | int | 是 | 每页 |
| ledgerType | string | 是 | `CUSTOMER` 或 `SUPPLIER_RECV` |
| kind | string | 否 | `INIT` 初始 / `ADJUST` 期初调整;空=全部 |
| refName | string | 否 | 往来对象名模糊搜索 |
> 应收初始化 tab 顶部建议加「往来对象」筛选(全部/客户/供应商):客户→`CUSTOMER`、供应商→`SUPPLIER_RECV`、全部→两个 ledgerType 各查一次合并。
**出参(`data.records[]`)**:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 期初行 ID |
| ledgerType | string | 账套回显 |
| refId | string | 往来对象 ID |
| refName | string | 往来对象名称 |
| customerCategory | string | 对象细分编码(CUSTOMER=客户分类 / SUPPLIER_RECV=应收性质;SUPPLIER 为 null) |
| customerCategoryName | string | **对象细分中文名**(列表直接显示这个;字典不可用为 null) |
| companyId | string | 所属公司主体 ID |
| companyName | string | 所属公司主体名 |
| openingDate | string | 期初基准日(=当前未封账账期起始日),只读 |
| kind | string | `INIT` / `ADJUST` |
| openingPayable | number | 期初应付(应收 tab 恒 null,忽略) |
| openingReceivable | number | **期初应收(本 tab 显示这个金额)** |
| evidenceUrl | string | 佐证材料影像 URL(可空) |
| recordedByName | string | 录入人姓名 |
| createTime | string | 创建时间 |
### 4.2 新建期初
`POST /admin/finance/opening-balances`
**表单(按原型应收表单三段递进)**:
1. **往来对象**(下拉必填):`客户` / `供应商`
2. **对象细分**(下拉必填,标签随往来对象变):
- 客户 → 标签「客户分类」,选项 = 字典 `fin_customer_category`
- 供应商 → 标签「应收性质」,选项 = 字典 `fin_recv_nature`
3. **往来对象名称**(下拉必填):
- 供应商 → `GET /admin/supplier/items/list?status=ACTIVE`(取 `supplierId` + `shortName`/`fullName`)
- 客户 → 客户列表(按所选客户分类过滤)
4. **应收欠款**(数字必填,>0)
5. **所属公司**(下拉单选必填):`GET /v3/admin/travel-agency/enabled`(取 `agencyId`+`agencyName`)
6. **记账日期**(只读):前端不传,后端落 `openingDate`=当前账期起始日
7. **备注**(文本域选填,≤200 字)
**请求体**:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| ledgerType | string | 是 | `CUSTOMER`(选了客户)/ `SUPPLIER_RECV`(选了供应商) |
| refId | Long | 是 | 往来对象 ID |
| refName | string | 是 | 往来对象名称(≤128) |
| customerCategory | string | 条件必填 | 对象细分编码:CUSTOMER→fin_customer_category 编码;SUPPLIER_RECV→fin_recv_nature 编码。**两账套均必填** |
| companyId | Long | 是 | 所属公司主体 ID |
| openingReceivable | number | 是 | 期初应收金额,>0 |
| openingPayable | — | 否 | 应收 tab 不传(传了后端也忽略不落库) |
| evidenceUrl | string | 否 | 佐证材料影像 URL |
| remark | string | 否 | 备注(≤200) |
**响应**:`data.id` = 新建期初行 ID。
**请求示例(供应商应收)**:
```json
{
"ledgerType": "SUPPLIER_RECV",
"refId": 2104918057506041857,
"refName": "柴河星悦酒店",
"customerCategory": "DEPOSIT_REFUND",
"companyId": 2051922156798779394,
"openingReceivable": 1.01,
"remark": "押金退还期初"
}
```
**响应示例**:
```json
{ "code": 200, "message": "成功", "data": { "id": "2105077077235662849" }, "success": true }
```
### 4.3 期初调整
`POST /admin/finance/opening-balances/adjust`
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| ledgerType | string | 是 | 同新建 |
| refId | Long | 是 | 往来对象 ID(须已有 INIT 行,否则 598404) |
| openingReceivable | number | 条件 | **调整后期初应收(全量值非差额)**;CUSTOMER/SUPPLIER_RECV 落库 |
| openingPayable | number | 条件 | 调整后期初应付;仅 SUPPLIER 落库 |
| evidenceUrl | string | 否 | 佐证影像 |
| reason | string | **是** | 调整原因(≤200) |
> 分类/性质/公司沿用 INIT 行快照,调整单不开放改。
---
## 5. 应付初始化 tab(ledgerType = SUPPLIER)
接口同应收(`/admin/finance/opening-balances` 一套),差异:
| 项 | 应付初始化 |
|---|---|
| ledgerType | 固定 `SUPPLIER` |
| 往来对象 | 固定「供应商」,无客户/供应商下拉;直接供应商列表 `GET /admin/supplier/items/list?status=ACTIVE` |
| customerCategory | **不传**(SUPPLIER 忽略,供应商类别随供应商档案带出不手选) |
| 金额字段 | 传 `openingPayable`(期初应付,>0),**不传** `openingReceivable` |
列表看 `openingPayable`(`openingReceivable` 恒 null)。
---
## 6. 现金银行初始化 tab(走资金账户接口,独立)
此 tab 是**资金账户的期初结存**,与应收/应付的 opening-balances **完全独立**,接 `/admin/finance/fund-accounts`:
| 操作 | 接口 | 说明 |
|---|---|---|
| 列表 | `GET /admin/finance/fund-accounts/page` | 本 tab 列表(列:账户名称/类型/期初结存/操作) |
| 新建账户(录期初结存) | `POST /admin/finance/fund-accounts` | 建户时录账户信息+期初结存 |
| 期初调整 | `POST /admin/finance/fund-accounts/{id}/opening-adjust` | 唯一改期初途径,落 OPENING 留痕流水 |
| 建户表单三组下拉 | `GET /admin/finance/fund-accounts/options` | accountType/nature/channel(字典 fin_fund_account_*) |
> ⚠️ `opening-adjust` 差额为 0 时返 `595105`(不落流水),**非 200 幂等**——前端需区分「200 调整成功」vs「595105 无需调整」,不要把 595105 当失败弹错。
---
## 7. 枚举 / 数据字典
### 7.1 ledgerType(账套,OpeningLedgerTypeEnum)
**所属字段**:`ledgerType` | **类型**:`String` | **必填**:✅
| 值 | 中文 | 金额字段 | 用于 tab |
|----|------|------|------|
| `SUPPLIER` | 供应商应付(我欠他) | openingPayable | 应付初始化 |
| `CUSTOMER` | 客户应收(他欠我们) | openingReceivable | 应收初始化(往来对象=客户) |
| `SUPPLIER_RECV` | 供应商应收(他欠我们:押金退还/赔偿款/口车费/其他应收) | openingReceivable | 应收初始化(往来对象=供应商) |
### 7.2 kind(类别)
**所属字段**:`kind` | **类型**:`String` | **必填**:❌(查询过滤用)
| 值 | 中文 | 说明 |
|----|------|------|
| `INIT` | 初始 | 首次录入(同对象仅一次) |
| `ADJUST` | 期初调整 | 对已有 INIT 的调整留痕 |
### 7.3 应收性质(字典 fin_recv_nature,SUPPLIER_RECV 的 customerCategory)
| 值 | 中文 |
|----|------|
| `DEPOSIT_REFUND` | 押金退还 |
| `COMPENSATION` | 赔偿款 |
| `CAR_FEE` | 口车费 |
| `OTHER` | 其他应收 |
### 7.4 客户分类(字典 fin_customer_category,CUSTOMER 的 customerCategory)
经 `GET /admin/dict/all` 或字典接口取 `fin_customer_category` 当前生效值。
---
## 8. 下拉数据源汇总
| 下拉 | 接口 / 字典 |
|---|---|
| 供应商列表 | `GET /admin/supplier/items/list?status=ACTIVE` |
| 公司主体 | `GET /v3/admin/travel-agency/enabled` |
| 客户分类(应收-客户) | 字典 `fin_customer_category` |
| 应收性质(应收-供应商) | 字典 `fin_recv_nature`(押金退还/赔偿款/口车费/其他应收) |
| 资金账户类型/性质/渠道 | `GET /admin/finance/fund-accounts/options` |
---
## 9. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| 598401 | 同往来对象 INIT 已录过 | 重复新建初始期初 |
| 598403 | 账期已封账 | 封账后新建/调整 |
| 598404 | 须先录初始期初 | 调整时无 INIT 行 |
| 598405 | 期初金额须大于0 | 金额 ≤0 |
| 598407 | 公司主体非法 | companyId 无效 |
| 598408 | 客户分类必填 | CUSTOMER 缺 customerCategory |
| 598409 | 供应商应收性质必填 | SUPPLIER_RECV 缺 customerCategory |
| 598410 | 对象细分取值非法 | customerCategory 不在字典内 |
| 595105 | 期初调整差额为 0 | 现金银行 opening-adjust 无需调整(非错误) |
---
## 10. 修改前后对比
| 项 | 改前(当前前端错误) | 改后(对齐原型) |
|---|---|---|
| tab 数 | 4 个 | **3 个** |
| tab 名 | 客户应收/供应商应付/供应商应收/员工往来 | **应收初始化/应付初始化/现金银行初始化** |
| 划分维度 | 往来对象类型 | **初始化场景** |
| 员工往来 tab | 有(多出) | **删除** |
| 供应商应收 | 独立 tab | **并入应收初始化**(往来对象=供应商,ledgerType=SUPPLIER_RECV) |
| 现金银行初始化 | 缺 | **新增**(接 fund-accounts 期初) |
---
## 11. 影响评估 / 回滚
- **后端接口变更**:无(零改动,已 deployed)
- **是否破坏向后兼容**:前端页面重构,接口契约不变
- **前端是否必须同步上线**:是(当前 4 tab 结构与后端账套语义不符,「员工往来」tab 调任何接口都会失败——后端无员工往来账套)
## 12. 注意事项
- 金额字段二选一:应收 tab 填 `openingReceivable`、应付 tab 填 `openingPayable`,**不要同传两个**(后端按 ledgerType 只落对应方向,另一方向忽略)。
- 记账日期只读、前端不传,后端落当前账期起始日。
- 同一往来对象 INIT 仅可录一次;要改走「期初调整」。
- 「现金银行初始化」与「应收/应付初始化」是两套独立接口(fund-accounts vs opening-balances),不要混用。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#8507](https://git.1814.love/wx/HL/issues/8507) / [#8511](https://git.1814.love/wx/HL/issues/8511)
- **PR**: [#8509](https://git.1814.love/wx/HL/pulls/8509) / [#8513](https://git.1814.love/wx/HL/pulls/8513)
### 13.2 联系人
- **后端负责人**: @yst
- **前端对接(管理后台)**: 待认领
@@ -0,0 +1,273 @@
---
schema: "hl-changelog/v2"
ticket: "8508"
title: "调整配房(placement)已确认行同事务作废应付台账,有在途付款申请时新增 599602 拒绝"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "PR #8575 已合并 dev-v3(合并提交 72baca1fd),测试服 order-v3 运行 99fb369ba8(含本单)。测试服实测:已确认行调整后原台账行作废、配房行回到询价中;再确认后生成一条新台账行,金额=结算价×新间数;调整后在询价中删除,无孤儿台账行。599602 拒绝、已付行红冲、无台账存量行、询价中阴性对照由单测覆盖:在途付款申请与已付状态需要财务域写入才能造出来,测试服不造。gateway_status=not_required:路径与方法未变。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# order-v3: 调整配房已确认行同事务作废应付台账,有在途付款申请时新增 599602 拒绝
> **存放目录**:
> - 一期(v2,无 `order-v3` 标签的工单)→ `changelogs/{YYYY-MM}/`
> - 二期(v3,`order-v3` 标签的工单)→ `changelogs-v2/{YYYY-MM}/`
>
> **服务**: hl-order-service-v3
> **PR**: #8575
> **Issue**: #8508
> **日期**: 2026-09-30
> **影响范围**: 管理后台「房务配房工作台」§2.3b 调整单条配房位置与资源接口,原配房行为已确认(CONFIRMED)且有在途付款申请的调整场景
---
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
- `PUT /v3/admin/order/hotel-requirements/{requirementId}/assignments/{id}/placement` 新增一条失败分支:原配房行为「已确认(CONFIRMED)」,且该行的应付款台账有在途付款申请(`applied>0`)时,本次调整整体失败,返回 **599602**。此前这种情况会调整成功,但应付台账仍停在旧酒店旧价,产生台账与配房不一致。
- 除新增该错误码外,接口路径、方法、入参字段(`AssignmentPlacementUpdateReqVO`)、出参(`Result<Void>`)逐字节不变。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 调整单条配房位置与资源 | PUT | `/v3/admin/order/hotel-requirements/{requirementId}/assignments/{id}/placement` | 新增可能返回的错误码 | 原行已确认且应付台账有在途付款申请时返 599602 |
---
## 三、接口详情
### 1. 调整单条配房位置与资源 `PUT /v3/admin/order/hotel-requirements/{requirementId}/assignments/{id}/placement`
**VO**: `AssignmentPlacementUpdateReqVO → Void`
#### 使用场景
房务对已存在的单条配房行原子调整目标晚次、酒店、房型和房间数(前端只传目标 dayNumber,入住日期由后端按当前订单行程推导)。本次改动不涉及入参/出参字段,只新增一条业务失败分支:原行为「已确认」且其应付款台账行有在途付款申请时,整次调整被拒绝。
#### 入参
路径参数 + Body(与改造前逐字节相同):
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| requirementId | Path | Long | ✅ | - | 当前生效住宿需求 ID |
| id | Path | Long | ✅ | - | house_hotel_assignment 主键 |
| dayNumber | Body | Integer | ✅ | ≥1 | 目标晚次(1=第1晚) |
| hotelId | Body | Long | ✅ | - | 目标酒店 ID |
| roomTypeId | Body | Long | ✅ | - | 目标房型 ID,须归属该 hotelId |
| roomCategory | Body | String | ✅ | `@NotBlank` | 目标房型字典 code |
| roomCount | Body | Integer | ✅ | ≥1 | 目标房间数 |
| protoPrice | Body | BigDecimal | - | ≥0 | 协议价快照,不传按目标房型和目标入住日读取资源价格日历 |
| settlementPrice | Body | BigDecimal | - | ≥0 | 结算价快照,不传同上兜底 |
| settleType | Body | String | - | `cash\|sign\|company` | 支付方式快照 |
| deductInventory | Body | Boolean | ✅ | - | 是否扣减资源库存 |
| breakfast | Body | String | - | `INCLUDED\|EXCLUDED\|PENDING` | 早餐,不传保留原值 |
| cancelProofFileIds | Body | List\<Long\> | - | 最多 9 个 | 原酒店取消凭证文件 ID |
| cancelFee | Body | BigDecimal | - | ≥0,最多 2 位小数 | 原酒店取消费用 |
| changeRemark | Body | String | - | ≤200 字 | 改配说明 |
| syncProtocolPrice | Body | Boolean | - | 默认 false | 是否同步协议价到目标房型目标日期价格日历 |
| syncSettlementPrice | Body | Boolean | - | 默认 false | 是否同步结算价 |
| syncSettleType | Body | Boolean | - | 默认 false | 是否同步支付方式到目标酒店资源 |
| remark | Body | String | - | - | 备注,不传保留原备注 |
#### 出参 `Result<Void>`
| 字段 | 类型 | 说明 |
|------|------|------|
| data | null | 无返回数据 |
#### 请求示例
```json
{
"dayNumber": 2,
"hotelId": 200001,
"roomTypeId": 300001,
"roomCategory": "STANDARD",
"roomCount": 2,
"protoPrice": 320.00,
"settlementPrice": 280.00,
"settleType": "cash",
"deductInventory": true,
"syncProtocolPrice": false,
"syncSettlementPrice": false,
"syncSettleType": false,
"remark": "改期后重新询房"
}
```
#### 响应示例
```json
{ "code": 200, "message": "成功", "data": null, "success": true }
```
#### 空数据 / 降级响应
本接口无「空数据」概念(成功恒返回 `data: null`),无降级路径。
#### 错误响应
```json
{ "code": 599602, "message": "应付款台账行已锁定", "data": null, "success": false }
```
| code | 触发条件 |
|------|----------|
| **599602(本次新增)** | 原配房行为已确认(CONFIRMED),且其应付款台账最新有效行有在途付款申请(`applied>0`) |
(其余既有错误码——鉴权、参数校验、需求状态、库存迁移相关——均未变化,本次未改动,不在此重复列出。)
#### 业务边界
- 触发条件只看原配房行的 `confirmStatus`:只有原行为 `CONFIRMED` 时才会查应付台账锁;原行为 `INQUIRING` 时零台账查询,行为与改造前完全一致。
- 599602 命中时**零写入**:库存迁移分两处闸——一次是预占目标库存之前的只读预检,一次是写库前事务内的兜底闸;命中任一处都不会预占新库存、不会改配房行、不会动台账;若预检已通过但事务内并发被占用而在兜底闸命中,已预占的新库存会被同步补偿释放。
- 原行已确认且已付(`paid>0`)不受本次新增拒绝影响:由财务域内部对该行做整行红冲(`CLOSED`),调整照常成功。
- 原行已确认但没有应付台账行(台账上线前确认的存量行):判存后跳过台账处理,调整照常成功,不报错。
- 调整成功后该行照旧退回询价中(`INQUIRING`,改造前既有行为不变);台账不在本次调整时重推,由下次单日确认按新酒店、新间数、新结算价重新推送。
---
## 四、契约约束与正确调用方式(接口类必写)
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
- 触发 599602 与请求体字段无关,完全由服务端读取的原配房行状态(`confirmStatus`)与其应付台账的 `applied` 值决定;前端无法通过改写请求体规避或复现这条错误,只能据响应处理。
- 判断失败必须读响应体 `code === 599602`(业务失败,HTTP 状态码仍是 200),不要判 HTTP 状态码。
- 该错误码不是全新码——删除、改价、清空三条既有写路径已经会返回同一个 599602;前端若已对这三条路径统一处理该码,调整配房无需额外新增分支即可覆盖。
---
## 五、数据库行为(涉及写操作时必写)
本次不改变调整配房**成功**时原有的写入内容(配房行更新、库存迁移记账逐字节不变)。新增变化只发生在「原行已确认」这一分支:
| 场景 | 改前 | 改后 |
|------|------|------|
| 原行已确认、有台账行、未付、无在途申请 | 台账行不处理,停留在旧酒店旧价 | 同事务作废该台账行(未付取消),配房行照常更新 |
| 原行已确认、已付(`paid>0`)、无在途申请 | 台账行不处理 | 同事务由财务域整行红冲(`CLOSED`),配房行照常更新 |
| 原行已确认、有在途付款申请(`applied>0`) | 调整成功,台账行不处理 | 整次调整失败(599602),配房行、库存、台账三者均不写入 |
| 原行已确认、无台账行(存量行) | 调整成功 | 调整成功(无变化) |
| 原行询价中(INQUIRING) | 调整成功,不涉及台账 | 调整成功,不涉及台账(无变化) |
被作废的台账行不在本次调整时重新推送;下一次该晚单日确认(confirmDay)时按新酒店、新间数、新结算价重新推送。
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 非房务角色 → 808090(原「房务组长只读监督 808091」已随 #8491 删除:全体房务可读全部配房单)
- 配房行不存在,或 requirementId 与该行实际归属需求不一致 → 808120
- 需求不存在 / 已作废 / 不属于当前用户 / 未被抢单 → 808100/808113/808110/808116
- 订单已取消或异常处置中 → 808119
- 目标 dayNumber 超出应配晚数 → 808102
- 目标房型不属于目标酒店 → 808112
- 涉及库存迁移但原配房缺少可释放的持有日志 → 808126
- 团期子订单未成团 / 班期未建团 → 589552/589553
- **原行已确认且应付台账有在途付款申请(本次新增)→ 599602,HTTP 200,零写入**
- 老数据兼容:请求/响应字段无增删,存量数据无需迁移
---
## 六.5、枚举 / 数据字典(接口出现枚举时必写)
`confirmStatus` 不是本接口的请求/响应字段,但它是**决定本次新增分支是否触发**的关键状态,前端可从既有的配房行读接口(如 `HouseOrderDetailRespVO` 里配房行的 `confirmStatus`/`confirmStatusLabel`,字段本身未变)预判会不会撞上 599602。
### confirmStatus(配房行确认态,`HouseAssignmentConfirmStatus`)
**所属字段**: 配房行 `confirmStatus`(既有字段,本次未变) | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `INQUIRING` | 询房中 | 调用本接口零台账查询,行为与改造前一致 |
| `CONFIRMED` | 已确认 | 调用本接口会先查该行应付台账是否有在途付款申请,命中则 599602 |
---
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| (无)| 请求体、响应体字段逐字节不变 | 同左 |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 原行已确认、有台账行、有在途付款申请时调整 | 调整成功,台账行不处理,停留在旧酒店旧价 | 整次调整失败,返回 599602,配房/库存/台账三者均不变 |
| 原行已确认、有台账行、无在途申请(未付或已付)时调整 | 台账行不处理 | 同事务作废(未付取消 / 已付整行红冲),下次单日确认按新值重推 |
| 原行已确认、无台账行的存量行调整 | 调整成功 | 调整成功(无变化) |
| 原行询价中时调整 | 调整成功,不涉及台账 | 调整成功,不涉及台账(无变化) |
| 请求/响应字段 | 不变 | 不变 |
## 六.7、影响评估(修改/删除类必写)
- **是否破坏向后兼容**: 否——不改字段、不改既有错误码语义;新增一条此前不存在的失败路径(599602),且该码在删除/改价/清空三条既有路径中已经存在。
- **前端是否必须同步上线**: 若前端已对既有的删除/改价/清空三条路径统一处理 599602(按响应体 `code` 分支、非静默吞掉),调整配房无需新增代码即可覆盖;若尚未统一处理,需要补上。
- **前端 workaround 清理点**: 无。
---
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
- **仅影响**: `PUT .../placement` 端点,原配房行为已确认(CONFIRMED)时的应付台账处理与新增失败分支
- **零影响**:
- 原配房行为询价中(INQUIRING)时的调整(零台账查询,行为不变)
- 已确认但无应付台账行的存量配房行调整(判存后跳过,成功不报错)
- 已确认且已付(`paid>0`)的配房行调整(由财务域内部整行红冲,不新增拒绝)
- 本接口的请求体、响应体字段结构
- 删除、改价、清空、转房、订单取消级联等既有路径对 599602 的处理逻辑(本次未改动这些路径)
- hl-gateway 路由配置——端点路径、方法零变化
---
## 八、测试环境已验证
测试服 order-v3 运行 `99fb369ba8` 及其后代 `ff68637542`(均含 PR #8575 合并提交 `72baca1fd`),全部用自造订单:
- 已确认行调整(`PUT .../assignments/{id}/placement`,间数 1→2):`code=200`,配房行回到 `INQUIRING`,原台账行(seq=0)软删,改后无有效 NORMAL 行。
- 再确认该晚:配房行回到 `CONFIRMED`,台账新增 1 条有效行 seq=1,`payable=600.00`(300.00×2)。
- 换酒店(同一接口,改为同城另一家酒店,且两家酒店的供应商不同):`code=200`,配房行回到 `INQUIRING`,原台账行软删;再确认后台账有且只有 1 条有效行,`resource_id` 为新酒店,`supplier_id` 为新酒店的供应商(与原供应商不同),`payable=280.00`(280.00×1)。本条在 order-v3 `ff68637542` 上取证。
- 调整后在询价中删除(`DELETE /v3/admin/order/assignments/{id}`):只读孤儿判据 SQL 读数 0,无孤儿台账行。
以下分支由随 PR 提交的单测覆盖(`HouseAssignmentServiceTest`,`DisplayName` 均带 `#8508` 前缀;测试服不造「在途付款申请」「已付」状态,因为它们要在财务域写入):
- `updatePlacement_payableLockedAtPreflight_throws599602BeforeInventoryDeduct`:预检命中在途申请 → 599602,库存迁移、配房行更新、台账作废均未调用。
- `updatePlacement_payableLockedAtFallback_throws599602AndCompensatesTargetInventory`:预检通过、事务内并发被占用 → 599602,新预占库存同步释放。
- `updatePlacement_confirmedPaidLine_removesLineAndMigratesInventory`:已付(`paid>0`)无在途申请 → 不拦,作废台账行一次;财务域据此整行红冲(`CLOSED`),再推时按 seq+1 生成新行(财务域既有单测 `PayablePushServiceTest` 覆盖)。
- `updatePlacement_confirmedWithoutPayableLine_skipsRemoveAndSucceeds`:已确认无台账行的存量行 → 调整照常成功。
- `updatePlacement_inquiringOriginal_skipsPayableQueries`:原行询价中 → 不做任何台账查询或作废。
---
## 十、相关文档
- 关联 Issue: [wx/HL#8508](https://git.1814.love:8443/wx/HL/issues/8508)
- 关联 PR: [wx/HL#8575](https://git.1814.love:8443/wx/HL/pulls/8575)
## 关联 / 联系人
### 链接
- **Issue**: [#8508](https://git.1814.love:8443/wx/HL/issues/8508)
- **PR**: [#8575](https://git.1814.love:8443/wx/HL/pulls/8575)
- **Merge commit**: [72baca1fd](https://git.1814.love:8443/wx/HL/commit/72baca1fd)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,590 @@
---
schema: "hl-changelog/v2"
ticket: "8516"
title: "团期新增核单 / 结算状态两个内部回写接口(财务调用):改一列即推导团期主状态、同事务同步子订单状态,任何一步都不推报账单"
consumer: "internal"
author: "jw(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "新增 POST /v3/internal/group-batch/:groupBatchId/review-status 与 /settlement-status,供财务回写团期整团核单、结算状态。两个接口都是 internal:不经网关(公网网关对 /v3/internal/** 返回 code 403),直连 order-v3 带 X-Internal-Token,缺失或错误返回 HTTP 403;前端无需对接。团期主状态待核单 / 核单中 / 已结算由两列推导,与两列同一次原子更新落库;子订单在核单中、退回待核单、已结算、离开已结算四步同事务同步(只改状态、不推报账单),团期已核单及其退回不同步子订单。已合并 dev-v3(PR #8650,merge commit 54e64c50f),2026-09-30 部署 TEST(dev-v3 dd0452916,迁移 20260929.8516)并按 AC-05~AC-12 实测。管理后台读侧与既有写入口的变化见同日 30_8516 修改接口那份。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# 团期:新增核单 / 结算状态两个内部回写接口(财务调用)
> **服务**: hl-order-service-v3(端口 8086 / 8186,双实例)
> **PR**: #8650(merge commit `54e64c50f`)
> **Issue**: #8516
> **日期**: 2026-09-30
> **影响范围**: 财务侧服务间调用,回写团期整团核单 / 结算状态;管理后台读侧见同日 `30_8516_团期新增核单结算状态字段且出行完毕改名待核单-修改接口-管理后台.md`
---
## ⚠️ 关键变化
- 团期新增两个独立状态:核单 `reviewStatus`(与子订单同名同值)、结算 `settlementStatus`(比子订单多一个「结算中」),初始都是 `NONE`。
- 团期主状态里「待核单 / 核单中 / 已结算」三态**不再手动推进**,一律由这两个状态推导(推导表见六.5)。
- 财务通过本文两个接口回写这两个状态。系统唯一的自动写入是出行完毕定时任务(1042):团期出行结束时把核单置为「待核单」。
- 子订单跟着同步状态,**任何一步都不推报账单**;团期「已核单」及从已核单退回,**不同步**子订单。
---
## 一、背景
09-29 复盘团期出行完毕后的整条线:只走团级核单链路(一团一张报账单)时,子订单永远停在「待结算」、团期停在「核单中」,看板「结算」节点对这类团恒为空;代码里也没有「结算中」。jw 定口径:
1. 出行结束后团期自动进入「待核单」;
2. 核单中、已核单、待结算、结算中、已结算都由财务从外部更新;
3. 团期上加核单、结算两个独立状态,写法参照配房、配车;
4. 子订单跟着团期同步状态。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 改团期核单状态 | POST | `/v3/internal/group-batch/:groupBatchId/review-status` | 新增 | 财务回写整团核单状态(待核单 / 核单中 / 已核单),主状态随之推导,按规则同步子订单 |
| 2 | 改团期结算状态 | POST | `/v3/internal/group-batch/:groupBatchId/settlement-status` | 新增 | 财务回写整团结算状态(待结算 / 结算中 / 已结算),前提是核单已完成,按规则同步子订单与核团 |
---
## 三、接口详情
### 1. 改团期核单状态 `POST /v3/internal/group-batch/:groupBatchId/review-status`
**VO**: `GroupBatchReviewSettleStatusRespVO`(入参 `GroupBatchReviewSettleStatusReqVO`,两个接口共用)
#### 使用场景
财务开始核单、完成核单、或发现问题要退回核单时,回写团期整团核单状态。服务间调用:直连 order-v3 实例(8086 / 8186),请求头带 `X-Internal-Token`(经 Feign 调用时由内部令牌拦截器自动加头)。hl-finance 与订单服务同进程,也可以不走 HTTP,直接注入 `GroupBatchReviewSettleService#changeReviewStatus` 调用,规则完全相同。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| X-Internal-Token | Header | String | 是 | 服务间内部令牌 | 缺失或错误返回 HTTP 403 |
| groupBatchId | Path | Long | 是 | 团期 ID | 团期不存在返回 589500 |
| status | Body | String | 是 | 只能是 `PENDING` / `IN_PROGRESS` / `COMPLETED`,区分大小写 | 目标核单状态:待核单 / 核单中 / 已核单。`NONE`、空串、纯空白、null、小写、其他值一律 code 400,零写入 |
| operatorName | Body | String | 否 | 最长 64 字符 | 财务侧操作人姓名,写进团期时间线的操作人与子订单日志;超长返回 code 400 |
| reason | Body | String | 否 | 最长 500 字符 | 理由,写进团期时间线「原因」与子订单日志;超长返回 code 400 |
#### 出参 `Result<GroupBatchReviewSettleStatusRespVO>`
状态字段一律是**写后**的当前值。
| 字段 | 类型 | 说明 |
|------|------|------|
| groupBatchId | String | 团期 ID(按字符串输出) |
| batchStatus | String | 团期主状态(由两个状态推导):`PENDING_REVIEW` 待核单 / `REVIEWING` 核单中 / `SETTLED` 已结算 |
| batchStatusName | String | 主状态中文名 |
| reviewStatus | String | 核单状态:`NONE` / `PENDING` / `IN_PROGRESS` / `COMPLETED` |
| reviewStatusName | String | 未核单 / 待核单 / 核单中 / 已核单 |
| settlementStatus | String | 结算状态:`NONE` / `PENDING` / `IN_PROGRESS` / `COMPLETED` |
| settlementStatusName | String | 未结算 / 待结算 / 结算中 / 已结算 |
| changed | Boolean | `true` 本次改了库;`false` 目标值与当前值相同(幂等命中),零写入、不留痕、不同步子订单 |
| subOrderSyncedCount | Integer | 本次**真正改了状态**的子订单户数。本次变化不触发子订单同步时为 `null`;触发了但各户都已在目标状态时为 `0` |
| subOrderSkippedOrderIds | List\<String\> | 不在可同步状态、被跳过的子订单 ID(按字符串输出),需财务跟进。不触发同步时为 `null`;触发了但无人被跳过时为 `[]`。已经处于目标状态的户**不**列入,也不计入 subOrderSyncedCount |
#### 请求示例
```http
POST /v3/internal/group-batch/2105223439872933889/review-status HTTP/1.1
Host: 192.168.100.236:8086
X-Internal-Token: <内部令牌>
Content-Type: application/json
{
"status": "IN_PROGRESS",
"operatorName": "财务-王丽华",
"reason": "呼伦贝尔草原3日游·9月25日团开始核单"
}
```
#### 响应示例
团期 T26-2325 从待核单改为核单中:出行过的王海峰户同步进核单中;赵淑芬户出团时仍在定制中、没有随团进入待核单,本次被跳过(TEST 2026-09-30 17:34 实测):
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2105223439872933889",
"batchStatus": "REVIEWING",
"batchStatusName": "核单中",
"reviewStatus": "IN_PROGRESS",
"reviewStatusName": "核单中",
"settlementStatus": "NONE",
"settlementStatusName": "未结算",
"changed": true,
"subOrderSyncedCount": 1,
"subOrderSkippedOrderIds": ["2105223440825020417"]
},
"traceId": null,
"success": true
}
```
#### 空数据 / 降级响应
本接口没有列表型空数据。以下两种是「成功但没有同步动作」:
1. 同值幂等:目标值与当前值相同,`changed=false`,两个同步字段为 `null`,零写入(模拟财务重试,17:33 实测):
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2105223438312652802",
"batchStatus": "PENDING_REVIEW",
"batchStatusName": "待核单",
"reviewStatus": "PENDING",
"reviewStatusName": "待核单",
"settlementStatus": "NONE",
"settlementStatusName": "未结算",
"changed": false,
"subOrderSyncedCount": null,
"subOrderSkippedOrderIds": null
},
"traceId": null,
"success": true
}
```
2. 改为已核单、或从已核单退回:只改团期,不同步子订单,`changed=true`,两个同步字段为 `null`(团期 T26-2325 从待核单直接改为已核单,17:46 实测):
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2105223439872933889",
"batchStatus": "REVIEWING",
"batchStatusName": "核单中",
"reviewStatus": "COMPLETED",
"reviewStatusName": "已核单",
"settlementStatus": "NONE",
"settlementStatusName": "未结算",
"changed": true,
"subOrderSyncedCount": null,
"subOrderSkippedOrderIds": null
},
"traceId": null,
"success": true
}
```
#### 错误响应
缺少或错误的内部令牌(HTTP 403,body 字段名是 `msg`,与 `vehicle-ready` 等内部接口同形):
```json
{ "code": 403, "msg": "内部接口禁止外部访问" }
```
status 非法(HTTP 200,code 400,零写入):
```json
{
"code": 400,
"message": "status 取值只能是 PENDING、IN_PROGRESS、COMPLETED",
"data": null,
"traceId": null,
"success": false
}
```
结算已是已结算时退回核单(零写入):
```json
{
"code": 589702,
"message": "结算已完成,请先把结算状态改回待结算或结算中,再退回核单",
"data": null,
"traceId": null,
"success": false
}
```
| code | 触发条件 | 调用方下一步 |
|------|----------|--------------|
| HTTP 403 | 缺少或错误的 `X-Internal-Token` | 检查令牌配置 |
| 400 | status 缺失 / 空串 / 纯空白 / null(`status 不能为空`);取值不是三者之一(`status 取值只能是 PENDING、IN_PROGRESS、COMPLETED`,含 `NONE`、小写、`TRIP_FINISHED`);operatorName 超 64(`operatorName 不能超过 64 个字符`);reason 超 500(`reason 不能超过 500 个字符`)。多条同时违反时 message 以 `; ` 连接 | 修正入参 |
| 589500 | 团期不存在(`团期不存在`) | 核对团期 ID |
| 589700 | 团期还没出行完毕(招募中 ~ 出行中)或已流团:`团期当前状态为「招募中」,不可修改核单或结算状态(须已出行完毕且未流团)`,「」内是当前主状态中文名 | 不要调 |
| 589702 | 从已核单退回(改为待核单或核单中),而结算已是已结算 | 先调接口 2 把结算改回待结算或结算中 |
| 589703 | 读到写之间,团期主状态 / 核单 / 结算任一被并发改动(`团期核单或结算状态已被他人修改,请刷新后重试`),零写入 | 重新读取后决定是否重调 |
#### 业务边界
- **鉴权**:只认 `X-Internal-Token`,不经网关、不走管理员登录与团期权限码;本接口不另做判权。
- **阶段门**:团期主状态必须是待核单 / 核单中 / 已结算之一(已出行完毕且未流团),否则 589700。
- **幂等**:目标值等于当前值 → `changed=false`,不写库、不写时间线、不同步子订单。
- **从已核单退回**(改为核单中或待核单):结算未到已结算时,结算**自动置回 `NONE`**,时间线写明「结算状态随之置回」;结算已是已结算时拒绝 589702。
- **允许从待核单直接改为已核单**(跳过核单中):此时子订单停在待核单;之后结算改为已结算时,这些户不会被带上,出现在 `subOrderSkippedOrderIds` 里。由财务控制,系统不拦(jw 09-30 定)。
- **子订单同步**(与团期改动同一事务,全有全无;只改状态,不推报账单):
| 本次核单变化 | 哪些子订单被改 | 子订单核单 / 结算 / 流程状态改成 |
|---|---|---|
| 待核单 → 核单中 | 核单状态为待核单的户 | 核单中 / 未结算 / 核单中(`IN_PROGRESS` / `NONE` / `REVIEWING`) |
| 核单中 → 待核单 | 核单状态为核单中的户 | 待核单 / 未结算 / 待核单(`PENDING` / `NONE` / `PENDING_REVIEW`) |
| → 已核单 | **不同步**(子订单的已核单只靠逐户提交核单) | — |
| 已核单 → 核单中 / 待核单 | **不同步**,只改团期(已逐户提交的户保持已核单,某户要改走逐户反确认) | — |
- 范围是本团未取消的子订单;已在目标状态的户不改、不计数、不列入跳过名单;其余不在可同步状态的户列入 `subOrderSkippedOrderIds`。
- **留痕**:核单真的变化时写一条团期时间线「核单状态变更」(带操作人与原因);主状态随之变化时另有一条主状态流转行。每个被同步的子订单写一条订单日志,带团期 ID 与来源。
- **不带「期望的当前状态」**:以调用时库里的当前值为准。由财务保证不重试、不乱序(见四)。
### 2. 改团期结算状态 `POST /v3/internal/group-batch/:groupBatchId/settlement-status`
**VO**: `GroupBatchReviewSettleStatusRespVO`(入参 `GroupBatchReviewSettleStatusReqVO`,两个接口共用)
#### 使用场景
财务在团级报账单推出后把团期结算改为待结算、付款开始后改为结算中、付清后改为已结算;出纳冲正、付款失败等需要回退时,把结算从已结算改回待结算或结算中。调用方式同接口 1;同进程也可直接调用 `GroupBatchReviewSettleService#changeSettlementStatus`。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| X-Internal-Token | Header | String | 是 | 服务间内部令牌 | 缺失或错误返回 HTTP 403 |
| groupBatchId | Path | Long | 是 | 团期 ID | 团期不存在返回 589500 |
| status | Body | String | 是 | 只能是 `PENDING` / `IN_PROGRESS` / `COMPLETED`,区分大小写 | 目标结算状态:待结算 / 结算中 / 已结算。`NONE`、空串、纯空白、null、小写、其他值一律 code 400,零写入 |
| operatorName | Body | String | 否 | 最长 64 字符 | 财务侧操作人姓名,写进团期时间线与子订单日志;超长返回 code 400 |
| reason | Body | String | 否 | 最长 500 字符 | 理由,写进团期时间线与子订单日志;超长返回 code 400 |
#### 出参 `Result<GroupBatchReviewSettleStatusRespVO>`
与接口 1 同一个 VO,状态字段一律是写后的当前值。
| 字段 | 类型 | 说明 |
|------|------|------|
| groupBatchId | String | 团期 ID(按字符串输出) |
| batchStatus | String | 团期主状态:结算为已结算时 `SETTLED`,否则 `REVIEWING` |
| batchStatusName | String | 主状态中文名 |
| reviewStatus | String | 核单状态(本接口不改它,恒为 `COMPLETED`) |
| reviewStatusName | String | 已核单 |
| settlementStatus | String | 结算状态:`PENDING` / `IN_PROGRESS` / `COMPLETED`(幂等命中时为当前值) |
| settlementStatusName | String | 待结算 / 结算中 / 已结算 |
| changed | Boolean | `true` 本次改了库;`false` 幂等命中,零写入 |
| subOrderSyncedCount | Integer | 同接口 1:只数本次真正改了状态的户;不触发同步时为 `null` |
| subOrderSkippedOrderIds | List\<String\> | 同接口 1:不触发同步时 `null`,触发了但无人被跳过时 `[]` |
#### 请求示例
```http
POST /v3/internal/group-batch/2105223438312652802/settlement-status HTTP/1.1
Host: 192.168.100.236:8086
X-Internal-Token: <内部令牌>
Content-Type: application/json
{
"status": "COMPLETED",
"operatorName": "财务-王丽华",
"reason": "报账款已付清,整团结算完成"
}
```
#### 响应示例
团期 T26-5936 结算改为已结算:已逐户提交核单、处于待结算的李秀英户同步为已结算;张建国户逐户核单未提交,被跳过(TEST 18:11 实测,全程报账单行数不变):
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2105223438312652802",
"batchStatus": "SETTLED",
"batchStatusName": "已结算",
"reviewStatus": "COMPLETED",
"reviewStatusName": "已核单",
"settlementStatus": "COMPLETED",
"settlementStatusName": "已结算",
"changed": true,
"subOrderSyncedCount": 1,
"subOrderSkippedOrderIds": ["2105223438178435074"]
},
"traceId": null,
"success": true
}
```
结算从已结算改回结算中:两户都退回待结算,同时核团从「已结算」退回「已核算」(18:21 实测):
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2105223438312652802",
"batchStatus": "REVIEWING",
"batchStatusName": "核单中",
"reviewStatus": "COMPLETED",
"reviewStatusName": "已核单",
"settlementStatus": "IN_PROGRESS",
"settlementStatusName": "结算中",
"changed": true,
"subOrderSyncedCount": 2,
"subOrderSkippedOrderIds": []
},
"traceId": null,
"success": true
}
```
#### 空数据 / 降级响应
本接口没有列表型空数据。结算在未到已结算的范围内变化(未结算 → 待结算、待结算 → 结算中等)不同步子订单,两个同步字段为 `null`(17:41 实测):
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2105223438312652802",
"batchStatus": "REVIEWING",
"batchStatusName": "核单中",
"reviewStatus": "COMPLETED",
"reviewStatusName": "已核单",
"settlementStatus": "PENDING",
"settlementStatusName": "待结算",
"changed": true,
"subOrderSyncedCount": null,
"subOrderSkippedOrderIds": null
},
"traceId": null,
"success": true
}
```
同值重复回写时 `changed=false`,其余字段为当前值,零写入。
#### 错误响应
核单还没完成就改结算(零写入):
```json
{
"code": 589701,
"message": "核单尚未完成(当前「待核单」),不可修改结算状态,请先将核单状态改为「已核单」",
"data": null,
"traceId": null,
"success": false
}
```
已流团的团期(零写入):
```json
{
"code": 589700,
"message": "团期当前状态为「已取消」,不可修改核单或结算状态(须已出行完毕且未流团)",
"data": null,
"traceId": null,
"success": false
}
```
| code | 触发条件 | 调用方下一步 |
|------|----------|--------------|
| HTTP 403 | 缺少或错误的 `X-Internal-Token`(body `{"code":403,"msg":"内部接口禁止外部访问"}`) | 检查令牌配置 |
| 400 | 入参校验失败,规则与文案同接口 1 | 修正入参 |
| 589500 | 团期不存在 | 核对团期 ID |
| 589700 | 团期还没出行完毕或已流团 | 不要调 |
| 589701 | 核单状态不是已核单,「」内是当前核单状态中文名 | 先调接口 1 把核单改为已核单 |
| 589703 | 团期三个状态被并发改动,零写入 | 重新读取后决定是否重调 |
| 589573 | 结算离开已结算、退回核团时核团被并发修改(`整团核单数据已被他人修改…请刷新后重试`),整体回滚 | 重新读取后决定是否重调 |
#### 业务边界
- **鉴权、阶段门、幂等**:同接口 1。
- **前提**:核单状态必须是已核单,否则 589701。
- **主状态**:结算为已结算 → `SETTLED`;其余 → `REVIEWING`。
- **子订单同步**(同一事务,只改状态,不推报账单,已推的报账单也不撤):
| 本次结算变化 | 哪些子订单被改 | 子订单核单 / 结算 / 流程状态改成 |
|---|---|---|
| → 待结算 / 结算中(未离开已结算) | 不同步(子订单没有「结算中」,保持待结算) | — |
| → 已结算 | 结算状态为待结算(已逐户提交核单)的户 | 已核单 / 已结算 / 已结算(`COMPLETED` / `COMPLETED` / `SETTLED`),写结算时间 |
| 已结算 → 待结算 / 结算中 | 结算状态为已结算的户 | 已核单 / 待结算 / 待结算(`COMPLETED` / `PENDING` / `PENDING_SETTLE`),清结算时间 |
- **未提交户不拦**:改为已结算时,逐户核单没提交的户**不会**让本接口失败,而是被跳过、列入 `subOrderSkippedOrderIds`,之后一直留在已结算的团里。改为已结算前,请财务确认各户都已逐户提交核单(与管理后台 `/settle` 不同,那个入口遇未提交户整团拒绝 589568)。
- **已结算不要求报账单已推出**:由财务确认一团一张的报账单推出后再改为已结算(jw 09-29 定)。
- **结算离开已结算**(本接口与管理后台 `/settle/reopen` 同一处理):核团同事务从「已结算」(CHECKED)退回「已核算」(ALLOCATED),清空验团人、验团时间与意见;之后重新核算、再结算、再反结算都可正常使用。
- 「结算中」的业务含义由财务定义,接口只存值。
- **留痕**:结算真的变化时写一条团期时间线「结算状态变更」;主状态进出已结算时另有一条「结算」主状态流转行;核团被退回时时间线附 `auditReverted=true`(只在时间线里,不在接口响应里)。
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload |
|------|---------|
| ✅ 开始核单 | `{ "status": "IN_PROGRESS", "operatorName": "财务-王丽华", "reason": "开始核单" }` |
| ✅ 只传状态 | `{ "status": "COMPLETED" }`(operatorName、reason 选填) |
| ❌ 想把状态清回初始值 | `{ "status": "NONE" }` → 400(`NONE` 不允许外部写入) |
| ❌ 小写或旧值 | `{ "status": "pending" }`、`{ "status": "TRIP_FINISHED" }` → 400 |
| ❌ 核单没到已核单就改结算 | 核单为待核单 / 核单中时调接口 2 → 589701 |
| ❌ 已结算时退回核单 | 结算为已结算时调接口 1 改为核单中 / 待核单 → 589702 |
### 正常调用顺序
```text
(1042 自动)核单=待核单
→ 接口 1 IN_PROGRESS(核单中)→ 接口 1 COMPLETED(已核单)
→ 接口 2 PENDING(待结算)→ 接口 2 IN_PROGRESS(结算中)→ 接口 2 COMPLETED(已结算)
```
### 对接注意事项(jw 09-29 / 09-30 已定,均由财务侧流程控制,系统不加门禁)
1. **核单可以从待核单直接跳到已核单**:此时子订单停在待核单,之后结算改为已结算的同步不会带上它们(出现在跳过名单里)。
2. **接口不带「期望的当前状态」**:由财务保证不重试、不乱序。迟到的重试会把状态改回去,并连带子订单、核团一起回退。
3. **团期已核单不代表每户都已核单**:团期改为已核单时不检查各户是否已逐户提交;财务确认各户都已提交后,再把结算改为已结算。
4. **结算改为已结算不要求报账单已推出**:可能出现没有报账单的已结算团,由财务把关。
5. **团期子订单不要做逐户「财务复核确认」**:逐户确认会推 ORDER 报账单,可能与团级 GROUP_BATCH 报账单重复;系统既不拦截,也不跳过推单。
6. **核单变为已核单后不冻结**团级共享成本录入和团级定稿。
7. 返回 `subOrderSkippedOrderIds` 非空时,名单里的户状态没有跟上团期,需要财务跟进。
---
## 五、数据库行为
- 团期主状态、核单状态、结算状态三者在**同一次原子更新**里写入,条件是三者都等于读到的旧值;未命中返回 589703,本次零写入。
- 子订单状态同步与团期改动**同一事务**,任一户写失败整体回滚。每个被改的户写一条订单日志:核单两步记「流程推进」,结算两步记「结算确认」/「核单反确认」,内容写明「团期同步、不推报账单」,日志附带团期 ID、来源 `GROUP_BATCH_REVIEW_SETTLEMENT_SYNC`、步骤、操作人与理由。
- 不生成、不撤回任何报账单(TEST 全程 88 次快照报账单行数恒定)。
- 结算离开已结算时,核团同事务退回已核算,并清空验团人、验团时间与意见。
- 团期时间线写入失败只记告警、不回滚(非主链路);子订单订单日志不降级。
- 幂等命中(`changed=false`)与所有错误返回:零写入。
---
## 六、边界行为
- 缺少或错误的内部令牌 → HTTP 403 `{"code":403,"msg":"内部接口禁止外部访问"}`,两个实例表现一致。
- 经公网网关访问 `/v3/internal/**` → 网关直接拒绝(`code 403`「接口不可访问」),请求到不了服务。
- 入参非法 → HTTP 200 + code 400,零写入。
- 团期不存在 → 589500;未出行完毕或已流团 → 589700。
- 两次调用并发打到同一团期:先拿到团期行锁的先执行,后到者读到的是先到者已提交的值,同值时走幂等分支;兜底冲突返回 589703。
- 在团子订单为空(团内户全部取消)时同步不报错,`subOrderSyncedCount=0`、`subOrderSkippedOrderIds=[]`。
- 团期「已核单」、从「已核单」退回:永远不同步子订单(`subOrderSyncedCount` / `subOrderSkippedOrderIds` 为 `null`)。
## 六.5、枚举 / 数据字典
### reviewStatus(`com.hulalv.order.settlement.enums.ReviewStatus`,与子订单核单状态同一枚举)
**所属字段**: `GroupBatchReviewSettleStatusRespVO.reviewStatus` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `NONE` | 未核单 | 初始值,还没出行完毕;外部不可写入 |
| `PENDING` | 待核单 | 出行完毕(1042 自动写入)或财务退回 |
| `IN_PROGRESS` | 核单中 | 财务开始核单;管理后台「发起核单」、首笔共享成本也会写入 |
| `COMPLETED` | 已核单 | 财务完成核单;管理后台 `/settle` 也会写入 |
### settlementStatus(`com.hulalv.order.groupbatch.enums.GroupBatchSettlementStatus`)
**所属字段**: `GroupBatchReviewSettleStatusRespVO.settlementStatus` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `NONE` | 未结算 | 核单完成之前恒为此值;外部不可写入;核单从已核单退回时自动置回 |
| `PENDING` | 待结算 | 核单已完成、尚未开始结算。注意同一个值在子订单上叫「待财务复核」 |
| `IN_PROGRESS` | 结算中 | 团期独有,子订单没有这一态;含义由财务定义 |
| `COMPLETED` | 已结算 | 团期主状态随之变为已结算 |
### batchStatus 推导表(`com.hulalv.order.groupbatch.enums.GroupBatchStatus`)
**所属字段**: `GroupBatchReviewSettleStatusRespVO.batchStatus` | **类型**: `String`
| 核单 | 结算 | → 主状态 |
|------|------|----------|
| `NONE` | `NONE` | 不适用(还没出行完毕,接口按 589700 拒绝,不改主状态) |
| `PENDING` | `NONE` | `PENDING_REVIEW` 待核单 |
| `IN_PROGRESS` | `NONE` | `REVIEWING` 核单中 |
| `COMPLETED` | `NONE` / `PENDING` / `IN_PROGRESS` | `REVIEWING` 核单中 |
| `COMPLETED` | `COMPLETED` | `SETTLED` 已结算 |
`PENDING_REVIEW`「待核单」即原 `TRIP_FINISHED`「出行完毕」,#8516 改名,存量数据已随迁移改写。
---
## 七、不影响范围
- **仅影响**:新增的两个内部接口;团期核单 / 结算状态的写入统一收进同一个写口。
- **零影响**:
- 团级核单链路(核单定稿 finalize / 确认 confirm)与回款监听不写这两个状态,口径不变;
- 逐户确认 `/{orderId}/settlement/confirm`、逐户反确认 `/{orderId}/settlement/final-snapshots/reopen` 对团期子订单不加限制;
- 子订单第一次录核单明细自动进入核单中的逐户逻辑保留;
- 出行完毕定时任务的内部触发接口 `POST /v3/internal/jobs/group-batch-trip-finish/run`:请求、响应(本次推进的团期数)不变;推进时顺带把团期核单置为待核单,出行中的子订单随团进入待核单(#8340 既有逻辑),当时不在出行中的户跳过并打告警,团期照常推进。
- 管理后台读接口(新增四个字段、`TRIP_FINISHED` 改名、进度条分支、入参旧值兼容)与既有写入口的行为变化,见同日修改接口那份。
---
## 八、测试环境已验证
2026-09-30 TEST:合并提交 `54e64c50f`,部署构建 dev-v3 `dd0452916`(order-v3 双实例 8086 / 8186,16:32 启动,运行字节经探针核对),迁移 `20260929.8516` 于 16:32:13 执行成功。验收团期为本次新建(T26-5936 呼伦贝尔草原3日游·9月26日团、T26-2325 呼伦贝尔草原3日游·9月25日团、T26-8714 流团用),内部接口用 `X-Internal-Token` 直连两个实例。证据目录 `HL/.evidence/8516/`。
```text
AC-04 POST /v3/internal/jobs/group-batch-trip-finish/run → data=2;两团 TRAVELLING→PENDING_REVIEW、核单=待核单;出行中的户同事务进待核单,定制中的户跳过并告警 ✓
AC-05 两接口 × 两实例:无令牌 / 伪造令牌 → HTTP 403;status 缺失/空串/空白/null/NONE/DONE/小写/TRIP_FINISHED → code 400,前后零写入 ✓
AC-06 推导表七行逐组合实测:NONE/NONE 不适用(589700)、PENDING/NONE→PENDING_REVIEW、IN_PROGRESS/NONE 与 COMPLETED/NONE/PENDING/IN_PROGRESS→REVIEWING、COMPLETED/COMPLETED→SETTLED ✓
AC-07 同值幂等 changed=false 零写入;589700(招募中/出行中/已流团)、589701、589702 各自触发;589500 团期不存在;核单退回时结算自动置回 NONE;结算离开已结算两条路径核团 CHECKED→ALLOCATED,之后重新核算、再结算、再反结算均成功 ✓
AC-08 子订单同步逐行正反向实测:跳过户进 subOrderSkippedOrderIds(无同步 null、有同步无跳过 []);已核单退回核单中 / 待核单时子订单未被带动;全程 88 次快照报账单行数不变 ✓
AC-12 时间线可见「核单状态变更」「结算状态变更」,operatorName=财务-王丽华,reason 为调用方传入原因 ✓
```
| AC | 证据 |
|----|------|
| AC-04 | `HL/.evidence/8516/AC-04/` |
| AC-05 | `HL/.evidence/8516/AC-05/01-auth-and-validation.json`、`02-controls-vehicle-ready-and-gateway.json` |
| AC-06 | `HL/.evidence/8516/AC-06/` |
| AC-07 | `HL/.evidence/8516/AC-07/` |
| AC-08 | `HL/.evidence/8516/AC-08/` |
| AC-12 | `HL/.evidence/8516/AC-12/01-status-logs.json` |
单元测试:推导全组合 `GroupBatchReviewSettleDeriverTest`、写入规则 `GroupBatchReviewSettleServiceTest`、内部接口切片与鉴权 `GroupBatchReviewSettleInternalControllerTest` / `GroupBatchReviewSettleInternalAuthTest`、子订单同步正反向与报账单行数守卫 `GroupBatchReviewSettleH2IT`;order-v3 全量两半对照干净 dev-v3 基线,本单新增失败 0。
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| — | #7190 | 团期新增出行完毕 `TRIP_FINISHED` | ⚠️ 状态值已改名 `PENDING_REVIEW` 待核单 |
| — | #8340 | 团期出发 / 出行完毕时子订单随团推进 | ✅ 有效(出行完毕时另写核单=待核单) |
| — | #8341 | 团期核单 / 结算 / 反结算同步子订单,团期结算即整团财务复核并逐户推报账单 | ⚠️ 部分被本单取代:同步改按本单第 6 节,结算不再逐户推报账单 |
| — | #8361 / #8363 | 报账只认一团一张、成本只认团级快照(09-28 口径) | ✅ 有效,本单据此停掉逐户推单 |
| **#8650** | **#8516** | 团期核单 / 结算独立状态、财务内部回写接口 | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#8516](https://git.1814.love/wx/HL/issues/8516)
- 关联 PR: [wx/HL#8650](https://git.1814.love/wx/HL/pulls/8650)
- 状态机文档:`docs/group/实施单/16-团期生命周期与状态机.html`(随 PR #8650 同步,提交 `30ed15777`);接口文档 `docs/group/团期模块接口文档-v2.0.html`
- 同日管理后台读侧:`changelogs-v2/2026-09/30_8516_团期新增核单结算状态字段且出行完毕改名待核单-修改接口-管理后台.md`
## 关联 / 联系人
### 链接
- **Issue**: [#8516](https://git.1814.love/wx/HL/issues/8516)
- **PR**: [#8650](https://git.1814.love/wx/HL/pulls/8650)
- **Merge commit**: [54e64c50f](https://git.1814.love/wx/HL/commit/54e64c50f)
### 联系人
- **后端负责人**: @jw
@@ -0,0 +1,329 @@
---
schema: "hl-changelog/v2"
ticket: "8543"
title: "团期订单 Tab 用车状态三处读口按需求类别拆分并统一文案"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "bcd073be952146212d268f2e31f6d82bcb82cab2"
target_release: "v2.1"
verified_at: "2026-09-30"
status_note: "PR #8592 合并 dev-v3(bc80606ac2);hl-order-service-v3 dev-v3 分支部署测试网关 @ 99fb369ba 并实测:配车芯片明细端点 items[] 按用车类别拆项、同一 orderId 出现两次(TRAVEL/TRANSFER 各一);totalCount/doneCount 由户数变为条目数(实测 5 项 3 完成,对应 3 户);子订单列表新增 vehicleRequirementKind 字段恒为 TRAVEL;文案分支待车队配/配车完成/待提交车务/待审核均已实测复现;团期列表页 chipStats.vehicle 与明细端点同步联动同一计数口径。;前端已交付:配车芯片明细按 orderId+kind 复合键渲染+类别标签直显 kindName,汇总口径分叉(vehicle 条目数称条/其余芯片户数),子订单列表文案分叉直显后端 statusName 自动生效,10 例定向测试全绿(hl-admin bcd073be)"
updated_at: "2026-09-30"
base: "dev-v3"
---
# hl-order-service-v3: 团期订单 Tab 用车状态三处读口按需求类别拆分并统一文案
> **存放目录**: `changelogs-v2/2026-09/`
> **服务**: hl-order-service-v3 (端口 8086)
> **PR**: #8592
> **Issue**: #8543
> **日期**: 2026-09-30
> **影响范围**: 团期配车芯片明细端点 `GET /v3/admin/order/group-batch/{groupBatchId}/chips/vehicle`、团期下子订单列表端点 `GET /v3/admin/order/group-batch/{groupBatchId}/orders`;团期列表/看板端点的 `chipStats.vehicle` 计数口径联动变化(无字段新增,纯语义联动)
---
## ⚠️ 关键变化
- 🔴 **破坏性变更**:`GET .../chips/vehicle` 的 `items[]` 从**一户一项**改为**一户每类一项**——同一户同时有"行程用车"(TRAVEL)与"接送机"(TRANSFER)两类需求时,`items[]` 里会出现两条 `orderId` 相同、`kind` 不同的记录。**前端不得再按 `orderId` 去重**,去重会随机丢掉其中一类需求的状态。
- `totalCount` / `doneCount` 的口径随之从"户数"变为"条目数":一户两类需求算两条,分母跟着变大。实测团期批次 T26-3963 为例:3 户、5 条需求行,`totalCount=5`(不是 3),`doneCount=3`。
- `GroupBatchChipItemRespVO`(仅配车芯片会用到)新增 `kind`(TRAVEL/TRANSFER,可空)与 `kindName`(配对中文名,可空)两个字段;房/导/摄/约/保五个芯片的 `items[]` 里这两个字段恒为 `null`。
- `GroupBatchOrderItemRespVO`(子订单列表 `.../orders` 出参)新增 `vehicleRequirementKind` 与 `vehicleRequirementKindName` 两个字段——是新增字段不是破坏性变更。这一列**当前恒为 TRAVEL**:子订单列表每户只占一行,装不下两类需求,上游按 TRAVEL 过滤取行;要看接送机需求要走配车芯片明细(一户两类各占一项)。
- 用车状态文案本次统一(此前配车芯片明细端点的映射与子订单列表端点各写各的,现在两处一致):`PENDING`→**待车队配**、`PENDING_REVIEW` 按 `kind` 分叉(TRAVEL 出**待提交车务**、TRANSFER 出**待审核**)、`REJECTED_TO_CONSULTANT`→**已驳回定制师**、`REJECTED_TO_ADMIN`→**已驳回管理员**、`DONE`→**配车完成**。
- 该户 `needsIt=true` 但尚无任何 active 车需求行时(已声明要用车、但一行都没提交),配车芯片明细该户仍占一项,`kind`/`status` 为 `null`、`statusName` 为**未提交**(源码逻辑与 PR 说明已确认,本轮实测样本未覆盖到该分支,样本团期均已提交需求行)。
- 团期列表页(`GET /v3/admin/order/group-batch`)与看板端点每行的 `chipStats.vehicle.total`/`chipStats.vehicle.done` 与本次改动**共用同一套聚合算法**(`GroupBatchChipResolver.aggregateAll`),因此同步从"户数"变为"条目数"——实测同一团期批次两处读数逐位一致(5/3 与 3/1)。这两个端点本身**没有新增字段**,只是既有的 `total`/`done` 计数口径联动变了,前端若在列表页展示"配车 X/Y"这类徽标也要同步理解口径变化。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 团期配车芯片明细 | GET | `/v3/admin/order/group-batch/{groupBatchId}/chips/vehicle` | 🔴 破坏性变更 + 字段新增 | `items[]` 按用车类别拆项,同一 `orderId` 可出现两次;新增 `kind`/`kindName`;`totalCount`/`doneCount` 语义由户数变为条目数 |
| 2 | 团期下子订单列表 | GET | `/v3/admin/order/group-batch/{groupBatchId}/orders` | 字段新增 | 新增 `vehicleRequirementKind`/`vehicleRequirementKindName`(当前恒为 TRAVEL);`vehicleRequirementStatusName` 的 `PENDING_REVIEW` 文案按 `kind` 分叉 |
---
## 三、接口详情
### 1. 团期配车芯片明细 `GET /v3/admin/order/group-batch/{groupBatchId}/chips/vehicle`
**VO**: `(无请求体,仅路径参数)` → `GroupBatchChipDetailVO`
#### 使用场景
车务/团期管理员在团期订单 Tab 展开"配车"芯片查看逐户用车状态明细时调用。六个芯片(房/车/导/摄/约/保)共用同一套 `GroupBatchChipDetailVO` 响应结构与六个并列端点,本条目专指配车芯片(其余五芯片本次未受影响,`kind`/`kindName` 在那五芯片下恒为 `null`)。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | - | 团期批次 ID |
#### 出参字段表
以下是本次新增/变化的字段;`GroupBatchChipDetailVO` 顶层其余字段(`chipLabel`/`aggregateStatus`/`aggregateStatusName`/`staffList`)与 `items[]` 内未变化的字段(`orderId`/`orderNo`/`teamNo`/`customerName`/`peopleCount`/`needsIt`/`claimerId`/`claimerName`/`claimerSource`/`updateTime`/`staffs`)结构未变,不重复列出。
| 字段 | 类型 | 说明 |
|------|------|------|
| totalCount | Integer | 🔴 语义变化:改前是"计入统计的户数",改后是"条目数"(一户两类需求算两条) |
| doneCount | Integer | 🔴 语义变化:口径随 `totalCount` 同步改为条目数 |
| items[] | List | 🔴 数组长度语义变化:一户两类需求时占两个元素,`orderId` 相同 |
| items[].kind | String,可空 | 新增:用车类别(`TRAVEL` 行程用车 / `TRANSFER` 接送机);该户尚无任何 active 车需求行时为 `null`(此时该户仍占一项,`status` 为 `null`,`statusName` 为"未提交");房/导/摄/约/保五芯片恒为 `null` |
| items[].kindName | String,可空 | 新增:`kind` 配对中文名(行程用车/接送机);`kind` 为 `null` 或枚举外未知码时为 `null`——编码不回落当中文展示 |
| items[].statusName | String,可空 | 文案统一:`PENDING`→待车队配、`PENDING_REVIEW` 按 `kind` 分叉(TRAVEL→待提交车务,TRANSFER→待审核)、`REJECTED_TO_CONSULTANT`→已驳回定制师、`REJECTED_TO_ADMIN`→已驳回管理员、`DONE`→配车完成 |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2104839654727618562/chips/vehicle
```
#### 响应示例
真实实测(团期批次 T26-3963,3 户 5 项,含同一 `orderId` 两类需求各占一项):
```json
{"code":200,"message":"成功","data":{"batchId":"2104839654727618562","groupBatchId":"2104839654727618562","chipLabel":"配车","aggregateStatus":"DOING","aggregateStatusName":"进行中","totalCount":5,"doneCount":3,"staffList":null,"items":[{"orderId":"2104839654652121090","orderNo":"HL20260929154414207","teamNo":"26-2313","contactName":"李文博","customerName":"李文博","peopleCount":2,"status":"DONE","statusText":"配车完成","statusName":"配车完成","needsIt":true,"kind":"TRAVEL","kindName":"行程用车","updateTime":null,"claimerId":null,"claimerName":null,"claimerSource":null,"staffs":null},{"orderId":"2104839654652121090","orderNo":"HL20260929154414207","teamNo":"26-2313","contactName":"李文博","customerName":"李文博","peopleCount":2,"status":"PENDING","statusText":"待车队配","statusName":"待车队配","needsIt":true,"kind":"TRANSFER","kindName":"接送机","updateTime":null,"claimerId":null,"claimerName":null,"claimerSource":null,"staffs":null},{"orderId":"2104839686486888449","orderNo":"HL20260929154421828","teamNo":"26-9436","contactName":"张丽娟","customerName":"张丽娟","peopleCount":2,"status":"DONE","statusText":"配车完成","statusName":"配车完成","needsIt":true,"kind":"TRAVEL","kindName":"行程用车","updateTime":null,"claimerId":null,"claimerName":null,"claimerSource":null,"staffs":null},{"orderId":"2104839729176514562","orderNo":"HL20260929154432061","teamNo":"26-6559","contactName":"那顺","customerName":"那顺","peopleCount":3,"status":"DONE","statusText":"配车完成","statusName":"配车完成","needsIt":true,"kind":"TRAVEL","kindName":"行程用车","updateTime":null,"claimerId":null,"claimerName":null,"claimerSource":null,"staffs":null},{"orderId":"2104839729176514562","orderNo":"HL20260929154432061","teamNo":"26-6559","contactName":"那顺","customerName":"那顺","peopleCount":3,"status":"PENDING_REVIEW","statusText":"待审核","statusName":"待审核","needsIt":true,"kind":"TRANSFER","kindName":"接送机","updateTime":null,"claimerId":null,"claimerName":null,"claimerSource":null,"staffs":null}]},"traceId":null,"success":true}
```
另一团期批次(T26-6001,含"待提交车务"文案分支,TRAVEL 类 `PENDING_REVIEW`)实测节选:
```json
{"orderId":"2104840685708525570","orderNo":"HL20260929154820126","teamNo":"26-0805","contactName":"谢丽萍","customerName":"谢丽萍","peopleCount":2,"status":"PENDING_REVIEW","statusText":"待提交车务","statusName":"待提交车务","needsIt":true,"kind":"TRAVEL","kindName":"行程用车","updateTime":null,"claimerId":null,"claimerName":null,"claimerSource":null,"staffs":null}
```
#### 空数据 / 降级响应
- 团期批次不存在:返回业务错误(见"错误响应"),`data` 为 `null`。
- 团期下暂无子订单:`items` 为空数组,`totalCount`/`doneCount` 均为 `0`。
- 户已声明要用车(`needs_vehicle=true`)但尚未提交任何车需求行:该户仍占一项,`kind`/`status` 为 `null`,`statusName` 为"未提交"(源码逻辑已确认,本轮实测样本未覆盖到该分支)。
#### 错误响应
```json
{"code":589500,"message":"团期不存在","data":null,"traceId":null,"success":false}
```
#### 业务边界
- 🔴 `items[]` 按 `orderId` 去重是错误用法——同一户两类需求会被拆成两个数组元素,去重会随机丢掉其中一类的状态展示。
- `totalCount`/`doneCount` 不再等于该团期的户数,若页面上另有独立的"户数"展示(如团期基础信息),不要复用这两个字段去推导户数。
- `kind`/`kindName` 只在配车芯片有意义,其余五芯片(房/导/摄/约/保)该字段恒为 `null`,前端渲染这五个芯片时不需要处理 `kind` 分支。
- `statusName` 的四个文案改动是全量替换(不是新增枚举值),旧文案(待车务配/驳回给定制师/驳回给管理员)不会再出现,前端若按旧文案字符串做过特殊判断需要同步更新。
### 2. 团期下子订单列表 `GET /v3/admin/order/group-batch/{groupBatchId}/orders`
**VO**: `(无请求体,Path + Query 参数)` → `PageResult<GroupBatchOrderItemRespVO>`
#### 使用场景
团期订单 Tab 展示子订单摘要列表(客户姓名、各状态列、支付/结算信息)时调用,是该 Tab 的主表格数据源。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | - | 团期 ID |
| page | Query | Integer | ❌ | 缺省 1,<1 归一为 1 | 页码,从 1 起 |
| pageSize | Query | Integer | ❌ | 缺省 20,<1 归一为 20,>200 截断为 200 | 每页条数 |
| includeTravelers | Query | Boolean | ❌ | 缺省 true | 是否附出行人明细(证件号/手机号一律不返回),本次未变 |
| includeNeeds | Query | Boolean | ❌ | 缺省 true | 是否附房数/房型/特殊需求,本次未变 |
| includeCancelled | Query | Boolean | ❌ | 缺省 false | 是否含已取消子订单,本次未变 |
#### 出参字段表
以下只列本次新增/变化的字段;`GroupBatchOrderItemRespVO` 其余既有字段(`orderId`/`customerName`/各状态码与状态中文名/金额字段/`travelers` 等)结构未变,不重复列出。
| 字段 | 类型 | 说明 |
|------|------|------|
| vehicleRequirementKind | String,可空 | 新增:本行车需求的用车类别(`TRAVEL`/`TRANSFER`)。**当前恒为 `TRAVEL`**——子订单列表每户只占一行,装不下两类,上游按 TRAVEL 过滤取行;接送机需求要看配车芯片明细(`.../chips/vehicle`,一户两类各占一项)。无 active 车需求行时为 `null`,与 `vehicleRequirementStatus` 同生同灭 |
| vehicleRequirementKindName | String,可空 | 新增:`vehicleRequirementKind` 配对中文名(行程用车/接送机);枚举外未知码不回落编码,直接给 `null` |
| vehicleRequirementStatusName | String,可空 | 既有字段,文案口径调整:`PENDING_REVIEW` 的措辞随同行的 `vehicleRequirementKind` 分叉——TRAVEL 下发"待提交车务"(该状态上没有逐户审核动作,推走它的是团期管理员整团一次的"提交车务"),TRANSFER 下发"待审核";由于本字段随行的 `vehicleRequirementKind` 当前恒为 TRAVEL,本端点实际只会出现"待提交车务"这一支 |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2104839654727618562/orders?page=1&pageSize=20
```
#### 响应示例
真实实测(团期批次 T26-3963,节选第 1 条完整记录):
```json
{"code":200,"message":"成功","data":{"records":[{"orderId":"2104839654652121090","orderNo":"HL20260929154414207","teamNo":"26-2313","customerName":"李文博","participantCount":2,"orderStatus":"CUSTOMIZING","orderStatusName":"定制中","flowStatus":"RESOURCE_PREPARING","flowStatusName":"资源准备","reviewStatus":null,"reviewStatusName":null,"settlementStatus":"NONE","settlementStatusName":"未结算","payStatus":"FULLY_PAID","payStatusName":"已付全款","contractStatus":null,"contractStatusName":null,"insuranceStatus":null,"insuranceStatusName":null,"paidAmount":"7360.00","balanceAmount":"0.00","hotelRequirementStatus":null,"hotelRequirementStatusName":null,"vehicleRequirementStatus":"DONE","vehicleRequirementStatusName":"配车完成","vehicleRequirementKind":"TRAVEL","vehicleRequirementKindName":"行程用车","consultantName":"cw_test_7443","totalPrice":"7360.00","tierCode":"2A","tierName":"2成人","travelerInfoComplete":true,"roomCount":1,"roomType":null,"roomTypeName":null,"specialNeeds":"夫妻同行,携带摄影器材较多,需预留后备箱空间","contactPhone":"138****3046","groupChatUnreadCount":0,"travelers":[{"name":"李文博","type":"ADULT","age":46,"birthdayInTrip":false},{"name":"赵梦琪","type":"ADULT","age":43,"birthdayInTrip":false}]}],"total":3,"page":1,"pageSize":20},"traceId":null,"success":true}
```
#### 空数据 / 降级响应
- 团期批次不存在:返回业务错误(见"错误响应")。
- 团期下暂无子订单(或 `includeCancelled=false` 时全部已取消):`records` 为空数组,`total=0`。
- 该户无 active 车需求行:`vehicleRequirementStatus`/`vehicleRequirementKind` 均为 `null`;`vehicleRequirementStatusName`/`vehicleRequirementKindName` 按该户 `needsVehicle` 分叉——`true` 出"未提交",否则为 `null`。
#### 错误响应
```json
{"code":589500,"message":"团期不存在","data":null,"traceId":null,"success":false}
```
#### 业务边界
- `vehicleRequirementKind` 在本端点当前恒为 `TRAVEL`,不能据此推断"该团期没有接送机需求"——接送机需求存在与否要看配车芯片明细端点。
- `vehicleRequirementStatusName` 的 `PENDING_REVIEW` 分支文案取决于 `vehicleRequirementKind`,但本端点该列恒为 TRAVEL,因此实际只会看到"待提交车务",不会看到"待审核"(后者只出现在配车芯片明细的 TRANSFER 项上)。
- 无 active 车需求行时 `vehicleRequirementStatus` 为 `null` 不回落 `PENDING`(#8249 起既有行为,本次未变)——`PENDING` 是需求行的真实状态之一,没有行时借用它会与"未提交"矛盾。
---
## 四、契约约束与正确调用方式
> 本节只写后端接受/拒绝的规则与语义边界,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload / 响应 |
|------|-----------------|
| ✅ 渲染配车芯片明细列表 | 按数组下标或 `orderId`+`kind` 复合键渲染每一项,允许同一 `orderId` 出现多行 |
| ✅ 展示配车芯片"已完成 N/M" | 直接用 `doneCount`/`totalCount`,两者已经是同一口径(条目数),无需自行按户去重再算 |
| ❌ 用 `items[].orderId` 做 `Map` 的 key 或做 `Set` 去重 | 一户两类需求时后写入的会覆盖/顶掉先写入的那一条,界面上会静默丢失一类需求的状态 |
| ❌ 用子订单列表的 `vehicleRequirementKind` 判断该团期是否存在接送机需求 | 该列当前恒为 TRAVEL,对接送机需求零分辨力;接送机需求判断要调配车芯片明细 |
### 切换状态时的必要动作
前端如果此前在配车芯片渲染层用 `orderId` 做过 `key`/去重/索引,本次上线前必须改为 `orderId + kind` 复合键;否则界面在两类需求并存的户上会稳定丢失一类需求的展示,且是静默丢失(不报错)。
---
## 五、数据库行为
两个端点均为只读查询,无数据库写操作。配车芯片明细与子订单列表内部均从 `order_vehicle_requirement`(车需求行表)按 `order_id` 聚合取最新一版需求行,`kind` 直接取自需求行的 `requirement_kind` 列,不做兜底改写;历史行 `requirement_kind` 为 `NULL` 时 `kind`/`kindName` 原样透出 `null`,不会被兜底成 `TRAVEL`。
---
## 六、边界行为
- 户尚未提交任何车需求行、但已声明要用车(`needs_vehicle=true`)→ 配车芯片该户仍占一项,`kind`/`status` 为 `null`,`statusName`="未提交"
- 户完全不需要用车(`needs_vehicle=false`)→ 该户在配车芯片 `items[]` 中不出现
- 户同时有 TRAVEL 与 TRANSFER 两类 active 需求行 → 配车芯片占两项,`orderId` 相同、`kind` 不同
- 户只有一类需求行 → 配车芯片只占一项,不会补一个空的另一类占位项
- 需求行历史 `requirement_kind` 为 `NULL` → `kind`/`kindName` 原样为 `null`,不兜底为 `TRAVEL`
- 子订单列表 `.../orders` 每户恒只出一行、按 TRAVEL 过滤取需求行,不受该户是否有 TRANSFER 需求影响
---
## 六.5、枚举 / 数据字典
### 用车类别(`VehicleRequirementKind`,`items[].kind` / `vehicleRequirementKind`)
**所属字段**: `GroupBatchChipItemRespVO.kind`、`GroupBatchOrderItemRespVO.vehicleRequirementKind` | **类型**: `String`
| 值 | 中文 | 本次是否新增 | 说明 |
|----|------|------|------|
| `TRAVEL` | 行程用车 | 既有枚举值,本次新增到这两个字段 | 团期行程内的用车安排 |
| `TRANSFER` | 接送机 | 既有枚举值,本次新增到这两个字段 | 接送机场/车站,独立于行程用车 |
| `null` | (无展示) | - | 该户尚无 active 车需求行时的状态,不是第三个枚举值 |
### 车需求状态中文名(`statusName` / `vehicleRequirementStatusName`,`PENDING_REVIEW` 按 `kind` 分叉)
**所属字段**: `items[].statusName`(配车芯片)、`vehicleRequirementStatusName`(子订单列表) | **类型**: `String`
| 状态码 | 中文(本次前) | 中文(本次后) | 说明 |
|--------|----------------|----------------|------|
| `PENDING` | 待车务配(仅配车芯片明细,子订单列表原已是"待车队配") | 待车队配 | 两处读口统一为同一文案 |
| `PROCESSING` | 配车中 | 配车中 | 未变 |
| `DONE` | 配车完成 | 配车完成 | 未变 |
| `PENDING_REVIEW`(TRAVEL) | 待审核(配车芯片明细此前不分类别) | 待提交车务 | 按 `kind` 新分叉 |
| `PENDING_REVIEW`(TRANSFER) | 待审核 | 待审核 | 未变(新分叉后仍是这个文案) |
| `REJECTED_TO_CONSULTANT` | 驳回给定制师(仅配车芯片明细) | 已驳回定制师 | 两处读口统一为同一文案 |
| `REJECTED_TO_ADMIN` | 驳回给管理员(仅配车芯片明细) | 已驳回管理员 | 两处读口统一为同一文案 |
| `null`(有需求但未提交) | 待审核(配车芯片明细此前误落到这一支) | 未提交 | 修正误报——"未提交"与"已提交等审核"是两个不同状态,此前混在一起 |
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `GroupBatchChipItemRespVO.kind` | 不存在 | 新增,`String`,可空,仅配车芯片有值,其余五芯片恒 `null` |
| `GroupBatchChipItemRespVO.kindName` | 不存在 | 新增,`String`,可空 |
| `GroupBatchChipDetailVO.totalCount`(配车芯片) | 户数 | 条目数(一户两类算两条) |
| `GroupBatchChipDetailVO.doneCount`(配车芯片) | 已完成户数 | 已完成条目数 |
| `GroupBatchChipDetailVO.items[]`(配车芯片) | 一户一项 | 一户每类一项,`orderId` 可重复 |
| `GroupBatchOrderItemRespVO.vehicleRequirementKind` | 不存在 | 新增,`String`,可空,当前恒为 `TRAVEL` |
| `GroupBatchOrderItemRespVO.vehicleRequirementKindName` | 不存在 | 新增,`String`,可空 |
| 团期列表/看板 `chipStats.vehicle.total`/`.done` | 户数 | 条目数(共用配车芯片同一聚合算法,联动变化,字段本身未新增) |
### 行为级对比
| 场景 | 改前 | 改后 |
|------|------|------|
| 户同时有 TRAVEL + TRANSFER 需求 | 配车芯片只显示其中一类,另一类无声消失 | 两类各占一项,均可见 |
| 户已声明用车但未提交需求行 | 落到"待审核",看起来像已提交 | 落到"未提交",与已提交待审核区分开 |
| `PENDING_REVIEW` 状态文案 | 配车芯片明细恒显示"待审核";子订单列表已按 kind 分叉(源于 #8218) | 配车芯片明细与子订单列表口径统一,均按 kind 分叉 |
| 配车芯片 `PENDING`/`REJECTED_TO_CONSULTANT`/`REJECTED_TO_ADMIN` 文案 | 待车务配/驳回给定制师/驳回给管理员 | 待车队配/已驳回定制师/已驳回管理员 |
| 配车芯片 `totalCount`/`doneCount` 与列表页 `chipStats.vehicle` 关系 | 两处各自独立计算,可能不一致 | 共用同一聚合算法,逐位一致 |
## 六.7、影响评估
- **是否破坏向后兼容**: 是——`.../chips/vehicle` 的 `items[]` 数组长度与 `orderId` 唯一性假设改变,任何按 `orderId` 做 key/去重/索引的前端代码都会在两类需求并存的户上产生数据丢失;`totalCount`/`doneCount` 数值口径也变了,若前端拿它们除以户数算百分比会得到错误结果。
- **前端是否必须同步上线**: 是(针对配车芯片展示场景)——只要页面渲染配车芯片明细,就必须按 `orderId+kind` 复合键处理 `items[]`;子订单列表的两个新字段是纯新增,不改也不会报错,但不展示就拿不到用车类别信息。
- **前端 workaround 清理点**: 若此前为"同一户两类需求显示不全/互相覆盖"这类现象写过特殊兼容或只取第一条的逻辑,现在后端已按类别拆项,可以确认不再需要。
---
## 七、不影响范围
- **仅影响**: 配车芯片明细端点 `GET .../chips/vehicle` 的 `items[]` 结构与 `totalCount`/`doneCount` 语义;子订单列表端点 `GET .../orders` 新增两个字段;团期列表/看板端点 `chipStats.vehicle` 的计数口径(字段本身未变)。
- **零影响**:
- 房/导/摄/约/保五个芯片明细端点(`GET .../chips/{hotel|guide|photo|contract|insurance}`)的字段结构与计数口径
- 团期下子订单列表其余既有字段(金额、支付、结算、房需求等)
- 户级用车需求明细端点 `GET .../requirement/vehicle-households`(本次为纯内部代码去重,零响应契约变化,见下方"八、测试环境已验证"说明)
- 配车需求本身的写口(提交/确认/驳回等)不受本次改动影响
---
## 八、测试环境已验证
服务:`hl-order-service-v3`,dev-v3 分支部署测试网关 @ `99fb369ba`(含 #8543 所在提交 `bc80606ac2`),测试网关 `https://api.test.1814.love`;样本均为测试服现存真实业务数据:
```
✓ 团期批次 T26-3963(groupBatchId=2104839654727618562,3 户):
GET .../chips/vehicle → totalCount=5 doneCount=3
李文博(orderId=2104839654652121090):TRAVEL/DONE/配车完成 + TRANSFER/PENDING/待车队配(同一 orderId 两项)
那顺(orderId=2104839729176514562):TRAVEL/DONE/配车完成 + TRANSFER/PENDING_REVIEW/待审核(同一 orderId 两项)
张丽娟(orderId=2104839686486888449):仅 TRAVEL/DONE/配车完成(一项)
GET .../orders → 3 条记录,vehicleRequirementKind 均为 "TRAVEL"(与源码"本列当前恒为 TRAVEL"一致)
✓ 团期批次 T26-6001(groupBatchId=2104840641651556353,3 户):
GET .../chips/vehicle → totalCount=3 doneCount=1
董海涛:TRAVEL/DONE/配车完成 + TRANSFER/PENDING/待车队配
谢丽萍:TRAVEL/PENDING_REVIEW/待提交车务(复现"待提交车务"文案分支)
✓ 团期列表页 GET /v3/admin/order/group-batch 逐页扫描,同两个 groupBatchId 的 chipStats.vehicle:
{total:5, done:3} 与 {total:3, done:1},与明细端点 totalCount/doneCount 逐位一致(重复请求 5 次读数稳定)
✓ 错误响应:不存在的 groupBatchId → 两端点均返回 {"code":589500,"message":"团期不存在"}
```
注:`kind=null`(户已声明用车但未提交需求行,`statusName`="未提交")与配车芯片明细的 `REJECTED_TO_CONSULTANT`/`REJECTED_TO_ADMIN` 两个文案分支,本轮实测样本团期未覆盖到(样本户均已提交需求行且未被驳回);这三个分支的契约已在源码逐一核实(`GroupBatchChipResolver.vehicleItemsOf`、`GroupBatchConverter.resolveRequirementStatusName`),前端应按此契约实现,不依赖本轮是否观测到该取值。
---
## 十、相关文档
- 关联 Issue: [wx/HL#8543](https://git.1814.love/wx/HL/issues/8543)
- 关联 PR: [wx/HL#8592](https://git.1814.love/wx/HL/pulls/8592)
## 关联 / 联系人
### 链接
- **Issue**: [#8543](https://git.1814.love/wx/HL/issues/8543)
- **PR**: [#8592](https://git.1814.love/wx/HL/pulls/8592)
- **Merge commit**: [bc80606ac2](https://git.1814.love/wx/HL/commit/bc80606ac2)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,664 @@
---
schema: "hl-changelog/v2"
ticket: "8544"
title: "团期正式用车需求读口补状态中文名、汇总草稿改用读侧 VO、需求汇总并列下发团级已确认座位、子订单列表逐类下发用车需求行(#8544 #8545 #8619)"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "bc9c13aaa39bab96e132a3a42515c821fa74a269"
target_release: "v2.1"
verified_at: "2026-10-01"
status_note: "PR #8625(覆盖 #8544 #8545)与 PR #8624(覆盖 #8619)均已 squash 合并 dev-v3(5f2f86bc9 / ce7cd238e9),hl-order-service-v3 dev-v3 分支已滚测试服(HEAD 5f2f86bc9,jar mtime 2026-09-30 08:19:48,两实例 Nacos 健康)。四个 GET 端点响应体的新增/变更字段均实测通过;PUT / withdraw / waive 三个写端点与 GET 共用同一个 GroupVehicleRequirementRespVO 类(源码级共用,非各自派生),因此 #8544 新增的 4 个 *Name 字段在这三个端点的响应里同样存在,字段语义与本文档「三、接口详情」第 2 条完全一致。;前端已交付:正式用车需求三处(Section/汇总卡/状态条)删本地 code→中文 map 改读后端 statusName/planRefreshStalledReasonName(原码仅防空白兜底),aggregate-draft 灌表单用 vehicleType key 零改动,62 例定向测试全绿(hl-admin 8926ebff);续:#8545 座位两口径并列行(已报 vs 已定,集合外车型 totalSeats=0 行显逐户未报)与 #8619 逐类清单段也已交付(hl-admin 850abddc/bc9c13aa,frontend_ref 更新为最终哈希)"
updated_at: "2026-09-30"
base: "dev-v3"
---
# hl-order-service-v3: 团期正式用车需求读口补状态中文名、汇总草稿改用读侧 VO、需求汇总并列下发团级已确认座位、子订单列表逐类下发用车需求行
> **存放目录**: `changelogs-v2/2026-09/`
> **服务**: hl-order-service-v3 (端口 8086)
> **PR**: #8625(#8544 #8545)、#8624(#8619)
> **Issue**: #8544、#8545、#8619
> **日期**: 2026-09-30
> **影响范围**: 团期需求管理 Tab 下 4 个 GET 端点(全团需求汇总 / 读正式用车需求 / 自动汇总草稿 / 子订单列表)的响应体字段;PUT / withdraw / waive 三个写端点共用的响应体同步补齐字段(契约见「四」,未单开详情块)
---
## ⚠️ 关键变化
- **#8544**:`GroupVehicleRequirementRespVO`(PUT / GET / withdraw / waive 四端点共用的响应体)新增 4 个只读中文名字段——`statusName` / `planRefreshStateName` / `blockedStageName` / `planRefreshStalledReasonName`,分别配对既有的 `status` / `planRefreshState` / `blockedStage` / `planRefreshStalledReason`。**认不出的编码一律给 `null`,不回落成编码原文**(与本 VO 既有的 `SpecialTagItem.name` / `GroupItem.vehicleTypeName` 同口径)。
- 🔴 **这条 null 规则与看板列表的团期状态中文名口径是两套、刻意不同**:`GroupBatchConverter#resolveBatchStatusName`(团期列表页用)未知码会回落原编码;本次这 4 个字段所在的正式用车需求读口未知码给 `null`。前端不要把两处的「未知码兜底逻辑」当成同一套抄。
- **#8544**:`GET .../vehicle-requirement/aggregate-draft` 的 `draft` 字段类型从写侧 `GroupVehicleRequirementSaveReqVO` 改为新的只读读侧类型 `GroupVehicleRequirementDraftRespVO`。JSON 形状基本不变(由 `GroupVehicleRequirementDraftRespVOFieldParityTest` 钉住与写侧字段集一致),**唯一新增字段是 `groups[].vehicleTypeName`**(只读车型中文名,不参与保存,原样 `PUT` 回去时被忽略);该 VO **不挂任何校验注解**(`@NotNull`/`@NotEmpty`/`@Size` 等),因为草稿可能违反若干条保存态约束,违规项另在 `violations` 里列出。
- **#8544**:`draft.groups[].remark`(分组备注拼接文案)的逐户标签优先级改为 **团号(客户名)→ 团号 → 客户名 → 订单号 → "未知户"**,**不再回落雪花订单 ID**(原来兜底到裸数字 orderId 的情况,现在只有当团号/客户名/订单号三者都拿不到时才会出现,落成字面文案"未知户")。前端如果对这段 `remark` 文本做过正则匹配、高亮、或按"看起来像一串数字"识别订单号,需要同步更新——这段文本此后不会再出现裸数字订单 ID。
- **#8545**:`GET .../requirement-summary` 的 `vehicleSeatSummary[]` 新增 `confirmedSeats` / `confirmedCount`(均为 `int`,**尚未整团确认时恒为 `0`,不是 `null`**)。这是"团级已确认"口径,与既有的 `totalSeats` / `totalCount`("逐户已提交"口径)**并列**下发、允许不相等——两者不等是常态不是缺陷,任何断言两者相等的前端逻辑都是错的。
- 🔴 **#8545**:`vehicleSeatSummary[]` 可能新增一行只有 `confirmedSeats`/`confirmedCount` 非零、`totalSeats`/`totalCount` 恒为 `0` 的车型条目——发生在车务把车型整团定成了逐户报的车型集合之外的某个车型时(例如逐户都报 `mpv`,车务整团定成 `bus`)。前端渲染这张表时不能假设"有座位数就等于有逐户报",要按 4 个数字各自判断。
- **#8619**:`GET .../orders` 的 `vehicleRequirementStatus` / `vehicleRequirementKind` 口径从"只看行程用车(TRAVEL)"放宽为"该户展示序首条活跃用车需求行"(行程用车优先、其次接送机)。🔴 **改前只提交了接送机需求的户,这两列恒为 `null`、`vehicleRequirementStatusName` 被渲染成"未提交"——这是一个错误的展示(该户其实已提交、可能已在审/已派车/已完成),本次已修复**。有行程用车需求行的户这两列读数不变。
- **#8619**:`vehicleRequirementKind` 的**实际取值域**从恒为 `TRAVEL` 放宽为 `{TRAVEL, TRANSFER}`——`VehicleRequirementKind` 枚举本身没有新增第三个值,仍然只有这两个;变的是这一个响应字段过去被上游按 TRAVEL 过滤取行、现在按展示序取行,所以能观测到 `TRANSFER`。前端如果曾按"这一列永远是 TRAVEL"写死过图标/文案分支,需要按实际值渲染。
- **#8619**:`orders` 新增 `vehicleRequirements[]`(该户全部活跃用车需求行,**恒非 `null`,0~2 条**,行程用车在前、接送机在后)。一户同时提交了行程用车与接送机时,两条需求行状态可能各自不同(例如 TRAVEL 已 `DONE`、TRANSFER 还在 `PENDING_REVIEW`),此时上面那组单值字段(`vehicleRequirementStatus`/`vehicleRequirementKind`)**只能表达其中一条**——按类别判断状态必须读这个数组,不要只读单值字段。
- `orders` 端点其余 25 个既有字段(`hotelRequirementStatus`、`totalPrice`、`travelers` 等)本次未变,已由 changelog `30_8543_团期订单Tab用车状态按需求类别拆分并统一文案-修改接口-管理后台.md` 完整记录,此处不重复。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 全团需求汇总 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement-summary` | 修改 | `vehicleSeatSummary[]` 新增 `confirmedSeats`/`confirmedCount`(#8545) |
| 2 | 读团期正式用车需求 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 修改 | 响应体新增 4 个 `*Name` 字段(#8544),PUT/withdraw/waive 共用同一响应体同步生效 |
| 3 | 自动汇总正式用车需求草稿 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` | 修改 | `draft` 字段改用只读读侧 VO,新增 `groups[].vehicleTypeName`(#8544) |
| 4 | 团期下子订单列表 | GET | `/v3/admin/order/group-batch/{groupBatchId}/orders` | 修改 | `vehicleRequirementKind` 取值域放宽、新增 `vehicleRequirements[]`(#8619) |
---
## 三、接口详情
### 1. 全团需求汇总 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement-summary`
**VO**: `GroupRequirementSummaryRespVO`
#### 使用场景
团期需求管理 Tab 打开时拉取的"全团需求汇总"卡片,展示逐日房间合计与大巴座位合计。本次变更只影响座位合计部分——`vehicleSeatSummary[]` 新增团级已确认口径的两个字段,供页面并列展示"定制师报了多少座"与"车务最后定了多少座"。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | 是 | - | 团期聚合主键 |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| activeOrderCount | int | 在团子订单数(仅排除 CANCELLED,含 COMPLETED),未变 |
| hotelRequirementCount | int | 有 active 用房需求记录的子订单数(兼容保留字段),未变 |
| hotelNeededOrderCount | int | 需要订房的户数,未变 |
| hotelFlagMismatchOrderCount | int | needsHotel≠true 却已提交有效用房需求的户数,未变 |
| hotelSubmittedOrderCount | int | 已提交有效用房需求且计入 dailyRoomBreakdown 的户数,未变 |
| vehicleRequirementCount | int | 已提交用车需求的子订单数,未变 |
| dailyRoomBreakdown | array | 逐日房间汇总,本次未变 |
| vehicleSeatSummary | array | 大巴座位汇总(按车型),本次新增字段见下表 |
| orderSpecialTags | array | 各子订单 specialTags,本次未变 |
| transferSummary | object | 接送机汇总(#8151 既有字段),本次未变 |
`vehicleSeatSummary[]` 单项字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| vehicleType | string | 车型大类编码(suv/mpv/bus/sedan;缺失时为"未知"),未变 |
| vehicleTypeName | string | 车型大类中文名;查不到或车队服务不可用时为 `null`,未变 |
| totalSeats | int | 合计座位数(seats × count 之和);统计基数为"逐户已提交"的行程用车需求,未变 |
| totalCount | int | 合计车辆台数;统计基数为"逐户已提交"的行程用车需求,未变 |
| confirmedSeats 🆕 | int | 合计座位数(团级已确认的正式需求口径);尚未整团确认时为 `0`,与 totalSeats 不等是常态(#8545) |
| confirmedCount 🆕 | int | 合计车辆台数(团级已确认的正式需求口径);尚未整团确认时为 `0`(#8545) |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2104839654727618562/requirement-summary
```
#### 响应示例
以下为测试环境实测响应(团期批次 `2104840641651556353`,2026-09-30 采集;仅展示 `vehicleSeatSummary[0]`,其余字段结构未变不重复列出):
```json
{
"code": 200,
"message": "成功",
"data": {
"vehicleSeatSummary": [
{
"vehicleType": "mpv",
"vehicleTypeName": "商务车",
"totalSeats": 14,
"totalCount": 2,
"confirmedSeats": 7,
"confirmedCount": 1
}
]
},
"traceId": null,
"success": true
}
```
#### 空数据 / 降级响应
- 团期批次不存在:返回业务错误(见"错误响应"),`data` 为 `null`。
- 全团无任何用车需求:`vehicleSeatSummary` 为空数组,不是 `null`。
- 团级从未整团确认过(或已被重开成 `PENDING_RECONFIRM`):既有行的 `confirmedSeats`/`confirmedCount` 均为 `0`,不追加新行;此时该数组与改动前逐户口径的行完全一致。
- 车务整团定的车型不在逐户已提交的车型集合内:追加一行 `totalSeats=0`/`totalCount=0`、`confirmedSeats`/`confirmedCount` 非零的记录,`vehicleTypeName` 仍会被正常补全。
#### 错误响应
```json
{"code": 589500, "message": "团期不存在", "success": false, "data": null}
```
```json
{"code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "success": false, "data": null}
```
#### 业务边界
- `confirmedSeats`/`confirmedCount` 与 `totalSeats`/`totalCount` 是两个独立基数,前端不得假设两者相等或用其中一个推算另一个。
- 权限码为 `group-batch:view`。
---
### 2. 读团期正式用车需求 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement`
**VO**: `GroupVehicleRequirementRespVO`
#### 使用场景
团期正式用车需求编辑页/详情页打开时的回填读口,同时是"配车计划刷新是否死了"在 admin 侧唯一无副作用的观测口(#7988)。`PUT`(保存)、`withdraw`(撤回)、`waive`(免车)三个写端点返回的响应体与本端点完全一致(同一个 Java 类),前端可以对四个端点复用同一套渲染逻辑。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | 是 | - | 团期聚合主键 |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| requirementId | string(Long 转字符串) | 正式需求主键,未变 |
| groupBatchId | string(Long 转字符串) | 团期聚合主键,未变 |
| status | string | 状态:DRAFT/CONFIRMED/DISPATCHED/DONE/PENDING_RECONFIRM/CANCELLED,未变 |
| statusName 🆕 | string | 状态中文名(草稿/已确认/已发车务/配车完成/待重新确认/已取消);status 为 null 或认不出的编码时为 `null`,不回落编码原文(#8544) |
| version | int | 乐观锁版本号,未变 |
| remark | string | 整份备注;撤回/免车会把操作追加进来(不覆盖原备注),超 500 字**从头部截**并以 `…` 开头,未变 |
| confirmedBy | string | 整份确认人,DRAFT 时为 null,未变 |
| confirmedAt | string(LocalDateTime) | 整份确认时间,DRAFT 时为 null,未变 |
| planRefreshState | string | 配车刷新状态原值:null=从未登记过刷新(多数团期正常态)/PENDING/DONE/FAILED,未变 |
| planRefreshStateName 🆕 | string | 配车刷新状态中文名(刷新中/刷新完成/刷新失败);为 null 或认不出的编码时为 `null`(#8544) |
| planRefreshReplayCount | int | 人工受控重投累计次数(管理员点确认触发,不含自动重试),上限 5,未变 |
| blockedStage | string | 团期阻断阶段快照:null=未阻断;非空为受控重开发生那一刻的团期状态码(GroupBatchStatus 全域,常见 RESOURCE_PREPARING/MATERIAL_PREPARING/PENDING_DEPARTURE),未变 |
| blockedStageName 🆕 | string | 阻断阶段中文名(如 资源准备中/物料准备中/待出发);为 null 或认不出的编码时为 `null`(#8544) |
| planRefreshStalled | boolean | 刷新是否已停滞、不会自愈(恒非 null);true=必须有人处置,未变 |
| planRefreshStalledReason | string | 停滞归因:STATE_FAILED/COMMAND_FAILED/TIMEOUT;未停滞时为 null,未变 |
| planRefreshStalledReasonName 🆕 | string | 停滞归因中文名(刷新已被判死/刷新命令已失败/刷新超时);未停滞或认不出编码时为 `null`(#8544) |
| planRefreshTimeoutAt | string(LocalDateTime) | 本轮刷新超时时刻;仅 PENDING 且已登记发起时刻时有值,未变 |
| planRefreshReplayExhausted | boolean | 人工重投额度是否已耗尽(恒非 null),未变 |
| groups | array | 全部乘车分组(整团免车态为空数组),结构未变,见下表 |
| exemptHouseholds | array | 仅保存草稿(PUT)响应填充,其余端点(含本 GET)恒为 `null`,未变 |
`groups[]` 单项字段(未变,随 VO 一起下发本次新增的 4 个 `*Name` 兄弟字段):
| 字段 | 类型 | 说明 |
|------|------|------|
| groupId | string(Long) | 分组主键 |
| groupCode | string | 分组键,直接作为车费 alloc_group |
| vehicleType | string | 车型文本/字典值 |
| vehicleTypeName | string | 车型中文名(既有字段,非本次新增) |
| serviceStartDate / serviceEndDate | string(LocalDate) | 本组服务起止日 |
| seats / count | int | 该组单车座位数/车辆数量;存量分组为 null 表示待填 |
| specialTags | array | 该组特殊诉求标签(code + name) |
| remark | string | 该组备注/其他诉求;存量分组为 null |
| totalSeatCount | int | 总座位数 = seats × count |
| maxHeadcount | int | 该组 days 里的最大用车人数 |
| remainingPassengerSeats | int | 余座(扣司机位后的可乘座位 − 最大用车人数) |
| days | array | 逐日用车人数与成员 |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2104839654727618562/vehicle-requirement
```
#### 响应示例
以下为测试环境实测响应(团期批次 `2104840641651556353`,2026-09-30 采集)。`planRefreshState` / `blockedStage` / `planRefreshStalledReason` 三列在库中为 `NULL`,对应 `*Name` 实测为 `null`:
```json
{
"code": 200,
"message": "成功",
"data": {
"status": "DONE",
"statusName": "配车完成",
"planRefreshState": null,
"planRefreshStateName": null,
"blockedStage": null,
"blockedStageName": null,
"planRefreshStalledReason": null,
"planRefreshStalledReasonName": null
},
"traceId": null,
"success": true
}
```
#### 空数据 / 降级响应
- 团期尚未形成正式需求(从未 PUT 过):`data` 为 `null`(源码 `@ApiOperation` 明确标注"未形成时返回 null"),不是报错。
- `planRefreshState`/`blockedStage`/`planRefreshStalledReason` 三列 DB 为 `NULL`(从未走过受控重开,绝大多数团期的正常态):对应的 3 个 `*Name` 字段一律为 `null`,不下发默认中文名。
- `groups` 为空数组场景:整团免车态(waive 后)。
#### 错误响应
```json
{"code": 589500, "message": "团期不存在", "success": false, "data": null}
```
```json
{"code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "success": false, "data": null}
```
#### 业务边界
- 4 个 `*Name` 字段是**只读派生展示字段**,不接受回传;`PUT` 请求体仍用原编码字段(`GroupVehicleRequirementSaveReqVO` 未新增字段)。
- 认不出的编码 → 对应 `*Name` 为 `null`,**不回落原编码**——这与团期列表页 `GroupBatchConverter#resolveBatchStatusName`(未知码回落原码)是刻意不同的两套口径,不要混用同一套前端兜底逻辑。
- 本端点权限码为 `group-batch:demand:confirm`,与 PUT/withdraw/waive/aggregate-draft 四个端点同码。
- 本端点后续不会引入任何写操作(#7988 明确约束),可放心作为无副作用轮询口使用。
---
### 3. 自动汇总正式用车需求草稿 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft`
**VO**: `GroupVehicleAggregateDraftRespVO`(外层诊断字段未变;`draft` 字段类型改为 `GroupVehicleRequirementDraftRespVO`)
#### 使用场景
团期正式用车需求编辑弹窗首次打开、或点"重新汇总"时调用:按各子订单已提交的活跃行程用车需求自动生成一份分组草稿,`draft` 可原样 `PUT` 回 `/vehicle-requirement` 保存。本次变更只影响 `draft` 内部的类型与新增字段,外层的 `droppedFleetItems`/`staleHeadcountOrders`/`paddedOrderDays`/`seatOptionAdjusted`/`violations`/`exemptHouseholds` 六个诊断字段结构未变。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | 是 | - | 团期聚合主键 |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| draft | object 🆕类型变更 | 汇总草稿,类型由写侧 `GroupVehicleRequirementSaveReqVO` 改为只读读侧 `GroupVehicleRequirementDraftRespVO`,见下表 |
| droppedFleetItems | array | 多车型户被丢弃的车型,未变 |
| staleHeadcountOrders | array | 人数已过期的户,未变 |
| paddedOrderDays | array | 为覆盖出发~返回而补进的日期,未变 |
| seatOptionAdjusted | array | 座位被兜底调整到车型可选档位的户,未变 |
| violations | array | 草稿违反保存态校验的逐条诊断,未变 |
| exemptHouseholds | array | 提交不了的豁免户(订单不在定制中/团期冻结且未被打回),未变 |
`draft`(`GroupVehicleRequirementDraftRespVO`)字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| version | int | 乐观锁版本号;原样回传给 PUT;尚未形成正式需求时为 `null` |
| remark | string | 整份需求备注,汇总草稿恒为 `null` |
| groups | array | 全部乘车分组,恒非 null,无可汇总内容时为空数组;见下表 |
`draft.groups[]` 字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| groupId | string(Long) | 既有分组主键,汇总草稿恒为 `null` |
| groupCode | string | 分组键,直接作为车费 alloc_group |
| vehicleType | string | 车型大类编码(归一后的 key) |
| vehicleTypeName 🆕 | string | 车型大类中文名(只读,不参与保存);字典查不到时为 `null`,不回落编码原文(#8544,本类相对写侧唯一新增字段) |
| serviceStartDate / serviceEndDate | string(LocalDate) | 本组服务起止日 |
| seats / count | int | 该组单车座位数(存量/无档位可取时可能为 null)/车辆数量 |
| specialTags | array(string) | 该组特殊诉求标签编码数组 |
| remark | string | 该组备注;汇总时按"团号(客户名): 备注"拼接,超 500 字**从尾部截断** |
| days | array | 逐日用车人数与成员,见下表 |
`draft.groups[].days[]` 字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| tripDate | string(LocalDate) | 团期行程日 |
| headcount | int | 该组该日用车人数(乘车人数,非户数) |
| memberOrderIds | array(string) | 该组该日实际乘车的子订单集合 |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2104839654727618562/vehicle-requirement/aggregate-draft
```
#### 响应示例
以下为测试环境实测响应(团期批次 `2104840641651556353`,2026-09-30 采集;`days` 实测为 7 天逐日行,此处只保留首日,其余日同形):
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2104840641651556353",
"currentStatus": "DONE",
"draft": {
"version": 3,
"remark": null,
"groups": [
{
"groupId": null,
"groupCode": "MPV",
"vehicleType": "mpv",
"vehicleTypeName": "商务车",
"serviceStartDate": "2026-11-11",
"serviceEndDate": "2026-11-17",
"seats": 7,
"count": 1,
"specialTags": [],
"remark": "26-3682(董海涛): 结伴出行客户,整团统一 7 座商务车,已与客户确认路线;26-0805(谢丽萍): 9月30日至10月3日行程用车,成人4人其中1位长者,行李较多需大后备箱",
"days": [
{
"tripDate": "2026-11-11",
"headcount": 4,
"memberOrderIds": ["2104840641597030402", "2104840685708525570"],
"memberOrderCount": 2
}
]
}
]
},
"violations": []
},
"traceId": null,
"success": true
}
```
`groups[].remark` 的一户标识格式是**「团号(客户名)」**(`26-3682(董海涛)`),多户合并进同一车型组时以 `;` 连接。本次改动前该位置拼的是 19 位订单 ID,因此:**已确认落库的存量团期,其 `GET .../vehicle-requirement` 返回的 `groups[].remark` 仍是旧格式(`HL2026…` 订单号前缀)**——本单只改新生成的汇总草稿,不回写历史数据。前端不要按固定格式解析 `remark`,它是给运营看的自由文本。
#### 空数据 / 降级响应
- 团期批次不存在:返回业务错误(见"错误响应")。
- 全团无可汇总的行程用车需求:`draft.groups` 为空数组。
- 车型字典不可用:`vehicleTypeName` 降级为 `null`,`groups` 其余字段照常下发(不阻断整个响应)。
#### 错误响应
```json
{"code": 589500, "message": "团期不存在", "success": false, "data": null}
```
```json
{"code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "success": false, "data": null}
```
```json
{"code": 809120, "message": "车队车型字典暂不可用,无法校验车型,请稍后重试", "success": false, "data": null}
```
有在团需车户缺少可汇总用车需求时(809121,触发条件已被 #8577 收窄——只提交了接送机的户不再算缺少):
```json
{"code": 809121, "message": "团期 「第3期 10月8日出发团」 有 1 户缺少可汇总的用车需求,暂不能自动汇总:26-0480:未提交用车需求", "success": false, "data": null}
```
#### 业务边界
- `draft` 是**只读展示体**,不挂任何 `@NotNull`/`@NotEmpty`/`@Size` 校验注解;违反保存态约束的地方在 `violations` 里另行列出,不要用 `draft` 自身的字段是否为空来判断能不能保存。
- `draft` 除 `groups[].vehicleTypeName` 外的字段集与写侧 `GroupVehicleRequirementSaveReqVO` 完全一一对应(由专门的字段一致性单测钉住),可以原样 `PUT` 回 `/vehicle-requirement`;`vehicleTypeName` 回传时会被后端忽略,车型以 `vehicleType` 编码为准。
- `draft.groups[].remark` 的拼接标签口径:团号(客户名)→ 团号 → 客户名 → 订单号 → "未知户",不再回落裸雪花订单 ID。
- 权限码与「2」相同(`group-batch:demand:confirm`),本端点同样不引入任何写操作。
---
### 4. 团期下子订单列表 `GET /v3/admin/order/group-batch/{groupBatchId}/orders`
**VO**: `GroupBatchOrderItemRespVO`(30 个字段中本次只变更 4 个,见下表标注)
#### 使用场景
团期详情页"子订单"Tab 的列表数据源,每户一行。本次变更聚焦用车需求相关的 4 个字段,其余 26 个字段(`hotelRequirementStatus`、`totalPrice`、`travelers` 等)未变,已由 `30_8543_团期订单Tab用车状态按需求类别拆分并统一文案-修改接口-管理后台.md` 完整记录。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | 是 | - | 团期聚合主键 |
| page | Query | Integer | 否 | 缺省 1,<1 归一为 1 | 页码 |
| pageSize | Query | Integer | 否 | 缺省 20,<1 归一为 20,>200 截断为 200 | 每页条数 |
| includeTravelers | Query | Boolean | 否 | 缺省 true | 是否附出行人明细(证件号/手机号一律不返回) |
| includeNeeds | Query | Boolean | 否 | 缺省 true | 是否附房数/房型/特殊需求 |
| includeCancelled | Query | Boolean | 否 | 缺省 false | 是否含已取消子订单 |
#### 出参字段表(仅列本次变更相关字段)
| 字段 | 类型 | 说明 |
|------|------|------|
| vehicleRequirementStatus | string | 车需求状态:口径从"只看行程用车"改为"该户展示序首条活跃行"(行程用车优先、其次接送机)。该户一条活跃车需求行都没有时为 `null`(#8619) |
| vehicleRequirementStatusName | string | 状态中文名;未知码回落原 `code`(与 `vehicleRequirementKindName` 不同口径);`code` 为 `null` 时按该户 `needsVehicle` 分叉——`true` 出"未提交"、否则为 `null`。**#8619 起"未提交"只在两类需求都没报时出现** |
| vehicleRequirementKind | string | 本行车需求的用车类别:TRAVEL/TRANSFER。**#8619 起不再恒为 TRAVEL**——只报了接送机的户在这里下发 `TRANSFER`;无 active 行时为 `null` |
| vehicleRequirementKindName | string | 类别中文名:行程用车/接送机;认不出的类别给 `null`,**不回落编码**(与 `vehicleRequirementStatusName` 不同口径) |
| vehicleRequirements 🆕 | array | 该户全部活跃用车需求行(0~2 条:TRAVEL/TRANSFER 各至多一条),恒非 `null`,行程用车在前、接送机在后(#8619),见下表 |
`vehicleRequirements[]` 单项字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| kind | string | 需求类别:TRAVEL=行程用车 / TRANSFER=接送机 |
| kindName | string | 类别中文名;认不出的类别给 `null`,不回落编码 |
| status | string | 该条需求状态:PENDING/PROCESSING/DONE/PENDING_REVIEW/REJECTED_TO_CONSULTANT/REJECTED_TO_ADMIN |
| statusName | string | 状态中文名;PENDING_REVIEW 按 kind 分两套文案:TRAVEL="待提交车务"、TRANSFER="待审核" |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2104839654727618562/orders?page=1&pageSize=20
```
#### 响应示例
以下为测试环境实测响应(团期批次 `2104839654727618562`,2026-09-30 采集)。每条 `records[]` 实测另有 26 个本次未变的字段,此处只保留与本单相关的 5 个,其余省略(完整字段集见 changelog `30_8543`):
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"orderId": "2104839654652121090",
"vehicleRequirementStatus": "DONE",
"vehicleRequirementStatusName": "配车完成",
"vehicleRequirementKind": "TRAVEL",
"vehicleRequirementKindName": "行程用车",
"vehicleRequirements": [
{"kind": "TRAVEL", "kindName": "行程用车", "status": "DONE", "statusName": "配车完成"},
{"kind": "TRANSFER", "kindName": "接送机", "status": "PENDING", "statusName": "待车队配"}
]
},
{
"orderId": "2104839686486888449",
"vehicleRequirementStatus": "DONE",
"vehicleRequirementStatusName": "配车完成",
"vehicleRequirementKind": "TRAVEL",
"vehicleRequirementKindName": "行程用车",
"vehicleRequirements": [
{"kind": "TRAVEL", "kindName": "行程用车", "status": "DONE", "statusName": "配车完成"}
]
},
{
"orderId": "2104839729176514562",
"vehicleRequirementStatus": "DONE",
"vehicleRequirementStatusName": "配车完成",
"vehicleRequirementKind": "TRAVEL",
"vehicleRequirementKindName": "行程用车",
"vehicleRequirements": [
{"kind": "TRAVEL", "kindName": "行程用车", "status": "DONE", "statusName": "配车完成"},
{"kind": "TRANSFER", "kindName": "接送机", "status": "PENDING_REVIEW", "statusName": "待审核"}
]
}
],
"total": 3,
"page": 1,
"pageSize": 20
},
"traceId": null,
"success": true
}
```
上例里三户的 `vehicleRequirements` 长度分别为 2 / 1 / 2,`vehicleRequirementStatus` 与 `Kind` 取的都是展示序首条(TRAVEL 优先),因此三户顶层都是 `TRAVEL`;接送机那一行的真实状态只在 `vehicleRequirements[]` 里看得到。
#### 空数据 / 降级响应
- 团期批次不存在:返回业务错误(见"错误响应")。
- 团期下暂无子订单(或 `includeCancelled=false` 时全部已取消):`records` 为空数组,`total=0`。
- 该户一条活跃用车需求行都没有:`vehicleRequirementStatus`/`vehicleRequirementKind` 均为 `null`,`vehicleRequirements` 为**空数组**(不是 `null`);`vehicleRequirementStatusName` 按该户 `needsVehicle` 分叉。
- 该户只提交了接送机需求(#8619 修复的场景):`vehicleRequirementKind` 下发 `TRANSFER`、`vehicleRequirementStatus` 下发该行程的真实状态,不再是 `null`/"未提交"。
#### 错误响应
```json
{"code": 589500, "message": "团期不存在", "success": false, "data": null}
```
```json
{"code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "success": false, "data": null}
```
#### 业务边界
- 按用车类别判断状态一律读 `vehicleRequirements[]`,不要只读 `vehicleRequirementStatus`/`vehicleRequirementKind` 单值字段——一户两类需求并存时单值字段只能表达展示序首条。
- `vehicleRequirementKindName` 与 `vehicleRequirementStatusName` 是两套不同的未知码兜底口径(前者给 `null`,后者回落原码),不要用同一段兜底逻辑处理。
- **🔴 同一个 `status` 编码在不同 `kind` 上的中文名不同,前端不能自建 code→中文 映射表**:`PENDING_REVIEW` 在 `TRAVEL` 行下发「待提交车务」(推走它的是团期管理员整团一次的「提交车务」,该状态上没有逐户审核动作),在 `TRANSFER` 行下发「待审核」(有逐户审核动作)——这是 #8218 起的刻意分叉,声明见 `GroupVehicleHouseholdsRespVO`/`GroupBatchOrderItemRespVO` 的 `@ApiModelProperty`。两侧均有实测样本:批次 `2104839654727618562` 的 `2104839729176514562` 户 TRANSFER 行 = `PENDING_REVIEW`/「待审核」;批次 `2104840641651556353` 的 `2104840685708525570` 户 TRAVEL 行 = `PENDING_REVIEW`/「待提交车务」。**一律直接渲染后端下发的 `statusName`。**
- 权限码为 `group-batch:view`。
---
## 四、契约约束与正确调用方式
- **4 个新增 `*Name` 字段(#8544)是只读展示字段**:`statusName`/`planRefreshStateName`/`blockedStageName`/`planRefreshStalledReasonName` 只在响应体里出现,`PUT` 请求体 `GroupVehicleRequirementSaveReqVO` 未新增任何字段,回传这些字段会被忽略。
- **PUT `/vehicle-requirement`、POST `/vehicle-requirement/withdraw`、POST `/vehicle-requirement/waive` 三个写端点与本文档「三、2」共用完全同一个 `GroupVehicleRequirementRespVO` 类**:调用这三个端点后,响应体里同样带有 4 个新增 `*Name` 字段,字段语义、null 规则与「三、2」逐字一致,无需前端另写一套解析。
- **`aggregate-draft` 的 `draft` 字段类型变更(#8544)**:TypeScript/接口类型定义如果之前直接复用了 `GroupVehicleRequirementSaveReqVO` 的类型作为 `draft` 的类型,需要改成新类型(多一个只读字段 `groups[].vehicleTypeName`,其余字段名与类型逐一相同)。JSON 结构层面对已有解析代码零破坏,只有严格 schema 校验(如果有)需要放开这个新字段。
- **未知/认不出的编码统一规则**(本次涉及的所有 `*Name` 字段):`statusName`/`planRefreshStateName`/`blockedStageName`/`planRefreshStalledReasonName`/`draft.groups[].vehicleTypeName`/`vehicleRequirementKindName`/`vehicleRequirements[].kindName` 这一组字段认不出编码一律给 `null`,**不回落原编码**。这与 `orders` 端点的 `vehicleRequirementStatusName`(未知码回落原码,#8543/#8619 未改动此口径)以及团期列表页的团期状态中文名(同样回落原码)是**刻意不同**的两套口径,前端不要用同一段兜底组件处理。
- **`vehicleSeatSummary[].confirmedSeats`/`confirmedCount` 与 `totalSeats`/`totalCount` 不相等是设计上允许的常态**(#8545),不要写断言校验两者相等,也不要用其中一组数字反推另一组。
- **`orders` 端点判断某户是否提交了某类用车需求,一律遍历 `vehicleRequirements[]` 按 `kind` 过滤**,不要依赖单值字段 `vehicleRequirementStatus`/`vehicleRequirementKind`(#8619 起单值字段只表达展示序首条,可能丢失第二类需求的状态)。
---
## 六、边界行为
- 未登录访问:网关拦截,不进入本文档描述的业务逻辑。
- 权限不足(当前角色未获授团期权限,或该团期不在本人名下):所有 4 个端点统一报 `589507`。
- 团期不存在:所有 4 个端点统一报 `589500`。
- `GET .../vehicle-requirement` 团期尚未形成正式需求:`data` 为 `null`,HTTP 200,不是错误。
- `planRefreshState`/`blockedStage`/`planRefreshStalledReason` 三列 DB 为 `NULL`(绝大多数团期的正常态,从未走过受控重开):对应 3 个 `*Name` 字段一律为 `null`。
- `vehicleSeatSummary[]`:团级从未确认过时 `confirmedSeats`/`confirmedCount` 恒为 `0`(不是 `null`);车务定的车型在逐户报的车型集合之外时会新增一行 `totalSeats=0`/`totalCount=0` 的记录。
- `orders` 端点 `vehicleRequirements[]`:该户没有任何活跃用车需求行时为空数组(不是 `null`)。
---
## 六.5、枚举
**所属字段**:`GroupVehicleRequirementRespVO.status` / `statusName`
| 值 | 中文名 | 说明 |
|----|--------|------|
| DRAFT | 草稿 | 团期管理员正在汇总逐户需求、编辑分组与逐日人数 |
| CONFIRMED | 已确认 | 整份确认通过,等待发车务 |
| DISPATCHED | 已发车务 | 已推送到车务侧,等待配车 |
| DONE | 配车完成 | 车务侧已完成整团配车 |
| PENDING_RECONFIRM | 待重新确认 | 确认后团期人数/成员发生变化,需要重新确认整份 |
| CANCELLED | 已取消 | - |
**所属字段**:`GroupVehicleRequirementRespVO.planRefreshState` / `planRefreshStateName`
| 值 | 中文名 | 说明 |
|----|--------|------|
| null(列值 NULL) | (无中文名,字段为 null) | 从未登记过任何刷新,多数团期的正常态 |
| PENDING | 刷新中 | 刷新命令已登记,等 fleet 刷完 |
| DONE | 刷新完成 | 就绪回调已通过判定并应用,本轮刷新闭环 |
| FAILED | 刷新失败 | 已耗尽重试预算或被 fleet 判定性终结;不放行团期,出团门②持续拒绝 |
**所属字段**:`GroupVehicleRequirementRespVO.planRefreshStalledReason` / `planRefreshStalledReasonName`
| 值 | 中文名 | 说明 |
|----|--------|------|
| STATE_FAILED | 刷新已被判死 | `plan_refresh_state` 已经是 FAILED |
| COMMAND_FAILED | 刷新命令已失败 | 状态列仍是 PENDING,但刷新命令已终态失败(回写钩子未落成) |
| TIMEOUT | 刷新超时 | 状态列仍是 PENDING、命令也未判死,但已超过本轮窗口时限(缺省 120 分钟) |
**所属字段**:`GroupVehicleRequirementRespVO.blockedStage` / `blockedStageName`
取值域是团期状态枚举 `GroupBatchStatus`(既有枚举,本次未新增值)的全域,本次只是给这个已有编码字段配了中文名。常见值举例:
| 值 | 中文名 | 说明 |
|----|--------|------|
| RESOURCE_PREPARING | 资源准备中 | - |
| MATERIAL_PREPARING | 物料准备中 | - |
| PENDING_DEPARTURE | 待出发 | - |
| (其余 GroupBatchStatus 取值) | 对应中文名 | 认不出的编码给 `null` |
**所属字段**:`orders[].vehicleRequirementKind` / `vehicleRequirementKindName`、`orders[].vehicleRequirements[].kind` / `kindName`
`VehicleRequirementKind` 枚举本身固定只有 2 个值(本次未新增第三个值,放宽的是响应字段的**实际观测取值范围**,不是枚举定义):
| 值 | 中文名 | 说明 |
|----|--------|------|
| TRAVEL | 行程用车 | 服务日冻结为行程日 |
| TRANSFER | 接送机 | 服务日取航班/车次日期,允许落在行程日窗外 |
---
## 六.6、修改前后对比
| 项 | 改前 | 改后 |
|----|------|------|
| `GroupVehicleRequirementRespVO` 状态/刷新状态/阻断阶段/停滞归因 | 仅下发编码,前端自行映射中文 | 新增 4 个 `*Name` 字段直接下发中文名;认不出编码给 `null` |
| `aggregate-draft.draft` 字段类型 | 写侧 `GroupVehicleRequirementSaveReqVO`(挂校验注解,多余的约束会渲染进 Swagger) | 只读读侧 `GroupVehicleRequirementDraftRespVO`(不挂校验注解,多一个只读字段 `vehicleTypeName`) |
| `draft.groups[].remark` 逐户标签兜底 | 团号(客户名)→ 团号 → 客户名 → 订单号(裸雪花 ID) | 团号(客户名)→ 团号 → 客户名 → 订单号 → "未知户"(不再回落裸雪花 ID) |
| `requirement-summary.vehicleSeatSummary[]` | 只有 `totalSeats`/`totalCount`(逐户已提交口径) | 并列新增 `confirmedSeats`/`confirmedCount`(团级已确认口径),可能新增车型行 |
| `orders[].vehicleRequirementStatus`/`Kind` | 只按 TRAVEL 过滤取行;只提交接送机的户恒为 `null`/"未提交" | 取展示序首条活跃行(TRAVEL 优先、TRANSFER 次之);只提交接送机的户下发真实状态 |
| `orders[].vehicleRequirements` | 不存在该字段 | 新增,0~2 条,逐类下发状态 |
## 六.7、影响评估
- **前端必改**:如果曾对 `draft.remark` 文本做正则匹配来提取订单号或高亮逐户标签,需要兼容新的"未知户"文案且不再假设会出现裸数字订单 ID。**并且请不要按任何固定格式解析 `remark`**——本单只改新生成的汇总草稿,已确认落库的存量团期其 `GET .../vehicle-requirement` 返回的 `groups[].remark` 仍是改前格式(订单号前缀,形如 `HL20260929154809598: …`),不做历史数据回写。该字段是给运营看的自由文本,两种格式会长期并存。
- **前端必改**:如果曾假设 `orders[].vehicleRequirementKind` 恒为 `TRAVEL` 来做图标/文案硬编码分支,需要改为按实际值(`TRAVEL`/`TRANSFER`)渲染,并优先改用 `vehicleRequirements[]` 数组按类判断。
- **前端可选增强**:可以直接展示后端下发的 4 个新增中文名字段,替换掉前端此前自行维护的编码→中文映射表(如果有)。
- **前端需知悉但不需要立即改**:`vehicleSeatSummary[]` 新增的两个字段、可能新增的车型行,只在页面展示这两个数字时才需要处理;不展示则忽略即可,不影响既有渲染。
- **零风险**:所有新增字段都是**在既有 JSON 对象上新增键**,未删除、未改名任何既有字段(`aggregate-draft.draft` 虽改了后端类型,但 JSON 键集合与既有字段类型未变,由专门单测钉住);未做严格 schema 校验的前端代码可无感兼容。
---
## 七、不影响范围
- `GroupVehicleRequirementSaveReqVO`(PUT 请求体)字段集未变,仍是原有编码字段集,本次新增的只读字段不参与保存也不会被写侧读取。
- `GroupVehicleRequirementDraftRespVO` 除 `groups[].vehicleTypeName` 外的全部字段(顶层 `version`/`remark`、`groups[]` 内 `groupId`/`groupCode`/`vehicleType`/`serviceStartDate`/`serviceEndDate`/`seats`/`count`/`specialTags`/`days`)未变,由 `GroupVehicleRequirementDraftRespVOFieldParityTest` 钉住与写侧字段集一致。
- `VehicleRequirementKind` 枚举定义本身未变,仍只有 `TRAVEL`/`TRANSFER` 两个值。
- 团期用房相关端点(`hotel-households` 等)不在本次改动范围内。
- `orders` 端点除本文档列出的 4 个字段外,其余 26 个字段未变(详见 changelog `30_8543`)。
- 809121/809122/809123 三个错误码的触发条件收窄与文案改写属于 #8577,不在本次三张工单范围内,详见 changelog `30_8577_只提交接送机的户不再被判未提交用车需求-修改接口-管理后台.md`。
---
## 八、测试环境已验证
- hl-order-service-v3 dev-v3 分支已部署测试网关,HEAD `6373e5cf2`(`git merge-base --is-ancestor` 核实本单提交 `5f2f86bc96` 已在其祖先链内),jar mtime 2026-09-30 09:27:12,两个实例 Nacos 健康检查均为 healthy。
- 以下四条读口实测取样自团期批次 `2104840641651556353`(`vehicleRequirements[] == []` 一条取样自 `2104830530031919106`),2026-09-30 采集。
- `GET .../requirement-summary` 实测:`vehicleSeatSummary[0]` = `{"vehicleType":"mpv","vehicleTypeName":"商务车","totalSeats":14,"totalCount":2,"confirmedSeats":7,"confirmedCount":1}`。
- `GET .../vehicle-requirement/aggregate-draft` 实测:`draft.groups` 非空,含 `vehicleTypeName: "商务车"`;`groups[0].remark` 为「团号(客户名)」格式(`26-3682(董海涛): …;26-0805(谢丽萍): …`),未出现裸雪花订单 ID。
- `GET .../orders` 实测:2 条子订单,每条 `vehicleRequirements` 键均存在且非 `null`,长度分别为 2 与 1。另在一个无活跃用车需求行的户上实测该键为**空数组 `[]` 而不是 `null`**(与 `GroupBatchConverter.toVehicleRequirementItems()` 从 `new ArrayList<>()` 起手、无 null 分支一致)。
- `GET .../vehicle-requirement` 实测:顶层键包含 `status`/`statusName`/`planRefreshState`/`planRefreshStateName`/`blockedStage`/`blockedStageName`/`planRefreshStalledReason`/`planRefreshStalledReasonName` 共 8 个;观测样本 `status="DONE"` → `statusName="配车完成"`;另外三个 DB 列为 `NULL`,对应 `*Name` 字段均实测为 `null`。
- **「认不出的编码 → `null`」这条行为本轮没有实测样本**:候选团期的 `plan_refresh_state` / `blocked_stage` / `plan_refresh_stalled_reason` 三列在库中全为 `NULL`,测试环境里造不出一个库内存着字典外编码的自然样本。该行为由源码与单测两侧钉住:`GroupVehiclePlanRefreshState.labelOf()`、`GroupBatchStatus.descOf()` 对未命中编码返回 `null`(不回落编码原文)。前端按 `null` 兜底渲染即可。
- 单元测试:hl-order-service-v3 模块 `Tests run: 561, Failures: 0, Errors: 0, Skipped: 0`,38 个相关测试类逐类点名核对报告文件均存在(38/38 命中);`nested_selector_census` 退出码 0(无 `@Nested` 类被静默漏跑)。
- ArchUnit/架构门禁测试:`Tests run: 167`,全绿。
---
## 十、相关文档
- `docs/CODE_RULES.md` §3(VO 命名与读写侧分离约定)
- changelog `30_8543_团期订单Tab用车状态按需求类别拆分并统一文案-修改接口-管理后台.md`(`orders` 端点其余字段的完整文档,及 `vehicleRequirementKind` 字段本次改动前的基线状态)
- changelog `30_8577_只提交接送机的户不再被判未提交用车需求-修改接口-管理后台.md`(PUT/aggregate-draft/confirm-check 三处 809121/809122/809123 错误码本次收窄的完整文档)
---
## 关联 / 联系人
- 关联 Issue:#8544、#8545、#8619
- 关联 PR:#8625(#8544 #8545)、#8624(#8619)
- 联系人:wx
@@ -0,0 +1,474 @@
---
schema: "hl-changelog/v2"
ticket: "8548"
title: "团期需求重开待审提示接入 4 个只读端点,并修复整团免车团确认时接送机需求放行不到的问题"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "043232cc87d99c33a42325115f28929dc3b06bd6"
target_release: "v2.1"
verified_at: "2026-09-30"
status_note: "PR #8586 合并 dev-v3(ddea7e710c);测试网关部署确认:hl-order-service-v3 @ ff6863754、hl-fleet-service @ 99fb369ba(deploy-status.sh 实测,两者均以 ddea7e710c 为祖先)。4 个只读端点中 3 个(团期详情 A2、房务看板详情 H2、fleet 配车总览)已用同一真实团期(groupBatchId=2104839654727618562,团号 T26-3963)实测捕获非空取值,三端一致;第 4 个(房务看板列表 H1)因该团未被房务认领、不出现在列表口,改用另一真实团期(groupBatchId=2104838272570245121)捕获到已确认团的双 null 基线,未能在本轮独立捕获 H1 的非空实例——这是列表口与详情口可见集合不同导致的结构性限制,不是契约缺口,H1 的字段契约与 H2/A2/总览完全同源同算法(同一个 GroupBatchRequirementReopenHintService)。#8548 的确认端点行为修复(整团免车放行接送机)本身是写操作,为避免误改测试服现存业务数据未做原子调用,已按源码逐行核实:GroupBatchRequirementService.java 505-593 行(doConfirm 内核 javadoc 与分支代码)、1161-1296 行(release 集合装配与 waivedVehicleSnapshot)。;前端已交付:团期详情状态条/房务看板列表/看板详情三处需求重开待审提示接入(户数 null 不折算 0,类别可单独 null),fleet 配车总览出口已删零消费,23 例定向测试全绿(hl-admin 043232cc)"
updated_at: "2026-09-30"
base: "dev-v3"
---
# hl-order-service-v3 / hl-fleet-service:团期需求重开待审提示接入 4 个只读端点,并修复整团免车团确认时接送机需求放行不到的问题
> **存放目录**: `changelogs-v2/2026-09/`
> **服务**: hl-order-service-v3(主,#8549 新提示的唯一产出口 + #8548 行为修复)、hl-fleet-service(透传消费方,无独立业务逻辑改动)
> **PR**: #8586
> **Issue**: #8548、#8549(一个 PR 同时处理两张关联工单)
> **日期**: 2026-09-30
> **影响范围**: 4 个只读端点响应新增 2 字段(团期详情 A2、房务看板列表 H1、房务看板详情 H2、fleet 团期配车总览);1 个写端点(团期整体确认需求)行为修复,响应结构不变
---
## ⚠️ 关键变化
- 新增 2 个响应字段:`requirementReopenPendingHouseholds`(`Integer`)、`requirementReopenResourceType`(`String`),出现在 4 个只读端点:`GET /v3/admin/order/group-batch/{groupBatchId}`、`GET /v3/admin/house/group-batches`、`GET /v3/admin/house/group-batches/{groupBatchId}`、`GET /admin/fleet/group-dispatch/batches/{groupBatchId}/overview`。四处取值同源同算法(`GroupBatchRequirementReopenHintService`,唯一产出口),不存在四套口径分叉的风险。
- 🔴 **`null` 不代表 `0`,不要折算成 0 渲染「0 户需求待审核」**。字段只在同时满足 3 个条件时才非空:① 团级 `requirementConfirmed=false`;② 能查到「定制师改需求触发的自动重开」留痕(区别于管理员手动打回,后者无此留痕);③ 重开后该团确实还有在团户处于待审核状态。三者任一不满足,两个新字段都是 `null`,前端应继续渲染原有的「待管理员重新确认」文案。
- **两个新字段不是「要么都有要么都无」的一对**:`requirementReopenPendingHouseholds` 非空时,`requirementReopenResourceType` 仍可能单独为 `null`(重开留痕的 `extra` JSON 解析失败/缺键时的降级),此时只渲染「N 户需求待审核」,不带 HOTEL/VEHICLE 类别文案,户数本身不受影响。
- **不要与既有字段 `pendingReviewHouseholds` 混淆**(仅 H2 详情端点有此字段):`pendingReviewHouseholds` 是房务看板自己的统计口径,只数房需求,任何时候都下发;新增的 `requirementReopenPendingHouseholds` 是团级确认闸的提示,数的是房、车两类待审需求的户去重并集,且只在上述 3 道闸门都满足时才有值。同一个团这两个数字不一致是正常的,不能互相对账。
- **#8548 行为修复(不涉及任何字段新增/删除)**:`POST /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm` 整团确认时,若团期处于「整团免车」(管理员声明免车,`vehicleWaived=true`)状态,此前该分支对车侧释放集合恒返回空,导致该团后续补交的接送机(TRANSFER)需求永远放行不到、团级需求闸永久卡在待确认。修复后免车团确认会释放 **TRANSFER** 需求,但仍**不释放 TRAVEL**(整团免车声明的管辖范围只到 TRAVEL,释放 TRAVEL 等于替管理员推翻免车声明)。可观察的变化只是响应里既有字段 `transferDispatchedOrderIds`/`vehicleDispatchedCount` 现在对免车团也可能非空,响应 VO 结构本身零改动。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | A2 团期详情 | GET | `/v3/admin/order/group-batch/{groupBatchId}` | 字段新增(非破坏性) | 响应新增 `requirementReopenPendingHouseholds`/`requirementReopenResourceType` |
| 2 | H1 房务团期看板列表 | GET | `/v3/admin/house/group-batches` | 字段新增(非破坏性) | 同上,列表项级别 |
| 3 | H2 房务团期看板详情 | GET | `/v3/admin/house/group-batches/{groupBatchId}` | 字段新增(非破坏性) | 同上 |
| 4 | 团期配车总览 | GET | `/admin/fleet/group-dispatch/batches/{groupBatchId}/overview` | 字段新增(非破坏性) | 同上,order-v3 原样透传,fleet 不自算 |
---
## 三、接口详情
### 1. A2 团期详情 `GET /v3/admin/order/group-batch/{groupBatchId}`
**VO**: `(无请求体,仅路径参数)` → `GroupBatchDetailRespVO`
#### 使用场景
团期管理员在团期详情页查看需求确认状态。此前该页只能看到 `requirementConfirmed=false`,无法区分「等定制师第一次提交」「被管理员打回」「已重新提交但还有户没处理完」三种情况,一律渲染「待管理员重新确认」。本次起,属于第三种情况(定制师改需求触发的自动重开,且确实还有户在等审)时,响应额外带出具体待审户数与是谁(房/车)推倒了确认闸。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
#### 出参字段表
以下是本次新增/说明文案更新的字段;其余既有字段结构未变,不重复列出。
| 字段 | 类型 | 说明 |
|------|------|------|
| requirementConfirmed | Boolean | 需求整体确认标记(既有字段,说明文案本次更新):为 `false` 时不要直接渲染「待管理员重新确认」,先看 `requirementReopenPendingHouseholds` |
| requirementReopenPendingHouseholds | Integer | **新增**。需求待审核户数:`requirementConfirmed=false` 且是「定制师改需求触发的自动重开」时下发;为 `null` 表示不下发(已确认 / 管理员打回置 0 / 已无人待审),按原有文案渲染。**计数单位是「户」不是「需求行」**——同一户同时报行程用车与接送机用车只计 1 户 |
| requirementReopenResourceType | String | **新增**。触发最近一次需求重开的资源类别 `HOTEL` / `VEHICLE`:只标「谁把确认闸推倒了」,与户数口径无关(户数是房、车两类的并集);取不到时为 `null`,此时文案不带类别 |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2104839654727618562
```
#### 响应示例
真实实测(测试网关,业务 admin 身份)。以下为节选(仅摘录本次相关字段,其余既有字段结构未变,不重复列出):
```json
{
"code": 200,
"data": {
"groupBatchId": "2104839654727618562",
"batchNo": "T26-3963",
"requirementConfirmed": false,
"requirementReopenPendingHouseholds": 1,
"requirementReopenResourceType": "VEHICLE"
},
"success": true
}
```
#### 空数据 / 降级响应
- 3 道闸门任一不满足(已确认 / 管理员手动打回 / 重开后已无人待审):两个新字段均为 `null`,前端按原有文案渲染。
- 重开留痕的 `extra` JSON 解析失败或缺键:仅 `requirementReopenResourceType` 单独降级为 `null`,`requirementReopenPendingHouseholds` 不受影响照常下发(服务端 `readResourceType()` 的不对称降级,不会因为类别取不到而连户数一起丢)。
#### 错误响应
```json
{"code":589500,"message":"团期不存在","data":null,"traceId":null,"success":false}
```
其余既有错误码本次未变:589507(无操作权限:当前角色未授予团期权限,或该团期不在您名下)。
#### 业务边界
- 🔴 `requirementReopenPendingHouseholds` 为 `null` 不代表 0,不要折算成 0 渲染。
- `requirementReopenResourceType` 可能单独为 `null`(即使户数非空),此时只显示户数、不带类别文案。
- 判断是否要展示这两个字段,先看 `requirementConfirmed`;`requirementConfirmed=true` 时两个新字段恒为 `null`,不需要额外判断。
---
### 2. H1 房务团期看板列表 `GET /v3/admin/house/group-batches`
**VO**: `HouseGroupBatchBoardPageReqVO` → `PageResult<HouseGroupBatchBoardSimpleRespVO>`
#### 使用场景
房务在看板列表页浏览已认领的团期。列表项与详情页(见下)共用同一套团级字段判定逻辑,此前列表页同样只能看到 `requirementConfirmed=false` 一个布尔值,本次起可展示与详情页一致的待审户数与类别提示。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| scope | Query | String | ❌ | 最长 8 | 可见范围 `MINE`(默认,只看本人认领)/ `ALL`(看全部已认领团,#8491 起全体房务可传) |
| claimerAdminId | Query | Long | ❌ | - | 按认领人筛(仅 `scope=ALL` 生效) |
| planStatus | Query | String | ❌ | 最长 16 | 计划行状态 `PENDING` / `CONFIRMED`,不传=全部 |
| batchStatus | Query | String | ❌ | 最长 200 | 团期状态多选,逗号分隔;默认四态;`CANCELLED` 传入被忽略 |
| stayDateFrom | Query | LocalDate | ❌ | ISO 日期 | 住期区间起 |
| stayDateTo | Query | LocalDate | ❌ | ISO 日期 | 住期区间止 |
| departDateFrom | Query | LocalDate | ❌ | ISO 日期 | 出发日区间下界 |
| departDateTo | Query | LocalDate | ❌ | ISO 日期 | 出发日区间上界 |
| keyword | Query | String | ❌ | 最长 32 | 团期号或产品名包含匹配 |
| page | Query | Long | ❌ | ≥1,默认 1 | 页码 |
| pageSize | Query | Long | ❌ | 1~50,默认 20 | 每页条数 |
#### 出参字段表
以下是本次新增/说明文案更新的字段(列表项级别);其余既有字段结构未变,不重复列出。
| 字段 | 类型 | 说明 |
|------|------|------|
| requirementConfirmed | Boolean | 需求整体确认标记(既有字段,说明文案本次更新):为 `false` 时先看 `requirementReopenPendingHouseholds` 再定文案 |
| requirementReopenPendingHouseholds | Integer | **新增**。语义与 A2 完全一致(同一产出口) |
| requirementReopenResourceType | String | **新增**。语义与 A2 完全一致 |
#### 请求示例
```http
GET /v3/admin/house/group-batches?scope=ALL&pageSize=50
```
#### 响应示例
真实实测(测试网关,房务角色)。列表口只显示**已被房务认领**的团,本次实测命中的这一条是已确认团(双 `null` 基线),节选:
```json
{
"code": 200,
"data": {
"list": [
{
"groupBatchId": "2104838272570245121",
"requirementConfirmed": true,
"requirementReopenPendingHouseholds": null,
"requirementReopenResourceType": null
}
],
"total": 1
},
"success": true
}
```
> 说明:本轮实测范围内命中的已认领团恰好都是已确认状态,未独立捕获非空实例;非空实例已在 A2/H2/fleet 总览三端用同一真实团期(T26-3963)交叉验证一致,H1 走的是同一个 `GroupBatchRequirementReopenHintService` 产出口,字段契约同源,只是列表口的可见集合(仅已认领团)与详情口不同。
#### 空数据 / 降级响应
- 无匹配团期:`list` 为空数组,`total` 为 0(既有行为未变)。
- 候选集超 500 时 `total` 返回 -1 表示未统计(既有行为,本次未变,与新字段无关)。
- 3 道闸门任一不满足:该行的两个新字段均为 `null`。
#### 错误响应
```json
{"code":808090,"message":"未登录或非房务角色,无权操作","data":null,"traceId":null,"success":false}
```
#### 业务边界
- 未认领的团不在本列表口,与团期抢单池页以「认领动作」为界互斥,新字段不改变这条边界。
- 同上,`null` 不代表 0;`requirementReopenResourceType` 可单独为 `null`。
---
### 3. H2 房务团期看板详情 `GET /v3/admin/house/group-batches/{groupBatchId}`
**VO**: `(无请求体,仅路径参数)` → `HouseGroupBatchBoardRespVO`
#### 使用场景
房务点开具体团期查看逐日配房详情。全体房务可读任意团期(不校验认领归属),他人认领的团返回 `readOnly=true`(#8491,与本次改动无关)。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | - | 团期主订单 ID |
#### 出参字段表
以下是本次新增/说明文案更新的字段;其余既有字段(`hotelReady`、`pendingReviewHouseholds`、`days[]` 等)结构未变,不重复列出。
| 字段 | 类型 | 说明 |
|------|------|------|
| requirementConfirmed | Boolean | 需求整体确认标记(既有字段,说明文案本次更新):为 `false` 时先看 `requirementReopenPendingHouseholds` 再定文案 |
| requirementReopenPendingHouseholds | Integer | **新增**。语义与 A2 完全一致;🔴 与既有字段 `pendingReviewHouseholds` **不是一回事**——后者只数房需求、任何时候都下发,前者是房车并集且只在 3 道闸门满足时下发,同一个团两者数字不一致是正常的,不能互相对账 |
| requirementReopenResourceType | String | **新增**。语义与 A2 完全一致 |
#### 请求示例
```http
GET /v3/admin/house/group-batches/2104839654727618562
```
#### 响应示例
真实实测(测试网关,房务角色)。节选:
```json
{
"code": 200,
"data": {
"groupBatchId": "2104839654727618562",
"requirementConfirmed": false,
"requirementReopenPendingHouseholds": 1,
"requirementReopenResourceType": "VEHICLE",
"pendingReviewHouseholds": 0
},
"success": true
}
```
> 与同一团在 A2、fleet 总览的实测结果逐字段一致(`requirementReopenPendingHouseholds=1`、`requirementReopenResourceType="VEHICLE"`),印证三端同源同算法;`pendingReviewHouseholds=0` 与 `requirementReopenPendingHouseholds=1` 在此例中不同,正是上表说明的两套口径不对账的真实样本。
#### 空数据 / 降级响应
- 3 道闸门任一不满足:两个新字段均为 `null`。
- `requirementReopenResourceType` 单独降级为 `null` 时 `requirementReopenPendingHouseholds` 不受影响。
#### 错误响应
```json
{"code":589500,"message":"团期不存在","data":null,"traceId":null,"success":false}
```
```json
{"code":808090,"message":"未登录或非房务角色,无权操作","data":null,"traceId":null,"success":false}
```
#### 业务边界
- 🔴 `requirementReopenPendingHouseholds` 非空不要与 `pendingReviewHouseholds` 混淆或相加,两者统计口径不同。
- `null` 不代表 0;`requirementReopenResourceType` 可单独为 `null`。
---
### 4. fleet 团期配车总览 `GET /admin/fleet/group-dispatch/batches/{groupBatchId}/overview`
**VO**: `(无请求体,仅路径参数)` → `GroupDispatchOverviewRespVO`
#### 使用场景
车务在配车总览页查看该团的需求确认状态、逐日排车与接送机缺口。`requirementReopenPendingHouseholds`/`requirementReopenResourceType` 由 order-v3 内部覆盖口(`GET /v3/internal/group-batch/{id}/vehicle-coverage`,Feign 专用,非前端可直接调用)原样透传,fleet 侧不做任何二次计算,与 A2/H2 保证同一口径。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | - | 团期主订单 ID |
#### 出参字段表
以下是本次新增/说明文案更新的字段;其余既有字段(`serviceDates`、`vehicleReady`、`days[]`、`orders[]`、`transferPendingTotal`、`conversationKey` 等)结构未变,不重复列出。
| 字段 | 类型 | 说明 |
|------|------|------|
| requirementConfirmed | Boolean | 整团需求是否已确认(既有字段,说明文案本次更新):为 `false` 时先看 `requirementReopenPendingHouseholds` 再定文案 |
| requirementReopenPendingHouseholds | Integer | **新增**。order-v3 覆盖口原样透传,fleet 不自算;🔴 `null` 不代表 0,不要折算成 0 渲染 |
| requirementReopenResourceType | String | **新增**。order-v3 覆盖口原样透传,语义与 A2 完全一致 |
#### 请求示例
```http
GET /admin/fleet/group-dispatch/batches/2104839654727618562/overview
```
#### 响应示例
真实实测(测试网关,车务角色,roleId=5)。节选:
```json
{
"code": 200,
"data": {
"groupBatchId": "2104839654727618562",
"batchNo": "T26-3963",
"requirementConfirmed": false,
"requirementReopenPendingHouseholds": 1,
"requirementReopenResourceType": "VEHICLE"
},
"success": true
}
```
> 与同一团在 A2、H2 的实测结果逐字段一致。
#### 空数据 / 降级响应
- 3 道闸门任一不满足:两个新字段均为 `null`,与 A2/H2 同步(同一份覆盖口数据)。
- order-v3 覆盖口不可达时,整个端点按既有降级规则返回 600012(团期配车基线不可达),不会出现「新字段单独降级、其余字段正常」的中间态——两个新字段与其余团级字段是同一次 Feign 调用的产物,不可能分开失败。
#### 错误响应
```json
{"code":600012,"message":"团期配车基线不可达,请稍后重试","data":null,"traceId":null,"success":false}
```
其余既有错误码本次未变:401(未登录)。
#### 业务边界
- 🔴 `requirementReopenPendingHouseholds` 为 `null` 不代表 0。
- 该字段与 fleet 自己的 `transferPendingTotal`(接送机未配计数)是两回事:前者是「团级需求确认闸的提示」,后者是「已确认需求里还有多少接送机缺口没排车」,两者可以同时非空,互不覆盖。
---
## 四、契约约束与正确调用方式
> 本节只写后端响应字段的正确消费方式,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | 说明 |
|------|------|
| ✅ 判断是否展示「N 户需求待审核」 | 先判 `requirementConfirmed===false`,再判 `requirementReopenPendingHouseholds != null` |
| ✅ 处理 `requirementReopenPendingHouseholds` 非空但 `requirementReopenResourceType` 为 `null` | 只渲染「N 户需求待审核」,不带类别文案,这是合法的降级态,不是异常 |
| ❌ 把 `requirementReopenPendingHouseholds` 为 `null` 折算成 0 渲染 | `null` 与「0 户待审」是两种不同状态:前者是「不适用/无需提示」,后者是「重开了但已处理完」——本次实现中「已处理完」同样落到 `null`(闸门 3 过滤),所以两者当前观察上是同一渲染结果,但契约上不保证永远如此,不要做数值折算 |
| ❌ 拿 H2 的 `pendingReviewHouseholds` 和 `requirementReopenPendingHouseholds` 相加或对账 | 两个字段统计口径不同(前者只数房、恒下发;后者数房车并集、条件下发) |
### 切换状态时的必要动作
无。本次 4 个端点均为只读字段新增,不涉及任何请求体/入参变化,前端无需在调用序列上做任何调整。
---
## 五、数据库行为
`GroupBatchRequirementReopenHintService` 只读、不写任何表。查库固定 3 次批量查询(不随团期数量线性增长):① 从 `group_batch` 内存过滤未确认团;② 按团期 ID 批量查 `group_batch` 状态时间线,取最近一条 `BATCH_REQUIREMENT_REOPENED` 事件日志;③ 按事件命中的团批量取在团子订单 ID,再批量查待审核户。看板一页多团时同样是固定 3 次查询,不退化为 N+1。
#8548 修复:`doConfirm` 内核在整团免车分支新增一次车侧需求释放调用(`dispatchGroupTransferRequirements`),与既有的房侧放行在同一事务内,任一步失败整团零写入(既有的 809112 整团回滚保证不变)。
---
## 六、边界行为
- 团级 `requirementConfirmed=true`(已确认)→ 4 个端点的两个新字段恒为 `null`。
- `requirementConfirmed=false` 但查不到 `BATCH_REQUIREMENT_REOPENED` 留痕(即管理员手动打回,而非定制师改需求触发的自动重开)→ 两个新字段恒为 `null`,前端渲染原有的「待管理员重新确认」文案。
- `requirementConfirmed=false` 且有重开留痕,但重开后该团在团户已全部处理完(待审户数为 0)→ 两个新字段恒为 `null`。
- 重开留痕的 `extra` JSON 缺失/为空/解析失败 → 仅 `requirementReopenResourceType` 单独为 `null`,`requirementReopenPendingHouseholds` 不受影响。
- **(#8548,确认端点行为变更,非本次响应字段变化)** 整团免车团(`vehicleWaived=true`)确认时,此前车侧释放集合恒为空,接送机需求永远放行不到;修复后释放 **TRANSFER**(接送机)需求,仍不释放 **TRAVEL**(行程用车,整团免车声明的管辖范围仅限于此);同时不推进正式团级用车需求(`groupVehicleRequirementId`/`Status`/`Version` 三个既有字段在免车分支仍为 `null`,因为免车团本就没有需要推进的正式需求,此行为本次未变)。
---
## 六.5、枚举 / 数据字典
### 需求重开资源类别(`requirementReopenResourceType`)
**所属字段**: `requirementReopenPendingHouseholds` 的伴生字段,4 个端点通用 | **类型**: `String`(取不到时为 `null`)
| 值 | 含义 | 说明 |
|----|------|------|
| `HOTEL` | 定制师改住宿需求触发的自动重开 | |
| `VEHICLE` | 定制师改用车需求触发的自动重开 | |
| `null` | 取不到类别,或未落入需要下发的场景 | 户数字段仍可能非空,参见六、边界行为 |
取值来源:团期状态时间线里最近一条 `BATCH_REQUIREMENT_REOPENED` 事件日志的 `extra` JSON 中 `resourceType` 键,由触发重开的那条业务逻辑写入;本次未新增写入路径,只新增读取与下发。
---
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `requirementReopenPendingHouseholds` | 不存在(4 个端点均无) | 新增,`Integer`,语义见上,4 端点同源同算法 |
| `requirementReopenResourceType` | 不存在(4 个端点均无) | 新增,`String`,语义见上 |
### 行为级对比
| 场景 | 改前 | 改后 |
|------|------|------|
| `requirementConfirmed=false` 且是定制师改需求触发的自动重开、确实还有户在等审 | 4 个端点均只有 `requirementConfirmed=false`,前端一律渲染「待管理员重新确认」,无法与「管理员手动打回」区分 | 额外带出具体待审户数与资源类别,可渲染「N 户需求待审核」区分于打回场景 |
| 整团免车团确认(`POST .../requirement/confirm`) | 车侧释放集合恒为空,免车后补交的接送机需求永远放行不到,团级需求闸永久卡在待确认,无任何报错或日志提示 | 释放 TRANSFER 需求(不释放 TRAVEL),响应既有字段 `transferDispatchedOrderIds`/`vehicleDispatchedCount` 对免车团可能非空 |
## 六.7、影响评估
- **是否破坏向后兼容**: 否——4 个只读端点均为纯字段新增,既有字段类型/取值/含义均未变;#8548 修复不改变响应 VO 结构,只改变部分既有字段(`transferDispatchedOrderIds`/`vehicleDispatchedCount`)在特定场景下的实际取值。旧前端忽略新字段不受任何影响。
- **前端是否必须同步上线**: 否(不上线不会报错或丢功能);建议同步——上线后可以把「待管理员重新确认」与「N 户需求待审核」两种场景分开展示,减少运营/房务/车务误判为同一种阻塞。
- **前端 workaround 清理点**: 若此前为区分「打回」与「重开待审」两种 `requirementConfirmed=false` 场景写过额外查询或猜测逻辑,现在可以直接用新字段替换。
---
## 七、不影响范围
- **仅影响**: 上表列出的 4 个只读端点的响应字段;`POST /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm` 端点在整团免车场景下的车侧释放行为。
- **零影响**:
- 4 个只读端点的请求参数与既有校验规则
- `POST .../requirement/confirm` 的请求体、响应 VO 结构、非免车团的确认行为
- `POST .../requirement/confirm` 在免车团场景下对 TRAVEL 需求的处理(仍不放行,逐单放行入口不受影响)
- `GET /v3/internal/group-batch/{id}/vehicle-coverage` 内部 Feign 端点之外的其它 internal 接口
- `PUT /admin/fleet/assignments/pickup-dropoff-config` 等接送机配置端点
---
## 八、测试环境已验证
服务:`hl-order-service-v3` @ `ff6863754`、`hl-fleet-service` @ `99fb369ba`(deploy-status.sh 实测部署登记,测试网关 `https://api.test.1814.love`);`git merge-base --is-ancestor ddea7e710c ff6863754` 与 `... 99fb369ba` 均为真,确认本单所在提交已随两个服务的当前部署一并上线。
```
✓ GET /v3/admin/order/group-batch/2104839654727618562(业务 admin):真实返回
requirementConfirmed=false, requirementReopenPendingHouseholds=1, requirementReopenResourceType="VEHICLE"
✓ GET /v3/admin/house/group-batches/2104839654727618562(房务角色):同一团返回同一取值,
三端一致;附带既有字段 pendingReviewHouseholds=0,印证两套口径不对账
✓ GET /admin/fleet/group-dispatch/batches/2104839654727618562/overview(车务角色,roleId=5):
同一团返回同一取值,三端一致
✓ GET /v3/admin/house/group-batches?scope=ALL&pageSize=50(房务角色):真实返回列表,
命中已认领团(groupBatchId=2104838272570245121)为已确认状态,
requirementReopenPendingHouseholds=null、requirementReopenResourceType=null,验证了 null 基线分支
```
注:H1 列表口本轮未独立捕获非空实例——T26-3963(本次用于交叉验证的团)未被房务认领,不出现在 H1 的可见集合里;H1 与其余三端共用同一个 `GroupBatchRequirementReopenHintService` 产出口,字段契约同源,此限制是列表口「仅显示已认领团」这一既有边界导致的取样限制,不是实现差异。
`#8548` 确认端点在整团免车分支释放 TRANSFER 需求的修复,本轮未做真实原子调用验证(该端点为写端点,会推进团级需求状态,测试服现存数据上误调用有污染业务状态的风险);已按源码逐行核实:`GroupBatchRequirementService.java` 505-593 行(`doConfirm` 内核 javadoc 第三段与 569-572 行分支代码)、1161-1296 行(放行集合装配 javadoc 与 `waivedVehicleSnapshot` 方法 javadoc)。前端如需验证该行为,应在确认后核对响应里 `transferDispatchedOrderIds` 是否包含预期订单,而不是依赖某个新字段(该端点响应结构本次未变)。
---
## 十、相关文档
- 关联 Issue: [wx/HL#8548](https://git.1814.love/wx/HL/issues/8548)、[wx/HL#8549](https://git.1814.love/wx/HL/issues/8549)
- 关联 PR: [wx/HL#8586](https://git.1814.love/wx/HL/pulls/8586)
## 关联 / 联系人
### 链接
- **Issue**: [#8548](https://git.1814.love/wx/HL/issues/8548)、[#8549](https://git.1814.love/wx/HL/issues/8549)
- **PR**: [#8586](https://git.1814.love/wx/HL/pulls/8586)
- **Merge commit**: [ddea7e710c](https://git.1814.love/wx/HL/commit/ddea7e710c)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,262 @@
---
schema: "hl-changelog/v2"
ticket: "8556"
title: "派单看板订单详情识别团期配车,排车节点与实派车辆数不再只认逐户派车行"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "585a59b06e4c63c4b31da95d6de4ce7d16d1f245"
target_release: ""
verified_at: "2026-09-30"
status_note: "PR #8583 合并 dev-v3(9c7ac93829);hl-fleet-service dev-v3 分支部署测试网关 @ 9c7ac9382 并实测:团期订单详情下发 groupBatchId/groupDispatchManaged/groupDispatchReady/groupDispatchPlan,排车节点由 WAITING 改为 SKIPPED,actualVehicleCount 由团期子订单实测的 0 变为与团期配车总览一致的实派车数;同一订单可同时存在团期配车与个人派车两组数据且互不覆盖。;前端已交付:OrderDrawer 新增「团期统一配车」只读卡(实派车数 null 显未知/ready=false 警告/团级配车行未落车未落司机兜底/与逐户派车并存),SKIPPED 既有分支覆盖,spec 4 例全绿(hl-admin 585a59b0)"
updated_at: "2026-09-30"
base: "dev-v3"
---
# hl-fleet-service: 派单看板订单详情识别团期配车,排车节点与实派车辆数不再只认逐户派车行
> **存放目录**: `changelogs-v2/2026-09/`
> **服务**: hl-fleet-service (端口 8087)
> **PR**: #8583
> **Issue**: #8556
> **日期**: 2026-09-30
> **影响范围**: 管理后台派单看板订单详情端点 `GET /admin/fleet/board/orders/{orderId}`
---
## ⚠️ 关键变化
- 🔴 **破坏性变更**:`actualVehicleCount` 字段的响应类型声明一直是 `Integer`(可空),但改动前的实现从未真正下发过 `null`——本次改动起,**团期子订单在团期配车事实暂不可用时会真实下发 `null`**(触发条件:`groupDispatchManaged=true` 且 `groupDispatchReady=false`)。前端若曾经把该字段当作恒为数字直接做算术或比较,现在必须先判空。
- 新增 4 个字段:`groupBatchId`(当前归属运营团期 ID,非团期订单为 `null`)、`groupDispatchManaged`(用车是否由团期统一编排)、`groupDispatchReady`(团期配车事实是否已取到,`false` 含义是"未知"不是"没有车")、`groupDispatchPlan`(本单所在乘车分组的团级配车行只读列表)。
- 团期子订单(`groupDispatchManaged=true`)且本地没有任何逐户派车行时,第 2 步"排车"(`code=DISPATCH`)的状态由 `WAITING` 改为 **`SKIPPED`**(`SKIPPED` 是该字段既有的合法取值,非新增枚举值);语义是"本步不由本单单独执行",不是"未排车"。
- `actualVehicleCount` 的计算口径变化:团期子订单现在统计"本单逐户派车 ∪ 本单所在乘车分组的团级配车"去重后的车辆并集,不再只数逐户派车行。
- 团期配车(团级统一编排)与逐户接送机派车可以**同时存在于同一张订单**,二者互不覆盖:`dailyVehiclePlan`(逐户派车)与 `groupDispatchPlan`(团级配车)各自独立返回,`currentAssignment` 仍然只指向逐户派车行。
- 本次**只改了** `assignment == null`(本地无任何逐户派车行)这一分支的排车节点状态;订单若同时存在逐户派车行,排车节点继续如实反映那条真实派车行的状态,不受团期标记影响。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 派单看板订单详情 | GET | `/admin/fleet/board/orders/{orderId}` | 🔴 破坏性变更 + 字段新增 | 新增团期配车相关 4 字段,`actualVehicleCount` 可为 `null`,排车节点新增 `SKIPPED` 用法 |
---
## 三、接口详情
### 1. 派单看板订单详情 `GET /admin/fleet/board/orders/{orderId}`
**VO**: `(无请求体,仅路径参数)` → `BoardOrderDetailVO`
#### 使用场景
车务打开派单弹窗 Step1 查看当前订单详情时调用。本次改动解决团期子订单在本端点与团期配车总览端点给出相反结论的问题:团期已排车的订单此前在本端点被画成"未排车 / 0 辆"。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Path | Long | ✅ | - | 订单 ID |
#### 出参字段表
以下是本次新增/变化的字段;其余既有字段(`dailyVehiclePlan`、`currentAssignment`、`progressSteps` 等)结构未变,此处不重复列出。
| 字段 | 类型 | 说明 |
|------|------|------|
| groupBatchId | Long→String,可空 | 当前归属运营团期 ID(非团期订单为 `null`);活体优先、order-v3 降级时回退派车行建行快照 |
| groupDispatchManaged | Boolean,可空 | 用车是否由团期统一编排;`true`=排车节点为 `SKIPPED`,车辆事实见 `groupDispatchPlan` |
| groupDispatchReady | Boolean,可空 | 团期配车事实是否已取到;`false`=未知(非团期基线不可达或该团尚无活跃用车需求),**不是**"没有车";非团期订单为 `null` |
| actualVehicleCount | Integer,🔴 可空 | 车务当前实派车辆数(团期子订单含团级配车去重并集);`null`=团期配车事实暂不可用,前端不得按 `0` 渲染 |
| groupDispatchPlan | `List<GroupDispatchPlanVO>` | 本单所在乘车分组的团级配车(只读展示,按服务日、配车行 ID 升序) |
| ├─ dispatchId | Long→String | 团级配车行 ID(排障定位用,不作为任何写口入参) |
| ├─ tripDate | LocalDate | 服务日 |
| ├─ groupCode | String | 本单当日所在乘车分组键(`order_group_vehicle_group.group_code`) |
| ├─ vehicleId | Long→String,可空 | 车辆 ID(团级配车允许未落车,此时为 `null`) |
| ├─ vehiclePlate | String | 车牌 |
| ├─ vehicleModel | String | 车型名 |
| ├─ driverId | Long→String,可空 | 司机 ID(团级配车允许未落司机,此时为 `null`) |
| ├─ driverName | String | 司机姓名 |
| ├─ driverPhone | String | 司机手机(已脱敏) |
| └─ status | String | 团级配车行状态(原样透出 `fleet_group_dispatch.status`,如 `ASSIGNED`/`CONFIRMED`) |
#### 请求示例
```http
GET /admin/fleet/board/orders/2104840641597030402
```
#### 响应示例
真实实测(团期订单 A,团号 26-3682,仅团期配车、无个人派车行)关键字段:
```json
{"code":200,"data":{"id":"HL20260929154809598","teamNo":"26-3682","groupBatchId":"2104840641651556353","groupDispatchManaged":true,"groupDispatchReady":true,"actualVehicleCount":1},"success":true}
```
对照:非团期订单(团号 26-0013):
```json
{"code":200,"data":{"id":"HL20260924152729671","teamNo":"26-0013","groupBatchId":null,"groupDispatchManaged":false,"groupDispatchReady":null},"success":true}
```
#### 空数据 / 降级响应
- 非团期订单:`groupBatchId`/`groupDispatchManaged`/`groupDispatchReady` 均为 `null`/`false`,`groupDispatchPlan` 为空列表,`actualVehicleCount` 按逐户派车行正常计数(不受本次改动影响,不会是 `null`)。
- 团期订单但团期配车事实取不到(`groupDispatchReady=false`,如 order-v3 团期基线不可达、或该团尚无活跃正式用车需求):`groupDispatchPlan=[]`,`actualVehicleCount=null`——前端应渲染为"团期配车信息暂不可用"这一类提示,不得退化显示为"未排车 / 0 辆"。
- 团期分组已铺开但本单不在任何乘车分组(整团免车 / 本单自理):`groupDispatchReady=true` 且 `groupDispatchPlan=[]`,这是已验证的业务结论(本单不占团期用车),与上一条"未知"态不同,不要混为一谈。
#### 错误响应
本端点既有错误码未变:
```json
{"code":605311,"message":"当前需求存在多个不透明派车方案代际","data":null,"success":false}
```
#### 业务边界
- 🔴 `actualVehicleCount` 从本次起可以真实为 `null`:判定条件是 `groupDispatchManaged=true && groupDispatchReady=false`。非团期订单、以及团期配车已就绪的订单,该字段仍是非空整数。
- 排车节点 `SKIPPED` 只出现在"本地无任何逐户派车行 + 团期统一编排"这一种情况;订单若同时有逐户派车行,排车节点继续如实反映那条派车行的真实状态(`WAITING`/`PROCESSING`/`DONE`/`CANCELED`),团期标记不覆盖它。
- `groupDispatchReady=false` 的含义是"未知",不是"没有车";只有 `groupDispatchReady=true` 且 `groupDispatchPlan=[]` 才是"本单确实不占团期用车"这个已验证的业务结论。
- `groupDispatchPlan` 是只读展示,团期用车的修改入口在团期配车总览页,不在本端点。
---
## 四、契约约束与正确调用方式
> 本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload / 响应 |
|------|-----------------|
| ✅ 渲染 `actualVehicleCount` 前先判空 | `null` 时渲染为"暂不可用",非 `null` 时按数字展示 |
| ✅ 判断"本单是否不占团期用车" | 必须同时看 `groupDispatchReady===true && groupDispatchPlan.length===0`,不能只看 `groupDispatchPlan` 是否为空数组 |
| ❌ 继续把 `actualVehicleCount` 当作恒不为空的数字直接参与计算 | 团期未就绪场景会拿到 `null`,直接参与算术会产生运行时异常 |
| ❌ 把排车节点 `SKIPPED` 当作未知枚举值兜底处理 | `SKIPPED` 是 `AssignmentProgressStatusEnum` 既有取值,前端 `allowableValues` 已包含,无需新增分支兜底逻辑,但需要有对应的展示文案 |
### 切换状态时的必要动作
前端渲染 `actualVehicleCount` 与排车进度节点前,必须先读 `groupDispatchManaged`/`groupDispatchReady` 两个标记决定展示分支;直接复用非团期订单的展示逻辑会在团期订单上产生误导性的"0 辆 / 未排车"提示。
---
## 五、数据库行为
本端点为只读查询,无数据库写操作。新增的团期配车事实来自跨服务只读查询:`groupDispatchManaged=true` 时才会额外发起一次到 order-v3 的 Feign 调用取团期基线,非团期订单不受影响、不多打这次调用。查询失败或该团无活跃需求时返回"未知"态(`groupDispatchReady=false`),不抛异常、不影响本端点其余字段的正常返回。
---
## 六、边界行为
- 非团期订单 → `groupBatchId`/`groupDispatchReady` 为 `null`,`groupDispatchManaged=false`,`groupDispatchPlan=[]`,`actualVehicleCount` 按逐户派车行正常计数
- 团期订单、团期配车基线不可达或该团无活跃需求 → `groupDispatchReady=false`,`groupDispatchPlan=[]`,`actualVehicleCount=null`
- 团期订单、团期分组已铺开但本单不在任何分组 → `groupDispatchReady=true`,`groupDispatchPlan=[]`,`actualVehicleCount` 按逐户派车行计数(可能为 0,这是已验证结论不是未知态)
- 团期订单、团期配车已就绪且本单在某分组 → `groupDispatchReady=true`,`groupDispatchPlan` 非空,`actualVehicleCount` 为逐户 ∪ 团级去重后的并集大小
- 本地无逐户派车行 + 团期统一编排 → 排车节点 `SKIPPED`
- 本地有逐户派车行(不论是否团期订单)→ 排车节点如实反映该派车行状态,不受团期标记影响
- 存量 `group_id` 为 `NULL` 的团级配车行(`V20260916_002` 迁移前落库、明确不回填)不进入本单的 `groupDispatchPlan`,但不报错、不影响其它行
---
## 六.5、枚举 / 数据字典
### 排车进度节点状态(`AssignmentProgressStatusEnum`,`progressSteps[].status`,`code=DISPATCH` 这一步)
**所属字段**: `progressSteps[].status`(当 `progressSteps[].code=DISPATCH`) | **类型**: `String`
| 值 | 中文 | 本次是否新增 | 说明 |
|----|------|------|------|
| `WAITING` | 等待中 | 既有值 | 非团期订单本地无派车行时的状态,本次未变 |
| `PROCESSING` | 进行中 | 既有值 | 本次未变 |
| `DONE` | 已完成 | 既有值 | 本次未变 |
| `CANCELED` | 已取消 | 既有值 | 本次未变 |
| `SKIPPED` | 已跳过 | 本次起用于排车节点 | 团期统一编排且本地无逐户派车行时的新用法;该取值本身已在 VO `allowableValues` 中存在(此前用于其它步骤),本次是新增了"排车"这一步会用到它 |
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `groupBatchId` | 不存在 | 新增,`Long→String`,可空 |
| `groupDispatchManaged` | 不存在 | 新增,`Boolean`,可空 |
| `groupDispatchReady` | 不存在 | 新增,`Boolean`,可空 |
| `groupDispatchPlan` | 不存在 | 新增,`List<GroupDispatchPlanVO>`(10 个子字段,见出参字段表) |
| `actualVehicleCount` | 字段声明类型一直是 `Integer`,但实现从未真正下发过 `null`(内部局部变量此前是不可空计算) | 团期子订单在配车事实未就绪时,Service 层真实计算出 `null` 并下发 |
### 行为级对比
| 场景 | 改前 | 改后 |
|------|------|------|
| 团期子订单、本地无逐户派车行 | 排车节点 `WAITING`,`actualVehicleCount=0` | 排车节点 `SKIPPED`,`actualVehicleCount` 取团期配车去重实派车数(就绪时)或 `null`(未就绪时) |
| 团期子订单、同时有逐户派车行 | 排车节点按该派车行真实状态 | 不变,仍按该派车行真实状态 |
| `actualVehicleCount` 统计口径(团期子订单) | 只数本单逐户派车行 | 本单逐户派车 ∪ 本单所在乘车分组的团级配车,去重后的并集 |
| 非团期订单 | 无本次描述的任何字段/行为 | 无变化(新增字段均为 `null`/`false`,`actualVehicleCount` 计算口径不变) |
## 六.7、影响评估
- **是否破坏向后兼容**: 是——`actualVehicleCount` 的字面类型虽然一直是 `Integer`,但运行时从未观测到过 `null`;前端若曾经把它当作恒为数字的字段直接做算术/比较,现在会在团期未就绪场景下遇到真实的 `null`。
- **前端是否必须同步上线**: 是(仅对涉及团期订单展示的场景)——非团期订单的响应字段与行为完全不变,可以不改;但只要页面会展示团期订单,就必须先对 `actualVehicleCount` 判空,并依据 `groupDispatchManaged`/`groupDispatchReady` 决定排车节点与实派车数的展示分支。
- **前端 workaround 清理点**: 若此前为"团期订单详情显示未排车/0 辆,但团期配车总览显示已排车"这类矛盾现象写过特殊兼容或屏蔽逻辑,现在两端点结论已一致,可以确认不再需要。
---
## 七、不影响范围
- **仅影响**: 派单看板订单详情端点 `GET /admin/fleet/board/orders/{orderId}` 的响应字段与团期子订单的排车节点/实派车数展示逻辑。
- **零影响**:
- 派单看板列表端点 `GET /admin/fleet/board/orders`(`BoardOrderRecordVO`)未受本次改动波及
- 非团期订单的响应字段与行为
- 接送机步骤(`PICKUP_DROPOFF`)与确认执行步骤(`CONFIRM_EXECUTE`)的判定逻辑
- `dailyVehiclePlan`(逐户派车方案)的既有字段结构与计算口径
- 团期配车总览/就绪判定等团期配车域自身的写口与其余读口
---
## 八、测试环境已验证
服务:`hl-fleet-service`,dev-v3 分支部署测试网关 @ `9c7ac9382`(含 #8556 所在提交),测试网关 `https://api.test.1814.love`;样本均为测试服现存真实业务数据:
```
✓ 团期订单 A(orderId=2104840641597030402,团号 26-3682,团期批次 2104840641651556353,仅团期配车无个人派车):
groupBatchId="2104840641651556353" groupDispatchManaged=true groupDispatchReady=true
progressSteps[DISPATCH].status=SKIPPED statusLabel=已跳过 active=false
actualVehicleCount=1;groupDispatchPlan 共 7 条(2026-11-11~2026-11-17)
与同批次团期配车总览 GET /admin/fleet/group-dispatch/batches/2104840641651556353/overview 对照:
vehicleReady=true,7 天 dispatched=true,结论一致(改前两端点结论相反)
✓ 非团期订单 C(orderId=2103023501973848066,团号 26-0013):
groupBatchId=null groupDispatchManaged=false groupDispatchReady=null
✓ 团期订单 B(orderId=2104839654652121090,同时有团期配车与个人派车):
groupDispatchManaged=true groupDispatchReady=true actualVehicleCount=2
dailyVehiclePlan:1 条,车辆蒙C10E10/司机铁木尔(个人派车)
groupDispatchPlan:多条,首条车辆蒙A-K1999/司机巴特尔(团期配车)
两组数据同时非空、互不顶替;currentAssignment 仍指向个人派车行(蒙C10E10/铁木尔)
```
注:`actualVehicleCount=null` 这一具体取值未在本轮实测中被真实触发(测试服 order-v3 全程可达,两个团期样本 `groupDispatchReady` 均为 `true`);该分支的契约(字段类型可空、触发条件 `groupDispatchManaged=true && groupDispatchReady=false`)已在源码逐一核实(`BoardOrderService.java`、`BoardOrderDetailVO.java`),前端应按此契约做防御性判空,不依赖本轮是否观测到该取值。
---
## 十、相关文档
- 关联 Issue: [wx/HL#8556](https://git.1814.love/wx/HL/issues/8556)
- 关联 PR: [wx/HL#8583](https://git.1814.love/wx/HL/pulls/8583)
## 关联 / 联系人
### 链接
- **Issue**: [#8556](https://git.1814.love/wx/HL/issues/8556)
- **PR**: [#8583](https://git.1814.love/wx/HL/pulls/8583)
- **Merge commit**: [9c7ac93829](https://git.1814.love/wx/HL/commit/9c7ac93829)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,416 @@
---
schema: "hl-changelog/v2"
ticket: "8559"
title: "团期子订单用车需求记录:countedHouseholdCount / countedInSummary 不再随 kind 筛选归零"
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: "PR #8612 已 squash 合并 dev-v3(3ecf387979),hl-order-service-v3 dev-v3 分支已滚测试服。本条只改取值语义,字段名、字段个数、HTTP 形态全部不变。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# hl-order-service-v3: 团期子订单用车需求记录的「计入汇总」读数不再随 kind 筛选归零
> **存放目录**: `changelogs-v2/2026-09/`
> **服务**: hl-order-service-v3 (端口 8086)
> **PR**: #8612
> **Issue**: #8559
> **日期**: 2026-09-30
> **影响范围**: 团期「查看需求」Tab 用车板块逐户明细端点 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households` 的两个取值(顶层 `countedHouseholdCount`、每户 `households[].countedInSummary`)
---
## ⚠️ 关键变化
- 🔴 **这是取值语义纠错,不是字段变更**:`countedHouseholdCount` 和 `households[].countedInSummary` 的**字段名、类型、位置全部没动**,变的是它们在带 `kind` 筛选时返回的**值**。
- **改前**:带 `kind=TRANSFER` 调用时,`countedHouseholdCount` **恒为 0**,并且返回的每一户 `countedInSummary` **恒为 false**。原因是「这一户有没有被汇总计入座位」这个判据建在已经被 `kind` 截断过的需求行上——`kind=TRANSFER` 的结果集里不可能出现 TRAVEL 行,判据对每一户都落成 false。而团级汇总里那些户的座位是实实在在加进去的,同一页面两块数据互相矛盾。
- **改后**:判据改成「这一户在**全部活跃需求行**里有没有 TRAVEL 行」,与本次 `kind` 筛选无关。三种调用(不传 `kind` / `kind=TRAVEL` / `kind=TRANSFER`)下,同一户的 `countedInSummary` 取值相同,`countedHouseholdCount` 读数相同。
- **前端要做的事**:如果页面里有「`kind=TRANSFER` 时这个数恒为 0,所以隐藏/特判」这类兜底分支,**请删掉**——它现在会把正确的非零读数吞掉。另外不要再用「`countedInSummary` 全 false」去推断当前处于接送机筛选态。
- **刻意没改**:卡片出不出现**仍然随 `kind` 变**(`kind=TRANSFER` 时只报了行程用车的户不出卡),这是 #8151 起的既有行为,本次不动。
- **刻意没改**:`householdCount`(应报车户数)与 `vehicleRowCount`(需求行数)的口径,本次一个字都没动。
---
## 一、背景
团期「查看需求」Tab 的用车板块是上下两块:上面是团级汇总,下面是逐户明细。逐户明细支持按 `kind` 切换筛选(行程用车 / 接送机 / 全部)。`countedHouseholdCount` 回答的问题是「这个团有几户被汇总计入了座位」——它是用来跟上面那块汇总对账的,天然与「我现在正在看哪一类需求」无关。
| 维度 | 改前(`kind=TRANSFER`) | 改后(`kind=TRANSFER`) |
|------|------------------------|------------------------|
| `countedHouseholdCount` | 恒 `0` | 与不传 `kind` 时相同 |
| `households[].countedInSummary` | 每户恒 `false` | 与不传 `kind` 时逐户相同 |
| 卡片出现范围 | 随 `kind` 变 | 随 `kind` 变(未改) |
| 查库往返次数 | 1 次(IN 单值) | 1 次(IN 两值) |
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 团期子订单用车需求记录 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households` | 取值语义修正 | `countedHouseholdCount` 与 `households[].countedInSummary` 不再随 `kind` 筛选归零 |
---
## 三、接口详情
### 1. 团期子订单用车需求记录 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households`
**VO**: `GroupVehicleHouseholdsRespVO`
#### 使用场景
团期详情「查看需求」Tab 的用车板块下半部分(逐户明细列表)。进入 Tab 时前端默认**不传** `kind`(两类都返);用户点「行程用车 / 接送机」切页签时带上 `kind`。本端点只读,无副作用。权限点 `group-batch:view`。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `groupBatchId` | Path | Long | ✅ | 团期主订单 ID | 团期不存在时抛团期未找到错误 |
| `kind` | Query | String | ❌ | `TRAVEL` / `TRANSFER`,不传或空白 = 两类都返 | 其它取值抛 809000「用车需求类别非法」。注意与提交侧「不传按 TRAVEL」的缺省刻意相反 |
#### 出参 `Result<GroupVehicleHouseholdsRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `groupBatchId` | String | 团期主订单 ID(雪花,序列化为字符串) |
| `departDate` | String(`yyyy-MM-dd`) | 团期出发日,可为 null |
| `endDate` | String(`yyyy-MM-dd`) | 团期结束日,与团期详情同源,可为 null |
| `householdCount` | Integer | 应报车户数 = `households` 数组长度(含一份需求都没提交的空卡) |
| `vehicleRowCount` | Integer | 需求行数 = 各户 `requirements` 长度之和;**可以小于 `householdCount`**,不要当不变式用 |
| `countedHouseholdCount` | Integer | 🔴 被汇总计入座位的户数 = `households` 中 `countedInSummary=true` 的条数。**不随 `kind` 筛选变化**,三种筛选读数相同。与 `householdCount` 的差 = 只报接送机的户 + 未提交的户 |
| `households` | Array | 逐户卡片,按 `orderNo` 升序(`orderNo` 为空的排最后);单次最多 500 户 |
| `households[].orderId` | String | 子订单 ID(雪花,序列化为字符串) |
| `households[].orderNo` | String | 子订单号 |
| `households[].teamNo` | String | 团号,可为 null(不用订单号顶替) |
| `households[].customerName` | String | 客户姓名 |
| `households[].participantCount` | Integer | 出行人数 |
| `households[].consultantId` | String | 定制师 adminId,未指派为 null |
| `households[].consultantName` | String | 定制师姓名,未指派为 null |
| `households[].countedInSummary` | Boolean | 🔴 该户是否被团级汇总计入座位。**不随 `kind` 筛选变化**,同一户三种筛选下取值相同 |
| `households[].status` | String | 展示用需求状态;该户一份需求都没提交时为 null |
| `households[].statusName` | String | 状态中文名,`status` 为 null 时为 null |
| `households[].requirements` | Array | 该户活跃需求行,0~2 条;无行时为**空数组**不是 null |
| `households[].requirements[].requirementId` | String | 需求行 ID(雪花,序列化为字符串) |
| `households[].requirements[].kind` | String | `TRAVEL` / `TRANSFER` |
| `households[].requirements[].kindName` | String | 类别中文名 |
| `households[].requirements[].status` | String | `PENDING_REVIEW` / `PENDING` / `PROCESSING` / `DONE` |
| `households[].requirements[].statusName` | String | 状态中文名 |
| `households[].requirements[].fleet` | Array | 车型项:`vehicleType` / `vehicleTypeName` / `seats` / `count` |
| `households[].requirements[].specialTags` | Array | 特殊诉求标签:`code` / `name` |
| `households[].requirements[].remark` | String | 备注,≤500,可为 null |
| `households[].requirements[].serviceDates` | Array\<String\> | 服务日期(`yyyy-MM-dd`) |
| `households[].requirements[].headcount` | Integer | 用车人数 |
| `households[].requirements[].totalSeatCount` | Integer | 总座位数 |
| `households[].requirements[].remainingPassengerSeats` | Integer | 余座(扣司机位后) |
| `households[].requirements[].pickupRequired` | Boolean | **仅 TRANSFER 行有值**,TRAVEL 行为 null |
| `households[].requirements[].dropoffRequired` | Boolean | **仅 TRANSFER 行有值**,TRAVEL 行为 null |
| `households[].requirements[].returnRemark` | String | 打回备注,可为 null |
| `households[].requirements[].returnedAt` | String(date-time) | 打回时刻,可为 null |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2101690789438570497/requirement/vehicle-households?kind=TRANSFER HTTP/1.1
Host: <admin-gateway>
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"groupBatchId": "2101690789438570497",
"departDate": "2026-09-12",
"endDate": "2026-09-16",
"householdCount": 3,
"vehicleRowCount": 1,
"countedHouseholdCount": 2,
"households": [
{
"orderId": "2102000000000000001",
"orderNo": "HL20260912100000001",
"teamNo": "26-0480",
"customerName": "陈昊",
"participantCount": 4,
"consultantId": "10001",
"consultantName": "李四",
"countedInSummary": true,
"status": "PENDING",
"statusName": "待车队配",
"requirements": [
{
"requirementId": "2103000000000000011",
"kind": "TRANSFER",
"kindName": "接送机",
"status": "PENDING",
"statusName": "待车队配",
"fleet": [
{ "vehicleType": "suv", "vehicleTypeName": "SUV", "seats": 7, "count": 1 }
],
"specialTags": [
{ "code": "child_seat", "name": "儿童安全座椅" }
],
"remark": null,
"serviceDates": ["2026-09-12"],
"headcount": 4,
"totalSeatCount": 7,
"remainingPassengerSeats": 2,
"pickupRequired": true,
"dropoffRequired": false,
"returnRemark": null,
"returnedAt": null
}
]
},
{
"orderId": "2102000000000000002",
"orderNo": "HL20260912100000002",
"teamNo": "26-0480",
"customerName": "王琳",
"participantCount": 2,
"consultantId": "10001",
"consultantName": "李四",
"countedInSummary": true,
"status": null,
"statusName": null,
"requirements": []
},
{
"orderId": "2102000000000000003",
"orderNo": "HL20260912100000003",
"teamNo": null,
"customerName": "赵敏",
"participantCount": 3,
"consultantId": null,
"consultantName": null,
"countedInSummary": false,
"status": null,
"statusName": null,
"requirements": []
}
]
}
}
```
> 上例即改后行为:`kind=TRANSFER` 筛选下仍然有 `countedHouseholdCount=2`(前两户各有一条活跃 TRAVEL 行,被团级汇总计入了座位),只有第三户没报行程用车所以是 `false`。改前这三户的 `countedInSummary` 会全是 `false`、`countedHouseholdCount` 会是 `0`。
#### 空数据 / 降级响应
团期下没有在团子订单(全部退团 / 取消)时,三个计数全 `0`、`households` 为**空数组**(不是 null):
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"groupBatchId": "2101690789438570497",
"departDate": "2026-09-12",
"endDate": "2026-09-16",
"householdCount": 0,
"vehicleRowCount": 0,
"countedHouseholdCount": 0,
"households": []
}
}
```
单次返回的户数上限为 500 户,超过时**截断**返回前 500 条并在服务端留 warn,不报错——页面仍可用。截断后 `householdCount` / `vehicleRowCount` / `countedHouseholdCount` 都按截断后的列表重算,三者与 `households` 数组始终自洽。
#### 错误响应
`kind` 传了 `TRAVEL` / `TRANSFER` 以外的值:
```json
{
"code": 809000,
"message": "用车需求类别非法:BUS",
"success": false,
"data": null
}
```
#### 业务边界
- **鉴权**:需要权限点 `group-batch:view`;未登录由网关拦截返 401。
- **只读**:本端点零写入,重复调用无副作用,可安全轮询。
- **户范围**:在团 = 仅排除 CANCELLED。退团 / 取消的户不出现在列表里。
- **只取活跃行**:被打回的需求行已失活,**不在本列表内**(与用房侧「列出打回户」的行为刻意不同)。因此一户被打回后在这里表现为 `requirements: []` 的空卡,而不是灰条。
- **卡片范围仍随 `kind` 变**:卡片集合 = 「应报车的户」∪「本次筛选命中需求行的户」。这一条本次未改。
- **`countedInSummary` 不随 `kind` 变**:它读的是该户在**全部**活跃行里有没有 TRAVEL 行。
- **`pickupRequired` / `dropoffRequired` 只在 TRANSFER 行有值**,TRAVEL 行恒 null,不要用 `false` 去区分。
- **雪花 ID 一律是字符串**:`groupBatchId` / `orderId` / `consultantId` / `requirementId` 都以字符串下发,JS 直接当数字用会被静默截断。
---
## 四、契约约束与正确调用方式
> 本节只写后端接受 / 拒绝请求的规则,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误调用对照
| 场景 | 请求 |
|------|------|
| ✅ 进 Tab 默认拉全部 | `GET .../vehicle-households`(不带 `kind`) |
| ✅ 切到行程用车页签 | `GET .../vehicle-households?kind=TRAVEL` |
| ✅ 切到接送机页签 | `GET .../vehicle-households?kind=TRANSFER` |
| ✅ 显式传空值 | `GET .../vehicle-households?kind=`(空白按「两类都返」处理) |
| ❌ 传车型当类别 | `GET .../vehicle-households?kind=bus` → 809000 |
| ❌ 传小写类别 | `GET .../vehicle-households?kind=transfer` → 809000 |
### 切换筛选时的必要动作
切换 `kind` 时,**顶部「计入汇总 N 户」这类读数不需要跟着置灰或隐藏**——它在三种筛选下是同一个数。如果前端此前为了绕开恒 0 做过「接送机页签下不展示该数」的兜底,现在应当删掉,否则接送机页签会永远看不到这个已经正确的读数。
---
## 五、数据库行为
本端点是 **GET 只读**,零写入:不建行、不改行、不软删、不产生任何 outbox / MQ 事件。
本次改动只调整了服务端的**读法**(原先按 `kind` 下推到查询条件,现在恒查两类再在内存里按 `kind` 分流),SQL 往返次数未变(同一条 IN 查询,`requirement_kind` 的 IN 列表从 1 个值放宽到 2 个值)。
---
## 六、边界行为
- 未登录 → 401(网关拦截)。
- 权限点缺失 → 权限校验失败,不返回数据。
- 团期 ID 不存在 → 抛团期未找到业务错误,HTTP 200 + 业务错误码。
- 团期下无在团子订单 → 三个计数 0 + `households: []`,不报错。
- 户数超 500 → 截断到前 500 条,三个计数按截断后重算,HTTP 200。
- 在团订单 ID 存在但订单行缺失(脏数据)→ 跳过该户并在服务端留 warn,整页仍可打开。
- 需求行 `requirement_kind` 为空的脏行 → 显式跳过,不进任何计数。
- 老数据兼容:历史需求行缺 `seats` / `count` 等字段时对应位为 null,不异常。
---
## 六.5、枚举 / 数据字典
### kind(用车需求类别,`VehicleRequirementKind`)
**所属字段**: 请求 Query `kind`、响应 `households[].requirements[].kind` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `TRAVEL` | 行程用车 | 团期行程期间的用车需求;`countedInSummary` 的判据只认这一类 |
| `TRANSFER` | 接送机 | 接送机 / 接送站需求;`pickupRequired` / `dropoffRequired` 只在这一类上有值 |
请求侧不传或传空白 = 两类都返(不是「默认 TRAVEL」)。
### status(需求行状态)
**所属字段**: `households[].status`、`households[].requirements[].status` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `PENDING_REVIEW` | 待审核 | 定制师已提交,等团期管理员下发车务 |
| `PENDING` | 待车队配 | 已下发车务,等车队配车 |
| `PROCESSING` | 配车中 | 车务处理中 |
| `DONE` | 配车完成 | 已完成 |
户级 `status` 为 null 表示该户一份活跃需求都没有(空卡)。
---
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `countedHouseholdCount` | 字段存在,类型 Integer;`kind=TRANSFER` 时恒 `0` | 字段、类型不变;三种筛选读数相同 |
| `households[].countedInSummary` | 字段存在,类型 Boolean;`kind=TRANSFER` 时恒 `false` | 字段、类型不变;同一户三种筛选取值相同 |
| 其余全部字段 | — | 未变(无新增、无删除、无改名、无类型变化) |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 不传 `kind` 时的两个读数 | 正确 | 正确(未变) |
| `kind=TRAVEL` 时的两个读数 | 正确 | 正确(未变) |
| `kind=TRANSFER` 时的两个读数 | `countedHouseholdCount=0`、每户 `countedInSummary=false` | 与不传 `kind` 时一致 |
| 「计入汇总」的判据数据源 | 已被 `kind` 截断的需求行 | 该户的全部活跃需求行 |
| 卡片是否随 `kind` 变 | 变 | 变(未改) |
| `householdCount` / `vehicleRowCount` | — | 未变 |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 否(无字段增删改名;只是 `kind=TRANSFER` 时的取值由恒 0 / 恒 false 变为真实值)
- **前端是否必须同步上线**: 否(不改也能正常渲染,只是接送机页签下该数从「永远 0」变成真实值)
- **前端 workaround 清理点**: 若页面里有「`kind=TRANSFER` 时 `countedHouseholdCount` 恒 0,所以隐藏该数 / 走另一套算法 / 用 `countedInSummary` 全 false 判断当前筛选态」这类兜底分支,**删掉它们**——保留会吞掉正确读数或做出错误的筛选态判断。
---
## 七、不影响范围
- **仅影响**: `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households` 的 `countedHouseholdCount` 与 `households[].countedInSummary` 两个取值。
- **零影响**:
- 团级用车汇总端点(本次改的是明细侧读法,汇总侧一个字没动)
- 团期正式用车需求的存 / 读 / 撤回 / 免车四个端点
- 用房侧 `requirement/hotel-households`
- 单户用车需求的提交、下发、打回链路
- 团期配车(fleet 侧)任何端点
- 历史数据:本次只改读法,不做任何数据迁移
---
## 八、测试环境已验证
- **代码事实**(对 `origin/dev-v3` 逐一查证):
- 合并提交 `3ecf387979`(PR #8612 squash 合并进 `dev-v3`)。
- `GroupBatchVehicleHouseholdService#households` 查库改为恒取 `TRAVEL` + `TRANSFER` 两类,另建 `travelOrderIds` 集合承载「计入汇总」判据;`buildHousehold` 签名增加 `boolean countedInSummary` 形参,替代原先在被截断的 `rows` 上做的 `anyMatch`。
- `GroupVehicleHouseholdsRespVO#countedHouseholdCount` 与 `HouseholdItem#countedInSummary` 的 `@ApiModelProperty` 已同步写明「不随 kind 筛选变化,三种筛选读数相同」。
- 卡片集合仍取自按 `kind` 过滤后的 `rowsByOrder`(既有行为,未改)。
- **部署**:`hl-order-service-v3` 的 `dev-v3` 分支已滚到测试服,本端点走管理端网关 `/v3/admin/**` 既有通配路由,无新增路由。
```
GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households → 200 ✓
GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households?kind=TRAVEL → 200 ✓
GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households?kind=TRANSFER → 200 + countedHouseholdCount 与前两次一致 ✓
```
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| — | #8151 | 首次提供本端点(逐户明细 + `kind` 筛选) | ✅ 有效 |
| — | #8195 | 缺陷 2:`householdCount` 改为「应报车户数」含空卡;缺陷 5:`endDate` 与团期详情同源 | ✅ 有效 |
| **本 PR #8612** | **#8559** | 「计入汇总」判据改为不受 `kind` 截断 | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#8559](https://git.1814.love:8443/wx/HL/issues/8559)
- 关联 PR: [wx/HL#8612](https://git.1814.love:8443/wx/HL/pulls/8612)
## 关联 / 联系人
### 链接
- **Issue**: [#8559](https://git.1814.love:8443/wx/HL/issues/8559)
- **PR**: [#8612](https://git.1814.love:8443/wx/HL/pulls/8612)
- **Merge commit**: [3ecf387979](https://git.1814.love:8443/wx/HL/commit/3ecf387979)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,237 @@
---
schema: "hl-changelog/v2"
ticket: "8560"
title: "矩阵未派订单清单响应新增用车需求类别字段,同订单双需求两张卡可区分"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "8d23a3815ee13e252eba253a32e2b6e7756ef220"
target_release: ""
verified_at: "2026-09-30"
status_note: "PR #8589 合并 dev-v3(8b045a321b);测试网关部署确认:hl-fleet-service 现部署 @ 99fb369ba(deploy-status.sh 实测,状态 ok,该 SHA 经 git merge-base --is-ancestor 确认已包含 8b045a321b)。GET /admin/fleet/matrix/unassigned-orders 已用车务角色测试账号实测:2026-09 月拿到 TRAVEL 示例(订单 HL20260911193207642)、2026-11 月拿到 TRANSFER 示例(订单 HL20260929154809598),均为测试服真实响应;对 2026-06~2027-03 共 10 个月窗口扫描未发现 requirementKind=null 或同订单双卡的活跃实例,这两种边界行为当前仅由单元测试覆盖(BoardRequirementIdentitiesKindTest 5/5、MatrixServiceTest 新增 6 个 #8560 方法),尚未在测试服活数据上复现。;前端已交付:矩阵主窗未派池卡与分窗甘特条加类别标签(null 不渲染不兜底 TRAVEL),适配层显式透传,spec 6 例全绿(hl-admin 8d23a381)"
updated_at: "2026-09-30"
base: "dev-v3"
---
# hl-fleet-service:矩阵未派订单清单响应新增用车需求类别字段,同订单双需求两张卡可区分
> **存放目录**: `changelogs-v2/2026-09/`
> **服务**: hl-fleet-service
> **PR**: #8589
> **Issue**: #8560
> **日期**: 2026-09-30
> **影响范围**: 1 个只读端点响应新增 2 字段(矩阵未派订单清单)
---
## ⚠️ 关键变化
- `GET /admin/fleet/matrix/unassigned-orders` 响应每条记录新增 `requirementKind`(`TRAVEL`/`TRANSFER`/`null`)与 `requirementKindLabel`(`行程用车`/`接送机`/`null`),两者恒成对(一个为 null 另一个必为 null)。
- 背景(#7439):同一订单可并存两条活跃用车需求(行程用车 TRAVEL + 接送机 TRANSFER),车务分别对两者派车,未派池会出现同一订单的两张卡。此前两张卡除车型/日期外没有任何字段能分辨谁是哪一类——既有字段 `vehicleCategory`/`categoryLabel` 是车型(suv/bus)不是需求类别。新增这两个字段就是用来分辨这两张卡的。
- 🔴 **`null` 不兜底成 `TRAVEL`**,这是本次修复的核心边界。判不出类别(跨服务降级 context=null;或派车行挂着 #5720 换版过渡窗里的上一版 `requirement_id`,命中不了任何当前活跃身份;或命中的身份自身 `kind` 为空白)时两个新字段均为 `null`,前端应不显示类别标签,**禁止自行按业务猜测补默认值**——尤其禁止把 `null` 当 `TRAVEL` 处理。
- 与看板列表(`BoardOrderRecordVO.requirementKind`,#8518 既有)**在判不出这一档口径不同**:看板列表的解析方法判不出时兜底返 `TRAVEL`(那里类别同时是筛选维度,返空会让卡片从筛选后的视图里彻底消失);矩阵未派卡判不出时返 `null`(那里类别只是展示标签,车务会照标签去排完全不同的活,标错比不标更危险)。**同一张实体卡在两个入口可能显示不一致的类别信息,这是刻意保留的差异**,不是缺陷。
- 真实未派行与虚拟待派条目(`virtualPending=true`,#7067)两类条目都携带这两个新字段,取值口径一致。
- 类别取的是**这张卡自身所属需求**(真实行用该行自己的 `requirement_id`,虚拟条目用该候选自己的 `requirementId`)解析出的类别,**不是**已有字段 `requirementId`(该字段取「订单侧单值」,#5667 口径,同一订单两类需求并存时恒指向身份列表首项、即恒为 TRAVEL 那条)。⚠️ `requirementId` 字段两张卡是否相同**取决于这张卡走哪条路径**:`virtualPending=true`(未派池的虚拟待派条目,未派订单最常见的形态)下它取该候选自身的需求 ID,两张卡**不相同**;`virtualPending=false`(已有派车行的真实行)下它优先取订单侧单值,两张卡**相同**。两条路径都不能拿 `requirementId` 判类别——相同时它分辨不出,不同时它也只是碰巧对得上。判类别一律只认 `requirementKind`/`requirementKindLabel`。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 矩阵未派订单清单 | GET | `/admin/fleet/matrix/unassigned-orders` | 字段新增(非破坏性) | 响应新增 `requirementKind`/`requirementKindLabel` |
---
## 三、接口详情
### 1. 矩阵未派订单清单 `GET /admin/fleet/matrix/unassigned-orders`
**VO**: `MatrixUnassignedReqVO → List<MatrixUnassignedOrderVO>`
#### 使用场景
派单矩阵页右面板「未派订单窗口」,车务从此列表拖拽卡片到左面板某车某日完成派车;同一订单若同时有行程用车与接送机两条活跃需求,会在此列表出现两张卡,车务需要靠新增的类别字段区分要往哪类需求上派车,不能再靠车型/日期/备注这类间接信息猜。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| year | query | Integer | 是 | 2020-2100,越界返 605076 | 年份 |
| month | query | Integer | 是 | 1-12,越界返 605010 | 月份 |
| typeKeys | query | String[] | 否 | 取值 suv/mpv/bus/sedan,规范小写 | 车型大类多选,空=全部;对虚拟待派条目按当前需求车型明细任一项归一后命中过滤 |
#### 出参字段表(仅列本次新增字段及理解其语义所需的上下文字段,VO 全量共 39 个字段)
| 字段 | 类型 | 说明 |
|------|------|------|
| requirementKind | String | **新增**。用车需求类别:`TRAVEL`=行程用车 / `TRANSFER`=接送机 / `null`=判不出(不兜底为 TRAVEL) |
| requirementKindLabel | String | **新增**。类别中文名:`行程用车`/`接送机`/`null`,与 requirementKind 恒成对 |
| requirementId | Long(字符串序列化) | 既有字段,这张卡对应的用车需求 ID。`virtualPending=false` 时优先取订单侧单值(#5667,两类并存时恒指向 TRAVEL 那条);`virtualPending=true` 时取该候选自身的需求 ID。**两条路径取值口径不同,一律不能用它推导 requirementKind** |
| assignmentId | Long(字符串序列化) | 既有字段,本行唯一主键;虚拟待派条目为 null |
| virtualPending | Boolean | 既有字段,true=虚拟待派条目(零派车行订单,按需求上下文补出) |
| vehicleCategory | String | 既有字段,规范小写车型 key(suv/mpv/bus/sedan),与需求类别是两个不同维度 |
| categoryLabel | String | 既有字段,车型中文标签(恒非 null) |
| orderId / orderNo | String | 既有字段,订单号 |
| teamNo | String | 既有字段,团号 |
#### 请求示例
```http
GET /admin/fleet/matrix/unassigned-orders?year=2026&month=11&typeKeys=mpv
```
#### 响应示例
实测取自测试服真实数据(2026-11 月,TRANSFER 示例):
```json
{
"code": 200,
"message": "success",
"data": [
{
"orderId": "HL20260929154809598",
"orderNumericId": "2104840641597030402",
"orderNo": "HL20260929154809598",
"teamNo": "26-3682",
"virtualPending": true,
"assignmentId": null,
"assignmentGroupId": null,
"requirementId": "2104844928733548545",
"requirementKind": "TRANSFER",
"requirementKindLabel": "接送机",
"vehicleCategory": "mpv",
"categoryLabel": "商务车",
"customerName": "董海涛",
"headcount": 2,
"headcountLabel": "2大",
"startDate": "2026-11-11",
"endDate": "2026-11-17",
"pickupAt": "阿尔山伊尔施机场",
"dropoffAt": "阿尔山伊尔施机场",
"assignmentStatus": "unassigned",
"urgentBadge": null,
"vehicleAdvice": null,
"parallelAssignments": []
}
]
}
```
对照:2026-09 月同一账号实测取到的 TRAVEL 示例(订单 `HL20260911193207642`),响应结构完全相同,仅 `requirementKind="TRAVEL"`、`requirementKindLabel="行程用车"`、`requirementId="2098950695167148034"`。两个示例均为测试服真实取数,未做任何字段改写。
#### 空数据 / 降级响应
- 当月无未派条目:`data: []`,非错误。
- `vehicleAdvice` 恒为 `null`(M2 数据源未建,既有降级行为,与本次改动无关)。
- Nacos `fleet.board.virtual-candidates-enabled=false` 或 order-v3 候选服务不可用时:只丢虚拟待派条目,真实未派行原样返回(fail-open);真实行的 `requirementKind` 解析走独立的上下文查询,不受此开关影响。
- `requirementKind`/`requirementKindLabel` 判不出时为 `null`(见「⚠️ 关键变化」),这不是接口异常,是正常的降级取值,前端应按无标签渲染,不得折算为 `TRAVEL`。
#### 错误响应
```json
{
"code": 605010,
"message": "月份超出范围",
"data": null
}
```
```json
{
"code": 605076,
"message": "年份超出范围(仅支持 2020-2100 年)",
"data": null
}
```
- `100001` 参数非法:year/month 缺失(框架校验)。
- `401` 未登录。
#### 业务边界
- **同一订单两类需求并存时,两张卡的 `requirementKind`/`requirementKindLabel` 必不相同;而 `requirementId` 字段是否相同取决于路径**——真实行(`virtualPending=false`)下两张卡相同(均取订单侧单值),虚拟待派条目(`virtualPending=true`)下两张卡各取自身需求 ID、并不相同。单元测试 `MatrixServiceTest#queryUnassignedOrders_orderWithBothKinds_twoCardsCarryDifferentKinds` 断言的是**真实行**那条路径。两条路径都不能拿 `requirementId` 反推类别,前端也不能这么做。
- `requirementKind=null` 时前端**禁止**折算成 `TRAVEL`;这既是判不出的真实状态,也是修复前的错误行为,回退等于复发。
- 矩阵未派卡与看板列表对同一张孤儿行(#5720 换版过渡窗)的类别展示口径不同(前者 null、后者兜底 TRAVEL),这是刻意保留的差异,不要据此判断某一端有 bug。
- 真实未派行与虚拟待派条目两种类型都下发这两个字段,前端不需要按 `virtualPending` 分支处理类别逻辑。
---
## 四、契约约束与正确调用方式
- 判类别只认 `requirementKind`/`requirementKindLabel` 这两个新字段,不要用 `requirementId` 做二次推导。
- `requirementKind` 取值集合当前为 `{TRAVEL, TRANSFER, null}`,前端不应写死「非 TRANSFER 即 TRAVEL」的二值判断——若未来 order-v3 新增第三类需求,后端会同步扩展该字段取值与中文映射,二值判断会把新类别误标成 TRAVEL。
- 类别中文名由后端下发,前端不需要、也不应该自行维护 `TRAVEL`/`TRANSFER` 到中文的映射表。
---
## 五、数据库行为
无数据库结构变更。本次改动只是查询层新增两次内存解析(基于已查出的订单需求上下文按 `requirement_id` 匹配),不新增表、不新增列、不新增索引,无 Flyway 迁移。
---
## 六、边界行为
- 上下文降级(跨服务 Feign 调用失败,`OrderFleetBoardContextDTO` 为 `null`):类别字段为 `null`。
- 派车行/候选自身的 `requirement_id` 命中不到订单当前任何活跃需求身份(#5720 换版过渡窗孤儿行):类别字段为 `null`,不回退用订单上下文单值猜测。
- 命中的身份自身 `kind` 字段为空白:类别字段为 `null`(防御性分支;order-v3 当前写路径恒写枚举 `.name()`,正常不触发,仅覆盖历史/异常数据)。
- 以上三种 `null` 场景均只有单元测试覆盖(见八节),本次实测扫描未在测试服活数据中观测到对应真实记录。
---
## 六.5、枚举 / 数据字典
| 取值 | 中文标签 | 说明 |
|------|----------|------|
| TRAVEL | 行程用车 | 行程用车需求 |
| TRANSFER | 接送机 | 接送机需求(#7439 引入) |
| null | (不显示标签) | 判不出类别,前端不得兜底为 TRAVEL |
---
## 六.6、修改前后对比
- **字段层面**:`MatrixUnassignedOrderVO` 新增 `requirementKind`(String)、`requirementKindLabel`(String),VO 字段总数由 37 增至 39。
- **行为层面**:改动前,同一订单的两张未派卡在字段层面完全无法区分类别,只能靠车型/日期/备注人工判断,判断错了会把车派到错误的需求线上;改动后两张卡各自携带准确的类别标识,且判不出时明确返回 null 而非静默给出错误猜测。
---
## 六.7、影响评估
- 破坏性:无。两个新增字段为可选新增,未删除/未重命名/未改变任何既有字段的类型或取值口径。
- 涉及消费端:仅管理后台派单矩阵页。
- 前端无需为此做兼容降级处理:未取到新字段(`undefined`)与取到 `null` 应做同等处理——均不显示类别标签。
---
## 七、不影响范围
- 矩阵主数据端点 `GET /admin/fleet/matrix/grid`、年度月度统计 `GET /admin/fleet/matrix/month-counts`、当天订单清单 `GET /admin/fleet/matrix/day-orders`:均未改动。
- 看板列表端点(`BoardOrderRecordVO.requirementKind`,#8518):未改动,其判不出类别时仍兜底 TRAVEL 的既有行为不变。
- 写操作(拖拽派车、改派、取消等):本次改动只涉及查询响应字段新增,不涉及任何写路径。
- `MatrixUnassignedReqVO` 请求参数:未新增/未修改(year/month/typeKeys 均为既有字段,越界错误码路由此前已分别由 #8561/#8571 调整完成)。
---
## 八、测试环境已验证
- **部署确认**:`hl-fleet-service` 现部署 SHA `99fb369ba`(`deploy-status.sh` 实测,状态 `ok`),经 `git merge-base --is-ancestor 8b045a321b 99fb369ba8` 确认已包含本次改动的合并提交 `8b045a321b`(PR #8589)。
- **实测(真实请求,非构造数据)**:使用车务角色测试账号(切至 VEHICLE_MANAGER 角色)对 `GET /admin/fleet/matrix/unassigned-orders` 发起真实请求:
- 2026-09 月:2 条记录,`requirementKind` 均为 `TRAVEL`,含示例订单 `HL20260911193207642`(见响应示例节)。
- 2026-11 月:1 条记录,`requirementKind` 为 `TRANSFER`,订单 `HL20260929154809598`(见响应示例节)。
- 对 2026-06 ~ 2027-03 共 10 个月窗口的扫描(合计 14 条记录)未发现 `requirementKind=null` 的记录,也未发现同一订单出现两条不同类别记录的活跃实例——测试服当前业务数据里暂未出现这两种边界场景,实测未覆盖,靠下面的单元测试兜底。
- **单元测试覆盖(源码单测验证,未在测试服活数据上复现)**:
- `BoardRequirementIdentitiesKindTest`(5/5 通过):覆盖双身份按需求 ID 各取各类别、上下文降级返 null(对照既有方法仍兜底 TRAVEL)、陈旧需求 ID 不猜返 null、身份自身类别空白返 null、灰度上下文合成 TRAVEL 身份仍可取到。
- `MatrixServiceTest` 新增 6 个 `#8560` 测试方法(均通过):同订单两类需求两张卡类别互不相同(含反向对照:**真实行路径**下两张卡 `requirementId` 字段完全相同;虚拟待派路径不适用该对照)、需求身份类别空白返 null 不兜底 TRAVEL、上下文降级返 null 不兜底 TRAVEL、派车行挂陈旧需求 ID(#5720)返 null 不兜底 TRAVEL、虚拟待派条目携带类别、虚拟待派条目无身份列表时类别为 null。
- 聚合结果(`mvn -pl hl-fleet-service -am test`):`Tests run: 365, Failures: 0, Errors: 0, Skipped: 0`,`BUILD SUCCESS`;含 `VehicleRequirementKindsTest` 4、`BoardOrderServiceTest` 233、`FleetRedLineArchTest` 18(架构守护门禁绿)。
- 嵌套用例选择器守卫(`nested_selector_census`):通过,内层名比对无缺组。
- `spotless:check`:`BUILD SUCCESS`,916 文件全部合规。
---
## 十、相关文档
- Issue #8560
- PR #8589(合并提交 `8b045a321b`)
- 相关既有机制:#7439(TRAVEL/TRANSFER 双需求引入)、#8518(看板列表既有类别字段)、#7067(虚拟待派条目/去槽位化)、#5667(`requirementId` 订单侧单值口径)、#5720(换版过渡窗孤儿行)
---
## 关联 / 联系人
- 后端:wx(GIT)
- 消费端:管理后台(派单矩阵页)
@@ -0,0 +1,350 @@
---
schema: "hl-changelog/v2"
ticket: "8561"
title: "派单矩阵三个入口补年份区间校验,新增错误码 605076"
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: "PR #8565 合并 dev-v3(ad3c6e4305);hl-fleet-service dev-v3 分支部署测试网关 @ 9c7ac9382 并实测:grid/month-counts/unassigned-orders 三入口越界年份(1800/9999/1990)均返回 605076,区间内年份(含 2020/2100 两端边界,已在 grid 入口实测)行为不变。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# hl-fleet-service: 派单矩阵三个入口补年份区间校验,新增错误码 605076
> **存放目录**: `changelogs-v2/2026-09/`
> **服务**: hl-fleet-service (端口 8087)
> **PR**: #8565
> **Issue**: #8561
> **日期**: 2026-09-30
> **影响范围**: 管理后台派单矩阵三个查询入口(grid / month-counts / unassigned-orders)的年份校验
---
## ⚠️ 关键变化
- 派单矩阵三个查询入口(`grid`/`month-counts`/`unassigned-orders`)此前只校月份是否在 1-12,**不校年份**——`year=1800`、`year=9999` 这类明显异常值会被当成合法年份继续查询。现在统一在 Service 层加了年份区间校验 `[2020, 2100]`,越界抛**新增错误码 605076**。
- 605076 的错误文案是 `年份超出范围(仅支持 2020-2100 年)`(`AssignmentErrorCode.MATRIX_YEAR_OUT_OF_RANGE`,文案里的年份区间已按当前常量渲染为 2020/2100)。
- 校验顺序是**先年后月**:`year` 越界时直接抛 605076,不会先看 `month`。
- 区间内年份(含边界 2020、2100)行为完全不变,仍按原逻辑正常查询并返回 200。
- 三入口的 `year` 字段本身仍是**必填**(`@NotNull`),缺失依旧是既有的参数校验码 `100001`,本次未改动这一路径。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 矩阵主数据 | GET | `/admin/fleet/matrix/grid` | 校验补充 | 新增年份区间校验,越界返 605076 |
| 2 | 矩阵年度月度统计 | GET | `/admin/fleet/matrix/month-counts` | 校验补充 | 新增年份区间校验,越界返 605076 |
| 3 | 矩阵未派订单清单 | GET | `/admin/fleet/matrix/unassigned-orders` | 校验补充 | 新增年份区间校验,越界返 605076 |
---
## 三、接口详情
### 1. 矩阵主数据 `GET /admin/fleet/matrix/grid`
**VO**: `MatrixGridReqVO` → `MatrixGridRespVO`
#### 使用场景
车务打开派单矩阵页查看某年某月逐日车辆占用/待派情况时调用。本次改动只影响 `year` 越界场景的响应,区间内查询字段结构与既有行为未变。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| year | Query | Integer | ✅ | 🔴 新增:2020-2100(越界返 605076) | 年份 |
| month | Query | Integer | ✅ | 1-12(越界返 605010) | 月份 |
| season | Query | String | - | active/pending/archived/blacklist | 司机赛季筛选,默认 active |
| fleetTeamIds | Query | Long[] | - | - | 车队 ID 多选,空=全部 |
| fleets | Query | String[] | - | 已废弃,仅客户端迁移兼容 | 旧版车队稳定编码多选 |
| typeKeys | Query | String[] | - | 规范小写,空=全部 | 车型大类多选 |
| status | Query | String | - | all/unassigned/assigned | 兼容矩阵筛选 |
| statuses | Query | String[] | - | 非空时优先于 status | 有效状态精确筛选 |
#### 出参字段表
响应结构本次未改动,以下仅列出与本次校验相关、已实测确认的顶层字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| year | Integer | 年份(回显) |
| month | Integer | 月份(回显) |
| daysInMonth | Integer | 该月天数 |
| vehicles | List | 车辆逐日占用数据 |
#### 请求示例
```http
GET /admin/fleet/matrix/grid?year=2020&month=6&season=active
```
#### 响应示例
区间下边界(year=2020)实测:
```json
{"code":200,"message":"成功","data":{"year":2020,"month":6,"daysInMonth":30},"traceId":null,"success":true}
```
区间上边界(year=2100)实测:
```json
{"code":200,"message":"成功","data":{"year":2100,"month":6},"traceId":null,"success":true}
```
#### 空数据 / 降级响应
区间内查询若当月无任何车辆/派车数据,`vehicles` 为空数组,属正常业务结果,不是错误;本次改动不影响此形态。
#### 错误响应
```json
{"code":605076,"message":"年份超出范围(仅支持 2020-2100 年)","data":null,"traceId":null,"success":false}
```
(实测 `year=1800` 与 `year=9999` 均返回上述响应体,仅请求参数不同。)
#### 业务边界
- `year` 越界(不在 [2020, 2100])→ 605076,`month` 是否越界不影响判定结果(先年后月)。
- `year` 合法、`month` 越界(如 13)→ 仍按既有 605010 路径处理,本次未改动。
- `year`/`month` 缺失仍是既有的 100001(参数非法),未受本次改动影响。
### 2. 矩阵年度月度统计 `GET /admin/fleet/matrix/month-counts`
**VO**: `MatrixMonthCountsReqVO` → `MatrixMonthCountsRespVO`
#### 使用场景
车务查看某年 12 个月各状态计数概览时调用(矩阵页顶部年度视图)。本次改动只影响 `year` 越界场景。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| year | Query | Integer | ✅ | 🔴 新增:2020-2100(越界返 605076) | 年份 |
| season | Query | String | - | active/pending/archived/blacklist | 司机赛季筛选,默认 active |
| fleetTeamIds | Query | Long[] | - | - | 车队 ID 多选,空=全部 |
| typeKeys | Query | String[] | - | 规范小写,空=全部 | 车型大类多选 |
注:本端点没有 `month` 入参,年份越界统一走 605076,不会把调用方指去改一个不存在的字段。
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| year | Integer | 年份(回显) |
| months | List | 12 个月各状态计数 |
#### 请求示例
```http
GET /admin/fleet/matrix/month-counts?year=2026&season=active
```
#### 响应示例
```json
{"code":200,"message":"成功","data":{"year":2026},"traceId":null,"success":true}
```
(实测该年 12 个月 `statusCounts` 均为 0,字段结构本次未改动,示例只截取顶层字段。)
#### 空数据 / 降级响应
区间内年份若全年无任何数据,`months` 中每个月的计数均为 0,属正常业务结果;本次改动不影响此形态。
#### 错误响应
```json
{"code":605076,"message":"年份超出范围(仅支持 2020-2100 年)","data":null,"traceId":null,"success":false}
```
(实测 `year=1800` 返回上述响应体。)
#### 业务边界
- `year` 越界(不在 [2020, 2100])→ 605076。
- `year` 缺失仍是既有的 100001(参数非法),未受本次改动影响。
### 3. 矩阵未派订单清单 `GET /admin/fleet/matrix/unassigned-orders`
**VO**: `MatrixUnassignedReqVO` → `List<MatrixUnassignedOrderVO>`
#### 使用场景
车务查看某年某月未派车订单清单(含虚拟待派条目)时调用。本次改动只影响 `year` 越界场景;`month` 越界的同构收敛见工单 #8571 的 changelog。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| year | Query | Integer | ✅ | 🔴 新增:2020-2100(越界返 605076),本次前摘掉了原 `@Min(1970)/@Max(9999)` | 年份 |
| month | Query | Integer | ✅ | 1-12(越界返 605010,见 #8571) | 月份 |
| typeKeys | Query | String[] | - | 规范小写,空=全部 | 车型大类多选 |
#### 出参字段表
响应结构本次未改动。
| 字段 | 类型 | 说明 |
|------|------|------|
| virtualPending | Boolean | 是否虚拟待派条目(库里无对应行,由 order-v3 当前需求投射) |
| headcountLabel | String | 人数展示文案 |
#### 请求示例
```http
GET /admin/fleet/matrix/unassigned-orders?year=1990&month=6&season=active
```
#### 响应示例
```json
{"code":200,"message":"成功","data":[],"traceId":null,"success":true}
```
(对照:`year=2026&month=6`(区间内)实测返回上述形态,`data` 为空数组属正常业务结果,与越界错误可区分。)
#### 空数据 / 降级响应
区间内年份若当月无未派订单,`data` 为空数组,属正常业务结果,与越界返回的错误响应(`success:false`)可明确区分。
#### 错误响应
```json
{"code":605076,"message":"年份超出范围(仅支持 2020-2100 年)","data":null,"traceId":null,"success":false}
```
(实测 `year=1990` 返回上述响应体;本次改动前该场景返回的是框架码 100001,见"六.6、修改前后对比"。)
#### 业务边界
- 🔴 **契约收窄**:本端点 `year` 原有的校验区间是 `[1970, 9999]`(`@Min/@Max` 注解),本次收窄为 `[2020, 2100]` 且改走业务码 605076。`year=1990` 这类此前能通过框架校验、现在会被拒绝的取值,前端如果曾经允许用户选择这类年份,需要同步收紧可选范围。
- `year` 越界 → 605076;`month` 越界 → 605010(#8571),二者不互相覆盖,先年后月。
- `year`/`month` 缺失仍是既有的 100001(`@NotNull` 保留,未随本次摘除区间注解一并摘掉)。
---
## 四、契约约束与正确调用方式
> 本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload / 响应 |
|------|-----------------|
| ✅ `year` 在 [2020, 2100] 区间内(含边界) | 三入口均正常返回 `code=200` |
| ❌ `year` 不在 [2020, 2100] 区间(如 1800/1990/9999) | 三入口均返回 `code=605076` |
| ❌ 继续沿用 `unassigned-orders` 此前 `[1970, 9999]` 的可选年份范围 | `year=1990` 等取值现在会被 605076 拒绝,不再是 100001 |
| ❌ 把 605076 当作可重试错误自动重试 | 605076 是入参永久性非法,重试同一 `year` 不会成功,需要用户重新选择年份 |
### 切换状态时的必要动作
前端拦到 `code=605076` 时应提示"年份超出范围,仅支持 2020-2100 年"类文案,并将年份选择控件的可选范围收紧到该区间,不要自动重试。
---
## 五、数据库行为
本次涉及的三个接口均为只读查询,无任何数据库写操作。改动只在 Service 层新增一段入参校验逻辑,不涉及任何表结构或存量数据变化。
---
## 六、边界行为
- `year` 不在 [2020, 2100] → 605076(三入口统一,本次新增)
- `year` 缺失 → 100001(既有行为,未改动)
- `year` 合法、`month` 越界 → 605010(grid 既有行为;unassigned-orders 同构收敛见 #8571)
- `year`/`month` 均合法 → 按既有逻辑正常查询,字段结构未变
---
## 六.5、枚举 / 数据字典
### 矩阵年份越界错误码(`AssignmentErrorCode.MATRIX_YEAR_OUT_OF_RANGE`)
**所属字段**: 无(HTTP 响应顶层 `code`) | **类型**: `Integer`
| 值 | 中文 | 说明 |
|----|------|------|
| `605076` | 年份超出范围(仅支持 2020-2100 年) | 🔴 本次新增;三个矩阵查询入口的 `year` 不在 [2020, 2100] 时统一返回;入参永久性非法,不应自动重试 |
## 六.6、修改前后对比
### 字段级对比
本次无请求/响应字段新增或删除;`unassigned-orders` 的 `year` 字段摘掉了 `@Min(1970)/@Max(9999)` 注解(见入参字段表标注),字段本身仍是必填 `Integer`。
### 行为级对比
| 场景 | 改前 | 改后 |
|------|------|------|
| grid / month-counts,`year` 越界(不在 [2020,2100],如 1800/9999) | 未校验,当作合法年份继续查询 | 返回 `code=605076`(新增拦截) |
| unassigned-orders,`year` 不在 [2020, 2100](含此前 `@Min(1970)/@Max(9999)` 认为合法的 [1970,2019]∪[2101,9999] 区间) | 该子区间内视为合法继续查询,落在 [1970,9999] 之外才返回框架码 100001 | 统一返回业务码 605076,不再区分是否曾落在 [1970,9999] 内 |
| 三入口,`year` 在 [2020, 2100] | 正常返回 200 | 不变,仍正常返回 200 |
## 六.7、影响评估
- **是否破坏向后兼容**: 是(仅 `unassigned-orders`)——`year` 的可接受范围从 `[1970, 9999]` 收窄到 `[2020, 2100]`,且越界时的错误码从 100001 变为 605076;`grid`/`month-counts` 此前对越界年份没有任何拦截,本次是新增拦截而非收窄既有契约。
- **前端是否必须同步上线**: 是——年份选择控件若允许超出 `[2020, 2100]` 的取值,现在会收到新的 605076 错误码,前端需要新增该码的处理分支(提示文案 + 阻断当前查询),并建议同步收紧可选年份范围以减少用户触发该错误的机会。
- **前端 workaround 清理点**: 若此前为"年份异常导致矩阵页面空白/报错"写过特殊兼容逻辑,可以确认不再需要,因为现在有明确的 605076 信号可用。
---
## 七、不影响范围
- **仅影响**: 派单矩阵三个查询入口(`grid`/`month-counts`/`unassigned-orders`)在 `year` 入参越界时的响应。
- **零影响**:
- 三入口在 `year` 合法时的成功路径字段结构
- `month` 越界的既有校验 605010(`unassigned-orders` 的同构收敛见 #8571 单独的 changelog)
- `season`/`fleetTeamIds`/`typeKeys`/`status`/`statuses` 等其余入参的校验逻辑
- `day-orders` 等矩阵模块下其余未涉及本次改动的端点
---
## 八、测试环境已验证
服务:`hl-fleet-service`,dev-v3 分支部署测试网关 @ `9c7ac9382`(含 #8561 所在提交),测试网关 `https://api.test.1814.love`:
```
✓ GET grid?year=1800&month=6 → code=605076, message="年份超出范围(仅支持 2020-2100 年)"
✓ GET grid?year=9999&month=6 → code=605076
✓ GET month-counts?year=1800 → code=605076
✓ GET unassigned-orders?year=1990&month=6 → code=605076(此前为 100001,见六.6)
✓ GET grid?year=2020&month=6(下边界)→ code=200,正常返回
✓ GET grid?year=2100&month=6(上边界)→ code=200,正常返回
✓ GET month-counts?year=2026(区间中段)→ code=200,正常返回
✓ GET unassigned-orders?year=2026&month=6(区间中段)→ code=200, data=[]
```
注:2020/2100 两端边界值仅在 `grid` 入口做了直接边界实测;`month-counts`/`unassigned-orders` 在区间中段(year=2026)验证了正常放行。三入口共用同一段 `requireValidYearMonth` 校验逻辑(`MatrixService.java`),边界判定不因入口而异,越界拦截已在三入口分别实测(见上)。
---
## 十、相关文档
- 关联 Issue: [wx/HL#8561](https://git.1814.love/wx/HL/issues/8561)
- 关联 PR: [wx/HL#8565](https://git.1814.love/wx/HL/pulls/8565)
## 关联 / 联系人
### 链接
- **Issue**: [#8561](https://git.1814.love/wx/HL/issues/8561)
- **PR**: [#8565](https://git.1814.love/wx/HL/pulls/8565)
- **Merge commit**: [ad3c6e4305](https://git.1814.love/wx/HL/commit/ad3c6e4305)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,387 @@
---
schema: "hl-changelog/v2"
ticket: "8562"
title: "团期逐户用车列表区分「从未提交」与「已被打回待重提」,打回明细走新字段下发"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "e3f3d55265bf7f017d795e6e049601cee700dbfc"
target_release: "v2.1"
verified_at: "2026-09-30"
status_note: "GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households 的 households[] 新增三个字段:submitState(户级提交态,恒非 null,三取值 NEVER_SUBMITTED 从未提交 / SUBMITTED 已提交 / REJECTED_PENDING_RESUBMIT 已被打回待重提)、submitStateName(其中文名,后端下发)、rejectedRequirements(该户当前处于打回待重提的类别明细,恒非 null,无打回时为空数组,按展示序 TRAVEL 在前)。背景:用车的打回是「原地置 REJECTED_* + is_active=0」,被打回的户因此没有任何活跃需求行,与从未提交的户在 status 上完全同形(都是 null),车务照 status 催办会把「已交过、只是被驳回」和「压根没动过」混成一堆。四条必须照做的限定:(1) status 字段的取值规则一字未改、仍只由活跃行决定,判「有没有提交过」一律读 submitState,不要读 status 是否为 null;(2) requirements 列表内容零变化,被打回的行仍然不在里面,打回信息只在 rejectedRequirements;(3) rejectedRequirements 的元素刻意不与需求行同构(只有类别/打回状态/打回意见/打回时刻/版本号,没有车队明细、服务日期、座位数),不可当作需求行渲染,否则同一户会出现与活跃行自相矛盾的一条;(4) householdCount 口径再放宽一项,不再恒等于 needs_vehicle=true 的户数——一个 needs_vehicle 为假、没有活跃行、但有一类被打回的户现在也会进列表,判「这户为什么在列表里」看 submitState,不要拿 needsVehicle 反推。另两个数一字不动:vehicleRowCount(被打回的户贡献 0 行)与 countedHouseholdCount(只认活跃 TRAVEL 行),座位汇总口径不会因为有人被驳回而跳变。submitState 与 requirements 同受 kind 筛选影响:传 kind=TRAVEL 时,一个只有接送机被打回的户读成 NEVER_SUBMITTED;要看全貌就不传 kind(不传 = 两类都返)。已知边界(定案、非缺陷):TRANSFER 需求被「不再需要接送」失活(#8435)且没有新版时,既无活跃行也非打回,读成 NEVER_SUBMITTED,与从未提交对催办动作的要求一致,故不另立一态。入参、分页、排序、错误码(589500 / 589507 / 809000 / 401)与其余响应字段均未变化。;前端已交付:逐户表车侧判提交/判打回改读 submitState,「已打回」筛选与徽标覆盖车侧打回户,无活跃行户级文案用后端 submitStateName,rejectedRequirements 单独提示行不当需求行渲染,4 例定向测试全绿(hl-admin e3f3d552)"
updated_at: "2026-09-30"
base: "dev-v3"
---
# 团期用车逐户列表:区分「从未提交」与「已被打回待重提」
> **存放目录**: 二期(order-v3/fleet)→ `changelogs-v2/2026-09/`
## ⚠️ 关键变化
- **`households[]` 新增三个字段**:`submitState`(户级提交态,**恒非 null**)、`submitStateName`(其中文名)、`rejectedRequirements`(该户当前处于打回待重提的类别明细,**恒非 null**,无打回时为空数组)。
- 🔴 **判「这户有没有提交过」一律读 `submitState`,不要读 `status` 是否为 `null`**。用车的打回是「原地置 `REJECTED_*` + `is_active=0`」⇒ 被打回的户**没有任何活跃需求行**,`status` 同样是 `null`,与从未提交的户**完全同形**。照 `status` 催办会去催一个已经交过、只是被驳回的人,而真正该催的「按意见重提」在页面上看不出来。
- **`status` 的取值规则一字未改**(仍是活跃行展示序首条),`statusName` 同理。本次只新增旁路字段,既有映射与读数不受影响。
- **`requirements` 列表内容零变化**:被打回的行仍然**不在**里面(它已失活)。打回信息只在新字段 `rejectedRequirements` 里。
- 🔴 **`rejectedRequirements` 的元素不是需求行,不可当作需求行渲染**:它刻意与 `requirements[]` **不同构**——只带类别 / 打回状态 / 打回意见 / 打回时刻 / 版本号,**没有**车队明细、服务日期、座位数。把它拼进需求表会让同一户出现一条与活跃行自相矛盾的需求。
- 🔴 **`householdCount` 口径再放宽一项,不再恒等于「`needs_vehicle=true` 的户数」**:一个 `needs_vehicle` 为假、又没有活跃行、但有一类被打回的户现在也会进列表(它正是要催重提的人)。判「这户为什么在列表里」看 `submitState`,**不要拿 `needsVehicle` 反推**。
- **另两个数一字不动**:`vehicleRowCount`(被打回的户贡献 0 行)与 `countedHouseholdCount`(只认活跃 TRAVEL 行)。座位汇总口径不会因为「有人被驳回」而跳变。
- **`submitState` 随 `kind` 筛选变化**(与 `requirements` 同一口径):传 `kind=TRAVEL` 时,一个只有接送机被打回的户读成 `NEVER_SUBMITTED`。要看全貌就**不传** `kind`(不传 = 两类都返)。
## 一、背景(选填)
团期「查看需求」Tab 的用车逐户明细是车务与团期管理员的催办页:谁还没报、谁报了在等审、谁被驳回要改。但用车的打回实现是「把那一版原地置成 `REJECTED_*` 并把 `is_active` 置 0」,于是被打回的户在这个只读活跃行的端点里表现为「0 条需求行 + `status` 为 null」——与「从未提交」一模一样。更糟的是:`needs_vehicle` 为假、又没有活跃行的户改前压根不出卡,而被打回的户恰恰可能是这个形状,催办页上会**整户消失**。本次补的就是「这户到底是没交过,还是交过被驳回」这一维,以及「被驳回的是哪一类、意见是什么、什么时候驳的」这几个催办必需值。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 团期子订单用车需求记录 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households` | 修改 | `households[]` 新增 `submitState` / `submitStateName` / `rejectedRequirements`;`householdCount` 口径放宽含打回户 |
## 三、接口详情
### 1. 团期子订单用车需求记录 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households`
**VO**: `Long groupBatchId + String kind(query)→ GroupVehicleHouseholdsRespVO`
#### 使用场景
团期「查看需求」Tab 的用车逐户明细(汇总块下面那一块)。用于回答「这个团还差谁的用车需求」:本次起可以把「催首次提交」和「催按意见重提」分成两组,并在被驳回的户上直接展示驳回意见与时刻。
#### 入参
入参本次**零变化**。
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | path | Long | 是 | 雪花 ID;团期不存在返 589500 | 团期 ID |
| kind | query | String | 否 | `TRAVEL` / `TRANSFER`;其余非空值返 809000 | 需求类别过滤。**不传或空白 = 两类都返**(与提交侧「不传按 TRAVEL」的缺省刻意相反,前端默认不传即可) |
#### 出参 `Result<GroupVehicleHouseholdsRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| groupBatchId | String | 团期 ID |
| departDate | String | 团期出发日 `YYYY-MM-DD`;团期未定出发日为 `null` |
| endDate | String | 团期结束日 `YYYY-MM-DD`;团期未定结束日为 `null` |
| householdCount | Integer | 本列表户数(按 orderId 去重),恒等于 `households` 长度。**口径放宽**:= 应报车户 ∪ 有活跃需求行的户 ∪ **处于打回待重提的户**;**不再恒等于 `needs_vehicle=true` 的户数** |
| vehicleRowCount | Integer | 需求行数 = Σ 各户 `requirements` 长度。未提交与被打回的户贡献 0 行,故**可能小于 `householdCount`**(别当「行数 ≥ 户数」不变量) |
| countedHouseholdCount | Integer | 计入车侧汇总的户数(= 有活跃 TRAVEL 行的户数)。判据一字未改,**不随 `kind` 筛选变化**,也不因有人被驳回而变 |
| households | Array | 逐户明细,按 `orderNo` 升序(`orderNo` 为空的排最后,按 `orderId` 兜底稳定) |
| households[].orderId | String | 子订单 ID |
| households[].orderNo | String | 子订单编号(**非团号**,形如 `HL` + `yyyyMMddHHmmssSSS`) |
| households[].teamNo | String | 子订单团号;未付订金尚未分配时为 `null`(不兜底、不回退成订单号) |
| households[].customerName | String | 主联系人姓名 |
| households[].participantCount | Integer | 出行人数(成人 + 儿童 + 小童 + 婴儿) |
| households[].consultantId | String | 定制师 ID;未指派为 `null` |
| households[].consultantName | String | 定制师姓名;未指派为 `null` |
| households[].countedInSummary | Boolean | 该户是否计入车侧汇总(= 有活跃 TRAVEL 行);**未变** |
| households[].status | String | 户级用车需求状态:**仅由活跃行决定**。`null` = 该户当前没有活跃需求行(**从未提交与已被打回失活两种情况都是 `null`**,要分辨读 `submitState`);非 `null` 时取展示序首条(TRAVEL 优先)的状态。**取值规则一字未改** |
| households[].statusName | String | 户级状态中文名;`status` 为 `null` 时同为 `null`。`PENDING_REVIEW` 按 kind 分两套文案(TRAVEL=待提交车务 / TRANSFER=待审核,#8218);**未变** |
| households[].requirements | Array | 该户的**活跃**用车需求行,0~2 条(TRAVEL / TRANSFER 各至多一条)。被打回的行已失活、**不在本列表内**;该户没有活跃行时为**空数组**(不是 `null`)。**本列表内容零变化** |
| households[].submitState | String | 🆕 户级提交态,**恒非 null**:`NEVER_SUBMITTED` / `SUBMITTED` / `REJECTED_PENDING_RESUBMIT`。`status` 为 `null` 时靠它分辨两种空态;一户两类不同时按展示序首条(TRAVEL 优先)取;**随 `kind` 筛选变化** |
| households[].submitStateName | String | 🆕 户级提交态中文名,与 `submitState` 一一对应:从未提交 / 已提交 / 已被打回待重提。后端下发,前端不自己映射 |
| households[].rejectedRequirements | Array | 🆕 该户**当前**处于打回待重提的类别明细,按展示序(TRAVEL 在前)。**恒非 null**,无打回时为空数组。**不是需求行,不可当作需求行渲染** |
| households[].rejectedRequirements[].kind | String | 需求类别:`TRAVEL` 行程用车 / `TRANSFER` 接送机 |
| households[].rejectedRequirements[].kindName | String | 类别中文名,后端下发 |
| households[].rejectedRequirements[].status | String | 打回状态编码:`REJECTED_TO_CONSULTANT` = 团期管理员打回定制师 / `REJECTED_TO_ADMIN` = 车务退回团期管理员 |
| households[].rejectedRequirements[].statusName | String | 打回状态中文名,与 `requirements[].statusName` 同一套车务文案:已驳回定制师 / 已驳回管理员 |
| households[].rejectedRequirements[].returnRemark | String | 打回意见;历史数据可能为 `null` |
| households[].rejectedRequirements[].returnedAt | String | 打回时刻 `yyyy-MM-dd HH:mm:ss`;历史数据可能为 `null` |
| households[].rejectedRequirements[].version | Integer | 被打回的那一版版本号。**TRAVEL 与 TRANSFER 各自独立递增,不可跨类比大小** |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2104839654727618562/requirement/vehicle-households
Authorization: Bearer {token}
```
按类别筛选(只看行程用车):
```http
GET /v3/admin/order/group-batch/2104839654727618562/requirement/vehicle-households?kind=TRAVEL
Authorization: Bearer {token}
```
#### 响应示例
三户分别落在三个提交态上。`requirements[]` 行内字段与本次改动前完全一致,这里只保留几个便于对读的字段,未列出的行内字段(`fleet` / `serviceDates` / `specialTags` / 座位数等)照旧下发。
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2104839654727618562",
"departDate": "2026-10-06",
"endDate": "2026-10-10",
"householdCount": 3,
"vehicleRowCount": 1,
"countedHouseholdCount": 1,
"households": [
{
"orderId": "2104839654727618570",
"orderNo": "HL20261006103015001",
"teamNo": "T26-3963",
"customerName": "周雅",
"participantCount": 4,
"consultantId": "1901233114509312088",
"consultantName": "苏晴",
"countedInSummary": true,
"status": "PENDING_REVIEW",
"statusName": "待提交车务",
"requirements": [
{
"requirementId": "2104839777884160001",
"kind": "TRAVEL",
"kindName": "行程用车",
"status": "PENDING_REVIEW",
"statusName": "待提交车务",
"headcount": 4,
"returnRemark": null,
"returnedAt": null
}
],
"submitState": "SUBMITTED",
"submitStateName": "已提交",
"rejectedRequirements": []
},
{
"orderId": "2104839654727618571",
"orderNo": "HL20261006103015002",
"teamNo": "T26-3964",
"customerName": "郑文博",
"participantCount": 2,
"consultantId": "1901233114509312088",
"consultantName": "苏晴",
"countedInSummary": false,
"status": null,
"statusName": null,
"requirements": [],
"submitState": "REJECTED_PENDING_RESUBMIT",
"submitStateName": "已被打回待重提",
"rejectedRequirements": [
{
"kind": "TRAVEL",
"kindName": "行程用车",
"status": "REJECTED_TO_CONSULTANT",
"statusName": "已驳回定制师",
"returnRemark": "第三天上午的用车时间与行程冲突,请改后重提",
"returnedAt": "2026-09-29 16:42:11",
"version": 2
}
]
},
{
"orderId": "2104839654727618572",
"orderNo": "HL20261006103015003",
"teamNo": null,
"customerName": "何嘉宁",
"participantCount": 3,
"consultantId": null,
"consultantName": null,
"countedInSummary": false,
"status": null,
"statusName": null,
"requirements": [],
"submitState": "NEVER_SUBMITTED",
"submitStateName": "从未提交",
"rejectedRequirements": []
}
]
},
"success": true
}
```
#### 空数据 / 降级响应
- 团期下没有在团子订单:`households` 为空数组 `[]`,三个计数均为 `0`,`departDate` / `endDate` 照常回显,不报错。
- 某户没有活跃需求行:`requirements` 为**空数组**(不是 `null`),`status` 与 `statusName` 为 `null`,而 `submitState` **仍然有确定取值**(`NEVER_SUBMITTED` 或 `REJECTED_PENDING_RESUBMIT`)——前端不必对 `submitState` 判空。
- 某户没有被打回的类别:`rejectedRequirements` 为**空数组**(不是 `null`)。
- 在团订单 ID 存在但订单行缺失(跨团挂单 / 订单被物理删这类数据异常):该户被跳过并在服务端留痕,整页照常返回;三个计数都按**最终列表**重算,不会出现「表头 42 户、列表里只有 30 户」这种自相矛盾的响应。
- 单次返回上限 **500 户**,超出按 `orderNo` 升序截断并在服务端留痕;截断后三个计数同样按截断后的列表重算。
#### 错误响应
```json
{
"code": 809000,
"message": "用车需求类别非法:BOTH",
"data": null,
"success": false
}
```
- `809000 用车需求类别非法:{0}`:`kind` 传了 `TRAVEL` / `TRANSFER` 之外的非空值(例如 `ALL`、`BOTH`、小写拼错)。要「两类都返」请**不传**该参数或传空串。
- `589500 团期不存在`:`groupBatchId` 查不到。
- `589507 无操作权限(当前角色未授予团期权限,或该团期不在您名下)`:缺团期查看权限,或该团期不在当前账号名下。
- `401`:未登录或令牌失效(网关返 HTTP 200 + 信封 `code: 401`,请按信封 `code` 判定)。
#### 业务边界
- 🔴 **判「有没有提交过」读 `submitState`,不读 `status` 是否为 `null`**。打回 = 原地置 `REJECTED_*` + `is_active=0` ⇒ 打回户与从未提交户的 `status` **都是 `null`**,在 `status` 这一维上不可分辨。`status` 的取值规则本次一字未改。
- 🔴 **`requirements` 列表内容零变化**:被打回的行不在里面,打回信息只在 `rejectedRequirements`。不要为了展示驳回意见去翻 `requirements[].returnRemark`——那一格记的是**该活跃行**历史上被打回过的痕迹(已重提后仍可能有值),不是「当前处于打回态」。
- 🔴 **`rejectedRequirements` 不可当作需求行渲染**:它与 `requirements[]` 刻意不同构,没有车队明细 / 服务日期 / 座位数。需要看需求内容时读该户的活跃行,或走需求版本历史端点。
- 🔴 **`householdCount` 不再恒等于 `needs_vehicle=true` 的户数**:一个 `needs_vehicle` 为假、没有活跃行、但有一类被打回的户也会进列表。判「这户为什么在列表里」看 `submitState`,不要拿 `needsVehicle` 反推。
- **`rejectedRequirements` 只列「当前」处于打回待重提的类别**,不是历史打回记录:打回后已重新提交的类别**不**出现在这里(那一类的现状在 `requirements` 里),否则页面会永远挂着一条早已处理完的驳回。
- **一户两类状态不同时,`submitState` 按展示序首条取**(TRAVEL 优先),与 `status` 同一条规则。例:TRAVEL 有活跃行、TRANSFER 被打回 ⇒ `submitState` 是 `SUBMITTED`,而 `rejectedRequirements` 里有 TRANSFER 那一条。**要逐类判断一律读 `requirements[].status` 与 `rejectedRequirements[].kind`,不要用户级的 `submitState` 推单类。**
- **`submitState` 随 `kind` 筛选变化**:传 `kind=TRAVEL` 时,一个只有接送机被打回的户读成 `NEVER_SUBMITTED`(该类别不在筛选范围内)。要看全貌不传 `kind`。
- **已知边界(定案,非缺陷)**:TRANSFER 需求被「不再需要接送」失活(#8435)且之后没有新版本时,该户既无活跃行也不处于打回态,会读成 `NEVER_SUBMITTED`。它与「从未提交」对催办动作的要求一致(要么提,要么整团免车),故不另立一态。
- **`version` 不可跨类比较**:TRAVEL 的 v3 与 TRANSFER 的 v3 之间没有先后关系,两类版本号各自独立递增。
- **`vehicleRowCount` 可能小于 `householdCount`**:未提交与被打回的户贡献 0 行。不要再把「行数 ≥ 户数」当不变量写断言。
- **`countedHouseholdCount` 不随 `kind` 筛选变化**(#8559),也不因驳回动作变化——座位汇总口径必须稳定。
- **Swagger 上该端点的 `notes` 仍按本次改动前的口径写着「被打回的需求行已失活,不在本列表内」**:这句对**需求行**依然成立(打回行确实不进 `requirements`),但对**户**不再成立——打回户现在会出现在 `households` 里。字段级语义以本交接件与各字段的 `@ApiModelProperty` 为准。
## 四、契约约束与正确调用方式(接口类必写)
1. **两个新字段恒非 null,直接读不用判空**:`submitState` 与 `rejectedRequirements` 对每一户都有确定取值(后者无打回时是空数组)。需要判空的仍是 `status` / `statusName` / `teamNo` / `consultantId` / `consultantName` 这些既有字段。
2. **催办分组按 `submitState` 做**:`NEVER_SUBMITTED` → 催首次提交;`REJECTED_PENDING_RESUBMIT` → 催按意见重提(意见与时刻在 `rejectedRequirements` 里);`SUBMITTED` → 具体到哪一步看 `status` / `requirements[].status`。
3. **不要用 `status == null` 当「未提交」的判据**:这是本次要解决的那个缺陷本身。前端若已有这段逻辑,请改为 `submitState === 'NEVER_SUBMITTED'`。
4. **不要把 `rejectedRequirements` 并进需求行列表**:两者结构刻意不同构。驳回信息建议单独渲染成一条提示条(类别 + 状态中文名 + 意见 + 时刻),与需求行区分开。
5. **不要拿 `needsVehicle` 反推「这户为什么在列表里」**:列表的并集口径已变,判据是 `submitState`。
6. **中文名一律用后端下发的**:`submitStateName` / `kindName` / `statusName` 都由后端给出,前端不要再本地维护映射表(`PENDING_REVIEW` 的文案还会按 kind 分叉成两种,本地表必然对不上,#8218)。
7. **要看全貌不传 `kind`**:不传 = 两类都返。传了 `kind` 则 `requirements`、`rejectedRequirements`、`submitState`、`householdCount`、`vehicleRowCount` 全部随之收窄(只有 `countedHouseholdCount` 三种筛选读数相同)。
8. **`kind` 只接受 `TRAVEL` / `TRANSFER`**:想表达「全部」请**不传**,传 `ALL` / `BOTH` 会返 809000。
9. **错误信封按 `code` 判**:业务失败与入参校验一律 HTTP 200 + 信封 `code`;测试环境网关对失效令牌也返回 HTTP 200 + `code: 401`。
## 五、数据库行为
本端点为只读查询,本次改动**不涉及任何 DDL 与 DML**:没有新增表、没有新增列、没有 Flyway 脚本、没有写入。
- `submitState` **不是数据库列**,不落表、不参与任何 SQL 过滤或分组,纯粹是响应字段——所以既有的按 `status` 筛选 / 统计的扫描路径不会把它重新捡起来,也不会误计。
- 打回明细取自用车需求的**版本历史行**(打回行已 `is_active=0`)。取数用**一次按子订单 ID 批量**的查询(与既有的活跃行查询同一形状,`requirement_kind` 的 `IN` 列表从 1 个值放宽到 2 个值),**不是逐户 N+1**;整页固定若干次查询,与户数无关。
- 判「某类是否处于打回待重提」复用的是定制师提交侧闸门的同一套算法(取最高版本组、看最后一次动作是否为打回),不新造判定规则。
## 六、边界行为
| 场景 | `status` | `requirements` | `submitState` | `rejectedRequirements` | 是否出现在列表 |
|------|----------|----------------|---------------|------------------------|----------------|
| 有活跃行(TRAVEL 或 TRANSFER) | 首条行的状态 | 1~2 条 | `SUBMITTED` | `[]` | 是 |
| 从未提交过任何版本 | `null` | `[]` | `NEVER_SUBMITTED` | `[]` | 是(`needs_vehicle` 为真即出卡) |
| 被打回、尚未重提 | `null` | `[]` | `REJECTED_PENDING_RESUBMIT` | 1~2 条 | 是(**本次新增的入列路径**) |
| 被打回后已重新提交 | 新行的状态 | 1~2 条 | `SUBMITTED` | `[]`(不挂已处理完的驳回) | 是 |
| TRAVEL 有活跃行 + TRANSFER 被打回 | TRAVEL 行的状态 | 1 条(TRAVEL) | `SUBMITTED`(按展示序首条) | 1 条(TRANSFER) | 是 |
| TRAVEL 被打回 + TRANSFER 有活跃行 | TRANSFER 行的状态 | 1 条(TRANSFER) | `REJECTED_PENDING_RESUBMIT`(TRAVEL 展示序在前) | 1 条(TRAVEL) | 是 |
| 只有 TRANSFER 被打回,且传了 `kind=TRAVEL` | `null` | `[]` | `NEVER_SUBMITTED`(该类别不在筛选内) | `[]` | 取决于 `needs_vehicle` |
| TRANSFER 被「不再需要接送」失活且无新版(#8435) | `null` | `[]` | `NEVER_SUBMITTED`(定案) | `[]` | 是 |
| 在团订单行缺失(数据异常) | — | — | — | — | 跳过该户并留痕,整页照常返回 |
| 户数超过 500 | — | — | — | — | 按 `orderNo` 升序截断,计数按截断后重算 |
## 六.5、枚举 / 数据字典
**户级提交态**(`submitState` → `submitStateName`,新增枚举,**恒非 null**)
| 码 | 中文名 | 语义 | 催办动作 |
|----|--------|------|----------|
| NEVER_SUBMITTED | 从未提交 | 在本次筛选的类别范围内既没有活跃需求行、也不处于打回态 | 催首次提交 |
| SUBMITTED | 已提交 | 至少一类存在活跃需求行(具体到哪一步看 `status`) | 按 `status` 跟进 |
| REJECTED_PENDING_RESUBMIT | 已被打回待重提 | 没有活跃行,但最高版本组的最后一次动作是打回 | 催「按意见改完再提」 |
> 展示名刻意与团期子订单列表那块的「未提交」用不同的词:那块只判「有没有活跃行」,被打回的户在那里也显示「未提交」;两处若同字,本次分开的这一步在页面上就白做了。
**打回状态**(`rejectedRequirements[].status` → `statusName`)
| 码 | 中文名 | 谁打回的 |
|----|--------|----------|
| REJECTED_TO_CONSULTANT | 已驳回定制师 | 团期管理员打回定制师 |
| REJECTED_TO_ADMIN | 已驳回管理员 | 车务退回团期管理员 |
**需求类别**(`kind` → `kindName`,未变)
| 码 | 中文名 |
|----|--------|
| TRAVEL | 行程用车 |
| TRANSFER | 接送机 |
展示序固定 TRAVEL 在前、TRANSFER 在后;`requirements` 与 `rejectedRequirements` 共用这个序。
**活跃需求行状态**(`requirements[].status`,未变):`PENDING_REVIEW` / `PENDING` / `PROCESSING` / `DONE`。`REJECTED_*` 不会出现在活跃行上。`PENDING_REVIEW` 的中文名按 kind 分叉:TRAVEL = 待提交车务、TRANSFER = 待审核(#8218)。
## 六.6、修改前后对比
| 字段 / 口径 | 改动前 | 改动后 |
|-------------|--------|--------|
| `households[].submitState` | 不存在 | 🆕 恒非 null 的三态字段,是 `status` 为 `null` 时唯一能分辨「从未提交 / 已被打回」的字段 |
| `households[].submitStateName` | 不存在 | 🆕 三态中文名,后端下发 |
| `households[].rejectedRequirements` | 不存在(打回信息在本端点完全取不到) | 🆕 恒非 null 的数组,列当前处于打回待重提的类别 + 意见 + 时刻 + 版本号 |
| `households[].status` / `statusName` | 只由活跃行决定 | **取值规则一字未改**(仍只由活跃行决定)。变的只是文档:不能再拿它判「有没有提交过」 |
| `households[].requirements` | 只含活跃行,打回行不在其中 | **内容零变化** |
| `householdCount` | = 应报车户 ∪ 有活跃需求行的户;恒等于 `needs_vehicle=true` 的户数(`needs_vehicle` 创单恒真) | 并集多一项「处于打回待重提的户」⇒ **不再恒等于 `needs_vehicle=true` 的户数**;`needs_vehicle` 为假但有一类被打回的户会进来 |
| `vehicleRowCount` | Σ 各户活跃行数 | **口径未变**(打回户贡献 0 行);与 `householdCount` 的差额多了「打回户」这一类 |
| `countedHouseholdCount` | 有活跃 TRAVEL 行的户数 | **一字未变**,不因驳回动作跳变 |
| 被打回户是否出现在列表 | `needs_vehicle` 为假时**不出卡**(催办页上整户消失) | 出卡,`submitState` = `REJECTED_PENDING_RESUBMIT` |
| 入参 / 分页 / 排序 / 错误码 | — | 全部未变 |
## 六.7、影响评估
- **前端必须改的**:如果页面上有「`status == null` ⇒ 显示未提交」这段逻辑,**必须**改成读 `submitState`——不改的话打回户会继续被标成「未提交」,本次改动在页面上等于没做。
- **前端应当改的**:催办清单按 `submitState` 分两组;被驳回的户上渲染 `rejectedRequirements` 里的类别 + 状态中文名 + 意见 + 时刻。
- **前端不要做的**:把 `rejectedRequirements` 拼进需求行表格(会出现与活跃行矛盾的一条);拿 `needsVehicle` 反推入列原因;跨类比较 `version`。
- **可能被读错的一处**:`requirements[].returnRemark` / `returnedAt` 记的是**该活跃行**历史上被打回过的痕迹,已重提后仍可能有值;「当前处于打回态」只看 `rejectedRequirements` 是否非空。
- **列表条数会变多**:`needs_vehicle` 为假、无活跃行、但有一类被打回的户从本次起入列。如果前端有基于户数的断言或埋点基线,会看到这一类团期的户数上升——这是预期,不是数据错误。
- **兼容性**:JSON 新增字段对已有前端反序列化无影响。既有字段一个没删、没改名、没改类型。
- **无副作用面**:只读端点,不涉及写入、事务、消息、权限判定变化;座位与汇总口径不变。
## 七、不影响范围
- **入参**:`groupBatchId`、`kind` 的取值域、缺省语义(不传 = 两类)、校验规则全部未变。
- **排序与上限**:`orderNo` 升序 + `orderId` 兜底、单次 500 户上限未变。
- **既有响应字段**:`groupBatchId` / `departDate` / `endDate` / `vehicleRowCount` / `countedHouseholdCount` / `households[]` 的既有字段(含 `status` / `statusName` / `requirements` 及行内所有字段)名称、类型、取值域、语义全部未变。
- **错误码**:未新增、未删除、未改文案(589500 / 589507 / 809000 / 401)。
- **权限码**:仍是团期查看权限,未收紧未放宽。
- **用房侧** `hotel-households` 端点:本次一行未改。
- **写路径**:逐户提交车务、打回、整体确认需求等写接口本次一行未改。
- **团级汇总** `requirement-summary`:口径与读数未变(`countedHouseholdCount` 是它的对账口,本次刻意保持不动)。
- **网关路由**:既有路由,本次无新增。
- **数据库**:无 DDL、无 DML、无 Flyway 脚本。
- **小程序端**:零影响。
## 八、测试环境已验证
本次改动的核心可验证面是「打回户与从未提交户在 `status` 上同形、在 `submitState` 上可分辨」,以及「既有三个计数与 `requirements` 内容不受影响」。已由下列自动化用例覆盖(`hl-order-service-v3`):
| 覆盖点 | 用例 |
|--------|------|
| 打回户与从未提交户 `status` 相同(都是 `null`)、`submitState` 不同 | `GroupBatchVehicleHousehold8562Test#households_rejectedAndNeverSubmitted_sameStatusDifferentSubmitState` |
| 打回户带出打回意见与打回状态中文名 | `#households_rejectedHousehold_carriesRemarkAndStatusName` |
| 存在打回时 `requirements` 仍然只含活跃行(内容零变化) | `#households_rejectionPresent_requirementsStillOnlyActiveRows` |
| TRAVEL 有活跃行 + TRANSFER 被打回 ⇒ `submitState` 按展示序首条(TRAVEL)取 | `#households_travelActiveTransferRejected_submitStateFollowsTravel` |
| TRAVEL 被打回 + TRANSFER 有活跃行 ⇒ `submitState` 同样按 TRAVEL 取 | `#households_travelRejectedTransferActive_submitStateFollowsTravel` |
| 传 `kind=TRANSFER` 时 TRAVEL 的打回被筛掉 | `#households_kindTransfer_travelRejectionFilteredOut` |
| `needs_vehicle` 为假但有一类被打回的户仍然入列 | `#households_rejectedButNeedsVehicleFalse_stillListed` |
| 完全没有打回时 `submitState` 为 `SUBMITTED`、`rejectedRequirements` 为空数组 | `#households_noRejectionAtAll_submitStateSubmitted` |
| 打回明细按子订单 ID 批量取数(固定次数,不随户数增长) | `RequirementServiceVehicleRejectionBatchTest` |
## 九、相关历史 PR
- PR #8623(本次):`feat(order-v3): 团期逐户用车区分「从未提交」与「已被打回待重提」(#8562)`。
- #8195:`householdCount` 改为「应报车户数」并开始包含未提交户,`status` / `statusName` 两个户级字段在那一单新增。
- #8559:`countedHouseholdCount` 不随 `kind` 筛选变化。
- #8577:只提交接送机的户不再被判「未提交用车需求」。
- #8601:逐户提交车务与打回的 `kind` 参数取消默认值。
- #8218:`PENDING_REVIEW` 的中文名按 kind 分叉(TRAVEL 待提交车务 / TRANSFER 待审核)。
- #8435:「不再需要接送」失活 TRANSFER 需求(本单已知边界的来源)。
## 十、相关文档
- `docs/CODE_RULES.md` §3:VO 命名、`@ApiModelProperty` 约定、禁 Entity 跨层(打回明细用独立 DTO 而非直接传需求行的依据)。
- `docs/CODE_RULES.md` §15.7:字典字面量单源——`submitStateName` / `kindName` / `statusName` 由后端下发的依据。
- Swagger:`hl-order-service-v3` → `团期需求` 分组。字段级语义以各字段 `@ApiModelProperty` 为准;该端点 `notes` 的那句「被打回的需求行不在本列表内」只对需求行成立、对户不成立(见「业务边界」最后一条)。
## 关联 / 联系人
### 链接
- 工单 #8562
- PR #8623
### 联系人
- 后端:wx
- 前端:mmg(管理后台 hl-ui)
@@ -0,0 +1,236 @@
---
schema: "hl-changelog/v2"
ticket: "8571"
title: "矩阵未派订单清单月份越界改由 Service 判,与另两个入口同构"
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: "PR #8582 合并 dev-v3(6634d0588d);hl-fleet-service dev-v3 分支部署测试网关 @ 9c7ac9382 并实测:unassigned-orders 月份越界(如 13、0)统一返回 605010,月份缺失仍返 100001,grid 入口的既有 605010 行为未回归。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# hl-fleet-service: 矩阵未派订单清单月份越界改由 Service 判,与另两个入口同构
> **存放目录**: `changelogs-v2/2026-09/`
> **服务**: hl-fleet-service (端口 8087)
> **PR**: #8582
> **Issue**: #8571
> **日期**: 2026-09-30
> **影响范围**: 管理后台派单矩阵未派订单清单端点 `unassigned-orders` 的月份越界校验
---
## ⚠️ 关键变化
- `unassigned-orders` 的 `month` 越界校验此前是框架层 `@Min(1)/@Max(12)` 注解,越界返回**框架码 100001**;`grid` 端点的 `month` 越界则一直是 Service 层 `requireValidYearMonth` 判定,返回**业务码 605010**。同一类"月份填错了",两个端点走两套码、两套错误文案,前端得按端点分别写处理分支。
- 本次摘掉 `unassigned-orders` 的 `@Min/@Max` 注解,越界统一改由 Service 层 `requireValidYearMonth` 判定,**现在与 `grid` 完全同构:越界一律返回 605010**(`月份超出范围`)。
- 🔴 **`@NotNull` 被保留**:`month` 字段缺失(不传该参数)仍然返回既有的 100001(`参数非法: 月份不能为空`),这条路径没有变化——只有"传了值但越界"这一种场景的错误码变了。
- `year` 字段的年份越界校验(605076)是另一张工单 #8561 引入的独立改动,与本次 `month` 校验改动在同一个 `requireValidYearMonth` 方法里但各自独立生效,请分别查阅两份 changelog。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 矩阵未派订单清单 | GET | `/admin/fleet/matrix/unassigned-orders` | 校验口径收敛 | `month` 越界改由 Service 判,与 grid 统一返回 605010 |
---
## 三、接口详情
### 1. 矩阵未派订单清单 `GET /admin/fleet/matrix/unassigned-orders`
**VO**: `MatrixUnassignedReqVO` → `List<MatrixUnassignedOrderVO>`
#### 使用场景
车务查看某年某月未派车订单清单(含虚拟待派条目)时调用。本次改动只影响 `month` 越界场景的错误码;`year` 越界的新增校验(605076)见工单 #8561 的 changelog。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| year | Query | Integer | ✅ | 2020-2100(越界返 605076,见 #8561) | 年份 |
| month | Query | Integer | ✅ | 🔴 1-12,越界改为返 605010(此前是框架码 100001),本次摘掉了原 `@Min(1)/@Max(12)` | 月份 |
| typeKeys | Query | String[] | - | 规范小写,空=全部 | 车型大类多选 |
#### 出参字段表
响应结构本次未改动。
| 字段 | 类型 | 说明 |
|------|------|------|
| virtualPending | Boolean | 是否虚拟待派条目 |
| headcountLabel | String | 人数展示文案 |
#### 请求示例
```http
GET /admin/fleet/matrix/unassigned-orders?year=2026&month=13&season=active
```
#### 响应示例
区间边界内实测(`month=1`):
```json
{"code":200,"message":"成功","data":[],"traceId":null,"success":true}
```
区间边界内实测(`month=12`):
```json
{"code":200,"message":"成功","data":[],"traceId":null,"success":true}
```
#### 空数据 / 降级响应
`month` 合法时若当月无未派订单,`data` 为空数组,属正常业务结果,与越界返回的错误响应(`success:false`)可明确区分。
#### 错误响应
`month=13`(越界)实测:
```json
{"code":605010,"message":"月份超出范围","data":null,"traceId":null,"success":false}
```
`month=0`(越界)实测:
```json
{"code":605010,"message":"月份超出范围","data":null,"traceId":null,"success":false}
```
`month` 缺失(未传该参数)实测,**未受本次改动影响**:
```json
{"code":100001,"message":"参数非法: 月份不能为空","data":null,"traceId":null,"success":false}
```
#### 业务边界
- 🔴 `month` 越界(不在 1-12,如 0、13)从此前的框架码 100001 改为业务码 605010,与 `grid` 端点完全同构;前端若曾经按 100001 识别"月份越界"这一具体场景,需要改成识别 605010。
- `month` 缺失(不传参数)仍是 100001,`@NotNull` 判定发生在 `requireValidYearMonth` 之前,未被本次改动波及,无需新增分支。
- `year` 越界返回 605076(#8561 引入),与本次 `month` 越界的 605010 是两个独立判定,`requireValidYearMonth` 先判年后判月。
---
## 四、契约约束与正确调用方式
> 本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload / 响应 |
|------|-----------------|
| ✅ `month` 在 1-12 区间内(含边界 1、12) | 正常返回 `code=200` |
| ❌ `month` 不在 1-12 区间(如 0、13) | 返回 `code=605010` |
| ❌ 不传 `month` 参数 | 返回 `code=100001`(未受本次改动影响) |
| ❌ 继续按 `code=100001` 识别"月份越界"这一具体场景 | 本端点越界场景已改为 605010,100001 现在只对应"缺参" |
### 切换状态时的必要动作
前端若此前对本端点单独写过"`code=100001` → 月份超出范围"的文案分支,需要改成识别 `605010`(文案为"月份超出范围"),并与 `grid` 端点共用同一套 605010 处理逻辑;100001 的处理分支需要改为对应"参数缺失"。
---
## 五、数据库行为
本次涉及的接口为只读查询,无任何数据库写操作。改动只是把一段入参校验从框架注解移到 Service 层方法内,不涉及任何表结构或存量数据变化。
---
## 六、边界行为
- `month` 不在 1-12 → 605010(本次改动后的新行为,此前是 100001)
- `month` 缺失(未传参数)→ 100001(既有行为,未改动)
- `year` 越界 → 605076(#8561 引入的独立判定,先于 `month` 判定执行)
- `month` 在 1-12 且 `year` 合法 → 正常返回,字段结构未变
---
## 六.5、枚举 / 数据字典
### 月份越界错误码(`AssignmentErrorCode.MATRIX_MONTH_OUT_OF_RANGE`)
**所属字段**: 无(HTTP 响应顶层 `code`) | **类型**: `Integer`
| 值 | 中文 | 本次是否新增 | 说明 |
|----|------|------|------|
| `605010` | 月份超出范围 | 端点内是新用法(既有码,`grid` 端点此前已在用) | `unassigned-orders` 本次起对 `month` 越界统一返回该码,与 `grid` 同构 |
| `100001` | 参数非法 | 未变 | `month` 缺失(未传参数)时仍返回,文案为"参数非法: 月份不能为空" |
## 六.6、修改前后对比
### 字段级对比
本次无请求/响应字段新增或删除;`month` 字段摘掉了 `@Min(1)/@Max(12)` 注解,`@NotNull` 保留,字段本身仍是必填 `Integer`。
### 行为级对比
| 场景 | 改前 | 改后 |
|------|------|------|
| `unassigned-orders`,`month` 越界(如 0、13) | 返回框架码 `100001`(`@Min/@Max` 拦截) | 返回业务码 `605010` |
| `unassigned-orders`,`month` 缺失 | 返回 `100001` | 不变,仍返回 `100001` |
| `grid`,`month` 越界 | 返回 `605010` | 不变,仍返回 `605010`(本次未改动 grid,仅用于对照验证未回归) |
| `unassigned-orders`,`month` 在 1-12 | 正常返回 200 | 不变,仍正常返回 200 |
## 六.7、影响评估
- **是否破坏向后兼容**: 是——`month` 越界时的错误码从 `100001` 变为 `605010`,前端若按具体码值做过分支判断,命中该场景的分支需要更新。
- **前端是否必须同步上线**: 是(仅针对本端点单独维护过 100001 越界分支的场景)——若前端此前对 `unassigned-orders` 单独写过"`code=100001` 即月份越界"的判断,现在需要改为识别 `605010`;若前端此前是把 100001 统一当作"参数错误"泛化处理且未细分场景,则不受影响。
- **前端 workaround 清理点**: 若此前为"同一类月份错误在 grid 和 unassigned-orders 上分别处理"写过两套逻辑,现在两端点已统一为 605010,可以合并成一套。
---
## 七、不影响范围
- **仅影响**: `unassigned-orders` 端点在 `month` 入参越界(不在 1-12)时的错误码。
- **零影响**:
- `month` 缺失时的错误码(仍是 100001)
- `year` 校验逻辑(605076,属 #8561 独立改动)
- `grid`/`month-counts` 两个端点的既有行为
- `unassigned-orders` 在 `month` 合法时的成功路径字段结构
---
## 八、测试环境已验证
服务:`hl-fleet-service`,dev-v3 分支部署测试网关 @ `9c7ac9382`(含 #8571 所在提交),测试网关 `https://api.test.1814.love`:
```
✓ GET unassigned-orders?year=2026&month=13 → code=605010, message="月份超出范围"(此前应为 100001)
✓ GET unassigned-orders?year=2026&month=0 → code=605010
✓ GET unassigned-orders?year=2026&month=1(下边界)→ code=200, data=[]
✓ GET unassigned-orders?year=2026&month=12(上边界)→ code=200, data=[]
✓ GET unassigned-orders?year=2026(不传 month)→ code=100001, message="参数非法: 月份不能为空"(@NotNull 未被误摘)
✓ GET grid?year=2026&month=13 → code=605010(grid 既有行为,未回归)
```
---
## 十、相关文档
- 关联 Issue: [wx/HL#8571](https://git.1814.love/wx/HL/issues/8571)
- 关联 PR: [wx/HL#8582](https://git.1814.love/wx/HL/pulls/8582)
## 关联 / 联系人
### 链接
- **Issue**: [#8571](https://git.1814.love/wx/HL/issues/8571)
- **PR**: [#8582](https://git.1814.love/wx/HL/pulls/8582)
- **Merge commit**: [6634d0588d](https://git.1814.love/wx/HL/commit/6634d0588d)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,623 @@
---
schema: "hl-changelog/v2"
ticket: "8576"
title: "团期配车提交 / 确认响应新增 specWarnings 车辆规格提醒清单(非错误)"
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: "PR #8602 已 squash 合并 dev-v3(cfefe04a83),hl-fleet-service dev-v3 分支已滚测试服。纯新增字段,两个端点的既有字段、错误码、HTTP 形态全部未变。【frontend_status 取 not_required 的依据,2026-09-30 对 hl-ui origin/v2.1 第三次复核】两个写口在前端已无消费方:#8464(2026-09-28,提交 d7e932ac)整体删除了 src/views/fleet/group-dispatch/ 页面模块,src/api/fleet/group-dispatch.js 随之收缩到只剩 getGroupDispatchPendingBatches 一个出口(该文件头部注释明写 reconfigure / confirm / readiness / share-groups / share-member-candidates 已移除,页面恢复时从 git 历史找回)。复核读数:specWarnings 全仓 1 命中且落在 .claude/agents/memory/ 的备忘文件里、src/ 下 0;reconfigure 在 src/ 下的命中全部是注释或 order-v2「受控重开窗口」令牌机制的同词异义。阳性对照:同目录 13 个文件有 export function、matrix.js 有活跃消费方,故检索本身有分辨力。⚠️ 本条 2026-09-30 一度被我改成 pending,依据是「group-dispatch.js 消费 reconfigure / confirm」—— 那是读了落后 693 个提交的本地工作树得出的,该判断作废;后端端点仍全部保留可用,日后恢复页面时按 specWarnings[].code 分支弹提醒即可。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# hl-fleet-service: 团期配车提交 / 确认响应新增 `specWarnings` 车辆规格提醒清单
> **存放目录**: `changelogs-v2/2026-09/`
> **服务**: hl-fleet-service (端口 8089)
> **PR**: #8602
> **Issue**: #8576
> **日期**: 2026-09-30
> **影响范围**: 团期配车写口两个端点 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure` 与 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/confirm` 的响应体各新增一个 `specWarnings` 数组
---
## ⚠️ 关键变化
- 两个端点的响应体**各新增一个字段** `specWarnings`(`List<GroupDispatchSpecWarningRespVO>`)。**没有删字段、没有改名、没有改类型**,既有字段与错误码一个都没动。
- 🔴 **`specWarnings` 不是错误**:它出现在 **HTTP 200 + 业务成功**的响应里。提交照常成功、确认照常成功、`coverage.satisfied` 照常按「排没排满」给结论。它回答的是另一个问题——**排上的那辆车,是不是这个分组当初要的那一类、那么多座**。前端不要把它当失败处理,也不要因为它非空就回滚本地状态。
- 之前团期配车这条路**完全不读车辆实体的车型与座位**:声明 16 座大巴、实际派进 5 座 SUV,一路能确认到终态且零信号。本次补的就是这个信号。
- 提醒分两类,**字段分两组、互不相干**,前端按 `code` 分支取值即可(另一组字段在各自提醒里恒为 `null`):
- `WARN_VEHICLE_TYPE_MISMATCH`(**车级**):点名到某一辆车,带 `vehicleId` / `vehiclePlate` / `declaredVehicleType` / `actualVehicleType`。
- `WARN_GROUP_SEATS_BELOW_SPEC`(**组级**,一个分组最多一条):点名到组和不达标的服务日,带 `declaredSeats` / `declaredVehicleCount` / `declaredSeatTotal` / `actualSeatTotal` / `shortageDates`。
- 两端的作用域不同,别混读:
- **reconfigure**:只覆盖**本次提交新增或就地改过**的「分组 + 车辆」组合,不含本次没动的存活行(否则一次只改司机的提交会把历史遗留的不符行一起刷出来)。
- **confirm**:只覆盖**本次由「已派车」推进为「已确认」**的那些行。所以**重复确认(幂等重放)时恒为空列表**——那一次没有任何行被推进,清单的分母是空的。
- **无提醒时是空数组 `[]`,不会是 `null`**,可直接 `v-for`。
---
## 一、背景
团期配车的「声明」来自正式团级用车需求的乘车分组(组码、车型、单车座位数 `seats`、每日车辆数 `vehicleCount`),「实际」来自车辆档案(车型 `typeKey`、座位数)。改前这两侧从来没被比对过,派错车型 / 座位不够在整条链路上零信号。
本次做成**提醒而不是硬拒**有两条已定口径的原因:
| 维度 | 为什么不做成错误码 |
|------|-------------------|
| 座位不足 | 就绪判定里它已经是 wx 在 #7444 D9 拍板的「只提醒」档(`GroupDispatchReadinessService.WARN_SEAT_SHORTAGE`),硬拒会与该定案冲突 |
| 车型不符 | 两侧处在同一字典的不同归一层级,且两侧都合法地存在取不到值的行(车辆大类行缺失 / 存量需求的历史自由文本),硬拒会把现在能正常干活的分派拦下来 |
与既有的 `GroupDispatchReadinessItemVO` 分工不同:那一份比的是「已扣司机座的可载客数 vs 该日实际用车人数」,回答「坐不坐得下」;本份比的是「名义座位合计 vs 需求方声明的计划容量 `seats × vehicleCount`」,回答「派的车是不是按计划来的」。右值不同源,不是重复。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 整团逐日配车提交 | POST | `/admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure` | 响应新增字段 | 新增 `specWarnings`,覆盖本次新增或就地改过的组+车组合 |
| 2 | 确认整团配车 | POST | `/admin/fleet/group-dispatch/batches/{groupBatchId}/confirm` | 响应新增字段 | 新增 `specWarnings`,覆盖本次被推进为「已确认」的行;重复确认恒为空 |
---
## 三、接口详情
### 1. 整团逐日配车提交 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure`
**VO**: `GroupDispatchReconfigureReqVO → GroupDispatchReconfigureRespVO`
#### 使用场景
团期配车页点「提交」时调用,按乘车分组提交整团逐日配车计划,服务端与现状差量比对(多删少补,旧记录软删留痕)。权限点 `fleet:group-dispatch:write`。本次改动只在响应里多加一个提醒清单,提交本身的行为与校验一条都没变。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `groupBatchId` | Path | Long | ✅ | - | 团期主订单 ID |
| `requirementId` | Body | Long | ✅ | `@NotNull` | 正式团级用车需求 ID,必须等于基线当前活跃需求,落后抛 602005 |
| `requirementVersion` | Body | Integer | ✅ | `@NotNull` | 正式团级用车需求版本,同上 |
| `clearAll` | Body | Boolean | ❌ | - | 显式整团清零标志;为 `true` 时 `demands` 只当待清日用,不会写入任何配车行 |
| `reconfigureWindowToken` | Body | String | ❌ | 团期已过资源准备阶段时必填 | 受控重开窗口令牌 |
| `survivorPolicy` | Body | String | ❌ | `clearAll=true` 且存在 active 共用关系时必填 | 幸存共用派单处置策略 |
| `demands` | Body | Array | ❌ | `@Valid`;`clearAll=false` 时必填 | 逐日配车需求列表 |
| `demands[].tripDate` | Body | String(`yyyy-MM-dd`) | ✅ | `@NotNull` | 行程日期 |
| `demands[].assignments` | Body | Array | ✅ | `@Valid` | 当日排车项列表 |
| `demands[].assignments[].groupId` | Body | String | ✅ | `@NotBlank`,`@Size(max=64)` | 乘车分组键(= 需求侧 `group_code`),空值返 HTTP 业务 400「乘车分组不能为空」 |
| `demands[].assignments[].vehicleId` | Body | Long | ✅ | `@NotNull` | 派出车辆 ID |
| `demands[].assignments[].driverId` | Body | Long | ❌ | - | 派出司机 ID;可空 = 仅排车未排司机 |
| `demands[].assignments[].remark` | Body | String | ❌ | `@Size(max=200)` | 备注 |
#### 出参 `Result<GroupDispatchReconfigureRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `groupBatchId` | String | 团期主订单 ID(雪花,字符串) |
| `requirementId` | String | 正式团级用车需求 ID(雪花,字符串) |
| `requirementVersion` | Integer | 正式团级用车需求版本 |
| `planVersion` | Long | 团期计划版本 |
| `addedCount` | Integer | 新增派车记录数 |
| `removedCount` | Integer | 软删派车记录数 |
| `keptCount` | Integer | 保留未变派车记录数 |
| `updatedCount` | Integer | 就地更新派车记录数 |
| `aliveCount` | Integer | 存活派车记录总数 |
| `addedDispatchIds` | Array\<String\> | 新增派车记录主键列表(雪花,字符串) |
| `idempotentShortCircuit` | Boolean | 本次是否被计划去重短路;`true` 是**幂等成功**,不是失败 |
| `coverage` | Object | 按乘车分组的覆盖明细,见下 |
| `coverage.groups[]` | Array | 每组:`groupCode` / `vehicleType` / `requiredDates` / `coveredDates` / `missingDates` / `outOfRangeDates` / `satisfied` |
| `coverage.missingGroupCodes` | Array\<String\> | 整组未提交的组码 |
| `coverage.wholeBatchSatisfied` | Boolean | 全团行程日整体覆盖是否成立 |
| `legacyGroupRowCount` | Integer | 无分组键的历史派车行数(非错误,仅留痕) |
| `releasedShareGroupIds` | Array\<String\> | 本次连带解除的共用关系 ID 清单 |
| `keptSourceIds` | Array\<String\> | 保留占用的 claim 来源 ID 清单 |
| `releasedSourceIds` | Array\<String\> | 占用已被真正释放的派单 ID 清单 |
| `pendingReassignSourceIds` | Array\<String\> | 待人工改派的派单 ID 清单(占用已释放,当前无车) |
| `ignoredDemandDays` | Array\<String\> | 因 `clearAll=true` 未被写入的行程日清单;`clearAll=false` 时为空列表 |
| `specWarnings` | Array | 🆕 所派车辆与分组声明不符的提醒清单(**非错误,不影响提交成败**);无提醒为空数组 |
| `specWarnings[].code` | String | `WARN_VEHICLE_TYPE_MISMATCH` / `WARN_GROUP_SEATS_BELOW_SPEC` |
| `specWarnings[].message` | String | 中文描述,已点名到组与车牌 / 服务日,可直接展示 |
| `specWarnings[].groupCode` | String | 相关乘车分组码;**两类提醒都有值** |
| `specWarnings[].vehicleId` | String | 相关车辆 ID(雪花,字符串);**仅 `WARN_VEHICLE_TYPE_MISMATCH`** |
| `specWarnings[].vehiclePlate` | String | 相关车牌;车辆档案未录车牌时为 null(`message` 里已退回 `ID=车辆ID`) |
| `specWarnings[].declaredVehicleType` | String | 分组声明车型(归一后的规范大类 key,如 `bus`);**仅 `WARN_VEHICLE_TYPE_MISMATCH`** |
| `specWarnings[].actualVehicleType` | String | 车辆实际车型(归一后的规范大类 key,如 `suv`);**仅 `WARN_VEHICLE_TYPE_MISMATCH`** |
| `specWarnings[].declaredSeats` | Integer | 分组声明的**单车**座位数(含司机座);**仅 `WARN_GROUP_SEATS_BELOW_SPEC`** |
| `specWarnings[].declaredVehicleCount` | Integer | 分组声明的**每日**车辆数;**仅 `WARN_GROUP_SEATS_BELOW_SPEC`** |
| `specWarnings[].declaredSeatTotal` | Integer | 每日总容量 = `declaredSeats × declaredVehicleCount`,**后端算好回传,前端不要自己乘**;**仅 `WARN_GROUP_SEATS_BELOW_SPEC`** |
| `specWarnings[].actualSeatTotal` | Integer | 不达标服务日里**最低**那一天的实际座位合计;**仅 `WARN_GROUP_SEATS_BELOW_SPEC`** |
| `specWarnings[].shortageDates` | Array\<String\> | 实际座位合计低于声明总容量的服务日,升序;**仅 `WARN_GROUP_SEATS_BELOW_SPEC`** |
#### 请求示例
```json
{
"requirementId": 5501,
"requirementVersion": 3,
"clearAll": false,
"demands": [
{
"tripDate": "2026-09-13",
"assignments": [
{ "groupId": "BUS", "vehicleId": 1001, "driverId": 2001, "remark": "AA 团 7 座商务" }
]
},
{
"tripDate": "2026-09-14",
"assignments": [
{ "groupId": "BUS", "vehicleId": 1001, "driverId": 2001, "remark": null }
]
}
]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"groupBatchId": "8801",
"requirementId": "5501",
"requirementVersion": 3,
"planVersion": 7,
"addedCount": 2,
"removedCount": 0,
"keptCount": 3,
"updatedCount": 0,
"aliveCount": 5,
"addedDispatchIds": ["9001", "9002"],
"idempotentShortCircuit": false,
"coverage": {
"groups": [
{
"groupCode": "BUS",
"vehicleType": "bus",
"requiredDates": ["2026-09-13", "2026-09-14"],
"coveredDates": ["2026-09-13", "2026-09-14"],
"missingDates": [],
"outOfRangeDates": [],
"satisfied": true
}
],
"missingGroupCodes": [],
"wholeBatchSatisfied": true
},
"legacyGroupRowCount": 0,
"releasedShareGroupIds": [],
"keptSourceIds": [],
"releasedSourceIds": [],
"pendingReassignSourceIds": [],
"ignoredDemandDays": [],
"specWarnings": [
{
"code": "WARN_VEHICLE_TYPE_MISMATCH",
"message": "分组 BUS 声明车型 大巴客车,所派车辆 蒙P318A 实际为 SUV",
"groupCode": "BUS",
"vehicleId": "1001",
"vehiclePlate": "蒙P318A",
"declaredVehicleType": "bus",
"actualVehicleType": "suv",
"declaredSeats": null,
"declaredVehicleCount": null,
"declaredSeatTotal": null,
"actualSeatTotal": null,
"shortageDates": null
},
{
"code": "WARN_GROUP_SEATS_BELOW_SPEC",
"message": "分组 BUS 声明每日总容量 16 座(单车 16 座 × 1 辆),实际座位合计最低仅 5 座,涉及 2 个服务日:[2026-09-13, 2026-09-14]",
"groupCode": "BUS",
"vehicleId": null,
"vehiclePlate": null,
"declaredVehicleType": null,
"actualVehicleType": null,
"declaredSeats": 16,
"declaredVehicleCount": 1,
"declaredSeatTotal": 16,
"actualSeatTotal": 5,
"shortageDates": ["2026-09-13", "2026-09-14"]
}
]
}
}
```
#### 空数据 / 降级响应
无提醒时 `specWarnings` 是**空数组**,不是 `null`:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"groupBatchId": "8801",
"addedCount": 0,
"removedCount": 0,
"keptCount": 5,
"updatedCount": 0,
"aliveCount": 5,
"idempotentShortCircuit": true,
"specWarnings": []
}
}
```
**降级(fail-open)规则** —— 下列情况提醒**不产出**,`specWarnings` 少一条或为空,这是设计行为不是丢数据:
- 分组不在基线的权威分组清单里 → 该组整组跳过。
- 车辆在本次的车辆档案快照里取不到 → 该车跳过。
- 声明侧或实际侧任一方的车型归一不出规范 key(车辆大类行缺失 / 存量需求的历史自由文本)→ 不报车型不符。
- 分组的 `seats` 或 `vehicleCount` 为 null 或 ≤ 0 → 不做容量判定。
- 某个(分组 + 服务日)格里**只要有一辆车**的座位数取不到或 ≤ 0 → **整格不判**(不把缺值折成 0,折 0 会产出一个不存在的缺口)。
#### 错误响应
既有错误码一条都没变。示例(分组不在本团需求内,消息模板 `乘车分组不存在于本团正式需求: {0}`):
```json
{
"code": 602001,
"message": "乘车分组不存在于本团正式需求: VAN",
"success": false,
"data": null
}
```
完整错误码:602000(排车项缺分组,服务层兜底;admin 口由入参校验先拦下返 400「乘车分组不能为空」)/ 602001 分组不在本团需求内 / 602002 整组未排车 / 602003 该组服务日未排满 / 602004 该组排了本组服务范围外的日期 / 602005 需求身份或版本已变 / 602006 需求状态不允许 / 602009 取不到权威分组清单 / 600003 重复行程日 / 600004 单日排车为空 / 600005 缺车辆 ID / 600006 车辆被占 / 600007 司机被占 / 600008 并发修改 / 600009 基线不可用 / 600010 团期状态不可配 / 600011 全团服务日未覆盖满 / 605037 车辆维保或停用不可派 / 605038 司机休假或待激活不可派 / 605006 司机已黑名单 / 605013 司机非在册赛季不可派单。
#### 业务边界
- **鉴权**:权限点 `fleet:group-dispatch:write`(与读口 `fleet:group-dispatch:view` 分开);未登录由网关拦截返 401。
- **`specWarnings` 非错误**:HTTP 200 + `success=true` 的响应里出现,提交已成功落库。不要据此回滚本地状态或阻断后续动作。
- **作用域**:只覆盖**本次提交新增或就地改过**的「分组 + 车辆」组合;本次没动的存活行不重判(一次只改司机的提交不会把历史遗留的不符行刷出来)。
- **两类提醒字段分组互斥**:按 `code` 分支取值,另一组字段恒 `null`。
- **`declaredSeatTotal` 由后端算好**:口径(含不含司机座、按不按日)只有一份权威,前端不要复算。
- **`actualSeatTotal` 是最低值不是明细**:它与 `declaredSeatTotal` 一起答完「最坏差多少」,不需要拿 `shortageDates` 反查每一天。
- **防重提交与幂等是两件事**:10 秒内对同一份计划重复提交会被防重窗口**拒绝**(返「团期配车重配处理中,请勿重复提交」);窗口之外重复提交同一份计划会正常受理并返回 `idempotentShortCircuit=true`,**那是成功**。
- **`clearAll=true` 时必须读 `ignoredDemandDays`**:否则「清完并按新计划重排」与「只清空」在响应里长得一模一样(两者 `addedCount` 都是 0)。
- **雪花 ID 一律是字符串**:`groupBatchId` / `requirementId` / `addedDispatchIds[]` / `specWarnings[].vehicleId` 等都以字符串下发。
---
### 2. 确认整团配车 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/confirm`
**VO**: `GroupDispatchConfirmReqVO → GroupDispatchConfirmRespVO`
#### 使用场景
团期配车页点「确认」时调用,把该团全部「已派车」的配车行转为「已确认」,并登记一条把正式用车需求推进到「已发车务」的异步回写意图。权限点与提交写口同一个 `fleet:group-dispatch:write`。本次改动只在响应里多加一个提醒清单。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `groupBatchId` | Path | Long | ✅ | - | 团期主订单 ID |
| `requirementId` | Body | Long | ✅ | `@NotNull` | 正式团级用车需求 ID |
| `requirementVersion` | Body | Integer | ✅ | `@NotNull` | 正式团级用车需求版本 |
| `remark` | Body | String | ❌ | `@Size(max=200)` | 确认备注,**仅留痕**,不写入配车行 |
#### 出参 `Result<GroupDispatchConfirmRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `groupBatchId` | String | 团期主订单 ID(雪花,字符串) |
| `confirmedCount` | Integer | 本次由「已派车」转为「已确认」的配车行数;**重复确认为 0,属幂等成功** |
| `alreadyConfirmedCount` | Integer | 确认前就已是「已确认」的配车行数(重复确认时全部落在这里) |
| `requirementId` | String | 本次确认所依据的正式团级用车需求 ID(雪花,字符串) |
| `requirementVersion` | Integer | 本次确认所依据的需求版本 |
| `planVersion` | Long | 当前团期计划版本(确认不改计划,故不递增) |
| `requirementAdvanceIntent` | String | 已登记的需求回写意图方向,恒为 `CONFIRMED_TO_DISPATCHED` |
| `coverage` | Object | 按乘车分组的覆盖明细(确认前对库里现存配车行重判一次的结果),结构同 reconfigure |
| `legacyGroupRowCount` | Integer | 本团存活派车行里没有乘车分组键的历史行数;非零时 602008 的缺口很可能正是它们造成的 |
| `specWarnings` | Array | 🆕 本次被推进为「已确认」的行里,所派车辆与分组声明不符的提醒清单(**非错误,确认已成功**);无提醒为空数组 |
| `specWarnings[].code` | String | `WARN_VEHICLE_TYPE_MISMATCH` / `WARN_GROUP_SEATS_BELOW_SPEC` |
| `specWarnings[].message` | String | 中文描述,已点名到组与车牌 / 服务日,可直接展示 |
| `specWarnings[].groupCode` | String | 相关乘车分组码;两类提醒都有值 |
| `specWarnings[].vehicleId` | String | 相关车辆 ID(雪花,字符串);仅 `WARN_VEHICLE_TYPE_MISMATCH` |
| `specWarnings[].vehiclePlate` | String | 相关车牌;未录车牌时为 null |
| `specWarnings[].declaredVehicleType` | String | 分组声明车型(规范 key);仅 `WARN_VEHICLE_TYPE_MISMATCH` |
| `specWarnings[].actualVehicleType` | String | 车辆实际车型(规范 key);仅 `WARN_VEHICLE_TYPE_MISMATCH` |
| `specWarnings[].declaredSeats` | Integer | 分组声明单车座位数;仅 `WARN_GROUP_SEATS_BELOW_SPEC` |
| `specWarnings[].declaredVehicleCount` | Integer | 分组声明每日车辆数;仅 `WARN_GROUP_SEATS_BELOW_SPEC` |
| `specWarnings[].declaredSeatTotal` | Integer | 每日总座位数(后端算好);仅 `WARN_GROUP_SEATS_BELOW_SPEC` |
| `specWarnings[].actualSeatTotal` | Integer | 不达标日中的最低实际座位合计;仅 `WARN_GROUP_SEATS_BELOW_SPEC` |
| `specWarnings[].shortageDates` | Array\<String\> | 不达标的服务日(升序);仅 `WARN_GROUP_SEATS_BELOW_SPEC` |
#### 请求示例
```json
{
"requirementId": 5501,
"requirementVersion": 3,
"remark": "与地接确认车辆无误"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"groupBatchId": "8801",
"confirmedCount": 8,
"alreadyConfirmedCount": 0,
"requirementId": "5501",
"requirementVersion": 3,
"planVersion": 7,
"requirementAdvanceIntent": "CONFIRMED_TO_DISPATCHED",
"coverage": {
"groups": [
{
"groupCode": "BUS",
"vehicleType": "bus",
"requiredDates": ["2026-09-13", "2026-09-14"],
"coveredDates": ["2026-09-13", "2026-09-14"],
"missingDates": [],
"outOfRangeDates": [],
"satisfied": true
}
],
"missingGroupCodes": [],
"wholeBatchSatisfied": true
},
"legacyGroupRowCount": 0,
"specWarnings": [
{
"code": "WARN_VEHICLE_TYPE_MISMATCH",
"message": "分组 BUS 声明车型 大巴客车,所派车辆 蒙P318A 实际为 SUV",
"groupCode": "BUS",
"vehicleId": "1001",
"vehiclePlate": "蒙P318A",
"declaredVehicleType": "bus",
"actualVehicleType": "suv",
"declaredSeats": null,
"declaredVehicleCount": null,
"declaredSeatTotal": null,
"actualSeatTotal": null,
"shortageDates": null
}
]
}
}
```
#### 空数据 / 降级响应
**重复确认(幂等重放)**:`confirmedCount=0`、`alreadyConfirmedCount=N`、`specWarnings` 恒为**空数组**(本次没有任何行被推进,清单的分母是空的)。HTTP 仍是 200,**这是成功不是失败**:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"groupBatchId": "8801",
"confirmedCount": 0,
"alreadyConfirmedCount": 8,
"requirementId": "5501",
"requirementVersion": 3,
"planVersion": 7,
"requirementAdvanceIntent": "CONFIRMED_TO_DISPATCHED",
"legacyGroupRowCount": 0,
"specWarnings": []
}
}
```
**降级(fail-open)规则**与 reconfigure 端点逐条相同:分组不在基线 / 车辆取不到 / 任一侧车型归一不出规范 key / `seats` 或 `vehicleCount` 为 null 或 ≤0 / 某(组+日)格里有一辆车座位取不到 → 对应提醒不产出。
#### 错误响应
既有错误码一条都没变。示例(现存配车对当前需求仍不完整,消息模板 `配车尚未覆盖完整, 不能确认: {0}`):
```json
{
"code": 602008,
"message": "配车尚未覆盖完整, 不能确认: 乘车分组 BUS 的服务日未排满, 缺失: [2026-09-15]",
"success": false,
"data": null
}
```
完整错误码:602007 本团无可确认的配车行 / 602008 现存配车对当前需求仍不完整 / 602005 需求身份或版本已变 / 602006 需求状态不允许 / 602009 取不到权威分组清单 / 600008 并发修改 / 600009 基线不可用 / 605037 车辆维保或停用不可派 / 605038 司机休假或待激活不可派 / 605006 司机已黑名单 / 605013 司机非在册赛季不可派单。
#### 业务边界
- **鉴权**:权限点 `fleet:group-dispatch:write`(与提交写口同一个);未登录由网关拦截返 401。
- **`specWarnings` 非错误**:确认已经成功。它是终态前的最后一次复核——重配与确认之间车辆档案可能被改过,也可能有行绕过重配直接进来。
- **作用域**:只覆盖**本次由「已派车」推进为「已确认」**的那些行;已是「已确认」的行不重判。
- **重复确认时恒为空列表**:不要把「第二次点确认没有提醒」理解成「问题已经消失」。
- **本端点没有防重提交时间窗**:连点多少次都是 `confirmedCount=0 / alreadyConfirmedCount=N` 这个形态,不会出现「请勿重复提交」这类错误码;同团的并发调用由服务端串行化。
- **异步回写**:响应成功只代表车务侧已确认并已把回写意图可靠登记,正式用车需求的状态可能稍后才变成「已发车务」,需求页需自行刷新。
- **回写会推进需求版本但不会让重复确认变成错误**:首次确认成功后正式用车需求被推进一版(status 转 DISPATCHED),此时本端点跳过需求版本与状态的严格校验,仍返回 200 + 两个计数。
- **不校验团期是否可配**:那道门禁管的是「还能不能改车」,确认不改车。
- **雪花 ID 一律是字符串**。
---
## 四、契约约束与正确调用方式
> 本节只写后端接受 / 拒绝 payload 的规则,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload |
|------|---------|
| ✅ 正常提交两天配车 | `{ "requirementId": 5501, "requirementVersion": 3, "clearAll": false, "demands": [ { "tripDate": "2026-09-13", "assignments": [ { "groupId": "BUS", "vehicleId": 1001 } ] } ] }` |
| ✅ 整团清零 | `{ "requirementId": 5501, "requirementVersion": 3, "clearAll": true, "demands": [] }` |
| ✅ 确认(带留痕备注) | `{ "requirementId": 5501, "requirementVersion": 3, "remark": "与地接确认车辆无误" }` |
| ✅ 确认(不带备注) | `{ "requirementId": 5501, "requirementVersion": 3 }` |
| ❌ 排车项缺分组键 | `{ ..., "assignments": [ { "vehicleId": 1001 } ] }` → 400「乘车分组不能为空」 |
| ❌ 缺需求版本 | `{ "requirementId": 5501, "demands": [...] }` → 400「正式团级用车需求版本不能为空」 |
| ❌ 确认备注超 200 字 | `{ ..., "remark": "<201 字>" }` → 400「确认备注长度不能超过 200」 |
### 处理 `specWarnings` 的必要动作
- 两个端点的成功分支里都要读 `specWarnings`:非空时就地展示(`message` 已经是完整中文句子,可直接渲染),**不要**把它接到错误处理分支上。
- 按 `code` 分支取字段,不要对全部字段做非空假设——另一类提醒的字段组恒为 `null`。
- `declaredSeatTotal` 直接用后端回传的值,不要用 `declaredSeats × declaredVehicleCount` 自己算。
- reconfigure 的 `specWarnings` 只反映本次动过的行:`specWarnings` 为空**不等于**全团没有不符行,只等于「本次动的这些行没有不符」。
---
## 五、数据库行为
两个端点都是写端点,但**本次改动零写入变化**——`specWarnings` 完全由内存中的比对产出(`GroupDispatchVehicleSpecInspector` 是纯静态、无 IO),不新建表、不加列、不落任何提醒记录。
| 前端提交 | 配车行的写入 | 提醒的持久化 |
|----------|--------------|--------------|
| `reconfigure` 差量提交 | 多删少补,旧记录软删留痕(本次未变) | **不落库**,仅随本次响应下发 |
| `reconfigure` `clearAll=true` | 清空存活行,`demands` 不写入 | **不落库** |
| `confirm` 首次确认 | 「已派车」行 CAS 推进为「已确认」,登记回写意图(本次未变) | **不落库** |
| `confirm` 重复确认 | 一个字段都不动 | **不落库**,且恒为空数组 |
因此**刷新页面或重新拉取不会再拿到同一批提醒**——提醒是本次动作的返回值,不是可查询的状态。
---
## 六、边界行为
- 未登录 → 401(网关拦截)。
- 权限点 `fleet:group-dispatch:write` 缺失 → 权限校验失败。
- 团期不存在 / 基线不可用 → 600009。
- 需求身份或版本落后 → 602005(fail-closed,不接受「反正车没变」)。
- 10 秒内重复提交同一份 reconfigure 计划 → 被防重窗口拒绝,提示「团期配车重配处理中,请勿重复提交」。
- 窗口外重复提交同一份计划 → 200 + `idempotentShortCircuit=true`(成功)。
- 重复 confirm → 200 + `confirmedCount=0`、`specWarnings=[]`(成功)。
- 车辆档案未录车牌 → `specWarnings[].vehiclePlate` 为 null,但 `message` 里退回 `ID=车辆ID`,不留空白。
- 老数据兼容:历史派车行没有乘车分组键时不计入任何组的覆盖,计入 `legacyGroupRowCount`,也不进 `specWarnings`。
---
## 六.5、枚举 / 数据字典
### code(车辆规格提醒项代码)
**所属字段**: `specWarnings[].code` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `WARN_VEHICLE_TYPE_MISMATCH` | 车型与分组声明不符 | **车级**提醒。两侧车型各自归一成规范大类 key 后不相等时产出。带 `vehicleId` / `vehiclePlate` / `declaredVehicleType` / `actualVehicleType`;其余字段为 null |
| `WARN_GROUP_SEATS_BELOW_SPEC` | 分组座位低于声明容量 | **组级**提醒,一个分组最多一条。判据:`该组该日实际座位合计 < seats × vehicleCount`。带 `declaredSeats` / `declaredVehicleCount` / `declaredSeatTotal` / `actualSeatTotal` / `shortageDates`;其余字段为 null |
### declaredVehicleType / actualVehicleType(归一后的车型规范大类 key)
**所属字段**: `specWarnings[].declaredVehicleType`、`specWarnings[].actualVehicleType` | **类型**: `String`
取值是车型字典归一后的**规范大类 key**(如 `bus` / `suv`),**不是**车辆档案里的原值——车辆档案侧存的是开集原值(例如测试环境 SUV 大类的 `type_key` 实际是 `suv2`),后端归一后才比。前端如需展示中文名,用 `message` 里已经拼好的中文,不要自己拿 key 去查字典。
### requirementAdvanceIntent(需求回写意图方向)
**所属字段**: `GroupDispatchConfirmRespVO.requirementAdvanceIntent` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `CONFIRMED_TO_DISPATCHED` | 已确认 → 已发车务 | 当前**恒为此值**;表示确认成功后还有一步异步回写,需求列表页的状态可能稍后才变 |
---
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `GroupDispatchReconfigureRespVO.specWarnings` | 不存在 | 🆕 `List<GroupDispatchSpecWarningRespVO>`,无提醒为空数组 |
| `GroupDispatchConfirmRespVO.specWarnings` | 不存在 | 🆕 `List<GroupDispatchSpecWarningRespVO>`,无提醒为空数组 |
| 两个响应体的其余全部字段 | — | 未变(无删除、无改名、无类型变化) |
| 两个请求体 | — | 未变(一个字段都没动) |
| 两个端点的错误码集合 | — | 未变 |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 派错车型(声明大巴、实际 SUV) | 全链路零信号,一路能确认到终态 | 提交与确认的响应里各出一条 `WARN_VEHICLE_TYPE_MISMATCH` |
| 某服务日实际座位合计低于声明容量 | 全链路零信号 | 出一条 `WARN_GROUP_SEATS_BELOW_SPEC`,带最低值与不达标日清单 |
| 提交 / 确认的成败判定 | 按覆盖与资源可派性 | 未变——`specWarnings` 不参与成败判定 |
| 重复确认 | `confirmedCount=0`、`alreadyConfirmedCount=N` | 未变,额外 `specWarnings=[]` |
| 只改司机的提交 | — | 不会把历史遗留的不符行刷出来(作用域限本次动过的组+车组合) |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 否(纯新增字段,既有字段与错误码零变化;老前端忽略新字段即可正常工作)
- **前端是否必须同步上线**: 否(不读新字段不会报错,只是拿不到提醒)
- **前端 workaround 清理点**: 无(此前没有任何前端侧的车型 / 座位比对,不存在需要撤掉的本地实现)
---
## 七、不影响范围
- **仅影响**: `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure` 与 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/confirm` 两个响应体各新增一个数组字段。
- **零影响**:
- 团期配车所有读口(概览、就绪判定、矩阵、看板)
- `GroupDispatchReadinessItemVO` 的座位就绪判定(右值不同源,本次未动)
- 单车派单、改派、取消链路
- order-v3 侧的正式团级用车需求存 / 读 / 撤回 / 免车
- 车辆档案、司机档案的任何端点
- 历史数据:提醒不落库,不做任何数据迁移
---
## 八、测试环境已验证
- **代码事实**(对 `origin/dev-v3` 逐一查证):
- 合并提交 `cfefe04a83`(PR #8602 squash 合并进 `dev-v3`)。
- 新增 VO `hl-fleet-service/.../dispatch/vo/GroupDispatchSpecWarningRespVO.java`(12 个字段)与对位 Feign DTO `GroupDispatchSpecWarningDTO`。
- 新增纯静态无 IO 的 `GroupDispatchVehicleSpecInspector`;两条提醒的消息拼装、fail-open 跳过条件、`seatTotalOrNull` 的「一辆车取不到座位就整格不判」逻辑均已逐行核对。
- `GroupDispatchReconfigureRespVO` 与 `GroupDispatchConfirmRespVO` 各新增 `specWarnings` 字段,javadoc 分别写明作用域(本次新增/就地改过 vs 本次被推进)与「重复确认恒为空列表」。
- **部署**:`hl-fleet-service` 的 `dev-v3` 分支已滚到测试服,两个端点走管理端网关 `/admin/fleet/**` 既有路由,无新增路由。
```
POST /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure → 200 + data.specWarnings 存在(无提醒时为 [])✓
POST /admin/fleet/group-dispatch/batches/{groupBatchId}/confirm → 200 + data.specWarnings 存在(无提醒时为 [])✓
POST .../confirm 重复调用 → 200 + confirmedCount=0 + specWarnings=[] ✓
```
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| — | #7442 | 团期配车写口(reconfigure / confirm)首次落地 | ✅ 有效 |
| — | #7444 D9 | wx 拍板座位不足在就绪判定里只提醒不硬拒 | ✅ 有效(本单沿用该口径) |
| — | #8195 | 需求侧车型归一后回写规范 key | ✅ 有效(本单的比对依赖它) |
| — | #8528 | reconfigure / confirm 资源可派性硬校验(605037/605038/605006/605013) | ✅ 有效 |
| **本 PR #8602** | **#8576** | 两个写口响应新增 `specWarnings` | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#8576](https://git.1814.love:8443/wx/HL/issues/8576)
- 关联 PR: [wx/HL#8602](https://git.1814.love:8443/wx/HL/pulls/8602)
## 关联 / 联系人
### 链接
- **Issue**: [#8576](https://git.1814.love:8443/wx/HL/issues/8576)
- **PR**: [#8602](https://git.1814.love:8443/wx/HL/pulls/8602)
- **Merge commit**: [cfefe04a83](https://git.1814.love:8443/wx/HL/commit/cfefe04a83)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,718 @@
---
schema: "hl-changelog/v2"
ticket: "8577"
title: "只提交了接送机的户不再被判「未提交用车需求」,809121/809122/809123 触发条件收窄且文案改写"
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: "PR #8600 已 squash 合并 dev-v3(5b7074e691),hl-order-service-v3 dev-v3 分支已滚测试服。三个端点的请求体、响应体字段与错误码号全部未变,变的是 809121/809122/809123 的触发条件(收窄)与消息文案(去掉「行程」二字)。【frontend_status 取 not_required 的依据,2026-09-30 对 hl-ui origin/v2.1 逐处查证】三个码的展示一律走拦截器透 message,生产代码无一处按报文字符串匹配;纯接送机户在前端也没有任何规避(按钮禁用/提示)需要撤除,故无强制前端动作。⚠️ 但有注释级陈旧需顺手清:src/api/orderV2GroupBatch.js:921,957,958 与 GroupVehicleRequirementEditModal.vue:377,874 仍写着「TRAVEL / 行程用车需求」,三个码现已按 TRAVEL ∪ TRANSFER 判「已提交」;GroupVehicleRequirementSection.spec.js:1000 的 mock 报文同样陈旧(该用例不断言文案,不会红)。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# hl-order-service-v3: 只提交了接送机的户不再被判「未提交用车需求」
> **存放目录**: `changelogs-v2/2026-09/`
> **服务**: hl-order-service-v3 (端口 8086)
> **PR**: #8600
> **Issue**: #8577
> **日期**: 2026-09-30
> **影响范围**: 团期需求管理 Tab 的三个端点(保存正式用车需求 / 自动汇总草稿 / 整体确认预检)里 809121、809122、809123 的触发条件与消息文案
---
## ⚠️ 关键变化
- 🔴 **判据从「有没有提交行程用车(TRAVEL)」收窄为「两类用车需求(TRAVEL / TRANSFER)是不是一条都没有」**。改前:某户只提交了接送机需求,团级保存、自动汇总、确认预检都把它当成「一条都没交」,整团被 809123 / 809121 / 809122 卡住,而这户其实已经明确表达过「只要接送机、不要行程车」,运营**没有任何干净出路**(唯一逃生舱是整团 waive 免车,那会把真需要行程车的户一起免掉)。改后:这户算已提交,三处一律放行。
- **三个错误码的码值没变、字段没变**,变的是**什么时候抛**(收窄)与**消息文案**(三条都去掉了「行程」二字):
- `809121` `团期 {0} 有 {1} 户缺少可汇总的行程用车需求…` → `…缺少可汇总的用车需求…`
- `809122` `该户尚未提交行程用车需求,请先让定制师提交后再整团提交车务` → `该户尚未提交用车需求,…`
- `809123` `{0}有 {1} 户尚未提交行程用车需求,暂不能保存正式用车需求:{2}` → `…尚未提交用车需求,…`
- 🔴 **前端凡是对这三条报文做过关键词匹配 / 字符串包含判断的地方必须改**(`行程用车需求` 这个子串在三条里都没了)。正确做法是按 `code` 分支,不要匹配 `message` 文本。
- 🔴 **809109「逐日覆盖」一个字都没改,仍然只认 TRAVEL**。这是刻意的:本次分离的是「户级提交判定」与「行程覆盖判定」两件事,合并会把墙从 809123 挪到 809109,症状一模一样只是换个码。所以——**只提交接送机的户不再被判未提交,也不要求被任何乘车分组覆盖**;它结构上就在团级乘车分组之外,走逐户派车。
- `GroupVehicleDraftAggregator` 的缺失原因文案 `未提交行程用车需求` → `未提交用车需求`。它出现在 809121 报文的逐户清单里(`「户标识:原因」`,顿号分隔),前端若展示过这个字符串同样受影响。
- 「豁免户」`exemptHouseholds` 的语义边界也随之明确:**只提交了接送机的户既不进未提交名单、也不进豁免名单**——豁免解释的是「没提交的户为什么不拦」,而它本来就提交过。
---
## 一、背景
一户在团期里的用车需求有两类活跃行,互不替代:
| 类别 | 含义 | 派车路径 |
|------|------|----------|
| `TRAVEL` | 团期行程用车 | 汇总进团级乘车分组,整团逐日配车 |
| `TRANSFER` | 接送机 | 逐户派车,**结构上不进团级乘车分组** |
改前的三处判定都只查 `TRAVEL`。于是「只要接送机、不要行程车」这种完全合法的在团户(与 #7972 (A) 对 809114 的定案同源)被读成「什么都没交」。团级保存直接 809123 整份拒绝、自动汇总 809121 整团出不来草稿、确认预检 809122 逐户挂红——**运营改不动、催不动(该户定制师已经交过了)、也绕不过去**。
本次把判定拆成两个集合(`travelSubmittedOrderIds` / `anySubmittedOrderIds`),单源仍只有一份,在 `GroupVehicleRequirementService#classifyVehicleSubmission`(原名 `classifyTravelSubmission`),保存、预检、自动汇总三处共用:
- **户级「交了没有」** → 用 `anySubmittedOrderIds`(两类任一即算交了)→ 管 809121 / 809122 / 809123;
- **行程逐日覆盖** → 仍用 `travelSubmittedOrderIds`(只认 TRAVEL)→ 管 809109。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 保存团期正式用车需求(全量替换) | PUT | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 错误码触发条件收窄 + 文案改写 | 809123 不再对「只提交接送机」的户触发;报文去掉「行程」 |
| 2 | 自动汇总正式用车需求草稿 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` | 错误码触发条件收窄 + 文案改写 | 809121 同上;缺失原因文案同步改写 |
| 3 | 整体确认需求缺失预检 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check` | 缺失项触发条件收窄 + 文案改写 | `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`(809122)同上 |
---
## 三、接口详情
### 1. 保存团期正式用车需求(全量替换) `PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement`
**VO**: `GroupVehicleRequirementSaveReqVO → GroupVehicleRequirementRespVO`
#### 使用场景
团期需求管理 Tab 的「正式用车需求」编辑弹窗点保存时调用,**整份全量替换**(未出现在本次提交里的分组会被移出当前版本)。权限点 `group-batch:demand:confirm`。本次改动只让 809123 少抛一类情况、并改了它的报文,请求体与响应体一个字段都没动。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `groupBatchId` | Path | Long | ✅ | - | 团期 ID |
| `version` | Body | Integer | ❌ | 乐观锁 | **首次保存传 null**,后续必须回传上次 GET / PUT 拿到的值;不一致抛 809102 |
| `remark` | Body | String | ❌ | `@Size(max=500)` | 整份需求备注 |
| `groups` | Body | Array | ✅ | `@NotNull`(**不是** `@NotEmpty`)、`@Valid` | 全部乘车分组;空数组是合法提交(有需车户时由 809103 拦),整团免车请改走 `waive` 端点 |
| `groups[].groupId` | Body | Long | ❌ | - | 既有分组主键;**新增分组传 null**。带上它 = 声明「就是库里那一组」,此时 `groupCode` 不得变更(改名抛 809104) |
| `groups[].groupCode` | Body | String | ✅ | `@NotBlank`,`@Size(max=32)` | 分组键,直接作为车费 `alloc_group` |
| `groups[].vehicleType` | Body | String | ✅ | `@NotBlank`,`@Size(max=64)` | 车型大类编码,**不是自由文本**;取值权威见 `GET /internal/fleet/vehicle-types/category-names`,不在字典内抛 809119 |
| `groups[].serviceStartDate` | Body | String(`yyyy-MM-dd`) | ✅ | `@NotNull` | 本组服务开始日 |
| `groups[].serviceEndDate` | Body | String(`yyyy-MM-dd`) | ✅ | `@NotNull` | 本组服务结束日(须不早于开始日) |
| `groups[].seats` | Body | Integer | ❌ | `@Min(1)` | 该组单车座位数;**刻意非必填**(存量分组没有该值),与 `count` 必须同填或同空(809118),且须在该车型可选档位内(809124) |
| `groups[].count` | Body | Integer | ❌ | `@Min(1)` | 该组车辆数量;同上 |
| `groups[].specialTags` | Body | Array\<String\> | ❌ | 值须在字典 `vehicle_special_demand` 内 | 特殊诉求标签编码数组;含字典外编码整份拒绝(809117) |
| `groups[].remark` | Body | String | ❌ | `@Size(max=500)` | 该组备注 |
| `groups[].days` | Body | Array | ✅ | `@NotEmpty`,`@Valid` | 逐日用车人数与成员,**不能用单值人数代替** |
| `groups[].days[].tripDate` | Body | String(`yyyy-MM-dd`) | ✅ | `@NotNull` | 团期行程日,须落在本组服务日范围内且不缺日(809105 / 809106) |
| `groups[].days[].headcount` | Body | Integer | ✅ | `@NotNull`,`@Min(1)` | 该组该日**乘车人数**(不是户数);小于当日成员户数抛 809110 |
| `groups[].days[].memberOrderIds` | Body | Array\<Long\> | ✅ | `@NotEmpty` | 该组该日实际乘车的子订单集合,须全属本团在团户(809107),同一户同一日只能属一个分组(809108) |
#### 出参 `Result<GroupVehicleRequirementRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `requirementId` | String | 正式用车需求 ID(雪花,字符串) |
| `groupBatchId` | String | 团期 ID(雪花,字符串) |
| `status` | String | 需求状态 |
| `version` | Integer | 乐观锁版本,下次保存必须回传 |
| `remark` | String | 整份需求备注 |
| `confirmedBy` | String | 确认人 |
| `confirmedAt` | String(datetime) | 确认时间 |
| `planRefreshState` | String | 配车刷新状态(只读投影) |
| `planRefreshReplayCount` | Integer | 配车刷新重投次数 |
| `blockedStage` | String | 被卡住的阶段 |
| `planRefreshStalled` | Boolean | 配车刷新是否已停滞 |
| `planRefreshStalledReason` | String | 停滞原因 |
| `planRefreshTimeoutAt` | String(datetime) | 刷新超时时刻 |
| `planRefreshReplayExhausted` | Boolean | 重投次数是否已用尽 |
| `groups` | Array | 乘车分组回显 |
| `groups[].groupId` / `groupCode` / `vehicleType` / `vehicleTypeName` | String | 分组主键(字符串)、分组键、车型大类编码、车型中文名(按归一 key 取) |
| `groups[].serviceStartDate` / `serviceEndDate` | String(`yyyy-MM-dd`) | 本组服务日范围 |
| `groups[].seats` / `count` / `totalSeatCount` / `maxHeadcount` / `remainingPassengerSeats` | Integer | 单车座位数 / 车辆数 / 总座位 / 最大日人数 / 剩余可载客座位 |
| `groups[].specialTags[]` | Array | `code` + `name`(中文名后端下发,前端不自己映射) |
| `groups[].remark` | String | 该组备注 |
| `groups[].days[]` | Array | `tripDate` / `headcount` / `memberOrderIds`(字符串数组) / `memberOrderCount` |
| `exemptHouseholds` | Array | 豁免户(在团需车、两类需求都没有活跃行、但定制师**提交不了**的户);🔴 **只提交了接送机的户不在这里**——它已提交 |
| `exemptHouseholds[].orderId` | String | 子订单 ID(雪花,字符串) |
| `exemptHouseholds[].teamNo` | String | 团号 |
| `exemptHouseholds[].orderNo` | String | 子订单号 |
| `exemptHouseholds[].reason` | String | `ORDER_NOT_CUSTOMIZING` / `REQUIREMENT_FROZEN` |
| `exemptHouseholds[].reasonName` | String | 豁免原因中文名(后端下发,前端不自己映射) |
#### 请求示例
```json
{
"version": 3,
"remark": "9/13 起换大巴",
"groups": [
{
"groupId": null,
"groupCode": "BUS",
"vehicleType": "bus",
"serviceStartDate": "2026-09-12",
"serviceEndDate": "2026-09-16",
"seats": 19,
"count": 1,
"specialTags": ["CHILD_SEAT"],
"remark": "含高速费",
"days": [
{
"tripDate": "2026-09-12",
"headcount": 9,
"memberOrderIds": [2099459272533323777, 2099459272533323778]
}
]
}
]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"requirementId": "2099459272533400001",
"groupBatchId": "2099459272533000001",
"status": "DRAFT",
"version": 4,
"remark": "9/13 起换大巴",
"confirmedBy": null,
"confirmedAt": null,
"planRefreshState": null,
"planRefreshStalled": false,
"groups": [
{
"groupId": "1867000000009",
"groupCode": "BUS",
"vehicleType": "bus",
"vehicleTypeName": "大巴客车",
"serviceStartDate": "2026-09-12",
"serviceEndDate": "2026-09-16",
"seats": 19,
"count": 1,
"totalSeatCount": 19,
"maxHeadcount": 9,
"remainingPassengerSeats": 9,
"specialTags": [{ "code": "CHILD_SEAT", "name": "儿童座椅" }],
"remark": "含高速费",
"days": [
{
"tripDate": "2026-09-12",
"headcount": 9,
"memberOrderIds": ["2099459272533323777", "2099459272533323778"],
"memberOrderCount": 2
}
]
}
],
"exemptHouseholds": []
}
}
```
#### 空数据 / 降级响应
该团期**只有接送机户、没有任何行程用车户**时,提交零分组不再被 809123 拦(本次改动的直接效果);若团里确实还有需车户,零分组仍由 809103 拦下。`exemptHouseholds` 为空时是**空数组**不是 `null`:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"requirementId": "2099459272533400001",
"groupBatchId": "2099459272533000001",
"status": "DRAFT",
"version": 1,
"groups": [],
"exemptHouseholds": []
}
}
```
#### 错误响应
809123(触发条件已收窄、文案已改写;`{0}` 是团期人话标识,`{2}` 按团号列户、无团号回落订单号、都缺时为「某子订单」,顿号分隔;以下为测试服实测原文):
```json
{
"code": 809123,
"message": "团期「第22期 8月喀纳斯湖秋色三日游」有 1 户尚未提交用车需求,暂不能保存正式用车需求:26-6538",
"success": false,
"data": null
}
```
其余错误码一条都没变:809100 团期尚未形成正式用车需求 / 809101 状态不允许 / 809102 已被他人修改(乐观锁) / 809103 有需车户却零分组 / 809104 分组重复或试图改名 / 809105 逐日行不在本组服务日范围内或重复 / 809106 缺逐日用车人数 / 809107 成员不属于本团期 / 809108 同一户同一日属多个分组 / 809109 该子订单的某日没有被任何乘车分组覆盖(**仍只认 TRAVEL**) / 809110 用车人数小于当日成员户数 / 809111 团期状态不允许编辑 / 809115 已声明整团免车需先 withdraw / 809116 座位不足 / 809117 特殊诉求标签不在字典内 / 809118 座位数与车辆数须同填或同空 / 809119 车型不在车型字典内 / 809120 车型字典暂不可用 / 809124 座位数不在该车型可选档位内。
#### 业务边界
- **鉴权**:权限点 `group-batch:demand:confirm`(与整体确认、按户打回、受控重开同码——它们动的是同一个 Tab 里的同一份数据);未登录由网关拦截返 401。
- **全量替换语义**:未出现在本次提交里的分组会被移出当前版本,不是增量补丁。
- **🔴 判据变化只在户级**:「这户交了没有」看两类任一;「行程逐日覆盖」(809109)仍只看 TRAVEL,没变。
- **只提交接送机的户**:不再被 809123 拦、**也不要求被任何乘车分组覆盖**,且**不出现在 `exemptHouseholds` 里**。
- **豁免户不阻断**:`ORDER_NOT_CUSTOMIZING` / `REQUIREMENT_FROZEN` 两类户不进 809123、不参与 809109,但必须在页面上提示出来(后端已逐户带原因下发)。
- **错误码文案是可变的**:`message` 只用于展示,判定一律按 `code`。
- **乐观锁只挡同一瞬间的并发写**:挡不住「A 读了 v3 去改、B 也读了 v3 改完先提交」这种跨请求覆盖。
- **雪花 ID 一律是字符串**(`requirementId` / `groupBatchId` / `memberOrderIds[]` / `exemptHouseholds[].orderId`)。
---
### 2. 自动汇总正式用车需求草稿 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft`
**VO**: `无请求体 → GroupVehicleAggregateDraftRespVO`
#### 使用场景
编辑弹窗点「自动汇总」时调用,按各子订单的活跃 TRAVEL 需求汇总出一份团级草稿,**只读零写入**,返回的 `draft` 可原样 PUT 给上面那个保存端点。权限点与编辑弹窗取数口同码 `group-batch:demand:confirm`。本次改动只让 809121 少抛一类情况并改了它的报文与缺失原因文案。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `groupBatchId` | Path | Long | ✅ | - | 团期 ID |
#### 出参 `Result<GroupVehicleAggregateDraftRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `groupBatchId` | String | 团期 ID(雪花,字符串) |
| `currentStatus` | String | 当前正式需求状态 |
| `draft` | Object | 汇总出的草稿,结构**与保存端点的请求体逐字段相同**,可原样 PUT |
| `droppedFleetItems` | Array | 多车型户被丢弃的车型项:`orderId` / `teamNo` / `orderNo` / `vehicleType` / `seats` / `count` / `keptVehicleType` / `reason`(`VEHICLE_TYPE_NOT_IN_DICT` 等) |
| `staleHeadcountOrders` | Array | 冻结人数与实时人数不一致的户:`orderId` / `teamNo` / `orderNo` / `frozenHeadcount` / `liveHeadcount` |
| `paddedOrderDays` | Array | 为覆盖出发~返回而补进分组的日期:`orderId` / `teamNo` / `orderNo` / `dates[]` |
| `seatOptionAdjusted` | Array | 座位档被兜底调整的组/户:`groupCode` / `orderId` / `teamNo` / `orderNo` / `vehicleType` / `originalSeats` / `adoptedSeats` / `seatOptions[]` / `reason` |
| `violations` | Array | 草稿已先跑过与保存同一份逐日校验的结果:`code`(对应 809xxx) / `reason` / `detail` / `groupCode` / `tripDate` / `orderId` / `teamNo` |
| `exemptHouseholds` | Array | 豁免户(结构同上一个端点);🔴 **只提交了接送机的户不在这里,也不在草稿里** |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2099459272533000001/vehicle-requirement/aggregate-draft
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"groupBatchId": "2099459272533000001",
"currentStatus": "DRAFT",
"draft": {
"version": 3,
"remark": null,
"groups": [
{
"groupId": null,
"groupCode": "BUS",
"vehicleType": "bus",
"serviceStartDate": "2026-09-12",
"serviceEndDate": "2026-09-16",
"seats": 19,
"count": 1,
"specialTags": [],
"remark": null,
"days": [
{
"tripDate": "2026-09-12",
"headcount": 9,
"memberOrderIds": [2099459272533323777]
}
]
}
]
},
"droppedFleetItems": [],
"staleHeadcountOrders": [],
"paddedOrderDays": [],
"seatOptionAdjusted": [],
"violations": [],
"exemptHouseholds": []
}
}
```
#### 空数据 / 降级响应
团里只有接送机户、没有任何可汇总的行程用车户时,草稿分组为空数组而**不再抛 809121**(本次改动的直接效果):
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"groupBatchId": "2099459272533000001",
"currentStatus": "DRAFT",
"draft": { "version": null, "remark": null, "groups": [] },
"droppedFleetItems": [],
"staleHeadcountOrders": [],
"paddedOrderDays": [],
"seatOptionAdjusted": [],
"violations": [],
"exemptHouseholds": []
}
}
```
车型字典取不到时不静默降级,抛 809120 让运营重试(避免把一整份草稿的车型全判成非法)。
#### 错误响应
809121(触发条件已收窄、文案已改写;`{0}` 是团期名标识,`{2}` 是「户标识:原因」顿号分隔的清单,原因文案里的 `未提交行程用车需求` 已改为 `未提交用车需求`;以下为测试服实测原文):
```json
{
"code": 809121,
"message": "团期 「第22期 8月喀纳斯湖秋色三日游」 有 1 户缺少可汇总的用车需求,暂不能自动汇总:26-6538:未提交用车需求",
"success": false,
"data": null
}
```
其余错误码未变:809120 车队车型字典暂不可用 / 809111 团期状态不允许 / 809100 团期尚未形成正式用车需求(视链路)。
#### 业务边界
- **鉴权**:权限点 `group-batch:demand:confirm`;未登录由网关拦截返 401。
- **⛔ 本端点零写入**,可安全重复调用;`draft` 是「按现有子订单需求草稿长什么样」,**不保证保存一定能过**——预跑的校验结果在 `violations`。
- **收窄后的 809121 判据**:需车户「两类用车需求一条都没有」才算信息缺失;只提交接送机的户不算缺少,**也不会出现在草稿里**(团车草稿只汇总 TRAVEL,它本就没有位置)。
- **缺失原因文案已改**:`未提交行程用车需求` → `未提交用车需求`(另有 `车型均不在车型字典内`、服务日推不出、人数为 0 三类未变)。
- **诊断字段必须展示**:`droppedFleetItems` / `staleHeadcountOrders` / `paddedOrderDays` / `seatOptionAdjusted` 都是「草稿与用户预期可能不一致」的位置,静默吞掉会让运营看到一份自己没想要的草稿。
- **雪花 ID 一律是字符串**。
---
### 3. 整体确认需求缺失预检 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check`
**VO**: `无请求体 → GroupBatchRequirementCheckRespVO`
#### 使用场景
「查看需求」Tab 进入时与点「确认」前调用,据 `ready` 置灰确认按钮、据 `missing` / `vehicleMissing` 展示缺哪几户。**只读无副作用**。权限点 `group-batch:demand:confirm`。本次改动只让缺失项 `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`(809122)少产出一类情况并改了它的报文。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `groupBatchId` | Path | Long | ✅ | - | 团期 ID |
#### 出参 `Result<GroupBatchRequirementCheckRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `groupBatchId` | String | 团期 ID(雪花,字符串) |
| `batchStatus` / `batchStatusName` | String | 团期状态编码与中文名 |
| `ready` | Boolean | 是否可以整体确认(置灰按钮用) |
| `missing` | Array | 房侧缺失户:`orderId` / `teamNo` / `orderNo` / `customerName` / `consultantId` / `consultantName` / `reason` / `reasonName` / `dayNumber` / `segmentIndex` / `expectedNights` / `actualNights` |
| `checkedResourceTypes` | Array\<String\> | 恒为 `["HOTEL","VEHICLE"]`;文案已更新为「车侧逐户查**用车需求行**是否提交(#8577 起行程用车与接送机任一有即算已提交)」 |
| `vehicleWaived` | Boolean | 是否已声明整团免车 |
| `vehicleMissing` | Array | 车侧缺失项,见下 |
| `vehicleMissing[].reason` | String | `GROUP_REQUIREMENT_NOT_FOUND` / `GROUP_REQUIREMENT_STATUS_INVALID` / `NO_GROUP` / `GROUP_CODE_INVALID` / `DAY_OUT_OF_GROUP_RANGE` / `DAY_GAP_IN_GROUP_RANGE` / `MEMBER_FOREIGN_ORDER` / `MEMBER_DUPLICATE_DAY` / `ORDER_DAY_UNCOVERED` / `HEADCOUNT_LESS_THAN_MEMBERS` / **`HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`** / `MEMBER_GROUP_MISMATCH` / `TRANSFER_SERVICE_DATES_NOT_BACKFILLED` / `TRANSFER_WINDOW_INCOMPLETE` |
| `vehicleMissing[].groupCode` | String | 涉及的乘车分组编码;无分组维度时 null |
| `vehicleMissing[].tripDate` | String(`yyyy-MM-dd`) | 涉及的日期;无日期维度时 null |
| `vehicleMissing[].orderId` | String | 涉及的子订单 ID(雪花,字符串);无订单维度时 null |
| `vehicleMissing[].teamNo` / `orderNo` | String | 团号 / 子订单号快照 |
| `vehicleMissing[].detail` | String | 人话描述,**与整团确认时抛出的错误报文逐字相同**,可直接展示 |
| `vehicleExemptHouseholds` | Array | 车侧豁免户(结构同前两个端点);🔴 **只提交了接送机的户不在这里** |
| `groupVehicleRequirementId` | String | 团级正式用车需求 ID(雪花,字符串) |
| `groupVehicleRequirementStatus` | String | 团级正式用车需求状态 |
| `groupVehicleRequirementVersion` | Integer | 团级正式用车需求版本 |
| `transferSubmitEnabled` | Boolean | 接送机提交灰度开关当前状态 |
| `transferDeclaredWithoutRequirement` | Array | 声明了接送机却没有活跃 TRANSFER 行的户:`orderId` / `teamNo` / `orderNo` / `customerName` / `consultantId` / `consultantName` / `pickupRequired` / `dropoffRequired` / `pickupRemark` |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2099459272533000001/requirement/confirm-check
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"groupBatchId": "2099459272533000001",
"batchStatus": "RESOURCE_PREPARING",
"batchStatusName": "资源准备中",
"ready": false,
"missing": [],
"checkedResourceTypes": ["HOTEL", "VEHICLE"],
"vehicleWaived": false,
"vehicleMissing": [
{
"reason": "HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED",
"groupCode": null,
"tripDate": null,
"orderId": "2099459272533323779",
"teamNo": "26-0482",
"orderNo": "HL2606010003",
"detail": "该户尚未提交用车需求,请先让定制师提交后再整团提交车务"
}
],
"vehicleExemptHouseholds": [],
"groupVehicleRequirementId": "2099459272533400001",
"groupVehicleRequirementStatus": "DRAFT",
"groupVehicleRequirementVersion": 4,
"transferSubmitEnabled": true,
"transferDeclaredWithoutRequirement": []
}
}
```
#### 空数据 / 降级响应
全部就绪时 `ready=true`,三个清单都是**空数组**不是 `null`:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"groupBatchId": "2099459272533000001",
"batchStatus": "RESOURCE_PREPARING",
"batchStatusName": "资源准备中",
"ready": true,
"missing": [],
"checkedResourceTypes": ["HOTEL", "VEHICLE"],
"vehicleWaived": false,
"vehicleMissing": [],
"vehicleExemptHouseholds": [],
"transferDeclaredWithoutRequirement": []
}
}
```
#### 错误响应
本端点是只读预检,把缺失**列成清单**而不是抛码;仍可能出现的错误只有权限与团期不存在两类:
```json
{
"code": 403,
"message": "无权限执行该操作",
"success": false,
"data": null
}
```
#### 业务边界
- **鉴权**:权限点 `group-batch:demand:confirm`;未登录由网关拦截返 401。
- **⛔ 只读无副作用**,可随页面进入反复调用。
- **它是缺失明细的唯一来源**:整团确认失败时抛出的 589533 只带汇总户数,逐户明细只能从本端点取。
- **收窄后的 `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED` 判据**:在团需车户**两类用车需求都没提交**才产出;只提交接送机的户不产出,**也不进 `vehicleExemptHouseholds`**。
- **`detail` 与错误报文逐字相同**:所以它也跟着改了文案(`行程用车需求` → `用车需求`),前端不要做子串匹配。
- **`ORDER_DAY_UNCOVERED`(809109)仍只认 TRAVEL**:只提交接送机的户不会因为「没被任何乘车分组覆盖」出现在这里。
- **`transferDeclaredWithoutRequirement` 里两个 flag 都为 false 是合法组合**:该户的声明落在 `direction` 为空或不在 ARRIVAL/DEPARTURE 两值内的批次上,仍确实声明了接送机,前端照常展示、不要过滤掉。
- **雪花 ID 一律是字符串**。
---
## 四、契约约束与正确调用方式
> 本节只写后端接受 / 拒绝 payload 的规则,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误 payload 对照(保存端点)
| 场景 | payload |
|------|---------|
| ✅ 首次保存(无版本) | `{ "version": null, "groups": [ { "groupCode": "BUS", "vehicleType": "bus", "serviceStartDate": "2026-09-12", "serviceEndDate": "2026-09-16", "days": [ { "tripDate": "2026-09-12", "headcount": 9, "memberOrderIds": [2099459272533323777] } ] } ] }` |
| ✅ 改既有组(带 groupId,groupCode 不变) | `{ "version": 3, "groups": [ { "groupId": 1867000000009, "groupCode": "BUS", ... } ] }` |
| ✅ 座位与车辆数同空(存量分组) | `{ ..., "seats": null, "count": null }` |
| ✅ 团里只有接送机户 → 提交零分组 | `{ "version": null, "groups": [] }` → 200(改前该团常因某户「只交了接送机」撞 809123) |
| ❌ `groups` 传 null | `{ "version": 3, "groups": null }` → 400「乘车分组列表不能为 null(整团免车请改用 waive 端点)」 |
| ❌ 带 groupId 却改了 groupCode | `{ "groupId": 1867000000009, "groupCode": "BUS2", ... }` → 809104 |
| ❌ 只填 seats 不填 count | `{ "seats": 19, "count": null }` → 809118 |
| ❌ 车型填自由文本 | `{ "vehicleType": "35座大巴" }` → 809119 |
### 前端必须做的一处改动
- 🔴 **凡是对 809121 / 809122 / 809123 的 `message`(或预检 `vehicleMissing[].detail`)做过字符串包含判断的地方,一律改成按 `code` / `reason` 分支**。三条报文里的 `行程用车需求` 已改为 `用车需求`,旧的子串匹配会静默失配(不报错,只是那条分支再也不进)。
- 其余全部字段、校验规则、请求格式不变,不需要任何别的适配。
---
## 五、数据库行为
只有保存端点(PUT)是写端点,本次改动**没有任何表结构或写入语义变化**——变的是写之前那道户级阻断的判据。
| 场景 | 改前 | 改后 |
|------|------|------|
| 某户只有活跃 TRANSFER 行,团级 PUT 提交 | 809123 整份拒绝,**零写入** | 正常落库(该户不需要被任何分组覆盖) |
| 某户两类都没有活跃行且提交得了 | 809123 整份拒绝,零写入 | 未变,仍 809123 零写入 |
| 某户两类都没有活跃行但提交不了(豁免户) | 不阻断,列入 `exemptHouseholds` | 未变 |
| 正常提交 | 全量替换:本次未出现的分组移出当前版本、版本号 +1 | 未变 |
**失败零写入**:809123 抛在乐观锁比对与分组改名守卫之后、任何写入之前,整份拒绝不留半份数据。
自动汇总(GET)与确认预检(GET)两个端点**零写入**,本次未改变这一点。
---
## 六、边界行为
- 未登录 → 401(网关拦截)。
- 权限点 `group-batch:demand:confirm` 缺失 → 403。
- 团期尚未形成正式用车需求 → 809100(报文用「该团期」,不带雪花 id)。
- 正式需求已被他人修改 → 809102,带提交版本与当前版本。
- 团期已过配置阶段 → 809111。
- 已声明整团免车又提交分组 → 809115(需先 withdraw 回草稿)。
- 车队车型字典不可用 → 809120(不静默降级,让运营重试)。
- 老数据兼容:存量分组没有 `seats` / `count`,编辑时原样回传 null 不会 400;库里被 `V20260924_402` 归一过的车型可正常回显,归一认不出的历史自由文本原样保留,但**再提交一次仍会被 809119 拒**——编辑态请把字典外的当前值显式标出提示重选,不要渲染成空。
- 推不出服务日的户(`departDate` / `returnDate` 任一为空)跳过 809109 覆盖判定(已知盲区,不是遗漏)。
---
## 六.5、枚举 / 数据字典
### reason(车侧缺失项原因码)
**所属字段**: `GroupBatchRequirementCheckRespVO.vehicleMissing[].reason` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `GROUP_REQUIREMENT_NOT_FOUND` | 团级正式需求未形成 | 对应 809100 |
| `GROUP_REQUIREMENT_STATUS_INVALID` | 团级正式需求状态不允许 | 对应 809101 |
| `NO_GROUP` | 有需车户却零分组 | 对应 809103 |
| `GROUP_CODE_INVALID` | 分组编码重复或改名 | 对应 809104 |
| `DAY_OUT_OF_GROUP_RANGE` | 逐日行不在本组服务日范围内 | 对应 809105 |
| `DAY_GAP_IN_GROUP_RANGE` | 本组服务日范围内缺日 | 对应 809106 |
| `MEMBER_FOREIGN_ORDER` | 成员不属于本团期 | 对应 809107 |
| `MEMBER_DUPLICATE_DAY` | 同一户同一日属多个分组 | 对应 809108 |
| `ORDER_DAY_UNCOVERED` | 该户某日未被任何分组覆盖 | 对应 809109;🔴 **仍只认 TRAVEL,本次未改** |
| `HEADCOUNT_LESS_THAN_MEMBERS` | 用车人数小于当日成员户数 | 对应 809110 |
| `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED` | 该户尚未提交用车需求 | 对应 809122;🔴 **#8577 收窄:行程用车与接送机任一有即不报**。只带 `orderId` / `orderNo`,处置是催该户定制师提交 |
| `MEMBER_GROUP_MISMATCH` | 该户车型与覆盖它的分组车型不符 | 对应 809125;排在户级未提交之后(交都没交的户没有车型可比) |
| `TRANSFER_SERVICE_DATES_NOT_BACKFILLED` | 待放行的接送机需求未回填服务日 | 对应 809007,只带 `orderId` |
| `TRANSFER_WINDOW_INCOMPLETE` | 接送机需求窗没盖住大交通派生日期 | 对应 809126,带 `orderId` / `orderNo` 与首个越窗日期 |
### reason(团级用车需求豁免户原因码)
**所属字段**: `exemptHouseholds[].reason`、`vehicleExemptHouseholds[].reason` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `ORDER_NOT_CUSTOMIZING` | 订单不在定制中 | 定制师提交会被 582017 拒,所以该户不算「没交」 |
| `REQUIREMENT_FROZEN` | 团期已过资源准备、需求已冻结且该户未被打回 | 定制师提交会被 589536 拒 |
🔴 **只提交了接送机的户不属于任何一档**——它已提交,既不进未提交名单也不进豁免名单。
### 用车需求类别(判定用,不直接出现在本次三个响应的字段里)
| 值 | 中文 | 在本次判定中的角色 |
|----|------|-------------------|
| `TRAVEL` | 团期行程用车 | 汇总进团级乘车分组;**809109 逐日覆盖只认它** |
| `TRANSFER` | 接送机 | 走逐户派车、结构上在团级乘车分组之外;#8577 起它也算「已提交用车需求」,参与 809121 / 809122 / 809123 的判定 |
---
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| 三个端点的全部请求字段 | — | 未变(一个都没动) |
| 三个端点的全部响应字段 | — | 未变(无新增、无删除、无改名、无类型变化) |
| `checkedResourceTypes` 的字段说明文案 | 「车侧逐户查**行程**用车需求行是否提交」 | 「车侧逐户查用车需求行是否提交(#8577 起行程用车与接送机任一有即算已提交)」 |
| `vehicleMissing[].reason` 的取值集合 | 14 个 | 未变(仍 14 个,只是其中一个的触发条件收窄) |
| `exemptHouseholds` 的成员判据 | 在团需车 ∧ 无 active TRAVEL ∧ 提交不了 | 在团需车 ∧ **两类都无 active 行** ∧ 提交不了 |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 某户只提交了接送机,团级 PUT 保存 | 809123 整份拒绝,运营无干净出路 | 正常保存 |
| 某户只提交了接送机,点自动汇总 | 809121 整团出不来草稿 | 正常出草稿(该户不进草稿,也不进缺失清单) |
| 某户只提交了接送机,进确认预检 | 该户挂 `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`,`ready=false` | 不产出该缺失项 |
| 某户只提交了接送机,点整体确认(`POST .../requirement/confirm`,真实派车/写库) | 809122 抛出,确认失败、零写入 | 正常确认,该户随团一起派车(前提其余条件都满足) |
| 某户只提交了接送机,是否要求被乘车分组覆盖 | 会走到 809109 | 不要求(它结构上在团级分组之外) |
| 809121 报文 | `团期 {0} 有 {1} 户缺少可汇总的**行程**用车需求…` | `…缺少可汇总的用车需求…` |
| 809122 报文 | `该户尚未提交**行程**用车需求,…` | `该户尚未提交用车需求,…` |
| 809123 报文 | `{0}有 {1} 户尚未提交**行程**用车需求,…` | `{0}有 {1} 户尚未提交用车需求,…` |
| 汇总缺失原因文案 | `未提交行程用车需求` | `未提交用车需求` |
| 809109 逐日覆盖的判据 | 只认 TRAVEL | 未变,仍只认 TRAVEL |
| 两类都没提交的户 | 三处照旧阻断 | 未变 |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 否(无字段增删改;只是三个错误码少抛一类情况、报文文案改写)
- **前端是否必须同步上线**: 否;但**若前端对这三条报文做过字符串包含判断,必须改**(改成按 `code` / `reason` 分支),否则那条分支会静默失配
- **前端 workaround 清理点**: 若为绕开「纯接送机户卡住整团」在页面上加过提示、屏蔽过确认按钮、或引导过运营去整团免车,可以撤掉
---
## 七、不影响范围
- **仅影响**: 团期需求管理 Tab 的三个端点里 809121 / 809122 / 809123 的触发条件与报文文案。
- **⚠️ `POST .../requirement/confirm`(整体确认,真实派车/写库)不是零影响**:它的响应字段结构、派车/写库机制本身未改一行代码,但它内部同样经 `GroupBatchRequirementService.doConfirm`(`GroupBatchRequirementService.java:562`)调 `loadVehicleSnapshot`,后者第 1240-1241 行直接调用本次收窄后的 `classifyVehicleSubmission` 来判 809122——即它对 809122 的**实际触发条件与 `confirm-check` 端点同步收窄**:纯接送机户此前会在这里被 809122 拦下、零写入(回归用例 `GroupBatchRequirementServiceConfirmVehicleTest#doConfirm_householdWithoutTravelRequirement_throws809122` 钉住的正是「两类都没提交」仍会拦的边界),现在能正常通过、随团一起派车,见六.6 行为级对比。前端若曾据「纯接送机户点确认必失败」写过分支或禁用逻辑,需按该行为级对比同步调整。
- **零影响**:
- 809109 逐日覆盖判定(仍只认 TRAVEL)
- `POST .../requirement/confirm` 的响应字段结构(`GroupBatchRequirementConfirmRespVO` 字段清单未变)与派车 / 写库机制(`doConfirm` 方法体本身未改)
- 受控重开、整份撤回、整团免车、按户打回四个端点
- 接送机批量确认 `POST .../requirement/transfer/batch-confirm`
- 户级用车需求的提交 / 编辑 / 打回链路
- 车务侧(hl-fleet-service)的配车、派单、就绪判定
- 历史数据:不做任何迁移,存量团期下次调用时按新判据生效
---
## 八、测试环境已验证
- **代码事实**(对 `origin/dev-v3` 逐一查证):
- 合并提交 `5b7074e691`(PR #8600 squash 合并进 `dev-v3`),17 文件 / +559 −137。
- `GroupVehicleRequirementErrorCode` 三条 `IErrorCode.of` 的字面量 diff 已逐字核对(809121 / 809122 / 809123 各去掉「行程」二字),码值与常量名未变。
- `GroupVehicleDraftAggregator.MISSING_NOT_SUBMITTED` 由 `未提交行程用车需求` 改为 `未提交用车需求`;`Household` record 新增 `boolean transferSubmitted` 位,判缺失处改为 `household.needsVehicle() && !household.transferSubmitted()`。
- `classifyTravelSubmission` 更名为 `classifyVehicleSubmission`,`VehicleSubmission` 内 `travelSubmittedOrderIds` 与 `anySubmittedOrderIds` 是两个分开的字段——809109 用前者、三码用后者,合并会把墙挪到 809109。
- 三个端点的 Controller 签名、`@RequestBody` VO、响应 VO 字段清单逐一核对,确认零字段变化。
- 回归钉在 `GroupVehicleRequirementValidateTest#save_frozenRejectedButTransferSubmitted_noLongerThrows809123` 等用例上(本 PR 新增 / 改写测试 6 个文件、+400 余行)。
- **部署**:`hl-order-service-v3` 的 `dev-v3` 分支已滚到测试服,三个端点走管理端网关 `/v3/admin/order/**` 既有路由,无新增路由。
```
PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement → 200 ✓(纯接送机户不再触发 809123)
GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft → 200 ✓(纯接送机户不再触发 809121)
GET /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check → 200 ✓(不再产出 HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED)
```
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| — | #7441 | 团期正式用车需求首次落地(809100-809115 段) | ✅ 有效 |
| — | #8219 | 有户未提交时阻断团级 PUT,新开 809123 与豁免户机制 | ✅ 有效(本单在其基础上收窄判据) |
| — | #8220 | 自动汇总草稿端点与 809121 | ✅ 有效 |
| — | #8249 | 预检加户级 809122 | ✅ 有效 |
| — | #8306 | 报文按团号列户、不出现雪花 id | ✅ 有效 |
| **本 PR #8600** | **#8577** | 户级提交判定与行程覆盖判定分离,三码收窄 + 文案改写 | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#8577](https://git.1814.love:8443/wx/HL/issues/8577)
- 关联 PR: [wx/HL#8600](https://git.1814.love:8443/wx/HL/pulls/8600)
## 关联 / 联系人
### 链接
- **Issue**: [#8577](https://git.1814.love:8443/wx/HL/issues/8577)
- **PR**: [#8600](https://git.1814.love:8443/wx/HL/pulls/8600)
- **Merge commit**: [5b7074e691](https://git.1814.love:8443/wx/HL/commit/5b7074e691)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,266 @@
---
schema: "hl-changelog/v2"
ticket: "8579"
title: "接送机配置接口补发条件放宽:门禁已满足且订单有接送机声明时重存也会补发最终方案,NO_GATE_TRANSITION 出现频率下降"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "PR #8595 已合并 dev-v3(0f19f383f),hl-fleet-service 已部署 TEST(8e00cc98b)并经 Gateway 实测。响应结构未变,前端无需改代码。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# hl-fleet-service: 接送机配置接口补发条件放宽
> **存放目录**: `changelogs-v2/2026-09/`
> **服务**: hl-fleet-service (端口 8089)
> **PR**: #8595
> **Issue**: #8579
> **日期**: 2026-09-30
> **影响范围**: 车务四步向导第③步「接送机配置」保存后的最终方案补发
---
## ⚠️ 关键变化
- **以前**:只有「本次保存前门禁不满足、保存后满足」才补发最终方案;门禁本来就满足时再保存一次,一律回 `NO_GATE_TRANSITION`、不补发。
- **现在**:保存后门禁满足,并且(保存前不满足,**或**订单大交通有接送机声明)就尝试补发。门禁本来就满足、订单有接送机声明时重存,也会补发。
- **补发仍要过全部守卫**:换版后留下旧定稿(`STALE_FINALIZED_PLAN`)、方案代际不一致、没派满,照样不发,并如实回原因。换版后的订单仍须车务重新确认执行,本次没有绕开这道守卫。
- **前端无需改代码**:请求体、响应结构、错误码都没变;只是 `finalPlanPublished=true` 出现得更多、`NO_GATE_TRANSITION` 出现得更少。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 接送机配置(按派车行整批幂等覆盖) | PUT | `/admin/fleet/assignments/pickup-dropoff-config` | 补发时机放宽 | 请求、响应结构不变 |
---
## 三、接口详情
### 1. 接送机配置 `PUT /admin/fleet/assignments/pickup-dropoff-config`
**VO**: `PickupDropoffConfigReqVO → PickupDropoffConfigRespVO`
#### 使用场景
车务四步向导第③步保存接送机勾选。排车(`POST /admin/fleet/assignments/batch`)时大交通要求接/送机的日期还没配车,最终方案被压住(batch 回 `GATE_UNSATISFIED`);在本接口配齐后,服务端当场补发。
#### 入参(未变)
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `orderId` | Body | Long | ✅ | - | 订单 ID |
| `requirementId` | Body | Long | ✅ | - | 当前生效用车需求 ID |
| `kind` | Body | String | - | `TRAVEL`(默认)/ `TRANSFER` | 需求类别 |
| `requestId` | Body | String | ✅ | 非空 | 幂等请求标识 |
| `items[]` | Body | Array | ✅ | 只列参与接送的行 | 两个标志都为 false 的行不要放进来(否则 400);未列入的行服务端置 0 |
| `items[].assignmentId` | Body | Long | ✅ | 当前需求下生效派车行 | 派车行 ID |
| `items[].pickupParticipant` | Body | Boolean | ✅ | - | 当天是否参与接机 |
| `items[].dropoffParticipant` | Body | Boolean | ✅ | - | 当天是否参与送机 |
#### 出参 `Result<PickupDropoffConfigRespVO>`(结构未变)
| 字段 | 类型 | 说明 |
|------|------|------|
| `finalPlanPublished` | Boolean | 本次是否补发了最终方案 |
| `finalPlanNotPublishedReason` | String | 未补发原因,补发时为 `null`;取值见六.5 |
| `requirementReopened` | Boolean | 是否把已完成订单拉回处理中(未变) |
| `reopenBlockedReason` | String | 本该拉回却没拉回的原因(未变) |
| `pickupDropoffGate` | Object | 写入后的门禁状态:`arrivalRequiredDates` / `departureRequiredDates` / `missingPickupDates` / `missingDropoffDates` / `declared` / `satisfied` |
#### 请求示例
```json
{
"orderId": "2105196047255126017",
"requirementId": "2105197351138410497",
"kind": "TRAVEL",
"requestId": "pd-2105196047255126017-20260930-02",
"items": [
{ "assignmentId": "2105197584132046850", "pickupParticipant": true, "dropoffParticipant": false },
{ "assignmentId": "2105197584157212674", "pickupParticipant": false, "dropoffParticipant": true }
]
}
```
#### 响应示例(配齐即补发)
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"finalPlanPublished": true,
"finalPlanNotPublishedReason": null,
"requirementReopened": false,
"reopenBlockedReason": null,
"pickupDropoffGate": {
"arrivalRequiredDates": ["2026-10-12"],
"departureRequiredDates": ["2026-10-14"],
"missingPickupDates": [],
"missingDropoffDates": [],
"declared": true,
"satisfied": true
}
}
}
```
#### 响应示例(换版后配齐,仍被陈旧定稿拦下)
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"finalPlanPublished": false,
"finalPlanNotPublishedReason": "STALE_FINALIZED_PLAN",
"requirementReopened": false,
"reopenBlockedReason": null,
"pickupDropoffGate": { "declared": true, "satisfied": true, "missingPickupDates": [], "missingDropoffDates": [] }
}
}
```
#### 空数据 / 降级响应
订单大交通没有接送机声明时 `pickupDropoffGate` 各日期数组为空、`declared=false`、`satisfied=true`;此时重存不补发:
```json
{ "code": 200, "success": true, "data": { "finalPlanPublished": false, "finalPlanNotPublishedReason": "NO_GATE_TRANSITION", "pickupDropoffGate": { "declared": false, "satisfied": true, "missingPickupDates": [], "missingDropoffDates": [] } } }
```
#### 错误响应
```json
{
"code": 400,
"message": "同一行接送机标志不能全为 false;不参与的行不要出现在配置列表中",
"success": false,
"data": null
}
```
#### 业务边界
- 补发时机:保存后门禁满足,且保存前不满足或订单有接送机声明。订单大交通没有接送机声明时,重存不会补发(回 `NO_GATE_TRANSITION`),避免每存一次都发一版没有变化的最终方案。
- 补发仍依次过陈旧定稿、方案代际、满派拓扑、接送机门禁四道判据,只回第一个没通过的原因。
- 同一需求多次补发,order-v3 的配车记录按最新一版整体替换,不会重复累加。
---
## 四、契约约束与正确调用方式
- `finalPlanPublished=false` 不是错误,按 `finalPlanNotPublishedReason` 提示车务下一步:`GATE_UNSATISFIED`/`NO_GATE_TRANSITION` 看 `pickupDropoffGate.missing*Dates` 补配;`STALE_FINALIZED_PLAN` 提示车务重新确认执行;`PLAN_INCOMPLETE` 提示补派。
- 不要按 `NO_GATE_TRANSITION` 判断「这次保存没生效」:勾选照常落库,它只表示本次没有尝试补发。
---
## 五、数据库行为
- 补发时写 fleet `fleet_vehicle_assignment_snapshot_state`(修订号 +1)和 `fleet_vehicle_assignment_snapshot_outbox`,再异步写 order-v3 `order_vehicle_assignment`(旧行软删、按新一版重建)。
- 不补发时只更新派车行的接送标志。
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- `items` 含两个标志都为 false 的行 → 400「同一行接送机标志不能全为 false」
- 换版后未经车务重新确认 → 不补发,回 `STALE_FINALIZED_PLAN`
---
## 六.5、枚举 / 数据字典
### finalPlanNotPublishedReason(`FinalPlanNotPublishedReasons`)
| 取值 | 含义 |
|------|------|
| `STALE_FINALIZED_PLAN` | 存在按旧需求定稿的陈旧行,需车务重新确认 |
| `INVALID_PLAN_GENERATION` | 方案代际不一致 |
| `PLAN_INCOMPLETE` | 满派拓扑不完整(缺车 / 缺司机 / 在途行越窗等) |
| `CAPACITY_INSUFFICIENT` | 未定稿方案载客量不足 |
| `GATE_UNSATISFIED` | 大交通要求的接/送机日未配车 |
| `NO_GATE_TRANSITION` | **口径变化**:本次没有尝试补发——保存后门禁仍不满足,或门禁满足但订单没有接送机声明且保存前已满足 |
| `PICKUP_DROPOFF_GATE_DISABLED` | 接送机门禁开关关闭 |
---
## 六.6、修改前后对比
### 行为级对比
| 场景 | 修改前 | 修改后 |
|------|--------|--------|
| 保存前缺配、保存后配齐 | 补发 | 补发(不变) |
| 门禁本已满足、订单有接送机声明,再保存 | `NO_GATE_TRANSITION`,不补发 | 尝试补发(过全部守卫) |
| 门禁本已满足、订单无接送机声明,再保存 | `NO_GATE_TRANSITION` | `NO_GATE_TRANSITION`(不变) |
| 保存后仍缺配 | `NO_GATE_TRANSITION` | `NO_GATE_TRANSITION`(不变) |
| 换版后配齐 | `NO_GATE_TRANSITION` 或 `STALE_FINALIZED_PLAN` | `STALE_FINALIZED_PLAN` |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 否
- **前端是否必须同步上线**: 否
- **前端 workaround 清理点**: 无
---
## 七、不影响范围
- **仅影响**: `PUT /admin/fleet/assignments/pickup-dropoff-config` 的补发时机
- **零影响**: 请求体与响应结构;`POST /admin/fleet/assignments/batch`;确认执行接口(缺失日期字段的删除见 #8603 changelog)
---
## 八、测试环境已验证
2026-09-30 TEST(hl-fleet-service @ 8e00cc98b)经 Gateway 实测,夹具订单验收后已行前取消:
| 场景 | 结果 |
|------|------|
| batch 排满三天、首日接机末日送机未配 | `finalPlanPublished=false` + `GATE_UNSATISFIED`,缺 `2026-10-12` / `2026-10-14` |
| 只配接机 | `NO_GATE_TRANSITION`,门禁仍缺 `2026-10-14` |
| 配齐接机 + 送机 | `finalPlanPublished=true`,outbox 1 行,order-v3 配车记录 3 行 |
| 换版后配齐 | `finalPlanPublished=false` + `STALE_FINALIZED_PLAN`,门禁 `satisfied=true`,无快照发出 |
---
## 九、相关历史 PR
- #8429:统一 batch / 接送机配置 / 确认三处的最终方案发布判据,引入 `finalPlanNotPublishedReason`。
- #8603(PR #8608):确认执行响应删除恒为空的缺失日期字段,缺失日期改由 605914/605915 错误消息承载。
---
## 十、相关文档
- 关联 Issue: [wx/HL#8579](https://git.1814.love/wx/HL/issues/8579)
- 关联 PR: [wx/HL#8595](https://git.1814.love/wx/HL/pulls/8595)
## 关联 / 联系人
### 链接
- **Issue**: [#8579](https://git.1814.love/wx/HL/issues/8579)
- **PR**: [#8595](https://git.1814.love/wx/HL/pulls/8595)
- **Merge commit**: [0f19f383f](https://git.1814.love/wx/HL/commit/0f19f383faf3f0df9489b342c15afb42addf09f5)
### 联系人
- 后端:@lc
@@ -0,0 +1,267 @@
---
schema: "hl-changelog/v2"
ticket: "8593"
title: "待配车团期清单 transferPendingCount 由硬编码 0 改为真值,取不到给 null"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "GET /admin/fleet/group-dispatch/pending-batches 响应体每行新增真值语义:records[].transferPendingCount 此前恒为硬编码 0,本次改为按团期实际接送机声明缺口计算的真值,且新增 null 语义——取不到时返回 null 而不是 0,前端必须把 null 渲染成未知态(如「—」),不得折算成 0;0 表示查过了确无缺口,null 表示本团有没有缺口未知。该字段与团期配车总览端点(GET /admin/fleet/group-dispatch/batches/{groupBatchId}/overview)的 transferPendingTotal 走同一判定方法,服务端保证两者恒等。声明数据经内部 Feign 端点(order-v3 提供,仅供服务间调用,非管理后台直接可调)按页批量整取,不做逐团 N+1 请求;该内部读口不可达时不会静默返回空列表,会返回失败结果,使 transferPendingCount 整页退化为 null,不影响该页其余字段(包括 unreadCount,是另一个独立软依赖,user-service 不可达时退化为 0)。字段类型未变(仍是 Integer),仅新增 null 作为合法取值;若前端此前对该字段做过兜底成 0 或完全未渲染,需要补上 null 分支与展示逻辑。清单本身的分页/过滤/排序、其余字段与错误码(600012/600013/401)均未变化。backend_status=deployed:hl-fleet-service 已部署测试服并与本条目所依据的源码提交一致(deploy-status.sh 实测 COMMIT=d57498d38,STATE=ok,2026-09-30 05:34:07 部署);该路由已实测可达(未登录态返 200 信封 code=401)。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# 车务团期配车:待配车团期清单接送机未配计数改为真值
> **存放目录**: 二期(order-v3/fleet)→ `changelogs-v2/2026-09/`
>
> **服务**: hl-fleet-service(端口 8087)
> **PR**: [#8617](https://git.1814.love:8443/wx/HL/pulls/8617)
> **Issue**: [#8593](https://git.1814.love:8443/wx/HL/issues/8593)
> **日期**: 2026-09-30
> **影响范围**: 管理后台「待配车团期清单」列表页的 `transferPendingCount` 一列
---
## ⚠️ 关键变化
「待配车团期清单」(`pending-batches`)每行的 `transferPendingCount` 字段,此前**恒为硬编码 0**,不反映任何真实数据——前端如果曾据此判断「所有团都没有接送机缺口」,这个判断从一开始就是假的。本次改为**真实计算值**,并引入 **`null` 语义**:取不到声明数据时返回 `null` 而不是 `0`。**`0` 与 `null` 含义不同,不能互相折算**:`0` = 查过了、确无接送机缺口;`null` = 这一刻没查到、本团有没有缺口未知。前端如果沿用旧的「反正恒为 0,不用管」的假设,现在会看到非零真值和偶发 `null`,必须补上渲染逻辑。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 待配车团期清单 | GET | `/admin/fleet/group-dispatch/pending-batches` | 修改接口 | `transferPendingCount` 由硬编码 0 改为真值+null 语义 |
---
## 三、接口详情
### 1. 待配车团期清单 `GET /admin/fleet/group-dispatch/pending-batches`
**VO**: `GroupDispatchPendingBatchPageReqVO → PageResult<GroupDispatchPendingBatchRespVO>`
#### 使用场景
车务在「团期配车」列表页查看尚未完成配车(`requirementConfirmed=true` 且 `vehicleReady=false`)的团期,支持按出发日区间、团号/团名关键词、配车进度过滤。本次改动只影响列表行里的 `transferPendingCount` 一列,接口路径、分页参数、其余字段均未变化。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| departDateFrom | Query | LocalDate(ISO,`yyyy-MM-dd`) | - | 不传=不限 | 出发日下界(含) |
| departDateTo | Query | LocalDate(ISO,`yyyy-MM-dd`) | - | 不传=不限 | 出发日上界(含) |
| keyword | Query | String | - | ≤50 字符 | 团号/团名模糊关键词 |
| dispatchProgress | Query | String | - | 仅 `NOT_STARTED`/`PARTIAL`/`FULL` | 配车进度过滤(fleet 侧内存过滤,先分页后过滤) |
| page | Query | Integer | - | ≥1,默认 1 | 页码 |
| pageSize | Query | Integer | - | 1-100,默认 20 | 每页条数 |
#### 出参 `Result<PageResult<GroupDispatchPendingBatchRespVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| records | Array | 团期行列表,见下 |
| records[].groupBatchId | String(Long 转字符串) | 团期主订单 ID |
| records[].batchNo | String | 团号 |
| records[].batchName | String | 团名 |
| records[].batchStatus | String | 团期状态 |
| records[].departDate | String(`yyyy-MM-dd`) | 出发日 |
| records[].endDate | String(`yyyy-MM-dd`) | 结束日 |
| records[].serviceDayCount | Integer | 服务日天数 |
| records[].enrolledOrders | Integer | 报名子订单数 |
| records[].enrolledPeople | Integer | 报名人数 |
| records[].requirementConfirmed | Boolean | 需求是否已确认 |
| records[].vehicleReady | Boolean | 车辆是否已就绪 |
| records[].dispatchedDayCount | Integer | 已排车天数 |
| records[].dispatchProgress | String | 配车进度:`NOT_STARTED`/`PARTIAL`/`FULL` |
| records[].**transferPendingCount** | Integer(可空) | **本次变更字段**:本团接送机未配计数;`null`=未取到(前端须渲染未知态),`0`=确无缺口 |
| records[].unreadCount | Integer | 团期车务会话团队未读数(软依赖,取不到退 0) |
| total | Integer | 总记录数(`dispatchProgress` 过滤前) |
| page | Integer | 当前页码 |
| pageSize | Integer | 每页条数 |
#### 请求示例
```http
GET /admin/fleet/group-dispatch/pending-batches?departDateFrom=2026-09-01&departDateTo=2026-09-30&keyword=T26-8867&dispatchProgress=PARTIAL&page=1&pageSize=20
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"groupBatchId": "1934567890123456789",
"batchNo": "T26-8867",
"batchName": "额吉的故乡 9/12 团",
"batchStatus": "RESOURCE_PREPARING",
"departDate": "2026-09-12",
"endDate": "2026-09-16",
"serviceDayCount": 5,
"enrolledOrders": 6,
"enrolledPeople": 17,
"requirementConfirmed": true,
"vehicleReady": false,
"dispatchedDayCount": 2,
"dispatchProgress": "PARTIAL",
"transferPendingCount": 2,
"unreadCount": 3
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"success": true
}
```
#### 空数据 / 降级响应
当没有满足过滤条件的团期时,返回 `records: [], total: 0`(HTTP 200,非错误)。当 order-v3 的接送机声明批量读口不可达时,**本页所有行的 `transferPendingCount` 一律返回 `null`**(不是 0,也不会让整个请求失败)——前端必须把 `transferPendingCount=null` 渲染成未知态(如「—」),不能当作「确认无缺口」折算成 0。`unreadCount` 是另一个独立的软依赖:user-service 不可达时退化为 0,清单其余字段照常返回,不受影响。
#### 错误响应
```json
{
"code": 600013,
"message": "参数非法: 页码必须≥1",
"success": false,
"data": null
}
```
其余可能返回的错误码:
| code | 触发条件 | message |
|---|---|---|
| 600012 | order-v3 团期候选基线不可达(降级/返错),**不会静默返空列表** | `团期配车基线不可达,请稍后重试` |
| 600013 | 日期区间倒置、分页越界、关键词超长(>50 字)、`dispatchProgress` 枚举非法 | `参数非法: {具体原因}` |
| 401 | 未登录(网关统一信封,HTTP 状态码仍是 200,信封内 `code=401`) | `缺少有效的 Authorization 头` |
#### 业务边界
- `dispatchProgress` 是 fleet 侧内存过滤(order-v3 侧没有配车事实,无法下推),是「先分页再过滤」——单页返回条数可能少于 `pageSize`,`total` 是过滤**前**的总数。
- `transferPendingCount` 与团期配车总览端点(`GET /admin/fleet/group-dispatch/batches/{groupBatchId}/overview`)的 `transferPendingTotal` 走**同一个判定方法**,服务端保证两者恒等——清单页与详情页的这个数字不会对不上。
- 声明数据经内部批量读口整页一次取齐(每页最多按团期数一次 Feign 调用),不做逐团 N+1 请求。
- `transferPendingCount` 的 `null` 与 `unreadCount` 的「退 0」是两种不同的降级策略,分别对应各自读口的可靠性设计,不要混用同一套判空逻辑处理。
- 主候选数据(团期本身)取不到时整个请求失败关闭(600012),不会把「后端没拿到」渲染成「该团没有需求」;这与 `transferPendingCount` 单列退化为 `null`(其余字段正常返回)是两个不同粒度的降级,不要合并处理。
---
## 四、契约约束与正确调用方式
### 正确渲染 `transferPendingCount` 的方式
| 取值 | 含义 | 渲染建议 |
|------|------|----------|
| `0` | 查过了,确无接送机缺口 | 正常展示 `0` |
| 正整数 | 查过了,有对应数量的缺口 | 正常展示数值,可高亮提醒 |
| `null` | 本次没有取到该团的声明数据,缺口未知 | 渲染成未知态(如「—」),**不要**当作 `0` |
❌ 错误用法:`transferPendingCount ?? 0` 或任何把 `null` 静默折算成 `0` 的写法——这会把「未知」误报成「已确认无缺口」,反而比改动前的硬编码 0 更危险(因为界面上看起来像是「查过了」)。
---
## 六、边界行为
- 未登录 → 网关统一信封 `code=401`(HTTP 状态码 200,非 HTTP 401)
- 无匹配团期 → `records: [], total: 0`,HTTP 200
- `dispatchProgress` 过滤导致单页为空 → `records: []`,但 `total` 仍是过滤前总数,不为 0
- order-v3 团期候选基线不可达 → 600012,整个请求失败,不返回部分数据
- order-v3 接送机声明批量读口不可达 → 请求仍然成功,仅 `transferPendingCount` 整页退化为 `null`
- user-service 不可达 → 请求仍然成功,仅 `unreadCount` 退化为 `0`
---
## 六.5、枚举 / 数据字典
### dispatchProgress(配车进度)
**所属字段**: `records[].dispatchProgress`(`GroupDispatchPendingBatchRespVO`),同名字段也用于入参过滤 | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `NOT_STARTED` | 未开始 | 已排车天数为 0 |
| `PARTIAL` | 部分配车 | 已排车天数大于 0 但未盖满全部服务日 |
| `FULL` | 已配齐 | 已排车天数盖满全部权威服务日 |
---
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `transferPendingCount` | 恒为 `0`(硬编码占位符,从未反映真实缺口) | 真实计算值;取不到声明数据时为 `null`(不是 `0`) |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 声明数据获取方式 | 未获取,字段硬编码为 0 | 按页批量调用 order-v3 内部读口一次取齐(非逐团 N+1),取不到时该列整体退化为 `null` |
| 与 overview 端点的一致性 | 无法比较(清单侧恒 0,overview 侧是真值,两者结构性不可能相等) | 清单与 overview 走同一判定方法,服务端保证恒等 |
| 字段类型 | `Integer`,实际恒非空 | `Integer`,新增合法取值 `null` |
## 六.7、影响评估
- **是否破坏向后兼容**: 否——字段名与类型(`Integer`)未变,只是语义从「恒定占位符」变为「真实业务值 + 可空」。原本恒为 0 意味着这个字段此前对使用方没有任何信息量,语义上不存在"旧行为被依赖"的合理场景。
- **前端是否必须同步上线**: 是——如果前端此前完全没有渲染这个字段(因为它恒为 0、没有展示价值),现在需要补充展示逻辑,包括 `null` 的未知态处理;如果前端此前渲染了这个字段但做了 `?? 0` 之类的兜底,需要去掉这个兜底、改为区分 `0` 与 `null`。
- **前端 workaround 清理点**: 若前端此前因为「这个字段没用、永远是 0」而完全跳过读取或做了防御性兜底,需要重新接入并按上方「正确渲染方式」处理;无其它 workaround。
---
## 七、不影响范围
- **仅影响**: 「待配车团期清单」列表每行的 `transferPendingCount` 字段
- **零影响**:
- 清单接口的分页参数、过滤参数(`departDateFrom`/`departDateTo`/`keyword`/`dispatchProgress`)语义
- `records[]` 内除 `transferPendingCount` 外的其余字段
- `unreadCount` 字段的取值逻辑(软依赖降级策略本身未变)
- 团期配车总览端点 `GET /admin/fleet/group-dispatch/batches/{groupBatchId}/overview` 的路径、参数、响应结构(其 `transferPendingTotal` 此前就已经是真值,本次不涉及该端点改动,只是清单侧现在与它口径一致)
- 错误码 600012/600013/401 的触发条件与数值
---
## 八、测试环境已验证
- `deploy-status.sh`(测试服现状表)实测:`hl-fleet-service` COMMIT=`d57498d38`、STATE=ok,2026-09-30 05:34:07 部署——与本条目所依据的源码提交(含 #8593 合并提交 `d57498d381`)一致。
- 测试服内网 `curl` 实测路由已挂载且鉴权前置生效:
```
GET http://127.0.0.1:8080/admin/fleet/group-dispatch/pending-batches?page=1&pageSize=1 (无 Authorization 头)
→ HTTP 200
{"code":401,"message":"缺少有效的 Authorization 头","data":null,"traceId":"e0e9988b33394199","success":false}
```
---
## 十、相关文档
- 关联 Issue: [wx/HL#8593](https://git.1814.love:8443/wx/HL/issues/8593)
- 关联 PR: [wx/HL#8617](https://git.1814.love:8443/wx/HL/pulls/8617)
## 关联 / 联系人
### 链接
- **Issue**: [#8593](https://git.1814.love:8443/wx/HL/issues/8593)
- **PR**: [#8617](https://git.1814.love:8443/wx/HL/pulls/8617)
- **Merge commit**: [`d57498d381`](https://git.1814.love:8443/wx/HL/commit/d57498d38138fd37ce7e844b6f2b01c190706950)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,451 @@
---
schema: "hl-changelog/v2"
ticket: "8597"
title: "退团房清空免费退改期限:截止时刻与提醒天数保留原值(订正 #8491 交接件三处表述),异常检查补回源单已取消的退团房待办"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "PR #8599(提交 ff68637542)已合入 dev-v3;测试服 hl-order-service-v3 运行提交 d57498d38、BEHIND 0/N、STATE ok、2026-09-30 05:32:57,ff68637542 是 d57498d381 的祖先。两个端点的路径与方法均未变,网关 /v3/admin/** 路由块已通配,零网关改动。【frontend_status 取 not_required 的依据与限定,2026-09-30 对 hl-ui origin/v2.1 查证】「房务控制台」整个功能域目前在前端**不存在任何代码**:house-console / houseConsole / 退团房 / room-transfer 等 9 个中英文关键词在 src/ 下全部 0 命中(阳性对照:同为房务域的 house-allocation 在 src/api/housekeeper/ 与 src/views/housekeeper/ 均有活跃文件,故检索有分辨力)。因此这里的 not_required 含义是「没有可改的前端代码」,不是「改动对现有页面透明」——本条是 #8491『房务控制台接口』(frontend_status: pending,17 个新增端点)的后续订正,待该模块被前端认领落地时,本条的口径需与它一并核对,勿据本条认为这块功能已可用。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# 房务控制台: 退团房期限清空口径订正 + 异常检查退团房待办补全
> **存放目录**: 二期 → `changelogs-v2/2026-09/`
>
> **服务**: hl-order-service-v3
> **Issue**: #8597
> **PR**: #8599
> **日期**: 2026-09-30
> **影响范围**: 管理后台「房务控制台」的「退团房」页签(设期限弹窗)与「异常检查」页签(待办列表)
---
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
- 🔴 **订正 #8491 交接件的三处表述**:`PUT .../room-transfers/{id}/deadline` 传 `cancelDays: null` 清除期限时,**只有 `cancelDays` 变成 `null`,`cancelCutoff` 与 `remindDays` 保留该行原值,不会被置空**。`changelogs-v2/2026-09/30_8491_房务控制台接口-新增接口-管理后台.md` 的第 746、747 行(入参表)、第 787 行(空数据 / 降级响应)、第 1623 行(显式 SET NULL 说明)写的「三列一并清空 / 本字段被忽略、一并清空」与实际行为不符,以本条为准;该文件第 1847 行的测试环境读数(`cancel_cutoff / remind_days 保留原值`)才是正确的那一条。
- 🔴 **判「这一行有没有设免费退改期限」只能看 `cancelDays === null` 或 `risk === "NO_DEADLINE"`**,不能看 `cancelCutoff` / `remindDays` 是否为 `null`——清空后这两个字段仍有值(该行从未设过期限时是建表默认的 `"18:00"` 与 `1`)。
- **清除期限从「必定失败」改为成功**:本次改动前,`cancelDays: null` 的请求 100% 返回 **808932**「房务状态已被并发修改,请刷新后重试」(并非真的并发冲突),现在返回 `code=200` 并给出更新后的整行。
- **异常检查 `tasks[]` 的 `TRANSFER_PENDING` 条目不再漏行**:待处理退团房行的窗口归属改为只看源团期 / 源订单的**出发日**,**不看源单状态**;源单查不到、或源单出发日为空时同样列出。取消订单恰恰是产生退团房最常见的原因,订正前这些待办在控制台唯一的待办载体上看不到。**同一窗口下本端点返回的 `tasks` 行数会比订正前多。**
---
## 一、背景(选填)
退团房行有两处独立缺陷,都发生在「房务控制台」已交付的端点上:
1. 清期限走的乐观锁写口,其入参守卫要求截止时刻与提醒天数非空(两列在库中是 NOT NULL);旧代码在 `cancelDays` 为 `null` 时把这两项也一起传 `null`,守卫直接返回「影响行数 0」,上层把 0 当成版本冲突抛 808932。表现是「清除期限」按钮永远失败,而错误文案指向刷新重试,看不出是入参问题。
2. 异常检查的退团房待办原先用「窗口内的团期集合 / 散单集合」判归属,这两个集合按占用口径排除了已取消的单,于是「订单取消 → 释放房间 → 待转出」这条最常见的链路产出的待办从不出现。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 设置 / 清除退团房免费退改期限 | PUT | `/v3/admin/order/house-console/room-transfers/{id}/deadline` | 修改 | 清除期限由必定失败改为成功;`cancelCutoff` / `remindDays` 保留原值(订正交接件表述) |
| 2 | 房务异常检查 | GET | `/v3/admin/order/house-console/audit` | 修改 | `tasks[]` 的 `TRANSFER_PENDING` 按源单出发日归属、不看源单状态,补回源单已取消的行 |
---
## 三、接口详情
### 1. 设置 / 清除退团房免费退改期限 `PUT /v3/admin/order/house-console/room-transfers/{id}/deadline`
**VO**: `HouseRoomTransferDeadlineSaveReqVO → HouseRoomTransferRespVO`
#### 使用场景
「退团房」页签某一行点「设期限」,录入酒店给的免费取消规则(入住前几天、当天几点前)与提前几天提醒;也用于把已设的期限清掉。清掉后该行的风险分档变为 `NO_DEADLINE`,前端应按 `cancelDays` 是否为 `null` 渲染「未设免费取消期」,而不是按 `cancelCutoff` / `remindDays` 是否有值判断。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | 必须是退团房**父行**(子行是转出明细,不可改期限);不存在或已删返 808320 | 退团房行 ID |
| cancelDays | Body | Integer | ❌ | 0~60,越界返 808328;传 `null` 表示清除期限 | 入住前几天可免费取消 |
| cancelCutoff | Body | String | ❌ | `HH:mm` 24 小时制(`00:00`~`23:59`),格式不符返 808328;`cancelDays` 非空而本字段为空 / 空白时取 `18:00`;**`cancelDays` 为 `null` 时本字段被忽略,该行原值保留** | 截止当天的时刻 |
| remindDays | Body | Integer | ❌ | 0~30,越界返 808328;`cancelDays` 非空而本字段为空时取 `1`;**`cancelDays` 为 `null` 时本字段被忽略,该行原值保留** | 提前几天提醒 |
#### 出参 `Result<HouseRoomTransferRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| (整行) | HouseRoomTransferRespVO | 更新后的该退团房父行,字段与退团房分页 `records[]` 同构 |
| cancelDays | Integer | 入住前几天免费取消;**清除期限后为 `null`** |
| cancelCutoff | String | 截止当天的时刻 `HH:mm`;**清除期限后仍是该行原值(从未设过则是 `"18:00"`),不会变成 `null`** |
| remindDays | Integer | 提前几天提醒;**清除期限后仍是该行原值(从未设过则是 `1`),不会变成 `null`** |
| deadlineAt | LocalDateTime | 免费取消截止时刻 = 入住晚 − `cancelDays` 天的 `cancelCutoff`;`cancelDays` 为 `null` 或缺入住晚时为 `null` |
| risk | String | 风险码 `OVERDUE` / `NEAR` / `NO_DEADLINE` / `NORMAL`;仅 `PENDING` 行有值,其余为 `null` |
| riskLabel | String | 风险中文;`NO_DEADLINE` 对应「未设免费取消期」 |
| status / statusLabel | String | 行状态 `PENDING` / `TRANSFERRED` / `CANCELLED` 与中文;本端点只对 `PENDING` 行成功 |
| id / sourceOrderId / sourceGroupBatchId / hotelId / roomTypeId | Long(String) | 雪花 ID,JSON 中为字符串 |
| teamNo | String | 源订单团号(批量读订单主表团号);取不到为 `null` |
| stayDate | LocalDate | 入住晚 |
| roomCount / remainingCount | Integer | 原始间数 / 剩余待处理间数 |
| readOnly / readOnlyReason | Boolean / String | 对当前操作人是否只读(源单由他人处理)与理由 |
#### 请求示例
```json
PUT /v3/admin/order/house-console/room-transfers/1950000000000000001/deadline
{
"cancelDays": null
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "1950000000000000001",
"sourceType": "ORDER",
"sourceOrderId": "1930000000000000001",
"teamNo": "HL20261001A",
"sourceGroupBatchId": null,
"sourceBatchNo": null,
"stayDate": "2026-10-02",
"cityName": "海拉尔",
"hotelId": "100001",
"hotelName": "海拉尔草原酒店",
"roomTypeId": "300001",
"roomTypeName": "豪华双床房",
"roomCount": 2,
"remainingCount": 2,
"status": "PENDING",
"statusLabel": "待处理",
"cancelDays": null,
"cancelCutoff": "18:00",
"remindDays": 1,
"deadlineAt": null,
"risk": "NO_DEADLINE",
"riskLabel": "未设免费取消期",
"readOnly": false,
"readOnlyReason": null
},
"success": true
}
```
#### 空数据 / 降级响应
- 本端点恒返回整行,没有空响应形态。
- 清除期限(`cancelDays: null`)是正常成功路径:`data.cancelDays = null`、`data.deadlineAt = null`、`data.risk = "NO_DEADLINE"`、`data.riskLabel = "未设免费取消期"`,而 `data.cancelCutoff` 与 `data.remindDays` 仍是该行原值。
- 源单(订单需求 / 团期)读不到时,只影响「谁是源单处理人」的判定,不影响本端点的写入结果;非源单处理人且非超管一律返 808326。
#### 错误响应
```json
{
"code": 808328,
"message": "退改期限参数不合法",
"data": null,
"success": false
}
```
| code | message | 触发 |
|------|---------|------|
| 808090 | 未登录或非房务角色,无权操作 | 非房务角色 |
| 808320 | 转房记录不存在 | `id` 不存在、已删、或不是父行 |
| 808321 | 该房间已处理 | 行已不是 `PENDING`(已转出 / 已取消) |
| 808326 | 只有原单处理人可以处理退团房间 | 非源单处理人且非超管 |
| 808328 | 退改期限参数不合法 | `cancelDays` 超 0~60、`remindDays` 超 0~30、`cancelCutoff` 不是 `HH:mm` |
| 808932 | 房务状态已被并发修改,请刷新后重试 | 真并发写冲突(行版本在锁定读与写之间被改)。**订正前 `cancelDays: null` 会恒定命中这一条,订正后不再出现这种假冲突** |
| 100502 | 修改处理中,请勿重复提交 | 3 秒幂等窗口内重复提交同一请求 |
#### 业务边界
- 入参不走 Bean Validation,范围与格式错误统一以 **808328** 返回,HTTP 状态仍是 200,不是 400。
- 清除期限只清 `cancelDays` 这一项语义;`cancelCutoff` / `remindDays` 是「下次设期限时的默认值」,保留它们不影响「有没有期限」的判定,因为 `deadlineAt` 与 `risk` 都只在 `cancelDays` 非空时才成立。
- 该行处于 `PENDING` 才可改期限;`TRANSFERRED` / `CANCELLED` 返 808321。
- 只有源单处理人或超管可改;源订单来源看该需求的持有人,团期来源看该团期的房务认领人。
- 同一行的改期限、转房、向酒店取消共用一把行级锁,前端不必自己串行化。
### 2. 房务异常检查 `GET /v3/admin/order/house-console/audit`
**VO**: `HouseConsoleAuditReqVO → HouseConsoleAuditRespVO`
#### 使用场景
「异常检查」页签:按出发日期区间一次列出数据对不上的问题(`issues`)和还没办完的事(`tasks`)。本次订正只影响 `tasks` 里 `TRANSFER_PENDING`(退团房未结清)这一类条目的**取行范围**,字段结构未变;前端按 `refId` 跳转退团房处理页的逻辑不变。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| scope | Query | String | ❌ | `mine` / `all`,默认 `mine`;其他取值返 400 | 只作用于 `tasks`:`mine` 只留当前登录人是处理人的条目;`issues` 不受它影响 |
| departDateFrom | Query | LocalDate | ❌ | `yyyy-MM-dd`,默认今天 | 出发日期区间起(含) |
| departDateTo | Query | LocalDate | ❌ | `yyyy-MM-dd`,默认起始日 +30 天;早于起始日或跨度超 92 天返 808313 | 出发日期区间止(含) |
#### 出参 `Result<HouseConsoleAuditRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| issues | Array | 数据不一致问题,不归属处理人,不受 `scope` 影响;无则空数组 |
| tasks | Array | 待处理事项,受 `scope` 过滤;无则空数组 |
| issues[].code / tasks[].code | String | 问题码 / 待办码,取值见「六.5、枚举」 |
| tasks[].taskCode | String | 待办码(仅 `tasks` 有值),与 `code` 同值 |
| issues[].codeLabel / tasks[].codeLabel | String | 中文标签,后端给出直接展示;`TRANSFER_PENDING` 为「退团房未结清」 |
| tasks[].orderId | Long(String) | 源订单 ID;退团房条目取该行的源订单 |
| tasks[].teamNo | String | 源订单团号;取不到为 `null` |
| tasks[].groupBatchId / tasks[].batchNo | Long(String) / String | 源团期 ID 与批次号;散单来源为 `null` |
| tasks[].stayDate | LocalDate | 入住晚 |
| tasks[].hotelId / hotelName / roomTypeId / roomTypeName | Long(String) / String | 酒店与房型 |
| tasks[].refId | Long(String) | 关联单据 ID,`TRANSFER_PENDING` 为退团房父行 ID,前端据此跳转 |
| tasks[].detail | String | 说明文案,`TRANSFER_PENDING` 为「剩余 N 间待转出或向酒店取消」 |
#### 请求示例
```http
GET /v3/admin/order/house-console/audit?scope=mine&departDateFrom=2026-10-01&departDateTo=2026-10-31
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"issues": [],
"tasks": [
{
"code": "TRANSFER_PENDING",
"codeLabel": "退团房未结清",
"taskCode": "TRANSFER_PENDING",
"orderId": "1930000000000000001",
"teamNo": "HL20261001A",
"groupBatchId": null,
"batchNo": null,
"stayDate": "2026-10-02",
"hotelId": "100001",
"hotelName": "海拉尔草原酒店",
"roomTypeId": "300001",
"roomTypeName": "豪华双床房",
"refId": "1950000000000000001",
"detail": "剩余 2 间待转出或向酒店取消"
}
]
},
"success": true
}
```
#### 空数据 / 降级响应
```json
{ "code": 200, "message": "成功", "data": { "issues": [], "tasks": [] }, "success": true }
```
- `issues` 与 `tasks` 恒为数组,不会是 `null`。
- 退团房条目的源单读不到(源订单或源团期查不到、或出发日为空)时,该条目**仍然列出**,`teamNo` / `batchNo` 可能为 `null`;口径是「宁可多报一条,也不让待办从唯一载体上消失」。
- `STOCK_LEDGER_MISMATCH`(库存账不平)只在资源侧全局库存追踪开关打开时检查,开关关闭时不产出该问题码。
#### 错误响应
```json
{
"code": 808313,
"message": "日期跨度不能超过 92 天",
"data": null,
"success": false
}
```
| code | message | 触发 |
|------|---------|------|
| 400 | scope 取值非法 | `scope` 不是 `mine` / `all` |
| 808090 | 未登录或非房务角色,无权操作 | 非房务角色 |
| 808313 | 日期跨度不能超过 {0} 天 | 出发日期区间跨度超 92 天,或止日早于起日 |
#### 业务边界
- `TRANSFER_PENDING` 的归属判据是**源团期 / 源订单的出发日落在窗口内**,与源单当前状态(含已取消)无关;这是本次订正的点。
- 只列 `PENDING` 的退团房父行;已转出、已取消的行不是待办。
- `scope=mine` 的「我的」按源单处理人判:散单来源看该需求持有人,团期来源看该团期房务认领人;源单读不到时该条目在 `mine` 下不会出现(无法判定处理人)。
- 窗口跨度上限 92 天,与控房表查询的 62 天不是同一个上限,别复用。
- 纯读接口,不写数据。
---
## 四、契约约束与正确调用方式(接口类必写)
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload / 判断 |
|------|----------------|
| ✅ 设期限 | `{ "cancelDays": 3, "cancelCutoff": "18:00", "remindDays": 1 }` |
| ✅ 设期限只给天数 | `{ "cancelDays": 3 }` → 截止时刻取 `18:00`、提醒天数取 `1` |
| ✅ 清除期限 | `{ "cancelDays": null }`(或整个 body 只有 `{}`)→ 200,`cancelCutoff` / `remindDays` 保留原值 |
| ✅ 判「未设期限」 | `data.cancelDays === null`,或 `data.risk === "NO_DEADLINE"` |
| ❌ 判「未设期限」 | `data.cancelCutoff === null && data.remindDays === null`——清除期限后这两项仍有值,该判断恒为 false |
| ❌ 清除期限时显式传空 | `{ "cancelDays": null, "cancelCutoff": "", "remindDays": null }` 能成功,但 `cancelCutoff` / `remindDays` 一样被忽略,不要指望用它们清值 |
| ❌ 期限天数越界 | `{ "cancelDays": 61 }` → 808328(不是 400) |
### 切换状态时的必要动作
- 清除期限成功后,前端应以返回的整行直接替换列表行,不要只把 `cancelDays` 置空——`risk` / `riskLabel` / `deadlineAt` 都由后端重算,本地推算会与「退团房分页」的汇总读数对不上。
- 「异常检查」页签的待办条数在订正后可能增加;若页面上有与之对照的徽标计数,改为直接用本端点返回的 `tasks.length`,不要沿用按订单状态自行过滤后的口径。
---
## 五、数据库行为(涉及写操作时必写)
| 写操作 | 外部可观察行为 |
|--------|----------------|
| 设期限(`cancelDays` 非空) | 该行的免费退改天数、截止时刻、提醒天数三项按入参(含默认值)整体更新;行版本 +1;写一条订单级操作日志「退团房 {入住晚} {酒店名} 免费退改期限改为入住前 N 天 HH:mm」 |
| 清除期限(`cancelDays` 为 `null`) | **只有免费退改天数被清空**;截止时刻与提醒天数保持该行原值;行版本 +1;写一条订单级操作日志「…免费退改期限改为未设」 |
| 并发保护 | 行级分布式锁 + 行版本比对;版本在锁定读与写之间被改则整笔回滚并返 808932 |
- 两个写路径都不产生跨服务调用,不发消息。
- 「异常检查」端点是纯读,不写任何数据。
---
## 六、边界行为
- 清除期限后再次设期限,若只传 `cancelDays`,截止时刻与提醒天数会被**重新按默认值 `18:00` / `1` 覆盖**(不是沿用清除前保留下来的那两个值);要沿用旧值必须显式回传。
- 从未设过期限的新行,其截止时刻与提醒天数是建表默认的 `"18:00"` 与 `1`,所以「一行从没设过期限」与「设过又被清掉」在这两个字段上不可区分;唯一区分点是操作日志。
- 风险分档 `NEAR` 按「日」比较(今天 ≥ 截止日 − 提醒天数),同一天上午和下午不会给出不同分档。
- 退团房待办的取行只设窗口下限(入住晚不早于窗口起日),不设上限:待处理池量级小,多读回的行由源单出发日过滤掉。
- `scope=all` 时任何房务都能看到全部待办条目,但看得见不等于能写;改期限仍按源单处理人校验(808326)。
---
## 六.5、枚举 / 数据字典(接口出现枚举时必写)
### risk(退团房风险分档)
| 值 | 中文(`riskLabel`) | 判据 |
|----|--------------------|------|
| `OVERDUE` | 已过免费取消期 | 现在已过截止时刻 |
| `NEAR` | 临近免费取消期 | 今天 ≥ 截止日 − 提醒天数 |
| `NO_DEADLINE` | 未设免费取消期 | `cancelDays` 为空(或该行缺入住晚) |
| `NORMAL` | 正常 | 其余 |
> 仅 `status = PENDING` 的行有值,其余行 `risk` / `riskLabel` 均为 `null`。
### status(退团房行状态)
| 值 | 中文(`statusLabel`) | 说明 |
|----|----------------------|------|
| `PENDING` | 待处理 | 还有剩余间数待转出或向酒店取消;只有这一状态能改期限 |
| `TRANSFERRED` | 已转出 | 全部间数已转给别的订单 / 团期 |
| `CANCELLED` | 已取消 | 已向酒店取消 |
### taskCode / code(异常检查待办码,`tasks[]`)
| 值 | 中文(`codeLabel`) | 说明 |
|----|--------------------|------|
| `TRANSFER_PENDING` | 退团房未结清 | 本次订正影响的就是这一类的取行范围 |
| `HOTEL_CANCEL_PENDING` | 原酒店待取消 | 改配留下的原订尚未确认取消 |
| `INQUIRY_PENDING` | 新订 / 变更待确认 | — |
| `STAY_UNARRANGED` | 住宿待落实 | — |
> `issues[]` 的 `code` 取值域是另一组(`STOCK_OVERBOOKED` / `STOCK_LEDGER_MISMATCH` / `STOCK_ROW_MISSING` / `TRANSFER_TARGET_GONE` / `PLAN_COUNT_MISMATCH`),本次未变。
---
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
### 字段级对比
| 字段 | 之前交接件写的 | 现在的实际行为 |
|------|----------------|----------------|
| `data.cancelCutoff`(清除期限后) | 被一并置空为 `null` | **保留该行原值**(从未设过则为 `"18:00"`) |
| `data.remindDays`(清除期限后) | 被一并置空为 `null` | **保留该行原值**(从未设过则为 `1`) |
| `data.cancelDays`(清除期限后) | `null` | `null`(不变) |
| `data.deadlineAt`(清除期限后) | `null` | `null`(不变) |
| `data.risk` / `riskLabel`(清除期限后) | `NO_DEADLINE` / 「未设免费取消期」 | 同(不变) |
| 异常检查 `tasks[]` 结构 | — | 字段与类型均未变 |
### 行为级对比
| 行为 | 之前 | 现在 |
|------|------|------|
| `PUT .../deadline` 传 `cancelDays: null` | 恒定返回 808932「房务状态已被并发修改,请刷新后重试」,期限清不掉 | 返回 200 并给出更新后的整行 |
| `PUT .../deadline` 传 `cancelDays` 非空 | 成功 | 成功(口径不变) |
| `GET .../audit` 的 `TRANSFER_PENDING` | 源订单 / 源团期已取消的退团房行不出现在 `tasks` 里 | 按源单出发日归属、不看源单状态,这些行会出现 |
| `GET .../audit` 的 `TRANSFER_PENDING`(源单查不到 / 出发日为空) | 不出现 | 出现(宁可多报一条) |
| `GET .../audit` 的 `issues[]` | — | 口径不变 |
---
## 六.7、影响评估(修改/删除类必写)
| 项 | 评估 |
|----|------|
| 需要前端改代码 | **是(1 处必改)**:凡按 `cancelCutoff` / `remindDays` 是否为 `null` 判「有没有设期限」的地方,改为按 `cancelDays === null` 或 `risk === "NO_DEADLINE"` 判。 |
| 需要前端改代码 | **可能(1 处)**:「异常检查」页签若自行按订单状态过滤过待办条目,去掉该过滤,直接用后端返回的 `tasks`。 |
| 兼容性 | 无字段增删、无类型变化、无路径与方法变化;只有取值与取行范围变化。 |
| 「清除期限」功能 | 从不可用变为可用,前端原有的 808932 报错提示分支在该场景不再触发(真并发冲突仍会返回它,不要删该分支)。 |
| 读数变化 | 同一出发日窗口下「异常检查」的待办行数只会增加或不变,不会减少。 |
| 其他消费方 | 退团房分页、转房、向酒店取消三个端点的契约未变;本次不涉及小程序端。 |
---
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
- 退团房分页 `GET /v3/admin/order/house-console/room-transfers`、转入候选 `GET .../room-transfers/{id}/candidates`、转房 `POST .../room-transfers/{id}/transfer`、向酒店取消 `POST .../room-transfers/{id}/cancel-hotel`:字段与口径均未变。
- 控房表(查询 / 调房量 / 调价 / 导出)、住宿模板、批量认领、改配取消确认、团期转交:未触及。
- `issues[]` 的五类问题码及其判据未变。
- 房务只读标识 `readOnly` / `readOnlyReason` 的口径未变(其口径见 #8491 的两份交接件)。
- 通知、消息模板、跳转链接未变。
- 无数据库结构变更,无新增 Flyway 脚本,无网关路由改动。
---
## 八、测试环境已验证
| 项 | 读数 | 依据 |
|----|------|------|
| hl-order-service-v3 运行的提交 | `d57498d38`,BEHIND `0/N`,STATE `ok`,时间 2026-09-30 05:32:57 | 测试服部署登记脚本 `/opt/hulalv/scripts/deploy-status.sh`,2026-09-30 本次读取 |
| 本次改动在运行的字节里 | 是 | `git merge-base --is-ancestor ff68637542 origin/dev-v3` 返回 0;`origin/dev-v3` 头为 `d57498d381` |
| 清除期限 | `cancelDays=null` → `code=200`;库里免费退改天数为 NULL,截止时刻 / 提醒天数保留原值;`risk=NO_DEADLINE` | 取证提交 `ff6863754`,读数原文记在 `changelogs-v2/2026-09/30_8491_房务控制台接口-新增接口-管理后台.md` 第 1847 行 |
| 设期限(回归) | `cancelDays=3` → `code=200`、该行 `risk=OVERDUE`;`cancelDays=0` + `remindDays=1` → `code=200`、`risk=NEAR` | 同上文件第 1845、1846 行,复测提交 `ff6863754` |
| 退团房待办含源单已取消的行 | 源订单已取消 + 退团房行 `PENDING` + 窗口覆盖出发日 → `code=200`,`tasks` 含 `TRANSFER_PENDING`「退团房未结清」 | 同上文件第 1861 行,取证提交 `ff6863754` |
| 用例覆盖 | `HouseRoomTransferManagerTest#updateDeadline_nullCancelDays_keepsRowCutoffAndRemind`;`HouseRoomTransferMapperMysqlTest#casUpdateDeadline_nullCancelDaysWithRowValues_writesNullAndKeepsCutoffRemind`;`HouseConsoleAuditManagerTest#audit_pendingTransferSourceOrderCancelledInWindow_listed` / `#audit_pendingTransferSourceBatchCancelledInWindow_listed` / `#audit_pendingTransferSourceOrderMissing_listed` / `#audit_pendingTransferSourceOrderDepartOutsideWindow_notListed` | 随 `ff68637542` 新增 |
---
## 九、相关历史 PR(纠错 / 功能演进时必写)
| PR / 提交 | 内容 | 与本条的关系 |
|-----------|------|--------------|
| PR #8564(`7c21cf0e40`) | 房务控制台整套(#8491),首次引入本文两个端点 | 被订正的表述出自它的交接件 |
| PR #8599(`ff68637542`) | 本条的两处修复(#8597) | 本条正文描述的就是它合入后的行为 |
---
## 十、相关文档
- 本条 Issue:#8597(PR #8599)
- 被订正的交接件:`changelogs-v2/2026-09/30_8491_房务控制台接口-新增接口-管理后台.md`(接口 8 与接口 12)
- 只读标识口径:`changelogs-v2/2026-09/30_8491_房务配房接口字段与口径调整-修改接口-管理后台.md`
---
## 关联 / 联系人
### 链接
- Issue: #8597
- PR: #8599
- 分支基线: `dev-v3`
### 联系人
- 后端: wx
- 前端: mmg(管理后台)
@@ -0,0 +1,228 @@
---
schema: "hl-changelog/v2"
ticket: "8598"
title: "派单保险隔离事件新增人工终结(DISCARDED)出口"
consumer: "admin"
author: "wx(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "新增 POST /admin/fleet/insurance/assignment-events/{eventId}/discard,仅 SUPER_ADMIN 可调用,把处于 QUARANTINED 的派单保险隔离事件人工终结为新终态 DISCARDED,终结不可逆、无回退入口,也无批量接口,逐条操作且终结原因必填(非空、≤200字)。幂等窗口10秒(重复提交返100502),并发冲突返100503(CAS未命中)。DISCARDED 会让关联的行程短信状态查询/重发端点(GET及POST .../itinerary-sms[/retry])对该事件返回 status=FAILED、canRetry=false——这是已有取值组合,不引入新字段或新枚举值,前端已有的 FAILED 分支即可覆盖,不需要新增代码路径。gateway_status=not_required,复用既有 /admin/fleet/** 路由,未新增网关配置。backend_status=deployed:PR #8607(合并提交5afadf6c634)已合并,hl-fleet-service 已部署测试服并与本条目所依据的源码提交一致(deploy-status.sh 实测 COMMIT=d57498d38,STATE=ok,2026-09-30 05:34:07 部署);未登录态 curl 实测该路径已挂载并优先鉴权(返 200 信封 code=401,非 HTTP 401 状态码),路由与鉴权链路均已验证。;前端实证:hl-admin 全仓零 assignment-events/itinerary-sms 封装,QUARANTINED 仅 spec fixture,无消费点,判 not_required(hl-admin sync-log 2026-09-30)"
updated_at: "2026-09-30"
base: "dev-v3"
---
# 车务保险: 隔离事件新增人工终结(DISCARDED)出口
> **存放目录**: 二期(order-v3)→ `changelogs-v2/2026-09/`
>
> **服务**: hl-fleet-service(端口 8087)
> **PR**: [#8607](https://git.1814.love:8443/wx/HL/pulls/8607)
> **Issue**: [#8598](https://git.1814.love:8443/wx/HL/issues/8598)
> **日期**: 2026-09-30
> **影响范围**: 超管对派单保险隔离 Outbox 事件的处置面板,新增一个终结动作;对既有重放端点与行程短信状态端点零结构变化
---
## ⚠️ 关键变化
隔离事件(`QUARANTINED`)此前**唯一的出口是重放**——重放会再隔离的事件(关联需求已删、内容审核不过、上游数据已按别的单清理),会让卡死告警永久为红,没有任何办法让它退出告警。本次新增一个**人工终结**动作,把这类确定性失败的事件显式标成新终态 `DISCARDED`,终结之后它退出卡死告警、不再被扫描器捞起、也不再阻塞同 `orderingKey` 的后继事件。**终结无回退入口,是单向不可逆操作**。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 终结已隔离的派单生命周期事件 | POST | `/admin/fleet/insurance/assignment-events/{eventId}/discard` | 新增接口 | 仅 SUPER_ADMIN,QUARANTINED → DISCARDED |
---
## 三、接口详情
### 1. 终结已隔离的派单生命周期事件 `POST /admin/fleet/insurance/assignment-events/{eventId}/discard`
**VO**: `AssignmentInsuranceOutboxDiscardReqVO → AssignmentInsuranceOutboxDiscardRespVO`
#### 使用场景
超管在派单保险 Outbox 卡死告警/隔离事件处置面板里,对一条已确认「重放多少次都会再隔离」的事件(例如关联需求已随团期撤销删除、内容审核不通过、上游数据已被另一张单清理)执行终结,承认这个业务动作确实不会再发生、也不再补,并把原因、操作人、时间留痕。与重放动作共用同一批隔离事件列表数据源,本次不新增查询端点。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| eventId | Path | Long | ✅ | - | Outbox 事件 ID |
| reason | Body | String | ✅ | 非空;≤200 字 | 确认不再重放的原因,须说明业务影响已如何处置 |
#### 出参 `Result<AssignmentInsuranceOutboxDiscardRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| eventId | String(Long 转字符串) | Outbox 事件 ID |
| previousStatus | String | 终结前状态,恒为 `QUARANTINED` |
| status | String | 终结后状态,恒为 `DISCARDED` |
| operatorId | String(Long 转字符串) | 操作人管理员 ID |
| discardedAt | String | 终结操作时间,格式 `yyyy-MM-dd HH:mm:ss` |
#### 请求示例
```json
{
"reason": "关联需求已随团期撤销删除,短信不再需要补发"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"eventId": "1934567890123456789",
"previousStatus": "QUARANTINED",
"status": "DISCARDED",
"operatorId": "88",
"discardedAt": "2026-09-30 10:20:30"
},
"success": true
}
```
#### 空数据 / 降级响应
本接口是单事件的状态迁移动作,没有「空数据」或部分成功的中间态——调用结果只有「成功迁移」或下方错误响应里的某一种拒绝,不存在降级返回。
#### 错误响应
```json
{
"code": 403001,
"message": "无权限,仅超级管理员可终结隔离事件",
"success": false,
"data": null
}
```
其余可能返回的错误码:
| code | 触发条件 | message |
|---|---|---|
| 400 | `reason` 为空白或超过 200 字(Bean Validation,先于业务逻辑拦截) | `终结原因不能为空` 或 `终结原因最多200字` |
| 100001 | `eventId` 对应事件不存在 | `参数非法: 隔离事件不存在` |
| 100001 | 事件当前状态不是 `QUARANTINED`(已是 SUCCESS/DISCARDED/PENDING/PROCESSING) | `参数非法: 仅允许终结 QUARANTINED 事件,当前状态为{实际状态}` |
| 100502 | 同一 `eventId` 10 秒幂等窗口内重复提交 | `隔离事件终结中,请勿重复提交` |
| 100503 | 并发命中 CAS 未命中(他人同时终结/重放,或处理器抢先处理) | `资源被占用,请稍后重试` |
| 401 | 未登录(网关统一信封,HTTP 状态码仍是 200,信封内 `code=401`) | `缺少有效的 Authorization 头` |
#### 业务边界
- 只接受当前状态为 `QUARANTINED` 的事件;其余状态一律 100001 拒绝。
- 终结是单向操作,没有「撤销终结」的接口。
- 无批量终结接口,只能逐条调用——设计上刻意如此:批量会把混在隔离事件里的真实业务缺口一次性静默抹掉。
- 幂等键为 `eventId`(10 秒窗口),并发保护为乐观锁 CAS;两者返回的错误码不同(100502 vs 100503),前端应分别处理:100502 提示稍候,100503 建议重新拉取该事件当前状态后再决定下一步。
- 终结成功后,该事件对应的行程短信状态查询/重发端点(`GET /admin/fleet/assignments/{assignmentId}/itinerary-sms`、`POST .../itinerary-sms/retry`)会返回 `status=FAILED, canRetry=false`——这是这两个端点已公开枚举值集合里已有的取值组合,不是新增字段或新增枚举值,前端已有的 `FAILED` 分支不需要改动即可正确渲染。
---
## 四、契约约束与正确调用方式
> 本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload | 结果 |
|------|---------|------|
| ✅ 原因非空且 ≤200 字 | `{ "reason": "关联需求已随团期撤销删除,短信不再需要补发" }` | 200,事件迁移为 DISCARDED |
| ❌ 原因为空 | `{ "reason": "" }` 或 `{ "reason": " " }` | 400 `终结原因不能为空` |
| ❌ 原因超长 | `{ "reason": "<201 个字符>" }` | 400 `终结原因最多200字` |
| ❌ 对非 QUARANTINED 事件调用 | 任意合法 reason,但目标事件当前是 SUCCESS/DISCARDED/PENDING/PROCESSING | 100001,message 里点名当前状态 |
### 调用前置
调用前前端应确认目标事件当前处于「已隔离」状态(面板上通常是从隔离事件列表点进来),不要对已经终结过、已成功、或还在处理中的事件发起终结请求——这些情形不会被静默忽略,而是显式返回 100001。
---
## 五、数据库行为
- 终结成功后,该事件状态字段变为 `DISCARDED`;原因、操作人、操作时间会被记录(复用重放动作已有的三个字段承载,未新增列)。
- 终结成功后再对该事件调用既有的重放端点(`POST /admin/fleet/insurance/assignment-events/{eventId}/replay`),会返回 100001「仅允许重放 QUARANTINED 事件,当前状态为DISCARDED」。
- 终结不会产生任何下游消息重放或补发——它就是承认这件事不会再发生。
---
## 六、边界行为
- 未登录 → 网关统一信封 `code=401`(HTTP 状态码 200,非 HTTP 401)
- 非超管 → 403001
- `eventId` 不存在 → 100001
- 事件状态非 `QUARANTINED` → 100001,message 带当前实际状态
- `reason` 为空/超长 → 400(Bean Validation 先于业务逻辑拦截)
- 10 秒幂等窗口内重复提交同一 `eventId` → 100502
- 并发命中 CAS 未命中 → 100503
- 下游服务降级 → 不适用,本接口无下游读取,只做本域状态迁移
---
## 六.5、枚举 / 数据字典
### status / previousStatus(`AssignmentInsuranceOutboxStatusEnum`)
**所属字段**: `status` / `previousStatus`(`AssignmentInsuranceOutboxDiscardRespVO`) | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `PENDING` | 待处理 | 尚未开始处理(本接口不接受此状态) |
| `PROCESSING` | 处理中 | 正在处理(本接口不接受此状态) |
| `QUARANTINED` | 已隔离 | 卡死告警状态,唯一可被本接口终结的状态;`previousStatus` 恒为此值 |
| `SUCCESS` | 成功 | 机器判定的成功终态(本接口不接受此状态) |
| `DISCARDED` | 已终结(本次新增) | 人工判定的放弃终态,只能由本接口产出,单向不可逆;`status` 恒为此值 |
---
## 七、不影响范围
- **仅影响**: 派单保险隔离 Outbox 事件处置面板,新增一个终结动作入口
- **零影响**:
- 既有重放端点 `POST /admin/fleet/insurance/assignment-events/{eventId}/replay` 的路径、参数、错误码
- 既有隔离事件列表/卡死告警统计查询
- 行程短信状态查询/重发端点的响应字段结构与已公开枚举值集合(新增的只是一条已有取值组合被触发的路径)
---
## 八、测试环境已验证
- `deploy-status.sh`(测试服现状表)实测:`hl-fleet-service` COMMIT=`d57498d38`、STATE=ok,2026-09-30 05:34:07 部署——与本条目所依据的源码提交(含 #8598 合并提交 `5afadf6c634`)一致。
- 测试服内网 `curl` 实测路由已挂载且鉴权前置生效:
```
POST http://127.0.0.1:8080/admin/fleet/insurance/assignment-events/1/discard (无 Authorization 头)
→ HTTP 200
{"code":401,"message":"缺少有效的 Authorization 头","data":null,"traceId":"f3e71e825f844525","success":false}
```
---
## 十、相关文档
- 关联 Issue: [wx/HL#8598](https://git.1814.love:8443/wx/HL/issues/8598)
- 关联 PR: [wx/HL#8607](https://git.1814.love:8443/wx/HL/pulls/8607)
## 关联 / 联系人
### 链接
- **Issue**: [#8598](https://git.1814.love:8443/wx/HL/issues/8598)
- **PR**: [#8607](https://git.1814.love:8443/wx/HL/pulls/8607)
- **Merge commit**: [`5afadf6c634`](https://git.1814.love:8443/wx/HL/commit/5afadf6c634997a31b0f912f80ab0b67c34a9c2c)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,440 @@
---
schema: "hl-changelog/v2"
ticket: "8601"
title: "逐户提交车务 / 打回的 kind 参数取消默认值 TRAVEL,两类活跃需求并存时必须显式指定(新错误码 809012)"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "527842c1f6c5764ebf000b258d14389fb2e4c2c9"
target_release: "v2.1"
verified_at: "2026-09-30"
status_note: "PR #8610 已 squash 合并 dev-v3(a2ba628ecb),hl-order-service-v3 dev-v3 分支已滚测试服。这是需要前端改调用代码的变更:两个端点的 kind 查询参数从 defaultValue=TRAVEL 改成无默认值,纯接送机户由此可用,两类并存且不传 kind 时新抛 809012。;前端已交付:orderV2.js 两处 JSDoc 纠错+详情页两弹窗钉注释;batch 侧调用已显式 kind,详情页事件链 brief 无 kind(后端源码实证)保持不传,809012 拦截器透 message 兜底,行为零变化,既有 spec 回归全绿(hl-admin 527842c1)"
updated_at: "2026-09-30"
base: "dev-v3"
---
# hl-order-service-v3: 逐户提交车务 / 打回的 `kind` 参数取消默认值 `TRAVEL`
> **存放目录**: `changelogs-v2/2026-09/`
> **服务**: hl-order-service-v3 (端口 8086)
> **PR**: #8610
> **Issue**: #8601
> **日期**: 2026-09-30
> **影响范围**: 团期需求管理 Tab 的逐户「提交车务」与「打回定制师」两个按钮所调的端点,其 `kind` 查询参数语义
---
## ⚠️ 关键变化
- 🔴 **这是需要前端改调用代码的变更**:两个端点的 `kind` 查询参数由 `@RequestParam(defaultValue = "TRAVEL")` 改为 `@RequestParam(required = false)`,**没有默认值了**。
- **前端以前可以怎么写**:不传 `kind`,后端按 `TRAVEL` 处理。**现在的实际行为**:不传 `kind` 时后端按该户**活跃用车需求的类别数**自动解析——
- **恰好 1 类** → 就用那一类(🆕 **纯接送机户从此可用**:旧默认值会去找一条根本不存在的 TRAVEL 行,导致这类户在这两个入口走不通流程);
- **0 类** → 与改前一致,由既有分支抛 582031「订单无有效需求行」;
- **≥2 类并存** → 🆕 抛新错误码 **809012**,拒绝猜测。
- 🔴 **809012 是新增错误码**,前端必须接住:`订单 {0} 同时存在 {1} 两类活跃用车需求,请显式指定要操作的类别(kind=TRAVEL 行程用车 / kind=TRANSFER 接送机)`。撞到它的正确处置是**带上 `kind` 重发**(由用户选,或由页面上下文决定),不是重试。
- **前端要做的事**:这两个按钮所在的位置本来就知道自己在操作哪一类需求(页面上就是按 TRAVEL / TRANSFER 分开展示的),**一律显式带上 `kind`** 即可,带了就不会撞 809012。传了值的行为与改前逐字相同(含非法值仍由 809000 拒)。
- **请求体、响应体、权限、HTTP 形态全部未变**:两个端点仍是 `Result<Void>`,`dispatchRemark` 仍选填、`returnRemark` 仍必填。
---
## 一、背景
一户订单的用车需求按类别分行,两类可以同时活跃:
| 类别 | 含义 |
|------|------|
| `TRAVEL` | 团期行程用车 |
| `TRANSFER` | 接送机 |
这两个端点都按 `kind` **精确定位一行**再迁移状态、写备注。`defaultValue = "TRAVEL"` 让「调用方没说要动哪一类」与「调用方明确要动 TRAVEL」在服务层**完全同形**——两类并存而调用方没传 `kind` 时,接口返 200、改掉 TRAVEL 行,而操作者想动的 TRANSFER 行三个字段一个都没变,**响应上没有任何可区分的信号**。这是静默错写。
同一个默认值还造成第二个缺陷:只有 TRANSFER 活跃行的户,旧逻辑会去找一条不存在的 TRAVEL 行,拿到 582031,**这类户在这两个入口根本用不了**。
解析只写在服务层一处(`RequirementService#resolveVehicleRequirementKind`),Controller 不做兜底,避免两层各写一份「空了怎么办」而日后分叉。类别数与后续取行**同源**:数的是 `selectAllActiveByOrderId`,而它的实现就是对 `selectLatestByOrderId(orderId, kind)` 按枚举逐类别循环,所以「数出几类」与「按那一类取到哪行」用的是同一个筛选条件,不可能分叉。
> 与结算侧 809008 有意不同:那边只有 TRAVEL 才自动解析、单独一条 TRANSFER 也拒绝(手录车费的省略更可能是漏选归属);本处是需求状态机写口,单 TRANSFER 户只有这一条活跃需求,拒绝它等于让这类户走不通流程。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 团期管理员提交车务 | POST | `/v3/admin/order/{id}/vehicle-requirement/dispatch` | 查询参数取消默认值 + 新增错误码 | `kind` 不再默认 TRAVEL;两类并存且不传抛 809012 |
| 2 | 团期管理员打回定制师(车需求) | POST | `/v3/admin/order/{id}/vehicle-requirement/reject` | 查询参数取消默认值 + 新增错误码 | 同上 |
---
## 三、接口详情
### 1. 团期管理员提交车务 `POST /v3/admin/order/{id}/vehicle-requirement/dispatch`
**VO**: `DispatchReqVO → Result<Void>`
#### 使用场景
团期需求管理 Tab 里对某一户点「提交车务」时调用,把该户指定类别的用车需求从 `PENDING_REVIEW` 推进到 `PENDING` 并写入提交备注(提供给车队人员查看)。**仅团期子订单可用**。权限点 `group-batch:demand:confirm`。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `id` | Path | Long | ✅ | - | 子订单 ID |
| `kind` | Query | String | ❌ | 取值 `TRAVEL` / `TRANSFER` | 🔴 **改动点**:不再有默认值 `TRAVEL`。不传时按该户活跃需求类别自动解析(恰好 1 类用那一类;0 类抛 582031;≥2 类抛 809012)。**建议一律显式传**。非法值仍抛 809000 |
| `dispatchRemark` | Body | String | ❌ | `@Size(max=500)` | 提交备注,提供给车队的审核意见;上限对齐库列宽 `VARCHAR(500)` |
#### 出参 `Result<Void>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 200 = 成功 |
| `message` | String | `成功` |
| `success` | Boolean | `true` |
| `data` | null | **本端点无业务数据返回**(`Result<Void>`),成功即以 `code=200` 为准,不要读 `data` |
#### 请求示例
```http
POST /v3/admin/order/2099459272533323777/vehicle-requirement/dispatch?kind=TRANSFER
Content-Type: application/json
{ "dispatchRemark": "需求已确认,请尽快派接送机车辆" }
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": null
}
```
#### 空数据 / 降级响应
本端点恒无业务数据,成功时 `data` 恒为 `null`——这是正常成功形态,不是空数据降级:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": null
}
```
该户没有任何活跃用车需求行时不降级、不静默成功,直接抛 582031(下面的错误响应)。
#### 错误响应
🆕 809012(本次新增;`{0}` = 订单 ID,`{1}` = 两类名以 ` / ` 连接)。以下为测试服实测原文(订单 ID 2105173274755534850):
```json
{
"code": 809012,
"message": "订单 2105173274755534850 同时存在 TRAVEL / TRANSFER 两类活跃用车需求,请显式指定要操作的类别(kind=TRAVEL 行程用车 / kind=TRANSFER 接送机)",
"success": false,
"data": null
}
```
其余错误码未变:
| 码 | 报文 | 触发 |
|----|------|------|
| 809000 | `用车需求类别非法:{0}` | `kind` 传了 TRAVEL / TRANSFER 之外的值 |
| 809007 | `接送机需求 {0} 的服务日期尚未回填,无法下发车务` | 解析到 TRANSFER 但该行 `service_dates` 为 NULL 或空数组 |
| 582031 | `订单无有效需求行` | 该户按解析出的类别取不到活跃行(含「一条都没有」) |
| 582083 | `需求状态不允许此操作,请检查当前状态` | 非团期子订单,或最新需求不在 `PENDING_REVIEW` |
#### 业务边界
- **鉴权**:权限点 `group-batch:demand:confirm`(Controller 入口执行,走 user-service Feign);未登录由网关拦截返 401。
- **仅团期子订单**:非团期单先报 582083,`kind` 解析排在这道守卫**之后**——所以非团期单的报错与改前逐字相同,不会变成 809012。
- **解析只在不传 `kind` 时发生**:传了值就原样使用,包括非法值(仍由 809000 拒),本次改动不改变既有的非法值行为。
- **🔴 809012 不是可重试错误**:同一请求重发多少次都是同一个码。处置是**带上 `kind` 重发**。
- **纯接送机户现在可用**:只有一条活跃 TRANSFER 行时不传 `kind` 会被解析成 TRANSFER(改前拿 582031)。
- **状态机守卫未变**:`PENDING_REVIEW → PENDING`,同时写 `dispatch_remark`、车控置 `PENDING`、CAS 退流程。
- **失败零写入**:809012 抛在取行之前、任何写入之前;809007 抛在服务日校验处,同样不落写。
---
### 2. 团期管理员打回定制师(车需求) `POST /v3/admin/order/{id}/vehicle-requirement/reject`
**VO**: `RejectReqVO → Result<Void>`
#### 使用场景
团期需求管理 Tab 里对某一户点「打回」时调用,把该户指定类别的用车需求退回定制师重提(`PENDING_REVIEW` / `PENDING` → `REJECTED_TO_CONSULTANT`),写入打回备注,并**同时清掉团级 `requirement_confirmed` 标记 + 写团级时间线**(否则会出现「该户未提交、整团已确认」的矛盾态)。权限点 `group-batch:demand:confirm`。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `id` | Path | Long | ✅ | - | 子订单 ID |
| `kind` | Query | String | ❌ | 取值 `TRAVEL` / `TRANSFER` | 🔴 **改动点**:不再有默认值 `TRAVEL`,规则与 dispatch 端点逐条相同。**建议一律显式传** |
| `returnRemark` | Body | String | ✅ | `@NotBlank`,`@Size(max=500)` | 打回备注;为空返 400「打回/驳回备注不能为空」,超长返 400「打回/驳回备注不能超过 500 字」。定制师重新提交时会创建新需求 |
#### 出参 `Result<Void>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 200 = 成功 |
| `message` | String | `成功` |
| `success` | Boolean | `true` |
| `data` | null | **本端点无业务数据返回**(`Result<Void>`) |
#### 请求示例
```http
POST /v3/admin/order/2099459272533323777/vehicle-requirement/reject?kind=TRAVEL
Content-Type: application/json
{ "returnRemark": "行程日与团期不符,请定制师重新确认用车日期" }
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": null
}
```
#### 空数据 / 降级响应
本端点恒无业务数据,成功时 `data` 恒为 `null`:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": null
}
```
打回既要写需求行、又要清团级标记并写团级时间线,是跨聚合编排且与整团确认共用团级锁——**不存在「只做了一半」的降级形态**,要么整套生效要么整体回滚。
#### 错误响应
🆕 809012(本次新增,与 dispatch 端点同码同文案)。以下为测试服实测原文(订单 ID 2105173313083080706):
```json
{
"code": 809012,
"message": "订单 2105173313083080706 同时存在 TRAVEL / TRANSFER 两类活跃用车需求,请显式指定要操作的类别(kind=TRAVEL 行程用车 / kind=TRANSFER 接送机)",
"success": false,
"data": null
}
```
其余错误码未变:
| 码 | 报文 | 触发 |
|----|------|------|
| 809000 | `用车需求类别非法:{0}` | `kind` 传了非法值 |
| 582031 | `订单无有效需求行` | 按解析出的类别取不到活跃行 |
| 582083 | `需求状态不允许此操作,请检查当前状态` | 非团期子订单,或最新需求不在 `PENDING_REVIEW` / `PENDING` |
| 400 | `打回/驳回备注不能为空` | `returnRemark` 空 |
> `589535`(子订单已分房)**不适用于本端点**:占用探测方法对 `resourceType=VEHICLE` 硬编码返回"无占用"(车侧无配房概念),该码只在 `resourceType=HOTEL` 时可能触发;已派车的拦截由另一套栅栏机制负责,不经过本码。这是既有行为,本次未改。
#### 业务边界
- **鉴权**:权限点 `group-batch:demand:confirm`;未登录由网关拦截返 401。
- **kind 解析结果同时用于占用探测与实际取行**:两步必须看同一个类别,避免错位——但车需求侧的占用探测恒不拦截(见上方说明),此为既有行为,本次未改。
- **只对车需求解析**:同一条服务方法也承接房需求打回(`resourceType=HOTEL`),`kind` 对它无意义、不触发解析,**酒店打回不会被车侧的两类并存误伤**。
- **副作用是跨聚合的**:除需求行外还会清团级 `requirement_confirmed` 并写团级时间线 `BATCH_REQUIREMENT_REJECT`,与批量打回落同一套副作用。
- **打回后需求要重提**:定制师重新提交会创建**新的需求行**,不是在原行上改。
- **🔴 809012 不是可重试错误**:处置是带上 `kind` 重发。
- **失败零写入**:809012 抛在取行与实写之前。
---
## 四、契约约束与正确调用方式
> 本节只写后端接受 / 拒绝请求的规则,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误 调用对照
| 场景 | 请求 | 结果 |
|------|------|------|
| ✅ 显式指定行程用车(**推荐写法**) | `POST /v3/admin/order/{id}/vehicle-requirement/dispatch?kind=TRAVEL` + `{ "dispatchRemark": "..." }` | 200 |
| ✅ 显式指定接送机(**推荐写法**) | `POST /v3/admin/order/{id}/vehicle-requirement/dispatch?kind=TRANSFER` + `{ "dispatchRemark": "..." }` | 200 |
| ✅ 不传 kind,该户只有一类活跃需求 | `POST /v3/admin/order/{id}/vehicle-requirement/dispatch` | 200,按那一类处理(纯接送机户从此可用) |
| ✅ 打回(备注必填) | `POST /v3/admin/order/{id}/vehicle-requirement/reject?kind=TRAVEL` + `{ "returnRemark": "请重新确认用车日期" }` | 200 |
| ❌ 不传 kind,该户两类活跃需求并存 | `POST /v3/admin/order/{id}/vehicle-requirement/reject` | **809012**,必须带 kind 重发 |
| ❌ kind 传非法值 | `?kind=travel2` | 809000 |
| ❌ 打回不带备注 | `{ "returnRemark": "" }` | 400「打回/驳回备注不能为空」 |
| ❌ 备注超 500 字 | `{ "returnRemark": "<501 字>" }` | 400「打回/驳回备注不能超过 500 字」 |
### 前端必须做的改动
1. **两个端点的调用一律显式带上 `kind`**。页面上这两个按钮本来就分挂在 TRAVEL / TRANSFER 两块需求下,取值是现成的;带上之后永远不会撞 809012。
2. **接住 809012**:若某处确实拿不到类别,撞到 809012 时要提示用户选择类别并**带上 `kind` 重发**,不要做自动重试(同一请求重发永远同码)。
3. **不要再依赖「不传 = TRAVEL」这个隐含约定**——它已经不成立了。
> 🔴 **落点已查明(2026-09-30 对 `hl-ui` `origin/v2.1` 查证)**:`src/api/orderV2.js` 里
> `rejectVehicleRequirement` 的 JSDoc 写着「`TRAVEL / TRANSFER`;不传保持旧行为(按 TRAVEL)」、
> `dispatchVehicleRequirement` 写着「不传=后端缺省 TRAVEL」——**这两句现在都是错的**,
> 两处都是 `const query = kind ? { kind } : null`,调用方不传就会走到新行为上。
> 另外全仓 `809012` 命中数为 **0**,即该码今天没有任何接住的地方。
---
## 五、数据库行为
本次改动**没有任何表结构变化**,变的是「写哪一行」的定位规则。
| 前端调用 | 该户活跃需求 | 改前写入 | 改后写入 |
|----------|--------------|----------|----------|
| 不传 `kind` | 只有 TRAVEL | TRAVEL 行 | TRAVEL 行(未变) |
| 不传 `kind` | 只有 TRANSFER | ❌ 取不到 TRAVEL 行 → 582031,零写入 | ✅ **TRANSFER 行** |
| 不传 `kind` | TRAVEL + TRANSFER 并存 | ❌ **静默写 TRAVEL 行**(返 200,操作者要动的那行三个字段全不变) | ✅ **809012 拒绝,零写入** |
| 不传 `kind` | 一条活跃行都没有 | 582031,零写入 | 582031,零写入(未变) |
| 传 `kind=TRAVEL` | 任意 | TRAVEL 行 | TRAVEL 行(未变) |
| 传 `kind=TRANSFER` | 任意 | TRANSFER 行 | TRANSFER 行(未变) |
写入内容本身未变:
- **dispatch**:`order_vehicle_requirement` 该行 `status` `PENDING_REVIEW → PENDING`、写 `dispatch_remark`(≤500)、车控置 `PENDING`、CAS 退流程。
- **reject**:该行 `status → REJECTED_TO_CONSULTANT`、写 `return_remark`(≤500);同事务清团级 `requirement_confirmed`、写团级时间线 `BATCH_REQUIREMENT_REJECT`。
**失败零写入**:809012 抛在解析阶段(dispatch 里排在团期守卫之后、取行之前;reject 里排在占用探测之前),任何一条数据都不会落。
---
## 六、边界行为
- 未登录 → 401(网关拦截)。
- 权限点 `group-batch:demand:confirm` 缺失 → 403。
- 非团期子订单 → 582083(这道守卫排在 `kind` 解析之前,报错与改前逐字相同)。
- 需求状态不在允许的源状态集合 → 582083。
- 该户按解析出的类别取不到活跃行 → 582031。
- 解析到 TRANSFER 但服务日未回填 → 809007(dispatch 端点,失败关闭不放行)。
- `kind` 传非法值 → 809000(与改前一致,本次未改变非法值行为)。
- 老数据兼容:不改表、不迁移;存量订单下次调用时按新解析规则生效。
---
## 六.5、枚举 / 数据字典
### kind(用车需求类别)
**所属字段**: 两个端点的查询参数 `kind` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `TRAVEL` | 团期行程用车 | 汇总进团级乘车分组、整团逐日配车的那一类 |
| `TRANSFER` | 接送机 | 逐户派车的那一类;服务日由大交通派生,未回填时 dispatch 抛 809007 |
| (不传) | — | 🔴 **不再等价于 `TRAVEL`**。按该户活跃需求类别数解析:1 类用那一类 / 0 类抛 582031 / ≥2 类抛 809012 |
| 其他任意值 | — | 非法,抛 809000 |
---
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `kind`(两个端点的查询参数) | `@RequestParam(defaultValue = "TRAVEL")`,Swagger 标 `defaultValue=TRAVEL` | `@RequestParam(required = false)`,**无默认值**;Swagger 文案改为「不传按该户活跃需求类别自动解析,两类并存时必须显式指定」 |
| `DispatchReqVO.dispatchRemark` | 选填 ≤500 | 未变 |
| `RejectReqVO.returnRemark` | 必填 ≤500 | 未变 |
| 两个端点的响应 | `Result<Void>` | 未变 |
| 错误码集合 | 809000 / 809007(仅 dispatch)/ 582031 / 582083 | 🆕 **增加 809012** |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 不传 `kind` + 两类并存 | **静默改 TRAVEL 行,返 200**;操作者要动的 TRANSFER 行原封不动,响应上无任何信号 | 抛 **809012**,零写入 |
| 不传 `kind` + 只有 TRANSFER | 去找不存在的 TRAVEL 行 → 582031,**这类户走不通流程** | 解析成 TRANSFER,正常执行 |
| 不传 `kind` + 只有 TRAVEL | TRAVEL 行 | 未变 |
| 不传 `kind` + 一条活跃行都没有 | 582031 | 未变 |
| 传了 `kind`(合法或非法) | 原样使用 / 809000 | 未变 |
| 非团期子订单 | 582083 | 未变(守卫排在解析之前) |
| 房需求打回(`resourceType=HOTEL`) | `kind` 无意义 | 未变(不触发车侧解析,不会被两类并存误伤) |
---
## 六.7、影响评估
- **是否破坏向后兼容**: **是**。旧调用方「不传 `kind` = 按 TRAVEL」的隐含约定已失效;两类活跃需求并存的户上,原本返 200 的请求现在会返 809012。
- **前端是否必须同步上线**: **是(建议)**。不改也不会报错的前提是「该户只有一类活跃需求」,一旦出现两类并存就会撞 809012。改法极小:调用时把已知的类别放进 `kind` 查询参数,并接住 809012。
- **前端 workaround 清理点**: 若为绕开「纯接送机户点提交车务报 582031」做过按钮置灰、隐藏或提示,可以撤掉——该场景已修好。
---
## 七、不影响范围
- **仅影响**: `POST /v3/admin/order/{id}/vehicle-requirement/dispatch` 与 `POST /v3/admin/order/{id}/vehicle-requirement/reject` 两个端点的 `kind` 参数语义。
- **零影响**:
- 房需求(`resourceType=HOTEL`)的提交与打回链路
- 定制师侧提交 / 修改 / 调整用车需求 `PUT /v3/admin/order/{id}/vehicle-requirement`
- 团级正式用车需求的保存 / 汇总 / 预检 / 确认 / 撤回 / 免车
- 批量打回、整团确认的既有行为
- 结算侧手录车费的归属解析(809008,口径有意不同,本次未动)
- 车务侧(hl-fleet-service)配车、派单
- 历史数据:不改表、不迁移
---
## 八、测试环境已验证
- **代码事实**(对 `origin/dev-v3` 逐一查证):
- 合并提交 `a2ba628ecb`(PR #8610 squash 合并进 `dev-v3`),7 文件 / +491 −27。
- `VehicleRequirementAdminController` 两处 `@RequestParam(defaultValue = "TRAVEL")` → `@RequestParam(required = false)`,`@ApiParam` 文案同步改写,diff 已逐行核对。
- 新增 `VehicleRequirementKindErrorCode.VEHICLE_REQUIREMENT_KIND_REQUIRED = IErrorCode.of(809012, …)`,消息模板与占位符含义(`{0}`=订单 ID、`{1}`=两类名以 ` / ` 连接)已核对;同段既有码 809000/809001/809002/809007/809008/809009/809010/809011 未变。
- 新增私有方法 `RequirementService#resolveVehicleRequirementKind(Long, String)`,三条分支(非空白原样返回 / 0 类原样返回 / 1 类用那一类 / ≥2 类抛 809012)逐行核对;dispatch 里的调用点排在团期守卫之后、取行之前,reject 里排在 `probeGroupAdminReject` 之前且仅对 `resourceType=VEHICLE` 生效。
- 回归覆盖:`RequirementServiceTest` +301 行、`VehicleRequirementAdminControllerTest` +72 行、`VehicleRequirementKindErrorCodeMessageTest` +22 行(含 809012 报文渲染断言)。
- **部署**:`hl-order-service-v3` 的 `dev-v3` 分支已滚到测试服,两个端点走管理端网关 `/v3/admin/order/**` 既有路由,无新增路由。
```
POST /v3/admin/order/{id}/vehicle-requirement/dispatch?kind=TRANSFER → 200 ✓
POST /v3/admin/order/{id}/vehicle-requirement/reject?kind=TRAVEL → 200 ✓
POST /v3/admin/order/{id}/vehicle-requirement/dispatch(两类并存不传 kind) → 809012 ✓
POST /v3/admin/order/{id}/vehicle-requirement/dispatch(纯接送机户不传 kind) → 200 ✓(改前 582031)
```
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| — | #7210 | 逐单打回源状态放宽到 PENDING,接团期权限守卫 | ✅ 有效 |
| — | #7439 | 用车需求按 kind 分家,两个端点加 `kind` 参数(当时带默认值 TRAVEL) | ⚠️ 默认值部分已被本单撤销 |
| — | #8435 | 团期订单接送变更走团期放行(809011) | ✅ 有效 |
| **本 PR #8610** | **#8601** | `kind` 取消默认值,空值按活跃类别数分流,新增 809012 | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#8601](https://git.1814.love:8443/wx/HL/issues/8601)
- 关联 PR: [wx/HL#8610](https://git.1814.love:8443/wx/HL/pulls/8610)
## 关联 / 联系人
### 链接
- **Issue**: [#8601](https://git.1814.love:8443/wx/HL/issues/8601)
- **PR**: [#8610](https://git.1814.love:8443/wx/HL/pulls/8610)
- **Merge commit**: [a2ba628ecb](https://git.1814.love:8443/wx/HL/commit/a2ba628ecb)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,430 @@
---
schema: "hl-changelog/v2"
ticket: "8603"
title: "派单原子确认响应删除恒为空的 missingPickupDates / missingDropoffDates,缺失日期只由 605914/605915 错误消息承载"
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: "PR #8608 已 squash 合并 dev-v3(b4eb919b47),hl-fleet-service dev-v3 分支已滚测试服。删除的两个字段是 #8579 加的、在本端点上恒为空数组;接送机缺口在本端点是硬门禁,日期写在 605914/605915 的错误消息里。真正带「已落库但还差几天」中间态的是 POST /admin/fleet/assignments/batch 的 pickupDropoffGate 对象,该对象未动。【同名字段两个载体,勿按字段名 grep 判断影响面,2026-09-30 查证】hl-ui origin/v2.1 的 src/views/fleet/board/components/Step3PickupDropoff.vue:178-179 确实渲染 missingPickupDates / missingDropoffDates(「待配置接机 / 送机」两行),但它取的是 props.gate,而 gate 由 AssignModal.vue:1626 与 OrderDrawer.vue:803 从 props.order.pickupDropoffGate 派生—— 即上面那个未动的对象,与本次删字段的 ConfirmRequirementRespVO 不是同一个载体。本端点(POST /admin/fleet/assignments/requirements/{id}/confirm,前端出口 src/api/fleet/board.js:162)的响应上,这两个键没有任何读取点,故 not_required 成立。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# hl-fleet-service: 派单原子确认响应删除恒为空的接送机缺失日期字段
> **存放目录**: `changelogs-v2/2026-09/`
> **服务**: hl-fleet-service (端口 8089)
> **PR**: #8608
> **Issue**: #8603
> **日期**: 2026-09-30
> **影响范围**: 车务四步向导第③步「按需求整组原子确认」的响应体
---
## ⚠️ 关键变化
- 🔴 **`ConfirmRequirementRespVO` 删除两个字段**:`missingPickupDates`、`missingDropoffDates`。它们是 #8579 加进来的,在**本端点上恒为空数组**。
- **为什么恒空**:本端点的接送机门禁是**硬门禁**——有缺口一定在写入之前抛 605914 / 605915,缺哪几天以 `yyyy-MM-dd` 逗号分隔原样写在错误消息里。**能拿到 200 响应,就说明门禁已经通过了**,此时「缺失日期」这个概念在本端点上不存在。
- 🔴 **这两个字段的存在制造了一个不存在的中间态**:前端若按 `finalPlanPublished=false && missingPickupDates.length>0` 去渲染「确认成功但还差 N 天」,这个分支**永远不会成立**——本端点没有这种中间态。
- ✅ **「已落库但还差几天」这个中间态确实存在,但它在另一个端点上**:`POST /admin/fleet/assignments/batch`(批量创建派单)的响应里,字段挂在 **`pickupDropoffGate` 对象**下(`arrivalRequiredDates` / `departureRequiredDates` / `missingPickupDates` / `missingDropoffDates` / `declared` / `satisfied`)。**该对象本次未动,全部字段照旧**。要做「还差哪几天」的提示,读那里。
- **前端要做的事**:把本端点响应里对 `missingPickupDates` / `missingDropoffDates` 的读取删掉,改成**捕获 605914 / 605915 并把错误消息里的日期展示给用户**;若已有「差 N 天」的提示 UI,把它的数据源指向 `POST /batch` 的 `pickupDropoffGate`。
- **其余字段全部未变**:`requirementId`、`dispatchPlanGeneration`、`confirmed`、`finalPlanPublished`、`finalPlanNotPublishedReason`、`groups` 及其内部结构逐字段不变;请求体完全未变。
---
## 一、背景
车务四步向导第③步是「按当前派车方案代际原子确认全部执行段」。接送机门禁在这条路径上有**两个不同位置**的判定,二者的失败表现完全不同:
| 位置 | 时机 | 门禁不满足时 |
|------|------|--------------|
| `assertPickupDropoffCoverage` | **确认动作开始之前**(硬门禁) | 抛 605914 / 605915,**整笔不执行**,缺失日期在错误消息里 |
| 最终方案发布漏斗 | 确认已成功、准备发布 finalPlan 时 | 确认仍算成功,`finalPlanPublished=false` + `finalPlanNotPublishedReason` 给原因 |
#8579 把 `missingPickupDates` / `missingDropoffDates` 加进响应,想表达的是第二个位置的「还差几天」。但**第一个位置排在前面且是硬门禁**:门禁开启且真有缺口时,请求在第一个位置就被拦掉了,根本走不到组装响应那一步;门禁关闭时则两处都不判缺口。两条路都不会产出非空的缺失日期列表,于是这两个字段在本端点上**结构性恒为空数组**——它们不是"通常为空",是**没有任何取值路径能让它们非空**。
真正存在该中间态的是批量提交端点:那里"写入成功"与"门禁满足"确实是两件独立的事,所以 `BatchAssignmentWriteRespVO.pickupDropoffGate` 里的六个字段有实际取值。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 按当前派车方案代际原子确认全部执行段 | POST | `/admin/fleet/assignments/requirements/{requirementId}/confirm` | 响应删除字段 | 删除恒为空的 `missingPickupDates` / `missingDropoffDates`;缺口由 605914/605915 承载 |
---
## 三、接口详情
### 1. 按当前派车方案代际原子确认全部执行段 `POST /admin/fleet/assignments/requirements/{requirementId}/confirm`
**VO**: `ConfirmRequirementReqVO → ConfirmRequirementRespVO`
#### 使用场景
车务四步向导第③步的整组原子确认入口。请求必须**精确列出**当前最终方案的全部有效派车组及各组是否发行程短信;服务端按 `expectedPlanGeneration` 锁定并重读完整方案,重跑最终确认基线 + 行程短信决策一致性校验 + 接送机门禁,通过后重发最终方案快照。任一组缺失、过期或通知歧义则**整笔回滚**,不产生部分 assigned、不产生部分副作用。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `requirementId` | Path | Long | ✅ | - | 当前用车需求 ID |
| `orderId` | Body | Long | ✅ | `@NotNull` | 订单 ID;为空返 400「订单ID不能为空」 |
| `requestId` | Body | String | ✅ | `@NotBlank`,`@Size(max=64)` | 幂等请求标识;同一 `requestId` 用于**不同**确认内容时返 605059 |
| `expectedRequirementVersion` | Body | Integer | ✅ | `@NotNull` | 预期当前有效用车需求版本,取自 Board 读口 |
| `expectedRequirementSha256` | Body | String | ✅ | `@NotBlank`,`@Pattern("^[0-9a-f]{64}$")` | Board 返回的当前用车需求 canonical SHA-256;必须是**小写**十六进制 64 位,否则返 400 |
| `expectedPlanGeneration` | Body | Long | ✅ | `@NotNull` | 预期当前最终派车方案代际 |
| `groups` | Body | Array | ✅ | `@NotEmpty`,`@Size(max=50)` | 当前有效执行段的**精确集合**;超 50 个返 400「单次确认执行段不能超过50个」 |
| `groups[].assignmentGroupId` | Body | Long | ✅ | `@NotNull` | 当前有效派车组 ID |
| `groups[].sendItinerarySms` | Body | Boolean | ✅ | `@NotNull` | 是否向本执行段司机发送行程短信;为空返 400「请选择是否向本段司机发送行程短信」 |
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| `requirementId` | String | 用车需求 ID(雪花 ID,JSON 中为字符串) |
| `dispatchPlanGeneration` | String | 已确认的最终派车方案代际(JSON 中为字符串) |
| `confirmed` | Boolean | 整组是否原子确认成功;返 200 时恒为 `true` |
| `finalPlanPublished` | Boolean | 本次是否真的发布了最终方案快照。`false` 表示确认已成功但订单车控仍处理中(方案未派满等) |
| `finalPlanNotPublishedReason` | String | 最终方案未发布的原因;已发布时为 `null`。取值见「六.5、枚举 / 数据字典」 |
| ~~`missingPickupDates`~~ | ~~Array~~ | 🔴 **本次删除**(#8579 加入,在本端点恒为空数组)。缺失接机日改由 605914 的错误消息承载 |
| ~~`missingDropoffDates`~~ | ~~Array~~ | 🔴 **本次删除**(同上)。缺失送机日改由 605915 的错误消息承载 |
| `groups` | Array | 各执行段确认结果 |
| `groups[].assignmentId` | String | 代表派单 ID(雪花 ID,JSON 中为字符串) |
| `groups[].assignmentGroupId` | String | 派车组 ID;历史行无该 ID 时回退下发 `assignmentId`,对任何真实行恒非空 |
| `groups[].assignmentStatus` | String | 派单状态 |
| `groups[].confirmedAt` | String | 车务最终确认时间(`yyyy-MM-dd HH:mm:ss`) |
| `groups[].sendItinerarySms` | Boolean | 本段是否选择了发送行程短信(回显请求中的选择) |
| `groups[].itinerarySmsEventId` | String | 行程短信 Outbox 事件 ID;**未发送时为 `null`** |
| `groups[].itinerarySmsStatus` | String | 行程短信状态,取值见「六.5、枚举 / 数据字典」 |
| `groups[].itineraryUrl` | String | 本段电子行程单 H5 链接;本端点组装时**恒为 `null`**,签发链接请走行程短信状态查询端点 |
#### 请求示例
```json
{
"orderId": "2099459272533323777",
"requestId": "confirm-2099459272533323777-20260930-01",
"expectedRequirementVersion": 3,
"expectedRequirementSha256": "9f2c4e1ab7d05836c41fbe2907a5d4638e1c0b7a53d92f8146ce70bb2d5a3ff4",
"expectedPlanGeneration": "12",
"groups": [
{ "assignmentGroupId": "2099461003812864001", "sendItinerarySms": true },
{ "assignmentGroupId": "2099461003812864002", "sendItinerarySms": false }
]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"requirementId": "2099460881234567890",
"dispatchPlanGeneration": "12",
"confirmed": true,
"finalPlanPublished": true,
"finalPlanNotPublishedReason": null,
"groups": [
{
"assignmentId": "2099461003812864001",
"assignmentGroupId": "2099461003812864001",
"assignmentStatus": "assigned",
"confirmedAt": "2026-09-30 10:12:33",
"sendItinerarySms": true,
"itinerarySmsEventId": "2099461099887766554",
"itinerarySmsStatus": "PENDING",
"itineraryUrl": null
},
{
"assignmentId": "2099461003812864002",
"assignmentGroupId": "2099461003812864002",
"assignmentStatus": "assigned",
"confirmedAt": "2026-09-30 10:12:33",
"sendItinerarySms": false,
"itinerarySmsEventId": null,
"itinerarySmsStatus": "NOT_SENT",
"itineraryUrl": null
}
]
}
}
```
**注意响应里没有 `missingPickupDates` / `missingDropoffDates` 两个键**——不是值为空数组,是**键本身不存在**。
#### 空数据 / 降级响应
「确认成功但最终方案未发布」是本端点唯一的部分成功形态:`confirmed=true` + `finalPlanPublished=false` + `finalPlanNotPublishedReason` 给出原因。此时**没有缺失日期可读**:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"requirementId": "2099460881234567890",
"dispatchPlanGeneration": "12",
"confirmed": true,
"finalPlanPublished": false,
"finalPlanNotPublishedReason": "PLAN_INCOMPLETE",
"groups": [
{
"assignmentId": "2099461003812864001",
"assignmentGroupId": "2099461003812864001",
"assignmentStatus": "assigned",
"confirmedAt": "2026-09-30 10:12:33",
"sendItinerarySms": false,
"itinerarySmsEventId": null,
"itinerarySmsStatus": "NOT_SENT",
"itineraryUrl": null
}
]
}
}
```
`groups` 恒非空(请求 `@NotEmpty` 保证至少一段,且每段都要有结果)。幂等重放命中已成功回执时返回**与首次逐字段相同**的结果,含冻结在回执里的 `finalPlanPublished` 与 `finalPlanNotPublishedReason`。
#### 错误响应
接送机缺口的唯一载体(`{0}` = 缺失日期,`yyyy-MM-dd` 逗号分隔、升序):
```json
{
"code": 605914,
"message": "大交通要求接机,以下日期未配置接机车辆:2026-10-08,2026-10-09",
"success": false,
"data": null
}
```
```json
{
"code": 605915,
"message": "大交通要求送机,以下日期未配置送机车辆:2026-10-12",
"success": false,
"data": null
}
```
其余错误码(本次未变):
| 码 | 报文 | 触发 / 处置 |
|----|------|-------------|
| 605062 | `派车日期 {0} 越出当前{1}日期窗(版本 v{2},窗内服务日 {3}):请先调整或取消这些越窗槽位,或让定制师重新提交{1}换版后再派车` | 存在越窗在途槽位;须先调整或取消 |
| 605037 | `车辆处于维保或停用状态,不能派车:{0}` | 先改派换车 |
| 605038 | `司机处于休假或待激活状态,不能派车:{0}` | 先改派换司机 |
| 605059 | `幂等请求标识已用于不同确认内容` | 同一 `requestId` 配了不同载荷;**换新 `requestId` 重试** |
| 605063 | `原子确认回执已损坏,无法幂等重放,请联系管理员` | 🔴 **不可自愈终态**,重试同一 `requestId` 永远同码;前端**不得自动重试、不得静默轮询**,须直接提示用户联系管理员 |
#### 业务边界
- **鉴权**:`AssignmentController` 未挂方法级权限注解,只有网关登录态校验;未登录返 401。
- 🔴 **缺失日期只存在于错误消息里**:本端点拿到 200 就代表接送机门禁已通过,不要在响应体里找缺口字段。
- 🔴 **「还差几天」的中间态在 `POST /admin/fleet/assignments/batch`**:读其响应的 `pickupDropoffGate` 对象(含 `arrivalRequiredDates` / `departureRequiredDates` / `missingPickupDates` / `missingDropoffDates` / `declared` / `satisfied`),该对象本次未动。
- **门禁开关关闭时不判缺口**:接送机门禁受服务端配置开关控制;关闭时硬门禁直接放行、发布漏斗也不判门禁,所以既不会抛 605914/605915,也不会因接送机原因压住发布。这是服务端配置项,**不是请求参数,前端无法也无需感知**。
- **`GATE_UNSATISFIED` 在本端点上几乎不可达**:门禁开启且有缺口时请求在硬门禁处就被拦成 605914/605915;门禁关闭时不判。它只剩「需求身份不全」的兜底分支,而那条分支按源码注释本来就**没有任何缺失日期可言**。
- **`groups` 必须是精确集合**:少给一组、多给一组、或组已过期,整笔回滚返错,不会部分生效。
- **幂等**:以 `requestId` 为键;重放已成功的回执返回同一份结果(含冻结的发布结论),载荷变了返 605059。
- **`itineraryUrl` 在本响应中恒为 `null`**:行程单链接由行程短信状态查询端点下发。
- **失败零副作用**:所有门禁与基线校验都排在写入之前,报错时不产生部分 assigned、不产生短信事件。
---
## 四、契约约束与正确调用方式
> 本节只写后端接受 / 拒绝请求的规则与响应字段的正确读法,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误 读法对照
| 目的 | ✅ 正确做法 | ❌ 错误做法 |
|------|-------------|-------------|
| 判断「接送机缺哪几天」 | 捕获 605914 / 605915,从 `message` 里取日期(`yyyy-MM-dd` 逗号分隔) | 读本端点响应的 `missingPickupDates` / `missingDropoffDates`——**这两个键已不存在** |
| 渲染「已落库但还差 N 天」 | 读 `POST /admin/fleet/assignments/batch` 响应的 `data.pickupDropoffGate.missingPickupDates` / `.missingDropoffDates` | 在本端点响应上拼这个中间态——本端点没有该中间态 |
| 判断「确认成功了吗」 | 看 HTTP 层 `code=200` + `data.confirmed` | 看 `finalPlanPublished`——它答的是另一个问题(方案有没有发布) |
| 判断「最终方案发出去了吗」 | `data.finalPlanPublished`;为 `false` 时读 `finalPlanNotPublishedReason` | 假定 `confirmed=true` 就等于已发布 |
| 撞到 605063 | 停止重试,提示用户联系管理员 | 自动重试 / 静默轮询——同一 `requestId` 永远返同码 |
| 撞到 605059 | **换一个新的 `requestId`** 重发 | 用同一个 `requestId` 重试 |
### 前端必须做的改动
1. **删掉对本端点响应 `missingPickupDates` / `missingDropoffDates` 的一切读取**(含可选链兜底、空数组判断、TS 类型定义)。
2. **接送机缺口提示改走 605914 / 605915 的错误消息**,日期在 `message` 里逐字给出。
3. 若页面上有「已落库但还差几天」的提示块,**把它的数据源改指向 `POST /admin/fleet/assignments/batch` 的 `pickupDropoffGate`**。
---
## 五、数据库行为
**本次改动不涉及任何数据库变更**:无建表、无加列、无改列、无数据迁移、无 Flyway 脚本。
端点自身的写入行为(本次未变):
| 动作 | 写入 |
|------|------|
| 整组原子确认 | 各执行段派单行 `assignment_status → assigned`、写 `confirmed_at` |
| 行程短信 | `sendItinerarySms=true` 的段写一条短信 Outbox 事件,`itinerary_sms_event_id` 回填到派单行 |
| 幂等回执 | 落一条确认回执,冻结本次结果(含 `finalPlanPublished` 与 `finalPlanNotPublishedReason`)供重放 |
| 最终方案快照 | 发布判据全部通过时冻结一次 DAILY_V3 finalPlan,由 order-v3 消费后把车控状态推进 |
**失败零写入**:接送机硬门禁、越窗门禁、基线校验全部排在写入之前;任一失败整事务回滚。
---
## 六、边界行为
- 未登录 → 401(网关拦截)。
- 请求体字段缺失 / 格式不符(`expectedRequirementSha256` 不是小写 64 位十六进制、`groups` 为空、超 50 段等)→ 400,消息即上表「约束」列所写的校验文案。
- 接送机门禁开启且缺接机日 → 605914,日期在消息里,零写入。
- 接送机门禁开启且缺送机日 → 605915,日期在消息里,零写入(接机缺口先判,两者都缺时先报 605914)。
- 接送机门禁关闭 → 不判缺口,既不抛 605914/605915,也不因接送机压住发布。
- 存在越窗在途槽位 → 605062,零写入。
- 车辆维保/停用、司机休假/待激活 → 605037 / 605038,零写入。
- 同一 `requestId` 配不同载荷 → 605059;换新 `requestId` 即可。
- 回执损坏 → 605063,不可自愈终态。
- 幂等重放命中成功回执 → 200,返回与首次逐字段相同的结果。
- 确认成功但方案未发布 → 200 + `confirmed=true` + `finalPlanPublished=false` + `finalPlanNotPublishedReason`,**此时无缺失日期可读**。
---
## 六.5、枚举 / 数据字典
### finalPlanNotPublishedReason(最终方案未发布原因)
**所属字段**: `data.finalPlanNotPublishedReason` | **类型**: `String` | 已发布时为 `null`
判据按固定顺序执行,**只回第一个没通过的原因**:
| 顺序 | 值 | 含义 |
|------|----|------|
| 1 | `STALE_FINALIZED_PLAN` | 存在按旧需求定稿的陈旧行,需车务对当前需求重新确认 |
| 2 | `INVALID_PLAN_GENERATION` | 当前生效行的方案代际不一致(部分已定稿、部分未定稿或代际不同) |
| 3 | `PLAN_INCOMPLETE` | 满派拓扑不完整:有逻辑 key 没派车、缺司机、在途行越窗、同 key 多行等 |
| 4 | `CAPACITY_INSUFFICIENT` | 未定稿分支上当日载客量不足以覆盖需求人数 |
| 5 | `GATE_UNSATISFIED` | 大交通要求的接/送机日没有配车。🔴 **在本端点上几乎不可达**(有缺口时硬门禁先抛 605914/605915) |
> 另有 `NO_GATE_TRANSITION` 与 `PICKUP_DROPOFF_GATE_DISABLED` 两个值,**只在接送机配置端点出现**,本端点不会返回。
### itinerarySmsStatus(行程短信状态)
**所属字段**: `data.groups[].itinerarySmsStatus` | **类型**: `String`
| 值 | 含义 |
|----|------|
| `NOT_SENT` | 本段未选择发送,或历史行没有短信事件 |
| `PENDING` | 本次已产生短信 Outbox 事件,投递中 |
| `SENT` | 短信已发出(出现在已确认段的重放回显里) |
---
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `data.missingPickupDates` | `Array<String>`,**恒为空数组 `[]`** | 🔴 **键已删除,响应中不存在** |
| `data.missingDropoffDates` | `Array<String>`,**恒为空数组 `[]`** | 🔴 **键已删除,响应中不存在** |
| `data.requirementId` | String | 未变 |
| `data.dispatchPlanGeneration` | String | 未变 |
| `data.confirmed` | Boolean | 未变 |
| `data.finalPlanPublished` | Boolean | 未变 |
| `data.finalPlanNotPublishedReason` | String / null | 未变 |
| `data.groups[*]` 全部字段 | 8 个字段 | 未变 |
| 请求体全部字段 | — | 未变 |
| 错误码集合 | 605062 / 605914 / 605915 / 605037 / 605038 / 605059 / 605063 | 未变 |
| `BatchAssignmentWriteRespVO.pickupDropoffGate` | 6 个字段 | **未变**(缺失日期的正确来源) |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 接送机有缺口 + 门禁开启 | 抛 605914/605915(响应根本到不了组装步) | 未变 |
| 接送机门禁通过、拿到 200 | 响应带两个**恒为空**的日期数组 | 响应**不含**这两个键 |
| 前端按 `missingPickupDates.length > 0` 判缺口 | 永远为 `false`,分支不可达 | 该字段不存在;改捕获 605914/605915 |
| 「已落库但还差几天」的读法 | 本端点读不到(恒空),实际在 `POST /batch` | 未变,仍在 `POST /batch` 的 `pickupDropoffGate` |
---
## 六.7、影响评估
- **是否破坏向后兼容**: **是(响应删字段)**。但删的是**在本端点恒为空数组**的两个字段,任何依赖它们做判断的前端分支在改前也永远不成立——即行为上前端看不到差异,看得到差异的是**读取代码本身**(可选链失效 / TS 类型不匹配 / 空数组默认值)。
- **前端是否必须同步上线**: **建议同步**。JS 里读不存在的键得 `undefined`,若代码写的是 `resp.data.missingPickupDates.length` 会抛 TypeError;写成 `?.length` 或有默认值则不报错。TS 侧需删掉类型声明里的这两个字段。
- **前端 workaround 清理点**: 如果曾为「这两个字段总是空」做过兜底(写死不展示、或转去读别的来源),可以连同兜底一起清掉,直接按 605914/605915 + `POST /batch` 的 `pickupDropoffGate` 这两条正路走。
- **联调注意**: 缺口提示的数据源从此分两处——**硬门禁报错**(本端点,错误消息)与**中间态展示**(`POST /batch`,`pickupDropoffGate` 对象),不要把两者混为一处。
---
## 七、不影响范围
- **仅影响**: `POST /admin/fleet/assignments/requirements/{requirementId}/confirm` 的响应体字段集合。
- **零影响**:
- `POST /admin/fleet/assignments/batch` 及其 `pickupDropoffGate` 对象(六个字段全部保留,取值逻辑未动)
- 接送机配置端点 `POST /admin/fleet/assignments/requirements/{requirementId}/pickup-dropoff`(含它专属的 `NO_GATE_TRANSITION` / `PICKUP_DROPOFF_GATE_DISABLED` 两个原因值)
- 派单创建 / 修改 / 取消 / 软清 / 一键重派推荐 / 候选查询 / 预校验
- 行程短信状态查询与受控重发
- 接送机门禁自身的判定逻辑与开关语义(**只删了响应回显,门禁一步没动**)
- 最终方案发布漏斗与 order-v3 的车控状态推进
- 数据库:无表结构或数据变更
---
## 八、测试环境已验证
- **代码事实**(对 `origin/dev-v3` 逐一查证):
- 合并提交 `b4eb919b47`(PR #8608 squash 合并进 `dev-v3`),8 文件 / +119 −55。
- `ConfirmRequirementRespVO` 当前字段集已逐字段核对:`requirementId` / `dispatchPlanGeneration` / `confirmed` / `finalPlanPublished` / `finalPlanNotPublishedReason` / `groups`,两个日期字段处留有说明注释、字段已删。
- `AssignmentController` 的 `@ApiOperation(notes=…)` 新增 6 行说明,逐行核对:缺失日期载体是错误码、`yyyy-MM-dd` 逗号分隔、中间态在 `POST /batch` 的 `pickupDropoffGate`。
- `assertPickupDropoffCoverage` 两条抛错分支(605914 接机、605915 送机,日期以 `,` join)与开关关闭时的早返回逐行核对;确认主流程里该硬门禁排在回执重放与任何写入之前。
- `BatchAssignmentWriteRespVO.pickupDropoffGate` 与 `PickupDropoffGateVO` 六字段在 `dev-v3` 上原样存在,本提交未触及这两个文件。
- `FinalPlanNotPublishedReasons` 七个常量与两份 Swagger 说明文本已核对:本端点用的是只含五个取值的通用说明。
- 回归覆盖:`AssignmentControllerTest` 断言 `$.data.missingPickupDates` / `$.data.missingDropoffDates` **不存在**;`AssignmentServicePickupDropoffTest` 新增「门禁显式开启且有缺口时抛错且日期在消息里」用例;`RequirementConfirmationReceiptServiceTest` 新增回执往返用例。
- **部署**:`hl-fleet-service` 的 `dev-v3` 分支已滚到测试服,端点走管理端网关 `/admin/fleet/**` 既有路由,无新增路由。
```
POST /admin/fleet/assignments/requirements/{requirementId}/confirm → 200,响应无 missingPickupDates / missingDropoffDates 两键 ✓
POST /admin/fleet/assignments/requirements/{requirementId}/confirm(缺接机日) → 605914,日期在 message ✓
POST /admin/fleet/assignments/batch → 200,data.pickupDropoffGate 六字段照旧 ✓
```
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| — | #7067 | 接送机门禁落地:605914/605915 + 最终方案发布漏斗 | ✅ 有效 |
| — | #8429 | 未发布原因 `finalPlanNotPublishedReason` 进响应 | ✅ 有效 |
| — | #8579 | 给确认响应加 `missingPickupDates` / `missingDropoffDates` | ❌ **已被本单撤销**(在本端点恒为空) |
| **本 PR #8608** | **#8603** | 删除上述两个恒空字段,缺口归错误码承载 | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#8603](https://git.1814.love:8443/wx/HL/issues/8603)
- 关联 PR: [wx/HL#8608](https://git.1814.love:8443/wx/HL/pulls/8608)
## 关联 / 联系人
### 链接
- **Issue**: [#8603](https://git.1814.love:8443/wx/HL/issues/8603)
- **PR**: [#8608](https://git.1814.love:8443/wx/HL/pulls/8608)
- **Merge commit**: [b4eb919b47](https://git.1814.love:8443/wx/HL/commit/b4eb919b47)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,140 @@
---
schema: "hl-changelog/v2"
ticket: "8613"
title: "605072 恢复动作文案订正 + 605002 座位强禁码下线口径澄清(本条目同时覆盖 #8614)"
consumer: "admin"
author: "wx(GIT)"
change_type: "修复"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "本条目合并覆盖 #8613 与 #8614(同一 PR #8616 一并修复,两者均为文案订正,未新增/删除/变更任何字段或路径)。#8613:605002「车型座位不足」自 #5810 起已是死码,create/change 均不再做座位强禁校验,全仓(含测试)零产出方;POST /admin/fleet/assignments 的 headcount、strictSeats 两个字段的 Swagger 文案已订正为如实描述——headcount 仅落库记录与下游统计,不再用于任何服务端座位判定;strictSeats 是历史兼容字段,服务端完全忽略,传 true 不会触发 605002,新代码不要依赖它做分支。座位不足的非阻断提示只在 POST /admin/fleet/assignments/precheck 以 warning(type=seats_short)形式给出,precheck 本身零变化。#8614:605072 错误码数值不变(仍是 605072),message 文案从「请先释放资源再处置完成」订正为「请对本单重新执行一次「一键清除已取消派单的占用」后再处置完成」——即撞上 605072 时正确的前端引导是让车务重新调用 POST /admin/fleet/assignments/clear-cancelled-occupancy,而不是原地反复重试 POST /admin/fleet/assignments/resolve-exception;服务端拦回的同时已在独立事务补写一次占用反算意图,按提示重新执行清除占用后再重试 resolve-exception 通常可以收敛。backend_status=deployed:hl-fleet-service 已部署测试服并与本条目所依据的源码提交一致(deploy-status.sh 实测 COMMIT=d57498d38,STATE=ok,2026-09-30 05:34:07 部署);resolve-exception 路由已实测可达(未登录态返 200 信封 code=401)。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# 车务派单:605072 恢复动作文案订正 + 605002 座位强禁码下线口径澄清
> **存放目录**: 二期(order-v3/fleet)→ `changelogs-v2/2026-09/`
>
> **服务**: hl-fleet-service(端口 8087)
> **PR**: [#8616](https://git.1814.love:8443/wx/HL/pulls/8616)
> **Issue**: [#8613](https://git.1814.love:8443/wx/HL/issues/8613)、[#8614](https://git.1814.love:8443/wx/HL/issues/8614)
> **日期**: 2026-09-30
> **性质**: 两处均为 Swagger 描述文案 / 错误消息文案订正,**接口路径、方法、请求参数、响应字段结构、错误码数值均零变更**——不触发接口契约模板,本文档按「修复」类轻量格式书写。
---
## ⚠️ 关键变化
1. **605072(异常派单占用未释放)的错误消息文案变了,正确的恢复动作也变了**:旧文案「请先释放资源再处置完成」不点名具体动作,车务只能反复点「处置完成」(`resolve-exception`)本身,而这在「一车/一司机被多张单的异常行共用、逐单释放」的场景下会卡死——即便实际占用已经释放,缓存态仍可能停在旧读数上。新文案明确指向唯一有效的恢复动作:**重新调用一次「一键清除已取消派单的占用」(`clear-cancelled-occupancy`)**,再重试处置完成。**若前端在 605072 分支里硬编码过旧文案、或只做了「提示后原地重试」的处理,需要改成引导用户重新执行清除占用。**
2. **605002(车型座位不足)确认为死码,不会再从 create/change 派单接口抛出**:这不是本次改的行为,而是订正一处此前不准确的文档描述——该码自 #5810 起已无任何产出方。若前端此前在派车弹窗里对 605002 做过专门的错误分支处理,那段代码从未被触发过、以后也不会。座位不足唯一的提示渠道是 `precheck` 预检接口的非阻断 warning(`type=seats_short`),该渠道本身没有变化。
---
## 二、涉及接口(非接口契约变更,仅列出文案改动落点,供联调核对)
| 接口 | 方法 | 路径 | 改动内容 |
|------|------|------|----------|
| 异常派单处置完成 | POST | `/admin/fleet/assignments/resolve-exception` | 605072 错误消息文案改写(#8614) |
| 创建派单 | POST | `/admin/fleet/assignments` | `headcount`/`strictSeats` 两字段 Swagger 描述文案订正(#8613),字段本身未增删未改类型 |
---
## 三、逐项说明
### 1. `POST /admin/fleet/assignments/resolve-exception`(#8614)
**背景**:该接口把订单下全部 `exception` 状态派单行推进到 `completed`,前置条件是这些行的车辆/司机占用已经释放。占用是否释放,判定依据是车辆/司机的**缓存状态列**(`vehicle_status`/`driver_status`)是否为 `busy`。这个缓存列只在特定写口(如 `clear-cancelled-occupancy`)被触发时才会反算刷新。
**问题场景**:同一车辆或司机被多张单各自的 `exception` 行共用时,逐单释放会出现死局——释放 A 单时缓存态被判 `busy`(当时正确);随后处置完 A 单,B 单这边再没有任何写口触发反算 ⇒ 缓存态永远停在旧的 `busy`,即便实际已经没有在途占用支撑,B 单调用 `resolve-exception` 也会永远撞 605072。
**本次改动**:
- 错误消息文案(605072 数值不变):
| | 内容 |
|---|---|
| 旧 | `异常派单的车辆/司机占用尚未释放,请先释放资源再处置完成` |
| 新 | `异常派单的车辆/司机占用尚未释放,请对本单重新执行一次「一键清除已取消派单的占用」后再处置完成` |
- 服务端在拦回抛出 605072 的同时,已在独立事务里补写一次「按当前在途口径」的占用反算意图(不影响本次请求仍会失败,是为下一次重试铺路)。
- Swagger `@ApiOperation` 说明文本同步更新,明确写出 605072 的恢复动作。
**对前端的影响**:撞上 605072 时,正确引导是提示用户重新调用 `POST /admin/fleet/assignments/clear-cancelled-occupancy`(该接口路径/参数/行为本身未变),再重试 `resolve-exception`;不建议做「原地无限重试 resolve-exception」的兜底逻辑,因为缓存态陈旧这种情形下光重试 `resolve-exception` 本身不会让状态收敛(要靠 `clear-cancelled-occupancy` 触发反算)。若之前的前端文案直接透传了服务端 message 字符串,会自动拿到新文案,无需改代码;若前端针对 605072 有自己的本地化文案覆盖了服务端 message,建议同步这句新的恢复动作提示。
### 2. `POST /admin/fleet/assignments`(#8613)
**背景**:该接口的 `CreateAssignmentReqVO` 里有 `headcount`(人数)和 `strictSeats`(座位严格模式)两个历史字段。#5810 起,车型/座位差异已经不再阻断派车(`create`/`change` 均不做座位强禁校验),但这两个字段的 Swagger 描述当时没有同步更新,仍然写着「座位不足判定用」「true=座位不足强禁抛605002」,与实际行为不符。
**本次改动(仅 `@ApiModelProperty` 描述文案,字段名/类型/是否必填均未变)**:
| 字段 | 旧描述 | 新描述 |
|------|--------|--------|
| `headcount` | `人数(座位不足判定用,可空时不判座位)` | `人数(仅落库记录与下游统计;#5810 起 create 不做任何座位校验,车辆座位少于人数也照常派车、不会返回 605002。座位不足的非阻断提示只在 precheck 预检端点以 warning(seats_short) 形式返回,create 侧不产出该提示;本字段可空)` |
| `strictSeats` | (Java 层注释,非 Swagger 描述)`座位严格模式:true=座位不足强禁抛 605002 / false=仅 warning 不阻断(默认 false)` | `历史兼容字段,#5810 起服务端完全忽略:座位差异不再阻断派车,传 true 也不会抛 605002。全仓无读取方,仅装配侧恒写 false 以保持 BO 形状;新代码不要依赖本字段做任何分支` |
**对前端的影响**:
- 如果前端此前依赖「create 接口会因座位不足报 605002」做过任何拦截逻辑(例如提交前弹确认框、或捕获 605002 单独处理),这段逻辑**从未生效过**——create/change 从 #5810 起就不做这个校验,以后也不会恢复(605002 码位保留但不会复用给别的语义)。
- `strictSeats` 传什么值都不影响服务端行为,前端无需继续维护/传递这个字段的真实语义(可以继续传,服务端只是忽略)。
- 座位不足的唯一提示渠道是 `precheck`(`POST /admin/fleet/assignments/precheck`)响应里的 warning 数组,`type=seats_short`——这个渠道本身没有任何变化,仍照旧使用。
---
## 四、契约约束与正确调用方式
- `resolve-exception` 撞 605072 后的正确恢复序列:`POST clear-cancelled-occupancy` → 重试 `POST resolve-exception`。中间不需要额外等待,服务端的补写反算意图是同步在拦回请求的事务外完成的。
- `create`(`POST /admin/fleet/assignments`)不会因为座位不足返回任何错误码;如需在提交前给用户座位不足提示,唯一正确渠道是先调用 `precheck` 读取 warning 数组。
---
## 六、边界行为
- `resolve-exception` 605072 之外的错误码(401 未登录、其它业务校验失败码)均未变化,本次不涉及。
- `create` 接口除 Swagger 描述文本外,请求校验、成功路径、其余错误码均未变化。
---
## 七、不影响范围
- `POST /admin/fleet/assignments/precheck` 的请求/响应结构与 `seats_short` warning 的产生条件——零变化。
- `POST /admin/fleet/assignments/clear-cancelled-occupancy` 的路径、参数、返回结构——零变化。
- 605002、605072 两个错误码的**数值**本身——均未变化(只是 605072 的 message 文案变了,605002 的可触发性说明被订正,数值都没动)。
- 除本文档列出的 2 处 `@ApiModelProperty`/错误消息字符串外,`CreateAssignmentReqVO`、`ResolveExceptionReqVO`、响应 VO 均无字段增删或类型变更。
---
## 八、测试环境已验证
- `deploy-status.sh`(测试服现状表)实测:`hl-fleet-service` COMMIT=`d57498d38`、STATE=ok,2026-09-30 05:34:07 部署——与本条目所依据的源码提交(含 #8613/#8614 合并提交 `2bf98beb491`)一致。
- 测试服内网 `curl` 实测 `resolve-exception` 路由已挂载且鉴权前置生效:
```
POST http://127.0.0.1:8080/admin/fleet/assignments/resolve-exception (无 Authorization 头)
→ HTTP 200
{"code":401,"message":"缺少有效的 Authorization 头","data":null,"traceId":"f8da1e80a5e1400b","success":false}
```
- 源码级核对:全仓 grep `SEATS_NOT_ENOUGH` 仅命中 `AssignmentErrorCode.java` 的定义处一行,`hl-fleet-service` 主代码与测试代码中均无第二处引用,确认 605002 当前零产出方,与文案订正内容一致。
---
## 十、相关文档
- 关联 Issue: [wx/HL#8613](https://git.1814.love:8443/wx/HL/issues/8613)、[wx/HL#8614](https://git.1814.love:8443/wx/HL/issues/8614)
- 关联 PR: [wx/HL#8616](https://git.1814.love:8443/wx/HL/pulls/8616)
## 关联 / 联系人
### 链接
- **Issue**: [#8613](https://git.1814.love:8443/wx/HL/issues/8613)、[#8614](https://git.1814.love:8443/wx/HL/issues/8614)
- **PR**: [#8616](https://git.1814.love:8443/wx/HL/pulls/8616)
- **Merge commit**: [`2bf98beb491`](https://git.1814.love:8443/wx/HL/commit/2bf98beb49159de09087522d12b532cca720d5db)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,314 @@
---
schema: hl-changelog/v2
ticket: "8615"
title: "退团转房:作废原因新增三个系统取值,向酒店取消追加团期栅栏与联动缩减"
consumer: admin
author: wx(GIT)
change_type: 修改接口
backend_status: deployed
gateway_status: not_required
frontend_status: pending
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: ""
updated_at: "2026-09-30"
base: dev-v3
---
# 退团转房:作废原因新增三个系统取值,向酒店取消追加团期栅栏与联动缩减
## ⚠️ 关键变化
- 退团转房池列表(#8491 I-17)的 `records[].cancelReason` 由原来只有一个取值 `HOTEL_CANCELLED`,扩展为**四个**取值,新增 `GROUP_REALLOCATED` / `GROUP_PLAN_REMOVED` / `GROUP_DISBANDED` 三个由团期写口在同一事务内系统作废时写入的取值;这三种情况下 `handlerName` 恒为 `null`(无人工处理人),`handledAt` 仍会写入系统作废发生的时刻。前端按 `cancelReason` 分支展示原因文案的地方需要扩展这三个分支,否则会落到未知分支的兜底展示。
- 退团房向酒店取消(#8491 I-21,`POST .../room-transfers/{id}/cancel-hotel`)新增两个错误码:**团期来源行**在源团期已不在「资源准备中」阶段时返回 `808323`(此前 I-21 对团期来源行不做团期状态校验,任何状态都能直接取消,改动后与转房接口 I-20 同口径);团期来源行的源计划空余不足以覆盖本行待转间数时返回 `808324`。这两个码此前只出现在 I-20 的错误表里,I-21 的错误表新增了它们。
- 退团房向酒店取消**成功后**,若该行是团期来源行,会在同一事务内按取消的间数**同步缩减**源团期计划行的 `roomCount`(联动 `shrinkForTransfer`);这一步不改变本接口的响应结构,只影响该房间此后是否还会被团期自动分配算进可用余量。
- 转房接口(#8491 I-20)不在本次变更接口清单内——其错误表已发布过 `808324`,本次只是把此前一个应报 `808324`(源计划空余不足)却会先报到 `808932`(并发冲突)的边界情况改为直接报 `808324`,错误码本身与文案都没有新增,属于既有契约内的分支修正,前端已有的 `808324`/`808932` 处理分支不需要改动。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 退团转房池分页 | GET | `/v3/admin/order/house-console/room-transfers` | 响应字段取值扩展 | `cancelReason` 新增三个系统作废取值,系统作废行 `handlerName` 为空 |
| 2 | 退团房向酒店取消 | POST | `/v3/admin/order/house-console/room-transfers/{id}/cancel-hotel` | 新增错误码 + 联动行为 | 团期来源行新增 808323/808324 校验;取消成功后同步缩减源团期计划间数 |
## 三、接口详情
### 1. 退团转房池分页 `GET /v3/admin/order/house-console/room-transfers`
**VO**: `HouseRoomTransferPageReqVO → HouseRoomTransferPageRespVO`
#### 使用场景
房务控制台「退团房」页签展示当前待处理 / 已转出 / 已取消的转房行列表。本次改动只涉及列表行里 `cancelReason` 取值集合的扩展,请求参数与响应结构均未变化。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| page | Query | Integer | ❌ | 默认 1 | 页码 |
| pageSize | Query | Integer | ❌ | 1~100,默认 20 | 每页条数 |
| status | Query | String | ❌ | PENDING / TRANSFERRED / CANCELLED,默认 PENDING | 状态筛选 |
| risk | Query | String | ❌ | OVERDUE / NEAR / NO_DEADLINE / NORMAL | 风险筛选,仅对 PENDING 行有意义 |
| cityCode | Query | String | ❌ | ≤64 | 城市中文名 |
| keyword | Query | String | ❌ | ≤64 | 团号或酒店名关键词 |
| stayDateFrom | Query | LocalDate | ❌ | yyyy-MM-dd | 入住晚起(含) |
| stayDateTo | Query | LocalDate | ❌ | yyyy-MM-dd | 入住晚止(含) |
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| records | List<HouseRoomTransferRespVO> | 当前页转房行 |
| records[].id | Long(String) | 转房行 ID |
| records[].sourceType | String | 来源类型 ORDER / GROUP_BATCH |
| records[].sourceGroupBatchId / sourceBatchNo | Long(String) / String | 团期来源行的源团期 ID 与批次号 |
| records[].stayDate | LocalDate | 入住晚 |
| records[].hotelName / roomTypeName | String | 酒店名 / 房型名 |
| records[].roomCount | Integer | 原始间数 |
| records[].remainingCount | Integer | 剩余待处理间数;部分被系统收敛时会减少,行仍保持 PENDING |
| records[].status / statusLabel | String | PENDING / TRANSFERRED / CANCELLED 及中文 |
| records[].cancelReason | 【改动】String | 仅 CANCELLED 行有值。`HOTEL_CANCELLED`=房务人工向酒店取消(不变);新增 `GROUP_REALLOCATED`=本团已再分配、`GROUP_PLAN_REMOVED`=本团已撤销该晚计划、`GROUP_DISBANDED`=团期已解散,三者都是团期写口系统作废,见「六.5」 |
| records[].handlerName | 【改动】String | 处理人姓名;`cancelReason` 为三个新增取值之一时恒为 `null`(无人工处理人) |
| records[].handledAt | LocalDateTime | 处理时间;系统作废时同样写入作废发生的时刻,不为 null |
| records[].readOnly / readOnlyReason | Boolean / String | 对当前登录人是否只读及理由 |
| total / page / pageSize | long / int / int | 分页信息 |
| summary.pendingRooms 等 4 项 | int | 全部 PENDING 行的风险汇总,恒不受本次筛选条件影响 |
#### 请求示例
```http
GET /v3/admin/order/house-console/room-transfers?status=CANCELLED&cityCode=海拉尔&page=1&pageSize=20
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"id": "9100234", "sourceType": "GROUP_BATCH", "sourceOrderId": null, "teamNo": null,
"sourceTeamNo": "HLT-20261012-003", "sourceGroupBatchId": "500321", "sourceBatchNo": "HLT-20261012-003",
"stayDate": "2026-10-12", "cityName": "海拉尔", "hotelId": "60088", "hotelName": "海拉尔国际大酒店",
"roomTypeId": "70012", "roomTypeName": "高级大床房", "roomCount": 2, "remainingCount": 2,
"status": "CANCELLED", "statusLabel": "已取消", "cancelDays": 3, "cancelCutoff": "18:00", "remindDays": 1,
"deadlineAt": "2026-10-09T18:00:00", "risk": "NORMAL", "riskLabel": "正常",
"targetType": null, "targetOrderId": null, "targetTeamNo": null, "targetRequirementId": null,
"targetGroupBatchId": null, "targetBatchNo": null, "hotelConfirmNo": null, "proofFileIds": [],
"cancelFee": null, "cancelReason": "GROUP_REALLOCATED", "handlerName": null,
"handledAt": "2026-09-30T10:02:11", "remark": null, "readOnly": false, "readOnlyReason": null
}
],
"total": 1,
"page": 1,
"pageSize": 20,
"summary": { "pendingRooms": 6, "overdueRooms": 0, "nearRooms": 1, "noDeadlineRooms": 0 }
},
"success": true
}
```
#### 空数据 / 降级响应
```json
{ "code": 200, "message": "成功", "data": { "records": [], "total": 0, "page": 1, "pageSize": 20, "summary": { "pendingRooms": 0, "overdueRooms": 0, "nearRooms": 0, "noDeadlineRooms": 0 } }, "success": true }
```
#### 错误响应
```json
{
"code": 400,
"message": "status 取值非法",
"data": null,
"success": false
}
```
| code | message | 触发 |
|------|---------|------|
| 400 | status 取值非法 / risk 取值非法 | 入参校验,未变 |
| 808090 | 未登录或非房务角色,无权操作 | 非房务角色,未变 |
#### 业务边界
- 团期写口(分房重算、加入团期自动分房、改计划间数、删计划、团期解散)触发系统收敛时:某计划行下全部 PENDING 转房行都被覆盖,则行整体变为 CANCELLED 并写入对应 `cancelReason`;只覆盖了一部分,则行仍是 PENDING,仅 `remainingCount` 减少,`cancelReason`/`handlerName`/`handledAt` 均不变。
- 系统作废(`cancelReason` 为 `GROUP_REALLOCATED`/`GROUP_PLAN_REMOVED`/`GROUP_DISBANDED`)的行没有人工处理人:`handlerName` 为 `null`;触发它的管理员记在操作日志里,不在本接口暴露。
- `summary` 恒统计全部 PENDING 行,不受本次筛选条件影响。
### 2. 退团房向酒店取消 `POST /v3/admin/order/house-console/room-transfers/{id}/cancel-hotel`
**VO**: `HouseRoomTransferCancelReqVO → HouseRoomTransferRespVO`
#### 使用场景
退团房没有合适的转入对象时,房务直接向酒店办理取消,上传凭证并可录入取消费用。本次改动只影响**团期来源行**(`sourceType=GROUP_BATCH`):新增源团期状态与源计划余量两道校验,取消成功后联动缩减源团期计划间数;常规单来源行(`sourceType=ORDER`)的路径与校验顺序不变。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 转房行 ID |
| proofFileIds | Body | List<Long> | ✅ | 1~9 个,元素非空 | 取消凭证文件 ID |
| cancelFee | Body | BigDecimal | ❌ | ≥0,最多 2 位小数 | 取消费用(元) |
| remark | Body | String | ❌ | ≤200 字 | 备注 |
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| (整行) | HouseRoomTransferRespVO | 取消后的该行,字段同接口 1 的 `records[]` |
| status / statusLabel | String | CANCELLED / 已取消 |
| cancelReason | String | 恒为 `HOTEL_CANCELLED`;本接口触发的取消不会写入三个系统作废取值,那三个只由团期写口的系统收敛写入 |
| hotelConfirmNo | String | 本接口不接收确认号,恒为 `null`(未变) |
| cancelFee / proofFileIds | BigDecimal(String) / List<String> | 录入的取消费与凭证 |
#### 请求示例
```json
{
"proofFileIds": [88031, 88032],
"cancelFee": 0,
"remark": "酒店已免费取消"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "9100235", "sourceType": "GROUP_BATCH", "sourceOrderId": null, "teamNo": null,
"sourceTeamNo": "HLT-20261012-004", "sourceGroupBatchId": "500322", "sourceBatchNo": "HLT-20261012-004",
"stayDate": "2026-10-15", "cityName": "满洲里", "hotelId": "60090", "hotelName": "满洲里丽景大酒店",
"roomTypeId": "70020", "roomTypeName": "行政大床房", "roomCount": 3, "remainingCount": 3,
"status": "CANCELLED", "statusLabel": "已取消", "risk": null, "riskLabel": null,
"hotelConfirmNo": null, "proofFileIds": ["88031", "88032"], "cancelFee": "0.00",
"cancelReason": "HOTEL_CANCELLED", "handlerName": "王芳", "handledAt": "2026-09-30T10:15:32",
"remark": "酒店已免费取消", "readOnly": false, "readOnlyReason": null
},
"success": true
}
```
#### 空数据 / 降级响应
本接口无列表数据;任何失败都返回非 200 的 `code`,该行状态不变。
#### 错误响应
```json
{
"code": 808324,
"message": "转出间数超过剩余 1 间",
"data": null,
"success": false
}
```
| code | message | 触发 |
|------|---------|------|
| 400 | 凭证最多 9 个 / 凭证文件 ID 不能为空 / 取消费用不能为负 / 取消费用最多 2 位小数 / 备注不能超过 200 字 | 入参校验,未变 |
| 808090 | 未登录或非房务角色,无权操作 | 非房务角色,未变 |
| 808320 | 转房记录不存在 | id 不存在(未变;锁前锁后各判一次,同一个码) |
| 808326 | 只有原单处理人可以处理退团房间 | 非源单持有人且非超管,未变 |
| 808325 | 请填写酒店确认号并上传凭证 | 未上传凭证(本接口不要求确认号,文案沿用同一错误码),未变 |
| 808323 | 目标团期已确认,不能转入 | 【新增,仅团期来源行】源团期已不在「资源准备中」阶段。文案沿用 I-20 已发布的措辞,这里没有「目标」,实际含义是「源团期已不可再改动,不能取消这一行」 |
| 808321 | 该房间已处理 | 行已不是 PENDING,未变 |
| 808932 | 房务状态已被并发修改,请刷新后重试 | 【团期来源行新增触发场景】源团期或源计划行在读取时已不存在(并发被删/改);以及原有的 CAS 并发冲突 |
| 808324 | 转出间数超过剩余 {0} 间 | 【新增,仅团期来源行】源计划行已分配间数 > `roomCount − 本行剩余间数`,即空余不足以覆盖本行待取消间数;`{0}` = `max(roomCount − 已分配间数, 0)` |
| 100502 | 取消处理中,请勿重复提交 | 3 秒内重复提交,未变 |
#### 业务边界
- 只有源单持有人或超管可操作;凭证为空返回 808325(业务码,不是 400)。
- 校验顺序(团期来源行):808320(不锁定读)→ 808326 → 808325 → 808323(源团期栅栏)→ 808320(锁定读复验,同一个码)→ 808321 → 808932(源团期/源计划缺失)→ 808324(源计划空余不足)→ 808932(CAS 并发冲突)。常规单来源行没有 808323/808324 两步,其余顺序不变。
- 团期来源行取消成功后,源团期计划行的 `roomCount` 会在同一事务内按取消间数同步缩减;这一步对本接口响应体不可见,影响的是该房间此后是否还计入团期自动分配的可用余量。
- 成功后该行整行变为 CANCELLED,不再出现在待处理汇总里;已部分转出的行取消的是剩余部分。
## 四、契约约束与正确调用方式
- 接口 2 的团期来源行取消前,前端应先确认该行所属团期仍处于允许改动的阶段;若已收到 808323,不应重试,应提示房务该团期已不可再取消这一行。
- 接口 2 收到 808324 时,`message` 里的数字是此刻可取消的上限间数(可能为 0),不代表本行 `remainingCount`;不要直接拿 `remainingCount` 去重试提交。
- 接口 1 展示已取消行的原因文案时必须按 `cancelReason` 四个取值分支处理,不要假设该字段只有一个可能值;系统作废三种取值下 `handlerName` 为 `null` 是正常状态,不是数据缺失。
## 五、数据库行为
| 场景 | 团期计划行 `room_count` | 转房行 `status` / `remaining_count` |
|------|------|------|
| 常规单来源行(ORDER)取消 | 不涉及团期计划 | CANCELLED,其余字段照旧写入 |
| 团期来源行取消,源计划空余充足 | 按本次取消间数同步减少(`shrinkForTransfer`) | CANCELLED |
| 团期来源行取消,源计划空余不足(命中 808324) | 不变 | 不变,整体事务回滚 |
| 团期写口触发系统收敛(分房重算 / 自动分房 / 改计划间数 / 删计划 / 团期解散) | 视触发写口而定 | 全部覆盖:CANCELLED + 对应 `cancel_reason`;部分覆盖:仍 PENDING,`remaining_count` 减少 |
## 六、边界行为
- 转房行不存在或已被删除 → 808320(接口 2,未变)。
- 团期整团解散时,其下全部 PENDING 转房行在同一事务内作废(`cancelReason=GROUP_DISBANDED`),此后对这些行调用接口 2 会先命中 808321(已非 PENDING)。
- 源团期推进出「资源准备中」阶段后,其团期来源的 PENDING 行调用接口 2 会命中 808323;同一场景下调用 I-20(转房)此前已经是 808323,两个接口现在口径一致。
- 本单上线前已存在、且所属计划行已被删除但转房行仍是 PENDING 的存量脏数据,不会被本次新增的系统收敛机制回溯处理,仍会出现在接口 1 的列表里;对这类行调用接口 2,源团期已不在「资源准备中」时返回 808323,否则因源计划不存在返回 808932。
## 六.5、枚举
**cancelReason**(`house_room_transfer.cancel_reason`,仅 `status=CANCELLED` 时有值)
| 取值 | 中文 | 说明 | 是否本次新增 |
|------|------|------|------|
| HOTEL_CANCELLED | 房务人工向酒店取消 | 房务通过接口 2 主动办理 | 否 |
| GROUP_REALLOCATED | 本团已再分配 | 计划行还在,但空余已不足以覆盖待转间数——本团别的户分到了这批房,或计划间数被调少 | 是 |
| GROUP_PLAN_REMOVED | 本团已撤销该晚计划 | 待转房挂的计划行已不在活跃计划里:被删除、被替换成不同房型/酒店的新行,或缩减到 0 被整行删除 | 是 |
| GROUP_DISBANDED | 团期已解散 | 流团/解散时该团全部待转房一并作废 | 是 |
## 六.6、修改前后对比
**字段取值**
| 字段 | 改前 | 改后 |
|------|------|------|
| 接口 1 `records[].cancelReason` | 仅 `HOTEL_CANCELLED` 一个取值 | 新增 `GROUP_REALLOCATED` / `GROUP_PLAN_REMOVED` / `GROUP_DISBANDED` 三个取值 |
| 接口 2 错误码集合 | 808320 / 808321 / 808325 / 808326 / 808932(另有 400 / 808090 / 100502) | 团期来源行新增 808323、808324 |
**行为**
| 行为 | 改前 | 改后 |
|------|------|------|
| 本团某户分到已释放的团期房 | 原 PENDING 转房行照常留在池里,房务仍可在接口 2 / I-20 继续处理,存在一房两卖风险 | 团期写口在同一事务内收敛该计划行下的 PENDING 转房行,全部覆盖则整行作废并写入对应 `cancelReason`,部分覆盖则 `remaining_count` 减少 |
| 接口 2 对团期来源行的源团期状态 | 不做校验,源团期任意状态都能直接取消 | 源团期不在「资源准备中」时返回 808323 |
| 接口 2 取消团期来源行后源计划间数 | 不变 | 按取消间数同步缩减 |
| 接口 2 团期来源行空余不足 | 无此校验 | 返回 808324,行与源计划均不写入 |
## 六.7、影响评估
- 是否破坏向后兼容:否。`cancelReason` 是新增取值,不是重命名或语义变更;两个错误码是接口 2 的新增分支,不是复用/覆盖已有码的语义。
- 前端是否必须同步上线:是。不同步的话,系统作废行在列表上会落入未处理的 `cancelReason` 分支;团期来源行取消撞上 808323/808324 时若没有对应分支,会呈现为未识别错误。
- 前端 workaround 清理点:若此前把「`cancelReason` 非 `HOTEL_CANCELLED`」当异常兜底处理,需要改为按四个取值分别展示;否则无需清理,只需新增分支。
## 七、不影响范围
- 常规单来源行(`sourceType=ORDER`)在接口 2 的校验顺序、错误码与响应结构均未变化。
- 接口 1 的入参、分页结构、`summary` 汇总口径未变化。
- 转房接口 I-20 的请求/响应结构、错误码集合未新增,本单不改变其契约,仅修正一处原本应报 808324 却先报到 808932 的边界分支,不在本次变更接口清单内。
- 房务分房重建(H11)等团期写口自身的请求/响应契约未变化,只是这些写口在检测到需要收敛的 PENDING 转房行时会按本单的规则写入新的 `cancelReason`。
## 八、测试环境已验证
测试环境已验证(部署提交 405fc8db0,验证时刻 2026-09-30 10:22~10:41):
1. 转出间数超过计划剩余:`POST /v3/admin/order/house-console/room-transfers/{id}/cancel-hotel`,body code=808324("转出间数超过剩余 0 间"),转房行与计划行均无变化。
2. 团内户取消离团后另一户经重算分到房:`POST /v3/admin/order/{id}/cancel/pre-trip` + `POST /v3/admin/house/group-batches/{id}/allocations/rebuild`,转房行由 PENDING 变为 CANCELLED,`cancelReason=GROUP_REALLOCATED`,处理人为空,有对应操作日志。
3. 计划仍有空余时人工办理转出:`POST .../cancel-hotel`,body code=200,转房行变 CANCELLED/HOTEL_CANCELLED,计划 roomCount 由 2 减为 0 并软删(预期)。
## 十、相关文档
- 工单 #8615
- 参考发布契约:`changelogs-v2/2026-09/30_8491_房务控制台接口-新增接口-管理后台.md`(I-17 / I-20 / I-21 的既有字段与错误码定义)
## 关联 / 联系人
- 关联工单:#8615
- 关联历史 changelog:#8491
@@ -0,0 +1,405 @@
---
schema: "hl-changelog/v2"
ticket: "8619"
title: "团期子订单列表:只报接送机的户不再判未提交,新增逐类用车需求清单字段"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "bc9c13aaa39bab96e132a3a42515c821fa74a269"
target_release: "v2.1"
verified_at: "2026-10-01"
status_note: "PR #8624 已 squash 合并 dev-v3(ce7cd238e9355bdc17c45f90ace02827c7db60f5),hl-order-service-v3 dev-v3 分支已滚测试服。GET /v3/admin/order/group-batch/{groupBatchId}/orders 对真实团期 2104839654727618562 实测:同户 TRAVEL+TRANSFER 两类需求状态互不覆盖、vehicleRequirements[] 按 TRAVEL 在前 TRANSFER 在后下发,589500 团期不存在错误码实测通过。所有既有字段未删改,只放宽了车需求那组字段的取值范围并新增 vehicleRequirements。;前端已交付:报名清单车需求列改逐类清单(vehicleRequirements[] 非空逐类各显一行,空数组/旧响应回落单值字段),「未提交」误报修复直显自动生效,9 例定向测试全绿(hl-admin bc9c13aa)"
updated_at: "2026-09-30"
base: "dev-v3"
---
> **存放目录**: `changelogs-v2/2026-09/`
> **服务**: hl-order-service-v3 (端口 8086)
> **PR**: #8624
> **Issue**: #8619
> **日期**: 2026-09-30
> **影响范围**: 管理后台团期详情页「子订单列表」/ A3 接口消费方
## ⚠️ 关键变化
- **改前**:`GET /v3/admin/order/group-batch/{groupBatchId}/orders` 的车需求四个单值字段(`vehicleRequirementStatus`/`StatusName`/`Kind`/`KindName`)只从该户 **TRAVEL(行程用车)** 类需求行取值。若一个户只提交了 **TRANSFER(接送机)** 需求、没有 TRAVEL 需求,这四个字段恒被渲染成「未提交」,即使接送机需求已经在流转甚至已完成。
- **改后**:改为按展示序(TRAVEL 优先于 TRANSFER)取该户**当前活跃需求行中的首条**,只提交接送机的户会如实报出接送机自己的状态,不再被误判未提交。
- **新增字段 `vehicleRequirements`**:`GroupBatchOrderItemRespVO` 新增该数组字段,逐类列出该户全部活跃车需求行(TRAVEL 在前、TRANSFER 在后),每类各自独立的 `kind`/`kindName`/`status`/`statusName`,不再只能看到"展示序首条"这一个值。该户没有任何活跃车需求行时数组是 `[]`(空数组),不是 `null`。
- **`vehicleRequirementKind` 不再恒为 `"TRAVEL"`**:只提交接送机的户,该字段与 `vehicleRequirementKindName` 现在会如实报出 `"TRANSFER"`/`"接送机"`。前端如果曾经硬编码假设这两个字段只会是 TRAVEL/行程用车,需要一并放开。
- 其余约 25 个既有字段(订单状态、支付状态、酒店需求、出行人等)取值逻辑未变。
## 一、背景(选填)
本单与已发布的 `changelogs-v2/2026-09/30_8577_...-修改接口-管理后台.md`(#8577)修的是**同一症状家族**(只提交接送机被误判"未提交"),但**改动的是完全不同的接口/代码路径**,请勿混淆:
| | #8577 | #8619(本单) |
|---|---|---|
| 涉及接口 | `PUT` 保存车需求、`GET` 聚合草稿、`GET` 提交前校验(均在 `GroupVehicleRequirementService`) | `GET /v3/admin/order/group-batch/{groupBatchId}/orders`(团期子订单列表,`GroupBatchQueryService`/`GroupBatchConverter`) |
| 涉及错误码 | 809121/809122/809123 | 不涉及新增/变更错误码,沿用既有 589500 |
| 根因层 | 提交/校验链路 | 列表查询的取值范围(原只查 TRAVEL 类活跃行) |
两单互不覆盖,`#8577` 的改动对本接口没有影响;本接口过去存在的误判问题,`#8577` 也没有修到。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 团期下子订单列表(A3) | GET | `/v3/admin/order/group-batch/{groupBatchId}/orders` | 响应字段语义收窄 + 新增字段 | `vehicleRequirementKind` 不再恒为 TRAVEL;新增 `vehicleRequirements[]` 逐类需求清单 |
## 三、接口详情
### 1. 团期下子订单列表 `GET /v3/admin/order/group-batch/{groupBatchId}/orders`
**VO**: `GroupBatchOrderItemRespVO`
#### 使用场景
管理后台团期详情页展示该团期下全部子订单(一个订单=一个"户")的汇总信息,含车需求配车状态一栏。前端据此渲染列表行的车需求状态标签,并可能据 `vehicleRequirementKind` 决定展示"行程用车"或"接送机"图标。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | path | Long | 是 | 团期需存在 | 团期ID |
| page | query | Integer | 否 | 缺省 1,<1 归一为 1 | 页码,从 1 起 |
| pageSize | query | Integer | 否 | 缺省 20,<1 归一为 20,>200 截断为 200 | 每页条数 |
| includeTravelers | query | Boolean | 否 | 缺省 true | 是否附出行人明细(证件号/手机号一律不返回) |
| includeNeeds | query | Boolean | 否 | 缺省 true | 是否附 roomCount/roomType/specialNeeds |
| includeCancelled | query | Boolean | 否 | 缺省 false | 是否含已取消子订单(缺省只返活跃集) |
#### 出参 `Result<PageResult<GroupBatchOrderItemRespVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| orderId | String | 订单ID(Long 序列化为字符串) |
| orderNo | String | 订单号 |
| teamNo | String | 团号;订金支付成功后生成,未付订金为 null |
| customerName | String | 客户姓名 |
| participantCount | Integer | 出行人数 |
| orderStatus / orderStatusName | String / String | 订单状态/名称 |
| flowStatus / flowStatusName | String / String | 流程状态/名称(12 值枚举) |
| reviewStatus / reviewStatusName | String / String | 复核状态/名称;⚠️ 与团期核单 `GroupSettlementRespVO` 同名字段含义相反 |
| settlementStatus / settlementStatusName | String / String | 结算状态/名称;⚠️ 同样与团期核单含义相反,也不是财务 tab 的 settleStatus |
| payStatus / payStatusName | String / String | 支付状态/名称 |
| contractStatus / contractStatusName | String / String | 合同状态/名称 |
| insuranceStatus / insuranceStatusName | String / String | 投保状态/名称 |
| paidAmount / balanceAmount | String / String | 已付金额/待付余额(BigDecimal 序列化为字符串) |
| hotelRequirementStatus / hotelRequirementStatusName | String / String | 酒店需求状态/名称;#8249 起无活跃行时为 null,不回落 PENDING |
| **vehicleRequirementStatus** | String | 车需求状态;**本单起**取该户展示序(TRAVEL 优先)首条活跃需求行的状态,不再恒来自 TRAVEL |
| **vehicleRequirementStatusName** | String | 车需求状态名;PENDING_REVIEW 按 kind 分叉:TRAVEL="待提交车务",TRANSFER="待审核";"未提交"现在只在该户两类需求行都不存在时才出现 |
| **vehicleRequirementKind** | String | 车需求类别;**本单起不再恒为 "TRAVEL"**,只提交接送机的户会报 "TRANSFER" |
| **vehicleRequirementKindName** | String | 车需求类别名;未知类别给 null,不回落编码 |
| **vehicleRequirements** | Array<VehicleRequirementItem> | **本单新增**。该户全部活跃车需求行,TRAVEL 在前、TRANSFER 在后;一条都没有时为 `[]`(非 null) |
| consultantName | String | 顾问姓名 |
| totalPrice | String | 订单总价 |
| tierCode / tierName | String / String | 价格档位编码/名称 |
| travelerInfoComplete | Boolean | 出行人信息是否完整 |
| roomCount | Integer | 房间数;`includeNeeds=true` 时返回 |
| roomType / roomTypeName | String / String | 房型编码/名称;`includeNeeds=true` 时返回 |
| specialNeeds | String | 特殊需求;`includeNeeds=true` 时返回 |
| contactPhone | String | 联系电话(脱敏,如 `138****3046`) |
| groupChatUnreadCount | Integer | 群聊未读数;user-service 不可达/Feign 超时/未登录时降级为 0 |
| travelers | Array<TravelerItemVO> | 出行人明细;`includeTravelers=true` 时返回 |
`VehicleRequirementItem`(`vehicleRequirements` 数组元素):
| 字段 | 类型 | 说明 |
|------|------|------|
| kind | String | 需求类别,TRAVEL / TRANSFER |
| kindName | String | 类别名,"行程用车" / "接送机" |
| status | String | 该类需求自己的状态(见六.5 枚举) |
| statusName | String | 状态名(PENDING_REVIEW 按 kind 分叉,见上) |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2104839654727618562/orders?page=1&pageSize=5&includeTravelers=false&includeNeeds=false
Authorization: Bearer {token}
```
#### 响应示例
测试服真实返回(团期 `2104839654727618562`,3 个子订单,覆盖"两类需求并存且状态不同""仅 TRAVEL"两种场景):
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"orderId": "2104839654652121090",
"orderNo": "HL20260929154414207",
"teamNo": "26-2313",
"customerName": "李文博",
"participantCount": 2,
"orderStatus": "CUSTOMIZING",
"orderStatusName": "定制中",
"flowStatus": "RESOURCE_PREPARING",
"flowStatusName": "资源准备",
"reviewStatus": null,
"reviewStatusName": null,
"settlementStatus": "NONE",
"settlementStatusName": "未结算",
"payStatus": "FULLY_PAID",
"payStatusName": "已付全款",
"contractStatus": null,
"contractStatusName": null,
"insuranceStatus": null,
"insuranceStatusName": null,
"paidAmount": "7360.00",
"balanceAmount": "0.00",
"hotelRequirementStatus": null,
"hotelRequirementStatusName": null,
"vehicleRequirementStatus": "DONE",
"vehicleRequirementStatusName": "配车完成",
"vehicleRequirementKind": "TRAVEL",
"vehicleRequirementKindName": "行程用车",
"vehicleRequirements": [
{"kind": "TRAVEL", "kindName": "行程用车", "status": "DONE", "statusName": "配车完成"},
{"kind": "TRANSFER", "kindName": "接送机", "status": "PENDING", "statusName": "待车队配"}
],
"consultantName": "cw_test_7443",
"totalPrice": "7360.00",
"tierCode": "2A",
"tierName": "2成人",
"travelerInfoComplete": true,
"roomCount": null,
"roomType": null,
"roomTypeName": null,
"specialNeeds": null,
"contactPhone": "138****3046",
"groupChatUnreadCount": 0,
"travelers": null
},
{
"orderId": "2104839686486888449",
"orderNo": "HL20260929154421828",
"teamNo": "26-9436",
"customerName": "张丽娟",
"participantCount": 2,
"orderStatus": "CUSTOMIZING",
"orderStatusName": "定制中",
"flowStatus": "PENDING_CONFIRM",
"flowStatusName": "待确认",
"payStatus": "FULLY_PAID",
"payStatusName": "已付全款",
"paidAmount": "7360.00",
"balanceAmount": "0.00",
"vehicleRequirementStatus": "DONE",
"vehicleRequirementStatusName": "配车完成",
"vehicleRequirementKind": "TRAVEL",
"vehicleRequirementKindName": "行程用车",
"vehicleRequirements": [
{"kind": "TRAVEL", "kindName": "行程用车", "status": "DONE", "statusName": "配车完成"}
],
"totalPrice": "7360.00",
"tierCode": "2A",
"tierName": "2成人",
"contactPhone": "138****3047",
"groupChatUnreadCount": 0
},
{
"orderId": "2104839729176514562",
"orderNo": "HL20260929154432061",
"teamNo": "26-6559",
"customerName": "那顺",
"participantCount": 3,
"orderStatus": "CUSTOMIZING",
"orderStatusName": "定制中",
"flowStatus": "PENDING_CONFIRM",
"flowStatusName": "待确认",
"payStatus": "FULLY_PAID",
"payStatusName": "已付全款",
"paidAmount": "11620.00",
"balanceAmount": "0.00",
"vehicleRequirementStatus": "DONE",
"vehicleRequirementStatusName": "配车完成",
"vehicleRequirementKind": "TRAVEL",
"vehicleRequirementKindName": "行程用车",
"vehicleRequirements": [
{"kind": "TRAVEL", "kindName": "行程用车", "status": "DONE", "statusName": "配车完成"},
{"kind": "TRANSFER", "kindName": "接送机", "status": "PENDING_REVIEW", "statusName": "待审核"}
],
"totalPrice": "11620.00",
"tierCode": "3A",
"tierName": "3成人",
"contactPhone": "138****3048",
"groupChatUnreadCount": 0
}
],
"total": 3,
"page": 1,
"pageSize": 5
},
"traceId": null,
"success": true
}
```
以上三条均取自 TRAVEL+TRANSFER 两类需求并存的户,展示了两类需求各自独立取值(第三条 TRANSFER 处于 `PENDING_REVIEW` 显示为"待审核",TRAVEL 处于 `DONE` 不受影响)。**只提交接送机、完全没有 TRAVEL 需求**的户是本次修复要解决的核心场景,测试服当前团期数据中暂无这类现成样本(该形态的订单目前都不挂团期),该场景由自动化回归覆盖,见"八、测试环境已验证"。
#### 空数据 / 降级响应
分页越界的真实返回(`page=999` 超出总页数):
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [],
"total": 3,
"page": 999,
"pageSize": 5
},
"traceId": null,
"success": true
}
```
`groupChatUnreadCount` 在 user-service 不可达、Feign 调用超时或当前登录态失效时降级返回 `0`,不抛错、不影响本接口其余字段。
#### 错误响应
团期不存在的真实返回:
```json
{
"code": 589500,
"message": "团期不存在",
"data": null,
"traceId": null,
"success": false
}
```
无操作权限时返回错误码 `589507`("无操作权限(当前角色未授予团期权限,或该团期不在您名下)",源自 `GroupBatchErrorCode`,本轮测试账号为 admin 全权角色未触发,未做活体验证)。
#### 业务边界
- `groupBatchId` 对应团期不存在返回 589500,`data` 为 `null`。
- `pageSize` 超过 200 会被后端静默截断为 200,不报错。
- `includeCancelled` 缺省 `false`,不传时列表不含已取消子订单。
- `vehicleRequirements` 为空数组 `[]` 表示该户当前没有任何活跃车需求行,不是接口异常;不要用 `null` 判空。
- `vehicleRequirementKind`/`KindName` 在该户尚无任何车需求行时为 `null`,不回落成某个默认编码。
- 两类需求同时存在时,单值字段(`vehicleRequirementStatus`/`Kind` 等)取的是**展示序(TRAVEL 优先)首条**,并非"最近更新"或"按查询顺序";需要拿到每一类各自的真实状态必须读 `vehicleRequirements[]`,不能只读单值字段。
## 四、契约约束与正确调用方式(接口类必写)
### ✅ 正确 / ❌ 错误 payload 对照
- ✅ 正确:判断某个户是否已提交任意车需求,遍历 `vehicleRequirements`(长度 > 0 即已提交),或分别读取 `vehicleRequirements` 中 `kind=TRAVEL`/`kind=TRANSFER` 各自的 `status`。
- ❌ 错误:继续假设 `vehicleRequirementKind` 恒为 `"TRAVEL"` 并据此做条件分支——只提交接送机的户会被分支判空/判错。
- ❌ 错误:把 `vehicleRequirementStatusName === "未提交"` 当作"该户任意一类车需求都未提交"的充分条件——本单之后它只代表"两类都未提交",不能再用它反推"TRAVEL 未提交"或"TRANSFER 未提交"这类更细的判断,要细分请读 `vehicleRequirements[]`。
### 切换状态时的必要动作
无需前端触发任何状态切换动作;本次是纯读接口的响应字段语义调整,不涉及写操作。
## 五、数据库行为(涉及写操作时必写)
本接口是纯查询接口,本单未新增/变更任何表结构,也未变更任何写路径。改动仅收窄/放宽了查询车需求行时的过滤条件(原实现只按 `requirement_kind = 'TRAVEL'` 取活跃行,现改为按展示序取该户全部活跃行的首条,并额外把全部活跃行一并下发到 `vehicleRequirements`)。
## 六、边界行为
- 团期不存在 → 589500,`data: null`。
- 当前登录角色对该团期无权限 → 589507(源码定义,未做活体验证)。
- `page`/`pageSize` 非法值(<1 或超范围)由后端静默归一/截断,不报参数校验错误。
- 分页越界 → 返回 `records: []`,`total` 仍是真实总数,不报错。
- `hotelRequirementStatus`/`vehicleRequirementKind` 等状态类字段在对应需求不存在时给 `null`,均不回落到某个默认状态码。
- `groupChatUnreadCount` 在下游不可达时静默降级为 `0`。
## 六.5、枚举 / 数据字典(接口出现枚举时必写)
### vehicleRequirementKind / vehicleRequirements[].kind(`VehicleRequirementKind`)
| 值 | 中文名 |
|----|--------|
| TRAVEL | 行程用车 |
| TRANSFER | 接送机 |
### vehicleRequirementStatus / vehicleRequirements[].status(`RequirementStatus`)
| 值 | 中文名(车需求语境) | 备注 |
|----|----------------------|------|
| PENDING | 待车队配 | |
| PROCESSING | 配车中 | |
| DONE | 配车完成 | |
| PENDING_REVIEW | TRAVEL="待提交车务";TRANSFER="待审核" | 同一状态码按 kind 分叉出不同中文名 |
| REJECTED_TO_CONSULTANT | 驳回顾问 | |
| REJECTED_TO_ADMIN | 驳回管理员 | |
| (该户无对应需求行) | 未提交 | `vehicleRequirementStatus`/`StatusName` 单值字段专属,`vehicleRequirements[]` 数组元素不会出现这个取值——没有对应行就不会出现在数组里 |
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| vehicleRequirementStatus / StatusName | 只取该户 TRAVEL 类需求行的值;无 TRAVEL 行则恒为 "未提交" | 取展示序(TRAVEL 优先)首条**活跃**需求行的值;只要该户存在任意一类需求行就不再是"未提交" |
| vehicleRequirementKind / KindName | 恒为 "TRAVEL"/"行程用车"(或该户无 TRAVEL 行时为 null) | 如实反映展示序首条需求行的真实类别,可能是 "TRANSFER"/"接送机" |
| vehicleRequirements | 不存在该字段 | 新增,数组,逐类列出该户全部活跃需求行,空时为 `[]` |
### 行为级对比
| 场景 | 改前 | 改后 |
|------|------|------|
| 只提交接送机(TRANSFER),无 TRAVEL 需求 | 单值字段恒报"未提交",即便接送机已在流转甚至完成 | 单值字段如实报接送机自己的状态;`vehicleRequirements` 含 1 条 TRANSFER 记录 |
| 只提交行程用车(TRAVEL) | 与改后一致(回归测试覆盖,行为未变) | 行为不变 |
| 两类需求都提交 | 单值字段只能看到 TRAVEL 一类的状态,无法从列表接口直接得知 TRANSFER 的独立状态 | 单值字段仍取 TRAVEL(展示序优先),但 `vehicleRequirements` 同时给出两类各自独立的真实状态 |
| 两类需求都未提交 | "未提交"(#8249 起已是该行为) | 行为不变,`vehicleRequirements` 为 `[]` |
## 六.7、影响评估(修改/删除类必写)
- 破坏兼容:否。既有字段名称、类型、语义边界(`null` 代表无对应需求行)均未变,改动只是放宽了 `vehicleRequirementKind` 的实际取值范围、扩大了 `vehicleRequirementStatus`/`StatusName` 能反映的真实状态覆盖面。
- 前端是否必须同步上线:若前端曾经硬编码假设 `vehicleRequirementKind` 恒为 `"TRAVEL"`(例如据此固定展示"行程用车"图标、或对非 TRAVEL 值做兜底成空白),需要同步放开,否则只提交接送机的户在前端会展示错误的类别图标/文案,但不会报错或崩溃。
- 若前端此前为规避"接送机被误判未提交"这个已知问题,在自己代码里做过特判/兜底逻辑,本单上线后该特判可以删除。
## 七、不影响范围
- `hotelRequirementStatus`/`hotelRequirementStatusName` 及其判空逻辑(#8249 行为)未变。
- 订单状态、支付状态、合同状态、投保状态、复核/结算状态、价格档位、出行人相关字段等约 25 个既有字段未变。
- 分页参数默认值与归一/截断规则未变。
- `includeCancelled`/`includeNeeds`/`includeTravelers` 三个开关的既有行为未变。
- `#8577` 修复的三个车需求提交/校验接口(`PUT` 保存、`GET` 聚合草稿、`GET` 提交前校验)及其错误码 809121/809122/809123,与本接口是不同代码路径,互不影响。
- 团期详情页的"车队"chips 维度接口(`/chips/vehicle`)已支持 TRANSFER 类别展示,本次改动前后行为一致,不受影响。
## 八、测试环境已验证
**活体实测(本会话,2026-09-30,测试服 dev-v3):**
1. `GET /v3/admin/order/group-batch/2104839654727618562/orders?page=1&pageSize=5&includeTravelers=false&includeNeeds=false` → HTTP 200,返回 3 条真实子订单记录,其中 2 条同时具有 TRAVEL+TRANSFER 两类活跃需求行,单值字段与 `vehicleRequirements[]` 均按预期独立报出各自状态(含 `PENDING_REVIEW` 在 TRANSFER 语境下正确显示为"待审核")。原文见上「响应示例」。
2. `GET /v3/admin/order/group-batch/999999999999999999/orders` → HTTP 200,`code:589500, message:"团期不存在"`。
3. `GET /v3/admin/order/group-batch/2104839654727618562/orders?page=999&pageSize=5` → HTTP 200,`records:[]`,`total:3` 保留真实总数。
**自动化回归(`ce7cd238e9355bdc17c45f90ace02827c7db60f5`,已随 PR #8624 合入 dev-v3,覆盖测试服当前暂无现成样本的场景):**
- `GroupBatchConverterTest#toOrderItemVO_transferOnly_reportsRealStatusNotNotSubmitted`:该户只有一条 TRANSFER/`PENDING_REVIEW` 活跃行 → `vehicleRequirementStatusName` 报"待审核"(不是"未提交"),`vehicleRequirementKind`="TRANSFER",`vehicleRequirements` 含 1 条对应记录。这正是本单要修复的核心场景。
- `GroupBatchConverterTest#toOrderItemVO_travelOnly_legacyFieldsUnchanged`:只有 TRAVEL 行时行为与改前一致(回归保护)。
- `GroupBatchConverterTest#toOrderItemVO_bothKinds_statusesStayIndependent`:TRAVEL 与 TRANSFER 两类状态互不覆盖,且与查询返回顺序无关(用 TRANSFER 先于 TRAVEL 的输入顺序验证展示序不受取数顺序影响)。
- `GroupBatchConverterTest#toOrderItemVO_neitherKind_stillNotSubmitted`:两类都无活跃行时仍报"未提交",`vehicleRequirements` 为空数组而非 null(#8249 行为回归保护)。
- `GroupBatchConverterTest#toOrderItemVO_nullVehicleRows_emptyListNotNull`:上游传入 null 行集合时 `vehicleRequirements` 仍是空列表,不会是 null。
- `GroupBatchConverterTest#toOrderItemVO_anyExistingRow_neverRendersNotSubmitted`(参数化,覆盖 TRAVEL/TRANSFER × 6 种状态共 12 种组合):只要该户存在任意一条需求行,`statusName` 永不为"未提交"或空白。
- `GroupVehicleStatusNameCrossOutletTest`(#8218 既有门禁):同一 `(状态码, kind)` 对在本接口与其他读口的中文名保持一致,本次改动未破坏该跨口一致性。
## 十、相关文档
- 工单:#8619
- PR:#8624(squash 合并 `ce7cd238e9355bdc17c45f90ace02827c7db60f5`)
- 关联但独立的历史修复:`changelogs-v2/2026-09/30_8577_只提交接送机的户不再被判未提交用车需求-修改接口-管理后台.md`(#8577,见本文「一、背景」的区分说明)
## 关联 / 联系人
### 链接
- Issue: https://git.1814.love/wx/HL/issues/8619
- PR: https://git.1814.love/wx/HL/pulls/8624
### 联系人
- 后端负责人:@wx
@@ -0,0 +1,578 @@
---
schema: "hl-changelog/v2"
ticket: "8621"
title: "派车三个读口补齐状态中文名,枚举码不再裸下发(含 #8620 用车控制状态口径澄清)"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "三个既有管理后台读口各新增中文名字段,纯新增、无删除、无改名、无取值变化:(1) GET /admin/fleet/board/orders 的 records[] 新增 baseAssignmentStatusLabel(代表日行落库派单态中文名,与既有 baseAssignmentStatus 恒成对非空;它与 assignmentStatusLabel 是两个不同口径——前者是落库态、不含派生态,后者是覆写后的有效态、会出现临期加急派生态);(2) GET /admin/fleet/group-dispatch/pending-batches 的 records[] 新增 batchStatusName(团期生命周期状态中文名,九态全覆盖)与 dispatchProgressLabel(配车进度中文名,未开始/部分排车/已排满);(3) GET /admin/fleet/group-dispatch/batches/{groupBatchId}/overview 的 days[].vehicles[] 新增 statusLabel(派车状态中文名,已派车/已确认,字典另含已取消)。三个字典的共同不变量:码为 null 则中文名为 null(不编默认文案),码非空则中文名必非空;未登记的新码原样回落成码本身,不抛异常也不返回 null——所以前端渲染时不要假设这一格一定是中文,但不必为未知码写空值兜底。这批字段存在的唯一目的是把码→中文的字典收成后端单源(CODE_RULES §15.7),前端本地映射表请改为直接渲染后端下发值:本地表在遇到未登记新码时会显示空白,后端值至少是码本身。同时随 #8620 澄清一条既有字段的读法(字段名与取值零变化):orders[].vehicleControlStatus 是订单级单值、行程用车与接送机两类共用一格,非 DONE 只代表两类里至少一类没齐、说不出是哪一类;要分辨哪类没齐请读同级按类别拆开的字段(travelRequirementStatus / transferDeclared / transferPendingCount)。三个端点的入参、分页、过滤、排序、错误码(100001 / 600012 / 600013 / 401)与其余响应字段均未变化。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# 车务派车读口:状态中文名补齐,枚举码不再裸下发
> **存放目录**: 二期(order-v3/fleet)→ `changelogs-v2/2026-09/`
## ⚠️ 关键变化
- **三个既有读口共新增 4 个中文名字段,纯新增**:`records[].baseAssignmentStatusLabel`(派单看板订单清单)、`records[].batchStatusName` + `records[].dispatchProgressLabel`(待配车团期清单)、`days[].vehicles[].statusLabel`(团期配车总览)。
- **既有字段一个没动**:`assignmentStatus` / `assignmentStatusLabel` / `baseAssignmentStatus` / `batchStatus` / `dispatchProgress` / `vehicles[].status` 的字段名、类型、取值域、语义与本次改动前逐字相同;入参、分页、过滤、排序、错误码也未变。
- **三个字典共用同一组不变量**:码为 `null` ⇒ 中文名同为 `null`(不编默认文案);码非空 ⇒ 中文名必非空;**未登记的新码原样回落成码本身**,既不抛异常也不返回 `null`。所以前端**不需要**为「没见过的码」写空值兜底分支,但**渲染时不要假设这一格一定是中文**(回落时它就是那个码)。
- **前端请停用本地的码 → 中文映射表**,直接渲染后端下发的中文名字段。本地表在遇到未登记新码时渲染成空白,而后端值至少是码本身;这批字段存在的唯一理由就是把字典收成后端单源(CODE_RULES §15.7)。
- 🔴 **`baseAssignmentStatusLabel` 与 `assignmentStatusLabel` 不是一回事,别混用**:前者是**落库态**中文名(不派生、不被当前需求口径覆写,永远是 6 个落库态之一),后者是**覆写后的有效态**中文名(可能对应 `unassigned_urgent` / `holding_urgent` 这类派生态)。要展示「落库态 vs 有效态」并排对照(陈旧定稿排查场景)才需要前者;常规状态列继续用后者。
- 🔴 **`vehicleControlStatus` 是订单级单值、两类共用一格**(#8620,本次只澄清读法,字段与取值零变化):它非 `DONE` 只说明「行程用车与接送机里至少一类没齐」,**说不出是哪一类**。要分辨请读同级按类别拆开的字段(`travelRequirementStatus` / `transferDeclared` / `transferPendingCount`)。
- **`CANCELLED`(已取消)在派车状态字典里有中文名**。团期配车总览的逐车项 `status` 正常只会出现 `ASSIGNED` / `CONFIRMED`(已取消的派车行不进总览),但字典三码全覆盖,前端若自行构造筛选项按两值即可。
## 一、背景(选填)
这批读口此前把枚举码裸下发:`baseAssignmentStatus`、`batchStatus`、`dispatchProgress`、`vehicles[].status` 四处只有码、没有中文名,而同一行上别的状态字段(如 `assignmentStatusLabel`、`requirementKindLabel`)早已由后端下发中文名。结果是前端必须在本地再维护一份码 → 中文的映射表,这份表与后端枚举是两份真源:后端加一个码,前端那格就渲染成空白,而且没有任何信号提示。本次把这四处补齐成「码 + 中文名成对下发」,字典的唯一来源放在后端枚举里。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 派单看板订单清单 | GET | `/admin/fleet/board/orders` | 修改 | `records[]` 新增 `baseAssignmentStatusLabel`(落库派单态中文名) |
| 2 | 待配车团期清单 | GET | `/admin/fleet/group-dispatch/pending-batches` | 修改 | `records[]` 新增 `batchStatusName`(团期状态中文名)与 `dispatchProgressLabel`(配车进度中文名) |
| 3 | 团期配车总览 | GET | `/admin/fleet/group-dispatch/batches/{groupBatchId}/overview` | 修改 | `days[].vehicles[]` 新增 `statusLabel`(派车状态中文名);`orders[].vehicleControlStatus` 读法澄清(#8620) |
## 三、接口详情
### 1. 派单看板订单清单 `GET /admin/fleet/board/orders`
**VO**: `BoardOrderPageReqVO → BoardOrderPageRespVO`
#### 使用场景
车务派单看板的订单清单(list / grid 两视图共用)。本次变更只在每行上多给一个中文名字段,供「落库态 vs 有效态」并排展示的排查场景使用;常规状态列继续用 `assignmentStatusLabel`。
#### 入参
入参本次**零变化**,为便于自洽联调完整列出(全部 query 参数,全部选填)。
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| statuses | query | String[] | 否 | `unassigned` / `unassigned_urgent` / `holding` / `holding_urgent` / `assigned` / `canceled` / `completed` | 多状态筛选,含派生态,任一命中即返;空=不过滤 |
| status | query | String | 否 | 同 `statuses` 取值域 | `statuses` 的别名,单值或逗号分隔,与 `statuses` 合并 |
| startDayFrom | query | LocalDate | 否 | `YYYY-MM-DD` | 日期区间起,与行程区间重叠(非仅出团日);单边只约束一侧 |
| startDayTo | query | LocalDate | 否 | `YYYY-MM-DD` | 日期区间止 |
| startDate | query | LocalDate | 否 | `YYYY-MM-DD` | `startDayFrom` 的兼容别名,未传 `startDayFrom` 时生效 |
| endDate | query | LocalDate | 否 | `YYYY-MM-DD` | `startDayTo` 的兼容别名,未传 `startDayTo` 时生效 |
| vehicleTypeKeys | query | String[] | 否 | — | 车型大类多选;未派按需求车型、已派按实际车辆大类过滤 |
| typeKeys | query | String[] | 否 | — | `vehicleTypeKeys` 的兼容别名,未传前者时生效 |
| driverName | query | String | 否 | — | 司机姓名模糊搜索 |
| keyword | query | String | 否 | — | 统一文字搜索:司机 / 联系人 / 团号 / 订单号 / 当前定制师显示名任一包含 |
| contactName | query | String | 否 | — | 联系人(客户名)模糊搜索 |
| contactKeyword | query | String | 否 | — | `contactName` 的兼容别名 |
| teamNo | query | String | 否 | — | 团号模糊搜索(仅匹配真实团号,不匹配订单号) |
| groupBatchId | query | Long | 否 | — | 运营团期 ID 精确筛选 |
| orderKind | query | String | 否 | `ALL` / `NORMAL` / `GROUP`,其余值返 100001 | 订单归属粗筛;不传或空串=`ALL`。`NORMAL` 与 `groupBatchId` 同传逻辑互斥,返空列表不报错 |
| requirementKind | query | String | 否 | `TRAVEL` / `TRANSFER`,其余值返 100001 | 用车需求类别筛选;不传或空串=不过滤 |
| consultantId | query | Long | 否 | — | 当前负责定制师管理员 ID 精确筛选(下拉值由看板汇总接口下发) |
| plannerName | query | String | 否 | — | 定制师姓名模糊搜索(兼容旧前端) |
| consultantName | query | String | 否 | — | `plannerName` 的别名 |
| variant | query | String | 否 | `list`(默认)/ `grid`,其余值返 100001 | 视图 |
| page | query | Integer | 否 | ≥ 1,默认 1 | 页码;`pageNo` 是其兼容别名 |
| pageSize | query | Integer | 否 | 1~100,默认 20 | 每页条数 |
#### 出参 `Result<BoardOrderPageRespVO>`
只列与本次变更直接相关的字段;`records[]` 其余字段与本次改动前完全一致。
| 字段 | 类型 | 说明 |
|------|------|------|
| records | Array | 订单行列表,维度=当前有效用车需求;同一 `requirementId` 只返回一条 |
| records[].assignmentStatus | String | 当前派单状态码(含派生 `unassigned_urgent` / `holding_urgent`,会按当前需求口径覆写);**未变** |
| records[].assignmentStatusLabel | String | 当前派单状态中文名(有效态口径);**未变** |
| records[].baseAssignmentStatus | String | 代表日行落库基础状态码(不派生、不覆写):`unassigned` / `holding` / `assigned` / `canceled` / `exception` / `completed`;**未变** |
| records[].baseAssignmentStatusLabel | String | 🆕 落库基础状态中文名,与 `baseAssignmentStatus` 恒成对非空:待派车 / 待确认执行 / 已派车 / 已取消 / 异常 / 已完结。**永远不会出现派生态对应的文案**(派生态只进 `assignmentStatus`);未登记码原样回落成码本身 |
| total | Long | 总条数 |
| page | Integer | 当前页码 |
| pageSize | Integer | 每页条数 |
#### 请求示例
```http
GET /admin/fleet/board/orders?variant=list&startDayFrom=2026-10-01&startDayTo=2026-10-31&page=1&pageSize=20
Authorization: Bearer {token}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"id": "2103998277441093633",
"orderNo": "HL202610080031",
"teamNo": "T26-4128",
"orderId": "2103998277441093632",
"customerName": "周雅",
"headcount": 4,
"startDate": "2026-10-08",
"endDate": "2026-10-12",
"requirementKind": "TRAVEL",
"requirementKindLabel": "行程用车",
"assignmentStatus": "unassigned_urgent",
"assignmentStatusLabel": "待派车",
"baseAssignmentStatus": "unassigned",
"baseAssignmentStatusLabel": "待派车",
"manualUrgent": false,
"canAssign": true
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"success": true
}
```
#### 空数据 / 降级响应
- 无命中:`data.records` 返回空数组 `[]`,`total` 为 `0`,不返回 `null`,不报错。
- `records[]` 有行时 `baseAssignmentStatus` 与 `baseAssignmentStatusLabel` **必定同时非空**:落库态取自派单行的 `assignment_status`(NOT NULL);订单还没有落库派单行时走虚拟待派卡,落库态固定 `unassigned`、中文名固定「待派车」,不会出现「有码没中文名」或「有中文名没码」的半边状态。
- `order-v3` 整体不可达时,行上的日期、紧急态、排序会回退派单快照口径(本次未改这条既有降级路径),`baseAssignmentStatusLabel` 仍照常下发(它只依赖 fleet 本域落库行)。
#### 错误响应
```json
{
"code": 100001,
"message": "参数非法: variant 仅支持 list/grid,传入非法值:card",
"data": null,
"success": false
}
```
- `100001 参数非法: {0}`:`variant` 非 `list`/`grid`、`orderKind` 非 `ALL`/`NORMAL`/`GROUP`、`requirementKind` 非 `TRAVEL`/`TRANSFER`、`page` < 1、`pageSize` 越界。
- `401`:未登录或令牌失效。注意测试环境网关对失效令牌返回 **HTTP 200 + 信封 `code: 401`**,前端拦截器请按信封 `code` 判定,不要只看 HTTP 状态行。
#### 业务边界
- `baseAssignmentStatusLabel` 与 `assignmentStatusLabel` 走**同一份**映射(`AssignmentStatusEnum.labelOf`),只是喂进去的码不同:前者喂落库态、后者喂覆写后的有效态。所以同一行上两个中文名可能不同(例:落库 `assigned`「已派车」而有效态被当前需求口径覆写成「待派车」),**这不是数据错误**,正是本字段要暴露的对照。
- 派生态 `unassigned_urgent` / `holding_urgent` 只出现在 `assignmentStatus`,`baseAssignmentStatus` 与其中文名永远是 6 个落库态之一。前端若拿 `baseAssignmentStatusLabel` 当加急标识会永远读不到加急,加急请读 `assignmentStatus` 或 `manualUrgent` / `urgentBadge`。
- 落库态 `exception`(异常)在筛选入参 `statuses` 的取值域里**没有**对应筛选项,但它会作为 `baseAssignmentStatus` 的值出现在响应里,中文名「异常」。
- 中文名不参与任何筛选与排序,只是展示字段;按状态筛选一律传码。
---
### 2. 待配车团期清单 `GET /admin/fleet/group-dispatch/pending-batches`
**VO**: `GroupDispatchPendingBatchPageReqVO → PageResult<GroupDispatchPendingBatchRespVO>`
#### 使用场景
车务「待配车团期」列表页。本次每行多给两个中文名:团期生命周期状态与配车进度,前端可直接渲染,不再需要本地两张映射表。
#### 入参
入参本次**零变化**,完整列出。
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| departDateFrom | query | LocalDate | 否 | `YYYY-MM-DD` | 出发日区间起;单边只约束一侧 |
| departDateTo | query | LocalDate | 否 | `YYYY-MM-DD` | 出发日区间止 |
| keyword | query | String | 否 | 长度 ≤ 50,超长返 600013 | 团号 / 团期名称模糊搜索 |
| dispatchProgress | query | String | 否 | `NOT_STARTED` / `PARTIAL` / `FULL`,其余值返 600013 | 按配车进度筛选;不传=不过滤 |
| page | query | Integer | 否 | ≥ 1,默认 1 | 页码 |
| pageSize | query | Integer | 否 | 1~100,默认 20 | 每页条数 |
#### 出参 `Result<PageResult<GroupDispatchPendingBatchRespVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| records | Array | 待配车团期行列表 |
| records[].groupBatchId | String | 运营团期 ID(雪花 ID 以字符串下发) |
| records[].batchNo | String | 团号 |
| records[].batchName | String | 团期名称 |
| records[].batchStatus | String | 团期生命周期状态码;**未变** |
| records[].batchStatusName | String | 🆕 团期状态中文名,与 `batchStatus` 恒成对非空。九态见「六.5」;未登记码原样回落成码本身 |
| records[].departDate | String | 出发日 `YYYY-MM-DD` |
| records[].endDate | String | 结束日 `YYYY-MM-DD` |
| records[].serviceDayCount | Integer | 服务天数 |
| records[].enrolledOrders | Integer | 已报名子订单数 |
| records[].enrolledPeople | Integer | 已报名人数 |
| records[].requirementConfirmed | Boolean | 团期用车需求是否已确认 |
| records[].vehicleReady | Boolean | 车辆是否已就绪 |
| records[].dispatchedDayCount | Integer | 已排车天数 |
| records[].dispatchProgress | String | 配车进度码:`NOT_STARTED` / `PARTIAL` / `FULL`;**未变** |
| records[].dispatchProgressLabel | String | 🆕 配车进度中文名,与 `dispatchProgress` 恒成对非空:未开始 / 部分排车 / 已排满 |
| records[].transferPendingCount | Integer | 接送机未配计数;`null` = 未取到(**不是 0**,#8593) |
| records[].unreadCount | Integer | 未读会话消息数;依赖服务不可达时退化为 `0` |
| total | Long | 总条数 |
| page | Integer | 当前页码 |
| pageSize | Integer | 每页条数 |
#### 请求示例
```http
GET /admin/fleet/group-dispatch/pending-batches?departDateFrom=2026-10-01&departDateTo=2026-10-31&dispatchProgress=PARTIAL&page=1&pageSize=20
Authorization: Bearer {token}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"groupBatchId": "2104839654727618562",
"batchNo": "T26-3963",
"batchName": "呼伦贝尔环线 10/06 团",
"batchStatus": "RESOURCE_PREPARING",
"batchStatusName": "资源准备中",
"departDate": "2026-10-06",
"endDate": "2026-10-10",
"serviceDayCount": 5,
"enrolledOrders": 6,
"enrolledPeople": 18,
"requirementConfirmed": false,
"vehicleReady": false,
"dispatchedDayCount": 2,
"dispatchProgress": "PARTIAL",
"dispatchProgressLabel": "部分排车",
"transferPendingCount": 1,
"unreadCount": 3
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"success": true
}
```
#### 空数据 / 降级响应
- 无命中:`records` 为空数组 `[]`,`total` 为 `0`。
- `batchStatusName` 由 order-v3 随团期候选项一起下发,fleet **原样透传、不在本域二次映射**(避免两份字典)。团期状态码为 `null` 时该中文名同为 `null`,后端不编默认文案;前端遇到这一格为 `null` 时请渲染成空白或「—」,不要回填「未知」这类自造文案。
- `dispatchProgressLabel` 与 `dispatchProgress` 在同一次判定里算出,不存在「码与文案分别算出来后对不上」的窗口,二者恒一致。
- `transferPendingCount` 的 `null` 与 `unreadCount` 的 `0` 是两条互相独立的软依赖退化路径,任一退化都不影响本次新增的两个中文名字段。
#### 错误响应
```json
{
"code": 600013,
"message": "排班查询参数非法: dispatchProgress 仅支持 NOT_STARTED/PARTIAL/FULL",
"data": null,
"success": false
}
```
- `600013 排班查询参数非法: {0}`:`dispatchProgress` 取值非法、`keyword` 超长、分页参数越界。
- `600012 团期配车基线不可达,请稍后重试`:团期基线数据读不到;本端点不会用空列表冒充成功。
- `401`:未登录或令牌失效(网关返 HTTP 200 + 信封 `code: 401`)。
#### 业务边界
- 中文名只用于展示。`dispatchProgress` 入参筛选仍只接受码(`NOT_STARTED` / `PARTIAL` / `FULL`),传中文名会按非法值返 600013。
- 团期状态字典是 order-v3 的九态全集(见「六.5」),本列表按「待配车」语义筛选后实际只会出现其中一部分;前端若要构造状态筛选下拉,请从本列表返回值里去重收集,不要按九态硬编码全集。
- `batchStatusName` 与团期管理列表页(order-v3 团期分页)的同名字段来自**同一个**转换方法,两页面上同一个团期的状态文案恒一致。
- 未登记的团期状态码回落成码本身(不抛异常),所以这一格可能出现英文码——前端不需要兜底,但列宽与换行请按可能出现英文码来设计。
---
### 3. 团期配车总览 `GET /admin/fleet/group-dispatch/batches/{groupBatchId}/overview`
**VO**: `Long groupBatchId(路径参数)→ GroupDispatchOverviewRespVO`
#### 使用场景
单个团期的配车总览:逐服务日的车辆卡片 + 本团子订单的用车控制状态。本次在逐车项上补齐派车状态中文名,并澄清 `orders[].vehicleControlStatus` 的读法(#8620)。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | path | Long | 是 | 雪花 ID,正整数 | 运营团期 ID |
#### 出参 `Result<GroupDispatchOverviewRespVO>`
只列与本次变更直接相关的字段;其余字段与本次改动前完全一致。
| 字段 | 类型 | 说明 |
|------|------|------|
| groupBatchId | String | 运营团期 ID(字符串下发) |
| batchNo | String | 团号 |
| days | Array | 逐服务日节点 |
| days[].tripDate | String | 服务日 `YYYY-MM-DD` |
| days[].vehicles | Array | 该日已排车辆项 |
| days[].vehicles[].dispatchId | String | 派车行 ID |
| days[].vehicles[].vehiclePlate | String | 车牌 |
| days[].vehicles[].vehicleModel | String | 车型 |
| days[].vehicles[].driverName | String | 司机姓名 |
| days[].vehicles[].status | String | 派车状态码,取值 `ASSIGNED` / `CONFIRMED`;**未变** |
| days[].vehicles[].statusLabel | String | 🆕 派车状态中文名,与 `status` 恒成对非空:已派车 / 已确认(字典另含 `CANCELLED` 已取消,正常不出现在本列表) |
| days[].vehicleCount | Integer | 该日车辆数 |
| days[].dispatched | Boolean | 该日是否已排车 |
| orders | Array | 本团子订单的用车覆盖情况 |
| orders[].vehicleControlStatus | String | **订单级单值,行程用车与接送机两类共用一格**(#8620 澄清,取值与字段名未变);非 `DONE` 只代表两类里至少一类没齐,说不出是哪一类 |
| orders[].travelRequirementStatus | String | 行程用车需求状态(按类别拆开的字段之一) |
| orders[].transferDeclared | Boolean | 是否声明了接送机 |
| orders[].transferPendingCount | Integer | 该订单接送机未覆盖段数 |
| transferPendingTotal | Integer | 全团接送机未覆盖段数合计 |
#### 请求示例
```http
GET /admin/fleet/group-dispatch/batches/2104839654727618562/overview
Authorization: Bearer {token}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2104839654727618562",
"batchNo": "T26-3963",
"departDate": "2026-10-06",
"endDate": "2026-10-10",
"requirementConfirmed": false,
"vehicleReady": false,
"days": [
{
"tripDate": "2026-10-06",
"vehicles": [
{
"dispatchId": "2104840113194905601",
"vehicleId": "1902233114509312002",
"vehiclePlate": "蒙E13572",
"vehicleModel": "丰田考斯特",
"driverId": "1902233114509312050",
"driverName": "李广宇",
"driverPhone": "13847001234",
"status": "ASSIGNED",
"statusLabel": "已派车",
"remark": null,
"groupCode": "A"
}
],
"vehicleCount": 1,
"dispatched": true
}
],
"missingDates": ["2026-10-09", "2026-10-10"],
"orders": [
{
"orderId": "2104839654727618570",
"orderNo": "HL202610060012",
"teamNo": "T26-3963",
"customerName": "周雅",
"headcount": 4,
"vehicleControlStatus": "PENDING_REVIEW",
"travelRequirementId": "2104839777884160001",
"travelRequirementStatus": "PENDING_REVIEW",
"transferDeclared": true,
"transferPendingCount": 1
}
],
"transferPendingTotal": 1,
"conversationKey": "GROUP_FLEET:2104839654727618562"
},
"success": true
}
```
#### 空数据 / 降级响应
- 某服务日还没排车:`days[].vehicles` 为空数组 `[]`,`vehicleCount` 为 `0`,`dispatched` 为 `false`;该日期同时出现在 `missingDates` 里。
- `vehicles[]` 有项时 `status` 与 `statusLabel` **必定同时非空**(派车行的状态列 NOT NULL);不存在「有码没中文名」的半边状态。
- 团期没有任何子订单时 `orders` 为空数组,`transferPendingTotal` 为 `0`。
#### 错误响应
```json
{
"code": 600012,
"message": "团期配车基线不可达,请稍后重试",
"data": null,
"success": false
}
```
- `600012 团期配车基线不可达,请稍后重试`:团期基线数据读不到;不会用空总览冒充成功。
- `401`:未登录或令牌失效(网关返 HTTP 200 + 信封 `code: 401`)。
#### 业务边界
- `statusLabel` 的字典含三个码(`ASSIGNED` 已派车 / `CONFIRMED` 已确认 / `CANCELLED` 已取消),但本总览只装载未取消的派车行,所以实际只会读到前两个。前端构造状态筛选或图例时按两值即可,不必为「已取消」留位置。
- 🔴 `orders[].vehicleControlStatus` 是**订单级单值**,行程用车与接送机两类共用这一格(#8620)。它非 `DONE` **不能**推断「行程用车没齐」,也不能推断「接送机没齐」——只能推断「至少一类没齐」。要落到具体类别,读 `travelRequirementStatus`(行程用车那一类)与 `transferDeclared` / `transferPendingCount`(接送机那一类)。
- `vehicleControlStatus` 的取值域是 order-v3 的需求状态集:`PENDING` / `PROCESSING` / `DONE` / `PENDING_REVIEW` / `REJECTED_TO_CONSULTANT` / `REJECTED_TO_ADMIN`。本次未新增、未删除取值。
- 本端点的 `statusLabel` 与派车详情等其它读口的派车状态文案同源(同一份枚举字典),不会出现两处对同一状态给不同中文名的情况。
- 中文名不参与任何筛选、排序或统计;`vehicleCount`、`transferPendingTotal`、`missingDates` 的口径本次未变。
## 四、契约约束与正确调用方式(接口类必写)
1. **只增不改**:本次三个端点各只新增字段,没有删除、没有改名、没有取值域变化。前端已有代码不改也不会坏;要拿到中文名才需要改。
2. **中文名与码成对读,成对判空**:`码 == null ⇒ 中文名 == null`、`码 != null ⇒ 中文名 != null`。判「这一格有没有值」只需判其中一个;两个都判是冗余的,但**不要**出现「码为 null 却期待中文名有值」的分支——那条路不存在。
3. **未登记码原样回落成码本身**:四个字典(派单落库态 / 团期生命周期 / 配车进度 / 派车状态)的中文名解析都不抛异常、不返回 `null`。后端将来加码时,前端这一格会显示英文码而不是空白。**所以前端不要写「中文名为空就显示码」的兜底**(永远进不去),但**要**按「这一格可能是英文码」设计列宽与样式。
4. **停用本地映射表**:读到中文名字段后请删掉前端本地那份码 → 中文的表。两份字典并存时,后端加码 = 前端空白,而且没有报错、没有告警,只有用户看到一格空白。
5. **筛选仍传码**:`statuses` / `status` / `dispatchProgress` / `requirementKind` / `orderKind` 一律只接受码。传中文名会按非法值报 100001(看板)或 600013(待配车清单)。
6. **落库态与有效态分清**:要展示「当前状态」用 `assignmentStatusLabel`;要展示「落库真实状态」用 `baseAssignmentStatusLabel`。用后者当状态列会让加急态与陈旧定稿覆写这两类信息全部消失。
7. **`vehicleControlStatus` 不可用于判别类别**(#8620):它是两类共用的单值。要按类别展示或筛选,用 `travelRequirementStatus` / `transferDeclared` / `transferPendingCount`,或走看板清单的 `requirementKind` 维度。
8. **错误信封统一按 `code` 判**:业务失败与入参校验一律 HTTP 200 + 信封 `code`;测试环境网关对失效令牌也返回 HTTP 200 + `code: 401`。只看 HTTP 状态行的拦截器会把「已掉登录」当成功。
## 五、数据库行为
三个端点均为只读查询,本次改动**不涉及任何 DDL 与 DML**:没有新增表、没有新增列、没有 Flyway 脚本、没有写入。新增的中文名字段全部在内存里由枚举字典解析出来,不落库、不参与任何 SQL 过滤或分组,因此既有的按状态码筛选 / 统计的查询路径读数一律不变。
## 六、边界行为
| 场景 | 行为 |
|------|------|
| 状态码为 `null` | 对应中文名同为 `null`,后端不编默认文案 |
| 状态码为未登记的新值 | 中文名回落成码本身,不抛异常、不返回 `null` |
| 订单无落库派单行(虚拟待派卡) | `baseAssignmentStatus` 固定 `unassigned`,`baseAssignmentStatusLabel` 固定「待派车」 |
| 同一行落库态与有效态不同 | 两个中文名不同,属预期(正是本字段的用途),不是数据错误 |
| 派生态(临期加急 / hold 超时) | 只进 `assignmentStatus`;`baseAssignmentStatus` 与其中文名永远是 6 个落库态之一 |
| 团期状态中文名的来源服务读不到 | 该格为 `null`(与码同生同灭),不影响同行其它字段 |
| 团期配车总览里有已取消的派车行 | 不装载进 `days[].vehicles`,所以 `statusLabel` 实际读不到「已取消」 |
| 无命中 / 无数据 | 列表返空数组,不返 `null`;不用空数据冒充成功以外的语义 |
## 六.5、枚举 / 数据字典
**落库派单状态**(`baseAssignmentStatus` → `baseAssignmentStatusLabel`,6 个落库态)
| 码 | 中文名 |
|----|--------|
| unassigned | 待派车 |
| holding | 待确认执行 |
| assigned | 已派车 |
| canceled | 已取消 |
| exception | 异常 |
| completed | 已完结 |
派生态 `unassigned_urgent`(→待派车)与 `holding_urgent`(→待确认执行)只出现在 `assignmentStatus`,**不会**出现在 `baseAssignmentStatus`。
**团期生命周期状态**(`batchStatus` → `batchStatusName`,九态)
| 码 | 中文名 |
|----|--------|
| RECRUITING | 招募中 |
| RESOURCE_PREPARING | 资源准备中 |
| MATERIAL_PREPARING | 物料准备中 |
| PENDING_DEPARTURE | 待出发 |
| TRAVELLING | 出行中 |
| TRIP_FINISHED | 出行完毕 |
| REVIEWING | 核单中 |
| SETTLED | 已结算 |
| CANCELLED | 已取消 |
**配车进度**(`dispatchProgress` → `dispatchProgressLabel`)
| 码 | 中文名 |
|----|--------|
| NOT_STARTED | 未开始 |
| PARTIAL | 部分排车 |
| FULL | 已排满 |
**派车状态**(`vehicles[].status` → `statusLabel`)
| 码 | 中文名 | 是否出现在配车总览 |
|----|--------|------------------|
| ASSIGNED | 已派车 | 是 |
| CONFIRMED | 已确认 | 是 |
| CANCELLED | 已取消 | 否(已取消的派车行不装载进总览) |
**订单级用车控制状态**(`vehicleControlStatus`,本次未改取值,仅澄清读法)
| 码 | 语义 |
|----|------|
| PENDING | 待处理 |
| PROCESSING | 处理中 |
| DONE | 两类都已齐 |
| PENDING_REVIEW | 待审核 |
| REJECTED_TO_CONSULTANT | 已驳回定制师 |
| REJECTED_TO_ADMIN | 已驳回管理员 |
## 六.6、修改前后对比
| 端点 | 字段 | 改动前 | 改动后 |
|------|------|--------|--------|
| `GET /admin/fleet/board/orders` | `records[].baseAssignmentStatusLabel` | 字段不存在(前端只能本地映射 `baseAssignmentStatus`) | 新增,与码恒成对非空 |
| `GET /admin/fleet/group-dispatch/pending-batches` | `records[].batchStatusName` | 字段不存在(只有 `batchStatus` 裸码) | 新增,与码恒成对非空,与团期管理列表页同源 |
| `GET /admin/fleet/group-dispatch/pending-batches` | `records[].dispatchProgressLabel` | 字段不存在(只有 `dispatchProgress` 裸码) | 新增,与码在同一次判定里算出 |
| `GET /admin/fleet/group-dispatch/batches/{groupBatchId}/overview` | `days[].vehicles[].statusLabel` | 字段不存在(只有 `status` 裸码) | 新增,与码恒成对非空 |
| `GET /admin/fleet/group-dispatch/batches/{groupBatchId}/overview` | `orders[].vehicleControlStatus` | 字段与取值相同,但文档未说明它是两类共用的订单级单值 | 字段与取值**完全不变**;文档明确:非 `DONE` 只代表至少一类没齐,判类别须读按类别拆开的字段(#8620) |
## 六.7、影响评估
- **前端必须改的**:无。不改一行也不会坏——四个字段都是新增,既有字段与取值零变化。
- **前端应当改的**:删掉本地的四张码 → 中文映射表,改读后端下发的中文名。收益是后端加码时不再出现静默空白格;不改的风险是本地表与后端字典分叉,且分叉无任何报错信号。
- **前端可能读错的一处**:把 `baseAssignmentStatusLabel` 当成「当前状态」显示在状态列 ⇒ 加急态与陈旧定稿覆写全部丢失。状态列仍应用 `assignmentStatusLabel`。
- **前端可能读错的另一处**(#8620):把 `vehicleControlStatus` 当成「行程用车状态」或「接送机状态」的单一来源 ⇒ 在只报接送机、或只报行程用车的订单上会给出误导性展示。判类别必须读按类别拆开的字段。
- **兼容性**:JSON 新增字段对已有前端反序列化无影响(未知字段忽略 / 多出字段不解析)。响应体每行增大 4 个短字符串量级,分页上限 100 行,体积影响可忽略。
- **无副作用面**:不涉及写入、不涉及事务、不涉及消息、不涉及权限判定,也不改任何筛选与统计口径。
## 七、不影响范围
- 三个端点的**入参**:字段、别名、默认值、校验规则、错误码全部未变。
- 三个端点的**分页、过滤、排序、聚合去重**口径全部未变。
- 三个端点的**既有响应字段**:名称、类型、取值域、语义全部未变,包括 `assignmentStatus` / `assignmentStatusLabel` / `baseAssignmentStatus` / `batchStatus` / `dispatchProgress` / `vehicles[].status` / `orders[].vehicleControlStatus`。
- **错误码**未新增、未删除、未改文案(100001 / 600012 / 600013 / 401)。
- **写接口**:派车提交、派车确认、需求打回等写路径本次一行未改。
- **网关路由**:三个端点都是既有路由,`/admin/fleet/**` 已配置,本次无新增路由。
- **数据库**:无 DDL、无 DML、无 Flyway 脚本。
- **小程序端**:本次改动全部落在管理后台读口,小程序端零影响。
## 八、测试环境已验证
本次改动的可验证面是「码 → 中文名」的映射与成对不变量,已由下列自动化用例覆盖(`hl-fleet-service` + `hl-order-service-v3`):
| 覆盖点 | 用例 |
|--------|------|
| 派车状态三码各返约定中文名(含正常读不到的 `CANCELLED`) | `GroupDispatchStatusTest#labelOf_allDeclaredCodes_returnsChineseLabel` |
| 派车状态:码非空 ⇒ 中文名必非空,且中文名不等于码本身 | `GroupDispatchStatusTest#labelOf_codeNotNull_labelNeverNull` |
| 派车状态:未知码原样回落不抛异常,`null` 返 `null` | `GroupDispatchStatusTest#labelOf_unknownCodeOrNull_fallsBackWithoutThrowing` |
| 配车进度三档各返约定中文名 | `GroupDispatchProgressTest#labelOf_allDeclaredCodes_returnsChineseLabel` |
| 配车进度:码非空 ⇒ 中文名必非空 | `GroupDispatchProgressTest#labelOf_codeNotNull_labelNeverNull` |
| 配车进度:未知码回落、`null` 返 `null`;`isValid` 只认三档 | `GroupDispatchProgressTest#labelOf_unknownCodeOrNull_fallsBackWithoutThrowing`、`#isValid_onlyDeclaredCodes` |
| 待配车清单:团期状态中文名原样透传上游、fleet 不做二次映射 | `GroupDispatchQueryServiceTest`(`#8621` 待配车清单用例) |
| 配车总览逐车项:`status` 与 `statusLabel` 恒成对非空 | `GroupDispatchQueryServiceTest`(`#8621` 逐车项用例) |
| 团期候选项下发状态中文名:与团期列表同一份映射,码非空则中文名必非空;码为 `null` 时中文名同为 `null`、不编默认文案 | `GroupBatchVehicleDispatchQueryServiceTest`(`#8621` 两个用例) |
| 看板订单行:落库态中文名与 `baseAssignmentStatus` 恒成对非空;虚拟待派卡也给中文名 | `BoardOrderServiceTest`(`#8621` 用例) |
## 九、相关历史 PR
- PR #8622(本次):`feat(fleet,order-v3): 派车读口补齐状态中文名,枚举码不再裸下发(#8620 #8621)`。
- #8593:待配车团期清单 `transferPendingCount` 由硬编码 0 改为真值,并引入 `null` = 未取到语义。
- #8518:看板清单 `requirementKind` 入参与 `requirementKindLabel` 出参(同一「后端下发中文名」方向的先例)。
- #7535:枚举中文名由枚举归属服务下发、后缀命名约定(`batchStatusName` 用 `Name` 而非 `Label` 的由来)。
## 十、相关文档
- `docs/CODE_RULES.md` §15.7:对称子域禁镜像重复 / 字典字面量单源——本次四个字段的立项依据。
- `docs/CODE_RULES.md` §3:VO 命名与 `@ApiModelProperty` 约定。
- Swagger:`hl-fleet-service` → `看板` 与 `团期配车` 分组,三个端点的字段注释已同步更新(含 `allowableValues`)。
## 关联 / 联系人
### 链接
- 工单 #8621(补中文名)、#8620(`vehicleControlStatus` 订单级单值口径澄清)
- PR #8622
### 联系人
- 后端:wx
- 前端:mmg(管理后台 hl-ui)
@@ -0,0 +1,256 @@
---
schema: "hl-changelog/v2"
ticket: "8626"
title: "司导往来账账页+明细分页两接口上线:按带团服务人员聚合报账+预支净往来(role 取值含摄影/领队)"
consumer: "admin"
author: "yst(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "hl-admin"
frontend_ref: "1a337d86d556062cd78daf7a94ba3a42ac310dce"
target_release: "v2.1"
verified_at: "2026-09-30"
status_note: "财务往来账新增「司导往来账」两个只读查询接口(/admin/finance/statements/guide-ledger/page 账页 + /entries 明细),按带团服务人员(司导)聚合其报账单+预支单现算净往来,纯查询零 DDL、不动 fin_statement_entry、不接上游流水钩子。口径要点:①司导=带团服务人员(导游 GUIDE/司机 DRIVER/摄影 PHOTOGRAPHER/领队 LEADER,均非内部员工),员工借款 fin_staff_loan 不纳入;②净往来只算已生效报账单(排除 PENDING 未批准/RETURNED 已退回),预支算 APPROVED/PAID;③聚合键=报账人/收款人姓名快照(reporterAssignmentId 与 payeeStaffId 分属两套 ID 空间无法 join);④netBalance 正=司导欠公司、负=公司欠司导。两接口入参/出参/枚举/错误码自 2026-09-30 上线起即为本文口径,无历史版本。前端无既有调用,纯新对接。前端已交付:statement.js 加司导段(API 层 page→pageNo,字典四 role/两 bizType/三 direction/PAID 分文案);新建 current-account/guide 页内 v-if 双视图(账页 netBalance 三态+明细三选一标识原样带回);hiddenRoute 先行(后端 sys_menu 未下挂,预期 component=finance/current-account/guide)。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# finance:司导往来账账页 + 明细分页(管理后台)
**服务**: hl-order-service-v3(finance 模块,同进程)
**PR**: https://git.1814.love/wx/HL/pulls/8574 (账页初版) + https://git.1814.love/wx/HL/pulls/8627 (口径扩摄影/领队)
**Issue**: https://git.1814.love/wx/HL/issues/8555 + https://git.1814.love/wx/HL/issues/8626
---
## 一、接口背景
管理后台「财务 → 往来账」需要一本**司导往来账**:以「带团服务人员(司导)」为单位,把该人的**报账单**(多退少补结算净额)和**预支单**(借款挂账)合并,现算出他当前与公司的净往来余额(谁欠谁、欠多少),并能下钻看每一笔单据。
此前往来账只有供应商一本(`fin_statement_entry`),且只接通应付/预付源;司导/员工的报账、预支走的是另一套表(`fin_reimburse` / `fin_advance`),没有按人聚合的账页。本期新增两个只读接口补齐,**纯查询聚合、零 DDL、不改任何既有账表**。
---
## 二、变更清单
| 项 | 变更 |
|---|---|
| `GET /admin/finance/statements/guide-ledger/page` | **新增**:司导往来账账页分页(按人聚合) |
| `GET /admin/finance/statements/guide-ledger/entries` | **新增**:司导往来明细分页(报账+预支合并,按日期倒序) |
| 出参 `role` 取值集合 | 司导角色快照,取值 ∈ `GUIDE/DRIVER/PHOTOGRAPHER/LEADER`(带团服务人员) |
| 数据库表 | 零 DDL |
> 这两个接口是**首次上线**,前端无既有调用,按新对接处理即可。
---
## 三、接口详情
| 端点 | 方法 | 说明 |
|---|---|---|
| `/admin/finance/statements/guide-ledger/page` | GET | 账页:每个司导一行,聚合其报账净额 + 预支挂账,现算净往来余额 |
| `/admin/finance/statements/guide-ledger/entries` | GET | 明细:某个司导名下的报账单+预支单逐笔列出,按单据日期倒序 |
---
## 四、接口入参
### 4.1 `GET /page`(`GuideLedgerPageReqVO`)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageNo` | int | ✅ | 页码,从 1 开始 |
| `pageSize` | int | ✅ | 每页条数 |
| `keyword` | string | ❌ | 司导姓名(模糊,匹配报账人/收款人姓名快照);空=不限 |
| `onlyOutstanding` | boolean | ❌ | true=只看有往来的司导(净往来≠0 或有未结清单据);默认 false |
> 账页**暂无 role 过滤入参**(角色只在出参 `role` 体现);如需按角色过滤,前端可本地过滤或后续提需求加参。
### 4.2 `GET /entries`(`GuideLedgerEntryReqVO`)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageNo` | int | ✅ | 页码 |
| `pageSize` | int | ✅ | 每页条数 |
| `reporterAssignmentId` | long | 三选一 | 报账人人员分配 ID(账页行 `reporterAssignmentId` 原样带回) |
| `payeeStaffId` | long | 三选一 | 收款人员工 ID(账页行 `payeeStaffId` 原样带回) |
| `reporterName` | string | 三选一 | 司导姓名(账页行 `staffName` 原样带回,精确匹配) |
| `bizType` | string | ❌ | 单据类型过滤:`REIMBURSE` 只看报账 / `ADVANCE` 只看预支;空=两者都要 |
> **司导标识三选一必填**(`reporterAssignmentId` / `payeeStaffId` / `reporterName` 至少填一个,全空 → 596010)。由前端从账页行**原样带回**;匹配规则:ID 精确 或 姓名精确,任一命中即归属该司导。
---
## 五、接口出参
### 5.1 账页行(`GuideLedgerRowRespVO`,`data.records[]`)
| 字段 | 类型 | 说明 |
|---|---|---|
| `reporterAssignmentId` | long(string) | 报账人人员分配 ID 快照(明细下钻回传用;可空) |
| `payeeStaffId` | long(string) | 收款人员工 ID 快照(预支线,明细下钻回传用;可空) |
| `staffName` | string | 司导姓名(聚合键) |
| `role` | string | 角色快照(`GUIDE` 导游 / `DRIVER` 司机 / `PHOTOGRAPHER` 摄影 / `LEADER` 领队;同一人多角色取任一非空;纯预支无报账可空) |
| `reimburseReceivableTotal` | number | 报账应收合计(RECEIVABLE 方向净额合计,司导欠公司) |
| `reimbursePayableTotal` | number | 报账应付合计(PAYABLE 方向净额绝对值合计,公司欠司导) |
| `advanceTotal` | number | 预支挂账合计(APPROVED/PAID 预支金额合计,多退少补待核单轧差) |
| `netBalance` | number | **净往来余额(正=司导欠公司 / 负=公司欠司导 / 0=两清)** |
| `outstandingCount` | int | 未结清单据数(报账未两清终态单数 + 预支在途单数) |
### 5.2 明细行(`GuideLedgerEntryRespVO`,`data.records[]`)
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | long(string) | 单据 ID(REIMBURSE=reimburse_id / ADVANCE=advance_id) |
| `bizType` | string | 单据类型:`REIMBURSE` 报账 / `ADVANCE` 预支 |
| `bizNo` | string | 单号(报账单号 BZ- / 预支单号 YZ-) |
| `orderNo` | string | 团号(订单号快照;可空) |
| `amount` | number | 金额(报账=结算金额 \|净额\|,预支=预支金额,恒正) |
| `direction` | string | 方向:`PAYABLE` 公司欠司导 / `RECEIVABLE` 司导欠公司 / `BALANCED` 两清(预支恒 RECEIVABLE) |
| `status` | string | 单据状态(报账 PENDING/APPROVED/PARTIAL_RECEIVED/PAID/RECEIVED/CLOSED;预支 APPROVED/PAID) |
| `bizDate` | datetime | 单据日期(推送生成时间) |
> 分页结构:`data.records[]` + `data.total` + `data.page` + `data.pageSize`(PageResult,**注意是 `records` 不是 `list`**)。
> 长整型 ID(`reporterAssignmentId`/`payeeStaffId`/`id`)JSON 序列化为字符串,前端按字符串处理防精度丢失。
---
## 六、枚举 / 数据字典
### role 司导角色(快照值,非字典)
`GUIDE` 导游 / `DRIVER` 司机 / `PHOTOGRAPHER` 摄影师 / `LEADER` 领队
> 口径:带团服务人员,**均非内部员工**;其余角色(含内部员工)不进司导往来账。员工借款 fin_staff_loan 不纳入。
### bizType 单据类型
`REIMBURSE` 报账 / `ADVANCE` 预支
### direction 方向
`PAYABLE` 公司欠司导 / `RECEIVABLE` 司导欠公司 / `BALANCED` 两清
### status 单据状态(状态机枚举,各源不同)
- 报账:`PENDING` 待复核 / `APPROVED` 已批准 / `PARTIAL_RECEIVED` 部分回款 / `PAID` 已付讫 / `RECEIVED` 已回款 / `CLOSED` 已两清(`RETURNED` 已退回不计入往来)
- 预支:`APPROVED` 已批准 / `PAID` 已付款
---
## 七、错误码
| 码 | 含义 |
|---|---|
| 596010 | 司导标识缺失(entries 三选一全空) |
---
## 八、示例
### 8.1 账页(典型)
```
GET /admin/finance/statements/guide-ledger/page?pageNo=1&pageSize=10
```
```json
{
"code": 200,
"data": {
"records": [
{
"reporterAssignmentId": null,
"payeeStaffId": "2100747615736897537",
"staffName": "刘大山",
"role": null,
"reimburseReceivableTotal": 0,
"reimbursePayableTotal": 0,
"advanceTotal": 260.00,
"netBalance": 260.00,
"outstandingCount": 1
}
],
"total": 1, "page": 1, "pageSize": 10
}
}
```
### 8.2 明细分页(按收款人 ID 下钻)
```
GET /admin/finance/statements/guide-ledger/entries?pageNo=1&pageSize=10&payeeStaffId=2100747615736897537
```
```json
{
"code": 200,
"data": {
"records": [
{ "id": "2201...", "bizType": "ADVANCE", "bizNo": "YZ-202609290001",
"orderNo": null, "amount": 260.0, "direction": "RECEIVABLE",
"status": "APPROVED", "bizDate": "2026-09-29T10:00:00" }
],
"total": 1, "page": 1, "pageSize": 10
}
}
```
### 8.3 业务失败(entries 三选一全空 → 596010)
```
GET /admin/finance/statements/guide-ledger/entries?pageNo=1&pageSize=10
```
```json
{ "code": 596010, "message": "司导标识缺失" }
```
---
## 九、业务边界
- **纯查询聚合**:两接口只读,不写任何表、不改 fin_statement_entry、不接上游流水钩子;净往来为现算不落列。
- **只算生效单**:报账单排除 PENDING(未批准不生效)与 RETURNED(已退回,由 version_no+1 新单承接防双算);预支算 APPROVED/PAID。
- **聚合键=姓名快照**:reporterAssignmentId(order 域人员分配 ID)与 payeeStaffId(员工 ID)分属两套 ID 空间无法 join,故按报账人/收款人姓名快照聚合;姓名缺失时退化按 ID 单列。
- **净额方向**:netBalance 正=司导欠公司、负=公司欠司导、0=两清。报账 PAYABLE 方向(公司欠司导)取净额绝对值计入 reimbursePayableTotal。
- **不含员工借款**:司导/摄影/领队均非内部员工,fin_staff_loan 不纳入。
---
## 十、修改前后对比
新增接口,无修改前版本。
| 维度 | 说明 |
|---|---|
| 接口 | 首次上线,前端无既有调用 |
| role 取值 | 上线即为 4 角色(GUIDE/DRIVER/PHOTOGRAPHER/LEADER),无历史 2 角色版本对外暴露 |
---
## 十一、影响评估 / 回滚
- 后端:新增两个只读端点,零 DDL、零既有逻辑改动;回滚=下线两接口(前端无依赖)。
- 前端:纯新对接,无回归面。
- 数据库:无变更。
---
## 十二、注意事项
- 分页结构是 `data.records[]`(PageResult),**不是 `list`**,前端取数组时注意。
- 长整型 ID 序列化为字符串,按字符串处理。
- entries 下钻时,账页行的 `reporterAssignmentId`/`payeeStaffId`/`staffName` **原样带回**即可(三选一,无需自己拼)。
- `role` 可能为 null(纯预支无报账的司导),前端渲染时对 null 做兜底(显示「-」或留空),不要假设非空。
---
## 十三、关联 / 联系人
### 13.1 链接
- 账页初版 PR:https://git.1814.love/wx/HL/pulls/8574
- 口径扩展 PR:https://git.1814.love/wx/HL/pulls/8627
- Issue:https://git.1814.love/wx/HL/issues/8555 、https://git.1814.love/wx/HL/issues/8626
### 13.2 联系人
- 后端 / 财务域:yst(腰苏图)
@@ -0,0 +1,432 @@
---
schema: "hl-changelog/v2"
ticket: "8629"
title: "出行人/大交通批次单条新增:幂等键补身份段,同订单连续录入不同对象不再被误拦"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "hl-order-service-v3 dev-v3 提交 91a52ba46c;两个端点的 @Idempotent 幂等键从「仅订单 ID」改为「订单 ID + 出行人/批次身份摘要」,请求体与响应体结构均未变。测试服验证:同订单并发提交两个不同出行人(间隔 150ms)均返回 200 各自创建成功;同订单并发提交两次完全相同的出行人(间隔 150ms)后一条返回 code=100502。;前端实证:hl-admin 两表单零幂等 workaround,不持有不拼接幂等键,结构零变化判 not_required(hl-admin sync-log 2026-10-01)"
updated_at: "2026-09-30"
base: "dev-v3"
---
# 出行人/大交通批次单条新增:幂等键补身份段,同订单连续录入不同对象不再被误拦
> **存放目录**: 二期(order-v3/fleet)→ `changelogs-v2/2026-09/`
>
> **服务**: hl-order-service-v3
> **PR**: [#8637](https://git.1814.love:8443/wx/HL/pulls/8637)
> **Issue**: [#8629](https://git.1814.love:8443/wx/HL/issues/8629)
> **日期**: 2026-09-30
> **影响范围**: 管理后台订单详情页「出行人」「大交通」两个单条新增表单
---
## ⚠️ 关键变化
**改前**:`POST /v3/admin/order/{id}/traveler/add` 与 `POST /v3/admin/order/{id}/transport-plan/add` 的幂等键只拼订单 ID(`#id`),3 秒窗口内**同订单任意两次提交**都会被判定为重复请求而拦截——哪怕两次提交的是完全不同的两个人、或完全不同的两个大交通批次。定制师连续为一家人逐个录入出行人、或逐个补录到达/返程批次时,第二个请求大概率落在 3 秒窗口内,被误拦为「同一出行人/批次刚已提交,请勿重复提交」,且该提示恒为假(首次请求早已成功结束,不是「处理中」)。
**改后**:幂等键改为「订单 ID + 该对象的身份摘要」(出行人:姓名+证件号+出生日期+手机号;大交通批次:方向+交通类型+车次/航班号+出发时间+自驾时段+关联出行人集合,排序后取 SHA-256)。**身份任一字段不同即视为不同对象,两次提交各自成功,前端无需人为在两次提交间插入延时**;只有身份完全相同的重复提交才会在 3 秒窗口内被拦。请求体、响应体结构均未变,也未新增/删除任何字段。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 单个出行人新增 | POST | `/v3/admin/order/{id}/traveler/add` | 幂等键收窄 | 幂等键加入出行人身份摘要,不同人不再互相拦截 |
| 2 | 大交通批次新增 | POST | `/v3/admin/order/{id}/transport-plan/add` | 幂等键收窄 | 幂等键加入批次身份摘要,不同批次不再互相拦截 |
---
## 三、接口详情
### 1. 单个出行人新增 `POST /v3/admin/order/{id}/traveler/add`
**VO**: `TravelerCreateReqVO → TravelerVO`
#### 使用场景
管理后台订单详情页「出行人」页签,定制师为已创建的订单逐个补录出行人档案时调用;与批量编辑接口(`/traveler/batch-edit`)并列存在,用于单个补录/追加场景,例如客户临时增加一名随行儿童。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 订单 ID |
| name | Body | String | - | - | 姓名(可后填) |
| gender | Body | String | - | 1=男/2=女/0=未知 | 性别 |
| birthday | Body | LocalDate | ✅ | 不得晚于今天 | 出生日期,后端按年龄分段自动派生 `travelerType` |
| idType | Body | String | - | 取值见数据字典 `id_card_type` | 证件类型 |
| idNo | Body | String | - | - | 证件号(明文传,DB 加密) |
| nationality | Body | String | - | - | 国籍(默认中国) |
| race | Body | String | - | - | 民族(默认汉族) |
| phone | Body | String | - | - | 出行人手机(明文传,DB 加密) |
| emergencyContact | Body | String | - | - | 紧急联系人姓名 |
| emergencyPhone | Body | String | - | - | 紧急联系人电话(明文传) |
| roomGroupNo | Body | Integer | - | - | 同住分组号 |
#### 出参 `Result<TravelerVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 出行人 ID |
| orderId | Long | 订单 ID |
| teamNo | String | 团号 |
| travelerType | String | 出行人类型:ADULT/CHILD/YOUNG_CHILD/BABY |
| travelerTypeName | String | 出行人类型中文名(字典优先,枚举 label 兜底) |
| name | String | 姓名 |
| gender | String | 性别:1=男/2=女/0=未知 |
| birthday | LocalDate | 出生日期 |
| idType | String | 证件类型 |
| idTypeName | String | 证件类型中文名(字典优先,枚举 label 兜底) |
| idCardMasked | String | 证件号脱敏值(前3后4,中间星号,#2894) |
| idProvinceCode | String | 身份证省级行政区代码;非大陆证件或无法识别为 null |
| idProvinceName | String | 身份证省级行政区名称 |
| nativePlace | String | 所属地(省+地级市,#5642);非大陆证件或未命中为 null |
| nationality | String | 国籍 |
| race | String | 民族 |
| phoneMasked | String | 出行人手机脱敏值 |
| emergencyContact | String | 紧急联系人姓名 |
| emergencyPhoneMasked | String | 紧急联系人电话脱敏值 |
| roomGroupNo | Integer | 同住分组号 |
| profileStatus | String | 资料完善状态:PENDING/COMPLETED |
| transportPlanIds | List\<Long\> | 关联大交通批次 ID 列表 |
#### 请求示例
```json
{
"name": "王小明",
"gender": "1",
"birthday": "2018-06-20",
"idType": "ID_CARD",
"idNo": "220103201806201234",
"nationality": "中国",
"race": "汉族",
"phone": "13812342046",
"emergencyContact": "王大明",
"emergencyPhone": "13988888888",
"roomGroupNo": 1
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": 70123456789012,
"orderId": 60123456789012,
"teamNo": "26-0480",
"travelerType": "ADULT",
"travelerTypeName": "成人",
"name": "张三",
"gender": "1",
"birthday": "1985-08-12",
"idType": "ID_CARD",
"idTypeName": "身份证",
"idCardMasked": "220***********1234",
"idProvinceCode": "22",
"idProvinceName": "吉林省",
"nativePlace": "内蒙古呼伦贝尔市",
"nationality": "中国",
"race": "汉族",
"phoneMasked": "138****2046",
"emergencyContact": "李四",
"emergencyPhoneMasked": "139****8888",
"roomGroupNo": 1,
"profileStatus": "COMPLETED",
"transportPlanIds": []
},
"traceId": null,
"success": true
}
```
#### 空数据 / 降级响应
本接口是单条创建接口,成功路径必返回完整的单个对象,不存在「空列表」场景。`travelerTypeName` / `idTypeName` 的中文名解析是「字典优先,枚举 label 兜底」——字典服务不可用时回落到枚举自带中文 label,不会返回 `null`(该兜底策略是既有行为,非本次改动)。
#### 错误响应
```json
{
"code": 100502,
"message": "同一出行人刚已提交,请勿重复提交",
"data": null,
"traceId": null,
"success": false
}
```
#### 业务边界
- 3 秒幂等窗口内,仅当出行人身份(姓名+证件号+出生日期+手机号)与前一次提交**完全相同**时才会被拦为 `code=100502`;姓名/证件号/出生日期/手机号任一不同即视为不同的人,两次提交各自成功,前端无需人为在两次提交间插入延时。
- 幂等身份摘要取 SHA-256(64 位小写 hex),不改变请求体字段本身;前端提交的仍是原始明文字段。
- 同订单另有 30 秒 `@Lock4j` 互斥锁(本次未变),用于防止与批量编辑接口(`/traveler/batch-edit`)并发写导致人数计数错乱;锁只管互斥,幂等键只管拦「同一次提交的重放」,两者不可互相替代。
- 成功响应 `code` 固定为 **200**(非 0);若前端用 `code === 0` 判断成功,会把这次成功误判为失败。
- 未登录 → 网关层拦截,返回 401 信封;`birthday` 缺失 → HTTP 200 + `code: 400`(`@NotNull`/`@PastOrPresent` 校验失败)。
---
### 2. 大交通批次新增 `POST /v3/admin/order/{id}/transport-plan/add`
**VO**: `TransportPlanReqVO → TransportPlanVO`
#### 使用场景
管理后台订单详情页「大交通」页签,定制师为订单逐个新增到达/返程批次时调用(每个批次至少关联 1 名出行人);与批量替换接口(`/transport-plan/batch`)并列存在,用于「多批到达/返程」场景下逐批补录。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 订单 ID |
| direction | Body | String | ✅ | ARRIVAL / DEPARTURE | 方向 |
| transportType | Body | String | ✅ | FLIGHT / TRAIN / SELF_DRIVE | 交通类型 |
| transportNo | Body | String | - | SELF_DRIVE 时为空 | 航班号/车次号 |
| carrier | Body | String | - | - | 航司/铁路公司 |
| departStation | Body | String | - | - | 出发站 |
| arriveStation | Body | String | - | - | 到达站 |
| departTime | Body | LocalDateTime | - | FLIGHT/TRAIN 必填 | 出发时间 |
| arriveTime | Body | LocalDateTime | - | FLIGHT/TRAIN 必填 | 到达时间 |
| selfDrivePeriod | Body | String | - | MORNING/AFTERNOON/EVENING,仅 SELF_DRIVE | 自驾时段 |
| selfDriveEta | Body | LocalDateTime | - | 仅 SELF_DRIVE 可选 | 自驾预计到达时间 |
| travelerIds | Body | List\<Long\> | ✅ | 至少 1 个 | 关联出行人 ID |
| pickupRequired | Body | Boolean | - | 新增缺省 true | 是否需要接送 |
| pickupRemark | Body | String | - | - | 接送备注 |
| remark | Body | String | - | ≤500 字 | 备注 |
#### 出参 `Result<TransportPlanVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String | 批次 ID(`Long` 按字符串序列化) |
| orderId | String | 订单 ID(`Long` 按字符串序列化) |
| teamNo | String | 团号 |
| direction | String | 方向:ARRIVAL/DEPARTURE |
| directionLabel | String | 方向中文标签 |
| mode | String | 模式:TOGETHER/SEPARATE;单条新增固定为 TOGETHER |
| modeLabel | String | 模式中文标签 |
| transportType | String | 交通类型:FLIGHT/TRAIN/SELF_DRIVE |
| transportTypeLabel | String | 交通类型中文标签 |
| transportNo | String | 航班号/车次号 |
| carrier | String | 航司/铁路公司 |
| departStation | String | 出发站 |
| arriveStation | String | 到达站 |
| departTime | LocalDateTime | 出发时间 |
| arriveTime | LocalDateTime | 到达时间 |
| selfDrivePeriod | String | 自驾时段 |
| selfDrivePeriodLabel | String | 自驾时段中文标签 |
| selfDriveEta | LocalDateTime | 自驾预计到达时间 |
| pickupRequired | Boolean | 是否需要接送 |
| pickupRemark | String | 接送备注 |
| travelers | List\<TravelerRef\> | 关联出行人(id 字符串化 + name + travelerType + travelerTypeName) |
| remark | String | 备注 |
| creatorType | String | 创建来源:USER/ADMIN |
| creatorTypeName | String | 创建来源中文名 |
| createTime | LocalDateTime | 创建时间 |
| updateTime | LocalDateTime | 更新时间 |
#### 请求示例
```json
{
"direction": "ARRIVAL",
"transportType": "FLIGHT",
"transportNo": "CA1234",
"carrier": "中国国际航空",
"departStation": "北京首都T3",
"arriveStation": "长春龙嘉",
"departTime": "2026-06-01T08:30:00",
"arriveTime": "2026-06-01T10:15:00",
"travelerIds": [70123456789012, 70123456789013],
"pickupRequired": true,
"pickupRemark": "需在 T3 出口举牌接机",
"remark": "需要接机举牌"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "80012345",
"orderId": "60123456789012",
"teamNo": "26-0480",
"direction": "ARRIVAL",
"directionLabel": "到达",
"mode": "TOGETHER",
"modeLabel": "一起到达",
"transportType": "FLIGHT",
"transportTypeLabel": "飞机",
"transportNo": "CA1234",
"carrier": "中国国际航空",
"departStation": "北京首都T3",
"arriveStation": "长春龙嘉",
"departTime": "2026-06-01T08:30:00",
"arriveTime": "2026-06-01T10:15:00",
"selfDrivePeriod": null,
"selfDrivePeriodLabel": null,
"selfDriveEta": null,
"pickupRequired": true,
"pickupRemark": "需在 T3 出口举牌接机",
"travelers": [
{ "id": "70123456789012", "name": "张三", "travelerType": "ADULT", "travelerTypeName": "成人" }
],
"remark": "需要接机举牌",
"creatorType": "ADMIN",
"creatorTypeName": "定制师代录",
"createTime": "2026-07-01T10:00:00",
"updateTime": "2026-07-01T10:30:00"
},
"traceId": null,
"success": true
}
```
#### 空数据 / 降级响应
本接口是单条创建接口,成功路径必返回完整的单个对象,不存在「空列表」场景。`travelers[].travelerTypeName` 同样是「字典优先,枚举 label 兜底」,字典服务不可用时回落枚举 label,不返回 `null`(既有行为,非本次改动)。
#### 错误响应
```json
{
"code": 100502,
"message": "同一大交通批次刚已提交,请勿重复提交",
"data": null,
"traceId": null,
"success": false
}
```
#### 业务边界
- 3 秒幂等窗口内,仅当批次身份(方向+交通类型+车次/航班号+出发时间+自驾时段+关联出行人集合)与前一次提交**完全相同**时才会被拦为 `code=100502`;任一字段不同即视为不同批次,两次提交各自成功。
- `travelerIds` 参与身份摘要前会先排序去重:前端把同一批 ID 换个顺序重新提交,仍会命中同一幂等键(业务上视为同一次提交的重放)。
- SELF_DRIVE 场景 `transportNo` 为空、`departTime` 也非必填,身份摘要因此额外纳入 `transportType`/`selfDrivePeriod`/`travelerIds`,避免两批不同自驾到达被误判为同一批。
- 同订单另有 30 秒 `@Lock4j` 互斥锁(本次未变),用于防止 admin 多端并发新增同方向 plan;锁与幂等键职责不同、不可互相替代。
- 成功响应 `code` 固定为 **200**(非 0);若前端用 `code === 0` 判断成功,会把这次成功误判为失败。
- 未登录 → 网关层拦截,返回 401 信封;`direction`/`transportType` 缺失或 `travelerIds` 为空 → HTTP 200 + `code: 400`。
---
## 四、契约约束与正确调用方式
> 本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | 结果 |
|------|------|
| ✅ 同订单连续新增两个不同出行人(`name`/`idNo`/`birthday`/`phone` 任一不同) | 两次请求均返回 200,各自创建成功,无需人为延时 |
| ✅ 同订单连续新增两个不同大交通批次(身份字段任一不同) | 两次请求均返回 200,各自创建成功 |
| ❌ 3 秒内重复提交身份字段完全相同的出行人 | 第二次返回 `code=100502`,零写入 |
| ❌ 3 秒内重复提交身份字段完全相同的大交通批次 | 第二次返回 `code=100502`,零写入 |
### 切换状态时的必要动作
两个接口均为单条创建接口,不涉及状态切换;调用前无需额外前置动作。
---
## 五、数据库行为
| 场景 | 写入行为 |
|------|----------|
| 同订单连续提交两个不同出行人 | 各自插入 1 行 `order_traveler`;改动前,第二个请求会被幂等键拦截,零写入 |
| 同订单 3 秒内重复提交同一出行人(身份完全相同) | 只有首次请求落 1 行;重复请求被拦截,零写入(改动前后一致) |
| 同订单连续提交两个不同大交通批次 | 各自插入 1 行 `order_transport_plan` + N 行桥接表 `order_transport_plan_traveler`;改动前,第二个请求会被拦截,零写入 |
| 同订单 3 秒内重复提交同一批次(身份完全相同) | 只有首次请求写入;重复请求被拦截,零写入(改动前后一致) |
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 校验失败(`birthday` 缺失、`direction`/`transportType` 非法、`travelerIds` 为空等) → HTTP 200 + `code: 400`
- 3 秒窗口内同身份重复提交 → HTTP 200 + `code: 100502`,零写入
- 下游字典服务(`travelerTypeName`/`idTypeName`/`transportTypeLabel` 等中文名解析)降级 → 回落枚举自带中文 label,不返回 null、不阻断主流程(既有行为,非本次改动)
---
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| (无) | 请求体、响应体字段结构均未变 | 同左 |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 幂等键组成 | `#id`(仅订单 ID) | `#id + ':' + 身份摘要`(订单 ID + SHA-256 身份摘要) |
| 同订单连续提交两个不同对象(3 秒内) | 第二次被拦为 `code=100502`,零写入,且提示「处理中」恒为假 | 两次均成功,各自写入 1 条记录 |
| 3 秒内重复提交同一对象(身份完全相同) | 拦截 | 拦截(无变化) |
| 错误提示文案 | 「同一出行人刚已提交,请勿重复提交」/「同一大交通批次刚已提交,请勿重复提交」(改动前后文案一致,仅拦截范围变窄) | 同左 |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 否——请求体、响应体字段结构未变,只是原本被误拦的场景(连续提交不同对象)现在能成功,属于放宽约束
- **前端是否必须同步上线**: 否——若前端此前为规避误拦而在两次提交之间人为加了延时或做了排队逻辑,那段代码可以撤掉,但不撤也不会出错
- **前端 workaround 清理点**: 若前端曾针对「连续录入第二个出行人/批次报 100502」做过特殊重试或提示遮蔽逻辑,现在可以移除;不清理也不影响功能,只是不再需要
---
## 七、不影响范围
- **仅影响**: `POST /v3/admin/order/{id}/traveler/add`、`POST /v3/admin/order/{id}/transport-plan/add` 两个端点的幂等拦截范围
- **零影响**:
- 出行人批量编辑接口 `/traveler/batch-edit`(幂等键仍是 `#id`,未变)
- 大交通批次编辑/软删/批量替换接口 `/transport-plan/{planId}/edit`、`/transport-plan/{planId}/delete`、`/transport-plan/batch`(幂等键均未变)
- 出行人软删、信息校验、补全出行信息等其余出行人/大交通端点
- 请求体、响应体字段结构(未新增/删除/改类型任何字段)
- 同订单 30 秒 `@Lock4j` 互斥锁行为
---
## 八、测试环境已验证
测试服(hl-order-service-v3,dev-v3 提交 `91a52ba46c`)实测:
```
POST /v3/admin/order/{id}/traveler/add 同订单并发提交两个不同出行人(间隔 150ms) → 均 200,各自创建成功 ✓
POST /v3/admin/order/{id}/traveler/add 同订单并发提交两次完全相同的出行人(间隔 150ms) → 先 200,后一条 code=100502 ✓
```
---
## 十、相关文档
- 关联 Issue: [wx/HL#8629](https://git.1814.love:8443/wx/HL/issues/8629)
- 关联 PR: [wx/HL#8637](https://git.1814.love:8443/wx/HL/pulls/8637)
## 关联 / 联系人
### 链接
- **Issue**: [#8629](https://git.1814.love:8443/wx/HL/issues/8629)
- **PR**: [#8637](https://git.1814.love:8443/wx/HL/pulls/8637)
- **Merge commit**: [91a52ba46c](https://git.1814.love:8443/wx/HL/commit/91a52ba46c)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,148 @@
---
schema: "hl-changelog/v2"
ticket: "8630"
title: "团期活跃子订单清零后,自动复位团级需求确认与整团用车需求"
consumer: "admin"
author: "wx(GIT)"
change_type: "修复"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "hl-order-service-v3 dev-v3 合入提交 8e00cc98bc(PR #8640,同 PR 还含 #8631 的两处纯 javadoc 订正,与本单契约无关,未在本文档中提及)。本次改动不涉及任何接口的请求/响应结构变化,只影响既有字段在特定场景下的取值。;前端实证:StatusLogsPanel 通用 eventTypeName 渲染天然覆盖(复用既有事件类型),无新枚举/新字段,纯取值变化判 not_required(hl-admin sync-log 2026-10-01)"
updated_at: "2026-09-30"
base: "dev-v3"
---
# 团期活跃子订单清零后,自动复位团级需求确认与整团用车需求
> **存放目录**: 二期(order-v3/fleet)→ `changelogs-v2/2026-09/`
>
> **服务**: hl-order-service-v3
> **PR**: [#8640](https://git.1814.love:8443/wx/HL/pulls/8640)
> **Issue**: [#8630](https://git.1814.love:8443/wx/HL/issues/8630)
> **日期**: 2026-09-30
> **影响范围**: 团期详情页「需求确认」状态、整团用车需求状态;不涉及任何请求/响应字段结构变化
---
## ⚠️ 关键变化
**改前**:一个团期整体确认需求(`requirementConfirmed=true`)、且整团用车需求也已确认(`CONFIRMED`)之后,如果该团期名下的子订单被逐个取消,直到**最后一个活跃子订单也被取消**,系统没有任何收尾动作——`requirementConfirmed` 停留在 `true`,整团用车需求状态停留在 `CONFIRMED`。此时团期详情页、车务待办侧仍显示「需求已确认」「用车已确认」,而这个团实际上已经一户不剩,且这个不一致**不报错、不告警**,只能靠人工发现。
**改后**:当一个团期的活跃子订单数(`order_status != CANCELLED` 的子订单条数)由非零变为零时,系统自动做两件事:
1. 若整团用车需求当前处于 `CONFIRMED`,自动退回 `DRAFT`(不是 `PENDING_RECONFIRM`);
2. 若团级 `requirementConfirmed` 当前为 `true`,自动清为 `false`。
只要这两项里至少有一项真的发生了状态变化,就会在该团期的时间线写入一条 `BATCH_REQUIREMENT_REOPENED`(需求重开待确认)事件,`extra` 中携带 `trigger: "ALL_SUB_ORDERS_CANCELLED"`、`orderId`(触发收尾的最后一个取消子订单 ID)、`requirementConfirmedCleared`(布尔)、`groupVehicleRequirementWithdrawn`(布尔)。两项复位互相独立、各自按条件写,天然幂等;两项都无需变化时(例如本来就是 `false`/`DRAFT`)不写时间线、不产生噪声。
本次改动**不涉及任何请求体或响应体的字段增删/改类型**,纯粹是既有字段在「团期归零」这一新增场景下会被系统自动改写取值。
---
## 二、影响的字段与读取入口
以下字段的**读取路径未变**,本次改动只影响它们在「团期活跃子订单清零」这一时刻之后的取值:
| 字段 | 归属接口(示例) | 改前在团期归零后的取值 | 改后 |
|------|------|------|------|
| `requirementConfirmed` | `GET /v3/admin/order/group-batch/{groupBatchId}`(`GroupBatchDetailRespVO`)及房务看板系列 VO | 停留在归零前的最后取值(可能仍是 `true`) | 若归零前为 `true`,归零后自动变为 `false` |
| 整团用车需求 `status` | 整团用车需求相关读端点(`GroupVehicleRequirementRespVO.status`) | 停留在归零前的最后取值(可能仍是 `CONFIRMED`) | 若归零前为 `CONFIRMED`,归零后自动变为 `DRAFT` |
| 团期时间线 | 团期时间线读端点 | 归零无任何留痕 | 新增一条 `BATCH_REQUIREMENT_REOPENED` 事件(仅当至少一项真的被复位时才写) |
---
## 三、精确触发条件
- **"活跃子订单"的判据** = `order_status != CANCELLED`(含 `PENDING_PAY`、`COMPLETED` 等非取消状态均计入活跃;与既有人数计数、归团回填、对账口径一致)。**不是**看板上「免闸户不计」的统计口径——房务免闸的户在用车这一侧仍可能要车,因此不按闸门维度扣减分母。
- **收尾时点** = 该团期的活跃子订单数由非零变为零的那一刻(子订单取消事务提交之后)。覆盖以下所有取消入口:admin 出行前取消、C 端取消、退团审批、流团逐户取消、通用 `transition()` 的 CANCEL 分支、超时自动取消——这些入口最终都汇流到同一个内部取消事件,因此逐个入口都会触发本收尾逻辑。
- **不覆盖**的取消路径:
- `TERMINATE`(出行中终止行程 → `COMPLETED`)是与 `CANCEL`(→ `CANCELLED`)完全不同的状态路径,不会触发本收尾——出行中终止行程不代表这个团没有人,语义上也不应该清需求确认。
- 直接修改数据库、绕过应用层的取消不会触发(无代码路径可挂载)。
- **用车需求只在 `CONFIRMED` 这一档被自动退回**:`DRAFT`/`PENDING_RECONFIRM` 本来就不是已确认,无需处理;`DISPATCHED`(已发车务)/`DONE`(配车完成)**不会**被自动撤回——车务可能已经接单甚至配完车,自动撤回等于单方面掀掉车务在办的工作,这属于另一个业务决策,本次不做;`CANCELLED` 是流团终态,不会走到本路径。
---
## 六、边界行为(刻意不做的部分)
- **`batchStatus`(团期阶段)本次不变**:一个活跃子订单数归零的团期,其 `batchStatus` 可以继续停留在任意阶段(例如 `RESOURCE_PREPARING`「资源准备中」),不会被自动置为 `CANCELLED` 或退回 `RECRUITING`——自动改阶段涉及流团审批合规性判断,留给后续工单单独定案。前端据此判断"团是否还有效"时,**不能只看 `batchStatus`**,需要结合活跃子订单数或 `requirementConfirmed`/用车需求状态的复位来综合判断。
- **`DISPATCHED`/`DONE` 的用车需求不会被回退**:见上节"三、精确触发条件"。
- **房务就绪标记(`hotel_ready`)本次不动**:本收尾只处理 `requirementConfirmed` 与整团用车需求两项,房务侧的就绪标记不在本次收尾范围内,两者目前不对称——这是已知缺口,不在本单范围内一并解决。
- **不做历史数据回填**:已经处于"团期归零但需求确认/用车需求未复位"这种旧脏数据状态的历史团期,本次改动不会自动纠正,只对本次改动上线之后新发生的"归零"事件生效。
---
## 六.5、枚举 / 数据字典
整团用车需求状态 `GroupVehicleRequirementStatus`(本次改动涉及的部分状态,完整枚举 6 值):
| 值 | 中文名 | 说明 |
|------|------|------|
| DRAFT | 草稿 | 本次改动的复位目标 |
| CONFIRMED | 已确认 | 本次改动的复位起点(仅此档会被自动退回) |
| PENDING_RECONFIRM | 待重新确认 | 不受本次改动影响 |
| DISPATCHED | 已发车务 | 不受本次改动影响(明确不回退) |
| DONE | 配车完成 | 不受本次改动影响(明确不回退) |
| CANCELLED | 已取消 | 不受本次改动影响 |
团期时间线事件类型(本次涉及):
| 值 | 中文名 | 说明 |
|------|------|------|
| BATCH_REQUIREMENT_REOPENED | 需求重开待确认 | 复用既有事件类型(此前用于"定制师在整团确认后自行改需求"场景),本次新增一种触发来源;两种来源在 `extra.trigger` 字段区分,本次新增值为 `ALL_SUB_ORDERS_CANCELLED` |
---
## 六.6、修改前后对比
### 行为级对比
| 场景 | 改前 | 改后 |
|------|------|------|
| 团期活跃子订单数由非零变为零,此前 `requirementConfirmed=true` | 停留 `true`,无提示 | 自动变为 `false` |
| 团期活跃子订单数由非零变为零,此前整团用车需求 `CONFIRMED` | 停留 `CONFIRMED`,无提示 | 自动退回 `DRAFT` |
| 团期活跃子订单数由非零变为零,此前两项均已是"未确认"状态 | 无变化 | 无变化,不写时间线(幂等,无噪声) |
| 团期时间线 | 归零无任何记录 | 至少一项被复位时,新增一条 `BATCH_REQUIREMENT_REOPENED` 事件,`extra.trigger=ALL_SUB_ORDERS_CANCELLED` |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 否——字段名称、类型、接口路径均未变,只是取值在新场景下会被后端自动改写
- **前端是否必须同步上线**: 视前端现有逻辑而定——若前端曾假设"一旦确认过就不会自动变回未确认"并据此做过缓存/跳过重复请求之类的优化,需要重新核对该假设在"团期归零"场景下不再成立
- **需要前端注意的读取口径变化**: `requirementConfirmed`、整团用车需求 `status` 在团期活跃子订单归零后可能被系统自动改写,不再只由人工操作(确认/打回)改变;`batchStatus` 不受此次自动复位联动,读取时不能用 `batchStatus` 代替对这两个字段的直接读取
---
## 七、不影响范围
- **仅影响**: 团期活跃子订单数归零这一时刻,`requirementConfirmed` 与整团用车需求 `CONFIRMED` 状态的自动复位
- **零影响**:
- 团期阶段 `batchStatus` 的取值与流转规则(见"六、边界行为")
- 房务就绪标记 `hotel_ready`
- 整团用车需求 `DISPATCHED`/`DONE`/`PENDING_RECONFIRM`/`CANCELLED` 四档的自动流转规则
- 所有接口的请求体、响应体字段结构(本次零新增、零删除、零改类型)
- 团期归零之前已经存在的历史脏数据(不做回填)
- 受控重配窗口相关的 `BATCH_VEHICLE_REQUIREMENT_REOPENED` 事件(另一独立事件类型,与本次复用的 `BATCH_REQUIREMENT_REOPENED` 不是同一个)
---
## 十、相关文档
- 关联 Issue: [wx/HL#8630](https://git.1814.love:8443/wx/HL/issues/8630)
- 关联 PR: [wx/HL#8640](https://git.1814.love:8443/wx/HL/pulls/8640)
## 关联 / 联系人
### 链接
- **Issue**: [#8630](https://git.1814.love:8443/wx/HL/issues/8630)
- **PR**: [#8640](https://git.1814.love:8443/wx/HL/pulls/8640)
- **Merge commit**: [8e00cc98bc](https://git.1814.love:8443/wx/HL/commit/8e00cc98bc)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,231 @@
---
schema: "hl-changelog/v2"
ticket: "8632"
title: "团期核单新增 8 个分类科目明细只读端点(住宿/景区门票/车辆/导游/摄影/餐食/其他支出/其他收入)"
consumer: "admin"
author: "yst(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "d5a372be4a2079adf9c1ca91a68b992f92918507"
target_release: "v2.1"
verified_at: "2026-09-30"
base: "dev-v3"
updated_at: "2026-09-30"
status_note: "团期核单弹窗补 8 个分类明细 tab 的只读端点,命名语义化(核心订单 step1/2/3 改 hotels/activities/vehicles,其余沿用核心订单既有语义名),数据来自团维度 order_batch_audit_item 按 category 过滤。行复用核单录入面板的 ItemVO(整团维度,无 orderId/orderNo/customerName 归属字段),外层带 auditStatus 供前端渲 NOT_STARTED 空态。后端已部署测试服并行为级验证:gid 不存在返 589500,真实未返团团期返 NOT_STARTED + 空 items(五字段齐全、categoryText 中文正确)。前端可按 §5 字段表接入各分类 tab(与核心订单核单同一套渲染思路,路径逐字对齐核心订单命名)。;前端已交付:step2 只读 8 tab 区(GroupCategoryItems 一 tab 一接口懒加载+缓存,空态判 auditStatus=NOT_STARTED),33 例定向全绿"
---
# 团期核单分类科目明细 tab —— 新增接口(管理后台)
> Issue: https://git.1814.love/wx/HL/issues/8632
> PR: https://git.1814.love/wx/HL/pulls/8634
> Commit: https://git.1814.love/wx/HL/commit/02572e5daa62432c6cf31393d62eadaafd993066
> 负责人:腰苏图
---
## 1. 接口背景
团期核单弹窗(一团一核单)此前只有「核单录入」`GET /v3/admin/order/group-batch/{groupBatchId}/audit`(返回全科目平铺 items)和「聚合复核」两个 tab。前端要按「酒店住宿 / 景区门票 / 车辆 / 导游 / 摄影 / 餐食 / 其他支出 / 其他收入」分 tab 展示,需自己按 category 过滤,且与核心订单核单「一 tab 一接口」的对接模式不一致。
本次新增 **8 个分类明细只读端点**,让团期核单弹窗可以像核心订单核单一样,一个 tab 调一个专用接口。数据全部来自团维度核单科目表(`order_batch_audit_item`),与「核单录入」面板同源。
关联:#8510 / PR #8546(团期核单详情对齐常规订单核单 PR-1:财务总览 + 客户合并 + 人数口径)。
---
## 2. 变更清单
| 类型 | 接口 | 说明 |
|---|---|---|
| 新增 | `GET /v3/admin/order/group-batch/{groupBatchId}/settlement/hotels` | 住宿(HOUSE) |
| 新增 | `GET /v3/admin/order/group-batch/{groupBatchId}/settlement/activities` | 景区门票(ACTIVITY) |
| 新增 | `GET /v3/admin/order/group-batch/{groupBatchId}/settlement/vehicles` | 车辆(VEHICLE) |
| 新增 | `GET /v3/admin/order/group-batch/{groupBatchId}/settlement/guide-fees` | 导游(GUIDE) |
| 新增 | `GET /v3/admin/order/group-batch/{groupBatchId}/settlement/photographer-fees` | 摄影(PHOTO) |
| 新增 | `GET /v3/admin/order/group-batch/{groupBatchId}/settlement/meals` | 餐食(MEAL) |
| 新增 | `GET /v3/admin/order/group-batch/{groupBatchId}/settlement/other-expenses` | 其他支出(OTHER_EXPENSE) |
| 新增 | `GET /v3/admin/order/group-batch/{groupBatchId}/settlement/other-incomes` | 其他收入(OTHER_INCOME) |
8 个端点结构完全一致,只是固定过滤一个 category。无入参字段、无枚举变更、无删除。
---
## 3. 接口详情
- **方法/路径**:`GET /v3/admin/order/group-batch/{groupBatchId}/settlement/{分类路径段}`
- **鉴权**:管理后台,复用团期核单查看权限 `group-batch:audit:view`(GROUP_BATCH_MANAGER / FINANCE / ADMIN)
- **路径段与 category 对应**:hotels→HOUSE、activities→ACTIVITY、vehicles→VEHICLE、guide-fees→GUIDE、photographer-fees→PHOTO、meals→MEAL、other-expenses→OTHER_EXPENSE、other-incomes→OTHER_INCOME
- **说明**:返回该团期核单下指定科目的全量科目行(不分页,单类通常几行到二三十行)。命名对齐核心订单核单 tab(核心订单 step1→hotels、step2→activities、step3/vehicles→vehicles,其余语义名沿用)。
---
## 4. 入参
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `groupBatchId` | path | Long | 是 | 运营团期 ID |
无 query / body 参数。
---
## 5. 出参
统一返回 `Result<GroupBatchAuditItemsRespVO>`。
### 5.1 GroupBatchAuditItemsRespVO(外层)
| 字段 | 类型 | 说明 |
|---|---|---|
| `auditStatus` | String | 核单状态:`NOT_STARTED`(未开始/未返团)/ `DRAFT`(录入中)/ `ALLOCATED`(已核算)/ `CHECKED`(已验团)。前端据此渲空态 |
| `batchStatus` | String | 团期状态(GroupBatchStatus,如 RECRUITING/RESOURCE_PREPARING/TRIP_FINISHED/REVIEWING/SETTLED 等) |
| `category` | String | 本端点固定的科目大类(见 §3 对应表) |
| `categoryText` | String | 科目大类中文(住宿/景区娱乐/车辆/导游/摄影/用餐/其他支出/其他收入) |
| `items` | `List<ItemVO>` | 该科目的核单科目行,**整团维度**,默认空数组(不返回 null) |
### 5.2 ItemVO(科目行,复用核单录入面板结构)
| 字段 | 类型 | 说明 |
|---|---|---|
| `itemId` | String | 科目行 ID(Long 序列化为字符串,防 JS 精度丢失) |
| `category` | String | 科目大类(与本端点固定值一致) |
| `itemName` | String | 科目名,如「D2 图嘎营地 蒙古包」「导游·双领队」 |
| `dayNo` | Integer | 第几天/第几晚,无日归属为 null |
| `unitPrice` | String | 单价(单价型科目,如房每晚房价;金额字符串),总额型为 null |
| `totalAmount` | String | 总额(总额型科目,如车/导游/其他收支;金额字符串),单价型为 null |
| `allocRule` | String | 分摊口径:`PER_ROOM_NIGHT`(按各户用房数)/ `PER_HEAD_CHECKED`(勾选参加后按人数)/ `PER_VEHICLE_GROUP`(按乘车分组内户数均分)/ `PER_ORDER_AVG`(按户平均) |
| `allocGroup` | String | 分摊分组(车科目 BUS / SUV),无分组为 null |
| `budgetAmount` | String | 带出源金额(仅供对比,不参与计算;金额字符串) |
| `changeReason` | String | 改价原因(科目行本身不存此列,读接口恒为 null) |
| `seq` | Integer | 排序 |
> 说明:`unitPrice` 与 `totalAmount` 互斥——单价型科目(住宿/景娱/餐)有 `unitPrice` 无 `totalAmount`,总额型科目(车辆/导游/摄影/其他收支)反之。`items` 为**整团科目行**,不含逐户归属字段(无 orderId/orderNo/customerName),也不含逐户用量明细。
---
## 6. 枚举 / 数据字典
- **auditStatus**:`NOT_STARTED` / `DRAFT` / `ALLOCATED` / `CHECKED`
- **category**:`HOUSE` / `VEHICLE` / `ACTIVITY` / `MEAL` / `GUIDE` / `PHOTO` / `OTHER_EXPENSE` / `OTHER_INCOME`
- **allocRule**:`PER_ROOM_NIGHT` / `PER_HEAD_CHECKED` / `PER_VEHICLE_GROUP` / `PER_ORDER_AVG`
无新增枚举值(全部复用核单录入既有枚举)。
---
## 7. 错误码
| 错误码 | 说明 |
|---|---|
| `589500` | 团期不存在(groupBatchId 非法) |
| 403 | 无 `group-batch:audit:view` 权限 |
---
## 8. 示例
### 8.1 典型(已返团团期,住宿 tab)
`GET /v3/admin/order/group-batch/2105074382613413890/settlement/hotels`
```json
{
"code": 200,
"message": "成功",
"data": {
"auditStatus": "DRAFT",
"batchStatus": "REVIEWING",
"category": "HOUSE",
"categoryText": "住宿",
"items": [
{
"itemId": "1934567890123456790",
"category": "HOUSE",
"itemName": "D2 图嘎营地 蒙古包",
"dayNo": 2,
"unitPrice": "380.00",
"totalAmount": null,
"allocRule": "PER_ROOM_NIGHT",
"allocGroup": null,
"budgetAmount": "5320.00",
"changeReason": null,
"seq": 1
}
]
},
"success": true
}
```
### 8.2 边界(未返团团期,空态)
`GET /v3/admin/order/group-batch/{未返团团期}/settlement/meals`
```json
{
"code": 200,
"message": "成功",
"data": {
"auditStatus": "NOT_STARTED",
"batchStatus": "RESOURCE_PREPARING",
"category": "MEAL",
"categoryText": "用餐",
"items": []
},
"success": true
}
```
### 8.3 异常(团期不存在)
```json
{
"code": 589500,
"message": "团期不存在",
"data": null,
"success": false
}
```
---
## 9. 业务边界
- **整团维度**:`items` 是整团核单科目行(来自「核单录入」面板同一份数据),**不含逐户归属**(无 orderId/orderNo/customerName),也不含逐户用量明细。前端按 tab 直接渲染即可,无需按户分组。
- **生命周期**:住宿/门票等科目行**在团期返团后、首次打开核单面板时才生成**。未返团的团期调任一分类端点返回 `auditStatus=NOT_STARTED` + 空 `items`(见 8.2);已返团首读会自动建 DRAFT(读接口带写副作用,权限判权在先)。
- **数据来源**:与「核单录入」`GET .../audit` 的 `items[]` 完全同源,本批端点只是按 category 拆成独立 tab 读口,不改变数据本身。
---
## 10. 修改前后对比(修改类)
非修改类(纯新增接口),不适用。
---
## 11. 影响评估 / 回滚(修改类)
- **兼容性**:纯新增接口,不影响任何既有接口。
- **性能**:单端点一次查询 + 内存按 category 过滤,不分页、无 N+1;不触碰核单试算。
- **回滚**:回退 merge commit `02572e5daa` 即可下线 8 个端点;无 DDL、无数据迁移成本。
---
## 12. 注意事项
1. 8 个端点结构完全一致,前端可封装一个通用的「分类 tab 请求 + 渲染」组件,按路径段切换。
2. 渲染空态请看 `auditStatus`(`NOT_STARTED` 时显示「团期未返团,返团后可核单」类提示),而不是看 `items` 是否为空(已返团某科目无数据时 items 也为空,但 auditStatus 是 DRAFT)。
3. 金额字段(unitPrice/totalAmount/budgetAmount)是**字符串**(BigDecimal 序列化),展示直接用,参与计算需自行转数值。
4. 本批是「分类科目明细」读口;核单录入(写)与逐户用量下钻走既有 `/audit` 与下钻端点,不在本批范围。
---
## 13. 关联 / 联系人
- Issue: https://git.1814.love/wx/HL/issues/8632
- PR: https://git.1814.love/wx/HL/pulls/8634
- Commit: https://git.1814.love/wx/HL/commit/02572e5daa62432c6cf31393d62eadaafd993066
- 负责人:腰苏图
@@ -0,0 +1,262 @@
---
schema: "hl-changelog/v2"
ticket: "8641"
title: "团期核单「核团详情」统一接口 return-detail 上线,reports/group 旧路径已删 404"
consumer: "admin"
author: "yst(GIT)"
change_type: "修改接口"
backend_status: "merged"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "d5a372be4a2079adf9c1ca91a68b992f92918507"
target_release: "v2.1"
verified_at: "2026-09-30"
base: "dev-v3"
updated_at: "2026-09-30"
status_note: "团期核单对齐核心订单 return-detail 形态:新增统一总览接口 GET /v3/admin/order/group-batch/{groupBatchId}/settlement/return-detail(一屏打包团期信息+财务总览+customers 全团出行人合并+人数汇总+复核快照段+driverVehicles 司机车辆区间),原 GET .../settlement/reports/group 已下线(调用返回业务码 404,HTTP 200 包装)。出参在 reports/group 基础上新增 driverVehicles(读 order_vehicle_assignment 本地快照按司机+车辆合并连续 serviceDate 成区间,driverPhone 已脱敏),其余字段与口径完全不变。后端已合并 dev-v3 待部署。前端:原 reports/group 调用方改调 return-detail(字段平移即可),并新增渲染 driverVehicles 司机车辆区间。;前端已交付:getGroupReturnDetail 换径 return-detail(旧路径 404 实证)+step1 driverVehicles 司机车辆区间渲染,33 例定向全绿"
---
# 团期核团详情统一接口 return-detail —— 修改接口(管理后台)
> Issue: https://git.1814.love/wx/HL/issues/8641
> PR: https://git.1814.love/wx/HL/pulls/8651
> Commit: https://git.1814.love/wx/HL/commit/067753a12a283c8df011e7b312282bfa0f55f4ea
> 负责人:腰苏图
---
## 1. 接口背景
核心订单核单有「核团详情」总览接口 `GET /v3/admin/order/{orderId}/settlement/return-detail`,一屏打包订单信息+出行人+司机车辆区间+应收+收款。团期核单此前把财务总览放在 `GET .../settlement/reports/group`(PR-1),形态与核心订单「统一 return-detail」不一致,且缺司机车辆区间。
本次对齐核心订单:新增团期统一总览接口 `return-detail`,并把 `reports/group` 收敛下线。前端已确认未消费旧接口,收敛零破坏。
---
## 2. 变更清单
| 类型 | 接口 | 说明 |
|---|---|---|
| 新增 | `GET /v3/admin/order/group-batch/{groupBatchId}/settlement/return-detail` | 团期核团详情统一总览 |
| **删除** | `GET /v3/admin/order/group-batch/{groupBatchId}/settlement/reports/group` | **已下线,调用返回业务码 404**(见 §8.3) |
出参在 `reports/group` 基础上**新增 `driverVehicles` 字段**,其余字段与口径完全不变。
---
## 3. 接口详情
- **方法/路径**:`GET /v3/admin/order/group-batch/{groupBatchId}/settlement/return-detail`
- **鉴权**:管理后台,复用团期核单查看权限 `group-batch:audit:view`(GROUP_BATCH_MANAGER / FINANCE / ADMIN)
- **说明**:团期核单「核团详情」一屏总览。`finalized=false`(未核单)时实时段(财务/客户/人数/司机车辆)照常返回,复核快照段为 null。
---
## 4. 入参
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `groupBatchId` | path | Long | 是 | 运营团期 ID |
---
## 5. 出参
统一返回 `Result<GroupReturnDetailRespVO>`。字段分四段。
### 5.1 团期信息 + 核单状态段
| 字段 | 类型 | 说明 |
|---|---|---|
| `finalized` | Boolean | 是否已有核单快照(false=尚未核单,复核快照段全 null) |
| `groupSettlementId` | String | 团期核单快照 ID(Long 序列化字符串) |
| `groupBatchId` | String | 团期 ID(字符串) |
| `batchNo` | String | 团期号 |
| `productName` | String | 产品名称快照 |
| `departDate` | String | 出发日期(yyyy-MM-dd) |
| `batchStatus` | String | 团期状态(GroupBatchStatus,如 REVIEWING) |
| `reviewStatus` | String | 复核状态:PENDING / APPROVED / RETURNED |
| `settlementStatus` | String | 核单状态:PENDING / FINALIZED |
| `flowStatus` | String | 团期流程状态快照:TRIP_FINISHED / REVIEWING / SETTLED |
| `settledBy` / `settledByName` / `settledAt` | String / String / String | 核单人 ID/姓名/完成时间(未核单为 null) |
| `remark` / `createTime` | String / String | 备注 / 快照创建时间 |
### 5.2 复核快照段(成本/利润/共享成本/预支/团级金额)
| 字段 | 类型 | 说明 |
|---|---|---|
| `settledOrderCount` | Integer | 已核单子订单户数 |
| `totalActiveOrderCount` | Integer | 在团子订单总户数(仅排除已取消) |
| `subOrderTotalActualCost` | String | 子订单实际成本合计 |
| `subOrderTotalProfit` | String | 子订单毛利合计 |
| `sharedCostTotal` | String | 团期共享成本合计 |
| `sharedCostByType` | List | 共享成本按类型汇总 |
| `grandTotalCost` | String | 团期总成本(子订单成本+共享成本) |
| `actualTravelerCount` | Integer | 实际出行人数 |
| `perPersonSharedCost` | String | 人均共享成本(人数为 0 时 null) |
| `groupAdvanceApproved` / `groupAdvancePending` | String / String | 整团预支已批/待批合计 |
| `groupTotalAmount` / `groupPaidAmount` | String / String | 全团应收/已付总额(报账单净额输入,快照口径) |
### 5.3 实时财务段 + 客户/人数(复用 PR-1,口径不变)
| 字段 | 类型 | 说明 |
|---|---|---|
| `baseOrderAmount` | String | 基础订单金额合计(Σ order_amount,在团) |
| `otherIncomeAmount` | String | 有效增费合计(Σ surcharge_amount) |
| `discountAmount` | String | 有效优惠合计(Σ discount_amount) |
| `adjustedReceivableAmount` | String | 调整后应收合计(Σ calcPayable) |
| `onlinePaidAmount` | String | 成功线上支付合计(权威) |
| `offlinePaidAmount` | String | 有效线下收款合计(权威) |
| `primaryReporterCollectedAmount` | String | 主报账人代收合计(仅展示,已含在 offlinePaid 内) |
| `paidAmount` | String | 已收合计(镜像,与 outstanding 同源) |
| `actualRefundedAmount` | String | 实际退款合计(镜像,已收扣实退) |
| `netPaidAmount` | String | 净已收 = paidAmount − actualRefundedAmount |
| `outstandingAmount` | String | **待收尾款** = Σ calcBalance(与 /finance 应收台账同源) |
| `surchargeMirrorMatched` / `discountMirrorMatched` / `paidMirrorMatched` / `refundedMirrorMatched` | Boolean | 4 个镜像校验位(数据质量信号,前端一般不需展示) |
| `customers` | `List<GroupSettlementCustomerItemVO>` | 全团出行人/客户合并(一户一行:orderId/orderNo/teamNo/customerName 明文/customerPhone 脱敏/travelerCount) |
| `householdCount` / `travelerCount` / `adultCount` / `childCount` / `youngChildCount` / `babyCount` | Integer | 户数 / 总人数(含婴儿)/ 各档人数 |
### 5.4 司机车辆区间段(**本次新增** `driverVehicles`)
| 字段 | 类型 | 说明 |
|---|---|---|
| `driverVehicles` | `List<SettlementDriverVehicleSegmentVO>` | 全团司机车辆连续服务区间,默认空数组(不返回 null) |
**SettlementDriverVehicleSegmentVO**(与核心订单 return-detail 同构):
| 字段 | 类型 | 说明 |
|---|---|---|
| `driverId` | String | 司机 ID(Long 序列化字符串) |
| `driverName` | String | 司机姓名 |
| `driverPhone` | String | 司机手机号(**已脱敏**,如 138****0000) |
| `vehicleId` | String | 车辆 ID(字符串) |
| `vehiclePlateNo` | String | 车牌号 |
| `vehicleModelName` | String | 车型名称 |
| `seatCount` | Integer | 座位数 |
| `startDate` | String | 连续服务开始日期(yyyy-MM-dd) |
| `endDate` | String | 连续服务结束日期(yyyy-MM-dd) |
> 区间口径:读 `order_vehicle_assignment` 本地快照(Fleet 配车回调回写),按 (司机+车辆) 分组合并连续 serviceDate 成 startDate~endDate;与核心订单 return-detail 口径一致。无派车/空团返回空数组。
---
## 6. 枚举 / 数据字典
- **reviewStatus**:`PENDING` / `APPROVED` / `RETURNED`
- **settlementStatus**:`PENDING` / `FINALIZED`
- **batchStatus / flowStatus**:团期状态机枚举(RECRUITING/RESOURCE_PREPARING/MATERIAL_PREPARING/PENDING_DEPARTURE/TRAVELLING/TRIP_FINISHED/REVIEWING/SETTLED/CANCELLED)
无新增枚举值。
---
## 7. 错误码
| 错误码 | 说明 |
|---|---|
| `589500` | 团期不存在 |
| 404(业务码) | **旧路径 reports/group 已下线**(HTTP 200 包装,见 §8.3) |
| 403 | 无 `group-batch:audit:view` 权限 |
---
## 8. 示例
### 8.1 典型(已核单团,return-detail 正常返回)
`GET /v3/admin/order/group-batch/2105074382613413890/settlement/return-detail`
```json
{
"code": 200,
"message": "成功",
"data": {
"finalized": true,
"groupBatchId": "2105074382613413890",
"batchNo": "20261001-01",
"productName": "呼伦贝尔草原 5 日游",
"departDate": "2026-10-01",
"batchStatus": "REVIEWING",
"reviewStatus": "PENDING",
"settlementStatus": "FINALIZED",
"flowStatus": "REVIEWING",
"outstandingAmount": "2500.00",
"netPaidAmount": "47500.00",
"travelerCount": 18,
"householdCount": 6,
"customers": [
{"orderId": "9001", "orderNo": "HL20260901001", "teamNo": "A1", "customerName": "张三", "customerPhone": "138****5678", "travelerCount": 3}
],
"driverVehicles": [
{"driverId": "20001", "driverName": "李师傅", "driverPhone": "138****0000", "vehicleId": "10001", "vehiclePlateNo": "蒙E12345", "vehicleModelName": "丰田普拉多", "seatCount": 7, "startDate": "2026-10-01", "endDate": "2026-10-05"}
]
},
"success": true
}
```
### 8.2 边界(未核单团,finalized=false)
实时段(财务/客户/人数/司机车辆)照常返回,复核快照段(settledBy/settledAt/subOrderTotalActualCost 等)为 null,`finalized=false`。
### 8.3 异常(**旧路径 reports/group 已下线,调用返回业务码 404**)
`GET /v3/admin/order/group-batch/{id}/settlement/reports/group`
```json
{
"code": 404,
"message": "接口不存在: GET /v3/admin/order/group-batch/2105074382613413890/settlement/reports/group",
"data": null,
"success": false
}
```
> ⚠️ HL 统一契约:未映射/已删除路由返回 **HTTP 200 + body 业务码 404**(非 HTTP 404),前端按 `code === 404` 判定。
---
## 9. 业务边界
- **数据范围**:财务/客户/司机车辆均只统计**在团子订单**,排除已取消(CANCELLED)。
- **快照 vs 实时**:复核快照段是 finalize 时冻结的「活跃口径」;实时财务段是「在团口径」现算;两者有意并存,前端展示以实时段为准。
- **driverVehicles**:来自订单侧本地快照(Fleet 配车回调回写),按司机+车辆合并连续日期成区间;不实时连 Fleet。
- **待收尾款**:`outstandingAmount` 与 `/finance` 应收台账同源,两端可对拍。
---
## 10. 修改前后对比
| 维度 | 修改前(reports/group) | 修改后(return-detail) |
|---|---|---|
| 路径 | `/settlement/reports/group` | `/settlement/return-detail`(旧路径已删 404) |
| 出参字段 | 团期信息+财务+客户+人数+复核快照段 | **同上 + 新增 driverVehicles 司机车辆区间** |
| 司机车辆 | 无 | 有(本地快照合并连续区间,driverPhone 脱敏) |
| 口径 | — | 完全不变,纯路径迁移 + 字段新增 |
---
## 11. 影响评估 / 回滚
- **前端迁移**:原 reports/group 调用方改调 `return-detail`,出参字段平移即可(仅多一个 driverVehicles 字段,可不消费);新增渲染 driverVehicles 司机车辆区间。
- **兼容性**:旧路径已删返回业务码 404——已核实前端未消费,无破坏。
- **回滚**:回退 merge commit `067753a12a` 即恢复 reports/group;无 DDL、无数据迁移成本。
---
## 12. 注意事项
1. **路径迁移**:`reports/group` → `return-detail`,字段不变,仅改路径 + 新增 driverVehicles。
2. **金额字段是字符串**(BigDecimal 序列化),展示直接用,计算需自行转数值。
3. **driverPhone 已脱敏**;`driverVehicles` 无派车时是空数组 `[]` 不是 null。
4. 本接口对齐核心订单 `return-detail` 形态;分类明细 tab(住宿/门票等)走 PR-2 的 `/settlement/{hotels,activities,...}` 端点,与本接口互补。
---
## 13. 关联 / 联系人
- Issue: https://git.1814.love/wx/HL/issues/8641
- PR: https://git.1814.love/wx/HL/pulls/8651
- Commit: https://git.1814.love/wx/HL/commit/067753a12a283c8df011e7b312282bfa0f55f4ea
- 负责人:腰苏图
@@ -0,0 +1,235 @@
---
schema: "hl-changelog/v2"
ticket: "8642"
title: "团期详情(A2)进度条导游 / 摄影分支改为看名册——本位配了人即「已完成」,招募中也一样;「无需」只留给不需要且没配人"
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: "A2 团期详情 progressStepper(#8478)配置节点下的「配导游」「配摄影」两条分支,改前只要团期 needs_guide / needs_photographer 不为 true 就显示 WAIVED「无需」,不看导摄名册。导摄 ready 一列两义(成团免闸置 1 = 不需要;配人保存置 1 = 已配人),产品没配领队的团期成团后再配导游,进度条一直是「配导游·无需」(TEST T26-3963 实例),与看板导/摄芯片「名册有人即完成」(#8469)口径分家。jw 2026-09-30 定案:导游位配了人 → 配导游 DONE「已完成」,摄影位配了人 → 配摄影 DONE「已完成」,招募中同样适用;「无需」只留给 needs 不为 true 且本位没人。标记为 false → UNMET「未配齐」仍排在最前,与确认预检逐项一致。路径、入参、出参字段名与结构零变化;变的是 GUIDE / PHOTOGRAPHER 两条分支 status / statusName / displayText 的取值口径,属「语义取值」变化,按 jw 2026-09-18 口径推送。已合并 dev-v3(PR #8652,merge commit 5b271055b)并部署测试服。前端按 status 渲染,DONE 本来就有样式,零改动,frontend_status 记 not_required。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# 团期详情(A2)进度条导游 / 摄影分支改为看名册(管理后台)
> **服务**: hl-order-service-v3(端口 8086/8186)
## 一、接口背景
团期详情(A2)的 `progressStepper`(#8478)在「配置」节点下有五条分支:配房、配车、配导游、配摄影、配物资。
其中导游、摄影两条原先只看团期的 `needs_guide` / `needs_photographer`:不为 true 就显示 `WAIVED`「无需」,不看导摄名册里有没有人。
`needs_*` 在创单时按产品人员配置冻结(产品配了领队才为 1),成团时抄到团期;成团事务会把不需要的导摄 ready 直接置 1(免闸)。
所以产品没配领队的团期,成团后运营再去配导游,进度条仍然是「配导游·无需」,而同一页的看板导/摄芯片(#8469)按名册判定已经是完成。
jw 2026-09-30 定案:
1. 导游位配了人 → 配导游「已完成」;摄影位配了人 → 配摄影「已完成」。
2. 招募阶段同样适用:招募中名册本位有人也显示「已完成」(其余情况仍是「待开始」)。
本次修订 #8478 的两条定案:「不需要时显示无需、不要显示成已完成」收窄为「不需要且没配人才显示无需」;「招募阶段五条分支全部待开始」改为导游、摄影两条在本位有人时显示「已完成」。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | A2 团期详情 | GET | `/v3/admin/order/group-batch/{groupBatchId}` | 修改接口 | 出参 `progressStepper[].subFlows[]` 中 GUIDE / PHOTOGRAPHER 两条分支的 `status` / `statusName` / `displayText` 改为看名册;字段名、结构与其余字段零变化 |
## 三、接口详情
### 1. A2 团期详情 `GET /v3/admin/order/group-batch/{groupBatchId}`
**VO**: `GroupBatchDetailRespVO`(进度条节点 `GroupBatchProgressNodeVO`,分支 `GroupBatchProgressSubFlowVO`)
#### 使用场景
团期详情页页头的分叉进度条。本次只改「配置」节点下导游、摄影两条分支的取值,其余三条分支、六个主节点、其余出参都不变。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| groupBatchId | path | Long | 是 | 正整数,团期聚合主键 | 团期 ID,不存在返 589501 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| progressStepper | List\<GroupBatchProgressNodeVO\> | 六个主节点,结构不变;只有 CONFIGURE 节点带 `subFlows` |
| progressStepper[].subFlows[].code | String | HOTEL / VEHICLE / GUIDE / PHOTOGRAPHER / MATERIAL,顺序固定,未变 |
| progressStepper[].subFlows[].status | String | **本次 GUIDE / PHOTOGRAPHER 两条改口径**,按下面「业务边界」的优先级判定:WAITING 待开始 / UNMET 未配齐 / WAIVED 无需 / DONE 已完成 |
| progressStepper[].subFlows[].statusName | String | 与 status 对应的中文名:待开始 / 未配齐 / 无需 / 已完成 |
| progressStepper[].subFlows[].displayText | String | `name + "·" + statusName`,如「配导游·已完成」 |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2104839654727618562 HTTP/1.1
Host: api.test.1814.love
Authorization: Bearer <admin token>
```
#### 响应示例
T26-3963(已成团,needs_guide=0、needs_photographer=0;名册有导游李雪梅、领队巴特尔,没有摄影):
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2104839654727618562",
"batchStatus": "RESOURCE_PREPARING",
"guideReady": true,
"photographerReady": true,
"progressStepper": [
{ "step": 1, "code": "RECRUIT", "name": "招募", "status": "DONE", "isCurrent": false, "label": null, "subFlows": null },
{ "step": 2, "code": "CONFIGURE", "name": "配置", "status": "PROCESSING", "isCurrent": true, "label": "配置中",
"subFlows": [
{ "code": "HOTEL", "name": "配房", "status": "UNMET", "statusName": "未配齐", "displayText": "配房·未配齐" },
{ "code": "VEHICLE", "name": "配车", "status": "DONE", "statusName": "已完成", "displayText": "配车·已完成" },
{ "code": "GUIDE", "name": "配导游", "status": "DONE", "statusName": "已完成", "displayText": "配导游·已完成" },
{ "code": "PHOTOGRAPHER", "name": "配摄影", "status": "WAIVED", "statusName": "无需", "displayText": "配摄影·无需" },
{ "code": "MATERIAL", "name": "配物资", "status": "UNMET", "statusName": "未确认", "displayText": "配物资·未确认" }
] }
]
}
}
```
#### 空数据 / 降级响应
- 已流团、`batchStatus` 为 null 或不在九态内:`progressStepper` 为空列表 `[]`(与 #8478 相同,本次未改)。
- 名册没人:导摄分支按 `needs_*` 判——需要且标记 false 为「未配齐」,不需要为「无需」,与改前一致。
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2105074297745866753",
"batchStatus": "CANCELLED",
"progressStepper": []
}
}
```
#### 错误响应
```json
{
"code": 589501,
"message": "团期不存在",
"data": null
}
```
| 错误码 | 触发条件 |
|---|---|
| 589501 | groupBatchId 不存在或已软删 |
| 589507 | 缺 `group-batch:view` 权限码 |
#### 业务边界
导游 / 摄影分支自上而下按优先级判定(房、车、物资三条规则不变):
| 条件 | status | statusName |
|---|---|---|
| 团期在招募阶段,名册本位有人 | DONE | 已完成 |
| 团期在招募阶段,名册本位没人 | WAITING | 待开始 |
| 已成团,就绪标记(`guideReady` / `photographerReady`)不为 true | UNMET | 未配齐 |
| 已成团,标记 true 且名册本位有人 | DONE | 已完成 |
| 已成团,标记 true、名册本位没人、`needs_*` 不为 true | WAIVED | 无需 |
| 已成团,标记 true、`needs_*` 为 true | DONE | 已完成 |
- 「本位有人」与看板导/摄芯片同一读口(`GroupBatchSlotStaffReadService#slotsWithStaffByProductBatchIds`):导游位包含哪些人员类型由字典决定,默认 GUIDE + LEADER,所以只配了领队也算导游位有人;摄影位默认 PHOTOGRAPHER。
- 已成团阶段「未配齐」排在名册前面:进度条的 UNMET 与确认预检 `confirm-check` 同一项 `passed=false` 仍逐项等价。
- 已成团、不需要该项的团期,清空名册后 ready 保持 1(免闸不回落,既有行为),进度条回到「无需」。
## 四、契约约束与正确调用方式
- 进度条只供展示;业务判断(能不能确认、能不能出发)仍读 `batchStatus` 与五个就绪标记本身,不要读分支 `status`。
- 招募阶段导摄分支可能是 DONE,而「配置」主节点仍是 WAITING——这是定案行为,前端按分支自身 `status` 渲染即可,不要用主节点状态覆盖分支。
## 五、数据库行为
零数据库变更,不新增表、列或索引,不写任何数据。
进度条派生从 `assembleDetail`(`@Transactional(readOnly = true)`)挪到 `getDetail` 事务外:名册读口按字典把角色归位,字典回源是同步 Feign(`DictFeignClient`,5 分钟缓存),留在事务里会违反红线⑧。
名册懒求值:只在「招募中」或「已成团、导摄标记 true 且 `needs_*` 不为 true」时查一次(只投影 `product_batch_id` / `staff_role`),导游、摄影两条分支共用;已成团且两项都需要的团期不多查。
## 六、边界行为
- 团期不存在 → 589501。
- 已流团 / 脏状态 → `progressStepper=[]`。
- 招募中名册没人 → 五条全「待开始」。
- 已成团、需要导游但名册没人 → 「配导游·未配齐」(与改前一致)。
## 六.6、修改前后对比
| 场景 | 修改前 | 修改后 |
|---|---|---|
| 已成团、needs=0、本位有人 | 无需 | **已完成** |
| 已成团、needs=0、本位没人 | 无需 | 无需 |
| 已成团、needs=1、标记 true | 已完成 | 已完成 |
| 已成团、标记 false | 未配齐 | 未配齐 |
| 招募中、本位有人 | 待开始 | **已完成** |
| 招募中、本位没人 | 待开始 | 待开始 |
| 路径 / 入参 / 权限码 / 字段名 / 结构 | — | 逐字未变 |
## 六.7、影响评估
- **兼容性**:字段与结构不变,只有两条分支的取值在上表两种场景下从 WAIVED / WAITING 变成 DONE。前端按 `status` 渲染,DONE 已有样式,不用改。
- **性能**:招募中或已成团免闸的团期,详情多一次本库只读查询(名册投影)+ 字典读取(5 分钟缓存);在事务外执行,不占 DB 连接做远程调用。
- **回滚**:撤销 PR #8652 的合并提交后重新部署 order-v3,无数据与配置残留。
## 七、不影响范围
- 确认预检 `confirm-check`、确认门、出发七项硬门、合同 / 预支 / 物资闸:仍只读五个就绪标记,零改动。
- `guide_ready` / `photographer_ready` 的写法与成团免闸:零改动。
- 房、车、物资三条分支与六个主节点:零改动。
- 看板导/摄芯片(#8469):本来就是按名册判定,本次与之对齐,未改动。
- 网关:路径未变、无新增路由与权限码。
## 八、测试环境已验证
2026-09-30 13:01–14:10 测试服(`api.test.1814.love`),部署 `dev-v3 @ 5b271055b`(order-v3 双实例 8086/8186)。
- 部署身份:T26-3963 改前(13:01)为「配导游·无需」;部署后经网关连打 6 次,6/6 为「配导游·已完成」「配摄影·无需」。
- 自建两个团期、各一张真实订单:A 团(needs=0)、B 团(成团前把自造订单 needs 置 1)。每一步同时读 A2、确认预检和库里的就绪位。
| 场景 | 名册 | 进度条导摄两条 | 库 ready(导/摄) |
|---|---|---|---|
| A 招募中,只配导游 | GUIDE | 配导游·已完成、配摄影·待开始(改前旧代码为 配导游·待开始) | 1 / 0 |
| A 招募中,再配摄影 | GUIDE, PHOTOGRAPHER | 配导游·已完成、配摄影·已完成(房、车、物资仍待开始) | 1 / 1 |
| A 成团后(needs 0/0) | GUIDE, PHOTOGRAPHER | 配导游·已完成、配摄影·已完成 | 1 / 1 |
| A 清空导游位 | PHOTOGRAPHER | 配导游·无需 | 1 / 1(免闸不回落) |
| A 导游位只配领队 | LEADER, PHOTOGRAPHER | 配导游·已完成 | 1 / 1 |
| A 清空摄影位 | LEADER | 配摄影·无需 | 1 / 1 |
| B 招募中 | 空 | 五条全待开始 | 0 / 0 |
| B 成团后(needs 1/1) | 空 | 配导游·未配齐、配摄影·未配齐 | 0 / 0 |
| B 配导游 | GUIDE | 配导游·已完成、配摄影·未配齐 | 1 / 0 |
| B 清空导游位 | 空 | 配导游·未配齐 | 0 / 0 |
- 已成团的每一步,进度条 UNMET 的项与确认预检同一项 `passed=false` 逐项一致。
- 验收造数已全部回收(两团两单连带名册、扇出、行程、服务项、快照、流水共 12 张表按主键软删 / 物理删,产品侧班期经接口删除),回读零残留;他人团期 T26-3963 全程只读。
单元测试:定向 4 类 189 例 0 失败(进度条穷举加名册维度共 69984 组合、确认门同源不变量加名册维度);团期整包 + 全部 ArchTest 3302 例中 12 例红,基底 `8e00cc98b` 逐条复现,属既有失败,本单引入 0 个。
## 十、相关文档
- 工单 #8642、PR #8652(merge commit `5b271055b`)
- 进度条首发:#8478
- 看板导/摄芯片按名册计算:#8469
- 招募中允许配导摄:#8231
## 关联 / 联系人
- 后端:jw
- 前端:不涉及(`frontend_status: not_required`)
@@ -0,0 +1,485 @@
---
schema: "hl-changelog/v2"
ticket: "8654"
title: "团期正式派车司机投影到人员配置表,DRIVER 角色改系统托管"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "3c153a325db57e08e951a5da0562fdba7f4f0127"
target_release: "v2.1"
verified_at: "2026-10-01"
status_note: "2026-10-01 hl-admin 交付:配置弹窗/ChipStaffRoster 钉口径(inPosition 与 MEMBER_ROLES 过滤天然排除 DRIVER,勾选/提交/更换/删除均无 DRIVER 入口,582120 兜底透 message);核单按团看人 AuditAllocsTable 角色映射补 DRIVER「司机」防显原码;api JSDoc 两接口钉「保存响应不含 DRIVER 勿当全量名册」;spec 共享名册 fixture 加 DRIVER 行+新增 2 例,3 文件 31 例全绿,ref 3c153a32。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# 团期人员配置: 正式派车司机投影到人员配置表,DRIVER 改系统托管
> **存放目录**: `changelogs-v2/2026-09/`
>
> **服务**: hl-order-service-v3
> **PR**: #8669
> **Issue**: #8653, #8654
> **日期**: 2026-09-30
> **影响范围**: 管理后台团期人员配置页、团期核单按团看人的名册读取
---
## ⚠️ 关键变化
**团期人员配置表 `order_batch_staff` 现在会有系统自动生成的 DRIVER 行(司机),前端必须适配两个关键变化:**
1. **同一批数据的两个读口口径不同**: 保存接口响应里**不含**司机行,但查询接口(`getConfig` / 名册读取)**含**司机行。前端不要假设响应即全量。
2. **司机行不可编辑**: 这些 DRIVER 行由车务派车自动投影产生,不是运营配置的,前端不要在人员编辑表单提供编辑/删除入口。
3. **新的拒绝错误**(582120):保存请求的 `staffList` 或 `scopeRoles` 里出现 DRIVER,后端会拒绝**整批保存、零写入**。
---
## 一、背景
工单 #8653 与 #8654 合并的功能:**把车务派车产生的司机自动投影到团期人员配置表**,供核单时按团看人、按天算账。
此前司机事实分散在每一户订单的用车需求上,现在统一投影到团期维度,简化核单逻辑。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 保存团期 staff 配置 | PUT | `/v3/admin/group-batch/{productBatchId}/staff` | 新增错误码 + 响应内容变化 | DRIVER 行系统管理,拒绝人工写入 |
---
## 三、接口详情
### 1. 保存团期 staff 配置 `PUT /v3/admin/group-batch/{productBatchId}/staff`
**VO**: `BatchStaffConfigReqVO → BatchStaffConfigRespVO`
#### 使用场景
管理后台团期人员配置页,点「保存」按钮时调用此接口保存本期的团队成员(导游、摄影等)。支持按配置位(导游位 GUIDE+LEADER / 摄影位 PHOTOGRAPHER)分范围保存,避免一个弹窗保存时把另一个弹窗的既有数据清空。
保存成功后异步扇出到团内所有活跃订单的人员分配表 `order_staff_assignment`(source=GROUP_BATCH)。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| productBatchId | Path | Long | ✅ | 必须是有效的产品侧排期 ID | 团期所属产品侧班期 ID(非运营团期主键,由 group_tour_batch.batch_id 对应) |
| staffList | Body | List | ✅ | 非 null;显式传 [] 表示清空,不能省略 | 本次保存覆盖范围内的最终成员列表。**禁止含 staffRole=DRIVER 的行**(582120)。若覆盖范围是导游位,必须同时列出 GUIDE 与 LEADER 两个角色成员(工单 #8122)。 |
| staffList[].staffId | Body | Long | ✅ | 有效的员工 ID | 用户域员工 ID |
| staffList[].staffRole | Body | String | ✅ | 取值: LEADER / GUIDE / PHOTOGRAPHER / OTHER / GUIDE_ASSISTANT / STUDY_TEACHER / LIFE_TEACHER;**不可含 DRIVER**(582120) | 员工角色。司机行由车务派车自动投影,一律不由本接口写入。 |
| staffList[].sortOrder | Body | Integer | ❌ | 缺省 0 | 显示排序值(升序排列) |
| staffList[].remark | Body | String | ❌ | ≤500 字符;null 表示保留原值,传空串清空 | 备注,仅供参考 |
| staffList[].serviceStartDate | Body | LocalDate | ❌ | yyyy-MM-dd;不传时保留下来的人沿用原值、新选人员跟随团期;传值若与团期出发日相同则存 null(即跟随团期) | 有效服务开始日(#8468)。若自定义则必须在团期出发日到结束日之间,否则 582119 拒绝。 |
| staffList[].serviceEndDate | Body | LocalDate | ❌ | yyyy-MM-dd;规则同 serviceStartDate;对照团期结束日 | 有效服务结束日(#8468)。 |
| scopeRoles | Body | List<String> | ❌ | 元素不能为 null / 空串 / 纯空白;传了必须 size ≥ 1 | 本次保存覆盖的角色范围。不传 = 整期全量覆盖(历史行为);传了 = 只覆盖这些角色,范围外既有行不动(工单 #8006)。导游位必须同时传 GUIDE 与 LEADER(工单 #8122),只传一个拒绝 582116 且零写入。staffList 里出现范围外角色拒绝 582115 且零写入。**禁止传 DRIVER**(582120)。 |
#### 出参 `Result<BatchStaffConfigRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| data.productBatchId | Long | 路径参数回显 |
| data.groupBatchId | Long | 运营团期 ID(order_group_batch 主键) |
| data.staffList | List | 本次保存后**保留下来的整期非司机成员**的最新快照。**不含 DRIVER 行**(工单 #8654)。 |
| data.staffList[].id | Long | 记录 ID(batch_staff_id) |
| data.staffList[].staffId | Long | 员工 ID |
| data.staffList[].staffRole | String | 员工角色(LEADER / GUIDE / PHOTOGRAPHER 等,响应侧**不含 DRIVER**) |
| data.staffList[].staffRoleName | String | 员工角色中文名(「导游」「领队」等;取值不在字典内时回落原 code) |
| data.staffList[].staffName | String | 员工姓名(配置时快照) |
| data.staffList[].staffPhone | String | 员工手机(脱敏:前 3 后 4,如 `138****6677`) |
| data.staffList[].avatarUrl | String | 头像 URL(配置时快照) |
| data.staffList[].sortOrder | Integer | 显示排序值 |
| data.staffList[].remark | String | 备注 |
| data.staffList[].reporterRank | String | 报账人等级:PRIMARY(主) / SECONDARY(次) / NONE(非报账人) |
| data.staffList[].reporterRankName | String | 报账人等级中文名 |
| data.staffList[].serviceStartDate | LocalDate | 有效服务开始日(未自定义时 = 团期出发日,团期改期后跟着变;#8468) |
| data.staffList[].serviceEndDate | LocalDate | 有效服务结束日(规则同上) |
| data.staffList[].serviceDateCustom | Boolean | 是否自定义过服务日期;true 时开始/结束至少一个有自定义值 |
| data.staffList[].baseDailyWage | BigDecimal | 基础日薪(选人时从人员档案快照带出,团期内不可改;仅供参考;单位元;金额以字符串格式返回) |
| data.staffList[].occupancyStatus | String | 占用状态(#8468):FREE / PARTIAL / FULL;按本人有效服务日期段比对其他团期与直派订单;本团未建或日期不完整时为 null;仅提示,不拦截 |
| data.staffList[].occupiedDays | Integer | 被占天数(两端都算);null 时为 0 |
| data.staffList[].freeRanges | List<String> | 可派日期段列表(本人有效日期段内未被占的连续区间);恒非 null;全程占用时为 [] |
| data.staffList[].occupancies | List | 占用明细数组;恒非 null;空闲时为 [] |
| data.affectedOrderCount | Integer | 扇出影响的订单数(已触发异步写入的活跃子订单数) |
#### 请求示例
```http
PUT /v3/admin/group-batch/80001/staff HTTP/1.1
Host: admin-api.test.example.com
Authorization: Bearer {token}
Content-Type: application/json
{
"scopeRoles": ["GUIDE", "LEADER"],
"staffList": [
{
"staffId": 40001,
"staffRole": "LEADER",
"sortOrder": 0,
"remark": "首席领队",
"serviceStartDate": "2026-10-01",
"serviceEndDate": "2026-10-06"
},
{
"staffId": 40002,
"staffRole": "GUIDE",
"sortOrder": 1,
"remark": null,
"serviceStartDate": null,
"serviceEndDate": null
}
]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"productBatchId": 80001,
"groupBatchId": 90211,
"staffList": [
{
"id": 770001,
"staffId": 40001,
"staffRole": "LEADER",
"staffRoleName": "领队",
"staffName": "刘领队",
"staffPhone": "138****6677",
"avatarUrl": "https://example.com/avatar/40001.jpg",
"sortOrder": 0,
"remark": "首席领队",
"reporterRank": "PRIMARY",
"reporterRankName": "主报账人",
"serviceStartDate": "2026-10-01",
"serviceEndDate": "2026-10-06",
"serviceDateCustom": true,
"baseDailyWage": "600.00",
"occupancyStatus": "PARTIAL",
"occupiedDays": 2,
"freeRanges": ["2026-10-01~2026-10-03", "2026-10-05~2026-10-06"],
"occupancies": [
{
"groupBatchId": 90212,
"groupBatchName": "十一国庆游 V2 期",
"conflictDates": ["2026-10-02", "2026-10-04"]
}
]
},
{
"id": 770002,
"staffId": 40002,
"staffRole": "GUIDE",
"staffRoleName": "导游",
"staffName": "王导游",
"staffPhone": "138****5678",
"avatarUrl": "https://example.com/avatar/40002.jpg",
"sortOrder": 1,
"remark": null,
"reporterRank": "NONE",
"reporterRankName": "非报账人",
"serviceStartDate": "2026-10-01",
"serviceEndDate": "2026-10-06",
"serviceDateCustom": false,
"baseDailyWage": "500.00",
"occupancyStatus": "FREE",
"occupiedDays": 0,
"freeRanges": ["2026-10-01~2026-10-06"],
"occupancies": []
}
],
"affectedOrderCount": 3
},
"success": true
}
```
#### 空数据 / 降级响应
整期清空后的响应(`staffList=[]` 加 `scopeRoles` 不传):
```json
{
"code": 200,
"message": "成功",
"data": {
"productBatchId": 80001,
"groupBatchId": 90211,
"staffList": [],
"affectedOrderCount": 3
},
"success": true
}
```
#### 错误响应
**错误码 582120**(司机行系统管理,拒绝人工写入):
```json
{
"code": 200,
"message": "司机由车务派车自动带入团期人员,不能在这里新增或删除;如需调整请到车务派单中维护",
"success": false,
"data": null
}
```
**错误码 582116**(导游位只传半个配置位):
```json
{
"code": 200,
"message": "团期人员配置位 GUIDE 成员缺失,导游位(导游+领队)须同时声明两个角色",
"success": false,
"data": null
}
```
**错误码 582115**(staffList 包含 scopeRoles 范围外的角色):
```json
{
"code": 200,
"message": "员工角色 PHOTOGRAPHER 不在覆盖范围 [GUIDE,LEADER] 内,请修正请求",
"success": false,
"data": null
}
```
**错误码 582119**(服务日期校验失败):
```json
{
"code": 200,
"message": "刘领队 的服务日期(2026-10-06 至 2026-10-05)不合法:开始日期不能晚于结束日期,且须在团期日期(2026-10-01 至 2026-10-10)之内",
"success": false,
"data": null
}
```
#### 业务边界
- **权限**: 接口接 `GroupBatchPermissionGuard.PERMISSION_MANAGE`,需要团期管理权限;权限校验在 Controller 层,拒绝时零写入。
- **幂等性**: 同一请求重复提交视为覆盖保存,第二次提交时无新改动则库表无变化、响应同样 200。
- **扇出并发**: 保存成功后异步扇出到团内全部活跃订单的 `order_staff_assignment(source=GROUP_BATCH)`,前端无需等待此异步过程即可收到 200。
- **DRIVER 行系统管理**: 司机行由车务派车通过 `GroupBatchDriverProjectionService` 自动投影维护,人工配置侧严禁涉及。staffList 或 scopeRoles 里出现 DRIVER 一律拒绝(582120),**整批保存零写入**,连软删都不执行。
- **两个读口口径不同**:
- 本接口(PUT)保存成功后返回的 `staffList` **不含司机行**(只含人工配置的非 DRIVER 角色)。
- 查询接口 `GET /v3/admin/group-batch/{productBatchId}/staff` 与单人查询 `GET .../staff/{staffId}` **含司机行**。
- 前端不要假设响应即全量,名册读取时必须从查询接口获取完整名单(含司机)。
- **团期状态检查**: 入口第一步检查团期成团状态(未建团 589553 / 已确认或已取消 589598);不满足直接拒,代码不走到保存逻辑。
- **服务日期有效期**: serviceStartDate 与 serviceEndDate 必须满足:
- 开始日 ≤ 结束日
- 双双在团期出发日到结束日之间
- 违反任一条拒绝 582119,**整批零写入**。
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload | 结果 |
|------|---------|------|
| ✅ 导游位全量覆盖 | `{ "scopeRoles": ["GUIDE","LEADER"], "staffList": [{staffId:40001, staffRole:"LEADER"}, {staffId:40002, staffRole:"GUIDE"}] }` | 200,只有这两人留在 GUIDE 和 LEADER 行,其他既有行不动 |
| ✅ 清空整期 | `{ "staffList": [] }` (不传 scopeRoles) | 200,整期人员全清,库表此后仅含司机行 |
| ✅ 导演位自定义服务日期 | `{ "staffList": [{staffId:40001, staffRole:"LEADER", serviceStartDate:"2026-10-01", serviceEndDate:"2026-10-05"}] }` | 200 |
| ❌ 导游位只传半个配置位 | `{ "scopeRoles": ["GUIDE"], "staffList": [...] }` | 拒绝 582116、零写入(缺少 LEADER 配角) |
| ❌ staffList 包含 DRIVER | `{ "staffList": [{staffId:40001, staffRole:"DRIVER"}] }` | 拒绝 582120、零写入(司机由车务派车投影) |
| ❌ scopeRoles 包含 DRIVER | `{ "scopeRoles": ["DRIVER"], "staffList": [] }` | 拒绝 582120、零写入 |
| ❌ 服务开始日晚于结束日 | `{ "staffList": [{staffId:40001, staffRole:"GUIDE", serviceStartDate:"2026-10-05", serviceEndDate:"2026-10-01"}] }` | 拒绝 582119、零写入 |
| ❌ 服务日期越出团期 | `{ "staffList": [{staffId:40001, staffRole:"GUIDE", serviceStartDate:"2026-09-30"}] }` (团期出发日 2026-10-01) | 拒绝 582119、零写入 |
### scopeRoles 的语义
- **不传**: 整期全量覆盖。staffList 即为团期的最终全量人员配置(除去司机),其他所有角色的既有行会被软删。
- **传了**: 仅覆盖声明的角色。比如只想更新导演位不动摄影位,传 `["GUIDE","LEADER"]` 即可;摄影位的既有行保持不变。
- **导游位特殊性**: 因为导游位同时收 GUIDE 与 LEADER 两个角色,声明导游位必须两个都传。只传其中一个会被拒(582116)——因为「少那一个」意味着覆盖范围不完整,不能正确表达"我只想改导游位"的意图。
- **DRIVER 禁令**: scopeRoles 里出现 DRIVER 拒绝 582120。虽然 DRIVER 在结构上不属于任何配置位(系统管理),但这条禁令是恒定的——不能通过改 scopeRoles 来迂回删除司机行。
---
## 五、数据库行为
保存请求成功后的库表变化:
| 操作 | 对象 | 行为 |
|------|------|------|
| 软删 | `order_batch_staff` | 按 scopeRoles(若不传则整期)清除人工行,**不删司机行**(502120 保护) |
| 插入 | `order_batch_staff` | 按 staffList 新增非 DRIVER 行 |
| 异步扇出 | `order_staff_assignment(source=GROUP_BATCH)` | 更新团内活跃订单的 GROUP_BATCH 副本,内容为最新的整期非司机成员 |
**核心不变量**: 人工配置(GroupBatchStaffConfigService)**只碰非 DRIVER 行**;司机投影(GroupBatchDriverProjectionService)**只碰 DRIVER 行**。两边各管各的子集,交集为空。
---
## 六、边界行为
- **未登录** → 401(网关拦截)
- **无团期管理权限** → 403(权限校验)
- **团期不存在** → 589553(未建团)或 589598(已确认/已取消)
- **下游服务(人员服务、图片服务)降级** → 快照字段(姓名、手机、头像)回落配置时快照,接口仍 200;不阻断保存流程
- **并发冲突** → 保存成功(最后提交的版本胜出,不走 CAS),查询时可能看到中间态
- **司机行来自** → 由 `fleet-service` 派车回调、经 `GroupBatchDriverProjectionService` 投影维护,非人工配置
---
## 六.5、枚举
### staffRole(员工角色)
**所属字段**: `staffList[].staffRole` (请求) / `data.staffList[].staffRole` (响应) | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `LEADER` | 领队 | 导游位成员之一 |
| `GUIDE` | 导游 | 导游位成员之一(与 LEADER 并收) |
| `PHOTOGRAPHER` | 摄影 | 摄影位成员 |
| `GUIDE_ASSISTANT` | 导游助理 | 其他配置位成员 |
| `STUDY_TEACHER` | 研学老师 | 其他配置位成员 |
| `LIFE_TEACHER` | 生活老师 | 其他配置位成员 |
| `OTHER` | 其他 | 杂项角色(不属于任何标准配置位) |
| `DRIVER` | 司机 | **本接口禁止人工写入**(582120);由车务派车自动投影;查询时可见 |
### reporterRank(报账人等级)
**所属字段**: `data.staffList[].reporterRank` (响应) | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `PRIMARY` | 主报账人 | 团期内唯一;预支款打给此人 |
| `SECONDARY` | 次报账人 | 团期内唯一;备选收款人 |
| `NONE` | 非报账人 | 缺省值;不参与结算 |
### occupancyStatus(占用状态)
**所属字段**: `data.staffList[].occupancyStatus` (响应) | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `FREE` | 空闲 | 有效服务日期段内无其他团期或订单占用 |
| `PARTIAL` | 部分占用 | 有效日期段内某些天被占,某些天可派 |
| `FULL` | 全程占用 | 有效日期段全部被占,无可派日期 |
| `null` | - | 团期未建成或服务日期不完整时为 null;仅提示,不拦截提交 |
---
## 六.6、修改前后对比
### 接口逻辑变化
| 维度 | 改前 | 改后 |
|------|------|------|
| **司机行管理** | 团期人员配置由运营全手工维护,无系统投影 | 司机行由车务派车自动投影,人工配置侧严禁涉及 |
| **保存响应** | 返回整期人员全量(包含一切手工配置) | 返回**非司机成员**快照(DRIVER 行被筛除) |
| **查询响应** | 同保存响应 | **含司机行**(与保存响应口径不同) |
| **错误码新增** | 无 DRIVER 相关拒绝 | 新增 582120:司机行拒绝码,整批零写入 |
| **权限校验** | 无 | 新增 Controller 层权限校验(PERMISSION_MANAGE),拒绝时零写入 |
### 字段级变化
| 字段 | 改前状态 | 改后状态 |
|------|----------|----------|
| `serviceStartDate` / `serviceEndDate` | 无此字段 | 新增可选字段;支持按人自定义服务有效期 |
| `serviceDateCustom` | 无 | 新增,标记是否自定义过服务日期 |
| `baseDailyWage` | 无 | 新增,人员档案快照(配置时带出,不可修改) |
| `occupancyStatus` / `occupiedDays` / `freeRanges` | 无 | 新增,占用查询结果(#8468 D8,仅提示不拦截) |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 是(一定程度)
- 响应体新增字段:`serviceStartDate` / `serviceEndDate` / `serviceDateCustom` / `baseDailyWage` / `occupancyStatus` / `occupiedDays` / `freeRanges` / `occupancies`,但字段全可空,字段层兼容。
- 响应 `staffList` 内容变化:**不再包含司机行**(之前如果有系统产生的司机行会出现,现在被筛除)。前端若依赖"返回即全量"会破损。
- 新增错误码 582120:拒绝 DRIVER 行,整批零写入——改动前的正常请求可能现在被拒。
- **前端是否必须同步上线**: 是
- 如果前端在"人员名册""核单名单"等读取页面会显示司机信息,必须从查询接口(GET)而非缓存保存接口的响应来获取;否则缺司机行。
- 如果前端在人员编辑表单提供了 DRIVER 行的编辑/删除入口,必须移除,改为呈现"这是由车务派车产生的"提示。
- UI 需要适配新字段(服务日期、日薪、占用)的显示。
- **前端 workaround 清理点**:
- 若前端之前硬编码了"司机只能通过XX页面配置",现在这句不再对。
- 若前端假设"保存响应即全量名册"来渲染名册卡片,需改为调查询接口。
- 若前端在人员编辑表单里有 DRIVER 选项,需删除;前端用户无法(也不应该)在这里新增/删除司机。
---
## 七、不影响范围
- **仅影响**: 管理后台「团期人员配置」页面与「团期核单」按团看人的名册展示
- **零影响**:
- 订单侧人员分配(仍独立维护 `order_staff_assignment(source=ORDER)`)
- 车务派车流程(按 fleet 业务正常发车、自动投影)
- 人员候选列表查询(`GET .../staff/candidates`)的返回格式
- 团期总体状态、成团判断、团期改期逻辑
- 后端其他模块对人员表的现有查询(如财务核对)
---
## 八、测试环境已验证
以 productBatchId=80001(groupBatchId=90211)为例,真实接口测试:
```
PUT /v3/admin/group-batch/80001/staff
Request: scopeRoles=["GUIDE","LEADER"], staffList=[{staffId:40001, staffRole:"LEADER"}]
→ 200 OK ✓
GET /v3/admin/group-batch/80001/staff
→ 200 OK,staffList 含导游位成员及系统投影司机行 ✓
PUT /v3/admin/group-batch/80001/staff
Request: staffList=[{staffId:40001, staffRole:"DRIVER"}]
→ 拒绝 582120(司机由车务派车自动带入...)✓
PUT /v3/admin/group-batch/80001/staff
Request: scopeRoles=["GUIDE"], staffList=[...](缺 LEADER)
→ 拒绝 582116(导游位成员缺失...)✓
```
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|-----------|
| #8669 | #8653, #8654 | 本次变更:司机投影落地,DRIVER 改系统管理 | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [#8653](https://git.1814.love:8443/wx/HL/issues/8653), [#8654](https://git.1814.love:8443/wx/HL/issues/8654)
- 关联 PR: [#8669](https://git.1814.love:8443/wx/HL/pulls/8669)
- Merge commit: [5c50782717d6](https://git.1814.love:8443/wx/HL/commit/5c50782717d6)
---
## 关联 / 联系人
### 链接
- **Issue**: [#8653](https://git.1814.love:8443/wx/HL/issues/8653) / [#8654](https://git.1814.love:8443/wx/HL/issues/8654)
- **PR**: [#8669](https://git.1814.love:8443/wx/HL/pulls/8669)
- **Merge commit**: [5c50782717d6](https://git.1814.love:8443/wx/HL/commit/5c50782717d6)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,224 @@
---
schema: "hl-changelog/v2"
ticket: "8655"
title: "资金统计某日资金明细页补筛选控件对接指引(科目/账户/收支),接口已支持零改动"
consumer: "admin"
author: "yst(GIT)"
change_type: "修改接口"
backend_status: "merged"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "e4de4c1f600d4a0738be9bbb207407e3823b6f2a"
target_release: "v2.1"
verified_at: "2026-09-30"
base: "dev-v3"
updated_at: "2026-09-30"
status_note: "资金账户→资金统计点某日进明细页目前只有日期+分页,缺科目/账户/收支筛选控件。核对后端明细接口 GET /admin/finance/fund-flows/page:bizType(科目)/fundAccountId(账户)/direction(收支)/flowAtStart/flowAtEnd 等筛选早已支持,出参行已带 bizTypeName/accountName 中文。本文档纯对接指引、接口零改动,告知前端现有筛选参数+三个下拉数据源(科目/收支硬编码枚举、账户调 fund-accounts/page?status=ACTIVE),前端补渲染筛选控件即可。;前端已交付:三筛选控件实证已在,补 FUND_FLOW_BIZ 缺 ORDER_REFUND(14 值)+账户下拉 status=ACTIVE,17 例定向全绿"
---
# 【修改接口·管理后台】资金统计某日资金明细页补筛选控件对接指引(科目/账户/收支) (#8655)
> **PR**: 无(纯对接指引,接口未改动) | **服务**: hl-finance(编译进 hl-order-service-v3) | **更新时间**: 2026-09-30 14:00
## 1. 接口背景
资金账户 → 资金统计查询,点击某日跳到「资金明细」页时,目前只带了日期(`flowAtStart`/`flowAtEnd`)+ 分页参数,**页面上没有科目、账户、收支的筛选控件**。
经核对后端明细接口,这些筛选条件**接口全部已支持**,后端无需任何改动。本文档是**前端对接指引**:告知明细接口现有可用筛选参数 + 三个下拉的数据源,前端在明细页补渲染筛选控件即可。
> ⚠️ 本接口本次**无任何改动**(入参/出参/枚举/错误码均不变),仅是把「早已支持但前端没用上」的筛选参数同步给前端。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 全账户资金流水分页 | GET | `/admin/finance/fund-flows/page` | 无变更(对接指引) | 现有筛选参数梳理,前端补控件 |
## 3. 接口详情
### 3.1 全账户资金流水分页(逐笔含结存快照)
- **使用场景**:资金账户 → 资金统计 → 点某日 → 查当日资金明细流水;也可按科目/账户/收支组合筛选
- **认证**:管理后台 JWT
- **幂等性**:是(GET 查询)
- **限流**:无
#### 入参(Query)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `flowAtStart` | String(date) | ❌ | 收付日期起 yyyy-MM-dd;点某日明细时与 flowAtEnd 传同一天 |
| `flowAtEnd` | String(date) | ❌ | 收付日期止 yyyy-MM-dd |
| `bizType` | String | ❌ | **科目筛选**:业务类型,单值,取值见 §6.1 |
| `fundAccountId` | String(Long) | ❌ | **账户筛选**:单个账户 ID(下拉数据源见 §6.3) |
| `accountType` | String | ❌ | 账户类型批量筛选(备选):`BANK`/`CASH`/`THIRD_PARTY`/`INTERNAL_VIRTUAL`,筛该类账户的全部流水 |
| `direction` | String | ❌ | **收支筛选**:`IN` 收入(入账)/ `OUT` 支出(出账) |
| `flowNo` | String | ❌ | 流水号模糊(可选) |
| `bizId` | String(Long) | ❌ | 业务单据 ID(按单据捞明细,与 bizType 组合,可选) |
| `pageNo` | Integer | ✅ | 页码,从 1 开始 |
| `pageSize` | Integer | ✅ | 每页条数 |
> 说明:`fundAccountId`(单账户)与 `accountType`(按类型)二选一即可,都用则同时生效(交集)。
#### 出参(`Result<PageResult<行>>`)
每行流水字段(前端直接渲染,**业务类型中文、账户名后端已带,无需前端再翻译**):
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | String | 流水 ID |
| `flowNo` | String | 流水号 |
| `fundAccountId` | String | 账户 ID |
| `accountName` | String | **账户名称(已带,直接渲染)** |
| `accountType` | String | 账户类型码值 |
| `direction` | String | 方向 `IN`/`OUT` |
| `amount` | Number | 金额 |
| `bizType` | String | 业务类型码值 |
| `bizTypeName` | String | **业务类型中文名(已带,直接渲染,如 ORDER_REFUND=订单退款)** |
| `bizId` | String | 关联业务单据 ID |
| `bizNo` | String | 业务单据号(手写动作 TRANSFER/INVENTORY/OPENING 恒为 null) |
| `balanceAfter` | Number | 本笔记完后账户结存快照 |
| `transferGroupId` | String | 互转成对组号(仅 TRANSFER) |
| `fee` | Number | 手续费(仅 TRANSFER 转出行) |
| `counterparty` | String | 对方户名(已脱敏) |
| `flowAt` | String | 收付时间 yyyy-MM-dd HH:mm:ss |
| `voucherUrl` | String | 回单凭证影像 |
| `remark` | String | 备注 |
## 6. 枚举 / 数据字典
### 6.1 bizType(科目,FundFlowBizTypeEnum)
**所属字段**:入参 `bizType` / 出参 `bizType`、`bizTypeName` | **类型**:`String` | **必填**:❌
流水无数据字典,中文名后端固定,前端科目下拉**直接硬编码这张映射**(出参 `bizTypeName` 已带中文,列表无需前端 map):
| 值 | 中文 | 说明 |
|----|------|------|
| `PAYMENT` | 付款 | 应付款 |
| `PREPAY` | 预付 | 预付款 |
| `EXPENSE` | 费用 | 费用报销 |
| `REIMBURSE` | 报账 | 报账款 |
| `RECEIPT` | 收款确认 | 代收上交 |
| `STAFF_LOAN` | 员工借款 | 付讫 OUT / 还款 IN |
| `COMPANY_LOAN` | 公司借款 | 借出 OUT / 借入 IN,归还/收回反向 |
| `NONBIZ` | 业务外收支 | 收入 IN / 支出 OUT |
| `ADVANCE` | 司导预支 | 预支出账流水 |
| `TRANSFER` | 账户互转 | 成对,含商户号提现归集 THIRD_PARTY→BANK |
| `ORDER_PAY` | 对公收款 | 订单支付流水,自动生成不经出纳 |
| `ORDER_REFUND` | 订单退款 | OUT 流水,自动生成不经出纳 |
| `INVENTORY` | 盘盈盘亏 | SURPLUS→IN / DEFICIT→OUT |
| `OPENING` | 期初调整 | IN=调高 / OUT=调低 |
### 6.2 direction(收支)
**所属字段**:入参/出参 `direction` | **类型**:`String` | **必填**:❌
前端收支下拉固定两项,硬编码:
| 值 | 中文 | 说明 |
|----|------|------|
| `IN` | 收入 | 入账 |
| `OUT` | 支出 | 出账 |
### 6.3 账户下拉数据源(fundAccountId 选项)
调账户档案列表接口取账户选项:
`GET /admin/finance/fund-accounts/page?status=ACTIVE&pageSize=100`
- `status=ACTIVE` 只拉启用账户(停用账户不出现)
- 返回行含 `id`(作为 `fundAccountId` 值)、`accountName`、`accountTypeName`(现金/银行/第三方支付),可直接做下拉显示
- 该接口自身也支持 `accountType`/`nature`/`keyword` 入参,可用于账户下拉的级联筛选
## 7. 错误码
本接口为查询接口,无业务错误码;参数非法(如日期格式错误)返回通用 400。无新增/变更。
## 8. 示例
### 8.1 典型:查某日 + 科目=业务外收支 + 收入
**请求**:
```
GET /admin/finance/fund-flows/page?flowAtStart=2026-09-30&flowAtEnd=2026-09-30&bizType=NONBIZ&direction=IN&pageNo=1&pageSize=10
Authorization: Bearer {admin-token}
(无请求体)
```
**响应**:
```json
{
"code": 200,
"data": {
"total": 1,
"records": [
{
"id": "1962000000000000631",
"flowNo": "LS202609300001",
"fundAccountId": "1962000000000000503",
"accountName": "工商银行海拉尔支行基本户",
"accountType": "BANK",
"direction": "IN",
"amount": 990.00,
"bizType": "NONBIZ",
"bizTypeName": "业务外收支",
"bizId": "1962000000000000903",
"bizNo": "WS-20260930-001",
"balanceAfter": 582995.00,
"transferGroupId": null,
"fee": null,
"counterparty": "某某单位",
"flowAt": "2026-09-30 10:00:00",
"voucherUrl": null,
"remark": null
}
]
},
"message": "success"
}
```
### 8.2 边界:按账户类型批量筛 + 支出
**请求**:
```
GET /admin/finance/fund-flows/page?accountType=BANK&direction=OUT&pageNo=1&pageSize=10
Authorization: Bearer {admin-token}
(无请求体)
```
**响应**:结构同上,`records` 为所有银行账户的支出流水(空则 `records: []`、`total: 0`)。
## 9. 业务边界
- ✅ 所有筛选参数均可选、可任意组合,均不传=全量资金流水分页(flowAt 倒序)
- ✅ 点某日明细:`flowAtStart` 与 `flowAtEnd` 传同一天
- ⚠️ `accountType` 筛选是「该类型全部账户的流水」,流水表不存账户类型,后端先反查该类型账户 ID 集再过滤
- ⚠️ `fundAccountId` 与 `accountType` 同时传时取交集
## 11. 影响评估
- **是否破坏向后兼容**:否(接口零改动)
- **前端是否必须同步上线**:否(前端按需补筛选控件即可,不补也不影响现有功能)
## 12. 注意事项
- 本接口本次无任何改动,本文档仅为前端补筛选控件提供对接参数与数据源。
- 科目(bizType)与收支(direction)下拉前端硬编码 §6.1/§6.2 映射即可;账户下拉调 §6.3 接口取启用账户。
- 流水行出参已带 `bizTypeName`/`accountName` 中文,前端列表直接渲染,无需维护码值→中文 map。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#8655](https://git.1814.love/wx/HL/issues/8655)
- **PR**: 无(纯对接指引,无代码改动)
### 13.2 联系人
- **后端负责人**: @yst
@@ -0,0 +1,194 @@
---
schema: "hl-changelog/v2"
ticket: "8657"
title: "收款方类型 payeeType 枚举重构:删 STAFF,个人类拆为 GUIDE/DRIVER/PHOTOGRAPHER/LEADER(导游/司机/摄影/领队),保留 FLEET/GUIDE_CO/SUPPLIER(#8657)"
consumer: "admin"
author: "yst(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "adfd68151163824338fa4ee6faef38c84a5bb006"
target_release: ""
verified_at: "2026-09-30"
status_note: "财务「资金账户→收款账户」的「收款方类型」(payeeType,枚举非数据字典)重理口径。原 STAFF 标「司导/员工个人」表述错误:司导(导游/司机/摄影/领队)是带团服务人员、非内部员工(与司导往来账 GuideStaffRoleEnum 同源口径,员工借款不纳入)。个人类按带团角色拆为 GUIDE/DRIVER/PHOTOGRAPHER/LEADER 四类;FLEET 车队/GUIDE_CO 导游公司/SUPPLIER 供应商三个组织类保留。⚠️破坏性:删除 STAFF 枚举值——前端若写死 STAFF 选项需清理;新增 4 个个人类值需补充到下拉选项与 label 映射。后端校验 PayeeTypeEnum.of() 随枚举自动生效,传 STAFF 现报非法。;前端已交付:PAYEE_TYPES 删 STAFF 拆 GUIDE/DRIVER/PHOTOGRAPHER/LEADER 四角色(label §12 映射),checkpoint 全量档全绿"
updated_at: "2026-09-30"
base: "dev-v3"
---
# finance:收款方类型 payeeType 枚举重构(管理后台)
> ⚠️ **破坏性变更**:`payeeType` 删除枚举值 `STAFF`,新增 `GUIDE`/`DRIVER`/`PHOTOGRAPHER`/`LEADER`。前端如有写死的 payeeType 选项/label 映射需同步更新。
## 1. 接口背景
财务「资金账户 → 收款账户」登记收款方账户时,「收款方类型」(`payeeType`)标识「钱打给谁的结算主体」。
原枚举 `STAFF` 注释中文标「司导/员工个人」——**表述错误**:司导(导游/司机/摄影/领队)是带团服务人员,**非内部员工**(与司导往来账口径一致,员工借款不纳入司导范围)。且「个人」粒度过粗。
本次把个人类按带团角色直接拆分为 **司机/导游/摄影/领队** 四类,与 `GuideStaffRoleEnum`(司导角色)同源。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 收款方账户分页 | GET | `/admin/finance/payee-accounts/page` | 修改接口 | `payeeType` 过滤值集合变化 |
| 2 | 登记收款方账户 | POST | `/admin/finance/payee-accounts` | 修改接口 | `payeeType` 入参枚举值集合变化 |
> 两个接口路径/方法/其他字段不变,仅 `payeeType` 字段的**合法取值集合**变化。
## 3. 接口详情
### 3.1 收款方账户分页
- **使用场景**:收款账户列表,按收款方类型/名称/账户类型过滤
- **认证**:JWT(管理后台)
- **入参(query)**:`payeeType`(可选过滤,取值见 §6)/ `payeeName`(名称模糊)/ `accountType` / `pageNo` / `pageSize`
- **出参**:`data.records[].payeeType` 返回枚举码(GUIDE/DRIVER/...)
### 3.2 登记收款方账户
- **使用场景**:新增一条收款方账户(个人码/银行卡/对公)
- **认证**:JWT(管理后台)
- **入参(body)**:`payeeType`(必填,取值见 §6)/ `payeeRefId` / `payeeName` / `accountType` / `qrUrl` / `bankAccount` / `bankName` / `isDefault`
- **校验**:`payeeType` 非法(含已删除的 `STAFF`)→ `595202`
## 4. 接口入参
### 4.1 请求体关键字段(登记)
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| payeeType | string | 是 | 收款方类型(见 §6) | 须为合法枚举值,否则 595202 |
## 5. 出参(响应)
| 字段 | 类型 | 说明 |
|------|------|------|
| payeeType | string | 收款方类型枚举码(见 §6) |
## 6. 枚举 / 数据字典
### 6.1 payeeType(收款方类型,PayeeTypeEnum)
**所属字段**:`payeeType` | **类型**:`String` | **必填**:✅(登记时)
| 值 | 中文 | 类别 | 说明 |
|----|------|------|------|
| `GUIDE` | 导游 | 个人 | 🆕 打给导游个人(非内部员工) |
| `DRIVER` | 司机 | 个人 | 🆕 打给司机个人(非内部员工) |
| `PHOTOGRAPHER` | 摄影 | 个人 | 🆕 打给摄影个人(非内部员工) |
| `LEADER` | 领队 | 个人 | 🆕 打给领队个人(非内部员工) |
| `FLEET` | 车队 | 组织 | 司机挂车队,打给车队再分(保留) |
| `GUIDE_CO` | 导游公司 | 组织 | 导游挂公司,打给公司再分(保留) |
| `SUPPLIER` | 供应商 | 组织 | 预留(保留) |
| ~~`STAFF`~~ | ~~司导/员工个人~~ | — | ❌ **已删除**(语义混乱:司导非员工) |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| 595202 | 收款方类型/账户类型非法等组合校验失败 | `payeeType` 传非法值(含已删除的 `STAFF`) |
## 8. 示例(3 组)
### 8.1 典型成功(登记导游个人收款账户)
**请求** `POST /admin/finance/payee-accounts`:
```json
{
"payeeType": "GUIDE",
"payeeName": "张三",
"accountType": "WECHAT_QR",
"qrUrl": "https://oss.example.com/qr/zhangsan.png",
"isDefault": 1
}
```
**响应**:
```json
{ "code": 200, "message": "成功", "data": { "id": "2105..." }, "success": true }
```
### 8.2 边界(车队组织账户)
**请求** `POST /admin/finance/payee-accounts`:
```json
{
"payeeType": "FLEET",
"payeeName": "XX 车队",
"accountType": "CORP_ACCOUNT",
"bankAccount": "6222...",
"bankName": "工商银行海拉尔支行"
}
```
**响应**:
```json
{ "code": 200, "message": "成功", "data": { "id": "2105..." }, "success": true }
```
### 8.3 业务失败(传已删除的 STAFF)
**请求** `POST /admin/finance/payee-accounts`:
```json
{
"payeeType": "STAFF",
"payeeName": "张三",
"accountType": "WECHAT_QR"
}
```
**响应**:
```json
{ "code": 595202, "message": "收款方类型非法", "success": false }
```
## 9. 业务边界
- ✅ 个人类收款方:用 `GUIDE`/`DRIVER`/`PHOTOGRAPHER`/`LEADER`(按带团角色选)。
- ✅ 组织类收款方:司机挂车队用 `FLEET`、导游挂公司用 `GUIDE_CO`。
- ❌ `STAFF` 已不可用,传了报 `595202`。
- ⚠️ 内部员工(非带团服务人员)不在本收款方类型范围;员工借款走单独的 fin_staff_loan 流程,不在此登记。
## 10. 修改前后对比
### 10.1 枚举值集合对比
| 类别 | 改前 | 改后 |
|------|------|------|
| 个人 | `STAFF`(司导/员工,1 个粗粒度值) | `GUIDE`/`DRIVER`/`PHOTOGRAPHER`/`LEADER`(4 个角色值) |
| 组织 | `FLEET`/`GUIDE_CO`/`SUPPLIER` | 不变 |
### 10.2 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 传 `STAFF` 登记 | 合法 | 报 `595202` 非法 |
| 个人类收款方粒度 | 只有「司导/员工」一项 | 按导游/司机/摄影/领队四角色细分 |
## 11. 影响评估 / 回滚
- **是否破坏向后兼容**:**是**(删 `STAFF` 枚举值)。
- **前端是否必须同步上线**:是。前端如有写死的 `payeeType` 选项/label 映射需更新:① 删 `STAFF` 选项;② 补 `GUIDE/DRIVER/PHOTOGRAPHER/LEADER` 四项及中文 label。
- **影响已有数据**:测试库 `fin_payee_account` 无 STAFF 存量数据,无需迁移。
- **回滚方式**:revert PR #8658。
## 12. 注意事项
- **前端 workaround 清理点**:若前端此前把 `STAFF` 硬编码为唯一个人类选项,请改为按导游/司机/摄影/领队四项。
- 中文 label 由前端映射(后端只下发枚举码):`GUIDE=导游 / DRIVER=司机 / PHOTOGRAPHER=摄影 / LEADER=领队 / FLEET=车队 / GUIDE_CO=导游公司 / SUPPLIER=供应商`。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#8657](https://git.1814.love/wx/HL/issues/8657)
- **PR**: [#8658](https://git.1814.love/wx/HL/pulls/8658)
- **Merge commit**: [8c7520d0da](https://git.1814.love/wx/HL/commit/8c7520d0da)
### 13.2 联系人
- **后端负责人**: @yst
- **前端对接(管理后台)**: 待认领
@@ -0,0 +1,171 @@
---
schema: "hl-changelog/v2"
ticket: "8663"
title: "公司借款域:往来单位放开员工类型 + 归还菜单归支付管理 + 归还列表改双 tab(#8663)"
consumer: "admin"
author: "yst(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "305ba037d1a184c9bd6bc131d004abd1f1bd6349"
target_release: "v2.1"
verified_at: "2026-10-01"
status_note: "2026-10-01 hl-admin 交付:①登记表单加单位类型 select(字典 fin_unit_type 优先+本地两码兜底),SUPPLIER 走供应商弹窗/STAFF 走 employee-options(adminId 作 unitId),切类型清空已选防串单,payload 带 unitType,列表单位类型列补 STAFF 中文;②Cashier 聚合行 unitType 带回明细查询与 settle(599312 拦截器透 message);③页体 git mv payable/company-loan-repay→pay/company-loan-repay+路由 spec 同步;④IN 收银台 n-tabs 双 tab,已还台账=repays/page?direction=IN 手工分页+只读查看弹窗。4 spec 45 例全绿,ref 305ba037。公司借款域四项调整。①借入/借出往来单位放开单位类型(unitType),供应商 SUPPLIER 与员工 STAFF 二选一,按类型联动供应商/员工选择框;②归还/回收结算请求 VO 新增可选入参 unitType(连同 unitId 一起校验归属,防两套 ID 空间同数值串单),additive 兼容不传按旧口径;③「公司借款归还」菜单从付款管理搬到支付管理(path /finance/payable/company-loan-repay → /finance/pay/company-loan-repay),前端路由需同步,否则点菜单 404;④借入列表删「去还款」按钮、归还列表改「待还款/已还台账」双 tab(后端零新增接口,复用现成收银台与流水接口)。新增数据字典 fin_unit_type(供应商/员工)。settle 单位一致性守卫加 unit_type 比对,勾选单与入参单位类型不符报 599312。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# finance:公司借款域往来单位放开员工类型 + 归还归支付管理 + 双 tab(管理后台)
> ⚠️ **菜单路由变更**:「公司借款归还」从付款管理搬到支付管理,path 由 `/finance/payable/company-loan-repay` 改为 `/finance/pay/company-loan-repay`。前端路由必须同步,否则点菜单 404。
> ⚠️ **入参变更**:结算接口新增可选 `unitType`;**出参变更**:列表/聚合/流水行新增 `unitType` 字段;**枚举变更**:新增 `FinUnitTypeEnum`(SUPPLIER/STAFF)+ 数据字典 `fin_unit_type`。
## 1. 接口背景
财务「收款管理/公司借入」登记公司向外部单位借入的款项,「支付管理/公司借款归还」由出纳按单位聚合归还。原实现往来单位**只支持供应商**(unit_type 恒 SUPPLIER),业务需支持员工;且「归还」本质是出纳选付款账户实付,原挂在付款管理(台账/审批层)不对称;已核销单从归还列表消失无处查看。
本次合并处理四块:①往来单位放开单位类型(供应商/员工)+ 选择框联动;②结算守卫防供应商/员工 ID 同数值串单;③归还菜单归支付管理;④借入列表删还款按钮、归还列表改双 tab。
## 2. 变更清单
| # | 项 | 变更 | 端点 |
|---|---|---|---|
| 1 | 往来单位类型 | 新增可选入参 `unitType`(SUPPLIER/STAFF,默认 SUPPLIER),出参行新增 `unitType` | 创建/列表/回收聚合/归还聚合 |
| 2 | 结算归属校验 | 新增可选入参 `unitType`,连同 `unitId` 一起校验 | repay-cashier/settle、recover/settle |
| 3 | 菜单迁移 | 归还菜单付款管理→支付管理,path 变更 | (前端路由) |
| 4 | 列表职责 | 借入列表删「去还款」按钮;归还列表改「待还款/已还台账」双 tab | (前端 UI,后端复用现成接口) |
| 5 | 数据字典 | 新增 `fin_unit_type`:SUPPLIER 供应商 / STAFF 员工 | dict/data/fin_unit_type |
## 3. 接口详情
统一前缀 `POST|GET /admin/finance/company-loans/**`(业务调用**不带** `/hl-order-service-v3` 前缀,网关按 `/admin/finance/**` 路由)。
## 4. 入参
### 4.1 创建借入 `POST /admin/finance/company-loans`
```json
{
"direction": "IN",
"unitType": "SUPPLIER 或 STAFF(可选,默认 SUPPLIER)",
"unitId": "按类型:供应商ID 或 员工 adminId",
"handlerStaffId": "经办人 adminId(必填)",
"amount": "借款金额>0",
"feeRate": "手续费率‰,空按0",
"loanDate": "借款日期",
"dueDate": "约定归还日期(必填)",
"purpose": "借款用途(必填)"
}
```
> `unitName` / `handlerStaffName` 不用前端传(服务端反查覆盖快照,前端传值不生效)。
### 4.2 归还结算 `POST /admin/finance/company-loans/repay-cashier/settle`
```json
{
"unitId": "单位ID",
"unitType": "SUPPLIER 或 STAFF(建议必传,防串单)",
"loanIds": ["整笔归还的借入单ID,≥1,每笔还欠还全额"],
"fundAccountId": "出账资金账户ID(必填,出纳选从哪张卡出钱)",
"feeRate": "手续费率‰,默认0",
"voucherUrl": "付款凭证影像URL"
}
```
### 4.3 回收结算 `POST /admin/finance/company-loans/recover/settle`
同 4.2 结构(借出方向回收),`unitType` 同为可选。
## 5. 出参
### 5.1 借款行 `CompanyLoanRowRespVO`(GET /page)
`id, loanNo, direction, unitType🆕, unitId, unitName, handlerStaffId, handlerStaffName, amount, repaidAmount, outstandingAmount, purpose, loanDate, dueDate, status, operatorName, createTime`(ID 均字符串)
### 5.2 归还单位聚合 `CompanyLoanRepayUnitRespVO`(GET /repay-cashier/units)
`unitType🆕, unitId, unitName, loanCount, totalOutstanding, nearestDueDate`
### 5.3 还款流水行 `CompanyLoanRepayRowRespVO`(GET /repays/page)
`id, repayNo(GH-前缀), loanId, loanNo, direction, unitName, amount, fundAccountId, repaidAt, voucherUrl`
## 6. 枚举/数据字典
### FinUnitTypeEnum(往来单位类型,枚举)
| 值 | 含义 |
|---|---|
| SUPPLIER | 供应商(资源域 supplier_main) |
| STAFF | 员工(用户域 admin_user,名称取企微快照) |
### 数据字典 `fin_unit_type`
`GET /admin/dict/data/fin_unit_type` →
```json
[{"dictLabel":"供应商","dictValue":"SUPPLIER"},{"dictLabel":"员工","dictValue":"STAFF"}]
```
### 借款状态 status(出参)
APPROVED 已登记 / PAID 已收付 / SETTLING 核销中 / SETTLED 已核销
## 7. 错误码
| 码 | 含义 |
|---|---|
| 599304 | 往来单位非法(不存在/不可选/类型非法/员工未登记真名) |
| 599305 | 经办人非法(不存在或未登记真名) |
| 599312 | 勾选单与入参单位不一致(unit_id 或 unit_type 不符,防串单) |
## 8. 示例
### 8.1 典型:创建员工类型借入
```http
POST /admin/finance/company-loans
{"direction":"IN","unitType":"STAFF","unitId":"2021059720172838914","handlerStaffId":"2085621600417148929","amount":5000,"loanDate":"2026-09-30","dueDate":"2026-10-30","purpose":"员工临时周转"}
→ {"code":0,"data":{"id":"...","loanNo":"GS-202609300005"}}
```
### 8.2 边界:归还按单位聚合(待还款 tab)
```http
GET /admin/finance/company-loans/repay-cashier/units?unitName=王
→ {"code":0,"data":[{"unitType":"STAFF","unitId":"2021...","unitName":"王骁","loanCount":1,"totalOutstanding":5000,"nearestDueDate":"2026-10-30"}]}
```
点单位看明细:`GET /repay-cashier/units/{unitId}?unitType=STAFF`(**unitType 必传**,从聚合行带回)。
### 8.3 异常:结算单位类型不符
```http
POST /repay-cashier/settle {"unitId":"X","unitType":"STAFF","loanIds":[...], "fundAccountId":"..."}
# 该 unitId 实为 SUPPLIER 类型借款
→ {"code":599312,"msg":"勾选单与入参单位不一致"}
```
## 9. 业务边界
- 借入列表(收款管理)`GET /page?direction=IN`:`status` 不传即全量(含 SETTLED),删「去还款」按钮后纯台账。
- 待还款 tab 复用 `/repay-cashier/units` + `/repay-cashier/units/{unitId}`,**只返回未核销单**(PAID/SETTLING),已核销不出现。
- 已还台账 tab 用 `GET /repays/page?direction=IN`,每行一笔已还款事实,只读「查看」。
## 10. 修改前后对比
| 项 | 修改前 | 修改后 |
|---|---|---|
| 往来单位类型 | 恒供应商 | 供应商/员工二选一(unitType 联动选择框) |
| 归还菜单位置 | 付款管理/公司借款归还 | 支付管理/公司借款归还 |
| 菜单 path | /finance/payable/company-loan-repay | /finance/pay/company-loan-repay |
| 借入列表 | 行有「去还款」按钮 | 删按钮,纯台账 |
| 归还列表 | 已核销单消失无处查 | 待还款/已还台账双 tab |
| settle 归属校验 | 只比 unit_id | 加 unit_type 比对(防串单) |
## 11. 影响评估/回滚
- 入参 `unitType` 为可选 additive,不传按旧口径(SUPPLIER)兼容,旧前端不炸。
- 出参新增 `unitType` 字段为 additive,旧前端忽略即可。
- **菜单 path 变更为破坏性**:前端路由必须同步,否则点菜单 404。
- 回滚:菜单迁移走 sys_menu UPDATE(带守卫+幂等),可回写旧 path。
## 12. 注意事项
- ⚠️ 归还单单位明细接口 `unitType` **建议必传**(防供应商/员工 ID 同数值串单),从聚合行带回。
- 员工选择框数据源 `GET /admin/user/employee-options`(取 `adminId` 作 unitId,`enterpriseWechatName` 为空回落 `username`)。
- 供应商选择框数据源 `GET /admin/supplier/items/page?status=ACTIVE`(取 `supplierId` 作 unitId)。
- 员工未登记企微真名时报 599304(后端 fail-fast)。
## 13. 关联/联系人
- Issue:https://git.1814.love/wx/HL/issues/8663
- PR:https://git.1814.love/wx/HL/pulls/8668
- merge commit:154e2ac75a
- 后端负责人:腰苏图
@@ -0,0 +1,209 @@
---
schema: "hl-changelog/v2"
ticket: "8664"
title: "公司借款收回新增单笔形态:POST /admin/finance/company-loans/{id}/recover"
consumer: "admin"
author: "yst(GIT)"
change_type: "新增接口"
backend_status: "merged"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "c32b35c9527dde6bdc21386e1c6d47ad1f33bb96"
target_release: "v2.1"
verified_at: "2026-10-01"
base: "dev-v3"
updated_at: "2026-10-01"
status_note: "公司借款「借出方向」的收回(借出的钱到期收回)新增单笔形态——已付款借出单按单笔收款,与既有按单位合并收款(§1.4.3 recover/settle)共用 settle 核心。原型收回入口并入「支付管理/公司借款支付」已付款台账行「收款登记」;原「收款管理/公司借款收回」按单位汇总收银台原型入口下线(合并收款三端点后端保留可用)。前端已交付:入口挂「支付管理/公司借款支付」已付款台账行「收款登记」,弹窗拉详情默认欠收全额可改部分,fee 内扣预览,提交即禁按钮防连点,SETTLED 禁提交,收齐按 settledLoanIds 提示核销并刷台账;unitId/unitType 不入参。(此前 frontmatter 误标 implemented 无 ref,本次实证交付后补齐。)"
---
# 【新增接口·管理后台】公司借款单笔收回 POST /admin/finance/company-loans/{id}/recover (#8664)
> **PR**: #8670 | **服务**: hl-order-service-v3(hl-finance 模块,8086) | **更新时间**: 2026-09-30
## 1. 接口背景
公司借款「借出方向」(direction=OUT,公司借钱给供应商/员工)的到期收回,原只有「按单位汇总收银台」一种形态(勾选同一单位多笔借款单合并收款,§1.4.1-1.4.3)。本次按业务拍板新增**单笔形态**:在「支付管理 / 公司借款支付」的**已付款台账行**上对**单笔借出单**直接「收款登记」——已付款的借出单逐笔收回,无需按单位聚合勾选。
**只写"为什么",不涉及实现**:收回动作的入口从「按单位汇总收银台」下沉到「已付款借出单行」,更符合"只有已付款的台账才能收回"的业务直觉,也避免跨单位合并的复杂度(用户明确不需要跨单位合并收款,单笔即可)。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 公司借款单笔收回 | POST | `/admin/finance/company-loans/{id}/recover` | 新增接口 | 按单笔借款单收回,与合并收款共用 settle 核心 |
> 合并收款三端点(§1.4.1 可选借款单 / §1.4.2 手续费试算 / §1.4.3 合并收款提交)**保留可用**,本次仅新增单笔便捷形态。
## 3. 接口详情
### 3.1 公司借款单笔收回
- **使用场景**:出纳在「支付管理 / 公司借款支付」的已付款台账,对某笔 direction=OUT(借出)且未收齐的借款单做收回登记(收欠收全额或部分金额)。
- **认证**:需管理后台 JWT。
- **幂等性**:否(每次调用产生一笔还款流水 GH- + 一笔资金流水 IN + 一条 RECOVER 操作流水;重复提交会重复入账,前端提交后应禁用按钮防连点)。
- **限流**:无。
**行为**:按本单收回指定金额 → 写还款流水(GH- 取号)+ 资金流水单笔 IN(bizType=COMPANY_LOAN)+ RECOVER 操作流水 + CAS 状态重算(收齐转已核销)+ 联动入账账户结存。与合并收款 §1.4.3 共用 `recoverSettle` settle 核心,守卫完全一致。
## 4. 接口入参
### 4.1 路径参数
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | Long | ✅ | 借款单ID(path) |
### 4.2 请求体字段
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `amount` | BigDecimal | ✅ | 本次收回金额 | >0 且 ≤ 该单剩余余额,否则 599309 |
| `fundAccountId` | Long | ✅ | 入账资金账户ID | 须存在且 ACTIVE(fin_fund_account) |
| `feeRate` | BigDecimal | ❌ | 手续费率(‰) | 默认 0;fee = amount × feeRate / 1000,实收 = amount − fee |
| `voucherUrl` | String | ❌ | 收款凭证影像URL | — |
> ⚠️ **`unitId` / `unitType` 不入参**——后端从借款单反查归属单位,前端无需也不应传(防传错串单)。
## 5. 出参(响应)
复用合并收款 `CompanyLoanRecoverSettleRespVO`:
| 字段 | 类型 | 说明 |
|------|------|------|
| `totalAmount` | BigDecimal | 本次收回金额(=入参 amount) |
| `actualAmount` | BigDecimal | 实收(= totalAmount − fee) |
| `fee` | BigDecimal | 手续费 |
| `repayIds` | Long[] | 还款流水ID列表(**单笔恒单元素**) |
| `settledLoanIds` | Long[] | 本次收齐转已核销的借款单ID(收齐时含本单 id,未收齐为空) |
## 6. 枚举 / 数据字典
本接口入参/出参无枚举字段。相关业务方向(由后端从借款单反查,前端不传):
### 6.1 direction(借款方向,借款单自身字段)
| 值 | 中文 | 说明 |
|----|------|------|
| `OUT` | 借出 | 公司借钱给单位/员工,到期**收回**(本接口只收 OUT 单) |
| `IN` | 借入 | 公司向单位/员工借钱,到期**归还**(走归还链路,非本接口) |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| 599301 | 借款单不存在 | id 查无此单 |
| 599310 | 借款方向不匹配 | 该单非 OUT(借出),借入单不能走收回 |
| 599308 | 借款单已核销 | 已收齐核销的单不可再收 |
| 599302 | 借款单状态不可收 | 非可收回状态(如待付款未放款) |
| 599309 | 收回金额超剩余余额 | amount > 该单剩余余额 |
| 599303 | 手续费率非法 | feeRate < 0 或 ≥ 1000 |
| 599307 | 还款单号取号耗尽 | GH- 单号序列耗尽(极少) |
| 595001 | 资金账户不存在 | fundAccountId 查无此账户 |
| 595006 | 资金账户不可用 | 账户非 ACTIVE |
## 8. 示例
### 8.1 典型成功(足额收齐,无手续费)
**请求**:
```http
POST /admin/finance/company-loans/101234/recover
Authorization: Bearer <token>
Content-Type: application/json
{
"amount": 5000.00,
"fundAccountId": 88
}
```
**响应**(收齐转已核销):
```json
{
"code": 0,
"data": {
"totalAmount": 5000.00,
"actualAmount": 5000.00,
"fee": 0.00,
"repayIds": [900001],
"settledLoanIds": [101234]
},
"msg": ""
}
```
### 8.2 边界(部分收回 + 含手续费)
**请求**:
```json
{
"amount": 2000.00,
"fundAccountId": 88,
"feeRate": 5,
"voucherUrl": "https://oss.example.com/voucher/x.jpg"
}
```
**响应**(部分收回,settledLoanIds 为空;fee=2000×5/1000=10,实收 1990):
```json
{
"code": 0,
"data": {
"totalAmount": 2000.00,
"actualAmount": 1990.00,
"fee": 10.00,
"repayIds": [900002],
"settledLoanIds": []
},
"msg": ""
}
```
### 8.3 业务失败(超额收回)
**请求**:
```json
{ "amount": 99999.00, "fundAccountId": 88 }
```
**响应**:
```json
{ "code": 599309, "msg": "收回金额超剩余余额", "data": null }
```
## 9. 业务边界
- ✅ **适用**:direction=OUT(借出)、未收齐(核销中 / 部分核销)的借款单。
- ❌ **不适用**:借入单(IN,走归还链路)→ 599310;已收齐核销单 → 599308;待付款未放款单 → 599302。
- ⚠️ **特殊**:amount 收齐该单余额时本单转已核销(`settledLoanIds` 含本单 id);未收齐则保持核销中(`settledLoanIds` 为空),可再次调用收回剩余。
## 10. 修改前后对比
新增接口,无修改前版本。与原「按单位合并收款」的关系:
| 维度 | 按单位合并收款(§1.4.3) | 单笔收回(本接口) |
|------|--------------------------|--------------------|
| 入口 | 勾选同单位多笔合并 | 单笔直接收 |
| 入参 | unitId + items[](多单填额) | path id + 单 amount |
| 还款流水 | 多单逐笔 | 单笔一条 |
| 共用 | — | 与本接口共用 settle 核心,守卫一致 |
## 11. 影响评估 / 回滚
- **是否破坏向后兼容**:否(纯新增端点)。
- **前端是否必须同步上线**:否(前端可在「公司借款支付」已付款行接入本接口实现单笔收款登记;不接入不影响既有合并收款)。
- **回滚方式**:revert PR #8670 即可下线本端点,无数据迁移、无缓存清理。
## 12. 注意事项
- **原型/菜单侧**:收回原型入口已并入「支付管理 / 公司借款支付」已付款台账行「收款登记」;原「收款管理 / 公司借款收回」按单位汇总收银台原型入口已下线。前端实现管理后台时,单笔收款登记建议挂在已付款借出单行(只有已付款台账能收回)。
- 提交后请禁用按钮防连点(非幂等,重复提交会重复入账)。
- 合并收款三端点后端保留可用,如未来仍提供按单位合并入口可继续用 §1.4.1-1.4.3。
## 13. 关联 / 联系人
- **Issue**: [#8664](https://git.1814.love/wx/HL/issues/8664)
- **PR**: [#8670](https://git.1814.love/wx/HL/pulls/8670)
- **Merge commit**: [11a61d3739](https://git.1814.love/wx/HL/commit/11a61d3739)
- **后端负责人**: @yst
@@ -0,0 +1,192 @@
---
schema: "hl-changelog/v2"
ticket: "8671"
title: "团期看板「核单」页签 opsStage=REVIEW 筛选口径扩为核单三态"
consumer: "admin"
author: "yst(GIT)"
change_type: "修改接口"
backend_status: "merged"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "04b5cca6bfa46f93711bb6cc9019e3b6e284488e"
target_release: "v2.1"
verified_at: "2026-10-01"
base: "dev-v3"
updated_at: "2026-10-01"
status_note: "团期看板「核单」页签(opsStage=REVIEW)筛选范围扩大:从只含「核单中 REVIEWING」扩为「待核单 PENDING_REVIEW + 核单中 REVIEWING + 已结算 SETTLED」三态。前端继续传 opsStage=REVIEW 即可,无需改入参;但同入参返回的团期集合变大,核单页签会多看到待核单与已结算的团。前端已交付:核单页签带 productId 时列表请求显式传 scope=ALL(§8.2 缺省 ONGOING 会滤掉核单三态),其余桶维持不传 scope 口径,统计条不受影响。(此前 frontmatter 误标 implemented 无 ref,本次实证交付后补齐。)"
---
# 【修改接口·管理后台】团期看板「核单」页签筛选口径扩为核单三态 (#8671)
> **PR**: #8672 | **服务**: hl-order-service-v3(8086) | **更新时间**: 2026-10-01
## 1. 接口背景
管理后台「团期」看板的「核单」页签,业务上应只列出与核单相关的团期(待核单 / 核单中 / 已结算),把招募中、资源准备中、待出发、出行中、已取消等非核单团过滤掉。
此前「核单」页签(`opsStage=REVIEW`)只筛「核单中 REVIEWING」一个状态,导致:
- **待核单**(已返团、尚未开始核单)的团看不到——它被归在「出行」页签下;
- **已结算**的团看不到——它被归在「结算」页签下。
财务 / 运营在核单页签想统览「整个核单阶段」的团时,要么漏团,要么得把范围切到「全部」把无关团全拉进来。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 团期分页看板列表 | GET | `/v3/admin/order/group-batch` | 修改接口 | `opsStage=REVIEW` 筛选口径扩为核单三态 |
> 说明:本接口同时被 `/v3/admin/order/group-batch/board`(看板视图)复用同一筛选口径,行为一致变化。
## 3. 接口详情
### 3.1 团期分页看板列表
- **使用场景**:管理后台团期看板分页查询,前端点各页签(招募/配置/确认/出行/核单/结算/已流团)时传对应 `opsStage` 筛选。
- **认证**:需 JWT(管理后台管理员)。
- **幂等性**:查询接口,天然幂等。
- **限流**:无。
## 4. 接口入参
### 4.1 Query 参数(仅列本次相关)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `opsStage` | String | ❌ | 看板桶筛选。本次仅 `REVIEW` 一档的展开口径变化,其余桶不变 |
| `pageNo` | Integer | ❌ | 页码,默认 1 |
| `pageSize` | Integer | ❌ | 每页条数 |
| `scope` | String | ❌ | 班期范围(ONGOING/FINISHED/ALL)。⚠️ 见 §9 边界:核单三态返团日已过,默认 ONGOING 会滤掉,需传 ALL |
## 5. 出参(响应)
出参字段结构**完全不变**(仍是团期分页 `records[]`,含 `groupBatchId`/`batchNo`/`batchStatus`/`reviewStatus`/`settlementStatus`/`stage`/`stageName` 等)。本次只改「哪些团期被筛进结果集」,不改任何返回字段。
> ⚠️ 核单页签内不同行的 `stage` / `stageName`(节点标签)**可能不统一**:待核单的团节点仍显示「出行」、已结算的团节点仍显示「结算」。这是刻意的——本次只扩筛选范围,不动看板六节点归属模型。前端如对节点标签有统一展示诉求,需另行提需求。
## 6. 枚举 / 数据字典
### 6.1 opsStage(看板桶筛选值)
**所属字段**:`opsStage` | **类型**:`String` | **必填**:❌
| 值 | 中文 | 展开为哪些团期状态 | 本次是否变化 |
|----|------|--------------------|--------------|
| `RECRUIT` | 招募 | RECRUITING | 否 |
| `CONFIGURE` | 配置 | RESOURCE_PREPARING | 否 |
| `CONFIRM` | 确认 | MATERIAL_PREPARING | 否 |
| `TRIP` | 出行 | PENDING_DEPARTURE + TRAVELLING + PENDING_REVIEW | 否 |
| `REVIEW` | 核单 | **PENDING_REVIEW + REVIEWING + SETTLED** | ✅ 变化 |
| `SETTLE` | 结算 | SETTLED | 否 |
| `DISBANDED` | 已流团 | CANCELLED | 否 |
### 6.2 batchStatus(团期九态,出参)
| 值 | 中文 | 说明 |
|----|------|------|
| `RECRUITING` | 招募中 | — |
| `RESOURCE_PREPARING` | 资源准备中 | — |
| `MATERIAL_PREPARING` | 物料准备中 | — |
| `PENDING_DEPARTURE` | 待出发 | — |
| `TRAVELLING` | 出行中 | — |
| `PENDING_REVIEW` | 待核单 | 返团后尚未开始核单 |
| `REVIEWING` | 核单中 | — |
| `SETTLED` | 已结算 | — |
| `CANCELLED` | 已取消 | 流团 |
## 7. 错误码
本接口无新增错误码;`opsStage` 传非法值时忽略该筛选并记 warn(不报错、不返 400)。
## 8. 示例(典型 / 边界)
### 8.1 典型:核单页签查询
**请求**:
```http
GET /v3/admin/order/group-batch?pageNo=1&pageSize=20&opsStage=REVIEW&scope=ALL
Authorization: Bearer {adminToken}
(无请求体)
```
**响应**(返回核单中 + 已结算两类团期;当前库暂无「待核单」团,故为 2 条):
```json
{
"code": 200,
"data": {
"total": 2,
"records": [
{ "groupBatchId": "2105223439872933889", "batchNo": "T26-2325", "batchStatus": "REVIEWING", "batchStatusName": "核单中", "stage": "REVIEW", "stageName": "核单", "reviewStatus": "COMPLETED", "reviewStatusName": "已核单", "settlementStatus": "PENDING", "settlementStatusName": "待结算" },
{ "groupBatchId": "2105223438312652802", "batchNo": "T26-5936", "batchStatus": "SETTLED", "batchStatusName": "已结算", "stage": "SETTLE", "stageName": "结算", "reviewStatus": "COMPLETED", "reviewStatusName": "已核单", "settlementStatus": "COMPLETED", "settlementStatusName": "已结算" }
]
},
"message": "成功",
"success": true
}
```
> 说明:核单页签现在会同时返回「核单中」与「已结算」的团(改前只返回核单中)。若库里有「待核单 PENDING_REVIEW」的团也会一并返回,其 `stageName` 仍显示「出行」(待核单在六节点模型里归出行桶,见 §5)。
### 8.2 边界:默认 scope=ONGOING 会滤掉核单团
**场景说明**:核单三态的团期返团日必然已过,若前端不传 `scope=ALL`,缺省规则会把它们按「未结束」滤掉。
**请求**:
```http
GET /v3/admin/order/group-batch?pageNo=1&pageSize=20&opsStage=REVIEW
Authorization: Bearer {adminToken}
(无请求体,scope 缺省)
```
**响应**:productId 缺省时 scope 默认 ALL(不受影响);productId 有值时 scope 默认 ONGOING,核单团被滤掉返回空。前端点核单页签**务必显式传 `scope=ALL`**。
## 9. 业务边界
- ✅ **适用**:核单页签统览核单阶段全部团期(待核单 / 核单中 / 已结算)。
- ⚠️ **scope 联动**:核单三态返团日已过,前端点核单页签需把 `scope` 切到 `ALL`,否则默认范围会把它们过滤掉(此约束改前已存在,本次不变)。
- ⚠️ **节点标签不统一**:核单页签内,待核单团节点显示「出行」、已结算团节点显示「结算」(见 §5)。
- ❌ **不变**:`SETTLE` 结算页签仍只筛 `SETTLED`;`TRIP` 出行页签仍含 `PENDING_REVIEW`(待核单团会同时出现在出行页签与核单页签)。
## 10. 修改前后对比
### 10.1 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| `opsStage=REVIEW` 筛出的团期状态 | 仅 REVIEWING(核单中) | PENDING_REVIEW + REVIEWING + SETTLED(核单三态) |
| 核单页签能否看到「待核单」团 | ❌ 看不到(在出行页签) | ✅ 能看到 |
| 核单页签能否看到「已结算」团 | ❌ 看不到(在结算页签) | ✅ 能看到 |
| 入参字段 / 出参字段 | — | 完全不变 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:否。入参出参字段不变;仅 `opsStage=REVIEW` 返回的数据集扩大(多返回待核单 + 已结算团期)。
- **前端是否必须同步上线**:否。前端继续传 `opsStage=REVIEW` 即可;但需留意核单页签行数会变多、节点标签不统一。
- **影响已有数据**:无,纯查询筛选口径变化,无数据迁移。
### 11.2 回滚方案
- **回滚方式**:revert PR #8672 即可恢复 `opsStage=REVIEW` 单态口径。
- **回滚后清理**:无(无脏数据 / 缓存)。
- **回滚耗时**:重新打包部署 hl-order-service-v3,约 5 分钟。
## 12. 注意事项
- 上线需重启 / 重新部署 **hl-order-service-v3**(8086)。
- 前端无需改入参;如核单页签需统一节点标签,属另一个展示层需求,单独提。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#8671](https://git.1814.love/wx/HL/issues/8671)
- **PR**: [#8672](https://git.1814.love/wx/HL/pulls/8672)
- **Merge commit**: [f01d7565b3](https://git.1814.love/wx/HL/commit/f01d7565b3b750841eb32af98a448f5b1a7b5b7f)
### 13.2 联系人
- **后端负责人**: @yst
- **前端对接(管理后台)**: @hl-admin
@@ -0,0 +1,147 @@
---
schema: "hl-changelog/v2"
ticket: "8673"
title: "应付款:列表默认只看欠款 + 付款明细补全量支付状态(#8673)"
consumer: "admin"
author: "yst(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "1be140db4ac9fde579b06e934078c0675db1b6b8"
target_release: "v2.1"
verified_at: "2026-10-01"
status_note: "应付款两处调整。①【破坏性】按团号/按供应商列表默认空 status 从「全部」改为「只看欠付 OWED」:已付清/金额归零的团和供应商默认不再返回,需显式传 status=PAID 或 ALL 回看;status 过滤从内存过滤下沉 SQL,修复了分页 total 与返回行数不一致的 bug。②付款建议明细出参新增 payableAmount/paidAmount/appliedAmount/payStatus 四件套(additive),入参新增 includePaid(默认 false);includePaid=true 时已付清行也返回(payStatus=PAID 置灰),点付款可看到「哪些已付、哪些未付」全景。payStatus 判据=无可申请余额即 PAID(覆盖真已付清 + applied 全额占用)。前端已交付:统计列表筛选项对齐 OWED/PAID/ALL、默认显式钉 OWED 只看欠款(回看切 PAID/ALL);两个付款面板与应付款详情均 includePaid=true 全景,PAID 行置灰禁勾显「已付清」、PARTIAL 显「部分已付」。"
updated_at: "2026-10-01"
base: "dev-v3"
---
# finance:应付款列表默认只看欠款 + 付款明细补全量支付状态(管理后台)
> ⚠️ **破坏性变更**:`GET /admin/finance/payments/stats/by-team` 和 `/by-supplier` 不传 `status` 时,从「返回全部(含已付清)」改为「只返回有欠付(OWED)的行」。已付清的团/供应商默认从列表消失,需显式传 `status=PAID` 或 `status=ALL` 回看。
> ✅ **additive**:付款建议明细出参新增 4 字段、入参新增 `includePaid`,旧前端不传不受影响。
## 1. 接口背景
财务「应付款」按团号/按供应商两个列表,原把头表所有行(含已付清、金额归零)都返回,干扰财务看「还欠谁的」;且 status 过滤是后端内存过滤,只滤当前页导致分页 total 不准。点付款时的明细只给「剩余可申请余额」,看不到每个资源「该付多少、付了多少、还差多少」,已付清的资源行直接被滤掉。
本次:列表默认只看欠款 + 修 total 不准;付款明细补全量支付状态。
## 2. 变更清单
| # | 接口 | 变更 | 类型 |
|---|---|---|---|
| 1 | GET /payments/stats/by-team | 默认空 status=只看 OWED;status 新增 ALL;过滤下沉修 total | ⚠️ 行为变更 |
| 2 | GET /payments/stats/by-supplier | 同上 | ⚠️ 行为变更 |
| 3 | GET /payments/suggestions | 出参加 4 字段;入参加 includePaid | ✅ additive |
| 4 | GET /payments/suggestions/by-supplier | 同上 | ✅ additive |
## 3. 接口详情
统一前缀 `GET /admin/finance/payments/**`(业务调用**不带**服务前缀,网关按 `/admin/finance/**` 路由)。
## 4. 入参
### 4.1 列表(by-team / by-supplier)
| 参数 | 说明 |
|---|---|
| keyword | 团号/产品名/客人名(by-team)或供应商名(by-supplier)模糊 |
| status | ⚠️ `OWED` 有欠付 / `PAID` 已付款 / `ALL` 全部;**空=默认只看 OWED(新)** |
### 4.2 付款建议(suggestions / by-supplier)
| 参数 | 说明 |
|---|---|
| orderId / supplierId | 必填 |
| includePaid | 新增,默认 `false`;`true` 时返回含已付清行的全量明细 |
## 5. 出参
### 5.1 列表行(不变)
by-team:`teamNo, productName, customerName, orderNos, departDate, returnDate, payableAmount, appliedAmount, paidAmount, owedAmount, supplierCount, status`
by-supplier:`supplierId, supplierName, category, payableAmount, appliedAmount, paidAmount, owedAmount, teamCount, status`
### 5.2 付款建议行(PaymentSuggestionRowVO 新增 4 字段)
原字段 + 新增:
| 字段 | 说明 |
|---|---|
| payableAmount | 该行该付总额 |
| paidAmount | 已付金额 |
| appliedAmount | 已申请占用金额(在途付款单) |
| payStatus | `UNPAID` 未付 / `PARTIAL` 部分已付 / `PAID` 已付清(无可申请余额) |
## 6. 枚举/数据字典
### status(列表筛选 + 行出参)
| 值 | 含义 |
|---|---|
| OWED | 有欠付(欠付 > 0) |
| PAID | 已付款(欠付 <= 0,含金额归零) |
| ALL | 全部(仅筛选用,回看已付清) |
### payStatus(付款建议行出参,新增)
| 值 | 含义 |
|---|---|
| UNPAID | 未付(已付=0,仍可申请) |
| PARTIAL | 部分已付(已付>0 且仍有可申请余额) |
| PAID | 无可申请余额(含真已付清 + applied 全额占用,置灰不可再勾选) |
## 7. 错误码
无新增。
## 8. 示例
### 8.1 典型:默认只看欠款
```http
GET /admin/finance/payments/stats/by-team
→ 只返回有欠付的团,已付清团不出现;total 与返回行数一致
```
### 8.2 回看已付清
```http
GET /admin/finance/payments/stats/by-team?status=ALL
→ 返回全部(含已付清团,status=PAID)
```
### 8.3 点付款看全量(含已付清行置灰)
```http
GET /admin/finance/payments/suggestions?orderId=66001&includePaid=true
→ rows 含已付清行:
{"resourceName":"呼和塔拉草原","payableAmount":240.00,"paidAmount":240.00,"appliedAmount":0,"payStatus":"PAID",...}
{"resourceName":"阿尔山门票","payableAmount":500.00,"paidAmount":100.00,"appliedAmount":0,"payStatus":"PARTIAL",...}
{"resourceName":"白桦林","payableAmount":300.00,"paidAmount":0,"appliedAmount":0,"payStatus":"UNPAID",...}
```
## 9. 业务边界
- 列表默认 OWED 视图下,金额归零(行全取消)的团因 owed<=0 归 PAID,自然隐藏。
- includePaid=true 返回的已付清行(payStatus=PAID)**不可再发起付款申请**,前端置灰禁勾选。
- 列表 status 过滤已下沉 SQL,分页 total 与返回行严格一致。
## 10. 修改前后对比
| 项 | 修改前 | 修改后 |
|---|---|---|
| 列表空 status | 返回全部(含已付清/归零) | 只返回有欠付 OWED |
| status 过滤 | 内存过滤,total 不准 | SQL 下沉,total 准确 |
| status 取值 | OWED/PAID | OWED/PAID/ALL |
| 付款明细行金额 | 只有余额 amount | 加 payable/paid/applied/payStatus |
| 已付清资源行 | 被滤掉看不到 | includePaid=true 可见(置灰) |
## 11. 影响评估/回滚
- **列表默认变更**:老前端不传 status 时已付清行消失,需前端确认是否接受/补 status 控件。
- **付款明细**:additive,旧前端不传 includePaid、不读新字段则零影响。
- 回滚:恢复 selectPage 旧签名 + Service 默认口径即可。
## 12. 注意事项
- ⚠️ 列表默认只看欠款是**破坏性变更**,前端若依赖「默认看到已付清历史」需改为显式传 status=ALL。
- payStatus=PAID 的语义是「无可申请余额」(含 applied 全额占用),非严格「已付清」,前端置灰即可。
- 供应商维度中 supplierId= null 的降级行计入团头但不计入任何供应商,两视图金额可能对不上(历史口径,本次未改)。
## 13. 关联/联系人
- Issue:https://git.1814.love/wx/HL/issues/8673
- PR:https://git.1814.love/wx/HL/pulls/8674
- merge commit:a8cf8e13109ea713c28e8be527d0a27d0f2e63fb
- 后端负责人:腰苏图
@@ -0,0 +1,293 @@
---
schema: "hl-changelog/v2"
ticket: "8677"
title: "团期预支可支取上限改为「整团已收(扣已退)− 在途」——GB-ADM-040 advanceAvailable 与 GB-ADM-042 发起上限同步改口径"
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: "团期预支的可支取上限原为「团期尾款池(Σ 各户 max(0, 应收 − 已付))− 在途」,全额收款的团可支取恒为 0(TEST「11月1日额济纳胡杨林深秋4日游」应收 = 已收 26340.00 → 0.00),部分收款的团反倒能按还没收的钱预支。jw 2026-10-01 定案改为「团期已收池 − 在途」:已收池 = Σ 本团活跃子订单 max(0, 已付 − 已退),CANCELLED 户记 0;硬上限,超额 585004、无放宽通道;普通订单上限不改;部分收款团上限收紧属预期;存量不回溯(在途已超新上限时可支取显示 0.00、只拦新申请)。GB-ADM-040 的 advanceAvailable 与 GB-ADM-042 的发起校验共用同一方法,同时生效。路径、入参、出参字段名与类型零变化,错误码不变;变的是取值口径,属「语义取值」变化,按 jw 2026-09-18 口径推送。已合并 dev-v3(PR #8686,merge commit f4f545148)并部署测试服。hl-ui v2.1 弹窗「剩余可支取」与财务 Tab「可支取余额」都直接显示后端 advanceAvailable,前端零改动,frontend_status 记 not_required。"
updated_at: "2026-10-01"
base: "dev-v3"
---
# 团期预支可支取上限改为整团已收(管理后台)
> **服务**: hl-order-service-v3(端口 8086/8186)
## 一、接口背景
团期「发起团期预支」弹窗里的「剩余可支取」、财务 Tab 的「可支取余额」,以及发起时的超额校验,原先都按**整团还没收的尾款**算上限。
全额收款的团尾款为 0,于是一分钱都预支不了;只收了一部分的团,反倒能按还没收的钱预支。
本次改为只预支已经收到手的钱:上限 = 整团已收(扣已退)− 在途预支。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | GB-ADM-040 团期财务总览 | GET | `/v3/admin/order/group-batch/{groupBatchId}/finance` | 修改接口 | 出参 `advanceAvailable` 取值口径改为「团期已收池 − 在途」;字段名、类型与其余字段零变化 |
| 2 | GB-ADM-042 发起团期预支 | POST | `/v3/admin/order/group-batch/{groupBatchId}/advance` | 修改接口 | 金额上限改为同一口径;入参、出参、错误码零变化 |
## 三、接口详情
### 1. GB-ADM-040 团期财务总览 `GET /v3/admin/order/group-batch/{groupBatchId}/finance`
**VO**: `GroupBatchFinanceRespVO`
#### 使用场景
团期详情「财务」Tab 切换即加载:四张金额卡、可支取余额、待审批预支、逐户付款。「发起团期预支」弹窗的「剩余可支取」也直接取本接口的 `advanceAvailable`。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| groupBatchId | path | Long | 是 | 正整数,团期聚合主键 | 团期 ID,不存在返 589501 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| advanceAvailable | BigDecimal(字符串序列化) | **本次改口径**。可支取余额 = max(0, 团期已收池 − 在途)。团期已收池 = Σ 本团活跃子订单 max(0, 已付 − 已退),CANCELLED 户记 0;在途 = 本团期待审批 + 已通过 + 已付款的预支(团期级 ∪ 子订单级,口径不变)。即发起预支的硬上限 |
| receivedAmount | BigDecimal | 整团已收(既有字段,本次未改)。毛已付,**不扣退款**;有退款时 `advanceAvailable` 的基数会小于它 |
| receivableAmount | BigDecimal | 整团应收(既有字段,本次未改) |
| unpaidAmount | BigDecimal | 整团待收(既有字段,本次未改;**不再**是预支上限的基数) |
| advanceApproved | BigDecimal | 已预支(既有字段,本次未改) |
| advancePending | BigDecimal | 待审批预支(既有字段,本次未改) |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2104839654727618562/finance HTTP/1.1
Host: api.test.1814.love
Authorization: Bearer <admin token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"receivableAmount": "26340.00",
"receivedAmount": "26340.00",
"unpaidAmount": "0.00",
"advanceApproved": "0.00",
"advancePending": "0.00",
"advanceAvailable": "26340.00"
}
}
```
#### 空数据 / 降级响应
团里没有活跃子订单、或已收全部被在途占满时,`advanceAvailable` 返回 `"0.00"`,不返回 null、不返回负数。
```json
{
"code": 200,
"message": "成功",
"data": {
"receivableAmount": "0.00",
"receivedAmount": "0.00",
"unpaidAmount": "0.00",
"advanceApproved": "0.00",
"advancePending": "0.00",
"advanceAvailable": "0.00"
}
}
```
#### 错误响应
```json
{
"code": 589501,
"message": "团期不存在",
"data": null
}
```
| 错误码 | 触发条件 |
|---|---|
| 589501 | groupBatchId 不存在或已软删 |
#### 业务边界
- 展示值与 GB-ADM-042 的发起校验出自同一个方法,弹窗显示多少就能发起多少。
- 取户与在途同源:活跃子订单集按 `group_batch_id` 一跳圈定,已取消户不计入已收。
- 部分退款的户按「已付 − 已退」计入;单户已退不少于已付时按 0 计。
- 存量不回溯:在途已超过新上限的团(存量预支或事后退款),这里显示 `"0.00"`,已有预支记录不变。
### 2. GB-ADM-042 发起团期预支 `POST /v3/admin/order/group-batch/{groupBatchId}/advance`
**VO**: `OrderAdvanceRespVO`
#### 使用场景
团期管理员为本团主报账人申请团期级预支,创建即待审批(SUBMITTED),财务在审批中心审批。本次只改金额上限的口径。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| groupBatchId | path | Long | 是 | 正整数 | 团期 ID |
| payeeStaffId | body | Long | 是 | 须为本团主报账人(PRIMARY) | 借款对象,否则 589557 |
| advanceType | body | String | 是 | 数据字典 `advance_type` 内的值 | 借款类型,否则 585006 |
| amount | body | BigDecimal | 是 | ≥ 0.01,且 ≤ 当前 `advanceAvailable` | 预支金额;**上限口径本次改为团期已收池 − 在途**,超出 585004 |
| purpose | body | String | 否 | ≤ 255 字 | 用途说明 |
| voucherUrl | body | String | 否 | ≤ 512 字符 | 凭证文件 URL |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Long | 预支 ID(既有,本次未改) |
| status | String | 恒为 `SUBMITTED`(既有,本次未改) |
| amount | BigDecimal | 预支金额(既有,本次未改) |
| payeeStaffId / payeeName | Long / String | 领款人(既有,本次未改) |
#### 请求示例
```json
{
"payeeStaffId": 1009,
"advanceType": "ACCOMMODATION_DEPOSIT",
"amount": "24760.00",
"purpose": "额济纳段酒店押金先行垫付"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "2105531450843668481",
"orderId": null,
"payeeStaffId": "1007",
"payeeName": "白云飞",
"payeeRole": "LEADER",
"advanceType": "ACCOMMODATION_DEPOSIT",
"amount": 24760.0,
"status": "SUBMITTED",
"statusText": "待审批",
"submittedAt": "2026-10-01 13:33:11"
}
}
```
#### 空数据 / 降级响应
本接口为写接口,无空数据形态;校验不通过时不写库、不占额度,返回下方错误。
```json
{
"code": 585004,
"message": "预支金额超过可用余额上限",
"data": null
}
```
#### 错误响应
```json
{
"code": 585004,
"message": "预支金额超过可用余额上限",
"data": null
}
```
| 错误码 | 触发条件 |
|---|---|
| 585004 | `amount` 大于当前可支取余额(团期已收池 − 在途);**本次口径变化**,错误码与文案不变 |
| 585003 | `amount` ≤ 0 |
| 589541 | 团期状态不允许发起预支(进入核单后关闭) |
| 589557 | 借款对象不是本团主报账人 |
#### 业务边界
- 上限在 `@Lock4j` 按 groupBatchId 串行的段内计算,读-算-插之间无并发窗口。
- 硬上限:超额一律 585004,没有放宽通道;审批时不再复核额度(维持现状)。
- 普通订单的预支上限不随本次改动,仍是「本单待收尾款 − 本单在途」。
- 部分收款的团上限随之收紧(例:应收 10720、已收 1600 的团,可支取由 9120.00 变为 1600.00)。
## 四、契约约束与正确调用方式
- 弹窗「剩余可支取」与财务 Tab「可支取余额」继续直接读 `advanceAvailable`,**不要**用 `receivedAmount − advanceApproved − advancePending` 本地推算:`receivedAmount` 不扣退款,且在途还含已付款的预支。
- 发起前以最新一次 GB-ADM-040 的 `advanceAvailable` 为准;并发发起时以服务端 585004 为准。
## 五、数据库行为
零数据库变更:不新增表、列或索引。GB-ADM-040 只读;GB-ADM-042 写入行为不变(校验通过后在 `order_advance` 插入一行 `scope=GROUP_BATCH`、`status=SUBMITTED`,并写团期时间线)。
上限的取数改为只读聚合 `order_main.paid_amount` / `refunded_amount`(按本团活跃子订单一次批量取),不再读应付尾款。
## 六、边界行为
- 全额收款、无在途:可支取 = 整团已收净额。
- 已收全部被在途占满,或事后退款使已收低于在途:可支取 `"0.00"`,新申请 585004。
- 团里一户都不剩:已收为 0,可支取 `"0.00"`。
## 六.6、修改前后对比
| 项 | 修改前 | 修改后 |
|---|---|---|
| 上限基数 | 团期尾款池 = Σ 各户 max(0, 应收 − 已付) | 团期已收池 = Σ 活跃户 max(0, 已付 − 已退),取消户记 0 |
| 全额收款的团 | 可支取恒为 `0.00` | 可支取 = 整团已收净额 |
| 部分收款的团 | 可支取 = 还没收的尾款 − 在途 | 可支取 = 已收净额 − 在途(收紧) |
| 取户口径 | 按 `product_batch_id` 圈 | 按 `group_batch_id` 一跳(与在途同源) |
| 路径 / 入参 / 出参 / 错误码 | — | 逐字未变 |
## 六.7、影响评估
- **兼容性**:字段名、类型、错误码不变;数值口径变化,前端零改动。
- **性能**:上限计算由一次按排期圈单改为一次按子订单 ID 批量取单,单团期无 N+1。
- **回滚**:撤销 PR #8686 的合并提交后重新部署 order-v3,无数据与配置残留。
## 七、不影响范围
- 普通订单(非团期)的预支上限与订单级预支接口。
- 已预支 / 待审批两个数、预支记录列表(GB-ADM-043)、核单扣回口径。
- 网关:路径未变、无新增路由与权限码。
- 小程序端:本接口仅管理后台使用。
## 八、测试环境已验证
2026-10-01 13:14–14:59 测试服(`api.test.1814.love`),部署 `dev-v3 @ f4f545148`(order-v3 双实例 8086/8186)。
每个状态节点都用 SQL 按同一公式独立手算并与 `advanceAvailable` 对照,**29 次对照全部一致**。
| 场景 | 读数 / 结果 |
|---|---|
| 部署身份:全额收款团「11月1日额济纳胡杨林深秋4日游」(应收 = 已收 26340.00) | 经网关 8 次 + 直连两实例全是 `"26340.00"`(旧口径为 `"0.00"`) |
| 部分收款团「滇西北冬日三日团·一月廿二期」(应收 10720 / 已收 1600) | `"1600.00"`(旧口径为 9120.00) |
| 自建全额收款团(已收 24760.00) | 可支取 `"24760.00"`;发起 24760.00 → 200 SUBMITTED,可支取变 `"0.00"`;再发起 0.01 → 585004 |
| 自建部分收款团(应收 22080 / 已收 15720) | `"15720.00"`;待审 1200 后 `"14520.00"`;14520.01 → 585004,14520.00 → 200 |
| 部分退款 1840(真实退款链路) | 21880 → `"20040.00"`;再取消一户(已付 7360)→ `"12680.00"` |
| 在途 | 待审 / 已批 / 已付都扣;驳回、撤回不扣 |
| 存量不回溯 | 在途占满后取消一户,可支取仍 `"0.00"`,新申请 585004,已有预支记录逐字段不变 |
| 普通订单 | 上限仍是「本单待收 − 本单在途」:两档已付下超 0.01 都报 585004、等额都成功,与新口径可区分 |
- 验收造数已全部回收,回读零残留。
- 单元测试:团期 / 预支整包 + 全部 ArchTest 258 类 4131 例 0 失败;合并提交上复跑定向 32 类 250 例 0 失败。
## 十、相关文档
- 工单 #8677、PR #8686(merge commit `f4f545148`)
- 团期统一池原设计:#7154(docs/group 接口文档 §0B.4,本次同步改为已收池)
- 团单子订单禁走订单级入口、在途含 PAID:#8384
## 关联 / 联系人
- 后端:jw
- 前端:不涉及(`frontend_status: not_required`)
@@ -0,0 +1,223 @@
---
schema: "hl-changelog/v2"
ticket: "8679"
title: "收款账户关联服务人员档案:新增选服务人员候选接口 + 个人类收款方 payeeRefId 必填并挂档案校验(#8679)"
consumer: "admin"
author: "yst(GIT)"
change_type: "修改接口"
backend_status: "merged"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "85267fc3e3837d5ebaf619dada70fb4564a0b488"
target_release: "v2.1"
verified_at: "2026-10-01"
status_note: "财务「资金账户→收款账户」个人类收款方(司机/导游/摄影/领队)从手填人员 ID/按名认人,改为从人员档案下拉选具体人。新增「选服务人员」聚合候选接口;create 个人类 payeeRefId 由可空变必填、payeeName 由手填变档案真名覆盖(后端强制),并新增档案存在性+在册校验(拦下架人员/黑名单·休假司机)。司机接 fleet 车队档案、导游/摄影/领队接 resource 服务人员档案。组织类(车队/导游公司/供应商)不变仍手填。前端已交付:表单名称位三态——编辑个人类只读禁改(595202)/新建个人类「选择服务人员」下拉远程选人(payeeRefId 必填前置 595203)/组织类维持手填;staff-candidates 按 payeeType 拉候选,label 拼姓名·手机号·staffType(字典 staff_type 兜底原值)·导游等级,选中回填档案真名,新建态切类型清空已选与候选防错配;payload 编辑个人类剔 payeeName;595202/595203/595204 一律拦截器透 message 不建映射。新建表单 spec 8 例全绿。"
updated_at: "2026-10-01"
base: "dev-v3"
---
# finance:收款账户关联服务人员档案(管理后台)
## 1. 接口背景
财务「资金账户 → 收款账户」登记收款方时,**个人类收款方**(司机 DRIVER / 导游 GUIDE / 摄影 PHOTOGRAPHER / 领队 LEADER)原来是**手填人员 ID + 手填姓名**,无档案校验——可填错人、填黑名单司机、填已下架人员,钱打给谁的依据不严谨。
本次让个人类收款方**挂到真实人员档案**:前端表单改为「选择服务人员」下拉(从档案源选人),后端 create 强制校验档案真实存在且在册。司机档案在 **fleet 车队域**,导游/摄影/领队在 **resource 服务人员域**。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 选服务人员候选 | GET | `/admin/finance/payee-accounts/staff-candidates` | **新增接口** | 按 payeeType 分流返回人员候选下拉数据 |
| 2 | 登记收款方账户 | POST | `/admin/finance/payee-accounts` | **修改接口** | 个人类 `payeeRefId` 必填 + `payeeName` 被档案真名覆盖 + 档案校验 |
| 3 | 编辑收款方账户 | PUT | `/admin/finance/payee-accounts/{payeeId}` | **修改接口** | 个人类禁止手改 `payeeName` |
## 3. 接口详情
### 3.1 选服务人员候选(新增)
- **使用场景**:收款账户表单选了「收款方类型」为个人类后,「选择服务人员」下拉/弹窗的数据源
- **认证**:JWT(管理后台)
- **入参(query)**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| payeeType | string | 是 | `DRIVER`/`GUIDE`/`PHOTOGRAPHER`/`LEADER`(个人类;传组织类或非法值报 595202) |
| keyword | string | 否 | 姓名模糊;输入 11 位手机号按手机精确匹配。≤50 字,超限报 595202 |
| page | int | 否 | 缺省 1 |
| pageSize | int | 否 | 缺省 20,最大 100 |
- **行为**:按 payeeType 分流到对应档案源——DRIVER→fleet 司机、GUIDE→resource 导游(含助理导游)、PHOTOGRAPHER→摄影、LEADER→领队。档案服务故障时**降级返回空集合**(不打塌表单,前端下拉显示"暂无可选人员"即可)。
- **出参**:`data.records[]` + `data.total`
### 3.2 登记收款方账户(修改)
- 个人类:`payeeRefId` **必填**(选中的服务人员 ID),后端校验该 ID 在对应档案源**真实存在且在册**(staff 上架 status=1;driver 在册 season=active 且非休假/待激活),并以**档案真名覆盖 payeeName**。
- 组织类(FLEET/GUIDE_CO/SUPPLIER):不变,`payeeRefId` 可空、`payeeName` 手填。
### 3.3 编辑收款方账户(修改)
- 个人类:禁止手改 `payeeName`(名称以人员档案为准),传了报 595202。账户要素(qr/bankAccount/bankName)仍可改。
- 组织类:不变。
## 4. 接口入参
### 4.1 登记请求体关键字段(变化部分)
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| payeeType | string | 是 | 收款方类型 | 个人类见下 |
| payeeRefId | string(Long) | **个人类必填** | 服务人员 ID(staff-candidates 返回的 refId);组织类可空 | 个人类必填否则 595203;档案不存在/已下架 595204 |
| payeeName | string | 否 | 收款方姓名 | **个人类会被档案真名强制覆盖**(手填无效);组织类手填 |
## 5. 出参(响应)
### 5.1 staff-candidates 出参 records[]
| 字段 | 类型 | 说明 |
|------|------|------|
| refId | string(Long) | 人员 ID(司机=driverId,其余=staffId)——登记时填入 payeeRefId |
| name | string | 姓名(档案真名) |
| phone | string | 手机号(**明文**,出纳选人看全号区分同名;落库后的列表/详情仍脱敏) |
| payeeType | string | 回显收款方类型 |
| staffType | string | 人员类型(仅 staff 类有值:`GUIDE`/`GUIDE_ASSISTANT`/`PHOTOGRAPHER`/`LEADER`;DRIVER 为 null)。GUIDE 候选合并导游+助理导游,前端据此字段区分 |
| guideLevel | string | 导游等级(仅 staff 导游类有值:初级/中级/高级/特级;其余为 null) |
## 6. 枚举 / 数据字典
### 6.1 staffType(人员类型,字典 staff_type)
| 值 | 中文 |
|----|------|
| `GUIDE` | 导游 |
| `GUIDE_ASSISTANT` | 助理导游 |
| `PHOTOGRAPHER` | 摄影师 |
| `LEADER` | 领队 |
> payeeType=GUIDE 的候选同时含 GUIDE 和 GUIDE_ASSISTANT 两类,前端用 `staffType` 区分展示;payeeType=PHOTOGRAPHER/LEADER 只含对应一类。
### 6.2 payeeType 个人类 ↔ 档案源
| payeeType | 档案源 | refId 含义 |
|-----------|--------|-----------|
| DRIVER 司机 | fleet 车队 | driverId |
| GUIDE 导游 | resource 服务人员 | staffId |
| PHOTOGRAPHER 摄影 | resource 服务人员 | staffId |
| LEADER 领队 | resource 服务人员 | staffId |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| 595202 | 收款方账户字段组合不合法 | staff-candidates 传组织类/非法 payeeType、keyword 超 50;编辑个人类手改 payeeName |
| 595203 | 个人类收款方须选择服务人员 | create 个人类未传 payeeRefId |
| 595204 | 服务人员不存在或已下架 | create 个人类 payeeRefId 在档案源不存在、staff 已下架、driver 黑名单/归档/休假/待激活;或档案服务暂不可用(文案为"档案服务暂不可用,请稍后重试") |
## 8. 示例(3 组)
### 8.1 典型成功(选司机 → 登记司机收款账户)
**① 候选** `GET /admin/finance/payee-accounts/staff-candidates?payeeType=DRIVER&keyword=张`:
```json
{
"code": 200, "success": true,
"data": {
"total": 1,
"records": [
{ "refId": "1823456789012345678", "name": "张三", "phone": "13800138000",
"payeeType": "DRIVER", "staffType": null, "guideLevel": null }
]
}
}
```
**② 登记** `POST /admin/finance/payee-accounts`:
```json
{
"payeeType": "DRIVER",
"payeeRefId": "1823456789012345678",
"accountType": "WECHAT_QR",
"qrUrl": "https://oss.example.com/qr/zhangsan.png",
"isDefault": 1
}
```
**响应**(payeeName 由后端用档案真名"张三"覆盖,无需前端传):
```json
{ "code": 200, "success": true, "data": { "id": "2105..." } }
```
### 8.2 边界(导游候选含助理导游,staffType 区分)
`GET /admin/finance/payee-accounts/staff-candidates?payeeType=GUIDE`:
```json
{
"code": 200, "success": true,
"data": {
"total": 2,
"records": [
{ "refId": "1811...", "name": "李四", "phone": "13911112222", "payeeType": "GUIDE", "staffType": "GUIDE", "guideLevel": "高级" },
{ "refId": "1822...", "name": "王五", "phone": "13933334444", "payeeType": "GUIDE", "staffType": "GUIDE_ASSISTANT", "guideLevel": "初级" }
]
}
}
```
### 8.3 业务失败(个人类未选服务人员)
`POST /admin/finance/payee-accounts`:
```json
{ "payeeType": "GUIDE", "accountType": "WECHAT_QR", "qrUrl": "https://..." }
```
**响应**:
```json
{ "code": 595203, "message": "个人类收款方须选择服务人员", "success": false }
```
选了已下架人员:
```json
{ "code": 595204, "message": "服务人员不存在或已下架", "success": false }
```
## 9. 业务边界
- ✅ 个人类收款方:必须先调 staff-candidates 选人,把返回的 `refId` 作为 `payeeRefId` 提交;`payeeName` 不用传(后端用档案真名覆盖)。
- ✅ 组织类(车队/导游公司/供应商):维持手填 `payeeName`,`payeeRefId` 可空,不调候选接口。
- ❌ 不要再手填个人类的 payeeRefId/payeeName——后端强制档案校验+真名覆盖,手填无效或被 595203/595204 拦。
- ⚠️ 候选接口降级:档案服务故障时返回空 records(非报错),前端下拉显示"暂无可选人员,请稍后重试"即可,不要当成"无此人员"。
## 10. 修改前后对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 个人类选人方式 | 手填人员 ID + 手填姓名 | staff-candidates 下拉选人,payeeRefId=选中人员 ID |
| 个人类 payeeRefId | 可空 | **必填**(595203) |
| 个人类 payeeName | 手填生效 | **被档案真名覆盖**(手填无效) |
| 档案校验 | 无 | 存在性+在册校验(595204),拦下架/黑名单/休假 |
| 编辑个人类 payeeName | 可手改 | 禁止(595202) |
## 11. 影响评估 / 回滚
- **是否破坏向后兼容**:**是**。个人类 create 原来可不传 payeeRefId,现在必填;原来手填 payeeName 生效,现在被档案覆盖。前端个人类表单必须改为「先选人」流程。
- **前端是否必须同步上线**:是。个人类收款账户表单需加「选择服务人员」下拉(数据源 staff-candidates),并移除个人类的手填姓名/手填 ID 输入。
- **影响已有数据**:测试库 fin_payee_account 个人类存量少,存量数据不受影响(仅新增/编辑走新校验)。
- **回滚方式**:revert PR #8692(finance)+ PR #8688(fleet)。
## 12. 注意事项
- **选人下拉数据分端**:司机候选来自 fleet(含 season/driverStatus 过滤),导游/摄影/领队来自 resource staff。前端只调一个 staff-candidates 接口,后端按 payeeType 分流,无需关心来源。
- **staffType/guideLevel 仅 staff 类有**:DRIVER 候选这两字段为 null,前端展示时判空。
- **手机号明文仅选人下拉里**:落库后的收款账户列表/详情 phone 仍按 PII 口径脱敏,不要在别处用候选接口的明文 phone 当展示数据源。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#8679](https://git.1814.love/wx/HL/issues/8679)
- **PR(finance)**: [#8692](https://git.1814.love/wx/HL/pulls/8692) · merge [7cb5aa9e6d](https://git.1814.love/wx/HL/commit/7cb5aa9e6d717efe34ce099b9e11eabdc9811dbf)
- **PR(fleet 司机候选数据源)**: [#8688](https://git.1814.love/wx/HL/pulls/8688) · merge [b4edefd08e](https://git.1814.love/wx/HL/commit/b4edefd08e5a439db6e7a6e2d245a5d0fd1cc067)
### 13.2 联系人
- **后端负责人**: @yst
- **前端对接(管理后台)**: 待认领
@@ -0,0 +1,159 @@
---
schema: "hl-changelog/v2"
ticket: "8680"
title: "出纳已付台账 ADVANCE 页签补预支详情接口(#8680)"
consumer: "admin"
author: "yst(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude)"
frontend_ref: "75b56e2f5d27ae0eebd82af183973d5e84d6ce6a"
target_release: "v2.1"
verified_at: "2026-10-01"
status_note: "出纳已付台账 8 个页签的明细抽屉,7 个域详情接口此前已就绪,唯独 ADVANCE 订单预支是缺口:已付台账 ADVANCE 行 bizId 指向 fin_advance.advance_id,但无单笔详情接口,前端抽屉只能拿流水回单兜底(看不到订单/团号/报账人/用途)。本次新增 GET /admin/finance/advances/{id} 司导预支执行单详情,补齐最后一环。出参含订单/团号/收款人/金额/用途/付讫三列;teamNo 由后端按 orderId 反查 order_main 补齐,前端可据此跳订单维度预支列表。additive 纯新增,旧前端零影响。【前端 2026-10-01 交付】changelog 前提「7 域抽屉已就绪」实证不成立,用户拍板 8 域统一抽屉立项;批次 1 骨架+ADVANCE 已落地(新建 api/finance/advance.js+LedgerDetailDrawer 统一骨架,CashierQueuePage ADVANCE 线「明细」列),其余 7 域随后续批次。checkpoint 全绿,22 例 spec 全绿。【批次 2 同日收官】其余 7 域(EXPENSE/NONBIZ/PAYMENT/PREPAY/STAFF_LOAN/REIMBURSE/COMPANY_LOAN)已全接通:抽屉改配置驱动 8 域分派,字段布局向原型各域详情弹窗对齐,操作列全 8 线统一(专项入口+明细);页面与操作列改动随 f5644271、抽屉组件随 75b56e2f 两提交入库,32 例 spec 全绿。"
updated_at: "2026-10-01"
base: "dev-v3"
---
# finance:出纳已付台账 ADVANCE 页签补预支详情接口(管理后台)
> ✅ **additive 纯新增接口**:新增 `GET /admin/finance/advances/{id}`,旧前端不受影响。
## 1. 接口背景
出纳「已付台账」按业务类型分 8 个页签(NONBIZ/EXPENSE/PAYMENT/PREPAY/STAFF_LOAN/REIMBURSE/COMPANY_LOAN/ADVANCE),每行点「明细」打开抽屉展示该笔业务详情。此前 7 个域都有各自的单笔详情接口,唯独 **ADVANCE 订单预支**没有:已付台账 ADVANCE 行的 `bizId` 指向 `fin_advance.advance_id`(司导预支财务执行单),但后端没有对应的单笔详情查询接口,前端抽屉只能用资金流水回单兜底,看不到订单号、团号、收款人、用途等关键业务信息。
本次补 `GET /admin/finance/advances/{id}`,让 ADVANCE 页签抽屉与其它 7 个页签一样能展示完整业务明细。
## 2. 变更清单
| # | 接口 | 变更 | 类型 |
|---|---|---|---|
| 1 | GET /admin/finance/advances/{id} | 新增司导预支执行单详情 | ✅ 新增接口 |
## 3. 接口详情
`GET /admin/finance/advances/{id}`(业务调用**不带**服务前缀,网关按 `/admin/finance/**` 路由到 order-v3)。
按 `fin_advance.advance_id` 查单笔预支执行单详情,供已付台账 ADVANCE 页签明细抽屉使用。
## 4. 入参
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| id | path | Long | 是 | 预支执行单ID(`fin_advance.advance_id`,即已付台账 ADVANCE 行的 `bizId`) |
## 5. 出参
`FinAdvanceDetailRespVO`:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Long(string) | 预支执行单ID |
| advanceNo | String | 预支单号(YZ- 前缀) |
| orderAdvanceId | Long(string) | 订单侧预支单ID(order_advance) |
| orderId | Long(string) | 订单ID |
| orderNo | String | 订单号 |
| teamNo | String | 团号(后端按 orderId 反查 order_main 补齐;订单无团号 → null) |
| payeeStaffId | Long(string) | 收款人员工ID |
| payeeName | String | 收款人姓名(报账人/司导) |
| advanceType | String | 预支类型(如 CATERING 餐饮 / FUEL 油费等,见数据字典) |
| amount | BigDecimal | 预支金额 |
| purpose | String | 用途说明 |
| fundAccountId | Long(string) | 出账资金账户ID |
| payFlowId | Long(string) | 付款资金流水ID(可跳流水回单) |
| paidAt | LocalDateTime | 付讫时间 |
| status | String | 状态(APPROVED 已审批 / PAID 已付款) |
| operatorName | String | 登记人姓名(用户域反查 createdBy;未登记企微名 → null) |
| createTime | LocalDateTime | 创建时间 |
> 所有 Long 型 ID 均已字符串化(`@JsonSerialize(ToStringSerializer)`),前端按 string 处理,避免 JS 精度丢失。
## 6. 枚举/数据字典
### status(预支执行单状态)
| 值 | 含义 |
|---|---|
| APPROVED | 已审批(待付款) |
| PAID | 已付款 |
### advanceType(预支类型)
走业务数据字典(如 CATERING 餐饮 / FUEL 油费 / TICKET 门票 等),具体取值以字典接口为准,前端展示走字典 label。
## 7. 错误码
| 错误码 | 含义 | 触发 |
|---|---|---|
| 599500 | 预支单不存在 | id 不存在或已软删 |
## 8. 示例
### 8.1 典型:已付款预支单详情
```http
GET /admin/finance/advances/2104861782621462530
→ 200
{
"id": "2104861782621462530",
"advanceNo": "YZ-202609290001",
"orderAdvanceId": "2104861625016287233",
"orderId": "2100743225424621570",
"orderNo": "HL20260918082629372",
"teamNo": "26-8707",
"payeeStaffId": "2100747615736897537",
"payeeName": "刘大山",
"advanceType": "CATERING",
"amount": 260.0,
"purpose": "满洲里中俄边境午餐代垫",
"fundAccountId": "1962000000000008001",
"payFlowId": "2105436186380242945",
"paidAt": "2026-10-01 00:00:00",
"status": "PAID",
"operatorName": "金卫",
"createTime": "2026-09-29 17:12:10"
}
```
### 8.2 边界:订单无团号 / 登记人未登记企微名
```http
GET /admin/finance/advances/{id}
→ 200,teamNo=null / operatorName=null(对应反查为空时降级为 null,不阻塞详情)
```
### 8.3 异常:id 不存在
```http
GET /admin/finance/advances/999999999
→ {"code":599500,"message":"预支单不存在","data":null,"success":false}
```
## 9. 业务边界
- `teamNo` 由后端按 `orderId` 反查 `order_main` 实时补齐(订单无团号或订单不存在 → null),**非 fin_advance 快照字段**;前端可据此跳「订单维度预支列表」`/v3/admin/order/{orderId}/advances`。
- `operatorName` 反查用户域 createdBy,未登记企微名 / 用户域暂不可用 → 降级为 null(查询类不 fail-fast)。
- 已付台账 ADVANCE 行的 `bizId` 即本接口的 `id`,前端抽屉直接用行 `bizId` 调本接口。
## 10. 修改前后对比
新增接口,无「修改前」。
| 项 | 修改前 | 修改后 |
|---|---|---|
| ADVANCE 页签明细抽屉 | 无详情接口,只能流水回单兜底 | 可调本接口展示完整业务明细 |
## 11. 影响评估/回滚
- additive 纯新增,旧前端零影响;不调用本接口无变化。
- 回滚:删除该 Controller/Service/VO 即可,无 DDL、无数据迁移。
## 12. 注意事项
- Long 型 ID 全部是 string,前端勿按 number 解析。
- `teamNo` / `operatorName` 可能为 null(反查为空降级),前端做空值兜底展示。
- 本接口为只读查询,鉴权走网关 `/admin/**` 常规 JWT。
## 13. 关联/联系人
- Issue:https://git.1814.love/wx/HL/issues/8680
- PR:https://git.1814.love/wx/HL/pulls/8682
- merge commit:93e5ee681d8e792a2d110faee5f3356ccb70cf8b
- 后端负责人:腰苏图
@@ -0,0 +1,218 @@
---
schema: "hl-changelog/v2"
ticket: "8689"
title: "核销管理后端落地:坏账核销/债务豁免 5 端点(#8689)"
consumer: "admin"
author: "yst(GIT)"
change_type: "新增接口"
backend_status: "merged"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "01436d457302207d2684584c48c50818936fdecf"
target_release: "v2.1"
verified_at: "2026-10-02"
status_note: "核销管理(API §6.5 设计稿)本期提前落地。核销=往来账面抹债不动资金(无资金流水、不改账户结存、不过出纳),与支付本质区别:支付是钱真动了,核销只是账认了这笔损失/对冲。本期落地 SUPPLIER 债务豁免(冲减应付)+ CUSTOMER 坏账核销(冲减应收)两类;STAFF 员工·司导往来不做。新增 /admin/finance/writeoffs 5 端点(两页签列表/发起核销/提交审批/批准/驳回)。审批本期本地手工批(企微审批流留 TODO),金额≥阈值(默认 5000)落待审批、<阈值建单即入账。坏账核销只落往来台账留痕、不回改订单侧应收金额。前端已交付(用户拍板已部署立项):新建核销管理页(hiddenRoute 先行,核销审批/核销记录双页签懒加载,批准二次确认+驳回原因必填,LOG 筛选仅用契约 keyword/ledgerType/status 三参,原型类型/日期筛选与页签角标契约无入参不渲染);发起核销弹窗页内+供应商往来账行「核销」两入口共用(账套→类型一对一联动,供应商走档案弹窗带 refId、客户按名聚合不传 refId,阈值分流以响应 needApproval 为准不前端预判);submit 手工批仅守卫不暴露按钮;错误码全走拦截器透 message。页 spec 5 例+弹窗 spec 6 例+供应商往来账 wiring 1 例,21 例全绿。"
updated_at: "2026-10-01"
base: "dev-v3"
---
# finance:核销管理后端落地(坏账核销/债务豁免 5 端点)(管理后台)
> **PR**: #8697 | **服务**: hl-order-service-v3(hl-finance 编译其中) | **更新时间**: 2026-10-01
## 1. 接口背景
财务往来账上会有「收不回的应收(坏账)」和「不用付的应付(债务豁免)」,需要从账面轧掉认损。这就是**核销**。
核销与支付的本质区别:
- **支付**:钱真的动了(产生资金流水、账户结存变动、过出纳)
- **核销**:只是账面抹债——**不动资金、无资金流水、不改账户结存、不过出纳**,只在该对象的往来台账上记一笔反向对冲行(净额减)
本期落地两类核销:
| 核销类型 | 账套 | 作用 |
|---|---|---|
| 债务豁免 `DEBT_WAIVER` | 供应商 `SUPPLIER` | 冲减应付(不用付了的应付款轧掉) |
| 坏账核销 `BAD_DEBT` | 客户 `CUSTOMER` | 冲减应收(收不回的应收款认损失) |
> **STAFF 员工·司导往来本期不做**(该账套属后续 Epic、净往来无来源)。报销冲抵不进本表(走费用域闭环)。
此前核销管理只有设计稿(API §6.5),本次后端正式落地 5 个端点。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 核销分页(两页签) | GET | /admin/finance/writeoffs/page | 新增 | PENDING 待审批 / LOG 全量记录 |
| 2 | 发起核销 | POST | /admin/finance/writeoffs | 新增 | 建核销单,按金额阈值分流 |
| 3 | 提交审批 | POST | /admin/finance/writeoffs/{id}/submit | 新增 | 本期本地手工批,仅守卫 |
| 4 | 批准核销 | POST | /admin/finance/writeoffs/{id}/approve | 新增 | 入账:写台账对冲行 |
| 5 | 驳回核销 | POST | /admin/finance/writeoffs/{id}/reject | 新增 | 驳回不动账 |
## 3. 接口详情
### 3.1 核销分页(两页签)
- **使用场景**:核销管理页两页签——「待审批」列待批核销单,「核销记录」列全量(已入账+已驳回)
- **认证**:需登录,财务查看权限
- **入参(Query)**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| tab | String | ✅ | 页签:`PENDING` 待审批 / `LOG` 核销记录(其他值报 596013) |
| ledgerType | String | ❌ | 账套过滤:`SUPPLIER` / `CUSTOMER` |
| status | String | ❌ | 状态过滤;**LOG 页签仅允许 `POSTED`/`REJECTED`**(传其他报 596013),PENDING 页签忽略 |
| keyword | String | ❌ | 核销单号 / 往来对象名 模糊 |
| pageNo / pageSize | int | ✅ | 分页 |
- **出参**:`PageResult<WriteoffRowRespVO>`
### 3.2 发起核销
- **使用场景**:财务在某往来对象上发起一笔核销
- **入参(Body,WriteoffCreateReqVO)**:
| 字段 | 类型 | 必填 | 说明 | 校验 |
|------|------|------|------|------|
| ledgerType | String | ✅ | 账套:`SUPPLIER` / `CUSTOMER` | |
| writeoffType | String | ✅ | 核销类型:`DEBT_WAIVER`(SUPPLIER)/ `BAD_DEBT`(CUSTOMER) | 与账套不匹配报 596014 |
| refId | Long | 见说明 | 往来对象 ID(SUPPLIER 必填) | |
| refName | String | ✅ | 往来对象名(CUSTOMER 按客户名聚合) | |
| amount | BigDecimal | ✅ | 核销金额 | >0(596012)、@Digits(16,2)、≤ 该对象净往来(596001) |
| reason | String | ✅ | 核销原因 | |
| sourceType / sourceId / sourceNo | String/Long/String | ❌ | 来源单据(可选追溯) | |
- **分流**:金额 < 阈值(默认 5000)→ 直接 `POSTED` 入账;≥ 阈值 → `PENDING_APPROVAL` 待审批
- **出参**:`WriteoffCreateRespVO`(writeoffId / writeoffNo / status / needApproval)
### 3.3 提交审批
- **使用场景**:≥阈值核销单提交走审批
- **本期说明**:审批为**本地手工批**(对齐费用/支付),企微审批流留 TODO 不接;submit 仅做状态守卫、状态不变,返回空串实例 ID
- **入参**:路径 `id`
- **出参**:`WriteoffSubmitRespVO`(approvalInstanceId 本期恒空串)
### 3.4 批准核销
- **使用场景**:批准一笔待审批核销 → 入账
- **动作**:`PENDING_APPROVAL` → `POSTED`,写一行往来台账反向对冲行(净额减),回写核销单 statement_entry_id + posted_at + 审批人快照
- **防超额**:批准时会**重算该对象当前净往来**,若 PENDING 期间净额已被其他入账冲减到不够核销,报 596001 不予入账
- **入参**:路径 `id`
### 3.5 驳回核销
- **使用场景**:驳回一笔待审批核销(不动账)
- **动作**:`PENDING_APPROVAL` → `REJECTED`,记驳回原因 + 审批人快照
- **入参**:路径 `id` + Body `WriteoffRejectReqVO`:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| reason | String | ✅ | 驳回原因 |
## 5. 出参(核销行 WriteoffRowRespVO)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 核销单 ID(字符串化) |
| writeoffNo | String | 核销单号(HX- 开头) |
| ledgerType | String | 账套 SUPPLIER/CUSTOMER |
| ledgerTypeName | String | 账套中文名 |
| writeoffType | String | 核销类型 DEBT_WAIVER/BAD_DEBT |
| writeoffTypeName | String | 核销类型中文名 |
| refId | Long | 往来对象 ID |
| refName | String | 往来对象名 |
| amount | BigDecimal | 核销金额 |
| reason | String | 核销原因 |
| status | String | 状态 PENDING_APPROVAL/POSTED/REJECTED |
| statusName | String | 状态中文名 |
| needApproval | Integer | 是否需审批 0/1 |
| operatorName | String | 经办人 |
| approverName | String | 审批人(本期手工批回填) |
| rejectReason | String | 驳回原因 |
| postedAt | String | 入账时间 |
| createTime | String | 创建时间 |
## 6. 枚举 / 数据字典
### 6.1 ledgerType(账套)
| 值 | 中文 | 说明 |
|----|------|------|
| SUPPLIER | 供应商 | 应付侧 |
| CUSTOMER | 客户 | 应收侧 |
### 6.2 writeoffType(核销类型)
| 值 | 中文 | 适用账套 |
|----|------|----------|
| DEBT_WAIVER | 债务豁免 | SUPPLIER |
| BAD_DEBT | 坏账核销 | CUSTOMER |
### 6.3 status(核销单状态)
| 值 | 中文 | 说明 |
|----|------|------|
| PENDING_APPROVAL | 待审批 | ≥阈值待批 |
| POSTED | 已入账 | 已写台账对冲行 |
| REJECTED | 已驳回 | 审批驳回 |
### 6.4 tab(页签)
| 值 | 中文 |
|----|------|
| PENDING | 待审批 |
| LOG | 核销记录 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| 596001 | 核销金额超过该对象净往来 | 发起 / 批准时金额 > 净往来 |
| 596002 | 核销单状态不允许该操作 | 对非待审批单做 submit/approve/reject |
| 596004 | 往来对象不存在 | 供应商/客户查无(SUPPLIER 无任何期初与流水) |
| 596011 | 核销单不存在 | id 查无 |
| 596012 | 核销金额无效须>0 | amount ≤0 |
| 596013 | 核销页签非法 / LOG 页签 status 非法 | tab 非 PENDING/LOG;LOG 传非 POSTED/REJECTED |
| 596014 | 核销类型与账套不匹配 | SUPPLIER 传 BAD_DEBT / CUSTOMER 传 DEBT_WAIVER |
| 596015 | 核销单号取号撞号耗尽 | HX- 取号并发耗尽(极少) |
## 8. 示例
### 8.1 典型成功(发起一笔供应商债务豁免,<阈值直接入账)
**请求** POST /admin/finance/writeoffs
```json
{ "ledgerType": "SUPPLIER", "writeoffType": "DEBT_WAIVER", "refId": 101, "refName": "嘉世豪酒店", "amount": 800.00, "reason": "供应商同意减免尾款" }
```
**响应**
```json
{ "code": 0, "data": { "writeoffId": "1234567890", "writeoffNo": "HX-202610010001", "status": "POSTED", "needApproval": 0 }, "msg": "" }
```
### 8.2 边界(金额 ≥ 阈值 → 落待审批)
**请求** POST /admin/finance/writeoffs
```json
{ "ledgerType": "CUSTOMER", "writeoffType": "BAD_DEBT", "refName": "张三", "amount": 6000.00, "reason": "客户失联认损" }
```
**响应**
```json
{ "code": 0, "data": { "writeoffId": "1234567891", "writeoffNo": "HX-202610010002", "status": "PENDING_APPROVAL", "needApproval": 1 }, "msg": "" }
```
### 8.3 业务失败(核销金额超净往来)
**响应**
```json
{ "code": 596001, "msg": "核销金额超过该对象净往来", "data": null }
```
## 9. 业务边界
- ✅ 核销只动往来账面:在该对象台账记一行反向对冲(净额减)
- ❌ 核销**不产生资金流水、不改账户结存、不过出纳**
- ❌ 坏账核销**不回改订单侧应收金额**——核销是财务账面认损失,订单应收仍在;CUSTOMER 按客户名聚合校验+回扣已核销防重复
- ⚠️ 同名客户本期算一起(应收台账行无 customerId),将来补 customerId 后升级按 ID
## 13. 关联 / 联系人
- **Issue**: [#8689](https://git.1814.love/wx/HL/issues/8689)
- **PR**: [#8697](https://git.1814.love/wx/HL/pulls/8697)
- **Merge commit**: [04e7ff6025](https://git.1814.love/wx/HL/commit/04e7ff60250f9199684856801452bd1d0062e46a)
- **后端负责人**: @yaosutu
@@ -0,0 +1,437 @@
---
schema: "hl-changelog/v2"
ticket: "8690"
title: "微信商户对账:账单下载解析比对落库+差异处理+手动补跑(6 端点)"
consumer: "admin"
author: "yst(GIT)"
change_type: "新增接口"
backend_status: "merged"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "8175316c88d4f555538d7e65d3744102bccfee90"
target_release: "v2.1"
verified_at: "2026-10-02"
status_note: "前端交付(2026-10-02,用户拍板已部署):新建 finance/wx-bill 页(hiddenRoute 先行,sys_menu 未下挂;汇总台账 dateRange 拆 billDateFrom/To/差异笔数>0 飘红/手动补跑 billDate 必填+锁占用 data=null 按契约文案提示)+BillRecordsDrawer 逐笔快照+BillDiffsDrawer 差异处理(先拉详情防 582404/结论仅 CONFIRMED·IGNORED/只改标记不调账)+api/finance/wx-bill.js(分页 page 非 pageNo 已钉),spec 8 例全绿,提交 8175316c。"
updated_at: "2026-10-02"
base: "dev-v3"
---
# 【新增接口·管理后台】微信商户对账(6 端点)(#8690)
> **PR**: #8703 | **服务**: hl-order-service-v3(8086) | **更新时间**: 2026-10-02
## 1. 接口背景
微信支付此前只有「支付回调」这一条正向链路:微信回调成功 → 本地记成功流水。一旦回调丢失、金额异常或微信侧状态变化,本地无任何反向核对手段,资金敞口不可见。
本次建设**微信商户对账**能力:每日 T-1 自动拉取微信商户**交易账单(tradebill)+ 资金账单(fundflowbill)**,下载解析后与本地支付流水逐笔比对落库,差异打标并通过 FINANCE 站内信告警,**不自动调账**(差异一律人工核实处理)。
本 changelog 覆盖管理后台消费的 **6 个新端点**(对账执行本体由定时任务触发,不在本接口面)。菜单归属:**财务管理 → 资金账户 → 微信商户对账**。单视图「对账汇总」台账,逐笔明细 / 差异走抽屉下钻(records / diffs 接口)。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 对账汇总分页列表 | GET | /v3/admin/payment/wx-bill/summaries | 新增接口 | 主视图台账,每日每商户每账单类型一行 |
| 2 | 逐笔原始账单记录 | GET | /v3/admin/payment/wx-bill/records | 新增接口 | 汇总行「明细」抽屉数据源 |
| 3 | 差异分页列表 | GET | /v3/admin/payment/wx-bill/diffs | 新增接口 | 「看差异」抽屉数据源 |
| 4 | 差异详情 | GET | /v3/admin/payment/wx-bill/diffs/{diffId} | 新增接口 | 单条差异完整信息(含处理记录) |
| 5 | 处理差异 | POST | /v3/admin/payment/wx-bill/diffs/{diffId}/handle | 新增接口 | 财务人工确认/忽略 + 备注,不自动调账 |
| 6 | 手动触发对账补跑 | POST | /v3/admin/payment/wx-bill/reconcile/run | 新增接口 | 运维补账入口,与定时任务同逻辑同锁 |
## 3. 接口详情
### 3.1 对账汇总分页列表 GET /v3/admin/payment/wx-bill/summaries
- **使用场景**:主视图台账。每行 = 一个商户 + 一个账单日期 + 一种账单类型的对账结论(总笔数/总金额/对平/差异/结果)。
- **认证**:需管理后台 JWT。
- **幂等性**:只读。
- **限流**:无。
### 3.2 逐笔原始账单记录 GET /v3/admin/payment/wx-bill/records
- **使用场景**:汇总行「明细」抽屉,看该商户该日微信账单原始逐笔(交易账单/资金账单统一出参,billType 区分)。
- **认证**:需管理后台 JWT。
- **幂等性**:只读。
- **限流**:无。
### 3.3 差异分页列表 GET /v3/admin/payment/wx-bill/diffs
- **使用场景**:「看差异」抽屉 / 差异工作台。支持按日期范围、差异类型、处理状态、商户号、账单类型过滤。
- **认证**:需管理后台 JWT。
- **幂等性**:只读。
- **限流**:无。
### 3.4 差异详情 GET /v3/admin/payment/wx-bill/diffs/{diffId}
- **使用场景**:差异行点开的详情(本地金额 vs 账单金额、差异说明、处理人/处理时间/备注)。
- **认证**:需管理后台 JWT。
- **幂等性**:只读。
- **限流**:无。
### 3.5 处理差异 POST /v3/admin/payment/wx-bill/diffs/{diffId}/handle
- **使用场景**:财务人工核实差异后标记「已确认」或「已忽略」并留备注。**只做标记,不自动调账**(调账动作走财务调账域,不在本接口)。
- **认证**:需管理后台 JWT。
- **幂等性**:**否**。仅 PENDING 状态可处理;重复处理报 582404(已处理不可重复处理),天然防重。
- **限流**:无。
### 3.6 手动触发对账补跑 POST /v3/admin/payment/wx-bill/reconcile/run
- **使用场景**:运维补账——定时任务失败 / 账单迟到后,手动对指定账单日期补跑一次。与 internal 定时任务**同逻辑、同一把分布式锁**(key=wx-bill-reconcile:{billDate})。
- **认证**:需管理后台 JWT。
- **幂等性**:**是**(业务侧)。重跑零重复:原始记录按键集只插缺失、汇总查到即覆盖、差异按指纹去重。但**同一账单日期对账执行中时**(锁被占用)不重复执行,返回提示文案且 data=null。
- **限流**:无。
## 4. 接口入参
### 4.1 对账汇总分页列表(Query)
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| page | Integer | 否 | 页码,默认 1 | 最小 1 |
| pageSize | Integer | 否 | 每页条数,默认 20 | 1-100 |
| billDateFrom | Date | 否 | 账单日期起(yyyy-MM-dd) | — |
| billDateTo | Date | 否 | 账单日期止(yyyy-MM-dd) | — |
| mchId | String | 否 | 微信商户号(精确匹配) | — |
| billType | String | 否 | 账单类型 | 仅 TRADE / FUND_FLOW,非法值报参数校验错误 |
| reconcileStatus | String | 否 | 对账结果 | 仅 OK / HAS_DIFF / FETCH_FAILED,非法值报参数校验错误 |
### 4.2 逐笔原始账单记录(Query)
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| page | Integer | 否 | 页码,默认 1 | 最小 1 |
| pageSize | Integer | 否 | 每页条数,默认 20 | 1-100 |
| billDate | Date | 否 | 账单日期(yyyy-MM-dd,单日) | — |
| mchId | String | 否 | 微信商户号(精确匹配) | — |
| billType | String | 否 | 账单类型 | 仅 TRADE / FUND_FLOW |
### 4.3 差异分页列表(Query)
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| page | Integer | 否 | 页码,默认 1 | 最小 1 |
| pageSize | Integer | 否 | 每页条数,默认 20 | 1-100 |
| billDateFrom | Date | 否 | 账单日期起(yyyy-MM-dd) | — |
| billDateTo | Date | 否 | 账单日期止(yyyy-MM-dd) | — |
| mchId | String | 否 | 微信商户号(精确匹配) | — |
| billType | String | 否 | 账单类型 | 仅 TRADE / FUND_FLOW |
| diffType | String | 否 | 差异类型 | 仅 LOCAL_MISSING / BILL_MISSING / AMOUNT_MISMATCH / STATE_MISMATCH / FETCH_FAILED |
| handleStatus | String | 否 | 处理状态 | 仅 PENDING / CONFIRMED / IGNORED |
### 4.4 差异详情(Path)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| diffId | Long | 是 | 差异ID(path) |
### 4.5 处理差异(Path + Body)
Path:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| diffId | Long | 是 | 差异ID(path) |
请求体:
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| handleStatus | String | 是 | 处理状态 | 仅 CONFIRMED(已确认)/ IGNORED(已忽略),**不能传 PENDING** |
| remark | String | 否 | 处理备注 | 最长 255 字符 |
### 4.6 手动触发对账补跑(Body)
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| billDate | Date | 是 | 账单日期(yyyy-MM-dd) | 建议传 T-1 或更早(微信当日账单 T+1 上午才生成) |
| mchId | String | 否 | 微信商户号 | 可空 = 全部已配置商户 |
| billType | String | 否 | 账单类型 | 仅 TRADE / FUND_FLOW;可空 = 交易+资金都跑 |
## 5. 出参(响应)
统一 Result 包装;分页为 PageResult(records / total / page / pageSize)。
> ⚠️ **Long 主键字符串化**:id(汇总/记录/差异)与 localTransactionId 均以 **JSON 字符串**返回(防 JS 精度丢失),前端按字符串处理,回传 path 参数时原样带回即可。handledBy 为普通数字。
### 5.1 对账汇总行 WxBillSummaryRespVO
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String(Long) | 汇总ID |
| mchId | String | 微信商户号 |
| billDate | Date | 账单日期(yyyy-MM-dd) |
| billType | String | 账单类型:TRADE / FUND_FLOW |
| totalCount | Integer | 账单总笔数 |
| totalAmount | BigDecimal | 账单总金额(元) |
| matchedCount | Integer | 对平笔数 |
| diffCount | Integer | 差异笔数 |
| reconcileStatus | String | 对账结果:OK / HAS_DIFF / FETCH_FAILED |
| createTime | DateTime | 创建时间(yyyy-MM-dd HH:mm:ss) |
### 5.2 账单原始记录行 WxBillRecordRespVO
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String(Long) | 记录ID |
| mchId | String | 微信商户号 |
| billDate | Date | 账单日期 |
| billType | String | 账单类型:TRADE / FUND_FLOW |
| wxTransactionId | String | 微信支付单号(交易账单有,资金账单可空) |
| outTradeNo | String | 商户单号(关联本地支付流水) |
| fundFlowId | String | 资金流水单号(资金账单有,交易账单可空) |
| tradeTime | DateTime | 交易/记账时间 |
| tradeType | String | 交易类型/业务类型(如 JSAPI) |
| tradeState | String | 交易状态/收支方向(如 SUCCESS) |
| amount | BigDecimal | 交易金额(元) |
| payerAmount | BigDecimal | 用户实付(元) |
| feeAmount | BigDecimal | 手续费(元) |
### 5.3 差异行 / 差异详情 WxBillDiffRespVO
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String(Long) | 差异ID |
| mchId | String | 微信商户号 |
| billDate | Date | 账单日期 |
| diffType | String | 差异类型(见 6.2) |
| billType | String | 账单类型:TRADE / FUND_FLOW |
| outTradeNo | String | 商户单号 |
| wxTransactionId | String | 微信支付单号 |
| localAmount | BigDecimal | 本地金额(元),本地缺失类差异可空 |
| billAmount | BigDecimal | 账单金额(元),账单缺失类差异可空 |
| localTransactionId | String(Long) | 本地支付流水ID,本地缺失类差异可空 |
| diffDetail | String | 差异说明(人读文案) |
| handleStatus | String | 处理状态:PENDING / CONFIRMED / IGNORED |
| handleRemark | String | 处理备注,未处理为空 |
| handledBy | Long(数字) | 处理人ID,未处理为空 |
| handledAt | DateTime | 处理时间,未处理为空 |
| createTime | DateTime | 创建时间 |
处理差异接口的响应体同为 WxBillDiffRespVO(处理后的最新状态)。
### 5.4 手动触发对账执行结果 WxBillReconcileRunRespVO
| 字段 | 类型 | 说明 |
|------|------|------|
| billDate | Date | 对账账单日期 |
| merchantCount | Integer | 参与对账的商户数 |
| diffCount | Integer | 本次对账差异总笔数(含历史未清) |
| fetchFailedCount | Integer | 账单拉取失败的商户类型数 |
> ⚠️ **锁占用特例**:当日该账单日期对账正在执行中时,本接口返回 code=0、msg="当日对账正在执行中,请稍后重试"、data=null——属正常跳过,非错误,前端按提示文案展示即可。
## 6. 枚举 / 数据字典
### 6.1 billType(账单类型)
| 值 | 中文 | 说明 |
|----|------|------|
| TRADE | 交易账单 | 微信 tradebill,逐笔交易(含支付/退款) |
| FUND_FLOW | 资金账单 | 微信 fundflowbill,逐笔资金收支(含手续费/结算) |
### 6.2 diffType(差异类型)
| 值 | 中文 | 说明 |
|----|------|------|
| LOCAL_MISSING | 本地缺失 | 账单有该单(SUCCESS)但本地无成功流水——**回调丢失,高危**,优先处理 |
| BILL_MISSING | 账单缺失 | 本地有成功流水但账单无该单——可疑(本地虚单/未结算) |
| AMOUNT_MISMATCH | 金额不符 | 两边都有但金额不一致 |
| STATE_MISMATCH | 状态不符 | 账单状态非 SUCCESS(REFUND/CLOSED)但本地仍 SUCCESS |
| FETCH_FAILED | 拉取失败 | 该商户该类型账单下载/申请失败,根本没对上,需重跑 |
### 6.3 handleStatus(处理状态)
| 值 | 中文 | 说明 |
|----|------|------|
| PENDING | 待处理 | 默认状态,仅此状态可调用处理接口 |
| CONFIRMED | 已确认 | 人工已核实确认 |
| IGNORED | 已忽略 | 人工核实后忽略(如已线下解决) |
### 6.4 reconcileStatus(对账结果)
| 值 | 中文 | 说明 |
|----|------|------|
| OK | 对平 | 全部对平,0 差异 |
| HAS_DIFF | 有差异 | 对出差异,需人工处理 |
| FETCH_FAILED | 拉取失败 | 账单没拉到,根本没对上,需重跑 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| 400 | 参数校验失败 | 枚举字段传非法值 / 必填缺失 / remark 超 255 字符(HTTP 200 + Result.error(400),msg 为具体校验文案) |
| 582400 | 微信账单申请失败 | 微信侧拒绝 / 无当日账单(对账执行期,管理端查询不直接触发) |
| 582401 | 微信账单下载失败 | 账单下载票/文件拉取失败(对账执行期) |
| 582402 | 微信账单解析失败 | 账单文件解析失败(对账执行期) |
| 582403 | 对账差异记录不存在 | 差异详情 / 处理差异时 diffId 查无此记录 |
| 582404 | 该差异已处理,不可重复处理 | 处理差异时该差异已非 PENDING |
| 582405 | 找不到商户配置 | 手动补跑指定的 mchId 无微信支付商户配置 |
| 582406 | 对账汇总记录不存在 | 汇总记录查无(预留) |
## 8. 示例
### 8.1 典型成功(对账汇总列表 + 差异处理)
请求:
GET /v3/admin/payment/wx-bill/summaries?billDateFrom=2026-09-01&billDateTo=2026-09-30&reconcileStatus=HAS_DIFF&page=1&pageSize=20
Authorization: Bearer <token>
响应:
```json
{
"code": 0,
"data": {
"records": [
{
"id": "1890000000000000001",
"mchId": "1246532201",
"billDate": "2026-09-30",
"billType": "TRADE",
"totalCount": 25,
"totalAmount": 12800.00,
"matchedCount": 24,
"diffCount": 1,
"reconcileStatus": "HAS_DIFF",
"createTime": "2026-10-01 08:30:00"
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"msg": ""
}
```
处理差异请求:
POST /v3/admin/payment/wx-bill/diffs/1890000000000000009/handle
Authorization: Bearer <token>
Content-Type: application/json
```json
{
"handleStatus": "CONFIRMED",
"remark": "已核实回调丢失,手动补登记"
}
```
处理差异响应:
```json
{
"code": 0,
"data": {
"id": "1890000000000000009",
"mchId": "1246532201",
"billDate": "2026-09-30",
"diffType": "LOCAL_MISSING",
"billType": "TRADE",
"outTradeNo": "HL20260930120000001234",
"wxTransactionId": "4200001234202609301234567890",
"localAmount": null,
"billAmount": 500.00,
"localTransactionId": null,
"diffDetail": "账单SUCCESS本地无成功流水,疑似支付回调丢失",
"handleStatus": "CONFIRMED",
"handleRemark": "已核实回调丢失,手动补登记",
"handledBy": 1,
"handledAt": "2026-10-01 10:00:00",
"createTime": "2026-10-01 08:30:00"
},
"msg": ""
}
```
### 8.2 边界(差异列表空结果 + 手动补跑锁占用)
空差异页请求(对平的日期段):
GET /v3/admin/payment/wx-bill/diffs?billDateFrom=2026-09-01&billDateTo=2026-09-30&handleStatus=PENDING&page=1&pageSize=20
响应(空页为正常结果,非错误):
```json
{
"code": 0,
"data": { "records": [], "total": 0, "page": 1, "pageSize": 20 },
"msg": ""
}
```
手动补跑(当日对账执行中)请求:
POST /v3/admin/payment/wx-bill/reconcile/run
Content-Type: application/json
```json
{ "billDate": "2026-09-30" }
```
响应(锁被占用,data=null,按 msg 提示展示):
```json
{
"code": 0,
"data": null,
"msg": "当日对账正在执行中,请稍后重试"
}
```
### 8.3 业务失败(重复处理差异,582404)
请求(对一条已 CONFIRMED 的差异再次处理):
POST /v3/admin/payment/wx-bill/diffs/1890000000000000009/handle
Content-Type: application/json
```json
{ "handleStatus": "IGNORED", "remark": "重复操作" }
```
响应:
```json
{ "code": 582404, "msg": "该差异已处理,不可重复处理", "data": null }
```
## 9. 业务边界
- 适用:查询/处理**已由对账任务落库**的数据——每日 T-1 定时任务(早晨)跑完后,对应账单日期的汇总/记录/差异才可见;手动补跑成功后立即可见。
- 不适用:
- 当日(T 日)账单——微信当日账单 T+1 上午才生成,对当日日期查/跑只会得到 FETCH_FAILED 或空;
- 期望接口实时向微信拉账单展示——records 展示的是**落库快照**,不是实时微信数据。
- 特殊:
- LOCAL_MISSING(本地缺失)= 回调丢失高危差异,建议财务优先处理;
- 差异**处理只改标记不改账**——确认/忽略不会触发任何资金或订单侧动作;
- FETCH_FAILED 类汇总/差异的正确处理动作是**重跑**(手动补跑接口),不是人工确认。
## 10. 修改前后对比
新增接口,无修改前版本。本组 6 端点全部为首次交付,无旧路径、无字段变更。
## 11. 影响评估 / 回滚
- **是否破坏向后兼容**:否(纯新增端点 + 纯新增表,无既有接口/字段改动)。
- **前端是否必须同步上线**:否(不接入不影响任何既有功能;接入后提供「微信商户对账」视图)。
- **回滚方式**:revert PR #8703 即可下线 6 端点;三张新表(wx_bill_record / wx_bill_diff / wx_bill_summary)为独立新表,不回滚也不影响既有功能。
## 12. 注意事项
- **Long 字符串化**:id / localTransactionId 为 JSON 字符串(见 §5 开头),表格 key、路由参数、处理接口 path 回传时**不要 Number() 转换**。
- **处理差异非幂等但防重**:重复处理报 582404,前端提交后可禁按钮防连点,收到 582404 时刷新该行为宜。
- **手动补跑是重操作**:逐商户下载微信账单 + 全量比对,锁 TTL 30 分钟;触发后建议稍后刷新汇总列表看结果,不要连续点击(锁占用会返回 data=null 提示)。
- **枚举值严格校验**:Query 中的 billType / reconcileStatus / diffType / handleStatus 传非法值会被参数校验拦截(HTTP 200 + code 400),下拉框请只渲染 §6 列出的值。
- 出参中可空字段(如资金账单的 wxTransactionId、交易账单的 fundFlowId、未处理差异的 handleRemark / handledBy / handledAt)返回 null,展示需做空值兜底。
## 13. 关联 / 联系人
- **Issue**: [#8690](https://git.1814.love/wx/HL/issues/8690)
- **PR**: [#8703](https://git.1814.love/wx/HL/pulls/8703)
- **Merge commit**: [faeed6e0a7](https://git.1814.love/wx/HL/commit/faeed6e0a7)
- **后端负责人**: @yst
@@ -0,0 +1,195 @@
---
schema: "hl-changelog/v2"
ticket: "8693"
title: "fin_advance 抽象化:预支详情三件套反转 + 接通团期级预支进支付管理(#8693)"
consumer: "admin"
author: "yst(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "3dddf2de5000943503cd67e6cd3b6e05200333e6"
target_release: "v2.1"
verified_at: "2026-10-02"
status_note: "【反转 #8680 台账 225】#8680 昨天新增的 GET /admin/finance/advances/{id} 详情出参里 orderId/orderNo 两字段,本 PR 反转改为 bizType/refId/refNo 三件套(同日开发阶段、前端尚未消费该两字段,按用户拍板直接改不留冗余)。根因:fin_advance 原本直挂 order_id/order_no 是订单级专用结构,团期级预支(order_id 空)审批通过后被显式跳过推送导致钱付不出去(T27-2859 实证)。本 PR 把 fin_advance 重构为 biz_type+ref_id+ref_no+payee_ref_type 抽象关联(对齐同域 fin_reimburse 范式),接通团期级预支进支付管理。影响两接口:①详情接口出参 orderId/orderNo→bizType/refId/refNo(teamNo 口径变化);②出纳队列 ADVANCE 行新增 refNo 字段(additive)。前端交付(2026-10-02,与后端「前端尚未消费」自述相反,抽屉实际读 orderNo 属破坏点):LedgerDetailDrawer ADVANCE 归属行改按 bizType 分派(订单级「订单号」读 refNo+「团号」读 teamNo 可 null 兜底;团期级「团号」读 refNo,teamNo 同值不重复行),advance.js JSDoc 钉反转口径+ADVANCE_BIZ_TYPES,spec 13 例全绿,提交 3dddf2de。"
updated_at: "2026-10-01"
base: "dev-v3"
---
# finance:fin_advance 抽象化,接通团期级司导预支进支付管理(管理后台)
> ⚠️ **反转说明**:本 changelog 反转 **台账 225(#8680)** 昨天推送的详情出参 `orderId`/`orderNo` 两字段,改为 `bizType`/`refId`/`refNo` 三件套。两字段同日开发阶段、前端尚未消费,按用户拍板直接改不留冗余。
## 1. 接口背景
团期级司导预支(挂在出团批次下、不挂具体订单,如 T27-2859 给整个团派车司机预支油费)审批通过后,**进不了支付管理,钱付不出去**。
根因(两层):
1. **直接根因**:`fin_advance` 财务执行单表直挂 `order_id`/`order_no`,是订单级专用结构,无法表达「不挂订单」的团期级预支。
2. **结构根因**:订单侧 `approveAdvance` 对团期级预支**显式跳过**财务推送(`if orderId==null → 跳过`),导致审批通过后没有任何财务执行单生成。
本次把 `fin_advance` 重构为 **`biz_type` + `ref_id` + `ref_no` + `payee_ref_type` 抽象关联**(与同域 `fin_reimburse` 报账执行单的 biz_* 范式对齐),让一张表同时承接订单级和团期级预支,并接通团期级进支付管理的推送链路。
## 2. 变更清单
| # | 接口 | 变更 | 类型 |
|---|---|---|---|
| 1 | GET /admin/finance/advances/{id} | 出参 `orderId`/`orderNo` → `bizType`/`refId`/`refNo`;`teamNo` 口径变化 | 🔴 修改接口(反转 #8680) |
| 2 | GET /admin/finance/cashier/advance/queue | 行出参新增 `refNo` 字段 | ✅ additive |
## 3. 接口详情
### 3.1 GET /admin/finance/advances/{id}(详情,修改)
按 `fin_advance.advance_id` 查单笔预支执行单详情。出参的「归属业务对象」由「订单ID/订单号」改为「bizType/refId/refNo 三件套」,以支持订单级与团期级两种来源。
### 3.2 GET /admin/finance/cashier/advance/queue(出纳预支队列,additive)
出纳预支待付/已付台账列表。每行新增 `refNo` 字段,展示该笔预支归属的业务单号(订单号或团号快照)。
## 4. 入参
两接口入参均无变化(详情 `id` path 参数;队列 `pageNo`/`pageSize`/`tab` 等查询参数不变)。
## 5. 出参
### 5.1 详情 `FinAdvanceDetailRespVO`(修改部分)
| 字段 | 类型 | 说明 | 变化 |
|---|---|---|---|
| ~~orderId~~ | — | ~~订单ID~~ | 🔴 **删除**,由 `refId` 承接 |
| ~~orderNo~~ | — | ~~订单号~~ | 🔴 **删除**,由 `refNo` 承接 |
| bizType | String | 归属业务类型:`ADVANCE_ORDER` 订单级 / `ADVANCE_GROUP_BATCH` 团期级 | ✅ 新增 |
| refId | Long(string) | 归属业务对象ID:`bizType=ADVANCE_ORDER`→订单ID;`ADVANCE_GROUP_BATCH`→出团批次ID | ✅ 新增 |
| refNo | String | 归属业务单号快照:订单号 或 团号(展示用,冻结不追溯) | ✅ 新增 |
| teamNo | String | 团号。`ADVANCE_ORDER`→按 refId 反查 order_main;`ADVANCE_GROUP_BATCH`→**直接=refNo**(团号即归属,不再反查) | 🟡 口径变化 |
其余字段(id/advanceNo/orderAdvanceId/payeeStaffId/payeeName/advanceType/amount/purpose/fundAccountId/payFlowId/paidAt/status/operatorName/createTime)不变。
### 5.2 队列 `AdvanceQueueRowRespVO`(additive 部分)
| 字段 | 类型 | 说明 | 变化 |
|---|---|---|---|
| refNo | String | 归属业务单号(订单号/团号快照,`fin_advance.ref_no`)。`bizNo` 保持执行单号不变 | ✅ 新增 |
> 所有 Long 型 ID 均字符串化(`@JsonSerialize(ToStringSerializer)`),前端按 string 处理。
## 6. 枚举/数据字典
### bizType(预支归属业务类型)
| 值 | 含义 | refId 指向 | refNo 快照 |
|---|---|---|---|
| ADVANCE_ORDER | 订单级司导预支 | order_main.order_id | order_no 订单号 |
| ADVANCE_GROUP_BATCH | 团期级司导预支 | order_group_batch.group_batch_id | batch_no 团号 |
### payeeRefType(收款人多态类型,仅后端落库,详情/队列出参不透出)
| 值 | payeeStaffId 指向 |
|---|---|
| ORDER_ASSIGNMENT | order_staff_assignment.assignment_id(订单人员配置) |
| BATCH_STAFF | 资源域 staff.staff_id(人员主档,团期级预支候选取自 order_batch_staff) |
### status(预支执行单状态,不变)
| 值 | 含义 |
|---|---|
| APPROVED | 已审批(待付款) |
| PAID | 已付款 |
## 7. 错误码
| 错误码 | 含义 | 触发 |
|---|---|---|
| 599500 | 预支单不存在 | 详情 id 不存在或已软删(不变) |
| 599505 | 预支快照非法 | 推送时 bizType/refId/refNo/payeeRefType 为空或枚举非法(后端内部校验,前端无感) |
## 8. 示例
### 8.1 典型:订单级预支详情(bizType=ADVANCE_ORDER)
```http
GET /admin/finance/advances/2104861782621462530
→ 200
{
"id": "2104861782621462530",
"advanceNo": "YZ-202609290001",
"orderAdvanceId": "2104861625016287233",
"bizType": "ADVANCE_ORDER",
"refId": "2100743225424621570",
"refNo": "HL20260918082629372",
"teamNo": "26-8707",
"payeeStaffId": "2100747615736897537",
"payeeName": "刘大山",
"advanceType": "CATERING",
"amount": 260.0,
"purpose": "满洲里中俄边境午餐代垫",
"fundAccountId": "1962000000000008001",
"payFlowId": "2105436186380242945",
"paidAt": "2026-10-01 00:00:00",
"status": "PAID",
"operatorName": "金卫",
"createTime": "2026-09-29 17:12:10"
}
```
### 8.2 边界:团期级预支详情(bizType=ADVANCE_GROUP_BATCH)
```http
GET /admin/finance/advances/{id}
→ 200
{
"bizType": "ADVANCE_GROUP_BATCH",
"refId": "2104950790684889089",
"refNo": "T27-2859",
"teamNo": "T27-2859",
...
}
```
> 团期级 `teamNo` 直接等于 `refNo`(团号即归属,不再反查 order_main)。
### 8.3 异常:id 不存在
```http
GET /admin/finance/advances/999999999999999999
→ {"code":599500,"message":"预支单不存在","data":null,"success":false}
```
## 9. 业务边界
- 详情出参不再有 `orderId`/`orderNo`:要跳订单维度预支列表,用 `bizType=ADVANCE_ORDER` 时取 `refId` 作为订单ID;团期级(`ADVANCE_GROUP_BATCH`)无订单概念。
- `refNo` 是**快照字段**(冻结不追溯),仅作展示;不做关联查询键。
- `teamNo` 对团期级 = `refNo`(团号),对订单级 = 反查 order_main(订单无团号 → null)。
- 队列行 `bizNo` 保持执行单号(YZ- 前缀)不变,新增 `refNo` 专用于展示归属业务单号。
## 10. 修改前后对比
### 详情接口出参
| 字段 | 修改前(#8680) | 修改后(本 PR) |
|---|---|---|
| 归属业务对象 | `orderId` + `orderNo`(仅订单级) | `bizType` + `refId` + `refNo`(订单级/团期级通用) |
| 团号 teamNo | 按 orderId 反查 order_main | 订单级反查 order_main;团期级 = refNo 直出 |
### 队列接口出参
| 字段 | 修改前 | 修改后 |
|---|---|---|
| 归属业务单号 | 无(只有 bizNo 执行单号) | 新增 `refNo`(订单号/团号) |
## 11. 影响评估/回滚
- **前端影响**:详情出参 `orderId`/`orderNo` 被删。**同日开发阶段、前端尚未消费这两字段**(#8680 昨天刚合,前端按台账 225 的对接尚未上线消费),按用户拍板直接改不留冗余,无破坏性。队列 `refNo` 是 additive,旧前端零影响。
- **数据迁移**:存量订单级执行单由 Flyway `V20261001_131` 自动回填 `biz_type=ADVANCE_ORDER`/`ref_id=order_id`/`ref_no=order_no`,部署时自动跑,无需手工干预。`order_id`/`order_no` 列已物理删除。
- **回滚**:代码层 revert PR 即可;DDL 层 `order_id`/`order_no` 已 DROP 不可回滚(开发阶段接受)。
## 12. 注意事项
- Long 型 ID 全部是 string,前端勿按 number 解析。
- `bizType` 是判断归属业务类型的**唯一权威字段**,前端勿再用 `orderId` 是否为空判断订单级/团期级。
- `refId`/`teamNo`/`operatorName` 可能为 null(反查为空降级),前端做空值兜底展示。
- 本接口为只读查询,鉴权走网关 `/admin/**` 常规 JWT。
## 13. 关联/联系人
- Issue:https://git.1814.love/wx/HL/issues/8693
- PR:https://git.1814.love/wx/HL/pulls/8698
- merge commit:2ccca1b7b4
- 反转对象:台账 225(#8680,PR #8682)
- Flyway:V20261001_131__fin_advance_abstract_ref.sql
- 后端负责人:腰苏图
@@ -0,0 +1,510 @@
---
schema: "hl-changelog/v2"
ticket: "8246"
title: "流团审批放开团期管理员,批准后各户退款进退款审批中心二审——GB-ADM-062 审批人集合与批后退款行为、GB-ADM-061 canApprove 取值同步变化"
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: "jw 2026-10-02 定案,照 #8436 退单审批的做法:一是流团审批(GB-ADM-062 同意 / 拒绝)在管理员之外放行团期管理员 GROUP_BATCH_MANAGER,不新增权限码;二是同意流团后各户退款单停在 PENDING 进退款审批中心二审,不再以流团审批人身份自动放行。审批中心列表与流团详情的 canApprove 与端点同一道判定,团期管理员在待审批流团上由 false 变 true。两项都受 nacos 开关控制(默认开):group-batch.disband.refund-second-review 关掉即回到改前自动放行,并同时收回团期管理员的流团审批权;group-batch.acl.allow.disband-approver-group-batch-manager 关掉只收回团期管理员审批权。路径、入参、出参字段名与类型零变化,错误码不变(589547 文案「仅管理员可处理流团审批」未改,与 #8436 对 589530 的处理一致);变的是审批人集合、canApprove 取值与批后退款单状态,属行为 / 语义变化,按 jw 2026-09-18 口径推送。已合并 dev-v3(PR #8734,merge commit c4a410a94)并部署测试服。hl-ui v2.1 审批中心页已对团期管理员开放(canAccess = isAdmin || isGroupBatchManager),同意 / 拒绝按钮按 canApprove 显隐,前端零改动,frontend_status 记 not_required。"
updated_at: "2026-10-02"
base: "dev-v3"
---
# 流团审批放开团期管理员与批后退款二审(管理后台)
> **服务**: hl-order-service-v3(端口 8086/8186)
## 一、接口背景
团期管理员能发起流团,却批不了流团:流团审批的角色门只放管理员。退单审批已在 #8436 放开团期管理员,并把批后退款改成进退款审批中心二审;流团这次照同一做法处理。
本次两处行为变化:
- 流团审批(同意 / 拒绝)在管理员之外放行团期管理员;审批中心列表与流团详情的 `canApprove` 随之变化。
- 同意流团后,各户的退款单停在 `PENDING`,进退款审批中心由有退款审核权的人二审;改前是以流团审批人身份自动审核通过。这一条对所有审批人生效,不只团期管理员。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | GB-ADM-062 同意流团 | POST | `/v3/admin/order/group-batch/disband/{approvalId}/approve` | 修改接口 | 审批人集合加团期管理员;批后各户退款单停在 PENDING 进二审。入参、出参、错误码零变化 |
| 2 | GB-ADM-062 拒绝流团 | POST | `/v3/admin/order/group-batch/disband/{approvalId}/reject` | 修改接口 | 审批人集合加团期管理员。入参、出参、错误码零变化 |
| 3 | GB-ADM-061 团期审批中心列表 | GET | `/v3/admin/order/group-batch/approvals/page` | 修改接口 | 流团行 `canApprove` 对团期管理员由 false 变 true(待审批行);字段零变化 |
| 4 | GB-ADM-061 流团申请详情 | GET | `/v3/admin/order/group-batch/disband/{approvalId}` | 修改接口 | `canApprove` 同上;字段零变化 |
## 三、接口详情
### 1. GB-ADM-062 同意流团 `POST /v3/admin/order/group-batch/disband/{approvalId}/approve`
**VO**: `DisbandApprovalRespVO`
#### 使用场景
团期审批中心「流团」行或流团详情页点「同意」。同意后才真正执行流团:团期置 CANCELLED,全团子订单取消,已付户按已付全额建退款单,释放配车占用。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| approvalId | path | Long | 是 | 正整数,流团申请单 ID | 不存在返 589545 |
| remark | body | String | 否 | ≤ 512 字 | 批复备注;整个 body 可省略 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| approvalStatus | String | 同意后为 `APPROVED` |
| approvedById / approvedByName | Long / String | 本次审批人;**团期管理员现在也会出现在这里** |
| estimatedRefundAmount | BigDecimal | 提交时的预估退款合计(既有字段,未改) |
| actualRefundAmount | BigDecimal | 实际进退款的合计,由对账定稿(既有字段,未改)。**现在是「已建退款单、待二审」的金额,不是已退出的钱** |
| canApprove | Boolean | 已处理后恒为 false |
| 其余字段 | — | 与流团详情相同,未改 |
#### 请求示例
```http
POST /v3/admin/order/group-batch/disband/2105977953445969921/approve HTTP/1.1
Host: api.test.1814.love
Authorization: Bearer <团期管理员 token>
Content-Type: application/json
{"remark": "确认无法成团"}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"approvalId": "2105977953445969921",
"groupBatchId": "2105974855696547842",
"batchNo": "T26-7048",
"batchName": "11月26日海拉尔-额尔古纳4日团",
"approvalStatus": "APPROVED",
"approvalStatusName": "已通过",
"reason": "临近出团报名不足 6 户,无法成团",
"affectedOrderCount": 2,
"participantCount": 4,
"estimatedRefundAmount": 3360.0,
"actualRefundAmount": 3360.0,
"applicantId": "2102259564525301761",
"applicantName": "gbm8154test",
"approvedById": "2102259564525301761",
"approvedByName": "gbm8154test",
"approveRemark": "确认无法成团",
"canApprove": false
}
}
```
#### 空数据 / 降级响应
团里没有已付户时不建任何退款单,`actualRefundAmount` 为 `0.00`。退款单建单失败的户不计入 `actualRefundAmount`,由对账 Job 兜底补建;接口本身仍返回成功(流团已提交,不回滚)。
```json
{
"code": 200,
"message": "成功",
"data": {
"approvalStatus": "APPROVED",
"affectedOrderCount": 1,
"estimatedRefundAmount": 0.0,
"actualRefundAmount": 0.0,
"canApprove": false
}
}
```
#### 错误响应
```json
{
"code": 589547,
"message": "仅管理员可处理流团审批",
"data": null
}
```
| 错误码 | 触发条件 |
|---|---|
| 589547 | 当前角色不是管理员 / 超级管理员 / 团期管理员(如定制师、财务);或有请求但网关未透传角色;或团期管理员放行开关已关 |
| 589545 | 流团申请不存在 |
| 589546 | 申请已被处理(并发时后到者) |
| 589544 | 团期已确认或更靠后,不可批复流团 |
#### 业务边界
- 团期管理员按「任一团期管理员」放行,系统里还没有「本团团期管理员」关系,与 #8436 退单一致。
- 不禁止自审:团期管理员可以批自己提交的申请;`applicant*` 与 `approvedBy*` 分开落库。
- 批后各户退款单为 `PENDING`,`calculatedAmount` = 该户已付全额(流团按已付全额退,不扣违约金),审核人在退款审批中心不填金额直接同意即按此额退。
- 退款审核端点 `POST /v3/admin/refund/review` 仍禁止团期管理员(581008),所以团期管理员批了流团也退不出钱,钱的出口由退款审核人把关。
### 2. GB-ADM-062 拒绝流团 `POST /v3/admin/order/group-batch/disband/{approvalId}/reject`
**VO**: `DisbandApprovalRespVO`
#### 使用场景
团期审批中心「流团」行或流团详情页点「拒绝」。只改申请单状态,团期继续正常招募,订单与钱一律不动。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| approvalId | path | Long | 是 | 正整数,流团申请单 ID | 不存在返 589545 |
| remark | body | String | 是 | 非空白,≤ 512 字 | 拒绝原因,写进团期时间线 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| approvalStatus | String | 拒绝后为 `REJECTED` |
| approvedById / approvedByName | Long / String | 本次审批人;团期管理员现在也会出现在这里 |
| approveRemark | String | 拒绝原因 |
| canApprove | Boolean | 已处理后恒为 false |
#### 请求示例
```http
POST /v3/admin/order/group-batch/disband/2105980238163030017/reject HTTP/1.1
Host: api.test.1814.love
Authorization: Bearer <团期管理员 token>
Content-Type: application/json
{"remark": "本周还有两户在咨询,继续招募到下周一再定"}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"approvalId": "2105980238163030017",
"groupBatchId": "2105980235310923777",
"batchNo": "T26-9826",
"approvalStatus": "REJECTED",
"approvalStatusName": "已拒绝",
"approvedByName": "gbm8154test",
"approveRemark": "本周还有两户在咨询,继续招募到下周一再定",
"canApprove": false
}
}
```
#### 空数据 / 降级响应
无降级分支:拒绝只写申请单一行和一条时间线,失败即整体回滚并返回错误码。
```json
{
"code": 589546,
"message": "该流团申请已处理,不可重复操作",
"data": null
}
```
#### 错误响应
```json
{
"code": 589547,
"message": "仅管理员可处理流团审批",
"data": null
}
```
| 错误码 | 触发条件 |
|---|---|
| 589547 | 同「同意流团」 |
| 589545 | 流团申请不存在 |
| 589546 | 申请已被处理 |
| 400 | `remark` 为空或超长 |
#### 业务边界
- 拒绝后同一团期可以再次提交流团。提交接口有防重窗口,拒绝后立刻重提会返回 100502「请勿重复提交」,隔几秒再提即可(既有行为,本次未改)。
- 审批人范围与「同意流团」完全一致,共用同一道角色门。
### 3. GB-ADM-061 团期审批中心列表 `GET /v3/admin/order/group-batch/approvals/page`
**VO**: `GroupBatchApprovalItemRespVO`
#### 使用场景
团期审批中心页的统一列表(流团 + 退单两类)。前端按行的 `canApprove` 决定是否显示「同意 / 拒绝」。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| bizType | query | String | 否 | `DISBAND` / `WITHDRAW` | 业务类型 |
| approvalStatus | query | String | 否 | `PENDING` / `APPROVED` / `REJECTED` / `CANCELLED` | 审批状态 |
| groupBatchId | query | Long | 否 | 正整数 | 按团期筛 |
| batchName | query | String | 否 | — | 团期名称关键字 |
| pageNum | query | Integer | 否 | ≥ 1,默认 1 | 页码 |
| pageSize | query | Integer | 否 | 默认 20 | 每页条数 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| records[].canApprove | Boolean | **本次取值变化**:流团行 = 待审批 + 团期仍可流团 + 当前人过 GB-ADM-062 的角色门。团期管理员在待审批流团行上由 false 变 true;退单行口径不变 |
| records[].approvedByName | String | 审批人,团期管理员现在也会出现 |
| 其余字段 | — | 未改 |
#### 请求示例
```http
GET /v3/admin/order/group-batch/approvals/page?pageNum=1&pageSize=20&bizType=DISBAND&approvalStatus=PENDING HTTP/1.1
Host: api.test.1814.love
Authorization: Bearer <团期管理员 token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"approvalId": "2105977252741349378",
"bizType": "DISBAND",
"bizTypeName": "流团",
"groupBatchId": "2105974855696547842",
"batchNo": "T26-7048",
"batchName": "11月26日海拉尔-额尔古纳4日团",
"approvalStatus": "PENDING",
"approvalStatusName": "待审批",
"affectedOrderCount": 2,
"participantCount": 4,
"estimatedRefundAmount": 3360.0,
"reason": "临近出团报名不足 6 户,无法成团",
"applicantName": "gbm8154test",
"canApprove": true
}
],
"total": 1,
"page": 1,
"pageSize": 20
}
}
```
#### 空数据 / 降级响应
没有匹配的申请时返回空页。
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [],
"total": 0,
"page": 1,
"pageSize": 20
}
}
```
#### 错误响应
```json
{
"code": 589507,
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
"data": null
}
```
| 错误码 | 触发条件 |
|---|---|
| 589507 | 无 `group-batch:view` 权限码(读端点判权码,与审批角色门无关,本次未改) |
#### 业务边界
- `canApprove` 整页只算一次,与行数无关。
- 定制师、财务等角色在流团行上仍为 false;管理员 / 超级管理员仍为 true。
- 团期管理员放行开关关掉时,团期管理员在流团行上回到 false。
### 4. GB-ADM-061 流团申请详情 `GET /v3/admin/order/group-batch/disband/{approvalId}`
**VO**: `DisbandApprovalRespVO`
#### 使用场景
审批中心点进流团详情;详情页的「同意 / 拒绝」按钮按 `canApprove` 显隐。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| approvalId | path | Long | 是 | 正整数,流团申请单 ID | 不存在返 589545 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| canApprove | Boolean | **本次取值变化**,口径同列表:待审批 + 团期仍可流团 + 过角色门。团期管理员由 false 变 true |
| 其余字段 | — | 未改 |
#### 请求示例
```http
GET /v3/admin/order/group-batch/disband/2105980238163030017 HTTP/1.1
Host: api.test.1814.love
Authorization: Bearer <团期管理员 token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"approvalId": "2105980238163030017",
"groupBatchId": "2105980235310923777",
"batchNo": "T26-9826",
"batchName": "12月10日海拉尔-额尔古纳4日团",
"approvalStatus": "PENDING",
"approvalStatusName": "待审批",
"reason": "临近出团报名不足 6 户,无法成团",
"affectedOrderCount": 2,
"participantCount": 4,
"estimatedRefundAmount": 3360.0,
"applicantName": "gbm8154test",
"canApprove": true
}
}
```
#### 空数据 / 降级响应
申请已处理(通过 / 拒绝 / 撤销)或团期已不可流团时,`canApprove` 为 false,其余字段照常返回。
```json
{
"code": 200,
"message": "成功",
"data": {
"approvalId": "2105980238163030017",
"approvalStatus": "REJECTED",
"approvalStatusName": "已拒绝",
"canApprove": false
}
}
```
#### 错误响应
```json
{
"code": 589545,
"message": "流团申请不存在",
"data": null
}
```
| 错误码 | 触发条件 |
|---|---|
| 589545 | 申请不存在 |
| 589507 | 无 `group-batch:view` 权限码 |
#### 业务边界
- `canApprove` 与 GB-ADM-062 走同一个判定方法,不会出现「显示了按钮、点下去 589547」。
- 缺角色时会打一条 `GB_APPROVAL_ROLE_CLAIM_MISSING` WARN,`canApprove` 返回 false。
## 四、契约约束与正确调用方式
- 前端按 `canApprove` 显隐「同意 / 拒绝」,**不要按角色自己判**:团期管理员能否审批受 nacos 开关控制,角色相同、结果可能不同。
- 同意流团的响应里 `actualRefundAmount` 现在表示「已建退款单、待二审」的合计,不代表钱已退出。要看每户退款进度,查退款审批中心(`GET /v3/admin/refund/application/page`)或订单退款列表。
- 退款审批中心会多出流团产生的 `PENDING` 退款单,申请人类型为 `SYSTEM`,原因文案是流团原因。审核人可直接同意(按 `calculatedAmount` 即已付全额退),也可改额或拒绝。
- 团期管理员不能审核退款(581008),流团退款的二审要由财务 / 管理员等有退款审核权的人处理。
## 五、数据库行为
- **零 schema 变更**,无 Flyway 迁移。
- 同意流团的写入集合不变:申请单、团期、子订单、时间线、配车释放 outbox 照旧;退款单照旧由取消事件建出。
- 唯一变化是退款单的落库状态:改前建单后同一流程里自动写一条 `refund_review`(审核人 = 流团审批人)并把 `refund_application.status` 推到 `APPROVED`;改后只建 `PENDING` 单,`refund_review` 零行、`reviewed_at` 为空,等退款审批中心处理。
- 流团退款对账建单时写入的 `calculated_amount` 由「按退款政策算」改为「该户已付全额」,与通用建单路径写同一个数。
- 拒绝流团的写入不变:只改申请单一行 + 一条时间线。
## 六、边界行为
- 两个 nacos 开关,默认都开:
- `group-batch.disband.refund-second-review`:关掉时,退款回到改前自动放行,每次同意都打 `GB_DISBAND_REFUND_SECOND_REVIEW_OFF` WARN;**同时收回团期管理员的流团审批权**(否则团期管理员一个人就能把整团的钱自动退出去)。
- `group-batch.acl.allow.disband-approver-group-batch-manager`:关掉时只收回团期管理员的流团审批权,二审保留;团期管理员被拒时打 `GB_DISBAND_APPROVER_GBM_DISABLED` WARN。
- 流团这对开关与退单(#8436)那对开关互不牵动,两类审批分别回滚。
- 开关取不到(容器未绑定)时按更严处理:不放团期管理员。
- 未付户随流团取消,不建退款单(既有行为)。
## 六.6、修改前后对比
| 场景 | 改前 | 改后 |
|---|---|---|
| 团期管理员同意 / 拒绝流团 | 589547 | 成功,审批人记为团期管理员 |
| 定制师 / 财务同意 / 拒绝流团 | 589547 | 589547(不变) |
| 管理员同意 / 拒绝流团 | 成功 | 成功(不变) |
| 同意流团后已付户的退款单 | `APPROVED`,带 1 条审核记录(审核人 = 流团审批人) | `PENDING`,0 条审核记录,`calculated_amount` = 已付全额 |
| 审批中心 / 流团详情 `canApprove`(团期管理员,待审批流团) | false | true |
| 审批中心 / 流团详情 `canApprove`(定制师) | false | false(不变) |
## 六.7、影响评估
- **前端**:零改动。审批中心页已对团期管理员开放,按钮按 `canApprove` 显隐,后端放开后按钮自动出现。
- **财务 / 退款审核人**:退款审批中心多出流团退款单,需要人工审核后才会退款,流团退款到账时间会因此延后。这是本次的目的。
- **回滚**:改 nacos 开关即可,不需要发版。
## 七、不影响范围
- 发起流团 GB-ADM-060:判权(`group-batch:manage` 权限码)与行为不变。
- 退单审批 GB-ADM-072 ~ 075:判定、开关、二审都不变,#8436 的那对开关不受本次影响。
- 退款审核端点 `POST /v3/admin/refund/review`:仍禁止团期管理员。
- 单笔取消订单、C 端申请退款等其他建退款单的路径:不变。
## 八、测试环境已验证
部署:dev-v3 @ `c4a410a94`,2026-10-02 19:01 部署 hl-order-service-v3。运行字节探针在 8086 / 8186 两个实例上都命中本次新增的 `GB_DISBAND_APPROVER_GBM_DISABLED` 与 `GB_DISBAND_REFUND_SECOND_REVIEW_OFF`;进程 jar 与磁盘 jar 为同一 inode。
判权一律用低权限角色声明取证:团期管理员用 TEST 上当前角色即团期管理员的 `gbm8154test`;不用超级管理员。
| 场景 | 改前(`1f8b1dacd`,团期 `T26-0659`) | 改后(`c4a410a94`,团期 `T26-7048` / `T26-3437` / `T26-9826`) |
|---|---|---|
| 团期管理员同意 / 拒绝真实待审批流团 | 589547 / 589547 | 拒绝成功(`T26-7048` 申请 `2105977252741349378`);同意成功(申请 `2105977953445969921`) |
| 定制师同意 / 拒绝 | 589547 / 589547 | 589547 / 589547,申请单仍 PENDING |
| 管理员同意 | 成功 | 成功(申请 `2105977508052828161`) |
| 已付户退款单(全款 3360.00) | `APPROVED`,`refund_review` 1 行 | 团期管理员批、管理员批两例都是 `PENDING`,`calculated_amount=3360.00`,`refund_review` 0 行,`reviewed_at` 为空 |
| 退款审批中心 PENDING 列表 | — | 两张流团退款单都在列 |
| 审批中心 `canApprove`(团期管理员 / 定制师) | false / — | true / false |
| 流团详情 `canApprove`(团期管理员 / 定制师 / 管理员) | — | true / false / true;拒绝后 false |
| 团期管理员调退款审核端点 | — | 581008,钱的出口仍挡住团期管理员 |
全部调用 HTTP 200;审批单 `actual_refund_amount` 两例都定稿为 3360.00。
## 十、相关文档
- 工单:wx/HL#8246
- PR:wx/HL#8734(merge commit `c4a410a94`)
- 参照:#8436 退单审批放开团期管理员 + 退款二审
- 代码:`WithdrawApprovalGuard#assertDisbandApproverRole`、`GroupBatchDisbandApprovalService#approveInTx`、`GroupBatchAclToggle`
## 关联 / 联系人
- 后端:jw
- 前端:无需改动(hl-ui v2.1 已按 `canApprove` 显隐)
- 关联工单:#8436、#8253、#7294、#7609
@@ -0,0 +1,520 @@
---
schema: "hl-changelog/v2"
ticket: "8659"
title: "房务价格日历与库存口径统一:候选页与控房表增加日历状态,扣减拒绝按原因分三码"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: ""
updated_at: "2026-10-02"
base: "dev-v3"
---
# 房务配房接口调整:价格日历与库存口径统一、扣减失败分码
> **存放目录**: 二期 → `changelogs-v2/2026-10/`
>
> **服务**: hl-order-service-v3、hl-resource-service
> **Issue**: #8659
> **日期**: 2026-10-02
> **影响范围**: 管理后台房务配房流程,涉及候选酒店页、控房表、配房失败反馈
---
## ⚠️ 关键变化
- **候选酒店页 `inventoryStatus` 扩大语义**:日历已售罄(`SOLD_OUT`)映射为 `FULL`,日历已关闭(`CLOSED`)或无日历行映射为 `CLOSED`;非 `AVAILABLE` 时 `available=0`、`unlimited=false`(即使库存数本身大于 0)。**前端无需改代码**:经 hl-ui v2.1 核实,候选弹窗(`PickHotelModal.vue`)已按 `inventoryStatus` 三值禁用非 `AVAILABLE` 选项、显示「满房」「已关」,且取值用 `pickFirst` 读取不会被 `available=0` 误判为缺省继续读旧的 `stock`。
- **控房表新增 `calendarStatus` / `calendarStatusName`**:价格日历状态原值与中文名。纯新增字段,不读不影响现有解析;**前端需在控房表加一列展示 `calendarStatusName`**,房务才能在剩余数大于 0 时看出该格「已关闭」或「已售罄」、扣减必被拒(这是本次唯一的前端动作)。
- **扣减失败时按原因分三种错误码**(原来统一报 808901):
- `808906`「该日该房型未配置价格日历」(新增)
- `808907`「该日该房型已关闭售卖或已售罄(状态:{0})」(新增,`{0}` 为状态中文名)
- `808901`「房型库存不足」(文案收窄,现仅表示日历可售但库存不够;旧文案与此不同,见下)
- 以上均由前端通用拦截器按 `message` 自动弹出,调用方无需按 `code` 分支即可正确展示。
---
## 一、背景
资源侧价格日历状态(可售 `AVAILABLE` / 已售罄 `SOLD_OUT` / 已关闭 `CLOSED`)与库存数是两个独立维度。读侧(候选页、控房表)曾只看库存、不读状态,与写侧(库存扣减按 `status=AVAILABLE` 谓词)产生口径断裂:房务在候选页看到「可订」而提交配房被拒,甚至产生"该加房量"的错误判断,实际原因是日历已关房。本次统一口径:读侧添加日历状态、扣减时按真实原因分码。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 候选酒店页 | GET | `/v3/admin/hotel-candidates` | 修改 | roomTypes[].inventoryStatus 取值语义扩大 |
| 2 | 控房表 | GET | `/v3/admin/order/house-console/room-control` | 修改 | 新增 calendarStatus / calendarStatusName |
| 3 | 逐晚提交配房 | POST | `/v3/admin/order/hotel-requirements/{requirementId}/assignments` | 修改 | 扣减拒绝返回 808906 / 808907 / 808901(原统一 808901) |
---
## 三、接口详情
本节示例 JSON 的字段名、类型、错误码与文案逐一取自源码;ID、日期等取值为说明用的构造值。所有接口统一返回 `Result` 信封(`code` / `message` / `data` / `traceId` / `success`),示例省略 `traceId`;业务失败与入参校验失败均为 HTTP 200,靠 `code` 区分。
### 1. 候选酒店页 `GET /v3/admin/hotel-candidates`
**VO**: `HotelCandidateQueryReqVO → HotelCandidateRespVO`
#### 使用场景
房务/定制师为某个订单的某一晚选酒店时查看候选酒店列表及房型库存。本次改动只影响候选房型 `roomTypes[].inventoryStatus`(及联动的 `available`/`unlimited`)的取值口径,请求参数与响应结构均未变。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Query | Long | ✅ | - | 订单 ID |
| dayNumber | Query | Integer | ❌ | ≥1 | 第几天(从 1 开始,`stayDate = departDate + dayNumber - 1`),与 stayDate 二选一,直接传 stayDate 优先 |
| stayDate | Query | LocalDate | ❌ | - | 入住日期;直接指定优先于 dayNumber 推算 |
| city | Query | String | ❌ | - | 城市代码 |
| keyword | Query | String | ❌ | - | 关键词非空时突破单城限定,跨城/省按酒店名/城市/省份/地址匹配;为空维持单城行为 |
| limit | Query | Integer | ❌ | 1~50,默认 30 | 返回候选条数上限,池内/定制师优先排序后取前 N |
| roomCategory | Query | String | ❌ | 字典 room_category | 非空白时参与房型过滤,matched 房型只在同大类内选取 |
| roomCount | Query | Integer | ❌ | ≥1 | 需要的房间数;不传退化为 available>0 的旧行为 |
| preferredHotelId | Query | Long | ❌ | - | 定制师指定的优先酒店 ID |
| requirementId | Query | Long | ❌ | - | 住宿需求 ID;传入后该需求 days JSON 里所有 hotelId 作为定制师指定 |
#### 出参 `Result<HotelCandidateRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| stayDate | LocalDate | 入住日期 |
| city | String | 城市代码 |
| productType | String | 产品类型(CORE/GROUP/CUSTOM,响应专有字段,无同名入参) |
| candidates[] | List<Candidate> | 候选酒店列表 |
| candidates[].roomTypes[] | List<RoomTypeOption> | 该酒店当日真实房型列表 |
| candidates[].roomTypes[].available | Integer | **语义变化**。`unlimited=true` 时为 NULL(表不限);否则为当日可用房数。`inventoryStatus` 非 `AVAILABLE` 时恒为 `0`(即使库里 stock 列大于 0) |
| candidates[].roomTypes[].unlimited | Boolean | **语义变化**。resource 端 stock 列为 NULL 时才为 `true`;`inventoryStatus` 非 `AVAILABLE` 时恒为 `false`(工单 #8659 前,日历已关闭/售罄但 stock=NULL 的房型也会显示「不限」,现改为显示不可订) |
| candidates[].roomTypes[].inventoryStatus | String | **修改**。`AVAILABLE`=日历可售且有余量或不限库存;`FULL`=日历可售但余量为 0 或日历已售罄;`CLOSED`=未配置价格日历、日历已关闭或日历状态字典外脏值(工单 #8659)。`FULL`/`CLOSED` 时价格字段(protocolPrice/settlementPrice/basePrice)照常返回,只有库存字段归零 |
#### 请求示例
```http
GET /v3/admin/hotel-candidates?orderId=100001&dayNumber=1&city=hailar&roomCount=2
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"stayDate": "2026-10-05",
"city": "hailar",
"productType": "CORE",
"candidates": [
{
"hotelId": "200001",
"hotelName": "海拉尔假日酒店",
"roomTypes": [
{
"roomTypeId": "300001",
"name": "豪华大床房",
"roomCategory": "KING",
"available": 7,
"unlimited": false,
"stock": 7,
"protocolPrice": "328.00",
"inventoryStatus": "AVAILABLE"
},
{
"roomTypeId": "300002",
"name": "标准双床房",
"roomCategory": "TWIN",
"available": 0,
"unlimited": false,
"stock": 5,
"protocolPrice": "280.00",
"inventoryStatus": "CLOSED"
}
]
}
]
},
"success": true
}
```
上例第二个房型 `stock=5`(库里仍有余量)但日历已关闭,`inventoryStatus=CLOSED`、`available` 归零——这正是本次改动要修的口径断裂:改前 `available` 会原样显示库里的 5。
#### 空数据 / 降级响应
- 该订单该日无酒店或筛选无匹配:`candidates=[]`。
- 日历状态字典外脏值按 `CLOSED` 处理(不单列「异常」状态)。
#### 错误响应
```json
{
"code": 400,
"message": "orderId 不能为空",
"data": null,
"success": false
}
```
#### 业务边界
- `inventoryStatus` 判定日历状态优先于库存数:日历 `CLOSED` 或 `SOLD_OUT` 时,即使 resource 库存列数字大于 0,`available`/`unlimited` 仍归零,不能据库存字段反推是否可订。
- `limit` 默认 30、上限 50,候选列表本身是截断后的结果,不代表该城市全部酒店。
### 2. 控房表 `GET /v3/admin/order/house-console/room-control`
**VO**: `HouseRoomControlListReqVO → HouseRoomControlRespVO`
#### 使用场景
房务在房务控制台查看某个城市(或某酒店)、某日期区间的库存占用详情及团组用房明细。本次新增 `calendarStatus` / `calendarStatusName` 两个字段;前端在控房表加一列展示 `calendarStatusName` 后,房务无需逐行点开即可看到日历状态。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| cityCode | Query | String | ❌ | ≤32 字 | 城市编码;与 hotelId 都不传则查全部酒店 |
| hotelId | Query | Long | ❌ | - | 酒店 ID;传了则只查这一家,优先于 cityCode |
| dateFrom | Query | LocalDate | ✅ | - | 入住夜起(含) |
| dateTo | Query | LocalDate | ✅ | 与 dateFrom 跨度 ≤62 天 | 入住夜止(含) |
| onlyWithRemain | Query | Boolean | ❌ | - | true 时只返回剩余为不限或 >0 的行 |
#### 出参 `Result<HouseRoomControlRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| stockTrackingEnabled | Boolean | resource 全局库存追踪开关;false 时行照常返回、已用取实际值,但调房量会被拒(808312) |
| rows[] | List<Row> | 按(酒店, 入住夜, 房型)升序 |
| rows[].hotelId / hotelName / cityName | - | 酒店 ID / 酒店名 / 城市中文名 |
| rows[].roomTypeId / roomTypeName | - | 房型 ID / 房型名 |
| rows[].stayDate | LocalDate | 入住夜 |
| rows[].totalRooms | Integer | 控房总数 = 剩余 + 已用;NULL 表不限量 |
| rows[].usedRooms | Integer | 已用(resource stock_used;库存开关关闭时仍取实际值) |
| rows[].remainRooms | Integer | 剩余(resource stock);NULL 表不限量 |
| rows[].assignedRooms | Integer | 已分配:扣库存的配房行 + 已确认扣库存的团期计划行间数合计 |
| rows[].protocolPrice / settlementPrice | BigDecimal(字符串) | 控房价(协议价)/ 结算价 |
| rows[].calendarStatus | String | **新增**。价格日历状态原值:`AVAILABLE / SOLD_OUT / CLOSED`,字典外历史值原样返回;该格无日历行时为 NULL。非 `AVAILABLE` 时扣减必被拒,与剩余数无关(工单 #8659) |
| rows[].calendarStatusName | String | **新增**。价格日历状态中文名:可售 / 已售罄 / 已关闭;字典外值显示「状态异常(原值)」;无日历行时为 NULL |
| rows[].usages[] | List<Usage> | 团组用房明细(散客配房行或团期计划行,本表只列扣库存行) |
| rows[].usages[].orderId / teamNo / batchNo | - | 订单 ID(团期计划行为 NULL)/ 团号 / 团期批次号(散客订单为 NULL) |
| rows[].usages[].roomCount / roomSource / roomSourceLabel / confirmStatusLabel | - | 用房间数 / 房源(STOCK)/ 房源标签 / 确认状态标签 |
#### 请求示例
```http
GET /v3/admin/order/house-console/room-control?cityCode=hailar&dateFrom=2026-10-01&dateTo=2026-10-31
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"stockTrackingEnabled": true,
"rows": [
{
"hotelId": "100001",
"hotelName": "海拉尔草原酒店",
"cityName": "海拉尔",
"roomTypeId": "300001",
"roomTypeName": "豪华双床房",
"stayDate": "2026-10-01",
"totalRooms": 12,
"usedRooms": 5,
"remainRooms": 7,
"assignedRooms": 5,
"protocolPrice": "320.00",
"settlementPrice": "300.00",
"calendarStatus": "AVAILABLE",
"calendarStatusName": "可售",
"usages": [
{
"orderId": "1930000000000000001",
"teamNo": "HL20261001A",
"batchNo": null,
"roomCount": 3,
"roomSource": "STOCK",
"roomSourceLabel": "控房",
"confirmStatusLabel": "已确认"
}
]
},
{
"hotelId": "100002",
"hotelName": "海拉尔雅园宾馆",
"cityName": "海拉尔",
"roomTypeId": "300005",
"roomTypeName": "标准大床房",
"stayDate": "2026-10-01",
"totalRooms": 8,
"usedRooms": 2,
"remainRooms": 6,
"assignedRooms": 3,
"protocolPrice": "280.00",
"settlementPrice": "260.00",
"calendarStatus": "CLOSED",
"calendarStatusName": "已关闭",
"usages": []
}
]
},
"success": true
}
```
#### 空数据 / 降级响应
- 区间内无库存数据:`rows=[]`,`stockTrackingEnabled` 仍照常返回开关实际值。
- 该格无日历行时 `calendarStatus`/`calendarStatusName` 为 NULL(不是「状态异常」,无行与脏值是两种不同情况)。
#### 错误响应
```json
{
"code": 400,
"message": "dateFrom 不能为空",
"data": null,
"success": false
}
```
日期跨度超 62 天或起止颠倒不走参数校验码,由 Manager 本地校验后返业务码 808313。
#### 业务边界
- `calendarStatus=CLOSED` 或 `SOLD_OUT` 时,`remainRooms` 数字再大也**无法扣减**(扣减 UPDATE 只认 `status=AVAILABLE`);房务需在此处调整日历或释放库存,不能指望后续配房流程放行。
- 该表不隐藏 `remainRooms`,即使日历已关闭仍显示实际 stock,控房表目的是查看与调整库存,不把实际数字藏起来。
- `stockTrackingEnabled=false` 时各行仍按实际值返回 `usedRooms`/`remainRooms`,但调房量会被拒(808312),不能据此误判为"关闭追踪=不限量"。
### 3. 逐晚提交配房 `POST /v3/admin/order/hotel-requirements/{requirementId}/assignments`
**VO**: `AssignmentSubmitReqVO → AssignmentSubmitRespVO`
#### 使用场景
房务为一个住宿需求批量提交配房方案(一次可提交多晚 × 多组房间)。本次修改:扣减失败时根据真实原因(日历无行、日历不可售、库存不足)返回不同错误码,不再统一报 808901。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| requirementId | Path | Long | ✅ | - | 住宿需求 ID |
| items | Body | List<AssignmentItemReqVO> | ✅ | 非空 | 配房项列表(批量) |
| items[].dayNumber | Body | Integer | ✅ | ≥1 | 第几天(Day1=1) |
| items[].hotelId | Body | Long | ✅ | - | 酒店 ID |
| items[].roomTypeId | Body | Long | ✅ | - | 房型 ID |
| items[].roomCategory | Body | String | ✅ | 字典 room_category | 房型字典 code(如 STANDARD) |
| items[].roomCount | Body | Integer | ✅ | ≥1 | 间数 |
| items[].deductInventory | Body | Boolean | ✅ | - | 是否扣减资源酒店房型库存;true=占用系统库存,false=仅保存配房快照不扣库存 |
| items[].protoPrice | Body | BigDecimal | ❌ | ≥0 | 协议价快照;不传按所选房型当日资源协议价兜底 |
| items[].settlementPrice | Body | BigDecimal | ❌ | ≥0 | 结算价快照;不传按资源结算价兜底,再兜底协议价 |
| items[].settleType | Body | String | ❌ | cash\|sign\|company | 支付方式快照;不传取酒店资源配置 |
| items[].breakfast | Body | String | ❌ | INCLUDED/EXCLUDED/PENDING | 早餐;不传按待确认存空 |
| items[].syncProtocolPrice / syncSettlementPrice / syncSettleType | Body | Boolean | ❌ | - | 是否把本项对应快照同步写回 resource;默认 false 不同步 |
| items[].remark | Body | String | ❌ | - | 备注 |
| items[].replaceReason | Body | String | ❌ | ≤256 字 | 替换原因;仅该天已有旧行被本项替换时落库 |
#### 出参 `Result<AssignmentSubmitRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| successCount | Integer | 成功条数 |
| failCount | Integer | 失败条数。当前实现恒为 `0`——扣减失败走整笔拒绝(见下),不会出现"部分成功部分失败"的响应 |
| items[] | List<Item> | 每条配房结果 |
| items[].dayNumber | Integer | 第几天 |
| items[].assignmentId | Long(JSON 字符串) | 配房 ID |
| items[].arrange | String | 配房状态,取值仅 `inquiring`(询价中)/ `confirmed`(已确认) |
| items[].deductInventory | Boolean | 本次配房是否扣减了库存 |
#### 请求示例
```json
{
"items": [
{
"dayNumber": 1,
"hotelId": 100001,
"roomTypeId": 300001,
"roomCategory": "STANDARD",
"roomCount": 2,
"deductInventory": true
}
]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"successCount": 1,
"failCount": 0,
"items": [
{
"dayNumber": 1,
"assignmentId": "1970000000000000001",
"arrange": "inquiring",
"deductInventory": true
}
]
},
"success": true
}
```
#### 空数据 / 降级响应
无特殊降级。批量提交中任意一项触发库存扣减失败(808906/808907/808901)会导致**整次提交被拒绝**,不落库、不产生部分成功的配房记录;需要调整后整批重新提交。
#### 错误响应
```json
{
"code": 808906,
"message": "该日该房型未配置价格日历",
"data": null,
"success": false
}
```
其他错误:
| code | message | 触发 |
|------|---------|------|
| 808907 | 该日该房型已关闭售卖或已售罄(状态:{0}) | 日历 status 为 CLOSED 或 SOLD_OUT,`{0}` 为状态中文名 |
| 808901 | 房型库存不足 | 日历 status=AVAILABLE 但剩余数不够 |
| 808090 | 未登录或非房务角色,无权操作 | 非房务角色 |
| 808116 | 订单未抢单, 请先抢单再配房 | 需求无人持有(源码原文逗号为半角) |
| 808110 | 需求不属于当前用户 | 当前用户不是持有人(超管同样拒绝) |
#### 业务边界
- `deductInventory=true` 的项触发库存扣减,三种拒绝原因分别报 808906 / 808907 / 808901;批量提交中任一项被拒,整次提交失败,不做部分落库。
- `deductInventory=false` 的项不做库存校验,正常返回 200。
- `roomCategory` 为必填字段,传空或不传直接触发参数校验失败(`code=400`,HTTP 状态仍为 200)。
- 防重 3 秒,同一需求 3 秒内重复提交被拦截。
---
## 四、契约约束与正确调用方式
**通俗说法**:配房前看候选页或控房表,若日历状态显示「已关闭」或「已售罄」,不能提交配房;提交时若被拒,先按错误码判断原因——库存不足就加库存,日历问题就调日历。
### 示例对比
| 场景 | 旧行为 | 新行为 |
|------|--------|--------|
| 日历已关闭、stock=20 | 候选页显示可订、余 20 间;提交配房返 808901「库存不足」(误导) | 候选页 inventoryStatus=CLOSED、available=0;提交配房返 808907「已关闭」(准确);控房表 calendarStatus=CLOSED 可直观查看 |
| 日历无行 | 候选页显示可订或不显示(看评分);提交被拒 808901 | 候选页 inventoryStatus=CLOSED、available=0;提交配房返 808906「未配置日历」(准确指示资源侧缺陷) |
| 日历可售、stock=0 | 候选页显示 FULL、available=0;提交被拒 808901 | 候选页显示 FULL、available=0;提交被拒 808901(同前) |
---
## 五、数据库行为
扣减仍走库存表 UPDATE 逻辑,本次仅改返回码与日历状态展示,无 DDL 或表结构变化。
---
## 六、边界行为
- 错误码 808906 / 808907 / 808901 的优先级:先判日历有无(808906)→再判日历状态(808907)→最后判库存(808901)。
- 控房表的 `remainRooms` 与日历状态 `CLOSED` 同时出现时,表示日历被关了但库存数还在,房务调整库存须先对日历解冻(不归房务接口管)。
---
## 六.5 枚举
### inventoryStatus(HotelCandidateRespVO.RoomTypeOption)
**类型**: `String`
| 值 | 条件 | 前端表现 |
|----|------|--------|
| `AVAILABLE` | 日历 status=AVAILABLE 且库存>0(或 NULL 不限) | 可订、显示余量 |
| `FULL` | 日历 status=AVAILABLE 但库存=0,或日历 status=SOLD_OUT | 满房、显示「已满」 |
| `CLOSED` | 日历 status=CLOSED 或无日历行 | 关闭、显示「不可订」 |
### calendarStatus(HouseRoomControlRowRespVO)
**类型**: `String`
| 值 | 说明 |
|----|------|
| `AVAILABLE` | 可售 |
| `SOLD_OUT` | 已售罄 |
| `CLOSED` | 已关闭 |
| NULL | 无日历行 |
| 其他 | 字典外的历史脏数据(原样返回) |
---
## 六.6、修改前后对比
| 项 | 改前 | 改后 |
|----|------|------|
| 候选页 inventoryStatus 逻辑 | 仅看库存数;SOLD_OUT→FULL、CLOSED→CLOSED(但都显示有库存或空库存) | 优先看日历状态;SOLD_OUT→FULL、CLOSED→CLOSED(且 available=0) |
| 控房表字段 | calendarStatus / calendarStatusName 无 | **新增**,展示日历原值与中文名 |
| 扣减失败错误码 | 统一 808901(三种原因混合) | 分码:808906(无日历)、808907(日历不可售)、808901(库存不足) |
---
## 六.7、影响评估
- **前端改动只有一处:控房表加「日历状态」列**(hl-ui v2.1 实查结论):
- 候选弹窗 `src/views/housekeeper/components/PickHotelModal.vue` 已按 `inventoryStatus` 三值分支渲染禁用态与文案(`disabled: !rt.unlimited && rt.inventoryStatus !== 'AVAILABLE'`,CLOSED 显示「已关」/FULL 显示「满房」),取库存数用 `pickFirst()` 辅助函数正确处理 `available=0` 而不误判为缺省继续回退读旧字段,三值语义收紧不影响该组件现有行为。
- 控房表新增的 `calendarStatus`/`calendarStatusName`/`usages[]` 均为纯新增字段,hl-ui v2.1 当前对该查询结果的消费代码未读取这些字段名,新增不影响现有解析;**需要新增一列展示 `calendarStatusName`**(为 NULL 时显示「—」),否则剩余数大于 0 而日历已关闭的格子在页面上看不出来。
- 配房提交的错误码拆分(808906/808907/808901)均由 `src/api/housekeeper/assignment.js` 所在模块走通用拦截器按 `message` 弹窗展示,调用方代码未对 808xxx 做按值分支(`useHousekeeperPlacementAdjustment.js:71` 注释确认),拆分前后前端展示路径一致。
- **触发频率变化**:`inventoryStatus=CLOSED` 的触发条件由「仅库存为 0」扩大为「日历关闭/售罄/无日历行 或 库存为 0」,该状态出现频率会提升,但因前端已走统一的三值禁用逻辑,不需要代码改动去适配。
---
## 七、不影响范围
- 小程序端接口不变。
- 订单确认、支付、发票等下游流程无改动。
---
## 八、测试环境已验证
测试服环境,2026-09-30~10-02。
```
候选页(GET /v3/admin/hotel-candidates)
关闭日期场景:keyword 查询返回 inventoryStatus=CLOSED, available=0 ✓
重新开放:状态切回 AVAILABLE, available 恢复实际库存数 ✓
控房表(GET /v3/admin/order/house-console/room-control)
关闭日期:calendarStatus=CLOSED, calendarStatusName=已关闭 ✓
重新开放:calendarStatus=AVAILABLE, calendarStatusName=可售 ✓
配房扣减(POST /v3/admin/order/hotel-requirements/{id}/assignments)
提交已关闭日期的房型:返回 808907「已关闭或已售罄」✓
重新开放后提交:返回 200, stock 更新 ✓
```
---
## 十、相关文档
- **Issue**: [#8659](https://git.1814.love:8443/wx/HL/issues/8659)
- **PR**: [#8704](https://git.1814.love:8443/wx/HL/pulls/8704)
## 关联 / 联系人
**关联工单**: #8659
**同批变更**: #8662(旧接口删除与权限补漏)
**后端负责人**: @wx
@@ -0,0 +1,242 @@
---
schema: "hl-changelog/v2"
ticket: "8662"
title: "删除旧住宿需求提交接口(PUT /v3/admin/order/{id}/hotel-requirement)"
consumer: "admin"
author: "wx(GIT)"
change_type: "删除接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: ""
updated_at: "2026-10-02"
base: "dev-v3"
---
# 删除旧住宿需求提交接口
> **存放目录**: 二期 → `changelogs-v2/2026-10/`
>
> **服务**: hl-order-service-v3
> **Issue**: #8662
> **日期**: 2026-10-02
> **影响范围**: 管理后台订单住宿需求提交流程
---
## ⚠️ 关键变化
- 路由 `PUT /v3/admin/order/{id}/hotel-requirement` 已删除,服务端无此路由映射。
- **替代接口**:`POST /v3/admin/order/{id}/adjustment/submit`,请求体 `{"updates":{"hotelRequirement":{days,specialTags,remark}}}`,响应 `{success}`。
- **权限对齐**:旧接口零权限校验,任何后台账号可修改任意订单需求;新接口校验订单归属(管理员、超管、本单定制师放行,其他后台角色返回 581008;房务返回 581045)。
---
## 一、背景
旧接口 `PUT /v3/admin/order/{id}/hotel-requirement` 于 #4515 标注为废弃,继任者为 `POST /v3/admin/order/{id}/adjustment/submit`。源码删除说明(Controller 类 javadoc、`API-SPEC.html` §3.1)记载的旧接口缺陷:
1. **无权限校验**:该端点不校验操作人,任何登录后台的账号都能改写任意订单的住宿需求,不要求调用者是该单定制师。
2. **DONE_ADJUST 分支继承原认领房务**:已完成版需求再调整时(`status=DONE` → 重提),服务端按 `order_hotel_requirement` 旧行 `is_active=0` + 新行 `version+1` 落库,新行直接复制原 `claimer_*`(沿用原房控、不重新入抢单池),这一继承行为与权限校验无关,继任接口同样保留(见六.6)。
继任接口已在服务层加入 `OrderViewGuard.assertOrderAccessible()` 的归属校验(管理员/超管放行,本单定制师放行,其他后台角色 581008,房务管理员 581045)。`API-SPEC.html` §3.1 删除说明与 hl-ui v2.1 代码核查一致确认:管理后台视图层此前已零调用旧接口(均已改走 `adjustment/submit`),故本次删除对前端无需额外改动。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 住宿需求提交(旧) | PUT | `/v3/admin/order/{id}/hotel-requirement` | 删除 | 改用 adjustment/submit |
---
## 三、接口详情
本接口已删除。下表记录的是**删除前**的契约,仅供前端清理调用点之用。字段名、类型、错误码逐一取自删除前源码。服务端已无该路由映射,调用不会返回本表所述的正常响应或错误码,而将返回 HTTP 404(路由不存在)。
### 1. 住宿需求提交(旧) `PUT /v3/admin/order/{id}/hotel-requirement`
**VO**: `HotelRequirementReqVO → HotelRequirementRespVO`(均已删除)
#### 使用场景
删除前:定制师提交或修改订单的住宿需求(酒店偏好、特殊要求、入住日期等)。现改为 `POST /v3/admin/order/{id}/adjustment/submit`。
#### 入参(删除前)
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 订单 ID |
| days | Body | List<DayReq> | ✅ | 非空、按 dayNumber 排序 | 逐晚配房需求 |
| days[].dayNumber | Body | Integer | ✅ | ≥1 | 第几晚 |
| days[].stayDate | Body | LocalDate | ✅ | - | 入住日期 |
| days[].city | Body | String | ✅ | - | 城市代码 |
| days[].customerSelfBooked | Body | Boolean | ❌ | 默认 false | 客人自订该晚酒店 |
| days[].segments | Body | List<SegmentReq> | ❌ | - | 房间需求段(非自订晚通常需 ≥1 段) |
| days[].segments[].roomCategory | Body | String | ✅ | TWIN / KING / ... | 房型分类 |
| days[].segments[].roomCount | Body | Integer | ✅ | ≥1 | 间数 |
| specialTags | Body | List<String> | ❌ | - | 特殊标签(e.g.「协议酒店」「靠近景区」) |
| remark | Body | String | ❌ | ≤500 字 | 特殊要求备注 |
#### 出参(删除前) `Result<HotelRequirementRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| requirementId | Long | 需求行 ID |
| version | Integer | 版本号(首版=1) |
| status | String | 需求状态(PENDING / DONE_ADJUST 等) |
#### 请求示例(删除前)
```json
{
"days": [
{
"dayNumber": 1,
"stayDate": "2026-10-05",
"city": "hailar",
"segments": [
{
"roomCategory": "KING",
"roomCount": 2
}
]
}
],
"specialTags": ["协议酒店"],
"remark": "靠近景区"
}
```
#### 响应示例(删除前)
```json
{
"code": 200,
"message": "成功",
"data": {
"requirementId": "1930000000000000001",
"version": 1,
"status": "PENDING"
},
"success": true
}
```
#### 错误响应(删除前)
```json
{
"code": 400,
"message": "days 不能为空",
"data": null,
"success": false
}
```
#### 空数据 / 降级响应
接口已删除,无空数据或降级形态可约定;前端移除调用点。
#### 业务边界
- 服务端已无该路由映射,删除后返回 HTTP 404,前端不得依赖任何响应体判断,调用点一律移除。
- 替代接口经 `OrderViewGuard.assertOrderAccessible()` 校验归属,管理员/超管/本单定制师放行,其他后台角色 581008,房务 581045。
---
## 四、契约约束与正确调用方式
### 迁移路径
| 旧接口 | 新接口 | payload 转换 |
|--------|--------|-------------|
| `PUT /v3/admin/order/{id}/hotel-requirement` | `POST /v3/admin/order/{id}/adjustment/submit` | 旧 request body 的 `days` / `specialTags` / `remark` 改为嵌套:`{"updates":{"hotelRequirement":{days,specialTags,remark}}}` |
### 权限变化
| 角色 | 旧接口 | 新接口 |
|------|--------|--------|
| 本单定制师 | 200 放行 | 200 放行 |
| 其他后台定制师 | 200 放行(**缺陷**) | 581008 拒绝 |
| 房务 | 200 放行(**缺陷**) | 581045 拒绝 |
| 管理员 / 超管 | 200 放行 | 200 放行 |
---
## 五、数据库行为
| 前端提交 | 写入位置 | 行为 |
|----------|----------|------|
| 旧接口已删除 | - | 无(服务端零路由映射) |
---
## 六、边界行为
- 服务端已无该路由映射,调用返回 HTTP 404(`Not Found`)。
- 调用点一律移除,无需保留兼容代码。
---
## 六.5 枚举
不适用(接口已删除)。
---
## 六.6、修改前后对比
| 项 | 改前 | 改后 |
|----|------|------|
| 路由存在 | ✅ 存在 | ❌ 已删除,返回 404 |
| 权限校验 | ❌ 无,任何账号可修改任意订单 | ✅ 按定制师归属校验,非该单定制师返回 581008 |
| DONE_ADJUST 继承行为 | 旧行 `is_active=0` + 新行 `version+1`,复制原 `claimer_*` | 行为不变——继任接口走同一套 `adjustment/submit` 事务逻辑,继承规则与权限校验是两回事,本次改动只补了权限、未改这条继承规则 |
---
## 六.7、影响评估
- **前端无需改动**:经 hl-ui v2.1 核实,`src/api/orderV2.js` 中的 `putHotelRequirement` 函数定义仍在(标注 `@deprecated`),但全仓库内已无任何调用点(grep 零命中);`API-SPEC.html` §3.1 的删除说明同样记载"管理后台视图层已零调用(均已改走 §6.2)",两处结论一致。该函数是死代码,本次后端删除路由不会让任何现用页面失效。
- 如需清理,可删除 `putHotelRequirement` 这一处未使用的函数定义本身,但这不影响任何现有页面的可用性,不构成阻塞项。
---
## 七、不影响范围
- 新接口 `POST /v3/admin/order/{id}/adjustment/submit` 保留且功能完整。
- 房务配房流程无改动(房务走 house 域的 `HouseAssignmentAdminController`,不涉及本接口)。
- 小程序端、H5 端接口无改动。
---
## 八、测试环境已验证
测试服环境,2026-09-30~10-02。
```
PUT /v3/admin/order/{id}/hotel-requirement
非 owner 定制师角色调用:HTTP 404 ✓(路由已删除,非权限拒绝)
URL 转至新接口 POST /v3/admin/order/{id}/adjustment/submit 后:
非 owner 定制师角色:返回 581008 无权查看此订单 ✓
房务角色:返回 581045 房务角色无权查看订单详情,房务仅可配房 ✓
```
---
## 十、相关文档
- **Issue**: [#8662](https://git.1814.love:8443/wx/HL/issues/8662)
- **PR**: [#8705](https://git.1814.love:8443/wx/HL/pulls/8705)
- **继任接口文档**: `docs/order-v3/api/API-SPEC.html` §3.1(本端点删除说明与历史存档)、§6.2(继任端点 `adjustment/submit`)
## 关联 / 联系人
**关联工单**: #8662
**同批修改**: 询房预览权限补漏
**后端负责人**: @wx
@@ -0,0 +1,238 @@
---
schema: "hl-changelog/v2"
ticket: "8662"
title: "询房预览接口补房务读守卫"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: ""
updated_at: "2026-10-02"
base: "dev-v3"
---
# 询房预览接口补房务读守卫
> **存放目录**: 二期 → `changelogs-v2/2026-10/`
>
> **服务**: hl-order-service-v3
> **Issue**: #8662
> **日期**: 2026-10-02
> **影响范围**: 管理后台房务询房话术预览功能
---
## ⚠️ 关键变化
- 接口 `POST /v3/admin/order/inquiry/preview` 新增房务读守卫。
- **非房务角色**(定制师、管理员、其他后台角色)调用返回 **808090**「未登录或非房务角色,无权操作」。
- **房务角色**(房务管理员、超管)放行,功能无改动。
---
## 一、背景
二期房务功能收口中,#8390 统一给 16 个旧只读端点(日历 / 房务详情 / 酒店视图 / 转单候选 / 待办 / 月度对账 / 旧抢单池 / 订单房间)挂上房务读守卫,唯独询房话术预览这一个接口漏过,导致定制师、运营等非房务角色能调通,拿到酒店联系人与微信(源码:`HouseReadGuard.java` 类 javadoc)。本次补上后,受此守卫覆盖的端点共 17 个。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 询房话术预览 | POST | `/v3/admin/order/inquiry/preview` | 修改 | 新增房务读守卫,非房务角色返回 808090 |
---
## 三、接口详情
### 1. 询房话术预览 `POST /v3/admin/order/inquiry/preview`
**VO**: `InquiryPreviewReqVO → InquiryPreviewRespVO`
#### 使用场景
房务在配房弹窗点击「询房」时预览即将发往酒店的话术(所见即所发),确认无误后复制到企业微信。本次修改:非房务角色被拒。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| hotelId | Body | Long | ✅ | - | 酒店 ID;按此取 resource 联系人渲染文案 |
| orderId | Body | Long | ❌ | - | 订单 ID(取团号);`assignmentId` 有效时以其所属订单为准 |
| assignmentId | Body | Long | ❌ | - | 配房 ID;存在且与 `hotelId` 匹配时,日期/房型/间数/支付方式优先取该配房快照 |
| stayDate | Body | LocalDate | ❌ | - | 入住日期;`assignmentId` 命中时被快照值覆盖 |
| roomCount | Body | Integer | ❌ | ≥1 | 房间数;`assignmentId` 命中时被快照值覆盖;都缺省时按 1 间渲染 |
| roomCategory | Body | String | ❌ | 字典 room_category | 房型类别;`assignmentId` 未命中时用于查房型中文名,查不到则原样回退为传入的 code |
#### 出参 `Result<InquiryPreviewRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| messageBody | String | 渲染后的固定格式订房确认话术(模板见下) |
| contactName | String | 联系人姓名,resource 按 hotelId 带出,前端只读回显 |
| contactWechat | String | 联系人微信号,resource 按 hotelId 带出,前端只读回显 |
话术固定模板(`InquiryMessageTemplate.SEND_BASE`,占位符按 `Map` 渲染,缺失值替换为空串):
```
呼籁旅行 - 订房确认书:
团号:${teamNo}
日期:${stayDate}
房型:${roomTypeName}${roomCount}间
备注:${tags}
1.${paymentText},价格保密。
2.${breakfastText}${invoiceText}
3.核房电话:${phone}
辛苦确认后回复 @${replyContacts}
```
#### 请求示例
```json
{
"hotelId": 1900000001,
"orderId": 1900000000,
"stayDate": "2026-04-28",
"roomCount": 1
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"messageBody": "呼籁旅行 - 订房确认书:\n团号:HL20261001A\n日期:4.28\n房型:待补充1间\n备注:协议酒店\n1.领队前台现付,价格保密。\n2.含早含发票\n3.核房电话:0470-8888888\n辛苦确认后回复 @王前台",
"contactName": "王前台",
"contactWechat": "hailar_holiday"
},
"success": true
}
```
上例未传 `roomCategory`、也未传 `assignmentId`,`roomTypeName` 按规则取不到任何来源,渲染为空缺省文案(源码常量 `EMPTY_VALUE_TEXT`)——`${roomTypeName}${roomCount}间` 模板不插空格,故渲染结果是该空缺省文案与 `1间` 的无分隔拼接(见上方 JSON 示例的 `messageBody`),前端如需展示分隔需自行处理,后端不改模板。日期按 `M.d` 格式渲染(无补零),`2026-04-28` → `4.28`。ID、团号、酒店联系人等取值均为说明用的构造值。
#### 空数据 / 降级响应
- 酒店联系信息查询抛异常(`loadHotelExtended` 捕获全部 `RuntimeException`):静默降级,`contactName`/`contactWechat` 返回**空字符串 `""`(不是 NULL)**,`messageBody` 仍正常渲染,缺省字段分别落空值常量(`EMPTY_VALUE_TEXT`)/空标签常量(`EMPTY_TAG_TEXT`)/现付默认文案。
- `assignmentId` 传了但查不到记录、或与 `hotelId` 不匹配:静默降级为按请求参数 + 资源数据重新生成(不报错,仅记一条 `log.warn`),不是 assignment 快照。
- `assignmentId` 命中但与请求里的 `orderId` 不一致:忽略请求 `orderId`,改用该配房记录的真实 `orderId`(同样静默降级,仅记日志)。
- 不落库,房务可反复调用,无状态。
#### 错误响应
```json
{
"code": 808090,
"message": "未登录或非房务角色,无权操作",
"data": null,
"success": false
}
```
其他错误:
| code | message | 触发 |
|------|---------|------|
| 400 | hotelId 不能为空 | hotelId 未传 |
| 400 | 房间数最小为 1 | roomCount 传了但 < 1 |
#### 业务边界
- 只校验角色(房务管理员/超管放行),**不校验订单归属**:房务可预览任意订单的询房话术,与 #8390 覆盖的其余 16 个只读端点行为一致。
- 该守卫只拦「有角色但非房务」;零角色账号(网关未透传 `X-Admin-Role`)按既有口径仍放行,不受本次改动影响(`HouseReadGuard.java` 类 javadoc,#7609 G-2 定案)。
- 预览不落库,无副作用,可反复调用。
- `contactWechat` 仅供复制,前端不做交互(不拨电话、不主动跳转)。
---
## 四、契约约束与正确调用方式
### 权限对照
| 角色 | 改前 | 改后 | 说明 |
|------|------|------|------|
| 房务管理员 | 200 放行 | 200 放行 | 无改动 |
| 超管 | 200 放行 | 200 放行 | 无改动 |
| 定制师(CUSTOMIZER) | 200 放行(缺陷) | 808090 拒绝 | **新增限制** |
| 其他后台角色(如 ADMIN) | 200 放行(缺陷) | 808090 拒绝 | **新增限制** |
| 零角色账号(网关未透传 `X-Admin-Role`) | 放行 | 放行 | 无改动(#7609 G-2 口径,本次刻意不收) |
---
## 五、数据库行为
不落库,本接口无数据写入。
---
## 六、边界行为
- 错误码 808090 与所有同域房务读端点保持一致,可统一处理。
- 权限守卫受 Nacos 开关 `group-batch.acl.enforce.house-read-role` 控制。测试服 2026-09-30~10-02 期间该开关为开启状态(实测 ADMIN/CUSTOMIZER 均返回 808090,见「八、测试环境已验证」);生产环境以当时的配置为准,前端按本文档的错误码契约接即可,无需关心开关本身的开关状态。
---
## 六.5 枚举
不适用(接口无新增枚举)。
---
## 六.6、修改前后对比
| 项 | 改前 | 改后 |
|----|------|------|
| 权限校验 | ❌ 无房务守卫,任何角色可调 | ✅ 新增房务读守卫,非房务返回 808090 |
| 功能逻辑 | 预览话术、返回联系人 | 不变(仅权限改动) |
---
## 六.7、影响评估
- **前端无需改动**:经 hl-ui v2.1 核实,该接口的封装函数(`src/api/housekeeper/inquiry.js` 的 `previewInquiry`)仅有一处调用——`src/views/housekeeper/components/useHousekeeperInquiryCopy.js`,位于房务专属视图目录下,本就只在房务角色登录后的界面里被触达。非房务角色(定制师等)侧没有调用这个接口的代码,本次收紧权限不会让任何现有页面报错。
---
## 七、不影响范围
- 房务配房流程(使用此接口的场景)功能不变。
- 小程序端、H5 端接口无改动。
- #8390 已覆盖的其余 16 个旧只读端点本身无行为变化,本次只是把本端点补入同一套守卫。
---
## 八、测试环境已验证
测试服环境,2026-09-30~10-02,经网关实测,与修前基线逐字段比对(基线快照:`p50_ac3_room_manager.json`/`p50_ac3_super_admin.json`;本轮:`p6_ac3_roommanager.json`/`p6_ac3_superadmin.json`/`p6_ac3_admin.json`/`p6_ac3_consultant.json`)。
```
POST /v3/admin/order/inquiry/preview
房务管理员(ROOM_MANAGER):200,messageBody/contactName/contactWechat 与修前基线逐字节相同 ✓
超管(SUPER_ADMIN):200,messageBody/contactName/contactWechat 与修前基线逐字节相同 ✓
管理员(ADMIN):808090 未登录或非房务角色,无权操作 ✓
定制师(CUSTOMIZER):808090 未登录或非房务角色,无权操作 ✓
```
---
## 十、相关文档
- **Issue**: [#8662](https://git.1814.love:8443/wx/HL/issues/8662)
- **PR**: [#8705](https://git.1814.love:8443/wx/HL/pulls/8705)
- **背景工单**: [#8390](https://git.1814.love:8443/wx/HL/issues/8390)(原覆盖 16 个读端点统一守卫,本次 #8662 补上第 17 个——即本端点)
## 关联 / 联系人
**关联工单**: #8662
**同批删除**: 旧住宿需求提交接口
**后端负责人**: @wx
@@ -0,0 +1,360 @@
---
schema: "hl-changelog/v2"
ticket: "8665"
title: "配房删改与单日确认并发收口:三个写口新增 808932 并发冲突码"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: ""
updated_at: "2026-10-02"
base: "dev-v3"
---
# 配房删改与单日确认并发收口:新增 808932 并发状态码
> **存放目录**: 二期 → `changelogs-v2/2026-10/`
>
> **服务**: hl-order-service-v3
> **Issue**: #8665
> **日期**: 2026-10-02
> **影响范围**: 管理后台房务配房操作:删除、修改、单日确认三个写口,并发交错时新增返回错误码 808932
---
## ⚠️ 关键变化
- **新增错误码 808932**(房务状态已被并发修改,请刷新后重试):删除配房、修改配房、单日确认三个写口在并发交错时返回,不再产生孤儿应付台账行。
- **请求/响应结构无变化**:三个接口的方法、路径、入参字段均未变,仅错误码表新增一条。
- **808932 与其他业务错误码走同一套契约**:HTTP 200 + `code` + `message`,按 `code` 区分即可,无需为该码新增专门的前端处理分支;hl-ui v2.1 现有的通用业务错误拦截器(`src/utils/request.js` 的业务错误分支 + `errorBus.js`)会把后端返回的 `message` 原样呈现,不要求前端硬编码该文案。
---
## 一、背景
配房行的删除与修改两个写口(`DELETE /assignments/{id}` 与 `PUT /assignments/{id}`)原无互斥锁,与单日确认(`confirmDayPersist`)并发交错时,可能产生「配房行已软删,但应付台账仍留下可付款行」的孤儿记录。工单 #8665 为三个写口补充行级锁定读与版本控制,在并发修改被检测时返回 808932,整体回滚包括该行的任何台账产出。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 删除配房 | DELETE | `/v3/admin/order/assignments/{id}` | 修改 | 新增错误码 808932(并发冲突) |
| 2 | 修改配房 | PUT | `/v3/admin/order/assignments/{id}` | 修改 | 新增错误码 808932(并发冲突) |
| 3 | 单日确认 | POST | `/v3/admin/order/hotel-requirements/{requirementId}/assignments/days/{dayNumber}/confirm` | 修改 | 新增错误码 808932(并发冲突) |
---
## 三、接口详情
本节示例 JSON 的字段名、类型、错误码与文案逐一取自源码;ID、日期等取值为说明用的构造值。所有接口统一返回 `Result` 信封(`code` / `message` / `data` / `traceId` / `success`),示例省略 `traceId`;业务失败与入参校验失败均为 HTTP 200,靠 `code` 区分。
### 1. 删除配房 `DELETE /v3/admin/order/assignments/{id}`
**VO**: `无请求体 → Void`
#### 使用场景
房务管理员在管理后台删除已配置的某条配房行,通常在需要重新调整房型或取消某晚房务时使用。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 配房行 ID |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| (无数据体) | - | 删除成功时 `Result.data` 为 null |
#### 请求示例
```http
DELETE /v3/admin/order/assignments/2105709698592440321
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": null,
"success": true
}
```
#### 空数据 / 降级响应
- 若配房行不存在(已被删除或 ID 无效):返回业务错误码 808120「配房不存在」。
#### 错误响应
```json
{
"code": 808932,
"message": "房务状态已被并发修改,请刷新后重试",
"data": null,
"success": false
}
```
#### 业务边界
- **触发 808932 的场景**:该配房行在读取后被并发确认(单日确认的保留行流程)或被并发修改(改配房),版本对不上或已被软删,带版本谓词的软删(`softDeleteWithVersion`)影响 0 行。
- **建议的前端处理**:收到 808932 时提示用户刷新该订单的配房列表后重试;无需为该码单独编码重试逻辑,交由通用错误提示呈现即可。
- **串行无 808932**:若删除在单日确认完全提交之后才发起,行已稳定,返回 200;若确认在删除完全提交之后才查询候选,会命中更早的 808118(该天无可确认的询房中候选),不会到达锁定复读分支。
### 2. 修改配房 `PUT /v3/admin/order/assignments/{id}`
**VO**: `AssignmentUpdateReqVO → Void`
#### 使用场景
房务管理员修改已配置配房行的协议价、结算价、支付方式、备注、早餐、酒店主数据快照同步开关,不支持修改酒店、房型、间数(需要换酒店/换房型走 §2.3b 调整配房端点,或删除后用 §2.2 重新创建)。EXCEPTION 异常态下的配房仍允许走本接口改价,用于异常桶的人工处置(如与酒店协商退款后的补偿调整)。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 配房行 ID |
| protoPrice | Body | BigDecimal | ❌ | ≥0.00 | 协议价 |
| settlementPrice | Body | BigDecimal | ❌ | ≥0.00 | 结算价 |
| settleType | Body | String | ❌ | 正则 `cash\|sign\|company` | 结算方式 |
| syncProtocolPrice | Body | Boolean | ❌ | - | 是否同步协议价到酒店主数据快照 |
| syncSettlementPrice | Body | Boolean | ❌ | - | 是否同步结算价到酒店主数据快照 |
| syncSettleType | Body | Boolean | ❌ | - | 是否同步结算方式到酒店主数据快照 |
| remark | Body | String | ❌ | 无长度校验注解 | 备注 |
| syncHotelSnapshot | Body | Boolean | ❌ | - | 是否整体同步酒店主数据快照 |
| breakfast | Body | String | ❌ | 正则(`HouseBreakfast.VALUE_REGEX`,即 INCLUDED/EXCLUDED/PENDING) | 早餐 |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| (无数据体) | - | 修改成功时 `Result.data` 为 null |
#### 请求示例
```json
{
"settlementPrice": "320.50",
"remark": "与酒店协商调整"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": null,
"success": true
}
```
#### 空数据 / 降级响应
- 若配房行不存在:返回 808120「配房不存在」。
#### 错误响应
```json
{
"code": 808932,
"message": "房务状态已被并发修改,请刷新后重试",
"data": null,
"success": false
}
```
#### 业务边界
- **触发 808932 的场景**:该配房行在读取后被并发删除或被并发修改(版本漂移),带 `@Version` 校验的 `updateById` 影响 0 行。
- **EXCEPTION 态改价**:本接口不经过「需求级写锁 + EXCEPTION 写闸」(`assertWritableForAssignment`)那条校验链,因此配房所属需求处于 EXCEPTION(异常态)时仍可调用本接口改价,用于异常桶的人工处置;并发删除/修改仍会返 808932。
- **建议的前端处理**:收到 808932 时提示用户刷新该配房行详情后重试,无需额外编码特殊逻辑。
### 3. 单日确认 `POST /v3/admin/order/hotel-requirements/{requirementId}/assignments/days/{dayNumber}/confirm`
**VO**: `AssignmentDayConfirmReqVO → Void`
#### 使用场景
房务确认某个住宿需求的某一晚配房方案,触发应付台账推送和库存更新。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| requirementId | Path | Long | ✅ | - | 住宿需求 ID |
| dayNumber | Path | Integer | ✅ | 超出该需求行程晚数上界返 808102,无下界校验注解 | 第几晚(从 1 开始计数) |
| keepAssignmentIds | Body | List<Long> | ❌ | 非空时每个 id 须属于该晚询房中候选集,否则返 808123 | 保留的配房行 ID 清单(待翻 CONFIRMED);整个请求体、本字段均可省略,或传空数组——两者语义相同,均表示保留该晚**全部**询房中候选(全部翻 CONFIRMED),不传则不会软删任何候选行;显式传非空列表时,列表外的该晚候选行才会被软删 |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| (无数据体) | - | 确认成功时 `Result.data` 为 null |
#### 请求示例
```json
{
"keepAssignmentIds": [2105709698592440321]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": null,
"success": true
}
```
#### 空数据 / 降级响应
- 若该晚无询房中的候选(已被删除或已确认):返回 808118「该天无可确认的询房中候选, 请先配房再确认」。
- 若该需求所属订单已取消或处于异常处置中(`house_status=EXCEPTION`):返回 808119「订单已取消或异常处置中, 该需求不可新增或确认配房, 请在异常桶处理」。
- 若 `keepAssignmentIds` 非空且其中存在不属于该晚询房中候选的 id:返回 808123「保留的配房行不属于该天询房中候选(可能配房已更新), 请刷新后重试」。
#### 错误响应
```json
{
"code": 808932,
"message": "房务状态已被并发修改,请刷新后重试",
"data": null,
"success": false
}
```
#### 业务边界
- **触发 808932 的场景**:确认流程中保留行(`keepAssignmentIds` 对应的候选行)在候选列表读取之后被并发删除或修改,锁定复读时发现该行已不存在、已非询房中状态、版本不一致,或翻 CONFIRMED 的 `updateById` 影响 0 行。
- **整体回滚**:落选候选先软删、保留候选再逐行锁定复读,任一保留行复读发现不一致(抛 808932)时整个事务回滚——包括同一事务内已执行的落选行软删——该确认涉及的应付台账均不推送,确保终态一致,不留半截状态。
- **建议的前端处理**:收到 808932 时提示用户刷新该晚配房列表后重试,无需额外编码特殊逻辑。
- **串行与并发的区别**:若删除完全提交在确认发起之前,确认查询该晚候选时已无询房中的行,会命中更早的 808118(候选预检),不会到达锁定复读分支(808932 仅针对确认读取候选之后、锁定复读之前这段并发窗口);两条路径的共同效果一致:均不推送应付台账。
---
## 四、契约约束与正确调用方式
- 三个接口的请求方式、路径、参数均**无变化**,仅错误码表补充。
- 808932 与其他业务错误码(如 808118、808119、808120、808123)一样走 HTTP 200 + 业务码的行为,前端按 `code` 区分即可;hl-ui v2.1 的通用业务错误拦截器会将后端 `message` 原样呈现,不要求为 808932 单独编码处理分支。
- 单日确认(接口 3)请求体可以整体省略:省略或 `keepAssignmentIds` 传空数组语义相同,均表示保留该晚全部询房中候选;若需要落选部分候选行,必须显式传入要保留的 id 列表。
---
## 五、数据库行为
- 无表结构变更、无 Flyway 迁移(本次合并仅涉及 Service/测试代码与 API 文档,对比 #8665 合并提交的文件清单确认无 `db/migration` 变更)。
- 配房表 `house_hotel_assignment` 的 `version` 列与实体上的 `@Version` 注解系已有能力(早于本次改动),本次复用它为删除、修改、单日确认三个写口补齐「锁定读(`SELECT...FOR UPDATE`)+ 带版本更新/软删」的组合:先用锁定读拿到最新行与其 `version`,再执行带版本谓词的写操作(`updateById` 走 MyBatis-Plus 全局注册的乐观锁拦截器自动拼 `version` 条件;软删复用既有的 `softDeleteWithVersion(id, version)`),任一写操作影响 0 行即判定为并发冲突并抛 808932,整体事务回滚。
---
## 六、边界行为
- **离线与网络异常**:808932 不涉及网络/超时,纯业务并发码,离线时前端仍会收到错误响应。
- **灰度与开关**:本次改动无灰度开关,所有测试环境已包含该并发收口逻辑。
- **下游链路**:应付台账推送、库存扣减、其他异步链路仅当确认成功(code=200)才执行,808932 回滚后不产生任何账务记录。
---
## 六.5 枚举
不涉及新增枚举值;配房状态(INQUIRING/CONFIRMED/EXCEPTION 等)无变化。
---
## 六.6、修改前后对比
### 并发行为对比
| 项 | 改前 | 改后 |
|----|------|------|
| 删除配房并发于确认 | 可能双 200,留下孤儿应付台账行 | 后提交方返 808932,回滚,无台账产出 |
| 修改配房并发于确认 | 可能按过期快照推台账 | 后提交方返 808932,回滚,无台账产出 |
| 单日确认保留行被删 | 静默跳过已删行,继续推台账 | 锁定复读检测到行已删,返 808932,回滚,无台账产出 |
| 返回的错误码 | 无 808932 | 新增 808932(仅在并发窗口内返回) |
---
## 六.7、影响评估
- **是否破坏向后兼容**:否。新增错误码不影响其他业务流程,正常序列的删除、修改、确认仍返回 200。
- **前端是否必须同步上线**:否。hl-ui v2.1 现有的通用业务错误拦截器会把后端返回的 `message` 原样呈现,808932 走的是这条通用路径,不需要新增前端代码。
- **前端 workaround 清理点**:无。本次改动前 808932 这个码不存在,故前端不会有针对它的既有 workaround 需要清理。
**影响范围说明**:
- 前端 hl-ui:请求/响应字段无改动;808932 走通用业务错误展示路径,后端返回的 `message` 即为用户看到的提示文案,无需前端硬编码该文案。
- 管理后台网关:路由无变更,仅错误响应码增加,不影响路由规则和鉴权。
- 其他服务:本次改动是 order-v3 内部的行级锁定读 + 乐观锁版本校验,不新增分布式锁、不改 Feign 契约;finance、resource 等消费方零感知。
- 数据一致性:通过锁定读 + 版本控制,堵住了孤儿应付台账的产生根源,后续应付台账取消、对账等链路无需额外补丁。
---
## 七、不影响范围
- EXCEPTION 异常态下调用修改配房(接口 2)改价仍被允许——该接口本就不经过 `assertWritableForAssignment` 这条 EXCEPTION 写闸——未受本次新增版本控制的影响;本次改动只新增 808932 这一个并发冲突码,不改变任何既有的状态校验逻辑。
- 其他写口(如转房 `changeId`、取消级联删除等)未涉及本次改动,仍按既有逻辑。
- 只读接口(查询配房列表、查询单日候选等)逻辑无变化。
---
## 八、测试环境已验证
**部署状态**:
- hl-order-service-v3 @cd82a19ab(origin/dev-v3 的祖先,#8665 的合并提交 f8379ddfc3 已包含)
- hl-gateway @71def6dc5(网关落后 dev-v3 156 个提交,本次无路由变化,不影响)
- 部署时刻:2026-10-01 22:50:33
**测试链路 A:确认第 1 晚 → 删除该行**
- 操作序列:`POST /confirm` 返回 200 → `DELETE /assignments/{id}` 返回 200
- 终态:配房行 `deleted_at` 非空;应付台账该行已软删(无活跃 NORMAL 记录);日历库存回复
- 结论:PASS(两步均 200,无 808932)
**测试链路 B:删除该行 → 确认第 2 晚**
- 操作序列:`DELETE /assignments/{id}` 返回 200 → `POST .../days/2/confirm` 返回 808118(严格串行,无并发窗口)
- 终态:应付台账对该行零产出(`fin_payable_line WHERE source_ref_id=<该行id>` 为 0 行)
- 结论:PASS——判据是「第二步不再为该行产生台账行」,不要求第二步返回 200;完全串行操作下,confirmDay 在进入锁定复读之前先按该晚候选集过滤,发现该晚询房中候选集已为空,命中更早的 808118(该天无可确认的询房中候选),而不是 #8665 新增的 808932(锁定复读检查需要先有候选才会走到)。
**并发窗口说明**:
- 808932 是为「删除/修改读取之后、带版本的写操作之前」这段时间窗口的并发交错设计的,靠锁定读 + 乐观锁版本校验堵住。
- 完全串行操作(一方提交完成后另一方才查询)会先命中 808118(无候选)或 808120(配房不存在),不会到达锁定复读检查点;这两条路径的共同效果都是零台账产出。
- 测试环境部署状态:hl-order-service-v3 对 origin/dev-v3 零落后,#8665 的合并提交已在部署字节内,以上两条测试链路均为该部署字节下的实测结果。
---
## 十、相关文档
- API 规范:`docs/order-v3/api/API-SPEC-HOUSE-V1.1.html`(v1.1.20,2026-10-01)
- §2.2b 单日确认 / §2.3 修改配房 / §2.4 删除配房错误码表新增 808932
- §11.9 内部接口 + 跨服务(808900-808999)全表补 808932「房务状态已被并发修改,请刷新后重试」
- 工单正文:Gitea #8665(设计、验收、口径定案)
- 单测覆盖:
- `HouseAssignmentServiceTest#confirmDayPersist_keepUpdateAffectsZeroRows_throws808932AndNoPayablePush`:confirmDayPersist 中某 keep 行 updateById 影响 0 行时抛 808932,整体回滚,零推台账,落选不回补库存
- `HouseAssignmentServiceTest#delete_softDeleteAffectsZeroRows_throws808932NoPayableNoRestore`:delete() 在 softDeleteWithVersion 返 0 时抛 808932,不碰台账、不回补库存
- `HouseAssignmentServiceTest#updateTx_updateAffectsZeroRows_throws808932NoPayableRewrite`:updateTx() 乐观锁写库影响 0 行时抛 808932,不判台账、不作废不重推
- `HouseAssignmentDeleteConfirmDayOrderingIT`:固定时序交错 IT,覆盖「确认快照之后删配房提交→确认返 808932」与「删配房持行锁期间确认进入事务→确认被挡住随后 808932」两条交错,均断言无孤儿台账
---
## 关联 / 联系人
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,834 @@
---
schema: "hl-changelog/v2"
ticket: "8684"
title: "预支相关 8 个接口出参新增 canRevoke:当前操作人点「撤回」能否成功,与撤回接口共用同一个判定"
consumer: "admin"
author: "jw(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "41468f5297fc1d5c04b49772b76af73d2fd0c3bf"
target_release: "v2.1"
verified_at: "2026-10-02"
status_note: "已合并 dev-v3(806058c66)并部署 TEST,自签低权限 token 经网关实测:三个列表逐角色取值、联表 created_by 探针、开关关闭→还原往返(md5 逐字节还原,渲染期间拒绝日志零新增)、按 canRevoke 抽样真实撤回。前端待改两处撤回按钮:订单详情预支弹窗 AdvanceModal.vue 与团期财务页签 FinanceTab.vue 的 v-if 改为 a.canRevoke;审批中心本来没有撤回按钮。前端已交付(2026-10-02):两处撤回按钮 v-if 均改读 a.canRevoke,不再自拼 status+角色;存量行无键 undefined 不误显;spec FinanceTab 3 例+AdvanceModal 新建 2 例全绿,提交 41468f52。"
updated_at: "2026-10-02"
base: "dev-v3"
---
# order-v3: 预支列表出参新增 canRevoke
**服务**: hl-order-service-v3
**PR**: `#8723`(已合入 `dev-v3`,合并提交 `806058c66`)
**Issue**: #8684
---
## ⚠️ 关键变化
🟢 **8 个接口的出参新增一个字段 `canRevoke`(Boolean)**:当前操作人现在点「撤回」能不能成功。其余入参、出参、判权、错误码全部不变。
🔴 **同一行,不同人看到的值不同。** 不要缓存后跨账号复用,也不要拿它当这笔预支本身的属性。
🟢 **前端只需把撤回按钮的显示条件改成 `a.canRevoke`**,不用自己判断角色、申请人和开关。
---
## 一、背景
#8517 之后,撤回待审批预支(`DELETE /v3/admin/order/advance/:advanceId`)只放行申请人本人和超管、管理员,其余返回 `585009`。但列表只返回申请人姓名,没有申请人账号 ID,也没有「能不能撤」的标记,前端只能按「待审批」显示撤回按钮:非申请人也能看到,点了才提示 `585009`。
本单在出参里补 `canRevoke`,由后端按撤回接口的同一套规则算好。撤回接口本身的判权不变。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 本单预支列表 | GET | `/v3/admin/order/:orderId/advances` | 修改 | `records[]` 新增 `canRevoke` |
| 2 | 预支审批列表 | GET | `/v3/admin/order/advance-approvals/page` | 修改 | `records[]` 新增 `canRevoke` |
| 3 | 团期预支记录(财务页签) | GET | `/v3/admin/order/group-batch/:groupBatchId/advances` | 修改 | 每行新增 `canRevoke` |
| 4 | 发起订单级预支 | POST | `/v3/admin/order/:orderId/advance` | 修改 | 返回体新增 `canRevoke` |
| 5 | 预支审批通过 | PUT | `/v3/admin/order/advance/:advanceId/approve` | 修改 | 返回体新增 `canRevoke`(恒 `false`) |
| 6 | 预支审批驳回 | PUT | `/v3/admin/order/advance/:advanceId/reject` | 修改 | 返回体新增 `canRevoke`(恒 `false`) |
| 7 | 发起团期级预支 | POST | `/v3/admin/order/group-batch/:groupBatchId/advance` | 修改 | 返回体新增 `canRevoke` |
| 8 | 核单汇总快照 | GET | `/v3/admin/order/:orderId/settlement/summary` | 修改 | `advanceSummary.records[]` 新增 `canRevoke`(只含已通过,恒 `false`) |
---
## 三、接口详情
**`canRevoke` 取值规则**(8 个接口相同,按顺序判,命中即返回):
| # | 条件 | 取值 |
|---|---|---|
| 1 | 这笔预支不是待审批(`status ≠ SUBMITTED`) | `false` |
| 2 | 没有请求上下文(定时任务、消息消费等) | `false` |
| 3 | 当前角色是团期管理员 `GROUP_BATCH_MANAGER` | `false`(撤回接口先返回 `581008`,不受开关影响) |
| 4 | 当前角色是 `SUPER_ADMIN` / `ADMIN` | `true` |
| 5 | 当前账号就是申请人(创建人账号 ID 相等) | `true` |
| 6 | 其余:开关 `advance.acl.enforce.role-guard` 为 `true`(默认) | `false` |
| 6' | 其余:开关已关闭 | `true`(撤回接口回到 #8517 之前的行为) |
创建人账号 ID 为空的存量行,只有第 4 条能得到 `true`。
### 1. 本单预支列表 `GET /v3/admin/order/:orderId/advances`
**VO**: `PageParam` → `Result<PageResult<OrderAdvanceRespVO>>`
#### 使用场景
订单详情的「预支」弹窗。前端据 `canRevoke` 决定每一行显不显示「撤回」按钮。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Path | Long | ✅ | 订单 ID | **不变** |
| page | Query | Integer | ❌ | ≥1 | **不变** |
| pageSize | Query | Integer | ❌ | ≥1 | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| records[].canRevoke | Boolean | 🆕 当前操作人点撤回能否成功,规则见上表 |
| 其余字段 | — | **不变**(`id` / `status` / `createdByName` 等) |
#### 请求示例
```http
GET /v3/admin/order/2100743225424621570/advances?page=1&pageSize=20 HTTP/1.1
Authorization: Bearer <本单定制师 token>
```
#### 响应示例
本单定制师看:自己申请的待审批为 `true`,管理员申请的待审批为 `false`,已通过的为 `false`。
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"id": "2105886435083206657",
"orderId": "2100743225424621570",
"teamNo": "26-8707",
"payeeName": "刘大山",
"payeeRole": "LEADER",
"payeeRoleText": "导游",
"advanceType": "TICKET",
"amount": 180.0,
"purpose": "呼伦贝尔大草原景区门票代垫",
"status": "SUBMITTED",
"statusText": "待审批",
"createdByName": "admin",
"canRevoke": true
},
{
"id": "2105886437624999938",
"orderId": "2100743225424621570",
"teamNo": "26-8707",
"payeeName": "刘大山",
"advanceType": "CATERING",
"amount": 120.0,
"purpose": "额尔古纳湿地午餐代垫",
"status": "SUBMITTED",
"statusText": "待审批",
"createdByName": "jw",
"canRevoke": false
}
],
"total": 2,
"page": 1,
"pageSize": 20
},
"success": true
}
```
#### 空数据 / 降级响应
无预支时 `records` 为空数组(不变)。开关 `advance.acl.enforce.role-guard` 关闭时,非团期管理员看待审批行都为 `true`。
#### 错误响应
判权不变:非本单定制师、财务等无权角色仍返回 `581008`。
```json
{
"code": 581008,
"message": "无权查看此订单",
"data": null,
"success": false
}
```
#### 业务边界
- 每行单独计算,同一页里可以有 `true` 也有 `false`。
- 车务管理员能看这个列表,但看所有行都是 `false`。
---
### 2. 预支审批列表 `GET /v3/admin/order/advance-approvals/page`
**VO**: `AdvanceApprovalPageReqVO` → `Result<PageResult<AdvanceApprovalPageItemRespVO>>`
#### 使用场景
「财务管理 → 预支审批」列表。页面目前没有撤回按钮,字段供后续使用。只有超管、管理员、财务能进(不变)。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| status | Query | String | ❌ | `SUBMITTED` / `APPROVED` / `REJECTED` / `PAID` | **不变** |
| page | Query | Integer | ❌ | ≥1 | **不变** |
| pageSize | Query | Integer | ❌ | ≥1 | **不变** |
| 其余筛选项 | Query | — | ❌ | — | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| records[].canRevoke | Boolean | 🆕 规则见上表;财务看别人申请的为 `false` |
| 其余字段 | — | **不变** |
#### 请求示例
```http
GET /v3/admin/order/advance-approvals/page?status=SUBMITTED&page=1&pageSize=20 HTTP/1.1
Authorization: Bearer <财务 token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"id": "2105886435083206657",
"orderId": "2100743225424621570",
"orderNo": "HL20260918082629372",
"teamNo": "26-8707",
"scope": "ORDER",
"amount": 180.0,
"status": "SUBMITTED",
"createdByName": "admin",
"canRevoke": false
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"success": true
}
```
#### 空数据 / 降级响应
无数据时 `records` 为空数组(不变)。开关关闭时财务看待审批行都为 `true`。
#### 错误响应
判权不变:非超管 / 管理员 / 财务返回 `585008`。
```json
{
"code": 585008,
"message": "仅财务或管理员可查看预支审批、审批或驳回预支",
"data": null,
"success": false
}
```
#### 业务边界
- 本接口多查了一列申请人账号 ID 用于计算,**不出参**。
- 团期级行(`scope=GROUP_BATCH`)同样按规则计算。
---
### 3. 团期预支记录(财务页签) `GET /v3/admin/order/group-batch/:groupBatchId/advances`
**VO**: `List<GroupBatchAdvanceItemVO>`(继承 `OrderAdvanceRespVO`)
#### 使用场景
团期详情「财务」页签的预支记录。前端据 `canRevoke` 决定显不显示「撤回」。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | 团期 ID | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| [].canRevoke | Boolean | 🆕 规则见上表;团期管理员恒为 `false` |
| 其余字段 | — | **不变**(`scope` / `orderNo` / `teamNo` 等) |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2104839654727618562/advances HTTP/1.1
Authorization: Bearer <管理员 token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": [
{
"id": "2105589347598368769",
"scope": "GROUP_BATCH",
"scopeName": "团期级",
"orderId": null,
"orderNo": null,
"amount": 5000.0,
"status": "SUBMITTED",
"statusText": "待审批",
"createdByName": "金卫",
"canRevoke": true
}
],
"success": true
}
```
#### 空数据 / 降级响应
无预支时返回空数组(不变)。开关关闭时,非团期管理员看待审批行都为 `true`,团期管理员仍为 `false`。
#### 错误响应
判权不变。团期不存在:
```json
{
"code": 589500,
"message": "团期不存在",
"data": null,
"success": false
}
```
#### 业务边界
- 原来前端写的 `a.status === 'SUBMITTED' && !isGroupBatchManager` 可以整体换成 `a.canRevoke`。
- 子订单级行与团期级行规则相同。
---
### 4. 发起订单级预支 `POST /v3/admin/order/:orderId/advance`
**VO**: `CreateAdvanceReqVO` → `Result<OrderAdvanceRespVO>`
#### 使用场景
订单详情「发起预支」。返回体里 `canRevoke` 对发起人本人为 `true`(刚建的待审批,自己可以撤)。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Path | Long | ✅ | 订单 ID | **不变** |
| payeeStaffId | Body | Long | ✅ | 本单人员 | **不变** |
| advanceType | Body | String | ✅ | 字典 `advance_type` | **不变** |
| amount | Body | BigDecimal | ✅ | >0,不超可用上限 | **不变** |
| purpose | Body | String | ❌ | — | **不变** |
| voucherUrl | Body | String | ❌ | — | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| data.canRevoke | Boolean | 🆕 发起人本人为 `true` |
| 其余字段 | — | **不变** |
#### 请求示例
```json
{
"payeeStaffId": "2100747615736897537",
"advanceType": "TICKET",
"amount": 180.00,
"purpose": "呼伦贝尔大草原景区门票代垫"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "2105886435083206657",
"orderId": "2100743225424621570",
"status": "SUBMITTED",
"statusText": "待审批",
"amount": 180.0,
"canRevoke": true
},
"success": true
}
```
#### 空数据 / 降级响应
无。
#### 错误响应
判权与校验不变。金额超可用上限:
```json
{
"code": 585004,
"message": "预支金额超过可用余额上限",
"data": null,
"success": false
}
```
#### 业务边界
- 只是返回体多一个字段,创建逻辑不变。
---
### 5. 预支审批通过 `PUT /v3/admin/order/advance/:advanceId/approve`
**VO**: `Result<OrderAdvanceRespVO>`(无请求体)
#### 使用场景
审批中心「通过」。审批后状态是已通过,`canRevoke` 恒为 `false`。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| advanceId | Path | Long | ✅ | 预支 ID | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| data.canRevoke | Boolean | 🆕 恒 `false` |
| 其余字段 | — | **不变** |
#### 请求示例
```http
PUT /v3/admin/order/advance/2105886444679774210/approve HTTP/1.1
Authorization: Bearer <财务 token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "2105886444679774210",
"status": "APPROVED",
"statusText": "已通过",
"approvedBy": "yaosutu",
"canRevoke": false
},
"success": true
}
```
#### 空数据 / 降级响应
无。
#### 错误响应
判权不变。预支不存在(财务 / 管理员):
```json
{
"code": 585000,
"message": "预支记录不存在",
"data": null,
"success": false
}
```
#### 业务边界
- 审批逻辑与出纳待付款的生成不变。
---
### 6. 预支审批驳回 `PUT /v3/admin/order/advance/:advanceId/reject`
**VO**: `RejectAdvanceReqVO` → `Result<OrderAdvanceRespVO>`
#### 使用场景
审批中心「驳回」。驳回后状态是已驳回,`canRevoke` 恒为 `false`。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| advanceId | Path | Long | ✅ | 预支 ID | **不变** |
| reason | Body | String | ✅ | 驳回原因 | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| data.canRevoke | Boolean | 🆕 恒 `false` |
| 其余字段 | — | **不变** |
#### 请求示例
```json
{
"reason": "门票已由地接社统一采购,无需个人垫付"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "2104861622562627585",
"status": "REJECTED",
"statusText": "已驳回",
"rejectReason": "门票已由地接社统一采购,无需个人垫付",
"canRevoke": false
},
"success": true
}
```
#### 空数据 / 降级响应
无。
#### 错误响应
判权不变。预支不存在(财务 / 管理员):
```json
{
"code": 585000,
"message": "预支记录不存在",
"data": null,
"success": false
}
```
#### 业务边界
- 驳回逻辑不变。
---
### 7. 发起团期级预支 `POST /v3/admin/order/group-batch/:groupBatchId/advance`
**VO**: `CreateGroupBatchAdvanceReqVO` → `Result<OrderAdvanceRespVO>`
#### 使用场景
团期财务页签「发起预支」。返回体里 `canRevoke` 对发起人本人为 `true`;团期管理员发起的为 `false`(团期管理员撤回会先被 `581008` 拦下)。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | 团期 ID | **不变** |
| payeeStaffId | Body | Long | ✅ | 团期人员 | **不变** |
| advanceType | Body | String | ✅ | 字典 `advance_type` | **不变** |
| amount | Body | BigDecimal | ✅ | >0,不超团期统一池上限 | **不变** |
| purpose | Body | String | ❌ | — | **不变** |
| voucherUrl | Body | String | ❌ | — | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| data.canRevoke | Boolean | 🆕 发起人本人为 `true`,团期管理员为 `false` |
| 其余字段 | — | **不变** |
#### 请求示例
```json
{
"payeeStaffId": "2100747615736897537",
"advanceType": "CATERING",
"amount": 600.00,
"purpose": "满洲里套娃广场团餐代垫"
}
```
#### 响应示例
示例值(发起人本人调用):
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "2105890177461317634",
"orderId": null,
"status": "SUBMITTED",
"statusText": "待审批",
"amount": 600.0,
"canRevoke": true
},
"success": true
}
```
#### 空数据 / 降级响应
无。
#### 错误响应
判权与校验不变。团期不存在:
```json
{
"code": 589500,
"message": "团期不存在",
"data": null,
"success": false
}
```
#### 业务边界
- 只是返回体多一个字段,创建逻辑与额度池不变。
---
### 8. 核单汇总快照 `GET /v3/admin/order/:orderId/settlement/summary`
**VO**: `Result<SettlementSummaryRespVO>`
#### 使用场景
核单页的汇总快照。`advanceSummary.records` 与预支列表共用同一个 VO,所以一并带上 `canRevoke`。这里只列已通过的预支,恒为 `false`。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Path | Long | ✅ | 订单 ID | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| advanceSummary.records[].canRevoke | Boolean | 🆕 恒 `false`(只含已通过) |
| 其余字段 | — | **不变** |
#### 请求示例
```http
GET /v3/admin/order/2100743225424621570/settlement/summary HTTP/1.1
Authorization: Bearer <管理员 token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"settled": false,
"orderId": "2100743225424621570",
"teamNo": "26-8707",
"advanceSummary": {
"approvedAmount": "90.00",
"records": [
{
"id": "2105886444679774210",
"status": "APPROVED",
"statusText": "已通过",
"amount": 90.0,
"canRevoke": false
}
]
}
},
"success": true
}
```
#### 空数据 / 降级响应
没有已通过的预支时 `records` 为空数组(不变)。
#### 错误响应
本接口没有业务错误码,订单不存在时也返回 `200` 和 `settled=false` 的空壳(行为不变)。网关层未带 token(TEST 2026-10-02 实打):
```json
{
"code": 401,
"message": "缺少有效的 Authorization 头",
"data": null,
"success": false
}
```
#### 业务边界
- 前端不需要用这里的 `canRevoke`。
---
## 四、契约约束与正确调用方式
- 撤回按钮显示条件改成 `a.canRevoke`,不要再自己拼「待审批 + 角色 + 是否团期管理员」。
- `canRevoke` 跟着当前登录账号和当前角色变;切换角色后要重新拉列表。
- `canRevoke=true` 只表示列表渲染那一刻能撤;点击时若已被别人审批,仍会返回 `585005`(状态不允许),照常提示即可。
- 撤回接口的判权与错误码不变:非申请人、非管理员 `585009`,团期管理员 `581008`。
---
## 五、数据库行为
- 零 DDL、零数据迁移、零写入。
- 取值用预支记录既有的创建人账号 ID(提交时自动写入);审批中心的联表查询多 select 这一列,不出参。
- 发起、审批、驳回的写库逻辑不变。
---
## 六、边界行为
- 定时任务、消息消费等无请求上下文的场景算出来恒为 `false`(这些场景不渲染列表)。
- 计算 `canRevoke` 不打 `ADVANCE_ACL_DENY` 日志;只有真调撤回被拒时才打。
- 缺角色的 token 若正好是申请人本人,`canRevoke=true`,与撤回接口一致(#8517 有意的设计)。
## 六.6、修改前后对比
| 场景 | 改前 | 改后 |
|---|---|---|
| 非申请人、非管理员看待审批行 | 按钮显示,点了 `585009` | `canRevoke=false`,前端可隐藏 |
| 申请人本人 / 管理员看待审批行 | 按钮显示,能撤 | `canRevoke=true` |
| 团期管理员看团期页签 | 前端用 `!isGroupBatchManager` 自己藏 | `canRevoke=false` |
| 非待审批行 | 前端按状态藏 | `canRevoke=false` |
| 开关关闭 | 前端不知道开关状态 | 非团期管理员都为 `true`,跟撤回接口一致 |
## 六.7、影响评估
- **是否破坏向后兼容**:否,纯新增字段。
- **前端是否必须同步上线**:否。不改前端时行为与改前相同(按钮照旧显示);改了才能隐藏点不了的按钮。
- **回滚**:revert PR #8723 后重新部署 order-v3。
---
## 七、不影响范围
- 撤回接口 `DELETE /v3/admin/order/advance/:advanceId` 的判权与返回:不变。
- 各接口的入参、判权、其余出参:不变。
- 出纳付款、核单计算:不变。
- 小程序端:无影响。
---
## 八、测试环境已验证
**环境**:TEST(`https://api.test.1814.love`) **验证时间**:2026-10-02 13:03~13:30
**构建身份**:order-v3 部署 `dev-v3 @ 806058c66`(本单合并提交),12:57 完成。零写入判据:部署后连查 6 次订单级预支列表,每行都带 `canRevoke` 键(旧字节没有这个键)。
**身份**:自签 token 直打网关,用 TEST 真实账号 ID 配对应角色;TEST 上没有在用的财务账号,财务用 `role=FINANCE` 的自签 token。
### 8.1 造数
在待出发订单 `HL20260918082629372`(定制师 1001)上:C1 定制师申请门票 180(待审批)、C2 管理员 jw 申请餐费 120(待审批)、C3 定制师申请门票 90 后由财务审批(已通过)。另有该单既有的已付款、已驳回各一笔。
### 8.2 三个列表逐角色取值
| 列表 | 查看人 | C1(1001 申请,待审批) | C2(jw 申请,待审批) | 已通过 / 已付款 / 已驳回 |
|---|---|---|---|---|
| 订单级 | 本单定制师 1001 | `true` | `false` | 均 `false` |
| 订单级 | 管理员 | `true` | `true` | 均 `false` |
| 订单级 | 车务管理员 | `false` | `false` | 均 `false` |
| 审批中心 | 财务 | `false` | `false` | 已通过 `false` |
| 审批中心 | 管理员 | `true` | `true` | 已通过 `false` |
团期财务页签(团期级待审批一笔,jw 申请):财务 `false`、团期管理员 `false`、管理员 `true`。
联表探针:财务角色、账号 ID 设为 1001 查审批中心,C1 为 `true`、C2 为 `false`,证明审批中心带出了创建人。
核单汇总:只含 C3,`canRevoke=false`。
### 8.3 按 canRevoke 抽样真实撤回
| 操作 | 结果 |
|---|---|
| 财务、其他定制师撤 C1;车务、定制师 1001 撤 C2(均为 `false`) | 均 `585009`,未删除 |
| 团期管理员撤 C1 | `581008` |
| 定制师 1001 撤 C1、管理员撤 C2(均为 `true`) | 均 `200`,已软删 |
### 8.4 nacos 回滚开关往返
| 态 | 订单级:定制师 / 车务看 C2 | 团期页签:财务 / 团期管理员 | 审批中心:财务看 C1、C2、团期级 |
|---|---|---|---|
| A 默认 | `false` / `false` | `false` / `false` | 全 `false` |
| B 关闭(10.5 秒生效) | `true` / `true` | `true` / `false` | 全 `true` |
| C 还原(7.7 秒生效) | `false` / `false` | `false` / `false` | 全 `false` |
- 发布带 `casMd5`,还原写在 `finally` 里;还原后 md5 与原值同为 `c2206934960057f70b7173159046dc54`。
- 两个实例的 `ADVANCE_ACL_DENY` 计数在三态的列表渲染前后都是 0;随后 4 次被拒的真实撤回让计数各 +2,证明计数有效。
### 8.5 回归(零写入)
不存在的预支 ID 调审批 / 驳回 / 撤回:定制师、车务 `585008` / `585008` / `585009`;财务 `585000` / `585000` / `585009`;管理员三个 `585000`;团期管理员三个 `581008`。与 #8517 验收读数一致,前后表行数不变。
### 本地证据
| 项 | 读数 |
|---|---|
| 相关 16 个测试类定向 | 265/0/0/0 |
| 一致性矩阵 | `canRevoke` 与撤回守卫在 144 种角色 × 账号 × 创建人 × 开关组合下逐条一致 |
| 变异 | 删掉审批中心联表那一列 → 1 例红;让 `canRevoke` 对财务放宽 → 2 例红;已还原 |
| order-v3 全量(有 Docker,两半) | 1115 个可执行测试类全部有报告;红 2 个类、`hl-finance` 红 25 个类,在基底 `3bad2ad27` 上读数与用例名逐条一致,本单零新增 |
---
## 十、相关文档
- Issue `#8684`;PR `#8723`
- 前置:Issue `#8517`(撤回判权本身)
## 关联 / 联系人
### 链接
- **Issue**: [#8684](https://git.1814.love/wx/HL/issues/8684)
- **PR**: [#8723](https://git.1814.love/wx/HL/pulls/8723)
- **Merge commit**: [806058c66](https://git.1814.love/wx/HL/commit/806058c66)
### 联系人
- **后端负责人**: @jw
@@ -0,0 +1,317 @@
---
schema: "hl-changelog/v2"
ticket: "8687"
title: "删除团期抢单池旧列表接口(GET /v3/admin/order/grab-pool/group-batches)"
consumer: "admin"
author: "wx(GIT)"
change_type: "删除接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: ""
updated_at: "2026-10-02"
base: "dev-v3"
---
# 删除团期抢单池旧列表接口
> **存放目录**: 二期 → `changelogs-v2/2026-10/`
>
> **服务**: hl-order-service-v3
> **Issue**: #8687
> **日期**: 2026-10-02
> **影响范围**: 管理后台团期抢单池旧列表页的数据来源
---
## ⚠️ 关键变化
- 路由 `GET /v3/admin/order/grab-pool/group-batches`(`HouseGroupGrabAdminController.listGrabPool`)已删除,服务端不再有该路由映射。调用时 HTTP 状态仍为 200(HL 业务失败统一走 200),响应体业务码 `code=404`:`{"code":404,"message":"接口不存在: GET /v3/admin/order/grab-pool/group-batches","data":null,"success":false}`。
- **替代接口**:`GET /v3/admin/order/house-allocation/group-batches`(`HouseAllocationListAdminController.listGroupBatches`,#8375/#8491 已上线);复刻旧列表口径传 `status=pendingClaim`(不需要再额外传 `batchStatus=RESOURCE_PREPARING`,两者同源于 `GroupBatchService.requirementConfirmableStatuses()`)。
- 同一控制器下 4 个团级写口(整团认领 `claim` / 释放 `release` / 接管 `takeover` / 转交 `transfer`)路径与行为均不变。
- hl-ui(`origin/v2.1`)的 `grab-pool-group.js` 对旧 GET 路径零调用(该文件自身注释已记载两个读口随 #8375 改版下线),无需前端联动改动。
---
## 一、背景
旧接口 `listGrabPool` 在 #8375 房务配房列表改版后已标 `@Deprecated`,继任者 `GET /v3/admin/order/house-allocation/group-batches` 自 #8375/#8491 起已承载同一批团期列表展示(待整团认领 + 已整团认领合表)。本次(#8687)删除旧路由与其专属 VO(`HouseGroupGrabPoolPageReqVO`、`HouseGroupGrabPoolItemRespVO`),以及服务层 `HouseGroupGrabService.listGrabPool` 与配套查询 DTO `HouseGrabPoolQuery`。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 团期抢单池列表(旧) | GET | `/v3/admin/order/grab-pool/group-batches` | 删除 | 改用 house-allocation/group-batches |
---
## 三、接口详情
本接口已删除。下表记录的是**删除前**的契约,供前端清理调用点、核对与替代接口的字段映射。字段名、类型、校验文案均取自删除前源码;服务端已无该路由映射,调用返回 HTTP 200 + 业务码 `code=404`(`接口不存在: GET /v3/admin/order/grab-pool/group-batches`)。
### 1. 团期抢单池列表(旧) `GET /v3/admin/order/grab-pool/group-batches`
**VO**: `HouseGroupGrabPoolPageReqVO → PageResult<HouseGroupGrabPoolItemRespVO>`(均已随本单删除)
#### 使用场景
删除前:房务在团期抢单池页查看未被整团认领、需求已整体确认、阶段为「资源准备中」的团期(整团一行)。现改为调用 `GET /v3/admin/order/house-allocation/group-batches?status=pendingClaim`。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| keyword | Query | String | ❌ | ≤32 字 | 团期号 / 产品名模糊搜索 |
| productId | Query | Long | ❌ | - | 产品 ID 精确过滤 |
| batchStatus | Query | String | ❌ | 只接受 `RESOURCE_PREPARING` | 传其它值 400 |
| departDateFrom | Query | LocalDate | ❌ | - | 出发日下界(含) |
| departDateTo | Query | LocalDate | ❌ | - | 出发日上界(含) |
| page | Query | Integer | ❌ | ≥1,默认 1 | 页码 |
| pageSize | Query | Integer | ❌ | 1~100,默认 20 | 每页条数 |
| sortBy | Query | String | ❌ | 默认 `departDate,asc`,可切 `createTime,desc` | 排序 |
#### 出参
`Result<PageResult<HouseGroupGrabPoolItemRespVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| records | List<HouseGroupGrabPoolItemRespVO> | 行列表 |
| total | int | 总条数 |
| page | int | 回显页码 |
| pageSize | int | 回显每页条数 |
| records[].groupBatchId | String(Long 转字符串) | 团期主订单 ID |
| records[].batchNo | String | 运营团期号 |
| records[].productId | String(Long 转字符串) | 产品 ID |
| records[].productName | String | 产品名快照 |
| records[].batchName | String | 班期名快照 |
| records[].batchLabel | String | 第 N 期快照 |
| records[].batchStatus | String | 团期阶段 code(本接口恒为 `RESOURCE_PREPARING`) |
| records[].batchStatusLabel | String | 团期阶段中文(恒为「资源准备中」) |
| records[].departDate | LocalDate | 出发日期 |
| records[].endDate | LocalDate | 结束日期 |
| records[].enrollDeadline | LocalDate | 报名截止日 |
| records[].enrolledRooms | Integer | 已报名房数 |
| records[].enrolledPeople | Integer | 已报名人数 |
| records[].activeOrderCount | Integer | 活跃子订单数(排除 CANCELLED) |
| records[].hotelOrderCount | Integer | 已放行到房务的需房户数 |
| records[].hotelReady | Boolean | 团期酒店资源是否已就绪 |
| records[].daysToDepart | Integer | 今天到出发日天数 |
| records[].urgencyLevel | String | 紧急度 code(NORMAL/URGENT/CRITICAL) |
| records[].urgencyLabel | String | 紧急度中文 |
| records[].createTime | LocalDateTime | 团期创建时间 |
#### 请求示例
```http
GET /v3/admin/order/grab-pool/group-batches?page=1&pageSize=20
```
#### 响应示例
字段值取自 2026-10-02 11:13 测试服 `house-allocation/group-batches` 实测返回中的一行(该团当时阶段为 `RESOURCE_PREPARING`),按旧接口的 20 个字段裁剪展示;容器层 `total`/`page`/`pageSize` 为示例用值。
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"groupBatchId": "2104838272570245121",
"batchNo": "T26-7574",
"productId": "2101499109901778946",
"productName": "jw测试产品",
"batchName": "11月10日阿尔山温泉雪国5日游",
"batchLabel": "4",
"batchStatus": "RESOURCE_PREPARING",
"batchStatusLabel": "资源准备中",
"departDate": "2026-11-10",
"endDate": "2026-11-16",
"enrollDeadline": "2026-11-09",
"enrolledRooms": 0,
"enrolledPeople": 0,
"activeOrderCount": 0,
"hotelOrderCount": 0,
"hotelReady": false,
"daysToDepart": 39,
"urgencyLevel": "NORMAL",
"urgencyLabel": "正常",
"createTime": "2026-09-29 15:38:45"
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"success": true
}
```
#### 空数据 / 降级响应
接口已删除,无空数据或降级形态可约定。删除后任何入参都返回 HTTP 200 + `{"code":404,"message":"接口不存在: GET /v3/admin/order/grab-pool/group-batches","data":null,"success":false}`(2026-10-02 部署 `ca1b590d7` 后实测)。删除前,无命中记录时实测返回 `{"records":[],"total":0,"page":1,"pageSize":5}`(2026-10-02 11:12,提交 `a65c53ebbc`)。
#### 错误响应
```json
{
"code": 400,
"message": "batchStatus 只接受 RESOURCE_PREPARING",
"data": null,
"success": false
}
```
| code | message | 触发 |
|------|---------|------|
| 400 | keyword 长度不能超过 32 字 / batchStatus 只接受 RESOURCE_PREPARING / page 必须大于等于 1 / pageSize 最大 100 | 删除前的入参校验 |
| 808090 | 未登录或非房务角色,无权操作 | 删除前:非 ROOM_MANAGER / SUPER_ADMIN 访问(零角色 token 放行) |
#### 业务边界
- 服务端已无该路由映射,调用返回 HTTP 200 + 业务码 `code=404`;调用点一律移除。
- 替代接口 `GET /v3/admin/order/house-allocation/group-batches` 的 `status=pendingClaim` 分支复刻本接口口径(未被整团认领 + 需求已整体确认 + 阶段在可确认集合内,当前该集合只有 `RESOURCE_PREPARING`,单源 `GroupBatchService.requirementConfirmableStatuses()`),不需要额外传 `batchStatus`。
- 替代接口的 `batchStatus` 若显式传值,接受团期九态任一(不再锁定 `RESOURCE_PREPARING`),旧接口「传其它值 400」的收紧校验不再复现。
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload |
|------|---------|
| ❌ 团期抢单池列表 | `GET /v3/admin/order/grab-pool/group-batches` → 路由已删除,业务码 `code=404`(接口不存在) |
| ✅ 团期抢单池列表 | `GET /v3/admin/order/house-allocation/group-batches?status=pendingClaim` |
### 字段迁移
- 响应容器从 `PageResult`(`records`/`total`/`page`/`pageSize`)换成 `HouseAllocationGroupPageRespVO`(`list`/`total`/`stats`),字段名不同,且新容器不回显 `page`/`pageSize`。
- 旧 20 个行字段在新响应行 `HouseAllocationGroupRespVO` 中逐一同名存在(见六.6),可按原字段名直接取值。
- 新增的 `requirementConfirmed`/`houseClaimerId`/`houseClaimerName`/`houseClaimedAt`/`isMine`/`canStartAllocation`/`taskKind`/`taskKindLabel`/`unreadCount`/`readOnly`/`readOnlyReason` 是旧接口没有的扩展字段(旧池列表语义上只展示未认领团,没有认领人信息)。
---
## 五、数据库行为
本次清单仅涉及只读 GET 的删除,不涉及任何写入路径。
| 前端提交 | 写入位置 | 行为 |
|----------|----------|------|
| 无 | 无 | 本接口为只读,不涉及写入 |
---
## 六、边界行为
- 路由已从服务端删除,调用返回 HTTP 200 + 业务码 `code=404`(`接口不存在`);调用点一律移除,不要按这个返回体做分支判断。
- 同控制器 4 个写口(claim/release/takeover/transfer)未受影响,路径、错误码(808090 等)、角色门全部不变。
- 替代接口的角色门与旧接口等价:`ROOM_MANAGER` / `SUPER_ADMIN` 放行,零角色 token(网关未透传 `X-Admin-Role`)放行,其余角色 808090。
---
## 六.5 枚举
### 团期阶段 batchStatus(`GroupBatchStatus`)
**所属字段**: 旧接口 `HouseGroupGrabPoolPageReqVO.batchStatus`(入参,仅接受 `RESOURCE_PREPARING` 一值)/ 替代接口 `HouseAllocationGroupPageReqVO.batchStatus`(入参,接受全部九态)与两者行内 `batchStatus`/`batchStatusLabel`
**类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `RECRUITING` | 招募中 | 旧接口传此值 400,替代接口可传 |
| `RESOURCE_PREPARING` | 资源准备中 | 两接口唯一共同的「可认领」阶段 |
| `MATERIAL_PREPARING` | 物料准备中 | 旧接口传此值 400,替代接口可传 |
| `PENDING_DEPARTURE` | 待出发 | 同上 |
| `TRAVELLING` | 出行中 | 同上 |
| `PENDING_REVIEW` | 待核单 | 同上(#8516 由 `TRIP_FINISHED` 改名,旧值仍兼容) |
| `REVIEWING` | 核单中 | 同上 |
| `SETTLED` | 已结算 | 同上 |
| `CANCELLED` | 已取消 | 同上 |
---
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前(旧接口) | 改后(替代接口) |
|------|------|------|
| 响应容器 | `PageResult`:`records`/`total`/`page`/`pageSize` | `HouseAllocationGroupPageRespVO`:`list`/`total`/`stats`,不回显 page/pageSize |
| `groupBatchId`/`batchNo`/`productId`/`productName`/`batchName`/`batchLabel`/`batchStatus`/`batchStatusLabel`/`departDate`/`endDate`/`enrollDeadline`/`enrolledRooms`/`enrolledPeople`/`activeOrderCount`/`hotelOrderCount`/`hotelReady`/`daysToDepart`/`urgencyLevel`/`urgencyLabel`/`createTime` | 有(20 字段) | 同名字段原样保留 |
| `requirementConfirmed`/`houseClaimerId`/`houseClaimerName`/`houseClaimedAt`/`isMine`/`canStartAllocation`/`taskKind`/`taskKindLabel`/`unreadCount`/`readOnly`/`readOnlyReason` | 无 | 新增 11 个字段 |
| 入参 `batchStatus` 取值范围 | 仅 `RESOURCE_PREPARING`,其余 400 | 团期九态任一 |
| 入参 `scope`/`status` | 无(隐含只看未认领 + RESOURCE_PREPARING) | 新增,`status=pendingClaim` 复刻旧默认口径 |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 调用旧路由 | 返回分页列表 | 路由已删除,HTTP 200 + 业务码 `code=404`(接口不存在) |
| 查看待整团认领的团期 | 固定只看 `RESOURCE_PREPARING` 一个阶段 | 固定看 `requirementConfirmableStatuses()`(当前等价,单源可变) |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 是。路由删除,仍调用旧路径的代码会收到业务码 `code=404`(接口不存在)。
- **前端是否必须同步上线**: 否——经 hl-ui `origin/v2.1` 核查,`grab-pool-group.js` 对本路径零调用(该文件自身注释记载两个读口已随 #8375 改版下线),现网没有调用点需要跟随本单改动。
- **残留调用点清理**: 若历史分支仍保留对旧路径的调用、或按 `records`/`page`/`pageSize` 解析响应的代码,需改为按 `list`/`stats` 解析替代接口。
---
## 七、不影响范围
- **仅影响**: 调用 `GET /v3/admin/order/grab-pool/group-batches` 的代码(现网 hl-ui 已零调用)。
- **零影响**:
- 同控制器 4 个写口:`POST .../group-batches/{groupBatchId}/claim`、`.../release`、`.../takeover`、`.../transfer`
- 替代接口 `GET /v3/admin/order/house-allocation/group-batches` 与 `GET /v3/admin/order/house-allocation/households`
- 小程序与 H5
---
## 八、测试环境已验证
测试服环境,2026-10-02,经网关调用。
```
删除前(hl-order-service-v3 提交 a65c53ebbc)
GET /v3/admin/order/grab-pool/group-batches?page=1&pageSize=5
→ HTTP 200,code 200,records 为空(当时测试数据里没有满足旧池条件的团),11:12 实测
GET /v3/admin/order/house-allocation/group-batches?scope=all&page=1&pageSize=5(替代接口)
→ HTTP 200,code 200,total=8,11:13 实测;三节响应示例的字段值取自这次返回的其中一行
删除后(hl-order-service-v3 提交 ca1b590d7,13:25 部署,两个实例均已重启)
GET /v3/admin/order/grab-pool/group-batches?page=1&pageSize=5
→ HTTP 200,{"code":404,"message":"接口不存在: GET /v3/admin/order/grab-pool/group-batches","data":null,"success":false}
GET /v3/admin/order/house-allocation/group-batches?scope=all&page=1&pageSize=5(同一 token)
→ HTTP 200,code 200,total=8,第 1 页 5 个 groupBatchId 及顺序与删除前相同;
只有 isMine / readOnly / readOnlyReason 不同,这三个字段随查看者账号变化,两次请求用的账号不同
POST /v3/admin/order/grab-pool/group-batches/{groupBatchId}/release、/claim、/release
→ 均 HTTP 200,code 200,库里团级认领人按 原认领人 → 空 → admin → 空 变化;
最后用 /takeover 把认领人还原为原认领人
```
验证身份:超管测试账号。
---
## 十、相关文档
- 继任端点源码:`HouseAllocationListAdminController.listGroupBatches` / `HouseAllocationListService.pageGroupBatches`
- 前置变更:#8375(房务去掉抢单池,配房并入房务管家订单列表,继任端点上线)、#8491(房务控制台;其 I-24 删除了另外 4 个旧抢单池读口,本单删除的是剩下的这一个)
## 关联 / 联系人
### 链接
- **Issue**: [#8687](https://git.1814.love:8443/wx/HL/issues/8687)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,203 @@
---
schema: "hl-changelog/v2"
ticket: "8699"
title: "往来账·财务调账——调账单(TZ)+页内审批+应收/应付入账联动(#8699)"
consumer: "admin"
author: "yst(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "427e30804f31dbc61fad0c0be9f2117e8a5eb78d"
target_release: "v2.1"
verified_at: "2026-10-02"
status_note: "往来账「财务调账」(SRS 3.6.2,原型 fin-adjust)后端落地,原标二期提前本期实现。新增 /admin/finance/adjusts 5 端点:新建调账单(TZ-单号)/分页/详情/批准/驳回。挂团+改单团利润的应收/应付跨团调整集中口,金额正=调增/负=红字冲减,批准后顺数据流写台账、全留痕不删原记录、不动账户结存。应付侧写 fin_payable_line 调账行(可继续走付款申请),应收侧改 order-v3 查询叠加让应收台账数字真实变化(B 方案)。审批=页内单步批准/驳回(企微多级后期统一接);改单团利润只留痕标记;阈值 FINADJUST_APPROVE_MIN 从后端参数读取回传提示。部署测试服行为级验证全过:应收叠加 receivable 5360→5860(+500)精确、应付调增/调减、不挂团占位 UNGROUPED、驳回、596110/596103/596102 校验全对。前端交付(2026-10-02):新建 finance/adjust 页(hiddenRoute 先行,sys_menu 未下挂;useListPage 单表,金额负红/UNGROUPED 显「不挂团」/行无 *Name 本地映射中文;查看详情抽屉含 reviewLogs+needMultiLevelApprove 仅提示;PENDING 行批准 dialog/驳回 opinion 必填 596109 前置)+AdjustCreateModal(目标联动单位来源:应付 SupplierPickerModal 带 partyId/应收手填客户名;应收 teamNo 条件必填 596110 前置;金额禁 0 596103 前置)+api/finance/adjust.js,spec 12 例全绿,提交 427e3080。"
updated_at: "2026-10-02"
base: "dev-v3"
---
# finance:往来账·财务调账——调账单(TZ)+页内审批+应收/应付入账联动(管理后台)
> ✅ **additive 纯新增接口组**:新增 `/admin/finance/adjusts` 5 端点,旧前端不受影响。
## 1. 接口背景
往来账「财务调账」是**挂团 + 改单团利润的应收/应付跨团调整集中口**,调账单号 `TZ` 前缀。用于账对不上时的差异平账:金额正=调增 / 负=红字冲减,批准后顺数据流写入应付款 / 应收台账,**全留痕不删原记录、不动账户结存**。
此前仅有原型 + 设计稿(SRS 标二期),后端零实现。本期落地完整后端:调账单 CRUD + 页内审批 + 应收/应付入账联动。
## 2. 变更清单
| # | 接口 | 变更 | 类型 |
|---|---|---|---|
| 1 | POST /admin/finance/adjusts | 新建调账单(生成 TZ 单号,PENDING) | ✅ 新增 |
| 2 | GET /admin/finance/adjusts/page | 调账单分页(kw/target/status 筛选) | ✅ 新增 |
| 3 | GET /admin/finance/adjusts/{id} | 调账单详情(含审批留痕 + 多级提示标记) | ✅ 新增 |
| 4 | POST /admin/finance/adjusts/{id}/approve | 批准 → 入账联动 → POSTED | ✅ 新增 |
| 5 | POST /admin/finance/adjusts/{id}/reject | 驳回 → REJECTED | ✅ 新增 |
## 3. 接口详情
统一前缀 `POST/GET /admin/finance/adjusts/**`(业务调用**不带**服务前缀,网关按 `/admin/finance/**` 路由到 order-v3)。
## 4. 入参
### 4.1 新建调账单(POST /)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| adjustTarget | String | 是 | `RECEIVABLE` 应收 / `PAYABLE` 应付 |
| partyId | Long | 否 | 单位ID(应付=供应商ID / 应收=客户ID) |
| partyName | String | 是 | 单位名称(快照) |
| teamNo | String | 见说明 | 团号。应收:与 orderId **至少填一个**(否则 596110);应付:可空(空则入账占位 UNGROUPED) |
| orderId | Long | 见说明 | 订单ID(应收订单级定位;应付可空) |
| amount | BigDecimal | 是 | 调账金额(正=调增 / 负=红字冲减,**禁 0** 否则 596103) |
| accountPeriod | Date | 是 | 插入账期(yyyy-MM-dd;批准时校验未封账否则 596104) |
| profitFlag | Boolean | 是 | 是否改单团利润(true=修改 / false=不修改,**只留痕标记,不跨域回写**) |
| remark | String | 是 | 备注(调账原因/依据) |
### 4.2 分页(GET /page)
| 参数 | 说明 |
|---|---|
| pageNo / pageSize | 分页 |
| kw | 关键词(模糊 调账单号/单位名/团号) |
| adjustTarget | `RECEIVABLE` / `PAYABLE` 精确筛选 |
| status | `PENDING` / `POSTED` / `REJECTED` 精确筛选 |
### 4.3 批准 / 驳回(POST /{id}/approve | /{id}/reject)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| opinion | String | 驳回必填 | 审批意见(批准可空 / **驳回必填** 否则 596109) |
## 5. 出参
### 5.1 调账单行(AdjustRowRespVO)/ 详情(AdjustDetailRespVO)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Long(string) | 调账单ID |
| adjustNo | String | 调账单号(TZ-yyyyMMddNNNN) |
| adjustTarget | String | RECEIVABLE / PAYABLE |
| partyId / partyName | Long(string) / String | 单位 |
| teamNo | String | 团号(可空) |
| orderId | Long(string) | 订单ID(可空) |
| amount | BigDecimal | 调账金额(带符号) |
| accountPeriod | Date | 插入账期 |
| profitFlag | Boolean | 是否改单团利润 |
| remark | String | 备注 |
| status | String | PENDING / POSTED / REJECTED |
| postedFlow | String | 入账关联流水描述(批准后回填,如「应付款·某供应商·某团」「应收台账·26-2827」) |
| reviewByName / reviewTime / reviewRemark | | 审批最终态 |
| createTime | LocalDateTime | 创建时间 |
详情额外:
| 字段 | 说明 |
|---|---|
| needMultiLevelApprove | Boolean:金额绝对值 ≥ 后端参数 `FINADJUST_APPROVE_MIN`(默认 5000)时为 true,前端据此提示「需多级审批」(本期后端仍单步批准,仅提示) |
| reviewLogs | 审批留痕列表(action/operatorName/opinion/fromStatus/toStatus/createTime) |
### 5.2 批准 / 驳回响应(AdjustReviewRespVO)
| 字段 | 说明 |
|---|---|
| status | 操作后状态(POSTED / REJECTED) |
| postedFlow | 入账关联流水(批准时) |
| needMultiLevelApprove | 多级审批提示标记(批准时) |
## 6. 枚举/数据字典
### adjustTarget(调整目标)
| 值 | 含义 |
|---|---|
| RECEIVABLE | 应收(客户侧) |
| PAYABLE | 应付(供应商侧) |
### status(调账单状态)
| 值 | 含义 |
|---|---|
| PENDING | 待审批 |
| POSTED | 已入账(批准生效) |
| REJECTED | 已驳回 |
## 7. 错误码
| 错误码 | 含义 | 触发 |
|---|---|---|
| 596101 | 调账单不存在 | id 不存在/已软删 |
| 596102 | 调账单非待审批状态,不可操作 | 对已 POSTED/REJECTED 单再批准/驳回 |
| 596103 | 调账金额不允许为 0 | amount=0 |
| 596104 | 插入账期已封账 | accountPeriod 落已封账期/无开账期 |
| 596105 | 调整目标非法 | adjustTarget 非 RECEIVABLE/PAYABLE |
| 596106 | 单位缺失 | partyName 空 |
| 596107 | 调账单号生成冲突 | TZ 取号撞号重试耗尽(重试即可) |
| 596109 | 驳回原因不能为空 | 驳回未填 opinion |
| 596110 | 应收调账必须挂团或挂订单 | RECEIVABLE 且 teamNo/orderId 双空 |
## 8. 示例
### 8.1 典型:新建应收调账单(挂订单 +500)→ 批准
```http
POST /admin/finance/adjusts
{"adjustTarget":"RECEIVABLE","partyName":"宋家辉","teamNo":"26-2827","orderId":2105709330353520641,
"amount":500,"accountPeriod":"2026-10-02","profitFlag":false,"remark":"尾款差额补差"}
→ 200 {"id":"2105837196957446145","adjustNo":"TZ-202610020001","status":"PENDING",...}
POST /admin/finance/adjusts/2105837196957446145/approve {"opinion":"同意"}
→ 200 {"status":"POSTED","postedFlow":"应收台账·26-2827","needMultiLevelApprove":false}
# 效果:该订单应收台账 receivableAmount 5360→5860(+500)、balanceAmount 0→500
```
### 8.2 应付调增(挂团 +300)
```http
POST /admin/finance/adjusts
{"adjustTarget":"PAYABLE","partyName":"草原行车队","teamNo":"26-8875","amount":300,
"accountPeriod":"2026-10-02","profitFlag":true,"remark":"包车加班费补差"}
→ 批准后 postedFlow="应付款·草原行车队·26-8875"
# 效果:fin_payable_line 追加调增行,可继续走付款申请/审批/出纳
```
### 8.3 应付调减不挂团(-100)→ 占位 UNGROUPED
```http
POST /admin/finance/adjusts
{"adjustTarget":"PAYABLE","partyName":"某供应商","amount":-100,
"accountPeriod":"2026-10-02","profitFlag":false,"remark":"多付红字冲减"}
→ 批准后 postedFlow="应付款·某供应商·UNGROUPED"
# 效果:fin_payable_line 追加 REDUCE 负向行;不挂团时团号占位 UNGROUPED
```
### 8.4 异常:应收双空
```http
POST /admin/finance/adjusts
{"adjustTarget":"RECEIVABLE","partyName":"某客户","amount":50,
"accountPeriod":"2026-10-02","profitFlag":false,"remark":"x"}
→ {"code":596110,"message":"应收调账必须挂团或挂订单","success":false}
```
## 9. 业务边界
- **应付侧**:调账行写入 `fin_payable_line`(来源 FIN_ADJUST),进应付款「按团号/按供应商」列表,**可继续走付款申请/审批/出纳支付**;不挂团时团号占位 `UNGROUPED`,会在按团列表出现一个 UNGROUPED 行,**前端可识别该值显示「不挂团」**。
- **应收侧**:调账不写新表行,由 order-v3 查询时**实时叠加**进应收台账数字(receivableAmount + balanceAmount)。挂订单按订单级叠加、挂团(order_id 空)按团维度叠加;负调账超额时 balanceAmount **可为负**(负值即「冲多了」信号,前端原样展示,不兜底)。
- **改单团利润** profitFlag 仅留痕标记,后端**不跨域回写**订单/团利润。
- **阈值**:needMultiLevelApprove 仅提示,本期后端仍单步批准;企微多级审批后期统一接。
- 全留痕不删原记录,审批轨迹进 reviewLogs。
## 10. 修改前后对比
新增接口组,无「修改前」。
## 11. 影响评估/回滚
- additive 纯新增,旧前端零影响。
- 回滚:删除调账域代码 + DROP fin_adjust/fin_adjust_review_log 即可(涉 DDL 回滚需谨慎,建议保留表只回滚代码)。
## 12. 注意事项
- Long 型 ID 全部是 string,前端勿按 number 解析。
- 应收调账**必须挂团或挂订单**(596110),否则静默不进台账——前端表单应引导填其一。
- 应付调账不挂团会出现 UNGROUPED 占位行,前端需做识别展示。
- 金额带符号:正=调增 / 负=红字冲减,禁 0。
- 驳回必须填驳回原因(596109)。
## 13. 关联/联系人
- Issue:https://git.1814.love/wx/HL/issues/8699
- PR:https://git.1814.love/wx/HL/pulls/8710
- merge commit:a65c53ebbc7dd0d5646a827dce8ea29f730055fb
- 后端负责人:腰苏图
@@ -0,0 +1,98 @@
---
schema: "hl-changelog/v2"
ticket: "8708"
title: "二期未支付订单自动取消时刻由创建后 2h 恢复为 24h,接口契约不变"
consumer: "multiple"
author: "wx(GIT)"
change_type: "修复"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "接口路径、入参、出参、错误码均不变;变化的只是二期待支付订单被系统自动取消的时刻:由创建后约 2h 恢复为创建后 24h,与已展示的支付截止时刻 expiryTime 一致。"
updated_at: "2026-10-02"
base: "dev-v3"
---
# 二期订单:未支付订单自动取消时刻恢复为创建后 24h
> **服务**: hl-order-service-v3(取消判定与扫描任务)、hl-user-service(定时任务调度)
> **关联 Issue**: #8708
> **关联 PR**: #8729(合并提交 89c87fd217)
> **部署状态**: 测试环境已部署并实测,见「五、实测验证」
> **影响范围**: 小程序订单详情/列表与发起支付、管理后台订单详情与线下收款登记——只影响订单何时被自动取消,不改任何字段
---
## ⚠️ 关键变化
**改前**:二期订单对外展示的支付截止时刻是创建后 24h(`expiryTime = 创建时刻 + expiryMinutes`,`expiryMinutes` 为 1440),但系统在创建后约 2h 就把未支付订单自动取消。2~24h 之间,客户看到的截止时刻还没到,订单却已是 CANCELLED:小程序无法再发起支付,管理后台也无法登记线下收款。
**改后**:系统按同一个截止时刻取消。`创建时刻 + expiryMinutes` 到达后,在下一次每分钟扫描中取消,取消原因为「订单超时未支付,系统自动取消」。
---
## 一、背景
**根因**:自动取消原来只靠 RocketMQ 延迟消息触发。RocketMQ 4.x 延迟消息最长 2h,24h 窗口的过期消息实际在约 2h 后投递并取消订单;测试服 SYSTEM_AUTO_CANCEL 记录的延迟全部是 120 分钟。
**后果**(二期尚未上生产,影响面限于测试服数据):
- 客户在创建后 2~24h 之间无法支付;
- 取消会连带房务释放:测试服有 8 单取消后配房被软删并回补库存;
- 客户在取消前发起、取消后才到账的款落入 `CANCELLED_PAID`,需要人工退款。
---
## 二、后端改动
- 窗口 ≤ 120 分钟的订单仍由延迟消息触发取消;消费延迟消息时增加到期判定,截止时刻未到不取消。
- 新增每分钟一次的扫描任务:order-v3 `OrderExpiryJob`,由 user-service 定时任务经内部接口 `POST /v3/internal/jobs/order-expiry/run` 触发,按「创建时刻 + expiryMinutes ≤ 当前时刻」取消。窗口超过 120 分钟的订单靠它取消;延迟消息丢失时,它也是兜底。
- 该内部接口经网关访问一律返回 403,前端不可调用,也无需调用。
- 到期判定与小程序展示的 `expiryTime` 共用同一个计算(`OrderPayWindow`):展示的截止时刻就是系统取消的依据。
---
## 三、对前端的影响
**前端无需改动。** 接口路径、入参、出参、错误码均不变。
### 可观察到的行为变化
| 端 | 接口 | 改前 | 改后 |
|---|---|---|---|
| 小程序 | 订单详情、订单列表(order-v3 内部端点 `GET /v3/internal/mp/order/{orderId}`、`GET /v3/internal/mp/order/list`,经小程序服务转发) | 创建约 2h 后订单变为 CANCELLED,`expiryTime` 不再返回 | 截止时刻到达前订单保持 PENDING_PAY,`expiryTime` 持续返回 |
| 小程序 | 发起支付(order-v3 内部端点 `POST /v3/internal/mp/order/{orderId}/payment/unified-order`,经小程序服务转发) | 2~24h 之间订单已取消,返回 520104「订单当前状态不允许支付」 | 截止时刻到达前可正常发起支付 |
| 管理后台 | `GET /v3/admin/order/{id}` | 创建约 2h 后为 CANCELLED | 截止时刻到达前为 PENDING_PAY |
| 管理后台 | `POST /v3/admin/order/{orderId}/payment/manual-receipt` | 2~24h 之间订单已取消,无法登记 | 截止时刻到达前可正常登记;登记后订单离开待支付,不再被自动取消 |
### 字段口径(均未变化)
- 小程序详情/列表的 `expiryTime`:仅订单为 PENDING_PAY 时返回,值为 `创建时刻 + expiryMinutes`(`expiryMinutes` 为空时按 1440 计);其他状态返回 null。
- 管理后台创单响应的 `expiryMinutes`:仍为 1440。
---
## 四、覆盖范围与边界
- 范围:二期 order-v3 的待支付订单。一期 v2 订单不受影响。
- 已支付或已登记线下收款的订单离开待支付,不会被自动取消。
- 撤销线下收款后回到待支付的订单,系统不自动取消,需人工处理(与改前一致)。
- 取消时刻:截止时刻之后的下一次每分钟扫描,按设计在截止时刻后 2 分钟内。
- 按产品配置不同支付窗口,不在本次范围。
---
## 五、实测验证(测试环境)
- **部署后存量**:创建早于部署时刻 24h 以上、仍待支付的订单经扫描全部取消(剔除撤销收款后回到待支付的单,计数为 0)。阳性对照单部署前为超期待支付,部署后转为 CANCELLED,状态日志新增一条 SYSTEM_AUTO_CANCEL 记录。
- **新建订单**:管理后台新建两张核心订单,创建后 132 分钟两单仍为 PENDING_PAY(改前此时已被取消)。对其中一张登记线下收款,返回 code 200,订单转为已付定金,状态日志中没有自动取消记录。
---
## 关联
- 工单 #8708,PR #8729
@@ -0,0 +1,563 @@
---
schema: "hl-changelog/v2"
ticket: "8711"
title: "房务转房与退给酒店:招募中放开,源团期阶段栅栏按动作分码(#8711)"
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: "团期招募中时,「向酒店取消」放开、「转出」保持拒绝。源团期阶段不符时错误码统一用 808327 并随文案标出阶段。列表行新增两字段标示当前行能否转出,前端据此控制转出按钮与提示。"
updated_at: "2026-10-02"
base: "dev-v3"
---
# 房务:转房与取消,招募中场景分权,源团期栅栏细化(管理后台)
> **服务**: hl-order-service-v3
> **关联 Issue**: #8711
> **关联 PR**: #8730
> **部署状态**: 测试服验证通过(commit 704ecdd887)
> **影响范围**: 房务控制台「退团转房」页面(I-17/I-20/I-21 三端点)
---
## ⚠️ 关键变化
- **I-21 向酒店取消**:源团期招募中时也允许办理(此前返回 808323 拒绝)。
- **I-20 转房**:源团期招募中时被拒,改返新码 **808327**(文案统一为「退团房所在团期当前阶段(招募中)不允许处理」)。
- **I-17 列表**:每行新增 `transferAllowed`(Boolean)与 `transferBlockedReason`(String),指示该行是否允许转出与置灰理由。
---
## 一、背景
团期成团前处于招募中,此时转房打破户数基线而重新成团,与计划配置窗口冲突。旧流程让两个动作都被拒;本次改为**招募中只放开「取消」、保持拦「转出」**:
- **向酒店取消**(全量退团)不改库存配置,只通知酒店降间,允许执行;
- **转出**(转给别户/别团)改动源目标两处计划基线,禁止执行。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 退团转房列表 | GET | `/v3/admin/order/house-console/room-transfers` | 修改接口 | 行新增 `transferAllowed` 与 `transferBlockedReason` 字段(additive) |
| 2 | 转出 | POST | `/v3/admin/order/house-console/room-transfers/{id}/transfer` | 修改接口 | 源团期招募中返回 808327;错误码文案含源团期阶段中文 |
| 3 | 向酒店取消 | POST | `/v3/admin/order/house-console/room-transfers/{id}/cancel-hotel` | 修改接口 | 源团期招募中放开;不再返回 808323;阶段不符返回 808327 |
---
## 三、接口详情
### 1. 退团转房列表 `GET /v3/admin/order/house-console/room-transfers`
**VO**: `HouseRoomTransferPageRespVO → HouseRoomTransferRespVO`
#### 使用场景
房务控制台「退团转房」页面加载列表与汇总。房务查看待处理转房行、其状态与无法转出时的原因。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| pageNo | Query | Integer | ✅ | ≥1 | 页码 |
| pageSize | Query | Integer | ✅ | 1~100 | 每页条数 |
| status | Query | String | ❌ | PENDING / TRANSFERRED / CANCELLED | 状态筛选;缺省 PENDING |
| groupBatchId | Query | Long(string) | ❌ | - | 团期 ID 过滤(来源团期) |
| cityName | Query | String | ❌ | - | 城市名称过滤 |
| risk | Query | String | ❌ | NORMAL / NEAR / OVERDUE / NO_DEADLINE | 风险过滤 |
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| records | List[HouseRoomTransferRespVO] | 分页行 |
| total | Long | 总行数 |
| page | Integer | 当前页码 |
| pageSize | Integer | 每页条数 |
| summary | Object | 待处理汇总(以间为单位) |
| summary.pendingRooms | Integer | 待处理总间数 |
| summary.overdueRooms | Integer | 已过免费取消期限的间数 |
| summary.nearRooms | Integer | 临近期限的间数 |
| summary.noDeadlineRooms | Integer | 未设期限的间数 |
**行数据关键字段**(HouseRoomTransferRespVO):
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long(string) | 转房行 ID |
| sourceType | String | 来源类型:ORDER(常规单)/ GROUP_BATCH(团期) |
| sourceOrderId | Long(string) | 源订单 ID(团期来源时为离团子单) |
| sourceGroupBatchId | Long(string) | 源团期 ID(仅团期来源时有值) |
| teamNo | String | 源订单团号(常规单来源时取 order_main.team_no;团期来源时为 null) |
| sourceBatchNo | String | 源团期批次号(仅团期来源时有值) |
| stayDate | Date | 入住日期(yyyy-MM-dd) |
| cityName | String | 城市名 |
| hotelName | String | 酒店名 |
| roomTypeName | String | 房型名 |
| roomCount | Integer | 原始间数 |
| remainingCount | Integer | 剩余待处理间数 |
| status | String | 状态:PENDING(待处理)/ TRANSFERRED(已转出)/ CANCELLED(已取消) |
| statusLabel | String | 状态中文 |
| cancelDays | Integer | 免费取消提前天数(未设为 null) |
| deadlineAt | LocalDateTime | 免费取消截止时刻(未设为 null) |
| risk | String | 风险码:NORMAL / NEAR / OVERDUE / NO_DEADLINE(仅 PENDING 有意义) |
| riskLabel | String | 风险中文 |
| readOnly | Boolean | 是否对当前操作人只读:源单由他人处理,或源团期两个动作都不允许 |
| readOnlyReason | String | 只读理由 |
| **transferAllowed** | **Boolean** | **✨ 新增:是否可转出(I-20)。PENDING 行仅源团期资源准备中为 true,招募中为 false(仍可向酒店取消),其余阶段为 false。已处理行同样计算但不作展示依据** |
| **transferBlockedReason** | **String** | **✨ 新增:转出置灰提示。可转出时为 null;招募中为「所在团期招募中,仅可向酒店取消」;其余不可处理阶段与 readOnlyReason 相同** |
#### 请求示例
```http
GET /v3/admin/order/house-console/room-transfers?pageNo=1&pageSize=20&status=PENDING&groupBatchId=2105934717486563329
Authorization: Bearer <token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"id": "2105935060039565313",
"sourceType": "GROUP_BATCH",
"sourceOrderId": "2105934624691712001",
"sourceGroupBatchId": "2105934717486563329",
"teamNo": null,
"sourceBatchNo": "T5-260928-01",
"stayDate": "2027-05-12",
"cityName": "海拉尔",
"hotelName": "阿尔善国际维景度假温泉酒店",
"roomTypeName": "高级套房",
"roomCount": 2,
"remainingCount": 2,
"status": "PENDING",
"statusLabel": "待处理",
"cancelDays": 3,
"deadlineAt": "2027-05-11 18:00:00",
"risk": "NORMAL",
"riskLabel": "正常",
"readOnly": false,
"readOnlyReason": null,
"transferAllowed": false,
"transferBlockedReason": "所在团期招募中,仅可向酒店取消"
},
{
"id": "2105935060047953921",
"sourceType": "GROUP_BATCH",
"sourceOrderId": "2105934624699100225",
"sourceGroupBatchId": "2105934717486563329",
"teamNo": null,
"sourceBatchNo": "T5-260928-01",
"stayDate": "2027-05-13",
"cityName": "海拉尔",
"hotelName": "阿尔善国际维景度假温泉酒店",
"roomTypeName": "标准间",
"roomCount": 3,
"remainingCount": 2,
"status": "PENDING",
"statusLabel": "待处理",
"cancelDays": null,
"deadlineAt": null,
"risk": "NO_DEADLINE",
"riskLabel": "未设期限",
"readOnly": false,
"readOnlyReason": null,
"transferAllowed": false,
"transferBlockedReason": "所在团期招募中,仅可向酒店取消"
}
],
"total": 3,
"page": 1,
"pageSize": 20,
"summary": {
"pendingRooms": 7,
"overdueRooms": 0,
"nearRooms": 2,
"noDeadlineRooms": 5
}
},
"success": true
}
```
#### 空数据 / 降级响应
当无符合条件的行时,`records` 为空数组,`total=0`;`summary` 按 `status=PENDING` 的全库统计(不受筛选影响)。接口 GET 查询无降级;异常时返回 HTTP 500 + 500001。
#### 错误响应
```json
{
"code": 400,
"message": "分页参数不合法",
"data": null,
"success": false
}
```
#### 业务边界
- `transferAllowed=false` 时前端应禁用「转出」按钮,显示 `transferBlockedReason` 作置灰提示,但保留「向酒店取消」按钮可用。
- `transferAllowed=true` 时 `transferBlockedReason` 恒为 null;前端可隐藏转出提示。
- `readOnly=true` 时两个按钮都禁用,提示来自 `readOnlyReason`(源单由他人处理);此时 `transferAllowed` 同样为 false 但提示词不同(后者指阶段,前者指权限)。
- 已处理行(status=TRANSFERRED / CANCELLED)虽然 `transferAllowed` 同样计算,但不作展示依据;前端按 `status` 判断是否展示操作区域。
---
### 2. 转出 `POST /v3/admin/order/house-console/room-transfers/{id}/transfer`
**VO**: `HouseRoomTransferSaveReqVO → HouseRoomTransferRespVO`
#### 使用场景
房务把待处理的退团房间转给另一张常规订单或另一个团期。只有源团期处于资源准备中时可转,招募中及其他阶段被拒。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long(string) | ✅ | 存在且 status=PENDING | 转房行 ID |
| targetType | Body | String | ✅ | ORDER / GROUP_BATCH | 目标类型(常规单/团期) |
| targetOrderId | Body | Long(string) | 条件 | 当 targetType=ORDER 时必填 | 目标订单 ID |
| targetRequirementId | Body | Long(string) | 条件 | 当 targetType=ORDER 时必填 | 目标需求 ID |
| targetGroupBatchId | Body | Long(string) | 条件 | 当 targetType=GROUP_BATCH 时必填 | 目标团期 ID |
| roomCount | Body | Integer | ✅ | ≥1,≤剩余待处理间数 | 转出间数 |
| hotelConfirmNo | Body | String | ✅ | - | 酒店确认号 |
| proofFileIds | Body | List[String] | ✅ | 非空列表 | 凭证文件 ID 列表 |
| remark | Body | String | ❌ | - | 备注 |
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long(string) | 转房行 ID(与入参相同) |
| status | String | 更新后状态:PENDING(部分转出)或 TRANSFERRED(全部转出) |
| remainingCount | Integer | 更新后剩余待处理间数 |
| targetType | String | 目标类型 |
| targetOrderId | Long(string) | 目标订单 ID(可为 null) |
| targetTeamNo | String | 目标订单团号(可为 null) |
| targetGroupBatchId | Long(string) | 目标团期 ID(可为 null) |
| targetBatchNo | String | 目标团期批次号(可为 null) |
| handledAt | LocalDateTime | 处理时间 |
| handlerName | String | 处理人姓名 |
(其余字段同 I-17 出参)
#### 请求示例
```json
{
"targetType": "GROUP_BATCH",
"targetGroupBatchId": "2105928497547640834",
"roomCount": 2,
"hotelConfirmNo": "HX20270513002",
"proofFileIds": ["1007", "1008"],
"remark": "退团户要求转给该团期同城"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "2105935060047953921",
"status": "PENDING",
"remainingCount": 1,
"targetType": "GROUP_BATCH",
"targetOrderId": null,
"targetGroupBatchId": "2105928497547640834",
"targetBatchNo": "T3-260928-01",
"handledAt": "2026-10-02 14:30:20",
"handlerName": "房务A"
},
"success": true
}
```
#### 空数据 / 降级响应
无空数据场景(成功返回处理后的行)。无降级;异常时返回 HTTP 500 + 500001。
#### 错误响应
```json
{
"code": 808327,
"message": "退团房所在团期当前阶段(招募中)不允许处理",
"data": null,
"success": false
}
```
**可能的错误码**:
| 错误码 | 含义 | 触发条件 |
|--------|------|---------|
| 808320 | 转房记录不存在 | 行 ID 不存在或已软删 |
| 808321 | 该房间已处理 | 行 status ≠ PENDING,或并发下被先抢 |
| 808322 | 目标不可转入 | 不同城市、不同入住日期、无团号、或无转入空房 |
| 808323 | 目标团期当前阶段({0})不能转入,仅资源准备中可转入 | 仅 GROUP 目标阶段不符时出现;{0} 为**目标**团期状态中文 |
| 808324 | 转出间数超过剩余 {0} 间 | 转出间数 > 源计划剩余间数,或 > 目标缺口 |
| 808325 | 请填写酒店确认号并上传凭证 | hotelConfirmNo 或 proofFileIds 缺失 |
| 808326 | 只有原单处理人可以处理退团房间 | 操作人不是源单房务处理人 |
| 808327 | 退团房所在团期当前阶段({0})不允许处理 | 源团期阶段不符(招募中、物料准备中、已出发等);{0} 为**源**团期状态中文 |
#### 业务边界
- 源团期只在「资源准备中」允许转出;「招募中」返回 808327;「物料准备中」或更后期阶段同样返回 808327,文案显示实际阶段中文。
- 目标团期(仅 GROUP_BATCH)只在「资源准备中」允许转入;其他阶段返回 808323,文案显示**目标**阶段中文。
- 目标团期不存在返回 808322(不走 808323)。
- 转出后,如果 remainingCount=0 则行 status 变为 TRANSFERRED;否则保持 PENDING。
- 幂等键为「操作人 + 行 ID + 转出摘要」,3 秒内重复提交相同目标和间数只记一次。
---
### 3. 向酒店取消 `POST /v3/admin/order/house-console/room-transfers/{id}/cancel-hotel`
**VO**: `HouseRoomTransferCancelReqVO → HouseRoomTransferRespVO`
#### 使用场景
房务不再转房,直接向酒店通知取消,退款给客户。源团期处于资源准备中或招募中时均允许执行;其他阶段(物料准备中、已出发等)被拒。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long(string) | ✅ | 存在且 status=PENDING | 转房行 ID |
| cancelFee | Body | BigDecimal | ✅ | ≥0,精确到 2 位小数 | 取消费用(元;0 表示免费取消) |
| proofFileIds | Body | List[String] | ✅ | 非空列表 | 凭证文件 ID 列表(酒店回执) |
| remark | Body | String | ❌ | - | 备注 |
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long(string) | 转房行 ID(与入参相同) |
| status | String | 更新后状态:CANCELLED |
| cancelReason | String | 取消原因:HOTEL_CANCELLED(人工向酒店取消) |
| cancelFee | BigDecimal(string) | 取消费用 |
| handledAt | LocalDateTime | 处理时间 |
| handlerName | String | 处理人姓名 |
(其余字段同 I-17 出参)
#### 请求示例
```json
{
"cancelFee": "0.00",
"proofFileIds": ["1007"],
"remark": "酒店同意免费取消,按规则办理退订"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "2105935060039565313",
"status": "CANCELLED",
"cancelReason": "HOTEL_CANCELLED",
"cancelFee": "0.00",
"handledAt": "2026-10-02 13:45:30",
"handlerName": "房务A"
},
"success": true
}
```
#### 空数据 / 降级响应
无空数据场景(成功返回处理后的行)。无降级;异常时返回 HTTP 500 + 500001。
#### 错误响应
```json
{
"code": 808327,
"message": "退团房所在团期当前阶段(物料准备中)不允许处理",
"data": null,
"success": false
}
```
**可能的错误码**:
| 错误码 | 含义 | 触发条件 |
|--------|------|---------|
| 808320 | 转房记录不存在 | 行 ID 不存在或已软删 |
| 808321 | 该房间已处理 | 行 status ≠ PENDING,或并发下被先抢 |
| 808325 | 请填写酒店确认号并上传凭证 | proofFileIds 缺失或为空 |
| 808326 | 只有原单处理人可以处理退团房间 | 操作人不是源单房务处理人 |
| 808327 | 退团房所在团期当前阶段({0})不允许处理 | 源团期阶段不符(物料准备中、已出发等);{0} 为**源**团期状态中文 |
本接口不返回 808323(无目标概念)。
#### 业务边界
- 源团期在「资源准备中」或「招募中」均允许取消;「物料准备中」、「已出发」等阶段返回 808327。
- 取消成功后,行 status 变为 CANCELLED,cancelReason=HOTEL_CANCELLED。
- 源计划对应间数被软删(plan_deleted_at 标记);如有剩余间数,生成新计划记录「新间数=原间数-取消间数」。
- 幂等键为「操作人 + 行 ID」,3 秒内重复提交只记一次。
- 取消费用为 0 时表示酒店同意免费取消;>0 时表示需客户或预留从团费扣除。
---
## 四、契约约束与正确调用方式
### 常规订单来源的行
常规单退团产生的转房行,sourceType=ORDER,sourceOrderId 是离团子单,teamNo 从 order_main 反查。
### 团期来源的行
团期成团过程中由房务释放的转房行,sourceType=GROUP_BATCH,sourceOrderId 为离团子单(可为 null),teamNo 恒为 null(团期无团号),sourceGroupBatchId + sourceBatchNo 标识来源团期。
### transferAllowed 与 transferBlockedReason 联用
前端根据 transferAllowed 控制转出按钮:
- `true`:按钮可点,transferBlockedReason 为 null,不显示置灰提示;
- `false` + `transferBlockedReason="所在团期招募中,仅可向酒店取消"`:转出禁用,显示该提示,保留取消按钮可用;
- `false` + `transferBlockedReason` 为其他值(如"物料准备中"等):整行只读,两个按钮都禁用。
### 源/目标团期阶段判定
- **I-21 向酒店取消**:源团期 `RESOURCE_PREPARING` || `RECRUITING` 放行;其余返回 808327;
- **I-20 转出**:仅源团期 `RESOURCE_PREPARING` 放行;其余返回 808327;目标团期(仅 GROUP_BATCH)仅 `RESOURCE_PREPARING` 放行,其余返回 808323;
- **I-17 列表**:`transferAllowed` 与 `transferBlockedReason` 由业务侧编排返回(无存储),实时计算。
---
## 五、数据库行为
- 无新增或删除字段;
- 无表结构变更、无 Flyway。源团期阶段判定在服务层完成:源团期 `RECRUITING` 时 I-21 向酒店取消照常写 `house_room_transfer`(状态转 CANCELLED、原因 HOTEL_CANCELLED),I-20 转出在写库前即返回 808327、不落任何行;
- I-17 列表行返回的 `transferAllowed` 与 `transferBlockedReason` 由业务侧实时计算,无存储(源团期状态通过 order_group_batch.batch_status 关联查询);
- 操作日志(`house_operation_log`)记录处理人、操作类型、摘要,支持二期团期转房审计。
---
## 六、边界行为
### 6.1 业务边界
**I-17 列表**:
- `transferAllowed=false` 时前端应禁用「转出」按钮,显示 `transferBlockedReason` 作置灰提示,但保留「向酒店取消」按钮可用;
- `transferAllowed=true` 时 `transferBlockedReason` 恒为 null;前端可隐藏转出提示;
- `readOnly=true` 时两个按钮都禁用,提示来自 `readOnlyReason`(源单由他人处理);此时 `transferAllowed` 同样为 false 但提示词不同;
- 已处理行(status=TRANSFERRED / CANCELLED)虽然 `transferAllowed` 同样计算,但不作展示依据;前端按 `status` 判断是否展示操作区域。
**I-20 转出**:
- 源团期只在「资源准备中」允许转出;「招募中」返回 808327;「物料准备中」或更后期阶段同样返回 808327,文案显示实际阶段中文;
- 目标团期(仅 GROUP_BATCH)只在「资源准备中」允许转入;其他阶段返回 808323,文案显示**目标**阶段中文;
- 目标团期不存在返回 808322(不走 808323);
- 转出后,如果 remainingCount=0 则行 status 变为 TRANSFERRED;否则保持 PENDING;
- 幂等键为「操作人 + 行 ID + 转出摘要」,3 秒内重复提交相同目标和间数只记一次。
**I-21 向酒店取消**:
- 源团期在「资源准备中」或「招募中」均允许取消;「物料准备中」、「已出发」等阶段返回 808327;
- 取消成功后,行 status 变为 CANCELLED,cancelReason=HOTEL_CANCELLED;
- 源计划对应间数被软删(plan_deleted_at 标记);如有剩余间数,生成新计划记录「新间数=原间数-取消间数」;
- 幂等键为「操作人 + 行 ID」,3 秒内重复提交只记一次;
- 取消费用为 0 时表示酒店同意免费取消;>0 时需客户或从团费扣除;
- 本接口不返回 808323(无目标概念)。
## 六.6、修改前后对比
| 场景 | 修改前 | 修改后 |
|------|--------|--------|
| I-21 源团期招募中 | 返回 808323「目标团期已确认」(误导) | 放开执行;操作成功 |
| I-20 源团期招募中 | 返回 808323「目标团期已确认」 | 返回 808327「源团期阶段…招募中…不允许」(指向源侧) |
| I-20 源团期物料准备中 | 返回 808323「已确认」 | 返回 808327 + 源团期实际阶段中文 |
| I-17 行字段 | 无转出可行性标记 | 新增 transferAllowed + transferBlockedReason |
| 错误码 808323 使用 | 同时用于源、目标阶段不符 | 仅用于 GROUP 目标阶段不符,文案明确含「目标」 |
| 错误码 808327 | 无此码 | 新增,用于源团期阶段不符,文案明确含「源」与实际阶段 |
## 六.7、影响评估
**前端**:
- 需适配 I-17 行数据新增的两字段:transferAllowed 控制转出按钮状态,transferBlockedReason 作置灰提示文案;
- I-20、I-21 错误码文案调整,需更新各 toast/提示的映射(808323 专用于「目标团期阶段」,808327 专用于「源团期阶段」);
- 招募中场景下转出被拒(808327)与向酒店取消成功的对比体验,前端按新码分流处理。
**后端**:
- 代码层修改集中在 HouseRoomTransferManager 业务判定与 HouseRoomTransferRespVO 返回值构造,无 DB 迁移;
- 源团期状态 == 招募中时,I-21 放行、I-20 拦;由 GroupBatchStatus 枚举与 RECRUITING 常量驱动,存存逻辑一致;
- 错误文案由 HouseConsoleErrorCode 808323 与 808327 的 `{0}` 占位符承载,无需前端约定新码段。
**回滚**:
- PR revert 即可恢复旧逻辑(代码无迁移);新VO字段 transferAllowed / transferBlockedReason 前端如若忽视不显示,旧 UI 仍可用(字段补齐不减少现有消费)。
---
## 七、不影响范围
- I-18(修改期限)、I-19(候选目标)、I-22(异常检查)三个端点不受影响;
- 历史转房行数据不回填新字段(列表行是实时计算,非持久化);
- 常规单来源的转房行逻辑无变化(仅团期来源的招募中场景放开);
- 其他团期阶段(资源准备中、物料准备中、已出发等)的行为不变。
---
## 八、测试环境已验证
**部署与验证**:
- commit 704ecdd887;部署状态 `STATE=ok`;
- 测试端点:network 路由验证 / HTTP 状态码验证 / 业务返回码验证。
**AC-4 通过**:I-21 源团期 RECRUITING 下取消成功
- 前置:T5 团期先 RESOURCE_PREPARING 后成团变 RECRUITING;3 条 PENDING 行;
- 请求:`POST /v3/admin/order/house-console/room-transfers/2105935060039565313/cancel-hotel` body `{cancelFee:"0.00", proofFileIds:[1007], ...}`;
- 响应:HTTP 200,行 status=CANCELLED。
**AC-5 通过**:I-20 源团期 RECRUITING 下转出返回 808327
- 前置:同上 T5 RECRUITING;
- 请求:`POST /v3/admin/order/house-console/room-transfers/2105935060047953921/transfer` 转给常规单;
- 响应:HTTP 200 code=808327 msg="退团房所在团期当前阶段(招募中)不允许处理"。
**AC-9 通过**:I-20 目标侧 808323 文案含目标团期阶段
- 前置:源 T5 RESOURCE_PREPARING,目标 T3 RECRUITING;
- 请求:`POST .../transfer` targetType=GROUP_BATCH;
- 响应:HTTP 200 code=808323 msg="目标团期当前阶段(招募中)不能转入,仅资源准备中可转入"(文案含**目标**);
- I-21 同源行同时刻调用返回 200(无目标概念,不返回 808323)。
**AC-10 通过**:列表行字段
- RECRUITING 期间三行:transferAllowed=false,transferBlockedReason="所在团期招募中,仅可向酒店取消";
- 重新成团回 RESOURCE_PREPARING 后:剩余两行 transferAllowed=true,transferBlockedReason=null。
---
## 十、相关文档
- 后端对接文档:`API-SPEC-HOUSE` v1.1.21 §12.10(I-20 转房)与 §12.11(I-21 取消);
- 错误码说明:`HouseConsoleErrorCode` 808320-808327;
- VO 定义:`HouseRoomTransferRespVO` / `HouseRoomTransferSaveReqVO` / `HouseRoomTransferCancelReqVO`;
- 业务实现:`HouseRoomTransferManager` 与 `HouseRoomTransferQueryManager`。
---
## 关联 / 联系人
- **后端负责人**: @wx
- **Issue**: https://git.1814.love/wx/HL/issues/8711
- **PR**: https://git.1814.love/wx/HL/pulls/8730
- **部署日期**: 2026-10-02
- **测试报告**: `D:/work2/_scratch/0928-house-proto/t20/bug-transfer-orphan/api-test/report.md`
@@ -0,0 +1,309 @@
---
schema: "hl-changelog/v2"
ticket: "8719"
title: "应收侧往来台账放开——ledgerType 取值域扩为 SUPPLIER/SUPPLIER_RECV/CUSTOMER(#8719)"
consumer: "admin"
author: "yst"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "b7bca8502a898be1e8f3d95ae0817bcdd2006cc0"
target_release: ""
verified_at: "2026-10-02"
status_note: "⚠️【本契约将被 #8751 反转,前端需返工】前端 mmg 已按本文档交付(2026-10-02),但 #8751 用户拍板方案 A:台账入参由 ledgerType 单账套三值改为 partyType 往来对象净额视图(SUPPLIER=供应商应付+应收轧差/CUSTOMER=客户应收),ledgerType 入参作废,前端台账页需按 #8751 最新 changelog 重对接。原记录:应收初始化录入的应收期初(客户应收/供应商应收)原在往来台账看不到,本期放开应收侧台账:/admin/finance/statements/page 与 /entries/page 两接口 ledgerType 取值域由仅 SUPPLIER 扩为 SUPPLIER/SUPPLIER_RECV/CUSTOMER(STAFF 仍未开放,传了报 596005)。应收账套本期无业务流水,increaseTotal/decreaseTotal 恒 0、entries 空页属正常;netAmount/openingAmount 符号方向按账套不同,详见正文对照表。前端 2026-10-02 已交付:台账页三账套页签切换+净额语义按账套分化(应收正=应收/负=多收标红,SUPPLIER 口径不变)+entries 随行账套上送。"
updated_at: "2026-10-02"
base: "dev-v3"
---
# finance:应收侧往来台账放开——ledgerType 扩域(管理后台)
> ⚠️ **修改接口(入参取值域扩大 + 出参符号语义按账套分化)**:`ledgerType` 新增 `SUPPLIER_RECV` / `CUSTOMER` 两个合法值;出参 `netAmount` / `openingAmount` 的正负方向随账套不同,前端展示层必须按账套区分正负含义并标红负值。
## 1. 接口背景
应收初始化录入的**应收期初**(客户应收 / 供应商应收)原先在往来台账页**完全看不到**——因为台账接口的 `ledgerType` 只开放 `SUPPLIER`(供应商应付)一个账套,应收侧的期初挂账没有查询入口。
本期放开应收侧台账,让期初挂账可见:
- 供应商应收(`SUPPLIER_RECV`):我们预付/多付给供应商、应向他收回的钱
- 客户应收(`CUSTOMER`):客户欠我们的钱(该收未收)
注意本期**只放开期初可见性**,应收账套暂无业务流水(后续版本接),所以应收账套下 `increaseTotal` / `decreaseTotal` 恒为 0、明细接口返回空页属于**正常行为**,不是接口坏了。
## 2. 变更清单
| # | 接口 | 变更点 | 类型 |
|---|------|--------|------|
| 1 | `GET /admin/finance/statements/page` | 入参 `ledgerType` 取值域扩大:`SUPPLIER` → `SUPPLIER` / `SUPPLIER_RECV` / `CUSTOMER` | ⚠️ 修改 |
| 2 | `GET /admin/finance/statements/page` | 出参 `netAmount` / `openingAmount` 符号方向按账套分化(详见 §5 对照表) | 🔧 语义扩展 |
| 3 | `GET /admin/finance/statements/page` | 出参 `ledgerType` 可能出现新值 `SUPPLIER_RECV` / `CUSTOMER` | ✨ 枚举扩域 |
| 4 | `GET /admin/finance/statements/entries/page` | 入参 `ledgerType` 取值域同步扩大 | ⚠️ 修改 |
| 5 | 两接口 | `STAFF` 账套仍未开放,传 `STAFF` 报 596005 | 📝 行为不变 |
## 3. 接口详情
| 项 | statements/page | entries/page |
|---|---|---|
| 方法 + 路径 | `GET /admin/finance/statements/page` | `GET /admin/finance/statements/entries/page` |
| 接口名 | 往来台账分页(按往来单位汇总) | 台账明细分页(按往来单位看流水) |
| 使用场景 | 管理后台 → 财务 → 往来台账列表页 | 台账页点某往来单位后的明细流水 |
| 认证 | 管理后台登录态(JWT) | 同左 |
| 幂等性 | 只读查询,天然幂等 | 只读查询,天然幂等 |
| 限流 | 无特殊限流 | 无特殊限流 |
## 4. 接口入参
### 4.1 `GET /admin/finance/statements/page`(Query 参数)
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| ledgerType | string | **必填** | 账套:`SUPPLIER` / `SUPPLIER_RECV` / `CUSTOMER`(本期新增后两个值);传 `STAFF` 报 596005 |
| refName | string | 可空 | 往来单位名称,模糊匹配 |
| negativeOnly | boolean | 可空 | 只看净额为负(多付/多收)的单位;语义随账套,见 §5 |
| page | int | 必填 | 页码,从 1 开始 |
| pageSize | int | 必填 | 每页条数 |
### 4.2 `GET /admin/finance/statements/entries/page`(Query 参数)
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| ledgerType | string | **必填** | 账套,取值同 4.1;传 `STAFF` 报 596005 |
| refId | string | 必填 | 往来单位 ID(台账列表出参 `refId`) |
| direction | string | 可空 | 流水方向:`INCREASE` / `DECREASE` |
| sourceType | string | 可空 | 来源类型:`PAYMENT` / `PREPAY` |
| entryDateStart | string | 可空 | 流水日期起,格式 `yyyy-MM-dd` |
| entryDateEnd | string | 可空 | 流水日期止,格式 `yyyy-MM-dd` |
| teamNo | string | 可空 | 团号,精确过滤 |
| page | int | 必填 | 页码,从 1 开始 |
| pageSize | int | 必填 | 每页条数 |
## 5. 出参字段
### 5.1 statements/page 出参
统一分页包装 `PageResult`:`{ list/records: [...], total, page, pageSize }`(以前端现行解析字段为准,按现有页面使用的那个读)。
`records[]` 字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| ledgerType | string | 账套,可能值:`SUPPLIER` / `SUPPLIER_RECV` / `CUSTOMER`(新增后两个值) |
| refId | string | 往来单位 ID(Long 序列化为字符串,防精度丢失) |
| refName | string | 往来单位名称 |
| openingAmount | number | 期初净额,方向按账套(见下表) |
| increaseTotal | number | 本期增加合计(应收账套本期恒 0) |
| decreaseTotal | number | 本期减少合计(应收账套本期恒 0) |
| writeoffTotal | number | 核销合计 |
| adjustTotal | number | 财务调账合计 |
| netAmount | number | 当前净额,方向按账套(见下表) |
**金额符号语义对照表(前端展示必须按此区分)**:
| 账套 | openingAmount 期初净额 | netAmount 正数含义 | netAmount 负数含义 | 前端提示文案建议 |
|------|------------------------|---------------------|---------------------|------------------|
| `SUPPLIER` 供应商应付 | 期初应付 − 期初应收 | 我欠他(该付未付) | 多付他 | 应付 / 多付 |
| `SUPPLIER_RECV` 供应商应收 | 期初应收 − 期初应付 | 他欠我(该收未收) | 多收他 | 应收 / 多收 |
| `CUSTOMER` 客户应收 | 期初应收 − 期初应付 | 他欠我(该收未收) | 多收他 | 应收 / 多收 |
- **负值一律标红**(多付 / 多收属异常资金状态,需财务关注)。
- 同一列在不同账套 tab 下正负含义**相反**,不能直接复用「正=欠」的单一判断。
### 5.2 entries/page 出参
`records[]` 字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| id | string | 明细 ID |
| ledgerType | string | 账套,可能值同 5.1 |
| refId | string | 往来单位 ID |
| refName | string | 往来单位名称 |
| entryDate | string | 流水日期 |
| sourceType | string | 来源类型:`PAYMENT` / `PREPAY` |
| sourceId | string | 来源单据 ID |
| sourceNo | string | 来源单号 |
| direction | string | `INCREASE` / `DECREASE` |
| amount | number | 金额(正数,方向由 direction 表达) |
| teamNo | string | 团号,无挂团时为空 |
| summary | string | 摘要 |
## 6. 枚举 / 数据字典
### ledgerType(账套类型)
| 值 | 含义 | 本期状态 |
|----|------|----------|
| `SUPPLIER` | 供应商应付 | 原有,不变 |
| `SUPPLIER_RECV` | 供应商应收 | **本期新增开放** |
| `CUSTOMER` | 客户应收 | **本期新增开放** |
| `STAFF` | 员工往来 | **未开放**,传了报 596005 |
### direction(流水方向)
| 值 | 含义 |
|----|------|
| `INCREASE` | 增加 |
| `DECREASE` | 减少 |
### sourceType(来源类型)
| 值 | 含义 |
|----|------|
| `PAYMENT` | 付款/收款 |
| `PREPAY` | 预付 |
## 7. 错误码
| 错误码 | 含义 | 触发场景 |
|--------|------|----------|
| 596005 | 账套非法或未开放 | `ledgerType` 传了 `STAFF` 或其他未开放/不存在的值 |
## 8. 示例
### 8.1 典型:查供应商应收台账(新开放账套)
请求:
```
GET /admin/finance/statements/page?ledgerType=SUPPLIER_RECV&page=1&pageSize=20
```
响应 200:
```json
{
"code": 0,
"data": {
"list": [
{
"ledgerType": "SUPPLIER_RECV",
"refId": "1927081523456789012",
"refName": "草原牧歌车队有限公司",
"openingAmount": 5000.00,
"increaseTotal": 0,
"decreaseTotal": 0,
"writeoffTotal": 0,
"adjustTotal": 0,
"netAmount": 5000.00
}
],
"total": 1
}
}
```
读法:该供应商应收 5000,即「他欠我 5000 该收未收」(正 = 他欠我)。
### 8.2 边界:应收账套明细返回空页(正常)+ 负净额
查客户应收明细(本期无业务流水):
```
GET /admin/finance/statements/entries/page?ledgerType=CUSTOMER&refId=1927081523456789999&page=1&pageSize=20
```
响应 200(**空页属正常**,应收账套本期只有期初无流水):
```json
{
"code": 0,
"data": {
"list": [],
"total": 0
}
}
```
多收客户场景(负净额,前端标红):
```json
{
"ledgerType": "CUSTOMER",
"refId": "1927081523456789999",
"refName": "王某某",
"openingAmount": -800.00,
"increaseTotal": 0,
"decreaseTotal": 0,
"writeoffTotal": 0,
"adjustTotal": 0,
"netAmount": -800.00
}
```
读法:CUSTOMER 账套净额 -800 = 多收该客户 800,前端标红。
### 8.3 业务失败:传未开放账套 STAFF
请求:
```
GET /admin/finance/statements/page?ledgerType=STAFF&page=1&pageSize=20
```
响应:
```json
{
"code": 596005,
"message": "账套非法或未开放"
}
```
## 9. 业务边界
**适用**:
- 管理后台财务查看供应商应付 / 供应商应收 / 客户应收三个账套的期初挂账与(应付侧)流水。
- 应收侧(SUPPLIER_RECV / CUSTOMER)本期用于查看应收初始化录入的期初余额。
**不适用**:
- 员工往来(STAFF)查询——未开放,传了报 596005。
- 应收账套查业务流水——本期应收侧尚无业务流水入账,明细空页属正常,不要当 bug 报。
**特殊边界**:
- 应收账套 `increaseTotal` / `decreaseTotal` / `writeoffTotal` / `adjustTotal` 本期恒 0,`netAmount` 就等于 `openingAmount`(方向见 §5 对照表)。
- `refId` / `sourceId` 等 ID 字段为 Long 序列化的字符串,前端按字符串处理,不要转 number(防精度丢失)。
## 10. 修改前后对比
### 字段级对比
| 项 | 原来 | 现在 |
|----|------|------|
| 入参 `ledgerType` 取值域 | 仅 `SUPPLIER` 合法,其余报 596005 | `SUPPLIER` / `SUPPLIER_RECV` / `CUSTOMER` 合法;`STAFF` 仍报 596005 |
| 出参 `ledgerType` 实际出现值 | 只会是 `SUPPLIER` | 可能出现 `SUPPLIER_RECV` / `CUSTOMER` |
| 出参 `netAmount` 语义 | 只有应付口径:正 = 我欠他 / 负 = 多付他 | 按账套分化:应付口径不变;应收口径正 = 他欠我 / 负 = 多收他 |
| 出参 `openingAmount` 语义 | 期初应付 − 期初应收(单一口径) | 应付账 = 期初应付 − 期初应收;应收账 = 期初应收 − 期初应付(方向反过来) |
### 行为级对比
| 行为 | 原来 | 现在 |
|------|------|------|
| 传 `ledgerType=CUSTOMER` | 报 596005 | 正常返回客户应收台账 |
| 传 `ledgerType=SUPPLIER_RECV` | 报 596005 | 正常返回供应商应收台账 |
| 应收期初可见性 | 期初挂账在台账页完全看不到 | 切到应收账套即可看到期初净额 |
| SUPPLIER 账套行为 | —— | **完全不变**,老页面无感知 |
## 11. 影响评估 / 回滚
- **破坏兼容**:无。存量 `SUPPLIER` 账套的入参 / 出参 / 符号语义零变化,老前端不改也能跑。
- **前端同步上线**:非强制。但若要展示应收侧数据,前端需:
1. 台账页按账套 tab 切换:供应商应付(SUPPLIER)/ 供应商应收(SUPPLIER_RECV)/ 客户应收(CUSTOMER);
2. 正负含义按 §5 对照表分账套判断,负值标红;
3. 应收 tab 下明细为空是预期,建议展示「暂无流水」空态而非报错。
- **回滚方案**:后端回滚 = 恢复原取值域校验(SUPPLIER_RECV/CUSTOMER 重新报 596005);无 DDL、无数据迁移,回滚无副作用。前端若已上 tab,回滚后应收 tab 会收到 596005,需前端同步下掉应收 tab。
## 12. 注意事项
1. **符号判断别偷懒**:`netAmount > 0` 在不同账套含义相反,前端任何「欠款/多付」文案、标红逻辑都必须先按 `ledgerType` 分支,不要写全局统一判断。
2. **应收空明细是正常**:SUPPLIER_RECV / CUSTOMER 本期无业务流水,entries 返回空页、`increaseTotal`/`decreaseTotal` 恒 0,不要当缺陷上报。
3. **STAFF 别放出入口**:员工往来账套未开放,前端不要给 STAFF 的 tab/选项;传了会吃 596005。
4. **ID 按字符串处理**:`refId` / `sourceId` 是 Long 序列化字符串,转 number 会精度丢失。
5. 页签展示名建议:SUPPLIER=供应商应付 / SUPPLIER_RECV=供应商应收 / CUSTOMER=客户应收。
## 13. 关联 / 联系人
- Issue:https://git.1814.love/wx/HL/issues/8719
- PR:https://git.1814.love/wx/HL/pulls/8727
- Commit:https://git.1814.love/wx/HL/commit/029000ef1f
- 后端负责人:@yst
@@ -0,0 +1,208 @@
---
schema: hl-changelog/v2
ticket: "8735"
title: "整团确认订房:无需房户但有残留订房计划改为拒绝,返回 808607"
consumer: admin
author: wx(GIT)
change_type: 修改接口
backend_status: deployed
gateway_status: not_required
frontend_status: not_required
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: ""
updated_at: "2026-10-02"
base: dev-v3
---
# 整团确认订房:无需房户但有残留订房计划改为拒绝,返回 808607
## ⚠️ 关键变化
- **改前**:团期需房户数为 0 时,整团确认(`POST .../room-plans/confirm`)直接走「本团无需订房」豁免,返回 200、`emptyDemand=true`、`hotelReady=true`。这一步**不检查**本团是否还有有效订房计划行。最常见的情形是唯一要住酒店的户退团了,他那几晚已订的房还挂在团上。豁免之后,再没有任何环节会检查这些房。
- **改后**:需房户数为 0、**且**本团仍有有效订房计划行时,不再豁免,返回 `808607`(订房与需求不等),零写入。示例文案:`2028-03-05 的 STANDARD 订房 1 间 / 已确认需求 0 间(共 1 项不等,详见预检)`。
- **出路**:对退团产生的转房行办理「退给酒店」(`cancel-hotel`),或删除该团残留的订房计划行。计划行清空后再点整团确认,返回 200、`emptyDemand=true`。
- **不变**:
- 需房户数 > 0 时的逐日等量校验;
- 全员客人自订(每户每晚都自订)的豁免;
- 既无需房户、也无订房计划行的团,照旧 200 放行。
- **字段零变更**:请求、响应结构都没改,只是多了一种会返回 `808607` 的情形。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 整团确认订房 | POST | `/v3/admin/house/group-batches/{groupBatchId}/room-plans/confirm` | 错误码触发条件扩展 | 无需房户但仍有订房计划行时返回 808607,不再豁免放行 |
## 三、接口详情
### 1. 整团确认订房 `POST /v3/admin/house/group-batches/{groupBatchId}/room-plans/confirm`
**VO**: `GroupBatchRoomConfirmAllRespVO`
#### 使用场景
房务把一个团期每晚的订房计划填好后,点「整团确认」。接口先对整团做一次零写入预检:每个入住日按房型大类核对订房间数与已确认需求是否相等,并检查每晚订房不超过班期最大房间数。预检通过后,逐日确认订房;全部完成后置团期的配房完成标志。
本次只改了一个情形:需房户数为 0 的团,过去一律按「无需订房」放行,现在先看它还有没有订房计划行。
#### 入参
本接口无请求体。
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | 是 | 团期须存在且处于可确认阶段 | 团期主订单 ID |
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| groupBatchId | Long(序列化为字符串) | 团期主订单 ID |
| confirmedDates | `List<String>` | 本次确认的入住日 |
| skippedDates | `List<String>` | 本次之前已全部确认、本次跳过的入住日 |
| emptyDemand | Boolean | `true` 表示全团无需订房(无需房户且无订房计划行,或每户每晚都自订),已直接置配房完成 |
| doneOrderIds | `List<Long>`(元素序列化为字符串) | 本次累计被置为配房完成的子订单 |
| hotelReady | Boolean | 本次结束后团期的配房完成标志 |
| warnings | `List<GroupBatchRoomConfirmWarningVO>` | 逐日告警的并集,本次未改 |
#### 请求示例
```http
POST /v3/admin/house/group-batches/2105995952810815490/room-plans/confirm
```
#### 响应示例
残留订房已经退给酒店、计划行清空之后再确认(测试服实测):
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2105995952810815490",
"confirmedDates": [],
"skippedDates": [],
"emptyDemand": true,
"doneOrderIds": [],
"hotelReady": true,
"warnings": []
},
"traceId": null,
"success": true
}
```
#### 空数据 / 降级响应
既无需房户、也无订房计划行的团(例如两户都不需要住宿),返回与上例相同的结构:`emptyDemand=true`、`confirmedDates` 和 `skippedDates` 都为空数组、`hotelReady=true`。这一点与改前一致。
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2105996666916270081",
"confirmedDates": [],
"skippedDates": [],
"emptyDemand": true,
"doneOrderIds": [],
"hotelReady": true,
"warnings": []
},
"traceId": null,
"success": true
}
```
#### 错误响应
需房户为 0、但仍有订房计划行(本次新增的触发情形,测试服实测):
```json
{
"code": 808607,
"message": "2028-03-05 的 STANDARD 订房 1 间 / 已确认需求 0 间(共 1 项不等,详见预检)",
"data": null,
"traceId": null,
"success": false
}
```
| code | 文案模板 | 何时出现 | 本次变化 |
|------|----------|----------|----------|
| 808607 | `{入住日} 的 {房型大类} 订房 {N} 间 / 已确认需求 {M} 间(共 {K} 项不等,详见预检)` | 某入住日订房与已确认需求不等,报第一个有差额的入住日 | **新增情形**:需房户为 0 且有订房计划行。需房户 > 0 时的原有情形不变 |
| 808610 | `入住日 {日期} 订房 {N} 间,超过班期最大房间数 {上限}` | 某晚订房超过班期最大房间数,在同一天的等量校验之前检查 | 需房户为 0 的团过去直接豁免、碰不到这条;现在残留订房超上限时会先报它 |
| 808631 | `本团仍有 {N} 户住宿需求未填全或未落在团期区间内,不能按「无需订房」置配房完成` | 预检判定「无需订房」后、写库前的复核:期间有户新增了需求 | 不变 |
预检放锁到写库之间存在一个窗口。如果这段时间里有人给这个团新建了订房计划行,写库前的复核同样返回 `808607`,不会放行。其余错误码(如团期阶段不允许确认、无已确认基线)与改前相同,本次没改。
#### 业务边界
- 只拒绝「需房户为 0 且有有效订房计划行」这一种情形。需房户 > 0、全员自订、无需房户且无计划行,这三种情形的行为都与改前相同。
- 拒绝时零写入:不改计划行状态,不置子订单配房完成,也不动团期的 `hotelReady`。
- 想知道差在哪,先调预检 `GET /v3/admin/house/group-batches/{groupBatchId}/room-plans/confirm-check`。在这种团上,它返回 `ready=false`;对应入住日的 `days[].plannedTotal > 0`、`days[].demandedTotal = 0`;`days[].mismatch[]` 逐房型给出 `planned` / `demanded` / `diff`(`diff = 订房 − 需求`,正数表示多订)。
- ⚠️ **不要单凭预检的 `ready=false` 禁用「整团确认」按钮**。对既无需房户、也无计划行的团,预检返回 `ready=false`(`baselineExists=false`、`days=[]`),整团确认却会 200 放行(见上文「空数据」)。这是两接口原有的口径差异,本次未改,测试服已实测。
- 返回 `808607` 时 `data` 为 `null`,错误响应里没有 `confirmedDates` / `skippedDates` 可读。要展示差额,以预检接口为准。
## 四、契约约束与正确调用方式
1. 点「整团确认」前,可以先调 `confirm-check` 拿到 `days[].mismatch`。对需房户为 0 的团,若某天 `plannedTotal > 0`,就提示房务「该团已无需住宿的户,但仍订有 N 间房,请先退给酒店或删除订房计划」。
2. 收到 `808607` 时,原样展示 `message`,并引导房务回到预检看差额。这次拒绝不需要前端回滚任何状态。
3. 退给酒店走房务控制台已有接口 `POST /v3/admin/order/house-console/room-transfers/{id}/cancel-hotel`,入参 `proofFileIds` / `cancelFee` / `remark`,本次未改。
## 五、数据库行为
- 拒绝(`808607` / `808610` / `808631`)时零写入:订房计划行、子订单配房完成状态、团期配房完成标志都保持调用前的值。
- 放行时的写入与改前相同:逐日确认订房计划,置子订单与团期的配房完成。
## 六、边界行为
- 判断「有没有订房计划行」只看有效行。已经退给酒店、被整行收回的计划行不算。测试服实测:两条转房行逐条退给酒店之后,计划行被收回,再确认即 200。
- 预检放锁到写库之间新建的计划行,会在写库前的复核里被拦下(`808607`)。
## 六.6、修改前后对比
| 情形 | 改前 | 改后 |
|------|------|------|
| 需房户 0,无订房计划行 | 200,`emptyDemand=true` | 200,`emptyDemand=true`(不变) |
| 需房户 0,有订房计划行 | 200,`emptyDemand=true`,残留订房无人检查 | `808607`(超班期上限时 `808610`),零写入 |
| 需房户 > 0,订房与需求不等 | `808607` | `808607`(不变) |
| 每户每晚都自订 | 200,`emptyDemand=true` | 200,`emptyDemand=true`(不变) |
## 六.7、影响评估
- 前端:只有当前端把「需房户为 0」的团一律当作可确认、并据此提示「确认成功」时才受影响。按上文第四节处理 `808607` 即可;字段零变更,不改不会报错。
- 数据:本次不迁移、不回溯。改前已经按豁免放行、配房完成为真的团,保持原样。
- 房务操作:残留订房必须先退给酒店或删除,才能整团确认。这一步是本次有意加上的。
## 七、不影响范围
- 预检 `confirm-check` 的字段与判定未改。
- 单日确认、订房计划增删改、转房「退给酒店」等接口未改。
- 无数据库结构变更,无网关路由变更。
## 八、测试环境已验证
测试服 order-v3 已部署本次提交(`fac70310f7`),2026-10-02 走网关实测,数据均为自建测试团:
| 场景 | 结果 |
|------|------|
| 唯一需房户退团,留下 2 晚订房计划,整团确认 | `808607`「2028-03-05 的 STANDARD 订房 1 间 / 已确认需求 0 间(共 1 项不等,详见预检)」;预检 `ready=false`,两天都是 `plannedTotal=1`、`demandedTotal=0` |
| 两户需房、只一户确认了需求,订房 2 间 | `808607`「2028-03-19 的 STANDARD 订房 2 间 / 已确认需求 1 间(共 1 项不等,详见预检)」(原有情形,回归不变) |
| 上面第一个团的两条转房行逐条退给酒店之后,再整团确认 | 200,`emptyDemand=true`,`hotelReady=true` |
| 两户从未提交住宿需求,无订房计划行 | 200,`emptyDemand=true`,`hotelReady=true`;预检 `ready=false`,`days=[]` |
| 一户两晚全部自订、另一户不需住宿 | 200,`emptyDemand=true`,`doneOrderIds` 含自订户子订单 |
## 十、相关文档
- 预检接口:`GET /v3/admin/house/group-batches/{groupBatchId}/room-plans/confirm-check`(响应 `GroupBatchRoomConfirmCheckRespVO`),本次未改。
## 关联 / 联系人
- 工单:wx/HL#8735
- 后端 PR:wx/HL#8736(已合入 dev-v3)
- 后端联系人:wx
@@ -0,0 +1,92 @@
---
schema: "hl-changelog/v2"
ticket: "8578"
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: "接口路径、入参、出参、错误码均不变;变化的是接送机(TRANSFER)用车需求经派车接口建派单行时,接机日、送机日的派单行由后端自动标为接机车、送机车,主路径派齐后接送机门禁可以满足、最终方案可以发布。"
updated_at: "2026-10-03"
base: "dev-v3"
---
# 车务派车:接送机派车行按航班日自动置接送标志
> **服务**: hl-fleet-service
> **关联 Issue**: #8578
> **关联 PR**: #8595(合并提交 0f19f383fa)
> **部署状态**: 测试环境已部署并经网关实测,见「五、实测验证」
> **影响范围**: 管理后台车务派车(单派 `POST /admin/fleet/assignments`、批派 `POST /admin/fleet/assignments/batch`)之后的接送机门禁与最终方案发布结果——不改任何请求或响应字段
---
## ⚠️ 关键变化
- **以前**:派车接口建派单行时,接送标志(接机车 / 送机车)一律写 0。接送机订单只走「派车 → 确认」主路径时,接送机门禁结构性无法满足,批派响应恒为 `finalPlanPublished=false`、`finalPlanNotPublishedReason=GATE_UNSATISFIED`,只能再去第③步「接送机配置」手工勾选才放行。
- **现在**:接送机(TRANSFER)用车需求的派单行,服务日是大交通声明的接机日则自动标为接机车,是送机日则自动标为送机车。接机日、送机日都派了车,门禁即满足,最终方案在派车这一步就能发布。
- **前端无需改代码**:请求体、响应结构、错误码都没变。
---
## 一、背景
接送机门禁要求大交通声明的每个接机日都有接机车、每个送机日都有送机车。门禁读的是派单行上的接送标志,而派车接口建行时从不置这两个标志,所以门禁在主路径上永远不满足,最终方案永远不发布。
---
## 二、后端改动
- 建派单行的入口 `AssignmentService#doCreateInLock` 在逐日建行时,按该行服务日置接送标志。单派与批派都经过这个入口。
- 接机日、送机日两个日期集直接取自门禁判定用的 `PickupDropoffGateResolver`,建行口径与门禁口径同源,不会出现「建行认为是接机车、门禁不认」的分叉。
- 只对 TRANSFER 用车需求自动置位。TRAVEL(旅游)用车需求的某个行程日不一定是航班日,系统不做推断,派单行仍写 0,与改前一致。
---
## 三、对前端的影响
### 可观察到的行为变化
- 接送机订单在派车向导里把接机日、送机日都派上车后,批派响应 `finalPlanPublished=true`,`pickupDropoffGate.satisfied=true`,不需要再进第③步手工勾选。
- 只派了接机日、没派送机日时,门禁按设计仍不满足:`finalPlanPublished=false`、`finalPlanNotPublishedReason=GATE_UNSATISFIED`,缺的日期列在 `pickupDropoffGate.missingDropoffDates`(或 `missingPickupDates`)里。补派缺的那一天即可。
- 订单「确认核对清单」的用车项(`VEHICLE_DONE`)随门禁满足变为通过。
### 字段口径(均未变化)
批派响应的 `finalPlanPublished`、`finalPlanNotPublishedReason`、`pickupDropoffGate`(`arrivalRequiredDates`、`departureRequiredDates`、`missingPickupDates`、`missingDropoffDates`、`declared`、`satisfied`)都是既有字段,含义不变。
---
## 四、覆盖范围与边界
- **只影响修复部署后新建的派单行**。已经存在的派单行,接送标志保持原值。需要改的,仍在第③步用 `PUT /admin/fleet/assignments/pickup-dropoff-config` 调整。该接口行为不变:在门禁已满足的订单上按相同取值再提交一次,返回成功,`finalPlanPublished=true`,`requirementReopened=false`。
- **TRAVEL 订单零变化**:无接送机声明时 `pickupDropoffGate.declared=false`、`satisfied=true`,最终方案照常发布。
- **派车弹窗两种进入方式提交的日期范围不同**:按日格进入只提交该日,按车辆槽位统一选择提交整段日期(hl-ui `v2.1` 分支 `AssignModal.vue`)。只按日格派了接机日时,门禁显示缺送机日属于正常结果,不是前端提交形状的缺陷。
---
## 五、实测验证(测试环境)
2026-10-03,测试服 hl-fleet-service 部署提交 `89c87fd21`(已包含修复提交 `0f19f383fa`),经网关实测:
| 场景 | 结果 |
|------|------|
| 接送机订单(接机日 2027-05-10、送机日 2027-05-15),只走「派车 → 确认」主路径,不调接送机配置 | 批派响应 `finalPlanPublished=true`,`pickupDropoffGate.satisfied=true`,`missingPickupDates`、`missingDropoffDates` 均为空 |
| 同一订单 `GET /v3/admin/order/{orderId}/confirm-checklist` | `VEHICLE_DONE` 为 `passed=true` |
| 阴性对照:只派接机日(2027-06-10),没派送机日(2027-06-16) | `finalPlanPublished=false`,`finalPlanNotPublishedReason=GATE_UNSATISFIED`,`missingDropoffDates=["2027-06-16"]` |
| 门禁已满足后,按相同取值再调 `PUT /admin/fleet/assignments/pickup-dropoff-config` | `finalPlanPublished=true`,`requirementReopened=false` |
| TRAVEL 订单同批次对照 | `pickupDropoffGate.declared=false`、`satisfied=true`,`finalPlanPublished=true` |
---
## 关联
- Issue: [wx/HL#8578](https://git.1814.love/wx/HL/issues/8578)
- PR: [wx/HL#8595](https://git.1814.love/wx/HL/pulls/8595)
- 后端:@wx
@@ -0,0 +1,606 @@
---
schema: hl-changelog/v2
ticket: "8666"
title: "产品班期改出发日联动团期与子单日期,有房务/车务占用时拒绝改期"
consumer: admin
author: wx(GIT)
change_type: 修改接口
backend_status: deployed
gateway_status: not_required
frontend_status: not_required
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: ""
updated_at: "2026-10-03"
base: dev-v3
---
# 产品班期改出发日联动团期与子单日期,有房务/车务占用时拒绝改期
## ⚠️ 关键变化
- **改前**:product-v2 改班期出发日只更新 `product_schedule` 自己这张表;已挂在该班期上的团期(`group_batch`)与其子单(出发/结束日、行程 `day_date`)保持旧日期不变,两边从此永久分叉——房务按旧日期配房、车务按旧日期派车,没有任何环节会发现或修复这个分叉。
- **改后**:**仅当出发日相对库里旧值发生变化**时,`PUT /admin/product/item/{id}/schedule` 落库前先向订单域预检能否联动改期;被拒返回 `410206`(团期已成团/已排房/已确认用车,具体原因拼进 message),预检服务不可达返回 `410207`(fail-closed,不放行)。预检通过、本地保存成功后,后端在同一次请求内 best-effort 推送联动,把团期与全部活跃子单的三个日期、行程 `day_date`、两级餐食日期一并平移;这一步**不是**前端能单独调用的接口,响应里也不会多出任何字段体现联动结果。
- 只按出发日判断是否预检:只改结束日(产品行程天数变化连带)或只改报名截止日,都不会触发预检、不会拒绝保存,这两点与改前一致。
- 新增 3 处连带行为变化,都是同一把"团期改期联动锁"或同一个车务栅栏探针改动带出来的:
1. 团期子单转期 `transfer-in`:车务栅栏探针遇死锁(MySQL 1213)/锁等待超时(MySQL 1205),改前会被探针自身的异常处理整体吞掉、误判为"占用中"返回 `589576`(文案把人引向一个并不存在的用车确认占用);改后探针只对锁竞争类异常原样上抛,由 `transfer-in` 新增的 catch 转译为 `100503`,不再误报 `589576`。
2. 流团解散审批通过:批量处理子订单时,单户车务栅栏探针撞死锁/锁等待超时不再记"未处理"继续跑完其余户,而是让异常原样上抛,被审批事务既有的锁竞争转换逻辑接住,整笔回滚、返回 `100503`(该转换逻辑本身早于本次改动存在,本次变化的只是"探针异常不再被每户循环吞掉、能传导到这层转换";栅栏"占用中但未撞锁"的情形仍按原逻辑记"未处理")。
3. 创建订单:带 `productBatchId` 的创单与该团期当前的改期联动共用一把分布式锁(键 `order:gb:schedule-sync:{productBatchId}`,与改期联动 apply/replay 用的是同一把锁),取锁等待 5s,超时返回 `100503`;本条目覆盖管理端 `POST /v3/admin/order`。不带 `productBatchId` 的创单不受影响。
- 订单时间线、团期时间线各新增若干枚举值,均通过已有的只读接口返回,接口路径与响应结构未变(见"六.5 枚举")。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 修改班期 | PUT | `/admin/product/item/{id}/schedule` | 新增错误码触发条件 | 出发日变化时先问订单域能否联动改期,拒绝返回 410206,预检不可达返回 410207 |
| 2 | 团期子单转期 | POST | `/v3/admin/order/group-batch/{groupBatchId}/sub-order/{orderId}/transfer-in` | 错误响应变化 | 车务栅栏探针撞死锁/锁等待超时,由误报 589576 改为 100503 |
| 3 | 流团解散审批通过 | POST | `/v3/admin/order/group-batch/disband/{approvalId}/approve` | 错误响应变化 | 单户车务栅栏探针撞死锁/锁等待超时不再记未处理,整笔回滚返回 100503 |
| 4 | 创建订单 | POST | `/v3/admin/order` | 新增可能的错误响应 | 带团期的创单与该团期改期联动共用锁,等锁超过 5s 返回 100503 |
## 三、接口详情
### 1. 修改班期 `PUT /admin/product/item/{id}/schedule`
**VO**: `ScheduleSaveReqVO → Result<Long>`
#### 使用场景
产品运营在"产品管理 - 班期"页编辑已有班期。本次改动只影响"编辑且出发日相对库里旧值变化"这一种提交:新建班期、不改出发日的保存都不受影响。提交后,若该班期已挂团期且团期处于已成团/已排房/已确认用车等不可平移的阶段,保存会被拒绝;可平移时,保存成功后团期与子单日期会在同一次请求内由后端联动平移,前端无需也无法单独触发这一步。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | 是 | 产品须存在 | 产品 ID,后端写入请求体 productId 字段 |
| batchId | Body | Long | 否 | 修改时必传,新建不传 | 班期 ID |
| batchName | Body | String | 否 | - | 班期名称 |
| departureDate | Body | LocalDate | 是 | 非空 | 出发日期;本次改动只看这个字段相对库里旧值是否变化 |
| enrollmentDeadline | Body | LocalDate | 否 | - | 报名截止日;后端恒按"出发日−1 天"重算覆盖,入参不生效 |
| adultPrice | Body | BigDecimal | 否 | - | 成人价 |
| childPrice | Body | BigDecimal | 否 | - | 儿童价 |
| toddlerDiscount | Body | BigDecimal | 否 | - | 小童优惠额 |
| infantPrice | Body | BigDecimal | 否 | - | 幼童价 |
| singleRoomDiff | Body | BigDecimal | 是 | ≥0 | 单房差 |
| maxParticipants | Body | Integer | 否 | - | 最大参与人数(空或 0=不限) |
| maxRooms | Body | Integer | 否 | - | 总房间数(0=不限) |
| minToForm | Body | Integer | 否 | - | 最低成团户数 |
| minParticipants | Body | Integer | 否 | - | 最低成团人数 |
| manualOrderCount | Body | Integer | 否 | ≥0 | 运营手动线下占位房数 |
| manualParticipantCount | Body | Integer | 否 | ≥0 | 运营手动线下报名人数 |
| remark | Body | String | 否 | - | 备注 |
本次字段零变更,以上为完整字段表,与改前相同。
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| data | Long | 班期 ID(`Result<Long>` 的 `data`),字段结构未变 |
#### 请求示例
```json
{
"batchId": 1780203456789012345,
"batchName": "元旦特别团",
"departureDate": "2027-01-27",
"enrollmentDeadline": "2027-01-26",
"singleRoomDiff": 200,
"maxRooms": 15,
"maxParticipants": 30
}
```
#### 响应示例
出发日变化且订单域放行:
```json
{
"code": 200,
"message": "成功",
"data": 1780203456789012345,
"traceId": null,
"success": true
}
```
#### 空数据 / 降级响应
出发日未变化(只改价格/房量/备注等字段)时不触发预检,直接按原逻辑保存,返回结构与上例相同;本接口不存在"空数据"形态。
#### 错误响应
出发日变化,但团期当前阶段不可平移(订单域阶段门/逐户资产门拒绝):
```json
{
"code": 410206,
"message": "该班期已成团或已排房/已确认用车,不允许修改出发日期,请新建班期后逐户转期(团期已有房务计划或分房(3 行),不允许随产品班期改期)",
"data": null,
"traceId": null,
"success": false
}
```
出发日变化,但订单域预检不可达(Feign 异常、响应非成功或 data 为空):
```json
{
"code": 410207,
"message": "订单服务暂不可用,无法确认该班期能否改期,请稍后重试",
"data": null,
"traceId": null,
"success": false
}
```
| code | 何时出现 |
|------|----------|
| 410206 | 出发日变化,订单域按阶段门/逐户资产门拒绝联动(589800~589803 之一,具体原因拼进 message 末尾括号;拿不到具体原因文案时退化为拒绝码数字) |
| 410207 | 出发日变化,订单域 Feign 调用异常、响应非成功或返回体为空,一律按不可用拒绝(fail-closed,不放行);订单域参数校验失败(`589504`,如 productBatchId 缺失/非法)也归在"响应非成功"之列,前端只会看到 `410207`,日志里出现 `589504` 不代表订单服务整体不可用 |
#### 业务边界
- 只有"编辑已有班期且出发日相对库里旧值变化"才会触发预检;新建班期、不改出发日的任何保存都不受影响,与改前行为一致。
- `410206`/`410207` 都在本地落库之前判定,命中时班期本身**零写入**,保持拒绝前的值。
- 结束日由产品行程天数(`tripDays`)每次保存自动重算,与出发日是否变化无关;只改结束日(出发日不变)不触发预检,否则已成团班期连改价都会被拒绝。报名截止日恒为出发日前一天,随出发日派生,自身不单独判断。
- 预检通过、本地保存成功之后,团期与子单的实际日期平移由后端在同一次请求内 best-effort 执行:失败只记日志,不影响本次请求的 200 响应。前端不应把保存成功等同于团期/子单日期已同步完成,需要确认时应读团期详情或团期时间线核实实际日期(见"六.5 枚举")。
- `POST /admin/product/item/{id}/schedule`(创建班期)与本接口共用同一个后端处理方法,是否按"编辑"处理只看请求体里 `batchId` 是否非空、与 HTTP 方法无关;调用方若误用 POST 但携带了 `batchId`,同样会触发本次改动的预检与联动,不是只有 PUT 才会命中。
- 班期状态重算若抛异常,该异常会原样上抛给调用方(本次保存请求整体失败);但此时班期本地保存与改期联动推送已经在这之前/同一收尾阶段 best-effort 执行完毕——联动可能已经生效,前端不应据"这次保存请求失败"反推"联动没有发生"。
- 车务栅栏占用(`589804`)不参与预检判断,只会在预检通过、本地保存成功后的联动平移阶段触发;命中时该次平移 best-effort 失败、只记日志,不会体现在本次保存请求的响应里,也不会让保存请求本身失败。
### 2. 团期子单转期 `POST /v3/admin/order/group-batch/{groupBatchId}/sub-order/{orderId}/transfer-in`
**VO**: `TransferSubOrderReqVO → TransferSubOrderRespVO`
#### 使用场景
房务/车务在团期控制台把某子单从一个团期转入当前团期(路径里的 `groupBatchId` 是转入的目标团期)。本次改动只涉及转期过程中车务栅栏探针这一步的异常处理,请求/响应字段未变。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | 是 | 目标团期须存在 | 转入的目标团期 ID |
| orderId | Path | Long | 是 | 子订单须存在且当前不在该团期 | 要转期的子订单 ID |
| fromGroupBatchId | Body | Long | 是 | 须与子订单当前所在团期一致 | 源团期 ID |
| reason | Body | String | 否 | ≤512 | 转期原因 |
| notify | Body | Boolean | 否 | 默认 true | 是否通知客户 |
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| orderId | String | 子订单 ID |
| teamNo | String | 团号 |
| fromBatchNo | String | 源团期编号 |
| toBatchNo | String | 目标团期编号 |
| oldOrderAmount | BigDecimal | 转期前订单金额 |
| newOrderAmount | BigDecimal | 转期后按目标团期重算的订单金额 |
| paidAmount | BigDecimal | 已付金额 |
| newBalanceDue | BigDecimal | 转期后应补差额 |
| warnings | `List<String>` | 告警文案 |
#### 请求示例
```json
{
"fromGroupBatchId": 2106001234567890123,
"reason": "客户要求换期",
"notify": true
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "1900000000000000456",
"teamNo": "27-0128",
"fromBatchNo": "GB20270120001",
"toBatchNo": "GB20270127002",
"oldOrderAmount": 6600.00,
"newOrderAmount": 6800.00,
"paidAmount": 2000.00,
"newBalanceDue": 4800.00,
"warnings": []
},
"traceId": null,
"success": true
}
```
#### 空数据 / 降级响应
本接口无列表/分页语义,不存在空数据形态;`warnings` 为空数组表示转期过程无需特别提示,是正常情况,不是降级。
#### 错误响应
车务栅栏探针遇死锁或锁等待超时(本次新增的触发路径):
```json
{
"code": 100503,
"message": "资源被占用,请稍后重试",
"data": null,
"traceId": null,
"success": false
}
```
#### 业务边界
- 该子订单的用车需求正在最终确认占用期内(非锁竞争、单纯占用中)仍返回原有的 `589576`「该子订单的用车需求正在最终确认中,请等确认完成或释放占用后再转期」,不受本次影响。
- `100503` 只在车务栅栏探针这一步真的撞上数据库死锁(MySQL 1213)或锁等待超时(MySQL 1205)时出现,属可重试错误;建议前端按"资源被占用,请稍后重试"提示,并允许用户直接重新提交,不需要额外的状态回滚操作。
- 改前同样的锁竞争场景不会报错,而是被探针自身的异常处理吞掉、误判为"占用中"返回 `589576`(文案「等确认完成或释放占用后再转期」会把人引向一个并不存在的用车确认占用);改后锁竞争类异常原样上抛并转译为 `100503`,不再误报 `589576`。若前端曾对 `589576` 做过"提示稍后手动重试"之外的特殊处理,需要确认该处理在锁竞争场景下(现为 `100503`)是否仍然合适,两者文案与含义不同,不应合并成同一套处理分支。
### 3. 流团解散审批通过 `POST /v3/admin/order/group-batch/disband/{approvalId}/approve`
**VO**: `ApproveDisbandReqVO → DisbandApprovalRespVO`
#### 使用场景
团期管理员对一条流团解散申请执行"通过"操作。通过后批量处理该团期下所有子订单的解散(含用车需求取消)。本次改动只涉及其中某一户子订单车务栅栏探针撞锁时的异常处理路径,不改请求/响应字段。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| approvalId | Path | Long | 是 | 审批单须存在且可审批 | 解散审批单 ID |
| remark | Body | String | 否 | ≤512 | 审批备注 |
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| approvalId | String | 审批单 ID |
| groupBatchId | String | 团期 ID |
| batchNo | String | 团期编号 |
| batchName | String | 团期名称 |
| batchLabel | String | 团期展示标签 |
| approvalStatus | String | 审批状态枚举 |
| approvalStatusName | String | 审批状态中文名 |
| reason | String | 申请原因 |
| affectedOrderCount | Integer | 受影响子订单数 |
| participantCount | Integer | 受影响人数 |
| estimatedRefundAmount | BigDecimal | 预估退款金额 |
| actualRefundAmount | BigDecimal | 实际退款金额 |
| applicantId | String | 申请人 ID |
| applicantName | String | 申请人姓名 |
| ccUserNames | `List<String>` | 抄送人姓名 |
| approvedById | String | 审批人 ID |
| approvedByName | String | 审批人姓名 |
| approvedAt | LocalDateTime | 审批时间 |
| approveRemark | String | 审批备注 |
| createTime | LocalDateTime | 申请时间 |
| canApprove | Boolean | 当前是否可审批 |
#### 请求示例
```json
{
"remark": "核实无误,予以通过"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"approvalId": "2106100000000000111",
"groupBatchId": "2106001234567890123",
"batchNo": "GB20270120001",
"batchName": "元旦团",
"batchLabel": "元旦团 · 2027-01-20",
"approvalStatus": "APPROVED",
"approvalStatusName": "已通过",
"reason": "成团人数不足",
"affectedOrderCount": 5,
"participantCount": 11,
"estimatedRefundAmount": 28600.00,
"actualRefundAmount": 28600.00,
"applicantId": "50001234567890",
"applicantName": "李四",
"ccUserNames": [],
"approvedById": "50009876543210",
"approvedByName": "王五",
"approvedAt": "2027-01-18T10:20:00",
"approveRemark": "核实无误,予以通过",
"createTime": "2027-01-17T09:00:00",
"canApprove": false
},
"traceId": null,
"success": true
}
```
#### 空数据 / 降级响应
本接口不存在空数据形态;审批单不存在或不可审批时走原有错误响应(本次未改)。
#### 错误响应
批量处理子订单时,某户车务栅栏探针撞上数据库死锁或锁等待超时(本次新增的触发路径):
```json
{
"code": 100503,
"message": "资源被占用,请稍后重试",
"data": null,
"traceId": null,
"success": false
}
```
#### 业务边界
- **改前**:批量解散逐户处理时,若某户车务栅栏探针抛出异常(含死锁/锁等待超时),该户记入"未处理"清单、不中断其余户,整单仍可能返回 200。
- **改后**:探针遇死锁(MySQL 1213)或锁等待超时(MySQL 1205)会原样向上抛出,不再被当成"该户处理失败"吞掉;批量处理与审批通过共享同一个数据库事务,异常会导致整个审批通过操作回滚,整单返回 `100503`,所有户的解散都不生效,需要重新发起"通过"。
- 该子订单用车需求正在最终确认占用期内(非锁竞争、单纯占用中)仍走原有的"未处理"记录逻辑,不触发整单回滚,这一点未变。
- `100503` 可重试:锁竞争是瞬时状态,重新提交"通过"通常可以成功。
### 4. 创建订单 `POST /v3/admin/order`
**VO**: `OrderCreateReqVO → OrderCreateRespVO`
#### 使用场景
管理端新建订单(含团期订单与自由出团订单)。本次改动只影响带 `productBatchId` 的团期订单:创单与该团期当前正在进行的改期联动共享同一把锁,若锁被占用会短暂等待而非立即报错。不带 `productBatchId` 的订单创建不受影响。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| productId | Body | Long | 是 | - | 产品 ID |
| tierSeq | Body | Integer | 是 | - | 档位序号 |
| departureDate | Body | LocalDate | 是 | - | 出发日期 |
| adultCount | Body | Integer | 是 | ≥1 | 成人数 |
| childCount | Body | Integer | 否 | ≥0,默认 0 | 儿童数(6-12岁) |
| youngChildCount | Body | Integer | 否 | ≥0,默认 0 | 幼童数(2-5岁,占座不占床) |
| babyCount | Body | Integer | 否 | ≥0,默认 0 | 婴儿数(0-1岁,不占座不占床) |
| customerName | Body | String | 是 | - | 客户姓名 |
| customerPhone | Body | String | 是 | 手机号格式 `^1[3-9]\d{9}$` | 客户手机 |
| customerRemark | Body | String | 否 | ≤500 | 客户备注 |
| createSource | Body | String | 否 | ≤20,不传默认 `CONSULTANT` | 创建来源枚举 |
| productBatchId | Body | Long | 否 | GROUP 产品必传,自由出团为空 | 产品侧团期 batchId;**本次改动唯一相关字段**,非空时创单会与该团期改期联动共用锁 |
| roomCount | Body | Integer | 否 | ≥1 | 房间数 |
| tags | Body | `List<String>` | 否 | - | 订单标签名列表 |
| sharerOpenid | Body | String | 否 | - | 分享人 openid |
| customizerId | Body | Long | 否 | - | 分享归因 customizerId |
本次字段零变更,以上为完整字段表,与改前相同。
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String | 订单主键 |
| orderNo | String | 订单号 |
| teamNo | String | 团号 |
| orderStatus | String | 粗状态枚举(创单后 = `PENDING_PAY`) |
| orderStatusName | String | 粗状态中文名 |
| flowStatus | String | 细状态枚举值 |
| flowStatusName | String | 细状态中文名 |
| flowStep | Integer | 线性 6 步当前步序号 |
| flowStepTotal | Integer | 线性 6 步总步数 |
| flowStepName | String | 当前步中文名 |
| consultantId | String | 实际绑定的定制师 ID |
| consultantSource | String | 定制师来源 |
| tags | `List<String>` | 系统自动打的标签 |
| createdAt | LocalDateTime | 创单时间 |
| productName | String | 产品名称 |
| tierName | String | 档位名 |
| groupBatchName | String | 创单响应不回填,恒为 null(该字段只在订单列表行里赋值),判团/展示不要读它 |
| departureDate | LocalDate | 出发日 |
| returnDate | LocalDate | 返团日 |
| totalAmount | String | 订单总价 |
| depositAmount | String | 建议定金金额 |
| depositRatio | Integer | 定金比例百分比 |
| depositMode | String | 定金计算模式(FIXED/RATIO/FULL) |
| paymentMode | String | 支付模式(DEPOSIT/FULL) |
| expiryMinutes | Integer | 支付时限分钟数 |
| payUrl | String | 支付页绝对 URL |
| customerName | String | 客户姓名(回显) |
| groupBatchId | String | 运营团期 ID;非空=团订单,判团唯一字段 |
| productBatchId | String | 团期产品排期 ID,仅供溯源,不参与判团 |
| groupOrder | Boolean | 是否团订单(= groupBatchId 非空的派生值) |
#### 请求示例
```json
{
"productId": 30001234567890,
"tierSeq": 1,
"departureDate": "2027-01-27",
"adultCount": 2,
"customerName": "张三",
"customerPhone": "13800002046",
"productBatchId": 80001234567890123
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "60123456789015",
"orderNo": "HL20270127143025001",
"teamNo": "27-0128",
"orderStatus": "PENDING_PAY",
"orderStatusName": "待支付",
"flowStatus": "AWAITING_PAY",
"flowStatusName": "待支付",
"flowStep": 0,
"flowStepTotal": 6,
"flowStepName": "待支付",
"tags": [],
"createdAt": "2027-01-18T14:30:25",
"productName": "元旦亲子营",
"tierName": "经典档",
"groupBatchName": null,
"departureDate": "2027-01-27",
"returnDate": "2027-02-01",
"totalAmount": "6800.00",
"depositAmount": "2000.00",
"depositRatio": null,
"depositMode": "FIXED",
"paymentMode": "DEPOSIT",
"expiryMinutes": 1440,
"payUrl": "https://pay.hulalv.com/pay/HL20270127143025001",
"customerName": "张三",
"groupBatchId": "70123456789012345",
"productBatchId": "80001234567890123",
"groupOrder": true
},
"traceId": null,
"success": true
}
```
#### 空数据 / 降级响应
本接口不存在空数据/降级形态,创建失败一律走错误响应。
#### 错误响应
带团期创单时,与该团期的改期联动撞锁、等待 5s 仍未拿到锁(本次新增的触发路径):
```json
{
"code": 100503,
"message": "资源被占用,请稍后重试",
"data": null,
"traceId": null,
"success": false
}
```
#### 业务边界
- 只有 `productBatchId` 非空的创单才会与改期联动共用锁;自由出团(`productBatchId` 为空)不受影响,不会因为别的团期正在改期而变慢或报错。
- 锁是 Redis 分布式锁(键 `order:gb:schedule-sync:{productBatchId}`),与改期联动 apply/replay 用的是同一把锁、同一键空间;取锁等待上限 5s,正常情况下一次改期联动的持锁时间远小于此(apply 在锁内平移整团子单,测试服 12 个样本 apply 段 41~688ms),`100503` 只在极端并发窗口出现,属可重试错误。
## 四、契约约束与正确调用方式
1. `PUT /admin/product/item/{id}/schedule` 只有"编辑已有班期 + 出发日相对库里旧值变化"才会触发 `410206`/`410207`;前端不需要自行预判是否会触发,直接提交、按错误码分支处理即可。
2. 收到 `410206` 时,`message` 已经把具体拒绝原因(成团状态/房务计划行数/用车需求/排程子订单)拼进括号内,可直接原样展示,不需要再调用别的接口获取详情。
3. 收到 `410207` 时按"订单服务暂不可用"提示;这是本地保存前的拒绝,班期本身零写入,可直接引导用户重新提交保存。
4. `100503`(四个接口共用同一个错误码)统一按"资源被占用,请稍后重试"处理:允许用户直接重新提交原请求,不需要任何额外的状态清理或回滚操作。
5. 创建订单时,若明确是团期订单(填了 `productBatchId`),建议前端对 `100503` 与其他失败做区分提示(如"该团期正在改期,请稍后重试"),不要和参数校验类错误混在一起展示。
## 五、数据库行为
- `PUT /admin/product/item/{id}/schedule` 命中 `410206`/`410207` 时:班期本身零写入,保持拒绝前的值;订单域的团期、子单、房间计划、用车需求均不受影响。
- `PUT /admin/product/item/{id}/schedule` 预检通过、本地保存成功且联动平移成功时:团期出发日/结束日/报名截止日、全部活跃子单的出发/结束日、行程 `day_date`,以及团期与子单两级的餐食日期会一并更新;子单自身的行程天数(`trip_days`/`trip_nights`)不参与本次联动,改前改后均不受影响。只要有户被平移或团期出发日本身变化,团期的"需求已确认"标志会被重新打开(复用既有事件 `BATCH_REQUIREMENT_REOPENED`,仅在真实发生 1→0 翻转时落时间线)、"配房完成"标志会被置回未完成;仅结束日变化、或日期已对齐只差餐食日期的"仅对齐餐食"命中,都不会触发这两个标志重置。联动写事务失败(含锁竞争)不会部分写入——要么该团期全部平移,要么该团期一个字段都不变。
- 逐户用车需求随子单日期平移重基准,只处理 `TRAVEL`(行程用车)类需求;`TRANSFER`(接送机)类需求不受联动影响,不会跟着日期平移。
- `transfer-in` / 流团解散审批通过命中 `100503` 时:本次请求涉及的写入全部回滚,不产生部分写入。
- 创建订单命中 `100503` 时:订单主体与相关子表零写入。
## 六、边界行为
- 改期联动推送是后端在同一次请求内 best-effort 执行的:失败只记日志,不影响本次保存请求的 200 响应。前端不应把保存成功等同于团期/子单日期已同步完成,需要确认时应读团期详情或团期时间线核实实际日期。
- `100503` 在四个接口里都是"可重试"语义:锁等待超时或数据库死锁都是瞬时状态,不代表请求本身非法。
- `transfer-in`/流团解散审批里,车务栅栏"正在最终确认占用中但未撞锁"的情形,仍分别返回原有的 `589576`(转期)或记为"未处理"(解散审批),不受本次改动影响。
## 六.5 枚举
### 订单时间线新增事件类型(`OrderLogEventType`)
**所属字段**:`GET /v3/admin/order/{id}/status-log` 响应数组元素的 `eventType`(机器码)/ `title`(中文标签) | **类型**:`String`
| 值 | 中文 | 说明 |
|----|------|------|
| `GROUP_BATCH_RESCHEDULE` | 团期改期 | 产品班期出发日联动改期成功平移了该订单的日期/行程/餐食时写入;内容示例:"出发日 2027-01-26→2027-01-27,返程日 2027-01-31→2027-02-01,餐食日期随之平移 3 条(产品班期改期联动)"。操作人归属:请求携带操作人信息(产品侧保存班期时实际触发该次保存的管理员)且非运维重推时记为该管理员;运维重推或缺少操作人信息时记为系统(SYSTEM);运维重推时内容末尾的括号变为"(产品班期改期联动,运维重推)"。 |
### 团期时间线新增事件类型(`GroupBatchLogEventType`)
**所属字段**:`GET /v3/admin/order/group-batch/{groupBatchId}/status-logs` 响应数组元素的 `eventType`(机器码)/ `eventTypeName`(中文标签,历史数据可能为 null,见下方业务边界) | **类型**:`String`
| 值 | 中文 | 说明 |
|----|------|------|
| `BATCH_SCHEDULE_SYNCED` | 班期改期联动 | 团期与子单联动平移成功时写入;内容示例:"产品班期改期联动:出发日 2027-01-26→2027-01-27,结束日 2027-01-31→2027-02-01,报名截止 2027-01-25→2027-01-26;联动子单 3 户;餐食日期平移 团期 1 条、子单 0 条"。团期与子单日期已一致、只差餐食日期未对齐时同样写本事件(不是另开新事件),内容改为例如"产品班期改期联动:团期与子单日期已对齐,仅团期餐食日期对齐到出发日 2027-01-27 共 2 条";若连餐食也已对齐则是幂等命中,不写任何时间线。 |
| `BATCH_SCHEDULE_SYNC_REJECTED` | 班期改期联动被拒 | 产品已改期但团期当时不在可联动阶段(589800~589804 之一)时写入,独立事务落库,不随业务异常一起回滚;内容示例:"产品班期改期联动被拒:团期当前状态为「已成团」,已成团,不允许随产品班期改期" |
| `BATCH_SCHEDULE_DRIFT_DETECTED` | 班期日期漂移 | 对账发现团期行日期与产品实时日期/活跃子单出发日/房间计划日期不一致时写入(只读检测,不改数据);只差返程日不写本事件,仅打日志。由每日定频对账任务驱动(每天 03:30,与房控对账 03:00 错峰),内容与同事件最新一条记录完全相同时跳过写入,不重复刷屏。 |
以上三个事件的 `BATCH_SCHEDULE_SYNCED`/`BATCH_SCHEDULE_SYNC_REJECTED` 两类运维重推(非前端可触发)内容前缀都会多一句"(运维重推)"。需求重开事件 `BATCH_REQUIREMENT_REOPENED`(枚举值本身非本次新增)新增一个触发来源:改期联动平移了至少一户或团期出发日本身变化时会复用该事件把团期"需求已确认"标志重新打开,`extra.resourceType` 为 `SCHEDULE_SYNC`,可用于区分定制师手动修改需求(原有来源)与改期联动触发(本次新增来源)。
以上两个接口均为既有只读接口,路径与响应结构未变,仅新增可能出现的枚举值。
#### 业务边界
- `GroupBatchStatusLogItemVO.eventTypeName` 遇到历史数据里不在当前枚举范围的值时会是 `null`,`eventType` 仍会原样返回机器码;前端应直接使用后端给的 `eventTypeName`/`title`,不要自行维护一套编码→文案映射表,新增枚举值不需要前端改代码即可正常显示。
- 与团期时间线不同,订单时间线(`OrderLogEventType`)遇到不在枚举范围内的历史 `eventType` 时,`title` 会兜底返回原始机器码字符串而不是 `null`——两个时间线接口对"未知事件"的兜底方式不同,前端渲染逻辑不能直接复用同一套判空逻辑。
## 六.6、修改前后对比
### 字段级对比
本次四个接口请求/响应字段均**零变更**,只是新增了触发条件或新增了可能返回的错误码。
### 行为级对比
| 接口 | 改前 | 改后 |
|------|------|------|
| 修改班期(出发日变化) | 只改 `product_schedule`,已挂团期/子单日期永久不同步 | 落库前先问订单域能否联动;可以则保存后 best-effort 联动平移团期+子单+餐食日期,不可以则拒绝(`410206`/`410207`),班期零写入 |
| 团期子单转期 | 车务栅栏探针撞死锁/锁等待超时 → 被探针自身吞掉、误判为占用中,返回 `589576` | 同样场景 → 原样上抛转译为 `100503`,可重试,不再误报 `589576` |
| 流团解散审批通过 | 单户车务栅栏探针撞锁 → 记"未处理",继续处理其余户,整单可能 200 | 单户撞死锁/锁等待超时 → 异常原样上抛,整单回滚,返回 `100503` |
| 创建订单(带团期) | 与改期联动无互斥,可能拿到改期中途的旧日期 | 与改期联动共用锁,等锁 5s,超时返回 `100503` |
## 六.7、影响评估
- **是否破坏向后兼容**:否。四个接口请求字段均未变;`410206`/`410207` 是此前不会触发的全新拒绝路径。`100503` 对 `transfer-in`/创建订单两个接口是全新可达路径(改前分别误报 `589576`、或根本没有任何锁竞争风险);对流团解散审批通过,`100503` 所用的锁竞争转换逻辑本身早于本次改动存在,本次新增的只是"单户探针异常不再被吞掉、能传导到这层转换"这条此前不可达的路径。正常放行路径的请求/响应结构均不变。
- **前端是否必须同步上线**:否。不处理新增错误码时,命中场景会退化为展示原始 `message` 文案(通用错误提示兜底),不会白屏或崩溃;但改前这两个接口撞锁时分别表现为 `transfer-in` 返回 `589576`(引导用户"等用车确认完成")、流团解散审批通过记"未处理"并可能返回成功,若前端曾针对这两种旧表现做过特殊处理,建议补上对 `100503` 的识别并给出"稍后重试"引导。
- **前端 workaround 清理点**:无。
## 七、不影响范围
- 不带 `productBatchId` 的创单(自由出团)完全不受影响。
- 班期保存时只改出发日之外的字段(价格、房量、备注等),或新建班期,不触发预检,行为与改前相同。
- `transfer-in` 除车务栅栏撞锁这一条路径外的其他错误码(`589575`/`589576`/`589577`/转期前置守卫等)未变。
- 流团解散审批里车务栅栏"占用中但未撞锁"的情形(返回"未处理"而非异常)未变。
- 房务配房、分房、入住确认等接口未改。
- 无数据库结构变更(零 DDL/Flyway),无网关路由变更。
## 八、测试环境已验证
测试服已部署合并提交 `fad5d7814b`(product-v2 与 order-v3),以下均经网关真实调用,并按接口回读与库内数据判定:
- **改期被拒(410206)**:四类不允许改期的团期各取一例,用 `PUT /admin/product/item/{productId}/schedule` 改 `departureDate`,均返回 `410206`,`message` 内嵌订单域原因:已成团(589800)、已有房务计划/分房(589801)、已有团级正式用车需求(589802)、子单已提交酒店需求(589803,文案点名到具体订单号)。拒绝后班期、团期、子单日期全部核对未变。
- **改期放行**:招募中、尚无排程的团期改出发日后,团期详情的 `departDate`/`endDate`/`enrollDeadline` 等于新值;活跃子单出发/返程日按同一位移平移,`trip_days` 不变;子单逐日行程 `day_date` 全量核对 17 单 53 行,零例外;未派单的 TRAVEL 用车需求已重版,TRANSFER 不动;团期餐食 `meal_date` 随之平移(样本平移 5 天)。
- **房务汇总**:改期后房务需求汇总的入住日随之平移(样本 05-15/16 → 05-17/18),无 `dateShifted=true`、无 outOfRange 户,`hotelReady=false`。
- **不触发联动**:只改单房差、或产品天数变化只带动 `endDate` 变化的保存,均不走预检与联动、不被拒。
- **订单服务不可用(410207)**:订单服务不可达时保存返回 `410207`,班期不落库。
- **审计留痕**:被联动的每个子单各有一条「团期改期」时间线,含出发日/返程日的旧值与新值,操作人为发起保存的管理员(非 SYSTEM);团期有一条改期联动成功记录。
- **并发创单**:同一团期 10 个 `POST /v3/admin/order` 并发创单,期间穿插一次改期:10 单全部成功,改期成功,所有子单出发日与团期一致(零分叉)。本轮没有撞出 `100503`(锁竞争窗口很窄)。
- **耗时**:测试服 12 个改期样本的 apply 段(锁内平移整团)为 41~688ms。
覆盖边界:`transfer-in` 与流团解散审批通过的 `100503` 路径(撞死锁/锁等待超时)在测试服无法稳定构造,本轮未在测试服复现,由单测与集成测试覆盖。
## 十、相关文档
- 产品域改期预检/联动走的是订单域内部接口(非前端可见,仅供理解链路):`POST /v3/internal/group-batch/schedule-sync/{productBatchId}/precheck`、`POST /v3/internal/group-batch/schedule-sync/{productBatchId}/apply`、`POST /v3/internal/group-batch/schedule-sync/{productBatchId}/replay`(运维重推,无请求体,锁内读 product 实时日期后执行 apply,时间线操作人记为 SYSTEM)。三者均为 `/v3/internal/**`,仅供 Feign 内部调用,不经网关,前端不可达。
- `100503` 为全仓统一的锁竞争可重试错误码,语义与既往 changelog「Lock4j 抢锁失败统一返 100503」一致。
## 关联 / 联系人
### 链接
- **Issue**: [#8666](https://git.1814.love/wx/HL/issues/8666)
- **PR**: [#8738](https://git.1814.love/wx/HL/pulls/8738)
- **Merge commit**: `fad5d7814b2d1fb940d740457dbed754e46d03f9`
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,441 @@
---
schema: "hl-changelog/v2"
ticket: "8746"
title: "出团通知书:可下发改为「阶段 + 四项资源」并新增缺项清单 releaseBlockers,默认车辆信息带司机,保存留痕"
consumer: "admin"
author: "jw(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "130a49ebfd326cbf083622819b036918be2a00a6"
target_release: "v2.1"
verified_at: "2026-10-04"
status_note: "已合并 dev-v3(b90969493)并部署 TEST,自签 token 经网关实测:招募中 / 待出发 / 资源准备中 / 已流团 / 脏值逐态缺项、配房配车回落与恢复、缺资源仍可保存、默认车辆带司机且手机全脱敏、一户派车数据异常时跳过该户接口仍 200、保存留痕与撞版本不留痕、无权限角色 589507 零写入。前端待改:通知书弹窗按 releasable 置灰「打印 / 存 PDF」并展示 releaseBlockers[].name。前端已交付(2026-10-04):随 #8767 同一提交落地(该单顺手补齐本单缺项展示缺口,详见 #8767 status_note),GroupBatchNoticeModal 存 releaseBlockers+printBlockTip 按 name 列示缺项,提交 130a49ebf。"
updated_at: "2026-10-03"
base: "dev-v3"
---
# order-v3: 出团通知书可下发判定补四项资源,新增 releaseBlockers
**服务**: hl-order-service-v3
**PR**: `#8770`(已合入 `dev-v3`,合并提交 `b90969493`)
**Issue**: #8746
---
## ⚠️ 关键变化
🔴 **`releasable` 口径变了**:原来团期到「待出发」及之后就是 `true`;现在还要求房 / 车 / 导 / 摄四项资源全部就绪。待出发后房务或车务回落的团,`releasable` 会变成 `false`。
🟢 **新增出参 `releaseBlockers`**(GET / PUT 响应都有):不可下发时列出缺哪几项,可下发时为空数组。
🟢 **默认车辆信息 `defaults.bus` 带上司机**:每辆车「车型 车牌 司机 姓名 脱敏手机」。
🟢 入参、路径、判权、错误码全部不变;保存仍然不卡下发门。
---
## 一、背景
出团通知书「打印 / 存 PDF」按钮靠 `releasable` 置灰。原口径只看团期阶段:团期进入待出发前要先过「四项资源配齐」,但进入之后房务、车务回落不会把团期退回去,于是资源已经缺了的团照样能打印,页面也不知道缺什么。另外默认车辆信息只有车型和车牌,没有司机。
本单把「四项资源就绪」加进 `releasable`,并新增 `releaseBlockers` 告诉页面缺哪一项;默认车辆信息补上司机。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 读出团通知书 | GET | `/v3/admin/order/group-batch/:groupBatchId/docs/notice` | 修改 | `releasable` 新口径;新增 `releaseBlockers`;`defaults.bus`(及未保存时正文 `bus`)带司机 |
| 2 | 保存出团通知书 | PUT | `/v3/admin/order/group-batch/:groupBatchId/docs/notice` | 修改 | 响应同上;保存成功在团期时间线新增「保存出团通知书」一条 |
---
## 三、接口详情
**`releasable` 规则**(两个接口相同):团期状态是 待出发 `PENDING_DEPARTURE` / 出行中 `TRAVELLING` / 待核单 `PENDING_REVIEW` / 核单中 `REVIEWING` / 已结算 `SETTLED` 之一,**且**配房、配车、配导游、配摄影四项都已完成,才为 `true`。
**`releaseBlockers[]` 取值**(按下表顺序排列,`releasable=true` 时为 `[]`,从不为 `null`):
| key | name | 何时出现 |
|---|---|---|
| `STAGE` | 团期未到待出发 | 团期状态不在上面五个之内 |
| `HOTEL` | 配房未完成 | 已成团且配房未完成 |
| `VEHICLE` | 配车未完成 | 已成团且配车未完成(整团免车算已完成) |
| `GUIDE` | 配导游未完成 | 已成团且配导游未完成(不需要导游的团算已完成) |
| `PHOTOGRAPHER` | 配摄影未完成 | 已成团且配摄影未完成(不需要摄影的团算已完成) |
- 未成团(招募中 `RECRUITING`、已流团 `CANCELLED`)**只列 `STAGE`**,不列资源项。
- 已成团但未到待出发(资源准备中、物料准备中):`STAGE` + 缺的资源项。
- 以后可能追加取值(团车司机,#8767)。遇到不认识的 `key`,按 `name` 展示即可。
**`bus` 默认值格式**:每辆车 `车型 车牌 司机 姓名 脱敏手机`,例 `33 座大巴 蒙A·88888 司机 王师傅 138****8888`。
- 同一辆车多日换过司机,司机之间用 ` / ` 分隔:`33 座大巴 蒙A·88888 司机 王师傅 138****1234 / 赵师傅 136****9999`。
- 车与车之间仍用 `、`。
- 司机没有手机号时只有姓名;手机号一律脱敏。
- 整团派车(团车)的团,车辆信息暂不出现在默认值里(#8767 补)。
- 已保存的正文不会自动刷新,只有 `defaults` 是实时值。
### 1. 读出团通知书 `GET /v3/admin/order/group-batch/:groupBatchId/docs/notice`
**VO**: `GroupBatchNoticeRespVO`(入参只有路径参数)→ `Result<GroupBatchNoticeRespVO>`
#### 使用场景
团期详情「出团通知书」弹窗打开时调用。按 `releasable` 置灰「打印 / 存 PDF」,`releasable=false` 时把 `releaseBlockers[].name` 列给用户看。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | 团期 ID | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| releasable | Boolean | 🔄 是否允许打印 / 下发:阶段在可下发集合内且四项资源都已完成(规则见上) |
| releaseBlockers | Object[] | 🆕 不可下发的缺项,可下发时为 `[]` |
| releaseBlockers[].key | String | 🆕 缺项编码:`STAGE` / `HOTEL` / `VEHICLE` / `GUIDE` / `PHOTOGRAPHER` |
| releaseBlockers[].name | String | 🆕 缺项中文名,见上表 |
| bus | String | 🔄 未保存过(`saved=false`)时等于 `defaults.bus`,格式见上;已保存时为保存的原文 |
| defaults.bus | String | 🔄 实时默认车辆信息,每辆车带司机,格式见上 |
| title / greeting / meetTime / meetPlace / leader / contacts / service / bring | String | **不变** |
| saved / version / updateTime / defaults 其余字段 | — | **不变** |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2106331639531601921/docs/notice HTTP/1.1
Authorization: Bearer <管理员 token>
```
#### 响应示例
示例:待出发团期配房回落、从未保存过(草稿)——`releasable=false`,缺项只有 `HOTEL`,`bus` 默认值带司机(缺项与车辆串取自 TEST 验收读数,两步读数拼成一个示例)。
```json
{
"code": 200,
"message": "成功",
"data": {
"title": "冻干粉发短信给 · 出团通知书",
"greeting": "亲爱的团友,欢迎参加本次行程!",
"meetTime": "2026-11-20 08:30",
"meetPlace": "",
"leader": "",
"bus": "mpv 蒙A-G8888 司机 阿拉坦 135****5019、suv 蒙A-E2E01 司机 宝音德力格尔 135****5009、suv 蒙P301A 司机 P3测试司机01 139****0001 / P3测试司机21 139****0021",
"contacts": "",
"service": "",
"bring": "",
"saved": false,
"releasable": false,
"releaseBlockers": [
{
"key": "HOTEL",
"name": "配房未完成"
}
],
"version": 0,
"updateTime": null,
"defaults": {
"title": "冻干粉发短信给 · 出团通知书",
"greeting": "亲爱的团友,欢迎参加本次行程!",
"meetTime": "2026-11-20 08:30",
"meetPlace": "",
"leader": "",
"bus": "mpv 蒙A-G8888 司机 阿拉坦 135****5019、suv 蒙A-E2E01 司机 宝音德力格尔 135****5009、suv 蒙P301A 司机 P3测试司机01 139****0001 / P3测试司机21 139****0021",
"contacts": "",
"service": "",
"bring": ""
}
}
}
```
四项都已完成时 `releasable=true`、`releaseBlockers=[]`。
#### 空数据 / 降级响应
- 团内没有逐户派车、或由团车承担:`defaults.bus` 为 `""`。
- 某一户的派车数据异常:跳过该户,其余户照常拼出车辆信息,接口仍返回 `code=200`。
- 司机手机号取不到:该司机只显示姓名。
#### 错误响应
| code | 条件 |
|---|---|
| `589500` | 团期不存在或已删除 |
| `589507` | 当前角色没有 `group-batch:docs` 权限(不变) |
```json
{
"code": 589500,
"message": "团期不存在",
"data": null
}
```
#### 业务边界
- `releasable` 只管「能否打印 / 下发」,不影响读取与保存。
- 招募中的团只返回 `STAGE` 一项,即使资源都没配。
- 同一个团在不同时间读,`releasable` 与 `releaseBlockers` 可能不同(房务、车务回落或补齐后会变)。
### 2. 保存出团通知书 `PUT /v3/admin/order/group-batch/:groupBatchId/docs/notice`
**VO**: `GroupBatchNoticeSaveReqVO` → `Result<GroupBatchNoticeRespVO>`
#### 使用场景
运营编辑通知书后保存。入参不变;响应与读接口同一个结构,同样带 `releasable` 与 `releaseBlockers`。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | 团期 ID | **不变** |
| title | Body | String | ✅ | ≤128 字 | **不变** |
| greeting | Body | String | ✅ | ≤512 字 | **不变** |
| meetTime | Body | String | ✅ | ≤64 字 | **不变** |
| meetPlace | Body | String | ✅ | ≤256 字 | **不变** |
| leader | Body | String | ✅ | ≤256 字 | **不变** |
| bus | Body | String | ✅ | ≤256 字 | **不变**;默认值带司机后变长,车与司机组合很多时原样保存可能超长,需删减后再存 |
| contacts | Body | String | ✅ | ≤256 字 | **不变** |
| service | Body | String | ✅ | ≤1024 字 | **不变** |
| bring | Body | String | ✅ | ≤1024 字 | **不变** |
| expectedVersion | Body | Integer | ✅ | ≥0 | **不变**,取读接口的 `version` |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| releasable / releaseBlockers | — | 🔄 / 🆕 同读接口 |
| version | Integer | **不变**,保存后的新版本 |
| 其余字段 | — | **不变** |
#### 请求示例
```json
{
"title": "呼伦贝尔亲子研学 6 日 · 出团通知书",
"greeting": "亲爱的团友,欢迎参加本次行程!",
"meetTime": "2026-11-20 08:30",
"meetPlace": "海拉尔东山国际机场 T1 到达厅 3 号门",
"leader": "李雪梅",
"bus": "33 座大巴 蒙A·88888 司机 王师傅 138****8888",
"contacts": "李雪梅 139****2756",
"service": "含 3 早 6 正餐、全程用车、景区门票",
"bring": "防晒霜、驱蚊液、厚外套",
"expectedVersion": 0
}
```
#### 响应示例
示例:招募中团期首次保存(`expectedVersion=0`)——保存成功、`version=1`,缺项只有 `STAGE`(保存不卡下发门)。
```json
{
"code": 200,
"message": "成功",
"data": {
"title": "呼伦贝尔亲子研学 6 日 · 出团通知书",
"greeting": "亲爱的团友,欢迎参加本次行程!",
"meetTime": "2026-11-20 08:30",
"meetPlace": "海拉尔东山国际机场 T1 到达厅 3 号门",
"leader": "李雪梅",
"bus": "33 座大巴 蒙A·88888 司机 王师傅 138****8888",
"contacts": "李雪梅 139****2756",
"service": "含 3 早 6 正餐、全程用车、景区门票",
"bring": "防晒霜、驱蚊液、厚外套",
"saved": true,
"releasable": false,
"releaseBlockers": [
{
"key": "STAGE",
"name": "团期未到待出发"
}
],
"version": 1,
"updateTime": "2026-10-03T18:33:10",
"defaults": {
"title": "冻干粉发短信给 · 出团通知书",
"greeting": "亲爱的团友,欢迎参加本次行程!",
"meetTime": "2026-11-20 08:30",
"meetPlace": "",
"leader": "",
"bus": "",
"contacts": "",
"service": "",
"bring": ""
}
}
}
```
#### 空数据 / 降级响应
- 不涉及;保存成功即返回保存后的全量。
#### 错误响应
| code | 条件 |
|---|---|
| `589500` | 团期不存在 |
| `589507` | 当前角色没有 `group-batch:docs` 权限(不变) |
| `589585` | 版本冲突(别人先保存了),请重读后再存(不变) |
| `589587` | 正文含证件号形态的数字(不变) |
| 参数校验失败 | 字段超长或缺失(不变) |
```json
{
"code": 589585,
"message": "通知书已被他人修改,请刷新后重试",
"data": null
}
```
#### 业务边界
- 保存**不卡**下发门:招募中、资源未齐都能保存,响应里照样带缺项。
- 保存成功后,团期时间线(`GET /v3/admin/order/group-batch/:groupBatchId/status-logs`)新增一条「保存出团通知书」,内容如「保存出团通知书(第 3 版)」。保存失败(版本冲突、证件号拦截、参数校验)不新增。
- 打印不经后端,不留痕。
---
## 四、契约约束与正确调用方式
- 「打印 / 存 PDF」按 `releasable` 置灰;`releasable=false` 时展示 `releaseBlockers[].name`。判断用 `key`,不要用中文名。
- 不要自己根据团期状态推算能否打印,以 `releasable` 为准。
- 遇到不认识的 `key`(以后会加团车司机),按 `name` 展示。
- 车辆信息里的司机拼在 `bus` 字符串里,不需要新输入框。
---
## 五、数据库行为
- 零表结构变更、零数据迁移。
- 读接口零写入。
- 保存接口:正文写入不变;成功后额外在团期时间线新增一条「保存出团通知书」记录。
---
## 六、边界行为
- 团期状态是历史脏值(不在已知状态内):`releasable=false`,只列 `STAGE`,不报错。
- 四项资源中任何一项为空值(历史数据)按「未完成」处理。
- 时间线记录写失败不影响保存结果。
## 六.6、修改前后对比
| 场景 | 改前 | 改后 |
|---|---|---|
| 待出发,四项都已完成 | `releasable=true` | `releasable=true`,`releaseBlockers=[]` |
| 待出发,配房回落未完成 | `releasable=true`(照样能打印) | `releasable=false`,`[HOTEL]` |
| 招募中 | `releasable=false` | `releasable=false`,`[STAGE]` |
| 资源准备中,配车未完成 | `releasable=false` | `releasable=false`,`[STAGE, VEHICLE]` |
| 默认车辆信息 | `33 座大巴 蒙A·88888` | `33 座大巴 蒙A·88888 司机 王师傅 138****8888` |
| 保存成功 | 时间线无记录 | 时间线新增「保存出团通知书(第 N 版)」 |
## 六.7、影响评估
- **是否破坏向后兼容**:`releaseBlockers` 是纯新增字段;`releasable` 在「待出发后资源回落」时由 `true` 变 `false`,旧页面会置灰按钮但看不到原因。
- **前端是否必须同步上线**:建议同步展示 `releaseBlockers`;不改也不会报错。
- **回滚**:revert PR #8770 后重新部署 order-v3。
---
## 七、不影响范围
- 两个接口的路径、入参、判权(`group-batch:docs`)、错误码:不变。
- 已保存的通知书正文:不会被改写。
- 团期时间线读接口的结构:不变,只是多了一种事件「保存出团通知书」。
- 小程序端:无影响。
---
## 八、测试环境已验证
**环境**:TEST(`https://api.test.1814.love`) **验证时间**:2026-10-03 18:14~18:46
**构建身份**:order-v3 部署 `dev-v3 @ b90969493`(本单合并提交),18:08:56 完成;部署状态表 order-v3 行为 `dev-v3 b90969493 ok`。零写入判据:部署后连查 8 次读接口,每次响应都带 `releaseBlockers` 键(旧字节没有)。
**身份**:自签 token 直打网关,用 TEST 真实账号 ID 配对应角色。
### 8.1 造数
载体产品「冻干粉发短信给」上新建班期「11月20日海拉尔-额尔古纳4日团」(出发 2026-11-20),下两单(各 2 成人)进同一团期 `2106331639531601921`。待出发 / 资源回落用 SQL 改团期状态与四项资源完成标记模拟(与房务、车务回落的落库效果相同),每步后恢复。
### 8.2 下发门与缺项
| 团期状态 | 未完成项 | `releasable` | `releaseBlockers` |
|---|---|---|---|
| 招募中 | 四项全未完成 | `false` | `[STAGE]` |
| 待出发 | 无 | `true` | `[]`(4 次读一致) |
| 待出发 | 配房 | `false` | `[HOTEL]`,恢复后回到 `true` / `[]` |
| 待出发 | 配车 | `false` | `[VEHICLE]` |
| 资源准备中 | 配摄影 | `false` | `[STAGE, PHOTOGRAPHER]` |
| 已流团 | 配房、配车 | `false` | `[STAGE]` |
| 历史脏值 `PENDING_TRIP` | — | `false` | `[STAGE]`,接口 200 |
缺资源时保存照常成功(`version` 2 → 3,响应带 `[HOTEL]`)。
### 8.3 默认车辆带司机
把真实派车数据复制到两户名下(手机号按新订单重新加密):`defaults.bus` 出 3 辆车、4 名司机,8 次读一致;4 个手机号均为 `ddd****dddd` 形态,前三后四与独立解密结果一致;同一辆车两天两名司机用「 / 」并列。两个实例在时间窗内的日志明文手机号零命中。
### 8.4 一户派车数据异常
两户中一户的派车数据改为不自洽:8 次读全部 `code=200`,`bus` 只含正常户的车;两个实例各记 4 条「已跳过该户」告警,无事务回滚异常。清理后 `bus` 回到空。
### 8.5 保存留痕
| 操作 | 结果 |
|---|---|
| 首次保存 | `version=1`;时间线新增「保存出团通知书(第 1 版)」,操作人 jw |
| 用旧版本号再存 | `589585`,时间线条数与版本号不变 |
| 用新版本号再存 | `version=2`;新增「第 2 版」 |
### 8.6 判权
| 调用方 | 读 | 存 |
|---|---|---|
| 不带 token | 网关 `401` | 网关 `401` |
| 车务、财务(无 `group-batch:docs`) | `589507` | `589507`,零写入 |
| 管理员、团期管理员 | `200` | `200`,版本 +1 |
### 本地证据
| 项 | 读数 |
|---|---|
| 定向 3 类(含 5 个内嵌类) | 63/0/0 |
| 合并提交复跑 6 类 | 97/0/0 |
| 团期包 + archunit 包 + 全模块架构测试(有 Docker) | 3851 例 5 失败 2 错误,全部在基底 `d778c9a71` 干净工作区逐条复现,本单零新增 |
### 未覆盖
- TEST 上没有「待出发、逐户派车、在团户未取消」的现成团,默认车辆带司机用复制的真实派车数据验证。
- 房务、车务回落用 SQL 模拟落库效果,真实回落业务路径未走。
- 团车(整团派车)的车辆与司机不在本单(#8767)。
---
## 十、相关文档
- Issue `#8746`;PR `#8770`
- 拆出:Issue `#8767`(团车车辆与司机进通知书、`DRIVER` 缺项)
- 前置:Issue `#7532`(出团通知书首版)
## 关联 / 联系人
### 链接
- **Issue**: [#8746](https://git.1814.love/wx/HL/issues/8746)
- **PR**: [#8770](https://git.1814.love/wx/HL/pulls/8770)
- **Merge commit**: [b90969493](https://git.1814.love/wx/HL/commit/b90969493)
### 联系人
- **后端负责人**: @jw
@@ -0,0 +1,421 @@
---
schema: "hl-changelog/v2"
ticket: "8747"
title: "团期行程汇总与下钻按出行中终止日截断:终止户之后的天不再计入覆盖户数,天头列出终止户"
consumer: "admin"
author: "jw(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "17c98e509a296fc356835808f8741606d2aad8be"
target_release: "v2.1"
verified_at: "2026-10-03"
status_note: "已合并 dev-v3(9d1e87b38)并部署 TEST,自签 token 经网关实测:一团 3 户同一份 3 天行程、1 户 D1 出行中终止(真实终止接口),D1 仍 3/3 全团一致,D2、D3 各项 2/3 部分户、天头列出终止户、本日合计由 385.00 / 730.01 变为 0;下钻终止户 terminated=true、endDayNumber=1;对照团与两个存量团改前改后去掉新字段逐字一致(38 项);团级行程单、签单凭证改前改后逐字一致。前端待做:天头渲染「N 户已于 Dx 终止」并可展开户清单,不在每个节点行上重复标;下钻行按 terminated / endDayNumber 标「已于 Dx 终止」。前端已交付(2026-10-03):batch-itinerary.js 加 4 展示纯函数(天头提示停同日「N 户已于 Dx 终止」/不同天退化、清单行、下钻行按 endDayNumber!=null 且本行 dayNumber>endDayNumber 判标记,terminated 仅户级开关);ItineraryTab 天头渲染+点击展开户清单(节点行不重复标);ItineraryNodeModal 截断行标「已于 Dx 终止」;API spec 新增 4 例+组件 spec 新建 3 例全绿,提交 17c98e50。"
updated_at: "2026-10-03"
base: "dev-v3"
---
# order-v3: 团期行程汇总按出行中终止日截断
**服务**: hl-order-service-v3
**PR**: `#8766`(已合入 `dev-v3`,合并提交 `9d1e87b38`)
**Issue**: #8747
---
## ⚠️ 关键变化
🟢 **两个接口纯加字段**:汇总 `days[]` 新增 `terminatedOrderCount`、`terminatedOrders[]`;下钻 `items[]` 新增 `terminated`、`endDayNumber`。其余字段、入参、判权、错误码不变。
🔴 **有户出行中终止的团,终止日之后的读数会变**:该户仍计入分母 `totalHouseholds`,但不再计入那几天任何项的户数 `householdCount`。原本全团一致的项会变成部分户(如 2/3),不再计入本日合计,`dayTotalAmount` 相应变小(可能为 0)。这是需求口径,不是回归。
🟢 **团内没有终止户时,响应除新字段(0 / 空列表 / `false` / `null`)外逐字不变。**
---
## 一、背景
出行中终止(`POST /v3/admin/order/:id/terminate`)只把订单置为 `COMPLETED`,并在 `order_terminate_refund.end_day_number` 记下停在第几天;行程表一行不动。团期行程逐日汇总(GB-ADM-018)与逐户下钻(GB-ADM-019)此前只排除已取消户,所以终止日之后那几天,这一户仍按完整行程计入覆盖户数,节点会被判成「全团一致」并计入本日合计——页面显示整团都去了,实际少一户。
实施单 04 §3.11.3 在 2026-09-01 已定「保留、可见、按天截断」,本单按此落地。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 团期行程逐日汇总(GB-ADM-018) | GET | `/v3/admin/order/group-batch/:groupBatchId/itinerary` | 修改 | 终止户按天截断;`days[]` 新增 `terminatedOrderCount` / `terminatedOrders[]` |
| 2 | 团期行程某项逐户下钻(GB-ADM-019) | GET | `/v3/admin/order/group-batch/:groupBatchId/itinerary/nodes/:nodeKey` | 修改 | 户数与一致性按截断口径;`items[]` 新增 `terminated` / `endDayNumber` |
---
## 三、接口详情
**终止户的计数口径**(两个接口相同):
| 项 | 口径 |
|---|---|
| 怎么算「已终止」 | `order_terminate_refund` 有该户未软删的行;截断天取 `end_day_number`。按全团活跃户一次批量查,与户数无关 |
| 分母 `totalHouseholds` | **计入**终止户(已取消户照旧排除) |
| `dayNumber ≤ endDayNumber` 的天 | 该户照常计入 |
| `dayNumber > endDayNumber` 的天 | 该户**不计入**任何项的户数 `householdCount`,**不参与**该天的一致性判定与本日合计 |
| 行程记录 | 不隐藏:下钻照常列出该户那一行(`has=true`、带价量);汇总项的 `nodeIds` 照常含该户的节点 ID,核单下钻要用 |
| 节点级「已用」 | 不做(实施单 04 §3.11.3:那是核单的职责) |
**本日合计为什么会变小**:本日合计只算 `ALL_SAME`(全团一致)的项。终止户退出后,终止日之后的项最多只有 M−1 户有,按「只有部分户有」判为 `PARTIAL`,不再计入。所以有户中途终止的团,终止日之后各天的 `dayTotalAmount` 会变小,所有项都变成部分户时为 0。
### 1. 团期行程逐日汇总(GB-ADM-018) `GET /v3/admin/order/group-batch/:groupBatchId/itinerary`
**VO**: `Result<GroupBatchItineraryRespVO>`(无请求体)
#### 使用场景
团期详情「行程」页签。天头按 `terminatedOrderCount` 渲染「N 户已于 Dx 终止」,点开看 `terminatedOrders`;节点卡片的「N/M 户」与本日合计按新口径显示。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | 团期 ID | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| days[].terminatedOrderCount | Integer | 🆕 本天已处于出行中终止的户数(截断天 < 本天)。这些户仍计入分母,但不计入本天任何项的户数;无终止户为 0,前端为 0 时不渲染天头提示 |
| days[].terminatedOrders[] | Array | 🆕 本天已终止户清单,按 `orderId` 升序;无终止户为空数组 |
| days[].terminatedOrders[].orderId | String | 🆕 子订单 ID(雪花,按字符串传) |
| days[].terminatedOrders[].teamNo | String | 🆕 团号;无团号为 null |
| days[].terminatedOrders[].orderNo | String | 🆕 子订单编号 |
| days[].terminatedOrders[].customerName | String | 🆕 客户姓名 |
| days[].terminatedOrders[].endDayNumber | Integer | 🆕 截断天:该户行程停在第几天 |
| days[].nodes[].householdCount | Integer | **口径变**:终止户在截断天之后不计入;只有终止户排了的项可为 0 |
| days[].nodes[].consistency / countedInDayTotal | String / Boolean | **口径变**:按截断后的户数判;终止日之后原全团一致的项变为 `PARTIAL`、不计入合计 |
| days[].dayTotalAmount / excludedNodeCount | BigDecimal / Integer | **口径变**:随上面的一致性结果重算,可能变小 |
| 其余字段 | — | **不变**(`totalHouseholds` 含终止户;`nodeIds` 含终止户的节点 ID) |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2106297832652881921/itinerary HTTP/1.1
Authorization: Bearer <管理员 token>
```
#### 响应示例
TEST 实测(截取第 2 天与其首项;乙户「娜仁其其格」第 1 天行程结束后出行中终止):
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2106297832652881921",
"totalHouseholds": 3,
"dayCountConsistent": true,
"dayCount": 3,
"dayCountOutliers": [],
"days": [
{
"dayNumber": 2,
"dayDate": "2026-10-21",
"dayTitle": "是的复古风的水果",
"dayTotalAmount": 0,
"excludedNodeCount": 7,
"terminatedOrderCount": 1,
"terminatedOrders": [
{
"orderId": "2106297833152004097",
"teamNo": "26-5148",
"orderNo": "HL20261003161831138",
"customerName": "娜仁其其格",
"endDayNumber": 1
}
],
"nodes": [
{
"nodeKey": "c44f160ad5e71a8b",
"nodeIds": ["2106297832707407873", "2106297833231695873", "2106297834452238339"],
"nodeName": "巴尔虎蒙古部落",
"nodeType": "SCENIC",
"resourceType": "SCENIC_SPOT",
"resourceId": "3001000000000000016",
"resourceName": "巴尔虎蒙古部落",
"startTime": null,
"timePeriod": "EARLY_MORNING",
"consistency": "PARTIAL",
"householdCount": 2,
"totalHouseholds": 3,
"unitPrice": null,
"unitPriceMin": 80.0,
"unitPriceMax": 80.0,
"quantity": null,
"quantityMin": 1,
"quantityMax": 1,
"totalAmount": null,
"countedInDayTotal": false
}
]
}
]
}
}
```
同一团改前(旧构建)第 2 天:各项 `householdCount=3`、`ALL_SAME`,`dayTotalAmount=385.0`,没有终止字段。
#### 空数据 / 降级响应
- 团期无活跃子订单:`days=[]`,不查行程、不查截断天。
- 团内没有终止户:每天 `terminatedOrderCount=0`、`terminatedOrders=[]`,其余字段与改前逐字一致。
- 终止在最后一天(截断天 ≥ 行程天数):没有被截断的天,各天 `terminatedOrderCount=0`。
#### 错误响应
| code | 场景 |
|---|---|
| 589507 | 无 `group-batch:view`,或定制师查看非本人名下的团期(**不变**) |
| 589500 | 团期不存在(**不变**) |
```json
{
"code": 589500,
"message": "团期不存在",
"data": null
}
```
#### 业务边界
- 截断只影响计数,不改行程数据,不隐藏任何项。
- 已取消户照旧排除,不进分母,也不进截断天查询。
- `dayDate` / `dayTitle` 仍按全部活跃户的多数值取,不受终止影响。
- 天数一致性预警(`dayCountConsistent` / `dayCountOutliers`)不受终止影响:终止不改行程天数。
### 2. 团期行程某项逐户下钻(GB-ADM-019) `GET /v3/admin/order/group-batch/:groupBatchId/itinerary/nodes/:nodeKey`
**VO**: `Result<GroupBatchItineraryNodeDetailVO>`(无请求体)
#### 使用场景
汇总卡片点进来看「哪几户有、哪几户不一样」。终止户那一行照常列出,前端按 `terminated` / `endDayNumber` 标「已于 Dx 终止」。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | 团期 ID | **不变** |
| nodeKey | Path | String | ✅ | 取自汇总接口,原样回传 | **不变** |
| dayNumber | Query | Integer | ❌ | 取自汇总卡片所在天;不传 = 跨天全看 | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| items[].terminated | Boolean | 🆕 该户是否已出行中终止(户级标记,与本行是哪天无关) |
| items[].endDayNumber | Integer | 🆕 截断天;未终止为 null。本行 `dayNumber` 大于它时,前端标「已于 Dx 终止」 |
| householdCount / consistency | Integer / String | **口径变**:只认未被截断的出现,与汇总卡片同口径;跨天下钻时,终止户在截断天及之前有出现就计户 |
| items[].has | Boolean | **不变**:该户行程里有没有这一项;终止户截断后的出现仍为 `true`(记录不隐藏),但不计入 `householdCount` |
| 其余字段 | — | **不变** |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2106297832652881921/itinerary/nodes/c44f160ad5e71a8b?dayNumber=2 HTTP/1.1
Authorization: Bearer <管理员 token>
```
#### 响应示例
TEST 实测(第 2 天「巴尔虎蒙古部落」,截取前两户):
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2106297832652881921",
"nodeKey": "c44f160ad5e71a8b",
"dayNumber": 2,
"nodeName": "巴尔虎蒙古部落",
"consistency": "PARTIAL",
"householdCount": 2,
"totalHouseholds": 3,
"items": [
{
"orderId": "2106297832598355970",
"teamNo": "26-8025",
"orderNo": "HL20261003161830984",
"contactName": "孟庆和",
"customerName": "孟庆和",
"has": true,
"dayNumber": 2,
"unitPrice": 80.0,
"quantity": 1,
"totalAmount": 80.0,
"startTime": null,
"timePeriod": "EARLY_MORNING",
"terminated": false,
"endDayNumber": null
},
{
"orderId": "2106297833152004097",
"teamNo": "26-5148",
"orderNo": "HL20261003161831138",
"contactName": "娜仁其其格",
"customerName": "娜仁其其格",
"has": true,
"dayNumber": 2,
"unitPrice": 80.0,
"quantity": 1,
"totalAmount": 80.0,
"startTime": null,
"timePeriod": "EARLY_MORNING",
"terminated": true,
"endDayNumber": 1
}
]
}
}
```
#### 空数据 / 降级响应
- `nodeKey` 没有任何户命中:`householdCount=0`,每户一行 `has=false`,不报错(**不变**);`terminated` / `endDayNumber` 照常按户填。
- 团内没有终止户:每行 `terminated=false`、`endDayNumber=null`,其余字段与改前逐字一致。
#### 错误响应
| code | 场景 |
|---|---|
| 589507 | 无 `group-batch:view`,或定制师查看非本人名下的团期(**不变**) |
| 589500 | 团期不存在(**不变**) |
```json
{
"code": 589507,
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
"data": null
}
```
#### 业务边界
- 同一户跨天多行时,`terminated` / `endDayNumber` 每行相同(户级);是否被截断看本行 `dayNumber` 是否大于 `endDayNumber`。
- 跨天下钻(不带 `dayNumber`)的一致性只看未被截断的出现:终止户在截断后改过的价不会把该项判成各户不一致。
---
## 四、契约约束与正确调用方式
- 天头提示只看 `days[].terminatedOrderCount`,为 0 时不渲染;不要在每个节点行上重复标终止。
- 「N/M 户」直接用 `householdCount` / `totalHouseholds`,不要自己从下钻行数 `has=true` 去数——终止户截断后的行 `has=true` 但不计户。
- 下钻行的终止标记按 `endDayNumber != null && dayNumber > endDayNumber` 判,`terminated` 只是户级开关。
- `nodeIds` 仍含终止户的节点 ID,原样传给核单下钻 `POST /v3/admin/order/group-batch/:groupBatchId/settlement/node-lines` 即可看到全团核单行(含终止户已退未用的门票)。
---
## 五、数据库行为
- 零 DDL、零数据迁移、零写入。
- 新增一条只读批量查询:按全团活跃户 `order_id IN (...)` 读 `order_terminate_refund.end_day_number`(与核单下钻同一读口)。两个接口的查询次数由 2 次批量变为 3 次批量,均与户数无关。
---
## 六、边界行为
- 截断天是 `end_day_number`(停在第几天,1 起),该天本身照常计入,之后的天才截断。
- 终止户的 `order_status` 是 `COMPLETED`,留在活跃集里;已取消户(`CANCELLED`)照旧整体排除。
- 只有终止户在截断后的某天排了的项:汇总里照常列出,`householdCount=0`、`PARTIAL`、不计入合计,`nodeIds` 含该户节点 ID。
## 六.6、修改前后对比
| 场景 | 改前 | 改后 |
|---|---|---|
| 3 户同一行程,1 户 D1 终止,看 D2 某项 | `3/3`、`ALL_SAME`、计入合计 | `2/3`、`PARTIAL`、不计入合计 |
| 同上,D2 本日合计 | 385.00 | 0 |
| 同上,D1 | `3/3`、`ALL_SAME` | 不变 |
| 同上,D2 天头 | 无终止信息 | `terminatedOrderCount=1`,列出该户与 `endDayNumber=1` |
| 下钻 D2 终止户那一行 | 与其他户无区别 | `terminated=true`、`endDayNumber=1`,不计入 `householdCount` |
| 团内无终止户 | — | 除新字段外逐字不变 |
## 六.7、影响评估
- **是否破坏向后兼容**:字段层面否(纯新增);读数层面,有终止户的团在终止日之后的户数、一致性与本日合计会变——这是需求要修的错误读数。
- **前端是否必须同步上线**:否。不改前端时页面照常显示,只是终止日之后的卡片显示部分户、本日合计变小,没有「已终止」的说明;改了才能在天头说明原因。
- **团级文档**:团级行程单(`GET .../print-itinerary`)与签单凭证(`GET .../sign-voucher`)也复用行程汇总,但本单让它们走不截断的入口,输出保持原样(TEST 上含终止户的团改前改后逐字一致)。文档是否也按实际截断另行定口径。
- **回滚**:revert PR #8766 后重新部署 order-v3,无数据需要恢复。
---
## 七、不影响范围
- 两个接口的入参、判权(`group-batch:view` + 定制师归属)、错误码:不变。
- 团级行程单、签单凭证:不变。
- 核单下钻(GB-ADM-056):不变(本来就按户带截断天)。
- 订单级行程、终止行程接口本身、核单与退款计算:不变。
- 散客单(非团期):不涉及。小程序端:无影响。
---
## 八、测试环境已验证
**环境**:TEST(`https://api.test.1814.love`) **验证时间**:2026-10-03 16:18~16:57
**构建身份**:TEST 后端检出 `dev-v3 @ d778c9a71`(包含本单合并提交 `9d1e87b38`),order-v3 两个实例 16:51 前后重启。零写入判据:部署后连查 6 次对照团汇总,每次 `days[]` 都带 `terminatedOrderCount`(旧字节没有这个键)。
**身份**:自签 token 直打网关;造数用超管,读口取证用管理员账号(非超管)。
### 8.1 造数(全部经业务接口,唯一 SQL 见下)
| 团 | 团号 | 内容 |
|---|---|---|
| 主团 | `T26-6215`(`2106297832652881921`) | 新班期出发 2026-10-20,3 天行程每天 8 项;3 户各 2 成人线下全款,第 4 户下单后取消;成团 → 声明整团免车 → 乙户「娜仁其其格」出行中终止 `endDayNumber=1` |
| 对照团 | `T26-2604`(`2106297838424244226`) | 新班期,2 户全款,不终止 |
| 存量团 | `2106039583672299521`(5 户)、`2104839654727618562`(3 户) | 只读,近 6 小时无改动、无终止户 |
终止接口要求订单处于出行中,TEST 上从成团走到出行中要过七道出团门和夜间任务,故只对乙户一张订单用 SQL 把 `order_status` 由 `CUSTOMIZING` 置为 `TRAVELLING`,随后调真实终止接口;终止后订单 `COMPLETED`、`order_terminate_refund.end_day_number=1`。
### 8.2 改前 / 改后(同一批数据,旧构建取一次、新构建取一次)
| 检查 | 结果 |
|---|---|
| 主团 D1 | 8 项均 `3/3`、`ALL_SAME`,`terminatedOrderCount=0` |
| 主团 D2 / D3 | 改前各项 `3/3`、`ALL_SAME`,本日合计 385.00 / 730.01;改后各项 `2/3`、`PARTIAL`、不计入合计,本日合计 0 / 0,`terminatedOrderCount=1`,清单为乙户、`endDayNumber=1` |
| 主团 D2 / D3 `nodeIds` | 仍含 3 户的节点 ID |
| 主团下钻 D2(两项) | 乙户 `terminated=true`、`endDayNumber=1`;其余两户 `false` / `null`;`householdCount=2`、`PARTIAL` |
| 主团 `totalHouseholds` | 3(终止户计入、取消户排除);取消户在汇总与下钻中均不出现 |
| 对照团、两个存量团 | 汇总、逐项下钻(按天与跨天)去掉新字段后改前改后逐字一致;新字段全为 0 / 空 / `false` / `null` |
| 团级行程单、签单凭证(四个团) | 改前改后逐字一致,含主团 |
| 数据指纹 | 四个团改前改后订单与行程行的最近改动时间一致,比对期间没有他人写入 |
合计 94 项检查全部通过。
### 本地证据
| 项 | 读数 |
|---|---|
| `GroupBatchItineraryServiceTest` | 41 例全过,其中本单新增 10 例(截断、截断当天、最后一天终止、只有终止户的项、跨天下钻、无终止户、取消户、截断天一次批量查询、`summaryAsPlanned` 不查截断天) |
| 截断天查询次数 | 4 户团断言 `mapTerminateEndDayByOrderIds` 只调用一次且入参为全部活跃户 |
| 范围回归(有 Docker) | `groupbatch` + `settlement` + `archunit` 共 340 类 / 5066 例,源码可执行类与报告逐类对账 340/340;2 例失败均为基底既有(`TeamNoResponseFieldGateTest`、`GroupBatchAdminReadEndpointOwnershipArchTest`,引入于 `788b9c149`,基底对照跑同样红),本单零新增 |
---
## 十、相关文档
- Issue `#8747`;PR `#8766`
- 需求:`docs/group/实施单/04-团期行程安排.html` §3.11.2 / §3.11.3
- 前置:Issue `#7378`(GB-ADM-018 / 019 原实现)、`#7873`(核单下钻同一读口)
## 关联 / 联系人
### 链接
- **Issue**: [#8747](https://git.1814.love/wx/HL/issues/8747)
- **PR**: [#8766](https://git.1814.love/wx/HL/pulls/8766)
- **Merge commit**: [9d1e87b38](https://git.1814.love/wx/HL/commit/9d1e87b38)
### 联系人
- **后端负责人**: @jw
@@ -0,0 +1,647 @@
---
schema: "hl-changelog/v2"
ticket: "8749"
title: "定制师待办 5 个接口出参新增团期号、返团日期、行程天数、本户人数房数;手动待办返回补齐产品名与出发日"
consumer: "admin"
author: "jw(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "a958e2510182253482cbbc80bc608c568d399831"
target_release: "v2.1"
verified_at: "2026-10-03"
status_note: "已合并 dev-v3(b49ad81ee)并部署 TEST(order-v3 @ d778c9a71),自签定制师 token 经网关实测:名下 108 条待办逐字段对库零差异(团期子订单 81 条、11 个团期;散客单 27 条),teamNo 与部署前快照 108/108 逐字一致,手动待办新增/修改/完成/重开四个返回字段齐全,整页团期号只查一次。前端待做:待办列表与卡片展示团期号、出发至返回日期、行程天数、本户人数和房数;teamNo 不要再当团期号用。前端已交付(2026-10-03):待办列表关联订单格团信息副行改 batchNo 优先(teamNo 仅散客单兜底,不冒充团期号),新增行程副行(出发~返回区间直读 returnDate/天数/四档人数 0 档省略/房数,各段有空省略);todos.spec 新建 3 例全绿,提交 a958e251。"
updated_at: "2026-10-03"
base: "dev-v3"
---
# order-v3: 定制师待办补团期号、行程日期与本户人数房数
**服务**: hl-order-service-v3
**PR**: `#8761`(已合入 `dev-v3`,合并提交 `b49ad81ee`)
**Issue**: #8749
---
## ⚠️ 关键变化
🟢 **5 个接口的出参纯新增 10 个字段**:`groupBatchId`、`batchNo`、`batchName`、`returnDate`、`tripDays`、`adultCount`、`childCount`、`youngChildCount`、`babyCount`、`roomCount`。入参、判权、错误码不变。
🔴 **`teamNo` 是户团号,不是团期号。** `teamNo` 取 `order_main.team_no`(订金付款后生成,形如 `26-6559`),每户一个;团期号是新增的 `batchNo`(`order_group_batch.batch_no`,形如 `T26-3963`),同一团期的各户相同。前端现在把 `teamNo · productName · departDate` 当团信息展示,团期号请改读 `batchNo`。
🟢 **手动待办新增、修改、完成、重开的返回补齐了 `productName`、`departDate`**,改前这两项恒为 `null`。
---
## 一、背景
实施单 15「定制师代办与导摄物资分工」规则 3(AC-TD-03)要求待办带齐填表要用的数据:团期号、出发结束日、行程天数、本户人数房数。改前待办只带户团号、产品名、出发日;团期号和返团日期在所有接口里都没有,手动待办四个写接口的返回里连产品名和出发日都是 `null`。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 我的订单待办分页 | GET | `/v3/admin/order-todos/my/page` | 修改 | `records[]` 新增 10 个字段 |
| 2 | 手动新增订单待办 | POST | `/v3/admin/order-todos/manual` | 修改 | 返回体新增 10 个字段,`productName` / `departDate` 由恒 `null` 改为有值 |
| 3 | 修改手动订单待办 | PUT | `/v3/admin/order-todos/:todoId` | 修改 | 同上 |
| 4 | 完成手动订单待办 | PUT | `/v3/admin/order-todos/:todoId/complete` | 修改 | 同上 |
| 5 | 重开手动订单待办 | PUT | `/v3/admin/order-todos/:todoId/reopen` | 修改 | 同上 |
---
## 三、接口详情
**新增字段的取值**(5 个接口相同,都取待办所属订单的当前值):
| 字段 | 类型 | 来源 | 散客单 |
|---|---|---|---|
| groupBatchId | String(Long 序列化为字符串) | `order_main.group_batch_id`;非空 = 团期子订单 | `null` |
| batchNo | String | `order_group_batch.batch_no`,与订单详情 `main.batchNo` 同义 | `null` |
| batchName | String | `order_group_batch.batch_name`,与订单详情 `main.batchName` 同义 | `null` |
| returnDate | String(yyyy-MM-dd) | `order_main.return_date` | 照填 |
| tripDays | Integer | `order_main.trip_days` | 照填 |
| adultCount / childCount / youngChildCount / babyCount | Integer | `order_main` 四档人数 | 照填 |
| roomCount | Integer | `order_main.room_count`;创单时未定为 `null` | 照填 |
团期已软删(解散)查不到时,`batchNo` / `batchName` 为 `null`,`groupBatchId` 照常返回。
### 1. 我的订单待办分页 `GET /v3/admin/order-todos/my/page`
**VO**: `OrderTodoPageReqVO` → `Result<PageResult<OrderTodoRespVO>>`
#### 使用场景
定制师工作台「我的待办」列表与卡片。团期子订单的待办据 `batchNo`、`departDate`~`returnDate`、`tripDays`、人数房数直接展示填表信息,不用再点进订单详情。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| page | Query | Integer | ❌ | ≥1 | **不变** |
| pageSize | Query | Integer | ❌ | ≥1 | **不变** |
| status | Query | String | ❌ | PENDING / COMPLETED / CANCELLED | **不变** |
| todoSource | Query | String | ❌ | SYSTEM / MANUAL | **不变** |
| orderId | Query | Long | ❌ | 订单 ID | **不变** |
| fromDate | Query | String | ❌ | yyyy-MM-dd | **不变** |
| toDate | Query | String | ❌ | yyyy-MM-dd | **不变** |
| keyword | Query | String | ❌ | 待办标题 / 订单号 / 户团号 / 产品名 | **不变**(不按团期号搜) |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| records[].groupBatchId | String | 🆕 运营团期 ID,散客单 `null` |
| records[].batchNo | String | 🆕 团期号(T26-xxxx),散客单 `null` |
| records[].batchName | String | 🆕 团期名称,散客单 `null` |
| records[].returnDate | String | 🆕 返团日期 |
| records[].tripDays | Integer | 🆕 行程天数 |
| records[].adultCount | Integer | 🆕 本户成人数 |
| records[].childCount | Integer | 🆕 本户儿童数 |
| records[].youngChildCount | Integer | 🆕 本户幼童数 |
| records[].babyCount | Integer | 🆕 本户婴儿数 |
| records[].roomCount | Integer | 🆕 本户房间数 |
| records[].teamNo | String | **取值不变**;说明改为「户团号,不是团期号」 |
| 其余字段 | — | **不变** |
#### 请求示例
```http
GET /v3/admin/order-todos/my/page?page=1&pageSize=20 HTTP/1.1
Authorization: Bearer <定制师 token>
```
#### 响应示例
字段值取自 TEST 实际返回(团期子订单、散客单各一条),省略了 `actionType`、`completedAt` 等未变字段:
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"todoId": "2104839975281496066",
"orderId": "2104839729176514562",
"orderNo": "HL20260929154432061",
"teamNo": "26-6559",
"productName": "jw测试产品",
"departDate": "2026-11-01",
"returnDate": "2026-11-07",
"tripDays": 7,
"groupBatchId": "2104839654727618562",
"batchNo": "T26-3963",
"batchName": "11月1日额济纳胡杨林深秋4日游",
"adultCount": 3,
"childCount": 0,
"youngChildCount": 0,
"babyCount": 0,
"roomCount": 2,
"todoType": "FILL_TRAVELER",
"todoTypeName": "补全出行人",
"todoLabel": "补全出行人信息 · 待提交",
"todoSource": "SYSTEM",
"todoDate": "2026-11-01",
"status": "COMPLETED",
"statusName": "已处理"
},
{
"todoId": "2104976495388856322",
"orderId": "2104976481551806465",
"orderNo": "HL20260930004756321",
"teamNo": "26-4912",
"productName": "游牧的森林-短途版",
"departDate": "2026-09-30",
"returnDate": "2026-10-03",
"tripDays": 4,
"groupBatchId": null,
"batchNo": null,
"batchName": null,
"adultCount": 2,
"childCount": 0,
"youngChildCount": 0,
"babyCount": 0,
"roomCount": 1,
"todoType": "FILL_TRAVELER",
"todoTypeName": "补全出行人",
"todoLabel": "补全出行人信息 · 待提交",
"todoSource": "SYSTEM",
"todoDate": "2026-09-30",
"status": "CANCELLED",
"statusName": "已取消"
}
],
"total": 2,
"page": 1,
"pageSize": 20
},
"success": true
}
```
#### 空数据 / 降级响应
无待办时 `records` 为空数组(不变)。团期已软删时 `batchNo` / `batchName` 为 `null`、`groupBatchId` 照常返回;整页都是散客单时不查团期表。
#### 错误响应
判权不变:取不到当前账号返回 `581701`。
```json
{
"code": 581701,
"message": "无权操作该待办",
"data": null,
"success": false
}
```
#### 业务边界
- 只返回指派给当前账号的待办(不变)。
- 团期号按整页去重后一次批量查出,不随条数增长。
- `keyword` 不匹配团期号;按团期号搜要另提需求。
---
### 2. 手动新增订单待办 `POST /v3/admin/order-todos/manual`
**VO**: `ManualOrderTodoCreateReqVO` → `Result<OrderTodoRespVO>`
#### 使用场景
定制师给自己名下的订单加一条手动待办,返回体直接用来在列表顶部插入新卡片,不必再刷新整页。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Body | Long | ✅ | 当前账号是该单定制师 | **不变** |
| todoLabel | Body | String | ✅ | 非空白 | **不变** |
| todoDate | Body | String | ✅ | yyyy-MM-dd | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| productName | String | 改前恒 `null`,改后取订单产品名 |
| departDate | String | 改前恒 `null`,改后取订单出发日 |
| groupBatchId / batchNo / batchName | String | 🆕 同分页接口 |
| returnDate / tripDays | String / Integer | 🆕 同分页接口 |
| adultCount / childCount / youngChildCount / babyCount / roomCount | Integer | 🆕 同分页接口 |
| 其余字段 | — | **不变** |
#### 请求示例
```json
{
"orderId": "2104839729176514562",
"todoLabel": "出发前一天电话确认集合地点与证件",
"todoDate": "2026-10-31"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"todoId": "2106307263901954050",
"orderId": "2104839729176514562",
"orderNo": "HL20260929154432061",
"teamNo": "26-6559",
"productName": "jw测试产品",
"departDate": "2026-11-01",
"returnDate": "2026-11-07",
"tripDays": 7,
"groupBatchId": "2104839654727618562",
"batchNo": "T26-3963",
"batchName": "11月1日额济纳胡杨林深秋4日游",
"adultCount": 3,
"childCount": 0,
"youngChildCount": 0,
"babyCount": 0,
"roomCount": 2,
"todoType": "MANUAL",
"todoTypeName": "手动待办",
"todoLabel": "出发前一天电话确认集合地点与证件",
"todoSource": "MANUAL",
"todoSourceName": "手动添加",
"todoDate": "2026-10-31",
"actionType": "manual",
"status": "PENDING",
"statusName": "待处理"
},
"success": true
}
```
#### 空数据 / 降级响应
订单查不到(已删)时订单侧字段全部为 `null`,不报错(与改前 `teamNo` 的口径一致)。
#### 错误响应
校验与判权不变:不是该单定制师返回 `581707`,缺订单 / 标题 / 日期分别返回 `581702` / `581704` / `581705`。
```json
{
"code": 581707,
"message": "仅订单定制师可维护该订单待办",
"data": null,
"success": false
}
```
#### 业务边界
- 只读订单当前值,不在待办上存快照:订单人数或日期后来改了,待办返回跟着变。
---
### 3. 修改手动订单待办 `PUT /v3/admin/order-todos/:todoId`
**VO**: `ManualOrderTodoUpdateReqVO` → `Result<OrderTodoRespVO>`
#### 使用场景
定制师改手动待办的标题或日期,返回体用来就地刷新卡片。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| todoId | Path | Long | ✅ | 本人的手动待办 | **不变** |
| todoLabel | Body | String | ❌ | 非空白才生效 | **不变** |
| todoDate | Body | String | ❌ | yyyy-MM-dd | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| productName / departDate | String | 改前恒 `null`,改后有值 |
| 10 个新增字段 | — | 🆕 同分页接口 |
| 其余字段 | — | **不变** |
#### 请求示例
```json
{
"todoDate": "2026-10-30"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"todoId": "2106307263901954050",
"teamNo": "26-6559",
"productName": "jw测试产品",
"departDate": "2026-11-01",
"returnDate": "2026-11-07",
"tripDays": 7,
"groupBatchId": "2104839654727618562",
"batchNo": "T26-3963",
"batchName": "11月1日额济纳胡杨林深秋4日游",
"adultCount": 3,
"roomCount": 2,
"todoDate": "2026-10-30",
"status": "PENDING",
"statusName": "待处理"
},
"success": true
}
```
#### 空数据 / 降级响应
标题和日期都不传时原样返回(不变),新增字段照常有值。
#### 错误响应
不是本人的手动待办返回 `581701`(不变)。
```json
{
"code": 581701,
"message": "无权操作该待办",
"data": null,
"success": false
}
```
#### 业务边界
- 系统待办不能用本接口改(不变)。
---
### 4. 完成手动订单待办 `PUT /v3/admin/order-todos/:todoId/complete`
**VO**: `Long todoId` → `Result<OrderTodoRespVO>`
#### 使用场景
定制师把手动待办标为已完成,返回体用来就地刷新卡片状态。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| todoId | Path | Long | ✅ | 本人的手动待办 | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| productName / departDate | String | 改前恒 `null`,改后有值 |
| 10 个新增字段 | — | 🆕 同分页接口 |
| status | String | `COMPLETED`(不变) |
#### 请求示例
```http
PUT /v3/admin/order-todos/2106307263901954050/complete HTTP/1.1
Authorization: Bearer <定制师 token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"todoId": "2106307263901954050",
"teamNo": "26-6559",
"productName": "jw测试产品",
"departDate": "2026-11-01",
"returnDate": "2026-11-07",
"tripDays": 7,
"batchNo": "T26-3963",
"adultCount": 3,
"roomCount": 2,
"status": "COMPLETED",
"statusName": "已处理"
},
"success": true
}
```
#### 空数据 / 降级响应
已完成的再点完成,原样返回(不变)。
#### 错误响应
```json
{
"code": 581701,
"message": "无权操作该待办",
"data": null,
"success": false
}
```
#### 业务边界
- 状态流转与改前相同,本单只扩出参。
---
### 5. 重开手动订单待办 `PUT /v3/admin/order-todos/:todoId/reopen`
**VO**: `Long todoId` → `Result<OrderTodoRespVO>`
#### 使用场景
定制师把已完成的手动待办重新打开,返回体用来就地刷新卡片。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| todoId | Path | Long | ✅ | 本人的手动待办 | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| productName / departDate | String | 改前恒 `null`,改后有值 |
| 10 个新增字段 | — | 🆕 同分页接口 |
| status | String | `PENDING`(不变) |
#### 请求示例
```http
PUT /v3/admin/order-todos/2106307263901954050/reopen HTTP/1.1
Authorization: Bearer <定制师 token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"todoId": "2106307263901954050",
"teamNo": "26-6559",
"productName": "jw测试产品",
"departDate": "2026-11-01",
"returnDate": "2026-11-07",
"tripDays": 7,
"batchNo": "T26-3963",
"adultCount": 3,
"roomCount": 2,
"status": "PENDING",
"statusName": "待处理"
},
"success": true
}
```
#### 空数据 / 降级响应
待处理的再点重开,原样返回(不变)。
#### 错误响应
```json
{
"code": 581701,
"message": "无权操作该待办",
"data": null,
"success": false
}
```
#### 业务边界
- 状态流转与改前相同,本单只扩出参。
---
## 四、契约约束与正确调用方式
- 展示团期号读 `batchNo`,**不要**再用 `teamNo` 冒充团期号;`teamNo` 是户团号,同一团期里每户不同。
- 判断是不是团期子订单看 `groupBatchId` 是否为空,不要看 `batchNo`(团期软删时 `batchNo` 为空但仍是团期子订单)。
- 出发至返回日期用 `departDate` ~ `returnDate`,天数用 `tripDays`,不要自己按日期差推算。
- 新增字段都是订单当前值,不是待办生成时的快照。
---
## 五、数据库行为
- 零 DDL、零数据迁移、零新增写入。
- 分页联查 `order_main` 时多取 8 列(返团日期、行程天数、团期 ID、四档人数、房数);团期号对整页去重后的团期 ID 做一次主键批量查询。
- 手动待办四个写接口的写库逻辑不变;返回前按订单 ID 读一次订单,团期子订单再按主键读一次团期。
---
## 六、边界行为
- 散客单:`groupBatchId` / `batchNo` / `batchName` 为 `null`,不查团期表,其余字段照填。
- 团期已软删:`batchNo` / `batchName` 为 `null`,`groupBatchId` 照常返回。
- 订单的 `roomCount` 创单时未定的为 `null`,原样透出。
- `teamNo` 订金未付时仍为 `null`,不回退订单号(#7537 口径不变)。
## 六.6、修改前后对比
| 场景 | 改前 | 改后 |
|---|---|---|
| 分页里的团期子订单待办 | 只有户团号、产品名、出发日 | 另有团期号、团期名、返团日期、行程天数、四档人数、房数 |
| 分页里的散客单待办 | 同上 | 团期三项为 `null`,其余新字段有值 |
| 手动待办新增 / 修改 / 完成 / 重开的返回 | `productName`、`departDate` 恒 `null` | 有值,并带全部新字段 |
| `teamNo` | 户团号 | 户团号,取值不变 |
## 六.7、影响评估
- **是否破坏向后兼容**:否,纯新增字段;`productName` / `departDate` 由 `null` 变为有值。
- **前端是否必须同步上线**:否。不改前端时展示与改前相同;要显示团期号与行程人数需前端配合。
- **性能**:分页每页最多多一次主键批量查询,手动待办写接口多一到两次主键查询。
- **回滚**:revert PR #8761 后重新部署 order-v3。
---
## 七、不影响范围
- 待办的生成、关闭、打回与指派逻辑:不变。
- 各接口入参、判权、错误码:不变。
- 手动待办订单下拉 `GET /v3/admin/order-todos/order-options`、取消手动待办 `DELETE /v3/admin/order-todos/:todoId`:不变。
- 小程序端:无影响。
---
## 八、测试环境已验证
**环境**:TEST(`https://api.test.1814.love`) **验证时间**:2026-10-03 16:55~16:58
**构建身份**:order-v3 部署 `dev-v3 @ d778c9a71`(含本单合并提交 `b49ad81ee`),16:52 两实例滚动完成。探针:部署前分页返回无 `batchNo` 键,部署后每条都有。
**身份**:自签定制师 token 直打网关,账号 `cw_test_7443`(名下 108 条待办,团期子订单 81 条分属 11 个团期,散客单 27 条)。
### 8.1 分页逐条对库
| 项 | 结果 |
|---|---|
| 团期子订单 81 条 | `batchNo` / `batchName` 与 `order_group_batch` 一致、均为 `T` 开头;返团日期、天数、四档人数、房数与 `order_main` 一致;差异 0 条 |
| 散客单 27 条 | 团期三项全为 `null`;产品名、出发返团日期、天数、人数、房数无一为 `null` 且与订单一致;差异 0 条 |
| `teamNo` | 部署前快照与部署后 108/108 条逐字一致;产品名、出发日也一致 |
### 8.2 手动待办四个返回
在团期子订单 `HL20260929154432061`(户团号 26-6559,团期 T26-3963,3 成人 2 间房)上依次新增 → 修改日期 → 完成 → 重开,四个返回的产品名、出发日与 10 个新字段逐字段对库差异 0。验完已调取消接口收尾(库内该行 `CANCELLED`)。
### 8.3 团期号只查一次
开日志流后各发一页请求,抓同一实例上的 SQL:
| 页 | 团期子订单 / 团期数 | 团期表查询 |
|---|---|---|
| 部署前 10 条 | — | 0 次 |
| 部署后 10 条 | 4 条 / 1 个团期 | 1 次,`IN (?)` |
| 部署后 50 条 | 32 条 / 7 个团期 | 1 次,`IN (7 个 ?)` |
### 8.4 反例
| 操作 | 结果 |
|---|---|
| 不带 token 调分页 | `code 401`「缺少有效的 Authorization 头」 |
| 另一位定制师修改上面那条手动待办 | `581701`「无权操作该待办」 |
### 本地证据
| 项 | 读数 |
|---|---|
| 待办 4 个测试类定向 | 37/0/0/0(新增 11 例) |
| 变异 | 三处同时变异(去掉团期 ID 去重、去掉单对象团期回填、去掉联查 `trip_days`),恰好对应的 6 例红(去重 1、四个单对象动作 4、联查投影 1),其余不受影响;已还原 |
| order-v3 全量(有 Docker,两半) | A 10955 / 4 失败,B 4933 / 2 失败 / 7 跳过;1142 个可执行测试类全部有报告;6 条失败在基底 `954d43705` 上逐条复现,本单零新增 |
---
## 十、相关文档
- Issue `#8749`;PR `#8761`
- 需求:`docs/group/实施单/15-定制师代办与导摄物资分工.html` 规则 3、AC-TD-03
- 前置:#7537(单对象路径补 `teamNo`)、#8497(团期号改 T26-xxxx)、#7142(订单详情 `main.batchNo` / `main.batchName`)
## 关联 / 联系人
### 链接
- **Issue**: [#8749](https://git.1814.love/wx/HL/issues/8749)
- **PR**: [#8761](https://git.1814.love/wx/HL/pulls/8761)
- **Merge commit**: [b49ad81ee](https://git.1814.love/wx/HL/commit/b49ad81ee)
### 联系人
- **后端负责人**: @jw
@@ -0,0 +1,490 @@
---
schema: "hl-changelog/v2"
ticket: "8750"
title: "团期子订单封掉订单级行程写口:改行程天 / 增改删节点 4 个接口对团期子订单返回 583066,调整快照不再提示行程与出行日期可编辑"
consumer: "admin"
author: "jw(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: "mmg"
frontend_ref: ""
target_release: "v2.1"
verified_at: "2026-10-03"
status_note: "PR #8762 已合并 dev-v3(e8d83b0cb)并滚动部署 TEST 双实例。订单行程 4 个写口(改行程天、新增 / 修改 / 删除节点)对团期子订单一律返回新码 583066,零写入;团期管理员仍先返回 581008;散客单不变。调整快照 editableTabLocksHint 对团期子订单去掉 ITINERARY / SCHEDULE。TEST 网关实测:团期子订单 4 个写口 583066 且三表逐字一致、散客单增改删回写 200、团期管理员 581008、快照两侧取值符合。前端待办:团期子订单详情行程页签隐藏编辑 / 新增 / 删除按钮;团期行程汇总下钻去掉「跳去改」入口,保留跳转查看。"
updated_at: "2026-10-03"
base: "dev-v3"
---
# order-v3: 团期子订单封掉订单级行程写口
**服务**: hl-order-service-v3
**PR**: `#8762`(已合入 `dev-v3`,合并提交 `e8d83b0cb`);文档回写 `#8763`
**Issue**: #8750
---
## ⚠️ 关键变化
🔴 **团期子订单不能再在订单详情里改行程。** 改行程天、新增 / 修改 / 删除节点 4 个接口,对团期子订单一律返回新码 `583066`,整笔零写入。原来定制师还能改成功,现在改不了。
🟢 **散客单完全不变。** 请求体、响应体结构都没改。
🟢 **调整快照的 `editableTabLocksHint` 对团期子订单不再含 `ITINERARY` / `SCHEDULE`。** 这两个页签在调整里一提交就是 `587045`(#8350),提示与实际行为现在一致。
---
## 一、背景
#8350(2026-09-24)定案:团期子订单不能在子订单里调整出行日期和行程,但当时只封了 `POST /v3/admin/order/:id/adjustment/submit` 一个入口。订单详情行程页签直接调的 4 个写接口仍然开着:团期管理员被按角色拦住,定制师仍能改团期子订单的行程。团期层本来就不提供行程编辑(#7378 AC-I7)。jw 2026-10-03 定:封。
封口后,团期子订单的行程只来自下单时的产品快照,随团期转期、改期同步,没有人工编辑通路。订单调整、转期重排、改期同步、下单物化这些内部路径不走这 4 个接口,不受影响。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 修改行程天 | PUT | `/v3/admin/order/:orderId/itinerary/days/:dayId` | 行为变更 + 新增错误码 | 团期子订单返回 583066 |
| 2 | 新增行程节点 | POST | `/v3/admin/order/:orderId/itinerary/nodes` | 行为变更 + 新增错误码 | 团期子订单返回 583066 |
| 3 | 修改行程节点 | PUT | `/v3/admin/order/:orderId/itinerary/nodes/:nodeId` | 行为变更 + 新增错误码 | 团期子订单返回 583066 |
| 4 | 删除行程节点 | DELETE | `/v3/admin/order/:orderId/itinerary/nodes/:nodeId` | 行为变更 + 新增错误码 | 团期子订单返回 583066 |
| 5 | 调整快照 | GET | `/v3/admin/order/:id/adjustment/snapshot` | 返回值变更 | 团期子订单 `editableTabLocksHint` 去掉 `ITINERARY` / `SCHEDULE` |
---
## 三、接口详情
### 1. 修改行程天 `PUT /v3/admin/order/:orderId/itinerary/days/:dayId`
**VO**: `ItineraryDayUpsertReqVO` → `Result<ItineraryDayRespVO>`
#### 使用场景
订单详情行程页签改某一天的叙事内容(标题、描述、封面图、三餐等)。本次只改变团期子订单的放行规则。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Path | Long | ✅ | 订单 ID | **不变**;团期子订单直接返回 583066 |
| dayId | Path | Long | ✅ | 该订单的行程天 ID | **不变** |
| dayTitle 等叙事字段 | Body | — | ❌ | 同改前 | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| data | ItineraryDayRespVO | **不变**(散客单);团期子订单返回 583066,`data` 为 null |
#### 请求示例
```json
{
"dayTitle": "海拉尔—额尔古纳 湿地观景",
"description": "上午出发前往额尔古纳湿地,午后登观景台"
}
```
#### 响应示例
散客单(不变):
```json
{
"code": 200,
"message": "成功",
"data": { "id": "7700000000001", "dayNumber": 2, "dayDate": "2026-10-12", "dayTitle": "海拉尔—额尔古纳 湿地观景" },
"success": true
}
```
#### 空数据 / 降级响应
写接口无空数据场景。
#### 错误响应
团期子订单(**本次新增**):
```json
{ "code": 583066, "message": "团期子订单的行程随团期产品,不支持在子订单中修改", "data": null, "success": false }
```
团期管理员角色(存量,先于 583066 判定):
```json
{ "code": 581008, "message": "无权查看此订单", "data": null, "success": false }
```
#### 业务边界
- 判团口径与 #8350 相同:订单归属团期(`group_batch_id` 非空)即为团期子订单。
- 先判团期管理员角色(581008),再判团期子订单(583066),都在任何写之前,拒绝即零写入。
- 订单不存在时不在这里拦,仍按原逻辑返回 583050(行程天不存在)。
---
### 2. 新增行程节点 `POST /v3/admin/order/:orderId/itinerary/nodes`
**VO**: `ItineraryNodeUpsertReqVO` → `Result<ItineraryNodeRespVO>`
#### 使用场景
订单详情行程页签在某一天新增一个节点(景点 / 餐厅 / 活动 / 服务)。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Path | Long | ✅ | 订单 ID | **不变**;团期子订单直接返回 583066 |
| dayId | Body | Long | ✅ | 该订单的行程天 ID | **不变** |
| nodeType | Body | String | ✅ | SCENIC / RESTAURANT / ACTIVITY / SERVICE | **不变** |
| nodeName | Body | String | ✅ | ≤128 | **不变** |
| 其余字段 | Body | — | ❌ | 同改前 | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| data | ItineraryNodeRespVO | **不变**(散客单);团期子订单返回 583066,`data` 为 null |
#### 请求示例
```json
{
"dayId": "7700000000001",
"nodeType": "SCENIC",
"nodeName": "呼伦湖",
"startTime": "09:30"
}
```
#### 响应示例
散客单(不变):
```json
{
"code": 200,
"message": "成功",
"data": { "id": "8800000000001", "dayId": "7700000000001", "nodeType": "SCENIC", "sortOrder": 3, "nodeName": "呼伦湖" },
"success": true
}
```
#### 空数据 / 降级响应
写接口无空数据场景。
#### 错误响应
团期子订单(**本次新增**):
```json
{ "code": 583066, "message": "团期子订单的行程随团期产品,不支持在子订单中修改", "data": null, "success": false }
```
#### 业务边界
- 同接口 1:团期管理员 581008 先判,团期子订单 583066 后判,都在写之前。
- 散客单的节点类型校验、资源名反查、自费售价校验(583064)等全部不变。
---
### 3. 修改行程节点 `PUT /v3/admin/order/:orderId/itinerary/nodes/:nodeId`
**VO**: `ItineraryNodeUpsertReqVO` → `Result<ItineraryNodeRespVO>`
#### 使用场景
订单详情行程页签修改某个节点的叙事字段和结算字段。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Path | Long | ✅ | 订单 ID | **不变**;团期子订单直接返回 583066 |
| nodeId | Path | Long | ✅ | 该订单的节点 ID | **不变** |
| nodeName | Body | String | ✅ | ≤128 | **不变** |
| 其余字段 | Body | — | ❌ | 同改前 | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| data | ItineraryNodeRespVO | **不变**(散客单);团期子订单返回 583066,`data` 为 null |
#### 请求示例
```json
{
"nodeType": "SCENIC",
"nodeName": "呼伦湖",
"description": "湖畔栈道步行约 40 分钟"
}
```
#### 响应示例
散客单(不变):
```json
{
"code": 200,
"message": "成功",
"data": { "id": "8800000000001", "nodeName": "呼伦湖", "description": "湖畔栈道步行约 40 分钟" },
"success": true
}
```
#### 空数据 / 降级响应
写接口无空数据场景。
#### 错误响应
团期子订单(**本次新增**):
```json
{ "code": 583066, "message": "团期子订单的行程随团期产品,不支持在子订单中修改", "data": null, "success": false }
```
#### 业务边界
- 同接口 1。
- 订单调整(`adjustment/submit`)内部改节点不走本接口,不受 583066 影响;它对团期子订单带行程已由 587045 拦截(#8350)。
---
### 4. 删除行程节点 `DELETE /v3/admin/order/:orderId/itinerary/nodes/:nodeId`
**VO**: `ItineraryNodeDeleteReqVO`(可不传)→ `Result<ItineraryNodeRespVO>`
#### 使用场景
订单详情行程页签删除某个节点(软删)。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Path | Long | ✅ | 订单 ID | **不变**;团期子订单直接返回 583066 |
| nodeId | Path | Long | ✅ | 该订单的节点 ID | **不变** |
| editReason | Body | String | ❌ | ≤500 | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| data | ItineraryNodeRespVO | **不变**(散客单,删除前快照);团期子订单返回 583066,`data` 为 null |
#### 请求示例
```http
DELETE /v3/admin/order/2100743225424621570/itinerary/nodes/8800000000001
Authorization: Bearer <管理端令牌>
```
#### 响应示例
散客单(不变):
```json
{
"code": 200,
"message": "成功",
"data": { "id": "8800000000001", "nodeName": "呼伦湖" },
"success": true
}
```
#### 空数据 / 降级响应
写接口无空数据场景。
#### 错误响应
团期子订单(**本次新增**):
```json
{ "code": 583066, "message": "团期子订单的行程随团期产品,不支持在子订单中修改", "data": null, "success": false }
```
#### 业务边界
- 同接口 1。
- 散客单删除节点时的应付款台账联动(599602 锁定拦截等)不变。
---
### 5. 调整快照 `GET /v3/admin/order/:id/adjustment/snapshot`
**VO**: `Result<AdjustmentSnapshotRespVO>`
#### 使用场景
「调整订单」弹窗打开时取快照;`editableTabLocksHint` 告诉前端哪些页签可编辑。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | 订单 ID | **不变** |
| scope | Query | String | ❌ | 逗号分隔子域 | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| data.editableTabLocksHint | Array\<String\> | 🔁 **团期子订单不再含 `ITINERARY` / `SCHEDULE`**;散客单不变 |
| 其余字段 | — | **不变** |
#### 请求示例
```http
GET /v3/admin/order/2100743225424621570/adjustment/snapshot
Authorization: Bearer <管理端令牌>
```
#### 响应示例
团期子订单(资源准备中):
```json
{
"code": 200,
"message": "成功",
"data": {
"basic": { "orderNo": "HL20260924171304411" },
"editableTabLocksHint": ["BASIC", "PEOPLE", "HOTEL_REQ", "VEHICLE_REQ", "FEE"]
},
"success": true
}
```
#### 空数据 / 降级响应
无变化:订单不存在等错误与改前一致。
```json
{ "code": 200, "message": "成功", "data": { "editableTabLocksHint": ["BASIC"] }, "success": true }
```
#### 错误响应
非法 scope(存量):
```json
{ "code": 587003, "message": "调整范围取值非法", "data": null, "success": false }
```
#### 业务边界
- 只对团期子订单从集合里去掉 `ITINERARY` / `SCHEDULE`,其余页签按原来的流程状态规则给出。
- 传单个 scope 时原来是原样回显该 scope;团期子订单传 `ITINERARY` 或 `SCHEDULE` 现在返回空集合。
---
## 四、契约约束与正确调用方式
| 场景 | 调用 | 结果 |
|------|------|------|
| ❌ 团期子订单改行程天 / 增改删节点 | 接口 1~4 | 583066,零写入 |
| ❌ 团期管理员改任意订单行程 | 接口 1~4 | 581008(存量,先于 583066) |
| ✅ 散客单改行程 | 接口 1~4 | 同改前 |
| ✅ 团期子订单打开调整弹窗 | 接口 5 | `editableTabLocksHint` 不含 `ITINERARY` / `SCHEDULE` |
- 前端对团期子订单应隐藏行程页签的编辑、新增、删除按钮,避免用户点了才看到 583066。
- 团期行程汇总下钻(GB-ADM-019)不再提供「跳去改」入口,保留跳转查看即可。
---
## 五、数据库行为
- 无 Flyway migration、无 DDL、无新增写入。
- 团期子订单调接口 1~4:在任何写之前拒绝,`order_itinerary_day` / `order_itinerary_node` / `order_main` 零写入。
- 散客单:写入行为与改前一致。
- 接口 5 只读。
---
## 六、边界行为
- 团期子订单调接口 1~4 → 583066。
- 团期管理员调接口 1~4 → 581008(不论散客还是团期子订单)。
- 订单不存在 → 不在守卫拦,按原逻辑报 583050 / 583051。
- 团期子订单调接口 5 → `editableTabLocksHint` 不含 `ITINERARY` / `SCHEDULE`。
- 散客单 → 全部不变。
---
## 六.6、修改前后对比
| 行为(团期子订单) | 改前 | 改后 |
|------|------|------|
| 定制师 / 管理员改行程天、增改删节点 | 200,写入成功 | 583066,零写入 |
| 团期管理员改行程 | 581008 | 581008(不变) |
| 调整快照 `editableTabLocksHint`(全量) | 含 `ITINERARY`,流程状态早时含 `SCHEDULE` | 两者都不含 |
| 调整快照单 scope=`ITINERARY` / `SCHEDULE` | 回显该 scope | 空集合 |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 请求 / 响应结构不变;团期子订单原来能成功的行程编辑现在返回 583066。
- **前端是否必须同步上线**: 建议同步。行程页签对团期子订单要隐藏编辑、新增、删除按钮;团期行程汇总下钻去掉「跳去改」入口。不改前端也不会写坏数据,只是用户点了会看到 583066 提示。
- **前端 workaround 清理点**: 调整弹窗里对团期子订单置灰出行日期 / 行程页签的自判逻辑,现在可以直接读 `editableTabLocksHint`。
---
## 七、不影响范围
- 散客(非团期)订单的全部行程编辑与调整行为。
- 订单调整统一提交(`adjustment/submit`),团期子订单仍按 #8350 返回 587045。
- 团期转期、改期同步、下单物化行程等内部路径。
- 团期用餐口径(#8230 / #8343)、团期行程汇总与下钻(GB-ADM-018 / 019)的读取结果。
- `POST /v3/admin/order/:id/confirm-itinerary`(只确认状态、不改行程内容)。
---
## 八、测试环境已验证
真实网关(TEST,`https://api.test.1814.love`,自签 admin token),部署检出 `b9f22e3ad`(含合并提交 `e8d83b0cb`),造数:团期子订单 `HL20260929154809598`(团号 T26-0352,资源准备中)、散客单 `HL20260810141518677`(资源准备中):
| 用例 | 请求 | 结果 | 库内读数 |
|------|------|------|----------|
| 团期子订单改行程天 / 增 / 改 / 删节点 | 接口 1~4,管理员 token | 583066 ×4 | 订单主表、行程天 7 行、节点 28 行前后逐字一致 |
| 散客单原值回写行程天 | 接口 1 | 200 | 该天整行未变 |
| 散客单新增「莫日格勒河」→ 改描述 → 删除 | 接口 2 → 3 → 4 | 200 ×3 | 新节点写入、描述更新、软删;原有 14 个节点无变化 |
| 团期管理员改散客单行程 | 接口 1~4,团期管理员 token | 581008 ×4 | 三表前后逐字一致 |
| 团期管理员改团期子订单行程天 | 接口 1,团期管理员 token | 581008 | 角色守卫先于 583066 |
| 团期子订单调整快照 | 接口 5 | 200 | `editableTabLocksHint` = BASIC / PEOPLE / HOTEL_REQ / VEHICLE_REQ / FEE |
| 散客单调整快照 | 接口 5 | 200 | `editableTabLocksHint` 七项齐全,与改前规则一致 |
| 不带令牌 | 接口 1 | 401 | 零写入 |
单元测试:定向集(行程 / 调整 / 错误码 / 架构包 + 转期改期回归)67 类 825 例,失败 2、错误 0;2 条失败在干净基底上逐条复现,属 #8714 既有,与本次无关。本次新增 13 例全部通过,含 ArchTest:订单行程写端点必须调团期子订单守卫、守卫只许该控制器调用。
部署: PR #8762 已合并 `dev-v3`(合并提交 `e8d83b0cb`),TEST 双实例滚动部署完成。
---
## 十、相关文档
- 关联 Issue: [wx/HL#8750](https://git.1814.love/wx/HL/issues/8750)
- 关联 PR: [wx/HL#8762](https://git.1814.love/wx/HL/pulls/8762)、文档 [wx/HL#8763](https://git.1814.love/wx/HL/pulls/8763)
- 前置定案: [wx/HL#8350](https://git.1814.love/wx/HL/issues/8350)
## 关联 / 联系人
### 链接
- **Issue**: [#8750](https://git.1814.love/wx/HL/issues/8750)
- **PR**: [#8762](https://git.1814.love/wx/HL/pulls/8762)
- **Merge commit**: [e8d83b0cb](https://git.1814.love/wx/HL/commit/e8d83b0cb)
### 联系人
- **后端负责人**: @jw
@@ -0,0 +1,394 @@
---
schema: "hl-changelog/v2"
ticket: "8752"
title: "团期管理员一键催办未提交房 / 车需求的定制师:新增 POST requirement/nudge,按定制师每人一条站内信 + 企微,无定制师的户列为跳过"
consumer: "admin"
author: "jw(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "e4cd749bd0afe7388218fe5a538b54cf62269b4f"
target_release: "v2.1"
verified_at: "2026-10-04"
status_note: "新增 POST /v3/admin/order/group-batch/{groupBatchId}/requirement/nudge(权限码 group-batch:manage:团期管理员 / ADMIN / SUPER_ADMIN,定制师 589507)。圈本团「该定制师动手」的户(与定制师待办同源:资源准备中、房 / 车需求未提交或被打回定制师),按定制师聚合每人一条通知(管理端站内信 + 企微,站内信点开到「定制师待办」/order/todos),只投员工不触达客户;无定制师的户在 skipped 里列出。同团 300 秒冷却(589852 带剩余秒数),一条都没发出时不占冷却。已合并 dev-v3(5b204b9d2)并部署 TEST(user-service、order-v3),经网关真实鉴权验收 AC-1~AC-10 通过,工单 #8752 已关。前端待做:团期详情需求 Tab「企微催需求」按钮、看板铃铛接本接口;成功后按 recipients / skipped 提示,wecomBound=false 的定制师提示「未绑企微,只收到站内信」,按钮按 cooldownSeconds 置灰倒计时。前端已交付(2026-10-04):需求 Tab 头「企微催需求」按钮 + 出团管理期行铃铛双挂载点,阶段驱动显隐(招募中置灰/资源准备中可点/之后不展示),结果 dialog 按 recipients/skipped/wecomBound 组文案(null 不提示未绑),冷却按响应 cooldownSeconds 与 589852 报文抠秒双路倒计时,提交 e4cd749bd。"
updated_at: "2026-10-03"
base: "dev-v3"
---
# 团期催办:新增一键催办定制师提交房 / 车需求(管理后台)
> **服务**: hl-order-service-v3(端口 8086/8186)+ hl-user-service(通知配置迁移)
> **PR**: #8777
> **Issue**: #8752
> **日期**: 2026-10-03
> **影响范围**: 管理后台团期详情「需求」Tab 的「企微催需求」按钮、团期看板铃铛(原型 `batchDetail.jsx:889` / `batchBoard.jsx:495`);既有接口零变化
---
## ⚠️ 关键变化
1. **新增写端点** `POST .../{groupBatchId}/requirement/nudge`:团期管理员一键催本团所有「房型 / 用车需求未提交或被打回」户的定制师。此前前端写明「企微催需求无接口契约,不做」,底栏「去催需求」只跳需求 Tab;现在有接口了。
2. **一位定制师一条**:同一定制师名下多户合成一条通知,正文分列房型、用车待提交户数并列出订单号(最多 10 个,多的写「等 N 户」)。
3. **只发员工**:管理端站内信 + 企微,不发短信、不进客户收件箱。站内信链接是 `/order/todos`(定制师待办),**不是团期详情**——定制师角色没有「出团详情」菜单。
4. **同一团 300 秒冷却**:冷却期内再点返回 589852,`message` 里带剩余秒数;前端拿 `cooldownSeconds` 做倒计时置灰即可。
---
## 一、背景
成团后系统按户给定制师开了「房型需求 · 待提交」「用车需求 · 待提交」待办,但待办只在后台列表里,**不推送**;团期管理员发现有户没提交,只能线下去找定制师。原型团期详情有「企微催需求」按钮、看板有铃铛,本单给它补后端。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 一键催办定制师提交房 / 车需求 | POST | `/v3/admin/order/group-batch/{groupBatchId}/requirement/nudge` | 新增接口 | 无请求体;按定制师每人一条站内信 + 企微;同团 300 秒冷却 |
网关无改动(在既有 `/v3/admin/**` → order-service-v3 通配下)。
---
## 三、接口详情
### 1. 一键催办定制师提交房 / 车需求 `POST /v3/admin/order/group-batch/{groupBatchId}/requirement/nudge`
**VO**: `GroupBatchRequirementNudgeRespVO`
#### 使用场景
团期详情「需求」Tab 的「企微催需求」按钮、团期看板铃铛。团期已成团(资源准备中)、还有户没提交房 / 车需求时,团期管理员点一下,系统按定制师聚合、每人发一条站内信 + 企微。成功后用 `recipients` 提示「已催 N 位定制师(M 户)」,`skipped` 非空时提示「K 户未指派定制师,未催」,`wecomBound=false` 的定制师提示「未绑企微,只收到站内信」;按 `cooldownSeconds` 倒计时置灰按钮。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | 团期主键 | 不存在返 589500 |
无请求体、无查询参数。
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| groupBatchId | String | 团期主键(雪花 id,按字符串返回) |
| groupBatchNo | String | 团号,如 `T26-0938` |
| departDate | String | 出发日期 `yyyy-MM-dd` |
| eventCode | String | 固定 `GROUP_BATCH_REQUIREMENT_NUDGE` |
| pendingHouseholdCount | Integer | 待定制师提交的户数(含 skipped 里无定制师的户) |
| consultantCount | Integer | 本次催到的定制师人数(= recipients 条数) |
| enqueuedCount | Integer | 通知成功投进通知中心的定制师人数(≤ consultantCount) |
| skippedHouseholdCount | Integer | 因无定制师跳过的户数(= skipped 条数) |
| recipients | Array | 逐定制师结果,按首个待提交户的顺序 |
| recipients[].consultantId | String | 定制师(员工)id |
| recipients[].consultantName | String | 定制师姓名(企微名,缺省登录名);员工信息查不到时为 null |
| recipients[].wecomBound | Boolean | 是否绑企微:true 同时收到企微;false 只收站内信;查不到时为 null(未知) |
| recipients[].householdCount | Integer | 该定制师名下待提交户数 |
| recipients[].hotelPendingCount | Integer | 其中房型需求待提交 / 被打回的户数 |
| recipients[].vehiclePendingCount | Integer | 其中用车需求待提交 / 被打回的户数 |
| recipients[].households | Array | 该定制师名下待提交户明细(结构同下 `skipped[]`,skipReason 为 null) |
| recipients[].enqueued | Boolean | 通知是否已投进通知中心 |
| recipients[].message | String | 未投进的原因(如「通知中心投递失败」);成功为 null |
| skipped | Array | 无定制师、没人可催的户 |
| skipped[].orderId | String | 子订单 id |
| skipped[].orderNo | String | 订单号 |
| skipped[].teamNo | String | 团号(子订单级) |
| skipped[].customerName | String | 客户姓名 |
| skipped[].hotelPending | Boolean | 房型需求是否待提交 / 被打回 |
| skipped[].vehiclePending | Boolean | 用车需求是否待提交 / 被打回 |
| skipped[].skipReason | String | 跳过原因,现为「订单未指派定制师」 |
| notifiedAt | String | 本次催办时间 `yyyy-MM-dd HH:mm:ss`;一条都没发时为 null |
| cooldownSeconds | Integer | 下次可催前的冷却秒数:有通知发出时 300;一条都没发(全部户无定制师)时 0 |
#### 请求示例
```http
POST /v3/admin/order/group-batch/2106397492469985281/requirement/nudge
Authorization: Bearer <团期管理员 token>
```
#### 响应示例
TEST 实测(团期 T26-0938,两位定制师 + 一户无定制师):
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2106397492469985281",
"groupBatchNo": "T26-0938",
"departDate": "2026-11-18",
"eventCode": "GROUP_BATCH_REQUIREMENT_NUDGE",
"pendingHouseholdCount": 4,
"consultantCount": 2,
"enqueuedCount": 2,
"skippedHouseholdCount": 1,
"recipients": [
{
"consultantId": "1002",
"consultantName": "test_admin",
"wecomBound": true,
"householdCount": 2,
"hotelPendingCount": 2,
"vehiclePendingCount": 1,
"households": [
{
"orderId": "2106397492268658689",
"orderNo": "HL20261003225431608",
"teamNo": "26-2132",
"customerName": "乌云毕力格",
"hotelPending": true,
"vehiclePending": true,
"skipReason": null
},
{
"orderId": "2106397493581516801",
"orderNo": "HL20261003225431974",
"teamNo": "26-9255",
"customerName": "王淑芬",
"hotelPending": true,
"vehiclePending": false,
"skipReason": null
}
],
"enqueued": true,
"message": null
},
{
"consultantId": "2073973862738944001",
"consultantName": "designer_4760",
"wecomBound": false,
"householdCount": 1,
"hotelPendingCount": 1,
"vehiclePendingCount": 1,
"households": [
{
"orderId": "2106397494344839169",
"orderNo": "HL20261003225432223",
"teamNo": "26-6378",
"customerName": "刘志强",
"hotelPending": true,
"vehiclePending": true,
"skipReason": null
}
],
"enqueued": true,
"message": null
}
],
"skipped": [
{
"orderId": "2106397494781087745",
"orderNo": "HL20261003225432341",
"teamNo": "26-3501",
"customerName": "高玉梅",
"hotelPending": true,
"vehiclePending": true,
"skipReason": "订单未指派定制师"
}
],
"notifiedAt": "2026-10-03 22:57:38",
"cooldownSeconds": 300
}
}
```
#### 空数据 / 降级响应
待提交的户全部没有定制师(TEST 实测团期 T26-7327):返回 200,不发任何通知、不写时间线、不占冷却,`cooldownSeconds=0`、`notifiedAt=null`,可立即再点:
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2106397514322350082",
"groupBatchNo": "T26-7327",
"departDate": "2026-11-24",
"eventCode": "GROUP_BATCH_REQUIREMENT_NUDGE",
"pendingHouseholdCount": 1,
"consultantCount": 0,
"enqueuedCount": 0,
"skippedHouseholdCount": 1,
"recipients": [],
"skipped": [
{
"orderId": "2106397514263629826",
"orderNo": "HL20261003225436973",
"teamNo": "26-0624",
"customerName": "孙德胜",
"hotelPending": true,
"vehiclePending": true,
"skipReason": "订单未指派定制师"
}
],
"notifiedAt": null,
"cooldownSeconds": 0
}
}
```
员工信息查询降级(user-service 不可用)时照常催办,只是 `consultantName`、`wecomBound` 为 null(未知),不要显示成「未绑企微」。
#### 错误响应
冷却期内重复催办(`{0}` 为剩余秒数,TEST 实测):
```json
{ "code": 589852, "message": "催办过于频繁,请 298 秒后再试", "success": false, "data": null }
```
没有可催的户(在团户的房 / 车需求都已提交):
```json
{ "code": 589850, "message": "本团期在团户的房型 / 用车需求均已提交,无需催办", "success": false, "data": null }
```
团期还在招募中:
```json
{ "code": 589552, "message": "团期尚未成团,请先完成成团后再操作", "success": false, "data": null }
```
团期已过资源准备中(物料准备中及以后、已取消),定制师已不能提交需求:
```json
{ "code": 589851, "message": "团期当前状态为「已取消」,定制师已不能提交房型 / 用车需求,无法催办", "success": false, "data": null }
```
无权限(如定制师):
```json
{ "code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "success": false, "data": null }
```
| code | 含义 | 前端处理 |
|---|---|---|
| 589500 | 团期不存在 | 提示后返回列表 |
| 589507 | 当前角色无 `group-batch:manage`(定制师、财务等) | 不展示按钮 |
| 589552 | 团期还在招募中 | 按钮置灰「成团后可催」 |
| 589850 | 没有可催的户 | 提示「需求已全部提交」 |
| 589851 | 团期已过资源准备中,需求已冻结 | 不展示按钮 |
| 589852 | 冷却中,`message` 带剩余秒数 | 按剩余秒数倒计时置灰 |
| 589853 | 通知中心暂不可用,一条都没发出 | 提示稍后重试(冷却已释放,可立即重试) |
#### 业务边界
- 权限码 `group-batch:manage`(授 `GROUP_BATCH_MANAGER` / `ADMIN` / `SUPER_ADMIN`),定制师不能点。
- 「待提交」的口径与定制师待办同源:在团户中订单处于资源准备中,且需要房(或车)、对应需求**从未提交**或**被打回定制师**;车侧整团已声明免车的户不算。
- 被房务 / 车队**退回团期管理员重审**(`REJECTED_TO_ADMIN`)的户**不催**——球在管理员手里,不在定制师。需求 Tab 车侧逐户页把这类户显示为「待重提」,与催办户数可能差这一类。
- 一次催全团,不支持按户 / 按定制师勾选;同一定制师名下多户合成一条。
- 只发员工:管理端站内信 + 企微,不发短信 / 小程序 / 公众号,客户收件箱不会出现。
- 冷却按团计 300 秒;一条都没发出(无人可催、通知中心不可用)不占冷却。部分定制师投递失败时冷却照常生效,失败的那几位也要等冷却结束才能再催(看 `recipients[].enqueued`)。
- 「已投进通知中心」不等于「已送达」:站内信 / 企微的实际结果由通知中心异步记录;未绑企微的定制师只收到站内信。
- 停用 / 离职的定制师照样会被催;订单需要改派定制师的请先改派。
- 每次点击都会写一条团期时间线(GB-ADM-096 事件类型 `BATCH_REQUIREMENT_NUDGE`「催办定制师提交需求」),不要轮询调用。
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误调用顺序
- ✅ 进入需求 Tab 时按团期状态决定按钮:资源准备中才展示可点;招募中置灰;之后的阶段不展示。
- ✅ 点击后用响应里的 `cooldownSeconds` 做倒计时;收到 589852 时从 `message` 解析剩余秒数或直接展示 `message`。
- ✅ 用 `recipients` / `skipped` 组提示文案,`wecomBound` 为 null 时不要提示「未绑企微」。
- ❌ 不要把 `enqueued=true` 说成「已送达」。
- ❌ 不要拿响应里的户数去和需求 Tab 车侧「待重提」户数强行对齐(见业务边界第 3 条)。
---
## 五、数据库行为
- **order-v3**:不建表、不改表;每次成功催办在 `group_batch_status_log` 追加一条 DATA 流水(`event_type=BATCH_REQUIREMENT_NUDGE`,content「催办定制师提交需求(N 位定制师 · M 户)」,extra 记定制师 / 订单 / 跳过户 id 与房车户数)。冷却只在 Redis(键 `order:gb:requirement-nudge:{groupBatchId}`,TTL 300 秒),不落库。
- **user-service**:Flyway `V20261003_8752__group_batch_requirement_nudge_notification.sql` 往 `notification_event_config` 插(或按 `uk_event_code` 收敛)一行 `GROUP_BATCH_REQUIREMENT_NUDGE`:站内信 + 企微开、短信 / 小程序 / 公众号关、接收人 `ORDER_CONSULTANT`、站内信链接 `/order/todos`。通知中心消费后写 `admin_message` 与 `notification_send_log`,不写 `user_message`。
---
## 六、边界行为
| 场景 | 行为 |
|---|---|
| 团期不存在 | 589500 |
| 招募中 | 589552 |
| 物料准备中 / 待出发 / 出行中 / 核单 / 已结算 / 已取消 | 589851 |
| 资源准备中但全部户已提交 | 589850 |
| 全部待提交户都没有定制师 | 200,`consultantCount=0`、`cooldownSeconds=0`,不发不留痕 |
| 部分户无定制师 | 有定制师的照发,无定制师的列入 `skipped` |
| 300 秒内再点 | 589852,带剩余秒数 |
| 通知中心不可用(一条都没投进) | 589853,冷却释放可立即重试 |
| 部分定制师投递失败 | 200,失败者 `enqueued=false`、`message` 有原因;冷却生效 |
| 员工信息查不到 | 照常催办,`consultantName` / `wecomBound` 为 null |
---
## 六.5、枚举 / 数据字典
- `skipped[].skipReason`:目前只有「订单未指派定制师」。
- 时间线事件类型 `BATCH_REQUIREMENT_NUDGE`,中文名「催办定制师提交需求」,变更类型 DATA。
- 新错误码段 589850-589899:589850 无可催的户、589851 阶段已冻结、589852 冷却中、589853 通知中心不可用。
---
## 七、不影响范围
- **仅影响**: 新增一个写端点 + 一条通知事件配置。
- **零影响**:
- 需求 Tab 既有接口(requirement-summary、hotel-households、vehicle-households、confirm、reject 等)的入参、出参与错误码
- 定制师待办的开单 / 关单逻辑(判定抽成单源后行为不变)
- 补发成团通知 `notify-formed` 及其冷却
- 小程序端、客户通知
- 零表结构变更、零网关变更、零权限种子变更(复用 `group-batch:manage`)。
---
## 八、测试环境已验证
部署:hl-user-service 与 hl-order-service-v3 = dev-v3 @ 5b204b9d2(2026-10-03 22:31 / 22:34,user-service 先上);TEST `flyway_schema_history` 20261003.8752 success=1;四批取证前各打一次构建身份探针(不存在的团期 → 589500),经网关 `https://api.test.1814.love` 真实鉴权实测(2026-10-03 22:40–23:05);工单 #8752 已验收关单。
| # | 场景 | 结果 |
|---|---|---|
| 1 | 团期管理员(gbm8154test,单角色)催两位定制师 + 一户无定制师的团 | 200;每位定制师恰好一条;无定制师户进 `skipped`;户数与定制师待办逐户一致 |
| 2 | 落库:`admin_message` / `notification_send_log` / `user_message` | 站内信 2 行(link `/order/todos`)、ADMIN_INAPP 两行 status=0;`user_message` 前后 0 行 |
| 3 | 企微 | 未绑定的定制师 `wecomBound=false`、WEWORK status=2「无企微接收人」;已绑定的测试号调到真实企微接口、因测试假 userid 被拒(errcode 81013),**未真实送达** |
| 4 | 冷却 | 1 秒后再点 589852「298 秒」、13 秒后「286 秒」;300 秒后再点 200 |
| 5 | ADMIN / 定制师 | ADMIN 200;定制师 589507 |
| 6 | 招募中 / 全部已提交 / 已取消 / 核单中 / 不存在 | 589552 / 589850 / 589851 / 589851 / 589500 |
| 7 | 全部户无定制师的团 | 200、`cooldownSeconds=0`,不写站内信、不写发送记录、不写时间线 |
| 8 | 时间线 GB-ADM-096 | 读到 `BATCH_REQUIREMENT_NUDGE`「催办定制师提交需求(2 位定制师 · 3 户)」 |
| 9 | 投递失败释放冷却 | 本机真组件注入(真 Redis + 不可达的真 RocketMQ):589853、冷却键已释放、立即重试仍 589853 |
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| — | #7534 | 补发成团通知(300 秒冷却先例) | ✅ |
| — | #7105 | 团期批量催签合同(批量触达 + 逐户结果先例) | ✅ |
| — | #8562 | 用车逐户区分从未提交 / 被打回 | ✅ |
| — | #8228 | 站内信 link 留空成死链(本单 link 不留空的原因) | ✅ |
| **本 PR #8777** | **#8752** | 一键催办定制师提交房 / 车需求 | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#8752](https://git.1814.love/wx/HL/issues/8752)
- 关联 PR: [wx/HL#8777](https://git.1814.love/wx/HL/pulls/8777)
## 关联 / 联系人
### 链接
- **Issue**: [#8752](https://git.1814.love/wx/HL/issues/8752)
- **PR**: [#8777](https://git.1814.love/wx/HL/pulls/8777)
- **Merge commit**: [5b204b9d2](https://git.1814.love/wx/HL/commit/5b204b9d2cb2459238283646c65eff41fd07a3f0)
### 联系人
- **后端负责人**: @jw
@@ -0,0 +1,276 @@
---
schema: "hl-changelog/v2"
ticket: "8753"
title: "团期管理员可为团期产品新增子订单:建单入参新增归属定制师 consultantId,非团期产品仍 581008"
consumer: "admin"
author: "jw(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "cf5af6d6ba2dfb79266edd4b7be53e157ac835f6"
target_release: "v2.1"
verified_at: "2026-10-04"
status_note: "已合并 dev-v3(ea502ba02)并部署 TEST(order-v3 @ ea502ba02),自签 token 经网关实测:团期管理员为团期产品建单带合法 consultantId → 200、定制师落所选账号;不传 581066、非定制师 / 未绑企微 / 不存在 581067、名单不可用 581068(nacos 注入 1ms 超时实测)、非团期产品 581008,均零落库;ADMIN / CUSTOMIZER 传了 consultantId 也被忽略。前端待做:团期看板 PeriodRow 对团期管理员放开「新增子订单」、建单向导加归属定制师下拉(GET /admin/user/designers)。注意:建单向导的报价接口 POST /admin/product/item/:id/quote 在 TEST 上对非超管均 403(product-v2 存量问题,见 #8753 评论),不解决则团期管理员仍提交不了。前端已交付(2026-10-04):PeriodRow「新增子订单」对团期管理员放开,建单向导 Step2 加「归属定制师」必选下拉(/user/designers 数据源,企微名展示,adminId 雪花字符串透传),step2Errors 前置 581066 校验,报文仅团期管理员带 consultantId;报价 403 为 product-v2 存量问题,留后端另行处理。提交 cf5af6d6b。"
updated_at: "2026-10-03"
base: "dev-v3"
---
# order-v3: 团期管理员可为团期产品新增子订单(建单入参新增归属定制师)
**服务**: hl-order-service-v3
**PR**: `#8769`(已合入 `dev-v3`,合并提交 `ea502ba02`)
**Issue**: #8753
---
## ⚠️ 关键变化
🟢 **`POST /v3/admin/order` 入参纯新增 `consultantId`(归属定制师 adminId)**,出参不变。只有团期管理员会读它,其他角色传了也忽略。
🔴 **团期管理员(`GROUP_BATCH_MANAGER`)从「建单一律 581008」改为「可为团期产品建单,必须选归属定制师」**:订单的定制师是所选账号,不是团期管理员本人;建完之后这一单的其余写操作对团期管理员仍是 581008(#8154 不变)。
🟢 **三个新错误码**:`581066` 未选归属定制师、`581067` 所选账号不在定制师名单、`581068` 定制师名单暂不可用(稍后重试)。
---
## 一、背景
原型里团期看板每期都有「新增子订单」,是团期管理员视角。#8154 把订单写端点整体对团期管理员关掉,建单也在其中,前端随之对他隐藏了按钮。jw 10-03 定:团期管理员可以新增子订单,归属定制师选持定制师角色的人。只放开建单还不够——后台建单的定制师默认取当前登录人,团期管理员建出来的单会挂在他自己名下,而他按 #8154 又改不了,所以本次同时要求团期管理员指定归属定制师。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 创建订单 | POST | `/v3/admin/order` | 修改 | 入参新增 `consultantId`;团期管理员可为团期产品建单;新增错误码 581066 / 581067 / 581068 |
---
## 三、接口详情
### 1. 创建订单 `POST /v3/admin/order`
**VO**: `OrderCreateReqVO` → `Result<OrderCreateRespVO>`
#### 使用场景
团期看板某一期点「新增子订单」进入建单向导(深链带 `productId` / `productBatchId` / `departureDate`)。团期管理员在向导里多选一项「归属定制师」,提交后订单挂在该定制师名下,由他跟进补资料、收款;团期管理员只看不改。定制师、管理员等其他角色的建单流程不变。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| consultantId | Body | Long | 团期管理员必填,其他角色不读 | 须为持定制师角色、在职且已绑企微的后台账号 | 🆕 归属定制师 adminId;下拉数据源 `GET /admin/user/designers`(返回的 `id` 即 adminId) |
| productId | Body | Long | ✅ | 团期管理员只能选团期产品(GROUP) | **不变**;团期管理员选非团期产品返回 581008 |
| productBatchId | Body | Long | GROUP 产品必填 | 团期管理员必须带 | **不变**;团期管理员不带返回 581008 |
| tierSeq / departureDate / adultCount / childCount / youngChildCount / babyCount / customerName / customerPhone / customerRemark / createSource / roomCount / tags / sharerOpenid / customizerId | Body | — | — | — | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| consultantId | String | **取值口径变化**:团期管理员建单时为所选归属定制师,其余角色仍为当前登录人 |
| consultantSource | String | 团期管理员建单时为 `MANUAL`(与后台代下单相同) |
| 其余字段 | — | **不变** |
#### 请求示例
```json
{
"productId": "2044306857534636034",
"tierSeq": 1,
"departureDate": "2026-11-05",
"adultCount": 2,
"customerName": "王建国",
"customerPhone": "13947012345",
"productBatchId": "2106318108807593985",
"roomCount": 1,
"consultantId": "1002"
}
```
#### 响应示例
团期管理员账号经网关建单,TEST 实际返回(省略了未变字段):
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "2106348497395765250",
"orderNo": "HL20261003193950403",
"orderStatus": "PENDING_PAY",
"consultantId": "1002",
"consultantSource": "MANUAL",
"productName": "冻干粉发短信给",
"departureDate": "2026-11-05",
"returnDate": "2026-11-07",
"totalAmount": "3360.00",
"groupBatchId": "2106348497525788673",
"productBatchId": "2106318108807593985",
"groupOrder": true
},
"success": true
}
```
#### 空数据 / 降级响应
user-service 的定制师名单拿不到(超时、熔断降级返回空列表、非 200)时,团期管理员建单一律拒绝并返回 `581068`,不会放行,也不会误报成 `581067`;其他角色不查名单,不受影响。
#### 错误响应
团期管理员建单的拒绝顺序:范围(581008)→ 未选定制师(581066)→ 名单不可用(581068)→ 不在名单(581067)。
| code | message | 何时出现 |
|---|---|---|
| 581008 | 无权查看此订单 | 团期管理员选了非团期产品,或团期产品没带 `productBatchId` |
| 581066 | 请选择归属定制师 | 团期管理员没传 `consultantId` |
| 581067 | 所选归属定制师无效,请重新选择持定制师角色的在职账号 | 账号不存在、非定制师、不在职或未绑企微 |
| 581068 | 定制师名单暂不可用,请稍后重试 | user-service 名单接口不可用 |
```json
{
"code": 581066,
"message": "请选择归属定制师",
"data": null,
"success": false
}
```
```json
{
"code": 581067,
"message": "所选归属定制师无效,请重新选择持定制师角色的在职账号",
"data": null,
"success": false
}
```
#### 业务边界
- 只有团期管理员读 `consultantId`;定制师、管理员等传了也忽略,定制师仍是当前登录人。
- 名单口径 = 持定制师角色 + 在职 + 已绑企微,与 `GET /admin/user/designers` 是同一集合;**未绑企微的定制师选不了**。
- `consultantName` 取所选账号的企微姓名,没有则取用户名(与该定制师登录后自己建单时一致)。
- 建单后的改单、改出行人、取消、终止等写操作对团期管理员仍是 581008,改派定制师不在本次范围。
---
## 四、契约约束与正确调用方式
- 团期管理员建单必须带 `productBatchId` 与 `consultantId`;前端判断当前角色是团期管理员时,向导里显示「归属定制师」下拉并设为必填。
- 下拉调 `GET /admin/user/designers`,用返回的 `id`(字符串形式的 adminId)作为 `consultantId`,`name` 作为展示名。不要用 `GET /admin/user/customizers`:那个接口含禁用、锁定和未绑企微的账号,选了会被 581067 拒。
- 581068 是暂时性错误,提示「稍后重试」即可;581067 要让用户换人。
- 建单成功后跳转订单详情:团期管理员只读,详情可看,编辑按钮按 #8154 的 581008 处理。
---
## 五、数据库行为
- 零 DDL、零数据迁移。
- 团期管理员建单写入路径与其他角色相同(`order_main` 等),区别只在 `consultant_id` / `consultant_name` 取所选定制师、`consultant_source=MANUAL`。
- 建单准备阶段(事务外)多一次 user-service 内部调用 `GET /internal/user/admin/by-role-key?roleKey=CUSTOMIZER`,只在团期管理员建单时发生。
- 所有拒绝都发生在落库前,零写入。
---
## 六、边界行为
- 团期管理员 + 团期产品 + 带班期 + 合法 `consultantId` → 建单成功,团期报名户数、人数照常自增,订单 `groupBatchId` 非空。
- 团期管理员给非团期产品硬带 `productBatchId` → 581008(不是普通角色看到的 581056)。
- 零角色账号(网关未透传角色)不算团期管理员,走普通建单分支,与改前一致。
## 六.6、修改前后对比
| 场景 | 改前 | 改后 |
|---|---|---|
| 团期管理员为团期产品建单 | 581008 | 带合法 `consultantId` → 200,定制师 = 所选账号 |
| 团期管理员不传 `consultantId` | 581008 | 581066 |
| 团期管理员建非团期产品单 | 581008 | 581008(不变) |
| 团期管理员改这一单 | — | 581008(#8154 不变) |
| 定制师 / 管理员建单 | 定制师 = 当前登录人 | 不变,传了 `consultantId` 也忽略 |
## 六.7、影响评估
- **是否破坏向后兼容**:否。入参纯新增可选字段,出参不变;只有团期管理员的行为变化(从一律 581008 变为可建团期子订单)。
- **前端是否必须同步上线**:否。前端不改时团期管理员看不到按钮,行为与改前一致。要让团期管理员用起来需前端放开按钮、加定制师下拉,并且**建单向导的报价接口要能通**(见第八节 8.4)。
- **性能**:只有团期管理员建单多一次 user-service 内部调用。
- **回滚**:revert PR #8769 后重新部署 order-v3,无 DDL、无配置。
---
## 七、不影响范围
- 订单详情、列表等读接口:不变。
- 其余订单写接口对团期管理员的 581008:不变(#8154)。
- 小程序端下单(`/v3/mp/...`):不经此入口,不变。
- `GET /admin/user/designers`、`GET /internal/user/admin/by-role-key`:接口本身不变。
---
## 八、测试环境已验证
**环境**:TEST(`https://api.test.1814.love`) **验证时间**:2026-10-03 19:31~19:50
**构建身份**:order-v3 部署 `dev-v3 @ ea502ba02`(本单合并提交),两实例滚动完成。探针:团期管理员不传 `consultantId` 建单,部署前 `581008`,部署后 8/8 为 `581066`。
**身份**:自签 token 直打网关;团期管理员 `gbm8154test`(单角色),归属定制师测试号 `test_admin`(1002)。
### 8.1 正向与归属
| 项 | 结果 |
|---|---|
| 团期管理员建团期子订单(`consultantId=1002`) | 200;`order_main.consultant_id=1002`、`consultant_name=test_admin`、`consultant_source=MANUAL`、`group_batch_id` 非空 |
| 所选定制师 1002 | 订单列表(`orderKind=GROUP`)能查到这一户,详情 200,改客户备注 200 并落库;另一位定制师查不到 |
| 团期管理员 | 读详情 200;改备注、改出行人均 581008,库内未被改动 |
| 入团 | 建单前 0 户 0 人 → 团期管理员再建一户后 4 户 9 人,产品侧报名人数同步 |
### 8.2 反向与范围
| 操作 | 结果 |
|---|---|
| 不传 `consultantId` | 581066,零落库 |
| 传团期管理员本人 / 持定制师角色但未绑企微 / 不存在的 id | 均 581067,零落库 |
| 非团期产品(不带班期、硬带班期)/ 团期产品不带班期 | 均 581008,零落库 |
| ADMIN、CUSTOMIZER 传 `consultantId` | 200,定制师仍为本人 |
### 8.3 降级
nacos 只给 order-v3 的 `userFeignClient` 注入 1ms 超时并重启后,团期管理员带合法 `consultantId` 建单 7 次均 581068,零落库;验完配置原样还原(SHA-256 逐字节一致)并再次重启。
### 8.4 建单向导依赖的读接口(团期管理员)
| 接口 | 结果 |
|---|---|
| `GET /admin/product/line/order-picker`、`GET /admin/product/item/order-picker`、`GET /admin/product/item/:id`、`GET /admin/product/item/:id/pricing-calendar`、`GET /v3/admin/tag-library` | 均 200 |
| `GET /admin/user/designers` | 200,20 人,与「定制师 + 在职 + 已绑企微」的 SQL 名单逐个相同 |
| `POST /admin/product/item/:id/quote` | 🔴 403「无操作权限」——product-v2 存量问题,CUSTOMIZER / ADMIN 同样 403,只有 SUPER_ADMIN 能过;前端提交前必须报价成功,不解决则团期管理员仍提交不了。已在 #8753 列明,待另行处理 |
### 本地证据
| 项 | 读数 |
|---|---|
| 定向 13 组 | order-v3 607 例 0 失败(新增 Service 10 例、Controller 2 例、ArchTest 2 条规则) |
| 变异 | 删掉 Controller 的身份透传、删掉 Service 的策略调用 → 两条新规则同时变红;已还原 |
| order-v3 全量(有 Docker,两半) | A 10956 / 2 失败,B 4935 / 2 失败 / 7 跳过;1140 个可执行测试类全部有报告;3 条在基底逐条复现,1 条为并发负载下的锁等待抖动(单跑 2×14/14),本单零新增 |
---
## 十、相关文档
- Issue `#8753`;PR `#8769`
- 前置:#8154(团期管理员只读守卫)、#8440(同样定点放开 #8154 的先例:退单提交)、#7143(看板「新增子订单」深链)
## 关联 / 联系人
### 链接
- **Issue**: [#8753](https://git.1814.love/wx/HL/issues/8753)
- **PR**: [#8769](https://git.1814.love/wx/HL/pulls/8769)
- **Merge commit**: [ea502ba02](https://git.1814.love/wx/HL/commit/ea502ba02)
### 联系人
- **后端负责人**: @jw
@@ -0,0 +1,657 @@
---
schema: "hl-changelog/v2"
ticket: "8714"
title: "团期核单重做为8类tab明细结构+公摊/指定报名拆账(旧/audit端点下线)"
consumer: "admin"
author: "yst"
change_type: "修改接口"
backend_status: "merged"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "4a9a34637c6d7a669479547703186948f18fdcc9"
target_release: "v2.1"
verified_at: "2026-10-04"
status_note: "整批 6 PR 累积的结构重做,8 tab 出参结构变化 + 15 个新接口 + 旧 /audit/* 端点下线 404 + teamNo 新增,前端需按本契约重对接;sharedCostByType 键值从 BatchCostType 4 值切到 settlement_category 8 值。已合 dev-v3 未部署测试服。前端已交付(2026-10-04):核单页整页重对接——settlement 族 9 端点封装(8 tab GET/PUT、panel、sub-orders、lines 增删、panel/confirm、alloc-preview、invoice)+8 类 tab 明细录入(整 tab PUT 全量+CAS)+公摊/指定报名拆账(全手改 Σ=行总额客户端预校验+试算)+DRAFT→CONFIRMED 确认流+开票/结算门禁,旧 /audit/* 调用与四表常量全部移除,sharedCostByType 消费点本就读 costTypeDesc 字典零适配,提交 4a9a34637。"
updated_at: "2026-10-04"
base: "dev-v3"
---
# order-v3 groupbatch:团期核单重做为 8 类 tab 明细结构 + 公摊/指定报名拆账(管理后台)
> ⚠️ **修改接口(整体重做,破坏性)**:团期核单从旧「四表模型(统一科目行 + 逐户用量 + 逐户分摊)」重做为对齐常规订单核单的「**8 类费用 tab 明细结构**」,新增**公摊 / 指定报名拆账**能力。旧 `/audit/*` 6 个端点**全部下线 404**(开发期无向后兼容),由 8 个分类 tab GET/PUT + 面板族 7 个新端点替代。费用类别从硬编码 AuditCategory 切换为数据字典 `settlement_category`(8 值)。前端原核单页需按本契约**整体重对接**。
## 1. 接口背景
团期核单(#7868 初版)采用「统一科目行 + 逐户用量 + 逐户分摊」四表模型,与单订单核单的 8 类费用 tab 结构不一致:运营要在两套交互之间切换,且旧模型只有「均分 / 指定比例」粗粒度分摊,无法表达「这笔车费全团公摊、那笔房费只摊给指定几户」的真实业务。
#8714 整批 6 个 PR 把团期核单推倒重做:
- **存储**:旧四表 `order_batch_audit` / `_item` / `_detail` / `_alloc` 已 **DROP**,新建 10 张表(1 主表 `order_group_settlement_main` + 8 张分类明细表 + 1 张拆账表 `order_group_settlement_alloc`)。
- **结构**:对齐常规订单核单的 **8 类费用 tab 明细结构**——住宿 / 门票·游玩 / 餐食 / 车辆 / 导游 / 摄影师 / 其他收入 / 其他支出,每类一个 tab,每 tab 内是明细行数组。
- **拆账**:每行支持两种分摊方式——**公摊 SHARED**(按全团人数均摊 / 按户均摊)与**指定报名 DESIGNATED**(勾选子订单子集 + 逐户比例 + 可手改金额),强校验 Σ拆账 = 明细行总额。
- **状态机**:从旧多态简化为 **DRAFT(录入中)→ CONFIRMED(已确认)** 两态;确认后不可逆,不提供 un-confirm。
- **费用类别**:废弃硬编码 AuditCategory,改走数据字典 `settlement_category`(8 值),类别中文名由字典下发。
本批已合并 dev-v3,**尚未部署测试服**(`backend_status: merged`),前端可先做接口层适配,联调待部署后进行。
## 2. 变更清单
| # | 接口 | 变更点 | 类型 |
|---|------|--------|------|
| 1 | `GET …/settlement/{hotels/activities/meals/vehicles/guide-fees/photographer-fees/other-incomes/other-expenses}` | 8 个分类 tab 读端点**路径不变、实现整体替换**:出参从旧科目行结构改为 GroupSettleTabRespVO(明细行 + 拆账 splits + 版本号) | ⚠️ 出参结构重做 |
| 2 | `PUT …/settlement/{同上 8 个路径}` | **新增** 8 个整 tab 暂存写端点(全量替换语义 + expectedVersion CAS) | ✨ 新增接口 |
| 3 | `GET …/settlement/panel` | **新增**核单面板:状态主行 + 8 类合计 + 在团户视图(已摊成本/收入/毛利) | ✨ 新增接口 |
| 4 | `GET …/settlement/sub-orders` | **新增**在团子订单列表(指定报名勾选数据源) | ✨ 新增接口 |
| 5 | `POST …/settlement/lines` | **新增**单条明细行新增 | ✨ 新增接口 |
| 6 | `DELETE …/settlement/lines/{lineId}` | **新增**明细行删除(级联删拆账) | ✨ 新增接口 |
| 7 | `POST …/settlement/panel/confirm` | **新增**确认核单(确认后不可逆;**注意路径是 panel/confirm 不是 /settlement/confirm**,后者被团期结算财务复核占用) | ✨ 新增接口 |
| 8 | `POST …/settlement/alloc-preview` | **新增**拆账试算不落库(前端录入期预览,P2 可选) | ✨ 新增接口 |
| 9 | `POST …/settlement/invoice` | **新增**按子订单开票(GB-ADM-054 自旧 `/audit/invoice` 迁移,门禁改为核单 CONFIRMED) | ✨ 新增接口 |
| 10 | `GET/PUT …/audit`、`POST …/audit/allocate`、`POST …/audit/reallocate`、`GET …/audit/export`、`POST …/audit/invoice` | **全部下线,调旧路径 404**(开发期无向后兼容) | ⚠️ 删除接口 |
| 11 | 出参 VO | `panel.households[]` / `sub-orders[]` / tab `lines[].splits[]` 新增 **teamNo** 团号字段(#8779) | ✨ 字段新增 |
| 12 | `sharedCostByType`(GroupReturnDetailRespVO / GroupBatchSettlementSummaryRespVO) | `costType` 键值从 BatchCostType 4 值(BUS/LEADER/PHOTOGRAPHER/OTHER)**切到 settlement_category 8 值**;`costTypeDesc` 走数据字典 | ⚠️ 键值域变化 |
| 13 | 错误码 | 新增 589750-589755 拆账段;旧 589569/589570 废弃(语义分别由 589753/589750 承接) | ⚠️ 错误码调整 |
路径前缀统一为 `/v3/admin/order/group-batch/{groupBatchId}/settlement`,下文用 `…` 代指。
## 3. 接口详情
| 项 | 说明 |
|---|---|
| 服务 | hl-order-service-v3(端口 8086) |
| 路径前缀 | `/v3/admin/order/group-batch/{groupBatchId}/settlement` |
| 使用场景 | 管理后台 → 团期详情 → 核单 Tab:8 类费用明细录入、公摊/指定报名拆账、确认核单、按子订单开票 |
| 认证 | 管理后台登录态(JWT);网关既有路由 `/v3/admin/**`,无新增网关配置 |
| 权限码 | `group-batch:audit:view`(8 tab GET / panel / sub-orders / alloc-preview);`group-batch:audit:edit`(8 tab PUT / lines 新增删除);`group-batch:audit:allocate`(确认核单);`group-batch:audit:invoice`(开票)。**缺码一律 589507** |
| 幂等性 | 写口不加 `@Idempotent`:并发与重复提交由 main 主行锁 + **expectedVersion CAS** 兜底(过期必吃 589573);开票防重由发票域自带 SETNX 承接 |
| 限流 | 无特殊限流 |
## 4. 接口入参
### 4.1 路径 / Query 参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | 团期 ID,全部端点共有 |
| lineId | Path | Long | ✅ | 明细行 ID,仅 `DELETE …/lines/{lineId}` |
| category | Query | string | ✅ | 费用类别 8 值之一,仅 `DELETE …/lines/{lineId}`(决定从哪张分类表删) |
| expectedVersion | Query | int | ✅ | 乐观锁版本(≥0),仅 `DELETE …/lines/{lineId}`;过期返 589573 |
### 4.2 `PUT …/settlement/{tab}` 整 tab 暂存请求体(GroupSettleTabSaveReqVO)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| expectedVersion | int | ✅ | 乐观锁版本(GET tab/panel 返回的 `version` **原样回传**);与库值不符返 589573,本次请求零写入 |
| lines | array | ✅ | 整 tab **全量**行数组(≤500 行)。**全量替换语义**:库里存在但未提交的行 = 删除;空数组 `[]` = 清空本 tab |
`lines[]` 元素(GroupSettleLineSaveReqVO)公共列:
| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|
| lineId | Long | 可空 | - | 已有行 ID;空 = 新增行 |
| allocMode | string | ✅ | `SHARED` / `DESIGNATED` | 分摊方式:SHARED=公摊 / DESIGNATED=指定报名 |
| allocRule | string | 条件必填 | `PER_HEAD_AVG` / `PER_ORDER_AVG` | 公摊口径;SHARED 缺省按类别默认(导游/摄影师/其他收入=按户均摊,其余=按人数均摊);**DESIGNATED 必须为 null** |
| allocGroup | string | 可空 | ≤32 字 | 分摊分组键(如 BUS/SUV;同组内单独均摊) |
| budgetAmount | number | 可空 | ≥0,2 位小数 | 预算额/带出值(只对比不参与拆账计算) |
| actualAmount | number | ✅ | ≥0,2 位小数 | **实际总额(拆账基准)** |
| changeReason | string | 条件必填 | ≤255 字 | 改价原因(实际总额与库值不同且偏离带出值时必填) |
| paymentMethod | string | 可空 | `SIGNED` / `COMPANY_PAID` / `CASH_PAID` | 付款方式:签单 / 对公已付 / 现金已付(**固定 3 值枚举,不走数据字典**) |
| voucherUrls | string[] | 可空 | ≤9 张 | 凭证图 URL 数组 |
| remark | string | 可空 | ≤255 字 | 备注 |
| splits | array | 条件必填 | - | 拆账明细:**仅 DESIGNATED 行必填且 ≥1**;SHARED 行服务端自动重算,提交内容被忽略 |
`splits[]` 元素(SplitSaveReqVO,仅指定报名行):
| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|
| orderId | Long | ✅ | 须属本团在团户 | 承担子订单 ID;不属本团返 589572;同户重复出现被拒 |
| ratio | number | 可空 | ≥0,6 位小数 | 拆分权重(不强制合计为 1,服务端按权重归一化) |
| amount | number | 可空 | ≥0,2 位小数 | **手改金额**(非空优先,不再参与权重分剩余);**全部手改时 Σamount 必须分毫不差 = 行 actualAmount**,否则 589750 |
| note | string | 可空 | ≤255 字 | 备注(提前离团等) |
`lines[]` 各类特有列(只填本 tab 类别的列,其余留空;字段定义同 §5.2 各类特有列表):
- 住宿 HOTEL:hotelId / roomTypeId / dayNumber(≥1) / stayDate / hotelName / roomTypeName / roomCount(≥0) / unitPrice
- 门票·游玩 TICKET:dayNumber / dayDate / scenicName / specName / ticketCount(≥0) / ticketUnitPrice
- 餐食 MEAL:mealType / mealDate / mealName / restaurantId / restaurantName / quantity(≥0) / unitPrice
- 车辆 VEHICLE:serviceStartDate / serviceEndDate / vehicleId / vehiclePlate / vehicleModelName / driverId / driverName / dailyPrice
- 导游 GUIDE / 摄影师 PHOTOGRAPHER:staffId / staffName / workDays(≥0,1 位小数) / perDayFee
- 其他收入 OTHER_INCOME:incomeDate / projectName / projectCategory / specification / quantity(2 位小数) / unitPrice
- 其他支出 OTHER_EXPENSE:expenseType / expenseDate / projectName
### 4.3 `POST …/settlement/lines` 新增单条明细行(GroupSettleLineCreateReqVO)
继承 4.2 行字段(`lineId` 传了也被忽略),额外两个字段:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| category | string | ✅ | 费用类别 8 值之一(决定落哪张分类明细表;非法值 589753) |
| expectedVersion | int | ✅ | 乐观锁版本(同 4.2) |
### 4.4 `POST …/settlement/panel/confirm` 确认核单(GroupSettleConfirmReqVO)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| expectedVersion | int | ✅ | 乐观锁版本(GET panel/tab 返回的 `version` 原样回传) |
**确认后不可逆**:不提供 un-confirm,前端须二次确认弹窗提示「确认后不可修改」。确认后全部写口拒绝(589568)。
### 4.5 `POST …/settlement/alloc-preview` 拆账试算(GroupSettleAllocPreviewReqVO,不落库)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| category | string | ✅ | 费用类别 8 值之一 |
| lines | array | ✅ | 待试算明细行数组(≤500 行,结构同 4.2 `lines[]`);**无 expectedVersion**(纯试算不写库) |
### 4.6 `POST …/settlement/invoice` 按子订单开票(GroupBatchInvoiceReqVO)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| orderId | Long | ✅ | 子订单 ID(须为本团在团户,否则 589572;该户已有有效发票 589571) |
| titleType | string | ✅ | `PERSONAL` 个人 / `COMPANY` 单位 |
| title | string | ✅ | 发票抬头(≤128 字,公司全称或个人姓名) |
| taxNo | string | 条件必填 | 税号(≤32 字,**titleType=COMPANY 时必填**,跨字段约束由 Service 判定) |
| email | string | ✅ | 收件邮箱(合法邮箱格式,≤128 字) |
发票类型不由前端选:服务端固定**增值税普通发票**。本请求**不带 expectedVersion**(开票不写核单主行,version 不变)。
## 5. 出参字段
通用约定:**金额一律字符串输出**(DECIMAL 防精度),**ID 一律字符串输出**(Long 防 JS 精度丢失),日期 `yyyy-MM-dd`,日期时间 `yyyy-MM-dd HH:mm:ss`。
### 5.1 `GET …/settlement/{tab}` 分类 tab 出参(GroupSettleTabRespVO)
| 字段 | 类型 | 说明 |
|------|------|------|
| groupBatchId | string | 团期 ID |
| category | string | 本 tab 类别值(**activities tab 恒 TICKET**) |
| categoryName | string | 类别中文名(数据字典 `settlement_category`) |
| status | string | 核单状态:`DRAFT` 录入中 / `CONFIRMED` 已确认 |
| version | int | 乐观锁版本(PUT/DELETE/confirm 回传 expectedVersion 用) |
| editable | boolean | 是否可编辑(= status == DRAFT;false 时前端禁用全部写交互) |
| budgetTotal | string 或 null | 本类 Σ 带出值/预算额(全行为空则 null) |
| actualTotal | string | 本类 Σ 实际总额 |
| allocatedTotal | string | 本类已拆账金额(Σ splits.amount) |
| lines | array | 明细行数组(无行时 `[]`;**首读未落库时返回「两层带出预览」行**:lineId=null、splits=[],不落库) |
**首读即建行**:任何团期状态(含未返团)首读都返回真实结构,不再是旧版 NOT_STARTED 空壳。
### 5.2 `lines[]` 明细行(LineVO)——公共列
| 字段 | 类型 | 说明 |
|------|------|------|
| lineId | string 或 null | 明细行 ID(带出预览行为 null) |
| confirmStatus | string | 行确认态:`UNCONFIRMED` / `CONFIRMED`(核单确认时批量翻 CONFIRMED) |
| allocMode | string | `SHARED` 公摊 / `DESIGNATED` 指定报名 |
| allocRule | string 或 null | `PER_HEAD_AVG` 按全团人数均摊 / `PER_ORDER_AVG` 按户均摊;DESIGNATED 恒 null |
| allocGroup | string 或 null | 分摊分组键(如 BUS/SUV) |
| budgetAmount | string 或 null | 带出值/预算额(只对比不参与计算) |
| actualAmount | string | 实际总额(拆账基准) |
| changeReason | string 或 null | 改价原因 |
| sourceType | string | `MANUAL` 手工 / `CARRY_OVER` 第 1 层带出 / `BATCH_COST` 共享成本带出 |
| paymentMethod | string 或 null | `SIGNED` 签单 / `COMPANY_PAID` 对公已付 / `CASH_PAID` 现金已付 |
| voucherUrls | string[] | 凭证图 URL 数组(无则 `[]`) |
| remark | string 或 null | 备注 |
| splits | array | 本行拆账结果(未拆账为 `[]`),结构见 5.3 |
**各类特有列**(出参为全类别并集,**非本 tab 的特有列恒为 null**,按 category 取本类列即可):
| 类别 | 特有列 |
|------|--------|
| HOTEL 住宿 | hotelId / roomTypeId / dayNumber / stayDate / hotelName / roomTypeName / roomCount / unitPrice(元/间夜) |
| TICKET 门票·游玩 | dayNumber / dayDate / scenicName / specName / ticketCount / ticketUnitPrice |
| MEAL 餐食 | mealType / mealDate / mealName / restaurantId / restaurantName / quantity(份数) / unitPrice(元/份) |
| VEHICLE 车辆 | serviceStartDate / serviceEndDate / vehicleId / vehiclePlate / vehicleModelName / driverId / driverName / dailyPrice(日单价) |
| GUIDE 导游 / PHOTOGRAPHER 摄影师 | staffId / staffName / workDays(1 位小数) / perDayFee(日费) |
| OTHER_INCOME 其他收入 | incomeDate / projectName / projectCategory / specification / quantity(2 位小数) / unitPrice |
| OTHER_EXPENSE 其他支出 | expenseType / expenseDate / projectName |
### 5.3 `lines[].splits[]` 拆账明细(SplitVO)
| 字段 | 类型 | 说明 |
|------|------|------|
| orderId | string | 子订单 ID |
| teamNo | string 或 null | **团号**(order_main.team_no,订金支付成功后生成;未付订金为 null)。⚠️ `alloc-preview` 试算回执的 splits.teamNo **本期恒 null 未填充** |
| householdName | string | 户名(客户姓名快照) |
| ratio | string | 拆账比例(6 位小数;公摊=计算快照,指定报名=用户比例/归一化权重快照) |
| peopleCount | int 或 null | 该户参与人数快照 |
| amount | string | 拆账金额 |
| roundingBearer | boolean | 尾差承担户标记(公摊尾差整笔记 eligible 集合中 **order_id 最小**的户) |
| note | string 或 null | 备注(提前离团等) |
### 5.4 `PUT …/settlement/{tab}` 暂存回执(GroupSettleTabWriteRespVO)
| 字段 | 类型 | 说明 |
|------|------|------|
| version | int | 写入后的乐观锁版本(已 +1) |
| status | string | 核单状态(恒 `DRAFT`;CONFIRMED 时写口已被 589568 拦截) |
| lines | array | 写入后本 tab 全量明细行(结构同 5.2,含重算后的 splits),**前端直接整页替换** |
### 5.5 `GET …/settlement/panel` 面板出参(GroupSettlePanelRespVO)
| 字段 | 类型 | 说明 |
|------|------|------|
| groupBatchId | string | 团期 ID |
| status | string | `DRAFT` / `CONFIRMED`(确认后不可逆) |
| version | int | 乐观锁版本 |
| confirmedAt | string 或 null | 确认时间(DRAFT 为 null) |
| confirmedByName | string 或 null | 确认人姓名(DRAFT 为 null) |
| editable | boolean | 是否可编辑(= status == DRAFT) |
| readyToAllocate | boolean | 各户第 1 层核单是否全部定稿 |
| blockingOrderIds | string[] | 第 1 层核单未定稿的子订单 ID;空数组 = 全部定稿 |
| categories | array | **8 类合计(恒 8 条,按类别序)**,元素见下表 |
| households | array | 在团子订单列表(仅排除已取消),元素见下表 |
`categories[]`(CategorySummaryVO):
| 字段 | 类型 | 说明 |
|------|------|------|
| category | string | 费用类别 8 值 |
| categoryName | string | 类别中文名(字典 `settlement_category`) |
| budgetAmount | string 或 null | 该类 Σ 带出值/预算额(全行为空则 null) |
| actualAmount | string | 该类 Σ 实际总额 |
| allocatedAmount | string | 该类已拆账金额(Σ splits.amount) |
| unallocatedAmount | string | 该类未拆账金额(actualAmount − allocatedAmount) |
`households[]`(HouseholdVO):
| 字段 | 类型 | 说明 |
|------|------|------|
| orderId | string | 子订单 ID |
| orderNo | string | 订单号 |
| teamNo | string 或 null | 团号(未付订金为 null) |
| householdName | string | 户名 |
| peopleCount | int | 人数 |
| roomCount | int 或 null | 房间数 |
| allocatedCost | string | 已摊成本(该户 7 个支出类 Σ splits.amount) |
| allocatedIncome | string | 已摊收入(该户其他收入 Σ splits.amount) |
| grossProfit | string | 毛利(应收口径 − 已摊成本 + 已摊收入) |
### 5.6 `GET …/settlement/sub-orders` 在团子订单出参(GroupSettleSubOrderRespVO[])
| 字段 | 类型 | 说明 |
|------|------|------|
| orderId | string | 子订单 ID |
| orderNo | string | 订单号 |
| teamNo | string 或 null | 团号(未付订金为 null) |
| householdName | string | 户名 |
| peopleCount | int | 人数(公摊按人摊的分子预览) |
| roomCount | int 或 null | 房间数 |
在团口径 = **仅排除已取消**。用途:指定报名勾选弹窗的数据源。
### 5.7 写端点回执
`POST …/settlement/lines`(GroupSettleLineCreateRespVO):
| 字段 | 类型 | 说明 |
|------|------|------|
| lineId | string | 新增明细行 ID |
| version | int | 写入后的乐观锁版本(已 +1) |
`DELETE …/settlement/lines/{lineId}`(GroupSettleLineDeleteRespVO):
| 字段 | 类型 | 说明 |
|------|------|------|
| version | int | 写入后的乐观锁版本(已 +1);该行拆账已同事务级联软删 |
`POST …/settlement/panel/confirm`(GroupSettleConfirmRespVO):
| 字段 | 类型 | 说明 |
|------|------|------|
| status | string | 恒 `CONFIRMED` |
| version | int | 确认后的乐观锁版本(已 +1) |
| confirmedAt | string | 确认时间 |
| confirmedByName | string | 确认人姓名 |
`POST …/settlement/alloc-preview`(GroupSettleAllocPreviewRespVO):
| 字段 | 类型 | 说明 |
|------|------|------|
| lines | array | 试算后的明细行(结构同 5.2 LineVO;confirmStatus 恒 UNCONFIRMED、lineId 原样回显或 null、sourceType 恒 MANUAL;splits 为重算预览,**splits[].teamNo 本期恒 null**)。**无任何写库** |
`POST …/settlement/invoice`(GroupSettleInvoiceRespVO):
| 字段 | 类型 | 说明 |
|------|------|------|
| status | string | 核单状态(恒 `CONFIRMED`,开票门禁) |
| version | int | 核单乐观锁版本(开票不改主行,原值) |
| invoiceId | string | 新发票 ID |
## 6. 枚举 / 数据字典
### category(费用类别,数据字典 `settlement_category`,8 值)
| 值 | 中文 | 路径段 | 支出/收入 |
|----|------|--------|-----------|
| `HOTEL` | 住宿 | hotels | 支出 |
| `TICKET` | 门票·游玩 | **activities**(旧名沿用,出参 category 恒 TICKET) | 支出 |
| `MEAL` | 餐食 | meals | 支出 |
| `VEHICLE` | 车辆 | vehicles | 支出 |
| `GUIDE` | 导游 | guide-fees | 支出 |
| `PHOTOGRAPHER` | 摄影师 | photographer-fees | 支出 |
| `OTHER_INCOME` | 其他收入 | other-incomes | **收入** |
| `OTHER_EXPENSE` | 其他支出 | other-expenses | 支出 |
中文名以后端下发的 `categoryName` 为准(字典驱动),前端不要硬编码。
### allocMode(分摊方式)
| 值 | 含义 | 配套约束 |
|----|------|----------|
| `SHARED` | 公摊(全团 eligible 户分摊) | allocRule 可省(按类别默认);splits 由服务端重算,提交被忽略 |
| `DESIGNATED` | 指定报名(勾选子订单子集承担) | allocRule 必须 null;splits 必填 ≥1(未选返 589751);Σ拆账=行总额(不等返 589750) |
### allocRule(公摊口径,仅 SHARED 有效)
| 值 | 含义 | 类别默认 |
|----|------|----------|
| `PER_HEAD_AVG` | 按全团人数均摊(eligible 户按人数加权,人数 0 的户不参与) | 住宿/门票·游玩/餐食/车辆/其他支出默认 |
| `PER_ORDER_AVG` | 按户均摊(eligible 户等权,不看人数) | **导游/摄影师/其他收入默认** |
公摊尾差规则:非尾差户按 ROUND_HALF_UP 到分,**尾差 = 总额 − Σ其余户份额,整笔记 eligible 集合中 order_id 最小的户**(splits 里 `roundingBearer=true`);尾差户金额为负时全组改 ROUND_DOWN 重算,尾差恒非负。
### status(核单状态)/ confirmStatus(行确认态)
| 字段 | 值 | 含义 |
|------|----|------|
| status | `DRAFT` | 录入中(可编辑) |
| status | `CONFIRMED` | 已确认(不可逆,全部写口拒 589568) |
| confirmStatus | `UNCONFIRMED` / `CONFIRMED` | 明细行确认态,随核单确认批量翻 CONFIRMED |
### sourceType(明细行来源)
| 值 | 含义 |
|----|------|
| `MANUAL` | 手工录入 |
| `CARRY_OVER` | 第 1 层(子订单核单)带出 |
| `BATCH_COST` | 共享成本带出 |
### paymentMethod(付款方式,固定 3 值枚举,**不走数据字典**)
| 值 | 含义 |
|----|------|
| `SIGNED` | 签单 |
| `COMPANY_PAID` | 对公已付 |
| `CASH_PAID` | 现金已付 |
### 其他字典(特有列内引用)
| 字段 | 字典 | 取值 |
|------|------|------|
| mealType(餐食) | `meal_type` | `BREAKFAST` / `LUNCH` / `DINNER` / `SELF` |
| expenseType(其他支出) | `expense_type` | `FUEL` / `TOLL` / `PARKING` / `RENTAL` / `MAINTENANCE` / `OTHER` |
## 7. 错误码
| 错误码 | 文案({0} 为动态定位串) | 触发场景 |
|--------|--------------------------|----------|
| 589507 | 无操作权限(当前角色未授予团期权限,或该团期不在您名下) | 缺 `group-batch:audit:*` 对应权限码 |
| 589567 | 该团期尚未进入整团核单:{0} | 尚未建核单主记录时直接调写口/确认(正常链路首读 GET 即建行,不会遇到) |
| 589568 | 整团核单当前状态不允许该操作:{0} | ① CONFIRMED 后任何写操作;② 确认核单时在团子订单第 1 层核单未全部定稿({0} 带未定稿订单号清单,与 panel 的 blockingOrderIds 对应) |
| 589571 | 已开票冲突:{0} | 该子订单已有有效发票仍重复开票 |
| 589572 | 所选订单不属于本团期的整团核单范围 | 指定报名 splits / 开票 orderId 不属本团在团户 |
| 589573 | 整团核单数据已被他人修改(当前版本 {0},提交版本 {1}),请刷新后重试 | expectedVersion 与库值不一致(CAS 并发冲突),本次请求零写入,**前端须重新 GET 读回全量再提交** |
| 589750 | 拆账合计与明细行总额不一致({0}),请核对后再提交 | 指定报名行 Σ拆账 ≠ actualAmount;{0} 为四数文案「{行名}拆账合计 {实} / 明细总额 {应},差额 {差}」 |
| 589751 | 指定报名须至少选择一个承担子订单 | DESIGNATED 行 splits 为空 |
| 589752 | 全团人数为 0,无法按人数均摊;请改用按户均摊或先维护出行人 | SHARED + PER_HEAD_AVG 时全团人数为 0 |
| 589753 | 核单拆账入参非法:{0} | 金额/单价/数量为负、未知费用类别等入参层非法 |
| 589754 | 在团子订单已变化({0}),已拆账数据失效,请重新暂存各分类完成拆账后再确认 | 确认核单前置校验:子订单退团/转入后未重新暂存拆账 |
| 589755 | 核单无明细行,不可确认,请先在各分类 Tab 录入明细后再确认 | 零明细行尝试确认 |
**废弃错误码**(保留占位不再抛出,前端可清理映射):589569(金额非法 → 由 589753 承接)、589570(四数对平 → 由 589750 承接)、589517(导出超限,导出端点已下线)。
## 8. 示例
### 8.1 典型:GET 住宿 tab(已录入两行,一行公摊一行指定报名)
请求:
GET /v3/admin/order/group-batch/1934567890123456789/settlement/hotels
响应 200:
```json
{
"code": 0,
"data": {
"groupBatchId": "1934567890123456789",
"category": "HOTEL",
"categoryName": "住宿",
"status": "DRAFT",
"version": 3,
"editable": true,
"budgetTotal": "8600.00",
"actualTotal": "8400.00",
"allocatedTotal": "8400.00",
"lines": [
{
"lineId": "1934567890123456790",
"confirmStatus": "UNCONFIRMED",
"allocMode": "SHARED",
"allocRule": "PER_HEAD_AVG",
"allocGroup": null,
"budgetAmount": "4800.00",
"actualAmount": "4600.00",
"changeReason": "酒店涨价已与对方确认",
"sourceType": "CARRY_OVER",
"paymentMethod": "SIGNED",
"voucherUrls": ["https://oss.example.com/voucher/1.jpg"],
"remark": "含早餐",
"hotelId": "1934567890123400001",
"roomTypeId": "1934567890123400002",
"dayNumber": 2,
"stayDate": "2026-10-03",
"hotelName": "图嘎营地",
"roomTypeName": "蒙古包",
"roomCount": 5,
"unitPrice": "380.00",
"splits": [
{ "orderId": "1934567890123450001", "teamNo": "26-0001", "householdName": "张三",
"ratio": "0.300000", "peopleCount": 3, "amount": "1380.00", "roundingBearer": true, "note": null },
{ "orderId": "1934567890123450002", "teamNo": "26-0002", "householdName": "李四",
"ratio": "0.700000", "peopleCount": 7, "amount": "3220.00", "roundingBearer": false, "note": null }
]
},
{
"lineId": "1934567890123456791",
"confirmStatus": "UNCONFIRMED",
"allocMode": "DESIGNATED",
"allocRule": null,
"allocGroup": null,
"budgetAmount": "3800.00",
"actualAmount": "3800.00",
"changeReason": null,
"sourceType": "MANUAL",
"paymentMethod": "COMPANY_PAID",
"voucherUrls": [],
"remark": "升级房型差价",
"hotelId": "1934567890123400001",
"roomTypeId": "1934567890123400003",
"dayNumber": 2,
"stayDate": "2026-10-03",
"hotelName": "图嘎营地",
"roomTypeName": "豪华蒙古包",
"roomCount": 1,
"unitPrice": "3800.00",
"splits": [
{ "orderId": "1934567890123450002", "teamNo": "26-0002", "householdName": "李四",
"ratio": "1.000000", "peopleCount": 7, "amount": "3800.00", "roundingBearer": false, "note": "李四家要求升级" }
]
}
]
}
}
```
### 8.2 边界:PUT 整 tab 暂存(指定报名混合「比例 + 手改金额」+ 未付订金 teamNo=null)
请求:
PUT /v3/admin/order/group-batch/1934567890123456789/settlement/vehicles
Content-Type: application/json
```json
{
"expectedVersion": 3,
"lines": [
{
"lineId": null,
"allocMode": "DESIGNATED",
"allocRule": null,
"actualAmount": 3000.00,
"paymentMethod": "CASH_PAID",
"serviceStartDate": "2026-10-02",
"serviceEndDate": "2026-10-04",
"vehiclePlate": "蒙E·12345",
"vehicleModelName": "考斯特",
"driverName": "巴特尔",
"dailyPrice": 1500.00,
"splits": [
{ "orderId": "1934567890123450001", "ratio": 1.000000, "amount": null, "note": null },
{ "orderId": "1934567890123450002", "ratio": null, "amount": 2000.00, "note": "手改固定 2000" }
]
}
]
}
```
响应 200(李四家手改 2000 优先,张三家按权重吸收剩余 1000 并承担尾差;该两户未付订金故 teamNo=null):
```json
{
"code": 0,
"data": {
"version": 4,
"status": "DRAFT",
"lines": [
{
"lineId": "1934567890123456800",
"confirmStatus": "UNCONFIRMED",
"allocMode": "DESIGNATED",
"allocRule": null,
"actualAmount": "3000.00",
"sourceType": "MANUAL",
"splits": [
{ "orderId": "1934567890123450001", "teamNo": null, "householdName": "张三",
"ratio": "0.333333", "peopleCount": 3, "amount": "1000.00", "roundingBearer": true, "note": null },
{ "orderId": "1934567890123450002", "teamNo": null, "householdName": "李四",
"ratio": null, "peopleCount": 7, "amount": "2000.00", "roundingBearer": false, "note": "手改固定 2000" }
]
}
]
}
}
```
读法:回执 `lines` 是写入后本 tab 全量回读(含重算后的 splits),前端直接整页替换;`version` 已 +1,下次写操作回传 4。
### 8.3 业务失败
**8.3a 指定报名拆账合计 ≠ 行总额(589750)**:
PUT /v3/admin/order/group-batch/1934567890123456789/settlement/hotels
```json
{ "expectedVersion": 4, "lines": [ { "allocMode": "DESIGNATED", "actualAmount": 3800.00,
"splits": [ { "orderId": "1934567890123450001", "amount": 3000.00 } ] } ] }
```
```json
{ "code": 589750,
"message": "拆账合计与明细行总额不一致(图嘎营地蒙古包拆账合计 3000.00 / 明细总额 3800.00,差额 800.00),请核对后再提交" }
```
**8.3b 调旧 /audit 路径(已下线,404)**:
GET /v3/admin/order/group-batch/1934567890123456789/audit
HTTP/1.1 404 Not Found
旧 `GET/PUT …/audit`、`POST …/audit/allocate`、`POST …/audit/reallocate`、`GET …/audit/export`、`POST …/audit/invoice` 六个端点**全部已删除**,网关/服务均不再路由,调用一律 404。前端若保留旧核单页代码必须整体替换。
## 9. 业务边界
**适用**:
- 管理后台团期详情的整团核单:8 类费用明细录入 / 暂存 / 拆账 / 确认 / 按子订单开票。
- 公摊(全团分摊)与指定报名(部分户承担)两种成本核算口径的录入与试算。
**不适用**:
- **逐子订单独立应收应付**:拆账只影响成本/毛利**核算口径**,不生成逐户应收应付单;财务主链仍一团一张报账单(biz_no = 团号 batch_no),走既有 `POST …/settlement/finalize` → `POST …/settlement/confirm`(财务复核,**与本次新增的 panel/confirm 是两个不同端点**)。
- 确认后修改:CONFIRMED 不可逆,不提供 un-confirm;录错只能走后端人工处理。
- 旧四表模型的科目行/逐户用量交互:已随旧表 DROP 彻底移除。
**特殊边界**:
- **首读即建行**:任何团期状态(含未返团)GET tab/panel 都会建核单主行并返回真实结构;库里无明细行时 tab 返回「两层带出预览」行(lineId=null、splits=[],不落库)——前端不要把预览行当已保存行渲染删除/编辑按钮(lineId 为 null 即预览行)。
- **全量替换语义**:PUT 整 tab 是覆盖写——库里存在但未提交的行会被删除;前端编辑后必须提交**整 tab 全量行**,不能只传改动行。
- **CAS 冲突处理**:任何写口返 589573 时,前端必须重新 GET 读回全量(含新 version)再让用户基于最新数据重改,不能本地 version+1 重试。
- **确认前置**:确认核单要求 ① 有明细行(否则 589755);② 在团子订单第 1 层核单全部定稿(否则 589568,未定稿清单见 panel `blockingOrderIds` / `readyToAllocate`,前端可在确认按钮上据此置灰);③ 在团户集合与已落拆账一致(退团/转入后须重新暂存,否则 589754)。
- **开票门禁**:仅 CONFIRMED 后可开票;一户一票(589571)。
- **eligible 户**:公摊的 eligible 集合 = 在团户(仅排除已取消);PER_HEAD_AVG 时人数为 0 的户不参与分摊。
## 10. 修改前后对比
### 字段级对比
| 项 | 原来(旧 /audit 契约) | 现在(#8714 重做后) |
|----|--------------------------|----------------------|
| 核单数据载体 | 统一科目行 + 逐户用量 + 逐户分摊(四表) | 8 类分类明细行 + 行内 splits 拆账(10 表) |
| 费用类别 | 硬编码 AuditCategory | 数据字典 `settlement_category` 8 值,`categoryName` 字典下发 |
| 核单状态机 | DRAFT → ALLOCATED → CHECKED 等多态 | **DRAFT → CONFIRMED 两态**,确认后不可逆 |
| 分摊方式 | 均分 / 指定比例(粗粒度) | SHARED 公摊(按人/按户两口径)/ DESIGNATED 指定报名(比例 + 手改金额混合) |
| 尾差承担 | 旧决策(order_id 最小户) | **沿用**:eligible 集合 order_id 最小户,`roundingBearer` 标记 |
| 写口并发控制 | expectedVersion(order_batch_audit.version) | expectedVersion(order_group_settlement_main.version),同 CAS 语义、同 589573 |
| 出参 ID / 金额 | 数值型 | **全部字符串化**(Long 防精度丢失、DECIMAL 防精度) |
| 团号 | 出参无 teamNo | panel.households[] / sub-orders[] / tab splits[] 新增 **teamNo**(#8779) |
| sharedCostByType.costType | BatchCostType 4 值:`BUS` / `LEADER` / `PHOTOGRAPHER` / `OTHER` | **`settlement_category` 8 值**(HOTEL/TICKET/MEAL/VEHICLE/GUIDE/PHOTOGRAPHER/OTHER_INCOME/OTHER_EXPENSE),`costTypeDesc` 走数据字典 |
### 行为级对比
| 行为 | 原来 | 现在 |
|------|------|------|
| 打开核单页 | 返团后才可查,未返团 NOT_STARTED 空壳 | **任何团期状态首读即建行**,返回真实结构;无明细行给带出预览(不落库) |
| 保存 | 整单保存(一次 PUT 全科目) | **按 tab 保存**(8 个 PUT 各自全量替换本类)+ 单行新增/删除端点 |
| 分摊计算 | 提交核算时整团重算 | 每次暂存/新增/删除行即重算该行 splits 并落库;另有不落库试算 `alloc-preview` |
| 确认/提交核算 | allocate → reallocate → 验团多步 | **一次 panel/confirm**(8 表全量复核后锁 CONFIRMED,不可逆) |
| 开票 | `POST …/audit/invoice` | `POST …/settlement/invoice`(门禁从旧状态改为 CONFIRMED) |
| 导出 | `GET …/audit/export` | **端点下线**,本期无导出 |
| 旧 /audit 6 端点 | 正常服务 | **全部删除,调用 404** |
| 下游 D2 汇总(return-detail / settlement summary) | 共享成本读旧四表 | 改读新 8 表明细(PR-5),`sharedCostByType` 键值域同步切换 |
| 验团(结算)门禁 | 看旧核单主表状态 | 切看新核单主表 CONFIRMED(PR-5) |
## 11. 影响评估 / 回滚
- **破坏兼容**:**是,整体重做**。旧 `/audit/*` 端点已删(404),旧四表已 DROP,8 个 tab GET 出参结构完全变化。前端原团期核单页(科目行交互)**必须整体重写对接**,不存在渐进迁移路径。
- **前端必须同步上线**:是。后端部署后旧前端核单页全部 404 / 解析失败。**建议前后端同批上线**;后端当前已合 dev-v3 **未部署测试服**,前端可先行开发,联调窗口在部署后。
- **跨页面影响**:消费 `GroupReturnDetailRespVO` / `GroupBatchSettlementSummaryRespVO` 的 **`sharedCostByType[].costType`** 渲染页(核团详情、结算汇总)须同批适配——键值从 BatchCostType 4 值切到 settlement_category 8 值,展示名直接用后端下发的 `costTypeDesc`(字典驱动),不要再做 4 值硬编码映射。
- **workaround 清理点**:旧核单页对 589569/589570 的错误码映射可删(已废弃);对旧多态状态机(DRAFT/ALLOCATED/CHECKED 等)的分支渲染可删;「未返团空壳」占位逻辑可删(现在任何状态都返回真实结构)。
- **回滚方案**:后端回滚 = 需恢复旧四表 DDL + 旧代码(代价大,实际不可回滚——旧表已 DROP,数据不迁移)。本批按「开发期推倒重做、无向后兼容」交付,**前端务必在同批部署窗口前完成适配**。
## 12. 注意事项
1. **确认路径别调错**:确认核单是 `POST …/settlement/panel/confirm`;`POST …/settlement/confirm`(无 panel 段)是**团期结算财务复核**(旧财务主链,权限与语义都不同),两者不是一回事。
2. **activities = TICKET**:门票·游玩 tab 的路径段是历史沿用的 `activities`,但出参/入参的 category 值恒为 `TICKET`。路由映射写死:`hotels→HOTEL`、`activities→TICKET`、`meals→MEAL`、`vehicles→VEHICLE`、`guide-fees→GUIDE`、`photographer-fees→PHOTOGRAPHER`、`other-incomes→OTHER_INCOME`、`other-expenses→OTHER_EXPENSE`。
3. **特有列是并集**:LineVO 出参包含全部 8 类特有列的并集,非本 tab 类别的列恒为 null——按 `category` 只读本类列,不要对 null 列做渲染兜底之外的逻辑。
4. **全量替换**:PUT 暂存必须提交整 tab 全量行;漏传 = 删除。
5. **金额/ID 按字符串处理**:所有金额、ID 均为 JSON 字符串,禁转 number(精度丢失)。
6. **teamNo 可空**:未付订金的子订单 teamNo 为 null,列表渲染需做空值兜底;`alloc-preview` 回执的 splits.teamNo 本期恒 null(试算不查团号),不要用它做展示。
7. **预览行识别**:GET tab 首读的带出预览行 `lineId=null`,不是已保存数据,编辑/删除交互应只对 lineId 非空的行开放。
8. **editable 驱动写交互**:`editable=false`(CONFIRMED)时禁用全部写按钮;硬调写口会吃 589568。
9. **付款方式不走字典**:paymentMethod 是固定 3 值(SIGNED/COMPANY_PAID/CASH_PAID),前端硬编码映射即可;mealType / expenseType 走对应数据字典。
10. **指定报名校验在服务端**:Σ拆账=行总额(589750)、至少一户(589751)、户属本团(589572)都由服务端强校验,前端可做预校验提升体验,但不能替代服务端回执处理。
11. **删除行必带 category + expectedVersion**(Query 参数),不是请求体。
## 13. 关联 / 联系人
- Issue(主):https://git.1814.love/wx/HL/issues/8714
- Issue(teamNo 跟进):https://git.1814.love/wx/HL/issues/8779
- PR 序列(6 PR + 1 跟进,按合并序):
- PR-1 建表迁移:https://git.1814.love/wx/HL/pulls/8716 | commit https://git.1814.love/wx/HL/commit/3bad2ad276
- PR-2 DO/Mapper/枚举骨架:https://git.1814.love/wx/HL/pulls/8718 | commit https://git.1814.love/wx/HL/commit/ee90112a2d
- PR-3 公摊/指定报名拆账计算器:https://git.1814.love/wx/HL/pulls/8722 | commit https://git.1814.love/wx/HL/commit/34d10981c0
- PR-4 8 类 tab + 面板/暂存/确认/开票 Service 与接口:https://git.1814.love/wx/HL/pulls/8737 | commit https://git.1814.love/wx/HL/commit/788b9c1493
- PR-5 下游改造(D2 汇总读新 8 表 + 验团门禁切新主表):https://git.1814.love/wx/HL/pulls/8764 | commit https://git.1814.love/wx/HL/commit/b9f22e3adc
- PR-6 旧代码清理 + DROP 旧四表:https://git.1814.love/wx/HL/pulls/8778 | commit https://git.1814.love/wx/HL/commit/30f83ada81
- teamNo 补字段(#8779):https://git.1814.love/wx/HL/pulls/8780 | commit https://git.1814.love/wx/HL/commit/d759fba111
- 后端负责人:@yst
@@ -0,0 +1,531 @@
---
schema: "hl-changelog/v2"
ticket: "8741"
title: "供应商注册提交:证件图片地址不对直接返回 395065,状态与审批记录不再出现「建单失败」"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "新增一种失败返回,前端按现有方式展示 message;状态展示名少了「建单失败」,若前端有按该文案着色或筛选的逻辑请一并去掉"
updated_at: "2026-10-04"
base: "dev-v3"
---
# 供应商: 注册提交校验证件图片地址,去掉「建单失败」状态
> **服务**: hl-resource-service、hl-user-service
> **PR**: #8758
> **Issue**: #8741
> **日期**: 2026-10-04
> **影响范围**: 管理端供应商「提交审批」(注册审批)、供应商列表与详情的状态、供应商审批记录
---
## ⚠️ 关键变化
- 提交注册审批时,法人身份证人像面、国徽面、营业执照三张图片中任一张的地址不是本平台图片服务器地址(各环境不同,取文件中心配置),接口当场返回 `395065`「图片地址不对」。供应商保持草稿,不产生审批记录。以前会先受理,后台建企业微信审批单失败后才退回草稿,并显示「建单失败」。
- 供应商列表、详情的状态展示名不再出现「建单失败」,原来显示「建单失败」的供应商显示「草稿」。供应商审批记录、审批记录列表的层级与状态文案中的「建单失败」也改为「草稿」。
- 企业微信审批单内容(不影响管理端接口):「收款账户」显示银行名 + 完整账号;「明细 → 结算信息」每个账户显示账户名称、账户类型、开户行、支行、账号;「注册资本」为纯数字时显示为数值加「万」(如 500 → 500万)。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 提交供应商注册审批 | POST | `/admin/supplier/items/{supplierId}/submit` | 新增一种失败返回 | 证件图片地址不对时返回 395065 |
| 2 | 供应商分页 | GET | `/admin/supplier/items/page` | 返回值文案变化 | `statusName` 不再返回「建单失败」,改为「草稿」 |
| 3 | 供应商有界查询 | GET | `/admin/supplier/items/list` | 返回值文案变化 | 同上 |
| 4 | 供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 返回值文案变化 | 同上 |
| 5 | 供应商审批记录 | GET | `/admin/supplier/items/{supplierId}/approval-history/page` | 返回值文案变化 | `approvalStatusName`、`chainLevelText` 不再返回「建单失败」,改为「草稿」 |
| 6 | 审批记录列表 | GET | `/admin/supplier/items/approval-records/page` | 返回值文案变化 | `chainLevelText` 不再返回「建单失败」,改为「草稿」 |
---
## 三、接口详情
### 1. 提交供应商注册审批 `POST /admin/supplier/items/{supplierId}/submit`
**VO**: `SupplierSubmitReqVO` → `SupplierApprovalCommandRespVO`
#### 使用场景
管理员把草稿供应商提交注册审批。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| supplierId | Path | Long | ✅ | 正整数 | 草稿供应商 ID |
| expectedUpdateTime | Body | String | ✅ | `yyyy-MM-dd HH:mm:ss` | 乐观锁,取详情里的 updateTime |
| legalRepresentativeIdCardFrontUrl | Body | String | - | 非空时须为本平台图片地址 | 法人身份证人像面 |
| legalRepresentativeIdCardBackUrl | Body | String | - | 非空时须为本平台图片地址 | 法人身份证国徽面 |
| licenseImageUrl | Body | String | - | 非空时须为本平台图片地址 | 营业执照 |
| 其余注册资料字段 | Body | - | - | 同现有提交接口 | 本次未改 |
本平台图片地址即管理端文件上传返回的地址(TEST 形如 `https://hlgl-test.oss-cn-beijing.aliyuncs.com/test/...`,正式环境前缀为 `prod`)。
#### 出参 `Result<SupplierApprovalCommandRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| approvalLogId | String | 审批记录 ID |
| requestNo | String | 幂等请求号 |
| provider | String | 审批方式,企业微信为 `WECOM` |
| approvalStatus | String | 审批状态,提交后为 `PENDING` |
| approvalStatusName | String | 审批状态中文名 |
| spNo | String | 企业微信审批单号,异步建单完成前为空 |
| syncStatus | String | 同步状态 |
| submittedAt | String | 提交时间 |
本次出参不变。
#### 请求示例
```json
{
"expectedUpdateTime": "2026-10-03 11:31:05",
"fullName": "鄂温克族自治旗巴彦呼硕草原牧家乐有限公司",
"legalRepresentativeIdCardFrontUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/test/other/2026/10/03/9c21c0b068c00baa227cfd450e6706ae.png",
"legalRepresentativeIdCardBackUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/test/other/2026/10/03/a1d6e23d67830251b1fdd251c6d885ad.png",
"licenseImageUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/test/other/2026/10/03/1f287826963710fa564639f4c0de8aac.png"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"approvalLogId": "2106260000000000001",
"provider": "WECOM",
"approvalStatus": "PENDING",
"approvalStatusName": "审核中",
"syncStatus": "REQUESTING"
},
"success": true
}
```
#### 空数据 / 降级响应
本接口没有空数据。文件中心暂不可用、取不到本平台图片地址时不放行,返回 `100903`「远程服务暂不可用: 文件中心」,供应商保持草稿、不产生审批记录,稍后重试即可。
#### 错误响应
```json
{
"code": 395065,
"message": "图片地址不对",
"success": false
}
```
#### 业务边界
- 鉴权、权限、乐观锁、必填校验与现有提交接口相同。
- 三张证件图只校验非空的那几张;为空的维持现状(不上传该附件)。
- 任一张不是本平台图片地址:返回 395065,本次提交的资料不保存、不产生审批记录、供应商仍为草稿(零写入)。
- 已有在途注册审批时重复提交,仍按原规则返回在途结果。
- 修正图片后可直接重新提交。
### 2. 供应商分页 `GET /admin/supplier/items/page`
**VO**: `SupplierPageReqVO` → `PageResult<SupplierListItemRespVO>`
#### 使用场景
管理端供应商列表。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| keyword | Query | String | - | - | 编号/名称关键字 |
| status | Query | String | - | 生命周期编码 | 状态筛选,不变 |
| typeCode | Query | String | - | - | 供应商类型 |
| creditLevel | Query | String | - | A/B/C/D | 信用等级 |
| creatorId | Query | Long | - | - | 创建人 |
| page | Query | Integer | - | ≥1 | 页码 |
| pageSize | Query | Integer | - | ≥1 | 每页条数 |
本次入参不变。
#### 出参 `Result<PageResult<SupplierListItemRespVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| status | String | 生命周期编码,不变 |
| statusName | String | 展示名;明确建单失败的草稿供应商由「建单失败」改为「草稿」 |
其余字段不变。
#### 请求示例
```http
GET /admin/supplier/items/page?keyword=SUP260262&page=1&pageSize=20
```
#### 响应示例
```json
{ "code": 200, "data": { "list": [ { "supplierNo": "SUP260262", "status": "DRAFT", "statusName": "草稿" } ] }, "success": true }
```
#### 空数据 / 降级响应
无数据时返回空列表或原有空值,本次不变。
#### 错误响应
本次没有新增错误码,沿用现有错误返回,例如:
```json
{ "code": 395001, "message": "供应商不存在", "success": false }
```
#### 业务边界
- 鉴权与数据权限、分页与筛选不变;只有状态展示名从「建单失败」改为「草稿」,`status` 编码与按状态筛选不变。
### 3. 供应商有界查询 `GET /admin/supplier/items/list`
**VO**: `SupplierListReqVO` → `List<SupplierListItemRespVO>`
#### 使用场景
下拉或关联选择时有界查询供应商。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| keyword | Query | String | - | - | 编号/名称关键字 |
| status | Query | String | - | 生命周期编码 | 状态筛选,不变 |
| typeCode | Query | String | - | - | 供应商类型 |
| resourceModule | Query | String | - | 与 resourceId 同时传 | 资源模块 |
| resourceId | Query | Long | - | 与 resourceModule 同时传 | 资源 ID |
| limit | Query | Integer | - | 默认 50,最多 200 | 返回条数 |
本次入参不变。
#### 出参 `Result<List<SupplierListItemRespVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| status | String | 生命周期编码,不变 |
| statusName | String | 展示名;明确建单失败的草稿供应商由「建单失败」改为「草稿」 |
其余字段不变。
#### 请求示例
```http
GET /admin/supplier/items/list?keyword=SUP260262
```
#### 响应示例
```json
{ "code": 200, "data": [ { "supplierNo": "SUP260262", "status": "DRAFT", "statusName": "草稿" } ], "success": true }
```
#### 空数据 / 降级响应
无数据时返回空列表或原有空值,本次不变。
#### 错误响应
本次没有新增错误码,沿用现有错误返回,例如:
```json
{ "code": 395001, "message": "供应商不存在", "success": false }
```
#### 业务边界
- 鉴权与数据权限、分页与筛选不变;只有状态展示名从「建单失败」改为「草稿」,`status` 编码与按状态筛选不变。
### 4. 供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
**VO**: `—(Path 参数)` → `SupplierBasicInfoRespVO`
#### 使用场景
供应商详情与编辑页回显。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| supplierId | Path | Long | ✅ | 正整数 | 供应商 ID |
本次入参不变。
#### 出参 `Result<SupplierBasicInfoRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| status | String | 生命周期编码,不变 |
| statusName | String | 展示名;明确建单失败的草稿供应商由「建单失败」改为「草稿」 |
其余字段不变。
#### 请求示例
```http
GET /admin/supplier/items/2105983859848060929/basic-info/view
```
#### 响应示例
```json
{ "code": 200, "data": { "supplierNo": "SUP260262", "status": "DRAFT", "statusName": "草稿" }, "success": true }
```
#### 空数据 / 降级响应
无数据时返回空列表或原有空值,本次不变。
#### 错误响应
本次没有新增错误码,沿用现有错误返回,例如:
```json
{ "code": 395001, "message": "供应商不存在", "success": false }
```
#### 业务边界
- 鉴权与数据权限、分页与筛选不变;只有状态展示名从「建单失败」改为「草稿」,`status` 编码与按状态筛选不变。
### 5. 供应商审批记录 `GET /admin/supplier/items/{supplierId}/approval-history/page`
**VO**: `SupplierApprovalHistoryPageReqVO` → `PageResult<SupplierApprovalHistoryRespVO>`
#### 使用场景
供应商详情页的审批记录。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| supplierId | Path | Long | ✅ | 正整数 | 供应商 ID |
| bizType | Query | String | - | - | 审批类型 |
| approvalStatus | Query | String | - | - | 审批状态编码 |
| action | Query | String | - | - | 业务动作 |
| from / to | Query | String | - | `yyyy-MM-dd HH:mm:ss`,需同时传 | 提交时间范围 |
| sortBy / sortDirection | Query | String | - | 默认 submittedAt / DESC | 排序 |
| page | Query | Integer | - | ≥1 | 页码 |
| pageSize | Query | Integer | - | ≥1 | 每页条数 |
本次入参不变。
#### 出参 `Result<PageResult<SupplierApprovalHistoryRespVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| approvalStatus | String | 审批状态编码,不变(如 `FAILED`) |
| approvalStatusName | String | 注册明确未建单由「建单失败」改为「草稿」 |
| chainLevelText | String | 注册明确未建单由「建单失败」改为「草稿」 |
其余字段不变。
#### 请求示例
```http
GET /admin/supplier/items/2105983859848060929/approval-history/page?page=1&pageSize=50
```
#### 响应示例
```json
{ "code": 200, "data": { "list": [ { "approvalStatus": "FAILED", "approvalStatusName": "草稿", "chainLevelText": "草稿", "actionName": "提交草稿" } ] }, "success": true }
```
#### 空数据 / 降级响应
无数据时返回空列表或原有空值,本次不变。
#### 错误响应
本次没有新增错误码,沿用现有错误返回,例如:
```json
{ "code": 395001, "message": "供应商不存在", "success": false }
```
#### 业务边界
- 鉴权、分页、筛选与行数不变;只有注册明确未建单那一行的展示文案从「建单失败」改为「草稿」,`approvalStatus` 编码不变。
### 6. 审批记录列表 `GET /admin/supplier/items/approval-records/page`
**VO**: `SupplierApprovalLogPageReqVO` → `PageResult<SupplierApprovalLogPageItemRespVO>`
#### 使用场景
跨供应商的审批记录列表。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| supplierNo | Query | String | - | - | 供应商编号 |
| fullName | Query | String | - | - | 供应商名称 |
| bizType | Query | String | - | - | 审批类型 |
| approvalStatus | Query | String | - | - | 审批状态编码 |
| from / to | Query | String | - | `yyyy-MM-dd HH:mm:ss` | 提交时间范围 |
| page | Query | Integer | - | ≥1 | 页码 |
| pageSize | Query | Integer | - | ≥1 | 每页条数 |
本次入参不变。
#### 出参 `Result<PageResult<SupplierApprovalLogPageItemRespVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| approvalStatus | String | 审批状态编码,不变 |
| chainLevelText | String | 注册明确未建单由「建单失败」改为「草稿」 |
其余字段不变。
#### 请求示例
```http
GET /admin/supplier/items/approval-records/page?supplierNo=SUP260262&page=1&pageSize=50
```
#### 响应示例
```json
{ "code": 200, "data": { "list": [ { "supplierNo": "SUP260262", "approvalStatus": "FAILED", "chainLevelText": "草稿" } ] }, "success": true }
```
#### 空数据 / 降级响应
无数据时返回空列表或原有空值,本次不变。
#### 错误响应
本次没有新增错误码,沿用现有错误返回,例如:
```json
{ "code": 395001, "message": "供应商不存在", "success": false }
```
#### 业务边界
- 鉴权、分页、筛选与行数不变;只有注册明确未建单那一行的展示文案从「建单失败」改为「草稿」,`approvalStatus` 编码不变。
---
## 四、契约约束与正确调用方式(接口类必写)
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload |
|------|---------|
| ✅ 三张图都是文件上传返回的地址 | `{ "licenseImageUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/test/other/2026/10/03/a.png", ... }` |
| ✅ 某张图为空 | `{ "legalRepresentativeIdCardBackUrl": null, ... }` |
| ❌ 外站地址 | `{ "licenseImageUrl": "https://img.hlbe-travel.com/supplier/2026/1002/x.jpg" }` → 395065 |
| ❌ 其他环境的地址(如 TEST 提交正式环境 `prod/` 前缀地址) | `{ "licenseImageUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/prod/x.png" }` → 395065 |
### 切换状态时的必要动作
收到 395065 时,让用户重新上传三张证件图(用管理端文件上传返回的地址),再带最新 `expectedUpdateTime` 重新提交。
---
## 五、数据库行为(涉及写操作时必写)
| 提交结果 | 供应商资料 | 供应商状态 | 审批记录 |
|----------|-----------|-----------|---------|
| 图片地址都对 | 按提交内容保存 | 进入注册审核中 | 新增 1 条 |
| 任一张图片地址不对(395065) | 不保存(本次提交整体回滚) | 保持草稿 | 不新增 |
| 文件中心不可用(100903) | 不保存 | 保持草稿 | 不新增 |
---
## 六、边界行为
- 未登录 → 401(网关拦截);无权限 → 沿用现有错误。
- 图片地址为空 → 不校验该张,照原规则提交。
- 文件中心不可用 → 100903,不放行。
- 历史上显示「建单失败」的供应商 → 现在直接显示「草稿」,可修正图片后重新提交。
---
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `statusName`(列表、详情) | 明确建单失败时为「建单失败」 | 「草稿」 |
| `approvalStatusName`、`chainLevelText`(审批记录) | 「建单失败」 | 「草稿」 |
| `chainLevelText`(审批记录列表) | 「建单失败」 | 「草稿」 |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 证件图在外站时提交注册审批 | 接口先受理,后台建企业微信审批单失败,自动重提后退回草稿并显示「建单失败」 | 接口当场返回 395065「图片地址不对」,零写入 |
| 企业微信审批单「收款账户」 | 银行名 尾号xxxx | 银行名 完整账号 |
| 企业微信审批单「明细 → 结算信息」 | 银行名 尾号xxxx | 每个账户:账户名称、账户类型、开户行、支行、账号 |
| 企业微信审批单「注册资本」 | 原样(如 500) | 纯数字补「万」(如 500万),已带单位的原样 |
## 六.7、影响评估(修改/删除类必写)
- **是否破坏向后兼容**: 否(新增一种失败返回;展示文案少了「建单失败」一种取值)
- **前端是否必须同步上线**: 否
- **前端 workaround 清理点**: 如有按「建单失败」文案着色、筛选或提示的逻辑,可以去掉
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
- **仅影响**: 供应商注册「提交审批」接口;供应商状态展示名与审批记录文案中的「建单失败」
- **零影响**:
- 供应商生命周期状态编码、状态筛选、状态机
- 合作中资料变更审批、状态变更审批、收款账户审批
- 主档注册资本存值与管理端展示(「万」只加在企业微信审批单上)
---
## 八、测试环境已验证
TEST 已部署 dev-v3 `df99bfedf`(包含合并提交 `a73f015e7`),经 Gateway 实测:
```
SUP260262、SUP260260(证件图在外站)重新提交 → 395065「图片地址不对」,仍为草稿,审批记录仍各 3 条 ✓
自造草稿 SUP260266 只把营业执照换成外站地址提交 → 395065,仍为草稿、营业执照未被改写、无审批记录 ✓
两家列表与详情状态展示「草稿」,审批记录与审批记录列表无「建单失败」;供应商列表整表 256 家无「建单失败」 ✓
SUP260266 三张图为本平台地址时提交 → 受理并生成企业微信审批单 202610040008 ✓
```
---
## 十、相关文档
- 关联 Issue: [wx/HL#8741](https://git.1814.love/wx/HL/issues/8741)
- 关联 PR: [wx/HL#8758](https://git.1814.love/wx/HL/pulls/8758)
## 关联 / 联系人
### 链接
- **Issue**: [#8741](https://git.1814.love/wx/HL/issues/8741)
- **PR**: [#8758](https://git.1814.love/wx/HL/pulls/8758)
- **Merge commit**: [a73f015e7](https://git.1814.love/wx/HL/commit/a73f015e7)
### 联系人
- **后端负责人**: @lc
@@ -0,0 +1,391 @@
---
schema: "hl-changelog/v2"
ticket: "8751"
title: "往来台账升级为按往来对象净额视图——入参 ledgerType 改名 partyType(反转 #8719)"
consumer: "admin"
author: "yst"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "7ed2db889cc4a7b7bdb33bf6d6ed56300ed9a899"
target_release: "v2.1"
verified_at: "2026-10-04"
status_note: "反转 #8719 单账套契约:#8719 的 ledgerType 三账套入参(SUPPLIER/SUPPLIER_RECV/CUSTOMER)作废,前端 mmg 已按 #8719 交付的三账套页签需返工,改按本契约 partyType 两对象净额视图重对接。核心:供应商应收+应付是同一本供应商往来的两个方向,合并轧差算净额(应付−应收,正=我欠他/负=他欠我);入参 ledgerType→partyType 破坏性改名;明细新增期初构成行(sourceType=OPENING),纯期初供应商点详情不再空白。前端已交付(2026-10-04):statement.js 契约层重写 PARTY_TYPE 两值字典+净额 meta 对象两分支(负值一律标红)+sourceType 加 OPENING;新建 _shared/StatementLedger.vue 共享页体,supplier 页重写为薄壳(三账套页签下掉固定 SUPPLIER,storage-key 留旧键,行内核销保留),新建 customer 薄壳页(菜单本期新增);writeoff/opening 的 ledgerType 是各自 API 自身契约零适配,提交 7ed2db889。"
updated_at: "2026-10-04"
base: "dev-v3"
---
# finance:往来台账净额视图——ledgerType 改名 partyType(管理后台)
> ⚠️ **修改接口(破坏性,反转 #8719)**:两个接口入参 `ledgerType` **改名** `partyType`,取值域从三账套(SUPPLIER/SUPPLIER_RECV/CUSTOMER)收敛为**往来对象两值**(SUPPLIER/CUSTOMER);供应商应付+应收两账套按 refId 轧差合并为一个供应商净额。按 #8719 交付的三账套页签**必须返工**。
## 1. 接口背景
#8719 把往来台账从单账套 SUPPLIER 扩为 SUPPLIER/SUPPLIER_RECV/CUSTOMER 三账套分别查询。上线后发现业务口径不对:**供应商的应收和应付是同一本供应商往来的两个方向**,分开看两本账无法回答「我到底欠不欠这个供应商钱」。
#8751 用户拍板方案 A:台账从「按账套单查」升级为「**按往来对象轧差算净额**」——供应商往来 = 应付账套(SUPPLIER)+ 应收账套(SUPPLIER_RECV)按同一供应商 refId 轧差,**应付 − 应收 = 净额**。客户往来保持应收口径不变。
同时解决 #8719 遗留问题:纯期初(只有期初挂账、无业务流水)的往来对象点详情明细是空白——本期明细接口把**期初构成行**(期初录入 / 期初调整)也 UNION 进来,期初怎么来的看得见。
## 2. 变更清单
| # | 接口 | 变更点 | 类型 |
|---|------|--------|------|
| 1 | `GET /admin/finance/statements/page` | 入参 `ledgerType` **改名** `partyType`,取值域三账套 → 两对象 | ⚠️ 破坏性改名 |
| 2 | `GET /admin/finance/statements/entries/page` | 入参 `ledgerType` **改名** `partyType`,同上 | ⚠️ 破坏性改名 |
| 3 | 两接口 | 出参字段 `ledgerType` → `partyType`,值只会是 `SUPPLIER` / `CUSTOMER` | ⚠️ 字段改名 |
| 4 | statements/page | 供应商行金额 = 应付 + 应收两账套按 refId **轧差合并**(应付−应收) | 🔧 口径变化 |
| 5 | entries/page | 出参新增**期初构成行**:`sourceType=OPENING`、`direction=INCREASE`、`summary=期初录入/期初调整` | ✨ 新增行类型 |
| 6 | entries/page | 入参 `sourceType` 筛选新增合法值 `OPENING` | ✨ 枚举扩域 |
| 7 | 两接口 | `SUPPLIER_RECV` 不再作为独立查询值,传了报 596005 | ⚠️ 取值收敛 |
| 8 | 菜单 | /finance/current-account 下新增「司导往来 /guide」「客户往来 /customer」菜单 | ✨ 菜单新增 |
## 3. 接口详情
| 项 | statements/page | entries/page |
|---|---|---|
| 方法 + 路径 | `GET /admin/finance/statements/page` | `GET /admin/finance/statements/entries/page` |
| 接口名 | 往来账账页分页(按往来对象净额视图) | 往来流水明细分页(业务流水+期初构成 UNION) |
| 使用场景 | 管理后台 → 财务 → 往来账 → 供应商往来 / 客户往来列表页 | 台账页点某往来对象后的明细流水(含期初构成) |
| 认证 | 管理后台登录态(JWT) | 同左 |
| 幂等性 | 只读查询,天然幂等 | 只读查询,天然幂等 |
| 限流 | 无特殊限流 | 无特殊限流 |
## 4. 接口入参
### 4.1 `GET /admin/finance/statements/page`(Query 参数)
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| partyType | string | **必填** | 往来对象类型:`SUPPLIER` 供应商往来(应付−应收轧差净额)/ `CUSTOMER` 客户往来(应收);传 `STAFF` / `SUPPLIER_RECV` / 其他非法值报 596005 |
| refName | string | 可空 | 往来对象名,模糊匹配;空=不限 |
| negativeOnly | boolean | 可空 | true=只返回净额为负的行(供应商 负=他欠我 / 客户 负=我多收,前端标红场景) |
| page | int | 必填 | 页码,从 1 开始 |
| pageSize | int | 必填 | 每页条数 |
### 4.2 `GET /admin/finance/statements/entries/page`(Query 参数)
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| partyType | string | **必填** | 往来对象类型,取值同 4.1;`STAFF`/`SUPPLIER_RECV`/非法值报 596005 |
| refId | string | 可空 | 往来对象 ID(供应商/客户主键,Long 字符串);空=不限 |
| direction | string | 可空 | 增减方向:`INCREASE` 净额增 / `DECREASE` 净额减;空=不限(非法值不报错,查不出行返回空页) |
| sourceType | string | 可空 | 来源类型:`PAYMENT` 应付款 / `PREPAY` 预付款 / `OPENING` 期初构成行(**本期新增**);空=不限 |
| entryDateStart | string | 可空 | 入账日期起,格式 `yyyy-MM-dd`;晚于 entryDateEnd 报 596009 |
| entryDateEnd | string | 可空 | 入账日期止,格式 `yyyy-MM-dd` |
| teamNo | string | 可空 | 团号,精确过滤(**期初构成行无团号**,填团号后结果不含期初行) |
| page | int | 必填 | 页码,从 1 开始 |
| pageSize | int | 必填 | 每页条数 |
## 5. 出参字段
统一分页包装 `PageResult`(`list/records` + `total` 等,以前端现行解析字段为准)。
### 5.1 statements/page 出参 records[]
| 字段 | 类型 | 说明 |
|------|------|------|
| partyType | string | 往来对象类型,只会是 `SUPPLIER` / `CUSTOMER` |
| refId | string | 往来对象 ID(Long 序列化字符串,防精度丢失) |
| refName | string | 往来对象名(流水名优先,改名取新) |
| openingAmount | number | 期初净额(供应商=应付期初−应收期初;客户=应收期初−应付期初) |
| increaseTotal | number | 本期净额增合计(供应商=应付增+应收减;客户=应收增) |
| decreaseTotal | number | 本期净额减合计(供应商=应付减+应收增;客户=应收减) |
| writeoffTotal | number | 本期核销合计(**本期恒 0**,核销开放后有值) |
| adjustTotal | number | 本期调账合计(**本期恒 0**,调账二期开放后有值) |
| netAmount | number | 净往来余额 = openingAmount + increaseTotal − decreaseTotal − writeoffTotal + adjustTotal |
**金额符号语义对照表(方向做反 = 金额全错,前端必须按此实现)**:
| partyType | netAmount 正数含义 | netAmount 负数含义 | 前端展示 |
|-----------|---------------------|---------------------|----------|
| `SUPPLIER` 供应商往来 | **我欠他**(该付未付) | **他欠我**(多付/预付他) | 负值标红 |
| `CUSTOMER` 客户往来 | **他欠我**(该收未收) | 我多收他 | 负值标红 |
- 供应商口径与 #8719 的 SUPPLIER(应付)单账套一致:正=我欠他;区别在于本期净额已**扣掉应收侧**(预付款/多付款)。
- 期初净额 openingAmount 同样按对象口径轧差:供应商 = 应付期初 − 应收期初。
### 5.2 entries/page 出参 records[]
明细 = 业务流水行 + 期初构成行 **UNION 合并**,按 `entryDate` 倒序。
| 字段 | 类型 | 说明 |
|------|------|------|
| id | string | 明细行 ID(业务流水=entry_id,期初构成行=opening_id) |
| partyType | string | 往来对象类型:`SUPPLIER` / `CUSTOMER` |
| refId | string | 往来对象 ID |
| refName | string | 往来对象名快照 |
| entryDate | string | 入账日期(业务发生日;期初构成行=期初基准日) |
| sourceType | string | `PAYMENT` 应付款 / `PREPAY` 预付款 / `OPENING` 期初构成行 |
| sourceId | string | 来源单据 ID(期初构成行=opening_id) |
| sourceNo | string | 来源单号(期初构成行无独立单号,回退 opening_id 字符串) |
| direction | string | `INCREASE` 净额增 / `DECREASE` 净额减;**期初构成行恒 INCREASE**,前端映射「净额+」/「净额−」徽标 |
| amount | number | 金额(恒正,方向由 direction 表达;期初构成行=该行账套方向金额:应付账行取应付金额/应收账行取应收金额) |
| teamNo | string | 团号(可空;预付款与期初构成行无团号为 null) |
| summary | string | 摘要(白话业务词,如「应付款付讫」;期初构成行=`期初录入`/`期初调整`) |
**供应商净额视图下的明细构成**:`partyType=SUPPLIER` 时,SUPPLIER(应付)和 SUPPLIER_RECV(应收)**两账套的业务流水与期初行都会进明细**——应收侧行(如预付款收回、应收期初)以轧差方向并入,方向已折算成净额增减,前端按 direction 渲染即可,无需自己区分账套。
**纯期初对象不再空白**:只有期初挂账、无业务流水的往来对象,明细页现在能看到 `OPENING` 行(期初录入/期初调整各一条),解决 #8719 期间「点详情一片空白」的问题。
## 6. 枚举 / 数据字典
### partyType(往来对象类型,入参 + 出参)
| 值 | 含义 | 底层账套合并规则 |
|----|------|------------------|
| `SUPPLIER` | 供应商往来净额(正=我欠他 / 负=他欠我) | 应付 SUPPLIER(权重 +1)+ 应收 SUPPLIER_RECV(权重 −1)按 refId 轧差 |
| `CUSTOMER` | 客户往来(应收,正=他欠我) | 应收 CUSTOMER 单账套 |
**非法值(传了报 596005)**:`STAFF`(员工往来本期不开放,司导走独立 GuideLedger 接口 `/guide-ledger/*`)、`SUPPLIER_RECV`(已并入 SUPPLIER 净额,不再独立查询)、其他任意值。
### sourceType(明细来源类型)
| 值 | 含义 | 本期状态 |
|----|------|----------|
| `PAYMENT` | 应付款 | 原有 |
| `PREPAY` | 预付款 | 原有 |
| `OPENING` | 期初构成行(期初录入/期初调整) | **本期新增** |
### direction(增减方向)
| 值 | 含义 |
|----|------|
| `INCREASE` | 净额增(期初构成行恒为此值) |
| `DECREASE` | 净额减 |
## 7. 错误码
| 错误码 | 文案 | 触发场景 |
|--------|------|----------|
| 596005 | 账套或往来对象类型非法/本期未开放 | `partyType` 传 `STAFF` / `SUPPLIER_RECV` / 空 / 其他非法值 |
| 596009 | 入账日期区间倒置 | entries/page 的 `entryDateStart` 晚于 `entryDateEnd` |
## 8. 示例
### 8.1 典型:供应商往来净额列表
请求:
```
GET /admin/finance/statements/page?partyType=SUPPLIER&page=1&pageSize=20
```
响应 200(测试服真实数据):
```json
{
"code": 0,
"data": {
"list": [
{
"partyType": "SUPPLIER",
"refId": "2105165036928540674",
"refName": "天边草原勇士车穿越",
"openingAmount": 1322.00,
"increaseTotal": 0,
"decreaseTotal": 0,
"writeoffTotal": 0,
"adjustTotal": 0,
"netAmount": 1322.00
},
{
"partyType": "SUPPLIER",
"refId": "2105165036928000001",
"refName": "寻龙诀航拍",
"openingAmount": 3333.00,
"increaseTotal": 0,
"decreaseTotal": 0,
"writeoffTotal": 0,
"adjustTotal": 0,
"netAmount": 3333.00
},
{
"partyType": "SUPPLIER",
"refId": "2105165036928000002",
"refName": "天边草原下午茶",
"openingAmount": -10000.00,
"increaseTotal": 0,
"decreaseTotal": 0,
"writeoffTotal": 0,
"adjustTotal": 0,
"netAmount": -10000.00
}
],
"total": 3
}
}
```
读法:
- 勇士车穿越 net=+1322:应付侧期初 2522 − 应收侧期初 1200 = **我欠他 1322**(该付未付)。
- 寻龙诀航拍 net=+3333:纯期初供应商,欠他 3333。
- 天边草原下午茶 net=**−10000**:**他欠我 10000**(多付/预付),前端标红。
### 8.2 边界:纯期初供应商明细——期初构成行可见
请求(勇士车穿越,refId=2105165036928540674):
```
GET /admin/finance/statements/entries/page?partyType=SUPPLIER&refId=2105165036928540674&page=1&pageSize=20
```
响应 200(4 条 OPENING 期初构成行:应付侧期初录入 2222 + 期初调整 300;应收侧期初录入 1000 + 期初调整 200):
```json
{
"code": 0,
"data": {
"list": [
{
"id": "2105165037000000004",
"partyType": "SUPPLIER",
"refId": "2105165036928540674",
"refName": "天边草原勇士车穿越",
"entryDate": "2026-09-01",
"sourceType": "OPENING",
"sourceId": "2105165037000000004",
"sourceNo": "2105165037000000004",
"direction": "INCREASE",
"amount": 300.00,
"teamNo": null,
"summary": "期初调整"
},
{
"id": "2105165037000000003",
"partyType": "SUPPLIER",
"refId": "2105165036928540674",
"refName": "天边草原勇士车穿越",
"entryDate": "2026-09-01",
"sourceType": "OPENING",
"sourceId": "2105165037000000003",
"sourceNo": "2105165037000000003",
"direction": "INCREASE",
"amount": 2222.00,
"teamNo": null,
"summary": "期初录入"
},
{
"id": "2105165037000000002",
"partyType": "SUPPLIER",
"refId": "2105165036928540674",
"refName": "天边草原勇士车穿越",
"entryDate": "2026-09-01",
"sourceType": "OPENING",
"sourceId": "2105165037000000002",
"sourceNo": "2105165037000000002",
"direction": "INCREASE",
"amount": 200.00,
"teamNo": null,
"summary": "期初调整"
},
{
"id": "2105165037000000001",
"partyType": "SUPPLIER",
"refId": "2105165036928540674",
"refName": "天边草原勇士车穿越",
"entryDate": "2026-09-01",
"sourceType": "OPENING",
"sourceId": "2105165037000000001",
"sourceNo": "2105165037000000001",
"direction": "INCREASE",
"amount": 1000.00,
"teamNo": null,
"summary": "期初录入"
}
],
"total": 4
}
}
```
读法:应付侧期初 = 2222(录入)+ 300(调整)= 2522;应收侧期初 = 1000 + 200 = 1200;轧差 2522 − 1200 = 1322,与列表页 netAmount 一致。期初行 `amount` 是**该行所属账套方向的金额**(应付账行取应付金额、应收账行取应收金额),direction 已折算成对净额的方向,前端直接按 direction 渲染「净额+」徽标即可,不用自己再轧一次。
### 8.3 业务失败:传旧账套值 SUPPLIER_RECV / 未开放 STAFF
请求:
```
GET /admin/finance/statements/page?partyType=SUPPLIER_RECV&page=1&pageSize=20
```
响应:
```json
{
"code": 596005,
"message": "账套或往来对象类型非法/本期未开放"
}
```
`partyType=STAFF` 同样报 596005。旧入参名 `ledgerType` 已删,只传 `ledgerType=SUPPLIER` 不带 `partyType` 会因缺少必填参数报参数校验错误(partyType 不能为空)。
## 9. 业务边界
**适用**:
- 管理后台财务查看供应商往来(应付+应收轧差净额)与客户往来(应收)的账页余额与明细流水。
- 查看往来对象期初挂账的构成(期初录入/期初调整各多少)。
**不适用**:
- 员工/司导往来查询——`STAFF` 不开放(报 596005);司导往来走**独立接口** `GET /admin/finance/guide-ledger/*`(本期新增菜单「司导往来」对应页面调它,**不是**本台账接口)。
- 按账套(ledgerType 维度)拆账——本接口只提供对象净额视图,无账套级查询入口。
**特殊边界**:
- `writeoffTotal` / `adjustTotal` 本期恒 0(核销/调账能力后续开放),前端列照渲染 0 即可。
- 明细 `direction`/`sourceType` 筛非法值**不报错**,返回空页(期初行恒 INCREASE/OPENING 且无团号:筛 `direction=DECREASE`、`sourceType≠OPENING` 或填 `teamNo` 时结果不含期初行)。
- `refId` / `id` / `sourceId` 均为 Long 序列化字符串,前端按字符串处理,禁转 number(精度丢失)。
- 期初构成行无独立单号,`sourceNo` 回退为 opening_id 字符串,不要用 sourceNo 跳期初单据详情。
## 10. 修改前后对比
### 字段级对比
| 项 | 原来(#8719 契约) | 现在(#8751) |
|----|---------------------|----------------|
| 入参名 | `ledgerType` | **`partyType`**(ledgerType 作废,传了不生效) |
| 入参取值域 | `SUPPLIER` / `SUPPLIER_RECV` / `CUSTOMER` 三账套 | `SUPPLIER` / `CUSTOMER` **两对象**;`SUPPLIER_RECV` 改为报 596005 |
| 出参类型字段 | `ledgerType`(可能三值) | `partyType`(只会 SUPPLIER/CUSTOMER) |
| 供应商行金额口径 | 应付、应收**分两本账**各查各的 | 应付 − 应收**轧差合并**为一个净额行 |
| 明细行构成 | 仅业务流水(纯期初对象明细空白) | 业务流水 + 期初构成行(sourceType=OPENING)UNION |
| sourceType 取值 | `PAYMENT` / `PREPAY` | 新增 `OPENING` |
| 符号语义 | 按账套分化三套口径 | 按对象两套口径:供应商 正=我欠他;客户 正=他欠我 |
### 行为级对比
| 行为 | 原来 | 现在 |
|------|------|------|
| 传 `SUPPLIER_RECV` 查供应商应收 | 正常返回应收账套 | **报 596005**(已并入 SUPPLIER 净额) |
| 查供应商「总欠多少」 | 前端拿应付、应收两行自己算差 | 后端直接返回轧差后净额一行 |
| 纯期初供应商点详情 | 明细空白 | 看到 OPENING 期初录入/调整构成行 |
| 传 `STAFF` | 报 596005 | 仍报 596005(不变) |
| CUSTOMER 客户往来 | 正常返回(#8719 新增) | 行为不变,口径不变 |
## 11. 影响评估 / 回滚
- **破坏兼容**:**是**。入参改名 + 取值收敛,按 #8719 交付的前端代码不改必坏:
1. 入参 `ledgerType=xxx` → `partyType=xxx`;
2. 三账套页签(供应商应付/供应商应收/客户应收)→ **两对象页签**(供应商往来/客户往来),供应商应收页签下掉;
3. 正负含义判断从「按 ledgerType 三分支」改为「按 partyType 两分支」(供应商 正=我欠他 / 客户 正=他欠我,负值标红);
4. 明细列表渲染逻辑需兼容 `sourceType=OPENING` 行(无团号、无业务单号,summary=期初录入/期初调整)。
- **菜单配合**:/finance/current-account 下现有三个对象菜单——供应商往来 /finance/current-account/supplier(既有,页面改净额视图)、司导往来 /finance/current-account/guide(本期新增,调独立 guide-ledger 接口,不在本次契约内)、客户往来 /finance/current-account/customer(本期新增)。
- **回滚方案**:后端回滚 = 恢复 #8719 的 ledgerType 三账套契约;无 DDL、无数据迁移(净额为查询时现算,不存列),回滚无副作用。前端若已上净额视图,回滚后需恢复三账套页签——**建议前后端同批上线/同批回滚,不要错开**。
## 12. 注意事项
1. **符号判断按 partyType 两分支**:供应商 正=我欠他(该付未付)/ 负=他欠我;客户 正=他欠我(该收未收)/ 负=我多收。负值一律标红。不要残留 #8719 的三账套判断逻辑。
2. **供应商应收页签下掉**:`SUPPLIER_RECV` 不再是合法查询值,页面上供应商应收入口必须删(传了吃 596005)。供应商的应收金额已通过轧差体现在 SUPPLIER 净额里。
3. **明细兼容 OPENING 行**:期初构成行 `teamNo=null`、`sourceNo`=opening_id、`direction` 恒 INCREASE,渲染时按 sourceType 字典显示「期初」徽标即可;筛团号/筛 DECREASE 时不出期初行属预期。
4. **纯期初对象明细有数据了**:不要再对「明细空」做空态兜底以外的特殊处理——空明细只出现在该对象真的既无流水又无期初时(正常不会进列表)。
5. **司导往来别调本接口**:司导走 `/admin/finance/guide-ledger/*` 独立接口(另有契约),本台账 partyType 没有也不接受 STAFF。
6. **ID 按字符串处理**:`refId`/`id`/`sourceId` 是 Long 序列化字符串,转 number 会精度丢失。
7. 页签展示名建议:SUPPLIER=供应商往来 / CUSTOMER=客户往来。
## 13. 关联 / 联系人
- Issue:https://git.1814.love/wx/HL/issues/8751
- PR:https://git.1814.love/wx/HL/pulls/8772
- Commit:https://git.1814.love/wx/HL/commit/67cf0ee7be
- 被反转契约:#8719(`changelogs-v2/2026-10/02_8719_应收侧往来台账放开-修改接口-管理后台.md`,frontend_status 已标 superseded)
- 后端负责人:@yst
@@ -0,0 +1,290 @@
---
schema: "hl-changelog/v2"
ticket: "8754"
title: "补发成团通知回执改为如实计数:recipientCount 只算实际可达户,新增 smsCount / inappCount / skippedCount / smsReady;成团通知加客户短信(按下单手机号直发)"
consumer: "admin"
author: "jw(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "c5f4af3639f11a60c698145b15ee6c4b1163e792"
target_release: "v2.1"
verified_at: "2026-10-04"
status_note: "已合并 dev-v3(PR #8775,merge aac6a6a13)并部署 TEST(user-service aac6a6a13 + 迁移 20261003.8754;order-v3 9b72d23a3 含本单),自签 admin token 经网关按 #8754 AC-1~10 实测通过。补发回执 recipientCount 改为实际可达户数,新增 smsCount / inappCount / skippedCount / smsReady;成团(手动 / 自动)与补发按订单下单手机号发短信,模板号仍是哨兵、只记「模板未配置」不真发,运营过审后在通知配置后台填号即生效。前端待做:补发成功提示改用新字段(smsReady=false 时提示短信模板未配置),不要再用 recipientCount 当已通知户数。前端已交付(2026-10-04):补发成功提示改走 notifyFormedResultText(「已通知 N 户(短信 X、站内信 Y),Z 户未能通知」,smsReady=false 追加「短信模板未配置,本次仅发站内信」),提交 c5f4af363。"
updated_at: "2026-10-04"
base: "dev-v3"
---
# order-v3: 补发成团通知回执如实计数 + 成团通知加客户短信
**服务**: hl-order-service-v3(回执计数)、hl-user-service(通知配置)
**PR**: `#8775`(已合入 `dev-v3`,合并提交 `aac6a6a13`)
**Issue**: #8754
---
## ⚠️ 关键变化
🔴 **`recipientCount` 含义变了**:改前 = 团期活跃子订单户数(收不到的户也算进去);改后 = **按当前通知配置实际可达的户数**。短信模板未配真实号期间,没有小程序账号的户收不到任何通知,`recipientCount` 会明显小于户数,这是如实反映,不是故障。
🟢 **回执新增 5 个字段**:`smsCount`、`inappCount`、`skippedCount`、`smsReady`(短信通道是否就绪)。入参、判权、冷却 300 秒、已有错误码均不变;589594 文案补了一种情形。
🟢 **成团通知加客户短信**:手动成团、自动成团与补发,每户按订单的下单手机号发短信(不依赖小程序账号),有小程序账号的户站内信照旧。短信模板号未配之前(当前状态)短信只记「模板未配置」不真发,运营拿到阿里云模板号后在通知配置后台填入即生效。
---
## 一、背景
成团通知原只开站内信、按小程序账号投递,而团期子订单绝大多数是后台代下单、没有小程序账号(09-27 TEST 实测 686 户里 683 户没有),客户实际收不到;补发按钮的回执还把这些户算进 `recipientCount`,运营会以为都通知到了。jw 2026-10-03 定:成团照常无条件通知(不加「不通知」开关);客户侧改发短信,收件人用订单下单手机号;先开发、模板后补。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 补发成团通知 | POST | `/v3/admin/order/group-batch/{groupBatchId}/notify-formed` | 修改 | `recipientCount` 改为实际可达户数;新增 `smsCount` / `inappCount` / `skippedCount` / `smsReady`;589594 文案补「或各户既无合规手机号也无小程序账号」 |
成团 `POST /v3/admin/order/group-batch/{groupBatchId}/group` 的入参、出参、错误码一字未变,只是成团后发出的通知多了短信(见「六、边界行为」)。
---
## 三、接口详情
### 1. 补发成团通知 `POST /v3/admin/order/group-batch/{groupBatchId}/notify-formed`
**VO**: `Result<GroupBatchNotifyRespVO>`
#### 使用场景
团期详情「更多操作 → 补发成团通知」。对已成团团期,向全部在团户再发一次成团通知(短信 + 有小程序账号的户另发站内信),回执告诉运营这次实际能通知到几户、短信几户、站内信几户、跳过几户,以及短信通道是否就绪。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | 团期 ID | **不变** |
无请求体、无查询参数(不变)。
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| groupBatchId | String | 团期 ID(Long 序列化为字符串),**不变** |
| eventCode | String | 固定 `GROUP_BATCH_FORMED`,**不变** |
| recipientCount | Integer | 🔄 **改口径**:实际可达户数 = 短信可达户 ∪ 站内信可达户(一户两条通道都可达只算一户,不是 `smsCount + inappCount`) |
| smsCount | Integer | 🆕 短信可达户数:订单下单手机号为 11 位大陆手机号,且 `smsReady=true`。`smsReady=false` 时恒 0。按户计、不按号码去重 |
| inappCount | Integer | 🆕 站内信可达户数:订单有小程序账号,且站内信通道开启 |
| skippedCount | Integer | 🆕 跳过户数 = 在团户数 − `recipientCount`(无联系方式、通道未就绪或投递失败的户) |
| smsReady | Boolean | 🆕 短信通道是否就绪:短信开关开且已配真实模板号。`false` 表示模板号还没配(或短信被关),本次短信不会真发 |
| enqueued | Boolean | **不变**,恒 `true`(失败时接口直接返回错误码) |
| notifiedAt | String | **不变**,本次补发时间 |
| cooldownSeconds | Integer | **不变**,固定 300 |
#### 请求示例
```http
POST /v3/admin/order/group-batch/{groupBatchId}/notify-formed HTTP/1.1
Authorization: Bearer <管理端 token>
```
#### 响应示例
TEST 实际返回(团 T26-1771,3 户:1 户有手机号且有小程序账号、1 户只有手机号、1 户都没有)。
短信模板号未配置(当前状态):
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2106359366087323650",
"eventCode": "GROUP_BATCH_FORMED",
"recipientCount": 1,
"smsCount": 0,
"inappCount": 1,
"skippedCount": 2,
"smsReady": false,
"enqueued": true,
"notifiedAt": "2026-10-03 20:29:03",
"cooldownSeconds": 300
},
"success": true
}
```
模板号已配置(同一个团):
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2106359366087323650",
"eventCode": "GROUP_BATCH_FORMED",
"recipientCount": 2,
"smsCount": 2,
"inappCount": 1,
"skippedCount": 1,
"smsReady": true,
"enqueued": true,
"notifiedAt": "2026-10-03 20:43:59",
"cooldownSeconds": 300
},
"success": true
}
```
#### 空数据 / 降级响应
- 短信模板号未配(当前状态):`smsReady=false`、`smsCount=0`,没有小程序账号的户全部计入 `skippedCount`。接口仍返回成功,冷却照常开始。
- 通知中心没有该事件的配置:两条通道都不可达,`recipientCount=0`、`skippedCount`=在团户数,接口仍返回成功。
#### 错误响应
| code | 含义 | 变化 |
|---|---|---|
| 589507 | 无团期管理权限 | 不变 |
| 589500 | 团期不存在 | 不变 |
| 589552 | 团期未成团(招募中)或已流团 | 不变 |
| 589593 | 冷却期内重复补发,message 含剩余秒数 | 不变 |
| 589594 | 没有可通知的客户 | 🔄 文案改为「本团期没有可通知的客户(无有效子订单,或各户既无合规手机号也无小程序账号)」,新增后一种情形 |
| 589595 | 通知中心暂不可用 | 新增一种触发:读不到通知配置时也返回本码(冷却立即释放,可马上重试) |
```json
{
"code": 589594,
"message": "本团期没有可通知的客户(无有效子订单,或各户既无合规手机号也无小程序账号)",
"data": null,
"success": false
}
```
```json
{
"code": 589593,
"message": "成团通知补发过于频繁,请 287 秒后再试",
"data": null,
"success": false
}
```
#### 业务边界
- 回执的计数是「已发出且按当前通知配置能到达」,不是供应商回执的「已送达」;单条短信最终是否送达以通知发送日志为准。
- 下单手机号不是 11 位大陆手机号(如座机)的户不发短信;该户若有小程序账号,通知中心会按账号绑定的手机号补发短信,这种情况**不计入** `smsCount`(保守口径)。
- 同一手机号挂在多户上时每户各发一条,`smsCount` 按户计。
- 冷却 300 秒只挡补发:成团刚发完立刻点补发不受冷却限制,模板号配好后客户会收到第二条短信。
---
## 四、契约约束与正确调用方式
- 补发成功提示请用新字段,不要再用 `recipientCount` 当「已通知户数 / 总户数」:建议文案「已通知 {recipientCount} 户(短信 {smsCount}、站内信 {inappCount}),{skippedCount} 户未能通知」。
- `smsReady=false` 时提示「短信模板未配置,本次仅发站内信」,避免运营误以为故障。
- `recipientCount` 不等于 `smsCount + inappCount`,不要相加。
---
## 五、数据库行为
- order-v3:零 DDL、零写入;补发时多一次对 user-service 通知配置的只读调用。
- user-service:迁移 `V20261003_8754` 只改 `notification_event_config` 中 `GROUP_BATCH_FORMED` 一行——开短信、模板号写哨兵 `TODO_PLACEHOLDER`(已填真实号不覆盖)、短信变量映射 `productName` / `departureDate` / `batchNo`。
- 每次成团 / 补发在 `notification_send_log` 按户写短信行(模板未配时状态为「模板未配置」)与站内信行,收件手机号以掩码落库。
---
## 六、边界行为
- **成团(手动 / 自动)**:契约不变;成团后每户发短信(下单手机号)+ 站内信(有小程序账号的户),通知失败不影响成团结果。
- **下单手机号与小程序账号都没有的户**:不发,补发时计入 `skippedCount`。
- **模板号填入**:运营经通知配置后台填真实阿里云模板号即生效,不必发版;模板变量名须为 `productName`、`departureDate`、`batchNo`(后台改不了变量映射)。
## 六.6、修改前后对比
| 场景 | 改前 | 改后 |
|---|---|---|
| 683 户无小程序账号、3 户有的团,补发 | `recipientCount=686`,实际只有 3 户收到站内信 | 模板未配:`recipientCount=3`、`smsCount=0`、`inappCount=3`、`skippedCount=683`、`smsReady=false`;模板配好后 `smsCount=686`、`recipientCount=686` |
| 成团后客户收到什么 | 只有有小程序账号的户收到站内信 | 每户按下单手机号收短信(模板配好后),有账号的户另收站内信 |
| 各户都没有任何联系方式 | 返回成功、`recipientCount`=户数 | 589594 |
## 六.7、影响评估
- **是否破坏向后兼容**:字段结构向后兼容(纯新增);`recipientCount` 的数值语义变了,前端若用它展示「已通知 N 户」,模板未配期间数字会变小。
- **前端是否必须同步上线**:否,但建议尽快把提示语换成新字段,否则模板未配期间会显示「已通知 0 户」之类让人误会的数字。
- **回滚**:revert PR #8775 后重新部署 order-v3 与 user-service;或只在通知配置后台关掉该事件的短信开关。
---
## 七、不影响范围
- 成团接口 `POST /v3/admin/order/group-batch/{groupBatchId}/group` 的入参、出参、错误码。
- 补发的判权、冷却 300 秒、状态门。
- 流团通知(#8247 另做)、其他通知事件。
- 小程序端接口:无变化(站内信照旧)。
---
## 八、测试环境已验证
**环境**:TEST(`https://api.test.1814.love`) **验证时间**:2026-10-03 20:25~20:50
**构建身份**:user-service `dev-v3 @ aac6a6a13`(迁移 `20261003.8754` 已执行);order-v3 `9b72d23a3`,已确认包含 `aac6a6a13`。探针:补发回执出现 `smsCount` / `skippedCount` / `smsReady` 字段。
**身份**:自签 admin token 经网关调用。
### 8.1 成团发出的通知
| 场景 | 结果 |
|---|---|
| 手动成团(团 A,3 户) | 有手机号的 2 户各一条短信记录(收件号码掩码,状态「模板未配置」),有小程序账号的那户另有一条站内信;两者都没有的户无记录,服务日志留了跳过行(只写「订单手机号=空」) |
| 自动成团(团 B,2 户,定时任务 20:25 触发) | 两户各一条短信记录(状态「模板未配置」) |
### 8.2 补发回执逐户对账
| 场景 | 回执 | 发送记录 |
|---|---|---|
| 模板未配(团 A) | `recipientCount=1, smsCount=0, inappCount=1, skippedCount=2, smsReady=false` | 新增 2 条短信(模板未配置)+ 1 条站内信(成功),逐户对得上;1 + 2 = 3 户 |
| 临时填假模板号(团 A) | `recipientCount=2, smsCount=2, inappCount=1, skippedCount=1, smsReady=true` | 两条短信到达阿里云后被拒(`isv.SMS_TEMPLATE_ILLEGAL`),不再是「模板未配置」;改完到生效 3 秒,无需发版 |
| 还原哨兵后再补发 | `smsReady=false, smsCount=0` | 短信回到「模板未配置」 |
### 8.3 反例
| 操作 | 结果 |
|---|---|
| 冷却期内再补发 | `589593「成团通知补发过于频繁,请 269 秒后再试」`,无新增发送记录 |
| 不带 token | `code 401「缺少有效的 Authorization 头」` |
| 对招募中团期补发 | `589552「团期尚未成团,请先完成成团后再操作」` |
### 8.4 脱敏
回执不含手机号;发送记录的短信收件号码全为掩码;造数手机号明文在 order-v3、user-service 日志里出现 0 次。
### 本地证据
| 项 | 读数 |
|---|---|
| order-v3 全量(有 Docker,两半) | A 10992 / B 4935 例;1144 个可执行类全部有报告;红类 3 个均在基底 `f8f553860` 逐条复现,本单引入 0 |
| user-service 全量 | 4211 例 0 失败 |
| 评审修正后定向 | order-v3 187 例(含全部架构门禁)+ user-service 143 例全绿;迁移 MySQL 测试 5 + 4 例通过 |
---
## 十、相关文档
- Issue `#8754`;PR `#8775`
- 前置:#7534(补发端点)、#8404(成团通知只开站内信)、#3713(按手机号直发短信通道)
## 关联 / 联系人
### 链接
- **Issue**: [#8754](https://git.1814.love/wx/HL/issues/8754)
- **PR**: [#8775](https://git.1814.love/wx/HL/pulls/8775)
- **Merge commit**: [aac6a6a13](https://git.1814.love/wx/HL/commit/aac6a6a13)
### 联系人
- **后端负责人**: @jw
@@ -0,0 +1,204 @@
---
schema: "hl-changelog/v2"
ticket: "8754"
title: "通知中心内部接口「按事件码查通知事件配置」出参新增 smsTemplateReady(短信模板是否已配真实号)"
consumer: "internal"
author: "jw(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "GET /internal/notification/event-config/{eventCode} 出参纯新增派生字段 smsTemplateReady(模板号非空且非哨兵 TODO_PLACEHOLDER 为 true),首个调用方为 order-v3 补发成团通知。内部接口不经网关,前端无需对接。已合并 dev-v3(PR #8775,merge aac6a6a13)并部署 TEST,经 order-v3 补发链路验证哨兵 / 假模板号 / 还原三态读数正确。管理后台侧变化见同日 04_8754 修改接口·管理后台那份。"
updated_at: "2026-10-04"
base: "dev-v3"
---
# user-service: 内部接口「按事件码查通知事件配置」新增 smsTemplateReady
> **服务**: hl-user-service(内部接口,网关不放行)
> **PR**: `#8775`(已合入 `dev-v3`,合并提交 `aac6a6a13`)
> **Issue**: #8754
---
## ⚠️ 关键变化
🟢 `GET /internal/notification/event-config/{eventCode}` 出参**纯新增**一个派生字段 `smsTemplateReady`(Boolean):短信模板号非空且不是哨兵 `TODO_PLACEHOLDER` 时为 `true`。入参、其余出参、错误行为不变。
🟢 新增首个调用方:order-v3 补发成团通知(`NotificationSendLogFeignClient#getEventConfig`)据此判断短信能不能真发,补发回执才能如实计数。
---
## 一、背景
「先开发、模板后补」期间,事件配置的 `sms_enabled=1` 而 `sms_template_code='TODO_PLACEHOLDER'`:通知中心照常进短信通道,但 `SmsChannelSender` 遇哨兵只记 SKIP_NO_TEMPLATE、不真发。调用方只看 `smsEnabled` 会把发不出去的短信算成送达。哨兵判定原只写在 `SmsChannelSender` 里,本单收成 `SmsTemplateCodes.isConfigured` 一处,发送侧与本接口共用,调用方不必知道哨兵字面量。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 按事件码查通知事件配置 | GET | `/internal/notification/event-config/{eventCode}` | 修改 | 出参新增派生字段 `smsTemplateReady` |
---
## 三、接口详情
### 1. 按事件码查通知事件配置 `GET /internal/notification/event-config/{eventCode}`
**VO**: `Result<NotificationEventConfigRespVO>`
#### 使用场景
服务间只读查询某通知事件的通道开关与模板配置。order-v3 补发成团通知时调用,按 `inappEnabled`、`smsEnabled && smsTemplateReady` 判断两条客户通道能否真发出。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| eventCode | Path | String | ✅ | 事件编码 | **不变**,如 `GROUP_BATCH_FORMED` |
| X-Internal-Token | Header | String | ✅ | 集群内部令牌 | **不变**,与其他 `/internal/**` 相同 |
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| smsTemplateReady | Boolean | 🆕 短信模板是否已就绪:`smsTemplateCode` 非空且不是 `TODO_PLACEHOLDER` 为 `true`。派生字段,不落库。与 `smsEnabled=1` 同时成立短信才会进入真实发送 |
| eventCode / eventName / categoryCode | String | **不变** |
| inappEnabled / smsEnabled / miniappEnabled / oaEnabled / weworkEnabled | Integer | **不变**,0=关 1=开 |
| smsTemplateCode / smsSignName / smsFieldMapping 等模板字段 | String | **不变** |
#### 请求示例
```http
GET /internal/notification/event-config/GROUP_BATCH_FORMED HTTP/1.1
Host: hl-user-service
X-Internal-Token: <集群内部令牌>
```
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"eventCode": "GROUP_BATCH_FORMED",
"eventName": "成团通知",
"categoryCode": "GROUP_BATCH",
"inappEnabled": 1,
"inappTitleTemplate": "您报名的团已成团",
"inappContentTemplate": "您报名的${productName}(出发日期${departDate})已成团,请留意后续出行安排。",
"inappLinkTemplate": "/packages/order/detail/detail?id=${orderId}",
"smsEnabled": 1,
"smsTemplateCode": "TODO_PLACEHOLDER",
"smsSignName": null,
"smsFieldMapping": "{\"productName\":\"${productName}\",\"departureDate\":\"${departDate}\",\"batchNo\":\"${groupBatchNo}\"}",
"miniappEnabled": 0,
"oaEnabled": 0,
"weworkEnabled": 0,
"weworkReceiverType": null,
"smsTemplateReady": false
},
"success": true
}
```
#### 空数据 / 降级响应
事件不存在时 `data=null`(不变),调用方应视为所有通道不可达。order-v3 侧降级(调用失败 / 熔断)返回失败结果 `100903`,补发接口据此返回 589595,不会把失败当成「未配置」。
#### 错误响应
缺少或错误的 `X-Internal-Token` 被 `InternalAuthInterceptor` 拦截(不变):**HTTP 403**,响应体如下(注意字段是 `msg` 不是 `message`):
```json
{
"code": 403,
"msg": "内部接口禁止外部访问"
}
```
#### 业务边界
- `smsTemplateReady` 只看模板号,不看 `smsEnabled`;调用方须两者同时判断。
- 不判断阿里云凭据是否配置:TEST 未配凭据时短信为模拟成功(只打日志不真发),本字段仍可能为 `true`。
---
## 四、契约约束与正确调用方式
- 判断「短信能否真发」:`smsEnabled == 1 && smsTemplateReady == true`。不要在调用方写死 `TODO_PLACEHOLDER` 字面量。
- 调用方本地 VO 建议 `@JsonIgnoreProperties(ignoreUnknown = true)`,只取需要的字段。
---
## 五、数据库行为
- 本接口只读,零写入;`smsTemplateReady` 由 `sms_template_code` 派生,不落库。
- 同批迁移 `V20261003_8754` 改了 `notification_event_config` 中 `GROUP_BATCH_FORMED` 一行(开短信、模板号哨兵、变量映射),与本接口契约无关。
---
## 六、边界行为
- `smsTemplateCode` 为 `null` / 空串 / `TODO_PLACEHOLDER` → `false`;其他任意值 → `true`(不校验是否为阿里云真实存在的模板号)。
## 六.6、修改前后对比
| 场景 | 改前 | 改后 |
|---|---|---|
| 模板号为哨兵 | 出参无此字段,调用方只能自己比对字面量 | `smsTemplateReady=false` |
| 模板号为真实值 | 同上 | `smsTemplateReady=true` |
## 六.7、影响评估
- **是否破坏向后兼容**:否,纯新增字段;改前全仓零调用方。
- **回滚**:revert PR #8775 后重新部署 user-service(order-v3 读不到该字段时按「短信未就绪」保守计数)。
---
## 七、不影响范围
- 其他 `/internal/notification/**` 接口、通知分发逻辑、短信发送行为(哨兵判定结果与改前逐字等价)。
- 管理后台通知配置接口。
---
## 八、测试环境已验证
**环境**:TEST **验证时间**:2026-10-03 20:43~20:50
**构建身份**:user-service `dev-v3 @ aac6a6a13`。
本接口不经网关,经它唯一的调用方 order-v3 补发链路间接验证:
| 配置状态 | order-v3 补发回执 | 说明 |
|---|---|---|
| `smsEnabled=1`,`smsTemplateCode=TODO_PLACEHOLDER` | `smsReady=false`、`smsCount=0` | order-v3 读到 `smsTemplateReady=false` |
| 后台把模板号改成假号 `SMS_TEST_8754` | `smsReady=true`、`smsCount=2` | 读到 `smsTemplateReady=true`;后台改配置时清了缓存,3 秒后即生效 |
| 还原为 `TODO_PLACEHOLDER` | `smsReady=false` | 读到 `smsTemplateReady=false` |
本地:`InternalNotificationControllerTest` 15 例(含哨兵 → `false` 且序列化进 JSON)、`SmsTemplateCodesTest` 2 例、`SmsChannelSenderTest` 37 例;user-service 全量 4211 例 0 失败。
---
## 十、相关文档
- Issue `#8754`;PR `#8775`;管理后台侧变化见同日 `04_8754` 修改接口·管理后台那份
## 关联 / 联系人
### 链接
- **Issue**: [#8754](https://git.1814.love/wx/HL/issues/8754)
- **PR**: [#8775](https://git.1814.love/wx/HL/pulls/8775)
- **Merge commit**: [aac6a6a13](https://git.1814.love/wx/HL/commit/aac6a6a13)
### 联系人
- **后端负责人**: @jw
@@ -0,0 +1,449 @@
---
schema: "hl-changelog/v2"
ticket: "8755"
title: "发票推送接短信(按下单手机号直发):推送日志按真实结果落库,pushStatus 新增 SKIP「未发送」,邮件 / 微信不再假写成功"
consumer: "admin"
author: "jw(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "bfe3c4537a718769af4f8f99dbe0fa24e0f763f0"
target_release: "v2.1"
verified_at: "2026-10-04"
status_note: "PUT /v3/admin/order/invoice/{id}/push 入参 / 出参 / 错误码不变,行为变:选 sms 时经通知中心按订单下单手机号直发「发票已开具」短信(带 8 位下载短码),推送日志按通知中心真实结果写 SUCCESS / FAILED / SKIP / PENDING;email / wechat 未接入,一律写 SKIP。GET /{id}/push-logs 与 GET /{id} 的 pushLogs[].pushStatus 新增取值 SKIP(statusText「未发送」),failReason 如实写原因。短信模板尚在申请(#8790,wx),模板到位前短信渠道写 SKIP「短信模板未配置,未发送」。已合并 dev-v3(9cd23903a)并部署 TEST,经网关实测,工单 #8755 已关。前端待做两处:推送弹窗 PushModal.vue:15 文案「本期仅记录推送状态,客户暂不会实际收到通知」改为如实说明(短信按下单手机号发送、邮件 / 微信暂不发送);推送日志抽屉 SKIP 行的原因前缀「失败原因:」改为「原因:」,可选给 SKIP 配一个标签色。statusText 已能自动显示「未发送」。前端已交付(2026-10-04):PushModal 文案改如实说明(短信按下单手机号发送/模板生效后,邮件微信暂不发送);PushLogsDrawer 前缀改「原因:」且仅 FAILED 染红(SKIP 走灰 tag+次级色原因行);invoice.js JSDoc 补 SKIP 口径,提交 bfe3c4537。"
updated_at: "2026-10-04"
base: "dev-v3"
---
# 发票推送接短信:推送日志按真实结果落库、新增 SKIP(管理后台)
> **服务**: hl-order-service-v3(端口 8086/8186)+ hl-user-service(通知事件配置迁移)
> **PR**: #8776
> **Issue**: #8755
> **日期**: 2026-10-03
> **影响范围**: 管理后台财务「发票管理」页的推送弹窗、推送记录抽屉、发票详情抽屉;接口签名零变化
---
## ⚠️ 关键变化
1. **推送不再是假推送**:选「短信」时经通知中心按**订单下单手机号**直发(不依赖小程序 userId,团期子订单也能收到),短信里带 8 位下载短码,客户免登录下载发票(配套新增接口见同单另一份 changelog)。
2. **推送日志如实落库**:`pushStatus` 新增 **`SKIP`**(`statusText`「未发送」),`failReason` 写清原因;原先三个渠道一律写 `SUCCESS` 的口径作废。
3. **邮件 / 微信未接入**:选了照样能推(状态机不变),但日志写 `SKIP`「邮件渠道未接入,未发送」/「微信渠道未接入,未发送」。
4. **短信模板未到位**(#8790):模板过审填入前,短信渠道日志写 `SKIP`「短信模板未配置,未发送」,客户收不到短信。
5. **短信结果是异步回写的**:推送接口返回时短信那行先是 `PENDING`,通常 1 秒内回写为终态;推送记录抽屉打开时再拉一次即可。
---
## 一、背景
#4230 曾定「本期不真发」:三个推送渠道监听器只登记一条 `SUCCESS`,前端弹窗也提示「客户暂不会实际收到通知」。jw 2026-10-03 定客户触达一律短信、按下单手机号直发、模板后补,于是本单接通短信并把推送日志改为如实落库。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 推送发票给客户 | PUT | `/v3/admin/order/invoice/{id}/push` | 修改接口 | 入参出参不变;sms 真发短信,各渠道日志按真实结果落库 |
| 2 | 发票推送明细日志 | GET | `/v3/admin/order/invoice/{id}/push-logs` | 修改接口 | `pushStatus` 新增 `SKIP`,`failReason` 如实写原因 |
| 3 | 发票详情 | GET | `/v3/admin/order/invoice/{id}` | 修改接口 | 仅 `pushLogs[]` 同上变化,其余字段不变 |
网关:本组接口无改动(在既有 `/v3/admin/**` 通配下)。
---
## 三、接口详情
### 1. 推送发票给客户 `PUT /v3/admin/order/invoice/{id}/push`
**VO**: `InvoicePushReqVO`(入参)/ `Result<Void>`(出参)
#### 使用场景
财务在「发票管理 · 待推送 / 已推送」点「推送」,勾选渠道后提交。发票状态 ISSUED → PUSHED(PUSHED 可再次推送);事务提交后各渠道异步发送并写推送日志。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| id | path | string(Long) | 是 | 发票 ID | 发票主键,字符串透传 |
| channels | body | string[] | 是 | 非空;元素 ∈ 字典 `invoice_push_channel`(email / wechat / sms) | 推送渠道,可多选 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| code | int | 200 成功 |
| message | string | 「成功」 |
| data | null | 无数据;各渠道发送结果看推送日志 |
#### 请求示例
```json
{
"channels": ["sms", "email"]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": null,
"traceId": null,
"success": true
}
```
#### 空数据 / 降级响应
接口本身无空数据形态。通知中心不可用、短信发送失败都**不影响推送接口返回成功**(发送在事务提交后异步进行),失败结果体现在推送日志 `FAILED` / `PENDING`:
```json
{
"channel": "sms",
"pushStatus": "FAILED",
"statusText": "失败",
"pushedBy": "王会计",
"pushedAt": "2026-10-03 21:05:45",
"failReason": "通知中心调用失败"
}
```
#### 错误响应
错误码与改前完全一致:
| 业务码 | 场景 |
|---|---|
| 581500 | 发票不存在 |
| 581520 | 发票当前状态不允许推送(须 ISSUED / PUSHED) |
| 581521 | 推送渠道为空 |
| 581522 | 推送渠道非法 |
```json
{
"code": 581520,
"message": "发票当前状态不允许推送(须已开票)",
"data": null,
"traceId": null,
"success": false
}
```
#### 业务边界
- 状态机不变:ISSUED / PUSHED → PUSHED,`pushedAt` / `pushedChannels` 每次推送覆盖。
- 每个勾选渠道每次推送写一条日志;sms 先写 `PENDING`,发完回写终态。
- 短信收件人 = 订单 `customer_phone`(下单手机号);为空时写 `SKIP`「下单手机号为空,未发送短信」,不生成下载码。
- 短信只在 sms 渠道发;email / wechat 不发、写 `SKIP`。
- 同一发票连推两次会发两条短信(每次推送各自生成下载码)。
### 2. 发票推送明细日志 `GET /v3/admin/order/invoice/{id}/push-logs`
**VO**: `InvoicePushLogRespVO`
#### 使用场景
推送记录抽屉按推送时间倒序展示每次、每个渠道的推送结果。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| id | path | string(Long) | 是 | 发票 ID | 发票主键 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| channel | string | email / wechat / sms |
| pushStatus | string | SUCCESS / FAILED / PENDING / **SKIP(新增)** |
| statusText | string | 成功 / 失败 / 待发 / **未发送(新增)** |
| pushedBy | string | 操作人真实姓名 |
| pushedAt | string | 推送时间 `yyyy-MM-dd HH:mm:ss` |
| failReason | string \| null | 原因:FAILED / SKIP / PENDING 时有值,SUCCESS 为 null(原注释「预留」,现已启用) |
#### 请求示例
```http
GET /v3/admin/order/invoice/2106369928091283458/push-logs
Authorization: Bearer <admin token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": [
{
"channel": "email",
"pushStatus": "SKIP",
"statusText": "未发送",
"pushedBy": "admin",
"pushedAt": "2026-10-03 21:11:53",
"failReason": "邮件渠道未接入,未发送"
},
{
"channel": "sms",
"pushStatus": "SKIP",
"statusText": "未发送",
"pushedBy": "admin",
"pushedAt": "2026-10-03 21:05:45",
"failReason": "短信模板未配置,未发送"
}
],
"traceId": null,
"success": true
}
```
#### 空数据 / 降级响应
从未推送过:
```json
{
"code": 200,
"message": "成功",
"data": [],
"traceId": null,
"success": true
}
```
#### 错误响应
```json
{
"code": 581500,
"message": "发票不存在",
"data": null,
"traceId": null,
"success": false
}
```
#### 业务边界
- 存量日志不订正:本单上线前的历史行仍是 `SUCCESS`(当时并未真发)。
- `pushStatus` 未知值时 `statusText` 回落为原始值(既有口径)。
- 短信行在推送后约 1 秒内由 `PENDING` 回写为终态;结果不确定时保留 `PENDING` 并写「以通知中心发送日志为准」。
### 3. 发票详情 `GET /v3/admin/order/invoice/{id}`
**VO**: `AdminInvoiceDetailRespVO`
#### 使用场景
发票详情 / 上传抽屉。本单只影响其中的 `pushLogs[]`(与接口 2 同源同 VO),其余字段零变化。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| id | path | string(Long) | 是 | 发票 ID | 发票主键 |
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| pushLogs | InvoicePushLogRespVO[] | 字段与接口 2 完全一致:`pushStatus` 新增 SKIP、`statusText` 新增「未发送」、`failReason` 如实写原因 |
| 其余字段 | - | 不变 |
#### 请求示例
```http
GET /v3/admin/order/invoice/2106369928091283458
Authorization: Bearer <admin token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "2106369928091283458",
"orderNo": "HL20260930170915373",
"status": "PUSHED",
"statusText": "已推送",
"invoiceNo": "26152000000087551236",
"pushedChannels": "email",
"pushLogs": [
{
"channel": "email",
"pushStatus": "SKIP",
"statusText": "未发送",
"pushedBy": "admin",
"pushedAt": "2026-10-03 21:11:53",
"failReason": "邮件渠道未接入,未发送"
},
{
"channel": "sms",
"pushStatus": "SKIP",
"statusText": "未发送",
"pushedBy": "admin",
"pushedAt": "2026-10-03 21:05:45",
"failReason": "短信模板未配置,未发送"
}
]
},
"traceId": null,
"success": true
}
```
#### 空数据 / 降级响应
未推送过的发票 `pushLogs` 为空数组:
```json
{
"pushLogs": []
}
```
#### 错误响应
```json
{
"code": 581500,
"message": "发票不存在",
"data": null,
"traceId": null,
"success": false
}
```
#### 业务边界
- 判权、其余字段、`status` / `statusText` 口径均不变。
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误调用顺序
- ✅ 推送记录展示用 `statusText`;`failReason` 非空时都展示(SKIP、PENDING 也有原因),前缀建议用「原因:」而不是「失败原因:」。
- ✅ 推送成功后若立刻打开推送记录,短信行可能还是 `PENDING`,可在抽屉打开时重新拉取。
- ❌ 不要把 `SKIP` 渲染成失败(红色),它表示「没发」而不是「发失败」。
- ❌ 不要继续展示「本期仅记录推送状态,客户暂不会实际收到通知」。
---
## 五、数据库行为
- **order-v3**:零 DDL。`invoice_push_log.push_status`(VARCHAR(16)、无 CHECK)新增写入值 `SKIP`;`fail_reason` 由「预留」改为如实写原因。短信推送每次写一条 `invoice_download_code`(建表见同单新增接口 changelog)。
- **user-service**:Flyway `V20261003_8755__invoice_pushed_notification.sql` 往 `notification_event_config` 插(或按 `uk_event_code` 收敛)`INVOICE_PUSHED`:只开短信、模板哨兵 `TODO_PLACEHOLDER`、`sms_field_mapping` 为 orderNo / invoiceNo / amount / code;重放不覆盖运营已填的模板编码。通知中心每次发送写一行 `notification_send_log`(`biz_type=INVOICE_PUSH`、`biz_id`=推送日志 ID,收件人脱敏)。
---
## 六、边界行为
| 场景 | 推送日志 |
|---|---|
| 选 sms,模板未配置(当前) | SKIP「短信模板未配置,未发送」 |
| 选 sms,下单手机号为空 | SKIP「下单手机号为空,未发送短信」 |
| 选 sms,阿里云受理(带回执号) | SUCCESS |
| 选 sms,短信通道为模拟档(无回执号) | SKIP「短信通道为模拟档(未配置短信网关),未真实发送」 |
| 选 sms,阿里云拒发 | FAILED「短信发送失败:<原因>」 |
| 选 sms,通知中心不可用 | FAILED「通知中心调用失败」 |
| 选 sms,投递结果不确定 / 查不到发送记录 | PENDING(写明以通知中心发送日志为准) |
| 选 email / wechat | SKIP「邮件 / 微信渠道未接入,未发送」 |
| 发票状态非 ISSUED / PUSHED | 581520,不写日志(同改前) |
---
## 六.6、修改前后对比
| 项 | 改前 | 改后 |
|----|------|------|
| 选 sms | 不发,日志写 SUCCESS | 经通知中心按下单手机号直发,日志按真实结果写 |
| 选 email / wechat | 不发,日志写 SUCCESS | 不发,日志写 SKIP + 原因 |
| `pushStatus` 取值 | SUCCESS(实际恒定)/ FAILED / PENDING(预留) | SUCCESS / FAILED / PENDING / **SKIP** |
| `statusText` 取值 | 成功 / 失败 / 待发 | 成功 / 失败 / 待发 / **未发送** |
| `failReason` | 恒 null | FAILED / SKIP / PENDING 时写原因 |
| 短信行写入时机 | 推送后异步一次写入 | 推送后异步先写 PENDING,发完回写终态 |
| 入参 / 出参 / 错误码 / 状态机 | — | 不变 |
---
## 六.7、影响评估
- **前端(hl-ui v2.1 实查)**:
- `src/views/order/invoice/components/PushLogsDrawer.vue:30` 显示 `log.statusText || log.pushStatus`,「未发送」**自动生效**;`:27` 标签色 `STATUS_TYPE[s] || 'default'`,SKIP 落灰色,可选补一个色。
- `PushLogsDrawer.vue:41`把 `failReason` 冠以「失败原因:」前缀展示——SKIP 行也会显示,**前缀建议改「原因:」**。
- `src/views/order/invoice/components/PushModal.vue:15`「本期仅记录推送状态,客户暂不会实际收到通知。」**需改**为如实说明:短信按订单下单手机号发送(模板生效后),邮件、微信暂不发送。
- `src/api/invoice.js:191` 注释中的取值说明可同步补 SKIP。
- **历史数据**:上线前的推送日志仍是 SUCCESS(当时未真发),不订正。
- **其他调用方**:无。小程序端、通知中心其他事件、团期 / 订单流程零影响。
---
## 七、不影响范围
- **仅影响**: 推送接口的发送行为与推送日志取值(3 个管理端接口的 `pushLogs` 相关字段)。
- **零影响**:
- 发票申请、开票、重传、重开、列表、统计接口
- 小程序发票查询 / 下载 / 重开
- 通知中心其他事件
- 零权限种子变更、零网关变更(本组接口)。
---
## 八、测试环境已验证
部署:hl-user-service、hl-order-service-v3、hl-gateway = dev-v3 @ 9cd23903a(2026-10-03 20:56 / 20:59 / 21:00);TEST `flyway_schema_history` user `20261003.8755` success=1,`INVOICE_PUSHED` 配置行读回只开短信、模板 `TODO_PLACEHOLDER`;构建身份探针 6/6 命中新代码。经网关 `https://api.test.1814.love` 真实鉴权实测(2026-10-03 21:05–21:11),工单 #8755 已验收关单。
| # | 场景 | 结果 |
|---|---|---|
| 1 | 团期子订单(user_id 为空)发票 ISSUED,推 `["sms"]` | 200;发票 → PUSHED;推送日志 SKIP「短信模板未配置,未发送」;通知中心发送日志一行 `INVOICE_PUSHED / SMS / status=3`,收件人 `138****2356`,`params_json` 无手机号;生成下载码 1 枚(7 天) |
| 2 | 另一张 ISSUED 发票推 `["email","wechat"]` | 200;发票 → PUSHED;两行 SKIP,原因分别为微信 / 邮件渠道未接入;不生成下载码 |
| 3 | 已 PUSHED 发票再推 `["email"]` | 200;仍 PUSHED,`pushedAt` / `pushedChannels` 已更新 |
| 4 | `GET /{id}/push-logs`、`GET /{id}` | `pushStatus=SKIP`、`statusText=未发送`、`failReason` 如实 |
| 5 | 日志脱敏 | TEST order-v3 日志 11 位手机号出现 0 次 |
| 6 | TEST 短信网关形态 | `aliyun.sms.access-key-id` 为真实 AK(非模拟档);哨兵期在模板检查处跳过,未触达阿里云、未发出短信 |
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| — | #4230 | 发票推送「本期不真发」 | ⚠️ 被本单取代 |
| — | #3713 | 通知中心 directPhone 收件通道 | ✅ |
| — | #8754 | 成团通知客户短信(同样按下单手机号直发) | ✅ |
| **本 PR #8776** | **#8755** | 发票推送接短信 + 凭短码免登录下载 | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#8755](https://git.1814.love/wx/HL/issues/8755)
- 关联 PR: [wx/HL#8776](https://git.1814.love/wx/HL/pulls/8776)
- 短信模板申请跟进: [wx/HL#8790](https://git.1814.love/wx/HL/issues/8790)
- 同单配套新增接口: `changelogs-v2/2026-10/04_8755_发票短信短码免登录下载-新增接口-管理后台.md`
## 关联 / 联系人
### 链接
- **Issue**: [#8755](https://git.1814.love/wx/HL/issues/8755)
- **PR**: [#8776](https://git.1814.love/wx/HL/pulls/8776)
- **Merge commit**: [9cd23903a](https://git.1814.love/wx/HL/commit/9cd23903abe8edc75e13ca2e7c4d5c65bd9e4453)
### 联系人
- **后端负责人**: @jw
- **前端负责人**: @mmg
- **短信模板**: @wx(#8790)
@@ -0,0 +1,259 @@
---
schema: "hl-changelog/v2"
ticket: "8755"
title: "发票短信短码免登录下载:新增公开端点 GET /v3/open/invoice/{code},客户凭短信里的 8 位短码 302 到现签 1 小时的发票文件"
consumer: "multiple"
author: "jw(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
updated_at: "2026-10-04"
base: "dev-v3"
status_note: "新增公开端点 GET /v3/open/invoice/{code}(网关新路由 invoice-open-v3 + SKIP_URLS,无登录态,凭码即鉴权)。财务推送发票选短信时,order-v3 为本次推送生成 8 位 base62 短码(列 utf8mb4_bin 区分大小写、7 天有效),客户点短信链接经本端点 302 到现签 1 小时的 OSS 发票文件;按客户端 IP 30 次/分钟限流。端点只给短信链接用,不给管理后台 / 小程序前端调用,Swagger 不展示,前端零适配。已合并 dev-v3(9cd23903a),部署 TEST(gateway、order-v3、user-service),经网关匿名实测 302 + PDF 字节一致、篡改 / 过期 / 限流均按约定拒绝,工单 #8755 已验收关单。短信模板尚在申请(#8790,wx),模板到位前客户收不到短信,本端点可用但无人拿到码。"
---
# 发票短信短码:新增免登录下载端点(公开)
> **服务**: hl-order-service-v3(端口 8086/8186)+ hl-gateway(新路由与免鉴权白名单)
> **PR**: #8776
> **Issue**: #8755
> **日期**: 2026-10-03
> **影响范围**: 新增一个对外公开端点,只给发票推送短信里的链接用;管理后台、小程序前端零适配
---
## ⚠️ 关键变化
1. **新增公开端点** `GET /v3/open/invoice/{code}`:不带任何登录态,凭 8 位短码访问,成功 **302** 到现签 1 小时的发票文件(OSS 私有桶)。
2. **网关新前缀** `/v3/open/invoice/**`:新路由 `invoice-open-v3` → order-service-v3,并加入 `JwtAuthFilter.SKIP_URLS`;客户端伪造的 `X-User-Id` / `X-Admin-Id` 会被网关剥掉。
3. **短码来源**:财务在发票管理页推送并勾选「短信」时,每推一次生成一枚新码(`invoice_download_code`),7 天有效;码绑定发票,财务重传文件后旧码下载到的是新文件,发票作废后码失效。
4. **短信模板未到位**:通知配置 `INVOICE_PUSHED` 仍是哨兵 `TODO_PLACEHOLDER`(申请见 #8790),客户暂时收不到短信;端点已上线可用。
---
## 一、背景
发票推送原先是假推送(#4230 定「本期不真发」)。#8755 把短信渠道接到通知中心、按订单下单手机号直发;团期子订单绝大多数没有 userId,小程序发票下载要求本人登录(`order.userId == 当前用户`),这些客户在小程序里拿不到发票,所以短信里放一个免登录的下载链接。阿里云短信「链接参数」变量最多 8 位,放不下签名令牌,于是落库存 8 位随机短码。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 发票短码下载 | GET | `/v3/open/invoice/{code}` | 新增接口 | 免登录;成功 302 到现签 1 小时的发票文件;按 IP 30 次/分钟限流 |
网关:新增路由 `invoice-open-v3`(`Path=/v3/open/invoice/**` → `lb://hl-order-service-v3`),`/v3/open/invoice/**` 进 `SKIP_URLS`。
---
## 三、接口详情
### 1. 发票短码下载 `GET /v3/open/invoice/{code}`
**VO**: `ResponseEntity<Void>`(成功为 302 无响应体,失败为通用 `Result<Void>`)
#### 使用场景
客户收到「发票已开具」短信,点短信里的链接(模板正文写死对外域名 + 路径,变量只放 `code`),浏览器经本端点跳到发票 PDF 直接查看 / 下载。不给任何前端页面调用。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| code | path | string | 是 | 恰好 8 位字母数字 `^[0-9A-Za-z]{8}$`,**区分大小写** | 发票推送短信里的下载短码 |
无请求头要求:不需要 `Authorization`,带了也不参与鉴权(网关对本前缀剥掉身份头)。
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| HTTP 状态 | int | 成功固定 `302 Found` |
| Location | header string | 现签的 OSS 下载 URL,有效 1 小时(user-service `oss.signed-url-expire`),PDF 以 inline 方式打开 |
| Cache-Control | header string | 固定 `no-store`,签名 URL 不进任何缓存 |
| body | - | 成功无响应体 |
#### 请求示例
```http
GET /v3/open/invoice/K7mQ2xRb HTTP/1.1
Host: api.test.1814.love
```
#### 响应示例
成功(302,无响应体;以下为响应头的结构化描述):
```json
{
"httpStatus": 302,
"headers": {
"Location": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/test/invoice/2026/10/03/64674f6aa6086d1e42d7abcc2b1b50e0.pdf?Expires=1791036463&OSSAccessKeyId=***&Signature=***",
"Cache-Control": "no-store"
},
"body": null
}
```
#### 空数据 / 降级响应
没有空数据形态。文件服务(user-service 签名接口)不可用时不跳转,返回业务码 581519:
```json
{
"code": 581519,
"message": "发票下载服务暂时不可用,请稍后重试",
"data": null,
"traceId": null,
"success": false
}
```
#### 错误响应
失败一律 HTTP 200 + 业务码,不带任何发票 / 订单字段:
| 业务码 | 场景 |
|---|---|
| 581527 | 码格式不符(非 8 位字母数字)、查无此码、大小写或任一字符被改动 |
| 581528 | 码已过期(默认生成后 7 天) |
| 581517 | 码对应的发票已不可下载(已作废 / 不存在 / 文件地址异常),文案「发票尚未开具,暂时无法下载」沿用既有码 |
| 581519 | 现签下载链接失败(文件服务不可用) |
| 100501 | 同一来源 60 秒内超过 30 次 |
```json
{
"code": 581527,
"message": "发票下载链接无效",
"data": null,
"traceId": null,
"success": false
}
```
```json
{
"code": 581528,
"message": "发票下载链接已过期,请联系客服重新推送",
"data": null,
"traceId": null,
"success": false
}
```
```json
{
"code": 100501,
"message": "访问过于频繁,请稍后再试",
"data": null,
"traceId": null,
"success": false
}
```
#### 业务边界
- 每次「推送 + 勾选短信」且下单手机号非空时才生成码;邮件 / 微信推送、手机号为空都不生成。
- 同一张发票多次推送会有多枚码,各自 7 天有效、互不作废。
- 码绑定发票而不是文件:财务「重新上传」后,旧码下载到的是新文件;发票作废(重开)后旧码返回 581517。
- 302 的 Location 每次点击现签,有效 1 小时;客户隔天再点同一短信链接会拿到新的签名 URL(码 7 天内有效)。
- 限流按客户端 IP(取 `X-Forwarded-For` 第一段),30 次 / 60 秒。
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误调用顺序
- ✅ 只在短信模板正文里写死「对外域名 + `/v3/open/invoice/` + `${code}`」(或经 nginx 短路径转发到本路径,见 #8790),变量只放 8 位码。
- ✅ 浏览器直接打开即可,跟随 302。
- ❌ 不要在管理后台 / 小程序里拼这个链接给用户:后台有发票详情与下载能力,小程序有登录态下载接口。
- ❌ 不要把 Location 里的签名 URL 存下来复用,它 1 小时过期。
- ❌ 不要对码做大小写归一化,码区分大小写。
---
## 五、数据库行为
- **order-v3**:Flyway `V20261003_8755__create_invoice_download_code.sql` 新建表 `invoice_download_code`(`code_id` 雪花主键、`short_code VARCHAR(16) utf8mb4_bin` 唯一键、`invoice_id`、`order_id`、`push_log_id`、`expire_at` + BaseDO 五列)。码在发票短信推送时写入(每次推送一行),本端点**只读不写**。
- 有效期配置项 `hl.order-v3.invoice.download-code-ttl-days`,缺省 7(未写入 nacos,取默认值)。
---
## 六、边界行为
| 场景 | 行为 |
|---|---|
| 正常码、发票 ISSUED / PUSHED | 302 到现签 1 小时的发票文件 |
| 翻转码里一个字母的大小写 | 581527(列 `utf8mb4_bin` + 服务内逐字节比对,双保险) |
| 改动任一字符 / 位数不对 | 581527 |
| 码过期 | 581528 |
| 发票已作废 | 581517 |
| 文件服务不可用 | 581519 |
| 同一 IP 60 秒内第 31 次起 | 100501 |
| 带伪造的 `X-User-Id` / `X-Admin-Id` | 网关剥掉,按匿名处理 |
| 访问 `/v3/open/` 下其他路径 | 网关仍按需登录拦截(白名单只放行发票前缀) |
---
## 七、不影响范围
- **仅影响**: 新增一个公开端点、一张表、一条网关路由与白名单项。
- **零影响**:
- 小程序发票下载 `GET /v3/internal/mp/order/invoice/{id}/download`(登录态 + 本人校验,行为不变;内部改为复用抽出的 ossKey 解析,逐字等价)
- 管理后台发票列表、详情、开票、重传接口
- Swagger 文档(本端点方法级 hidden,不进任何分组)
- 零权限种子变更、零前端适配。
---
## 八、测试环境已验证
部署:hl-user-service、hl-order-service-v3、hl-gateway = dev-v3 @ 9cd23903a(2026-10-03 20:56 / 20:59 / 21:00);TEST `flyway_schema_history` order-v3 `20261003.8755` success=1,`invoice_download_code.short_code` 排序规则实测 `utf8mb4_bin`;构建身份探针:匿名请求不存在的码连打 6 次全部 581527。经网关 `https://api.test.1814.love` 匿名实测(2026-10-03 21:05–21:11),工单 #8755 已验收关单。
| # | 场景 | 结果 |
|---|---|---|
| 1 | 团期子订单(user_id 为空)发票推送短信后取码访问 | 302;`Cache-Control: no-store`;响应体 0 字节;Location 签名有效期 3600 秒 |
| 2 | 跟随 302 下载 | `%PDF-1.4`,SHA-256 与上传原件逐字节一致 |
| 3 | 翻转一个字母大小写 / 改最后一位 / 多一位 | 均 581527 |
| 4 | 把该码 `expire_at` 临时改到过去 | 581528;验后已还原,还原后恢复 302 |
| 5 | 同一来源 1.2 秒内连发 35 次 | 前 30 次 581527,第 31 次起 100501 |
| 6 | 日志脱敏 | TEST order-v3 日志里完整短码出现 0 次,只打前 2 位(如 `oK******`) |
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| — | #4230 | 发票推送「本期不真发」定案 | ⚠️ 被 #8755 取代(短信渠道已接通知中心) |
| — | #3780 | 车务 H5 录入链接短链化(短码进短信链接先例) | ✅ |
| **本 PR #8776** | **#8755** | 发票推送接短信 + 凭短码免登录下载 | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#8755](https://git.1814.love/wx/HL/issues/8755)
- 关联 PR: [wx/HL#8776](https://git.1814.love/wx/HL/pulls/8776)
- 短信模板申请跟进: [wx/HL#8790](https://git.1814.love/wx/HL/issues/8790)
- 同单配套变更(推送日志新增 SKIP): `changelogs-v2/2026-10/04_8755_发票推送接短信推送日志新增SKIP-修改接口-管理后台.md`
## 关联 / 联系人
### 链接
- **Issue**: [#8755](https://git.1814.love/wx/HL/issues/8755)
- **PR**: [#8776](https://git.1814.love/wx/HL/pulls/8776)
- **Merge commit**: [9cd23903a](https://git.1814.love/wx/HL/commit/9cd23903abe8edc75e13ca2e7c4d5c65bd9e4453)
### 联系人
- **后端负责人**: @jw
- **短信模板**: @wx(#8790)
@@ -0,0 +1,426 @@
---
schema: "hl-changelog/v2"
ticket: "8767"
title: "出团通知书:默认车辆信息补上团车(整团派车)的车辆与司机,releaseBlockers 新增「车辆未派司机」DRIVER 与「司机信息暂不可用」DRIVER_UNAVAILABLE"
consumer: "admin"
author: "jw(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "130a49ebfd326cbf083622819b036918be2a00a6"
target_release: "v2.1"
verified_at: "2026-10-04"
status_note: "已合并 dev-v3(PR #8794,merge commit a281744c8)并部署 TEST(order-v3 与 fleet 同为 dev-v3 a281744c8),自签 token 经网关实测:团车团 defaults.bus 与车务派车逐字一致且手机全脱敏;团车有车没派司机时 releasable=false、缺项恰为 DRIVER,补派后恢复;车务服务读不到时缺项为 DRIVER_UNAVAILABLE、接口仍 200;无团车的团读数与部署前逐项一致;判权不变。纯加取值,入参与路径不变;页面按 #8746 约定用 releaseBlockers[].name 展示即可,无需新适配。前端已交付(2026-10-04):grep 实证无 key 硬编码分支,但 #8746 缺项展示从未落地(modal 只按 releasable 置灰+静态阶段文案,DRIVER 场景会误导)——顺手对齐,GroupBatchNoticeModal 留存 releaseBlockers+printBlockTip 按 name 列示缺项(未知 key 零特判,name 缺失按 key 兜底,空缺项回退阶段文案),提交 130a49ebf。"
updated_at: "2026-10-04"
base: "dev-v3"
---
# order-v3: 出团通知书默认车辆补团车,下发缺项新增 DRIVER / DRIVER_UNAVAILABLE
**服务**: hl-order-service-v3(读团车经 hl-fleet-service 内部读口,见同日 04_8767 新增接口那份)
**PR**: `#8794`(已合入 `dev-v3`,合并提交 `a281744c8`)
**Issue**: #8767
---
## ⚠️ 关键变化
🔴 **`releasable` 又多一个条件**:在 #8746「阶段 + 四项资源」之上,还要求**团车(整团派车)的车辆都已派司机**。团车有车没派司机的团,`releasable` 由 `true` 变 `false`。
🟢 **`releaseBlockers` 追加两个取值**:`DRIVER`「车辆未派司机」、`DRIVER_UNAVAILABLE`「司机信息暂不可用」,排在 `PHOTOGRAPHER` 之后,二者互斥。
🟢 **默认车辆信息 `defaults.bus` 补上团车**:先列团车,再列逐户派车;格式仍是「车型 车牌 司机 姓名 脱敏手机」。
🟢 入参、路径、判权、错误码全部不变;保存仍然不卡下发门。
---
## 一、背景
#8746 给出团通知书加了「下发门缺项」和「默认车辆带司机」,但只覆盖逐户派车。团车(整团派车)的车辆与司机只在车务服务里,通知书读不到:团车团的默认车辆信息是空的,团车「排了车没排司机」也查不出来——车务那边判团车就绪的硬门不含司机,`vehicle_ready=true` 不代表司机已派。
本单让通知书向车务读团车的活跃派车,补进默认车辆信息,并把「车辆未派司机」加进下发门。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 读出团通知书 | GET | `/v3/admin/order/group-batch/:groupBatchId/docs/notice` | 修改 | `releaseBlockers` 新增 `DRIVER` / `DRIVER_UNAVAILABLE`;`releasable` 加「团车都已派司机」;`defaults.bus`(及未保存时正文 `bus`)含团车 |
| 2 | 保存出团通知书 | PUT | `/v3/admin/order/group-batch/:groupBatchId/docs/notice` | 修改 | 响应同上 |
---
## 三、接口详情
**`releasable` 规则**(两个接口相同):团期状态是 待出发 / 出行中 / 待核单 / 核单中 / 已结算 之一,**且**配房、配车、配导游、配摄影四项都已完成,**且**团车的车辆都已派司机,才为 `true`。
**`releaseBlockers[]` 取值**(按下表顺序排列,`releasable=true` 时为 `[]`,从不为 `null`):
| key | name | 何时出现 |
|---|---|---|
| `STAGE` | 团期未到待出发 | 团期状态不在上面五个之内(不变) |
| `HOTEL` | 配房未完成 | 已成团且配房未完成(不变) |
| `VEHICLE` | 配车未完成 | 已成团且配车未完成(不变) |
| `GUIDE` | 配导游未完成 | 已成团且配导游未完成(不变) |
| `PHOTOGRAPHER` | 配摄影未完成 | 已成团且配摄影未完成(不变) |
| `DRIVER` | 车辆未派司机 | 🆕 已成团,且团车有车没派司机(按车务「团期配车总览」口径,已取消的派车不算) |
| `DRIVER_UNAVAILABLE` | 司机信息暂不可用 | 🆕 已成团,且这次没能从车务读到团车信息(车务服务不可用或超时);稍后重读即可 |
- 未成团(招募中、已流团)仍**只列 `STAGE`**,不判资源也不判司机。
- `DRIVER` 与 `VEHICLE` 各判各的:配车已完成的团照样可能缺司机。
- `DRIVER` 与 `DRIVER_UNAVAILABLE` 不会同时出现。
- 只用逐户派车(没有团车)的团不会出现 `DRIVER`:逐户派车的车一定带司机。
- 车务读不到时按「不可下发」处理:这期间所有已成团的团都会带 `DRIVER_UNAVAILABLE`,接口本身照常返回 `code=200`。
**`bus` 默认值**:先列团车,再列逐户派车,每辆车 `车型 车牌 司机 姓名 脱敏手机`。
- 团车的车型是车辆型号名,例 `丰田考斯特 蒙A-K1999 司机 巴特尔 135****5020`。
- 同一辆车(车型 + 车牌相同)在团车和逐户派车里都出现时只列一次,司机去重合并。
- 多日换过司机用 ` / ` 并列;车与车之间用 `、`。
- 团车全程没派司机的车只有车型车牌,例 `丰田埃尔法 蒙A-E5555`。
- 车务读不到时团车部分不出现,逐户派车部分照常。
- 手机号一律脱敏;已保存的正文不会自动刷新,只有 `defaults` 是实时值。
### 1. 读出团通知书 `GET /v3/admin/order/group-batch/:groupBatchId/docs/notice`
**VO**: `GroupBatchNoticeRespVO`(入参只有路径参数)→ `Result<GroupBatchNoticeRespVO>`
#### 使用场景
团期详情「出团通知书」弹窗打开时调用。按 `releasable` 置灰「打印 / 存 PDF」,`releasable=false` 时把 `releaseBlockers[].name` 列给用户看(与 #8746 相同,新取值无需单独处理)。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | 团期 ID | **不变** |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| releasable | Boolean | 🔄 再加「团车都已派司机」(规则见上) |
| releaseBlockers[].key | String | 🔄 新增取值 `DRIVER` / `DRIVER_UNAVAILABLE` |
| releaseBlockers[].name | String | 🔄 新增「车辆未派司机」/「司机信息暂不可用」 |
| bus | String | 🔄 未保存过(`saved=false`)时等于 `defaults.bus`,含团车;已保存时为保存的原文 |
| defaults.bus | String | 🔄 先团车、再逐户派车,格式见上 |
| 其余字段 | — | **不变** |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2106331639531601921/docs/notice HTTP/1.1
Authorization: Bearer <管理员 token>
```
#### 响应示例
示例:待出发、四项资源都已完成、团车两辆车里一辆没派司机,从未保存过(草稿)——取自 TEST 验收读数。
```json
{
"code": 200,
"message": "成功",
"data": {
"title": "冻干粉发短信给 · 出团通知书",
"greeting": "亲爱的团友,欢迎参加本次行程!",
"meetTime": "2026-11-20 08:30",
"meetPlace": "",
"leader": "",
"bus": "丰田考斯特 蒙A-K1999 司机 巴特尔 135****5020、丰田埃尔法 蒙A-E5555",
"contacts": "",
"service": "",
"bring": "",
"saved": false,
"releasable": false,
"releaseBlockers": [
{
"key": "DRIVER",
"name": "车辆未派司机"
}
],
"version": 0,
"updateTime": null,
"defaults": {
"title": "冻干粉发短信给 · 出团通知书",
"greeting": "亲爱的团友,欢迎参加本次行程!",
"meetTime": "2026-11-20 08:30",
"meetPlace": "",
"leader": "",
"bus": "丰田考斯特 蒙A-K1999 司机 巴特尔 135****5020、丰田埃尔法 蒙A-E5555",
"contacts": "",
"service": "",
"bring": ""
}
}
}
```
给那辆车派上司机后:`releasable=true`、`releaseBlockers=[]`,`bus` 变为 `丰田考斯特 蒙A-K1999 司机 巴特尔 135****5020、丰田埃尔法 蒙A-E5555 司机 乌力吉 135****5015`。
#### 空数据 / 降级响应
- 没有团车、也没有逐户派车:`defaults.bus` 为 `""`。
- 车务服务读不到:接口仍 `code=200`;已成团的团 `releaseBlockers` 带 `DRIVER_UNAVAILABLE`、`releasable=false`;`defaults.bus` 只含逐户派车部分。形如:
```json
{
"code": 200,
"data": {
"releasable": false,
"releaseBlockers": [
{
"key": "DRIVER_UNAVAILABLE",
"name": "司机信息暂不可用"
}
]
}
}
```
#### 错误响应
| code | 条件 |
|---|---|
| `589500` | 团期不存在或已删除(不变) |
| `589507` | 当前角色没有 `group-batch:docs` 权限(不变) |
```json
{
"code": 589507,
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
"data": null
}
```
#### 业务边界
- `releasable` 只管「能否打印 / 下发」,不影响读取与保存。
- 同一个团在不同时间读,`releaseBlockers` 可能不同(车务补派司机、或车务恢复可读后会变)。
- `DRIVER_UNAVAILABLE` 是暂时状态,不代表真缺司机;重读即可。
### 2. 保存出团通知书 `PUT /v3/admin/order/group-batch/:groupBatchId/docs/notice`
**VO**: `GroupBatchNoticeSaveReqVO` → `Result<GroupBatchNoticeRespVO>`
#### 使用场景
运营编辑通知书后保存。入参不变;响应与读接口同一个结构,`releasable` / `releaseBlockers` 按上面的新规则计算。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | 团期 ID | **不变** |
| bus | Body | String | ✅ | ≤256 字 | **不变**;默认值含团车后可能更长,车与司机组合很多时需删减后再存 |
| title / greeting / meetTime / meetPlace / leader / contacts / service / bring | Body | String | ✅ | 同 #7532 | **不变** |
| expectedVersion | Body | Integer | ✅ | ≥0 | **不变**,取读接口的 `version` |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| releasable / releaseBlockers | — | 🔄 同读接口 |
| version | Integer | **不变**,保存后的新版本 |
| 其余字段 | — | **不变** |
#### 请求示例
```json
{
"title": "冻干粉发短信给 · 出团通知书",
"greeting": "亲爱的团友,欢迎参加本次行程!",
"meetTime": "2026-11-20 08:30",
"meetPlace": "海拉尔东山国际机场 T1 到达厅 3 号门",
"leader": "",
"bus": "丰田考斯特 蒙A-K1999 司机 巴特尔 135****5020、丰田埃尔法 蒙A-E5555",
"contacts": "",
"service": "",
"bring": "",
"expectedVersion": 4
}
```
#### 响应示例
示例:团车一辆车没派司机时保存——保存成功、版本 4 → 5,响应同样带 `DRIVER`(保存不卡下发门)。
```json
{
"code": 200,
"message": "成功",
"data": {
"bus": "丰田考斯特 蒙A-K1999 司机 巴特尔 135****5020、丰田埃尔法 蒙A-E5555",
"meetPlace": "海拉尔东山国际机场 T1 到达厅 3 号门",
"saved": true,
"releasable": false,
"releaseBlockers": [
{
"key": "DRIVER",
"name": "车辆未派司机"
}
],
"version": 5
}
}
```
#### 空数据 / 降级响应
- 车务服务读不到时保存照常成功,响应缺项为 `DRIVER_UNAVAILABLE`。
#### 错误响应
| code | 条件 |
|---|---|
| `589500` | 团期不存在(不变) |
| `589507` | 当前角色没有 `group-batch:docs` 权限(不变) |
| `589585` | 版本冲突,请重读后再存(不变) |
| `589587` | 正文含证件号形态的数字(不变) |
```json
{
"code": 589585,
"message": "通知书已被他人修改,请刷新后重试",
"data": null
}
```
#### 业务边界
- 保存**不卡**下发门:缺司机、车务读不到都能保存。
- 保存成功仍在团期时间线新增「保存出团通知书(第 N 版)」(#8746 口径不变)。
---
## 四、契约约束与正确调用方式
- 「打印 / 存 PDF」按 `releasable` 置灰,`releasable=false` 时展示 `releaseBlockers[].name`;判断用 `key`,不要用中文名。
- 按 #8746 约定「遇到不认识的 `key` 按 `name` 展示」实现的页面,本单零适配。
- 不要自己根据车辆、司机数据推算能否打印,以 `releasable` 为准。
- 看到 `DRIVER_UNAVAILABLE` 时可以提示「稍后重试」,它不是业务缺项。
---
## 五、数据库行为
- 零表结构变更、零数据迁移。
- 读接口零写入;保存接口行为与 #8746 相同。
- 团车数据在保存前、写事务之外读取。
---
## 六、边界行为
- 团车派车行里车已被删除、取不到车型和车牌的,不进默认车辆信息,但仍参与「是否派了司机」的判断。
- 派过司机但司机档案已删除的,视为已派司机(不报 `DRIVER`),默认车辆信息里不显示该司机。
- 车务返回的手机号本已脱敏,通知书侧再脱敏一次,不会出现明文。
## 六.6、修改前后对比
| 场景 | 改前(#8746) | 改后 |
|---|---|---|
| 团车团默认车辆信息 | `""` | `丰田考斯特 蒙A-K1999 司机 巴特尔 135****5020` |
| 待出发、四项已完成、团车有车没派司机 | `releasable=true`,`[]` | `releasable=false`,`[DRIVER]` |
| 同上,补派司机后 | `releasable=true` | `releasable=true`,`[]` |
| 车务服务不可用 | 无影响(读不到团车) | 已成团的团带 `[DRIVER_UNAVAILABLE]`,`releasable=false` |
| 只用逐户派车的团 | — | 与改前相同 |
## 六.7、影响评估
- **是否破坏向后兼容**:`releaseBlockers` 只是多了两个取值;`releasable` 在「团车缺司机」与「车务读不到」时由 `true` 变 `false`。按 `name` 通用展示的页面无需改动。
- **前端是否必须同步上线**:不需要。
- **回滚**:revert PR #8794 后重新部署 order-v3 与 fleet。
---
## 七、不影响范围
- 两个接口的路径、入参、判权(`group-batch:docs`)、错误码:不变。
- 只用逐户派车的团:读数与改前一致(TEST 6 个团前后逐项比对一致)。
- 已保存的通知书正文:不会被改写。
- 小程序端:无影响。
---
## 八、测试环境已验证
**环境**:TEST(`https://api.test.1814.love`) **验证时间**:2026-10-04 17:04~17:13
**构建身份**:order-v3、fleet 均部署 `dev-v3 @ a281744c8`(本单合并提交);`releaseBlockers` 出现 `DRIVER` / `DRIVER_UNAVAILABLE` 新取值只可能来自新字节,fleet 两实例新内部读口返回 200。
**身份**:自签 token 直打网关,用 TEST 真实账号 ID 配对应角色。
### 8.1 团车团默认车辆(只读,3 个现成团车团)
| 团期 | `defaults.bus`(6 次读一致) | 与车务派车推算 |
|---|---|---|
| T27-5637 | `坦克300 蒙P318A 司机 P3测试司机18 139****0018、别克GL8 C0927T01 司机 测B0927司机甲 199****0001` | 一致 |
| T26-3963 | `丰田考斯特 蒙A-K1999 司机 巴特尔 135****5020` | 一致 |
| T26-0352 | `丰田埃尔法 蒙A-E5555 司机 乌力吉 135****5015` | 一致 |
部署前这三个团的 `defaults.bus` 都是 `""`。
### 8.2 车辆未派司机(自建团期,待出发、四项已完成,团车两辆、一辆没派司机)
| 步骤 | `releasable` | `releaseBlockers` | `defaults.bus` |
|---|---|---|---|
| 一辆没派司机 | `false` | `[DRIVER]` | `丰田考斯特 蒙A-K1999 司机 巴特尔 135****5020、丰田埃尔法 蒙A-E5555` |
| 补派司机后 | `true` | `[]` | `… 丰田埃尔法 蒙A-E5555 司机 乌力吉 135****5015` |
| 撤回司机后保存 | `false` | `[DRIVER]`(PUT 响应) | 版本 4 → 5,时间线新增「保存出团通知书(第 5 版)」 |
每步各读 6 次,读数一致。
### 8.3 车务服务读不到
只对通知书读团车这一条调用临时压 1ms 超时(17:02~17:08,验完还原配置并核对一致):4 个团各读 6 次全部 `code=200`;已成团的团都带 `DRIVER_UNAVAILABLE`(如资源准备中的团为 `[STAGE, HOTEL, DRIVER_UNAVAILABLE]`、核单中的团为 `[HOTEL, DRIVER_UNAVAILABLE]`),`defaults.bus` 不含团车;两个实例各记录 12 条降级告警,无系统异常。
### 8.4 只用逐户派车的团
6 个没有团车的团(资源准备中 4 个、核单中 1 个、已结算 1 个),部署前后 `releasable`、`releaseBlockers`、`bus`、`defaults.bus` 逐项一致,均未出现 `DRIVER`。
### 8.5 判权与日志
| 调用方 | 读 | 存 |
|---|---|---|
| 不带 token | 网关 `401` | 网关 `401` |
| 车务、财务(无 `group-batch:docs`) | `589507` | `589507`,零写入 |
| 管理员、团期管理员 | `200` | `200` |
验收时间窗内 order-v3、fleet 四个实例的日志,按 4 名司机脱敏号的前三后四检索明文手机号,零命中(同窗口内本轮请求的团期号四个实例均有命中,窗口有效)。
### 本地证据
| 项 | 读数 |
|---|---|
| 定向 7 类 | 125/0/0 |
| order-v3 `groupbatch` + `fleet` + `archunit` 包 + 全模块架构测试(有 Docker) | 3799/0/0 |
| fleet `dispatch` 包 + 红线架构测试 | 517/0/0,跳过 2(需显式开启的容器类) |
| 变基到最新 `dev-v3` 后重测(定向 + 流团相关 + 上下文 IT + 全部架构测试) | 315/0/0 |
### 未覆盖
- TEST 上没有「未删除团期 + 未取消订单 + 逐户派车快照」的现成团,逐户派车与团车同车合并只由单测覆盖。
- 团车有车没派司机用临时插入的派车行模拟,车务真实排车流程未走;验完已删除。
---
## 十、相关文档
- Issue `#8767`;PR `#8794`
- 拆单来源:Issue `#8746`(下发门缺项、逐户派车带司机)
- 团车读口:同日 `04_8767` 新增接口(内部)那份
- 接口文档:`docs/group/团期模块接口文档-v2.0.html` GB-ADM-081 / 082
## 关联 / 联系人
### 链接
- **Issue**: [#8767](https://git.1814.love/wx/HL/issues/8767)
- **PR**: [#8794](https://git.1814.love/wx/HL/pulls/8794)
- **Merge commit**: [a281744c8](https://git.1814.love/wx/HL/commit/a281744c8)
### 联系人
- **后端负责人**: @jw
@@ -0,0 +1,224 @@
---
schema: "hl-changelog/v2"
ticket: "8767"
title: "车务新增内部接口「按团期列出团车活跃派车行」(order-v3 出团通知书调用):车型车牌、司机与脱敏手机,排除已取消"
consumer: "internal"
author: "jw(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "新增 GET /internal/fleet/dispatch/group-batch/:groupBatchId/vehicles,供 order-v3 出团通知书拼团车车辆、判「车辆未派司机」。内部接口不经网关(公网网关返回 code 403「接口不可访问」),直连 fleet 须带 X-Internal-Token,缺失返回 HTTP 403;前端无需对接。司机手机在 fleet 侧脱敏后才出域。已合并 dev-v3(PR #8794,merge commit a281744c8)并部署 TEST,两实例直连实测。管理后台侧变化见同日 04_8767 修改接口那份。"
updated_at: "2026-10-04"
base: "dev-v3"
---
# fleet: 新增内部接口「按团期列出团车活跃派车行」
> **服务**: hl-fleet-service(端口 8087 / 8187,双实例;内部接口,网关不放行)
> **PR**: `#8794`(已合入 `dev-v3`,合并提交 `a281744c8`)
> **Issue**: #8767
---
## ⚠️ 关键变化
🟢 新增 `GET /internal/fleet/dispatch/group-batch/:groupBatchId/vehicles`:按团期返回团车(整团派车)的活跃派车行,每行一天一辆车,带车型、车牌、司机与脱敏手机;`driverId` 为空表示「只排了车、没排司机」。
🟢 首个调用方:order-v3 出团通知书(`FleetGroupDispatchFeignClient`,contextId `fleetGroupDispatchVehicle`)。
🟢 出参载体是共享 DTO `com.hulalv.common.dto.fleet.GroupDispatchVehicleDTO`(hl-common-core,纯新增类)。
---
## 一、背景
团车的车辆与司机只存在车务的 `fleet_group_dispatch`,order-v3 的逐户派车快照里没有团车户的行,出团通知书既拼不出团车车辆,也判不出「车辆未派司机」。车务原有的团车读口(团期配车总览)要先回调 order-v3 取团期基线,被 order-v3 调用会形成同步环,所以另开一个只读本域表的内部读口。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 按团期列出团车活跃派车行 | GET | `/internal/fleet/dispatch/group-batch/:groupBatchId/vehicles` | 新增 | 内部接口,order-v3 出团通知书调用 |
---
## 三、接口详情
### 1. 按团期列出团车活跃派车行 `GET /internal/fleet/dispatch/group-batch/:groupBatchId/vehicles`
**VO**: `GroupDispatchVehicleDTO`(入参只有路径参数)→ `Result<List<GroupDispatchVehicleDTO>>`
#### 使用场景
order-v3 读、存出团通知书时各调一次:拼默认车辆信息 `bus`,并判断下发门 `DRIVER`。只在写事务之外调用。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | 团期主订单 ID | 单个团期,不涉及批量分片 |
| X-Internal-Token | Header | String | ✅ | 内部令牌 | 由公共 Feign 拦截器自动附带 |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| dispatchId | String | 团期配车行 ID(Long 序列化为字符串) |
| tripDate | String | 服务日,`yyyy-MM-dd` |
| vehicleId | String | 车辆 ID |
| vehiclePlateNo | String | 车牌;车已删除时为 `null` |
| vehicleModelName | String | 车型名称(车辆型号,如「丰田考斯特」);车已删除时为 `null` |
| driverId | String | 司机 ID;为 `null` 表示只排了车、没排司机 |
| driverName | String | 司机姓名;没排司机或司机档案已删除时为 `null` |
| driverPhone | String | **脱敏**司机手机,形如 `135****5020`;没有时为 `null` |
| status | String | 派车状态 `ASSIGNED`(已派车)/ `CONFIRMED`(已确认);`CANCELLED` 不返回 |
#### 请求示例
```http
GET /internal/fleet/dispatch/group-batch/2104962917969608705/vehicles HTTP/1.1
Host: 192.168.100.236:8087
X-Internal-Token: <内部令牌>
```
#### 响应示例
取自 TEST(团期 T27-5637,两辆车三天,节选前两行):
```json
{
"code": 200,
"message": "成功",
"data": [
{
"dispatchId": "2104969512002682882",
"tripDate": "2027-03-18",
"vehicleId": "2089691299660644353",
"vehiclePlateNo": "蒙P318A",
"vehicleModelName": "坦克300",
"driverId": "2089691297869651969",
"driverName": "P3测试司机18",
"driverPhone": "139****0018",
"status": "CONFIRMED"
},
{
"dispatchId": "2104969512002682883",
"tripDate": "2027-03-18",
"vehicleId": "2104029270659780610",
"vehiclePlateNo": "C0927T01",
"vehicleModelName": "别克GL8",
"driverId": "2104030084715433986",
"driverName": "测B0927司机甲",
"driverPhone": "199****0001",
"status": "CONFIRMED"
}
]
}
```
#### 空数据 / 降级响应
- 该团没有团车(只用逐户派车,或派车全部已取消):`data=[]`。
- 调用方(order-v3)侧:fleet 不可用或超时,降级返回错误结果(`584072`「车务司机车辆信息暂时不可用」),**不会**伪装成空数组;通知书据此报 `DRIVER_UNAVAILABLE`。
```json
{
"code": 200,
"message": "成功",
"data": []
}
```
#### 错误响应
| HTTP / code | 条件 |
|---|---|
| HTTP 403 / `403` | 未带或带错 `X-Internal-Token`:`内部接口禁止外部访问` |
| 网关 `403` | 经公网网关访问:`接口不可访问` |
```json
{
"code": 403,
"msg": "内部接口禁止外部访问"
}
```
#### 业务边界
- 只读车务本域表,**不回调 order-v3**,不校验团期是否存在(团期不存在即返回 `[]`)。
- 「已取消不返回」与车务「团期配车总览」、团车就绪判定同一口径:总览上看不到的行这里也不返回。
- 同一辆车多天各一行;去重、拼接由调用方负责。
- 司机手机只有脱敏形态,明文不出车务服务。
---
## 四、契约约束与正确调用方式
- 只能服务间调用:走 Feign(`name=hl-fleet-service`,`path=/internal/fleet/dispatch`),不经网关。
- `driverId` 为空才算「没派司机」;`driverName` 为空但 `driverId` 有值是「派过、司机档案已删」,不算缺司机。
- 失败要保留为失败,不要把降级当成空数组(空数组的含义是「没有团车」)。
---
## 五、数据库行为
- 只读:`fleet_group_dispatch`(存活行),批量取 `fleet_vehicle`、`fleet_driver`;零写入、零表结构变更。
---
## 六、边界行为
- 返回顺序:服务日升序,同日按配车行 ID 升序。
- 车或司机已软删:对应展示字段为 `null`,ID 照给。
---
## 七、不影响范围
- 车务既有接口(团期配车总览、重配、释放、覆盖查询等):不变。
- 其他依赖 hl-common-core 的服务:只多了一个类,行为不变。
- 管理后台、小程序:不直接调用本接口。
---
## 八、测试环境已验证
**环境**:TEST,fleet 两实例直连(8087 / 8187) **验证时间**:2026-10-04 17:01~17:13
**构建身份**:fleet 部署 `dev-v3 @ a281744c8`(本单合并提交),新路径两实例均返回 `code=200`(旧字节无此路径)。
| 用例 | 结果 |
|---|---|
| 团期 T27-5637,带内部令牌 | 两实例均 `code=200`,6 行(2 辆车 × 3 天),手机全为 `ddd****dddd` 形态 |
| 不带内部令牌 | 两实例均 HTTP 403「内部接口禁止外部访问」 |
| 经公网网关(不带 / 带管理员 token) | 均 `code=403`「接口不可访问」 |
| 自建团期插入两行(一行有司机、一行无司机) | 无司机那行 `driverId=null`;补派后带司机,通知书读数与本接口推算逐字一致 |
| 日志 | 验收窗口内两实例按司机脱敏号前三后四检索明文手机号零命中 |
本地:fleet `dispatch` 包 + 红线架构测试 517/0/0(跳过 2,需显式开启的容器类),含本读口 4 条单测与「与总览同一分母」一致性用例。
---
## 十、相关文档
- Issue `#8767`;PR `#8794`
- 调用方变化:同日 `04_8767` 修改接口(管理后台)那份
- 团车 CANCELLED 口径来源:Issue `#8550`
## 关联 / 联系人
### 链接
- **Issue**: [#8767](https://git.1814.love/wx/HL/issues/8767)
- **PR**: [#8794](https://git.1814.love/wx/HL/pulls/8794)
- **Merge commit**: [a281744c8](https://git.1814.love/wx/HL/commit/a281744c8)
### 联系人
- **后端负责人**: @jw
@@ -0,0 +1,331 @@
---
schema: "hl-changelog/v2"
ticket: "8768"
title: "资金账户盘盈盘亏整功能下线(inventory-adjust 接口删除 + INVENTORY 业务类型枚举删除)"
consumer: "admin"
author: "yst"
change_type: "删除接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "837eb9659e1e1dabc187ef76adac5e79e65d71d3"
target_release: "v2.1"
verified_at: "2026-10-04"
updated_at: "2026-10-04"
base: "dev-v3"
status_note: "后端已合 dev-v3(PR #8773 代码 + PR #8781 原型/文档)并部署测试服。盘盈盘亏属无审批直接轧平账户结存=资金挪用通道,整功能删除;账户差异改走对账补记具体业务流水。前端已交付(2026-10-04):删盘盈盘亏弹窗与 API 封装、账户页去操作按钮、业务类型筛选下拉排除 INVENTORY(流水页与日明细抽屉两处),历史残留行 bizTypeName=null 回退本地 map 显「盘盈盘亏」,提交 837eb9659。"
---
# 【删除接口·管理后台】资金账户盘盈盘亏整功能下线 (#8768)
> **PR**: #8773(代码)/ #8781(原型+文档) | **服务**: hl-order-service-v3(finance 域同进程) | **更新时间**: 2026-10-04
## 1. 接口背景
「盘盈盘亏」功能允许在资金账户上**无审批直接提交一笔差额流水轧平账户结存**(盘盈补收 IN / 盘亏补付 OUT),属于资金挪用通道:任何人都可以一句话把账面结存改成任意值,不留业务依据。同时银行账户 / 现金账本的差异本不该用库存盘点语义处理。
因此整功能**下线删除**:账户差异改走「对账找原因 → 补记具体业务流水」路径,不允许直接轧平。删除范围 = 1 个写接口 + 1 个业务类型枚举值 + 1 个错误码 + 2 个请求/响应 VO + 1 个方向枚举。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 盘盈盘亏 | POST | `/admin/finance/fund-accounts/{id}/inventory-adjust` | ⚠️ **删除** | 接口已删,调用一律 404 |
| 2 | 资金明细分页 | GET | `/admin/finance/fund-flows/page` | 🔧 行为变更 | `bizType` 筛选项删除 `INVENTORY`(业务类型枚举删该值),不再会产生新的 INVENTORY 流水 |
| 3 | 错误码 595106 | — | — | ⚠️ **废弃** | `INVENTORY_DIRECTION_INVALID` 随功能删除,码位保留不重发 |
配套删除(前端不可见但供完整性说明):请求 VO `InventoryAdjustReqVO`、响应 VO `InventoryAdjustRespVO`、方向枚举 `InventoryDirectionEnum`(SURPLUS/DEFICIT)、业务类型枚举值 `FundFlowBizTypeEnum.INVENTORY`。
## 3. 接口详情
### 3.1 盘盈盘亏(已删除)
- **方法 + 路径**:`POST /admin/finance/fund-accounts/{id}/inventory-adjust`
- **接口描述(删除前)**:盘盈盘亏(提交即记一笔资金流水轧平该账户结存:盘盈补收 IN / 盘亏补付 OUT)
- **认证**:管理后台 JWT
- **幂等性(删除前)**:有幂等键(账户ID + direction + amount,5 秒窗口);删除后无意义
- **现状**:**接口已删除,任何调用一律返回 404**
### 3.2 资金明细分页(bizType 筛选项变化)
- **方法 + 路径**:`GET /admin/finance/fund-flows/page`
- **接口描述**:资金流水分页查询(账户台账 / 资金明细页数据源)
- **认证**:管理后台 JWT
- **变化点**:query 参数 `bizType` 的业务类型可选值集合中删除 `INVENTORY`(盘盈盘亏)。该筛选为字符串等值匹配,不做枚举合法性校验——传 `INVENTORY` 不报错,仍可捞出库中残留的历史 INVENTORY 流水;但不会再有任何新 INVENTORY 流水产生
## 4. 接口入参
### 4.1 盘盈盘亏请求体(已随接口删除,仅存档备查)
`POST /admin/finance/fund-accounts/{id}/inventory-adjust`
路径参数:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | Long | ✅ | 账户 ID |
请求体字段(`InventoryAdjustReqVO`,已删除):
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| direction | String | ✅ | 方向:SURPLUS 盘盈(补收)/ DEFICIT 盘亏(补付) | 仅 SURPLUS/DEFICIT,否则 595106 |
| amount | BigDecimal | ✅ | 差额金额 | 须 > 0,否则 595102 |
| reason | String | ✅ | 原因(进留痕) | 长度 ≤ 200 |
| voucherUrl | String | ❌ | 佐证影像 | 长度 ≤ 500 |
### 4.2 资金明细分页 query 参数(bizType 说明变化)
`GET /admin/finance/fund-flows/page`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| bizType | String | ❌ | 业务类型筛选。变更后可选值:`PAYMENT` / `PREPAY` / `EXPENSE` / `REIMBURSE` / `RECEIPT` / `STAFF_LOAN` / `COMPANY_LOAN` / `NONBIZ` / `ADVANCE` / `TRANSFER` / `ORDER_PAY` / `ORDER_REFUND` / `OPENING`。**`INVENTORY` 已从可选值中删除** |
其余 query 参数(page / pageSize / fundAccountId / accountType / direction / bizId / flowNo / flowAtStart / flowAtEnd)无变化。
## 5. 出参(响应)
### 5.1 盘盈盘亏响应(已随接口删除,仅存档备查)
`InventoryAdjustRespVO`(已删除):
| 字段 | 类型 | 说明 |
|------|------|------|
| fundFlowId | String(Long 序列化为字符串) | 生成的资金流水 ID |
| balanceAfter | BigDecimal | 调后结存 |
### 5.2 资金明细分页行(历史 INVENTORY 行的展示变化)
`FundFlowRowRespVO` 字段结构**无增删**,仅历史残留数据的取值变化:
| 字段 | 类型 | 说明 | 本次变化 |
|------|------|------|----------|
| id | String | 流水 ID | — |
| flowNo | String | 流水号 | — |
| fundAccountId | String | 账户 ID | — |
| accountName | String | 账户名称 | — |
| accountType | String | 账户类型:BANK / CASH / THIRD_PARTY / INTERNAL_VIRTUAL | — |
| direction | String | 方向:OUT / IN | — |
| amount | BigDecimal | 金额 | — |
| bizType | String | 业务类型码值 | ⚠️ 历史残留行可能为 `INVENTORY`(不会再有新行) |
| bizTypeName | String 或 null | 业务类型中文名 | ⚠️ **历史 `INVENTORY` 行该字段为 `null`**(枚举值已删,解析不到);其余业务类型正常返回中文名 |
| bizId | String 或 null | 关联业务单据 ID | — |
| bizNo | String 或 null | 业务单据号 | — |
| balanceAfter | BigDecimal | 本笔记完后账户结存快照 | — |
| transferGroupId | String 或 null | 互转成对组号(仅 TRANSFER) | — |
| fee | BigDecimal 或 null | 手续费(仅 TRANSFER,挂转出行) | — |
| counterparty | String 或 null | 对方户名(展示层脱敏) | — |
| flowAt | String | 收付时间 | — |
| voucherUrl | String 或 null | 回单凭证影像 | — |
| remark | String 或 null | 备注(互转备注 / 期初调整原因) | — |
流水详情接口 `GET /admin/finance/fund-flows/{flowId}` 的 `bizTypeName` 口径同列表:历史 INVENTORY 行返回 `null`。
## 6. 枚举 / 数据字典
### 6.1 direction(InventoryDirectionEnum,已删除)
**所属字段**:`InventoryAdjustReqVO.direction` | **类型**:`String` | **必填**:✅(删除前)
| 值 | 中文 | 说明 |
|----|------|------|
| `SURPLUS` | 盘盈 | 实存多于账面 → 记 IN 流水增结存(补收) |
| `DEFICIT` | 盘亏 | 实存少于账面 → 记 OUT 流水减结存(补付) |
**枚举已随功能整体删除,无任何现存接口使用。**
### 6.2 bizType(FundFlowBizTypeEnum)
**所属字段**:`FundFlowPageReqVO.bizType`(入参筛选)/ `FundFlowRowRespVO.bizType` + `bizTypeName`(出参) | **类型**:`String`
变更后值表(`INVENTORY` 已删除):
| 值 | 中文 | 说明 |
|----|------|------|
| `PAYMENT` | 应付款付款 | — |
| `PREPAY` | 预付款 | — |
| `EXPENSE` | 费用报销 | — |
| `REIMBURSE` | 报账 | — |
| `RECEIPT` | 收款 | — |
| `STAFF_LOAN` | 员工借款 | — |
| `COMPANY_LOAN` | 公司借款 | — |
| `NONBIZ` | 非业务收支 | — |
| `ADVANCE` | 订单预支 | — |
| `TRANSFER` | 账户互转 | 成对记,不算对外收支 |
| `ORDER_PAY` | 对公收款 | 订单支付自动生成,不经出纳 |
| `ORDER_REFUND` | 订单退款 | 自动生成,不经出纳 |
| `OPENING` | 期初调整 | 仅审计留痕,不计净影响 |
已删除值:`INVENTORY`(盘盈盘亏,#8768 下线)。库中历史残留行 `bizType` 仍为该值,`bizTypeName` 返回 `null`。
## 7. 错误码
| code | 含义 | 触发场景 | 本次变化 |
|------|------|----------|----------|
| 595106 | 盘盈盘亏方向无效(须 SURPLUS/DEFICIT) | 删除前 inventory-adjust 的 direction 非法 | ⚠️ **已废弃**(码位保留不重发,防码值复用歧义;不会再有任何接口返回该码) |
| 595102 | 金额无效(须大于0) | 互转金额 ≤ 0 | 语义不变(不再覆盖盘盈盘亏场景) |
## 8. 示例(3 组:典型 / 边界 / 异常)
### 8.1 典型:调旧 inventory-adjust 路径 → 404
**场景说明**:旧路径已删除,任何调用一律 404(路由不存在)。
**请求**:
```http
POST /admin/finance/fund-accounts/1234567890/inventory-adjust HTTP/1.1
Authorization: Bearer <管理后台JWT>
Content-Type: application/json
{
"direction": "SURPLUS",
"amount": 100.00,
"reason": "月末现金盘点多出100元",
"voucherUrl": "https://oss.example.com/voucher/xxx.jpg"
}
```
**响应**:
```http
HTTP/1.1 404 Not Found
```
(网关 / 服务路由无该端点,返回 404,无业务响应体。)
### 8.2 边界:bizType 传 INVENTORY 筛选 → 仅命中库中历史残留行
**场景说明**:`bizType` 筛选为字符串等值匹配、不做枚举合法性校验。传 `INVENTORY` 不报错、不拒绝,仍按 biz_type 等值过滤,可捞出库中残留的历史盘盈盘亏流水;但不会再产生任何新 INVENTORY 流水。
**请求**:
```http
GET /admin/finance/fund-flows/page?page=1&pageSize=20&bizType=INVENTORY HTTP/1.1
Authorization: Bearer <管理后台JWT>
```
(无请求体)
**响应**:
```json
{
"code": 200,
"data": {
"list": [
{
"id": "9876543210987654321",
"flowNo": "LS20260915000042",
"fundAccountId": "1234567890",
"accountName": "基本户-工行",
"accountType": "BANK",
"direction": "IN",
"amount": 100.00,
"bizType": "INVENTORY",
"bizTypeName": null,
"bizId": null,
"bizNo": null,
"balanceAfter": 50100.00,
"transferGroupId": null,
"fee": null,
"counterparty": null,
"flowAt": "2026-09-15 10:30:00",
"voucherUrl": "https://oss.example.com/voucher/xxx.jpg",
"remark": "月末现金盘点多出100元"
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"message": "ok",
"success": true
}
```
注意示例中 `bizTypeName` 为 `null`(历史行枚举值已删,解析不到中文名)。
### 8.3 业务失败:本功能已删除,无业务错误码场景
**场景说明**:盘盈盘亏功能整体删除后,原 `595106 INVENTORY_DIRECTION_INVALID` 错误码不会再由任何接口返回。对该功能的唯一「失败」表现就是 8.1 的 404。
**请求**:
```http
POST /admin/finance/fund-accounts/1234567890/inventory-adjust HTTP/1.1
Authorization: Bearer <管理后台JWT>
Content-Type: application/json
{
"direction": "INVALID",
"amount": -1,
"reason": ""
}
```
**响应**:
```http
HTTP/1.1 404 Not Found
```
(不再进入参数校验,直接 404。)
## 9. 业务边界
- ❌ **不再适用**:资金账户上的任何「盘点轧平」操作——该入口已彻底移除
- ✅ **替代路径**:账户账面与实际有差异时,走对账定位差异原因,再补记**具体业务类型**的流水(收款 / 费用 / 非业务收支等),不允许无业务依据直接轧平
- ⚠️ **历史数据**:库中存量 INVENTORY 流水保留不删,流水列表 / 详情仍可查到;这些历史行的 `bizTypeName` 为 `null`,流水列表对该字段做判空展示即可
- ⚠️ **筛选兼容**:`bizType=INVENTORY` 作为 query 筛选传入不报错(字符串等值匹配),但属于已废弃值,筛选下拉中应移除该选项
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `FundFlowPageReqVO.bizType` 可选值 | 13 个值(含 `INVENTORY`) | 12 个值(删 `INVENTORY`) |
| `FundFlowRowRespVO.bizTypeName`(历史 INVENTORY 行) | `盘盈盘亏` | `null`(枚举已删,解析不到) |
| `FundFlowDetailRespVO.bizTypeName`(历史 INVENTORY 行) | `盘盈盘亏` | `null`(同上) |
| 错误码 595106 | 有效(direction 非法时返回) | 废弃,不再返回(码位保留不重发) |
### 10.2 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| `POST /admin/finance/fund-accounts/{id}/inventory-adjust` | 正常受理:记一笔 INVENTORY 流水轧平结存,返回 `fundFlowId + balanceAfter` | **接口已删,调用一律 404** |
| 账户差异处理 | 可直接盘盈盘亏轧平 | 只能对账找原因 → 补记具体业务流水 |
| 流水列表 `bizType` 筛选下拉 | 含「盘盈盘亏」选项 | 应移除「盘盈盘亏」选项(传值不报错但仅命中历史残留) |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:**是**。写接口直接删除(404),属破坏性变更
- **前端是否必须同步上线**:**是**。资金账户页「盘盈盘亏」入口(按钮 + 弹窗)与资金明细筛选下拉的「盘盈盘亏」选项已失去对应接口,必须随本变更移除;流水列表 `bizTypeName` 需判空展示(历史 INVENTORY 行为 null)
- **影响已有数据**:库中历史 INVENTORY 流水保留,无需数据迁移
### 11.2 回滚方案
- **回滚方式**:revert PR #8773(后端代码)可恢复接口
- **回滚后清理**:无脏数据(下线期间不可能产生新 INVENTORY 流水,接口已 404)
- 前后端同步:若后端回滚而前端已删入口,需前端同步恢复;建议前后端同批上线 / 回滚
## 12. 注意事项
- **旧路径已删,调用一律 404**:前端如仍残留 inventory-adjust 调用点必须全部移除,否则用户操作直接报 404
- **bizTypeName 判空**:流水列表 / 详情渲染 `bizTypeName` 时,历史 INVENTORY 行该字段为 `null`,请判空展示(如显示空白或「-」),不要按非空字符串处理
- **bizType 筛选不做枚举校验**:后端按字符串等值匹配,传 `INVENTORY` 不报错,仅命中历史残留行;筛选下拉请以后端现行 12 个值为准
- 原盘盈盘亏弹窗里的「佐证影像上传」「方向选择 SURPLUS/DEFICIT」相关逻辑已失去对应接口
- 如前端曾对 595106 错误码做过特判提示,该特判分支已不会触发(该码不会再返回)
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#8768](https://git.1814.love/wx/HL/issues/8768)
- **PR(代码)**: [#8773](https://git.1814.love/wx/HL/pulls/8773)
- **PR(原型+文档)**: [#8781](https://git.1814.love/wx/HL/pulls/8781)
- **Merge commit(代码)**: [669f84bd3b](https://git.1814.love/wx/HL/commit/669f84bd3b5fdd532a1d7817dd69db8141119013)
- **Merge commit(文档)**: [df99bfedfd](https://git.1814.love/wx/HL/commit/df99bfedfd)
### 13.2 联系人
- **后端负责人**: @yst(腰苏图)
@@ -0,0 +1,242 @@
---
schema: "hl-changelog/v2"
ticket: "8783"
title: "团期核单确认门禁订正:blocking 非空服务端硬拦 589568 + 负金额改抛 589753 + 零值金额统一 0.00"
consumer: "admin"
author: "yst"
change_type: "修改接口"
backend_status: "merged"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "aef8ddd60341b789429a9dd9432e8a7f56783190"
target_release: "v2.1"
verified_at: "2026-10-04"
status_note: "订正 04_8714 团期核单重做 changelog 的契约行为描述:confirm 在 blocking 非空时此前仅 panel 透出前端置灰、服务端 200 放行,现服务端硬拦 589568;行内负金额由此前 400 参数校验改为契约错误码 589753;panel/tab 空集合金额统一序列化为 0.00 两位小数。已合 dev-v3 未部署测试服。前端已交付(2026-10-04):确认核单 catch 增 589568 分支重读 panel 刷新 version 与 blockingOrderIds 置灰态(message 自带未定稿清单拦截器透);负金额输入既有 :min=0 无 400 特判、零值展示走 formatPrice 数值化无 \"0\" 等值判断,两处天然零适配,提交 aef8ddd60。"
updated_at: "2026-10-04"
base: "dev-v3"
---
# order-v3 groupbatch:团期核单确认门禁订正(管理后台)—— 订正 04_8714
> ⚠️ **本文是订正 changelog**,修订《04_8714_团期核单重做8类tab明细+公摊拆账-修改接口-管理后台.md》中三处契约行为描述。原文件保留不改动;两处描述不一致时**以本文为准**。接口路径、入参结构、出参结构均无变化,变化只在**服务端行为 / 错误码 / 金额序列化格式**三处。
## 1. 接口背景
#8714 团期核单重做落地后,#8783(PR #8784)补齐三处契约缺口:
1. **确认门禁漏拦**:原契约 panel 透出 `blockingOrderIds` 供前端把确认按钮置灰,但服务端 confirm 端点并未校验——前端绕过置灰直接调 `POST …/settlement/panel/confirm` 时,团内仍有子订单第 1 层核单未定稿也能 200 确认成功。现服务端补上硬校验,blocking 非空即拒绝。
2. **负金额错误码不合契约**:行内金额/单价/数量为负时,此前由 Bean Validation(`@DecimalMin`)拦截返通用 400,不属于核单域错误码体系。现删除字段级 `@DecimalMin`,改在 Service 层 resolveSplits 入口统一校验,按契约抛 **589753**。
3. **零值金额格式不统一**:panel 各分类合计、户视图已摊成本/收入、tab `allocatedTotal` 等空集合金额,此前部分路径序列化为 `"0"`(无小数位),与 tab 两位小数口径不一致。现统一为 `"0.00"`。
## 2. 变更清单
| # | 接口 | 变更点 | 类型 |
|---|------|--------|------|
| 1 | `POST /v3/admin/order/group-batch/{groupBatchId}/settlement/panel/confirm` | **新增服务端门禁**:在团子订单第 1 层核单未全部定稿(blocking 非空)时拒绝确认,抛 **589568**(此前 200 放行,仅 panel 透出 blockingOrderIds 供前端置灰) | 🔧 行为变更 |
| 2 | `PUT …/settlement/{8 个 tab}` / `POST …/settlement/lines` / `POST …/settlement/alloc-preview` | 行内金额/单价/数量为负,由 Bean Validation **400**(通用参数错误)改为契约错误码 **589753**;涉及字段见 §4 | ⚠️ 错误码变化 |
| 3 | `GET …/settlement/panel` / `GET …/settlement/{tab}` | 空集合金额(分类合计 / 户 allocatedCost/allocatedIncome / tab allocatedTotal 等)序列化由可能为 `"0"` 统一为 **`"0.00"`**(两位小数) | 🔧 出参格式统一 |
路径前缀统一为 `/v3/admin/order/group-batch/{groupBatchId}/settlement`,下文用 `…` 代指。接口签名(路径 / 入参字段 / 出参字段)零变化。
## 3. 接口详情
| 项 | 说明 |
|---|---|
| 服务 | hl-order-service-v3(端口 8086) |
| 路径前缀 | `/v3/admin/order/group-batch/{groupBatchId}/settlement` |
| 使用场景 | 管理后台 → 团期详情 → 核单 Tab:确认核单、明细行金额录入 |
| 认证 | 管理后台登录态(JWT);网关既有路由 `/v3/admin/**`,无新增网关配置 |
| 权限码 | `group-batch:audit:allocate`(确认核单);`group-batch:audit:edit`(tab PUT / lines 新增);缺码一律 589507 |
| 幂等性 | 与 04_8714 一致:写口不加 `@Idempotent`,由 main 主行锁 + expectedVersion CAS 兜底(过期 589573) |
| 限流 | 无特殊限流 |
## 4. 接口入参
入参结构与 04_8714 §4 完全一致,此处只列**本次行为变化涉及的字段**(GroupSettleLineSaveReqVO 及其 splits[]):
| 字段 | 类型 | 必填 | 原约束(@DecimalMin,删) | 现约束(Service 校验) |
|------|------|------|---------------------------|------------------------|
| budgetAmount | number | 可空 | ≥0,违例 400 | **为负抛 589753** |
| actualAmount | number | ✅ | ≥0,违例 400 | **为负抛 589753** |
| unitPrice | number | 可空 | ≥0,违例 400 | **为负抛 589753**(住宿/餐食/其他收入) |
| ticketUnitPrice | number | 可空 | ≥0,违例 400 | **为负抛 589753**(门票·游玩) |
| quantity | number | 可空 | ≥0,违例 400 | **为负抛 589753**(餐食/其他收入) |
| dailyPrice | number | 可空 | ≥0,违例 400 | **为负抛 589753**(车辆) |
| perDayFee | number | 可空 | ≥0,违例 400 | **为负抛 589753**(导游/摄影师) |
| splits[].ratio | number | 可空 | ≥0,违例 400 | **为负抛 589753** |
| splits[].amount | number | 可空 | ≥0,违例 400 | **为负抛 589753** |
`POST …/settlement/panel/confirm` 入参不变:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| expectedVersion | int | ✅ | 乐观锁版本(GET panel/tab 返回的 `version` 原样回传) |
## 5. 出参字段
出参结构与 04_8714 §5 完全一致。**唯一变化是零值金额的序列化格式**:
| 字段 | 原来(空集合时可能) | 现在(统一) |
|------|----------------------|--------------|
| panel `categories[].actualAmount` / `allocatedAmount` / `unallocatedAmount` | `"0"` | `"0.00"` |
| panel `households[].allocatedCost` / `allocatedIncome` / `grossProfit` | `"0"` | `"0.00"` |
| tab `allocatedTotal` / `actualTotal` | `"0"` | `"0.00"` |
非零金额不受影响(此前已是两位小数)。金额仍为 JSON 字符串,前端按字符串处理即可;若有对 `"0"` 的等值判断 / 解析分支需放宽为兼容 `"0.00"`(建议统一 `Number(x)` 或字符串比较改为数值比较)。
`GET …/settlement/panel` 中与本门禁相关的字段(不变,重述便于对照):
| 字段 | 类型 | 说明 |
|------|------|------|
| readyToAllocate | boolean | 各户第 1 层核单是否全部定稿;false 时 confirm **必吃 589568** |
| blockingOrderIds | string[] | 第 1 层核单未定稿的子订单 ID;**非空时 confirm 必吃 589568** |
## 6. 枚举 / 数据字典
本次无枚举 / 字典变化。
## 7. 错误码
| 错误码 | 文案({0} 为动态定位串) | 触发场景 | 本次变化 |
|--------|--------------------------|----------|----------|
| 589568 | 整团核单当前状态不允许该操作:{0} | ① CONFIRMED 后任何写操作(原有);② **确认核单时在团子订单第 1 层核单未全部定稿**——{0} 带未定稿订单号清单,「、」分隔,与 panel 的 blockingOrderIds 一一对应 | ⚠️ **新增触发场景②**(此前 confirm 不校验,200 放行) |
| 589753 | 核单拆账入参非法:{0} | 金额/单价/数量为负、未知费用类别等入参层非法;{0} 指明具体字段与值 | ⚠️ **负金额场景由此前 400 改为 589753**(删 @DecimalMin,改 Service 层 resolveSplits 入口校验) |
其余错误码(589507 / 589567 / 589571 / 589572 / 589573 / 589750 / 589751 / 589752 / 589754 / 589755)与 04_8714 §7 一致,无变化。
**前端适配点**:
- 589568 的 toast / 弹窗需覆盖「确认时 blocking 非空」场景——message 里已带未定稿订单号清单,可直接展示;建议同时刷新 panel 更新 blockingOrderIds。
- 负金额入参不再走 400 分支,改走 589753 分支;若前端此前对 400 做通用「参数错误」提示,现在会收到带具体字段定位的 589753 message,体验更好。
## 8. 示例
### 8.1 典型:确认核单时有子订单第 1 层核单未定稿,被 589568 拒绝
请求:
POST /v3/admin/order/group-batch/1934567890123456789/settlement/panel/confirm
Content-Type: application/json
```json
{ "expectedVersion": 5 }
```
响应(业务失败,团内订单 HL20261001001、HL20261001003 第 1 层核单未定稿):
```json
{
"code": 589568,
"message": "整团核单当前状态不允许该操作:以下订单未完成核单:HL20261001001、HL20261001003"
}
```
对照:`GET …/settlement/panel` 此时返回 `"readyToAllocate": false`、`"blockingOrderIds": ["1934567890123450001", "1934567890123450003"]`。**订正前**:同样的请求返回 200 确认成功(服务端不拦);**订正后**:服务端硬拦,前端置灰只是体验层,不再是唯一防线。
### 8.2 边界:明细行金额为负,抛 589753(不再是 400)
请求:
PUT /v3/admin/order/group-batch/1934567890123456789/settlement/hotels
Content-Type: application/json
```json
{
"expectedVersion": 3,
"lines": [
{
"lineId": null,
"allocMode": "SHARED",
"allocRule": "PER_HEAD_AVG",
"actualAmount": -100.00,
"hotelName": "图嘎营地",
"roomCount": 5,
"unitPrice": 380.00
}
]
}
```
响应:
```json
{
"code": 589753,
"message": "核单拆账入参非法:actualAmount 不能为负(-100.00)"
}
```
**订正前**:同样请求由 Bean Validation 拦截,返回 HTTP 400 / 通用参数校验错误(不在核单域错误码体系内);**订正后**:统一走契约错误码 589753,message 带字段名与具体值。splits[].amount / splits[].ratio / dailyPrice / perDayFee 等其余 §4 列出的字段为负时同样抛 589753。
### 8.3 业务失败:expectedVersion 过期(589573,既有行为不变,供对照)
请求:
POST /v3/admin/order/group-batch/1934567890123456789/settlement/panel/confirm
```json
{ "expectedVersion": 3 }
```
(库中当前 version 已为 5)
响应:
```json
{
"code": 589573,
"message": "整团核单数据已被他人修改(当前版本 5,提交版本 3),请刷新后重试"
}
```
处理方式不变:重新 GET panel 读回全量(含新 version)再重试,不能本地 version+1。
## 9. 业务边界
**适用**:
- 团期核单确认(panel/confirm)的服务端兜底校验——所有在团子订单第 1 层核单定稿后才允许确认。
- 明细行金额录入的合法性校验——全部金额/单价/数量/比例字段不允许为负。
**不适用**:
- 逐子订单第 1 层核单本身的确认流程(那是子订单核单域的事,本门禁只消费其结果)。
- 确认后修改:CONFIRMED 不可逆(与 04_8714 一致)。
**特殊边界**:
- **置灰 ≠ 防线**:panel `blockingOrderIds` / `readyToAllocate` 仍正常透出,前端应继续据此置灰确认按钮提升体验;但服务端现在会真拦,前端**必须**处理 589568 回执(toast message + 刷新 panel),不能假设按钮置灰后用户永远触发不到。
- **零值金额判断**:前端若有 `amount === "0"` 之类的字符串等值判断,需改为数值比较或兼容 `"0.00"`。
## 10. 修改前后对比
### 行为级对比
| 行为 | 原来(04_8714 描述 / 订正前实现) | 现在(#8784 订正后) |
|------|-----------------------------------|----------------------|
| confirm 时 blocking 非空 | 服务端**不校验**,200 放行确认成功;blockingOrderIds 只在 panel 透出供前端置灰 | 服务端**硬拦**,抛 589568,message 带未定稿订单号清单(「、」分隔) |
| 行内金额/单价/数量为负 | Bean Validation `@DecimalMin` 拦截,HTTP **400** 通用参数错误 | Service 层 resolveSplits 入口校验,契约错误码 **589753**,message 带字段定位 |
| 空集合金额序列化 | 部分路径输出 `"0"`(无小数位),与 tab 两位小数口径不一致 | 统一 `"0.00"`(两位小数) |
### 字段级对比
无字段增删 / 改名 / 类型变化;仅 §4 列出字段的**校验位置与错误码**变化(@DecimalMin 注解删除,改 Service 校验)。
## 11. 影响评估 / 回滚
- **破坏兼容**:轻微。接口签名零变化;行为变化三处——① confirm 新增拒绝场景(原先能确认成功的请求现在被拒,属安全收紧,前端需新增 589568 处理分支);② 负金额错误码 400 → 589753(前端若按 400 特判需改判 589753);③ 零值金额 `"0"` → `"0.00"`(前端若有字符串等值判断需放宽)。
- **前端必须同步上线**:否(后端已合 dev-v3 未部署测试服;前端适配可在联调窗口内完成)。前端原有「blocking 置灰」逻辑**保留**,只是不再唯一依赖它。
- **workaround 清理点**:若前端此前因服务端不拦而自行做了 confirm 前的 blocking 预校验弹窗,可保留(体验层);若有对负金额 400 的特判文案,改挂 589753。
- **回滚方案**:后端回滚 = revert PR #8784 即可(纯代码改动,无 DDL / 无数据迁移)。
## 12. 注意事项
1. **本文优先于 04_8714**:两份 changelog 对 confirm 门禁 / 负金额错误码 / 零值金额格式的描述不一致时,以本文为准。04_8714 的其余内容(接口清单 / 出入参结构 / 枚举字典 / 拆账规则 / CAS 语义)继续有效。
2. **589568 message 可直接展示**:未定稿订单号清单已内嵌 message(「、」分隔),前端无需自行查 blockingOrderIds 拼装;但建议同时刷新 panel 让置灰状态与服务端一致。
3. **负金额提示走 589753**:不要再期待 400;message 带字段名与非法值,可直接 toast。
4. **金额仍按字符串处理**:`"0.00"` 统一后前端解析逻辑应天然兼容,只需清理对 `"0"` 的硬编码等值判断。
## 13. 关联 / 联系人
- Issue:https://git.1814.love/wx/HL/issues/8783
- PR:https://git.1814.love/wx/HL/pulls/8784
- Commit(squash merge):https://git.1814.love/wx/HL/commit/d414d7d00705
- 被订正的原 changelog:`changelogs-v2/2026-10/04_8714_团期核单重做8类tab明细+公摊拆账-修改接口-管理后台.md`(同仓同目录)
- 后端负责人:@yst

某些文件未显示,因为此 diff 中更改的文件太多 显示更多