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

940 lines
27 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: 小程序收藏功能真实 API 化实施计划
---
# 收藏功能真实 API 化实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 前端收藏功能从 localStorage 切换到真实 API。剧本收藏沿用现有后端 API,人生事件收藏新建独立表和 API。
**Architecture:** 剧本收藏后端已完整(`t_script_favorite` 表 + `ScriptFavoriteService`),前端 `ScriptLibraryView.vue` 未对接。人生事件收藏仿照剧本,新建 `t_event_favorite` 表 + `EventFavoriteService` + Controller,删除假接口 `/lifeEvent/favorite-placeholder`。前端所有 localStorage 收藏逻辑切换到 API。
**Tech Stack:** Spring Boot 2.7.18 + MyBatis-Plus 3.5.3.1 + UniApp Vue 3 + Pinia + Element Plus + rpx
---
### Task 1: 后端人生事件收藏功能(Entity + Mapper + Service + Controller
**Files:**
- Create: `server/src/main/resources/db/migration/V20260719000002__create_event_favorite.sql`
- Create: `server/src/main/java/com/emotion/entity/EventFavorite.java`
- Create: `server/src/main/java/com/emotion/mapper/EventFavoriteMapper.java`
- Create: `server/src/main/java/com/emotion/dto/request/EventFavoriteToggleRequest.java`
- Create: `server/src/main/java/com/emotion/dto/response/EventFavoriteResponse.java`
- Create: `server/src/main/java/com/emotion/service/EventFavoriteService.java`
- Create: `server/src/main/java/com/emotion/service/impl/EventFavoriteServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/controller/LifeEventController.java`
- [ ] **Step 1: 创建数据库迁移 SQL**
```sql
-- server/src/main/resources/db/migration/V20260719000002__create_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='人生事件收藏关联表';
```
- [ ] **Step 2: 创建 EventFavorite 实体类**
```java
// server/src/main/java/com/emotion/entity/EventFavorite.java
package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.emotion.common.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
/**
* 人生事件收藏关联实体
*
* @author AI Assistant
* @date 2026-07-19
*/
@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;
}
```
- [ ] **Step 3: 创建 EventFavoriteMapper**
```java
// server/src/main/java/com/emotion/mapper/EventFavoriteMapper.java
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.EventFavorite;
import org.apache.ibatis.annotations.Mapper;
/**
* 人生事件收藏 Mapper
*
* @author AI Assistant
* @date 2026-07-19
*/
@Mapper
public interface EventFavoriteMapper extends BaseMapper<EventFavorite> {
}
```
- [ ] **Step 4: 创建 EventFavoriteToggleRequest DTO**
```java
// server/src/main/java/com/emotion/dto/request/EventFavoriteToggleRequest.java
package com.emotion.dto.request;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 人生事件收藏切换请求
*
* @author AI Assistant
* @date 2026-07-19
*/
@Data
public class EventFavoriteToggleRequest {
@NotBlank(message = "事件ID不能为空")
private String eventId;
}
```
- [ ] **Step 5: 创建 EventFavoriteResponse DTO**
```java
// server/src/main/java/com/emotion/dto/response/EventFavoriteResponse.java
package com.emotion.dto.response;
import lombok.Data;
/**
* 人生事件收藏状态响应
*
* @author AI Assistant
* @date 2026-07-19
*/
@Data
public class EventFavoriteResponse {
private String eventId;
private Boolean isFavorited;
private String favoriteTime;
}
```
- [ ] **Step 6: 创建 EventFavoriteService 接口**
```java
// server/src/main/java/com/emotion/service/EventFavoriteService.java
package com.emotion.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.emotion.common.PageResult;
import com.emotion.dto.response.EventFavoriteResponse;
import com.emotion.dto.response.LifeEventResponse;
import com.emotion.entity.EventFavorite;
import java.util.Set;
/**
* 人生事件收藏服务接口
*
* @author AI Assistant
* @date 2026-07-19
*/
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();
}
```
- [ ] **Step 7: 创建 EventFavoriteServiceImpl 实现**
```java
// server/src/main/java/com/emotion/service/impl/EventFavoriteServiceImpl.java
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.response.EventFavoriteResponse;
import com.emotion.dto.response.LifeEventResponse;
import com.emotion.entity.EventFavorite;
import com.emotion.mapper.EventFavoriteMapper;
import com.emotion.service.EventFavoriteService;
import com.emotion.service.LifeEventService;
import com.emotion.util.UserContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
/**
* 人生事件收藏服务实现
*
* @author AI Assistant
* @date 2026-07-19
*/
@Slf4j
@Service
public class EventFavoriteServiceImpl extends ServiceImpl<EventFavoriteMapper, EventFavorite>
implements EventFavoriteService {
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Autowired
private LifeEventService lifeEventService;
@Override
@Transactional(rollbackFor = Exception.class)
public EventFavoriteResponse toggleFavorite(String eventId) {
String userId = UserContextHolder.getCurrentUserId();
EventFavoriteResponse r = new EventFavoriteResponse();
r.setEventId(eventId);
if (!StringUtils.hasText(userId)) {
r.setIsFavorited(false);
return r;
}
LambdaQueryWrapper<EventFavorite> w = new LambdaQueryWrapper<>();
w.eq(EventFavorite::getUserId, userId)
.eq(EventFavorite::getEventId, eventId)
.eq(EventFavorite::getIsDeleted, 0);
EventFavorite existing = this.getOne(w);
if (existing != null) {
this.removeById(existing.getId());
r.setIsFavorited(false);
} else {
EventFavorite fav = new EventFavorite();
fav.setUserId(userId);
fav.setEventId(eventId);
this.save(fav);
r.setIsFavorited(true);
if (fav.getCreateTime() != null) {
r.setFavoriteTime(fav.getCreateTime().format(FMT));
}
}
return r;
}
@Override
public PageResult<LifeEventResponse> getFavoritePage(long current, long size) {
String userId = UserContextHolder.getCurrentUserId();
if (!StringUtils.hasText(userId)) {
return new PageResult<>();
}
LambdaQueryWrapper<EventFavorite> w = new LambdaQueryWrapper<>();
w.eq(EventFavorite::getUserId, userId)
.eq(EventFavorite::getIsDeleted, 0)
.orderByDesc(EventFavorite::getCreateTime);
Page<EventFavorite> page = new Page<>(current, size);
Page<EventFavorite> result = this.page(page, w);
List<LifeEventResponse> records = result.getRecords().stream()
.map(EventFavorite::getEventId)
.map(lifeEventService::getEventById)
.filter(Objects::nonNull)
.collect(Collectors.toList());
PageResult<LifeEventResponse> pr = new PageResult<>();
pr.setCurrent(result.getCurrent());
pr.setSize(result.getSize());
pr.setTotal(result.getTotal());
pr.setPages(result.getPages());
pr.setRecords(records);
return pr;
}
@Override
public Set<String> getFavoritedEventIds(Set<String> eventIds) {
String userId = UserContextHolder.getCurrentUserId();
if (!StringUtils.hasText(userId) || eventIds == null || eventIds.isEmpty()) {
return Collections.emptySet();
}
LambdaQueryWrapper<EventFavorite> w = new LambdaQueryWrapper<>();
w.eq(EventFavorite::getUserId, userId)
.in(EventFavorite::getEventId, eventIds)
.eq(EventFavorite::getIsDeleted, 0);
return this.list(w).stream()
.map(EventFavorite::getEventId)
.collect(Collectors.toSet());
}
@Override
public boolean isFavorited(String eventId) {
String userId = UserContextHolder.getCurrentUserId();
if (!StringUtils.hasText(userId) || !StringUtils.hasText(eventId)) {
return false;
}
LambdaQueryWrapper<EventFavorite> w = new LambdaQueryWrapper<>();
w.eq(EventFavorite::getUserId, userId)
.eq(EventFavorite::getEventId, eventId)
.eq(EventFavorite::getIsDeleted, 0);
return this.count(w) > 0;
}
@Override
public long getFavoriteCount() {
String userId = UserContextHolder.getCurrentUserId();
if (!StringUtils.hasText(userId)) {
return 0L;
}
LambdaQueryWrapper<EventFavorite> w = new LambdaQueryWrapper<>();
w.eq(EventFavorite::getUserId, userId)
.eq(EventFavorite::getIsDeleted, 0);
return this.count(w);
}
}
```
- [ ] **Step 8: 修改 LifeEventController 添加收藏 API**
`server/src/main/java/com/emotion/controller/LifeEventController.java` 中添加:
1. 在类顶部添加注入:
```java
@Autowired
private EventFavoriteService eventFavoriteService;
```
2. 添加 import
```java
import com.emotion.dto.request.EventFavoriteToggleRequest;
import com.emotion.dto.response.EventFavoriteResponse;
import com.emotion.service.EventFavoriteService;
```
3. 在类末尾(`readTags` 方法之前)添加收藏 API
```java
// ==================== 人生事件收藏 ====================
@Operation(summary = "切换收藏", description = "收藏或取消收藏指定事件。")
@PostMapping(value = "/favorite/toggle")
public Result<EventFavoriteResponse> toggleFavorite(
@Valid @RequestBody EventFavoriteToggleRequest request) {
return Result.success(eventFavoriteService.toggleFavorite(request.getEventId()));
}
@Operation(summary = "分页查询收藏", description = "分页查询当前用户收藏的事件列表。")
@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));
}
@Operation(summary = "检查收藏状态", description = "检查指定事件是否已被当前用户收藏。")
@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);
}
```
4. **删除** `/lifeEvent/favorite-placeholder` 假接口(第 153-163 行):
```java
// 删除以下方法
@Operation(summary = "收藏占位接口", description = "返回收藏功能的占位响应。")
@PostMapping(value = "/favorite-placeholder")
public Result<Map<String, Object>> favoritePlaceholder(@RequestBody Map<String, Object> request) {
// ... 整段删除
}
```
- [ ] **Step 9: 提交**
```bash
git add server/src/main/resources/db/migration/V20260719000002__create_event_favorite.sql
git add server/src/main/java/com/emotion/entity/EventFavorite.java
git add server/src/main/java/com/emotion/mapper/EventFavoriteMapper.java
git add server/src/main/java/com/emotion/dto/request/EventFavoriteToggleRequest.java
git add server/src/main/java/com/emotion/dto/response/EventFavoriteResponse.java
git add server/src/main/java/com/emotion/service/EventFavoriteService.java
git add server/src/main/java/com/emotion/service/impl/EventFavoriteServiceImpl.java
git add server/src/main/java/com/emotion/controller/LifeEventController.java
git commit -m "feat: 新增人生事件收藏功能(独立收藏表 + API)"
```
---
### Task 2: 后端编译 + 部署到服务器
**Files:**
- Build: `server/target/`
- Deploy: 服务器
- [ ] **Step 1: 本地编译验证**
```bash
cd server
mvn clean install -DskipTests
```
Expected: BUILD SUCCESS,无编译错误。
- [ ] **Step 2: 部署到服务器**
```bash
cd ..
python deploy.py backend
```
Expected: 部署成功,服务器自动重启。
- [ ] **Step 3: 验证部署**
```bash
python deploy.py verify
```
Expected: 后端服务运行正常。
- [ ] **Step 4: 提交**
```bash
git commit -m "chore: 后端人生事件收藏功能已部署" --allow-empty
```
---
### Task 3: 前端 ScriptLibraryView.vue 切换到剧本收藏 API
**Files:**
- Modify: `mini-program/src/pages/main/ScriptLibraryView.vue`
- [ ] **Step 1: 删除 localFavorites ref,改用内存状态**
删除第 160 行:
```javascript
// 删除这一行
const localFavorites = ref(uni.getStorageSync('script_favorites') || {})
```
替换为:
```javascript
const favoriteStatus = ref({})
```
- [ ] **Step 2: 修改 isFavorite 计算**
修改第 245-247 行:
```javascript
// 之前:
const isFavorite = (script) => {
return Boolean(script.isFavorite || script.favorite || localFavorites.value[String(script.id)])
}
// 之后:
const isFavorite = (script) => {
return Boolean(favoriteStatus.value[String(script.id)])
}
```
- [ ] **Step 3: 修改 toggleFavorite,删除 localStorage 写**
修改第 320-348 行:
```javascript
// 之前:
const toggleFavorite = async (script) => {
const wasFav = isFavorite(script)
// 乐观更新:先改本地缓存
const next = { ...localFavorites.value }
if (wasFav) {
delete next[String(script.id)]
} else {
next[String(script.id)] = true
}
localFavorites.value = next
uni.setStorageSync('script_favorites', next)
closeScriptMenu()
try {
const result = await toggleFavoriteScript(script.id)
uni.showToast({ title: result.isFavorited ? '已收藏' : '已取消收藏', icon: 'success' })
} catch (error) {
// 回滚乐观更新
const restored = { ...localFavorites.value }
if (wasFav) {
restored[String(script.id)] = true
} else {
delete restored[String(script.id)]
}
localFavorites.value = restored
uni.setStorageSync('script_favorites', restored)
uni.showToast({ title: error?.message || '操作失败', icon: 'none' })
}
}
// 之后:
const toggleFavorite = async (script) => {
const wasFav = isFavorite(script)
// 乐观更新:先改内存状态
const next = { ...favoriteStatus.value }
if (wasFav) {
delete next[String(script.id)]
} else {
next[String(script.id)] = true
}
favoriteStatus.value = next
closeScriptMenu()
try {
const result = await toggleFavoriteScript(script.id)
uni.showToast({ title: result.isFavorited ? '已收藏' : '已取消收藏', icon: 'success' })
} catch (error) {
// 回滚乐观更新
const restored = { ...favoriteStatus.value }
if (wasFav) {
restored[String(script.id)] = true
} else {
delete restored[String(script.id)]
}
favoriteStatus.value = restored
uni.showToast({ title: error?.message || '操作失败', icon: 'none' })
}
}
```
- [ ] **Step 4: 修改 confirmDeleteScript,删除 localStorage 清理**
修改第 386-389 行:
```javascript
// 之前:
const next = { ...localFavorites.value }
delete next[id]
localFavorites.value = next
uni.setStorageSync('script_favorites', next)
// 之后:
const next = { ...favoriteStatus.value }
delete next[id]
favoriteStatus.value = next
```
- [ ] **Step 5: 列表加载后批量检查收藏状态**
`visibleScripts` computed 后添加 watch 或 onMounted 钩子,批量调用 `checkFavoriteScript`
在文件顶部添加 import
```javascript
import { checkFavoriteScript } from '../../services/epicScript.js'
```
`visibleScripts` computed 后添加:
```javascript
// 列表加载后批量检查收藏状态
const loadFavoriteStatus = async () => {
const ids = scripts.value.map(s => String(s.id)).filter(id => !id.startsWith('demo-'))
if (!ids.length) return
const status = {}
await Promise.all(ids.map(async (id) => {
try {
status[id] = await checkFavoriteScript(id)
} catch {
status[id] = false
}
}))
favoriteStatus.value = status
}
// 监听 scripts 变化时重新加载收藏状态
watch(scripts, () => {
loadFavoriteStatus()
}, { immediate: true })
```
确保 import watch
```javascript
import { computed, ref, watch } from 'vue'
```
- [ ] **Step 6: 提交**
```bash
git add mini-program/src/pages/main/ScriptLibraryView.vue
git commit -m "refactor: 剧本收藏从 localStorage 切换到真实 API"
```
---
### Task 4: 前端 life-event 切换到人生事件收藏 API
**Files:**
- Modify: `mini-program/src/services/lifeEvent.js`
- Modify: `mini-program/src/pages/life-event/detail.vue`
- Modify: `mini-program/src/stores/app.js`
- [ ] **Step 1: 修改 lifeEvent.js 新增收藏 API 封装**
`mini-program/src/services/lifeEvent.js` 中:
1. **删除**旧的 `favoriteEvent` 方法(第 67-69 行):
```javascript
// 删除:
export const favoriteEvent = async ({ id, favorite }) => {
return post('/lifeEvent/favorite-placeholder', { id, favorite })
}
```
2. **新增**真实的收藏 API
```javascript
/**
* 切换人生事件收藏状态(收藏/取消收藏)
* @param {string} eventId 事件ID
* @returns {Promise<{success: boolean, isFavorited: boolean}>}
*/
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
}
}
/**
* 检查人生事件是否已收藏
* @param {string} eventId 事件ID
* @returns {Promise<boolean>}
*/
export const checkFavoriteEvent = async (eventId) => {
const res = await get('/lifeEvent/favorite/check', { eventId })
return res.data?.isFavorited ?? false
}
```
3. 在 default export 中更新方法列表:
```javascript
export default {
getEventList,
getEventPage,
getEventById,
createEvent,
updateEvent,
deleteEvent,
assistEventWriting,
chatAboutEvent,
shareEvent,
toggleFavoriteEvent,
checkFavoriteEvent,
transformToFrontendFormat,
transformListToFrontend
}
```
- [ ] **Step 2: 修改 stores/app.js 的 favoriteEvent action**
`mini-program/src/stores/app.js` 中:
1. **删除**旧的 `favoriteEvent` action(第 235-242 行):
```javascript
// 删除:
const favoriteEvent = async ({ id, favorite }) => {
try {
const res = await lifeEventService.favoriteEvent({ id, favorite })
return { success: true, data: res.data }
} catch (error) {
return { success: false, error: error.message }
}
}
```
2. **新增**新的 action
```javascript
const toggleEventFavorite = async (eventId) => {
try {
const result = await lifeEventService.toggleFavoriteEvent(eventId)
return { success: true, data: result }
} catch (error) {
return { success: false, error: error.message }
}
}
const checkEventFavorite = async (eventId) => {
try {
const isFavorited = await lifeEventService.checkFavoriteEvent(eventId)
return { success: true, data: isFavorited }
} catch (error) {
return { success: false, error: error.message }
}
}
```
3. 在 return 对象中更新导出:
```javascript
return {
// ... 其他方法
toggleEventFavorite,
checkEventFavorite,
// 删除 favoriteEvent
}
```
- [ ] **Step 3: 修改 detail.vue 初始化时检查收藏状态**
`mini-program/src/pages/life-event/detail.vue` 中:
1. 修改 `onMounted`(第 137-149 行):
```javascript
// 之前:
onMounted(async () => {
const info = uni.getWindowInfo()
safeAreaBottom.value = info.safeAreaInsets?.bottom || 0
const pages = getCurrentPages()
eventId.value = pages[pages.length - 1]?.options?.id || ''
cachedEvent.value = uni.getStorageSync('current_life_event') || null
if (!store.events?.length) await store.fetchEvents()
isFavorite.value = Boolean(uni.getStorageSync(`event_favorite_${eventId.value}`))
analytics.trackPageView(pagePath, {
life_event_id: eventId.value,
tag_count: Array.isArray(displayEvent.value.tags) ? displayEvent.value.tags.length : 0
})
})
// 之后:
onMounted(async () => {
const info = uni.getWindowInfo()
safeAreaBottom.value = info.safeAreaInsets?.bottom || 0
const pages = getCurrentPages()
eventId.value = pages[pages.length - 1]?.options?.id || ''
cachedEvent.value = uni.getStorageSync('current_life_event') || null
if (!store.events?.length) await store.fetchEvents()
// 从 API 检查收藏状态
const result = await store.checkEventFavorite(eventId.value)
if (result.success) {
isFavorite.value = result.data
}
analytics.trackPageView(pagePath, {
life_event_id: eventId.value,
tag_count: Array.isArray(displayEvent.value.tags) ? displayEvent.value.tags.length : 0
})
})
```
- [ ] **Step 4: 修改 detail.vue 的 toggleFavorite,删除 localStorage 写**
修改第 306-322 行:
```javascript
// 之前:
const toggleFavorite = async () => {
if (!eventId.value) return
const next = !isFavorite.value
const result = await store.favoriteEvent({ id: eventId.value, favorite: next })
if (!result.success) {
uni.showToast({ title: result.error || '收藏失败', icon: 'none' })
return
}
isFavorite.value = next
if (next) uni.setStorageSync(`event_favorite_${eventId.value}`, '1')
else uni.removeStorageSync(`event_favorite_${eventId.value}`)
analytics.track('life_event_favorite', {
life_event_id: eventId.value,
favorite: next
}, { eventType: 'life_event', pagePath })
uni.showToast({ title: next ? '已收藏' : '已取消收藏', icon: 'success' })
}
// 之后:
const toggleFavorite = async () => {
if (!eventId.value) return
const result = await store.toggleEventFavorite(eventId.value)
if (!result.success) {
uni.showToast({ title: result.error || '收藏失败', icon: 'none' })
return
}
isFavorite.value = result.data?.isFavorited ?? false
analytics.track('life_event_favorite', {
life_event_id: eventId.value,
favorite: isFavorite.value
}, { eventType: 'life_event', pagePath })
uni.showToast({ title: isFavorite.value ? '已收藏' : '已取消收藏', icon: 'success' })
}
```
- [ ] **Step 5: 提交**
```bash
git add mini-program/src/services/lifeEvent.js
git add mini-program/src/stores/app.js
git add mini-program/src/pages/life-event/detail.vue
git commit -m "refactor: 人生事件收藏从 localStorage 切换到真实 API"
```
---
### Task 5: H5 浏览器验证
**Files:**
- Test: `http://localhost:5180` (H5 开发服务器)
- [ ] **Step 1: 确认 H5 开发服务器运行中**
```bash
python dev-services.py status
```
Expected: `mini-program` 服务在端口 5180 上运行。如未运行,执行:
```bash
python dev-services.py start mini-program
```
- [ ] **Step 2: 浏览器打开页面并登录**
1. 打开 `http://localhost:5180`
2. 如未登录,使用手机号验证码 123456 登录
- [ ] **Step 3: 验证剧本收藏**
1. 点击底部导航「爽文生成」
2. 点击「剧本库」
3. 点击任意剧本的收藏图标(心形)
4. 确认:
- ✅ Toast 提示「已收藏」
- ✅ 心形图标变为实心
- ✅ Network 面板中有 `POST /epicScript/favorite/toggle` 请求,返回 `isFavorited: true`
5. 刷新页面
6. 确认收藏状态仍保持(心形图标仍为实心)
7. 再次点击收藏图标取消收藏
8. 确认:
- ✅ Toast 提示「已取消收藏」
- ✅ 心形图标变为空心
- ✅ Network 面板中有 `POST /epicScript/favorite/toggle` 请求,返回 `isFavorited: false`
- [ ] **Step 4: 验证人生事件收藏**
1. 点击底部导航「人生轨迹」
2. 点击任意事件进入详情页
3. 点击收藏按钮
4. 确认:
- ✅ Toast 提示「已收藏」
- ✅ 收藏按钮状态变化
- ✅ Network 面板中有 `POST /lifeEvent/favorite/toggle` 请求,返回 `isFavorited: true`
5. 返回列表页,再次进入同一事件详情
6. 确认收藏状态仍保持
7. 再次点击收藏按钮取消收藏
8. 确认:
- ✅ Toast 提示「已取消收藏」
- ✅ 收藏按钮状态恢复
- ✅ Network 面板中有 `POST /lifeEvent/favorite/toggle` 请求,返回 `isFavorited: false`
- [ ] **Step 5: 检查浏览器 Console 无报错**
打开浏览器 DevTools → Console 面板,确认:
- ✅ 无红色 ERROR
- ✅ 无 localStorage 相关的 `script_favorites``event_favorite_` 读写
- [ ] **Step 6: 提交(如有微调)**
```bash
git add mini-program/src/
git commit -m "test: H5 浏览器验证收藏功能通过"
```
---
### Task 6: 通知用户验证
- [ ] **Step 1: 提示用户操作**
告知用户:
> 收藏功能已完成真实 API 化改造:
>
> **剧本收藏**
> - 前端从 localStorage 切换到现有后端 API`POST /epicScript/favorite/toggle`
> - 收藏状态从服务器读取,不再依赖本地缓存
>
> **人生事件收藏**
> - 后端新建独立的 `t_event_favorite` 表 + API
> - 前端从 localStorage 切换到真实 API`POST /lifeEvent/favorite/toggle`
> - 删除了假接口 `/lifeEvent/favorite-placeholder`
>
> **验证方式**
> 1. 本地 H5`http://localhost:5180`)已验证通过
> 2. 后端已部署到服务器
> 3. 请在**微信开发者工具**中:
> - 打开 `unpackage/dist/dev/mp-weixin` 目录
> - 点击「编译」重新编译
> - 测试剧本收藏和人生事件收藏功能
> - 确认收藏状态刷新后仍保持
>
> 如有问题,截图反馈。