diff --git a/changelogs-v2/2026-09/21_7973_共用关系确认新增跨常驻车确认入参-修改接口-管理后台.md b/changelogs-v2/2026-09/21_7973_共用关系确认新增跨常驻车确认入参-修改接口-管理后台.md new file mode 100644 index 00000000..1fd7fcb3 --- /dev/null +++ b/changelogs-v2/2026-09/21_7973_共用关系确认新增跨常驻车确认入参-修改接口-管理后台.md @@ -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