382 行
19 KiB
Markdown
382 行
19 KiB
Markdown
---
|
||
schema: "hl-changelog/v2"
|
||
ticket: "8159"
|
||
title: "团级配车成员共用候选查询"
|
||
consumer: "admin"
|
||
author: "wx(GIT)"
|
||
change_type: "新增接口"
|
||
backend_status: "deployed"
|
||
gateway_status: "verified"
|
||
frontend_status: "verified"
|
||
frontend_owner: "mmg"
|
||
frontend_ref: "bc4024952daa784232abe6fbf56b658b57361bb3"
|
||
target_release: "v2.1"
|
||
verified_at: "2026-09-22"
|
||
status_note: "2026-09-22 mmg 交付:ShareGroupEditModal 新建/编辑选举(浏览态三态 selectable 不误禁、shareGroupId 复算恢复本关系成员并预勾、members 全集提交、605036 确认重发、602106 刷新重选),ShareGroupPanel 加入口,抽屉透传 serviceDates;checkpoint 全绿 26 测试"
|
||
updated_at: "2026-09-22"
|
||
base: "dev-v3"
|
||
---
|
||
|
||
# 团级配车成员共用候选查询(#8159)
|
||
|
||
> **服务**: hl-fleet-service(经网关调用,无需关心服务端口)
|
||
> **PR**: #8188
|
||
> **Issue**: #8159
|
||
> **日期**: 2026-09-22
|
||
> **影响范围**: 管理后台团级配车页面成员共用选举
|
||
|
||
---
|
||
|
||
## ⚠️ 关键变化
|
||
|
||
新增查询端点,返回「成员共用」的候选行清单,喂给已有的「确认共用关系」写口。
|
||
|
||
**三条会直接影响你怎么写代码的点,按严重度排**:
|
||
|
||
1. 🔴 **`shareGroupId` 必须由前端比对,本接口不会替你做**(业务边界第 2 条)。编辑一个**已存在**的共用关系时,它自己的既有成员会被本接口置灰成 `ALREADY_IN_ANOTHER_GROUP`——前端要把 `shareGroupId` 等于「正在编辑的关系 ID」的行**恢复成可勾选**。并且**提交时 `members` 是全集不是增量**:漏掉的既有成员会被写口按「移出本关系」处理,**不报错、静默生效**。
|
||
2. 🔴 **`occupying` / `selectable` 是三态,`null` 是「未判定」不是「否」**。不传 `resourceId` 时 `occupying` 恒 `null`、`selectable` **永不为 `true`**——但它**可能是 `false`**(已属别的关系、或团期身份缺失这两条浏览态就判得出来),所以置灰要照常渲染。
|
||
3. **成功码是 `code: 200`,不是 `0`**;业务失败也返 HTTP 200,**一律按 `body.code` 判,不要按 HTTP 状态行判**。
|
||
|
||
---
|
||
|
||
## 一、背景(选填)
|
||
|
||
团级配车支持多个成员(派车行、团级配车行)共享同一车或司机资源的场景。选举新成员进关系时,需先查询该服务日下哪些成员行可选。本接口支持按资源维度(车/司机)和具体资源 ID 筛选候选。
|
||
|
||
---
|
||
|
||
## 二、变更接口清单
|
||
|
||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||
|---|------|------|------|----------|------|
|
||
| 1 | 查询成员共用候选 | GET | `/admin/fleet/group-dispatch/batches/{groupBatchId}/share-member-candidates` | 新增接口 | 返回该团服务日下可选成员清单 |
|
||
|
||
---
|
||
|
||
## 三、接口详情
|
||
|
||
### 1. 查询成员共用候选 `GET /admin/fleet/group-dispatch/batches/{groupBatchId}/share-member-candidates`
|
||
|
||
**VO**: `ShareMemberCandidateRespVO`
|
||
|
||
#### 使用场景
|
||
|
||
管理后台团级配车页面「成员共用」选举弹窗打开时,调用本接口获取候选成员清单。前端先让用户选择资源维度(车 / 司机),再传 `resourceId` 重新查询,获得勾选框可选状态。
|
||
|
||
#### 入参
|
||
|
||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||
|------|------|------|------|------|------|
|
||
| `groupBatchId` | Path | Long | ✅ | — | 团期 ID(`group_batch_id`)。🔴 **这不是 `product_batch_id`**;维度错误不报参数校验错,只返 `data: []` |
|
||
| `serviceDate` | Query | String(`yyyy-MM-dd`) | ✅ | 必须在团期服务日窗内 | 服务日。窗外日期返 `code: 602104` |
|
||
| `resourceType` | Query | String | ✅ | `VEHICLE` / `DRIVER` | 资源维度。缺失或非法值返 `code: 100001` |
|
||
| `resourceId` | Query | Long | ❌ | — | 具体资源 ID(车 ID / 司机 ID)。**不传时 `occupying` / `selectable` 返 `null`**(未判定状态,见业务边界第 1 条) |
|
||
|
||
#### 出参 `Result<List<ShareMemberCandidateRespVO>>`
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `sourceType` | String | `ASSIGNMENT`(派车行)/ `GROUP_DISPATCH`(团级配车行) |
|
||
| `sourceId` | String | 雪花 ID,**字符串形态**(19 位,前端一律当字符串处理,禁转 Number) |
|
||
| `requirementId` | String | 需求 ID(派车行有值,团级配车行可能 null) |
|
||
| `orderId` | String / null | 订单 ID。`GROUP_DISPATCH` 行可能为 null |
|
||
| `orderNo` | String | 订单号 |
|
||
| `customerName` | String | 客户名称 |
|
||
| `headcount` | Integer | 人数 |
|
||
| `pickupAt` | String | 上车地点 |
|
||
| `dropoffAt` | String / null | 下车地点,可能为 null |
|
||
| `pickupParticipant` | Integer | 上车人数 |
|
||
| `dropoffParticipant` | Integer | 下车人数 |
|
||
| `groupCode` | String / null | 团号,可能为 null |
|
||
| `vehicleModel` | String / null | 车型 |
|
||
| `occupiedVehicleId` | String / null | 该行实际占用的车 ID;未占用时 null |
|
||
| `occupiedVehiclePlate` | String / null | 该行实际占用的车牌号 |
|
||
| `occupiedDriverId` | String / null | 该行实际占用的司机 ID;未占用时 null |
|
||
| `occupiedDriverName` | String / null | 该行实际占用的司机名称 |
|
||
| `occupying` | Boolean / **null** | 本行在 `(serviceDate, resourceType, resourceId)` 上是否活跃占用。**`resourceId` 未传时为 `null`**(未判定) |
|
||
| `shareGroupId` | String / null | 本行所属的 ACTIVE 共用关系 ID;不属任何关系时 null。🔴 **调用方必须拿它和「正在编辑的关系 ID」比对**,见业务边界第 2 条 |
|
||
| `selectable` | Boolean / **null** | 是否可勾选。`true` = 可选;`false` = 不可选(此时 `unselectableReason` 非空);**`null` = 未判定**(`resourceId` 未传、且本行未被 `ALREADY_IN_ANOTHER_GROUP` / `CROSS_BATCH` 判死时出现)。🔴 **`resourceId` 未传时本字段永不为 `true`** |
|
||
| `unselectableReason` | String / null | 不可选原因枚举值(见六.5 章节);`selectable=true` 时为 null |
|
||
| `unselectableDetail` | String / null | 人类可读的补充说明,如「订单 HLxxxx 已在车 A 关系中」 |
|
||
|
||
#### 请求示例
|
||
|
||
```json
|
||
GET /admin/fleet/group-dispatch/batches/2102125467954044929/share-member-candidates?serviceDate=2026-12-25&resourceType=VEHICLE&resourceId=2085539421276286978
|
||
```
|
||
|
||
无请求体。Authorization 头由网关透传(管理后台 JWT)。
|
||
|
||
#### 响应示例
|
||
|
||
成功响应(带具体资源 ID 的正常清单):
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "成功",
|
||
"data": [
|
||
{
|
||
"sourceType": "ASSIGNMENT",
|
||
"sourceId": "2102125764705181697",
|
||
"requirementId": "2102125585692295169",
|
||
"orderId": "2102125467924684801",
|
||
"orderNo": "HL20260922035901739",
|
||
"customerName": "#8114AC8-X",
|
||
"headcount": 1,
|
||
"pickupAt": "满洲里口岸",
|
||
"dropoffAt": null,
|
||
"pickupParticipant": 0,
|
||
"dropoffParticipant": 0,
|
||
"groupCode": null,
|
||
"vehicleModel": "丰田普拉多",
|
||
"occupiedVehicleId": "2085539421276286978",
|
||
"occupiedVehiclePlate": "蒙A-E2E99",
|
||
"occupiedDriverId": "2065272150012444674",
|
||
"occupiedDriverName": "道尔吉",
|
||
"occupying": true,
|
||
"shareGroupId": null,
|
||
"selectable": true,
|
||
"unselectableReason": null,
|
||
"unselectableDetail": null
|
||
}
|
||
],
|
||
"success": true
|
||
}
|
||
```
|
||
|
||
**示例说明**:示例值取自测试环境某次真实调用,不构成可复现夹具。
|
||
|
||
#### 空数据 / 降级响应
|
||
|
||
该团该服务日无任何在飞派单与活跃团级配车行时返 `code: 200, data: []`(**不是 null、不抛错**)。
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "成功",
|
||
"data": [],
|
||
"success": true
|
||
}
|
||
```
|
||
|
||
🔴 **区分「空」与「失败关闭」**:拿不到团期权威基线时,本接口**不降级返空**,而是**失败关闭**抛 `GROUP_BATCH_BASELINE_UNAVAILABLE`。⇒ `data: []` 只意味着「确实没有候选」,前端可以放心按「无可选成员」渲染,不必担心它其实是一次被吞掉的下游故障。
|
||
|
||
#### 错误响应
|
||
|
||
**服务日不在团期窗内**:
|
||
|
||
```json
|
||
{
|
||
"code": 602104,
|
||
"message": "服务日不在团期服务日窗内: 2026-12-28",
|
||
"success": false,
|
||
"data": null
|
||
}
|
||
```
|
||
|
||
**参数非法**(缺 `resourceType` 或传非法值):
|
||
|
||
```json
|
||
{
|
||
"code": 100001,
|
||
"message": "参数非法: 资源维度非法: HOUSE",
|
||
"success": false,
|
||
"data": null
|
||
}
|
||
```
|
||
|
||
#### 业务边界
|
||
|
||
**0. 本清单直接喂给写口**:返回的 `sourceType` + `sourceId` 就是「确认共用关系」写口 `members[]` 要的两个字段,**原样透传即可**,不需要再做映射。
|
||
|
||
**1. `resourceId` 未传时的 `null` 三态语义**
|
||
|
||
| 字段 | 传了 `resourceId` | 未传 `resourceId`(浏览态) |
|
||
|---|---|---|
|
||
| `occupying` | `true` / `false` | 恒 `null`(未判定) |
|
||
| `selectable` | `true` / `false` | `false` **或** `null`,**永不为 `true`** |
|
||
| `unselectableReason` | `selectable=false` 时非空 | 同左 |
|
||
|
||
`null` 的含义是**未判定**,不是「否」。不传 `resourceId` 时「当日占没占着这个资源」根本算不出来,所以 `occupying` 一律 `null`。
|
||
|
||
🔴 **但浏览态下 `selectable` 并不总是 `null`**:有两条判据不依赖 `resourceId`,浏览态照样能判死——
|
||
- **已属别的 ACTIVE 共用关系** → `selectable=false`,`unselectableReason=ALREADY_IN_ANOTHER_GROUP`
|
||
- **团期身份缺失**(`requirement_id` 为空,或团期与本团不符)→ `selectable=false`,`unselectableReason=CROSS_BATCH`
|
||
⚠️ **订正 2026-09-22**:本行原写「`group_batch_id` / `requirement_id` 为空」,**`group_batch_id` 那一半不成立**——取数条件是 `eq(group_batch_id, ?)`,`group_batch_id` 为空的派单行**结构上查不出来**、压根不在清单里(见第六节「4. 覆盖范围」)。所以前端**不必**为「`group_batch_id` 为空 + `CROSS_BATCH`」写分支,那个组合不可能出现;会以 `CROSS_BATCH` 出现在清单里的,只有 `requirement_id` 为空或团期不符这两种。
|
||
|
||
⇒ 前端在浏览态**不能**把 `selectable == null` 当成 `false` 禁用勾选,也**不能**当成 `true` 放开;但**要照常渲染 `selectable === false` 的置灰与原因**——那些行在浏览态就已经定死了,选了具体资源也不会变回可选。
|
||
|
||
**2. 🔴 `shareGroupId` 必须由调用方比对——这是契约的一部分,不是可选优化**
|
||
|
||
本读口**不知道你正在编辑哪一个共用关系**,所以凡已属任一 ACTIVE 关系的行,一律置灰为 `ALREADY_IN_ANOTHER_GROUP`。而写口的 602106 拒的是「属于**另一个**关系」——**同一关系的既有成员,写口是收的**。
|
||
|
||
⇒ **给同一个 `shareGroupId` 追加第 3 个成员这个动作,本清单的置灰结构上覆盖不到。** 前端必须自己做两件事:
|
||
|
||
1. **把 `shareGroupId` 等于「正在编辑的关系 ID」的行视为可勾选**(忽略本接口给它的 `ALREADY_IN_ANOTHER_GROUP` 置灰)。不做这一步的表现是:编辑一个已有关系时,它自己的现有成员全部不可选,功能做不出来。
|
||
2. 🔴 **提交时 `members` 是全集不是增量**——要把该关系的**既有成员连同新成员一起**放进 `members`。**漏掉的既有成员会被写口按「移出本关系」处理。** 只提交新成员的写法不会报错,但会静默把原有成员全部踢出关系。
|
||
|
||
**3. `selectable=true` 不是「提交必成功」的承诺**
|
||
|
||
本清单是**时点快照、不加锁**。返回之后别的车务仍可能把某成员纳入另一个关系,提交时照样可能撞 `602106`。
|
||
|
||
⇒ 这种 602106 请渲染成「**有人先一步占了,请刷新候选清单重选**」,🔴 **不要当系统故障自动重试——重试必然同样失败**。这条竞态读口消除不了(加锁也不行,锁在响应返回那一刻就已放开),最终由写口的锁定读与唯一键兜底。
|
||
|
||
**4. 覆盖范围:未关联团期的派单行结构上不在本清单内**
|
||
|
||
本清单只列 `group_batch_id` 指向本团的派单行。`group_batch_id` 为空的派单行查不出来——但**这不是缺口**:这类行在写口同样恒被 602103 拒,「看不见」与「看得见也提交不了」**等价**。要让这类行可共用,得先补上团期身份(数据侧动作,不是前端能处理的)。
|
||
|
||
---
|
||
|
||
## 四、契约约束与正确调用方式
|
||
|
||
### ✅ 正确 / ❌ 错误请求对照
|
||
|
||
🔴 **先说判据**:本服务的业务失败与入参校验**一律返 HTTP 200**(`GlobalExceptionHandler` 带 `@ResponseStatus(HttpStatus.OK)`),**`body.code` 才是真相**。前端判错请一律读 `body.code`,**不要读 HTTP 状态行**——按状态行判会把所有业务错误当成成功。
|
||
|
||
| 场景 | 请求 | 预期(HTTP 恒 200) |
|
||
|------|------|------|
|
||
| ✅ 浏览态,看全清单 | `?serviceDate=2026-12-25&resourceType=VEHICLE`(无 resourceId) | `code: 200`;各行 `occupying=null`;`selectable` 为 `null` 或 `false`(被判死的行),**不会是 `true`** |
|
||
| ✅ 选定车后判可选 | `?serviceDate=2026-12-25&resourceType=VEHICLE&resourceId=2085539421276286978` | `code: 200`;各行 `occupying=true/false`,`selectable=true/false` |
|
||
| ✅ 同团其他服务日 | `?serviceDate=2026-12-26&resourceType=VEHICLE&resourceId=xxx` | 在该团服务日窗内则照常返回候选,否则 `code: 602104` |
|
||
| ❌ 维度错填 | `?serviceDate=2026-12-25&resourceType=HOUSE&resourceId=xxx` | `code: 100001`「参数非法: 资源维度非法: HOUSE」 |
|
||
| ❌ 缺维度参数 | `?serviceDate=2026-12-25&resourceId=xxx`(无 resourceType) | `code: 100001`「参数非法: 资源维度不能为空」 |
|
||
| ❌ 路径参数填成产品班期 ID | `/batches/{product_batch_id}/share-member-candidates?...` | 🔴 **不报参数错**,返 `code: 200` + `data: []`。失败形态是静默的,看起来像「接口没数据」 |
|
||
|
||
---
|
||
|
||
## 五、数据库行为
|
||
|
||
本接口**仅读**,无任何 DB 写入,不新增表、不改表结构、无 Flyway 脚本。
|
||
|
||
---
|
||
|
||
## 六、边界行为
|
||
|
||
**鉴权**(🔴 本单专门订正过一处长期误解,前端排查 403 时会用到):
|
||
|
||
- **未登录 / token 失效** → 网关 `JwtAuthFilter` 拦下
|
||
- **已登录但角色不符** → `FleetAdminRoleGuardInterceptor` 对 `/admin/fleet/**` **只放行 `VEHICLE_MANAGER` 与 `SUPER_ADMIN` 两个角色**
|
||
- 🔴 **鉴权的真相是角色,不是权限点**:`fleet:group-dispatch:view` / `fleet:group-dispatch:write` 这两个字符串在整个 Java 侧**零引用**,它们只是权限字典里的声明,不是运行时判据。⇒ **「给某个非车务角色开个只读权限点,让他看候选清单」这件事配置不出来**——那类角色连 `/admin/fleet/**` 都进不来。要改授权粒度得动拦截器,是另一件事。
|
||
|
||
**其余边界**(一律 HTTP 200,按 `body.code` 判):
|
||
|
||
- **资源维度缺失 / 非法** → `code: 100001`
|
||
- **服务日超出团期服务日窗** → `code: 602104`
|
||
- **团期权威基线拿不到** → **失败关闭**(不降级返空),见「空数据 / 降级响应」
|
||
- **本团本服务日无候选** → `code: 200` + `data: []`
|
||
|
||
**网关路由**:复用既有 `- Path=/admin/fleet/**` → `lb://hl-fleet-service`,本单**不新增路由**。
|
||
|
||
---
|
||
|
||
## 六.5 枚举 / 数据字典
|
||
|
||
### sourceType(行源类型)
|
||
|
||
**所属字段**: `sourceType` | **类型**: `String`
|
||
|
||
| 值 | 中文 | 说明 |
|
||
|---|---|---|
|
||
| `ASSIGNMENT` | 派车行 | 订单需求对应的派车行 |
|
||
| `GROUP_DISPATCH` | 团级配车行 | 团级配车单位产生的配车行 |
|
||
|
||
### resourceType(资源维度)
|
||
|
||
**所属字段**: `resourceType`(查询参数) | **类型**: `String`
|
||
|
||
| 值 | 中文 | 说明 |
|
||
|---|---|---|
|
||
| `VEHICLE` | 车辆 | 按车 ID 筛选候选 |
|
||
| `DRIVER` | 司机 | 按司机 ID 筛选候选 |
|
||
|
||
### unselectableReason(不可选原因)
|
||
|
||
**所属字段**: `unselectableReason` | **类型**: `String`
|
||
|
||
| 值 | 触发条件 | 浏览态(未传 `resourceId`)能否判出 | 对应写口错误码 |
|
||
|---|---|---|---|
|
||
| `CROSS_BATCH` | `requirement_id` **为空**,或团期与本团不符。⚠️ 订正 2026-09-22:**不含 `group_batch_id` 为空**,那类行结构上查不出来、不在清单里(见第六节「4. 覆盖范围」) | ✅ 能 | 602103 |
|
||
| `NOT_OCCUPYING` | 派单不在**可改派的在飞态**,或当日**标记不用车** | ❌ 不能(需 `resourceId`) | 602102 |
|
||
| `ALREADY_IN_ANOTHER_GROUP` | 本行已属某个 ACTIVE 共用关系(此时 `shareGroupId` **非空**) | ✅ 能 | 602106 |
|
||
| `GROUP_DISPATCH_INVALID` | 团级配车行**当日不在此资源上** | ❌ 不能(需 `resourceId`) | 602105 |
|
||
|
||
**说明**:这一列值**不是**接口返回的 `code`,而是 `unselectableReason` 字段的取值。本接口只要能返回清单,`code` 恒为 `200`;上面那些 6021xx 是**提交共用关系**(写口)失败时才会出现在 `code` 里的。
|
||
|
||
🔴 **`ALREADY_IN_ANOTHER_GROUP` 这一行前端必须自己复算**:本读口不知道你在编辑哪个关系,会把正在编辑的那个关系的既有成员也置灰成它。判据是 `shareGroupId` 是否等于正在编辑的关系 ID,详见业务边界第 2 条。
|
||
|
||
---
|
||
|
||
## 六.6 修改前后对比
|
||
|
||
无,本接口为新增。
|
||
|
||
---
|
||
|
||
## 六.7 影响评估
|
||
|
||
- **破坏兼容**:否,新增接口
|
||
- **前端同步上线要求**:否,新接口不影响现有流程
|
||
- **workaround 清理**:无
|
||
|
||
---
|
||
|
||
## 七、不影响范围
|
||
|
||
- **仅影响**:管理后台团级配车页面成员共用选举功能
|
||
- **零影响**:
|
||
- 派车行的查询 / 新增 / 编辑
|
||
- 订单创建 / 支付流程
|
||
- 团期成团 / 分配等其他流程
|
||
|
||
---
|
||
|
||
## 八、测试环境已验证
|
||
|
||
真实接口调用结果(hl-fleet-service 已部署 `dev-v3 @ cf9dc85a4`,merge commit `a584bd087`):
|
||
|
||
```
|
||
GET /admin/fleet/group-dispatch/batches/2102125467954044929/share-member-candidates
|
||
?serviceDate=2026-12-25&resourceType=VEHICLE&resourceId=2085539421276286978
|
||
→ 200 + 候选清单(occupying=true/false, selectable=true/false) ✓
|
||
|
||
GET /admin/fleet/group-dispatch/batches/2102125467954044929/share-member-candidates
|
||
?serviceDate=2026-12-25&resourceType=VEHICLE
|
||
→ 200 + 候选清单(occupying=null, selectable=null) ✓
|
||
|
||
GET /admin/fleet/group-dispatch/batches/2102125467954044929/share-member-candidates
|
||
?serviceDate=2026-12-28&resourceType=VEHICLE&resourceId=xxx
|
||
→ 602104(服务日不在团期服务日窗内) ✓
|
||
|
||
GET /admin/fleet/group-dispatch/batches/2102125467954044929/share-member-candidates
|
||
?serviceDate=2026-12-25&resourceType=HOUSE
|
||
→ 100001(参数非法: 资源维度非法: HOUSE) ✓
|
||
```
|
||
|
||
---
|
||
|
||
## 九、相关历史 PR
|
||
|
||
无,本接口为新增。
|
||
|
||
---
|
||
|
||
## 十、相关文档
|
||
|
||
- 关联 Issue: [wx/HL#8159](https://git.1814.love:8443/wx/HL/issues/8159)
|
||
- 关联 PR: [wx/HL#8188](https://git.1814.love:8443/wx/HL/pulls/8188)
|
||
|
||
---
|
||
|
||
## 关联 / 联系人
|
||
|
||
### 链接
|
||
|
||
- **Issue**: [#8159](https://git.1814.love:8443/wx/HL/issues/8159)
|
||
- **PR**: [#8188](https://git.1814.love:8443/wx/HL/pulls/8188)
|
||
- **Merge commit**: [a584bd087](https://git.1814.love:8443/wx/HL/commit/a584bd087)
|
||
|
||
### 联系人
|
||
|
||
- **后端负责人**: @wx
|