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

7.3 KiB
Raw Blame History

author, created_at, purpose
author created_at purpose
AI Assistant 2026-07-19 小程序收藏功能从 localStorage 走真实 API,在现有剧本/事件表增加 is_favorite 字段

小程序收藏功能真实 API 化设计

概述

当前小程序的「收藏」功能(剧本收藏、人生事件收藏)完全基于前端 localStorage

  • 剧本:uni.getStorageSync('script_favorites') → 对象字典 { id: true/false }
  • 人生事件:uni.getStorageSync('event_favorite_${id}') → 单条布尔

后端没有真实的剧本收藏 API,人生事件收藏接口 /lifeEvent/favorite-placeholder 是假的(echo 请求参数 + placeholder: true)。

本次修复:在现有 e_epic_script 表和 t_life_event 表上新增 is_favorite 字段,新增真实 API,前端从 localStorage 切换到 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] 读取
life-event/detail.vue:142 current_life_event localStorage 读整条事件数据
life-event/detail.vue:144 isFavoriteevent_favorite_${id} localStorage 读
life-event/detail.vue:315-316 toggle 写/删 event_favorite_${id} localStorage

后端

文件 问题
LifeEventController.java:153-163 /lifeEvent/favorite-placeholder 是空壳,返回 echo + placeholder: true
无剧本收藏 Controller /epicScript 下没有任何收藏端点

设计方案

存储方案(已确认)

在现有表新增 is_favorite 字段:

ALTER TABLE e_epic_script ADD COLUMN is_favorite TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否收藏:0-否 1-是';
ALTER TABLE t_life_event  ADD COLUMN is_favorite TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否收藏:0-否 1-是';

选择这个方案的理由

  1. 不新增表,改动最小
  2. is_favorite 是当前用户维度的标记;用户只操作自己的剧本/事件,不需要 user_id 维度
  3. 现有 listAll / page 接口返回全量字段,新增 isFavorite 字段后前端可直接读取,不需要额外的"收藏夹"接口
  4. 后续如需"按用户维度"收藏(如管理员视角),再迁移到独立表

后端改动

1. 实体类

  • EpicScript.java 新增:private Boolean isFavorite;
  • LifeEvent.java 新增:private Boolean isFavorite;

MyBatis-Plus 自动映射下划线 is_favorite 到驼峰 isFavorite

2. Controller API

剧本EpicScriptController):

POST /epicScript/favorite
入参:FavoriteRequest { Long id; Boolean favorite; }
出参:Result<Void>
逻辑:
  1. 通过 id 查询剧本
  2. 若不存在 → 抛业务异常("剧本不存在"
  3. update is_favorite = favorite
  4. return Result.ok()

人生事件LifeEventController):

POST /lifeEvent/favorite
入参:FavoriteRequest { Long id; Boolean favorite; }
出参:Result<Void>
逻辑:
  1. 通过 id 查询事件
  2. 若不存在 → 抛业务异常
  3. update is_favorite = favorite
  4. return Result.ok()

删除现有 /lifeEvent/favorite-placeholder 接口。

3. Service 层

// EpicScriptService
void setFavorite(Long id, Boolean favorite);

// LifeEventService
void setFavorite(Long id, Boolean favorite);

实现:lambdaUpdate().set(EpicScript::getIsFavorite, favorite).eq(EpicScript::getId, id).update()

4. DTO

新增公共请求对象 FavoriteRequest

public class FavoriteRequest {
    @NotNull
    private Long id;
    @NotNull
    private Boolean favorite;
}

放在 common 模块,两个 Controller 都能引用。

前端改动

1. stores/app.js

新增两个 action

async toggleScriptFavorite(id, favorite) {
  await epicScriptService.setFavorite(id, favorite);
  const script = this.scripts.find(s => String(s.id) === String(id));
  if (script) script.isFavorite = favorite;
},

async toggleEventFavorite(id, favorite) {
  await lifeEventService.setFavorite(id, favorite);
  // 事件列表如果是 store 内状态,同步更新
}

2. services/epicScript.js

新增:

setFavorite: (id, favorite) => post('/epicScript/favorite', { id: String(id), favorite })

3. services/lifeEvent.js

修改现有 favoriteEvent 实现:

// 之前:post('/lifeEvent/favorite-placeholder', ...)
// 之后:post('/lifeEvent/favorite', { id, favorite })

4. ScriptLibraryView.vue

  • 删除 const localFavorites = ref(uni.getStorageSync('script_favorites') || {})
  • 删除 toggleFavorite / deleteScriptFromFavorites 的 localStorage 写/删
  • isFavorite 计算改为:Boolean(script.isFavorite)
  • toggleFavorite 改为调 store.toggleScriptFavorite(id, !script.isFavorite)

5. life-event/detail.vue

  • 删除 uni.getStorageSync('current_life_event') 的回退读取(这是"把数据存到用户端"的另一处违规,在 sub-project B 一并清理)
  • 删除 uni.getStorageSync('event_favorite_${id}') 读取
  • isFavorite 改为 computed(() => Boolean(displayEvent.value?.isFavorite))
  • toggle 改为调 store.toggleEventFavorite(id, !isFavorite.value)lifeEventService.favoriteEvent

数据流

列表页 ScriptLibraryView
  └─ fetchScripts()  (GET /epicScript/listAll)
       └─ 后端查 e_epic_script 含 is_favorite 字段
            └─ 前端渲染 isFavorite

用户点收藏
  └─ store.toggleScriptFavorite(id, true/false)
       └─ POST /epicScript/favorite  { id, favorite }
            └─ 后端 update is_favorite
                 └─ 本地 state 同步,UI 响应式更新

详情页 life-event/detail
  └─ fetchLifeEventDetail(id)  (GET /lifeEvent/detail)
       └─ 后端查 t_life_event 含 is_favorite
            └─ 前端渲染 isFavorite

不影响范围

  • 列表渲染、筛选、排序逻辑不变,只换数据源(localStorage → API 字段)
  • 详情页展示逻辑不变(数据源变化在 sub-project B 清理)
  • 其他页面的剧本/事件展示(如首页推荐)自动受益于新的 isFavorite 字段

数据库迁移

新增迁移 SQL 文件 server/src/main/resources/db/migration/V{timestamp}__add_is_favorite.sql

ALTER TABLE e_epic_script ADD COLUMN is_favorite TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否收藏:0-否 1-是';
ALTER TABLE t_life_event  ADD COLUMN is_favorite TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否收藏:0-否 1-是';

如果项目不用 Flyway,则手动执行或写入项目约定的 SQL 目录。

验收标准

  • 剧本收藏:点击心形图标 → 调用 POST /epicScript/favorite → 后端字段更新 → 刷新页面后仍保持状态
  • 人生事件收藏:点击收藏 → 调用 POST /lifeEvent/favorite → 同上
  • 取消收藏:再次点击 → 同上
  • 列表页 isFavorite 来自 API,不再依赖 localStorage
  • 详情页 isFavorite 来自 API,不再依赖 localStorage
  • 浏览器 Console 无报错
  • 后端 mvn clean install 编译通过
  • 通过 deploy.py backend 部署到服务器后,在服务器环境验证