diff --git a/changelogs-v2/2026-08/12_5876_核单finalize-summary出参整改-修改接口-管理后台.md b/changelogs-v2/2026-08/12_5876_核单finalize-summary出参整改-修改接口-管理后台.md new file mode 100644 index 0000000..65b5346 --- /dev/null +++ b/changelogs-v2/2026-08/12_5876_核单finalize-summary出参整改-修改接口-管理后台.md @@ -0,0 +1,470 @@ +--- +schema: "hl-changelog/v2" +ticket: "5876" +title: "核单 finalize warnings 结构化带行id + summary 新增 settled 标识(warnings 出参破坏性变更)" +consumer: "admin" +change_type: "修改接口" +author: "yst" +backend_status: "deployed" +gateway_status: "pending" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "后端 PR #5881 已 merge 到 dev-v3 并部署测试服。finalize 出参 warnings 由字符串数组改为对象数组(破坏性,按字符串渲染的前端必须改读 message 字段);summary 出参新增 settled 字段,判是否已核单改读 settled,不要再判 id==null。" +updated_at: "2026-08-12" +base: "dev-v3" +--- + +# 【⚠️ 修改接口·管理后台】核单 finalize warnings 结构化 + summary 新增 settled 标识(#5876) + +## 1. 接口背景 + +核单结算域的两个管理后台接口: + +1. **完成核单 finalize**:核单页点「完成核单」时提交,原子冻结核单事实并生成核单汇总。出参带 `warnings` 软预警(现付缺凭证等,不阻塞提交),前端用于提示定制师补传凭证。 +2. **核单汇总快照 summary**:核单页 / 财务报表页读取整单金额、成本、毛利汇总。 + +本次整改两个出参可读性问题: + +1. **warnings 无法定位行**(旧结构):warnings 是 `List`,文案形如「住宿 D2 现付缺凭证」。同一天有多条住宿/门票明细时,前端拿到的多条文案完全相同,无法区分是哪一行缺凭证,也无法跳转/锚定到具体明细行。 +2. **summary 空壳无法可靠判空**(旧行为):订单未核单时接口返回一个空壳对象,此前前端用 `id == null` 判「尚未核单」,但 `advanceSummary` 字段恒有默认空对象(不是 null),单靠字段判空容易误判。 + +## 2. 变更清单 + +| # | 变更 | 类型 | +|---|------|------| +| 1 | finalize 出参 `warnings` 由 `List` 改为 `List`(每条带 settlementId / category / dayNumber / message) | ⚠️ 破坏性 | +| 2 | summary 出参新增 `settled` 字段(Boolean):已核单 = true,未核单空壳 = false | ✨ 新增字段 | + +无入参变化、无删除字段、无 DDL。 + +## 3. 接口详情 + +### 3.1 完成核单 + +| 项 | 值 | +|---|---| +| 方法 + 路径 | POST /v3/admin/order/{orderId}/settlement/finalize | +| 接口名 | 完成核单 | +| 使用场景 | 管理后台核单页,明细核对完成后提交,原子冻结核单事实并生成核单汇总快照 | +| 认证 | 管理后台 JWT(/v3/admin/* 走网关鉴权) | +| 角色限制 | 房务角色(HOUSE)不可访问,调了会被 403 拦截 | +| 幂等性 | 非幂等写操作;重复提交会被「缺少核单终态快照 / 快照已变化」类错误码拦截,前端不要自动重试 | +| 限流 | 走网关默认限流,无接口级特殊限流 | + +### 3.2 查核单汇总快照 + +| 项 | 值 | +|---|---| +| 方法 + 路径 | GET /v3/admin/order/{orderId}/settlement/summary | +| 接口名 | 查核单汇总快照 | +| 使用场景 | 管理后台核单页 / 财务报表页读取整单金额、成本、毛利汇总 | +| 认证 | 管理后台 JWT | +| 角色限制 | 无接口级角色限制(网关鉴权通过即可) | +| 幂等性 | 只读查询,幂等 | +| 限流 | 走网关默认限流 | + +## 4. 接口入参 + +### 4.1 路径参数 + +| 接口 | 参数 | 类型 | 必填 | 说明 | +|------|------|------|------|------| +| finalize | orderId | Long | 是 | 订单 ID,必须大于 0(否则 400「订单 ID 必须大于 0」) | +| summary | orderId | Long | 是 | 订单 ID | + +### 4.2 请求体 / Query + +两个接口均无请求体、无 Query 参数。 + +## 5. 出参字段 + +### 5.1 finalize 出参(Result,本次仅 warnings 变化) + +| 字段 | 类型 | 说明 | +|------|------|------| +| summaryId | Long(String) | 新写入的 settlement_summary 主键,序列化为字符串 | +| finalSnapshotId | Long(String) | 核单终态快照 ID,序列化为字符串 | +| finalSnapshotVersionNo | Integer | 核单终态快照版本号 | +| finalSnapshotStatus | String | 核单终态快照状态,固定 FINALIZED | +| orderId | Long(String) | 订单 ID,序列化为字符串 | +| settledAt | String | 核单完成时间,格式 yyyy-MM-ddTHH:mm:ss | +| totalAmount | String | 订单总金额快照(BigDecimal 序列化为字符串,下同) | +| paidAmount | String | 已付金额快照 | +| balanceAmount | String | 尾款金额快照 | +| roomCost | String | 住宿实际成本 | +| ticketCost | String | 门票实际成本 | +| staffCost | String | 人员费用实际成本 | +| subsidyCost | String | 补助实际成本 | +| mealCost | String | 餐食实际成本 | +| vehicleCost | String | 车辆基础服务总车费 | +| otherExpenseCost | String | 其他支出实际成本 | +| insurancePremium | String | 保险实际保费(未出单/已撤单 = 0) | +| totalActualCost | String | 总实际成本 | +| driverTransferAmount | String | 给司机/主报账人转回金额(有付款类型的分类仅计 CASH_PAID,不含保险) | +| profitAmount | String | 公司毛利 = totalAmount - totalActualCost | +| profitRate | Number | 毛利率(小数,totalAmount=0 时填 0) | +| orderStatusAfter | String | 结算后订单状态,如「待财务复核」 | +| mqTriggered | Boolean | 当前版本固定 false;完成核单不发布结算 MQ | +| warnings | Array | **本次变更**:软预警对象数组(原来为字符串数组);无预警时为空数组 [] | + +**WarningItemVO 字段表**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| settlementId | Long(String) | 触发预警的核单明细行 id(settlement_hotel / settlement_ticket 主键),序列化为字符串;前端据此锚定/跳转具体明细行 | +| category | String | 核单分类枚举名,当前仅 HOTEL(住宿)/ TICKET(门票/游玩项目)两类会触发预警 | +| dayNumber | Integer | 行程第几天 | +| message | String | 预警文案,与旧字符串元素一致,如「住宿 D2 现付缺凭证」 | + +### 5.2 summary 出参(Result,本次新增 settled) + +| 字段 | 类型 | 说明 | +|------|------|------| +| settled | Boolean | **本次新增**:是否已核单。summary 快照存在 = true;未核单空壳 = false | +| id | Long | settlement_summary 主键;未核单时为 null | +| orderId | Long | 订单 ID;空壳时也有值 | +| totalAmount | String | 订单总金额快照(BigDecimal 序列化为字符串,下同) | +| paidAmount | String | 已付金额快照 | +| balanceAmount | String | 尾款金额快照 | +| roomCost | String | 住宿实际成本 | +| ticketCost | String | 门票实际成本 | +| staffCost | String | 人员费用实际成本 | +| subsidyCost | String | 补助实际成本 | +| mealCost | String | 餐食实际成本 | +| vehicleCost | String | 车辆基础服务总车费(按 Fleet 派车组合计) | +| otherExpenseCost | String | 其他支出实际成本 | +| insurancePremium | String | 保险实际保费 | +| refundTotal | String | 返还合计(只读展示,不进利润公式) | +| totalActualCost | String | 总实际成本 | +| driverTransferAmount | String | 给司机/主报账人转回金额(仅计 CASH_PAID) | +| profitAmount | String | 公司毛利 | +| profitRate | Number | 毛利率(小数) | +| settledBy | Long | 核单人 id;未核单时为 null | +| settledByName | String | 核单人姓名快照 | +| settledAt | String | 核单完成时间,格式 yyyy-MM-ddTHH:mm:ss | +| remark | String | 核单备注 | +| finalSnapshotId | Long(String) | 当前核单终态快照 ID;未完成核单或已重新打开时为 null | +| finalSnapshotVersionNo | Integer | 当前核单终态快照版本号;无当前快照时为 null | +| finalSnapshotStatus | String | 有当前快照时固定 FINALIZED,否则为 null | +| finalizedAt | String | 当前核单终态快照完成时间;无当前快照时为 null | +| finalizedByName | String | 当前核单终态快照操作人姓名;无当前快照时为 null | +| advanceSummary | Object | 已审批通过的订单预支汇总;**恒下发对象,不是 null**(空壳时也有值) | + +**advanceSummary 子对象字段表**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| approvedAmount | String | 已审批通过预支合计金额,默认 "0" | +| records | Array | 已审批通过预支记录列表,默认空数组 [];元素结构同预支接口出参 | + +> 未核单空壳时:`settled=false`,仅 `orderId` 与 `advanceSummary`(默认空对象)有值,**其余字段全部为 null**。 + +## 6. 枚举 / 数据字典 + +| 字段 | 来源 | 取值 | +|------|------|------| +| warnings[].category | SettlementCategory 枚举 | 全量:HOTEL(住宿)/ TICKET(门票/游玩项目)/ MEAL(餐食)/ VEHICLE(车辆)/ GUIDE(导游)/ PHOTOGRAPHER(摄影)/ OTHER_INCOME(其他收入)/ OTHER_EXPENSE(其他支出);**当前预警只会出现 HOTEL / TICKET** | +| finalSnapshotStatus | 快照状态枚举 | FINALIZED(已定稿) | +| orderStatusAfter | 订单状态中文文案 | 如「待财务复核」 | + +本次无枚举值增删;category 只是从隐含在文案里变成显式字段。 + +## 7. 错误码 + +### 7.1 finalize + +| code | message | 触发场景 | +|------|---------|----------| +| 581007 | 订单不存在 | orderId 查不到订单 | +| 584081 | 对账数据不一致:已付金额与支付流水和线下收款合计不符,禁止带病结算 | paid_amount 与实际入账总额不符(存量脏数据或并发漂移) | +| 584085 | 存在辅助人员结算未完成,请全部结算后提交 | 任一非主报账司机费用行未结算完成或缺转账凭证号 | +| 584321 | 当前订单缺少核单终态快照,请重新完成核单 | 缺终态快照(含重复提交场景) | +| 400 | 订单 ID 必须大于 0 | orderId 路径参数校验失败 | +| 403 | 无权限访问 | 房务角色(HOUSE)JWT 调用 | + +> finalize 还有人员费用 / 其他收入未就绪等前置门禁错误码(584xxx 段),非本次变更,按响应 message 直接提示即可。 + +### 7.2 summary + +只读接口,无业务错误码;orderId 查不到快照时返回 200 + 空壳对象(settled=false),**不会报「订单不存在」**。 + +## 8. 示例 + +### 8.1 典型成功 + +**finalize**(核单完成,D2 住宿与 D2 门票各一条现付缺凭证,同日两行靠 settlementId 区分): + + POST /v3/admin/order/2087088947225038849/settlement/finalize + +```json +{ + "code": 200, + "message": "成功", + "data": { + "summaryId": "9600000000001", + "finalSnapshotId": "9600000000002", + "finalSnapshotVersionNo": 1, + "finalSnapshotStatus": "FINALIZED", + "orderId": "2087088947225038849", + "settledAt": "2026-08-12T10:30:25", + "totalAmount": "24800.00", + "paidAmount": "24800.00", + "balanceAmount": "0.00", + "roomCost": "4280.00", + "ticketCost": "3680.00", + "staffCost": "14260.00", + "subsidyCost": "720.00", + "mealCost": "860.00", + "vehicleCost": "5200.00", + "otherExpenseCost": "1260.00", + "insurancePremium": "180.00", + "totalActualCost": "23120.00", + "driverTransferAmount": "22940.00", + "profitAmount": "1680.00", + "profitRate": 0.0677, + "orderStatusAfter": "待财务复核", + "mqTriggered": false, + "warnings": [ + { + "settlementId": "9600000000003", + "category": "HOTEL", + "dayNumber": 2, + "message": "住宿 D2 现付缺凭证" + }, + { + "settlementId": "9600000000011", + "category": "TICKET", + "dayNumber": 2, + "message": "门票 D2 现付缺凭证" + } + ] + }, + "success": true +} +``` + +**summary**(已核单订单): + + GET /v3/admin/order/2087088947225038849/settlement/summary + +```json +{ + "code": 200, + "message": "成功", + "data": { + "settled": true, + "id": "9600000000001", + "orderId": "2087088947225038849", + "totalAmount": "24800.00", + "paidAmount": "24800.00", + "balanceAmount": "0.00", + "roomCost": "4280.00", + "ticketCost": "3680.00", + "staffCost": "14260.00", + "subsidyCost": "720.00", + "mealCost": "860.00", + "vehicleCost": "5200.00", + "otherExpenseCost": "1260.00", + "insurancePremium": "180.00", + "refundTotal": "2260.00", + "totalActualCost": "23120.00", + "driverTransferAmount": "22940.00", + "profitAmount": "1680.00", + "profitRate": 0.0677, + "settledBy": "30001", + "settledByName": "李定制师", + "settledAt": "2026-08-12T10:30:25", + "remark": "核单完成 出行 7 天 0 投诉", + "finalSnapshotId": "9600000000002", + "finalSnapshotVersionNo": 1, + "finalSnapshotStatus": "FINALIZED", + "finalizedAt": "2026-08-12T10:30:25", + "finalizedByName": "财务终审员", + "advanceSummary": { + "approvedAmount": "2000.00", + "records": [] + } + }, + "success": true +} +``` + +### 8.2 边界情况 + +**边界 1:finalize 无任何软预警**——warnings 为空数组,不是 null: + +```json +{ + "code": 200, + "data": { + "summaryId": "9600000000001", + "warnings": [] + }, + "success": true +} +``` + +**边界 2:summary 查未核单订单**——返回空壳对象(HTTP 200,不报错),仅 orderId / advanceSummary 有值,其余全 null,settled=false: + + GET /v3/admin/order/2087088947225038850/settlement/summary + +```json +{ + "code": 200, + "message": "成功", + "data": { + "settled": false, + "id": null, + "orderId": "2087088947225038850", + "totalAmount": null, + "paidAmount": null, + "balanceAmount": null, + "roomCost": null, + "ticketCost": null, + "staffCost": null, + "subsidyCost": null, + "mealCost": null, + "vehicleCost": null, + "otherExpenseCost": null, + "insurancePremium": null, + "refundTotal": null, + "totalActualCost": null, + "driverTransferAmount": null, + "profitAmount": null, + "profitRate": null, + "settledBy": null, + "settledByName": null, + "settledAt": null, + "remark": null, + "finalSnapshotId": null, + "finalSnapshotVersionNo": null, + "finalSnapshotStatus": null, + "finalizedAt": null, + "finalizedByName": null, + "advanceSummary": { + "approvedAmount": "0", + "records": [] + } + }, + "success": true +} +``` + +**边界 3:同一天同分类多条缺凭证**——旧结构下两条文案完全相同无法区分;新结构每条带独立 settlementId,可逐条定位/跳转: + +```json +"warnings": [ + { "settlementId": "9600000000003", "category": "HOTEL", "dayNumber": 2, "message": "住宿 D2 现付缺凭证" }, + { "settlementId": "9600000000005", "category": "HOTEL", "dayNumber": 2, "message": "住宿 D2 现付缺凭证" } +] +``` + +### 8.3 业务失败 + +**finalize 辅助人员结算未完成**: + + POST /v3/admin/order/2087088947225038849/settlement/finalize + +```json +{ + "code": 584085, + "message": "存在辅助人员结算未完成,请全部结算后提交", + "success": false +} +``` + +**finalize 订单不存在**: + + POST /v3/admin/order/999999999/settlement/finalize + +```json +{ + "code": 581007, + "message": "订单不存在", + "success": false +} +``` + +**finalize 房务角色访问被拦**(House 角色 JWT 调用): + +```json +{ + "code": 403, + "message": "无权限访问", + "success": false +} +``` + +## 9. 业务边界 + +**适用**: + +- finalize:订单核单明细已逐步保存完成、核对无误后点「完成核单」提交 +- summary:核单页 / 财务报表页随时读取汇总;未核单订单也可调(返回空壳),前端据此展示「尚未核单」占位 + +**不适用**: + +- finalize:明细未保存完整 / 辅助人员结算未完成 / 对账不一致的订单(被 584xxx 门禁拦截,见 §7) +- summary:想看明细行级数据时不要用它,走各分类明细接口;本接口只有汇总快照 + +**特殊边界**: + +- warnings 是**软预警**,不阻塞 finalize 提交;有预警也照常返回 200 完成核单 +- 预警仅覆盖 CASH_PAID(现付)且 voucher_urls 为空的住宿 / 门票明细行;签单、公司直付等其他支付方式不产生预警 +- summary 空壳的 advanceSummary 恒为默认对象(approvedAmount="0"、records=[]),**不是 null**,不能拿它当判空依据 + +## 10. 修改前后对比 + +### 10.1 字段级对比 + +| 接口 | 字段 | 修改前 | 修改后 | +|------|------|--------|--------| +| finalize | warnings | `List`,元素如 `"住宿 D2 现付缺凭证"` | `List`,元素为对象 `{ settlementId, category, dayNumber, message }` | +| summary | settled | 无此字段 | 新增 Boolean:已核单 = true,未核单空壳 = false | + +### 10.2 行为级对比 + +| 场景 | 修改前 | 修改后 | +|------|--------|--------| +| 同日多条同类缺凭证 | 多条相同文案,无法区分哪一行 | 每条带独立 settlementId,可定位/跳转具体明细行 | +| 前端渲染预警 | 直接渲染字符串 | 必须读 `warnings[].message`;可用 `settlementId` + `category` 做行锚定 | +| 前端判「是否已核单」 | 判 `data.id == null`(不可靠,advanceSummary 恒非 null 易误导) | 判 `data.settled === false` | +| 调旧结构(如把 warnings[i] 当字符串拼接) | 正常显示文案 | **显示 [object Object] 或报错**(字段结构已变) | + +## 11. 影响评估 / 回滚 + +### 11.1 影响评估 + +- **破坏兼容性**:⚠️ 部分破坏。finalize 的 warnings 结构变化是破坏性的:原按字符串数组渲染 warnings 的前端代码会显示异常。summary 仅新增字段,向后兼容。 +- **前端必须同步上线**:是(针对 finalize warnings 渲染处)。前端需要: + 1. warnings 渲染从「直接渲染字符串」改为读 `item.message` + 2. (可选)用 `item.settlementId` + `item.category` 实现点击预警跳转/高亮对应明细行 + 3. 判「是否已核单」改读 `settled` 字段,删除 `id == null` 判空逻辑 +- **后端兼容**:无 DDL、无数据迁移;未消费 warnings 的前端功能不受影响。 + +### 11.2 回滚方案 + +- 后端回滚 = revert PR #5881 的 merge commit(125bb9c96e),重启 hl-order-service-v3,warnings 恢复字符串数组、settled 字段消失。 +- 前端回滚 = 切回旧版前端包。注意新旧前后端要配套:新前端 + 旧后端时 warnings[].message 取不到值、settled 恒 undefined。 +- 零 DDL,回滚无残留风险。 + +## 12. 注意事项 + +1. **warnings 元素里的 settlementId 序列化为 JSON 字符串**(Long 防 JS 精度丢失),前端按 string 处理,不要 Number() 转换。 +2. warnings 无预警时返回空数组 [],不是 null,渲染前正常遍历即可。 +3. category 当前只会出现 HOTEL / TICKET;SettlementCategory 枚举还有其他值(MEAL / VEHICLE / GUIDE 等),前端如做映射表建议按全量枚举写,缺值兜底显示原 code。 +4. summary 判空**只认 settled**:`settled === false` 即「尚未核单」,此时除 orderId / advanceSummary 外全部字段为 null,展示必须兜底,不要直接渲染金额。 +5. advanceSummary 恒为对象(未核单也是 `{ approvedAmount: "0", records: [] }`),不要拿它判「是否已核单」。 +6. finalize 是写操作且有前置门禁(对账一致、人员结算完成等),失败按错误码 message 提示,**不要静默重试**;重复提交会被快照类错误码拦截。 +7. finalize / summary 两个接口的金额类字段(totalAmount / paidAmount / 各 cost / profitAmount 等)均序列化为字符串,前端展示直接用,计算需自行转数值。 + +## 13. 关联 / 联系人 + +- Issue:https://git.1814.love:8443/wx/HL/issues/5876 +- PR:https://git.1814.love:8443/wx/HL/pulls/5881 +- Commit:https://git.1814.love:8443/wx/HL/commit/125bb9c96e +- 服务:hl-order-service-v3(端口 8086) +- 后端负责人:yst(腰苏图)