docs(changelog): 12_fix_product_v2_toddler_discount_db_clear_on_update

PR #2111 / Issue #2110 — 修补 #2107 漏 UPDATE 路径; MP NOT_NULL 默认跳过 null
导致 child=0 时 toddler_discount DB 列残留. @TableField(updateStrategy=IGNORED)
强制 UPDATE 写入, 测试服 4 场景 DB 真测通过. 前端无需改动.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
这个提交包含在:
API Changelog Bot 2026-05-12 16:45:23 +08:00
父节点 424ae19438
当前提交 5442777489

查看文件

@ -0,0 +1,78 @@
---
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 仅备案。