Merge "Merge branch 'whx'"
diff --git a/src/main/java/com/example/g8backend/controller/PostController.java b/src/main/java/com/example/g8backend/controller/PostController.java
index 7b7b7c7..3557af2 100644
--- a/src/main/java/com/example/g8backend/controller/PostController.java
+++ b/src/main/java/com/example/g8backend/controller/PostController.java
@@ -54,4 +54,51 @@
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();
+ long userId = (long) authentication.getPrincipal();
+ 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();
+ long userId = (long) authentication.getPrincipal();
+ 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);
+ }
}
diff --git a/src/main/java/com/example/g8backend/service/IPostService.java b/src/main/java/com/example/g8backend/service/IPostService.java
index 58c13dc..f81053e 100644
--- a/src/main/java/com/example/g8backend/service/IPostService.java
+++ b/src/main/java/com/example/g8backend/service/IPostService.java
@@ -2,9 +2,14 @@
import com.example.g8backend.entity.Post;
import com.baomidou.mybatisplus.extension.service.IService;
-
import java.util.List;
public interface IPostService extends IService<Post> {
List<Post> getPostsByUserId(Long userId);
+ Post createPost(Post post);
+ Post updatePost(Post post);
+ List<Post> getPostsByType(String postType);
+ Long getPostLikeCount(Long postId);
+ void likePost(Long userId, Long postId);
+ void unlikePost(Long userId, Long postId);
}
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 90d353a..09a471e 100644
--- a/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java
+++ b/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java
@@ -1,21 +1,63 @@
package com.example.g8backend.service.impl;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.g8backend.entity.Post;
import com.example.g8backend.mapper.PostMapper;
import com.example.g8backend.service.IPostService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
-
+import java.sql.Timestamp;
import java.util.List;
@Service
public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements IPostService {
- @Resource
- private PostMapper postMapper;
+
+ private final PostMapper postMapper;
+
+ public PostServiceImpl(PostMapper postMapper) {
+ this.postMapper = postMapper;
+ this.baseMapper = postMapper; // 重要:设置 baseMapper
+ }
@Override
public List<Post> getPostsByUserId(Long userId) {
return postMapper.getPostsByUserId(userId);
}
+
+ @Override
+ public Post createPost(Post post) {
+ post.setCreatedAt(new Timestamp(System.currentTimeMillis()));
+ save(post);
+ return post;
+ }
+
+ @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表的插入
+ }
+
+ @Override
+ public void unlikePost(Long userId, Long postId) {
+ // TODO: 需要实现post_likes表的删除
+ }
}
diff --git a/src/main/java/com/example/g8backend/util/TorrentUtil.java b/src/main/java/com/example/g8backend/util/TorrentUtil.java
index e6c1f3b..e8360bc 100644
--- a/src/main/java/com/example/g8backend/util/TorrentUtil.java
+++ b/src/main/java/com/example/g8backend/util/TorrentUtil.java
@@ -54,7 +54,7 @@
}
}
- private static byte[] readBytes(File file) throws IOException {
+ public static byte[] readBytes(File file) throws IOException {
try (InputStream in = new FileInputStream(file)) {
return in.readAllBytes();
}
diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql
new file mode 100644
index 0000000..0907033
--- /dev/null
+++ b/src/main/resources/data.sql
@@ -0,0 +1 @@
+# 后面统一数据库数据用
\ No newline at end of file
diff --git a/src/main/resources/mapper/PostMapper.xml b/src/main/resources/mapper/PostMapper.xml
index bc4debe..c1dbda7 100644
--- a/src/main/resources/mapper/PostMapper.xml
+++ b/src/main/resources/mapper/PostMapper.xml
@@ -1,3 +1,4 @@
+.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
@@ -6,4 +7,18 @@
<select id="getPostsByUserId" resultType="com.example.g8backend.entity.Post">
SELECT * FROM posts WHERE user_id = #{userId}
</select>
+
+ <select id="getPostLikeCount" resultType="java.lang.Long">
+ SELECT COUNT(*) FROM post_likes WHERE post_id = #{postId}
+ </select>
+
+ <insert id="likePost">
+ INSERT INTO post_likes (user_id, post_id)
+ VALUES (#{userId}, #{postId})
+ </insert>
+
+ <delete id="unlikePost">
+ DELETE FROM post_likes
+ WHERE user_id = #{userId} AND post_id = #{postId}
+ </delete>
</mapper>
\ No newline at end of file
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index e88ff07..952313e 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -41,6 +41,21 @@
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
+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)
+);
+
+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)
+);
+
CREATE TABLE IF NOT EXISTS `post_likes` (
user_id INT NOT NULL,
post_id INT NOT NULL,
diff --git a/src/test/java/com/example/g8backend/service/PostServiceTest.java b/src/test/java/com/example/g8backend/service/PostServiceTest.java
new file mode 100644
index 0000000..6f54177
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/PostServiceTest.java
@@ -0,0 +1,152 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.entity.Post;
+import com.example.g8backend.mapper.PostMapper;
+import com.example.g8backend.service.impl.PostServiceImpl;
+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.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;
+
+ private PostServiceImpl postService;
+
+ private Post testPost;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ postService = new PostServiceImpl(postMapper);
+ testPost = createTestPost();
+ }
+
+ private Post createTestPost() {
+ Post post = new Post();
+ post.setPostId(1L);
+ post.setUserId(1L);
+ post.setPostTitle("测试标题");
+ post.setPostContent("测试内容");
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/example/g8backend/service/TorrentServiceTest.java b/src/test/java/com/example/g8backend/service/TorrentServiceTest.java
new file mode 100644
index 0000000..5063501
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/TorrentServiceTest.java
@@ -0,0 +1,158 @@
+package com.example.g8backend.service;
+
+import com.dampcake.bencode.Bencode;
+import com.example.g8backend.entity.Torrent;
+import com.example.g8backend.mapper.TorrentMapper;
+import com.example.g8backend.service.impl.TorrentServiceImpl;
+import com.example.g8backend.util.TorrentUtil;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class TorrentServiceTest {
+ @InjectMocks
+ private TorrentServiceImpl torrentService;
+
+ @Mock
+ private TorrentMapper torrentMapper;
+
+ @TempDir
+ Path tempDir;
+
+ private File createTestTorrentFile(Map<String, Object> info) throws Exception {
+ // 1. 构造 Bencode 数据结构
+ Map<String, Object> torrentMap = new HashMap<>();
+ torrentMap.put("announce", "http://localhost:6881/announce");
+ torrentMap.put("info", info);
+
+ // 2. 编码为 Bencode 字节流
+ Bencode bencode = new Bencode();
+ byte[] bencodeData = bencode.encode(torrentMap);
+
+ // 3. 写入临时文件
+ Path torrentPath = tempDir.resolve("dynamic.torrent");
+ Files.write(torrentPath, bencodeData);
+ return torrentPath.toFile();
+ }
+
+ @BeforeEach
+ public void setUp() {
+ torrentService = new TorrentServiceImpl();
+ torrentMapper = Mockito.mock(TorrentMapper.class);
+ // 注入 Mock 对象(反射)
+ try {
+ var field = TorrentServiceImpl.class.getDeclaredField("torrentMapper");
+ field.setAccessible(true);
+ field.set(torrentService, torrentMapper);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ public void testHandleTorrentUpload() throws Exception {
+ Map<String, Object> info = Map.of( "name", "test.txt", "length", 1024);
+ File torrentFile = createTestTorrentFile(info);
+ Long userId = 1L;
+ String passkey = "123456";
+ byte[] mockedBytes = "modified".getBytes();
+ String expectedInfoHash = "modified-info-hash";
+
+ // 注入 Mock 对象(静态方法)
+ try (MockedStatic<TorrentUtil> mockedStatic = Mockito.mockStatic(TorrentUtil.class)){
+ mockedStatic.when(() -> TorrentUtil.injectTracker(any(File.class), any(String.class)))
+ .thenReturn(mockedBytes);
+ mockedStatic.when(() -> TorrentUtil.getInfoHash(any(File.class)))
+ .thenReturn(expectedInfoHash);
+ Torrent torrent = torrentService.handleTorrentUpload(torrentFile, userId, passkey);
+
+ // 验证调用
+ verify(torrentMapper, times(1))
+ .insertTorrent(eq(1L),
+ anyString(),
+ eq("modified-info-hash"),
+ eq(torrentFile.length()/1024.0/1024.0));
+
+ assertEquals(expectedInfoHash, torrent.getInfoHash());
+ assertEquals(userId, torrent.getUserId());
+ assertEquals(torrentFile.length()/1024.0/1024.0, torrent.getFileSize());
+ } finally {
+ if (!torrentFile.delete()) {
+ System.err.println("Failed to delete temporary file: " + torrentFile.getAbsolutePath());
+ }
+ }
+ }
+
+ @Test
+ public void testHandleTorrentDownload() throws Exception {
+ Torrent torrent = new Torrent();
+ torrent.setTorrentName("sample.torrent");
+
+ File fakeTorrentFile = new File("uploaded-torrents/sample.torrent");
+ fakeTorrentFile.getParentFile().mkdirs();
+ byte[] originalBytes = "original-data".getBytes();
+ Files.write(fakeTorrentFile.toPath(), originalBytes);
+
+ String passkey = "testpasskey";
+ byte[] modifiedBytes = "modified-data".getBytes();
+
+ try (MockedStatic<TorrentUtil> mockedStatic = Mockito.mockStatic(TorrentUtil.class)) {
+ // mock injectTracker
+ mockedStatic.when(() -> TorrentUtil.injectTracker(any(File.class), anyString()))
+ .thenReturn(modifiedBytes);
+
+ File downloaded = torrentService.handleTorrentDownload(torrent, passkey);
+
+ assertTrue(downloaded.exists());
+ assertEquals("modified-data", Files.readString(downloaded.toPath()));
+
+ // 清理临时文件
+ downloaded.delete();
+ } finally {
+ fakeTorrentFile.delete(); // 清理原始文件
+ }
+ }
+
+
+ @Test
+ public void testFindByInfoHash() {
+ Torrent mockTorrent = new Torrent();
+ mockTorrent.setInfoHash("abc123");
+ when(torrentMapper.getTorrentByInfoHash("abc123")).thenReturn(mockTorrent);
+
+ Torrent result = torrentService.findByInfoHash("abc123");
+
+ assertEquals("abc123", result.getInfoHash());
+ verify(torrentMapper).getTorrentByInfoHash("abc123");
+ }
+
+ @Test
+ public void testFindByTorrentId() {
+ Torrent mockTorrent = new Torrent();
+ mockTorrent.setTorrentId(42L);
+ when(torrentMapper.getTorrentByTorrentId(42L)).thenReturn(mockTorrent);
+
+ Torrent result = torrentService.findByTorrentId(42L);
+
+ assertEquals(42L, result.getTorrentId());
+ verify(torrentMapper).getTorrentByTorrentId(42L);
+ }
+}
\ No newline at end of file