Merge "fix uploadResource" into main
diff --git a/.gitignore b/.gitignore
index a0134b4..a135372 100644
--- a/.gitignore
+++ b/.gitignore
@@ -165,3 +165,4 @@
 # Android studio 3.1+ serialized cache file
 .idea/caches/build_file_checksums.ser
 
+/upload
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..3dbfba9
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,27 @@
+# Step 1: 构建 jar
+FROM maven:3.9.9-eclipse-temurin-21 AS builder
+
+WORKDIR /app
+COPY . .
+RUN mvn clean package -DskipTests
+
+FROM nginx:alpine
+
+# 创建运行目录
+WORKDIR /app
+
+# 拷贝 jar 到运行目录
+COPY --from=builder /app/target/*.jar app.jar
+
+RUN mkdir /app/upload/
+
+# 拷贝 Nginx 配置文件
+COPY nginx.conf /etc/nginx/nginx.conf
+
+# 安装 OpenJDK 运行环境
+RUN apk add --no-cache openjdk21-jdk curl
+
+# 后台启动 Spring Boot + 前台运行 Nginx
+EXPOSE 5009
+
+CMD java -jar app.jar & nginx -g "daemon off;"
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..d06c697
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,56 @@
+worker_processes 1;
+
+events { worker_connections 1024; }
+
+http {
+    include       mime.types;
+    default_type  application/octet-stream;
+
+    sendfile        on;
+    keepalive_timeout  65;
+
+    server {
+        listen 5009;
+
+        # 支持 CORS 的全局选项处理(可选)
+        if ($request_method = OPTIONS) {
+            add_header Access-Control-Allow-Origin *;
+            add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, PUT, DELETE';
+            add_header Access-Control-Allow-Headers 'Origin, Content-Type, Accept, Authorization';
+            add_header Access-Control-Max-Age 3600;
+            return 204;
+        }
+
+        # 1. 代理静态资源:如 http://localhost/static/index.html
+        location /upload/ {
+            alias /app/upload/;
+            autoindex on;
+        }
+
+        # 反向代理 /api 到 localhost:8080
+        location /api/ {
+            proxy_pass http://localhost:8080/;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+            # CORS headers
+            add_header Access-Control-Allow-Origin *;
+            add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, PUT, DELETE';
+            add_header Access-Control-Allow-Headers 'Origin, Content-Type, Accept, Authorization';
+        }
+
+        # 反向代理 /tracker 到 localhost:6969/announce
+        location /tracker {
+            proxy_pass http://localhost:6969/announce;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+            # CORS headers
+            add_header Access-Control-Allow-Origin *;
+            add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, PUT, DELETE';
+            add_header Access-Control-Allow-Headers 'Origin, Content-Type, Accept, Authorization';
+        }
+    }
+}
diff --git a/pom.xml b/pom.xml
index fa73ca7..e677d09 100644
--- a/pom.xml
+++ b/pom.xml
@@ -46,6 +46,11 @@
             <version>3.5.8</version>
         </dependency>
         <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
             <groupId>jakarta.persistence</groupId>
             <artifactId>jakarta.persistence-api</artifactId>
             <version>3.1.0</version>
@@ -60,21 +65,11 @@
             <scope>runtime</scope>
         </dependency>
         <dependency>
-            <groupId>org.projectlombok</groupId>
-            <artifactId>lombok</artifactId>
-            <optional>true</optional>
-        </dependency>
-        <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>com.h2database</groupId>
-            <artifactId>h2</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
             <groupId>commons-io</groupId>
             <artifactId>commons-io</artifactId>
             <version>2.14.0</version>
@@ -89,6 +84,30 @@
             <artifactId>junit</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-client</artifactId>
+            <version>1.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-common</artifactId>
+            <version>1.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-tracker</artifactId>
+            <version>1.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.data</groupId>
+            <artifactId>spring-data-jpa</artifactId>
+            <version>3.3.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/src/main/java/com/g9/g9backend/controller/CommentController.java b/src/main/java/com/g9/g9backend/controller/CommentController.java
new file mode 100644
index 0000000..fec8e68
--- /dev/null
+++ b/src/main/java/com/g9/g9backend/controller/CommentController.java
@@ -0,0 +1,94 @@
+package com.g9.g9backend.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.g9.g9backend.pojo.Comment;
+import com.g9.g9backend.pojo.DTO.GetCommentDTO;
+import com.g9.g9backend.service.CommentService;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+
+@RestController
+@RequestMapping("/comment")
+public class CommentController {
+
+    private final CommentService commentService;
+
+    private final Logger logger = LoggerFactory.getLogger(CommentController.class);
+
+    public CommentController(CommentService commentService) {
+        this.commentService = commentService;
+    }
+
+    @PostMapping
+    public ResponseEntity<String> postComment(@RequestBody Comment comment) {
+        System.out.println(comment.toString());
+        commentService.save(comment);
+
+        logger.info("评论已发布");
+
+        return ResponseEntity.ok("");
+    }
+
+    @DeleteMapping
+    public ResponseEntity<String> deleteComment(@RequestParam Integer commentId) {
+        commentService.removeById(commentId);
+
+        return ResponseEntity.noContent().build();
+    }
+
+    @GetMapping
+    public ResponseEntity<GetCommentDTO> getComment(@RequestParam Integer id,
+                                                    @RequestParam Integer pageNumber,
+                                                    @RequestParam Integer rows,
+                                                    @RequestParam String type) {
+        Page<Comment> commentPage = new Page<>(pageNumber, rows);
+        LambdaQueryWrapper<Comment> rewardQuery = new LambdaQueryWrapper<Comment>()
+                .orderByDesc(Comment::getCreateAt);
+
+
+        if (Objects.equals(type, "资源")) {
+
+            rewardQuery.eq(Comment::getResourceId, id);
+        }
+
+        if (Objects.equals(type, "帖子")) {
+
+            rewardQuery.eq(Comment::getThreadId, id);
+        }
+
+        if (Objects.equals(type, "悬赏")) {
+
+            rewardQuery.eq(Comment::getRewardId, id);
+        }
+
+        Page<Comment> result = commentService.page(commentPage, rewardQuery);
+
+        GetCommentDTO getCommentDTO = wrapCommentPage(result, item -> {
+            GetCommentDTO.Comment comment = new GetCommentDTO.Comment();
+            comment.setCommentId(item.getCommentId());
+            comment.setUserId(item.getUserId());
+            comment.setReplyId(item.getReplyId() != null ? item.getReplyId() : 0);
+            comment.setContent(item.getContent());
+            comment.setCreateAt(item.getCreateAt());
+            return comment;
+        });
+
+        return ResponseEntity.ok(getCommentDTO);
+    }
+
+    @NotNull
+    private <T> GetCommentDTO wrapCommentPage(Page<T> page, Function<T, GetCommentDTO.Comment> mapper) {
+        List<GetCommentDTO.Comment> records = page.getRecords().stream().map(mapper).toList();
+
+        return new GetCommentDTO(records, (int) page.getTotal(), (int) page.getPages(), (int) page.getCurrent(), (int) page.getSize());
+    }
+
+}
diff --git a/src/main/java/com/g9/g9backend/controller/CommunityController.java b/src/main/java/com/g9/g9backend/controller/CommunityController.java
index 229fe96..ea79dbb 100644
--- a/src/main/java/com/g9/g9backend/controller/CommunityController.java
+++ b/src/main/java/com/g9/g9backend/controller/CommunityController.java
@@ -1,8 +1,25 @@
 package com.g9.g9backend.controller;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.g9.g9backend.pojo.Community;
+import com.g9.g9backend.pojo.DTO.*;
+import com.g9.g9backend.pojo.Subscription;
+import com.g9.g9backend.pojo.Thread;
+import com.g9.g9backend.pojo.ThreadLike;
+import com.g9.g9backend.service.*;
+import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
 
 /**
  * CommunityController 社区控制器类,处理与社区相关的请求
@@ -12,5 +29,250 @@
 @RestController
 public class CommunityController {
 
+    private final CommunityService communityService;
+
+    private final ThreadService threadService;
+
+    private final SubscriptionService subscriptionService;
+
+    private final ThreadLikeService threadLikeService;
+
     private final Logger logger = LoggerFactory.getLogger(CommunityController.class);
+
+    public CommunityController(CommunityService communityService, ThreadService threadService, SubscriptionService subscriptionService, ThreadLikeService threadLikeService) {
+        this.communityService = communityService;
+        this.threadService = threadService;
+        this.threadLikeService = threadLikeService;
+        this.subscriptionService = subscriptionService;
+    }
+
+    @PostMapping(value = "/thread")
+    public ResponseEntity<String> postThread(@RequestBody Thread thread) {
+        thread.setLikes(0);
+        thread.setCommentNumber(0);
+        threadService.save(thread);
+
+        Community community = communityService.getById(thread.getCommunityId());
+        community.setThreadNumber(community.getThreadNumber() + 1);
+        communityService.updateById(community);
+
+        logger.info("帖子已发布");
+
+        return ResponseEntity.ok("");
+    }
+
+    @PostMapping(value = "/thread/like")
+    public ResponseEntity<String> postThreadLike(@RequestBody Map<String, Integer> request) {
+        Integer userId = request.get("userId");
+        Integer threadId = request.get("threadId");
+
+        ThreadLike threadLike = new ThreadLike(userId, threadId);
+        threadLikeService.save(threadLike);
+
+        Thread thread = threadService.getById(threadId);
+        thread.setLikes(thread.getLikes() + 1);
+        threadService.updateById(thread);
+
+        logger.info("点赞成功");
+
+        return ResponseEntity.ok("");
+    }
+
+    @DeleteMapping(value = "/thread")
+    public ResponseEntity<String> deleteThread(@RequestParam Integer threadId) {
+        Thread thread = threadService.getById(threadId);
+        threadService.removeById(threadId);
+
+        Community community = communityService.getById(thread.getCommunityId());
+        community.setThreadNumber(community.getThreadNumber() - 1);
+        communityService.updateById(community);
+
+        logger.info("帖子已删除");
+
+        return ResponseEntity.noContent().build();
+    }
+
+    @DeleteMapping(value = "/thread/like")
+    public ResponseEntity<String> deleteLike(@RequestParam Integer userId, @RequestParam Integer threadId) {
+        LambdaQueryWrapper<ThreadLike> threadLikeQuery = new LambdaQueryWrapper<ThreadLike>()
+                .eq(ThreadLike::getUserId, userId)
+                .eq(ThreadLike::getThreadId, threadId);
+
+        threadLikeService.remove(threadLikeQuery);
+
+        Thread thread = threadService.getById(threadId);
+        thread.setLikes(thread.getLikes() - 1);
+        threadService.updateById(thread);
+
+        logger.info("取消点赞成功");
+
+        return ResponseEntity.noContent().build();
+    }
+
+    @GetMapping(value = "/community")
+    public ResponseEntity<GetCommunityDTO> getCommunity(@RequestParam String searchValue,
+                                                        @RequestParam String type,
+                                                        @RequestParam Integer pageNumber,
+                                                        @RequestParam Integer rows) {
+        Page<Community> communityPage = new Page<>(pageNumber, rows);
+        LambdaQueryWrapper<Community> communityQuery = new LambdaQueryWrapper<Community>()
+                .eq(Community::getType, type)
+                .like(StringUtils.isNotBlank(searchValue), Community::getCommunityName, searchValue)
+                .orderByDesc(Community::getHot);
+
+        Page<Community> result = communityService.page(communityPage, communityQuery);
+
+        GetCommunityDTO getCommunityDTO = wrapCommunityPage(result, item -> {
+            GetCommunityDTO.Community community = new GetCommunityDTO.Community();
+            community.setCommunityId(item.getCommunityId());
+            community.setCommunityName(item.getCommunityName());
+            community.setCommunityPicture(item.getCommunityPicture());
+            community.setDescription(item.getDescription());
+            community.setHot(item.getHot());
+            community.setThreadNumber(item.getThreadNumber());
+            community.setResourceId(item.getResourceId());
+            return community;
+        });
+
+        return ResponseEntity.ok(getCommunityDTO);
+    }
+
+    @NotNull
+    private <T> GetCommunityDTO wrapCommunityPage(Page<T> page, Function<T, GetCommunityDTO.Community> mapper) {
+        List<GetCommunityDTO.Community> records = page.getRecords().stream().map(mapper).toList();
+
+        return new GetCommunityDTO(records, (int) page.getTotal(), (int) page.getPages(), (int) page.getCurrent(), (int) page.getSize());
+    }
+
+    @GetMapping(value = "/community/info")
+    public ResponseEntity<Community> getCommunityInfo(@RequestParam Integer communityId) {
+
+        return ResponseEntity.ok(communityService.getById(communityId));
+    }
+
+    @GetMapping(value = "/thread")
+    public ResponseEntity<ThreadDTO> getThread(@RequestParam Integer threadId, @RequestParam Integer userId) {
+        LambdaQueryWrapper<ThreadLike> threadLikeQuery = new LambdaQueryWrapper<ThreadLike>()
+                .eq(ThreadLike::getUserId, userId)
+                .eq(ThreadLike::getThreadId, threadId);
+        Thread thread = threadService.getById(threadId);
+        ThreadDTO threadDTO = new ThreadDTO(
+                threadId,
+                thread.getUserId(),
+                thread.getThreadPicture(),
+                thread.getTitle(),
+                thread.getContent(),
+                thread.getLikes(),
+                threadLikeService.getOne(threadLikeQuery) != null,
+                thread.getCreateAt(),
+                thread.getCommentNumber(),
+                thread.getCommunityId()
+        );
+
+        return ResponseEntity.ok(threadDTO);
+    }
+
+    @GetMapping(value = "/community/threads")
+    public ResponseEntity<GetCommunityThreadsDTO> getCommunityThreads(@RequestParam Integer communityId,
+                                                                      @RequestParam Integer pageNumber,
+                                                                      @RequestParam Integer rows,
+                                                                      @RequestParam String option,
+                                                                      @RequestParam String searchValue,
+                                                                      @RequestParam Integer userId) {
+        Page<Thread> threadPage = new Page<>(pageNumber, rows);
+        LambdaQueryWrapper<Thread> threadQuery = new LambdaQueryWrapper<Thread>()
+                .eq(Thread::getCommunityId, communityId)
+                .like(StringUtils.isNotBlank(searchValue), Thread::getTitle, searchValue);
+
+        if (Objects.equals(option, "最高热度")) {
+
+            threadQuery.orderByDesc(Thread::getLikes);
+        } else {
+
+            List<Integer> userIds = subscriptionService.list(new LambdaQueryWrapper<Subscription>()
+                            .eq(Subscription::getUserId, userId))
+                            .stream()
+                            .map(Subscription::getFollowerId)
+                            .toList();
+
+            threadQuery.in(!userIds.isEmpty(), Thread::getUserId, userIds)
+                    .orderByDesc(Thread::getLikes);
+        }
+
+        Page<Thread> result = threadService.page(threadPage, threadQuery);
+
+        GetCommunityThreadsDTO getCommunityThreadsDTO = wrapThreadPage(result, item -> {
+            GetCommunityThreadsDTO.Thread thread = new GetCommunityThreadsDTO.Thread();
+            thread.setThreadId(item.getThreadId());
+            thread.setUserId(item.getUserId());
+            thread.setThreadPicture(item.getThreadPicture());
+            thread.setTitle(item.getTitle());
+            thread.setLikes(item.getLikes());
+            thread.setCreateAt(item.getCreateAt());
+            return thread;
+        });
+
+        return ResponseEntity.ok(getCommunityThreadsDTO);
+    }
+
+    @NotNull
+    private <T> GetCommunityThreadsDTO wrapThreadPage(Page<T> page, Function<T, GetCommunityThreadsDTO.Thread> mapper) {
+        List<GetCommunityThreadsDTO.Thread> records = page.getRecords().stream().map(mapper).toList();
+
+        return new GetCommunityThreadsDTO(records, (int) page.getTotal(), (int) page.getPages(), (int) page.getCurrent(), (int) page.getSize());
+    }
+
+    @GetMapping(value = "/community/hot")
+    public ResponseEntity<GetCommunityHotDTO> getCommunityHot() {
+        Page<Community> communityPage = new Page<>(1, 3);
+        LambdaQueryWrapper<Community> communityQuery = new LambdaQueryWrapper<Community>()
+                .orderByDesc(Community::getHot);
+
+        Page<Community> result = communityService.page(communityPage, communityQuery);
+
+        List<GetCommunityHotDTO.Community> communityList = getCommunityList(result);
+
+        GetCommunityHotDTO getCommunityHotDTO = new GetCommunityHotDTO(communityList);
+
+        return ResponseEntity.ok(getCommunityHotDTO);
+    }
+
+    @NotNull
+    private static List<GetCommunityHotDTO.Community> getCommunityList(Page<Community> communityPage) {
+        List<Community> communityListT = communityPage.getRecords();
+        List<GetCommunityHotDTO.Community> communityList = new ArrayList<>();
+        int status = 1;
+        for (Community community : communityListT) {
+            GetCommunityHotDTO.Community communityHot = new GetCommunityHotDTO.Community(
+                    community.getCommunityId(),
+                    community.getCommunityName(),
+                    community.getCommunityPicture(),
+                    community.getCommunityPicture(),
+                    community.getHot(),
+                    community.getType(),
+                    community.getThreadNumber(),
+                    community.getResourceId(),
+                    status);
+            communityList.add(communityHot);
+            status++;
+        }
+
+        return communityList;
+    }
+
+    @GetMapping(value = "/community/common")
+    public ResponseEntity<GetCommunityCommonDTO> getCommunityCommon() {
+        Page<Community> communityPage = new Page<>(2, 3);
+        LambdaQueryWrapper<Community> communityQuery = new LambdaQueryWrapper<Community>()
+                .orderByDesc(Community::getHot);
+
+        Page<Community> result = communityService.page(communityPage, communityQuery);
+
+        List<Community> communityList = result.getRecords();
+
+        GetCommunityCommonDTO getCommunityCommonDTO = new GetCommunityCommonDTO(communityList);
+
+        return ResponseEntity.ok(getCommunityCommonDTO);
+    }
+
 }
diff --git a/src/main/java/com/g9/g9backend/controller/FileController.java b/src/main/java/com/g9/g9backend/controller/FileController.java
index 1f9ccd2..7b4e339 100644
--- a/src/main/java/com/g9/g9backend/controller/FileController.java
+++ b/src/main/java/com/g9/g9backend/controller/FileController.java
@@ -1,18 +1,86 @@
 package com.g9.g9backend.controller;
 
+import com.g9.g9backend.pojo.TorrentRecord;
+import com.g9.g9backend.service.TorrentRecordService;
+import lombok.Getter;
+import lombok.Setter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
 
 /**
  * FileController 文件控制器类,处理与文件相关的请求
  *
  * @author Seamher
  */
