---
schema: "hl-changelog/v2"
ticket: "5963"
title: "单团核算表出参重构——收入拍平逐项+成本7分类明细数组+保险独立子块"
consumer: "admin"
change_type: "修改接口"
author: "yst"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "1072cec2"
target_release: ""
verified_at: "2026-08-15"
status_note: "PR #5966 已合并 dev-v3（merge 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 个 type：BASE_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 | 无权限访问 | 房务角色（HOUSE）JWT 调用 |
| 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 是稀疏并集行**：每行只有本族字段非 null（VEHICLE_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 #5966（merge 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 #5963：https://git.1814.love:8443/wx/HL/issues/5963
- PR #5966：https://git.1814.love:8443/wx/HL/pulls/5966
- Commit：https://git.1814.love:8443/wx/HL/commit/c8cae25131
- 服务：hl-order-service-v3（端口 8086）
- 后端负责人：yst（腰苏图）

## 14. 验证证据

- 代码已合并 dev-v3（PR #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 空态。
