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

27 KiB
Raw Blame History

author, created_at, purpose
author created_at purpose
AI Assistant 2026-07-19 小程序收藏功能真实 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

-- 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 实体类
// 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
// 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
// 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
// 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 接口
// 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 实现
// 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. 在类顶部添加注入:
@Autowired
private EventFavoriteService eventFavoriteService;
  1. 添加 import
import com.emotion.dto.request.EventFavoriteToggleRequest;
import com.emotion.dto.response.EventFavoriteResponse;
import com.emotion.service.EventFavoriteService;
  1. 在类末尾(readTags 方法之前)添加收藏 API
// ==================== 人生事件收藏 ====================

@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);
}
  1. 删除 /lifeEvent/favorite-placeholder 假接口(第 153-163 行):
// 删除以下方法
@Operation(summary = "收藏占位接口", description = "返回收藏功能的占位响应。")
@PostMapping(value = "/favorite-placeholder")
public Result<Map<String, Object>> favoritePlaceholder(@RequestBody Map<String, Object> request) {
    // ... 整段删除
}
  • Step 9: 提交
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: 本地编译验证

cd server
mvn clean install -DskipTests

Expected: BUILD SUCCESS,无编译错误。

  • Step 2: 部署到服务器
cd ..
python deploy.py backend

Expected: 部署成功,服务器自动重启。

  • Step 3: 验证部署
python deploy.py verify

Expected: 后端服务运行正常。

  • Step 4: 提交
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 行:

// 删除这一行
const localFavorites = ref(uni.getStorageSync('script_favorites') || {})

替换为:

const favoriteStatus = ref({})
  • Step 2: 修改 isFavorite 计算

修改第 245-247 行:

// 之前:
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 行:

// 之前:
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 行:

// 之前:
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

import { checkFavoriteScript } from '../../services/epicScript.js'

visibleScripts computed 后添加:

// 列表加载后批量检查收藏状态
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

import { computed, ref, watch } from 'vue'
  • Step 6: 提交
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 行):
// 删除:
export const favoriteEvent = async ({ id, favorite }) => {
  return post('/lifeEvent/favorite-placeholder', { id, favorite })
}
  1. 新增真实的收藏 API
/**
 * 切换人生事件收藏状态(收藏/取消收藏)
 * @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
}
  1. 在 default export 中更新方法列表:
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 行):
// 删除:
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 }
  }
}
  1. 新增新的 action
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 }
  }
}
  1. 在 return 对象中更新导出:
return {
  // ... 其他方法
  toggleEventFavorite,
  checkEventFavorite,
  // 删除 favoriteEvent
}
  • Step 3: 修改 detail.vue 初始化时检查收藏状态

mini-program/src/pages/life-event/detail.vue 中:

  1. 修改 onMounted(第 137-149 行):
// 之前:
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 行:

// 之前:
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: 提交
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 开发服务器运行中

python dev-services.py status

Expected: mini-program 服务在端口 5180 上运行。如未运行,执行:

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_favoritesevent_favorite_ 读写

  • Step 6: 提交(如有微调)

git add mini-program/src/
git commit -m "test: H5 浏览器验证收藏功能通过"

Task 6: 通知用户验证

  • Step 1: 提示用户操作

告知用户:

收藏功能已完成真实 API 化改造:

剧本收藏

  • 前端从 localStorage 切换到现有后端 APIPOST /epicScript/favorite/toggle
  • 收藏状态从服务器读取,不再依赖本地缓存

人生事件收藏

  • 后端新建独立的 t_event_favorite 表 + API
  • 前端从 localStorage 切换到真实 APIPOST /lifeEvent/favorite/toggle
  • 删除了假接口 /lifeEvent/favorite-placeholder

验证方式

  1. 本地 H5http://localhost:5180)已验证通过
  2. 后端已部署到服务器
  3. 请在微信开发者工具中:
    • 打开 unpackage/dist/dev/mp-weixin 目录
    • 点击「编译」重新编译
    • 测试剧本收藏和人生事件收藏功能
    • 确认收藏状态刷新后仍保持

如有问题,截图反馈。