blob: e2a4c7fcee33f8c69bda389f2bd18c112d2bcf04 [file] [log] [blame]
夜雨声烦368e3562025-04-24 01:49:46 +08001package com.example.g8backend.service;
夜雨声烦368e3562025-04-24 01:49:46 +08002import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
3import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4import com.example.g8backend.entity.Post;
5import com.example.g8backend.entity.PostView;
夜雨声烦f77d8132025-04-24 19:31:18 +08006import com.example.g8backend.entity.UserTagPreference;
7import com.example.g8backend.mapper.*;
夜雨声烦368e3562025-04-24 01:49:46 +08008import com.example.g8backend.service.impl.PostServiceImpl;
9import org.junit.jupiter.api.BeforeEach;
10import org.junit.jupiter.api.Test;
11import org.junit.jupiter.api.extension.ExtendWith;
夜雨声烦f77d8132025-04-24 19:31:18 +080012import org.mockito.ArgumentCaptor;
夜雨声烦368e3562025-04-24 01:49:46 +080013import org.mockito.InjectMocks;
14import org.mockito.Mock;
夜雨声烦f77d8132025-04-24 19:31:18 +080015import org.mockito.MockitoAnnotations;
夜雨声烦368e3562025-04-24 01:49:46 +080016import org.mockito.junit.jupiter.MockitoExtension;
17import org.springframework.transaction.annotation.Transactional;
夜雨声烦368e3562025-04-24 01:49:46 +080018import java.sql.Timestamp;
19import java.time.Instant;
夜雨声烦f77d8132025-04-24 19:31:18 +080020import java.util.*;
夜雨声烦368e3562025-04-24 01:49:46 +080021import static org.junit.jupiter.api.Assertions.*;
22import static org.mockito.ArgumentMatchers.*;
23import static org.mockito.Mockito.*;
夜雨声烦368e3562025-04-24 01:49:46 +080024@ExtendWith(MockitoExtension.class)
25@Transactional
26public class PostServiceRecommendTest {
夜雨声烦368e3562025-04-24 01:49:46 +080027 @Mock
28 private PostMapper postMapper;
夜雨声烦368e3562025-04-24 01:49:46 +080029 @Mock
30 private PostViewMapper postViewMapper;
夜雨声烦368e3562025-04-24 01:49:46 +080031 @Mock
32 private CommentMapper commentMapper;
夜雨声烦f77d8132025-04-24 19:31:18 +080033 @Mock
34 private UserTagPreferenceMapper userTagPreferenceMapper;
35 @Mock
36 private PostTagMapper postTagMapper;
夜雨声烦368e3562025-04-24 01:49:46 +080037 @InjectMocks
38 private PostServiceImpl postService;
夜雨声烦368e3562025-04-24 01:49:46 +080039 private Post mockPost;
40 private PostView mockPostView;
夜雨声烦368e3562025-04-24 01:49:46 +080041 @BeforeEach
42 void setUp() {
43 // 初始化测试数据
44 mockPost = new Post()
45 .setPostId(1L)
46 .setUserId(100L)
47 .setPostTitle("Test Post")
48 .setViewCount(50)
49 .setCreatedAt(Timestamp.from(Instant.now().minusSeconds(7200)))
50 .setHotScore(5.0);
夜雨声烦368e3562025-04-24 01:49:46 +080051 mockPostView = new PostView()
52 .setViewId(1L)
53 .setUserId(200L)
54 .setPostId(1L)
55 .setViewTime(Timestamp.from(Instant.now()).toLocalDateTime());
56 }
夜雨声烦f77d8132025-04-24 19:31:18 +080057 @Test
58 void testRecordViewHistory_NormalCase() {
59 // 调用方法
60 postService.recordViewHistory(100L, 1L);
61 // 验证 postTagMapper 被调用
62 verify(postTagMapper).findTagIdsByPostId(1L);
63 verify(postViewMapper).insert(any(PostView.class));
64 verify(postMapper).incrementViewCount(1L);
65 }
夜雨声烦368e3562025-04-24 01:49:46 +080066 @Test
67 public void testGetRecommendedPosts_ExcludesViewedPosts() {
68 // 模拟用户已浏览的帖子ID
69 Long userId = 200L;
70 when(postViewMapper.findViewedPostIds(userId))
71 .thenReturn(Arrays.asList(1L, 2L));
夜雨声烦368e3562025-04-24 01:49:46 +080072 // 模拟推荐结果(未浏览的帖子)
73 Post recommendedPost = new Post().setPostId(3L).setHotScore(8.0);
74 when(postMapper.selectPage(any(Page.class), any(QueryWrapper.class)))
75 .thenReturn(new Page<Post>().setRecords(Collections.singletonList(recommendedPost)));
夜雨声烦368e3562025-04-24 01:49:46 +080076 // 调用推荐接口
77 Page<Post> result = postService.getRecommendedPosts(1, 10, userId);
夜雨声烦368e3562025-04-24 01:49:46 +080078 // 验证结果
79 assertEquals(1, result.getRecords().size(), "应返回1条推荐结果");
80 assertEquals(3L, result.getRecords().get(0).getPostId(), "推荐结果应为未浏览的帖子ID 3");
81 assertFalse(result.getRecords().stream().anyMatch(p -> p.getPostId() == 1L), "结果中不应包含已浏览的帖子ID 1");
82 }
夜雨声烦368e3562025-04-24 01:49:46 +080083 @Test
84 public void testGetRecommendedPosts_NoViewedPosts() {
85 // 模拟用户未浏览任何帖子
86 Long userId = 300L;
87 when(postViewMapper.findViewedPostIds(userId))
88 .thenReturn(Collections.emptyList());
夜雨声烦368e3562025-04-24 01:49:46 +080089 // 模拟推荐结果(所有帖子按热度排序)
90 Post post1 = new Post().setPostId(1L).setHotScore(7.5);
91 Post post2 = new Post().setPostId(2L).setHotScore(9.0);
92 when(postMapper.selectPage(any(Page.class), any(QueryWrapper.class)))
93 .thenReturn(new Page<Post>().setRecords(Arrays.asList(post2, post1)));
夜雨声烦368e3562025-04-24 01:49:46 +080094 // 调用推荐接口
95 Page<Post> result = postService.getRecommendedPosts(1, 10, userId);
夜雨声烦368e3562025-04-24 01:49:46 +080096 // 验证结果
97 assertEquals(2, result.getRecords().size(), "应返回所有帖子");
98 assertEquals(2L, result.getRecords().get(0).getPostId(), "热度更高的帖子应排在前面");
99 }
夜雨声烦368e3562025-04-24 01:49:46 +0800100 @Test
101 public void testCalculateHotScores_UpdatesHotScoreCorrectly() {
102 // 设置存根
103 when(postMapper.selectList(any(QueryWrapper.class)))
104 .thenReturn(Collections.singletonList(mockPost));
105 when(postMapper.selectLikeCount(anyLong())).thenReturn(30L);
106 when(commentMapper.selectCountByPostId(anyLong())).thenReturn(20L);
107 when(postMapper.batchUpdateHotScore(anyList())).thenReturn(1);
夜雨声烦368e3562025-04-24 01:49:46 +0800108 // 执行并验证
109 postService.calculateHotScores();
110 double expectedScore = (Math.log(51) * 0.2 + 30 * 0.5 + 20 * 0.3) / Math.pow(4, 1.5);
111 assertEquals(expectedScore, mockPost.getHotScore(), 0.01);
112 verify(postMapper).batchUpdateHotScore(anyList());
113 }
夜雨声烦368e3562025-04-24 01:49:46 +0800114 //--------------------- 测试冷启动逻辑 ---------------------
115 @Test
116 public void testCreatePost_SetsInitialHotScore() {
117 Post newPost = new Post().setPostId(4L).setPostTitle("New Post");
118 postService.createPost(newPost);
夜雨声烦368e3562025-04-24 01:49:46 +0800119 assertEquals(5.0, newPost.getHotScore(), "新帖子的初始热度应为5.0");
120 assertNotNull(newPost.getCreatedAt(), "创建时间不应为空");
121 }
122 @Test
123 public void testConcurrentViewCountUpdate() {
124 // 设置存根
125 doNothing().when(postMapper).incrementViewCount(anyLong());
夜雨声烦368e3562025-04-24 01:49:46 +0800126 postService.recordViewHistory(100L, 1L);
127 postService.recordViewHistory(200L, 1L);
夜雨声烦368e3562025-04-24 01:49:46 +0800128 verify(postMapper, times(2)).incrementViewCount(1L);
129 verify(postViewMapper, times(2)).insert(any(PostView.class));
130 }
夜雨声烦f77d8132025-04-24 19:31:18 +0800131 @Test
132 public void testGetRecommendedByTags_WithPreferredTags() {
133 // 模拟用户偏好标签
134 Long userId = 200L;
135 UserTagPreference pref1 = new UserTagPreference().setTagId(100L).setWeight(2.0);
136 UserTagPreference pref2 = new UserTagPreference().setTagId(200L).setWeight(1.5);
137 when(userTagPreferenceMapper.selectByUserId(userId))
138 .thenReturn(Arrays.asList(pref1, pref2));
139 // 模拟标签关联的帖子ID
140 List<Long> expectedPostIds = Arrays.asList(3L, 4L, 5L);
141 when(postTagMapper.findPostIdsByTagIds(Arrays.asList(100L, 200L)))
142 .thenReturn(expectedPostIds);
143 // 使用 ArgumentCaptor 捕获 QueryWrapper 对象
144 ArgumentCaptor<QueryWrapper<Post>> wrapperCaptor = ArgumentCaptor.forClass(QueryWrapper.class);
145 Page<Post> mockPage = new Page<Post>().setRecords(Arrays.asList(
146 new Post().setPostId(5L).setHotScore(9.0),
147 new Post().setPostId(3L).setHotScore(8.0),
148 new Post().setPostId(4L).setHotScore(7.5)
149 ));
150 when(postMapper.selectPage(any(Page.class), wrapperCaptor.capture()))
151 .thenReturn(mockPage);
152 // 调用标签推荐接口
153 Page<Post> result = postService.getRecommendedByTags(1, 10, userId);
154 // ---------- 验证查询条件 ----------
155 QueryWrapper<Post> actualWrapper = wrapperCaptor.getValue();
156 String sqlSegment = actualWrapper.getSqlSegment();
157 // 验证 SQL 条件格式
158 assertTrue(
159 sqlSegment.matches(".*post_id\\s+IN\\s*\\(.*\\).*"),
160 "应包含 post_id IN 条件,实际条件:" + sqlSegment
161 );
162 // 验证参数值(忽略顺序)
163 Map<String, Object> params = actualWrapper.getParamNameValuePairs();
164 List<Object> actualPostIds = new ArrayList<>(params.values());
165 assertEquals(3, actualPostIds.size(), "IN 条件应包含3个参数");
166 assertTrue(
167 actualPostIds.containsAll(expectedPostIds),
168 "参数应包含所有预期帖子ID,实际参数:" + actualPostIds
169 );
170 // ---------- 验证结果排序和内容 ----------
171 assertEquals(3, result.getRecords().size(), "应返回3条结果");
172 assertEquals(5L, result.getRecords().get(0).getPostId(), "热度最高的帖子应为ID 5");
173 assertEquals(3L, result.getRecords().get(1).getPostId(), "热度次高的帖子应为ID 3");
174 assertEquals(4L, result.getRecords().get(2).getPostId(), "热度最低的帖子应为ID 4");
175 }
176 @Test
177 public void testGetRecommendedByTags_NoPreferredTags() {
178 // 模拟用户无偏好标签
179 Long userId = 300L;
180 when(userTagPreferenceMapper.selectByUserId(userId))
181 .thenReturn(Collections.emptyList());
182 // 调用标签推荐接口
183 Page<Post> result = postService.getRecommendedByTags(1, 10, userId);
184 // 验证结果为空或兜底逻辑
185 assertTrue(result.getRecords().isEmpty(), "无偏好标签时应返回空结果");
186 }
187 @Test
188 public void testGetRecommendedByTags_WithNonExistingTags() {
189 // 模拟用户偏好标签(但无关联帖子)
190 Long userId = 400L;
191 UserTagPreference pref = new UserTagPreference().setTagId(999L).setWeight(2.0);
192 when(userTagPreferenceMapper.selectByUserId(userId))
193 .thenReturn(Collections.singletonList(pref));
194 when(postTagMapper.findPostIdsByTagIds(Collections.singletonList(999L)))
195 .thenReturn(Collections.emptyList());
196 // 调用标签推荐接口
197 Page<Post> result = postService.getRecommendedByTags(1, 10, userId);
198 // 验证结果为空
199 assertNotNull(result, "分页结果不应为null");
200 assertTrue(result.getRecords().isEmpty(), "无关联帖子时应返回空结果");
201 // 验证postMapper.selectPage未被调用
202 verify(postMapper, never()).selectPage(any(Page.class), any(QueryWrapper.class));
203 }
夜雨声烦368e3562025-04-24 01:49:46 +0800204}