+@Getter
+@Setter
 @RestController
 @RequestMapping("/file")
 public class FileController {
 
+    private TorrentRecordService torrentRecordService;
+
     private final Logger logger = LoggerFactory.getLogger(FileController.class);
+
+    public FileController(TorrentRecordService torrentRecordService) {
+        this.torrentRecordService = torrentRecordService;
+    }
+
+    @PostMapping
+    public ResponseEntity<String> uploadFile(@RequestBody MultipartFile file) {
+        // 相对路径(可在资源根路径创建 upload 文件夹)
+        String UPLOAD_DIR = "upload";
+
+        // 获取原始文件名
+        String originalFilename = file.getOriginalFilename();
+        if (originalFilename.isEmpty()) {
+            return ResponseEntity.badRequest().body("文件名为空");
+        }
+
+        // 为文件名加时间戳,防止重名冲突
+        String filename = System.currentTimeMillis() + "_" + originalFilename;
+
+        // 构建文件保存目录(确保 upload 目录存在)
+        File uploadDir = new File(UPLOAD_DIR);
+        if (!uploadDir.exists()) {
+            boolean created = uploadDir.mkdirs(); // 创建目录
+            if (!created) {
+                logger.error("无法创建上传目录: " + uploadDir.getAbsolutePath());
+                return ResponseEntity.internalServerError().body("无法创建上传目录");
+            }
+        }
+
+        // 构建完整的文件路径
+        File dest = new File(uploadDir, filename);
+
+        try {
+            // 保存文件
+            file.transferTo(dest);
+        } catch (IOException e) {
+            logger.error("上传失败: " + e.getMessage(), e);
+            return ResponseEntity.internalServerError().body("上传失败");
+        }
+
+        // 返回访问 URL(你可能需要根据 Nginx 的静态资源映射来配置)
+        String url = "http://localhost:65/" + filename;
+
+        return ResponseEntity.ok(url);
+    }
+
+    @PostMapping(value = "bt")
+    public ResponseEntity<String> uploadBTFile(@RequestBody TorrentRecord torrentRecord) {
+        torrentRecordService.save(torrentRecord);
+
+        return ResponseEntity.ok("");
+    }
+
 }
