Merge branch 'master' into wyh

Change-Id: Idba8e92cb1c0516bac3878ec219cd1ee59e6e664
diff --git a/src/main/java/com/example/g8backend/controller/CommentController.java b/src/main/java/com/example/g8backend/controller/CommentController.java
new file mode 100644
index 0000000..353a45d
--- /dev/null
+++ b/src/main/java/com/example/g8backend/controller/CommentController.java
@@ -0,0 +1,35 @@
+package com.example.g8backend.controller;
+
+import com.example.g8backend.entity.Comment;
+import com.example.g8backend.service.ICommentService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/comments")
+public class CommentController {
+
+    @Autowired
+    private ICommentService commentService;
+
+    // 创建评论
+    @PostMapping
+    public void createComment(@RequestParam Long postId, @RequestParam Long userId,
+                              @RequestParam String content, @RequestParam(required = false) Long parentCommentId) {
+        commentService.createComment(postId, userId, content, parentCommentId);
+    }
+
+    // 获取某帖子下的所有评论,包括顶级评论及其子评论
+    @GetMapping("/post/{postId}")
+    public List<Comment> getCommentsByPostId(@PathVariable Long postId) {
+        return commentService.getCommentsByPostId(postId);
+    }
+
+    // 删除评论
+    @DeleteMapping("/{commentId}")
+    public void deleteComment(@PathVariable Long commentId) {
+        commentService.deleteComment(commentId);
+    }
+}
diff --git a/src/main/java/com/example/g8backend/controller/PostController.java b/src/main/java/com/example/g8backend/controller/PostController.java
index f373539..68e0f95 100644
--- a/src/main/java/com/example/g8backend/controller/PostController.java
+++ b/src/main/java/com/example/g8backend/controller/PostController.java
@@ -1,6 +1,10 @@
 package com.example.g8backend.controller;
-
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.example.g8backend.dto.PostCreateDTO;
+import com.example.g8backend.dto.PostHistoryDTO;
+import com.example.g8backend.entity.PostView;
+import com.example.g8backend.mapper.PostViewMapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.core.Authentication;
@@ -8,22 +12,20 @@
 import org.springframework.web.bind.annotation.*;
 import com.example.g8backend.entity.Post;
 import com.example.g8backend.service.IPostService;
-
 import java.util.List;
