Merge branch 'master' into muzhi
Change-Id: I43d45d4fa806ae88d6a5485c07e85ceb185012ef
diff --git a/pom.xml b/pom.xml
index d4d9fc2..39255c6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,7 +40,6 @@
<version>3.5.11</version>
</dependency>
-
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
@@ -101,6 +100,11 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.springframework.security</groupId>
+ <artifactId>spring-security-test</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/src/main/java/com/example/g8backend/config/SecurityConfig.java b/src/main/java/com/example/g8backend/config/SecurityConfig.java
index 179d95f..82c9946 100644
--- a/src/main/java/com/example/g8backend/config/SecurityConfig.java
+++ b/src/main/java/com/example/g8backend/config/SecurityConfig.java
@@ -1,26 +1,20 @@
package com.example.g8backend.config;
+import com.example.g8backend.filter.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import com.example.g8backend.filter.JwtAuthenticationFilter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
-
@Configuration
@EnableWebSecurity
public class SecurityConfig {
- @Bean
- public BCryptPasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder();
- }
-
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
@@ -28,15 +22,28 @@
}
@Bean
+ public BCryptPasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
- .csrf(AbstractHttpConfigurer::disable)
- .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
- .build();
+ .csrf(AbstractHttpConfigurer::disable)
+ .authorizeHttpRequests(auth -> auth
+ // 管理员接口需ADMIN角色
+ .requestMatchers("/admin/**").hasRole("ADMIN")
+ // 用户签到接口需认证
+ .requestMatchers("/user/signin").authenticated()
+ // 其他请求允许匿名访问(感觉这里应该还需要做修改,暂时先放着)
+ .anyRequest().permitAll()
+ )
+ .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
+ .build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/controller/AdminController.java b/src/main/java/com/example/g8backend/controller/AdminController.java
new file mode 100644
index 0000000..a4193cb
--- /dev/null
+++ b/src/main/java/com/example/g8backend/controller/AdminController.java
@@ -0,0 +1,115 @@
+package com.example.g8backend.controller;
+
+import com.example.g8backend.dto.ApiResponse;
+import com.example.g8backend.entity.Post;
+import com.example.g8backend.entity.Report;
+import com.example.g8backend.service.AdminService;
+import com.example.g8backend.service.IPostService;
+import com.example.g8backend.service.IReportService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/admin")
+public class AdminController {
+ @Autowired
+ private AdminService adminService;
+ private IReportService reportService;
+ @Autowired
+ private IPostService postService;
+ @PostMapping("/grant-vip/{userId}")
+ @PreAuthorize("hasRole('ADMIN')") // 仅允许管理员访问
+ public String grantVip(@PathVariable Long userId) {
+ boolean success = adminService.grantVip(userId);
+ return success ? "VIP授予成功" : "操作失败(用户不存在)";
+ }
+ // 获取举报记录(支持按状态过滤)
+ @GetMapping("/reports")
+ @PreAuthorize("hasRole('ADMIN')")
+ public ApiResponse<List<Report>> getReports(
+ @RequestParam(required = false) String status) {
+ Long adminId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+ return ApiResponse.success(reportService.getReports(status, adminId));
+ }
+ // 处理举报
+ @PutMapping("/reports/{reportId}")
+ @PreAuthorize("hasRole('ADMIN')")
+ public ApiResponse<String> resolveReport(
+ @PathVariable Long reportId,
+ @RequestParam String status,
+ @RequestParam(required = false) String notes) {
+ Long adminId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();//这里之前不太对,service改了
+ reportService.resolveReport(reportId, adminId, status, notes);
+ return ApiResponse.success("举报处理完成");
+ }
+
+
+ // 封禁用户
+ @PostMapping("/users/{userId}/ban")
+ @PreAuthorize("hasRole('ADMIN')")
+ public ApiResponse<String> banUser(
+ @PathVariable Long userId,
+ @RequestParam String reason) {
+ Long adminId = getCurrentAdminId();
+ boolean success = adminService.banUser(userId, reason, adminId);
+ return success ?
+ ApiResponse.success("用户封禁成功") :
+ ApiResponse.error(400, "操作失败");
+ }
+
+ // 解封用户
+ @PostMapping("/users/{userId}/unban")
+ @PreAuthorize("hasRole('ADMIN')")
+ public ApiResponse<String> unbanUser(@PathVariable Long userId) {
+ Long adminId = getCurrentAdminId();
+ boolean success = adminService.unbanUser(userId, adminId);
+ return success ?
+ ApiResponse.success("用户解封成功") :
+ ApiResponse.error(400, "操作失败");
+ }
+
+ // 锁定帖子
+ @PostMapping("/posts/{postId}/lock")
+ @PreAuthorize("hasRole('ADMIN')")
+ public ApiResponse<String> lockPost(
+ @PathVariable Long postId,
+ @RequestParam String reason) {
+ Long adminId = getCurrentAdminId();
+ boolean success = adminService.lockPost(postId, reason, adminId);
+ return success ?
+ ApiResponse.success("帖子已锁定") :
+ ApiResponse.error(400, "操作失败");
+ }
+
+ // 解锁帖子
+ @PostMapping("/posts/{postId}/unlock")
+ @PreAuthorize("hasRole('ADMIN')")
+ public ApiResponse<String> unlockPost(@PathVariable Long postId) {
+ Long adminId = getCurrentAdminId();
+ boolean success = adminService.unlockPost(postId, adminId);
+ return success ?
+ ApiResponse.success("帖子已解锁") :
+ ApiResponse.error(400, "操作失败");
+ }
+ @DeleteMapping("/{postId}")
+ public ResponseEntity<ApiResponse<String>> deletePost(@PathVariable Long postId) {
+ long userId = (long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+ Post post = postService.getById(postId);
+ if (post == null) {
+ return ResponseEntity.status(404).body(ApiResponse.error(404, "Post not found."));
+ }
+ postService.removeById(postId);
+ return ResponseEntity.ok(ApiResponse.message("Post deleted successfully."));
+ }
+
+ private Long getCurrentAdminId() {
+ return (Long) SecurityContextHolder.getContext()
+ .getAuthentication().getPrincipal();
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/controller/AuthController.java b/src/main/java/com/example/g8backend/controller/AuthController.java
index 4cbff03..1fdf906 100644
--- a/src/main/java/com/example/g8backend/controller/AuthController.java
+++ b/src/main/java/com/example/g8backend/controller/AuthController.java
@@ -10,9 +10,10 @@
import com.example.g8backend.util.mailUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
-
+import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@@ -94,6 +95,10 @@
return ApiResponse.error(400, "用户名或密码错误");
}
+ if (existingUser.getIsBanned()) {
+ return ApiResponse.error(403, "账号已被封禁,请联系管理员");
+ }
+
String token = jwtUtil.generateToken(existingUser.getUserId());
Map<String, String> response = new HashMap<>();
response.put("token", token);
@@ -107,5 +112,20 @@
Object value = redisTemplate.opsForValue().get("test");
return ApiResponse.success("test redis ok");
}
+
+ //刷新token
+ @PostMapping("/refresh-token")
+ public ApiResponse<String> refreshToken(HttpServletRequest request) {
+ Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+ if (!(principal instanceof Long userId)) {
+ return ApiResponse.error(401, "未认证,无法刷新token");
+ }
+ String oldToken = request.getHeader("Authorization");
+ if (oldToken != null && oldToken.startsWith("Bearer ")) {
+ oldToken = oldToken.substring(7);
+ }
+ String newToken = jwtUtil.generateToken(userId);
+ return ApiResponse.success("Token刷新成功", newToken);
+ }
}
diff --git a/src/main/java/com/example/g8backend/controller/PostController.java b/src/main/java/com/example/g8backend/controller/PostController.java
index 9ac733f..41be09a 100644
--- a/src/main/java/com/example/g8backend/controller/PostController.java
+++ b/src/main/java/com/example/g8backend/controller/PostController.java
@@ -8,17 +8,23 @@
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 com.example.g8backend.entity.Report;
+import com.example.g8backend.service.IReportService;
import java.util.List;
+
@RestController
@RequestMapping("/post")
+@Validated
public class PostController {
@Autowired
@@ -26,6 +32,8 @@
@Autowired
private PostViewMapper postViewMapper;
+ @Autowired
+ private IReportService reportService;
@PostMapping("")
public ResponseEntity<ApiResponse<Void>> createPost(@RequestBody PostCreateDTO postCreateDTO) {
@@ -43,26 +51,12 @@
@GetMapping("/{postId}")
public ResponseEntity<ApiResponse<Post>> getPost(@PathVariable Long postId) {
+ Post post = postService.getById(postId);
long userId = (long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
postService.recordViewHistory(userId, postId);
- Post post = postService.getById(postId);
return ResponseEntity.ok(ApiResponse.success(post));
}
- @DeleteMapping("/{postId}")
- public ResponseEntity<ApiResponse<String>> deletePost(@PathVariable Long postId) {
- long userId = (long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
- Post post = postService.getById(postId);
- if (post == null) {
- return ResponseEntity.status(404).body(ApiResponse.error(404, "Post not found."));
- }
- if (post.getUserId() != userId) {
- return ResponseEntity.status(403).body(ApiResponse.error(403, "You are not authorized to delete this post."));
- }
- postService.removeById(postId);
- return ResponseEntity.ok(ApiResponse.message("Post deleted successfully."));
- }
-
@GetMapping("/getAll")
public ResponseEntity<ApiResponse<List<Post>>> getAllPosts() {
return ResponseEntity.ok(ApiResponse.success(postService.list()));
@@ -149,4 +143,70 @@
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));
+ }
+ @PostMapping("/{postId}/report")
+ public ResponseEntity<ApiResponse<String>> reportPost(
+ @PathVariable Long postId,
+ @RequestParam String reason) {
+ long userId = (long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+ try {
+ reportService.submitReport(userId, postId, reason);
+ return ResponseEntity.ok(ApiResponse.message("举报已提交"));
+ } catch (IllegalArgumentException e) {
+ return ResponseEntity.badRequest().body(ApiResponse.error(400, e.getMessage()));
+ }
+ }
+
+ @DeleteMapping("/{postId}")
+ public ResponseEntity<ApiResponse<String>> deletePost(@PathVariable Long postId) {
+ long userId = (long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+ Post post = postService.getById(postId);
+ if (post == null) {
+ return ResponseEntity.status(404).body(ApiResponse.error(404, "Post not found."));
+ }
+ if (post.getUserId() != userId) {
+ return ResponseEntity.status(403).body(ApiResponse.error(403, "You are not authorized to delete this post."));
+ }
+ postService.removeById(postId);
+ return ResponseEntity.ok(ApiResponse.message("Post deleted successfully."));
+ }
+
}
diff --git a/src/main/java/com/example/g8backend/controller/UserController.java b/src/main/java/com/example/g8backend/controller/UserController.java
index 121bc1a..f9d0de4 100644
--- a/src/main/java/com/example/g8backend/controller/UserController.java
+++ b/src/main/java/com/example/g8backend/controller/UserController.java
@@ -3,12 +3,16 @@
import com.example.g8backend.dto.ApiResponse;
import com.example.g8backend.entity.Message;
import com.example.g8backend.entity.User;
+import com.example.g8backend.entity.UserSignin;
import com.example.g8backend.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
+import com.example.g8backend.service.ISigningService;
+import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@@ -90,4 +94,25 @@
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return (Long) authentication.getPrincipal();
}
+
+ @Autowired
+ private ISigningService signingService; // 修改接口名称
+
+ @PostMapping("/signin")
+ public ApiResponse<String> signIn() { // 修改返回类型为 ApiResponse
+ Long userId = getCurrentUserId();
+ boolean success = signingService.signIn(userId);
+ String message = success ? "签到成功" : "今日已签到";
+ return ApiResponse.success(message);
+ }
+
+ // 新增获取时间段内的签到记录接口
+ @GetMapping("/signins")
+ public ApiResponse<List<UserSignin>> getSignins(
+ @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
+ @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
+ Long userId = getCurrentUserId();
+ List<UserSignin> signins = signingService.getSigninsByDateRange(userId, startDate, endDate);
+ return ApiResponse.success(signins);
+ }
}
diff --git a/src/main/java/com/example/g8backend/dto/ApiResponse.java b/src/main/java/com/example/g8backend/dto/ApiResponse.java
index fb6ed0e..9940361 100644
--- a/src/main/java/com/example/g8backend/dto/ApiResponse.java
+++ b/src/main/java/com/example/g8backend/dto/ApiResponse.java
@@ -1,12 +1,15 @@
package com.example.g8backend.dto;
+import lombok.Data;
+
+@Data
public class ApiResponse<T> {
private int code;
private String message;
private T data;
public ApiResponse() {}
-
+
public ApiResponse(int code, String message, T data) {
this.code = code;
this.message = message;
@@ -30,4 +33,5 @@
}
// Getters and Setters 略,也可使用 Lombok 注解
-}
\ No newline at end of file
+}
+
diff --git a/src/main/java/com/example/g8backend/entity/Post.java b/src/main/java/com/example/g8backend/entity/Post.java
index 351e24d..e0f2a79 100644
--- a/src/main/java/com/example/g8backend/entity/Post.java
+++ b/src/main/java/com/example/g8backend/entity/Post.java
@@ -5,6 +5,9 @@
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.sql.Timestamp;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
import lombok.Data;
import lombok.experimental.Accessors;
@@ -20,6 +23,18 @@
private String postContent;
private Timestamp createdAt;
private String postType;
+ // 新增锁定相关字段
+ @TableField("is_locked")
+ private Boolean isLocked = false;
+
+ @TableField("locked_reason")
+ private String lockedReason;
+
+ @TableField("locked_at")
+ private LocalDateTime lockedAt;
+
+ @TableField("locked_by")
+ private Long lockedBy;
@TableField("view_count")
private Integer viewCount = 0;
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/Report.java b/src/main/java/com/example/g8backend/entity/Report.java
new file mode 100644
index 0000000..9ddfa5b
--- /dev/null
+++ b/src/main/java/com/example/g8backend/entity/Report.java
@@ -0,0 +1,23 @@
+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("reports")
+public class Report {
+ @TableId(type = IdType.AUTO)
+ private Long reportId;
+ private Long postId;
+ private Long userId;
+ private String reason;
+ private String status;
+ private LocalDateTime createdAt;
+ private Long resolvedBy;
+ private LocalDateTime resolvedAt;
+ private String resolutionNotes;
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/entity/User.java b/src/main/java/com/example/g8backend/entity/User.java
index 64f4f10..aea6335 100644
--- a/src/main/java/com/example/g8backend/entity/User.java
+++ b/src/main/java/com/example/g8backend/entity/User.java
@@ -1,11 +1,15 @@
package com.example.g8backend.entity;
import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
@Data
@TableName("users")
@Accessors(chain = true)
@@ -17,6 +21,22 @@
private String password;
private String userName;
private String email;
+ private String userLevel; // 用户等级(lv1/lv2/lv3/vip)
+ private Integer signinCount;
+ private LocalDate lastSigninDate;
+ private String role;
+ @TableField("is_banned")
+ private Boolean isBanned = false;
+
+ @TableField("banned_reason")
+ private String bannedReason;
+
+ @TableField("banned_at")
+ private LocalDateTime bannedAt;
+
+ @TableField("banned_by")
+ private Long bannedBy;
+
@Override
public String toString() {
diff --git a/src/main/java/com/example/g8backend/entity/UserSignin.java b/src/main/java/com/example/g8backend/entity/UserSignin.java
new file mode 100644
index 0000000..1af4b6c
--- /dev/null
+++ b/src/main/java/com/example/g8backend/entity/UserSignin.java
@@ -0,0 +1,17 @@
+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.LocalDate;
+
+@Data
+@TableName("user_signin")
+public class UserSignin {
+ @TableId(type = IdType.AUTO)
+ private Long signinId;
+ private Long userId;
+ private LocalDate signinDate;
+}
\ No newline at end of file
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/ReportMapper.java b/src/main/java/com/example/g8backend/mapper/ReportMapper.java
new file mode 100644
index 0000000..01e5630
--- /dev/null
+++ b/src/main/java/com/example/g8backend/mapper/ReportMapper.java
@@ -0,0 +1,16 @@
+package com.example.g8backend.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.g8backend.entity.Report;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface ReportMapper extends BaseMapper<Report> {
+ // 自定义查询方法(如按状态查询)
+ @Select("SELECT * FROM reports WHERE status = #{status}")
+ List<Report> selectByStatus(@Param("status") String status);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/mapper/UserMapper.java b/src/main/java/com/example/g8backend/mapper/UserMapper.java
index 0a9e562..98e6c67 100644
--- a/src/main/java/com/example/g8backend/mapper/UserMapper.java
+++ b/src/main/java/com/example/g8backend/mapper/UserMapper.java
@@ -4,10 +4,18 @@
import com.example.g8backend.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
@Mapper
public interface UserMapper extends BaseMapper<User> {
User getUserByName(@Param("userName") String userName);
User getUserByEmail(@Param("email") String email);
User getUserByPasskey(@Param("passkey") String passkey);
+
+ @Select("SELECT * FROM users WHERE user_name = #{name}")
+ User selectByUserName(@Param("name") String name);
+
+ @Update("UPDATE users SET user_level = #{userLevel} WHERE user_id = #{userId}")
+ int updateUserLevel(@Param("userId") Long userId, @Param("userLevel") String userLevel);
}
diff --git a/src/main/java/com/example/g8backend/mapper/UserSigninMapper.java b/src/main/java/com/example/g8backend/mapper/UserSigninMapper.java
new file mode 100644
index 0000000..42a4736
--- /dev/null
+++ b/src/main/java/com/example/g8backend/mapper/UserSigninMapper.java
@@ -0,0 +1,14 @@
+package com.example.g8backend.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.g8backend.entity.UserSignin;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+@Mapper
+public interface UserSigninMapper extends BaseMapper<UserSignin> {
+
+ @Select("SELECT COUNT(*) FROM user_signin WHERE user_id = #{userId} AND signin_date = #{date}")
+ boolean existsByUserIdAndDate(@Param("userId") Long userId, @Param("date") String date);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/service/AdminService.java b/src/main/java/com/example/g8backend/service/AdminService.java
new file mode 100644
index 0000000..da672f5
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/AdminService.java
@@ -0,0 +1,9 @@
+package com.example.g8backend.service;
+
+public interface AdminService {
+ boolean grantVip(Long targetUserId);
+ boolean banUser(Long userId, String reason, Long adminId);
+ boolean unbanUser(Long userId, Long adminId);
+ boolean lockPost(Long postId, String reason, Long adminId);
+ boolean unlockPost(Long postId, Long adminId);
+}
\ 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/IReportService.java b/src/main/java/com/example/g8backend/service/IReportService.java
new file mode 100644
index 0000000..5c67fa8
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/IReportService.java
@@ -0,0 +1,15 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.entity.Report;
+import java.util.List;
+
+public interface IReportService {
+ // 用户提交举报
+ boolean submitReport(Long userId, Long postId, String reason);
+
+ // 管理员处理举报
+ boolean resolveReport(Long reportId, Long adminUserId, String status, String notes);
+
+ // 获取举报列表(按状态过滤)
+ List<Report> getReports(String status,Long requesterUserId);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/service/ISigningService.java b/src/main/java/com/example/g8backend/service/ISigningService.java
new file mode 100644
index 0000000..08b1760
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/ISigningService.java
@@ -0,0 +1,12 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.dto.ApiResponse;
+import com.example.g8backend.entity.UserSignin;
+
+import java.time.LocalDate;
+import java.util.List;
+
+public interface ISigningService {
+ boolean signIn(Long userId);
+ List<UserSignin> getSigninsByDateRange(Long userId, LocalDate startDate, LocalDate endDate);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/service/impl/AdminServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/AdminServiceImpl.java
new file mode 100644
index 0000000..24434f4
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/AdminServiceImpl.java
@@ -0,0 +1,88 @@
+package com.example.g8backend.service.impl;
+
+import com.example.g8backend.entity.Post;
+import com.example.g8backend.entity.User;
+import com.example.g8backend.mapper.UserMapper;
+import com.example.g8backend.mapper.PostMapper;
+import com.example.g8backend.service.AdminService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+
+@Service
+@RequiredArgsConstructor
+public class AdminServiceImpl implements AdminService {
+ private final UserMapper userMapper;
+
+ @Override
+ @Transactional
+ public boolean grantVip(Long targetUserId) {
+ User user = userMapper.selectById(targetUserId);
+ if (user == null) {
+ return false; // 用户不存在
+ }
+ user.setUserLevel("vip");
+ userMapper.updateById(user);
+ return true;
+ }
+
+ private final PostMapper postMapper;
+
+ @Override
+ @Transactional
+ public boolean banUser(Long userId, String reason, Long adminId) {
+ User user = userMapper.selectById(userId);
+ if (user == null) return false;
+
+ user.setIsBanned(true);
+ user.setBannedReason(reason);
+ user.setBannedAt(LocalDateTime.now());
+ user.setBannedBy(adminId);
+
+ return userMapper.updateById(user) > 0;
+ }
+
+ @Override
+ @Transactional
+ public boolean unbanUser(Long userId, Long adminId) {
+ User user = userMapper.selectById(userId);
+ if (user == null) return false;
+
+ user.setIsBanned(false);
+ user.setBannedReason(null);
+ user.setBannedAt(null);
+ user.setBannedBy(null);
+
+ return userMapper.updateById(user) > 0;
+ }
+
+ @Override
+ @Transactional
+ public boolean lockPost(Long postId, String reason, Long adminId) {
+ Post post = postMapper.selectById(postId);
+ if (post == null) return false;
+
+ post.setIsLocked(true);
+ post.setLockedReason(reason);
+ post.setLockedAt(LocalDateTime.now());
+ post.setLockedBy(adminId);
+
+ return postMapper.updateById(post) > 0;
+ }
+
+ @Override
+ @Transactional
+ public boolean unlockPost(Long postId, Long adminId) {
+ Post post = postMapper.selectById(postId);
+ if (post == null) return false;
+
+ post.setIsLocked(false);
+ post.setLockedReason(null);
+ post.setLockedAt(null);
+ post.setLockedBy(null);
+
+ return postMapper.updateById(post) > 0;
+ }
+}
\ No newline at end of file
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/PostServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java
index 1fda1e2..2a43439 100644
--- a/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java
+++ b/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java
@@ -178,7 +178,6 @@
.orderByDesc("hot_score"); // 确保排序条件正确
return postMapper.selectPage(new Page<>(page, size), queryWrapper);
}
-
@Override
public List<PostHistoryDTO> getViewHistoryWithTitles(Long userId) {
// 1. 查询浏览记录(按时间倒序)
@@ -187,15 +186,12 @@
.eq("user_id", userId)
.orderByDesc("view_time")
);
-
-
// 2. 转换为DTO并填充标题
return history.stream().map(view -> {
PostHistoryDTO dto = new PostHistoryDTO();
dto.setViewId(view.getViewId());
dto.setPostId(view.getPostId());
dto.setViewTime(view.getViewTime());
-
// 3. 查询帖子标题
Post post = postMapper.selectById(view.getPostId());
if (post != null) {
diff --git a/src/main/java/com/example/g8backend/service/impl/ReportServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/ReportServiceImpl.java
new file mode 100644
index 0000000..a4d5c1a
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/ReportServiceImpl.java
@@ -0,0 +1,75 @@
+package com.example.g8backend.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.example.g8backend.entity.Report;
+import com.example.g8backend.entity.User;
+import com.example.g8backend.mapper.ReportMapper;
+import com.example.g8backend.mapper.UserMapper;
+import com.example.g8backend.service.IReportService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class ReportServiceImpl implements IReportService {
+ private final ReportMapper reportMapper;
+ private final UserMapper userMapper; // 新增 UserMapper 用于获取用户角色
+
+ @Override
+ @Transactional
+ public boolean submitReport(Long userId, Long postId, String reason) {
+ // 检查是否已存在未处理的举报
+ Long count = reportMapper.selectCount(
+ new QueryWrapper<Report>()
+ .eq("user_id", userId)
+ .eq("post_id", postId)
+ .eq("status", "pending")
+ );
+ if (count > 0) {
+ throw new IllegalArgumentException("您已举报过该帖子,请勿重复提交");
+ }
+
+ // 创建举报记录
+ Report report = new Report();
+ report.setPostId(postId);
+ report.setUserId(userId);
+ report.setReason(reason);
+ report.setStatus("pending");
+ report.setCreatedAt(LocalDateTime.now());
+ return reportMapper.insert(report) > 0;
+ }
+
+ @Override
+ @Transactional
+ public boolean resolveReport(Long reportId, Long adminUserId, String status, String notes) {
+ User adminUser = userMapper.selectById(adminUserId);
+ if (adminUser == null || !"ADMIN".equals(adminUser.getRole())) {
+ throw new AccessDeniedException("无权执行此操作:非管理员用户");
+ }
+
+ Report report = reportMapper.selectById(reportId);
+ if (report == null) {
+ throw new IllegalArgumentException("举报记录不存在");
+ }
+ report.setStatus(status);
+ report.setResolvedBy(adminUserId);
+ report.setResolvedAt(LocalDateTime.now());
+ report.setResolutionNotes(notes);
+ return reportMapper.updateById(report) > 0;
+ }
+
+ @Override
+ public List<Report> getReports(String status, Long requesterUserId) {
+ User requester = userMapper.selectById(requesterUserId);
+ if (requester == null || !"ADMIN".equals(requester.getRole())) {
+ throw new AccessDeniedException("无权查看举报记录:非管理员用户");
+ }
+ return reportMapper.selectList(
+ new QueryWrapper<Report>().eq("status", status)
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/service/impl/SigninServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/SigninServiceImpl.java
new file mode 100644
index 0000000..429efa8
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/SigninServiceImpl.java
@@ -0,0 +1,62 @@
+package com.example.g8backend.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.example.g8backend.entity.User;
+import com.example.g8backend.entity.UserSignin;
+import com.example.g8backend.mapper.UserMapper;
+import com.example.g8backend.mapper.UserSigninMapper;
+import com.example.g8backend.service.ISigningService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.time.LocalDate;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class SigninServiceImpl implements ISigningService{
+ private final UserMapper userMapper;
+ private final UserSigninMapper userSigninMapper;
+
+ @Override
+ @Transactional
+ public boolean signIn(Long userId) {
+ LocalDate today = LocalDate.now();
+ String todayStr = today.toString();
+
+ // 检查今日是否已签到
+ if (userSigninMapper.existsByUserIdAndDate(userId, todayStr)) {
+ return false; // 已签到
+ }
+
+ // 插入签到记录
+ UserSignin signin = new UserSignin();
+ signin.setUserId(userId);
+ signin.setSigninDate(today);
+ userSigninMapper.insert(signin);
+
+ // 更新用户信息
+ User user = userMapper.selectById(userId);
+ user.setSigninCount(user.getSigninCount() + 1);
+ user.setLastSigninDate(today);
+
+ // 根据签到次数升级等级 默认lv1 三次签到lv2 十次签到lv3
+ if (user.getSigninCount() >= 10) {
+ user.setUserLevel("lv3");
+ } else if (user.getSigninCount() >= 3) {
+ user.setUserLevel("lv2");
+ }
+
+ userMapper.updateById(user);
+ return true;
+ }
+
+ @Override
+ public List<UserSignin> getSigninsByDateRange(Long userId, LocalDate startDate, LocalDate endDate) {
+ QueryWrapper<UserSignin> queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq("user_id", userId)
+ .between("signin_date", startDate, endDate)
+ .orderByAsc("signin_date");
+ return userSigninMapper.selectList(queryWrapper);
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 4e44282..19ff0b5 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,6 +1,6 @@
-spring.datasource.password=12345678
-spring.datasource.username=root
-spring.datasource.url=jdbc:mysql://localhost:3306/g8backend
+spring.datasource.password=G812345678#
+spring.datasource.username=team8
+spring.datasource.url=jdbc:mysql://202.205.102.121:3306/g8backend
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.sql.init.mode=always
#logging.level.root=DEBUG
@@ -21,3 +21,5 @@
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
logging.level.org.springframework.data.redis= DEBUG
+
+spring.security.user.role-prefix=
diff --git a/src/main/resources/mapper/PostRatingMapper.xml b/src/main/resources/mapper/PostRatingMapper.xml
new file mode 100644
index 0000000..bb13dd4
--- /dev/null
+++ b/src/main/resources/mapper/PostRatingMapper.xml
@@ -0,0 +1,10 @@
+<?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.mapper.PostRatingMapper">
+ <insert id="insertOrUpdate">
+ INSERT INTO post_ratings (user_id, post_id, rating)
+ VALUES (#{userId}, #{postId}, #{rating})
+ ON DUPLICATE KEY UPDATE rating = VALUES(rating)
+ </insert>
+</mapper>
\ No newline at end of file
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index 09276b4..8a2387c 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -4,7 +4,16 @@
`user_name` VARCHAR(255) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) NOT NULL UNIQUE,
- `passkey` VARCHAR(255) NOT NULL UNIQUE
+ `passkey` VARCHAR(255) NOT NULL UNIQUE,
+ `user_level` ENUM('lv1', 'lv2', 'lv3', 'vip') DEFAULT 'lv1',
+ `signin_count` INT DEFAULT 0,
+ `last_signin_date` DATE,
+ `role` ENUM('USER', 'ADMIN') DEFAULT 'USER' COMMENT '用户角色',
+ `is_banned` BOOLEAN DEFAULT FALSE COMMENT '是否被封禁',
+ `banned_reason` VARCHAR(255) COMMENT '封禁原因',
+ `banned_at` DATETIME COMMENT '封禁时间',
+ `banned_by` BIGINT COMMENT '操作管理员ID',
+ INDEX `idx_user_level` (`user_level`) -- 按等级查询优化
);
-- 用户统计表
CREATE TABLE IF NOT EXISTS `user_stats` (
@@ -51,6 +60,12 @@
`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 '总评分人数',
+ `is_locked` BOOLEAN DEFAULT FALSE COMMENT '是否被锁定',
+ `locked_reason` VARCHAR(255) COMMENT '锁定原因',
+ `locked_at` DATETIME COMMENT '锁定时间',
+ `locked_by` BIGINT COMMENT '操作管理员ID',
FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`),
FOREIGN KEY (`torrent_id`) REFERENCES `torrents`(`torrent_id`),
INDEX `idx_hot_score` (`hot_score`), -- 新增热度索引
@@ -132,3 +147,36 @@
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 (post_id)
+);
+
+CREATE TABLE IF NOT EXISTS `reports` (
+ `report_id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '举报ID',
+ `post_id` INT NOT NULL COMMENT '被举报的帖子ID',
+ `user_id` INT NOT NULL COMMENT '举报人ID',
+ `reason` TEXT NOT NULL COMMENT '举报原因',
+ `status` ENUM('pending', 'resolved', 'rejected') DEFAULT 'pending' COMMENT '处理状态(待处理/已解决/已驳回)',
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '举报时间',
+ `resolved_by` INT DEFAULT NULL COMMENT '处理人ID(管理员)',
+ `resolved_at` TIMESTAMP DEFAULT NULL COMMENT '处理时间',
+ `resolution_notes` TEXT DEFAULT NULL COMMENT '处理备注',
+ FOREIGN KEY (`post_id`) REFERENCES `posts`(`post_id`),
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`),
+ FOREIGN KEY (`resolved_by`) REFERENCES `users`(`user_id`)
+);
+
+CREATE TABLE IF NOT EXISTS `user_signin` (
+ `signin_id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '签到记录ID',
+ `user_id` INT NOT NULL COMMENT '用户ID',
+ `signin_date` DATE NOT NULL COMMENT '签到日期',
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`) ON DELETE CASCADE,
+ UNIQUE KEY `unique_user_daily_signin` (`user_id`, `signin_date`) -- 唯一约束:用户每日只能签到一次
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
\ No newline at end of file
diff --git a/src/test/java/com/example/g8backend/service/AdminServiceImplTest.java b/src/test/java/com/example/g8backend/service/AdminServiceImplTest.java
new file mode 100644
index 0000000..a8983ba
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/AdminServiceImplTest.java
@@ -0,0 +1,147 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.entity.Post;
+import com.example.g8backend.entity.User;
+import com.example.g8backend.mapper.UserMapper;
+import com.example.g8backend.service.impl.AdminServiceImpl;
+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 com.example.g8backend.mapper.PostMapper;
+
+import java.time.LocalDateTime;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class AdminServiceImplTest {
+ @Mock
+ private UserMapper userMapper;
+ @InjectMocks
+ private AdminServiceImpl adminService;
+ @Mock
+ private PostMapper postMapper;
+
+ @Test
+ public void testGrantVip_Success() {
+ User user = new User();
+ user.setUserId(1L);
+ when(userMapper.selectById(1L)).thenReturn(user);
+
+ boolean result = adminService.grantVip(1L);
+ assertTrue(result);
+ assertEquals("vip", user.getUserLevel());
+ }
+
+ @Test
+ public void testGrantVip_UserNotFound() {
+ when(userMapper.selectById(1L)).thenReturn(null);
+ boolean result = adminService.grantVip(1L);
+ assertFalse(result);
+ }
+
+ void testBanUser_Success() {
+ User user = new User().setUserId(1L);
+ when(userMapper.selectById(1L)).thenReturn(user);
+ when(userMapper.updateById(any(User.class))).thenReturn(1);
+
+ boolean result = adminService.banUser(1L, "违规操作", 1001L);
+ assertTrue(result);
+
+ assertTrue(user.getIsBanned());
+ assertEquals("违规操作", user.getBannedReason());
+ assertNotNull(user.getBannedAt());
+ assertEquals(1001L, user.getBannedBy());
+ verify(userMapper).updateById(any(User.class));
+ }
+
+ @Test
+ void testBanUser_UserNotFound() {
+ when(userMapper.selectById(1L)).thenReturn(null);
+ boolean result = adminService.banUser(1L, "原因", 1001L);
+ assertFalse(result);
+ verify(userMapper, never()).updateById(any(User.class));
+ }
+
+ @Test
+ void testUnbanUser_Success() {
+ User user = new User().setUserId(1L)
+ .setIsBanned(true)
+ .setBannedReason("原因")
+ .setBannedAt(LocalDateTime.now())
+ .setBannedBy(1001L);
+ when(userMapper.selectById(1L)).thenReturn(user);
+ when(userMapper.updateById(any(User.class))).thenReturn(1);
+
+ boolean result = adminService.unbanUser(1L, 1001L);
+ assertTrue(result);
+
+ assertFalse(user.getIsBanned());
+ assertNull(user.getBannedReason());
+ assertNull(user.getBannedAt());
+ assertNull(user.getBannedBy());
+ verify(userMapper).updateById(any(User.class));
+ }
+
+ @Test
+ void testUnbanUser_UserNotFound() {
+ when(userMapper.selectById(1L)).thenReturn(null);
+ boolean result = adminService.unbanUser(1L, 1001L);
+ assertFalse(result);
+ verify(userMapper, never()).updateById(any(User.class));
+ }
+
+ @Test
+ void testLockPost_Success() {
+ Post post = new Post().setPostId(1L);
+ when(postMapper.selectById(1L)).thenReturn(post);
+ when(postMapper.updateById(any(Post.class))).thenReturn(1);
+
+ boolean result = adminService.lockPost(1L, "违规内容", 1001L);
+ assertTrue(result);
+
+ assertTrue(post.getIsLocked());
+ assertEquals("违规内容", post.getLockedReason());
+ assertNotNull(post.getLockedAt());
+ assertEquals(1001L, post.getLockedBy());
+ verify(postMapper).updateById(any(Post.class));
+ }
+
+ @Test
+ void testLockPost_PostNotFound() {
+ when(postMapper.selectById(1L)).thenReturn(null);
+ boolean result = adminService.lockPost(1L, "原因", 1001L);
+ assertFalse(result);
+ verify(postMapper, never()).updateById(any(Post.class));
+ }
+
+ @Test
+ void testUnlockPost_Success() {
+ Post post = new Post().setPostId(1L)
+ .setIsLocked(true)
+ .setLockedReason("原因")
+ .setLockedAt(LocalDateTime.now())
+ .setLockedBy(1001L);
+ when(postMapper.selectById(1L)).thenReturn(post);
+ when(postMapper.updateById(any(Post.class))).thenReturn(1);
+
+ boolean result = adminService.unlockPost(1L, 1001L);
+ assertTrue(result);
+
+ assertFalse(post.getIsLocked());
+ assertNull(post.getLockedReason());
+ assertNull(post.getLockedAt());
+ assertNull(post.getLockedBy());
+ verify(postMapper).updateById(any(Post.class));
+ }
+ @Test
+ void testUnlockPost_PostNotFound() {
+ when(postMapper.selectById(1L)).thenReturn(null);
+ boolean result = adminService.unlockPost(1L, 1001L);
+ assertFalse(result);
+ verify(postMapper, never()).updateById(any(Post.class));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/example/g8backend/service/AuthControllerTest.java b/src/test/java/com/example/g8backend/service/AuthControllerTest.java
new file mode 100644
index 0000000..7dbf524
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/AuthControllerTest.java
@@ -0,0 +1,86 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.controller.AuthController;
+import com.example.g8backend.util.JwtUtil;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
+
+@WebMvcTest(AuthController.class)
+@AutoConfigureMockMvc(addFilters = false) // 关键:禁用Security过滤器
+class AuthControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ // 为所有依赖的 Bean 添加 Mock
+ @MockBean
+ private com.example.g8backend.service.IUserService userService;
+ @MockBean
+ private com.example.g8backend.service.IUserStatsService userStatsService;
+ @MockBean
+ private com.example.g8backend.util.mailUtil mailUtil;
+ @MockBean
+ private org.springframework.security.crypto.password.PasswordEncoder passwordEncoder;
+ @MockBean
+ private JwtUtil jwtUtil;
+ @MockBean
+ private org.springframework.data.redis.core.RedisTemplate<String, Object> redisTemplate;
+
+ @BeforeEach
+ void setUp() {
+ // 默认设置为已认证用户
+ Authentication authentication = mock(Authentication.class);
+ when(authentication.getPrincipal()).thenReturn(123L);
+ SecurityContext securityContext = mock(SecurityContext.class);
+ when(securityContext.getAuthentication()).thenReturn(authentication);
+ SecurityContextHolder.setContext(securityContext);
+ }
+
+ @Test
+ @DisplayName("刷新token-认证通过返回新token")
+ void refreshToken_ShouldReturnNewToken() throws Exception {
+ String newToken = "new.jwt.token";
+ when(jwtUtil.generateToken(123L)).thenReturn(newToken);
+
+ mockMvc.perform(post("/auth/refresh-token")
+ .header("Authorization", "Bearer old.jwt.token")
+ .with(csrf())
+ .accept("application/json")) // 加上这一行
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.message").value("Token刷新成功"))
+ .andExpect(jsonPath("$.data").value(newToken));
+ }
+
+ @Test
+ @DisplayName("刷新token-未认证返回401")
+ void refreshToken_Unauthenticated_ShouldReturn401() throws Exception {
+ // 设置为未认证
+ Authentication authentication = mock(Authentication.class);
+ when(authentication.getPrincipal()).thenReturn("anonymousUser");
+ SecurityContext securityContext = mock(SecurityContext.class);
+ when(securityContext.getAuthentication()).thenReturn(authentication);
+ SecurityContextHolder.setContext(securityContext);
+
+ mockMvc.perform(post("/auth/refresh-token")
+ .with(csrf())
+ .accept("application/json")) // 加上这一行
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(401))
+ .andExpect(jsonPath("$.message").value("未认证,无法刷新token"));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/example/g8backend/service/PostRatingServiceImplTest.java b/src/test/java/com/example/g8backend/service/PostRatingServiceImplTest.java
new file mode 100644
index 0000000..942080a
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/PostRatingServiceImplTest.java
@@ -0,0 +1,103 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.entity.PostRating;
+import com.example.g8backend.mapper.PostMapper;
+import com.example.g8backend.mapper.PostRatingMapper;
+import com.example.g8backend.service.impl.PostRatingServiceImpl;
+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.transaction.annotation.Transactional;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+@Transactional
+public class PostRatingServiceImplTest {
+
+ @Mock
+ private PostRatingMapper postRatingMapper;
+
+ @Mock
+ private PostMapper postMapper;
+
+ @InjectMocks
+ private PostRatingServiceImpl postRatingService;
+
+ private final Long userId = 1L;
+ private final Long postId = 100L;
+ private final Integer validRating = 4;
+ private final Integer invalidRating = 6;
+
+ // 测试:合法评分应成功
+ @Test
+ public void testRatePost_Success() {
+ // 模拟依赖行为
+ when(postRatingMapper.insertOrUpdate(any(PostRating.class))).thenReturn(true);
+ when(postRatingMapper.calculateAverageRating(postId)).thenReturn(4.0);
+ when(postRatingMapper.getRatingCount(postId)).thenReturn(1);
+
+ // 调用方法并验证无异常
+ assertDoesNotThrow(() -> {
+ postRatingService.ratePost(userId, postId, validRating);
+ });
+
+ // 验证数据库交互
+ verify(postMapper).updateRatingStats(eq(postId), eq(4.0), eq(1));
+ }
+
+ // 测试:非法评分应抛出异常
+ @Test
+ public void testRatePost_InvalidRating() {
+ // 调用方法并验证异常
+ IllegalArgumentException exception = assertThrows(
+ IllegalArgumentException.class,
+ () -> postRatingService.ratePost(userId, postId, invalidRating)
+ );
+ assertEquals("评分值必须在1到5之间", exception.getMessage());
+
+ // 验证未调用数据库操作
+ verifyNoInteractions(postRatingMapper);
+ }
+
+ // 测试:重复评分应更新记录
+ @Test
+ public void testRatePost_UpdateExistingRating() {
+ // 模拟已存在评分
+ when(postRatingMapper.insertOrUpdate(any(PostRating.class))).thenReturn(true);
+ when(postRatingMapper.calculateAverageRating(postId)).thenReturn(3.5, 4.0); // 两次调用返回不同值
+ when(postRatingMapper.getRatingCount(postId)).thenReturn(1);
+
+ // 同一用户对同一帖子二次评分
+ assertDoesNotThrow(() -> {
+ postRatingService.ratePost(userId, postId, 3);
+ postRatingService.ratePost(userId, postId, 4);
+ });
+
+ // 验证两次更新统计信息
+ verify(postMapper, times(2)).updateRatingStats(eq(postId), anyDouble(), eq(1));
+ }
+
+ // 测试:数据库操作失败应抛出异常
+ @Test
+ public void testRatePost_DatabaseFailure() {
+ when(postRatingMapper.insertOrUpdate(any(PostRating.class))).thenReturn(false);
+ RuntimeException exception = assertThrows(
+ RuntimeException.class,
+ () -> postRatingService.ratePost(userId, postId, validRating)
+ );
+ assertEquals("评分操作失败", exception.getMessage());
+ }
+ // 测试:获取评分用户数量
+ @Test
+ public void testGetRatingUserCount() {
+ when(postRatingMapper.selectRatingUserCount(postId)).thenReturn(5L);
+ Long count = postRatingService.getRatingUserCount(postId);
+ assertEquals(5L, count);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/example/g8backend/service/ReportServiceImplTest.java b/src/test/java/com/example/g8backend/service/ReportServiceImplTest.java
new file mode 100644
index 0000000..8b59f82
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/ReportServiceImplTest.java
@@ -0,0 +1,131 @@
+package com.example.g8backend.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.example.g8backend.entity.Report;
+import com.example.g8backend.entity.User;
+import com.example.g8backend.mapper.ReportMapper;
+import com.example.g8backend.mapper.UserMapper;
+import com.example.g8backend.service.impl.ReportServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.lang.reflect.Field;
+import org.springframework.security.access.AccessDeniedException;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class ReportServiceImplTest {
+
+ @Mock
+ private ReportMapper reportMapper;
+
+ @Mock
+ private UserMapper userMapper;
+
+ @InjectMocks
+ private ReportServiceImpl reportService;
+
+ private static final Long TEST_USER_ID = 1L;
+ private static final Long TEST_ADMIN_ID = 999L;
+ private static final Long TEST_REPORT_ID = 1000L;
+
+ private User adminUser;
+ private User normalUser;
+ private Report pendingReport;
+
+ @BeforeEach
+ void setUp() {
+ // 初始化管理员用户
+ adminUser = new User();
+ adminUser.setUserId(TEST_ADMIN_ID);
+ adminUser.setRole("ADMIN");
+
+ // 初始化普通用户
+ normalUser = new User();
+ normalUser.setUserId(TEST_USER_ID);
+ normalUser.setRole("USER");
+
+ // 初始化待处理举报
+ pendingReport = new Report();
+ pendingReport.setReportId(TEST_REPORT_ID);
+ pendingReport.setStatus("pending");
+ }
+
+ // ------------------------- submitReport 测试 -------------------------
+ @Test
+ void submitReport_WhenNoDuplicate_ShouldReturnTrue() {
+ when(reportMapper.selectCount(any())).thenReturn(0L);
+ when(reportMapper.insert(any(Report.class))).thenReturn(1);
+
+ boolean result = reportService.submitReport(TEST_USER_ID, 2L, "违规内容");
+ assertTrue(result);
+ verify(reportMapper).insert(any(Report.class));
+ }
+
+ // ------------------------- resolveReport 测试 -------------------------
+ @Test
+ void resolveReport_WhenAdminProcesses_ShouldSucceed() {
+ when(userMapper.selectById(TEST_ADMIN_ID)).thenReturn(adminUser);
+ Report mockReport = new Report();
+ mockReport.setStatus("pending");
+ when(reportMapper.selectById(TEST_REPORT_ID)).thenReturn(mockReport);
+
+ when(reportMapper.updateById(any(Report.class))).thenReturn(1);
+
+ boolean result = reportService.resolveReport(TEST_REPORT_ID, TEST_ADMIN_ID, "resolved", "已处理");
+
+ assertTrue(result);
+ verify(reportMapper).updateById(any(Report.class)); // 明确验证参数类型
+ }
+
+ @Test
+ void resolveReport_WhenNonAdminAttempts_ShouldThrowAccessDenied() {
+ when(userMapper.selectById(TEST_USER_ID)).thenReturn(normalUser);
+
+ // 使用正确的异常类型
+ assertThrows(AccessDeniedException.class, () ->
+ reportService.resolveReport(TEST_REPORT_ID, TEST_USER_ID, "resolved", null)
+ );
+ verify(reportMapper, never()).updateById(any(Report.class));
+ }
+ // ------------------------- getReports 测试 -------------------------
+ @Test
+ void getReports_WhenAdminQueries_ShouldReturnFiltered() {
+ // 模拟管理员存在
+ when(userMapper.selectById(TEST_ADMIN_ID)).thenReturn(adminUser);
+ when(reportMapper.selectList(any())).thenReturn(Collections.singletonList(pendingReport));
+
+ // 执行方法
+ List<Report> result = reportService.getReports("pending", TEST_ADMIN_ID);
+ assertEquals(1, result.size());
+
+ // 验证是否调用了 selectList 方法
+ verify(reportMapper).selectList(any(QueryWrapper.class));
+
+ // 打印实际 SQL 用于调试(可选)
+ QueryWrapper<Report> queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq("status", "pending");
+ System.out.println("Expected SQL: " + queryWrapper.getTargetSql());
+ }
+ @Test
+ void getReports_WhenNonAdminQueries_ShouldThrowAccessDenied() {
+ when(userMapper.selectById(TEST_USER_ID)).thenReturn(normalUser);
+ assertThrows(AccessDeniedException.class, () ->
+ reportService.getReports("pending", TEST_USER_ID)
+ );
+ verify(reportMapper, never()).selectList(any());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/example/g8backend/service/SigninServiceImplTest.java b/src/test/java/com/example/g8backend/service/SigninServiceImplTest.java
new file mode 100644
index 0000000..48eec66
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/SigninServiceImplTest.java
@@ -0,0 +1,186 @@
+package com.example.g8backend.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.example.g8backend.entity.User;
+import com.example.g8backend.entity.UserSignin;
+import com.example.g8backend.mapper.UserMapper;
+import com.example.g8backend.mapper.UserSigninMapper;
+import com.example.g8backend.service.impl.SigninServiceImpl;
+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 java.time.LocalDate;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class SigninServiceImplTest {
+ @Mock
+ private UserMapper userMapper;
+ @Mock
+ private UserSigninMapper userSigninMapper;
+ @InjectMocks
+ private SigninServiceImpl signinService;
+
+ @Test
+ public void testSignIn_Success() {
+ // 模拟用户初始状态
+ User user = new User();
+ user.setUserId(1L);
+ user.setSigninCount(0);
+ user.setUserLevel("lv1");
+
+ when(userSigninMapper.existsByUserIdAndDate(eq(1L), any())).thenReturn(false);
+ when(userMapper.selectById(1L)).thenReturn(user);
+
+ boolean result = signinService.signIn(1L);
+ assertTrue(result);
+ assertEquals(1, user.getSigninCount());
+ }
+
+ @Test
+ public void testSignIn_AlreadySigned() {
+ when(userSigninMapper.existsByUserIdAndDate(eq(1L), any())).thenReturn(true);
+ boolean result = signinService.signIn(1L);
+ assertFalse(result);
+ }
+
+ @Test
+ public void testSignIn_UpgradeToLv2() {
+ User user = new User();
+ user.setUserId(1L);
+ user.setSigninCount(2); // 初始签到2次
+ user.setUserLevel("lv1");
+
+ when(userSigninMapper.existsByUserIdAndDate(eq(1L), any())).thenReturn(false);
+ when(userMapper.selectById(1L)).thenReturn(user);
+
+ boolean result = signinService.signIn(1L);
+ assertTrue(result);
+ assertEquals(3, user.getSigninCount());
+ assertEquals("lv2", user.getUserLevel());
+ }
+
+ @Test
+ public void testSignIn_UpgradeToLv3() {
+ User user = new User();
+ user.setUserId(1L);
+ user.setSigninCount(9); // 初始签到9次
+ user.setUserLevel("lv2");
+
+ when(userSigninMapper.existsByUserIdAndDate(eq(1L), any())).thenReturn(false);
+ when(userMapper.selectById(1L)).thenReturn(user);
+
+ boolean result = signinService.signIn(1L);
+ assertTrue(result);
+ assertEquals(10, user.getSigninCount());
+ assertEquals("lv3", user.getUserLevel());
+ }
+ @Test
+ public void testGetSigninsByDateRange_NormalCase() {
+ // 模拟输入参数
+ Long userId = 1L;
+ LocalDate startDate = LocalDate.of(2023, 10, 1);
+ LocalDate endDate = LocalDate.of(2023, 10, 31);
+
+ // 构建模拟数据
+ UserSignin signin1 = new UserSignin();
+ signin1.setSigninId(1L);
+ signin1.setUserId(userId);
+ signin1.setSigninDate(LocalDate.of(2023, 10, 5));
+
+ UserSignin signin2 = new UserSignin();
+ signin2.setSigninId(2L);
+ signin2.setUserId(userId);
+ signin2.setSigninDate(LocalDate.of(2023, 10, 15));
+
+ List<UserSignin> mockSignins = Arrays.asList(signin1, signin2);
+
+ when(userSigninMapper.selectList(any(QueryWrapper.class))).thenReturn(mockSignins);
+
+ // 执行方法
+ List<UserSignin> result = signinService.getSigninsByDateRange(userId, startDate, endDate);
+
+ // 断言结果
+ assertEquals(2, result.size());
+ assertEquals(LocalDate.of(2023, 10, 5), result.get(0).getSigninDate());
+ assertEquals(LocalDate.of(2023, 10, 15), result.get(1).getSigninDate());
+ verify(userSigninMapper).selectList(any(QueryWrapper.class));
+ }
+
+ @Test
+ public void testGetSigninsByDateRange_NoRecords() {
+ Long userId = 1L;
+ LocalDate startDate = LocalDate.of(2023, 11, 1);
+ LocalDate endDate = LocalDate.of(2023, 11, 30);
+
+ when(userSigninMapper.selectList(any())).thenReturn(Collections.emptyList());
+
+ List<UserSignin> result = signinService.getSigninsByDateRange(userId, startDate, endDate);
+
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void testGetSigninsByDateRange_SingleDay() {
+ Long userId = 1L;
+ LocalDate date = LocalDate.of(2023, 10, 10);
+
+ UserSignin signin = new UserSignin();
+ signin.setSigninId(3L);
+ signin.setUserId(userId);
+ signin.setSigninDate(date);
+
+ // 修正:返回包含模拟数据的列表
+ when(userSigninMapper.selectList(any(QueryWrapper.class))).thenReturn(Collections.singletonList(signin));
+
+ List<UserSignin> result = signinService.getSigninsByDateRange(userId, date, date);
+
+ assertEquals(1, result.size());
+ assertEquals(date, result.get(0).getSigninDate());
+ }
+ @Test
+ public void testGetSigninsByDateRange_InvalidDateOrder() {
+ Long userId = 1L;
+ LocalDate startDate = LocalDate.of(2023, 10, 31);
+ LocalDate endDate = LocalDate.of(2023, 10, 1);
+
+ when(userSigninMapper.selectList(any())).thenReturn(Collections.emptyList());
+
+ List<UserSignin> result = signinService.getSigninsByDateRange(userId, startDate, endDate);
+
+ assertTrue(result.isEmpty());
+ }
+
+ public void testGetSigninsByDateRange_SortedOrder() {
+ Long userId = 1L;
+ LocalDate startDate = LocalDate.of(2023, 10, 1);
+ LocalDate endDate = LocalDate.of(2023, 10, 31);
+
+ // 模拟数据并按日期升序排列
+ UserSignin signin1 = new UserSignin();
+ signin1.setSigninId(1L);
+ signin1.setSigninDate(LocalDate.of(2023, 10, 5));
+
+ UserSignin signin2 = new UserSignin();
+ signin2.setSigninId(2L);
+ signin2.setSigninDate(LocalDate.of(2023, 10, 15));
+
+ List<UserSignin> mockSignins = Arrays.asList(signin1, signin2); // 直接提供已排序的数据
+
+ when(userSigninMapper.selectList(any(QueryWrapper.class))).thenReturn(mockSignins);
+
+ List<UserSignin> result = signinService.getSigninsByDateRange(userId, startDate, endDate);
+
+ // 验证顺序
+ assertEquals(LocalDate.of(2023, 10, 5), result.get(0).getSigninDate());
+ assertEquals(LocalDate.of(2023, 10, 15), result.get(1).getSigninDate());
+ }
+}
\ No newline at end of file