# mp 订单详情单房差展示修复 + 5 字段独立暴露

> **服务**: hl-order-service-v2(8094)
> **PR**: #1906 + #1909 (Refs #1900)
> **日期**: 2026-05-09
> **影响范围**: 小程序订单详情页

---

## ⚠️ 关键变化

`GET /mp/order/{orderId}` 接口:

1. **修复 GROUP 订单 priceBreakdown JSON 漏字段**(#1906):创建 GROUP 订单时 `priceBreakdownJson` 没写 `singleRoomDiff` / `singleRoomSurcharge`,导致明细面板看不到"单房差"行
2. **新增 5 个独立字段**(#1909):对齐 `POST /mp/product/{id}/quote` 接口

---

## 一、新增字段(`MpOrderDetailVO`)

| 字段 | 类型 | 说明 |
|------|------|------|
| `singleRoomSurcharge` | BigDecimal | 单房差实际加价金额(下单时按 D1: `adultCount==1` 触发);老订单/不触发 = `null` |
| `earlyBirdDiscount` | BigDecimal | 早鸟优惠金额(从 `order_discount` 表 EARLY_BIRD 取);无早鸟 = `null` |
| `earlyBirdPlanName` | String | 命中的早鸟计划名称(格式 `"早鸟优惠:xxx"`);无早鸟 = `null` |
| `finalGrandTotal` | BigDecimal | **应付总额** = `priceBreakdown.totalPayable` = `totalPrice - 优惠总额 + 附加费总额` |
| `finalBalanceAmount` | BigDecimal | **应付尾款**:DEPOSIT 模式 = `balanceAmount - earlyBirdDiscount`;FULL 模式 = `null` |

字段位置:`discountAmount` 之后、`priceBreakdown` 之前。

## 二、响应示例

订单 1 大 1 小、班期单房差 500、命中早鸟优惠 200、DEPOSIT 模式(订金 1000):

```json
{
  "code": 200,
  "data": {
    "orderId": "2052941051093520386",
    "adultCount": 1,
    "childCount": 1,
    "totalPrice": 5850.00,
    "depositAmount": 1000.00,
    "balanceAmount": 5850.00,
    "discountAmount": 200.00,
    "singleRoomSurcharge": 500.00,
    "earlyBirdDiscount": 200.00,
    "earlyBirdPlanName": "早鸟优惠:暑期早鸟",
    "finalGrandTotal": 5650.00,
    "finalBalanceAmount": 5650.00,
    "priceBreakdown": {
      "items": [
        { "label": "成人", "unitPrice": 2925, "quantity": 1, "amount": 2925 },
        { "label": "儿童", "unitPrice": 2425, "quantity": 1, "amount": 2425 },
        { "label": "单房差", "unitPrice": null, "quantity": null, "amount": 500 }
      ],
      "subtotal": 5850.00,
      "discounts": [
        { "tag": "早鸟优惠", "description": "早鸟优惠:暑期早鸟", "amount": -200.00 }
      ],
      "totalDiscount": -200.00,
      "totalPayable": 5650.00
    }
  }
}
```

## 三、字段使用建议(mmg)

订单详情页价格区域可用**两种展示风格**任选其一:

### 风格 A:简洁(推荐,与 quote 接口一致)

```
团费总价     ¥5,850.00       ← totalPrice (含单房差)
单房差       +¥500.00         ← singleRoomSurcharge (条件展示, null 隐藏)
早鸟优惠     -¥200.00         ← earlyBirdDiscount (条件展示, null 隐藏)
应付总额     ¥5,650.00        ← finalGrandTotal (粗体,主视觉)
其中订金     ¥1,000.00        ← depositAmount
应付尾款     ¥5,650.00        ← finalBalanceAmount (DEPOSIT 模式)
```

### 风格 B:展开明细(用现有 priceBreakdown 数组渲染)

继续用 `priceBreakdown.items` + `priceBreakdown.discounts` + `priceBreakdown.surcharges` 列表渲染明细,**总价显示用 `finalGrandTotal`**(等同 `priceBreakdown.totalPayable`)。

## 四、PR #1906 修复内容

**问题**:`GroupOrderStrategy.calculatePrice` 序列化 `priceBreakdownJson` 时手动 copy 字段,**漏 set `singleRoomDiff` 和 `singleRoomSurcharge`**。导致 mp 端 `priceBreakdown.items` 解析时 `isMissingNode` 跳过,GROUP 订单详情明细面板看不到"单房差"行。

**修复**:加 2 行 `q.setSingleRoomDiff(quote.getSingleRoomDiff()); q.setSingleRoomSurcharge(quote.getSingleRoomSurcharge());`

`CoreOrderStrategy.calculatePrice` 直接 `objectMapper.writeValueAsString(quote)` 序列化整个 SimpleQuoteVO,**无此问题**。

## 五、向后兼容

- 老订单 `priceBreakdown` JSON 缺 `singleRoomSurcharge` → 5 字段中 `singleRoomSurcharge=null`,其他正常
- 无早鸟订单 → `earlyBirdDiscount=null`,`earlyBirdPlanName=null`,`finalBalanceAmount=balanceAmount`(无优惠减)
- FULL 模式 → `finalBalanceAmount=null`(全款无尾款概念)
- 老前端不读新字段不报错(Jackson `fail-on-unknown-properties=false`)
- 未改 `totalPrice` / `discountAmount` / `priceBreakdown` 现有字段语义

## 六、测试覆盖

- `GroupOrderStrategyTest +1`(快照含 singleRoomDiff/Surcharge,#1906)
- `MpOrderDetailAssemblerTest +4`(单房差+早鸟+老订单+FULL,#1909)
- 共 +5 用例,**全绿**

## 七、@mmg 关注点

1. 订单详情页价格区域改造:加"单房差"行 + "早鸟优惠"行(都是条件展示,null 隐藏)
2. 总价用 `finalGrandTotal`(应付总额)替代 `totalPrice`(后者含单房差但不减优惠,继续保留作为"团费原价")
3. 应付尾款 DEPOSIT 模式用 `finalBalanceAmount`,FULL 模式用 `totalPrice`
4. 老前端不动也能跑(只是看不到新字段拆分)
5. 已存在的"老订单"(单房差功能上线前下的)`singleRoomSurcharge=null` 仍按原 totalPrice 展示,行为不变
