--- schema: "hl-changelog/v2" ticket: "5916" title: "核单两报表出参瘦身合并版(#5876 + #5916):orderHeader 拍平 + 审计字段下线 + finalize warnings 结构化 + summary 新增 settled(破坏性变更)" consumer: "admin" change_type: "修改接口" author: "yst" backend_status: "deployed" gateway_status: "pending" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "ea2916fc" target_release: "" verified_at: "2026-08-12" status_note: "本份是 #5876(PR #5881)与 #5916(PR #5920)两个 PR 对核单报表接口的合并 changelog,以 #5916 合并后的最终出参为准。两份 PR 均已 merge 到 dev-v3 并部署测试服。破坏性:reimbursement 删顶层 orderHeader 块(11 抬头字段拍平进 baseInfo)+ baseInfo 删 10 字段(reportStatus/reportVersion/generated*/confirmed*/transferAmount/primaryReporterDueAmount)+ 新增 outstandingAmount;group 删 orderHeader 块(10 抬头字段平铺顶层)+ 删 reportStatus/generated*/confirmed* 共 8 字段;finalize warnings 由字符串数组改对象数组;summary 新增 settled。此前 #5876 单独那份(12_5876)的 finalize/summary 内容并入本份,前端按本份最终结构一次改到位即可。" updated_at: "2026-08-12" base: "dev-v3" --- # 【⚠️ 修改接口·管理后台】核单两报表出参瘦身(合并 #5876 + #5916 最终态) ## 1. 接口背景 核单结算域的 4 个管理后台接口,近期经过**两次连续变更**(#5876 → #5916),本份 changelog 合并描述**累计最终态**,前端按本份一次改到位即可,**不要**分两次对接: 1. **主报账人报账表**(reimbursement):核单页查看主报账人(通常是司机)的代收、垫付支出、预支与净额结算情况。 2. **单团核算表**(group):核单页 / 财务页查看整团收入、成本、毛利核算。 3. **完成核单**(finalize):核单页点「完成核单」提交,原子冻结核单事实并生成核单汇总。 4. **核单汇总快照**(summary):核单页 / 财务报表页读取整单金额、成本、毛利汇总。 两次变更分别做了什么: - **#5876(PR #5881)**:finalize 出参 warnings 由字符串数组改为对象数组(可定位明细行);summary 出参新增 settled 字段(可靠判「是否已核单」)。 - **#5916(PR #5920,最终态)**:两报表出参瘦身 —— 顶层 orderHeader 子块下线(抬头字段拍平),报表状态 / 生成确认审计字段出参下线,reimbursement 的尾款口径由「主报账人维度」换成「整单未收尾款」。 ## 2. 变更清单 | # | 接口 | 变更 | 类型 | |---|------|------|------| | 1 | reimbursement | 顶层 orderHeader 子块删除,11 个订单抬头字段拍平进 baseInfo(orderNo/teamNo/productName/productType/productTypeName/customerName/departDate/returnDate/consultantName/travelerCount/travelerComposition) | ⚠️ 破坏性 | | 2 | reimbursement | baseInfo 删除 10 个字段:reportStatus / reportVersion / generatedBy / generatedByName / generatedAt / confirmedBy / confirmedByName / confirmedAt / transferAmount / primaryReporterDueAmount | ⚠️ 破坏性 | | 3 | reimbursement | baseInfo 新增 outstandingAmount(整单未收尾款,口径同 financial-overview),替代已删的 primaryReporterDueAmount | ✨ 新增字段 | | 4 | group | 顶层 orderHeader 子块删除,10 个订单抬头字段平铺到顶层(orderNo/teamNo/productName/productType/productTypeName/customerName/departDate/returnDate/consultantName/travelerComposition) | ⚠️ 破坏性 | | 5 | group | 删除 8 个字段:reportStatus / generatedBy / generatedByName / generatedAt / confirmedBy / confirmedByName / confirmedAt,及 orderHeader 块整体 | ⚠️ 破坏性 | | 6 | finalize | 出参 warnings 由 List 改为 List(每条带 settlementId / category / dayNumber / message)(#5876 引入,本份仍有效) | ⚠️ 破坏性 | | 7 | summary | 出参新增 settled 字段(Boolean):已核单 = true,未核单空壳 = false(#5876 引入,本份仍有效) | ✨ 新增字段 | 无入参变化、无 DDL(报表状态 / 版本列仍保留在 DB,仅出参不再下发)。 ## 3. 接口详情 ### 3.1 查主报账人报账表 | 项 | 值 | |---|---| | 方法 + 路径 | GET /v3/admin/order/{orderId}/settlement/reports/reimbursement | | 接口名 | 查询主报账人报账表 | | 使用场景 | 管理后台订单核单页,查看主报账人代收/垫付/预支/净额结算报表 | | 认证 | 管理后台 JWT(/v3/admin/* 走网关鉴权) | | 角色限制 | 房务角色(HOUSE)不可访问,调了会被 403 拦截 | | 幂等性 | 只读查询,幂等 | | 限流 | 走网关默认限流,无接口级特殊限流 | ### 3.2 查单团核算表 | 项 | 值 | |---|---| | 方法 + 路径 | GET /v3/admin/order/{orderId}/settlement/reports/group | | 接口名 | 查询单团核算表 | | 使用场景 | 管理后台核单页 / 财务页,查看整团收入、成本、毛利核算 | | 认证 | 管理后台 JWT | | 角色限制 | 房务角色(HOUSE)不可访问,调了会被 403 拦截 | | 幂等性 | 只读查询,幂等 | | 限流 | 走网关默认限流 | ### 3.3 完成核单 | 项 | 值 | |---|---| | 方法 + 路径 | POST /v3/admin/order/{orderId}/settlement/finalize | | 接口名 | 完成核单 | | 使用场景 | 管理后台核单页,明细核对完成后提交,原子冻结核单事实并生成核单汇总快照 | | 认证 | 管理后台 JWT | | 角色限制 | 房务角色(HOUSE)不可访问,调了会被 403 拦截 | | 幂等性 | 非幂等写操作;重复提交会被「缺少核单终态快照 / 快照已变化」类错误码拦截,前端不要自动重试 | | 限流 | 走网关默认限流 | ### 3.4 查核单汇总快照 | 项 | 值 | |---|---| | 方法 + 路径 | GET /v3/admin/order/{orderId}/settlement/summary | | 接口名 | 查核单汇总快照 | | 使用场景 | 管理后台核单页 / 财务报表页读取整单金额、成本、毛利汇总 | | 认证 | 管理后台 JWT | | 角色限制 | 无接口级角色限制(网关鉴权通过即可) | | 幂等性 | 只读查询,幂等 | | 限流 | 走网关默认限流 | ## 4. 接口入参 ### 4.1 路径参数 | 接口 | 参数 | 类型 | 必填 | 说明 | |------|------|------|------|------| | 全部 4 个接口 | orderId | Long | 是 | 订单 ID,必须大于 0(否则 400「订单 ID 必须大于 0」) | ### 4.2 请求体 / Query 4 个接口均无请求体、无 Query 参数。 ## 5. 出参字段 ### 5.1 reimbursement 出参(Result,最终结构) **顶层结构**(只有 4 个字段,不再有 orderHeader 块、不再有平铺汇总字段): | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | baseInfo | Object | 是 | 基础信息(订单抬头 + 报表汇总 + 报账人),恒下发对象 | | incomeLines | Array | 是 | 收入行(司机/报账人代收);无数据固定返回空数组 [] | | expenseLines | Array | 是 | 支出行(统一扁平字段,仅报账人垫付 CASH_PAID 支出);无数据固定返回空数组 [] | | advanceLines | Array | 是 | 预支明细行(已审批预支逐条);无数据固定返回空数组 [] | **baseInfo 字段表(最终 21 字段)**: | 字段 | 类型 | 说明 | |------|------|------| | id | Long(String) | 核单记录 ID(settlement_recon.recon_id),序列化为字符串;未生成时不输出 | | orderId | Long(String) | 订单 ID,序列化为字符串 | | orderNo | String | 订单号 | | teamNo | String | 团号(订金未支付时不输出) | | productName | String | 产品名 | | productType | String | 产品类型枚举(如 CORE) | | productTypeName | String | 产品类型中文名(字典 product_type 回填) | | customerName | String | 客户姓名 | | departDate | String | 出团日期,格式 yyyy-MM-dd | | returnDate | String | 返回日期,格式 yyyy-MM-dd | | consultantName | String | 定制师姓名 | | travelerCount | Integer | 出行人总数(成人+儿童+幼童+婴儿,空档按 0 计) | | travelerComposition | String | 出行人构成(数量为 0 的档不显示,全空为 null),如「2大 1儿童 1幼童」 | | primaryReporterId | Long(String) | 主报账人人员安排 ID,序列化为字符串 | | primaryReporterName | String | 主报账人姓名 | | primaryReporterRole | String | 主报账人角色(如 DRIVER) | | primaryReporterCollectedAmount | BigDecimal | 主报账人代收金额(收入行合计),保留两位小数 | | approvedAdvanceAmount | BigDecimal | 已审批预支金额(预支行合计),保留两位小数 | | reportablePaidCostAmount | BigDecimal | 可报账已付成本(支出行合计),保留两位小数 | | reporterNetAmount | BigDecimal | 报账人净额(代收 + 预支 - 支出);正 = 报账人应转回公司,负 = 公司应补报账人 | | outstandingAmount | BigDecimal | **本次新增**:整单未收尾款(口径同核单应收财务总览 outstandingAmount),保留两位小数 | > 注:baseInfo 带 @JsonInclude(NON_NULL),null 字段不下发该 key,前端读取要兜底。 **转账方向判定**(transferAmount / transferDirection 均已删,前端自行推导): - reporterNetAmount > 0 → 报账人应转回公司 - reporterNetAmount < 0 → 公司应补报账人 - 转账金额 = reporterNetAmount 的绝对值 **incomeLines 元素字段表**: | 字段 | 类型 | 说明 | |------|------|------| | type | String | 行类型,当前仅 DRIVER_CASH_RECEIPT(司机/报账人现金收款) | | typeName | String | 行类型中文名(字典 settlement_report_line_type) | | receiptId | Long(String) | 线下收款记录 ID,序列化为字符串 | | amount | BigDecimal | 收款金额,保留两位小数 | | channel | String | 收款渠道:DRIVER_CASH / BANK_TRANSFER / CONSULTANT_COLLECTION | | channelName | String | 收款渠道中文名(枚举 label) | | payType | String | 收款款项类型:DEPOSIT / FULL / BALANCE | | payTypeName | String | 款项类型中文名(枚举 label) | | collectorStaffId | Long(String) | 收款人人员安排 ID,序列化为字符串 | | collectorName | String | 收款人姓名 | | collectorRole | String | 收款人角色(如 DRIVER) | | collectorRoleName | String | 收款人角色中文名(字典 staff_role) | | receivedAt | String | 收款时间,格式 yyyy-MM-dd HH:mm:ss | | remark | String | 备注;无备注时不输出 | **expenseLines 元素字段表(统一 12 字段,所有费用分类共用)**: | 字段 | 类型 | 说明 | |------|------|------| | category | String | 费用类别:HOTEL / TICKET / MEAL / VEHICLE / GUIDE / PHOTOGRAPHER / OTHER_EXPENSE / INSURANCE | | categoryName | String | 费用类别中文名(字典 settlement_category) | | itemName | String | 项目名(分类特有信息折叠:住宿=酒店-房型,门票=景区-规格,餐食=餐食名(餐类型),车辆=车牌 车型/司机,人员=姓名,其他=项目名) | | unitPrice | BigDecimal | 单价,保留两位小数;无单价概念的分类不输出 | | quantity | BigDecimal | 数量(住宿=房间数,餐食=份数,门票=票数,车辆按天每行=1);无数量概念的分类不输出 | | amount | BigDecimal | 实际金额,保留两位小数 | | reimburseAmount | BigDecimal | 报账金额,保留两位小数;人员行应报销与金额不同时分别给出,其余分类同 amount | | paymentMethod | String | 付款方式;报账支出行固定 CASH_PAID | | paymentMethodName | String | 付款方式中文名(字典 settlement_payment_method,缺值回退硬编码) | | date | String | 业务日期(住宿=入住日,门票=游玩日,餐食=用餐日,车辆=服务日,人员=结算日),格式 yyyy-MM-dd;无日期概念的分类不输出 | | remark | String | 备注;无备注时不输出 | | voucherUrls | Array | 凭证 URL 数组;无凭证时不输出该键 | **advanceLines 元素字段表**: | 字段 | 类型 | 说明 | |------|------|------| | type | String | 行类型,当前仅 APPROVED_ADVANCE(已审批预支) | | typeName | String | 行类型中文名(字典 settlement_report_line_type) | | advanceId | Long(String) | 预支单 ID,序列化为字符串 | | payeeStaffId | Long(String) | 借款对象人员安排 ID,序列化为字符串 | | payeeName | String | 借款对象姓名 | | payeeRole | String | 借款对象角色(如 DRIVER) | | payeeRoleName | String | 借款对象角色中文名(字典 staff_role) | | advanceType | String | 预支类型(如 ACCOMMODATION_DEPOSIT) | | advanceTypeName | String | 预支类型中文名(字典 advance_type) | | amount | BigDecimal | 预支金额,保留两位小数 | | purpose | String | 预支用途 | | voucherUrl | String | 凭证 URL | | status | String | 预支状态:SUBMITTED / APPROVED / REJECTED | | statusText | String | 预支状态中文名(枚举 label) | | submittedAt | String | 提交时间,格式 yyyy-MM-dd HH:mm:ss | | approvedAt | String | 审批时间,格式 yyyy-MM-dd HH:mm:ss | | approvedBy | String | 审批人姓名 | ### 5.2 group 出参(Result,最终结构:全部平铺顶层) | 字段 | 类型 | 说明 | |------|------|------| | id | Long(String) | 单团核算记录 ID,序列化为字符串 | | orderId | Long(String) | 订单 ID,序列化为字符串 | | orderNo | String | 订单号 | | teamNo | String | 团号(订金未支付为 null) | | productName | String | 产品名 | | productType | String | 产品类型枚举(如 CORE) | | productTypeName | String | 产品类型中文名(字典 product_type 回填) | | customerName | String | 客户姓名 | | departDate | String | 出团日期,格式 yyyy-MM-dd | | returnDate | String | 返回日期,格式 yyyy-MM-dd | | consultantName | String | 定制师姓名 | | travelerComposition | String | 出行人构成(数量为 0 的档不显示,全空为 null) | | baseOrderAmount | BigDecimal | 订单应收(基础订单金额) | | otherIncomeAmount | BigDecimal | 其他收入合计 | | discountAmount | BigDecimal | 优惠金额(负数) | | adjustedReceivableAmount | BigDecimal | 调整后应收 = baseOrderAmount + otherIncomeAmount + discountAmount | | paidAmount | BigDecimal | 已收金额 | | actualRefundedAmount | BigDecimal | 实际退款金额 | | netRevenueAmount | BigDecimal | 净收入 = adjustedReceivableAmount - actualRefundedAmount | | netReceivedAmount | BigDecimal | 净实收 = paidAmount - actualRefundedAmount | | outstandingAmount | BigDecimal | 未收尾款(口径同 financial-overview) | | hotelCost | BigDecimal | 住宿成本 | | ticketCost | BigDecimal | 门票成本 | | mealCost | BigDecimal | 餐食成本 | | vehicleCost | BigDecimal | 车辆成本 | | guideCost | BigDecimal | 导游成本 | | photographerCost | BigDecimal | 摄影成本 | | otherExpenseCost | BigDecimal | 其他支出成本 | | insurancePremium | BigDecimal | 保险保费 | | totalCost | BigDecimal | 总成本 | | paidCost | BigDecimal | 已付成本 | | unpaidCost | BigDecimal | 未付成本 | | grossProfit | BigDecimal | 毛利 = netRevenueAmount - totalCost | | grossProfitRate | BigDecimal | 毛利率(小数,netRevenueAmount=0 时填 0) | | travelerCount | Integer | 出行人总数 | | perCapitaRevenue | BigDecimal | 人均收入(travelerCount=0 时为 null) | | perCapitaCost | BigDecimal | 人均成本 | | perCapitaProfit | BigDecimal | 人均毛利 | | incomeLines | Array | 收入行,固定 4 行(见下) | | costCategories | Array | 成本分类行,固定 8 行(见下) | **incomeLines 元素**(固定 4 行): | 字段 | 类型 | 说明 | |------|------|------| | type | String | BASE_ORDER(订单应收)/ OTHER_INCOME(其他收入)/ DISCOUNT(优惠,负数)/ ACTUAL_REFUND(实际退款,负数) | | typeName | String | 行类型中文名(字典 settlement_report_line_type) | | amount | BigDecimal | 金额,保留两位小数;DISCOUNT / ACTUAL_REFUND 为负数 | | details | Array | 收入逐项明细;无逐项时为空数组 [] | **costCategories 元素**(固定 8 行): | 字段 | 类型 | 说明 | |------|------|------| | category | String | HOTEL / TICKET / MEAL / VEHICLE / GUIDE / PHOTOGRAPHER / OTHER_EXPENSE / INSURANCE | | categoryName | String | 费用类别中文名(字典 settlement_category) | | amount | BigDecimal | 金额,保留两位小数 | | lines | Array | 成本逐项明细(分类专属行结构,前端按 category 判别窄化);无逐项时为空数组 [] | ### 5.3 finalize 出参(Result,本次仅 warnings 变化) | 字段 | 类型 | 说明 | |------|------|------| | summaryId | Long(String) | 新写入的 settlement_summary 主键,序列化为字符串 | | finalSnapshotId | Long(String) | 核单终态快照 ID,序列化为字符串 | | finalSnapshotVersionNo | Integer | 核单终态快照版本号 | | finalSnapshotStatus | String | 核单终态快照状态,固定 FINALIZED | | orderId | Long(String) | 订单 ID,序列化为字符串 | | settledAt | String | 核单完成时间,格式 yyyy-MM-ddTHH:mm:ss | | totalAmount | String | 订单总金额快照(BigDecimal 序列化为字符串,下同) | | paidAmount | String | 已付金额快照 | | balanceAmount | String | 尾款金额快照 | | roomCost | String | 住宿实际成本 | | ticketCost | String | 门票实际成本 | | staffCost | String | 人员费用实际成本 | | subsidyCost | String | 补助实际成本 | | mealCost | String | 餐食实际成本 | | vehicleCost | String | 车辆基础服务总车费 | | otherExpenseCost | String | 其他支出实际成本 | | insurancePremium | String | 保险实际保费(未出单/已撤单 = 0) | | totalActualCost | String | 总实际成本 | | driverTransferAmount | String | 给司机/主报账人转回金额(仅计 CASH_PAID,不含保险) | | profitAmount | String | 公司毛利 = totalAmount - totalActualCost | | profitRate | Number | 毛利率(小数,totalAmount=0 时填 0) | | orderStatusAfter | String | 结算后订单状态中文文案,如「待财务复核」 | | mqTriggered | Boolean | 当前版本固定 false;完成核单不发布结算 MQ | | warnings | Array | **本次变更**:软预警对象数组(原来为字符串数组);无预警时为空数组 [] | **WarningItemVO 字段表**: | 字段 | 类型 | 说明 | |------|------|------| | settlementId | Long(String) | 触发预警的核单明细行 id(settlement_hotel / settlement_ticket 主键),序列化为字符串;前端据此锚定/跳转具体明细行 | | category | String | 核单分类枚举名,当前仅 HOTEL(住宿)/ TICKET(门票/游玩项目)两类会触发预警 | | dayNumber | Integer | 行程第几天 | | message | String | 预警文案,如「住宿 D2 现付缺凭证」 | ### 5.4 summary 出参(Result,本次新增 settled) | 字段 | 类型 | 说明 | |------|------|------| | settled | Boolean | **本次新增**:是否已核单。summary 快照存在 = true;未核单空壳 = false | | id | Long | settlement_summary 主键;未核单时为 null | | orderId | Long | 订单 ID;空壳时也有值 | | totalAmount | String | 订单总金额快照(BigDecimal 序列化为字符串,下同) | | paidAmount | String | 已付金额快照 | | balanceAmount | String | 尾款金额快照 | | roomCost | String | 住宿实际成本 | | ticketCost | String | 门票实际成本 | | staffCost | String | 人员费用实际成本 | | subsidyCost | String | 补助实际成本 | | mealCost | String | 餐食实际成本 | | vehicleCost | String | 车辆基础服务总车费 | | otherExpenseCost | String | 其他支出实际成本 | | insurancePremium | String | 保险实际保费 | | refundTotal | String | 返还合计(只读展示,不进利润公式) | | totalActualCost | String | 总实际成本 | | driverTransferAmount | String | 给司机/主报账人转回金额(仅计 CASH_PAID) | | profitAmount | String | 公司毛利 | | profitRate | Number | 毛利率(小数) | | settledBy | Long | 核单人 id;未核单时为 null | | settledByName | String | 核单人姓名快照 | | settledAt | String | 核单完成时间,格式 yyyy-MM-ddTHH:mm:ss | | remark | String | 核单备注 | | finalSnapshotId | Long(String) | 当前核单终态快照 ID;未完成核单或已重新打开时为 null | | finalSnapshotVersionNo | Integer | 当前核单终态快照版本号;无当前快照时为 null | | finalSnapshotStatus | String | 有当前快照时固定 FINALIZED,否则为 null | | finalizedAt | String | 当前核单终态快照完成时间;无当前快照时为 null | | finalizedByName | String | 当前核单终态快照操作人姓名;无当前快照时为 null | | advanceSummary | Object | 已审批通过的订单预支汇总;**恒下发对象,不是 null**(空壳时也有值),含 approvedAmount(String,默认 "0")+ records(Array,默认 []) | > 未核单空壳时:settled=false,仅 orderId 与 advanceSummary(默认空对象)有值,**其余字段全部为 null**。 ## 6. 枚举 / 数据字典 | 字段 | 来源 | 取值 | |------|------|------| | expenseLines[].category / costCategories[].category | settlement_category 字典 | HOTEL(住宿)/ TICKET(门票)/ MEAL(餐食)/ VEHICLE(车辆)/ GUIDE(导游)/ PHOTOGRAPHER(摄影)/ OTHER_EXPENSE(其他支出)/ INSURANCE(保险) | | expenseLines[].paymentMethod | settlement_payment_method 字典 | CASH_PAID(现金已付)/ COMPANY_PAID(公司直付)/ SIGNED(签单);报账支出行固定 CASH_PAID | | incomeLines[].type / advanceLines[].type | settlement_report_line_type 字典 | DRIVER_CASH_RECEIPT / APPROVED_ADVANCE / BASE_ORDER / OTHER_INCOME / DISCOUNT / ACTUAL_REFUND | | incomeLines[].channel(reimbursement) | PaymentChannelEnum 枚举 | DRIVER_CASH / BANK_TRANSFER / CONSULTANT_COLLECTION | | incomeLines[].payType(reimbursement) | PayType 枚举 | DEPOSIT(订金)/ FULL(全款)/ BALANCE(尾款) | | collectorRole / payeeRole / primaryReporterRole | staff_role 字典 | DRIVER(司机)/ GUIDE(导游)等 | | advanceLines[].advanceType | advance_type 字典 | 如 ACCOMMODATION_DEPOSIT(住宿押金)等 | | advanceLines[].status | AdvanceStatus 枚举 | SUBMITTED / APPROVED / REJECTED | | productType | product_type 字典 | 如 CORE(核心产品)等 | | warnings[].category | SettlementCategory 枚举 | 全量同 settlement_category;**当前预警只会出现 HOTEL / TICKET** | | finalSnapshotStatus | 快照状态枚举 | FINALIZED(已定稿) | 本次无枚举值增删;category 只是从隐含在预警文案里变成显式字段。 ## 7. 错误码 ### 7.1 reimbursement / group(两报表) | code | message | 触发场景 | |------|---------|----------| | 400 | 订单 ID 必须大于 0 | orderId 路径参数校验失败 | | 403 | 无权限访问 | 房务角色(HOUSE)JWT 调用 | | 581007 | 订单不存在 | orderId 查不到订单 | ### 7.2 finalize | code | message | 触发场景 | |------|---------|----------| | 581007 | 订单不存在 | orderId 查不到订单 | | 584081 | 对账数据不一致:已付金额与支付流水和线下收款合计不符,禁止带病结算 | paid_amount 与实际入账总额不符 | | 584085 | 存在辅助人员结算未完成,请全部结算后提交 | 任一非主报账司机费用行未结算完成或缺转账凭证号 | | 584321 | 当前订单缺少核单终态快照,请重新完成核单 | 缺终态快照(含重复提交场景) | | 400 | 订单 ID 必须大于 0 | orderId 路径参数校验失败 | | 403 | 无权限访问 | 房务角色(HOUSE)JWT 调用 | > finalize 还有人员费用 / 其他收入未就绪等前置门禁错误码(584xxx 段),非本次变更,按响应 message 直接提示即可。 ### 7.3 summary 只读接口,无业务错误码;orderId 查不到快照时返回 200 + 空壳对象(settled=false),**不会报「订单不存在」**。 ## 8. 示例 ### 8.1 典型成功 **reimbursement**(最终结构:抬头在 baseInfo 内,无 orderHeader 块): GET /v3/admin/order/2087088947225038849/settlement/reports/reimbursement ```json { "code": 200, "message": "成功", "data": { "baseInfo": { "id": "6001", "orderId": "2087088947225038849", "orderNo": "HL20260730001", "teamNo": "T20260730001", "productName": "呼伦贝尔草原 5 日游", "productType": "CORE", "productTypeName": "核心产品", "customerName": "张三", "departDate": "2026-07-30", "returnDate": "2026-08-03", "consultantName": "李四", "travelerCount": 5, "travelerComposition": "2大 2儿童 1幼童", "primaryReporterId": "7001", "primaryReporterName": "司机甲", "primaryReporterRole": "DRIVER", "primaryReporterCollectedAmount": 2000.00, "approvedAdvanceAmount": 500.00, "reportablePaidCostAmount": 1800.00, "reporterNetAmount": 700.00, "outstandingAmount": 0.00 }, "incomeLines": [ { "type": "DRIVER_CASH_RECEIPT", "typeName": "司机现金收款", "receiptId": "8001", "amount": 2000.00, "channel": "DRIVER_CASH", "channelName": "报账人收款", "payType": "BALANCE", "payTypeName": "尾款", "collectorStaffId": "7001", "collectorName": "司机甲", "collectorRole": "DRIVER", "collectorRoleName": "司机", "receivedAt": "2026-07-30 18:20:30", "remark": "尾款现金" } ], "expenseLines": [ { "category": "HOTEL", "categoryName": "住宿", "itemName": "草原明珠大酒店-标间", "unitPrice": 400.00, "quantity": 3, "amount": 1200.00, "reimburseAmount": 1200.00, "paymentMethod": "CASH_PAID", "paymentMethodName": "现金已付", "date": "2026-07-30", "remark": "含早", "voucherUrls": ["https://oss/v1.jpg"] } ], "advanceLines": [ { "type": "APPROVED_ADVANCE", "typeName": "已审批预支", "advanceId": "5001", "payeeStaffId": "7001", "payeeName": "司机甲", "payeeRole": "DRIVER", "payeeRoleName": "司机", "advanceType": "ACCOMMODATION_DEPOSIT", "advanceTypeName": "住宿押金", "amount": 500.00, "purpose": "酒店押金", "voucherUrl": "https://oss/advance-v1.jpg", "status": "APPROVED", "statusText": "已通过", "submittedAt": "2026-07-28 10:00:00", "approvedAt": "2026-07-28 12:00:00", "approvedBy": "财务丙" } ] }, "success": true } ``` **group**(最终结构:抬头平铺顶层,无 orderHeader 块): GET /v3/admin/order/2087088947225038849/settlement/reports/group ```json { "code": 200, "message": "成功", "data": { "id": "6002", "orderId": "2087088947225038849", "orderNo": "HL20260730001", "teamNo": "T20260730001", "productName": "呼伦贝尔草原 5 日游", "productType": "CORE", "productTypeName": "核心产品", "customerName": "张三", "departDate": "2026-07-30", "returnDate": "2026-08-03", "consultantName": "李四", "travelerComposition": "2大 2儿童 1幼童", "baseOrderAmount": 12800.00, "otherIncomeAmount": 600.00, "discountAmount": -200.00, "adjustedReceivableAmount": 13200.00, "paidAmount": 13200.00, "actualRefundedAmount": 0.00, "netRevenueAmount": 13200.00, "netReceivedAmount": 13200.00, "outstandingAmount": 0.00, "hotelCost": 3600.00, "ticketCost": 1500.00, "mealCost": 900.00, "vehicleCost": 2500.00, "guideCost": 800.00, "photographerCost": 0.00, "otherExpenseCost": 300.00, "insurancePremium": 180.00, "totalCost": 9780.00, "paidCost": 9780.00, "unpaidCost": 0.00, "grossProfit": 3420.00, "grossProfitRate": 0.2591, "travelerCount": 5, "perCapitaRevenue": 2640.00, "perCapitaCost": 1956.00, "perCapitaProfit": 684.00, "incomeLines": [ { "type": "BASE_ORDER", "typeName": "订单应收", "amount": 12800.00, "details": [] }, { "type": "OTHER_INCOME", "typeName": "其他收入", "amount": 600.00, "details": [] }, { "type": "DISCOUNT", "typeName": "优惠", "amount": -200.00, "details": [] }, { "type": "ACTUAL_REFUND", "typeName": "实际退款", "amount": 0.00, "details": [] } ], "costCategories": [ { "category": "HOTEL", "categoryName": "住宿", "amount": 3600.00, "lines": [] }, { "category": "TICKET", "categoryName": "门票", "amount": 1500.00, "lines": [] }, { "category": "MEAL", "categoryName": "餐食", "amount": 900.00, "lines": [] }, { "category": "VEHICLE", "categoryName": "车辆", "amount": 2500.00, "lines": [] }, { "category": "GUIDE", "categoryName": "导游", "amount": 800.00, "lines": [] }, { "category": "PHOTOGRAPHER", "categoryName": "摄影", "amount": 0.00, "lines": [] }, { "category": "OTHER_EXPENSE", "categoryName": "其他支出", "amount": 300.00, "lines": [] }, { "category": "INSURANCE", "categoryName": "保险", "amount": 180.00, "lines": [] } ] }, "success": true } ``` ### 8.2 边界情况 **边界 1:订金未支付、无团号**——reimbursement 的 baseInfo 因 NON_NULL 不输出 teamNo 键;group 顶层 teamNo 为 null。前端两处都要兜底: ```json "baseInfo": { "orderId": "2087088947225038850", "orderNo": "HL20260801002", "productName": "库布其沙漠 2 日游", "outstandingAmount": 3200.00 } ``` **边界 2:无任何明细行**——incomeLines / expenseLines / advanceLines 均为空数组 [],不是 null: ```json { "incomeLines": [], "expenseLines": [], "advanceLines": [] } ``` **边界 3:finalize 无软预警 + summary 查未核单订单**——warnings 为空数组 [];summary 返回空壳(settled=false,除 orderId / advanceSummary 外全 null): ```json { "code": 200, "data": { "settled": false, "id": null, "orderId": "2087088947225038850", "totalAmount": null, "advanceSummary": { "approvedAmount": "0", "records": [] } }, "success": true } ``` ### 8.3 业务失败 **finalize 辅助人员结算未完成**: ```json { "code": 584085, "message": "存在辅助人员结算未完成,请全部结算后提交", "success": false } ``` **房务角色访问报表被拦**(House 角色 JWT 调 reimbursement / group / finalize): ```json { "code": 403, "message": "无权限访问", "success": false } ``` **订单不存在**: ```json { "code": 581007, "message": "订单不存在", "success": false } ``` ## 9. 业务边界 **适用**: - reimbursement:订单核单页查看主报账人结算报表;支出行只含报账人垫付(CASH_PAID)支出,签单/公司直付不进本列表 - group:整团收入成本毛利核算;incomeLines 固定 4 行、costCategories 固定 8 行恒下发(金额为 0 的行也在) - finalize:核单明细已逐步保存完成、核对无误后提交 - summary:随时读取汇总;未核单订单也可调(返回空壳),前端据此展示「尚未核单」占位 **不适用**: - reimbursement:想看待收整单尾款时不要再用已删的 primaryReporterDueAmount(主报账人维度),改读 outstandingAmount(整单维度) - finalize:明细未保存完整 / 辅助人员结算未完成 / 对账不一致的订单(被 584xxx 门禁拦截,见 §7) - summary:想看明细行级数据时不要用它,走各分类明细接口;本接口只有汇总快照 **特殊边界**: - 报表的「报表状态 / 生成人 / 确认人」审计信息**不再下发**(DB 仍保留),前端如曾展示「已生成/已确认」徽标需删除该 UI - warnings 是**软预警**,不阻塞 finalize 提交;有预警也照常返回 200 完成核单 - 预警仅覆盖 CASH_PAID(现付)且 voucher_urls 为空的住宿 / 门票明细行;签单、公司直付等其他支付方式不产生预警 - summary 空壳的 advanceSummary 恒为默认对象(approvedAmount="0"、records=[]),**不是 null**,不能拿它当判空依据 ## 10. 修改前后对比 > 「修改前」= 两次变更前的旧结构(含 orderHeader 子块 + 审计字段);「修改后」= 本份最终态。 ### 10.1 字段级对比 | 接口 | 字段 | 修改前 | 修改后 | |------|------|--------|--------| | reimbursement | 顶层 orderHeader | Object 子块(11 个抬头字段) | **删除**,11 字段拍平进 baseInfo | | reimbursement | baseInfo.reportStatus / reportVersion | 有 | **删除** | | reimbursement | baseInfo.generatedBy / generatedByName / generatedAt | 有 | **删除** | | reimbursement | baseInfo.confirmedBy / confirmedByName / confirmedAt | 有 | **删除** | | reimbursement | baseInfo.transferAmount | 有(净额绝对值) | **删除**,前端用 reporterNetAmount 绝对值自取 | | reimbursement | baseInfo.primaryReporterDueAmount | 有(主报账人维度应收尾款) | **删除**,由 outstandingAmount 替代 | | reimbursement | baseInfo.outstandingAmount | 无 | **新增**:整单未收尾款(口径同 financial-overview) | | group | 顶层 orderHeader | Object 子块(10 个抬头字段) | **删除**,10 字段平铺到顶层 | | group | reportStatus | 有 | **删除** | | group | generatedBy / generatedByName / generatedAt | 有 | **删除** | | group | confirmedBy / confirmedByName / confirmedAt | 有 | **删除** | | finalize | warnings | List,元素如「住宿 D2 现付缺凭证」字符串 | List,元素为对象 { settlementId, category, dayNumber, message } | | summary | settled | 无此字段 | 新增 Boolean:已核单 = true,未核单空壳 = false | ### 10.2 行为级对比 | 场景 | 修改前 | 修改后 | |------|--------|--------| | 读订单抬头(两报表) | 读 orderHeader.orderNo 等 | 读 baseInfo.orderNo(reimbursement)/ 顶层 orderNo(group) | | 展示「已生成/已确认」徽标 | 读 reportStatus / confirmedAt | **字段已删除,恒取不到值**,UI 必须删除该徽标 | | 展示转账金额/方向 | 读 transferAmount / 推导方向 | 用 reporterNetAmount 正负推导方向,金额取绝对值 | | 展示应收尾款(reimbursement) | 读 primaryReporterDueAmount(主报账人维度) | 读 outstandingAmount(整单维度,与财务总览一致) | | 同日多条同类缺凭证预警 | 多条相同文案,无法区分哪一行 | 每条带独立 settlementId,可定位/跳转具体明细行 | | 前端渲染 finalize 预警 | 直接渲染字符串 | 必须读 warnings[].message | | 前端判「是否已核单」(summary) | 判 data.id == null(不可靠) | 判 data.settled === false | ## 11. 影响评估 / 回滚 ### 11.1 影响评估 - **破坏兼容性**:⚠️ 破坏性。删 orderHeader 块 + 删 baseInfo/顶层多字段 + finalize warnings 结构变化,按旧结构取值的前端代码会拿到 undefined / 渲染异常。 - **前端必须同步上线**:是。前端需要: 1. reimbursement 抬头取值从 orderHeader.xxx 改 baseInfo.xxx;group 抬头取值从 orderHeader.xxx 改顶层 xxx 2. 删除「报表状态 / 生成人 / 确认人」相关 UI(字段已删,恒 undefined) 3. 转账金额改取 reporterNetAmount 绝对值,方向按 reporterNetAmount 正负推导 4. 应收尾款改读 outstandingAmount(注意口径已从主报账人维度变整单维度,展示文案如有「主报账人应收」字样需同步改) 5. finalize warnings 渲染改读 item.message,可用 settlementId + category 做行锚定 6. summary 判「是否已核单」改读 settled 字段 - **后端兼容**:零 DDL、无数据迁移;DB 的 report_status / 版本 / 审计列保留,finalize 幂等与反确认逻辑不受影响。 - **本份覆盖旧份**:此前 2026-08-12 推送的 12_5876_核单finalize-summary出参整改-修改接口-管理后台.md 内容已并入本份;两报表结构**以本份为准**。 ### 11.2 回滚方案 - 后端回滚 = 依次 revert PR #5920 的 merge commit(b912ff4876)与 PR #5881 的 merge commit(125bb9c96e),重启 hl-order-service-v3。只 revert b912ff4876 则回到 #5876 后的中间态(orderHeader 块恢复 + 审计字段恢复,warnings/settled 保留)。 - 前端回滚 = 切回旧版前端包。注意新旧前后端要配套:新前端 + 旧后端时 baseInfo.orderNo / outstandingAmount / settled 取不到值。 - 零 DDL,回滚无残留风险。 ## 12. 注意事项 1. **所有 Long 主键字段序列化为 JSON 字符串**(id / orderId / primaryReporterId / receiptId / collectorStaffId / advanceId / payeeStaffId / settlementId 等,防 JS 精度丢失),前端按 string 处理,不要 Number() 转换。 2. reimbursement 的 baseInfo / incomeLines / expenseLines / advanceLines 均带 @JsonInclude(NON_NULL):**null 字段不下发该 key**(不是下 null),前端读取必须兜底,不能假设 key 恒存在。 3. group 出参不做 NON_NULL 裁剪,teamNo 等可空字段会下发 null,注意与 reimbursement 的差异。 4. expenseLines 支出行**只含报账人垫付(CASH_PAID)支出**;签单 / 公司直付不在本列表,不要拿 expenseLines 合计去对整单成本(整单成本看 group 报表)。 5. group 的 incomeLines 固定 4 行、costCategories 固定 8 行恒下发(含金额 0 的行),前端按 type / category 取值渲染即可,不要按数组下标硬编码。 6. outstandingAmount 是**整单**未收尾款(与核单应收财务总览同口径),不是主报账人维度;两报表该字段口径一致。 7. warnings 无预警时返回空数组 [] 不是 null;category 当前只会出现 HOTEL / TICKET,做映射表建议按全量 settlement_category 枚举写,缺值兜底显示原 code。 8. summary 判空**只认 settled**:settled === false 即「尚未核单」,此时除 orderId / advanceSummary 外全部字段为 null,展示必须兜底。 9. finalize 是写操作且有前置门禁,失败按错误码 message 提示,**不要静默重试**;重复提交会被快照类错误码拦截。 10. finalize / summary 的金额类字段序列化为**字符串**;两报表(reimbursement / group)的金额类字段为 JSON 数值(BigDecimal 两位小数),两类接口序列化风格不同,前端注意区分。 ## 13. 关联 / 联系人 - Issue #5876:https://git.1814.love:8443/wx/HL/issues/5876 - Issue #5916:https://git.1814.love:8443/wx/HL/issues/5916 - PR #5881:https://git.1814.love:8443/wx/HL/pulls/5881 - PR #5920:https://git.1814.love:8443/wx/HL/pulls/5920 - Commit(#5876):https://git.1814.love:8443/wx/HL/commit/125bb9c96e - Commit(#5916 最终态):https://git.1814.love:8443/wx/HL/commit/b912ff4876 - 服务:hl-order-service-v3(端口 8086) - 后端负责人:yst(腰苏图)