Files
happy-life-star/docs/superpowers/specs/2026-07-19-favorites-real-api-design.md
T

219 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
author: AI Assistant
created_at: 2026-07-19
purpose: 小程序收藏功能从 localStorage 走真实 API,在现有剧本/事件表增加 is_favorite 字段
---
# 小程序收藏功能真实 API 化设计
## 概述
当前小程序的「收藏」功能(剧本收藏、人生事件收藏)完全基于前端 localStorage
- 剧本:`uni.getStorageSync('script_favorites')` → 对象字典 `{ id: true/false }`
- 人生事件:`uni.getStorageSync('event_favorite_${id}')` → 单条布尔
后端没有真实的剧本收藏 API,人生事件收藏接口 `/lifeEvent/favorite-placeholder` 是假的(echo 请求参数 + `placeholder: true`)。
本次修复:在现有 `e_epic_script` 表和 `t_life_event` 表上新增 `is_favorite` 字段,新增真实 API,前端从 localStorage 切换到 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]` 读取 |
| `life-event/detail.vue:142` | 从 `current_life_event` localStorage 读整条事件数据 |
| `life-event/detail.vue:144` | `isFavorite``event_favorite_${id}` localStorage 读 |
| `life-event/detail.vue:315-316` | toggle 写/删 `event_favorite_${id}` localStorage |
### 后端
| 文件 | 问题 |
|---|---|
| `LifeEventController.java:153-163` | `/lifeEvent/favorite-placeholder` 是空壳,返回 echo + `placeholder: true` |
| 无剧本收藏 Controller | `/epicScript` 下没有任何收藏端点 |
## 设计方案
### 存储方案(已确认)
在现有表新增 `is_favorite` 字段:
```sql
ALTER TABLE e_epic_script ADD COLUMN is_favorite TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否收藏:0-否 1-是';
ALTER TABLE t_life_event ADD COLUMN is_favorite TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否收藏:0-否 1-是';
```
**选择这个方案的理由**
1. 不新增表,改动最小
2. `is_favorite` 是当前用户维度的标记;用户只操作自己的剧本/事件,不需要 user_id 维度
3. 现有 `listAll` / `page` 接口返回全量字段,新增 `isFavorite` 字段后前端可直接读取,不需要额外的"收藏夹"接口
4. 后续如需"按用户维度"收藏(如管理员视角),再迁移到独立表
### 后端改动
#### 1. 实体类
- `EpicScript.java` 新增:`private Boolean isFavorite;`
- `LifeEvent.java` 新增:`private Boolean isFavorite;`
MyBatis-Plus 自动映射下划线 `is_favorite` 到驼峰 `isFavorite`
#### 2. Controller API
**剧本**`EpicScriptController`):
```
POST /epicScript/favorite
入参:FavoriteRequest { Long id; Boolean favorite; }
出参:Result<Void>
逻辑:
1. 通过 id 查询剧本
2. 若不存在 → 抛业务异常("剧本不存在")
3. update is_favorite = favorite
4. return Result.ok()
```
**人生事件**`LifeEventController`):
```
POST /lifeEvent/favorite
入参:FavoriteRequest { Long id; Boolean favorite; }
出参:Result<Void>
逻辑:
1. 通过 id 查询事件
2. 若不存在 → 抛业务异常
3. update is_favorite = favorite
4. return Result.ok()
```
删除现有 `/lifeEvent/favorite-placeholder` 接口。
#### 3. Service 层
```java
// EpicScriptService
void setFavorite(Long id, Boolean favorite);
// LifeEventService
void setFavorite(Long id, Boolean favorite);
```
实现:`lambdaUpdate().set(EpicScript::getIsFavorite, favorite).eq(EpicScript::getId, id).update()`
#### 4. DTO
新增公共请求对象 `FavoriteRequest`
```java
public class FavoriteRequest {
@NotNull
private Long id;
@NotNull
private Boolean favorite;
}
```
放在 `common` 模块,两个 Controller 都能引用。
### 前端改动
#### 1. `stores/app.js`
新增两个 action
```js
async toggleScriptFavorite(id, favorite) {
await epicScriptService.setFavorite(id, favorite);
const script = this.scripts.find(s => String(s.id) === String(id));
if (script) script.isFavorite = favorite;
},
async toggleEventFavorite(id, favorite) {
await lifeEventService.setFavorite(id, favorite);
// 事件列表如果是 store 内状态,同步更新
}
```
#### 2. `services/epicScript.js`
新增:
```js
setFavorite: (id, favorite) => post('/epicScript/favorite', { id: String(id), favorite })
```
#### 3. `services/lifeEvent.js`
修改现有 `favoriteEvent` 实现:
```js
// 之前:post('/lifeEvent/favorite-placeholder', ...)
// 之后:post('/lifeEvent/favorite', { id, favorite })
```
#### 4. `ScriptLibraryView.vue`
- 删除 `const localFavorites = ref(uni.getStorageSync('script_favorites') || {})`
- 删除 `toggleFavorite` / `deleteScriptFromFavorites` 的 localStorage 写/删
- `isFavorite` 计算改为:`Boolean(script.isFavorite)`
- `toggleFavorite` 改为调 `store.toggleScriptFavorite(id, !script.isFavorite)`
#### 5. `life-event/detail.vue`
- 删除 `uni.getStorageSync('current_life_event')` 的回退读取(这是"把数据存到用户端"的另一处违规,在 sub-project B 一并清理)
- 删除 `uni.getStorageSync('event_favorite_${id}')` 读取
- `isFavorite` 改为 `computed(() => Boolean(displayEvent.value?.isFavorite))`
- toggle 改为调 `store.toggleEventFavorite(id, !isFavorite.value)``lifeEventService.favoriteEvent`
## 数据流
```
列表页 ScriptLibraryView
└─ fetchScripts() (GET /epicScript/listAll)
└─ 后端查 e_epic_script 含 is_favorite 字段
└─ 前端渲染 isFavorite
用户点收藏
└─ store.toggleScriptFavorite(id, true/false)
└─ POST /epicScript/favorite { id, favorite }
└─ 后端 update is_favorite
└─ 本地 state 同步,UI 响应式更新
详情页 life-event/detail
└─ fetchLifeEventDetail(id) (GET /lifeEvent/detail)
└─ 后端查 t_life_event 含 is_favorite
└─ 前端渲染 isFavorite
```
## 不影响范围
- 列表渲染、筛选、排序逻辑不变,只换数据源(localStorage → API 字段)
- 详情页展示逻辑不变(数据源变化在 sub-project B 清理)
- 其他页面的剧本/事件展示(如首页推荐)自动受益于新的 `isFavorite` 字段
## 数据库迁移
新增迁移 SQL 文件 `server/src/main/resources/db/migration/V{timestamp}__add_is_favorite.sql`
```sql
ALTER TABLE e_epic_script ADD COLUMN is_favorite TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否收藏:0-否 1-是';
ALTER TABLE t_life_event ADD COLUMN is_favorite TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否收藏:0-否 1-是';
```
如果项目不用 Flyway,则手动执行或写入项目约定的 SQL 目录。
## 验收标准
- [ ] 剧本收藏:点击心形图标 → 调用 `POST /epicScript/favorite` → 后端字段更新 → 刷新页面后仍保持状态
- [ ] 人生事件收藏:点击收藏 → 调用 `POST /lifeEvent/favorite` → 同上
- [ ] 取消收藏:再次点击 → 同上
- [ ] 列表页 `isFavorite` 来自 API,不再依赖 localStorage
- [ ] 详情页 `isFavorite` 来自 API,不再依赖 localStorage
- [ ] 浏览器 Console 无报错
- [ ] 后端 `mvn clean install` 编译通过
- [ ] 通过 `deploy.py backend` 部署到服务器后,在服务器环境验证