hl-api-changelog/changelogs/2026-04/2026-04-18_product-v2_toddler-discount-normalize-and-tierseq-hardening.md

7.7 KiB

fix(product-v2): 小童优惠0归一化null + 价格日历tierSeq拆箱加固

服务: hl-product-service-v2 (端口 8083) PR: #827 Issue: #826 Merge commit: d9d808cf7594 日期: 2026-04-18 影响范围: 管理端产品价格日历(批量保存/按月查询)+ 报价计算


一、背景

本次处理两类问题:

BUG-3小童优惠toddlerDiscount保存为 0 的语义歧义

  • 现象:前端表单 toddlerDiscount 字段初始绑定为 0,用户不填也会传 0,后端照实存成 0.00
  • 语义问题:0 有两种解读 ——「0 元优惠(就是没优惠)」 vs 「没设置优惠(未填)」,容易让后台回显/审核人员误会
  • 根因实证:后端本身对 null 和 0 的处理是正确的DB 允许 NULL,查询按实际值回显,问题发生在前端默认值
  • 本次决策(方案 A:后端兜底归一化,toddlerDiscount=0 入库时自动转成 NULL
  • infantPrice=0 不做归一化,因为「幼童价=0」=「幼童免费」是合法业务语义,不能误伤

NPE 加固Integer tierSeq 拆箱风险

  • 分析员代码走查发现两处 Integer == int 自动拆箱点:
    • ProductPricingService.calculateQuoteL473若某条 product_price_calendar.tier_seq 为 NULL,报价会直接 500 NPE
    • PriceCalendarAggregatorL81原有 != null 守卫实际拆箱安全,但为一致性一并改写
  • 改用 java.util.Objects.equals(a, b) 规避拆箱

BUG-4暂挂前端报告「小蒙马设计价格日历 500」

  • 测试环境实证:两个 GROUP 产品(20443068575346360342043696114145644545)的价格日历/报价/班期相关全部接口均 200
  • 历史服务日志已滚动丢失,无法追溯原始 500 堆栈
  • 本次先部署 NPE 加固,降低再次出现拆箱 500 的概率;若复现可抓新堆栈再精准定位

二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 批量保存价格日历 POST /admin/product/item/{id}/price-calendar/batch 入库逻辑变更 toddlerDiscount=0 后端自动归一化为 NULLinfantPrice=0 保留不变
2 按月查询价格日历 GET /admin/product/item/{id}/price-calendar?month=YYYY-MM&tierSeq=X 回显值可能变化 原存 0toddlerDiscount 本次保存后回显为 null(未动存量)

报价接口 POST /admin/product/item/{id}/quote 和建议价 GET /admin/product/item/{id}/suggest-price 路由不变、入参出参不变,仅内部加固了 tierSeq 拆箱。


三、接口详情

1. 批量保存价格日历 POST /admin/product/item/{id}/price-calendar/batch

请求 body 字段未变(本次只改后端落库逻辑):

字段 类型 必填 说明 本次行为变化
toddlerDiscount BigDecimal 小童优惠额(元/人) ⚠️0 后端自动转 NULL 入库;传正数/负数保持
infantPrice BigDecimal 幼童价(元/人) 无变化,0 保持 0=免费)
其他字段 - - - 无变化

落库行为对照表

前端传入 DB toddler_discount DB infant_price
toddlerDiscount=0, infantPrice=0 NULL 0.00
toddlerDiscount=50, infantPrice=100 50.00 100.00
两字段都不传 NULL NULL
toddlerDiscount=-500 -500.00(不动负数) -

2. 按月查询价格日历 GET /admin/product/item/{id}/price-calendar

响应结构未变,仅 toddlerDiscount 字段可能为 null

{
  "code": 0,
  "data": [
    {
      "priceDate": "2026-06-01",
      "tierSeq": 1,
      "adultSellPrice": 1500.00,
      "childSellPrice": 1200.00,
      "infantPrice": 0.00,
      "toddlerDiscount": null
    }
  ]
}

