feat: 完成情绪博物馆项目重构和功能增强 - 新增日记评论和帖子功能 - 重构前端架构,优化用户体验 - 完善WebSocket通信机制 - 更新项目文档和部署配置
This commit is contained in:
@@ -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.id,user_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));
|
||||
}
|
||||
}
|
||||
@@ -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开发中的主要方面,包括命名规范、代码结构、注释规范、异常处理、日志记录、数据库操作、接口设计、缓存使用、异步处理、单元测试、安全规范和版本控制等。遵循这些规范可以确保代码的质量、可维护性和可扩展性。
|
||||
遵循这些规范可以确保代码的质量、可维护性和可扩展性,提高团队开发效率。
|
||||
@@ -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. **高可用**: 完善的监控和异常处理机制
|
||||
|
||||
该方案可以作为项目开发的技术指导文档,确保项目的技术实现符合最佳实践。
|
||||
@@ -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层实现
|
||||
Reference in New Issue
Block a user