From 601e2724574c7564b168376d99bb50aa452ce8ee Mon Sep 17 00:00:00 2001 From: yaosutu <770858045@qq.com> Date: Wed, 29 Jul 2026 14:27:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=A0=B8=E5=8D=95=E6=8A=A5?= =?UTF-8?q?=E8=A1=A8=E5=AE=9E=E6=97=B6=E6=9F=A5=E8=AF=A2=E4=B8=8E=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E5=9B=BA=E5=8C=96=E6=8E=A5=E5=8F=A3=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=EF=BC=88#5342=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...消中间确认并由finalize固化-修改接口-管理后台.md | 894 ++++++++++++++++++ 1 file changed, 894 insertions(+) create mode 100644 changelogs-v2/2026-07/29_5342_核单报表取消中间确认并由finalize固化-修改接口-管理后台.md diff --git a/changelogs-v2/2026-07/29_5342_核单报表取消中间确认并由finalize固化-修改接口-管理后台.md b/changelogs-v2/2026-07/29_5342_核单报表取消中间确认并由finalize固化-修改接口-管理后台.md new file mode 100644 index 0000000..b308dfc --- /dev/null +++ b/changelogs-v2/2026-07/29_5342_核单报表取消中间确认并由finalize固化-修改接口-管理后台.md @@ -0,0 +1,894 @@ +--- +schema: "hl-changelog/v2" +ticket: "5342" +title: "核单报表取消中间确认并由 finalize 固化" +consumer: "admin" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "后端 PR #5345 已合并并部署;网关已验证旧确认路由 404、finalize 新校验与实时查询链路" +updated_at: "2026-07-29" +base: "dev-v3" +--- + +# 【修改接口·管理后台】核单报表取消中间确认并由 finalize 固化 (#5342) + +> **PR**: #5345 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-29 13:28 + +## 1. 接口背景 + +核单流程不再要求用户分别确认“主报账人报账表”和“单团核算表”。核单未完成时,两张报表查询接口按当前业务数据实时返回;点击完成核单时,前端一次性提交转账、垫资结清和签字凭证信息,服务端按提交时的当前数据重新计算并固化终态。核单完成后,两张报表查询接口只返回该次完成核单时固化的内容,后续来源数据变化不会改写该终态结果。 + +## 变更接口(2. 变更清单) + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 查询主报账人报账表 | GET | `/v3/admin/order/:orderId/settlement/reports/reimbursement` | 行为修改 | 未完成核单时实时计算;完成核单后读取终态结果 | +| 2 | 查询单团核算表 | GET | `/v3/admin/order/:orderId/settlement/reports/group` | 行为修改 | 未完成核单时实时计算;完成核单后读取终态结果 | +| 3 | 确认主报账人报账表 | POST | `/v3/admin/order/:orderId/settlement/reports/reimbursement/confirm` | 删除 | 接口下线,调用返回业务码 `404` | +| 4 | 确认单团核算表 | POST | `/v3/admin/order/:orderId/settlement/reports/group/confirm` | 删除 | 接口下线,调用返回业务码 `404` | +| 5 | 完成核单 | POST | `/v3/admin/order/:orderId/settlement/finalize` | 请求与行为修改 | 请求体改为必填;删除两个客户端指纹;新增转账、垫资和签字凭证字段;提交时实时重算并固化终态 | + +## 3. 接口详情 + +### 3.1 查询主报账人报账表 + +- **方法 + 路径**: `GET /v3/admin/order/:orderId/settlement/reports/reimbursement` +- **使用场景**: 打开核单报账表或刷新核单数据 +- **认证**: 管理后台 JWT;房务角色不可访问 +- **幂等性**: 幂等,只读 +- **请求体**: 无 + +**路径参数** + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `orderId` | String/Long | 是 | 订单 ID,必须大于 `0` | + +**响应字段** + +| 字段 | 类型 | 可空 | 说明 | +|------|------|------|------| +| `id` | String | 是 | 报账表记录 ID;实时报表和终态快照中可为 `null` | +| `orderId` | String | 否 | 订单 ID | +| `reportStatus` | String | 否 | 报表状态,见 §6.1 | +| `sourceFingerprint` | String | 否 | 当前报表来源指纹,仅用于识别数据版本;前端不再回传 | +| `primaryReporterId` | String | 是 | 主报账人 ID | +| `primaryReporterName` | String | 是 | 主报账人姓名 | +| `primaryReporterRole` | String | 是 | 主报账人角色 | +| `reportVersion` | Integer | 否 | 报账表结构版本 | +| `driverCollectedTailAmount` | Decimal | 否 | 主报账人代收尾款 | +| `approvedAdvanceAmount` | Decimal | 否 | 已审批垫资金额 | +| `reportablePaidCostAmount` | Decimal | 否 | 可报账的已付成本 | +| `reporterNetAmount` | Decimal | 否 | 报账人净额;大于 0 表示报账人应转给公司,小于 0 表示公司应转给报账人 | +| `primaryReporterCollectedAmount` | Decimal | 否 | 主报账人代收金额 | +| `publicPrepaidAmount` | Decimal | 否 | 公共预支金额 | +| `primaryReporterDueAmount` | Decimal | 否 | 主报账人应报账金额 | +| `advanceOutstandingAmount` | Decimal | 否 | 未结清垫资金额 | +| `reconNetAmount` | Decimal | 否 | 报账净额 | +| `transferDirection` | String | 否 | 转账方向,见 §6.2 | +| `transferAmount` | Decimal | 否 | 应转账金额的绝对值 | +| `incomeLines` | Array\ | 否 | 主报账人代收明细,字段见下表 | +| `expenseLines` | Array\ | 否 | 现金已付成本明细,字段随费用分类变化,字段见下表 | +| `advanceLines` | Array\ | 否 | 已审批垫资明细,字段见下表 | +| `vehicleLines` | Array\ | 否 | 车辆独立明细;当前返回空数组,车辆金额已进入费用分类和汇总金额 | +| `transferStatus` | String | 是 | 未完成核单时为 `null`;终态为 `COMPLETED` | +| `transferDate` | String/date | 是 | 转账日期,格式 `YYYY-MM-DD` | +| `transferRef` | String | 是 | 转账流水号 | +| `advanceSettledFlag` | Boolean | 是 | 垫资是否结清 | +| `signedVoucher` | Object | 是 | 签字凭证;结构同 finalize 请求的 `signedVoucher` | +| `generatedBy` | String | 是 | 历史生成操作人 ID;实时/终态模式下可为 `null` | +| `generatedByName` | String | 是 | 历史生成操作人姓名;实时/终态模式下可为 `null` | +| `generatedAt` | String/date-time | 是 | 历史生成时间;实时/终态模式下可为 `null` | +| `confirmedBy` | String | 是 | 完成核单操作人 ID;未完成核单时为 `null` | +| `confirmedByName` | String | 是 | 完成核单操作人姓名;未完成核单时为 `null` | +| `confirmedAt` | String/date-time | 是 | 完成核单时间;未完成核单时为 `null` | + +**`incomeLines[]` 字段** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `type` | String | 固定为 `DRIVER_CASH_RECEIPT` | +| `receiptId` | String | 收款记录 ID | +| `amount` | Decimal | 收款金额 | +| `channel` | String | 收款渠道;当前参与报账的值为 `DRIVER_CASH` | +| `payType` | String/null | 支付类型 | +| `collectorStaffId` | String/null | 收款人员 ID | +| `collectorName` | String/null | 收款人员姓名 | +| `collectorRole` | String/null | 收款人员角色 | +| `receivedAt` | String/date-time/null | 收款时间 | +| `remark` | String/null | 备注 | + +**`advanceLines[]` 字段** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `type` | String | 固定为 `APPROVED_ADVANCE` | +| `advanceId` | String | 垫资记录 ID | +| `payeeStaffId` | String/null | 收款人员 ID | +| `payeeName` | String/null | 收款人员姓名 | +| `payeeRole` | String/null | 收款人员角色 | +| `advanceType` | String/null | 垫资类型 | +| `amount` | Decimal | 已审批金额 | +| `purpose` | String/null | 用途 | +| `voucherUrl` | String/null | 垫资凭证地址 | +| `status` | String | 垫资状态 | +| `submittedAt` | String/date-time/null | 提交时间 | +| `approvedAt` | String/date-time/null | 审批时间 | +| `approvedBy` | String/null | 审批人 ID | + +**`expenseLines[]` 公共字段** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `category` | String | 费用分类,见 §6.3 | +| `kind` | String | 明细类型,例如 `HOTEL`、`TICKET`、`MEAL`、`VEHICLE_FEE`、`STAFF:DRIVER` | +| `amount` | Decimal | 当前行实际成本 | +| `paymentMethod` | String | 当前仅包含 `CASH_PAID` 行 | + +`expenseLines[]` 会按 `kind` 追加以下分类字段: + +- `HOTEL`: `hotelAssignmentId`、`hotelId`、`roomTypeId`、`dayNumber`、`stayDate`、`hotelName`、`roomType`、`roomTypeName`、`roomCount`、`unitPrice`、`plannedCost`、`sourceType`、`sourceId`、`voucherUrls`、`remark`。 +- `TICKET`: `sourceType`、`scenicAssignmentId`、`dayNumber`、`dayDate`、`scenicName`、`specName`、`ticketCount`、`ticketUnitPrice`、`sellPrice`、`totalAmount`、`plannedCost`、`voucherUrls`、`remark`。 +- `MEAL`: `mealType`、`mealDate`、`mealName`、`quantity`、`unitPrice`、`voucherUrls`、`remark`。 +- `VEHICLE_FEE`: `sourceRecordType`、`sourceDetailId`、`serviceDate`、`vehicleId`、`vehiclePlate`、`vehicleModelId`、`vehicleModelName`、`driverId`、`driverName`、`startDate`、`endDate`、`dailyPrice`、`paymentTypeCode`、`paymentTypeName`。 +- `STAFF:*`: `staffRole`、`staffId`、`staffName`、`totalPlannedCost`、`voucherUrls`、`reimburse`、`settleStatus`、`settledDate`、`transferRef`、`detail`、`remark`。 +- `EXPENSE:*`: `expenseType`、`projectName`、`expenseDate`、`voucherUrls`、`remark`。 +- `SUBSIDY:*`: `subsidyType`、`projectName`、`expenseDate`、`voucherUrls`、`remark`。 + +**行为** + +- 订单不存在当前终态快照时,每次请求均按当前核单数据计算,`reportStatus=GENERATED`。 +- 订单存在当前终态快照时,返回完成核单时固化的报账表,`reportStatus=CONFIRMED`。 +- `sourceFingerprint` 继续返回,但不再作为任何前端确认或 finalize 入参。 + +### 3.2 查询单团核算表 + +- **方法 + 路径**: `GET /v3/admin/order/:orderId/settlement/reports/group` +- **使用场景**: 打开单团核算表或刷新核算结果 +- **认证**: 管理后台 JWT;房务角色不可访问 +- **幂等性**: 幂等,只读 +- **请求体**: 无 + +**路径参数** + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `orderId` | String/Long | 是 | 订单 ID,必须大于 `0` | + +**响应字段** + +| 字段 | 类型 | 可空 | 说明 | +|------|------|------|------| +| `id` | String | 是 | 单团核算表记录 ID;实时报表和终态快照中可为 `null` | +| `orderId` | String | 否 | 订单 ID | +| `reportStatus` | String | 否 | 报表状态,见 §6.1 | +| `sourceFingerprint` | String | 否 | 当前报表来源指纹,仅用于识别数据版本;前端不再回传 | +| `baseOrderAmount` | Decimal | 否 | 订单基础金额 | +| `otherIncomeAmount` | Decimal | 否 | 其他收入金额 | +| `discountAmount` | Decimal | 否 | 优惠金额 | +| `adjustedReceivableAmount` | Decimal | 否 | 调整后应收金额 | +| `paidAmount` | Decimal | 否 | 已收金额 | +| `actualRefundedAmount` | Decimal | 否 | 实际退款金额 | +| `netRevenueAmount` | Decimal | 否 | 净收入 | +| `netReceivedAmount` | Decimal | 否 | 净已收 | +| `outstandingAmount` | Decimal | 否 | 待收金额 | +| `hotelCost` | Decimal | 否 | 住宿成本 | +| `ticketCost` | Decimal | 否 | 门票/游玩项目成本 | +| `mealCost` | Decimal | 否 | 餐食成本 | +| `vehicleCost` | Decimal | 否 | 车辆成本 | +| `guideCost` | Decimal | 否 | 导游/领队成本 | +| `photographerCost` | Decimal | 否 | 摄影成本 | +| `otherExpenseCost` | Decimal | 否 | 其他支出成本 | +| `insurancePremium` | Decimal | 否 | 保险保费 | +| `totalCost` | Decimal | 否 | 总成本 | +| `paidCost` | Decimal | 否 | 已付成本 | +| `unpaidCost` | Decimal | 否 | 未付成本 | +| `grossProfit` | Decimal | 否 | 毛利 | +| `grossProfitRate` | Decimal | 否 | 毛利率,小数形式 | +| `travelerCount` | Integer | 否 | 出行人数 | +| `perCapitaRevenue` | Decimal | 否 | 人均收入 | +| `perCapitaCost` | Decimal | 否 | 人均成本 | +| `perCapitaProfit` | Decimal | 否 | 人均利润 | +| `incomeLines` | Array\ | 否 | 收入汇总行,固定结构见下表 | +| `costCategories` | Array\ | 否 | 成本分类汇总,固定结构见下表 | +| `generatedBy` | String | 是 | 历史生成操作人 ID;实时/终态模式下可为 `null` | +| `generatedByName` | String | 是 | 历史生成操作人姓名;实时/终态模式下可为 `null` | +| `generatedAt` | String/date-time | 是 | 历史生成时间;实时/终态模式下可为 `null` | +| `confirmedBy` | String | 是 | 完成核单操作人 ID;未完成核单时为 `null` | +| `confirmedByName` | String | 是 | 完成核单操作人姓名;未完成核单时为 `null` | +| `confirmedAt` | String/date-time | 是 | 完成核单时间;未完成核单时为 `null` | + +**`incomeLines[]` 字段** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `type` | String | `BASE_ORDER`、`OTHER_INCOME`、`DISCOUNT` 或 `ACTUAL_REFUND` | +| `amount` | Decimal | 金额;优惠和实际退款以负数返回 | + +**`costCategories[]` 字段** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `category` | String | `HOTEL`、`TICKET`、`MEAL`、`VEHICLE`、`GUIDE`、`PHOTOGRAPHER`、`OTHER_EXPENSE` 或 `INSURANCE` | +| `amount` | Decimal | 分类成本 | + +**行为** + +- 订单不存在当前终态快照时,每次请求均按当前核单数据计算,`reportStatus=GENERATED`。 +- 订单存在当前终态快照时,返回完成核单时固化的单团核算表,`reportStatus=CONFIRMED`。 +- `sourceFingerprint` 继续返回,但不再作为任何前端确认或 finalize 入参。 + +### 3.3 完成核单 + +- **方法 + 路径**: `POST /v3/admin/order/:orderId/settlement/finalize` +- **使用场景**: 用户检查实时主报账表和单团核算表后,点击完成核单 +- **认证**: 管理后台 JWT;需要核单资金写权限;房务角色不可访问 +- **幂等性**: 已存在当前终态快照时,重复请求返回已有终态结果,不重新生成新终态 +- **限流**: 无接口级特殊限流 + +**路径参数** + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `orderId` | String/Long | 是 | 订单 ID,必须大于 `0` | + +**请求体字段** + +| 字段 | 类型 | 必填 | 说明 | 校验规则 | +|------|------|------|------|----------| +| `remark` | String | 否 | 核单整体备注 | 最多 `500` 字符 | +| `transferDate` | String/date | 条件必填 | 转账日期 | `reporterNetAmount != 0` 时必填;格式 `YYYY-MM-DD` | +| `transferRef` | String | 条件必填 | 转账流水号 | `reporterNetAmount != 0` 时必须为非空字符串;最多 `128` 字符 | +| `advanceSettledFlag` | Boolean | 是 | 垫资是否结清;必须明确传 `true` 或 `false` | 不可为 `null` | +| `signedVoucher` | Object | 是 | 签字凭证 | 不可为 `null` | +| `signedVoucher.files` | Array\ | 是 | 签字凭证文件 | `1`~`9` 项;重复 URL 按规范化后的 URL 去重并保留首项 | +| `signedVoucher.files[].name` | String | 否 | 文件名 | 最多 `255` 字符 | +| `signedVoucher.files[].url` | String | 是 | 文件地址 | 非空;最多 `1024` 字符;必须是带有效主机名的绝对 `http/https` URL | +| `signedVoucher.note` | String | 否 | 签字凭证备注 | 最多 `500` 字符 | + +以下字段已经删除,前端不得继续发送: + +| 删除字段 | 原类型 | 迁移方式 | +|----------|--------|----------| +| `reimbursementExpectedSourceFingerprint` | String | 删除本地缓存和提交逻辑;完成核单时不再回传报账表指纹 | +| `groupExpectedSourceFingerprint` | String | 删除本地缓存和提交逻辑;完成核单时不再回传单团核算表指纹 | + +**响应字段** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `summaryId` | String | 核单汇总 ID | +| `finalSnapshotId` | String | 核单终态快照 ID | +| `finalSnapshotVersionNo` | Integer | 终态快照版本号,从 `1` 开始 | +| `finalSnapshotStatus` | String | 当前终态固定为 `FINALIZED` | +| `orderId` | String | 订单 ID | +| `settledAt` | String/date-time | 核单完成时间 | +| `totalAmount` | String/Decimal | 订单总金额快照 | +| `paidAmount` | String/Decimal | 已付金额快照 | +| `balanceAmount` | String/Decimal | 尾款金额快照 | +| `roomCost` | String/Decimal | 住宿实际成本 | +| `ticketCost` | String/Decimal | 门票实际成本 | +| `staffCost` | String/Decimal | 人员费用实际成本 | +| `subsidyCost` | String/Decimal | 补助实际成本 | +| `mealCost` | String/Decimal | 餐食实际成本 | +| `vehicleCost` | String/Decimal | 车辆基础服务总车费 | +| `otherExpenseCost` | String/Decimal | 其他支出实际成本 | +| `insurancePremium` | String/Decimal | 保险实际保费 | +| `totalActualCost` | String/Decimal | 总实际成本 | +| `driverTransferAmount` | String/Decimal | 给司机/主报账人转回金额 | +| `profitAmount` | String/Decimal | 公司毛利 | +| `profitRate` | Decimal | 毛利率;订单总金额为 `0` 时返回 `0` | +| `orderStatusAfter` | String | 当前返回 `待财务复核` | +| `mqTriggered` | Boolean | 当前固定返回 `false`;完成核单不发布结算 MQ | +| `warnings` | Array\ | 软预警列表;不阻塞完成核单 | + +**提交行为** + +1. 前端不再先调用任何报表“确认”接口。 +2. 服务端按提交时的当前核单数据重新计算两张报表和所有汇总金额,不采信前端缓存的金额或指纹。 +3. `transferDate`、`transferRef`、`advanceSettledFlag` 和 `signedVoucher` 与本次完成核单结果一并固化。 +4. 成功后,两张 GET 报表接口返回本次固化结果。 + +### 3.4 已删除的报表确认接口 + +以下接口不再有可用请求契约: + +| 原接口 | 原请求字段 | 当前结果 | +|--------|------------|----------| +| `POST /v3/admin/order/:orderId/settlement/reports/reimbursement/confirm` | `expectedSourceFingerprint`、`transferStatus`、`transferDate`、`transferRef`、`advanceSettledFlag`、`signedVoucher` | 业务码 `404` | +| `POST /v3/admin/order/:orderId/settlement/reports/group/confirm` | `expectedSourceFingerprint` | 业务码 `404` | + +前端必须删除这两个请求,不要用忽略 `404`、重试或降级继续调用的方式兼容。 + +## 4. 接口入参 + +### 4.1 通用路径参数 + +| 接口 | 字段 | 类型 | 必填 | 规则 | +|------|------|------|------|------| +| 两张 GET 报表、finalize | `orderId` | String/Long | 是 | 必须大于 `0` | + +### 4.2 请求体变化总览 + +| 接口 | 修改前 | 修改后 | +|------|--------|--------| +| 主报账表确认 | 独立 POST 提交报账表指纹及凭证 | 接口删除 | +| 单团核算表确认 | 独立 POST 提交单团核算表指纹 | 接口删除 | +| finalize | 请求体可缺省;主要提交两个报告指纹和可选 `remark` | 请求体必填;提交 `remark`、`transferDate`、`transferRef`、`advanceSettledFlag`、`signedVoucher`;不再提交任何指纹 | + +## 5. 出参 + +### 5.1 报表查询 + +- 两张 GET 接口的字段结构保持不变。 +- 未完成核单时返回最新实时计算结果。 +- 完成核单后返回完成核单时固化的结果。 +- 报表 `sourceFingerprint` 仍存在于出参,但只表示数据版本,前端不得再将其用于确认或 finalize。 + +### 5.2 完成核单 + +- finalize 响应字段结构保持 `SettlementSubmitRespVO`。 +- `finalSnapshotId`、`finalSnapshotVersionNo`、`finalSnapshotStatus` 标识本次固化结果。 +- `mqTriggered` 的当前契约为固定 `false`,前端不得用该字段判断是否需要等待 MQ。 + +## 6. 枚举 / 数据字典 + +### 6.1 `reportStatus` + +**所属字段**: 两张报表响应 `reportStatus` | **类型**: String + +| 值 | 中文 | 当前语义 | +|----|------|----------| +| `GENERATED` | 实时结果 | 订单未完成核单,响应按当前数据实时计算 | +| `CONFIRMED` | 已固化 | 订单已完成核单,响应来自终态结果 | +| `STALE` | 历史过期状态 | 仅兼容历史报表数据;新实时查询流程不要求前端据此重新生成或确认 | + +### 6.2 `transferDirection` + +**所属字段**: 主报账人报账表 `transferDirection` | **类型**: String + +| 值 | 中文 | 说明 | +|----|------|------| +| `REPORTER_TO_COMPANY` | 报账人转给公司 | `reporterNetAmount > 0` | +| `COMPANY_TO_REPORTER` | 公司转给报账人 | `reporterNetAmount < 0` | +| `BALANCED` | 已平衡 | `reporterNetAmount = 0` | + +### 6.3 报账费用分类 + +**所属字段**: `expenseLines[].category`、`costCategories[].category` | **类型**: String + +| 值 | 中文 | 说明 | +|----|------|------| +| `HOTEL` | 住宿 | 住宿成本 | +| `TICKET` | 门票/游玩项目 | 门票及游玩项目成本 | +| `MEAL` | 餐食 | 餐食成本 | +| `VEHICLE` | 车辆 | 车辆成本 | +| `GUIDE` | 导游/领队 | 导游及领队成本 | +| `PHOTOGRAPHER` | 摄影 | 摄影成本 | +| `OTHER_EXPENSE` | 其他支出 | 其他支出成本 | +| `INSURANCE` | 保险 | 保险保费;仅用于单团核算表成本分类 | + +### 6.4 `finalSnapshotStatus` + +**所属字段**: finalize 响应 `finalSnapshotStatus` | **类型**: String + +| 值 | 中文 | 说明 | +|----|------|------| +| `FINALIZED` | 已固化 | 当前核单终态有效 | + +### 6.5 `transferStatus` + +**所属字段**: 主报账人报账表 `transferStatus` | **类型**: String/null + +| 值 | 中文 | 说明 | +|----|------|------| +| `COMPLETED` | 已完成 | finalize 成功后固化到终态报账表 | +| `null` | 未固化 | 尚未完成核单的实时报表 | + +## 7. 错误码 + +| code | 含义 | 触发场景 | +|------|------|----------| +| `400` | 请求参数校验失败 | `orderId <= 0`、finalize 缺请求体/必填字段、字段超长、凭证文件数量不在 1~9 | +| `403` | 无访问权限 | 房务角色或无权访问当前订单 | +| `404` | 接口不存在 | 继续调用两个已删除的 `/confirm` 接口 | +| `584051` | 当前核单状态不允许提交结算 | review 状态不是待核单或核单中 | +| `584056` | 订单已结算,不能重复提交 | 已存在结算汇总但缺少可返回的当前终态 | +| `584071` | 无权访问该订单(公司隔离) | 当前管理员不能查看该订单 | +| `584077` | 存在未确认的其他收入 | finalize 前其他收入未确认 | +| `584078` | 其他收入关联附加费已失效或金额不一致 | finalize 前其他收入与当前附加费不一致 | +| `584079` | 存在未纳入核单分类的有效附加费 | finalize 前还有有效附加费未进入核单 | +| `584081` | 对账数据不一致 | 已付金额与有效支付、线下收款合计不一致 | +| `584082` | 存在待收尾款 | 单团核算 `outstandingAmount != 0` | +| `584085` | 存在辅助人员结算未完成 | 辅助人员未全部完成结算或缺转账流水 | +| `584092` | 存在未确认的人员费用 | 人员费用确认状态未全部完成 | +| `584097` | 凭证 URL 格式或数量不合法 | URL 不是有效绝对 `http/https` 地址、为空、过长或数量超限 | +| `584100` | 车辆总车费暂时不可用 | 报表查询或 finalize 暂时无法取得车辆费用 | +| `584101` | 存在未完结派车或未确认车辆总车费 | 当前车辆数据尚不能用于核单 | +| `584102` | 当前用车需求没有可核单的车辆总车费 | 有用车需求但没有可用车辆费用 | +| `584315` | 核单来源数据已变化,请刷新后重新确认 | finalize 提交期间当前用车需求发生变化 | +| `584316` | 核单报告发生并发变化,请刷新后重试 | 同一订单并发完成核单发生冲突 | +| `584317` | 当前报告状态不允许执行该操作 | 报账人净额非 0 但缺转账日期/流水,或签字凭证不可用 | +| `584320` | 核单分类明细尚未保存完整 | 当前分类数据不能用于报账和 finalize | + +## 验证证据(8. 示例) + +### 8.1 典型成功:查询实时主报账人报账表 + +**请求** + +```http +GET /v3/admin/order/1914050000000001/settlement/reports/reimbursement +Authorization: Bearer +``` + +无请求体。 + +**响应** + +```json +{ + "code": 200, + "msg": "success", + "data": { + "id": null, + "orderId": "1914050000000001", + "reportStatus": "GENERATED", + "sourceFingerprint": "7a0e84a9f9d0cb411f9cff8d6a0d1c047726bb05b68c8e23af5629afdbdd67c1", + "primaryReporterId": "3001", + "primaryReporterName": "示例报账人", + "primaryReporterRole": "DRIVER", + "reportVersion": 1, + "driverCollectedTailAmount": 2000.00, + "approvedAdvanceAmount": 500.00, + "reportablePaidCostAmount": 1200.00, + "reporterNetAmount": 1300.00, + "primaryReporterCollectedAmount": 2000.00, + "publicPrepaidAmount": 1200.00, + "primaryReporterDueAmount": 800.00, + "advanceOutstandingAmount": 500.00, + "reconNetAmount": 1300.00, + "transferDirection": "REPORTER_TO_COMPANY", + "transferAmount": 1300.00, + "incomeLines": [ + { + "type": "DRIVER_CASH_RECEIPT", + "receiptId": "9100000000001", + "amount": 2000.00, + "channel": "DRIVER_CASH", + "payType": "CASH", + "collectorStaffId": "3001", + "collectorName": "示例报账人", + "collectorRole": "DRIVER", + "receivedAt": "2026-07-28T15:30:00", + "remark": "示例代收尾款" + } + ], + "expenseLines": [ + { + "category": "HOTEL", + "kind": "HOTEL", + "hotelAssignmentId": "9200000000001", + "hotelId": "1001", + "roomTypeId": "2001", + "dayNumber": 1, + "stayDate": "2026-07-20", + "hotelName": "示例酒店", + "roomType": "STANDARD", + "roomTypeName": "标准间", + "roomCount": 2, + "unitPrice": 300.00, + "plannedCost": 600.00, + "amount": 600.00, + "paymentMethod": "CASH_PAID", + "sourceType": "HOUSE_ASSIGNMENT", + "sourceId": "9200000000001", + "voucherUrls": ["https://oss.example.com/vouchers/hotel-1.pdf"], + "remark": null + } + ], + "advanceLines": [ + { + "type": "APPROVED_ADVANCE", + "advanceId": "9300000000001", + "payeeStaffId": "3001", + "payeeName": "示例报账人", + "payeeRole": "DRIVER", + "advanceType": "PUBLIC", + "amount": 500.00, + "purpose": "行程公共支出", + "voucherUrl": "https://oss.example.com/vouchers/advance-1.pdf", + "status": "APPROVED", + "submittedAt": "2026-07-19T10:00:00", + "approvedAt": "2026-07-19T11:00:00", + "approvedBy": "10001" + } + ], + "vehicleLines": [], + "transferStatus": null, + "transferDate": null, + "transferRef": null, + "advanceSettledFlag": null, + "signedVoucher": null, + "generatedBy": null, + "generatedByName": null, + "generatedAt": null, + "confirmedBy": null, + "confirmedByName": null, + "confirmedAt": null + } +} +``` + +### 8.2 典型成功:查询实时单团核算表 + +**请求** + +```http +GET /v3/admin/order/1914050000000001/settlement/reports/group +Authorization: Bearer +``` + +无请求体。 + +**响应** + +```json +{ + "code": 200, + "msg": "success", + "data": { + "id": null, + "orderId": "1914050000000001", + "reportStatus": "GENERATED", + "sourceFingerprint": "651cb6708a49f169e2ccb1b455267def9cc1a69d06935f9d9eeb41919897e0fb", + "baseOrderAmount": 24800.00, + "otherIncomeAmount": 500.00, + "discountAmount": 300.00, + "adjustedReceivableAmount": 25000.00, + "paidAmount": 25000.00, + "actualRefundedAmount": 0.00, + "netRevenueAmount": 25000.00, + "netReceivedAmount": 25000.00, + "outstandingAmount": 0.00, + "hotelCost": 4280.00, + "ticketCost": 3680.00, + "mealCost": 860.00, + "vehicleCost": 1260.00, + "guideCost": 800.00, + "photographerCost": 600.00, + "otherExpenseCost": 300.00, + "insurancePremium": 180.00, + "totalCost": 11960.00, + "paidCost": 11960.00, + "unpaidCost": 0.00, + "grossProfit": 13040.00, + "grossProfitRate": 0.521600, + "travelerCount": 5, + "perCapitaRevenue": 5000.00, + "perCapitaCost": 2392.00, + "perCapitaProfit": 2608.00, + "incomeLines": [ + { + "type": "BASE_ORDER", + "amount": 24800.00 + }, + { + "type": "OTHER_INCOME", + "amount": 500.00 + }, + { + "type": "DISCOUNT", + "amount": -300.00 + }, + { + "type": "ACTUAL_REFUND", + "amount": 0.00 + } + ], + "costCategories": [ + { + "category": "HOTEL", + "amount": 4280.00 + }, + { + "category": "TICKET", + "amount": 3680.00 + }, + { + "category": "MEAL", + "amount": 860.00 + }, + { + "category": "VEHICLE", + "amount": 1260.00 + }, + { + "category": "GUIDE", + "amount": 800.00 + }, + { + "category": "PHOTOGRAPHER", + "amount": 600.00 + }, + { + "category": "OTHER_EXPENSE", + "amount": 300.00 + }, + { + "category": "INSURANCE", + "amount": 180.00 + } + ], + "generatedBy": null, + "generatedByName": null, + "generatedAt": null, + "confirmedBy": null, + "confirmedByName": null, + "confirmedAt": null + } +} +``` + +### 8.3 典型成功:完成核单并固化 + +**请求** + +```http +POST /v3/admin/order/1914050000000001/settlement/finalize +Authorization: Bearer +Content-Type: application/json + +{ + "remark": "核单完成", + "transferDate": "2026-07-29", + "transferRef": "BANK-20260729-001", + "advanceSettledFlag": true, + "signedVoucher": { + "files": [ + { + "name": "签字单.pdf", + "url": "https://oss.example.com/vouchers/signed-20260729.pdf" + } + ], + "note": "签字凭证已回收" + } +} +``` + +**响应** + +```json +{ + "code": 200, + "msg": "success", + "data": { + "summaryId": "9400000000001", + "finalSnapshotId": "9400000000002", + "finalSnapshotVersionNo": 1, + "finalSnapshotStatus": "FINALIZED", + "orderId": "1914050000000001", + "settledAt": "2026-07-29T13:28:22", + "totalAmount": "25000.00", + "paidAmount": "25000.00", + "balanceAmount": "0.00", + "roomCost": "4280.00", + "ticketCost": "3680.00", + "staffCost": "1400.00", + "subsidyCost": "0.00", + "mealCost": "860.00", + "vehicleCost": "1260.00", + "otherExpenseCost": "300.00", + "insurancePremium": "180.00", + "totalActualCost": "11960.00", + "driverTransferAmount": "11780.00", + "profitAmount": "13040.00", + "profitRate": 0.5216, + "orderStatusAfter": "待财务复核", + "mqTriggered": false, + "warnings": [] + } +} +``` + +### 8.4 边界:报账人净额为 0 + +当最新 `reporterNetAmount=0` 时,`transferDate` 和 `transferRef` 可以省略;`advanceSettledFlag` 和 `signedVoucher` 仍必须提交。 + +**请求** + +```http +POST /v3/admin/order/1914050000000002/settlement/finalize +Authorization: Bearer +Content-Type: application/json + +{ + "remark": "收支已平衡", + "advanceSettledFlag": false, + "signedVoucher": { + "files": [ + { + "name": "签字单.jpg", + "url": "https://oss.example.com/vouchers/signed-balanced.jpg" + } + ] + } +} +``` + +**响应** + +```json +{ + "code": 200, + "msg": "success", + "data": { + "summaryId": "9400000000011", + "finalSnapshotId": "9400000000012", + "finalSnapshotVersionNo": 1, + "finalSnapshotStatus": "FINALIZED", + "orderId": "1914050000000002", + "settledAt": "2026-07-29T13:40:00", + "totalAmount": "0.00", + "paidAmount": "0.00", + "balanceAmount": "0.00", + "roomCost": "0.00", + "ticketCost": "0.00", + "staffCost": "0.00", + "subsidyCost": "0.00", + "mealCost": "0.00", + "vehicleCost": "0.00", + "otherExpenseCost": "0.00", + "insurancePremium": "0.00", + "totalActualCost": "0.00", + "driverTransferAmount": "0.00", + "profitAmount": "0.00", + "profitRate": 0, + "orderStatusAfter": "待财务复核", + "mqTriggered": false, + "warnings": [] + } +} +``` + +### 8.5 异常:仍调用已删除的确认接口 + +**请求** + +```http +POST /v3/admin/order/1914050000000001/settlement/reports/group/confirm +Authorization: Bearer +Content-Type: application/json + +{ + "expectedSourceFingerprint": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" +} +``` + +**响应** + +```json +{ + "code": 404, + "msg": "请求地址不存在", + "data": null +} +``` + +### 8.6 异常:车辆费用尚未可核单 + +**请求** + +```http +POST /v3/admin/order/1914050000000001/settlement/finalize +Authorization: Bearer +Content-Type: application/json + +{ + "transferDate": "2026-07-29", + "transferRef": "BANK-20260729-001", + "advanceSettledFlag": true, + "signedVoucher": { + "files": [ + { + "name": "签字单.pdf", + "url": "https://oss.example.com/vouchers/signed-20260729.pdf" + } + ] + } +} +``` + +**响应** + +```json +{ + "code": 584101, + "msg": "存在未完结派车或未确认车辆总车费,暂不能核单", + "data": null +} +``` + +## 9. 业务边界 + +- 报表 GET 的“实时”以每次请求时可用于核单的当前数据为准,前端不要把上一次响应当作提交凭据。 +- 完成核单前,前端可以重复查询两张报表;无需执行任何“生成”或“确认”步骤。 +- finalize 不接收前端金额。页面展示金额与提交时权威数据发生变化时,以提交时重新计算结果为准。 +- 报账人净额不为 `0` 时,必须同时提交 `transferDate` 和非空 `transferRef`。 +- `signedVoucher.files` 原始数组必须为 `1`~`9` 项;URL 会去除首尾空格、规范化并按 URL 去重。 +- 完成核单成功后,两张报表进入终态读取;只有业务上的核单反确认使当前终态失效后,查询才重新进入实时模式。 +- finalize 成功响应中的 `warnings` 是软预警,不表示提交失败。 +- 已有当前终态时重复调用 finalize 返回已有终态,不会依据本次请求改写已固化的转账或凭证信息。 + +## 10. 修改前后对比 + +### 10.1 字段级对比 + +| 接口/字段 | 修改前 | 修改后 | +|-----------|--------|--------| +| finalize 请求体 | 可缺省 | 必填 | +| `remark` | 可选,最多 500 字符 | 保持不变 | +| `reimbursementExpectedSourceFingerprint` | finalize 必填 | 删除 | +| `groupExpectedSourceFingerprint` | finalize 必填 | 删除 | +| `transferDate` | 在主报账表确认接口提交 | 移至 finalize;报账人净额非 0 时必填 | +| `transferRef` | 在主报账表确认接口提交 | 移至 finalize;报账人净额非 0 时必填,最多 128 字符 | +| `advanceSettledFlag` | 在主报账表确认接口提交 | 移至 finalize,必填 | +| `signedVoucher` | 在主报账表确认接口提交 | 移至 finalize,必填 | +| `signedVoucher.files` | 原确认接口字段 | finalize 中要求 1~9 项 | +| `signedVoucher.files[].name` | 原确认接口未明确长度 | 最多 255 字符 | +| `signedVoucher.files[].url` | 原确认接口未明确长度 | 必填;最多 1024 字符;绝对 http/https URL | +| `signedVoucher.note` | 原确认接口未明确长度 | 最多 500 字符 | +| `mqTriggered` | 示例和历史说明可能按 `true` 理解 | 当前固定 `false` | + +### 10.2 行为级对比 + +| 行为 | 修改前 | 修改后 | +|------|--------|--------| +| 主报账表 | 先读取,再调用独立 confirm | GET 实时读取;不再确认 | +| 单团核算表 | 主报账表确认后再读取并 confirm | GET 实时读取;不再确认 | +| 数据变化处理 | 前端携带两张报表指纹,指纹过期时刷新重试 | 前端不携带指纹;finalize 按提交时数据重算 | +| 转账/垫资/签字信息 | 主报账表 confirm 时提交 | finalize 时一次提交 | +| 完成核单后查询 | 依赖已确认报表记录 | 返回完成核单时固化的终态结果 | +| 重复 finalize | 依赖旧报告确认门禁 | 已有当前终态时返回已有结果 | + +## 11. 影响评估 / 回滚 + +### 11.1 影响评估 + +- **是否破坏向后兼容**: 是。两个 POST 确认接口删除,finalize 请求字段和必填规则改变。 +- **前端是否必须同步调整**: 是。旧页面继续调用 `/confirm` 会收到业务码 `404`;旧 finalize 请求缺少新必填字段会收到业务码 `400`。 +- **查询字段兼容性**: 两张 GET 报表的顶层字段结构保持不变,但数据时效语义变为“未终态实时、终态固定”。 + +### 11.2 回滚说明 + +- 如果接口契约回滚,前端需要同步恢复两次确认请求和两个指纹字段。 +- 前后端不能混用新旧流程:新版前端不再保留报表确认指纹,旧版后端仍会要求指纹和独立确认。 + +## 12. 注意事项 + +- 删除“确认主报账表”“确认单团核算表”按钮、请求封装、loading 状态、重试逻辑和指纹缓存。 +- 页面展示仍调用两个 GET 接口;无需在详情加载时调用任何写接口。 +- “完成核单”按钮直接提交 finalize 新请求体。 +- 不要把 GET 返回的 `sourceFingerprint` 填回 finalize。 +- 不要继续发送已删除字段;即使服务端当前可能忽略未知 JSON 字段,前端类型和请求对象也应删除。 +- `signedVoucher` 不是可选附件:至少需要一个有效 URL。 +- `mqTriggered=false` 是当前固定契约,不要显示“MQ 触发失败”或据此轮询。 + +## 13. 关联 / 联系人 + +### 13.1 链接 + +- **Issue**: [#5342](https://git.1814.love:8443/wx/HL/issues/5342) +- **PR**: [#5345](https://git.1814.love:8443/wx/HL/pulls/5345) +- **Merge commit**: [eb9ecfafad](https://git.1814.love:8443/wx/HL/commit/eb9ecfafadde4cb9e2dbe5be8193abcf569ca44c) + +### 13.2 联系人 + +- **后端负责人**: @yst +- **消费端**: v3 管理后台