post_rating

Change-Id: Ia1a6fb3f87b793a6307046e36951c1fb36b213c8
diff --git a/src/main/java/com/example/g8backend/controller/AuthController.java b/src/main/java/com/example/g8backend/controller/AuthController.java
index e824b63..538d433 100644
--- a/src/main/java/com/example/g8backend/controller/AuthController.java
+++ b/src/main/java/com/example/g8backend/controller/AuthController.java
@@ -1,13 +1,15 @@
 package com.example.g8backend.controller;
 
+import com.example.g8backend.dto.ApiResponse;
 import com.example.g8backend.dto.UserRegisterDTO;
 import com.example.g8backend.entity.User;
+import com.example.g8backend.entity.UserStats;
 import com.example.g8backend.service.IUserService;
+import com.example.g8backend.service.IUserStatsService;
 import com.example.g8backend.util.JwtUtil;
 import com.example.g8backend.util.mailUtil;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.http.ResponseEntity;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.web.bind.annotation.*;
 
@@ -21,72 +23,88 @@
 
     @Autowired
     private IUserService userService;
+
+    @Autowired
+    private IUserStatsService userStatsService;
+
     @Autowired
     private mailUtil mailUtil;
+
     @Autowired
     private PasswordEncoder passwordEncoder;
+
     @Autowired
     private JwtUtil jwtUtil;
+
     @Autowired
-    RedisTemplate<String, Object> redisTemplate;
+    private RedisTemplate<String, Object> redisTemplate;
 
     // 发送验证码
     @PostMapping("/send_verification_code")
