RecommendByHot_scores

Change-Id: Icfef76f6ce21a60288c61d60ad1cd2d70045f953
diff --git a/src/main/java/com/example/g8backend/controller/PostController.java b/src/main/java/com/example/g8backend/controller/PostController.java
index 51aa519..03687c3 100644
--- a/src/main/java/com/example/g8backend/controller/PostController.java
+++ b/src/main/java/com/example/g8backend/controller/PostController.java
@@ -1,6 +1,7 @@
 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.entity.PostView;
 import com.example.g8backend.mapper.PostViewMapper;
@@ -148,4 +149,19 @@
 
         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);
+    }
 }
diff --git a/src/main/java/com/example/g8backend/entity/Post.java b/src/main/java/com/example/g8backend/entity/Post.java
index 1a3cd25..351e24d 100644
--- a/src/main/java/com/example/g8backend/entity/Post.java
+++ b/src/main/java/com/example/g8backend/entity/Post.java
@@ -4,12 +4,13 @@
 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;
@@ -23,6 +24,13 @@
     @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() {
@@ -33,7 +41,9 @@
                 ", postContent='" + postContent + '\'' +
                 ", createdAt=" + createdAt +
                 ", postType='" + postType + '\'' +
-                ", viewCount=" + viewCount +  // ✅ 更新 toString 方法
+                ", viewCount=" + viewCount +
+                ", hotScore=" + hotScore +          // 新增字段
+                ", lastCalculated=" + lastCalculated + // 新增字段
                 '}';
     }
-}
+}
\ 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
index b8fa1bf..8387b80 100644
--- a/src/main/java/com/example/g8backend/mapper/CommentMapper.java
+++ b/src/main/java/com/example/g8backend/mapper/CommentMapper.java
@@ -23,4 +23,7 @@
     // 删除评论
     @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 d49125e..b5a5280 100644
--- a/src/main/java/com/example/g8backend/mapper/PostMapper.java
+++ b/src/main/java/com/example/g8backend/mapper/PostMapper.java
@@ -54,4 +54,27 @@
 
     @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);
 }
diff --git a/src/main/java/com/example/g8backend/service/IPostService.java b/src/main/java/com/example/g8backend/service/IPostService.java
index a349b59..ca5376b 100644
--- a/src/main/java/com/example/g8backend/service/IPostService.java
+++ b/src/main/java/com/example/g8backend/service/IPostService.java
@@ -1,7 +1,9 @@
 package com.example.g8backend.service;
 
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 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;
@@ -20,4 +22,10 @@
 
     @Transactional
     void recordViewHistory(Long userId, Long postId);
+
+    @Scheduled(cron = "0 */10 * * * *") // 每10分钟执行一次
+    @Transactional
+    void calculateHotScores();
+
+    Page<Post> getRecommendedPosts(int page, int size, Long userId);
 }
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 f2bef5f..ae47d8a 100644
--- a/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java
+++ b/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java
@@ -1,20 +1,25 @@
 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.PostLike;
 import com.example.g8backend.entity.PostTag;
 import com.example.g8backend.entity.PostView;
+import com.example.g8backend.mapper.CommentMapper;
 import com.example.g8backend.mapper.PostMapper;
 import com.example.g8backend.mapper.PostViewMapper;
 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;
 
 @Service
@@ -24,12 +29,15 @@
 
     private final PostViewMapper postViewMapper;
 
+    private final CommentMapper commentMapper;
+
     @Autowired
     private IPostTagService postTagService;
 
-    public PostServiceImpl(PostMapper postMapper, PostViewMapper postViewMapper) {
+    public PostServiceImpl(PostMapper postMapper, PostViewMapper postViewMapper, CommentMapper commentMapper) {
         this.postMapper = postMapper;
         this.postViewMapper = postViewMapper;
+        this.commentMapper = commentMapper;
         this.baseMapper = postMapper; // 重要:设置 baseMapper
     }
 
@@ -38,14 +46,15 @@
         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) {
@@ -110,4 +119,55 @@
         postMapper.incrementViewCount(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);
+    }
+
 }
\ No newline at end of file
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index d97701c..e5ddbb4 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -1,111 +1,122 @@
-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 `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,
-    hot_score DOUBLE DEFAULT 0.0,
-    view_count INT DEFAULT 0,
-    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 `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,      -- 评论ID
-    post_id INT NOT NULL,                            -- 所属帖子ID
-    user_id INT NOT NULL,                            -- 评论用户ID
-    parent_comment_id INT DEFAULT NULL,             -- 父评论ID, 如果是顶级评论则为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) -- 关联父评论
+  `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),
-    CONSTRAINT fk_post FOREIGN KEY (post_id) REFERENCES posts(post_id),
-    CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(user_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)
-);
+-- 帖子浏览记录表(新增复合索引)
+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)  -- 新增用户浏览时间索引
+);
\ No newline at end of file
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..23890ba
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/PostServiceRecommendTest.java
@@ -0,0 +1,144 @@
+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.mapper.CommentMapper;
+import com.example.g8backend.mapper.PostMapper;
+import com.example.g8backend.mapper.PostViewMapper;
+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.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+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;
+
+    @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
+    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));
+    }
+}
\ 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 9f8fb14..5d19711 100644
--- a/src/test/java/com/example/g8backend/service/PostServiceTest.java
+++ b/src/test/java/com/example/g8backend/service/PostServiceTest.java
@@ -1,6 +1,7 @@
 package com.example.g8backend.service;
 
 import com.example.g8backend.entity.Post;
+import com.example.g8backend.mapper.CommentMapper;
 import com.example.g8backend.mapper.PostMapper;
 import com.example.g8backend.mapper.PostViewMapper;
 import com.example.g8backend.service.impl.PostServiceImpl;
@@ -30,6 +31,9 @@
     @Mock
     private PostViewMapper postViewMapper;
 
+    @Mock
+    private CommentMapper commentMapper;
+
     private PostServiceImpl postService;
 
     private Post testPost;
@@ -37,7 +41,7 @@
     @BeforeEach
     void setUp() {
         MockitoAnnotations.openMocks(this);
-        postService = new PostServiceImpl(postMapper, postViewMapper);
+        postService = new PostServiceImpl(postMapper, postViewMapper,commentMapper);
         testPost = createTestPost();
     }