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
@@ -111,6 +111,33 @@ public class AuthController {
return Result.success(username);
}
/**
* 检查账号是否存在
*/
@GetMapping("/check-account")
public Result<Boolean> checkAccount(@RequestParam String account) {
boolean exists = authService.existsByAccount(account);
return Result.success(exists);
}
/**
* 检查邮箱是否存在
*/
@GetMapping("/check-email")
public Result<Boolean> checkEmail(@RequestParam String email) {
boolean exists = authService.existsByEmail(email);
return Result.success(exists);
}
/**
* 检查手机号是否存在
*/
@GetMapping("/check-phone")
public Result<Boolean> checkPhone(@RequestParam String phone) {
boolean exists = authService.existsByPhone(phone);
return Result.success(exists);
}
/**
* 从请求中提取访问令牌
*/
@@ -0,0 +1,332 @@
package com.emotion.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.emotion.common.BasePageRequest;
import com.emotion.common.PageResult;
import com.emotion.common.Result;
import com.emotion.dto.request.DiaryCommentCreateRequest;
import com.emotion.dto.response.DiaryCommentResponse;
import com.emotion.entity.DiaryComment;
import com.emotion.service.DiaryCommentService;
import com.emotion.service.DiaryPostService;
import com.emotion.service.UserService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 日记评论控制器
*
* @author emotion-museum
* @date 2025-07-23
*/
@RestController
@RequestMapping("/diary-comment")
public class DiaryCommentController {
@Autowired
private DiaryCommentService diaryCommentService;
@Autowired
private DiaryPostService diaryPostService;
@Autowired
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 分页查询评论
*/
@GetMapping("/page")
public Result<PageResult<DiaryCommentResponse>> getPage(@Validated BasePageRequest request) {
IPage<DiaryComment> page = diaryCommentService.getPage(request);
List<DiaryCommentResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<DiaryCommentResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(responses);
return Result.success(pageResult);
}
/**
* 根据日记ID分页查询评论
*/
@GetMapping("/diary/{diaryId}/page")
public Result<PageResult<DiaryCommentResponse>> getPageByDiaryId(@PathVariable String diaryId,
@Validated BasePageRequest request) {
IPage<DiaryComment> page = diaryCommentService.getPageByDiaryId(diaryId, request);
List<DiaryCommentResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<DiaryCommentResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(responses);
return Result.success(pageResult);
}
/**
* 根据用户ID分页查询评论
*/
@GetMapping("/user/{userId}/page")
public Result<PageResult<DiaryCommentResponse>> getPageByUserId(@PathVariable String userId,
@Validated BasePageRequest request) {
IPage<DiaryComment> page = diaryCommentService.getPageByUserId(userId, request);
List<DiaryCommentResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<DiaryCommentResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(responses);
return Result.success(pageResult);
}
/**
* 获取评论树结构
*/
@GetMapping("/diary/{diaryId}/tree")
public Result<List<DiaryCommentResponse>> getCommentTree(@PathVariable String diaryId) {
List<DiaryComment> comments = diaryCommentService.getCommentTree(diaryId);
List<DiaryCommentResponse> responses = comments.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
return Result.success(responses);
}
/**
* 根据ID获取评论详情
*/
@GetMapping("/{id}")
public Result<DiaryCommentResponse> getById(@PathVariable String id) {
DiaryComment comment = diaryCommentService.getById(id);
if (comment == null) {
return Result.notFound("评论不存在");
}
return Result.success(convertToResponse(comment));
}
/**
* 创建评论
*/
@PostMapping
public Result<DiaryCommentResponse> create(@Valid @RequestBody DiaryCommentCreateRequest request) {
DiaryComment comment = diaryCommentService.createComment(
request.getDiaryId(),
request.getUserId(),
request.getContent(),
request.getImages(),
request.getParentCommentId(),
request.getIsAnonymous()
);
// 更新日记的评论数
diaryPostService.incrementCommentCount(request.getDiaryId());
diaryPostService.updateLastCommentTime(request.getDiaryId());
return Result.success(convertToResponse(comment));
}
/**
* 更新评论
*/
@PutMapping("/{id}")
public Result<DiaryCommentResponse> update(@PathVariable String id, @Valid @RequestBody DiaryCommentCreateRequest request) {
boolean updated = diaryCommentService.updateComment(id, request.getContent(), request.getImages());
if (!updated) {
return Result.error("更新失败");
}
DiaryComment updatedComment = diaryCommentService.getById(id);
return Result.success(convertToResponse(updatedComment));
}
/**
* 删除评论
*/
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable String id) {
DiaryComment comment = diaryCommentService.getById(id);
if (comment == null) {
return Result.error("评论不存在");
}
boolean deleted = diaryCommentService.deleteComment(id);
if (!deleted) {
return Result.error("删除失败");
}
// 更新日记的评论数
diaryPostService.decrementCommentCount(comment.getDiaryId());
return Result.success();
}
/**
* 软删除评论
*/
@DeleteMapping("/{id}/soft")
public Result<Void> softDelete(@PathVariable String id) {
DiaryComment comment = diaryCommentService.getById(id);
if (comment == null) {
return Result.error("评论不存在");
}
boolean deleted = diaryCommentService.softDeleteComment(id);
if (!deleted) {
return Result.error("删除失败");
}
// 更新日记的评论数
diaryPostService.decrementCommentCount(comment.getDiaryId());
return Result.success();
}
/**
* 恢复评论
*/
@PutMapping("/{id}/restore")
public Result<Void> restore(@PathVariable String id) {
DiaryComment comment = diaryCommentService.getById(id);
if (comment == null) {
return Result.error("评论不存在");
}
boolean restored = diaryCommentService.restoreComment(id);
if (!restored) {
return Result.error("恢复失败");
}
// 更新日记的评论数
diaryPostService.incrementCommentCount(comment.getDiaryId());
return Result.success();
}
/**
* 点赞评论
*/
@PostMapping("/{id}/like")
public Result<Void> like(@PathVariable String id) {
boolean liked = diaryCommentService.incrementLikeCount(id);
if (!liked) {
return Result.error("点赞失败");
}
return Result.success();
}
/**
* 取消点赞评论
*/
@DeleteMapping("/{id}/like")
public Result<Void> unlike(@PathVariable String id) {
boolean unliked = diaryCommentService.decrementLikeCount(id);
if (!unliked) {
return Result.error("取消点赞失败");
}
return Result.success();
}
/**
* 设置置顶状态
*/
@PutMapping("/{id}/top/{isTop}")
public Result<Void> setTop(@PathVariable String id, @PathVariable Integer isTop) {
boolean set = diaryCommentService.setTop(id, isTop);
if (!set) {
return Result.error("设置置顶状态失败");
}
return Result.success();
}
/**
* 统计日记评论数量
*/
@GetMapping("/diary/{diaryId}/count")
public Result<Long> countByDiaryId(@PathVariable String diaryId) {
Long count = diaryCommentService.countByDiaryId(diaryId);
return Result.success(count);
}
/**
* 统计用户评论数量
*/
@GetMapping("/user/{userId}/count")
public Result<Long> countByUserId(@PathVariable String userId) {
Long count = diaryCommentService.countByUserId(userId);
return Result.success(count);
}
/**
* 统计回复数量
*/
@GetMapping("/parent/{parentCommentId}/replies/count")
public Result<Long> countReplies(@PathVariable String parentCommentId) {
Long count = diaryCommentService.countReplies(parentCommentId);
return Result.success(count);
}
/**
* 转换实体为响应DTO
*/
private DiaryCommentResponse convertToResponse(DiaryComment comment) {
DiaryCommentResponse response = new DiaryCommentResponse();
BeanUtils.copyProperties(comment, response);
// 转换时间格式
if (comment.getPublishTime() != null) {
response.setPublishTime(comment.getPublishTime().format(DATE_TIME_FORMATTER));
}
if (comment.getLastReplyTime() != null) {
response.setLastReplyTime(comment.getLastReplyTime().format(DATE_TIME_FORMATTER));
}
if (comment.getCreateTime() != null) {
response.setCreateTime(comment.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (comment.getUpdateTime() != null) {
response.setUpdateTime(comment.getUpdateTime().format(DATE_TIME_FORMATTER));
}
// 转换JSON字段
try {
if (comment.getImages() != null) {
response.setImages(objectMapper.readValue(comment.getImages(), new TypeReference<List<String>>() {}));
}
if (comment.getMetadata() != null) {
response.setMetadata(objectMapper.readValue(comment.getMetadata(), Object.class));
}
} catch (JsonProcessingException e) {
// 忽略JSON解析错误
}
return response;
}
}
@@ -0,0 +1,359 @@
package com.emotion.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.emotion.common.BasePageRequest;
import com.emotion.common.PageResult;
import com.emotion.common.Result;
import com.emotion.dto.request.DiaryPostCreateRequest;
import com.emotion.dto.request.DiaryPostUpdateRequest;
import com.emotion.dto.response.DiaryPostResponse;
import com.emotion.entity.DiaryPost;
import com.emotion.service.DiaryPostService;
import com.emotion.service.DiaryCommentService;
import com.emotion.service.UserService;
import com.emotion.service.AiChatService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
import com.emotion.entity.DiaryComment;
/**
* 用户日记控制器
*
* @author emotion-museum
* @date 2025-07-23
*/
@RestController
@RequestMapping("/diary-post")
public class DiaryPostController {
@Autowired
private DiaryPostService diaryPostService;
@Autowired
private DiaryCommentService diaryCommentService;
@Autowired
private UserService userService;
@Autowired
private AiChatService aiChatService;
@Autowired
private ObjectMapper objectMapper;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 分页查询日记
*/
@GetMapping("/page")
public Result<PageResult<DiaryPostResponse>> getPage(@Validated BasePageRequest request) {
IPage<DiaryPost> page = diaryPostService.getPage(request);
List<DiaryPostResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<DiaryPostResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(responses);
return Result.success(pageResult);
}
/**
* 根据用户ID分页查询日记
*/
@GetMapping("/user/{userId}/page")
public Result<PageResult<DiaryPostResponse>> getPageByUserId(@PathVariable String userId,
@Validated BasePageRequest request) {
IPage<DiaryPost> page = diaryPostService.getPageByUserId(userId, request);
List<DiaryPostResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<DiaryPostResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(responses);
return Result.success(pageResult);
}
/**
* 根据用户ID查询公开日记
*/
@GetMapping("/user/{userId}/public/page")
public Result<PageResult<DiaryPostResponse>> getPublicPageByUserId(@PathVariable String userId,
@Validated BasePageRequest request) {
IPage<DiaryPost> page = diaryPostService.getPublicPageByUserId(userId, request);
List<DiaryPostResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<DiaryPostResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(responses);
return Result.success(pageResult);
}
/**
* 查询精选日记
*/
@GetMapping("/featured/page")
public Result<PageResult<DiaryPostResponse>> getFeaturedPage(@Validated BasePageRequest request) {
IPage<DiaryPost> page = diaryPostService.getFeaturedPage(request);
List<DiaryPostResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<DiaryPostResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(responses);
return Result.success(pageResult);
}
/**
* 根据ID获取日记详情
*/
@GetMapping("/{id}")
public Result<DiaryPostResponse> getById(@PathVariable String id) {
DiaryPost diaryPost = diaryPostService.getById(id);
if (diaryPost == null) {
return Result.notFound("日记不存在");
}
// 增加浏览数
diaryPostService.incrementViewCount(id);
return Result.success(convertToResponse(diaryPost));
}
/**
* 创建日记
*/
@PostMapping
public Result<DiaryPostResponse> create(@Valid @RequestBody DiaryPostCreateRequest request) {
DiaryPost diaryPost = diaryPostService.createDiaryPost(request);
return Result.success(convertToResponse(diaryPost));
}
/**
* 发表日记并生成AI评论
*/
@PostMapping("/publish")
public Result<DiaryPostResponse> publish(@Valid @RequestBody DiaryPostCreateRequest request) {
return Result.success(diaryPostService.publishDiaryWithAiComment(request));
}
/**
* 更新日记
*/
@PutMapping("/{id}")
public Result<DiaryPostResponse> update(@PathVariable String id, @Valid @RequestBody DiaryPostUpdateRequest request) {
boolean updated = diaryPostService.updateDiaryPost(id, request);
if (!updated) {
return Result.error("更新失败");
}
DiaryPost updatedDiaryPost = diaryPostService.getById(id);
return Result.success(convertToResponse(updatedDiaryPost));
}
/**
* 删除日记
*/
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable String id) {
boolean deleted = diaryPostService.deleteDiaryPost(id);
if (!deleted) {
return Result.error("删除失败");
}
return Result.success();
}
/**
* 软删除日记
*/
@DeleteMapping("/{id}/soft")
public Result<Void> softDelete(@PathVariable String id) {
boolean deleted = diaryPostService.softDeleteDiaryPost(id);
if (!deleted) {
return Result.error("删除失败");
}
return Result.success();
}
/**
* 恢复日记
*/
@PutMapping("/{id}/restore")
public Result<Void> restore(@PathVariable String id) {
boolean restored = diaryPostService.restoreDiaryPost(id);
if (!restored) {
return Result.error("恢复失败");
}
return Result.success();
}
/**
* 点赞日记
*/
@PostMapping("/{id}/like")
public Result<Void> like(@PathVariable String id) {
boolean liked = diaryPostService.incrementLikeCount(id);
if (!liked) {
return Result.error("点赞失败");
}
return Result.success();
}
/**
* 取消点赞日记
*/
@DeleteMapping("/{id}/like")
public Result<Void> unlike(@PathVariable String id) {
boolean unliked = diaryPostService.decrementLikeCount(id);
if (!unliked) {
return Result.error("取消点赞失败");
}
return Result.success();
}
/**
* 分享日记
*/
@PostMapping("/{id}/share")
public Result<Void> share(@PathVariable String id) {
boolean shared = diaryPostService.incrementShareCount(id);
if (!shared) {
return Result.error("分享失败");
}
return Result.success();
}
/**
* 设置精选状态
*/
@PutMapping("/{id}/featured/{featured}")
public Result<Void> setFeatured(@PathVariable String id, @PathVariable Integer featured) {
boolean set = diaryPostService.setFeatured(id, featured);
if (!set) {
return Result.error("设置精选状态失败");
}
return Result.success();
}
/**
* 设置置顶优先级
*/
@PutMapping("/{id}/priority/{priority}")
public Result<Void> setPriority(@PathVariable String id, @PathVariable Integer priority) {
boolean set = diaryPostService.setPriority(id, priority);
if (!set) {
return Result.error("设置优先级失败");
}
return Result.success();
}
/**
* 统计用户日记数量
*/
@GetMapping("/user/{userId}/count")
public Result<Long> countByUserId(@PathVariable String userId) {
Long count = diaryPostService.countByUserId(userId);
return Result.success(count);
}
/**
* 统计用户公开日记数量
*/
@GetMapping("/user/{userId}/public/count")
public Result<Long> countPublicByUserId(@PathVariable String userId) {
Long count = diaryPostService.countPublicByUserId(userId);
return Result.success(count);
}
/**
* 统计精选日记数量
*/
@GetMapping("/featured/count")
public Result<Long> countFeatured() {
Long count = diaryPostService.countFeatured();
return Result.success(count);
}
/**
* 转换实体为响应DTO
*/
private DiaryPostResponse convertToResponse(DiaryPost diaryPost) {
DiaryPostResponse response = new DiaryPostResponse();
BeanUtils.copyProperties(diaryPost, response);
// 转换时间格式
if (diaryPost.getPublishTime() != null) {
response.setPublishTime(diaryPost.getPublishTime().format(DATE_TIME_FORMATTER));
}
if (diaryPost.getLastCommentTime() != null) {
response.setLastCommentTime(diaryPost.getLastCommentTime().format(DATE_TIME_FORMATTER));
}
if (diaryPost.getAiCommentTime() != null) {
response.setAiCommentTime(diaryPost.getAiCommentTime().format(DATE_TIME_FORMATTER));
}
if (diaryPost.getCreateTime() != null) {
response.setCreateTime(diaryPost.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (diaryPost.getUpdateTime() != null) {
response.setUpdateTime(diaryPost.getUpdateTime().format(DATE_TIME_FORMATTER));
}
// 转换JSON字段
try {
if (diaryPost.getImages() != null) {
response.setImages(objectMapper.readValue(diaryPost.getImages(), new TypeReference<List<String>>() {}));
}
if (diaryPost.getVideos() != null) {
response.setVideos(objectMapper.readValue(diaryPost.getVideos(), new TypeReference<List<String>>() {}));
}
if (diaryPost.getTags() != null) {
response.setTags(objectMapper.readValue(diaryPost.getTags(), new TypeReference<List<String>>() {}));
}
if (diaryPost.getAiKeywords() != null) {
response.setAiKeywords(objectMapper.readValue(diaryPost.getAiKeywords(), new 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 (JsonProcessingException e) {
// 忽略JSON解析错误
}
return response;
}
}
@@ -6,14 +6,18 @@ import com.emotion.common.PageResult;
import com.emotion.common.Result;
import com.emotion.dto.request.UserCreateRequest;
import com.emotion.dto.request.UserUpdateRequest;
import com.emotion.dto.request.UserProfileUpdateRequest;
import com.emotion.dto.response.UserResponse;
import com.emotion.entity.User;
import com.emotion.service.UserService;
import com.emotion.util.UserContextHolder;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.time.format.DateTimeFormatter;
import java.util.List;
@@ -132,6 +136,67 @@ public class UserController {
return Result.success(count);
}
/**
* 获取当前用户个人资料
*/
@GetMapping("/profile")
public Result<UserResponse> getCurrentUserProfile(HttpServletRequest request) {
String currentUserId = getCurrentUserId(request);
if (currentUserId == null) {
return Result.unauthorized("用户未登录");
}
User user = userService.getById(currentUserId);
if (user == null) {
return Result.notFound("用户不存在");
}
return Result.success(convertToResponse(user));
}
/**
* 更新当前用户个人资料
*/
@PutMapping("/profile")
public Result<UserResponse> updateCurrentUserProfile(@Valid @RequestBody UserProfileUpdateRequest request,
HttpServletRequest httpRequest) {
String currentUserId = getCurrentUserId(httpRequest);
if (currentUserId == null) {
return Result.unauthorized("用户未登录");
}
User user = new User();
BeanUtils.copyProperties(request, user);
user.setId(currentUserId);
boolean updated = userService.updateById(user);
if (!updated) {
return Result.error("更新失败");
}
User updatedUser = userService.getById(currentUserId);
return Result.success(convertToResponse(updatedUser));
}
/**
* 获取当前用户ID
* 从JWT拦截器设置的请求属性中获取用户ID
*/
private String getCurrentUserId(HttpServletRequest request) {
// 优先从UserContextHolder获取(线程本地存储)
String userId = UserContextHolder.getCurrentUserId();
if (userId != null) {
return userId;
}
// 如果UserContextHolder中没有,从请求属性中获取(兼容性)
Object userIdAttr = request.getAttribute("userId");
if (userIdAttr != null) {
return userIdAttr.toString();
}
return null;
}
/**
* 转换为响应对象
*/
@@ -0,0 +1,52 @@
package com.emotion.dto.request;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.List;
/**
* 创建日记评论请求DTO
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
public class DiaryCommentCreateRequest {
/**
* 日记ID
*/
@NotBlank(message = "日记ID不能为空")
private String diaryId;
/**
* 用户ID
*/
@NotBlank(message = "用户ID不能为空")
private String userId;
/**
* 评论内容
*/
@NotBlank(message = "评论内容不能为空")
private String content;
/**
* 评论图片
*/
private List<String> images;
/**
* 父评论ID (用于回复功能)
*/
private String parentCommentId;
/**
* 是否匿名评论: 0-实名, 1-匿名
*/
@NotNull(message = "是否匿名不能为空")
private Integer isAnonymous;
}
@@ -0,0 +1,97 @@
package com.emotion.dto.request;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.math.BigDecimal;
import java.util.List;
/**
* 创建日记请求DTO
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
public class DiaryPostCreateRequest {
/**
* 日记标题
*/
@Size(max = 200, message = "日记标题长度不能超过200个字符")
private String title;
/**
* 日记内容
*/
@NotBlank(message = "日记内容不能为空")
private String content;
/**
* 图片列表
*/
private List<String> images;
/**
* 视频列表
*/
private List<String> videos;
/**
* 发布地点
*/
@Size(max = 200, message = "发布地点长度不能超过200个字符")
private String location;
/**
* 纬度
*/
private BigDecimal latitude;
/**
* 经度
*/
private BigDecimal longitude;
/**
* 天气信息
*/
@Size(max = 50, message = "天气信息长度不能超过50个字符")
private String weather;
/**
* 心情状态
*/
@Size(max = 50, message = "心情状态长度不能超过50个字符")
private String mood;
/**
* 心情评分 (0-10)
*/
private BigDecimal moodScore;
/**
* 标签列表
*/
private List<String> tags;
/**
* 是否公开: 0-仅自己可见, 1-公开
*/
@NotNull(message = "是否公开不能为空")
private Integer isPublic;
/**
* 是否匿名发布: 0-实名, 1-匿名
*/
@NotNull(message = "是否匿名不能为空")
private Integer isAnonymous;
/**
* 用户ID
*/
@NotBlank(message = "用户ID不能为空")
private String userId;
}
@@ -0,0 +1,91 @@
package com.emotion.dto.request;
import lombok.Data;
import javax.validation.constraints.Size;
import java.math.BigDecimal;
import java.util.List;
/**
* 更新日记请求DTO
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
public class DiaryPostUpdateRequest {
/**
* 日记标题
*/
@Size(max = 200, message = "日记标题长度不能超过200个字符")
private String title;
/**
* 日记内容
*/
private String content;
/**
* 图片列表
*/
private List<String> images;
/**
* 视频列表
*/
private List<String> videos;
/**
* 发布地点
*/
@Size(max = 200, message = "发布地点长度不能超过200个字符")
private String location;
/**
* 纬度
*/
private BigDecimal latitude;
/**
* 经度
*/
private BigDecimal longitude;
/**
* 天气信息
*/
@Size(max = 50, message = "天气信息长度不能超过50个字符")
private String weather;
/**
* 心情状态
*/
@Size(max = 50, message = "心情状态长度不能超过50个字符")
private String mood;
/**
* 心情评分 (0-10)
*/
private BigDecimal moodScore;
/**
* 标签列表
*/
private List<String> tags;
/**
* 是否公开: 0-仅自己可见, 1-公开
*/
private Integer isPublic;
/**
* 是否匿名发布: 0-实名, 1-匿名
*/
private Integer isAnonymous;
/**
* 状态: draft-草稿, published-已发布, hidden-隐藏, deleted-已删除
*/
private String status;
}
@@ -32,6 +32,12 @@ public class RegisterRequest extends BaseRequest {
@Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间")
private String password;
/**
* 确认密码
*/
@NotBlank(message = "确认密码不能为空")
private String confirmPassword;
/**
* 用户名
*/
@@ -51,7 +57,7 @@ public class RegisterRequest extends BaseRequest {
/**
* 手机号
*/
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
@Pattern(regexp = "^$|^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
/**
@@ -0,0 +1,60 @@
package com.emotion.dto.request;
import lombok.Data;
import javax.validation.constraints.Email;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.time.LocalDate;
/**
* 用户个人资料更新请求类
*
* @author emotion-museum
* @date 2025-07-26
*/
@Data
public class UserProfileUpdateRequest {
/**
* 昵称
*/
@Size(max = 50, message = "昵称长度不能超过50个字符")
private String nickname;
/**
* 邮箱
*/
@Email(message = "邮箱格式不正确")
@Size(max = 100, message = "邮箱长度不能超过100个字符")
private String email;
/**
* 手机号
*/
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
/**
* 头像URL
*/
@Size(max = 500, message = "头像URL长度不能超过500个字符")
private String avatar;
/**
* 生日
*/
private LocalDate birthDate;
/**
* 所在地
*/
@Size(max = 100, message = "所在地长度不能超过100个字符")
private String location;
/**
* 个人简介
*/
@Size(max = 500, message = "个人简介长度不能超过500个字符")
private String bio;
}
@@ -5,46 +5,69 @@ import lombok.EqualsAndHashCode;
import javax.validation.constraints.Email;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.time.LocalDate;
/**
* 用户更新请求类
*
*
* @author emotion-museum
* @date 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class UserUpdateRequest extends BaseRequest {
/**
* 用户名
*/
@Size(max = 50, message = "用户名长度不能超过50个字符")
private String username;
/**
* 昵称
*/
@Size(max = 50, message = "昵称长度不能超过50个字符")
private String nickname;
/**
* 邮箱
*/
@Email(message = "邮箱格式不正确")
@Size(max = 100, message = "邮箱长度不能超过100个字符")
private String email;
/**
* 手机号
*/
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
/**
* 头像
* 头像URL
*/
@Size(max = 500, message = "头像URL长度不能超过500个字符")
private String avatar;
/**
* 生日
*/
private LocalDate birthDate;
/**
* 所在地
*/
@Size(max = 100, message = "所在地长度不能超过100个字符")
private String location;
/**
* 个人简介
*/
@Size(max = 500, message = "个人简介长度不能超过500个字符")
private String bio;
/**
* 状态
*/
private Integer status;
}
}
@@ -0,0 +1,136 @@
package com.emotion.dto.response;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* 日记评论响应DTO
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
public class DiaryCommentResponse {
/**
* 评论ID
*/
private String id;
/**
* 日记ID
*/
private String diaryId;
/**
* 评论用户ID
*/
private String userId;
/**
* 评论用户信息
*/
private UserInfoResponse userInfo;
/**
* 父评论ID (用于回复功能)
*/
private String parentCommentId;
/**
* 父评论信息
*/
private DiaryCommentResponse parentComment;
/**
* 评论内容
*/
private String content;
/**
* 评论图片
*/
private List<String> images;
/**
* 评论类型: user-用户评论, ai-AI评论, system-系统评论
*/
private String commentType;
/**
* AI评论来源 (如: emotion_analysis, sentiment_analysis)
*/
private String aiCommentSource;
/**
* 点赞数
*/
private Integer likeCount;
/**
* 回复数
*/
private Integer replyCount;
/**
* 是否匿名评论: 0-实名, 1-匿名
*/
private Integer isAnonymous;
/**
* 是否置顶: 0-普通, 1-置顶
*/
private Integer isTop;
/**
* 状态: published-已发布, hidden-隐藏, deleted-已删除
*/
private String status;
/**
* 发布时间
*/
private String publishTime;
/**
* 最后回复时间
*/
private String lastReplyTime;
/**
* 情绪评分
*/
private BigDecimal emotionScore;
/**
* 情感评分
*/
private BigDecimal sentimentScore;
/**
* 扩展元数据
*/
private Object metadata;
/**
* 创建时间
*/
private String createTime;
/**
* 更新时间
*/
private String updateTime;
/**
* 是否已点赞
*/
private Boolean isLiked;
/**
* 子评论列表
*/
private List<DiaryCommentResponse> replies;
}
@@ -0,0 +1,197 @@
package com.emotion.dto.response;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 日记响应DTO
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
public class DiaryPostResponse {
/**
* 日记ID
*/
private String id;
/**
* 用户ID
*/
private String userId;
/**
* 用户信息
*/
private UserInfoResponse userInfo;
/**
* 日记标题
*/
private String title;
/**
* 日记内容
*/
private String content;
/**
* 图片列表
*/
private List<String> images;
/**
* 视频列表
*/
private List<String> videos;
/**
* 发布地点
*/
private String location;
/**
* 纬度
*/
private BigDecimal latitude;
/**
* 经度
*/
private BigDecimal longitude;
/**
* 天气信息
*/
private String weather;
/**
* 心情状态
*/
private String mood;
/**
* 心情评分 (0-10)
*/
private BigDecimal moodScore;
/**
* 标签列表
*/
private List<String> tags;
/**
* 是否公开: 0-仅自己可见, 1-公开
*/
private Integer isPublic;
/**
* 是否匿名发布: 0-实名, 1-匿名
*/
private Integer isAnonymous;
/**
* 浏览数
*/
private Integer viewCount;
/**
* 点赞数
*/
private Integer likeCount;
/**
* 评论数
*/
private Integer commentCount;
/**
* 分享数
*/
private Integer shareCount;
/**
* AI评论内容
*/
private String aiComment;
/**
* AI评论时间
*/
private String aiCommentTime;
/**
* AI情绪分析结果
*/
private Object aiEmotionAnalysis;
/**
* AI情感评分 (-1到1)
*/
private BigDecimal aiSentimentScore;
/**
* AI提取的关键词
*/
private List<String> aiKeywords;
/**
* AI建议
*/
private String aiSuggestions;
/**
* 发布时间
*/
private String publishTime;
/**
* 最后评论时间
*/
private String lastCommentTime;
/**
* 状态: draft-草稿, published-已发布, hidden-隐藏, deleted-已删除
*/
private String status;
/**
* 优先级 (用于置顶功能)
*/
private Integer priority;
/**
* 是否精选: 0-普通, 1-精选
*/
private Integer featured;
/**
* 扩展元数据
*/
private Object metadata;
/**
* 创建时间
*/
private String createTime;
/**
* 更新时间
*/
private String updateTime;
/**
* 是否已点赞
*/
private Boolean isLiked;
/**
* 是否已收藏
*/
private Boolean isBookmarked;
}
@@ -0,0 +1,129 @@
package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 日记评论实体类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("diary_comment")
public class DiaryComment extends BaseEntity {
/**
* 日记ID
*/
@TableField("diary_id")
private String diaryId;
/**
* 评论用户ID
*/
@TableField("user_id")
private String userId;
/**
* 父评论ID (用于回复功能)
*/
@TableField("parent_comment_id")
private String parentCommentId;
/**
* 评论内容
*/
@TableField("content")
private String content;
/**
* 评论图片 (存储图片URL数组)
*/
@TableField("images")
private String images;
/**
* 评论类型: user-用户评论, ai-AI评论, system-系统评论
*/
@TableField("comment_type")
private String commentType;
/**
* AI评论来源 (如: emotion_analysis, sentiment_analysis)
*/
@TableField("ai_comment_source")
private String aiCommentSource;
/**
* 点赞数
*/
@TableField("like_count")
private Integer likeCount;
/**
* 回复数
*/
@TableField("reply_count")
private Integer replyCount;
/**
* 是否匿名评论: 0-实名, 1-匿名
*/
@TableField("is_anonymous")
private Integer isAnonymous;
/**
* 是否置顶: 0-普通, 1-置顶
*/
@TableField("is_top")
private Integer isTop;
/**
* 状态: published-已发布, hidden-隐藏, deleted-已删除
*/
@TableField("status")
private String status;
/**
* 发布时间
*/
@TableField("publish_time")
private LocalDateTime publishTime;
/**
* 最后回复时间
*/
@TableField("last_reply_time")
private LocalDateTime lastReplyTime;
/**
* 情绪评分
*/
@TableField("emotion_score")
private BigDecimal emotionScore;
/**
* 情感评分
*/
@TableField("sentiment_score")
private BigDecimal sentimentScore;
/**
* 扩展元数据
*/
@TableField("metadata")
private String metadata;
}
@@ -0,0 +1,207 @@
package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 用户日记实体类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("diary_post")
public class DiaryPost extends BaseEntity {
/**
* 用户ID
*/
@TableField("user_id")
private String userId;
/**
* 日记标题
*/
@TableField("title")
private String title;
/**
* 日记内容
*/
@TableField("content")
private String content;
/**
* 图片列表 (存储图片URL数组)
*/
@TableField("images")
private String images;
/**
* 视频列表 (存储视频URL数组)
*/
@TableField("videos")
private String videos;
/**
* 发布地点
*/
@TableField("location")
private String location;
/**
* 纬度
*/
@TableField("latitude")
private BigDecimal latitude;
/**
* 经度
*/
@TableField("longitude")
private BigDecimal longitude;
/**
* 天气信息
*/
@TableField("weather")
private String weather;
/**
* 心情状态
*/
@TableField("mood")
private String mood;
/**
* 心情评分 (0-10)
*/
@TableField("mood_score")
private BigDecimal moodScore;
/**
* 标签列表
*/
@TableField("tags")
private String tags;
/**
* 是否公开: 0-仅自己可见, 1-公开
*/
@TableField("is_public")
private Integer isPublic;
/**
* 是否匿名发布: 0-实名, 1-匿名
*/
@TableField("is_anonymous")
private Integer isAnonymous;
/**
* 浏览数
*/
@TableField("view_count")
private Integer viewCount;
/**
* 点赞数
*/
@TableField("like_count")
private Integer likeCount;
/**
* 评论数
*/
@TableField("comment_count")
private Integer commentCount;
/**
* 分享数
*/
@TableField("share_count")
private Integer shareCount;
/**
* AI评论内容
*/
@TableField("ai_comment")
private String aiComment;
/**
* AI评论时间
*/
@TableField("ai_comment_time")
private LocalDateTime aiCommentTime;
/**
* AI情绪分析结果
*/
@TableField("ai_emotion_analysis")
private String aiEmotionAnalysis;
/**
* AI情感评分 (-1到1)
*/
@TableField("ai_sentiment_score")
private BigDecimal aiSentimentScore;
/**
* AI提取的关键词
*/
@TableField("ai_keywords")
private String aiKeywords;
/**
* AI建议
*/
@TableField("ai_suggestions")
private String aiSuggestions;
/**
* 发布时间
*/
@TableField("publish_time")
private LocalDateTime publishTime;
/**
* 最后评论时间
*/
@TableField("last_comment_time")
private LocalDateTime lastCommentTime;
/**
* 状态: draft-草稿, published-已发布, hidden-隐藏, deleted-已删除
*/
@TableField("status")
private String status;
/**
* 优先级 (用于置顶功能)
*/
@TableField("priority")
private Integer priority;
/**
* 是否精选: 0-普通, 1-精选
*/
@TableField("featured")
private Integer featured;
/**
* 扩展元数据
*/
@TableField("metadata")
private String metadata;
}
@@ -0,0 +1,15 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.DiaryComment;
import org.apache.ibatis.annotations.Mapper;
/**
* 日记评论Mapper接口
*
* @author emotion-museum
* @date 2025-07-23
*/
@Mapper
public interface DiaryCommentMapper extends BaseMapper<DiaryComment> {
}
@@ -0,0 +1,15 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.DiaryPost;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户日记Mapper接口
*
* @author emotion-museum
* @date 2025-07-23
*/
@Mapper
public interface DiaryPostMapper extends BaseMapper<DiaryPost> {
}
@@ -117,4 +117,13 @@ public interface AiChatService {
* @return 包含情绪总结等信息的CompletableFuture
*/
java.util.concurrent.CompletableFuture<java.util.Map<String, Object>> generateEmotionSummaryAsync(String userId);
/**
* 发送日记内容生成AI总结评论
* @param conversationId 会话ID
* @param userMessage 用户日记内容
* @param userId 用户ID
* @return AI评论内容
*/
String sendSummaryMessage(String conversationId, String userMessage, String userId);
}
@@ -97,9 +97,33 @@ public interface AuthService {
/**
* 从令牌中获取用户名
*
*
* @param token 访问令牌
* @return 用户名
*/
String getUsernameFromToken(String token);
/**
* 检查账号是否存在
*
* @param account 账号
* @return 是否存在
*/
boolean existsByAccount(String account);
/**
* 检查邮箱是否存在
*
* @param email 邮箱
* @return 是否存在
*/
boolean existsByEmail(String email);
/**
* 检查手机号是否存在
*
* @param phone 手机号
* @return 是否存在
*/
boolean existsByPhone(String phone);
}
@@ -0,0 +1,135 @@
package com.emotion.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.emotion.common.BasePageRequest;
import com.emotion.entity.DiaryComment;
import java.math.BigDecimal;
import java.util.List;
/**
* 日记评论服务接口
*
* @author emotion-museum
* @date 2025-07-23
*/
public interface DiaryCommentService extends IService<DiaryComment> {
/**
* 分页查询评论
*/
IPage<DiaryComment> getPage(BasePageRequest request);
/**
* 根据日记ID分页查询评论
*/
IPage<DiaryComment> getPageByDiaryId(String diaryId, BasePageRequest request);
/**
* 根据用户ID分页查询评论
*/
IPage<DiaryComment> getPageByUserId(String userId, BasePageRequest request);
/**
* 根据父评论ID查询回复列表
*/
List<DiaryComment> getRepliesByParentId(String parentCommentId);
/**
* 根据评论类型查询评论列表
*/
List<DiaryComment> getByCommentType(String commentType);
/**
* 根据日记ID和评论类型查询评论列表
*/
List<DiaryComment> getByDiaryIdAndCommentType(String diaryId, String commentType);
/**
* 增加点赞数
*/
boolean incrementLikeCount(String commentId);
/**
* 减少点赞数
*/
boolean decrementLikeCount(String commentId);
/**
* 增加回复数
*/
boolean incrementReplyCount(String commentId);
/**
* 减少回复数
*/
boolean decrementReplyCount(String commentId);
/**
* 更新最后回复时间
*/
boolean updateLastReplyTime(String commentId);
/**
* 设置置顶状态
*/
boolean setTop(String commentId, Integer isTop);
/**
* 统计日记评论数量
*/
Long countByDiaryId(String diaryId);
/**
* 统计用户评论数量
*/
Long countByUserId(String userId);
/**
* 统计指定类型的评论数量
*/
Long countByCommentType(String commentType);
/**
* 统计回复数量
*/
Long countReplies(String parentCommentId);
/**
* 创建评论
*/
DiaryComment createComment(String diaryId, String userId, String content, List<String> images,
String parentCommentId, Integer isAnonymous);
/**
* 创建AI评论
*/
DiaryComment createAiComment(String diaryId, String content, String aiCommentSource,
BigDecimal emotionScore, BigDecimal sentimentScore);
/**
* 更新评论
*/
boolean updateComment(String commentId, String content, List<String> images);
/**
* 删除评论
*/
boolean deleteComment(String commentId);
/**
* 软删除评论
*/
boolean softDeleteComment(String commentId);
/**
* 恢复评论
*/
boolean restoreComment(String commentId);
/**
* 获取评论树结构
*/
List<DiaryComment> getCommentTree(String diaryId);
}
@@ -0,0 +1,166 @@
package com.emotion.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.emotion.common.BasePageRequest;
import com.emotion.entity.DiaryPost;
import java.math.BigDecimal;
import java.util.List;
/**
* 用户日记服务接口
*
* @author emotion-museum
* @date 2025-07-23
*/
public interface DiaryPostService extends IService<DiaryPost> {
/**
* 分页查询日记
*/
IPage<DiaryPost> getPage(BasePageRequest request);
/**
* 根据用户ID分页查询日记
*/
IPage<DiaryPost> getPageByUserId(String userId, BasePageRequest request);
/**
* 根据用户ID查询公开日记
*/
IPage<DiaryPost> getPublicPageByUserId(String userId, BasePageRequest request);
/**
* 查询精选日记
*/
IPage<DiaryPost> getFeaturedPage(BasePageRequest request);
/**
* 根据状态查询日记列表
*/
List<DiaryPost> getByStatus(String status);
/**
* 根据用户ID和状态查询日记列表
*/
List<DiaryPost> getByUserIdAndStatus(String userId, String status);
/**
* 根据心情状态查询日记列表
*/
List<DiaryPost> getByMood(String mood);
/**
* 根据标签查询日记列表
*/
List<DiaryPost> getByTags(List<String> tags);
/**
* 根据地点查询日记列表
*/
List<DiaryPost> getByLocation(String location);
/**
* 增加浏览数
*/
boolean incrementViewCount(String diaryId);
/**
* 增加点赞数
*/
boolean incrementLikeCount(String diaryId);
/**
* 减少点赞数
*/
boolean decrementLikeCount(String diaryId);
/**
* 增加评论数
*/
boolean incrementCommentCount(String diaryId);
/**
* 减少评论数
*/
boolean decrementCommentCount(String diaryId);
/**
* 增加分享数
*/
boolean incrementShareCount(String diaryId);
/**
* 更新最后评论时间
*/
boolean updateLastCommentTime(String diaryId);
/**
* 设置精选状态
*/
boolean setFeatured(String diaryId, Integer featured);
/**
* 设置置顶优先级
*/
boolean setPriority(String diaryId, Integer priority);
/**
* 统计用户日记数量
*/
Long countByUserId(String userId);
/**
* 统计用户公开日记数量
*/
Long countPublicByUserId(String userId);
/**
* 统计精选日记数量
*/
Long countFeatured();
/**
* 统计指定状态的日记数量
*/
Long countByStatus(String status);
/**
* 创建日记
*/
DiaryPost createDiaryPost(com.emotion.dto.request.DiaryPostCreateRequest request);
/**
* 更新日记
*/
boolean updateDiaryPost(String diaryId, com.emotion.dto.request.DiaryPostUpdateRequest request);
/**
* 删除日记
*/
boolean deleteDiaryPost(String diaryId);
/**
* 软删除日记
*/
boolean softDeleteDiaryPost(String diaryId);
/**
* 恢复日记
*/
boolean restoreDiaryPost(String diaryId);
/**
* 添加AI评论
*/
boolean addAiComment(String diaryId, String aiComment, Object aiEmotionAnalysis,
BigDecimal aiSentimentScore, List<String> aiKeywords, String aiSuggestions);
/**
* 发表日记并生成AI评论
* @param request 日记创建请求
* @return 日记响应对象,包含AI评论
*/
com.emotion.dto.response.DiaryPostResponse publishDiaryWithAiComment(com.emotion.dto.request.DiaryPostCreateRequest request);
}
@@ -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};
}
}
@@ -473,7 +473,122 @@ CREATE TABLE reward (
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '奖励表';
-- ============================================================================
-- 14. 访客用户表 (guest_user)
-- 14. 用户统计表 (user_stats)
-- ============================================================================
CREATE TABLE user_stats (
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
user_id VARCHAR(64) UNIQUE COMMENT '用户ID', -- 用户ID
total_conversations INT DEFAULT 0 COMMENT '总对话数', -- 总对话数
total_messages INT DEFAULT 0 COMMENT '总消息数', -- 总消息数
total_emotions_recorded INT DEFAULT 0 COMMENT '总情绪记录数', -- 总情绪记录数
topics_completed INT DEFAULT 0 COMMENT '完成的课题数', -- 完成的课题数
achievements_unlocked INT DEFAULT 0 COMMENT '解锁的成就数', -- 解锁的成就数
total_points INT DEFAULT 0 COMMENT '总积分', -- 总积分
consecutive_days INT DEFAULT 0 COMMENT '连续使用天数', -- 连续使用天数
max_consecutive_days INT DEFAULT 0 COMMENT '最大连续天数', -- 最大连续天数
locations_visited INT DEFAULT 0 COMMENT '访问的地点数', -- 访问的地点数
posts_created INT DEFAULT 0 COMMENT '创建的帖子数', -- 创建的帖子数
comments_made INT DEFAULT 0 COMMENT '评论数', -- 评论数
likes_received INT DEFAULT 0 COMMENT '收到的点赞数', -- 收到的点赞数
social_interactions INT DEFAULT 0 COMMENT '社交互动数', -- 社交互动数
-- 日记相关统计
diary_posts_created INT DEFAULT 0 COMMENT '创建的日记数', -- 创建的日记数
diary_likes_received INT DEFAULT 0 COMMENT '日记收到的点赞数', -- 日记收到的点赞数
diary_comments_received INT DEFAULT 0 COMMENT '日记收到的评论数', -- 日记收到的评论数
diary_views_received INT DEFAULT 0 COMMENT '日记收到的浏览数', -- 日记收到的浏览数
diary_shares_received INT DEFAULT 0 COMMENT '日记收到的分享数', -- 日记收到的分享数
diary_comments_made INT DEFAULT 0 COMMENT '发表的日记评论数', -- 发表的日记评论数
diary_likes_given INT DEFAULT 0 COMMENT '给他人日记的点赞数', -- 给他人日记的点赞数
featured_diary_count INT DEFAULT 0 COMMENT '精选日记数量', -- 精选日记数量
ai_comments_received INT DEFAULT 0 COMMENT '收到的AI评论数', -- 收到的AI评论数
-- 公共字段
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户统计表';
-- ============================================================================
-- 16. 用户日记表 (diary_post) - 类似朋友圈功能
-- 关联说明: user_id 关联 user.id,通过代码逻辑维护关联关系
-- ============================================================================
CREATE TABLE diary_post (
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
user_id VARCHAR(64) COMMENT '用户ID (关联user.id)', -- 用户ID (关联user.id)
title VARCHAR(200) COMMENT '日记标题', -- 日记标题
content TEXT COMMENT '日记内容', -- 日记内容
images JSON COMMENT '图片列表 (存储图片URL数组)', -- 图片列表 (存储图片URL数组)
videos JSON COMMENT '视频列表 (存储视频URL数组)', -- 视频列表 (存储视频URL数组)
location VARCHAR(200) COMMENT '发布地点', -- 发布地点
latitude DECIMAL(10, 8) COMMENT '纬度', -- 纬度
longitude DECIMAL(11, 8) COMMENT '经度', -- 经度
weather VARCHAR(50) COMMENT '天气信息', -- 天气信息
mood VARCHAR(50) COMMENT '心情状态', -- 心情状态
mood_score DECIMAL(3, 2) COMMENT '心情评分 (0-10)', -- 心情评分 (0-10)
tags JSON COMMENT '标签列表', -- 标签列表
is_public TINYINT DEFAULT 1 COMMENT '是否公开: 0-仅自己可见, 1-公开', -- 是否公开: 0-仅自己可见, 1-公开
is_anonymous TINYINT DEFAULT 0 COMMENT '是否匿名发布: 0-实名, 1-匿名', -- 是否匿名发布: 0-实名, 1-匿名
view_count INT DEFAULT 0 COMMENT '浏览数', -- 浏览数
like_count INT DEFAULT 0 COMMENT '点赞数', -- 点赞数
comment_count INT DEFAULT 0 COMMENT '评论数', -- 评论数
share_count INT DEFAULT 0 COMMENT '分享数', -- 分享数
ai_comment TEXT COMMENT 'AI评论内容', -- AI评论内容
ai_comment_time DATETIME COMMENT 'AI评论时间', -- AI评论时间
ai_emotion_analysis JSON COMMENT 'AI情绪分析结果', -- AI情绪分析结果
ai_sentiment_score DECIMAL(3, 2) COMMENT 'AI情感评分 (-1到1)', -- AI情感评分 (-1到1)
ai_keywords JSON COMMENT 'AI提取的关键词', -- AI提取的关键词
ai_suggestions TEXT COMMENT 'AI建议', -- AI建议
publish_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '发布时间', -- 发布时间
last_comment_time DATETIME COMMENT '最后评论时间', -- 最后评论时间
status VARCHAR(20) DEFAULT 'published' COMMENT '状态: draft-草稿, published-已发布, hidden-隐藏, deleted-已删除', -- 状态: draft-草稿, published-已发布, hidden-隐藏, deleted-已删除
priority INT DEFAULT 0 COMMENT '优先级 (用于置顶功能)', -- 优先级 (用于置顶功能)
featured TINYINT DEFAULT 0 COMMENT '是否精选: 0-普通, 1-精选', -- 是否精选: 0-普通, 1-精选
metadata JSON COMMENT '扩展元数据', -- 扩展元数据
-- 公共字段
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户日记表 - 类似朋友圈功能';
-- ============================================================================
-- 17. 日记评论表 (diary_comment)
-- 关联说明: diary_id 关联 diary_post.iduser_id 关联 user.id,通过代码逻辑维护关联关系
-- ============================================================================
CREATE TABLE diary_comment (
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
diary_id VARCHAR(64) COMMENT '日记ID (关联diary_post.id)', -- 日记ID (关联diary_post.id)
user_id VARCHAR(64) COMMENT '评论用户ID (关联user.id)', -- 评论用户ID (关联user.id)
parent_comment_id VARCHAR(64) COMMENT '父评论ID (用于回复功能)', -- 父评论ID (用于回复功能)
content TEXT COMMENT '评论内容', -- 评论内容
images JSON COMMENT '评论图片 (存储图片URL数组)', -- 评论图片 (存储图片URL数组)
comment_type VARCHAR(20) DEFAULT 'user' COMMENT '评论类型: user-用户评论, ai-AI评论, system-系统评论', -- 评论类型: user-用户评论, ai-AI评论, system-系统评论
ai_comment_source VARCHAR(50) COMMENT 'AI评论来源 (如: emotion_analysis, sentiment_analysis)', -- AI评论来源 (如: emotion_analysis, sentiment_analysis)
like_count INT DEFAULT 0 COMMENT '点赞数', -- 点赞数
reply_count INT DEFAULT 0 COMMENT '回复数', -- 回复数
is_anonymous TINYINT DEFAULT 0 COMMENT '是否匿名评论: 0-实名, 1-匿名', -- 是否匿名评论: 0-实名, 1-匿名
is_top TINYINT DEFAULT 0 COMMENT '是否置顶: 0-普通, 1-置顶', -- 是否置顶: 0-普通, 1-置顶
status VARCHAR(20) DEFAULT 'published' COMMENT '状态: published-已发布, hidden-隐藏, deleted-已删除', -- 状态: published-已发布, hidden-隐藏, deleted-已删除
publish_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '发布时间', -- 发布时间
last_reply_time DATETIME COMMENT '最后回复时间', -- 最后回复时间
emotion_score DECIMAL(3, 2) COMMENT '情绪评分', -- 情绪评分
sentiment_score DECIMAL(3, 2) COMMENT '情感评分', -- 情感评分
metadata JSON COMMENT '扩展元数据', -- 扩展元数据
-- 公共字段
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '日记评论表';
-- ============================================================================
-- 18. 访客用户表 (guest_user)
-- ============================================================================
CREATE TABLE guest_user (
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
@@ -496,34 +611,6 @@ CREATE TABLE guest_user (
remarks VARCHAR(500) COMMENT '备注' -- 备注
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '访客用户表';
-- ============================================================================
-- 15. 用户统计表 (user_stats)
-- ============================================================================
CREATE TABLE user_stats (
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
user_id VARCHAR(64) UNIQUE COMMENT '用户ID', -- 用户ID
total_conversations INT DEFAULT 0 COMMENT '总对话数', -- 总对话数
total_messages INT DEFAULT 0 COMMENT '总消息数', -- 总消息数
total_emotions_recorded INT DEFAULT 0 COMMENT '总情绪记录数', -- 总情绪记录数
topics_completed INT DEFAULT 0 COMMENT '完成的课题数', -- 完成的课题数
achievements_unlocked INT DEFAULT 0 COMMENT '解锁的成就数', -- 解锁的成就数
total_points INT DEFAULT 0 COMMENT '总积分', -- 总积分
consecutive_days INT DEFAULT 0 COMMENT '连续使用天数', -- 连续使用天数
max_consecutive_days INT DEFAULT 0 COMMENT '最大连续天数', -- 最大连续天数
locations_visited INT DEFAULT 0 COMMENT '访问的地点数', -- 访问的地点数
posts_created INT DEFAULT 0 COMMENT '创建的帖子数', -- 创建的帖子数
comments_made INT DEFAULT 0 COMMENT '评论数', -- 评论数
likes_received INT DEFAULT 0 COMMENT '收到的点赞数', -- 收到的点赞数
social_interactions INT DEFAULT 0 COMMENT '社交互动数', -- 社交互动数
-- 公共字段
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户统计表';
-- ============================================================================
-- 创建索引以提高查询性能
-- 注意: MySQL的CREATE INDEX不支持IF NOT EXISTS
@@ -813,6 +900,105 @@ CREATE INDEX idx_user_stats_update_time ON user_stats (update_time);
CREATE INDEX idx_user_stats_create_time ON user_stats (create_time);
-- user_stats表日记相关索引
CREATE INDEX idx_user_stats_diary_posts_created ON user_stats (diary_posts_created);
CREATE INDEX idx_user_stats_diary_likes_received ON user_stats (diary_likes_received);
CREATE INDEX idx_user_stats_diary_comments_received ON user_stats (diary_comments_received);
CREATE INDEX idx_user_stats_diary_views_received ON user_stats (diary_views_received);
CREATE INDEX idx_user_stats_featured_diary_count ON user_stats (featured_diary_count);
CREATE INDEX idx_user_stats_ai_comments_received ON user_stats (ai_comments_received);
-- diary_post表索引
CREATE INDEX idx_diary_post_user_id ON diary_post (user_id);
CREATE INDEX idx_diary_post_publish_time ON diary_post (publish_time);
CREATE INDEX idx_diary_post_last_comment_time ON diary_post (last_comment_time);
CREATE INDEX idx_diary_post_status ON diary_post (status);
CREATE INDEX idx_diary_post_priority ON diary_post (priority);
CREATE INDEX idx_diary_post_featured ON diary_post (featured);
CREATE INDEX idx_diary_post_create_time ON diary_post (create_time);
-- diary_comment表索引
CREATE INDEX idx_diary_comment_diary_id ON diary_comment (diary_id);
CREATE INDEX idx_diary_comment_user_id ON diary_comment (user_id);
CREATE INDEX idx_diary_comment_parent_comment_id ON diary_comment (parent_comment_id);
CREATE INDEX idx_diary_comment_publish_time ON diary_comment (publish_time);
CREATE INDEX idx_diary_comment_last_reply_time ON diary_comment (last_reply_time);
CREATE INDEX idx_diary_comment_emotion_score ON diary_comment (emotion_score);
CREATE INDEX idx_diary_comment_sentiment_score ON diary_comment (sentiment_score);
CREATE INDEX idx_diary_comment_create_time ON diary_comment (create_time);
-- diary_post表复合索引和功能索引
CREATE INDEX idx_diary_post_user_publish ON diary_post (user_id, publish_time);
CREATE INDEX idx_diary_post_user_status ON diary_post (user_id, status);
CREATE INDEX idx_diary_post_public_publish ON diary_post (is_public, publish_time);
CREATE INDEX idx_diary_post_featured_publish ON diary_post (featured, publish_time);
CREATE INDEX idx_diary_post_mood_score ON diary_post (mood_score);
CREATE INDEX idx_diary_post_ai_sentiment ON diary_post (ai_sentiment_score);
CREATE INDEX idx_diary_post_location ON diary_post (latitude, longitude);
CREATE INDEX idx_diary_post_like_count ON diary_post (like_count);
CREATE INDEX idx_diary_post_comment_count ON diary_post (comment_count);
CREATE INDEX idx_diary_post_view_count ON diary_post (view_count);
CREATE INDEX idx_diary_post_create_by ON diary_post (create_by);
CREATE INDEX idx_diary_post_update_by ON diary_post (update_by);
CREATE INDEX idx_diary_post_is_deleted ON diary_post (is_deleted);
-- diary_comment表复合索引和功能索引
CREATE INDEX idx_diary_comment_diary_publish ON diary_comment (diary_id, publish_time);
CREATE INDEX idx_diary_comment_diary_type ON diary_comment (diary_id, comment_type);
CREATE INDEX idx_diary_comment_user_publish ON diary_comment (user_id, publish_time);
CREATE INDEX idx_diary_comment_parent_publish ON diary_comment (parent_comment_id, publish_time);
CREATE INDEX idx_diary_comment_type_publish ON diary_comment (comment_type, publish_time);
CREATE INDEX idx_diary_comment_status ON diary_comment (status);
CREATE INDEX idx_diary_comment_is_top ON diary_comment (is_top);
CREATE INDEX idx_diary_comment_like_count ON diary_comment (like_count);
CREATE INDEX idx_diary_comment_reply_count ON diary_comment (reply_count);
CREATE INDEX idx_diary_comment_ai_source ON diary_comment (ai_comment_source);
CREATE INDEX idx_diary_comment_create_by ON diary_comment (create_by);
CREATE INDEX idx_diary_comment_update_by ON diary_comment (update_by);
CREATE INDEX idx_diary_comment_is_deleted ON diary_comment (is_deleted);
-- guest_user表索引
CREATE INDEX idx_guest_user_guest_user_id ON guest_user (guest_user_id);
@@ -0,0 +1,100 @@
package com.emotion.controller;
import com.emotion.service.AuthService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
/**
* AuthController 测试类
*
* @author emotion-museum
* @date 2025-07-26
*/
@WebMvcTest(AuthController.class)
public class AuthControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private AuthService authService;
@Test
public void testCheckAccountExists() throws Exception {
// 模拟账户存在的情况
when(authService.existsByAccount("existingUser")).thenReturn(true);
mockMvc.perform(get("/auth/check-account")
.param("account", "existingUser"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value(true));
}
@Test
public void testCheckAccountNotExists() throws Exception {
// 模拟账户不存在的情况
when(authService.existsByAccount("nonExistingUser")).thenReturn(false);
mockMvc.perform(get("/auth/check-account")
.param("account", "nonExistingUser"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value(false));
}
@Test
public void testCheckEmailExists() throws Exception {
// 模拟邮箱存在的情况
when(authService.existsByEmail("existing@example.com")).thenReturn(true);
mockMvc.perform(get("/auth/check-email")
.param("email", "existing@example.com"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value(true));
}
@Test
public void testCheckEmailNotExists() throws Exception {
// 模拟邮箱不存在的情况
when(authService.existsByEmail("nonexisting@example.com")).thenReturn(false);
mockMvc.perform(get("/auth/check-email")
.param("email", "nonexisting@example.com"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value(false));
}
@Test
public void testCheckPhoneExists() throws Exception {
// 模拟手机号存在的情况
when(authService.existsByPhone("13800138000")).thenReturn(true);
mockMvc.perform(get("/auth/check-phone")
.param("phone", "13800138000"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value(true));
}
@Test
public void testCheckPhoneNotExists() throws Exception {
// 模拟手机号不存在的情况
when(authService.existsByPhone("13900139000")).thenReturn(false);
mockMvc.perform(get("/auth/check-phone")
.param("phone", "13900139000"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value(false));
}
}
+727
View File
@@ -0,0 +1,727 @@
# Java代码规范
## 1. 命名规范
### 1.1 包命名
- 包名全部小写,使用点分隔
- 示例:`com.emotionmuseum.user.controller`
### 1.2 类命名
- 使用大驼峰命名法(PascalCase
- 示例:`UserController``UserService`
### 1.3 方法命名
- 使用小驼峰命名法(camelCase)
- 示例:`getUserById``createUser`
### 1.4 变量命名
- 使用小驼峰命名法(camelCase)
- 常量使用全大写+下划线(UPPER_SNAKE_CASE
- 示例:`userName``MAX_RETRY_COUNT`
## 2. 导入包规范
### 2.1 导入方式规范
**重要原则:必须使用常规的import方式导入类,严禁在代码中使用全路径限定名方式导入类。**
#### 2.1.1 正确的导入方式
```java
// ✅ 正确示例:使用import语句导入类
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.emotionmuseum.dto.request.CreateUserRequest;
import com.emotionmuseum.dto.response.UserResponse;
import com.emotionmuseum.service.UserService;
import com.emotionmuseum.common.result.Result;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Slf4j
public class UserController {
private final UserService userService;
@PostMapping
public Result<UserResponse> createUser(@RequestBody CreateUserRequest request) {
UserResponse response = userService.createUser(request);
return Result.success(response);
}
}
```
#### 2.1.2 错误的导入方式
```java
// ❌ 错误示例:在代码中使用全路径限定名
public class UserController {
@org.springframework.beans.factory.annotation.Autowired
private com.emotionmuseum.service.UserService userService;
@org.springframework.web.bind.annotation.PostMapping
public com.emotionmuseum.common.result.Result<com.emotionmuseum.dto.response.UserResponse>
createUser(@org.springframework.web.bind.annotation.RequestBody
com.emotionmuseum.dto.request.CreateUserRequest request) {
com.emotionmuseum.dto.response.UserResponse response = userService.createUser(request);
return com.emotionmuseum.common.result.Result.success(response);
}
}
```
### 2.2 导入顺序规范
```java
// 1. Java标准库导入
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
// 2. 第三方库导入(按字母顺序)
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
// 3. 项目内部导入(按包层次顺序)
import com.emotionmuseum.common.result.Result;
import com.emotionmuseum.dto.request.CreateUserRequest;
import com.emotionmuseum.dto.response.UserResponse;
import com.emotionmuseum.service.UserService;
```
### 2.3 导入优化规范
```java
// ✅ 推荐:使用通配符导入相关的类
import org.springframework.web.bind.annotation.*;
// ✅ 推荐:分别导入不同包的类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
// ❌ 不推荐:过度使用通配符导入
import org.springframework.*;
// ❌ 不推荐:导入未使用的类
import java.util.ArrayList; // 如果代码中没有使用ArrayList
```
### 2.4 静态导入规范
```java
// ✅ 正确:静态导入常用的静态方法
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
// ✅ 正确:静态导入项目中的工具类方法
import static com.emotionmuseum.common.util.DateUtil.formatDateTime;
import static com.emotionmuseum.common.util.StringUtil.isBlank;
// ❌ 错误:避免过度使用静态导入
import static java.lang.Math.*;
import static java.util.Collections.*;
```
### 2.5 导入冲突处理
```java
// 当出现类名冲突时,使用import别名或明确指定包名
import com.emotionmuseum.dto.response.Result;
import org.springframework.http.ResponseEntity;
public class UserController {
// 使用别名避免冲突
public com.emotionmuseum.dto.response.Result<UserResponse> createUser() {
// 业务逻辑
}
// 或者使用import别名(如果IDE支持)
// import com.emotionmuseum.dto.response.Result as EmotionResult;
}
```
## 3. 代码结构规范
### 3.1 类结构顺序
```java
public class ExampleClass {
// 1. 静态常量
public static final String CONSTANT = "value";
// 2. 实例变量
private String instanceVariable;
// 3. 构造方法
public ExampleClass() {}
// 4. 公共方法
public void publicMethod() {}
// 5. 私有方法
private void privateMethod() {}
}
```
### 3.2 方法结构
```java
public Result<UserResponse> getUserById(Long userId) {
// 1. 参数校验
if (userId == null) {
throw new IllegalArgumentException("用户ID不能为空");
}
// 2. 业务逻辑
User user = userService.getById(userId);
// 3. 数据转换
UserResponse response = UserConverter.toResponse(user);
// 4. 返回结果
return Result.success(response);
}
```
## 4. 注释规范
### 4.1 类注释
```java
/**
* 用户控制器
*
* @author 作者名
* @since 1.0.0
*/
@RestController
public class UserController {
}
```
### 4.2 方法注释
```java
/**
* 根据用户ID获取用户信息
*
* @param userId 用户ID
* @return 用户信息响应对象
* @throws UserNotFoundException 当用户不存在时抛出
*/
public Result<UserResponse> getUserById(Long userId) {
}
```
## 5. 异常处理规范
### 5.1 异常定义
```java
public class UserNotFoundException extends BusinessException {
public UserNotFoundException(String message) {
super(message);
}
}
```
### 5.2 异常抛出
```java
if (user == null) {
throw new UserNotFoundException("用户不存在,ID: " + userId);
}
```
## 6. Log4j2日志规范
### 6.1 日志级别使用
```java
// FATAL: 致命错误,系统无法继续运行
logger.fatal("系统严重错误,无法继续运行", e);
// ERROR: 系统错误
logger.error("数据库连接失败", e);
// WARN: 警告信息
logger.warn("用户{}尝试访问未授权资源", userId);
// INFO: 重要业务信息
logger.info("用户{}登录成功", username);
// DEBUG: 调试信息
logger.debug("查询用户参数: {}", queryParams);
// TRACE: 详细调试信息
logger.trace("进入方法: {}", methodName);
```
### 6.2 日志记录规范
```java
// 使用占位符,避免字符串拼接
logger.info("用户{}在{}登录成功", username, LocalDateTime.now());
// 异常日志记录
try {
// 业务逻辑
} catch (Exception e) {
logger.error("处理用户请求失败,用户ID: {}, 错误信息: {}", userId, e.getMessage(), e);
throw new BusinessException("处理失败");
}
// 性能日志记录
long startTime = System.currentTimeMillis();
// 业务逻辑
long endTime = System.currentTimeMillis();
logger.info("查询用户信息耗时: {}ms", endTime - startTime);
```
### 6.3 MDC日志追踪
```java
// 设置MDC信息
MDC.put("userId", userId.toString());
MDC.put("requestId", UUID.randomUUID().toString());
try {
// 业务逻辑
} finally {
// 清理MDC
MDC.clear();
}
```
## 7. 数据库操作规范
### 7.1 MyBatis-Plus使用
```java
// 查询单个
User user = userMapper.selectById(userId);
// 条件查询
List<User> users = userMapper.selectList(
new LambdaQueryWrapper<User>()
.eq(User::getStatus, UserStatus.ACTIVE)
.orderByDesc(User::getCreateTime)
);
```
## 8. 接口设计规范
### 8.1 Controller层规范
**重要原则:Controller层只负责接收请求、参数校验、调用Service层、返回结果,严禁在Controller层编写任何业务逻辑代码。**
```java
@RestController
@RequestMapping("/api/users")
@Validated
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public Result<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
// Controller层只做:参数校验(通过@Valid)、调用Service、返回结果
UserResponse response = userService.createUser(request);
return Result.success(response);
}
@GetMapping("/{id}")
public Result<UserResponse> getUserById(@PathVariable Long id) {
// Controller层只做:调用Service、返回结果
UserResponse response = userService.getUserById(id);
return Result.success(response);
}
@PutMapping("/{id}")
public Result<UserResponse> updateUser(@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
// Controller层只做:参数校验、调用Service、返回结果
UserResponse response = userService.updateUser(id, request);
return Result.success(response);
}
@DeleteMapping("/{id}")
public Result<Void> deleteUser(@PathVariable Long id) {
// Controller层只做:调用Service、返回结果
userService.deleteUser(id);
return Result.success(null);
}
}
```
### 8.2 Controller层禁止事项
```java
// ❌ 错误示例:在Controller层编写业务逻辑
@PostMapping("/users")
public Result<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
// 错误:在Controller层进行业务判断
if (userMapper.selectByUsername(request.getUsername()) != null) {
throw new BusinessException("用户名已存在");
}
// 错误:在Controller层进行数据转换
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
// 错误:在Controller层直接操作数据库
userMapper.insert(user);
// 错误:在Controller层进行复杂的数据处理
UserResponse response = new UserResponse();
response.setId(user.getId());
response.setUsername(user.getUsername());
return Result.success(response);
}
// ✅ 正确示例:Controller层只负责调用Service
@PostMapping("/users")
public Result<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
UserResponse response = userService.createUser(request);
return Result.success(response);
}
```
### 8.3 请求响应对象规范
```java
@Data
public class CreateUserRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
@Data
public class UserResponse {
private Long id;
private String username;
private LocalDateTime createTime;
}
```
### 8.4 Service层业务逻辑规范
**重要原则:所有业务逻辑必须在Service层完成,包括数据校验、业务规则判断、数据处理、事务管理等。**
```java
@Service
@Transactional
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserResponse createUser(CreateUserRequest request) {
// 1. 业务参数校验
validateCreateUserRequest(request);
// 2. 业务规则判断
checkUserExists(request.getUsername());
// 3. 数据转换和业务处理
User user = convertToUser(request);
user.setPassword(passwordEncoder.encode(request.getPassword()));
// 4. 数据持久化
userMapper.insert(user);
// 5. 返回结果转换
return convertToResponse(user);
}
@Override
public UserResponse getUserById(Long id) {
// 1. 参数校验
if (id == null) {
throw new IllegalArgumentException("用户ID不能为空");
}
// 2. 业务逻辑:查询用户
User user = userMapper.selectById(id);
if (user == null) {
throw new UserNotFoundException("用户不存在,ID: " + id);
}
// 3. 返回结果转换
return convertToResponse(user);
}
@Override
public UserResponse updateUser(Long id, UpdateUserRequest request) {
// 1. 参数校验
validateUpdateUserRequest(id, request);
// 2. 业务逻辑:检查用户是否存在
User existingUser = userMapper.selectById(id);
if (existingUser == null) {
throw new UserNotFoundException("用户不存在,ID: " + id);
}
// 3. 业务规则判断
if (StringUtils.hasText(request.getUsername())) {
checkUserExists(request.getUsername(), id);
}
// 4. 数据更新
updateUserFields(existingUser, request);
userMapper.updateById(existingUser);
// 5. 返回结果转换
return convertToResponse(existingUser);
}
@Override
public void deleteUser(Long id) {
// 1. 参数校验
if (id == null) {
throw new IllegalArgumentException("用户ID不能为空");
}
// 2. 业务逻辑:检查用户是否存在
User user = userMapper.selectById(id);
if (user == null) {
throw new UserNotFoundException("用户不存在,ID: " + id);
}
// 3. 业务规则判断:检查是否可以删除
checkUserCanBeDeleted(user);
// 4. 执行删除
userMapper.deleteById(id);
log.info("用户删除成功,ID: {}", id);
}
// 私有方法:业务校验
private void validateCreateUserRequest(CreateUserRequest request) {
if (request == null) {
throw new IllegalArgumentException("请求参数不能为空");
}
// 其他业务校验逻辑
}
private void checkUserExists(String username) {
User existingUser = userMapper.selectByUsername(username);
if (existingUser != null) {
throw new BusinessException("用户名已存在: " + username);
}
}
private void checkUserExists(String username, Long excludeId) {
User existingUser = userMapper.selectByUsernameAndIdNot(username, excludeId);
if (existingUser != null) {
throw new BusinessException("用户名已存在: " + username);
}
}
private void checkUserCanBeDeleted(User user) {
// 检查用户是否可以删除的业务逻辑
if (user.getStatus() == UserStatus.ACTIVE) {
// 检查是否有未完成的订单等
if (hasUnfinishedOrders(user.getId())) {
throw new BusinessException("用户有未完成的订单,无法删除");
}
}
}
// 私有方法:数据转换
private User convertToUser(CreateUserRequest request) {
User user = new User();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setStatus(UserStatus.ACTIVE);
return user;
}
private UserResponse convertToResponse(User user) {
UserResponse response = new UserResponse();
response.setId(user.getId());
response.setUsername(user.getUsername());
response.setEmail(user.getEmail());
response.setStatus(user.getStatus());
response.setCreateTime(user.getCreateTime());
return response;
}
}
```
## 9. 缓存使用规范
### 9.1 Redis缓存注解
```java
@Cacheable(value = "user", key = "#userId")
public UserResponse getUserById(Long userId) {
// 业务逻辑
}
@CacheEvict(value = "user", key = "#userId")
public void updateUser(Long userId, UpdateUserRequest request) {
// 更新逻辑
}
```
## 10. 异步处理规范
### 10.1 异步方法定义
```java
@Async("notificationExecutor")
public CompletableFuture<Void> sendNotificationAsync(Long userId, String message) {
try {
sendNotification(userId, message);
return CompletableFuture.completedFuture(null);
} catch (Exception e) {
logger.error("发送通知失败", e);
return CompletableFuture.failedFuture(e);
}
}
```
## 11. 单元测试规范
### 11.1 测试类结构
```java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserMapper userMapper;
@InjectMocks
private UserServiceImpl userService;
@Test
@DisplayName("根据ID获取用户 - 成功")
void getUserById_Success() {
// Given
Long userId = 1L;
User user = new User();
user.setId(userId);
when(userMapper.selectById(userId)).thenReturn(user);
// When
UserResponse result = userService.getUserById(userId);
// Then
assertThat(result).isNotNull();
assertThat(result.getId()).isEqualTo(userId);
}
}
```
## 12. Spring Security安全规范
### 12.1 安全注解使用
```java
@RestController
@RequestMapping("/api/users")
public class UserController {
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
public Result<List<UserResponse>> getAllUsers() {
// 只有ADMIN角色可以访问
}
@PreAuthorize("hasRole('USER') and #userId == authentication.principal.id")
@GetMapping("/{userId}")
public Result<UserResponse> getUserById(@PathVariable Long userId) {
// 只有USER角色且只能访问自己的信息
}
@PreAuthorize("permitAll()")
@PostMapping("/register")
public Result<UserResponse> register(@Valid @RequestBody CreateUserRequest request) {
// 允许所有人访问
}
}
```
### 12.2 输入验证
```java
@PostMapping("/users")
public Result<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
return userService.createUser(request);
}
```
### 12.3 敏感信息处理
```java
// 日志中脱敏
logger.info("用户{}登录成功", maskUsername(username));
private String maskUsername(String username) {
if (username == null || username.length() <= 2) {
return username;
}
return username.substring(0, 1) + "***" + username.substring(username.length() - 1);
}
```
### 12.4 密码处理规范
```java
@Service
public class UserServiceImpl implements UserService {
@Autowired
private PasswordEncoder passwordEncoder;
public void createUser(CreateUserRequest request) {
// 密码加密存储
String encodedPassword = passwordEncoder.encode(request.getPassword());
user.setPassword(encodedPassword);
// 密码验证
if (passwordEncoder.matches(rawPassword, encodedPassword)) {
// 密码正确
}
}
}
```
### 12.5 JWT Token处理
```java
@Component
public class JwtTokenProvider {
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
```
## 13. 版本控制规范
### 13.1 Git提交规范
```bash
feat(user): 添加用户注册功能
fix(auth): 修复登录验证bug
docs(api): 更新API文档
style(code): 格式化代码
refactor(service): 重构用户服务
```
## 总结
本代码规范涵盖了Java开发中的主要方面,包括命名规范、代码结构、注释规范、异常处理、日志记录、数据库操作、接口设计、缓存使用、异步处理、单元测试、安全规范和版本控制等。遵循这些规范可以确保代码的质量、可维护性和可扩展性。
遵循这些规范可以确保代码的质量、可维护性和可扩展性,提高团队开发效率。
+397
View File
@@ -0,0 +1,397 @@
# SpringBoot单体后端服务技术方案
## 1. 技术选型
### 1.1 核心框架
- **Spring Boot**: 3.2.0 (最新稳定版本)
- **Java版本**: JDK 21 (LTS版本)
- **Spring AI**: 0.8.0 (最新稳定版本)
- **WebSocket**: Spring WebSocket 6.1.0
### 1.2 数据存储
- **数据库**: MySQL 8.0+
- **缓存**: Redis 7.0+
- **ORM框架**: MyBatis-Plus 3.5.4 (最新稳定版本)
### 1.3 其他组件
- **连接池**: HikariCP (Spring Boot默认)
- **JSON处理**: Jackson
- **API文档**: SpringDoc OpenAPI 3
- **安全框架**: Spring Security 6.1.0
- **日志**: Log4j2 + SLF4J
- **测试**: JUnit 5 + Mockito
## 2. 项目架构设计
### 2.1 分层架构
```
src/main/java/com/emotionmuseum/
├── config/ # 配置类
├── controller/ # 控制器层(只负责接收请求、参数校验、调用Service、返回结果)
├── service/ # 服务层(所有业务逻辑都在这里实现)
│ ├── impl/ # 服务实现
├── mapper/ # 数据访问层
├── entity/ # 实体类
├── dto/ # 数据传输对象
│ ├── request/ # 请求对象
│ ├── response/ # 响应对象
├── common/ # 公共组件
│ ├── base/ # 基础类
│ ├── exception/ # 异常处理
│ ├── result/ # 统一返回结果
│ ├── util/ # 工具类
├── websocket/ # WebSocket相关
└── EmotionMuseumApplication.java
```
### 2.2 分层职责规范
- **Controller层**: 只负责接收HTTP请求、参数校验、调用Service层方法、返回HTTP响应
- **Service层**: 负责所有业务逻辑,包括数据校验、业务规则判断、数据处理、事务管理
- **Mapper层**: 只负责数据库操作,不包含业务逻辑
- **Entity层**: 数据库实体类,对应数据库表结构
- **DTO层**: 数据传输对象,用于前后端数据交互
### 2.3 包结构规范
- 按功能模块分包
- 每个模块包含完整的MVC层次
- 公共组件独立包管理
## 3. 核心配置设计
### 3.1 多环境配置
- `application.yml` - 主配置文件
- `application-dev.yml` - 开发环境
- `application-test.yml` - 测试环境
- `application-prod.yml` - 生产环境
### 3.2 异步配置
- 使用`@EnableAsync`启用异步
- 配置自定义线程池
- 支持异步方法调用
### 3.3 数据库配置
- 主从分离支持
- 连接池优化配置
- 事务管理配置
## 4. 代码规范
### 4.1 命名规范
- **类名**: 大驼峰命名法 (PascalCase)
- **方法名**: 小驼峰命名法 (camelCase)
- **常量**: 全大写+下划线 (UPPER_SNAKE_CASE)
- **包名**: 全小写+点分隔 (com.emotionmuseum)
### 4.2 注解规范
- 控制器类使用`@RestController`
- 服务类使用`@Service`
- 数据访问类使用`@Mapper`
- 异步方法使用`@Async`
### 4.3 异常处理规范
- 统一使用全局异常处理器
- 自定义业务异常类
- 异常信息国际化支持
## 5. 核心组件设计
### 5.1 BaseEntity设计
```java
@MappedSuperclass
@Data
public abstract class BaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@TableLogic
private Integer deleted;
}
```
### 5.2 统一返回结果设计
```java
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
private Long timestamp;
public static <T> Result<T> success(T data) {
// 成功返回
}
public static <T> Result<T> error(String message) {
// 错误返回
}
}
```
### 5.3 请求响应封装
- 所有Controller入参使用Request对象封装
- 所有Controller出参使用Response对象封装
- 统一使用Result包装返回结果
### 5.4 分层职责严格规范
**重要原则:严格遵循分层架构,各层职责明确,禁止跨层调用。**
- **Controller层职责**
- 接收HTTP请求
- 参数校验(使用@Valid注解
- 调用Service层方法
- 返回HTTP响应
- **严禁在Controller层编写任何业务逻辑代码**
- **Service层职责**
- 所有业务逻辑实现
- 数据校验和业务规则判断
- 数据处理和转换
- 事务管理
- 调用Mapper层进行数据操作
- 异常处理和日志记录
- **Mapper层职责**
- 数据库CRUD操作
- SQL语句编写
- 数据查询和映射
- **严禁在Mapper层编写业务逻辑**
## 6. Spring AI + Coze集成方案
### 6.1 Coze平台集成
- 使用Spring AI的ChatClient接口
- 配置Coze API密钥和端点
- 实现自定义ChatClient适配器
### 6.2 WebSocket实时通信
- 使用STOMP协议
- 支持一对一和广播消息
- 实现消息持久化
### 6.3 对话管理
- 对话历史记录存储
- 上下文管理
- 用户会话隔离
## 7. 缓存策略
### 7.1 Redis缓存设计
- 用户会话缓存
- 对话历史缓存
- 热点数据缓存
- 分布式锁实现
### 7.2 缓存更新策略
- 写入时更新
- 定时刷新
- 失效策略
## 8. 安全设计
### 8.1 Spring Security认证授权
- **JWT Token认证**: 基于JWT的无状态认证机制
- **角色权限控制**: 基于RBAC的权限模型
- **API访问控制**: 细粒度的接口权限控制
- **密码加密**: 使用BCrypt加密算法
- **会话管理**: 支持无状态和有状态会话
- **CSRF防护**: 跨站请求伪造防护
- **CORS配置**: 跨域资源共享配置
### 8.2 安全组件设计
- **SecurityConfig**: Spring Security主配置类
- **JwtAuthenticationFilter**: JWT认证过滤器
- **JwtTokenProvider**: JWT令牌提供者
- **UserDetailsService**: 用户详情服务
- **AuthenticationEntryPoint**: 认证失败处理
- **AccessDeniedHandler**: 访问拒绝处理
### 8.3 数据安全
- 敏感数据加密
- SQL注入防护
- XSS防护
- 输入验证和过滤
## 9. 性能优化
### 9.1 数据库优化
- 索引优化
- 查询优化
- 分页查询
### 9.2 缓存优化
- 多级缓存
- 缓存预热
- 缓存穿透防护
### 9.3 异步处理
- 消息队列
- 异步任务
- 批量处理
## 10. 监控和日志
### 10.1 应用监控
- 健康检查
- 性能监控
- 业务监控
### 10.2 Log4j2日志管理
- **日志框架**: Log4j2 + SLF4J
- **日志级别**: TRACE, DEBUG, INFO, WARN, ERROR, FATAL
- **日志输出**: 控制台、文件、数据库、远程服务器
- **日志格式**: JSON格式结构化日志
- **日志轮转**: 按大小和时间自动轮转
- **日志过滤**: 基于MDC的日志过滤
- **性能优化**: 异步日志记录
- **日志聚合**: ELK Stack集成支持
### 10.3 日志配置策略
- **开发环境**: 控制台输出,DEBUG级别
- **测试环境**: 文件输出,INFO级别
- **生产环境**: 文件+远程输出,WARN级别
- **安全日志**: 独立的审计日志文件
- **业务日志**: 按模块分离的日志文件
## 11. 部署方案
### 11.1 容器化部署
- Docker镜像构建
- Docker Compose编排
- 环境变量配置
### 11.2 CI/CD流程
- 自动化构建
- 自动化测试
- 自动化部署
## 12. 开发规范
### 12.1 代码提交规范
- 使用Conventional Commits
- 分支管理策略
- 代码审查流程
### 12.2 测试规范
- 单元测试覆盖率 > 80%
- 集成测试
- 端到端测试
### 12.3 文档规范
- API文档自动生成
- 代码注释规范
- 技术文档维护
## 13. 项目依赖管理
### 13.1 Maven依赖
```xml
<properties>
<java.version>21</java.version>
<spring-boot.version>3.2.0</spring-boot.version>
<spring-ai.version>0.8.0</spring-ai.version>
<mybatis-plus.version>3.5.4</mybatis-plus.version>
<spring-security.version>6.1.0</spring-security.version>
<log4j2.version>2.20.0</log4j2.version>
<jwt.version>0.11.5</jwt.version>
</properties>
```
### 13.2 依赖版本管理
- 统一版本管理
- 依赖冲突解决
- 安全漏洞检查
### 13.3 核心依赖配置
```xml
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
```
## 14. 开发工具配置
### 14.1 IDE配置
- IntelliJ IDEA推荐配置
- 代码格式化规则
- 代码检查规则
### 14.2 构建工具配置
- Maven配置优化
- 插件配置
- 构建脚本
## 15. 质量保证
### 15.1 代码质量
- SonarQube代码检查
- 代码规范检查
- 性能检查
### 15.2 测试质量
- 测试用例设计
- 测试数据管理
- 测试环境管理
---
## 总结
本技术方案提供了一个完整的SpringBoot单体后端服务架构,涵盖了从技术选型到部署运维的各个方面。方案注重:
1. **技术先进性**: 使用最新稳定版本的技术栈
2. **架构合理性**: 分层清晰,职责明确
3. **可扩展性**: 支持水平扩展和功能扩展
4. **可维护性**: 统一的代码规范和开发流程
5. **高性能**: 多级缓存和异步处理
6. **高可用**: 完善的监控和异常处理机制
该方案可以作为项目开发的技术指导文档,确保项目的技术实现符合最佳实践。
+764
View File
@@ -0,0 +1,764 @@
# 项目结构示例
## 1. 目录结构
```
src/main/java/com/emotionmuseum/
├── EmotionMuseumApplication.java # 启动类
├── config/ # 配置类
│ ├── AsyncConfig.java # 异步配置
│ ├── MybatisPlusConfig.java # MyBatis-Plus配置
│ ├── RedisConfig.java # Redis配置
│ ├── WebSocketConfig.java # WebSocket配置
│ ├── SecurityConfig.java # Spring Security配置
│ ├── JwtConfig.java # JWT配置
│ └── SwaggerConfig.java # API文档配置
├── controller/ # 控制器层
│ ├── UserController.java # 用户控制器
│ ├── ChatController.java # 聊天控制器
│ └── AuthController.java # 认证控制器
├── service/ # 服务层
│ ├── UserService.java # 用户服务接口
│ ├── ChatService.java # 聊天服务接口
│ ├── AuthService.java # 认证服务接口
│ └── impl/ # 服务实现
│ ├── UserServiceImpl.java
│ ├── ChatServiceImpl.java
│ └── AuthServiceImpl.java
├── mapper/ # 数据访问层
│ ├── UserMapper.java
│ ├── ChatMapper.java
│ └── MessageMapper.java
├── entity/ # 实体类
│ ├── User.java
│ ├── Chat.java
│ ├── Message.java
│ └── BaseEntity.java
├── dto/ # 数据传输对象
│ ├── request/ # 请求对象
│ │ ├── CreateUserRequest.java
│ │ ├── LoginRequest.java
│ │ ├── ChatRequest.java
│ │ └── PageRequest.java
│ └── response/ # 响应对象
│ ├── UserResponse.java
│ ├── LoginResponse.java
│ ├── ChatResponse.java
│ └── PageResult.java
├── common/ # 公共组件
│ ├── base/ # 基础类
│ │ ├── BaseEntity.java
│ │ └── BaseService.java
│ ├── exception/ # 异常处理
│ │ ├── GlobalExceptionHandler.java
│ │ ├── BusinessException.java
│ │ └── UserNotFoundException.java
│ ├── result/ # 统一返回结果
│ │ ├── Result.java
│ │ └── ResultCode.java
│ ├── security/ # 安全相关
│ │ ├── JwtAuthenticationFilter.java # JWT认证过滤器
│ │ ├── JwtTokenProvider.java # JWT令牌提供者
│ │ ├── UserDetailsServiceImpl.java # 用户详情服务实现
│ │ ├── AuthenticationEntryPointImpl.java # 认证失败处理
│ │ └── AccessDeniedHandlerImpl.java # 访问拒绝处理
│ └── util/ # 工具类
│ ├── JwtUtil.java
│ ├── RedisUtil.java
│ └── DateUtil.java
├── websocket/ # WebSocket相关
│ ├── WebSocketHandler.java
│ ├── ChatWebSocketHandler.java
│ └── dto/
│ ├── ChatMessage.java
│ └── WebSocketMessage.java
└── ai/ # AI相关
├── CozeClient.java # Coze客户端
├── ChatClient.java # 聊天客户端
└── config/
└── CozeConfig.java # Coze配置
```
## 3. 核心代码示例
### 3.1 启动类
```java
@SpringBootApplication
@EnableAsync
@EnableCaching
@EnableGlobalMethodSecurity(prePostEnabled = true)
@MapperScan("com.emotionmuseum.mapper")
public class EmotionMuseumApplication {
public static void main(String[] args) {
SpringApplication.run(EmotionMuseumApplication.class, args);
}
}
```
```
## 2. 核心代码示例
### 2.1 启动类
```java
@SpringBootApplication
@EnableAsync
@EnableCaching
@EnableGlobalMethodSecurity(prePostEnabled = true)
@MapperScan("com.emotionmuseum.mapper")
public class EmotionMuseumApplication {
public static void main(String[] args) {
SpringApplication.run(EmotionMuseumApplication.class, args);
}
}
```
### 2.2 BaseEntity
```java
@Data
@MappedSuperclass
public abstract class BaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@TableLogic
private Integer deleted;
}
```
### 2.3 统一返回结果
```java
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
private Long timestamp;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(ResultCode.SUCCESS.getCode());
result.setMessage(ResultCode.SUCCESS.getMessage());
result.setData(data);
result.setTimestamp(System.currentTimeMillis());
return result;
}
public static <T> Result<T> error(String message) {
Result<T> result = new Result<>();
result.setCode(ResultCode.ERROR.getCode());
result.setMessage(message);
result.setTimestamp(System.currentTimeMillis());
return result;
}
}
```
### 2.4 全局异常处理
```java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return Result.error(e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
log.warn("参数验证失败: {}", message);
return Result.error(message);
}
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常", e);
return Result.error("系统异常,请稍后重试");
}
}
```
### 2.5 异步配置
```java
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}
```
### 2.6 Coze客户端
```java
@Component
@Slf4j
public class CozeClient {
@Autowired
private RestTemplate restTemplate;
@Value("${coze.api.url}")
private String cozeApiUrl;
@Value("${coze.api.key}")
private String cozeApiKey;
public String chat(String message, String sessionId) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + cozeApiKey);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("message", message);
requestBody.put("session_id", sessionId);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(
cozeApiUrl + "/chat", request, Map.class);
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
return (String) response.getBody().get("response");
}
throw new BusinessException("调用Coze API失败");
} catch (Exception e) {
log.error("调用Coze API异常", e);
throw new BusinessException("AI服务暂时不可用");
}
}
}
```
### 2.7 WebSocket配置
```java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new ChatWebSocketHandler(), "/ws/chat")
.setAllowedOrigins("*");
}
}
```
### 2.8 Spring Security配置
```java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthFilter;
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/ws/**").permitAll()
.anyRequest().authenticated()
);
http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
```
### 2.9 JWT认证过滤器
```java
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
String username = jwtTokenProvider.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
log.error("无法设置用户认证: {}", e.getMessage());
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
```
### 2.10 聊天控制器
```java
@RestController
@RequestMapping("/api/chat")
@Validated
@Slf4j
public class ChatController {
@Autowired
private ChatService chatService;
@PreAuthorize("hasRole('USER')")
@PostMapping("/send")
public Result<ChatResponse> sendMessage(@Valid @RequestBody ChatRequest request) {
// Controller层只负责:参数校验、调用Service、返回结果
ChatResponse response = chatService.sendMessage(request);
return Result.success(response);
}
@PreAuthorize("hasRole('USER')")
@GetMapping("/history")
public Result<PageResult<ChatResponse>> getChatHistory(
@Valid PageRequest pageRequest,
@RequestParam(required = false) String sessionId) {
// Controller层只负责:参数校验、调用Service、返回结果
PageResult<ChatResponse> result = chatService.getChatHistory(pageRequest, sessionId);
return Result.success(result);
}
}
```
### 2.11 聊天服务实现
```java
@Service
@Transactional
@Slf4j
public class ChatServiceImpl implements ChatService {
@Autowired
private ChatMapper chatMapper;
@Autowired
private CozeClient cozeClient;
@Override
public ChatResponse sendMessage(ChatRequest request) {
// 1. 业务参数校验
validateChatRequest(request);
// 2. 业务逻辑:调用AI服务
String aiResponse = cozeClient.chat(request.getMessage(), request.getSessionId());
// 3. 业务逻辑:保存聊天记录
Chat chat = new Chat();
chat.setUserId(getCurrentUserId());
chat.setMessage(request.getMessage());
chat.setResponse(aiResponse);
chat.setSessionId(request.getSessionId());
chat.setCreateTime(LocalDateTime.now());
chatMapper.insert(chat);
// 4. 返回结果转换
return convertToChatResponse(chat);
}
@Override
public PageResult<ChatResponse> getChatHistory(PageRequest pageRequest, String sessionId) {
// 1. 业务逻辑:查询聊天历史
Page<Chat> page = new Page<>(pageRequest.getPageNum(), pageRequest.getPageSize());
LambdaQueryWrapper<Chat> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Chat::getUserId, getCurrentUserId());
if (StringUtils.hasText(sessionId)) {
wrapper.eq(Chat::getSessionId, sessionId);
}
wrapper.orderByDesc(Chat::getCreateTime);
Page<Chat> chatPage = chatMapper.selectPage(page, wrapper);
// 2. 数据转换
List<ChatResponse> responses = chatPage.getRecords().stream()
.map(this::convertToChatResponse)
.collect(Collectors.toList());
// 3. 返回分页结果
return new PageResult<>(responses, chatPage.getTotal(), pageRequest.getPageNum(), pageRequest.getPageSize());
}
// 私有方法:业务校验
private void validateChatRequest(ChatRequest request) {
if (request == null) {
throw new IllegalArgumentException("请求参数不能为空");
}
if (!StringUtils.hasText(request.getMessage())) {
throw new IllegalArgumentException("消息内容不能为空");
}
if (request.getMessage().length() > 1000) {
throw new IllegalArgumentException("消息内容长度不能超过1000字符");
}
}
// 私有方法:获取当前用户ID
private Long getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof UserDetails) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// 根据用户名获取用户ID的逻辑
return getUserService().getUserIdByUsername(userDetails.getUsername());
}
throw new UnauthorizedException("用户未登录");
}
// 私有方法:数据转换
private ChatResponse convertToChatResponse(Chat chat) {
ChatResponse response = new ChatResponse();
response.setId(chat.getId());
response.setMessage(chat.getMessage());
response.setResponse(chat.getResponse());
response.setSessionId(chat.getSessionId());
response.setCreateTime(chat.getCreateTime());
return response;
}
}
```
## 3. 配置文件示例
### 3.1 application.yml
```yaml
spring:
profiles:
active: dev
application:
name: emotion-museum
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: password
hikari:
maximum-pool-size: 20
minimum-idle: 5
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 3000ms
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# JWT配置
jwt:
secret: your-secret-key-here-must-be-very-long-and-secure
expiration: 86400000 # 24小时
coze:
api:
url: https://api.coze.com
key: your-coze-api-key
# Log4j2配置
logging:
config: classpath:log4j2-spring.xml
```
### 3.2 application-dev.yml
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/emotion_museum_dev?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
redis:
host: localhost
port: 6379
# 开发环境JWT配置
jwt:
secret: dev-secret-key-not-for-production
expiration: 86400000
```
### 3.3 log4j2-spring.xml
```xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
<Properties>
<Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Property>
<Property name="LOG_FILE_PATH">logs</Property>
</Properties>
<Appenders>
<!-- 控制台输出 -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_PATTERN}"/>
</Console>
<!-- 文件输出 -->
<RollingFile name="FileAppender" fileName="${LOG_FILE_PATH}/app.log"
filePattern="${LOG_FILE_PATH}/app-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<TimeBasedTriggeringPolicy />
<SizeBasedTriggeringPolicy size="100MB"/>
</Policies>
<DefaultRolloverStrategy max="10"/>
</RollingFile>
<!-- 错误日志文件 -->
<RollingFile name="ErrorFileAppender" fileName="${LOG_FILE_PATH}/error.log"
filePattern="${LOG_FILE_PATH}/error-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<TimeBasedTriggeringPolicy />
<SizeBasedTriggeringPolicy size="100MB"/>
</Policies>
<DefaultRolloverStrategy max="10"/>
</RollingFile>
<!-- 安全日志文件 -->
<RollingFile name="SecurityFileAppender" fileName="${LOG_FILE_PATH}/security.log"
filePattern="${LOG_FILE_PATH}/security-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<TimeBasedTriggeringPolicy />
<SizeBasedTriggeringPolicy size="100MB"/>
</Policies>
<DefaultRolloverStrategy max="10"/>
</RollingFile>
</Appenders>
<Loggers>
<!-- 应用日志 -->
<Logger name="com.emotionmuseum" level="debug" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="FileAppender"/>
</Logger>
<!-- 安全日志 -->
<Logger name="com.emotionmuseum.common.security" level="info" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="SecurityFileAppender"/>
</Logger>
<!-- 错误日志 -->
<Logger name="com.emotionmuseum" level="error" additivity="false">
<AppenderRef ref="ErrorFileAppender"/>
</Logger>
<!-- Spring Security日志 -->
<Logger name="org.springframework.security" level="debug" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="SecurityFileAppender"/>
</Logger>
<!-- 根日志器 -->
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="FileAppender"/>
</Root>
</Loggers>
</Configuration>
```
## 4. Maven依赖
### 4.1 pom.xml核心依赖
```xml
<properties>
<java.version>21</java.version>
<spring-boot.version>3.2.0</spring-boot.version>
<spring-ai.version>0.8.0</spring-ai.version>
<mybatis-plus.version>3.5.4</mybatis-plus.version>
<mysql.version>8.0.33</mysql.version>
<redis.version>3.2.0</redis.version>
<spring-security.version>6.1.0</spring-security.version>
<log4j2.version>2.20.0</log4j2.version>
<jwt.version>0.11.5</jwt.version>
</properties>
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- Spring AI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
```
这个项目结构示例提供了一个完整的SpringBoot单体后端服务的基础框架,包含了您要求的所有技术组件:Spring AI、WebSocket、Redis、MySQL、MyBatis-Plus、Spring Security、JWT认证、Log4j2日志、异步处理、全局异常处理、统一返回结果等。该框架具有以下特点:
1. **安全性**: 集成Spring Security和JWT,提供完整的认证授权机制
2. **可观测性**: 使用Log4j2提供结构化日志记录和日志分级管理
3. **高性能**: 支持异步处理、缓存和数据库优化
4. **可扩展性**: 模块化设计,易于扩展和维护
5. **标准化**: 统一的代码规范和异常处理机制
6. **分层清晰**: 严格遵循分层架构,Controller层只负责请求处理,所有业务逻辑都在Service层实现