From b20f30f91b31287cd8a324de66ea8175e13c89e7 Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 19 Jul 2026 21:38:53 +0800 Subject: [PATCH 01/16] feat(script-favorite): add database migration and backend service - Add t_script_favorite table migration SQL - Add ScriptFavorite entity, mapper, service interface and impl - Add toggle/page/check endpoints in EpicScriptController - Add toggleFavorite/getFavoritePage/checkFavorite in epicScript.js - Update ScriptLibraryView with backend-backed toggleFavorite From 43ac037f5b610da48ca4708020b6331b90d611e1 Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 19 Jul 2026 21:39:27 +0800 Subject: [PATCH 02/16] feat(script-favorite): add t_script_favorite migration SQL - Create t_script_favorite table with user_id, script_id - Add unique key uk_user_script (user_id, script_id) - Add indexes idx_user_id and idx_script_id - Support logical delete via is_deleted flag --- .../V20260719__add_script_favorite.sql | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 server/src/main/resources/db/migration/V20260719__add_script_favorite.sql diff --git a/server/src/main/resources/db/migration/V20260719__add_script_favorite.sql b/server/src/main/resources/db/migration/V20260719__add_script_favorite.sql new file mode 100644 index 0000000..b7e45df --- /dev/null +++ b/server/src/main/resources/db/migration/V20260719__add_script_favorite.sql @@ -0,0 +1,17 @@ +-- 剧本收藏关联表 +-- 存储用户对剧本的收藏关系,多对多关联 +CREATE TABLE t_script_favorite ( + id VARCHAR(64) NOT NULL PRIMARY KEY COMMENT '主键ID', + user_id VARCHAR(64) NOT NULL COMMENT '用户ID,关联t_user.id', + script_id VARCHAR(64) NOT NULL COMMENT '剧本ID,关联t_epic_script.id', + create_by VARCHAR(64) DEFAULT NULL COMMENT '创建人', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '收藏时间', + update_by VARCHAR(64) DEFAULT NULL COMMENT '更新人', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + is_deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除: 0-未删除, 1-已删除', + remarks VARCHAR(500) DEFAULT '' COMMENT '备注(预留,可存收藏夹名或备注)', + + UNIQUE KEY uk_user_script (user_id, script_id), + KEY idx_user_id (user_id), + KEY idx_script_id (script_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='剧本收藏关联表'; From 489131a04bbe0a89f15e8d7a3559e500ab823447 Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 19 Jul 2026 21:39:47 +0800 Subject: [PATCH 03/16] feat(script-favorite): add ScriptFavorite entity - Extends BaseEntity for id, createTime, updateTime, isDeleted - Maps to t_script_favorite table via MyBatis-Plus @TableName - Fields: userId, scriptId --- .../com/emotion/entity/ScriptFavorite.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 server/src/main/java/com/emotion/entity/ScriptFavorite.java diff --git a/server/src/main/java/com/emotion/entity/ScriptFavorite.java b/server/src/main/java/com/emotion/entity/ScriptFavorite.java new file mode 100644 index 0000000..df1f3b6 --- /dev/null +++ b/server/src/main/java/com/emotion/entity/ScriptFavorite.java @@ -0,0 +1,31 @@ +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 huazhongmin + * @date 2026-07-19 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_script_favorite") +public class ScriptFavorite extends BaseEntity { + + @TableField("user_id") + private String userId; + + @TableField("script_id") + private String scriptId; +} From d74d8b22c6f0fa8b932d7650ca85ac6cff709121 Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 19 Jul 2026 21:40:06 +0800 Subject: [PATCH 04/16] feat(script-favorite): add ScriptFavoriteMapper - Extends BaseMapper - Standard MyBatis-Plus mapper pattern matching project convention --- .../com/emotion/mapper/ScriptFavoriteMapper.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 server/src/main/java/com/emotion/mapper/ScriptFavoriteMapper.java diff --git a/server/src/main/java/com/emotion/mapper/ScriptFavoriteMapper.java b/server/src/main/java/com/emotion/mapper/ScriptFavoriteMapper.java new file mode 100644 index 0000000..e05a277 --- /dev/null +++ b/server/src/main/java/com/emotion/mapper/ScriptFavoriteMapper.java @@ -0,0 +1,15 @@ +package com.emotion.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.emotion.entity.ScriptFavorite; +import org.apache.ibatis.annotations.Mapper; + +/** + * 剧本收藏Mapper接口 + * + * @author huazhongmin + * @date 2026-07-19 + */ +@Mapper +public interface ScriptFavoriteMapper extends BaseMapper { +} From 0d27d1a0720b7c94f8d704d9c0d103f39f33f2cf Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 19 Jul 2026 21:41:02 +0800 Subject: [PATCH 05/16] feat(script-favorite): add ScriptFavoriteToggleRequest DTO - Single field: scriptId with @NotBlank validation - Uses javax.validation.constraints.NotBlank matching project convention --- .../request/ScriptFavoriteToggleRequest.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 server/src/main/java/com/emotion/dto/request/ScriptFavoriteToggleRequest.java diff --git a/server/src/main/java/com/emotion/dto/request/ScriptFavoriteToggleRequest.java b/server/src/main/java/com/emotion/dto/request/ScriptFavoriteToggleRequest.java new file mode 100644 index 0000000..21e1cdc --- /dev/null +++ b/server/src/main/java/com/emotion/dto/request/ScriptFavoriteToggleRequest.java @@ -0,0 +1,17 @@ +package com.emotion.dto.request; + +import lombok.Data; +import javax.validation.constraints.NotBlank; + +/** + * 剧本收藏切换请求 + * + * @author huazhongmin + * @date 2026-07-19 + */ +@Data +public class ScriptFavoriteToggleRequest { + + @NotBlank(message = "剧本ID不能为空") + private String scriptId; +} From 62ca1730b96657cb4f5552ae4d099de899715ec8 Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 19 Jul 2026 21:41:14 +0800 Subject: [PATCH 06/16] feat(script-favorite): add ScriptFavoriteResponse DTO - Fields: scriptId, isFavorited, favoriteTime - Used by toggle/check endpoints to return favorite status --- .../dto/response/ScriptFavoriteResponse.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 server/src/main/java/com/emotion/dto/response/ScriptFavoriteResponse.java diff --git a/server/src/main/java/com/emotion/dto/response/ScriptFavoriteResponse.java b/server/src/main/java/com/emotion/dto/response/ScriptFavoriteResponse.java new file mode 100644 index 0000000..dae5dc1 --- /dev/null +++ b/server/src/main/java/com/emotion/dto/response/ScriptFavoriteResponse.java @@ -0,0 +1,19 @@ +package com.emotion.dto.response; + +import lombok.Data; + +/** + * 剧本收藏状态响应 + * + * @author huazhongmin + * @date 2026-07-19 + */ +@Data +public class ScriptFavoriteResponse { + + private String scriptId; + + private Boolean isFavorited; + + private String favoriteTime; +} From 674188f471122869b32c89853fc7d2edb7f402f4 Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 19 Jul 2026 21:41:28 +0800 Subject: [PATCH 07/16] feat(script-favorite): add ScriptFavoriteService interface - toggleFavorite: toggle favorite status for a script - getFavoritePage: paginated list of user's favorite scripts - getFavoritedScriptIds: batch check which scripts are favorited - isFavorited: check single script favorite status - getFavoriteCount: count user's total favorites --- .../service/ScriptFavoriteService.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 server/src/main/java/com/emotion/service/ScriptFavoriteService.java diff --git a/server/src/main/java/com/emotion/service/ScriptFavoriteService.java b/server/src/main/java/com/emotion/service/ScriptFavoriteService.java new file mode 100644 index 0000000..bc50c00 --- /dev/null +++ b/server/src/main/java/com/emotion/service/ScriptFavoriteService.java @@ -0,0 +1,28 @@ +package com.emotion.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.emotion.common.PageResult; +import com.emotion.dto.response.EpicScriptResponse; +import com.emotion.dto.response.ScriptFavoriteResponse; +import com.emotion.entity.ScriptFavorite; + +import java.util.Set; + +/** + * 剧本收藏服务接口 + * + * @author huazhongmin + * @date 2026-07-19 + */ +public interface ScriptFavoriteService extends IService { + + ScriptFavoriteResponse toggleFavorite(String scriptId); + + PageResult getFavoritePage(long current, long size); + + Set getFavoritedScriptIds(Set scriptIds); + + boolean isFavorited(String scriptId); + + long getFavoriteCount(); +} From 9d352f40a0a43d0f0807c6c5e7472979ea744be5 Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 19 Jul 2026 21:41:54 +0800 Subject: [PATCH 08/16] feat(script-favorite): add ScriptFavoriteServiceImpl - toggleFavorite: upsert via logical-delete check + save, @Transactional - getFavoritePage: paginate favorites then fetch EpicScript details - getFavoritedScriptIds: batch IN query for checking multiple scripts - isFavorited: count > 0 check for single script - getFavoriteCount: total count for current user - Uses UserContextHolder for userId, consistent with project pattern --- .../impl/ScriptFavoriteServiceImpl.java | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 server/src/main/java/com/emotion/service/impl/ScriptFavoriteServiceImpl.java diff --git a/server/src/main/java/com/emotion/service/impl/ScriptFavoriteServiceImpl.java b/server/src/main/java/com/emotion/service/impl/ScriptFavoriteServiceImpl.java new file mode 100644 index 0000000..398451d --- /dev/null +++ b/server/src/main/java/com/emotion/service/impl/ScriptFavoriteServiceImpl.java @@ -0,0 +1,149 @@ +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.EpicScriptResponse; +import com.emotion.dto.response.ScriptFavoriteResponse; +import com.emotion.entity.ScriptFavorite; +import com.emotion.mapper.ScriptFavoriteMapper; +import com.emotion.service.EpicScriptService; +import com.emotion.service.ScriptFavoriteService; +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 huazhongmin + * @date 2026-07-19 + */ +@Slf4j +@Service +public class ScriptFavoriteServiceImpl extends ServiceImpl + implements ScriptFavoriteService { + + private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Autowired + private EpicScriptService epicScriptService; + + @Override + @Transactional(rollbackFor = Exception.class) + public ScriptFavoriteResponse toggleFavorite(String scriptId) { + String userId = UserContextHolder.getCurrentUserId(); + ScriptFavoriteResponse r = new ScriptFavoriteResponse(); + r.setScriptId(scriptId); + + if (!StringUtils.hasText(userId)) { + r.setIsFavorited(false); + return r; + } + + LambdaQueryWrapper w = new LambdaQueryWrapper<>(); + w.eq(ScriptFavorite::getUserId, userId) + .eq(ScriptFavorite::getScriptId, scriptId) + .eq(ScriptFavorite::getIsDeleted, 0); + + ScriptFavorite existing = this.getOne(w); + if (existing != null) { + this.removeById(existing.getId()); + r.setIsFavorited(false); + } else { + ScriptFavorite fav = new ScriptFavorite(); + fav.setUserId(userId); + fav.setScriptId(scriptId); + 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(ScriptFavorite::getUserId, userId) + .eq(ScriptFavorite::getIsDeleted, 0) + .orderByDesc(ScriptFavorite::getCreateTime); + + Page page = new Page<>(current, size); + Page result = this.page(page, w); + + List records = result.getRecords().stream() + .map(ScriptFavorite::getScriptId) + .map(epicScriptService::getScriptById) + .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 getFavoritedScriptIds(Set scriptIds) { + String userId = UserContextHolder.getCurrentUserId(); + if (!StringUtils.hasText(userId) || scriptIds == null || scriptIds.isEmpty()) { + return Collections.emptySet(); + } + + LambdaQueryWrapper w = new LambdaQueryWrapper<>(); + w.eq(ScriptFavorite::getUserId, userId) + .in(ScriptFavorite::getScriptId, scriptIds) + .eq(ScriptFavorite::getIsDeleted, 0); + + return this.list(w).stream() + .map(ScriptFavorite::getScriptId) + .collect(Collectors.toSet()); + } + + @Override + public boolean isFavorited(String scriptId) { + String userId = UserContextHolder.getCurrentUserId(); + if (!StringUtils.hasText(userId) || !StringUtils.hasText(scriptId)) { + return false; + } + + LambdaQueryWrapper w = new LambdaQueryWrapper<>(); + w.eq(ScriptFavorite::getUserId, userId) + .eq(ScriptFavorite::getScriptId, scriptId) + .eq(ScriptFavorite::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(ScriptFavorite::getUserId, userId) + .eq(ScriptFavorite::getIsDeleted, 0); + + return this.count(w); + } +} From 74fa304cd95a4a78fdd922ac5ab0f22a5eb7fde1 Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 19 Jul 2026 21:43:46 +0800 Subject: [PATCH 09/16] feat(script-favorite): add 3 favorite endpoints to EpicScriptController - POST /epicScript/favorite/toggle: toggle favorite for a script - GET /epicScript/favorite/page: paginated favorite list - GET /epicScript/favorite/check: check if script is favorited - Add ScriptFavoriteToggleRequest, ScriptFavoriteResponse, ScriptFavoriteService imports - Inject ScriptFavoriteService via @Autowired --- .../controller/EpicScriptController.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/server/src/main/java/com/emotion/controller/EpicScriptController.java b/server/src/main/java/com/emotion/controller/EpicScriptController.java index db4d3c9..fee221a 100644 --- a/server/src/main/java/com/emotion/controller/EpicScriptController.java +++ b/server/src/main/java/com/emotion/controller/EpicScriptController.java @@ -8,13 +8,16 @@ import com.emotion.dto.request.EpicScriptInspirationRequest; import com.emotion.dto.request.EpicScriptPageRequest; import com.emotion.dto.request.EpicScriptUpdateRequest; import com.emotion.dto.request.SwitchVersionRequest; +import com.emotion.dto.request.ScriptFavoriteToggleRequest; import com.emotion.dto.response.EpicScriptCreateWithDialogueResponse; import com.emotion.dto.response.EpicScriptInspirationResponse; import com.emotion.dto.response.EpicScriptResponse; import com.emotion.dto.response.InspirationSuggestionResponse; import com.emotion.service.EpicScriptDialogueService; +import com.emotion.dto.response.ScriptFavoriteResponse; import com.emotion.service.EpicScriptService; import io.swagger.v3.oas.annotations.Operation; +import com.emotion.service.ScriptFavoriteService; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; @@ -41,6 +44,9 @@ public class EpicScriptController { @Autowired private EpicScriptDialogueService epicScriptDialogueService; + @Autowired + private ScriptFavoriteService scriptFavoriteService; + /** * 分页查询当前用户的爽文剧本 */ @@ -172,4 +178,31 @@ public class EpicScriptController { } return Result.success(); } + + // ==================== 剧本收藏 ==================== + + @Operation(summary = "切换收藏", description = "收藏或取消收藏指定剧本。") + @PostMapping(value = "/favorite/toggle") + public Result toggleFavorite( + @Valid @RequestBody ScriptFavoriteToggleRequest request) { + return Result.success(scriptFavoriteService.toggleFavorite(request.getScriptId())); + } + + @Operation(summary = "分页查询收藏", description = "分页查询当前用户收藏的剧本列表。") + @GetMapping(value = "/favorite/page") + public Result> getFavoritePage( + @RequestParam(defaultValue = "1") long current, + @RequestParam(defaultValue = "10") long size) { + return Result.success(scriptFavoriteService.getFavoritePage(current, size)); + } + + @Operation(summary = "检查收藏状态", description = "检查指定剧本是否已被当前用户收藏。") + @GetMapping(value = "/favorite/check") + public Result checkFavorite(@RequestParam String scriptId) { + boolean favorited = scriptFavoriteService.isFavorited(scriptId); + ScriptFavoriteResponse r = new ScriptFavoriteResponse(); + r.setScriptId(scriptId); + r.setIsFavorited(favorited); + return Result.success(r); + } } From 50f1bdba3804f8881731ef542b1ed022c6464d9a Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 19 Jul 2026 21:48:00 +0800 Subject: [PATCH 10/16] feat(script-favorite): add favorite API wrappers to epicScript.js - toggleFavoriteScript: POST /epicScript/favorite/toggle - getFavoriteScriptPage: GET /epicScript/favorite/page - checkFavoriteScript: GET /epicScript/favorite/check - Added to default export object --- mini-program/src/services/epicScript.js | 38 ++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/mini-program/src/services/epicScript.js b/mini-program/src/services/epicScript.js index 93c313e..a002481 100644 --- a/mini-program/src/services/epicScript.js +++ b/mini-program/src/services/epicScript.js @@ -198,6 +198,39 @@ export const transformListToFrontend = (backendList) => { return backendList.map(transformToFrontendFormat) } +/** + * 切换剧本收藏状态(收藏/取消收藏) + * @param {string} scriptId 剧本ID + * @returns {Promise<{success: boolean, isFavorited: boolean}>} + */ +export const toggleFavoriteScript = async (scriptId) => { + const res = await post('/epicScript/favorite/toggle', { scriptId }) + return { + success: true, + isFavorited: res.data?.isFavorited ?? false, + favoriteTime: res.data?.favoriteTime ?? null + } +} + +/** + * 获取收藏剧本列表 + * @param {number} current 当前页 + * @param {number} size 每页大小 + */ +export const getFavoriteScriptPage = async (current = 1, size = 10) => { + return get('/epicScript/favorite/page', { current, size }) +} + +/** + * 检查剧本是否已收藏 + * @param {string} scriptId 剧本ID + * @returns {Promise} + */ +export const checkFavoriteScript = async (scriptId) => { + const res = await get('/epicScript/favorite/check', { scriptId }) + return res.data?.isFavorited ?? false +} + export default { getScriptList, getScriptPage, @@ -212,5 +245,8 @@ export default { buildCharacterInfo, buildLifeEventsSummary, transformToFrontendFormat, - transformListToFrontend + transformListToFrontend, + toggleFavoriteScript, + getFavoriteScriptPage, + checkFavoriteScript, } From 05ea918ee186d83bd101515cd0578dcabd98b66b Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 19 Jul 2026 21:51:20 +0800 Subject: [PATCH 11/16] feat(script-favorite): update toggleFavorite to use backend API - Import toggleFavoriteScript from epicScript service - Change toggleFavorite from sync local-only to async with API call - Implement optimistic update pattern: update local cache first, then call backend - Rollback on API failure with error toast - Preserves localFavorites for offline/fast-render scenarios --- .../src/pages/main/ScriptLibraryView.vue | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/mini-program/src/pages/main/ScriptLibraryView.vue b/mini-program/src/pages/main/ScriptLibraryView.vue index 8970c74..2479c18 100644 --- a/mini-program/src/pages/main/ScriptLibraryView.vue +++ b/mini-program/src/pages/main/ScriptLibraryView.vue @@ -149,6 +149,7 @@