feat: 人生事件和剧本收藏从 localStorage 切换为真实 API

- 后端:新增 t_event_favorite 表(UNIQUE KEY + MyBatis-Plus Entity)
- 后端:LifeEventController 新增 /favorite/toggle、/favorite/page、/favorite/check 三个真实端点
- 后端:EventFavoriteService 完整 CRUD,含 toggle/分页/批量查询/状态检查
- 后端:删除旧的 favorite-placeholder mock 端点
- 前端:life-event/detail.vue 从 localStorage 假收藏切换为真实 API + localStorage 缓存防闪烁
- 前端:ScriptLibraryView.vue 从 localFavorites 切换为 checkFavoriteScript 真实 API
- 前端:stores/app.js 新增 toggleEventFavorite / checkEventFavorite 方法
- 修复:审核发现的 UNIQUE 约束缺失、未使用 watch 导入、Controller 参数注解遗漏、Controller 委托 Service 构造 Response

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-19 23:23:40 +08:00
parent a3bfad099a
commit dd11cfae73
12 changed files with 451 additions and 41 deletions
@@ -2,10 +2,13 @@ package com.emotion.controller;
import com.emotion.common.PageResult;
import com.emotion.common.Result;
import com.emotion.dto.request.EventFavoriteToggleRequest;
import com.emotion.dto.request.LifeEventCreateRequest;
import com.emotion.dto.request.LifeEventPageRequest;
import com.emotion.dto.request.LifeEventUpdateRequest;
import com.emotion.dto.response.EventFavoriteResponse;
import com.emotion.dto.response.LifeEventResponse;
import com.emotion.service.EventFavoriteService;
import com.emotion.service.LifeEventService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -35,6 +38,9 @@ public class LifeEventController {
@Autowired
private LifeEventService lifeEventService;
@Autowired
private EventFavoriteService eventFavoriteService;
/**
* 分页查询当前用户的生命事件
*/
@@ -150,18 +156,6 @@ public class LifeEventController {
return Result.success(data);
}
@Operation(summary = "收藏占位接口", description = "返回收藏功能的占位响应。")
@PostMapping(value = "/favorite-placeholder")
public Result<Map<String, Object>> favoritePlaceholder(@RequestBody Map<String, Object> request) {
String id = stringValue(request.get("id"), "");
Boolean favorite = Boolean.TRUE.equals(request.get("favorite"));
Map<String, Object> data = new HashMap<>();
data.put("id", id);
data.put("favorite", favorite);
data.put("placeholder", true);
return Result.success(data);
}
private String stringValue(Object value, String fallback) {
if (value == null) {
return fallback;
@@ -170,6 +164,30 @@ public class LifeEventController {
return text.isEmpty() ? fallback : text;
}
// ==================== 人生事件收藏 ====================
@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(
@Parameter(description = "事件 ID") @RequestParam String eventId) {
return Result.success(eventFavoriteService.checkFavorite(eventId));
}
@SuppressWarnings("unchecked")
private List<String> readTags(Object value) {
List<String> tags = new ArrayList<>();
@@ -0,0 +1,18 @@
package com.emotion.dto.request;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 人生事件收藏切换请求
*
* @author huazhongmin
* @date 2026-07-19
*/
@Data
public class EventFavoriteToggleRequest {
@NotBlank(message = "事件ID不能为空")
private String eventId;
}
@@ -0,0 +1,19 @@
package com.emotion.dto.response;
import lombok.Data;
/**
* 人生事件收藏状态响应
*
* @author huazhongmin
* @date 2026-07-19
*/
@Data
public class EventFavoriteResponse {
private String eventId;
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_event_favorite")
public class EventFavorite extends BaseEntity {
@TableField("user_id")
private String userId;
@TableField("event_id")
private String eventId;
}
@@ -0,0 +1,15 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.EventFavorite;
import org.apache.ibatis.annotations.Mapper;
/**
* 人生事件收藏Mapper接口
*
* @author huazhongmin
* @date 2026-07-19
*/
@Mapper
public interface EventFavoriteMapper extends BaseMapper<EventFavorite> {
}
@@ -0,0 +1,66 @@
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 huazhongmin
* @date 2026-07-19
*/
public interface EventFavoriteService extends IService<EventFavorite> {
/**
* 切换收藏状态(收藏/取消收藏)
*
* @param eventId 事件ID
* @return 收藏状态响应
*/
EventFavoriteResponse toggleFavorite(String eventId);
/**
* 分页查询当前用户收藏的事件列表
*
* @param current 当前页码
* @param size 每页大小
* @return 分页结果
*/
PageResult<LifeEventResponse> getFavoritePage(long current, long size);
/**
* 批量查询已收藏的事件ID集合
*
* @param eventIds 待查询的事件ID集合
* @return 已收藏的事件ID集合
*/
Set<String> getFavoritedEventIds(Set<String> eventIds);
/**
* 检查收藏状态
*
* @param eventId 事件ID
* @return 收藏状态响应
*/
EventFavoriteResponse checkFavorite(String eventId);
/**
* 检查指定事件是否已被当前用户收藏
*
* @param eventId 事件ID
* @return 是否已收藏
*/
boolean isFavorited(String eventId);
/**
* 获取当前用户的收藏总数
*
* @return 收藏总数
*/
long getFavoriteCount();
}
@@ -0,0 +1,157 @@
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 huazhongmin
* @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 EventFavoriteResponse checkFavorite(String eventId) {
EventFavoriteResponse r = new EventFavoriteResponse();
r.setEventId(eventId);
r.setIsFavorited(isFavorited(eventId));
return r;
}
@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);
}
}