Merge "fix GET /user/purchase" 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/config/WebConfig.java b/src/main/java/com/g9/g9backend/config/WebConfig.java
new file mode 100644
index 0000000..193c76c
--- /dev/null
+++ b/src/main/java/com/g9/g9backend/config/WebConfig.java
@@ -0,0 +1,34 @@
+package com.g9.g9backend.config;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * WebConfig 类用于配置 Spring MVC 的拦截器和 CORS 设置
+ *
+ * @author Seamher
+ */
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+    private static final Logger logger = LoggerFactory.getLogger(WebConfig.class);
+
+    /**
+     * 配置 CORS 设置
+     *
+     * @param registry CORS 注册表
+     */
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        logger.info("Configuring CORS mappings");
+        registry.addMapping("/**")
+                .allowedOrigins("*")
+                .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
+                .allowedHeaders("*");
+        logger.info("CORS mappings configured to allow all origins, methods, and headers");
+    }
+
+}
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..313f61e 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 = "D:/nginx/nginx-1.26.2/MCPT";
+
+        // 获取原始文件名
+        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/ResourceController.java b/src/main/java/com/g9/g9backend/controller/ResourceController.java
index e78b8b4..abde7ef 100644
--- a/src/main/java/com/g9/g9backend/controller/ResourceController.java
+++ b/src/main/java/com/g9/g9backend/controller/ResourceController.java
@@ -55,6 +55,14 @@
 
     private final HotTrendService hotTrendService;
 
+    static Map<String, String> GAME_CLASSIFY = new HashMap() {{
+        put("模组", "mod");
+        put("地图", "map");
+        put("整合包", "modPack");
+        put("材质包", "resourcePack");
+    }};
+
+
     public ResourceController(ResourceService resourceService, GameplayService gameplayService, RewardService rewardService, UserUploadService userUploadService, CommunityService communityService, UserService userService, UserPurchaseService userPurchaseService, UserLikeService userLikeService, UserCollectionService userCollectionService, NotificationService notificationService, ResourceVersionService resourceVersionService, SearchHistoryService searchHistoryService, GameVersionService gameVersionService, TorrentRecordService torrentRecordService, HotTrendService hotTrendService) {
         this.resourceService = resourceService;
         this.gameplayService = gameplayService;
@@ -185,6 +193,8 @@
     @PostMapping("like")
     public ResponseEntity<String> likeResource(@RequestBody UserResourceDTO userResourceDTO) {
 
+        logger.info("User id: "+userResourceDTO.getUserId() + "Resource ID: " + userResourceDTO.getResourceId());
+
         QueryWrapper<User> userQuery = new QueryWrapper<>();
         userQuery.eq("user_id", userResourceDTO.getUserId());
         User user = userService.getOne(userQuery);
@@ -333,6 +343,7 @@
         QueryWrapper<Resource> resourceWrapper = new QueryWrapper<>();
         // 搜索名称字段不为空时,根据搜索名称进行模糊匹配,并将该次搜索存到搜索历史表
         if (!Objects.equals(searchValue, "")) {
+            logger.info("Filter by name: " + searchValue);
             resourceWrapper.like("resource_name", searchValue);
             SearchHistory searchHistory = new SearchHistory();
             searchHistory.setSearchContent(searchValue);
@@ -340,13 +351,16 @@
             searchHistoryService.save(searchHistory);
         }
         // 分类字段不为空时,根据类别筛选
-        if (!Objects.equals(classify, "")) {
-            resourceWrapper.eq("classify", classify);
+        logger.info("Classify: " + classify + " Mapped: " + GAME_CLASSIFY.get(classify));
+        if (!Objects.equals(GAME_CLASSIFY.get(classify), "")) {
+            logger.info("Filter by classify: " + classify);
+            resourceWrapper.eq("classify", GAME_CLASSIFY.get(classify));
         }
 
         // 主要玩法列表不为空时,根据主要玩法列表筛选(用户传入一个玩法列表,需要去 gameplay 表中查询该资源的所有玩法列表,只有当用户传入的玩法列表的每个值都在资源实际有的玩法列表中时,该资源才符合条件)
         if (gameplayList.length > 0) {
             // 该子查询统计了用户传入的玩法列表中有多少玩法是在资源的实际玩法列表中有的
+            logger.info("Filter by gameplay: " + Arrays.toString(gameplayList));
             String subQuery = "SELECT COUNT(*) " +
                     "FROM gameplay gp " +
                     "WHERE gp.resource_id = resource.resource_id " +
@@ -359,6 +373,7 @@
         // 游戏版本列表不为空时,根据游戏版本列表筛选
         if (gameVersionList.length > 0) {
             // 拼接游戏版本列表的 IN 子句
+            logger.info("Filter by game version: " + Arrays.toString(gameplayList));
             String gameVersionInClause = String.join(",", Arrays.stream(gameVersionList)
                     .map(v -> "'" + v + "'")
                     .toArray(String[]::new));
@@ -379,6 +394,7 @@
 
         IPage<Resource> resourcePage = resourceService.page(page, resourceWrapper);
         List<Resource> resourceList = resourcePage.getRecords();
+        logger.info("Resource list" + resourceList);
         long total = resourcePage.getTotal();
         long pages = resourcePage.getPages();
         long current = resourcePage.getCurrent();
@@ -470,7 +486,7 @@
     public ResponseEntity<GetResourcePageDTO> getHotResource(@RequestParam String classify, @RequestParam int pageNumber, @RequestParam int rows) {
         IPage<Resource> page = new Page<>(pageNumber, rows);
         QueryWrapper<Resource> resourceQuery = new QueryWrapper<>();
-        resourceQuery.eq("classify", classify);
+        resourceQuery.eq("classify", GAME_CLASSIFY.get(classify));
         // 动态计算热度并按降序排序
         resourceQuery.orderByDesc("(downloads * 0.25 + likes * 0.25 + collections * 0.25 + comments * 0.25)");
         IPage<Resource> resourcePage = resourceService.page(page, resourceQuery);
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/main/resources/application.yml b/src/main/resources/application.yml
index 8b9058d..07ec02f 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -4,7 +4,7 @@
   datasource:
     url: jdbc:mysql://localhost:3306/mcpt
     username: root
-    password: 123456
+    password:
   jpa:
     hibernate:
       ddl-auto: none
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());
+    }
+}