-
 @RestController
 @RequestMapping("/post")
 public class PostController {
     @Autowired
     private IPostService postService;
-
+    @Autowired  // ✅ 新增注入
+    private PostViewMapper postViewMapper;
     @PostMapping("")
     public ResponseEntity<?> createPost(@RequestBody PostCreateDTO postCreateDTO) {
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
         long userId = (long) authentication.getPrincipal();
         Post post = postCreateDTO.getPost();
         Long[] tagIds = postCreateDTO.getTagIds();
-
         post.setUserId(userId);
         if (tagIds.length > 0){
             postService.createPost(post, tagIds);
@@ -32,12 +34,16 @@
         }
         return ResponseEntity.ok().build();
     }
-
     @GetMapping("/{postId}")
-    public Post getPost(@PathVariable("postId") Long postId) {
+    public Post getPost(@PathVariable Long postId) {
+        // 获取当前用户ID
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        long userId = (long) authentication.getPrincipal();
+        // 记录浏览行为
+        postService.recordViewHistory(userId, postId);
+        // 返回帖子详情
         return postService.getById(postId);
     }
-
     @DeleteMapping("/{postId}")
     public ResponseEntity<?> deletePost(@PathVariable("postId") Long postId) {
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
@@ -52,42 +58,37 @@
         postService.removeById(postId);
         return ResponseEntity.ok().body("Post deleted successfully.");
     }
-
     @GetMapping("/getAll")
     public List<Post> getAllPosts() {
         return postService.list();
     }
-
     @GetMapping("/getByUserId/{userId}")
     public List<Post> getPostsByUserId(@PathVariable("userId") Long userId) {
         return postService.getPostsByUserId(userId);
     }
-
     @PutMapping("/{postId}")
     public ResponseEntity<?> updatePost(@PathVariable("postId") Long postId, @RequestBody Post post) {
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
         long userId = (long) authentication.getPrincipal();
         Post existingPost = postService.getById(postId);
-        
+
         if (existingPost == null) {
             return ResponseEntity.status(500).body("Post not found.");
         }
         if (existingPost.getUserId() != userId) {
             return ResponseEntity.status(403).body("You are not authorized to update this post.");
         }
-        
+
         post.setPostId(postId);
         post.setUserId(userId);
         postService.updateById(post);
         return ResponseEntity.ok().body("Post updated successfully.");
     }
-
     @GetMapping("/type/{postType}")
     public ResponseEntity<?> getPostsByType(@PathVariable String postType) {
         List<Post> posts = postService.getPostsByType(postType);
         return ResponseEntity.ok().body(posts);
     }
-
     @PostMapping("/{postId}/like")
     public ResponseEntity<?> likePost(@PathVariable Long postId) {
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
@@ -95,7 +96,6 @@
         postService.likePost(userId, postId);
         return ResponseEntity.ok().body("Post liked successfully.");
     }
-
     @DeleteMapping("/{postId}/like")
     public ResponseEntity<?> unlikePost(@PathVariable Long postId) {
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
@@ -103,10 +103,49 @@
         postService.unlikePost(userId, postId);
         return ResponseEntity.ok().body("Post unliked successfully.");
     }
-
     @GetMapping("/{postId}/likes")
     public ResponseEntity<?> getPostLikeCount(@PathVariable Long postId) {
         Long likeCount = postService.getPostLikeCount(postId);
         return ResponseEntity.ok().body(likeCount);
     }
-}
+    // 搜索帖子
+    @GetMapping("/search")
+    public List<Post> searchPosts(
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) List<Long> tags,  // 修改为接收多个标签
+            @RequestParam(required = false) String author) {
+        return postService.searchPosts(keyword, tags, author);
+    }
+
+    @GetMapping("/history")
+    public ResponseEntity<List<PostHistoryDTO>> getViewHistory() {
+        // 获取当前用户ID
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        long userId = (long) authentication.getPrincipal();
+
+        // 调用Service层
+        List<PostHistoryDTO> history = postService.getViewHistoryWithTitles(userId);
+        return ResponseEntity.ok(history);
+    }
+    @GetMapping("/recommended")
+    public ResponseEntity<Page<Post>> getRecommendedPosts(
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "10") int size) {
+        // 获取当前用户ID
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        long userId = (long) authentication.getPrincipal();
+        // 调用 Service 层方法
+        Page<Post> pageResult = postService.getRecommendedPosts(page, size, userId);
+        return ResponseEntity.ok(pageResult);
+    }
+    @GetMapping("/recommended-by-tags")
+    public ResponseEntity<Page<Post>> getRecommendedByTags(
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "10") int size) {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        long userId = (long) authentication.getPrincipal();
+        // 调用标签推荐方法
+        Page<Post> result = postService.getRecommendedByTags(page, size, userId);
+        return ResponseEntity.ok(result);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/dto/CommentDTO.java b/src/main/java/com/example/g8backend/dto/CommentDTO.java
new file mode 100644
index 0000000..d36e748
--- /dev/null
+++ b/src/main/java/com/example/g8backend/dto/CommentDTO.java
@@ -0,0 +1,12 @@
+package com.example.g8backend.dto;
+
+import java.util.List;
+
+public class CommentDTO {
+    private Long commentId;
+    private Long userId;
+    private String content;
+    private List<CommentDTO> replies;
+
+    // Getter and Setter
+}
diff --git a/src/main/java/com/example/g8backend/dto/PostHistoryDTO.java b/src/main/java/com/example/g8backend/dto/PostHistoryDTO.java
new file mode 100644
index 0000000..2afc7b3
--- /dev/null
+++ b/src/main/java/com/example/g8backend/dto/PostHistoryDTO.java
@@ -0,0 +1,13 @@
+// com.example.g8backend.dto.PostHistoryDTO.java
+package com.example.g8backend.dto;
+
+import lombok.Data;
+import java.time.LocalDateTime;
+
+@Data
+public class PostHistoryDTO {
+    private Long viewId;
+    private Long postId;
+    private String postTitle; // 新增标题字段
+    private LocalDateTime viewTime;
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/entity/Comment.java b/src/main/java/com/example/g8backend/entity/Comment.java
new file mode 100644
index 0000000..30fa763
--- /dev/null
+++ b/src/main/java/com/example/g8backend/entity/Comment.java
@@ -0,0 +1,73 @@
+package com.example.g8backend.entity;
+
+import java.sql.Timestamp;
+import java.util.List;
+
+public class Comment {
+    private Long commentId;            // 评论ID
+    private Long postId;               // 所属帖子ID
+    private Long userId;               // 评论用户ID
+    private String content;            // 评论内容
+    private Long parentCommentId;      // 父评论ID,如果是顶级评论则为NULL
+    private Timestamp createdAt;       // 评论时间
+
+    private List<Comment> replies;
+
+    // Getter and Setter
+    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;
+    }
+
+    public Long getUserId() {
+        return userId;
+    }
+
+    public void setUserId(Long userId) {
+        this.userId = userId;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public Long getParentCommentId() {
+        return parentCommentId;
+    }
+
+    public void setParentCommentId(Long parentCommentId) {
+        this.parentCommentId = parentCommentId;
+    }
+
+    public Timestamp getCreatedAt() {
+        return createdAt;
+    }
+
+    public void setCreatedAt(Timestamp createdAt) {
+        this.createdAt = createdAt;
+    }
+
+    // Getter and Setter for replies
+    public List<Comment> getReplies() {
+        return replies;
+    }
+
+    public void setReplies(List<Comment> replies) {
+        this.replies = replies;
+    }
+}
diff --git a/src/main/java/com/example/g8backend/entity/Post.java b/src/main/java/com/example/g8backend/entity/Post.java
index ac7ff20..351e24d 100644
--- a/src/main/java/com/example/g8backend/entity/Post.java
+++ b/src/main/java/com/example/g8backend/entity/Post.java
@@ -1,14 +1,16 @@
 package com.example.g8backend.entity;
 
 import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
-
 import java.sql.Timestamp;
 import lombok.Data;
+import lombok.experimental.Accessors;
 
 @Data
 @TableName("posts")
+@Accessors(chain = true)
 public class Post {
     @TableId(type = IdType.AUTO)
     private Long postId;
@@ -19,6 +21,17 @@
     private Timestamp createdAt;
     private String postType;
 
+    @TableField("view_count")
+    private Integer viewCount = 0;
+
+    // 新增热度评分字段
+    @TableField("hot_score")
+    private Double hotScore = 5.0; // 初始热度
+
+    // 新增最后计算时间字段
+    @TableField("last_calculated")
+    private Timestamp lastCalculated;
+
     @Override
     public String toString() {
         return "Post{" +
@@ -28,6 +41,9 @@
                 ", postContent='" + postContent + '\'' +
                 ", createdAt=" + createdAt +
                 ", postType='" + postType + '\'' +
+                ", viewCount=" + viewCount +
+                ", hotScore=" + hotScore +          // 新增字段
+                ", lastCalculated=" + lastCalculated + // 新增字段
                 '}';
     }
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/entity/PostLike.java b/src/main/java/com/example/g8backend/entity/PostLike.java
new file mode 100644
index 0000000..c3df741
--- /dev/null
+++ b/src/main/java/com/example/g8backend/entity/PostLike.java
@@ -0,0 +1,41 @@
+package com.example.g8backend.entity;
+
+import java.sql.Timestamp;
+
+public class PostLike {
+
+    private Long userId;
+    private Long postId;
+    private Timestamp createdAt;
+
+    public PostLike(Long userId, Long postId) {
+        this.userId = userId;
+        this.postId = postId;
+        this.createdAt = new Timestamp(System.currentTimeMillis());
+    }
+
+    // Getters and Setters
+    public Long getUserId() {
+        return userId;
+    }
+
+    public void setUserId(Long userId) {
+        this.userId = userId;
+    }
+
+    public Long getPostId() {
+        return postId;
+    }
+
+    public void setPostId(Long postId) {
+        this.postId = postId;
+    }
+
+    public Timestamp getCreatedAt() {
+        return createdAt;
+    }
+
+    public void setCreatedAt(Timestamp createdAt) {
+        this.createdAt = createdAt;
+    }
+}
diff --git a/src/main/java/com/example/g8backend/entity/PostView.java b/src/main/java/com/example/g8backend/entity/PostView.java
new file mode 100644
index 0000000..72de7ca
--- /dev/null
+++ b/src/main/java/com/example/g8backend/entity/PostView.java
@@ -0,0 +1,20 @@
+package com.example.g8backend.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.IdType;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.time.LocalDateTime;
+
+@Data
+@Accessors(chain = true)
+@TableName("post_views") // 指定数据库表名
+public class PostView {
+        @TableId(value = "view_id", type = IdType.AUTO) // 主键映射为 view_id,自增
+        private Long viewId;
+        private Long userId;    // 对应 user_id 字段(自动驼峰转下划线)
+        private Long postId;    // 对应 post_id 字段(自动驼峰转下划线)
+        private LocalDateTime viewTime; // 对应 view_time 字段
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/entity/User.java b/src/main/java/com/example/g8backend/entity/User.java
index a9f5746..64f4f10 100644
--- a/src/main/java/com/example/g8backend/entity/User.java
+++ b/src/main/java/com/example/g8backend/entity/User.java
@@ -26,6 +26,4 @@
                 ", email='" + email + '\'' +
                 '}';
     }
-
-
 }
diff --git a/src/main/java/com/example/g8backend/entity/UserTagPreference.java b/src/main/java/com/example/g8backend/entity/UserTagPreference.java
new file mode 100644
index 0000000..42ac4fd
--- /dev/null
+++ b/src/main/java/com/example/g8backend/entity/UserTagPreference.java
@@ -0,0 +1,15 @@
+
+package com.example.g8backend.entity;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import java.sql.Timestamp;
+@Data
+@Accessors(chain = true)
+@TableName("user_tag_preference") // 映射数据库表名
+public class UserTagPreference {
+    private Long userId;      // 用户ID
+    private Long tagId;       // 标签ID
+    private Double weight;    // 偏好权重
+    private Timestamp lastUpdated; // 最后更新时间
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/mapper/CommentMapper.java b/src/main/java/com/example/g8backend/mapper/CommentMapper.java
new file mode 100644
index 0000000..8387b80
--- /dev/null
+++ b/src/main/java/com/example/g8backend/mapper/CommentMapper.java
@@ -0,0 +1,29 @@
+package com.example.g8backend.mapper;
+
+import com.example.g8backend.entity.Comment;
+import org.apache.ibatis.annotations.*;
+
+import java.util.List;
+
+@Mapper
+public interface CommentMapper {
+
+    // 插入评论
+    @Insert("INSERT INTO comments (post_id, user_id, content, parent_comment_id) VALUES (#{postId}, #{userId}, #{content}, #{parentCommentId})")
+    int insert(Comment comment);
+
+    // 获取顶级评论
+    @Select("SELECT * FROM comments WHERE post_id = #{postId} AND parent_comment_id IS NULL ORDER BY created_at DESC")
+    List<Comment> findTopLevelCommentsByPostId(Long postId);
+
+    // 获取某个父评论ID对应的子评论
+    @Select("SELECT * FROM comments WHERE parent_comment_id = #{parentCommentId} ORDER BY created_at DESC")
+    List<Comment> findRepliesByParentCommentId(Long parentCommentId);
+
+    // 删除评论
+    @Delete("DELETE FROM comments WHERE comment_id = #{commentId}")
+    int deleteById(Long commentId);
+
+    @Select("SELECT COUNT(*) FROM comments WHERE post_id = #{postId}")
+    Long selectCountByPostId(@Param("postId") Long postId);
+}
diff --git a/src/main/java/com/example/g8backend/mapper/PostMapper.java b/src/main/java/com/example/g8backend/mapper/PostMapper.java
index 6db4e67..baebb17 100644
--- a/src/main/java/com/example/g8backend/mapper/PostMapper.java
+++ b/src/main/java/com/example/g8backend/mapper/PostMapper.java
@@ -1,13 +1,67 @@
 package com.example.g8backend.mapper;
-
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.example.g8backend.entity.Post;
-import org.apache.ibatis.annotations.Mapper;
-import org.apache.ibatis.annotations.Param;
-
+import com.example.g8backend.entity.PostLike;
+import org.apache.ibatis.annotations.*;
 import java.util.List;
-
 @Mapper
 public interface PostMapper extends BaseMapper<Post> {
+    // 获取用户的帖子
     List<Post> getPostsByUserId(@Param("userId") Long userId);
-}
+    // 搜索帖子
+    @Select("<script>" +
+            "SELECT p.* " +
+            "FROM posts p " +
+            "LEFT JOIN post_tag pt ON p.post_id = pt.post_id " +
+            "LEFT JOIN tags t ON pt.tag_id = t.tag_id " +
+            "LEFT JOIN users u ON p.user_id = u.user_id " +
+            "WHERE (p.post_title LIKE CONCAT('%', #{keyword}, '%') OR p.post_content LIKE CONCAT('%', #{keyword}, '%')) " +
+            "<if test='tagIds != null and tagIds.size() > 0'> " +
+            "AND pt.tag_id IN " +
+            "<foreach item='tagId' collection='tagIds' open='(' separator=',' close=')'> " +
+            "#{tagId} " +
+            "</foreach> " +
+            "</if>" +
+            "<if test='author != null'> " +
+            "AND u.user_name = #{author} " +
+            "</if>" +
+            "</script>")
+    List<Post> searchPosts(@Param("keyword") String keyword,
+                           @Param("tagIds") List<Long> tagIds,
+                           @Param("author") String author);
+    //  检查用户是否已经点赞该帖子
+    @Select("SELECT EXISTS (SELECT 1 FROM post_likes WHERE user_id = #{userId} AND post_id = #{postId})")
+    boolean existsByUserIdAndPostId(@Param("userId") Long userId, @Param("postId") Long postId);
+    // 插入一条点赞记录
+    @Insert("INSERT INTO post_likes (user_id, post_id) VALUES (#{userId}, #{postId})")
+    void insert(PostLike postLike);
+    // 删除用户对帖子的点赞记录
+    @Delete("DELETE FROM post_likes WHERE user_id = #{userId} AND post_id = #{postId}")
+    void deleteLikeByUserIdAndPostId(@Param("userId") Long userId, @Param("postId") Long postId);
+    // 获取某个帖子点赞数
+    @Select("SELECT COUNT(*) FROM post_likes WHERE post_id = #{postId}")
+    Long selectCount(@Param("postId") Long postId);
+    @Update("UPDATE posts SET view_count = view_count + 1 WHERE post_id = #{postId}")
+    void incrementViewCount(Long postId);
+    @Select("SELECT COUNT(*) FROM post_likes WHERE post_id = #{postId}")
+    Long selectLikeCount(Long postId);
+    @Select("SELECT post_id FROM post_views WHERE user_id = #{userId}")
+    List<Long> findViewedPostIds(Long userId);
+    @Update({
+            "<script>",
+            "UPDATE posts",
+            "SET hot_score = CASE",
+            "  <foreach collection='posts' item='post'>",
+            "    WHEN post_id = #{post.postId} THEN #{post.hotScore}",
+            "  </foreach>",
+            "END,",
+            "last_calculated = NOW()",
+            "WHERE post_id IN",
+            "  <foreach collection='posts' item='post' open='(' separator=',' close=')'>",
+            "    #{post.postId}",
+            "  </foreach>",
+            "</script>"
+    })
+    int batchUpdateHotScore(@Param("posts") List<Post> posts);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/mapper/PostTagMapper.java b/src/main/java/com/example/g8backend/mapper/PostTagMapper.java
index 184feb2..4c8cc1e 100644
--- a/src/main/java/com/example/g8backend/mapper/PostTagMapper.java
+++ b/src/main/java/com/example/g8backend/mapper/PostTagMapper.java
@@ -1,17 +1,27 @@
 package com.example.g8backend.mapper;
-
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.example.g8backend.entity.Post;
 import com.example.g8backend.entity.PostTag;
 import com.example.g8backend.entity.Tag;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
-
+import org.apache.ibatis.annotations.Select;
 import java.util.List;
-
 @Mapper
 public interface PostTagMapper extends BaseMapper<PostTag> {
     List<Post> getPostsByTagIds(@Param("tagIds") Long[] tagIds);
     List<Tag> getTagsByPostId(@Param("postId") Long postId);
     int deleteByIds(@Param("postId") Long postId, @Param("tagId") Long tagId);
-}
+    @Select("SELECT tag_id FROM post_tag WHERE post_id = #{postId}")
+    List<Long> findTagIdsByPostId(Long postId);
+    @Select({
+            "<script>",
+            "SELECT post_id FROM post_tag",
+            "WHERE tag_id IN",
+            "<foreach item='tagId' collection='tagIds' open='(' separator=',' close=')'>",
+            "#{tagId}",
+            "</foreach>",
+            "</script>"
+    })
+    List<Long> findPostIdsByTagIds(@Param("tagIds") List<Long> tagIds);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/mapper/PostViewMapper.java b/src/main/java/com/example/g8backend/mapper/PostViewMapper.java
new file mode 100644
index 0000000..7423418
--- /dev/null
+++ b/src/main/java/com/example/g8backend/mapper/PostViewMapper.java
@@ -0,0 +1,21 @@
+package com.example.g8backend.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.g8backend.entity.PostView;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface PostViewMapper extends BaseMapper<PostView> {
+
+
+    @Select("SELECT post_id FROM post_views WHERE user_id = #{userId}")
+    List<Long> findViewedPostIds(@Param("userId") Long userId);
+
+
+    @Select("SELECT * FROM post_views WHERE user_id = #{userId} ORDER BY view_time DESC LIMIT #{limit}")
+    List<PostView> selectRecentViews(@Param("userId") Long userId, @Param("limit") int limit);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/mapper/UserTagPreferenceMapper.java b/src/main/java/com/example/g8backend/mapper/UserTagPreferenceMapper.java
new file mode 100644
index 0000000..c59ea8e
--- /dev/null
+++ b/src/main/java/com/example/g8backend/mapper/UserTagPreferenceMapper.java
@@ -0,0 +1,22 @@
+package com.example.g8backend.mapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.g8backend.entity.UserTagPreference;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+import java.util.List;
+public interface UserTagPreferenceMapper extends BaseMapper<UserTagPreference> {
+    /**
+     * 插入或更新用户标签偏好权重
+     */
+    @Update("INSERT INTO user_tag_preference (user_id, tag_id, weight) " +
+            "VALUES (#{userId}, #{tagId}, #{increment}) " +
+            "ON DUPLICATE KEY UPDATE weight = weight + #{increment}")
+    void insertOrUpdateWeight(
+            @Param("userId") Long userId,
+            @Param("tagId") Long tagId,
+            @Param("increment") Double increment
+    );
+    @Select("SELECT * FROM user_tag_preference WHERE user_id = #{userId}")
+    List<UserTagPreference> selectByUserId(@Param("userId") Long userId);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/service/ICommentService.java b/src/main/java/com/example/g8backend/service/ICommentService.java
new file mode 100644
index 0000000..ea82f8f
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/ICommentService.java
@@ -0,0 +1,16 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.entity.Comment;
+
+import java.util.List;
+
+public interface ICommentService {
+
+    void createComment(Long postId, Long userId, String content, Long parentCommentId);
+
+    List<Comment> getCommentsByPostId(Long postId);
+
+    List<Comment> getReplies(Long parentCommentId, int depth);
+
+    boolean deleteComment(Long commentId);
+}
diff --git a/src/main/java/com/example/g8backend/service/IPostService.java b/src/main/java/com/example/g8backend/service/IPostService.java
index f914946..cd82ef4 100644
--- a/src/main/java/com/example/g8backend/service/IPostService.java
+++ b/src/main/java/com/example/g8backend/service/IPostService.java
@@ -1,7 +1,11 @@
 package com.example.g8backend.service;
 
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.example.g8backend.dto.PostHistoryDTO;
 import com.example.g8backend.entity.Post;
 import com.baomidou.mybatisplus.extension.service.IService;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.util.List;
 
@@ -14,4 +18,19 @@
     Long getPostLikeCount(Long postId);
     void likePost(Long userId, Long postId);
     void unlikePost(Long userId, Long postId);
+
+    List<Post> searchPosts(String keyword, List<Long> tagIds, String author); // 更新为支持多个标签
+
+    @Transactional
+    void recordViewHistory(Long userId, Long postId);
+
+    @Scheduled(cron = "0 */10 * * * *") // 每10分钟执行一次
+    @Transactional
+    void calculateHotScores();
+
+    Page<Post> getRecommendedPosts(int page, int size, Long userId);
+
+    Page<Post> getRecommendedByTags(int page, int size, Long userId);
+
+    List<PostHistoryDTO> getViewHistoryWithTitles(Long userId);
 }
diff --git a/src/main/java/com/example/g8backend/service/impl/CommentServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/CommentServiceImpl.java
new file mode 100644
index 0000000..fae8a1f
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/CommentServiceImpl.java
@@ -0,0 +1,70 @@
+package com.example.g8backend.service.impl;
+
+import com.example.g8backend.entity.Comment;
+import com.example.g8backend.mapper.CommentMapper;
+import com.example.g8backend.service.ICommentService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+@Service
+public class CommentServiceImpl implements ICommentService {
+
+    @Autowired
+    private CommentMapper commentMapper;
+
+    @Override
+    public void createComment(Long postId, Long userId, String content, Long parentCommentId) {
+        Comment comment = new Comment();
+        comment.setPostId(postId);
+        comment.setUserId(userId);
+        comment.setContent(content);
+        comment.setParentCommentId(parentCommentId); // 如果是顶级评论为NULL
+        commentMapper.insert(comment);
+    }
+
+    @Override
+// 获取帖子下的所有评论(顶级评论及其子评论)
+    public List<Comment> getCommentsByPostId(Long postId) {
+        // 获取顶级评论
+        List<Comment> topLevelComments = commentMapper.findTopLevelCommentsByPostId(postId);
+
+        // 获取每个评论的子评论
+        for (Comment comment : topLevelComments) {
+            List<Comment> replies = getReplies(comment.getCommentId(), 0); // 初始深度为0
+            comment.setReplies(replies);  // 设置子评论
+        }
+
+        return topLevelComments;
+    }
+
+
+    // 获取某个评论的子评论,并设置递归深度
+    public List<Comment> getReplies(Long parentCommentId, int depth) {
+        // 如果递归深度超过最大限制,停止递归
+        if (depth > 5) {
+            return new ArrayList<>();
+        }
+
+        // 获取当前评论的子评论
+        List<Comment> replies = commentMapper.findRepliesByParentCommentId(parentCommentId);
+
+        // 遍历每个子评论
+        for (Comment reply : replies) {
+            // 递归获取该子评论的子评论
+            List<Comment> childReplies = getReplies(reply.getCommentId(), depth + 1); // 深度加1
+            reply.setReplies(childReplies);  // 设置该子评论的子评论
+        }
+
+        return replies;
+    }
+
+
+    @Override
+    public boolean deleteComment(Long commentId) {
+        return commentMapper.deleteById(commentId) > 0;
+    }
+}
diff --git a/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java
index d40ddd6..1fda1e2 100644
--- a/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java
+++ b/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java
@@ -1,76 +1,207 @@
 package com.example.g8backend.service.impl;
-
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.example.g8backend.entity.Post;
-import com.example.g8backend.entity.PostTag;
-import com.example.g8backend.mapper.PostMapper;
+import com.example.g8backend.dto.PostHistoryDTO;
+import com.example.g8backend.entity.*;
+import com.example.g8backend.mapper.*;
 import com.example.g8backend.service.IPostService;
 import com.example.g8backend.service.IPostTagService;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.List;
-
+import java.util.stream.Collectors;
 @Service
 public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements IPostService {
-    
     private final PostMapper postMapper;
-
+    private final PostViewMapper postViewMapper;
+    private final CommentMapper commentMapper;
+    private final UserTagPreferenceMapper userTagPreferenceMapper;
+    private final PostTagMapper postTagMapper;
     @Autowired
     private IPostTagService postTagService;
-
-    public PostServiceImpl(PostMapper postMapper) {
+    public PostServiceImpl(PostMapper postMapper, PostViewMapper postViewMapper, CommentMapper commentMapper,
+                           UserTagPreferenceMapper userTagPreferenceMapper, PostTagMapper postTagMapper) {
         this.postMapper = postMapper;
+        this.postViewMapper = postViewMapper;
+        this.commentMapper = commentMapper;
+        this.userTagPreferenceMapper = userTagPreferenceMapper;
+        this.postTagMapper = postTagMapper;
         this.baseMapper = postMapper; // 重要:设置 baseMapper
     }
-
     @Override
     public List<Post> getPostsByUserId(Long userId) {
         return postMapper.getPostsByUserId(userId);
     }
-
-    @Override
     public void createPost(Post post) {
+        post.setHotScore(5.0); // 初始热度
         post.setCreatedAt(new Timestamp(System.currentTimeMillis()));
         save(post);
     }
-
     @Override
     public void createPost(Post post, Long[] tagIds) {
+        post.setHotScore(5.0); // 初始热度
         post.setCreatedAt(new Timestamp(System.currentTimeMillis()));
         save(post);
         for (long tagId : tagIds) {
             postTagService.save(new PostTag(post.getPostId(), tagId));
         }
     }
-
-    @Override 
+    @Override
     public Post updatePost(Post post) {
         updateById(post);
         return getById(post.getPostId());
     }
-
     @Override
     public List<Post> getPostsByType(String postType) {
         QueryWrapper<Post> wrapper = new QueryWrapper<>();
         wrapper.eq("post_type", postType);
         return list(wrapper);
     }
-
-    @Override
-    public Long getPostLikeCount(Long postId) {
-        // TODO: 需要实现post_likes表的查询
-        return 0L;
-    }
-
     @Override
     public void likePost(Long userId, Long postId) {
-        // TODO: 需要实现post_likes表的插入
+        // 检查用户是否已经点赞该帖子
+        boolean exists = postMapper.existsByUserIdAndPostId(userId, postId);
+        if (!exists) {
+            // 如果没有点赞,则执行插入操作
+            PostLike postLike = new PostLike(userId, postId);
+            postMapper.insert(postLike);  // 执行插入点赞记录
+        }
+    }
+    // 取消点赞功能
+    @Override
+    public void unlikePost(Long userId, Long postId) {
+        // 删除用户对帖子的点赞记录
+        postMapper.deleteLikeByUserIdAndPostId(userId, postId);  // 使用新的方法删除记录
+    }
+    // 获取帖子点赞数
+    @Override
+    public Long getPostLikeCount(Long postId) {
+        // 使用 postMapper 直接查询点赞数(调用数据库中的 SQL 查询)
+        return postMapper.selectCount(postId);
+    }
+    @Override
+    public List<Post> searchPosts(String keyword, List<Long> tagIds, String author) {
+        return postMapper.searchPosts(keyword, tagIds, author); // 调用mapper的搜索方法
+    }
+    @Override
+    @Transactional
+    public void recordViewHistory(Long userId, Long postId) {
+        // 1. 插入浏览记录
+        PostView view = new PostView()
+                .setUserId(userId)
+                .setPostId(postId)
+                .setViewTime(new Timestamp(System.currentTimeMillis()).toLocalDateTime());
+        postViewMapper.insert(view);
+        // 2. 原子更新浏览数
+        postMapper.incrementViewCount(postId); // 直接调用原子操作
+        // 3. 新增:更新用户标签偏好
+        updateUserTagPreference(userId, postId);
+    }
+    @Override
+    @Scheduled(cron = "0 */10 * * * *") // 每10分钟执行一次
+    @Transactional
+    public void calculateHotScores() {
+        // 1. 获取所有帖子
+        List<Post> posts = postMapper.selectList(new QueryWrapper<>());
+        Instant now = Instant.now();
+        // 2. 计算每个帖子的热度
+        posts.forEach(post -> {
+            // 计算时间衰减因子(以小时为单位)
+            long hoursSinceCreation = ChronoUnit.HOURS.between(
+                    post.getCreatedAt().toInstant(),
+                    now
+            );
+            // 获取互动数据(点赞数、评论数)
+            Long likeCount = postMapper.selectLikeCount(post.getPostId());
+            Long commentCount = commentMapper.selectCountByPostId(post.getPostId());
+            // 热度计算公式
+            double hotScore = (
+                    Math.log(post.getViewCount() + 1) * 0.2 +
+                            likeCount * 0.5 +
+                            commentCount * 0.3
+            ) / Math.pow(hoursSinceCreation + 2, 1.5);
+            post.setHotScore(hotScore);
+            post.setLastCalculated(new Timestamp(System.currentTimeMillis()));
+        });
+        // 3. 批量更新热度(自定义SQL实现)
+        postMapper.batchUpdateHotScore(posts);
+    }
+    @Override
+    public Page<Post> getRecommendedPosts(int page, int size, Long userId) {
+        // 1. 获取用户已浏览的帖子ID列表
+        List<Long> viewedPostIds = postViewMapper.findViewedPostIds(userId);
+        // 2. 构建查询条件:排除已浏览帖子,按热度降序
+        QueryWrapper<Post> queryWrapper = new QueryWrapper<>();
+        if (!viewedPostIds.isEmpty()) {
+            queryWrapper.notIn("post_id", viewedPostIds);
+        }
+        queryWrapper.orderByDesc("hot_score");
+        // 3. 分页查询
+        return postMapper.selectPage(new Page<>(page, size), queryWrapper);
+    }
+    private void updateUserTagPreference(Long userId, Long postId) {
+        // 获取帖子关联的标签ID列表
+        List<Long> tagIds = postTagMapper.findTagIdsByPostId(postId);
+        // 对每个标签增加权重(示例:每次浏览 +0.1)
+        tagIds.forEach(tagId -> {
+            userTagPreferenceMapper.insertOrUpdateWeight(
+                    userId,
+                    tagId,
+                    0.1 // 权重增量
+            );
+        });
+    }
+    @Override
+    public Page<Post> getRecommendedByTags(int page, int size, Long userId) {
+        // 获取用户偏好标签
+        List<UserTagPreference> preferences = userTagPreferenceMapper.selectByUserId(userId);
+        if (preferences.isEmpty()) {
+            return new Page<>(page, size);
+        }
+        // 获取标签关联的帖子ID
+        List<Long> tagIds = preferences.stream()
+                .map(UserTagPreference::getTagId)
+                .collect(Collectors.toList());
+        List<Long> postIds = postTagMapper.findPostIdsByTagIds(tagIds);
+        if (postIds.isEmpty()) {
+            return new Page<>(page, size);
+        }
+        // 构建查询条件
+        QueryWrapper<Post> queryWrapper = new QueryWrapper<>();
+        queryWrapper.in("post_id", postIds) // 确保正确添加 IN 条件
+                .orderByDesc("hot_score"); // 确保排序条件正确
+        return postMapper.selectPage(new Page<>(page, size), queryWrapper);
     }
 
     @Override
-    public void unlikePost(Long userId, Long postId) {
-        // TODO: 需要实现post_likes表的删除
+    public List<PostHistoryDTO> getViewHistoryWithTitles(Long userId) {
+        // 1. 查询浏览记录(按时间倒序)
+        List<PostView> history = postViewMapper.selectList(
+                new QueryWrapper<PostView>()
+                        .eq("user_id", userId)
+                        .orderByDesc("view_time")
+        );
+
+
+        // 2. 转换为DTO并填充标题
+        return history.stream().map(view -> {
+            PostHistoryDTO dto = new PostHistoryDTO();
+            dto.setViewId(view.getViewId());
+            dto.setPostId(view.getPostId());
+            dto.setViewTime(view.getViewTime());
+
+            // 3. 查询帖子标题
+            Post post = postMapper.selectById(view.getPostId());
+            if (post != null) {
+                dto.setPostTitle(post.getPostTitle());
+            }
+            return dto;
+        }).collect(Collectors.toList());
     }
-}
+}
\ No newline at end of file
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index dc59ec1..bf74097 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -1,14 +1,12 @@
-CREATE DATABASE IF NOT EXISTS g8backend;
-USE g8backend;
-
+-- 用户表(保持不变)
 CREATE TABLE IF NOT EXISTS `users` (
-  user_id INT AUTO_INCREMENT PRIMARY KEY,
-  user_name VARCHAR(255) NOT NULL,
-  password VARCHAR(255) NOT NULL,
-  email VARCHAR(255) NOT NULL UNIQUE,
-  passkey VARCHAR(255) NOT NULL UNIQUE
+  `user_id` INT AUTO_INCREMENT PRIMARY KEY,
+  `user_name` VARCHAR(255) NOT NULL,
+  `password` VARCHAR(255) NOT NULL,
+  `email` VARCHAR(255) NOT NULL UNIQUE,
+  `passkey` VARCHAR(255) NOT NULL UNIQUE
 );
-
+-- 用户统计表
 CREATE TABLE IF NOT EXISTS `user_stats` (
     user_id INT PRIMARY KEY,
     passkey VARCHAR(255) NOT NULL UNIQUE,
@@ -18,80 +16,116 @@
     FOREIGN KEY (user_id) REFERENCES users(user_id),
     FOREIGN KEY (passkey) REFERENCES users(passkey)
 );
-
+-- 种子表
 CREATE TABLE IF NOT EXISTS `torrents` (
-    torrent_id INT AUTO_INCREMENT PRIMARY KEY,
-    user_id INT NOT NULL,
-    torrent_name VARCHAR(255) NOT NULL,
-    info_hash BINARY(20) NOT NULL,
-    file_size FLOAT NOT NULL,
-    FOREIGN KEY (user_id) REFERENCES users(user_id)
+  `torrent_id` INT AUTO_INCREMENT PRIMARY KEY,
+  `user_id` INT NOT NULL,
+  `torrent_name` VARCHAR(255) NOT NULL,
+  `info_hash` BINARY(20) NOT NULL,
+  `file_size` FLOAT NOT NULL,
+  FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`)
 );
-
+-- Peer表(保持不变)
 CREATE TABLE IF NOT EXISTS `peers` (
-  passkey VARCHAR(255) NOT NULL,
-  info_hash BINARY(20) NOT NULL,
-  peer_id VARCHAR(20) NOT NULL,
-  ip_address VARCHAR(128) NOT NULL,
-  port INT NOT NULL,
-  uploaded FLOAT NOT NULL,
-  downloaded FLOAT NOT NULL,
-  last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-  FOREIGN KEY (passkey) REFERENCES users(passkey),
-  PRIMARY KEY (passkey, info_hash, peer_id)
+  `passkey` VARCHAR(255) NOT NULL,
+  `info_hash` BINARY(20) NOT NULL,
+  `peer_id` VARCHAR(20) NOT NULL,
+  `ip_address` VARCHAR(128) NOT NULL,
+  `port` INT NOT NULL,
+  `uploaded` FLOAT NOT NULL,
+  `downloaded` FLOAT NOT NULL,
+  `last_seen` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  FOREIGN KEY (`passkey`) REFERENCES `users`(`passkey`),
+  PRIMARY KEY (`passkey`, `info_hash`, `peer_id`)
 );
-
+-- 帖子表(新增 hot_score 和 last_calculated 字段)
 CREATE TABLE IF NOT EXISTS `posts` (
-    post_id INT AUTO_INCREMENT PRIMARY KEY,
-    user_id INT NOT NULL,
-    post_title VARCHAR(255) NOT NULL,
-    post_content TEXT NOT NULL,
-    post_type ENUM('resource', 'discussion') NOT NULL,
-    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    FOREIGN KEY (user_id) REFERENCES users(user_id)
+  `post_id` INT AUTO_INCREMENT PRIMARY KEY,
+  `user_id` INT NOT NULL,
+  `hot_score` DOUBLE DEFAULT 5.0 COMMENT '热度评分',
+  `view_count` INT DEFAULT 0 COMMENT '浏览数',
+  `post_title` VARCHAR(255) NOT NULL,
+  `post_content` TEXT NOT NULL,
+  `post_type` ENUM('resource', 'discussion') NOT NULL,
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `last_calculated` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后热度计算时间',
+  FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`),
+  INDEX `idx_hot_score` (`hot_score`),          -- 新增热度索引
+  INDEX `idx_post_type` (`post_type`)           -- 新增类型索引
 );
-
+-- 标签表(保持不变)
 CREATE TABLE IF NOT EXISTS `tags`(
-  tag_id INT AUTO_INCREMENT PRIMARY KEY,
-  tag_name VARCHAR(255) NOT NULL UNIQUE,
-  parent_id INT DEFAULT NULL,
-  FOREIGN KEY (parent_id) REFERENCES tags(tag_id)
+  `tag_id` INT AUTO_INCREMENT PRIMARY KEY,
+  `tag_name` VARCHAR(255) NOT NULL UNIQUE,
+  `parent_id` INT DEFAULT NULL,
+  FOREIGN KEY (`parent_id`) REFERENCES `tags`(`tag_id`)
 );
-
+-- 帖子标签关联表(保持不变)
 CREATE TABLE IF NOT EXISTS `post_tag` (
-    post_id INT NOT NULL,
-    tag_id INT NOT NULL,
-    FOREIGN KEY (post_id) REFERENCES posts(post_id),
-    FOREIGN KEY (tag_id) REFERENCES tags(tag_id),
-    PRIMARY KEY (post_id, tag_id)
+  `post_id` INT NOT NULL,
+  `tag_id` INT NOT NULL,
+  FOREIGN KEY (`post_id`) REFERENCES `posts`(`post_id`),
+  FOREIGN KEY (`tag_id`) REFERENCES `tags`(`tag_id`),
+  PRIMARY KEY (`post_id`, `tag_id`)
 );
-
-CREATE TABLE IF NOT EXISTS `post_likes` (
-    user_id INT NOT NULL,
-    post_id INT NOT NULL,
-    FOREIGN KEY (user_id) REFERENCES users(user_id),
-    FOREIGN KEY (post_id) REFERENCES posts(post_id),
-    PRIMARY KEY (user_id, post_id)
-);
-
--- 关注关系表
+-- 用户关注表(保持不变)
 CREATE TABLE IF NOT EXISTS `user_follows` (
-    follower_id INT NOT NULL,
-    followed_id INT NOT NULL,
-    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    FOREIGN KEY (follower_id) REFERENCES users(user_id),
-    FOREIGN KEY (followed_id) REFERENCES users(user_id),
-    PRIMARY KEY (follower_id, followed_id)
+  `follower_id` INT NOT NULL,
+  `followed_id` INT NOT NULL,
+  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+  FOREIGN KEY (`follower_id`) REFERENCES `users`(`user_id`),
+  FOREIGN KEY (`followed_id`) REFERENCES `users`(`user_id`),
+  PRIMARY KEY (`follower_id`, `followed_id`)
 );
-
--- 私信表
+-- 私信表(保持不变)
 CREATE TABLE IF NOT EXISTS `private_messages` (
-    message_id INT AUTO_INCREMENT PRIMARY KEY,
-    sender_id INT NOT NULL,
-    receiver_id INT NOT NULL,
-    content TEXT NOT NULL,
-    sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    is_read BOOLEAN DEFAULT false,
-    FOREIGN KEY (sender_id) REFERENCES users(user_id),
-    FOREIGN KEY (receiver_id) REFERENCES users(user_id)
+  `message_id` INT AUTO_INCREMENT PRIMARY KEY,
+  `sender_id` INT NOT NULL,
+  `receiver_id` INT NOT NULL,
+  `content` TEXT NOT NULL,
+  `sent_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+  `is_read` BOOLEAN DEFAULT false,
+  FOREIGN KEY (`sender_id`) REFERENCES `users`(`user_id`),
+  FOREIGN KEY (`receiver_id`) REFERENCES `users`(`user_id`)
+);
+-- 评论表(保持不变)
+CREATE TABLE IF NOT EXISTS `comments` (
+  `comment_id` INT AUTO_INCREMENT PRIMARY KEY,
+  `post_id` INT NOT NULL,
+  `user_id` INT NOT NULL,
+  `parent_comment_id` INT DEFAULT NULL,
+  `content` TEXT NOT NULL,
+  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+  FOREIGN KEY (`post_id`) REFERENCES `posts`(`post_id`),
+  FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`),
+  FOREIGN KEY (`parent_comment_id`) REFERENCES `comments`(`comment_id`),
+  INDEX `idx_post_id` (`post_id`)  -- 新增评论帖子索引
+);
+-- 帖子点赞表(保持不变)
+CREATE TABLE IF NOT EXISTS `post_likes` (
+  `user_id` INT NOT NULL,
+  `post_id` INT NOT NULL,
+  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`user_id`, `post_id`),
+  FOREIGN KEY (`post_id`) REFERENCES `posts`(`post_id`),
+  FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`)
+);
+-- 帖子浏览记录表(新增复合索引)
+CREATE TABLE IF NOT EXISTS `post_views` (
+  `view_id` INT AUTO_INCREMENT PRIMARY KEY,
+  `user_id` INT NOT NULL,
+  `post_id` INT NOT NULL,
+  `view_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+  FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`),
+  FOREIGN KEY (`post_id`) REFERENCES `posts`(`post_id`),
+  INDEX `idx_user_view_time` (`user_id`, `view_time` DESC)  -- 新增用户浏览时间索引
+);
+CREATE TABLE user_tag_preference (
+    user_id INT NOT NULL COMMENT '用户ID',
+    tag_id INT NOT NULL COMMENT '标签ID',
+    weight DOUBLE DEFAULT 1.0 COMMENT '偏好权重(浏览越多权重越高)',
+    last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '最后更新时间',
+    PRIMARY KEY (user_id, tag_id),
+    FOREIGN KEY (user_id) REFERENCES users(user_id),
+    FOREIGN KEY (tag_id) REFERENCES tags(tag_id)
 );
\ No newline at end of file
diff --git a/src/test/java/com/example/g8backend/service/CommentServiceTest.java b/src/test/java/com/example/g8backend/service/CommentServiceTest.java
new file mode 100644
index 0000000..f76880a
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/CommentServiceTest.java
@@ -0,0 +1,125 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.entity.Comment;
+import com.example.g8backend.mapper.CommentMapper;
+import com.example.g8backend.service.impl.CommentServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(SpringExtension.class)
+@DisplayName("评论服务测试")
+class CommentServiceTest {
+
+    @Mock
+    private CommentMapper commentMapper;
+
+    @InjectMocks
+    private CommentServiceImpl commentService;
+
+    private Comment testComment;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+        testComment = createTestComment();
+    }
+
+    private Comment createTestComment() {
+        Comment comment = new Comment();
+        comment.setCommentId(1L);
+        comment.setPostId(1L);
+        comment.setUserId(1L);
+        comment.setContent("测试评论内容");
+        return comment;
+    }
+
+    @Test
+    @DisplayName("创建评论-成功")
+    void createComment_ShouldSucceed() {
+        // Arrange
+        when(commentMapper.insert(any(Comment.class))).thenReturn(1);
+
+        // Act
+        commentService.createComment(1L, 1L, "测试评论内容", null);
+
+        // Assert
+        verify(commentMapper).insert(any(Comment.class));
+    }
+
+    @Test
+    @DisplayName("获取评论-根据帖子ID返回评论列表")
+    void getCommentsByPostId_ShouldReturnComments() {
+        // Arrange
+        List<Comment> expectedComments = Arrays.asList(testComment);
+        when(commentMapper.findTopLevelCommentsByPostId(1L)).thenReturn(expectedComments);
+
+        // Act
+        List<Comment> result = commentService.getCommentsByPostId(1L);
+
+        // Assert
+        assertNotNull(result);
+        assertFalse(result.isEmpty());
+        assertEquals(testComment.getCommentId(), result.get(0).getCommentId());
+        verify(commentMapper).findTopLevelCommentsByPostId(1L);
+    }
+
+    @Test
+    @DisplayName("删除评论-成功")
+    void deleteComment_ShouldSucceed() {
+        // Arrange
+        when(commentMapper.deleteById(1L)).thenReturn(1);
+
+        // Act
+        boolean result = commentService.deleteComment(1L);
+
+        // Assert
+        assertTrue(result);
+        verify(commentMapper).deleteById(1L);
+    }
+
+    @Test
+    @DisplayName("获取评论-没有评论")
+    void getCommentsByPostId_WhenNoComments_ShouldReturnEmptyList() {
+        // Arrange
+        when(commentMapper.findTopLevelCommentsByPostId(999L)).thenReturn(Arrays.asList());
+
+        // Act
+        List<Comment> result = commentService.getCommentsByPostId(999L);
+
+        // Assert
+        assertNotNull(result);
+        assertTrue(result.isEmpty());
+        verify(commentMapper).findTopLevelCommentsByPostId(999L);
+    }
+
+    @Test
+    @DisplayName("获取评论-根据父评论ID返回子评论")
+    void getReplies_ShouldReturnReplies() {
+        // Arrange
+        List<Comment> expectedReplies = Arrays.asList(testComment);
+        when(commentMapper.findRepliesByParentCommentId(1L)).thenReturn(expectedReplies);
+
+        // Act
+        List<Comment> result = commentService.getReplies(1L, 5);
+
+        // Assert
+        assertNotNull(result);
+        assertFalse(result.isEmpty());
+        assertEquals(testComment.getCommentId(), result.get(0).getCommentId());
+        verify(commentMapper).findRepliesByParentCommentId(1L);
+    }
+
+}
diff --git a/src/test/java/com/example/g8backend/service/PostHistoryServiceTest.java b/src/test/java/com/example/g8backend/service/PostHistoryServiceTest.java
new file mode 100644
index 0000000..c4a674d
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/PostHistoryServiceTest.java
@@ -0,0 +1,141 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.entity.PostView;
+import com.example.g8backend.mapper.*;
+import com.example.g8backend.service.impl.PostServiceImpl;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+@Transactional
+public class PostHistoryServiceTest {
+
+    @Mock
+    private PostViewMapper postViewMapper;
+
+    @Mock
+    private PostMapper postMapper;
+
+    @Mock
+    private PostTagMapper postTagMapper;
+
+    @Mock
+    private UserTagPreferenceMapper userTagPreferenceMapper;
+
+    @InjectMocks
+    private PostServiceImpl postService;
+
+    // ------------------------ 已有测试用例 ------------------------
+    @Test
+    public void testRecordViewHistory_NormalCase() {
+        // 测试数据
+        Long userId = 1L;
+        Long postId = 100L;
+
+        // 调用方法
+        postService.recordViewHistory(userId, postId);
+
+        // 验证行为
+        verify(postViewMapper, times(1)).insert(any(PostView.class));
+        verify(postMapper, times(1)).incrementViewCount(eq(postId));
+    }
+
+    @Test
+    public void testRecordViewHistory_CheckDataIntegrity() {
+        Long userId = 2L;
+        Long postId = 200L;
+
+        postService.recordViewHistory(userId, postId);
+
+        // 显式指定参数类型为 PostView
+        verify(postViewMapper).insert(argThat(new ArgumentMatcher<PostView>() {
+            @Override
+            public boolean matches(PostView view) {
+                return view.getUserId().equals(userId) &&
+                        view.getPostId().equals(postId) &&
+                        view.getViewTime() != null;
+            }
+        }));
+    }
+
+    @Test
+    public void testRecordViewHistory_MultipleCalls() {
+        // 模拟多次调用
+        Long postId = 300L;
+        postService.recordViewHistory(1L, postId);
+        postService.recordViewHistory(2L, postId);
+
+        // 验证浏览数更新次数
+        verify(postMapper, times(2)).incrementViewCount(postId);
+    }
+
+    // ------------------------ 新增测试用例 ------------------------
+    @Test
+    public void testRecordViewHistory_UpdateUserPreferenceWithTags() {
+        // 测试数据
+        Long userId = 3L;
+        Long postId = 400L;
+        List<Long> tagIds = Arrays.asList(10L, 20L);
+
+        // 模拟标签查询
+        when(postTagMapper.findTagIdsByPostId(postId)).thenReturn(tagIds);
+
+        // 调用方法
+        postService.recordViewHistory(userId, postId);
+
+        // 验证标签偏好更新
+        verify(userTagPreferenceMapper, times(2)).insertOrUpdateWeight(
+                eq(userId),
+                anyLong(),
+                eq(0.1) // 假设每次浏览权重增加0.1
+        );
+    }
+
+    @Test
+    public void testRecordViewHistory_NoTagsShouldNotUpdatePreference() {
+        // 测试数据
+        Long userId = 4L;
+        Long postId = 500L;
+
+        // 模拟无关联标签
+        when(postTagMapper.findTagIdsByPostId(postId)).thenReturn(Collections.emptyList());
+
+        // 调用方法
+        postService.recordViewHistory(userId, postId);
+
+        // 验证未调用偏好更新
+        verify(userTagPreferenceMapper, never()).insertOrUpdateWeight(
+                anyLong(),
+                anyLong(),
+                anyDouble()
+        );
+    }
+
+    @Test
+    public void testRecordViewHistory_ConcurrentUpdateSafety() {
+        // 测试数据
+        Long postId = 600L;
+
+        // 模拟多次并发调用
+        postService.recordViewHistory(1L, postId);
+        postService.recordViewHistory(2L, postId);
+        postService.recordViewHistory(3L, postId);
+
+        // 验证原子操作是否被正确调用
+        verify(postMapper, times(3)).incrementViewCount(postId);
+        verify(postViewMapper, times(3)).insert(any(PostView.class));
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/com/example/g8backend/service/PostLikeServiceTest.java b/src/test/java/com/example/g8backend/service/PostLikeServiceTest.java
new file mode 100644
index 0000000..987f672
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/PostLikeServiceTest.java
@@ -0,0 +1,84 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.entity.Post;
+import com.example.g8backend.entity.PostLike;
+import com.example.g8backend.mapper.PostMapper;
+import com.example.g8backend.service.impl.PostServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.Mockito.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class PostLikeServiceTest {
+
+    @InjectMocks
+    private PostServiceImpl postService;
+
+    @Mock
+    private PostMapper postMapper;
+
+    private Post post;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);  // 初始化mock对象
+        post = new Post();
+        post.setPostId(1L);
+        post.setPostTitle("Test Post");
+        post.setPostContent("This is a test post.");
+        post.setUserId(1L);
+    }
+
+    // 测试点赞功能
+    @Test
+    void likePost_ShouldAddLike_WhenNotAlreadyLiked() {
+        Long userId = 1L;
+        Long postId = 1L;
+
+        // 设置返回值,模拟查询用户是否已点赞
+        when(postMapper.existsByUserIdAndPostId(userId, postId)).thenReturn(false);
+
+        // 调用点赞方法
+        postService.likePost(userId, postId);
+
+        // 验证postMapper的insert方法被调用一次,表示插入点赞记录
+        verify(postMapper, times(1)).insert(any(PostLike.class));
+    }
+
+    // 测试取消点赞功能
+    @Test
+    void unlikePost_ShouldRemoveLike_WhenAlreadyLiked() {
+        Long userId = 1L;
+        Long postId = 1L;
+
+        // 设置返回值,模拟查询用户是否已点赞
+        when(postMapper.existsByUserIdAndPostId(userId, postId)).thenReturn(true);
+
+        // 调用取消点赞方法
+        postService.unlikePost(userId, postId);
+
+        // 验证postMapper的deleteLikeByUserIdAndPostId方法被调用一次,表示删除点赞记录
+        verify(postMapper, times(1)).deleteLikeByUserIdAndPostId(userId, postId);
+    }
+
+    // 测试获取帖子点赞数功能
+    @Test
+    void getPostLikeCount_ShouldReturnCorrectCount() {
+        Long postId = 1L;
+        Long expectedCount = 10L;
+
+        // 模拟查询返回点赞数
+        when(postMapper.selectCount(postId)).thenReturn(expectedCount);
+
+        // 调用获取点赞数方法
+        Long actualCount = postService.getPostLikeCount(postId);
+
+        // 验证返回的点赞数是否正确
+        assertEquals(expectedCount, actualCount);
+    }
+}
diff --git a/src/test/java/com/example/g8backend/service/PostServiceRecommendTest.java b/src/test/java/com/example/g8backend/service/PostServiceRecommendTest.java
new file mode 100644
index 0000000..e2a4c7f
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/PostServiceRecommendTest.java
@@ -0,0 +1,204 @@
+package com.example.g8backend.service;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.example.g8backend.entity.Post;
+import com.example.g8backend.entity.PostView;
+import com.example.g8backend.entity.UserTagPreference;
+import com.example.g8backend.mapper.*;
+import com.example.g8backend.service.impl.PostServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.transaction.annotation.Transactional;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+@ExtendWith(MockitoExtension.class)
+@Transactional
+public class PostServiceRecommendTest {
+    @Mock
+    private PostMapper postMapper;
+    @Mock
+    private PostViewMapper postViewMapper;
+    @Mock
+    private CommentMapper commentMapper;
+    @Mock
+    private UserTagPreferenceMapper userTagPreferenceMapper;
+    @Mock
+    private PostTagMapper postTagMapper;
+    @InjectMocks
+    private PostServiceImpl postService;
+    private Post mockPost;
+    private PostView mockPostView;
+    @BeforeEach
+    void setUp() {
+        // 初始化测试数据
+        mockPost = new Post()
+                .setPostId(1L)
+                .setUserId(100L)
+                .setPostTitle("Test Post")
+                .setViewCount(50)
+                .setCreatedAt(Timestamp.from(Instant.now().minusSeconds(7200)))
+                .setHotScore(5.0);
+        mockPostView = new PostView()
+                .setViewId(1L)
+                .setUserId(200L)
+                .setPostId(1L)
+                .setViewTime(Timestamp.from(Instant.now()).toLocalDateTime());
+    }
+    @Test
+    void testRecordViewHistory_NormalCase() {
+        // 调用方法
+        postService.recordViewHistory(100L, 1L);
+        // 验证 postTagMapper 被调用
+        verify(postTagMapper).findTagIdsByPostId(1L);
+        verify(postViewMapper).insert(any(PostView.class));
+        verify(postMapper).incrementViewCount(1L);
+    }
+    @Test
+    public void testGetRecommendedPosts_ExcludesViewedPosts() {
+        // 模拟用户已浏览的帖子ID
+        Long userId = 200L;
+        when(postViewMapper.findViewedPostIds(userId))
+                .thenReturn(Arrays.asList(1L, 2L));
+        // 模拟推荐结果(未浏览的帖子)
+        Post recommendedPost = new Post().setPostId(3L).setHotScore(8.0);
+        when(postMapper.selectPage(any(Page.class), any(QueryWrapper.class)))
+                .thenReturn(new Page<Post>().setRecords(Collections.singletonList(recommendedPost)));
+        // 调用推荐接口
+        Page<Post> result = postService.getRecommendedPosts(1, 10, userId);
+        // 验证结果
+        assertEquals(1, result.getRecords().size(), "应返回1条推荐结果");
+        assertEquals(3L, result.getRecords().get(0).getPostId(), "推荐结果应为未浏览的帖子ID 3");
+        assertFalse(result.getRecords().stream().anyMatch(p -> p.getPostId() == 1L), "结果中不应包含已浏览的帖子ID 1");
+    }
+    @Test
+    public void testGetRecommendedPosts_NoViewedPosts() {
+        // 模拟用户未浏览任何帖子
+        Long userId = 300L;
+        when(postViewMapper.findViewedPostIds(userId))
+                .thenReturn(Collections.emptyList());
+        // 模拟推荐结果(所有帖子按热度排序)
+        Post post1 = new Post().setPostId(1L).setHotScore(7.5);
+        Post post2 = new Post().setPostId(2L).setHotScore(9.0);
+        when(postMapper.selectPage(any(Page.class), any(QueryWrapper.class)))
+                .thenReturn(new Page<Post>().setRecords(Arrays.asList(post2, post1)));
+        // 调用推荐接口
+        Page<Post> result = postService.getRecommendedPosts(1, 10, userId);
+        // 验证结果
+        assertEquals(2, result.getRecords().size(), "应返回所有帖子");
+        assertEquals(2L, result.getRecords().get(0).getPostId(), "热度更高的帖子应排在前面");
+    }
+    @Test
+    public void testCalculateHotScores_UpdatesHotScoreCorrectly() {
+        // 设置存根
+        when(postMapper.selectList(any(QueryWrapper.class)))
+                .thenReturn(Collections.singletonList(mockPost));
+        when(postMapper.selectLikeCount(anyLong())).thenReturn(30L);
+        when(commentMapper.selectCountByPostId(anyLong())).thenReturn(20L);
+        when(postMapper.batchUpdateHotScore(anyList())).thenReturn(1);
+        // 执行并验证
+        postService.calculateHotScores();
+        double expectedScore = (Math.log(51) * 0.2 + 30 * 0.5 + 20 * 0.3) / Math.pow(4, 1.5);
+        assertEquals(expectedScore, mockPost.getHotScore(), 0.01);
+        verify(postMapper).batchUpdateHotScore(anyList());
+    }
+    //--------------------- 测试冷启动逻辑 ---------------------
+    @Test
+    public void testCreatePost_SetsInitialHotScore() {
+        Post newPost = new Post().setPostId(4L).setPostTitle("New Post");
+        postService.createPost(newPost);
+        assertEquals(5.0, newPost.getHotScore(), "新帖子的初始热度应为5.0");
+        assertNotNull(newPost.getCreatedAt(), "创建时间不应为空");
+    }
+    @Test
+    public void testConcurrentViewCountUpdate() {
+        // 设置存根
+        doNothing().when(postMapper).incrementViewCount(anyLong());
+        postService.recordViewHistory(100L, 1L);
+        postService.recordViewHistory(200L, 1L);
+        verify(postMapper, times(2)).incrementViewCount(1L);
+        verify(postViewMapper, times(2)).insert(any(PostView.class));
+    }
+    @Test
+    public void testGetRecommendedByTags_WithPreferredTags() {
+        // 模拟用户偏好标签
+        Long userId = 200L;
+        UserTagPreference pref1 = new UserTagPreference().setTagId(100L).setWeight(2.0);
+        UserTagPreference pref2 = new UserTagPreference().setTagId(200L).setWeight(1.5);
+        when(userTagPreferenceMapper.selectByUserId(userId))
+                .thenReturn(Arrays.asList(pref1, pref2));
+        // 模拟标签关联的帖子ID
+        List<Long> expectedPostIds = Arrays.asList(3L, 4L, 5L);
+        when(postTagMapper.findPostIdsByTagIds(Arrays.asList(100L, 200L)))
+                .thenReturn(expectedPostIds);
+        // 使用 ArgumentCaptor 捕获 QueryWrapper 对象
+        ArgumentCaptor<QueryWrapper<Post>> wrapperCaptor = ArgumentCaptor.forClass(QueryWrapper.class);
+        Page<Post> mockPage = new Page<Post>().setRecords(Arrays.asList(
+                new Post().setPostId(5L).setHotScore(9.0),
+                new Post().setPostId(3L).setHotScore(8.0),
+                new Post().setPostId(4L).setHotScore(7.5)
+        ));
+        when(postMapper.selectPage(any(Page.class), wrapperCaptor.capture()))
+                .thenReturn(mockPage);
+        // 调用标签推荐接口
+        Page<Post> result = postService.getRecommendedByTags(1, 10, userId);
+        // ---------- 验证查询条件 ----------
+        QueryWrapper<Post> actualWrapper = wrapperCaptor.getValue();
+        String sqlSegment = actualWrapper.getSqlSegment();
+        // 验证 SQL 条件格式
+        assertTrue(
+                sqlSegment.matches(".*post_id\\s+IN\\s*\\(.*\\).*"),
+                "应包含 post_id IN 条件,实际条件:" + sqlSegment
+        );
+        // 验证参数值(忽略顺序)
+        Map<String, Object> params = actualWrapper.getParamNameValuePairs();
+        List<Object> actualPostIds = new ArrayList<>(params.values());
+        assertEquals(3, actualPostIds.size(), "IN 条件应包含3个参数");
+        assertTrue(
+                actualPostIds.containsAll(expectedPostIds),
+                "参数应包含所有预期帖子ID,实际参数:" + actualPostIds
+        );
+        // ---------- 验证结果排序和内容 ----------
+        assertEquals(3, result.getRecords().size(), "应返回3条结果");
+        assertEquals(5L, result.getRecords().get(0).getPostId(), "热度最高的帖子应为ID 5");
+        assertEquals(3L, result.getRecords().get(1).getPostId(), "热度次高的帖子应为ID 3");
+        assertEquals(4L, result.getRecords().get(2).getPostId(), "热度最低的帖子应为ID 4");
+    }
+    @Test
+    public void testGetRecommendedByTags_NoPreferredTags() {
+        // 模拟用户无偏好标签
+        Long userId = 300L;
+        when(userTagPreferenceMapper.selectByUserId(userId))
+                .thenReturn(Collections.emptyList());
+        // 调用标签推荐接口
+        Page<Post> result = postService.getRecommendedByTags(1, 10, userId);
+        // 验证结果为空或兜底逻辑
+        assertTrue(result.getRecords().isEmpty(), "无偏好标签时应返回空结果");
+    }
+    @Test
+    public void testGetRecommendedByTags_WithNonExistingTags() {
+        // 模拟用户偏好标签(但无关联帖子)
+        Long userId = 400L;
+        UserTagPreference pref = new UserTagPreference().setTagId(999L).setWeight(2.0);
+        when(userTagPreferenceMapper.selectByUserId(userId))
+                .thenReturn(Collections.singletonList(pref));
+        when(postTagMapper.findPostIdsByTagIds(Collections.singletonList(999L)))
+                .thenReturn(Collections.emptyList());
+        // 调用标签推荐接口
+        Page<Post> result = postService.getRecommendedByTags(1, 10, userId);
+        // 验证结果为空
+        assertNotNull(result, "分页结果不应为null");
+        assertTrue(result.getRecords().isEmpty(), "无关联帖子时应返回空结果");
+        // 验证postMapper.selectPage未被调用
+        verify(postMapper, never()).selectPage(any(Page.class), any(QueryWrapper.class));
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/com/example/g8backend/service/PostServiceTest.java b/src/test/java/com/example/g8backend/service/PostServiceTest.java
index 6f54177..51bc26b 100644
--- a/src/test/java/com/example/g8backend/service/PostServiceTest.java
+++ b/src/test/java/com/example/g8backend/service/PostServiceTest.java
@@ -1,7 +1,6 @@
 package com.example.g8backend.service;
-
 import com.example.g8backend.entity.Post;
-import com.example.g8backend.mapper.PostMapper;
+import com.example.g8backend.mapper.*;
 import com.example.g8backend.service.impl.PostServiceImpl;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
@@ -10,33 +9,31 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.springframework.test.context.junit.jupiter.SpringExtension;
-
 import java.sql.Timestamp;
 import java.util.Arrays;
 import java.util.List;
-
 import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.*;
-
 @ExtendWith(SpringExtension.class)
 @DisplayName("帖子服务测试")
 class PostServiceTest {
-
     @Mock
     private PostMapper postMapper;
-
+    @Mock
+    private PostViewMapper postViewMapper;
+    @Mock
+    private CommentMapper commentMapper;
     private PostServiceImpl postService;
-
+    private UserTagPreferenceMapper userTagPreferenceMapper;
+    private PostTagMapper  postTagMapper;
     private Post testPost;
-
     @BeforeEach
     void setUp() {
         MockitoAnnotations.openMocks(this);
-        postService = new PostServiceImpl(postMapper);
+        postService = new PostServiceImpl(postMapper, postViewMapper,commentMapper,userTagPreferenceMapper,postTagMapper);
         testPost = createTestPost();
     }
-
     private Post createTestPost() {
         Post post = new Post();
         post.setPostId(1L);
@@ -46,107 +43,122 @@
         post.setCreatedAt(new Timestamp(System.currentTimeMillis()));
         return post;
     }
-
     @Test
     @DisplayName("创建帖子-成功")
     void save_ShouldSucceed() {
         // Arrange
         when(postMapper.insert(any(Post.class))).thenReturn(1);
-
         // Act
         boolean result = postService.save(testPost);
-
         // Assert
         assertTrue(result);
         verify(postMapper).insert(testPost);
     }
-
     @Test
     @DisplayName("获取帖子-通过ID存在")
     void getById_WhenExists_ShouldReturnPost() {
         // Arrange
         when(postMapper.selectById(1L)).thenReturn(testPost);
-
         // Act
         Post result = postService.getById(1L);
-
         // Assert
         assertNotNull(result);
         assertEquals(testPost.getPostId(), result.getPostId());
         verify(postMapper).selectById(1L);
     }
-
     @Test
     @DisplayName("获取帖子-通过ID不存在")
     void getById_WhenNotExists_ShouldReturnNull() {
         // Arrange
         when(postMapper.selectById(999L)).thenReturn(null);
-
         // Act
         Post result = postService.getById(999L);
-
         // Assert
         assertNull(result);
         verify(postMapper).selectById(999L);
     }
-
     @Test
     @DisplayName("更新帖子-成功")
     void updateById_ShouldSucceed() {
         // Arrange
         when(postMapper.updateById(any(Post.class))).thenReturn(1);
-
         // Act
         boolean result = postService.updateById(testPost);
-
         // Assert
         assertTrue(result);
         verify(postMapper).updateById(testPost);
     }
-
     @Test
     @DisplayName("删除帖子-成功")
     void removeById_ShouldSucceed() {
         // Arrange
         when(postMapper.deleteById(1L)).thenReturn(1);
-
         // Act
         boolean result = postService.removeById(1L);
-
         // Assert
         assertTrue(result);
         verify(postMapper).deleteById(1L);
     }
-
     @Test
     @DisplayName("获取用户帖子列表")
     void getPostsByUserId_ShouldReturnPosts() {
         // Arrange
         List<Post> expectedPosts = Arrays.asList(testPost);
         when(postMapper.getPostsByUserId(1L)).thenReturn(expectedPosts);
-
         // Act
         List<Post> result = postService.getPostsByUserId(1L);
-
         // Assert
         assertNotNull(result);
         assertFalse(result.isEmpty());
         assertEquals(testPost.getPostId(), result.get(0).getPostId());
         verify(postMapper).getPostsByUserId(1L);
     }
-
     @Test
     @DisplayName("获取用户帖子-空列表")
     void getPostsByUserId_WhenNoPosts_ShouldReturnEmptyList() {
         // Arrange
         when(postMapper.getPostsByUserId(999L)).thenReturn(Arrays.asList());
-
         // Act
         List<Post> result = postService.getPostsByUserId(999L);
-
         // Assert
         assertNotNull(result);
         assertTrue(result.isEmpty());
         verify(postMapper).getPostsByUserId(999L);
     }
+    // 新增测试方法:搜索帖子,支持多个标签
+    @Test
+    @DisplayName("搜索帖子-通过关键词和多个标签")
+    void searchPosts_WithKeywordAndTags_ShouldReturnPosts() {
+        // Arrange
+        List<Long> tagIds = Arrays.asList(1L, 2L); // 假设存在标签ID 1和2
+        String keyword = "测试内容";
+        String author = "作者";
+        List<Post> expectedPosts = Arrays.asList(testPost);
+        // 模拟PostMapper的searchPosts方法
+        when(postMapper.searchPosts(keyword, tagIds, author)).thenReturn(expectedPosts);
+        // Act
+        List<Post> result = postService.searchPosts(keyword, tagIds, author);
+        // Assert
+        assertNotNull(result);
+        assertFalse(result.isEmpty());
+        assertEquals(testPost.getPostId(), result.get(0).getPostId());
+        verify(postMapper).searchPosts(keyword, tagIds, author);
+    }
+    @Test
+    @DisplayName("搜索帖子-没有匹配的帖子")
+    void searchPosts_WhenNoPosts_ShouldReturnEmptyList() {
+        // Arrange
+        List<Long> tagIds = Arrays.asList(3L, 4L); // 假设标签ID 3和4没有匹配的帖子
+        String keyword = "不存在的内容";
+        String author = "不存在的作者";
+        List<Post> expectedPosts = Arrays.asList(); // 没有匹配的帖子
+        // 模拟PostMapper的searchPosts方法
+        when(postMapper.searchPosts(keyword, tagIds, author)).thenReturn(expectedPosts);
+        // Act
+        List<Post> result = postService.searchPosts(keyword, tagIds, author);
+        // Assert
+        assertNotNull(result);
+        assertTrue(result.isEmpty());
+        verify(postMapper).searchPosts(keyword, tagIds, author);
+    }
 }
\ No newline at end of file