feat: MP+admin 产品详情返回备品清单含 coverUrl (#2266)

这个提交包含在:
API Changelog Bot 2026-05-14 16:31:16 +08:00
父节点 36a4f810b0
当前提交 f2bcc3a5e4

查看文件

@ -0,0 +1,126 @@
# product-v2: 小程序 + 管理端产品详情返回备品清单(含封面图)
> **服务**: hl-product-service-v2 (端口 8083) + hl-resource-service (端口 8082)
> **PR**: #2272
> **Issue**: #2266
> **日期**: 2026-05-14
> **影响范围**: 小程序产品详情新增 `supplies` 数组 / 管理端产品详情 `supplies[].coverUrl` 新增字段
---
## ⚠️ 关键变化
小程序产品详情接口此前**完全没返回备品清单**,前端无法展示。本次补齐:
1. **小程序详情 `GET /mp/product/{id}` 顶层新增 `supplies` 数组**(此前完全没有这个字段)
2. **管理端详情 `GET /admin/product/item/{id}` `supplies[]` 子对象新增 `coverUrl` 字段**
3. 数据库 `product_supplies` 表新增 `cover_url` 列,Flyway V20260514_001 已在测试服跑完一次性 JOIN 回填:**引用资源库 supplies_item 的备品 100% 命中(73/73)**
---
## 一、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 小程序产品详情 | GET | `/mp/product/{id}` | **新增字段** | 顶层 `supplies: SuppliesVO[]` |
| 2 | 管理端产品详情 | GET | `/admin/product/item/{id}` | 字段扩展 | `supplies[]` 子对象新增 `coverUrl` |
---
## 二、接口详情
### 1. 小程序产品详情 `GET /mp/product/{id}`
**VO**: `MpProductDetailRespVO.SuppliesVO`(新内嵌静态类)
#### 新增响应字段
```json
{
"code": 200,
"data": {
"productId": "2045623908178046978",
"name": "带孩子,看一次真正的草原。",
"...": "其他既有字段保持不变",
"supplies": [
{
"id": 2045635184023285762,
"suppliesName": "团队识别手环",
"category": "个人装备",
"coverUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/test/material/2026/03/07/xxx.png",
"sortOrder": 0
},
{
"id": 2045635184027480066,
"suppliesName": "防晒喷雾",
"category": "个人装备",
"coverUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/test/material/2026/03/07/yyy.png",
"sortOrder": 1
}
]
}
}
```
| 字段 | 类型 | 可能为 null | 说明 |
|------|------|------------|------|
| `supplies` | `List<SuppliesVO>``null` | ✅ | 产品没备品时整个字段为 `null`,前端按 null 兜底空数组即可 |
| `supplies[].id` | `Long`(雪花) | ❌ | 备品行 ID(C 端通常用不到,可忽略) |
| `supplies[].suppliesName` | `String` | ❌ | 备品名称(已是快照,展示用) |
| `supplies[].category` | `String``null` | ✅ | 分类(中文文本或字典编码均可能,示例 `个人装备`/`保暖装备`/`CAMPING` 等,**前端不要做硬枚举映射**,直接展示文本) |
| `supplies[].coverUrl` | `String``null` | ✅ | 备品封面图 URL(快照),自定义备品或历史回填失败可能为 null,**前端务必做默认占位图兜底** |
| `supplies[].sortOrder` | `Integer` | ❌ | 排序字段,后端已按 ASC 返回,前端按数组顺序渲染即可 |
#### 排序
后端 `selectByProductId``orderByAsc(sortOrder)` 排序,前端直接按数组顺序展示。
### 2. 管理端产品详情 `GET /admin/product/item/{id}`
**VO**: `ProductDetailRespVO.SuppliesVO`(既有内嵌类,仅扩展字段)
`supplies[]` 子对象新增 `coverUrl: String`,语义同上(快照,可能为 null)。其他既有字段(`id` / `productId` / `suppliesResourceId` / `suppliesName` / `category` / `hasCost` / `billingType` / `unitPrice` / `quantity` / `sortOrder`)保持不变。
---
## 三、前端兜底要求
**重要**:`coverUrl` 字段**可能为 null**,出现场景:
1. **手工添加的自定义备品**(没勾选资源库 supplies_item,只填了名字) — 后端没有源头查 cover,直接 null
2. **历史数据中 supplies_item 关联资源已删 / material 已删 / oss_url 为空** — 一次性 JOIN 回填时被防御逻辑排除
**前端必须做默认占位图**,例如:
```js
const cover = supply.coverUrl || '/static/supplies-default.png';
```
或 CSS:
```css
.supplies-cover { background: url(/static/supplies-default.png) no-repeat center; }
.supplies-cover img { object-fit: cover; }
```
测试服回填命中率参考:**174 条 product_supplies 中 73 条有 supplies_resource_id,全部命中真实 OSS URL;另外 101 条是手工/旧数据,coverUrl 为 null**。
---
## 四、已知限制(后续优化)
- **手工(非组合)单品在编辑保存时 coverUrl 暂留 null**:本次未引入新 Feign,只通过 DB 一次性回填覆盖历史,新增/编辑手工备品 coverUrl 仍是 NULL,**前端兜底是必须的**
- **从备品组合("从组合选择"按钮)添加的备品 coverUrl 在保存时自动同步**:`SuppliesComboService.expandBatch` 已接入 MaterialService 批量解析,组合展开自然带 URL
---
## 五、测试服真测证据
```bash
# 测试服 9443 网关
curl https://api.test.1814.love:9443/mp/product/2045623908178046978
```
返回顶层 `supplies` 数组 4 条全部带 coverUrl 真实 OSS URL,按 sortOrder 0→1→2→3 升序。
Admin 端用 `test_admin (admin_id=1002)` 测试 productId=2053665899663024129(用户截图同一产品):3 条备品(团队识别手环 / 防晒喷雾 / 便携矿泉水)全部带 coverUrl ✅