Compare commits

...

10 Commits

Author SHA1 Message Date
peanut 05ea918ee1 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
2026-07-19 21:51:20 +08:00
peanut 50f1bdba38 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
2026-07-19 21:48:00 +08:00
peanut 74fa304cd9 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
2026-07-19 21:43:46 +08:00
peanut 9d352f40a0 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
2026-07-19 21:41:54 +08:00
peanut 674188f471 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
2026-07-19 21:41:28 +08:00
peanut 62ca1730b9 feat(script-favorite): add ScriptFavoriteResponse DTO
- Fields: scriptId, isFavorited, favoriteTime
- Used by toggle/check endpoints to return favorite status
2026-07-19 21:41:14 +08:00
peanut 0d27d1a072 feat(script-favorite): add ScriptFavoriteToggleRequest DTO
- Single field: scriptId with @NotBlank validation
- Uses javax.validation.constraints.NotBlank matching project convention
2026-07-19 21:41:02 +08:00
peanut d74d8b22c6 feat(script-favorite): add ScriptFavoriteMapper
- Extends BaseMapper<ScriptFavorite>
- Standard MyBatis-Plus mapper pattern matching project convention
2026-07-19 21:40:06 +08:00
peanut 489131a04b 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
2026-07-19 21:39:47 +08:00
peanut 43ac037f5b 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
2026-07-19 21:39:27 +08:00
10 changed files with 371 additions and 6 deletions
@@ -149,6 +149,7 @@
<script setup>
import { computed, ref } from 'vue'
import { useAppStore } from '../../stores/app.js'
import { toggleFavoriteScript } from '../../services/epicScript.js'
const store = useAppStore()
const activeType = ref('long')
@@ -316,15 +317,34 @@ const toggleScriptMenu = (script) => {
activeMenuId.value = activeMenuId.value === id ? '' : id
}
const toggleFavorite = (script) => {
const favorite = isFavorite(script)
const toggleFavorite = async (script) => {
const wasFav = isFavorite(script)
// 乐观更新:先改本地缓存
const next = { ...localFavorites.value }
if (favorite) delete next[String(script.id)]
else next[String(script.id)] = true
if (wasFav) {
delete next[String(script.id)]
} else {
next[String(script.id)] = true
}
localFavorites.value = next
uni.setStorageSync('script_favorites', next)
closeScriptMenu()
uni.showToast({ title: favorite ? '已取消收藏' : '已收藏', icon: 'success' })
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 continueScript = (script) => {
+37 -1
View File
@@ -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<boolean>}
*/
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,
}
@@ -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<ScriptFavoriteResponse> toggleFavorite(
@Valid @RequestBody ScriptFavoriteToggleRequest request) {
return Result.success(scriptFavoriteService.toggleFavorite(request.getScriptId()));
}
@Operation(summary = "分页查询收藏", description = "分页查询当前用户收藏的剧本列表。")
@GetMapping(value = "/favorite/page")
public Result<PageResult<EpicScriptResponse>> 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<ScriptFavoriteResponse> checkFavorite(@RequestParam String scriptId) {
boolean favorited = scriptFavoriteService.isFavorited(scriptId);
ScriptFavoriteResponse r = new ScriptFavoriteResponse();
r.setScriptId(scriptId);
r.setIsFavorited(favorited);
return Result.success(r);
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<ScriptFavorite> {
}
@@ -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<ScriptFavorite> {
ScriptFavoriteResponse toggleFavorite(String scriptId);
PageResult<EpicScriptResponse> getFavoritePage(long current, long size);
Set<String> getFavoritedScriptIds(Set<String> scriptIds);
boolean isFavorited(String scriptId);
long getFavoriteCount();
}
@@ -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<ScriptFavoriteMapper, ScriptFavorite>
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<ScriptFavorite> 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<EpicScriptResponse> getFavoritePage(long current, long size) {
String userId = UserContextHolder.getCurrentUserId();
if (!StringUtils.hasText(userId)) {
return new PageResult<>();
}
LambdaQueryWrapper<ScriptFavorite> w = new LambdaQueryWrapper<>();
w.eq(ScriptFavorite::getUserId, userId)
.eq(ScriptFavorite::getIsDeleted, 0)
.orderByDesc(ScriptFavorite::getCreateTime);
Page<ScriptFavorite> page = new Page<>(current, size);
Page<ScriptFavorite> result = this.page(page, w);
List<EpicScriptResponse> records = result.getRecords().stream()
.map(ScriptFavorite::getScriptId)
.map(epicScriptService::getScriptById)
.filter(Objects::nonNull)
.collect(Collectors.toList());
PageResult<EpicScriptResponse> 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> getFavoritedScriptIds(Set<String> scriptIds) {
String userId = UserContextHolder.getCurrentUserId();
if (!StringUtils.hasText(userId) || scriptIds == null || scriptIds.isEmpty()) {
return Collections.emptySet();
}
LambdaQueryWrapper<ScriptFavorite> 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<ScriptFavorite> 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<ScriptFavorite> w = new LambdaQueryWrapper<>();
w.eq(ScriptFavorite::getUserId, userId)
.eq(ScriptFavorite::getIsDeleted, 0);
return this.count(w);
}
}
@@ -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='剧本收藏关联表';