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