diff --git a/changelogs-v2/2026-09/20_8004_共用关系两个读口契约收口拆出shareEligible-修改接口-管理后台.md b/changelogs-v2/2026-09/20_8004_共用关系两个读口契约收口拆出shareEligible-修改接口-管理后台.md new file mode 100644 index 00000000..357c6516 --- /dev/null +++ b/changelogs-v2/2026-09/20_8004_共用关系两个读口契约收口拆出shareEligible-修改接口-管理后台.md @@ -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` + +#### 使用场景 + +派车弹窗 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` + +#### 使用场景 + +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