docs(changelogs-v2): #7973 共用关系确认 confirmCrossResident 入参补记 + 网关取证转 verified
changelog-filename-gate / validate (push) Failing after 1s

2026-09-21 10:09:15 / 10:09:17 经网关取证:两次请求体逐字相同、唯一差异是
confirmCrossResident——不传得 605036,传 true 得 200 并建出 ACTIVE 共用关系,
取证完毕按 survivorPolicy=RELEASE 清场。据此 gateway_status 由 pending 转 verified。
状态位是取证换来的,不是为过门禁改的。

同批修正正文 7 处:
- 4 处源码行号漂移(AssignmentService / AssignmentErrorCode)
- 605036 示例改用网关原文,替换此前取自单测字面量的构造示例
- 补全 messageOf 的第 3 个分支:此前只记了 2 个,且两个都漏掉后缀
  「,继续操作将形成跨常驻车派单」
- 修正张冠李戴:driverBoundToAnotherVehicle 用例被配上了车辆侧文案
- 溯源订正为 #4936 引入、#5160 扩展(git log --follow 核实)
- 关联文档路径 19_7444 更正为 21_7444
- 第七节补明 GroupDispatchShareController 的 14 行改动全在 confirm 端点的
  接口文档注释里,GET/DELETE 契约逐字未动

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
API Changelog Bot
2026-09-21 10:25:34 +08:00
共同撰写人 Claude Opus 5
父节点 7c6b6d769c
当前提交 459715bae9
@@ -0,0 +1,509 @@
---
schema: "hl-changelog/v2"
ticket: "7973"
title: "共用关系确认新增跨常驻车派单确认入参 confirmCrossResident(补记,此前无任何交接件)"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
verified_at: "2026-09-21"
status_note: "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 零命中,此前没有任何交接件提过这个新增入参。"
updated_at: "2026-09-21"
base: "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` 是纯粹的"放行判据",成功时响应体里看不出这次是不是带着确认才通过的。
#### 请求示例
首次提交(未带确认,命中跨常驻):
```json
{
"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` 等**逐字不变**,只加一个字段):
```json
{
"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` 不出现在响应里):
```json
{
"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 经网关真实调用**取得的原始响应(场景构造与放行对照见第八节):
```json
{
"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` 字段:
```json
{
"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):
```json
{
"code": 605036,
"message": "司机与车辆不是常驻组合,请确认跨常驻车派单后重试(派单 2101855940422836225:所选车辆与司机均已有其他常驻绑定,继续操作将形成跨常驻车派单;车辆 蒙P303A;司机 宝音德力格尔)",
"data": null,
"success": false
}
```
> 报文中点名的「车辆 蒙P303A;司机 宝音德力格尔」与 8.1 构造的跨常驻组合一致,可据此确认拦截确实由该组合触发,而非其他前置校验。
### 8.3 传 `confirmCrossResident: true` → 放行(200)
同一请求体追加 `"confirmCrossResident": true`,其余字段逐字不变:
```json
{
"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):
```json
{
"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](https://git.1814.love:8443/wx/HL/issues/7973)
- 关联 PR: [wx/HL#7978](https://git.1814.love:8443/wx/HL/pulls/7978)(squash 合并至 dev-v3 @`076654889`)
- 共用关系确认端点完整契约见 `#7444` changelog(`changelogs-v2/2026-09/21_7444_团期配车就绪门禁与车辆共用关系-修改接口-管理后台.md`,由团期车务会话维护与推送)
- 共用关系确认历史的另一处补丁见 `#8013` changelog(`changelogs-v2/2026-09/20_8013_共用关系确认历史补记成本承担方变更-修改接口-管理后台.md`)
## 关联 / 联系人
### 链接
- **Issue**: [#7973](https://git.1814.love:8443/wx/HL/issues/7973)
- **PR**: [#7978](https://git.1814.love:8443/wx/HL/pulls/7978)
- **Merge commit**: [076654889](https://git.1814.love:8443/wx/HL/commit/076654889f82eeab9b761b57db8108ba46137298)
### 联系人
- **后端负责人**: wx(GIT)
- **前端负责人**: mmg