From 9195d0dcd43b426214b0af432328864eacca5860 Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Mon, 28 Sep 2026 07:22:23 +0800 Subject: [PATCH] =?UTF-8?q?docs(changelog):=20#8435=20=E5=A4=A7=E4=BA=A4?= =?UTF-8?q?=E9=80=9A=E6=94=B9=E5=8F=98=E6=97=B6=E6=8E=A5=E9=80=81=E9=9C=80?= =?UTF-8?q?=E6=B1=82=E8=BD=A6=E5=8A=A1=E7=A1=AE=E8=AE=A4=E4=B8=8E=E7=9C=8B?= =?UTF-8?q?=E6=9D=BF=E6=A0=87=E8=AE=B0=EF=BC=88=E6=96=B0=E5=A2=9E=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=EF=BC=8C=E7=AE=A1=E7=90=86=E5=90=8E=E5=8F=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 fleet 车务确认接送变更端点与看板待确认标记的前端交接件, 附测试服第二轮实测(核心/定制/团期/小程序各路径)。 Refs wx/HL#8435 Co-Authored-By: Claude Opus 5.5 (1M context) --- ...¶接送需求车务确认与看板标记-新增接口-管理后台.md | 781 ++++++++++++++++++ 1 file changed, 781 insertions(+) create mode 100644 changelogs-v2/2026-09/28_8435_大交通改变时接送需求车务确认与看板标记-新增接口-管理后台.md diff --git a/changelogs-v2/2026-09/28_8435_大交通改变时接送需求车务确认与看板标记-新增接口-管理后台.md b/changelogs-v2/2026-09/28_8435_大交通改变时接送需求车务确认与看板标记-新增接口-管理后台.md new file mode 100644 index 00000000..edf223e2 --- /dev/null +++ b/changelogs-v2/2026-09/28_8435_大交通改变时接送需求车务确认与看板标记-新增接口-管理后台.md @@ -0,0 +1,781 @@ +--- +schema: "hl-changelog/v2" +ticket: "8435" +title: "大交通改变时接送需求车务确认与看板标记;删除接口改拒绝;整批替换新增拒绝" +consumer: "admin" +author: "wx(GIT)" +change_type: "新增接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "" +updated_at: "2026-09-28" +base: "dev-v3" +--- + +# fleet/order-v3: 大交通改变时接送需求车务确认与看板标记 + +**服务**: hl-fleet-service / hl-order-service-v3 +**PR**: `#8458`(已合入 `dev-v3`,squash `f75984d1d`)、`#8459`(接送变更时间线写入修正 + 删除口 IT 同步,squash `74b1df389`) +**Issue**: #8435 +**日期**: 2026-09-28 +**影响范围**: 管理后台车务看板列表、订单详情、车务确认接送变更;删除与整批替换四个接口;接送需求(TRANSFER)有新增批次时向车务推送提醒 + +--- + +## ⚠️ 关键变化 + +**大交通批次新增、修改或删除时,接送需求(TRANSFER)版本处理方式变更。** + +改前: +- 系统直接自动换版,客人改了接机时间车务立刻用新日期派车。 + +改后: +- **车务已接手**(接送需求已处理中或已完成、已记录配置人,或 fleet 已有派车行,三者任一;只打开看板详情**不算**接手,详情 GET 只推进行程用车需求,见「详情打开有副作用」):订单进入「待车务确认」,向车务推站内信、看板列表与详情出现「待确认」标记;车务在看板详情确认后才换版。同日只改时刻时确认不换版,只刷新确认基线。 +- **车务尚未接手**:系统直接换版(原有行为)。 +- **两个方向都改成不需要接送**:未接手的直接失活;已接手的同样先进「待车务确认」,车务确认后失活(`outcome=DEACTIVATED`)。 +- **车务首页工作台(`GET /admin/profile/dashboard`)的待办数不含待确认接送变更**,已知缺口 #8457;车务发现待确认单的入口是站内信与看板列表标记。 + +前端影响:看板列表新增「待确认」标记(三态:true=待确认、false=无待确认、null=团期订单不适用);订单详情新增「接送变更」对象展示原→新对比和指纹;确认按钮与删除提示;删除接口改返回 581127(不能删)、整批替换改返回 581127(清空方向时不能删)。 + +**团期订单走不同确认流程**(管理员放行,非车务确认),确认接口对团期单恒返 809011。 + +--- + +## 一、背景 + +接送机需求(TRANSFER)从大交通批次派生,但大交通写入口(管理端、小程序)一个都不调需求域,导致客人改了接机日期车务收不到信号。测试服实测:改了大交通,看板上的接送需求仍是旧版本,司机按旧日期出,客人落地没人接。 + +本单要做的: +1. 大交通不能删除(改为拒绝 581127),不需要接送改用标记不需要。 +2. 核心/定制单改大交通后,若车务已派车则等待车务确认后再换版;车务尚未派车则直接换版。同日仅改时刻时也要车务确认但不换版。 +3. 团期单由系统出新版本待团期管理员放行(非车务确认)。 +4. 派生口径排除不需要接送的批次;两个方向都不需要时失活。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 车务确认接送变更 | POST | `/admin/fleet/board/orders/{orderId}/transfer-change/confirm` | 新增 | 车务确认看板详情中的接送变更,触发需求换版或基线刷新 | +| 2 | 车务看板列表 | GET | `/admin/fleet/board/orders` | 修改 | 新增 `transferChangePending` 字段标记是否有待车务确认的接送变更 | +| 3 | 车务看板订单详情 | GET | `/admin/fleet/board/orders/{orderId}` | 修改 | 新增 `transferChange` 对象展示接送变更前后对比、指纹、确认状态 | +| 4 | 删除大交通批次(管理端;另有 3 个小程序变体同口径) | POST | `/v3/admin/order/{id}/transport-plan/{planId}/delete` | 修改 | 改为一律拒绝,返回 581127(不能删除) | +| 5 | 整批替换大交通(管理端;另有 3 个小程序变体同口径) | POST | `/v3/admin/order/{id}/transport-plan/batch` | 修改 | 新增拒绝条件:某方向改前有批次、改后为空时返回 581127 | + +--- + +## 三、接口详情 + +### 1. 车务确认接送变更 `POST /admin/fleet/board/orders/{orderId}/transfer-change/confirm` + +**VO**: `BoardTransferChangeConfirmReqVO` → `BoardTransferChangeConfirmRespVO` + +#### 使用场景 + +车务打开看板订单详情,看到「待车务确认」标记与接送变更摘要(原→新),点击确认按钮。车务需先与定制师确认该次改期是否可调度,然后在看板确认一次。确认后需求换版或标记失活,后续派车或取消的对象都是新版本。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId | Path | Long | 是 | 订单主键 | 看板详情页订单 ID | +| expectedFingerprint | Body | String | 是 | 最长 64 字符 | 车务确认时看到的当前接送计划指纹(取自详情 `transferChange.currentFingerprint`);乐观并发控制,防止车务确认前客人又改了一次 | +| remark | Body | String | 否 | 最长 500 字 | 确认备注,例如「已电话核实,改派 18:40 接机」 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| orderId | Long | 订单 ID | +| outcome | String | 确认结果:`NOTHING_PENDING`=本来就没有待确认变更;`BASELINE_REFRESHED`=只刷新确认基线(同日仅改时刻);`VERSION_SWITCHED`=已换出新版接送需求(日期变了);`DEACTIVATED`=两个方向都不需要接送,接送需求已失活 | +| requirementId | Long | 确认后生效的接送需求 ID;DEACTIVATED 时为被失活的那条;无接送需求(含开关关闭)时为 null | +| requirementVersion | Integer | 确认后生效的接送需求版本号;DEACTIVATED 时为被失活那条的版本号;无接送需求时为 null | +| confirmedFingerprint | String | 本次确认落库的接送计划指纹(SHA-256 十六进制) | +| confirmedAt | LocalDateTime | 确认时间 | +| teamNo | String | 团号(订单 team_no;为空时返回 null) | + +#### 请求示例 + +```http +POST /admin/fleet/board/orders/2104055021303939073/transfer-change/confirm +Authorization: Bearer <车务账号 token> +Content-Type: application/json + +{ + "expectedFingerprint": "3f0c1b2a9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a", + "remark": "已电话核实,改派 18:40 接机" +} +``` + +#### 响应示例 + +**情景 1:日期变化,需求换版** + +```json +{ + "code": 200, + "message": "成功", + "data": { + "orderId": "2104055021303939073", + "outcome": "VERSION_SWITCHED", + "requirementId": "2104059226781532162", + "requirementVersion": 2, + "confirmedFingerprint": "3f0c1b2a9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a", + "confirmedAt": "2026-09-28 10:30:00", + "teamNo": "HL20261001A" + }, + "success": true +} +``` + +**情景 2:仅改时刻,只刷新基线** + +```json +{ + "code": 200, + "message": "成功", + "data": { + "orderId": "2104055021303939073", + "outcome": "BASELINE_REFRESHED", + "requirementId": "2104059226781532162", + "requirementVersion": 1, + "confirmedFingerprint": "9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b", + "confirmedAt": "2026-09-28 10:31:00", + "teamNo": "HL20261001A" + }, + "success": true +} +``` + +**情景 3:无待确认(幂等)** + +```json +{ + "code": 200, + "message": "成功", + "data": { + "orderId": "2104055021303939073", + "outcome": "NOTHING_PENDING", + "requirementId": null, + "requirementVersion": null, + "confirmedFingerprint": "3f0c1b2a9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a", + "confirmedAt": "2026-09-28 10:32:00", + "teamNo": "HL20261001A" + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +- 团期订单调此接口:恒返 809011,不涉及车务确认。 +- 无 TRANSFER 接送需求(`hl.order.requirement.transfer-kind-submit-enabled` 开关关闭,或该订单从未产生过 TRANSFER 版本):`outcome=BASELINE_REFRESHED`,requirementId/requirementVersion 为 null。 +- 有活跃 TRANSFER 接送需求但两个方向都标记为不需要接送:`outcome=DEACTIVATED`,requirementId/requirementVersion 为**被失活的那条**需求的 id/版本号(非 null)。 + +#### 错误响应 + +```json +{ + "code": 809010, + "message": "接送变更已再次修改,请刷新后重新确认", + "data": null, + "success": false +} +``` + +| 错误码 | 触发条件 | +|---|---| +| 809010 | 接送变更已再次修改(车务确认时传入的 expectedFingerprint 与当前指纹不符,或本次确认过程中基线被并发写入);需要刷新看板详情重新拿指纹再确认 | +| 809011 | 团期订单的接送变更走团期放行,不经车务确认 | + +#### 业务边界 + +- 权限校验:网关 `JwtAuthFilter` 对 `/admin/fleet/**` 路径的 VEHICLE_MANAGER / SUPER_ADMIN 角色门禁,服务层不重复校验。 +- 幂等性:同一指纹重复确认返回 `NOTHING_PENDING`,不重复写入。 +- 并发控制:乐观锁机制,expectedFingerprint 不符时拒绝,避免「车务确认了一份自己没看过的计划」。 +- 确认后生效:需求立即生效,之后的派车/改派基于新版本;已发出的站内信收不回,但标记消失、不再催促。 +- 与团期确认区别:该接口**只用于散客/定制单**;团期订单直接返 809011,应走团期管理员的 `POST /v3/admin/order/group-batch/{groupBatchId}/requirement/transfer/batch-confirm` 放行接口。 + +--- + +### 2. 车务看板列表 `GET /admin/fleet/board/orders` + +**VO**: `BoardOrderRecordVO`(列表行对象新增字段) + +#### 使用场景 + +车务打开看板列表,按 `transferChangePending` 快速看出哪些订单等待自己确认接送变更,切进详情页核对与确认。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| 与改前相同 | — | — | — | — | 列表查询参数、分页、排序不变 | + +#### 出参字段表 + +只列新增字段。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| transferChangePending | Boolean | **三态字段**:true=有待车务确认的接送变更;false=无待确认;null=团期订单不适用或订单上下文取不到。同一订单的多张卡片(多条需求并存时)取值相同;不因需求类型而差异 | + +#### 请求示例 + +```http +GET /admin/fleet/board/orders?pageNo=1&pageSize=20 +Authorization: Bearer <车务账号 token> +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "list": [ + { + "id": "HL20260928001", + "orderNo": "HL20260928001", + "teamNo": "26-0001", + "orderId": "2104055021303939073", + "customerName": "赵先生", + "startDate": "2026-10-05", + "endDate": "2026-10-10", + "transferChangePending": true, + "assignmentStatus": "assigned", + "currentVehiclePlate": "蒙A-88888" + }, + { + "id": "HL20260928002", + "orderNo": "HL20260928002", + "teamNo": "26-0002", + "orderId": "2104055021303939074", + "customerName": "王女士", + "startDate": "2026-10-12", + "endDate": "2026-10-15", + "transferChangePending": false, + "assignmentStatus": "unassigned", + "currentVehiclePlate": null + }, + { + "id": "HL20260928003", + "orderNo": "HL20260928003", + "teamNo": null, + "orderId": "2104055021303939075", + "customerName": "李先生", + "startDate": "2026-09-30", + "endDate": "2026-10-05", + "transferChangePending": null, + "groupBatchId": "2104000000000000001", + "assignmentStatus": "assigned" + } + ], + "total": 100, + "pageNo": 1, + "pageSize": 20 + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +- 该车务无待处理订单:返回空列表 `[]`,`transferChangePending` 字段在所有行都下发(不省略)。 +- 订单上下文取不到(order-v3 不可达):`transferChangePending=null`(不知道 ≠ 没有,不下发 false 误判为「无待确认」)。 + +#### 错误响应 + +```json +{ + "code": 401, + "message": "权限不足", + "data": null, + "success": false +} +``` + +| 错误码 | 触发条件 | +|---|---| +| 401 | 无 VEHICLE_MANAGER 权限 | +| 422 | 参数校验失败(例如分页参数非法) | + +#### 业务边界 + +- 团期订单与散客单同时返回;前端据 `transferChangePending` 三态区分是否展示「待确认」标记。 +- null 表示「该订单可能有变更但无法判断」,不等同于 false;前端应区分对待,例如不在"有待确认"数据表中统计 null 行。 +- 待确认标记不按需求类型筛选(TRANSFER 和 TRAVEL 并存时、或仅有 TRANSFER 时同样下发该标记);前端看板只有一张卡(需求并存已投射到单卡)。 + +--- + +### 3. 车务看板订单详情 `GET /admin/fleet/board/orders/{orderId}` + +**VO**: `BoardOrderDetailVO`(新增 `transferChange` 对象) + +#### 使用场景 + +车务打开看板单卡详情,看到「接送变更」一行展示「原→新」对比、最近一次确认时刻与来源、当前指纹。根据摘要判断能否调度,取 `currentFingerprint` 作为确认接口的 expectedFingerprint 参数。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId | Path | Long | 是 | 订单主键 | 看板行里的订单 ID | + +#### 出参字段表 + +只列新增或改动字段。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| transferChange | Object | **仅散客/定制单下发;团期订单为 null**;对象内容见下表 | +| transferChange.pending | Boolean | 是否有待车务确认的接送变更;改回原样时标记自动消失 | +| transferChange.currentFingerprint | String | 当前接送计划指纹(SHA-256 十六进制);确认接口的 expectedFingerprint 须回传它 | +| transferChange.confirmedFingerprint | String | 车务上次确认时的接送计划指纹 | +| transferChange.confirmedAt | LocalDateTime | 车务上次确认时间;历史订单首次补建基线时为补建时刻 | +| transferChange.confirmedSource | String | 上次确认来源:`INIT`=初建、`FLEET_CONFIRM`=车务手工确认、`AUTO_SWITCH`=系统自动换版、`GROUP_SYSTEM`=团期系统换版、`LEGACY_BACKFILL`=历史数据补建 | +| transferChange.lastChangedAt | LocalDateTime | 接送计划最近一次实际变化的时间;改内容时刷新,改回原样不刷新 | +| transferChange.confirmedSegments | Array | 上次确认时的接送段列表(方向、日期、时刻、是否需要) | +| transferChange.currentSegments | Array | 当前接送段列表(同形) | +| transferChange.changeSummary | String | 变更摘要文案,车务一眼看懂改了什么,例如「到达:07-20 15:10 → 07-20 18:40」 | + +#### 请求示例 + +```http +GET /admin/fleet/board/orders/2104055021303939073 +Authorization: Bearer <车务账号 token> +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "id": "HL20260928001", + "orderNo": "HL20260928001", + "teamNo": "26-0001", + "customerName": "赵先生", + "startDate": "2026-10-05", + "endDate": "2026-10-10", + "transferChange": { + "pending": true, + "currentFingerprint": "3f0c1b2a9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a", + "confirmedFingerprint": "9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b", + "confirmedAt": "2026-09-27 10:00:00", + "confirmedSource": "AUTO_SWITCH", + "lastChangedAt": "2026-09-28 09:15:00", + "confirmedSegments": [ + { + "direction": "ARRIVAL", + "date": "2026-10-05", + "time": "15:10:00", + "pickupRequired": true + } + ], + "currentSegments": [ + { + "direction": "ARRIVAL", + "date": "2026-10-05", + "time": "18:40:00", + "pickupRequired": true + } + ], + "changeSummary": "到达:07-20 15:10 → 07-20 18:40" + }, + "assignmentStatus": "assigned", + "currentVehiclePlate": "蒙A-88888" + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +- 散客/定制单无待确认变更:`transferChange.pending=false`,但对象仍下发(字段完整),前端据 pending 判断是否展示标记。 +- 团期订单:`transferChange=null`(整个对象缺席,前端不展示该段信息)。 +- 订单上下文取不到(order-v3 不可达):`transferChange=null`。 + +#### 错误响应 + +```json +{ + "code": 605311, + "message": "当前需求存在多个不透明派车方案代际,请联系车务核对派车方案后重试", + "data": null, + "success": false +} +``` + +| 错误码 | 触发条件 | +|---|---| +| 605311 | 需求已定稿派车行出现多个方案代际或无代际行与有代际行混在一起;看板不猜当前代际,拒绝打开详情(HTTP 200 的业务异常) | + +#### 业务边界 + +- **详情打开有副作用**:GET 会把 TRAVEL 需求从 PENDING 推到 PROCESSING(若尚未处理);接送需求(TRANSFER)不受影响。 +- 指纹来源:指纹由接送计划(大交通批次+时刻)唯一决定,不含金额、旅客姓名、备注等敏感字段。 +- 变更摘要:面向车务人工阅读,文案区分「新增」「删除」「改期」「改时刻」四种操作。 +- 团期订单不走车务确认,故 `transferChange=null`;团期确认由 `POST /v3/admin/order/group-batch/{groupBatchId}/requirement/transfer/batch-confirm` 负责。 +- 同一订单的列表行与详情行的 `transferChangePending` / `pending` 字段取值相同。 + +--- + +### 4. 删除大交通批次 `POST /v3/admin/order/{id}/transport-plan/{planId}/delete` + +**VO**: `(无请求体,路径参数 id/planId) → Boolean` + +#### 使用场景 + +前端大交通批次列表的「删除」按钮调用。改前删除成功返回 `200 true`;改后一律拒绝返回 581127,前端应将「删除」入口替换为「标记为不需要接送」(调用批次 edit 接口把 `pickupRequired` 改为 `false`)。同一改动同时对以下 3 个小程序变体生效(方法、鉴权各自独立,拒绝口径一致;三者返回类型为 `Result`,与管理端 `Result` 不同): + +| 变体 | 方法 | 路径 | +|---|---|---| +| 小程序内部(B2B/后端间调用) | DELETE | `/v3/internal/mp/order/arrival/plan/{planId}` | +| 小程序客户端 | DELETE | `/mp/order/{orderId}/arrival/plan/{planId}` | +| 小程序 v3 客户端 | DELETE | `/mp/v3/order/arrival/plan/{planId}` | + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | 是 | 订单主键 | 管理端订单 ID(mp 三个变体路径参数名为 orderId,含义相同) | +| planId | Path | Long | 是 | 批次主键 | 待删除的大交通批次 ID | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| data | Boolean | 改动后该接口对存在且归属正确的批次恒抛 581127,不会返回 200 成功;`Boolean` 是编译期声明的类型,非本次变更后的真实可观察返回值 | + +#### 请求示例 + +```http +POST /v3/admin/order/2104055021303939073/transport-plan/2104059226781532162/delete +Authorization: Bearer <管理端 token> +``` + +#### 响应示例 + +改前(本次变更后不会再出现,仅供理解改前的返回结构): + +```json +{ + "code": 200, + "message": "成功", + "data": true, + "success": true +} +``` + +#### 空数据 / 降级响应 + +- 批次已被软删或不属于该订单:返回既有的「不存在」码(581144,见下方错误响应),不因本次变更而改码。 + +#### 错误响应 + +```json +{ + "code": 581127, + "message": "到达大交通批次不能删除;不需要接送请把该批次改为不需要接送", + "data": null, + "success": false +} +``` + +| 错误码 | 触发条件 | +|---|---| +| 581127 | 批次存在且归属正确,一律拒绝删除;`message` 的方向文案随批次的 `direction` 变化(`到达`/`离开`) | +| 581144 | 批次不存在或不属于该订单(存在性检查先于 581127) | +| 581122 | 订单不属于当前用户(归属检查先于 581127) | + +#### 业务边界 + +- 先经存在性检查(581144)与订单归属检查(581122),再抛 581127;对不存在或别人的批次不会因为删除被拒而改码,避免用 581127 反向探测 planId 是否存在。 +- 正向对照:edit 接口不受本次变更影响,仍可修改批次的日期、时刻、是否需要接送等字段,返回 200。 +- 管理端返回类型为 `Result`,mp 三个变体为 `Result`;本次变更后两者都只会抛异常,不产出正常 data。 + +--- + +### 5. 整批替换大交通 `POST /v3/admin/order/{id}/transport-plan/batch` + +**VO**: `TransportPlanBatchReqVO` → `TransportPlanBatchRespVO` + +#### 使用场景 + +前端「大交通批次整批编辑」保存时调用,全量覆盖该订单(或该方向)已有批次。改前某方向减少批次或清空都放行;改后某方向改前有批次、改后为空时拒绝 581127,方向内减少(但不清空)照常放行。同一拒绝条件同时对以下 3 个小程序变体生效(口径见业务边界的按方向/全单区别): + +| 变体 | 方法 | 路径 | +|---|---|---| +| 小程序内部整批 | POST | `/v3/internal/mp/order/arrival/{orderId}/batch` | +| 小程序客户端整批 | POST | `/mp/order/{orderId}/arrival/batch` | +| 小程序 v3 整批 | POST | `/mp/v3/order/arrival/{orderId}/batch` | + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | 是 | 订单主键 | 管理端整批替换的订单 ID | +| plans | Body | Array | 是 | 非空,最多 30 条 | 本次全量覆盖的批次列表(`TransportPlanReqVO`),每项含 direction/transportType/travelerIds 等既有字段,本次未变更 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| deletedCount | Integer | 本次软删的旧批次数 | +| createdCount | Integer | 本次新增的批次数 | +| plans | Array | 替换后该订单全部批次(`TransportPlanVO`),既有字段,本次未变更 | + +#### 请求示例 + +```json +{ + "plans": [ + { + "direction": "ARRIVAL", + "transportType": "FLIGHT", + "transportNo": "CA1234", + "arriveTime": "2026-10-05 18:40:00", + "travelerIds": [2104055021303939080], + "pickupRequired": true + } + ] +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "deletedCount": 1, + "createdCount": 1, + "plans": [] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +- 该订单写前没有任何批次:跳过 581127 校验(无方向可能被清空),直接按请求整批新建。 + +#### 错误响应 + +```json +{ + "code": 581127, + "message": "到达大交通批次不能删除;不需要接送请把该批次改为不需要接送", + "data": null, + "success": false +} +``` + +| 错误码 | 触发条件 | +|---|---| +| 581127 | 某方向写前有批次、写后一条都没有(方向内减少但不清空不触发);写前先做统计,判定通过后再执行软删+新建,判定不通过时零写入 | + +#### 业务边界 + +- 管理端整批替换全量覆盖两个方向,两个方向都参与「改前有改后无」判定;mp 三个变体按方向整批,只检查本次请求覆盖到的那个方向。 +- 方向内减少但不清空:例如到达从两条减到一条,允许。 +- 整个方向变空:拒绝,改用批次 edit 接口把 `pickupRequired` 标记为不需要接送这一专门途径,而不是清空该方向的所有批次。 +- 判定发生在第一条写(软删旧批次)之前,拒绝时不依赖事务回滚兜底,零写入。 +- 小程序 v3 整批(`/mp/v3/order/arrival/{orderId}/batch`)与小程序内部整批,`plans` 必须是同一方向、长度 2~20 条(入参校验层 `@Size(min=2, max=20)`)。空数组或只有 1 条会返回 400「内容长度不符合要求…」,走不到 581127;所以小程序「清空方向」表现为 400,方向内减少最小形态为 3 条减到 2 条。 + +--- + +## 四、契约约束与正确调用方式 + +### 确认接口的指纹碰撞处理 + +| 场景 | 操作 | 结果 | +|------|------|------| +| ✅ 正常确认 | 拿当前指纹 → 改大交通生效 → GET 详情 → 取新 currentFingerprint → 确认 | outcome=VERSION_SWITCHED / BASELINE_REFRESHED / DEACTIVATED | +| ❌ 指纹过期 | 拿指纹 F1 → 改大交通 → 再改一次 → 带 F1 确认 | 809010 需要刷新重来 | +| ✅ 幂等 | 成功确认一次 → 同样参数再发一次 | outcome=NOTHING_PENDING(已确认状态不变) | + +### 删除与整批替换的分界线 + +| 操作 | 请求体 | 结果 | +|------|--------|------| +| ✅ 删单条(方向还有其他) | 到达删除最后一条,但方向还有备选 | 200 | +| ✅ 减少(整批时) | 到达从 [A,B] 改成 [A] | 200 | +| ❌ 清空方向(整批时) | 到达从 [A,B] 改成 [] | 581127 | +| ✅ 标记不需要 | 某方向的需要接送标志改为 false | 200(推荐用此而非删) | + +### 既有接口新增可见错误码 581128 + +本单落地后,以下 2 个既有接口在满足条件时会**新增**抛出 581128(`大交通接送安排刚被修改,请刷新后重试`,与确认接口的 809010 同属乐观并发拒绝,但触发场景不同);此前这两个接口不会返回该码: + +| 方法 | 路径 | 触发条件 | +|---|---|---| +| PUT | `/v3/admin/order/{id}/vehicle-requirement` | 该订单首次提交 TRANSFER 用车需求(此前该订单从未有 TRANSFER 版本),且提交过程中大交通被并发写入,导致基线快照与锁定读不一致 | +| POST | `/v3/admin/order/{id}/adjustment/submit` | 调整单携带 transferRequirement 且该订单还没有 TRANSFER 版本,并发条件同上 | + +```json +{ + "code": 581128, + "message": "大交通接送安排刚被修改,请刷新后重试", + "data": null, + "success": false +} +``` + +以下写口**不会**返回 581128(用车需求换版的编排在大交通写事务提交之后、同一请求线程里执行,编排失败只记日志、不影响本次保存的 HTTP 响应;撞到基线并发冲突时会自动重跑编排,重跑仍失败只做降级处理,不会把 581128 抛给保存大交通的调用方): + +- 大交通新增、编辑、整批替换写口(管理端 add/edit/batch 三个 + 小程序端对应三个)。 +- 车务确认接送变更接口(本单新增,`POST /admin/fleet/board/orders/{orderId}/transfer-change/confirm`):该接口自身的并发拒绝码是 809010,不是 581128。 + +--- + +## 五、数据库行为 + +新表 `order_transfer_change_baseline`(order-v3 侧)记录接送计划指纹与确认状态,支撑「待确认」判定与确认基线回填;详见工单中的 Flyway 段。 + +--- + +## 六、边界行为 + +### 已知限制 + +1. **团期与散客串流**:同一订单在不同团期阶段转换时(例如从非团转入团期),确认接口的行为按当前订单状态判定(团期直接返 809011)。 +2. **确认前并发改变**:车务看板详情与小程序同时改大交通时,指纹会实时变化;车务确认前客人继续改则拿到 809010 并需重新刷新,符合业务预期。 +3. **站内信一次性**:首次进入待确认时发一条站内信,后续改变不再发(防刷屏),确认后清空发送记录,再次待确认时会重新发;已发的站内信收不回。 +4. **历史订单**:上线前已改过大交通的订单,如果尚无基线行则首次碰触时补建 `LEGACY_BACKFILL` 行,之后开始跟踪。二期未上生产,此特例只影响测试服存量。 + +--- + +## 六.5、枚举 / 数据字典 + +### outcome(车务确认接送变更接口的确认结果) + +**所属字段**: `BoardTransferChangeConfirmRespVO.outcome` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `NOTHING_PENDING` | 无待确认 | 本来就没有待确认变更(含幂等重复确认) | +| `BASELINE_REFRESHED` | 仅刷新基线 | 同日仅改时刻,或无活跃 TRANSFER 接送需求(开关关闭 / 该订单从未产生过 TRANSFER 版本) | +| `VERSION_SWITCHED` | 已换版 | 日期变化,接送需求已换出新版本 | +| `DEACTIVATED` | 已失活 | 有活跃 TRANSFER 接送需求,但两个方向都改为不需要接送,该需求被失活 | + +### confirmedSource(接送变更基线的上次确认来源) + +**所属字段**: `transferChange.confirmedSource` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `INIT` | 初建 | 订单首次提交 TRANSFER 用车需求时系统建立的基线 | +| `FLEET_CONFIRM` | 车务手工确认 | 车务在看板详情点击确认按钮 | +| `AUTO_SWITCH` | 系统自动换版 | 车务尚未派车时,系统检测到大交通变化直接换版 | +| `GROUP_SYSTEM` | 团期系统换版 | 团期订单由系统出新版本,走团期管理员放行 | +| `LEGACY_BACKFILL` | 历史数据补建 | 上线前已改过大交通、尚无基线行的订单,首次碰触时补建 | + +### transferChangePending(看板列表/详情的待确认三态标记) + +**所属字段**: `BoardOrderRecordVO.transferChangePending` / `transferChange.pending` | **类型**: `Boolean` + +| 值 | 说明 | +|----|------| +| `true` | 有待车务确认的接送变更 | +| `false` | 无待确认(列表:查询正常且确无待确认;详情:`transferChange` 对象仍下发,只是 `pending=false`) | +| `null` | 仅列表接口出现:团期订单不适用,或订单上下文取不到(order-v3 不可达);详情接口对应场景是整个 `transferChange` 对象为 `null`,不是字段值为 null | + +### direction(大交通批次方向,581127 提示文案占位符来源) + +**所属字段**: `transferChange.confirmedSegments[].direction` / `currentSegments[].direction`;同时是 581127 错误消息 `{0}` 占位符的取值来源 | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `ARRIVAL` | 到达 | 未知/脏方向值时占位符退化为空串,错误提示变成不带方向的通用提示 | +| `DEPARTURE` | 离开 | 同上 | + +--- + +## 七、不影响范围 + +- 看板列表的排序、分页、其他筛选条件:不变。 +- 看板详情的 TRAVEL 需求相关字段:逻辑不变(接送需求变更不影响行程用车)。 +- 既有的需求级确认接口 `POST /admin/fleet/assignments/requirements/{id}/confirm`:不变。 +- 删除接口的权限、存在性校验顺序:不变,只是末尾多了一次判定。 +- 小程序客户端行为:管理端删除改拒后,小程序原样透传 581127,小程序代码不做改动。 +- 大交通新增/编辑/整批替换写口(管理端 3 个 + 小程序端 3 个)本身的成功响应结构不变:接送需求换版编排在提交后异步执行,不影响本次保存的 HTTP 响应,也不会让这些写口新增返回 581128(详见「四、契约约束与正确调用方式」)。 + +--- + +## 八、测试环境已验证 + +部署:测试服 `hl-order-service-v3` 于 2026-09-28 05:17:10(CST)滚到 `dev-v3 74b1df389`(含 #8459);`hl-fleet-service` / `hl-user-service` 为 `f75984d1d`(#8458)。两个实例 8186 / 8086 分别于 05:16:52 / 05:17:07 启动完成,启动后 ERROR 0 行。 + +deploy-status(部署前 05:15:24 / 部署后 05:17:19,已去掉操作人列): + +``` +部署前: + hl-order-service-v3 dev-v3 f75984d1d 1/Y 2026-09-28 03:00:26 ok + hl-fleet-service dev-v3 f75984d1d 1/N 2026-09-28 03:01:30 ok + hl-user-service dev-v3 f75984d1d 1/N 2026-09-28 02:58:35 ok + +部署后: + hl-order-service-v3 dev-v3 74b1df389 0/N 2026-09-28 05:16:36 ok + hl-fleet-service dev-v3 f75984d1d 1/N 2026-09-28 03:01:30 ok + hl-user-service dev-v3 f75984d1d 1/N 2026-09-28 02:58:35 ok +``` + +逐项实测(经测试服网关 + 只读 SQL + 服务日志取证,全部为自建测试数据): + +| 场景 | 调用 | 实测结果 | +|---|---|---| +| 1. 车务未接手,改为不需要接送 | 不派车、不开看板详情,直接改大交通 | 到达改为不需要接送:系统直接换版,时间线「大交通变更,系统自动换版」(05:53:24);两个方向都改为不需要:系统自动失活(05:53:26),无活跃接送需求,不报 809002 | +| 2. 派车推到 DONE,改到达日期 | 派车推到 DONE 后改到达日期 | 看板列表 transferChangePending=true;需求 id/version 不变;站内信按车务角色一次推送一批;时间线「待车务确认用车需求」| +| 3. 只派了一部分车(需求仍 PENDING) | fleet 仅派到达段车,不开看板详情,改离开时刻 | 进入待确认,需求 id/version 不变,时间线「待车务确认用车需求」(05:47:39)| +| 4. 确认带旧指纹 | 拿到当前指纹后再改一次大交通,带旧指纹去确认 | 返 809010「接送变更已再次修改,请刷新后重新确认」,需求不变| +| 5. 确认换版 | 日期有变化,带正确当前指纹确认 | 200,outcome=VERSION_SWITCHED,返回新 requirementId;fleet 生成对账记录,服务日期随行程整体平移| +| 6. 同日只改时刻确认 | 同一天内仅改接送时刻,确认 | 200,outcome=BASELINE_REFRESHED,不出新版本,只刷新确认基线(05:52:15)| +| 7. 到达改为不需要接送后确认 | 改到达为不需要,确认看板 | 返 VERSION_SWITCHED,新版本仅剩离开日;离开也改为不需要后确认返 DEACTIVATED,原派车行被取消,取消来源 `cancel_source=requirement_replaced`,取消文案「用车需求已失效(换版或不再需要接送),按祖先链自动失效」,不是「订单取消」| +| 8. 同一指纹重复确认 | 确认后用同样指纹再确认一次 | 200,outcome=NOTHING_PENDING;改行程回原样待确认标记自动消失| +| 9. 待确认期间连续改两次 | pending 期间不确认就再改一次大交通 | 站内信仅一批;确认后再改会第二批(05:53:50 第一批,05:54:11 确认后 05:54:16 再改第二批)| +| 10. 团期单调车务确认接口 | 团期订单调 POST /admin/fleet/board/orders/{id}/transfer-change/confirm | 返 809011(团期走管理员放行,不走车务确认)| +| 11. 团期单定制师改到达日期 | CUSTOMIZER 改大交通日期 | 出新版本 PENDING_REVIEW;整团整体确认放行后转 PENDING;若整团已确认会清掉确认状态,时间线写「定制师修改需求,需重新整体确认」,再改不重复写| +| 12. 团期单同日只改时刻(需求 PENDING)| CUSTOMIZER 改同日时刻,需求状态 PENDING | 不出新版本,写一条「无需换版,已记为新基线」时间线 | +| 13. 团期单同日只改时刻(需求 DONE)| 派车后(DONE),再改同日时刻 | 出新版本 PENDING_REVIEW,写时间线「团期系统换版,新版待团期审核」(与 AC-11 逻辑一致)| +| 14. 管理端删除批次 | POST /v3/admin/order/{id}/transport-plan/{id}/delete | 返 581127「不能删除」| +| 15. 删除不存在的批次 | 调删除接口传不存在的 planId | 返 581144(存在性检查先于 581127)| +| 16. 小程序删除批次 | DELETE /mp/v3/order/arrival/plan/{planId} | 返 581127| +| 17. 小程序整批替换为空 | POST /mp/v3/order/arrival/{orderId}/batch 传 plans=[] | 返 400,入参校验拦截(Size min=2)| +| 18. 网关与权限 | 网关调 /v3/internal/order/orders/{id}/transfer-change/confirm;定制师角色调确认 | 网关返 403「接口不可访问」;定制师返 403「无权限...切换到车务角色」| +| 19. 时间线与日志 | 部署后改派车、确认等动作 | order_status_log TRANSFER_CHANGE 各路径(自动换版、待确认、确认后、失活)各有记录,to_status 全非空;失活每次只写一条;部署后两实例日志「接送变更时间线写入失败」0 条 | +| 20. 车务待办计数 | 内部 `todo-summary`(VEHICLE_MANAGER),改大交通前、改后、确认后各读一次 | arrangeVehicle 改前 4 → 改后 5 → 确认后 4;同时刻 SQL 口径(pending 的非团期订单数)2 → 3 → 2。计数为全局口径 | +| 21. 定制单(productType=CUSTOM) | 自建私人定制产品下单,派车到 DONE 后改到达时刻,再确认 | 需求 id/version 不变、transferChangePending=true、指纹变化、站内信 +1 批、待办 +1;确认返 VERSION_SWITCHED 后待办回落——与核心单一致 | +| 22. 小程序同方向减少批次 | POST /mp/v3/order/arrival/{orderId}/batch:3 条同方向到达批次整批替换为 2 条 | 200;被去掉的那条软删(整批软删 + 重建)| + +覆盖边界: + +- 接送需求没有由合法接口触发、又能稳定观测到的独立「处理中(PROCESSING)」窗口:看板详情 GET 只推进行程用车需求,接送需求无此副作用。所以「已接手」这一侧用派车和 DONE 状态来覆盖。 +- 车务首页工作台的待办数不含待确认接送变更,是已知缺口 #8457(第 41 行已写),不在本单验收范围。 + +--- + +## 十、相关文档 + +- 工单 #8429(同批,发布判据统一):https://git.1814.love/wx/HL/issues/8429 +- 工单 #8430(同批,纯接送机订单看板详情):https://git.1814.love/wx/HL/issues/8430 +- 接口契约文件位置:hl-fleet-service / hl-order-service-v3 + +--- + +## 关联 / 联系人 + +- **后端负责人**: @wx +- **同批工单**: [#8429](https://git.1814.love/wx/HL/issues/8429)(发布判据统一)、[#8430](https://git.1814.love/wx/HL/issues/8430)(纯接送机订单看板详情) +- **后续工单**: 企微通道支持 VEHICLE_TEAM 通知、超时升级提醒、待确认筛选等 +- **前端联系**: mmg 车务看板模块负责人