hl-api-changelog/changelogs-v2/2026-08/14_5963_单团核算表出参重构-修改接口-管理后台.md
yaosutu 34e50005f9
所有检测均成功
changelog-filename-gate / validate (push) Successful in 2s
docs(changelog): 单团核算表出参重构——收入拍平逐项+成本7分类明细数组+保险独立子块(#5963)
- 删 perCapita 人均三指标 + hotelCost 等 7 个成本分类小计 + costCategories 套层 + incomeLines[].details 嵌套
- incomeLines 拍平逐项行(4 type 各至少一行,无明细给占位行 amount=聚合值)
- 成本改 hotelLines 等 7 分类明细数组平铺顶层,空分类 []
- 新增 insuranceInfo 保险子块;insurancePremium 顶层保留且计入 totalCost
- PR #5966 已合并 dev-v3(c8cae25131),测试服部署 + 网关实调验证通过
2026-08-14 17:45:57 +08:00

37 KiB

schema, ticket, title, consumer, change_type, author, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
schema ticket title consumer change_type author backend_status gateway_status frontend_status frontend_owner frontend_ref target_release verified_at status_note updated_at base
hl-changelog/v2 5963 单团核算表出参重构——收入拍平逐项+成本7分类明细数组+保险独立子块 admin 修改接口 yst deployed verified pending PR #5966 已合并 dev-v3merge commit c8cae25131。破坏性契约变化删除 perCapitaRevenue/perCapitaCost/perCapitaProfit 人均三指标、hotelCost 等 7 个成本分类小计、costCategories 套层与 incomeLines[].details 嵌套;incomeLines 拍平为逐项行4 type 各至少一行,无明细给占位行 amount=聚合值);成本改 hotelLines 等 7 个分类明细数组平铺顶层;新增 insuranceInfo 保险子块。仅出参变化,无入参/枚举值/DDL 变化。 2026-08-14 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
{
  "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 元优惠(无逐项明细),无退款;餐食/导游/摄影/其他支出均无明细。

{
  "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,是键不存在

// ❌ 修改前(旧结构,已下线)
{
  "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": [] }
    ]
  }
}
// ✅ 修改后(新结构,同一份数据)
{
  "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 }
  }
}

另附通用业务失败响应(与修改前一致):

{ "code": 403, "message": "无权限访问", "success": false }
{ "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. 关联 / 联系人

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 空态。