25 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 7990 | 候选资源槽位需求锚点按 requirementId 反查需求类别,TRANSFER-only 订单座位/车型判定不再静默退化 | admin | wx(GIT) | 修改接口 | deployed | verified | not_required | mmg | PR #8062(squash bf4fba5a2)已合入 dev-v3(origin/dev-v3 祖先关系已核,本地 worktree HEAD 落后但不影响判断);fleet 已部署 TEST,deploy-status.sh 复核 0/N。2026-09-20 22:0x 经网关实测同一次候选查询里两辆车判定截然相反(蒙A-U1557 7座 seatsEnough=true/requirementMatched=true;蒙P321A 5座同车型 seatsEnough=false/requirementMatched=false/reasonCode=SEATS),坐实修复对 TRANSFER-only 订单确有生效。本条与本目录已推送的 20_7990_车务看板订单详情按需求各返一组身份三元组-修改接口-管理后台.md(PR #8048)是#7990 下两个独立 AC:那份讲看板详情接口新增 requirementIdentities 身份三元组,按 requirementMatched/seatsEnough/requirementMismatchReasonCode/候选/座位 六词逐一核对全部 0 命中,未覆盖本条候选面座位/车型判定行为,故另立本文档,不是重复推送。 前端实证翻 not_required(mmg 2026-09-20):candidates 四判定字段唯一消费面 VehiclePickerList 按字面语义渲染(requirementMatched===false 显 mismatch 文案、seatsEnough===false 显座位提示、null 不显),无 TRANSFER-only 恒 null 规避性特判,无 workaround 可撤;TRAVEL 逐字不变、TRANSFER 前端不可达(809009 未开,#7443 挂起),修复后 TRANSFER 开放时候选提示自动更准,零改动。 | 2026-09-20 | dev-v3 |
fleet: 候选资源槽位需求锚点按 requirementId 反查需求类别(TRANSFER-only 订单座位/车型判定修复)
存放目录: 二期(v3) →
changelogs-v2/{YYYY-MM}/服务: hl-fleet-service (端口 8009) PR: #8062 Issue: #7990 AC-8 日期: 2026-09-20 影响范围: 管理后台车务派单弹窗「查询派单候选资源」——候选车辆列表的车型/座位匹配提示(
seatsEnough/requirementMatched/requirementMismatchReasonCode/requirementMismatchMessage四字段),仅当订单挂有 TRANSFER(接送机)需求时取值受影响
⚠️ 关键变化
这不是新字段,是既有字段在特定订单形态下的取值口径修复:seatsEnough / requirementMatched / requirementMismatchReasonCode / requirementMismatchMessage 四个字段本身早已存在(#5871/#5788),本次改的是它们内部用来比对的「槽位需求锚点」在接送机相关订单上取错了对象。
- 两类需求并存的订单(同一订单既有行程用车又有接送机):接送机派车/改派时,候选面锚点此前恒取行程用车那条需求的对应车型项,合适的接送机用车会被误标「需求不匹配」。
- 只有接送机需求的订单(TRANSFER-only):此前锚点直接取不到值,整块退化成
SlotRequirement.empty()这条「仅提示不限制」的正常路径——不抛错、不打日志,座位判据悄悄回退到整单headcount,车务只会看到提示文案不对,没有任何信号可循。 - 本目录已推送的
20_7990_车务看板订单详情按需求各返一组身份三元组-修改接口-管理后台.md(PR #8048)讲的是同一工单下另一个 AC(看板详情接口新增requirementIdentities),按requirementMatched/seatsEnough/requirementMismatchReasonCode/候选/座位六个关键词逐一核对,那份正文全部 0 命中——它没有覆盖、也不能替代本条修复的说明。
一、背景
同一订单可以同时挂 TRAVEL(行程用车)与 TRANSFER(接送机)两条活跃需求(#7443 起的既有形态)。候选弹窗查询车辆/司机时,会按请求携带的 fleetItemIndex 在「当前生效需求」的 fleet[] 展开序列里定位一项,取它的车型与座位作为本槽位的判定锚点(resolveSlotRequirement,#5871)。
缺陷在于:这一步此前调用的是 OrderQueryFacade.getCurrentVehicleRequirementOrNull(orderId)——不带 kind 参数的重载,而 order-v3 侧对应 Feign 端点的 kind 默认值就是 TRAVEL。也就是说"不传 kind"在这里不是"不限类别",而是钉死取 TRAVEL:
| 订单形态 | 锚点实际取到的需求 | 后果 |
|---|---|---|
| TRAVEL + TRANSFER 并存 | 恒为 TRAVEL 那条 | 接送机槽位锚到另一条需求的车型/座位,合适的车被误判「需求不匹配」 |
| 仅 TRANSFER(TRANSFER-only) | null(该订单没有 TRAVEL 需求) |
锚点整块退空,座位判据回退到整单 headcount,车型判据回退前端传参 |
两种失败出口都是 SlotRequirement.empty(),这是这个方法「需求不可达时仅提示不限制」的既有正常降级路径——不抛错、不打日志。车务侧能观察到的只是候选面提示与实际情况对不上,没有任何错误码或日志可以顺藤摸瓜。
修法:改用 RequirementKindResolver.resolveForMutationByRequirementId(orderId, requirementId, travelSupplier) 做匹配式反查——请求本身已带 requirementId(改派场景必填),先用它去比对 TRAVEL 的当前需求 id,命中即用(TRAVEL 槽位逐字不变,且不产生任何额外远程读);不命中再去只读探测 TRANSFER 的当前需求 id 是否与之相等,相等才认;两边都对不上则原样退回 TRAVEL 分支的结果,与改动之前完全一致。没有新增请求参数,前端契约不变。
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 查询派单候选资源 | POST | /admin/fleet/assignments/candidates |
修改接口 | TRANSFER 相关订单上 seatsEnough/requirementMatched/requirementMismatchReasonCode/requirementMismatchMessage 四字段的判定锚点由「恒取 TRAVEL/退空」改为「按 requirementId 匹配式反查真实 kind」 |
三、接口详情
1. 查询派单候选资源 POST /admin/fleet/assignments/candidates
VO: AssignmentCandidateReqVO → AssignmentCandidateRespVO
使用场景
车务派单弹窗 Step 2「选车 / 选司机」列表,接送机(TRANSFER)派车或改派时同样调用本端点。前端据候选项的 seatsEnough / requirementMatched / requirementMismatchReasonCode / requirementMismatchMessage 渲染「这辆车是否满足本槽位需求」的提示——这四个字段只是提示,不限制能不能选车(requirementMatched=false 时 available/selectable 不受影响)。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| orderId | body | number(string) | 改派/接送机排除自身时必填 | 雪花 ID | 当前订单 ID;本次判定锚点新依赖它与 requirementId 联用 |
| requirementId | body | number(string) | 改派排除自身时必填 | 雪花 ID | 当前用车需求 ID;本次起是反查需求 kind 的唯一依据(不匹配任何 kind 的当前需求时退回 TRAVEL 分支,逐字兼容旧行为) |
| fleetItemIndex | body | integer | 否 | @Deprecated,仅用于回退推断车型/座位提示,不参与归属校验 |
需求车型项展开序号;反查到的需求 fleet[] 按 count 展开后,本序号落在哪项区间决定锚定到哪一项的车型/座位 |
| headcount | body | integer | 否 | ≥0 | 乘客人数;槽位需求不可达时座位判据回退到本字段(本次修复前 TRANSFER-only 订单必走这条回退,修复后仅在两类需求都反查不到时才走) |
| startDate / endDate | body | string(date) | 是 | yyyy-MM-dd |
本次派车服务起止日 |
| requiredVehicleType | body | string | 否 | — | 前端传参车型;仅用于槽位需求不可达时回退,槽位锚点解析成功时会压过它(既有语义,本次未变) |
其余字段(
excludeAssignmentId/assignmentGroupId/vehicleKeyword/分页参数等)本次未作任何改动,不重复列出。请求体没有新增任何字段——修复完全发生在服务端内部的需求反查逻辑。
出参
| 字段 | 类型 | 说明 |
|---|---|---|
| data.vehicles.records[].seatsEnough | boolean | 【本次取值口径修复】 可载客数是否满足本槽位座位需求;有槽位需求时按「车辆座位数 ≥ 槽位要求座位数」判,无槽位需求(锚点不可达)时回退整单 headcount |
| data.vehicles.records[].requirementMatched | boolean | 【本次取值口径修复】 车型和座位是否同时符合本槽位需求;false 仅提示,不影响 available/selectable |
| data.vehicles.records[].requirementMismatchReasonCode | string | 【本次取值口径修复】 不匹配的具体维度,枚举 VEHICLE_TYPE / SEATS / VEHICLE_TYPE_AND_SEATS;requirementMatched=true 或槽位需求彻底不可达时为 null(详见「六.5」) |
| data.vehicles.records[].requirementMismatchMessage | string | 【本次取值口径修复】 人读文案,以「本槽位需求为 XX」为主语;匹配时为 null |
| data.vehicles.records[].vehicleTypeKey / seats | string / integer | 车辆自身车型大类 key、核定座位数(未变,仅作为对照参与前面四字段的计算) |
其余字段(
vehicleId/plate/available/selectable/conflicts[]/司机候选等)本次一律未变,不重复列出。
请求示例
TRANSFER-only 订单(该单无行程用车需求,只有一条接送机需求)查询候选车辆:
{
"orderId": "2101580000000000001",
"requirementId": "2101580000000000009",
"fleetItemIndex": 0,
"startDate": "2026-09-25",
"endDate": "2026-09-25",
"headcount": 4,
"vehiclePage": 1,
"vehiclePageSize": 20
}
响应示例
2026-09-20 22:0x 经网关对同一 TRANSFER-only 订单的同一次候选查询,两辆车判定截然相反(车牌、座位数、seatsEnough/requirementMatched/requirementMismatchReasonCode 三字段取自当次实测;vehicleId/orderId/requirementId 未在交接材料中给出具体数值,此处用示意 ID,不代表真实雪花 ID):
{
"code": 200,
"message": "成功",
"data": {
"vehicles": {
"total": 2,
"records": [
{
"vehicleId": "2101590000000000101",
"plate": "蒙A-U1557",
"vehicleTypeKey": "suv",
"vehicleTypeName": "SUV",
"seats": 7,
"passengerCapacity": 6,
"seatsEnough": true,
"requirementMatched": true,
"requirementMismatchReasonCode": null,
"requirementMismatchMessage": null,
"available": true,
"selectable": true
},
{
"vehicleId": "2101590000000000102",
"plate": "蒙P321A",
"vehicleTypeKey": "suv",
"vehicleTypeName": "SUV",
"seats": 5,
"passengerCapacity": 4,
"seatsEnough": false,
"requirementMatched": false,
"requirementMismatchReasonCode": "SEATS",
"requirementMismatchMessage": "本槽位需求座位数为7,该车核定座位不足",
"available": true,
"selectable": true
}
]
}
},
"success": true
}
修复前这两辆车的 requirementMatched 都会是 null(锚点退空、仅提示不限制),看不出蒙P321A 座位不足。
空数据 / 降级响应
- 订单
requirementId对应的需求在 TRAVEL、TRANSFER 两个 kind 上都反查不到(如需求已被撤销)→ 原样退回 TRAVEL 分支取到的值(null时锚点整块为空)——与本次改动之前完全一致的降级路径,seatsEnough回退整单headcount,requirementMatched/requirementMismatchReasonCode/requirementMismatchMessage均为null。 orderId或requirementId任一为空 → 不触发反查,直接走既有 TRAVEL 分支(未变)。- 候选车辆本身为空 →
records为空数组、total=0,恒code=200。
错误响应
本端点是只读咨询,业务上恒 code=200,匹配结果在 data 体现,不会因为需求反查失败而报错。仅入参非法时报错,形态未变:
{
"code": 605010,
"message": "排除派单不属于当前订单",
"data": null,
"success": false
}
业务边界
- 只读,不写任何业务数据;反查是「匹配式」的,只读一次 TRANSFER 需求(TRAVEL 先命中即返回,不发生),不产生任何额外写操作。
requirementMatched=false只是提示,不影响available/selectable,最终一致仍以创建/改派锁内重校验为准。fleetItemIndex已@Deprecated,但候选面座位/车型回退推断仍在读它;新前端应尽快切到assignmentGroupId归属校验(#7067),本字段随其全量切换后一并删除。- 换版保留行(
supersededByRequirementId非空)与未派车占位行(UNASSIGNED)走的是「锚行自身快照」分支,不经过本次改动涉及的反查逻辑(既有行为,未变)。
四、契约约束与正确调用方式
本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。
4.1 反查依据 requirementId,不是新增 kind 参数
不要给请求体加 kind 字段去"帮忙"指定需求类别——后端不认,加了也不会生效。判定逻辑完全依赖已有的 requirementId:它唯一确定一条需求行,服务端据此反查其归属的 TRAVEL/TRANSFER 类别,前端无需也不应该猜测或传递类别信息。
4.2 改派/接送机场景 requirementId 必须是当前有效需求的 ID
反查是逐字匹配的:requirementId 必须与某个 kind 的当前生效需求 id 完全相等才会命中该 kind 的槽位锚点。传一个已过期/已换版的旧 requirementId,反查两边都不命中,原样退回 TRAVEL 分支的结果(可能是 null 锚点、也可能是另一条 TRAVEL 需求的值)——不会报错,前端界面上看到的是「提示不准」而不是接口失败,联调时如遇候选面提示怪异,先核对传的 requirementId 是否为当前有效需求。
✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload | 结果 |
|---|---|---|
| ✅ TRANSFER 改派,带当前有效需求 ID | { "orderId": "...", "requirementId": "<当前 TRANSFER 需求 id>", "fleetItemIndex": 0 } |
锚点反查命中 TRANSFER,座位/车型判定按接送机需求算 |
| ✅ TRAVEL 派车,带当前有效需求 ID | { "orderId": "...", "requirementId": "<当前 TRAVEL 需求 id>", "fleetItemIndex": 0 } |
TRAVEL 先命中即返回,逐字与改动前一致,无额外远程读 |
❌ 给请求体加 kind 字段 |
{ "orderId": "...", "requirementId": "...", "kind": "TRANSFER" } |
kind 字段被后端忽略(AssignmentCandidateReqVO 无此字段),判定仍按 requirementId 反查 |
⚠️ 传过期的旧 requirementId |
{ "orderId": "...", "requirementId": "<已换版旧 id>" } |
两个 kind 都反查不到,原样退回 TRAVEL 分支(可能是空锚点),不报错 |
五、数据库行为
无任何数据库写入。 本端点是只读咨询:
- 反查 TRANSFER 需求走的是 Feign 只读端点(
OrderQueryFacade.getCurrentVehicleRequirementOrNull),不落库、不写 outbox。 - TRAVEL 分支命中时不发生这次远程读(先命中即返回)。
- 无建表、无改表、无 Flyway 脚本、无索引变更。
六、边界行为
- TRAVEL 分支优先且短路:先比对 TRAVEL 的当前需求 id,命中即返回,不会为 TRAVEL 派车多打一次 TRANSFER 的远程读(单测
query_travelRequirementSlot_unchangedAndNoTransferProbe钉住这一点)。 - 反查失败静默退回旧行为:
orderId/requirementId为空,或两个 kind 都反查不到,一律原样退回 TRAVEL 分支的结果——这是「更严」不是「更松」的降级方向,不会把一条没核对过的需求放行。 fleetItemIndex越界或需求fleet[]为空:仍返回SlotRequirement.empty()(既有行为未变),四个判定字段回退到「仅提示不限制」语义。- 换版保留行/未派车占位行:直接读派单行自身快照,不进入本次改动涉及的反查分支(既有行为未变)。
- 该反查方法本是为写路径设计(方法名带
ForMutation),本次是它第 4 个调用点、也是唯一的只读调用点;它内部只做一次匹配式只读探测,不调用任何写端点、不走接送机派车的四步前置(回填/分流/GET/校验),在只读事务里复用是安全的。
六.5、枚举 / 数据字典
requirementMismatchReasonCode(AssignmentCandidateRespVO.VehicleCandidateVO.requirementMismatchReasonCode)
所属字段: data.vehicles.records[].requirementMismatchReasonCode | 类型: String
| 值 | 中文 | 说明 |
|---|---|---|
VEHICLE_TYPE |
车型不符 | 车型大类与槽位需求不一致,但座位数满足 |
SEATS |
座位不足 | 车型大类匹配,但核定座位数 < 槽位要求座位数 |
VEHICLE_TYPE_AND_SEATS |
车型与座位均不符 | 两个维度都不满足 |
null |
匹配 / 需求不可判定 | requirementMatched=true 时为 null;槽位需求彻底不可达(两个 kind 均反查不到且 TRAVEL 分支本身也取不到值)时同样为 null,此时只报 requirementMatched=false、不下发具体维度——这是本次修复要减少但不能完全消除的降级形态,仅当 requirementId 对应的需求确实已不存在于任何 kind 下才会触发 |
六.6、修改前后对比
字段级对比
本次没有新增或删除任何字段,改的是既有四个字段在特定订单形态下的计算口径:
| 字段 | 改前(TRANSFER 相关订单上) | 改后 |
|---|---|---|
seatsEnough |
两类需求并存:按 TRAVEL 需求座位比;TRANSFER-only:回退整单 headcount |
按 requirementId 反查到的真实 kind(TRAVEL 或 TRANSFER)需求座位比 |
requirementMatched |
两类需求并存:可能误判为 false;TRANSFER-only:恒 null(退化态) |
按真实 kind 需求计算,true/false 均可能,TRANSFER-only 不再恒退化 |
requirementMismatchReasonCode |
两类需求并存:可能报错误维度;TRANSFER-only:恒 null |
按真实 kind 需求计算 |
requirementMismatchMessage |
同上 | 同上 |
| TRAVEL-only 订单上以上四字段 | 未受影响 | 逐字不变(回归对照见「八」) |
行为级对比
| 场景 | 改前 | 改后 |
|---|---|---|
| 两类需求并存,接送机改派,槽位应为 7 座商务车 | 锚到 TRAVEL 需求(如 SUV 5 座),误标「本槽位需求为SUV5座,该车为商务车且核定座位不足」 | 锚到 TRANSFER 需求(商务车 7 座),匹配、无提示 |
| TRANSFER-only 订单,槽位需求 7 座、乘客 4 人,候选 5 座 SUV | 锚点退空,座位回退整单 headcount(4),5 座「够」,只报车型不符一条提示(少报一半) |
锚到 TRANSFER 需求(7 座),报 VEHICLE_TYPE_AND_SEATS,车型与座位都不符 |
| TRAVEL 派车/改派(无 TRANSFER 需求共存) | 锚到 TRAVEL 需求 | 逐字不变,且不多发一次远程读 |
六.7、影响评估
- 是否破坏向后兼容: 否。请求契约零变化;响应字段名与类型均未变,只是特定订单形态下取值更准确。
- 前端是否必须同步上线: 视既有实现而定——若前端曾针对 TRANSFER-only 订单这几个字段"总是空/总是不可信"做过规避性渲染(如判断到恒
null就不显示提示区),现在这些字段会按真实判定给出true/false/具体原因码,需要改回按字段值正常渲染;若前端一直按字面语义渲染(requirementMatched=false就显示requirementMismatchMessage),无需改动,字段口径变准确只会让提示更贴合实际。 - 前端 workaround 清理点: 若存在"TRANSFER-only 订单不渲染候选面车型/座位提示"的特判逻辑,可以撤销。
七、不影响范围
- 仅影响: 管理后台车务派单弹窗候选查询接口,且仅限订单挂有 TRANSFER 需求的场景。
- 零影响:
- 纯 TRAVEL 订单(无 TRANSFER 需求共存)——四个判定字段逐字不变,有单测
query_travelRequirementSlot_unchangedAndNoTransferProbe钉住,且不产生额外远程调用。 - 候选面其余字段:
available/selectable/conflicts[]/司机候选/价格日历/常驻关系等——一律未变。 POST /admin/fleet/assignments/precheck预校验冲突端点——未涉及本次改动路径,未变。- 派单创建/改派/取消/撤销取消等所有写接口——本次只改了只读候选查询里的一处需求反查,不改任何写口逻辑。
- 请求参数:无新增/删除/改名,无新增必填项。
- 数据库:无写入、无迁移、无索引变更。
- 小程序端——不涉及。
- 纯 TRAVEL 订单(无 TRANSFER 需求共存)——四个判定字段逐字不变,有单测
八、测试环境已验证
- 代码合入:PR #8062,squash 提交
bf4fba5a2357cf6a137105e6b175b48446ef0cc0。已核实该提交是origin/dev-v3的祖先(git merge-base --is-ancestor bf4fba5a2 origin/dev-v3为真);本地 worktree HEAD 当时落后于它,不影响该判断。 - 部署状态:fleet 已部署 TEST,
deploy-status.sh复核0/N(本条由任务发起方在部署机上核实)。 - 网关实测(2026-09-20 22:0x,
api.test.1814.love:9443,TRANSFER-only 订单,同一次候选查询):- 蒙A-U1557(7 座 SUV)→
seatsEnough=true/requirementMatched=true/requirementMismatchReasonCode=null; - 蒙P321A(5 座 SUV,与蒙A-U1557 同车型不同座位)→
seatsEnough=false/requirementMatched=false/requirementMismatchReasonCode=SEATS; - 同一次请求里两辆车判定截然相反,坐实锚点已切换到真实生效的 TRANSFER 需求(修复前这两辆车都会是
requirementMatched=null)。
- 蒙A-U1557(7 座 SUV)→
- 源码层面核实(本文档撰写时直接核对
origin/dev-v3上的实现):AssignmentCandidateService#resolveSlotRequirement已改为调用RequirementKindResolver#resolveForMutationByRequirementId,不再直接调不带 kind 的OrderQueryFacade#getCurrentVehicleRequirementOrNull(orderId);AssignmentCandidateServiceTest新增三条单测覆盖:两类需求并存锚到 TRANSFER、TRANSFER-only 订单锚点可解析、TRAVEL 场景逐字不变且不多发远程读(用例名分别为query_transferRequirementSlot_anchorsOnTransferRequirement、query_transferOnlyOrder_slotAnchorStillResolved、query_travelRequirementSlot_unchangedAndNoTransferProbe)——本次撰写时通过git show直接读取测试源码确认断言存在,未在本会话内重新执行该测试类。
- 说明:本文档撰写者(变更通知员角色)未在本次会话中持有部署机 SSH 访问权限,
deploy-status.sh的0/N结论与网关实测数据表由任务发起方提供并采纳;源码 diff、squash 提交号与origin/dev-v3祖先关系均为本次会话独立核实。
十、相关文档
- 工单 #7990 AC-8(本单)
changelogs-v2/2026-09/20_7990_车务看板订单详情按需求各返一组身份三元组-修改接口-管理后台.md(同一工单 #7990 下另一 AC,PR #8048,讲看板详情接口新增requirementIdentities,与本文档描述的候选面座位/车型判定修复互不重叠)- 工单 #7443(TRAVEL/TRANSFER 需求可并存这一形态的起源)
changelogs-v2/2026-09/20_7443_TRANSFER派车行确认改派基线复核修复-修改接口-管理后台.md(同族缺陷:其它端点"恒取 TRAVEL 当基线"的修复)changelogs-v2/2026-09/20_8004_共用关系两个读口契约收口拆出shareEligible-修改接口-管理后台.md(同一候选查询端点近期的另一处字段变更,互不重叠)
关联 / 联系人
链接
联系人
- 后端负责人: @wx