--- schema: "hl-changelog/v2" ticket: "8329" title: "团期配车确认重复调用不再返 602005:需求版本校验下沉到幂等分支之后,回写推进版本不再让重复确认变成错误" consumer: "admin" author: "wx(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "pending" frontend_owner: "mmg" frontend_ref: "" target_release: "v2.1" verified_at: "" status_note: "PR #8332 squash 合并 dev-v3(8675e1ff0)。部署:hl-fleet-service dev-v3 @ 2b4656424(2026-09-24 14:03 滚动部署完成;同批 order-v3 亦为 2b4656424)。测试服网关从零实测(自造团期 groupBatchId=2103003304906936322,正式需求 2103003334258675714 v2,3 条配车行):① reconfigure(requirementVersion=2)→ 200;② confirm 第一次(requirementVersion=2)→ 200,confirmedCount=3、alreadyConfirmedCount=0、requirementAdvanceIntent=CONFIRMED_TO_DISPATCHED,回写把正式需求推到 status=DONE、version=3;③ 用提交时的旧版本再 confirm 一次(requirementVersion=2)→ http 200、code=200、message=成功,confirmedCount=0、alreadyConfirmedCount=3。改前同一形态返 602005「用车需求已更新, 请刷新后重新配车(提交 …/v2, 当前 …/v3)」。定向单测 81 绿(GroupDispatchConfirmTest 23 / GroupDispatchServiceTest 58),fleet spotless:check 901 files clean。闸门按「基线需求状态已是 DISPATCHED/DONE(即本端点回写已落地)」放行,状态停在 CONFIRMED/PENDING_RECONFIRM 时仍 fail-closed 抛 602005。" updated_at: "2026-09-24" base: "dev-v3" --- # fleet 团期配车: 确认重复调用不再返 602005(版本校验下沉) > **存放目录**: 二期(v3) → `changelogs-v2/2026-09/` > > **服务**: hl-fleet-service(dispatch 域) > **PR**: https://git.1814.love/wx/HL/pulls/8332 > **Issue**: #8329 > **日期**: 2026-09-24 > **影响范围**: 车务「团期配车 → 确认配车」按钮的重复点击 / 刷新后重试 --- ## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写) - **重复确认不再返 602005**:整团配车确认成功后,本次确认会**推进正式用车需求版本**(回写);此前这个自己造成的版本前进会让「再点一次确认」被判成「需求已更新」,返回 602005 要求刷新重配——而刷新后拿到的还是同一个已完成的团。现在这一档返回 **200**,并以 `confirmedCount=0` / `alreadyConfirmedCount=N` 表达「本来就是已确认状态」。 - **602005 仍然存在,且判据更精确**:只有当「本次端点的回写**尚未落地**」而版本/状态又对不上时才抛(fail-closed),即真正的「业务侧改了需求,你拿的是旧版本」。 - **602007 的触发面变化**:本团一行配车行都没有(`assignedCount=0` 且 `alreadyConfirmedCount=0`)时抛 602007「本团没有可确认的配车行」,**不再**被版本校验抢先报成 602005。 - **602005 报文的两个占位符统一成 `id=…/v…` 形态**:前端**只应按错误码 602005 处置,不要解析 {0} / {1}**。 --- ## 一、背景(选填) 确认配车是定稿动作,它认下的是「这一版需求对应的这套车」,因此入参带了 `requirementId` + `requirementVersion` 作为需求身份。问题是这个端点自己有副作用:确认成功会把正式需求从 `CONFIRMED` 回写成 `DISPATCHED`,版本因此前进。改前版本校验排在幂等分支**之前**、且在事务外先跑,于是「重复确认」这条正常操作被自己造成的版本前进判成了错误。实测:`POST .../confirm {requirementId:2102982327141556225, requirementVersion:2}` → `code=602005`「用车需求已更新, 请刷新后重新配车(提交 2102982327141556225/v2, 当前 2102982327141556225/v3)」,而同刻回读需求已是 `v3`——前进正是首次确认自己的回写。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 确认整团配车 | POST | `/admin/fleet/group-dispatch/batches/{groupBatchId}/confirm` | 行为变更(校验时机 + 报文占位符口径) | 重复确认返 200;602005 只在「本端点回写未落地」时抛 | --- ## 三、接口详情 ### 1. 确认整团配车 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/confirm` **VO**: `GroupDispatchConfirmReqVO → GroupDispatchConfirmRespVO` #### 使用场景 车务在团期配车页点「确认配车」,把本团已排的车一次定稿。**没有防重提交时间窗**:连点多少次都是同一个形态,不会出现「请稍后重试」。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | groupBatchId | Path | Long | ✅ | - | 团期聚合主键 | | requirementId | Body | Long | ✅ | 须等于本团当前正式需求 | 不等于抛 602005(不受本次改动放宽) | | requirementVersion | Body | Integer | ✅ | 本次确认所依据的版本 | **语义细化(本单)**:版本不一致时——本团仍有「已派车」行待确认,或基线需求状态还停在 CONFIRMED / PENDING_RECONFIRM ⇒ 602005;基线需求状态已是 DISPATCHED / DONE(本端点回写已落地)⇒ 不抛,按 confirmedCount=0 返 200 | | remark | Body | String | ❌ | ≤200 | 仅日志留痕,**不写进配车行**(配车行 remark 参与 planDigest,刷进去会让下次重配误判「计划有变更」) | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | confirmedCount | Integer | 本次真正由 ASSIGNED 转 CONFIRMED 的行数;重复确认时为 0 | | alreadyConfirmedCount | Integer | 本次之前就已是 CONFIRMED 的行数 | | requirementId / requirementVersion | String / Integer | 本次提交的需求身份(照原样回显) | | planVersion | Integer | 配车计划版本 | | requirementAdvanceIntent | String | 回写意图,如 `CONFIRMED_TO_DISPATCHED` | | coverage | Object | 按组覆盖读数(groups / missingGroupCodes / satisfied) | #### 请求示例 ```json { "requirementId": "2103003334258675714", "requirementVersion": 2, "remark": "与地接确认车辆无误" } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "groupBatchId": "2103003304906936322", "confirmedCount": 0, "alreadyConfirmedCount": 3, "requirementId": "2103003334258675714", "requirementVersion": 2, "planVersion": 2, "requirementAdvanceIntent": "CONFIRMED_TO_DISPATCHED", "coverage": { "satisfied": true, "missingGroupCodes": [] } }, "success": true } ``` #### 空数据 / 降级响应 - 本团没有任何配车行时**不是**空成功,而是 602007(见「错误响应」)。 - 本团有配车行但已全部 CONFIRMED:200 + `confirmedCount=0`、`alreadyConfirmedCount=N`。 ```json { "code": 200, "message": "成功", "data": { "confirmedCount": 0, "alreadyConfirmedCount": 3 }, "success": true } ``` #### 错误响应 ```json { "code": 602005, "message": "用车需求已更新, 请刷新后重新配车(提交 id=2103003334258675714/v2, 当前 id=2103003334258675714/v4)", "success": false, "data": null } ``` ```json { "code": 602007, "message": "本团没有可确认的配车行, 请先提交配车计划", "success": false, "data": null } ``` #### 业务边界 - **真实的判定顺序(改后)**:事务外只做需求 **ID** 校验(602005 的一种形态);进事务、取团锁、锁内重读配车行后依次判 —— 行数都为 0 ⇒ 602007;**本端点回写是否已落地**(基线需求状态 ∈ DISPATCHED / DONE)⇒ 落地则跳过版本/状态校验(幂等重放),未落地则照旧 fail-closed 抛 602005 / 602006;随后做按组覆盖校验(602008),再 CAS 确认。 - **跳过的条件不是「没有待确认行」,而是「本端点的回写已经落地」**:状态停在 CONFIRMED / PENDING_RECONFIRM 而版本不等 ⇒ 推进版本的不是我们,是业务侧改了需求,必须照旧 602005。**不要按 assignedCount==0 一刀切。** - **需求被整份换过**(`requirementId` 都变了)时拿不到「提交版本」,报文只有 ID 那一半;两支统一拼 `id=…` 前缀。 - 重配端点(`reconfigure`)**未变**:仍整份调用身份校验。 --- ## 四、契约约束与正确调用方式(接口类必写) - **前端按码处置,不解析报文**:602005 = 需求被业务侧改过,刷新重配;200 且 `confirmedCount=0` = 本来就是已确认,无需任何动作。 - **不要把「重复确认」当成需要防抖的操作**:本端点没有防重提交时间窗,按钮可以做防抖,但服务端保证重复调用不报错。 - 确认成功后若要改车,走重配端点(`reconfigure`)而不是再点确认。 ### ✅ 正确 / ❌ 错误处置对照 ```text ✅ 200 + confirmedCount=0 → 已是确认态,直接展示「已确认」,不要提示失败 ✅ 602005 → 提示「需求已更新,请刷新后重新配车」 ❌ 把 200 + confirmedCount=0 渲染成错误或重复提交警告 ❌ 发现 602005 后原地重试同一个 requirementVersion(版本不会自己回来) ``` ### 切换状态时的必要动作 - 重配(`reconfigure`)成功后配车行回到 ASSIGNED/待确认,需要再点一次确认;本单未改这条链路。 --- ## 五、数据库行为(涉及写操作时必写) - 无表结构变更、无 Flyway。 - 确认成功的写入是「把本团存活配车行由 ASSIGNED 置 CONFIRMED」+「事务内意图记录回写需求状态」;重复确认时 CAS 命中 0 行,**零写入**。 - 602005 / 602006 / 602007 / 602008 都在写库之前抛出,可回读需求版本与配车行状态确认未变。 --- ## 六、边界行为 - 并发下的保护未变:确认前取团级变更锁并在锁内重读配车行,命中行数与 assignedCount 不符即判并发。 - 受控重开窗口(602012 / 602013)语义未变。 - 需求被整份换过仍抛 602005,不受本次放宽影响。 --- ## 六.5、枚举 / 数据字典(接口出现枚举时必写) | 码 | 触发(改后) | 报文 | |---|---|---| | 602005 | requirementId 不等于基线;或本端点回写未落地(需求状态非 DISPATCHED/DONE)而 requirementVersion 不等于基线当前版本 | 用车需求已更新, 请刷新后重新配车(提交 {0}, 当前 {1}),两个占位符均为 id=需求ID/v版本 | | 602006(不变) | 需求状态不允许配车(回写未落地时的状态判据) | 正式用车需求当前状态不允许配车: {0} | | 602007(触发面变更) | 本团 assignedCount=0 且 alreadyConfirmedCount=0 | 本团没有可确认的配车行, 请先提交配车计划 | | 602008(不变) | 库里现存计划仍不满足按组覆盖 | 配车尚未覆盖完整, 不能确认: {0} | --- ## 六.6、修改前后对比(修改/删除类接口必写,新增跳过) ### 字段级对比 | 字段 | 改前 | 改后 | |------|------|------| | 请求 / 响应字段 | — | 无新增、无删除,结构不变 | | 602005 的 {0} | 「换版」支不带前缀词 | 两支统一为 id=…/v… | ### 行为级对比 | 行为 | 改前 | 改后 | |------|------|------| | 首次确认成功 | 200,confirmedCount=N | 200,confirmedCount=N(不变) | | 需求版本已被本端点回写推进后再确认(旧版本) | **602005**(要求刷新重配) | **200**,confirmedCount=0、alreadyConfirmedCount=N | | 业务侧改了需求、本团仍有待确认行 | 602005 | 602005(不变,fail-closed) | | 业务侧改了需求、本团早已确认完(回写已落地) | 602005 | 200(幂等重放) | | 本团一行配车行都没有 | 可能先被版本校验报成 602005 | 602007 | --- ## 六.7、影响评估(修改/删除类必写) - **是否破坏向后兼容**:不破坏——只把「自己造成的版本前进」这一档从错误放宽成幂等成功;报文占位符口径变化不影响按码处置的前端。 - **前端是否必须同步上线**:不必须。建议把 confirmedCount=0 渲染成「已确认」而不是失败。 - **存量数据影响**:无迁移、无重算。 - **上线风险**:放宽的闸门以「基线需求状态已是 DISPATCHED / DONE」为唯一理由,状态未落地时判据与改前完全一致,因此不会掩盖「业务侧改需求」这一真实冲突。 --- ## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面) - **仅影响**:`POST /admin/fleet/group-dispatch/batches/{groupBatchId}/confirm` 的版本/状态校验时机与 602005 报文占位符形态。 - **零影响**: - 重配端点 `POST .../reconfigure`(仍整份校验身份)。 - 602006 / 602008 / 602012 / 602013 的判据与报文。 - 按组覆盖校验、并发锁、受控重开窗口。 - order-v3 侧对回写意图的幂等消费口径。 - readiness / overview / 配车行数据结构、数据库、网关路由。 --- ## 八、测试环境已验证 **部署读数**:`hl-fleet-service` dev-v3 @ `2b4656424`,2026-09-24 14:03 滚动部署完成。 **网关真实请求与响应**(`https://api.test.1814.love`,自造团期 `groupBatchId=2103003304906936322`,正式需求 `2103003334258675714`,3 条配车行): ```text ① POST /admin/fleet/group-dispatch/batches/2103003304906936322/reconfigure {requirementId:2103003334258675714, requirementVersion:2, demands:[3 天 × BUS × 1 辆]} → http 200, code=200;addedCount=3、aliveCount=3 ✓ ② POST .../confirm {requirementId:2103003334258675714, requirementVersion:2} → http 200, code=200, confirmedCount=3, alreadyConfirmedCount=0, requirementAdvanceIntent=CONFIRMED_TO_DISPATCHED ✓ 回读 GET /v3/admin/order/group-batch/2103003304906936322/vehicle-requirement → status=DONE, version=3(版本被本次回写推进)✓ ③ POST .../confirm {requirementId:2103003334258675714, requirementVersion:2} ← 用提交时的旧版本重复确认 → http 200, code=200, message=成功, confirmedCount=0, alreadyConfirmedCount=3 ✓(改前此处为 602005) ``` **定向测试逐类读数**(`mvn -o -pl hl-fleet-service -am test -Dtest='GroupDispatchConfirmTest,GroupDispatchServiceTest' -DfailIfNoTests=false -Dhl.surefire.failIfNoTests=false`,81 绿): | 测试类 | Tests run | |---|---| | GroupDispatchServiceTest | 58 | | GroupDispatchConfirmTest | 23 | fleet `spotless:check`:901 files clean、0 needs changes。 --- ## 十、相关文档 - 确认配车端点与 `GroupDispatchConfirmReqVO` 原始设计:#7442 - 受控重开窗口(602012 / 602013):#8308 系列 → `changelogs-v2/2026-09/` - 团期配车就绪检查:#8294 → `changelogs-v2/2026-09/24_8294_团期配车就绪检查座位不足黄牌扣司机座,新增-passengerSeatTotal-修改接口-管理后台.md` --- ## 关联 / 联系人 ### 链接 - Issue: https://git.1814.love/wx/HL/issues/8329 - 后端 PR: https://git.1814.love/wx/HL/pulls/8332 ### 联系人 - 后端: wx