-    public ResponseEntity<?> sendVerificationCode(@RequestBody UserRegisterDTO registerDTO) {
+    public ApiResponse<String> sendVerificationCode(@RequestBody UserRegisterDTO registerDTO) {
         if (userService.getUserByEmail(registerDTO.getEmail()) != null) {
-            return ResponseEntity.badRequest().body("邮箱已存在");
+            return ApiResponse.error(400, "邮箱已存在");
         }
 
         String verificationCode = UUID.randomUUID().toString().substring(0, 6);
         mailUtil.sendMail(registerDTO.getEmail(), "PT平台注册验证码", "您的验证码为:" + verificationCode + ",验证码十分钟内有效,请勿泄露。");
 
         redisTemplate.opsForValue().set(registerDTO.getEmail(), verificationCode, 10 * 60, java.util.concurrent.TimeUnit.SECONDS);
-        return ResponseEntity.ok("验证码发送成功");
+        return ApiResponse.success("验证码发送成功");
     }
 
     // 用户注册
     @PostMapping("/register")
-    public ResponseEntity<?> register(@RequestBody UserRegisterDTO registerDTO) {
+    public ApiResponse<String> register(@RequestBody UserRegisterDTO registerDTO) {
         if (userService.getUserByName(registerDTO.getUserName()) != null) {
-            return ResponseEntity.badRequest().body("用户名已存在");
+            return ApiResponse.error(400, "用户名已存在");
         }
 
         if (!redisTemplate.hasKey(registerDTO.getInvitationCode())) {
-            return ResponseEntity.badRequest().body("邀请码错误");
+            return ApiResponse.error(400, "邀请码错误");
         }
-        if (!registerDTO.getVerificationCode().equals(redisTemplate.opsForValue().get(registerDTO.getEmail()))) {
-            return ResponseEntity.badRequest().body("验证码错误");
+
+        Object cachedCode = redisTemplate.opsForValue().get(registerDTO.getEmail());
+        if (!registerDTO.getVerificationCode().equals(cachedCode)) {
+            return ApiResponse.error(400, "验证码错误");
         }
+
         redisTemplate.delete(registerDTO.getEmail());
 
         User user = new User();
         user.setUserName(registerDTO.getUserName());
         user.setPassword(passwordEncoder.encode(registerDTO.getPassword()));
         user.setEmail(registerDTO.getEmail());
-
-        // passkey 用于在客户端发送announce请求时获取用户信息
         user.setPasskey(UUID.randomUUID().toString().replace("-", ""));
         userService.save(user);
 
-        return ResponseEntity.ok("注册成功");
+        UserStats userStats = new UserStats();
+        userStats.setUserId(user.getUserId());
+        userStats.setPasskey(user.getPasskey());
+        userStatsService.save(userStats);
+
+        return ApiResponse.message("注册成功");
     }
 
     // 用户登录
     @PostMapping("/login")
-    public ResponseEntity<?> login(@RequestBody User user) {
+    public ApiResponse<Map<String, String>> login(@RequestBody User user) {
         User existingUser = userService.getUserByEmail(user.getEmail());
         if (existingUser == null || !passwordEncoder.matches(user.getPassword(), existingUser.getPassword())) {
-            return ResponseEntity.badRequest().body("用户名或密码错误");
+            return ApiResponse.error(400, "用户名或密码错误");
         }
+
         String token = jwtUtil.generateToken(existingUser.getUserId());
         Map<String, String> response = new HashMap<>();
         response.put("token", token);
-        return ResponseEntity.ok(response);
+
+        return ApiResponse.success(response);
     }
 
+    // 测试 Redis
     @GetMapping("/test_redis")
-    public ResponseEntity<?> testRedis() {
-        redisTemplate.opsForValue().get("test");
-        return ResponseEntity.ok("test redis ok");
+    public ApiResponse<String> testRedis() {
+        Object value = redisTemplate.opsForValue().get("test");
+        return ApiResponse.success("test redis ok");
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/example/g8backend/controller/ForgotPasswordController.java b/src/main/java/com/example/g8backend/controller/ForgotPasswordController.java
new file mode 100644
index 0000000..5ca8a8e
--- /dev/null
+++ b/src/main/java/com/example/g8backend/controller/ForgotPasswordController.java
@@ -0,0 +1,28 @@
+package com.example.g8backend.controller;
+
+import com.example.g8backend.dto.ApiResponse;
+import com.example.g8backend.dto.ForgotPasswordDTO;
+import com.example.g8backend.dto.ResetPasswordDTO;
+import com.example.g8backend.service.IForgotPasswordService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/forgot-password")
+public class ForgotPasswordController {
+
+    @Autowired
+    private IForgotPasswordService forgotPasswordService;
+
+    @PostMapping("/send-code")
+    public ApiResponse<String> sendCode(@RequestBody ForgotPasswordDTO dto) {
+        forgotPasswordService.sendCodeToEmail(dto.getUsername());
+        return ApiResponse.success("验证码已发送到注册邮箱");
+    }
+
+    @PostMapping("/reset")
+    public ApiResponse<String> resetPassword(@RequestBody ResetPasswordDTO dto) {
+        forgotPasswordService.resetPassword(dto.getUsername(), dto.getCode(), dto.getNewPassword());
+        return ApiResponse.success("密码重置成功");
+    }
+}
diff --git a/src/main/java/com/example/g8backend/controller/PostController.java b/src/main/java/com/example/g8backend/controller/PostController.java
index 9ac733f..e5e4eab 100644
--- a/src/main/java/com/example/g8backend/controller/PostController.java
+++ b/src/main/java/com/example/g8backend/controller/PostController.java
@@ -8,17 +8,20 @@
 import com.example.g8backend.entity.Post;
 import com.example.g8backend.entity.PostView;
 import com.example.g8backend.mapper.PostViewMapper;
+import com.example.g8backend.service.IPostRatingService;
 import com.example.g8backend.service.IPostService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
 
 @RestController
 @RequestMapping("/post")
+@Validated
 public class PostController {
 
     @Autowired
@@ -149,4 +152,43 @@
         Page<Post> result = postService.getRecommendedByTags(page, size, userId);
         return ResponseEntity.ok(ApiResponse.success(result));
     }
+    @Autowired
+    private IPostRatingService postRatingService;
+
+    @PostMapping("/{postId}/rate")
+    public ResponseEntity<ApiResponse<String>> ratePost(
+            @PathVariable Long postId,
+            @RequestParam Integer rating) {
+        try {
+            long userId = (long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+
+            // 调用服务层方法(服务层已内置校验逻辑)
+            postRatingService.ratePost(userId, postId, rating);
+
+            // 成功时返回空数据
+            return ResponseEntity.ok(ApiResponse.success("评分成功"));
+
+        } catch (IllegalArgumentException e) {
+            // 处理参数校验异常(如评分范围错误)
+            return ResponseEntity.badRequest()
+                    .body(ApiResponse.error(400, e.getMessage()));
+
+        } catch (RuntimeException e) {
+            // 处理数据库操作失败等运行时异常
+            return ResponseEntity.internalServerError()
+                    .body(ApiResponse.error(500, e.getMessage()));
+        }
+    }
+
+    @GetMapping("/{postId}/average-rating")
+    public ResponseEntity<ApiResponse<Double>> getAverageRating(@PathVariable Long postId) {
+        Double avg = postRatingService.getAverageRating(postId);
+        return ResponseEntity.ok(ApiResponse.success(avg));
+    }
+
+    @GetMapping("/{postId}/rating-users/count")
+    public ResponseEntity<ApiResponse<Long>> getRatingUserCount(@PathVariable Long postId) {
+        Long count = postRatingService.getRatingUserCount(postId);
+        return ResponseEntity.ok(ApiResponse.success(count));
+    }
 }
diff --git a/src/main/java/com/example/g8backend/controller/TorrentController.java b/src/main/java/com/example/g8backend/controller/TorrentController.java
index 0c016f4..ea7fdf2 100644
--- a/src/main/java/com/example/g8backend/controller/TorrentController.java
+++ b/src/main/java/com/example/g8backend/controller/TorrentController.java
@@ -1,15 +1,15 @@
 package com.example.g8backend.controller;
 
+import com.example.g8backend.dto.ApiResponse;
 import com.example.g8backend.entity.User;
 import com.example.g8backend.entity.Torrent;
 import com.example.g8backend.service.IUserService;
+import com.example.g8backend.service.ITorrentService;
 import jakarta.servlet.http.HttpServletResponse;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.ResponseEntity;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.web.bind.annotation.*;
-import com.example.g8backend.service.ITorrentService;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.io.File;
@@ -20,36 +20,51 @@
 @RestController
 @RequestMapping("/torrent")
 public class TorrentController {
+
     @Autowired
     private ITorrentService torrentService;
 
     @Autowired
     private IUserService userService;
 
-    @RequestMapping("/upload")
-    public ResponseEntity<?> handleTorrentUpload(@RequestParam("file") MultipartFile multipartFile) throws IOException {
+    // 处理种子文件上传
+    @PostMapping("/upload")
+    public ApiResponse<String> handleTorrentUpload(@RequestParam("file") MultipartFile multipartFile) throws IOException {
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
         long userId = (long) authentication.getPrincipal();
 
         User user = userService.getById(userId);
         String passkey = user.getPasskey();
 
+        String fileName = multipartFile.getOriginalFilename();
+
+        try {
+            assert fileName != null;
+        } catch (AssertionError e) {
+            return ApiResponse.error(400, "文件名不能为空");
+        }
+
+        if (!fileName.endsWith(".torrent")) {
+            return ApiResponse.error(400, "文件格式不正确,请上传.torrent格式的文件");
+        }
+
         File tempFile = File.createTempFile("upload-", ".torrent");
         multipartFile.transferTo(tempFile);
 
         try {
-            torrentService.handleTorrentUpload(tempFile, userId, passkey);
+            torrentService.handleTorrentUpload(tempFile, fileName, userId, passkey);
         } catch (IllegalArgumentException e) {
-            return ResponseEntity.badRequest().body(e.getMessage());
+            return ApiResponse.error(400, e.getMessage());
         }
 
         // 删除临时文件
-        if(!tempFile.delete()){
+        if (!tempFile.delete()) {
             throw new IOException("Failed to delete temporary file: " + tempFile.getAbsolutePath());
         }
-        return ResponseEntity.ok("种子上传成功");
+        return ApiResponse.success("种子上传成功");
     }
 
+    // 下载种子文件
     @GetMapping("/download/{torrentId}")
     public void downloadTorrent(@PathVariable String torrentId, HttpServletResponse response) throws IOException {
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
diff --git a/src/main/java/com/example/g8backend/controller/UserSecurityController.java b/src/main/java/com/example/g8backend/controller/UserSecurityController.java
new file mode 100644
index 0000000..0f41549
--- /dev/null
+++ b/src/main/java/com/example/g8backend/controller/UserSecurityController.java
@@ -0,0 +1,29 @@
+package com.example.g8backend.controller;
+
+import com.example.g8backend.dto.PasswordChangeDTO;
+import com.example.g8backend.dto.ApiResponse;
+import com.example.g8backend.service.IUserSecurityService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/user/security")
+public class UserSecurityController {
+
+    @Autowired
+    private IUserSecurityService userSecurityService;
+
+    @PutMapping("/change-password")
+    public ApiResponse<String> changePassword(@RequestBody PasswordChangeDTO dto) {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        Long userId = (Long) authentication.getPrincipal();
+
+        // 调用服务层进行密码修改
+        userSecurityService.changePassword(userId, dto.getOldPassword(), dto.getNewPassword());
+
+        // 返回统一的成功响应
+        return ApiResponse.success("密码修改成功");
+    }
+}
diff --git a/src/main/java/com/example/g8backend/dto/ApiResponse.java b/src/main/java/com/example/g8backend/dto/ApiResponse.java
index 45de32f..8ad28f7 100644
--- a/src/main/java/com/example/g8backend/dto/ApiResponse.java
+++ b/src/main/java/com/example/g8backend/dto/ApiResponse.java
@@ -31,3 +31,4 @@
 
     // Getters and Setters 略,也可使用 Lombok 注解
 }
+
diff --git a/src/main/java/com/example/g8backend/dto/ForgotPasswordDTO.java b/src/main/java/com/example/g8backend/dto/ForgotPasswordDTO.java
new file mode 100644
index 0000000..d9eb3ce
--- /dev/null
+++ b/src/main/java/com/example/g8backend/dto/ForgotPasswordDTO.java
@@ -0,0 +1,8 @@
+package com.example.g8backend.dto;
+
+import lombok.Data;
+
+@Data
+public class ForgotPasswordDTO {
+    private String username;
+}
diff --git a/src/main/java/com/example/g8backend/dto/PasswordChangeDTO.java b/src/main/java/com/example/g8backend/dto/PasswordChangeDTO.java
new file mode 100644
index 0000000..7a96ccf
--- /dev/null
+++ b/src/main/java/com/example/g8backend/dto/PasswordChangeDTO.java
@@ -0,0 +1,9 @@
+package com.example.g8backend.dto;
+
+import lombok.Data;
+
+@Data
+public class PasswordChangeDTO {
+    private String oldPassword;
+    private String newPassword;
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/dto/ResetPasswordDTO.java b/src/main/java/com/example/g8backend/dto/ResetPasswordDTO.java
new file mode 100644
index 0000000..2fa008c
--- /dev/null
+++ b/src/main/java/com/example/g8backend/dto/ResetPasswordDTO.java
@@ -0,0 +1,10 @@
+package com.example.g8backend.dto;
+
+import lombok.Data;
+
+@Data
+public class ResetPasswordDTO {
+    private String username;
+    private String code;
+    private String newPassword;
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/entity/PostRating.java b/src/main/java/com/example/g8backend/entity/PostRating.java
new file mode 100644
index 0000000..ac592b2
--- /dev/null
+++ b/src/main/java/com/example/g8backend/entity/PostRating.java
@@ -0,0 +1,18 @@
+package com.example.g8backend.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("post_ratings")
+public class PostRating {
+    @TableId(type = IdType.INPUT)
+    private Long userId;
+    private Long postId;
+    private Integer rating;
+    private LocalDateTime ratedAt;
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/entity/Torrent.java b/src/main/java/com/example/g8backend/entity/Torrent.java
index cb9a4eb..e52df8a 100644
--- a/src/main/java/com/example/g8backend/entity/Torrent.java
+++ b/src/main/java/com/example/g8backend/entity/Torrent.java
@@ -15,6 +15,7 @@
     private Long torrentId;
     private Long userId;
     private String torrentName;
+    private String filePath;
     private String infoHash;
     private Double fileSize;
 
diff --git a/src/main/java/com/example/g8backend/entity/UserStats.java b/src/main/java/com/example/g8backend/entity/UserStats.java
new file mode 100644
index 0000000..64225ee
--- /dev/null
+++ b/src/main/java/com/example/g8backend/entity/UserStats.java
@@ -0,0 +1,18 @@
+package com.example.g8backend.entity;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("user_stats")
+public class UserStats {
+    @TableId
+    private Long userId;
+    private String passkey;
+    private double total_upload;
+    private double total_download;
+    private LocalDateTime last_update_time;
+}
diff --git a/src/main/java/com/example/g8backend/mapper/PostMapper.java b/src/main/java/com/example/g8backend/mapper/PostMapper.java
index baebb17..ff13000 100644
--- a/src/main/java/com/example/g8backend/mapper/PostMapper.java
+++ b/src/main/java/com/example/g8backend/mapper/PostMapper.java
@@ -64,4 +64,11 @@
             "</script>"
     })
     int batchUpdateHotScore(@Param("posts") List<Post> posts);
+
+    @Update("UPDATE posts SET average_rating = #{averageRating}, rating_count = #{ratingCount} WHERE post_id = #{postId}")
+    void updateRatingStats(
+            @Param("postId") Long postId,
+            @Param("averageRating") Double averageRating,
+            @Param("ratingCount") Integer ratingCount
+    );
 }
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/mapper/PostRatingMapper.java b/src/main/java/com/example/g8backend/mapper/PostRatingMapper.java
new file mode 100644
index 0000000..edf2d26
--- /dev/null
+++ b/src/main/java/com/example/g8backend/mapper/PostRatingMapper.java
@@ -0,0 +1,18 @@
+package com.example.g8backend.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.g8backend.entity.PostRating;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+public interface PostRatingMapper extends BaseMapper<PostRating> {
+    // 自定义查询平均分
+    @Select("SELECT AVG(rating) FROM post_ratings WHERE post_id = #{postId}")
+    Double calculateAverageRating(@Param("postId") Long postId);
+
+    @Select("SELECT COUNT(*) FROM post_ratings WHERE post_id = #{postId}")
+    Integer getRatingCount(@Param("postId") Long postId);
+
+    @Select("SELECT COUNT(DISTINCT user_id) FROM post_ratings WHERE post_id = #{postId}")
+    Long selectRatingUserCount(@Param("postId") Long postId);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/mapper/TorrentMapper.java b/src/main/java/com/example/g8backend/mapper/TorrentMapper.java
index f78a68b..6c3d215 100644
--- a/src/main/java/com/example/g8backend/mapper/TorrentMapper.java
+++ b/src/main/java/com/example/g8backend/mapper/TorrentMapper.java
@@ -7,8 +7,9 @@
 
 @Mapper
 public interface TorrentMapper extends BaseMapper<Torrent> {
-    int insertTorrent (@Param("userId") Long userId,
+    void insertTorrent (@Param("userId") Long userId,
                        @Param("torrentName") String torrentName,
+                       @Param("filePath") String filePath,
                        @Param("infoHash") String infoHash,
                        @Param("fileSize") Double fileSize);
     Torrent getTorrentByInfoHash (@Param("infoHash") String infoHash);
diff --git a/src/main/java/com/example/g8backend/mapper/UserStatsMapper.java b/src/main/java/com/example/g8backend/mapper/UserStatsMapper.java
new file mode 100644
index 0000000..1e23c69
--- /dev/null
+++ b/src/main/java/com/example/g8backend/mapper/UserStatsMapper.java
@@ -0,0 +1,13 @@
+package com.example.g8backend.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.g8backend.entity.UserStats;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+@Mapper
+public interface UserStatsMapper extends BaseMapper<UserStats> {
+    void incrementTraffic(@Param("passkey") String passkey,
+                          @Param("deltaUploaded") double deltaUploaded,
+                          @Param("deltaDownloaded") double deltaDownloaded);
+}
diff --git a/src/main/java/com/example/g8backend/service/IForgotPasswordService.java b/src/main/java/com/example/g8backend/service/IForgotPasswordService.java
new file mode 100644
index 0000000..af865ae
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/IForgotPasswordService.java
@@ -0,0 +1,6 @@
+package com.example.g8backend.service;
+
+public interface IForgotPasswordService {
+    void sendCodeToEmail(String username);
+    boolean resetPassword(String username, String code, String newPassword);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/service/IPostRatingService.java b/src/main/java/com/example/g8backend/service/IPostRatingService.java
new file mode 100644
index 0000000..dc0a4f9
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/IPostRatingService.java
@@ -0,0 +1,10 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.dto.ApiResponse;
+
+public interface IPostRatingService {
+    void ratePost(Long userId, Long postId, Integer rating);
+    Double getAverageRating(Long postId);
+
+    Long getRatingUserCount(Long postId);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/service/ITorrentService.java b/src/main/java/com/example/g8backend/service/ITorrentService.java
index 87622c8..517887c 100644
--- a/src/main/java/com/example/g8backend/service/ITorrentService.java
+++ b/src/main/java/com/example/g8backend/service/ITorrentService.java
@@ -7,7 +7,7 @@
 import java.io.IOException;
 
 public interface ITorrentService extends IService<Torrent> {
-    Torrent handleTorrentUpload(File file, Long userId, String passkey) throws IOException;
+    Torrent handleTorrentUpload(File file, String fileName,Long userId, String passkey) throws IOException;
     File handleTorrentDownload(Torrent torrent, String passkey) throws IOException;
     Torrent findByInfoHash(String infoHash);
     Torrent findByTorrentId(Long torrentId);
diff --git a/src/main/java/com/example/g8backend/service/IUserSecurityService.java b/src/main/java/com/example/g8backend/service/IUserSecurityService.java
new file mode 100644
index 0000000..0ac40dc
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/IUserSecurityService.java
@@ -0,0 +1,5 @@
+package com.example.g8backend.service;
+
+public interface IUserSecurityService {
+    boolean changePassword(Long userId, String oldPassword, String newPassword);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/service/IUserStatsService.java b/src/main/java/com/example/g8backend/service/IUserStatsService.java
new file mode 100644
index 0000000..4771829
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/IUserStatsService.java
@@ -0,0 +1,12 @@
+package com.example.g8backend.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.example.g8backend.entity.UserStats;
+
+/**
+ * 这个service类暂时是空的,因为目前还没做流量检测相关的。
+ * 后面弄作弊检测的时候再补充相关的方法。
+ */
+public interface IUserStatsService extends IService<UserStats> {
+
+}
diff --git a/src/main/java/com/example/g8backend/service/impl/ForgotPasswordServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/ForgotPasswordServiceImpl.java
new file mode 100644
index 0000000..f75617b
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/ForgotPasswordServiceImpl.java
@@ -0,0 +1,66 @@
+package com.example.g8backend.service.impl;
+
+import com.example.g8backend.entity.User;
+import com.example.g8backend.mapper.UserMapper;
+import com.example.g8backend.service.IForgotPasswordService;
+import com.example.g8backend.util.mailUtil;  // 导入 mailUtil
+import jakarta.annotation.Resource;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+@Service
+public class ForgotPasswordServiceImpl implements IForgotPasswordService {
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private RedisTemplate<String, String> redisTemplate;
+
+    @Resource
+    private PasswordEncoder passwordEncoder;
+
+    @Resource
+    private mailUtil mailUtil;  // 注入 mailUtil
+
+    @Override
+    public void sendCodeToEmail(String username) {
+        User user = userMapper.getUserByName(username);
+        if (user == null || user.getEmail() == null) throw new RuntimeException("用户不存在或未绑定邮箱");
+
+        // 生成验证码
+        String code = String.valueOf(100000 + new Random().nextInt(900000));
+
+        // 将验证码存储到 Redis
+        redisTemplate.opsForValue().set("reset_code:" + username, code, 10, TimeUnit.MINUTES);
+
+        // 使用 mailUtil 发送邮件
+        String subject = "重置密码验证码";
+        String message = "您的验证码为:" + code + ",10分钟内有效。";
+        mailUtil.sendMail(user.getEmail(), subject, message);  // 发送邮件
+    }
+
+    @Override
+    public boolean resetPassword(String username, String code, String newPassword) {
+        String key = "reset_code:" + username;
+        String realCode = redisTemplate.opsForValue().get(key);
+
+        if (realCode == null || !realCode.equals(code)) throw new RuntimeException("验证码错误或已过期");
+
+        User user = userMapper.getUserByName(username);
+        if (user == null) throw new RuntimeException("用户不存在");
+
+        // 更新密码
+        user.setPassword(passwordEncoder.encode(newPassword));
+
+        // 删除 Redis 中存储的验证码
+        redisTemplate.delete(key);
+
+        // 更新用户信息
+        return userMapper.updateById(user) > 0;
+    }
+}
diff --git a/src/main/java/com/example/g8backend/service/impl/PostRatingServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/PostRatingServiceImpl.java
new file mode 100644
index 0000000..9afb01d
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/PostRatingServiceImpl.java
@@ -0,0 +1,55 @@
+package com.example.g8backend.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.g8backend.dto.ApiResponse;
+import com.example.g8backend.entity.Post;
+import com.example.g8backend.entity.PostRating;
+import com.example.g8backend.mapper.PostMapper;
+import com.example.g8backend.mapper.PostRatingMapper;
+import com.example.g8backend.service.IPostRatingService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class PostRatingServiceImpl extends ServiceImpl<PostRatingMapper, PostRating> implements IPostRatingService {
+
+    private final PostRatingMapper postRatingMapper;
+    private final PostMapper postMapper;
+
+    @Override
+    @Transactional
+    public void ratePost(Long userId, Long postId, Integer rating) {
+        // 校验评分范围
+        if (rating < 1 || rating > 5) {
+            throw new IllegalArgumentException("评分值必须在1到5之间");
+        }
+
+        // 插入或更新评分记录
+        PostRating postRating = new PostRating();
+        postRating.setUserId(userId);
+        postRating.setPostId(postId);
+        postRating.setRating(rating);
+        boolean success = postRatingMapper.insertOrUpdate(postRating);
+
+        if (!success) {
+            throw new RuntimeException("评分操作失败");
+        }
+
+        // 更新统计信息
+        Double avgRating = postRatingMapper.calculateAverageRating(postId);
+        Integer count = postRatingMapper.getRatingCount(postId);
+        postMapper.updateRatingStats(postId, avgRating, count);
+    }
+
+    @Override
+    public Double getAverageRating(Long postId) {
+        return postRatingMapper.calculateAverageRating(postId);
+    }
+
+    @Override
+    public Long getRatingUserCount(Long postId) {
+        return postRatingMapper.selectRatingUserCount(postId);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java
index 199ff2a..43855a4 100644
--- a/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java
+++ b/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java
@@ -20,7 +20,7 @@
     String tracker = "http://127.0.0.1:8080/tracker/announce/";
 
     @Override
-    public Torrent handleTorrentUpload(File file, Long userId, String passkey) throws IOException, IllegalArgumentException {
+    public Torrent handleTorrentUpload(File file, String fileName, Long userId, String passkey) throws IOException, IllegalArgumentException {
         // 修改 announce 字段
         byte[] modifiedBytes = TorrentUtil.injectTracker(file, tracker + passkey);
 
@@ -44,7 +44,7 @@
         }
 
         // 插入数据库
-        torrentMapper.insertTorrent(userId, file.getName(), infoHash, fileSize);
+        torrentMapper.insertTorrent(userId, fileName, file.getName(), infoHash, fileSize);
 
         // 构建返回实体
         Torrent torrent = new Torrent();
@@ -57,7 +57,7 @@
 
     @Override
     public File handleTorrentDownload(Torrent torrent, String passkey) throws IOException {
-        File torrentFile = new File("uploaded-torrents/" + torrent.getTorrentName());
+        File torrentFile = new File("uploaded-torrents/" + torrent.getFilePath());
         byte[] modifiedBytes = TorrentUtil.injectTracker(torrentFile, tracker + passkey);
 
         File tempFile = File.createTempFile("user_torrent_", ".torrent");
diff --git a/src/main/java/com/example/g8backend/service/impl/TrackerServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/TrackerServiceImpl.java
index ffb5e65..f3d966f 100644
--- a/src/main/java/com/example/g8backend/service/impl/TrackerServiceImpl.java
+++ b/src/main/java/com/example/g8backend/service/impl/TrackerServiceImpl.java
@@ -4,6 +4,7 @@
 import com.example.g8backend.dto.AnnounceResponseDTO;
 import com.example.g8backend.entity.Peer;
 import com.example.g8backend.mapper.PeerMapper;
+import com.example.g8backend.mapper.UserStatsMapper;
 import com.example.g8backend.service.ITrackerService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
@@ -19,6 +20,9 @@
     private PeerMapper peerMapper;
 
     @Autowired
+    private UserStatsMapper userStatsMapper;
+
+    @Autowired
     private RedisTemplate<String, Object> redisTemplate;
 
     @Override
@@ -47,7 +51,7 @@
             newPeer.setPeerId(peerId);
             newPeer.setInfo_hash(infoHash);
             newPeer.setPasskey(passkey);
-            newPeer.setIpAddress(ip); // TODO: 从 request 获取真实 IP
+            newPeer.setIpAddress(ip);
             newPeer.setPort(port);
             newPeer.setUploaded(uploaded);
             newPeer.setDownloaded(downloaded);
@@ -62,6 +66,33 @@
         // 缓存 peer 到 Redis
         redisTemplate.opsForSet().add(redisKey, peerId);
 
+        // 更新用户流量
+        if (!event.equalsIgnoreCase("started")){
+            String statKey = "user:peer:" + passkey + ":" + infoHash + ":" + peerId;
+            Map<Object, Object> last = redisTemplate.opsForHash().entries(statKey);
+
+            Object uploadedObj = last.get("uploaded");
+            Object downloadedObj = last.get("downloaded");
+
+            double lastUploaded = (uploadedObj instanceof Number) ? ((Number) uploadedObj).doubleValue() : 0.0;
+            double lastDownloaded = (downloadedObj instanceof Number) ? ((Number) downloadedObj).doubleValue() : 0.0;
+
+            // 计算流量差值
+            double deltaUploaded = Math.max(0, uploaded - lastUploaded);
+            double deltaDownloaded = Math.max(0, downloaded - lastDownloaded);
+
+            // 更新数据库中用户统计值
+            if (deltaUploaded > 0 || deltaDownloaded > 0) {
+                userStatsMapper.incrementTraffic(passkey, deltaUploaded, deltaDownloaded);
+            }
+
+            // Redis 中更新上传下载量缓存
+            Map<String, Object> current = new HashMap<>();
+            current.put("uploaded", uploaded);
+            current.put("downloaded", downloaded);
+            redisTemplate.opsForHash().putAll(statKey, current);
+        }
+
         // 构造返回 peer 列表
         List<Map<String, Object>> peerList = new ArrayList<>();
         Set<Object> peerIds = redisTemplate.opsForSet().members(redisKey);
diff --git a/src/main/java/com/example/g8backend/service/impl/UserSecurityServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/UserSecurityServiceImpl.java
new file mode 100644
index 0000000..e49551f
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/UserSecurityServiceImpl.java
@@ -0,0 +1,27 @@
+package com.example.g8backend.service.impl;
+
+import com.example.g8backend.entity.User;
+import com.example.g8backend.mapper.UserMapper;
+import com.example.g8backend.service.IUserSecurityService;
+import jakarta.annotation.Resource;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UserSecurityServiceImpl implements IUserSecurityService {
+    @Resource
+    private UserMapper userMapper;
+    @Resource
+    private PasswordEncoder passwordEncoder;
+
+    @Override
+    public boolean changePassword(Long userId, String oldPassword, String newPassword) {
+        User user = userMapper.selectById(userId);
+        if (user == null) throw new RuntimeException("用户不存在");
+        if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
+            throw new RuntimeException("原密码错误");
+        }
+        user.setPassword(passwordEncoder.encode(newPassword));
+        return userMapper.updateById(user) > 0;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/service/impl/UserStatsServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/UserStatsServiceImpl.java
new file mode 100644
index 0000000..9110d16
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/UserStatsServiceImpl.java
@@ -0,0 +1,11 @@
+package com.example.g8backend.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.g8backend.entity.UserStats;
+import com.example.g8backend.mapper.UserStatsMapper;
+import com.example.g8backend.service.IUserStatsService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UserStatsServiceImpl extends ServiceImpl<UserStatsMapper, UserStats> implements IUserStatsService {
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 22f3728..4e44282 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -3,6 +3,7 @@
 spring.datasource.url=jdbc:mysql://localhost:3306/g8backend
 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
 spring.sql.init.mode=always
+#logging.level.root=DEBUG
 
 mybatis-plus.mapper-locations=classpath*:/mapper/**/*.xml
 
diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql
index 607d353..a6860f1 100644
--- a/src/main/resources/data.sql
+++ b/src/main/resources/data.sql
@@ -1,11 +1,66 @@
-# # 后面统一数据库数据用
-#
-# # tags表父标签
-# INSERT INTO `tags` (tag_id, tag_name, parent_id) VALUES (1, '电影', NULL);
-# INSERT INTO `tags` (tag_id, tag_name, parent_id) VALUES (2, '游戏', NULL);
-# INSERT INTO `tags` (tag_id, tag_name, parent_id) VALUES (3, '音乐', NULL);
-#
-# # tags表子标签
-# INSERT INTO `tags` (tag_id, tag_name, parent_id) VALUES (4, '动作', 1);
-# INSERT INTO `tags` (tag_id, tag_name, parent_id) VALUES (5, '喜剧', 1);
-# INSERT INTO `tags` (tag_id, tag_name, parent_id) VALUES (6, '科幻', 1);
\ No newline at end of file
+-- Game 分类
+INSERT IGNORE INTO tags (tag_id, tag_name, parent_id) VALUES (0, 'Game', NULL);
+INSERT IGNORE INTO tags (tag_id, tag_name, parent_id) VALUES
+                                                          (1, 'android', 0),
+                                                          (2, 'mac', 0),
+                                                          (3, 'pc', 0),
+                                                          (4, 'ios', 0),
+                                                          (5, 'other', 0),
+                                                          (6, 'action', 0),
+                                                          (7, 'adventure', 0),
+                                                          (8, 'leisure', 0),
+                                                          (9, 'riddle', 0),
+                                                          (10, 'sport', 0),
+                                                          (11, 'strategy', 0),
+                                                          (12, 'table', 0);
+
+-- video 分类
+INSERT IGNORE INTO tags (tag_id, tag_name, parent_id) VALUES (20, 'video', NULL);
+INSERT IGNORE INTO tags (tag_id, tag_name, parent_id) VALUES
+                                                          (21, 'chinese', 20),
+                                                          (22, 'America', 20),
+                                                          (23, 'Japan', 20),
+                                                          (24, 'Korea', 20),
+                                                          (25, 'Europe', 20),
+                                                          (26, 'other', 20),
+                                                          (27, 'Short', 20),
+                                                          (28, 'plot', 20),
+                                                          (29, 'comedy', 20),
+                                                          (30, 'love', 20),
+                                                          (31, 'action', 20),
+                                                          (32, 'terror', 20),
+                                                          (33, 'science fiction', 20),
+                                                          (34, 'commit a crime', 20),
+                                                          (35, 'Thriller', 20);
+
+-- music 分类
+INSERT IGNORE INTO tags (tag_id, tag_name, parent_id) VALUES (40, 'music', NULL);
+INSERT IGNORE INTO tags (tag_id, tag_name, parent_id) VALUES
+                                                          (41, 'chinese', 40),
+                                                          (42, 'America', 40),
+                                                          (43, 'Japan', 40),
+                                                          (44, 'Korea', 40),
+                                                          (45, 'Europe', 40),
+                                                          (46, 'other', 40),
+                                                          (47, 'rap', 40),
+                                                          (48, 'Electric sound', 40),
+                                                          (49, 'Guofeng', 40),
+                                                          (50, 'motion', 40),
+                                                          (51, 'ballad', 40),
+                                                          (52, 'Rock and roll', 40),
+                                                          (53, 'classical', 40);
+
+-- software 分类
+INSERT IGNORE INTO tags (tag_id, tag_name, parent_id) VALUES (60, 'software', NULL);
+INSERT IGNORE INTO tags (tag_id, tag_name, parent_id) VALUES
+                                                          (61, 'android', 60),
+                                                          (62, 'mac', 60),
+                                                          (63, 'pc', 60),
+                                                          (64, 'ios', 60),
+                                                          (65, 'other', 60),
+                                                          (66, 'life', 60),
+                                                          (67, 'shopping', 60),
+                                                          (68, 'video', 60),
+                                                          (69, 'music', 60),
+                                                          (70, 'read', 60),
+                                                          (71, 'system', 60);
diff --git a/src/main/resources/mapper/PostRatingMapper.xml b/src/main/resources/mapper/PostRatingMapper.xml
new file mode 100644
index 0000000..097eeb8
--- /dev/null
+++ b/src/main/resources/mapper/PostRatingMapper.xml
@@ -0,0 +1,5 @@
+<insert id="insertOrUpdate">
+    INSERT INTO post_ratings (user_id, post_id, rating)
+    VALUES (#{userId}, #{postId}, #{rating})
+    ON DUPLICATE KEY UPDATE rating = VALUES(rating)
+</insert>
\ No newline at end of file
diff --git a/src/main/resources/mapper/TorrentMapper.xml b/src/main/resources/mapper/TorrentMapper.xml
index 9b53d29..243fd92 100644
--- a/src/main/resources/mapper/TorrentMapper.xml
+++ b/src/main/resources/mapper/TorrentMapper.xml
@@ -4,8 +4,8 @@
 
 <mapper namespace="com.example.g8backend.mapper.TorrentMapper">
     <insert id="insertTorrent" >
-        INSERT INTO torrents (user_id, torrent_name, info_hash, file_size)
-        VALUES (#{userId}, #{torrentName}, UNHEX(#{infoHash}), #{fileSize})
+        INSERT INTO torrents (user_id, torrent_name, file_path, info_hash, file_size)
+        VALUES (#{userId}, #{torrentName}, #{filePath}, UNHEX(#{infoHash}), #{fileSize})
     </insert>
 
     <select id="getTorrentByInfoHash" resultType="com.example.g8backend.entity.Torrent">
@@ -13,6 +13,7 @@
             torrent_id,
             user_id,
             torrent_name,
+            file_path,
             HEX(info_hash) AS infoHash,
             file_size
         FROM torrents
@@ -24,6 +25,7 @@
             torrent_id,
             user_id,
             torrent_name,
+            file_path,
             HEX(info_hash) AS infoHash,
             file_size
         FROM torrents
diff --git a/src/main/resources/mapper/UserStatsMapper.xml b/src/main/resources/mapper/UserStatsMapper.xml
new file mode 100644
index 0000000..3560a39
--- /dev/null
+++ b/src/main/resources/mapper/UserStatsMapper.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="com.example.g8backend.mapper.UserStatsMapper">
+    <update id="incrementTraffic">
+        UPDATE user_stats
+        SET total_upload = total_upload + #{deltaUploaded},
+            total_download = total_download + #{deltaDownloaded}
+        WHERE passkey = #{passkey}
+    </update>
+</mapper>
\ No newline at end of file
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index c0659b3..f6d061b 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -6,11 +6,22 @@
   `email` VARCHAR(255) NOT NULL UNIQUE,
   `passkey` VARCHAR(255) NOT NULL UNIQUE
 );
--- 种子表(保持不变)
+-- 用户统计表
+CREATE TABLE IF NOT EXISTS `user_stats` (
+    user_id INT PRIMARY KEY,
+    passkey VARCHAR(255) NOT NULL UNIQUE,
+    total_upload FLOAT NOT NULL DEFAULT 0,
+    total_download FLOAT NOT NULL DEFAULT 0,
+    last_update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (user_id) REFERENCES users(user_id),
+    FOREIGN KEY (passkey) REFERENCES users(passkey)
+);
+-- 种子表
 CREATE TABLE IF NOT EXISTS `torrents` (
   `torrent_id` INT AUTO_INCREMENT PRIMARY KEY,
   `user_id` INT NOT NULL,
   `torrent_name` VARCHAR(255) NOT NULL,
+  `file_path` VARCHAR(255) NOT NULL,
   `info_hash` BINARY(20) NOT NULL,
   `file_size` FLOAT NOT NULL,
   FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`)
@@ -36,17 +47,21 @@
   `view_count` INT DEFAULT 0 COMMENT '浏览数',
   `post_title` VARCHAR(255) NOT NULL,
   `post_content` TEXT NOT NULL,
+  `torrent_id` INT DEFAULT NULL,
   `post_type` ENUM('resource', 'discussion') NOT NULL,
   `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
   `last_calculated` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后热度计算时间',
+  `average_rating` DECIMAL(3,2) DEFAULT 0.00 COMMENT '帖子平均评分',
+  `rating_count` INT DEFAULT 0 COMMENT '总评分人数';
   FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`),
+  FOREIGN KEY (`torrent_id`) REFERENCES `torrents`(`torrent_id`),
   INDEX `idx_hot_score` (`hot_score`),          -- 新增热度索引
   INDEX `idx_post_type` (`post_type`)           -- 新增类型索引
 );
 -- 标签表(保持不变)
 CREATE TABLE IF NOT EXISTS `tags`(
-  `tag_id` INT AUTO_INCREMENT PRIMARY KEY,
-  `tag_name` VARCHAR(255) NOT NULL UNIQUE,
+  `tag_id` INT PRIMARY KEY,
+  `tag_name` VARCHAR(255) NOT NULL,
   `parent_id` INT DEFAULT NULL,
   FOREIGN KEY (`parent_id`) REFERENCES `tags`(`tag_id`)
 );
@@ -110,7 +125,7 @@
   FOREIGN KEY (`post_id`) REFERENCES `posts`(`post_id`),
   INDEX `idx_user_view_time` (`user_id`, `view_time` DESC)  -- 新增用户浏览时间索引
 );
-CREATE TABLE user_tag_preference (
+CREATE TABLE IF NOT EXISTS user_tag_preference (
     user_id INT NOT NULL COMMENT '用户ID',
     tag_id INT NOT NULL COMMENT '标签ID',
     weight DOUBLE DEFAULT 1.0 COMMENT '偏好权重(浏览越多权重越高)',
@@ -118,4 +133,14 @@
     PRIMARY KEY (user_id, tag_id),
     FOREIGN KEY (user_id) REFERENCES users(user_id),
     FOREIGN KEY (tag_id) REFERENCES tags(tag_id)
+);
+CREATE TABLE IF NOT EXISTS `post_ratings` (
+  `user_id` INT NOT NULL COMMENT '用户ID',
+  `post_id` INT NOT NULL COMMENT '帖子ID',
+  `rating` TINYINT NOT NULL CHECK (`rating` BETWEEN 1 AND 5) COMMENT '评分值(1-5)',
+  `rated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '评分时间',
+  PRIMARY KEY (`user_id`, `post_id`),  -- 确保每个用户对同一帖子只能评分一次
+  FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`),
+  FOREIGN KEY (`post_id`) REFERENCES `posts`(`post_id`),
+  INDEX idx_post_ratings_post_id ON post_ratings (post_id)
 );
\ No newline at end of file