diff --git a/changelogs-v2/2026-09/21_7444_团期配车就绪门禁与车辆共用关系-修改接口-管理后台.md b/changelogs-v2/2026-09/21_7444_团期配车就绪门禁与车辆共用关系-修改接口-管理后台.md new file mode 100644 index 00000000..5b77bc8c --- /dev/null +++ b/changelogs-v2/2026-09/21_7444_团期配车就绪门禁与车辆共用关系-修改接口-管理后台.md @@ -0,0 +1,1495 @@ +--- +schema: "hl-changelog/v2" +ticket: "7444" +title: "团期配车就绪门禁两档判定(blockers/warnings)+ 车辆共用关系(4 新增 + 6 改造接口)" +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: "gateway_status=verified 的判据(2026-09-21 实测):经网关 https://api.test.1814.love:9443 对本篇 10 个端点中**全部 8 个位于网关面上的 /admin/fleet/** 端点**逐个发起真实请求并记录响应信封的 code —— share-groups 确认(600009 不存在团期)/查询(200 空集)/解除(602110 缺策略、602108 关系不存在)、readiness(200 两档结构完整 ready=false+warned=true+3 类 blocker+WARN_SEAT_SHORTAGE;整团免车团期 602113 失败关闭不降级)、reconfigure(600002 与字段级 400)、precheck(200,conflicts[] 实测带齐 blocking/reasonCode/shareGroupId 三个新字段)、candidates(200 正常分页;缺 orderId 时 400)、restore-cancel(605009 派单不存在),结构与错误码均与正文一致,逐条读数见正文「八、测试环境已验证」8.1。写口一律用零副作用错误路径取证,测试环境未因取证产生业务数据变更。剩余 2 个 internal 端点(/internal/fleet/dispatch/.../release、/v3/internal/group-batch/.../vehicle-ready)**按网关设计就不在网关面上**:hl-gateway JwtAuthFilter 在所有鉴权分支之前对 /internal/**、/v3/internal/** 直接 forbidden('接口不可访问'),实测两者均返回 code=403;它们由服务间 Feign 触发、前端不可调也不需调,故不计入本字段的判定范围(不是失败)。 backend_status=deployed 的判据(2026-09-21 实测):deploy_panel deploy-backend 分别部署 hl-fleet-service 与 hl-order-service-v3,服务端 git sync done: branch=dev-v3 HEAD=bb4091074;fleet 17:14:32 新 jar 111247255 B,实例 8187(PID 1598964)/8087(PID 1599321) 滚动重启并健康检查 UP;order-v3 17:16:07 新 jar 328980166 B,实例 8186(PID 1607261)/8086(PID 1608074) UP;两次任务 exit_code=0。origin/dev-v3 在部署时刻即为 bb4091074,故测试服跑的字节就是本篇定稿点的字节。⚠️ 独立第二判据的局限(照实记):上一次部署点 311dc92ee 与 bb4091074 之间 13 个 fleet 提交里,行为差异(#8061/#8051/#8064/#7994)全部需要一条真实的活跃共用关系作前置才能观测,本次取证未构造该前置;原本可做版本判别的 swagger /hl-*/v2/api-docs 被测试服 nginx 前置 basic auth 401 拦掉(4 个路径变体与 order-v3 同路径一并 401,故是路径规则不是服务问题),actuator 亦被网关按小程序端口径 403。因此第二判据退化为「8 个端点在部署后经网关全部取到与 dev-v3 源码一致的业务读数」,它能证明本篇代码在测试服上活着,但对「本次 17:14 部署 vs 上一次部署」没有分辨力。 本轮对正文做的契约补充(对 19be7f21c..bb4091074 逐提交读码后确认原稿未覆盖):(1) precheck 的 cityJunctionShareCandidate 改写为与 candidates 同名字段逐字同义(#8004 已把两侧拉齐),原稿「两处不要复用同一个渲染判断」的说法与源码相反,已删;(2) precheck 与 candidates 入参表补 excludeAssignmentId(candidates 另补 assignmentGroupId 成对要求),并写明「改派不传即按新建派单处理、不享受共用授权,读数与功能没做一模一样」这一失败形态(#8004 AC-2/AC-3);(3) 接口 3 解除的三种策略范围按 #8061 改写为「只到本关系的成员为止」,并补 #8051「不跨服务日、不跨团期」,收口断言范围同宽;接口 5 clearAll 分支同步标注;(4) 补 #8064:自动收缩 AUTO_SINGLE_MEMBER 此前整段挂在 DUAL_WRITE 分支而环境全为 LEGACY,上线至今零执行,现已补 LEGACY 挂点,前端必须真的渲染这一态;并写明整团解除原因列豁免(AC-10)与 LEGACY 少覆盖的那一种情形;(5) 补 #8013:COST_BEARER_CHANGED 由死枚举转活,改前这类变更被记成 MEMBER_ADDED,且「什么都没变」不再写历史行;(6) 补 #8003:跨维度准入会把其它关系/其它维度里记着同一张旧派单 ID 的活跃成员行一起改绑,故接口 1 成功后 sourceId 的回读范围要扩到其它维度的关系;(7) 接口 5 补一条覆盖边界:602013 属 #7442 既有契约、不在本篇 10 端点变更面内,#7994 的调整不改其对外结构。 六.8「错误码全集(按端点对照)」已按 origin/dev-v3 的 10 个端点 javadoc 逐端点(不是按错误码类)重新核对:覆盖完整、无源码中不存在的码;602100 段实占 602100-602114 共 15 个,其中 GroupDispatchShareErrorCode.SEGMENT_END=602199 是段位预分配上界常量、不是错误码;809205/809206 是 discardCode 取值、正文已标注其非错误码。 本文件是 #7444 的唯一 changelog(2026-09-19 已合并去重同目录旧稿)。PR-1(#7956, d97babc9e)+PR-2a(#7959, 43a5b7153)+PR-2b(#7963, 19be7f21c) 均已合入 dev-v3,其后的契约面修订(#8004/#8003/#8013/#8051/#8061/#8064/#8002)已按上列条目并入正文。" +updated_at: "2026-09-21" +base: "dev-v3" +--- + +# 团期车务:配车就绪门禁(红/黄两档)与车辆共用关系(工单 #7444) + +> **存放目录**: 二期(order-v3 标签工单)→ `changelogs-v2/2026-09/` +> +> **服务**: hl-fleet-service(主,8087)+ hl-order-service-v3(副,8086,仅 `vehicle-ready` 回调链路内核微改) +> **PR**: #7956(PR-1 就绪门禁)、#7959(PR-2a 共用关系核心)、#7963(PR-2b 共用关系读侧与释放侧) +> **Issue**: #7444 +> **日期**: 2026-09-19(PR-2b 合入日;PR-1/PR-2a 分别合入于更早) +> **影响范围**: 管理后台「团期配车页」(车务共用确认交互,见 wx 拍板 D5)、车务派单页(预校验/候选列表标红变绿)、团期看板配车就绪展示 + +--- + +## ⚠️ 关键变化 + +- 🔴 **鉴权口径订正(对既有 javadoc 的改判,不是本单新增)**:`GroupDispatchShareController` 等控制器 javadoc 里写的"权限点 `fleet:group-dispatch:write`/`fleet:group-dispatch:view`"**在服务端不起任何作用**——全 fleet 服务 `hl-fleet-service/src/main/java/com/hulalv/fleet/dispatch` 包下零 `@PreAuthorize`/`@SaCheckPermission` 等权限注解;`fleet:group-dispatch:view` 虽注册进了库(`V20260910_007`),但 `V20260911_007__fix_group_dispatch_admin_grants.sql` 文件头明确记载"该权限码在 Java 侧零引用,即便显式授予仍 403(AC-12 实测)",并已把 ADMIN 角色对它的授权收回。`/admin/fleet/**` 全部端点(含本单新增/改造的全部 10 个)的真实准入是**路径级角色门禁**:服务层 `FleetAdminRoleGuardInterceptor`(挂在 `/admin/fleet/**` 全量,`FleetWebMvcConfig.java:21-22`)+ 网关层 `JwtAuthFilter.java:472-479` 双重校验请求头 `X-Admin-Role` 必须是 `VEHICLE_MANAGER` 或 `SUPER_ADMIN`,不满足直接 403「无权限访问车务管理,请切换到车务角色」,与具体权限码无关。**前端如果按"权限点"模型去控制按钮/菜单可见性,需要改成按角色判断**;下方各接口小节的"权限点"字样在业务边界里统一按本条订正理解。 +- **本次是两组能力的合并交接**:①团期配车就绪判定从"一个隐式布尔"改成**两档**——硬拦 `blockers[]`(红,阻断)与只提醒 `warnings[]`(黄,不阻断);②新增"同一辆车同一天既跑团级行程又接送若干户"的**车辆共用关系**机制,车务在团期配车页人工确认后,被确认的几方互相放行冲突判定。 +- **`ready=true && warned=true` 是合法且常见的正常态**,前端不得把它画成失败或红色——车排齐了、司机还没排,属于"能发车但有缺口"。 +- **`POST /admin/fleet/assignments/precheck` 的 `conflicts[]` 新增 `blocking`/`reasonCode`/`shareGroupId` 三个字段——是全新字段,不是既有字段改取值**。`PrecheckRespVO.ConflictItemVO` 在本次之前只有 `type`/`conflictAssignmentId`/`conflictOrderNo`/`conflictDateRange`/`cityJunctionShareCandidate`/`msg` 六个字段(源码字段名是 `conflictDateRange` 一个合并字符串,**不是**分开的 `conflictStartDate`/`conflictEndDate`,工单正文写的是内部 BO 字段名,与对外 VO 不一致,本文档以对外 VO 为准)。前端要为这三个新字段新增渲染分支,不能假设它们已经存在于历史响应里。 +- **共用确认端点带 `@Idempotent(timeout=10)`**:10 秒内重复提交同一份成员全集会被防重窗口**拒绝**(`code=100502`),前端按"请勿重复提交"提示,**不要当失败报红**;10 秒之外重复提交同一份成员全集会正常受理并返回同一个 `shareGroupId`,那是幂等覆盖,是成功。 +- 🔴 **`ASSIGNMENT` 成员的 `sourceId` 在被本端点派上车后会变,不能把成功响应当模板原样缓存重发**:`admissionIntent=PENDING_ADMISSION` 的成员走 `AssignmentService.change` 准入,旧派单行转 `canceled`、生成新行并改绑 `source_id`;十几秒后用同一份(已过期的)请求体重发会抛 `602102`,这不是幂等坏了,是引用的派单行已经不存在了。改动前端先调「接口 2」查询回读最新 `sourceId` 再拼请求体,详见「接口 1」业务边界。 +- **解除关系 / `reconfigure` 的 `clearAll` / 内部 `release`(流团)之后,幸存派单会被真实处置,不是只打标记**:`REASSIGN`/`RELEASE` 两种策略下,占用**已经真的被释放**,对应派单进入"待改派"业务态;响应里的 `pendingReassignSourceIds[]` 就是这份清单,前端**必须展示出来**,不展示等于把"有几户的车被撤了"这件事悄悄吞掉。 +- **`costSourceRefNo` 按关系身份生成**(`SHARE-{shareGroupId}`),**不是**按 `(团, 日, 维度, 资源)` 生成:同一个槽位上解除关系后重建,会拿到一个**新的** `costSourceRefNo`;核团/财务人工比对时必须以 `costSourceRefNo` 为准,不能按"团+日+车"去猜同一笔钱。 +- **窗外日(接送机日期落在团期服务日窗外)不进共用关系,这是有意边界,不是缺陷**:这几天的接送机 claim 仍走原有的城市衔接(R3-EX)判定。 +- 🔴 **源码核对发现工单正文有两处错误码写错**,本文档已按源码更正(详见「六.6、修改前后对比」与各接口的「错误响应」):`POST .../share-groups`(确认)与 `GET .../share-groups`(查询)两个端点里,`resourceType` 传非法值实际返回的是**通用参数错误 `100001`**,不是工单正文写的 `602114`;`602114` 实际只在 `GET .../readiness` 就绪读口里使用。 +- 🔴 **`POST /v3/internal/group-batch/{groupBatchId}/vehicle-ready` 请求体没有加 `readinessSnapshot` 字段**——这是工单正文最初的设计,但 2026-09-18 已被撤回(改 `hl-common-core` 代价太大,且该字段唯一的消费场景后来也被砍掉),源码里请求体 `GroupBatchVehicleReadyReqDTO` 仍是 `#7442` 定的四个字段(`requirementId`/`requirementVersion`/`planVersion`/`sourceRefNo`),未改动。该端点本次的唯一变化是内核多做一步"团级正式需求 `DISPATCHED → DONE`",对请求/响应契约无影响。 +- 🔴 **自动收缩(`RELEASED` + `reason=AUTO_SINGLE_MEMBER`)此前一次都没真正执行过,2026-09-21 起会真的触发**(#8064 / PR #8073):这条链原先整段挂在占用双写协调器的 `DUAL_WRITE` 分支上,而测试与生产环境 `fleet_occupancy_rollout_fence` 的 64 个 stripe 实测全为 `LEGACY` ⇒ 它上线至今零执行。现已补上 `LEGACY` 形态的对等挂点。**前端必须真的能渲染「关系被系统自动解除」这一态**(列表里关系消失 + 历史多一条 `RELEASED/AUTO_SINGLE_MEMBER`),不能按"理论上存在、实际不会出现"处理。配套(#8064 AC-10 / PR #8077):整团解除(`CLEAR_ALL` / `GROUP_RELEASE`)时原因列被**豁免**,不会被 `AUTO_SINGLE_MEMBER` 夺走,审计能分清"人解的"还是"系统收的"。 + - **覆盖边界(照写,不是缺陷)**:`LEGACY` 挂点比 `DUAL_WRITE` 挂点少覆盖一种情形——「同一个 claim 原地换到另一个资源」(成员仍活跃但资源变了)。`LEGACY` 下没有账本行能告诉它旧资源是哪个,这一支不触发自动收缩。 +- **`COST_BEARER_CHANGED` 从死枚举变成活枚举**(#8013 / PR #8020):改前**只改成本承担方**(成员全集没变)会被记成 `MEMBER_ADDED`,该枚举从未被写入过;现在这种变更记 `COST_BEARER_CHANGED`,且"什么都没变"的重复提交**不再写任何历史行**。前端若按 `history[].action` 分类渲染,要补这一支;也不要再把"只改了成本承担方"的记录当成"有人加了成员"。 +- 🔴 **跨维度准入会把「其它关系/其它维度」里记着同一张旧派单 ID 的活跃成员行一起改绑**(#8003 / `865fe40ec`):成员身份键是 `(服务日, 维度, 来源类型, 来源 ID)`,**维度在键里**,所以同一张接送派单可以同时是车维度关系与司机维度关系的成员;而准入走的 `AssignmentService.change` 换的是**整行**(旧行转 `canceled`、新行拿新 ID)。改前只改绑本次确认的那一个维度,另一维度会留一条指向 `canceled` 派单的悬挂成员行。**对前端的直接影响:「接口 1」成功后需要回读 `sourceId` 的范围不止本次确认的这个维度——同一张派单在其它维度关系里的成员行也换了 ID,那些关系的成员列表缓存同样要刷新**(否则拿旧 `sourceId` 重发会撞 `602102`)。 + +--- + +## 一、背景 + +团期车务链路走到"配车"这一段时,此前有两个缺口: + +1. **假就绪**:就绪判定只比对服务日集合,不看座位数、不看人数,每天排一辆 5 座车服务 17 人团照样判"就绪"。审查要求加容量校验,与既定设计(车型/车辆数/人数限制已在三周前全部放开,只留日期门禁)正面冲突。wx 拍板 D9:**缺口可见但不阻断**——硬拦三项(分组齐全/服务日逐日配满/需求与计划已确认)判 `ready`,只提醒两项(座位/司机)判 `warned`,两者互相独立。 +2. **同车同日既跑团级行程又接送逐户,此前无合法表达**:占用层与派单层的两两冲突守卫会把"团车 G 同时接甲、乙两户"里的 `(甲,乙)` 这一对纯逐户组合按城市衔接(R3-EX,跨城即拒)判掉,物理上允许的排车方式在系统里排不进去。wx 拍板 D11:共用是**集合语义**——`(团, 日, 维度, 资源)` 下的一个成员集合,同属一个已确认关系的 claim 互相放行;车务在团期配车页人工确认(wx 拍板 D5)。 + +| 维度 | 改前 | 改后 | +|------|------|------| +| 就绪判定 | 单一隐式布尔,只比服务日集合 | 硬拦 `blockers[]`(红)+ 只提醒 `warnings[]`(黄)两档,人数/座位纳入判定 | +| 同车同日接团级行程+逐户接送 | 无法表达,两两冲突守卫按城市衔接判 | 车务人工确认共用关系后,关系内成员互相放行 | +| 冲突判定原因 | 只有"是否城市衔接可共享候选" | 新增 `reasonCode=SHARE_GROUP_CONFIRMED`,可追溯是哪条人工授权放行的 | + +工单 #7444 是团期车务共 7 段(V-0~V-6)中的第 6 段(V-5),也是本期最后落地的一段,硬前置是 `#7442`(V-3 配车写口与恢复流程)与 `#7443`(V-4 接送机与团期身份)两张单,两者均已先行合入 `dev-v3`。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 确认团期车辆共用关系 | POST | `/admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups` | 新增接口 | 复合原子操作:授权+把成员派上车+占用准入同一事务 | +| 2 | 查询团期车辆共用关系 | GET | `/admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups` | 新增接口 | `includeReleased=true` 时含变更历史 | +| 3 | 解除团期车辆共用关系 | DELETE | `/admin/fleet/group-dispatch/share-groups/{shareGroupId}` | 新增接口 | 幸存成员三选一,均为实际占用处置 | +| 4 | 团期配车就绪判定 | GET | `/admin/fleet/group-dispatch/batches/{groupBatchId}/readiness` | 新增接口 | 硬拦 `blockers[]` + 提醒 `warnings[]` 两档 | +| 5 | 整团逐日配车提交 | POST | `/admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure` | 修改(`clearAll` 分支新增请求字段 + 4 个响应字段) | 沿用 #7442 端点,本次只改整团清零分支 | +| 6 | 派单预校验 | POST | `/admin/fleet/assignments/precheck` | 修改(`conflicts[]` 新增 3 字段) | 认共用关系后车不再误标红 | +| 7 | 查询派单候选资源 | POST | `/admin/fleet/assignments/candidates` | 修改(`conflicts[]` 新增字段 + 既有字段语义扩展) | 同上,候选面 | +| 8 | 撤销取消的派单 | POST | `/admin/fleet/assignments/{assignmentId}/restore-cancel` | 修改(新增共用关系放行分支) | 恢复本属共用关系的接送派单不再被误拒 | +| 9 | 幂等释放团期配车占用(内部) | POST | `/internal/fleet/dispatch/group-batch/{groupBatchId}/release` | 修改(新增共用关系连带处置) | 仅限内部 Feign,order-v3 流团/取消成团时调用 | +| 10 | 回填配车就绪(内部) | POST | `/v3/internal/group-batch/{groupBatchId}/vehicle-ready` | 修改(内核新增 `DISPATCHED → DONE` 一跳) | 仅限内部 Feign,请求/响应契约不变 | + +**独立核对结果(未照抄工单数字)**:本人逐条数了「接口变更」源节(工单 `D:/work2/_scratch/body_7444.md` 第 360~646 行)里标"新增"与标"改造"的小节标题,结果是 **新增 4 个(表中 #1~#4)、改造 6 个(表中 #5~#10)**,与工单 AC-32 自己声称的"4 新增 + 6 改造"**一致**。工单原文自曝过这个数字"此前一度写 6 而实际是 5,靠补的 `vehicle-ready` 一节才凑上,属巧合对上、不是核对过"——本次是重新数过的,不是沿用那次巧合。另有 1 个"复用不改"的内部端点 `GET /v3/internal/group-batch/{groupBatchId}/dispatch-baseline`(order-v3 → fleet,仅供 fleet 拉团期基线用),因为契约无变化、且 hl-ui 不会调用它,不计入本表,不建独立详情小节。 + +--- + +## 三、接口详情 + +### 1. 确认团期车辆共用关系 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups` + +**VO**: `ShareGroupConfirmReqVO → ShareGroupRespVO` + +(源码核对:`GroupDispatchShareController.java:53-79`、`ShareGroupConfirmReqVO.java`、`ShareGroupMemberReqVO.java`、`ShareGroupRespVO.java`、`ShareGroupMemberRespVO.java`) + +#### 使用场景 + +车务在**团期配车页**(wx 拍板 D5)排团期大巴时,同页看到本团当日还有哪几户要接送机,勾选"本车承接"后调用本接口。服务端在**同一次调用、同一个事务**里完成三件事:①落人工授权;②把 `admissionIntent=PENDING_ADMISSION` 的成员(尚未占上这辆车/这名司机的)派进去;③由占用准入读到刚落的授权而放行。中途任一步失败**整体回滚**,不会留下"关系已建但没占上车"或"占上车但没关系"的中间态。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | - | 团期主订单 ID(雪花) | +| serviceDate | Body | LocalDate | ✅ | 必须落在该团基线 `serviceDates[]` 内 | 共用发生的服务日;窗外抛 602104(有意边界) | +| resourceType | Body | String | ✅ | `VEHICLE`/`DRIVER`,非法值抛 **100001**(⚠️ 不是工单原写的 602114) | 资源维度;车与司机分别建关系 | +| resourceId | Body | Long | ✅ | - | 车辆 ID 或司机 ID | +| members | Body | Array | ✅ | 2~20 个,越界抛 602100/602101 | 成员**全集**(不是增量),重复提交按新全集覆盖 | +| members[].sourceType | Body | String | ✅ | `ASSIGNMENT`(逐户接送派单)/`GROUP_DISPATCH`(团级配车行) | - | +| members[].sourceId | Body | Long | ✅ | - | `fleet_assignment.assignment_id` 或 `fleet_group_dispatch.dispatch_id` | +| members[].admissionIntent | Body | String | ❌ | `OCCUPYING`(默认,已占着)/`PENDING_ADMISSION`(本次一并派入) | ⚠️ **服务端完全不读、不校验、不据它分支**,纯粹供前端做按钮文案/二次确认提示;服务端一律按库里实际占用现查现判 | +| costBearer | Body | String | ✅ | `GROUP`(记团级整车)/`ORDER`(记指定户)。⚠️ **缺失时返回的不是 602107,而是 HTTP 400 + `成本承担方不能为空`**(`ShareGroupConfirmReqVO:49` 的 `@NotBlank` 在业务层之前拦截,实测 2026-09-20)。602107 只在**业务层**触发:`costBearer=ORDER` 但 `costBearerOrderId` 缺失或不属于成员订单 | 成本承担方 | +| costBearerOrderId | Body | Long | 条件必填 | `costBearer=ORDER` 时必填,且必须是成员中某个 `ASSIGNMENT` 的订单 | - | +| remark | Body | String | ❌ | ≤200 | 确认备注 | +| confirmCrossResident | Body | Boolean | ❌ | 不传 / `false` 都表示**不确认**,服务端不会替你确认 | 🔴 **跨常驻车派单确认**:被派入的成员司机与这辆共用车不是常驻组合时,第一次提交会被 **605036** 拒;向车务展示消息原文并得到确认后,带 `confirmCrossResident=true` **原样重发同一份请求**。该字段不进防重键,被 605036 拒掉的那一次会释放 10 秒防重键,所以带 `true` 立刻重发不会撞「请勿重复提交」 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| shareGroupId | String | 共用关系 ID(雪花,序列化为字符串) | +| groupBatchId | String | 团期主订单 ID | +| serviceDate | LocalDate | 共用发生的服务日 | +| resourceType | String | `VEHICLE`/`DRIVER` | +| resourceId | String | 车辆或司机 ID | +| status | String | `ACTIVE`/`RELEASED` | +| costBearer | String | `GROUP`/`ORDER` | +| costBearerOrderId | String | `costBearer=ORDER` 时的承担订单 ID;否则 null | +| costSourceRefNo | String | 车费来源引用,固定形如 `SHARE-{shareGroupId}`;关系生命周期内恒定,解除后同槽重建会拿到新值 | +| members[] | Array | 成员全集 | +| members[].sourceType | String | `ASSIGNMENT`/`GROUP_DISPATCH` | +| members[].sourceId | String | 成员来源 ID | +| members[].requirementId | String | 用车需求 ID(`ASSIGNMENT` 成员的准入授权键;团级成员为 null) | +| members[].orderId | String | 订单 ID(`ASSIGNMENT` 成员;团级成员为 null) | +| confirmedBy | String | 确认人 adminId | +| confirmedAt | LocalDateTime | 确认时间 | +| version | Integer | 乐观锁版本号 | +| history | Array | 本端点恒为 null/空(只有 `includeReleased=true` 的查询接口才带历史) | + +⚠️ **成员出参没有 `orderNo`/`label` 字段**——`ShareGroupMemberRespVO` 实际字段是 `sourceType`/`sourceId`/`requirementId`/`orderId` 四个,没有工单正文示例响应里写的 `orderNo`/`label`,前端如需展示单号/文案需自行按 `orderId` 二次查询,不能指望本接口直出。 + +#### 请求示例 + +```json +{ + "serviceDate": "2026-09-12", + "resourceType": "VEHICLE", + "resourceId": 1, + "members": [ + { "sourceType": "GROUP_DISPATCH", "sourceId": 99001 }, + { "sourceType": "ASSIGNMENT", "sourceId": 88001, "admissionIntent": "OCCUPYING" }, + { "sourceType": "ASSIGNMENT", "sourceId": 88002, "admissionIntent": "PENDING_ADMISSION" } + ], + "costBearer": "GROUP", + "remark": "团车上午行程后接甲乙两户" +} +``` + +(路径参数 `groupBatchId=8801`) + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "shareGroupId": "77001", + "groupBatchId": "8801", + "serviceDate": "2026-09-12", + "resourceType": "VEHICLE", + "resourceId": "1", + "status": "ACTIVE", + "costBearer": "GROUP", + "costBearerOrderId": null, + "costSourceRefNo": "SHARE-77001", + "members": [ + { "sourceType": "GROUP_DISPATCH", "sourceId": "99001", "requirementId": null, "orderId": null }, + { "sourceType": "ASSIGNMENT", "sourceId": "88001", "requirementId": "5501", "orderId": "70123" }, + { "sourceType": "ASSIGNMENT", "sourceId": "88002", "requirementId": "5502", "orderId": "70124" } + ], + "confirmedBy": "1001", + "confirmedAt": "2026-09-12 18:20:33", + "version": 1 + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +本接口是同步写操作,不存在空数据形态。团期基线不可用时**失败关闭**,返回 600009(团期配车权威基线不可用),不会静默放行或降级判定。 + +#### 错误响应 + +```json +{ + "code": 602103, + "message": "成员派单不属于本团或团期身份未知: 88099", + "success": false, + "data": null +} +``` + +10 秒内重复提交同一份成员全集: + +```json +{ + "code": 100502, + "message": "共用关系确认处理中,请勿重复提交", + "success": false, + "data": null +} +``` + +🔴 成员司机与这辆共用车不是常驻组合,**需要车务确认后重发**(`confirmCrossResident=true`): + +```json +{ + "code": 605036, + "message": "司机与车辆不是常驻组合,请确认跨常驻车派单后重试(派单 88002:车侧已有常驻司机;车辆 蒙E·12345;司机 张建国)", + "success": false, + "data": null +} +``` + +抢到资源锁后锁租约丢失(**重试无效**,本次未提交): + +```json +{ + "code": 605075, + "message": "资源锁已失效(vehicle:1:2026-09-12),本次操作未提交;请确认是否存在并发操作后重新发起", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- **鉴权**:路径级角色门禁,`X-Admin-Role` 须为 `VEHICLE_MANAGER` 或 `SUPER_ADMIN`(见「⚠️ 关键变化」鉴权订正),不是权限点模型。 +- 🔴 **本接口会返回 602 段以外的码**:第 ② 步「把未占上车的成员派进这辆车」走的是**派单写路径**,所以派单域的 605 段错误码会原样透到本接口的响应里。按处置方式分三类: + - **需要车务表态后重发**:**605036**(跨常驻车派单需确认)——见下一条,前端必须实现确认框; + - **不可重试,必须人工核查**:**605075**(资源锁已失效,本次未提交)——**不要做自动重试**,提示"本次未提交,请确认是否有其他人同时在改这辆车/这名司机,核对后重新发起"; + - **可原样重试**:**605008**(抢锁超时,系统繁忙)——直接重发同一份请求即可。 + 另有"该成员的车/司机当下不可派"一类(**605037** 车辆维保或停用 / **605038** 司机休假或待激活 / **605070** 司机不存在),处置是换资源或把该成员移出本次成员全集,重试无用。 +- 🔴 **605036 跨常驻车派单需确认,前端必须实现确认框**:满足下列任一条即判"跨常驻",未确认一律拒——① 这辆共用车已设常驻司机,且与该成员当前司机不是同一人;② 该成员当前司机本身是**别的车**的常驻司机。⚠️ **两条是或的关系**:这辆共用车没设常驻司机只躲开 ①,② 照样会触发。 + ⚠️ **车务在页面上选的是"一辆车",但 605036 说的是司机**——`resourceType=VEHICLE` 时被派入的司机不是车务选的,是**该成员原派单上的司机**(服务端按成员原样带过去,本接口不改司机)。所以 605036 的 `message` 会点名「哪条派单 / 跨常驻的具体形态 / 车牌 / 司机姓名」四件事,**直接把 message 原文展示给车务**,不要自拟文案(自拟的话车务看不出是哪一个成员触发的,也就无从判断该不该勾确认)。 + **处置**:弹确认框「确认跨常驻车派单?」→ 车务点确认 → 把原请求体加上 `confirmCrossResident: true` **原样重发**。不传或传 `false` 都表示不确认,服务端不会替你确认。 +- 🔴 **605075 与 605008 长得像,处置相反**:两条都发生在抢资源锁这一环,文案都提到"锁",但 **605008 是没抢到,重试有效**;**605075 是抢到过、持有期内又丢了,重试无效**,且意味着持锁期间可能已有并发写入。UI 上 605008 给"重试"按钮,605075 **不要给**,改为提示核对后重新发起。 +- **防重与幂等是两件事**:10 秒内重复提交同一份 `(serviceDate, resourceType, resourceId, members 排序摘要)` 会被 `@Idempotent` 拒绝(`code=100502`),前端按"请勿重复提交"提示,不要当失败报红;10 秒之外重复提交同一份成员全集会正常受理并返回**同一个** `shareGroupId`,这是幂等覆盖,是成功,不是报错。 +- **只改 `costBearer` 不改成员时,10 秒防重窗口不受影响**——防重键含成员集合摘要,改成员集合即视为新操作,可立即受理。 +- 🔴 **`ASSIGNMENT` 成员的 `sourceId` 在准入后会变,重提必须先重读**:`admissionIntent=PENDING_ADMISSION` 的成员被本接口派上车时走 `AssignmentService.change`,旧派单行转 `canceled`、生成一条新行,共用关系成员表的 `source_id` 会被静默改绑到新行 ID(`GroupDispatchShareAdmissionService.java:604` `rebindSourceId`)。**前端不得把某次成功响应的请求体当模板原样缓存重发**——同一份 JSON 在 T0 成功,到 T+12s 用同一个(已过期的)`sourceId` 原样重发会抛 `602102`(成员在该资源日无活跃占用),这不是幂等窗口失效,是它引用的派单行已经不存在了。正确做法:改动成员集合前先调「接口 2」查询当前 `members[].sourceId`,用回读到的最新值拼请求体。 + - 🔴 **回读范围要扩到「其它维度的关系」**(#8003 / `865fe40ec`):成员身份键是 `(服务日, 维度, 来源类型, 来源 ID)`,**维度在键里**,同一张接送派单可以同时是车维度关系与司机维度关系的成员;而 `change` 换的是整行,两个维度记的都是那个旧 ID。现在准入会把**其它关系/其它维度**里仍记着旧 ID 的活跃成员行**一起**改绑。所以本次确认成功后,前端要刷新的不只是本关系——同一张派单参与的其它关系(典型是先建 `VEHICLE` 再建 `DRIVER`)的成员列表缓存同样已过期,拿旧 `sourceId` 提交会撞 `602102`。 +- **窗外日拒绝是有意边界**:`serviceDate` 不在团期基线服务日窗内一律 602104,不是缺陷,该日的接送机 claim 仍走原有城市衔接判定。 +- **成员已在另一个 active 关系里**(602106)——必须先解除原关系才能加入新关系。 +- **`GROUP_DISPATCH` 类型成员必须属于本团且当日在该资源上活跃**(602105,源码 `GroupDispatchShareAdmissionService.java:284-292`):`members[].sourceType=GROUP_DISPATCH` 时,`sourceId` 对应的团级配车行如果不存在、已软删,或属于另一个团,触发本码,`{0}` 占位符回填该 `sourceId`。前端应引导车务刷新本团配车行列表后重新勾选,不要提交页面缓存里的过期 `dispatchId`。 +- **提交前的回读断言**(602102):正常端到端流程中很少能拿到这个码——非占用成员会先在派单写路径上撞到它自己的码(605 段);602102 主要在"派单写路径异常返回成功但没有真正落 membership"这种边界情况下触发,触发后整体回滚。 +- **并发写冲突可重试**(602109,源码 `GroupDispatchShareAdmissionService.java:349-389`):本接口在"新建或复用该 `(团, 日, 维度, 资源)` 上的活跃关系"这一步命中——同一关系的乐观锁 CAS 未中(`costBearer` 覆盖时版本号被并发改写),或两个请求同时对同一 `(groupBatchId, serviceDate, resourceType, resourceId)` 抢建新关系触发唯一键冲突,均落本码。前端按"网络繁忙,请重试"直接重新提交即可,不需要人工介入;本码同样会出现在「接口 3」解除路径的并发场景,见该节业务边界。 +- 成员数越界(<2 或 >20)分别抛 602100/602101。 +- 车与司机**分别建关系**——同一批成员既要共用车又要共用司机,需要分别调两次本接口(`resourceType=VEHICLE` 一次、`DRIVER` 一次)。 + +--- + +### 2. 查询团期车辆共用关系 `GET /admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups` + +**VO**: `ShareGroupQueryReqVO → List` + +(源码核对:`GroupDispatchShareController.java:86-97`、`ShareGroupQueryReqVO.java`、`GroupDispatchShareService.java:125-144`) + +#### 使用场景 + +团期配车页加载共用关系列表;`includeReleased=true` 时一并查历史关系与变更记录,供核团/排障时回溯"这条关系是谁、什么时候、为什么解除的"。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | - | 团期主订单 ID | +| serviceDate | Query | LocalDate | ❌ | 不传=全部服务日 | - | +| resourceType | Query | String | ❌ | `VEHICLE`/`DRIVER`,非法值抛 **100001**(⚠️ 不是工单原写的 602114) | 不传=两者都查 | +| includeReleased | Query | Boolean | ❌ | 默认 false | true 时一并返回已解除关系与变更历史 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| (数组元素)shareGroupId / groupBatchId / serviceDate / resourceType / resourceId / status / costBearer / costBearerOrderId / costSourceRefNo / members[] / confirmedBy / confirmedAt / version | 同「接口 1」出参 | - | +| history[] | Array | 仅 `includeReleased=true` 时非空 | +| history[].action | String | `CREATE`/`MEMBER_ADDED`/`MEMBER_REMOVED`/`COST_BEARER_CHANGED`/`RELEASED` | +| history[].memberSnapshot | String | 该次变更后的成员**全集**快照(JSON 文本,不是增量);`RELEASED` 动作记的是被解散前的那一份名单 | +| history[].costBearerSnapshot | String | 该次变更后的成本承担方 | +| history[].operator | String | 操作人 adminId;系统自动触发(自动收缩/内部 release)为 `"0"` | +| history[].operateTime | LocalDateTime | 操作时间 | +| history[].reason | String | 解除原因:`MANUAL`/`CLEAR_ALL`/`GROUP_RELEASE`/`AUTO_SINGLE_MEMBER`(仅 `RELEASED` 动作有值) | +| history[].remark | String | 备注 | + +#### 请求示例 + +```http +GET /admin/fleet/group-dispatch/batches/8801/share-groups?includeReleased=true +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": [ + { + "shareGroupId": "77001", + "groupBatchId": "8801", + "serviceDate": "2026-09-12", + "resourceType": "VEHICLE", + "resourceId": "1", + "status": "ACTIVE", + "costBearer": "GROUP", + "costBearerOrderId": null, + "costSourceRefNo": "SHARE-77001", + "members": [ + { "sourceType": "GROUP_DISPATCH", "sourceId": "99001", "requirementId": null, "orderId": null }, + { "sourceType": "ASSIGNMENT", "sourceId": "88001", "requirementId": "5501", "orderId": "70123" } + ], + "confirmedBy": "1001", + "confirmedAt": "2026-09-12 18:20:33", + "version": 1, + "history": [ + { "action": "CREATE", "memberSnapshot": "[{\"sourceType\":\"GROUP_DISPATCH\",\"sourceId\":99001},{\"sourceType\":\"ASSIGNMENT\",\"sourceId\":88001}]", "costBearerSnapshot": "GROUP", "operator": "1001", "operateTime": "2026-09-12 18:20:33", "reason": null, "remark": null } + ] + } + ], + "success": true +} +``` + +#### 空数据 / 降级响应 + +本团在筛选条件下没有任何共用关系时返回空数组 `[]`;本接口无下游依赖,不存在降级路径。 + +#### 错误响应 + +```json +{ + "code": 100001, + "message": "参数非法: 资源维度非法: TRAIN", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- **鉴权**:同「接口 1」,路径级角色门禁 `VEHICLE_MANAGER`/`SUPER_ADMIN`。 +- `includeReleased=false`(默认)只返回当前 `ACTIVE` 的关系,不含历史。 +- `memberSnapshot` 是**全集**不是增量——"先加一个成员再删一个"这种操作序列,两条历史记录各自都是完整名单,前端回放不需要叠加计算。 +- 查询接口不加锁、不开事务,读到的是调用时刻的库状态,不保证与并发中的写操作严格串行。 + +--- + +### 3. 解除团期车辆共用关系 `DELETE /admin/fleet/group-dispatch/share-groups/{shareGroupId}` + +**VO**: `(PathVariable shareGroupId, RequestParam survivorPolicy) → ShareGroupReleaseRespVO` + +(源码核对:`GroupDispatchShareController.java:104-125`、`ShareGroupReleaseRespVO.java`、`GroupDispatchShareService.java:156-168`) + +#### 使用场景 + +车务在团期配车页显式解除一个共用关系(比如换车、行程调整后不再需要共用)。解除动作与幸存占用的**实际处置**在同一个事务内原子完成。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| shareGroupId | Path | Long | ✅ | - | 共用关系 ID | +| survivorPolicy | Query | String | ⚠️业务必填,框架层非必填 | `KEEP_LEGAL`/`REASSIGN`/`RELEASE`,缺失抛 602110 | 幸存成员处置策略;**前端必须始终传值**——`survivorPolicy` 在框架层声明为非必填是刻意的(声明必填会让 Spring 在进 Service 前就挡成 400,602110 这个业务码永远抛不出来),不传拿到的是 602110 而不是 400 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| shareGroupId | String | 共用关系 ID | +| status | String | 固定 `RELEASED` | +| survivorPolicy | String | 本次采用的处置策略 | +| keptSourceIds | Array\ | 保留占用的成员来源 ID | +| releasedSourceIds | Array\ | **占用已被真正释放**的成员来源 ID(不是标记) | +| pendingReassignSourceIds | Array\ | 需人工改派的成员来源 ID;占用已真实释放、派单已置"待改派"业务态,**前端必须展示这份清单** | + +#### 请求示例 + +```http +DELETE /admin/fleet/group-dispatch/share-groups/77001?survivorPolicy=REASSIGN +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "shareGroupId": "77001", + "status": "RELEASED", + "survivorPolicy": "REASSIGN", + "keptSourceIds": ["88001"], + "releasedSourceIds": ["88002"], + "pendingReassignSourceIds": ["88002"] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +本接口是同步写操作,不存在空数据形态;无下游降级路径。 + +#### 错误响应 + +```json +{ + "code": 602111, + "message": "保留合法共用失败,幸存成员之间不满足衔接规则: 88001,88002", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- **鉴权**:同「接口 1」,路径级角色门禁 `VEHICLE_MANAGER`/`SUPER_ADMIN`。 +- 🔴 **三种策略的处置范围一律只到「本关系的成员」为止**(#8061 / PR #8067,2026-09-20 收口):同一车/司机同一服务日上**不属于本关系**的在途派车行**一行不动**(接送机的接、送一对本来就该同日同车,它们不靠这个关系存在);**已提前离组的前成员同样不再被清**。前端不要再按「解除关系会把这辆车这天清空」去画提示。 +- **处置范围也不跨服务日、不跨团期**(#8051 / PR #8058):解除只动本关系那一个 `(团, 服务日, 维度, 资源)` 槽位上的成员,同一辆车/司机在**别的服务日**或**别的团期**上的在途派车行不会被连带软清。 +- **三种策略都必须完成实际占用处置**,不存在"只打标记"这一档:`KEEP_LEGAL`——仅当**本关系的幸存成员**与同槽其余 claim 两两全合法时整槽保留,有冲突则 **602111 拒绝**并点名冲突对(fail-closed,整请求回滚,不产生半成品状态);`REASSIGN`——**真释放本关系中冲突成员**的占用并把对应派单置"待改派",**成员与非成员冲突时让位的是成员**;`RELEASE`——释放**本关系**全部幸存成员(解除那一刻仍活跃的成员)的占用,团级配车行由团期侧路径负责、本策略不软删它。 +- **收口断言的判定范围与处置范围同宽**:提交前过「账本里零非法组合」断言(不成立抛 602112 回滚),但只判「至少一侧是本关系成员」的组合——**两条非成员之间的存量冲突不由本接口负责**,前端不要指望解除一次关系能顺手把别人的历史冲突清掉。 +- **关系不存在或已解除返回 602108**:常见于并发场景(另一个车务已先一步解除,或页面数据未及时刷新)。前端不需要重试,应提示"该共用关系已不存在"并重新拉取「接口 2」查询列表刷新页面,不要在同一个已失效的 `shareGroupId` 上继续操作。 +- **并发写冲突可重试**(602109,语义同「接口 1」):解除过程中先 `leaveActive` 成员行、再 `casRelease` 关系行(`GroupDispatchShareSurvivorService.java:181-191`),任一步命中乐观锁 CAS 未中(关系或成员行被其他请求同时修改)即抛本码。前端按"网络繁忙,请重试"直接重新发起解除请求即可。 +- 幸存占用处置与账本重建的收口断言不通过时抛 602112,整请求回滚。 +- 🔴 **本接口同样会返回 602 段以外的码**:`REASSIGN` / `RELEASE` 两种策略要真释放成员占用,走的是**派单软清写路径**,所以派单域 605 段的码会原样透到本接口的响应里,整请求回滚(关系不会半解除)。前端按下表处置: + + | 码 | 含义 | 前端处置 | + |----|------|---------| + | 605008 | 抢锁超时,系统繁忙 | **可重试**:给"重试"按钮 | + | 605075 | 资源锁已失效,本次操作未提交 | 🔴 **重试无效**:不给重试按钮,提示核对是否有人同时在操作这些派单后重新发起 | + | 605042 | 司机通知结果正在确认 | **可稍后重试**:有成员的 HOLD 通知结果未决,等几秒再解除 | + | 605043 | 仅排车中/已派车且行程未出发的派单可清空司机/车辆选中 | 不可重试:有成员已出发或已不是可清状态,需人工处理该成员后再解除 | + | 605047 | 行程已结束,派车信息只读 | 不可重试:该成员行程已结束 | + | 605009 | 派单不存在 | 不可重试:先调「接口 2」刷新成员列表 | + + ⚠️ 这几条**不是关系本身的问题**,是关系里某一个成员的派单当下动不了——提示文案要能让车务看出"卡在成员上",别写成"解除失败,请重试"。`KEEP_LEGAL` 策略在全部成员都合法时不做释放,通常不会撞上这一组。 +- 解除后,同一 `(groupBatchId, serviceDate, resourceType, resourceId)` 上可以重新建立关系,但新关系会拿到**新的** `shareGroupId` 与 `costSourceRefNo`(不复活旧值)。 + +--- + +### 4. 团期配车就绪判定 `GET /admin/fleet/group-dispatch/batches/{groupBatchId}/readiness` + +**VO**: `(PathVariable groupBatchId) → GroupDispatchReadinessRespVO` + +(源码核对:`GroupDispatchQueryController.java:116-139`、`GroupDispatchReadinessRespVO.java`、`GroupDispatchReadinessItemVO.java`、`GroupDispatchReadinessService.java:109-141`) + +#### 使用场景 + +团期配车页与看板展示配车就绪状态;也供 fleet 内部就绪回填链路判断"能不能回调 order-v3 置位 `vehicle_ready`"。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | - | 团期主订单 ID | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupBatchId | String | 团期主订单 ID | +| requirementId | String | 判定所依据的正式需求 ID | +| requirementVersion | Integer | 判定所依据的需求版本 | +| planVersion | String | fleet 当前计划版本;该团尚无任何计划行时为 null | +| ready | Boolean | **硬拦三项是否全过**(`= blockers.isEmpty()`),true 才可回填 `vehicle_ready` | +| warned | Boolean | **是否有黄牌**(`= !warnings.isEmpty()`),不阻断;`ready=true && warned=true` 是合法且常见的组合 | +| blockers[] | Array | 硬拦未过项 | +| blockers[].code | String | `BLOCK_GROUP_MISSING`(整组未排车)/`BLOCK_GROUP_DAYS_INCOMPLETE`(本组服务日未排满或越界)/`BLOCK_NOT_CONFIRMED`(正式需求未确认或配车行未全部确认) | +| blockers[].message | String | 中文描述,硬拦项必须点名到组 | +| blockers[].tripDate | LocalDate | 相关行程日;整组缺失或需求未确认时为 null | +| blockers[].groupCode | String | 相关乘车分组码;与整体有关时为 null | +| warnings[] | Array | 只提醒项,结构同 `blockers[]`,`code` 另取 `WARN_SEAT_SHORTAGE`/`WARN_DRIVER_MISSING` | +| warnings[].seatTotal / headcount / gap | Integer | 仅 `WARN_SEAT_SHORTAGE` 有值:该日座位合计/用车人数/缺口 | +| warnings[].dispatchId | String | 仅 `WARN_DRIVER_MISSING` 有值:相关配车行 ID | +| groups[] | Array | 逐组覆盖明细,与 `#7442` `reconfigure` 响应的 `coverage` **同源**(`GroupDispatchCoverageCalculator`),两处字段逐字可比对 | +| groups[].groupCode | String | 分组键(= 需求侧 `group_code`) | +| groups[].vehicleType | String | 车型文本/字典值 | +| groups[].requiredDates | Array\ | 本组权威服务日(升序) | +| groups[].coveredDates | Array\ | 本次计划为本组实际排车的日期(升序) | +| groups[].missingDates | Array\ | 本组缺失的服务日 | +| groups[].outOfRangeDates | Array\ | 本组越界的日期(排了权威服务日窗之外的日期) | +| groups[].satisfied | Boolean | 本组是否已满足 | +| shareGroupCount | Integer | 本团当前 `ACTIVE` 共用关系数(配车页展示用),接的是三张新表的真实行数 | + +#### 请求示例 + +```http +GET /admin/fleet/group-dispatch/batches/8801/readiness +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "groupBatchId": "8801", + "requirementId": "5501", + "requirementVersion": 3, + "planVersion": "7", + "ready": true, + "warned": true, + "blockers": [], + "warnings": [ + { + "code": "WARN_SEAT_SHORTAGE", + "message": "2026-09-13 座位合计 33,该日用车人数 40,缺 7 座", + "tripDate": "2026-09-13", + "groupCode": "BUS", + "seatTotal": 33, + "headcount": 40, + "gap": 7 + }, + { + "code": "WARN_DRIVER_MISSING", + "message": "2026-09-15 蒙E·12345 未排司机", + "tripDate": "2026-09-15", + "groupCode": "BUS", + "dispatchId": "99005" + } + ], + "groups": [ + { + "groupCode": "BUS", + "vehicleType": "宇通33座大巴", + "requiredDates": ["2026-09-12", "2026-09-13", "2026-09-14", "2026-09-15", "2026-09-16"], + "coveredDates": ["2026-09-12", "2026-09-13", "2026-09-14", "2026-09-15", "2026-09-16"], + "missingDates": [], + "outOfRangeDates": [], + "satisfied": true + } + ], + "shareGroupCount": 2 + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +**本接口没有降级路径,失败关闭**:拉不到团期配车权威基线(或该团未声明任何乘车分组)时抛 **602113**,绝不返回 `ready=true` 或空 `blockers[]` 来冒充"已判定"。前端应把 602113 当作"暂时无法判定"处理,不要重试后仍失败就默认放行。 + +#### 错误响应 + +```json +{ + "code": 602113, + "message": "无法取得本团的配车就绪基线, 就绪判定失败关闭: 8801", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- **鉴权**:同「接口 1」,路径级角色门禁 `VEHICLE_MANAGER`/`SUPER_ADMIN`。 +- **两档互相独立,前端必须两个都读**:只读 `ready` 会漏掉"能发车但有缺口"的提示;只读 `warned` 会把"根本不能发车"的情况当成普通提醒。 +- `blockers[]`/`warnings[]` 与配车提交/确认写口的 `coverage` 同源,逐字段可比对;出现不一致属后端 bug,不是前端理解错了。 +- `shareGroupCount` **只进展示,不参与 `ready`/`warned` 判定**,不要据它做阻断逻辑。 +- 🔴 **本读口只有两个业务错误码,别记混**:**602113** = 拉不到本团的配车就绪基线,或该团一个乘车分组都没声明(消息 `无法取得本团的配车就绪基线, 就绪判定失败关闭: {groupBatchId}`);**602114** = 就绪查询入参非法(消息 `就绪查询入参非法: {原因}`,如 `groupBatchId` 缺失或非正数)。两者都属"这次判定没有结论",前端提示文案应有区别(602113 提示稍后重试或联系后端,602114 提示检查入口传参),但**都不得展示为已判定的绿/红/黄结果**,也不得因为重试仍失败就默认放行。 +- ⚠️ 共用关系那两个端点(「接口 1」「接口 2」)的 `resourceType` 非法返回的是通用 **100001**,不是 602114——602114 只属本读口,见「六.6」。 + +--- + +### 5. 整团逐日配车提交 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure` + +**VO**: `GroupDispatchReconfigureReqVO → GroupDispatchReconfigureRespVO` + +(源码核对:`GroupDispatchReconfigureReqVO.java`、`GroupDispatchReconfigureRespVO.java`、`AssignmentService.java:437-541`——本单只改 `clearAll=true` 分支的连带处置,其余字段/行为沿用 `#7442`,不在本文档重复列出) + +#### 使用场景 + +`#7442` 交付的整团配车写口。本次改动**只涉及 `clearAll=true`(整团清零)这一分支**:若该团存在 `ACTIVE` 共用关系,清零时需要一并交代幸存派单怎么处置。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | - | 团期主订单 ID | +| requirementId / requirementVersion / clearAll / reconfigureWindowToken / demands | Body | - | 沿用 `#7442` | 不变 | 本文档不重复列出,详见 `#7442` changelog | +| survivorPolicy | Body | String | **【本单新增,条件必填】** | 仅当 `clearAll=true` **且**该团存在 `ACTIVE` 共用关系时必填;取值 `KEEP_LEGAL`/`REASSIGN`/`RELEASE`;缺失抛 602110 | 语义同「接口 3」的同名参数 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupBatchId / requirementId / requirementVersion / planVersion / addedCount / removedCount / keptCount / updatedCount / aliveCount / addedDispatchIds / idempotentShortCircuit / coverage / legacyGroupRowCount | - | 沿用 `#7442`,不变 | +| releasedShareGroupIds[] | Array\ | **【本单新增】** 本次 `clearAll` 连带解除的共用关系 ID 清单;非 `clearAll` 时为空数组 | +| keptSourceIds[] | Array\ | **【本单新增】** 关系解除后判定为"可保留"的 claim 来源 ID 清单 | +| releasedSourceIds[] | Array\ | **【本单新增】** 占用**已被真正释放**的派单 ID 清单 | +| pendingReassignSourceIds[] | Array\ | **【本单新增】** 需人工改派的派单 ID 清单(占用已释放,当前无车),**前端必须展示** | + +#### 请求示例 + +```json +{ + "requirementId": "5501", + "requirementVersion": 3, + "clearAll": true, + "survivorPolicy": "RELEASE" +} +``` + +(路径参数 `groupBatchId=8801`) + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "groupBatchId": "8801", + "requirementId": "5501", + "requirementVersion": 3, + "planVersion": "8", + "addedCount": 0, + "removedCount": 5, + "keptCount": 0, + "updatedCount": 0, + "aliveCount": 0, + "addedDispatchIds": [], + "idempotentShortCircuit": false, + "coverage": null, + "legacyGroupRowCount": 0, + "releasedShareGroupIds": ["77001"], + "keptSourceIds": [], + "releasedSourceIds": ["88001", "88002"], + "pendingReassignSourceIds": [] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +该团没有任何 `ACTIVE` 共用关系时,`releasedShareGroupIds`/`keptSourceIds`/`releasedSourceIds`/`pendingReassignSourceIds` 四个新增字段均为空数组,行为与本次改动前完全一致,不需要 `survivorPolicy`。 + +#### 错误响应 + +```json +{ + "code": 602110, + "message": "存在幸存共用派单,必须指定处置策略", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- **鉴权**:同「接口 1」,路径级角色门禁 `VEHICLE_MANAGER`/`SUPER_ADMIN`。 +- `survivorPolicy` **只在 `clearAll=true` 且该团存在 `ACTIVE` 共用关系时才是必填**;不满足条件时传了也不会报错,只是不生效。 +- 关系解除与占用处置在同一事务内原子完成,不会出现"团级行已软删但请求报错"的半成品状态(`KEEP_LEGAL` 冲突拒绝时会连同已软删的团级行一并回滚)。 +- 四个新增响应字段的语义与「接口 3」解除端点的对应字段完全一致,前端可复用同一套展示组件。 +- 🔴 **`clearAll=true` 且触发幸存处置时,本接口也会返回 605 段的码**:真释放走的是同一条派单软清写路径,因此「接口 3」业务边界里那张 605 表(605008 / 605075 / 605042 / 605043 / 605047 / 605009)在本接口**同样适用**,处置方式一致,整请求回滚。本接口原有的 600/602000 段码不变。 +- **幸存处置的范围限定与「接口 3」完全一致**:走的是同一条幸存者路径,所以 #8061「只处置本关系的成员」与 #8051「不跨服务日、不跨团期」两条收口在本接口的 `clearAll` 分支上同样成立——同一车/司机同一天上不属于这些关系的在途派车行不会被连带清空。 +- **覆盖边界(本交接件不覆盖,照写以免前端误读)**:本接口自身的受控重开窗口校验码 `602013`(「本次配车改动越出重开窗口授权范围」)属 #7442 既有契约,**不在本交接件的 10 个端点变更面内**;#7994 在 2026-09-21 调整过它的一条判定(无分组历史行只准被收编、不准被窗口删除),该调整不改变本接口对外的请求/响应结构,也不改 602013 的码与文案。前端对 602013 的处理沿用 #7442 的口径即可,本篇「六.8 错误码全集」按各端点 javadoc 声明列举,因此不含 602013。 + +--- + +### 6. 派单预校验 `POST /admin/fleet/assignments/precheck` + +**VO**: `PrecheckReqVO → PrecheckRespVO`(请求体不变,响应体 `conflicts[]` 新增字段) + +(源码核对:`AssignmentController.java:103-111`、`PrecheckRespVO.java`、`AssignmentService.java:948-1044`) + +#### 使用场景 + +车务派单页选车/选司机前的冲突预校验;共用关系确认后,本接口需要认得这条授权,不再把已确认可共享的车标成阻断冲突。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| (沿用既有 `PrecheckReqVO` 全部字段,本单不加字段) | - | - | - | - | - | +| excludeAssignmentId | body | String(Long) | 改派场景**必填** | 既有字段 | 🔴 **改派时不传,读数与「共用关系功能根本没做」一模一样**:本端点不会自动认出「哪条占用是调用方自己」,不排除自身时调用方会和自己冲突——返回的 `conflict=true` / `blocking=true` 里混着自己那条占用。共用关系判定同样以本字段为主体身份:**不传即按「新建派单」处理,一律不享受共用授权**(与写口 `create` 同口径,读口不得比写口松)。新建派单场景不传是对的 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| conflict | Boolean | 沿用既有:是否存在阻断性冲突 | +| conflicts[].type | String | 沿用既有:`vehicle`/`driver`/`vehicle_not_found`/`driver_not_found` | +| conflicts[].conflictAssignmentId / conflictOrderNo / conflictDateRange | - | 沿用既有 | +| conflicts[].cityJunctionShareCandidate | Boolean | 沿用既有,**取值逻辑不变**——只答「两段行程首尾同城吗」,**不含**共用关系那一支;命中共用关系的跨城车在本列仍为 `false`。❗ **与「接口 7」候选面的同名字段逐字同义**(#8004 把两侧拉齐了),可以复用同一个渲染判断;但本口**没有** `shareEligible` 这一列,本口判「能不能派」一律读 `blocking` | +| conflicts[].blocking | Boolean | **【本单新增字段】** 本条冲突是否阻断本次派车;无共用关系时 `= !cityJunctionShareCandidate`(与改前口径逐字等价),命中已确认共用关系时为 `false`;资源不存在类冲突恒为 `true` | +| conflicts[].reasonCode | String | **【本单新增字段】** `SHARE_GROUP_CONFIRMED`(已确认共用关系放行)/`CITY_JUNCTION_SHAREABLE`(同城首尾衔接)/`ASSIGNMENT_CONFLICT`(真冲突);资源不存在类冲突为 null | +| conflicts[].shareGroupId | String | **【本单新增字段】** 本条冲突与本次派车同属的已确认共用关系 ID(取重叠期首日那一个);无则 null | +| warnings[] | Array | 沿用既有,不变 | + +#### 请求示例 + +```json +{ + "vehicleId": "1", + "driverId": "2079857983403024387", + "startDate": "2026-09-12", + "endDate": "2026-09-16" +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "conflict": false, + "conflicts": [ + { + "type": "vehicle", + "conflictAssignmentId": "88001", + "conflictOrderNo": "26-0503", + "conflictDateRange": "2026-09-12~2026-09-16", + "cityJunctionShareCandidate": false, + "blocking": false, + "reasonCode": "SHARE_GROUP_CONFIRMED", + "shareGroupId": "77001", + "msg": "车 蒙E·12345 已确认共用" + } + ], + "warnings": [] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无冲突时 `conflicts` 为空数组 `[]`;本接口只读,无下游降级路径。 + +#### 错误响应 + +```json +{ + "code": 100001, + "message": "参数非法: startDate 不能晚于 endDate", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- **鉴权**:同「接口 1」,路径级角色门禁 `VEHICLE_MANAGER`/`SUPER_ADMIN`,本次未改动。 +- **不改这里,写口放行但页面预校验仍会显示阻断冲突**——`blocking` 字段是本次改动的核心,前端渲染冲突提示时必须改读 `blocking` 而不是继续按 `cityJunctionShareCandidate` 取反判断,否则会出现"写口能提交成功、但预校验一直标红"的体验矛盾。 +- `blocking` 非显式 `false` 一律按阻断处理(含缺省/null)。 +- `reasonCode`/`shareGroupId` 为 null 的冲突项仍应按原有逻辑展示(不是新字段缺失的错误,只是没有共用关系背景)。 + +--- + +### 7. 查询派单候选资源 `POST /admin/fleet/assignments/candidates` + +**VO**: `AssignmentCandidateReqVO → AssignmentCandidateRespVO`(请求体不变,响应体 `conflicts[]` 新增/变更字段) + +(源码核对:`AssignmentController.java:86`、`AssignmentCandidateRespVO.java:576-616`、`AssignmentCandidateService.java:666-729`、`AssignmentCandidateAvailabilityReasonEnum.java`) + +#### 使用场景 + +车务派单页选车/选司机时的候选列表;共用关系确认后,已授权的车/司机在候选列表里应从"标红不可选"变成"可选,并标明已确认共用"。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| (沿用既有 `AssignmentCandidateReqVO` 全部字段,本单不加字段) | - | - | - | - | - | +| excludeAssignmentId | body | String(Long) | 改派场景**必填** | 既有字段 | 🔴 **改派场景必须传,且要和 `assignmentGroupId` 成对传**(#8004 AC-2/AC-3):本端点不会自动认出「哪条占用是调用方自己」,不排除自身时**调用方会和自己冲突**——【实测】同一请求只带 `assignmentGroupId` 不带 `excludeAssignmentId`,返回 `selectable=false` / `availabilityReasonCode=ASSIGNMENT_CONFLICT` / `shareGroupId=null`,**与「共用关系功能根本没做」的读数一模一样**,前端会据此认定后端没做。共用关系判定同样以本字段为主体身份:**不传即按「新建派单」处理,一律不享受共用授权**(与写口 `create` 同口径,读口不得比写口松)。新建派单场景不传是对的 | +| assignmentGroupId | body | String(Long) | 改派场景**必填** | 既有字段 | 与 `excludeAssignmentId` 成对:它决定候选面按哪个派单组算冲突。只传其中一个拿到的是上一行那种「像没做」的读数 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| (其余候选字段沿用既有) | - | - | +| conflicts[].assignmentId / assignmentGroupId / orderNo / teamNo / startDate / endDate | - | 沿用既有 | +| conflicts[].cityJunctionShareCandidate | Boolean | 🔴 **2026-09-20 网关实测 + 源码复核订正(本节原描述已被 #8004 撤销)**:**取值逻辑不变,只答「两段行程首尾同城吗」**——共用关系**不**改写它。命中共用关系的跨城车在本列仍为 `false`。❗ **与「接口 6」预校验面的同名字段现已逐字同义**(#8004 把两侧拉齐了) | +| conflicts[].shareEligible | Boolean | ⚠️ **本字段在原交接件里从未登记,2026-09-20 补入**。它才是承载「可共享」语义的字段:**同城衔接 或 已确认共用关系**,任一成立即 `true`;`blocking` 恒等于 `!shareEligible`。**前端判「能不能选」请统一读本列或 `blocking`** | +| conflicts[].blocking | Boolean | **【行为变化,非新增】** 命中已确认共用关系时转 `false` | +| conflicts[].reasonCode | String | **【行为变化】** 新增枚举值 `SHARE_GROUP_CONFIRMED` | +| conflicts[].reasonMessage | String | 沿用既有:后端可直接展示的中文原因 | +| conflicts[].shareGroupId | String | **【本单新增字段】** 同属的共用关系 ID;无则 null | + +#### 请求示例 + +```json +{ + "requirementId": "5501", + "startDate": "2026-09-12", + "endDate": "2026-09-16", + "vehiclePage": 1, + "vehiclePageSize": 20 +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "vehicles": { + "records": [ + { + "vehicleId": "1", + "conflicts": [ + { + "assignmentId": "88001", + "assignmentGroupId": "88001", + "orderNo": "26-0503", + "startDate": "2026-09-12", + "endDate": "2026-09-16", + "cityJunctionShareCandidate": false, + "shareEligible": true, + "blocking": false, + "reasonCode": "SHARE_GROUP_CONFIRMED", + "reasonMessage": "已确认同团车辆共用关系,可共享", + "shareGroupId": "77001" + } + ] + } + ], + "total": 1, + "page": 1, + "pageSize": 20 + } + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无候选或无冲突时对应数组为空数组 `[]`;本接口只读,无下游降级路径。 + +#### 错误响应 + +```json +{ + "code": 100001, + "message": "excludeAssignmentId 不属于当前订单或当前用车需求", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- **鉴权**:同「接口 1」,路径级角色门禁 `VEHICLE_MANAGER`/`SUPER_ADMIN`,本次未改动。 +- 🔴 **2026-09-20 网关实测 + 源码复核订正(本节原描述已被 #8004 撤销)**:**两面的 `cityJunctionShareCandidate` 现已逐字同义,都只答「同城首尾衔接」**。 + 原文写的「两面语义不同」是 **PR-2b 当时的真实行为,但已被 #8004 撤销**。 + 🔴 那个缺陷的形态值得记一笔(源码 javadoc 原话):同一对跨城派单在两个读口给出相反的 true/false——**后端两侧各自都自洽,错只落在跨侧读的前端身上**,代码审查与单侧测试都看不见。 + ⇒ 现在前端**两面可以用同一个渲染判断**;要判「拦不拦得住」请读 `shareEligible` / `blocking`,**不要读 `cityJunctionShareCandidate`**。 +- 已确认共用关系的车/司机在候选列表中 `blocking=false` 才可被选中,前端选择逻辑统一取"`conflicts` 中 `blocking` 非显式 `false` 的项一律按阻断处理"。 + +--- + +### 8. 撤销取消的派单 `POST /admin/fleet/assignments/{assignmentId}/restore-cancel` + +**VO**: `(PathVariable assignmentId, 无 body) → RestoreCancelRespVO`(响应体不变,仅新增一条放行分支) + +(源码核对:`AssignmentController.java:551-571`、`RestoreCancelRespVO.java`、`AssignmentService.java:8639-8681`、`ShareMemberLeaveReason.java`) + +#### 使用场景 + +车务误取消一条本属于某已确认共用关系的接送派单后,撤销恢复。本次改动让恢复路径也认共用关系,避免"关系还在、但恢复被城市衔接规则误拒"。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| assignmentId | Path | Long | ✅ | - | 派单 ID | +| (无请求体) | - | - | - | - | - | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| assignmentStatus | String | 恢复后的派单状态(`assigned`/`holding`/`unassigned`),沿用既有 | +| assignmentGroupId | String | 派车组 ID,沿用既有 | +| vehicleStatusUpdated / driverStatusUpdated | String | 占用反算回写结果,沿用既有 | + +#### 请求示例 + +```http +POST /admin/fleet/assignments/88002/restore-cancel +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "assignmentStatus": "assigned", + "assignmentGroupId": "88002", + "vehicleStatusUpdated": "busy", + "driverStatusUpdated": "busy" + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无(同步写操作);旧 HOLD 通知结果仍在确认时返回既有 605042,本单不改这条。 + +#### 错误响应 + +```json +{ + "code": 605016, + "message": "派单已被其它单占用,无法撤销恢复", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- **鉴权**:同「接口 1」,路径级角色门禁 `VEHICLE_MANAGER`/`SUPER_ADMIN`,本次未改动。 +- **恢复放行的前提是"该派单当初是因占用被释放才自动离场共用关系"**——共用关系成员表新增的 `leave_reason` 字段区分两种离场:`AUTO_OCCUPANCY_RELEASE`(占用被释放触发的自动离场,可安全放回)与 `MANUAL_MEMBER_REMOVED`(车务人工把它移出成员全集,`restore-cancel` **不会**把它撤销回去)。这个区分对前端不可见,但意味着:如果车务在取消派单之前已经手动把它从共用关系里移出过,撤销取消**不会**让它自动回到关系里,仍走原有城市衔接判定,前端应据此提示车务必要时重新走共用确认。 +- 关系已解除(不是"自动离场")后再恢复,仍按城市衔接(R3-EX)判定,跨城则 605016。 +- **其余既有错误码不变**,按前端处置方式分三类(本单不改任何一条,此处补全是因为原稿漏了 605075): + + | 码 | 含义 | 前端处置 | + |----|------|---------| + | 605008 | 抢锁超时,系统繁忙 | **可重试**:给"重试"按钮,原样重发即可 | + | 605075 | 资源锁已失效,本次操作未提交 | 🔴 **重试无效**:**不要给重试按钮**,也不要自动重试。提示"本次未提交,请确认是否有其他人同时在操作这条派单,核对后重新发起"。消息里 `(…)` 中点名的是哪一把锁 | + | 605042 | 司机通知结果正在确认 | **可稍后重试**:本次保持 `canceled`,派单状态/通知代际/资源占用都没动 | + | 605009 | 派单不存在 | 不可重试:刷新列表 | + | 605015 | 超出撤销窗口,或该派单不是 `canceled` 态 | 不可重试:撤销入口应置灰 | + | 605016 | 已被其它单占用,无法恢复 | 不可重试:引导改派 | + | 605017 | 仅取消操作本人可撤销 | 不可重试:按钮对非本人置灰 | + | 605037 | 车辆已维保或停用,无法恢复占用 | 不可重试:引导换车改派 | + | 605038 | 司机已休假或待激活,无法恢复占用 | 不可重试:引导换司机改派 | + +- 🔴 **605075 与 605008 长得像,处置相反**:两条都在抢资源锁这一环、文案都提到"锁",但 605008 是**没抢到**(重试有效),605075 是**抢到过、持有期内又丢了**(重试无效,且意味着持锁期间可能已有并发写入,需要人工确认)。把它们渲染成同一种"稍后重试"提示,会让车务在一条实际未提交、且可能有并发写入的操作上反复点按钮。 + +--- + +### 9. 幂等释放团期配车占用(内部) `POST /internal/fleet/dispatch/group-batch/{groupBatchId}/release` + +**VO**: `(PathVariable groupBatchId, 无 body) → Void` + +(源码核对:`InternalGroupDispatchController.java:65-74`、`GroupDispatchService.java:895-935`、`AssignmentService.java:1800-1812`) + +#### 使用场景 + +**仅限内部 Feign 调用**,order-v3 取消成团/流团时触发。hl-ui **不直接调用**本接口,此处收录是因为它的行为变化会决定共用关系与占用的最终状态,前端需要理解"流团之后,之前建立的共用关系与幸存占用去哪了"。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | - | 团期主订单 ID | +| (无请求体,刻意不加必填参数——该端点由可靠命令链驱动,加参数会让存量重放失败) | - | - | - | - | - | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| (无响应体,`Result`) | - | - | + +#### 请求示例 + +```http +POST /internal/fleet/dispatch/group-batch/8801/release +``` + +#### 响应示例 + +```json +{ "code": 200, "message": "成功", "data": null, "success": true } +``` + +#### 空数据 / 降级响应 + +无活跃配车或已释放时直接成功(幂等);不存在降级路径。 + +#### 错误响应 + +```json +{ + "code": 602112, + "message": "占用账本重建后幸存成员与期望集合不符", + "success": false, + "data": null +} +``` + +(这是内部一致性兜底,正常情况不会触发;触发说明连带处置算法与账本状态不一致,整请求回滚) + +#### 业务边界 + +- **本次改动前**:只软删团级配车行、释放团级占用,共用关系与逐户接送占用一概不碰——流团后,之前建立的共用关系仍留在库里,幸存的逐户接送派单仍"共用"着一个此刻已经不存在依据的槽位。 +- **本次改动后**:同一事务内在既有动作之后,一并解除该团全部 `ACTIVE` 共用关系,并对每个被触及的资源日槽完成实际占用处置——按"同类内 `source_id` 升序、逐个尝试加入保留集"的确定性算法计算保留/释放集,释放集里的占用**被真正释放**,对应派单置"待改派"业务态。 +- **该端点不能拒绝**(流团必须成功):与「接口 3」的 `KEEP_LEGAL` 不同,有冲突时**释放低优先级成员**而不是报错。 +- 影响面:若前端有"团期解散后配车全部清空"这类展示,需要理解幸存派单不是被删除,而是转入"待改派",需引导车务去别的团期或另建订单改派。 + +--- + +### 10. 回填配车就绪(内部) `POST /v3/internal/group-batch/{groupBatchId}/vehicle-ready` + +**VO**: `GroupBatchVehicleReadyReqDTO → GroupBatchVehicleReadyRespDTO`(请求/响应契约均**不变**,仅内核新增一步状态迁移) + +(源码核对:`GroupBatchResourceController.java:66-83`、`GroupBatchVehicleReadyReqDTO.java`、`GroupBatchVehicleReadyRespDTO.java`、`GroupBatchService.java:797-921`) + +#### 使用场景 + +**仅限内部 Feign 调用**,fleet 整团配车就绪(`GET .../readiness` 的 `ready=true`)后回调本接口,order-v3 回填 `vehicle_ready=true`。hl-ui **不直接调用**本接口,此处收录是因为它的内核变化会影响团期正式需求的状态(进而影响 `#7442`/`#7441` 相关页面展示的需求状态)。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | - | 团期主订单 ID | +| requirementId | Body | Long | 条件必填 | 缺省=兼容期旧意图,走 legacy 路径 | 产出本次就绪意图的正式团级用车需求 ID | +| requirementVersion | Body | Integer | 条件必填 | ≥1 | 需求版本 | +| planVersion | Body | Long | 条件必填 | ≥1 | fleet 团期级计划版本 | +| sourceRefNo | Body | String | ❌ | ≤64 | 幂等追溯号(fleet Outbox 记录 ID) | + +⚠️ **本次没有新增 `readinessSnapshot` 字段**——这是工单最初的设计,2026-09-18 已撤回,源码请求体仍是上表四个字段,未改动。 + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| applied | Boolean | 沿用既有:本次是否真的落库 | +| discardReason / discardCode | String / Integer | 沿用既有:未生效原因及对应码(仅 809205/809206 两种有值) | +| currentRequirementId / currentRequirementVersion / currentPlanVersion / batchStatus | - | 沿用既有 | + +#### 请求示例 + +```json +{ + "requirementId": "5501", + "requirementVersion": 3, + "planVersion": "7", + "sourceRefNo": "880123" +} +``` + +(路径参数 `groupBatchId=8801`) + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "applied": true, + "discardReason": null, + "discardCode": null, + "currentRequirementId": "5501", + "currentRequirementVersion": 3, + "currentPlanVersion": "7", + "batchStatus": "RESOURCE_PREPARING" + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +请求体身份与当前活跃需求不一致、版本落后、或已应用过(重投)时,一律返回 `applied=false` + `discardReason`,HTTP 仍是 200,**不是错误**,不进入本文档「错误响应」范畴;此时本单新增的 `DISPATCHED → DONE` 第五步**不会执行**——它挂在"两级判定通过、`vehicle_ready` 真正落库"之后,判定没过就没有后续: + +```json +{ + "code": 200, + "message": "成功", + "data": { + "applied": false, + "discardReason": "IDENTITY_MISMATCH", + "discardCode": 809205, + "currentRequirementId": "5502", + "currentRequirementVersion": 4, + "currentPlanVersion": "9", + "batchStatus": "RESOURCE_PREPARING" + }, + "success": true +} +``` + +#### 错误响应 + +本接口对**业务性**未生效不用 HTTP 错误表达(见上「空数据 / 降级响应」的 `applied=false` 形态),本次改动不新增任何错误码;数据库/并发异常时仍是既有的兜底 500: + +```json +{ + "code": 100000, + "message": "系统异常,请稍后重试", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- **本次改动对请求/响应契约无任何影响**,纯内核行为变化:`applied=true` 落库成功后,事务内额外把该团**当前活跃**的正式团级用车需求从 `DISPATCHED` 推进到 `DONE`(源状态不是 `DISPATCHED` 则不推进,不抛异常,只留一条 WARN 日志与时间线记录)。 +- 需求已是 `DONE` 的重复回调:零写入、不报错、不重复留痕(幂等)。 +- **反向不做**:清零/流团/`clearAll` 不会把 `DONE` 退回;`DONE → PENDING_RECONFIRM` 由 `#7442` 的重开需求端点负责。 +- 对前端可观察的影响是**间接的**:若页面展示正式需求状态,`DONE` 状态会在配车就绪回调完成后出现,不是本接口直接返回的字段。 + +--- + +## 四、契约约束与正确调用方式 + +> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。 + +### ✅ 正确 / ❌ 错误 payload 对照(确认共用关系 `POST .../share-groups`) + +| 场景 | payload | +|------|---------| +| ✅ 成本记团级 | `{ "costBearer": "GROUP", "costBearerOrderId": null }` | +| ✅ 成本记指定户 | `{ "costBearer": "ORDER", "costBearerOrderId": 70123 }`(70123 必须是成员中某个 `ASSIGNMENT` 的订单) | +| ❌ 选 ORDER 但不填订单 | `{ "costBearer": "ORDER", "costBearerOrderId": null }` → 602107 | +| ❌ 选 ORDER 但订单不属于成员 | `{ "costBearer": "ORDER", "costBearerOrderId": 99999 }`(99999 不在成员订单里)→ 602107 | +| ✅ 成员 2~20 个 | `{ "members": [ {...}, {...} ] }` | +| ❌ 单成员 | `{ "members": [ {...} ] }` → 602100 | +| ✅ `admissionIntent` 可以完全不传 | `{ "sourceType": "ASSIGNMENT", "sourceId": 88001 }`(服务端自行现查现判占用状态) | + +### ✅ / ❌ payload 对照(解除关系 `DELETE .../share-groups/{id}` 与 `reconfigure` 的 `clearAll`) + +| 场景 | payload | +|------|---------| +| ✅ 显式传处置策略 | `?survivorPolicy=RELEASE` | +| ❌ 不传处置策略 | 不带 `survivorPolicy` → 602110(不是 400,因为该参数框架层刻意声明非必填) | +| ❌ 传非法策略值 | `?survivorPolicy=DELETE_ALL` → 100001(`CommonErrorCode.INVALID_PARAM`,不是 602 段) | + +### 切换状态时的必要动作 + +- 前端**不需要**、也**不应该**据 `admissionIntent` 做任何业务判断——它只是交互提示字段,服务端一律按库里实际占用重新判定。若前端据此字段跳过某个二次确认弹窗,实际放行结果仍取决于服务端判定,两者可能不一致。 +- `survivorPolicy` 每次调用解除/清零都必须显式传值,不要依赖"上次传过就记住"的前端本地状态——每次调用都是独立请求,服务端不保留上一次的选择。 + +--- + +## 五、数据库行为 + +- 确认共用关系成功后,会同时产生:①一条 `ACTIVE` 状态的共用关系记录;②该关系下每个成员一条成员记录(历史成员记录 `left_at` 保持独立,退出的成员不会被物理删除);③一条 `CREATE` 类型的变更历史记录。 +- `costSourceRefNo` 全局唯一,重复插入相同值会被数据库拒绝——这意味着同一关系生命周期内不会出现两条不同的 `costSourceRefNo`,解除后同槽重建关系必然拿到新值。 +- 解除关系(无论通过哪条路径:显式解除、`clearAll`、内部 `release`)时,`REASSIGN`/`RELEASE` 策略下对应的占用记录会被**真实标记为已释放**(不是软删占位),派单进入"待改派"业务态;`KEEP_LEGAL` 策略下若冲突则整个解除操作连同任何已发生的软删一并回滚,不留半成品。 +- 成员减到 1 个及以下时,关系会被系统自动置为 `RELEASED`,并落一条 `reason=AUTO_SINGLE_MEMBER` 的变更历史,不需要车务手动操作。🔴 **这条链 2026-09-21 才真正开始执行**(#8064,详见「⚠️ 关键变化」):改前它整段挂在占用双写协调器的 `DUAL_WRITE` 分支上,而环境实测 64 个 stripe 全为 `LEGACY`,于是自上线起零执行。前端必须真的把这一态画出来。**例外**:整团解除(`clearAll` / 团级 `release`)时原因列被豁免,仍记 `CLEAR_ALL` / `GROUP_RELEASE`。**边界**:「同一个 claim 原地换到另一个资源」这一种情形在 `LEGACY` 下不触发自动收缩。 +- 车与司机的共用关系是两条独立记录,互不牵连——同一批成员如果既要共用车又要共用司机,库里会有两条各自独立的关系记录。 + +--- + +### 就绪判定接受已完成需求(DONE) + +就绪判定的**硬拦第三项**(需求状态校验)接受的需求状态集为 `{CONFIRMED, DISPATCHED, DONE}`。这意味着: +- 即使团期正式需求已推进到 `DONE` 态(已完成用车),`GET /admin/fleet/group-dispatch/batches/{groupBatchId}/readiness` 仍可读到该需求**对应的配车状态仍判就绪**(若其他两项硬拦也通过)。 +- 就绪回填链路(`POST /v3/internal/group-batch/{groupBatchId}/vehicle-ready`)即使在需求已完成后再调一次仍可通过就绪判定,不会因"完成了"就变成"不就绪"。 +- 这个设计保证了配车完成→需求推进→回填就绪这三步不会出现"推进之后再回填就被拒"的尴尬情形。 + + + +## 六、边界行为 + +- 未登录 → 401(网关拦截) +- 请求参数非法(如 `resourceType` 传非 `VEHICLE`/`DRIVER`)→ **100001**(两个共用关系端点如此;就绪读口的入参非法是 602114,两者不是同一个码,见「六.6」) +- 就绪判定拉不到团期基线 → **602113,失败关闭**,绝不返回 `ready=true` 冒充判定结果 +- 窗外日的接送机 claim → 不进共用关系,仍走原有城市衔接(R3-EX)判定,属有意边界 +- 共用关系确认端点 10 秒内重复提交同一份成员全集 → **100502**,前端按"请勿重复提交"提示,不当失败处理 +- 老数据兼容:`fleet_assignment.group_batch_id` 为 NULL 的历史派车行(本列上线前的存量行、车务手工建的派车行)一律不能进共用关系,返回 602103——NULL 视为"团期身份未知",未知不得当同团通过 +- 🔴 **三个共用关系写口(接口 1 / 3 / 5)会返回 602 段以外的码**:内部走派单写路径,派单域 605 段既有码原样透出,其中 **605036 需车务确认后重发**、**605075 重试无效不给重试按钮**。**按端点的完整错误码清单见「六.8」**,不要只按 602 段写前端分支 + +--- + +## 六.5、枚举 / 数据字典 + +### `resourceType`(共用关系资源维度) + +**所属字段**: `ShareGroupConfirmReqVO.resourceType` / `ShareGroupRespVO.resourceType` / `ShareGroupQueryReqVO.resourceType` 等 | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `VEHICLE` | 车辆 | 共用车辆资源 | +| `DRIVER` | 司机 | 共用司机资源;车与司机分别建关系 | + +### `status`(共用关系状态) + +**所属字段**: `ShareGroupRespVO.status` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `ACTIVE` | 生效中 | 唯一可被守卫当作放行依据的状态 | +| `RELEASED` | 已解除 | 终态;再共用会建新关系,不复活旧行 | + +### `costBearer`(共用车费成本承担方) + +**所属字段**: `ShareGroupConfirmReqVO.costBearer` / `ShareGroupRespVO.costBearer` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `GROUP` | 记团级 | 整车成本记团级 `BatchCostType.BUS` 共享成本 | +| `ORDER` | 记指定户 | 需同时提供 `costBearerOrderId`,必须是成员中某个 `ASSIGNMENT` 的订单 | + +### `members[].sourceType`(共用关系成员来源类型) + +**所属字段**: `ShareGroupMemberReqVO.sourceType` / `ShareGroupMemberRespVO.sourceType` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `ASSIGNMENT` | 逐户接送派单 | `sourceId` = `fleet_assignment.assignment_id` | +| `GROUP_DISPATCH` | 团级配车行 | `sourceId` = `fleet_group_dispatch.dispatch_id` | + +### `members[].admissionIntent`(准入意图,仅前端交互提示) + +**所属字段**: `ShareGroupMemberReqVO.admissionIntent` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `OCCUPYING` | 已占着 | 默认值;该成员目前已占着这辆车/这名司机 | +| `PENDING_ADMISSION` | 本次一并派入 | 服务端会在同一次调用里把该成员派上车;⚠️ 服务端不读、不校验该字段,一律按库里实际占用重新判定 | + +### `survivorPolicy`(幸存成员处置策略) + +**所属字段**: `DELETE .../share-groups/{id}` 的 Query 参数 / `GroupDispatchReconfigureReqVO.survivorPolicy` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `KEEP_LEGAL` | 保留合法共用 | 仅当剩余 claim 两两全合法时整槽保留;有冲突则 602111 拒绝 | +| `REASSIGN` | 改派 | 真释放冲突成员占用,派单置"待改派",返回清单 | +| `RELEASE` | 释放 | 释放全部幸存成员的占用 | + +### `history[].reason`(共用关系解除原因) + +**所属字段**: `ShareGroupHistoryRespVO.reason` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `MANUAL` | 车务手动解除 | 调用 `DELETE .../share-groups/{id}` | +| `CLEAR_ALL` | 整团清零连带解除 | `reconfigure` 的 `clearAll=true` | +| `GROUP_RELEASE` | 团级释放连带解除 | 内部 `release`(取消成团/流团) | +| `AUTO_SINGLE_MEMBER` | 自动收缩 | 成员减到 ≤1,关系失去意义。🔴 **这一支 2026-09-21 才真正开始产生数据**(#8064:改前整段挂在占用双写 `DUAL_WRITE` 分支上,而环境实测全为 `LEGACY`,上线至今零执行)。前端不能按"理论值"处理。**整团解除(`CLEAR_ALL`/`GROUP_RELEASE`)的原因列被豁免**,不会被本值覆盖(#8064 AC-10) | + +### `history[].action`(共用关系变更历史动作) + +**所属字段**: `ShareGroupHistoryRespVO.action` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `CREATE` | 首次建立 | - | +| `MEMBER_ADDED` | 成员净增 | 幂等覆盖相对上一版全集净增。⚠️ 改前它还兼做「只改成本承担方」的记录,#8013 起不再(见下一行) | +| `MEMBER_REMOVED` | 成员净减 | 幂等覆盖或自动收缩时净减 | +| `COST_BEARER_CHANGED` | 成本承担方变更 | 🔴 **2026-09-21 前是死枚举、从未被写入过**(#8013 / PR #8020:成员全集没变而只改成本承担方时,改前一律记成 `MEMBER_ADDED`)。现在这种变更记本值;**「什么都没变」的重复提交不再写任何历史行** | +| `RELEASED` | 关系解除 | 四条解除路径共用此动作,用 `reason` 区分来路 | + +### `reasonCode`(冲突/候选判定原因码,`precheck` 与 `candidates` 共用) + +**所属字段**: `PrecheckRespVO.ConflictItemVO.reasonCode` / `AssignmentCandidateRespVO.ConflictVO.reasonCode` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `SHARE_GROUP_CONFIRMED` | 已确认共用关系 | **本单新增**;人工确认过的关系放行,不要求城市衔接 | +| `CITY_JUNCTION_SHAREABLE` | 同城首尾衔接 | 系统按行程首尾自动判出的可共享候选,非本单新增 | +| `ASSIGNMENT_CONFLICT` | 真冲突 | 非本单新增 | + +### `blockers[].code` / `warnings[].code`(就绪判定明细项代码) + +**所属字段**: `GroupDispatchReadinessItemVO.code` | **类型**: `String` + +| 值 | 中文 | 档位 | 说明 | +|----|------|------|------| +| `BLOCK_GROUP_MISSING` | 整组未排车 | 硬拦(红) | - | +| `BLOCK_GROUP_DAYS_INCOMPLETE` | 本组服务日未排满或越界 | 硬拦(红) | - | +| `BLOCK_NOT_CONFIRMED` | 需求或计划未确认 | 硬拦(红) | 需求状态接受集含 `CONFIRMED`/`DISPATCHED`/`DONE` | +| `WARN_SEAT_SHORTAGE` | 座位不足 | 提醒(黄) | 带 `seatTotal`/`headcount`/`gap` 三个数 | +| `WARN_DRIVER_MISSING` | 未排司机 | 提醒(黄) | 带 `dispatchId` | + +--- + +## 六.6、修改前后对比 + +### 字段级对比 + +| 字段 | 改前 | 改后 | +|------|------|------| +| `precheck` 响应 `conflicts[]` | 只有 `type`/`conflictAssignmentId`/`conflictOrderNo`/`conflictDateRange`/`cityJunctionShareCandidate`/`msg` | 新增 `blocking`/`reasonCode`/`shareGroupId` 三个字段 | +| `candidates` 响应 `conflicts[]` | 无 `shareGroupId`/`shareEligible`;`blocking`/`reasonCode` 只按城市衔接取值 | 🔴 **2026-09-20 网关实测 + 源码复核订正(本节原描述已被 #8004 撤销)**:新增 `shareGroupId` **与 `shareEligible`**;`blocking`/`reasonCode` 扩展为"同城衔接**或**已确认共用关系";**`cityJunctionShareCandidate` 取值逻辑不变**(原写它也扩展了,是错的) | +| `AssignmentCandidateAvailabilityReasonEnum` | 只有 `AVAILABLE`/`CITY_JUNCTION_SHAREABLE`/`ASSIGNMENT_CONFLICT` 三值 | 新增 `SHARE_GROUP_CONFIRMED` | +| `reconfigure` 请求体 | 无 `survivorPolicy` | 新增条件必填 `survivorPolicy`(仅 `clearAll=true` 且存在 `ACTIVE` 共用关系时必填) | +| `reconfigure` 响应体 | 无共用关系相关字段 | `clearAll=true` 时新增 `releasedShareGroupIds[]`/`keptSourceIds[]`/`releasedSourceIds[]`/`pendingReassignSourceIds[]` | +| 团期就绪判定 | 无独立读口,隐式布尔(覆盖完整即判就绪,`GroupDispatchService.java:300-304` 旧逻辑),且只比服务日集合 | 新增 `GET .../readiness` 读口,`ready`/`warned` 两档独立判定,纳入人数/座位 | +| `vehicle-ready` 请求体 | `requirementId`/`requirementVersion`/`planVersion`/`sourceRefNo` 四字段(`#7442` PR-C1 交付) | **不变**——工单曾计划新增 `readinessSnapshot`,2026-09-18 已撤回,源码未改 | +| 错误码段 | fleet 602100-602199 全空;order-v3 809300-809399 全空 | fleet 段新落 602100-602114 共 15 个;order-v3 809 段**未建**(原计划的 809300/809301 因 `readinessSnapshot` 撤回而失去唯一消费场景,一并取消)。⚠️ **本行说的是"本单新建了哪些码",不等于"端点会返回哪些码"**——三个共用关系写口(接口 1 / 3 / 5)内部会走派单写路径,派单域 605 段的既有码会原样透出来,**按端点的完整集合见「六.8」**,不要只按 602 段做前端分支 | + +### 行为级对比 + +| 行为 | 改前 | 改后 | +|------|------|------| +| 同一辆车同一天既跑团级行程又接送逐户 | 占用层 `requireShareableClaims` 两两遍历时按城市衔接(R3-EX)判,跨城即拒;派单层同样拒 | 车务在团期配车页人工确认共用关系后,同属一个关系的成员互相放行,不要求城市衔接 | +| `precheck`/`candidates` 对已确认共用关系的车/司机 | 仍显示阻断冲突(标红),写口能提交但页面选不上 | `blocking=false`,前端可正常选中 | +| 恢复本属共用关系的接送派单 | 只认城市衔接,跨城会被 605016 拒绝 | 若离场原因是"占用被释放触发的自动离场",可放回原关系;人工移出的不放回 | +| `clearAll`/内部 `release` 后幸存的共用派单 | `clearAll`:不碰 `fleet_assignment`,跨城组合非法占用留在库里但不被任何检查发现;内部 `release`:同上,共用关系与占用不被处置 | 两条路径都在同一事务内解除共用关系并对幸存占用做实际处置(保留/真释放+待改派) | +| 团级正式需求状态推进 | `DISPATCHED` 状态在配车就绪后不会自动推进 | 就绪回调(`vehicle-ready`)事务内核追加一步,把活跃正式需求从 `DISPATCHED` 推进到 `DONE` | +| 就绪判定接受的需求状态 | 无独立就绪判定 | 硬拦第三项(需求已确认)接受集 = `{CONFIRMED, DISPATCHED, DONE}`,完成后再读仍判就绪,不会"越完成越不就绪" | + +--- + +## 六.7、影响评估 + +- **是否破坏向后兼容**: 否,对已有接口(`precheck`/`candidates`/`reconfigure`/`restore-cancel`)都是**新增字段**或**既有字段语义扩展**,不删除、不改类型、不改路径;`reconfigure` 新增的 `survivorPolicy` 只在特定条件下必填,不影响未共用场景的既有调用方式。 +- **前端是否必须同步上线**: **是(部分)**——就绪两档展示(`GET .../readiness`)与共用关系的四个新端点若不接入,车务在团期配车页无法完成人工确认动作,共用关系机制整体无法使用;`precheck`/`candidates` 若不改读新字段,会出现"写口已放行、页面仍标红/选不中"的体验矛盾,属高优先级同步项。`restore-cancel`/内部两个端点即使前端暂不改动也不会 400,只是体验上可能有恢复被误拒的情况。 +- **前端 workaround 清理点**: + - `precheck` 冲突展示如果此前是"直接按 `cityJunctionShareCandidate` 取反判断是否阻断",需要改读新的 `blocking` 字段; + - 若有"就绪=覆盖完整"这类前端自行推断逻辑,需要删除,改为直接读 `GET .../readiness` 的 `ready`/`warned`; + - 🔴 **2026-09-20 网关实测 + 源码复核订正(本节原描述已被 #8004 撤销)**:前端把 `候选面 cityJunctionShareCandidate=true` 理解为"同城衔接"**是对的,保持不变**;要判"能不能选"请改读新字段 `shareEligible`(或等价的 `!blocking`),展示文案按 `reasonCode` 区分两种情况。 + +--- + +## 六.8、错误码全集(按端点对照) + +本节回答前端写错误分支时的两个问题:**什么时候会拿到它**、**UI 该不该给重试按钮**。 + +🔴 **为什么要单独列这一节**:本模块新建的码按"码段"看是 602100-602114,但**按端点看不是这个集合**——三个共用关系写口(接口 1 / 3 / 5)内部都会调派单写路径,派单域 605 段的**既有**码会原样透到它们的响应里。只照 602 段写分支,会漏掉两个后果最重的:需要车务二次确认才能继续的 **605036**,和必须**禁用**重试按钮的 **605075**。 + +**覆盖边界(照实说)**:下表是逐端点追调用路径得到的**已查证会抛出**的集合,不是"除此之外一定不会出现别的码"的穷举承诺——派单写路径上还有一批只在更少见条件下成立的守卫。**前端兜底分支照常要写**:拿到表外的码时按前三位归类即可,`605xxx` 一律按"派单侧拒绝——展示 `message` 原文、不自动重试"处理。 + +### 通用码(接口 1-8 共有) + +| 码 | 什么时候拿到 | 前端处置 | +|----|------------|---------| +| 401 | 未登录 / token 失效 | 网关拦截,跳登录 | +| 403 | 请求头 `X-Admin-Role` 不是 `VEHICLE_MANAGER` / `SUPER_ADMIN` | 不可重试;按角色控制入口可见性(不是权限点模型,见「⚠️ 关键变化」鉴权订正) | +| 100001 | 入参非法(枚举值错、必填缺、格式错) | 不可重试;本地校验后重提 | +| 100000 | 服务端兜底系统异常 | 可重试一次;连续出现即报障 | + +接口 9 / 10 是内部端点,不走后台角色门禁,无 401/403。 + +### 逐端点 + +**接口 1 `POST .../share-groups`(确认共用关系)** + +| 码 | 什么时候拿到 | 可否重试 / 前端处置 | +|----|------------|-------------------| +| 602100 | 成员少于 2 个 | 不可重试:本地先拦 | +| 602101 | 成员超过 20 个 | 不可重试:本地先拦 | +| 602102 | 成员在该资源日无活跃占用(常见于用了过期的 `sourceId`) | 不可重试:先调接口 2 回读最新 `sourceId` 再提交 | +| 602103 | 成员派单不属于本团,或团期身份未知 | 不可重试:移除该成员 | +| 602104 | `serviceDate` 不在团期服务日窗内 | 不可重试:该日不能建共用关系 | +| 602105 | 团级配车成员不属于本团或当日非活跃 | 不可重试:刷新本团配车行后重选 | +| 602106 | 成员已属于另一个共用关系 | 不可重试:先解除原关系(接口 3) | +| 602107 | `costBearer` 缺失或非法 | 不可重试:本地先拦 | +| 602109 | 同一关系被并发改写 / 两个请求抢建同一关系 | ✅ **可重试**:原样重发 | +| 100502 | 10 秒内重复提交同一份成员全集 | 不报红:提示"请勿重复提交",不是失败 | +| **605036** | 被派入的成员司机与这辆共用车不是常驻组合 | 🔴 **需车务表态**:原样展示 `message`,确认后带 `confirmCrossResident=true` **重发同一份请求** | +| **605075** | 抢到资源锁后租约丢失,本次未提交 | 🔴 **重试无效**:**不给重试按钮**,提示核对并发操作后重新发起 | +| 605008 | 抢锁超时,系统繁忙 | ✅ **可重试**:给重试按钮 | +| 605037 / 605038 / 605070 | 该成员的车维保停用 / 司机休假待激活 / 司机不存在 | 不可重试:换资源或把该成员移出本次成员全集 | + +**接口 2 `GET .../share-groups`(查询)** — 无业务错误码;`resourceType` 传非法值返 100001(不是 602114)。 + +**接口 3 `DELETE .../share-groups/{shareGroupId}`(解除)** + +| 码 | 什么时候拿到 | 可否重试 / 前端处置 | +|----|------------|-------------------| +| 602108 | 关系不存在或已解除 | 不可重试:提示已不存在,重拉接口 2 刷新 | +| 602109 | 解除过程中关系行/成员行被并发改写 | ✅ **可重试**:原样重发 | +| 602110 | 存在幸存共用派单却没传 `survivorPolicy` | 不可重试:弹策略选择框后带参重发 | +| 602111 | `KEEP_LEGAL` 下幸存成员之间不满足衔接规则 | 不可重试:`message` 会点名冲突对,改用 `REASSIGN`/`RELEASE` | +| 602112 | 占用账本重建后幸存集合与期望不符 | 不可重试:整请求已回滚,报障 | +| 605008 | 抢锁超时 | ✅ **可重试** | +| **605075** | 资源锁已失效,本次未提交 | 🔴 **重试无效**:不给重试按钮 | +| 605042 | 某成员的司机通知结果正在确认 | ✅ 可稍后重试 | +| 605043 | 某成员已出发或已不是可清状态 | 不可重试:卡在成员上,需人工处理该成员 | +| 605047 | 某成员行程已结束,派车信息只读 | 不可重试:卡在成员上 | +| 605009 | 某成员派单不存在 | 不可重试:先调接口 2 刷新成员列表 | + +**接口 4 `GET .../readiness`(就绪判定)** + +| 码 | 什么时候拿到 | 可否重试 / 前端处置 | +|----|------------|-------------------| +| 602113 | 取不到本团的配车就绪基线,判定失败关闭 | ✅ 可重试;**不要**把它渲染成"未就绪"的红灯,这是"没有结论"不是"判定为否" | +| 602114 | 就绪查询入参非法 | 不可重试:本地先拦 | + +**接口 5 `POST .../reconfigure`(整团逐日配车提交)** — 本单只新增 `survivorPolicy` 与四个响应字段,原有码沿用 `#7442`,不重复展开:602000 / 602001 / 602002 / 602003 / 602004 / 602005 / 602006 / 602009 与 600003 / 600004 / 600005 / 600006 / 600007 / 600008 / 600009 / 600010 / 600011。其中 600008(团期配车被并发修改)、600009(权威基线不可用)、602005(用车需求已更新)**可重试**,其余为入参/状态类,不可重试。 +🔴 **本单新增的透出**:`clearAll=true` 触发幸存处置时,接口 3 那张 605 表(605008 / 605075 / 605042 / 605043 / 605047 / 605009)在本接口**同样适用**,处置一致,整请求回滚。 + +**接口 6 `POST /admin/fleet/assignments/precheck`** — 无业务错误码,只读咨询恒 `code=200`,冲突信息在 `conflicts[]` 里返回;入参非法返 100001。 + +**接口 7 `POST /admin/fleet/assignments/candidates`** — 同上,无业务错误码;入参非法返 100001。 + +**接口 8 `POST /admin/fleet/assignments/{assignmentId}/restore-cancel`(撤销取消的派单)** + +| 码 | 什么时候拿到 | 可否重试 / 前端处置 | +|----|------------|-------------------| +| 605008 | 抢锁超时 | ✅ **可重试** | +| **605075** | 资源锁已失效,本次未提交 | 🔴 **重试无效**:不给重试按钮 | +| 605042 | 司机通知结果正在确认 | ✅ 可稍后重试;本次保持 `canceled`,无任何副作用 | +| 605009 | 派单不存在 | 不可重试:刷新列表 | +| 605015 | 超出撤销窗口,或该派单不是 `canceled` 态 | 不可重试:撤销入口置灰 | +| 605016 | 已被其它单占用,无法恢复 | 不可重试:引导改派 | +| 605017 | 仅取消操作本人可撤销 | 不可重试:按钮对非本人置灰 | +| 605037 | 车辆已维保或停用 | 不可重试:引导换车改派 | +| 605038 | 司机已休假或待激活 | 不可重试:引导换司机改派 | + +**接口 9 `POST /internal/fleet/dispatch/group-batch/{groupBatchId}/release`(内部幂等释放)** — 无业务错误码,幂等,重复调用零写入不报错。 + +**接口 10 `POST /v3/internal/group-batch/{groupBatchId}/vehicle-ready`(内部回填就绪)** — 无业务错误码;**业务性未生效不用错误码表达**,一律 `code=200` + `applied=false` + `discardReason`/`discardCode`(仅 809205 / 809206 两种取值);只有数据库/并发异常才落兜底 100000。 + +--- + +## 七、不影响范围 + +- **仅影响**: 管理后台团期配车页(共用确认交互、就绪两档展示)、车务派单页(预校验/候选列表标红变绿、撤销恢复) +- **零影响**: + - C 端小程序/H5 全部功能 + - 订单创建、订单详情、产品/资源模块 + - `GET /v3/internal/group-batch/{groupBatchId}/dispatch-baseline`(复用不改,仅供 fleet 内部拉取基线,前端不直连) + - 网关路由:`Path=/admin/fleet/**` 通配路由已覆盖全部新增端点,无需新增路由配置;鉴权按「⚠️ 关键变化」的角色门禁订正,未新增也未依赖任何权限码 + - 散客(非团期)派单:共用关系的主键含 `group_batch_id`,只在团期作用域内生效,不影响散客场景的既有冲突判定规则 + - `CityJunctionChecker`/R3-EX 判定逻辑本身:未被修改,共用关系是它之前的一层放行,未命中时仍按原规则判 + - **`POST /admin/fleet/assignments`(新建派单)的请求/响应契约本身零变化**,未列入「二、变更接口清单」;但它内部调用的 `assertOverlapsResolvable`/`assertNoGroupDispatchConflict`(`doCreateInLock` 内,`AssignmentService.java:2935/2940/2945`)与本单改造的守卫是**同一份实现**,所以新建派单时同样会认已确认的共用关系——前端若发现"直接新建一条本属共用关系的派单也不再被跨城误拒",这是本单的连带效果,不是另一处改动 + +--- + +## 八、测试环境已验证 + +**环境**:网关 `https://api.test.1814.love:9443`,分支 `dev-v3`,部署点 `bb4091074`。 +**部署事实**【实测】:`hl-fleet-service` 2026-09-21 17:14:32 产出新 jar(111247255 B),实例 8187(PID 1598964)、8087(PID 1599321)滚动重启并健康检查 UP;`hl-order-service-v3` 17:16:07 产出新 jar(328980166 B),实例 8186(PID 1607261)、8086(PID 1608074)UP。两次任务 `exit_code=0`,服务端 `git sync done: branch=dev-v3 HEAD=bb4091074`。 +**读数口径**:鉴权与业务失败**都走响应信封**(HTTP 200 + `code`),下表 `code` 列取自响应体 `code` 字段,不是 HTTP 状态行。前端同样必须每次都读 `code`。 + +### 8.1 经网关逐端点实测(2026-09-21,车务角色账号,单次登录) + +| # | 端点 | 本次请求形态 | 实测 `code` | 读数 | +|---|------|------------|-----------|------| +| 1 | `POST .../batches/{id}/share-groups` | 合法 body + 不存在的 `groupBatchId` | `600009` | `团期配车权威基线不可用,请稍后重试或检查团期状态`——请求已穿透网关进入业务层并在基线读取处失败,路由与鉴权通 | +| 2 | `GET .../batches/{id}/share-groups` | 真实团期 + `includeReleased=true` | `200` | `data: []`(该团当前无共用关系,空集是合法读数) | +| 3 | `DELETE .../share-groups/{id}` | 不带 `survivorPolicy` | `602110` | `存在幸存共用派单,必须指定处置策略`——与「接口 3」正文声明逐字一致,也实证了该参数在框架层故意非必填(声明成必填会被 Spring 挡成 400,这个业务码就永远抛不出来) | +| 3b | `DELETE .../share-groups/{id}` | 带 `survivorPolicy=KEEP_LEGAL` + 不存在的关系 | `602108` | `共用关系不存在或已解除`——与正文声明一致 | +| 4 | `GET .../batches/{id}/readiness` | 真实团期(两个样本) | `200` | 两档结构完整:`ready=false`、`warned=true`、`blockers[]` 含 `BLOCK_GROUP_MISSING` / `BLOCK_GROUP_DAYS_INCOMPLETE`(逐服务日一条,带 `tripDate`/`groupCode`)/ `BLOCK_NOT_CONFIRMED`,`warnings[]` 含 `WARN_SEAT_SHORTAGE`。字段名、层级与本文「接口 4」出参表一致 | +| 4b | `GET .../batches/{id}/readiness` | 整团免车的团期 | `602113` | `无法取得本团的配车就绪基线, 就绪判定失败关闭: 正式用车需求未声明任何乘车分组(整团免车的团期不判配车就绪)`——**失败关闭**,没有降级成 `ready=true`,与「接口 4」业务边界声明一致 | +| 5 | `POST .../batches/{id}/reconfigure` | `demands` 为空 / 逐日项缺字段 | `600002`、`400` | 依次命中 `逐日配车需求不能为空` 与字段级校验 `乘车分组不能为空; 派出车辆 ID 不能为空`,入参校验链路在位 | +| 6 | `POST /admin/fleet/assignments/precheck` | 不存在的车/司机 + 日期区间 | `200` | `conflicts[]` 每项都带齐 `type`/`conflictAssignmentId`/`conflictOrderNo`/`conflictDateRange`/`cityJunctionShareCandidate`/**`blocking`**/**`reasonCode`**/**`shareGroupId`**/`msg`——本单新增的三个字段**确实出现在响应里**(本次样本为资源不存在类冲突,故 `blocking=true`、`reasonCode=null`、`shareGroupId=null`,与出参表「资源不存在类冲突恒为 true / 为 null」一致);`warnings[]` 同时返回 | +| 7 | `POST /admin/fleet/assignments/candidates` | 带 `orderId` 的候选查询 | `200` | 正常返回车辆/司机候选分页,每车带 `selectable`/`availabilityReasonCode`/`availabilityWindows`/`residentMatch`/`crossResident`/`requiresCrossResidentConfirmation`/`relationMessage`/`conflicts` 等列。⚠️ **不传 `orderId` 时直接返回 `400 字段【order_id】未填写`**——这是本端点的既有必填约束,前端拼参时不要漏 | +| 8 | `POST /admin/fleet/assignments/{id}/restore-cancel` | 不存在的派单 | `605009` | `派单不存在`——与「接口 8」错误码表一致 | + +以上 8 个 `/admin/fleet/**` 端点**全部经网关可达**,响应结构与本文档契约一致。写口一律用**零副作用的错误路径**取证(不存在的团期 / 不存在的关系 / 不存在的派单 / 故意缺参),测试环境未因取证产生任何业务数据变更。 + +### 8.2 两个 internal 端点不在网关面上(结构性,不是失败) + +`POST /internal/fleet/dispatch/group-batch/{id}/release` 与 `POST /v3/internal/group-batch/{id}/vehicle-ready` 经网关实测均返回 `code=403`、`message=接口不可访问`。 + +这是**设计如此**:`hl-gateway` 的 `JwtAuthFilter` 在所有鉴权分支之前先判 `isInternalPath(path)`(`/internal`、`/internal/`、`/v3/internal`、`/v3/internal/` 前缀,并做了矩阵参数归一化防绕过),命中直接 `forbidden(exchange, "接口不可访问")`,注释写明「service-to-service only, never exposed to clients」。**这两个端点本来就不面向前端**,它们由 `hl-order-service-v3` / `hl-fleet-service` 之间的 Feign 调用触发,前端不需要、也不可能直接调。本文档收录它们只是为了说明「团期流团 / 车辆就绪回调」这两条链路上前端会看到的**间接后果**(共用关系被连带 `RELEASED`、团级需求推进到 `DONE`)。 + +### 8.3 本次未在网关上取证的业务路径(覆盖边界,前端照常可开工) + +下列路径需要一条**真实的、活跃的共用关系**及其成员派单作为前置,构造它会在共享测试环境上产生不可逆的业务数据,本次取证未构造: + +- 共用关系的 happy path:确认成功后 `precheck` / `candidates` 对该车返回 `blocking=false`、`reasonCode=SHARE_GROUP_CONFIRMED`、`shareEligible=true`、`shareGroupId` 非空。 +- `DELETE`(`REASSIGN` / `RELEASE`)与 `reconfigure`(`clearAll=true`)真释放幸存占用后 `pendingReassignSourceIds[]` 的内容。 +- 自动收缩 `RELEASED` + `AUTO_SINGLE_MEMBER` 的实际落库。 + +这三条的**契约形状**(字段名、类型、枚举值、错误码)已按 `origin/dev-v3` 源码逐项核对并写进上面的接口小节,前端按文档实现即可;它们影响的是「后端行为的实证深度」,不影响「前端怎么写这段代码」。 + +--- + +## 十、相关文档 + +- 关联 Issue: [wx/HL#7444](https://git.1814.love:8443/wx/HL/issues/7444) +- 关联 PR: [wx/HL#7956](https://git.1814.love:8443/wx/HL/pulls/7956)(PR-1)、[wx/HL#7959](https://git.1814.love:8443/wx/HL/pulls/7959)(PR-2a)、[wx/HL#7963](https://git.1814.love:8443/wx/HL/pulls/7963)(PR-2b) +- 前置工单: `#7442`(V-3 配车写口与恢复流程)、`#7443`(V-4 接送机与团期身份)——两者均已先行合入 `dev-v3`;相关 changelog 见 `changelogs-v2/2026-09/19_7442_团期配车受控重开窗口计划刷新状态收口-新增接口-管理后台.md`(与本单共享"团期配车"域,内容不重叠,`reconfigure`/`reopen`/`plan-refresh` 等端点的基础契约在那份文档) +- 后续工单: V-6 团期车费分摊(挂起,待定稿核团分摊口径;本单交付的 `costBearer`/`costSourceRefNo` 是它的输入) +- 相关权限/门禁背景: `hl-user-service/src/main/resources/db/migration/V20260911_007__fix_group_dispatch_admin_grants.sql`(工单 #5626/#7440,记载 `/admin/fleet/**` 角色门禁 vs 权限码的历史订正,见本文档「⚠️ 关键变化」鉴权订正条) +- 方案文档: `docs/tasks/group-batch-vehicle/团期车务实施方案.md` §3.2、§4(V-5.a 就绪判定、V-5.c 同团共用放行)、§8.8、§8.11 + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7444](https://git.1814.love:8443/wx/HL/issues/7444) +- **PR**: [#7956](https://git.1814.love:8443/wx/HL/pulls/7956)(PR-1)、[#7959](https://git.1814.love:8443/wx/HL/pulls/7959)(PR-2a)、[#7963](https://git.1814.love:8443/wx/HL/pulls/7963)(PR-2b) +- **Merge commit**: [d97babc9e](https://git.1814.love:8443/wx/HL/commit/d97babc9e)(PR-1)、[43a5b7153](https://git.1814.love:8443/wx/HL/commit/43a5b7153)(PR-2a)、[19be7f21c](https://git.1814.love:8443/wx/HL/commit/19be7f21c)(PR-2b) + +### 联系人 + +- **后端负责人**: @wx