docs: 收藏功能真实 API 化设计文档 v2(沿用独立收藏表架构)
This commit is contained in:
@@ -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<String>)` — 批量检查(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<EventFavorite> {
|
||||
}
|
||||
```
|
||||
|
||||
**Service**:`EventFavoriteService.java` + `EventFavoriteServiceImpl.java`
|
||||
|
||||
```java
|
||||
public interface EventFavoriteService extends IService<EventFavorite> {
|
||||
EventFavoriteResponse toggleFavorite(String eventId);
|
||||
PageResult<LifeEventResponse> getFavoritePage(long current, long size);
|
||||
Set<String> getFavoritedEventIds(Set<String> 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<EventFavoriteResponse> toggleFavorite(
|
||||
@Valid @RequestBody EventFavoriteToggleRequest request) {
|
||||
return Result.success(eventFavoriteService.toggleFavorite(request.getEventId()));
|
||||
}
|
||||
|
||||
@GetMapping(value = "/favorite/page")
|
||||
public Result<PageResult<LifeEventResponse>> getFavoritePage(
|
||||
@RequestParam(defaultValue = "1") long current,
|
||||
@RequestParam(defaultValue = "10") long size) {
|
||||
return Result.success(eventFavoriteService.getFavoritePage(current, size));
|
||||
}
|
||||
|
||||
@GetMapping(value = "/favorite/check")
|
||||
public Result<EventFavoriteResponse> 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` 部署到服务器后,在服务器环境验证
|
||||
Reference in New Issue
Block a user