# 小程序首页 Banner - 响应新增 linkTarget 嵌套对象

- **日期**: 2026-04-19
- **PR**: [#890](https://git.1814.love:8443/wx/HL/pulls/890) (Closes #889)
- **类型**: FEATURE
- **状态**: 已合并到 dev + 测试环境部署完成
- **服务**: hl-user-service + hl-product-service-v2
- **接口**: `GET /mp/banner/active`
- **兼容性**: **向后兼容**（仅新增字段，旧字段全部保留）

---

## 一、背景

小程序拿到 `/mp/banner/active` 列表后，当用户点到某条 Banner，前端还要再发一次产品/产品线详情请求才能展示封面、季节、产品类型，白屏等待感明显。

后端一把返回 Banner 展示所需的关联目标轻量信息，前端零二次请求。

---

## 二、响应新增字段

### 顶层新增 `linkTarget`

Banner 列表每个元素在原有字段基础上新增：

```jsonc
{
  "id": "1",
  "title": "小蒙马亲子专题",
  "imageUrl": "https://cdn.example.com/banner1.jpg",
  "linkType": "PRODUCT_LINE",
  "linkId": "1001",
  "linkTargetType": "GROUP",        // 保留,兼容旧前端
  "linkTargetName": "小蒙马亲子",    // 保留,admin 填的展示名

  // ⬇⬇⬇ 新增嵌套对象
  "linkTarget": {
    "type": "PRODUCT_LINE",
    "id": 1001,
    "name": "小蒙马亲子",
    "subtitle": null,
    "coverImageUrl": "https://cdn.example.com/line-cover.jpg",
    "productType": "GROUP",
    "seasons": ["spring", "summer"],
    "lineId": null
  }
}
```

### `linkTarget` 字段明细

| 字段 | 类型 | 说明 | 产品线 | 产品 |
|------|------|------|--------|------|
| `type` | string | `PRODUCT` \| `PRODUCT_LINE` | ✅ | ✅ |
| `id` | long | 目标 ID（产品 ID 或产品线 ID） | ✅ | ✅ |
| `name` | string | 名称 | ✅ | ✅ |
| `subtitle` | string \| null | 副标题 | null | ✅ |
| `coverImageUrl` | string \| null | 封面图 URL | ✅ | ✅ |
| `productType` | string | `CORE` / `GROUP` / `CUSTOM` | ✅ | ✅ |
| `seasons` | string[] | 适用季节，字典 `product_season`：`spring` / `summer` / `autumn` / `winter`（**小写**） | ✅ | ✅ |
| `lineId` | long \| null | 所属产品线 ID | null | ✅ |

### `linkTarget=null` 的情况

以下场景 `linkTarget` 整体为 `null`，前端按「无卡片详情可展示」处理即可（原有字段 `title` / `imageUrl` / `linkType` / `linkId` 照常渲染和跳转）：

- `linkType` 不是 `PRODUCT` 或 `PRODUCT_LINE`（例如 `SCENIC` / `PAGE` / `WEBVIEW` / `NONE`）
- 关联目标已被删除或下架
- 后端 Feign 降级（产品服务短暂不可用）

---

## 三、前端集成建议

### 卡片展示

```js
// 列表渲染时
banners.forEach(banner => {
  const target = banner.linkTarget;
  if (target) {
    // 可直接用 target.name / target.coverImageUrl / target.seasons 渲染副卡
    renderTargetCard({
      cover: target.coverImageUrl,
      title: target.name,
      subtitle: target.subtitle,
      tags: target.seasons,  // ['SPRING', 'SUMMER']
    });
  } else {
    // 仅渲染 Banner 主图,点击跳 linkType + linkId
  }
});
```

### 季节文案映射（前端字典）

```js
const SEASON_LABEL = {
  spring: '春季',
  summer: '夏季',
  autumn: '秋季',
  winter: '冬季',
};
// target.seasons.map(s => SEASON_LABEL[s]) → ['春季', '夏季']
```

### 跳转路径

跳转规则**不变**：继续用外层的 `linkType` + `linkId` 决定跳转目标路由。`linkTarget` 只用于 Banner 上的视觉展示。

---

## 四、其它说明

- **旧字段全部保留**：`linkType` / `linkId` / `linkTargetType` / `linkTargetName` 行为不变，前端原有兼容逻辑不用动
- **缓存**：Banner 列表带 30 分钟 Redis 缓存 + 事务提交后自动 evict，管理员改完 Banner 小程序下次拉取就能看到最新 `linkTarget`
- **admin 管理端接口未受影响**：banner 创建/编辑/列表接口未变（admin 只用 `linkTargetType` 做合法性提示，不返回 `linkTarget`）
