docs(changelog): #7442 PR-C2 受控重开窗口 + 三份补部署清单与 dispatch-baseline 条目
changelog-filename-gate / validate (push) Failing after 2s
changelog-filename-gate / validate (push) Failing after 2s
新增 PR-C2 那份(受控重开窗口 + 计划刷新状态收口),并给三份都补了此前缺的 「部署清单」一节 —— AC-19 ③ 逐字要求部署约束写进 PR 正文与 changelog, 实测 PR 正文有、三份 changelog 全文零命中。 滚动顺序逐 PR 分别分析,没有照抄同一份危险描述: - PR-C2:🔴 逆序会静默退化 —— order-v3 先滚,旧 fleet 收到 dispatchable=true 直接放行重配,而它不读 reconfigureWindow、不校验令牌与范围,两端日志都正常、 没有任何报错。所以 fleet 必须先于 order-v3。 - PR-A / PR-C1:两个方向都不静默出错(fleet 先滚会整体报 602009,是明确错误码 不是静默放行),如实写清与 PR-C2 的区别。 同时补记 GET /v3/internal/group-batch/{id}/dispatch-baseline —— 对 origin/dev-v3 查证,该端点响应体被 #7442 改过两次(8eb8e13cd 加四字段、c6aa1224f 加 reconfigureWindow),三份 changelog 此前都漏记。 三份都带上了「别按直接 pom 依赖查消费方」的警告:grep -rl 'hl-common-core' */pom.xml 只命中 gateway 与 hl-finance,其余六个服务经 hl-common-web / hl-starter-* 传递引入, 照那份清单部署漏掉的恰恰是本单真正改了的 order-v3 与 fleet。 Refs #7442 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
@@ -284,6 +284,36 @@ POST /admin/fleet/group-dispatch/batches/1934567890123456800/reconfigure
|
||||
|
||||
---
|
||||
|
||||
## 部署清单(本单改了 hl-common-core,CODE_RULES §16.6)
|
||||
|
||||
本单在 `hl-common-core` 的 `GroupBatchDispatchBaselineDTO`(团期配车权威基线)新增 `requirementId`/
|
||||
`requirementVersion`/`requirementStatus`/`groups` 四个字段,并新增 `GroupBatchVehicleGroupBaselineDTO`
|
||||
(乘车分组基线)全新类;order-v3 是这份基线的提供方,fleet 是消费方。
|
||||
|
||||
### 滚动顺序分析:本单与 #7957(PR-C2)的"fleet 必须先滚"不同——本单两个方向都不会静默出错
|
||||
|
||||
- **order-v3 先滚**:order-v3 开始下发新字段(`groups[]` 等),此时 fleet 还是旧版——**旧版 fleet 压根没有
|
||||
本单新增的 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure` 端点**(该端点是本单
|
||||
才新增的),不存在任何消费方读取这些新字段,**零风险**。
|
||||
- **fleet 先滚**:fleet 的新端点已经存在,但调用旧版 order-v3 的基线接口时**拿不到 `groups` 字段**
|
||||
(Jackson 反序列化为 `null`)——fleet 侧的按组覆盖校验属于**失败关闭**设计(602009「无法取得本团的权威
|
||||
乘车分组清单」),**这是本单与 PR-C2 最大的不同**:PR-C2 的 `dispatchable` 放宽会被旧 fleet 静默忽略、
|
||||
放行了本不该放行的请求;本单缺 `groups[]` 则是**整个新端点在这段时间内全部请求都报 602009**——响应是
|
||||
**明确的错误码,不是静默放行**,运营/车务会立刻发现"这功能用不了"而不是"这功能用了但结果不对"。
|
||||
- **结论**:两个方向都不会产生数据错乱,区别只是"功能完全不可用一段时间"(fleet 先滚)还是"零影响"
|
||||
(order-v3 先滚)。**仍然建议同批滚**(消除过渡期报错),但万一要分批,**order-v3 先滚风险更低**。
|
||||
|
||||
### 消费方清单不能按 `pom.xml` 直接依赖关系查
|
||||
|
||||
理由与判据同 `19_7442_团期配车受控重开窗口计划刷新状态收口-新增接口-管理后台.md`「部署清单」节——
|
||||
`grep -rl 'hl-common-core' */pom.xml` 只命中 `hl-gateway`/`hl-finance`,order-v3 与 fleet 都经
|
||||
`hl-common-web` 传递引入,不在直接依赖清单里,但正是本单真正改了代码的两个服务。
|
||||
|
||||
⇒ **实际部署单位共 7 个**:gateway / user / resource / product-v2 / order-v3 / mp / fleet;
|
||||
本单不强制要求特定先后顺序,但**同批滚**仍是最稳妥的做法。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 团期配车页的整团逐日提交动作
|
||||
|
||||
@@ -0,0 +1,802 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7442"
|
||||
title: "团期配车受控重开窗口 + 计划刷新状态收口(PR-C2)"
|
||||
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: "代码已合入 dev-v3(PR #7957,merge commit c6aa1224fb46c3a7671da8234886075efde3c4ed),测试服尚未部署到含本次改动的版本,backend_status/gateway_status 暂记 pending,不满足发布门禁。本文档是 #7442 PR-C(团级确认态与配车恢复流程)的第三份交接件:PR-B(fleet 确认整团配车 + order-v3 回写已发车务)已由 17_7442_团级确认态-需求已发车务回写-新增接口-管理后台.md 交付并 deployed;本文档只覆盖 PR-C2(受控重开窗口 + 计划刷新状态收口)新增/改造的 4 个端点,不重复 PR-B 内容。部署完成并网关实测后需回填 backend_status=deployed、gateway_status=verified。"
|
||||
updated_at: "2026-09-19"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# fleet/order-v3: 团期配车受控重开窗口 + 计划刷新状态收口(PR-C2)
|
||||
|
||||
> **存放目录**: changelogs-v2/{YYYY-MM}/
|
||||
>
|
||||
> **服务**: hl-fleet-service (端口 8082)、hl-order-service-v3 (端口 8083)
|
||||
> **PR**: [#7957](https://git.1814.love:8443/wx/HL/pulls/7957)
|
||||
> **Issue**: #7442 PR-C2(AC-22)
|
||||
> **日期**: 2026-09-19
|
||||
> **影响范围**: 团期已过资源准备阶段后的「受控重开配车」恢复流程——新增受控重开窗口开启端点、新增 fleet 侧计划刷新/覆盖查询两个内部端点、既有「整体确认需求」端点新增受控重投分支
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **设计稿里的独立 `reconfirm` 端点已取消**:早期设计曾计划新增
|
||||
`POST /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/reconfirm`,**该端点从未上线**。
|
||||
受控重开窗口内的「重新确认」动作最终挂在**既有**的整体确认端点
|
||||
`POST /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm` 上,服务端按需求当前状态自动路由到
|
||||
受控重开分支——**前端不需要调用任何新端点来完成重新确认,仍调那个熟悉的「确认」按钮对应的端点即可**。
|
||||
2. **确认成功 ≠ 团期立刻恢复推进**:受控重开窗口内的确认只是把「计划刷新」这件事可靠登记下来,
|
||||
响应新增 `planRefreshState`/`planRefreshOutboxId`/`advanceUnblocked` 等六个字段,其中
|
||||
**`advanceUnblocked` 恒为 `false`**——阻断解除发生在 fleet 侧就绪回调通过两级判定之后,不在本次调用里。
|
||||
**前端必须据此做等待或轮询**,不能在确认成功的瞬间就告诉运营「好了」。
|
||||
3. **响应字段名与 AC-22 原文不同**:AC-22 原文写的是 `planRefreshRequeued`(Boolean)+
|
||||
`planRefreshReplayCount`,但那不是最终落地的字段——2026-09-18 的订正已判定实际字段是
|
||||
`planRefreshReplayKind`(枚举 `REQUEUED`/`WOKEN`/`NOOP_IN_FLIGHT`/`NOOP_ALREADY_PICKABLE`/`NOOP_RACED`,
|
||||
五取值不能合成两档)+ `planRefreshReplayCount`。**本文档按代码实际字段名记录**。
|
||||
4. **「10 秒内重复提交返回上次结果」这句话不完全准确**:`POST /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure`
|
||||
(fleet 整团逐日配车提交端点,非本文档新增/改造范围,完整契约见补登文档
|
||||
`19_7442_团期配车分组写口reconfigure补登-新增接口-管理后台.md`;受影响的幂等语义与本次改动强相关)的
|
||||
`@Idempotent` 窗口是 10 秒,但窗口**内**重复提交是**拒绝**(返 100502「团期配车重配处理中,请勿重复提交」),
|
||||
不是「返回上次结果」;「提交同一份计划返回上次结果(`idempotentShortCircuit=true`)不是失败」这件事
|
||||
与 10 秒防重窗口是**两套独立机制**(前者按计划内容摘要判重,没有时间窗限制),详见「四、契约约束」。
|
||||
5. **错误码数量核对**:工单 AC-22 原文写「25 个新错误码」(fleet 16 + order-v3 9),本次逐个核对源码后
|
||||
实际落地 **21 个**——fleet `GroupDispatchAdminErrorCode` 602000-602014 共 **15 个**(不是 16)、
|
||||
order-v3 `GroupDispatchReconfigureErrorCode` 809203/809204/809205/809206/809209/809210 共 **6 个**
|
||||
(不是 9)。差额 4 个(fleet **602015**、order-v3 **809202/809207/809208**)是**登记但明确不建**的号
|
||||
(类注释里有完整的「0 调用即删」红线不可达论证/裁定记录,不是本单遗漏或漏抛),按 AC-22 数字去核对会
|
||||
误以为少了 4 个。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 受控重开正式用车需求 | POST | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/reopen` | 新增 | 团期管理员开一个带令牌/范围/有效期的窗口,让车务在窗口内改车 |
|
||||
| 2 | 整体确认需求(放行房务) | POST | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm` | 改造(入参不变,出参新增 6 字段) | 受控重开窗口内的重新确认改走 `confirmWithCoveragePrecheck`,新增受控重投分支 |
|
||||
| 3 | 按需求身份刷新团期配车计划 | POST | `/internal/fleet/dispatch/group-batch/{groupBatchId}/plan-refresh` | 新增(内部) | order-v3 的耐久命令执行面,让 fleet 按新需求身份重刷计划并重发就绪意图 |
|
||||
| 4 | 查询团期配车覆盖现状 | GET | `/internal/fleet/dispatch/group-batch/{groupBatchId}/coverage` | 新增(内部) | 重新确认前的快速失败预检,不是权威判定 |
|
||||
| 5 | 团期配车权威基线 | GET | `/v3/internal/group-batch/{groupBatchId}/dispatch-baseline` | 改造(内部,补记;PR-A 首次扩充 + 本次 PR-C2 追加 `reconfigureWindow`) | 新增受控重开窗口字段,`dispatchable` 语义放宽 |
|
||||
|
||||
> 本单不改动 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure`(整团逐日配车提交)、
|
||||
> `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/confirm`(确认整团配车)、
|
||||
> `POST /v3/internal/group-batch/{groupBatchId}/vehicle-requirement/dispatched`(回写已发车务)
|
||||
> 三个端点的契约——它们分别属于 #7442 更早的 PR-A、PR-B,PR-B 两个端点已由
|
||||
> `17_7442_团级确认态-需求已发车务回写-新增接口-管理后台.md` 交付;PR-A 的 `reconfigure` 端点**至今没有任何
|
||||
> changelog 记录**,本文档不越权补写,已在交付报告中单独提示。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 受控重开正式用车需求 `POST /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/reopen`
|
||||
|
||||
**VO**: `GroupVehicleRequirementReopenReqVO → GroupVehicleReopenRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期已经过了「资源准备」这个允许配车的阶段(进入 `MATERIAL_PREPARING`/`PENDING_DEPARTURE`),但客观情况变了
|
||||
(临时加人、换车),团期管理员在团期详情页点「受控重开」,显式开一个带令牌、带范围(哪几个乘车分组、哪几天)、
|
||||
带有效期的窗口,把已配车的正式需求打回 `PENDING_RECONFIRM`,同时清 `vehicle_ready`(团期推进随即被出团门②挡住)。
|
||||
车务拿到 `windowToken` 后在配车页重配时必须原样带回。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | 是 | - | 团期主订单 ID |
|
||||
| reason | Body | String | 是 | ≤200 | 重开原因,写进需求时间线留痕 |
|
||||
| scopeGroupCodes | Body | List\<String\> | 是 | 1-20 个 | 授权可改的乘车分组编码;必须是当前需求已有的组,越界 400 |
|
||||
| scopeDates | Body | List\<LocalDate\> | 是 | 1-60 个 | 授权可改的行程日;必须落在 scopeGroupCodes 各组服务日并集内 |
|
||||
| windowMinutes | Body | Integer | 否 | 10-1440,默认 120 | 窗口有效分钟数;同时是本轮计划刷新的超时阈值 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| requirementId | String(雪花 ID) | 被重开的正式需求 ID |
|
||||
| requirementVersion | Integer | 重开后的需求版本,**与重开前相同**(重开不递增版本) |
|
||||
| requirementStatus | String | 恒为 `PENDING_RECONFIRM` |
|
||||
| windowToken | String | 一次性窗口令牌,车务重配时必须原样带回,否则 fleet 报 602012 |
|
||||
| expiresAt | LocalDateTime | 窗口失效时刻 |
|
||||
| blockedStage | String | 重开时的团期阶段快照 |
|
||||
| batchStatus | String | 团期当前状态(重开不回退它) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
POST /v3/admin/order/group-batch/1934567890123456800/vehicle-requirement/reopen
|
||||
{
|
||||
"reason": "客户临时增加 2 人,需要加一辆车",
|
||||
"scopeGroupCodes": ["BUS-A"],
|
||||
"scopeDates": ["2026-10-01"],
|
||||
"windowMinutes": 120
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"requirementId": "1934567890123456789",
|
||||
"requirementVersion": 3,
|
||||
"requirementStatus": "PENDING_RECONFIRM",
|
||||
"windowToken": "a1b2c3d4e5f6...",
|
||||
"expiresAt": "2026-09-19 14:00:00",
|
||||
"blockedStage": "PENDING_DEPARTURE",
|
||||
"batchStatus": "PENDING_DEPARTURE"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
N/A。同一操作人在窗口有效期内重复调用本端点是幂等成功,原样返回既有令牌(不算「空数据」)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809203,
|
||||
"message": "该团已有生效中的重开窗口(开窗人 1001,2026-09-19 14:00:00 失效),请等其结束或由本人操作",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
可能的错误码:
|
||||
- `809100` - 该团期无活跃正式需求
|
||||
- `809101` - 需求状态/分组形态不允许重开(整团免车的团不许重开;`CONFIRMED` 只在刷新已失败时可重开)
|
||||
- `809111` - 团期阶段不允许重开(不在 `RESOURCE_PREPARING`/`MATERIAL_PREPARING`/`PENDING_DEPARTURE`)
|
||||
- `809203` - 已有他人开的生效中窗口
|
||||
- `809209` - 上一轮计划刷新尚未结束
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **鉴权**: 沿用团期需求既有权限点 `group-batch:demand:confirm`,本单不新增权限点
|
||||
- **幂等性**: `@Idempotent` 5 秒窗口 + 同一操作人在生效窗口内重复调用原样返回既有令牌
|
||||
- **窗口过期后续开是合法路径**: `PENDING_RECONFIRM` 且窗口已过期时可以再开一次窗口(不算异常状态),这是本端点唯一能让「到点车还没排完」的团解开的入口——不放行的话这个团会永久锁死在阻断态
|
||||
- **整团免车(零分组)的需求不能重开**:这类团在 fleet 侧压根没有配车行,「重新配车」对它没有意义,要改成需要车须先撤回免车声明
|
||||
|
||||
---
|
||||
|
||||
### 2. 整体确认需求(放行房务) `POST /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm`
|
||||
|
||||
**VO**: `无请求体(Path only) → GroupBatchRequirementConfirmRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
本端点是 #7210 起就有的既有端点,团期管理员在团期详情页点「确认」——本次改造前该端点只负责住宿+车侧的整体确认;
|
||||
本次改造后,Controller 改调 `confirmWithCoveragePrecheck`:受控重开窗口内的重新确认会先向车务问一次覆盖现状
|
||||
(不满足抛 809204),然后才进入既有的幂等/锁/事务流程。**普通整团确认(未受控重开的绝大多数场景)行为一字未变**,
|
||||
只有当活跃需求处于 `PENDING_RECONFIRM` 且有阻断留痕(即经上面「reopen」端点重开过)时才会走新增分支。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | 是 | - | 团期主订单 ID |
|
||||
|
||||
本端点无请求体,入参与改造前完全一致(零变化)。
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
出参在既有字段基础上新增 6 个受控重开专用字段(其余既有字段——`groupBatchId`/`requirementConfirmed`/
|
||||
`dispatchedOrderIds`/`skippedOrderIds`/`dispatchedCount`/`vehicleDispatchedOrderIds` 等——本次零改动,不在此重复列出):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| planRefreshState | String | `PENDING`(刷新命令已登记、尚未成立,首次调用几乎恒为此值)/ `DONE`(罕见:回调已在响应前先落库)/ `FAILED`(重入路径读到的上次失败态);**非受控重开链路为 null** |
|
||||
| planRefreshOutboxId | String(雪花 ID) | 本轮刷新命令的 Outbox ID,排障用;非受控重开链路为 null |
|
||||
| planRefreshReplayKind | String | 本次受控重投的落点:`REQUEUED`(终态命令已重新入队)/ `WOKEN`(退避已清,立即可领)/ `NOOP_IN_FLIGHT`(命令正在跑)/ `NOOP_ALREADY_PICKABLE`(本就待领)/ `NOOP_RACED`(并发抢跑);**首次确认(非重投)与非受控链路一律为 null** |
|
||||
| planRefreshReplayCount | Integer | 人工受控重投累计次数;达到 5 次后再点确认会收到 809210,此时提示应改为「请联系后台排查」 |
|
||||
| blockedStage | String | 团期阻断阶段快照(重开时记下的团期状态);非阻断为 null |
|
||||
| advanceUnblocked | Boolean | **恒为 `false`**——解除阻断只发生在 fleet 就绪回调通过两级判定之后,绝不在确认这一刻 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
POST /v3/admin/order/group-batch/1934567890123456800/requirement/confirm
|
||||
```
|
||||
|
||||
(本端点无请求体)
|
||||
|
||||
#### 响应示例(受控重开窗口内的重新确认,首次调用)
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "1934567890123456800",
|
||||
"requirementConfirmed": true,
|
||||
"dispatchedOrderIds": [],
|
||||
"skippedOrderIds": [],
|
||||
"dispatchedCount": 0,
|
||||
"vehicleDispatchedOrderIds": [],
|
||||
"transferDispatchedOrderIds": [],
|
||||
"vehicleSkippedOrderIds": [],
|
||||
"vehicleDispatchedCount": 0,
|
||||
"groupVehicleRequirementId": "1934567890123456789",
|
||||
"groupVehicleRequirementStatus": "CONFIRMED",
|
||||
"groupVehicleRequirementVersion": 4,
|
||||
"planRefreshState": "PENDING",
|
||||
"planRefreshOutboxId": "1934567890123456999",
|
||||
"planRefreshReplayKind": null,
|
||||
"planRefreshReplayCount": 0,
|
||||
"blockedStage": "PENDING_DEPARTURE",
|
||||
"advanceUnblocked": false
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
普通整团确认(非受控重开链路)时,上述 6 个受控重开字段**整块为 null/false**——这是合法形态,前端据此判断
|
||||
「这次确认要不要等刷新结果」;不要把整块 null 当成异常。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809204,
|
||||
"message": "车务配车尚未覆盖完整,不能重新确认: 乘车分组 BUS-A 缺失 2026-10-01",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
可能的错误码(受控重开分支新增,普通确认分支错误码零变化):
|
||||
- `809204` - 受控重开链路且车务配车仍未覆盖完整(fail-closed:Feign 降级/异常同样抛此码,零写入)
|
||||
- `809210` - 人工受控重投次数已达上限(达 5 次)
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **预检只在受控重开链路上发 Feign**:活跃需求不在 `PENDING_RECONFIRM`、或没有阻断留痕时,`confirmWithCoveragePrecheck` 直接跳过预检——普通整团确认因此一次远端调用都不多,不受本次改造影响
|
||||
- **受控重投是零写入的三种情形不扣重投预算**:命令正在跑(`NOOP_IN_FLIGHT`)/ 本就待领(`NOOP_ALREADY_PICKABLE`)/ 并发抢跑(`NOOP_RACED`)——只有 `REQUEUED`/`WOKEN` 才真的推进了一次,才计入 `planRefreshReplayCount`
|
||||
- **`planRefreshState=DONE` 时重复确认是幂等成功**:原样返回、零写入,不会对已闭环的刷新再发一次
|
||||
- **本端点没有单独的防重提交窗口**:`@Idempotent` 挂在既有的 `confirm` 方法上(5 秒),预检 Feign 调用被移到幂等窗口与锁窗口**之外**执行,避免一次 Feign 最坏墙钟穿透幂等窗口与 30 秒锁租约
|
||||
|
||||
---
|
||||
|
||||
### 3. 按需求身份刷新团期配车计划 `POST /internal/fleet/dispatch/group-batch/{groupBatchId}/plan-refresh`
|
||||
|
||||
⚠️ **[内部接口,不对前端开放]** order-v3 的 `GROUP_DISPATCH_PLAN_REFRESH` 耐久命令执行面,Feign 直连调用。
|
||||
|
||||
**VO**: `GroupDispatchPlanRefreshReqDTO → GroupDispatchPlanRefreshRespDTO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期管理员在受控重开窗口内点「重新确认」后,order-v3 在同一事务里登记一条计划刷新耐久命令;命令处理器随后(异步、
|
||||
无事务上下文)调用本端点,让 fleet 把计划行上钉住的需求身份推进到新版本,并重新发出就绪意图。按
|
||||
`(requirementId, requirementVersion)` 幂等:身份已对上时不递增计划版本,只把就绪意图重新放回投递链路。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | 是 | - | 团期主订单 ID |
|
||||
| requirementId | Body | Long | 是 | - | 刷新后计划应当钉住的正式团级用车需求 ID |
|
||||
| requirementVersion | Body | Integer | 是 | ≥1 | 刷新后计划应当钉住的需求版本 |
|
||||
| sourceRefNo | Body | String | 否 | ≤64 | 幂等追溯号(order-v3 侧 Outbox 命令 ID),仅用于日志对账 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| groupBatchId | String(雪花 ID) | 团期主订单 ID |
|
||||
| planVersion | Long | 刷新后的计划版本;已按同一身份刷过时返回上次的值且不递增 |
|
||||
| planDigest | String | 刷新后的计划摘要 |
|
||||
| versionBumped | Boolean | 本次是否真的递增了计划版本(幂等命中时为 false) |
|
||||
| readyIntentEmitted | Boolean | 就绪意图是否处于可投递态(幂等命中时仍为 true,但需靠受控重投真的推回 PENDING 才算数) |
|
||||
| readyIntentRequeued | Boolean | 本次是否把一条已终态(SUCCESS/QUARANTINED)的就绪意图推回了 PENDING;意图本就在途时为 false |
|
||||
| aliveCount | Integer | 参与本次计划的活跃配车行数 |
|
||||
| definitiveFailure | String | 判定性失败原因;`IDENTITY_STALE`(需求身份已陈旧)/ `COVERAGE_INCOMPLETE`(库里配车仍有覆盖缺口);null=成功 |
|
||||
| gaps | Array\<String\> | 覆盖缺口描述;仅 `COVERAGE_INCOMPLETE` 时非空 |
|
||||
| legacyGroupRowCount | Integer | 本团存活派车行里无分组键的历史行数(非错误,仅留痕) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
POST /internal/fleet/dispatch/group-batch/1934567890123456800/plan-refresh
|
||||
{
|
||||
"requirementId": "1934567890123456789",
|
||||
"requirementVersion": 4,
|
||||
"sourceRefNo": "1934567890123456999"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "1934567890123456800",
|
||||
"planVersion": 8,
|
||||
"planDigest": "a1b2c3d4...",
|
||||
"versionBumped": true,
|
||||
"readyIntentEmitted": true,
|
||||
"readyIntentRequeued": false,
|
||||
"aliveCount": 9,
|
||||
"definitiveFailure": null,
|
||||
"gaps": [],
|
||||
"legacyGroupRowCount": 0
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
判定性失败以 **HTTP 200** 表达,不是 4xx/5xx——在传输层它与「远端暂时不可用」完全同形,而两者的正确处置相反:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "1934567890123456800",
|
||||
"planVersion": 7,
|
||||
"planDigest": "旧摘要...",
|
||||
"versionBumped": false,
|
||||
"readyIntentEmitted": false,
|
||||
"readyIntentRequeued": false,
|
||||
"aliveCount": 9,
|
||||
"definitiveFailure": "COVERAGE_INCOMPLETE",
|
||||
"gaps": ["乘车分组 BUS-A 缺失 2026-10-01"],
|
||||
"legacyGroupRowCount": 0
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
真正可重试的故障(DB 不可用、CAS 并发冲突、该团零活跃配车行)仍以异常形式返回失败 Result,由 order-v3 侧命令处理器退避重试:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 602014,
|
||||
"message": "本团没有活跃配车行,无法刷新配车计划",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
可能的错误码:
|
||||
- `602014` - 该团零活跃配车行(不产出空计划)
|
||||
- `600008` - 计划版本 CAS 被并发推进
|
||||
- `600009` - 基线不可用
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **判定性失败 vs 可重试故障必须分开**:`IDENTITY_STALE`/`COVERAGE_INCOMPLETE` 由调用方立刻转终态 `FAILED`、不再指数退避空转;数据库不可用等由既有重试机制处理
|
||||
- **幂等命中时仍会把就绪意图推回可投递态**(受控重投):不是只回一个旧 `eventId` 就宣称重发过——`readyIntentRequeued=true` 才说明「回调丢失」这个场景真的被修复了
|
||||
- 本端点无 admin 入口,仅供内部 Feign 调用
|
||||
|
||||
---
|
||||
|
||||
### 4. 查询团期配车覆盖现状 `GET /internal/fleet/dispatch/group-batch/{groupBatchId}/coverage`
|
||||
|
||||
⚠️ **[内部接口,不对前端开放]** order-v3 消费方在受控重开窗口内「重新确认」之前的快速失败预检调用。
|
||||
|
||||
**VO**: `GroupDispatchCoverageQueryReqVO → GroupBatchDispatchCoverageDTO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
对应「三、接口详情」第 2 条里 `confirmWithCoveragePrecheck` 调用的正是本端点。order-v3 先问一句「库里这套车现在够不够」,
|
||||
够就放行确认,不够就当场报 809204 并把缺口念给管理员听——否则确认会成功、刷新命令随后被 fleet 判定性终结,管理员要等到
|
||||
刷新失败才知道车没排满。**它是快速失败的 UX 门,不是权威判定**,权威判定在 fleet 刷新事务内部(同一把团级锁、同一次当前读)。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | 是 | - | 团期主订单 ID |
|
||||
| requirementId | Query | Long | 是 | - | 按哪一份正式团级用车需求判覆盖 |
|
||||
| requirementVersion | Query | Integer | 是 | ≥1 | 按需求的哪一版判覆盖;与基线当前版本不一致时返回 `satisfied=false` 而不是抛错 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| groupBatchId | String(雪花 ID) | 团期主订单 ID |
|
||||
| satisfied | Boolean | 库里现存配车是否已完整覆盖权威需求;需求身份与入参不一致时同样返回 false |
|
||||
| gaps | Array\<String\> | 缺口描述(逐条给人看);`satisfied=true` 时为空列表 |
|
||||
| missingGroupCodes | Array\<String\> | 权威清单声明了、但一辆车都没排的组码——**本端点是这个字段唯一有意义的宿主**,重配写口的同名字段在任何路径上都只能是空列表(真的缺组时走抛 602002 的路径,根本不产生响应体) |
|
||||
| legacyGroupRowCount | Integer | 本团存活派车行里无分组键的历史行数,与重配/刷新响应上的同名字段同源同算法(#7442 AC-32) |
|
||||
| planVersion | String(雪花 ID) | fleet 当前计划版本;该团尚无计划时为 null |
|
||||
| planRequirementId | String(雪花 ID) | fleet 当前计划钉住的需求 ID;尚无计划时为 null |
|
||||
| planRequirementVersion | Integer | fleet 当前计划钉住的需求版本;尚无计划时为 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
GET /internal/fleet/dispatch/group-batch/1934567890123456800/coverage?requirementId=1934567890123456789&requirementVersion=4
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "1934567890123456800",
|
||||
"satisfied": false,
|
||||
"gaps": ["乘车分组 BUS-A 缺失 2026-10-01"],
|
||||
"missingGroupCodes": [],
|
||||
"legacyGroupRowCount": 0,
|
||||
"planVersion": "7",
|
||||
"planRequirementId": "1934567890123456789",
|
||||
"planRequirementVersion": 3
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
`satisfied=false` 本身就是本端点的正常「不满足」答案,不是错误;只读接口不产出空 data。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
本团正式需求零分组(整团免车)或无需求时失败关闭:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 602009,
|
||||
"message": "无法取得本团的权威乘车分组清单: 该团正式需求整团免车",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `missingGroupCodes` 在本端点才有非空的可能,其余端点(重配写口)的同名字段在任何路径上都只能是空列表——两处含义不同,前端渲染缺口清单只能信本端点
|
||||
- 只读接口,无副作用、无锁
|
||||
- 仅供内部 Feign 调用,无 admin 入口
|
||||
|
||||
---
|
||||
|
||||
### 5. 团期配车权威基线 `GET /v3/internal/group-batch/{groupBatchId}/dispatch-baseline`
|
||||
|
||||
⚠️ **[内部接口,不对前端开放]** fleet-service 重配前拉取权威基线,仅限内部 Feign 调用。本节是**补记**——
|
||||
该端点的响应契约实际分两步被 #7442 改造(PR-A 首次扩充、本次 PR-C2 再追加一个字段),此前三份 #7442
|
||||
changelog(含本文档早前版本)都漏记了它,排查 AC-19 ③ 部署约束缺口时一并发现并补齐。
|
||||
|
||||
**VO**: `Long(Path) → GroupBatchDispatchBaselineDTO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
fleet 侧提交整团逐日配车计划(`POST .../reconfigure`)、以及受控重开窗口内重新确认(本文档第 2 条)之前,
|
||||
都要先调本端点拉一次团期权威基线,据此做完整覆盖/越界/重复校验,并判断团期当前是否可配、有没有生效中的受控
|
||||
重开窗口。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | 是 | - | 团期主订单 ID |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| groupBatchId | String(雪花 ID) | 团期主订单 ID |
|
||||
| batchStatus | String | 团期生命周期状态(透传权威值,fleet 仅记录不自行判定) |
|
||||
| departDate | LocalDate | 出团日(团期权威服务日首日);null=团期未设置日期,不可配 |
|
||||
| endDate | LocalDate | 返团日(团期权威服务日末日);null=团期未设置日期,不可配 |
|
||||
| serviceDates | Array\<LocalDate\> | 权威服务日集合(`departDate..endDate` 连续日期,含首尾) |
|
||||
| dispatchable | Boolean | 团期是否可配;**【PR-C2 语义扩展,字段名/类型未变】** 改前 = 仅 `RESOURCE_PREPARING`;改后 = `RESOURCE_PREPARING` **或**「受控重开窗口有效 **且** 团期 ∈ {`MATERIAL_PREPARING`, `PENDING_DEPARTURE`}」,见「⚠️ 关键变化」相关说明 |
|
||||
| requirementId | String(雪花 ID) | 【**PR-A 新增**】当前活跃正式团级需求 ID;无活跃需求为 null |
|
||||
| requirementVersion | Integer | 【**PR-A 新增**】当前活跃需求版本 |
|
||||
| requirementStatus | String | 【**PR-A 新增**】当前活跃需求状态:`DRAFT`/`CONFIRMED`/`DISPATCHED`/`DONE`/`PENDING_RECONFIRM`/`CANCELLED` |
|
||||
| groups | Array | 【**PR-A 新增**】权威乘车分组清单;空数组触发 fleet 侧 602009 fail-closed |
|
||||
| groups[].groupCode | String | 分组键(=需求侧 `group_code`,车费分摊 `alloc_group` 来源) |
|
||||
| groups[].vehicleType | String | 车型文本/字典值(需求侧所报) |
|
||||
| groups[].serviceDates | Array\<LocalDate\> | 本组服务日(可短于全团) |
|
||||
| groups[].dailyHeadcount | Map\<LocalDate,Integer\> | 该组该日用车人数(乘车人数,非户数) |
|
||||
| groups[].memberOrderIdsByDate | Map\<LocalDate,Array\<Long\>\> | 该组该日实际乘车子订单集合;本单只透传不消费 |
|
||||
| reconfigureWindow | Object | 【**PR-C2 新增**】受控重开窗口;无窗口时为 null;**已过期的窗口也会下发**(不是 bug,理由见下方字段说明与「业务边界」) |
|
||||
| reconfigureWindow.windowToken | String | 一次性窗口令牌 |
|
||||
| reconfigureWindow.openedAt | LocalDateTime | 开窗时刻 |
|
||||
| reconfigureWindow.expiresAt | LocalDateTime | 窗口失效时刻 |
|
||||
| reconfigureWindow.allowedGroupCodes | Array\<String\> | 授权可改的乘车分组编码白名单;空列表/null 都不代表"全都可以改",消费方拿不到明确白名单必须失败关闭 |
|
||||
| reconfigureWindow.allowedDates | Array\<LocalDate\> | 授权可改的行程日白名单 |
|
||||
| reconfigureWindow.openedBy | String | 开窗管理员 ID(留痕,关窗不清) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
GET /v3/internal/group-batch/1934567890123456800/dispatch-baseline
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "1934567890123456800",
|
||||
"batchStatus": "PENDING_DEPARTURE",
|
||||
"departDate": "2026-10-01",
|
||||
"endDate": "2026-10-06",
|
||||
"serviceDates": ["2026-10-01", "2026-10-02", "2026-10-03", "2026-10-04", "2026-10-05", "2026-10-06"],
|
||||
"dispatchable": true,
|
||||
"requirementId": "1934567890123456789",
|
||||
"requirementVersion": 4,
|
||||
"requirementStatus": "PENDING_RECONFIRM",
|
||||
"groups": [
|
||||
{
|
||||
"groupCode": "BUS-A",
|
||||
"vehicleType": "宇通33座大巴",
|
||||
"serviceDates": ["2026-10-01", "2026-10-02"],
|
||||
"dailyHeadcount": {"2026-10-01": 30, "2026-10-02": 30},
|
||||
"memberOrderIdsByDate": {"2026-10-01": [70123, 70124]}
|
||||
}
|
||||
],
|
||||
"reconfigureWindow": {
|
||||
"windowToken": "a1b2c3d4e5f6...",
|
||||
"openedAt": "2026-09-19 10:00:00",
|
||||
"expiresAt": "2026-09-19 12:00:00",
|
||||
"allowedGroupCodes": ["BUS-A"],
|
||||
"allowedDates": ["2026-10-01"],
|
||||
"openedBy": "1001"
|
||||
}
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无活跃需求或整团免车(零分组)时,`requirementId`/`requirementVersion`/`requirementStatus` 为 null、
|
||||
`groups` 为空数组;`reconfigureWindow` 无生效/曾开过窗口时恒为 null:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "1934567890123456800",
|
||||
"batchStatus": "RESOURCE_PREPARING",
|
||||
"departDate": "2026-10-01",
|
||||
"endDate": "2026-10-06",
|
||||
"serviceDates": ["2026-10-01", "2026-10-02", "2026-10-03", "2026-10-04", "2026-10-05", "2026-10-06"],
|
||||
"dispatchable": true,
|
||||
"requirementId": null,
|
||||
"requirementVersion": null,
|
||||
"requirementStatus": null,
|
||||
"groups": [],
|
||||
"reconfigureWindow": null
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
团期不存在时返回失败 Result(fleet 侧收到失败即失败关闭):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589500,
|
||||
"message": "团期不存在",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **`dispatchable` 字段名与类型没有变,语义变了**:这类变更编译、契约测试、既有单测全部看不见——消费方
|
||||
若在 PR-C2 之前就把 `dispatchable=true` 简单等同于"可以无条件重配",PR-C2 之后这个等价关系仍然成立,
|
||||
但多出了一种"受控窗口内才可配"的子情形,必须配合 `reconfigureWindow` 一起判断,不能只看这一个布尔值
|
||||
- **已过期的窗口同样下发,不是缺陷**:只有下发了,fleet 才报得出 602012「窗口已过期,请重新开窗」,而不是
|
||||
一句会把人引错方向的 600010「团期不可配」
|
||||
- **`groups` 空数组时消费方必须失败关闭**:不能因为拿不到分组就放行一份没有分母的计划(对应 fleet 侧
|
||||
602009)
|
||||
- **本端点是 `hl-common-core` 跨服务契约变更的核心承载点**:响应结构由 order-v3 提供、fleet 消费,两端
|
||||
必须同批部署,详见「部署清单」
|
||||
|
||||
---
|
||||
|
||||
## 部署清单(本单改了 hl-common-core,CODE_RULES §16.6)
|
||||
|
||||
本单在 `hl-common-core` 新增/追加了字段:`GroupBatchDispatchBaselineDTO` 新增 `reconfigureWindow` 字段
|
||||
(PR-C2,`dispatchable` 取值域同时放宽,字段名/类型未变,属**契约语义变更**);`GroupDispatchPlanRefreshReqDTO`/
|
||||
`GroupDispatchPlanRefreshRespDTO`/`GroupBatchDispatchCoverageDTO`/`GroupBatchReconfigureWindowDTO` 四个
|
||||
全新 DTO。这类"字段名没变、类型没变、DTO 类没变,只是语义变了"的改动,**编译、契约测试、单测全部看不见**,
|
||||
只有真跑起来、两端版本不一致时才会暴露,而且暴露方式还是静默的(见下)。
|
||||
|
||||
### 🔴 滚动顺序:fleet 必须先于 order-v3,顺序反了不会报错,会静默把"受控重开"变成"不受控重开"
|
||||
|
||||
- **fleet 先滚(正确顺序)**:新版 fleet 部署时,order-v3 还是旧版、不下发 `reconfigureWindow` 字段
|
||||
⇒ Jackson 反序列化该字段为 `null` ⇒ fleet 走既有的"无窗口"分支,行为与本单改动前逐字一致,**安全**。
|
||||
- **order-v3 先滚(错误顺序)**:order-v3 已按新语义把 `dispatchable` 放宽到"受控窗口有效且团期在
|
||||
`MATERIAL_PREPARING`/`PENDING_DEPARTURE`"也返回 `true`;但**旧版 fleet 不认识"受控窗口"这个概念**,
|
||||
它的判定逻辑仍是"看到 `dispatchable=true` 就直接放行重配"——旧 fleet 根本不读 `reconfigureWindow`
|
||||
字段,也就不会校验令牌、范围、有效期。⇒ **"受控重开"这层门禁在这个窗口期内整个失效,退化成"只要
|
||||
`dispatchable=true` 就能任意重配"**,而且 **fleet 端与 order-v3 端的日志都是正常的、没有任何报错**——
|
||||
两边都认为自己做的事情是对的,只有拿两版代码对照才能看出问题。
|
||||
|
||||
### 消费方清单不能按 `pom.xml` 直接依赖关系查
|
||||
|
||||
⚠️ `grep -rl 'hl-common-core' */pom.xml` 的结果里**只命中 `hl-gateway` 与 `hl-finance`**——其余六个服务
|
||||
(`hl-user-service`/`hl-resource-service`/`hl-product-service-v2`/`hl-order-service-v3`/`hl-mp-service`/
|
||||
`hl-fleet-service`)全部经 `hl-common-web`/`hl-starter-*` **传递引入**,直接查 `pom.xml` 会把本单真正改了
|
||||
代码、也真正需要重启的 order-v3 与 fleet **恰好漏掉**。
|
||||
|
||||
| 部署单位 | 与 `hl-common-core` 的依赖关系 | 是否需要本次一起滚 |
|
||||
|---|---|---|
|
||||
| hl-gateway | 直接依赖(`pom.xml` 命中) | 是 |
|
||||
| hl-user-service | 经 `hl-common-web` 传递依赖 | 是 |
|
||||
| hl-resource-service | 经 `hl-common-web` 传递依赖 | 是 |
|
||||
| hl-product-service-v2 | 经 `hl-common-web` 传递依赖 | 是 |
|
||||
| hl-order-service-v3 | 经 `hl-common-web` 传递依赖 | **是(本单直接改了这个服务的生产代码)** |
|
||||
| hl-mp-service | 经 `hl-common-web` 传递依赖 | 是 |
|
||||
| hl-fleet-service | 经 `hl-common-web` 传递依赖 | **是(本单直接改了这个服务的生产代码,且必须先于 order-v3 启动)** |
|
||||
| hl-finance | 直接依赖(`pom.xml` 命中) | 不单独部署——`packaging=jar`,无 `spring-boot-maven-plugin`,是 order-v3 的库依赖,随 order-v3 一起滚 |
|
||||
|
||||
⇒ **实际部署单位共 7 个**:gateway / user / resource / product-v2 / order-v3 / mp / fleet;**fleet 必须先于
|
||||
order-v3 启动**。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
> 本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。
|
||||
|
||||
| 场景 | 做法 |
|
||||
|------|------|
|
||||
| 受控重开窗口内重新确认 | 不需要调新端点,仍调既有「整体确认」`POST /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm` |
|
||||
| 确认成功后判断能不能继续推进团期 | 看响应 `advanceUnblocked`(恒 false)与 `planRefreshState`——`DONE` 才代表刷新已闭环;`PENDING`/`FAILED` 都要等待或引导重投 |
|
||||
| 重投次数已到 5 次 | 响应 `planRefreshReplayCount=5` 且再点确认拿到 809210,按钮文案改「请联系后台排查」 |
|
||||
| 区分「团期配车重配 10 秒内防重拒绝」与「重复提交同一份计划不是失败」 | 前者是 `@Idempotent(timeout=10)` 拒绝(100502),后者是 `idempotentShortCircuit=true` 幂等成功——两者触发条件不同,见「⚠️ 关键变化」第 4 条 |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
| 操作 | 数据库影响 |
|
||||
|------|----------|
|
||||
| 受控重开(reopen) | `order_group_vehicle_requirement` 该行 `status → PENDING_RECONFIRM`,落 `reconfigure_window_token`/`reconfigure_opened_by`/`reconfigure_expires_at`/`reconfigure_scope` 等窗口列;`version 不变` |
|
||||
| 受控重开窗口内重新确认 | 同一条 CAS 内 `status → CONFIRMED`、`version +1`、关窗(窗口列清空)、登记刷新五列(`plan_refresh_state='PENDING'`、`plan_refresh_outbox_id` 等) |
|
||||
| 受控重投 | `plan_refresh_state` 保持/转 `PENDING`,`plan_refresh_replay_count +1`(仅 `REQUEUED`/`WOKEN` 两种落点) |
|
||||
| fleet 计划刷新成功 | `fleet_group_dispatch_plan.requirement_id`/`requirement_version` 推进到新身份,`plan_version` 按幂等规则递增或维持 |
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- **窗口过期后仍可续开**:`PENDING_RECONFIRM` 且窗口已过期不算异常状态,是唯一能解开「到点车还没排完」的团的入口
|
||||
- **零分组(整团免车)的需求不能重开**:这类团没有配车行,重新配车没有意义
|
||||
- **预检 Feign 降级/异常一律 fail-closed**:`assertCoverageComplete` 捕获 `RuntimeException`、响应为空、`satisfied` 非 true 三种情形都抛 809204,绝不放行
|
||||
- **重投预算不因「无实际动作」的三种 NOOP 情形被消耗**:`NOOP_IN_FLIGHT`/`NOOP_ALREADY_PICKABLE`/`NOOP_RACED` 不扣 `planRefreshReplayCount`
|
||||
- **`plan-refresh` 判定性失败零写入**:`IDENTITY_STALE`/`COVERAGE_INCOMPLETE` 两种情形在 fleet 事务内零写入之后返回,不留半态
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举
|
||||
|
||||
### planRefreshState(GroupBatchRequirementConfirmRespVO.planRefreshState)
|
||||
|
||||
**所属字段**: `planRefreshState` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `PENDING` | 刷新中 | 刷新命令已登记、尚未成立;首次调用几乎恒为此值 |
|
||||
| `DONE` | 已闭环 | 罕见:回调已在响应前先落库 |
|
||||
| `FAILED` | 已失败 | 重入路径读到的上次失败态,需人工重投或重开窗口 |
|
||||
|
||||
### planRefreshReplayKind(GroupBatchRequirementConfirmRespVO.planRefreshReplayKind)
|
||||
|
||||
**所属字段**: `planRefreshReplayKind` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `REQUEUED` | 已重新入队 | 终态命令(FAILED/SUCCEEDED)已放回 PENDING,process_version +1 |
|
||||
| `WOKEN` | 退避已清 | PENDING 且退避未到期,已清 next_retry_at 立即可领,process_version 不变 |
|
||||
| `NOOP_IN_FLIGHT` | 在途未动 | 命令正被 worker 处理中,本次不动它 |
|
||||
| `NOOP_ALREADY_PICKABLE` | 本就待领 | 命令本就 PENDING 且已到期,下一轮取件自然会跑到 |
|
||||
| `NOOP_RACED` | 并发抢跑 | 读写之间被并发推进,不报错也不回滚调用方事务 |
|
||||
|
||||
### definitiveFailure(GroupDispatchPlanRefreshRespDTO.definitiveFailure)
|
||||
|
||||
**所属字段**: `definitiveFailure` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `IDENTITY_STALE` | 需求身份已陈旧 | 本次要钉的需求身份不是基线此刻的活跃需求,重试不会变 |
|
||||
| `COVERAGE_INCOMPLETE` | 覆盖缺口未补齐 | 库里这套车按此刻的权威分组清单还有缺口,重试不会变 |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `GroupBatchRequirementConfirmRespVO`(`POST .../requirement/confirm` 出参) | 无受控重开相关字段 | 新增 `planRefreshState`/`planRefreshOutboxId`/`planRefreshReplayKind`/`planRefreshReplayCount`/`blockedStage`/`advanceUnblocked` 六个字段,非受控重开链路为 null/false |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 团期过了资源准备阶段后需要改车 | 无解——写口直接抛 `GROUP_BATCH_NOT_DISPATCHABLE`(600010) | 团期管理员可通过 reopen 端点开一个受控窗口 |
|
||||
| 受控重开窗口内重新确认 | 端点不存在 | 复用既有确认端点,自动路由到受控重投分支 |
|
||||
| 「车辆一字未改但需要重新确认」 | 会被 `planDigest` 幂等短路,不产生新意图、不发就绪事件 | 通过 `plan-refresh` 内部端点显式刷新,幂等命中时仍受控重投一次就绪意图 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否。`requirement/confirm` 端点入参零变化,出参只新增字段(非受控重开链路全为 null/false);两个内部端点均为新增,不影响既有调用方
|
||||
- **前端是否必须同步上线**: 是——前端在受控重开链路下必须依据 `advanceUnblocked` 恒 false 做等待/轮询设计,不能在确认成功时直接提示「已恢复」;若要支持「团期管理员发起受控重开」这个新功能点,还需接入 reopen 端点
|
||||
- **前端 workaround 清理点**: 无
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 团期已过资源准备阶段后的受控重开配车恢复链路(reopen、受控重投确认分支、刷新与覆盖两个内部端点)
|
||||
- **零影响**:
|
||||
- 团期在 `RESOURCE_PREPARING` 阶段内的正常配车提交/确认流程(`reconfigure`/`confirm` 两个端点契约零变化)
|
||||
- 普通(非受控重开)整团确认的既有字段、既有错误码语义
|
||||
- `POST /v3/internal/group-batch/{groupBatchId}/vehicle-ready`/`vehicle-ready-reset` 两个内部端点(属 #7442 PR-C1/#7923,不在本单范围)
|
||||
- #7443 接送机 TRANSFER 派车行相关端点
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
> ⚠️ 测试服尚未部署到含 PR #7957 改动的版本,本节暂无网关实测数据。部署完成后按以下清单补验:
|
||||
|
||||
```
|
||||
POST /v3/admin/order/group-batch/{id}/vehicle-requirement/reopen → 待验证
|
||||
POST /v3/admin/order/group-batch/{id}/requirement/confirm(受控重投分支) → 待验证
|
||||
POST /internal/fleet/dispatch/group-batch/{id}/plan-refresh → 待验证(内部接口)
|
||||
GET /internal/fleet/dispatch/group-batch/{id}/coverage → 待验证(内部接口)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#7442](https://git.1814.love:8443/wx/HL/issues/7442)
|
||||
- 关联 PR: [wx/HL#7957](https://git.1814.love:8443/wx/HL/pulls/7957)
|
||||
- 前序文档(本文不重复其内容): `changelogs-v2/2026-09/17_7442_团级确认态-需求已发车务回写-新增接口-管理后台.md`(PR-B,已 deployed)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#7442](https://git.1814.love:8443/wx/HL/issues/7442)
|
||||
- **PR**: [#7957](https://git.1814.love:8443/wx/HL/pulls/7957)
|
||||
- **Merge commit**: [c6aa1224fb46c3a7671da8234886075efde3c4ed](https://git.1814.love:8443/wx/HL/commit/c6aa1224fb46c3a7671da8234886075efde3c4ed)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -370,6 +370,36 @@ POST /v3/internal/group-batch/1934567890123456800/vehicle-ready-reset
|
||||
|
||||
---
|
||||
|
||||
## 部署清单(本单改了 hl-common-core,CODE_RULES §16.6)
|
||||
|
||||
本单在 `hl-common-core` 新增两个全新 DTO:`GroupBatchVehicleReadyReqDTO`(就绪回调两级判定身份)、
|
||||
`GroupBatchVehicleReadyRespDTO`(两级判定结果)。order-v3 是这两个端点(`vehicle-ready`/`vehicle-ready-reset`)
|
||||
的提供方,fleet 是发起方(调用方)。
|
||||
|
||||
### 滚动顺序分析:本单两个方向都不静默出错,但"order-v3 先滚"能让保护提前生效
|
||||
|
||||
- **order-v3 先滚**:order-v3 的端点方法签名新增了一个 `@RequestBody(required = false)` 参数,此时 fleet
|
||||
还是旧版、调用时**不带任何请求体**(旧代码逻辑本就没有这个概念)——`req == null` 命中 legacy 路径,
|
||||
行为与改动前逐字一致,**安全**;等 fleet 也滚上去开始携带身份后,两级判定立刻对新产生的回调生效。
|
||||
- **fleet 先滚**:fleet 开始在请求体里携带 `requirementId`/`requirementVersion`/`planVersion`,但旧版
|
||||
order-v3 的端点方法签名**根本没有声明接收请求体的参数**——Spring 对声明之外的请求体内容视而不见,
|
||||
不会报错也不会解析失败,等价于"这个请求体被忽略了",order-v3 仍按改动前的无身份逻辑处理。**净效果**:
|
||||
两级判定这项保护在这段过渡期内**还没有生效**(旧漏洞——乱序/旧版回调污染就绪状态——仍然存在),
|
||||
但不会比改动前更糟,也不会报错或崩溃。
|
||||
- **结论**:两个方向都不会比"改动前"更差,区别只是"保护何时开始生效"。**仍建议同批滚**,让两级判定
|
||||
尽快对新回调生效,避免过渡期内继续吃"旧漏洞"的亏。
|
||||
|
||||
### 消费方清单不能按 `pom.xml` 直接依赖关系查
|
||||
|
||||
理由与判据同 `19_7442_团期配车受控重开窗口计划刷新状态收口-新增接口-管理后台.md`「部署清单」节——
|
||||
`grep -rl 'hl-common-core' */pom.xml` 只命中 `hl-gateway`/`hl-finance`,order-v3 与 fleet 都经
|
||||
`hl-common-web` 传递引入,不在直接依赖清单里,但正是本单真正改了代码的两个服务。
|
||||
|
||||
⇒ **实际部署单位共 7 个**:gateway / user / resource / product-v2 / order-v3 / mp / fleet;本单不强制
|
||||
要求特定先后顺序,但**同批滚**仍是最稳妥的做法。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: fleet→order-v3 的就绪回调链路(两个内部端点)
|
||||
|
||||
在新工单中引用
屏蔽一个用户