# mp 收藏 / 浏览足迹接口: 字段全回填 + 新增产品类型字段 + 修复浏览记录保存后查不出

> **服务**: hl-mp-service (端口 8085)
> **PR**: #1338
> **Issue**: 无关联工单（Gitea API 异常未能建单，已对齐口头确认）
> **日期**: 2026-04-24
> **影响范围**: 小程序 收藏列表 + 浏览足迹列表 + 浏览记录保存接口

---

## ⚠️ 关键变化

1. **前一版 `/mp/user/favorite` 和 `/mp/user/footprint` 的 `records` 中 `targetType=PRODUCT`（或 `resourceType=PRODUCT`）的行，`name`/`coverUrl`/`tags` 全是 `null`**。本版修复后全部正确回填。
2. `FavoriteItemVO` 和 `FootprintItemVO` **新增** `productType` + `productTypeLabel` 两字段（仅 PRODUCT 类型有值，其它类型 `null`）。前端若展示收藏/足迹卡片带产品类型（核心/小蒙马/私人定制）角标，直接用 `productTypeLabel` 即可，不用自己查字典。
3. 保存足迹接口 `POST /mp/user/footprint` **修复隐性失败**：原实现下，用户"清空浏览记录"后再次访问同一资源时，接口返 200 但 DB 实际未保存记录，导致"保存成功却查不出来"。现在会正确复活已软删的记录。
4. `footprintId` / `favoriteId` 在响应 JSON 中**全栈改为 String 序列化**（此前是 number，19 位雪花 ID 会被 JS 精度截断）。前端若以字符串接收，无感知；若以 number 运算需调整。

---

## 一、背景

`MpProductFeignClient.getProductBrief` Feign 路径历史写错，指向 `/internal/product/batch-brief`（班期摘要接口，返回 `BatchBriefVO` 只有 batchId/batchCode/departureDate 等班期字段），**没有 name/coverUrl/tags**。导致 mp BFF `FavoriteAggregationService` 拿到空 list → 字段全 null。资源类型（SCENIC/RESTAURANT/ACTIVITY）的三个 brief 端点无此问题。

浏览记录的问题则是 `addFootprint` 服务方法缺软删复活逻辑：清空浏览走 `@TableLogic` 把 `deleted_at` 置 NOW，下次访问时 `getOne` 自动加 `WHERE deleted_at IS NULL` 查不到 existing，进入 `save` 分支后撞上 DB 唯一索引（不含 deleted_at）冲突被静默吞。

---

## 二、变更接口清单

| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 收藏列表 | GET | `/mp/user/favorite` | 响应字段补全 + 新增 | PRODUCT 行 `name`/`coverUrl`/`tags` 不再为 null；新增 `productType` + `productTypeLabel` |
| 2 | 浏览足迹列表 | GET | `/mp/user/footprint` | 响应字段补全 + 新增 | 同上 |
| 3 | 保存浏览记录 | POST | `/mp/user/footprint` | 行为修复 | 清空浏览后再次访问同资源不再静默失败 |

路径/方法/参数**无变化**。

---

## 三、字段变化详情

### 3.1 `/mp/user/favorite` 响应 `records[*]` 字段

| 字段 | 类型 | 之前 | 之后 | 备注 |
|------|------|------|------|------|
| `favoriteId` | String | String | String | 无变 |
| `targetType` | String | String | String | 无变，枚举 `PRODUCT` / `SCENIC` / `RESTAURANT` / `ACTIVITY` |
| `targetId` | String | String | String | 无变 |
| `name` | String | PRODUCT 时为 `null` | **全类型正确返回** | PRODUCT 取产品名称；其它类型取资源名称 |
| `coverUrl` | String | PRODUCT 时为 `null` | **全类型正确返回** | PRODUCT 取产品封面图；其它类型取资源封面图 |
| `tags` | List\<String\> | PRODUCT 时为 `null` | **全类型正确返回** | 仅存在标签时有值 |
| `productType` | String | **不存在** | **新增** | 仅 PRODUCT 有值，枚举 `CORE` / `GROUP` / `CUSTOM`（字典 `product_type`） |
| `productTypeLabel` | String | **不存在** | **新增** | 仅 PRODUCT 有值，中文，如 `核心产品` / `小蒙马` / `私人定制` |
| `createdAt` | String | String | String | 无变 |

### 3.2 `/mp/user/footprint` 响应 `records[*]` 字段

同 `/mp/user/favorite`，除主键字段叫 `footprintId`、业务类型字段叫 `resourceType`/`resourceId`。

### 3.3 `POST /mp/user/footprint` 响应
响应结构未变（仍为 `Result<void>` 或 `Result<Footprint>`，视既有契约）。仅内部实现行为修复。

---

## 四、示例

### 4.1 修复前（已观察实际响应）
```json
{
  "code": 200,
  "data": {
    "records": [
      {
        "favoriteId": "2047265158492815361",
        "targetType": "PRODUCT",
        "targetId": "2044306857534636034",
        "name": null,
        "coverUrl": null,
        "tags": null,
        "createdAt": "2026-04-23 18:43:45"
      }
    ],
    "total": 1
  }
}
```

### 4.2 修复后
```json
{
  "code": 200,
  "data": {
    "records": [
      {
        "favoriteId": "2047265158492815361",
        "targetType": "PRODUCT",
        "targetId": "2044306857534636034",
        "name": "冻干粉发短信给",
        "coverUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/xxx.jpg",
        "tags": ["亲子", "研学", "露营"],
        "productType": "GROUP",
        "productTypeLabel": "小蒙马",
        "createdAt": "2026-04-23 18:43:45"
      }
    ],
    "total": 1
  }
}
```

---

## 五、前端建议

1. 卡片 UI 如展示产品类型角标（核心/小蒙马/定制），直接用 `productTypeLabel`，不用前端再拉字典翻译。
2. `productType`/`productTypeLabel` 在非 PRODUCT 类型（SCENIC/RESTAURANT/ACTIVITY）上是 `null`，渲染需判空。
3. 原本用 `name?.trim() || "加载中..."` 等兜底文案的地方可以简化；字段现在一定不为 null（除非产品已被删除，此时 brief 返空 → 仍为 null，需保留兜底）。
4. `productTypeLabel` 来自字典 `product_type`，字典 value→label 映射：`CORE→核心产品`、`GROUP→小蒙马`、`CUSTOM→私人定制`。若字典中文名称后续调整，后端会自动跟随，前端无需改动。
