docs(changelog): #8004 候选面拆出 shareEligible,两个读口 cityJunctionShareCandidate 恢复同义(修改接口)
changelog-filename-gate / validate (push) Failing after 2s

同名字段在 candidates 与 precheck 上含义不同:#7444 把 candidates 一侧扩写成
「同城衔接 或 已确认共用关系」,precheck 一侧保持原义,于是同一对跨城派单
两个读口返回相反的 true/false。本次 candidates 该列回到 #5302 已发布的原义,
新增 shareEligible 承载「这条冲突被放行了吗」(恒等于 !blocking)。
precheck 出参与两端点入参一律不变。

收件人 mmg:若前端已按 #7444 口径把 cityJunctionShareCandidate 当「可不可以选」用,
须改读 shareEligible 或 blocking。

Refs #8004

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
jw
2026-09-20 15:40:36 +08:00
共同撰写人 Claude Opus 5
父节点 f5596bf88e
当前提交 59efa9d540
@@ -0,0 +1,436 @@
---
schema: "hl-changelog/v2"
ticket: "8004"
title: "派车共用关系:候选面拆出 shareEligible,cityJunctionShareCandidate 两个读口恢复同义"
consumer: "admin"
author: "jw(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "同名字段 cityJunctionShareCandidate 此前在 candidates 与 precheck 两个读口上含义不同:#7444 把 candidates 一侧扩写成『同城衔接 或 已确认共用关系』,precheck 一侧保持原义『只说同城衔接』,于是同一对跨城派单两个读口返回相反的 true/false。本次把 candidates 该列恢复为原义(与 precheck 同义),新增 shareEligible 承载『这条冲突被放行了吗』。candidates 出参新增一个字段、一个既有字段取值口径回滚;precheck 出参与两端点入参一律不变。后端已合并 dev-v3(32f87d022)并部署 TEST,网关实测三种取值组合齐全(工单 #8004 AC-1)。前端若已按 #7444 口径把 cityJunctionShareCandidate 当作『可不可以选这辆车』使用,须改读 shareEligible 或 blocking。"
updated_at: "2026-09-20"
base: "dev-v3"
---
# fleet: 共用关系两个读口契约收口——拆出 shareEligible
> **存放目录**: `changelogs-v2/{YYYY-MM}/`(管理后台)
>
> **服务**: hl-fleet-service (端口 8087)
> **PR**: [#8027](https://git.1814.love:8443/wx/HL/pulls/8027)
> **Issue**: [#8004](https://git.1814.love:8443/wx/HL/issues/8004)
> **日期**: 2026-09-20
> **影响范围**: 派车弹窗「选车/选司机」候选列表与保存前预校验的**冲突项展示**
---
## ⚠️ 关键变化
1. **`candidates` 响应 `conflicts[].cityJunctionShareCandidate` 的取值口径回滚**:从 `同城衔接 || 已确认共用关系` 改回 **只表示同城衔接**,与 `precheck` 的同名字段恢复同义。
2. **`candidates` 响应 `conflicts[]` 新增 `shareEligible`**:承载「这条冲突被放行了吗」(`同城衔接 || 已确认共用关系`),**恒等于 `!blocking`**。
3. 🔴 **如果前端此前按 #7444 的口径把 `cityJunctionShareCandidate` 当「这辆车可不可以选」用,必须改读 `shareEligible` 或 `blocking`** —— 否则在「跨城但有共用关系」的场景会把可选的车渲染成不可选。
4. **`precheck` 的出参形状一个字都没变**,只是文档写明了 `cityJunctionShareCandidate` 的确切含义;那侧「被放行了吗」一直由 `blocking` 表达。
5. **两个端点的入参、路径、方法、错误码、判权全部不变。** 本次只订正了 Swagger 里「改派必须传 `excludeAssignmentId`」这句一直缺失的契约说明(行为本来就是这样,不是行为变更)。
---
## 一、背景
`cityJunctionShareCandidate` 是既有已发布字段,changelog `#5302`(2026-07-28)与 `#6843`(2026-08-31)都为两个读口定义过它,语义均为「两段行程首尾能不能同城衔接」。
`#7444`(团期配车共用关系)在候选面引入「人工确认的共用关系也能放行」这条新依据时,把它**挂到了这个既有字段上**(`candidates` 一侧改成 `junction || shareAuthorized`),而预校验面 `precheck` 一侧刻意保持原义。结果是:
> **同一对跨城派单,`precheck` 报 `false`、`candidates` 报 `true`。**
后端两侧各自读都自洽,各自也都满足自己那半条验收项 —— 错只落在**跨两个读口取同名字段**的前端身上,代码审查与单侧测试都看不见。本次按「加新的放行依据就加新字段」把两个概念拆开。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 查询派单候选资源 | POST | `/admin/fleet/assignments/candidates` | 修改接口 | `conflicts[]` 新增 `shareEligible`;`cityJunctionShareCandidate` 取值回滚为只表示同城衔接 |
| 2 | 派单预校验冲突 | POST | `/admin/fleet/assignments/precheck` | 修改接口 | 出参形状与取值均不变;仅补齐 `cityJunctionShareCandidate` 的确切含义与「须传 `excludeAssignmentId`」的契约说明 |
---
## 三、接口详情
### 1. 查询派单候选资源 `POST /admin/fleet/assignments/candidates`
**VO**: `Result<AssignmentCandidateRespVO>`
#### 使用场景
派车弹窗 Step 2「选车 / 选司机」列表。每个候选资源带出它在所选服务日期内的占用冲突明细,前端据此渲染「这辆车能不能选、为什么」。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| startDate | body | string(date) | 是 | `yyyy-MM-dd` | 本次派车服务开始日 |
| endDate | body | string(date) | 是 | `yyyy-MM-dd` | 本次派车服务结束日 |
| pickupAt | body | string | 否 | — | 本次接车城市/地点,判同城衔接用 |
| dropoffAt | body | string | 否 | — | 本次送达城市/地点,判同城衔接用 |
| orderId | body | number | 否 | 改派排除自身时必填 | 当前订单 ID |
| requirementId | body | number | 否 | 改派排除自身时必填 | 当前用车需求 ID |
| excludeAssignmentId | body | number | 否 | **改派场景必传,见「四」** | 要排除的自身派单 ID;同时是共用关系判定的主体身份 |
| assignmentGroupId | body | number | 否 | **与 `excludeAssignmentId` 成对传** | 被排除派单所属派车组 ID;**单独传它不足以排除自身占用** |
| vehicleKeyword | body | string | 否 | — | 车牌/车型关键词过滤 |
| vehiclePage | body | integer | 否 | 默认 1 | 车辆分页页码(**不是 `pageNum`**) |
| vehiclePageSize | body | integer | 否 | 默认 20 | 车辆分页大小(**不是 `pageSize`**) |
| driverPage | body | integer | 否 | 默认 1 | 司机分页页码 |
| driverPageSize | body | integer | 否 | 默认 20 | 司机分页大小 |
> 入参**本次未作任何改动**,此表为便于前端对照完整列出关键字段。
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| data.vehicles.records[].conflicts[].cityJunctionShareCandidate | boolean | **【本次取值口径回滚】** 两段行程是否**同城首尾衔接**(R3-EX)。**只表示城市衔接,不承载共用关系**;与 `precheck` 的同名字段同义 |
| data.vehicles.records[].conflicts[].shareEligible | boolean | **【本次新增】** 这条冲突是否已被放行 = `同城衔接 \|\| 已确认共用关系`,**恒等于 `!blocking`** |
| data.vehicles.records[].conflicts[].blocking | boolean | 这条冲突是否拦得住本次派车(未变) |
| data.vehicles.records[].conflicts[].reasonCode | string | `SHARE_GROUP_CONFIRMED` / `CITY_JUNCTION_SHAREABLE` / `ASSIGNMENT_CONFLICT`(未变) |
| data.vehicles.records[].conflicts[].shareGroupId | string | 放行所凭的已确认共用关系 ID,无则 `null`(未变) |
| data.vehicles.records[].conflicts[].reasonMessage | string | 可直接展示的原因文案(未变) |
| data.vehicles.records[].selectable | boolean | 这辆车在页面上能不能选(未变) |
| data.drivers.records[].conflicts[] | array | 司机维度同构,字段与上表一致 |
> 其余字段(`vehicleId` / `plate` / `seats` / `protocolPrice` / `availabilityWindows` …)本次一律未变,此处不重复列出。
#### 请求示例
```json
{
"startDate": "2026-12-28",
"endDate": "2026-12-28",
"pickupAt": "满洲里口岸",
"orderId": "2101169963900207106",
"requirementId": "7330992522584337",
"excludeAssignmentId": "359563681173999616",
"assignmentGroupId": "359563681132056576",
"vehicleKeyword": "蒙C04E04",
"vehiclePage": 1,
"vehiclePageSize": 20
}
```
#### 响应示例
跨城重叠、但两条派单同属一个已确认共用关系 —— **两列取值相反,这正是本次拆字段要表达的情况**:
```json
{
"code": 200,
"message": "成功",
"data": {
"vehicles": {
"total": 1,
"records": [
{
"vehicleId": "2065329515642396673",
"plate": "蒙C04E04",
"available": true,
"selectable": true,
"availabilityReasonCode": "SHARE_GROUP_CONFIRMED",
"conflicts": [
{
"assignmentId": "2101173674454192130",
"orderNo": "HL20260919124211678",
"startDate": "2026-12-28",
"endDate": "2026-12-28",
"cityJunctionShareCandidate": false,
"shareEligible": true,
"blocking": false,
"reasonCode": "SHARE_GROUP_CONFIRMED",
"shareGroupId": "359563681060753408",
"reasonMessage": "已确认同团车辆共用关系,可共享"
}
]
}
]
}
},
"success": true
}
```
#### 空数据 / 降级响应
- 所选日期内该资源无任何占用 → `conflicts` 为**空数组**(不是 `null`),`available=true`、`availabilityReasonCode=AVAILABLE`。
- 候选资源本身为空 → `records` 为空数组、`total=0`,恒 `code=200`。
- 占用行日期不完整(`startDate`/`endDate` 缺失)→ 该段不参与同城衔接与共用关系判定,`cityJunctionShareCandidate=false`、`shareEligible=false`、`blocking=true`(取严,宁可误报冲突也不误放行)。
#### 错误响应
本端点是只读咨询,业务上恒 `code=200`,冲突在 `data` 体现。仅入参非法时报错:
```json
{
"code": 605010,
"message": "排除派单不属于当前订单",
"data": null,
"success": false
}
```
#### 业务边界
- 只读,不写任何业务数据。
- 最终一致以 `create` / `change` **锁内重校验**为准,不信任本端点的咨询结果。
- 读口不比写口松:不传 `excludeAssignmentId` 即按「新建派单」处理,一律不享受共用授权。
---
### 2. 派单预校验冲突 `POST /admin/fleet/assignments/precheck`
**VO**: `Result<PrecheckRespVO>`
#### 使用场景
Step 2 选定车 + 司机后、点保存前的最后一次冲突探查。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| vehicleId | body | number | 是 | — | 已选车辆 ID |
| driverId | body | number | 是 | — | 已选司机 ID |
| startDate | body | string(date) | 是 | `yyyy-MM-dd` | 服务开始日 |
| endDate | body | string(date) | 是 | `yyyy-MM-dd` | 服务结束日 |
| pickupAt | body | string | 否 | — | 接车城市/地点 |
| dropoffAt | body | string | 否 | — | 送达城市/地点 |
| orderId | body | number | 否 | — | 当前订单 ID |
| excludeAssignmentId | body | number | 否 | **改派场景必传,见「四」** | 要排除的自身派单 ID;同时是共用关系判定的主体身份 |
| headcount | body | integer | 否 | — | 乘车人数 |
> 入参**本次未作任何改动**。
#### 出参
| 字段 | 类型 | 说明 |
|---|---|---|
| data.conflict | boolean | 是否存在阻断性冲突(未变) |
| data.conflicts[].cityJunctionShareCandidate | boolean | 两段行程是否**同城首尾衔接**。**只表示城市衔接,不承载共用关系**;与 `candidates` 的同名字段同义。**本次取值未变**,仅文档写明含义 |
| data.conflicts[].blocking | boolean | 这条冲突是否拦得住本次派车。**本侧没有 `shareEligible`,它的等价物就是 `!blocking`**(未变) |
| data.conflicts[].reasonCode | string | 同 `candidates` 的取值表(未变) |
| data.conflicts[].shareGroupId | string | 放行所凭的共用关系 ID,无则 `null`(未变) |
| data.conflicts[].conflictAssignmentId | string | 冲突派单 ID(未变) |
| data.conflicts[].conflictDateRange | string | 冲突日期段 `start~end`(未变) |
| data.warnings[] | array | 非阻断提示(未变) |
#### 请求示例
```json
{
"orderId": "2101169963900207106",
"requirementId": "7330992522584337",
"vehicleId": "2065329515642396673",
"driverId": "2065272148229865473",
"startDate": "2026-12-28",
"endDate": "2026-12-28",
"pickupAt": "满洲里口岸",
"headcount": 2,
"excludeAssignmentId": "359563681173999616"
}
```
#### 响应示例
与上面 `candidates` 完全同一组车/日期/城市/主体 —— **`cityJunctionShareCandidate` 两侧同为 `false`,分叉已消除**:
```json
{
"code": 200,
"message": "成功",
"data": {
"conflict": false,
"conflicts": [
{
"type": "vehicle",
"conflictAssignmentId": "2101173674454192130",
"conflictOrderNo": "HL20260919124211678",
"conflictDateRange": "2026-12-28~2026-12-28",
"cityJunctionShareCandidate": false,
"blocking": false,
"reasonCode": "SHARE_GROUP_CONFIRMED",
"shareGroupId": "359563681060753408",
"msg": null
}
],
"warnings": []
},
"success": true
}
```
#### 空数据 / 降级响应
- 无任何重叠占用 → `conflicts` 为空数组、`conflict=false`。
- 资源不可用(车队停用等)→ `conflict=true`,原因在 `warnings[]`,`conflicts[]` 可为空。
#### 错误响应
只读咨询恒 `code=200` 不抛业务异常,冲突在 `data` 体现。仅入参校验失败时:
```json
{
"code": 400,
"message": "车辆ID不能为空",
"data": null,
"success": false
}
```
#### 业务边界
- 只读,不写任何业务数据。
- 最终一致以 `create` 锁内重校验为准。
---
## 四、契约约束与正确调用方式
### 4.1 判「这辆车能不能选」读哪个字段
| 读口 | 推荐读 | 也可读 | **不要读** |
|---|---|---|---|
| `candidates` | `selectable`(整车级)/ `shareEligible`(单条冲突级) | `!blocking` | ~~`cityJunctionShareCandidate`~~ |
| `precheck` | `!blocking` | `conflict`(整体级) | ~~`cityJunctionShareCandidate`~~ |
**`cityJunctionShareCandidate` 只回答「这两段行程首尾能不能同城接上」**,它是一个事实描述,不是放行结论。跨城但有共用关系时它是 `false` 而车是可选的。
**跨两个读口时统一读 `blocking`** —— 它是两处都有、且两处同义的那一个。
### 4.2 🔴 改派场景必须传 `excludeAssignmentId`
两个端点都**不会自动认出「哪条占用是调用方自己」**。不排除自身时,**调用方会和自己冲突**,而拿到的读数与「共用关系功能根本没做」**一模一样**,页面上选不到车且没有任何报错。
`assignmentGroupId` 是配套的**归属校验身份**,用于确认被排除的那条确属当前操作的这一组;**它不是排除动作本身,单独传它不足以排除自身占用**。两者**成对传**。
TEST 实测(同一请求,单变量):
| 入参 | `selectable` | `availabilityReasonCode` | `shareGroupId` |
|---|---|---|---|
| 带 `excludeAssignmentId` + `assignmentGroupId` | **true** | `SHARE_GROUP_CONFIRMED` | 非空 |
| **只带 `assignmentGroupId`** | **false** | `ASSIGNMENT_CONFLICT` | `null` |
### 4.3 分页字段名
`candidates` 的分页参数是 **`vehiclePage` / `vehiclePageSize` / `driverPage` / `driverPageSize`**,车辆与司机各自分页;**没有 `pageNum` / `pageSize`**。传错名字不会报错,会静默按默认值(第 1 页、20 条)返回。
---
## 五、数据库行为
**无任何数据库写入。** 两个端点均为只读咨询:
- 读 `fleet_assignment`(重叠占用)、`fleet_group_dispatch_share_group` + `fleet_group_dispatch_share_member`(共用关系授权)、`fleet_vehicle` / `fleet_driver`(候选资源)。
- 无建表、无改表、无 Flyway 脚本、无索引变更。
- 不产生 outbox 消息、不发事件。
---
## 六、边界行为
1. **共用关系逐日判定**:重叠期里只要有一天没被授权,这一对就回到原规则判,不整段放行。
2. **主体身份为空即新建场景**:不传 `excludeAssignmentId` 时一律不享受共用授权,与写口 `create` 同口径 —— 读口不得比写口松,否则会造出「列表能选、写口拒」。
3. **共用关系优先于同城衔接**:两者同时成立时 `reasonCode` 报 `SHARE_GROUP_CONFIRMED`,因为那是人工确认过的、更强也更该被看见的依据。
4. **取严失效方向**:占用行日期不完整时不参与判定、按阻断处理。
5. **`shareEligible` 与 `blocking` 恒互为反面**,不存在两者同真或同假的响应。
## 六.6、修改前后对比
### 字段级对比
| 接口 | 字段 | 改前 | 改后 |
|---|---|---|---|
| `candidates` | `conflicts[].cityJunctionShareCandidate` | `同城衔接 \|\| 已确认共用关系` | **`同城衔接`**(回到 #5302 原义) |
| `candidates` | `conflicts[].shareEligible` | **不存在** | **新增**:`同城衔接 \|\| 已确认共用关系`,恒 `= !blocking` |
| `candidates` | 其余所有字段 | — | 未变 |
| `precheck` | `conflicts[].cityJunctionShareCandidate` | `同城衔接` | `同城衔接`(**未变**) |
| `precheck` | 其余所有字段 | — | 未变 |
### 行为级对比
同一对「跨城重叠 + 已确认共用关系」的派单:
| 场景 | 读口 | 改前 `cityJunctionShareCandidate` | 改后 `cityJunctionShareCandidate` | 改后 `shareEligible` |
|---|---|---|---|---|
| 跨城 + 有共用关系 | `candidates` | `true` ❌ | **`false`** ✅ | `true` |
| 跨城 + 有共用关系 | `precheck` | `false` | `false` ✅ | *(本侧无此列,`!blocking`=`true`)* |
| 同城衔接 | `candidates` | `true` | `true` | `true` |
| 跨城 + 无共用关系 | `candidates` | `false` | `false` | `false` |
改前两个读口在第一行给出**相反**的答案;改后两侧同值。
## 六.7、影响评估
| 面 | 影响 |
|---|---|
| **前端(须确认)** | 🔴 若已按 #7444 口径把 `cityJunctionShareCandidate` 当「可不可以选」使用,**必须改读 `shareEligible` 或 `blocking`**。若一直按字面含义(同城衔接)使用,或读的是 `selectable` / `blocking` / `reasonCode`,则**无需改动** |
| **旧前端兼容** | 既有字段回到**已发布契约**(#5302 / #6843)的语义,新语义走新字段,未升级的前端不会因本次改动而行为变差 |
| **写口** | 零影响。`create` / `change` / `restore-cancel` 的锁内重校验逻辑一行未动 |
| **判权** | 零影响,未改任何守卫 |
| **数据** | 零影响,无写入、无迁移 |
| **性能** | 零影响,只是把已算出的两个布尔值分别下发,无新增查询 |
| **`#7444` 交接件** | 其中「候选面 `cityJunctionShareCandidate=true` 表示可共享」的表述**已被本单取代**,以本篇为准 |
---
## 七、不影响范围
- `precheck` 的响应形状、字段取值、错误码 —— 一律未变。
- 两个端点的**入参**(字段名、必填性、校验规则)—— 一律未变。
- 路径、方法、网关路由、判权规则 —— 一律未变。
- 派单创建 / 改派 / 取消 / 撤销取消等**所有写接口** —— 一律未变。
- 共用关系的建立、确认、释放链路 —— 一律未变。
- 小程序端 —— 不涉及。
---
## 八、测试环境已验证
- 后端已合并 `dev-v3`(合并提交 `32f87d022`,PR [#8027](https://git.1814.love:8443/wx/HL/pulls/8027)),`hl-fleet-service` 滚动部署 TEST,8087/8187 双实例健康。
- 部署记录:`[RECORD] hl-fleet-service <- dev-v3 @ 32f87d022`,`BUILD SUCCESS`,两实例各 7s 起健康。
- 定向单测 `AssignmentConverterTest,AssignmentControllerTest,AssignmentCandidateServiceTest,AssignmentServiceTest`:**677 tests,0 failures,0 errors,0 skipped**;`spotless:check` 通过。
- 变异验证:把 `.cityJunctionShareCandidate(junction)` 改回 `(shareable)`,`toConflict_sameShareGroup_...` 如期变红(已还原)。
- **真实网关实测**(`api.test.1814.love:9443`,共用关系 `359563681060753408`,VEHICLE / 2026-12-28,车 `蒙C04E04`):
- `candidates` 带 `excludeAssignmentId` + `assignmentGroupId` → `selectable=true`、冲突项 `cityJunctionShareCandidate=false` / `shareEligible=true` / `blocking=false` / `SHARE_GROUP_CONFIRMED` / `shareGroupId=359563681060753408`;
- `precheck` 同一车/日/城市/主体 → `cityJunctionShareCandidate=false` / `blocking=false` / `SHARE_GROUP_CONFIRMED` —— **两个读口同名字段同值,分叉消除**;
- **只带 `assignmentGroupId`** → `selectable=false` / `ASSIGNMENT_CONFLICT` / `shareGroupId=null`,且冲突项里**出现调用方自己那条派单**(「调用方与自己冲突」的直接证据);
- 同城衔接对照(12-28~12-29,两端海拉尔)→ 同一次响应里取到 `cityJunction=true`+`shareEligible=true`(`CITY_JUNCTION_SHAREABLE`)与 `cityJunction=false`+`shareEligible=false`(`ASSIGNMENT_CONFLICT`)两条,**证明两列相互独立**;
- 三种取值组合 `(true,true)` / `(false,false)` / `(false,true)` 全部取到。
- Swagger 实测(`:8087/v2/api-docs`):两端点 description 含「#8004 AC-2」契约说明;`候选资源冲突` 定义含 `shareEligible`;`派单候选资源查询` 定义含「单独传它不足以排除自身占用」。
- 全程只读,无任何业务写入,未改动 TEST 数据。
---
## 十、相关文档
- 工单 [#8004](https://git.1814.love:8443/wx/HL/issues/8004)(本单,含两处问题的原始取证)
- 工单 [#7444](https://git.1814.love:8443/wx/HL/issues/7444)(团期配车共用关系,分叉的来源;其 AC-9 ② 的字段表述以本篇为准)
- changelog `#5302`(2026-07-28)`changelogs-v2/2026-07/28_5302_派车档期按完整组判定同城衔接-修改接口-管理后台.md` —— `cityJunctionShareCandidate` 的原始已发布语义
- changelog `#6843`(2026-08-31)`changelogs-v2/2026-08/31_6843_供应商暂停合作联动停用车队与派单拦截-修改接口-管理后台.md`
---
## 关联 / 联系人
### 链接
- Issue: [#8004](https://git.1814.love:8443/wx/HL/issues/8004)
- PR: [#8027](https://git.1814.love:8443/wx/HL/pulls/8027)
- 合并提交: `32f87d022`
### 联系人
- 后端: jw
- 前端: mmg