Merge branch 'whx'
Change-Id: I45b82e910474c4a03357bccd17ae3a02f03ee51b
diff --git a/pom.xml b/pom.xml
index d4d9fc2..9238ad6 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>
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..394e445
--- /dev/null
+++ b/src/main/java/com/example/g8backend/controller/AdminController.java
@@ -0,0 +1,46 @@
+package com.example.g8backend.controller;
+
+import com.example.g8backend.dto.ApiResponse;
+import com.example.g8backend.entity.Report;
+import com.example.g8backend.service.AdminService;
+import com.example.g8backend.service.IReportService;
+import org.springframework.beans.factory.annotation.Autowired;
+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;
+ @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("举报处理完成");
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/controller/PostController.java b/src/main/java/com/example/g8backend/controller/PostController.java
index 9ac733f..6800666 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) {
@@ -149,4 +157,56 @@
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()));
+ }
+ }
+
}
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..8ad28f7 100644
--- a/src/main/java/com/example/g8backend/dto/ApiResponse.java
+++ b/src/main/java/com/example/g8backend/dto/ApiResponse.java
@@ -30,4 +30,5 @@
}
// Getters and Setters 略,也可使用 Lombok 注解
-}
\ 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/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..0645824 100644
--- a/src/main/java/com/example/g8backend/entity/User.java
+++ b/src/main/java/com/example/g8backend/entity/User.java
@@ -6,6 +6,8 @@
import lombok.Data;
import lombok.experimental.Accessors;
+import java.time.LocalDate;
+
@Data
@TableName("users")
@Accessors(chain = true)
@@ -17,6 +19,10 @@
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;
@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..1f84eec
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/AdminService.java
@@ -0,0 +1,5 @@
+package com.example.g8backend.service;
+
+public interface AdminService {
+ boolean grantVip(Long targetUserId);
+}
\ 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..d3be9fe
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/AdminServiceImpl.java
@@ -0,0 +1,26 @@
+package com.example.g8backend.service.impl;
+
+import com.example.g8backend.entity.User;
+import com.example.g8backend.mapper.UserMapper;
+import com.example.g8backend.service.AdminService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@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;
+ }
+}
\ 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/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..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/schema.sql b/src/main/resources/schema.sql
index 09276b4..4a3621e 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -4,7 +4,12 @@
`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 '用户角色',
+ INDEX `idx_user_level` (`user_level`) -- 按等级查询优化
);
-- 用户统计表
CREATE TABLE IF NOT EXISTS `user_stats` (
@@ -51,6 +56,8 @@
`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`), -- 新增热度索引
@@ -132,3 +139,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 ON post_ratings (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` BIGINT 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..f8e6229
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/AdminServiceImplTest.java
@@ -0,0 +1,38 @@
+package com.example.g8backend.service;
+
+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 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;
+
+ @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);
+ }
+}
\ 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