---
date: 2026-05-12
type: backend-fix
service: hl-product-service-v2
priority: high
notify: ["@mmg"]
status: verified
---

# 价格日历: 小童优惠儿童=0 时 DB 列**真正清空**(补 #2107 漏网回归)

> **PR**: [#2111](https://git.1814.love:8443/wx/HL/pulls/2111) — 已合并 dev + 部署测试服
> **Issue**: [#2110](https://git.1814.love:8443/wx/HL/issues/2110)
> **接口**: `POST /admin/product/item/{id}/price-calendar/batch` (无 API 变化)
> **状态**: ✅ **测试服已验证** — 4 场景 DB 落库 SQL 真测通过

---

## 背景

#2107 (changelog `12_fix_product_v2_toddler_discount_normalize_and_validate.md`) 修了"儿童=0 时强制 toddlerDiscount=null"逻辑, 单测 + INSERT 场景验证通过, **遗漏了 UPDATE 场景**:

- 原 DB 已有记录 `child=80, toddler=20`
- 用户改 child=0 保存 (前端把 toddler 输入框 disabled)
- 后端 step 0 把 `req.toddlerDiscount` 归一化为 null ✓
- `expandToDailyRecords` 兜底 0→null ✓
- **`batchUpsert` 走 `updateById(item)`, MP 默认 `FieldStrategy.NOT_NULL` 跳过 null 字段**, UPDATE SQL 不下发 `set toddler_discount=NULL`, **DB 列保留原值 20**

## 修复

```java
public class ProductPriceCalendarDO extends BaseDO {
    @TableField(updateStrategy = FieldStrategy.IGNORED)  // ← 加这一行
    private BigDecimal toddlerDiscount;
}
```

`FieldStrategy.IGNORED` 强制 UPDATE 写入此字段, 即使 null 也下发 `set toddler_discount=NULL`, 让 step 0 归一化真正落库。

## 测试服真测 (硬性凭证)

接口: `POST /admin/product/item/{id}/price-calendar/batch`
产品: 2053668473820622849 (test_admin 自有产品)

```
Scenario A: 已有 child=80,toddler=20 + 提交 child=0
  → DB seed: toddler_discount=20.00 (insert 阶段)
  → DB after: toddler_discount=NULL ✓ (update 阶段强制写入)

Scenario B: 已有 child=80,toddler=20 + 提交 child=60,toddler=0
  → DB seed: 20.00
  → DB after: NULL ✓ (expandToDailyRecords 0→null 归一化 + UPDATE 真写)

Scenario C: 已有 child=80,toddler=20 + 提交 child=60,toddler=15
  → DB seed: 20.00
  → DB after: 15.00 ✓ (正常 UPDATE)

Scenario D: 全新 child=0
  → DB after: NULL ✓ (INSERT 阶段 null 正常写入)
```

## 影响面

- **API 不变, 仅 DB 写入行为修正**, 前端无需改动
- `infantPrice/singleRoomDiff` 等其他金额字段**不需要**同样调整 (0=合法值, 无 0→null 归一化语义)
- `deductStock/restoreStock` 走 load → setSold → updateById, toddler_discount 字段从 DB 加载原值原样写回, IGNORED 策略下等价无变化, 安全

## 风险

- 极小. `@TableField(updateStrategy=IGNORED)` 是 MP 官方文档行为, 仅影响**该单字段**的 UPDATE 写入策略, 不影响 INSERT/SELECT/DELETE。

## 历史脏数据

DB 里可能存在 `childSellPrice=0 && toddler_discount IS NOT NULL` 的脏数据(#2107 之前的残留)。**下次该区间编辑保存**会被本 PR 真正清掉, 用户无感知, 无需单独迁移脚本。

## 前端 @mmg 无需改动

仅是后端 UPDATE 写入策略修正, 接口/字段/行为全无变化。本 changelog 仅备案。
