文件
hl-api-changelog/changelogs-v2/2026-09/22_8159_团级配车成员共用候选查询-新增接口-管理后台.md
T
2026-09-22 22:06:19 +08:00

382 行
19 KiB
Markdown
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
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