hl-api-changelog/changelogs/2026-04/2026-04-17_breaking_price-calendar-validation.md
API Changelog Bot f2e30e51ac ⚠️破坏性: 价格日历批量接口字段强校验(PR #748 Issue #747)
- childSellPrice 从可空升级为 @NotNull + @DecimalMin(0.01)
- tierSeq 从默认1升级为 @NotNull + @Min(1) + 必须≤产品档位数
- priceType 从默认NORMAL升级为 @NotBlank + @Pattern(NORMAL|PEAK|HOLIDAY|SPECIAL)
- adultSellPrice 新增 @DecimalMin(0.01)
- 服务: hl-product-service-v2, commit c311e6ee
- 前端必须同步升级表单必填校验,否则老前端保存会 400
2026-04-17 16:43:39 +08:00

8.0 KiB

⚠️ 破坏性变更:价格日历批量接口字段强校验

类型: 🔴 BREAKING CHANGE破坏性变更,老前端会 400 服务: hl-product-service-v2端口 8083 接口: POST /admin/product/item/{id}/price-calendar/batch Issue: #747 PR: #748已合并到 dev Commit: c311e6ee 日期: 2026-04-17 影响范围: 管理后台 Step4 定价管理 — 批量设置价格


🚨 一句话摘要(最重要)

3 个字段从「兜底默认值」升级为「强校验必传」。老前端如果不传 childSellPrice / tierSeq / priceType 会直接返回 400,无法保存价格日历。前端必须同步升级表单必填校验。


一、原漏洞(测试服实测)

测试服(升级前)存在以下脏数据写入漏洞:

# 用例 原行为( 漏洞)
1 childSellPrice 缺失 / 为 null 允许保存(儿童价变 null,前端列表显示空
2 tierSeq=99(产品实际只有 5 档) 允许保存(脏数据,点查详情无法匹配档位)
3 priceType=XXX(非字典枚举值) 允许保存(污染字典)
4 adultSellPrice=0 或负数 允许保存0 元购,业务事故)
5 startDate > endDate 允许保存(生成 0 条价格但 200

二、根因

PriceCalendarBatchSaveReqVO 对三个关键字段只做了「空值给默认值」的兜底处理,未使用 @NotNull / @NotBlank / @Pattern;Service 层也未校验 tierSeq 是否在产品实际档位范围内。

三、修复后行为(本地经网关实测)

用例 HTTP 响应 message
T1 正常请求 200 价格日历设置成功
T2 缺 childSellPrice 400 childSellPrice: 儿童售价不能为空
T3 tierSeq=995 档产品) 500 档位序号99不存在,本产品只有5档
T4 adultSellPrice=0 400 adultSellPrice: 成人售价必须大于0
T5 priceType=XXX 400 priceType: 价格类型必须是 NORMAL/PEAK/HOLIDAY/SPECIAL 之一
T6 startDate > endDate 500 开始日期不能晚于结束日期

四、接口契约

4.1 接口签名(未变)

方法 路径 说明
POST /admin/product/item/{id}/price-calendar/batch 批量设置某档位某日期区间的价格

4.2 请求体字段校验矩阵

字段 类型 必填 校验规则 说明
startDate String (yyyy-MM-dd) @NotNull 区间开始日期(含)
endDate String (yyyy-MM-dd) @NotNull + Service 校验 endDate ≥ startDate 区间结束日期(含)
tierSeq Integer 🔴 新:@NotNull + @Min(1) + Service 校验 tierSeq ≤ 产品实际档位数 档位序号1 起),多档位产品必传
priceType String 🔴 新:@NotBlank + @Pattern(NORMAL|PEAK|HOLIDAY|SPECIAL) 价格类型字典值
adultSellPrice BigDecimal 🔴 加强:@NotNull + @DecimalMin("0.01") 成人售价,必须 > 0
childSellPrice BigDecimal 🔴 新:@NotNull + @DecimalMin("0.01") 儿童售价,必须 > 0
toddlerDiscount BigDecimal 幼童价格调整(可负数,如 -500 表示减 500
infantPrice BigDecimal 婴儿价格
dailyStock Integer 每日库存

4.3 🔴 破坏性变更对比表(前端必改)

字段 升级前(老行为) 升级后(新强校验) 前端必须动作
childSellPrice 可不传 / 可为 null → 落库 null @NotNull + @DecimalMin(0.01) 必须让用户填写并提交
tierSeq 可不传 → 后端默认 1 @NotNull + @Min(1) + 不能超过产品档位数 必须传当前选中 tab 的 tierSeq
priceType 可不传 → 后端默认 NORMAL @NotBlank + @Pattern 只能是 NORMAL/PEAK/HOLIDAY/SPECIAL 必须传字典枚举值(表单下拉默认 NORMAL
adultSellPrice @NotNull(允许 0 @NotNull + @DecimalMin(0.01)(不能为 0 或负) 表单校验 > 0

五、完整请求 / 响应示例

5.1 请求体(合法)

POST /admin/product/item/2045018534152478721/price-calendar/batch
Content-Type: application/json

{
  "startDate": "2026-11-01",
  "endDate": "2026-11-05",
  "tierSeq": 3,
  "priceType": "NORMAL",
  "adultSellPrice": 3515,
  "childSellPrice": 1175,
  "toddlerDiscount": -500,
  "infantPrice": 500,
  "dailyStock": 20
}

5.2 成功响应200

{
  "code": 200,
  "message": "价格日历设置成功",
  "data": null,
  "success": true
}

5.3 失败响应 — @Valid 校验失败400

childSellPrice

{
  "code": 400,
  "message": "childSellPrice: 儿童售价不能为空",
  "data": null,
  "success": false
}

adultSellPrice=0

{
  "code": 400,
  "message": "adultSellPrice: 成人售价必须大于0",
  "data": null,
  "success": false
}

priceType=XXX

{
  "code": 400,
  "message": "priceType: 价格类型必须是 NORMAL/PEAK/HOLIDAY/SPECIAL 之一",
  "data": null,
  "success": false
}

5.4 失败响应 — Service 业务校验失败500,BusinessException

tierSeq=99(产品只有 5 档):

{
  "code": 500,
  "message": "档位序号99不存在,本产品只有5档",
  "data": null,
  "success": false
}

startDate > endDate

{
  "code": 500,
  "message": "开始日期不能晚于结束日期",
  "data": null,
  "success": false
}

⚠️ 前端展示错误时:400 和 500 都要弹 toast 显示 message,不能只处理 200。


六、前端应配合改进的 4 条P1

1. 保存按钮置灰(多档位产品)

多档位产品(tiers.length > 1)的「确认添加」/「保存」按钮:

  • 所有档位 tab 必须都填完「成人售价 + 儿童售价」才允许点击
  • 任一档位缺价 → 按钮禁用 + 提示「请完成所有档位的定价」

2. 切 tab 时 inline 校验

用户在档位 tab A 填了一半想切到 tab B

  • 当前 tab 的 adultSellPricechildSellPrice 未填 → 禁止切走 + 弹提示「请先填完当前档位的必填项」
  • 避免用户切走后忘了回来,最后提交时才发现少档位

3. 服务端 400 / 500 错误高亮

  • 400 错误的 message 格式为 {fieldName}: {中文说明},可按冒号切分定位到具体字段 → 表单红框高亮
  • 500 业务异常如「档位序号99不存在,本产品只有5档」直接 toast 显示 message,前端无需猜测语义
  • 若后续有多档聚合校验,异常消息可能形如 档位【舒适】缺少儿童售价 — 可用正则 /【(.+?)】/ 提取档位名,滚动到对应 tab

4. 单档位产品同样校验

tiers.length === 1 的产品也必须校验「成人售价 + 儿童售价」非空非 0,不能因为没 tab 就放行。


七、字典速查 — priceType 枚举

中文 说明
NORMAL 平日 默认值(工作日通用)
PEAK 旺季 节假日 / 旅游旺季
HOLIDAY 节假日 法定节假日
SPECIAL 特殊日 大促 / 特价

前端下拉框硬编码这 4 个选项即可(或走字典接口 /admin/dict-data/type/price_type)。


八、重启提示

  • 需要重启:hl-product-service-v2(端口 8083
  • 部署方式:通过 Deploy Panel 重新部署最新 jar测试服 192.168.100.160

九、合并信息

项目
Issue #747
PR #748
合并 commit c311e6ee
目标分支 dev
服务 hl-product-service-v2

十、优先级

🔴 P0 破坏性 — 老前端发版后如果不同步改,会让「批量设置价格」整条链路 400 失败,定价流程完全走不通。前端必须在后端发版前或同步发版,别拖。