文件
hl-api-changelog/changelogs-v2/2026-09/21_7973_共用关系确认新增跨常驻车确认入参-修改接口-管理后台.md
T

33 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 7973 共用关系确认新增跨常驻车派单确认入参 confirmCrossResident(补记,此前无任何交接件) admin wx(GIT) 修改接口 deployed verified not_required gateway_status=verified 的判据(2026-09-21 10:09:15 / 10:09:17 取证,原文见第八节):经网关对 POST .../share-groups 发起两次真实调用,两次请求体逐字相同、唯一差异是 confirmCrossResident 字段——不传得 605036(消息点名的车牌与司机姓名与夹具构造的跨常驻组合一致),带 true 得 200 并建出 ACTIVE 共用关系,取证完毕已按 survivorPolicy=RELEASE 清场。部署点判据:hl-fleet-service 测试服部署于 51571c58a,git merge-base --is-ancestor 076654889 51571c58a = true。⚠️ 部署点是时点读数——2026-09-21 复核时 origin/dev-v3 已前进到 79722aef1,但 51571c58a..origin/dev-v3 在 hl-fleet-service/ 下只有 a948c1b60(#7994,GroupDispatchService 空组码行收编判定)一条,与本篇 605036 链路零交集,故该读数不因部署点落后而失效。此前两轮保持 pending 的原因是「同一个端点被调通」不等于「本篇登记的那个入参被走到」;本轮已造出跨常驻场景直接触发 605036 分支,该顾虑解除,状态位是取证换来的、不是为过门禁改的。本篇是补记:#7973/#7978 合入以来 docs/ 与 hl-workflow/ 下 grep 7973/7978 零命中,此前没有任何交接件提过这个新增入参。前端实证 not_required(mmg 2026-09-21):share-groups/shareGroup 前端 src 全仓零命中——共用关系确认端点无调用方,属 #7444 未接入挂起域,confirmCrossResident 新入参当前无接入点;605036 全仓零命中,看板派单路径走 precheck/candidates 证据链不解析消息原文,消息模板变详细零影响;看板既有 confirmCrossResident 是 assignments create/change 派单写路径的同名字段,该路径契约本次零变更。随 #7444 接入共用关系确认 UI 时按本文实现:precheck 逐成员探路(cross_resident warning)→二次确认弹窗→带 confirmCrossResident=true 原样重发;605036 文案须补「车辆-司机常驻组合冲突」解释不直接弹原文。frontend_owner/verified_at 系后端预填,按 not_required 门禁口径(ba87a9b)清空。 2026-09-21 dev-v3

fleet: 共用关系确认新增跨常驻车派单确认入参(工单 #7973,补记)

存放目录: 二期(order-v3 标签工单)→ changelogs-v2/2026-09/

服务: hl-fleet-service(8087) PR: #7978 | Issue: #7973 | 合并提交: 076654889 日期: 2026-09-20(补记;实际合入日为 2026-09-19) 影响范围: 管理后台「团期配车页」共用关系确认(POST .../share-groups),跨常驻车/司机场景下的确认交互


⚠️ 关键变化

🔴 这是一份补记 changelog:#7973/#7978 已于 2026-09-19 16:13 合入 dev-v3,但截至本文撰写(2026-09-20),docs/ 与 hl-workflow/ 全仓 grep 7973/7978 零命中——没有任何交接件提过这个变化。mmg 大概率不知道 POST .../share-groups 已经新增了一个请求字段 confirmCrossResident,很可能仍按"该端点入参只有 serviceDate/resourceType/resourceId/members/costBearer/costBearerOrderId/remark 七项"在对接,一旦线上撞见跨常驻场景,会直接吃 605036 且不知道该传哪个字段重试。

🔴 错误文案与页面操作错位,前端必须显式处理,不能指望用户读懂原始错误码:车务在团期配车页上选的是"一辆共用车",但 605036 的消息说的是"司机与车辆不是常驻组合"——被判定跨常驻的司机不是车务选的,是系统按该成员原派单自动带过去的。如果前端把 605036 当成普通业务报错直接弹 message 原文,运营会去查司机资料,而真正需要处理的动作是"确认跨常驻车派单"。详见「四、契约约束与正确调用方式」。

在此之前(confirmCrossResident 字段出现之前),确认端点没有任何入参能表达这个确认——遇到跨常驻场景的共用关系,字面意义上永远建不出来,只能报 605036 死循环。本次修复解锁了这条此前完全不可达的成功路径。


一、背景

POST /admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups(团期车辆共用关系确认,#7444 落地)第 3 步会把尚未占用的成员改派到共用资源上,走的是既有派单写路径 AssignmentService#change。这条写路径上有一道提示型守卫(605036,源码 AssignmentResidentPolicy,早于 #7973 就存在:#4936 引入、#5160 扩展,经 git log --follow AssignmentResidentPolicy.java 核实):目标车已设常驻司机、或被派入的司机本身是别的车的常驻司机时,必须由人显式确认才放行。

问题是:#7973 之前,共用关系确认端点自身完全没有能表达"我确认"这件事的入参。车务在页面上选好共用车、勾好成员,一提交撞上跨常驻就报 605036,而没有任何字段可以带着"确认过了"再重试——这类共用关系客观上建不出来,唯一的绕法是联系后端手工处理。

#7973 缺陷一在 ShareGroupConfirmReqVO 上补了 confirmCrossResident 这个可选字段,原样透传到 change 命令,并把 605036 的消息模板从一句不点名的抽象提示,改成点名"哪条派单 / 跨常驻的具体形态 / 车牌 / 司机姓名"的详细提示。

维度 改前 改后
共用关系确认遇到跨常驻场景 无字段可确认,永远 605036,字面意义上建不出这条关系 可传 confirmCrossResident=true 重试放行
605036 错误消息 "司机与车辆不是常驻组合,请确认跨常驻车派单后重试"(无占位符,不点名任何具体对象) "司机与车辆不是常驻组合,请确认跨常驻车派单后重试({0}:{1};车辆 {2};司机 {3})"(点名触发的派单、跨常驻形态、车牌、司机姓名)
发生频率参考 - 据另一次测试服 DB 核查(本次交接未独立复核,见文末核对记录):fleet_vehicle 123 辆中 34 辆设了 primary_driver_id(27.6%),fleet_driver 116 人中 31 人是某辆车的常驻司机(26.7%)——不是高频,但也绝不罕见,值得做成显式二次确认而不是静默重试或忽略

二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 确认团期车辆共用关系 POST /admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups 修改接口 请求体新增可选字段 confirmCrossResident;605036 错误消息模板新增 4 个占位符

三、接口详情

1. 确认团期车辆共用关系 POST /admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups

VO: ShareGroupConfirmReqVO → ShareGroupRespVO

(源码核对:ShareGroupConfirmReqVO.java:59-83、GroupDispatchShareController.java:53-93、GroupDispatchShareAdmissionService.java:508-569、AssignmentService.java:3198,5656,6316-6324、AssignmentErrorCode.java:181-184、AssignmentResidentPolicy.java)

使用场景

车务在团期配车页确认共用关系时调用,端点本身的用途与 #7444/#8013 changelog 描述一致。本次改动是"提交后可能撞 605036,撞了之后怎么办"这一条分支:目标共用资源(车或司机)与成员当前实际占用的另一方存在"常驻绑定但不是同一组合"关系时,首次提交会被拒绝;车务确认后,前端需要带着 confirmCrossResident=true 原样重发同一份请求(不是换个端点,不是改动其他字段)。

入参字段表

字段 位置 类型 必填 约束 说明
groupBatchId Path Long ✅ - 团期主订单 ID
serviceDate / resourceType / resourceId / members / costBearer / costBearerOrderId / remark Body - - 与 #7444/#8013 changelog 描述完全一致 本次未改动
confirmCrossResident Body Boolean ❌ 不传或 false = 不确认;true = 确认 【本次新增】 跨常驻车派单的人工确认。成员当前实际司机与本共用资源不是常驻组合时必须传 true,否则该成员派入时被 605036 拒;不跨常驻时本字段取任何值都不影响结果

出参字段表

字段 类型 说明
shareGroupId / groupBatchId / serviceDate / resourceType / resourceId / status / costBearer / costBearerOrderId / costSourceRefNo / members[] / confirmedBy / confirmedAt / version / history - 与 #7444/#8013 changelog 描述完全一致,本次零变更

⚠️ 本次改动完全不影响响应体字段——confirmCrossResident 是纯粹的"放行判据",成功时响应体里看不出这次是不是带着确认才通过的。

请求示例

首次提交(未带确认,命中跨常驻):

{
  "serviceDate": "2026-09-12",
  "resourceType": "VEHICLE",
  "resourceId": 300,
  "members": [
    { "sourceType": "GROUP_DISPATCH", "sourceId": 99001 },
    { "sourceType": "ASSIGNMENT", "sourceId": 500, "admissionIntent": "PENDING_ADMISSION" }
  ],
  "costBearer": "GROUP"
}

车务确认后原样重发(路径参数、members、costBearer 等逐字不变,只加一个字段):

{
  "serviceDate": "2026-09-12",
  "resourceType": "VEHICLE",
  "resourceId": 300,
  "members": [
    { "sourceType": "GROUP_DISPATCH", "sourceId": 99001 },
    { "sourceType": "ASSIGNMENT", "sourceId": 500, "admissionIntent": "PENDING_ADMISSION" }
  ],
  "costBearer": "GROUP",
  "confirmCrossResident": true
}

(路径参数 groupBatchId=8801)

响应示例

带确认重试成功后,响应体与 #7444 changelog 描述的成功响应逐字节一致(confirmCrossResident 不出现在响应里):

{
  "code": 200,
  "message": "成功",
  "data": {
    "shareGroupId": "77001",
    "groupBatchId": "8801",
    "serviceDate": "2026-09-12",
    "resourceType": "VEHICLE",
    "resourceId": "300",
    "status": "ACTIVE",
    "costBearer": "GROUP",
    "costBearerOrderId": null,
    "costSourceRefNo": "SHARE-77001",
    "members": [
      { "sourceType": "GROUP_DISPATCH", "sourceId": "99001", "requirementId": null, "orderId": null },
      { "sourceType": "ASSIGNMENT", "sourceId": "500", "requirementId": "5501", "orderId": "70123" }
    ],
    "confirmedBy": "1001",
    "confirmedAt": "2026-09-12 18:20:33",
    "version": 1
  },
  "success": true
}

空数据 / 降级响应

本接口是同步写操作,不存在空数据形态;本次改动不引入新的降级路径。

错误响应

605036 跨常驻车派单需确认。下面是 2026-09-21 10:09:15 经网关真实调用取得的原始响应(场景构造与放行对照见第八节):

{
  "code": 605036,
  "message": "司机与车辆不是常驻组合,请确认跨常驻车派单后重试(派单 2101855940422836225:所选车辆与司机均已有其他常驻绑定,继续操作将形成跨常驻车派单;车辆 蒙P303A;司机 宝音德力格尔)",
  "data": null,
  "success": false
}

消息模板(源码 AssignmentErrorCode.java:181-184):

司机与车辆不是常驻组合,请确认跨常驻车派单后重试({0}:{1};车辆 {2};司机 {3})
占位符 含义 取值
{0} 触发方——哪条写路径撞上守卫 改派走 派单 {assignmentId}(源码 AssignmentService.java:5656);新建派单走 新建派单(:3198)
{1} 跨常驻的具体形态 3 种取值,见下表
{2} 车辆 车牌号,如 蒙P303A
{3} 司机 司机姓名,如 宝音德力格尔

🔴 {1} 一共 3 种取值(源码 AssignmentResidentPolicy#messageOf,逐字照抄):

触发条件 {1} 文案
车已有别的常驻司机 且 司机是别的车的常驻司机 所选车辆与司机均已有其他常驻绑定,继续操作将形成跨常驻车派单
仅「车已有别的常驻司机」 所选车辆已有其他常驻司机,继续操作将形成跨常驻车派单
仅「司机是别的车的常驻司机」 所选司机已有其他常驻车辆,继续操作将形成跨常驻车派单

⚠️ 三条文案都以「,继续操作将形成跨常驻车派单」结尾。前端若要靠文案区分分支,请匹配前缀、不要匹配全串;更稳的做法是把 message 原文直接展示给车务,不做解析——这条消息本来就是写给人看的。

业务边界

  • 鉴权、幂等窗口、其余校验规则均与 #7444/#8013 changelog 描述一致,本次未改动。
  • 🔴 confirmCrossResident 刻意不纳入 idempotentKey()(源码 ShareGroupConfirmReqVO.idempotentKey() 与 #7973 javadoc 明确说明理由):它不是业务身份的一部分——同一份成员全集确认两次,确认与否不改变"确认了哪些成员共用哪辆车"这个结果;而被 605036 拒掉的那一次是业务失败,@Idempotent 切面在失败路径上会释放 10 秒防重键。⇒ 拿到 605036 后带 confirmCrossResident=true 可以立刻重试,不会撞"请勿重复提交"防重窗口,前端不需要等待、也不需要提示"请稍后再试"。
  • 服务端刻意不写死 true(源码 javadoc 原话):那会让这条路径静默地自动确认所有跨常驻派单,而守卫存在的意义恰恰是让车务看见"你正在把车派离它的常驻司机",写死等于把这道守卫在共用关系这条路径上永久关掉,且调用方不会知道自己确认过什么。前端不应该在检测到跨常驻风险后自动带 true 重试而不经用户确认。
  • 不跨常驻时本字段取任何值都不影响结果——前端可以不管这个字段,只在拿到 605036 之后才需要处理它,不必在每次提交时都预先判断要不要带它。
  • 同一次确认可能派入多个成员,605036 只点名第一个撞上守卫的成员/派单:admitPendingMembers(GroupDispatchShareAdmissionService.java:534-569)在循环里逐个成员调 change,一旦某个成员触发 605036 整个事务立刻回滚(含此前已经处理过的其他成员),前端不能假设"确认后重试就一定全部通过"——如果不同成员触发的是不同司机/车辆的跨常驻,可能需要多轮"提交→拿到 605036→确认→重试"才能全部放行完。

四、契约约束与正确调用方式

本节只写后端接受/拒绝 payload 的规则,不写与本次改动无关的既有规则(完整规则见 #7444/#8013 changelog)。

🔴 车务在页面上选的是"车",605036 说的是"司机"——两者不是同一个对象

resourceType=VEHICLE 时,admitPendingMembers 把成员改派到共用资源的逻辑是(源码 GroupDispatchShareAdmissionService.java:550-565):

newVehicleId = resourceType == VEHICLE ? resourceId : row.getVehicleId()   // 车务选的共用车
newDriverId  = resourceType == DRIVER  ? resourceId : row.getDriverId()    // 成员原来的司机,不是车务选的

也就是说:车务只选了"这辆车",newDriverId 是系统按该成员原派单上的司机原样带过去的。守卫判的是"这名司机与这辆车是不是常驻组合"——于是"选一辆共用车"这个动作,撞上 605036 时,撞的原因写在一个车务根本没有主动选过的司机身上。resourceType=DRIVER(共用司机)时反过来:车务选的是司机,车是成员原来的车。

⇒ 前端不应该把 605036 的 message 原文直接展示给运营(会让人去查司机资料而不是理解"车辆-司机组合冲突"),至少要在文案上补一句解释这是"车辆-司机常驻组合冲突,不是司机本身有问题"。

跨常驻的两种独立形态(或关系,缺一不可全防)

守卫命中的条件是下列任一条成立即判"跨常驻"(源码 AssignmentResidentPolicy.evaluate):

形态 判定条件 605036 消息片段
车侧越界 目标车辆已设 primary_driver_id,且与被派入成员的当前司机不是同一人 "所选车辆已有其他常驻司机"
司机侧越界 被派入成员的当前司机本身是别的车的常驻司机 "所选司机已有其他常驻车辆"
两者皆是 车侧、司机侧同时越界 "所选车辆与司机均已有其他常驻绑定"

⚠️ 两条是"或"的关系:共用车没设常驻司机(primary_driver_id 为空)只躲开车侧那一条,司机侧照样可能触发——不能靠"挑一辆没设常驻司机的车"来规避这道守卫。

建议的前端交互:先调 precheck 探路,别盲提交吃错误码

POST /admin/fleet/assignments/precheck(#7444 之前就存在,本次未改动其契约)不取锁、不写库、恒成功(源码类注释:"只读、不抛异常、不写、不取锁",@Transactional(readOnly = true)),跨常驻组合会在响应的 warnings[] 里产出一条 type="cross_resident" 的提示项(PrecheckRespVO.WarningItemVO{type, msg}),不影响 conflict/conflicts[](跨常驻是 warning 不是 conflict,不会被判定为阻断)。

🔴 precheck 的调用方式有个坑,Swagger 里一个字都没写:它的入参是"一辆车 + 一名司机"(PrecheckReqVO{vehicleId, driverId, startDate, endDate, ...}),而共用关系确认里司机不是车务选的(见上文)。因此正确调法是:

  • resourceType=VEHICLE(共用车):对每个待派入成员分别调一次 precheck,入参 vehicleId=共用车 ID、driverId=该成员当前司机 ID
  • resourceType=DRIVER(共用司机):反过来,vehicleId=该成员当前车辆 ID、driverId=目标共用司机 ID

成员当前司机/车辆 ID 前端可以从既有只读端点拿到,不需要额外新接口:

  • GET /admin/fleet/group-dispatch/batches/{groupBatchId}/overview → GroupDispatchOverviewVehicleVO.driverId
  • GET /admin/fleet/group-dispatch/resource-schedule → ResourceScheduleItemVO.driverId

逐成员探完一遍 precheck 后,若任一成员命中 cross_resident warning,就在提交前弹出"存在跨常驻车派单,是否确认?"的二次确认框,用户确认后提交时才带 confirmCrossResident=true——这样可以避免车务盲提交后才第一次看到 605036。

✅ 正确 / ❌ 错误 payload 对照

场景 payload 结果
✅ 不跨常驻 不传 confirmCrossResident 200,正常放行(本字段不影响结果)
✅ 跨常驻,首次提交未确认 不传或 confirmCrossResident=false 605036,fleet_assignment 零写入,整个事务回滚
✅ 跨常驻,车务已确认 confirmCrossResident=true,其余字段与首次提交逐字相同 200,放行;10 秒防重键已在失败路径释放,无需等待
❌ 期望服务端自动确认(不传字段,指望后端默认放行) 同"未确认"payload 无效——服务端刻意不写死 true,不传就是不确认,会持续收到 605036
❌ 收到 605036 后改了其他字段(比如换了 resourceId)才重试 与首次不同的请求体 这是另一次业务操作,不是"确认重试"——是否命中跨常驻需要重新判定,不能假设加了 confirmCrossResident=true 就万能放行

五、数据库行为

confirmCrossResident 不落库、不新增任何列——它是运行时透传给 AssignmentService#change 命令的确认标志,仅用于守卫放行判断,本身不持久化。

场景 改前 改后
跨常驻且未确认 change() 内部即时抛 605036,fleet_assignment 零写入,confirmInTransaction 整个事务回滚 同左,行为不变——差异仅在错误消息文案更详细
跨常驻且 confirmCrossResident=true 不存在这个入参,无法达成——永远停在上一行的 605036,fleet_assignment 永远零写入 守卫放行,fleet_assignment 走既有"旧切片转 canceled + 新行 INSERT"逻辑正常写入(该写入逻辑本身不是本次改动,#7444 起就有)
非跨常驻的普通共用确认 不受影响 不受影响

六、边界行为

  • 未登录/网关未透传角色 → 401(网关拦截),既有行为
  • 其余既有错误码(602100~602112 段)均未变动
  • 老数据兼容:本次改动不涉及任何存量数据结构,纯粹是入参层的新增可选字段 + 错误消息模板调整

六.5、枚举 / 数据字典

confirmCrossResident(ShareGroupConfirmReqVO.confirmCrossResident)

所属字段: confirmCrossResident | 类型: Boolean(可空)

值 说明
null / 不传 不确认(默认态,与传 false 行为完全一致)
false 显式不确认,行为与不传一致
true 确认跨常驻车派单,撞上守卫时放行

605036 消息占位符(AssignmentErrorCode.CROSS_RESIDENT_CONFIRMATION_REQUIRED)

占位符 含义 取值示例
{0} 触发方描述 派单 500(改派场景)/ 新建派单(新建场景,共用关系确认走的是改派场景)
{1} 跨常驻的具体形态 所选车辆已有其他常驻司机 / 所选司机已有其他常驻车辆 / 所选车辆与司机均已有其他常驻绑定
{2} 车牌 蒙B-30000(缺车牌时退回 ID={vehicleId})
{3} 司机姓名 李师傅(缺姓名时退回 ID={driverId})

warnings[].type(PrecheckRespVO.WarningItemVO.type,precheck 端点既有字段,本次未新增,仅补充说明供前端建议交互使用)

值 说明
cross_resident 跨常驻(本篇涉及的类型)
seats_short 座位不足
vehicle_unavailable / driver_unavailable 车/司机不可用
license_expired 驾照过期
veh_inspect_expired / veh_insure_expired 车辆年检/保险过期

六.6、修改前后对比

字段级对比

字段 改前 改后
ShareGroupConfirmReqVO.confirmCrossResident 不存在 新增,可选 Boolean,默认语义为不确认
605036 (CROSS_RESIDENT_CONFIRMATION_REQUIRED) 消息模板 "司机与车辆不是常驻组合,请确认跨常驻车派单后重试"(静态文本,无占位符) "司机与车辆不是常驻组合,请确认跨常驻车派单后重试({0}:{1};车辆 {2};司机 {3})"(4 个占位符,点名触发方/形态/车牌/司机)

行为级对比

行为 改前 改后
共用关系确认遇到跨常驻场景 无法完成——没有字段可确认,永远 605036,唯一出路是联系后端手工处理 可传 confirmCrossResident=true 二次提交放行,业务上可自助完成
605036 报错时车务能否判断该怎么办 不能——消息不点名是哪条派单/哪种越界/哪辆车/哪个人 能——消息里逐一点名,且 create/change 两条路径同一份口径
带确认重试是否会撞 10 秒防重窗口 N/A(无法重试) 不会——confirmCrossResident 不纳入 idempotentKey(),且失败路径释放防重键

六.7、影响评估

  • 是否破坏向后兼容: 否——新增的是可选字段,旧前端不传该字段时行为与改动前逐字节一致(跨常驻场景下依旧收到 605036,只是消息文案变详细了)
  • 前端是否必须同步上线: 是——跨常驻场景此前是纯粹的死路(无法建出这类共用关系),本次解锁了一条此前完全不可达的成功路径;前端不接入就意味着车务在这类场景下永远卡在 605036、无法完成共用关系确认,运营会持续遇到"报错但不知道怎么处理"的问题。若前端此前对 605036 的 message 做过精确匹配/正则解析(不太可能但不能排除),需要检查是否受消息模板变化影响
  • 前端 workaround 清理点: 若此前车务遇到跨常驻场景的 workaround 是联系后端手工处理(比如手工改库建关系),本次上线后应当撤除该 workaround,改走"precheck 探测 → 二次确认弹窗 → 带 confirmCrossResident=true 提交"的正常流程

七、不影响范围

  • 仅影响: POST .../share-groups 请求体新增字段 confirmCrossResident,以及 605036 错误消息的文案(消息模板变化,错误码本身不变)
  • 零影响:
    • 请求体其余字段(serviceDate/resourceType/resourceId/members/costBearer/costBearerOrderId/remark)零变更
    • 响应体 ShareGroupRespVO 零变更,confirmCrossResident 不出现在任何响应里
    • GET .../share-groups(查询)、DELETE .../share-groups/{shareGroupId}(解除)两个端点零改动——本次 PR 确实触及 GroupDispatchShareController,但那 14 行全部落在 confirm 端点的接口文档注释里(@ApiOperation 的 notes),GET/DELETE 的方法签名与请求/响应契约逐字未动
    • precheck/candidates 两个读口的请求/响应契约本次未改动——cross_resident 这个 warnings[].type 值早在 #7973 之前就存在(AssignmentResidentPolicy 由 #4936 引入、#5160 扩展),不是本次新增
    • 非共用关系场景下的普通派车/改派(create/change 两个端点自身的请求/响应契约)零变更,跨常驻守卫判定逻辑本身也没变,只是错误消息更详细
    • 无新增 DB 表/列/索引;confirmCrossResident 不落库
    • #8013(共用关系确认历史)、#7988(团期配车刷新观测口)两篇的改动内容互不影响

八、测试环境已验证

环境:测试服;hl-fleet-service 部署点 51571c58a(该提交包含引入 confirmCrossResident 的 076654889,2026-09-19,已用 git merge-base --is-ancestor 核过祖先关系)。 取证时刻:2026-09-21 10:09:15 / 10:09:17 / 10:10:22(下列三次调用的实际发生时刻)。 取证方式:经网关真实调用,非单元测试、非 Mock。原始请求/响应全文见执行台账(会话内保留)。

8.1 场景构造

为得到「成员司机 ≠ 目标车辆常驻司机」这一前置,按以下顺序造数(全部为新建,未复用任何存量数据):

  1. 新建团期批次 groupBatchId=2101855487131877378(服务日 2026-10-16);
  2. 该团期下建 2 个子订单 A / B,各自提交用车需求(服务日 2026-10-16 ~ 10-18);
  3. 子订单 A 派单至 车 蒙A-T7777 + 司机 宝音德力格尔(D1),子订单 B 派单至 车 蒙P304A + 司机 P3测试司机04;
  4. 取第三辆车 蒙P303A(vehicleId=2089686351917039618) 作为共用目标车,其常驻司机为 2089686350138630146,与 D1 不同 —— 跨常驻条件成立。

8.2 未传 confirmCrossResident → 拦截(605036)

POST /admin/fleet/group-dispatch/batches/2101855487131877378/share-groups,请求体不含 confirmCrossResident 字段:

{
  "serviceDate": "2026-10-16",
  "resourceType": "VEHICLE",
  "resourceId": 2089686351917039618,
  "members": [
    { "sourceType": "ASSIGNMENT", "sourceId": 2101855940422836225 },
    { "sourceType": "ASSIGNMENT", "sourceId": 2101856068395245569 }
  ],
  "costBearer": "GROUP",
  "remark": "cross-resident probe"
}

响应(HTTP 200,业务码 605036):

{
  "code": 605036,
  "message": "司机与车辆不是常驻组合,请确认跨常驻车派单后重试(派单 2101855940422836225:所选车辆与司机均已有其他常驻绑定,继续操作将形成跨常驻车派单;车辆 蒙P303A;司机 宝音德力格尔)",
  "data": null,
  "success": false
}

报文中点名的「车辆 蒙P303A;司机 宝音德力格尔」与 8.1 构造的跨常驻组合一致,可据此确认拦截确实由该组合触发,而非其他前置校验。

8.3 传 confirmCrossResident: true → 放行(200)

同一请求体追加 "confirmCrossResident": true,其余字段逐字不变:

{
  "code": 200,
  "message": "成功",
  "data": {
    "shareGroupId": "360246074662850560",
    "groupBatchId": "2101855487131877378",
    "serviceDate": "2026-10-16",
    "resourceType": "VEHICLE",
    "resourceId": "2089686351917039618",
    "status": "ACTIVE",
    "costBearer": "GROUP",
    "costSourceRefNo": "SHARE-360246074662850560",
    "members": [
      { "sourceType": "ASSIGNMENT", "sourceId": "360246074935480320",
        "requirementId": "2101855514109640706", "orderId": "2101855487085740033" },
      { "sourceType": "ASSIGNMENT", "sourceId": "360246075224887296",
        "requirementId": "2101855521428647938", "orderId": "2101855500247465986" }
    ],
    "confirmedBy": "2101000047826030594",
    "confirmedAt": "2026-09-21 10:09:17",
    "version": 0
  },
  "success": true
}

两次调用唯一的差异就是这一个字段——同一批 members、同一 resourceId、同一 serviceDate,前者 605036、后者 200。

8.4 副作用核验(独立口径)

响应里 members[].sourceId 已由原派单 id 变为改派后的新派单 id(360246074935480320 / 360246075224887296)。另经两条互相独立的读口复核:

  • POST /admin/fleet/assignments/candidates 的 canonicalSnapshot.cells:2026-10-16 当天的 cell vehicleId 已变为 2089686351917039618(目标共用车),driverId 仍为 D1 —— 本端点只改车、不改司机,与三节接口说明一致;
  • 直查 fleet_assignment:上述两个新派单 id 的 vehicle_id 均为 2089686351917039618、service_date 为 2026-10-16,与网关响应逐字吻合。

8.5 现场清理

DELETE /admin/fleet/group-dispatch/share-groups/360246074662850560?survivorPolicy=RELEASE(2026-09-21 10:10:22):

{
  "code": 200, "message": "成功",
  "data": {
    "shareGroupId": "360246074662850560",
    "status": "RELEASED",
    "survivorPolicy": "RELEASE",
    "keptSourceIds": [],
    "releasedSourceIds": ["360246074935480320", "360246075224887296"],
    "pendingReassignSourceIds": []
  },
  "success": true
}

8.6 随 PR #7978 合入的单测(本轮未重新执行)

⚠️ 下列是代码仓内的断言意图,不是本轮的执行结果;本篇的实测证据是 8.2–8.5。 之所以仍然列出,是因为其中两条覆盖了网关取证没有覆盖到的分支(已标 ⭐):

AssignmentServiceTest(新增 2 条)
  change_crossResidentByDriverBoundToAnotherVehicle_throws605036
    → 只有「司机是别的车的常驻司机」这一侧成立时也抛 605036(不需要车侧同时成立);
       并断言 assignmentMapper 的 cancelActiveRowsByIds / insert 均未被调用
  change_crossResidentByDriverBoundToAnotherVehicle_confirmed_succeeds
    → 同场景带 confirmCrossResident=true 放行;
       ⭐ 断言 vehicleService.rebindResidentVehicleFromDriverSide 未被调用,
          即「确认跨常驻」不会顺手改写司机的常驻绑定(网关取证未覆盖此点)

GroupDispatchShareAdmissionServiceTest(新增 4 条)
  confirm_confirmCrossResidentTrue_isPassedThroughToChange
    → true 原样透传到 change 命令
  confirm_confirmCrossResidentFalse_isPassedThroughAsFalse
    → ⭐ 显式传 false 时原样透传、不会被悄悄改写成 true
       (网关取证只验了「不传」与「传 true」两种,没验显式 false)
  confirm_crossResidentWithoutConfirmation_605036PropagatesOut
    → 605036 原样抛出,不被本层吞掉、也不被翻译成 602xxx
  confirm_crossResidentConfirmed_passesTheSameGuard
    → 与上一条同一夹具,仅多带一个 true 即放行

十、相关文档

  • 关联 Issue: wx/HL#7973
  • 关联 PR: wx/HL#7978(squash 合并至 dev-v3 @076654889)
  • 共用关系确认端点完整契约见 #7444 changelog(changelogs-v2/2026-09/21_7444_团期配车就绪门禁与车辆共用关系-修改接口-管理后台.md,由团期车务会话维护与推送)
  • 共用关系确认历史的另一处补丁见 #8013 changelog(changelogs-v2/2026-09/20_8013_共用关系确认历史补记成本承担方变更-修改接口-管理后台.md)

关联 / 联系人

链接

联系人

  • 后端负责人: wx(GIT)
  • 前端负责人: mmg