diff --git a/changelogs-v2/2026-08/14_5963_单团核算表出参重构-修改接口-管理后台.md b/changelogs-v2/2026-08/14_5963_单团核算表出参重构-修改接口-管理后台.md new file mode 100644 index 0000000..07b4411 --- /dev/null +++ b/changelogs-v2/2026-08/14_5963_单团核算表出参重构-修改接口-管理后台.md @@ -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-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`。`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 空态。