From bebb7f166758be6cf0d220f30991cd966e22a087 Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 19 Jul 2026 22:16:15 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=94=B6=E8=97=8F=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=20API=20=E5=8C=96=E8=AE=BE=E8=AE=A1=E6=96=87?= =?UTF-8?q?=E6=A1=A3=20v2=EF=BC=88=E6=B2=BF=E7=94=A8=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E6=94=B6=E8=97=8F=E8=A1=A8=E6=9E=B6=E6=9E=84=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-07-19-favorites-real-api-design.md | 270 ++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-19-favorites-real-api-design.md diff --git a/docs/superpowers/specs/2026-07-19-favorites-real-api-design.md b/docs/superpowers/specs/2026-07-19-favorites-real-api-design.md new file mode 100644 index 0000000..7b6d8c6 --- /dev/null +++ b/docs/superpowers/specs/2026-07-19-favorites-real-api-design.md @@ -0,0 +1,270 @@ +--- +author: AI Assistant +created_at: 2026-07-19 +purpose: 小程序收藏功能真实 API 化,沿用现有独立收藏表架构 +--- + +# 小程序收藏功能真实 API 化设计(v2) + +## 概述 + +当前小程序的「收藏」功能(剧本收藏、人生事件收藏)完全基于前端 localStorage,未对接后端 API。 + +**重要发现**:剧本收藏后端 API 已经完整实现(独立的 `t_script_favorite` 表 + `ScriptFavoriteService` + Controller),但前端 `ScriptLibraryView.vue` 没有使用这些 API,还在用 localStorage。 + +本次修复: +1. **剧本收藏**:前端从 localStorage 切换到现有 API +2. **人生事件收藏**:仿照剧本收藏,新建独立的收藏表和 API,删除假接口 + +## 现状问题 + +### 剧本收藏 + +| 位置 | 问题 | +|---|---| +| `ScriptLibraryView.vue:159` | `localFavorites = ref(uni.getStorageSync('script_favorites') \|\| {})` | +| `ScriptLibraryView.vue:319-369` | `toggleFavorite` / `deleteScriptFromFavorites` 只写 localStorage | +| `ScriptLibraryView.vue:244` | `isFavorite` 从 `script.isFavorite \|\| localFavorites.value[id]` 读取 | +| `services/epicScript.js:206-232` | ✅ 已有 `toggleFavoriteScript` / `checkFavoriteScript` API 封装,但前端未调用 | + +**后端已实现**: +- `POST /epicScript/favorite/toggle` — 切换收藏 +- `GET /epicScript/favorite/check?scriptId=xxx` — 检查单个剧本收藏状态 +- `GET /epicScript/favorite/page` — 分页查询收藏列表 +- `ScriptFavoriteService.getFavoritedScriptIds(Set)` — 批量检查(Controller 未暴露) + +### 人生事件收藏 + +| 位置 | 问题 | +|---|---| +| `life-event/detail.vue:144` | `isFavorite` 从 `event_favorite_${id}` localStorage 读 | +| `life-event/detail.vue:315-316` | toggle 写/删 `event_favorite_${id}` localStorage | +| `services/lifeEvent.js:67-69` | `favoriteEvent` 调用 `/lifeEvent/favorite-placeholder` 假接口 | +| `LifeEventController.java:153-163` | `/lifeEvent/favorite-placeholder` 是空壳,返回 echo + `placeholder: true` | + +## 设计方案 + +### 存储方案(沿用现有架构) + +**剧本收藏**:沿用现有 `t_script_favorite` 独立表(无需改动)。 + +**人生事件收藏**:仿照剧本,新建独立的 `t_event_favorite` 表。 + +```sql +CREATE TABLE t_event_favorite ( + id VARCHAR(64) PRIMARY KEY, + user_id VARCHAR(64) NOT NULL COMMENT '用户ID', + event_id VARCHAR(64) NOT NULL COMMENT '事件ID', + create_by VARCHAR(64) COMMENT '创建人', + create_time DATETIME COMMENT '创建时间', + update_by VARCHAR(64) COMMENT '更新人', + update_time DATETIME COMMENT '更新时间', + is_deleted TINYINT(1) DEFAULT 0 COMMENT '是否删除:0-否 1-是', + remarks VARCHAR(500) COMMENT '备注', + INDEX idx_user_event (user_id, event_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='人生事件收藏关联表'; +``` + +**选择独立表的理由**: +1. 与剧本收藏架构保持一致,两套收藏机制对称 +2. 支持多用户场景(每个用户独立收藏) +3. 支持分页查询"我的收藏"列表 +4. 逻辑删除保留历史记录 + +### 后端改动 + +#### 1. 人生事件收藏(新增) + +**Entity**:`EventFavorite.java` + +```java +@Data +@EqualsAndHashCode(callSuper = true) +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_event_favorite") +public class EventFavorite extends BaseEntity { + @TableField("user_id") + private String userId; + + @TableField("event_id") + private String eventId; +} +``` + +**Mapper**:`EventFavoriteMapper.java` + +```java +@Mapper +public interface EventFavoriteMapper extends BaseMapper { +} +``` + +**Service**:`EventFavoriteService.java` + `EventFavoriteServiceImpl.java` + +```java +public interface EventFavoriteService extends IService { + EventFavoriteResponse toggleFavorite(String eventId); + PageResult getFavoritePage(long current, long size); + Set getFavoritedEventIds(Set eventIds); + boolean isFavorited(String eventId); + long getFavoriteCount(); +} +``` + +实现完全仿照 `ScriptFavoriteServiceImpl`。 + +**DTO**: +- `EventFavoriteToggleRequest.java`:`{ eventId: String }` +- `EventFavoriteResponse.java`:`{ eventId, isFavorited, favoriteTime }` + +**Controller**:`LifeEventController.java` 新增 + +```java +// ==================== 人生事件收藏 ==================== + +@PostMapping(value = "/favorite/toggle") +public Result toggleFavorite( + @Valid @RequestBody EventFavoriteToggleRequest request) { + return Result.success(eventFavoriteService.toggleFavorite(request.getEventId())); +} + +@GetMapping(value = "/favorite/page") +public Result> getFavoritePage( + @RequestParam(defaultValue = "1") long current, + @RequestParam(defaultValue = "10") long size) { + return Result.success(eventFavoriteService.getFavoritePage(current, size)); +} + +@GetMapping(value = "/favorite/check") +public Result checkFavorite(@RequestParam String eventId) { + boolean favorited = eventFavoriteService.isFavorited(eventId); + EventFavoriteResponse r = new EventFavoriteResponse(); + r.setEventId(eventId); + r.setIsFavorited(favorited); + return Result.success(r); +} +``` + +**删除**:`/lifeEvent/favorite-placeholder` 假接口 + +#### 2. 剧本收藏(无需改动) + +后端已完整,无需新增。 + +### 前端改动 + +#### 1. `ScriptLibraryView.vue` + +**删除**: +- `const localFavorites = ref(uni.getStorageSync('script_favorites') || {})` +- `toggleFavorite` / `deleteScriptFromFavorites` 的 localStorage 写/删逻辑 + +**修改**: +- `isFavorite` 计算改为:调用 `epicScriptService.checkFavoriteScript(script.id)` 或在列表加载后批量检查 +- `toggleFavorite` 改为调 `epicScriptService.toggleFavoriteScript(script.id)` + +**优化**:为避免 N+1 请求,可在后端新增 `GET /epicScript/favorite/ids` 接口,返回当前用户所有收藏的 scriptId 集合,前端列表加载后一次性拿到。但本次先用 `checkFavoriteScript` 逐个检查,后续优化。 + +#### 2. `services/lifeEvent.js` + +**修改**: +```js +// 之前:post('/lifeEvent/favorite-placeholder', { id, favorite }) +// 之后: +export const toggleFavoriteEvent = async (eventId) => { + const res = await post('/lifeEvent/favorite/toggle', { eventId }) + return { + success: true, + isFavorited: res.data?.isFavorited ?? false, + favoriteTime: res.data?.favoriteTime ?? null + } +} + +export const checkFavoriteEvent = async (eventId) => { + const res = await get('/lifeEvent/favorite/check', { eventId }) + return res.data?.isFavorited ?? false +} +``` + +**删除**:旧的 `favoriteEvent` 方法 + +#### 3. `life-event/detail.vue` + +**删除**: +- `const isFavorite = ref(Boolean(uni.getStorageSync('event_favorite_${id}')))` +- `toggleFavorite` 的 localStorage 写/删 + +**修改**: +- `isFavorite` 改为 `ref(false)`,页面加载时调用 `checkFavoriteEvent(id)` 初始化 +- `toggleFavorite` 改为调 `toggleFavoriteEvent(id)` + +## 数据流 + +``` +剧本列表页 ScriptLibraryView + └─ getScriptList() (GET /epicScript/listAll) + └─ 返回剧本列表 + └─ 对每个剧本调用 checkFavoriteScript(id) (GET /epicScript/favorite/check) + └─ 返回 isFavorited + └─ 前端渲染收藏状态 + +用户点剧本收藏 + └─ toggleFavoriteScript(id) (POST /epicScript/favorite/toggle) + └─ 后端 t_script_favorite 表 insert/delete + └─ 返回 isFavorited + └─ 前端更新 UI + +人生事件详情页 life-event/detail + └─ getEventById(id) (GET /lifeEvent/detail) + └─ 返回事件详情 + └─ checkFavoriteEvent(id) (GET /lifeEvent/favorite/check) + └─ 返回 isFavorited + └─ 前端渲染收藏状态 + +用户点事件收藏 + └─ toggleFavoriteEvent(id) (POST /lifeEvent/favorite/toggle) + └─ 后端 t_event_favorite 表 insert/delete + └─ 返回 isFavorited + └─ 前端更新 UI +``` + +## 不影响范围 + +- 列表渲染、筛选、排序逻辑不变,只换数据源(localStorage → API) +- 详情页展示逻辑不变(数据源变化在 sub-project B 清理) +- 其他页面的剧本/事件展示(如首页推荐)不受影响 + +## 数据库迁移 + +新增迁移 SQL 文件 `server/src/main/resources/db/migration/V20260719000002__create_event_favorite.sql`: + +```sql +CREATE TABLE t_event_favorite ( + id VARCHAR(64) PRIMARY KEY, + user_id VARCHAR(64) NOT NULL COMMENT '用户ID', + event_id VARCHAR(64) NOT NULL COMMENT '事件ID', + create_by VARCHAR(64) COMMENT '创建人', + create_time DATETIME COMMENT '创建时间', + update_by VARCHAR(64) COMMENT '更新人', + update_time DATETIME COMMENT '更新时间', + is_deleted TINYINT(1) DEFAULT 0 COMMENT '是否删除:0-否 1-是', + remarks VARCHAR(500) COMMENT '备注', + INDEX idx_user_event (user_id, event_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='人生事件收藏关联表'; +``` + +如果项目不用 Flyway,则手动执行或写入项目约定的 SQL 目录。 + +## 验收标准 + +- [ ] 剧本收藏:点击心形图标 → 调用 `POST /epicScript/favorite/toggle` → 后端字段更新 → 刷新页面后仍保持状态 +- [ ] 人生事件收藏:点击收藏 → 调用 `POST /lifeEvent/favorite/toggle` → 同上 +- [ ] 取消收藏:再次点击 → 同上 +- [ ] 列表页 `isFavorite` 来自 API,不再依赖 localStorage +- [ ] 详情页 `isFavorite` 来自 API,不再依赖 localStorage +- [ ] `/lifeEvent/favorite-placeholder` 假接口已删除 +- [ ] 浏览器 Console 无报错 +- [ ] 后端 `mvn clean install` 编译通过 +- [ ] 通过 `deploy.py backend` 部署到服务器后,在服务器环境验证