四、前端影响与必须配合

影响 1toddlerDiscount 回读值可能从 0 变成 null

如果前端当前代码这样用:

// ❌ 旧写法(只判 0
if (form.toddlerDiscount === 0) { /* 视为未设置 */ }

// ❌ 旧写法(只判 null
if (form.toddlerDiscount == null) { /* 视为未设置 */ }

请改为:

// ✅ 推荐:两者都视为"未设置/0 优惠"
if (form.toddlerDiscount == null || form.toddlerDiscount === 0) {
  // 显示为空输入框 / 文案"未设置优惠"
}

语义上 null 和 0 都表示「不减免」,对报价计算(后端 calculateQuote!= null ? x : ZERO 兜底)零影响。前端只需保证展示和编辑回填不出 bug。

影响 2infantPrice 无变化

  • infantPrice=0 合法且保留(= 幼童免费)
  • 前端逻辑沿用原有判断,不要跟着 toddlerDiscount 一起归一化

影响 3根治建议可选

建议前端同步修复表单默认值:toddlerDiscount 初始值由 0 改为 null/undefined,避免"未填"被序列化成 0。后端归一化已做兜底,前端改不改都能用。


五、NPE 加固说明

加固点 1ProductPricingService.calculateQuote

原代码(存在拆箱 NPE

int tierSeq = req.getTierSeq() != null ? req.getTierSeq() : ProductConstants.DEFAULT_TIER_SEQ;
// ↓ p.getTierSeq() 为 null 时自动拆箱 → NullPointerException
.filter(p -> p.getTierSeq() == tierSeq)

现代码:

Integer tierSeq = req.getTierSeq() != null ? req.getTierSeq() : ProductConstants.DEFAULT_TIER_SEQ;
.filter(p -> Objects.equals(p.getTierSeq(), tierSeq))

影响:若 DB product_price_calendar.tier_seq 历史脏数据有 NULL 行,过去调用 POST /admin/product/item/{id}/quote 会 500 NPE;现在改为正常业务异常 NotFoundException(查不到对应档位价格),HTTP 仍返 200,Result.code 为业务错误码。

加固点 2PriceCalendarAggregator

原代码有 != null 守卫,拆箱实际安全,但为一致性改写成 Objects.equals,降低后续维护误改风险。


六、BUG-4 仍在排查(前端请补充信息)

前端同事反馈的「小蒙马设计价格日历 500」本次未能在测试环境复现

  • 两个 GROUP 产品全部相关接口实测 200
  • 历史日志已丢失,无法还原原始堆栈

请前端同事补充以下信息,才能精准定位

  1. 完整接口 URL + HTTP method(例如 POST /admin/product/item/123/price-calendar/batch
  2. 产品 ID
  3. 操作步骤:「设计价格日历」具体在编辑页第几步、点了哪个按钮、是否涉及档位切换/班期绑定
  4. 请求 payload(从浏览器 DevTools Network 复制 Request body
  5. 响应 body(如能截到 500 的完整响应)

本次部署已经加固 2 处可能的 NPE 拆箱点,如果再次复现 500,可以从新日志抓到更清晰的堆栈,再做针对性修复。


七、未受影响接口(明确说明)

  • 产品创建/编辑/审核/上架下架(/admin/product-basic/*/admin/product/item/{id}/toggle-publish 等):零影响
  • 报价接口 POST /admin/product/item/{id}/quote:路由/入参/出参不变,仅内部 NPE 加固
  • 建议价 GET /admin/product/item/{id}/suggest-price:零影响
  • 班期管理 /admin/product/item/{id}/schedule/*:零影响
  • 小程序端全部接口:零影响

八、受影响服务 + 重启提示

  • 仅需重启 hl-product-service-v2(端口 8083
  • 测试环境已通过 Deploy Panel 完成部署commit d9d808cf7594
  • 正式环境尚未部署

九、相关链接