feat(2026-04-20): 第一期 BUG 修复 + 3 大新功能上线(早鸟多方案/备品组合/高德地图)
本批变更(测试环境已部署验证): 1. 后端修复 9 个 BUG(#956/957/958/959/960 + hotfix #965/966/967) 2. BUG 10 产品地图支持高德自动生成(#962) 3. BUG 15 备品组合功能(#963) 4. BUG 20 早鸟优惠改造为 1:N 多方案(#964) 前端待跟进 BUG:1/2/5/6/8/12/19
这个提交包含在:
父节点
bd28d3bd3e
当前提交
812fd5d787
@ -0,0 +1,131 @@
|
|||||||
|
# 第一期 BUG 批量修复(4-20 部署)
|
||||||
|
|
||||||
|
**日期**:2026-04-20
|
||||||
|
**PR**:#956、#957、#958、#959、#960 + gateway #966、ddl hotfix #965/#967
|
||||||
|
|
||||||
|
本批次共修复 9 个后端 BUG 并部署测试环境,同时列出 6 个纯前端 BUG 需前端同学跟进。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 后端已修(前端大多无需改动)
|
||||||
|
|
||||||
|
### BUG 11 — 价格日历报 "month: 月份格式错误"(前端无感)
|
||||||
|
`@Pattern` 正则从 `\d{4}-\d{2}` 放宽为 `\d{4}-(0?[1-9]|1[0-2])`,后端 `YearMonth.parse(month, "yyyy-M")` 容错。
|
||||||
|
**前端**:传 `month=2026-5` 或 `2026-05` 都可,**自动兼容**。
|
||||||
|
|
||||||
|
### BUG 16 — "数据操作违反约束"不给字段信息
|
||||||
|
`GlobalExceptionHandler` 对 `DataIntegrityViolationException` 5 种模式关键字匹配:
|
||||||
|
- `Column 'xxx' cannot be null` → `字段 【xxx】 不能为空`
|
||||||
|
- `Data too long for column 'xxx'` → `字段 【xxx】 长度超限`
|
||||||
|
- `Duplicate entry ... for key 'xxx'` → `数据重复,索引: xxx`
|
||||||
|
- `Unknown column 'xxx'` → `数据库字段 【xxx】 不存在(schema drift)`
|
||||||
|
- 外键失败 → `外键约束失败:关联数据不存在`
|
||||||
|
|
||||||
|
**前端**:错误 message 字段更友好,可直接透传给用户。
|
||||||
|
|
||||||
|
### BUG 17 — 小蒙马上架报 "至少需要1个未来有效价格"
|
||||||
|
`ProductValidationService.validatePublish` 按 `productType` 分流:
|
||||||
|
- CORE/CUSTOM → 走 `product_price_calendar` 校验(原逻辑)
|
||||||
|
- GROUP → 走 `group_tour_batch.adult_price` 校验(匹配"班期表价格"语义)
|
||||||
|
|
||||||
|
**前端**:无改动。小蒙马产品上架流程可正常走通。
|
||||||
|
|
||||||
|
### BUG 3 — 小蒙马小程序详情 startPrice=null 导致预订按钮灰
|
||||||
|
`MpProductDetailAssembler` 同步分流。GROUP 产品走 `groupTourBatchService.findMinAdultPriceFuture`。
|
||||||
|
|
||||||
|
**前端**:无改动。小蒙马产品详情 `startPrice` 现在会正确返回班期最低成人价。
|
||||||
|
|
||||||
|
### BUG 7 — 里程自动计算偏短
|
||||||
|
根因:
|
||||||
|
1. 白名单只认 SCENIC/ACTIVITY/HOTEL,自定义节点被静默过滤
|
||||||
|
2. 集合地/解散地坐标从未参与
|
||||||
|
|
||||||
|
修复:
|
||||||
|
- `AmapDrivingService.COORD_NODE_TYPES` 重构:新增 `BYO_COORD_NODE_TYPES={CUSTOM,NOTE,TRANSPORT,PHOTOGRAPHY,FREE}`,自带 lng/lat 才参与
|
||||||
|
- `ItineraryAutoCalculator.autoCalculateDailyMileage` 入参加 `List<DayItem>`,按 dayNumber 抽 gather/dismissal 透传
|
||||||
|
|
||||||
|
**前端**:`DailyMileageCalcReqVO` 新增可选字段:
|
||||||
|
- `NodeRef.lng/lat`(BigDecimal 可空,BYO 节点带坐标才参与计算)
|
||||||
|
- `DayNodes.gatherPlace/dismissalPlace`(Object 可空,同高德行政区 JSON `{name,adcode,center,level}`)
|
||||||
|
|
||||||
|
**前端 TODO**:行程编排页保存"计算里程"接口时,把每天的 `gatherPlace`/`dismissalPlace` 和各节点的 `lng/lat` 一并传,里程会更准确。不传也向下兼容。
|
||||||
|
|
||||||
|
### BUG 9 — 添加/编辑景区保存报 500
|
||||||
|
schema drift:Entity 有 `photographyGuide` 字段,DB 无 `photography_guide` 列。
|
||||||
|
修复:DDL 加列(独立迁移脚本 `sql/alter_scenic_spot_add_photography_guide.sql`,测试环境已执行)。
|
||||||
|
|
||||||
|
**前端**:无改动。景区创建/编辑接口正常。
|
||||||
|
|
||||||
|
### BUG 13 — 定制师看不见自己建的主题
|
||||||
|
`ProductLineService.saveLine` 创建分支加 `setCreatedBy` 兜底:若请求头缺 `X-Admin-Id` → 抛 `BusinessException("未识别到管理员身份")`,避免 `createdBy` 落 NULL。
|
||||||
|
|
||||||
|
**前端**:管理端创建产品线时**务必确保** HTTP 请求头带 `X-Admin-Id`(或 `Authorization: Bearer <token>` 让后端从中提取)。
|
||||||
|
|
||||||
|
### BUG 14 — 新建产品版本后小程序端看不到(缓存)
|
||||||
|
`ProductHelperService.registerAfterCommitInternal` 的 afterCommit 回调改调 `evictAllMpCache`,除产品详情缓存外,额外清:
|
||||||
|
- `mp:product-line:detail:*`(产品线详情)
|
||||||
|
- `mp:products-by-line:*`(产品线下产品列表)
|
||||||
|
|
||||||
|
**前端**:无改动。新建/上架/下架产品后小程序端立即可见,不再需要等 10 分钟缓存过期。
|
||||||
|
|
||||||
|
### BUG 18 — 产品线封面改了小程序端不更新
|
||||||
|
`ProductLineService.saveLine/deleteLine` 加 afterCommit 清缓存(同 BUG 14 的 evictAllMpCache 无参版)。
|
||||||
|
|
||||||
|
**前端**:无改动。产品线封面/描述改动立即在小程序端可见。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔵 需前端跟进的 BUG
|
||||||
|
|
||||||
|
### BUG 1 — 主题页产品名错(前端取字段错误)
|
||||||
|
现状:主题页显示"6 天 5 晚 · 亲子游",应为后台配置的 `name="带孩子,看一次真正的草原。"`。
|
||||||
|
|
||||||
|
**后端确认**:`MpProductService:387-399` 的 `name` 字段取自 `product.name`,值正确。**前端取错了字段**(可能把 `subtitle` 或拼接字符串当标题)。
|
||||||
|
|
||||||
|
**前端 TODO**:确认主题页产品标题读的是 `product.name` 而非 `subtitle` 或其他拼接逻辑。
|
||||||
|
|
||||||
|
### BUG 2 — 产品详情页第一屏没有标签
|
||||||
|
后端 `MpProductDetailRespVO.tags` 字段存在,DB 实际数据都有(如 `["亲子", "研学", "摄影"]`)。测试环境已查明:DB 数据完整,后端正确返回。**前端未渲染**。
|
||||||
|
|
||||||
|
**前端 TODO**:检查产品详情首屏的 tags 渲染逻辑。
|
||||||
|
|
||||||
|
### BUG 5 — 用户端第二页面卡片缺 icon
|
||||||
|
`BrandCard` DTO 只有 `title/subtitle/coverUrl`,**无 `iconUrl` 字段**。
|
||||||
|
|
||||||
|
**前端 TODO + 确认**:前端是否**必需**独立的 icon 字段(而非复用 coverUrl 缩略图)?
|
||||||
|
- 如确需:后端需改 common-core DTO(影响全服务重部署,成本高),请明确反馈
|
||||||
|
- 如可不加:复用 coverUrl 即可
|
||||||
|
|
||||||
|
### BUG 6 — 发布产品弹窗仍报"未配置实际用车车型"
|
||||||
|
测试 jar 反编译已验证 PR #932 的新文案已生效。**浏览器缓存问题**。
|
||||||
|
|
||||||
|
**前端 TODO / 用户**:Ctrl+Shift+R 硬刷后重试。
|
||||||
|
|
||||||
|
### BUG 8 — 资源库添加酒店区域分类无法使用
|
||||||
|
后端 `HotelQueryRequest` 无 `district` 筛选字段,无"按 city 列 district"字典接口。
|
||||||
|
|
||||||
|
**前端 TODO**:在 F12 Network 看这个下拉实际调的是哪个接口(带什么参数),反馈后再判断是否需要后端补接口。
|
||||||
|
|
||||||
|
### BUG 12 — 每日集合地/解散地不用必填
|
||||||
|
后端 `ProductItinerarySaveReqVO.DayItem.gatherPlace/dismissalPlace` 是 `Object` 且无任何校验注解,**后端不强制必填**。前端 `*` 号只是 UI 装饰。
|
||||||
|
|
||||||
|
**前端 TODO**:去掉这两个字段 UI 上的 `*` 号。副作用:空集合地时后端不生成首尾路线点和线路概览(已 null-safe 不报错)。
|
||||||
|
|
||||||
|
### BUG 19 — 产品选定金比例后小程序没返回定金金额
|
||||||
|
后端 `product_basic` 表有 `deposit_ratio` 和 `deposit_amount` 列,`MpProductDetailRespVO.depositRatio`/`depositAmount` 字段存在,`MpProductService.calculateQuote` 完整计算定金。**测试环境验证后端字段完整**。
|
||||||
|
|
||||||
|
**前端 TODO**:小程序端检查是否正确解析/展示 `depositRatio` 和 `depositAmount` 两个字段。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DDL 执行清单(生产部署前运维参考)
|
||||||
|
|
||||||
|
| 脚本 | 库 | 必要性 |
|
||||||
|
|---|---|---|
|
||||||
|
| `sql/alter_scenic_spot_add_photography_guide.sql` | hl_resource_service | BUG 9 必须 |
|
||||||
|
| `sql/alter_product_route_info_add_map_mode.sql` | hl_product_service | BUG 10 必须 |
|
||||||
|
| `sql/alter_product_supplies_add_from_combo_id.sql` | hl_product_service | BUG 15 可选(审计用) |
|
||||||
|
| `sql/supplies_schema.sql`(新增 supplies_combo+item 两张表) | hl_resource_service | BUG 15 必须 |
|
||||||
|
| `hl-order-service-v2/sql/migrations/20260420_bug20_early_bird_multi_plan.sql` | hl_order_service_v2 | BUG 20 必须 |
|
||||||
|
|
||||||
|
**部署顺序**:先 DDL → 再重启相关服务。
|
||||||
@ -0,0 +1,145 @@
|
|||||||
|
# 早鸟优惠改造:产品 1:N 多方案 + 阶梯文案(BUG 20)
|
||||||
|
|
||||||
|
**日期**:2026-04-20
|
||||||
|
**PR**:#964 + hotfix #967
|
||||||
|
**影响**:管理端早鸟优惠管理页 + 小程序产品详情页 + 下单报价
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 业务变化
|
||||||
|
|
||||||
|
### 1. 一个产品可绑定多个早鸟方案
|
||||||
|
|
||||||
|
**改造前**:产品 1:1 方案。管理端创建第 2 个绑定同一产品的方案报错「以下产品已绑定其他早鸟计划,不能重复绑定」。
|
||||||
|
|
||||||
|
**改造后**:产品 1:N。可以配阶梯:
|
||||||
|
- 2 人打 9 折 (`minPeople=2, maxPeople=2, discountType=PERCENT, discountPercent=90`)
|
||||||
|
- 3-5 人打 85 折 (`minPeople=3, maxPeople=5, discountType=PERCENT, discountPercent=85`)
|
||||||
|
- 6 人及以上打 8 折 (`minPeople=6, maxPeople=null, discountType=PERCENT, discountPercent=80`)
|
||||||
|
|
||||||
|
报价时后端按订单人数自动匹配最优方案,落 `order_discount` 表。
|
||||||
|
|
||||||
|
### 2. 优惠类型扩展(3 种)
|
||||||
|
|
||||||
|
`discount_type` 字段新增:
|
||||||
|
|
||||||
|
| 值 | 含义 | 示例 | 需填字段 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `AMOUNT_TOTAL`(**默认**) | 整单立减 | 整单减 500 元 | `discountAmount` |
|
||||||
|
| `AMOUNT_PER_PERSON` | 每人立减 | 每人减 100 元 | `discountAmount` |
|
||||||
|
| `PERCENT` | 按比例打折 | 8 折 | `discountPercent` (0-100) |
|
||||||
|
|
||||||
|
**⚠️ 重要:老数据默认为 `AMOUNT_TOTAL`**(历史上运营侧约定 `discountAmount=500` 即整单立减 500,所以迁移时存量数据全部回正为 `AMOUNT_TOTAL`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接口变化
|
||||||
|
|
||||||
|
### 1. 创建/更新方案(兼容老调用,新字段可选)
|
||||||
|
|
||||||
|
**`POST /admin/order/early-bird`**
|
||||||
|
**`PUT /admin/order/early-bird/{planId}`**
|
||||||
|
|
||||||
|
`EarlyBirdPlanSaveReqVO` 新增字段:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
"planName": "阶梯档 3-5 人 85 折",
|
||||||
|
"startDate": "2026-04-01",
|
||||||
|
"endDate": "2026-12-31",
|
||||||
|
"enabled": true,
|
||||||
|
"productIds": [123456],
|
||||||
|
|
||||||
|
// 老字段
|
||||||
|
"minPeople": 3,
|
||||||
|
"discountAmount": 0, // AMOUNT_TOTAL/PER_PERSON 必填;PERCENT 可为 null
|
||||||
|
|
||||||
|
// 新字段
|
||||||
|
"maxPeople": 5, // NEW 最高人数(含),不传=不限
|
||||||
|
"discountType": "PERCENT", // NEW 默认 AMOUNT_TOTAL
|
||||||
|
"discountPercent": 85, // NEW PERCENT 时必填 (0<v<100)
|
||||||
|
"priority": 0 // NEW 同区间多命中决胜,默认 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 响应 VO 新增字段
|
||||||
|
|
||||||
|
`EarlyBirdPlanVO`(列表/详情)新增后端拼好的**文案字段**:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
"peopleRangeText": "3-5 人", // NEW 例:"2 人"/"3-5 人"/"6 人及以上"
|
||||||
|
"discountText": "85 折", // NEW 例:"9 折"/"每人立减 100 元"/"整单立减 500 元"
|
||||||
|
// ... 其他 4 个新业务字段(maxPeople/discountType/discountPercent/priority)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
前端可直接用 `peopleRangeText` 和 `discountText` 做展示,无需自己拼。
|
||||||
|
|
||||||
|
### 3. 不再校验产品唯一
|
||||||
|
|
||||||
|
`POST /admin/order/early-bird` 去掉 `以下产品已绑定其他早鸟计划,不能重复绑定` 校验。同一 `productId` 可出现在任意多个方案。
|
||||||
|
|
||||||
|
### 4. 小程序产品详情页新增字段
|
||||||
|
|
||||||
|
**`GET /mp/product/item/{id}`** 响应 `MpProductDetailRespVO` 新增:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
"earlyBirdTip": "最低 8 折起(共 3 档早鸟方案)", // 无人数上下文的概览文案,无绑定=null
|
||||||
|
"earlyBirdLadder": [ // 早鸟阶梯(按 min_people 升序)
|
||||||
|
{
|
||||||
|
"planId": "123",
|
||||||
|
"peopleRangeText": "2 人",
|
||||||
|
"discountText": "9 折",
|
||||||
|
"startDate": "2026-04-01",
|
||||||
|
"endDate": "2026-12-31"
|
||||||
|
},
|
||||||
|
{ "planId": "124", "peopleRangeText": "3-5 人", "discountText": "85 折", ... },
|
||||||
|
{ "planId": "125", "peopleRangeText": "6 人及以上", "discountText": "8 折", ... }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
前端可按 ladder 画阶梯图。老字段 `earlyBirdTip` 语义微调为「概览性描述」,不再只拿第 1 个方案。
|
||||||
|
|
||||||
|
### 5. 小程序下单按人数本地匹配
|
||||||
|
|
||||||
|
前端可用 `earlyBirdLadder` 按用户选择的人数本地计算匹配档位,展示「您当前 X 人,命中 'Y 人 Z 折';满 M 人可升级到 'N 折'」提示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前端 TODO
|
||||||
|
|
||||||
|
### 管理端早鸟优惠管理页
|
||||||
|
1. 方案编辑表单加 **4 个新输入项**:
|
||||||
|
- 最高人数(数字,可空=不限)
|
||||||
|
- 优惠类型(单选:整单立减/每人立减/按比例打折),默认**整单立减**
|
||||||
|
- 折扣率(0-100,仅"按比例打折"时启用)
|
||||||
|
- 优先级(数字,默认 0)
|
||||||
|
2. 原"优惠金额 discountAmount"改为仅在"整单立减/每人立减"时启用
|
||||||
|
3. 产品绑定改为**多选 tag**(去掉 1:1 限制,可一个产品绑多个方案)
|
||||||
|
4. 建议加"阶梯预览"区块显示该产品当前所有方案的覆盖情况,帮肉眼核对重叠/漏洞
|
||||||
|
5. 列表展示:用后端返回的 `peopleRangeText + discountText` 直接显示,不用前端拼
|
||||||
|
|
||||||
|
### 小程序产品详情页
|
||||||
|
1. 读 `earlyBirdLadder` 画阶梯图(3 档示意)
|
||||||
|
2. `earlyBirdTip` 作为默认概览展示(如"最低 8 折起")
|
||||||
|
3. 老字段 `earlyBirdText`、`earlyBirdPlanName`、`earlyBirdDiscount` 废弃(值可能为 null 或第 1 档概览),请改读 `earlyBirdLadder[0]`
|
||||||
|
|
||||||
|
### 小程序下单页
|
||||||
|
1. 用户选完人数 + 日期后本地按 `earlyBirdLadder` 匹配:
|
||||||
|
```
|
||||||
|
match = ladder.find(l => totalPeople >= l.minPeople && (l.maxPeople == null || totalPeople <= l.maxPeople))
|
||||||
|
```
|
||||||
|
2. 匹配到方案时,提示文案可参考:`"您当前 N 人,命中 '${peopleRangeText} ${discountText}'"`
|
||||||
|
3. 可顺带提示"更优档位":若 `ladder` 里有 `minPeople > totalPeople` 且折扣更大的,提示"满 X 人可享 Y"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 兼容性
|
||||||
|
|
||||||
|
- 老数据:全部默认 `discount_type=AMOUNT_TOTAL`,行为不变(整单立减)
|
||||||
|
- 老接口:`EarlyBirdPlanSaveReqVO` 新字段全部可选,旧版管理端提交不传也可
|
||||||
|
- 老订单:已支付订单不回溯,`order_discount` 持久化的 `discount_amount` 不变
|
||||||
|
- 废弃但保留:`earlyBirdText/earlyBirdPlanName/earlyBirdDiscount` 三个老字段继续下发(值按新逻辑计算),下个大版本下架
|
||||||
@ -0,0 +1,141 @@
|
|||||||
|
# 产品地图支持高德自动生成(BUG 10)
|
||||||
|
|
||||||
|
**日期**:2026-04-20
|
||||||
|
**PR**:#962
|
||||||
|
**影响**:管理端产品编辑 Step3(路线地图)+ 小程序产品详情页
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 业务变化
|
||||||
|
|
||||||
|
运营反馈:定制路线没人有时间做线路图。期望一键用高德自动生成路径图。
|
||||||
|
|
||||||
|
**能力**:产品地图新增 3 模式,互斥:
|
||||||
|
|
||||||
|
| mapMode | 含义 | route_map_url |
|
||||||
|
|---|---|---|
|
||||||
|
| `UPLOAD`(默认,老数据行为) | 手动上传图片 | 前端上传后的 OSS URL |
|
||||||
|
| `AMOUNT_AUTO` | 高德根据行程坐标自动生成 | 后端生成、上传后的 OSS URL |
|
||||||
|
| `NONE` | 隐藏该模块(小程序不展示) | 可保留或清空 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 新增接口
|
||||||
|
|
||||||
|
### 1. `POST /admin/product/item/{id}/map/generate-by-amap`
|
||||||
|
|
||||||
|
**一键触发**:后端取产品行程所有坐标(首天 `gatherPlace.center` → 每天 SCENIC/ACTIVITY/HOTEL 节点 → 末天 `dismissalPlace.center`),调高德静态地图 API,下载 PNG 后落 OSS,写回 `routeMapUrl` + `mapMode=AMAP_AUTO`。
|
||||||
|
|
||||||
|
**请求**(全部可选,均有默认值):
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
"size": "750*500", // 可选,默认 750*500
|
||||||
|
"scale": 2 // 可选,默认 2(高清)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
"mapUrl": "https://oss.../product-route-map/....png",
|
||||||
|
"mapMode": "AMAP_AUTO",
|
||||||
|
"pointCount": 8,
|
||||||
|
"generatedAt": "2026-04-20T12:34:56"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**异常场景**:
|
||||||
|
- 坐标 < 2 个 → 400 `行程坐标不足,无法生成地图,请先完善行程`
|
||||||
|
- 高德返回非图片 → 400 `高德地图生成失败: <错误详情>`
|
||||||
|
- OSS 上传失败 → 500
|
||||||
|
- 幂等:10 秒内重复调用会被拦截(`@Idempotent(10)`)
|
||||||
|
|
||||||
|
### 2. `PUT /admin/product/item/{id}/map/mode`
|
||||||
|
|
||||||
|
**切换地图模式**。
|
||||||
|
|
||||||
|
**请求**:
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
"mapMode": "NONE", // @Pattern: NONE|AMAP_AUTO|UPLOAD
|
||||||
|
"clearMapUrl": false // 可选,true 时同时清空 routeMapUrl
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**:`{"code":200,"message":"地图模式切换成功"}`
|
||||||
|
|
||||||
|
### 3. 老接口 `PUT /admin/product/item/{id}/route` 扩展
|
||||||
|
|
||||||
|
`ProductRouteSaveReqVO` 新增:
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
"routeMapUrl": "...",
|
||||||
|
"mapMode": "UPLOAD" // NEW 可选,前端手动上传后可一并传 UPLOAD
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
手动上传流程不变:前端走现有 `/admin/file/upload/token` 拿预签名 URL → 上传 OSS → 回填 `routeMapUrl`,顺带把 `mapMode` 设为 `UPLOAD`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 小程序详情页字段扩展
|
||||||
|
|
||||||
|
`MpProductDetailRespVO` 新增:
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
"routeMapUrl": "...",
|
||||||
|
"mapMode": "UPLOAD" // NEW NONE/AMAP_AUTO/UPLOAD
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**管理端详情 VO `ProductDetailRespVO.RouteInfoVO`** 新增 3 字段:
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
"mapMode": "AMAP_AUTO",
|
||||||
|
"mapGeneratedAt": "2026-04-20T12:34:56",
|
||||||
|
"mapAmapSnapshot": { // 高德生成快照,用于展示"最近生成时间"
|
||||||
|
"points": [...],
|
||||||
|
"size": "750*500",
|
||||||
|
"scale": 2,
|
||||||
|
"generatedAt": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Internal VO**(供订单服务 / mp 聚合服务 Feign 使用)也同步加了 `mapMode`:`InternalProductDetailVO.RouteInfoVO`、`InternalMpCustomProductDetailVO`、`MpProductDetailInternalVO`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前端 TODO
|
||||||
|
|
||||||
|
### 管理端产品编辑 Step3
|
||||||
|
1. "路线地图"模块顶部加 **3 选 1 切换**:
|
||||||
|
- 🙈 隐藏(`NONE`)→ 下方模块折叠
|
||||||
|
- 🗺️ 高德自动生成(`AMAP_AUTO`)→ 显示"用高德生成"按钮
|
||||||
|
- 📤 手动上传(`UPLOAD`)→ 显示上传组件
|
||||||
|
2. 切换模式调 `PUT /map/mode`
|
||||||
|
3. "用高德生成"按钮调 `POST /map/generate-by-amap`,成功后回显 URL + "最近生成于 X" 提示
|
||||||
|
4. 手动上传提示文案:**建议图片 ≤ 2MB,尺寸 4:3 或 3:2,格式 jpg/png**(后端目前不强校验,靠前端提示)
|
||||||
|
5. 编辑页可显示"行程已更新,建议重新生成地图"(若 `mapGeneratedAt < product.updateTime`)
|
||||||
|
|
||||||
|
### 小程序产品详情页
|
||||||
|
1. 读 `mapMode` 判断:
|
||||||
|
- `NONE` → 不渲染地图模块
|
||||||
|
- `AMAP_AUTO` / `UPLOAD` → 渲染 `routeMapUrl`
|
||||||
|
2. 支持 `routeMapUrl` 为空时兜底隐藏
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 部署前置(已在测试环境完成,生产部署前运维注意)
|
||||||
|
|
||||||
|
- [ ] Nacos `hl-product-service-v2-dev.yml` / `-prod.yml` 需配置 `amap.web-service-key`(与现有 AmapDrivingService 共用同一个 key)
|
||||||
|
- [ ] OSS 存储类 `product-route-map` groupKey 权限放通
|
||||||
|
- [ ] 先 DDL 后重启服务(`sql/alter_product_route_info_add_map_mode.sql`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 兼容性
|
||||||
|
|
||||||
|
- 老数据:`product_route_info.map_mode` 默认 `UPLOAD`,已有图片保留,小程序端展示不变
|
||||||
|
- 老接口:`ProductRouteSaveReqVO.mapMode` 可选,不传走原路径
|
||||||
|
- 老前端:不读 `mapMode` 也能工作(`routeMapUrl` 字段语义未变)
|
||||||
@ -0,0 +1,145 @@
|
|||||||
|
# 备品组合功能(BUG 15)
|
||||||
|
|
||||||
|
**日期**:2026-04-20
|
||||||
|
**PR**:#963 + gateway 路由 hotfix #966
|
||||||
|
**影响**:管理端备品管理(新菜单)+ 产品编辑 Step3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 业务变化
|
||||||
|
|
||||||
|
运营反馈备品单选操作繁琐,常用备品组合(如"亲子研学基础包"=N 个备品打包)每次新建产品都要重复选。
|
||||||
|
|
||||||
|
**改造**:支持"备品组合"实体。一个组合 = N 个备品。产品编辑时可引用组合 ID,后端保存时自动展开合并到 supplies。
|
||||||
|
|
||||||
|
**展开规则**:同 `suppliesResourceId` 的,**前端手工项优先**(手工传 quantity/unitPrice 覆盖组合项)。自定义项(`suppliesResourceId=null`)不参与去重全保留。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 新增接口:`/admin/supplies-combo/*`
|
||||||
|
|
||||||
|
| Method | Path | 说明 | 注解 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| POST | `/admin/supplies-combo` | 创建组合 | @Idempotent(5s) |
|
||||||
|
| PUT | `/admin/supplies-combo/{comboId}` | 编辑(主信息+明细整体替换) | @Lock4j |
|
||||||
|
| DELETE | `/admin/supplies-combo/{comboId}` | 软删 | @Lock4j |
|
||||||
|
| GET | `/admin/supplies-combo/page?pageNum=1&pageSize=10&name=xx&status=ENABLED` | 分页 | — |
|
||||||
|
| GET | `/admin/supplies-combo/{comboId}` | 详情(含 items 展开) | — |
|
||||||
|
| GET | `/admin/supplies-combo/enabled` | 下拉框(List,不分页,仅 ENABLED) | — |
|
||||||
|
| PUT | `/admin/supplies-combo/{comboId}/toggle-status` | 启用/禁用切换 | @Lock4j |
|
||||||
|
|
||||||
|
### 创建/编辑请求 VO
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
"name": "亲子研学基础包", // @NotBlank, @Size(max=128)
|
||||||
|
"description": "适用 3-8 岁儿童", // @Size(max=512)
|
||||||
|
"status": "ENABLED", // ENABLED/DISABLED
|
||||||
|
"sortOrder": 0,
|
||||||
|
"items": [ // @NotEmpty, 1-50 个
|
||||||
|
{ "suppliesId": 123, "quantity": 2, "sortOrder": 0 },
|
||||||
|
{ "suppliesId": 456, "quantity": 1, "sortOrder": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
校验:
|
||||||
|
- `items.suppliesId` 去重(同组合不能重复加)
|
||||||
|
- 每个 `suppliesId` 必须存在于 `supplies_item` 表且 `status=1`(启用中的备品)
|
||||||
|
|
||||||
|
### 响应 VO(详情)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
"comboId": "2046079787104866306",
|
||||||
|
"name": "亲子研学基础包",
|
||||||
|
"description": "...",
|
||||||
|
"status": "ENABLED",
|
||||||
|
"sortOrder": 0,
|
||||||
|
"itemCount": 2, // 明细数量(列表也有)
|
||||||
|
"items": [ // 详情才返回,列表为 null
|
||||||
|
{
|
||||||
|
"id": "1001", "suppliesId": "123", "quantity": 2, "sortOrder": 0,
|
||||||
|
"suppliesName": "睡袋", // 从 supplies_item 回填
|
||||||
|
"categoryCode": "OUTDOOR",
|
||||||
|
"billingType": "PER_PERSON",
|
||||||
|
"basePrice": 50.00
|
||||||
|
},
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"createdBy": 1, "createdAt": "...", "updatedAt": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 下拉 VO(enabled)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
[
|
||||||
|
{ "comboId": "123", "name": "亲子研学基础包", "itemCount": 5 },
|
||||||
|
{ "comboId": "456", "name": "摄影出行包", "itemCount": 3 }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 产品 Step3 保存扩展
|
||||||
|
|
||||||
|
### `PUT /admin/product/item/{id}/route` 请求 VO
|
||||||
|
|
||||||
|
`ProductRouteSaveReqVO` 新增:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
// 老字段
|
||||||
|
"supplies": [
|
||||||
|
{ "suppliesResourceId": 123, "suppliesName": "睡袋", "quantity": 2, ... }
|
||||||
|
],
|
||||||
|
|
||||||
|
// 新字段
|
||||||
|
"supplyComboIds": [789, 790] // NEW 可选,引用的备品组合 ID 列表
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**后端展开逻辑**(前端无感):
|
||||||
|
1. 读 `supplyComboIds` 调内部 Feign 批量查组合展开
|
||||||
|
2. 合并到 `supplies`:同 `suppliesResourceId` 手工优先(保留前端 quantity/unitPrice)
|
||||||
|
3. 自定义项(`suppliesResourceId=null`)不参与去重
|
||||||
|
4. 展开项会被打上 `fromComboId`(DB 层面审计用)
|
||||||
|
|
||||||
|
`SuppliesItem` 内部类新增字段(前端无需传,后端审计):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
...
|
||||||
|
"fromComboId": null // NEW 来源组合ID,NULL=单品,非空=组合展开
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前端 TODO
|
||||||
|
|
||||||
|
### 1. 原生 BUG 排查
|
||||||
|
运营反馈"备品栏点击后选项框一直存在、单选很麻烦"。推断是产品编辑 Step3 `SuppliesPicker` 组件的下拉关闭逻辑问题,请前端同学排查。
|
||||||
|
|
||||||
|
### 2. 新菜单:备品组合管理
|
||||||
|
在"备品管理"菜单同组新建"备品组合管理":
|
||||||
|
- 列表页:分页 + 名称搜索 + 启用状态筛选
|
||||||
|
- 新建/编辑:表单含名称/描述/状态/排序 + 备品多选 + 每个备品的数量
|
||||||
|
- 启停按钮
|
||||||
|
- 删除按钮(软删)
|
||||||
|
|
||||||
|
### 3. 产品编辑 Step3 增强
|
||||||
|
- 备品区块加"从组合选择"按钮
|
||||||
|
- 点击后弹出下拉选组合(调 `/admin/supplies-combo/enabled`)
|
||||||
|
- 用户选中后把 `comboId` 追加到 `supplyComboIds` 字段
|
||||||
|
- 保存时和 `supplies` 一起提交
|
||||||
|
- 可视化:展开后的备品列表(后端返回的)可以标出"来自组合"徽标(读 `fromComboId`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 兼容性
|
||||||
|
|
||||||
|
- 老数据:产品表 `product_supplies.from_combo_id` 默认 NULL
|
||||||
|
- 老产品保存(前端不传 `supplyComboIds`):行为零变化
|
||||||
|
- 老接口:`ProductRouteSaveReqVO.supplies` 字段保留不动
|
||||||
正在加载...
x
在新工单中引用
屏蔽一个用户