diff --git a/src/main/java/com/g9/g9backend/controller/NotificationController.java b/src/main/java/com/g9/g9backend/controller/NotificationController.java
index c774041..4f451df 100644
--- a/src/main/java/com/g9/g9backend/controller/NotificationController.java
+++ b/src/main/java/com/g9/g9backend/controller/NotificationController.java
@@ -1,9 +1,15 @@
 package com.g9.g9backend.controller;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.g9.g9backend.pojo.Notification;
+import com.g9.g9backend.service.NotificationService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
 
 /**
  * NotificationController 通知控制器类,处理与通知相关的请求
@@ -14,5 +20,46 @@
 @RequestMapping("/notification")
 public class NotificationController {
 
+    private final NotificationService notificationService;
+
+    public NotificationController(NotificationService notificationService) {
+        this.notificationService = notificationService;
+    }
+
     private final Logger logger = LoggerFactory.getLogger(NotificationController.class);
+
+    @PostMapping(value = "/read")
+    public ResponseEntity<String> getNotificationRead(@RequestBody Notification notification) {
+        Integer notificationId = notification.getNotificationId();
+        System.out.println(notificationId);
+        Notification notificationUpdate = notificationService.getById(notificationId);
+        notificationUpdate.setRead(true);
+        notificationService.updateById(notificationUpdate);
+
+        return ResponseEntity.ok("");
+    }
+
+    @DeleteMapping
+    @ResponseStatus(HttpStatus.NO_CONTENT)
+    public ResponseEntity<String> deleteNotification(@RequestParam Integer notificationId) {
+        notificationService.removeById(notificationId);
+
+        return ResponseEntity.noContent().build();
+    }
+
+    @GetMapping
+    public ResponseEntity<IPage<Notification>> getNotification(@RequestParam Integer userId,
+                                                               @RequestParam Integer pageNumber,
+                                                               @RequestParam Integer rows) {
+        Page<Notification> notificationPage = new Page<>(pageNumber, rows);
+        LambdaQueryWrapper<Notification> notificationQuery = new LambdaQueryWrapper<Notification>()
+                .eq(Notification::getUserId, userId)
+                .orderByDesc(Notification::getCreateAt);
+
+        IPage<Notification> result = notificationService.page(notificationPage, notificationQuery);
+        logger.info("通知已返回");
+
+        return ResponseEntity.ok(result);
+    }
+
 }
diff --git a/src/main/java/com/g9/g9backend/controller/RewardController.java b/src/main/java/com/g9/g9backend/controller/RewardController.java
index c5b316b..3563f41 100644
--- a/src/main/java/com/g9/g9backend/controller/RewardController.java
+++ b/src/main/java/com/g9/g9backend/controller/RewardController.java
@@ -1,9 +1,23 @@
 package com.g9.g9backend.controller;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.g9.g9backend.pojo.DTO.GetRewardDTO;
+import com.g9.g9backend.pojo.Reward;
+import com.g9.g9backend.service.RewardService;
+import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
 
 /**
  * RewardController 悬赏控制器类,处理与悬赏相关的请求
@@ -14,5 +28,103 @@
 @RequestMapping("/reward")
 public class RewardController {
 
+    private final RewardService rewardService;
+
     private final Logger logger = LoggerFactory.getLogger(RewardController.class);
+
+    public RewardController(RewardService rewardService) {
+        this.rewardService = rewardService;
+    }
+
+    @PostMapping
+    public ResponseEntity<String> postReward(@RequestBody Reward reward) {
+        reward.setCompletedBy(null);
+        reward.setResourceId(null);
+        rewardService.save(reward);
+
+        logger.info("悬赏已发布");
+
+        return ResponseEntity.ok("");
+    }
+
+    @DeleteMapping
+    public ResponseEntity<String> deleteReward(@RequestParam Integer rewardId) {
+        rewardService.removeById(rewardId);
+
+        logger.info("悬赏已删除");
+
+        return ResponseEntity.noContent().build();
+    }
+
+    @GetMapping
+    public ResponseEntity<GetRewardDTO> getReward(@RequestParam Integer pageNumber,
+                                                  @RequestParam Integer rows,
+                                                  @RequestParam String searchValue,
+                                                  @RequestParam String option) {
+        Page<Reward> rewardPage = new Page<>(pageNumber, rows);
+        LambdaQueryWrapper<Reward> rewardQuery = new LambdaQueryWrapper<Reward>()
+                .like(StringUtils.isNotBlank(searchValue), Reward::getRewardName, searchValue);
+
+        if (Objects.equals(option, "赏金最高")) {
+
+            rewardQuery.orderByDesc(Reward::getPrice);
+        } else {
+
+            rewardQuery.orderByDesc(Reward::getCreateAt);
+        }
+
+        Page<Reward> result = rewardService.page(rewardPage, rewardQuery);
+
+        GetRewardDTO getRewardDTO = wrapRewardPage(result, item -> {
+            GetRewardDTO.Reward reward = new GetRewardDTO.Reward();
+            reward.setRewardId(item.getRewardId());
+            reward.setRewardName(item.getRewardName());
+            reward.setRewardPicture(item.getRewardPicture());
+            reward.setUserId(item.getUserId());
+            reward.setPrice(item.getPrice());
+            reward.setCreateAt(item.getCreateAt());
+            return reward;
+        });
+
+        return ResponseEntity.ok(getRewardDTO);
+    }
+
+    @NotNull
+    private <T> GetRewardDTO wrapRewardPage(Page<T> page, Function<T, GetRewardDTO.Reward> mapper) {
+        List<GetRewardDTO.Reward> records = page.getRecords().stream().map(mapper).toList();
+
+        return new GetRewardDTO(records, (int) page.getTotal(), (int) page.getPages(), (int) page.getCurrent(), (int) page.getSize());
+    }
+
+    @GetMapping(value = "/info")
+    public ResponseEntity<Reward> getRewardInfo(@RequestParam Integer rewardId) {
+
+        return ResponseEntity.ok(rewardService.getById(rewardId));
+    }
+
+    @PutMapping("/info")
+    public ResponseEntity<String> putReward(@RequestBody Reward reward) {
+        Reward rewardUpdate = rewardService.getById(reward.getRewardId());
+        rewardUpdate.setRewardName(reward.getRewardName());
+        rewardUpdate.setRewardPicture(reward.getRewardPicture());
+        rewardUpdate.setPrice(reward.getPrice());
+        rewardUpdate.setRewardDescription(reward.getRewardDescription());
+        rewardUpdate.setLastUpdateAt(Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant()));
+        rewardService.updateById(rewardUpdate);
+
+        return ResponseEntity.ok("");
+    }
+
+    @PostMapping(value = "/completion")
+    public ResponseEntity<String> putRewardCompletion(@RequestBody Reward reward) {
+        Reward rewardUpdate = rewardService.getById(reward.getRewardId());
+        rewardUpdate.setCompletedBy(reward.getCompletedBy());
+        rewardUpdate.setCompletedAt(reward.getCompletedAt());
+        rewardUpdate.setResourceId(reward.getResourceId());
+        rewardService.updateById(rewardUpdate);
+
+        return ResponseEntity.ok("");
+    }
+
+
 }
diff --git a/src/main/java/com/g9/g9backend/controller/TotalController.java b/src/main/java/com/g9/g9backend/controller/TotalController.java
new file mode 100644
index 0000000..2359586
--- /dev/null
+++ b/src/main/java/com/g9/g9backend/controller/TotalController.java
@@ -0,0 +1,67 @@
+package com.g9.g9backend.controller;
+
+import com.g9.g9backend.service.ResourceService;
+import com.g9.g9backend.service.ThreadService;
+import com.g9.g9backend.service.UserPurchaseService;
+import com.g9.g9backend.service.UserUploadService;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/total")
+public class TotalController {
+
+    private final ThreadService threadService;
+
+    private final UserPurchaseService userPurchaseService;
+
+    private final UserUploadService userUploadService;
+
+    private final ResourceService resourceService;
+
+    public TotalController(ThreadService threadService, UserPurchaseService userPurchaseService, UserUploadService userUploadService, ResourceService resourceService) {
+        this.threadService = threadService;
+        this.userPurchaseService = userPurchaseService;
+        this.userUploadService = userUploadService;
+        this.resourceService = resourceService;
+    }
+
+    private final Logger logger = LoggerFactory.getLogger(TotalController.class);
+
+
+    @GetMapping(value = "/info")
+    public ResponseEntity<Info> getTotalInfo() {
+
+        long threadCount = threadService.count();
+        long downloadCount = userPurchaseService.count();
+        long authorCount = userUploadService.count();
+        long resourceCount = resourceService.count();
+
+        Info info = new Info(threadCount, downloadCount, authorCount, resourceCount);
+        logger.info("统计数据返回:{}", info);
+
+        return ResponseEntity.ok(info);
+    }
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class Info {
+
+        private long threadCount;
+
+        private long downloadCount;
+
+        private long authorCount;
+
+        private long resourceCount;
+
+    }
+}
diff --git a/src/main/java/com/g9/g9backend/mapper/ThreadLikeMapper.java b/src/main/java/com/g9/g9backend/mapper/ThreadLikeMapper.java
new file mode 100644
index 0000000..6903868
--- /dev/null
+++ b/src/main/java/com/g9/g9backend/mapper/ThreadLikeMapper.java
@@ -0,0 +1,9 @@
+package com.g9.g9backend.mapper;
+
+import com.g9.g9backend.pojo.ThreadLike;
+import com.github.jeffreyning.mybatisplus.base.MppBaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ThreadLikeMapper extends MppBaseMapper<ThreadLike> {
+}
diff --git a/src/main/java/com/g9/g9backend/pojo/Comment.java b/src/main/java/com/g9/g9backend/pojo/Comment.java
index eddc42b..1f6e74f 100644
--- a/src/main/java/com/g9/g9backend/pojo/Comment.java
+++ b/src/main/java/com/g9/g9backend/pojo/Comment.java
@@ -21,13 +21,13 @@
 
     private int userId;
 
-    private int threadId;
+    private Integer threadId;
 
-    private int resourceId;
+    private Integer resourceId;
 
-    private int rewardId;
+    private Integer rewardId;
 
-    private int replyId;
+    private Integer replyId;
 
     private String content;
 
diff --git a/src/main/java/com/g9/g9backend/pojo/DTO/GetCommentDTO.java b/src/main/java/com/g9/g9backend/pojo/DTO/GetCommentDTO.java
new file mode 100644
index 0000000..0fb7c44
--- /dev/null
+++ b/src/main/java/com/g9/g9backend/pojo/DTO/GetCommentDTO.java
@@ -0,0 +1,42 @@
+package com.g9.g9backend.pojo.DTO;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Date;
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class GetCommentDTO {
+
+    private List<Comment> records;
+
+    private long total;
+
+    private long pages;
+
+    private long current;
+
+    private long size;
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class Comment {
+
+        private int commentId;
+
+        private int userId;
+
+        private int replyId;
+
+        private String content;
+
+        private Date createAt;
+
+    }
+
+}
diff --git a/src/main/java/com/g9/g9backend/pojo/DTO/GetCommunityCommonDTO.java b/src/main/java/com/g9/g9backend/pojo/DTO/GetCommunityCommonDTO.java
new file mode 100644
index 0000000..8e24f40
--- /dev/null
+++ b/src/main/java/com/g9/g9backend/pojo/DTO/GetCommunityCommonDTO.java
@@ -0,0 +1,17 @@
+package com.g9.g9backend.pojo.DTO;
+
+import com.g9.g9backend.pojo.Community;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class GetCommunityCommonDTO {
+
+    List<Community> communityList;
+
+}
diff --git a/src/main/java/com/g9/g9backend/pojo/DTO/GetCommunityDTO.java b/src/main/java/com/g9/g9backend/pojo/DTO/GetCommunityDTO.java
new file mode 100644
index 0000000..791bb10
--- /dev/null
+++ b/src/main/java/com/g9/g9backend/pojo/DTO/GetCommunityDTO.java
@@ -0,0 +1,45 @@
+package com.g9.g9backend.pojo.DTO;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class GetCommunityDTO {
+
+    private List<Community> records;
+
+    private Integer total;
+
+    private Integer pages;
+
+    private Integer current;
+
+    private Integer size;
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class Community {
+
+        private int communityId;
+
+        private String communityName;
+
+        private String communityPicture;
+
+        private String description;
+
+        private float hot;
+
+        private int threadNumber;
+
+        private int resourceId;
+
+    }
+
+}
diff --git a/src/main/java/com/g9/g9backend/pojo/DTO/GetCommunityHotDTO.java b/src/main/java/com/g9/g9backend/pojo/DTO/GetCommunityHotDTO.java
new file mode 100644
index 0000000..222eb07
--- /dev/null
+++ b/src/main/java/com/g9/g9backend/pojo/DTO/GetCommunityHotDTO.java
@@ -0,0 +1,42 @@
+package com.g9.g9backend.pojo.DTO;
+
+import com.g9.g9backend.pojo.Community;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class GetCommunityHotDTO {
+
+    List<Community> communityList;
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class Community{
+
+        private int communityId;
+
+        private String communityName;
+
+        private String communityPicture;
+
+        private String description;
+
+        private float hot;
+
+        private String type;
+
+        private int threadNumber;
+
+        private int resourceId;
+
+        private int status;
+
+    }
+
+}
diff --git a/src/main/java/com/g9/g9backend/pojo/DTO/GetCommunityThreadsDTO.java b/src/main/java/com/g9/g9backend/pojo/DTO/GetCommunityThreadsDTO.java
new file mode 100644
index 0000000..d734433
--- /dev/null
+++ b/src/main/java/com/g9/g9backend/pojo/DTO/GetCommunityThreadsDTO.java
@@ -0,0 +1,44 @@
+package com.g9.g9backend.pojo.DTO;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Date;
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class GetCommunityThreadsDTO {
+
+    private List<Thread> records;
+
+    private Integer total;
+
+    private Integer pages;
+
+    private Integer current;
+
+    private Integer size;
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class Thread{
+
+        private int threadId;
+
+        private int userId;
+
+        private String threadPicture;
+
+        private String title;
+
+        private int likes;
+
+        private Date createAt;
+
+    }
+
+}
diff --git a/src/main/java/com/g9/g9backend/pojo/DTO/GetRewardDTO.java b/src/main/java/com/g9/g9backend/pojo/DTO/GetRewardDTO.java
new file mode 100644
index 0000000..f56b878
--- /dev/null
+++ b/src/main/java/com/g9/g9backend/pojo/DTO/GetRewardDTO.java
@@ -0,0 +1,44 @@
+package com.g9.g9backend.pojo.DTO;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Date;
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class GetRewardDTO {
+
+    private List<Reward> records;
+
+    private long total;
+
+    private long pages;
+
+    private long current;
+
+    private long size;
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class Reward {
+
+        private int rewardId;
+
+        private String rewardName;
+
+        private String rewardPicture;
+
+        private int userId;
+
+        private float price;
+
+        private Date createAt;
+
+    }
+
+}
diff --git a/src/main/java/com/g9/g9backend/pojo/DTO/ThreadDTO.java b/src/main/java/com/g9/g9backend/pojo/DTO/ThreadDTO.java
new file mode 100644
index 0000000..5e0aec7
--- /dev/null
+++ b/src/main/java/com/g9/g9backend/pojo/DTO/ThreadDTO.java
@@ -0,0 +1,36 @@
+package com.g9.g9backend.pojo.DTO;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Date;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ThreadDTO {
+
+    private int threadId;
+
+    private int userId;
+
+    private String threadPicture;
+
+    private String title;
+
+    private String content;
+
+    private int likes;
+
+    @JsonProperty("isLike")
+    private boolean isLike;
+
+    private Date createAt;
+
+    private int commentNumber;
+
+    private int communityId;
+
+}
diff --git a/src/main/java/com/g9/g9backend/pojo/Notification.java b/src/main/java/com/g9/g9backend/pojo/Notification.java
index 7ebf7c1..249eb7e 100644
--- a/src/main/java/com/g9/g9backend/pojo/Notification.java
+++ b/src/main/java/com/g9/g9backend/pojo/Notification.java
@@ -2,6 +2,7 @@
 
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.*;
 
 import java.util.Date;
@@ -21,14 +22,13 @@
 
     private int userId;
 
-    private String type;
-
     private String title;
 
     private String content;
 
     private Date createAt;
 
+    @JsonProperty("isRead")
     private boolean isRead;
 
     private int triggeredBy;
diff --git a/src/main/java/com/g9/g9backend/pojo/Reward.java b/src/main/java/com/g9/g9backend/pojo/Reward.java
index aea9c84..fc2d1ed 100644
--- a/src/main/java/com/g9/g9backend/pojo/Reward.java
+++ b/src/main/java/com/g9/g9backend/pojo/Reward.java
@@ -33,9 +33,9 @@
 
     private Date lastUpdateAt;
 
-    private int completedBy;
+    private Integer completedBy;
 
     private Date completedAt;
 
-    private int resourceId;
+    private Integer resourceId;
 }
\ No newline at end of file
diff --git a/src/main/java/com/g9/g9backend/pojo/ThreadLike.java b/src/main/java/com/g9/g9backend/pojo/ThreadLike.java
new file mode 100644
index 0000000..6818ed4
--- /dev/null
+++ b/src/main/java/com/g9/g9backend/pojo/ThreadLike.java
@@ -0,0 +1,18 @@
+package com.g9.g9backend.pojo;
+
+import com.github.jeffreyning.mybatisplus.anno.MppMultiId;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ThreadLike {
+
+    @MppMultiId
+    private int userId;
+
+    @MppMultiId
+    private int threadId;
+}
diff --git a/src/main/java/com/g9/g9backend/service/ThreadLikeService.java b/src/main/java/com/g9/g9backend/service/ThreadLikeService.java
new file mode 100644
index 0000000..6b782df
--- /dev/null
+++ b/src/main/java/com/g9/g9backend/service/ThreadLikeService.java
@@ -0,0 +1,7 @@
+package com.g9.g9backend.service;
+
+import com.g9.g9backend.pojo.ThreadLike;
+import com.github.jeffreyning.mybatisplus.service.IMppService;
+
+public interface ThreadLikeService extends IMppService<ThreadLike> {
+}
diff --git a/src/main/java/com/g9/g9backend/service/TrackerService.java b/src/main/java/com/g9/g9backend/service/TrackerService.java
new file mode 100644
index 0000000..3d30d6c
--- /dev/null
+++ b/src/main/java/com/g9/g9backend/service/TrackerService.java
@@ -0,0 +1,26 @@
+package com.g9.g9backend.service;
+
+import com.turn.ttorrent.tracker.Tracker;
+import jakarta.annotation.PostConstruct;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+@Service
+public class TrackerService {
+
+    private final Logger logger = LoggerFactory.getLogger(TrackerService.class);
+
+    @PostConstruct
+    public void initTracker() {
+        try {
+            Tracker tracker = new Tracker(6969, "http://localhost:6969/announce");
+            tracker.setAcceptForeignTorrents(true);
+            tracker.start(false);
+            logger.info("Tracker服务器已启动,监听端口 6969");
+        } catch (Exception e) {
+            logger.error("Tracker启动失败:", e);
+        }
+    }
+
+}
diff --git a/src/main/java/com/g9/g9backend/service/impl/ThreadLikeServiceImpl.java b/src/main/java/com/g9/g9backend/service/impl/ThreadLikeServiceImpl.java
new file mode 100644
index 0000000..7dbd3bd
--- /dev/null
+++ b/src/main/java/com/g9/g9backend/service/impl/ThreadLikeServiceImpl.java
@@ -0,0 +1,11 @@
+package com.g9.g9backend.service.impl;
+
+import com.g9.g9backend.mapper.ThreadLikeMapper;
+import com.g9.g9backend.pojo.ThreadLike;
+import com.g9.g9backend.service.ThreadLikeService;
+import com.github.jeffreyning.mybatisplus.service.MppServiceImpl;
+import org.springframework.stereotype.Service;
+
+@Service
+public class ThreadLikeServiceImpl extends MppServiceImpl<ThreadLikeMapper, ThreadLike> implements ThreadLikeService {
+}
diff --git a/src/test/java/com/g9/g9backend/controller/CommentControllerTest.java b/src/test/java/com/g9/g9backend/controller/CommentControllerTest.java
new file mode 100644
index 0000000..d9f9f87
--- /dev/null
+++ b/src/test/java/com/g9/g9backend/controller/CommentControllerTest.java
@@ -0,0 +1,68 @@
+package com.g9.g9backend.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.g9.g9backend.pojo.Comment;
+import com.g9.g9backend.pojo.DTO.GetCommentDTO;
+import com.g9.g9backend.service.CommentService;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatchers;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import org.springframework.http.ResponseEntity;
+
+import java.util.Collections;
+import java.util.Date;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+import static org.mockito.BDDMockito.given;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class CommentControllerTest {
+
+    @Mock
+    private CommentService commentService;
+
+    @InjectMocks
+    private CommentController commentController;
+
+    @Test
+    void testPostComment() {
+        Comment comment = new Comment();
+        ResponseEntity<String> response = commentController.postComment(comment);
+        verify(commentService).save(comment);
+        assertEquals(200, response.getStatusCode().value());
+    }
+
+    @Test
+    void testDeleteComment() {
+        Integer id = 1;
+        ResponseEntity<String> response = commentController.deleteComment(id);
+        verify(commentService).removeById(id);
+        assertEquals(204, response.getStatusCode().value());
+    }
+
+    @Test
+    void testGetCommentWithResourceType() {
+        Comment comment = new Comment();
+        comment.setCommentId(1);
+        comment.setUserId(2);
+        comment.setReplyId(null);
+        comment.setContent("Test");
+        comment.setCreateAt(new Date());
+        comment.setResourceId(1);
+
+        Page<Comment> page = new Page<>();
+        page.setRecords(Collections.singletonList(comment));
+
+        given(commentService.page(ArgumentMatchers.<Page<Comment>>any(), ArgumentMatchers.<LambdaQueryWrapper<Comment>>any())).willReturn(page);
+
+        ResponseEntity<GetCommentDTO> response = commentController.getComment(1, 1, 10, "资源");
+        assertEquals(200, response.getStatusCode().value());
+        assertEquals(1, response.getBody().getRecords().size());
+    }
+}
diff --git a/src/test/java/com/g9/g9backend/controller/CommunityControllerTest.java b/src/test/java/com/g9/g9backend/controller/CommunityControllerTest.java
new file mode 100644
index 0000000..da9bf57
--- /dev/null
+++ b/src/test/java/com/g9/g9backend/controller/CommunityControllerTest.java
@@ -0,0 +1,268 @@
+package com.g9.g9backend.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.g9.g9backend.pojo.Community;
+import com.g9.g9backend.pojo.Subscription;
+import com.g9.g9backend.pojo.Thread;
+import com.g9.g9backend.service.CommunityService;
+import com.g9.g9backend.service.SubscriptionService;
+import com.g9.g9backend.service.ThreadLikeService;
+import com.g9.g9backend.service.ThreadService;
+import org.jetbrains.annotations.NotNull;
+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.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@ExtendWith(MockitoExtension.class)
+public class CommunityControllerTest {
+
+    private MockMvc mockMvc;
+
+    @InjectMocks
+    private CommunityController communityController;
+
+    @Mock
+    private CommunityService communityService;
+
+    @Mock
+    private ThreadService threadService;
+
+    @Mock
+    private SubscriptionService subscriptionService;
+
+    @Mock
+    private ThreadLikeService threadLikeService;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    @BeforeEach
+    public void setup() {
+        mockMvc = MockMvcBuilders.standaloneSetup(communityController).build();
+    }
+
+    @Test
+    public void shouldReturnHotCommunities_whenCallingHotEndpoint() throws Exception {
+        Community c1 = new Community(1, "社区A", "pic", "desc", 9.0f, "type", 5, 1);
+        Page<Community> page = createCommunityPage(List.of(c1));
+        when(communityService.page(any(), any())).thenReturn(page);
+
+        mockMvc.perform(get("/community/hot"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.communityList[0].communityId").value(1));
+    }
+
+    @Test
+    public void shouldReturnCommonCommunities_whenCallingCommonEndpoint() throws Exception {
+        Community c1 = new Community(2, "社区B", "pic", "desc", 7.0f, "type", 3, 1);
+        Page<Community> page = createCommunityPage(List.of(c1));
+        when(communityService.page(any(), any())).thenReturn(page);
+
+        mockMvc.perform(get("/community/common"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.communityList[0].communityId").value(2));
+    }
+
+    @Test
+    public void shouldReturnCommunityInfo_whenIdIsValid() throws Exception {
+        Community community = new Community(3, "社区C", "pic", "desc", 6.0f, "type", 4, 2);
+        when(communityService.getById(3)).thenReturn(community);
+
+        mockMvc.perform(get("/community/info").param("communityId", "3"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.communityId").value(3));
+    }
+
+    @Test
+    public void shouldReturnThreadDetails_whenThreadIdAndUserIdAreValid() throws Exception {
+        Thread thread = mockThread(1, 10);
+        when(threadService.getById(1)).thenReturn(thread);
+        when(threadLikeService.getOne(any())).thenReturn(null);
+
+        mockMvc.perform(get("/thread")
+                        .param("threadId", "1")
+                        .param("userId", "10"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.threadId").value(1))
+                .andExpect(jsonPath("$.userId").value(10));
+    }
+
+    @Test
+    public void shouldPostThread_whenValidRequest() throws Exception {
+        Thread thread = new Thread();
+        thread.setThreadId(100);
+        thread.setCommunityId(1);
+
+        Community community = new Community();
+        community.setCommunityId(1);
+        community.setThreadNumber(5);
+
+        when(communityService.getById(1)).thenReturn(community);
+
+        mockMvc.perform(post("/thread")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(thread)))
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    public void shouldLikeThread_whenValidInput() throws Exception {
+        Thread thread = new Thread();
+        thread.setThreadId(100);
+        thread.setLikes(5);
+
+        when(threadService.getById(100)).thenReturn(thread);
+
+        mockMvc.perform(post("/thread/like")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("{\"userId\":1, \"threadId\":100}"))
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    public void shouldDeleteThread_whenThreadExists() throws Exception {
+        Thread thread = new Thread();
+        thread.setThreadId(100);
+        thread.setCommunityId(1);
+
+        Community community = new Community();
+        community.setCommunityId(1);
+        community.setThreadNumber(5);
+
+        when(threadService.getById(100)).thenReturn(thread);
+        when(communityService.getById(1)).thenReturn(community);
+
+        mockMvc.perform(delete("/thread").param("threadId", "100"))
+                .andExpect(status().isNoContent());
+    }
+
+    @Test
+    public void shouldCancelLike_whenValidRequest() throws Exception {
+        Thread thread = new Thread();
+        thread.setThreadId(100);
+        thread.setLikes(5);
+
+        when(threadService.getById(100)).thenReturn(thread);
+
+        mockMvc.perform(delete("/thread/like")
+                        .param("userId", "1")
+                        .param("threadId", "100"))
+                .andExpect(status().isNoContent());
+    }
+
+    @Test
+    public void shouldReturnCommunities_whenSearchingByType() throws Exception {
+        Community c = new Community(1, "搜索社区", "pic", "desc", 9.5f, "test", 8, 5);
+        Page<Community> page = createCommunityPage(List.of(c));
+
+        when(communityService.page(any(), any())).thenReturn(page);
+
+        mockMvc.perform(get("/community")
+                        .param("searchValue", "搜索")
+                        .param("type", "test")
+                        .param("pageNumber", "1")
+                        .param("rows", "10"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.records[0].communityId").value(1));
+    }
+
+    @Test
+    public void shouldReturnHotThreads_whenOptionIsHot() throws Exception {
+        Thread t = new Thread();
+        t.setThreadId(1);
+        t.setUserId(2);
+        t.setTitle("热帖");
+        t.setLikes(10);
+        Page<Thread> page = new Page<>(1, 10);
+        page.setRecords(List.of(t));
+        page.setTotal(1L);
+
+        when(threadService.page(any(), any())).thenReturn(page);
+
+        mockMvc.perform(get("/community/threads")
+                        .param("communityId", "1")
+                        .param("pageNumber", "1")
+                        .param("rows", "10")
+                        .param("option", "最高热度")
+                        .param("searchValue", "")
+                        .param("userId", "2"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.records[0].threadId").value(1));
+    }
+
+    @Test
+    public void shouldReturnFollowedThreads_whenOptionIsFollowed() throws Exception {
+        Subscription s = new Subscription();
+        s.setUserId(1);
+        s.setFollowerId(2);
+
+        List<Subscription> subscriptions = new ArrayList<>();
+        subscriptions.add(s);
+
+        @SuppressWarnings("unchecked")
+        LambdaQueryWrapper<Subscription> wrapper = any(LambdaQueryWrapper.class);
+        when(subscriptionService.list(wrapper)).thenReturn(subscriptions);
+
+        Thread t = new Thread();
+        t.setThreadId(2);
+        t.setUserId(2);
+        t.setTitle("关注者帖子");
+        t.setLikes(8);
+
+        Page<Thread> page = new Page<>(1, 10);
+        page.setRecords(List.of(t));
+        page.setTotal(1L);
+
+        when(threadService.page(any(), any())).thenReturn(page);
+
+        mockMvc.perform(get("/community/threads")
+                        .param("communityId", "1")
+                        .param("pageNumber", "1")
+                        .param("rows", "10")
+                        .param("option", "关注者")
+                        .param("searchValue", "")
+                        .param("userId", "1"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.records[0].threadId").value(2));
+    }
+
+    @NotNull
+    private Page<Community> createCommunityPage(List<Community> communities) {
+        Page<Community> page = new Page<>(1, 3);
+        page.setRecords(communities);
+        page.setTotal(communities.size());
+        return page;
+    }
+
+    @NotNull
+    @SuppressWarnings("SameParameterValue")
+    private Thread mockThread(int threadId, int userId) {
+        Thread thread = new Thread();
+        thread.setThreadId(threadId);
+        thread.setUserId(userId);
+        thread.setThreadPicture("pic");
+        thread.setTitle("title");
+        thread.setContent("content");
+        thread.setLikes(5);
+        thread.setCreateAt(new Date());
+        thread.setCommentNumber(2);
+        thread.setCommunityId(99);
+        return thread;
+    }
+}
diff --git a/src/test/java/com/g9/g9backend/controller/FileControllerTest.java b/src/test/java/com/g9/g9backend/controller/FileControllerTest.java
new file mode 100644
index 0000000..a8b3d01
--- /dev/null
+++ b/src/test/java/com/g9/g9backend/controller/FileControllerTest.java
@@ -0,0 +1,82 @@
+package com.g9.g9backend.controller;
+
+import com.g9.g9backend.pojo.TorrentRecord;
+import com.g9.g9backend.service.TorrentRecordService;
+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 org.springframework.mock.web.MockMultipartFile;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+import static org.mockito.Mockito.verify;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.hamcrest.Matchers.containsString;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+
+public class FileControllerTest {
+
+    private MockMvc mockMvc;
+
+    @InjectMocks
+    private FileController fileController;
+
+    @Mock
+    private TorrentRecordService torrentRecordService;
+
+    @BeforeEach
+    public void setup() {
+        MockitoAnnotations.openMocks(this);
+        mockMvc = MockMvcBuilders.standaloneSetup(fileController).build();
+    }
+
+    // 测试上传文件接口(模拟 multipart/form-data)
+    @Test
+    public void testUploadFile_success() throws Exception {
+        MockMultipartFile mockFile = new MockMultipartFile(
+                "file",                // 参数名,必须为 file
+                "test.txt",            // 文件名
+                "text/plain",          // MIME 类型
+                "test content".getBytes(StandardCharsets.UTF_8) // 内容
+        );
+
+        mockMvc.perform(multipart("/file").file(mockFile))
+                .andExpect(status().isOk())
+                .andExpect(content().string(containsString("http://localhost:65/")));
+    }
+
+    // 测试上传 BT 文件接口
+    @Test
+    public void testUploadBTFile_success() throws Exception {
+        TorrentRecord record = new TorrentRecord();
+        record.setTorrentRecordId(1);
+        record.setTorrentUrl("test.torrent");
+        record.setInfoHash("abc123");
+        record.setUploaderUserId(1);
+        record.setUploadTime(new Date());
+
+        // 模拟 post 请求,传 json 数据
+        mockMvc.perform(post("/file/bt")
+                        .contentType("application/json")
+                        .content("""
+                                {
+                                  "torrentRecordId": 1,
+                                  "torrentUrl": "test.torrent",
+                                  "infoHash": "abc123",
+                                  "uploaderUserId": 1,
+                                  "uploadTime": "2025-06-09T10:00:00"
+                                }
+                                """))
+                .andExpect(status().isOk());
+
+        // 验证 service 是否调用
+        verify(torrentRecordService).save(org.mockito.ArgumentMatchers.any(TorrentRecord.class));
+    }
+}
diff --git a/src/test/java/com/g9/g9backend/controller/NotificationControllerTest.java b/src/test/java/com/g9/g9backend/controller/NotificationControllerTest.java
new file mode 100644
index 0000000..121258f
--- /dev/null
+++ b/src/test/java/com/g9/g9backend/controller/NotificationControllerTest.java
@@ -0,0 +1,99 @@
+package com.g9.g9backend.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.g9.g9backend.pojo.Notification;
+import com.g9.g9backend.service.NotificationService;
+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 org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.Date;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+public class NotificationControllerTest {
+
+    private MockMvc mockMvc;
+
+    @InjectMocks
+    private NotificationController notificationController;
+
+    @Mock
+    private NotificationService notificationService;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    @BeforeEach
+    public void setup() {
+        MockitoAnnotations.openMocks(this);
+        mockMvc = MockMvcBuilders.standaloneSetup(notificationController).build();
+    }
+
+    // ✅ 测试标记已读
+    @Test
+    public void testGetNotificationRead_success() throws Exception {
+        Notification notification = new Notification();
+        notification.setNotificationId(1);
+
+        Notification updatedNotification = new Notification();
+        updatedNotification.setNotificationId(1);
+        updatedNotification.setRead(false);
+
+        when(notificationService.getById(1)).thenReturn(updatedNotification);
+        when(notificationService.updateById(any())).thenReturn(true);
+
+        mockMvc.perform(post("/notification/read")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(notification)))
+                .andExpect(status().isOk());
+    }
+
+    // ✅ 测试删除通知
+    @Test
+    public void testDeleteNotification_success() throws Exception {
+        when(notificationService.removeById(1)).thenReturn(true);
+
+        mockMvc.perform(delete("/notification")
+                        .param("notificationId", "1"))
+                .andExpect(status().isNoContent());
+    }
+
+    // ✅ 测试分页获取通知
+    @Test
+    public void testGetNotification_success() throws Exception {
+        Notification notification = new Notification();
+        notification.setNotificationId(1);
+        notification.setUserId(1);
+        notification.setTitle("title");
+        notification.setContent("content");
+        notification.setCreateAt(new Date());
+        notification.setRead(false);
+        notification.setTriggeredBy(2);
+        notification.setRelatedId(3);
+
+        Page<Notification> page = new Page<>(1, 10);
+        page.setRecords(List.of(notification));
+        page.setTotal(1);
+
+        when(notificationService.page(any(), any())).thenReturn(page);
+
+        mockMvc.perform(get("/notification")
+                        .param("userId", "1")
+                        .param("pageNumber", "1")
+                        .param("rows", "10"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.records[0].notificationId").value(1))
+                .andExpect(jsonPath("$.records[0].title").value("title"))
+                .andExpect(jsonPath("$.total").value(1));
+    }
+}
diff --git a/src/test/java/com/g9/g9backend/controller/RewardControllerTest.java b/src/test/java/com/g9/g9backend/controller/RewardControllerTest.java
new file mode 100644
index 0000000..7f560f7
--- /dev/null
+++ b/src/test/java/com/g9/g9backend/controller/RewardControllerTest.java
@@ -0,0 +1,135 @@
+package com.g9.g9backend.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.g9.g9backend.pojo.Reward;
+import com.g9.g9backend.service.RewardService;
+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 org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.Collections;
+import java.util.Date;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+public class RewardControllerTest {
+
+    private MockMvc mockMvc;
+
+    @InjectMocks
+    private RewardController rewardController;
+
+    @Mock
+    private RewardService rewardService;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    @BeforeEach
+    public void setUp() {
+        MockitoAnnotations.openMocks(this);
+        mockMvc = MockMvcBuilders.standaloneSetup(rewardController).build();
+    }
+
+    @Test
+    public void testPostReward() throws Exception {
+        Reward reward = new Reward();
+        reward.setRewardName("测试悬赏");
+
+        mockMvc.perform(post("/reward")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(reward)))
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    public void testDeleteReward() throws Exception {
+        mockMvc.perform(delete("/reward")
+                        .param("rewardId", "1"))
+                .andExpect(status().isNoContent());
+    }
+
+    @Test
+    public void testGetReward_sortByPrice() throws Exception {
+        Reward reward = new Reward();
+        reward.setRewardId(1);
+        reward.setRewardName("测试");
+        reward.setPrice(100);
+        reward.setCreateAt(new Date());
+
+        Page<Reward> page = new Page<>(1, 10);
+        page.setRecords(Collections.singletonList(reward));
+        page.setTotal(1);
+
+        when(rewardService.page(any(), any())).thenReturn(page);
+
+        mockMvc.perform(get("/reward")
+                        .param("pageNumber", "1")
+                        .param("rows", "10")
+                        .param("searchValue", "")
+                        .param("option", "赏金最高"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.records[0].rewardId").value(1));
+    }
+
+    @Test
+    public void testGetRewardInfo() throws Exception {
+        Reward reward = new Reward();
+        reward.setRewardId(1);
+        reward.setRewardName("详情测试");
+
+        when(rewardService.getById(1)).thenReturn(reward);
+
+        mockMvc.perform(get("/reward/info")
+                        .param("rewardId", "1"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.rewardId").value(1));
+    }
+
+    @Test
+    public void testPutReward() throws Exception {
+        Reward oldReward = new Reward();
+        oldReward.setRewardId(1);
+
+        when(rewardService.getById(1)).thenReturn(oldReward);
+
+        Reward updateRequest = new Reward();
+        updateRequest.setRewardId(1);
+        updateRequest.setRewardName("更新名称");
+        updateRequest.setRewardDescription("更新描述");
+        updateRequest.setPrice(500);
+        updateRequest.setRewardPicture("pic.png");
+
+        mockMvc.perform(put("/reward/info")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(updateRequest)))
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    public void testPutRewardCompletion() throws Exception {
+        Reward existing = new Reward();
+        existing.setRewardId(1);
+
+        when(rewardService.getById(1)).thenReturn(existing);
+
+        Reward completed = new Reward();
+        completed.setRewardId(1);
+        completed.setCompletedBy(2);
+        completed.setCompletedAt(new Date());
+        completed.setResourceId(3);
+
+        mockMvc.perform(post("/reward/completion")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(completed)))
+                .andExpect(status().isOk());
+    }
+}
diff --git a/src/test/java/com/g9/g9backend/controller/TotalControllerTest.java b/src/test/java/com/g9/g9backend/controller/TotalControllerTest.java
new file mode 100644
index 0000000..35c2484
--- /dev/null
+++ b/src/test/java/com/g9/g9backend/controller/TotalControllerTest.java
@@ -0,0 +1,53 @@
+package com.g9.g9backend.controller;
+
+import com.g9.g9backend.service.ResourceService;
+import com.g9.g9backend.service.ThreadService;
+import com.g9.g9backend.service.UserPurchaseService;
+import com.g9.g9backend.service.UserUploadService;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.ResponseEntity;
+
+@ExtendWith(MockitoExtension.class)
+public class TotalControllerTest {
+
+    @Mock
+    private ThreadService threadService;
+
+    @Mock
+    private UserPurchaseService userPurchaseService;
+
+    @Mock
+    private UserUploadService userUploadService;
+
+    @Mock
+    private ResourceService resourceService;
+
+    @InjectMocks
+    private TotalController totalController;
+
+    @Test
+    void testGetTotalInfo() {
+        when(threadService.count()).thenReturn(10L);
+        when(userPurchaseService.count()).thenReturn(20L);
+        when(userUploadService.count()).thenReturn(5L);
+        when(resourceService.count()).thenReturn(50L);
+
+        ResponseEntity<TotalController.Info> response = totalController.getTotalInfo();
+
+        assertEquals(200, response.getStatusCode().value());
+        TotalController.Info info = response.getBody();
+        assertNotNull(info);
+        assertEquals(10L, info.getThreadCount());
+        assertEquals(20L, info.getDownloadCount());
+        assertEquals(5L, info.getAuthorCount());
+        assertEquals(50L, info.getResourceCount());
+    }
+}