--- schema: "hl-changelog/v2" ticket: "5342" title: "核单报表取消中间确认并由 finalize 固化" consumer: "admin" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "implemented" frontend_owner: "pi:019fadb7-dac9-74bf-9581-058835251208" frontend_ref: "1444fc7f0bf34efaec0ee9f775f7d529b609b847" target_release: "v2.1" verified_at: "2026-07-29T20:43:00+08:00" status_note: "管理后台已删除两张报表的中间确认调用;finalize 请求结构按后续 #5343 最终契约收口,pnpm checkpoint 全部通过。" 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 管理后台