feat: 人生事件和剧本收藏从 localStorage 切换为真实 API
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,939 @@
|
|||||||
|
---
|
||||||
|
author: AI Assistant
|
||||||
|
created_at: 2026-07-19
|
||||||
|
purpose: 小程序收藏功能真实 API 化实施计划
|
||||||
|
---
|
||||||
|
|
||||||
|
# 收藏功能真实 API 化实施计划
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 前端收藏功能从 localStorage 切换到真实 API。剧本收藏沿用现有后端 API,人生事件收藏新建独立表和 API。
|
||||||
|
|
||||||
|
**Architecture:** 剧本收藏后端已完整(`t_script_favorite` 表 + `ScriptFavoriteService`),前端 `ScriptLibraryView.vue` 未对接。人生事件收藏仿照剧本,新建 `t_event_favorite` 表 + `EventFavoriteService` + Controller,删除假接口 `/lifeEvent/favorite-placeholder`。前端所有 localStorage 收藏逻辑切换到 API。
|
||||||
|
|
||||||
|
**Tech Stack:** Spring Boot 2.7.18 + MyBatis-Plus 3.5.3.1 + UniApp Vue 3 + Pinia + Element Plus + rpx
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 后端人生事件收藏功能(Entity + Mapper + Service + Controller)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `server/src/main/resources/db/migration/V20260719000002__create_event_favorite.sql`
|
||||||
|
- Create: `server/src/main/java/com/emotion/entity/EventFavorite.java`
|
||||||
|
- Create: `server/src/main/java/com/emotion/mapper/EventFavoriteMapper.java`
|
||||||
|
- Create: `server/src/main/java/com/emotion/dto/request/EventFavoriteToggleRequest.java`
|
||||||
|
- Create: `server/src/main/java/com/emotion/dto/response/EventFavoriteResponse.java`
|
||||||
|
- Create: `server/src/main/java/com/emotion/service/EventFavoriteService.java`
|
||||||
|
- Create: `server/src/main/java/com/emotion/service/impl/EventFavoriteServiceImpl.java`
|
||||||
|
- Modify: `server/src/main/java/com/emotion/controller/LifeEventController.java`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建数据库迁移 SQL**
|
||||||
|
|
||||||
|
```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='人生事件收藏关联表';
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 创建 EventFavorite 实体类**
|
||||||
|
|
||||||
|
```java
|
||||||
|
// server/src/main/java/com/emotion/entity/EventFavorite.java
|
||||||
|
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 AI Assistant
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 创建 EventFavoriteMapper**
|
||||||
|
|
||||||
|
```java
|
||||||
|
// server/src/main/java/com/emotion/mapper/EventFavoriteMapper.java
|
||||||
|
package com.emotion.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.emotion.entity.EventFavorite;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人生事件收藏 Mapper
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @date 2026-07-19
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface EventFavoriteMapper extends BaseMapper<EventFavorite> {
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 创建 EventFavoriteToggleRequest DTO**
|
||||||
|
|
||||||
|
```java
|
||||||
|
// server/src/main/java/com/emotion/dto/request/EventFavoriteToggleRequest.java
|
||||||
|
package com.emotion.dto.request;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人生事件收藏切换请求
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @date 2026-07-19
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class EventFavoriteToggleRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "事件ID不能为空")
|
||||||
|
private String eventId;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 创建 EventFavoriteResponse DTO**
|
||||||
|
|
||||||
|
```java
|
||||||
|
// server/src/main/java/com/emotion/dto/response/EventFavoriteResponse.java
|
||||||
|
package com.emotion.dto.response;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人生事件收藏状态响应
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @date 2026-07-19
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class EventFavoriteResponse {
|
||||||
|
|
||||||
|
private String eventId;
|
||||||
|
|
||||||
|
private Boolean isFavorited;
|
||||||
|
|
||||||
|
private String favoriteTime;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: 创建 EventFavoriteService 接口**
|
||||||
|
|
||||||
|
```java
|
||||||
|
// server/src/main/java/com/emotion/service/EventFavoriteService.java
|
||||||
|
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 AI Assistant
|
||||||
|
* @date 2026-07-19
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: 创建 EventFavoriteServiceImpl 实现**
|
||||||
|
|
||||||
|
```java
|
||||||
|
// server/src/main/java/com/emotion/service/impl/EventFavoriteServiceImpl.java
|
||||||
|
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 AI Assistant
|
||||||
|
* @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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 8: 修改 LifeEventController 添加收藏 API**
|
||||||
|
|
||||||
|
在 `server/src/main/java/com/emotion/controller/LifeEventController.java` 中添加:
|
||||||
|
|
||||||
|
1. 在类顶部添加注入:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Autowired
|
||||||
|
private EventFavoriteService eventFavoriteService;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 添加 import:
|
||||||
|
|
||||||
|
```java
|
||||||
|
import com.emotion.dto.request.EventFavoriteToggleRequest;
|
||||||
|
import com.emotion.dto.response.EventFavoriteResponse;
|
||||||
|
import com.emotion.service.EventFavoriteService;
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 在类末尾(`readTags` 方法之前)添加收藏 API:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ==================== 人生事件收藏 ====================
|
||||||
|
|
||||||
|
@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(@RequestParam String eventId) {
|
||||||
|
boolean favorited = eventFavoriteService.isFavorited(eventId);
|
||||||
|
EventFavoriteResponse r = new EventFavoriteResponse();
|
||||||
|
r.setEventId(eventId);
|
||||||
|
r.setIsFavorited(favorited);
|
||||||
|
return Result.success(r);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **删除** `/lifeEvent/favorite-placeholder` 假接口(第 153-163 行):
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 删除以下方法
|
||||||
|
@Operation(summary = "收藏占位接口", description = "返回收藏功能的占位响应。")
|
||||||
|
@PostMapping(value = "/favorite-placeholder")
|
||||||
|
public Result<Map<String, Object>> favoritePlaceholder(@RequestBody Map<String, Object> request) {
|
||||||
|
// ... 整段删除
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 9: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add server/src/main/resources/db/migration/V20260719000002__create_event_favorite.sql
|
||||||
|
git add server/src/main/java/com/emotion/entity/EventFavorite.java
|
||||||
|
git add server/src/main/java/com/emotion/mapper/EventFavoriteMapper.java
|
||||||
|
git add server/src/main/java/com/emotion/dto/request/EventFavoriteToggleRequest.java
|
||||||
|
git add server/src/main/java/com/emotion/dto/response/EventFavoriteResponse.java
|
||||||
|
git add server/src/main/java/com/emotion/service/EventFavoriteService.java
|
||||||
|
git add server/src/main/java/com/emotion/service/impl/EventFavoriteServiceImpl.java
|
||||||
|
git add server/src/main/java/com/emotion/controller/LifeEventController.java
|
||||||
|
git commit -m "feat: 新增人生事件收藏功能(独立收藏表 + API)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: 后端编译 + 部署到服务器
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Build: `server/target/`
|
||||||
|
- Deploy: 服务器
|
||||||
|
|
||||||
|
- [ ] **Step 1: 本地编译验证**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
mvn clean install -DskipTests
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: BUILD SUCCESS,无编译错误。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 部署到服务器**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ..
|
||||||
|
python deploy.py backend
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 部署成功,服务器自动重启。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证部署**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python deploy.py verify
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 后端服务运行正常。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit -m "chore: 后端人生事件收藏功能已部署" --allow-empty
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: 前端 ScriptLibraryView.vue 切换到剧本收藏 API
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mini-program/src/pages/main/ScriptLibraryView.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 删除 localFavorites ref,改用内存状态**
|
||||||
|
|
||||||
|
删除第 160 行:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 删除这一行
|
||||||
|
const localFavorites = ref(uni.getStorageSync('script_favorites') || {})
|
||||||
|
```
|
||||||
|
|
||||||
|
替换为:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const favoriteStatus = ref({})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 修改 isFavorite 计算**
|
||||||
|
|
||||||
|
修改第 245-247 行:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 之前:
|
||||||
|
const isFavorite = (script) => {
|
||||||
|
return Boolean(script.isFavorite || script.favorite || localFavorites.value[String(script.id)])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 之后:
|
||||||
|
const isFavorite = (script) => {
|
||||||
|
return Boolean(favoriteStatus.value[String(script.id)])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 修改 toggleFavorite,删除 localStorage 写**
|
||||||
|
|
||||||
|
修改第 320-348 行:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 之前:
|
||||||
|
const toggleFavorite = async (script) => {
|
||||||
|
const wasFav = isFavorite(script)
|
||||||
|
// 乐观更新:先改本地缓存
|
||||||
|
const next = { ...localFavorites.value }
|
||||||
|
if (wasFav) {
|
||||||
|
delete next[String(script.id)]
|
||||||
|
} else {
|
||||||
|
next[String(script.id)] = true
|
||||||
|
}
|
||||||
|
localFavorites.value = next
|
||||||
|
uni.setStorageSync('script_favorites', next)
|
||||||
|
closeScriptMenu()
|
||||||
|
|
||||||
|
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 toggleFavorite = async (script) => {
|
||||||
|
const wasFav = isFavorite(script)
|
||||||
|
// 乐观更新:先改内存状态
|
||||||
|
const next = { ...favoriteStatus.value }
|
||||||
|
if (wasFav) {
|
||||||
|
delete next[String(script.id)]
|
||||||
|
} else {
|
||||||
|
next[String(script.id)] = true
|
||||||
|
}
|
||||||
|
favoriteStatus.value = next
|
||||||
|
closeScriptMenu()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await toggleFavoriteScript(script.id)
|
||||||
|
uni.showToast({ title: result.isFavorited ? '已收藏' : '已取消收藏', icon: 'success' })
|
||||||
|
} catch (error) {
|
||||||
|
// 回滚乐观更新
|
||||||
|
const restored = { ...favoriteStatus.value }
|
||||||
|
if (wasFav) {
|
||||||
|
restored[String(script.id)] = true
|
||||||
|
} else {
|
||||||
|
delete restored[String(script.id)]
|
||||||
|
}
|
||||||
|
favoriteStatus.value = restored
|
||||||
|
uni.showToast({ title: error?.message || '操作失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 修改 confirmDeleteScript,删除 localStorage 清理**
|
||||||
|
|
||||||
|
修改第 386-389 行:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 之前:
|
||||||
|
const next = { ...localFavorites.value }
|
||||||
|
delete next[id]
|
||||||
|
localFavorites.value = next
|
||||||
|
uni.setStorageSync('script_favorites', next)
|
||||||
|
|
||||||
|
// 之后:
|
||||||
|
const next = { ...favoriteStatus.value }
|
||||||
|
delete next[id]
|
||||||
|
favoriteStatus.value = next
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 列表加载后批量检查收藏状态**
|
||||||
|
|
||||||
|
在 `visibleScripts` computed 后添加 watch 或 onMounted 钩子,批量调用 `checkFavoriteScript`:
|
||||||
|
|
||||||
|
在文件顶部添加 import:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { checkFavoriteScript } from '../../services/epicScript.js'
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `visibleScripts` computed 后添加:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 列表加载后批量检查收藏状态
|
||||||
|
const loadFavoriteStatus = async () => {
|
||||||
|
const ids = scripts.value.map(s => String(s.id)).filter(id => !id.startsWith('demo-'))
|
||||||
|
if (!ids.length) return
|
||||||
|
const status = {}
|
||||||
|
await Promise.all(ids.map(async (id) => {
|
||||||
|
try {
|
||||||
|
status[id] = await checkFavoriteScript(id)
|
||||||
|
} catch {
|
||||||
|
status[id] = false
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
favoriteStatus.value = status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 scripts 变化时重新加载收藏状态
|
||||||
|
watch(scripts, () => {
|
||||||
|
loadFavoriteStatus()
|
||||||
|
}, { immediate: true })
|
||||||
|
```
|
||||||
|
|
||||||
|
确保 import watch:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add mini-program/src/pages/main/ScriptLibraryView.vue
|
||||||
|
git commit -m "refactor: 剧本收藏从 localStorage 切换到真实 API"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: 前端 life-event 切换到人生事件收藏 API
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mini-program/src/services/lifeEvent.js`
|
||||||
|
- Modify: `mini-program/src/pages/life-event/detail.vue`
|
||||||
|
- Modify: `mini-program/src/stores/app.js`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 修改 lifeEvent.js 新增收藏 API 封装**
|
||||||
|
|
||||||
|
在 `mini-program/src/services/lifeEvent.js` 中:
|
||||||
|
|
||||||
|
1. **删除**旧的 `favoriteEvent` 方法(第 67-69 行):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 删除:
|
||||||
|
export const favoriteEvent = async ({ id, favorite }) => {
|
||||||
|
return post('/lifeEvent/favorite-placeholder', { id, favorite })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **新增**真实的收藏 API:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* 切换人生事件收藏状态(收藏/取消收藏)
|
||||||
|
* @param {string} eventId 事件ID
|
||||||
|
* @returns {Promise<{success: boolean, isFavorited: boolean}>}
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查人生事件是否已收藏
|
||||||
|
* @param {string} eventId 事件ID
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
export const checkFavoriteEvent = async (eventId) => {
|
||||||
|
const res = await get('/lifeEvent/favorite/check', { eventId })
|
||||||
|
return res.data?.isFavorited ?? false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 在 default export 中更新方法列表:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default {
|
||||||
|
getEventList,
|
||||||
|
getEventPage,
|
||||||
|
getEventById,
|
||||||
|
createEvent,
|
||||||
|
updateEvent,
|
||||||
|
deleteEvent,
|
||||||
|
assistEventWriting,
|
||||||
|
chatAboutEvent,
|
||||||
|
shareEvent,
|
||||||
|
toggleFavoriteEvent,
|
||||||
|
checkFavoriteEvent,
|
||||||
|
transformToFrontendFormat,
|
||||||
|
transformListToFrontend
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 修改 stores/app.js 的 favoriteEvent action**
|
||||||
|
|
||||||
|
在 `mini-program/src/stores/app.js` 中:
|
||||||
|
|
||||||
|
1. **删除**旧的 `favoriteEvent` action(第 235-242 行):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 删除:
|
||||||
|
const favoriteEvent = async ({ id, favorite }) => {
|
||||||
|
try {
|
||||||
|
const res = await lifeEventService.favoriteEvent({ id, favorite })
|
||||||
|
return { success: true, data: res.data }
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **新增**新的 action:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const toggleEventFavorite = async (eventId) => {
|
||||||
|
try {
|
||||||
|
const result = await lifeEventService.toggleFavoriteEvent(eventId)
|
||||||
|
return { success: true, data: result }
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkEventFavorite = async (eventId) => {
|
||||||
|
try {
|
||||||
|
const isFavorited = await lifeEventService.checkFavoriteEvent(eventId)
|
||||||
|
return { success: true, data: isFavorited }
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 在 return 对象中更新导出:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
return {
|
||||||
|
// ... 其他方法
|
||||||
|
toggleEventFavorite,
|
||||||
|
checkEventFavorite,
|
||||||
|
// 删除 favoriteEvent
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 修改 detail.vue 初始化时检查收藏状态**
|
||||||
|
|
||||||
|
在 `mini-program/src/pages/life-event/detail.vue` 中:
|
||||||
|
|
||||||
|
1. 修改 `onMounted`(第 137-149 行):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 之前:
|
||||||
|
onMounted(async () => {
|
||||||
|
const info = uni.getWindowInfo()
|
||||||
|
safeAreaBottom.value = info.safeAreaInsets?.bottom || 0
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
eventId.value = pages[pages.length - 1]?.options?.id || ''
|
||||||
|
cachedEvent.value = uni.getStorageSync('current_life_event') || null
|
||||||
|
if (!store.events?.length) await store.fetchEvents()
|
||||||
|
isFavorite.value = Boolean(uni.getStorageSync(`event_favorite_${eventId.value}`))
|
||||||
|
analytics.trackPageView(pagePath, {
|
||||||
|
life_event_id: eventId.value,
|
||||||
|
tag_count: Array.isArray(displayEvent.value.tags) ? displayEvent.value.tags.length : 0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 之后:
|
||||||
|
onMounted(async () => {
|
||||||
|
const info = uni.getWindowInfo()
|
||||||
|
safeAreaBottom.value = info.safeAreaInsets?.bottom || 0
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
eventId.value = pages[pages.length - 1]?.options?.id || ''
|
||||||
|
cachedEvent.value = uni.getStorageSync('current_life_event') || null
|
||||||
|
if (!store.events?.length) await store.fetchEvents()
|
||||||
|
// 从 API 检查收藏状态
|
||||||
|
const result = await store.checkEventFavorite(eventId.value)
|
||||||
|
if (result.success) {
|
||||||
|
isFavorite.value = result.data
|
||||||
|
}
|
||||||
|
analytics.trackPageView(pagePath, {
|
||||||
|
life_event_id: eventId.value,
|
||||||
|
tag_count: Array.isArray(displayEvent.value.tags) ? displayEvent.value.tags.length : 0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 修改 detail.vue 的 toggleFavorite,删除 localStorage 写**
|
||||||
|
|
||||||
|
修改第 306-322 行:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 之前:
|
||||||
|
const toggleFavorite = async () => {
|
||||||
|
if (!eventId.value) return
|
||||||
|
const next = !isFavorite.value
|
||||||
|
const result = await store.favoriteEvent({ id: eventId.value, favorite: next })
|
||||||
|
if (!result.success) {
|
||||||
|
uni.showToast({ title: result.error || '收藏失败', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isFavorite.value = next
|
||||||
|
if (next) uni.setStorageSync(`event_favorite_${eventId.value}`, '1')
|
||||||
|
else uni.removeStorageSync(`event_favorite_${eventId.value}`)
|
||||||
|
analytics.track('life_event_favorite', {
|
||||||
|
life_event_id: eventId.value,
|
||||||
|
favorite: next
|
||||||
|
}, { eventType: 'life_event', pagePath })
|
||||||
|
uni.showToast({ title: next ? '已收藏' : '已取消收藏', icon: 'success' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 之后:
|
||||||
|
const toggleFavorite = async () => {
|
||||||
|
if (!eventId.value) return
|
||||||
|
const result = await store.toggleEventFavorite(eventId.value)
|
||||||
|
if (!result.success) {
|
||||||
|
uni.showToast({ title: result.error || '收藏失败', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isFavorite.value = result.data?.isFavorited ?? false
|
||||||
|
analytics.track('life_event_favorite', {
|
||||||
|
life_event_id: eventId.value,
|
||||||
|
favorite: isFavorite.value
|
||||||
|
}, { eventType: 'life_event', pagePath })
|
||||||
|
uni.showToast({ title: isFavorite.value ? '已收藏' : '已取消收藏', icon: 'success' })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add mini-program/src/services/lifeEvent.js
|
||||||
|
git add mini-program/src/stores/app.js
|
||||||
|
git add mini-program/src/pages/life-event/detail.vue
|
||||||
|
git commit -m "refactor: 人生事件收藏从 localStorage 切换到真实 API"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: H5 浏览器验证
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Test: `http://localhost:5180` (H5 开发服务器)
|
||||||
|
|
||||||
|
- [ ] **Step 1: 确认 H5 开发服务器运行中**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python dev-services.py status
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `mini-program` 服务在端口 5180 上运行。如未运行,执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python dev-services.py start mini-program
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 浏览器打开页面并登录**
|
||||||
|
|
||||||
|
1. 打开 `http://localhost:5180`
|
||||||
|
2. 如未登录,使用手机号验证码 123456 登录
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证剧本收藏**
|
||||||
|
|
||||||
|
1. 点击底部导航「爽文生成」
|
||||||
|
2. 点击「剧本库」
|
||||||
|
3. 点击任意剧本的收藏图标(心形)
|
||||||
|
4. 确认:
|
||||||
|
- ✅ Toast 提示「已收藏」
|
||||||
|
- ✅ 心形图标变为实心
|
||||||
|
- ✅ Network 面板中有 `POST /epicScript/favorite/toggle` 请求,返回 `isFavorited: true`
|
||||||
|
5. 刷新页面
|
||||||
|
6. 确认收藏状态仍保持(心形图标仍为实心)
|
||||||
|
7. 再次点击收藏图标取消收藏
|
||||||
|
8. 确认:
|
||||||
|
- ✅ Toast 提示「已取消收藏」
|
||||||
|
- ✅ 心形图标变为空心
|
||||||
|
- ✅ Network 面板中有 `POST /epicScript/favorite/toggle` 请求,返回 `isFavorited: false`
|
||||||
|
|
||||||
|
- [ ] **Step 4: 验证人生事件收藏**
|
||||||
|
|
||||||
|
1. 点击底部导航「人生轨迹」
|
||||||
|
2. 点击任意事件进入详情页
|
||||||
|
3. 点击收藏按钮
|
||||||
|
4. 确认:
|
||||||
|
- ✅ Toast 提示「已收藏」
|
||||||
|
- ✅ 收藏按钮状态变化
|
||||||
|
- ✅ Network 面板中有 `POST /lifeEvent/favorite/toggle` 请求,返回 `isFavorited: true`
|
||||||
|
5. 返回列表页,再次进入同一事件详情
|
||||||
|
6. 确认收藏状态仍保持
|
||||||
|
7. 再次点击收藏按钮取消收藏
|
||||||
|
8. 确认:
|
||||||
|
- ✅ Toast 提示「已取消收藏」
|
||||||
|
- ✅ 收藏按钮状态恢复
|
||||||
|
- ✅ Network 面板中有 `POST /lifeEvent/favorite/toggle` 请求,返回 `isFavorited: false`
|
||||||
|
|
||||||
|
- [ ] **Step 5: 检查浏览器 Console 无报错**
|
||||||
|
|
||||||
|
打开浏览器 DevTools → Console 面板,确认:
|
||||||
|
- ✅ 无红色 ERROR
|
||||||
|
- ✅ 无 localStorage 相关的 `script_favorites` 或 `event_favorite_` 读写
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交(如有微调)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add mini-program/src/
|
||||||
|
git commit -m "test: H5 浏览器验证收藏功能通过"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: 通知用户验证
|
||||||
|
|
||||||
|
- [ ] **Step 1: 提示用户操作**
|
||||||
|
|
||||||
|
告知用户:
|
||||||
|
|
||||||
|
> 收藏功能已完成真实 API 化改造:
|
||||||
|
>
|
||||||
|
> **剧本收藏**:
|
||||||
|
> - 前端从 localStorage 切换到现有后端 API(`POST /epicScript/favorite/toggle`)
|
||||||
|
> - 收藏状态从服务器读取,不再依赖本地缓存
|
||||||
|
>
|
||||||
|
> **人生事件收藏**:
|
||||||
|
> - 后端新建独立的 `t_event_favorite` 表 + API
|
||||||
|
> - 前端从 localStorage 切换到真实 API(`POST /lifeEvent/favorite/toggle`)
|
||||||
|
> - 删除了假接口 `/lifeEvent/favorite-placeholder`
|
||||||
|
>
|
||||||
|
> **验证方式**:
|
||||||
|
> 1. 本地 H5(`http://localhost:5180`)已验证通过
|
||||||
|
> 2. 后端已部署到服务器
|
||||||
|
> 3. 请在**微信开发者工具**中:
|
||||||
|
> - 打开 `unpackage/dist/dev/mp-weixin` 目录
|
||||||
|
> - 点击「编译」重新编译
|
||||||
|
> - 测试剧本收藏和人生事件收藏功能
|
||||||
|
> - 确认收藏状态刷新后仍保持
|
||||||
|
>
|
||||||
|
> 如有问题,截图反馈。
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
---
|
||||||
|
author: AI Assistant
|
||||||
|
created_at: 2026-07-19
|
||||||
|
purpose: 小程序收藏功能真实 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` | `isFavorite` 从 `script.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` | `isFavorite` 从 `event_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` 表。
|
||||||
|
|
||||||
|
```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='人生事件收藏关联表';
|
||||||
|
```
|
||||||
|
|
||||||
|
**选择独立表的理由**:
|
||||||
|
1. 与剧本收藏架构保持一致,两套收藏机制对称
|
||||||
|
2. 支持多用户场景(每个用户独立收藏)
|
||||||
|
3. 支持分页查询"我的收藏"列表
|
||||||
|
4. 逻辑删除保留历史记录
|
||||||
|
|
||||||
|
### 后端改动
|
||||||
|
|
||||||
|
#### 1. 人生事件收藏(新增)
|
||||||
|
|
||||||
|
**Entity**:`EventFavorite.java`
|
||||||
|
|
||||||
|
```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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mapper**:`EventFavoriteMapper.java`
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Mapper
|
||||||
|
public interface EventFavoriteMapper extends BaseMapper<EventFavorite> {
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Service**:`EventFavoriteService.java` + `EventFavoriteServiceImpl.java`
|
||||||
|
|
||||||
|
```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 }`
|
||||||
|
|
||||||
|
**Controller**:`LifeEventController.java` 新增
|
||||||
|
|
||||||
|
```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`
|
||||||
|
|
||||||
|
**修改**:
|
||||||
|
```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`:
|
||||||
|
|
||||||
|
```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` 部署到服务器后,在服务器环境验证
|
||||||
@@ -134,14 +134,41 @@ const isDescriptionCollapsed = ref(false)
|
|||||||
const scrollTop = ref(0)
|
const scrollTop = ref(0)
|
||||||
const currentScrollTop = ref(0)
|
const currentScrollTop = ref(0)
|
||||||
|
|
||||||
|
// 从 localStorage 读取缓存的收藏状态
|
||||||
|
const getCachedFavoriteStatus = (id) => {
|
||||||
|
const cached = uni.getStorageSync(`favorite_event_${id}`)
|
||||||
|
return cached === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存收藏状态到 localStorage
|
||||||
|
const setCachedFavoriteStatus = (id, status) => {
|
||||||
|
uni.setStorageSync(`favorite_event_${id}`, status ? 'true' : 'false')
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const info = uni.getWindowInfo()
|
const info = uni.getWindowInfo()
|
||||||
safeAreaBottom.value = info.safeAreaInsets?.bottom || 0
|
safeAreaBottom.value = info.safeAreaInsets?.bottom || 0
|
||||||
const pages = getCurrentPages()
|
const pages = getCurrentPages()
|
||||||
eventId.value = pages[pages.length - 1]?.options?.id || ''
|
eventId.value = pages[pages.length - 1]?.options?.id || ''
|
||||||
|
|
||||||
|
// 先从缓存读取收藏状态,避免闪烁
|
||||||
|
if (eventId.value) {
|
||||||
|
isFavorite.value = getCachedFavoriteStatus(eventId.value)
|
||||||
|
}
|
||||||
|
|
||||||
cachedEvent.value = uni.getStorageSync('current_life_event') || null
|
cachedEvent.value = uni.getStorageSync('current_life_event') || null
|
||||||
if (!store.events?.length) await store.fetchEvents()
|
if (!store.events?.length) await store.fetchEvents()
|
||||||
isFavorite.value = Boolean(uni.getStorageSync(`event_favorite_${eventId.value}`))
|
|
||||||
|
// 异步校验真实收藏状态并更新缓存
|
||||||
|
if (eventId.value) {
|
||||||
|
const result = await store.checkEventFavorite(eventId.value)
|
||||||
|
if (result.success) {
|
||||||
|
const realStatus = result.data
|
||||||
|
isFavorite.value = realStatus
|
||||||
|
setCachedFavoriteStatus(eventId.value, realStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
analytics.trackPageView(pagePath, {
|
analytics.trackPageView(pagePath, {
|
||||||
life_event_id: eventId.value,
|
life_event_id: eventId.value,
|
||||||
tag_count: Array.isArray(displayEvent.value.tags) ? displayEvent.value.tags.length : 0
|
tag_count: Array.isArray(displayEvent.value.tags) ? displayEvent.value.tags.length : 0
|
||||||
@@ -305,20 +332,19 @@ const editEvent = () => {
|
|||||||
|
|
||||||
const toggleFavorite = async () => {
|
const toggleFavorite = async () => {
|
||||||
if (!eventId.value) return
|
if (!eventId.value) return
|
||||||
const next = !isFavorite.value
|
const result = await store.toggleEventFavorite(eventId.value)
|
||||||
const result = await store.favoriteEvent({ id: eventId.value, favorite: next })
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
uni.showToast({ title: result.error || '收藏失败', icon: 'none' })
|
uni.showToast({ title: result.error || '收藏失败', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isFavorite.value = next
|
const newStatus = result.data?.isFavorited ?? false
|
||||||
if (next) uni.setStorageSync(`event_favorite_${eventId.value}`, '1')
|
isFavorite.value = newStatus
|
||||||
else uni.removeStorageSync(`event_favorite_${eventId.value}`)
|
setCachedFavoriteStatus(eventId.value, newStatus)
|
||||||
analytics.track('life_event_favorite', {
|
analytics.track('life_event_favorite', {
|
||||||
life_event_id: eventId.value,
|
life_event_id: eventId.value,
|
||||||
favorite: next
|
favorite: newStatus
|
||||||
}, { eventType: 'life_event', pagePath })
|
}, { eventType: 'life_event', pagePath })
|
||||||
uni.showToast({ title: next ? '已收藏' : '已取消收藏', icon: 'success' })
|
uni.showToast({ title: newStatus ? '已收藏' : '已取消收藏', icon: 'success' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatEvent = async () => {
|
const chatEvent = async () => {
|
||||||
|
|||||||
@@ -147,8 +147,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useAppStore } from '../../stores/app.js'
|
import { useAppStore } from '../../stores/app.js'
|
||||||
|
import { toggleFavoriteScript, checkFavoriteScript } from '../../services/epicScript.js'
|
||||||
|
|
||||||
const store = useAppStore()
|
const store = useAppStore()
|
||||||
const activeType = ref('long')
|
const activeType = ref('long')
|
||||||
@@ -156,7 +157,7 @@ const activeStatus = ref('all')
|
|||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
const sortMode = ref('updated')
|
const sortMode = ref('updated')
|
||||||
const viewMode = ref('list')
|
const viewMode = ref('list')
|
||||||
const localFavorites = ref(uni.getStorageSync('script_favorites') || {})
|
const favoriteStatus = ref({})
|
||||||
const activeMenuId = ref('')
|
const activeMenuId = ref('')
|
||||||
const deleteTarget = ref(null)
|
const deleteTarget = ref(null)
|
||||||
const deletingScript = ref(false)
|
const deletingScript = ref(false)
|
||||||
@@ -197,6 +198,26 @@ const visibleScripts = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 列表加载后批量检查收藏状态
|
||||||
|
const loadFavoriteStatus = async () => {
|
||||||
|
const ids = scripts.value.map(s => String(s.id)).filter(id => !id.startsWith('demo-'))
|
||||||
|
if (!ids.length) return
|
||||||
|
const status = {}
|
||||||
|
await Promise.all(ids.map(async (id) => {
|
||||||
|
try {
|
||||||
|
status[id] = await checkFavoriteScript(id)
|
||||||
|
} catch {
|
||||||
|
status[id] = false
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
favoriteStatus.value = status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 scripts 变化时重新加载收藏状态
|
||||||
|
watch(scripts, () => {
|
||||||
|
loadFavoriteStatus()
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
const sortLabel = computed(() => {
|
const sortLabel = computed(() => {
|
||||||
const map = { updated: '最近更新⌄', words: '字数最多⌄', progress: '进度最高⌄' }
|
const map = { updated: '最近更新⌄', words: '字数最多⌄', progress: '进度最高⌄' }
|
||||||
return map[sortMode.value] || '最近更新⌄'
|
return map[sortMode.value] || '最近更新⌄'
|
||||||
@@ -242,7 +263,7 @@ const getProgress = (script) => Math.max(0, Math.min(99, Number(script.progress
|
|||||||
const getInitial = (script) => (script.title || '剧').slice(0, 1)
|
const getInitial = (script) => (script.title || '剧').slice(0, 1)
|
||||||
|
|
||||||
const isFavorite = (script) => {
|
const isFavorite = (script) => {
|
||||||
return Boolean(script.isFavorite || script.favorite || localFavorites.value[String(script.id)])
|
return Boolean(favoriteStatus.value[String(script.id)])
|
||||||
}
|
}
|
||||||
|
|
||||||
const openScriptChat = (script) => {
|
const openScriptChat = (script) => {
|
||||||
@@ -316,15 +337,32 @@ const toggleScriptMenu = (script) => {
|
|||||||
activeMenuId.value = activeMenuId.value === id ? '' : id
|
activeMenuId.value = activeMenuId.value === id ? '' : id
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleFavorite = (script) => {
|
const toggleFavorite = async (script) => {
|
||||||
const favorite = isFavorite(script)
|
const wasFav = isFavorite(script)
|
||||||
const next = { ...localFavorites.value }
|
// 乐观更新:先改内存状态
|
||||||
if (favorite) delete next[String(script.id)]
|
const next = { ...favoriteStatus.value }
|
||||||
else next[String(script.id)] = true
|
if (wasFav) {
|
||||||
localFavorites.value = next
|
delete next[String(script.id)]
|
||||||
uni.setStorageSync('script_favorites', next)
|
} else {
|
||||||
|
next[String(script.id)] = true
|
||||||
|
}
|
||||||
|
favoriteStatus.value = next
|
||||||
closeScriptMenu()
|
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 = { ...favoriteStatus.value }
|
||||||
|
if (wasFav) {
|
||||||
|
restored[String(script.id)] = true
|
||||||
|
} else {
|
||||||
|
delete restored[String(script.id)]
|
||||||
|
}
|
||||||
|
favoriteStatus.value = restored
|
||||||
|
uni.showToast({ title: error?.message || '操作失败', icon: 'none' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const continueScript = (script) => {
|
const continueScript = (script) => {
|
||||||
@@ -363,10 +401,9 @@ const confirmDeleteScript = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const next = { ...localFavorites.value }
|
const next = { ...favoriteStatus.value }
|
||||||
delete next[id]
|
delete next[id]
|
||||||
localFavorites.value = next
|
favoriteStatus.value = next
|
||||||
uni.setStorageSync('script_favorites', next)
|
|
||||||
const conversationId = deleteTarget.value.conversationId || deleteTarget.value.plotJson?.conversationId
|
const conversationId = deleteTarget.value.conversationId || deleteTarget.value.plotJson?.conversationId
|
||||||
if (conversationId) uni.removeStorageSync(`script_conversation_history_${conversationId}`)
|
if (conversationId) uni.removeStorageSync(`script_conversation_history_${conversationId}`)
|
||||||
uni.removeStorageSync(`script_chat_history_${id}`)
|
uni.removeStorageSync(`script_chat_history_${id}`)
|
||||||
|
|||||||
@@ -198,6 +198,39 @@ export const transformListToFrontend = (backendList) => {
|
|||||||
return backendList.map(transformToFrontendFormat)
|
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 {
|
export default {
|
||||||
getScriptList,
|
getScriptList,
|
||||||
getScriptPage,
|
getScriptPage,
|
||||||
@@ -212,5 +245,8 @@ export default {
|
|||||||
buildCharacterInfo,
|
buildCharacterInfo,
|
||||||
buildLifeEventsSummary,
|
buildLifeEventsSummary,
|
||||||
transformToFrontendFormat,
|
transformToFrontendFormat,
|
||||||
transformListToFrontend
|
transformListToFrontend,
|
||||||
|
toggleFavoriteScript,
|
||||||
|
getFavoriteScriptPage,
|
||||||
|
checkFavoriteScript,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,8 +64,28 @@ export const shareEvent = async (eventData = {}) => {
|
|||||||
return post('/lifeEvent/share-placeholder', eventData)
|
return post('/lifeEvent/share-placeholder', eventData)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const favoriteEvent = async ({ id, favorite }) => {
|
/**
|
||||||
return post('/lifeEvent/favorite-placeholder', { id, favorite })
|
* 切换人生事件收藏状态(收藏/取消收藏)
|
||||||
|
* @param {string} eventId 事件ID
|
||||||
|
* @returns {Promise<{success: boolean, isFavorited: boolean}>}
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查人生事件是否已收藏
|
||||||
|
* @param {string} eventId 事件ID
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
export const checkFavoriteEvent = async (eventId) => {
|
||||||
|
const res = await get('/lifeEvent/favorite/check', { eventId })
|
||||||
|
return res.data?.isFavorited ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformToBackendFormat = (frontendData) => {
|
const transformToBackendFormat = (frontendData) => {
|
||||||
@@ -153,7 +173,8 @@ export default {
|
|||||||
assistEventWriting,
|
assistEventWriting,
|
||||||
chatAboutEvent,
|
chatAboutEvent,
|
||||||
shareEvent,
|
shareEvent,
|
||||||
favoriteEvent,
|
toggleFavoriteEvent,
|
||||||
|
checkFavoriteEvent,
|
||||||
transformToFrontendFormat,
|
transformToFrontendFormat,
|
||||||
transformListToFrontend
|
transformListToFrontend
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,10 +232,19 @@ const shareEvent = async (eventData) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const favoriteEvent = async ({ id, favorite }) => {
|
const toggleEventFavorite = async (eventId) => {
|
||||||
try {
|
try {
|
||||||
const res = await lifeEventService.favoriteEvent({ id, favorite })
|
const result = await lifeEventService.toggleFavoriteEvent(eventId)
|
||||||
return { success: true, data: res.data }
|
return { success: true, data: result }
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkEventFavorite = async (eventId) => {
|
||||||
|
try {
|
||||||
|
const isFavorited = await lifeEventService.checkFavoriteEvent(eventId)
|
||||||
|
return { success: true, data: isFavorited }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message }
|
||||||
}
|
}
|
||||||
@@ -479,7 +488,8 @@ export const useAppStore = () => {
|
|||||||
assistEventWriting,
|
assistEventWriting,
|
||||||
chatAboutEvent,
|
chatAboutEvent,
|
||||||
shareEvent,
|
shareEvent,
|
||||||
favoriteEvent,
|
toggleEventFavorite,
|
||||||
|
checkEventFavorite,
|
||||||
getEventById,
|
getEventById,
|
||||||
fetchScripts,
|
fetchScripts,
|
||||||
createScript,
|
createScript,
|
||||||
|
|||||||
@@ -8,13 +8,16 @@ import com.emotion.dto.request.EpicScriptInspirationRequest;
|
|||||||
import com.emotion.dto.request.EpicScriptPageRequest;
|
import com.emotion.dto.request.EpicScriptPageRequest;
|
||||||
import com.emotion.dto.request.EpicScriptUpdateRequest;
|
import com.emotion.dto.request.EpicScriptUpdateRequest;
|
||||||
import com.emotion.dto.request.SwitchVersionRequest;
|
import com.emotion.dto.request.SwitchVersionRequest;
|
||||||
|
import com.emotion.dto.request.ScriptFavoriteToggleRequest;
|
||||||
import com.emotion.dto.response.EpicScriptCreateWithDialogueResponse;
|
import com.emotion.dto.response.EpicScriptCreateWithDialogueResponse;
|
||||||
import com.emotion.dto.response.EpicScriptInspirationResponse;
|
import com.emotion.dto.response.EpicScriptInspirationResponse;
|
||||||
import com.emotion.dto.response.EpicScriptResponse;
|
import com.emotion.dto.response.EpicScriptResponse;
|
||||||
import com.emotion.dto.response.InspirationSuggestionResponse;
|
import com.emotion.dto.response.InspirationSuggestionResponse;
|
||||||
import com.emotion.service.EpicScriptDialogueService;
|
import com.emotion.service.EpicScriptDialogueService;
|
||||||
|
import com.emotion.dto.response.ScriptFavoriteResponse;
|
||||||
import com.emotion.service.EpicScriptService;
|
import com.emotion.service.EpicScriptService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
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.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -41,6 +44,9 @@ public class EpicScriptController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private EpicScriptDialogueService epicScriptDialogueService;
|
private EpicScriptDialogueService epicScriptDialogueService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ScriptFavoriteService scriptFavoriteService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询当前用户的爽文剧本
|
* 分页查询当前用户的爽文剧本
|
||||||
*/
|
*/
|
||||||
@@ -172,4 +178,31 @@ public class EpicScriptController {
|
|||||||
}
|
}
|
||||||
return Result.success();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ package com.emotion.controller;
|
|||||||
|
|
||||||
import com.emotion.common.PageResult;
|
import com.emotion.common.PageResult;
|
||||||
import com.emotion.common.Result;
|
import com.emotion.common.Result;
|
||||||
|
import com.emotion.dto.request.EventFavoriteToggleRequest;
|
||||||
import com.emotion.dto.request.LifeEventCreateRequest;
|
import com.emotion.dto.request.LifeEventCreateRequest;
|
||||||
import com.emotion.dto.request.LifeEventPageRequest;
|
import com.emotion.dto.request.LifeEventPageRequest;
|
||||||
import com.emotion.dto.request.LifeEventUpdateRequest;
|
import com.emotion.dto.request.LifeEventUpdateRequest;
|
||||||
|
import com.emotion.dto.response.EventFavoriteResponse;
|
||||||
import com.emotion.dto.response.LifeEventResponse;
|
import com.emotion.dto.response.LifeEventResponse;
|
||||||
|
import com.emotion.service.EventFavoriteService;
|
||||||
import com.emotion.service.LifeEventService;
|
import com.emotion.service.LifeEventService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
@@ -35,6 +38,9 @@ public class LifeEventController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private LifeEventService lifeEventService;
|
private LifeEventService lifeEventService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EventFavoriteService eventFavoriteService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询当前用户的生命事件
|
* 分页查询当前用户的生命事件
|
||||||
*/
|
*/
|
||||||
@@ -150,18 +156,6 @@ public class LifeEventController {
|
|||||||
return Result.success(data);
|
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) {
|
private String stringValue(Object value, String fallback) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -170,6 +164,30 @@ public class LifeEventController {
|
|||||||
return text.isEmpty() ? fallback : text;
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
private List<String> readTags(Object value) {
|
private List<String> readTags(Object value) {
|
||||||
List<String> tags = new ArrayList<>();
|
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,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 EventFavoriteResponse {
|
||||||
|
|
||||||
|
private String eventId;
|
||||||
|
|
||||||
|
private Boolean isFavorited;
|
||||||
|
|
||||||
|
private String favoriteTime;
|
||||||
|
}
|
||||||
@@ -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_event_favorite")
|
||||||
|
public class EventFavorite extends BaseEntity {
|
||||||
|
|
||||||
|
@TableField("user_id")
|
||||||
|
private String userId;
|
||||||
|
|
||||||
|
@TableField("event_id")
|
||||||
|
private String eventId;
|
||||||
|
}
|
||||||
@@ -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.EventFavorite;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人生事件收藏Mapper接口
|
||||||
|
*
|
||||||
|
* @author huazhongmin
|
||||||
|
* @date 2026-07-19
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface EventFavoriteMapper extends BaseMapper<EventFavorite> {
|
||||||
|
}
|
||||||
@@ -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,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,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,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,12 @@
|
|||||||
|
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 DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
update_by VARCHAR(64) COMMENT '更新人',
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
is_deleted TINYINT(1) DEFAULT 0 COMMENT '是否删除:0-否 1-是',
|
||||||
|
remarks VARCHAR(500) COMMENT '备注',
|
||||||
|
UNIQUE KEY uk_user_event (user_id, event_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='人生事件收藏关联表';
|
||||||
@@ -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='剧本收藏关联表';
|
||||||
Reference in New Issue
Block a user