比较提交

...
99 次代码提交
作者 SHA1 备注 提交日期
lc 36a85b8669 docs(changelog): 交付供应商暂停与拉黑接口(#6392)
changelog-filename-gate / validate (pull_request) Successful in 2s
2026-08-26 15:45:16 +08:00
lc 43b9c16a36 Merge pull request 'docs(changelog): 交付行政区划 ID 字符串契约(#6409)' (#75) from chore/6409-api-changelog into main
changelog-filename-gate / validate (push) Successful in 2s
2026-08-26 15:30:01 +08:00
lc fd76eea701 docs(changelog): 交付行政区划 ID 字符串契约(#6409)
changelog-filename-gate / validate (pull_request) Successful in 2s
2026-08-26 15:29:26 +08:00
Mimingguang 49f67134fd chore(changelog): #6406 管理端 verified (ref=080ea8df)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-26 15:24:50 +08:00
API Changelog Bot 314c228654 feat: 开票申请按税号自动回填企业工商信息(#6406 / PR #6415)
changelog-filename-gate / validate (push) Successful in 2s
新增 GET /v3/admin/invoice/company-info?taxNo=,按税号返回元典工商照面 +
本系统历史开票抬头两段数据,后端不合并由前端取舍;已合并 dev-v3 并部署测试服,
网关 API 实测 companyInfo / lastInvoiceTitle 往返一致,非法税号返回 581526。
2026-08-26 15:08:20 +08:00
Mimingguang 3e8ea93caf chore(changelog): #6391 管理端 verified (ref=74b3d8bf)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-26 14:53:22 +08:00
Mimingguang 33c0aa2c08 chore(changelog): #6343 管理端 verified (ref=d84c6c25)
changelog-filename-gate / validate (push) Failing after 1s
2026-08-26 14:16:40 +08:00
lc 6831ec3128 docs: 交付供应商法人证件接口(#6391)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-26 11:57:09 +08:00
Mimingguang 42a16b78c6 chore(changelog): #6397 管理端 verified (ref=f2a4200d)
changelog-filename-gate / validate (push) Failing after 1s
2026-08-26 11:49:48 +08:00
lc 257ecb1233 fix(changelog): 对齐 #6343 前端状态元数据
changelog-filename-gate / validate (push) Successful in 2s
2026-08-26 11:32:39 +08:00
lc e9b52319c9 docs(changelog): #6343 供应商类型可为空
changelog-filename-gate / validate (push) Failing after 2s
2026-08-26 11:30:55 +08:00
lc 4060c65261 docs: 交接供应商注册合同接口(#6397)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-26 11:13:31 +08:00
Mimingguang 0fcf0801cf chore(changelog): #6304 管理端 verified (ref=25fb4b27)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-25 17:04:58 +08:00
lc e1b0a667cd docs(api): 发布供应商资质有效期契约
changelog-filename-gate / validate (push) Successful in 2s
changelog-filename-gate / validate (pull_request) Successful in 2s
2026-08-25 16:23:36 +08:00
Mimingguang c3acb6e822 chore(changelog): #6316 供应商详情新增资源信息分页前端已验证(ref=de665364)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-25 14:48:12 +08:00
Mimingguang 8bdd0d9057 chore(changelog): #6312 供应商敏感字段与数值格式校验收紧前端已验证(ref=187b7c3b)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-25 14:27:33 +08:00
lc 1eafac1a59 docs(supplier): publish resource info page API for #6316
changelog-filename-gate / validate (push) Successful in 2s
2026-08-25 13:10:51 +08:00
lc ded9289d98 供应商敏感字段校验收紧交付(#6312)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-25 12:39:57 +08:00
Mimingguang 119c459808 docs(changelog): #6317 供应商详情中文名与资质状态前端 verified(ref=33fe59ac)
changelog-filename-gate / validate (push) Failing after 1s
2026-08-25 12:24:18 +08:00
lc 06fa788882 供应商详情展示字段交付(#6317)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-25 12:12:53 +08:00
Mimingguang a74320185f docs(changelog): #6313 景区供应商全称列前端 verified(ref=3c0edfd7)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-25 11:26:24 +08:00
lc e2b36e1b60 docs(changelog): #6313 景区列表增加供应商全称
changelog-filename-gate / validate (push) Successful in 3s
2026-08-25 11:20:23 +08:00
Mimingguang 3ae7d06158 chore(changelog): #6275 联系人角色字典与默认规则前端 verified(ref=f8a0d72d)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-24 22:49:05 +08:00
lc 0c3b9056e6 文档:下发供应商联系人默认与角色字典契约(#6275)
changelog-filename-gate / validate (push) Successful in 3s
2026-08-24 22:37:31 +08:00
Mimingguang 6b3d84ba7b chore(changelog): #6273 供应商审批编号列前端 verified(ref=a46da334)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-24 21:25:43 +08:00
lc 491adfc28e Merge pull request 'docs(changelog): 记录 #6273 供应商审批编号扩展' (#73) from changelog/6273-supplier-approval-record-display into main
changelog-filename-gate / validate (push) Successful in 1s
Reviewed-on: #73
2026-08-24 21:20:24 +08:00
lc 2cbdcfb13a docs(changelog): 记录 #6273 审批编号扩展
changelog-filename-gate / validate (pull_request) Successful in 3s
2026-08-24 21:18:26 +08:00
Mimingguang 2481ff8f5a chore(changelog): #6266 供应商审批记录展示字段前端 verified(ref=cd7fd4c4)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-24 18:35:27 +08:00
lc c591d15518 docs: 下发供应商审批记录展示契约(#6266)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-24 18:29:11 +08:00
Mimingguang c52cc720e8 chore(changelog): #6265 供应商移除审批/提交备注前端 verified(ref=757c2f52)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-24 18:03:03 +08:00
Mimingguang 944c79f51a chore(changelog): #6258 供应商联系人默认标识前端 verified(ref=36d66367)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-24 17:52:43 +08:00
lc f4b6d94611 docs(changelog): 记录供应商备注字段契约变更
changelog-filename-gate / validate (push) Successful in 2s
2026-08-24 17:46:33 +08:00
lc 6d59634818 docs(changelog): 记录供应商默认联系人字段
changelog-filename-gate / validate (push) Successful in 2s
2026-08-24 17:41:14 +08:00
Mimingguang 2333614961 chore(changelog): #6241/#6257 供应商契约变更前端 verified(ref=a3ef713b)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-24 16:56:11 +08:00
lc 97a52f3d71 docs(changelog): 记录供应商初始账户上限变更
changelog-filename-gate / validate (push) Successful in 2s
2026-08-24 16:30:31 +08:00
lc 97ca1fd45d docs(changelog): 记录供应商资质校验变更
changelog-filename-gate / validate (push) Successful in 2s
Refs wx/HL#6241
2026-08-24 15:35:34 +08:00
Mimingguang 22db05c349 chore(changelog): 标记 #6204 司机档案资产装备字段前端已 verified(ref be171bf7)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-24 09:35:24 +08:00
API Changelog Bot和Claude 89a08e4e50 changelog(mp): AI推荐产品多维筛选开放接口 (#6207)
changelog-filename-gate / validate (push) Successful in 2s
新增 GET /mp/product/ai-recommend 免登录开放接口,供 AI 推荐
调用。支持多维筛选(关键词/目的地/天数/季节/标签/报价/出发
日期/人数/资源类型等),排除私人定制 CUSTOM。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-23 13:48:45 +00:00
API Changelog Bot 6657de1404 docs(changelog): #6204 司机档案新增设备与车上装备字段
changelog-filename-gate / validate (push) Successful in 2s
2026-08-23 10:21:59 +00:00
Mimingguang 4484d924e6 chore(changelog): 标记 #6153/#6195 供应商模块前端已 verified(ref 9e1aba14)
changelog-filename-gate / validate (push) Failing after 3s
2026-08-23 18:20:15 +08:00
API Changelog Bot 4759d2302a 重写 #6210 前端缺陷条目对齐模板:补头信息块/接口清单/契约对照/验证证据/联系人,剔除后端实现细节,frontmatter 回正 not_required(保留前端 verified 状态)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-23 17:21:24 +08:00
lc 8f45e333f4 docs(supplier): align frontend changelog with template
changelog-filename-gate / validate (push) Successful in 3s
2026-08-23 17:14:43 +08:00
Mimingguang 0cedc09d76 chore(changelog): 标记 #6210 客户自订回显前端 verified(ref=97baecb6)
changelog-filename-gate / validate (push) Failing after 1s
2026-08-23 17:01:32 +08:00
lc e6e352e92f docs(supplier): publish frontend integration API pack
changelog-filename-gate / validate (push) Successful in 2s
2026-08-23 16:32:46 +08:00
API Changelog Bot ab46f0d0d5 docs(changelog): #6210 调整订单弹窗客户自订未回显重交丢失——前端缺陷交接(DB+网关实证,后端契约完整)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-23 16:28:16 +08:00
Mimingguang 91fe677e90 chore(changelog): 标记 #6152 异常桶前端 verified(ref=36261d83)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-23 15:58:05 +08:00
lc 1c5badc7b4 docs(changelog): 发布供应商注册与账户接口说明
changelog-filename-gate / validate (push) Successful in 2s
2026-08-23 15:04:51 +08:00
lc 3f10b2478e docs(changelog): 发布供应商资源关系接口说明
changelog-filename-gate / validate (push) Successful in 1s
Refs #6195

Refs #6197

Refs #6200
2026-08-23 14:02:20 +08:00
lc d58a2baca5 docs(changelog): 发布供应商归档失败关闭说明
changelog-filename-gate / validate (push) Successful in 2s
Refs #6174
2026-08-23 13:57:49 +08:00
lc cda1516aee 供应商接口接入网关与权限矩阵(#6191)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-23 10:27:35 +08:00
wx da7f0d888a docs(changelog): #6152 补各接口请求/响应 JSON 示例(网关实测片段,前端可照 Mock)
changelog-filename-gate / validate (push) Successful in 1s
2026-08-22 09:48:55 +00:00
wx df55131c3f docs(changelog): #6152 补正文头部信息块(PR/服务/作者/更新时间,对齐 6117 完整模板)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-22 09:39:55 +00:00
wx 13a48155d0 docs: 车务派单异常桶+处置完成写口+详情已取消行字段+异常留痕 changelog (#6152)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-22 09:10:44 +00:00
Mimingguang 937d06002f docs(changelog): #6155 前端 verified(mmg, ref 8569529c)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-22 16:32:57 +08:00
API Changelog Bot和Claude acf586b8f3 docs(changelog): #6155 补全出行信息身份证国籍/民族自动兜底(非身份证需前端补输入框)
changelog-filename-gate / validate (push) Successful in 2s
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-22 07:35:54 +00:00
yaosutu 3c0a8ce1a7 docs(changelog): 导游/摄影核单确认状态去签名重置-修改接口-管理后台 (#6168)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-22 15:28:41 +08:00
Mimingguang ca44f7dbc2 docs(changelog): #6152 前端 verified(mmg, ref 9871cbb1)
changelog-filename-gate / validate (push) Failing after 1s
2026-08-22 11:17:55 +08:00
API Changelog Bot d64fb9b8f0 docs(changelog): 行级释放占用对已取消全程槽行误拦截-前端缺陷告知(根治 #6152)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-22 10:31:43 +08:00
API Changelog Bot d0b68d526e docs(changelog): #6140 房务操作日志补「订单异常」留痕 + CLOSE 类型徽标中文化
changelog-filename-gate / validate (push) Successful in 2s
2026-08-22 09:40:01 +08:00
Mimingguang d16f03e052 docs(changelog): #6125 前端 verified(mmg, ref 8a2c26af)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-21 17:16:21 +08:00
API Changelog Bot e2bf0b7dde docs(changelog): #6125 房务工作台详情透出终止未用标记+异常原因(前端待渲染 ItineraryPanel)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-21 15:43:15 +08:00
Mimingguang 163ba33516 docs(changelog): #6112 前端 verified(mmg, ref d58ad932)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-21 12:54:30 +08:00
API Changelog Bot 32bc988a8a docs(changelog): #6112 看板详情透出终止行程取消原因(前端待渲染 canceled 行 cancelReason)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-21 12:42:38 +08:00
Mimingguang d2eb4cb817 docs(changelog): #6117 前端 verified(mmg, ref ec7f2a9a)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-21 12:00:11 +08:00
Mimingguang a3eee13f65 docs(changelog): #6111 渲染子项前端 verified(配房行程未用标记+释放)
changelog-filename-gate / validate (push) Failing after 1s
订单详情配房行程页 EXCEPTION 单补三处渲染:异常原因文案、未用房晚「未用」标记、
行级「资源释放」按钮(DELETE assignment 软删+恢复库存);正常单零变化。
frontend_ref=9172700f。
2026-08-21 11:35:43 +08:00
yaosutu b7357dbfb7 docs(changelog): 导游/摄影核单去EXCLUDED改全量替换,确认状态统一settlementConfirmStatus(#6117 / PR #6119)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-21 11:28:19 +08:00
API Changelog Bot e56d1c6fd8 docs(changelog): #6111 前端待渲染-配房行程未用房晚标记+异常原因+行级释放按钮
changelog-filename-gate / validate (push) Successful in 2s
后端 #6116 已上线 TEST(订单 26-9250 网关实测字段已返回)。
本条前端向告知:配房行程页补渲染未用房晚标记/异常原因/行级释放按钮,
字段已具备(exceptionReason(Label)、assignments[].unusedForTerminate/unusedLabel),
行级释放复用 DELETE /v3/admin/order/assignments/{id}。frontend_status=pending, owner=mmg。
2026-08-21 02:33:40 +00:00
Mimingguang 46079e505c docs(changelog): #6114 前端 verified(快照回显+行级释放)
changelog-filename-gate / validate (push) Failing after 3s
车务看板已取消派单行回显 cancelled* 派车快照,行内加行级「释放占用」入口按该行
serviceDates 释放;与 #6107 整单级入口并存。frontend_ref=0d3cdf64。
2026-08-21 09:31:25 +08:00
API Changelog Bot 188426d9d0 docs(changelog): 已取消派单显示派车快照+按未用天行级释放(#6114)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-20 10:17:41 +00:00
API Changelog Bot和Claude 9b09336538 docs(house): 配房行程标记终止未用房晚+异常原因透传(#6111 后端能力)
changelog-filename-gate / validate (push) Successful in 2s
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 10:09:34 +00:00
Mimingguang afef353fc2 docs(changelog): #6111 前端 verified(异常单禁最终确认/清空配房)
changelog-filename-gate / validate (push) Failing after 2s
房务订单详情弹窗 EXCEPTION 单禁用「最终确认/清空配房」(置灰+tooltip),「标记完成」
按勘误保留可点(#4307 异常收尾出口零改动)。frontend_ref=a586343b。
2026-08-20 16:59:24 +08:00
API Changelog Bot 42d0d0eec0 docs(house): 勘误-终止订单仅禁用最终确认/清空配房,标记完成保留(#4307 异常收尾出口)
changelog-filename-gate / validate (push) Successful in 3s
2026-08-20 08:15:47 +00:00
API Changelog Bot 0c62e86ced docs(house): 终止行程订单配房三按钮禁用(#6111 子项,纯前端渲染逻辑告知)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-20 07:59:38 +00:00
Mimingguang 71aa64896e docs(changelog): #6107 前端 verified(释放占用入口已接入)
changelog-filename-gate / validate (push) Failing after 2s
车务看板订单详情抽屉车辆执行段新增整单级「释放车辆/司机占用」按钮,调
clear-cancelled-occupancy 一键清已取消派单脏占用;与逐日软清 #5936 语义不同不混用。
frontend_ref=2ebfddc7。
2026-08-20 15:15:31 +08:00
API Changelog Bot 8f5ecc3b6f docs(fleet): 6107 changelog 补网关实测证据(clear-cancelled 幂等空集 200)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-20 06:41:03 +00:00
API Changelog Bot f6a6aa67ca docs(fleet): 6107 告知 clear-cancelled-occupancy 端点(终止行程后已取消派单释放车/司机占用入口)
changelog-filename-gate / validate (push) Successful in 2s
端点 #5921 已交付部署但当初漏发 changelog,前端不知其存在导致看板详情缺释放按钮。
本次纯告知取值+UI挂载点,后端无代码变更。
2026-08-20 06:34:25 +00:00
Mimingguang 5056ed423a docs(changelog): #6077 二次告知前端 verified(操作人字段已展示)
changelog-filename-gate / validate (push) Failing after 2s
订单详情客户信息卡新增展示最后操作房务/车务(v3Adapter 投影 overview.
customerInfo.houseOperatorName/vehicleOperatorName);住宿每晚 stayDate 前端早已
展示(RoomArrangeCard),零改动。frontend_ref=a684caf2。
2026-08-20 09:50:37 +08:00
API Changelog Bot 996fcf0105 docs(changelog): 二次告知前端-订单详情最后操作人(6077)+住宿每晚stayDate已返回
changelog-filename-gate / validate (push) Successful in 1s
2026-08-20 01:34:13 +00:00
Mimingguang 6e3ed604fb docs(changelog): #6079 前端 verified(releaseLines 双通道已交付)
changelog-filename-gate / validate (push) Failing after 1s
前端在终止行程弹窗提交体新增 releaseLines(与 lineUsages 独立双通道),
车辆未用行只进 releaseLines 驱动 fleet 截断、房未用行双进;空则不带键
走兼容路径。frontend_ref=7ce54680。
2026-08-19 17:02:52 +08:00
API Changelog Bot和Claude cdffec0f00 docs(changelog): #6079 终止行程按勾选未用行释放房车资源(releaseLines 双通道解耦)
changelog-filename-gate / validate (push) Successful in 1s
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 08:42:20 +00:00
API Changelog Bot 020076ec47 Merge branch 'main' of https://git.1814.love:8443/wx/hl-api-changelog
changelog-filename-gate / validate (push) Successful in 2s
2026-08-19 07:53:16 +00:00
API Changelog Bot 5dd236bf43 docs(guide): 明确 not_required 条目禁回写 frontend_owner/verified_at(#6077 实例) 2026-08-19 07:52:58 +00:00
API Changelog Bot 5ba2fc6082 docs(changelog): #6077 响应示例 + 修 frontmatter 门禁(not_required 清空 frontend_owner/verified_at) 2026-08-19 07:52:15 +00:00
API Changelog Bot 54f234fd0f docs(changelog): #6077 not_required 清空残留 frontend_owner/verified_at(门禁E_FRONTEND)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-19 07:48:59 +00:00
API Changelog Bot 790fb18ea1 docs(changelog): #6077 补「响应示例」一节(真实网关响应节选)
顶层 4 字段当前实现为 null,以 overview.customerInfo 内同名字段为准;
附已配房未派车实测示例。
2026-08-19 07:48:06 +00:00
Mimingguang c0019c8eba docs(changelogs-v2): #6077 回写 not_required(frontend_owner=mmg)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-19 15:41:19 +08:00
API Changelog Bot ebbd8ad6a3 docs(changelog): #6077 订单详情新增最后操作房务/车务人员 4 个可选字段(PR #6080)
changelog-filename-gate / validate (push) Successful in 2s
GET /v3/admin/order/{id} 响应顶层及 overview.customerInfo 新增
houseOperatorId/houseOperatorName/vehicleOperatorId/vehicleOperatorName,
无操作/无对应需求时为 null,前端无必须动作。
2026-08-19 07:30:14 +00:00
Mimingguang 7e1e6657ec docs(changelogs-v2): #6052/#6057 回写 not_required(frontend_owner=mmg)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-19 09:14:18 +08:00
Mimingguang 012e2e2a9a docs(changelogs-v2): #6045 回写 verified(frontend_ref=e94c0333)
changelog-filename-gate / validate (push) Failing after 2s
2026-08-19 09:07:25 +08:00
API Changelog Bot 9d7a1663e2 chore(6057): 多槽位越需求索引派车业务码 605911/605912 替代 500(已部署实测)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-19 01:33:38 +08:00
API Changelog Bot 2ee06034ef docs(changelog): #6052 新增 + #6022 补 target_release(解门禁)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-18 21:20:01 +08:00
API Changelog Bot 4130f26911 docs(changelog): 整程行改期换版stale详情回拨成待派车(#6052 / PR #6053) 2026-08-18 21:17:50 +08:00
API Changelog Bot 1cf12fc336 docs(changelog): #6045 status_note去重+补关联联系人段 2026-08-18 21:17:46 +08:00
API Changelog Bot 1028ae4da5 docs(changelog): #6022 响应示例去后端类名SnapshotCellVO,改字段名说明(前端视角) 2026-08-18 21:17:46 +08:00
API Changelog Bot 6b26d283c9 docs(changelog): #6045 确认执行页行程短信按槽位独立选择契约交接(前端契约 pending)
changelog-filename-gate / validate (push) Successful in 2s
2026-08-18 18:31:17 +08:00
Mimingguang 7075d4362d docs(changelogs-v2): #6033 回写 verified(frontend_ref=9bd9e23b)
changelog-filename-gate / validate (push) Failing after 1s
2026-08-18 17:06:38 +08:00
Mimingguang ce2e35ea07 docs(changelogs-v2): #6022 回写 verified(frontend_ref=c157cde4) 2026-08-18 17:01:47 +08:00
API Changelog Bot d22d6ea2a7 docs(changelog): 4篇负责人改wx+6014/6016改pending+补关联联系人段
changelog-filename-gate / validate (push) Successful in 1s
2026-08-18 16:58:11 +08:00
API Changelog Bot e1b4f9ebbe docs(changelog): #6014/#6016/#6022/#6033 补「关联/联系人」段(Issue/PR/Commit链接+后端负责人) 2026-08-18 16:55:38 +08:00
共修改 55 个文件,包含 9661 行新增和 9 行删除
@@ -0,0 +1,41 @@
---
schema: "hl-changelog/v2"
ticket: "5888"
title: "修复调整订单加费被核单金融守卫误拦——定制师本人无法提交(584086)"
consumer: "admin"
change_type: "修复缺陷"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: "2026-08-12"
status_note: "后端已修复并部署测试服。定制师本人调整订单产生加费差额不再 584086;同时补上 submit 全链路订单归属校验(跨单操作 581008),行为对前端透明。"
updated_at: "2026-08-12"
base: "dev-v3"
---
# 修复:调整订单加费被核单金融守卫误拦(#5888 / #5891)
> **服务**: hl-order-service-v3
> **PR**: #5889 + #5892(已合并 dev-v3 并部署测试服)
> **日期**: 2026-08-12
> **背景**: 定制师本人操作「调整订单」,产生加费差额(priceDelta>0)即报 `584086 无权修改核单资金数据,仅主管、管理员或财务可操作`,无法提交。减费方向不受影响。
---
## 修复内容(对前端透明,无接口契约变化)
1. **移除误加的金融角色守卫**:`DiscountService.insertActiveSurcharge` 是「调整订单算价差额写入专用」方法,7-21 核单其他收入 PR 给它误加了 `SettlementWriteGuard`(只放行主管/管理员/财务),导致定制师本人被拦。已移除,与减费侧(`insertActiveDiscount`)口径对齐。
2. **补全 submit 全链路订单归属校验**(#5891):`AdjustmentService.submit` 锁定订单后新增 `OrderViewGuard.assertOrderAccessible` —— ADMIN/SUPER_ADMIN 放行;房务角色 581045;其余角色须为本单定制师(`consultantId == adminId`),否则 **581008 无权查看此订单**。
## 行为变化
| 场景 | 旧 | 新 |
|---|---|---|
| 定制师本人调整订单加费 | ❌ 584086 | ✅ 正常提交 |
| 非本单定制师/车务等调整他人订单 | ⚠️ 可提交(越权) | ❌ 581008 拦截 |
| 房务角色提交调整 | ⚠️ 可提交(越权) | ❌ 581045 拦截 |
@@ -0,0 +1,70 @@
---
schema: "hl-changelog/v2"
ticket: "frontend"
title: "确认执行页「车·师傅」多段/已派槽位显示空白:单段分支读 UI 选中态 props 而非执行段数据"
consumer: "admin"
change_type: "前端缺陷"
author: "wx(GIT)"
backend_status: "not_required"
gateway_status: "not_required"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "后端零改动。已用测试服真实 API 核实(订单 26-3698,看板 GET /admin/fleet/board/orders/2086270138171994114):activeAssignments 返回 1 条完整执行段(vehiclePlate=蒙A-H7777 / vehicleModel=丰田汉兰达 / vehicleSeats=7 / driverName=阿拉坦 / driverPhone=135****5019 / startDate=08-17 / endDate=08-19),父层 confirmationAssignments 即取自该数据。根因在前端 Step4Confirm.vue:confirmationSegments.length<=1 时走单段分支,读的是父层 vehicle/driver props(= AssignModal 的 selVehicleObj/selDriverObj,useVehicleDriverPicker 的 UI 当前选中态),用户未做改派选择时为空 → 显示空白。应对单段分支也 fallback 到 confirmationSegments[0](或直接统一走 segments 分支)。"
updated_at: "2026-08-12"
base: "dev-v3"
---
# 确认执行页「车·师傅」多段/已派槽位显示空白(前端)
> **页面**:车务管理 → 派单看板 → 订单派车弹窗 → Step4「确认执行」→「车 · 师傅」区
> **后端**:本条**零改动**。下面结论来自测试服真实 API(订单 26-3698)。
---
## 一、现象
确认执行页(Step4)「车 · 师傅」区显示空白/不全:车辆「· · 座」、师傅「·」,明明该槽位已派 1 车 3 天。
## 二、后端已排除(数据完整)
`GET /admin/fleet/board/orders/2086270138171994114` → `activeAssignments` 返回 **1 条聚合执行段**:
```json
{
"vehiclePlate": "蒙A-H7777",
"vehicleModel": "丰田汉兰达",
"vehicleSeats": 7,
"driverName": "阿拉坦",
"driverPhone": "135****5019",
"serviceDate": null,
"startDate": "2026-08-17",
"endDate": "2026-08-19"
}
```
(单槽位聚合整段,startDate~endDate=3 天。字段完整,非空。)
## 三、根因(前端)
`AssignModal.vue` 把 `confirmationAssignments`(= `activeAssignments`)作为 `:assignments` 传给 `Step4Confirm.vue`,同时传 `:vehicle="selVehicleObj"` `:driver="selDriverObj"`。
`Step4Confirm.vue` 渲染逻辑(`confirmationSegments` computed):
- **多段分支**(`confirmationSegments.length > 1`):遍历 `confirmationSegments` 正确展示。
- **单段分支**(`length <= 1`,当前场景命中):读父层 props `vehicle?.plate / driver?.name` —— 即 `selVehicleObj/selDriverObj`(`useVehicleDriverPicker` 的 **UI 当前选中态**)。用户直接从已派状态进入 Step4 未做改派选择时,这两个对象为 **null** → 显示空白。
即:单段执行时错误依赖了「本次 UI 是否选中过车辆/司机」,而不是回退到服务端已有的执行段数据。
## 四、修复建议
- 单段分支也改为读 `confirmationSegments[0]`(vehiclePlate/vehicleModel/vehicleSeats + driverName/driverPhone),与多段分支同一数据源;
- 或直接去掉单段分支,统一走 `confirmationSegments` 遍历(数据已由父层聚合好)。
## 五、验收
- [ ] 已派 1 车 3 天的槽位进 Step4,「车·师傅」显示 蒙A-H7777 · 丰田汉兰达 · 7座 / 阿拉坦 · 135****5019
- [ ] 未做任何改派选择时也能正确回显(不再依赖 UI 选中态)
- [ ] 多段接续(同一槽位不同天不同车/司机)仍按段展示不受影响
+3 -1
查看文件
@@ -46,10 +46,12 @@ target_release: ""
verified_at: ""
```
- 需要前端修改:`frontend_status: "pending"`
- 需要前端修改:`frontend_status: "pending"`(此时前端侧可回写 `frontend_owner` 认领 + `verified_at` 实测日期)
- 不需要前端修改:`frontend_status: "not_required"`
- 后端不要代替前端填写 `implemented`、`released` 或 `verified`
**`not_required` 条目任何人(含前端)不得回写 `frontend_owner` / `frontend_ref` / `target_release` / `verified_at`**——这 4 个字段是「前端需要动作」的认领/验收标记,`not_required` 语义是「前端零改动、无需认领」,填了就会被发布门禁(E_FRONTEND 残留校验)拦下。前端要表达「已知悉」用评论/口头即可,不动 frontmatter。若前端评估后认为其实需要适配,应把 `frontend_status` 改成 `pending` 再回写 owner,而不是在 `not_required` 上叠字段。(2026-08-19 #6077 实例:mmg 回写 `not_required` 条目的 `frontend_owner=mmg`+`verified_at`,合并后被门禁拦,清空后放行。)
## 2.1 发布门禁(硬规则,2026-08-10 wx 定)
**给前端推送的 changelog,内容必须是测试环境已经存在、可实测到的。**
@@ -0,0 +1,392 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>供应商详情资源信息分页 API 接口规范 · v1.0</title>
<style>
:root {
--ink: #172033;
--muted: #667085;
--line: #dfe5ef;
--soft: #f6f8fb;
--blue: #1677ff;
--blue-soft: #eaf3ff;
--green: #15803d;
--green-soft: #eaf8ef;
--amber: #a15c00;
--amber-soft: #fff7e6;
--red: #b42318;
--code: #101828;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
color: var(--ink);
background: #eef2f7;
font: 14px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
.page {
width: min(1180px, calc(100% - 40px));
margin: 28px auto 64px;
background: #fff;
border: 1px solid var(--line);
border-radius: 16px;
box-shadow: 0 18px 48px rgba(16, 24, 40, .08);
overflow: hidden;
}
header {
padding: 42px 48px 34px;
color: #fff;
background: linear-gradient(135deg, #123d78, #1677ff 68%, #39a0ff);
}
header h1 { margin: 10px 0 8px; font-size: 30px; line-height: 1.3; }
header p { margin: 0; opacity: .9; }
.eyebrow { font-size: 12px; letter-spacing: .12em; text-transform: uppercase; opacity: .78; }
.badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 22px; }
.badge {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 3px 10px;
border: 1px solid rgba(255,255,255,.35);
border-radius: 999px;
background: rgba(255,255,255,.14);
font-size: 12px;
}
main { padding: 12px 48px 54px; }
nav {
position: sticky;
top: 0;
z-index: 2;
display: flex;
gap: 20px;
margin: 0 -48px 24px;
padding: 13px 48px;
overflow-x: auto;
background: rgba(255,255,255,.96);
border-bottom: 1px solid var(--line);
backdrop-filter: blur(8px);
}
nav a { color: #344054; text-decoration: none; white-space: nowrap; font-size: 13px; }
nav a:hover { color: var(--blue); }
section { scroll-margin-top: 62px; padding-top: 22px; }
h2 { margin: 0 0 14px; font-size: 22px; }
h3 { margin: 24px 0 10px; font-size: 16px; }
p { margin: 8px 0; }
code {
padding: 2px 5px;
border-radius: 4px;
background: var(--soft);
color: #175cd3;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: .93em;
}
pre {
margin: 12px 0;
padding: 18px 20px;
overflow: auto;
border-radius: 10px;
background: var(--code);
color: #d1e9ff;
font: 12px/1.65 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
pre code { padding: 0; color: inherit; background: transparent; }
table { width: 100%; border-collapse: collapse; margin: 12px 0 18px; }
th, td { padding: 10px 12px; border: 1px solid var(--line); text-align: left; vertical-align: top; }
th { background: var(--soft); color: #344054; font-weight: 600; }
td:first-child code { white-space: nowrap; }
.endpoint {
display: flex;
align-items: center;
gap: 10px;
margin: 12px 0 16px;
padding: 14px 16px;
border: 1px solid #b9d8ff;
border-radius: 10px;
background: var(--blue-soft);
overflow-x: auto;
}
.method { padding: 4px 9px; border-radius: 6px; color: #fff; background: var(--blue); font-weight: 700; }
.path { font: 600 14px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: nowrap; }
.callout {
margin: 14px 0;
padding: 13px 15px;
border-left: 4px solid var(--blue);
border-radius: 6px;
background: var(--blue-soft);
}
.callout.ok { border-color: var(--green); background: var(--green-soft); }
.callout.warn { border-color: #f59e0b; background: var(--amber-soft); }
.callout strong { display: block; margin-bottom: 2px; }
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 16px 0; }
.card { padding: 15px; border: 1px solid var(--line); border-radius: 10px; background: #fff; }
.card b { display: block; margin-bottom: 4px; color: #344054; }
.card span { color: var(--muted); }
.status-ok { color: var(--green); font-weight: 700; }
.status-pending { color: var(--amber); font-weight: 700; }
ul, ol { padding-left: 22px; }
li + li { margin-top: 5px; }
footer { padding: 24px 48px; border-top: 1px solid var(--line); background: var(--soft); color: var(--muted); }
@media (max-width: 760px) {
.page { width: 100%; margin: 0; border: 0; border-radius: 0; }
header, main, footer { padding-left: 20px; padding-right: 20px; }
nav { margin-left: -20px; margin-right: -20px; padding-left: 20px; padding-right: 20px; }
.grid { grid-template-columns: 1fr; }
table { display: block; overflow-x: auto; white-space: nowrap; }
}
@media print {
@page { size: A4; margin: 14mm; }
body { background: #fff; }
.page { width: 100%; margin: 0; border: 0; box-shadow: none; }
header { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
nav { display: none; }
main { padding: 10px 0 20px; }
footer { padding: 16px 0 0; }
section { break-inside: avoid-page; }
pre { white-space: pre-wrap; word-break: break-word; }
a { color: inherit; text-decoration: none; }
}
</style>
</head>
<body>
<article class="page">
<header>
<div class="eyebrow">HL Supplier API · Admin</div>
<h1>供应商详情资源信息分页 API 接口规范</h1>
<p>v1.0 · Issue #6316 · 2026-08-25</p>
<div class="badges">
<span class="badge">已实现 · 可联调</span>
<span class="badge">后端 deployed</span>
<span class="badge">Gateway verified</span>
<span class="badge">前端 pending</span>
<span class="badge">只读接口</span>
</div>
</header>
<main>
<nav aria-label="文档目录">
<a href="#overview">概览</a>
<a href="#request">请求</a>
<a href="#response">响应</a>
<a href="#fields">字段</a>
<a href="#semantics">业务口径</a>
<a href="#errors">错误码</a>
<a href="#frontend">前端接入</a>
<a href="#verification">验证</a>
<a href="#rollback">撤回</a>
</nav>
<section id="overview">
<h2>1. 接口概览</h2>
<div class="endpoint"><span class="method">GET</span><span class="path">/admin/supplier/items/{supplierId}/resource-info/page</span></div>
<p>用于供应商管理详情页“资源信息”页签,分页展示该供应商当前有效关系对应的资源权威信息。关系来源为本系统 <code>supplier_resource_rel</code>;Resource 提供九类本地资源,Fleet 提供车辆资源。</p>
<div class="grid">
<div class="card"><b>实现状态</b><span class="status-ok">已实现 · 可联调</span></div>
<div class="card"><b>调用方</b><span>管理后台;前端接入待完成</span></div>
<div class="card"><b>数据副作用</b><span>无写库、Redis、MQ、配置或审计副作用</span></div>
</div>
<div class="callout ok"><strong>契约真相</strong>本文以合并提交 <code>4f032cd6f</code> 的 Controller、请求/响应 VO、Service 权限门禁及 TEST Gateway 验收为准。</div>
</section>
<section id="request">
<h2>2. 请求契约</h2>
<h3>2.1 路径与查询参数</h3>
<table>
<thead><tr><th>参数</th><th>位置</th><th>类型</th><th>必填</th><th>约束与默认值</th></tr></thead>
<tbody>
<tr><td><code>supplierId</code></td><td>path</td><td>string</td><td>是</td><td>正整数;Snowflake ID 必须按字符串传递</td></tr>
<tr><td><code>page</code></td><td>query</td><td>integer</td><td>否</td><td>默认 1,最小 1;公共分页兼容 <code>pageNo</code></td></tr>
<tr><td><code>pageSize</code></td><td>query</td><td>integer</td><td>否</td><td>默认 20,范围 1..100</td></tr>
<tr><td><code>resourceModule</code></td><td>query</td><td>string</td><td>否</td><td>为空查询全部;非空按关系冻结模块精确筛选</td></tr>
</tbody>
</table>
<h3>2.2 支持的资源模块</h3>
<table>
<thead><tr><th>编码</th><th>moduleName</th><th>数据归属</th></tr></thead>
<tbody>
<tr><td><code>SCENIC</code></td><td>景区管理</td><td>Resource</td></tr>
<tr><td><code>RESTAURANT</code></td><td>餐厅管理</td><td>Resource</td></tr>
<tr><td><code>SUPPLIES</code></td><td>备品管理</td><td>Resource</td></tr>
<tr><td><code>SUPPLIES_COMBO</code></td><td>组合配品</td><td>Resource</td></tr>
<tr><td><code>ACTIVITY</code></td><td>游玩项目管理</td><td>Resource</td></tr>
<tr><td><code>HOTEL</code></td><td>酒店管理</td><td>Resource</td></tr>
<tr><td><code>SERVICE</code></td><td>服务管理</td><td>Resource</td></tr>
<tr><td><code>COST_ITEM</code></td><td>额外成本</td><td>Resource</td></tr>
<tr><td><code>STAFF</code></td><td>服务人员管理</td><td>Resource</td></tr>
<tr><td><code>VEHICLE</code></td><td>车队管理-车队管理</td><td>Fleet(内部批量聚合)</td></tr>
</tbody>
</table>
<h3>2.3 调用示例</h3>
<pre><code>GET /admin/supplier/items/2091715622923657217/resource-info/page?page=1&amp;pageSize=20&amp;resourceModule=SCENIC
Authorization: Bearer &lt;有效管理端访问令牌&gt;</code></pre>
<div class="callout warn"><strong>权限是双门禁</strong>服务端仅允许 ADMIN、FINANCE、SUPER_ADMIN,并同时要求 <code>supplier:view</code> 与 <code>supplier:resource:view</code>。不能只通过隐藏页签代替服务端授权。</div>
</section>
<section id="response">
<h2>3. 响应结构</h2>
<p>统一返回 <code>Result&lt;PageResult&lt;SupplierResourceInfoRespVO&gt;&gt;</code>。业务失败通常仍是 HTTP 200,调用方必须检查 <code>code</code>、<code>success</code> 与 <code>message</code>。</p>
<pre><code>{
"code": 200,
"message": "success",
"success": true,
"data": {
"records": [
{
"relationId": "2091715622923657218",
"resourceModule": "SCENIC",
"moduleName": "景区管理",
"resourceId": "2091715622923657001",
"resourceName": "示例景区",
"coverUrl": null,
"city": "海拉尔",
"isCharged": true,
"isChargedName": "是",
"settleTypeCode": "CASH",
"settleTypeName": "现付",
"tags": [
{ "tagId": "2084636804090089473", "tagName": "自然风光", "tagColor": "#52C41A" }
],
"seasons": [
{ "seasonCode": "spring", "seasonName": "春" }
],
"statusCode": "ENABLED",
"statusName": "启用",
"enabled": true,
"resourceAvailable": true,
"updateTime": "2026-08-25 10:00:00"
}
],
"total": 1,
"page": 1,
"pageSize": 20
}
}</code></pre>
</section>
<section id="fields">
<h2>4. records[] 字段</h2>
<table>
<thead><tr><th>字段</th><th>类型</th><th>可空</th><th>说明</th></tr></thead>
<tbody>
<tr><td><code>relationId</code></td><td>string</td><td>否</td><td>供应商资源关系 ID</td></tr>
<tr><td><code>resourceModule</code></td><td>string</td><td>否</td><td>关系冻结的资源模块编码</td></tr>
<tr><td><code>moduleName</code></td><td>string</td><td>否</td><td>模块中文名</td></tr>
<tr><td><code>resourceId</code></td><td>string</td><td>否</td><td>资源 ID;资源缺失仍保留关系原值</td></tr>
<tr><td><code>resourceName</code></td><td>string</td><td>是</td><td>当前资源名称</td></tr>
<tr><td><code>coverUrl</code></td><td>string</td><td>是</td><td>当前有效封面 URL</td></tr>
<tr><td><code>city</code></td><td>string</td><td>是</td><td>城市展示值</td></tr>
<tr><td><code>isCharged</code></td><td>boolean</td><td>是</td><td>是否收费;不适用/未知为 null</td></tr>
<tr><td><code>isChargedName</code></td><td>string</td><td>是</td><td>是否收费中文名</td></tr>
<tr><td><code>settleTypeCode</code></td><td>string</td><td>是</td><td>结算方式编码</td></tr>
<tr><td><code>settleTypeName</code></td><td>string</td><td>是</td><td>结算方式中文名</td></tr>
<tr><td><code>tags</code></td><td>array</td><td>否</td><td>标签列表;无数据为 []</td></tr>
<tr><td><code>seasons</code></td><td>array</td><td>否</td><td>标准季节列表;无数据为 []</td></tr>
<tr><td><code>statusCode</code></td><td>string</td><td>是</td><td>资源当前权威状态编码</td></tr>
<tr><td><code>statusName</code></td><td>string</td><td>是</td><td>归一化状态中文名</td></tr>
<tr><td><code>enabled</code></td><td>boolean</td><td>是</td><td>归一化启用标记</td></tr>
<tr><td><code>resourceAvailable</code></td><td>boolean</td><td>否</td><td>false 表示关系保留但资源已删除/缺失</td></tr>
<tr><td><code>updateTime</code></td><td>string</td><td>是</td><td>资源本身更新时间;yyyy-MM-dd HH:mm:ss</td></tr>
</tbody>
</table>
<h3>4.1 子结构</h3>
<table>
<thead><tr><th>数组</th><th>字段</th><th>说明</th></tr></thead>
<tbody>
<tr><td><code>tags[]</code></td><td><code>tagId</code>、<code>tagName</code>、<code>tagColor</code></td><td>标签 ID 沿用各资源模块的字符串表达</td></tr>
<tr><td><code>seasons[]</code></td><td><code>seasonCode</code>、<code>seasonName</code></td><td>标准季节编码与中文名</td></tr>
</tbody>
</table>
</section>
<section id="semantics">
<h2>5. 业务口径</h2>
<ul>
<li>只读取当前有效的供应商资源关系,按关系 <code>update_time DESC, rel_id DESC</code> 稳定排序。</li>
<li>响应中的 <code>updateTime</code> 是资源主数据更新时间,不是关系更新时间。</li>
<li>标量字段不适用、未知或资源缺失时返回 <code>null</code>;<code>tags</code> 与 <code>seasons</code> 永远返回数组。</li>
<li>资源被软删除或不存在时不丢弃关系行:<code>resourceAvailable=false</code>,名称、状态、更新时间等当前资源字段为 null。</li>
<li>十类资源以本系统权威数据为准;展示形式可参考现有资源列表,但不要从参考图硬编码字段值或状态。</li>
<li>供应商处于草稿、审批中、合作中、暂停、黑名单、归档等任意生命周期状态时均可只读查询。</li>
<li>Fleet/字典依赖出现空响应、非成功、重复、缺失、额外或非法数据时返回 395039,不降级为部分成功。</li>
</ul>
<div class="callout"><strong>内部依赖说明</strong>Resource 通过内部 Token 调用 <code>POST /internal/fleet/vehicles/supplier-resource-info/batch</code> 聚合车辆信息。该路径不面向管理端,前端不得调用、转发或持有内部 Token。</div>
</section>
<section id="errors">
<h2>6. 错误码</h2>
<table>
<thead><tr><th>业务码</th><th>场景</th><th>管理端建议</th></tr></thead>
<tbody>
<tr><td><code>401</code></td><td>未认证或登录态失效</td><td>按统一登录续期/退出逻辑处理</td></tr>
<tr><td><code>400</code></td><td>supplierId、page 或 pageSize 等参数不合法</td><td>修正请求,不自动重试</td></tr>
<tr><td><code>395001</code></td><td>供应商不存在</td><td>关闭失效详情或刷新列表</td></tr>
<tr><td><code>395034</code></td><td>resourceModule 不受支持</td><td>仅使用本文十个稳定编码</td></tr>
<tr><td><code>395039</code></td><td>Fleet、字典或资源必要依赖不可用/响应不完整</td><td>提示稍后重试,不展示旧数据冒充成功</td></tr>
</tbody>
</table>
<pre><code>{
"code": 395034,
"message": "不支持的资源模块",
"data": null,
"success": false
}</code></pre>
</section>
<section id="frontend">
<h2>7. 管理端接入清单</h2>
<ol>
<li>在供应商详情“账号信息”页签后新增“资源信息”页签;仅在页签打开时加载数据。</li>
<li>建议列:资源名称(封面 + 名称)、资源模块、城市、是否收费、结算方式、标签、季节、状态、更新时间。</li>
<li>所有 ID 按字符串保存、传参和比较,禁止转换为 JavaScript number。</li>
<li>优先展示服务端中文字段;未知字段显示“—”,不得前端猜测状态或字典名称。</li>
<li><code>resourceAvailable=false</code> 时保留行并明确显示“资源已删除/不可用”。</li>
<li>模块筛选或页容量改变时回到第 1 页;pageSize 最大 100。</li>
<li>按 <code>code/success</code> 判断业务结果;不要仅依据 HTTP 200。</li>
<li>页签为只读展示,不增加绑定、改绑、解绑、启停或删除按钮。</li>
</ol>
<h3>7.1 建议验收场景</h3>
<table>
<thead><tr><th>场景</th><th>预期</th></tr></thead>
<tbody>
<tr><td>有资源关系</td><td>按分页显示,中文字段、数组、字符串 ID 正常</td></tr>
<tr><td>无资源关系</td><td>records=[]、total=0,不显示错误空态</td></tr>
<tr><td>按模块筛选</td><td>返回行的 resourceModule 全部等于筛选值</td></tr>
<tr><td>关系存在但资源缺失</td><td>保留关系行,resourceAvailable=false</td></tr>
<tr><td>无双权限/未登录</td><td>服务端拒绝,页面不泄露数据</td></tr>
<tr><td>依赖暂不可用</td><td>展示 395039 对应提示,不展示不完整成功页</td></tr>
</tbody>
</table>
</section>
<section id="verification">
<h2>8. 实现与验收状态</h2>
<ul>
<li><strong>代码:</strong>PR #6330 已合并 <code>dev-v3</code>,合并提交 <code>4f032cd6f6698607a2f1524437533d595975fc9a</code>。</li>
<li><strong>自动化:</strong>Resource 全量 2043 项零失败;Fleet 可运行全量 3853 项零失败;GatewayRouteAuditTest 3 项零失败;独立审计套件再次通过。</li>
<li><strong>TEST:</strong>任务 <code>cc1fe248</code>、<code>2697d374</code> 精确部署同一合并提交,Resource/Fleet 双实例与 Nacos 各 2 实例健康。</li>
<li><strong>真实 Gateway:</strong>有效管理端会话只读查询 3 个供应商,实际取得 1 条 SCENIC 关系;18 字段、字符串 ID、数组、时间和模块筛选均通过。</li>
<li><strong>负向:</strong>395001、395034、page/pageSize 参数 400 与未认证 401 均实测通过。</li>
<li><strong>车辆样本:</strong>TEST 当时无 VEHICLE 供应商关系;未伪造数据,跨服务路径由双服务真实部署健康与内部契约/批量/失败关闭测试覆盖。</li>
<li><strong>环境恢复:</strong>任务 <code>45e843e5</code>、<code>c05eda6e</code> 已回切包含本次合并的 <code>dev-v3</code>;Resource 8082/8182、Fleet 8087/8187 及 Nacos 各 2 个实例健康,临时部署分支已删除。</li>
</ul>
<div class="callout ok"><strong>数据清理</strong>TEST 验收全部为 GET 只读请求,没有创建或修改供应商、资源、数据库、Redis、MQ 或配置数据。</div>
</section>
<section id="rollback">
<h2>9. 撤回方案</h2>
<ol>
<li>从最新 <code>dev-v3</code> 创建回退分支,执行 <code>git revert -m 1 --no-edit 4f032cd6f6698607a2f1524437533d595975fc9a</code>,经独立 PR 合入。</li>
<li>依次重新构建并滚动部署 Resource、Fleet,分别保持双实例可用。</li>
<li>本次无数据库、Redis、MQ、Nacos 或其他配置变更,无需 DDL、DML、缓存清理、消息补偿或配置恢复,也无不可逆影响。</li>
<li>管理端停止调用新增 GET 和读取本次字段;既有供应商详情、账号和资源管理接口不受影响。</li>
<li>经 Gateway 复测新增路径撤回、既有供应商详情正常,并确认两服务双实例和 Nacos 健康。</li>
</ol>
</section>
</main>
<footer>
关联:Issue #6316 · PR #6330 · 后端负责人 @lc · 本文件为可离线、可打印单页接口规范。
</footer>
</article>
</body>
</html>
@@ -0,0 +1,234 @@
---
schema: "hl-changelog/v2"
ticket: "6207"
title: "AI 推荐产品多维筛选开放接口(免登录,排除私人定制)"
consumer: "mp"
author: "wx(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
verified_at: "2026-08-23"
status_note: "GET /mp/product/ai-recommend 免登录开放接口,AI 推荐用。默认返回 PUBLISHED + CORE/GROUP(排除 CUSTOM 私人定制),支持多维筛选。后端/网关已测试服验证,前端待消费。"
updated_at: "2026-08-23"
base: "dev-v3"
---
# 【新增接口·小程序端】AI 推荐产品多维筛选开放接口(#6207)
> **PR**: #6220 | **服务**: hl-mp-service / hl-product-service-v2 / hl-gateway | **作者**: wx | **更新时间**: 2026-08-23
> 免登录开放接口,供 AI 做产品推荐调用。**除私人定制(CUSTOM)以外的全部已上架(PUBLISHED)产品**,多维筛选,全部条件可选可组合。
---
## 一、背景
AI 做产品推荐需要一个查询接口:能筛选除私人定制以外的全部已上架产品,条件越多越好(目的地、天数、报价范围、团期、包含资源、人数、季节等),**不需要登录**。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | AI推荐产品查询 | GET | `/mp/product/ai-recommend` | 🆕 新增 | 免登录,多维筛选,分页 |
> 网关白名单已放行(`JwtAuthFilter` SKIP_URLS),无 token 可直接调用。
---
## 三、接口详情
### 1. AI推荐产品查询 `GET /mp/product/ai-recommend`
BFF 透传至 product-service `GET /internal/mp/product/ai-query`。pageSize 最大 50。
**数据范围(硬性)**:
- `product.status = PUBLISHED`(已上架)
- `product_type IN (CORE, GROUP)`(**排除 CUSTOM 私人定制**)
#### 入参(全部可选,可任意组合)
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| keyword | Query | String | 否 | ≤100 字符 | 关键词/目的地(匹配产品名/副标题) |
| productType | Query | String | 否 | CORE/GROUP | 产品类型(默认 CORE+GROUP,传单一值可收窄) |
| category | Query | String | 否 | family/honeymoon/photography/experience/driving | 产品分类 |
| lineId | Query | Long | 否 | - | 产品线ID |
| minDays | Query | Integer | 否 | ≥1 | 最短天数 |
| maxDays | Query | Integer | 否 | ≥1 | 最长天数 |
| seasons | Query | String | 否 | spring/summer/autumn/winter,逗号分隔多值 | 季节 |
| tags | Query | String | 否 | 逗号分隔多值 | 产品标签 |
| minPrice | Query | BigDecimal | 否 | - | 最低起步价 |
| maxPrice | Query | BigDecimal | 否 | - | 最高起步价 |
| startDate | Query | String | 否 | yyyy-MM-dd | 出发日期起始 |
| endDate | Query | String | 否 | yyyy-MM-dd | 出发日期结束 |
| partySize | Query | Integer | 否 | ≥1 | 人数(仅与 startDate+endDate 组合时生效,过滤「订不下」的产品) |
| resourceType | Query | String | 否 | SCENIC/ACTIVITY/RESTAURANT/SERVICE/CUSTOM | 包含资源类型 |
| resourceKeyword | Query | String | 否 | - | 包含资源关键词 |
| orderBy | Query | String | 否 | sort/price_asc/price_desc/newest | 排序 |
| page | Query | Integer | 否 | 默认1,≥1 | 页码 |
| pageSize | Query | Integer | 否 | 默认10,1~50 | 每页条数(**最大 50**,>50 返 400) |
> **seasons/tags 传法**:逗号分隔多值,如 `seasons=summer,winter`、`tags=亲子,摄影`。
#### 出参 `Result<PageResult<MpProductAiQueryItemVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| records[].productId | String | 产品ID(雪花,字符串防精度丢失) |
| records[].productNo | String | 产品编号 |
| records[].productType | String | CORE/GROUP |
| records[].name | String | 产品名称 |
| records[].subtitle | String | 副标题 |
| records[].introduction | String | 产品简介 |
| records[].category | String | 产品分类 |
| records[].tripDays / tripNights | Integer | 天数 / 晚数 |
| records[].coverImageUrl | String | 封面图 |
| records[].carouselImages | String[] | 轮播图 |
| records[].tags / seasons | String[] | 标签 / 季节 |
| records[].lineId | String | 产品线ID |
| records[].lineName | String | 产品线名称 |
| records[].startPrice | String | 起步价(字符串) |
| records[].startPriceLabel | String | `¥3980起/人` |
| records[].paymentType | String | FULL / DEPOSIT |
| records[].status | String | PUBLISHED |
| records[].sortOrder | Integer | 排序号 |
| records[].earliestBookingDate | String | 最早可订日期 yyyy-MM-dd |
| records[].availableDateSummary | String | 可订日期摘要(如 `2026-09-11 起`) |
| records[].availabilitySummary | String | 余位摘要(GROUP: `8个班期可报名,余位充足(2026-09-11 ~ 2026-10-30)`;CORE: `9天可订(2026-08-23 ~ 2026-08-31)`) |
| records[].matchedDestinations | String[] | 匹配到的目的地/途经点摘要 |
| total | Integer | 总条数 |
| page / pageSize | Integer | 页码 / 每页条数 |
#### 请求示例
```
GET /mp/product/ai-recommend?productType=GROUP&seasons=summer&minPrice=0&maxPrice=20000&page=1&pageSize=5
```
#### 响应示例(测试服实测)
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"productId": "2056947670512971778",
"productNo": "G260423004",
"productType": "GROUP",
"name": "测试小蒙马-多档-固定金额",
"subtitle": "",
"introduction": "测试产品简介",
"category": "photography",
"tripDays": 5,
"tripNights": 4,
"coverImageUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/...",
"tags": ["小蒙马"],
"seasons": ["summer"],
"lineId": "2001",
"lineName": "测试产品线",
"startPrice": "8800.00",
"startPriceLabel": "¥8800起/人",
"paymentType": "FULL",
"status": "PUBLISHED",
"sortOrder": 0,
"earliestBookingDate": "2026-09-11",
"availableDateSummary": "2026-09-11 起",
"availabilitySummary": "8个班期可报名,余位充足(2026-09-11 ~ 2026-10-30)"
}
],
"total": 5,
"page": 1,
"pageSize": 5
},
"success": true
}
```
#### 空数据响应(无匹配条件)
```json
{
"code": 200,
"message": "成功",
"data": { "records": [], "total": 0, "page": 1, "pageSize": 5 },
"success": true
}
```
#### 错误响应(参数防御)
```json
{
"code": 400,
"message": "每页条数不能大于50",
"data": null,
"success": false
}
```
---
## 四、契约约束与正确调用方式
### 筛选语义(后端为准)
- **所有条件 AND 组合**:同时传多个条件时,返回同时满足全部条件的产品。
- **seasons / tags 多值取 OR**:`seasons=summer,winter` 表示「夏季 **或** 冬季」。
- **报价范围按起步价过滤**:起步价 = GROUP 未来班期最低成人价 或 CORE 价格日历未来最低价,`minPrice/maxPrice` 作用于该值。
- **人数(partySize)过滤语义**:**仅当同时传了出发日期范围(startDate/endDate 至少一个)+ partySize 才生效**——过滤「订不下」的产品:
- GROUP:看日期范围内是否有余位 ≥ partySize 的可报名班期(ENROLLING/NEARLY_FULL)
- CORE:看日期范围内是否有库存(dailyStock - sold)≥ partySize 的日期
- **只传 partySize 不传日期 → 不触发人数过滤**。
- **目的地关键词**:匹配产品名/副标题 **或** 途经点名称 **或** 行程节点名称(任一处命中即算)。
- **日期倒置自动修正**:startDate 晚于 endDate 时后端自动交换,不报错。
### ✅ 正确 / ⚠️ 注意
| 场景 | 行为 |
|------|------|
| ✅ 无条件调用 | 返回全部在售(CORE+GROUP 且 PUBLISHED)分页 |
| ✅ `seasons=summer,winter` | 夏季或冬季的产品 |
| ✅ `minPrice=1000&maxPrice=5000` | 起步价在区间内的产品 |
| ✅ `startDate=2026-09-01&endDate=2026-10-01&partySize=2` | 该日期范围内能订下 2 人的产品 |
| ⚠️ `pageSize=999` | 400 拒绝(上限 50) |
| ⚠️ 只传 `partySize` 不传日期 | 人数条件不生效 |
---
## 五、测试环境已验证(2026-08-23)
```
GET /mp/product/ai-recommend?page=1&pageSize=5 → 200 total=22 ✓
GET /mp/product/ai-recommend?keyword=草原&page=1&pageSize=5 → 200 total=13 ✓
GET /mp/product/ai-recommend?productType=GROUP&page=1&pageSize=3 → 200 total=6 ✓
GET /mp/product/ai-recommend?category=family&page=1&pageSize=5 → 200 total=12 ✓
GET /mp/product/ai-recommend?seasons=summer,winter&page=1&pageSize=5 → 200 total=19 ✓
GET /mp/product/ai-recommend?minPrice=1000&maxPrice=5000&page=1&pageSize=3 → 200 ✓
GET /mp/product/ai-recommend?orderBy=price_asc&page=1&pageSize=5 → 200 ✓(第一条约 2980 便宜档)
GET /mp/product/ai-recommend?productType=GROUP&seasons=summer&minPrice=0&maxPrice=20000 → 200 total=5 ✓
GET /mp/product/ai-recommend?startDate=2026-08-24&endDate=2026-10-22 → 200 total=14 ✓
GET /mp/product/ai-recommend?pageSize=999 → 400 每页条数不能大于50 ✓
GET /mp/product/ai-recommend?category=driving&seasons=winter&keyword=不存在xyz → 200 empty ✓
```
免登录(无 token)经网关调用全部返回 200。
---
## 六、相关历史
- 关联 Issue: [wx/HL#6207](https://git.1814.love:8443/wx/HL/issues/6207)
- 关联 PR: [wx/HL#6220](https://git.1814.love:8443/wx/HL/pulls/6220)
### 联系人
- **后端负责人**: @wx
- **前端负责人**: @mmg
@@ -7,7 +7,7 @@ author: "wx"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
@@ -51,3 +51,10 @@ generated: "2026-08-18T11:35:00+08:00"
- BoardOrderServiceTest **148/0 全绿**(新增 `queryOrders_pureRescheduleResidue_downgradesToUnassignedWithoutInflatedGap`,覆盖验收:卡片回拨 unassigned + staleFinalizedPlan + !finalizedByFleet、缺口 unassignedSlots=0 不虚高、canAssign+ACTION_ASSIGN 不死卡、残留行 ASSIGNED+含 CHANGE/CANCEL 动作可手动清、早筛与记录级口径一致——待派车分面有数、已派车分面空);spotless + ArchTest 门禁绿。
- 独立 CR(PASS-WITH-NITS,无阻断;已补 CANCEL_ASSIGNMENT 断言闭环验收)。
- 网关实测(TEST):改期单 `HL20260818092151954`(3 行 assigned、service_date 8-25/26/27、plan_finalized_requirement_id≠当前 requirementId)→ 待派车分面 **total=1**(`assignmentStatus=unassigned_urgent`、`staleFinalizedPlan=true`)、已派车分面 **total=0**、`unassignedSlots=0`/`assignedSlots=1`(缺口不虚高)。
## 关联 / 联系人
- Issue:https://git.1814.love:8443/wx/HL/issues/6014
- PR:https://git.1814.love:8443/wx/HL/pulls/6017
- Commit(merge):https://git.1814.love:8443/wx/HL/commit/6233b85542
- 后端负责人:wx
@@ -7,7 +7,7 @@ author: "wx"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
@@ -53,3 +53,10 @@ generated: "2026-08-18T12:30:00+08:00"
- 单测:HouseTodoServiceTest 4 用例(已配房带抢单人/无抢单人广播+endDay 兜底/REFUND 复用幂等/未配房软删)、HouseTodoEventListenerTest 4 用例(TERMINATE 透传/非 TERMINATE 忽略/null 忽略/异常降级)、HouseAssignmentServiceTest 3 用例(dayNumber 切分/边界前返 0/null 返 0)、OrderEventPublisherTest 1 用例(双路发布)、OrderServiceTest 断言 endDayNumber 透传。**house 包 922 + ArchTest 54 全绿**。
- 部署:PR #6020 squash 合并 dev-v3(merge commit 1690be35cc),Deploy Panel task 29a0c139 success,SSH 核实 jar 12:20:17 更新 + 双实例 12:20 重启生效。
- 端到端(造出行中订单→terminateTrip→验房侧落异常桶+产 REFUND)待补:测试库当前无现成 TRAVELLING 订单,按需补测。
## 关联 / 联系人
- Issue:https://git.1814.love:8443/wx/HL/issues/6016
- PR:https://git.1814.love:8443/wx/HL/pulls/6020
- Commit(merge):https://git.1814.love:8443/wx/HL/commit/1690be35cc
- 后端负责人:wx
@@ -7,10 +7,10 @@ author: "wx"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
frontend_ref: "c157cde4"
target_release: "2026-08-18"
verified_at: "2026-08-18"
status_note: "后端完成:PR #6029 已合并 dev-v3(bf80a6f2ac)并部署 TEST(hl-fleet-service jar 15:29 更新、双实例重启)。bug1 看板状态派生识别「槽位清空需求」回待派车;bug2 逐日 vehicle_fee_total 按天拆分 + Flyway V20260818_001 回填存量。测试随 PR 自证。"
updated_at: "2026-08-18"
@@ -40,7 +40,7 @@ generated: "2026-08-18T15:40:00+08:00"
## 响应示例(逐日车费,bug2)
确认执行页「逐日车费」逐日单元格(`SnapshotCellVO`)字段——每天一行,关键是 **`protocolPrice`(当天派车价/协议价)现在=该天单日价**,不再是整槽总价平铺:
确认执行页「逐日车费」接口返回里,**逐日单元格数组**的每个元素 = 一天的派车明细(字段名见下,前端按字段名取)。关键是 **`protocolPrice`(当天派车价/协议价)现在=该天单日价**,不再是整槽总价平铺:
```json
{
@@ -82,3 +82,10 @@ generated: "2026-08-18T15:40:00+08:00"
- 单测随 PR 自证(一键清空回待派车 + 已取消 tab 口径零回归;逐日费按天拆分 + Σ 校验 + Flyway 回填)。
- PR #6029 squash 合并 dev-v3(merge commit bf80a6f2ac),Deploy Panel 部署 hl-fleet-service(jar 15:29 更新、双实例重启生效)。
## 关联 / 联系人
- Issue:https://git.1814.love:8443/wx/HL/issues/6022
- PR:https://git.1814.love:8443/wx/HL/pulls/6029
- Commit(merge):https://git.1814.love:8443/wx/HL/commit/bf80a6f2ac
- 后端负责人:wx
@@ -7,9 +7,9 @@ author: "wx"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: ""
frontend_ref: "9bd9e23b"
target_release: ""
verified_at: "2026-08-18"
status_note: "后端完成:PR #6036 已合并 dev-v3(d4288c4941)并部署 TEST(hl-order-service-v3 jar 16:30 更新、双实例重启)。bug1 改走 listActiveHotelAssignmentsByOrderId(INQUIRING∪CONFIRMED)补回显;bug2 新增 hotelGroup.canUrge 显隐契约。2849 测试全绿 + ArchTest 54 绿。网关实测 itinerary 的 assignments 非空 + canUrge=false。"
@@ -87,3 +87,10 @@ generated: "2026-08-18T16:40:00+08:00"
- 单测:OrderDetailServiceTest +1(INQUIRING 候选传入 converter)、OrderDetailConverterTest +1(canUrge:CONFIRMED→false / 未加急→true / 已加急→false);order.core 全量回归 2849 绿 + ArchTest 54 绿(含 HouseModuleBoundaryArchTest)。
- 部署:PR #6036 squash 合并 dev-v3(merge commit d4288c4941),Deploy Panel 部署 hl-order-service-v3(jar 16:30 更新、双实例重启生效)。
- 网关实测(TEST):itinerary 的 `hotelGroup.assignments` 由空变 2 条(呼伦贝尔香格里拉大酒店 + stayDate 8-18/19 + roomCount + 已回配 + finalized=true),`canUrge=false`。
## 关联 / 联系人
- Issue:https://git.1814.love:8443/wx/HL/issues/6033
- PR:https://git.1814.love:8443/wx/HL/pulls/6036
- Commit(merge):https://git.1814.love:8443/wx/HL/commit/d4288c4941
- 后端负责人:wx
@@ -0,0 +1,82 @@
---
schema: "hl-changelog/v2"
ticket: "6045"
title: "确认执行页行程短信按槽位独立选择(groups[].sendItinerarySms)"
consumer: "admin"
author: "wx"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "e94c0333"
target_release: ""
verified_at: "2026-08-18"
status_note: "后端完成:PR #6049 已合并 dev-v3(4f32480f1)并部署 TEST(hl-fleet-service 双实例 18:27 重启、jar 18:27:32 更新)。后端 confirmRequirement 自 #5827 起已按槽位落地(groups[].sendItinerarySms 逐组独立创建/跳过 ITINERARY_SMS 事件),本次 #6045 正式补录 §8.3a 契约 + 测试补强(混合决策逐组事件断言 + 决策缺失 605056 拦截)。网关实测:需求级 confirm 带 null sendItinerarySms → 400「请选择是否向本段司机发送行程短信」(按槽位必填校验生效)。前端需按槽位渲染选择器、全部槽位选定后才可提交。"
updated_at: "2026-08-18"
base: "dev-v3"
generated: "2026-08-18T11:20:00+08:00"
---
# 确认执行页行程短信按槽位独立选择
> 前端契约交接(#6045)。后端能力已具备(#5827 起按槽位),前端需适配 UI 与提交结构。
## 变更接口
**需求级原子确认**:`POST /admin/fleet/assignments/requirements/{requirementId}/confirm`
(整组确认执行页当前已调用;§8.3a 契约详见 API-SPEC-FLEET-V1.5)
## 入参变更(关键字段)
`groups[]` 逐槽位携带 `sendItinerarySms`(布尔,**必填**):
```json
{
"orderId": "1934567890123456789",
"requestId": "fleet-confirm-20260815-001",
"expectedRequirementVersion": 3,
"expectedRequirementSha256": "<Board 返回的 64 位 hex>",
"expectedPlanGeneration": 1024,
"groups": [
{ "assignmentGroupId": "<执行段1 ID>", "sendItinerarySms": true },
{ "assignmentGroupId": "<执行段2 ID>", "sendItinerarySms": false }
]
}
```
## 前端要求
1. 「行程短信」区块按执行段(槽位)逐个渲染「发送短信 / 不发送短信」选择器,**逐槽位独立**提交 `groups[].sendItinerarySms`。
2. 可部分槽位发、部分槽位不发(槽位 1 发、槽位 2 不发可正常提交)。
3. **全部槽位都选定后才允许点「确认订单执行」**;任一槽位未选禁用按钮并提示(后端 605056「执行段集合已变化,请刷新后重试」兜底)。
4. 「不发送短信」文案口径沿用:「仅不发送确认后的行程短信,派单通知短信仍会发送」。
5. 单槽位订单退化为单选,行为与现状一致。
6. 出参 `groups[]` 逐组返回 `sendItinerarySms`(可回显已选状态)。
## 错误码(新增感知)
- 605056:执行段集合已变化或槽位短信决策缺失(刷新后重试,不可静默重发同 requestId 旧结构)
## 范围
- 只影响确认后的行程短信;派单通知短信(ASSIGNED 通知中心)不受影响。
- 单组确认 `POST /admin/fleet/assignments/{assignmentId}/confirm` 入参 `sendItinerarySms`(单布尔)不变,单槽位退化路径继续使用。
## 验证证据
- **PR**: #6049 squash 合并 dev-v3 → 4f32480f1(head 分支已删)
- **部署**: Deploy Panel task 9f3d36e9 success;SSH 核实 hl-fleet-service 双实例 18:27 重启(PID 4005261@8187 / 4006125@8087),jar 时间戳 2026-08-18 18:27:32,日志 Started FleetServiceApplication in 5.547s 无 ERROR
- **网关实测**(api.test.1814.love:9443,admin 登录):
- `GET /admin/fleet/board/orders` → 200(fleet 服务在线)
- `POST /admin/fleet/assignments/requirements/{requirementId}/confirm` 带 `groups[].sendItinerarySms=null` → code 400「请选择是否向本段司机发送行程短信」——按槽位必选校验真实生效
- **测试**: AssignmentServiceTest 509 绿(含 #6045 新增 2 用例:两槽位混合决策逐组事件断言、决策缺失 605056 拦截)+ AssignmentControllerTest 43 绿 + FleetRedLineArchTest 13 绿 + spotless:check 绿
## 关联 / 联系人
- Issue:https://git.1814.love:8443/wx/HL/issues/6045
- PR:https://git.1814.love:8443/wx/HL/pulls/6049
- Commit(merge):https://git.1814.love:8443/wx/HL/commit/4f32480f1b
- 后端负责人:wx
@@ -0,0 +1,89 @@
---
schema: "hl-changelog/v2"
ticket: "6052"
title: "整程行改期换版 stale 详情回拨成待派车(详情 activeAssignments/vehicleSlots 补 stale 覆写)"
consumer: "admin"
author: "wx"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
verified_at: "2026-08-19"
status_note: "后端完成:PR #6053 已合并 dev-v3(a433bfb7fa)并部署 TEST(hl-fleet-service jar 21:07、双实例 21:08 重启)。根因=C:详情组装路径(activeAssignments/vehicleSlots)不经 stale 覆写——#6014 只修了列表卡片,详情未覆盖;本单给详情两组装点补 stale 覆写(逐日/全程同判据)。board 域 238 全绿 + ArchTest 13 + spotless。网关实测复现单详情 activeAssignments/vehicleSlots 回拨 unassigned。"
updated_at: "2026-08-18"
base: "dev-v3"
generated: "2026-08-18T21:15:00+08:00"
---
# 整程行改期换版 stale 详情回拨成待派车(#6052)
## 背景
#6014 修了「改期换版后纯改期残留回拨成待派车」,但只覆盖了**看板列表卡片**(`GET /admin/fleet/board/orders` 的 `assignmentStatus`)。**订单详情接口**(`GET /admin/fleet/board/orders/{orderId}`)的 `activeAssignments` / `vehicleSlots` 是**另一条组装路径,没走 stale 覆写**——整程行(全程槽,service_date=NULL)改期换版后,详情接口仍显示「已派车(assigned)」,与列表卡片/订单侧「待配」分叉。
## 根因
详情组装(`activeAssignments` / `vehicleSlots`)只用落库状态走状态派生,**没有应用「陈旧定稿(stale)覆写」**;stale 覆写此前只存在于列表卡片与早筛。逐日切片行与整程行在详情路径共用同一断点。
## 变更接口
| 接口 | 变更 |
| --- | --- |
| `GET /admin/fleet/board/orders/{orderId}` 的 `activeAssignments[]` | **行为变更**。该需求存在陈旧定稿(`plan_finalized_requirement_id` ≠ 当前需求 ID)且可进配车流程时,`assignmentStatus` 由落库态回拨为「待派车(unassigned)」;`baseAssignmentStatus` 保留落库态(如 assigned),`availableActionCodes` 含 `ASSIGN` |
| `GET /admin/fleet/board/orders/{orderId}` 的 `vehicleSlots[]` | **行为变更**。同上口径,`slotStatus` 回拨 unassigned;`rescheduleResidue` 徽标、`canDelete`(取消/改派入口)保留 |
## 行为口径
- **状态回拨**:详情 `activeAssignments[].assignmentStatus` / `vehicleSlots[].slotStatus` 在「陈旧定稿 + 可进配车流程」时回拨成待派车,与列表卡片、订单侧对齐。
- **落库不改**:回拨只改读侧派生,`baseAssignmentStatus` 保留落库态;旧派车**不自动清除**(残留徽标 `rescheduleResidue`、取消/改派入口 `canDelete` 全保留,由车务手动清理)。
- **缺口不虚高**:列表缺口进度纯改期残留分流(unassigned=0)未动。
- **当代定稿不误伤**:`plan_finalized_requirement_id` = 当前需求 ID 时保持 assigned 不回拨。
- **V2 新槽机制**:#5810 槽位人工制——平移行保持人工现状不自动补槽,车务取消旧行后按新日期重派(同逐日切片机制)。
## 响应示例
`GET /admin/fleet/board/orders/{orderId}` → `data.activeAssignments[]`(修复后,整程行 stale 场景):
```json
{
"assignmentId": null,
"serviceDate": null,
"assignmentStatus": "unassigned",
"baseAssignmentStatus": "assigned",
"rescheduleResidue": true,
"canDelete": true,
"availableActionCodes": ["ASSIGN", "REJECT_REQUIREMENT"]
}
```
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `assignmentStatus` | String | **回拨后状态**:陈旧定稿可进配车流程时 = `unassigned`(待派车) |
| `baseAssignmentStatus` | String | 落库真实状态(如 `assigned`),不受回拨影响 |
| `rescheduleResidue` | Boolean | 是否改期残留旧日期行(true 挂「改期残留/待清理」徽标) |
| `canDelete` | Boolean | 是否可删除/取消(残留行恒 true,车务手动清理入口) |
| `availableActionCodes` | String[] | 可执行动作(回拨成待派车后含 `ASSIGN`) |
`vehicleSlots[]` 同口径:`slotStatus` 回拨 unassigned,`rescheduleResidue` / `canDelete` 保留。
## 前端动作
- 详情页 `activeAssignments` / `vehicleSlots` 直接消费 `assignmentStatus` / `slotStatus`(已是回拨后状态),`baseAssignmentStatus` 可用于显示落库真实态。
- 残留行 `rescheduleResidue=true` 时挂「改期残留」徽标,`canDelete=true` 时保留取消/改派入口。
- 列表卡片(#6014)行为不变。
## 验证证据
- 单测:BoardOrderServiceTest +2(整程行 stale 回拨 unassigned + baseAssignmentStatus=assigned + 车辆信息保留;当代定稿保持 assigned 防误伤);board 域 238 全绿 + ArchTest 13 + spotless。
- 部署:PR #6053 squash 合并 dev-v3(merge commit a433bfb7fa),Deploy Panel task c87ac963 success(jar 21:07、双实例 21:08 重启)。
- 网关实测(TEST):复现单 `2089664548595240962`(改期 V1→V2,整程行 plan_finalized_requirement_id=V1)→ 详情 `activeAssignments[0].assignmentStatus=unassigned`(待派车)、`baseAssignmentStatus=assigned`、`vehicleSlots[0].slotStatus=unassigned`、`rescheduleResidue=true`、`canDelete=true`。
## 关联 / 联系人
- Issue:https://git.1814.love:8443/wx/HL/issues/6052
- PR:https://git.1814.love:8443/wx/HL/pulls/6053
- Commit(merge):https://git.1814.love:8443/wx/HL/commit/a433bfb7fa
- 后端负责人:wx
@@ -0,0 +1,63 @@
---
schema: "hl-changelog/v2"
ticket: "6057"
title: "多槽位越需求索引派车业务码 605911/605912 替代 500(required_vehicle_type 落 NULL 防御)"
consumer: "admin"
author: "wx"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
verified_at: "2026-08-19"
status_note: "后端完成:PR #6064 + #6065 已合并 dev-v3(584f3adc3 / b07855c228)并部署 TEST(hl-fleet-service Deploy task d6cb4687 + 3bed5be8,jar 01:27:55、双实例 01:28 重启)。越界索引由 500(vehicleType is required)收敛为 605911 业务码;快照车型兜底缺失拦成 605912。错误消息自解释,前端全局错误处理可直接展示,无需适配;派车看板正常只发送已声明/已增槽槽位,行为不变。"
updated_at: "2026-08-19"
base: "dev-v3"
generated: "2026-08-19T01:32:00+08:00"
---
# 多槽位越需求索引派车业务码 605911/605912 替代 500(#6057)
## 背景
批量派车(车务最终实派方案)`fleetDecisionFinal` 放行的 `fleetItemIndex` 可能越出当前需求 `fleet[]` 声明的车辆总数(如需求声明 1 辆车却按 index=1 派第 2 辆)。该场景此前 `resolveFleetItemByIndex` 返 null、无占位可消费走拉单组装,`required_vehicle_type` 落 NULL(DAILY_V3 快照契约 required 字段),确认派车阶段快照校验炸 `vehicleType is required` **500**(P1 复现单 R2)。
## 变更接口
| 接口 | 变更 |
| --- | --- |
| `POST /admin/fleet/assignments/batch` 的 `items[]` | **行为变更**。`fleetItemIndex` 越出当前需求声明车辆数且该索引无 addSlot/展开占位可消费时,整批 fail-fast 返回 **605911**,零写入;有占位(经 `POST /admin/fleet/assignments/slots` 增槽或展开生成)的越界索引不受影响,正常消费占位车型快照派车 |
| `POST /admin/fleet/assignments/batch`(防御) | 需求项定位不到且所选车辆型号归一不出规范车型时,落库前拦 **605912**,不再留 NULL 车型脏行到确认派车阶段炸 500 |
## 新增错误码(fleet 605xxx 段)
| 错误码 | 消息 | 触发条件 | 前端处置 |
| --- | --- | --- | --- |
| `605911` | 用车条目索引越界:超出当前需求声明的车辆数 | `fleetItemIndex` ≥ 需求 `fleet[]` 全部 count 之和且该索引无占位 | 引导车务调整需求条目数或先经增槽(addSlot)再派;消息自解释可直接 toast |
| `605912` | 派车快照缺少需求车型,请检查用车需求车型明细 | 需求项定位不到且车辆型号无法归一(罕见,防御性) | 检查用车需求车型明细 |
## 行为口径
- **合法索引不变**:`index < 声明车辆数`(含 count-1 边界)与占位消费(增槽/展开)路径 200 正常,落库 `required_vehicle_type` 非空。
- **整批原子**:越界索引在锁内拉单组装前 fail-fast,任一项失败整批回滚、零写入。
- **对照一致**:需求声明条目数调整到位后同一请求 200(复现单 R2 实测验证)。
- 接口说明(swagger notes)已同步新口径:多派/新增槽位须先经 addSlot 生成占位。
## 前端动作
无必须动作:错误消息自解释,走全局错误提示即可。派车看板正常只发送已声明/已增槽的槽位索引,交互不变。若后续做 605911 专属引导文案,可按上表触发条件提示「调整需求车辆数或先增槽」。
## 验证证据
- 单测(AssignmentServiceTest 513 全绿):越界索引批派 605911 拦截且 `insert` 零写入;`index=count-1` 命中/`index=count` 越界边界;占位消费的新增槽位仍成功;车型无法归一 605912 防御。`mvn -pl hl-fleet-service -am verify` 3838 测试仅剩 1 个 `ReleaseEMixedBinaryHarnessTest` Windows 环境抖动失败(基线 dev-v3 @ b93c7ddcc 同款失败、单跑通过);Testcontainers 集成测试 12 条通过。
- 部署:PR #6064 squash 合并 dev-v3(merge commit `584f3adc3`)+ 文档对齐 PR #6065(`b07855c228`);Deploy Panel task `d6cb4687` + `3bed5be8` success,测试服 jar mtime 2026-08-19 01:27:54(110669169 bytes),双实例 8087/8187 滚动重启健康。
- 网关实测(TEST,admin token):复现单 R2 `HL20260818215924074`(需求声明 suv×1)批量派车 `fleetItemIndex=1` → **605911**(非 500);复现单 R1 `HL20260818215914552` 同场景 → **605911**;同单合法索引 `fleetItemIndex=0` → 200 assigned,落库 `required_vehicle_type='suv'` 非空、越界请求零写入(DB 核对仅 2 行合法行)。
## 关联 / 联系人
- Issue:https://git.1814.love:8443/wx/HL/issues/6057
- PR:https://git.1814.love:8443/wx/HL/pulls/6064 (主修复)+ https://git.1814.love:8443/wx/HL/pulls/6065 (接口说明对齐)
- Commit(merge):https://git.1814.love:8443/wx/HL/commit/584f3adc32e530389a994d92f3a002c312381632
- 后端负责人:wx
@@ -0,0 +1,77 @@
---
schema: "hl-changelog/v2"
ticket: "6077"
title: "订单详情显示最后操作的房务/车务人员"
consumer: "admin"
author: "wx"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "后端完成:PR #6080 已合并 dev-v3(39c489673)并部署 TEST(hl-order-service-v3 双实例 8086/8186,2026-08-19 15:02 重启)。订单详情响应新增最后操作房务/车务人员 ID 与姓名共 4 个可选只读字段,无操作/无对应需求时为 null,前端按需取读即可、不展示无影响。"
updated_at: "2026-08-19"
base: "dev-v3"
generated: "2026-08-19T15:22:40+08:00"
---
# 订单详情显示最后操作的房务/车务人员(#6077)
## 背景
订单详情页需要看到该订单最后一笔配房、派车分别由哪位房务/车务人员操作,便于追溯责任。取 `order_hotel_requirement` / `order_vehicle_requirement` 的 `updated_by`,经 `admin_user` 解析为姓名。
## 变更接口
| 接口 | 变更 |
| --- | --- |
| `GET /v3/admin/order/{id}`(订单详情,9 Tab 聚合) | **响应新增字段**(均可选只读,无操作/无对应需求时为 null,不影响既有任何字段):`houseOperatorId`(Long,顶层,最后操作房务人员 ID)、`vehicleOperatorId`(Long,顶层,最后操作车务人员 ID)、`houseOperatorName`(String,顶层 + `overview.customerInfo` 内均有,最后操作房务人员姓名)、`vehicleOperatorName`(String,顶层 + `overview.customerInfo` 内均有,最后操作车务人员姓名) |
## 响应示例
`GET /v3/admin/order/{id}` 返回(仅列本次新增字段,其余字段省略):
```json
{
"code": 200,
"data": {
"houseOperatorId": null,
"vehicleOperatorId": null,
"houseOperatorName": null,
"vehicleOperatorName": null,
"overview": {
"customerInfo": {
"houseOperatorName": "王骁",
"vehicleOperatorName": null
}
}
}
}
```
说明:该单已配房未派车,故 `overview.customerInfo.houseOperatorName="王骁"` 有值、`vehicleOperatorName=null`;顶层 4 字段当前实现为 null(以 `overview.customerInfo` 内同名字段为准),前端读 `overview.customerInfo.houseOperatorName` / `vehicleOperatorName` 即可。
## 行为口径
- 4 个字段均为**只读、可选**:该订单无对应用车/用房需求或尚无操作时值为 null,接口不报错。
- 纯新增字段,不影响既有任何字段的取值与结构。
- 姓名来源为管理后台用户表,按最后操作人 ID 解析;解析不到时同样为 null。
## 前端动作
无必须动作。需要展示「最后操作房务/车务」时读 `overview.customerInfo.houseOperatorName` / `vehicleOperatorName`(或顶层同名字段)即可,为 null 时前端自行决定隐藏或显示「-」。
## 验证证据
- 单测新增 113 行覆盖(正常有值 / 无需求为 null)。
- 部署:PR #6080 squash 合并 dev-v3(merge commit `39c489673`);测试服 hl-order-service-v3 双实例 8086/8186 于 2026-08-19 15:02 重启。
- 网关实测(TEST,admin token):`GET /v3/admin/order/2089928715696746498` 返 200,`overview.customerInfo.houseOperatorName="王骁"`(该单已配房),`vehicleOperatorName=null`(该单未派车),符合「有操作给姓名 / 无操作为 null」口径。
## 关联 / 联系人
- Issue:https://git.1814.love:8443/wx/HL/issues/6077
- PR:https://git.1814.love:8443/wx/HL/pulls/6080
- 后端负责人:wx
@@ -0,0 +1,78 @@
---
schema: "hl-changelog/v2"
ticket: "6079"
title: "终止行程按勾选「未用」行释放房车资源——金额/释放双通道解耦,替代按日期截断"
consumer: "admin"
author: "wx"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "7ce54680"
target_release: ""
verified_at: "2026-08-19"
status_note: "后端完成:PR #6083 已合并 dev-v3(6c30bc50c9d1)并部署 TEST(hl-order-service-v3 双实例 16:24 滚动重启成功)。测试服端到端实测两单通过(勾未用释放/无释放行兼容)。目标单测 556 + ArchTest 44 + IT 101 全绿。"
updated_at: "2026-08-19"
base: "dev-v3"
generated: "2026-08-19T16:50:00+08:00"
---
# 终止行程按勾选「未用」行释放房车资源(#6079)
## ⚠️ 关键变化
- **以前**:终止行程的房车资源释放按 `endDayNumber` 日期截断——车侧把「出发日+(endDayNumber-1)」及之后的派单整段取消,房侧自动释放该日及之后的未确认候选库存。
- **现在**:释放完全由前端**逐行勾选的「未用」项**驱动。请求新增可选字段 `releaseLines`(与金额用的 `lineUsages` 完全独立、互不影响):
- **车**:`releaseLines` 里 `used=false` 的车行把 fleet 截断边界**提前到最早勾选未用车行的服务日**(fleet 协议上终止日终身唯一、做不到真正逐行,勾选只能把边界提前——请按「尾部连续段」勾选未用天)。不传/空 → 仍按 `endDayNumber` 日期截断(兼容)。
- **房**:`releaseLines` 里 `used=false` 的房行按「哪晚哪间」精确释放未确认(INQUIRING)候选库存;**已确认(CONFIRMED)的房行即使勾了未用也不自动删**,由房务在异常桶手动处置。不传/空 → 不自动释放任何候选(旧按日期整段释放已下线)。
- **退款金额不变**:仍只按 `lineUsages[].used` 权威重算,与 `releaseLines` 无关;车行勾「未用」**不影响退款**(车辆退款按服务日权威判定),只影响资源释放。
## 变更接口
### `POST /v3/admin/order/{id}/terminate` 终止行程(出行中)
请求体新增可选字段(其余字段不变):
| 字段 | 类型 | 必填 | 说明 |
|---|---|:---:|---|
| `releaseLines` | `List<LineUsageItem>` | ❌ | 资源释放行。只取 `used=false` 且 lineKey 前缀为 `VEHICLE_ASSIGNMENT:`/`HOTEL_ASSIGNMENT:` 的行驱动释放;其它来源行忽略。与 `lineUsages` 不做一致性比对。`lineKey` 取值与退款预览 `refundLines[].lineKey` 一致 |
`LineUsageItem` 结构:`{ "lineKey": "VEHICLE_ASSIGNMENT:348303531385688064:2", "used": false }`。
请求示例(勾第 2 天车未用 + 第 2 晚房未住):
```json
{
"cancelReason": "客户高反送医终止",
"endDayNumber": 2,
"lineUsages": [
{"lineKey": "HOTEL_ASSIGNMENT:2089913001904680962:1", "used": true},
{"lineKey": "HOTEL_ASSIGNMENT:2089913001917263874:2", "used": false}
],
"releaseLines": [
{"lineKey": "VEHICLE_ASSIGNMENT:348303531385688064:2", "used": false},
{"lineKey": "HOTEL_ASSIGNMENT:2089913001917263874:2", "used": false}
]
}
```
## 前端动作
- 终止弹窗里「逐行勾选已用/未用」的房车行,**未用行同时放进 `lineUsages`(used=false,驱动退款)和 `releaseLines`(used=false,驱动释放)**;两个数组都要传,职责不同。
- 车辆未用天请按**尾部连续段**勾选(如 3 天行程勾第 2、3 天未用);断档勾法(勾第 2 天不勾第 3 天)会把第 3 天也一并截断释放(fleet 协议限制,terminateDate 只能取最早未用日)。
- 房行勾「未住」的已确认(CONFIRMED)行不会自动释放——房务会在「异常桶」看到该终止需求 + REFUND 待办,手动取消未入住部分。
## 验证证据(测试服实测)
- 单测:目标套件 9 类 556 用例全绿(新增 TerminateReleasePlanParserTest 6 用例 + OrderServiceTest 真提前场景/事件透传/兼容回退 + HouseTodo/HouseAssignment 按行释放与 CONFIRMED 跳过计数);ArchTest 44 绿;IT 101 跑 0 失败。CR Workflow 25 agents 对抗复核确认 2 个 P2 已收口。
- 实测 1(订单 HL20260819110439216,3 天行程当前第 2 天):`endDayNumber=2` + `releaseLines` 勾车 day2/房 day2 未用 → 退款 6504 按 lineUsages 独立重算;fleet outbox `terminateDate=2026-08-19`(=day2 服务日,**边界提前生效**),day2/day3 派单 canceled、day1 保留 assigned;房侧需求转 EXCEPTION + 产 REFUND 待办,勾的 CONFIRMED 房行保留未删(`skippedConfirmed=1`,符合设计)。
- 实测 2(订单 HL20260819110313216,不传 releaseLines):fleet 按 `endDayNumber=2` 日期截断(兼容路径),day2/day3 canceled;房侧不自动释放候选、需求转 EXCEPTION + REFUND 待办。
- 部署:PR #6083 squash 合并 dev-v3(merge 6c30bc50c9d1),Deploy Panel 滚动部署双实例 UP(8186/8086 均 16:24 重启健康)。
## 关联 / 联系人
- Issue:https://git.1814.love:8443/wx/HL/issues/6079
- PR:https://git.1814.love:8443/wx/HL/pulls/6083
- Commit(merge):https://git.1814.love:8443/wx/HL/commit/6c30bc50c9d1
- 后端负责人:wx
@@ -0,0 +1,93 @@
---
schema: "hl-changelog/v2"
ticket: "6077"
title: "订单详情最后操作房务/车务人员 + 住宿每晚日期回显(二次告知)"
consumer: "admin"
author: "wx"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "a684caf2"
target_release: ""
verified_at: "2026-08-20"
status_note: "二次告知:操作人字段(6077)早已部署测试服并网关实测,前端似乎未接;住宿每晚日期 stayDate 也确认已在返回。前端按本文取值即可。"
updated_at: "2026-08-20"
base: "dev-v3"
generated: "2026-08-20T09:00:00+08:00"
---
# 订单详情最后操作人 + 住宿每晚日期回显(二次告知)
## 背景
前端反馈:①订单详情页没看到「最后操作的房务/车务人员」;②住宿安排「第一晚没有具体时间」。
核实结论:**两块数据后端都已在返回**,是前端未接/未展示。本文把取值位置一次性写清。
## 变更接口
| 接口 | 变更 |
| --- | --- |
| `GET /v3/admin/order/{id}`(订单详情) | 无结构变更。`data.overview.customerInfo.houseOperatorName` / `vehicleOperatorName` 已在返回(#6077 交付),本文再次告知取值位置 |
| `GET /v3/admin/order/{id}/itinerary`(行程安排) | 无结构变更。`data.hotelGroup.assignments[].stayDate`(每晚日历日期)已在返回,本文再次告知取值位置 |
## 一、最后操作房务/车务人员(#6077,已部署测试服)
接口:`GET /v3/admin/order/{id}`(订单详情)
读这里(已实测有值):
```json
{
"data": {
"overview": {
"customerInfo": {
"houseOperatorName": "王骁",
"vehicleOperatorName": null
}
}
}
}
```
- `data.overview.customerInfo.houseOperatorName` — 最后操作房务人员姓名(无操作为 null)
- `data.overview.customerInfo.vehicleOperatorName` — 最后操作车务人员姓名(无操作为 null)
- 顶层 `data.houseOperatorName` 等同名字段当前为 null,**以 `overview.customerInfo` 内为准**。
- 为 null 时前端自行决定隐藏或显示「-」。
## 二、住宿安排每晚日期(第一晚具体时间)
接口:`GET /v3/admin/order/{id}/itinerary` → `data.hotelGroup.assignments[]`
每条回配酒店都有 `stayDate`(该晚日历日期)+ `dayNumber`(第几晚),实测:
```json
{
"dayNumber": 1,
"stayDate": "2026-08-19",
"hotelName": "呼伦贝尔香格里拉大酒店",
"roomType": "BIG_BED",
"roomCount": 1
}
```
出发日 2026-08-19 → 第一晚 `stayDate=2026-08-19`、第二晚 `2026-08-20`,已对齐。前端在住宿安排卡片每行渲染 `stayDate` 即可。
注意:只有**已配房**的订单 `assignments` 才有数据;未配房(需求阶段)为空数组属正常。
## 前端动作
1. 操作人:订单详情页读 `data.overview.customerInfo.houseOperatorName` / `vehicleOperatorName` 展示。
2. 每晚日期:住宿安排卡片每行展示 `assignments[].stayDate`。
## 验证证据
- 6077:PR #6080 已合并 dev-v3(merge commit 39c489673),测试服双实例 2026-08-19 15:02 重启;网关实测 `overview.customerInfo.houseOperatorName="王骁"` 有值。
- stayDate:2026-08-20 测试服网关实测订单 2090087196236095490,assignments 两条 stayDate=2026-08-19/2026-08-20 正确。
## 关联 / 联系人
- Issue #6077:https://git.1814.love:8443/wx/HL/issues/6077
- 后端负责人:wx
@@ -0,0 +1,87 @@
---
schema: "hl-changelog/v2"
ticket: "6107"
title: "终止行程后已取消派单「释放车辆/司机占用」入口(一键清除占用端点告知)"
consumer: "admin"
author: "wx"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "2ebfddc7"
target_release: ""
verified_at: "2026-08-20"
status_note: "端点 POST /admin/fleet/assignments/clear-cancelled-occupancy 由 #5921(关联 #5913/#5828)交付并部署 TEST,但当初未单独发 changelog,前端不知其存在,导致「终止行程后已取消派单仍占着车/司机、无释放按钮」。本文补告知取值与 UI 挂载点。后端本次无代码变更。"
updated_at: "2026-08-20"
base: "dev-v3"
generated: "2026-08-20T00:00:00+08:00"
---
# 终止行程后已取消派单「释放车辆/司机占用」入口(#6107)
## 背景
订单**中止行程**后,订单侧已把对应车务派单截断为 `canceled`,看板正确显示「已取消」。
但「已取消」只是把派单行冻结,**该行原本锁住的车辆/司机资源占用(脏 busy)不会自动全部归还**——需要车务同事手动触发一次「释放占用」。此前前端不知道后端已有这个释放端点,导致看板详情里看不到任何释放按钮,资源一直被冻结,影响后续派车。
**本文一次性告知:释放占用的后端能力早已上线,前端只需补挂一个按钮。**
## 变更接口
| 接口 | 变更 |
| --- | --- |
| `POST /admin/fleet/assignments/clear-cancelled-occupancy` | **已上线,本次补告知**。一键圈定某订单/需求下全部已取消(canceled)派单的车辆/司机,清掉它们的脏占用(合法 busy 不动)。**只清占用,不修改派单任何字段** |
| `POST /admin/fleet/assignments/{assignmentId}/soft-clear-assignment` | 已上线(#5936)。单条软清备用;本单默认用上面的整单一键清 |
## 一、调用方式(clear-cancelled-occupancy)
**入参**(body):
```json
{
"orderId": "1934567890123456789",
"requestId": "clear-cancel-20260820-001"
}
```
- `orderId` 与 `requirementId` **二选一**(都填/都不填返 100001)。车务看板详情页天然有 `orderId`,用它即可。
- `requestId` **必填**,幂等标识,≤64 字符,前端生成(如 `clear-cancel-` + 时间戳)。
**返回**(data):
```json
{
"vehicleDriverPairs": 2,
"intentsWritten": 2,
"assignmentIds": ["1934567890123456701"]
}
```
- `assignmentIds` 为本次实际圈到的已取消派单 ID 列表(雪花按字符串返回)。
- `intentsWritten=0` 表示没有可清的脏占用(幂等安全,可视为成功)。
**错误码**:`100001`(orderId 与 requirementId 二选一)/ `605047`(行程已结束)/ `605054`(终止冻结中,稍后重试)/ `605600`(已关账)。
## 二、前端动作(本次唯一要改的地方)
在**车务看板 → 订单详情抽屉 → 「车辆执行段」列表**补一个入口:
1. **可见条件**:`GET /admin/fleet/board/orders/{orderId}` 返回的 `activeAssignments[]` / `vehicleSlots[]` 里存在 `assignmentStatus === "canceled"` 的行,且该行 `assignmentId` 非空时,显示「释放车辆/司机占用」按钮(或文字链)。
2. **点击**:弹确认框(文案建议「将释放该订单下已取消派单占用的车辆/司机,是否继续?」),确认后调 `clear-cancelled-occupancy`,`orderId` 取当前订单、`requestId` 前端生成。
3. **成功后**:提示成功并**重新拉取详情**(占用释放是异步 outbox 反算,稍候在矩阵/资源日历可见车、司机恢复 idle)。
4. **非 canceled 行不展示该入口**;`holding`/`assigned` 仍走既有「清空司机/车辆」(#5936),语义不同,别混用。
## 三、取值字段(详情接口已返回,无需后端改)
`GET /admin/fleet/board/orders/{orderId}`(BoardOrderDetailVO)已含:
- `vehicleSlots[].assignmentStatus` / `assignmentStatusLabel`(canceled = 已取消)
- `vehicleSlots[].assignmentId`、`assignmentSlotId`、`vehicleId`、`driverId`
- `activeAssignments[].assignmentStatus` / `assignmentId`
## 验证证据
- 后端 `ClearCancelledOccupancy` 单测 7 例随 #5921 交付即绿;端点已在 TEST 部署。
- 全量 API 测试(2026-08-20,232 用例)复核:终止行程后 fleet assignment 正确置 `canceled`,仅缺前端释放按钮——纯前端缺口。
- 网关(TEST 9443,真 SUPER_ADMIN token)实测本端点:不存在 orderId 幂等返回 `200 {vehicleDriverPairs:0, intentsWritten:0, assignmentIds:[]}`,路由可达、结构符合 RespVO 契约。
@@ -0,0 +1,66 @@
---
schema: "hl-changelog/v2"
ticket: "6111"
title: "终止行程(异常)订单配房组「最终确认/清空配房」禁用(标记完成保留)——纯前端渲染逻辑告知"
consumer: "admin"
author: "wx"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "a586343b"
target_release: ""
verified_at: "2026-08-20"
status_note: "订单终止行程后配房组 houseStatus=EXCEPTION。本文告知前端:此类订单禁用「最终确认」「清空配房」两按钮;但「标记完成」**保留可点**——它是 #4307 专门设计的异常单收尾出口(仅 EXCEPTION 时显示,点它把异常单收尾进「已完成」),禁了会失去异常收尾能力。后端契约字段早已返回,本次无后端代码变更,纯前端渲染/禁用逻辑。本文为上版(误含标记完成)的勘误版。"
updated_at: "2026-08-20"
base: "dev-v3"
generated: "2026-08-20T00:00:00+08:00"
---
# 终止行程(异常)订单配房按钮处理(#6111 子项 · 勘误版)
> ⚠️ **勘误**:上一版本要求对异常订单禁用「标记完成」——**错误**。前端反馈并经后端核实:「标记完成」是工单 **#4307** 专门设计的**异常单收尾出口**,`仅当前 houseStatus=EXCEPTION 时可标记完成`(否则 808022),点它把异常单收尾推进到「已完成」。** EXCEPTION 时恰恰要保留它可点**,禁了就没有异常收尾出口了。本版更正。
## 背景
订单**中止行程**后,房务侧把配房组翻为异常(`houseStatus=EXCEPTION`),产生退订待办。此时该订单配房处于「异常/待处理退订」状态,**正常配房流程的动作**(最终确认、清空配房)对异常单无意义、易致状态错乱,应禁用。
但「**标记完成**」是异常桶的**处置完成出口**——房务把退订等异常处置完后,点它把异常单关掉(EXCEPTION → CONFIRMED/已完成)。**这个必须保留**。
## 变更接口
| 接口 | 变更 |
| --- | --- |
| `GET /v3/admin/order/{orderId}/detail`(订单详情) | **无契约变更**。`itinerary[].hotelGroup.houseStatus` / `actions` 字段早已返回,本文仅告知前端禁用逻辑 |
## 一、判断依据(契约字段,已返回)
订单详情 `itinerary[].hotelGroup`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `houseStatus` | string | 房务状态机值。**终止行程后 = `EXCEPTION`(异常)** |
## 二、前端渲染要求(`houseStatus === "EXCEPTION"` 时)
| 按钮 | 处理 | 说明 |
| --- | --- | --- |
| **最终确认** | **禁用** | 异常单不再走正常最终确认流程。建议置灰 + tooltip「订单已终止行程,配房异常待处理」 |
| **清空配房** | **禁用** | 异常单的退订/释放走行级释放(#6111 另一子项)或异常处置,不再整体清空 |
| **标记完成** | **保留可点** | ⚠️ #4307 异常收尾出口,**禁用即丢失异常收尾能力**。维持现状 `v-if="isException && canMutateRequirement"` 可点 |
## 三、正常订单不受影响
`houseStatus` 非 `EXCEPTION` 的订单,三按钮显隐维持现状。
## 四、配套说明
- 「**未用房晚标记 + 行级资源释放按钮**」与「**异常原因显示(客户中止)**」是 #6111 的另外两个子项,**需后端补字段**,在 #6111 工单跟进,不在本文范围。
- 本文只覆盖按钮禁用/保留逻辑,纯前端,字段已具备。
## 验证
- [ ] 终止行程订单(如 26-9250)详情页「最终确认」「清空配房」禁用/置灰
- [ ] 终止行程订单「标记完成」**仍可点**(异常收尾出口)
- [ ] 正常进行中订单三按钮显隐不变
@@ -0,0 +1,91 @@
---
schema: "hl-changelog/v2"
ticket: "6111"
title: "配房行程标记终止未用房晚+异常原因透传"
consumer: "admin"
author: "wx"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "后端完成:PR #6116 已合并 dev-v3(squash 3dd65f62d)并部署 TEST(hl-order-service-v3 双实例 8086/8186,2026-08-20 17:49 重启)。行程接口响应 hotelGroup 新增异常原因 2 字段、配房行新增未用标记 2 字段(均可选只读,正常单为 null/不影响既有字段),前端按需取读即可。"
updated_at: "2026-08-20"
base: "dev-v3"
generated: "2026-08-20T17:55:00+08:00"
---
# 配房行程标记终止未用房晚+异常原因透传(#6111)
## 背景
订单中止行程后,房务「配房行程」详情页看不到:(1) 哪几晚被运营在终止时勾选为「未用房晚」;(2) 异常桶旁的异常原因文案。本变更为房务侧补上这两个只读字段(纯新增,不影响既有字段)。
## 变更接口
| 接口 | 变更 |
| --- | --- |
| `GET /v3/admin/order/{id}/itinerary`(订单详情-行程安排) | **响应新增字段**(均可选只读,正常单为 null/false,不影响既有任何字段):`hotelGroup.exceptionReason`(String,异常原因,仅 houseStatus=EXCEPTION 时有值)、`hotelGroup.exceptionReasonLabel`(String,同 exceptionReason 中文展示)、`hotelGroup.assignments[].unusedForTerminate`(Boolean,该晚是否终止勾选未用)、`hotelGroup.assignments[].unusedLabel`(String,未用标记文案「未用」,未标记为 null) |
## 响应示例
`GET /v3/admin/order/{id}/itinerary` 返回(仅列本次新增字段,其余字段省略):
```json
{
"code": 200,
"data": {
"hotelGroup": {
"houseStatus": "EXCEPTION",
"exceptionReason": "客户中止",
"exceptionReasonLabel": "客户中止",
"assignments": [
{
"assignmentId": "2090093115967733761",
"dayNumber": 1,
"hotelName": "呼伦贝尔香格里拉大酒店",
"unusedForTerminate": true,
"unusedLabel": "未用"
},
{
"assignmentId": "2090093115976122369",
"dayNumber": 2,
"hotelName": "呼伦贝尔香格里拉大酒店",
"unusedForTerminate": true,
"unusedLabel": "未用"
}
]
}
}
}
```
说明:该单已终止且运营勾选两晚均未用,故两行 `unusedForTerminate=true`、`unusedLabel="未用"`;`houseStatus=EXCEPTION` 故 `exceptionReason` 透出终止原因。正常单(非终止)`exceptionReason/Label` 为 null、`unusedForTerminate=false`、`unusedLabel` 为 null。
## 行为口径
- 4 个字段均为**只读、可选**:正常单(未终止)`exceptionReason/Label` 为 null、`unusedForTerminate=false`、`unusedLabel` 为 null,接口不报错。
- `unusedForTerminate=true` 的行即终止时运营勾选的「未用房晚」,前端可渲染「未用」标记 + 行级「资源释放」按钮。
- **行级资源释放**复用现有 `DELETE /v3/admin/order/assignments/{id}`(软删+恢复库存,CONFIRMED 行自动解冻),前端按该行 `assignmentId` 调即可,**无新端点**。
- `exceptionReason` 来源为终止行程原因,仅异常桶(houseStatus=EXCEPTION)有值。
## 前端动作
无必须动作。需要展示「未用」标记时读 `assignments[].unusedForTerminate/unusedLabel`;需要显示异常原因时读 `hotelGroup.exceptionReason(Label)`;需要行级释放按钮时对该行调 `DELETE /v3/admin/order/assignments/{assignmentId}`。
## 验证证据
- 单测:OrderDetailConverterTest 44 / TerminateRefundServiceTest 8 / OrderDetailServiceTest 88 全绿(新增 7 例覆盖命中/未命中/未终止/null 边界)。
- 架构门禁:*ArchTest 44 全绿(跨子聚合走 Service 契约,零违规)。
- 部署:PR #6116 squash 合并 dev-v3(merge commit `3dd65f62d`);测试服 hl-order-service-v3 双实例 8086/8186 于 2026-08-20 17:49 滚动重启健康。
- 网关实测(TEST,admin token):终止单 `GET /v3/admin/order/2090093100083904514/itinerary` 返 200,`hotelGroup.exceptionReason="客户中止"`,两行配房 `unusedForTerminate=true`/`unusedLabel="未用"`;正常单(CUSTOMIZING)`exceptionReason=null`,符合「异常有值/正常 null」口径。
## 关联 / 联系人
- Issue:https://git.1814.love:8443/wx/HL/issues/6111
- PR:https://git.1814.love:8443/wx/HL/pulls/6116
- Commit(merge):https://git.1814.love:8443/wx/HL/commit/3dd65f62d
- 后端负责人:wx
@@ -0,0 +1,101 @@
---
schema: "hl-changelog/v2"
ticket: "6114"
title: "已取消派单显示当时派车快照+按未用天行级释放占用"
consumer: "admin"
author: "wx"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "0d3cdf64"
target_release: ""
verified_at: "2026-08-20"
status_note: "后端完成:PR #6115 已合并 dev-v3(squash fc3d40c92)并部署 TEST(hl-fleet-service 双实例 8087/8187,2026-08-20 18:09 重启)。车务看板已取消行新增 4 个取消时派车快照字段(可选只读,正常行为 null),清除占用接口新增可选 serviceDates 行级释放。前端据此在已取消行回显释放对象 + 按未用天逐行释放。"
updated_at: "2026-08-20"
base: "dev-v3"
generated: "2026-08-20T18:15:00+08:00"
---
# 已取消派单显示当时派车快照+按未用天行级释放占用(#6114)
## 背景
订单中止行程后,车务看板里被勾选的「未用天」对应派单行被置为已取消,但当前派车信息(车牌/车型/司机/电话)按 #5828 读侧清空成 null,导致前端无法展示「这辆车/这位司机」是被谁占用、也无法让运营对着具体行去释放。本变更:(1) 已取消行**额外回显取消当时的派车快照**(只读展示用,当前派车字段维持 null 不变);(2) 清除占用接口支持**按服务日子集行级释放**(对应中止时勾选的未用天)。纯新增,不影响既有任何字段与整单级释放行为。
## 变更接口
| 接口 | 变更 |
| --- | --- |
| `GET /admin/fleet/board/orders`(车务看板订单列表) | **响应新增字段**(均在派单行 slot 上,可选只读,正常/未取消行为 null):`cancelledVehiclePlate`(String,取消时车牌快照,仅已取消行有值)、`cancelledVehicleModel`(String,取消时车型快照)、`cancelledDriverName`(String,取消时司机姓名快照)、`cancelledDriverPhone`(String,取消时司机手机号**脱敏**快照,形如 `135****5005`)。**注意**:既有 `vehiclePlate/vehicleModel/driverName/driverPhone` 在已取消行**仍为 null**(#5828 读侧清空逻辑不变),请改用 `cancelled*` 快照字段展示释放对象。 |
| `POST /admin/fleet/assignments/clear-cancelled-occupancy`(清除已取消派单占用) | **请求新增可选字段** `serviceDates`(`LocalDate[]`,形如 `["2026-08-21","2026-08-22"]`):传值=只释放服务日落在该子集内的已取消派单(行级/按未用天释放);**不传或传空数组=维持原整单级释放全部**(向后兼容)。响应结构不变。 |
## 响应示例
`GET /admin/fleet/board/orders` 已取消行(仅列本次新增字段,其余字段省略):
```json
{
"code": 200,
"data": {
"list": [
{
"orderNo": "HL20260819132123689",
"fleetItems": [
{
"slots": [
{
"assignmentId": "2089945846031257601",
"assignmentStatus": "CANCELED",
"lifecycleStageLabel": "已取消",
"vehiclePlate": null,
"vehicleModel": null,
"driverName": null,
"driverPhone": null,
"cancelledVehiclePlate": "蒙A-K1999",
"cancelledVehicleModel": "丰田考斯特",
"cancelledDriverName": "乌云毕力格",
"cancelledDriverPhone": "135****5005"
}
]
}
]
}
]
}
}
```
## 请求示例(行级释放)
`POST /admin/fleet/assignments/clear-cancelled-occupancy` —— 只释放未用天 `2026-08-21`、`2026-08-22` 的已取消派单占用:
```json
{
"orderId": "2089945804415315969",
"requestId": "clear-cancel-20260820-001",
"serviceDates": ["2026-08-21", "2026-08-22"]
}
```
整单级释放(原有用法,`serviceDates` 不传):
```json
{
"orderId": "2089945804415315969",
"requestId": "clear-cancel-20260820-002"
}
```
## 前端改造点
1. **已取消行回显**:派单行 `assignmentStatus="CANCELED"`(或 `lifecycleStageCode` 已取消)时,车牌/车型/司机/电话改读 `cancelledVehiclePlate/cancelledVehicleModel/cancelledDriverName/cancelledDriverPhone`(为 null 再降级为「—」),不要再读 `vehiclePlate` 等当前字段(恒为 null)。
2. **行级释放按钮**:未用天对应的已取消车行上加「释放占用」入口,调 `POST /admin/fleet/assignments/clear-cancelled-occupancy`,把该行覆盖的服务日放入 `serviceDates`(单日行传 1 个日期即可)。
3. 释放成功后建议刷新看板占用展示。
## 验证证据
- 单测:`AssignmentServiceClearCancelledOccupancyTest` 9/9 绿(含 #6114 新增 `clearCancelledOccupancy_withServiceDates_onlyReleasesMatchingDays` / `..._noMatch_returnsZero`,行级只释放命中日、无命中幂等返回 0)。
- 网关实测(TEST,`GET /admin/fleet/board/orders?keyword=26-9250`):已取消行 `vehiclePlate/vehicleModel/driverName/driverPhone` 均为 null(#5828 不变),同时新增 `cancelledVehiclePlate="蒙A-K1999"`、`cancelledVehicleModel="丰田考斯特"`、`cancelledDriverName="乌云毕力格"`、`cancelledDriverPhone="135****5005"`(脱敏),快照正确回显。
- 部署:hl-fleet-service 双实例 8087/8187 于 2026-08-20 18:09 滚动重启生效(jar 含 `fc3d40c92` 字段,javap 已核)。
@@ -0,0 +1,200 @@
---
schema: "hl-changelog/v2"
ticket: "6111"
title: "终止行程订单配房行程页待渲染:未用房晚标记+异常原因+行级释放按钮(后端字段已上线)"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "9172700f"
target_release: ""
verified_at: ""
status_note: "后端 #6116 已合并 dev-v3 并部署 TEST,行程接口已返回 houseStatus=EXCEPTION 时的 exceptionReason(Label) 与 assignments[].unusedForTerminate/unusedLabel 4 个只读字段(网关实测已验证,订单 26-9250)。本条为前端向告知:配房行程页需补渲染「未用房晚标记」「异常原因文案」「行级资源释放按钮」三处,字段已具备,前端按需取读即可。"
updated_at: "2026-08-21"
base: "dev-v3"
---
# 终止行程订单配房行程页待渲染:未用房晚标记+异常原因+行级释放按钮(#6111)
> **存放目录**: 二期(v3, `order-v3` 工单) → `changelogs-v2/2026-08/`
>
> **服务**: hl-order-service-v3 (端口 8086/8186)
> **PR**: #6116
> **Issue**: #6111
> **日期**: 2026-08-21
> **影响范围**: 管理后台-订单详情-配房行程页(仅终止行程/异常单)
---
## ⚠️ 关键变化
订单**终止行程**后,配房行程页当前**看不到**:哪几晚被运营勾选为「未用房晚」、异常桶旁的异常原因、行级「资源释放」入口。后端字段**已全部上线**(网关实测订单 26-9250 已返回),本条告知前端按下方「待渲染清单」接入即可,**无需后端再改动**。
---
## 一、背景
运营在终止行程时会勾选「未用房晚」,房务侧据此判断哪些房晚可释放资源;异常桶也需要向房务展示终止原因。这两块信息后端在 #6116 已透传到行程接口,但前端配房行程页尚未渲染,导致房务看不到「未用」标记与异常原因,也没有行级释放入口。
测试服实测(订单 26-9250,已终止,`houseStatus=EXCEPTION`)接口返回:
| 字段 | 实测值 | 说明 |
|------|--------|------|
| `hotelGroup.houseStatus` | `EXCEPTION`(Label「异常」) | 异常桶 |
| `hotelGroup.exceptionReason` / `exceptionReasonLabel` | `客户家中急事` | 终止原因 ✅ 已返回 |
| `assignments[0]`(第1晚) | `unusedForTerminate=false`,`unusedLabel=null` | 已入住,不标未用 |
| `assignments[1]`(第2晚) | `unusedForTerminate=true`,`unusedLabel=未用` | ✅ 该标「未用」 |
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 订单详情-行程安排 | GET | `/v3/admin/order/{orderId}/itinerary` | 响应新增字段(已上线) | 新增 4 个只读字段,正常单为 null/false,不影响既有字段 |
| 2 | 行级资源释放 | DELETE | `/v3/admin/order/assignments/{assignmentId}` | 复用现有端点(无新增) | 软删+恢复库存,CONFIRMED 行自动解冻 |
---
## 三、接口详情
### 1. 订单详情-行程安排 `GET /v3/admin/order/{orderId}/itinerary`
本次前端需读取的新增字段(均可选只读,正常单为 null/false):
#### 出参(仅列本次新增字段,其余字段省略)
| 字段 | 位置 | 类型 | 说明 |
|------|------|------|------|
| `houseStatus` | `data.hotelGroup` | String | 房务状态机值。终止行程后 = `EXCEPTION`(异常) |
| `exceptionReason` | `data.hotelGroup` | String | 异常原因,仅 `houseStatus=EXCEPTION` 时有值,否则 null |
| `exceptionReasonLabel` | `data.hotelGroup` | String | `exceptionReason` 中文展示,否则 null |
| `unusedForTerminate` | `data.hotelGroup.assignments[]` | Boolean | 该晚是否终止勾选未用,正常单为 false |
| `unusedLabel` | `data.hotelGroup.assignments[]` | String | 未用标记文案「未用」,未标记为 null |
| `assignmentId` | `data.hotelGroup.assignments[]` | String | 行级释放时调 DELETE 端点用此 id |
#### 响应示例
```json
{
"code": 200,
"data": {
"hotelGroup": {
"houseStatus": "EXCEPTION",
"houseStatusLabel": "异常",
"exceptionReason": "客户家中急事",
"exceptionReasonLabel": "客户家中急事",
"assignments": [
{
"assignmentId": "2089945850783346690",
"dayNumber": 1,
"hotelName": "呼伦贝尔香格里拉大酒店",
"unusedForTerminate": false,
"unusedLabel": null
},
{
"assignmentId": "2089945850842066945",
"dayNumber": 2,
"hotelName": "呼伦贝尔香格里拉大酒店",
"unusedForTerminate": true,
"unusedLabel": "未用"
}
]
}
},
"success": true
}
```
### 2. 行级资源释放 `DELETE /v3/admin/order/assignments/{assignmentId}`
- 对 `unusedForTerminate=true` 的行,按该行 `assignmentId` 调本端点。
- 行为:软删该晚配房行 + 恢复对应库存;CONFIRMED 行自动解冻。
- **无新端点**,复用现有删除接口。
---
## 四、前端待渲染清单(`houseStatus === "EXCEPTION"` 时)
| 待渲染项 | 数据来源 | 处理 |
|----------|----------|------|
| **异常原因文案** | `hotelGroup.exceptionReasonLabel` | 异常桶旁显示终止原因(如「客户家中急事」) |
| **未用房晚标记** | `assignments[].unusedForTerminate` / `unusedLabel` | 为 true 的行渲染「未用」标记 |
| **行级「资源释放」按钮** | 该行 `assignmentId` | 仅对 `unusedForTerminate=true` 的行显示,点击调 `DELETE /v3/admin/order/assignments/{assignmentId}` |
正常订单(`houseStatus` 非 EXCEPTION)上述字段为 null/false,渲染逻辑不触发,既有页面不受影响。
---
## 五、边界行为
- 正常单(未终止)→ `exceptionReason(Label)` 为 null、`unusedForTerminate=false`、`unusedLabel` 为 null,接口不报错、页面不渲染。
- 已入住房晚(如 26-9250 第1晚)→ `unusedForTerminate=false`,不标「未用」、不出释放按钮。
- 字段缺失(老数据)→ 视为 null/false,不异常。
---
## 六、影响评估
- **是否破坏向后兼容**: 否(纯新增只读字段,正常单为 null/false)
- **前端是否必须同步上线**: 否(不接入不影响既有功能;接入后房务才能看到未用标记/异常原因/释放入口)
- **前端 workaround 清理点**: 无
---
## 七、不影响范围
- **仅影响**: 管理后台-订单详情-配房行程页(终止行程/异常单)
- **零影响**:
- 订单详情读取的既有字段
- 正常进行中订单的配房行程渲染
- C 端行程接口
- 车务侧(车辆执行段「释放车辆/司机占用」已在 #6107/#6114 接入,与本条独立)
---
## 八、测试环境已验证
真实网关调用(TEST,admin token):
```
GET /v3/admin/order/2089945804415315969/itinerary → 200 ✓
hotelGroup.houseStatus=EXCEPTION / houseStatusLabel=异常 ✓
hotelGroup.exceptionReason=客户家中急事 / exceptionReasonLabel=客户家中急事 ✓
assignments[0](第1晚) unusedForTerminate=false / unusedLabel=null ✓(已入住不标)
assignments[1](第2晚) unusedForTerminate=true / unusedLabel=未用 ✓
```
验证订单: `order_id=2089945804415315969`(团号 26-9250,行程中客户03,已终止)
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #6116 | #6111 | 后端补 4 个只读字段(exceptionReason/unusedForTerminate 等) | ✅ 有效(已上线 TEST) |
| —(前端向) | #6111 | 本条:告知前端接入配房行程页三处渲染 | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#6111](https://git.1814.love:8443/wx/HL/issues/6111)
- 关联 PR: [wx/HL#6116](https://git.1814.love:8443/wx/HL/pulls/6116)
- 同工单配套(纯前端按钮禁用,已 verified): `changelogs-v2/2026-08/20_6111_终止行程订单配房三按钮禁用-修改接口-管理后台.md`
## 关联 / 联系人
### 链接
- **Issue**: [#6111](https://git.1814.love:8443/wx/HL/issues/6111)
- **PR**: [#6116](https://git.1814.love:8443/wx/HL/pulls/6116)
- **Merge commit**: [3dd65f62d](https://git.1814.love:8443/wx/HL/commit/3dd65f62d)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,74 @@
---
schema: "hl-changelog/v2"
ticket: "6112"
title: "看板详情透出终止行程取消原因"
consumer: "admin"
author: "wx"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "d58ad932"
target_release: ""
verified_at: "2026-08-21"
status_note: "终止行程的真实原因(如「客户中止」)此前在 order 侧,fleet 落库的是写死文案。本次全链路透传:order → Feign DTO 加可选字段 → fleet 落库 → 看板详情 VO 透出。前端只需在已取消行渲染 cancelReason。"
updated_at: "2026-08-21"
base: "dev-v3"
generated: "2026-08-21T12:38:23+08:00"
---
# 看板详情透出终止行程取消原因(#6112)
## 背景
订单**终止行程**(出行中中止)时运营会填真实原因(如「客户中止」),但车务看板已取消派单只显示「已取消」,看不到原因——fleet 侧落库的是固定文案「订单终止行程,自终止生效日起截断未来派单」。
本次把真实原因全链路透传到看板详情,前端补渲染即可。与房务侧 #6111(异常原因透出)对齐。
## 变更接口
| 接口 | 变更 |
| --- | --- |
| `GET /admin/fleet/board/orders/{orderId}` | **响应新增可选字段**:`vehicleSlots[].cancelReason`、`currentAssignment.cancelReason`。已取消(`slotStatus==="canceled"`)的槽位/派单返回终止或取消原因;其他状态为 `null` |
内部链路(前端无需关心,仅供联调排查):order 侧 `terminateTrip` 把 `cancelReason` 经 `TruncateOrderAssignmentsRequest`(内部 Feign DTO,新增可选字段)传给 fleet,fleet 终止截断时落库 `fleet_assignment.cancel_reason`。
## 前端动作(本次唯一要改的地方)
在车务看板 → 订单详情抽屉 → 已取消派单行/车辆执行段:
1. **渲染条件**:`vehicleSlots[]` 中 `slotStatus === "canceled"` 且 `cancelReason` 非空时,在「已取消」badge 旁展示原因文本(如「客户中止」)。
2. **空态**:历史已取消派单 `cancelReason` 为 `null` → 不渲染原因区,不报错。
3. 非 canceled 行不展示(字段为 `null`)。
4. 无新增颜色/状态标签,纯文本。
## 取值示例(TEST 实测返回)
```json
{
"assignmentSlotId": "348032503497494528",
"slotStatus": "canceled",
"slotStatusLabel": "已取消",
"cancelReason": "客户中止(#6112 网关验收)",
"assignmentId": "2089642687186563073"
}
```
## 兼容性
- 旧版 order 不传 `cancelReason` → fleet 收到 `null`,落库保留原固定文案兜底(双向兼容,可独立发布)。
- 历史已取消派单 `cancel_reason` 为 NULL → VO 原样透传 `null`,前端不渲染即可。
- 手动取消(人工填的 cancelReason)写入路径未改动,不受影响。
## 验证证据
- 单测:fleet `AssignmentServiceTest`(透传落库/空值兜底)、`BoardOrderServiceTest`(已取消槽位透出/在途槽位 null)、order `OrderServiceTest`(透传断言)、`FleetAssignmentExpandFeignContractTest`(DTO wire 往返)全绿;`mvn -pl hl-fleet-service -am verify` BUILD SUCCESS(3846 测试),spotless 0 违规。
- 部署:hl-fleet-service(8087/8187)、hl-order-service-v3(8086/8186)已滚动部署 TEST。
- 网关实测(TEST 9443,真 SUPER_ADMIN token):对出行中订单 `HL20260818171654688` 以「客户中止」终止行程 → fleet 派单截断为 canceled → `GET /admin/fleet/board/orders/2089642686163128322` 返回 `vehicleSlots[].cancelReason="客户中止(#6112 网关验收)"`;历史终止单 `2090093100083904514` 返回原固定文案,无报错。
## 关联
- Issue: [#6112](https://git.1814.love:8443/wx/HL/issues/6112)
- PR: [#6124](https://git.1814.love:8443/wx/HL/pulls/6124)
- Merge commit: [a373d5be2](https://git.1814.love:8443/wx/HL/commit/a373d5be2)
@@ -0,0 +1,728 @@
---
schema: "hl-changelog/v2"
ticket: "6117"
title: "导游/摄影核单去 EXCLUDED + 全量替换,确认状态统一 settlementConfirmStatus"
consumer: "admin"
change_type: "修改接口"
author: "yst"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "ec7f2a9a"
target_release: ""
verified_at: "2026-08-21"
status_note: "PR #6119 已合并 dev-v3(merge commit 27ac77b500),测试服已验证。破坏性变化:导游/摄影核单废弃「按天候选 + EXCLUDED」机制改全量替换语义;保存入参删 candidateKey/completionState/sourceResolution/excludedCandidateKeys,传旧字段一律 400(584128);查询出参删 candidateKey/completionState/candidateResolution/sourceActive/pendingCandidateCount;确认状态统一 settlementConfirmStatus(UNCONFIRMED/CONFIRMED 二值);blockReasonCode 删 SOURCE_INACTIVE/CANDIDATES_UNRESOLVED 两值。无 DDL。"
updated_at: "2026-08-21"
base: "dev-v3"
---
# 【修改接口·管理后台】导游/摄影核单确认状态统一——去 EXCLUDED 改全量替换(#6117)
> **PR**: #6119 | **服务**: hl-order-service-v3 | **更新时间**: 2026-08-21
## 1. 接口背景
订单核单页「导游」「摄影」两个费用 tab 此前使用「按天候选 + EXCLUDED」机制:后端按订单行程天数预生成候选行,前端要在「纳入(INCLUDED)/ 排除(EXCLUDED)/ 未处理(UNRESOLVED)」三种候选处理结果之间来回切换,还要单独维护一份 `excludedCandidateKeys` 排除清单。这套机制字段多、状态绕,和酒店/门票 tab 的「全量保存 + 确认状态」模型完全不一致,前端两套交互逻辑要分别维护。
本次变更把导游/摄影 tab 拉齐到酒店/门票同款模型:
- 废弃候选机制,保存接口改**全量替换**语义——传当前应存在的全部行,没传的未确认行即删除;
- 确认状态统一为每行一个 `settlementConfirmStatus`(`UNCONFIRMED` / `CONFIRMED` 二值),保存时可直接把行置为已确认;
- 已确认行受保护:不能被全量替换顺手删掉,编辑业务字段会自动退回未确认、需重新确认。
涉及导游(guide-fees)与摄影(photographer-fees)两组共 6 个接口,两组结构完全同构,仅字段名有差异(导游用 `name`/`serviceType`,摄影用 `photographerName`/`feeType`)。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 查询导游单层费用明细 | GET | `/v3/admin/order/{orderId}/settlement/guide-fees` | 修改接口 | 出参删 candidateKey/completionState/candidateResolution/sourceActive/pendingCandidateCount |
| 2 | 全量保存导游单层费用明细 | PUT | `/v3/admin/order/{orderId}/settlement/guide-fees` | 修改接口 | 入参删 candidateKey/completionState/sourceResolution/excludedCandidateKeys,新增 settlementConfirmStatus;改全量替换语义 |
| 3 | 确认导游单层费用明细 | POST | `/v3/admin/order/{orderId}/settlement/guide-fees/confirm` | 修改接口 | 签名不变(itemIds),确认语义对齐新模型 |
| 4 | 查询摄影单层费用明细 | GET | `/v3/admin/order/{orderId}/settlement/photographer-fees` | 修改接口 | 同 #1 |
| 5 | 全量保存摄影单层费用明细 | PUT | `/v3/admin/order/{orderId}/settlement/photographer-fees` | 修改接口 | 同 #2 |
| 6 | 确认摄影单层费用明细 | POST | `/v3/admin/order/{orderId}/settlement/photographer-fees/confirm` | 修改接口 | 同 #3 |
## 3. 接口详情
### 3.1 查询导游单层费用明细(GET guide-fees)
- **使用场景**:打开核单页「导游」tab 时加载费用明细列表与分类汇总
- **认证**:管理后台 JWT(房务角色只读拦截,返回 403)
- **幂等性**:是(只读)
- **限流**:无
**入参**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| orderId | Long(路径参数) | ✅ | 订单 ID,必须大于 0 |
**出参**(`Result<SettlementGuideFeesRespVO>`)
响应级字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| category | String | 核算分类,恒为 `GUIDE` |
| totalAmount | String | 纳入核算的费用合计,金额字符串,如 `500.00` |
| cashPaidAmount | String | 现付费用合计,金额字符串 |
| unconfirmedCount | Integer | 未确认明细数(全行口径,见 §10.2) |
| settlementReady | Boolean | 是否满足本分类提交核单条件 |
| blockReasonCode | String | 阻断原因码;无阻断时为 null,取值见 §6.4 |
| items | Array | 费用明细数组;无数据返回空数组 |
| editable | Boolean | 当前订单是否允许编辑本分类 |
| readOnlyReasonCode | String | 只读原因码;可编辑时为 null |
items[] 行字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String | 费用明细 ID(Long 序列化为字符串);未落库的预填草稿行为 null |
| staffAssignmentId | String | 人员分配 ID;手工新增行为 null |
| serviceDate | String | 服务日期 `YYYY-MM-DD`;预填草稿行为 null |
| name | String | 导游姓名 |
| serviceType | String | 服务类型,取值见 §6.1 |
| serviceTypeName | String | 服务类型中文名;serviceType 为 null 时为 null |
| paymentMethod | String | 付款方式,取值见 §6.3 |
| paymentMethodName | String | 付款方式中文名;paymentMethod 为 null 时为 null |
| amount | String | 金额字符串;预填草稿行为 null |
| settlementConfirmStatus | String | 核单确认状态,取值见 §6.5 |
| settlementConfirmStatusName | String | 核单确认状态中文名 |
| remark | String | 备注;无备注为 null |
| sourceType | String | 来源类型:`STAFF_ASSIGNMENT` 人员安排 / `MANUAL` 手工 / `SYSTEM` 系统 |
| sourceTypeName | String | 来源类型中文名 |
| voucherUrls | Array<String> | 凭证 URL;无凭证返回空数组 |
**错误码**:见 §7 全组共用错误码表。
**业务边界**:首次查询(订单尚无该角色核单行)时,返回按订单人员分配预填的草稿行(不落库,行特征:`id` 为 null、`settlementConfirmStatus=UNCONFIRMED`);这些预填行会被计入 `unconfirmedCount`,影响 `settlementReady` 展示口径。
**示例(典型成功)**
请求:
```
GET /v3/admin/order/12345/settlement/guide-fees
Authorization: Bearer {admin-token}
(无请求体)
```
响应:
```json
{
"code": 200,
"data": {
"category": "GUIDE",
"totalAmount": "800.00",
"cashPaidAmount": "300.00",
"unconfirmedCount": 1,
"settlementReady": false,
"blockReasonCode": "ITEMS_UNCONFIRMED",
"editable": true,
"readOnlyReasonCode": null,
"items": [
{
"id": "9001",
"staffAssignmentId": "11",
"serviceDate": "2026-08-03",
"name": "导游甲",
"serviceType": "FULL_COURSE_GUIDE",
"serviceTypeName": "全陪导游",
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司支付",
"amount": "500.00",
"settlementConfirmStatus": "CONFIRMED",
"settlementConfirmStatusName": "已确认",
"remark": null,
"sourceType": "STAFF_ASSIGNMENT",
"sourceTypeName": "人员安排",
"voucherUrls": []
},
{
"id": "9002",
"staffAssignmentId": null,
"serviceDate": "2026-08-04",
"name": "导游乙",
"serviceType": "LOCAL_GUIDE",
"serviceTypeName": "地接导游",
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现付",
"amount": "300.00",
"settlementConfirmStatus": "UNCONFIRMED",
"settlementConfirmStatusName": "未确认",
"remark": "现场临时请的地陪",
"sourceType": "MANUAL",
"sourceTypeName": "手工",
"voucherUrls": ["https://oss.example.com/voucher/a.jpg"]
}
]
},
"msg": "",
"success": true
}
```
**示例(边界:首次查询返回预填草稿行)**
场景说明:订单分配了 1 名导游但从未保存过核单行,GET 返回不落库的预填草稿(`id`/`serviceDate`/`amount` 为 null)。
响应:
```json
{
"code": 200,
"data": {
"category": "GUIDE",
"totalAmount": "0.00",
"cashPaidAmount": "0.00",
"unconfirmedCount": 1,
"settlementReady": false,
"blockReasonCode": "ITEMS_UNCONFIRMED",
"editable": true,
"readOnlyReasonCode": null,
"items": [
{
"id": null,
"staffAssignmentId": "11",
"serviceDate": null,
"name": "导游甲",
"serviceType": null,
"serviceTypeName": null,
"paymentMethod": null,
"paymentMethodName": null,
"amount": null,
"settlementConfirmStatus": "UNCONFIRMED",
"settlementConfirmStatusName": "未确认",
"remark": null,
"sourceType": "STAFF_ASSIGNMENT",
"sourceTypeName": "人员安排",
"voucherUrls": []
}
]
},
"msg": "",
"success": true
}
```
### 3.2 全量保存导游单层费用明细(PUT guide-fees)
- **使用场景**:在「导游」tab 编辑完费用明细后整体保存(新增 / 修改 / 删除行都通过本接口一次性提交)
- **认证**:管理后台 JWT
- **幂等性**:是(全量替换语义,同一 body 重放结果一致)
- **限流**:无
**入参**
路径参数:`orderId`(Long,必填,订单 ID)。
请求体字段:
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| items | Array<ItemVO> | ✅ | 导游费用明细**全量集合**(当前应存在的全部行) | 最多 200 条,超出报 584121 |
ItemVO 字段:
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| id | String | ❌ | 费用明细 ID;已存在行必传,新增行不传 | 必须传字符串形式 |
| staffAssignmentId | String | ❌ | 人员分配 ID;关联人员分配的行传 | 非空时必须属本单且角色为导游,否则报 584115 |
| serviceDate | String | ❌ | 服务日期 `YYYY-MM-DD` | 必须在订单行程范围内,否则报 584112;订单出发/返程日期缺失时报 584123 |
| name | String | ❌ | 导游姓名 | 最长 64 字符 |
| serviceType | String | ❌ | 服务类型 | 取值见 §6.1,非法值报 584113 |
| paymentMethod | String | ❌ | 付款方式 | 取值见 §6.3 |
| amount | String | ❌ | 金额字符串 | 0 至 99999999.99 且最多两位小数,否则报 584114 |
| remark | String | ❌ | 备注;无备注传 null | 最长 500 字符 |
| voucherUrls | Array<String> | ❌ | 凭证 URL | 最多 9 个,单条最长 1024;重复值去重并保持首次出现顺序 |
| settlementConfirmStatus | String | ❌ | 核单确认状态;**缺省/null 按 UNCONFIRMED 处理**,保存时可直接置 CONFIRMED | 仅允许 `UNCONFIRMED` / `CONFIRMED`,其他值报 584128 |
**严格模式**:请求体(顶层或行内)出现任何未定义字段一律 400。改前旧字段 `candidateKey` / `completionState` / `sourceResolution` / `excludedCandidateKeys` 现已删除,**传了同样 400**(错误码 584128,msg 形如 `导游或摄影费用请求字段不合法:导游费用明细不支持字段: candidateKey`)。
**出参**:同 3.1 的响应结构(保存成功后返回最新全量)。
**错误码**:见 §7。
**业务边界**(全量替换语义):
- 本次请求传入的 `items` 即保存后的全部行;**库里存在但未传入的未确认行会被删除**;
- **已 CONFIRMED 行不可删除**——若库里某行已确认但本次未传,报 584120「已确认的有效费用不能直接删除,请先进入编辑状态」,整单保存失败;
- 已 CONFIRMED 行若本次修改了业务字段(姓名/金额/日期/类型/付款方式等),保存后自动重置回 `UNCONFIRMED`,需重新确认;
- 新增行可在保存时直接置 `CONFIRMED`(与酒店/门票 tab 行为一致)。
**示例(典型成功:两行全量保存,一行直接置已确认)**
请求:
```json
PUT /v3/admin/order/12345/settlement/guide-fees
Authorization: Bearer {admin-token}
{
"items": [
{
"id": "9001",
"staffAssignmentId": "11",
"serviceDate": "2026-08-03",
"name": "导游甲",
"serviceType": "FULL_COURSE_GUIDE",
"paymentMethod": "COMPANY_PAID",
"amount": "500.00",
"remark": null,
"voucherUrls": [],
"settlementConfirmStatus": "CONFIRMED"
},
{
"serviceDate": "2026-08-04",
"name": "导游乙",
"serviceType": "LOCAL_GUIDE",
"paymentMethod": "CASH_PAID",
"amount": "300.00",
"remark": "现场临时请的地陪",
"voucherUrls": ["https://oss.example.com/voucher/a.jpg"]
}
]
}
```
响应:`code=200`,`data` 为保存后的最新全量(结构同 3.1 响应示例)。
**示例(业务失败:误传已删除的旧字段)**
场景说明:前端未清理旧逻辑,行内仍带 `candidateKey`。
请求:
```json
PUT /v3/admin/order/12345/settlement/guide-fees
{
"items": [
{
"candidateKey": "2026-08-03#11",
"name": "导游甲",
"amount": "500.00"
}
]
}
```
响应:
```json
{
"code": 584128,
"data": null,
"msg": "导游或摄影费用请求字段不合法:导游费用明细不支持字段: candidateKey",
"success": false
}
```
**示例(业务失败:已确认行被全量替换遗漏)**
场景说明:库中行 9001 已 CONFIRMED,本次 items 只传了行 9002,相当于要删掉 9001。
响应:
```json
{
"code": 584120,
"data": null,
"msg": "已确认的有效费用不能直接删除,请先进入编辑状态",
"success": false
}
```
### 3.3 确认导游单层费用明细(POST guide-fees/confirm)
- **使用场景**:勾选若干未确认行后点「确认」,将这批行批量置为已确认
- **认证**:管理后台 JWT
- **幂等性**:是(对已确认行重复确认无副作用)
- **限流**:无
**入参**
路径参数:`orderId`(Long,必填)。
请求体字段:
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| itemIds | Array<String> | ✅ | 待确认的费用明细 ID 列表,JSON 中每项为字符串 | 1 至 200 条;ID 不存在或不属于本订单及导游角色报 584111 |
同样走严格模式,多传字段报 584128(msg 形如 `导游费用确认请求不支持字段: xxx`)。
**出参**:同 3.1 的响应结构(确认后的最新全量)。
**示例(典型成功)**
请求:
```json
POST /v3/admin/order/12345/settlement/guide-fees/confirm
Authorization: Bearer {admin-token}
{
"itemIds": ["9002"]
}
```
响应:
```json
{
"code": 200,
"data": {
"category": "GUIDE",
"totalAmount": "800.00",
"cashPaidAmount": "300.00",
"unconfirmedCount": 0,
"settlementReady": true,
"blockReasonCode": null,
"editable": true,
"readOnlyReasonCode": null,
"items": [
{
"id": "9002",
"staffAssignmentId": null,
"serviceDate": "2026-08-04",
"name": "导游乙",
"serviceType": "LOCAL_GUIDE",
"serviceTypeName": "地接导游",
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现付",
"amount": "300.00",
"settlementConfirmStatus": "CONFIRMED",
"settlementConfirmStatusName": "已确认",
"remark": "现场临时请的地陪",
"sourceType": "MANUAL",
"sourceTypeName": "手工",
"voucherUrls": ["https://oss.example.com/voucher/a.jpg"]
}
]
},
"msg": "",
"success": true
}
```
### 3.4 查询摄影单层费用明细(GET photographer-fees)
- **使用场景**:打开核单页「摄影」tab 时加载费用明细列表与分类汇总
- **认证 / 幂等 / 限流**:同 3.1
**入参**:同 3.1(`orderId` 路径参数)。
**出参**(`Result<SettlementPhotographerFeesRespVO>`):响应级字段与 3.1 完全一致(`category` 恒为 `PHOTOGRAPHER`),items[] 行字段仅以下差异,其余字段同 3.1:
| 字段 | 类型 | 说明 |
|------|------|------|
| photographerName | String | 摄影姓名(对应导游组的 `name`) |
| feeType | String | 摄影费用类型,取值见 §6.2 |
| feeTypeName | String | 摄影费用类型中文名;feeType 为 null 时为 null |
(行内不再有 `name` / `serviceType` / `serviceTypeName`。)
**示例(典型成功)**
请求:
```
GET /v3/admin/order/12345/settlement/photographer-fees
Authorization: Bearer {admin-token}
(无请求体)
```
响应:
```json
{
"code": 200,
"data": {
"category": "PHOTOGRAPHER",
"totalAmount": "300.00",
"cashPaidAmount": "0.00",
"unconfirmedCount": 0,
"settlementReady": true,
"blockReasonCode": null,
"editable": true,
"readOnlyReasonCode": null,
"items": [
{
"id": "9101",
"staffAssignmentId": "12",
"serviceDate": "2026-08-03",
"photographerName": "摄影甲",
"feeType": "FOLLOW_SHOOT",
"feeTypeName": "跟拍",
"paymentMethod": "SIGNED",
"paymentMethodName": "签单",
"amount": "300.00",
"settlementConfirmStatus": "CONFIRMED",
"settlementConfirmStatusName": "已确认",
"remark": null,
"sourceType": "STAFF_ASSIGNMENT",
"sourceTypeName": "人员安排",
"voucherUrls": []
}
]
},
"msg": "",
"success": true
}
```
首次查询同样返回按人员分配预填的草稿行(不落库,`UNCONFIRMED`),行为同 3.1 边界示例。
### 3.5 全量保存摄影单层费用明细(PUT photographer-fees)
- **使用场景 / 认证 / 幂等 / 限流**:同 3.2
**入参**:结构同 3.2,ItemVO 字段仅以下差异,其余(含 `settlementConfirmStatus`、严格模式、全量替换语义、584120 保护、已确认行编辑自动退回未确认)完全一致:
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| photographerName | String | ❌ | 摄影姓名 | 最长 64 字符 |
| feeType | String | ❌ | 摄影费用类型 | 取值见 §6.2,非法值报 584113 |
(不再有 `name` / `serviceType`;`staffAssignmentId` 非空时角色须为摄影,否则报 584115。)
**示例(典型成功)**
请求:
```json
PUT /v3/admin/order/12345/settlement/photographer-fees
Authorization: Bearer {admin-token}
{
"items": [
{
"id": "9101",
"staffAssignmentId": "12",
"serviceDate": "2026-08-03",
"photographerName": "摄影甲",
"feeType": "FOLLOW_SHOOT",
"paymentMethod": "SIGNED",
"amount": "300.00",
"settlementConfirmStatus": "CONFIRMED"
}
]
}
```
响应:`code=200`,`data` 为保存后的最新全量(结构同 3.4 响应示例)。
**示例(业务失败:金额非法)**
请求:
```json
PUT /v3/admin/order/12345/settlement/photographer-fees
{
"items": [
{
"photographerName": "摄影甲",
"feeType": "FOLLOW_SHOOT",
"amount": "-50.00"
}
]
}
```
响应:
```json
{
"code": 584114,
"data": null,
"msg": "金额必须为 0 至 99999999.99 且最多两位小数",
"success": false
}
```
### 3.6 确认摄影单层费用明细(POST photographer-fees/confirm)
- **使用场景 / 认证 / 幂等 / 限流**:同 3.3
**入参**:同 3.3(`itemIds` 字符串数组,1-200 条;ID 不存在或不属于本订单及摄影角色报 584111;多传字段报 584128,msg 形如 `摄影费用确认请求不支持字段: xxx`)。
**出参**:同 3.4 的响应结构。
**示例(典型成功)**
请求:
```json
POST /v3/admin/order/12345/settlement/photographer-fees/confirm
Authorization: Bearer {admin-token}
{
"itemIds": ["9101"]
}
```
响应:`code=200`,`data` 中该行 `settlementConfirmStatus` 变为 `CONFIRMED`、`unconfirmedCount` 相应减少(结构同 3.4 响应示例)。
## 4. 接口入参
已按接口分散在 §3.1 ~ §3.6 各自小节内(本组为多接口 changelog,入参不单独集中成节)。
## 5. 出参(响应)
已按接口分散在 §3.1 ~ §3.6 各自小节内。
## 6. 枚举 / 数据字典
### 6.1 serviceType(导游服务类型)
**所属字段**:导游组 ItemVO 的 `serviceType` | **类型**:`String` | **必填**:❌
| 值 | 中文 | 说明 |
|----|------|------|
| `FULL_COURSE_GUIDE` | 全陪导游 | — |
| `LOCAL_GUIDE` | 地接导游 | — |
| `COMMENTARY_SERVICE` | 讲解服务 | — |
| `TEMPORARY_SUPPLEMENT` | 临时补录 | — |
### 6.2 feeType(摄影费用类型)
**所属字段**:摄影组 ItemVO 的 `feeType` | **类型**:`String` | **必填**:❌
| 值 | 中文 | 说明 |
|----|------|------|
| `FOLLOW_SHOOT` | 跟拍 | — |
| `PORTRAIT` | 写真 | — |
| `AERIAL_SHOOT` | 航拍 | — |
| `EDITING_DELIVERY` | 剪辑出片 | — |
| `CAMERA_DRONE` | 相机/无人机 | — |
| `OTHER` | 其他 | — |
### 6.3 paymentMethod(付款方式)
**所属字段**:两组 ItemVO 的 `paymentMethod` | **类型**:`String` | **必填**:❌
| 值 | 中文 | 说明 |
|----|------|------|
| `COMPANY_PAID` | 公司支付 | — |
| `CASH_PAID` | 现付 | 计入 `cashPaidAmount` |
| `SIGNED` | 签单 | — |
### 6.4 blockReasonCode(阻断原因码)
**所属字段**:响应级 `blockReasonCode` | **类型**:`String` | **必填**:❌(无阻断时为 null)
| 值 | 中文 | 说明 |
|----|------|------|
| `ORDER_DATE_INCOMPLETE` | 订单日期不完整 | 订单出发或返程日期缺失,无法校验服务日期 |
| `ITEMS_UNCONFIRMED` | 存在未确认明细 | 存在 `UNCONFIRMED` 行(含预填草稿行),不满足提交核单条件 |
> ⚠️ 本次变更**删除**两个旧值:`SOURCE_INACTIVE`、`CANDIDATES_UNRESOLVED`,后端不再返回。
### 6.5 settlementConfirmStatus(核单确认状态)
**所属字段**:两组 ItemVO 的 `settlementConfirmStatus`(入参 + 出参) | **类型**:`String` | **必填**:❌(入参缺省按 UNCONFIRMED)
| 值 | 中文 | 说明 |
|----|------|------|
| `UNCONFIRMED` | 未确认 | 可被全量替换删除 |
| `CONFIRMED` | 已确认 | 不可删除;编辑业务字段自动退回 UNCONFIRMED |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| 584111 | 费用明细不存在或不属于当前订单及角色 | confirm 的 itemIds 含不存在/他单/角色不符的行 |
| 584112 | 服务日期不在订单行程范围内 | 保存时 serviceDate 超出订单出发~返程日期 |
| 584113 | 服务类型或摄影费用类型不合法 | serviceType / feeType 传了枚举外值 |
| 584114 | 金额必须为 0 至 99999999.99 且最多两位小数 | amount 为负数、超限或小数位超过 2 位 |
| 584115 | 人员分配不存在、角色不匹配或来源已失效 | staffAssignmentId 非空但不属本单,或角色不匹配(导游组传了摄影人员等) |
| 584120 | 已确认的有效费用不能直接删除,请先进入编辑状态 | 全量保存时遗漏了库里已 CONFIRMED 的行 |
| 584121 | 单个导游或摄影分类最多 200 条明细 | items 超过 200 条 |
| 584123 | 订单出发或返程日期缺失,无法校验服务日期 | 保存时订单日期不完整且传了 serviceDate |
| 584125 | 导游或摄影费用请求字段不合法 | 请求体缺失或整体解析失败(无具体字段原因时) |
| 584128 | 导游或摄影费用请求字段不合法:{0} | 请求体(顶层或行内)出现未定义字段(含已删除的旧字段 candidateKey / completionState / sourceResolution / excludedCandidateKeys);msg 带具体字段名 |
## 8. 示例(典型 / 边界 / 异常)
已按接口分散在 §3.1 ~ §3.6 各自小节内,每组示例均含请求 + 响应:
- 典型成功:§3.1 / §3.2 / §3.3 / §3.4 / §3.5 / §3.6
- 边界(首次查询预填草稿行,id/serviceDate/amount 为 null):§3.1 第二组示例
- 业务失败(584128 旧字段误传 / 584120 已确认行误删 / 584114 金额非法):§3.2 / §3.5
## 9. 业务边界
- ✅ **适用场景**:订单核单进行中(editable=true)时可调保存/确认接口;查询接口在核单各阶段均可调(只读阶段 editable=false + readOnlyReasonCode 指明原因)
- ❌ **不适用场景**:订单已提交核单终态后,保存/确认接口被拒(editable=false 时调用按只读规则拦截);房务角色(house)调本组接口返回 403
- ⚠️ **特殊边界 1(预填草稿行)**:首次 GET 返回的草稿行不落库(id 为 null),但计入 `unconfirmedCount` 影响 `settlementReady` 展示;而「完成核单」的提交门禁只查已落库行,两个口径有意分离(展示口径 ≠ 提交口径)
- ⚠️ **特殊边界 2(已确认行保护)**:已 CONFIRMED 行想删除,必须先修改该行业务字段使其退回 UNCONFIRMED(或走反确认流程),再在下一轮全量保存中不传该行
- ⚠️ **特殊边界 3(金额 0)**:amount 允许 `0.00`,不被非负校验拦截
## 10. 修改前后对比
### 10.1 字段级对比
保存入参(PUT,ItemVO 行内):
| 字段 | 改前 | 改后 |
|------|------|------|
| candidateKey | 有,候选行标识 | **删除**,传了报 400(584128) |
| completionState | 有(NEEDS_INPUT/COMPLETE/EXCLUDED) | **删除**,传了报 400(584128) |
| sourceResolution | 有,来源处理结果 | **删除**,传了报 400(584128) |
| settlementConfirmStatus | 无 | **新增**,非必填,UNCONFIRMED/CONFIRMED,缺省按 UNCONFIRMED |
保存入参(PUT,请求体顶层):
| 字段 | 改前 | 改后 |
|------|------|------|
| excludedCandidateKeys | 有,排除候选 key 清单 | **删除**,传了报 400(584128) |
查询出参(GET,ItemVO 行内):
| 字段 | 改前 | 改后 |
|------|------|------|
| candidateKey | 有 | **删除**,不再下发 |
| completionState | 有 | **删除**,不再下发 |
| candidateResolution | 有(INCLUDED/EXCLUDED/UNRESOLVED) | **删除**,不再下发 |
| sourceActive | 有 | **删除**,不再下发 |
| settlementConfirmStatus / settlementConfirmStatusName | 有(仅展示) | 保留,语义不变 |
查询出参(GET,响应级):
| 字段 | 改前 | 改后 |
|------|------|------|
| pendingCandidateCount | 有,未处理候选数 | **删除**,不再下发 |
| blockReasonCode 值域 | 含 `SOURCE_INACTIVE`、`CANDIDATES_UNRESOLVED` | **删除这两个值**,只保留 `ORDER_DATE_INCOMPLETE`、`ITEMS_UNCONFIRMED` |
### 10.2 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 保存语义 | 候选机制:行按 candidateKey 对齐候选,配合 excludedCandidateKeys 声明排除 | **全量替换**:items 即保存后的全部行,未传的未确认行删除 |
| 确认入口 | 只能通过 confirm 接口把 INCLUDED 行置已确认 | confirm 接口保留;**保存时也可直接置 CONFIRMED**(settlementConfirmStatus 入参) |
| 已确认行删除 | 通过 excludedCandidateKeys 排除 | 不可删:全量保存遗漏已确认行报 584120 |
| 已确认行编辑 | 编辑后走候选重算 | 编辑业务字段**自动退回 UNCONFIRMED**,需重新确认 |
| unconfirmedCount 口径 | 未确认的 INCLUDED 明细数 | 未确认明细数(全行口径,含预填草稿行) |
| 未处理候选 | 前端要处理 UNRESOLVED 候选(pendingCandidateCount > 0 阻断提交) | 机制删除,无此概念 |
| 旧字段容错 | 未知字段按 Jackson 默认处理 | **严格模式**:任何未定义字段(含全部旧字段)一律 400 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:**是**。保存接口传旧字段(candidateKey / completionState / sourceResolution / excludedCandidateKeys)从「生效」变为「一律 400」;查询出参删 5 个字段;blockReasonCode 删 2 个枚举值。
- **前端是否必须同步上线**:**是**。导游/摄影 tab 的保存请求必须清掉旧字段,否则保存全部 400;出参侧引用 candidateKey / completionState / candidateResolution / sourceActive / pendingCandidateCount 的渲染与逻辑必须同步删除。
### 11.2 回滚方案
- **回滚方式**:后端 revert PR #6119 即可恢复旧候选机制契约;无 DDL、无数据迁移,回滚不涉及数据清理
- **前端配合**:前端若已按新契约上线,后端回滚时需同步回退前端版本(新旧契约互不兼容)
## 12. 注意事项
- **前端 workaround 清理点**:此前为候选机制写的 workaround 可全部删除——按 candidateKey 对齐行的本地映射、excludedCandidateKeys 的收集逻辑、对 candidateResolution=UNRESOLVED 行的特殊渲染、等待 pendingCandidateCount 归零的轮询/重试逻辑
- **行内确认状态直接用 settlementConfirmStatus**:新模型下「行是否已确认」只看 `settlementConfirmStatus`,不要再拼接 completionState + candidateResolution 推断
- **保存即全量**:局部更新场景也必须先 GET 拿全量、改完整体 PUT,缺行等于删行(未确认行)
- **ID 一律字符串**:入参 id / staffAssignmentId / itemIds 均传字符串形式;出参 id / staffAssignmentId 也是字符串,不要按 Number 解析
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#6117](https://git.1814.love:8443/wx/HL/issues/6117)
- **PR**: [#6119](https://git.1814.love:8443/wx/HL/pulls/6119)
- **Merge commit**: [27ac77b500](https://git.1814.love:8443/wx/HL/commit/27ac77b500ff53496432a0e6e869dc96669e8f24)
### 13.2 联系人
- **后端负责人**: @yst(腰苏图)
@@ -0,0 +1,70 @@
---
schema: "hl-changelog/v2"
ticket: "6125"
title: "房务工作台订单详情抽屉透出终止未用标记+异常原因(字段补传)"
consumer: "admin"
author: "wx"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "8a2c26af"
target_release: ""
verified_at: "2026-08-21"
status_note: "#6111 的未用标记/异常原因/行级释放只落到了订单详情页(order-v2 RoomArrangeCard),房务实际工作页 /housekeeper/orders 抽屉走另一条接口 /admin/house/orders/{orderId},本次把同口径字段补到该接口。前端在 ItineraryPanel.vue 渲染即可,口径照抄 9172700f。"
updated_at: "2026-08-21"
base: "dev-v3"
generated: "2026-08-21T15:45:00+08:00"
---
# 房务工作台订单详情抽屉透出终止未用标记+异常原因(#6125)
## 背景
#6111 的「未用房晚标记 + 异常原因 + 行级释放」只覆盖订单详情页(`/v3/admin/order/{id}/itinerary` → `RoomArrangeCard.vue`)。房务实际工作页是**房务工作台 `/housekeeper/orders` 的订单详情抽屉**,它走 `GET /admin/house/orders/{orderId}`,此前无这些字段。本次后端补齐,前端渲染后即可在房务工作台看到。
## 变更接口
| 接口 | 变更 |
| --- | --- |
| `GET /admin/house/orders/{orderId}` | **响应新增可选字段**:① `itinerary[].assignments[].unusedForTerminate`(Boolean)/ `unusedLabel`(String,「未用」);② `progress.exceptionReason` / `exceptionReasonLabel`(String) |
## 取值说明(TEST 实测)
- 终止/异常单(`progress.houseStatus === "EXCEPTION"`):
- `progress.exceptionReason` / `exceptionReasonLabel` = 终止原因(如「#P1 行程中终止测试-客户身体原因」)
- 被勾选未用的房晚行:`assignments[].unusedForTerminate === true`、`unusedLabel === "未用"`;未命中行 `unusedForTerminate === false`
- 正常单:`exceptionReason` 为 `null`,各行 `unusedForTerminate`/`unusedLabel` 为 `null`
实测样例(终止单 26-9250 / `2090093100083904514`):
```json
{
"progress": { "houseStatus": "EXCEPTION", "exceptionReason": "#P1 行程中终止测试-客户身体原因" },
"itinerary": [{ "dayNumber": 1, "assignments": [{ "assignmentId": "2090093115967733761", "unusedForTerminate": true, "unusedLabel": "未用" }] }]
}
```
## 前端动作(房务工作台抽屉,ItineraryPanel.vue / OrderDetailModal.vue)
1. **未用标记**:行 `unusedForTerminate === true` 时渲染 `unusedLabel` 标记(样式照抄 order-v2 `RoomArrangeCard.vue`,commit `9172700f`)。
2. **异常原因**:`progress.houseStatus === "EXCEPTION"` 且 `exceptionReason` 非空时,在「异常」徽标旁展示原因文本。
3. **行级释放按钮**:未用行展示「资源释放」按钮,确认后调既有 `DELETE /v3/admin/order/assignments/{assignmentId}`(软删+恢复库存;`orderV2.js` 已有 `deleteOrderAssignment`,房务角色可调,本单已回归验证)。成功后刷新抽屉。
4. 正常单上述区域一律不渲染(字段为 null)。
## 验证证据
- 单测:`HouseDetailAggregatorTest` 新增终止单/正常单用例,定向 176 例全绿;契约审查 additive-only 通过。
- 部署:hl-order-service-v3(8086/8186)已滚动部署 TEST(task 5410dada)。
- 网关实测(TEST 9443,房务 ROOM_MANAGER 角色 token):
1. 终止单 26-9250 两行 `unusedForTerminate=true`/`unusedLabel=未用`、`progress.exceptionReason` 有值 ✅
2. 正常单字段全 null 无报错 ✅
3. 行级释放回归:`DELETE /v3/admin/order/assignments/2090082479703498753` → 200,查库 deleted_at 软删生效,测试后已 DB 还原 ✅
## 关联
- Issue: [#6125](https://git.1814.love:8443/wx/HL/issues/6125)
- PR: [#6131](https://git.1814.love:8443/wx/HL/pulls/6131)
- Merge commit: [77be1f27a](https://git.1814.love:8443/wx/HL/commit/77be1f27a)
- 前序:#6111 / PR #6116(订单详情页侧);前端参照实现 `hl-ui@9172700f`
@@ -0,0 +1,55 @@
---
schema: "hl-changelog/v2"
ticket: "6140"
title: "房务操作日志补「订单异常」留痕 + CLOSE 类型徽标中文化"
consumer: "admin"
author: "wx"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
verified_at: "2026-08-22"
status_note: "房务工作台订单详情「操作日志」Tab:①订单进异常态(行程中终止/取消)现在有留痕,含发生时间;存量异常单靠读侧合成兜底,前端无需改代码;②CLOSE 类型徽标中文化(异常关闭)。前端只需确认列表正常展示新 opType。"
updated_at: "2026-08-22"
base: "dev-v3"
generated: "2026-08-22T09:40:00+08:00"
---
# 房务操作日志补「订单异常」留痕 + CLOSE 类型徽标中文化(#6140)
## 背景
房务工作台订单详情抽屉「操作日志」Tab 三个问题(2026-08-21 wx 截图实证,TEST 订单 26-9250):
1. 订单「什么时候异常的」没有日志——只有「异常处置标记完成」,找不到进入异常态的那条;
2. CLOSE 类型徽标显示英文原文(根因:#4307 加了 `HouseOpType.CLOSE` 常量但漏放 `HouseOpTypeLabel` 中文映射,读侧回退原值);
3. 系统(事件驱动)操作不落 `house_operation_log`。
## 变更接口
| 接口 | 变更 |
| --- | --- |
| `GET /v3/admin/house/orders/{orderId}/operation-log`(操作日志列表) | **新增 opType 枚举值 `EXCEPTION`(订单异常)**;`opTypeLabel` 新增映射:`CLOSE→异常关闭`、`EXCEPTION→订单异常` |
无新端点、无字段结构变更;前端按 `opTypeLabel` 渲染的列表无需改代码即可看到新条目。
## 新条目说明
- **写侧落表**(本功能上线后的新终止/取消单):行程中终止或订单取消使订单进房务异常态(已配房分支)时,同事务写一条 `opType=EXCEPTION` 日志:`operatorName=系统`,`summary` 形如「系统 订单异常·行程中终止 · 原因:xxx · 释放未用 N 行」,`time`=终止/取消事件处理时间;按 `orderId+EXCEPTION` 查重,事件重投不产生重复条目。
- **读侧合成兜底**(存量异常单):已终止但无写侧 EXCEPTION 行的订单(如 26-9250),查询时从终止退款数据合成一条「订单异常」项(`time`=终止处理时间,`id=null`),与写侧按订单去重不双计。
- **summary 聚合**:合成项按各自 opType 计数(REQUIREMENT_SUBMIT 口径不变,EXCEPTION 至多 +1)。
口径说明:写侧「释放未用 N 行」记实际释放的未确认候选行数;读侧合成取终止退款明细勾选的未用房晚数——勾选行含 CONFIRMED 被跳过时两值不同,属预期。
## 前端动作
无强制改动。建议确认:操作日志列表/筛选器对新增 `EXCEPTION` 类型正常展示(徽标文案直接用后端 `opTypeLabel`,勿前端硬编码映射)。
## 验证证据
- 单测:`HouseOpTypeLabelTest`(反射防漏)、`HouseTodoServiceTest` +3、`HouseHotelViewServiceTest` +3 全绿;`HouseModuleBoundaryArchTest` 通过。
- 房务写路径 × 日志落痕全覆盖审计清单已随工单 #6140 评论归档(SYS_RECLAIM/询房写口功能未上线已注明;SWAP_HOTEL/SWAP_FAILED 缺口另立工单跟踪)。
- TEST 网关实测:见工单验收评论。
@@ -0,0 +1,186 @@
---
schema: "hl-changelog/v2"
ticket: "6152"
title: "车务派单异常态——已派车后取消进异常桶,处置完成转已完结"
consumer: "admin"
author: "wx"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "36261d83"
target_release: ""
verified_at: "2026-08-23"
status_note: "车务派单看板/详情:①已派车后取消的订单不再落「已取消」消失,改进新「异常」桶等人工处置;②详情已取消/异常槽位行补 serviceDate/startDate/endDate 三字段(根治行级释放占用解析服务日失败误拦截);③操作时间线新增「订单异常」「异常处置完成」系统留痕;④新增处置完成写口 POST /admin/fleet/assignments/resolve-exception。前端需适配异常分面 Tab + 详情异常行操作入口。"
updated_at: "2026-08-22"
base: "dev-v3"
generated: "2026-08-22T17:20:00+08:00"
---
# 【新增接口·管理后台】车务派单异常态——已派车后取消进异常桶,处置完成转已完结(#6152)
> **PR**: #6177(功能)+ #6178(complete_source 列宽修复) | **服务**: hl-fleet-service | **作者**: wx | **更新时间**: 2026-08-22
## 背景
现网案例:订单 26-9250(行程中中止,出团 2 天后取消)。派单看板详情显示「暂无逐日逐车方案 / 暂无实际用车费用」,已派好的车辆/司机明细完全看不到;且车辆/司机占用残留需人工释放,行级释放对已取消全程槽行报错「缺少该派单的服务日期」(前端解析服务日 7 种来源在已取消行上全空)。
**业务口径(wx 2026-08-22 拍板)**:
- 「已取消」分面只装**未派车即取消**的单;
- 凡**已派车(holding/assigned)后取消**(出行前取消/订单整体取消/行程中终止)一律进**异常桶**,保留车辆/司机占用,等车务人工处置;
- 异常处置完成后流转到「已完结」终态(对标房务异常桶「标记完成」)。
## 变更接口
### 新增端点
`POST /admin/fleet/assignments/resolve-exception`(异常处置完成):把该订单全部异常派单行转已完结。入参 `orderId`(整单级处置)+ `requestId`(幂等);前置门禁:车辆/司机占用须先经「一键清除已取消派单的占用」释放,否则 605072 拦回;无异常行幂等返回 0。
**请求**:
```json
{
"orderId": "2086697882311680002",
"requestId": "resolve-exception-20260822-001"
}
```
**响应(成功,处置 N 行)**:
```json
{
"code": 200,
"message": "成功",
"data": {
"resolvedCount": 4,
"assignmentIds": ["2086811469462011905", "2086811469466206209", "2086811469466206210", "2086811469470400514"]
},
"traceId": null,
"success": true
}
```
**响应(幂等重放/无异常行)**:`data` 为 `{"resolvedCount": 0, "assignmentIds": []}`,同样 code=200。
**响应(占用未清拦回)**:
```json
{
"code": 605072,
"message": "异常派单的车辆/司机占用尚未释放,请先释放资源再处置完成",
"data": null,
"traceId": null,
"success": false
}
```
**错误码**:`605072`(占用未释放)、`605073`(异常态写口锁——异常行拒绝确认/改派/拒接/软清/提前完结等推进类写操作)。
### 修改端点
#### `GET /admin/fleet/board/summary`(看板汇总)
`statusOptions` 新增第 6 项、`statusCounts` 新增 `exception` 计数。响应片段(实测):
```json
{
"statusCounts": {"unassigned": 52, "holding": 1, "assigned": 20, "completed": 1, "canceled": 12, "exception": 1, "unassignedUrgent": 44, "holdingUrgent": 1},
"statusOptions": [
{"value": "canceled", "label": "已取消", "description": "派车需求已取消(仅未派车即取消,#6152)", "count": 12, "urgentCount": 0},
{"value": "exception", "label": "异常", "description": "已派车后再取消,待人工处置(释放资源后处置完成)", "count": 1, "urgentCount": 0}
]
}
```
(statusOptions 完整共 6 项:待派车/排车中/已派车/已完结/已取消/异常,上表仅示后两项。)
#### `GET /admin/fleet/board/orders`(看板列表)
`statuses` 筛选参数新增合法值 `exception`;混合取消订单(部分行已派落异常+部分未派占位落取消)的代表状态= exception(落异常桶);每条记录 `assignmentSlots[]` 槽位行**新增 `serviceDate`/`startDate`/`endDate` 三字段**。槽位行片段(实测):
```json
{
"assignmentId": "2086811469462011905",
"assignmentStatus": "exception",
"serviceDate": null,
"startDate": "2026-08-27",
"endDate": "2026-08-27",
"vehiclePlate": "蒙A-H7777",
"driverName": "阿木古愣"
}
```
逐日切片行 `serviceDate` 有值;全程槽行 `serviceDate=null`,用 `startDate`~`endDate` 展开解析服务日。
#### `GET /admin/fleet/board/orders/{orderId}`(看板详情)
`vehicleSlots[]` 的 `slotStatus` 新增值 `exception`(`slotStatusLabel`=「异常」),身份字段保留展示不清空并带 `cancelReason`。槽位行片段(实测):
```json
{
"assignmentSlotId": "345201285773004800",
"slotStatus": "exception",
"slotStatusLabel": "异常",
"cancelReason": "6152异常桶验收-5784测试单",
"assignmentId": "2086811469462011905",
"serviceStartDate": "2026-08-28",
"serviceEndDate": "2026-08-31",
"serviceDays": 4,
"vehiclePlate": "蒙A-G8888",
"vehicleModel": "别克GL8",
"driverName": "满都拉",
"driverPhone": "135****5003",
"canDelete": true
}
```
(详情槽位用的是 `serviceStartDate`/`serviceEndDate` 字段名,列表槽位是 `startDate`/`endDate`,勿混用。)
#### `GET /admin/fleet/orders/{orderId}/operation-log`(派单操作时间线)
新增 `opType` 枚举值。新条目片段(实测):
```json
{
"id": "2091082889029419011",
"time": "2026-08-22 16:39:46",
"opType": "exception_marked",
"opTypeLabel": "订单异常",
"summary": "系统 订单异常:出行前取消,原因:6152异常桶验收-5784测试单",
"operatorName": "系统",
"effectiveDate": null,
"detailJson": "{\"scene\":\"出行前取消\",\"reason\":\"6152异常桶验收-5784测试单\",\"exceptionRowCount\":4}"
}
```
`exception_resolved` 条目:`opTypeLabel`=「异常处置完成」,summary 形如「admin 异常处置完成:共 4 行派单转已完结」,`detailJson` 为 `{"resolvedCount":4}`。存量已派后取消单读侧合成兜底(scene 判定:有终止退款标记=行程中终止 > order_cancelled=订单取消 > 出行前取消)。
#### `DELETE /admin/fleet/assignments/{assignmentId}`(取消派单)
已派车(holding/assigned)行取消后目标态从 canceled 改为 **exception**,响应 `data.assignmentStatus="exception"`、`sideEffects.vehicleStatusUpdated/driverStatusUpdated` 仍为 `"busy"`(保留占用);未派占位行维持 canceled。其余响应结构不变。
#### `POST /admin/fleet/assignments/clear-cancelled-occupancy`(一键清除占用)
圈定集合从已取消扩为已取消+异常;异常行的释放= 异常桶里的「释放资源」动作。响应结构不变(`vehicleDriverPairs`/`intentsWritten`/`assignmentIds[]`,实测 `vehicleDriverPairs:2, intentsWritten:2`)。
## 行为语义变化(前端需知)
1. **取消已派车的派单不再释放车辆/司机占用**:取消后车/司机仍 busy,须走「一键清除已取消派单的占用」释放后才能「处置完成」转已完结。推荐操作流:取消(自动进异常桶)→ 看板异常 Tab 找到单 → 详情释放占用(整单级或行级)→ 处置完成(resolve-exception)→ 单进已完结。
2. **「已取消」Tab 单量会减少**:已派车后取消的单从该 Tab 移出进「异常」Tab,属预期口径变化。
3. **详情已取消/异常行现在能解析服务日**:`serviceDate`/`startDate`/`endDate` 三字段补齐后,行级释放占用不再被前端解析盲区误拦截(#6114 修复)。
4. **撤销取消(restore-cancel)支持异常行**:误操作可撤销回原状态。
5. 保险退保、需求重开等取消副作用链路对异常行照常触发(占用保留语义只影响车/司机缓存态)。
## 前端动作
1. 看板新增「异常」Tab/分面(数据已在 statusOptions 返回,value=exception,红色系建议);
2. 详情槽位行(异常态)展示身份字段 +「释放占用」「处置完成」操作入口(按 slotStatus/assignmentStatus=exception 自判,与已取消行同模式);
3. 行级释放占用改用 serviceDate(null 时 startDate~endDate 展开)解析服务日;
4. 操作时间线渲染新 opTypeLabel(订单异常/异常处置完成),徽标文案直接用后端 label 勿硬编码。
## 验证证据
- 单测:3854 tests 全绿(含状态机 12 转移/看板分面/Mapper 圈定/resolve-exception 5 例)+ ArchTest 门禁 + spotless。
- TEST 网关实测(订单 2086697882311680002,验 5784A 测试单):取消 assigned 派单 → `assignmentStatus="exception"` 且车/司机保持 busy ✓;异常分面 count=1 ✓;resolve-exception 占用未清 605072 拦回 ✓;异常行 soft-clear 605073 拦截 ✓;操作时间线 `exception_marked` 留痕(系统·出行前取消+原因)✓;详情异常槽位身份字段保留 + serviceDate/startDate/endDate 补齐 ✓;清占用后车辆转 idle ✓;**处置完成闭环**:resolve-exception 成功转 4 行已完结(resolvedCount=4)✓、异常分面归 0 ✓、`exception_resolved` 留痕(共 4 行派单转已完结)✓、同 requestId 重放幂等返 0 ✓、终态行写口闸回归正常(605043)✓。
- 列宽修复:complete_source VARCHAR(16)→32(EXCEPTION_RESOLVE 17 字符落库超长,网关实测发现,PR #6178 已合并随本单部署)。
@@ -0,0 +1,43 @@
---
schema: "hl-changelog/v2"
ticket: "6155"
title: "补全出行信息:身份证国籍/民族自动兜底,非身份证需前端增加输入框"
consumer: "admin"
author: "wx"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "8569529c"
target_release: ""
verified_at: "2026-08-22"
status_note: "订单详情「补全出行信息」表单:身份证(ID_CARD)出行人提交时国籍/民族传空字符串不再被 581101 拒绝,后端自动兜底为「中国/汉族」;非身份证证件(护照等)仍要求国籍/民族非空,前端需在证件类型非身份证时增加国籍/民族输入框。"
updated_at: "2026-08-22"
base: "dev-v3"
---
# 补全出行信息:身份证国籍/民族自动兜底,非身份证需前端增加输入框(#6155)
## ⚠️ 关键变化
- **之前**:`PUT /v3/admin/orders/{id}/traveler-info`(补全出行信息)与 `POST /v3/admin/orders/{id}/traveler/batch-edit`(批量编辑)对国籍/民族**空字符串一律抛 581101**(12301 必报字段缺失)。前端表单无国籍/民族输入框,身份证出行人补全提交被拒。
- **现在**:证件类型 = 身份证(ID_CARD)时,国籍/民族空字符串放行,后端兜底为「中国/汉族」落库;**证件类型 ≠ 身份证时维持原行为**(空串仍抛 581101)。
## 变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 补全出行信息 | PUT | `/v3/admin/orders/{id}/traveler-info` | 校验行为变更 | ID_CARD 时 nationality/race 空串不再抛 581101,兜底「中国/汉族」;非身份证仍抛 |
| 2 | 批量编辑出行人 | POST | `/v3/admin/orders/{id}/traveler/batch-edit` | 校验行为变更 | 同上(两处共用 `validate12301Fields`,行为一致) |
## 前端动作(非身份证场景)
- 身份证场景:**前端无需改动**,空串提交即可,后端兜底。
- 非身份证(护照等)场景:**前端需增加国籍/民族输入框**,用户显式填写后再提交,否则仍报 581101。
- 字段说明:`nationality`(国籍)、`race`(民族)传空字符串时——身份证兜底「中国/汉族」;非身份证报 581101「12301 必报字段缺失(国籍 / 民族不能为空字符串)」。
## 验证证据
- 单测:`TravelerServiceTest` +3(身份证空串放行 / 非身份证 nationality 空串仍抛 / 非身份证 race 空串仍抛)、`TravelerConverterTest` +4(新增空串兜底 / 新增 null 兜底 / 更新空串不覆盖原值 / 更新 null 不覆盖原值),全绿。
- TEST 网关实测:见工单 #6155 验收评论。
@@ -0,0 +1,175 @@
---
schema: "hl-changelog/v2"
ticket: "6168"
title: "导游/摄影核单确认状态去签名重置——编辑不再自动退回待确认"
consumer: "admin"
change_type: "修改接口"
author: "yst"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: "2026-08-22"
status_note: "PR #6169 已合并 dev-v3(merge commit 22a09e556cfa),测试服已行为级验证。仅行为语义变化、字段签名不变:导游/摄影核单删除「编辑已确认行自动重置确认状态」的业务签名机制,保存时确认状态完全由入参 settlementConfirmStatus 决定(传啥存啥),与酒店/门票等其它费用 tab 行为拉齐。入参/出参字段、枚举值均无变化,无 DDL。"
updated_at: "2026-08-22"
base: "dev-v3"
---
# 【修改接口·管理后台】导游/摄影核单确认状态去签名重置——编辑不再自动退回待确认(#6168)
> **PR**: #6169 | **服务**: hl-order-service-v3 | **更新时间**: 2026-08-22
## 1. 接口背景
上一版(#6117 / PR #6119)把导游/摄影核单拉齐到「全量替换 + 统一 settlementConfirmStatus」时,给这两个 tab **额外**加了一道其它费用 tab 没有的「**业务签名重置**」机制:
> 后端把每行的 `staffId / 金额 / remark / voucherUrls / detail(日期/类型)` 等字段拼成一个"业务签名",**只要保存时签名和库里有任何差异(哪怕只是改了备注 remark 或传了凭证),就无视入参里显式传的 `settlementConfirmStatus: "CONFIRMED"`,强制把该行确认状态重置回 `UNCONFIRMED`(待确认)**。
这带来两个问题:
1. **入参被忽略**——前端/用户明明传了「已确认」,仅因顺手改了备注,状态就被后端悄悄打回「待确认」,语义反直觉;
2. **跨 tab 行为不一致**——酒店/门票/餐饮/车辆/补贴/其它收支等 tab 保存时是「**传啥存啥**」(确认状态原样落库,无任何签名校验),唯独导游/摄影多一道隐式重置闸,前端对接逻辑要区别对待。
本次变更**删除该签名重置机制**,导游/摄影确认状态逻辑与其它 6 个费用 tab 完全拉齐。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 保存导游单层费用明细 | PUT | `/v3/admin/order/{orderId}/settlement/guide-fees` | 修改接口 | 行为变化:编辑已确认行不再自动重置确认状态 |
| 2 | 保存摄影单层费用明细 | PUT | `/v3/admin/order/{orderId}/settlement/photographer-fees` | 修改接口 | 同上 |
> ⚠️ **字段签名零变化**:入参 / 出参字段、`settlementConfirmStatus` 枚举值(`UNCONFIRMED`/`CONFIRMED`)、`unconfirmedCount` / `settlementReady` / `blockReasonCode` 等聚合字段**全部不变**。变的只有「编辑已确认行时确认状态是否被自动重置」这一个**行为语义**。
## 3. 接口详情
两个保存接口均为「全量替换」语义(与 #6117 一致,本次未改),传入当前应存在的全部行。
## 4. 入参
无变化。每行 `settlementConfirmStatus` 仍为非必填,枚举 `UNCONFIRMED` / `CONFIRMED`。
## 5. 出参
无变化。
## 6. 确认状态落库规则(本次唯一变化点)
### 修改前(旧:签名重置)
| 场景 | 落库确认状态 |
|---|---|
| 新增行,入参 null | `UNCONFIRMED`(默认) |
| 新增行,入参有值 | 入参值 |
| 编辑行,**业务签名有变化**(改了 remark/金额/日期/凭证等任一) | **强制 `UNCONFIRMED`,无视入参** ⚠️ |
| 编辑行,签名未变,入参 null | 保留现值 |
| 编辑行,签名未变,入参有值 | 入参值 |
### 修改后(新:传啥存啥,与酒店/门票一致)
| 场景 | 落库确认状态 |
|---|---|
| 新增行,入参 null | `UNCONFIRMED`(默认) |
| 新增行,入参有值 | 入参值 |
| 编辑行,入参 null | **保留现值**(不再重置) |
| 编辑行,入参有值 | **入参值**(不再重置) |
**核心差异**:删掉了「编辑已确认行 → 业务签名变化 → 强制重置 UNCONFIRMED」这条分支。现在编辑任何字段(含备注 remark、凭证 voucherUrls、金额、日期)都**不会**触碰确认状态,确认状态完全由入参决定。
## 7. 错误码
无新增 / 无删除。`settlementConfirmStatus` 非法值仍报 `SETTLEMENT_CONFIRM_STATUS_INVALID`。
## 8. 示例
### 典型:编辑已确认行的备注,状态保持已确认(新行为)
请求:
```http
PUT /v3/admin/order/{orderId}/settlement/guide-fees
Content-Type: application/json
{
"items": [
{
"id": "2090634300167766018",
"serviceDate": "2026-08-29",
"name": "李雪梅",
"serviceType": "FULL_COURSE_GUIDE",
"paymentMethod": "CASH_PAID",
"amount": "100.00",
"remark": "已确认行-改了备注",
"voucherUrls": [],
"settlementConfirmStatus": "CONFIRMED"
}
]
}
```
响应(关键:status 保持 CONFIRMED,**旧逻辑会被重置成 UNCONFIRMED**):
```json
{
"code": 200,
"data": {
"items": [
{
"id": "2090634300167766018",
"name": "李雪梅",
"remark": "已确认行-改了备注",
"settlementConfirmStatus": "CONFIRMED",
"settlementConfirmStatusName": "已确认",
"amount": "100.00"
}
],
"unconfirmedCount": 0,
"settlementReady": true,
"blockReasonCode": null
},
"success": true
}
```
### 边界:编辑行不传 settlementConfirmStatus(null)→ 保留现值
若该行原状态为 `CONFIRMED`,本次保存入参不传 `settlementConfirmStatus`(或传 null),保存后仍为 `CONFIRMED`。
### 异常:settlementConfirmStatus 传非法值
```json
{ "code": 584xxx, "message": "确认状态非法", "success": false }
```
(传 `UNCONFIRMED` / `CONFIRMED` 之外的值时报 `SETTLEMENT_CONFIRM_STATUS_INVALID`)
## 9. 业务边界
- 「已 CONFIRMED 行不能被全量替换顺手删除」的守卫**保留不变**(删已确认行仍报 `STAFF_FEE_CONFIRMED_DELETE_FORBIDDEN`);本次只删「编辑重置」,没删「禁删」。
- 若业务上确实需要「改金额后强制重确认」,由**前端在编辑关键字段后主动把该行 `settlementConfirmStatus` 传 `UNCONFIRMED`** 来实现,后端不再隐式代劳。
## 10. 修改前后对比
| 维度 | 修改前 | 修改后 |
|---|---|---|
| 编辑已确认行(改备注/金额等) | 确认状态强制退回 UNCONFIRMED,入参 CONFIRMED 被忽略 | 确认状态按入参/现值,不自动退回 |
| 与其它费用 tab 一致性 | 导游/摄影独有一道签名重置闸 | 6+ 个 tab 行为完全统一 |
| 前端对接 | 需预知「编辑会被重置」的隐式行为 | 传啥存啥,无需特殊处理 |
## 11. 影响评估 / 回滚
- **影响面**:仅导游/摄影两个保存接口的确认状态落库语义;字段契约不变,前端无需改字段映射。
- **前端注意**:若此前针对「编辑已确认行会自动退回待确认」做过 UI 提示(如编辑后弹"需重新确认"),该提示触发条件已不存在,建议移除。
- **回滚**:`git revert 22a09e556cfa`(merge commit)即可恢复签名重置机制;无 DDL,纯代码。
## 12. 注意事项
- 本变更不改变任何字段名 / 类型 / 枚举值,OpenAPI(Knife4j)文档无需改动字段定义。
- 测试服已行为级验证:编辑已确认行(改 remark)+ 显式传 `CONFIRMED`,GET 复查保持 `CONFIRMED`、`unconfirmedCount=0`、`settlementReady=true`。
## 13. 关联 / 联系人
- Issue: https://git.1814.love:8443/wx/HL/issues/6168
- PR: https://git.1814.love:8443/wx/HL/pulls/6169
- merge commit: https://git.1814.love:8443/wx/HL/commit/22a09e556cfa
- 前序重构: Issue #6117 / PR #6119
- 负责人: yst
@@ -0,0 +1,56 @@
---
schema: "hl-changelog/v2"
ticket: "frontend-fleet-clear-cancelled-occupancy-row"
title: "行级「释放占用」对已取消全程槽行误报「缺少该派单的服务日期」"
consumer: "admin"
author: "kimi(wx)"
change_type: "前端缺陷"
backend_status: "not_required"
gateway_status: "not_required"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "9871cbb1"
target_release: ""
verified_at: "2026-08-22"
status_note: "前端缺陷:已取消的全程槽派单行(整单取消场景)行级「释放占用」解析服务日失败被误拦截,报「缺少该派单的服务日期,请刷新详情后重试」且刷新无效。建议解析为空时降级整单级释放(后端向后兼容)或该行隐藏行级按钮。根治在 #6152(后端详情将给已取消行补返起止日期)。临时绕路:用卡片顶「释放车辆/司机占用」整单级入口,功能正常。"
updated_at: "2026-08-22"
base: "dev-v3"
generated: "2026-08-22T18:30:00+08:00"
---
# 行级「释放占用」对已取消全程槽行误报「缺少该派单的服务日期」
> 前端缺陷,待前端修复。后端无问题(整单级释放入口功能正常,接口本身向后兼容)。
## 现象(TEST 实测,订单 26-9250)
订单行程中中止(整单取消),派单看板 → 订单详情 → 车辆执行段 → 已取消行内点「**释放占用**」(行级,#6114):
- 报错「缺少该派单的服务日期,请刷新详情后重试」,刷新详情后重试仍报同样错误(用户连点 4 次均失败)。
- 卡片顶部「**释放车辆/司机占用**」(整单级,#6107)**功能正常**,可作为临时绕路。
## 根因(前端)
`src/views/fleet/board/index.vue` `onClearCancelledOccupancyRow`:行级释放必须先解析该行的服务日期数组(先取 `assignment.serviceDate`,否则走 `resolveVehicleFeeServiceDates` 的 7 种来源:dailyVehiclePlan / dailyVehicleFees / chargeable+freeServiceDates / serviceDates / serviceDateSegments / itinerary.days / startDate~endDate),解析为空则拦截不发请求。
本条已取消派单是**全程槽行**(`service_date` 为 NULL),且已取消行按 #5828 读侧精简(身份字段置空、仅回显 `cancelled*` 快照,快照不含日期)——7 种日期来源在该行全部为空,于是误拦截。
#6114 实现时只覆盖了「终止勾选未用天」场景(行上带 serviceDates),未覆盖「整单取消的全程槽行」场景。
## 后端事实(无需改动)
`POST /admin/fleet/assignments/clear-cancelled-occupancy`:
- 不传 `serviceDates`(或传空数组)= 整单级释放该订单全部已取消派单占用,**向后兼容且幂等**(`intentsWritten=0` 视为成功)。
- 传 `serviceDates` 时后端对全程行按 startDate~endDate 自动展开为天(`coveredServiceDates`),即后端本身能处理全程槽行——缺的只是前端解析不出日期。
## 修复建议(前端二选一)
1. **降级整单级**:行级解析服务日为空时,确认弹窗文案改为整单级口径(「将释放该订单下已取消派单占用的车辆/司机」),请求不带 `serviceDates` 直接发——后端即整单级释放。
2. **隐藏行级按钮**:`canClearCancelledOccupancyRow` 增加「能解析出服务日」前置条件,解析不出的行不显示行级「释放占用」,保留卡片顶整单级入口。
推荐方案 1(操作路径不变,用户无感)。
## 根治跟踪
后端工单 **#6152**(feat(fleet): 已派车后取消进异常桶对标房务)验收项包含「已取消行补返起止日期字段」,落地后行级释放可恢复逐天精确语义;本前端降级修复与其不冲突(解析得到日期时仍走行级)。
@@ -0,0 +1,124 @@
---
schema: "hl-changelog/v2"
ticket: "6153"
title: "供应商注册、资料维护与收款账户管理接口"
consumer: "admin"
author: "lc(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "9e1aba14"
target_release: ""
verified_at: "2026-08-23"
status_note: "#6153 与 #6161-#6164 已合并 dev-v3,并随当前目标提交 03685ac245 完成隔离 TEST。13 个接口均经真实管理员登录、Gateway、角色切换、数据库和同步敏感读取审计验证;归档接口按 #6174 在权威清账契约交付前固定失败关闭。"
updated_at: "2026-08-23"
base: "dev-v3"
---
# 供应商注册、资料维护与收款账户管理
本次在 Resource 服务交付供应商主档注册、资料维护和收款账户管理能力。所有接口统一经过 Gateway 强制认证,并在服务端同时校验可信角色与 User 权限;请求体或请求头中的自报身份不能替代登录态。
## 变更接口
| 编号 | 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|------|
| SUP-ADM-001 | GET | `/admin/supplier/items/page` | `supplier:list` | 分页查询供应商 |
| SUP-ADM-043 | GET | `/admin/supplier/items/list` | `supplier:list` | 最多 200 条的有界列表 |
| SUP-ADM-002 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | `supplier:view` | 查询脱敏聚合详情 |
| SUP-ADM-003 | POST | `/admin/supplier/items/add` | `supplier:create` | 创建 DRAFT 注册草稿 |
| SUP-ADM-004 | PUT | `/admin/supplier/items/{supplierId}/update` | `supplier:update` | 按版本增量更新资料 |
| SUP-ADM-007 | POST | `/admin/supplier/items/{supplierId}/submit` | `supplier:update` + `supplier:approval:submit` | 提交注册审批 |
| SUP-ADM-010 | POST | `/admin/supplier/items/{supplierId}/archive` | `supplier:status:manage` | 清账契约交付前返回 `395032` |
| SUP-ADM-011 | DELETE | `/admin/supplier/items/{supplierId}/del` | `supplier:delete` | 软删除从未提交的草稿 |
| SUP-ADM-012 | GET | `/admin/supplier/items/{supplierId}/approval-records/page` | `supplier:approval:read` | 查询审批关联变更记录 |
| SUP-ADM-013 | GET | `/admin/supplier/items/{supplierId}/account-info/list` | `supplier:view` | 查询脱敏账户列表 |
| SUP-ADM-014 | POST | `/admin/supplier/items/{supplierId}/bank-accounts/add` | `supplier:account:manage` + `supplier:approval:submit` | 批量新增 1 至 50 个账户 |
| SUP-ADM-015 | GET | `/admin/supplier/bank-accounts/{accountId}/view` | `supplier:view`;证明附件另需 `supplier:account:proof:read` | 查询账户详情 |
| SUP-ADM-016 | PUT | `/admin/supplier/bank-accounts/{accountId}/default/update` | `supplier:account:manage` | 切换唯一默认账户 |
## 角色与权限
- `ADMIN` 可查询供应商、审批记录和脱敏账户,但不能创建、更新、提交、删除、维护账户或读取证明附件。
- `FINANCE` 可完成供应商注册、资料维护、草稿删除、账户维护与证明附件读取,但不能执行生命周期归档。
- `SUPER_ADMIN` 拥有上述全部权限;归档仍受权威清账门禁约束。
- 其他角色无供应商权限。菜单或按钮显隐不能替代服务端授权。
账户证明附件是独立敏感权限:无权限时 `proofFileUrls` 字段不序列化;有权限时,Resource 必须先同步写入不含 URL 和账号的 User 操作审计,审计失败则不返回附件。
## 主要业务语义
### 注册与资料
- 创建固定为 `DRAFT`、信用等级 `B`,返回的 `updateTime` 是后续更新或提交必须原样回传的并发版本。
- 注册提交采用准备事务、事务外审批 Provider、结果事务三阶段流程。本期 `LOCAL_AUTO` 成功后,供应商进入 `ACTIVE`,初始账户进入 `ACTIVE`,首个账户成为唯一默认账户。
- `ACTIVE` 后供应商全称和统一社会信用代码不可修改,返回 `395033`;旧版本返回 `395014`,均不产生业务写入。
- 草稿删除使用软删除并保留审批、创建和删除审计。非草稿状态删除返回 `395005`。
- 税号、电话、证照号和收款账号加密存储;查询只返回脱敏值。Snowflake ID 统一按 JSON String 返回。
### 收款账户
- 列表只返回未删除的 `PENDING/ACTIVE/DISABLED` 账户,默认账户优先,其余按 `createTime DESC、accountId DESC` 稳定排序。
- 批量新增先整体校验,再为每个账户生成独立审批记录;成功账户为 `ACTIVE/NO`,响应顺序与请求顺序一致。
- 重复账号返回 `395027`,不存在账户返回 `395001`,失败不新增账户或审批记录。
- 默认账户切换在供应商聚合锁和数据库行锁内完成,只允许 `ACTIVE` 供应商的 `ACTIVE` 账户。重复设置当前唯一默认账户按业务幂等成功,不更新时间也不写审计。
### 归档
`SUP-ADM-010` 已存在,但在 #6173 提供权威清账契约前固定返回 `395032`。该失败发生在归档审批、状态变更和供应商数据库写入之前,不能通过本地假定“已清账”降级放行。
## 主要错误码
| 业务码 | 含义 |
|-------:|------|
| `395001` | 供应商或账户不存在 |
| `395002` | 无供应商普通写权限 |
| `395005` | 当前状态不允许执行该操作 |
| `395006` | 主体证件号已建档 |
| `395014` | 并发版本已变化 |
| `395027` | 收款账号已被占用 |
| `395032` | 无法确认权威清账,归档失败关闭 |
| `395033` | 审批中或审批后主体身份字段不可修改 |
统一响应可能以 HTTP 200 承载业务失败,调用方必须同时检查 `code`、`success` 和 `data`。
## 验证证据
- 当前 TEST 部署目标提交:`03685ac24520dea5917c708cda76942dee89c2e2`,Nacos 中 Resource 仅有一个健康实例。
- 自动化回归:Supplier 聚焦测试 `150/150`,Resource 全量 `1906` 项零失败(38 项仓库既有条件跳过),Gateway 路由与 JWT 供应商测试 `8/8`;#6205 定向回归 `5/5`。
- 真实 Gateway E2E 共 `105` 个断言通过,覆盖匿名与伪造身份 `401`、ADMIN/FINANCE/SUPER_ADMIN 真实角色切换、参数和越权零写入、草稿创建与立即提交、审批状态、乐观锁、主体门禁、软删除、账户批量审批、敏感读取同步审计、唯一默认与幂等、归档失败关闭。
- 数据核对确认税号、电话、证照号、账号和审批候选均无明文落库;响应与服务日志未发现测试税号、账号或令牌。
- 验收使用独立一次性 User/Resource schema,原始 Resource schema 全程保持 0 张 `supplier_*` 表;一次性业务数据在证据核对后清理。
- HL 仓库未配置 Gitea workflow/status context;远端证据为 PR 合并态、目标分支包含关系和提交完整性核对,不将其表述为已运行远端 CI。
## 前端接入提示
- 更新和提交必须使用最近一次创建、更新或详情响应中的 `updateTime`,收到 `395014` 后重新查询。
- ADMIN 不展示供应商写操作、账户维护和证明附件;FINANCE 不展示归档;服务端仍会独立拒绝越权调用。
- 账户详情必须容忍 `proofFileUrls` 整个字段缺失,而不是把缺失视为接口异常。
- 归档按钮在 #6173 交付前应按不可用处理,不能重试绕过 `395032`。
## 撤回
1. 停止 13 个管理端接口的新流量,按账户管理、供应商主档、Gateway/权限依赖的逆序回退对应合并提交并重新部署。
2. 已应用的 13 张供应商表 migration 不修改、不删除;旧版本忽略新增表。已产生的主体、审批和审计数据保留,业务恢复通过后续受审计补偿操作完成,不直接改库。
3. 已应用的 User 权限和字典 migration 使用更高版本 migration 撤销精确角色绑定,不修改既有 Flyway 历史。
4. 锁、幂等和权限缓存仅按精确管理员或供应商键清理并验证,无 MQ 恢复项。
5. 回退后经 Gateway 验证新接口不可达,并复验既有 Resource、User 和 Gateway 路由。
## 关联 / 联系人
### 链接
- **供应商主档 Issue / PR**: [#6153](https://git.1814.love:8443/wx/HL/issues/6153) / [#6171](https://git.1814.love:8443/wx/HL/pulls/6171)
- **账户接口 Issues**: [#6161](https://git.1814.love:8443/wx/HL/issues/6161)、[#6162](https://git.1814.love:8443/wx/HL/issues/6162)、[#6163](https://git.1814.love:8443/wx/HL/issues/6163)、[#6164](https://git.1814.love:8443/wx/HL/issues/6164)
- **账户接口 PR**: [#6181](https://git.1814.love:8443/wx/HL/pulls/6181)
- **Gateway 与权限依赖**: [#6191](https://git.1814.love:8443/wx/HL/issues/6191)
- **归档失败关闭**: [#6174](https://git.1814.love:8443/wx/HL/issues/6174)
- **当前 TEST 目标提交**: [03685ac245](https://git.1814.love:8443/wx/HL/commit/03685ac24520dea5917c708cda76942dee89c2e2)
### 联系人
- **后端负责人**: @lc
@@ -0,0 +1,106 @@
---
schema: "hl-changelog/v2"
ticket: "6174"
title: "供应商归档在权威清账契约缺失时失败关闭"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "PR #6203 已合并 dev-v3,提交 041fb6d492 已部署 TEST。真实 Token 经 Gateway 实测:SUPER_ADMIN 且拥有生命周期权限时稳定返回 395032;ADMIN 先返回 395004;匿名与伪造身份返回 401。失败路径未进入供应商数据库,未创建审批或变更记录。"
updated_at: "2026-08-23"
base: "dev-v3"
---
# 供应商归档:无权威清账契约时失败关闭
供应商归档是不可逆生命周期操作。在 Finance 权威清账契约尚未交付时,现有接口不再创建本地审批或返回清账证据为空的假成功,而是在服务端权限校验后稳定失败关闭。
## 修改接口
| 方法 | 路径 | 请求体 | 权限 |
|------|------|--------|------|
| POST | `/admin/supplier/items/{supplierId}/archive` | 无 | 可信角色必须为 `SUPER_ADMIN`,且拥有 `supplier:status:manage` |
请求路径、请求参数和响应包络均未改变;本次修改的是业务失败语义。
## 行为变化
| 场景 | 修改前 | 修改后 |
|------|--------|--------|
| 清账能力尚未提供,权限通过 | 仍可能创建审批并把供应商推进至 `ARCHIVED` | 返回 `395032`,不进入审批准备、Provider 或结果应用 |
| ADMIN / FINANCE 请求归档 | 由既有生命周期角色门禁拒绝 | 保持不变,返回 `395004` |
| 未认证或伪造 `X-Admin-*` | Gateway 拒绝 | 保持不变,返回 `401` |
| 非法 `supplierId` | 参数绑定失败 | 保持不变,返回 `400` |
## 代表性响应
### SUPER_ADMIN 权限通过,但无法取得权威清账结果
```json
{
"code": 395032,
"message": "暂无法确认财务已清账,不能归档",
"data": null,
"success": false
}
```
### ADMIN 或 FINANCE 请求归档
```json
{
"code": 395004,
"message": "无权执行该供应商状态操作",
"data": null,
"success": false
}
```
统一响应可能以 HTTP 200 承载业务失败,调用方必须同时检查 `code` 和 `success`。
## 边界与兼容性
- 本次未新增错误码;`395032` 已在供应商错误码契约中冻结。
- 未修改 DTO 字段、数据库 migration、Redis、MQ、Nacos、Gateway 路由或 User 权限数据。
- 失败路径不调用审批 Provider、准备事务或结果应用器,不新增 `supplier_approval_log` / `supplier_change_log`,也不更新 `supplier_main`。
- 正向清账查询、账本版本证据和归档成功能力由后续工单 #6173 承接。在该依赖交付前,客户端应把 `395032` 视为暂不可归档,不应重试为“已归档”。
- 既有客户端已经按统一业务失败包络处理,无前端代码同步要求。
## 验证证据
- 最终部署提交:`041fb6d492cfe9552f443c592160dbd5dc5d6965`。
- 自动化回归:Supplier 套件 `149/149` 通过且 0 跳过;Resource 全量 `1905` 项,0 失败、0 错误;独立审计定向 `17/17` 通过。
- 真实登录经 Gateway 验收:
- 匿名和仅伪造身份头均返回 `401`;
- SUPER_ADMIN 连续两次请求均返回 `395032`;
- 切换 ADMIN 后先返回 `395004`;
- 非法 `supplierId` 返回 `400`。
- TEST 的本地 `hl_resource_service` 当前未初始化 `supplier_*` 表,且 Flyway 明确关闭;本接口在任何供应商数据库访问前返回 `395032`,验证前后未创建表、未产生供应商业务写入。
- 验收后已恢复原 `SUPER_ADMIN` 角色并注销访问令牌与刷新令牌,Redis Token 键无残留;正常登录和失败操作审计按合规要求保留。
- Gateway、User、Resource 健康检查均为 200;Nacos 中 Resource 仅有 `127.0.0.1:8082` 健康实例。
## 撤回
1. 停止归档流量,revert 合并提交 `041fb6d492` 并重新构建、部署 `hl-resource-service`。
2. 本次无数据库、配置、Redis 或 MQ 恢复项。
3. 若 #6173 尚未交付,代码撤回会重新暴露“未清账即归档”风险,必须同时阻断本接口流量;只有在权威清账门禁及其成功/失败测试均已交付后才能恢复归档成功路径。
4. 撤回后经 Gateway 验证归档不可达,供应商查询、更新、提交等其他接口保持正常。
## 关联 / 联系人
### 链接
- **Issue**: [#6174](https://git.1814.love:8443/wx/HL/issues/6174)
- **PR**: [#6203](https://git.1814.love:8443/wx/HL/pulls/6203)
- **依赖 Issue**: [#6173](https://git.1814.love:8443/wx/HL/issues/6173)
- **合并提交**: [041fb6d492](https://git.1814.love:8443/wx/HL/commit/041fb6d492cfe9552f443c592160dbd5dc5d6965)
### 联系人
- **后端负责人**: @lc
@@ -0,0 +1,169 @@
---
schema: "hl-changelog/v2"
ticket: "6191"
title: "供应商管理接口接入 Gateway 强制认证并启用最小角色权限矩阵"
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 #6192 与补充 PR #6194 已合并 dev-v3,最终提交 5d144c962 已部署 TEST。真实账号登录及角色切换实测:匿名和伪造身份返回业务码 401;ADMIN、FINANCE、SUPER_ADMIN、CUSTOMIZER 的十项供应商权限分别为 3、9、10、0;越权写入返回 395002/395004;临时身份、日志和精确缓存已清理。无前端、菜单或按钮改动。"
updated_at: "2026-08-23"
base: "dev-v3"
---
# 供应商管理:Gateway 强制认证与最小角色权限矩阵
本次只改变现有供应商管理接口的入口和服务端授权行为,不新增接口,也不改变请求字段、响应字段或业务错误码。所有 `/admin/supplier/**` 请求现在统一经 Gateway 路由到资源服务,并要求真实管理员登录态;菜单是否可见不再等同于服务端授权。
## 关键变化
| 场景 | 修改前 | 修改后 |
|------|--------|--------|
| Gateway 路由 | `/admin/supplier/**` 未纳入资源服务路由 | 路由到资源服务并执行强制认证 |
| 未登录或伪造身份头 | 无稳定的供应商入口保护契约 | 统一响应业务码 `401`,请求不会进入供应商业务 |
| 角色授权 | 供应商权限定义与角色绑定不完整 | 按 ADMIN 3 项、FINANCE 9 项、SUPER_ADMIN 10 项、其他角色 0 项收紧 |
| 凭证地址 | 仅依赖业务接口自身行为 | 只有 `supplier:account:proof:read` 才会在账户详情中返回凭证 URL |
## 变更接口清单
以下现有接口均纳入同一 Gateway 路由与权限门禁;入参、出参保持不变。
| # | 方法 | 路径 | 权限 |
|---:|------|------|------|
| 1 | GET | `/admin/supplier/items/page` | `supplier:list` |
| 2 | GET | `/admin/supplier/items/list` | `supplier:list` |
| 3 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | `supplier:view` |
| 4 | POST | `/admin/supplier/items/add` | `supplier:create` |
| 5 | PUT | `/admin/supplier/items/{supplierId}/update` | `supplier:update` |
| 6 | POST | `/admin/supplier/items/{supplierId}/submit` | `supplier:update` + `supplier:approval:submit` |
| 7 | POST | `/admin/supplier/items/{supplierId}/archive` | `supplier:status:manage` |
| 8 | DELETE | `/admin/supplier/items/{supplierId}/del` | `supplier:delete` |
| 9 | GET | `/admin/supplier/items/{supplierId}/approval-records/page` | `supplier:approval:read` |
| 10 | GET | `/admin/supplier/items/{supplierId}/account-info/list` | `supplier:view` |
| 11 | POST | `/admin/supplier/items/{supplierId}/bank-accounts/add` | `supplier:account:manage` + `supplier:approval:submit` |
| 12 | GET | `/admin/supplier/bank-accounts/{accountId}/view` | `supplier:view`;凭证 URL 另需 `supplier:account:proof:read` |
| 13 | PUT | `/admin/supplier/bank-accounts/{accountId}/default/update` | `supplier:account:manage` |
## 权限矩阵
| 权限 | ADMIN | FINANCE | SUPER_ADMIN | CUSTOMIZER/其他 |
|------|:-----:|:-------:|:-----------:|:---------------:|
| `supplier:list` | ✓ | ✓ | ✓ | — |
| `supplier:view` | ✓ | ✓ | ✓ | — |
| `supplier:approval:read` | ✓ | ✓ | ✓ | — |
| `supplier:create` | — | ✓ | ✓ | — |
| `supplier:update` | — | ✓ | ✓ | — |
| `supplier:approval:submit` | — | ✓ | ✓ | — |
| `supplier:account:manage` | — | ✓ | ✓ | — |
| `supplier:account:proof:read` | — | ✓ | ✓ | — |
| `supplier:delete` | — | ✓ | ✓ | — |
| `supplier:status:manage` | — | — | ✓ | — |
权限同时检查 Gateway 下传的可信角色和 User 权限点。仅有菜单或按钮权限不能绕过服务端门禁,客户端传入的 `X-Admin-Id`、`X-Admin-Role` 也不能替代有效 Token。
## 代表性调用与响应
### 已登录角色查询供应商列表
```http
GET /admin/supplier/items/page?page=1&pageSize=10
Authorization: Bearer <管理员访问令牌>
```
请求无 Body。请求和成功响应结构未变,典型成功包络如下:
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [],
"total": 0,
"page": 1,
"pageSize": 10
},
"success": true
}
```
### 未登录或仅伪造身份头
```json
{
"code": 401,
"message": "缺少有效的 Authorization 头",
"data": null,
"success": false
}
```
项目统一包络可能以 HTTP 200 承载业务失败,调用方必须检查 `code` 和 `success`。
### ADMIN 尝试创建供应商
```json
{
"code": 395002,
"message": "无权执行该供应商写操作",
"data": null,
"success": false
}
```
### FINANCE 尝试归档供应商
```json
{
"code": 395004,
"message": "无权执行该供应商状态操作",
"data": null,
"success": false
}
```
## 边界与兼容性
- 请求和响应字段、分页参数、供应商状态机、审批语义、错误码均未修改。
- 未新增 `sys_menu`,也未修改任何前端菜单、按钮或页面;前端无需同步发布。
- 账户详情中的凭证 URL 只按独立权限裁剪。本工单不新增文件 URL、文件状态或访问归属校验。
- 本工单只补供应商权限和路由,不扩大到公共 Feign、文件服务或其他业务模块。
- 无效 Token、伪造身份头和非目标角色均失败关闭,不产生供应商业务写入。
## 验证证据
- 最终部署提交:`5d144c962c3812d6adf23361e1d9d86ae01f3199`。
- Gateway 真实请求:匿名与伪造 `X-Admin-*` 身份均返回业务码 `401`;供应商路径命中 `hl-resource-service` 路由。
- 使用临时 TEST 管理员通过 `/admin/auth/login` 登录,并依次调用 `/admin/auth/switch-role`:
- SUPER_ADMIN:十项权限 `10/10`;
- ADMIN:仅三项读取权限 `3/10`,创建返回 `395002`;
- FINANCE:除生命周期管理外 `9/10`,凭证读取为 `true`,归档返回 `395004`;
- CUSTOMIZER:`0/10`,读取返回 `403`。
- 当前 TEST 的供应商类型字典基线未初始化,已授权的列表请求在通过 Gateway 与权限门禁后进入既有下游检查并返回 `395039`;该依赖数据不在本工单改动范围。权限链路、拒绝语义和零写入均已实测。
- 临时管理员、角色关系、登录日志、操作日志及对应 Redis Token/权限缓存均已精确删除;供应商业务表行数前后不变。
- 自动化回归:Gateway 定向 `8/8`、User 供应商迁移 `5/5`、Resource 供应商 `19/19`;User 全量 `3588`、Resource 全量 `1855`、Gateway 排除既有无关 Fleet 跨库审计后 `80` 项全部通过;24 模块构建通过。
## 影响评估
- **是否破坏向后兼容**:否。接口字段与业务动作不变,只补齐原本缺失的入口和授权边界。
- **前端是否必须同步上线**:否。
- **前端 workaround 清理点**:无;继续使用现有登录 Token,不要自行构造管理员身份头。
## 关联 / 联系人
### 链接
- **Issue**: [#6191](https://git.1814.love:8443/wx/HL/issues/6191)
- **主 PR**: [#6192](https://git.1814.love:8443/wx/HL/pulls/6192)
- **权限元数据补充 Issue**: [#6193](https://git.1814.love:8443/wx/HL/issues/6193)
- **权限元数据补充 PR**: [#6194](https://git.1814.love:8443/wx/HL/pulls/6194)
- **最终合并提交**: [5d144c962](https://git.1814.love:8443/wx/HL/commit/5d144c962c3812d6adf23361e1d9d86ae01f3199)
### 联系人
- **后端负责人**: @lc
@@ -0,0 +1,151 @@
---
schema: "hl-changelog/v2"
ticket: "6195"
title: "供应商模块前端联调接口汇总"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "9e1aba14"
target_release: ""
verified_at: "2026-08-23"
status_note: "汇总 #6153、#6161-#6164、#6174、#6195、#6197、#6200、#6205 的当前管理端接口契约,新增前端联调版单文件 HTML;16 个接口已实现可联调,SUP-ADM-010 保持 395032 失败关闭。"
updated_at: "2026-08-23"
base: "dev-v3"
---
# 供应商模块前端联调接口汇总
> **存放目录**: 二期 v3 → `changelogs-v2/2026-08/`
> **服务**: `hl-resource-service`(8082)
> **PR**: 供应商模块各实现工单对应合并请求
> **Issue**: #6195(汇总 #6153、#6161–#6164、#6174、#6197、#6200、#6205)
> **日期**: 2026-08-23
> **影响范围**: 管理后台供应商档案、账户和资源关联页面
## ⚠️ 关键变化
本条目将已交付的供应商接口统一整理为前端联调契约;共 16 个接口。归档接口 `SUP-ADM-010` 仅完成权限门禁和失败关闭,清账提供方未接入时固定返回 `395032`,不可按成功归档流程处理。
## 一、背景
本条目汇总已 TEST 验收并关闭的供应商工单,统一接口路径、权限、字典和失败语义,避免前端继续使用旧版设计稿或静态枚举。
本条目为前端联调索引,不替代接口卡片中的完整请求、响应、字段和错误码说明。完整契约见:[供应商模块 API 接口规范 v2.1 前端联调版](../../api-docs/supplier/%E4%BE%9B%E5%BA%94%E5%95%86%E6%A8%A1%E5%9D%97%20API%20%E6%8E%A5%E5%8F%A3%E8%A7%84%E8%8C%83-v2.1-%E5%89%8D%E7%AB%AF%E8%81%94%E8%B0%83%E7%89%88.html)。原始 v2.0 设计/生成基线保留不改。
## 变更接口
| 编号 | 方法 | 路径 | 权限 | 前端状态 |
|---|---|---|---|---|
| SUP-ADM-001 | GET | `/admin/supplier/items/page` | `supplier:list` | 已实现 · 可联调 |
| SUP-ADM-043 | GET | `/admin/supplier/items/list` | `supplier:list` | 已实现 · 可联调 |
| SUP-ADM-002 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | `supplier:view` | 已实现 · 可联调 |
| SUP-ADM-003 | POST | `/admin/supplier/items/add` | `supplier:create` | 已实现 · 可联调 |
| SUP-ADM-004 | PUT | `/admin/supplier/items/{supplierId}/update` | `supplier:update` | 已实现 · 可联调 |
| SUP-ADM-007 | POST | `/admin/supplier/items/{supplierId}/submit` | `supplier:update` + `supplier:approval:submit` | 已实现 · 可联调 |
| SUP-ADM-010 | POST | `/admin/supplier/items/{supplierId}/archive` | `supplier:status:manage` | 已实现 · 失败关闭 |
| SUP-ADM-011 | DELETE | `/admin/supplier/items/{supplierId}/del` | `supplier:delete` | 已实现 · 可联调 |
| SUP-ADM-012 | GET | `/admin/supplier/items/{supplierId}/approval-records/page` | `supplier:approval:read` | 已实现 · 可联调 |
| SUP-ADM-034 | GET | `/admin/supplier/items/{supplierId}/account-info/list` | `supplier:view` | 已实现 · 可联调 |
| SUP-ADM-035 | POST | `/admin/supplier/items/{supplierId}/bank-accounts/add` | `supplier:account:manage` + `supplier:approval:submit` | 已实现 · 可联调 |
| SUP-ADM-036 | GET | `/admin/supplier/bank-accounts/{accountId}/view` | `supplier:view`;证明附件另需 `supplier:account:proof:read` | 已实现 · 可联调 |
| SUP-ADM-041 | PUT | `/admin/supplier/bank-accounts/{accountId}/default/update` | `supplier:account:manage` | 已实现 · 可联调 |
| SUP-ADM-048 | GET | `/admin/supplier/resource-relations/{resourceModule}/{resourceId}/view` | `supplier:view` | 已实现 · 可联调 |
| SUP-ADM-049 | PUT | `/admin/supplier/resource-relations/{resourceModule}/{resourceId}/update` | `supplier:update` | 已实现 · 可联调 |
| SUP-ADM-050 | POST | `/admin/supplier/resource-relations/{resourceModule}/{resourceId}/unbind` | `supplier:update` | 已实现 · 可联调 |
## 三、接口详情
完整字段级请求、响应、错误码和接口约束见联调版 HTML;本表为入口索引,避免与 HTML 维护两份字段定义。
## 四、契约约束与正确调用方式
- 所有管理端请求必须经 Gateway 并携带真实管理员认证;不得伪造 `X-Admin-Id`。
- 字典接口返回的 `dictValue` 才是业务提交值;不得提交中文标签或 `dictDataId`。
- 更新、提交、资源改绑和解绑必须携带服务端返回的并发时间字段。
- `qualType`、`contactRole` 当前为自由文本,不按未确认字典拦截。
## 五、数据库行为
本条目不新增或修改数据库结构;供应商接口按既有事务、软删除、审计和敏感字段脱敏规则执行。
## 六、边界行为
- 未认证或无权限:由 Gateway/服务端拒绝,不产生业务写入。
- 业务失败可能使用 HTTP 200,必须同时判断 `code`、`success` 和 `data`。
- `SUP-ADM-010` 返回 `395032` 时不进入审批、状态迁移或审计写入。
## 六.5、枚举 / 数据字典
### 供应商类型(平台字典 `supplier_type`)
**所属字段**: `typeCode` / `types[].typeCode` | **类型**: `String`
运行时启用项、标签和排序以 `GET /admin/dict/data/supplier_type` 返回为准。
### 供应商生命周期(平台字典 `supplier_lifecycle_status`)
**所属字段**: `status` | **类型**: `String`
运行时启用项、标签和排序以 `GET /admin/dict/data/supplier_lifecycle_status` 返回为准;字典项不是全部可跳转状态。
## 六.7、影响评估
- **是否破坏向后兼容**: 否
- **前端是否必须同步上线**: 是,需按本联调契约接入
- **前端 workaround 清理点**: 清理页面内硬编码的供应商类型和生命周期中文文案
## 七、不影响范围
- **仅影响**: 管理后台供应商模块
- **零影响**: 小程序接口、订单服务、Fleet 业务数据、Finance 清账能力
## 统一接入约束
- 所有 `/admin/supplier/**` 请求必须经 Gateway,认证级别为 `MANDATORY`;客户端不得使用自报 `X-Admin-Id` 或角色替代有效登录态。
- 服务端按可信管理员身份校验平台权限;隐藏菜单或按钮不构成授权。
- Snowflake ID 按 JSON 字符串传输;`LocalDateTime` 使用 `yyyy-MM-dd HH:mm:ss`。
- 供应商类型和生命周期状态从平台字典读取,业务请求只提交 `dictValue`,前端不应写死中文标签。
- 本次字典范围仅确认 `supplier_type` 与 `supplier_lifecycle_status`;两者的运行时启用项、标签和排序以 `/admin/dict/data/{dictType}` 返回为准,文档中的编码表只是契约基线,不是环境现状。
- `qualType`、`contactRole` 暂不接入平台字典,保持自由文本;前端不得按未确认候选值做字典校验或自行新增下拉选项。
- 账号、税号、电话和证照号只返回脱敏值。`proofFileUrls` 可能因权限缺失而整个字段不返回。
- 更新、提交、资源改绑和解绑必须原样携带响应中的并发时间字段;收到 `395014` 或并发错误时重新查询后再操作。
- 统一响应可能以 HTTP 200 承载业务失败,必须同时判断 `code`、`success` 和 `data`。
## 归档限制
`SUP-ADM-010` 当前只完成权限门禁和失败关闭:权限通过后返回 `395032 SUPPLIER_CLEARANCE_CHECK_UNAVAILABLE`,不进入审批、状态迁移或审计写入。本期没有 Finance 清账提供方,因此前端不应按成功归档流程重试或伪造成功状态。
## TEST 验收证据
- 目标提交:`03685ac24520dea5917c708cda76942dee89c2e2`,已部署隔离 TEST。
- 真实管理员经 Gateway 完成 105 项 E2E 断言,覆盖认证、角色权限、参数校验、失败零写入、创建/提交、账户批量管理、默认账户、资源关系、版本并发、敏感字段和归档失败关闭。
- 自动化回归:Supplier 聚焦测试 `150/150`,Resource 全量 `1906` 项零失败(38 项既有条件跳过),Gateway 供应商路由/JWT `8/8`,#6205 定向 `5/5`。
- 测试使用独立一次性 schema;测试数据、临时账号、Token 和临时服务已清理,原环境已恢复。
## 八、测试环境已验证
真实管理员经 Gateway 完成 105 项 E2E 断言;Supplier 聚焦测试 `150/150`、Resource 全量 `1906` 零失败、Gateway 供应商路由/JWT `8/8`、#6205 定向 `5/5`。
## 九、相关历史 PR
本条目关联工单均已合并、部署、TEST 验收并关闭;具体工单链接见下方关联工单清单。
## 十、相关文档
- 联调版接口文档:[供应商模块 API 接口规范 v2.1 前端联调版](../../api-docs/supplier/%E4%BE%9B%E5%BA%94%E5%95%86%E6%A8%A1%E5%9D%97%20API%20%E6%8E%A5%E5%8F%A3%E8%A7%84%E8%8C%83-v2.1-%E5%89%8D%E7%AB%AF%E8%81%94%E8%B0%83%E7%89%88.html)
## 关联工单
- 主档与账户:[#6153](https://git.1814.love:8443/wx/HL/issues/6153)、[#6161](https://git.1814.love:8443/wx/HL/issues/6161)、[#6162](https://git.1814.love:8443/wx/HL/issues/6162)、[#6163](https://git.1814.love:8443/wx/HL/issues/6163)、[#6164](https://git.1814.love:8443/wx/HL/issues/6164)
- Gateway 与权限:[#6191](https://git.1814.love:8443/wx/HL/issues/6191)
- 归档失败关闭:[#6174](https://git.1814.love:8443/wx/HL/issues/6174)
- 资源关系:[#6195](https://git.1814.love:8443/wx/HL/issues/6195)、[#6197](https://git.1814.love:8443/wx/HL/issues/6197)、[#6200](https://git.1814.love:8443/wx/HL/issues/6200)
- 创建版本时间:[#6205](https://git.1814.love:8443/wx/HL/issues/6205)
## 撤回
本条目和联调文档为文档变更,撤回时删除本汇总文件并下架联调版 HTML 即可,不改后端数据库、配置、Redis 或 MQ。后端功能撤回按各工单已有撤回方案执行;已应用的 migration 和业务数据不得删除或回滚覆盖。
@@ -0,0 +1,184 @@
---
schema: "hl-changelog/v2"
ticket: "6195"
title: "供应商资源关系查询、设置改绑与解绑"
consumer: "admin"
author: "lc(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "9e1aba14"
target_release: ""
verified_at: "2026-08-23"
status_note: "主 PR #6196 与补充 PR #6198/#6201 均已合并 dev-v3,最终链路提交 b71cc41c8 已完成 TEST,并包含在当前部署提交 041fb6d492 中。真实角色、Gateway、绑定/查询/解绑、车辆依赖失败关闭及首次响应版本可直接回用均已验证;两条无安全删除入口的 ACTIVE 测试供应商主档作为已记录残留保留,未执行直接 DML。"
updated_at: "2026-08-23"
base: "dev-v3"
---
# 供应商资源关系:查询、设置改绑与解绑
本次新增资源页面维护供应商关系的三个管理端接口,覆盖读取当前关系、首次绑定或显式改绑、按并发版本解绑。关系统一存放在 Resource 服务,不向景区、酒店、车辆等资源主表写入 `supplier_id`,也不跨 schema 读写。
补充修复 #6197 和 #6200 已一并纳入本契约:Fleet 车辆摘要的所有远端运行时异常统一映射为 `395039`;首次绑定返回的 `updateTime` 与 MySQL 秒精度持久化值一致,可立即用于改绑或解绑。
## 变更接口
| 编号 | 方法 | 路径 | 权限 | 返回 |
|------|------|------|------|------|
| SUP-ADM-048 | GET | `/admin/supplier/resource-relations/{resourceModule}/{resourceId}/view` | `supplier:resource:view` + 资源查看范围 | 当前有效关系 |
| SUP-ADM-049 | PUT | `/admin/supplier/resource-relations/{resourceModule}/{resourceId}/update` | `supplier:resource:manage` + 资源编辑范围 | 新建或改绑后的关系 |
| SUP-ADM-050 | POST | `/admin/supplier/resource-relations/{resourceModule}/{resourceId}/unbind` | `supplier:resource:manage` + 资源编辑范围 | `null` |
三个路径已经被现有 Gateway `/admin/supplier/**` 路由覆盖,均要求真实管理员 Token。客户端传入的 `X-Admin-Id`、`X-Admin-Role` 不会替代可信身份。
## 角色与权限
| 角色 | `supplier:resource:view` | `supplier:resource:manage` | 默认能力 |
|------|:------------------------:|:--------------------------:|----------|
| ADMIN | ✓ | ✓ | 可查看;仅可维护本人创建、且自身有编辑范围的资源 |
| FINANCE | ✓ | — | 只读关系 |
| SUPER_ADMIN | ✓ | ✓ | 可查看和维护 |
| 其他角色 | — | — | 拒绝 |
这两项权限与供应商主档权限双向隔离:`supplier:update` 不能单独维护资源关系,`supplier:resource:manage` 也不会授予供应商主档编辑能力。
## 支持的资源模块
| `resourceModule` | 展示模块 | `requiredTypeCode` 规则 |
|------------------|----------|--------------------------|
| `SCENIC` | 景区管理 | 固定 `SCENIC` |
| `RESTAURANT` | 餐厅管理 | 固定 `RESTAURANT` |
| `SUPPLIES` | 备品管理 | 默认 `SUPPLIES`,允许显式类型 |
| `SUPPLIES_COMBO` | 组合配品 | 固定 `SUPPLIES` |
| `ACTIVITY` | 游玩项目管理 | 必须显式传入 |
| `HOTEL` | 酒店管理 | 固定 `HOTEL` |
| `SERVICE` | 服务管理 | 默认 `SERVICE`,允许显式类型 |
| `COST_ITEM` | 额外成本 | 必须显式传入 |
| `STAFF` | 服务人员管理 | 必须显式传入 |
| `VEHICLE` | 车队管理-车队管理 | 固定 `FLEET` |
未知模块返回 `395034`。资源不存在或已删除返回 `395035`。VEHICLE 通过 Internal Token 保护的 Fleet 精确摘要接口读取;超时、fallback、非成功、空响应以及 Feign 抛出的任意运行时异常均失败关闭为 `395039`。
## 请求与响应
### 首次绑定或改绑
```json
{
"supplierId": "1900000000000000001",
"requiredTypeCode": "SCENIC",
"remark": "主供应商",
"expectedCurrentSupplierId": "1900000000000000000",
"expectedRelationUpdateTime": "2026-08-23 11:20:30",
"changeReason": "资源供应商调整"
}
```
- 首次绑定不传两个 `expected*` 字段。
- 已有关系时必须同时传 `expectedCurrentSupplierId` 与 `expectedRelationUpdateTime`;缺一返回参数错误,值不匹配返回 `395014`。
- 同一供应商、同一要求类型和同一备注的重复请求按业务幂等成功,不更新时间,也不写空审计。
### 解绑
```json
{
"expectedCurrentSupplierId": "1900000000000000001",
"expectedRelationUpdateTime": "2026-08-23 11:20:30",
"changeReason": "停止合作"
}
```
三个字段均必填。关系不存在或已解绑返回 `395038`;版本不一致返回 `395014`。
### 关系响应
```json
{
"code": 200,
"message": "成功",
"data": {
"relationId": "1900000000000000100",
"supplierId": "1900000000000000001",
"supplierNo": "SUP1900000000000000001",
"supplierName": "示例供应商",
"resourceModule": "SCENIC",
"moduleName": "景区管理",
"resourceId": "10001",
"resourceName": "示例景区",
"requiredTypeCode": "SCENIC",
"requiredTypeName": "景区供应商",
"remark": "主供应商",
"available": true,
"unavailableReasons": [],
"createTime": "2026-08-23 11:20:30",
"updateTime": "2026-08-23 11:20:30"
},
"success": true
}
```
所有 Snowflake `Long` 输出为 JSON String;时间格式为 `yyyy-MM-dd HH:mm:ss`。响应不包含账户、资质附件、密文或跨服务内部字段。`updateTime` 是并发版本,客户端必须原样回传。
## 可用性与写入门禁
- 写入目标供应商必须为 `ACTIVE`,要求类型必须启用且属于该供应商,全部必备资质必须齐全且未过期。
- 查询不会因为后续资格变化删除历史关系,而是通过 `available=false` 和稳定原因列表返回:`SUPPLIER_NOT_ACTIVE`、`REQUIRED_TYPE_INACTIVE`、`SUPPLIER_TYPE_MISSING`、`REQUIRED_QUALIFICATION_MISSING`、`REQUIRED_QUALIFICATION_EXPIRED`。
- 写接口使用请求摘要幂等、资源粒度分布式锁、数据库行锁、预期版本围栏和唯一索引兜底。
- 绑定、改绑、解绑与 `supplier_change_log` 同事务;审计失败整笔回滚。改绑保留旧关系软删除历史,并分别为原、新供应商记录安全审计快照。
- 外部依赖调用在数据库事务外完成;依赖失败、越权、参数错误、版本冲突和资格门禁失败均不产生关系或变更审计写入。
## 主要错误码
| 业务码 | 含义 |
|-------:|------|
| `395014` | 关系并发版本已变化 |
| `395034` | 不支持的资源模块 |
| `395035` | 资源不存在或已删除 |
| `395036` | 资源已关联其他供应商,应显式改绑 |
| `395037` | 供应商类型不满足资源要求 |
| `395038` | 当前供应商资源关系不存在 |
| `395039` | 字典、权限、资源摘要或 Fleet 等必要依赖暂不可用 |
统一包络可能以 HTTP 200 承载业务失败,调用方必须检查 `code` 与 `success`。
## 验证证据
- 合并链:主 PR #6196(`9f83cf9c7`)、Fleet 异常映射补充 PR #6198(`81663f7b6`)、版本时间补充 PR #6201(`b71cc41c8`)。
- 自动化:Supplier 聚焦测试、Fleet 精确摘要契约、User 权限 migration/权限服务测试、Gateway 路由审计与构建均通过;补充修复分别覆盖 Feign `BusinessException` 映射和首次响应秒精度版本。
- 真实 TEST 经 Gateway 覆盖权限矩阵、有效资源关系首次绑定/查询/改绑/解绑、同值幂等、版本冲突、资格门禁、VEHICLE 精确摘要成功链及依赖失败 `395039`。
- #6200 复验确认首次 PUT 返回的 `updateTime` 与立即 GET 一致,并可直接用于解绑;关系和审计副作用按正常接口清理。
- 临时资源关系、临时角色、临时管理员和对应 Token/权限缓存已清理。两条 `HL6195-TEST` ACTIVE 供应商主档因状态机禁止 ACTIVE 直接删除、且当时没有安全暂停/归档入口而保留;未执行未授权 DML,残留已在三个关联工单记录。
- HL 仓库没有 Gitea workflow/status context;远端 PR 合并态、目标分支包含关系、提交和文件清单已复核。
## 前端接入提示
- 资源编辑页先调用 SUP-ADM-048 获取当前关系和 `updateTime`,再决定首次绑定或显式改绑。
- 改绑和解绑必须使用最近一次 GET/PUT 返回的供应商 ID 与 `updateTime`;收到 `395014` 后重新查询,不要覆盖服务端新版本。
- FINANCE 只展示关系,不显示或调用改绑、解绑动作;服务端仍会独立校验,按钮隐藏不能代替权限。
- `available=false` 时展示稳定原因,不应把关系静默当作不存在。
- `395039` 表示必要依赖无法可靠校验,不应降级为成功或在本地伪造资源名称。
## 撤回
1. 停止调用三个管理端接口,按 #6200 → #6197 → #6195 的逆序 revert 合并提交并重新构建。
2. 部署顺序先回退 Resource 消费方,再在确认无消费者后回退 Fleet internal Provider;既有供应商、资源和车辆接口保持运行。
3. 已产生的 `supplier_resource_rel` 与 `supplier_change_log` 历史不做物理删除;需要恢复业务关系时,在代码回退前依据审计通过正常改绑/解绑补偿。
4. 已应用的 User 权限 migration 不修改、不删除 Flyway 历史。两个权限可作为无消费者兼容数据保留;确需移除时新增更高版本 migration,先删除精确角色绑定,再删除 `supplier:resource:view/manage`。
5. 资源锁与幂等键按 TTL 自然过期;紧急处理只清理精确 module/resourceId 和管理员权限缓存,禁止宽泛清 Redis。本链路不使用 MQ,无 MQ 恢复项。
6. 回退后经 Gateway 验证新接口不可达,原供应商主档、资源查询及 Fleet options 契约不受影响。
## 关联 / 联系人
### 链接
- **主 Issue**: [#6195](https://git.1814.love:8443/wx/HL/issues/6195)
- **主 PR**: [#6196](https://git.1814.love:8443/wx/HL/pulls/6196)
- **Fleet 异常映射 Issue / PR**: [#6197](https://git.1814.love:8443/wx/HL/issues/6197) / [#6198](https://git.1814.love:8443/wx/HL/pulls/6198)
- **版本时间 Issue / PR**: [#6200](https://git.1814.love:8443/wx/HL/issues/6200) / [#6201](https://git.1814.love:8443/wx/HL/pulls/6201)
- **最终链路提交**: [b71cc41c8](https://git.1814.love:8443/wx/HL/commit/b71cc41c843828c31d6980a58ae6d82a4d1d9f09)
### 联系人
- **后端负责人**: @lc
@@ -0,0 +1,194 @@
---
schema: "hl-changelog/v2"
ticket: "6204"
title: "司机档案新增个人能力资产与装备字段"
consumer: "admin"
author: "wx(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "be171bf7"
target_release: ""
verified_at: "2026-08-23"
status_note: "已合并 dev-v3 并部署测试服,经网关 API 实测 assets/equipments 往返一致"
updated_at: "2026-08-23"
base: "dev-v3"
---
# 司机档案:新增个人能力资产与装备字段
> **PR**: [#6214](https://git.1814.love:8443/wx/HL/pulls/6214)
> **服务**: hl-fleet-service (8087/8187)
> **作者**: wx
> **更新时间**: 2026-08-23
---
## 变更概述
司机档案新增两个结构化字段,类目由数据字典驱动,后续新增类目只加字典数据,零代码。
### 数据字典(前端需动态读取)
| 字典类型 | 字典值 | 中文标签 | remark(采集指引) |
|----------|--------|----------|-------------------|
| `driver_asset_type` | `drone` | 无人机 | `model` — 采集设备型号 |
| `driver_asset_type` | `camera` | 专业摄影机 | `model` — 采集设备型号 |
| `driver_asset_type` | `tour_guide_cert` | 导游证 | `certNo` — 采集证号 |
| `driver_equipment` | `kite` | 风筝 | 空 — 仅有无 |
| `driver_equipment` | `football` | 足球 | 空 — 仅有无 |
| `driver_equipment` | `camping_chair` | 露营椅 | 空 — 仅有无 |
**前端获取方式**: `GET /admin/dict/data/{type}` — `driver_asset_type` 和 `driver_equipment`
---
## 变更接口
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 新增司机档案 | POST | `/admin/fleet/drivers` | 请求体新增字段 | assets/equipments 可选 |
| 2 | 编辑司机档案 | PUT | `/admin/fleet/drivers/{driverId}` | 请求体新增字段 | assets/equipments 可选 |
| 3 | 查询司机档案详情 | GET | `/admin/fleet/drivers/{driverId}` | 响应体新增字段 | assets/equipments |
| 4 | 分页查询司机列表 | GET | `/admin/fleet/drivers/page` | 响应体新增字段 | assets/equipments |
---
## 接口详情
### 新增 VO: `DriverAssetVO`
响应和请求中的 `assets` 数组元素:
| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|
| `assetType` | String | ✅ | 长度 ≤ 32 | 资产类型编码,来自字典 `driver_asset_type`(drone/camera/tour_guide_cert) |
| `model` | String | ❌ | 长度 ≤ 64 | 设备型号,字典 remark=model 时采集 |
| `certNo` | String | ❌ | 长度 ≤ 64 | 证号,字典 remark=certNo 时采集 |
### 1. 新增司机档案 `POST /admin/fleet/drivers`
#### 请求体新增字段
```json
{
"assets": [
{ "assetType": "drone", "model": "DJI Mavic 3" },
{ "assetType": "tour_guide_cert", "certNo": "L-2026-001" }
],
"equipments": ["kite", "football"]
}
```
#### 响应示例(新增字段部分)
```json
{
"code": 200,
"data": {
"id": "2091464705498394626"
},
"success": true
}
```
### 2. 编辑司机档案 `PUT /admin/fleet/drivers/{driverId}`
#### 请求体新增字段
```json
{
"assets": [
{ "assetType": "drone", "model": "DJI Mavic 3 Pro" }
],
"equipments": ["kite", "football", "camping_chair"]
}
```
**编辑语义**: 全量覆盖。不传 assets/equipments 保持原值,传空数组 `[]` 清空。
### 3. 查询司机档案详情 `GET /admin/fleet/drivers/{driverId}`
#### 响应新增字段
```json
{
"code": 200,
"data": {
"assets": [
{ "assetType": "drone", "model": "DJI Mavic 3", "certNo": null },
{ "assetType": "tour_guide_cert", "model": null, "certNo": "L-2026-001" }
],
"equipments": ["kite", "football"]
},
"success": true
}
```
### 4. 分页查询司机列表 `GET /admin/fleet/drivers/page`
#### 响应新增字段(列表项同详情)
```json
{
"code": 200,
"data": {
"list": [
{
"assets": [
{ "assetType": "drone", "model": "DJI Mavic 3", "certNo": null }
],
"equipments": ["kite", "football"]
}
]
},
"success": true
}
```
---
## 边界行为
- **空态**: 未填写时返回 `assets: []`, `equipments: []`
- **非法字典值**: 提交非法 assetType/equipments → 400(错误码 100001)
- **model/certNo 非必填**: 后端只校验 type 合法性,扩展键原样存储不强制
---
## 前端接入提示
- **表单选项**: 动态调 `GET /admin/dict/data/driver_asset_type` 和 `GET /admin/dict/data/driver_equipment` 渲染
- **输入框控制**: 根据字典 remark 决定渲染什么输入框:
- `remark=model` → 设备型号输入框
- `remark=certNo` → 证号输入框
- 空 → 仅 checkbox/switch(有无)
- **导游证照片**: 继续走现有 `qualification` 附件类目上传,不重复建设
- **H5 司机入驻/续签** 同样支持 assets/equipments 字段,接口另见 changelog(如有)
---
## 测试环境已验证
```
POST /admin/fleet/drivers → 200 + assets/equipments 写入成功 ✓
GET /admin/fleet/drivers/{id} → 200 + assets/equipments 回读一致 ✓
```
验证司机: `driverId=2091464705498394626`(已清理)
---
## 关联 / 联系人
### 链接
- **Issue**: [#6204](https://git.1814.love:8443/wx/HL/issues/6204)
- **PR**: [#6214](https://git.1814.love:8443/wx/HL/pulls/6214)
- **Merge commit**: [f2a02ee3a](https://git.1814.love:8443/wx/HL/commit/f2a02ee3a8b2c43c91a892fac253d4e085fbd63c)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,64 @@
---
schema: "hl-changelog/v2"
ticket: "6205"
title: "供应商草稿创建响应与持久化版本时间保持一致"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "PR #6206 已合并 dev-v3,提交 03685ac245 已部署隔离 TEST。创建响应 updateTime、立即 GET 与数据库秒级版本完全一致,并可直接用于紧随其后的提交;请求和响应结构、错误码及前端字段解析均未变化。"
updated_at: "2026-08-23"
base: "dev-v3"
---
# 供应商草稿创建版本时间一致性
本次修正供应商草稿创建响应中的并发版本时间。此前创建对象依赖插入填充和数据库时间,在跨秒边界可能出现响应 `updateTime` 比实际持久化版本早一秒,导致客户端立即提交时误报 `395014`。
## 行为变化
| 方法 | 路径 | 修改前 | 修改后 |
|------|------|--------|--------|
| POST | `/admin/supplier/items/add` | 极少数跨秒创建响应的 `updateTime` 与数据库值不同 | 创建前显式生成同一秒级 `createTime/updateTime`,响应、立即查询和数据库值一致 |
请求字段、响应字段、JSON 类型、时间格式、权限、状态机和业务错误码均未改变。`updateTime` 仍为 `yyyy-MM-dd HH:mm:ss`,客户端继续原样回传即可。
## 兼容性
- 修复向后兼容,不要求前端同步发布,也不新增字段。
- 创建后的 `updateTime` 可直接用于 `POST /admin/supplier/items/{supplierId}/submit` 或后续更新。
- 真实旧版本仍返回 `395014`;本次没有放宽乐观锁,也没有改变事务、幂等、审批或审计语义。
- 不涉及数据库 migration、配置、Redis、MQ 或其他服务。
## 验证证据
- #6205 定向回归 `5/5`;Supplier 聚焦测试 `150/150`;Resource 全量 `1906` 项零失败(38 项仓库既有条件跳过);Gateway 供应商路由与 JWT 测试 `8/8`。
- 独立审计确认仅修改 Resource 的供应商聚合创建时间初始化及对应测试,没有新增入口、绕过权限、修改状态机或跨 schema 写入。
- 当前部署提交 `03685ac24520dea5917c708cda76942dee89c2e2` 的真实 Gateway E2E 中:创建响应 `updateTime`、立即 GET 和 MySQL 值完全一致;使用创建响应版本立即提交成功并完成 `LOCAL_AUTO / APPROVED / APPLIED`;刻意使用旧版本仍返回 `395014` 且零写入。
- TEST 输出和服务日志未发现测试税号、账号或令牌;原始 Resource schema 未受影响。
- HL 仓库未配置 Gitea workflow/status context;已核对 PR 合并态、远端目标提交和文件清单,不宣称不存在的远端 CI。
## 撤回
1. 停止供应商创建流量,从最新 `dev-v3` 创建回退分支并 revert #6205 合并提交 `03685ac24520dea5917c708cda76942dee89c2e2`,经独立 PR 合入。
2. 重新构建并部署 `hl-resource-service`,无需恢复数据库、配置、Redis 或 MQ。
3. 回退会恢复旧的跨秒版本漂移风险,但响应结构保持兼容。
4. 回退后经 Gateway 重跑创建、立即查询、立即提交和真实旧版本冲突用例,并核对失败零写入。
## 关联 / 联系人
### 链接
- **Issue**: [#6205](https://git.1814.love:8443/wx/HL/issues/6205)
- **PR**: [#6206](https://git.1814.love:8443/wx/HL/pulls/6206)
- **合并提交**: [03685ac245](https://git.1814.love:8443/wx/HL/commit/03685ac24520dea5917c708cda76942dee89c2e2)
### 联系人
- **后端负责人**: @lc
@@ -0,0 +1,150 @@
---
schema: "hl-changelog/v2"
ticket: "6210"
title: "调整订单弹窗「客户自订」勾选未回显,重交后自订标记被静默抹掉"
consumer: "admin"
change_type: "前端缺陷"
author: "kimi(wx)"
backend_status: "not_required"
gateway_status: "not_required"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "97baecb6"
target_release: "hl-ui@97baecb6"
verified_at: "2026-08-23"
status_note: "前端缺陷:调整订单弹窗酒店 tab 未绑定 days[].customerSelfBooked——快照接口已返回该字段但「客户自订」勾选框恒不勾,且自订晚被按普通晚渲染默认空段;重交时标记丢失落库,该晚变回「待房务配房」,房务会重复配房。后端读/写/换版链路实证无 bug。前端已修复并验证(ref=97baecb6)。"
updated_at: "2026-08-23"
base: "dev-v3"
---
# 【前端缺陷·管理后台】调整订单弹窗「客户自订」勾选未回显,重交后自订标记丢失(#6210)
> **PR**: 无(前端缺陷,后端零改动) | **服务**: hl-order-service-v3 | **作者**: wx | **更新时间**: 2026-08-23
> 前端已修复并验证(frontend_ref=97baecb6,verified 2026-08-23);本文保留缺陷定位与契约口径备查。
## ⚠️ 关键变化
调整订单弹窗酒店 tab 的快照接口**一直返回** `hotelRequirement.days[].customerSelfBooked` 字段(#5838 交付)。前端此前未消费该字段——勾选框不回显、重交不带回——导致顾问在弹窗里修改任何酒店需求内容后,已标记的「客户自订」晚被静默抹掉,变回「待房务配房」。
## 1. 背景与现象(TEST 实测订单 HL20260823145314586)
该订单 2026-08-27 出行、6 天 5 晚、酒店 6 间:
| 时间 | 操作 | 结果 |
|------|------|------|
| 14:57 | 顾问在调整订单弹窗提交住宿需求:第 1/3/4/5 晚勾「客户自订」,第 2 晚选呼伦贝尔香格里拉大酒店 | 保存正确,4 晚自订标记在案 |
| 15:18 | 顾问再次打开弹窗修改需求(删除第 2 晚一个空候选)后提交 | **4 晚自订标记全部丢失**;且这 4 晚被各自补了一条默认空需求段(每段 1 间 + 空候选),形态与「未勾选过自订的普通晚」一致 |
之后任何人再打开该订单的调整弹窗:「客户自订」勾选框均为空(数据已丢),这 4 晚对房务显示为「待配房」,**存在房务重复配房风险**。
## 2. 变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 调整订单快照 | GET | `/v3/admin/order/{orderId}/adjustment/snapshot?scope=HOTEL_REQ` | 无变更(契约澄清) | 回显源:`hotelRequirement.days[].customerSelfBooked` 必须绑定到勾选框 |
| 2 | 统一提交调整 | POST | `/v3/admin/order/{orderId}/adjustment/submit` | 无变更(契约澄清) | `updates.hotelRequirement.days[]` 必须原样带回 `customerSelfBooked` |
| 3 | 提交/修改/调整房型需求 | PUT | `/v3/admin/order/{orderId}/hotel-requirement` | 无变更(契约澄清) | 同上字段语义 |
## 3. 接口详情
### 3.1 调整订单快照(回显源)
- **使用场景**:打开「调整订单」弹窗时加载当前生效的住宿需求,逐晚回显
- **认证**:管理后台 JWT
- **入参**:`orderId`(路径参数,Long,必填);`scope=HOTEL_REQ`(query,只取酒店需求子域;空=全量)
- **出参**(`hotelRequirement` 节点,当前无需求时整个节点为 null):
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String | 需求行 ID |
| version | Integer | 版本号 |
| status | String | 需求状态(PENDING/PROCESSING/DONE 等) |
| days | Array | 逐晚需求,结构同提交入参 |
| days[].dayNumber | Integer | 晚次序号(1 起) |
| **days[].customerSelfBooked** | **Boolean/null** | **客户自订标记:true=该晚客人自己订酒店、房务无需配房;null/false=普通晚** |
| days[].segments | Array | 该晚需求段(自订晚通常为空数组) |
| specialTags | Array<String> | 特殊诉求标签 |
| remark | String | 备注 |
**示例(典型成功,测试服实测)**
请求:
```
GET /v3/admin/order/2091418470443810817/adjustment/snapshot?scope=HOTEL_REQ
Authorization: Bearer {admin-token}
(无请求体)
```
响应(节选,days 每项都带 customerSelfBooked 键):
```json
{
"code": 200,
"data": {
"hotelRequirement": {
"id": "2091424805864583169",
"version": 2,
"status": "PROCESSING",
"days": [
{ "dayNumber": 1, "customerSelfBooked": null, "segments": [ { "roomCount": 1, "candidates": [ { "hotelId": null, "hotelName": null } ] } ] },
{ "dayNumber": 2, "customerSelfBooked": null, "segments": [ { "roomCount": 2, "budget": "560.00", "candidates": [ { "hotelId": "2023714929877450753", "hotelName": "呼伦贝尔香格里拉大酒店" } ] } ] }
]
}
},
"success": true
}
```
### 3.2 统一提交调整 / 房型需求 upsert(重交)
两个写口(§2 表 #2/#3)的 `days[]` 元素结构与快照出参一致,**契约要求「无损回显 + 原样重交」**:快照读到的 `customerSelfBooked` 必须在提交时带回,后端按请求原样保存。
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| days[].dayNumber | Integer | ✅ | 晚次序号 |
| days[].customerSelfBooked | Boolean | 否 | true=客户自订;不传/null/false=普通晚(**不传即丢标记**) |
| days[].segments | Array | 条件必填 | 自订晚可为空数组;非自订晚为空 → 400(582098「第{0}晚缺少用房需求,请填写房间需求或标记为客户自订」) |
## 4. 契约约束(✅/❌ payload 对照)
| 场景 | payload(单晚) | 结果 |
|------|-----------------|------|
| ✅ 标记自订 | `{"dayNumber":1,"customerSelfBooked":true,"segments":[]}` | 保存自订标记,房务不配该晚 |
| ✅ 自订+备注 | `{"dayNumber":1,"customerSelfBooked":true,"segments":[],"remark":"客人自订汉庭,勿重复订"}` | 同上,备注透出给房务 |
| ✅ 普通晚 | `{"dayNumber":2,"customerSelfBooked":false,"segments":[...]}` 或省略该键 | 正常配房需求 |
| ❌ 回显后丢键重交 | 快照读到 `customerSelfBooked:true`,提交时省略该键 | **自订标记被抹为普通晚,房务开始对该晚配房**(本缺陷事故形态) |
| ❌ 自订晚被补默认空段 | 未勾自订的晚被前端自动塞 `{"roomCount":1,"candidates":[{}]}` | 该晚被当成「等房控统筹」的真实需求保存 |
## 5. 前端修复要求(已落地,ref=97baecb6)
1. **回显绑定**:弹窗初始化时 `days[i].customerSelfBooked === true` → 第 i 晚「客户自订」勾选框置勾,并按自订态渲染(候选/需求段区域禁用或隐藏,与首次勾选自订时的交互一致)。
2. **原样重交**:提交组装时每晚带上 `customerSelfBooked`(勾选=true,未勾=false 或省略),**不得因用户没动过该晚而丢键**。
3. **自订晚不补空段**:勾选自订的晚提交 `segments: []`,不要自动补默认空需求段;未勾自订且 segments 为空的晚按现有校验提示用户(后端 582098 兜底)。
## 6. 边界行为
- 快照中历史需求(#5838 上线前提交)无此标记 → `customerSelfBooked` 为 null,按未勾选处理,属正常。
- 该晚已有房务配房记录时再标自订 → 400(582097「第{0}晚已配房,不能改为客户自订,请先删除该晚配房」)。
- 后端对自订晚不计入配房进度分母、不列「未配房」提示,房务侧行程 Tab 显示「客户自订」。
## 7. 测试环境已验证
```
GET /v3/admin/order/2091418470443810817/adjustment/snapshot?scope=HOTEL_REQ → 200 + days[] 每项含 customerSelfBooked 键 ✓
```
验证订单:`HL20260823145314586`(出行 2026-08-27,6 天 5 晚)。
## 8. 数据善后(TEST 订单 HL20260823145314586)
该订单第 1/3/4/5 晚自订标记已丢,当前对房务显示为「待配房」。前端修复已部署(ref=97baecb6):由顾问重新打开调整弹窗把这 4 晚重新勾「客户自订」提交即可恢复(走正常业务路径生成新版本);如需立即恢复可联系后端补数(需 wx 确认后另起数据单)。
## 关联 / 联系人
### 链接
- **Issue**: [#6210](https://git.1814.love:8443/wx/HL/issues/6210)(已关闭,含完整定位证据链)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,92 @@
---
schema: "hl-changelog/v2"
ticket: "6241"
title: "供应商资质类型字典化及证件编号公司级校验"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "a3ef713b"
target_release: ""
verified_at: "2026-08-24"
status_note: "PR #6254 已合并 dev-v3,合并提交 b17ace883 已随 Resource 双实例滚动部署到 TEST。供应商新增、编辑和提交统一校验 company_qualification_type 生效字典值及证件编号通用安全格式;权威/国家核验按确认口径降级为公司级校验。"
updated_at: "2026-08-24"
base: "dev-v3"
---
# 供应商资质类型字典化及证件编号公司级校验
供应商资质写入不再接受任意资质类型。新增、编辑和提交均以 User 服务的 `company_qualification_type` 生效项为准,并通过公共校验组件统一处理证件编号。
本次“核验”仅表示输入符合公司系统的格式和安全约束,不代表证照真实存在,也不声明已通过国家平台或第三方权威核验。
## 变更接口
| 方法 | 路径 | 行为变化 |
|---|---|---|
| POST | `/admin/supplier/items/add` | 显式资质类型必须是生效字典值;草稿证件编号可空,非空时校验通用格式 |
| PUT | `/admin/supplier/items/{supplierId}/update` | 新增或显式修改的资质类型、证件编号执行同一规则;未传类型的历史资质保持兼容 |
| POST | `/admin/supplier/items/{supplierId}/submit` | 提交时每项资质必须有合法证件编号;营业执照快捷影像也受字典门禁约束 |
请求和响应字段结构未变化。`qualType` 仍提交字符串,但其值必须来自:
`GET /admin/dict/data/company_qualification_type`
当前 TEST 已启用以下五项:
| 字典值 | 标签 |
|---|---|
| `BUSINESS_LICENSE` | 营业执照 |
| `INDUSTRY_LICENSE` | 行业经营许可证 |
| `FOOD_OPERATION_LICENSE` | 食品经营许可证 |
| `PUBLIC_HEALTH_LICENSE` | 公共场所卫生许可证 |
| `FIRE_SAFETY_CERTIFICATE` | 消防安全证明 |
## 证件编号规则
- 自动去除首尾空白,最大为 128 个 UTF-8 字节。
- 允许 Unicode 字母、数字及常见中英文证照分隔符。
- 排除空白、控制字符及可组成 HTML 标签的半角尖括号。
- 草稿允许为空;提交审批时每项资质必须填写。
- 读取仍只返回脱敏证件号,不返回明文。
## 新增业务错误码
| 错误码 | 含义 | 前端处理建议 |
|---:|---|---|
| `395040` | 资质类型不合法或已停用 | 刷新 `company_qualification_type` 字典并要求用户重新选择 |
| `395041` | 证件编号格式不合法 | 保留表单,提示用户修正编号;提交场景同时检查是否为空 |
业务失败可能仍使用 HTTP 200,客户端必须同时判断统一响应的 `code`、`success` 和 `data`。
## 兼容性与前端事项
- 不新增数据库 migration、配置、Redis、MQ 或跨服务写入。
- 既有历史自由文本资质可以读取;编辑既有项且不传 `qualType` 时不会被强制改写。
- 前端应将资质类型自由输入改为 `company_qualification_type` 字典选择,并只提交 `dictValue`。
- 前端无需修改字段名、JSON 类型或接口路径。
## 验证证据
- 自动化:Resource 全量 1925 项零失败(38 项仓库既有条件跳过);#6241 定向 35 项零失败;独立审计 48 项零失败。
- 部署:TEST 部署任务退出码 0,`hl-resource-service` 的 8182、8082 双实例依次健康;服务器部署 HEAD `04a6dba66` 明确包含合并提交 `b17ace883` 和功能提交 `bc899930e`。
- 真实 Gateway:非法资质类型返回“资质类型不合法或已停用”,非法证件编号返回“证件编号格式不合法”,两次失败后列表仍为原 3 条。
- 真实 Gateway:使用五个生效字典值和多种常见编号格式成功创建草稿;详情页五个证件号均为脱敏值。
- 清理:TEST 草稿删除成功,供应商列表恢复原 3 条,未留下测试数据。
## 撤回
1. 从最新 `dev-v3` 创建回退分支,revert #6254 合并提交 `b17ace88399269710ea515e9b19e63a159b888c9`,经独立 PR 合入。
2. 重新构建并滚动部署 `hl-resource-service`;无需恢复数据库、User 字典、配置、Redis 或 MQ。
3. 回退后既有资质数据保持可读,不做数据回写;写接口会恢复为旧的自由文本行为。
4. 经 Gateway 重跑新增、编辑、提交、非法类型、非法编号和读取脱敏用例,并核对失败零写入。
## 关联 / 联系人
- **Issue**: [#6241](https://git.1814.love:8443/wx/HL/issues/6241)
- **PR**: [#6254](https://git.1814.love:8443/wx/HL/pulls/6254)
- **合并提交**: [b17ace883](https://git.1814.love:8443/wx/HL/commit/b17ace88399269710ea515e9b19e63a159b888c9)
- **后端负责人**: @lc
@@ -0,0 +1,69 @@
---
schema: "hl-changelog/v2"
ticket: "6257"
title: "供应商初始收款账户最多一项"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "a3ef713b"
target_release: ""
verified_at: "2026-08-24"
status_note: "PR #6259 已合并 dev-v3,合并提交 1b74ad719 已随 Resource 双实例滚动部署到 TEST。创建供应商草稿和提交注册共享的 initialAccounts 现最多一项;历史多账户草稿即使省略账户快照,也会在审批创建前失败关闭。"
updated_at: "2026-08-24"
base: "dev-v3"
---
# 供应商初始收款账户最多一项
供应商注册流程中的初始收款账户从“最多 50 项”收紧为“最多 1 项”。创建草稿与提交注册使用同一请求结构和服务端门禁,不能通过绕过 Bean Validation 或省略提交快照继续保留多个初始账户。
## 变更接口
| 方法 | 路径 | 行为变化 |
|---|---|---|
| POST | `/admin/supplier/items/add` | `initialAccounts` 为 `null`、空集合或单元素时允许继续;二项及以上拒绝,供应商主体和账户均不写入 |
| POST | `/admin/supplier/items/{supplierId}/submit` | 显式账户快照最多一项;省略快照时会锁定并检查草稿现有账户,历史多账户草稿在创建审批前拒绝提交 |
`initialAccounts` 字段名、JSON 类型和响应结构均未变化。
## 校验与错误语义
- 请求包含二项及以上初始账户时,参数校验消息为 `初始收款账户最多1项`。
- 绕过 Controller 直接调用 Service 时仍执行同一数量门禁。
- 历史草稿已有多个初始账户且提交请求省略 `initialAccounts` 时,返回既有业务错误码 `395009`,消息为 `请修正随单收款账户后重新提交注册`。
- 失败发生在主体、账户、审计和审批写入之前,供应商保持草稿状态。
- 业务失败可能仍使用 HTTP 200,客户端必须同时检查统一响应的 `code`、`success`、`message` 和 `data`。
## 未变化的接口与兼容性
- 独立账户接口 `POST /admin/supplier/items/{supplierId}/bank-accounts/add` 仍支持每批 1 至 50 项,不受本次注册初始账户上限影响。
- 历史多账户草稿不会被自动删除或批量清理;用户可在完整提交快照中收敛为零或一个账户后继续注册。
- 已生效供应商的多账户维护能力不变。
- 不新增数据库 migration,不修改配置、Redis、MQ、Gateway、审批状态机或跨服务契约。
- 管理端当前仍需将旧的“最多 50 项”注册提示和交互改为最多一项;该前端工作不在本次后端工单范围内。
## 验证证据
- 自动化:#6257 定向 38 项零失败;Resource 全量 1928 项零失败(38 项仓库既有条件跳过);合并提交独立审计再跑 38 项零失败。
- 部署:TEST 部署任务退出码 0,`hl-resource-service` 的 8182、8082 双实例依次健康;服务器部署 HEAD `22be9bd8d` 明确包含合并提交 `1b74ad719` 和功能提交 `d95386706`。
- 真实 Gateway:二账户创建返回 `初始收款账户最多1项`,列表数量不变;单账户创建成功。
- 历史兼容:预置二账户草稿,提交表单省略 `initialAccounts` 时返回 `请修正随单收款账户后重新提交注册`,列表仍显示草稿且未进入审批。
- 清理:两条成功创建的 TEST 草稿均删除成功,供应商列表从 5 条恢复到原 3 条。资质表单上传的一份合成 SVG 由文件服务即时落入 TEST OSS,未关联任何供应商且不含真实证照或敏感信息;当前业务界面没有附件删除入口,已作为无业务关联测试附件残留登记。
## 撤回
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit 1b74ad719d535244350bb41f78d29644b539e684`,经独立 PR 合入。
2. 重新构建并滚动部署 `hl-resource-service`;无需恢复数据库、配置、Redis 或 MQ。
3. 回退后创建与提交的 `initialAccounts` 上限恢复为 50;现有表结构、历史草稿和已生效供应商账户数据保持兼容,不做数据回写。
4. 经 Gateway 复测空账户、单账户、二账户、历史多账户草稿省略快照,以及独立批量新增 1 至 50 个账户。
## 关联 / 联系人
- **Issue**: [#6257](https://git.1814.love:8443/wx/HL/issues/6257)
- **PR**: [#6259](https://git.1814.love:8443/wx/HL/pulls/6259)
- **合并提交**: [1b74ad719](https://git.1814.love:8443/wx/HL/commit/1b74ad719d535244350bb41f78d29644b539e684)
- **后端负责人**: @lc
@@ -0,0 +1,96 @@
---
schema: "hl-changelog/v2"
ticket: "6258"
title: "供应商联系人默认标识与唯一性逻辑"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "36d66367"
target_release: ""
verified_at: "2026-08-24"
status_note: "PR #6264 已合并 dev-v3,合并提交 5c27bee4f 已在 TEST Gateway 验证。供应商联系人请求和详情响应新增 isPrimary;服务端在聚合锁与本地事务内保证同一供应商至多一个默认联系人,并兼容旧请求省略字段。"
updated_at: "2026-08-24"
base: "dev-v3"
---
# 供应商联系人默认标识与唯一性逻辑
供应商注册和资料维护链路为联系人增加默认标识 `isPrimary`。字段为可空 Boolean;服务端根据创建、完整提交和增量更新的不同语义处理省略值,并保证一个供应商最多只有一个默认联系人。
## 变更接口
| 方法 | 路径 | 行为变化 |
|---|---|---|
| POST | `/admin/supplier/items/add` | 请求 `contacts[].isPrimary` 可选;省略按 `false` 保存;显式提交多个 `true` 时整次创建失败且零写入 |
| PUT | `/admin/supplier/items/{supplierId}/update` | 已有联系人省略 `isPrimary` 时保持原值;新增联系人省略时按 `false`;显式将另一联系人设为 `true` 时在同一事务内清除原默认联系人 |
| POST | `/admin/supplier/items/{supplierId}/submit` | 完整联系人快照支持 `isPrimary`;省略按 `false`;显式提交多个 `true` 时在主体、联系人、审计和审批写入前失败 |
| GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应 `contacts[]` 新增 Boolean 字段 `isPrimary`;联系电话仍只返回脱敏字段 `contactPhoneMask` |
请求和响应示例:
```json
{
"contacts": [
{
"contactId": "1234567890123456789",
"contactName": "联系人甲",
"contactPhone": "13800000000",
"contactRole": "BIZ",
"isPrimary": true,
"expectedUpdateTime": "2026-08-24 17:30:00"
}
]
}
```
`Long` 类型的 `contactId` 继续按字符串传输,时间格式保持 `yyyy-MM-dd HH:mm:ss`。
## 字段语义与兼容性
- 创建草稿和完整提交:`isPrimary=true` 表示默认联系人;`false`、`null` 或省略均按非默认联系人处理。
- 增量更新已有联系人:省略 `isPrimary` 表示不修改该联系人当前默认状态;显式 `false` 表示取消默认。
- 增量更新新增联系人:省略 `isPrimary` 按 `false` 保存。
- 显式设置新的默认联系人时,服务端锁定当前联系人集合,在同一聚合事务内清除原默认联系人并设置新值。
- 允许没有默认联系人;取消或删除默认联系人后不会自动选举其他联系人。
- 旧客户端不传 `isPrimary` 时,创建、更新和提交请求保持兼容。
- 不新增或修改数据库 migration;复用既有 `supplier_contact.is_primary` 列。
## 校验与错误语义
- 单次请求显式包含两个及以上 `isPrimary=true` 时返回业务码 `400`,消息为 `同一供应商只能设置一个默认联系人`。
- 重复默认校验在业务写入前执行;失败请求不会修改供应商版本、联系人、审计记录或审批状态。
- 写权限保持不变:`FINANCE` 和 `SUPER_ADMIN` 可写,`ADMIN` 返回业务码 `395002`、消息 `无权执行该供应商写操作`。
- 业务失败可能仍使用 HTTP 200,客户端必须同时检查统一响应的 `code`、`success`、`message` 和 `data`。
## 未变化范围
- 不新增接口、Gateway 路由、菜单权限、角色、状态机或审批节点。
- 不改变联系人电话加密存储、脱敏输出、软删除、审计、聚合锁、幂等与乐观版本语义。
- 不修改配置、Redis Key 或 MQ 契约。
- 本工单仅交付后端;管理端需按本记录接入 `isPrimary` 字段和默认联系人交互。
## 验证证据
- 自动化:定向 37 项零失败;Resource 全量 1941 项零失败、38 项仓库既有条件跳过;Gateway 8 项零失败;合并后独立审计再跑权限、事务和契约相关 55 项零失败。
- TEST Gateway:35 个真实认证断言通过,覆盖创建单默认、切换默认、旧请求省略字段、显式取消后零默认、创建/更新/提交多个默认失败且零写入、电话脱敏和 ADMIN 越权拒绝。
- TEST 数据库:两个有效联系人切换后统计为 `2:1`,显式取消后统计为 `2:0`。
- 部署证明:当前远端 `dev-v3` 包含合并提交 `5c27bee4f`,TEST Gateway 已实际回显并执行新增 `isPrimary` 行为。
- 清理:所有合成供应商草稿均已软删除,按测试前缀查询有效数据为 0;测试账号当前角色已恢复为 `SUPER_ADMIN`,此前登录失败计数已通过管理接口清理。
## 撤回
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit 5c27bee4f5d76d85c9c58727c05b330bc06ab608`,经独立 PR 合入。
2. 重新构建并滚动部署 `hl-resource-service`;不执行 DDL 或 DML,不恢复配置、Redis 或 MQ。
3. 回退代码会忽略既有 `supplier_contact.is_primary` 值;旧请求继续兼容,依赖新字段的管理端应同步停止使用 `isPrimary`。
4. 经 Gateway 复测创建、更新、提交和详情四个既有接口,并确认联系人电话仍脱敏、草稿删除与权限门禁正常。
## 关联 / 联系人
- **Issue**: [#6258](https://git.1814.love:8443/wx/HL/issues/6258)
- **PR**: [#6264](https://git.1814.love:8443/wx/HL/pulls/6264)
- **合并提交**: [5c27bee4f](https://git.1814.love:8443/wx/HL/commit/5c27bee4f5d76d85c9c58727c05b330bc06ab608)
- **后端负责人**: @lc
@@ -0,0 +1,74 @@
---
schema: "hl-changelog/v2"
ticket: "6265"
title: "供应商移除审批备注与提交说明"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "757c2f52"
target_release: ""
verified_at: "2026-08-24"
status_note: "PR #6267 已合并 dev-v3,合并提交 15340bfc0 已随 Resource 双实例滚动部署到 TEST。供应商创建、更新和注册提交请求不再暴露 approveNote,提交请求同时不再暴露 submitNote;仅保留内部 remark。旧客户端继续携带两个废弃字段时,服务端显式忽略且不写入业务、审计或审批数据。"
updated_at: "2026-08-24"
base: "dev-v3"
---
# 供应商移除审批备注与提交说明
供应商新增、修改和注册提交统一移除审批备注 `approveNote`,注册提交同时移除提交审批说明 `submitNote`。`remark` 是唯一保留的内部备注,不会转换为审批原因或发送给审批提供方。
## 变更接口
| 方法 | 路径 | 行为变化 |
|---|---|---|
| POST | `/admin/supplier/items/add` | 请求契约移除 `approveNote`,继续保留 `remark` |
| PUT | `/admin/supplier/items/{supplierId}/update` | 请求契约移除 `approveNote`,继续保留 `remark` |
| POST | `/admin/supplier/items/{supplierId}/submit` | 请求契约移除 `approveNote`、`submitNote`,继续保留 `remark` |
三个接口的路径、方法、统一响应结构和其余请求字段均未变化。
## 字段语义与兼容性
- `remark` 仅用于供应商内部备注,按原有新增、修改和完整提交语义持久化。
- 新请求模型和 Swagger 不再声明 `approveNote`、`submitNote`。
- 为兼容尚未同步的旧管理端,请求中继续携带 `approveNote` 或 `submitNote` 时不会报未知字段错误;服务端会显式忽略,不产生主体、审批记录、变更记录或审批候选写入。
- `remark` 不会替代已移除字段,也不会进入审批原因。
- 提交前完整表单审计原因固定为 `提交注册前保存完整表单`。
- 发起注册审批的候选原因固定为 `提交供应商注册审批`。
- 旧加密审批候选 JSON 即使包含 `approveNote` 仍可读取和重放,原幂等、并发和状态门禁保持不变。
## 数据兼容与未变化范围
- 不执行数据库 migration、DDL 或 DML。
- `supplier_main`、`supplier_approval_log`、`supplier_change_log` 中既有 `approve_note` 物理列和历史值保留,本次部署后不再由新链路写入。
- 不清理、不回写历史审批备注或提交说明。
- 写权限不变:`FINANCE`、`SUPER_ADMIN` 继续使用既有成功路径,`ADMIN` 继续按既有权限错误语义拒绝。
- 不修改 Gateway 路由、认证策略、供应商状态机、事务、聚合锁、幂等、软删除、Redis、MQ、Nacos 或跨服务契约。
- 本工单仅交付后端;管理端需移除新增、修改和提交表单中的废弃字段,仅保留内部备注。
## 验证证据
- 自动化:定向 57 项零失败;Resource 全量 1944 项零失败、38 项仓库既有条件跳过;合并提交独立审计再跑 57 项零失败。
- 部署:TEST 部署任务 `8301cec0` 退出码 0,`hl-resource-service` 的 8182、8082 双实例依次健康;部署服务器 HEAD `d1546d477` 包含目标合并提交 `15340bfc0` 和功能提交 `d7a2e98eb`。
- 真实 Gateway:以有效管理端身份携带旧 `approveNote` 完成草稿创建,并在更新时同时提交旧 `approveNote` 与新的 `remark`,创建和更新均成功,供应商保持草稿状态。
- 认证失败:列表、创建、更新和提交在缺少 Authorization 时统一返回业务码 `401`、`success=false`,未产生业务写入。
- 提交安全:未在 TEST 发起真实注册审批,避免产生企业微信审批和附件等外部副作用;废弃 `submitNote` 的忽略、固定审批原因及零差异由部署契约和自动化测试覆盖。
- 清理:合成草稿删除成功,供应商列表从 4 条恢复到测试前的 3 条,无供应商业务数据残留。
## 撤回
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit 15340bfc0e98b1eb64723441bf41e645f9cbe7d5`,经独立 PR 合入。
2. 重新构建并滚动部署 `hl-resource-service`;无需恢复数据库、配置、Redis 或 MQ。
3. 回退后请求契约会恢复 `approveNote`、`submitNote` 及原审批原因逻辑;既有物理列和历史值始终保留,不需要数据回写。
4. 经 Gateway 复测新增、修改、提交、内部备注、旧字段绑定、审批原因和旧候选重放。
## 关联 / 联系人
- **Issue**: [#6265](https://git.1814.love:8443/wx/HL/issues/6265)
- **PR**: [#6267](https://git.1814.love:8443/wx/HL/pulls/6267)
- **合并提交**: [15340bfc0](https://git.1814.love:8443/wx/HL/commit/15340bfc0e98b1eb64723441bf41e645f9cbe7d5)
- **后端负责人**: @lc
@@ -0,0 +1,259 @@
---
schema: "hl-changelog/v2"
ticket: "6266"
title: "供应商审批记录业务展示字段调整"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "cd7fd4c4"
target_release: ""
verified_at: ""
status_note: "PR #6270 已合并 dev-v3,合并提交 f14544fe0 已随 Resource 双实例滚动部署到 TEST。审批记录新增供应商名称、供应商类型、操作人姓名和审批角色,隐藏四个技术字段,并把变更前后值、状态和历史原因转换为中文脱敏业务展示。管理端需同步调整表格列和字段绑定。"
updated_at: "2026-08-24"
base: "dev-v3"
---
# 供应商审批记录业务展示字段调整
## ⚠️ 破坏性变更
供应商详情的审批记录响应改为面向业务展示:新增供应商名称、供应商类型、操作人姓名和审批角色;不再返回变更记录 ID、字段名、审批日志 ID 和操作人 ID。管理端必须按本契约调整表格列,不能继续依赖已移除字段。
## 变更接口
| 方法 | 路径 | 权限 | 行为变化 |
|---|---|---|---|
| GET | `/admin/supplier/items/{supplierId}/approval-records/page` | `supplier:approval:read` | 调整分页记录的响应字段和展示语义;请求参数、分页结构及权限不变 |
### 请求
```http
GET /admin/supplier/items/2091715622923657217/approval-records/page?page=1&pageSize=10 HTTP/1.1
Host: api.test.1814.love:9443
Authorization: Bearer <adminToken>
```
GET 请求无请求体。
#### 路径参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---:|---|
| `supplierId` | String | 是 | 供应商 ID |
#### 查询参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---:|---|---|
| `page` | Integer | 否 | `1` | 页码,最小为 1;兼容别名 `pageNo` |
| `pageSize` | Integer | 否 | `20` | 每页条数,范围 1–100 |
| `approvalLogId` | String | 否 | — | 按关联审批日志 ID 精确筛选;仅为请求筛选条件,响应不再回传该字段 |
| `operationType` | String | 否 | — | `CREATE`、`UPDATE`、`ENABLE`、`DISABLE`、`DELETE` |
| `targetType` | String | 否 | — | 当前仅支持 `SUPPLIER` |
| `fieldName` | String | 否 | — | 按历史变更字段精确筛选;仅为请求筛选条件,响应不再回传该字段 |
| `status` | String | 否 | — | 以生命周期编码筛选:`DRAFT`、`VETTING`、`ACTIVE`、`SUSPENDED`、`FROZEN`、`BLACKLIST`、`ARCHIVED` |
| `from` | String | 否 | — | 开始时间,格式 `yyyy-MM-dd HH:mm:ss`;必须与 `to` 同时传入 |
| `to` | String | 否 | — | 结束时间,格式 `yyyy-MM-dd HH:mm:ss`;不得早于 `from` |
| `sortBy` | String | 否 | `createTime` | `createTime` 或 `changeLogId` |
| `sortDirection` | String | 否 | `DESC` | `ASC` 或 `DESC`,大小写均可 |
## 响应字段变化
### 新增字段
| 字段 | 类型 | 空值语义 | 说明 |
|---|---|---|---|
| `supplierName` | String | 供应商存在时非空 | 当前供应商法定全称 |
| `supplierTypes` | Array | 无类型时 `[]` | 当前供应商全部类型,主类型优先;`typeCode` 为字典值,`typeName` 为运行时中文名称 |
| `operatorName` | String | 不返回 `null` | 优先返回操作人的企微姓名,其次为用户名;系统记录显示 `系统`,人员信息暂不可用时显示 `管理员#<ID>` |
| `operatorRole` | String | 不返回 `null` | 审计发生时的角色快照中文名;空角色显示 `系统`,未知历史角色显示 `其他角色(<CODE>)` |
`supplierTypes[]` 元素结构:
| 字段 | 类型 | 说明 |
|---|---|---|
| `typeCode` | String | 供应商类型字典值 |
| `typeName` | String | 供应商类型中文名称 |
### 移除字段
| 原字段 | 前端处理 |
|---|---|
| `changeLogId` | 删除“变更 ID”列及所有字段读取 |
| `fieldName` | 删除“字段”列及所有字段读取;变更内容已经合并到中文摘要 |
| `approvalLogId` | 删除“审批日志 ID”列及所有字段读取 |
| `operatorId` | 改为展示 `operatorName`,不要再直接展示管理员 ID |
上述字段仅从响应记录中移除;`approvalLogId`、`fieldName` 和 `sortBy=changeLogId` 作为既有查询能力继续兼容。
### 保留字段与新语义
| 字段 | 类型 | 新语义 |
|---|---|---|
| `supplierId` | String | 供应商 ID |
| `operationType` | String | 操作类型编码保持不变 |
| `targetType` | String | 当前固定为 `SUPPLIER` |
| `targetId` | String / null | 主体变更记录为 `null` |
| `oldValueMasked` | String | 变更前的中文脱敏业务摘要,不再直接返回生命周期等原始编码 |
| `newValueMasked` | String | 变更后的中文脱敏业务摘要,不再直接返回生命周期等原始编码 |
| `changeReason` | String | 业务原因;已识别的历史注册记录同步校正为正确原因 |
| `status` | String / null | 变更后生命周期中文名,不再返回原始状态编码 |
| `createTime` | String | `yyyy-MM-dd HH:mm:ss` |
生命周期展示值如下:
| 原编码 | 响应中文值 |
|---|---|
| `DRAFT` | `草稿` |
| `VETTING` | `注册审核中` |
| `ACTIVE` | `合作中` |
| `SUSPENDED` | `暂停合作` |
| `FROZEN` | `已冻结` |
| `BLACKLIST` | `黑名单` |
| `ARCHIVED` | `已归档` |
审批角色常用展示值如下:
| 角色快照 | `operatorRole` |
|---|---|
| `SUPER_ADMIN` | `超级管理员` |
| `ADMIN` | `管理员` |
| `FINANCE` | `财务` |
| `OPERATOR` | `运营人员` |
| `CUSTOMIZER` | `定制师` |
| `HOUSEKEEPING_ADMIN` | `房务管理员` |
| `LOGISTICS_ADMIN` | `车务管理员` |
| `VEHICLE_MANAGER` | `车辆管理员` |
历史注册链路中可明确识别的原因统一为:
| 场景 | `changeReason` |
|---|---|
| 创建供应商草稿 | `创建供应商注册草稿` |
| 草稿提交注册审核 | `提交供应商注册审批` |
| 注册审批通过并启用 | `供应商注册审批通过并启用合作` |
## 典型成功响应
```json
{
"code": 200,
"message": "成功",
"traceId": "6266-success-example",
"success": true,
"data": {
"records": [
{
"supplierId": "2091715622923657217",
"supplierName": "内蒙古示例景区有限公司",
"supplierTypes": [
{
"typeCode": "SCENIC",
"typeName": "景区管理"
}
],
"operationType": "ENABLE",
"targetType": "SUPPLIER",
"targetId": null,
"oldValueMasked": "供应商状态:注册审核中;供应商编号:无",
"newValueMasked": "供应商状态:合作中;供应商编号:SUP2091715622923657217",
"changeReason": "供应商注册审批通过并启用合作",
"status": "合作中",
"operatorName": "示例管理员",
"operatorRole": "超级管理员",
"createTime": "2026-08-24 10:34:34"
}
],
"total": 1,
"page": 1,
"pageSize": 10
}
}
```
前端不能从示例中的名称、类型或状态推导固定值;供应商名称和类型取当前档案,状态、角色和变更内容由服务端提供中文展示。
## 边界响应
供应商存在但筛选条件下没有审批记录时返回空分页,不返回 `null`:
```json
{
"code": 200,
"message": "成功",
"traceId": "6266-empty-example",
"success": true,
"data": {
"records": [],
"total": 0,
"page": 1,
"pageSize": 10
}
}
```
## 错误语义
| 场景 | HTTP | 业务码 | 说明 |
|---|---:|---:|---|
| 缺少或无效认证 | 可能为 200 | `401` | `success=false`;不能只判断 HTTP 状态 |
| 无可信管理员身份、角色或 `supplier:approval:read` 权限 | 403 | `403` | 拒绝查询,不返回审批记录 |
| 供应商不存在或已删除 | 200 | `395001` | `供应商不存在` |
| 查询枚举、分页或时间范围非法 | 200 | `400` | 返回对应参数校验消息 |
未认证示例:
```json
{
"code": 401,
"message": "缺少有效的 Authorization 头",
"data": null,
"traceId": "6266-auth-example",
"success": false
}
```
## 脱敏与兼容约束
- 税号、联系电话、证件号和银行账号等敏感内容始终按字段语义重新脱敏;历史摘要也不会因旧数据格式而返回完整敏感值。
- 集合型资料只返回有界中文摘要;未知历史资料显示为安全的“其他资料:已变更”,客户端不要解析摘要反推结构化表单。
- 无变更值统一显示 `—`;前端直接展示服务端摘要,不再对 `oldValueMasked`、`newValueMasked` 或 `status` 做生命周期编码翻译。
- `supplierName` 和 `supplierTypes` 是当前供应商档案信息,不是每次审批发生时的历史快照。
- 本次不修改请求参数、分页外壳、认证方式、权限码、Gateway 路由或操作类型编码。
## 管理端改造清单
1. 隐藏“变更 ID”“字段”“审批日志 ID”列。
2. 新增“供应商名称”“供应商类型”“操作人”“审批角色”列,分别绑定 `supplierName`、`supplierTypes`、`operatorName`、`operatorRole`。
3. “变更前(脱敏)”“变更后(脱敏)”直接展示 `oldValueMasked`、`newValueMasked`,不再显示原始状态编码。
4. “状态快照”直接展示中文 `status`;操作类型筛选仍提交既有英文编码。
5. 删除对 `changeLogId`、`fieldName`、`approvalLogId`、`operatorId` 的响应依赖。
## 验证证据
- 自动化:审批记录投影与契约定向测试 22 项零失败;Supplier 聚焦回归 196 项零失败(1 项条件跳过);Resource 全量 1953 项零失败(38 项仓库既有条件跳过)。
- 部署:TEST 部署任务 `73bfe0aa` 成功、退出码 0;`hl-resource-service` 的 8182、8082 双实例依次启动并健康。部署服务器 HEAD `4de8e1703` 包含目标合并提交 `f14544fe0` 和功能提交 `bdcb48d78`。
- 真实 Gateway:管理端开发代理明确指向 `https://api.test.1814.love:9443`;有效超级管理员查询得到 `code=200`、`success=true` 和 4 条真实审批记录。
- 响应断言:四个新增字段在每条记录中均存在,四个移除字段均不存在;变更前后摘要和状态未出现 `DRAFT`、`VETTING`、`ACTIVE` 等原始生命周期编码;操作人返回姓名、审批角色返回中文。
- 原因断言:同一供应商的创建、提交、审批通过记录分别返回正确中文业务原因。
- 认证门禁:直接经 TEST Gateway 不携带 Authorization 查询,返回 HTTP 200、业务码 `401`、`success=false`。
- 清理:本次 TEST 验收仅执行只读查询,未创建、修改或删除业务数据,无需数据清理。
## 撤回
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit f14544fe0430ccafb7b74bca1ef43c080eabf491`,经独立 PR 合入。
2. 重新构建并滚动部署 `hl-resource-service`;无需恢复数据库、配置、Redis 或 MQ,也没有不可逆数据影响。
3. 回退后审批记录会恢复旧响应字段和旧展示语义;管理端若已接入新字段,必须同步回退字段绑定,避免空列。
4. 经 Gateway 复测成功分页、空分页、未认证、无权限、中文/旧编码展示、敏感字段脱敏和历史原因。
5. 撤回本 Changelog 时,以新的文档提交删除本文件并通知管理端停止消费新契约;不要重写已发布提交历史。
## 关联 / 联系人
- **Issue**: [#6266](https://git.1814.love:8443/wx/HL/issues/6266)
- **PR**: [#6270](https://git.1814.love:8443/wx/HL/pulls/6270)
- **合并提交**: [f14544fe0](https://git.1814.love:8443/wx/HL/commit/f14544fe0430ccafb7b74bca1ef43c080eabf491)
- **后端负责人**: @lc
@@ -0,0 +1,117 @@
---
schema: "hl-changelog/v2"
ticket: "6273"
title: "供应商审批记录补充供应商编号"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "a46da334"
target_release: ""
verified_at: "2026-08-24"
status_note: "PR #6274 已合并 dev-v3,合并提交 588573ae4 已随 Resource 双实例滚动部署到 TEST。审批记录分页项新增当前供应商编号 supplierNo;既有目标字段、脱敏字段、权限、分页和闭区间时间筛选保持不变。管理端列展示和自然日边界组装仍待前端交付。"
updated_at: "2026-08-24"
base: "dev-v3"
---
# 供应商审批记录补充供应商编号
供应商详情的审批记录分页项新增当前供应商编号 `supplierNo`,供管理端直接展示。此次是向后兼容的响应扩展,不删除或重命名既有字段,也不改变查询、权限和脱敏语义。
## 变更接口
| 方法 | 路径 | 权限 | 行为变化 |
|---|---|---|---|
| GET | `/admin/supplier/items/{supplierId}/approval-records/page` | `supplier:approval:read` | 每条分页记录新增 `supplierNo` |
请求参数、分页外壳和排序规则均未变化。
## 新增响应字段
| 字段 | 类型 | 空值语义 | 说明 |
|---|---|---|---|
| `supplierNo` | String / null | 尚未生成编号的草稿返回 `null` | 当前供应商编号,直接来自供应商主体档案;不能从供应商 ID、审计 JSON 或展示摘要推导 |
典型分页项如下;示例值仅说明字段结构:
```json
{
"supplierId": "2091715622923657217",
"supplierNo": "SUP2091715622923657217",
"supplierName": "示例供应商",
"supplierTypes": [],
"operationType": "UPDATE",
"targetType": "SUPPLIER",
"targetId": null,
"oldValueMasked": "统一社会信用代码:****",
"newValueMasked": "统一社会信用代码:****",
"changeReason": "提交注册前保存完整表单",
"status": "草稿",
"operatorName": "示例管理员",
"operatorRole": "超级管理员",
"createTime": "2026-08-24 10:34:34"
}
```
## 保持兼容的字段与行为
- `targetType` 和 `targetId` 继续返回;管理端可以隐藏“目标类型”列,但不能把前端隐藏等同于删除接口字段。
- `oldValueMasked` 和 `newValueMasked` 继续返回脱敏后的业务摘要。管理端表头可简化为“变更前”“变更后”,但不得改为读取或展示未脱敏数据。
- `page`、`pageSize`、`operationType`、`status`、`from`、`to`、`sortBy` 和 `sortDirection` 的请求约束不变。
- 查询仍使用 `createTime >= from AND createTime <= to` 的闭区间语义;服务端不替客户端推导自然日边界。
- 认证方式、`supplier:approval:read` 权限、管理员数据范围、排序、错误码、Gateway 路由和统一 `Result<T>` 外壳均未变化。
- 不新增数据库 migration,不修改配置、Redis、MQ、审批状态机、审计写入或跨服务契约。
## 管理端待办
1. 在审批记录表格最前面增加“供应商编号”列,绑定 `supplierNo`;空值显示 `—`。
2. 隐藏“目标类型”列,但保留接口兼容字段,不要求后端删除 `targetType`。
3. 将“变更前(脱敏)”“变更后(脱敏)”表头改为“变更前”“变更后”;单元格仍绑定 `oldValueMasked`、`newValueMasked`。
4. 日期查询开始值组装为当天 `00:00:00`,结束值组装为当天 `23:59:59`,并在查询条件变化后重置到第 1 页。
若选择同一天,不能把开始和结束都提交为 `00:00:00`。例如查询 2026-08-24 应提交:
```text
from=2026-08-24 00:00:00
to=2026-08-24 23:59:59
```
## 错误与边界语义
| 场景 | 结果 |
|---|---|
| `from` 晚于 `to` | 参数校验失败,消息为 `from不能晚于to` |
| 只传 `from` 或只传 `to` | 参数校验失败,时间边界必须成对提交 |
| 未认证 | 统一响应 `code=401`、`success=false`;HTTP 状态可能仍为 200 |
| 无读取权限或超出数据范围 | 服务端拒绝查询,不返回审批记录 |
| 筛选结果为空 | 返回空分页,`records=[]`,不返回 `null` |
## 验证证据
- 自动化:供应商审批记录 Controller、投影和查询聚焦测试 16 项零失败;Resource 全量 1954 项零失败,38 项仓库既有条件跳过。
- 部署:TEST 部署任务 `32288c37` 成功、退出码 0;`hl-resource-service` 的 8182、8082 双实例依次启动并健康。部署服务器 HEAD `435ae0a86` 包含目标合并提交 `588573ae4` 和功能提交 `9cf77177c`。
- 真实 Gateway:使用现有有效超级管理员登录态查询 4 条审批记录,每条均返回当前 `supplierNo`;`targetType`、`targetId`、`oldValueMasked` 和 `newValueMasked` 同时保留,脱敏摘要仍包含掩码。
- 分页兼容:请求 `page=1&pageSize=2` 返回 2 条记录、总数 4,新增字段不改变分页外壳。
- 时间边界:同一自然日 `00:00:00` 至 `23:59:59` 返回 4 条;旧前端把结束值提交为 `00:00:00` 时返回 0 条,证明日期问题来自客户端结束边界组装。
- 参数门禁:反向时间范围被拒绝并返回 `from不能晚于to`。
- 认证门禁:不携带 Authorization 经 TEST Gateway 查询,返回 HTTP 200、业务码 `401`、`success=false`;该结果仅作为负向门禁验证,不计作正向验收。
- 清理:本次 TEST 验收只执行只读分页查询,未产生数据库、Redis、MQ、文件或外部业务副作用,无需测试数据清理。
## 撤回
1. 先停止发布或回退依赖 `supplierNo` 的管理端版本,避免后端字段回退后出现空列。
2. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit 588573ae4ae016829d90dbb365f5c8ec3df981f0`,经独立 PR 合入。
3. 重新构建并滚动部署 `hl-resource-service`;无需恢复数据库、配置、Redis 或 MQ,没有不可逆数据影响。
4. 回退后接口不再保证返回 `supplierNo`;复测原有分页、权限、数据范围、脱敏字段、目标字段、排序和闭区间时间筛选。
5. 撤回本 Changelog 时,以新的文档提交删除本文件或明确标记撤回,并通知管理端停止消费新增字段;不要重写已发布提交历史。
## 关联 / 联系人
- **Issue**: [#6273](https://git.1814.love:8443/wx/HL/issues/6273)
- **PR**: [#6274](https://git.1814.love:8443/wx/HL/pulls/6274)
- **合并提交**: [588573ae4](https://git.1814.love:8443/wx/HL/commit/588573ae4ae016829d90dbb365f5c8ec3df981f0)
- **后端负责人**: @lc
- **前端负责人**: @mmg
@@ -0,0 +1,157 @@
---
schema: "hl-changelog/v2"
ticket: "6275"
title: "供应商联系人默认规则与角色字典校验"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "f8a0d72d"
target_release: ""
verified_at: ""
status_note: "PR #6276 已合并 dev-v3,合并提交 f7b442b67 已由部署任务 #785ce882 发布到 TEST 并经 Gateway 验证。非空联系人快照现在始终归一为且仅为一个默认联系人;联系人角色按 sup_content_role 当前 ACTIVE 字典值校验。管理端待将角色/职务改为动态下拉并适配新的默认联系人规则。"
updated_at: "2026-08-24"
base: "dev-v3"
---
# 供应商联系人默认规则与角色字典校验
供应商创建、草稿更新和注册提交统一收紧联系人契约:联系人列表非空时,服务端保证最终恰好一个默认联系人;联系人角色必须使用字典 `sup_content_role` 当前启用项的 `dictValue`。本记录覆盖 #6258 中“允许没有默认联系人、取消或删除后不自动选举”的旧语义。
## 变更接口
| 方法 | 路径 | 行为变化 |
|---|---|---|
| POST | `/admin/supplier/items/add` | 非空 `contacts` 未显式选择默认项时,服务端将请求第一项设为默认;联系人角色按生效字典校验 |
| PUT | `/admin/supplier/items/{supplierId}/update` | 非空联系人快照最终未保留或选中默认项时,请求第一项自动成为默认;显式提交的角色按生效字典校验 |
| POST | `/admin/supplier/items/{supplierId}/submit` | 完整联系人快照执行相同的唯一默认归一和角色字典校验;失败发生在主体、联系人和审批写入前 |
| GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 返回服务端归一后的 `contacts[].isPrimary` 和已保存的 `contacts[].contactRole`;电话仍仅返回 `contactPhoneMask` |
管理端角色下拉继续使用既有通用字典接口,该接口本工单未修改:
| 方法 | 路径 | 用途 |
|---|---|---|
| GET | `/admin/dict/data/sup_content_role` | 获取联系人角色选项;展示 `dictLabel`,提交 `dictValue`,仅使用 `status=ACTIVE` 的项 |
业务失败可能仍返回 HTTP 200,调用方必须同时判断统一响应中的 `code`、`success`、`message` 和 `data`。
## 默认联系人规则
- `contacts` 为非空列表时,保存后必须且只会有一个 `isPrimary=true`。
- 只有一个联系人时,即使请求传 `false`、`null` 或省略 `isPrimary`,该联系人也会成为默认联系人。
- 多个联系人且没有任何项传 `true` 时,按请求数组顺序将第一项设为默认联系人。
- 恰好一个联系人传 `true` 时保留该选择。
- 两个及以上联系人传 `true` 时返回业务码 `400`、消息 `同一供应商只能设置一个默认联系人`,整次请求零写入。
- 草稿增量更新中,已有联系人省略 `isPrimary` 时优先保留原默认状态;显式传 `false` 表示取消。若删除或取消原默认联系人后没有其他默认项,则请求列表第一项自动成为默认。
- 增量更新的 `contacts=null` 或省略字段表示联系人不变;`contacts=[]` 表示清空联系人,空列表允许零默认联系人。
- 客户端应在提交后重新读取详情,以服务端返回的 `isPrimary` 为最终状态。
创建或完整提交示例(两个联系人均未选择默认):
```json
{
"contacts": [
{
"contactName": "联系人甲",
"contactPhone": "13800000001",
"contactRole": "contentBus",
"isPrimary": false
},
{
"contactName": "联系人乙",
"contactPhone": "13800000002",
"contactRole": "contentMoney",
"isPrimary": false
}
]
}
```
保存后详情中的第一项为默认联系人:
```json
{
"contacts": [
{
"contactName": "联系人甲",
"contactPhoneMask": "138****0001",
"contactRole": "contentBus",
"isPrimary": true
},
{
"contactName": "联系人乙",
"contactPhoneMask": "138****0002",
"contactRole": "contentMoney",
"isPrimary": false
}
]
}
```
`Long` 类型的 `contactId` 继续按字符串传输;更新已有联系人仍需携带详情返回的 `expectedUpdateTime`。
## 联系人角色字典规则
- 创建和注册提交的每个联系人都必须提供非空 `contactRole`,值必须精确匹配 `sup_content_role` 当前 `status=ACTIVE` 项的 `dictValue`。
- 草稿增量更新已有联系人时,省略 `contactRole` 表示保持原值,不会因为本次无关更新而额外依赖字典;新增联系人或显式修改角色时必须提交生效的 `dictValue`。
- 空白、未知或已停用值返回业务码 `400`、消息 `联系人角色不合法或已停用`,失败请求不修改供应商或联系人。
- 字典服务异常、响应失败、空数据或没有启用项时失败关闭,返回“联系人角色字典暂不可用,请稍后重试”,不降级为硬编码选项。
- TEST 当前可见项为 `contentBus`(业务人员)和 `contentMoney`(财务人员),仅用于联调示例;客户端必须动态读取字典,不能将这些值固化在代码中。
字典响应示例:
```json
{
"code": 200,
"success": true,
"data": [
{
"dictType": "sup_content_role",
"dictLabel": "业务人员",
"dictValue": "contentBus",
"sortOrder": 1,
"status": "ACTIVE"
}
]
}
```
## 管理端接入事项
1. “角色/职务”字段改为下拉框,进入联系人页面时请求 `/admin/dict/data/sup_content_role`,展示 `dictLabel`、提交 `dictValue`,过滤非 `ACTIVE` 项。
2. 单联系人场景将该联系人呈现为默认;多联系人可选择一个默认联系人,但不可同时选择多个。未选择时服务端会把请求第一项设为默认。
3. 新增、删除、拖动或调整联系人顺序后,提交数组顺序应与界面顺序一致;保存成功后重新读取详情并以服务端返回值刷新默认标识。
4. 更新已有联系人时,未修改角色可省略 `contactRole`;新增联系人必须提交从字典选择的角色值。
## 未变化范围
- 不新增接口、Gateway 路由、数据库 migration、字典数据、菜单权限、角色、状态机或审批节点。
- 不改变联系人电话加密存储、脱敏输出、软删除、审计、聚合锁、幂等与乐观版本语义。
- 不修改配置、Redis Key 或 MQ 契约,也不回填历史联系人数据。
- 本工单只交付后端;管理端代码未在后端仓库修改,前端状态保持 `pending` 直至完成上述接入并提供验证提交。
## 验证证据
- 自动化:默认联系人写入 16 项、应用服务 14 项、角色字典 11 项、聚合校验 6 项,共 47 项定向测试零失败;Resource 全量 1966 项零失败,38 项仓库既有条件跳过。
- 合并后独立审计:在目标提交上重新执行 47 项定向测试,零失败且未发现确定性缺口。
- TEST 部署:部署任务 #785ce882 成功,`hl-resource-service` 的 8082、8182 实例滚动重启并恢复健康;部署服务器 HEAD `1f2e0ac6d` 包含目标合并提交 `f7b442b67`。
- TEST Gateway:使用真实管理员会话验证单联系人自动默认、多联系人无选择时第一项默认、删除默认后重新选举、角色省略保持原值、清空联系人、重复默认与非法角色零写入、提交失败零审批副作用;未认证请求返回业务码 `401`。
- 清理:合成供应商草稿已通过既有删除接口清理,精确查询结果为 0;未产生外部文件、Redis、MQ 或配置副作用。
## 撤回
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit f7b442b67f4d989fac6b29a771331ca6d66148e3`,经独立 PR 合入。
2. 重新构建并滚动部署 `hl-resource-service`;本次没有数据库、字典数据、配置、Redis 或 MQ 变更,无需执行 DDL、DML 或数据恢复。
3. 回退后恢复 #6258 的旧语义:允许联系人列表非空但没有默认联系人,角色不再由后端按 `sup_content_role` 动态校验;接口路径和字段结构不变。
4. 经 Gateway 复测创建、更新、提交和详情接口,确认回退后的默认联系人语义、角色传值、电话脱敏、权限和失败零写入符合目标版本。
5. 若管理端已上线本次交互,应同步停止依赖“服务端必定选出一个默认联系人”的保证;字典下拉仍可继续使用既有通用字典接口,但需按回退版本确认提交兼容性。
## 关联 / 联系人
- **Issue**: [#6275](https://git.1814.love:8443/wx/HL/issues/6275)
- **PR**: [#6276](https://git.1814.love:8443/wx/HL/pulls/6276)
- **合并提交**: [f7b442b67](https://git.1814.love:8443/wx/HL/commit/f7b442b67f4d989fac6b29a771331ca6d66148e3)
- **后端负责人**: @lc
@@ -0,0 +1,419 @@
---
schema: "hl-changelog/v2"
ticket: "6304"
title: "供应商资质增加永久有效与到期剩余天数"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "25fb4b27"
target_release: ""
verified_at: "2026-08-25"
status_note: "PR #6342 与补充修复 PR #6349 均已合并 dev-v3,TEST 最终部署提交为 af3a7bae5。真实 SUPER_ADMIN 经 Gateway 已验证显式永久有效、395042 冲突零写入、历史省略兼容和数据清理;未认证请求仍返回标准 401 业务响应。管理端尚待适配,frontend_status 保持 pending。"
updated_at: "2026-08-25"
base: "dev-v3"
---
# 供应商资质增加永久有效与到期剩余天数
供应商资质的有效期输入改为显式的“永久有效”契约;非永久有效资质必须提交到期日期。详情按上海时区自然日返回有符号剩余天数,便于页面直接展示“剩余 N 天”“今日到期”或“已过期 N 天”。
> **服务**: `hl-resource-service`
> **PR**: #6342、补充修复 #6349
> **Issue**: #6304、补充修复 #6347
> **日期**: 2026-08-25
> **影响范围**: 管理端供应商新建、编辑、提交审批和基本信息详情的资质证照区域
## ⚠️ 关键变化
- 资质写入字段新增 `permanentValid`;`true` 时不得提交 `expiryDate`,`false` 时必须提交 `expiryDate`。
- 详情资质项新增 `daysUntilExpiry`;永久有效时返回 `null`,有限期按 `Asia/Shanghai` 自然日返回正数、零或负数。
- 既有只读字段 `isRequired` 仍表示“该供应商类型是否要求此资质”,没有改义,也不能作为永久有效标识。
## 一、变更接口清单
| # | 接口 | 方法 | 路径 | 变化 |
|---|---|---|---|---|
| 1 | 创建供应商注册草稿 | POST | `/admin/supplier/items/add` | `qualifications[].permanentValid` 可写 |
| 2 | 更新供应商 | PUT | `/admin/supplier/items/{supplierId}/update` | `qualifications[].permanentValid` 可写,并支持增量保留语义 |
| 3 | 提交供应商注册审批 | POST | `/admin/supplier/items/{supplierId}/submit` | `qualifications[].permanentValid` 可写 |
| 4 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | `qualifications[]` 新增 `daysUntilExpiry` |
统一响应仍为 `Result<T>`。业务校验失败可能仍返回 HTTP 200,调用方必须同时判断 `code`、`success` 和 `data`。
## 二、字段契约
### 2.1 写入字段
| 字段 | 位置 | JSON 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `permanentValid` | `qualifications[]` | Boolean | 条件必填 | `true` 表示永久有效,`false` 表示存在到期日;历史调用方可省略并触发兼容推导 |
| `expiryDate` | `qualifications[]` | String / null | 条件必填 | 格式 `yyyy-MM-dd`;仅 `permanentValid=false` 时必须有值 |
合法组合:
| 场景 | `permanentValid` | `expiryDate` | 结果 |
|---|---:|---|---|
| 永久有效 | `true` | 省略或 `null` | 接受,到期日为空 |
| 有到期日 | `false` | `yyyy-MM-dd` | 接受,保存该到期日 |
| 历史永久请求 | 省略 | 省略或 `null` | 接受,兼容推导为 `true` |
| 历史有限期请求 | 省略 | `yyyy-MM-dd` | 接受,兼容推导为 `false` |
| 矛盾组合 | `true` | `yyyy-MM-dd` | 拒绝,业务码 `395042` |
| 缺少到期日 | `false` | 省略或 `null` | 拒绝,业务码 `395042` |
更新既有资质时还有以下增量规则:
- `permanentValid` 与 `expiryDate` 均省略:保留当前有效期,不做修改。
- 仅提交非空 `expiryDate`:兼容推导 `permanentValid=false`。
- 显式提交 `permanentValid=true` 且不提交到期日:切换为永久有效并清空原到期日。
- 新增资质且两字段均省略:按历史兼容口径创建为永久有效。
### 2.2 详情响应字段
| 字段 | JSON 类型 | 空值 | 说明 |
|---|---|---|---|
| `permanentValid` | Boolean | 不为空 | 由 `expiryDate` 是否为空动态推导 |
| `daysUntilExpiry` | Integer / null | 永久有效时为 `null` | 到期日相对上海业务当日的有符号自然日差 |
| `expired` | Boolean | 不为空 | 仅当 `daysUntilExpiry < 0` 时为 `true` |
| `isRequired` | Boolean | 不为空 | 历史只读兼容字段;继续表示供应商类型规则是否要求该资质 |
以 2026-08-25(上海日期)为例:
| `expiryDate` | `permanentValid` | `daysUntilExpiry` | `expired` |
|---|---:|---:|---:|
| `null` | `true` | `null` | `false` |
| `2026-08-26` | `false` | `1` | `false` |
| `2026-08-25` | `false` | `0` | `false` |
| `2026-08-24` | `false` | `-1` | `true` |
## 三、接口详情
### 3.1 创建供应商注册草稿 `POST /admin/supplier/items/add`
使用场景:新建供应商草稿时保存资质证照。`permanentValid` 位于每个 `qualifications[]` 项内。
典型请求:
```json
{
"fullName": "示例酒店管理有限公司",
"shortName": "示例酒店",
"taxNo": "91110108MA00000001",
"types": [
{
"typeCode": "HOTEL"
}
],
"mainCooperation": "酒店住宿服务",
"qualifications": [
{
"qualType": "BUSINESS_LICENSE",
"certNo": "LIC-2026-0001",
"imageUrl": "https://files.example.test/supplier/license-1.jpg",
"permanentValid": false,
"expiryDate": "2027-08-25"
}
]
}
```
成功响应:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2092152294748352514",
"supplierNo": null,
"status": "DRAFT",
"onboardingStage": "PROFILE_DRAFT",
"initialAccounts": [],
"updateTime": "2026-08-25 15:21:00"
}
}
```
永久有效请求只需将资质项改为:
```json
{
"qualType": "BUSINESS_LICENSE",
"certNo": "LIC-2026-PERM",
"imageUrl": "https://files.example.test/supplier/license-perm.jpg",
"permanentValid": true,
"expiryDate": null
}
```
### 3.2 更新供应商 `PUT /admin/supplier/items/{supplierId}/update`
使用场景:编辑既有资质或切换永久有效状态。`qualifications` 一旦传入,仍代表资质集合的完整当前快照;既有项必须携带 `qualificationId` 和该项的 `expectedUpdateTime`。
将有限期资质切换为永久有效:
```json
{
"qualifications": [
{
"qualificationId": "2092152300000000001",
"qualType": "BUSINESS_LICENSE",
"certNo": "LIC-2026-0001",
"imageUrl": "https://files.example.test/supplier/license-1.jpg",
"permanentValid": true,
"expiryDate": null,
"expectedUpdateTime": "2026-08-25 15:21:00"
}
],
"changeReason": "营业执照变更为永久有效",
"expectedUpdateTime": "2026-08-25 15:21:00"
}
```
成功响应:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2092152294748352514",
"supplierNo": null,
"status": "DRAFT",
"onboardingStage": "PROFILE_DRAFT",
"initialAccounts": [],
"updateTime": "2026-08-25 15:35:00"
}
}
```
若本次不修改既有资质的有效期,可同时省略该项的 `permanentValid` 和 `expiryDate`;不要回传未经确认的默认值。
### 3.3 提交供应商注册审批 `POST /admin/supplier/items/{supplierId}/submit`
使用场景:提交完整供应商表单。资质有效期组合规则与创建草稿完全一致。
典型请求:
```json
{
"fullName": "示例酒店管理有限公司",
"shortName": "示例酒店",
"taxNo": "91110108MA00000001",
"types": [
{
"typeCode": "HOTEL"
}
],
"mainCooperation": "酒店住宿服务",
"licenseImageUrl": "https://files.example.test/supplier/license-1.jpg",
"qualifications": [
{
"qualificationId": "2092152300000000001",
"qualType": "BUSINESS_LICENSE",
"certNo": "LIC-2026-0001",
"imageUrl": "https://files.example.test/supplier/license-1.jpg",
"permanentValid": false,
"expiryDate": "2027-08-25"
}
],
"expectedUpdateTime": "2026-08-25 15:35:00"
}
```
成功响应:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"approvalLogId": "2092152400000000001",
"requestNo": "SUP-PROFILE-20260825-0001",
"provider": "LOCAL_AUTO",
"approvalStatus": "APPROVED",
"spNo": null,
"spStatus": null,
"syncStatus": "APPLIED",
"submittedAt": "2026-08-25 15:36:00",
"finishedAt": "2026-08-25 15:36:00"
}
}
```
### 3.4 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
使用场景:展示供应商详情的资质证照。请求需要有效的管理端 `Authorization`,无请求体。
```http
GET /admin/supplier/items/2092152294748352514/basic-info/view
Authorization: Bearer <有效管理端令牌>
```
有限期资质成功响应(假设上海日期为 2026-08-25):
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2092152294748352514",
"supplierNo": null,
"fullName": "示例酒店管理有限公司",
"shortName": "示例酒店",
"tax_no": "9111********0001",
"legalRepresentative": null,
"contactPhoneMask": null,
"establishDate": null,
"registeredCapital": null,
"businessScope": null,
"staffScale": null,
"mainCooperation": "酒店住宿服务",
"status": "DRAFT",
"creditLevel": "B",
"totalScore": null,
"types": [
{
"typeCode": "HOTEL",
"typeName": "酒店"
}
],
"contacts": [],
"qualifications": [
{
"qualificationId": "2092152300000000001",
"qualType": "BUSINESS_LICENSE",
"qualTypeName": "营业执照",
"certNoMask": "LIC****0001",
"imageUrl": "https://files.example.test/supplier/license-1.jpg",
"expiryDate": "2026-08-26",
"permanentValid": false,
"daysUntilExpiry": 1,
"validityStatus": "VALID",
"validityStatusName": "有效",
"isRequired": true,
"expired": false,
"updateTime": "2026-08-25 15:21:00"
}
],
"updateTime": "2026-08-25 15:21:00"
}
}
```
永久有效资质的差异字段为:
```json
{
"expiryDate": null,
"permanentValid": true,
"daysUntilExpiry": null,
"validityStatus": "VALID",
"validityStatusName": "有效",
"expired": false
}
```
## 四、错误响应
新增业务错误码:
| 业务码 | 消息 | 触发条件 | 写入结果 |
|---:|---|---|---|
| `395042` | 永久有效标记与到期日期不一致 | `permanentValid=true` 且到期日非空,或 `permanentValid=false` 且到期日为空 | 整笔请求失败,零写入 |
异常请求片段:
```json
{
"qualifications": [
{
"qualType": "BUSINESS_LICENSE",
"permanentValid": true,
"expiryDate": "2027-08-25"
}
]
}
```
错误响应:
```json
{
"code": 395042,
"message": "永久有效标记与到期日期不一致",
"success": false,
"data": null
}
```
既有错误码继续有效,包括资质类型无效、证照编号无效、必备资质缺失、资质过期和并发版本冲突。
## 五、修改前后对比
### 字段级对比
| 字段 | 修改前 | 修改后 |
|---|---|---|
| 写入 `qualifications[].permanentValid` | 无显式写入契约,只能由到期日是否为空隐式表达 | 可显式提交;省略时仍兼容历史请求 |
| 响应 `qualifications[].daysUntilExpiry` | 无 | 有限期返回有符号整数,永久有效返回 `null` |
| 响应 `qualifications[].isRequired` | 供应商类型是否要求此资质 | 语义不变,继续只读返回 |
### 行为级对比
| 行为 | 修改前 | 修改后 |
|---|---|---|
| 永久有效输入 | 仅通过不传到期日表达 | 优先显式提交 `permanentValid=true`;旧请求仍兼容 |
| 非永久有效输入 | 提交到期日 | 提交 `permanentValid=false` 与到期日 |
| 到期提示 | 调用方自行按日期计算 | 直接读取 `daysUntilExpiry`,无需自行处理时区和跨日 |
## 六、管理端接入事项
1. 将资质证照区域的“是否必备”编辑控件改为“永久有效”;提交字段必须使用 `permanentValid`,不得复用或改写 `isRequired`。
2. `permanentValid=true` 时清空并禁用 `expiryDate`;`false` 时要求用户选择到期日。
3. 非永久有效时在到期日期附近显示后端返回的 `daysUntilExpiry`:正数表示剩余天数,零表示今日到期,负数表示已过期天数。
4. 永久有效时不显示到期天数。
5. 编辑既有数据以详情返回的 `permanentValid` 回显;不要从 `isRequired` 推导。
## 七、兼容性与影响评估
- **是否破坏向后兼容**: 否。旧客户端省略 `permanentValid` 时仍按 `expiryDate` 兼容推导。
- **前端是否必须同步上线**: 是。当前界面尚未提交新字段,也未展示剩余天数。
- **前端 workaround 清理点**: 若页面存在本地到期天数计算,改为直接消费 `daysUntilExpiry`。
- 不新增接口、Gateway 路由、权限点、数据库迁移、配置、Redis 或 MQ 契约。
- 不改变供应商状态机、审批语义、资质必备规则、证照编号脱敏、软删除和数据范围。
## 八、TEST 验证证据
- 原实现 PR #6342 与补充修复 PR #6349 均已合并;远端精确部署分支 `deploy/6347-supplier-qualification-null-expiry` 的 HEAD 与最终 `dev-v3` 合并提交 `af3a7bae5f3d19b97a7ad52daf31bb8fd18d5676` 完全一致,TEST 双实例健康。
- 真实 `SUPER_ADMIN` 经 Gateway 将既有有限期资质切换为永久有效后,更新成功;重新查询返回 `expiryDate=null`、`permanentValid=true`、`daysUntilExpiry=null`、`expired=false`、`validityStatus=VALID`。
- 同一资质提交 `permanentValid=true` 与非空 `expiryDate` 时返回业务码 `395042`、消息“永久有效标记与到期日期不一致”;失败前后供应商和资质的 `updateTime` 均保持 `2026-08-25 16:19:20`,证明零写入。
- 历史客户端省略 `permanentValid`、仅提交 `expiryDate=2026-08-26` 时更新成功;详情兼容返回 `permanentValid=false`、`daysUntilExpiry=1`、`expired=false`。
- 本工单创建的 DRAFT 测试供应商已通过业务删除接口清理;按精确测试名称分页查询 `total=0`,按原 ID 查询返回 `395001 供应商不存在`。
- 未认证访问供应商入口返回业务码 `401`、`success=false`、`data=null`,证明请求仍经 Gateway 认证门禁。
- 原实现定向 80 项零失败、Resource 全量 2058 项零失败(38 项条件跳过);补充修复定向 46 项零失败、Resource 全量 2059 项零失败(38 项条件跳过)。离线 HTML 自检通过,文档仓库 `npm test` 46 项零失败。
- 管理端适配不属于本后端工单,`frontend_status` 保持 `pending`,不把后端契约验证误记为前端已完成。
## 九、撤回
1. 从最新 `dev-v3` 创建回退分支,先执行 `git revert -m 1 --no-edit af3a7bae5f3d19b97a7ad52daf31bb8fd18d5676` 撤回补充修复,再执行 `git revert -m 1 --no-edit c5b1a84aea6b1c7beb7a626131376355008e662c` 撤回原实现;分别审查并经独立 PR 合入。
2. 重新构建并滚动部署 `hl-resource-service`;本次无数据库、配置、Redis 或 MQ 变更,无需执行 DDL、DML 或数据恢复。
3. 回退前确认管理端不再提交 `permanentValid` 或读取 `daysUntilExpiry`;旧字段 `expiryDate`、`expired` 和 `isRequired` 以及存量数据无需迁移。
4. 回退后经 Gateway 复测草稿创建、更新、提交、详情、未认证和永久/有限期边界,并确认资质必备规则与证照脱敏保持原行为。
## 关联 / 联系人
- **Issue**: [#6304](https://git.1814.love:8443/wx/HL/issues/6304)
- **PR**: [#6342](https://git.1814.love:8443/wx/HL/pulls/6342)
- **合并提交**: [c5b1a84ae](https://git.1814.love:8443/wx/HL/commit/c5b1a84aea6b1c7beb7a626131376355008e662c)
- **补充 Issue**: [#6347](https://git.1814.love:8443/wx/HL/issues/6347)
- **补充 PR**: [#6349](https://git.1814.love:8443/wx/HL/pulls/6349)
- **最终部署提交**: [af3a7bae5](https://git.1814.love:8443/wx/HL/commit/af3a7bae5f3d19b97a7ad52daf31bb8fd18d5676)
- **后端负责人**: @lc
@@ -0,0 +1,454 @@
---
schema: "hl-changelog/v2"
ticket: "6312"
title: "供应商敏感字段与数值格式校验收紧"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "187b7c3b"
target_release: ""
verified_at: "2026-08-25"
status_note: "PR #6322 已合并 dev-v3,合并提交 52a61a88 已由部署任务 b5d27d2b 发布到 TEST。供应商新增、修改、注册提交和独立新增收款账户现统一校验税号、电话、证照编号、银行账号、税率、金额、评分、枚举及业务 ID;接口路径、字段名和成功响应结构不变。"
updated_at: "2026-08-25"
base: "dev-v3"
---
# 供应商敏感字段与数值格式校验收紧
供应商新增、修改、注册提交和独立新增收款账户的输入契约现统一收紧。管理端必须在提交前按本文校验税号、电话、证照编号、银行账号、税率、金额、评分和枚举,并将供应商子表的 `Long` ID 作为规范正整数字符串发送。
本次没有新增、删除或重命名字段,也没有改变成功响应结构;但以前可能被接受的错误格式现在会返回参数错误,因此属于请求兼容性收紧。
## 🔧 关键变化
| 项目 | 修改前 | 修改后 |
|---|---|---|
| 敏感字段格式 | 部分入口仅校验非空或宽松长度,Controller 与 Service 口径可能不一致 | Controller 与 Service 使用同一业务口径,绕过 Controller 也不能写入非法值 |
| 银行账号 | 展示格式和规范化后的数字长度缺少完整门禁 | 去除空格、连字符后必须为 8 至 32 位数字 |
| 税率、金额、评分 | 个别入口可进入后续流程后才失败 | 请求入口即校验范围和小数位 |
| 子表业务 ID | `null`、数字令牌、零或溢出值可能被错误解释为新增或类型错误 | 只有字段省略表示新增;显式 ID 必须是规范正整数字符串 |
| 枚举 | DTO 与直接 Service 调用的口径可能不一致 | 人员规模、账户、结算、发票、合同类型和合同状态均双层校验 |
业务失败可能仍返回 HTTP 200。管理端必须同时判断统一响应中的 `code`、`success`、`message` 和 `data`。
## 变更接口清单
| # | 方法 | 路径 | 权限 | 变化 |
|---:|---|---|---|---|
| 1 | POST | `/admin/supplier/items/add` | `supplier:create` | 收紧主体、联系人、资质和初始账户输入 |
| 2 | PUT | `/admin/supplier/items/{supplierId}/update` | `supplier:update` | 收紧主体、子表 ID、合同和评价输入 |
| 3 | POST | `/admin/supplier/items/{supplierId}/submit` | `supplier:update` + `supplier:approval:submit` | 完整注册表单复用与新增一致的字段规则 |
| 4 | POST | `/admin/supplier/items/{supplierId}/bank-accounts/add` | `supplier:account:manage` + `supplier:approval:submit` | 独立账户复用初始账户的账号、结算和开票规则 |
## 通用字段规则
### 主体、联系人和资质
| 字段 | JSON 类型 | 空值语义 | 当前约束 |
|---|---|---|---|
| `taxNo` | String | 新增、提交必填;修改时省略表示不改 | 原始文本 6 至 64 位,只允许字母、数字、空格和连字符;服务端移除空格、连字符并转大写后仍须为 6 至 64 位字母或数字;可识别的统一社会信用代码、居民身份证同时校验日期或校验位 |
| `contactPhone` | String / null | 公司电话可省略 | 原始文本 7 至 20 位;去除空格、括号、连字符及 `+86`/`0086` 后,须为中国大陆手机号、带 `0` 的座机或 400/800 服务号 |
| `contacts[].contactPhone` | String | 新增、完整提交的联系人必填;修改既有联系人时省略表示不改 | 与公司电话使用同一规范化和号码类型规则 |
| `qualifications[].certNo` | String / null | 草稿可空;提交是否必填继续按资质规则判断 | 最大 128 个 UTF-8 字节;超过长度直接拒绝,读取仍只返回脱敏值 |
| `staffScale` | String / null | 省略表示不改或不设置 | 仅允许 `LT50`、`R50_200`、`R200_500`、`GT500`,区分大小写 |
电话示例:
| 输入 | 结果 |
|---|---|
| `+86 138-0013-8000` | 接受,按境内手机号规范化 |
| `010-12345678` | 接受,按境内座机规范化 |
| `1234567` | 拒绝,消息为 `公司联系电话格式不合法` 或 `联系人电话格式不合法` |
| `13800ABC000` | 拒绝,电话不能包含字母 |
### 收款账户、结算和开票
`initialAccounts[]` 与独立账户接口 `accounts[]` 使用同一规则:
| 字段 | 必填 | 当前约束 |
|---|---|---|
| `accountType` | 是 | `CORPORATE`(对公)或 `PERSONAL`(对私) |
| `bankName` | 是 | 非空,最大 500 字符 |
| `bankBranch` | 否 | 最大 500 字符 |
| `accountNo` | 是 | 只允许数字、空格和连字符;去除展示分隔符后必须为 8 至 32 位数字 |
| `settleMode` | 否 | `PREPAY`、`MONTHLY`、`SINGLE` |
| `accountPeriod` | 条件必填 | 只有 `MONTHLY` 必须填写;其他结算方式必须为空 |
| `invoiceType` | 否 | `SPECIAL`、`NORMAL`、`NONE` |
| `taxRate` | 条件必填 | `SPECIAL`/`NORMAL` 必填,`NONE` 必须为空;格式为 `0%` 至 `100%`,最多两位小数 |
合法税率包括 `0%`、`6%`、`6.5%`、`6.50%`、`100%`、`100.00%`。`5.123%`、`100.01%`、缺少 `%` 或负数均拒绝。
### 合同、评价和枚举
| 字段 | 当前约束 |
|---|---|
| `contracts[].amount` | `0` 至 `9999999999.99`,最多两位小数 |
| `contracts[].contractType` | `FRAME`、`SINGLE_TRIP`、`PURCHASE` |
| `contracts[].status` | `DRAFT`、`ACTIVE`、`EXPIRED` |
| `evaluations[].score` | `0` 至 `5`,最多两位小数 |
枚举均区分大小写。空值是否表示“不修改”继续由对应修改字段语义决定;显式提交未知值一律拒绝。
### 路径和子表业务 ID
路径中的 `{supplierId}` 必须大于 0。JSON 中以下可选子表 ID 使用同一严格规则:
- `contacts[].contactId`
- `qualifications[].qualificationId`
- `contracts[].contractId`
- `evaluations[].evaluationId`
- `evaluations[].orderId`
- `evaluations[].resourceId`
- `evaluations[].evaluator`
| 发送值 | 结果 |
|---|---|
| 字段省略 | 接受,表示新增子项或未建立可选关联 |
| `"1"` 至 `"9223372036854775807"` | 接受,必须是 1 至 19 位且在 Java `Long` 正整数范围内 |
| `null`、`""`、`"0"`、`"-1"` | 拒绝 |
| `"01"`、`" 1"` | 拒绝,不允许前导零或空白 |
| `1` | 拒绝,数字 JSON 令牌不能替代字符串 |
| `"9223372036854775808"` 或 20 位以上数字 | 拒绝,超出范围或长度 |
管理端更新已有子项时,应原样回传详情接口返回的字符串 ID;新增子项必须省略 ID 字段,不能传 `null`。
## 接口详情
### 1. 创建供应商草稿 `POST /admin/supplier/items/add`
请求头:
```text
Authorization: Bearer <accessToken>
Content-Type: application/json
```
典型合法请求:
```json
{
"fullName": "示例旅行服务有限公司",
"shortName": "示例旅行",
"taxNo": "91350211M000100Y46",
"contactPhone": "010-12345678",
"staffScale": "R50_200",
"mainCooperation": "住宿与地接服务",
"types": [
{ "typeCode": "HOTEL" }
],
"contacts": [
{
"contactName": "示例联系人",
"contactPhone": "+86 138-0013-8000",
"contactRole": "contentBus",
"isPrimary": true
}
],
"initialAccounts": [
{
"accountType": "CORPORATE",
"bankName": "示例银行",
"accountNo": "6222 0212 3456 7890",
"settleMode": "SINGLE",
"invoiceType": "SPECIAL",
"taxRate": "6.00%"
}
]
}
```
成功响应结构不变:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2092000000000000001",
"supplierNo": null,
"status": "DRAFT",
"onboardingStage": "PROFILE_DRAFT",
"initialAccounts": [
{
"accountId": "2092000000000000002",
"accountNoMask": "6222********7890",
"status": "DRAFT"
}
],
"updateTime": "2026-08-25 12:35:00"
}
}
```
无效税号、电话、证照编号或初始账户会在供应商、联系人、账户和审批写入前失败。
### 2. 修改供应商 `PUT /admin/supplier/items/{supplierId}/update`
请求头:
```text
Authorization: Bearer <accessToken>
Content-Type: application/json
```
更新已有联系人时,`contactId` 必须使用字符串;新增联系人则省略该字段:
```json
{
"contactPhone": "+86 138-0013-8000",
"staffScale": "R200_500",
"contacts": [
{
"contactId": "2092000000000000010",
"contactPhone": "010-87654321",
"expectedUpdateTime": "2026-08-25 12:35:00"
},
{
"contactName": "新增联系人",
"contactPhone": "13900139000",
"contactRole": "contentMoney",
"isPrimary": false
}
],
"contracts": [
{
"contractName": "年度合作框架",
"contractType": "FRAME",
"amount": 1000000.50,
"status": "DRAFT"
}
],
"evaluations": [
{
"orderId": "2092000000000000020",
"score": 4.75,
"content": "履约正常"
}
],
"changeReason": "更新联系方式与合作资料",
"expectedUpdateTime": "2026-08-25 12:35:00"
}
```
成功响应仍为 `Result<SupplierWriteRespVO>`:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2092000000000000001",
"supplierNo": null,
"status": "DRAFT",
"onboardingStage": "PROFILE_DRAFT",
"initialAccounts": [],
"updateTime": "2026-08-25 12:40:00"
}
}
```
任一子项 ID、金额、评分或枚举非法时,整个更新失败,既有主体与子表快照不变。
### 3. 提交供应商注册 `POST /admin/supplier/items/{supplierId}/submit`
请求头:
```text
Authorization: Bearer <accessToken>
Content-Type: application/json
```
提交接口接收完整注册表单,主体、联系人、资质和 `initialAccounts` 复用创建接口的全部规则,并要求 `expectedUpdateTime`:
```json
{
"fullName": "示例旅行服务有限公司",
"taxNo": "91350211M000100Y46",
"contactPhone": "010-12345678",
"mainCooperation": "住宿与地接服务",
"types": [
{ "typeCode": "HOTEL" }
],
"contacts": [
{
"contactName": "示例联系人",
"contactPhone": "13800138000",
"contactRole": "contentBus",
"isPrimary": true
}
],
"qualifications": [
{
"qualType": "BUSINESS_LICENSE",
"certNo": "LIC-2026-000001",
"imageUrl": "https://files.example.com/supplier/license.png"
}
],
"expectedUpdateTime": "2026-08-25 12:40:00"
}
```
成功响应结构不变:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"approvalLogId": "2092000000000000030",
"requestNo": "SUPPLIER-PROFILE-EXAMPLE",
"provider": "LOCAL_AUTO",
"approvalStatus": "APPROVED",
"spNo": null,
"spStatus": null,
"syncStatus": "APPLIED",
"submittedAt": "2026-08-25 12:41:00",
"finishedAt": "2026-08-25 12:41:00"
}
}
```
格式错误会在创建审批事实前失败;必备资质、并发版本、重复主体和状态机规则保持既有语义。
### 4. 独立新增收款账户 `POST /admin/supplier/items/{supplierId}/bank-accounts/add`
请求头:
```text
Authorization: Bearer <accessToken>
Content-Type: application/json
```
请求示例:
```json
{
"accounts": [
{
"accountType": "CORPORATE",
"bankName": "示例银行",
"bankBranch": "示例支行",
"accountNo": "6222-0212-3456-7890",
"settleMode": "MONTHLY",
"accountPeriod": "月结30天",
"invoiceType": "NORMAL",
"taxRate": "6%",
"proofFileUrls": [
"https://files.example.com/supplier/account-proof.png"
]
}
]
}
```
成功响应仍为按请求顺序返回的数组:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": [
{
"accountId": "2092000000000000040",
"approvalLogId": "2092000000000000041",
"requestNo": "SUPPLIER-ACCOUNT-EXAMPLE",
"provider": "LOCAL_AUTO",
"approvalStatus": "APPROVED",
"spNo": null,
"spStatus": null,
"syncStatus": "APPLIED",
"accountStatus": "ACTIVE",
"isDefault": "NO",
"submittedAt": "2026-08-25 12:45:00",
"finishedAt": "2026-08-25 12:45:00"
}
]
}
```
账号、结算或开票组合非法时整批在账户和审批写入前失败。供应商尚未完成注册时,合法格式仍返回既有业务错误 `395010`(`请先完成供应商注册`)。
## 典型错误响应
字段格式失败统一使用业务码 `400`,例如:
```json
{
"code": 400,
"message": "收款账号规范化后必须为8到32位数字",
"success": false,
"data": null
}
```
| 场景 | `code` | 典型消息 |
|---|---:|---|
| `{supplierId}` 为 0 | `400` | `供应商ID必须为正数` |
| 路径 ID 超出 `Long` 范围 | `400` | `参数 supplierId 格式错误,请检查后重试` |
| 可识别的统一社会信用代码校验位错误 | `400` | `主体证件号的统一社会信用代码校验位不合法` |
| 公司或联系人电话不符合境内号码规则 | `400` | `公司联系电话格式不合法` / `联系人电话格式不合法` |
| 子表 ID 显式为 `null`、空、数字令牌、零或溢出 | `400` | `请求数据格式错误:字段 [...] 格式错误` |
| 账号规范化后少于 8 位或超过 32 位 | `400` | `收款账号规范化后必须为8到32位数字` |
| 税率超过 100% 或多于两位小数 | `400` | `税率必须为0%至100%且最多两位小数` |
| 合同金额超过范围或多于两位小数 | `400` | `合同金额最多10位整数和2位小数` |
| 评价分数超过 5 或多于两位小数 | `400` | `评分不能大于5` / `评分最多1位整数和2位小数` |
| 未认证 | `401` | 统一认证失败;HTTP 状态可能仍为 200 |
本次没有新增业务错误码;既有权限、供应商不存在、未注册、必备资质、乐观锁和审批状态错误继续使用原错误码。
## 管理端接入事项
1. 新建、编辑和提交表单按本文在前端同步限制长度、数字格式、范围、小数位和枚举,避免用户提交后才看到后端错误。
2. 银行账号输入可保留空格或连字符用于展示,但提交前应确认去除分隔符后为 8 至 32 位数字;不要使用 JavaScript `Number` 保存账号。
3. 税率必须保留 `%`;根据 `invoiceType` 联动必填和清空,根据 `settleMode` 联动 `accountPeriod`。
4. 所有子表业务 ID 使用字符串保存和提交。新增项省略 ID,不能传 `null`;禁止用 `Number(...)` 转换雪花 ID。
5. 合同金额和评分使用十进制定点输入,分别限制两位小数和对应范围。
6. 接口失败时保留用户表单,展示后端 `message`;不能只判断 HTTP 状态。
## 影响评估与未变化范围
- **是否破坏向后兼容**:对合法请求兼容;对以前可能被接受的非法格式不兼容。
- **前端是否需要同步**:需要。请求字段和响应结构未变,但表单校验、ID 序列化及结算/开票联动应对齐。
- **历史数据**:不迁移、不批量重写;存量数据读取不受影响,下次显式修改相关字段时按新规则校验。
- **权限与状态机**:不变,仍使用既有管理端角色、平台权限、乐观版本、聚合锁、幂等、审批、审计和软删除规则。
- **基础设施**:无数据库 migration、配置、Redis Key、MQ Topic、Feign 或 Gateway 路由变化。
- **响应与脱敏**:成功响应结构不变;税号、电话、证照号和账号读取仍只返回脱敏值。
## 验证证据
- 自动化:12 个供应商聚焦测试类全部通过;Resource 及依赖模块全量 2018 项测试零失败、零错误,38 项仓库既有条件跳过,13 个 Maven 模块构建成功。
- 独立审查:合并提交的补丁等价、字段边界、Controller/Service 入口一致性和失败零副作用测试均通过,未发现确定性缺口。
- TEST 部署:部署任务 `b5d27d2b` 成功,`hl-resource-service` 的 8182、8082 双实例依次启动;服务器运行提交确认包含合并提交 `52a61a88c`,Nacos 两实例均为 healthy、enabled。
- 真实 Gateway:使用真实有效 SUPER_ADMIN 登录完成 35 项验收,覆盖未认证、路径 ID、税号、公司电话、联系人电话、证照长度、初始/独立账户、税率、子表 ID、合同金额、评价分数、枚举、合法创建、脱敏读取、合法更新和业务状态失败。
- 零写入:合法草稿建立后的数据库快照为 `1` 个主体、`1` 个类型、`1` 个联系人、`1` 个初始账户、`0` 个审批;全部失败用例执行后快照完全一致。
- 清理:合成草稿通过既有删除接口成功清理,活动主体、类型、联系人、账户、评价和审批均为 0;按审计要求保留 3 条主体变更事实和 2 条账户变更事实,不含真实个人或企业数据。
- 运行健康:验收后 8082、8182 仍健康,Gateway 可达,两实例本次启动日志 `ERROR=0`;未触发文件、配置或 MQ 副作用,未生成需要清理的重复确认令牌。
## 撤回
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit 52a61a88c906395d5cafd72cbda2390ad31a02a3`,经新的独立 PR 合入。
2. 重新构建并滚动部署 `hl-resource-service`,确认部署提交不再包含本工单变更。
3. 无数据库 migration、数据改写、配置、Redis、MQ 或跨服务契约变更,不需要执行 DDL、DML、缓存清理、消息补偿或配置恢复;现有业务和审计数据保持不变。
4. 回退后接口路径、字段名和成功响应结构仍不变,但输入接受范围恢复为本工单前的宽松口径;管理端若已上线严格校验可继续保持,避免重新接受明显非法值。
5. 经 Gateway 重跑合法新增、修改、提交、独立账户、未认证、失败零写入和脱敏读取用例,并确认 Nacos 双实例健康、服务日志无新增错误。
6. 若撤回本 Changelog,以新的文档提交删除本文件或标记撤回,不重写已发布历史。
## 关联 / 联系人
### 链接
- **Issue**: [#6312](https://git.1814.love:8443/wx/HL/issues/6312)
- **PR**: [#6322](https://git.1814.love:8443/wx/HL/pulls/6322)
- **合并提交**: [52a61a88c](https://git.1814.love:8443/wx/HL/commit/52a61a88c906395d5cafd72cbda2390ad31a02a3)
### 联系人
- **后端负责人**: @lc
@@ -0,0 +1,69 @@
---
schema: "hl-changelog/v2"
ticket: "6313"
title: "景区资源列表增加供应商全称字段"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "3c0edfd7"
target_release: ""
verified_at: "2026-08-25"
status_note: "PR #6315 已合并 dev-v3,合并提交 ae76a3407 已随 Resource 双实例滚动部署到 TEST。管理端景区分页列表现固定返回 supplierFullName;有效关联返回供应商当前全称,未关联或关系主体缺失返回 null。管理端列表列展示仍待前端接入。"
updated_at: "2026-08-25"
base: "dev-v3"
---
# 景区资源列表增加供应商全称字段
管理端景区分页列表新增供应商全称响应字段,供资源列表直接展示当前有效关联的供应商名称。字段只扩展管理端列表响应,不改变查询参数、分页、排序、权限或既有业务错误。
## 变更接口
| 方法 | 路径 | 新增字段 | 类型 | 语义 |
|---|---|---|---|---|
| GET | `/admin/scenic/spots` | `data.records[].supplierFullName` | `String \| null` | 当前有效供应商关系对应的供应商全称;无有效关系或主体缺失时为 `null` |
`supplierFullName` 在每条管理端景区记录中固定存在。客户端可按可空字符串处理,不需要根据字段是否存在做兼容分支。
## 取值规则
- 只匹配当前未删除、`resourceModule=SCENIC` 且 `activeFlag=1` 的供应商资源关系。
- 返回供应商主体当前的 `fullName`,不保存名称快照;供应商改名后列表随当前值更新。
- 供应商处于暂停合作等非删除状态时仍显示全称。
- 景区未绑定供应商时返回 `null`。
- 有效关系指向已删除或缺失主体时记录服务端告警并返回 `null`,不会中断整页查询。
## 性能、安全与兼容性
- 空页不追加供应商查询;非空页最多一次批量查询关系、一次批量查询供应商主体,不产生逐行查询。
- 供应商查询只投影标识和全称,不读取或返回税号、账户、联系人、资质等敏感字段。
- `/internal/mp/scenic/list`、景区详情及其他资源列表不新增该字段。
- 不新增筛选或排序能力,不修改请求参数、统一响应结构、权限、错误码、Gateway 路由或跨服务契约。
- 不涉及数据库 migration、配置、Redis、MQ 或数据回写。
- 管理端当前页面尚未增加“供应商全称”列;前端接入时直接读取 `supplierFullName`,并将 `null` 展示为空值占位即可。
## 验证证据
- 自动化:定向测试 `24/24` 通过;Resource 全量 `1977` 项零失败、零错误(38 项仓库既有条件跳过);结构检查和独立合并后审计通过。
- 部署:TEST 任务 `447a06bc` 退出码 0,`hl-resource-service` 的 8182、8082 双实例依次健康;服务器 HEAD `29d7c011e` 明确包含合并提交 `ae76a3407`。
- 真实 Gateway:首屏 10 条记录全部包含 `supplierFullName`,其中同时覆盖非空名称与 `null`;无匹配条件返回 `total=0`、空 `records`;统一响应 `code=200`、`success=true`。
- 认证:不带 `Authorization` 的请求返回业务码 `401`、`success=false`、`data=null`。
- 清理:验收只执行只读 GET,查询条件已重置;未写入数据库、缓存、MQ、审批、派单或会话数据。
## 撤回
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit ae76a3407b5f1b2d50b0f347eb4420aaa7649435`,经独立 PR 合入。
2. 重新构建并滚动部署 `hl-resource-service`;无需恢复数据库、配置、Redis 或 MQ。
3. 撤回前调用方停止依赖 `supplierFullName`;撤回不会修改景区、供应商或关联数据。
4. 撤回后经 Gateway 复测 `/admin/scenic/spots` 恢复旧响应,并确认 `/internal/mp/scenic/list`、分页和筛选保持正常。
## 关联 / 联系人
- **Issue**: [#6313](https://git.1814.love:8443/wx/HL/issues/6313)
- **PR**: [#6315](https://git.1814.love:8443/wx/HL/pulls/6315)
- **合并提交**: [ae76a3407](https://git.1814.love:8443/wx/HL/commit/ae76a3407b5f1b2d50b0f347eb4420aaa7649435)
- **后端负责人**: @lc
@@ -0,0 +1,163 @@
---
schema: "hl-changelog/v2"
ticket: "6316"
title: "供应商详情新增资源信息分页"
consumer: "admin"
author: "lc(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "de665364"
target_release: ""
verified_at: "2026-08-25"
status_note: "PR #6330 已合并 dev-v3,合并提交 4f032cd6f 已由任务 cc1fe248、2697d374 精确发布 Resource/Fleet 至 TEST 并经真实 Gateway 只读验收;验收后任务 45e843e5、c05eda6e 已回切包含该提交的 dev-v3,两个服务均保持双实例健康。"
updated_at: "2026-08-25"
base: "dev-v3"
---
# 供应商详情新增资源信息分页
供应商详情新增只读“资源信息”分页接口。数据以本系统 `supplier_resource_rel` 当前有效关系为入口,展示 Resource 本地九类资源及 Fleet 车辆的当前权威信息;供应商关系已保留但资源已删除时返回缺失占位,避免详情悄然丢行。
## 变更接口
| 方法 | 路径 | 用途 |
|---|---|---|
| GET | `/admin/supplier/items/{supplierId}/resource-info/page` | 分页查询供应商当前有效关系对应的资源信息 |
查询参数:
| 参数 | 类型 | 必填 | 默认/约束 | 说明 |
|---|---|---:|---|---|
| `supplierId` | string | 是 | 正整数 | 路径参数,Snowflake ID 按字符串处理 |
| `page` | integer | 否 | 默认 `1`,最小 `1` | 页码;兼容公共分页参数 `pageNo` |
| `pageSize` | integer | 否 | 默认 `20`,范围 `1..100` | 每页条数 |
| `resourceModule` | string | 否 | 十选一 | 按关系冻结的资源模块精确筛选 |
支持模块:`SCENIC`、`RESTAURANT`、`SUPPLIES`、`SUPPLIES_COMBO`、`ACTIVITY`、`HOTEL`、`SERVICE`、`COST_ITEM`、`STAFF`、`VEHICLE`。
## 返回字段
分页 `data` 使用统一 `PageResult`:`records`、`total`、`page`、`pageSize`。`records[]` 字段如下:
| 字段 | JSON 类型 | 语义 |
|---|---|---|
| `relationId` | string | 供应商资源关系 ID |
| `resourceModule` | string | 冻结模块编码 |
| `moduleName` | string | 模块中文名 |
| `resourceId` | string | 资源 ID;资源缺失时仍保留关系原值 |
| `resourceName` | string/null | 当前资源名称 |
| `coverUrl` | string/null | 当前有效封面 URL |
| `city` | string/null | 城市展示值 |
| `isCharged` | boolean/null | 是否收费 |
| `isChargedName` | string/null | 是否收费中文名 |
| `settleTypeCode` | string/null | 结算方式编码 |
| `settleTypeName` | string/null | 结算方式中文名 |
| `tags` | array | 标签数组,永不返回 `null` |
| `seasons` | array | 标准季节数组,永不返回 `null` |
| `statusCode` | string/null | 资源当前权威状态编码 |
| `statusName` | string/null | 归一化状态中文名 |
| `enabled` | boolean/null | 归一化启用状态 |
| `resourceAvailable` | boolean | 资源当前是否存在;`false` 为关系保留、资源缺失 |
| `updateTime` | string/null | 资源本身更新时间,格式 `yyyy-MM-dd HH:mm:ss` |
`tags[]` 返回 `tagId`、`tagName`、`tagColor`;`seasons[]` 返回 `seasonCode`、`seasonName`。标量不适用或未知时返回 `null`,集合无数据时返回 `[]`。结果按关系 `update_time DESC, rel_id DESC` 排序,展示的 `updateTime` 则来自资源主数据。
响应示例:
```json
{
"code": 200,
"message": "success",
"success": true,
"data": {
"records": [
{
"relationId": "2091715622923657218",
"resourceModule": "SCENIC",
"moduleName": "景区管理",
"resourceId": "2091715622923657001",
"resourceName": "示例景区",
"coverUrl": null,
"city": "海拉尔",
"isCharged": true,
"isChargedName": "是",
"settleTypeCode": "CASH",
"settleTypeName": "现付",
"tags": [
{ "tagId": "2084636804090089473", "tagName": "自然风光", "tagColor": "#52C41A" }
],
"seasons": [
{ "seasonCode": "spring", "seasonName": "春" }
],
"statusCode": "ENABLED",
"statusName": "启用",
"enabled": true,
"resourceAvailable": true,
"updateTime": "2026-08-25 10:00:00"
}
],
"total": 1,
"page": 1,
"pageSize": 20
}
}
```
## 权限、依赖与错误码
- Gateway 强制认证;服务端仅允许 `ADMIN`、`FINANCE`、`SUPER_ADMIN`,并同时要求 `supplier:view` 与 `supplier:resource:view`。
- 任意供应商生命周期状态均可查询;接口没有写入、缓存、MQ 或操作审计副作用。
- 本地九类资源批量查询 Resource 数据;`VEHICLE` 通过内部 Token 调用 Fleet 批量接口,管理端不得直接调用内部接口。
- Resource、Fleet 或必要字典依赖异常时失败关闭,不返回半真半假的成功数据。
| 业务码 | 场景 |
|---:|---|
| `401` | 未认证或登录态失效 |
| `400` | 路径/分页参数不合法 |
| `395001` | 供应商不存在 |
| `395034` | 不支持的 `resourceModule` |
| `395039` | Fleet、字典或资源必要依赖不可用/响应不完整 |
## 管理端接入事项
1. 在供应商详情“账号信息”页签后新增“资源信息”页签,进入页签后按需请求本接口。
2. 建议列为资源名称(含封面)、模块/类型、城市、是否收费、结算方式、标签、季节、状态、更新时间;字段与用户提供的资源列表示意保持一致,但以本接口实际数据为准。
3. ID 全程按字符串传递和比较,禁止转为 JavaScript `number`。
4. 展示名称优先使用 `moduleName`、`isChargedName`、`settleTypeName`、`statusName`;未知值显示占位符,不自行推导业务码。
5. `tags`、`seasons` 直接按数组渲染;`resourceAvailable=false` 时保留该行并展示“资源已删除/不可用”,不要过滤。
6. 分页筛选变化时回到第 1 页;页容量不得超过 100。管理端不提供资源关系的新增、编辑、删除或状态切换操作。
## 兼容性与未变化范围
- 新增只读 GET,不修改既有供应商详情、账号、审批、资源管理写接口及响应字段。
- 不新增或修改数据库 migration;沿用既有 `supplier_resource_rel`、Resource 主表与 Fleet 车辆数据。
- 不变更 Gateway 顶级路由、Nacos 配置、Redis、MQ、状态机、审批、数据范围或软删除语义。
- 后端仓库未修改任何管理端源码;管理端接入状态保持 `pending`。
## 验证证据
- 自动化:供应商相关测试 282 项零失败(1 项条件跳过);Fleet 车辆包 228 项零失败;Resource 全量 2043 项零失败(38 项条件跳过);Fleet 可运行全量 3853 项零失败(7 项条件跳过);Gateway 路由审计 3 项零失败。
- 独立审计:在合并提交 `4f032cd6f` 的 detached worktree 重新核对权限、错误码、软删除、缺失资源占位、Fleet 批量与失败关闭;供应商审计套件和 Fleet 审计套件 249 项均零失败。
- TEST 精确部署:Resource 任务 `cc1fe248`(13:02:08–13:02:42)和 Fleet 任务 `2697d374`(13:03:42–13:04:18)均退出码 0;部署端 HEAD 为 `4f032cd6f`,Resource 8082/8182、Fleet 8087/8187 滚动恢复健康,Nacos `test` 中各有 2 个实例。
- 真实 Gateway:使用现有有效管理端会话只读查询全部 3 个 TEST 供应商,实际取得 1 条 `SCENIC` 资源关系;18 个字段齐全,Snowflake ID 为字符串,`tags`/`seasons` 为数组,时间格式及模块筛选通过。不存在供应商、未知模块、`page=0`、`pageSize=101` 分别返回 `395001`、`395034`、`400`、`400`;未认证请求为 HTTP 200、业务码 `401`、`success=false`。
- TEST 当前没有 `VEHICLE` 供应商关系样本,因此未伪造数据;跨服务车辆路径由真实双服务部署健康、内部 Token 控制器契约、批量查询、重复/缺失/额外响应拒绝及 tombstone 测试覆盖。
- 环境恢复:验收后任务 `45e843e5`(Resource)与 `c05eda6e`(Fleet)依次回切包含本次合并的 `dev-v3`,退出码均为 0;8082/8182、8087/8187 及 Nacos 各 2 个实例保持健康,临时精确部署分支已删除。
- 清理:验收全程只读,无供应商、资源、数据库、Redis、MQ 或配置数据需要清理。
## 撤回
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit 4f032cd6f6698607a2f1524437533d595975fc9a`,经独立 PR 合入。
2. 依次重新构建并滚动部署 `hl-resource-service`、`hl-fleet-service`;回退期间两个服务分别维持双实例滚动策略。
3. 本次无数据库、Redis、MQ、Nacos 或其他配置变更,不需要 DDL、DML、缓存清理、消息补偿或配置恢复;不存在不可逆数据影响。
4. 回退后管理端停止调用新增 GET 和读取本次字段;既有供应商详情、账号及资源管理接口继续兼容。
5. 经 Gateway 复测新增路径不可用/已撤回、既有供应商详情正常、两个服务双实例与 Nacos 健康,并核对数据库、缓存和 MQ 无副作用。
## 关联 / 联系人
- **Issue**: [#6316](https://git.1814.love:8443/wx/HL/issues/6316)
- **PR**: [#6330](https://git.1814.love:8443/wx/HL/pulls/6330)
- **合并提交**: [4f032cd6f](https://git.1814.love:8443/wx/HL/commit/4f032cd6f6698607a2f1524437533d595975fc9a)
- **后端负责人**: @lc
@@ -0,0 +1,103 @@
---
schema: "hl-changelog/v2"
ticket: "6317"
title: "供应商详情补充中文名称与资质有效状态"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "33fe59ac"
target_release: ""
verified_at: "2026-08-25"
status_note: "PR #6323 已合并 dev-v3,合并提交 e9e523733 已由部署任务 318eb344 精确发布到 TEST 并经真实 Gateway 验证;验收后任务 faa1df6e 已将 Resource 恢复到包含该提交的最新 dev-v3。供应商详情新增联系人角色、资质类型中文名,以及永久使用和有效状态字段;isRequired 保持原必备规则并集语义,仅为兼容保留,详情业务不展示。"
updated_at: "2026-08-25"
base: "dev-v3"
---
# 供应商详情补充中文名称与资质有效状态
供应商基本信息详情补充面向管理端展示的中文名称和资质有效性字段。联系人角色与资质类型继续保留原始字典值,同时返回中文标签;资质有效性由有效期截止日按上海自然日动态推导。
## 变更接口
| 方法 | 路径 | 行为变化 |
|---|---|---|
| GET | `/admin/supplier/items/{supplierId}/basic-info/view` | `contacts[]` 新增 `contactRoleName`;`qualifications[]` 新增 `qualTypeName`、`permanentValid`、`validityStatus` 和 `validityStatusName` |
原字段 `contactRole`、`qualType`、`expiryDate`、`isRequired` 和 `expired` 均继续返回,字段名与 JSON 类型不变。
响应片段示例:
```json
{
"contacts": [
{
"contactRole": "contentBus",
"contactRoleName": "业务人员"
}
],
"qualifications": [
{
"qualType": "BUSINESS_LICENSE",
"qualTypeName": "营业执照",
"expiryDate": "2027-08-25",
"permanentValid": false,
"validityStatus": "VALID",
"validityStatusName": "有效",
"isRequired": true,
"expired": false
}
]
}
```
## 字段与状态语义
- `contactRoleName` 来自 `sup_content_role` 生效字典项的中文标签;`qualTypeName` 来自 `company_qualification_type` 生效字典项的中文标签。
- 历史值、自由文本值或已停用值未命中当前生效字典时,中文名称字段回退为原始值,避免详情空白。
- 字典依赖异常时详情查询失败关闭,不伪造硬编码中文标签。
- `expiryDate=null` 表示永久使用,返回 `permanentValid=true`、`validityStatus=VALID`、`validityStatusName=有效`。
- `expiryDate` 早于上海时区当天时返回 `INVALID`、`无效`;等于当天或晚于当天时返回 `VALID`、`有效`。
- `isRequired` 继续按供应商全部类型对应必备资质规则的并集计算,不改义为“是否永久”,也不参与有效状态推导。该字段仅为历史调用方兼容保留,供应商详情业务不展示。
- 既有 `expired` 字段继续与“截止日早于上海当天”保持一致;调用方可迁移到新的状态字段,但无需立即停止兼容读取。
## 管理端接入事项
1. 联系人角色展示使用 `contactRoleName`,提交和稳定判断仍使用 `contactRole`。
2. 资质类型展示使用 `qualTypeName`,提交和稳定判断仍使用 `qualType`。
3. 资质区域展示固定为“有效期至”“永久使用”“状态”;“永久使用”按 `permanentValid` 显示“是/否”,“状态”按 `validityStatusName` 显示“有效/无效”。
4. 详情页隐藏 `isRequired`,不要将其当作永久标识;`expiryDate` 为空时可显示占位符,永久语义以 `permanentValid=true` 为准。
## 兼容性与未变化范围
- 本次是既有 GET 响应的向后兼容扩展,不新增接口、请求参数、业务错误码、Gateway 路由或权限点。
- 不修改供应商联系人、资质、类型规则等数据库数据,不新增 migration,也不回填历史值。
- 不改变联系人电话和证件编号脱敏、软删除、数据范围、`supplier:view` 权限或统一响应结构。
- 不修改配置、Redis、MQ、审批、状态机或跨服务写契约。
- 本工单只交付后端;管理端源码未在后端仓库修改,前端状态保持 `pending`,直至完成上述展示适配并提供验证提交。
## 验证证据
- 自动化:新增详情展示 4 项测试,联合供应商字典 11 项定向测试共 15 项零失败;Resource 全量 2022 项零失败,38 项仓库既有条件跳过。
- 合并后独立审计:在合并提交 `e9e523733` 上重新执行 15 项定向测试,零失败;同时核对 Gateway 强制认证、`supplier:view` 权限、脱敏、软删除和 `isRequired` 并集逻辑均未被绕过。
- TEST 部署:任务 `318eb344` 将合并提交 `e9e523733` 精确发布,12:07:42 至 12:08:23、退出码 0;`hl-resource-service` 的 8182、8082 依次恢复健康,部署服务器 HEAD 与合并提交一致,Nacos `test` 命名空间实例数为 2。
- 真实 Gateway:使用现有 `SUPER_ADMIN` 会话只读查询三条有效供应商;详情实际返回 `contactRoleName=业务人员`、`qualTypeName=营业执照`、`permanentValid=true`、`validityStatus=VALID` 和 `validityStatusName=有效`;同一资质的 `isRequired=false`,证明该字段未被改义为永久标识。未认证请求返回 HTTP 200、业务码 `401`、`success=false`。
- 环境恢复:验收后任务 `faa1df6e` 将 Resource 恢复到包含 `e9e523733` 的最新 `dev-v3`,退出码 0,8082/8182 与 Nacos 两实例保持健康;临时精确提交部署分支已删除。
- 清理:本次验收只读查询,不创建或修改供应商、文件、数据库、Redis、MQ 或配置数据,无测试数据需要清理。
## 撤回
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit e9e523733c51dda3e5d5cd9a69cc20972cd8b741`,经独立 PR 合入。
2. 重新构建并滚动部署 `hl-resource-service`;本次无数据库、配置、Redis 或 MQ 变更,无需执行 DDL、DML 或数据恢复。
3. 回退后详情不再返回本次新增的四类展示字段;历史字段和数据保持兼容,管理端应同步停止读取 `contactRoleName`、`qualTypeName`、`permanentValid`、`validityStatus` 和 `validityStatusName`。
4. 经 Gateway 复测详情成功、未认证、无权限、历史字典值回退和资质永久/过期/今日到期/未来到期边界,并确认电话与证件号仍脱敏。
## 关联 / 联系人
- **Issue**: [#6317](https://git.1814.love:8443/wx/HL/issues/6317)
- **PR**: [#6323](https://git.1814.love:8443/wx/HL/pulls/6323)
- **合并提交**: [e9e523733](https://git.1814.love:8443/wx/HL/commit/e9e523733c51dda3e5d5cd9a69cc20972cd8b741)
- **后端负责人**: @lc
@@ -0,0 +1,74 @@
---
schema: "hl-changelog/v2"
ticket: "6343"
title: "供应商新建修改允许供应商类型为空"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "d84c6c25"
target_release: ""
verified_at: "2026-08-26"
status_note: "PR #6385 已合并 dev-v3,合并提交 b8d99f58 已部署到 TEST。供应商创建草稿的 types 可省略、传 null 或空数组;更新省略 types 保持原关系,显式空数组清空无资源占用的类型;注册提交仍至少需要一个类型。"
updated_at: "2026-08-26"
base: "dev-v3"
---
# 供应商新建修改允许供应商类型为空
供应商草稿阶段不再强制选择供应商类型。创建草稿可不传类型,编辑草稿也可显式清空全部未被资源占用的类型;注册提交的完整性门禁保持不变。
## 变更接口
| 方法 | 路径 | 行为变化 |
|---|---|---|
| POST | `/admin/supplier/items/add` | `types` 省略、为 `null` 或空数组时均可创建无类型草稿;非空时继续校验最多 15 项、生效字典值和唯一性 |
| PUT | `/admin/supplier/items/{supplierId}/update` | 省略 `types` 时保持原类型关系;显式传空数组时清空全部无资源占用的类型;非空快照继续执行原校验 |
| POST | `/admin/supplier/items/{supplierId}/submit` | 行为不变:没有供应商类型时仍拒绝提交注册 |
| GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 无类型供应商的 `types` 返回空数组 |
字段名和 JSON 类型没有变化。`types` 非空时仍提交对象数组,类型值来自供应商类型生效字典。
## 校验与错误语义
- 创建无类型草稿仅放宽草稿保存,不放宽注册提交、审批或状态机。
- 更新显式清空前仍检查资源关系占用;存在有效资源关系的类型不能被移除,失败时主体、类型关系和审计保持零变化。
- 无类型草稿提交继续返回既有业务错误码 `395008`,草稿状态和并发版本不变。
- 非空类型继续校验最多 15 项、生效字典值、重复值和主类型约束。
- 业务失败可能仍使用 HTTP 200,客户端必须同时检查统一响应的 `code`、`success`、`message` 和 `data`。
## 兼容性与前端事项
- 管理端请求字段和响应结构不变,现有非空类型流程无需调整,因此无前端源码变更。
- 无类型供应商仍可被列表和详情读取;客户端应兼容 `types: []`。
- 不新增数据库 migration,不修改配置、Gateway、Redis、MQ、Nacos、Feign 或跨服务写入。
- 本次不自动修改历史数据,也不绕过既有权限、数据范围、软删除、资源占用和审计规则。
## 验证证据
- 合并后独立审计:#6343 相关跨层定向 153 项零失败;Resource 最新目标分支全量 2111 项零失败、38 项仓库既有条件跳过;`git diff --check` 通过。
- TEST 运行态:服务器后端仓库为 `dev-v3` 的 `1a16a5aec`,包含合并提交 `b8d99f58c`;`hl-resource-service` 的 8082、8182 双实例均监听。
- 真实 Gateway 正向:`types` 省略、`null`、空数组分别成功创建三个 `DRAFT`,详情均返回空类型数组;带一个生效类型的草稿创建成功。
- 更新语义:省略 `types` 后原一项类型保持不变,显式 `types: []` 后详情返回空数组。
- 状态门禁:无类型草稿提交返回 `395008`,状态仍为 `DRAFT`、版本不变,确认零写入。
- 权限门禁:未登录请求返回业务码 401,真实 `CUSTOMIZER` 返回 `395002`,两者均通过列表回读确认零写入;正向使用真实 `SUPER_ADMIN` 身份。
- 清理:四条成功创建的 TEST 草稿全部删除;逐条详情返回 `395001`,按随机全名查询均为零条,没有留下测试业务数据。
## 撤回
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit b8d99f58c84daa12ef50d68bcc3090f38f57a22d`,经独立 PR 合入。
2. 通过 Deploy Panel API 重新构建并滚动部署 `hl-resource-service`,复核 8082、8182 双实例和 Nacos 健康状态。
3. 无数据库、配置、Redis 或 MQ 变更,不执行 DDL、DML、缓存清理或消息补偿;不要回退后续无关工单的提交或 migration。
4. 回退前让调用方恢复创建时至少提交一个生效类型,并停止更新时发送 `types: []`;更新不改类型时继续省略该字段。
5. 回退后经 Gateway 复测非空类型创建、更新省略保持、无类型创建拒绝、无类型提交拒绝、资源占用移除保护和越权零写入。
## 关联 / 联系人
- **Issue**: [#6343](https://git.1814.love:8443/wx/HL/issues/6343)
- **PR**: [#6385](https://git.1814.love:8443/wx/HL/pulls/6385)
- **功能提交**: [d625f0448](https://git.1814.love:8443/wx/HL/commit/d625f04489655e11982b9baeeb45d74ba4f7c0e1)
- **合并提交**: [b8d99f58c](https://git.1814.love:8443/wx/HL/commit/b8d99f58c84daa12ef50d68bcc3090f38f57a22d)
- **后端负责人**: @lc
@@ -0,0 +1,369 @@
---
schema: "hl-changelog/v2"
ticket: "6391"
title: "供应商法人证件号与身份证正反面"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "74b3d8bf"
target_release: ""
verified_at: "2026-08-26"
status_note: "PR #6401 已合并 dev-v3 并部署 TEST;供应商创建、更新、提交新增法人身份证号及正反面永久地址,详情仅返回脱敏证件号。旧客户端省略三字段时保持兼容,前端待接入输入与双面上传。"
updated_at: "2026-08-26"
base: "dev-v3"
---
# 🔧 供应商法人证件号与身份证正反面
供应商“基本信息-法定代表人”新增身份证号、人像面和国徽面三个字段。身份证图片继续使用既有文件上传能力,本次接口只接收上传完成后的永久 HTTPS 地址,不新增上传或 OCR 接口。
## 变更接口
| 方法 | 路径 | 权限 | 变化 |
|---|---|---|---|
| POST | `/admin/supplier/items/add` | `supplier:create` | 创建草稿可保存三个法人证件字段 |
| PUT | `/admin/supplier/items/{supplierId}/update` | `supplier:update` | 增量更新三个法人证件字段 |
| POST | `/admin/supplier/items/{supplierId}/submit` | `supplier:update`、`supplier:approval:submit` | 完整注册表单可提交三个法人证件字段 |
| GET | `/admin/supplier/items/{supplierId}/basic-info/view` | `supplier:view` | 返回脱敏证件号及身份证正反面地址 |
## 通用字段与校验
| 字段 | 类型 | 必填 | 规则 |
|---|---|---|---|
| `legalRepresentativeIdNo` | string | 条件必填 | 18 位中国大陆居民身份证号;校验长度、出生日期、顺序码及校验位;末位小写 `x` 会规范为大写 `X` |
| `legalRepresentativeIdCardFrontUrl` | string | 条件必填 | 身份证人像面永久地址,最长 1000 字符;必须为公网 HTTPS 地址,且不能含账号密码、查询串或片段 |
| `legalRepresentativeIdCardBackUrl` | string | 条件必填 | 身份证国徽面永久地址,规则同人像面 |
三个字段必须“全部省略”或“同时提供”:
- 全部省略:兼容旧客户端和没有法人证件数据的存量供应商。
- 任意一个有值:三个字段必须同时有值,不能只保存证件号或单面图片。
- 法定代表人可能对应多个供应商,证件号不作为供应商之间的唯一键。
- 写接口的成功响应不回传证件号;需要展示时调用详情接口。
- 统一响应可能以 HTTP 200 承载业务失败,必须同时判断 `code`、`success` 和 `data`。
## 1. 创建供应商草稿
### 使用场景
在新建供应商基本信息时,同时保存法定代表人身份证号及正反面扫描件地址。
### 请求
```http
POST /admin/supplier/items/add
Authorization: Bearer <管理员令牌>
Content-Type: application/json
```
本次新增的三个字段遵循上方通用规则;创建草稿的既有最低必填字段仍为 `fullName`、`taxNo`、`mainCooperation`。
典型成功请求:
```json
{
"fullName": "法人证件联调示例供应商",
"taxNo": "L6391EXAMPLE001",
"legalRepresentative": "示例法人",
"legalRepresentativeIdNo": "11010519491231002x",
"legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/id/front.jpg",
"legalRepresentativeIdCardBackUrl": "https://files.example.com/supplier/id/back.jpg",
"mainCooperation": "旅游资源供应"
}
```
典型成功响应:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "1900000000000000001",
"supplierNo": null,
"status": "DRAFT",
"onboardingStage": "PROFILE_DRAFT",
"initialAccounts": [],
"updateTime": "2026-08-26 11:40:00"
}
}
```
兼容边界:旧客户端可完全省略三个新字段,其余请求保持原样。
异常请求(仅传人像面):
```json
{
"fullName": "法人证件联调示例供应商",
"taxNo": "L6391EXAMPLE001",
"legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/id/front.jpg",
"mainCooperation": "旅游资源供应"
}
```
```json
{
"code": 400,
"message": "法定代表人证件号、人像面和国徽面必须同时提供",
"success": false,
"data": null
}
```
失败不会创建供应商草稿。
## 2. 更新供应商
### 使用场景
在供应商基本信息编辑页补录或替换完整的法人证件三字段。
### 请求
```http
PUT /admin/supplier/items/1900000000000000001/update
Authorization: Bearer <管理员令牌>
Content-Type: application/json
```
`changeReason` 和 `expectedUpdateTime` 沿用原接口必填约束;三个法人证件字段作为一组增量字段处理。典型成功请求:
```json
{
"legalRepresentative": "示例法人",
"legalRepresentativeIdNo": "11010519491231002X",
"legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/id/front-v2.jpg",
"legalRepresentativeIdCardBackUrl": "https://files.example.com/supplier/id/back-v2.jpg",
"changeReason": "补录法人身份证扫描件",
"expectedUpdateTime": "2026-08-26 11:40:00"
}
```
典型成功响应:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "1900000000000000001",
"supplierNo": null,
"status": "DRAFT",
"onboardingStage": "PROFILE_DRAFT",
"initialAccounts": [],
"updateTime": "2026-08-26 11:42:00"
}
}
```
兼容边界:三个新字段全部省略时,不修改现有法人证件数据。若请求中出现任一新字段,服务端会与当前值合并后再次检查三字段是否完整。
异常请求(身份证校验位错误):
```json
{
"legalRepresentativeIdNo": "110105194912310021",
"legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/id/front-v2.jpg",
"legalRepresentativeIdCardBackUrl": "https://files.example.com/supplier/id/back-v2.jpg",
"changeReason": "补录法人身份证扫描件",
"expectedUpdateTime": "2026-08-26 11:40:00"
}
```
```json
{
"code": 400,
"message": "法定代表人证件号的日期或校验位不合法",
"success": false,
"data": null
}
```
失败时主体版本、法人证件数据和变更记录均保持原状。
## 3. 提交供应商注册审批
### 使用场景
提交草稿的完整注册表单时,将法人证件三字段一并纳入审批内容。
### 请求
```http
POST /admin/supplier/items/1900000000000000001/submit
Authorization: Bearer <管理员令牌>
Content-Type: application/json
```
本接口继续要求完整注册表单和当前 `expectedUpdateTime`。典型成功请求:
```json
{
"fullName": "法人证件联调示例供应商",
"taxNo": "L6391EXAMPLE001",
"legalRepresentative": "示例法人",
"legalRepresentativeIdNo": "11010519491231002X",
"legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/id/front.jpg",
"legalRepresentativeIdCardBackUrl": "https://files.example.com/supplier/id/back.jpg",
"mainCooperation": "旅游资源供应",
"expectedUpdateTime": "2026-08-26 11:42:00"
}
```
典型成功响应:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"approvalLogId": "1900000000000000101",
"requestNo": "SUP-REQ-20260826-0001",
"provider": "LOCAL_AUTO",
"approvalStatus": "APPROVED",
"spNo": null,
"spStatus": null,
"syncStatus": "APPLIED",
"submittedAt": "2026-08-26 11:43:00",
"finishedAt": "2026-08-26 11:43:00"
}
}
```
兼容边界:没有法人证件数据的旧草稿仍可按旧请求提交;若提交法人证件,则三字段必须完整。
异常请求(图片地址带查询串):
```json
{
"fullName": "法人证件联调示例供应商",
"taxNo": "L6391EXAMPLE001",
"legalRepresentativeIdNo": "11010519491231002X",
"legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/id/front.jpg?token=temporary",
"legalRepresentativeIdCardBackUrl": "https://files.example.com/supplier/id/back.jpg",
"mainCooperation": "旅游资源供应",
"expectedUpdateTime": "2026-08-26 11:42:00"
}
```
```json
{
"code": 400,
"message": "法人证件人像面地址不在允许范围",
"success": false,
"data": null
}
```
失败时不会创建审批或改变供应商状态。
## 4. 查询供应商基本信息
### 使用场景
编辑页回显法人证件信息。证件号只返回掩码,不能用于恢复原文或再次提交;图片地址可用于有权限页面的预览。
### 请求
```http
GET /admin/supplier/items/1900000000000000001/basic-info/view
Authorization: Bearer <管理员令牌>
```
无请求体。
典型成功响应:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "1900000000000000001",
"supplierNo": null,
"fullName": "法人证件联调示例供应商",
"shortName": null,
"tax_no": "L639****E001",
"legalRepresentative": "示例法人",
"legalRepresentativeIdNoMask": "110105********002X",
"legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/id/front.jpg",
"legalRepresentativeIdCardBackUrl": "https://files.example.com/supplier/id/back.jpg",
"contactPhoneMask": null,
"establishDate": null,
"registeredCapital": null,
"businessScope": null,
"staffScale": null,
"mainCooperation": "旅游资源供应",
"status": "DRAFT",
"creditLevel": "B",
"totalScore": null,
"types": [],
"contacts": [],
"qualifications": [],
"updateTime": "2026-08-26 11:42:00"
}
}
```
兼容边界:存量供应商没有法人证件数据时,三个响应字段均为 `null`。响应中不存在 `legalRepresentativeIdNo` 明文字段。
未认证示例:
```json
{
"code": 401,
"message": "未认证或登录已失效",
"success": false,
"data": null
}
```
## 错误与前端处理
| 响应码 | 触发条件 | 前端处理 |
|---:|---|---|
| `400` | 身份证不是 18 位、出生日期/顺序码/校验位错误、三字段不完整、图片地址不符合规则 | 保留表单并定位到法人证件区域;不要自动重试 |
| `401` | 未登录或 Gateway 认证失效 | 进入统一重新登录流程 |
| `403` | 角色、功能权限或数据范围不足 | 隐藏无权操作并展示统一无权限提示 |
| `395014` | `expectedUpdateTime` 已过期 | 重新读取详情,提示用户确认后再提交 |
本次不新增业务错误码;参数失败继续使用统一 `400`。
## 前端改造清单
- 在“新建/编辑供应商-基本信息-法定代表人”后增加身份证号输入框、人像面上传和国徽面上传。
- 上传完成后提交永久 HTTPS 地址;不要提交临时签名 URL、查询参数 URL、Base64 或文件二进制。
- 前端可做 18 位长度和末位 `X/x` 预校验,但最终以服务端日期及校验位结果为准。
- 三字段联动必填;旧数据三个字段均为空时允许继续按原流程操作。
- 详情只展示 `legalRepresentativeIdNoMask`;不得寻找或缓存身份证号明文。
- 写成功后如需回显,重新调用详情接口,不要从写响应读取新字段。
## 验证证据
- 自动化:最新 `dev-v3` 的 Resource 全量测试 2111 项通过、0 失败、0 错误,38 项既有条件跳过;法人证件聚焦测试 89 项通过。
- TEST:Deploy Panel 任务 `118fcff1` 将目标提交 `de615b49e3ecef4be13bd6bc78b3100d08ef0bd2` 部署到双实例;该提交包含 #6391 合并提交 `b909c8f7dfd73712886a33b574c72030dde89d4f`,服务与 Nacos 健康检查通过。
- 真实 Gateway:7 组场景通过,覆盖合法创建、错误校验位零写入、详情脱敏与图片回显、未认证拒绝、非法更新零写入、旧客户端省略字段兼容和迁移状态。
- 清理:本轮验收草稿已通过业务删除接口软删除并保留正常删除审计,不遗留可用测试供应商。
## 撤回
1. 从最新 `dev-v3` 创建回退分支,revert #6401 合并提交并经独立 PR 合入。
2. 重新部署 `hl-resource-service`;新增的可空数据结构保留,不执行破坏性删除。
3. 旧客户端、存量供应商和已保存的安全数据保持兼容;无需恢复配置、Redis 或 MQ。
4. 经 Gateway 重跑旧请求、合法/非法证件号、三字段完整性、详情脱敏和失败零写入检查。
## 关联 / 联系人
- **Issue**: [#6391](https://git.1814.love:8443/wx/HL/issues/6391)
- **PR**: [#6401](https://git.1814.love:8443/wx/HL/pulls/6401)
- **合并提交**: [b909c8f7d](https://git.1814.love:8443/wx/HL/commit/b909c8f7dfd73712886a33b574c72030dde89d4f)
- **后端负责人**: @lc
@@ -0,0 +1,114 @@
---
schema: "hl-changelog/v2"
ticket: "6392"
title: "供应商暂停合作与拉黑原因必填接口"
consumer: "admin"
author: "lc(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "PR #6403 已合并 dev-v3;两个状态命令已部署 TEST,ACTIVE→SUSPENDED、ACTIVE/SUSPENDED→BLACKLIST、原因审计、权限、并发、幂等和失败零写入均经真实 Gateway 验证。前端待接入原因弹窗与并发版本。"
updated_at: "2026-08-26"
base: "dev-v3"
---
# 供应商暂停合作与拉黑原因必填接口
供应商管理新增两个窄状态命令:暂停合作与列入黑名单。服务端强制要求业务原因和调用方读取到的并发版本,并沿用可信管理员身份、专用平台权限、聚合锁、短窗幂等、行锁、状态机及同事务审计。
## 变更接口
| 方法 | 路径 | 允许来源状态 | 目标状态 |
|---|---|---|---|
| POST | `/admin/supplier/items/{supplierId}/suspend` | `ACTIVE` | `SUSPENDED` |
| POST | `/admin/supplier/items/{supplierId}/blacklist` | `ACTIVE`、`SUSPENDED` | `BLACKLIST` |
两个接口都要求真实 `SUPER_ADMIN` 身份且拥有 `supplier:status:manage` 平台权限。前端按钮可按角色和状态控制展示,但不能替代服务端门禁。
## 公共请求
```json
{
"reason": "供应商连续违约,暂停合作复核",
"expectedUpdateTime": "2026-08-26 15:40:00"
}
```
| 字段 | 类型 | 必填 | 规则 |
|---|---|---|---|
| `reason` | string | 是 | 去除首尾空白后必须非空,最长 500 字符;规范化后的原因为状态审计内容 |
| `expectedUpdateTime` | string | 是 | 调用方最近一次读取到的供应商 `updateTime`,格式 `yyyy-MM-dd HH:mm:ss` |
路径参数 `supplierId` 必须为正 Long。请求体不接受角色、操作者或目标状态,身份仅来自 Gateway 验证后的可信属性。
## 成功响应
暂停合作成功:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "1900000000000000001",
"status": "SUSPENDED",
"updateTime": "2026-08-26 15:40:01"
}
}
```
拉黑成功时结构相同,`status` 为 `BLACKLIST`。`supplierId` 固定为 JSON string;后续状态命令必须使用响应或详情中的新 `updateTime`,不能继续提交旧版本。
每次成功迁移与一条 `supplier_change_log` 状态审计在同一本地事务内提交。审计记录包含 `fromStatus`、`toStatus`、操作者、角色、Trace ID 和规范化后的 `reason`;审计写入失败时状态整体回滚。
## 失败语义
| 场景 | 业务码 | 结果 |
|---|---:|---|
| 未登录或 Token 无效 | `401` | Gateway/服务认证拒绝,零业务写入 |
| `reason` 缺失、空白、纯空格、超过 500 字符,或缺少版本 | `400` | 参数/服务双层拒绝,零业务写入 |
| 供应商不存在或已软删除 | `395001` | 零业务写入 |
| 非 `SUPER_ADMIN` 或缺少 `supplier:status:manage` | `395004` | 专用状态权限失败关闭,零业务写入 |
| suspend 来源不是 `ACTIVE`;blacklist 来源不是 `ACTIVE/SUSPENDED` | `395005` | 状态机拒绝,零业务写入 |
| `expectedUpdateTime` 与数据库秒级版本不一致 | `395014` | 状态和审计均不写入,调用方应刷新后重试 |
| 5 秒内同操作者、供应商和相同请求重复提交 | `100502` | 重复请求被拒绝,不产生第二条状态审计 |
统一响应可能以 HTTP 200 承载业务失败,客户端必须同时检查 `code`、`success`、`message` 和 `data`。
## 前端联调事项
- 在合作中供应商上提供“暂停合作”和“列入黑名单”;暂停合作供应商仅提供“列入黑名单”。其他状态不展示这两个动作。
- 点击动作后弹出必填原因输入框,前端限制 500 字符;提交详情或列表中最近一次读取到的 `updateTime`。
- `395014` 应提示数据已变化并刷新详情;`395005` 应刷新当前状态;`100502` 应提示不要重复提交。
- 成功后使用响应中的 `status` 和 `updateTime` 更新页面或重新拉取详情/列表。
- 前端显示控制不构成授权;不得从请求体传入操作者、角色或目标状态。
## 验证证据
- 代码:PR #6403 合并提交为 `3f3a0bf734737ca0e84ed1cfcad1801835cd46d5`;供应商目标测试 62 项通过,Resource 全量 2100 项通过、0 失败、0 错误(38 项既有条件跳过),Gateway 路由/认证 9 项通过,`hl-verify` 全部通过。
- 部署:原规范任务 `d08c52f6` 成功,目标/实际提交均为 `4b31612aeba5c0d3ac690cc6af15b66290bd8b48`;只读恢复绑定原任务期样本,5 个可验证样本均健康、3 个本地不可验证样本、0 个故障样本,没有重复部署。
- 当前制品:后续同服务规范任务 `da90f211` 将 TEST 更新为 `dev-v3@692547a5989ec1f8fa1b1495fc8dd501cddc0fe0`,该提交包含本工单合并提交;6/6 个任务期样本均为双进程、Nacos 双 healthy/enabled 且端口匹配,0 不可验证、0 故障样本。本工单最终业务验收在该当前制品上完成。
- 真实 Gateway:验证 `ACTIVE→SUSPENDED→BLACKLIST` 和 `ACTIVE→BLACKLIST`,成功响应、详情状态、并发版本与同事务审计一致;原因首尾空白按规范化值入审计。
- 失败分支:未认证、真实 `CUSTOMIZER` 越权、原因缺失/空白/超长、缺版本、旧版本、DRAFT 来源、不存在对象、重复状态与短窗重复请求全部返回预期错误,主体版本、状态和审计计数均保持不变。
- 清理:仅创建 3 个随机 TEST 草稿,并以 ID、随机全名、当前状态三重条件设置 2 个 ACTIVE 夹具;未触碰既有供应商。验收后精确恢复 2 行为 DRAFT,再通过业务删除接口软删除全部 3 个夹具;有效供应商总量恢复为 4,`HL6392-` 活跃命名空间为 0。9 条脱敏审计按系统约定保留;两枚验收会话已注销,幂等键等待 6 秒自然过期,未手工修改 Redis。
## 撤回
1. 从最新 `dev-v3` 创建独立回退分支,执行 `git revert -m 1 --no-edit 3f3a0bf734737ca0e84ed1cfcad1801835cd46d5`,经评审 PR 合入;不要回退后续无关提交。
2. 通过 Deploy Panel API 对精确回退目标执行新鲜预检和显式部署 `hl-resource-service`,复核双实例、Nacos、日志以及既有供应商查询、建档、资料更新和审批链路。
3. 本次无 DDL、配置、Redis、MQ 或跨服务写入变更,不执行 schema、缓存或消息回退。
4. 已实际发生的 `SUSPENDED`、`BLACKLIST` 状态和审计不会随代码回退自动反转;禁止直接改库,只能由后续合法恢复合作/解除黑名单业务流程处理。
5. 回退后两个新增 POST 接口恢复不可用;前端在回退部署前隐藏/停用对应动作,并继续兼容既有供应商接口。
## 关联 / 联系人
- **Issue**: [#6392](https://git.1814.love:8443/wx/HL/issues/6392)
- **PR**: [#6403](https://git.1814.love:8443/wx/HL/pulls/6403)
- **合并提交**: [3f3a0bf73](https://git.1814.love:8443/wx/HL/commit/3f3a0bf734737ca0e84ed1cfcad1801835cd46d5)
- **后端负责人**: @lc
@@ -0,0 +1,392 @@
---
schema: "hl-changelog/v2"
ticket: "6397"
title: "供应商注册合同聚合信息"
consumer: "admin"
author: "lc(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "f2a4200d"
target_release: ""
verified_at: "2026-08-26"
status_note: "PR #6405 已合并 dev-v3;Deploy Panel 任务 af3f205b 成功发布 Resource 双实例,合同接口已完成 23 项真实 Gateway 验收。TEST 工作副本存在服务器本地提交导致精确提交回读仍被阻塞,后端工单保持开启;本条只交接已经实测存在的接口契约。"
updated_at: "2026-08-26"
base: "dev-v3"
---
# 🔧 供应商注册合同聚合信息
供应商创建草稿、提交注册和基础信息详情现统一支持合同完整快照。管理端应把“合同信息”放在“资质证照”之后、现有账户区域之前,并把原“初始账户”展示标题改为“结算信息”。
展示名调整不改变接口字段:原请求字段仍为 `initialAccounts`,不得改成 `settlementInfo` 或其他名称。
## 变更接口清单
| # | 接口 | 方法 | 路径 | 变化 |
|---:|---|---|---|---|
| 1 | 创建供应商注册草稿 | POST | `/admin/supplier/items/add` | 请求可选增加 `contracts` 完整集合 |
| 2 | 提交供应商注册 | POST | `/admin/supplier/items/{supplierId}/submit` | 请求可选增加 `contracts` 完整快照 |
| 3 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应增加 `contracts` 列表 |
统一响应均为 `Result<T>`。业务失败可能仍为 HTTP 200,调用方必须同时判断 `code`、`success`、`message` 和 `data`。
## 公共合同字段
### 请求字段 `contracts[]`
| 字段 | 类型 | 创建必填 | 提交既有项必填 | 约束与说明 |
|---|---|---:|---:|---|
| `contractId` | String | 否,且禁止传入 | 是 | 正整数 ID 字符串;必须属于当前供应商,同一快照不得重复 |
| `contractName` | String | 是 | 是 | 非空白,最长 500 字符 |
| `contractNo` | String | 否 | 否 | 最长 100 字符 |
| `contractType` | String | 是 | 是 | `FRAME`、`SINGLE_TRIP`、`PURCHASE` |
| `signDate` | String | 否 | 否 | `yyyy-MM-dd` |
| `startDate` | String | 是 | 是 | `yyyy-MM-dd` |
| `endDate` | String | 是 | 是 | `yyyy-MM-dd`,不得早于 `startDate` |
| `amount` | Number | 否 | 否 | 大于等于 0,最多 10 位整数和 2 位小数 |
| `pricingMode` | String | 否 | 否 | 计价方式说明,最长 100 字符 |
| `settleCycle` | String | 否 | 否 | 结算周期说明,最长 32 字符 |
| `status` | String | 是 | 是 | 写接口允许 `DRAFT`、`ACTIVE`、`EXPIRED` |
| `scanFileUrl` | String | 否 | 否 | 合同扫描件永久地址,最长 1000 字符 |
| `remark` | String | 否 | 否 | 最长 500 字符 |
### 响应字段 `contracts[]`
详情返回上述全部业务字段,并额外返回:
| 字段 | 类型 | 说明 |
|---|---|---|
| `contractId` | String | 合同 ID,始终按字符串处理,不得转 JavaScript Number |
| `updateTime` | String | 合同当前版本,格式 `yyyy-MM-dd HH:mm:ss` |
历史数据可能返回只读状态 `TERMINATED`;创建和提交请求不得发送该状态。
## 1. 创建供应商注册草稿
`POST /admin/supplier/items/add`
### 使用场景与边界
- `contracts` 可省略、为 `null` 或空数组,旧客户端行为不变。
- 非空时最多 100 项,合同与供应商主体、资质和 `initialAccounts` 一起成功或一起失败。
- 创建请求中的每个合同都是新合同,禁止携带 `contractId`。
- 写入仍要求现有供应商创建权限;仅可信 `FINANCE`、`SUPER_ADMIN` 且拥有对应平台权限的身份可执行。
### 典型请求
```http
POST /admin/supplier/items/add
Authorization: Bearer <admin-token>
Content-Type: application/json
```
```json
{
"fullName": "示例供应商有限公司",
"shortName": "示例供应商",
"taxNo": "91350211M000100Y46",
"types": [
{
"typeCode": "SCENIC"
}
],
"mainCooperation": "景区资源合作",
"licenseImageUrl": "https://files.example.com/license.png",
"qualifications": [
{
"qualType": "BUSINESS_LICENSE",
"certNo": "LIC-2026-001",
"imageUrl": "https://files.example.com/license.png",
"permanentValid": true
}
],
"contracts": [
{
"contractName": "2026 年度框架合同",
"contractNo": "HT-2026-001",
"contractType": "FRAME",
"signDate": "2026-08-26",
"startDate": "2026-09-01",
"endDate": "2027-08-31",
"amount": 1200.50,
"pricingMode": "按团结算",
"settleCycle": "MONTHLY",
"status": "DRAFT",
"scanFileUrl": "https://files.example.com/contracts/HT-2026-001.pdf",
"remark": "年度合作"
}
],
"initialAccounts": []
}
```
### 成功响应
```json
{
"code": 200,
"message": "成功",
"data": {
"supplierId": "2090300000000063970",
"supplierNo": null,
"status": "DRAFT",
"onboardingStage": "PROFILE_DRAFT",
"initialAccounts": [],
"updateTime": "2026-08-26 11:10:00"
},
"success": true
}
```
### 失败响应:创建携带合同 ID
```json
{
"code": 400,
"message": "创建草稿不能携带合同ID",
"data": null,
"success": false
}
```
失败时不会留下供应商主体或部分合同。
## 2. 提交供应商注册
`POST /admin/supplier/items/{supplierId}/submit`
### 使用场景与快照语义
- `contracts` 省略或为 `null`:本次不处理合同,保留草稿当前合同。
- `contracts: []`:明确清空当前全部合同。
- 非空数组:作为完整快照;带 `contractId` 的项覆盖当前合同,不带 ID 的项新增,当前已有但数组中遗漏的合同删除。
- 带 ID 的合同必须属于路径中的供应商;不属于当前供应商、重复 ID、非法枚举、负金额或日期逆序均失败。
- `expectedUpdateTime` 仍是供应商聚合并发版本;发生并发修改时调用方应刷新详情后重新组装完整表单。
- 合同快照会进入本次审批资料,但提交注册不会自动改写合同自身的 `status`。
### 典型请求
```http
POST /admin/supplier/items/2090300000000063970/submit
Authorization: Bearer <admin-token>
Content-Type: application/json
```
```json
{
"fullName": "示例供应商有限公司",
"shortName": "示例供应商",
"taxNo": "91350211M000100Y46",
"types": [
{
"typeCode": "SCENIC"
}
],
"mainCooperation": "景区资源合作",
"licenseImageUrl": "https://files.example.com/license.png",
"qualifications": [
{
"qualType": "BUSINESS_LICENSE",
"certNo": "LIC-2026-001",
"imageUrl": "https://files.example.com/license.png",
"permanentValid": true
}
],
"contracts": [
{
"contractId": "2090300000000063971",
"contractName": "2026 年度框架合同",
"contractNo": "HT-2026-001",
"contractType": "FRAME",
"signDate": "2026-08-26",
"startDate": "2026-09-01",
"endDate": "2027-08-31",
"amount": 1200.50,
"pricingMode": "按团结算",
"settleCycle": "MONTHLY",
"status": "ACTIVE",
"scanFileUrl": "https://files.example.com/contracts/HT-2026-001.pdf",
"remark": "提交审批"
}
],
"initialAccounts": [],
"expectedUpdateTime": "2026-08-26 11:10:00"
}
```
### 成功响应
```json
{
"code": 200,
"message": "成功",
"data": {
"approvalLogId": "2090300000000063972",
"requestNo": "SUP-REQ-7b8c9d00112233445566778899aabbccddeeff00112233445566778899aabbcc",
"provider": "LOCAL_AUTO",
"approvalStatus": "APPROVED",
"spNo": null,
"spStatus": null,
"syncStatus": "APPLIED",
"submittedAt": "2026-08-26 11:11:00",
"finishedAt": "2026-08-26 11:11:00"
},
"success": true
}
```
### 失败响应:合同不属于当前供应商
```json
{
"code": 400,
"message": "合同不属于当前供应商",
"data": null,
"success": false
}
```
该失败会回滚本次提交表单中的主体、资质、合同和审计变化,供应商仍保持原状态和原版本。
## 3. 查询供应商基本信息
`GET /admin/supplier/items/{supplierId}/basic-info/view`
### 请求
```http
GET /admin/supplier/items/2090300000000063970/basic-info/view
Authorization: Bearer <admin-token>
```
无请求体。读取继续要求可信读角色和 `supplier:view` 平台权限。
### 成功响应
```json
{
"code": 200,
"message": "成功",
"data": {
"supplierId": "2090300000000063970",
"supplierNo": null,
"fullName": "示例供应商有限公司",
"shortName": "示例供应商",
"tax_no": "9135**********0Y46",
"legalRepresentative": null,
"legalRepresentativeIdNoMask": null,
"legalRepresentativeIdCardFrontUrl": null,
"legalRepresentativeIdCardBackUrl": null,
"contactPhoneMask": null,
"establishDate": null,
"registeredCapital": null,
"businessScope": null,
"staffScale": null,
"mainCooperation": "景区资源合作",
"status": "DRAFT",
"creditLevel": "B",
"totalScore": null,
"types": [
{
"typeCode": "SCENIC",
"typeName": "景区"
}
],
"contacts": [],
"qualifications": [
{
"qualificationId": "2090300000000063973",
"qualType": "BUSINESS_LICENSE",
"qualTypeName": "营业执照",
"certNoMask": "LI*********01",
"imageUrl": "https://files.example.com/license.png",
"expiryDate": null,
"permanentValid": true,
"daysUntilExpiry": null,
"validityStatus": "VALID",
"validityStatusName": "有效",
"isRequired": false,
"expired": false,
"updateTime": "2026-08-26 11:10:00"
}
],
"contracts": [
{
"contractId": "2090300000000063971",
"contractName": "2026 年度框架合同",
"contractNo": "HT-2026-001",
"contractType": "FRAME",
"signDate": "2026-08-26",
"startDate": "2026-09-01",
"endDate": "2027-08-31",
"amount": 1200.50,
"pricingMode": "按团结算",
"settleCycle": "MONTHLY",
"status": "DRAFT",
"scanFileUrl": "https://files.example.com/contracts/HT-2026-001.pdf",
"remark": "年度合作",
"updateTime": "2026-08-26 11:10:00"
}
],
"updateTime": "2026-08-26 11:10:00"
},
"success": true
}
```
合同按 `contractId` 升序返回,只包含当前有效合同。没有合同时返回空数组 `[]`,不返回 `null`。
### 常见失败
| 场景 | `code` | 前端处理 |
|---|---:|---|
| 未登录或 Token 失效 | `401` | 跳转登录,不展示空详情 |
| 可信角色或 `supplier:view` 平台权限不足 | `403` | 展示无权限状态 |
| 供应商不存在或已删除 | `395001` | 返回列表并刷新 |
## 修改前后对比
| 场景 | 修改前 | 修改后 |
|---|---|---|
| 创建草稿 | 请求不能携带合同 | 可选携带完整 `contracts`,与草稿一起成功或失败 |
| 提交注册 | 提交表单不能维护合同 | 可省略保留、空数组清空或提交完整合同快照 |
| 基础信息 | 不返回合同列表 | 返回完整 `contracts[]` 及字符串 ID、版本 |
| 账户区域标题 | 页面显示“初始账户” | 页面应显示“结算信息”,接口字段仍为 `initialAccounts` |
| 页面区块顺序 | 资质后直接进入账户区域 | 资质证照 → 合同信息 → 结算信息 |
## 兼容性与管理端接入事项
1. 在“资质证照”区域之后新增“合同信息”表格,在合同之后显示原账户表格。
2. 原账户表格标题改为“结算信息”;所有请求和响应继续使用 `initialAccounts`,不要改字段名。
3. 创建草稿时合同为完整新项,不发送 `contractId`;编辑后提交时,既有合同必须原样带回字符串 `contractId`。
4. 提交表单是完整快照。用户明确删除全部合同时发送 `contracts: []`;未加载合同或不处理合同时省略字段,不要误发空数组。
5. 所有 ID 均作为字符串保存、比较和回传,不经过 Number 转换。
6. 本次不新增接口路径、权限点或业务错误码;旧客户端省略 `contracts` 时继续可用。
7. 管理端源码不在本后端工单中修改,前端状态保持 `pending`,直至完成页签、标题和表格接入并提供前端引用。
## TEST 验证证据
- 自动化:供应商定向测试 115 项通过;`hl-resource-service` 全量 2,111 项,0 失败、0 错误,38 项条件跳过;`hl-verify` 与差异检查通过。
- 部署:Deploy Panel API 任务 `af3f205b` 终态 `success`、退出码 0、`has_build_error=false`,未发现 Maven、编译或滚动发布错误。
- 健康:`hl-resource-service` 的 8082、8182 双实例运行;Nacos `test` 命名空间两实例均 `healthy=true`、`enabled=true`。
- 真实 Gateway:23 项断言通过,覆盖合同随草稿创建、详情完整回显、字符串 ID、创建携带 ID 失败、普通 ADMIN 越权、提交外部合同 ID 完整回滚、日期逆序零写入和旧客户端省略 `contracts`。
- 清理:两个临时 DRAFT 均通过业务删除接口软删除并回读为不存在;仅保留不可逆的 CREATE/DELETE 操作审计。
- 环境限制:部署前锁定 `origin/dev-v3=1a16a5aec7d0b95ec87e6fb222581060a3135984`,但面板 Git API 回读到服务器本地短提交 `6f7d3ca78`,Gitea 无法解析该对象。接口行为已真实验证,后端工单仍等待测试环境恢复精确远端提交后复验,不能据此宣称最终交付完成。
## 撤回
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit 1a16a5aec7d0b95ec87e6fb222581060a3135984`,经独立 PR 合入。
2. 重新滚动部署 `hl-resource-service`;无需执行数据库结构、配置、Redis 或 MQ 恢复。
3. 管理端停止发送和读取 `contracts`,恢复原页面结构;`initialAccounts` 契约始终不变。
4. 已保存的合同资料保留,不做破坏性批量清理;回退后旧客户端继续按省略 `contracts` 的路径工作。
5. 经 Gateway 复测创建、提交、基础信息、未认证、越权、非法合同 ID、失败零写入和旧客户端兼容,并确认双实例与 Nacos 健康。
## 关联 / 联系人
- **Issue**: [#6397](https://git.1814.love:8443/wx/HL/issues/6397)
- **PR**: [#6405](https://git.1814.love:8443/wx/HL/pulls/6405)
- **合并提交**: [1a16a5aec](https://git.1814.love:8443/wx/HL/commit/1a16a5aec7d0b95ec87e6fb222581060a3135984)
- **后端负责人**: @lc
@@ -0,0 +1,188 @@
---
schema: "hl-changelog/v2"
ticket: "6406"
title: "开票申请按税号自动回填企业工商信息"
consumer: "admin"
author: "wx(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "080ea8df"
target_release: ""
verified_at: "2026-08-26"
status_note: "PR #6415 已合并 dev-v3 并部署测试服,经网关 API 实测 companyInfo 与 lastInvoiceTitle 两段往返一致,非法税号返回 581526"
updated_at: "2026-08-26"
base: "dev-v3"
---
# 🔍 开票申请:按税号自动回填企业工商信息
开票申请填写页面现支持按税号自动回填企业工商信息。后端对接第三方企业信息查询服务(元典开放平台),并同时返回本系统同税号最近一条历史开票抬头,两段数据并列返回、由前端自行决定展示与回填优先级,后端不做合并取舍。
> **PR**: [#6415](https://git.1814.love:8443/wx/HL/pulls/6415) | **服务**: hl-order-service-v3 | **作者**: wx | **更新时间**: 2026-08-26
---
## 一、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 开票企业信息自动回填查询 | GET | `/v3/admin/invoice/company-info` | 新增 | 按税号查两段回填数据 |
---
## 二、接口详情
### 1. 开票企业信息自动回填查询 `GET /v3/admin/invoice/company-info`
**入参**: `taxNo`(Query 参数)
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| taxNo | Query | String | ✅ | 非空;去空格后长度 15 或 18 | 统一社会信用代码(或旧 15 位注册号) |
**出参 `Result<CompanyInfoRespVO>`**:
| 字段 | 类型 | 说明 |
|------|------|------|
| companyInfo | Object 或 null | 元典第三方工商照面;查不到/第三方异常/全 key 耗尽时为 null |
| lastInvoiceTitle | Object 或 null | 本系统同税号最近一条非作废发票的历史抬头;无历史时为 null |
**companyInfo 字段**(元典工商照面):
| 字段 | 类型 | 说明 |
|------|------|------|
| titleName | String | 企业名称 |
| taxNo | String | 统一社会信用代码 |
| legalPersonName | String | 法定代表人 |
| registAddress | String | 注册地址 |
| regStatus | String | 经营状态(存续/注销/吊销等,由第三方返回) |
**lastInvoiceTitle 字段**(本系统历史抬头):
| 字段 | 类型 | 说明 |
|------|------|------|
| titleName | String | 开票抬头 |
| bankName | String | 开户行 |
| bankAccount | String | 银行账号 |
| registAddress | String | 注册地址 |
| registPhone | String | 注册电话 |
| email | String | 邮箱 |
#### 请求示例
```
GET /v3/admin/invoice/company-info?taxNo=91110000802100433B
Authorization: Bearer <admin-token>
```
#### 成功响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"companyInfo": {
"titleName": "北京百度网讯科技有限公司",
"taxNo": "91110000802100433B",
"registAddress": "北京市海淀区上地十街10号百度大厦2层",
"legalPersonName": "梁志祥",
"regStatus": "存续"
},
"lastInvoiceTitle": {
"titleName": "北京百度网讯科技有限公司",
"bankName": "招商银行北京分行",
"bankAccount": "110900100011110",
"registAddress": "北京市海淀区上地十街10号百度大厦2层",
"registPhone": "010-59928888",
"email": "invoice@baidu.com"
}
},
"success": true
}
```
#### 降级响应示例(任一段查不到为 null,互不阻塞)
```json
{
"code": 200,
"message": "成功",
"data": {
"companyInfo": null,
"lastInvoiceTitle": null
},
"success": true
}
```
#### 非法税号错误响应
```json
{
"code": 581526,
"message": "税号格式不正确(统一社会信用代码须为 15 或 18 位)",
"data": null,
"success": false
}
```
---
## 三、边界行为
- 未登录 → 401(网关拦截)
- 税号为空 / 去空格后非 15 或 18 位 → `581526` 参数错误
- 元典查无该企业 → `companyInfo = null`,`lastInvoiceTitle` 照常返回
- 元典接口超时 / 异常 / 全部 key 不可用 → 降级 `companyInfo = null`,不影响 `lastInvoiceTitle`
- 本系统无同税号历史发票 → `lastInvoiceTitle = null`
- 税号含空格 → 后端自动去空格后再查询,不报错
---
## 四、不影响范围(显式声明)
- **仅新增**:本查询接口为纯只读旁路查询
- **零影响**:
- 开票申请提交接口(`/v3/admin/order/{orderId}/invoice/apply`)行为不变
- 开票流程、开票记录写入逻辑
- 既有发票列表/详情查询
- 订单、订单核心、房务、车务模块
---
## 五、测试环境已验证
网关实测(`https://api.test.1814.love:9443`):
```
GET /v3/admin/invoice/company-info?taxNo=91110000802100433B
→ 200 + companyInfo(北京百度网讯科技有限公司/梁志祥/存续) ✓
→ 200 + lastInvoiceTitle(招商银行北京分行/110900100011110/invoice@baidu.com) ✓
GET /v3/admin/invoice/company-info?taxNo=123
→ 581526 税号格式不正确 ✓
未带 Authorization 头
→ 401 缺少有效的 Authorization 头 ✓
```
---
## 十、相关文档
- 关联 Issue: [wx/HL#6406](https://git.1814.love:8443/wx/HL/issues/6406)
- 关联 PR: [wx/HL#6415](https://git.1814.love:8443/wx/HL/pulls/6415)
## 关联 / 联系人
### 链接
- **Issue**: [#6406](https://git.1814.love:8443/wx/HL/issues/6406)
- **PR**: [#6415](https://git.1814.love:8443/wx/HL/pulls/6415)
- **Merge commit**: [f2c6e9fef53315a458b79f71747ee3494c420694](https://git.1814.love:8443/wx/HL/commit/f2c6e9fef53315a458b79f71747ee3494c420694)
### 联系人
- **后端负责人**: @wx
@@ -0,0 +1,64 @@
---
schema: "hl-changelog/v2"
ticket: "6409"
title: "行政区划响应 ID 固定为字符串"
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 #6412 已合并 dev-v3;hl-user-service 已以提交 4b31612a 部署 TEST,create、children、path 经真实 Gateway 验证全部 Long ID 固定为 JSON string,空层级保持 null。"
updated_at: "2026-08-26"
base: "dev-v3"
---
# 行政区划响应 ID 固定为字符串
行政区划创建、单级联动和父链回显接口中的 Long ID 现在始终按 JSON 字符串返回,避免小整数因全局安全整数规则被输出为 JSON number。请求参数、权限、业务校验和错误码不变。
## 变更接口
| 方法 | 路径 | 响应变化 |
|---|---|---|
| POST | `/admin/region` | 成功响应 `data` 从 JSON number 固定为 string |
| GET | `/admin/region/children` | `parentId`、`defaultId`、`items[].id`、`items[].parentId` 固定为 string;空值保持 `null` |
| GET | `/admin/region/{id}/path` | `selectedId`、`provinceId`、`prefectureId`、`countyId` 及 `path` 节点 ID 固定为 string;缺失层级保持 `null` |
更新接口 `PUT /admin/region/{id}` 的请求和响应不变。所有请求路径、Query/Path 参数仍按原 Long 语义解析。
## 兼容性与错误语义
- 这是已发布接口 JSON 类型的契约修正,不新增字段,也不改变字段名称。
- 行政区划 Redis 缓存可同时反序列化旧 JSON number 与新 JSON string,滚动部署期间兼容。
- 根层 `parentId`、无默认节点的 `defaultId` 和父链中不存在的层级继续返回 `null`,不会变成字符串 `"null"`。
- 未认证请求继续返回业务码 `401`;非法正整数参数、缺失节点和非法节点类型继续使用既有错误语义,失败路径零业务写入。
- 无数据库 migration、配置、Gateway、MQ、缓存键或跨服务契约变更;无需修改前端源码,调用方按既定字符串 ID 契约消费即可。
- 统一响应可能以 HTTP 200 承载业务失败,客户端必须同时检查 `code`、`success`、`message` 和 `data`。
## TEST 验证证据
- PR #6412 合并提交为 `8494028aa2dfbb4e39147c09695898d247045d4c`;TEST 目标提交 `4b31612aeba5c0d3ac690cc6af15b66290bd8b48` 包含该合并提交。
- Deploy Panel API 任务 `b10dd2ed` 成功执行 `/opt/hulalv/scripts/deploy-backend.sh`;User 的 8081、8181 双实例和 Nacos 两个 healthy/enabled 实例通过,任务期 9 个可验证采样均有可用实例,0 个故障采样。
- 真实 TEST 管理员经 Gateway 验证 create、children、path:创建 ID、联动列表 ID、父链 ID 和对应 Redis 缓存 ID 均为字符串,根节点和缺失层级空值保持 `null`。
- 未认证请求返回 `401`;非法父节点、缺失节点、空创建和旧非法节点类型均按既有错误返回,并确认失败路径零写入。
- 成功创建和 CAS 更新后,精确删除本次创建的 1 行;数据库总量恢复且测试命名空间为 0。4 个相关 Redis key 按基线值及绝对过期时间恢复,验收会话已注销;操作审计按系统约定保留。
- 本地验证:Controller/Cache 定向 22 项通过;User 全量 3644 项通过、0 失败、0 错误,8 项条件跳过;`hl-verify` 与 `git diff --check` 通过。
## 撤回
1. 从最新 `dev-v3` 创建独立回退分支,执行 `git revert -m 1 --no-edit 8494028aa2dfbb4e39147c09695898d247045d4c`,经评审 PR 合入;不要回退后续无关提交。
2. 通过 Deploy Panel API 对回退目标执行新鲜预检和显式部署 `hl-user-service`,复核双实例、Nacos、日志和精确提交。
3. 本修复无数据库、配置或 MQ 变更,不执行 DDL、DML 或消息补偿。缓存新旧表示均可读取,通常无需清理;确需强制恢复旧表示时,仅在停止行政区划刷新后精确处理 `hl:user:region:v1:` 命名空间并由回退版本预热。
4. 撤回前确认调用方可重新接受 JSON number ID;撤回后经 Gateway 复测 create、children、path、空层级、未认证和失败零写入。
## 关联 / 联系人
- **Issue**: [#6409](https://git.1814.love:8443/wx/HL/issues/6409)
- **PR**: [#6412](https://git.1814.love:8443/wx/HL/pulls/6412)
- **合并提交**: [8494028aa](https://git.1814.love:8443/wx/HL/commit/8494028aa2dfbb4e39147c09695898d247045d4c)
- **后端负责人**: @lc