--- schema: "hl-changelog/v2" ticket: "7527" title: "团期发起核单端点(TRIP_FINISHED → REVIEWING)" consumer: "admin" author: "jw(GIT)" change_type: "新增接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "pending" frontend_owner: "mmg" frontend_ref: "" target_release: "" verified_at: "2026-09-12" status_note: "团期详情/看板需加「发起核单」按钮。重复点击返 200 且 alreadyStarted=true,前端应据此提示「该团期已在核单中」而非再弹一次成功。录成本自动推进的老行为不变,两条入口并存。" updated_at: "2026-09-12" base: "dev-v3" --- # order-v3: 团期发起核单端点(TRIP_FINISHED → REVIEWING) **服务**: hl-order-service-v3 **PR**: #7568 **Issue**: #7527 --- ## ⚠️ 关键变化 🔴 **补的是一个「团期卡死」的入口缺失,不是新功能。** `TRIP_FINISHED → REVIEWING` 这一跳此前 **唯一入口是录共享成本的副作用**。于是**不用大巴、没请导摄的团根本没有共享成本可录**, 就没有触发点,团期永久停在「出行完毕」——既进不了核单,也归不了档, 八桶看板的「核团中」桶对这类团恒为空。 🔴 **两条入口并存,老行为一行未改。** 「先点发起核单再录成本」与「直接录第一笔成本自动推进」 都继续可用。前端不需要为本端点调整任何现有调用。 🔴 **重复点击返 200,不是报错。** 用 `data.alreadyStarted` 区分:`false` = 本次真的推进了; `true` = 调用前团期已在核单中,本次是幂等命中、**零写入零时间线**。 前端若只看 HTTP 码,重复点会连弹两次「已发起核单」。 --- ## 一、背景 团期生命周期第 6 跳(出行完毕 → 核单中)没有人工入口。运营在八桶看板上点不动「核团中」桶, 只能等财务去改库,或者随便录一笔 0 元成本把状态机「骗」过去。 这是把一个**流程动作**绑死在一个**数据录入动作**上的后果。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 发起核单 | POST | `/v3/admin/order/group-batch/{groupBatchId}/review/start` | 新增 | TRIP_FINISHED → REVIEWING 的显式人工入口;重复点幂等返 200 | --- ## 三、接口详情 ### 1. 发起核单 `POST /v3/admin/order/group-batch/{groupBatchId}/review/start` **VO**: `GroupBatchReviewStartRespVO` #### 使用场景 hl-ui 管理后台「团期详情 / 团期看板」的「发起核单」按钮。团期返团后(状态「出行完毕」)由 运营或财务点一下,把团期推进到「核单中」,之后才能走验团归档。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | groupBatchId | Path | Long | ✅ | 雪花 ID,不存在返 589500 | 团期主订单 ID | **无请求体。** #### 出参字段表 | 字段 | 类型 | 说明 | |------|------|------| | groupBatchId | String | 团期主订单 ID(Long 经 ToStringSerializer 序列化,防精度丢失) | | batchStatus | String | 推进后的团期状态,**恒为** `REVIEWING` | | batchStatusDesc | String | 状态中文名,恒为「核单中」 | | alreadyStarted | Boolean | `false`=本次调用完成了推进;`true`=调用前团期**已在**核单中,本次为幂等命中、未产生任何写入与时间线 | #### 请求示例 ```http POST /v3/admin/order/group-batch/2089679374176464898/review/start Authorization: Bearer ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "groupBatchId": "2089679374176464898", "batchStatus": "REVIEWING", "batchStatusDesc": "核单中", "alreadyStarted": false }, "success": true } ``` #### 空数据 / 降级响应 重复点击(团期已在核单中)返 200,`alreadyStarted=true`,**零写入、零时间线**: ```json { "code": 200, "message": "成功", "data": { "groupBatchId": "2089679374176464898", "batchStatus": "REVIEWING", "batchStatusDesc": "核单中", "alreadyStarted": true }, "success": true } ``` 时间线写入失败时**不回滚状态推进**,接口仍返成功(时间线不是主链路)。 #### 错误响应 | 码 | 符号 | 触发 | 建议前端提示 | |----|------|------|------| | 589500 | `GROUP_BATCH_NOT_FOUND` | groupBatchId 查不到团期 | 团期不存在 | | 589507 | `GROUP_BATCH_PERMISSION_DENIED` | 角色未持 `group-batch:finance:advance` | 无操作权限 | | 589564 | `GROUP_BATCH_REVIEW_START_STATUS_INVALID` | 状态早于「出行完毕」(招募中/资源准备中/物料准备中/待出发/出行中),或库中为脏值 | 「团队还没返团,返团后才能发起核单」 | | 589565 | `GROUP_BATCH_REVIEW_START_AFTER_SETTLED` | 团期已验团归档 | 「该团已验团归档,如需重新核单请先做验团反确认」,并给一个跳转到反确认的入口 | | 589566 | `GROUP_BATCH_REVIEW_START_DISBANDED` | 团期已流团 / 取消 | 「该团已流团,不能发起核单」,按钮应置灰 | ```json { "code": 589564, "message": "当前团期状态不可发起核单(须为「出行完毕」)", "data": null, "traceId": null, "success": false } ``` ```json { "code": 589565, "message": "团期已验团归档,如需重新核单请先做验团反确认", "data": null, "traceId": null, "success": false } ``` ```json { "code": 589566, "message": "团期已流团,不可发起核单", "data": null, "traceId": null, "success": false } ``` #### 业务边界 - **幂等**:由 CAS 的 `from = TRIP_FINISHED` 条件天然保证。重复调用与并发双击的败者都落到 `alreadyStarted=true` 的成功分支,不会写出第二条时间线。前端无需做防抖去重(做了更好)。 - **三个错误码刻意分开**,不共用通用的 589501「团期状态不允许当前操作」:这三种拒绝对运营的 下一步动作完全不同(等团回来 / 去做验团反确认 / 这团已经流了),共用一个码前端只能给出 一句无法行动的提示。 - **成功后团期进入「核单中」**,共享成本录入端点 `POST .../settlement/cost` 与验团归档端点 `POST .../settle` 随之可用。 - 权限码与录成本端点**同一个**(`group-batch:finance:advance`):今天能录成本的人就能发起核单, 没有人凭空多出权限,前端的按钮可见性判据可直接复用录成本那一套。 --- ## 四、契约约束与正确调用方式 - **不要把 200 当成「推进成功」**:必须看 `data.alreadyStarted`。`true` 时提示「该团期已在核单中」, 不要再弹一次「发起成功」。 - **不要用它做状态查询**:本端点是写动作,读团期状态请用团期详情接口。 - **不要为它做重试**:幂等安全,但重试没有意义——失败的三个码都是业务拒绝,重试结果相同。 - **589565 要给出口**:提示里带上「验团反确认」的跳转,对应端点 `POST /v3/admin/order/group-batch/{groupBatchId}/settle/reopen`。 - **老入口不变**:`POST /v3/admin/order/group-batch/{groupBatchId}/settlement/cost` 在 `TRIP_FINISHED` 上录第一笔成本仍会自动推进到 `REVIEWING`,行为一行未改。 --- ## 五、数据库行为 **无表变更、无 Flyway、无新增列。** 本端点只写两处既有的地方: - `order_group_batch.batch_status`:由 `TRIP_FINISHED` CAS 更新为 `REVIEWING`(幂等命中时不写); - `group_batch_status_log`:追加一条 `event_type=BATCH_TRIP_END` / `from_status=TRIP_FINISHED` / `to_status=REVIEWING` / `content=发起核单(管理员手动)` 的时间线(幂等命中时不写)。 时间线写入失败仅记 WARN 降级,**不回滚状态推进**——流水不是主链路,不能因为它写失败把 状态推进撤销掉。 --- ## 六、边界行为 | 场景 | 行为 | |---|---| | 团期在「出行完毕」 | 200,`alreadyStarted=false`,状态变「核单中」,时间线 +1 | | 团期已在「核单中」 | 200,`alreadyStarted=true`,**零写入、零时间线** | | 并发双击 / 与录成本并发 | 只有一个请求 CAS 成功;败者重读状态后也返 200 `alreadyStarted=true` | | 团期早于「出行完毕」 | 589564 | | 团期已「已结算」 | 589565 | | 团期已「已取消」(流团) | 589566 | | 团期不存在 | 589500 | | 无权限 | 589507,**零落库** | | 库中 `batch_status` 为枚举外脏值 | 589564(业务码,**不是 500**) | | 时间线写入失败 | 状态已推进,接口仍返 200 | --- ## 七、不影响范围 - **`POST /v3/admin/order/group-batch/{groupBatchId}/settlement/cost` 一行未改**——其内嵌的 `TRIP_FINISHED → REVIEWING` 副作用保持原样,两条入口并存。 - 验团归档 `POST .../settle` 与验团反确认 `POST .../settle/reopen` 未改动。 - 无 Mapper 改动(复用既有 `casAdvanceToReviewing`)、无 Entity 改动、无网关路由改动 (命中已有 `/v3/admin/**` 通配)、无 hl-user-service 权限种子改动(复用既有权限码)。 - 核团业务内容(核团四表、主次报账人记账、成本分摊)**不在本单**,仍归 GB-ADM-050~052。 --- ## 八、测试环境已验证 **部署**:`hl-order-service-v3` @ `feat/7527-group-batch-review-start` / `9f3ee37d9`,双实例滚动重启完成。 **全部实测经真实网关 `api.test.1814.love:9443` + Bearer 鉴权**,载体是 TEST 上一个 0 子订单的空闲团期 `2089679374176464898`。 | 场景 | 请求 | 响应 | |---|---|---| | 出行完毕 → 发起 | `POST .../2089679374176464898/review/start` | 200,`alreadyStarted=false`,`batchStatus=REVIEWING`;库 `batch_status` 变 `REVIEWING`;`group_batch_status_log` 新增 `BATCH_TRIP_END / TRIP_FINISHED → REVIEWING / 发起核单(管理员手动)` | | 再点一次 | 同上 | 200,`alreadyStarted=true`;状态不变,时间线**条数不变** | | 出行中 | 同上 | 589564「当前团期状态不可发起核单(须为「出行完毕」)」 | | 已结算 | 同上 | 589565「团期已验团归档,如需重新核单请先做验团反确认」 | | 已取消 | 同上 | 589566「团期已流团,不可发起核单」 | | 团期不存在 | `POST .../999999999/review/start` | 589500「团期不存在」 | | 无权限角色 | 同上(roleKey=GUIDE / CUSTOMER_SERVICE) | 589507 | | **老行为零回归** | 在「出行完毕」上 `POST .../settlement/cost` | 200,库 `batch_status` 仍自动变 `REVIEWING` | > 前置条件达成方式:`TRIP_FINISHED` / `TRAVELLING` / `SETTLED` / `CANCELLED` 四态在 TEST 上一个都不存在 > (它们由 `GroupBatchLifecycleJobService` 按班期日期定时推进),故用 SQL 直更 `batch_status` 逐个构造前置态, > 取证后已还原为原始的 `RESOURCE_PREPARING`。**该路径没有验到状态机上游的推进逻辑,只验本端点自身的判据。** **本地全量**:`mvn -o -pl hl-order-service-v3 -am test` → **9935 例 / Failures 0**; `RedLineArchTest` / `LayerEnforcementTest` / `MapperBoundaryArchTest` 均通过(非 Skipped)。 本单新增 10 例单测。 --- ## 十、相关文档 - Issue:https://git.1814.love:8443/wx/HL/issues/7527 - PR:https://git.1814.love:8443/wx/HL/pulls/7568 - 兄弟端点(验团归档 / 反确认):`POST /v3/admin/order/group-batch/{groupBatchId}/settle`、`.../settle/reopen` --- ## 关联 / 联系人 - 后端:jw - 前端(hl-ui 管理后台):mmg - 需求定案:wx