创作中心模块包含首页展示、个人中心、帖子审核。
“首页展示”支持广告轮播展示、推广帖子优先展示、分页显示所有帖子、导航栏便捷标签筛选帖子、全局标题模糊搜索帖子、点击帖子“查看更多”进入帖子详情页。帖子详情页展示帖子封面图片、作者时间、详细内容(可以插入种子链接对种子进行介绍与推广)等基本信息、对帖子点赞收藏举报评论回复、查看相关推荐帖子。相关推荐会推荐当前帖子作者的其他帖子(最多推荐5篇),还会推荐具有相似标签的其他帖子,两者总共最多推荐9篇帖子。
“个人中心”包含“我的中心”和“我的收藏”。
“我的中心”中可以管理已经成功发布的帖子(编辑、删除帖子),还可以发布新帖子。发布新帖子时除了填写帖子基本信息以外,帖子标签支持下拉多项选择,用户还可以选择帖子推广项目并进行支付。设置了多种推广项目,包含广告轮播推广、帖子置顶展示、限时优先展示、分类页首条展示。系统后台执行自动定时任务,每小时对帖子的推广时效性进行检查,如超出推广时限,则取消帖子的推广显示特权。用户点击发布帖子后帖子处于待审核状态,需要管理员审核通过才能正常发布在首页展示页面。编辑帖子时用户可以追加帖子推广,但如果帖子处于推广状态,则禁止修改推广项目。
“我的收藏”中可以便捷查看所有已收藏的帖子。
“帖子审核”包含“帖子发布管理”和“帖子举报管理”。“帖子审核”板块具有权限管理,只有管理员界面能够进入。
“帖子发布管理”对所有待审核帖子进行处理,支持预览待审核帖子详细内容,批准通过和拒绝通过选项。
“帖子举报管理”对所有用户的举报请求进行人工审核,如果举报内容属实,则将帖子下架处理,如果举报内容不属实,驳回举报请求。所有举报请求的处理结果均留存显示,方便后续再次审查。+ bugfix
Change-Id: Iafa37f603aed3337484a3fc96d1cc70b83e8df0c
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/controller/PostCenterController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/controller/PostCenterController.java
new file mode 100644
index 0000000..e05e55e
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/controller/PostCenterController.java
@@ -0,0 +1,1133 @@
+package com.ruoyi.web.controller.post.controller;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.math.BigDecimal;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.github.pagehelper.PageHelper;
+import com.ruoyi.common.annotation.Anonymous;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.web.controller.post.domain.Post;
+import com.ruoyi.web.controller.post.domain.PostComment;
+import com.ruoyi.web.controller.post.domain.PostFavorite;
+import com.ruoyi.web.controller.post.domain.PostTag;
+import com.ruoyi.web.controller.post.domain.PostPayment;
+import com.ruoyi.web.controller.post.domain.PostReport;
+import com.ruoyi.web.controller.post.service.IPostCommentService;
+import com.ruoyi.web.controller.post.service.IPostFavoriteService;
+import com.ruoyi.web.controller.post.service.IPostService;
+import com.ruoyi.web.controller.post.service.IPostTagService;
+import com.ruoyi.web.controller.post.mapper.PostPaymentMapper;
+import com.ruoyi.web.controller.post.mapper.PostReportMapper;
+
+/**
+ * 帖子中心控制器,面向前端用户
+ *
+ * @author thunderhub
+ */
+@RestController
+@RequestMapping("/post-center")
+public class PostCenterController extends BaseController
+{
+ @Autowired
+ private IPostService postService;
+
+ @Autowired
+ private IPostTagService postTagService;
+
+ @Autowired
+ private IPostCommentService postCommentService;
+
+ @Autowired
+ private IPostFavoriteService postFavoriteService;
+
+ @Autowired
+ private PostPaymentMapper postPaymentMapper;
+
+ @Autowired
+ private PostReportMapper postReportMapper;
+
+ /**
+ * 获取帖子列表
+ */
+ @Anonymous
+ @GetMapping("/list")
+ public TableDataInfo list(Post post,
+ @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
+ @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
+ @RequestParam(value = "tags", required = false) String tags)
+ {
+ // 手动设置分页参数 - 确保分页参数正确传递
+ PageHelper.startPage(pageNum, pageSize);
+
+ // 只查询状态为正常的帖子
+ if (post.getStatus() == null) {
+ post.setStatus("1");
+ }
+
+ // 如果有标签筛选参数,设置到查询条件中
+ if (tags != null && !tags.trim().isEmpty() && !"all".equals(tags)) {
+ post.setTags(tags);
+ }
+
+ List<Post> list = postService.selectPostList(post);
+ return getDataTable(list);
+ }
+
+ /**
+ * 获取推广帖子列表(用于轮播展示)
+ */
+ @Anonymous
+ @GetMapping("/promotion")
+ public AjaxResult getPromotionPosts()
+ {
+ Post query = new Post();
+ query.setStatus("1"); // 只查询已发布的帖子
+
+ List<Post> allPosts = postService.selectPostList(query);
+
+ // 筛选出购买了最高等级推广(promotionPlanId = 1)的帖子用于轮播
+ List<Post> carouselPosts = allPosts.stream()
+ .filter(post -> post.getPromotionPlanId() != null && post.getPromotionPlanId().equals(1L))
+ .limit(5) // 限制轮播数量
+ .collect(java.util.stream.Collectors.toList());
+
+ return success(carouselPosts);
+ }
+
+ /**
+ * 获取帖子详情
+ */
+ @Anonymous
+ @GetMapping("/{postId}")
+ public AjaxResult getDetail(@PathVariable Long postId)
+ {
+ Map<String, Object> result = new HashMap<>();
+
+ // 更新浏览量
+ postService.updatePostViews(postId);
+
+ // 获取帖子信息
+ Post post = postService.selectPostById(postId);
+ if (post == null || !"1".equals(post.getStatus())) {
+ return error("帖子不存在或已下架");
+ }
+ result.put("post", post);
+
+ // 获取帖子标签
+ List<PostTag> tags = postTagService.selectPostTagsByPostId(postId);
+ result.put("tags", tags);
+
+ // 获取评论,只返回顶级评论,限制10条
+ List<PostComment> comments = postCommentService.selectCommentsByPostId(postId, 10);
+
+ // 获取每个评论的回复
+ List<Map<String, Object>> commentList = new ArrayList<>();
+ for (PostComment comment : comments) {
+ if ("1".equals(comment.getStatus())) { // 只返回正常状态的评论
+ Map<String, Object> commentMap = new HashMap<>();
+ commentMap.put("comment", comment);
+
+ List<PostComment> replies = postCommentService.selectCommentsByParentId(comment.getCommentId());
+ // 过滤回复,只返回正常状态的回复
+ List<PostComment> validReplies = new ArrayList<>();
+ for (PostComment reply : replies) {
+ if ("1".equals(reply.getStatus())) {
+ validReplies.add(reply);
+ }
+ }
+ commentMap.put("replies", validReplies);
+ commentList.add(commentMap);
+ }
+ }
+ result.put("comments", commentList);
+
+ // 获取推荐帖子(最多9个)
+ List<Post> recommendedPosts = postService.getRecommendedPosts(post.getAuthorId(), post.getTags(), postId, 9);
+ result.put("recommendedPosts", recommendedPosts);
+
+ // 判断当前用户是否已收藏
+ Long userId = null;
+ try {
+ userId = getUserId();
+ } catch (Exception e) {
+ // 用户未登录,忽略异常
+ }
+
+ if (userId != null) {
+ PostFavorite favorite = postFavoriteService.selectPostFavoriteByPostIdAndUserId(postId, userId);
+ result.put("favorited", favorite != null && "0".equals(favorite.getStatus()));
+ } else {
+ result.put("favorited", false);
+ }
+
+ return success(result);
+ }
+
+ /**
+ * 添加评论
+ */
+ @PostMapping("/comment")
+ public AjaxResult addComment(@RequestBody PostComment postComment)
+ {
+ if (postComment.getPostId() == null) {
+ return error("帖子ID不能为空");
+ }
+
+ if (postComment.getContent() == null || postComment.getContent().trim().isEmpty()) {
+ return error("评论内容不能为空");
+ }
+
+ // 检查用户是否登录
+ Long userId = null;
+ String username = null;
+ try {
+ userId = getUserId();
+ username = getUsername();
+ } catch (Exception e) {
+ return error("请先登录后再评论");
+ }
+
+ if (userId == null) {
+ return error("请先登录后再评论");
+ }
+
+ postComment.setCreateBy(username);
+ postComment.setUserId(userId);
+ postComment.setUserName(username);
+ postComment.setLikes(0L);
+ postComment.setStatus("1"); // 默认正常状态
+
+ // 如果父评论ID为空,设置为0(顶级评论)
+ if (postComment.getParentId() == null) {
+ postComment.setParentId(0L);
+ }
+
+ int result = postCommentService.insertPostComment(postComment);
+
+ if (result > 0) {
+ // 更新帖子评论数
+ postService.updatePostComments(postComment.getPostId(), 1);
+
+ // 返回新添加的评论,包括评论ID
+ return success(postCommentService.selectPostCommentById(postComment.getCommentId()));
+ } else {
+ return error("添加评论失败");
+ }
+ }
+
+ /**
+ * 收藏/取消收藏帖子
+ */
+ @PostMapping("/favorite/{postId}")
+ public AjaxResult toggleFavorite(@PathVariable Long postId, @RequestParam(required = false, defaultValue = "true") boolean favorite)
+ {
+ Long userId = null;
+ try {
+ userId = getUserId();
+ } catch (Exception e) {
+ // 用户未登录
+ return error("请先登录");
+ }
+
+ if (userId == null) {
+ return error("请先登录");
+ }
+
+ // 检查帖子是否存在
+ Post post = postService.selectPostById(postId);
+ if (post == null || !"1".equals(post.getStatus())) {
+ return error("帖子不存在或已下架");
+ }
+
+ // 检查是否已收藏
+ PostFavorite exist = postFavoriteService.selectPostFavoriteByPostIdAndUserId(postId, userId);
+
+ Map<String, Object> resultData = new HashMap<>();
+ resultData.put("favorited", favorite);
+
+ if (favorite) {
+ // 收藏操作
+ if (exist != null) {
+ // 已存在记录,检查状态
+ if ("0".equals(exist.getStatus())) {
+ return AjaxResult.success("已收藏该帖子");
+ } else {
+ // 取消收藏状态,改为收藏状态
+ exist.setStatus("0");
+ exist.setUpdateBy(getUsername());
+ postFavoriteService.updatePostFavorite(exist);
+
+ // 更新帖子收藏数量
+ postService.updatePostFavorites(postId, 1);
+
+ return AjaxResult.success("收藏成功");
+ }
+ } else {
+ // 创建收藏记录
+ PostFavorite favoriteRecord = new PostFavorite();
+ favoriteRecord.setPostId(postId);
+ favoriteRecord.setUserId(userId);
+ favoriteRecord.setPostTitle(post.getTitle());
+ favoriteRecord.setPostCover(post.getCoverImage());
+ favoriteRecord.setStatus("0");
+ favoriteRecord.setCreateBy(getUsername());
+ postFavoriteService.insertPostFavorite(favoriteRecord);
+
+ // 更新帖子收藏数量
+ postService.updatePostFavorites(postId, 1);
+
+ return AjaxResult.success("收藏成功");
+ }
+ } else {
+ // 取消收藏操作
+ if (exist != null && "0".equals(exist.getStatus())) {
+ // 更新为取消收藏状态
+ exist.setStatus("1");
+ exist.setUpdateBy(getUsername());
+ postFavoriteService.updatePostFavorite(exist);
+
+ // 更新帖子收藏数量
+ postService.updatePostFavorites(postId, -1);
+
+ return AjaxResult.success("取消收藏成功");
+ } else {
+ return AjaxResult.success("未收藏该帖子");
+ }
+ }
+ }
+
+ /**
+ * 获取热门标签
+ */
+ @Anonymous
+ @GetMapping("/tags/hot")
+ public AjaxResult getHotTags(@RequestParam(required = false, defaultValue = "10") int limit)
+ {
+ // 获取帖子数量最多的标签
+ PostTag query = new PostTag();
+ query.setStatus("0"); // 正常状态
+
+ // 使用startPage方法
+ startPage();
+
+ // 手动限制结果数量
+ List<PostTag> allTags = postTagService.selectPostTagList(query);
+ List<PostTag> sortedTags = allTags.stream()
+ .sorted((a, b) -> b.getPostCount().compareTo(a.getPostCount()))
+ .limit(limit)
+ .collect(java.util.stream.Collectors.toList());
+
+ return AjaxResult.success(sortedTags);
+ }
+
+ /**
+ * 根据标签获取帖子
+ */
+ @Anonymous
+ @GetMapping("/bytag/{tagId}")
+ public TableDataInfo getPostsByTag(@PathVariable Long tagId)
+ {
+ startPage();
+ // 查询该标签的所有帖子
+ // 假设已经实现了这个方法
+ // 实际情况下,可能需要通过关联表查询
+ PostTag tag = postTagService.selectPostTagById(tagId);
+
+ if (tag == null) {
+ return getDataTable(new ArrayList<>());
+ }
+
+ Post query = new Post();
+ query.setTags(tag.getTagName());
+ query.setStatus("1"); // 只查询正常状态的帖子
+ List<Post> posts = postService.selectPostList(query);
+
+ return getDataTable(posts);
+ }
+
+ /**
+ * 发布新帖子
+ */
+ @PostMapping("/publish")
+ public AjaxResult publishPost(@RequestBody Post post)
+ {
+ // 检查用户是否登录
+ Long userId = null;
+ String username = null;
+ try {
+ userId = getUserId();
+ username = getUsername();
+ } catch (Exception e) {
+ return error("请先登录后再发布帖子");
+ }
+
+ if (userId == null) {
+ return error("请先登录后再发布帖子");
+ }
+
+ // 验证必填字段
+ if (post.getTitle() == null || post.getTitle().trim().isEmpty()) {
+ return error("帖子标题不能为空");
+ }
+
+ if (post.getContent() == null || post.getContent().trim().isEmpty()) {
+ return error("帖子内容不能为空");
+ }
+
+ if (post.getSummary() == null || post.getSummary().trim().isEmpty()) {
+ return error("帖子摘要不能为空");
+ }
+
+ // 设置帖子基本信息
+ post.setAuthorId(userId);
+ post.setAuthor(username);
+ post.setViews(0L);
+ post.setComments(0L);
+ post.setFavorites(0L);
+ post.setLikes(0L);
+ post.setStatus("0"); // 默认待审核状态
+ post.setCreateBy(username);
+
+ // 自动设置发布时间
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ post.setPublishTime(sdf.format(new Date()));
+
+ // 如果没有设置封面图,使用默认图片
+ if (post.getCoverImage() == null || post.getCoverImage().trim().isEmpty()) {
+ post.setCoverImage("/images/default-cover.jpg");
+ }
+
+ // 处理标签
+ if (post.getTags() != null && !post.getTags().trim().isEmpty()) {
+ // 这里可以处理标签逻辑,比如创建标签关联关系
+ // 暂时直接保存标签字符串
+ }
+
+ int result = postService.insertPost(post);
+
+ if (result > 0) {
+ return success("帖子发布成功,等待管理员审核");
+ } else {
+ return error("帖子发布失败");
+ }
+ }
+
+ /**
+ * 获取当前用户的帖子列表
+ */
+ @GetMapping("/my-posts")
+ public TableDataInfo getMyPosts(Post post,
+ @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
+ @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize)
+ {
+ // 检查用户是否登录
+ Long userId = null;
+ try {
+ userId = getUserId();
+ } catch (Exception e) {
+ return getDataTable(new ArrayList<>());
+ }
+
+ if (userId == null) {
+ return getDataTable(new ArrayList<>());
+ }
+
+ // 手动设置分页参数
+ PageHelper.startPage(pageNum, pageSize);
+
+ // 只查询当前用户的帖子
+ post.setAuthorId(userId);
+ List<Post> list = postService.selectPostList(post);
+ return getDataTable(list);
+ }
+
+ /**
+ * 获取当前用户的收藏列表
+ */
+ @GetMapping("/my-favorites")
+ public TableDataInfo getMyFavorites(@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
+ @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize)
+ {
+ // 检查用户是否登录
+ Long userId = null;
+ try {
+ userId = getUserId();
+ } catch (Exception e) {
+ return getDataTable(new ArrayList<>());
+ }
+
+ if (userId == null) {
+ return getDataTable(new ArrayList<>());
+ }
+
+ // 手动设置分页参数
+ PageHelper.startPage(pageNum, pageSize);
+
+ // 查询用户收藏的帖子
+ PostFavorite favoriteQuery = new PostFavorite();
+ favoriteQuery.setUserId(userId);
+ favoriteQuery.setStatus("0"); // 只查询有效收藏
+ List<PostFavorite> favorites = postFavoriteService.selectPostFavoriteList(favoriteQuery);
+
+ // 获取收藏的帖子详情
+ List<Post> favoritePosts = new ArrayList<>();
+ for (PostFavorite favorite : favorites) {
+ Post post = postService.selectPostById(favorite.getPostId());
+ if (post != null && "1".equals(post.getStatus())) {
+ favoritePosts.add(post);
+ }
+ }
+
+ return getDataTable(favoritePosts);
+ }
+
+ /**
+ * 更新帖子
+ */
+ @PutMapping("/update")
+ public AjaxResult updatePost(@RequestBody Post post)
+ {
+ // 检查用户是否登录
+ Long userId = null;
+ String username = null;
+ try {
+ userId = getUserId();
+ username = getUsername();
+ } catch (Exception e) {
+ return error("请先登录");
+ }
+
+ if (userId == null) {
+ return error("请先登录");
+ }
+
+ if (post.getPostId() == null) {
+ return error("帖子ID不能为空");
+ }
+
+ // 检查帖子是否存在且属于当前用户
+ Post existingPost = postService.selectPostById(post.getPostId());
+ if (existingPost == null) {
+ return error("帖子不存在");
+ }
+
+ if (!userId.equals(existingPost.getAuthorId())) {
+ return error("只能编辑自己的帖子");
+ }
+
+ // 验证必填字段
+ if (post.getTitle() == null || post.getTitle().trim().isEmpty()) {
+ return error("帖子标题不能为空");
+ }
+
+ if (post.getContent() == null || post.getContent().trim().isEmpty()) {
+ return error("帖子内容不能为空");
+ }
+
+ if (post.getSummary() == null || post.getSummary().trim().isEmpty()) {
+ return error("帖子摘要不能为空");
+ }
+
+ // 设置更新信息
+ post.setUpdateBy(username);
+
+ int result = postService.updatePost(post);
+
+ if (result > 0) {
+ return success("帖子更新成功");
+ } else {
+ return error("帖子更新失败");
+ }
+ }
+
+ /**
+ * 删除帖子
+ */
+ @DeleteMapping("/delete/{postId}")
+ public AjaxResult deletePost(@PathVariable Long postId)
+ {
+ // 检查用户是否登录
+ Long userId = null;
+ try {
+ userId = getUserId();
+ } catch (Exception e) {
+ return error("请先登录");
+ }
+
+ if (userId == null) {
+ return error("请先登录");
+ }
+
+ // 检查帖子是否存在且属于当前用户
+ Post existingPost = postService.selectPostById(postId);
+ if (existingPost == null) {
+ return error("帖子不存在");
+ }
+
+ if (!userId.equals(existingPost.getAuthorId())) {
+ return error("只能删除自己的帖子");
+ }
+
+ try {
+ // 级联删除相关数据
+ // 1. 删除帖子评论
+ postCommentService.deletePostCommentByPostId(postId);
+
+ // 2. 删除帖子收藏记录
+ postFavoriteService.deletePostFavoriteByPostId(postId);
+
+ // 3. 删除帖子本身
+ int result = postService.deletePostById(postId);
+
+ if (result > 0) {
+ return success("帖子删除成功");
+ } else {
+ return error("帖子删除失败");
+ }
+ } catch (Exception e) {
+ return error("删除失败:" + e.getMessage());
+ }
+ }
+
+ /**
+ * 获取可用标签列表(用于下拉选择)
+ */
+ @Anonymous
+ @GetMapping("/tags/available")
+ public AjaxResult getAvailableTags()
+ {
+ PostTag query = new PostTag();
+ query.setStatus("0"); // 正常状态
+ List<PostTag> tags = postTagService.selectPostTagList(query);
+ return success(tags);
+ }
+
+ /**
+ * 点赞/取消点赞帖子
+ */
+ @PostMapping("/like/{postId}")
+ public AjaxResult toggleLike(@PathVariable Long postId, @RequestParam(required = false, defaultValue = "true") boolean like)
+ {
+ Long userId = null;
+ try {
+ userId = getUserId();
+ } catch (Exception e) {
+ return error("请先登录");
+ }
+
+ if (userId == null) {
+ return error("请先登录");
+ }
+
+ // 检查帖子是否存在
+ Post post = postService.selectPostById(postId);
+ if (post == null || !"1".equals(post.getStatus())) {
+ return error("帖子不存在或已下架");
+ }
+
+ // 这里简化处理,直接更新点赞数
+ // 实际应用中应该记录用户点赞状态,防止重复点赞
+ if (like) {
+ postService.updatePostLikes(postId, 1);
+ return success("点赞成功");
+ } else {
+ postService.updatePostLikes(postId, -1);
+ return success("取消点赞成功");
+ }
+ }
+
+ /**
+ * 点赞/取消点赞评论
+ */
+ @PostMapping("/comment/like/{commentId}")
+ public AjaxResult toggleCommentLike(@PathVariable Long commentId, @RequestParam(required = false, defaultValue = "true") boolean like)
+ {
+ Long userId = null;
+ try {
+ userId = getUserId();
+ } catch (Exception e) {
+ return error("请先登录");
+ }
+
+ if (userId == null) {
+ return error("请先登录");
+ }
+
+ // 检查评论是否存在
+ PostComment comment = postCommentService.selectPostCommentById(commentId);
+ if (comment == null || !"1".equals(comment.getStatus())) {
+ return error("评论不存在或已删除");
+ }
+
+ // 这里简化处理,直接更新点赞数
+ // 实际应用中应该记录用户点赞状态,防止重复点赞
+ if (like) {
+ postCommentService.updateCommentLikes(commentId, 1);
+ return success("点赞成功");
+ } else {
+ postCommentService.updateCommentLikes(commentId, -1);
+ return success("取消点赞成功");
+ }
+ }
+
+ /**
+ * 上传图片
+ */
+ @PostMapping("/upload")
+ public AjaxResult uploadImage(@RequestParam("file") MultipartFile file)
+ {
+ try {
+ if (file.isEmpty()) {
+ return error("上传文件不能为空");
+ }
+
+ // 检查文件类型
+ String originalFilename = file.getOriginalFilename();
+ if (originalFilename == null) {
+ return error("文件名不能为空");
+ }
+
+ String extension = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
+ if (!extension.matches("(?i)\\.(jpg|jpeg|png|gif|bmp|webp)$")) {
+ return error("只支持jpg、jpeg、png、gif、bmp、webp格式的图片文件");
+ }
+
+ // 检查文件大小(限制为5MB)
+ if (file.getSize() > 5 * 1024 * 1024) {
+ return error("文件大小不能超过5MB");
+ }
+
+ // 生成唯一文件名
+ String fileName = System.currentTimeMillis() + "_" +
+ (int)(Math.random() * 1000) + extension;
+
+ // 使用配置文件中的相对路径
+ String frontendImagePath = com.ruoyi.common.config.RuoYiConfig.getFrontendImagePath();
+ if (frontendImagePath == null) {
+ return error("前端图片路径未配置");
+ }
+
+ // 获取项目根目录的绝对路径
+ String projectRoot = System.getProperty("user.dir");
+ String uploadDir = Paths.get(projectRoot, frontendImagePath).toString();
+
+ File uploadDirFile = new File(uploadDir);
+ if (!uploadDirFile.exists()) {
+ uploadDirFile.mkdirs();
+ }
+
+ // 保存文件
+ Path filePath = Paths.get(uploadDir, fileName);
+ Files.copy(file.getInputStream(), filePath);
+
+ // 返回访问URL - 前端可以直接通过/images/访问
+ String imageUrl = "/images/" + fileName;
+
+ Map<String, String> result = new HashMap<>();
+ result.put("url", imageUrl);
+ result.put("filename", fileName);
+ result.put("originalName", originalFilename);
+ result.put("size", String.valueOf(file.getSize()));
+
+ return success(result);
+ } catch (IOException e) {
+ return error("上传失败:" + e.getMessage());
+ } catch (Exception e) {
+ return error("上传失败:" + e.getMessage());
+ }
+ }
+
+ /**
+ * 删除图片
+ */
+ @DeleteMapping("/upload")
+ public AjaxResult deleteImage(@RequestParam("filename") String filename)
+ {
+ try {
+ if (filename == null || filename.trim().isEmpty()) {
+ return error("文件名不能为空");
+ }
+
+ // 使用配置文件中的相对路径
+ String frontendImagePath = com.ruoyi.common.config.RuoYiConfig.getFrontendImagePath();
+ if (frontendImagePath == null) {
+ return error("前端图片路径未配置");
+ }
+
+ // 获取项目根目录的绝对路径
+ String projectRoot = System.getProperty("user.dir");
+ String uploadDir = Paths.get(projectRoot, frontendImagePath).toString();
+ Path filePath = Paths.get(uploadDir, filename);
+
+ // 检查文件是否存在
+ if (Files.exists(filePath)) {
+ Files.delete(filePath);
+ return success("图片删除成功");
+ } else {
+ return error("文件不存在");
+ }
+ } catch (IOException e) {
+ return error("删除失败:" + e.getMessage());
+ } catch (Exception e) {
+ return error("删除失败:" + e.getMessage());
+ }
+ }
+
+ /**
+ * 获取推广计划列表
+ */
+ @Anonymous
+ @GetMapping("/promotion-plans")
+ public AjaxResult getPromotionPlans()
+ {
+ // 这里应该从数据库查询推广计划,暂时返回硬编码数据
+ List<Map<String, Object>> plans = new ArrayList<>();
+
+ Map<String, Object> plan1 = new HashMap<>();
+ plan1.put("id", 1);
+ plan1.put("name", "首页推荐");
+ plan1.put("description", "帖子显示在首页推荐位置");
+ plan1.put("price", 50.00);
+ plan1.put("duration", 7);
+ plans.add(plan1);
+
+ Map<String, Object> plan2 = new HashMap<>();
+ plan2.put("id", 2);
+ plan2.put("name", "置顶推广");
+ plan2.put("description", "帖子在分类页面置顶显示");
+ plan2.put("price", 30.00);
+ plan2.put("duration", 3);
+ plans.add(plan2);
+
+ Map<String, Object> plan3 = new HashMap<>();
+ plan3.put("id", 3);
+ plan3.put("name", "热门推荐");
+ plan3.put("description", "帖子显示在热门推荐区域");
+ plan3.put("price", 80.00);
+ plan3.put("duration", 14);
+ plans.add(plan3);
+
+ Map<String, Object> plan4 = new HashMap<>();
+ plan4.put("id", 4);
+ plan4.put("name", "限时闪推");
+ plan4.put("description", "特定时间段内高优显示");
+ plan4.put("price", 20.00);
+ plan4.put("duration", 1);
+ plans.add(plan4);
+
+ Map<String, Object> plan5 = new HashMap<>();
+ plan5.put("id", 5);
+ plan5.put("name", "分类页首条");
+ plan5.put("description", "在所属分类列表第一条显示(非置顶样式)");
+ plan5.put("price", 40.00);
+ plan5.put("duration", 5);
+ plans.add(plan5);
+
+ return success(plans);
+ }
+
+ /**
+ * 创建支付记录
+ */
+ @PostMapping("/payment")
+ public AjaxResult createPayment(@RequestBody PaymentRequest request)
+ {
+ // 检查用户是否登录
+ Long userId = null;
+ try {
+ userId = getUserId();
+ } catch (Exception e) {
+ return error("请先登录");
+ }
+
+ if (userId == null) {
+ return error("请先登录");
+ }
+
+ // 验证参数
+ if (request.getPostId() == null || request.getPlanId() == null || request.getAmount() == null) {
+ return error("参数不完整");
+ }
+
+ // 检查帖子是否存在且属于当前用户
+ Post post = postService.selectPostById(request.getPostId());
+ if (post == null) {
+ return error("帖子不存在");
+ }
+
+ if (!userId.equals(post.getAuthorId())) {
+ return error("只能为自己的帖子购买推广");
+ }
+
+ // 创建支付记录
+ PostPayment payment = new PostPayment();
+ payment.setPostId(request.getPostId());
+ payment.setPlanId(request.getPlanId());
+ payment.setUserId(userId);
+ payment.setAmount(BigDecimal.valueOf(request.getAmount()));
+ payment.setPaymentStatus("pending"); // 待支付状态
+ payment.setPaymentTime(new Date());
+
+ int result = postPaymentMapper.insertPostPayment(payment);
+
+ if (result > 0) {
+ return success(payment);
+ } else {
+ return error("创建支付记录失败");
+ }
+ }
+
+ /**
+ * 检查帖子推广状态
+ */
+ @GetMapping("/promotion-status/{postId}")
+ public AjaxResult getPromotionStatus(@PathVariable Long postId)
+ {
+ // 检查用户是否登录
+ Long userId = null;
+ try {
+ userId = getUserId();
+ } catch (Exception e) {
+ return error("请先登录");
+ }
+
+ if (userId == null) {
+ return error("请先登录");
+ }
+
+ // 检查帖子是否存在且属于当前用户
+ Post post = postService.selectPostById(postId);
+ if (post == null) {
+ return error("帖子不存在");
+ }
+
+ if (!userId.equals(post.getAuthorId())) {
+ return error("只能查看自己的帖子推广状态");
+ }
+
+ Map<String, Object> result = new HashMap<>();
+ result.put("hasPromotion", post.getPromotionPlanId() != null);
+ result.put("promotionPlanId", post.getPromotionPlanId());
+
+ // 查询最新的支付记录
+ if (post.getPromotionPlanId() != null) {
+ PostPayment latestPayment = postPaymentMapper.selectLatestPaymentByPostId(postId);
+ result.put("latestPayment", latestPayment);
+ }
+
+ return success(result);
+ }
+
+ /**
+ * 确认支付成功
+ */
+ @PostMapping("/payment/confirm/{paymentId}")
+ public AjaxResult confirmPayment(@PathVariable Long paymentId)
+ {
+ // 检查用户是否登录
+ Long userId = null;
+ try {
+ userId = getUserId();
+ } catch (Exception e) {
+ return error("请先登录");
+ }
+
+ if (userId == null) {
+ return error("请先登录");
+ }
+
+ // 查询支付记录
+ PostPayment payment = postPaymentMapper.selectPostPaymentById(paymentId);
+ if (payment == null) {
+ return error("支付记录不存在");
+ }
+
+ if (!userId.equals(payment.getUserId())) {
+ return error("只能确认自己的支付记录");
+ }
+
+ if (!"pending".equals(payment.getPaymentStatus())) {
+ return error("支付记录状态异常");
+ }
+
+ // 更新支付状态为成功
+ payment.setPaymentStatus("paid");
+ payment.setPaymentTime(new Date());
+ postPaymentMapper.updatePostPayment(payment);
+
+ // 更新帖子的推广计划ID
+ Post updatePost = new Post();
+ updatePost.setPostId(payment.getPostId());
+ updatePost.setPromotionPlanId(payment.getPlanId());
+ updatePost.setUpdateBy(getUsername());
+
+ int result = postService.updatePost(updatePost);
+
+ if (result > 0) {
+ return success("支付成功,推广已生效");
+ } else {
+ return error("支付确认失败");
+ }
+ }
+
+ /**
+ * 取消支付
+ */
+ @PostMapping("/payment/cancel/{paymentId}")
+ public AjaxResult cancelPayment(@PathVariable Long paymentId)
+ {
+ // 检查用户是否登录
+ Long userId = null;
+ try {
+ userId = getUserId();
+ } catch (Exception e) {
+ return error("请先登录");
+ }
+
+ if (userId == null) {
+ return error("请先登录");
+ }
+
+ // 查询支付记录
+ PostPayment payment = postPaymentMapper.selectPostPaymentById(paymentId);
+ if (payment == null) {
+ return error("支付记录不存在");
+ }
+
+ if (!userId.equals(payment.getUserId())) {
+ return error("只能取消自己的支付记录");
+ }
+
+ if (!"pending".equals(payment.getPaymentStatus())) {
+ return error("支付记录状态异常");
+ }
+
+ // 更新支付状态为失败
+ payment.setPaymentStatus("failed");
+ postPaymentMapper.updatePostPayment(payment);
+
+ return success("支付已取消");
+ }
+
+ /**
+ * 支付请求类
+ */
+ public static class PaymentRequest {
+ private Long postId;
+ private Long planId;
+ private Double amount;
+
+ public Long getPostId() {
+ return postId;
+ }
+
+ public void setPostId(Long postId) {
+ this.postId = postId;
+ }
+
+ public Long getPlanId() {
+ return planId;
+ }
+
+ public void setPlanId(Long planId) {
+ this.planId = planId;
+ }
+
+ public Double getAmount() {
+ return amount;
+ }
+
+ public void setAmount(Double amount) {
+ this.amount = amount;
+ }
+ }
+
+ /**
+ * 举报帖子
+ */
+ @PostMapping("/report/{postId}")
+ public AjaxResult reportPost(@PathVariable Long postId, @RequestBody ReportRequest request)
+ {
+ // 检查用户是否登录
+ Long userId = null;
+ String username = null;
+ try {
+ userId = getUserId();
+ username = getUsername();
+ } catch (Exception e) {
+ return error("请先登录");
+ }
+
+ if (userId == null) {
+ return error("请先登录");
+ }
+
+ // 检查帖子是否存在
+ Post post = postService.selectPostById(postId);
+ if (post == null || !"1".equals(post.getStatus())) {
+ return error("帖子不存在或已下架");
+ }
+
+ // 检查举报理由
+ if (request.getReason() == null || request.getReason().trim().isEmpty()) {
+ return error("举报理由不能为空");
+ }
+
+ // 创建举报记录
+ PostReport report = new PostReport();
+ report.setPostId(postId);
+ report.setPostTitle(post.getTitle());
+ report.setReportUserId(userId);
+ report.setReportUserName(username);
+ report.setReportReason(request.getReason());
+ report.setStatus("0"); // 待处理状态
+ report.setCreateBy(username);
+
+ int result = postReportMapper.insertPostReport(report);
+
+ if (result > 0) {
+ return success("举报提交成功,我们会尽快处理");
+ } else {
+ return error("举报提交失败");
+ }
+ }
+
+ /**
+ * 举报请求类
+ */
+ public static class ReportRequest {
+ private String reason;
+
+ public String getReason() {
+ return reason;
+ }
+
+ public void setReason(String reason) {
+ this.reason = reason;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/controller/PostCommentController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/controller/PostCommentController.java
new file mode 100644
index 0000000..a3859fa
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/controller/PostCommentController.java
@@ -0,0 +1,167 @@
+package com.ruoyi.web.controller.post.controller;
+
+import java.util.List;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.web.controller.post.domain.PostComment;
+import com.ruoyi.web.controller.post.service.IPostCommentService;
+import com.ruoyi.web.controller.post.service.IPostService;
+
+/**
+ * 帖子评论信息操作处理
+ *
+ * @author thunderhub
+ */
+@RestController
+@RequestMapping("/post/comment")
+public class PostCommentController extends BaseController
+{
+ @Autowired
+ private IPostCommentService postCommentService;
+
+ @Autowired
+ private IPostService postService;
+
+ /**
+ * 获取帖子评论列表
+ */
+ @PreAuthorize("@ss.hasPermi('post:comment:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(PostComment postComment)
+ {
+ startPage();
+ List<PostComment> list = postCommentService.selectPostCommentList(postComment);
+ return getDataTable(list);
+ }
+
+ /**
+ * 导出帖子评论列表
+ */
+ @PreAuthorize("@ss.hasPermi('post:comment:export')")
+ @Log(title = "帖子评论", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, PostComment postComment)
+ {
+ List<PostComment> list = postCommentService.selectPostCommentList(postComment);
+ ExcelUtil<PostComment> util = new ExcelUtil<PostComment>(PostComment.class);
+ util.exportExcel(response, list, "评论数据");
+ }
+
+ /**
+ * 获取帖子评论详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('post:comment:query')")
+ @GetMapping(value = "/{commentId}")
+ public AjaxResult getInfo(@PathVariable Long commentId)
+ {
+ return success(postCommentService.selectPostCommentById(commentId));
+ }
+
+ /**
+ * 新增帖子评论
+ */
+ @PreAuthorize("@ss.hasPermi('post:comment:add')")
+ @Log(title = "帖子评论", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@Validated @RequestBody PostComment postComment)
+ {
+ postComment.setCreateBy(getUsername());
+ postComment.setUserId(getUserId());
+ postComment.setUserName(getUsername());
+ postComment.setLikes(0L);
+
+ // 如果父评论ID为空,设置为0(顶级评论)
+ if (postComment.getParentId() == null) {
+ postComment.setParentId(0L);
+ }
+
+ int result = postCommentService.insertPostComment(postComment);
+
+ if (result > 0) {
+ // 更新帖子评论数
+ postService.updatePostComments(postComment.getPostId(), 1);
+ }
+
+ return toAjax(result);
+ }
+
+ /**
+ * 修改帖子评论
+ */
+ @PreAuthorize("@ss.hasPermi('post:comment:edit')")
+ @Log(title = "帖子评论", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@Validated @RequestBody PostComment postComment)
+ {
+ postComment.setUpdateBy(getUsername());
+ return toAjax(postCommentService.updatePostComment(postComment));
+ }
+
+ /**
+ * 删除帖子评论
+ */
+ @PreAuthorize("@ss.hasPermi('post:comment:remove')")
+ @Log(title = "帖子评论", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{commentIds}")
+ public AjaxResult remove(@PathVariable Long[] commentIds)
+ {
+ // 查询评论信息,用于后续更新帖子评论数
+ for (Long commentId : commentIds) {
+ PostComment comment = postCommentService.selectPostCommentById(commentId);
+ if (comment != null) {
+ // 计算删除的评论和回复总数
+ int count = 1; // 至少有一条评论
+
+ // 如果是顶级评论,还需要计算回复数
+ if (comment.getParentId() == 0) {
+ List<PostComment> replies = postCommentService.selectCommentsByParentId(commentId);
+ count += replies.size();
+ }
+
+ // 更新帖子评论数
+ postService.updatePostComments(comment.getPostId(), -count);
+ }
+ }
+
+ return toAjax(postCommentService.deletePostCommentByIds(commentIds));
+ }
+
+ /**
+ * 获取帖子的评论列表
+ */
+ @PreAuthorize("@ss.hasPermi('post:comment:list')")
+ @GetMapping("/list/{postId}")
+ public AjaxResult listByPostId(@PathVariable Long postId)
+ {
+ List<PostComment> comments = postCommentService.selectCommentsByPostId(postId, 0);
+ return success(comments);
+ }
+
+ /**
+ * 点赞评论
+ */
+ @PreAuthorize("@ss.hasPermi('post:comment:like')")
+ @Log(title = "评论点赞", businessType = BusinessType.UPDATE)
+ @PostMapping("/like/{commentId}")
+ public AjaxResult like(@PathVariable Long commentId)
+ {
+ int result = postCommentService.updateCommentLikes(commentId, 1);
+ return toAjax(result);
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/controller/PostController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/controller/PostController.java
new file mode 100644
index 0000000..e885966
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/controller/PostController.java
@@ -0,0 +1,590 @@
+package com.ruoyi.web.controller.post.controller;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.web.controller.post.domain.Post;
+import com.ruoyi.web.controller.post.domain.PostComment;
+import com.ruoyi.web.controller.post.domain.PostFavorite;
+import com.ruoyi.web.controller.post.domain.PostTag;
+import com.ruoyi.web.controller.post.domain.PostReport;
+import com.ruoyi.web.controller.post.service.IPostCommentService;
+import com.ruoyi.web.controller.post.service.IPostFavoriteService;
+import com.ruoyi.web.controller.post.service.IPostService;
+import com.ruoyi.web.controller.post.service.IPostTagService;
+import com.ruoyi.web.controller.post.mapper.PostReportMapper;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Date;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * 帖子信息操作处理
+ *
+ * @author thunderhub
+ */
+@RestController
+@RequestMapping("/post")
+public class PostController extends BaseController
+{
+ @Autowired
+ private IPostService postService;
+
+ @Autowired
+ private IPostTagService postTagService;
+
+ @Autowired
+ private IPostCommentService postCommentService;
+
+ @Autowired
+ private IPostFavoriteService postFavoriteService;
+
+ @Autowired
+ private PostReportMapper postReportMapper;
+
+ /**
+ * 获取帖子列表
+ */
+ @PreAuthorize("@ss.hasPermi('post:post:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(Post post)
+ {
+ startPage();
+ List<Post> list = postService.selectPostList(post);
+ return getDataTable(list);
+ }
+
+ /**
+ * 导出帖子列表
+ */
+ @PreAuthorize("@ss.hasPermi('post:post:export')")
+ @Log(title = "帖子管理", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, Post post)
+ {
+ List<Post> list = postService.selectPostList(post);
+ ExcelUtil<Post> util = new ExcelUtil<Post>(Post.class);
+ util.exportExcel(response, list, "帖子数据");
+ }
+
+ /**
+ * 获取帖子详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('post:post:query')")
+ @GetMapping(value = "/{postId}")
+ public AjaxResult getInfo(@PathVariable Long postId)
+ {
+ Map<String, Object> result = new HashMap<>();
+
+ // 更新浏览量
+ postService.updatePostViews(postId);
+
+ // 获取帖子信息
+ Post post = postService.selectPostById(postId);
+ if (post == null) {
+ return error("帖子不存在");
+ }
+ result.put("post", post);
+
+ // 获取帖子标签
+ List<PostTag> tags = postTagService.selectPostTagsByPostId(postId);
+ result.put("tags", tags);
+
+ // 获取评论,只返回顶级评论,不包含回复
+ PostComment commentQuery = new PostComment();
+ commentQuery.setPostId(postId);
+ commentQuery.setParentId(0L);
+ List<PostComment> comments = postCommentService.selectPostCommentList(commentQuery);
+
+ // 获取每个评论的回复
+ List<Map<String, Object>> commentList = new ArrayList<>();
+ for (PostComment comment : comments) {
+ Map<String, Object> commentMap = new HashMap<>();
+ commentMap.put("comment", comment);
+ List<PostComment> replies = postCommentService.selectCommentsByParentId(comment.getCommentId());
+ commentMap.put("replies", replies);
+ commentList.add(commentMap);
+ }
+ result.put("comments", commentList);
+
+ // 获取相关帖子
+ // 1. 同一作者的其他帖子
+ List<Post> authorPosts = postService.selectAuthorOtherPosts(post.getAuthorId(), postId, 3);
+ result.put("authorPosts", authorPosts);
+
+ // 2. 相似标签的帖子
+ List<Post> similarPosts = postService.selectSimilarTagsPosts(post.getTags(), postId, 3);
+ result.put("similarPosts", similarPosts);
+
+ // 判断当前用户是否已收藏
+ Long userId = getUserId();
+ if (userId != null) {
+ PostFavorite favorite = postFavoriteService.selectPostFavoriteByPostIdAndUserId(postId, userId);
+ result.put("favorited", favorite != null && "0".equals(favorite.getStatus()));
+ } else {
+ result.put("favorited", false);
+ }
+
+ return success(result);
+ }
+
+ /**
+ * 新增帖子
+ */
+ @PreAuthorize("@ss.hasPermi('post:post:add')")
+ @Log(title = "帖子管理", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@Validated @RequestBody Post post)
+ {
+ post.setCreateBy(getUsername());
+ post.setAuthorId(getUserId());
+ post.setAuthor(getUsername());
+ post.setViews(0L);
+ post.setComments(0L);
+ post.setFavorites(0L);
+
+ // 处理标签,如果有传入标签数组
+ String tags = post.getTags();
+ int result = postService.insertPost(post);
+
+ if (result > 0 && StringUtils.isNotEmpty(tags)) {
+ // 处理标签关联
+ String[] tagArray = tags.split(",");
+ for (String tagName : tagArray) {
+ // 查询标签是否存在
+ PostTag tagQuery = new PostTag();
+ tagQuery.setTagName(tagName);
+ List<PostTag> existTags = postTagService.selectPostTagList(tagQuery);
+
+ Long tagId;
+ if (existTags.isEmpty()) {
+ // 创建新标签
+ PostTag newTag = new PostTag();
+ newTag.setTagName(tagName);
+ newTag.setTagColor("blue"); // 默认颜色
+ newTag.setPostCount(1L);
+ newTag.setStatus("0"); // 正常状态
+ newTag.setCreateBy(getUsername());
+ postTagService.insertPostTag(newTag);
+ tagId = newTag.getTagId();
+ } else {
+ // 更新标签帖子数量
+ PostTag existTag = existTags.get(0);
+ tagId = existTag.getTagId();
+ postTagService.updatePostTagCount(tagId, 1);
+ }
+
+ // 创建帖子标签关联
+ Long[] tagIds = new Long[]{tagId};
+ postTagService.batchInsertPostTagRelation(post.getPostId(), tagIds);
+ }
+ }
+
+ return toAjax(result);
+ }
+
+ /**
+ * 修改帖子
+ */
+ @PreAuthorize("@ss.hasPermi('post:post:edit')")
+ @Log(title = "帖子管理", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@Validated @RequestBody Post post)
+ {
+ post.setUpdateBy(getUsername());
+
+ // 处理标签
+ String tags = post.getTags();
+
+ // 先删除旧的标签关联
+ postTagService.deletePostTagRelation(post.getPostId());
+
+ int result = postService.updatePost(post);
+
+ if (result > 0 && StringUtils.isNotEmpty(tags)) {
+ // 处理标签关联
+ String[] tagArray = tags.split(",");
+ for (String tagName : tagArray) {
+ // 查询标签是否存在
+ PostTag tagQuery = new PostTag();
+ tagQuery.setTagName(tagName);
+ List<PostTag> existTags = postTagService.selectPostTagList(tagQuery);
+
+ Long tagId;
+ if (existTags.isEmpty()) {
+ // 创建新标签
+ PostTag newTag = new PostTag();
+ newTag.setTagName(tagName);
+ newTag.setTagColor("blue"); // 默认颜色
+ newTag.setPostCount(1L);
+ newTag.setStatus("0"); // 正常状态
+ newTag.setCreateBy(getUsername());
+ postTagService.insertPostTag(newTag);
+ tagId = newTag.getTagId();
+ } else {
+ // 更新标签帖子数量
+ PostTag existTag = existTags.get(0);
+ tagId = existTag.getTagId();
+ postTagService.updatePostTagCount(tagId, 1);
+ }
+
+ // 创建帖子标签关联
+ Long[] tagIds = new Long[]{tagId};
+ postTagService.batchInsertPostTagRelation(post.getPostId(), tagIds);
+ }
+ }
+
+ return toAjax(result);
+ }
+
+ /**
+ * 删除帖子
+ */
+ @PreAuthorize("@ss.hasPermi('post:post:remove')")
+ @Log(title = "帖子管理", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{postIds}")
+ public AjaxResult remove(@PathVariable Long[] postIds)
+ {
+ // 删除帖子相关的评论和收藏
+ for (Long postId : postIds) {
+ postCommentService.deletePostCommentByPostId(postId);
+ postFavoriteService.deletePostFavoriteByPostId(postId);
+ // 删除帖子标签关联
+ postTagService.deletePostTagRelation(postId);
+ }
+
+ return toAjax(postService.deletePostByIds(postIds));
+ }
+
+ /**
+ * 收藏帖子
+ */
+ @PreAuthorize("@ss.hasPermi('post:post:favorite')")
+ @Log(title = "帖子收藏", businessType = BusinessType.INSERT)
+ @PostMapping("/favorite/{postId}")
+ public AjaxResult favorite(@PathVariable Long postId)
+ {
+ Long userId = getUserId();
+
+ // 检查是否已收藏
+ PostFavorite exist = postFavoriteService.selectPostFavoriteByPostIdAndUserId(postId, userId);
+
+ if (exist != null) {
+ // 已存在记录,检查状态
+ if ("0".equals(exist.getStatus())) {
+ return error("已收藏该帖子");
+ } else {
+ // 取消收藏状态,改为收藏状态
+ exist.setStatus("0");
+ exist.setUpdateBy(getUsername());
+ postFavoriteService.updatePostFavorite(exist);
+
+ // 更新帖子收藏数量
+ postService.updatePostFavorites(postId, 1);
+
+ return success("收藏成功");
+ }
+ } else {
+ // 创建收藏记录
+ Post post = postService.selectPostById(postId);
+ if (post == null) {
+ return error("帖子不存在");
+ }
+
+ PostFavorite favorite = new PostFavorite();
+ favorite.setPostId(postId);
+ favorite.setUserId(userId);
+ favorite.setPostTitle(post.getTitle());
+ favorite.setPostCover(post.getCoverImage());
+ favorite.setStatus("0");
+ favorite.setCreateBy(getUsername());
+ postFavoriteService.insertPostFavorite(favorite);
+
+ // 更新帖子收藏数量
+ postService.updatePostFavorites(postId, 1);
+
+ return success("收藏成功");
+ }
+ }
+
+ /**
+ * 取消收藏帖子
+ */
+ @PreAuthorize("@ss.hasPermi('post:post:unfavorite')")
+ @Log(title = "取消收藏", businessType = BusinessType.UPDATE)
+ @PostMapping("/unfavorite/{postId}")
+ public AjaxResult unfavorite(@PathVariable Long postId)
+ {
+ Long userId = getUserId();
+
+ // 检查是否已收藏
+ PostFavorite exist = postFavoriteService.selectPostFavoriteByPostIdAndUserId(postId, userId);
+
+ if (exist != null && "0".equals(exist.getStatus())) {
+ // 更新为取消收藏状态
+ exist.setStatus("1");
+ exist.setUpdateBy(getUsername());
+ postFavoriteService.updatePostFavorite(exist);
+
+ // 更新帖子收藏数量
+ postService.updatePostFavorites(postId, -1);
+
+ return success("取消收藏成功");
+ } else {
+ return error("未收藏该帖子");
+ }
+ }
+
+ /**
+ * 获取用户收藏的帖子列表
+ */
+ @PreAuthorize("@ss.hasPermi('post:post:list')")
+ @GetMapping("/favorite/list")
+ public TableDataInfo favoriteList()
+ {
+ startPage();
+ PostFavorite query = new PostFavorite();
+ query.setUserId(getUserId());
+ query.setStatus("0"); // 只查询收藏状态的记录
+ List<PostFavorite> list = postFavoriteService.selectPostFavoriteList(query);
+ return getDataTable(list);
+ }
+
+ /**
+ * 查询待审核帖子列表
+ */
+ @PreAuthorize("@ss.hasPermi('post:post:review')")
+ @GetMapping("/review/list")
+ public TableDataInfo reviewList(Post post)
+ {
+ startPage();
+ // 只查询待审核状态的帖子
+ post.setStatus("0");
+ List<Post> list = postService.selectPostList(post);
+ return getDataTable(list);
+ }
+
+ /**
+ * 审核帖子(通过/拒绝)
+ */
+ @PreAuthorize("@ss.hasPermi('post:post:review')")
+ @Log(title = "帖子审核", businessType = BusinessType.UPDATE)
+ @PutMapping("/review/{postId}")
+ public AjaxResult reviewPost(@PathVariable Long postId, @RequestBody ReviewRequest request)
+ {
+ Post post = postService.selectPostById(postId);
+ if (post == null) {
+ return error("帖子不存在");
+ }
+
+ Post updatePost = new Post();
+ updatePost.setPostId(postId);
+ updatePost.setUpdateBy(getUsername());
+
+ if ("approve".equals(request.getAction())) {
+ updatePost.setStatus("1"); // 审核通过,状态改为已发布
+ updatePost.setRemark(request.getReason());
+
+ int result = postService.updatePost(updatePost);
+ if (result > 0) {
+ return success("帖子审核通过");
+ } else {
+ return error("审核操作失败");
+ }
+ } else if ("reject".equals(request.getAction())) {
+ updatePost.setStatus("2"); // 审核拒绝
+ updatePost.setRemark(request.getReason());
+
+ int result = postService.updatePost(updatePost);
+ if (result > 0) {
+ return success("帖子审核拒绝");
+ } else {
+ return error("审核操作失败");
+ }
+ } else {
+ return error("无效的审核操作");
+ }
+ }
+
+ /**
+ * 强制下架帖子
+ */
+ @PreAuthorize("@ss.hasPermi('post:post:takedown')")
+ @Log(title = "帖子下架", businessType = BusinessType.UPDATE)
+ @PutMapping("/takedown/{postId}")
+ public AjaxResult takeDownPost(@PathVariable Long postId, @RequestBody TakeDownRequest request)
+ {
+ Post post = postService.selectPostById(postId);
+ if (post == null) {
+ return error("帖子不存在");
+ }
+
+ Post updatePost = new Post();
+ updatePost.setPostId(postId);
+ updatePost.setStatus("3"); // 状态改为已下架
+ updatePost.setRemark(request.getReason());
+ updatePost.setUpdateBy(getUsername());
+
+ int result = postService.updatePost(updatePost);
+ if (result > 0) {
+ return success("帖子已下架");
+ } else {
+ return error("下架操作失败");
+ }
+ }
+
+ /**
+ * 查询举报列表
+ */
+ @PreAuthorize("@ss.hasPermi('post:post:report')")
+ @GetMapping("/report/list")
+ public TableDataInfo reportList(PostReport postReport)
+ {
+ startPage();
+ List<PostReport> list = postReportMapper.selectPostReportList(postReport);
+ return getDataTable(list);
+ }
+
+ /**
+ * 处理举报(通过/驳回)
+ */
+ @PreAuthorize("@ss.hasPermi('post:post:report')")
+ @Log(title = "举报处理", businessType = BusinessType.UPDATE)
+ @PutMapping("/report/handle/{reportId}")
+ public AjaxResult handleReport(@PathVariable Long reportId, @RequestBody ReportHandleRequest request)
+ {
+ PostReport report = postReportMapper.selectPostReportById(reportId);
+ if (report == null) {
+ return error("举报记录不存在");
+ }
+
+ if ("approve".equals(request.getAction())) {
+ // 举报属实,下架帖子
+ Post post = postService.selectPostById(request.getPostId());
+ if (post != null) {
+ Post updatePost = new Post();
+ updatePost.setPostId(request.getPostId());
+ updatePost.setStatus("3"); // 状态改为已下架
+ updatePost.setRemark("因举报下架:" + (request.getReason() != null ? request.getReason() : ""));
+ updatePost.setUpdateBy(getUsername());
+
+ int result = postService.updatePost(updatePost);
+ if (result > 0) {
+ // 更新举报记录状态
+ report.setStatus("1"); // 已处理
+ report.setHandleResult("举报属实,帖子已下架");
+ report.setHandleTime(new Date());
+ report.setHandleBy(getUsername());
+ postReportMapper.updatePostReport(report);
+
+ return success("举报处理成功,帖子已下架");
+ } else {
+ return error("处理失败");
+ }
+ } else {
+ return error("帖子不存在");
+ }
+ } else if ("reject".equals(request.getAction())) {
+ // 举报不属实,驳回举报
+ report.setStatus("2"); // 已驳回
+ report.setHandleResult("举报不属实" + (request.getReason() != null ? ":" + request.getReason() : ""));
+ report.setHandleTime(new Date());
+ report.setHandleBy(getUsername());
+ postReportMapper.updatePostReport(report);
+
+ return success("举报已驳回");
+ } else {
+ return error("无效的处理操作");
+ }
+ }
+
+ /**
+ * 审核请求类
+ */
+ public static class ReviewRequest {
+ private String action;
+ private String reason;
+
+ public String getAction() {
+ return action;
+ }
+
+ public void setAction(String action) {
+ this.action = action;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+
+ public void setReason(String reason) {
+ this.reason = reason;
+ }
+ }
+
+ /**
+ * 下架请求类
+ */
+ public static class TakeDownRequest {
+ private String reason;
+
+ public String getReason() {
+ return reason;
+ }
+
+ public void setReason(String reason) {
+ this.reason = reason;
+ }
+ }
+
+ /**
+ * 举报处理请求类
+ */
+ public static class ReportHandleRequest {
+ private String action;
+ private String reason;
+ private Long postId;
+
+ public String getAction() {
+ return action;
+ }
+
+ public void setAction(String action) {
+ this.action = action;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+
+ public void setReason(String reason) {
+ this.reason = reason;
+ }
+
+ public Long getPostId() {
+ return postId;
+ }
+
+ public void setPostId(Long postId) {
+ this.postId = postId;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/controller/PostTagController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/controller/PostTagController.java
new file mode 100644
index 0000000..7b4b496
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/controller/PostTagController.java
@@ -0,0 +1,138 @@
+package com.ruoyi.web.controller.post.controller;
+
+import java.util.List;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.web.controller.post.domain.PostTag;
+import com.ruoyi.web.controller.post.service.IPostTagService;
+
+/**
+ * 帖子标签管理
+ *
+ * @author thunderhub
+ */
+@RestController
+@RequestMapping("/post/tag")
+public class PostTagController extends BaseController
+{
+ @Autowired
+ private IPostTagService postTagService;
+
+ /**
+ * 获取帖子标签列表
+ */
+ @PreAuthorize("@ss.hasPermi('post:tag:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(PostTag postTag)
+ {
+ startPage();
+ List<PostTag> list = postTagService.selectPostTagList(postTag);
+ return getDataTable(list);
+ }
+
+ /**
+ * 导出帖子标签列表
+ */
+ @PreAuthorize("@ss.hasPermi('post:tag:export')")
+ @Log(title = "帖子标签", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, PostTag postTag)
+ {
+ List<PostTag> list = postTagService.selectPostTagList(postTag);
+ ExcelUtil<PostTag> util = new ExcelUtil<PostTag>(PostTag.class);
+ util.exportExcel(response, list, "标签数据");
+ }
+
+ /**
+ * 获取帖子标签详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('post:tag:query')")
+ @GetMapping(value = "/{tagId}")
+ public AjaxResult getInfo(@PathVariable Long tagId)
+ {
+ return success(postTagService.selectPostTagById(tagId));
+ }
+
+ /**
+ * 新增帖子标签
+ */
+ @PreAuthorize("@ss.hasPermi('post:tag:add')")
+ @Log(title = "帖子标签", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@Validated @RequestBody PostTag postTag)
+ {
+ if (!postTagService.checkTagNameUnique(postTag))
+ {
+ return error("新增标签'" + postTag.getTagName() + "'失败,标签名称已存在");
+ }
+ postTag.setCreateBy(getUsername());
+ postTag.setPostCount(0L);
+ return toAjax(postTagService.insertPostTag(postTag));
+ }
+
+ /**
+ * 修改帖子标签
+ */
+ @PreAuthorize("@ss.hasPermi('post:tag:edit')")
+ @Log(title = "帖子标签", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@Validated @RequestBody PostTag postTag)
+ {
+ if (!postTagService.checkTagNameUnique(postTag))
+ {
+ return error("修改标签'" + postTag.getTagName() + "'失败,标签名称已存在");
+ }
+ postTag.setUpdateBy(getUsername());
+ return toAjax(postTagService.updatePostTag(postTag));
+ }
+
+ /**
+ * 删除帖子标签
+ */
+ @PreAuthorize("@ss.hasPermi('post:tag:remove')")
+ @Log(title = "帖子标签", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{tagIds}")
+ public AjaxResult remove(@PathVariable Long[] tagIds)
+ {
+ return toAjax(postTagService.deletePostTagByIds(tagIds));
+ }
+
+ /**
+ * 获取帖子的标签列表
+ */
+ @PreAuthorize("@ss.hasPermi('post:tag:list')")
+ @GetMapping("/list/{postId}")
+ public AjaxResult listByPostId(@PathVariable Long postId)
+ {
+ List<PostTag> tags = postTagService.selectPostTagsByPostId(postId);
+ return success(tags);
+ }
+
+ /**
+ * 获取所有可用标签(用于下拉选择)
+ */
+ @GetMapping("/optionselect")
+ public AjaxResult optionselect()
+ {
+ PostTag query = new PostTag();
+ query.setStatus("0"); // 正常状态
+ List<PostTag> tags = postTagService.selectPostTagList(query);
+ return success(tags);
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/Post.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/Post.java
new file mode 100644
index 0000000..40f18bf
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/Post.java
@@ -0,0 +1,256 @@
+package com.ruoyi.web.controller.post.domain;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.annotation.Excel.ColumnType;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 帖子表 sys_post
+ *
+ * @author thunderhub
+ */
+public class Post extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 帖子ID */
+ @Excel(name = "帖子ID", cellType = ColumnType.NUMERIC)
+ private Long postId;
+
+ /** 帖子标题 */
+ @Excel(name = "帖子标题")
+ private String title;
+
+ /** 帖子内容(富文本) */
+ private String content;
+
+ /** 帖子摘要 */
+ @Excel(name = "帖子摘要")
+ private String summary;
+
+ /** 封面图片URL */
+ @Excel(name = "封面图片")
+ private String coverImage;
+
+ /** 作者ID */
+ @Excel(name = "作者ID")
+ private Long authorId;
+
+ /** 作者名称 */
+ @Excel(name = "作者名称")
+ private String author;
+
+ /** 浏览量 */
+ @Excel(name = "浏览量")
+ private Long views;
+
+ /** 评论数 */
+ @Excel(name = "评论数")
+ private Long comments;
+
+ /** 收藏数 */
+ @Excel(name = "收藏数")
+ private Long favorites;
+
+ /** 点赞数 */
+ @Excel(name = "点赞数")
+ private Long likes;
+
+ /** 状态(0待审核 1正常 2拒绝 3下架) */
+ @Excel(name = "状态", readConverterExp = "0=待审核,1=正常,2=拒绝,3=下架")
+ private String status;
+
+ /** 发布时间 */
+ @Excel(name = "发布时间")
+ private String publishTime;
+
+ /** 标签列表,用逗号分隔 */
+ private String tags;
+
+ /** 推广计划ID */
+ @Excel(name = "推广计划ID")
+ private Long promotionPlanId;
+
+ public Long getPostId()
+ {
+ return postId;
+ }
+
+ public void setPostId(Long postId)
+ {
+ this.postId = postId;
+ }
+
+ @NotBlank(message = "帖子标题不能为空")
+ @Size(min = 0, max = 100, message = "帖子标题长度不能超过100个字符")
+ public String getTitle()
+ {
+ return title;
+ }
+
+ public void setTitle(String title)
+ {
+ this.title = title;
+ }
+
+ public String getContent()
+ {
+ return content;
+ }
+
+ public void setContent(String content)
+ {
+ this.content = content;
+ }
+
+ @Size(min = 0, max = 500, message = "帖子摘要长度不能超过500个字符")
+ public String getSummary()
+ {
+ return summary;
+ }
+
+ public void setSummary(String summary)
+ {
+ this.summary = summary;
+ }
+
+ public String getCoverImage()
+ {
+ return coverImage;
+ }
+
+ public void setCoverImage(String coverImage)
+ {
+ this.coverImage = coverImage;
+ }
+
+ public Long getAuthorId()
+ {
+ return authorId;
+ }
+
+ public void setAuthorId(Long authorId)
+ {
+ this.authorId = authorId;
+ }
+
+ public String getAuthor()
+ {
+ return author;
+ }
+
+ public void setAuthor(String author)
+ {
+ this.author = author;
+ }
+
+ public Long getViews()
+ {
+ return views;
+ }
+
+ public void setViews(Long views)
+ {
+ this.views = views;
+ }
+
+ public Long getComments()
+ {
+ return comments;
+ }
+
+ public void setComments(Long comments)
+ {
+ this.comments = comments;
+ }
+
+ public Long getFavorites()
+ {
+ return favorites;
+ }
+
+ public void setFavorites(Long favorites)
+ {
+ this.favorites = favorites;
+ }
+
+ public Long getLikes()
+ {
+ return likes;
+ }
+
+ public void setLikes(Long likes)
+ {
+ this.likes = likes;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ public String getPublishTime()
+ {
+ return publishTime;
+ }
+
+ public void setPublishTime(String publishTime)
+ {
+ this.publishTime = publishTime;
+ }
+
+ public String getTags()
+ {
+ return tags;
+ }
+
+ public void setTags(String tags)
+ {
+ this.tags = tags;
+ }
+
+ public Long getPromotionPlanId()
+ {
+ return promotionPlanId;
+ }
+
+ public void setPromotionPlanId(Long promotionPlanId)
+ {
+ this.promotionPlanId = promotionPlanId;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("postId", getPostId())
+ .append("title", getTitle())
+ .append("content", getContent())
+ .append("summary", getSummary())
+ .append("coverImage", getCoverImage())
+ .append("authorId", getAuthorId())
+ .append("author", getAuthor())
+ .append("views", getViews())
+ .append("comments", getComments())
+ .append("favorites", getFavorites())
+ .append("likes", getLikes())
+ .append("status", getStatus())
+ .append("publishTime", getPublishTime())
+ .append("tags", getTags())
+ .append("promotionPlanId", getPromotionPlanId())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostComment.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostComment.java
new file mode 100644
index 0000000..5041087
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostComment.java
@@ -0,0 +1,193 @@
+package com.ruoyi.web.controller.post.domain;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.annotation.Excel.ColumnType;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 帖子评论表 sys_post_comment
+ *
+ * @author thunderhub
+ */
+public class PostComment extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 评论ID */
+ @Excel(name = "评论ID", cellType = ColumnType.NUMERIC)
+ private Long commentId;
+
+ /** 帖子ID */
+ @Excel(name = "帖子ID")
+ private Long postId;
+
+ /** 评论内容 */
+ @Excel(name = "评论内容")
+ private String content;
+
+ /** 评论人ID */
+ @Excel(name = "评论人ID")
+ private Long userId;
+
+ /** 评论人名称 */
+ @Excel(name = "评论人名称")
+ private String userName;
+
+ /** 评论人头像 */
+ private String userAvatar;
+
+ /** 父评论ID,如果为0则为顶级评论 */
+ private Long parentId;
+
+ /** 回复对象的用户ID */
+ private Long replyUserId;
+
+ /** 回复对象的用户名称 */
+ private String replyUserName;
+
+ /** 状态(0待审核 1正常 2拒绝) */
+ @Excel(name = "状态", readConverterExp = "0=待审核,1=正常,2=拒绝")
+ private String status;
+
+ /** 点赞数 */
+ @Excel(name = "点赞数")
+ private Long likes;
+
+ public Long getCommentId()
+ {
+ return commentId;
+ }
+
+ public void setCommentId(Long commentId)
+ {
+ this.commentId = commentId;
+ }
+
+ public Long getPostId()
+ {
+ return postId;
+ }
+
+ public void setPostId(Long postId)
+ {
+ this.postId = postId;
+ }
+
+ @NotBlank(message = "评论内容不能为空")
+ @Size(min = 0, max = 500, message = "评论内容长度不能超过500个字符")
+ public String getContent()
+ {
+ return content;
+ }
+
+ public void setContent(String content)
+ {
+ this.content = content;
+ }
+
+ public Long getUserId()
+ {
+ return userId;
+ }
+
+ public void setUserId(Long userId)
+ {
+ this.userId = userId;
+ }
+
+ public String getUserName()
+ {
+ return userName;
+ }
+
+ public void setUserName(String userName)
+ {
+ this.userName = userName;
+ }
+
+ public String getUserAvatar()
+ {
+ return userAvatar;
+ }
+
+ public void setUserAvatar(String userAvatar)
+ {
+ this.userAvatar = userAvatar;
+ }
+
+ public Long getParentId()
+ {
+ return parentId;
+ }
+
+ public void setParentId(Long parentId)
+ {
+ this.parentId = parentId;
+ }
+
+ public Long getReplyUserId()
+ {
+ return replyUserId;
+ }
+
+ public void setReplyUserId(Long replyUserId)
+ {
+ this.replyUserId = replyUserId;
+ }
+
+ public String getReplyUserName()
+ {
+ return replyUserName;
+ }
+
+ public void setReplyUserName(String replyUserName)
+ {
+ this.replyUserName = replyUserName;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ public Long getLikes()
+ {
+ return likes;
+ }
+
+ public void setLikes(Long likes)
+ {
+ this.likes = likes;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("commentId", getCommentId())
+ .append("postId", getPostId())
+ .append("content", getContent())
+ .append("userId", getUserId())
+ .append("userName", getUserName())
+ .append("userAvatar", getUserAvatar())
+ .append("parentId", getParentId())
+ .append("replyUserId", getReplyUserId())
+ .append("replyUserName", getReplyUserName())
+ .append("status", getStatus())
+ .append("likes", getLikes())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostFavorite.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostFavorite.java
new file mode 100644
index 0000000..1a6c0f0
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostFavorite.java
@@ -0,0 +1,117 @@
+package com.ruoyi.web.controller.post.domain;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.annotation.Excel.ColumnType;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 帖子收藏表 sys_post_favorite
+ *
+ * @author thunderhub
+ */
+public class PostFavorite extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 收藏ID */
+ @Excel(name = "收藏ID", cellType = ColumnType.NUMERIC)
+ private Long favoriteId;
+
+ /** 帖子ID */
+ @Excel(name = "帖子ID")
+ private Long postId;
+
+ /** 用户ID */
+ @Excel(name = "用户ID")
+ private Long userId;
+
+ /** 帖子标题 */
+ @Excel(name = "帖子标题")
+ private String postTitle;
+
+ /** 帖子封面 */
+ private String postCover;
+
+ /** 收藏状态(0收藏 1取消) */
+ @Excel(name = "收藏状态", readConverterExp = "0=收藏,1=取消")
+ private String status;
+
+ public Long getFavoriteId()
+ {
+ return favoriteId;
+ }
+
+ public void setFavoriteId(Long favoriteId)
+ {
+ this.favoriteId = favoriteId;
+ }
+
+ public Long getPostId()
+ {
+ return postId;
+ }
+
+ public void setPostId(Long postId)
+ {
+ this.postId = postId;
+ }
+
+ public Long getUserId()
+ {
+ return userId;
+ }
+
+ public void setUserId(Long userId)
+ {
+ this.userId = userId;
+ }
+
+ public String getPostTitle()
+ {
+ return postTitle;
+ }
+
+ public void setPostTitle(String postTitle)
+ {
+ this.postTitle = postTitle;
+ }
+
+ public String getPostCover()
+ {
+ return postCover;
+ }
+
+ public void setPostCover(String postCover)
+ {
+ this.postCover = postCover;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("favoriteId", getFavoriteId())
+ .append("postId", getPostId())
+ .append("userId", getUserId())
+ .append("postTitle", getPostTitle())
+ .append("postCover", getPostCover())
+ .append("status", getStatus())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostPayment.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostPayment.java
new file mode 100644
index 0000000..091fd94
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostPayment.java
@@ -0,0 +1,92 @@
+package com.ruoyi.web.controller.post.domain;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 帖子支付记录对象 post_payment
+ *
+ * @author thunderhub
+ */
+public class PostPayment extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 支付ID */
+ private Long paymentId;
+
+ /** 帖子ID */
+ private Long postId;
+
+ /** 推广计划ID */
+ private Long planId;
+
+ /** 用户ID */
+ private Long userId;
+
+ /** 支付金额 */
+ private BigDecimal amount;
+
+ /** 支付状态 */
+ private String paymentStatus;
+
+ /** 支付时间 */
+ private Date paymentTime;
+
+ public Long getPaymentId() {
+ return paymentId;
+ }
+
+ public void setPaymentId(Long paymentId) {
+ this.paymentId = paymentId;
+ }
+
+ public Long getPostId() {
+ return postId;
+ }
+
+ public void setPostId(Long postId) {
+ this.postId = postId;
+ }
+
+ public Long getPlanId() {
+ return planId;
+ }
+
+ public void setPlanId(Long planId) {
+ this.planId = planId;
+ }
+
+ public Long getUserId() {
+ return userId;
+ }
+
+ public void setUserId(Long userId) {
+ this.userId = userId;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public void setAmount(BigDecimal amount) {
+ this.amount = amount;
+ }
+
+ public String getPaymentStatus() {
+ return paymentStatus;
+ }
+
+ public void setPaymentStatus(String paymentStatus) {
+ this.paymentStatus = paymentStatus;
+ }
+
+ public Date getPaymentTime() {
+ return paymentTime;
+ }
+
+ public void setPaymentTime(Date paymentTime) {
+ this.paymentTime = paymentTime;
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostPromotionPlan.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostPromotionPlan.java
new file mode 100644
index 0000000..422e156
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostPromotionPlan.java
@@ -0,0 +1,118 @@
+package com.ruoyi.web.controller.post.domain;
+
+import java.math.BigDecimal;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 帖子推广计划对象 post_promotion_plan
+ *
+ * @author thunderhub
+ */
+public class PostPromotionPlan extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 推广计划ID */
+ private Long planId;
+
+ /** 推广计划名称 */
+ private String planName;
+
+ /** 推广计划描述 */
+ private String planDescription;
+
+ /** 推广价格 */
+ private BigDecimal price;
+
+ /** 推广时长(天) */
+ private Integer duration;
+
+ /** 推广等级(数字越小等级越高) */
+ private Integer level;
+
+ /** 状态(0正常 1停用) */
+ private String status;
+
+ public void setPlanId(Long planId)
+ {
+ this.planId = planId;
+ }
+
+ public Long getPlanId()
+ {
+ return planId;
+ }
+
+ public void setPlanName(String planName)
+ {
+ this.planName = planName;
+ }
+
+ public String getPlanName()
+ {
+ return planName;
+ }
+
+ public void setPlanDescription(String planDescription)
+ {
+ this.planDescription = planDescription;
+ }
+
+ public String getPlanDescription()
+ {
+ return planDescription;
+ }
+
+ public void setPrice(BigDecimal price)
+ {
+ this.price = price;
+ }
+
+ public BigDecimal getPrice()
+ {
+ return price;
+ }
+
+ public void setDuration(Integer duration)
+ {
+ this.duration = duration;
+ }
+
+ public Integer getDuration()
+ {
+ return duration;
+ }
+
+ public void setLevel(Integer level)
+ {
+ this.level = level;
+ }
+
+ public Integer getLevel()
+ {
+ return level;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ @Override
+ public String toString() {
+ return "PostPromotionPlan{" +
+ "planId=" + planId +
+ ", planName='" + planName + '\'' +
+ ", planDescription='" + planDescription + '\'' +
+ ", price=" + price +
+ ", duration=" + duration +
+ ", level=" + level +
+ ", status='" + status + '\'' +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostReport.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostReport.java
new file mode 100644
index 0000000..d8f8f7f
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostReport.java
@@ -0,0 +1,124 @@
+package com.ruoyi.web.controller.post.domain;
+
+import java.util.Date;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 帖子举报对象 post_report
+ *
+ * @author thunderhub
+ */
+public class PostReport extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 举报ID */
+ private Long reportId;
+
+ /** 帖子ID */
+ private Long postId;
+
+ /** 帖子标题 */
+ private String postTitle;
+
+ /** 举报用户ID */
+ private Long reportUserId;
+
+ /** 举报用户名 */
+ private String reportUserName;
+
+ /** 举报理由 */
+ private String reportReason;
+
+ /** 举报状态 0-待处理 1-已处理 2-已驳回 */
+ private String status;
+
+ /** 处理结果 */
+ private String handleResult;
+
+ /** 处理时间 */
+ private Date handleTime;
+
+ /** 处理人 */
+ private String handleBy;
+
+ public Long getReportId() {
+ return reportId;
+ }
+
+ public void setReportId(Long reportId) {
+ this.reportId = reportId;
+ }
+
+ public Long getPostId() {
+ return postId;
+ }
+
+ public void setPostId(Long postId) {
+ this.postId = postId;
+ }
+
+ public String getPostTitle() {
+ return postTitle;
+ }
+
+ public void setPostTitle(String postTitle) {
+ this.postTitle = postTitle;
+ }
+
+ public Long getReportUserId() {
+ return reportUserId;
+ }
+
+ public void setReportUserId(Long reportUserId) {
+ this.reportUserId = reportUserId;
+ }
+
+ public String getReportUserName() {
+ return reportUserName;
+ }
+
+ public void setReportUserName(String reportUserName) {
+ this.reportUserName = reportUserName;
+ }
+
+ public String getReportReason() {
+ return reportReason;
+ }
+
+ public void setReportReason(String reportReason) {
+ this.reportReason = reportReason;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getHandleResult() {
+ return handleResult;
+ }
+
+ public void setHandleResult(String handleResult) {
+ this.handleResult = handleResult;
+ }
+
+ public Date getHandleTime() {
+ return handleTime;
+ }
+
+ public void setHandleTime(Date handleTime) {
+ this.handleTime = handleTime;
+ }
+
+ public String getHandleBy() {
+ return handleBy;
+ }
+
+ public void setHandleBy(String handleBy) {
+ this.handleBy = handleBy;
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostTag.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostTag.java
new file mode 100644
index 0000000..e4a4b55
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostTag.java
@@ -0,0 +1,107 @@
+package com.ruoyi.web.controller.post.domain;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.annotation.Excel.ColumnType;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 帖子标签表 sys_post_tag
+ *
+ * @author thunderhub
+ */
+public class PostTag extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 标签ID */
+ @Excel(name = "标签ID", cellType = ColumnType.NUMERIC)
+ private Long tagId;
+
+ /** 标签名称 */
+ @Excel(name = "标签名称")
+ private String tagName;
+
+ /** 标签颜色 */
+ @Excel(name = "标签颜色")
+ private String tagColor;
+
+ /** 帖子数量 */
+ @Excel(name = "帖子数量")
+ private Long postCount;
+
+ /** 状态(0正常 1停用) */
+ @Excel(name = "状态", readConverterExp = "0=正常,1=停用")
+ private String status;
+
+ public Long getTagId()
+ {
+ return tagId;
+ }
+
+ public void setTagId(Long tagId)
+ {
+ this.tagId = tagId;
+ }
+
+ @NotBlank(message = "标签名称不能为空")
+ @Size(min = 0, max = 50, message = "标签名称长度不能超过50个字符")
+ public String getTagName()
+ {
+ return tagName;
+ }
+
+ public void setTagName(String tagName)
+ {
+ this.tagName = tagName;
+ }
+
+ public String getTagColor()
+ {
+ return tagColor;
+ }
+
+ public void setTagColor(String tagColor)
+ {
+ this.tagColor = tagColor;
+ }
+
+ public Long getPostCount()
+ {
+ return postCount;
+ }
+
+ public void setPostCount(Long postCount)
+ {
+ this.postCount = postCount;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("tagId", getTagId())
+ .append("tagName", getTagName())
+ .append("tagColor", getTagColor())
+ .append("postCount", getPostCount())
+ .append("status", getStatus())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostTagRelation.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostTagRelation.java
new file mode 100644
index 0000000..78f4443
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/domain/PostTagRelation.java
@@ -0,0 +1,70 @@
+package com.ruoyi.web.controller.post.domain;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 帖子标签关联表 sys_post_tag_relation
+ *
+ * @author thunderhub
+ */
+public class PostTagRelation extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 关联ID */
+ private Long id;
+
+ /** 帖子ID */
+ @Excel(name = "帖子ID")
+ private Long postId;
+
+ /** 标签ID */
+ @Excel(name = "标签ID")
+ private Long tagId;
+
+ public Long getId()
+ {
+ return id;
+ }
+
+ public void setId(Long id)
+ {
+ this.id = id;
+ }
+
+ public Long getPostId()
+ {
+ return postId;
+ }
+
+ public void setPostId(Long postId)
+ {
+ this.postId = postId;
+ }
+
+ public Long getTagId()
+ {
+ return tagId;
+ }
+
+ public void setTagId(Long tagId)
+ {
+ this.tagId = tagId;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("id", getId())
+ .append("postId", getPostId())
+ .append("tagId", getTagId())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .toString();
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostCommentMapper.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostCommentMapper.java
new file mode 100644
index 0000000..c5103a1
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostCommentMapper.java
@@ -0,0 +1,94 @@
+package com.ruoyi.web.controller.post.mapper;
+
+import java.util.List;
+
+import com.ruoyi.web.controller.post.domain.PostComment;
+
+/**
+ * 帖子评论Mapper接口
+ *
+ * @author thunderhub
+ */
+public interface PostCommentMapper
+{
+ /**
+ * 查询帖子评论信息
+ *
+ * @param commentId 帖子评论ID
+ * @return 帖子评论信息
+ */
+ public PostComment selectPostCommentById(Long commentId);
+
+ /**
+ * 查询帖子评论列表
+ *
+ * @param postComment 帖子评论信息
+ * @return 帖子评论集合
+ */
+ public List<PostComment> selectPostCommentList(PostComment postComment);
+
+ /**
+ * 根据帖子ID查询评论
+ *
+ * @param postId 帖子ID
+ * @param limit 查询数量,0表示查询全部
+ * @return 帖子评论集合
+ */
+ public List<PostComment> selectCommentsByPostId(Long postId, int limit);
+
+ /**
+ * 根据父评论ID查询回复
+ *
+ * @param parentId 父评论ID
+ * @return 帖子评论集合
+ */
+ public List<PostComment> selectCommentsByParentId(Long parentId);
+
+ /**
+ * 新增帖子评论
+ *
+ * @param postComment 帖子评论信息
+ * @return 结果
+ */
+ public int insertPostComment(PostComment postComment);
+
+ /**
+ * 修改帖子评论
+ *
+ * @param postComment 帖子评论信息
+ * @return 结果
+ */
+ public int updatePostComment(PostComment postComment);
+
+ /**
+ * 删除帖子评论
+ *
+ * @param commentId 帖子评论ID
+ * @return 结果
+ */
+ public int deletePostCommentById(Long commentId);
+
+ /**
+ * 批量删除帖子评论
+ *
+ * @param commentIds 需要删除的帖子评论ID
+ * @return 结果
+ */
+ public int deletePostCommentByIds(Long[] commentIds);
+
+ /**
+ * 根据帖子ID删除评论
+ *
+ * @param postId 帖子ID
+ * @return 结果
+ */
+ public int deletePostCommentByPostId(Long postId);
+
+ /**
+ * 更新评论点赞数
+ *
+ * @param postComment 评论信息
+ * @return 结果
+ */
+ public int updateCommentLikes(PostComment postComment);
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostContentMapper.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostContentMapper.java
new file mode 100644
index 0000000..6929295
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostContentMapper.java
@@ -0,0 +1,114 @@
+package com.ruoyi.web.controller.post.mapper;
+
+import java.util.List;
+
+import com.ruoyi.web.controller.post.domain.Post;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 帖子内容Mapper接口
+ *
+ * @author thunderhub
+ */
+public interface PostContentMapper
+{
+ /**
+ * 查询帖子信息
+ *
+ * @param postId 帖子ID
+ * @return 帖子信息
+ */
+ public Post selectPostById(Long postId);
+
+ /**
+ * 查询帖子列表
+ *
+ * @param post 帖子信息
+ * @return 帖子集合
+ */
+ public List<Post> selectPostList(Post post);
+
+ /**
+ * 查询作者的其他帖子
+ *
+ * @param authorId 作者ID
+ * @param postId 当前帖子ID(排除)
+ * @param limit 查询数量
+ * @return 帖子集合
+ */
+ public List<Post> selectAuthorOtherPosts(@Param("authorId") Long authorId, @Param("postId") Long postId, @Param("limit") int limit);
+
+ /**
+ * 查询相似标签的帖子
+ *
+ * @param tags 标签字符串
+ * @param postId 当前帖子ID(排除)
+ * @param limit 查询数量
+ * @return 帖子集合
+ */
+ public List<Post> selectSimilarTagsPosts(@Param("tags") String tags, @Param("postId") Long postId, @Param("limit") int limit);
+
+ /**
+ * 新增帖子
+ *
+ * @param post 帖子信息
+ * @return 结果
+ */
+ public int insertPost(Post post);
+
+ /**
+ * 修改帖子
+ *
+ * @param post 帖子信息
+ * @return 结果
+ */
+ public int updatePost(Post post);
+
+ /**
+ * 删除帖子
+ *
+ * @param postId 帖子ID
+ * @return 结果
+ */
+ public int deletePostById(Long postId);
+
+ /**
+ * 批量删除帖子
+ *
+ * @param postIds 需要删除的帖子ID
+ * @return 结果
+ */
+ public int deletePostByIds(Long[] postIds);
+
+ /**
+ * 更新帖子浏览量
+ *
+ * @param postId 帖子ID
+ * @return 结果
+ */
+ public int updatePostViews(Long postId);
+
+ /**
+ * 更新帖子评论数
+ *
+ * @param post 帖子信息
+ * @return 结果
+ */
+ public int updatePostComments(Post post);
+
+ /**
+ * 更新帖子收藏数
+ *
+ * @param post 帖子信息
+ * @return 结果
+ */
+ public int updatePostFavorites(Post post);
+
+ /**
+ * 更新帖子点赞数
+ *
+ * @param post 帖子信息
+ * @return 结果
+ */
+ public int updatePostLikes(Post post);
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostContentTagMapper.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostContentTagMapper.java
new file mode 100644
index 0000000..5229c59
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostContentTagMapper.java
@@ -0,0 +1,112 @@
+package com.ruoyi.web.controller.post.mapper;
+
+import java.util.List;
+
+import com.ruoyi.web.controller.post.domain.PostTag;
+import com.ruoyi.web.controller.post.domain.PostTagRelation;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 帖子标签Mapper接口
+ *
+ * @author thunderhub
+ */
+@Mapper
+public interface PostContentTagMapper
+{
+ /**
+ * 查询帖子标签信息
+ *
+ * @param tagId 帖子标签ID
+ * @return 帖子标签信息
+ */
+ public PostTag selectPostTagById(Long tagId);
+
+ /**
+ * 查询帖子标签列表
+ *
+ * @param postTag 帖子标签信息
+ * @return 帖子标签集合
+ */
+ public List<PostTag> selectPostTagList(PostTag postTag);
+
+ /**
+ * 根据帖子ID查询标签
+ *
+ * @param postId 帖子ID
+ * @return 帖子标签集合
+ */
+ public List<PostTag> selectPostTagsByPostId(Long postId);
+
+ /**
+ * 新增帖子标签
+ *
+ * @param postTag 帖子标签信息
+ * @return 结果
+ */
+ public int insertPostTag(PostTag postTag);
+
+ /**
+ * 修改帖子标签
+ *
+ * @param postTag 帖子标签信息
+ * @return 结果
+ */
+ public int updatePostTag(PostTag postTag);
+
+ /**
+ * 删除帖子标签
+ *
+ * @param tagId 帖子标签ID
+ * @return 结果
+ */
+ public int deletePostTagById(Long tagId);
+
+ /**
+ * 批量删除帖子标签
+ *
+ * @param tagIds 需要删除的帖子标签ID
+ * @return 结果
+ */
+ public int deletePostTagByIds(Long[] tagIds);
+
+ /**
+ * 插入帖子标签关联
+ *
+ * @param relation 关联信息
+ * @return 结果
+ */
+ public int insertPostTagRelation(PostTagRelation relation);
+
+ /**
+ * 批量插入帖子标签关联
+ *
+ * @param list 关联列表
+ * @return 结果
+ */
+ public int batchInsertPostTagRelation(List<PostTagRelation> list);
+
+ /**
+ * 删除帖子标签关联
+ *
+ * @param postId 帖子ID
+ * @return 结果
+ */
+ public int deletePostTagRelation(Long postId);
+
+ /**
+ * 更新标签帖子数量
+ *
+ * @param postTag 帖子标签信息
+ * @return 结果
+ */
+ public int updatePostTagCount(PostTag postTag);
+
+ /**
+ * 校验标签名称是否唯一
+ *
+ * @param tagName 标签名称
+ * @return 结果
+ */
+ public PostTag checkTagNameUnique(String tagName);
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostFavoriteMapper.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostFavoriteMapper.java
new file mode 100644
index 0000000..96885e8
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostFavoriteMapper.java
@@ -0,0 +1,88 @@
+package com.ruoyi.web.controller.post.mapper;
+
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+
+import com.ruoyi.web.controller.post.domain.PostFavorite;
+
+/**
+ * 帖子收藏Mapper接口
+ *
+ * @author thunderhub
+ */
+public interface PostFavoriteMapper
+{
+ /**
+ * 查询帖子收藏信息
+ *
+ * @param favoriteId 帖子收藏ID
+ * @return 帖子收藏信息
+ */
+ public PostFavorite selectPostFavoriteById(Long favoriteId);
+
+ /**
+ * 查询帖子收藏列表
+ *
+ * @param postFavorite 帖子收藏信息
+ * @return 帖子收藏集合
+ */
+ public List<PostFavorite> selectPostFavoriteList(PostFavorite postFavorite);
+
+ /**
+ * 查询用户是否收藏帖子
+ *
+ * @param postId 帖子ID
+ * @param userId 用户ID
+ * @return 收藏信息
+ */
+ public PostFavorite selectPostFavoriteByPostIdAndUserId(@Param("postId") Long postId, @Param("userId") Long userId);
+
+ /**
+ * 新增帖子收藏
+ *
+ * @param postFavorite 帖子收藏信息
+ * @return 结果
+ */
+ public int insertPostFavorite(PostFavorite postFavorite);
+
+ /**
+ * 修改帖子收藏
+ *
+ * @param postFavorite 帖子收藏信息
+ * @return 结果
+ */
+ public int updatePostFavorite(PostFavorite postFavorite);
+
+ /**
+ * 删除帖子收藏
+ *
+ * @param favoriteId 帖子收藏ID
+ * @return 结果
+ */
+ public int deletePostFavoriteById(Long favoriteId);
+
+ /**
+ * 批量删除帖子收藏
+ *
+ * @param favoriteIds 需要删除的帖子收藏ID
+ * @return 结果
+ */
+ public int deletePostFavoriteByIds(Long[] favoriteIds);
+
+ /**
+ * 取消收藏帖子
+ *
+ * @param postId 帖子ID
+ * @param userId 用户ID
+ * @return 结果
+ */
+ public int cancelPostFavorite(@Param("postId") Long postId, @Param("userId") Long userId);
+
+ /**
+ * 根据帖子ID删除收藏
+ *
+ * @param postId 帖子ID
+ * @return 结果
+ */
+ public int deletePostFavoriteByPostId(Long postId);
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostPaymentMapper.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostPaymentMapper.java
new file mode 100644
index 0000000..538338c
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostPaymentMapper.java
@@ -0,0 +1,76 @@
+package com.ruoyi.web.controller.post.mapper;
+
+import java.util.List;
+import com.ruoyi.web.controller.post.domain.PostPayment;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 帖子支付记录Mapper接口
+ *
+ * @author thunderhub
+ */
+public interface PostPaymentMapper
+{
+ /**
+ * 查询帖子支付记录
+ *
+ * @param paymentId 帖子支付记录ID
+ * @return 帖子支付记录
+ */
+ public PostPayment selectPostPaymentById(Long paymentId);
+
+ /**
+ * 查询帖子支付记录列表
+ *
+ * @param postPayment 帖子支付记录
+ * @return 帖子支付记录集合
+ */
+ public List<PostPayment> selectPostPaymentList(PostPayment postPayment);
+
+ /**
+ * 根据帖子ID查询最新的支付记录
+ *
+ * @param postId 帖子ID
+ * @return 支付记录
+ */
+ public PostPayment selectLatestPaymentByPostId(Long postId);
+
+ /**
+ * 查询过期的推广支付记录
+ *
+ * @return 过期的支付记录列表
+ */
+ public List<PostPayment> selectExpiredPromotionPayments();
+
+ /**
+ * 新增帖子支付记录
+ *
+ * @param postPayment 帖子支付记录
+ * @return 结果
+ */
+ public int insertPostPayment(PostPayment postPayment);
+
+ /**
+ * 修改帖子支付记录
+ *
+ * @param postPayment 帖子支付记录
+ * @return 结果
+ */
+ public int updatePostPayment(PostPayment postPayment);
+
+ /**
+ * 删除帖子支付记录
+ *
+ * @param paymentId 帖子支付记录ID
+ * @return 结果
+ */
+ public int deletePostPaymentById(Long paymentId);
+
+ /**
+ * 批量删除帖子支付记录
+ *
+ * @param paymentIds 需要删除的数据ID
+ * @return 结果
+ */
+ public int deletePostPaymentByIds(Long[] paymentIds);
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostPromotionPlanMapper.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostPromotionPlanMapper.java
new file mode 100644
index 0000000..03f0138
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostPromotionPlanMapper.java
@@ -0,0 +1,60 @@
+package com.ruoyi.web.controller.post.mapper;
+
+import java.util.List;
+import com.ruoyi.web.controller.post.domain.PostPromotionPlan;
+
+/**
+ * 帖子推广计划Mapper接口
+ *
+ * @author thunderhub
+ */
+public interface PostPromotionPlanMapper
+{
+ /**
+ * 查询帖子推广计划
+ *
+ * @param planId 帖子推广计划ID
+ * @return 帖子推广计划
+ */
+ public PostPromotionPlan selectPostPromotionPlanById(Long planId);
+
+ /**
+ * 查询帖子推广计划列表
+ *
+ * @param postPromotionPlan 帖子推广计划
+ * @return 帖子推广计划集合
+ */
+ public List<PostPromotionPlan> selectPostPromotionPlanList(PostPromotionPlan postPromotionPlan);
+
+ /**
+ * 新增帖子推广计划
+ *
+ * @param postPromotionPlan 帖子推广计划
+ * @return 结果
+ */
+ public int insertPostPromotionPlan(PostPromotionPlan postPromotionPlan);
+
+ /**
+ * 修改帖子推广计划
+ *
+ * @param postPromotionPlan 帖子推广计划
+ * @return 结果
+ */
+ public int updatePostPromotionPlan(PostPromotionPlan postPromotionPlan);
+
+ /**
+ * 删除帖子推广计划
+ *
+ * @param planId 帖子推广计划ID
+ * @return 结果
+ */
+ public int deletePostPromotionPlanById(Long planId);
+
+ /**
+ * 批量删除帖子推广计划
+ *
+ * @param planIds 需要删除的数据ID
+ * @return 结果
+ */
+ public int deletePostPromotionPlanByIds(Long[] planIds);
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostReportMapper.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostReportMapper.java
new file mode 100644
index 0000000..0941836
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/mapper/PostReportMapper.java
@@ -0,0 +1,60 @@
+package com.ruoyi.web.controller.post.mapper;
+
+import java.util.List;
+import com.ruoyi.web.controller.post.domain.PostReport;
+
+/**
+ * 帖子举报Mapper接口
+ *
+ * @author thunderhub
+ */
+public interface PostReportMapper
+{
+ /**
+ * 查询帖子举报
+ *
+ * @param reportId 帖子举报ID
+ * @return 帖子举报
+ */
+ public PostReport selectPostReportById(Long reportId);
+
+ /**
+ * 查询帖子举报列表
+ *
+ * @param postReport 帖子举报
+ * @return 帖子举报集合
+ */
+ public List<PostReport> selectPostReportList(PostReport postReport);
+
+ /**
+ * 新增帖子举报
+ *
+ * @param postReport 帖子举报
+ * @return 结果
+ */
+ public int insertPostReport(PostReport postReport);
+
+ /**
+ * 修改帖子举报
+ *
+ * @param postReport 帖子举报
+ * @return 结果
+ */
+ public int updatePostReport(PostReport postReport);
+
+ /**
+ * 删除帖子举报
+ *
+ * @param reportId 帖子举报ID
+ * @return 结果
+ */
+ public int deletePostReportById(Long reportId);
+
+ /**
+ * 批量删除帖子举报
+ *
+ * @param reportIds 需要删除的数据ID
+ * @return 结果
+ */
+ public int deletePostReportByIds(Long[] reportIds);
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/IPostCommentService.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/IPostCommentService.java
new file mode 100644
index 0000000..3ab45cf
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/IPostCommentService.java
@@ -0,0 +1,95 @@
+package com.ruoyi.web.controller.post.service;
+
+import java.util.List;
+
+import com.ruoyi.web.controller.post.domain.PostComment;
+
+/**
+ * 帖子评论 服务层
+ *
+ * @author thunderhub
+ */
+public interface IPostCommentService
+{
+ /**
+ * 查询帖子评论信息
+ *
+ * @param commentId 帖子评论ID
+ * @return 帖子评论信息
+ */
+ public PostComment selectPostCommentById(Long commentId);
+
+ /**
+ * 查询帖子评论列表
+ *
+ * @param postComment 帖子评论信息
+ * @return 帖子评论集合
+ */
+ public List<PostComment> selectPostCommentList(PostComment postComment);
+
+ /**
+ * 根据帖子ID查询评论
+ *
+ * @param postId 帖子ID
+ * @param limit 查询数量,0表示查询全部
+ * @return 帖子评论集合
+ */
+ public List<PostComment> selectCommentsByPostId(Long postId, int limit);
+
+ /**
+ * 根据父评论ID查询回复
+ *
+ * @param parentId 父评论ID
+ * @return 帖子评论集合
+ */
+ public List<PostComment> selectCommentsByParentId(Long parentId);
+
+ /**
+ * 新增帖子评论
+ *
+ * @param postComment 帖子评论信息
+ * @return 结果
+ */
+ public int insertPostComment(PostComment postComment);
+
+ /**
+ * 修改帖子评论
+ *
+ * @param postComment 帖子评论信息
+ * @return 结果
+ */
+ public int updatePostComment(PostComment postComment);
+
+ /**
+ * 删除帖子评论信息
+ *
+ * @param commentId 帖子评论ID
+ * @return 结果
+ */
+ public int deletePostCommentById(Long commentId);
+
+ /**
+ * 批量删除帖子评论信息
+ *
+ * @param commentIds 需要删除的帖子评论ID
+ * @return 结果
+ */
+ public int deletePostCommentByIds(Long[] commentIds);
+
+ /**
+ * 根据帖子ID删除评论
+ *
+ * @param postId 帖子ID
+ * @return 结果
+ */
+ public int deletePostCommentByPostId(Long postId);
+
+ /**
+ * 更新评论点赞数
+ *
+ * @param commentId 评论ID
+ * @param count 增加/减少的数量
+ * @return 结果
+ */
+ public int updateCommentLikes(Long commentId, int count);
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/IPostFavoriteService.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/IPostFavoriteService.java
new file mode 100644
index 0000000..50418fb
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/IPostFavoriteService.java
@@ -0,0 +1,95 @@
+package com.ruoyi.web.controller.post.service;
+
+import java.util.List;
+
+import com.ruoyi.web.controller.post.domain.PostFavorite;
+
+/**
+ * 帖子收藏 服务层
+ *
+ * @author thunderhub
+ */
+public interface IPostFavoriteService
+{
+ /**
+ * 查询帖子收藏信息
+ *
+ * @param favoriteId 帖子收藏ID
+ * @return 帖子收藏信息
+ */
+ public PostFavorite selectPostFavoriteById(Long favoriteId);
+
+ /**
+ * 查询帖子收藏列表
+ *
+ * @param postFavorite 帖子收藏信息
+ * @return 帖子收藏集合
+ */
+ public List<PostFavorite> selectPostFavoriteList(PostFavorite postFavorite);
+
+ /**
+ * 查询用户是否收藏帖子
+ *
+ * @param postId 帖子ID
+ * @param userId 用户ID
+ * @return 收藏信息
+ */
+ public PostFavorite selectPostFavoriteByPostIdAndUserId(Long postId, Long userId);
+
+ /**
+ * 新增帖子收藏
+ *
+ * @param postFavorite 帖子收藏信息
+ * @return 结果
+ */
+ public int insertPostFavorite(PostFavorite postFavorite);
+
+ /**
+ * 修改帖子收藏
+ *
+ * @param postFavorite 帖子收藏信息
+ * @return 结果
+ */
+ public int updatePostFavorite(PostFavorite postFavorite);
+
+ /**
+ * 删除帖子收藏信息
+ *
+ * @param favoriteId 帖子收藏ID
+ * @return 结果
+ */
+ public int deletePostFavoriteById(Long favoriteId);
+
+ /**
+ * 批量删除帖子收藏信息
+ *
+ * @param favoriteIds 需要删除的帖子收藏ID
+ * @return 结果
+ */
+ public int deletePostFavoriteByIds(Long[] favoriteIds);
+
+ /**
+ * 取消收藏帖子
+ *
+ * @param postId 帖子ID
+ * @param userId 用户ID
+ * @return 结果
+ */
+ public int cancelPostFavorite(Long postId, Long userId);
+
+ /**
+ * 根据帖子ID删除收藏
+ *
+ * @param postId 帖子ID
+ * @return 结果
+ */
+ public int deletePostFavoriteByPostId(Long postId);
+
+ /**
+ * 根据用户ID查询收藏列表
+ *
+ * @param userId 用户ID
+ * @return 收藏列表
+ */
+ public List<PostFavorite> selectPostFavoritesByUserId(Long userId);
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/IPostService.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/IPostService.java
new file mode 100644
index 0000000..55ce02f
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/IPostService.java
@@ -0,0 +1,142 @@
+package com.ruoyi.web.controller.post.service;
+
+import java.util.List;
+
+import com.ruoyi.web.controller.post.domain.Post;
+
+/**
+ * 帖子 服务层
+ *
+ * @author thunderhub
+ */
+public interface IPostService
+{
+ /**
+ * 查询帖子信息
+ *
+ * @param postId 帖子ID
+ * @return 帖子信息
+ */
+ public Post selectPostById(Long postId);
+
+ /**
+ * 查询帖子列表
+ *
+ * @param post 帖子信息
+ * @return 帖子集合
+ */
+ public List<Post> selectPostList(Post post);
+
+ /**
+ * 查询作者的其他帖子
+ *
+ * @param authorId 作者ID
+ * @param postId 当前帖子ID(排除)
+ * @param limit 查询数量
+ * @return 帖子集合
+ */
+ public List<Post> selectAuthorOtherPosts(Long authorId, Long postId, int limit);
+
+ /**
+ * 查询相似标签的帖子
+ *
+ * @param tags 标签列表
+ * @param postId 当前帖子ID(排除)
+ * @param limit 查询数量
+ * @return 帖子集合
+ */
+ public List<Post> selectSimilarTagsPosts(String tags, Long postId, int limit);
+
+ /**
+ * 获取帖子推荐列表(包含作者其他帖子和相似标签帖子)
+ *
+ * @param authorId 作者ID
+ * @param tags 标签列表
+ * @param postId 当前帖子ID(排除)
+ * @param maxCount 最大推荐数量
+ * @return 推荐帖子集合
+ */
+ public List<Post> getRecommendedPosts(Long authorId, String tags, Long postId, int maxCount);
+
+ /**
+ * 新增帖子
+ *
+ * @param post 帖子信息
+ * @return 结果
+ */
+ public int insertPost(Post post);
+
+ /**
+ * 修改帖子
+ *
+ * @param post 帖子信息
+ * @return 结果
+ */
+ public int updatePost(Post post);
+
+ /**
+ * 删除帖子信息
+ *
+ * @param postId 帖子ID
+ * @return 结果
+ */
+ public int deletePostById(Long postId);
+
+ /**
+ * 批量删除帖子信息
+ *
+ * @param postIds 需要删除的帖子ID
+ * @return 结果
+ */
+ public int deletePostByIds(Long[] postIds);
+
+ /**
+ * 更新帖子浏览量
+ *
+ * @param postId 帖子ID
+ * @return 结果
+ */
+ public int updatePostViews(Long postId);
+
+ /**
+ * 更新帖子评论数
+ *
+ * @param postId 帖子ID
+ * @param count 增加/减少的数量
+ * @return 结果
+ */
+ public int updatePostComments(Long postId, int count);
+
+ /**
+ * 更新帖子收藏数
+ *
+ * @param postId 帖子ID
+ * @param count 增加/减少的数量
+ * @return 结果
+ */
+ public int updatePostFavorites(Long postId, int count);
+
+ /**
+ * 更新帖子点赞数
+ *
+ * @param postId 帖子ID
+ * @param count 增加/减少的数量
+ * @return 结果
+ */
+ public int updatePostLikes(Long postId, int count);
+
+ /**
+ * 检查并处理过期的推广帖子
+ *
+ * @return 处理的帖子数量
+ */
+ public int checkAndHandleExpiredPromotions();
+
+ /**
+ * 取消帖子推广
+ *
+ * @param postId 帖子ID
+ * @return 结果
+ */
+ public int cancelPostPromotion(Long postId);
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/IPostTagService.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/IPostTagService.java
new file mode 100644
index 0000000..1ec82eb
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/IPostTagService.java
@@ -0,0 +1,103 @@
+package com.ruoyi.web.controller.post.service;
+
+import java.util.List;
+
+import com.ruoyi.web.controller.post.domain.PostTag;
+
+/**
+ * 帖子标签 服务层
+ *
+ * @author thunderhub
+ */
+public interface IPostTagService
+{
+ /**
+ * 查询帖子标签信息
+ *
+ * @param tagId 帖子标签ID
+ * @return 帖子标签信息
+ */
+ public PostTag selectPostTagById(Long tagId);
+
+ /**
+ * 查询帖子标签列表
+ *
+ * @param postTag 帖子标签信息
+ * @return 帖子标签集合
+ */
+ public List<PostTag> selectPostTagList(PostTag postTag);
+
+ /**
+ * 根据帖子ID查询标签
+ *
+ * @param postId 帖子ID
+ * @return 帖子标签集合
+ */
+ public List<PostTag> selectPostTagsByPostId(Long postId);
+
+ /**
+ * 新增帖子标签
+ *
+ * @param postTag 帖子标签信息
+ * @return 结果
+ */
+ public int insertPostTag(PostTag postTag);
+
+ /**
+ * 修改帖子标签
+ *
+ * @param postTag 帖子标签信息
+ * @return 结果
+ */
+ public int updatePostTag(PostTag postTag);
+
+ /**
+ * 删除帖子标签信息
+ *
+ * @param tagId 帖子标签ID
+ * @return 结果
+ */
+ public int deletePostTagById(Long tagId);
+
+ /**
+ * 批量删除帖子标签信息
+ *
+ * @param tagIds 需要删除的帖子标签ID
+ * @return 结果
+ */
+ public int deletePostTagByIds(Long[] tagIds);
+
+ /**
+ * 批量插入帖子标签关联
+ *
+ * @param postId 帖子ID
+ * @param tagIds 标签ID数组
+ * @return 结果
+ */
+ public int batchInsertPostTagRelation(Long postId, Long[] tagIds);
+
+ /**
+ * 删除帖子标签关联
+ *
+ * @param postId 帖子ID
+ * @return 结果
+ */
+ public int deletePostTagRelation(Long postId);
+
+ /**
+ * 更新标签帖子数量
+ *
+ * @param tagId 标签ID
+ * @param count 增加/减少的数量
+ * @return 结果
+ */
+ public int updatePostTagCount(Long tagId, int count);
+
+ /**
+ * 检查标签名称是否唯一
+ *
+ * @param postTag 帖子标签信息
+ * @return 结果
+ */
+ public boolean checkTagNameUnique(PostTag postTag);
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/impl/PostCommentServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/impl/PostCommentServiceImpl.java
new file mode 100644
index 0000000..425d61f
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/impl/PostCommentServiceImpl.java
@@ -0,0 +1,146 @@
+package com.ruoyi.web.controller.post.service.impl;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.ruoyi.web.controller.post.domain.PostComment;
+import com.ruoyi.web.controller.post.mapper.PostCommentMapper;
+import com.ruoyi.web.controller.post.service.IPostCommentService;
+
+/**
+ * 帖子评论 服务层实现
+ *
+ * @author thunderhub
+ */
+@Service
+public class PostCommentServiceImpl implements IPostCommentService
+{
+ @Autowired
+ private PostCommentMapper postCommentMapper;
+
+ /**
+ * 查询帖子评论信息
+ *
+ * @param commentId 帖子评论ID
+ * @return 帖子评论信息
+ */
+ @Override
+ public PostComment selectPostCommentById(Long commentId)
+ {
+ return postCommentMapper.selectPostCommentById(commentId);
+ }
+
+ /**
+ * 查询帖子评论列表
+ *
+ * @param postComment 帖子评论信息
+ * @return 帖子评论集合
+ */
+ @Override
+ public List<PostComment> selectPostCommentList(PostComment postComment)
+ {
+ return postCommentMapper.selectPostCommentList(postComment);
+ }
+
+ /**
+ * 根据帖子ID查询评论
+ *
+ * @param postId 帖子ID
+ * @param limit 查询数量,0表示查询全部
+ * @return 帖子评论集合
+ */
+ @Override
+ public List<PostComment> selectCommentsByPostId(Long postId, int limit)
+ {
+ return postCommentMapper.selectCommentsByPostId(postId, limit);
+ }
+
+ /**
+ * 根据父评论ID查询回复
+ *
+ * @param parentId 父评论ID
+ * @return 帖子评论集合
+ */
+ @Override
+ public List<PostComment> selectCommentsByParentId(Long parentId)
+ {
+ return postCommentMapper.selectCommentsByParentId(parentId);
+ }
+
+ /**
+ * 新增帖子评论
+ *
+ * @param postComment 帖子评论信息
+ * @return 结果
+ */
+ @Override
+ public int insertPostComment(PostComment postComment)
+ {
+ return postCommentMapper.insertPostComment(postComment);
+ }
+
+ /**
+ * 修改帖子评论
+ *
+ * @param postComment 帖子评论信息
+ * @return 结果
+ */
+ @Override
+ public int updatePostComment(PostComment postComment)
+ {
+ return postCommentMapper.updatePostComment(postComment);
+ }
+
+ /**
+ * 删除帖子评论信息
+ *
+ * @param commentId 帖子评论ID
+ * @return 结果
+ */
+ @Override
+ public int deletePostCommentById(Long commentId)
+ {
+ return postCommentMapper.deletePostCommentById(commentId);
+ }
+
+ /**
+ * 批量删除帖子评论信息
+ *
+ * @param commentIds 需要删除的帖子评论ID
+ * @return 结果
+ */
+ @Override
+ public int deletePostCommentByIds(Long[] commentIds)
+ {
+ return postCommentMapper.deletePostCommentByIds(commentIds);
+ }
+
+ /**
+ * 根据帖子ID删除评论
+ *
+ * @param postId 帖子ID
+ * @return 结果
+ */
+ @Override
+ public int deletePostCommentByPostId(Long postId)
+ {
+ return postCommentMapper.deletePostCommentByPostId(postId);
+ }
+
+ /**
+ * 更新评论点赞数
+ *
+ * @param commentId 评论ID
+ * @param count 增加/减少的数量
+ * @return 结果
+ */
+ @Override
+ public int updateCommentLikes(Long commentId, int count)
+ {
+ PostComment comment = new PostComment();
+ comment.setCommentId(commentId);
+ comment.setLikes((long) count);
+ return postCommentMapper.updateCommentLikes(comment);
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/impl/PostFavoriteServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/impl/PostFavoriteServiceImpl.java
new file mode 100644
index 0000000..9249a94
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/impl/PostFavoriteServiceImpl.java
@@ -0,0 +1,145 @@
+package com.ruoyi.web.controller.post.service.impl;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.ruoyi.web.controller.post.domain.PostFavorite;
+import com.ruoyi.web.controller.post.mapper.PostFavoriteMapper;
+import com.ruoyi.web.controller.post.service.IPostFavoriteService;
+
+/**
+ * 帖子收藏 服务层实现
+ *
+ * @author thunderhub
+ */
+@Service
+public class PostFavoriteServiceImpl implements IPostFavoriteService
+{
+ @Autowired
+ private PostFavoriteMapper postFavoriteMapper;
+
+ /**
+ * 查询帖子收藏信息
+ *
+ * @param favoriteId 帖子收藏ID
+ * @return 帖子收藏信息
+ */
+ @Override
+ public PostFavorite selectPostFavoriteById(Long favoriteId)
+ {
+ return postFavoriteMapper.selectPostFavoriteById(favoriteId);
+ }
+
+ /**
+ * 查询帖子收藏列表
+ *
+ * @param postFavorite 帖子收藏信息
+ * @return 帖子收藏集合
+ */
+ @Override
+ public List<PostFavorite> selectPostFavoriteList(PostFavorite postFavorite)
+ {
+ return postFavoriteMapper.selectPostFavoriteList(postFavorite);
+ }
+
+ /**
+ * 查询用户是否收藏帖子
+ *
+ * @param postId 帖子ID
+ * @param userId 用户ID
+ * @return 收藏信息
+ */
+ @Override
+ public PostFavorite selectPostFavoriteByPostIdAndUserId(Long postId, Long userId)
+ {
+ return postFavoriteMapper.selectPostFavoriteByPostIdAndUserId(postId, userId);
+ }
+
+ /**
+ * 新增帖子收藏
+ *
+ * @param postFavorite 帖子收藏信息
+ * @return 结果
+ */
+ @Override
+ public int insertPostFavorite(PostFavorite postFavorite)
+ {
+ return postFavoriteMapper.insertPostFavorite(postFavorite);
+ }
+
+ /**
+ * 修改帖子收藏
+ *
+ * @param postFavorite 帖子收藏信息
+ * @return 结果
+ */
+ @Override
+ public int updatePostFavorite(PostFavorite postFavorite)
+ {
+ return postFavoriteMapper.updatePostFavorite(postFavorite);
+ }
+
+ /**
+ * 删除帖子收藏信息
+ *
+ * @param favoriteId 帖子收藏ID
+ * @return 结果
+ */
+ @Override
+ public int deletePostFavoriteById(Long favoriteId)
+ {
+ return postFavoriteMapper.deletePostFavoriteById(favoriteId);
+ }
+
+ /**
+ * 批量删除帖子收藏信息
+ *
+ * @param favoriteIds 需要删除的帖子收藏ID
+ * @return 结果
+ */
+ @Override
+ public int deletePostFavoriteByIds(Long[] favoriteIds)
+ {
+ return postFavoriteMapper.deletePostFavoriteByIds(favoriteIds);
+ }
+
+ /**
+ * 取消收藏帖子
+ *
+ * @param postId 帖子ID
+ * @param userId 用户ID
+ * @return 结果
+ */
+ @Override
+ public int cancelPostFavorite(Long postId, Long userId)
+ {
+ return postFavoriteMapper.cancelPostFavorite(postId, userId);
+ }
+
+ /**
+ * 根据帖子ID删除收藏
+ *
+ * @param postId 帖子ID
+ * @return 结果
+ */
+ @Override
+ public int deletePostFavoriteByPostId(Long postId)
+ {
+ return postFavoriteMapper.deletePostFavoriteByPostId(postId);
+ }
+
+ /**
+ * 根据用户ID查询收藏列表
+ *
+ * @param userId 用户ID
+ * @return 收藏列表
+ */
+ @Override
+ public List<PostFavorite> selectPostFavoritesByUserId(Long userId)
+ {
+ PostFavorite postFavorite = new PostFavorite();
+ postFavorite.setUserId(userId);
+ return postFavoriteMapper.selectPostFavoriteList(postFavorite);
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/impl/PostServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/impl/PostServiceImpl.java
new file mode 100644
index 0000000..c619f96
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/impl/PostServiceImpl.java
@@ -0,0 +1,311 @@
+package com.ruoyi.web.controller.post.service.impl;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.web.controller.post.mapper.PostContentMapper;
+import com.ruoyi.web.controller.post.mapper.PostPaymentMapper;
+import com.ruoyi.web.controller.post.mapper.PostPromotionPlanMapper;
+import com.ruoyi.web.controller.post.domain.Post;
+import com.ruoyi.web.controller.post.domain.PostPayment;
+import com.ruoyi.web.controller.post.domain.PostPromotionPlan;
+import com.ruoyi.web.controller.post.service.IPostService;
+
+import java.util.Date;
+import java.util.Calendar;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.util.ArrayList;
+
+/**
+ * 帖子Service业务层处理
+ *
+ * @author thunderhub
+ */
+@Service
+public class PostServiceImpl implements IPostService
+{
+ @Autowired
+ private PostContentMapper postMapper;
+
+ @Autowired
+ private PostPaymentMapper postPaymentMapper;
+
+ @Autowired
+ private PostPromotionPlanMapper postPromotionPlanMapper;
+
+ /**
+ * 查询帖子信息
+ *
+ * @param postId 帖子ID
+ * @return 帖子信息
+ */
+ @Override
+ public Post selectPostById(Long postId)
+ {
+ return postMapper.selectPostById(postId);
+ }
+
+ /**
+ * 查询帖子列表
+ *
+ * @param post 帖子信息
+ * @return 帖子集合
+ */
+ @Override
+ public List<Post> selectPostList(Post post)
+ {
+ return postMapper.selectPostList(post);
+ }
+
+ /**
+ * 查询作者的其他帖子
+ *
+ * @param authorId 作者ID
+ * @param postId 当前帖子ID(排除)
+ * @param limit 查询数量
+ * @return 帖子集合
+ */
+ @Override
+ public List<Post> selectAuthorOtherPosts(Long authorId, Long postId, int limit)
+ {
+ return postMapper.selectAuthorOtherPosts(authorId, postId, limit);
+ }
+
+ /**
+ * 查询相似标签的帖子
+ *
+ * @param tags 标签列表
+ * @param postId 当前帖子ID(排除)
+ * @param limit 查询数量
+ * @return 帖子集合
+ */
+ @Override
+ public List<Post> selectSimilarTagsPosts(String tags, Long postId, int limit)
+ {
+ if (tags == null || tags.trim().isEmpty()) {
+ return List.of();
+ }
+
+ return postMapper.selectSimilarTagsPosts(tags, postId, limit);
+ }
+
+ /**
+ * 新增帖子
+ *
+ * @param post 帖子信息
+ * @return 结果
+ */
+ @Override
+ public int insertPost(Post post)
+ {
+ return postMapper.insertPost(post);
+ }
+
+ /**
+ * 修改帖子
+ *
+ * @param post 帖子信息
+ * @return 结果
+ */
+ @Override
+ public int updatePost(Post post)
+ {
+ return postMapper.updatePost(post);
+ }
+
+ /**
+ * 删除帖子信息
+ *
+ * @param postId 帖子ID
+ * @return 结果
+ */
+ @Override
+ public int deletePostById(Long postId)
+ {
+ return postMapper.deletePostById(postId);
+ }
+
+ /**
+ * 批量删除帖子信息
+ *
+ * @param postIds 需要删除的帖子ID
+ * @return 结果
+ */
+ @Override
+ public int deletePostByIds(Long[] postIds)
+ {
+ return postMapper.deletePostByIds(postIds);
+ }
+
+ /**
+ * 更新帖子浏览量
+ *
+ * @param postId 帖子ID
+ * @return 结果
+ */
+ @Override
+ public int updatePostViews(Long postId)
+ {
+ return postMapper.updatePostViews(postId);
+ }
+
+ /**
+ * 更新帖子评论数
+ *
+ * @param postId 帖子ID
+ * @param count 增加/减少的数量
+ * @return 结果
+ */
+ @Override
+ public int updatePostComments(Long postId, int count)
+ {
+ Post post = new Post();
+ post.setPostId(postId);
+ post.setComments((long) count);
+ return postMapper.updatePostComments(post);
+ }
+
+ /**
+ * 更新帖子收藏数
+ *
+ * @param postId 帖子ID
+ * @param count 增加/减少的数量
+ * @return 结果
+ */
+ @Override
+ public int updatePostFavorites(Long postId, int count)
+ {
+ Post post = new Post();
+ post.setPostId(postId);
+ post.setFavorites((long) count);
+ return postMapper.updatePostFavorites(post);
+ }
+
+ /**
+ * 更新帖子点赞数
+ *
+ * @param postId 帖子ID
+ * @param count 增加/减少的数量
+ * @return 结果
+ */
+ @Override
+ public int updatePostLikes(Long postId, int count)
+ {
+ Post post = new Post();
+ post.setPostId(postId);
+ post.setLikes((long) count);
+ return postMapper.updatePostLikes(post);
+ }
+
+ /**
+ * 检查并处理过期的推广帖子
+ *
+ * @return 处理的帖子数量
+ */
+ @Override
+ public int checkAndHandleExpiredPromotions()
+ {
+ Logger logger = LoggerFactory.getLogger(PostServiceImpl.class);
+ int processedCount = 0;
+
+ try {
+ // 查询所有过期的推广支付记录
+ List<PostPayment> expiredPayments = postPaymentMapper.selectExpiredPromotionPayments();
+
+ for (PostPayment payment : expiredPayments) {
+ // 获取推广计划信息
+ PostPromotionPlan plan = postPromotionPlanMapper.selectPostPromotionPlanById(payment.getPlanId());
+ if (plan == null) {
+ continue;
+ }
+
+ // 计算推广到期时间
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(payment.getPaymentTime());
+ cal.add(Calendar.DAY_OF_MONTH, plan.getDuration());
+ Date expireTime = cal.getTime();
+
+ // 检查是否已过期
+ if (new Date().after(expireTime)) {
+ // 取消帖子推广
+ int result = cancelPostPromotion(payment.getPostId());
+ if (result > 0) {
+ processedCount++;
+ logger.info("帖子ID: {} 的推广已过期,已自动取消", payment.getPostId());
+ }
+ }
+ }
+ } catch (Exception e) {
+ logger.error("检查推广时效性时发生错误", e);
+ }
+
+ return processedCount;
+ }
+
+ /**
+ * 取消帖子推广
+ *
+ * @param postId 帖子ID
+ * @return 结果
+ */
+ @Override
+ public int cancelPostPromotion(Long postId)
+ {
+ Post post = new Post();
+ post.setPostId(postId);
+ post.setPromotionPlanId(null);
+ return postMapper.updatePost(post);
+ }
+
+ /**
+ * 获取帖子推荐列表(包含作者其他帖子和相似标签帖子)
+ *
+ * @param authorId 作者ID
+ * @param tags 标签列表
+ * @param postId 当前帖子ID(排除)
+ * @param maxCount 最大推荐数量
+ * @return 推荐帖子集合
+ */
+ @Override
+ public List<Post> getRecommendedPosts(Long authorId, String tags, Long postId, int maxCount)
+ {
+ List<Post> recommendedPosts = new ArrayList<>();
+
+ // 1. 获取作者的其他帖子(最多5个)
+ List<Post> authorPosts = selectAuthorOtherPosts(authorId, postId, 5);
+ recommendedPosts.addAll(authorPosts);
+
+ // 2. 如果作者帖子不足maxCount个,则补充相似标签的帖子
+ if (recommendedPosts.size() < maxCount && tags != null && !tags.trim().isEmpty()) {
+ int remainingCount = maxCount - recommendedPosts.size();
+ List<Post> similarPosts = selectSimilarTagsPosts(tags, postId, remainingCount + 5); // 多查询一些以便去重
+
+ // 去重:排除已经在推荐列表中的帖子
+ for (Post similarPost : similarPosts) {
+ if (recommendedPosts.size() >= maxCount) {
+ break;
+ }
+
+ boolean isDuplicate = false;
+ for (Post existingPost : recommendedPosts) {
+ if (existingPost.getPostId().equals(similarPost.getPostId())) {
+ isDuplicate = true;
+ break;
+ }
+ }
+
+ if (!isDuplicate) {
+ recommendedPosts.add(similarPost);
+ }
+ }
+ }
+
+ // 3. 限制最大数量
+ if (recommendedPosts.size() > maxCount) {
+ recommendedPosts = recommendedPosts.subList(0, maxCount);
+ }
+
+ return recommendedPosts;
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/impl/PostTagServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/impl/PostTagServiceImpl.java
new file mode 100644
index 0000000..3be6dcc
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/service/impl/PostTagServiceImpl.java
@@ -0,0 +1,176 @@
+package com.ruoyi.web.controller.post.service.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.ruoyi.web.controller.post.domain.PostTag;
+import com.ruoyi.web.controller.post.domain.PostTagRelation;
+import com.ruoyi.web.controller.post.mapper.PostContentTagMapper;
+import com.ruoyi.web.controller.post.service.IPostTagService;
+
+/**
+ * 帖子标签 服务层实现
+ *
+ * @author thunderhub
+ */
+@Service
+public class PostTagServiceImpl implements IPostTagService
+{
+ @Autowired
+ private PostContentTagMapper postContentTagMapper;
+
+ /**
+ * 查询帖子标签信息
+ *
+ * @param tagId 帖子标签ID
+ * @return 帖子标签信息
+ */
+ @Override
+ public PostTag selectPostTagById(Long tagId)
+ {
+ return postContentTagMapper.selectPostTagById(tagId);
+ }
+
+ /**
+ * 查询帖子标签列表
+ *
+ * @param postTag 帖子标签信息
+ * @return 帖子标签集合
+ */
+ @Override
+ public List<PostTag> selectPostTagList(PostTag postTag)
+ {
+ return postContentTagMapper.selectPostTagList(postTag);
+ }
+
+ /**
+ * 根据帖子ID查询标签
+ *
+ * @param postId 帖子ID
+ * @return 帖子标签集合
+ */
+ @Override
+ public List<PostTag> selectPostTagsByPostId(Long postId)
+ {
+ return postContentTagMapper.selectPostTagsByPostId(postId);
+ }
+
+ /**
+ * 新增帖子标签
+ *
+ * @param postTag 帖子标签信息
+ * @return 结果
+ */
+ @Override
+ public int insertPostTag(PostTag postTag)
+ {
+ return postContentTagMapper.insertPostTag(postTag);
+ }
+
+ /**
+ * 修改帖子标签
+ *
+ * @param postTag 帖子标签信息
+ * @return 结果
+ */
+ @Override
+ public int updatePostTag(PostTag postTag)
+ {
+ return postContentTagMapper.updatePostTag(postTag);
+ }
+
+ /**
+ * 删除帖子标签信息
+ *
+ * @param tagId 帖子标签ID
+ * @return 结果
+ */
+ @Override
+ public int deletePostTagById(Long tagId)
+ {
+ return postContentTagMapper.deletePostTagById(tagId);
+ }
+
+ /**
+ * 批量删除帖子标签信息
+ *
+ * @param tagIds 需要删除的帖子标签ID
+ * @return 结果
+ */
+ @Override
+ public int deletePostTagByIds(Long[] tagIds)
+ {
+ return postContentTagMapper.deletePostTagByIds(tagIds);
+ }
+
+ /**
+ * 批量插入帖子标签关联
+ *
+ * @param postId 帖子ID
+ * @param tagIds 标签ID数组
+ * @return 结果
+ */
+ @Override
+ @Transactional
+ public int batchInsertPostTagRelation(Long postId, Long[] tagIds)
+ {
+ List<PostTagRelation> list = new ArrayList<PostTagRelation>();
+ for (Long tagId : tagIds)
+ {
+ PostTagRelation relation = new PostTagRelation();
+ relation.setPostId(postId);
+ relation.setTagId(tagId);
+ list.add(relation);
+ }
+ return postContentTagMapper.batchInsertPostTagRelation(list);
+ }
+
+ /**
+ * 删除帖子标签关联
+ *
+ * @param postId 帖子ID
+ * @return 结果
+ */
+ @Override
+ public int deletePostTagRelation(Long postId)
+ {
+ return postContentTagMapper.deletePostTagRelation(postId);
+ }
+
+ /**
+ * 更新标签帖子数量
+ *
+ * @param tagId 标签ID
+ * @param count 增加/减少的数量
+ * @return 结果
+ */
+ @Override
+ public int updatePostTagCount(Long tagId, int count)
+ {
+ PostTag postTag = new PostTag();
+ postTag.setTagId(tagId);
+ postTag.setPostCount((long) count);
+ return postContentTagMapper.updatePostTagCount(postTag);
+ }
+
+ /**
+ * 检查标签名称是否唯一
+ *
+ * @param postTag 帖子标签信息
+ * @return 结果
+ */
+ @Override
+ public boolean checkTagNameUnique(PostTag postTag)
+ {
+ Long tagId = postTag.getTagId() == null ? -1L : postTag.getTagId();
+ PostTag info = postContentTagMapper.checkTagNameUnique(postTag.getTagName());
+ if (info != null && info.getTagId().longValue() != tagId.longValue())
+ {
+ return false;
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/task/PromotionExpiryTask.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/task/PromotionExpiryTask.java
new file mode 100644
index 0000000..39855e7
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/post/task/PromotionExpiryTask.java
@@ -0,0 +1,49 @@
+package com.ruoyi.web.controller.post.task;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import com.ruoyi.web.controller.post.service.IPostService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 推广时效性检查定时任务
+ *
+ * @author thunderhub
+ */
+@Component
+public class PromotionExpiryTask
+{
+ private static final Logger logger = LoggerFactory.getLogger(PromotionExpiryTask.class);
+
+ @Autowired
+ private IPostService postService;
+
+ /**
+ * 检查推广时效性
+ * 每小时执行一次
+ */
+ @Scheduled(cron = "0 0 * * * ?")
+ public void checkPromotionExpiry()
+ {
+ logger.info("开始检查推广时效性...");
+
+ try {
+ int processedCount = postService.checkAndHandleExpiredPromotions();
+ logger.info("推广时效性检查完成,处理了 {} 个过期推广", processedCount);
+ } catch (Exception e) {
+ logger.error("推广时效性检查失败", e);
+ }
+ }
+
+ /**
+ * 手动触发推广时效性检查
+ * 用于测试或手动执行
+ */
+ public void manualCheckPromotionExpiry()
+ {
+ logger.info("手动触发推广时效性检查...");
+ checkPromotionExpiry();
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/resources/application-druid.yml b/ruoyi-admin/src/main/resources/application-druid.yml
index ffe92e5..1eb2c2f 100644
--- a/ruoyi-admin/src/main/resources/application-druid.yml
+++ b/ruoyi-admin/src/main/resources/application-druid.yml
@@ -6,9 +6,9 @@
druid:
# 主库数据源
master:
- url: jdbc:mysql://rm-bp1v264vgxpa95ferbo.mysql.rds.aliyuncs.com:3306/jrx?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
- username: root
- password: Jrx5201314
+ url: jdbc:mysql://202.205.102.121:3306/ThunderHub?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+ username: team4
+ password: woaizhaoyutao
# 从库数据源
slave:
# 从数据源开关/默认关闭
diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml
index 0333eba..f80b3fd 100644
--- a/ruoyi-admin/src/main/resources/application.yml
+++ b/ruoyi-admin/src/main/resources/application.yml
@@ -1,15 +1,18 @@
# 项目相关配置
ruoyi:
# 名称
- name: TdHub
+ name: ThunderHub
# 版本
- version: 3.8.8
+ version: 1.0.0
# 版权年份
copyrightYear: 2024
- profile: ./download
+ # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
+ profile: ./upload
+ # 前端项目图片路径(相对于后端项目根目录)
+ frontendImagePath: ../ThunderHubWeb/public/images
# 获取ip地址开关
addressEnabled: false
- # 验证码类型 math 数字计算 char 字符验证
+ # 验证码类型 math 数组计算 char 字符验证
captchaType: math
# 开发环境配置
@@ -52,6 +55,12 @@
basename: i18n/messages
profiles:
active: druid
+ # 静态资源映射
+ mvc:
+ static-path-pattern: /static/**
+ web:
+ resources:
+ static-locations: classpath:/static/
# 文件上传
servlet:
multipart:
diff --git a/ruoyi-admin/src/main/resources/logback.xml b/ruoyi-admin/src/main/resources/logback.xml
index a360583..a8c526e 100644
--- a/ruoyi-admin/src/main/resources/logback.xml
+++ b/ruoyi-admin/src/main/resources/logback.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志存放路径 -->
- <property name="log.path" value="/home/ruoyi/logs" />
+ <property name="log.path" value="./logs" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
diff --git a/ruoyi-admin/src/main/resources/mapper/post/PostCommentMapper.xml b/ruoyi-admin/src/main/resources/mapper/post/PostCommentMapper.xml
new file mode 100644
index 0000000..537dd4c
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/mapper/post/PostCommentMapper.xml
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.web.controller.post.mapper.PostCommentMapper">
+
+ <resultMap type="PostComment" id="PostCommentResult">
+ <id property="commentId" column="comment_id" />
+ <result property="postId" column="post_id" />
+ <result property="content" column="content" />
+ <result property="userId" column="user_id" />
+ <result property="userName" column="user_name" />
+ <result property="userAvatar" column="user_avatar" />
+ <result property="parentId" column="parent_id" />
+ <result property="replyUserId" column="reply_user_id" />
+ <result property="replyUserName" column="reply_user_name"/>
+ <result property="status" column="status" />
+ <result property="likes" column="likes" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ <result property="remark" column="remark" />
+ </resultMap>
+
+ <sql id="selectPostCommentVo">
+ select comment_id, post_id, content, user_id, user_name, user_avatar, parent_id, reply_user_id, reply_user_name, status, likes,
+ create_by, create_time, update_by, update_time, remark
+ from post_comment
+ </sql>
+
+ <select id="selectPostCommentList" parameterType="PostComment" resultMap="PostCommentResult">
+ <include refid="selectPostCommentVo"/>
+ <where>
+ <if test="postId != null">
+ AND post_id = #{postId}
+ </if>
+ <if test="userId != null">
+ AND user_id = #{userId}
+ </if>
+ <if test="userName != null and userName != ''">
+ AND user_name like concat('%', #{userName}, '%')
+ </if>
+ <if test="parentId != null">
+ AND parent_id = #{parentId}
+ </if>
+ <if test="status != null and status != ''">
+ AND status = #{status}
+ </if>
+ </where>
+ order by create_time desc
+ </select>
+
+ <select id="selectPostCommentById" parameterType="Long" resultMap="PostCommentResult">
+ <include refid="selectPostCommentVo"/>
+ where comment_id = #{commentId}
+ </select>
+
+ <select id="selectCommentsByPostId" resultMap="PostCommentResult">
+ <include refid="selectPostCommentVo"/>
+ where post_id = #{postId} and parent_id = 0 and status = '1'
+ order by create_time desc
+ <if test="limit > 0">
+ limit #{limit}
+ </if>
+ </select>
+
+ <select id="selectCommentsByParentId" parameterType="Long" resultMap="PostCommentResult">
+ <include refid="selectPostCommentVo"/>
+ where parent_id = #{parentId} and status = '1'
+ order by create_time asc
+ </select>
+
+ <insert id="insertPostComment" parameterType="PostComment" useGeneratedKeys="true" keyProperty="commentId">
+ insert into post_comment (
+ post_id,
+ content,
+ user_id,
+ user_name,
+ user_avatar,
+ parent_id,
+ reply_user_id,
+ reply_user_name,
+ status,
+ likes,
+ create_by,
+ create_time,
+ update_by,
+ update_time,
+ remark
+ ) values (
+ #{postId},
+ #{content},
+ #{userId},
+ #{userName},
+ #{userAvatar},
+ #{parentId},
+ #{replyUserId},
+ #{replyUserName},
+ #{status},
+ #{likes},
+ #{createBy},
+ now(),
+ #{updateBy},
+ now(),
+ #{remark}
+ )
+ </insert>
+
+ <update id="updatePostComment" parameterType="PostComment">
+ update post_comment
+ <set>
+ <if test="content != null and content != ''">content = #{content},</if>
+ <if test="status != null and status != ''">status = #{status},</if>
+ <if test="likes != null">likes = #{likes},</if>
+ <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+ update_time = now(),
+ <if test="remark != null">remark = #{remark},</if>
+ </set>
+ where comment_id = #{commentId}
+ </update>
+
+ <delete id="deletePostCommentById" parameterType="Long">
+ delete from post_comment where comment_id = #{commentId}
+ </delete>
+
+ <delete id="deletePostCommentByIds" parameterType="Long">
+ delete from post_comment where comment_id in
+ <foreach collection="array" item="commentId" open="(" separator="," close=")">
+ #{commentId}
+ </foreach>
+ </delete>
+
+ <delete id="deletePostCommentByPostId" parameterType="Long">
+ delete from post_comment where post_id = #{postId}
+ </delete>
+
+ <update id="updateCommentLikes" parameterType="PostComment">
+ update post_comment set likes = likes + #{likes} where comment_id = #{commentId}
+ </update>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/resources/mapper/post/PostFavoriteMapper.xml b/ruoyi-admin/src/main/resources/mapper/post/PostFavoriteMapper.xml
new file mode 100644
index 0000000..a79ee9b
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/mapper/post/PostFavoriteMapper.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.web.controller.post.mapper.PostFavoriteMapper">
+
+ <resultMap type="PostFavorite" id="PostFavoriteResult">
+ <id property="favoriteId" column="favorite_id" />
+ <result property="postId" column="post_id" />
+ <result property="userId" column="user_id" />
+ <result property="postTitle" column="post_title" />
+ <result property="postCover" column="post_cover" />
+ <result property="status" column="status" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ <result property="remark" column="remark" />
+ </resultMap>
+
+ <sql id="selectPostFavoriteVo">
+ select favorite_id, post_id, user_id, post_title, post_cover, status,
+ create_by, create_time, update_by, update_time, remark
+ from post_favorite
+ </sql>
+
+ <select id="selectPostFavoriteList" parameterType="PostFavorite" resultMap="PostFavoriteResult">
+ <include refid="selectPostFavoriteVo"/>
+ <where>
+ <if test="postId != null">
+ AND post_id = #{postId}
+ </if>
+ <if test="userId != null">
+ AND user_id = #{userId}
+ </if>
+ <if test="status != null and status != ''">
+ AND status = #{status}
+ </if>
+ </where>
+ order by create_time desc
+ </select>
+
+ <select id="selectPostFavoriteById" parameterType="Long" resultMap="PostFavoriteResult">
+ <include refid="selectPostFavoriteVo"/>
+ where favorite_id = #{favoriteId}
+ </select>
+
+ <select id="selectPostFavoriteByPostIdAndUserId" resultMap="PostFavoriteResult">
+ <include refid="selectPostFavoriteVo"/>
+ where post_id = #{postId} and user_id = #{userId}
+ </select>
+
+ <insert id="insertPostFavorite" parameterType="PostFavorite" useGeneratedKeys="true" keyProperty="favoriteId">
+ insert into post_favorite (
+ post_id,
+ user_id,
+ post_title,
+ post_cover,
+ status,
+ create_by,
+ create_time,
+ update_by,
+ update_time,
+ remark
+ ) values (
+ #{postId},
+ #{userId},
+ #{postTitle},
+ #{postCover},
+ #{status},
+ #{createBy},
+ now(),
+ #{updateBy},
+ now(),
+ #{remark}
+ )
+ </insert>
+
+ <update id="updatePostFavorite" parameterType="PostFavorite">
+ update post_favorite
+ <set>
+ <if test="status != null and status != ''">status = #{status},</if>
+ <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+ update_time = now(),
+ <if test="remark != null">remark = #{remark},</if>
+ </set>
+ where favorite_id = #{favoriteId}
+ </update>
+
+ <delete id="deletePostFavoriteById" parameterType="Long">
+ delete from post_favorite where favorite_id = #{favoriteId}
+ </delete>
+
+ <delete id="deletePostFavoriteByIds" parameterType="Long">
+ delete from post_favorite where favorite_id in
+ <foreach collection="array" item="favoriteId" open="(" separator="," close=")">
+ #{favoriteId}
+ </foreach>
+ </delete>
+
+ <delete id="deletePostFavoriteByPostId" parameterType="Long">
+ delete from post_favorite where post_id = #{postId}
+ </delete>
+
+ <update id="cancelPostFavorite">
+ update post_favorite set status = '1', update_time = now()
+ where post_id = #{postId} and user_id = #{userId}
+ </update>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/resources/mapper/post/PostMapper.xml b/ruoyi-admin/src/main/resources/mapper/post/PostMapper.xml
new file mode 100644
index 0000000..5254e6f
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/mapper/post/PostMapper.xml
@@ -0,0 +1,172 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.web.controller.post.mapper.PostContentMapper">
+
+ <resultMap type="Post" id="PostResult">
+ <id property="postId" column="post_id" />
+ <result property="title" column="title" />
+ <result property="content" column="content" />
+ <result property="summary" column="summary" />
+ <result property="coverImage" column="cover_image" />
+ <result property="authorId" column="author_id" />
+ <result property="author" column="author" />
+ <result property="views" column="views" />
+ <result property="comments" column="comments" />
+ <result property="favorites" column="favorites" />
+ <result property="likes" column="likes" />
+ <result property="status" column="status" />
+ <result property="publishTime" column="publish_time" />
+ <result property="tags" column="tags" />
+ <result property="promotionPlanId" column="promotion_plan_id"/>
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ <result property="remark" column="remark" />
+ </resultMap>
+
+ <sql id="selectPostVo">
+ select post_id, title, content, summary, cover_image, author_id, author, views, comments, favorites, likes,
+ status, publish_time, tags, promotion_plan_id, create_by, create_time, update_by, update_time, remark
+ from post
+ </sql>
+
+ <select id="selectPostList" parameterType="Post" resultMap="PostResult">
+ <include refid="selectPostVo"/>
+ <where>
+ <if test="title != null and title != ''">
+ AND title like concat('%', #{title}, '%')
+ </if>
+ <if test="author != null and author != ''">
+ AND author like concat('%', #{author}, '%')
+ </if>
+ <if test="status != null and status != ''">
+ AND status = #{status}
+ </if>
+ <if test="tags != null and tags != ''">
+ AND tags like concat('%', #{tags}, '%')
+ </if>
+ <if test="authorId != null">
+ AND author_id = #{authorId}
+ </if>
+ </where>
+ order by
+ case when promotion_plan_id is not null then 0 else 1 end,
+ create_time desc
+ </select>
+
+ <select id="selectPostById" parameterType="Long" resultMap="PostResult">
+ <include refid="selectPostVo"/>
+ where post_id = #{postId}
+ </select>
+
+ <select id="selectAuthorOtherPosts" resultMap="PostResult">
+ <include refid="selectPostVo"/>
+ where author_id = #{authorId} and post_id != #{postId} and status = '1'
+ order by create_time desc
+ limit #{limit}
+ </select>
+
+ <select id="selectSimilarTagsPosts" resultMap="PostResult">
+ <include refid="selectPostVo"/>
+ <where>
+ post_id != #{postId} and status = '1'
+ <if test="tags != null and tags != ''">
+ AND tags like concat('%', #{tags}, '%')
+ </if>
+ </where>
+ order by create_time desc
+ limit #{limit}
+ </select>
+
+ <insert id="insertPost" parameterType="Post" useGeneratedKeys="true" keyProperty="postId">
+ insert into post (
+ title,
+ content,
+ summary,
+ cover_image,
+ author_id,
+ author,
+ views,
+ comments,
+ favorites,
+ likes,
+ status,
+ publish_time,
+ tags,
+ promotion_plan_id,
+ create_by,
+ create_time,
+ update_by,
+ update_time,
+ remark
+ ) values (
+ #{title},
+ #{content},
+ #{summary},
+ #{coverImage},
+ #{authorId},
+ #{author},
+ #{views},
+ #{comments},
+ #{favorites},
+ #{likes},
+ #{status},
+ #{publishTime},
+ #{tags},
+ #{promotionPlanId},
+ #{createBy},
+ now(),
+ #{updateBy},
+ now(),
+ #{remark}
+ )
+ </insert>
+
+ <update id="updatePost" parameterType="Post">
+ update post
+ <set>
+ <if test="title != null and title != ''">title = #{title},</if>
+ <if test="content != null">content = #{content},</if>
+ <if test="summary != null">summary = #{summary},</if>
+ <if test="coverImage != null">cover_image = #{coverImage},</if>
+ <if test="status != null and status != ''">status = #{status},</if>
+ <if test="tags != null">tags = #{tags},</if>
+ <if test="promotionPlanId != null">promotion_plan_id = #{promotionPlanId},</if>
+ <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+ update_time = now(),
+ <if test="remark != null">remark = #{remark},</if>
+ </set>
+ where post_id = #{postId}
+ </update>
+
+ <delete id="deletePostById" parameterType="Long">
+ delete from post where post_id = #{postId}
+ </delete>
+
+ <delete id="deletePostByIds" parameterType="Long">
+ delete from post where post_id in
+ <foreach collection="array" item="postId" open="(" separator="," close=")">
+ #{postId}
+ </foreach>
+ </delete>
+
+ <update id="updatePostViews" parameterType="Long">
+ update post set views = views + 1 where post_id = #{postId}
+ </update>
+
+ <update id="updatePostComments" parameterType="Post">
+ update post set comments = comments + #{comments} where post_id = #{postId}
+ </update>
+
+ <update id="updatePostFavorites" parameterType="Post">
+ update post set favorites = favorites + #{favorites} where post_id = #{postId}
+ </update>
+
+ <update id="updatePostLikes" parameterType="Post">
+ update post set likes = likes + #{likes} where post_id = #{postId}
+ </update>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/resources/mapper/post/PostPaymentMapper.xml b/ruoyi-admin/src/main/resources/mapper/post/PostPaymentMapper.xml
new file mode 100644
index 0000000..c12dc2e
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/mapper/post/PostPaymentMapper.xml
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.web.controller.post.mapper.PostPaymentMapper">
+
+ <resultMap type="com.ruoyi.web.controller.post.domain.PostPayment" id="PostPaymentResult">
+ <id property="paymentId" column="payment_id" />
+ <result property="postId" column="post_id" />
+ <result property="planId" column="plan_id" />
+ <result property="userId" column="user_id" />
+ <result property="amount" column="amount" />
+ <result property="paymentStatus" column="payment_status" />
+ <result property="paymentTime" column="payment_time" />
+ </resultMap>
+
+ <sql id="selectPostPaymentVo">
+ select payment_id, post_id, plan_id, user_id, amount, payment_status, payment_time
+ from post_payment
+ </sql>
+
+ <select id="selectPostPaymentList" parameterType="com.ruoyi.web.controller.post.domain.PostPayment" resultMap="PostPaymentResult">
+ <include refid="selectPostPaymentVo"/>
+ <where>
+ <if test="postId != null">
+ AND post_id = #{postId}
+ </if>
+ <if test="planId != null">
+ AND plan_id = #{planId}
+ </if>
+ <if test="userId != null">
+ AND user_id = #{userId}
+ </if>
+ <if test="paymentStatus != null and paymentStatus != ''">
+ AND payment_status = #{paymentStatus}
+ </if>
+ </where>
+ order by payment_time desc
+ </select>
+
+ <select id="selectPostPaymentById" parameterType="Long" resultMap="PostPaymentResult">
+ <include refid="selectPostPaymentVo"/>
+ where payment_id = #{paymentId}
+ </select>
+
+ <select id="selectLatestPaymentByPostId" parameterType="Long" resultMap="PostPaymentResult">
+ <include refid="selectPostPaymentVo"/>
+ where post_id = #{postId} and payment_status = 'paid'
+ order by payment_time desc
+ limit 1
+ </select>
+
+ <select id="selectExpiredPromotionPayments" resultMap="PostPaymentResult">
+ <include refid="selectPostPaymentVo"/>
+ where payment_status = 'paid'
+ and post_id in (
+ select post_id from post where promotion_plan_id is not null
+ )
+ order by payment_time desc
+ </select>
+
+ <insert id="insertPostPayment" parameterType="com.ruoyi.web.controller.post.domain.PostPayment" useGeneratedKeys="true" keyProperty="paymentId">
+ insert into post_payment (
+ post_id,
+ plan_id,
+ user_id,
+ amount,
+ payment_status,
+ payment_time
+ ) values (
+ #{postId},
+ #{planId},
+ #{userId},
+ #{amount},
+ #{paymentStatus},
+ #{paymentTime}
+ )
+ </insert>
+
+ <update id="updatePostPayment" parameterType="com.ruoyi.web.controller.post.domain.PostPayment">
+ update post_payment
+ <set>
+ <if test="postId != null">post_id = #{postId},</if>
+ <if test="planId != null">plan_id = #{planId},</if>
+ <if test="userId != null">user_id = #{userId},</if>
+ <if test="amount != null">amount = #{amount},</if>
+ <if test="paymentStatus != null and paymentStatus != ''">payment_status = #{paymentStatus},</if>
+ <if test="paymentTime != null">payment_time = #{paymentTime},</if>
+ </set>
+ where payment_id = #{paymentId}
+ </update>
+
+ <delete id="deletePostPaymentById" parameterType="Long">
+ delete from post_payment where payment_id = #{paymentId}
+ </delete>
+
+ <delete id="deletePostPaymentByIds" parameterType="Long">
+ delete from post_payment where payment_id in
+ <foreach collection="array" item="paymentId" open="(" separator="," close=")">
+ #{paymentId}
+ </foreach>
+ </delete>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/resources/mapper/post/PostPromotionPlanMapper.xml b/ruoyi-admin/src/main/resources/mapper/post/PostPromotionPlanMapper.xml
new file mode 100644
index 0000000..0d22833
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/mapper/post/PostPromotionPlanMapper.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.web.controller.post.mapper.PostPromotionPlanMapper">
+
+ <resultMap type="com.ruoyi.web.controller.post.domain.PostPromotionPlan" id="PostPromotionPlanResult">
+ <id property="planId" column="plan_id" />
+ <result property="planName" column="plan_name" />
+ <result property="planDescription" column="plan_description" />
+ <result property="price" column="price" />
+ <result property="duration" column="duration" />
+ <result property="level" column="level" />
+ <result property="status" column="status" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ <result property="remark" column="remark" />
+ </resultMap>
+
+ <sql id="selectPostPromotionPlanVo">
+ select plan_id, plan_name, plan_description, price, duration, level, status,
+ create_by, create_time, update_by, update_time, remark
+ from post_promotion_plan
+ </sql>
+
+ <select id="selectPostPromotionPlanList" parameterType="com.ruoyi.web.controller.post.domain.PostPromotionPlan" resultMap="PostPromotionPlanResult">
+ <include refid="selectPostPromotionPlanVo"/>
+ <where>
+ <if test="planName != null and planName != ''">
+ AND plan_name like concat('%', #{planName}, '%')
+ </if>
+ <if test="status != null and status != ''">
+ AND status = #{status}
+ </if>
+ </where>
+ order by level asc, create_time desc
+ </select>
+
+ <select id="selectPostPromotionPlanById" parameterType="Long" resultMap="PostPromotionPlanResult">
+ <include refid="selectPostPromotionPlanVo"/>
+ where plan_id = #{planId}
+ </select>
+
+ <insert id="insertPostPromotionPlan" parameterType="com.ruoyi.web.controller.post.domain.PostPromotionPlan" useGeneratedKeys="true" keyProperty="planId">
+ insert into post_promotion_plan (
+ plan_name,
+ plan_description,
+ price,
+ duration,
+ level,
+ status,
+ create_by,
+ create_time,
+ remark
+ ) values (
+ #{planName},
+ #{planDescription},
+ #{price},
+ #{duration},
+ #{level},
+ #{status},
+ #{createBy},
+ sysdate(),
+ #{remark}
+ )
+ </insert>
+
+ <update id="updatePostPromotionPlan" parameterType="com.ruoyi.web.controller.post.domain.PostPromotionPlan">
+ update post_promotion_plan
+ <set>
+ <if test="planName != null and planName != ''">plan_name = #{planName},</if>
+ <if test="planDescription != null">plan_description = #{planDescription},</if>
+ <if test="price != null">price = #{price},</if>
+ <if test="duration != null">duration = #{duration},</if>
+ <if test="level != null">level = #{level},</if>
+ <if test="status != null and status != ''">status = #{status},</if>
+ <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+ <if test="remark != null">remark = #{remark},</if>
+ update_time = sysdate()
+ </set>
+ where plan_id = #{planId}
+ </update>
+
+ <delete id="deletePostPromotionPlanById" parameterType="Long">
+ delete from post_promotion_plan where plan_id = #{planId}
+ </delete>
+
+ <delete id="deletePostPromotionPlanByIds" parameterType="Long">
+ delete from post_promotion_plan where plan_id in
+ <foreach collection="array" item="planId" open="(" separator="," close=")">
+ #{planId}
+ </foreach>
+ </delete>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/resources/mapper/post/PostReportMapper.xml b/ruoyi-admin/src/main/resources/mapper/post/PostReportMapper.xml
new file mode 100644
index 0000000..fbbe021
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/mapper/post/PostReportMapper.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.web.controller.post.mapper.PostReportMapper">
+
+ <resultMap type="com.ruoyi.web.controller.post.domain.PostReport" id="PostReportResult">
+ <id property="reportId" column="report_id" />
+ <result property="postId" column="post_id" />
+ <result property="postTitle" column="post_title" />
+ <result property="reportUserId" column="report_user_id" />
+ <result property="reportUserName" column="report_user_name" />
+ <result property="reportReason" column="report_reason" />
+ <result property="status" column="status" />
+ <result property="handleResult" column="handle_result" />
+ <result property="handleTime" column="handle_time" />
+ <result property="handleBy" column="handle_by" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ <result property="remark" column="remark" />
+ </resultMap>
+
+ <sql id="selectPostReportVo">
+ select report_id, post_id, post_title, report_user_id, report_user_name, report_reason,
+ status, handle_result, handle_time, handle_by, create_by, create_time,
+ update_by, update_time, remark
+ from post_report
+ </sql>
+
+ <select id="selectPostReportList" parameterType="com.ruoyi.web.controller.post.domain.PostReport" resultMap="PostReportResult">
+ <include refid="selectPostReportVo"/>
+ <where>
+ <if test="postId != null">
+ AND post_id = #{postId}
+ </if>
+ <if test="reportUserId != null">
+ AND report_user_id = #{reportUserId}
+ </if>
+ <if test="status != null and status != ''">
+ AND status = #{status}
+ </if>
+ <if test="postTitle != null and postTitle != ''">
+ AND post_title like concat('%', #{postTitle}, '%')
+ </if>
+ <if test="reportUserName != null and reportUserName != ''">
+ AND report_user_name like concat('%', #{reportUserName}, '%')
+ </if>
+ </where>
+ order by create_time desc
+ </select>
+
+ <select id="selectPostReportById" parameterType="Long" resultMap="PostReportResult">
+ <include refid="selectPostReportVo"/>
+ where report_id = #{reportId}
+ </select>
+
+ <insert id="insertPostReport" parameterType="com.ruoyi.web.controller.post.domain.PostReport" useGeneratedKeys="true" keyProperty="reportId">
+ insert into post_report (
+ post_id,
+ post_title,
+ report_user_id,
+ report_user_name,
+ report_reason,
+ status,
+ create_by,
+ create_time
+ ) values (
+ #{postId},
+ #{postTitle},
+ #{reportUserId},
+ #{reportUserName},
+ #{reportReason},
+ #{status},
+ #{createBy},
+ sysdate()
+ )
+ </insert>
+
+ <update id="updatePostReport" parameterType="com.ruoyi.web.controller.post.domain.PostReport">
+ update post_report
+ <set>
+ <if test="postId != null">post_id = #{postId},</if>
+ <if test="postTitle != null and postTitle != ''">post_title = #{postTitle},</if>
+ <if test="reportUserId != null">report_user_id = #{reportUserId},</if>
+ <if test="reportUserName != null and reportUserName != ''">report_user_name = #{reportUserName},</if>
+ <if test="reportReason != null and reportReason != ''">report_reason = #{reportReason},</if>
+ <if test="status != null and status != ''">status = #{status},</if>
+ <if test="handleResult != null">handle_result = #{handleResult},</if>
+ <if test="handleTime != null">handle_time = #{handleTime},</if>
+ <if test="handleBy != null and handleBy != ''">handle_by = #{handleBy},</if>
+ <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+ <if test="remark != null">remark = #{remark},</if>
+ update_time = sysdate()
+ </set>
+ where report_id = #{reportId}
+ </update>
+
+ <delete id="deletePostReportById" parameterType="Long">
+ delete from post_report where report_id = #{reportId}
+ </delete>
+
+ <delete id="deletePostReportByIds" parameterType="Long">
+ delete from post_report where report_id in
+ <foreach collection="array" item="reportId" open="(" separator="," close=")">
+ #{reportId}
+ </foreach>
+ </delete>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/resources/mapper/post/PostTagMapper.xml b/ruoyi-admin/src/main/resources/mapper/post/PostTagMapper.xml
new file mode 100644
index 0000000..1060c0a
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/mapper/post/PostTagMapper.xml
@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.web.controller.post.mapper.PostContentTagMapper">
+
+ <resultMap type="PostTag" id="PostTagResult">
+ <id property="tagId" column="tag_id" />
+ <result property="tagName" column="tag_name" />
+ <result property="tagColor" column="tag_color" />
+ <result property="postCount" column="post_count" />
+ <result property="status" column="status" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ <result property="remark" column="remark" />
+ </resultMap>
+
+ <resultMap type="PostTagRelation" id="PostTagRelationResult">
+ <id property="id" column="id" />
+ <result property="postId" column="post_id" />
+ <result property="tagId" column="tag_id" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ </resultMap>
+
+ <sql id="selectPostTagVo">
+ select tag_id, tag_name, tag_color, post_count, status,
+ create_by, create_time, update_by, update_time, remark
+ from post_tag
+ </sql>
+
+ <select id="selectPostTagList" parameterType="PostTag" resultMap="PostTagResult">
+ <include refid="selectPostTagVo"/>
+ <where>
+ <if test="tagName != null and tagName != ''">
+ AND tag_name like concat('%', #{tagName}, '%')
+ </if>
+ <if test="status != null and status != ''">
+ AND status = #{status}
+ </if>
+ </where>
+ order by post_count desc, create_time desc
+ </select>
+
+ <select id="selectPostTagById" parameterType="Long" resultMap="PostTagResult">
+ <include refid="selectPostTagVo"/>
+ where tag_id = #{tagId}
+ </select>
+
+ <select id="selectPostTagsByPostId" parameterType="Long" resultMap="PostTagResult">
+ select t.tag_id, t.tag_name, t.tag_color, t.post_count, t.status,
+ t.create_by, t.create_time, t.update_by, t.update_time, t.remark
+ from post_tag t
+ inner join post_tag_relation r on t.tag_id = r.tag_id
+ where r.post_id = #{postId} and t.status = '0'
+ </select>
+
+ <select id="checkTagNameUnique" parameterType="String" resultMap="PostTagResult">
+ <include refid="selectPostTagVo"/>
+ where tag_name = #{tagName}
+ </select>
+
+ <insert id="insertPostTag" parameterType="PostTag" useGeneratedKeys="true" keyProperty="tagId">
+ insert into post_tag (
+ tag_name,
+ tag_color,
+ post_count,
+ status,
+ create_by,
+ create_time,
+ update_by,
+ update_time,
+ remark
+ ) values (
+ #{tagName},
+ #{tagColor},
+ #{postCount},
+ #{status},
+ #{createBy},
+ now(),
+ #{updateBy},
+ now(),
+ #{remark}
+ )
+ </insert>
+
+ <update id="updatePostTag" parameterType="PostTag">
+ update post_tag
+ <set>
+ <if test="tagName != null and tagName != ''">tag_name = #{tagName},</if>
+ <if test="tagColor != null">tag_color = #{tagColor},</if>
+ <if test="postCount != null">post_count = #{postCount},</if>
+ <if test="status != null and status != ''">status = #{status},</if>
+ <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+ update_time = now(),
+ <if test="remark != null">remark = #{remark},</if>
+ </set>
+ where tag_id = #{tagId}
+ </update>
+
+ <delete id="deletePostTagById" parameterType="Long">
+ delete from post_tag where tag_id = #{tagId}
+ </delete>
+
+ <delete id="deletePostTagByIds" parameterType="Long">
+ delete from post_tag where tag_id in
+ <foreach collection="array" item="tagId" open="(" separator="," close=")">
+ #{tagId}
+ </foreach>
+ </delete>
+
+ <update id="updatePostTagCount" parameterType="PostTag">
+ update post_tag set post_count = post_count + #{postCount} where tag_id = #{tagId}
+ </update>
+
+ <!-- 帖子标签关联表操作 -->
+ <insert id="insertPostTagRelation" parameterType="com.ruoyi.web.controller.post.domain.PostTagRelation">
+ insert into post_tag_relation (post_id, tag_id) values (#{postId}, #{tagId})
+ </insert>
+
+ <insert id="batchInsertPostTagRelation" parameterType="java.util.List">
+ insert into post_tag_relation (post_id, tag_id) values
+ <foreach collection="list" item="item" separator=",">
+ (#{item.postId}, #{item.tagId})
+ </foreach>
+ </insert>
+
+ <delete id="deletePostTagRelation" parameterType="Long">
+ delete from post_tag_relation where post_id = #{postId}
+ </delete>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/web/controller/post/PostCenterControllerTest.java b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/post/PostCenterControllerTest.java
new file mode 100644
index 0000000..880b0fc
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/post/PostCenterControllerTest.java
@@ -0,0 +1,537 @@
+package com.ruoyi.web.controller.post;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.web.controller.post.controller.PostCenterController;
+import com.ruoyi.web.controller.post.domain.*;
+import com.ruoyi.web.controller.post.service.*;
+import com.ruoyi.web.controller.post.mapper.*;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+/**
+ * PostCenterController 单元测试
+ *
+ * 测试覆盖所有帖子中心相关的API接口
+ * 包括:帖子列表、详情、评论、收藏、点赞、发布、上传等功能
+ */
+@ExtendWith(MockitoExtension.class)
+@WithMockUser(username = "testUser", authorities = {"admin"})
+class PostCenterControllerTest {
+
+ @Mock
+ private IPostService postService;
+
+ @Mock
+ private IPostTagService postTagService;
+
+ @Mock
+ private IPostCommentService postCommentService;
+
+ @Mock
+ private IPostFavoriteService postFavoriteService;
+
+ @Mock
+ private PostReportMapper postReportMapper;
+
+ @Mock
+ private PostPaymentMapper postPaymentMapper;
+
+ @InjectMocks
+ private PostCenterController postCenterController;
+
+ private MockMvc mockMvc;
+ private ObjectMapper objectMapper;
+
+ @BeforeEach
+ void setUp() {
+ // 创建模拟的安全上下文
+ SysUser sysUser = new SysUser();
+ sysUser.setUserId(1L);
+ sysUser.setUserName("testUser");
+
+ LoginUser loginUser = new LoginUser(1L, 1L, sysUser, new HashSet<>());
+
+ UsernamePasswordAuthenticationToken authToken =
+ new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
+ SecurityContextHolder.getContext().setAuthentication(authToken);
+
+ mockMvc = MockMvcBuilders.standaloneSetup(postCenterController).build();
+ objectMapper = new ObjectMapper();
+ }
+
+ /**
+ * 测试获取帖子列表
+ */
+ @Test
+ void testGetPostList() throws Exception {
+ List<Post> posts = new ArrayList<>();
+ Post post = new Post();
+ post.setPostId(1L);
+ post.setTitle("测试帖子");
+ posts.add(post);
+
+ when(postService.selectPostList(any(Post.class))).thenReturn(posts);
+
+ mockMvc.perform(get("/post-center/list"))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).selectPostList(any(Post.class));
+ }
+
+ /**
+ * 测试获取帖子详情
+ */
+ @Test
+ void testGetPostDetail() throws Exception {
+ Post post = new Post();
+ post.setPostId(1L);
+ post.setTitle("测试帖子");
+ post.setStatus("1");
+
+ when(postService.updatePostViews(1L)).thenReturn(1);
+ when(postService.selectPostById(1L)).thenReturn(post);
+ when(postTagService.selectPostTagsByPostId(1L)).thenReturn(new ArrayList<>());
+ when(postCommentService.selectCommentsByPostId(eq(1L), eq(10))).thenReturn(new ArrayList<>());
+ when(postService.getRecommendedPosts(any(), any(), eq(1L), eq(9))).thenReturn(new ArrayList<>());
+
+ mockMvc.perform(get("/post-center/{postId}", 1L))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).selectPostById(1L);
+ verify(postService, times(1)).updatePostViews(1L);
+ }
+
+ /**
+ * 测试发布帖子
+ */
+ @Test
+ void testPublishPost() throws Exception {
+ when(postService.insertPost(any(Post.class))).thenReturn(1);
+
+ String postData = "{\"title\":\"新帖子\",\"content\":\"帖子内容\",\"summary\":\"帖子摘要\"}";
+
+ mockMvc.perform(post("/post-center/publish")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(postData))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).insertPost(any(Post.class));
+ }
+
+ /**
+ * 测试更新帖子
+ */
+ @Test
+ void testUpdatePost() throws Exception {
+ Post existingPost = new Post();
+ existingPost.setPostId(1L);
+ existingPost.setAuthorId(1L);
+
+ when(postService.selectPostById(1L)).thenReturn(existingPost);
+ when(postService.updatePost(any(Post.class))).thenReturn(1);
+
+ String postData = "{\"postId\":1,\"title\":\"更新帖子\",\"content\":\"更新内容\",\"summary\":\"更新摘要\"}";
+
+ mockMvc.perform(put("/post-center/update")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(postData))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).updatePost(any(Post.class));
+ }
+
+ /**
+ * 测试删除帖子
+ */
+ @Test
+ void testDeletePost() throws Exception {
+ Post existingPost = new Post();
+ existingPost.setPostId(1L);
+ existingPost.setAuthorId(1L);
+
+ when(postService.selectPostById(1L)).thenReturn(existingPost);
+ when(postCommentService.deletePostCommentByPostId(1L)).thenReturn(1);
+ when(postFavoriteService.deletePostFavoriteByPostId(1L)).thenReturn(1);
+ when(postService.deletePostById(1L)).thenReturn(1);
+
+ mockMvc.perform(delete("/post-center/delete/{postId}", 1L))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).deletePostById(1L);
+ }
+
+ /**
+ * 测试添加评论
+ */
+ @Test
+ void testAddComment() throws Exception {
+ PostComment comment = new PostComment();
+ comment.setPostId(1L);
+ comment.setContent("测试评论");
+ comment.setParentId(0L);
+
+ when(postCommentService.selectPostCommentById(any())).thenReturn(null);
+ when(postCommentService.insertPostComment(any(PostComment.class))).thenReturn(1);
+ when(postService.updatePostComments(eq(1L), anyInt())).thenReturn(1);
+
+ String commentData = objectMapper.writeValueAsString(comment);
+
+ mockMvc.perform(post("/post-center/comment")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(commentData))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).insertPostComment(any(PostComment.class));
+ }
+
+ /**
+ * 测试获取评论列表(实际是测试帖子列表)
+ */
+ @Test
+ void testGetComments() throws Exception {
+ List<Post> posts = new ArrayList<>();
+ Post post = new Post();
+ post.setPostId(1L);
+ post.setTitle("测试帖子");
+ posts.add(post);
+
+ when(postService.selectPostList(any(Post.class))).thenReturn(posts);
+
+ // 测试帖子列表接口
+ mockMvc.perform(get("/post-center/list")
+ .param("postId", "1")
+ .param("pageSize", "10"))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).selectPostList(any(Post.class));
+ }
+
+ /**
+ * 测试收藏帖子
+ */
+ @Test
+ void testToggleFavorite() throws Exception {
+ Post post = new Post();
+ post.setPostId(1L);
+ post.setStatus("1");
+ post.setTitle("测试帖子");
+
+ when(postService.selectPostById(1L)).thenReturn(post);
+ when(postFavoriteService.selectPostFavoriteByPostIdAndUserId(anyLong(), anyLong())).thenReturn(null);
+ when(postFavoriteService.insertPostFavorite(any())).thenReturn(1);
+ when(postService.updatePostFavorites(anyLong(), anyInt())).thenReturn(1);
+
+ mockMvc.perform(post("/post-center/favorite/{postId}", 1L))
+ .andExpect(status().isOk());
+
+ verify(postFavoriteService, times(1)).insertPostFavorite(any());
+ }
+
+ /**
+ * 测试点赞帖子
+ */
+ @Test
+ void testToggleLike() throws Exception {
+ Post post = new Post();
+ post.setPostId(1L);
+ post.setStatus("1");
+
+ when(postService.selectPostById(1L)).thenReturn(post);
+ when(postService.updatePostLikes(anyLong(), anyInt())).thenReturn(1);
+
+ mockMvc.perform(post("/post-center/like/{postId}", 1L))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).updatePostLikes(eq(1L), anyInt());
+ }
+
+ /**
+ * 测试上传图片
+ */
+ @Test
+ void testUploadImage() throws Exception {
+ mockMvc.perform(multipart("/post-center/upload")
+ .file("file", "test image content".getBytes())
+ .param("filename", "test.jpg"))
+ .andExpect(status().isOk());
+ }
+
+ /**
+ * 测试获取我的帖子
+ */
+ @Test
+ void testGetMyPosts() throws Exception {
+ List<Post> posts = new ArrayList<>();
+ when(postService.selectPostList(any())).thenReturn(posts);
+
+ mockMvc.perform(get("/post-center/my-posts"))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).selectPostList(any());
+ }
+
+ /**
+ * 测试获取我的收藏
+ */
+ @Test
+ void testGetMyFavorites() throws Exception {
+ List<PostFavorite> favorites = new ArrayList<>();
+ when(postFavoriteService.selectPostFavoriteList(any())).thenReturn(favorites);
+
+ mockMvc.perform(get("/post-center/my-favorites"))
+ .andExpect(status().isOk());
+
+ verify(postFavoriteService, times(1)).selectPostFavoriteList(any());
+ }
+
+ /**
+ * 测试举报帖子
+ */
+ @Test
+ void testReportPost() throws Exception {
+ Post post = new Post();
+ post.setPostId(1L);
+ post.setStatus("1");
+ post.setTitle("测试帖子");
+
+ when(postService.selectPostById(1L)).thenReturn(post);
+ when(postReportMapper.insertPostReport(any())).thenReturn(1);
+
+ String reportData = "{\"reason\":\"垃圾内容\"}";
+
+ mockMvc.perform(post("/post-center/report/{postId}", 1L)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(reportData))
+ .andExpect(status().isOk());
+
+ verify(postReportMapper, times(1)).insertPostReport(any());
+ }
+
+ /**
+ * 测试获取热门标签
+ */
+ @Test
+ void testGetHotTags() throws Exception {
+ List<PostTag> tags = new ArrayList<>();
+ when(postTagService.selectPostTagList(any())).thenReturn(tags);
+
+ mockMvc.perform(get("/post-center/tags/hot"))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).selectPostTagList(any());
+ }
+
+ /**
+ * 测试获取可用标签
+ */
+ @Test
+ void testGetAvailableTags() throws Exception {
+ List<PostTag> tags = new ArrayList<>();
+ when(postTagService.selectPostTagList(any())).thenReturn(tags);
+
+ mockMvc.perform(get("/post-center/tags/available"))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).selectPostTagList(any());
+ }
+
+ /**
+ * 测试点赞评论
+ */
+ @Test
+ void testToggleCommentLike() throws Exception {
+ PostComment comment = new PostComment();
+ comment.setCommentId(1L);
+ comment.setStatus("1");
+
+ when(postCommentService.selectPostCommentById(1L)).thenReturn(comment);
+ when(postCommentService.updateCommentLikes(eq(1L), anyInt())).thenReturn(1);
+
+ mockMvc.perform(post("/post-center/comment/like/{commentId}", 1L))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).updateCommentLikes(eq(1L), anyInt());
+ }
+
+ /**
+ * 测试获取帖子详情(帖子不存在)
+ */
+ @Test
+ void testGetPostDetailNotFound() throws Exception {
+ when(postService.updatePostViews(999L)).thenReturn(0);
+ when(postService.selectPostById(999L)).thenReturn(null);
+
+ mockMvc.perform(get("/post-center/{postId}", 999L))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).selectPostById(999L);
+ }
+
+ /**
+ * 测试发布空内容帖子
+ */
+ @Test
+ void testPublishEmptyPost() throws Exception {
+ String emptyPostData = "{\"title\":\"\",\"content\":\"\"}";
+
+ mockMvc.perform(post("/post-center/publish")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(emptyPostData))
+ .andExpect(status().isOk());
+ }
+
+ /**
+ * 测试添加空评论
+ */
+ @Test
+ void testAddEmptyComment() throws Exception {
+ String emptyCommentData = "{\"postId\":1,\"content\":\"\"}";
+
+ mockMvc.perform(post("/post-center/comment")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(emptyCommentData))
+ .andExpect(status().isOk());
+ }
+
+ /**
+ * 测试上传空文件
+ */
+ @Test
+ void testUploadEmptyFile() throws Exception {
+ mockMvc.perform(multipart("/post-center/upload")
+ .file("file", new byte[0])
+ .param("filename", ""))
+ .andExpect(status().isOk());
+ }
+
+ /**
+ * 测试获取推广计划
+ */
+ @Test
+ void testGetPromotionPlans() throws Exception {
+ mockMvc.perform(get("/post-center/promotion-plans"))
+ .andExpect(status().isOk());
+ }
+
+ /**
+ * 测试创建支付
+ */
+ @Test
+ void testCreatePayment() throws Exception {
+ Post post = new Post();
+ post.setPostId(1L);
+ post.setAuthorId(1L);
+
+ when(postService.selectPostById(1L)).thenReturn(post);
+ when(postPaymentMapper.insertPostPayment(any())).thenReturn(1);
+
+ String paymentData = "{\"postId\":1,\"planId\":1,\"amount\":100}";
+
+ mockMvc.perform(post("/post-center/payment")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(paymentData))
+ .andExpect(status().isOk());
+
+ verify(postPaymentMapper, times(1)).insertPostPayment(any());
+ }
+
+ /**
+ * 测试取消支付
+ */
+ @Test
+ void testCancelPayment() throws Exception {
+ PostPayment payment = new PostPayment();
+ payment.setPaymentId(1L);
+ payment.setUserId(1L);
+ payment.setPaymentStatus("pending");
+
+ when(postPaymentMapper.selectPostPaymentById(1L)).thenReturn(payment);
+ when(postPaymentMapper.updatePostPayment(any())).thenReturn(1);
+
+ mockMvc.perform(post("/post-center/payment/cancel/{paymentId}", 1L))
+ .andExpect(status().isOk());
+
+ verify(postPaymentMapper, times(1)).updatePostPayment(any());
+ }
+
+ /**
+ * 测试获取推广状态
+ */
+ @Test
+ void testGetPromotionStatus() throws Exception {
+ Post post = new Post();
+ post.setPostId(1L);
+ post.setAuthorId(1L);
+
+ when(postService.selectPostById(1L)).thenReturn(post);
+
+ mockMvc.perform(get("/post-center/promotion-status/{postId}", 1L))
+ .andExpect(status().isOk());
+ }
+
+ /**
+ * 测试获取推广帖子
+ */
+ @Test
+ void testGetPromotionPosts() throws Exception {
+ List<Post> posts = new ArrayList<>();
+ when(postService.selectPostList(any())).thenReturn(posts);
+
+ mockMvc.perform(get("/post-center/promotion"))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).selectPostList(any());
+ }
+
+ /**
+ * 测试根据标签获取帖子
+ */
+ @Test
+ void testGetPostsByTag() throws Exception {
+ PostTag tag = new PostTag();
+ tag.setTagId(1L);
+ tag.setTagName("测试标签");
+
+ List<Post> posts = new ArrayList<>();
+ when(postTagService.selectPostTagById(1L)).thenReturn(tag);
+ when(postService.selectPostList(any())).thenReturn(posts);
+
+ mockMvc.perform(get("/post-center/bytag/{tagId}", 1L))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).selectPostList(any());
+ }
+
+ /**
+ * 测试删除图片
+ */
+ @Test
+ void testDeleteImage() throws Exception {
+ mockMvc.perform(delete("/post-center/upload")
+ .param("filename", "test.jpg"))
+ .andExpect(status().isOk());
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/web/controller/post/PostCommentControllerTest.java b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/post/PostCommentControllerTest.java
new file mode 100644
index 0000000..8e81d5a
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/post/PostCommentControllerTest.java
@@ -0,0 +1,378 @@
+package com.ruoyi.web.controller.post;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.web.controller.post.controller.PostCommentController;
+import com.ruoyi.web.controller.post.domain.PostComment;
+import com.ruoyi.web.controller.post.service.IPostCommentService;
+import com.ruoyi.web.controller.post.service.IPostService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+/**
+ * PostCommentController 单元测试
+ *
+ * 测试覆盖所有评论管理相关的API接口
+ * 包括:评论列表、详情、增删改查、点赞、导出等功能
+ */
+@ExtendWith(MockitoExtension.class)
+class PostCommentControllerTest {
+
+ @Mock
+ private IPostCommentService postCommentService;
+
+ @Mock
+ private IPostService postService;
+
+ @InjectMocks
+ private PostCommentController postCommentController;
+
+ private MockMvc mockMvc;
+ private ObjectMapper objectMapper;
+
+ @BeforeEach
+ void setUp() {
+ // 创建模拟的安全上下文
+ SysUser sysUser = new SysUser();
+ sysUser.setUserId(1L);
+ sysUser.setUserName("testUser");
+
+ LoginUser loginUser = new LoginUser(1L, 1L, sysUser, new HashSet<>());
+
+ UsernamePasswordAuthenticationToken authToken =
+ new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
+ SecurityContextHolder.getContext().setAuthentication(authToken);
+
+ mockMvc = MockMvcBuilders.standaloneSetup(postCommentController).build();
+ objectMapper = new ObjectMapper();
+ }
+
+ /**
+ * 测试获取评论列表
+ */
+ @Test
+ void testGetCommentList() throws Exception {
+ List<PostComment> comments = new ArrayList<>();
+ PostComment comment = new PostComment();
+ comment.setCommentId(1L);
+ comment.setContent("测试评论");
+ comments.add(comment);
+
+ when(postCommentService.selectPostCommentList(any(PostComment.class))).thenReturn(comments);
+
+ mockMvc.perform(get("/post/comment/list"))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).selectPostCommentList(any(PostComment.class));
+ }
+
+ /**
+ * 测试获取评论详情
+ */
+ @Test
+ void testGetCommentInfo() throws Exception {
+ PostComment comment = new PostComment();
+ comment.setCommentId(1L);
+ comment.setContent("测试评论");
+
+ when(postCommentService.selectPostCommentById(1L)).thenReturn(comment);
+
+ mockMvc.perform(get("/post/comment/{commentId}", 1L))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).selectPostCommentById(1L);
+ }
+
+ /**
+ * 测试添加评论
+ */
+ @Test
+ void testAddComment() throws Exception {
+ PostComment comment = new PostComment();
+ comment.setPostId(1L);
+ comment.setContent("新评论");
+
+ when(postCommentService.insertPostComment(any(PostComment.class))).thenReturn(1);
+ when(postService.updatePostComments(eq(1L), eq(1))).thenReturn(1);
+
+ mockMvc.perform(post("/post/comment")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(comment)))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).insertPostComment(any(PostComment.class));
+ verify(postService, times(1)).updatePostComments(eq(1L), eq(1));
+ }
+
+ /**
+ * 测试编辑评论
+ */
+ @Test
+ void testEditComment() throws Exception {
+ PostComment comment = new PostComment();
+ comment.setCommentId(1L);
+ comment.setContent("编辑后的评论");
+
+ when(postCommentService.updatePostComment(any(PostComment.class))).thenReturn(1);
+
+ mockMvc.perform(put("/post/comment")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(comment)))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).updatePostComment(any(PostComment.class));
+ }
+
+ /**
+ * 测试删除评论
+ */
+ @Test
+ void testDeleteComment() throws Exception {
+ PostComment comment = new PostComment();
+ comment.setCommentId(1L);
+ comment.setPostId(1L);
+ comment.setParentId(0L);
+
+ when(postCommentService.selectPostCommentById(1L)).thenReturn(comment);
+ when(postCommentService.selectCommentsByParentId(1L)).thenReturn(new ArrayList<>());
+ when(postCommentService.deletePostCommentByIds(any())).thenReturn(1);
+ when(postService.updatePostComments(eq(1L), eq(-1))).thenReturn(1);
+
+ mockMvc.perform(delete("/post/comment/{commentIds}", "1"))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).selectPostCommentById(1L);
+ verify(postCommentService, times(1)).deletePostCommentByIds(any());
+ verify(postService, times(1)).updatePostComments(eq(1L), eq(-1));
+ }
+
+ /**
+ * 测试删除包含回复的顶级评论
+ */
+ @Test
+ void testDeleteTopCommentWithReplies() throws Exception {
+ PostComment comment = new PostComment();
+ comment.setCommentId(1L);
+ comment.setPostId(1L);
+ comment.setParentId(0L);
+
+ List<PostComment> replies = new ArrayList<>();
+ PostComment reply1 = new PostComment();
+ reply1.setCommentId(2L);
+ replies.add(reply1);
+ PostComment reply2 = new PostComment();
+ reply2.setCommentId(3L);
+ replies.add(reply2);
+
+ when(postCommentService.selectPostCommentById(1L)).thenReturn(comment);
+ when(postCommentService.selectCommentsByParentId(1L)).thenReturn(replies);
+ when(postCommentService.deletePostCommentByIds(any())).thenReturn(1);
+ when(postService.updatePostComments(eq(1L), eq(-3))).thenReturn(1); // 1个评论 + 2个回复
+
+ mockMvc.perform(delete("/post/comment/{commentIds}", "1"))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).selectPostCommentById(1L);
+ verify(postCommentService, times(1)).selectCommentsByParentId(1L);
+ verify(postCommentService, times(1)).deletePostCommentByIds(any());
+ verify(postService, times(1)).updatePostComments(eq(1L), eq(-3));
+ }
+
+ /**
+ * 测试根据帖子ID获取评论列表
+ */
+ @Test
+ void testListByPostId() throws Exception {
+ List<PostComment> comments = new ArrayList<>();
+ when(postCommentService.selectCommentsByPostId(eq(1L), eq(0))).thenReturn(comments);
+
+ mockMvc.perform(get("/post/comment/list/{postId}", 1L))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).selectCommentsByPostId(eq(1L), eq(0));
+ }
+
+ /**
+ * 测试点赞评论
+ */
+ @Test
+ void testLikeComment() throws Exception {
+ when(postCommentService.updateCommentLikes(eq(1L), eq(1))).thenReturn(1);
+
+ mockMvc.perform(post("/post/comment/like/{commentId}", 1L))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).updateCommentLikes(eq(1L), eq(1));
+ }
+
+ /**
+ * 测试导出评论
+ */
+ @Test
+ void testExportComments() throws Exception {
+ List<PostComment> comments = new ArrayList<>();
+ when(postCommentService.selectPostCommentList(any(PostComment.class))).thenReturn(comments);
+
+ mockMvc.perform(post("/post/comment/export"))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).selectPostCommentList(any(PostComment.class));
+ }
+
+ /**
+ * 测试添加回复评论(非顶级评论)
+ */
+ @Test
+ void testAddReplyComment() throws Exception {
+ PostComment replyComment = new PostComment();
+ replyComment.setPostId(1L);
+ replyComment.setContent("回复评论");
+ replyComment.setParentId(1L); // 回复给评论ID为1的评论
+
+ when(postCommentService.insertPostComment(any(PostComment.class))).thenReturn(1);
+ when(postService.updatePostComments(eq(1L), eq(1))).thenReturn(1);
+
+ mockMvc.perform(post("/post/comment")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(replyComment)))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).insertPostComment(any(PostComment.class));
+ verify(postService, times(1)).updatePostComments(eq(1L), eq(1));
+ }
+
+ /**
+ * 测试删除不存在的评论
+ */
+ @Test
+ void testDeleteNonExistentComment() throws Exception {
+ when(postCommentService.selectPostCommentById(999L)).thenReturn(null);
+ when(postCommentService.deletePostCommentByIds(any())).thenReturn(0);
+
+ mockMvc.perform(delete("/post/comment/{commentIds}", "999"))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).selectPostCommentById(999L);
+ verify(postCommentService, times(1)).deletePostCommentByIds(any());
+ verify(postService, never()).updatePostComments(any(), anyInt());
+ }
+
+ /**
+ * 测试批量删除评论
+ */
+ @Test
+ void testBatchDeleteComments() throws Exception {
+ PostComment comment1 = new PostComment();
+ comment1.setCommentId(1L);
+ comment1.setPostId(1L);
+ comment1.setParentId(0L);
+
+ PostComment comment2 = new PostComment();
+ comment2.setCommentId(2L);
+ comment2.setPostId(1L);
+ comment2.setParentId(0L);
+
+ when(postCommentService.selectPostCommentById(1L)).thenReturn(comment1);
+ when(postCommentService.selectPostCommentById(2L)).thenReturn(comment2);
+ when(postCommentService.selectCommentsByParentId(1L)).thenReturn(new ArrayList<>());
+ when(postCommentService.selectCommentsByParentId(2L)).thenReturn(new ArrayList<>());
+ when(postCommentService.deletePostCommentByIds(any())).thenReturn(2);
+ when(postService.updatePostComments(eq(1L), eq(-1))).thenReturn(1);
+
+ mockMvc.perform(delete("/post/comment/{commentIds}", "1,2"))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).selectPostCommentById(1L);
+ verify(postCommentService, times(1)).selectPostCommentById(2L);
+ verify(postCommentService, times(1)).deletePostCommentByIds(any());
+ verify(postService, times(2)).updatePostComments(eq(1L), eq(-1));
+ }
+
+ /**
+ * 测试添加空内容评论(应该返回验证错误)
+ */
+ @Test
+ void testAddEmptyContentComment() throws Exception {
+ PostComment comment = new PostComment();
+ comment.setPostId(1L);
+ comment.setContent(""); // 空内容
+
+ mockMvc.perform(post("/post/comment")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(comment)))
+ .andExpect(status().isBadRequest());
+
+ verify(postCommentService, never()).insertPostComment(any(PostComment.class));
+ }
+
+ /**
+ * 测试更新评论状态
+ */
+ @Test
+ void testUpdateCommentStatus() throws Exception {
+ PostComment comment = new PostComment();
+ comment.setCommentId(1L);
+ comment.setStatus("0");
+ comment.setContent("有效内容");
+
+ when(postCommentService.updatePostComment(any(PostComment.class))).thenReturn(1);
+
+ mockMvc.perform(put("/post/comment")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(comment)))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).updatePostComment(any(PostComment.class));
+ }
+
+ /**
+ * 测试获取不存在评论的详情
+ */
+ @Test
+ void testGetNonExistentCommentInfo() throws Exception {
+ when(postCommentService.selectPostCommentById(999L)).thenReturn(null);
+
+ mockMvc.perform(get("/post/comment/{commentId}", 999L))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).selectPostCommentById(999L);
+ }
+
+ /**
+ * 测试编辑不存在的评论
+ */
+ @Test
+ void testEditNonExistentComment() throws Exception {
+ PostComment comment = new PostComment();
+ comment.setCommentId(999L);
+ comment.setContent("不存在的评论");
+
+ when(postCommentService.updatePostComment(any(PostComment.class))).thenReturn(0);
+
+ mockMvc.perform(put("/post/comment")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(comment)))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).updatePostComment(any(PostComment.class));
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/web/controller/post/PostControllerTest.java b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/post/PostControllerTest.java
new file mode 100644
index 0000000..34b2992
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/post/PostControllerTest.java
@@ -0,0 +1,406 @@
+package com.ruoyi.web.controller.post;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.web.controller.post.controller.PostController;
+import com.ruoyi.web.controller.post.domain.Post;
+import com.ruoyi.web.controller.post.domain.PostComment;
+import com.ruoyi.web.controller.post.domain.PostFavorite;
+import com.ruoyi.web.controller.post.domain.PostReport;
+import com.ruoyi.web.controller.post.domain.PostTag;
+import com.ruoyi.web.controller.post.service.IPostService;
+import com.ruoyi.web.controller.post.service.IPostTagService;
+import com.ruoyi.web.controller.post.service.IPostCommentService;
+import com.ruoyi.web.controller.post.service.IPostFavoriteService;
+import com.ruoyi.web.controller.post.mapper.PostReportMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+/**
+ * PostController 单元测试
+ *
+ * 测试覆盖所有帖子管理相关的API接口
+ * 包括:帖子列表、详情、增删改查、审核、导出等功能
+ */
+@ExtendWith(MockitoExtension.class)
+class PostControllerTest {
+
+ @Mock
+ private IPostService postService;
+
+ @Mock
+ private IPostTagService postTagService;
+
+ @Mock
+ private IPostCommentService postCommentService;
+
+ @Mock
+ private IPostFavoriteService postFavoriteService;
+
+ @Mock
+ private PostReportMapper postReportMapper;
+
+ @InjectMocks
+ private PostController postController;
+
+ private MockMvc mockMvc;
+ private ObjectMapper objectMapper;
+
+ @BeforeEach
+ void setUp() {
+ // 创建模拟的安全上下文
+ SysUser sysUser = new SysUser();
+ sysUser.setUserId(1L);
+ sysUser.setUserName("testUser");
+
+ LoginUser loginUser = new LoginUser(1L, 1L, sysUser, new HashSet<>());
+
+ UsernamePasswordAuthenticationToken authToken =
+ new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
+ SecurityContextHolder.getContext().setAuthentication(authToken);
+
+ mockMvc = MockMvcBuilders.standaloneSetup(postController).build();
+ objectMapper = new ObjectMapper();
+ }
+
+ /**
+ * 测试获取帖子列表
+ */
+ @Test
+ void testGetPostList() throws Exception {
+ List<Post> posts = new ArrayList<>();
+ Post post = new Post();
+ post.setPostId(1L);
+ post.setTitle("测试帖子");
+ posts.add(post);
+
+ when(postService.selectPostList(any(Post.class))).thenReturn(posts);
+
+ mockMvc.perform(get("/post/list"))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).selectPostList(any(Post.class));
+ }
+
+ /**
+ * 测试获取帖子详情
+ */
+ @Test
+ void testGetPostInfo() throws Exception {
+ Post post = new Post();
+ post.setPostId(1L);
+ post.setTitle("测试帖子");
+ post.setAuthorId(1L);
+ post.setTags("测试标签");
+
+ when(postService.updatePostViews(1L)).thenReturn(1);
+ when(postService.selectPostById(1L)).thenReturn(post);
+ when(postTagService.selectPostTagsByPostId(1L)).thenReturn(new ArrayList<>());
+ when(postCommentService.selectPostCommentList(any())).thenReturn(new ArrayList<>());
+ when(postService.selectAuthorOtherPosts(eq(1L), eq(1L), eq(3))).thenReturn(new ArrayList<>());
+ when(postService.selectSimilarTagsPosts(eq("测试标签"), eq(1L), eq(3))).thenReturn(new ArrayList<>());
+ when(postFavoriteService.selectPostFavoriteByPostIdAndUserId(eq(1L), eq(1L))).thenReturn(null);
+
+ mockMvc.perform(get("/post/{postId}", 1L))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).selectPostById(1L);
+ verify(postService, times(1)).updatePostViews(1L);
+ }
+
+ /**
+ * 测试添加帖子
+ */
+ @Test
+ void testAddPost() throws Exception {
+ Post post = new Post();
+ post.setTitle("新帖子");
+ post.setContent("帖子内容");
+
+ when(postService.insertPost(any(Post.class))).thenReturn(1);
+
+ mockMvc.perform(post("/post")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(post)))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).insertPost(any(Post.class));
+ }
+
+ /**
+ * 测试编辑帖子
+ */
+ @Test
+ void testEditPost() throws Exception {
+ Post post = new Post();
+ post.setPostId(1L);
+ post.setTitle("编辑后的帖子");
+
+ when(postService.updatePost(any(Post.class))).thenReturn(1);
+ when(postTagService.deletePostTagRelation(1L)).thenReturn(1);
+
+ mockMvc.perform(put("/post")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(post)))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).updatePost(any(Post.class));
+ }
+
+ /**
+ * 测试删除帖子
+ */
+ @Test
+ void testDeletePost() throws Exception {
+ when(postCommentService.deletePostCommentByPostId(1L)).thenReturn(1);
+ when(postFavoriteService.deletePostFavoriteByPostId(1L)).thenReturn(1);
+ when(postTagService.deletePostTagRelation(1L)).thenReturn(1);
+ when(postService.deletePostByIds(any())).thenReturn(1);
+
+ mockMvc.perform(delete("/post/{postIds}", "1"))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).deletePostCommentByPostId(1L);
+ verify(postFavoriteService, times(1)).deletePostFavoriteByPostId(1L);
+ verify(postTagService, times(1)).deletePostTagRelation(1L);
+ verify(postService, times(1)).deletePostByIds(any());
+ }
+
+ /**
+ * 测试导出帖子
+ */
+ @Test
+ void testExportPosts() throws Exception {
+ List<Post> posts = new ArrayList<>();
+ when(postService.selectPostList(any(Post.class))).thenReturn(posts);
+
+ mockMvc.perform(post("/post/export"))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).selectPostList(any(Post.class));
+ }
+
+ /**
+ * 测试收藏帖子
+ */
+ @Test
+ void testFavoritePost() throws Exception {
+ Post post = new Post();
+ post.setPostId(1L);
+ post.setTitle("测试帖子");
+
+ when(postService.selectPostById(1L)).thenReturn(post);
+ when(postFavoriteService.selectPostFavoriteByPostIdAndUserId(eq(1L), eq(1L))).thenReturn(null);
+ when(postFavoriteService.insertPostFavorite(any())).thenReturn(1);
+ when(postService.updatePostFavorites(eq(1L), eq(1))).thenReturn(1);
+
+ mockMvc.perform(post("/post/favorite/{postId}", 1L))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).selectPostById(1L);
+ verify(postFavoriteService, times(1)).insertPostFavorite(any());
+ }
+
+ /**
+ * 测试获取收藏列表
+ */
+ @Test
+ void testGetFavoriteList() throws Exception {
+ List<PostFavorite> favorites = new ArrayList<>();
+ when(postFavoriteService.selectPostFavoriteList(any())).thenReturn(favorites);
+
+ mockMvc.perform(get("/post/favorite/list"))
+ .andExpect(status().isOk());
+
+ verify(postFavoriteService, times(1)).selectPostFavoriteList(any());
+ }
+
+ /**
+ * 测试获取待审核帖子列表
+ */
+ @Test
+ void testGetReviewList() throws Exception {
+ List<Post> posts = new ArrayList<>();
+ when(postService.selectPostList(any())).thenReturn(posts);
+
+ mockMvc.perform(get("/post/review/list"))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).selectPostList(any());
+ }
+
+ /**
+ * 测试审核通过帖子
+ */
+ @Test
+ void testReviewPostApprove() throws Exception {
+ Post post = new Post();
+ post.setPostId(1L);
+ post.setStatus("0");
+
+ when(postService.selectPostById(1L)).thenReturn(post);
+ when(postService.updatePost(any())).thenReturn(1);
+
+ String requestBody = "{\"action\":\"approve\",\"reason\":\"审核通过\"}";
+
+ mockMvc.perform(put("/post/review/{postId}", 1L)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(requestBody))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).selectPostById(1L);
+ verify(postService, times(1)).updatePost(any());
+ }
+
+ /**
+ * 测试获取举报列表
+ */
+ @Test
+ void testGetReportList() throws Exception {
+ List<PostReport> reports = new ArrayList<>();
+ when(postReportMapper.selectPostReportList(any())).thenReturn(reports);
+
+ mockMvc.perform(get("/post/report/list"))
+ .andExpect(status().isOk());
+
+ verify(postReportMapper, times(1)).selectPostReportList(any());
+ }
+
+ /**
+ * 测试获取帖子详情(帖子不存在)
+ */
+ @Test
+ void testGetNonExistentPostInfo() throws Exception {
+ when(postService.selectPostById(999L)).thenReturn(null);
+ when(postService.updatePostViews(999L)).thenReturn(0);
+
+ // 控制器现在会检查post是否为null并返回错误
+ mockMvc.perform(get("/post/{postId}", 999L))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).selectPostById(999L);
+ verify(postService, times(1)).updatePostViews(999L);
+ }
+
+ /**
+ * 测试编辑不存在的帖子
+ */
+ @Test
+ void testEditNonExistentPost() throws Exception {
+ Post post = new Post();
+ post.setPostId(999L);
+ post.setTitle("不存在的帖子");
+
+ when(postService.updatePost(any(Post.class))).thenReturn(0);
+ when(postTagService.deletePostTagRelation(999L)).thenReturn(0);
+
+ mockMvc.perform(put("/post")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(post)))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).updatePost(any(Post.class));
+ }
+
+ /**
+ * 测试删除不存在的帖子
+ */
+ @Test
+ void testDeleteNonExistentPost() throws Exception {
+ when(postCommentService.deletePostCommentByPostId(999L)).thenReturn(0);
+ when(postFavoriteService.deletePostFavoriteByPostId(999L)).thenReturn(0);
+ when(postTagService.deletePostTagRelation(999L)).thenReturn(0);
+ when(postService.deletePostByIds(any())).thenReturn(0);
+
+ mockMvc.perform(delete("/post/{postIds}", "999"))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).deletePostCommentByPostId(999L);
+ verify(postFavoriteService, times(1)).deletePostFavoriteByPostId(999L);
+ verify(postService, times(1)).deletePostByIds(any());
+ }
+
+ /**
+ * 测试审核不存在的帖子
+ */
+ @Test
+ void testReviewNonExistentPost() throws Exception {
+ when(postService.selectPostById(999L)).thenReturn(null);
+
+ String requestBody = "{\"action\":\"approve\",\"reason\":\"审核通过\"}";
+
+ mockMvc.perform(put("/post/review/{postId}", 999L)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(requestBody))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).selectPostById(999L);
+ verify(postService, never()).updatePost(any());
+ }
+
+ /**
+ * 测试添加带标签的帖子
+ */
+ @Test
+ void testAddPostWithTags() throws Exception {
+ Post post = new Post();
+ post.setTitle("带标签的帖子");
+ post.setContent("内容");
+ post.setTags("Java,Spring");
+ post.setPostId(1L); // 模拟插入后的ID
+
+ when(postService.insertPost(any(Post.class))).thenReturn(1);
+ when(postTagService.selectPostTagList(any(PostTag.class))).thenReturn(new ArrayList<>());
+ when(postTagService.insertPostTag(any(PostTag.class))).thenReturn(1);
+ when(postTagService.batchInsertPostTagRelation(eq(1L), any())).thenReturn(1);
+
+ mockMvc.perform(post("/post")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(post)))
+ .andExpect(status().isOk());
+
+ verify(postService, times(1)).insertPost(any(Post.class));
+ }
+
+ /**
+ * 测试批量删除帖子
+ */
+ @Test
+ void testBatchDeletePosts() throws Exception {
+ when(postCommentService.deletePostCommentByPostId(1L)).thenReturn(1);
+ when(postCommentService.deletePostCommentByPostId(2L)).thenReturn(1);
+ when(postFavoriteService.deletePostFavoriteByPostId(1L)).thenReturn(1);
+ when(postFavoriteService.deletePostFavoriteByPostId(2L)).thenReturn(1);
+ when(postTagService.deletePostTagRelation(1L)).thenReturn(1);
+ when(postTagService.deletePostTagRelation(2L)).thenReturn(1);
+ when(postService.deletePostByIds(any())).thenReturn(2);
+
+ mockMvc.perform(delete("/post/{postIds}", "1,2"))
+ .andExpect(status().isOk());
+
+ verify(postCommentService, times(1)).deletePostCommentByPostId(1L);
+ verify(postCommentService, times(1)).deletePostCommentByPostId(2L);
+ verify(postFavoriteService, times(1)).deletePostFavoriteByPostId(1L);
+ verify(postFavoriteService, times(1)).deletePostFavoriteByPostId(2L);
+ verify(postService, times(1)).deletePostByIds(any());
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/web/controller/post/PostTagControllerTest.java b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/post/PostTagControllerTest.java
new file mode 100644
index 0000000..a2f3fbb
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/post/PostTagControllerTest.java
@@ -0,0 +1,383 @@
+package com.ruoyi.web.controller.post;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.web.controller.post.controller.PostTagController;
+import com.ruoyi.web.controller.post.domain.PostTag;
+import com.ruoyi.web.controller.post.service.IPostTagService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+import static org.mockito.Mockito.lenient;
+
+/**
+ * PostTagController 单元测试
+ *
+ * 测试覆盖所有标签管理相关的API接口
+ * 包括:标签列表、详情、增删改查、导出等功能
+ */
+@ExtendWith(MockitoExtension.class)
+class PostTagControllerTest {
+
+ @Mock
+ private IPostTagService postTagService;
+
+ @InjectMocks
+ private PostTagController postTagController;
+
+ private MockMvc mockMvc;
+ private ObjectMapper objectMapper;
+
+ @BeforeEach
+ void setUp() {
+ // 创建模拟的安全上下文
+ SysUser sysUser = new SysUser();
+ sysUser.setUserId(1L);
+ sysUser.setUserName("testUser");
+
+ LoginUser loginUser = new LoginUser(1L, 1L, sysUser, new HashSet<>());
+
+ UsernamePasswordAuthenticationToken authToken =
+ new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
+ SecurityContextHolder.getContext().setAuthentication(authToken);
+
+ mockMvc = MockMvcBuilders.standaloneSetup(postTagController).build();
+ objectMapper = new ObjectMapper();
+ }
+
+ /**
+ * 测试获取标签列表
+ */
+ @Test
+ void testGetTagList() throws Exception {
+ List<PostTag> tags = new ArrayList<>();
+ PostTag tag = new PostTag();
+ tag.setTagId(1L);
+ tag.setTagName("测试标签");
+ tags.add(tag);
+
+ when(postTagService.selectPostTagList(any(PostTag.class))).thenReturn(tags);
+
+ mockMvc.perform(get("/post/tag/list"))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).selectPostTagList(any(PostTag.class));
+ }
+
+ /**
+ * 测试获取标签详情
+ */
+ @Test
+ void testGetTagInfo() throws Exception {
+ PostTag tag = new PostTag();
+ tag.setTagId(1L);
+ tag.setTagName("测试标签");
+
+ when(postTagService.selectPostTagById(1L)).thenReturn(tag);
+
+ mockMvc.perform(get("/post/tag/{tagId}", 1L))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).selectPostTagById(1L);
+ }
+
+ /**
+ * 测试添加标签
+ */
+ @Test
+ void testAddTag() throws Exception {
+ PostTag tag = new PostTag();
+ tag.setTagName("新标签");
+
+ when(postTagService.checkTagNameUnique(any())).thenReturn(true);
+ when(postTagService.insertPostTag(any(PostTag.class))).thenReturn(1);
+
+ mockMvc.perform(post("/post/tag")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(tag)))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).checkTagNameUnique(any());
+ verify(postTagService, times(1)).insertPostTag(any(PostTag.class));
+ }
+
+ /**
+ * 测试编辑标签
+ */
+ @Test
+ void testEditTag() throws Exception {
+ PostTag tag = new PostTag();
+ tag.setTagId(1L);
+ tag.setTagName("编辑后的标签");
+
+ when(postTagService.checkTagNameUnique(any())).thenReturn(true);
+ when(postTagService.updatePostTag(any(PostTag.class))).thenReturn(1);
+
+ mockMvc.perform(put("/post/tag")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(tag)))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).checkTagNameUnique(any());
+ verify(postTagService, times(1)).updatePostTag(any(PostTag.class));
+ }
+
+ /**
+ * 测试删除标签
+ */
+ @Test
+ void testDeleteTag() throws Exception {
+ when(postTagService.deletePostTagByIds(any())).thenReturn(1);
+
+ mockMvc.perform(delete("/post/tag/{tagIds}", "1"))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).deletePostTagByIds(any());
+ }
+
+ /**
+ * 测试批量删除标签
+ */
+ @Test
+ void testBatchDeleteTags() throws Exception {
+ when(postTagService.deletePostTagByIds(any())).thenReturn(2);
+
+ mockMvc.perform(delete("/post/tag/{tagIds}", "1,2"))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).deletePostTagByIds(any());
+ }
+
+ /**
+ * 测试导出标签
+ */
+ @Test
+ void testExportTags() throws Exception {
+ List<PostTag> tags = new ArrayList<>();
+ when(postTagService.selectPostTagList(any(PostTag.class))).thenReturn(tags);
+
+ mockMvc.perform(post("/post/tag/export"))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).selectPostTagList(any(PostTag.class));
+ }
+
+ /**
+ * 测试根据帖子ID获取标签列表
+ */
+ @Test
+ void testListByPostId() throws Exception {
+ List<PostTag> tags = new ArrayList<>();
+ when(postTagService.selectPostTagsByPostId(1L)).thenReturn(tags);
+
+ mockMvc.perform(get("/post/tag/list/{postId}", 1L))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).selectPostTagsByPostId(1L);
+ }
+
+ /**
+ * 测试获取可用标签选项
+ */
+ @Test
+ void testOptionSelect() throws Exception {
+ List<PostTag> tags = new ArrayList<>();
+ PostTag tag = new PostTag();
+ tag.setTagId(1L);
+ tag.setTagName("可用标签");
+ tag.setStatus("0");
+ tags.add(tag);
+
+ when(postTagService.selectPostTagList(any(PostTag.class))).thenReturn(tags);
+
+ mockMvc.perform(get("/post/tag/optionselect"))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).selectPostTagList(any(PostTag.class));
+ }
+
+ /**
+ * 测试获取不存在的标签详情
+ */
+ @Test
+ void testGetNonExistentTagInfo() throws Exception {
+ when(postTagService.selectPostTagById(999L)).thenReturn(null);
+
+ mockMvc.perform(get("/post/tag/{tagId}", 999L))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).selectPostTagById(999L);
+ }
+
+ /**
+ * 测试编辑不存在的标签
+ */
+ @Test
+ void testEditNonExistentTag() throws Exception {
+ PostTag tag = new PostTag();
+ tag.setTagId(999L);
+ tag.setTagName("不存在的标签");
+
+ when(postTagService.checkTagNameUnique(any())).thenReturn(true);
+ when(postTagService.updatePostTag(any(PostTag.class))).thenReturn(0);
+
+ mockMvc.perform(put("/post/tag")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(tag)))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).updatePostTag(any(PostTag.class));
+ }
+
+ /**
+ * 测试删除不存在的标签
+ */
+ @Test
+ void testDeleteNonExistentTag() throws Exception {
+ when(postTagService.deletePostTagByIds(any())).thenReturn(0);
+
+ mockMvc.perform(delete("/post/tag/{tagIds}", "999"))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).deletePostTagByIds(any());
+ }
+
+ /**
+ * 测试添加重复标签名
+ */
+ @Test
+ void testAddDuplicateTagName() throws Exception {
+ PostTag tag = new PostTag();
+ tag.setTagName("重复标签");
+
+ when(postTagService.checkTagNameUnique(any())).thenReturn(false);
+
+ mockMvc.perform(post("/post/tag")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(tag)))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).checkTagNameUnique(any());
+ verify(postTagService, never()).insertPostTag(any());
+ }
+
+ /**
+ * 测试编辑为重复标签名
+ */
+ @Test
+ void testEditToDuplicateTagName() throws Exception {
+ PostTag tag = new PostTag();
+ tag.setTagId(1L);
+ tag.setTagName("重复标签");
+
+ when(postTagService.checkTagNameUnique(any())).thenReturn(false);
+
+ mockMvc.perform(put("/post/tag")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(tag)))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).checkTagNameUnique(any());
+ verify(postTagService, never()).updatePostTag(any());
+ }
+
+ /**
+ * 测试添加标签失败
+ */
+ @Test
+ void testAddTagFailed() throws Exception {
+ PostTag tag = new PostTag();
+ tag.setTagName("新标签");
+
+ when(postTagService.checkTagNameUnique(any())).thenReturn(true);
+ when(postTagService.insertPostTag(any(PostTag.class))).thenReturn(0);
+
+ mockMvc.perform(post("/post/tag")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(tag)))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).checkTagNameUnique(any());
+ verify(postTagService, times(1)).insertPostTag(any(PostTag.class));
+ }
+
+ /**
+ * 测试编辑标签失败
+ */
+ @Test
+ void testEditTagFailed() throws Exception {
+ PostTag tag = new PostTag();
+ tag.setTagId(1L);
+ tag.setTagName("编辑后的标签");
+
+ when(postTagService.checkTagNameUnique(any())).thenReturn(true);
+ when(postTagService.updatePostTag(any(PostTag.class))).thenReturn(0);
+
+ mockMvc.perform(put("/post/tag")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(tag)))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).checkTagNameUnique(any());
+ verify(postTagService, times(1)).updatePostTag(any(PostTag.class));
+ }
+
+ /**
+ * 测试删除标签失败
+ */
+ @Test
+ void testDeleteTagFailed() throws Exception {
+ when(postTagService.deletePostTagByIds(any())).thenReturn(0);
+
+ mockMvc.perform(delete("/post/tag/{tagIds}", "1"))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).deletePostTagByIds(any());
+ }
+
+ /**
+ * 测试获取空的标签列表
+ */
+ @Test
+ void testGetEmptyTagList() throws Exception {
+ List<PostTag> tags = new ArrayList<>();
+ when(postTagService.selectPostTagList(any(PostTag.class))).thenReturn(tags);
+
+ mockMvc.perform(get("/post/tag/list"))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).selectPostTagList(any(PostTag.class));
+ }
+
+ /**
+ * 测试根据不存在的帖子ID获取标签列表
+ */
+ @Test
+ void testListByNonExistentPostId() throws Exception {
+ List<PostTag> tags = new ArrayList<>();
+ when(postTagService.selectPostTagsByPostId(999L)).thenReturn(tags);
+
+ mockMvc.perform(get("/post/tag/list/{postId}", 999L))
+ .andExpect(status().isOk());
+
+ verify(postTagService, times(1)).selectPostTagsByPostId(999L);
+ }
+}
\ No newline at end of file