From cf97a39075a8964f158cdd5b6da45ccb6013acc5 Mon Sep 17 00:00:00 2001 From: yaosutu <770858045@qq.com> Date: Wed, 29 Jul 2026 16:09:11 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=A0=B8=E5=8D=95=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E6=8E=A5=E5=8F=A3=E5=8F=8C=E6=8C=87=E7=BA=B9=E4=B8=8E?= =?UTF-8?q?=E5=87=AD=E6=8D=AE=E5=A5=91=E7=BA=A6=EF=BC=88#5343=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...单确认收口到完成核单-修改接口-管理后台.md | 874 ++++++++++++++++++ 1 file changed, 874 insertions(+) create mode 100644 changelogs-v2/2026-07/29_5343_核单确认收口到完成核单-修改接口-管理后台.md diff --git a/changelogs-v2/2026-07/29_5343_核单确认收口到完成核单-修改接口-管理后台.md b/changelogs-v2/2026-07/29_5343_核单确认收口到完成核单-修改接口-管理后台.md new file mode 100644 index 0000000..22dd0cd --- /dev/null +++ b/changelogs-v2/2026-07/29_5343_核单确认收口到完成核单-修改接口-管理后台.md @@ -0,0 +1,874 @@ +--- +schema: "hl-changelog/v2" +ticket: "5343" +title: "核单确认收口到完成核单" +consumer: "admin" +change_type: "修改接口" +backend_status: "merged" +gateway_status: "pending" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "后端 PR #5347 已合并;等待测试服部署与网关验证,前端待按本文纠正 finalize 请求契约" +updated_at: "2026-07-29" +base: "dev-v3" +--- + +# ⚠️【修改接口·管理后台】核单确认收口到完成核单 (#5343) + +> **PR**: #5347 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-29 + +## 1. 接口背景 + +主报账表和单团核算表不再各自提供“确认”写操作。页面先通过两张 GET 报表取得同一轮核单事实对应的两个 `sourceFingerprint`,再由“完成核单”一次提交双指纹、转账信息、预支处理标志和签字凭证。 + +本文纠正并取代 `29_5342_核单报表取消中间确认并由finalize固化-修改接口-管理后台.md` 中关于 finalize 请求的说明:**双指纹没有删除,仍是 finalize 必填字段;转账与凭证字段必须放在必填的 `reimbursementConfirmation` 对象内。** + +## 2. 变更清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 查询主报账表 | GET | `/v3/admin/order/{orderId}/settlement/reports/reimbursement` | 行为明确 | 返回主报账数据及 `sourceFingerprint`,该指纹必须回传给 finalize | +| 2 | 查询单团核算表 | GET | `/v3/admin/order/{orderId}/settlement/reports/group` | 行为明确 | 返回单团核算数据及 `sourceFingerprint`,该指纹必须回传给 finalize | +| 3 | 完成核单 | POST | `/v3/admin/order/{orderId}/settlement/finalize` | 请求与行为修改 | 必填双指纹和嵌套 `reimbursementConfirmation`;成功后一次完成核单 | +| 4 | 确认主报账表 | POST | `/v3/admin/order/{orderId}/settlement/reports/reimbursement/confirm` | 删除接口 | 路由继续保持删除,不得调用 | +| 5 | 确认单团核算表 | POST | `/v3/admin/order/{orderId}/settlement/reports/group/confirm` | 删除接口 | 路由继续保持删除,不得调用 | + +## 3. 接口详情 + +### 3.1 查询主报账表 + +- **方法与路径**:`GET /v3/admin/order/{orderId}/settlement/reports/reimbursement` +- **使用场景**:展示主报账表,并在调用 finalize 前取得最新主报账指纹 +- **认证**:管理后台 JWT;房务角色不可访问 +- **幂等性**:幂等,只读 +- **限流**:无接口级特殊限流 + +**路径参数** + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `orderId` | String/Long | 是 | 订单 ID,必须大于 `0` | + +**请求体** + +无。 + +**响应字段** + +| `data` 字段 | JSON 类型 | 可空 | 说明 | +|-------------|-----------|:---:|------| +| `id` | string | 是 | 报账表记录 ID | +| `orderId` | string | 否 | 订单 ID | +| `reportStatus` | string | 否 | 报表状态,见 §6.1 | +| `sourceFingerprint` | string | 否 | 64 位小写十六进制 SHA-256;传给 finalize 的 `reimbursementExpectedSourceFingerprint` | +| `primaryReporterId` | string | 是 | 主报账人 ID | +| `primaryReporterName` | string | 是 | 主报账人姓名 | +| `primaryReporterRole` | string | 是 | 主报账人角色 | +| `reportVersion` | integer | 否 | 报账表结构版本 | +| `driverCollectedTailAmount` | number | 否 | 主报账人代收尾款 | +| `approvedAdvanceAmount` | number | 否 | 已审批预支金额 | +| `reportablePaidCostAmount` | number | 否 | 可报账的已付成本 | +| `reporterNetAmount` | number | 否 | 主报账人净额;决定转账日期和流水是否必填 | +| `primaryReporterCollectedAmount` | number | 否 | 主报账人代收金额 | +| `publicPrepaidAmount` | number | 否 | 公共预支金额 | +| `primaryReporterDueAmount` | number | 否 | 主报账人应报账金额 | +| `advanceOutstandingAmount` | number | 否 | 未结清预支金额 | +| `reconNetAmount` | number | 否 | 报账净额 | +| `transferDirection` | string | 否 | 转账方向,见 §6.2 | +| `transferAmount` | number | 否 | 应转账金额的绝对值 | +| `incomeLines` | array<object> | 否 | 主报账人代收明细,结构见下表 | +| `expenseLines` | array<object> | 否 | 主报账成本明细,结构见下表 | +| `advanceLines` | array<object> | 否 | 已审批预支明细,结构见下表 | +| `vehicleLines` | array<object> | 否 | 车辆独立明细;没有独立行时为 `[]` | +| `transferStatus` | string | 是 | 未完成核单时可为 `null`;终态为 `COMPLETED` | +| `transferDate` | string(date) | 是 | 转账日期,格式 `YYYY-MM-DD` | +| `transferRef` | string | 是 | 转账流水号 | +| `advanceSettledFlag` | boolean | 是 | 预支是否已处理 | +| `signedVoucher` | object | 是 | 签字凭证;结构与 finalize 的凭证一致 | +| `generatedBy` | string | 是 | 历史生成操作人 ID | +| `generatedByName` | string | 是 | 历史生成操作人姓名 | +| `generatedAt` | string(date-time) | 是 | 历史生成时间 | +| `confirmedBy` | string | 是 | 完成核单操作人 ID | +| `confirmedByName` | string | 是 | 完成核单操作人姓名 | +| `confirmedAt` | string(date-time) | 是 | 完成核单时间 | + +**`incomeLines[]` 字段** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `type` | string | 当前为 `DRIVER_CASH_RECEIPT` | +| `receiptId` | string | 收款记录 ID | +| `amount` | number | 收款金额 | +| `channel` | string | 收款渠道 | +| `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` | number | 已审批金额 | +| `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` | number | 当前行实际成本 | +| `paymentMethod` | string | 当前报账明细使用 `CASH_PAID` | + +不同 `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`。 + +**错误与业务边界** + +- `orderId <= 0` 返回 `400`。 +- 房务角色或无订单访问权限返回 `403`/对应订单访问错误。 +- 未完成核单时返回当前核单事实的实时视图和当前指纹。 +- 已完成核单时返回当前有效终态版本中的报账表和该版本指纹。 +- 管理员反确认后再次 GET 会回到实时视图;前端必须重新取得指纹。 + +**典型请求** + +```http +GET /v3/admin/order/1914050000000001/settlement/reports/reimbursement +Authorization: Bearer +``` + +无请求体。 + +**典型响应** + +```json +{ + "code": 200, + "message": "成功", + "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": [], + "expenseLines": [], + "advanceLines": [], + "vehicleLines": [], + "transferStatus": null, + "transferDate": null, + "transferRef": null, + "advanceSettledFlag": null, + "signedVoucher": null, + "generatedBy": null, + "generatedByName": null, + "generatedAt": null, + "confirmedBy": null, + "confirmedByName": null, + "confirmedAt": null + }, + "traceId": "a1b2c3d4-e5f6-7890", + "success": true +} +``` + +### 3.2 查询单团核算表 + +- **方法与路径**:`GET /v3/admin/order/{orderId}/settlement/reports/group` +- **使用场景**:展示单团核算表,并在调用 finalize 前取得最新单团指纹 +- **认证**:管理后台 JWT;房务角色不可访问 +- **幂等性**:幂等,只读 +- **限流**:无接口级特殊限流 + +**路径参数** + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `orderId` | String/Long | 是 | 订单 ID,必须大于 `0` | + +**请求体** + +无。 + +**响应字段** + +| `data` 字段 | JSON 类型 | 可空 | 说明 | +|-------------|-----------|:---:|------| +| `id` | string | 是 | 单团核算表记录 ID | +| `orderId` | string | 否 | 订单 ID | +| `reportStatus` | string | 否 | 报表状态,见 §6.1 | +| `sourceFingerprint` | string | 否 | 64 位小写十六进制 SHA-256;传给 finalize 的 `groupExpectedSourceFingerprint` | +| `baseOrderAmount` | number | 否 | 订单基础金额 | +| `otherIncomeAmount` | number | 否 | 其他收入金额 | +| `discountAmount` | number | 否 | 优惠金额 | +| `adjustedReceivableAmount` | number | 否 | 调整后应收金额 | +| `paidAmount` | number | 否 | 已收金额 | +| `actualRefundedAmount` | number | 否 | 实际退款金额 | +| `netRevenueAmount` | number | 否 | 净收入 | +| `netReceivedAmount` | number | 否 | 净已收 | +| `outstandingAmount` | number | 否 | 待收金额;不为 `0` 时不能 finalize | +| `hotelCost` | number | 否 | 住宿成本 | +| `ticketCost` | number | 否 | 门票/游玩项目成本 | +| `mealCost` | number | 否 | 餐食成本 | +| `vehicleCost` | number | 否 | 车辆成本 | +| `guideCost` | number | 否 | 导游/领队成本 | +| `photographerCost` | number | 否 | 摄影成本 | +| `otherExpenseCost` | number | 否 | 其他支出成本 | +| `insurancePremium` | number | 否 | 保险保费 | +| `totalCost` | number | 否 | 总成本 | +| `paidCost` | number | 否 | 已付成本 | +| `unpaidCost` | number | 否 | 未付成本 | +| `grossProfit` | number | 否 | 毛利 | +| `grossProfitRate` | number | 否 | 毛利率,小数形式 | +| `travelerCount` | integer | 否 | 出行人数 | +| `perCapitaRevenue` | number | 否 | 人均收入 | +| `perCapitaCost` | number | 否 | 人均成本 | +| `perCapitaProfit` | number | 否 | 人均利润 | +| `incomeLines` | array<object> | 否 | 收入汇总行 | +| `costCategories` | array<object> | 否 | 成本分类汇总 | +| `generatedBy` | string | 是 | 历史生成操作人 ID | +| `generatedByName` | string | 是 | 历史生成操作人姓名 | +| `generatedAt` | string(date-time) | 是 | 历史生成时间 | +| `confirmedBy` | string | 是 | 完成核单操作人 ID | +| `confirmedByName` | string | 是 | 完成核单操作人姓名 | +| `confirmedAt` | string(date-time) | 是 | 完成核单时间 | + +**`incomeLines[]` 字段** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `type` | string | `BASE_ORDER`、`OTHER_INCOME`、`DISCOUNT` 或 `ACTUAL_REFUND` | +| `amount` | number | 金额;优惠和实际退款以负数返回 | + +**`costCategories[]` 字段** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `category` | string | `HOTEL`、`TICKET`、`MEAL`、`VEHICLE`、`GUIDE`、`PHOTOGRAPHER`、`OTHER_EXPENSE` 或 `INSURANCE` | +| `amount` | number | 分类成本 | + +**错误与业务边界** + +- `orderId <= 0` 返回 `400`。 +- 房务角色或无订单访问权限返回 `403`/对应订单访问错误。 +- 未完成核单时返回实时视图;已完成核单时返回当前有效终态版本。 +- 管理员反确认后,下一次 GET 会生成新的实时结果和指纹。 + +**典型请求** + +```http +GET /v3/admin/order/1914050000000001/settlement/reports/group +Authorization: Bearer +``` + +无请求体。 + +**典型响应** + +```json +{ + "code": 200, + "message": "成功", + "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": 5200.00, + "guideCost": 800.00, + "photographerCost": 600.00, + "otherExpenseCost": 1200.00, + "insurancePremium": 180.00, + "totalCost": 16800.00, + "paidCost": 16800.00, + "unpaidCost": 0.00, + "grossProfit": 8200.00, + "grossProfitRate": 0.328, + "travelerCount": 5, + "perCapitaRevenue": 5000.00, + "perCapitaCost": 3360.00, + "perCapitaProfit": 1640.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": 5200.00}, + {"category": "GUIDE", "amount": 800.00}, + {"category": "PHOTOGRAPHER", "amount": 600.00}, + {"category": "OTHER_EXPENSE", "amount": 1200.00}, + {"category": "INSURANCE", "amount": 180.00} + ], + "generatedBy": null, + "generatedByName": null, + "generatedAt": null, + "confirmedBy": null, + "confirmedByName": null, + "confirmedAt": null + }, + "traceId": "a1b2c3d4-e5f6-7890", + "success": true +} +``` + +### 3.3 完成核单 + +- **方法与路径**:`POST /v3/admin/order/{orderId}/settlement/finalize` +- **使用场景**:两张报表核对完成后,一次提交双指纹和主报账凭据 +- **认证**:管理后台 JWT;房务角色不可访问 +- **幂等性**:严格幂等,比较双指纹、规范化后的凭据和 `remark` +- **限流**:无接口级特殊限流 + +**路径参数** + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `orderId` | String/Long | 是 | 订单 ID,必须大于 `0` | + +**请求体字段** + +| 字段 | JSON 类型 | 必填 | 校验与规范化 | +|------|-----------|:---:|--------------| +| `remark` | string/null | 否 | 最长 500;去除首尾空格,空串按 `null` 比较 | +| `reimbursementExpectedSourceFingerprint` | string | 是 | 必须等于主报账 GET 返回的 64 位小写十六进制 `sourceFingerprint` | +| `groupExpectedSourceFingerprint` | string | 是 | 必须等于单团 GET 返回的 64 位小写十六进制 `sourceFingerprint` | +| `reimbursementConfirmation` | object | 是 | 主报账转账、预支和签字凭据 | +| `reimbursementConfirmation.transferDate` | string(date)/null | 条件必填 | `reporterNetAmount != 0` 时必填;净额为 `0` 时可为 `null` | +| `reimbursementConfirmation.transferRef` | string/null | 条件必填 | 去除首尾空格后最长 128;净额非 `0` 时长度必须为 1~128 | +| `reimbursementConfirmation.advanceSettledFlag` | boolean | 是 | 必须明确传值,`false` 合法 | +| `reimbursementConfirmation.signedVoucher` | object | 是 | 缺失返回 `400` | +| `reimbursementConfirmation.signedVoucher.files` | array<object> | 业务必填 | 1~9 项;为 `null`、空数组、超过 9 项或含 `null` 项返回 `584317` | +| `reimbursementConfirmation.signedVoucher.files[].url` | string | 业务必填 | 去除首尾空格后长度 1~1024;不符合返回 `584317` | +| `reimbursementConfirmation.signedVoucher.files[].name` | string/null | 否 | 去除首尾空格;空串归一化为 `null`;非空最长 255 | +| `reimbursementConfirmation.signedVoucher.note` | string/null | 否 | 去除首尾空格;空串归一化为 `null`;非空最长 500 | + +`transferStatus` **不得提交**。finalize 成功后,报账终态中的 `transferStatus` 固定为 `COMPLETED`。 + +签字凭证文件按规范化后的 `url`、`name` 升序稳定保存。不得依赖请求数组原顺序进行严格幂等判断。 + +**响应字段** + +| `data` 字段 | JSON 类型 | 说明 | +|-------------|-----------|------| +| `summaryId` | string | 核单汇总 ID | +| `finalSnapshotId` | string | 核单终态快照 ID | +| `finalSnapshotVersionNo` | integer | 终态版本号;首次为 1,反确认后再次 finalize 为上一版本 + 1 | +| `finalSnapshotStatus` | string | 成功固定为 `FINALIZED` | +| `orderId` | string | 订单 ID | +| `settledAt` | string(date-time) | ISO-8601 核单完成时间 | +| `totalAmount` | string | 订单总金额快照 | +| `paidAmount` | string | 已付金额快照 | +| `balanceAmount` | string | 尾款金额快照 | +| `roomCost` | string | 住宿实际成本 | +| `ticketCost` | string | 门票实际成本 | +| `staffCost` | string | 人员费用实际成本 | +| `subsidyCost` | string | 补助实际成本 | +| `mealCost` | string | 餐食实际成本 | +| `vehicleCost` | string | 车辆成本 | +| `otherExpenseCost` | string | 其他支出实际成本 | +| `insurancePremium` | string | 保险实际保费 | +| `totalActualCost` | string | 总实际成本 | +| `driverTransferAmount` | string | 给司机/主报账人转回金额 | +| `profitAmount` | string | 公司毛利 | +| `profitRate` | number | 毛利率;订单总金额为 0 时为 0 | +| `orderStatusAfter` | string | 成功后为 `待财务复核` | +| `mqTriggered` | boolean | 当前固定为 `false` | +| `warnings` | array<string> | 软预警列表;无预警为 `[]` | + +**错误与业务边界** + +- 缺 body、非法 JSON、`remark` 超长、双指纹格式错误,或缺少 `reimbursementConfirmation`、`advanceSettledFlag`、`signedVoucher`:返回 `400`。 +- 双指纹任一与当前冻结事实不一致:返回 `584315`,须重新 GET 两张报表。 +- `transferRef` 条件不满足或超过 128,凭证 `files`/文件项/`url` 无效,或 `name`/`note` 超长:返回 `584317`。 +- 单团核算的 `outstandingAmount != 0`:返回 `584082`,不能完成核单。 +- 完全相同的终态请求重试返回原 `summaryId`、`finalSnapshotId` 和版本号,不产生新版本。 +- 已有当前终态时,双指纹、规范化凭据或 `remark` 任一不同:返回 `584316`。 +- 任一失败不留下部分完成结果。 + +**典型请求:净报账金额非 0** + +```http +POST /v3/admin/order/1914050000000001/settlement/finalize +Authorization: Bearer +Content-Type: application/json + +{ + "remark": "主报账人与单团核算均已核对", + "reimbursementExpectedSourceFingerprint": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "groupExpectedSourceFingerprint": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + "reimbursementConfirmation": { + "transferDate": "2026-07-29", + "transferRef": "FT202607290001", + "advanceSettledFlag": false, + "signedVoucher": { + "files": [ + { + "name": "司机签字报账单.pdf", + "url": "https://oss.example.com/settlement/driver-signed-20260729.pdf" + } + ], + "note": "司机现场签字后上传" + } + } +} +``` + +**典型响应** + +```json +{ + "code": 200, + "message": "成功", + "data": { + "summaryId": "9600000000001", + "finalSnapshotId": "9600000000002", + "finalSnapshotVersionNo": 1, + "finalSnapshotStatus": "FINALIZED", + "orderId": "1914050000000001", + "settledAt": "2026-07-29T10:30:25", + "totalAmount": "24800.00", + "paidAmount": "24800.00", + "balanceAmount": "0.00", + "roomCost": "4280.00", + "ticketCost": "3680.00", + "staffCost": "7000.00", + "subsidyCost": "720.00", + "mealCost": "860.00", + "vehicleCost": "5200.00", + "otherExpenseCost": "1200.00", + "insurancePremium": "180.00", + "totalActualCost": "23120.00", + "driverTransferAmount": "22940.00", + "profitAmount": "1680.00", + "profitRate": 0.0677, + "orderStatusAfter": "待财务复核", + "mqTriggered": false, + "warnings": [] + }, + "traceId": "a1b2c3d4-e5f6-7890", + "success": true +} +``` + +**边界请求:`reporterNetAmount = 0`** + +```http +POST /v3/admin/order/1914050000000002/settlement/finalize +Authorization: Bearer +Content-Type: application/json + +{ + "remark": null, + "reimbursementExpectedSourceFingerprint": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "groupExpectedSourceFingerprint": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + "reimbursementConfirmation": { + "transferDate": null, + "transferRef": null, + "advanceSettledFlag": false, + "signedVoucher": { + "files": [ + { + "name": null, + "url": "https://oss.example.com/settlement/zero-net-signed.jpg" + } + ], + "note": null + } + } +} +``` + +**边界响应** + +```json +{ + "code": 200, + "message": "成功", + "data": { + "summaryId": "9600000000011", + "finalSnapshotId": "9600000000012", + "finalSnapshotVersionNo": 1, + "finalSnapshotStatus": "FINALIZED", + "orderId": "1914050000000002", + "settledAt": "2026-07-29T10:35: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": [] + }, + "traceId": "a1b2c3d4-e5f6-7890", + "success": true +} +``` + +**异常请求:凭证包含空 URL** + +```http +POST /v3/admin/order/1914050000000001/settlement/finalize +Authorization: Bearer +Content-Type: application/json + +{ + "reimbursementExpectedSourceFingerprint": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "groupExpectedSourceFingerprint": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + "reimbursementConfirmation": { + "transferDate": "2026-07-29", + "transferRef": "FT202607290001", + "advanceSettledFlag": true, + "signedVoucher": { + "files": [ + {"name": "签字单.pdf", "url": " "} + ] + } + } +} +``` + +**异常响应** + +```json +{ + "code": 584317, + "message": "当前报告状态不允许执行该操作", + "data": null, + "traceId": "a1b2c3d4-e5f6-7890", + "success": false +} +``` + +**异常请求:缺少 `signedVoucher`** + +```http +POST /v3/admin/order/1914050000000001/settlement/finalize +Authorization: Bearer +Content-Type: application/json + +{ + "reimbursementExpectedSourceFingerprint": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "groupExpectedSourceFingerprint": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + "reimbursementConfirmation": { + "transferDate": "2026-07-29", + "transferRef": "FT202607290001", + "advanceSettledFlag": true + } +} +``` + +**异常响应** + +```json +{ + "code": 400, + "message": "参数校验失败", + "data": null, + "traceId": "a1b2c3d4-e5f6-7890", + "success": false +} +``` + +### 3.4 已删除:确认主报账表 + +- **原方法与路径**:`POST /v3/admin/order/{orderId}/settlement/reports/reimbursement/confirm` +- **当前契约**:接口已删除,无有效请求体或成功响应。 +- **前端动作**:删除请求封装、按钮、loading、重试和错误忽略逻辑。 + +**请求示例** + +```http +POST /v3/admin/order/1914050000000001/settlement/reports/reimbursement/confirm +Authorization: Bearer +Content-Type: application/json + +{} +``` + +**响应示例** + +```json +{ + "code": 404, + "message": "请求地址不存在", + "data": null, + "success": false +} +``` + +### 3.5 已删除:确认单团核算表 + +- **原方法与路径**:`POST /v3/admin/order/{orderId}/settlement/reports/group/confirm` +- **当前契约**:接口已删除,无有效请求体或成功响应。 +- **前端动作**:删除请求封装、按钮、loading、重试和错误忽略逻辑。 + +**请求示例** + +```http +POST /v3/admin/order/1914050000000001/settlement/reports/group/confirm +Authorization: Bearer +Content-Type: application/json + +{} +``` + +**响应示例** + +```json +{ + "code": 404, + "message": "请求地址不存在", + "data": null, + "success": false +} +``` + +## 4. 接口入参汇总 + +| 接口 | 入参 | +|------|------| +| 主报账 GET | 路径参数 `orderId`;无请求体 | +| 单团 GET | 路径参数 `orderId`;无请求体 | +| finalize | 路径参数 `orderId`;请求体必须包含两个指纹及 `reimbursementConfirmation` | +| 两个旧 confirm | 已删除,无有效入参 | + +双指纹映射必须严格如下: + +| 来源 | finalize 字段 | +|------|---------------| +| 主报账 GET 的 `data.sourceFingerprint` | `reimbursementExpectedSourceFingerprint` | +| 单团 GET 的 `data.sourceFingerprint` | `groupExpectedSourceFingerprint` | + +## 5. 出参汇总 + +- 两张 GET 均返回 `Result<报表对象>`,其中 `sourceFingerprint` 是 finalize 的提交凭据。 +- finalize 返回 `Result`,完整字段见 §3.3。 +- 两个旧 confirm 不再返回业务成功响应,只会命中不存在的路由。 +- 金额序列化以各字段表和示例为准:finalize 的金额字段为字符串,两张 GET 的金额字段为 JSON number。 + +## 6. 枚举 / 数据字典 + +### 6.1 `reportStatus` + +**所属字段**:两张报表响应 `reportStatus` | **类型**:string + +| 值 | 中文 | 说明 | +|----|------|------| +| `GENERATED` | 实时结果 | 当前不存在有效终态,按当前核单事实计算 | +| `CONFIRMED` | 已固化 | 返回当前有效终态版本中的报表 | +| `STALE` | 历史过期 | 兼容历史报表状态,不用于当前 finalize | + +### 6.2 `transferDirection` + +**所属字段**:主报账响应 `transferDirection` | **类型**:string + +| 值 | 中文 | 说明 | +|----|------|------| +| `REPORTER_TO_COMPANY` | 报账人转公司 | `reporterNetAmount > 0` | +| `COMPANY_TO_REPORTER` | 公司转报账人 | `reporterNetAmount < 0` | +| `BALANCED` | 已平衡 | `reporterNetAmount = 0` | + +### 6.3 `category` + +**所属字段**:`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` | 尚未固化 | 实时报表可为空 | + +`transferStatus` 只出现在响应中,不是 finalize 入参。 + +## 7. 错误码 + +| code | 含义 | 触发场景 | +|------|------|----------| +| `400` | 请求/参数校验失败 | `orderId <= 0`、缺请求体、非法 JSON、双指纹格式错误、缺 `reimbursementConfirmation`/`advanceSettledFlag`/`signedVoucher`、`remark` 超长 | +| `403` | 无访问权限 | 房务角色或无权访问当前订单 | +| `404` | 路由不存在 | 调用两个已删除的报表 confirm 接口 | +| `584082` | 存在待收尾款 | 单团核算 `outstandingAmount != 0` | +| `584100` | 车辆费用暂时不可用 | 报表查询或 finalize 当前无法取得可核单车辆费用 | +| `584101` | 车辆事实未完成 | 存在未完结派车或未确认车辆费用 | +| `584102` | 缺少车辆费用 | 有用车需求但没有可核单车辆费用 | +| `584315` | 核单来源数据已变化 | 车辆候选与冻结事实不一致,或任一双指纹过期 | +| `584316` | 并发或严格幂等冲突 | 终态重试请求不同、并发完成/反确认冲突 | +| `584317` | 转账条件或签字凭证不合法 | 净额非 0 缺日期/流水、流水超长、files/文件项/url 无效、name/note 超长 | +| `584320` | 核单明细未准备好 | 当前分类数据不能用于报账或 finalize | +| `584321` | 缺少当前终态 | 后续财务复核缺少 current `FINALIZED` 终态 | +| `584325` | 双指纹兜底校验失败 | finalize 发现双指纹不完整或不合法 | +| `584326` | 终态组合不一致 | 当前终态、关联汇总或订单终态不匹配 | + +## 8. 示例索引 + +| 场景 | 位置 | +|------|------| +| 主报账 GET 典型请求与响应 | §3.1 | +| 单团 GET 典型请求与响应 | §3.2 | +| finalize 净额非 0 典型成功 | §3.3 | +| finalize 净额为 0 合法边界 | §3.3 | +| finalize 凭证 URL 非法返回 584317 | §3.3 | +| finalize 缺 `signedVoucher` 返回 400 | §3.3 | +| 两个旧 confirm 返回 404 | §3.4、§3.5 | + +## 9. 业务边界 + +- 必须先分别 GET 两张报表,再把两个 `sourceFingerprint` 一一映射到 finalize;不能复用旧指纹、互换字段或只传一个。 +- 任一核单事实变化后,旧双指纹都会失效;收到 `584315` 后必须重新 GET 两张表。 +- `outstandingAmount` 必须为 `0` 才能 finalize。 +- `reporterNetAmount != 0` 时,`transferDate` 和非空 `transferRef` 同时必填;净额为 `0` 时二者可为 `null`。 +- `advanceSettledFlag=false` 是有效业务值,不等同于缺失。 +- `signedVoucher` 始终必填,且 `files` 必须有 1~9 个合法文件项。 +- 完全相同请求重试严格幂等;任何双指纹、规范化凭据或 `remark` 差异均返回 `584316`。 +- 管理员反确认使当前终态失效后,两张 GET 重新返回实时结果;再次 finalize 必须使用新双指纹,成功响应的 `finalSnapshotVersionNo` 为上一版本 + 1。 +- finalize 成功后订单进入“待财务复核”。既有财务复核接口 `POST /v3/admin/order/{orderId}/settlement/confirm` 的请求/响应结构未在本次变更:请求仅含可选 `confirmRemark`;当前没有独立财务角色校验;成功 `data` 为 `orderId`、`settlementStatus=COMPLETED`、`settledAt`、`flowStatus=SETTLED`。其复核前提为当前有效 `FINALIZED` 终态及其关联汇总,旧报表 confirm 状态不参与判断。 + +## 10. 修改前后对比 + +### 10.1 字段级对比 + +| 接口/字段 | 修改前或错误说明 | 当前正确契约 | +|-----------|------------------|--------------| +| finalize 双指纹 | #5342 通知误写为删除 | 两个字段均必填 | +| `reimbursementExpectedSourceFingerprint` | 误写为不再回传 | 来自主报账 GET 的 `sourceFingerprint` | +| `groupExpectedSourceFingerprint` | 误写为不再回传 | 来自单团 GET 的 `sourceFingerprint` | +| `reimbursementConfirmation` | #5342 把内部字段错误提升到 finalize 顶层 | 必填嵌套对象 | +| `transferDate` | 误写为 finalize 顶层 | 位于 `reimbursementConfirmation` | +| `transferRef` | 误写为 finalize 顶层 | 位于 `reimbursementConfirmation`,trim 后最长 128 | +| `advanceSettledFlag` | 误写为 finalize 顶层 | 位于 `reimbursementConfirmation`,必填 boolean | +| `signedVoucher` | 误写为 finalize 顶层 | 位于 `reimbursementConfirmation`,必填 object | +| `transferStatus` | 可能沿用旧 confirm 传值 | finalize 不接收,成功后固定为 `COMPLETED` | + +### 10.2 行为级对比 + +| 行为 | 修改前 | 当前 | +|------|--------|------| +| 主报账确认 | 独立 POST confirm | 接口删除,由 finalize 一次完成 | +| 单团确认 | 独立 POST confirm | 接口删除,由 finalize 一次完成 | +| finalize 前的数据校验 | 分散在两个 confirm | 两张 GET 取双指纹,finalize 一次校验 | +| 重复 finalize | 旧流程语义不明确 | 完全相同返回原结果,任一差异返回 `584316` | +| 反确认后再次核单 | 可能沿用旧报表结果 | 重新 GET 新指纹,再 finalize 生成版本号 + 1 | + +## 11. 影响评估 / 回滚 + +### 11.1 影响评估 + +- **是否破坏向后兼容**:是。两个 POST confirm 已删除,finalize 的双指纹及嵌套凭据均为必填。 +- **前端是否必须同步上线**:是。按 #5342 错误契约提交会因缺双指纹或缺 `reimbursementConfirmation` 返回 `400`/业务错误。 +- **查询兼容性**:两张 GET 的字段结构保持,`sourceFingerprint` 的用途明确为 finalize 必填凭据。 + +### 11.2 回滚说明 + +- 前后端必须使用同一版核单流程;不能混用“独立 confirm”和“finalize 双指纹”两套调用顺序。 +- 若后端契约回滚,前端也需同步恢复对应请求模型与调用链,不能只单独回滚一端。 + +## 12. 注意事项 + +- 删除两个报表确认按钮及对应请求、loading、重试、错误忽略代码。 +- 保留两个 GET 返回的 `sourceFingerprint`,并在点击完成核单前保存当前两份值。 +- finalize 请求模型必须新增必填 `reimbursementConfirmation`,其余凭据字段不得放在顶层。 +- 不要发送 `transferStatus`;页面在 finalize 成功后按响应/重新 GET 展示终态。 +- 不要继续沿用 #5342 通知中的“删除双指纹”“finalize 顶层凭据字段”实现。 +- 对 `584315` 进行刷新两张报表后重试;对 `584316` 不要静默覆盖终态。 + +## 13. 关联 / 联系人 + +### 13.1 链接 + +- **Issue**: [#5343](https://git.1814.love:8443/wx/HL/issues/5343) +- **PR**: [#5347](https://git.1814.love:8443/wx/HL/pulls/5347) +- **Merge commit**: [a892a6b56a](https://git.1814.love:8443/wx/HL/commit/a892a6b56a2c3c0c2e4e345156096ac1ac5750c0) + +### 13.2 联系人 + +- **后端负责人**: @yst +- **消费端**: v3 管理后台