feat: 完成情绪博物馆项目重构和功能增强 - 新增日记评论和帖子功能 - 重构前端架构,优化用户体验 - 完善WebSocket通信机制 - 更新项目文档和部署配置

This commit is contained in:
2025-07-27 10:05:59 +08:00
parent 6903ac1c0d
commit cc886cd4d5
126 changed files with 21179 additions and 15734 deletions
@@ -672,7 +672,7 @@ public class AiChatServiceImpl implements AiChatService {
/**
* 发送总结消息到Coze AI
*/
private String sendSummaryMessage(String conversationId, String userMessage, String userId) {
public String sendSummaryMessage(String conversationId, String userMessage, String userId) {
log.info("发送总结消息到Coze AI: conversationId={}, userId={}", conversationId, userId);
// 创建API调用记录(总结不需要messageId)
@@ -12,6 +12,8 @@ import com.emotion.exception.CaptchaException;
import com.emotion.exception.TokenException;
import com.emotion.service.AuthService;
import com.emotion.service.UserService;
import com.emotion.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
@@ -33,10 +35,11 @@ import java.util.concurrent.TimeUnit;
/**
* 认证服务实现类
*
*
* @author emotion-museum
* @date 2025-07-23
*/
@Slf4j
@Service
public class AuthServiceImpl implements AuthService {
@@ -49,6 +52,9 @@ public class AuthServiceImpl implements AuthService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtUtil jwtUtil;
private static final String CAPTCHA_PREFIX = "captcha:";
private static final String TOKEN_PREFIX = "token:";
private static final String REFRESH_TOKEN_PREFIX = "refresh_token:";
@@ -105,28 +111,53 @@ public class AuthServiceImpl implements AuthService {
throw new CaptchaException("验证码错误或已过期");
}
// 验证密码确认
if (!request.getPassword().equals(request.getConfirmPassword())) {
throw new BusinessException("密码与确认密码不一致");
}
// 检查账号是否已存在
if (userService.getByAccount(request.getAccount()) != null) {
throw new BusinessException("账号已存在");
}
// 检查邮箱是否已存在
// 检查邮箱是否已存在(只有当邮箱不为空时才检查)
if (StringUtils.hasText(request.getEmail()) && userService.getByEmail(request.getEmail()) != null) {
throw new BusinessException("邮箱已被使用");
}
// 检查手机号是否已存在(只有当手机号不为空时才检查)
if (StringUtils.hasText(request.getPhone()) && userService.getByPhone(request.getPhone()) != null) {
throw new BusinessException("手机号已被使用");
}
// 处理用户名:如果为空则使用账号
String username = StringUtils.hasText(request.getUsername()) ? request.getUsername() : request.getAccount();
// 处理邮箱:如果为空字符串则设为null
String email = StringUtils.hasText(request.getEmail()) ? request.getEmail() : null;
// 处理手机号:如果为空字符串则设为null
String phone = StringUtils.hasText(request.getPhone()) ? request.getPhone() : null;
// 创建用户(密码在UserService中加密,这里不需要预先加密)
User user = userService.createUser(
request.getAccount(),
StringUtils.hasText(request.getUsername()) ? request.getUsername() : request.getAccount(),
username,
request.getPassword(),
request.getEmail(),
request.getPhone()
email,
phone
);
// 设置昵称
// 设置昵称(如果昵称为空则使用用户名)
if (StringUtils.hasText(request.getNickname())) {
user.setNickname(request.getNickname());
} else if (StringUtils.hasText(username)) {
user.setNickname(username);
}
// 如果有昵称变更,更新用户信息
if (StringUtils.hasText(user.getNickname())) {
userService.updateById(user);
}
@@ -267,7 +298,18 @@ public class AuthServiceImpl implements AuthService {
if (!StringUtils.hasText(token)) {
return false;
}
return redisTemplate.hasKey(TOKEN_PREFIX + token);
try {
// 首先尝试JWT验证
if (jwtUtil.validateToken(token)) {
// JWT验证成功,再检查Redis中是否存在(用于登出功能)
return redisTemplate.hasKey(TOKEN_PREFIX + token);
}
return false;
} catch (Exception e) {
log.warn("Token验证失败: {}", e.getMessage());
return false;
}
}
@Override
@@ -275,28 +317,50 @@ public class AuthServiceImpl implements AuthService {
if (!StringUtils.hasText(token)) {
return null;
}
return (String) redisTemplate.opsForValue().get(TOKEN_PREFIX + token);
try {
// 首先尝试从JWT中获取用户ID
String userId = jwtUtil.getUserIdFromToken(token);
if (userId != null) {
// 验证Redis中是否存在该token(确保未被登出)
if (redisTemplate.hasKey(TOKEN_PREFIX + token)) {
return userId;
}
}
return null;
} catch (Exception e) {
log.warn("从Token获取用户ID失败: {}", e.getMessage());
return null;
}
}
@Override
public String getUsernameFromToken(String token) {
String userId = getUserIdFromToken(token);
if (userId == null) {
if (!StringUtils.hasText(token)) {
return null;
}
try {
// 直接从JWT中获取用户名
return jwtUtil.getUsernameFromToken(token);
} catch (Exception e) {
log.warn("从Token获取用户名失败: {}", e.getMessage());
return null;
}
User user = userService.getById(userId);
return user != null ? user.getUsername() : null;
}
/**
* 生成访问令牌
*/
private String generateAccessToken(User user) {
String token = UUID.randomUUID().toString().replace("-", "");
// 使用JWT生成token
String token = jwtUtil.generateToken(user.getId(), user.getUsername());
// 同时在Redis中存储token信息,用于快速验证和登出功能
redisTemplate.opsForValue().set(
TOKEN_PREFIX + token,
user.getId(),
TOKEN_EXPIRE_HOURS,
TOKEN_PREFIX + token,
user.getId(),
TOKEN_EXPIRE_HOURS,
TimeUnit.HOURS
);
return token;
@@ -306,11 +370,14 @@ public class AuthServiceImpl implements AuthService {
* 生成刷新令牌
*/
private String generateRefreshToken(User user) {
String refreshToken = UUID.randomUUID().toString().replace("-", "");
// 使用JWT生成刷新token
String refreshToken = jwtUtil.generateRefreshToken(user.getId(), user.getUsername());
// 在Redis中存储刷新token信息
redisTemplate.opsForValue().set(
REFRESH_TOKEN_PREFIX + refreshToken,
user.getId(),
REFRESH_TOKEN_EXPIRE_DAYS,
REFRESH_TOKEN_PREFIX + refreshToken,
user.getId(),
REFRESH_TOKEN_EXPIRE_DAYS,
TimeUnit.DAYS
);
return refreshToken;
@@ -404,4 +471,31 @@ public class AuthServiceImpl implements AuthService {
}
return response;
}
@Override
public boolean existsByAccount(String account) {
if (!StringUtils.hasText(account)) {
return false;
}
User user = userService.getByAccount(account);
return user != null;
}
@Override
public boolean existsByEmail(String email) {
if (!StringUtils.hasText(email)) {
return false;
}
User user = userService.getByEmail(email);
return user != null;
}
@Override
public boolean existsByPhone(String phone) {
if (!StringUtils.hasText(phone)) {
return false;
}
User user = userService.getByPhone(phone);
return user != null;
}
}
@@ -0,0 +1,290 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.BasePageRequest;
import com.emotion.entity.DiaryComment;
import com.emotion.mapper.DiaryCommentMapper;
import com.emotion.service.DiaryCommentService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* 日记评论服务实现类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Service
public class DiaryCommentServiceImpl extends ServiceImpl<DiaryCommentMapper, DiaryComment> implements DiaryCommentService {
@Autowired
private ObjectMapper objectMapper;
@Override
public IPage<DiaryComment> getPage(BasePageRequest request) {
Page<DiaryComment> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<DiaryComment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryComment::getIsDeleted, 0)
.orderByDesc(DiaryComment::getIsTop)
.orderByDesc(DiaryComment::getPublishTime);
return this.page(page, wrapper);
}
@Override
public IPage<DiaryComment> getPageByDiaryId(String diaryId, BasePageRequest request) {
Page<DiaryComment> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<DiaryComment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryComment::getDiaryId, diaryId)
.eq(DiaryComment::getIsDeleted, 0)
.orderByDesc(DiaryComment::getIsTop)
.orderByDesc(DiaryComment::getPublishTime);
return this.page(page, wrapper);
}
@Override
public IPage<DiaryComment> getPageByUserId(String userId, BasePageRequest request) {
Page<DiaryComment> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<DiaryComment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryComment::getUserId, userId)
.eq(DiaryComment::getIsDeleted, 0)
.orderByDesc(DiaryComment::getPublishTime);
return this.page(page, wrapper);
}
@Override
public List<DiaryComment> getRepliesByParentId(String parentCommentId) {
LambdaQueryWrapper<DiaryComment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryComment::getParentCommentId, parentCommentId)
.eq(DiaryComment::getIsDeleted, 0)
.orderByAsc(DiaryComment::getPublishTime);
return this.list(wrapper);
}
@Override
public List<DiaryComment> getByCommentType(String commentType) {
LambdaQueryWrapper<DiaryComment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryComment::getCommentType, commentType)
.eq(DiaryComment::getIsDeleted, 0)
.orderByDesc(DiaryComment::getPublishTime);
return this.list(wrapper);
}
@Override
public List<DiaryComment> getByDiaryIdAndCommentType(String diaryId, String commentType) {
LambdaQueryWrapper<DiaryComment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryComment::getDiaryId, diaryId)
.eq(DiaryComment::getCommentType, commentType)
.eq(DiaryComment::getIsDeleted, 0)
.orderByDesc(DiaryComment::getPublishTime);
return this.list(wrapper);
}
@Override
public boolean incrementLikeCount(String commentId) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, commentId)
.setSql("like_count = like_count + 1");
return this.update(wrapper);
}
@Override
public boolean decrementLikeCount(String commentId) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, commentId)
.setSql("like_count = GREATEST(like_count - 1, 0)");
return this.update(wrapper);
}
@Override
public boolean incrementReplyCount(String commentId) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, commentId)
.setSql("reply_count = reply_count + 1");
return this.update(wrapper);
}
@Override
public boolean decrementReplyCount(String commentId) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, commentId)
.setSql("reply_count = GREATEST(reply_count - 1, 0)");
return this.update(wrapper);
}
@Override
public boolean updateLastReplyTime(String commentId) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, commentId)
.set(DiaryComment::getLastReplyTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean setTop(String commentId, Integer isTop) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, commentId)
.set(DiaryComment::getIsTop, isTop);
return this.update(wrapper);
}
@Override
public Long countByDiaryId(String diaryId) {
LambdaQueryWrapper<DiaryComment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryComment::getDiaryId, diaryId)
.eq(DiaryComment::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countByUserId(String userId) {
LambdaQueryWrapper<DiaryComment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryComment::getUserId, userId)
.eq(DiaryComment::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countByCommentType(String commentType) {
LambdaQueryWrapper<DiaryComment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryComment::getCommentType, commentType)
.eq(DiaryComment::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countReplies(String parentCommentId) {
LambdaQueryWrapper<DiaryComment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryComment::getParentCommentId, parentCommentId)
.eq(DiaryComment::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public DiaryComment createComment(String diaryId, String userId, String content, List<String> images,
String parentCommentId, Integer isAnonymous) {
DiaryComment comment = DiaryComment.builder()
.diaryId(diaryId)
.userId(userId)
.content(content)
.images(convertListToJson(images))
.parentCommentId(parentCommentId)
.commentType("user")
.likeCount(0)
.replyCount(0)
.isAnonymous(isAnonymous)
.isTop(0)
.status("published")
.publishTime(LocalDateTime.now())
.build();
this.save(comment);
// 如果有父评论,更新父评论的回复数
if (StringUtils.hasText(parentCommentId)) {
this.incrementReplyCount(parentCommentId);
this.updateLastReplyTime(parentCommentId);
}
return comment;
}
@Override
public DiaryComment createAiComment(String diaryId, String content, String aiCommentSource,
BigDecimal emotionScore, BigDecimal sentimentScore) {
DiaryComment comment = DiaryComment.builder()
.diaryId(diaryId)
.userId("system") // AI评论使用system用户ID
.content(content)
.commentType("ai")
.aiCommentSource(aiCommentSource)
.likeCount(0)
.replyCount(0)
.isAnonymous(0)
.isTop(0)
.status("published")
.publishTime(LocalDateTime.now())
.emotionScore(emotionScore)
.sentimentScore(sentimentScore)
.build();
this.save(comment);
return comment;
}
@Override
public boolean updateComment(String commentId, String content, List<String> images) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, commentId)
.set(StringUtils.hasText(content), DiaryComment::getContent, content)
.set(images != null, DiaryComment::getImages, convertListToJson(images));
return this.update(wrapper);
}
@Override
public boolean deleteComment(String commentId) {
return this.removeById(commentId);
}
@Override
public boolean softDeleteComment(String commentId) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, commentId)
.set(DiaryComment::getIsDeleted, 1);
return this.update(wrapper);
}
@Override
public boolean restoreComment(String commentId) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, commentId)
.set(DiaryComment::getIsDeleted, 0);
return this.update(wrapper);
}
@Override
public List<DiaryComment> getCommentTree(String diaryId) {
// 获取所有顶级评论(没有父评论的评论)
LambdaQueryWrapper<DiaryComment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryComment::getDiaryId, diaryId)
.isNull(DiaryComment::getParentCommentId)
.eq(DiaryComment::getIsDeleted, 0)
.orderByDesc(DiaryComment::getIsTop)
.orderByDesc(DiaryComment::getPublishTime);
List<DiaryComment> topComments = this.list(wrapper);
// 为每个顶级评论加载回复
return topComments.stream()
.peek(comment -> {
List<DiaryComment> replies = getRepliesByParentId(comment.getId());
// 这里可以递归加载更深层的回复,但为了性能考虑,通常只加载一层
})
.collect(Collectors.toList());
}
/**
* 将List转换为JSON字符串
*/
private String convertListToJson(List<?> list) {
if (list == null || list.isEmpty()) {
return null;
}
try {
return objectMapper.writeValueAsString(list);
} catch (JsonProcessingException e) {
return null;
}
}
}
@@ -0,0 +1,446 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.BasePageRequest;
import com.emotion.entity.DiaryPost;
import com.emotion.mapper.DiaryPostMapper;
import com.emotion.service.DiaryPostService;
import com.emotion.service.DiaryCommentService;
import com.emotion.service.AiChatService;
import com.emotion.dto.request.DiaryPostCreateRequest;
import com.emotion.dto.response.DiaryPostResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 用户日记服务实现类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Service
public class DiaryPostServiceImpl extends ServiceImpl<DiaryPostMapper, DiaryPost> implements DiaryPostService {
@Autowired
private AiChatService aiChatService;
@Autowired
private DiaryCommentService diaryCommentService;
@Autowired
private ObjectMapper objectMapper;
@Override
public IPage<DiaryPost> getPage(BasePageRequest request) {
Page<DiaryPost> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryPost::getIsDeleted, 0)
.orderByDesc(DiaryPost::getPriority)
.orderByDesc(DiaryPost::getPublishTime);
return this.page(page, wrapper);
}
@Override
public IPage<DiaryPost> getPageByUserId(String userId, BasePageRequest request) {
Page<DiaryPost> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryPost::getUserId, userId)
.eq(DiaryPost::getIsDeleted, 0)
.orderByDesc(DiaryPost::getPriority)
.orderByDesc(DiaryPost::getPublishTime);
return this.page(page, wrapper);
}
@Override
public IPage<DiaryPost> getPublicPageByUserId(String userId, BasePageRequest request) {
Page<DiaryPost> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryPost::getUserId, userId)
.eq(DiaryPost::getIsPublic, 1)
.eq(DiaryPost::getStatus, "published")
.eq(DiaryPost::getIsDeleted, 0)
.orderByDesc(DiaryPost::getPriority)
.orderByDesc(DiaryPost::getPublishTime);
return this.page(page, wrapper);
}
@Override
public IPage<DiaryPost> getFeaturedPage(BasePageRequest request) {
Page<DiaryPost> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryPost::getFeatured, 1)
.eq(DiaryPost::getIsPublic, 1)
.eq(DiaryPost::getStatus, "published")
.eq(DiaryPost::getIsDeleted, 0)
.orderByDesc(DiaryPost::getPriority)
.orderByDesc(DiaryPost::getPublishTime);
return this.page(page, wrapper);
}
@Override
public List<DiaryPost> getByStatus(String status) {
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryPost::getStatus, status)
.eq(DiaryPost::getIsDeleted, 0)
.orderByDesc(DiaryPost::getPublishTime);
return this.list(wrapper);
}
@Override
public List<DiaryPost> getByUserIdAndStatus(String userId, String status) {
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryPost::getUserId, userId)
.eq(DiaryPost::getStatus, status)
.eq(DiaryPost::getIsDeleted, 0)
.orderByDesc(DiaryPost::getPublishTime);
return this.list(wrapper);
}
@Override
public List<DiaryPost> getByMood(String mood) {
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryPost::getMood, mood)
.eq(DiaryPost::getIsPublic, 1)
.eq(DiaryPost::getStatus, "published")
.eq(DiaryPost::getIsDeleted, 0)
.orderByDesc(DiaryPost::getPublishTime);
return this.list(wrapper);
}
@Override
public List<DiaryPost> getByTags(List<String> tags) {
// 这里需要根据实际需求实现标签查询逻辑
// 由于tags字段是JSON格式,可能需要使用数据库的JSON查询功能
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryPost::getIsPublic, 1)
.eq(DiaryPost::getStatus, "published")
.eq(DiaryPost::getIsDeleted, 0)
.orderByDesc(DiaryPost::getPublishTime);
return this.list(wrapper);
}
@Override
public List<DiaryPost> getByLocation(String location) {
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
wrapper.like(DiaryPost::getLocation, location)
.eq(DiaryPost::getIsPublic, 1)
.eq(DiaryPost::getStatus, "published")
.eq(DiaryPost::getIsDeleted, 0)
.orderByDesc(DiaryPost::getPublishTime);
return this.list(wrapper);
}
@Override
public boolean incrementViewCount(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.setSql("view_count = view_count + 1");
return this.update(wrapper);
}
@Override
public boolean incrementLikeCount(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.setSql("like_count = like_count + 1");
return this.update(wrapper);
}
@Override
public boolean decrementLikeCount(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.setSql("like_count = GREATEST(like_count - 1, 0)");
return this.update(wrapper);
}
@Override
public boolean incrementCommentCount(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.setSql("comment_count = comment_count + 1");
return this.update(wrapper);
}
@Override
public boolean decrementCommentCount(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.setSql("comment_count = GREATEST(comment_count - 1, 0)");
return this.update(wrapper);
}
@Override
public boolean incrementShareCount(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.setSql("share_count = share_count + 1");
return this.update(wrapper);
}
@Override
public boolean updateLastCommentTime(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.set(DiaryPost::getLastCommentTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean setFeatured(String diaryId, Integer featured) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.set(DiaryPost::getFeatured, featured);
return this.update(wrapper);
}
@Override
public boolean setPriority(String diaryId, Integer priority) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.set(DiaryPost::getPriority, priority);
return this.update(wrapper);
}
@Override
public Long countByUserId(String userId) {
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryPost::getUserId, userId)
.eq(DiaryPost::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countPublicByUserId(String userId) {
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryPost::getUserId, userId)
.eq(DiaryPost::getIsPublic, 1)
.eq(DiaryPost::getStatus, "published")
.eq(DiaryPost::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countFeatured() {
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryPost::getFeatured, 1)
.eq(DiaryPost::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countByStatus(String status) {
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryPost::getStatus, status)
.eq(DiaryPost::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public DiaryPost createDiaryPost(com.emotion.dto.request.DiaryPostCreateRequest request) {
// 处理标题:如果为空,则设置为null或生成默认标题
String title = request.getTitle();
if (title == null || title.trim().isEmpty()) {
// 可以选择设置为null,或者生成一个默认标题
// 这里我们设置为null,让数据库使用默认值
title = null;
}
DiaryPost diaryPost = DiaryPost.builder()
.userId(request.getUserId())
.title(title)
.content(request.getContent())
.images(convertListToJson(request.getImages()))
.videos(convertListToJson(request.getVideos()))
.location(request.getLocation())
.weather(request.getWeather())
.mood(request.getMood())
.tags(convertListToJson(request.getTags()))
.isPublic(request.getIsPublic())
.isAnonymous(request.getIsAnonymous())
.viewCount(0)
.likeCount(0)
.commentCount(0)
.shareCount(0)
.publishTime(LocalDateTime.now())
.status("published")
.priority(0)
.featured(0)
.build();
this.save(diaryPost);
return diaryPost;
}
@Override
public boolean updateDiaryPost(String diaryId, com.emotion.dto.request.DiaryPostUpdateRequest request) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.set(StringUtils.hasText(request.getTitle()), DiaryPost::getTitle, request.getTitle())
.set(StringUtils.hasText(request.getContent()), DiaryPost::getContent, request.getContent())
.set(request.getImages() != null, DiaryPost::getImages, convertListToJson(request.getImages()))
.set(request.getVideos() != null, DiaryPost::getVideos, convertListToJson(request.getVideos()))
.set(StringUtils.hasText(request.getLocation()), DiaryPost::getLocation, request.getLocation())
.set(StringUtils.hasText(request.getWeather()), DiaryPost::getWeather, request.getWeather())
.set(StringUtils.hasText(request.getMood()), DiaryPost::getMood, request.getMood())
.set(request.getTags() != null, DiaryPost::getTags, convertListToJson(request.getTags()))
.set(request.getIsPublic() != null, DiaryPost::getIsPublic, request.getIsPublic())
.set(request.getIsAnonymous() != null, DiaryPost::getIsAnonymous, request.getIsAnonymous())
.set(StringUtils.hasText(request.getStatus()), DiaryPost::getStatus, request.getStatus());
return this.update(wrapper);
}
@Override
public boolean deleteDiaryPost(String diaryId) {
return this.removeById(diaryId);
}
@Override
public boolean softDeleteDiaryPost(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.set(DiaryPost::getIsDeleted, 1);
return this.update(wrapper);
}
@Override
public boolean restoreDiaryPost(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.set(DiaryPost::getIsDeleted, 0);
return this.update(wrapper);
}
@Override
public boolean addAiComment(String diaryId, String aiComment, Object aiEmotionAnalysis,
BigDecimal aiSentimentScore, List<String> aiKeywords, String aiSuggestions) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.set(DiaryPost::getAiComment, aiComment)
.set(DiaryPost::getAiCommentTime, LocalDateTime.now())
.set(DiaryPost::getAiEmotionAnalysis, convertObjectToJson(aiEmotionAnalysis))
.set(DiaryPost::getAiSentimentScore, aiSentimentScore)
.set(DiaryPost::getAiKeywords, convertListToJson(aiKeywords))
.set(DiaryPost::getAiSuggestions, aiSuggestions);
return this.update(wrapper);
}
@Override
public com.emotion.dto.response.DiaryPostResponse publishDiaryWithAiComment(com.emotion.dto.request.DiaryPostCreateRequest request) {
// 1. 保存日记
DiaryPost diaryPost = createDiaryPost(request);
// 2. 生成AI评论
String aiComment = null;
try {
String conversationId = "diary-" + diaryPost.getId();
aiComment = aiChatService.sendSummaryMessage(conversationId, diaryPost.getContent(), request.getUserId());
} catch (Exception e) {
aiComment = "开开:AI评论生成失败";
}
// 3. 写入AI评论到diary_comment表
if (aiComment != null) {
diaryCommentService.createAiComment(
diaryPost.getId(),
aiComment,
"diary_ai_summary",
null,
null
);
addAiComment(
diaryPost.getId(),
aiComment,
null,
null,
null,
null
);
}
// 4. 返回日记详情(含AI评论)
com.emotion.dto.response.DiaryPostResponse response = new com.emotion.dto.response.DiaryPostResponse();
org.springframework.beans.BeanUtils.copyProperties(diaryPost, response);
// 查询AI评论
List<com.emotion.entity.DiaryComment> aiComments = diaryCommentService.getByDiaryIdAndCommentType(diaryPost.getId(), "ai");
if (!aiComments.isEmpty()) {
response.setAiComment(aiComments.get(0).getContent());
}
// 转换时间格式
if (diaryPost.getPublishTime() != null) {
response.setPublishTime(diaryPost.getPublishTime().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
if (diaryPost.getLastCommentTime() != null) {
response.setLastCommentTime(diaryPost.getLastCommentTime().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
if (diaryPost.getAiCommentTime() != null) {
response.setAiCommentTime(diaryPost.getAiCommentTime().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
if (diaryPost.getCreateTime() != null) {
response.setCreateTime(diaryPost.getCreateTime().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
if (diaryPost.getUpdateTime() != null) {
response.setUpdateTime(diaryPost.getUpdateTime().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
// 转换JSON字段
try {
if (diaryPost.getImages() != null) {
response.setImages(objectMapper.readValue(diaryPost.getImages(), new com.fasterxml.jackson.core.type.TypeReference<List<String>>() {}));
}
if (diaryPost.getVideos() != null) {
response.setVideos(objectMapper.readValue(diaryPost.getVideos(), new com.fasterxml.jackson.core.type.TypeReference<List<String>>() {}));
}
if (diaryPost.getTags() != null) {
response.setTags(objectMapper.readValue(diaryPost.getTags(), new com.fasterxml.jackson.core.type.TypeReference<List<String>>() {}));
}
if (diaryPost.getAiKeywords() != null) {
response.setAiKeywords(objectMapper.readValue(diaryPost.getAiKeywords(), new com.fasterxml.jackson.core.type.TypeReference<List<String>>() {}));
}
if (diaryPost.getAiEmotionAnalysis() != null) {
response.setAiEmotionAnalysis(objectMapper.readValue(diaryPost.getAiEmotionAnalysis(), Object.class));
}
if (diaryPost.getMetadata() != null) {
response.setMetadata(objectMapper.readValue(diaryPost.getMetadata(), Object.class));
}
} catch (com.fasterxml.jackson.core.JsonProcessingException e) {
// 忽略JSON解析错误
}
return response;
}
/**
* 将List转换为JSON字符串
*/
private String convertListToJson(List<?> list) {
if (list == null || list.isEmpty()) {
return null;
}
try {
return objectMapper.writeValueAsString(list);
} catch (JsonProcessingException e) {
return null;
}
}
/**
* 将Object转换为JSON字符串
*/
private String convertObjectToJson(Object obj) {
if (obj == null) {
return null;
}
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
return null;
}
}
}
@@ -262,26 +262,8 @@ public class WebSocketServiceImpl implements WebSocketService {
userId
);
// 构建AI回复消息(不分割,保持完整性)
WebSocketMessage aiMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.conversationId(conversationId)
.type(WebSocketMessage.MessageType.TEXT)
.content(aiReply)
.senderId("ai")
.senderType(WebSocketMessage.SenderType.AI)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
// AI回复已经在sendChatMessageForWebSocket中保存了,这里不需要重复保存
// 发送AI回复
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", aiMessage);
if (conversationId != null) {
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, aiMessage);
}
// 根据换行符分割AI回复并按顺序发送多条消息
sendAiReplyInParts(userId, conversationId, aiReply);
// 更新会话的最后活跃时间和消息数量
updateConversationActivity(conversationId);
@@ -398,4 +380,158 @@ public class WebSocketServiceImpl implements WebSocketService {
log.error("更新会话活跃状态失败: conversationId={}", conversationId, e);
}
}
/**
* 根据换行符分割AI回复并按顺序发送多条消息
*
* @param userId 用户ID
* @param conversationId 会话ID
* @param aiReply AI回复内容
*/
private void sendAiReplyInParts(String userId, String conversationId, String aiReply) {
try {
log.info("开始处理AI回复消息: userId={}, conversationId={}, aiReply长度={}",
userId, conversationId, aiReply != null ? aiReply.length() : 0);
if (aiReply == null || aiReply.trim().isEmpty()) {
log.warn("AI回复内容为空,跳过发送");
return;
}
// 检查是否需要分割
boolean needsSplit = aiReply.contains("\n\n") || aiReply.contains("\n");
if (!needsSplit) {
// 不需要分割,直接发送完整消息
log.info("AI回复无换行符,发送完整消息");
sendSingleAiMessage(userId, conversationId, aiReply.trim());
return;
}
// 需要分割,按换行符分割并发送多条消息
log.info("AI回复包含换行符,开始分割发送");
String[] replyParts = splitAiReply(aiReply);
log.info("AI回复分割完成,共{}个部分", replyParts.length);
// 按顺序发送每个部分
int sentCount = 0;
for (int i = 0; i < replyParts.length; i++) {
String part = replyParts[i].trim();
// 跳过空白部分
if (part.isEmpty()) {
continue;
}
// 发送消息部分
sendSingleAiMessage(userId, conversationId, part);
sentCount++;
log.info("发送AI回复部分 {}/{}: 内容长度={}", sentCount, replyParts.length, part.length());
// 在多个部分之间添加短暂延迟,模拟自然对话节奏
if (i < replyParts.length - 1) {
try {
Thread.sleep(500); // 延迟500毫秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("发送AI回复时被中断");
break;
}
}
}
log.info("AI回复发送完成: userId={}, conversationId={}, 实际发送{}条消息",
userId, conversationId, sentCount);
} catch (Exception e) {
log.error("分割发送AI回复失败: userId={}, conversationId={}", userId, conversationId, e);
// 发送错误时,尝试发送完整的原始回复
try {
WebSocketMessage fallbackMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.conversationId(conversationId)
.type(WebSocketMessage.MessageType.TEXT)
.content(aiReply)
.senderId("ai")
.senderType(WebSocketMessage.SenderType.AI)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", fallbackMessage);
if (conversationId != null) {
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, fallbackMessage);
}
log.info("已发送完整AI回复作为备用方案");
} catch (Exception fallbackError) {
log.error("发送备用AI回复也失败", fallbackError);
sendErrorMessage(userId, "AI回复发送失败,请稍后重试");
}
}
}
/**
* 发送单条AI消息
*
* @param userId 用户ID
* @param conversationId 会话ID
* @param content 消息内容
*/
private void sendSingleAiMessage(String userId, String conversationId, String content) {
// 构建AI回复消息
WebSocketMessage aiMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.conversationId(conversationId)
.type(WebSocketMessage.MessageType.TEXT)
.content(content)
.senderId("ai")
.senderType(WebSocketMessage.SenderType.AI)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
// 发送给用户私有队列
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", aiMessage);
// 发送到会话公共频道
if (conversationId != null) {
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, aiMessage);
}
}
/**
* 智能分割AI回复内容
*
* @param aiReply AI回复内容
* @return 分割后的内容数组
*/
private String[] splitAiReply(String aiReply) {
if (aiReply == null || aiReply.trim().isEmpty()) {
return new String[0];
}
// 首先尝试按双换行符分割(段落分割)
if (aiReply.contains("\n\n")) {
String[] parts = aiReply.split("\n\n");
log.debug("按双换行符分割,得到{}个部分", parts.length);
return parts;
}
// 如果没有双换行符,按单换行符分割(行分割)
if (aiReply.contains("\n")) {
String[] parts = aiReply.split("\n");
log.debug("按单换行符分割,得到{}个部分", parts.length);
return parts;
}
// 如果没有换行符,返回原始内容(这种情况不应该到达这里)
log.debug("没有换行符,返回原始内容");
return new String[]{aiReply};
}
}