# 未确认优惠不再扣减尾款(修复 balanceAmount 负数显示)

**日期**: 2026-04-24
**PR**: #1340 (dev)
**Issue**: #1339
**类型**: fix
**服务**: hl-order-service-v2

---

## 摘要

订单详情页/列表页的**尾款字段 `balanceAmount`** 之前会把**未确认优惠**也扣进去,导致优惠金额大于总售价时显示为**负数**(例如总售价 ¥2.00、未确认优惠 ¥560.00、已付 ¥0.10 → 尾款 ¥-558.10),但同页文案又写着「**以下优惠未确认,暂不影响尾款**」,自相矛盾。

本次修复统一让 3 处尾款计算走 `BalanceCalculator`(架构详设 3.1 唯一真相源),**只扣 `confirmed=1` 的已确认优惠/增项**,符合清单确认前「暂不影响尾款」的业务承诺。

## 影响接口(路径/返回结构不变)

所有返回 `balanceAmount` 字段的接口,行为一致性修复:

| 接口 | 来源 |
|------|------|
| `GET /admin/order/{orderId}` | `OrderDetailQueryService.assembleDetail` |
| `GET /admin/order/page` | `OrderListQueryService.toOrderListVO` |
| `GET /mp/order/page` | `OrderListQueryService.toMpOrderListVO` |
| `GET /mp/order/{orderId}` | `MpOrderDetailAssembler.buildBasicFields` |

## 新旧对比

### 旧公式(3 处都一样)
```
balance = totalPrice - order.discountAmount + order.surchargeAmount - (depositAmount 或 paidAmount)
```
其中 `order.discountAmount` = DB 缓存字段,**包含未确认优惠**。

### 新公式(统一走 BalanceCalculator)
```
balance = totalPrice - Σ(confirmed=1 优惠) + Σ(confirmed=1 增项) - paidAmount
```
未确认条目(confirmed=0 或 NULL)**不算入尾款**。

## 前端视图影响

- **顶部「尾款」** 由 -558.10 之类的异常值变为合理值(本订单场景 = 1.90)
- **顶部「优惠」合计金额**(`discountAmount` 字段) **保持不变** — 仍显示所有优惠之和(含 pending),前端继续按原字段渲染无需改动
- 确认清单时后端 T5 状态机 guard 会拦截「尾款为负」导致的锁单(此行为原已存在,本 PR 不动)

## 行为举例

订单场景:总售价 ¥2.00,订金 ¥0.10,已付 ¥0.10,一条 ¥560 房间优惠 confirmed=0

| 字段 | 修复前 | 修复后 |
|------|--------|--------|
| totalPrice | 2.00 | 2.00 |
| paidAmount | 0.10 | 0.10 |
| discountAmount(合计展示) | 560.00 | 560.00 |
| **balanceAmount(尾款)** | **-558.10** | **1.90** |

确认清单把优惠 `confirmed` 置 1 后,balanceAmount 才会变为 -558.10;届时 T5 状态机 guard 会拒绝锁单,提示管理员先清理超额优惠。

## 风险与回滚

- 只改 VO 组装层,不改接口签名/字段/DB schema
- 所有单测(MpOrderDetailAssemblerTest 63/63、OrderDetailQueryServiceTest 17/17、OrderListQueryServiceTest 8/8)+ 全量 2414/2415 通过
- 测试服 curl 验证:订单 `HL20260423175303-5448` balanceAmount 从 -558.10 修正为 1.90
- 回滚:revert PR #1340 即可恢复旧行为

## 遗留优化(后续独立 PR)

`order_info.discount_amount` 字段由 `OrderDiscountService.sumByOrderId` 写入时未过滤 `confirmed`,语义上包含 pending。本 PR 未动(前端「优惠合计」依赖这个字段显示 pending+confirmed 总和)。后续会单独统一语义并给前端提供独立 `pendingDiscountSum` / `confirmedDiscountSum` 字段,本次不涉及。
