diff --git a/changelogs-v2/2026-09/12_7527_团期发起核单端点-新增接口-管理后台.md b/changelogs-v2/2026-09/12_7527_团期发起核单端点-新增接口-管理后台.md new file mode 100644 index 00000000..d800b95a --- /dev/null +++ b/changelogs-v2/2026-09/12_7527_团期发起核单端点-新增接口-管理后台.md @@ -0,0 +1,278 @@ +--- +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