文件
hl-api-changelog/changelogs-v2/2026-09/20_7990_候选资源槽位需求锚点按requirementId反查修复-修改接口-管理后台.md
T
2026-09-20 20:08:06 +08:00

25 KiB
原始文件 Blame 文件历史

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 脚本、无索引变更。

六、边界行为

  1. TRAVEL 分支优先且短路:先比对 TRAVEL 的当前需求 id,命中即返回,不会为 TRAVEL 派车多打一次 TRANSFER 的远程读(单测 query_travelRequirementSlot_unchangedAndNoTransferProbe 钉住这一点)。
  2. 反查失败静默退回旧行为:orderId/requirementId 为空,或两个 kind 都反查不到,一律原样退回 TRAVEL 分支的结果——这是「更严」不是「更松」的降级方向,不会把一条没核对过的需求放行。
  3. fleetItemIndex 越界或需求 fleet[] 为空:仍返回 SlotRequirement.empty()(既有行为未变),四个判定字段回退到「仅提示不限制」语义。
  4. 换版保留行/未派车占位行:直接读派单行自身快照,不进入本次改动涉及的反查分支(既有行为未变)。
  5. 该反查方法本是为写路径设计(方法名带 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 预校验冲突端点——未涉及本次改动路径,未变。
    • 派单创建/改派/取消/撤销取消等所有写接口——本次只改了只读候选查询里的一处需求反查,不改任何写口逻辑。
    • 请求参数:无新增/删除/改名,无新增必填项。
    • 数据库:无写入、无迁移、无索引变更。
    • 小程序端——不涉及。

八、测试环境已验证

  • 代码合入: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)。
  • 源码层面核实(本文档撰写时直接核对 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