docs(changelog): 单团核算表出参重构——收入拍平逐项+成本7分类明细数组+保险独立子块(#5963)
所有检测均成功
changelog-filename-gate / validate (push) Successful in 2s

- 删 perCapita 人均三指标 + hotelCost 等 7 个成本分类小计 + costCategories 套层 + incomeLines[].details 嵌套
- incomeLines 拍平逐项行(4 type 各至少一行,无明细给占位行 amount=聚合值)
- 成本改 hotelLines 等 7 分类明细数组平铺顶层,空分类 []
- 新增 insuranceInfo 保险子块;insurancePremium 顶层保留且计入 totalCost
- PR #5966 已合并 dev-v3(c8cae25131),测试服部署 + 网关实调验证通过
这个提交包含在:
yaosutu 2026-08-14 17:45:57 +08:00
父节点 84c22ea77a
当前提交 34e50005f9

查看文件

@ -0,0 +1,728 @@
---
schema: "hl-changelog/v2"
ticket: "5963"
title: "单团核算表出参重构——收入拍平逐项+成本7分类明细数组+保险独立子块"
consumer: "admin"
change_type: "修改接口"
author: "yst"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "PR #5966 已合并 dev-v3merge commit c8cae25131。破坏性契约变化删除 perCapitaRevenue/perCapitaCost/perCapitaProfit 人均三指标、hotelCost 等 7 个成本分类小计、costCategories 套层与 incomeLines[].details 嵌套;incomeLines 拍平为逐项行4 type 各至少一行,无明细给占位行 amount=聚合值);成本改 hotelLines 等 7 个分类明细数组平铺顶层;新增 insuranceInfo 保险子块。仅出参变化,无入参/枚举值/DDL 变化。"
updated_at: "2026-08-14"
base: "dev-v3"
---
# 【修改接口·管理后台】单团核算表出参重构——收入拍平逐项 + 成本 7 分类明细数组 + 保险独立子块(#5963
## 1. 接口背景
单团核算表reports/group是管理后台订单核单页查看整团收入、成本、毛利全貌的只读报表抬头是订单/产品/客户信息,中部是收入与成本逐项明细,底部是净收入、总成本、毛利汇总。
此前出参有两层嵌套套层收入是「4 个汇总行 + 每行内嵌 details 明细数组」,成本是「costCategories 8 个分类行 + 每类内嵌 lines 明细数组」,外加 7 个成本分类小计字段和 3 个人均指标字段。前端渲染明细要钻两层嵌套,小计/人均字段与明细数组存在重复表达。
本次变更把出参**拍平**
- 收入 `incomeLines` 去 details 嵌套,逐项明细直接平铺为多行,每行带全列;
- 成本删 `costCategories` 套层,改 7 个分类明细数组(`hotelLines` 等)平铺顶层,空分类给 `[]`
- 删除 7 个成本分类小计字段(前端自算各分类数组 sum与人均三指标;
- 保险从成本分类中独立出来,新增 `insuranceInfo` 子块;顶层 `insurancePremium` 保留且仍计入 totalCost。
## 2. 变更清单
| # | 位置 | 变更 | 类型 |
|---|------|------|------|
| 1 | perCapitaRevenue / perCapitaCost / perCapitaProfit | **字段删除**:人均营收/人均成本/人均毛利不再透出(需要时前端用 汇总值 ÷ travelerCount 自算) | ⚠️ 破坏性删除 |
| 2 | hotelCost / ticketCost / mealCost / vehicleCost / guideCost / photographerCost / otherExpenseCost | **字段删除**7 个成本分类小计不再透出,前端改为自算对应分类明细数组的 amount 合计 | ⚠️ 破坏性删除 |
| 3 | costCategories | **结构删除**成本套层8 个分类行各内嵌 lines整体下线,改 7 个分类明细数组平铺顶层(见 #5);原 INSURANCE 分类行由 insuranceInfo 子块 + insurancePremium 替代 | ⚠️ 破坏性删除 |
| 4 | incomeLines[].details | **结构删除**:收入行内嵌明细数组下线,逐项明细直接拍平为 incomeLines 多行 | ⚠️ 破坏性删除 |
| 5 | hotelLines / ticketLines / mealLines / vehicleLines / guideLines / photographerLines / otherExpenseLines | **新增**7 个成本分类明细数组平铺顶层,行结构 = 原 costCategories[].lines 的分类专属行;无明细的分类给空数组 `[]` | ✨ 新增字段 |
| 6 | incomeLines[] 行结构 | **结构变化**:每行在 type/typeName/amount 基础上补齐 itemName/content/unitPrice/headCount/quantity/paymentMethod/paymentMethodName/source/sourceName/sourceType/sourceTypeName/remark/voucherUrls 全列;同一 type 多条平铺多行;某 type 无逐项明细时给一行占位行(仅 type/typeName/amount,amount=该 type 聚合值) | 🔧 结构变化 |
| 7 | insuranceInfo | **新增**保险子块insured/productName/bizType/bizTypeName/extPolicyNo/totalPremium;无有效保单时 insured=false、保单字段不下发、totalPremium=0 | ✨ 新增字段 |
| 8 | 抬头区 + 汇总金额字段baseOrderAmount … grossProfitRate / travelerCount / insurancePremium | **保留**,口径不变;insurancePremium 是原 8 个小计中唯一保留的,仍计入 totalCost | 📝 口径声明(数值零漂移) |
无入参变化、无新接口、无枚举值/字典变化、无 DDL。净收入 netRevenueAmount、总成本 totalCost、毛利 grossProfit/grossProfitRate 数值口径**零漂移**。
## 3. 接口详情
| 项 | 值 |
|---|---|
| 方法 + 路径 | GET /v3/admin/order/{orderId}/settlement/reports/group |
| 接口名 | 查询单团核算表 |
| 使用场景 | 管理后台订单核单页,查看整团收入/成本/毛利只读报表(含逐项明细) |
| 认证 | 管理后台 JWT/v3/admin/* 走网关鉴权) |
| 角色限制 | 房务角色HOUSE不可访问,调了会被 403 拦截 |
| 幂等性 | 只读查询,幂等 |
| 限流 | 走网关默认限流,无接口级特殊限流 |
## 4. 接口入参
### 4.1 路径参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| orderId | Long | 是 | 订单 ID,必须大于 0否则 400「订单 ID 必须大于 0」 |
### 4.2 请求体 / Query
无请求体、无 Query 参数。
## 5. 出参字段
返回 `Result<SettlementGroupReportRespVO>``data` 块结构:**抬头区字段 + 汇总金额字段 + incomeLines 收入逐项行数组 + 7 个成本分类明细数组 + insuranceInfo 保险子块**。costCategories 与 details 两层嵌套已彻底下线。
### 5.1 抬头区 + 汇总金额(保留字段,口径不变)
| 字段 | 类型 | 说明 |
|------|------|------|
| 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,如「1大」「2大 1儿童」 |
| travelerCount | Integer | 出行人总数 |
| baseOrderAmount | BigDecimal | 订单应收(基础团费),保留两位小数 |
| otherIncomeAmount | BigDecimal | 其他收入合计,保留两位小数 |
| discountAmount | BigDecimal | 优惠合计(正数表达,冲减应收),保留两位小数 |
| adjustedReceivableAmount | BigDecimal | 调整后应收 = baseOrderAmount + otherIncomeAmount discountAmount,保留两位小数 |
| paidAmount | BigDecimal | 已收金额合计(线上 SUCCESS + 线下手工收款),保留两位小数 |
| actualRefundedAmount | BigDecimal | 实际退款合计,保留两位小数 |
| netRevenueAmount | BigDecimal | 净收入 = 调整后应收 实际退款 = incomeLines 各行 amount 合计,保留两位小数 |
| netReceivedAmount | BigDecimal | 净实收 = 已收 实际退款,保留两位小数 |
| outstandingAmount | BigDecimal | 未收金额 = 调整后应收 已收(负截 0,保留两位小数 |
| ~~hotelCost 等 7 个分类小计~~ | — | **已删除**(见 §10 |
| insurancePremium | BigDecimal | 保费合计(原 8 个小计中唯一保留),计入 totalCost,保留两位小数 |
| totalCost | BigDecimal | 总成本 = 7 个成本分类明细数组 amount 合计 + insurancePremium,保留两位小数 |
| paidCost | BigDecimal | 已付成本,保留两位小数 |
| unpaidCost | BigDecimal | 未付成本,保留两位小数 |
| grossProfit | BigDecimal | 毛利 = netRevenueAmount totalCost,保留两位小数 |
| grossProfitRate | BigDecimal | 毛利率 = grossProfit ÷ netRevenueAmount,小数表达如 0.139100 = 13.91%),保留六位小数 |
| ~~perCapitaRevenue / perCapitaCost / perCapitaProfit~~ | — | **已删除**(见 §10 |
### 5.2 incomeLines 收入逐项行(拍平结构,去 details 嵌套)
固定 4 个 typeBASE_ORDER 订单应收 / OTHER_INCOME 其他收入 / DISCOUNT 优惠(负值)/ ACTUAL_REFUND 实际退款(负值)。同一 type 有多条逐项明细时平铺为多行(用 type + itemName 区分);**某 type 无逐项明细时给一行占位行**(仅 type/typeName/amount,amount = 该 type 聚合值,可能为 0,保证 4 个 type 各至少一行、各行 amount 合计 = netRevenueAmount 不丢数。本数组**不含已收/未收维度**(已收看顶层 paidAmount/outstandingAmount
| 字段 | 类型 | 说明 |
|------|------|------|
| type | String | 行类型BASE_ORDER / OTHER_INCOME / DISCOUNT / ACTUAL_REFUND |
| typeName | String | 行类型中文名(字典 settlement_report_line_type |
| itemName | String | 项目名(如 订单应收/增费项目名/优惠名称/退款来源);占位行不下发 |
| content | String | 内容说明(如 规格/退款原因);无则不下发 |
| source | String | 人工返还来源(仅 ACTUAL_REFUND 人工返还行透出DRIVER_ONSITE / COMPANY_COMPENSATION |
| sourceName | String | 人工返还来源中文名(字典 settlement_refund_source,缺值回退硬编码 |
| unitPrice | BigDecimal | 单价,保留两位小数;无单价概念时不下发 |
| headCount | Integer | 人数;无人数概念时不下发 |
| quantity | BigDecimal | 数量;无数量概念时不下发 |
| amount | BigDecimal | 金额,保留两位小数;DISCOUNT / ACTUAL_REFUND 行为**负数**;占位行为该 type 聚合值 |
| paymentMethod | String | 付款方式CASH_PAID / COMPANY_PAID / SIGNED;无则不下发 |
| paymentMethodName | String | 付款方式中文名(字典 settlement_payment_method,缺值回退硬编码 |
| sourceType | String | 来源MANUAL / HOUSE_ASSIGNMENT / SCENIC_ASSIGNMENT / ACTIVITY_ASSIGNMENT / MEAL_ASSIGNMENT / FLEET / STAFF_ASSIGNMENT / ORDER_SURCHARGE / SYSTEM;无则不下发 |
| sourceTypeName | String | 来源中文名SettlementDetailSourceType 枚举 label |
| remark | String | 备注;无则不下发 |
| voucherUrls | String[] | 凭证 URL 数组;无凭证时不下发 |
### 5.3 成本 7 个分类明细数组(平铺顶层,空分类给 `[]`
7 个数组:**hotelLines住宿/ ticketLines门票/ mealLines餐食/ vehicleLines用车/ guideLines导游/ photographerLines摄影/ otherExpenseLines其他支出**。无明细的分类返回空数组 `[]`(不是不下发)。各分类小计由前端自算数组内 amount 合计;7 分类合计 + insurancePremium = totalCost。
公共列(每个分类行都有,语义相同,下表不再重复):`amount`(核算金额/实际成本,BigDecimal 两位小数,必填)、`paymentMethod`/`paymentMethodName`(付款方式 + 中文名)、`sourceType`/`sourceTypeName`(来源 + 中文名)、`remark`(备注)、`voucherUrls`(凭证 URL 数组)。
#### 5.3.1 hotelLines 住宿行
| 字段 | 类型 | 说明 |
|------|------|------|
| stayDate | String | 入住日期,yyyy-MM-dd |
| hotelName | String | 酒店名称 |
| roomTypeName | String | 房型名称 |
| roomCount | Integer | 房间数 |
| unitPrice | BigDecimal | 单价,两位小数 |
| plannedCost | BigDecimal | 计划成本,两位小数 |
#### 5.3.2 ticketLines 门票行
| 字段 | 类型 | 说明 |
|------|------|------|
| dayDate | String | 游玩日期,yyyy-MM-dd |
| scenicName | String | 景区/项目名称 |
| specName | String | 规格名称(如 成人票) |
| ticketCount | Integer | 票数 |
| ticketUnitPrice | BigDecimal | 门票单价,两位小数 |
| sellPrice | BigDecimal | 销售价,两位小数;无则不下发 |
| plannedCost | BigDecimal | 计划成本,两位小数 |
#### 5.3.3 mealLines 餐食行
| 字段 | 类型 | 说明 |
|------|------|------|
| mealDate | String | 用餐日期,yyyy-MM-dd |
| mealName | String | 餐食名称 |
| mealType | String | 餐食类型BREAKFAST / LUNCH / DINNER / SELF |
| mealTypeName | String | 餐食类型中文名(字典 meal_type |
| quantity | Integer | 份数 |
| unitPrice | BigDecimal | 单价,两位小数 |
#### 5.3.4 vehicleLines 用车行VEHICLE_FEE 逐日车费行 + EXPENSE 车辆费用行的稀疏并集,每行仅本族字段非 null
| 字段 | 类型 | 说明 |
|------|------|------|
| serviceDate | String | [VEHICLE_FEE] 服务日期,yyyy-MM-dd |
| startDate | String | [VEHICLE_FEE] 服务开始日期,yyyy-MM-dd |
| endDate | String | [VEHICLE_FEE] 服务结束日期,yyyy-MM-dd |
| vehiclePlate | String | [VEHICLE_FEE] 车牌号 |
| vehicleModelName | String | [VEHICLE_FEE] 车型名称 |
| driverName | String | [VEHICLE_FEE] 司机姓名 |
| dailyPrice | BigDecimal | [VEHICLE_FEE] 日单价(核算单价列),两位小数 |
| paymentTypeName | String | [VEHICLE_FEE] 车务付款类型名称(如 现金已付) |
| expenseType | String | [EXPENSE] 车辆费用类型FUEL / TOLL / PARKING / RENTAL / MAINTENANCE |
| expenseTypeName | String | [EXPENSE] 车辆费用类型中文名(字典 expense_type |
| projectName | String | [EXPENSE] 项目名称(如 全程油费) |
| expenseDate | String | [EXPENSE] 费用发生日期,yyyy-MM-dd |
说明vehicleLines **只含已确认行**(与车辆成本口径一致);无单价/数量概念,quantity 恒不下发。
#### 5.3.5 guideLines / photographerLines 人员费用行(两数组同一行结构)
| 字段 | 类型 | 说明 |
|------|------|------|
| serviceDate | String | 服务日期,yyyy-MM-dd;无服务日时不下发 |
| name | String | 人员姓名 |
| serviceType | String | 服务类型GUIDE / PHOTOGRAPHER |
| serviceTypeName | String | 服务类型中文名(字典 staff_role |
说明:人员费用无单价/数量概念,均不下发。
#### 5.3.6 otherExpenseLines 其他支出行EXPENSE 其他费用行 + SUBSIDY 补贴行的稀疏并集)
| 字段 | 类型 | 说明 |
|------|------|------|
| expenseDate | String | 费用发生日期,yyyy-MM-dd |
| projectName | String | 项目名称(如 全程矿泉水) |
| expenseType | String | [EXPENSE] 其他费用类型OTHER |
| expenseTypeName | String | [EXPENSE] 其他费用类型中文名(字典 expense_type,如 其他) |
| subsidyType | String | [SUBSIDY] 补贴类型PHONE / OVERTIME |
| subsidyTypeName | String | [SUBSIDY] 补贴类型中文名(字典 subsidy_type,如 话补) |
### 5.4 insuranceInfo 保险子块(新增)
| 字段 | 类型 | 说明 |
|------|------|------|
| insured | Boolean | 是否已投保(有有效保单),恒下发 |
| productName | String | 保险产品名称;无有效保单时不下发 |
| bizType | String | 业务类型ORDER / DRIVER;无有效保单时不下发 |
| bizTypeName | String | 业务类型中文名(字典 insurance_biz_type,缺值回退硬编码ORDER 订单险 / DRIVER 司机险) |
| extPolicyNo | String | 外部保单号;无有效保单时不下发 |
| totalPremium | BigDecimal | 保费合计(与顶层 insurancePremium 同口径,计入 totalCost,两位小数,恒下发无保单为 0 |
## 6. 枚举 / 数据字典
本次无新增/变更枚举值,以下为出参涉及的既有枚举与字典(值 + 中文名):
| 字典/枚举 | 取值 → 中文名 | 用于字段 |
|-----------|--------------|----------|
| settlement_report_line_type字典 | BASE_ORDER → 订单应收;OTHER_INCOME → 其他收入;DISCOUNT → 优惠;ACTUAL_REFUND → 实际退款 | incomeLines[].typeName |
| settlement_payment_method字典 | CASH_PAID → 现金已付;COMPANY_PAID → 公司支付;SIGNED → 签单 | 各行 paymentMethodName |
| settlement_refund_source字典 | DRIVER_ONSITE → 司机现场返还;COMPANY_COMPENSATION → 公司赔付 | incomeLines[].sourceName仅 ACTUAL_REFUND 人工返还行) |
| SettlementDetailSourceType枚举 | MANUAL → 手工;HOUSE_ASSIGNMENT → 配房结果;SCENIC_ASSIGNMENT → 景区;ACTIVITY_ASSIGNMENT → 游玩项目;MEAL_ASSIGNMENT → 餐饮安排;FLEET → 车务;STAFF_ASSIGNMENT → 人员安排;ORDER_SURCHARGE → 订单增费;SYSTEM → 系统 | 各行 sourceTypeName |
| meal_type字典 | BREAKFAST → 早餐;LUNCH → 午餐;DINNER → 晚餐;SELF → 自理 | mealLines[].mealTypeName |
| staff_role字典 | GUIDE → 导游;PHOTOGRAPHER → 摄影师 | guideLines/photographerLines[].serviceTypeName |
| expense_type字典 | FUEL → 油费;TOLL → 过路费;PARKING → 停车费;RENTAL → 租车费;MAINTENANCE → 维保费;OTHER → 其他 | vehicleLines[].expenseTypeName / otherExpenseLines[].expenseTypeName |
| subsidy_type字典 | PHONE → 话补;OVERTIME → 加班补贴 | otherExpenseLines[].subsidyTypeName |
| insurance_biz_type字典 | ORDER → 订单险;DRIVER → 司机险 | insuranceInfo.bizTypeName |
原 costCategories 用的 settlement_category 字典HOTEL/TICKET/MEAL/VEHICLE/GUIDE/PHOTOGRAPHER/OTHER_EXPENSE/INSURANCE 8 值)随套层下线**不再出现于本接口出参**;7 个分类改由数组名hotelLines 等)直接判别,无需 category 字段窄化。
## 7. 错误码
本次无新增错误码,沿用既有:
| code | message | 触发场景 |
|------|---------|----------|
| 400 | 订单 ID 必须大于 0 | orderId 路径参数校验失败 |
| 403 | 无权限访问 | 房务角色HOUSEJWT 调用 |
| 581007 | 订单不存在 | orderId 查不到订单 |
## 8. 示例
### 8.1 典型成功(含住宿/门票/车辆多分类成本 + 已投保的真实单)
以下为部署后网关实调的真实返回(订单 2087487607792463873,2026-08-14 测试服实测3 天行程,住宿 2 晚 × 320、门票 6 项、用车 3 天 × 1080.50,已投保订单险 88.00;无增费/优惠/退款,incomeLines 后 3 行为占位行。
GET /v3/admin/order/2087487607792463873/settlement/reports/group
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "2087487623802122242",
"orderId": "2087487607792463873",
"orderNo": "HL20260812183323909",
"teamNo": "26-6276",
"productName": "测试核心产品-多档-固定订金",
"productType": "CORE",
"productTypeName": "核心产品",
"customerName": "沈梦瑶",
"departDate": "2026-08-28",
"returnDate": "2026-08-30",
"consultantName": "dzs_yst",
"travelerComposition": "1大",
"baseOrderAmount": 5000.0,
"otherIncomeAmount": 0,
"discountAmount": 0,
"adjustedReceivableAmount": 5000.0,
"paidAmount": 5000.0,
"actualRefundedAmount": 0,
"netRevenueAmount": 5000.0,
"netReceivedAmount": 5000.0,
"outstandingAmount": 0.0,
"insurancePremium": 88.0,
"totalCost": 4304.5,
"paidCost": 4304.5,
"unpaidCost": 0,
"grossProfit": 695.5,
"grossProfitRate": 0.1391,
"travelerCount": 1,
"incomeLines": [
{
"type": "BASE_ORDER",
"typeName": "订单应收",
"itemName": "订单应收",
"amount": 5000.0
},
{
"type": "OTHER_INCOME",
"typeName": "其他收入",
"amount": 0
},
{
"type": "DISCOUNT",
"typeName": "优惠",
"amount": 0
},
{
"type": "ACTUAL_REFUND",
"typeName": "实际退款",
"amount": 0
}
],
"hotelLines": [
{
"stayDate": "2026-08-28",
"hotelName": "呼伦贝尔香格里拉大酒店",
"roomTypeName": "大床房",
"roomCount": 1,
"unitPrice": 320.0,
"plannedCost": 320.0,
"amount": 320.0,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现金已付",
"sourceType": "HOUSE_ASSIGNMENT",
"sourceTypeName": "配房结果",
"remark": "裸HTTP派房D1"
},
{
"stayDate": "2026-08-29",
"hotelName": "呼伦贝尔香格里拉大酒店",
"roomTypeName": "大床房",
"roomCount": 1,
"unitPrice": 320.0,
"plannedCost": 320.0,
"amount": 320.0,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现金已付",
"sourceType": "HOUSE_ASSIGNMENT",
"sourceTypeName": "配房结果",
"remark": "裸HTTP派房D2"
}
],
"ticketLines": [
{
"dayDate": "2026-08-29",
"scenicName": "额尔古纳湿地漂流",
"specName": "成人票",
"ticketCount": 1,
"ticketUnitPrice": 119.0,
"plannedCost": 119.0,
"amount": 119.0,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现金已付",
"sourceType": "ACTIVITY_ASSIGNMENT",
"sourceTypeName": "游玩项目"
},
{
"dayDate": "2026-08-30",
"scenicName": "超长冰滑梯(不限次)",
"specName": "成人票",
"ticketCount": 1,
"ticketUnitPrice": 29.0,
"plannedCost": 29.0,
"amount": 29.0,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现金已付",
"sourceType": "ACTIVITY_ASSIGNMENT",
"sourceTypeName": "游玩项目"
},
{
"dayDate": "2026-08-29",
"scenicName": "呼和诺尔草原旅游区",
"specName": "成人票",
"ticketCount": 1,
"ticketUnitPrice": 59.0,
"plannedCost": 59.0,
"amount": 59.0,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现金已付",
"sourceType": "SCENIC_ASSIGNMENT",
"sourceTypeName": "景区"
},
{
"dayDate": "2026-08-29",
"scenicName": "套娃景区",
"specName": "成人票",
"ticketCount": 1,
"ticketUnitPrice": 99.0,
"plannedCost": 99.0,
"amount": 99.0,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现金已付",
"sourceType": "SCENIC_ASSIGNMENT",
"sourceTypeName": "景区"
},
{
"dayDate": "2026-08-30",
"scenicName": "恩和俄罗斯民族乡",
"specName": "成人票",
"ticketCount": 1,
"ticketUnitPrice": 29.0,
"plannedCost": 29.0,
"amount": 29.0,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现金已付",
"sourceType": "SCENIC_ASSIGNMENT",
"sourceTypeName": "景区"
},
{
"dayDate": "2026-08-30",
"scenicName": "蓝房子(乌苏浪子湖)",
"specName": "成人票",
"ticketCount": 1,
"ticketUnitPrice": 0.0,
"plannedCost": 0.0,
"amount": 0.0,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现金已付",
"sourceType": "SCENIC_ASSIGNMENT",
"sourceTypeName": "景区"
}
],
"mealLines": [],
"vehicleLines": [
{
"serviceDate": "2026-08-28",
"startDate": "2026-08-28",
"endDate": "2026-08-28",
"vehiclePlate": "蒙A-S6666",
"vehicleModelName": "丰田赛那",
"driverName": "菜单师傅",
"dailyPrice": 1080.5,
"paymentTypeName": "现金已付",
"amount": 1080.5,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现金已付",
"sourceType": "FLEET",
"sourceTypeName": "车务",
"voucherUrls": []
},
{
"serviceDate": "2026-08-29",
"startDate": "2026-08-29",
"endDate": "2026-08-29",
"vehiclePlate": "蒙A-S6666",
"vehicleModelName": "丰田赛那",
"driverName": "菜单师傅",
"dailyPrice": 1080.5,
"paymentTypeName": "现金已付",
"amount": 1080.5,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现金已付",
"sourceType": "FLEET",
"sourceTypeName": "车务",
"voucherUrls": []
},
{
"serviceDate": "2026-08-30",
"startDate": "2026-08-30",
"endDate": "2026-08-30",
"vehiclePlate": "蒙A-S6666",
"vehicleModelName": "丰田赛那",
"driverName": "菜单师傅",
"dailyPrice": 1080.5,
"paymentTypeName": "现金已付",
"amount": 1080.5,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现金已付",
"sourceType": "FLEET",
"sourceTypeName": "车务",
"voucherUrls": []
}
],
"guideLines": [],
"photographerLines": [],
"otherExpenseLines": [],
"insuranceInfo": {
"insured": true,
"productName": "“保游天下”平安自驾游综合保障计划",
"bizType": "ORDER",
"bizTypeName": "订单险",
"extPolicyNo": "MOCKPOL-2087487619058364419",
"totalPremium": 88.0
}
},
"traceId": null,
"success": true
}
```
勾稽验证以上真实数据incomeLines 合计 = 5000 + 0 + 0 + 0 = 5000.00 = netRevenueAmount ✅;hotelLines(640) + ticketLines(335) + mealLines(0) + vehicleLines(3241.50) + guideLines(0) + photographerLines(0) + otherExpenseLines(0) + insurancePremium(88) = 4304.50 = totalCost ✅;netRevenueAmount totalCost = 695.50 = grossProfit ✅。
### 8.2 边界情况(无逐项明细的 type 给占位行 amount=聚合值 + 空成本分类给 `[]`
场景:订单有 500 元其他收入(手工录入、无逐项明细来源)和 200 元优惠(无逐项明细),无退款;餐食/导游/摄影/其他支出均无明细。
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "2087487607792463899",
"orderNo": "HL20260814001",
"baseOrderAmount": 5000.00,
"otherIncomeAmount": 500.00,
"discountAmount": 200.00,
"adjustedReceivableAmount": 5300.00,
"paidAmount": 5300.00,
"actualRefundedAmount": 0,
"netRevenueAmount": 5300.00,
"netReceivedAmount": 5300.00,
"outstandingAmount": 0.00,
"insurancePremium": 0,
"totalCost": 640.00,
"paidCost": 640.00,
"unpaidCost": 0,
"grossProfit": 4660.00,
"grossProfitRate": 0.879245,
"travelerCount": 2,
"incomeLines": [
{ "type": "BASE_ORDER", "typeName": "订单应收", "itemName": "订单应收", "amount": 5000.00 },
{ "type": "OTHER_INCOME", "typeName": "其他收入", "amount": 500.00 },
{ "type": "DISCOUNT", "typeName": "优惠", "amount": -200.00 },
{ "type": "ACTUAL_REFUND", "typeName": "实际退款", "amount": 0 }
],
"hotelLines": [
{
"stayDate": "2026-08-28",
"hotelName": "草原明珠大酒店",
"roomTypeName": "标间",
"roomCount": 1,
"unitPrice": 320.00,
"plannedCost": 320.00,
"amount": 640.00,
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司支付",
"sourceType": "HOUSE_ASSIGNMENT",
"sourceTypeName": "配房结果"
}
],
"ticketLines": [],
"mealLines": [],
"vehicleLines": [],
"guideLines": [],
"photographerLines": [],
"otherExpenseLines": [],
"insuranceInfo": { "insured": false, "totalPremium": 0 }
}
}
```
边界要点:
1. **OTHER_INCOME / DISCOUNT 占位行**:只有 type/typeName/amount 三键,amount = 该 type 聚合值500 / 200,**不是 0**),占位行也参与「各行合计 = 净收入」勾稽;
2. **空成本分类给 `[]`**ticketLines 等空分类是空数组,不是不下发该键;
3. **未投保**insuranceInfo 恒下发,insured=false、保单字段productName/bizType/bizTypeName/extPolicyNo键不存在、totalPremium=0;顶层 insurancePremium=0;
4. 占位行无 itemName 等明细列NON_NULL 省略),前端按「只有 type/typeName/amount 三键」识别占位行。
### 8.3 业务失败(已删字段读取 undefined——新旧结构对比
**旧代码读法全部失效**(字段不是 null,是键不存在
```json
// ❌ 修改前(旧结构,已下线)
{
"data": {
"hotelCost": 640.00,
"ticketCost": 335.00,
"perCapitaRevenue": 5000.00,
"perCapitaCost": 4304.50,
"perCapitaProfit": 695.50,
"incomeLines": [
{
"type": "OTHER_INCOME",
"typeName": "其他收入",
"amount": 500.00,
"details": [
{ "itemName": "增费-升级房型", "amount": 500.00, "unitPrice": 500.00, "quantity": 1 }
]
}
],
"costCategories": [
{
"category": "HOTEL",
"categoryName": "住宿",
"amount": 640.00,
"lines": [ { "stayDate": "2026-08-28", "hotelName": "草原明珠大酒店", "amount": 640.00 } ]
},
{ "category": "INSURANCE", "categoryName": "保险", "amount": 88.00, "lines": [] }
]
}
}
```
```json
// ✅ 修改后(新结构,同一份数据)
{
"data": {
"insurancePremium": 88.00,
"incomeLines": [
{ "type": "OTHER_INCOME", "typeName": "其他收入", "itemName": "增费-升级房型", "unitPrice": 500.00, "quantity": 1, "amount": 500.00 }
],
"hotelLines": [ { "stayDate": "2026-08-28", "hotelName": "草原明珠大酒店", "amount": 640.00 } ],
"ticketLines": [],
"mealLines": [],
"vehicleLines": [],
"guideLines": [],
"photographerLines": [],
"otherExpenseLines": [],
"insuranceInfo": { "insured": true, "productName": "保游畅享境内游保险", "bizType": "ORDER", "bizTypeName": "订单险", "extPolicyNo": "PY20260814001", "totalPremium": 88.00 }
}
}
```
另附通用业务失败响应(与修改前一致):
```json
{ "code": 403, "message": "无权限访问", "success": false }
```
```json
{ "code": 581007, "message": "订单不存在", "success": false }
```
## 9. 业务边界
**适用**
- 核单页查看整团收入逐项、成本 7 分类逐项、保险信息、毛利汇总;各分类小计与占比由前端自算。
**不适用**
- 已收/未收维度分析 —— incomeLines 是**应收口径**(不含收款维度),已收/未收看顶层 paidAmount / outstandingAmount / netReceivedAmount。
**特殊边界(前端必知)**
1. **勾稽关系(可用来校验渲染)**
- `incomeLines 各行 amount 合计 = netRevenueAmount`(占位行也参与合计);
- `7 个成本分类明细数组 amount 合计 + insurancePremium = totalCost`
- `netRevenueAmount totalCost = grossProfit`
2. **占位行不丢数**:某 type 无逐项明细时仍有一行,amount = 该 type 聚合值(可能非 0。前端按 type 固定渲染 4 块时,不要因为「没有 details」就不渲染该块。
3. **DISCOUNT / ACTUAL_REFUND 行 amount 为负数**,合计时直接相加即可,不要取绝对值。
4. **空成本分类是 `[]` 不是键不存在**;行内 null 字段才是键不存在NON_NULL 省略)。
5. **insuranceInfo 恒下发**:无有效保单时 insured=false、保单字段键不存在、totalPremium=0;不要按「insuranceInfo 不存在」判未投保。
6. **vehicleLines 只含已确认行**(与车辆成本口径一致),未确认/已取消的派单不出现在数组里。
7. **vehicleLines / otherExpenseLines 是稀疏并集行**:每行只有本族字段非 nullVEHICLE_FEE 族 vs EXPENSE 族、EXPENSE vs SUBSIDY,渲染列时按字段是否存在判别行种。
## 10. 修改前后对比
### 10.1 字段级对比
| 位置 | 字段 | 修改前 | 修改后 |
|------|------|--------|--------|
| 顶层 | perCapitaRevenue | 人均营收 | **已删除**(需要时 自算 netRevenueAmount ÷ travelerCount |
| 顶层 | perCapitaCost | 人均成本 | **已删除**(自算 totalCost ÷ travelerCount |
| 顶层 | perCapitaProfit | 人均毛利 | **已删除**(自算 grossProfit ÷ travelerCount |
| 顶层 | hotelCost / ticketCost / mealCost / vehicleCost / guideCost / photographerCost / otherExpenseCost | 7 个成本分类小计 | **已删除**(自算对应分类数组 amount 合计) |
| 顶层 | costCategories | 成本套层8 分类行(含 INSURANCE,每行 {category, categoryName, amount, lines[]} | **已删除**,改 7 个分类明细数组平铺顶层 |
| 顶层 | hotelLines … otherExpenseLines | 无(明细埋在 costCategories[].lines | **新增** 7 个分类明细数组,行结构不变(原 lines 元素直接上移),空分类 `[]` |
| 顶层 | insurancePremium | 保费合计,8 小计之一 | **保留**,口径不变,仍计入 totalCost |
| 顶层 | insuranceInfo | 无(保险只是 costCategories 里一行汇总) | **新增** 保险子块insured/productName/bizType/bizTypeName/extPolicyNo/totalPremium |
| incomeLines[] | details | 每行内嵌逐项明细数组 | **已删除**,逐项明细拍平为多行 |
| incomeLines[] | itemName/content/unitPrice/headCount/quantity/paymentMethod(+/Name)/source(+/Name)/sourceType(+/Name)/remark/voucherUrls | 在 details 元素上 | **上移到行本身**,每行带全列 |
| incomeLines[] | 行数 | 固定 4 行(每 type 一行汇总) | 每 type ≥ 1 行有明细平铺多行,无明细给占位行amount=聚合值) |
| 顶层 | 抬头区 + 汇总金额其余字段 | 原样 | **零变化** |
### 10.2 行为级对比
| 场景 | 修改前 | 修改后 |
|------|--------|--------|
| 渲染收入明细 | 遍历 incomeLines[4],钻每行 details 数组 | 直接遍历 incomeLines 多行,按 type 分组渲染;占位行只有三键 |
| 渲染成本明细 | 遍历 costCategories[8],按 category 窄化 lines 元素结构 | 直接按数组名取 7 个分类数组,行结构即分类专属结构,无需窄化 |
| 成本分类小计 | 读 hotelCost 等 7 字段 / costCategories[].amount | **前端自算**各分类数组 amount 合计 |
| 保险信息 | costCategories 里 category=INSURANCE 一行汇总金额 | insuranceInfo 子块(产品名/保单号/保费合计)+ 顶层 insurancePremium |
| 人均指标 | 直接读 perCapita* 三字段 | **前端自算**(汇总值 ÷ travelerCount,注意 travelerCount=0 时不展示) |
| 分类判别 | category 枚举settlement_category 字典) | 数组名hotelLines 等,settlement_category 不再出现于出参 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **破坏兼容性**:⚠️ **破坏性变更**。删除 13 处出参3 个人均指标 + 7 个成本分类小计 + costCategories 套层 + incomeLines[].details 嵌套 + INSURANCE 分类行),凡读取这些字段的前端代码必须改造。
- **前端是否必须同步上线****是**。后端上线后旧字段立即消失,读旧结构的页面会得到 undefined/空白,前端新结构改造需与后端同窗口上线。
- **改造点清单**
1. 删 perCapitaRevenue/perCapitaCost/perCapitaProfit 所有读取,需要人均处改自算;
2. 删 hotelCost 等 7 个小计字段读取,改自算 7 个分类数组 amount 合计;
3. 成本渲染从 costCategories 两层遍历改为直读 7 个分类数组,去掉按 category 窄化的判别逻辑;
4. 收入渲染从 incomeLines[].details 改为 incomeLines 平铺多行,按 type 分组;适配占位行(仅 type/typeName/amount
5. 保险区块改读 insuranceInfo + insurancePremium;
6. 空分类按 `[]` 空态渲染,未投保按 insured=false 空态渲染。
- **无 DDL、无枚举值、无字典变更**;汇总金额数值口径零漂移netRevenueAmount/totalCost/grossProfit 修改前后同值)。
### 11.2 回滚方案
- 后端回滚 = revert PR #5966merge commit c8cae25131,重启 hl-order-service-v3,旧结构costCategories/details/小计/人均)恢复。
- 前端若已按新结构上线而后端回滚,新字段读到 undefined → 前端回滚需同步;建议前后端同窗口切换。
- 零 DDL / 零数据迁移,回滚无数据残留风险。
## 12. 注意事项
1. **NON_NULL 省略**:全出参带 @JsonInclude(NON_NULL),null 字段不下发该键(不是下 null;占位行只有 type/typeName/amount 三键,前端读取可空字段必须兜底。
2. **空数组与键不存在的区别**7 个成本分类数组空分类下 `[]`(键存在);行内可空列是键不存在。不要混用两种判空。
3. **Long 主键序列化为字符串**id / orderId,前端按 string 处理,不要 Number() 转换。
4. **金额保留两位小数**,BigDecimal JSON 输出为 number如 5000.00;grossProfitRate 是六位小数的小数0.139100 = 13.91%),展示百分比需 ×100。
5. **DISCOUNT / ACTUAL_REFUND 行 amount 为负数**,合计直接相加;占位行 amount 也可能是非 0 聚合值。
6. **占位行参与勾稽**「incomeLines 合计 = netRevenueAmount」成立的前提是把占位行也算进去;若前端只渲染有 itemName 的行,合计校验会对不上。
7. 本变更只影响单团核算表reports/group;主报账人报账表reports/reimbursement结构不受影响。
## 13. 关联 / 联系人
- Issue #5963https://git.1814.love:8443/wx/HL/issues/5963
- PR #5966https://git.1814.love:8443/wx/HL/pulls/5966
- Commithttps://git.1814.love:8443/wx/HL/commit/c8cae25131
- 服务hl-order-service-v3端口 8086
- 后端负责人yst腰苏图
## 14. 验证证据
- 代码已合并 dev-v3PR #5966,merge commit c8cae25131并部署测试服,网关实调验证通过§8.1 为真实返回,订单 2087487607792463873,2026-08-14 实测)。
- 勾稽实证incomeLines 合计 5000.00 = netRevenueAmount;7 分类合计 4216.50 + insurancePremium 88.00 = 4304.50 = totalCost;5000.00 4304.50 = 695.50 = grossProfit。
- 前端联调验证点:旧 13 处字段消费点全部改造;§8.2 占位行amount=聚合值非 0与空分类 `[]` 渲染;§8.3 新旧结构对照迁移;未投保 insuranceInfo 空态。