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

9.6 KiB
Raw Blame History

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

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. 人生事件收藏(新增)

EntityEventFavorite.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;
}

MapperEventFavoriteMapper.java

@Mapper
public interface EventFavoriteMapper extends BaseMapper<EventFavorite> {
}

ServiceEventFavoriteService.java + EventFavoriteServiceImpl.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 }

ControllerLifeEventController.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

修改

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

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 部署到服务器后,在服务器环境验证