修正核单完成接口双指纹与凭据契约(#5343)
一些检查失败了
changelog-filename-gate / validate (push) Failing after 2s

这个提交包含在:
yaosutu 2026-07-29 16:09:11 +08:00
父节点 2ce66df423
当前提交 cf97a39075

查看文件

@ -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 <admin-jwt>
```
无请求体。
**典型响应**
```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&lt;object&gt; | 否 | 收入汇总行 |
| `costCategories` | array&lt;object&gt; | 否 | 成本分类汇总 |
| `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 <admin-jwt>
```
无请求体。
**典型响应**
```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` 时长度必须为 1128 |
| `reimbursementConfirmation.advanceSettledFlag` | boolean | 是 | 必须明确传值,`false` 合法 |
| `reimbursementConfirmation.signedVoucher` | object | 是 | 缺失返回 `400` |
| `reimbursementConfirmation.signedVoucher.files` | array&lt;object&gt; | 业务必填 | 19 项;为 `null`、空数组、超过 9 项或含 `null` 项返回 `584317` |
| `reimbursementConfirmation.signedVoucher.files[].url` | string | 业务必填 | 去除首尾空格后长度 11024;不符合返回 `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&lt;string&gt; | 软预警列表;无预警为 `[]` |
**错误与业务边界**
- 缺 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 <admin-jwt>
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 <admin-jwt>
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 <admin-jwt>
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 <admin-jwt>
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 <admin-jwt>
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 <admin-jwt>
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<SettlementSubmitRespVO>`,完整字段见 §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` 必须有 19 个合法文件项。
- 完全相同请求重试严格幂等;任何双指纹、规范化凭据或 `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 管理后台