docs(7982): 订正共用关系确认端点的契约段落——首版字段名与错误码文案有误
changelog-filename-gate / validate (push) Failing after 1s
changelog-filename-gate / validate (push) Failing after 1s
首版(3328730)的「接口详情」章把入参 resourceType 写成了不存在的 dimension、 漏掉 serviceDate 与 costBearer 两个必填字段、引用了两个并不存在的 VO 类名 (CreateShareGroupReqVO / ShareGroupCreateRespVO)、响应示例里给出了 VO 上不存在 的 id / createdAt / admissionAt 字段,并把 605001 的提示文案写成了另一段文字 (真实文案是「派单冲突:该车日期段已派」)。照首版的请求示例构造的请求发不出去 ——字段名对不上,且缺两个必填字段。 本版每一个字段、每一条错误码文案均取自 dev-v3 上的源码(VO 的 @ApiModelProperty 声明与 IErrorCode.of 定义),并逐处标注出处文件。同时补上首版缺失的内容:600009 这个前端会先撞上的码、三步校验顺序、605036 跨常驻车派单需确认的前端交互、以及 「防重与幂等是两件事」的区分。 删除首版那张声称来自测试环境实测的读数表:其数据无法溯源到任何一次真实调用 (首版构造请求所用的字段名在服务端并不存在,该请求不可能被受理)。修复行为的 证据来源改为如实标注为源码与真库集成测试 ShareGroupOverlapProjectionIntegrationTest, 并写明测试环境上不存在可供对跑的修复前环境(修复 2026-09-19 已合入)。 gateway_status=verified 的依据同步换成 2026-09-22 经网关的实调读数(HTTP 200 + 业务码 600009,配不存在路径的阴性对照 code=404,两者可分辨),并在 status_note 与正文里都写明它的覆盖边界:只验到端点可达性与契约反序列化,未验证修复行为本身。 Refs #7982 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
@@ -1,7 +1,7 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7982"
|
||||
title: "fleet: 创建共用关系时同批准入多个新成员不再误报冲突码 605001(行为修复,无接口字段变化)"
|
||||
title: "fleet: 确认共用关系时同批准入多个新成员不再误报冲突码 605001(行为修复,无接口字段变化)"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修复"
|
||||
@@ -12,54 +12,58 @@ frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "backend_status=deployed:修复 PR #7983(`e7cecb6d7`)+ 测试补强 PR #8096(`970f9a187`)已合入 dev-v3;fleet 测试服部署点 `bd19b79c8`(2026-09-22 02:50),`git merge-base --is-ancestor e7cecb6d7 bd19b79c8` 与 `970f9a187 bd19b79c8` **均为真**。gateway_status=verified:2026-09-22 02:50 测试服真实调用实测,POST /admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups 传入 3 个成员(1 个 GROUP_DISPATCH/OCCUPYING + 2 个 ASSIGNMENT/PENDING_ADMISSION)、2 个待准入分属海拉尔东山机场与满洲里西郊机场 → HTTP 200、`success=true`、`status=ACTIVE`、响应 members 数组长度 3;服务端 DB 三成员行齐全、状态一致。frontend_status=not_required:接口形状(路径、入参、响应字段、错误码)**完全未变**,本次仅修复冲突判定的内部投影缺列。"
|
||||
status_note: "backend_status=deployed:修复 PR #7983(`e7cecb6d7`)+ 测试补强 PR #8096(`970f9a187`)已合入 dev-v3;fleet 测试服部署点 `bd19b79c8`,`git merge-base --is-ancestor e7cecb6d7 bd19b79c8` 与 `970f9a187 bd19b79c8` **均为真**(2026-09-22 逐条复核)。gateway_status=verified:2026-09-22 经网关实调该端点 `POST https://api.test.1814.love:9443/admin/fleet/group-dispatch/batches/1/share-groups`,HTTP 200 + 业务码 600009(刻意使用不存在的 groupBatchId=1,第一道校验即失败关闭);同轮阴性对照打在不存在的路径上返回 `code=404 接口不存在`,两者可分辨 ⇒ 路由、鉴权(需 VEHICLE_MANAGER 角色)、请求体反序列化三项均通,且全程未触达写库路径。**该验证的覆盖边界**:只验到端点可达性与契约反序列化,**未验证本次修复的行为本身**(同批多个待准入成员不再误报 605001)——那需要真实团期与派单夹具;修复行为的证据来源是源码与真库集成测试 `ShareGroupOverlapProjectionIntegrationTest`。frontend_status=not_required:接口形状(路径、入参、响应字段、错误码)**完全未变**,本次仅修复冲突判定的内部投影缺列。"
|
||||
updated_at: "2026-09-22"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# fleet: 创建共用关系时同批准入多个新成员不再误报冲突码 605001(行为修复,无接口字段变化)
|
||||
# fleet: 确认共用关系时同批准入多个新成员不再误报冲突码 605001(行为修复,无接口字段变化)
|
||||
|
||||
> **存放目录**: 二期(fleet 车务)→ `changelogs-v2/2026-09/`
|
||||
|
||||
## 🔴 2026-09-22 订正说明(首版字段写错,请重新核对)
|
||||
|
||||
本文件**首版**(提交 `3328730`)的「接口详情」章把请求/响应契约写错了,具体:把入参 `resourceType` 写成了不存在的 `dimension`、漏掉 `serviceDate` 与 `costBearer` 两个**必填**字段、引用了两个不存在的 VO 类名(`CreateShareGroupReqVO` / `ShareGroupCreateRespVO`)、响应示例里给出了 VO 上不存在的 `id` / `createdAt` / `admissionAt` 字段,并把 605001 的提示文案写成了另一段文字。**照首版的请求示例构造的请求发不出去**(缺必填字段 + 字段名不匹配)。
|
||||
|
||||
**若已照首版写过代码,请按本版重新核对字段名。** 本版每一个字段、每一个错误码文案均取自 `dev-v3` 分支上的源码(VO 的 `@ApiModelProperty` 声明与 `IErrorCode.of(...)` 定义),下文逐处标注了出处文件。
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**接口契约零变化**——路径、方法、请求参数、响应字段、错误码全部不动。
|
||||
|
||||
**变的是冲突判定的投影**。从本次修复上线起:
|
||||
|
||||
1. 创建共用关系时,**同一个 POST 请求里传入 2 个或更多「待准入」成员**(`members` 里 `admissionIntent=PENDING_ADMISSION` 的 `ASSIGNMENT` 类型项),且这些成员分属**不同城市**的行程(即不同出行中户,资源占用冲突判定的基准单位)时,**从第二个成员开始不再误报错误码 605001**;
|
||||
2. 修复后同一请求返回 HTTP 200,关系状态 `ACTIVE`,全部成员入库。
|
||||
1. 确认共用关系时,**同一个 POST 请求里传入 2 个或更多尚未占用该资源的 `ASSIGNMENT` 成员**、且没有既有共用关系可对标时,**从第二个成员开始不再误报错误码 605001**(`605001 = 派单冲突:该车日期段已派`);
|
||||
2. 修复后同一请求正常受理,全部成员入库成该关系的成员行。
|
||||
|
||||
🔴 **这个误报此前在同批多成员场景下是 100% 必现的**(不是"偶尔会触发",是判定逻辑数据缺列导致每次都做错对端认证)。
|
||||
🔴 **这个误报在上述场景下是必现的,不是偶发**:根因是一处数据投影恒缺列(见下),每次走到这条回退路径都会做错判定。
|
||||
|
||||
## 一、背景
|
||||
|
||||
### 缺陷形态
|
||||
|
||||
创建共用关系时的冲突判定有两层:
|
||||
确认共用关系时的冲突判定有两层:
|
||||
|
||||
1. **第一层**:按既有共用关系逐一检查新增成员是否与它们的成员产生冲突;
|
||||
2. **第二层**(回退):若找不到既有关系可对标,则按**新增成员间的相互关系**判定冲突。
|
||||
2. **第二层(回退)**:若找不到既有关系可对标,则按**新增成员之间的相互关系**判定冲突。
|
||||
|
||||
第一层是正常路径,拿得到全量数据。第二层回退判定在**同批请求内做多成员间相互认对**时,其数据投影缺了**申请人字段**(`applicant_id`),导致对端"这个人有没有被授权共用"这一核心前提始终查不出来。
|
||||
第一层是正常路径,拿得到全量数据。第二层回退靠「**按需求认对端**」——用成员行的 `requirement_id` 去认出另一侧是谁。但组行视图与候选面的数据投影里**没有把 `requirement_id` 选出来**,于是这条回退路径从上线起就恒失效。
|
||||
|
||||
⇒ **在同批多成员且无既有关系的场景下,从第二个成员开始每一个都会命中「无法确认对端身份」这条分支,必定返回 605001。**
|
||||
为什么偏偏卡在这一列:成员正在改绑的那段窗口里,成员表存的还是旧的派车行,**按 ID 查对端必然落空**,`requirement_id` 是那段窗口里唯一还认得出对端的键——而它恰好是被投影丢掉的那一列。
|
||||
|
||||
### 这个判断怎么坐实的
|
||||
⇒ **在同批多成员且无既有关系可对标的场景下,从第二个成员开始每一个都认不出对端,必定返回 605001。**
|
||||
|
||||
修复前后在同一测试环境同一个请求体上对跑:
|
||||
### 这个判断的证据来源
|
||||
|
||||
| 读数 | 修复前 | 修复后 |
|
||||
|---|---|---|
|
||||
| 同批传 3 成员(GROUP_DISPATCH + 2×ASSIGNMENT PENDING_ADMISSION)的 HTTP 状态码 | **605001**(业务码) | **200** |
|
||||
| 响应 `success` 字段 | **false** | **true** |
|
||||
| DB 共用关系 `status` | 未入库 | **ACTIVE** |
|
||||
| DB 成员行数 | 0 | **3** |
|
||||
**证据是源码与真库集成测试(`ShareGroupOverlapProjectionIntegrationTest`),不是测试环境上的前后对跑读数。** 修复已于 2026-09-19 合入 `dev-v3`,测试环境此后一直处于修复后的状态,客观上不存在可供对跑的修复前环境。
|
||||
|
||||
不是推测,是**修复前的请求在修复后一字不改直接过了**。
|
||||
本文件**不提供**任何声称来自测试环境实测的修复前/修复后请求读数。(首版曾给出一张这样的表格,已在本版删除——那张表的数据无法溯源到任何一次真实调用。)
|
||||
|
||||
### 修复
|
||||
|
||||
PR #7983(`e7cecb6d7`):把缺失的 `applicant_id` 字段加入第二层回退判定的数据投影,使其真正能判断"对端是否被授权"。
|
||||
PR #7983(`e7cecb6d7`):把 `requirement_id` 补进组行视图与候选面的投影列清单,让「按需求认对端」的回退真正取得到那一列。提交标题逐字为「组行/候选面投影补 requirement_id——「按需求认对端」的回退从未生效过」。
|
||||
|
||||
测试补强 PR #8096(`970f9a187`):补真库集成测试。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
@@ -69,64 +73,108 @@ PR #7983(`e7cecb6d7`):把缺失的 `applicant_id` 字段加入第二层回
|
||||
|
||||
| METHOD | Path | 变化 |
|
||||
|---|---|---|
|
||||
| POST | `/admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups` | 同批创建多个待准入成员时,冲突判定不再误报 605001;允许创建多成员共用关系一次请求完成 |
|
||||
| POST | `/admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups` | 同批确认多个尚未占用的 `ASSIGNMENT` 成员时,冲突判定不再误报 605001;多成员共用关系可一次请求完成 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 创建团期共用关系 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups`
|
||||
### 1. 确认团期车辆共用关系 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups`
|
||||
|
||||
**VO**: `CreateShareGroupReqVO → ShareGroupCreateRespVO`(**字段无增删改**)
|
||||
**VO**:`ShareGroupConfirmReqVO` → `Result<ShareGroupRespVO>`(**字段无增删改**)
|
||||
|
||||
> 出处:`hl-fleet-service/src/main/java/com/hulalv/fleet/dispatch/controller/GroupDispatchShareController.java`,
|
||||
> `@RequestMapping("/admin/fleet/group-dispatch")` + `@PostMapping("/batches/{groupBatchId}/share-groups")`。
|
||||
> 网关路由复用既有 `- Path=/admin/fleet/**` → `lb://hl-fleet-service`(`hl-gateway/src/main/resources/application.yml`),**本单不新增路由**。
|
||||
> 权限点:写口 `fleet:group-dispatch:write`(读口 `fleet:group-dispatch:view`),本单不新增权限点。
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台「团期配车」页,车务操作「绑定共用」。一次请求可以准入多个派车行成员进入同一个共用关系,这次修复消除了多成员时的冲突误判。
|
||||
管理后台「团期配车」页,车务确认「本团这一个服务日、这一辆车(或这一名司机),由下列几方共用」。
|
||||
|
||||
⚠️ **端点名是「确认」不是「创建」,这不是措辞问题**:`members` 是**成员全集而不是增量**——同一 `(团, 日, 维度, 资源)` 重复提交按新全集覆盖,返回**同一个** `shareGroupId`,不抛错。前端不要按「新增一个成员就调一次」来用。
|
||||
|
||||
#### ⚠️ 这个端点是复合原子操作,不是一次纯粹的关系登记
|
||||
|
||||
服务端在**同一事务**里依次做三件事:先落授权 → 再把尚未占用的成员派进这辆车 → 由准入检查读到刚落的授权而放行 → 最后回读断言收口。中途任一步失败**整体回滚**,不留「关系已建但没占上车」或「占上车但没关系」的中间态。
|
||||
|
||||
之所以不能拆成「先各自派好车、再来建关系」:跨城场景下第二户根本派不进来(占用准入跨城即拒)。
|
||||
|
||||
#### 入参(**本次无变化**)
|
||||
|
||||
> 字段与文案出处:`hl-fleet-service/.../dispatch/vo/ShareGroupConfirmReqVO.java`、`ShareGroupMemberReqVO.java` 的 `@ApiModelProperty`。
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| dimension | String | ✅ | 共用维度:`VEHICLE`(共用车辆)或 `DRIVER`(共用司机) |
|
||||
| resourceId | Long | ✅ | 资源 ID(车辆 id 或司机 id) |
|
||||
| members | List<Member> | ✅ | 成员列表(GROUP_DISPATCH 与 ASSIGNMENT 的混合) |
|
||||
| members[i].sourceType | String | ✅ | 成员来源类型:`GROUP_DISPATCH` 或 `ASSIGNMENT` |
|
||||
| members[i].sourceId | Long | ✅ | 成员来源 ID |
|
||||
| members[i].admissionIntent | String | ✅ | 准入意向:`OCCUPYING`(已占用)或 `PENDING_ADMISSION`(待准入) |
|
||||
| `serviceDate` | String `yyyy-MM-dd` | ✅ | 共用发生的服务日;必须落在该团基线 `serviceDates` 内,窗外抛 **602104** |
|
||||
| `resourceType` | String | ✅ | 资源维度:`VEHICLE` / `DRIVER`;车与司机**分别建关系** |
|
||||
| `resourceId` | Long | ✅ | 车辆 ID 或司机 ID |
|
||||
| `members` | List | ✅ | **成员全集(不是增量)**,2-20 个,越界抛 **602100** / **602101** |
|
||||
| `members[i].sourceType` | String | ✅ | `ASSIGNMENT`=逐户接送派单 / `GROUP_DISPATCH`=团级配车行 |
|
||||
| `members[i].sourceId` | Long | ✅ | `fleet_assignment.assignment_id` 或 `fleet_group_dispatch.dispatch_id` |
|
||||
| `members[i].admissionIntent` | String | ❌ | `OCCUPYING`=已占着这辆车 / `PENDING_ADMISSION`=本次一并派入。**仅供前端交互提示** |
|
||||
| `costBearer` | String | ✅ | 成本承担方:`GROUP`=记团级整车 / `ORDER`=记指定户;缺失或非法抛 **602107** |
|
||||
| `costBearerOrderId` | Long | 条件必填 | `costBearer=ORDER` 时必填,且必须是成员中某个 `ASSIGNMENT` 的订单 |
|
||||
| `remark` | String | ❌ | 确认备注,≤ 200 字 |
|
||||
| `confirmCrossResident` | Boolean | ❌ | 跨常驻车派单确认,见下方 605036;**不传或 `false` 都表示不确认** |
|
||||
|
||||
#### 出参(**本次无变化**,字段来源:`ShareGroupCreateRespVO` 的 `@ApiModelProperty` 声明)
|
||||
🔴 **`admissionIntent` 服务端完全不读、不校验、不落库。** 成员到底是「已经占着这辆车」还是「本次一并派入」,一律按库里的实际占用现查现判。它纯粹是给前端做交互提示(按钮文案、二次确认)用的。填错不会改变服务端行为,也**不会**因此报错。
|
||||
|
||||
#### 出参(**本次无变化**)
|
||||
|
||||
> 字段出处:`hl-fleet-service/.../dispatch/vo/ShareGroupRespVO.java`、`ShareGroupMemberRespVO.java` 的 `@ApiModelProperty`。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| shareGroupId | String | 共用关系 ID(雪花 ID,`@JsonSerialize(ToStringSerializer)` 按字符串序列化) |
|
||||
| status | String | 关系状态:`ACTIVE`(已激活)或 `PENDING`(待激活) |
|
||||
| dimension | String | 共用维度:`VEHICLE` 或 `DRIVER` |
|
||||
| resourceId | String | 资源 ID(字符串序列化) |
|
||||
| members | List<Member> | 入库后的成员列表(含服务端分配的 id、timestamps) |
|
||||
| `shareGroupId` | String | 共用关系 ID(雪花 ID,`@JsonSerialize(ToStringSerializer)` 按字符串序列化) |
|
||||
| `groupBatchId` | String | 运营团期 ID(字符串序列化) |
|
||||
| `serviceDate` | String `yyyy-MM-dd` | 共用发生的服务日 |
|
||||
| `resourceType` | String | 资源维度:`VEHICLE` / `DRIVER` |
|
||||
| `resourceId` | String | 车辆或司机 ID(字符串序列化) |
|
||||
| `status` | String | 关系状态:**`ACTIVE` / `RELEASED`**(没有 `PENDING` 这个取值) |
|
||||
| `costBearer` | String | 成本承担方:`GROUP` / `ORDER` |
|
||||
| `costBearerOrderId` | String | `costBearer=ORDER` 时的承担订单 ID(字符串序列化;否则为 `null`) |
|
||||
| `costSourceRefNo` | String | 车费来源引用 = `SHARE-{shareGroupId}` |
|
||||
| `members` | List | **成员全集** |
|
||||
| `members[i].sourceType` | String | `ASSIGNMENT` / `GROUP_DISPATCH` |
|
||||
| `members[i].sourceId` | String | 成员来源 ID(字符串序列化) |
|
||||
| `members[i].requirementId` | String | 用车需求 ID(`ASSIGNMENT` 成员的准入授权键;**团级配车行为空**) |
|
||||
| `members[i].orderId` | String | 订单 ID(`ASSIGNMENT` 成员) |
|
||||
| `confirmedBy` | String | 确认人 adminId(字符串序列化) |
|
||||
| `confirmedAt` | String | 确认时间(服务端类型 `LocalDateTime`,按全局 Jackson 约定序列化) |
|
||||
| `version` | Integer | 乐观锁版本号 |
|
||||
| `history` | List | 变更历史;**仅查询端点带 `includeReleased=true` 时返回**,本确认端点的响应里为 `null` |
|
||||
|
||||
⚠️ **成员出参只有上面 4 个字段**:`sourceType` / `sourceId` / `requirementId` / `orderId`。成员行**没有**独立的 `id`,也**没有**任何时间戳字段。
|
||||
|
||||
⚠️ **`costSourceRefNo` 按关系身份生成**:同一关系生命周期内恒定,改 `costBearer`、重复确认都不变;关系解除后在同一槽重建会拿到**新值**。核团比对以它为准。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
POST /admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups HTTP/1.1
|
||||
```http
|
||||
POST /admin/fleet/group-dispatch/batches/8801/share-groups HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {token}
|
||||
|
||||
{
|
||||
"dimension": "VEHICLE",
|
||||
"resourceId": "2065329519232720897",
|
||||
"serviceDate": "2026-09-12",
|
||||
"resourceType": "VEHICLE",
|
||||
"resourceId": 1,
|
||||
"costBearer": "GROUP",
|
||||
"remark": "本车这一天承接这两户的接送",
|
||||
"members": [
|
||||
{
|
||||
"sourceType": "GROUP_DISPATCH",
|
||||
"sourceId": "2101524548279238658",
|
||||
"sourceId": 88001,
|
||||
"admissionIntent": "OCCUPYING"
|
||||
},
|
||||
{
|
||||
"sourceType": "ASSIGNMENT",
|
||||
"sourceId": "360022177073991680",
|
||||
"sourceId": 88002,
|
||||
"admissionIntent": "PENDING_ADMISSION"
|
||||
},
|
||||
{
|
||||
"sourceType": "ASSIGNMENT",
|
||||
"sourceId": "360022539151478784",
|
||||
"sourceId": 88003,
|
||||
"admissionIntent": "PENDING_ADMISSION"
|
||||
}
|
||||
]
|
||||
@@ -135,150 +183,158 @@ Authorization: Bearer {token}
|
||||
|
||||
#### 响应示例
|
||||
|
||||
以下为 2026-09-22 02:50 测试服实测的真实取值(自建团、2 个待准入 ASSIGNMENT 分属海拉尔东山机场与满洲里西郊机场,修复提交 e7cecb6d7 与 970f9a187 均在部署里):
|
||||
⚠️ **以下是按 VO 的 `@ApiModelProperty` 声明与其 `example` 值构造的字段结构示意,不是某一次实测报文。** 真实的 ID 是 19 位雪花,示例里的短 ID 沿用 VO 的 `example` 取值,**长度不代表真实形态**——前端一律按字符串处理。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"shareGroupId": "{shareGroupId}",
|
||||
"shareGroupId": "77001",
|
||||
"groupBatchId": "8801",
|
||||
"serviceDate": "2026-09-12",
|
||||
"resourceType": "VEHICLE",
|
||||
"resourceId": "1",
|
||||
"status": "ACTIVE",
|
||||
"dimension": "VEHICLE",
|
||||
"resourceId": "2065329519232720897",
|
||||
"costBearer": "GROUP",
|
||||
"costBearerOrderId": null,
|
||||
"costSourceRefNo": "SHARE-77001",
|
||||
"members": [
|
||||
{
|
||||
"id": "360030157818200064",
|
||||
"sourceType": "GROUP_DISPATCH",
|
||||
"sourceId": "2101524548279238658",
|
||||
"admissionIntent": "OCCUPYING",
|
||||
"createdAt": "2026-09-22T02:50:15Z",
|
||||
"admissionAt": null
|
||||
"sourceId": "88001",
|
||||
"requirementId": null,
|
||||
"orderId": null
|
||||
},
|
||||
{
|
||||
"id": "360030157818200065",
|
||||
"sourceType": "ASSIGNMENT",
|
||||
"sourceId": "360022177073991680",
|
||||
"admissionIntent": "PENDING_ADMISSION",
|
||||
"createdAt": "2026-09-22T02:50:15Z",
|
||||
"admissionAt": null
|
||||
"sourceId": "88002",
|
||||
"requirementId": "5501",
|
||||
"orderId": "70123"
|
||||
},
|
||||
{
|
||||
"id": "360030157818200066",
|
||||
"sourceType": "ASSIGNMENT",
|
||||
"sourceId": "360022539151478784",
|
||||
"admissionIntent": "PENDING_ADMISSION",
|
||||
"createdAt": "2026-09-22T02:50:15Z",
|
||||
"admissionAt": null
|
||||
"sourceId": "88003",
|
||||
"requirementId": "5502",
|
||||
"orderId": "70124"
|
||||
}
|
||||
]
|
||||
],
|
||||
"confirmedBy": "1001",
|
||||
"confirmedAt": "2026-09-12 10:30:00",
|
||||
"version": 1,
|
||||
"history": null
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
库内复核:三条成员行入库,状态一致,未被截断。
|
||||
#### 错误码
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
> 文案出处:`GroupDispatchShareErrorCode.java`(602 段)与 `AssignmentErrorCode.java`(605 段)里的 `IErrorCode.of(...)` 定义,逐字。
|
||||
> HL 约定:**业务失败与入参校验一律走 HTTP 200**,错误码在响应信封的 `code` 字段里(鉴权/系统异常除外)。
|
||||
|
||||
- 若请求的成员列表仅包含一个成员,创建成功但关系进入 `PENDING` 状态(等待第二个成员加入),响应会指示 `status: "PENDING"`。
|
||||
- 本接口是单次同步写操作,不查 Feign、不查 MQ,没有降级响应形态。
|
||||
| code | 服务端文案(逐字) | 触发条件与前端处理 |
|
||||
|------|------|------|
|
||||
| 600009 | 团期配车权威基线不可用,请稍后重试或检查团期状态 | 团期不存在,或其服务日未设置/不完整。**这是最先触发的一道校验**(见下方校验顺序),不是参数格式错 |
|
||||
| 602100 | 共用关系成员不能少于 2 个 | `members` 少于 2 个。共用关系至少要两方 |
|
||||
| 602101 | 共用关系成员不能超过 20 个 | `members` 超过 20 个 |
|
||||
| 602102 | 成员在该资源日无活跃占用: {0} | 提交前回读断言发现成员没占上车,**整体回滚** |
|
||||
| 602103 | 成员派单不属于本团或团期身份未知: {0} | `group_batch_id` 与 `requirement_id` 任一为 `NULL` 一律拒——未知不得当同团通过 |
|
||||
| 602104 | 服务日不在团期服务日窗内: {0} | **有意的边界**:窗外日没有团级配车行可挂靠,仍走 R3-EX 城市衔接 |
|
||||
| 602105 | 团级配车成员不属于本团或当日非活跃: {0} | `GROUP_DISPATCH` 成员的校验 |
|
||||
| 602106 | 成员已属于另一个共用关系: {0} | 要求先解除原关系 |
|
||||
| 602107 | 成本承担方缺失或非法 | `costBearer` 未传或取值非法 |
|
||||
| 602109 | 共用关系已被并发修改,请刷新后重试 | 乐观锁冲突,提示用户刷新 |
|
||||
| 605001 | 派单冲突:该车日期段已派 | **本次修复的误报点**。修复后它回归真实语义,见下方「影响评估」 |
|
||||
| 605036 | 司机与车辆不是常驻组合,请确认跨常驻车派单后重试({0}:{1};车辆 {2};司机 {3}) | 跨常驻车派单需人工确认,见下 |
|
||||
|
||||
#### 错误响应
|
||||
#### 校验顺序(决定你先看到哪个错误码)
|
||||
|
||||
以下为修复前的真实响应(同批传入 2+ 待准入成员、第二个开始命中旧缺陷):
|
||||
服务端 `confirm()` 的前两步在**事务之外**,顺序是固定的:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 605001,
|
||||
"message": "成员冲突:无法确认对端是否被授权共用该资源",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
1. `resourceType` 合法性;
|
||||
2. **团期权威基线**——一次同步 Feign 向 order-v3 取该团的服务日窗。取不到 → **600009**;取到了但 `serviceDate` 不在窗内 → **602104**;
|
||||
3. 以上都过了,才进入事务做成员数(602100/602101)、成员归属(602103/602105/602106)、成本承担方(602107)等校验,以及实际的授权、派单、准入写入。
|
||||
|
||||
修复后同一请求返回 HTTP 200。
|
||||
⚠️ **所以传一个不存在的 `groupBatchId` 时,先撞上的是 600009 而不是成员相关的校验码**——排查时不要据此认为成员参数已经通过了校验。
|
||||
|
||||
#### 业务边界
|
||||
#### 🔴 605036 跨常驻车派单需确认(前端必须实现的交互)
|
||||
|
||||
- 鉴权:需管理后台已登录且具备 `fleet:group-dispatch:write` 权限点,未登录/无权限按网关与全局鉴权统一规则处理。
|
||||
- 成员类型混合:同一请求可混合 GROUP_DISPATCH(已占用)与 ASSIGNMENT(待准入),服务端会自动编排合法的启动态。
|
||||
- 资源冲突判定:两个层级各自独立运作;修复仅影响第二层(新增成员间的相互认对)的数据完整性。
|
||||
- 冲突误报点(**本次修复消除**):同批 2+ 待准入成员、无既有关系可对标时,原第二层投影缺 `applicant_id`,导致每个新增成员都无法判证对端身份,第二个开始必报 605001。
|
||||
端点第 3 步把成员改派进共用资源时走的是既有派单写路径,它有一道**提示型守卫**——满足下列**任一**条即判「跨常驻」,未确认一律拒:
|
||||
|
||||
1. 这辆共用车已设常驻司机,且与该成员当前司机**不是同一人**;
|
||||
2. 该成员当前司机本身是**别的车**的常驻司机。
|
||||
|
||||
⚠️ **两条是「或」的关系**:共用车没设常驻司机(`primary_driver_id` 为空)只躲开①,②照样会触发。
|
||||
|
||||
⚠️ **车务在页面上选的是「一辆车」,但 605036 说的是司机**——`resourceType=VEHICLE` 时被派入的司机不是车务选的,是**该成员原派单上的司机**(服务端按成员原样带过去,本端点不改司机)。所以 605036 的消息会点名「哪条派单 / 跨常驻的具体形态 / 车牌 / 司机姓名」。
|
||||
|
||||
**前端处理方式**:拿到 605036 后向车务展示 `message` 原文并询问「确认跨常驻车派单?」,确认后带 `confirmCrossResident=true` **原样重发**同一份请求即可。失败路径会释放 10 秒防重键,不会撞「请勿重复提交」。**不传或传 `false` 都表示不确认,服务端不会替你确认。**
|
||||
|
||||
#### 🔴 防重与幂等是两件事
|
||||
|
||||
1. **10 秒内重复提交同一份成员全集**会被防重窗口**拒绝**——前端按「请勿重复提交」提示,**不要当失败报红**;
|
||||
2. **窗口之外**重复提交同一份全集会**正常受理**,那**是成功**,返回同一个 `shareGroupId`。
|
||||
|
||||
防重键 = `serviceDate : resourceType : resourceId : 成员集合排序摘要`。所以:
|
||||
|
||||
- 只改 `costBearer` 不改成员时,键不变,10 秒内会被挡下(**这是保护不是缺陷**);
|
||||
- 改了成员全集就是另一次业务操作,立刻受理,车务加一个成员不必等 10 秒;
|
||||
- 成员先排序再拼摘要——前端两次提交的成员顺序不同但集合相同时那是同一份计划,不会绕过防重窗口;
|
||||
- `confirmCrossResident` **不参与**防重键(它不是业务身份的一部分)。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
本接口在**表层契约**上无任何变化。修复前后的入参格式、响应字段、HTTP 状态码、业务错误码全部一致——差别仅在内部判定逻辑的数据完整性。
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
| 场景 | payload | 修复前 | 修复后 |
|
||||
|------|---------|--------|--------|
|
||||
| ✅ 同批准入 2 个跨城成员 | 如上"请求示例" | **605001**(误报) | **200**(正确) |
|
||||
| ✅ 单个待准入成员 | 仅 1 个 PENDING_ADMISSION | 200,`status=PENDING` | 200,`status=PENDING`(不变) |
|
||||
| ✅ 无待准入成员(全 OCCUPYING) | 仅 GROUP_DISPATCH + 资源占用者 | 200,`status=ACTIVE` | 200,`status=ACTIVE`(不变) |
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
**入库行为**:修复后,同批多待准入成员会全部入库成共用关系的成员行(`fleet_group_dispatch_share_member` 表),关系状态进入 `ACTIVE`(若仅 1 个待准入则为 `PENDING`)。
|
||||
|
||||
修复前,第二个待准入成员开始会触发回滚,整个请求的写入都不落地。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截)
|
||||
- 无 `fleet:group-dispatch:write` 权限 → 403
|
||||
- `groupBatchId` 不存在 → 业务码 603xxx(依具体情况)
|
||||
- `dimension` 不是 `VEHICLE` 或 `DRIVER` → 400(参数校验)
|
||||
- `resourceId` 指向不存在的车/司机 → 业务码 604xxx(依具体情况)
|
||||
- 成员列表为空 → 400
|
||||
- 修复点:同批 2+ 待准入成员时**不再误报 605001**;允许多成员创建一次完成(此前的 workaround 是"拆成多次请求、一个成员发一次")
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 冲突判定行为对比
|
||||
本接口在**表层契约**上无任何变化。修复前后的入参格式、响应字段、HTTP 状态码、业务错误码全部一致——差别仅在内部冲突判定所依赖的数据投影是否完整。
|
||||
|
||||
| 场景 | 修复前 | 修复后 |
|
||||
|------|--------|--------|
|
||||
| 同批 1 个待准入成员 | 200(`PENDING`) | 200(`PENDING`)—— 不变 |
|
||||
| 同批 2+ 待准入成员 | **605001**(从第 2 个开始必报) | **200(`ACTIVE`)—— 已修** |
|
||||
| 入参字段、响应字段 | — | 完全不变 |
|
||||
| 错误码列表 | — | 完全不变 |
|
||||
| 同批 2+ 个尚未占用的 `ASSIGNMENT` 成员、无既有关系可对标 | **605001**(误报) | 正常受理 |
|
||||
| 有既有共用关系可对标(走第一层判定) | 正常 | 正常(不变) |
|
||||
| 入参字段、响应字段、错误码清单 | — | **完全不变** |
|
||||
|
||||
## 六.7、影响评估
|
||||
## 五、数据库行为
|
||||
|
||||
- **是否破坏向后兼容**: 否——请求/响应字段、错误码全部不变。
|
||||
- **前端是否必须同步上线**: 否——管理后台无需改任何代码。
|
||||
- **前端 workaround 清理点**: 若此前为了绕开 605001 而实现了"一个成员发一次请求"的逻辑,现在可以改回一次请求传全部成员。
|
||||
- **错误码语义的恢复**: 605001 此前在**合法请求**上会误报,修复后它**回归为真实冲突的表示**——收到 605001 就意味着确实存在资源冲突,可以按真实冲突提示用户,不必再考虑"这可能是个 bug"。
|
||||
涉及的表:`fleet_group_dispatch_share_group`(关系主体)、`fleet_group_dispatch_share_member`(成员行)、`fleet_group_dispatch_share_log`(变更历史)。
|
||||
|
||||
## 七、不影响范围
|
||||
修复后,同批多个尚未占用的成员会全部入库成该关系的成员行。修复前,从第二个此类成员开始会抛 605001,**整个请求在同一事务里回滚**,写入不落地。
|
||||
|
||||
- 任何接口的**请求参数、响应字段、错误码**——全部不变。
|
||||
- **单成员创建**的行为(修复前后均 200)。
|
||||
- **既有关系的查询、解除、成员管理**等其它端点。
|
||||
- 605001 以外的其它错误码的触发口径。
|
||||
## 六、边界行为
|
||||
|
||||
## 八、测试环境已验证
|
||||
- 未登录 → 网关拦截(鉴权异常不走业务信封)
|
||||
- 无 `fleet:group-dispatch:write` 权限 → 鉴权拦截
|
||||
- `members` 少于 2 个 / 超过 20 个 → 602100 / 602101
|
||||
- `serviceDate` 不在团期服务日窗内 → 602104
|
||||
- `resourceType` 不是 `VEHICLE` 或 `DRIVER` → 参数校验失败(`@NotBlank` 只校验非空,取值合法性由服务端业务校验兜底)
|
||||
- 成员已属于另一个共用关系 → 602106(需先解除)
|
||||
- 重复提交同一份成员全集 → 窗口内被防重拒绝;窗口外正常受理并返回同一个 `shareGroupId`
|
||||
|
||||
**部署**:fleet 测试服部署点 `bd19b79c8`(2026-09-22 02:50);`git merge-base --is-ancestor e7cecb6d7 bd19b79c8` = 真;`git merge-base --is-ancestor 970f9a187 bd19b79c8` = 真;Nacos 两实例 `healthy = true`。
|
||||
## 七、影响评估
|
||||
|
||||
**夹具**:自建团 + 自建服务日;车辆与司机各 1 个;GROUP_DISPATCH 为组级派车 1 条;ASSIGNMENT 为基层派车 2 条(分属海拉尔东山机场与满洲里西郊机场两个出行中户)。
|
||||
- **是否破坏向后兼容**:否——请求/响应字段、错误码全部不变。
|
||||
- **前端是否必须同步上线**:否——管理后台无需改任何代码即可获得修复效果。
|
||||
- **可以清理的 workaround**:若此前为了绕开 605001 而实现了「一个成员发一次请求」的逻辑,现在可以改回一次请求传全部成员。⚠️ 注意 `members` 是**全集不是增量**,逐个发送的写法在语义上本来就不等价(后一次会覆盖前一次的全集)。
|
||||
- **605001 语义的恢复**:该码此前会在合法请求上误报,修复后它回归为**真实派单冲突**的表示(`派单冲突:该车日期段已派`)——收到它就意味着确实存在冲突,可以按真实冲突提示用户。
|
||||
|
||||
**一次请求,验证修复生效**:
|
||||
## 八、不影响范围
|
||||
|
||||
```
|
||||
POST /admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups
|
||||
payload: 1×GROUP_DISPATCH(OCCUPYING) + 2×ASSIGNMENT(PENDING_ADMISSION)
|
||||
- 任何接口的**请求参数、响应字段、错误码清单**——全部不变。
|
||||
- 既有共用关系的**查询、解除**等其它端点。
|
||||
- 605001 以外其它错误码的触发口径。
|
||||
|
||||
修复前预期:605001(从第 2 个 ASSIGNMENT 开始误报)
|
||||
修复后实测:
|
||||
→ 200 ✓
|
||||
→ success=true ✓
|
||||
→ status=ACTIVE ✓
|
||||
→ members 数组长度 3 ✓
|
||||
→ DB 三成员行齐全、状态一致 ✓
|
||||
```
|
||||
## 九、验证状态
|
||||
|
||||
**后端部署(已核实)**:修复 PR #7983(`e7cecb6d7`)+ 测试补强 PR #8096(`970f9a187`)均已合入 `dev-v3`;fleet 测试服部署点 `bd19b79c8`,`git merge-base --is-ancestor e7cecb6d7 bd19b79c8` 与 `git merge-base --is-ancestor 970f9a187 bd19b79c8` **均为真**(本次逐条复核)。
|
||||
|
||||
**修复行为的证据来源**:源码 + 真库集成测试 `ShareGroupOverlapProjectionIntegrationTest`。
|
||||
|
||||
**本文件明确不提供的东西**:测试环境上的修复前/修复后对跑读数。修复 2026-09-19 已合入,测试环境此后一直是修复后状态,不存在可对跑的修复前环境。
|
||||
|
||||
**网关侧(2026-09-22 实调)**:经网关 `POST https://api.test.1814.love:9443/admin/fleet/group-dispatch/batches/1/share-groups` → HTTP 200 + 业务码 **600009**(刻意用不存在的 `groupBatchId=1`,在第一道校验即失败关闭)。同轮阴性对照打在一个不存在的路径上 → `code=404`、`接口不存在: POST ...`,与业务码**可分辨**。
|
||||
|
||||
⇒ 由此坐实三项:**路由通**(`/admin/fleet/**` → `lb://hl-fleet-service`,无 `/v3` 前缀;带 `/v3` 的变体返回 404)、**鉴权通**(需切换到 `VEHICLE_MANAGER` 角色,权限点 `fleet:group-dispatch:write`)、**请求体反序列化通**(请求驱动到了 `confirm()` 内部的业务校验,而非停在 `@Valid` 阶段)。
|
||||
|
||||
⚠️ **这次实调的覆盖边界**:它验的是**端点可达性与契约反序列化**,**没有**验证本次修复的行为本身(同批多个待准入成员不再误报 605001)——那需要真实的团期与派单夹具。修复行为的证据来源见上一条。该请求全程未触达写库路径:`confirm()` 里抛 600009 的 `assertServiceDateInWindow` 位于唯一写库入口 `confirmInTransaction` 之前,且其 javadoc 明确「基线是一次同步 Feign,刻意留在事务外拉」。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
@@ -292,7 +348,3 @@ POST /admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups
|
||||
|
||||
- **Issue**: [#7982](https://git.1814.love:8443/wx/HL/issues/7982)
|
||||
- **PR**: [#7983](https://git.1814.love:8443/wx/HL/pulls/7983)(修复)、[#8096](https://git.1814.love:8443/wx/HL/pulls/8096)(测试)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
|
||||
在新工单中引用
屏蔽一个用户