--- schema: "hl-changelog/v2" ticket: "7347" title: "整单核单 finalize 新增团期住宿户级闸门,未安排好住宿不允许结算(新增错误码 584130)" consumer: "admin" author: "wx(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "not_required" frontend_status: "not_required" frontend_owner: "mmg" frontend_ref: "" target_release: "" verified_at: "" status_note: "not_required 2026-09-10 mmg: 仅 finalize 错误码集合扩大新增 584130,HTTP 恒 200 判 code,请求/成功响应/方法/路径一字未改;后端自答前端唯一事=给 584130 直接展示后端 message(不二次包装)。实证该诉求已被既有通用业务码分支覆盖:request.js:512-528 未知码取 data.message 原样弹(errorBus message.error),finalize 经 settlementService.js 调用无 silentError、catch 不改写 message,后端 message 自动原样弹出,新增映射反违「不二次包装」。旧口径清理点实证无(财务核单无按住宿派生行推断能否结算逻辑)。" updated_at: "2026-09-10" base: "dev-v3" --- # 整单核单 finalize 新增团期住宿户级闸门(修改接口) > **服务**: hl-order-service-v3 > **Issue**: [#7347](https://git.1814.love:8443/wx/HL/issues/7347) > **日期**: 2026-09-10 > **影响范围**: 既有端点 `POST /v3/admin/order/{orderId}/settlement/finalize`(完成核单)的**错误码集合**扩大;不新增/不删除端点,请求体、成功响应体结构、方法、路径、网关路由**一字未改** --- ## ⚠️ 关键变化 1. **团期子订单完成核单(finalize)新增一道硬阻断**:该订单需要配房(`needsHotel=true`)但其住宿需求还没被房务推到 `DONE` 时,finalize 直接拒绝,新增业务错误码 **`584130`**。改前这种情况能直接结算通过(尤其是"一条住宿派生行都没有"的团期子订单,改前完全不受阻拦)。 2. **HTTP 状态码恒为 200**,闸门以 `Result.code = 584130` 的业务错误体返回,前端必须判 `code`,不能按 HTTP 状态识别。 3. 免房户(`needsHotel=false`)与非团期订单(`productBatchId` 为空)**完全不受影响,行为与改前逐字节一致**。 4. 管理者已对测试库执行存量统计 SQL,结果为 **0**(团期子订单里没有一单会被新闸拦住),因此**全量生效,不做灰度**,没有需要提前处理的存量数据。 5. 前端需要做的唯一事情:给 `584130` 加提示文案,见下方"前端提示文案建议"。不涉及任何请求/响应字段改动。 --- ## 一、背景 团期房务批次(`#7322`–`#7328`)把住宿的**行级**确认接进了核单:某户某晚没分平,那一行就不能被确认(`#7327` 的 584129)。但**整单核单完成(finalize)此前对住宿没有任何硬性闸门**——一个团期子订单哪怕住宿完全没安排好,只要没有已录入的住宿行,照样能走完 finalize 结算掉(既有的住宿检查落在 `SettlementCategoryCheckService`,条件是 `rowCount > 0`,0 行时天然放行)。 本单在 `performSubmitBlockingChecks`(既有 finalize 阻断检查方法)里新增一道**户级**闸门:只要该团期子订单还需要配房,就必须等房务把户级住宿需求推到 `DONE` 才允许结算。闸门用户级谓词而不是团级 `order_group_batch.hotel_ready`——团级标志只要有一户没推平就是 false,会把整团所有户的结算一起冻住,违反"不让一户卡整团"的既有原则;户级谓词与房务侧完成判定 `finalizeHotelRequirementDone` 写的是同一个字段,两侧口径天然对齐。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|---|---|---|---|---| | 1 | 完成核单(finalize) | POST | `/v3/admin/order/{orderId}/settlement/finalize` | 错误码集合扩大 | 新增可能返回 `584130`;请求体(无)、成功响应体结构、方法、路径均未改;网关路由零改动 | --- ## 三、接口详情 ### 1. 完成核单 `POST /v3/admin/order/{orderId}/settlement/finalize` **VO**: `无请求体(Path 参数 orderId)→ Result`(成功响应结构本单未改,字段集合不重复列出) #### 使用场景 核单员在核单页点击"完成核单",前端调用本端点。团期子订单在此新增一道住宿完成校验;本节判定入口与触发条件表见下方"业务边界"前的说明表。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `orderId` | Path | Long | 是 | 大于 0 | 订单 ID;本单未改 | **判定入口与触发条件**(前端可直接照此做提示判定表):新闸位于 `SettlementService.assertGroupHotelReadyForFinalize(OrderInfo)`,在既有的司机结算完成检查之后、`CASH_PAID` 缺凭证软预警之前触发(硬阻断,非软预警): | 条件 | 判定结果 | |---|---| | `productBatchId == null`(非团期订单) | **不受本单影响**,行为与改前完全一致 | | `productBatchId != null` 且 `needsHotel != true`(团期免房户) | **放行**,行为与改前一致 | | `productBatchId != null` 且 `needsHotel == true`,该户**最新一条住宿需求 `status == DONE`** | **放行** | | `productBatchId != null` 且 `needsHotel == true`,住宿需求**不存在**(一条派生行都没有,改前的漏洞场景) | 抛 **`584130`** | | `productBatchId != null` 且 `needsHotel == true`,住宿需求存在但 `status` 为 `PENDING` / `PROCESSING` / `PENDING_REVIEW` / `REJECTED_TO_CONSULTANT` / `REJECTED_TO_ADMIN`(即非 `DONE`) | 抛 **`584130`** | | 该户住宿派生行已被撤销转为 `GROUP_BATCH_PLAN_REVOKED`,但住宿需求本身仍非 `DONE` | 抛 **`584130`**(`REVOKED` 行不构成"已安排好"的证据) | 判定只读户级住宿需求的 `status` 字段,**不看有没有派生行、也不解析日期判断"是否自助订房"**——这些口径统一由房务侧的完成判定负责写 `DONE`,本闸只读结果。 #### 出参 成功响应结构本单**未改**,`Result` 字段集合与改前完全一致(不重复列出全部字段,仅摘录与本单相关的部分): | 字段 | 类型 | 说明 | |---|---|---| | `data.orderId` | Long | 订单 ID;本单未改 | | `data.finalSnapshotStatus` | String | 核单终态快照状态;本单未改 | | `data.warnings` | `List` | 软预警列表(如 `CASH_PAID` 缺凭证);本单未改,`584130` 不进这个列表,而是走错误响应 | #### 请求示例 ```http POST /v3/admin/order/1000123/settlement/finalize ``` (无请求体,仅 Path 参数 `orderId`;本单未改请求形态) #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": { "orderId": 1000123, "finalSnapshotStatus": "CONFIRMED", "warnings": [] } } ``` (成功路径响应结构本单未改,仅摘录关键字段) #### 空数据 / 降级响应 不适用:本闸不产生空数据/降级语义,判定只有"放行"或"抛 584130"两种结果。 #### 错误响应 ```json { "code": 584130, "message": "团期子订单 GB202609090001 的住宿尚未安排完成(住宿需求当前状态:PROCESSING),请等房务配房完成后再提交核单", "success": false, "data": null } ``` message 模板(`SettlementErrorCode.SETTLEMENT_GROUP_HOTEL_NOT_READY`,`SettlementErrorCode.java:339-342`)为: ``` 团期子订单 {0} 的住宿尚未安排完成(住宿需求当前状态:{1}),请等房务配房完成后再提交核单 ``` 两个占位符:`{0}` = 订单号(`orderNo`),`{1}` = 住宿需求当前状态。**住宿需求行完全不存在时**,`{1}` 填固定文案 **`未提交住宿需求`**(常量 `SettlementService.GROUP_HOTEL_REQUIREMENT_ABSENT`,逐字抄自源码,不是状态枚举值),而不是某个状态码。 #### 业务边界 - 闸门只对 `productBatchId != null`(团期子订单)生效,核心订单的房控闸是另一套逻辑(`OrderService.needsHotel != true → 视为房控通过`),本单不动它。 - 闸门读取的是"该户最新一条住宿需求"(`RequirementService.getLatestHotelRequirementByOrderId`),走既有 Service 公共只读方法,未直连 Mapper。 - 与既有"已录入住宿行全部 CONFIRMED"的行级检查(`SettlementCategoryCheckService`)并列不重叠:旧检查看的是已录入行的确认状态(行级),新闸看的是住宿需求 `status`(户级),两者判定顺序上新闸更早触发(`performSubmitBlockingChecks` 早于 `SettlementReportFlowService.prepareFinalizationInCurrentTransaction`)。 --- ## 四、契约约束与正确调用方式 - **必须按响应体 `code == 584130` 识别,不能按 HTTP 状态码识别**——HTTP 恒为 200。 - 建议直接展示后端 `message`,其中已包含订单号与住宿需求当前状态,无需前端自行拼接。 - "该团期子订单是否可以完成核单"以 finalize 实际返回结果为准,前端**不应该**通过"是否存在住宿派生行"自行推断能否结算——这正是改前的漏洞场景(0 行时误以为可以结算)。 ### 切换状态时的必要动作 无。本单不涉及任何需要调用方额外置空/切换的字段,闸门完全由后端根据现有数据判定。 --- ## 五、数据库行为 - `584130` 在阻断检查阶段抛出,属于既有 finalize 阻断检查的一部分,抛出后**不产生任何写入**(不写核单快照、不写车辆冻结、不写日志表)。 - 判定只读 `order_main`(`productBatchId`/`needsHotel`)与住宿需求表(`status`),**无表结构变更、无 Flyway**。 --- ## 六、边界行为 - 团期免房户(`needsHotel=false`):放行,不查住宿需求。 - 团期需房户,住宿需求 `status=DONE`:放行。 - 团期需房户,住宿需求缺失(零派生行):拒绝,`584130`,`message` 中状态位为"未提交住宿需求"。 - 团期需房户,住宿需求非 DONE(`PENDING`/`PROCESSING`/`PENDING_REVIEW`/`REJECTED_TO_CONSULTANT`/`REJECTED_TO_ADMIN`):拒绝,`584130`,`message` 中状态位为该实际状态值。 - 该户住宿派生行已转 `GROUP_BATCH_PLAN_REVOKED`,但需求 `status` 未到 `DONE`:仍拒绝,`584130`(`REVOKED` 行不算完成证据)。 - 非团期订单:完全不受影响,无论住宿情况如何都走改前逻辑。 - **全自订户**(每一晚都自助订房)的 `needsHotel` 依然为 `true`,其住宿需求需先由房务侧完成判定推到 `DONE` 才能通过本闸;这条口径由另一单负责写 `DONE`,本单只读结果、不重复判定。 --- ## 六.5、枚举 / 数据字典 ### 新增错误码(`SettlementErrorCode`,`hl-order-service-v3/src/main/java/com/hulalv/order/settlement/errorcode/SettlementErrorCode.java:339-342`) | code | 常量名 | message 模板 | 触发条件 | |---|---|---|---| | `584130` | `SETTLEMENT_GROUP_HOTEL_NOT_READY` | `团期子订单 {0} 的住宿尚未安排完成(住宿需求当前状态:{1}),请等房务配房完成后再提交核单` | 团期子订单需要配房,且住宿需求当前状态不是 `DONE`(含需求缺失) | ### 住宿需求状态枚举(`RequirementStatus`,本单只读不改,供理解 `{1}` 占位取值) | value | label | 是否放行本闸 | |---|---|---| | `PENDING` | 待房务配 | 否 | | `PROCESSING` | 配房中 | 否 | | `DONE` | 配房完成 | 是(唯一放行值) | | `PENDING_REVIEW` | 待审核 | 否 | | `REJECTED_TO_CONSULTANT` | 驳回 | 否 | | `REJECTED_TO_ADMIN` | 驳回 | 否 | | (需求行不存在) | —— | 否,`message` 状态位显示"未提交住宿需求" | ### 前端提示文案建议 - 直接展示 `message` 原文即可(已含订单号 + 当前状态),无需二次包装。 - 若需要更醒目的引导,可在 `message` 之外追加一句操作指引,例如:"请前往房务模块查看该团期订单的住宿安排进度,配房完成后再回来提交核单。" - 不建议把 `584130` 与其他 40xxxx/58xxxx 段错误码合并成同一个通用提示——该码语义明确(住宿未完成),合并展示会丢失"该去催房务"这条关键信息。 --- ## 六.6、修改前后对比 ### 行为级对比 | 场景 | 改前 | 改后 | |---|---|---| | 团期需房户,一条住宿派生行都没有 | **可以直接结算通过**(改前存在的漏洞) | 拒绝,`584130` | | 团期需房户,住宿需求非 `DONE`(有派生行但未分平) | 可能继续核单(仅受行级 `CONFIRMED` 检查约束,0 行时不受约束) | 拒绝,`584130` | | 团期需房户,住宿需求 `DONE` | 可核单 | 行为不变,仍可核单 | | 团期免房户 | 可核单 | 行为不变 | | 非团期订单 | 可核单(受核心订单自己的房控闸约束) | 行为不变,核心房控闸逻辑本单未动 | | 该户住宿派生行已被撤销为 `GROUP_BATCH_PLAN_REVOKED` | 若无行级检查约束可能通过 | 拒绝,`584130`(除非需求 `status` 已是 `DONE`) | ### 字段级对比 无字段变化。请求体(无)、成功响应体 `SettlementSubmitRespVO` 的字段集合均未改动,仅错误响应的 `code` 取值范围新增了 `584130`。 --- ## 六.7、影响评估 - **是否破坏向后兼容**:否。端点方法/路径/请求体/成功响应体结构均未变化;免房户、`DONE` 户、非团期订单的行为与改前逐字节一致。 - **前端是否必须同步上线**:需要新增 `584130` 的提示文案,否则该错误会被前端当成未知错误码兜底展示(用户体验较差,但不会导致请求失败或数据错误)。 - **前端 workaround 清理点**:如果前端此前依赖"住宿派生行是否存在"来判断该团期子订单是否可结算,需要清理——这个判断口径本身就是改前的漏洞,不应再使用;一律以 finalize 实际返回结果为准。 --- ## 七、不影响范围 - **仅影响**:`POST /v3/admin/order/{orderId}/settlement/finalize` 端点在团期子订单(`productBatchId != null`)且需要配房(`needsHotel=true`)时的错误码集合。 - **零影响**: - 无新增/修改/删除任何其他端点;网关路由零改动。 - 免房户(`needsHotel=false`)与非团期订单(`productBatchId` 为空)的 finalize 行为与改前完全一致。 - 成功响应体 `SettlementSubmitRespVO` 字段集合未变。 - 不读取团级 `order_group_batch.hotel_ready` 字段,不会出现"一户卡整团"的情况。 - 不涉及表结构变更、Flyway、数据迁移。 - 既有的住宿"已录入行全部 CONFIRMED"行级检查(`SettlementCategoryCheckService`)逻辑未改,新闸与它并列而非替代。 --- ## 八、测试环境已验证 - **存量影响评估**:管理者已对测试库执行存量统计 SQL(统计"团期子订单中会被新闸拦住"的数量),**结果为 0**——没有任何存量团期子订单处于会被新闸拦截的状态。因此本单**全量生效,不做灰度**,无需推平存量数据。 - **部署状态**:测试服已部署 HEAD `18c1df126`,该 HEAD 即本单的实现提交本身(PR #7433)。 - **网关验证**:端点路径/方法未变,无需新增网关路由配置;沿用既有 `/v3/admin/order/**` 路由规则。 - **兼容性结论**:端点结构不变,仅扩展业务错误码集合,前端只需新增对 `584130` 的分支处理。 --- ## 十、相关文档 - 关联 Issue: [wx/HL#7347](https://git.1814.love:8443/wx/HL/issues/7347) - 前置/关联依赖:`#7327`(团期住宿行级确认,584129 与本单 584130 相邻但语义不同)、`#7325`(户级住宿完成判定,本单读取其写入的 `status` 字段) ## 关联 / 联系人 ### 链接 - **Issue**: [#7347](https://git.1814.love:8443/wx/HL/issues/7347) - **PR**: 尚未创建(合并后回填 [#N](https://git.1814.love:8443/wx/HL/pulls/N)) - **Merge commit**: 尚未产生(合并后回填 [https://git.1814.love:8443/wx/HL/commit/sha](https://git.1814.love:8443/wx/HL/commit/sha)) ### 联系人 - **后端负责人**: @wx - **前端负责人**: @mmg