diff --git a/changelogs/2026-04/2026-04-19_early-bird-plan-product-unique.md b/changelogs/2026-04/2026-04-19_early-bird-plan-product-unique.md new file mode 100644 index 0000000..8aadd93 --- /dev/null +++ b/changelogs/2026-04/2026-04-19_early-bird-plan-product-unique.md @@ -0,0 +1,80 @@ +# refactor: 早鸟计划保存强制"一产品一早鸟"唯一约束 + +> **服务**: hl-order-service-v2 +> **PR**: #929 +> **Issue**: #917 +> **日期**: 2026-04-19 +> **前端是否需要改动**: **建议改动错误展示**(新增约束冲突错误,需前端友好展示) + +--- + +## 一、背景 + +一个产品同时只能绑定一个早鸟优惠计划(业务规则)。之前 DB 无唯一约束,Service 也不校验,运营可重复绑定同一产品到多个计划,导致 `matchPlan` 命中不确定。 + +本次从 **Service 层 + DB 索引** 两道防线强制约束。 + +## 二、变更接口 + +| 方法 | 路径 | 变更类型 | +|------|------|---------| +| POST | `/admin/order/early-bird` | 新增业务约束校验 | +| PUT | `/admin/order/early-bird/{id}` | 同上 | + +## 三、新增约束行为 + +**保存前校验**:请求体 `productIds` 中任意产品已被其他早鸟计划绑定 → 直接返回 400 + 明细消息。 + +编辑时会自动排除当前计划 ID,即编辑同一计划仍使用原产品不算冲突。 + +## 四、错误响应示例 + +### 冲突场景 + +```json +POST /admin/order/early-bird +{ + "planName": "五一早鸟", + "discountAmount": 100.00, + "minPeople": 2, + "startDate": "2026-05-01", + "endDate": "2026-05-15", + "productIds": [2045345825172639746, 2045345825172639747] +} + +// 响应(产品 2045345825172639747 已被其他计划绑定) +{ + "code": 400, + "message": "以下产品已绑定其他早鸟计划,不能重复绑定:阿尔山3日(产品ID=2045345825172639747, 已关联计划ID=2045586012523864065)", + "success": false +} +``` + +- 多个冲突会用 `; ` 分隔列出 +- 消息含产品名(Feign 失败时退化为 "产品[ID]" 占位)+ 产品 ID + 占用的计划 ID + +### 编辑同一计划的产品(不算冲突) + +```json +PUT /admin/order/early-bird/2045586012523864065 +{ + "planName": "默认早鸟优惠", + "productIds": [2045345825172639746] // 同一计划原本就绑的产品 +} +// 响应: 200 OK +``` + +## 五、前端改动建议 + +1. **错误提示展示**:后端返回的错误消息已自带产品名/ID/冲突计划 ID,前端直接用 `message` 字段展示即可 +2. **可选优化**:前端在选择 "适用产品" 时,可先调 `GET /admin/order/early-bird` 列表接口收集已占用的产品 ID,在下拉框禁用或标灰提示"已被计划 XX 占用" +3. **编辑时**:保持原绑定的产品不算冲突,前端无需特殊处理 + +## 六、兼容性 + +- 之前一个产品绑多个计划的脏数据在生产环境由运维按 `sql/migrations/20260419_917_early_bird_product_unique.sql` 清洗(默认保留最早一条) +- 本地 / 测试环境已完成 DDL 迁移,本地 DB 若存在重复数据请运行脚本中【审计】+【清洗】SQL 后再拉代码 + +## 七、关联 + +- 后端 changelog: `2026-04-19_early-bird-product-unique-ddl.md`(含 DDL 迁移细节) diff --git a/changelogs/2026-04/2026-04-19_schedule-max-participants-required.md b/changelogs/2026-04/2026-04-19_schedule-max-participants-required.md new file mode 100644 index 0000000..8be56d0 --- /dev/null +++ b/changelogs/2026-04/2026-04-19_schedule-max-participants-required.md @@ -0,0 +1,75 @@ +# fix: 小蒙马班期保存 maxParticipants 改为必填字段 + +> **服务**: hl-product-service-v2 +> **PR**: #927 +> **Issue**: #859 +> **日期**: 2026-04-19 +> **前端是否需要改动**: **需要改动**(之前允许不传或传 null,现在必须显式传数字;0=不限) + +--- + +## 一、背景 + +测试环境反馈前端保存班期时缺传 `maxParticipants` 会收到模糊的"违反约束"报错,用户无法分辨是哪个字段出问题。 + +后端之前 `ScheduleSaveReqVO.maxParticipants` 只用 `@ApiModelProperty(required=true)` 做了文档标记,Bean Validation 层未强制校验,结果 null 一路打到 SQL 层才报错。 + +## 二、变更接口 + +| 方法 | 路径 | 变更类型 | +|------|------|---------| +| POST | `/admin/product/item/{id}/schedule` | 请求体字段校验收紧 | +| POST | `/admin/product/item/{id}/schedule/batch-create` | 同上 | + +## 三、字段约束变化 + +| 字段 | 之前 | 现在 | 允许值 | +|------|------|------|--------| +| `maxParticipants` | 可不传 / null(后端兜底 0) | **必传** | 0=不限;>0=上限人数 | + +## 四、错误响应示例 + +### 缺传 maxParticipants + +```json +POST /admin/product/item/123/schedule +{ + "batchName": "五一特别团", + "departureDate": "2026-05-01", + "adultPrice": 5160, + "childPrice": 4580 +} + +// 响应(之前是 "数据操作违反约束,请检查输入",歧义) +{ + "code": 400, + "message": "最大参与人数不能为空", + "success": false +} +``` + +### 正常请求 + +```json +POST /admin/product/item/123/schedule +{ + "batchName": "五一特别团", + "departureDate": "2026-05-01", + "adultPrice": 5160, + "childPrice": 4580, + "maxParticipants": 30, // 必填 + "maxRooms": 15 // 可选,默认取行程最少房间数 +} +``` + +## 五、前端改动建议 + +1. **班期创建/编辑表单**:`maxParticipants` 输入框加前端必填校验,避免到后端才被拦下 +2. **批量创建表单**:同上 +3. **提示文案**:如果字段空值,提示"最大参与人数不能为空,0 表示不限人数" + +## 六、兼容性 + +- 前端原本填写 0 或具体数字的调用方不受影响 +- 前端如有"不填=不限"的历史 UX,需改成"显式填 0=不限" +- 后端 Service 层 null→0 兜底代码保留作为双保险,但实际不会再走到