--- 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 { } ``` - [ ] **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 { EventFavoriteResponse toggleFavorite(String eventId); PageResult getFavoritePage(long current, long size); Set getFavoritedEventIds(Set 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 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 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 getFavoritePage(long current, long size) { String userId = UserContextHolder.getCurrentUserId(); if (!StringUtils.hasText(userId)) { return new PageResult<>(); } LambdaQueryWrapper w = new LambdaQueryWrapper<>(); w.eq(EventFavorite::getUserId, userId) .eq(EventFavorite::getIsDeleted, 0) .orderByDesc(EventFavorite::getCreateTime); Page page = new Page<>(current, size); Page result = this.page(page, w); List records = result.getRecords().stream() .map(EventFavorite::getEventId) .map(lifeEventService::getEventById) .filter(Objects::nonNull) .collect(Collectors.toList()); PageResult 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 getFavoritedEventIds(Set eventIds) { String userId = UserContextHolder.getCurrentUserId(); if (!StringUtils.hasText(userId) || eventIds == null || eventIds.isEmpty()) { return Collections.emptySet(); } LambdaQueryWrapper 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 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 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 toggleFavorite( @Valid @RequestBody EventFavoriteToggleRequest request) { return Result.success(eventFavoriteService.toggleFavorite(request.getEventId())); } @Operation(summary = "分页查询收藏", description = "分页查询当前用户收藏的事件列表。") @GetMapping(value = "/favorite/page") public Result> 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 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> favoritePlaceholder(@RequestBody Map 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} */ 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` 目录 > - 点击「编译」重新编译 > - 测试剧本收藏和人生事件收藏功能 > - 确认收藏状态刷新后仍保持 > > 如有问题,截图反馈。