Compare commits

...

40 Commits
tss ... main

@ -2,7 +2,7 @@ package cn.org.alan.exam.common.exception;
/**
* @Author WeiJin
* @Version 1.0
* @Version 1.022222
* @Date 2024/3/29 21:06
*/
public class AppException extends RuntimeException { // 定义一个名为AppException的自定义异常类它继承自RuntimeException。

@ -1,19 +1,21 @@
package cn.org.alan.exam.controller;
// 声明该类所在的包名,用于对类进行组织和分类管理,表明此控制器类属于该指定包
import cn.org.alan.exam.common.group.AnswerGroup;
// 引入AnswerGroup类可能用于对答案相关操作的分组验证等功能从名字推测
import cn.org.alan.exam.common.result.Result;
// 引入Result类通常用于封装操作的结果信息比如包含操作成功与否以及对应的数据等情况方便统一返回格式
import cn.org.alan.exam.model.form.answer.CorrectAnswerFrom;
import cn.org.alan.exam.model.vo.answer.AnswerExamVO;
import cn.org.alan.exam.model.vo.answer.UncorrectedUserVO;
import cn.org.alan.exam.model.vo.answer.UserAnswerDetailVO;
import cn.org.alan.exam.service.IManualScoreService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import jakarta.annotation.Resource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
// 引入CorrectAnswerFrom类可能是代表正确答案相关的表单数据模型类用于接收前端提交的正确答案相关的数据结构
import cn.org.alan.exam.model.vo.answer.AnswerExamVO; // 引入AnswerExamVO类大概率是视图对象Value ObjectVO用于向客户端传递特定的与考试相关的数据展示格式
import cn.org.alan.exam.model.vo.answer.UncorrectedUserVO; // 引入UncorrectedUserVO类同样是视图对象类可能用于展示待批阅用户相关的数据展示形式
import cn.org.alan.exam.model.vo.answer.UserAnswerDetailVO; // 引入UserAnswerDetailVO类用于向客户端展示答卷详细信息的视图对象
import cn.org.alan.exam.service.IManualScoreService; // 引入IManualScoreService类这应该是一个服务接口用于定义和实现与手动评分相关的业务逻辑
import com.baomidou.mybatisplus.core.metadata.IPage; // 引入IPage类这是MyBatis Plus框架提供的用于表示分页数据的类型用于处理分页查询相关业务逻辑
import jakarta.annotation.Resource; // 用于进行资源注入在这里可将实现了IManualScoreService接口的具体实例注入到当前控制器类中方便调用对应的服务方法
import org.springframework.security.access.prepost.PreAuthorize; // 用于在方法级别进行权限控制,根据配置的权限表达式来判断当前用户是否有权限访问对应的方法,保障系统安全性
import org.springframework.validation.annotation.Validated; // 结合具体的分组类如AnswerGroup.CorrectGroup.class可以对传入方法的参数进行基于分组的验证确保数据的合法性和有效性
import org.springframework.web.bind.annotation.*; // 通配符导入包含了很多Spring Web相关的注解用于定义控制器类中的请求处理方法以及映射对应的HTTP请求路径和请求方式等
import java.util.List; // 引入Java标准库中的List接口用于表示列表类型在这里用于处理返回多个数据元素的情况
/**
*
@ -22,44 +24,44 @@ import java.util.List;
* @Version
* @Date 2024/3/25 11:20 AM
*/
@RestController
@RequestMapping("/api/answers")
public class AnswerController {
@RestController // 这是Spring框架提供的复合注解结合了@Controller和@ResponseBody的功能意味着这个类是一个Spring MVC中的控制器类并且类中的方法返回值会直接以JSON等格式响应给客户端默认情况下无需额外配置视图解析等相关内容
@RequestMapping("/api/answers") // 用于给整个控制器类的所有请求处理方法定义一个基础的请求路径前缀表明该控制器类中所有处理请求的方法的URL路径都将以/api/answers开头方便对一组相关的API进行统一的路径管理
public class AnswerController { // 定义一个名为AnswerController的公共类作为控制器来处理相关业务逻辑请求
@Resource
private IManualScoreService manualScoreService;
@Resource // 使用@Resource注解将一个实现了IManualScoreService接口的实例注入到当前的AnswerController类中使得后续方法可以调用该服务接口所定义的业务逻辑方法
private IManualScoreService manualScoreService; // 声明一个私有变量manualScoreService类型为IManualScoreService用于后续调用相关服务方法实现具体业务逻辑
/**
*
* @return
*/
@GetMapping("/detail")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
public Result<List<UserAnswerDetailVO>> getDetail(@RequestParam Integer userId,
@RequestParam Integer examId) {
return manualScoreService.getDetail(userId, examId);
@GetMapping("/detail") // 表示这个方法用于处理HTTP GET请求对应的请求路径是在类级别定义的基础路径/api/answers基础上追加/detail即完整请求路径为/api/answers/detail
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')") // 进行权限控制只有拥有role_teacher或者role_admin权限的用户才能访问这个方法确保只有教师或管理员角色的用户可以查询答卷详情增强系统的安全性
public Result<List<UserAnswerDetailVO>> getDetail(@RequestParam Integer userId, // 表示从请求中获取名为userId的参数参数类型为整数用于指定用户ID以便准确查询对应的用户答卷详情
@RequestParam Integer examId) { // 表示从请求中获取名为examId的参数参数类型为整数用于指定考试ID配合userId一起准确查询对应的用户答卷详情
return manualScoreService.getDetail(userId, examId); // 方法体内部直接调用注入的manualScoreService接口的getDetail方法并将获取到的请求参数传递进去最终将服务层返回的结果直接返回给客户端由服务层去具体实现根据传入的用户ID和考试ID查询答卷详细信息的业务逻辑
}
/**
*
* @return
*/
@PutMapping("/correct")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
public Result<String> Correct(@RequestBody @Validated(AnswerGroup.CorrectGroup.class) List<CorrectAnswerFrom> correctAnswerFroms) {
return manualScoreService.correct(correctAnswerFroms);
@PutMapping("/correct") // 指定这个方法处理HTTP PUT请求请求路径是在类基础路径上追加/correct也就是/api/answers/correctPUT请求常用于更新资源等操作这里用于批改试卷这个更新数据的场景比较合适
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')") // 进行权限控制,限定只有教师或管理员角色能执行此操作
public Result<String> Correct(@RequestBody @Validated(AnswerGroup.CorrectGroup.class) List<CorrectAnswerFrom> correctAnswerFroms) { // @RequestBody表示方法的参数correctAnswerFroms是从请求的正文中获取数据即将前端发送过来的JSON等格式的数据转换为对应的Java对象列表同时通过@Validated结合指定分组类对传入的参数列表进行数据验证确保数据合法性只有验证通过的数据才进入方法内部进行后续处理
return manualScoreService.correct(correctAnswerFroms); // 方法返回Result<String>类型调用manualScoreService的correct方法由服务层去具体实现批改试卷的业务逻辑并将结果封装到Result对象中返回给控制器再由控制器返回给客户端这里的String可能用于表示批改试卷操作的一些相关反馈信息比如批改是否成功等简单提示
}
/**
*
* @return
*/
@GetMapping("/exam/page")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
public Result<IPage<AnswerExamVO>> examPage(@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "examName", required = false) String examName) {
return manualScoreService.examPage(pageNum, pageSize, examName);
@GetMapping("/exam/page") // 表明此方法处理HTTP GET请求对应的请求路径是/api/answers/exam/page用于分页查找待阅卷考试相关信息
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')") // 权限控制,保证只有教师或管理员角色能访问该方法
public Result<IPage<AnswerExamVO>> examPage(@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum, // 获取请求中的pageNum参数可选参数若请求中未提供则默认值为1用于指定分页查询的页码
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize, // 获取pageSize参数同样可选默认值为10代表每页显示的记录数量
@RequestParam(value = "examName", required = false) String examName) { // 获取examName参数可选可能用于根据考试名称进行模糊查询等筛选操作以便更精准地查找待阅卷的考试信息
return manualScoreService.examPage(pageNum, pageSize, examName); // 方法返回Result<IPage<AnswerExamVO>>类型由服务层去实现具体的分页查询待阅卷考试信息的业务逻辑并返回相应结果给客户端IPage<AnswerExamVO>是MyBatis Plus中的分页对象包含了符合条件的AnswerExamVO类型的分页数据
}
/**
@ -69,12 +71,12 @@ public class AnswerController {
* @param examId
* @return
*/
@GetMapping("/exam/stu")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
public Result<IPage<UncorrectedUserVO>> stuExamPage(@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "examId") Integer examId,
@RequestParam(value = "realName", required = false) String realName) {
return manualScoreService.stuExamPage(pageNum, pageSize, examId,realName);
@GetMapping("/exam/stu") // 注解说明这个方法处理HTTP GET请求请求路径为/api/answers/exam/stu用于查询待批阅的用户相关信息
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')") // 进行权限控制,只有教师或管理员角色的用户可访问该方法
public Result<IPage<UncorrectedUserVO>> stuExamPage(@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum, // 用于指定页码可选参数若未提供则默认为1
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize, // 用于指定每页记录数可选参数默认为10
@RequestParam(value = "examId") Integer examId, // 是必填参数用于指定考试的ID以此来确定要查询哪一场考试下待批阅的用户
@RequestParam(value = "realName", required = false) String realName) { // 是可选参数,可能用于根据用户的真实姓名进行模糊查询等操作,更精准地查找待批阅用户
return manualScoreService.stuExamPage(pageNum, pageSize, examId,realName); // 方法返回Result<IPage<UncorrectedUserVO>>类型,由服务层去实现具体根据给定条件查询待批阅用户信息的业务逻辑,并将结果封装在合适的对象中返回给客户端
}
}
}

@ -1,95 +1,90 @@
package cn.org.alan.exam.controller;
import cn.org.alan.exam.common.group.UserGroup;
import cn.org.alan.exam.common.result.Result;
import cn.org.alan.exam.model.form.Auth.LoginForm;
import cn.org.alan.exam.model.form.UserForm;
import cn.org.alan.exam.service.IAuthService;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Map;
package cn.org.alan.exam.controller; // 定义该类所属的包名,通过包名对类进行分类管理,让代码结构更清晰,此控制器类属于该指定的包,便于在项目中区分不同功能模块的代码
import cn.org.alan.exam.common.group.UserGroup; // 引入UserGroup类可能用于对用户相关操作按照不同场景、规则等进行分组验证例如不同的用户注册、修改信息等操作的验证分组便于灵活控制数据验证逻辑
import cn.org.alan.exam.common.result.Result; // 引入Result类通常用来统一封装业务操作的结果包含操作是否成功的标识以及可能需要返回的数据等信息方便以一致的格式返回给调用者如前端
import cn.org.alan.exam.model.form.Auth.LoginForm; // 引入LoginForm类这是一个数据模型类用于承载用户登录时从前端传递过来的相关信息比如用户名、密码等登录所需的数据结构
import cn.org.alan.exam.model.form.UserForm; // 引入UserForm类同样是数据模型类用于接收用户相关的信息在不同场景如注册、信息更新等存储用户提交的各种数据内容符合相应的业务数据格式要求
import cn.org.alan.exam.service.IAuthService; // 引入IAuthService接口定义了与权限认证相关的一系列业务方法例如登录、注销、注册等操作的逻辑抽象具体的实现由对应的服务类来完成该控制器类会依赖这个接口来调用相应功能
import jakarta.annotation.Resource; // 这是用于进行资源注入的注解在这里的作用是将实现了IAuthService接口的具体实例注入到当前的AuthController类中使得类中的方法可以方便地调用对应的服务方法实现具体业务逻辑
import jakarta.servlet.http.HttpServletRequest; // 引入HttpServletRequest类它代表了客户端发送过来的HTTP请求对象在控制器方法中可借助它获取请求中的各种信息比如请求参数、请求头信息也常用于获取session相关信息像获取sessionId等操作
import jakarta.servlet.http.HttpServletResponse; // 引入HttpServletResponse类用于在控制器方法中对HTTP响应进行处理比如设置响应的状态码、响应头例如设置内容类型等以及向客户端输出响应内容如返回图片验证码等情况
import org.springframework.http.ResponseEntity; // 引入ResponseEntity类Spring框架中用于构建更灵活、全面的HTTP响应实体可方便地设置响应的各种属性如状态码、头信息以及响应体内容等不过在此代码中暂时未看到直接使用它的地方
import org.springframework.security.access.prepost.PreAuthorize; // 用于在方法级别进行权限控制的注解,根据配置的权限表达式判断当前用户是否有相应权限来访问对应的方法,以此保证系统资源只能被授权用户访问,增强系统的安全性和权限管理
import org.springframework.validation.annotation.Validated; // 结合具体的分组类像这里的UserGroup.RegisterGroup.class等可以对传入方法的参数进行分组验证确保接收到的参数数据符合特定业务规则和验证要求保证数据的合法性和准确性
import org.springframework.web.bind.annotation.*; // 通配符导入包含了众多Spring Web相关的注解例如用于定义请求处理方法的不同请求方式注解如@GetMapping、@PostMapping等以及处理请求路径、请求参数等相关的注解方便在控制器类中构建API接口
import java.time.Duration; // 引入Duration类用于表示时间间隔常用于处理涉及时间长度相关的业务逻辑例如设置某个操作的超时时间、计算两个时间点之间的时长等但在当前代码片段中未明确体现其使用场景
import java.time.LocalDateTime; // 引入LocalDateTime类用于表示本地的日期和时间在业务逻辑中可能会用于记录操作发生的时间、判断时间有效性等情况不过此处暂时未看到具体使用它的地方
import java.util.Map; // 引入Java标准库中的Map接口用于存储键值对形式的数据结构常用于传递多个相关参数、存储配置信息等场景在当前代码里尚未看到具体运用的地方
/**
*
*
*
* @Author WeiJin
* @Version
* @Date 2024/3/25 11:05 AM
*/
@RestController
@RequestMapping("/api/auths")
public class AuthController {
@RestController // 这是Spring框架提供的复合注解兼具@Controller和@ResponseBody的功能。意味着这个类是Spring MVC中的控制器类并且类中方法的返回值默认会直接以JSON等格式响应给客户端无需额外配置视图解析相关操作
@RequestMapping("/api/auths") // 用于给整个控制器类下的所有请求处理方法设置一个公共的请求路径前缀表明此类中所有处理请求的方法对应的URL路径都将以/api/auths开头便于统一管理和组织与权限认证相关的一组API接口路径
public class AuthController { // 定义一个公共的类类名为AuthController作为权限管理相关业务逻辑的处理中心对外提供各种权限认证相关的接口接收和处理对应的HTTP请求
@Resource
private IAuthService iAuthService;
@Resource // 使用@Resource注解来进行依赖注入目的是让Spring容器查找并注入一个实现了IAuthService接口的实例到当前类中这样后续的方法就能方便地调用该服务实例所提供的各种权限认证相关的业务方法
private IAuthService iAuthService; // 声明一个私有成员变量iAuthService其类型为IAuthService接口类型通过依赖注入后它将指向对应的服务实现类实例用于在控制器方法中调用具体的权限认证相关业务逻辑方法
/**
*
* @param request requestsessionId
* @param user
* @return token
* @param request requestsessionIdsessionsessionId
* @param user LoginFormLoginForm
* @return tokentokentoken
*/
@PostMapping("/login")
public Result<String> login(HttpServletRequest request,
@Validated @RequestBody LoginForm loginForm) {
return iAuthService.login(request,loginForm);
@PostMapping("/login") // 该注解表明这个方法用于处理HTTP POST请求并且其请求路径是在类级别定义的基础路径/api/auths基础上添加/login即完整的请求路径为/api/auths/login。POST请求常用于向服务器提交数据在此处符合用户提交登录信息的场景
public Result<String> login(HttpServletRequest request, // 接收HttpServletRequest类型的参数request通过它可以获取请求相关的各种详细信息比如从请求头中获取客户端相关标识信息、从请求的Cookie中获取sessionId等在登录逻辑中会利用这些信息辅助完成登录验证等操作
@Validated @RequestBody LoginForm loginForm) { // @Validated注解结合@RequestBody注解@RequestBody表示从请求的正文中获取数据并将其转换为LoginForm类型的对象而@Validated用于对这个转换后的LoginForm对象依据相关规则此处可能结合了默认验证规则或特定分组验证规则等进行数据合法性验证只有验证通过的数据才会进入方法内部进行后续的登录业务逻辑处理
return iAuthService.login(request,loginForm); // 方法体内部直接调用通过依赖注入获取的iAuthService接口实例的login方法并将接收到的request和经过验证的loginForm参数传递进去由服务层去具体实现用户登录的详细业务逻辑比如验证用户名和密码是否匹配、生成有效的登录token等最后将服务层返回的包含登录结果及token信息等的Result对象直接返回给客户端
}
/**
*
* @param request requestsession
* @return
* @param request requestsessionsession访
* @return ResultResult
*/
@DeleteMapping("/logout")
public Result<String> logout(HttpServletRequest request) {
return iAuthService.logout(request);
@DeleteMapping("/logout") // 此注解指定这个方法用于处理HTTP DELETE请求其请求路径是在类的基础路径/api/auths基础上追加/logout也就是/api/auths/logout。DELETE请求常用于删除资源等操作这里用于表示删除用户的登录状态这一资源符合注销操作的语义
public Result<String> logout(HttpServletRequest request) { // 接收HttpServletRequest类型的参数request借助它来操作session找到与当前登录用户对应的session信息进而执行清除session内容的操作实现注销用户登录状态的功能
return iAuthService.logout(request); // 调用通过依赖注入获取的iAuthService接口实例的logout方法由服务层去具体实现注销的详细业务逻辑比如清理session中的用户相关信息、更新相关的登录状态记录等然后将处理后的结果封装到Result对象中返回给客户端告知注销操作的执行情况
}
/**
*
* @param request requestsessionId
* @param userForm
* @return
* @param request requestsessionIdsessionsessionsessionId
* @param userForm userFormUserFormUserGroup.RegisterGroup.class
* @return Result
*/
@PostMapping("/register")
public Result<String> register(HttpServletRequest request,
@RequestBody @Validated(UserGroup.RegisterGroup.class) UserForm userForm) {
return iAuthService.register(request, userForm);
@PostMapping("/register") // 表明该方法用于处理HTTP POST请求其请求路径为/api/auths/registerPOST请求常用于向服务器提交新的数据在此场景下符合用户提交注册信息的操作特点
public Result<String> register(HttpServletRequest request, // 接收HttpServletRequest类型的参数request通过它可以获取到请求相关的各种信息例如获取sessionId用于关联注册操作与特定的会话或者从请求中获取一些必要的辅助验证信息等服务层在处理注册逻辑时会利用这些信息
@RequestBody @Validated(UserGroup.RegisterGroup.class) UserForm userForm) { // @RequestBody注解表示从请求正文中获取数据并转换为UserForm类型的对象@Validated结合特定的分组类UserGroup.RegisterGroup.class对转换后的userForm对象进行数据验证确保用户提交的注册信息符合相应的业务规则要求只有验证通过的数据才会进入后续的注册业务逻辑处理流程
return iAuthService.register(request, userForm); // 调用iAuthService接口的register方法由服务层去具体实现用户注册的详细业务逻辑比如将用户注册信息保存到数据库中、为新注册的学生用户设置默认权限等最后把注册操作的结果封装到Result对象中返回给客户端告知注册是否成功以及可能的提示信息等情况
}
/**
*
* @param request requestsessionId
* @param response response
* @param request requestsessionIdsession便sessionId
* @param response responseHTTPimage/png
*/
@GetMapping("/captcha")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) {
iAuthService.getCaptcha(request, response);
@GetMapping("/captcha") // 表示这个方法用于处理HTTP GET请求其请求路径是/api/auths/captchaGET请求常用于获取资源这里用于获取图片验证码这一资源的操作符合GET请求的使用场景
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) { // 接收HttpServletRequest类型的参数request用于获取请求相关信息特别是获取sessionId等用于验证码相关操作的辅助信息接收HttpServletResponse类型的参数response用于对响应进行操作如设置响应头告知客户端返回的是图片数据以及将生成的图片验证码数据写入到响应流中返回给客户端等
iAuthService.getCaptcha(request, response); // 直接调用iAuthService接口的getCaptcha方法由服务层去具体实现生成图片验证码、将验证码相关信息存储到session通过操作request获取的session来存储以及把图片验证码通过response返回给客户端等一系列业务逻辑该方法没有返回值因为主要操作是通过response直接向客户端返回图片验证码数据
}
/**
*
* @param request requestsessionId
* @param code
* @return
* @param request requestsessionIdsessionrequestsessionIdsession
* @param code @PathVariablesession
* @return ResultResult
*/
@PostMapping("/verifyCode/{code}")
public Result<String> verifyCode(HttpServletRequest request, @PathVariable("code") String code) {
return iAuthService.verifyCode(request, code);
@PostMapping("/verifyCode/{code}") // 表示该方法用于处理HTTP POST请求其请求路径为/api/auths/verifyCode/{code},其中{code}是路径变量,通过@PathVariable注解来获取用户输入的验证码作为路径的一部分POST请求常用于提交数据进行验证等操作这里用于提交用户输入的验证码进行校验的场景符合POST请求的语义
public Result<String> verifyCode(HttpServletRequest request, @PathVariable("code") String code) { // 接收HttpServletRequest类型的参数request用于获取请求相关信息特别是通过获取sessionId来查找之前存储的验证码相关内容通过@PathVariable("code")注解获取用户输入的验证码字符串,然后将这些信息传递给服务层进行验证码校验的业务逻辑处理
return iAuthService.verifyCode(request, code); // 调用iAuthService接口的verifyCode方法由服务层去具体实现校验用户输入的验证码与之前存储在session中的原始验证码是否一致、判断验证码是否在有效期内等业务逻辑最后把校验结果封装到Result对象中返回给客户端告知验证码校验的情况
}
@PostMapping("/track-presence")
public Result<String> trackPresence(HttpServletRequest request) {
return iAuthService.sendHeartbeat(request);
@PostMapping("/track-presence") // 表示此方法用于处理HTTP POST请求其请求路径为/api/auths/track-presence从方法名推测可能是用于跟踪用户的某种存在状态比如在线状态、活跃状态等POST请求常用于提交相关数据来触发相应的业务逻辑这里用于提交相关信息以启动对用户状态的跟踪操作
public Result<String> trackPresence(HttpServletRequest request) { // 接收HttpServletRequest类型的参数request用于获取请求相关信息比如获取用户的标识信息等以便服务层能够准确判断是哪个用户的状态需要被跟踪为后续的业务逻辑处理提供必要的数据基础
return iAuthService.sendHeartbeat(request); // 调用iAuthService接口的sendHeartbeat方法由服务层去具体实现发送心跳推测是类似定期发送信号来表明用户处于某种期望的状态比如在线状态等相关业务逻辑最后把操作的结果封装到Result对象中返回给客户端告知用户状态跟踪的相关情况
}
}
}

@ -1,97 +1,96 @@
package cn.org.alan.exam.controller;
import cn.org.alan.exam.common.group.CertificateGroup;
import cn.org.alan.exam.common.result.Result;
import cn.org.alan.exam.model.entity.Certificate;
import cn.org.alan.exam.model.form.CertificateForm;
import cn.org.alan.exam.model.vo.certificate.MyCertificateVO;
import cn.org.alan.exam.service.ICertificateService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import jakarta.annotation.Resource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
package cn.org.alan.exam.controller; // 声明该类所在的包名,用于在项目中对类进行分类组织,使代码结构更清晰,表明此控制器类属于该特定的包,便于区分不同功能模块的代码
import cn.org.alan.exam.common.group.CertificateGroup; // 引入CertificateGroup类可能用于对证书相关操作进行分组验证比如不同的证书添加、修改等操作按照特定规则进行分组便于灵活控制数据验证逻辑
import cn.org.alan.exam.common.result.Result; // 引入Result类通常用于统一封装业务操作后的结果信息包含操作是否成功以及可能需要返回的数据等内容以一致的格式返回给调用者如前端
import cn.org.alan.exam.model.entity.Certificate; // 引入Certificate类这大概率是代表证书实体的数据模型类对应数据库中存储证书相关信息的表结构用于在业务逻辑中操作证书的实际数据
import cn.org.alan.exam.model.form.CertificateForm; // 引入CertificateForm类是用于接收前端传递过来的与证书相关操作如添加、修改证书的参数数据模型符合前端提交的相应业务数据格式要求
import cn.org.alan.exam.model.vo.certificate.MyCertificateVO; // 引入MyCertificateVO类应该是视图对象Value ObjectVO用于向客户端展示特定格式的已获证书相关信息通常是经过处理、整合后适合展示的数据形式
import cn.org.alan.exam.service.ICertificateService; // 引入ICertificateService接口定义了与证书管理相关的一系列业务方法例如证书的添加、查询、修改、删除等操作的逻辑抽象具体的实现由对应的服务类来完成该控制器类会依赖这个接口来调用相应功能
import com.baomidou.mybatisplus.core.metadata.IPage; // 引入IPage类这是MyBatis Plus框架提供的用于表示分页数据的类型在进行分页查询相关业务逻辑时用于承载分页后的证书数据等信息
import jakarta.annotation.Resource; // 用于进行资源注入的注解在这里的作用是将实现了ICertificateService接口的具体实例注入到当前的CertificateController类中使得类中的方法可以方便地调用对应的服务方法实现具体业务逻辑
import org.springframework.security.access.prepost.PreAuthorize; // 用于在方法级别进行权限控制的注解,根据配置的权限表达式判断当前用户是否有相应权限来访问对应的方法,以此保证系统资源只能被授权用户访问,增强系统的安全性和权限管理
import org.springframework.validation.annotation.Validated; // 结合具体的分组类像这里的CertificateGroup.CertificateInsertGroup.class等可以对传入方法的参数进行分组验证确保接收到的参数数据符合特定业务规则和验证要求保证数据的合法性和准确性
import org.springframework.web.bind.annotation.*; // 通配符导入包含了众多Spring Web相关的注解例如用于定义请求处理方法的不同请求方式注解如@GetMapping、@PostMapping等以及处理请求路径、请求参数等相关的注解方便在控制器类中构建API接口
/**
*
*
*
*
* @author zsx
* @since 2024-04-1
*/
@RestController
@RequestMapping("/api/certificate")
public class CertificateController {
@RestController // 这是Spring框架提供的复合注解兼具@Controller和@ResponseBody的功能。意味着这个类是Spring MVC中的控制器类并且类中方法的返回值默认会直接以JSON等格式响应给客户端无需额外配置视图解析相关操作
@RequestMapping("/api/certificate") // 用于给整个控制器类下的所有请求处理方法设置一个公共的请求路径前缀表明此类中所有处理请求的方法对应的URL路径都将以/api/certificate开头便于统一管理和组织与证书管理相关的一组API接口路径
public class CertificateController { // 定义一个公共的类类名为CertificateController作为证书管理相关业务逻辑的处理中心对外提供各种证书管理相关的接口接收和处理对应的HTTP请求
@Resource
private ICertificateService iCertificateService;
@Resource // 使用@Resource注解来进行依赖注入目的是让Spring容器查找并注入一个实现了ICertificateService接口的实例到当前类中这样后续的方法就能方便地调用该服务实例所提供的各种证书管理相关的业务方法
private ICertificateService iCertificateService; // 声明一个私有成员变量iCertificateService其类型为ICertificateService接口类型通过依赖注入后它将指向对应的服务实现类实例用于在控制器方法中调用具体的证书管理相关业务逻辑方法
/**
*
* @param certificateForm
* @return
* @param certificateForm CertificateForm
* @return Result
*/
@PostMapping
@PreAuthorize("hasAnyAuthority('role_admin')")
@PostMapping // 该注解表明这个方法用于处理HTTP POST请求其请求路径就是类级别定义的基础路径/api/certificate因为这里没有额外指定路径POST请求常用于向服务器提交数据在此处符合向服务器提交证书添加相关信息的场景
@PreAuthorize("hasAnyAuthority('role_admin')") // 此注解进行权限控制,只有拥有"role_admin"权限的用户才能访问这个方法,确保只有管理员角色的用户可以执行添加证书的操作,增强了系统对证书添加功能的权限管理
public Result<String> addCertificate(@RequestBody @Validated(CertificateGroup.CertificateInsertGroup.class)
CertificateForm certificateForm) {
//从token获取用户id放入创建人id属性
return iCertificateService.addCertificate(certificateForm);
CertificateForm certificateForm) { // @RequestBody表示从请求的正文中获取数据并将其转换为CertificateForm类型的对象@Validated结合指定的分组类CertificateGroup.CertificateInsertGroup.class对这个转换后的对象进行数据合法性验证只有验证通过的数据才会进入方法内部进行后续的添加证书业务逻辑处理。这里验证可能涉及到证书必填字段是否填写、格式是否正确等方面
//从token获取用户id放入创建人id属性这里应该是在具体的业务逻辑中通过解析用户登录的token获取当前操作的用户ID并将其设置到证书的创建人ID属性上以便记录证书是由谁创建的后续便于追溯等操作
return iCertificateService.addCertificate(certificateForm); // 方法体内部直接调用通过依赖注入获取的iCertificateService接口实例的addCertificate方法并将经过验证的certificateForm参数传递进去由服务层去具体实现添加证书的详细业务逻辑比如将证书信息保存到数据库中最后将服务层返回的包含添加结果等信息的Result对象直接返回给客户端
}
/**
*
* @param pageNum
* @param pageSize
* @param certificateName
* @param certificationUnit
* @return
* @param pageNum pageNum1
* @param pageSize 1010
* @param certificateName
* @param certificationUnit
* @return ResultIPage<Certificate>IPageResult
*/
@GetMapping("/paging")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
public Result<IPage<Certificate>> pagingCertificate(@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "certificateName", required = false) String certificateName,
@RequestParam(value = "certificationUnit", required = false) String certificationUnit) {
return iCertificateService.pagingCertificate(pageNum, pageSize, certificateName, certificationUnit);
@GetMapping("/paging") // 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/certificate基础上添加/paging即/api/certificate/pagingGET请求常用于获取资源这里用于获取分页后的证书信息资源的操作符合GET请求的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')") // 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,意味着教师和管理员角色的用户可以执行分页查询证书的操作,保障了该功能的权限安全性
public Result<IPage<Certificate>> pagingCertificate(@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum, // 通过@RequestParam注解获取请求中的pageNum参数设置其为可选参数required = false若客户端未传入则使用默认值1方便客户端灵活指定要查询的页码
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize, // 同样通过@RequestParam获取pageSize参数也是可选参数默认值为10用于客户端指定每页显示的记录数量
@RequestParam(value = "certificateName", required = false) String certificateName, // 获取certificateName参数可选参数用于根据证书名称进行筛选查询
@RequestParam(value = "certificationUnit", required = false) String certificationUnit) { // 获取certificationUnit参数同样是可选参数用于按照认证单位进行筛选查询
return iCertificateService.pagingCertificate(pageNum, pageSize, certificateName, certificationUnit); // 调用通过依赖注入获取的iCertificateService接口实例的pagingCertificate方法由服务层去具体实现根据传入的页码、每页记录数以及筛选条件等信息进行分页查询证书的详细业务逻辑最后将查询结果封装到Result对象中返回给客户端
}
/**
*
* @param id id
* @param certificateForm
* @return
* @param id idID
* @param certificateForm CertificateForm
* @return Result
*/
@PutMapping("/{id}")
@PreAuthorize("hasAnyAuthority('role_admin')")
public Result<String> updateCertificate(@PathVariable("id") Integer id,
@RequestBody CertificateForm certificateForm) {
certificateForm.setId(id);
return iCertificateService.updateCertificate(certificateForm);
@PutMapping("/{id}") // 此注解指定这个方法用于处理HTTP PUT请求其请求路径是在类的基础路径/api/certificate基础上添加/{id},其中{id}是路径变量用于接收要修改的证书的IDPUT请求常用于更新资源这里符合对指定ID的证书进行修改更新的操作场景
@PreAuthorize("hasAnyAuthority('role_admin')") // 进行权限控制,只有拥有"role_admin"权限的用户才能访问这个方法,确保只有管理员角色的用户可以执行修改证书的操作,保障了证书修改功能的权限安全性
public Result<String> updateCertificate(@PathVariable("id") Integer id, // 通过@PathVariable注解获取路径中传入的证书ID参数将其赋值给变量id以便在后续业务逻辑中准确找到对应的证书记录
@RequestBody CertificateForm certificateForm) { // @RequestBody表示从请求正文中获取数据并转换为CertificateForm类型的对象用于接收前端传来的修改证书的相关信息
certificateForm.setId(id); // 将获取到的证书ID设置到certificateForm对象中确保在服务层进行修改操作时能准确知道是对哪个证书进行修改将前端传来的更新信息与对应的证书记录关联起来
return iCertificateService.updateCertificate(certificateForm); // 调用iCertificateService接口的updateCertificate方法由服务层去具体实现根据传入的更新信息对指定ID的证书进行修改的详细业务逻辑比如更新数据库中对应证书记录的字段值等最后将操作结果封装到Result对象中返回给客户端
}
/**
*
* @param id id
* @return
* @param id idID
* @return Result
*/
@DeleteMapping("/delete/{id}")
@PreAuthorize("hasAnyAuthority('role_admin')")
public Result<String> deleteCertificate(@PathVariable("id") Integer id) {
return iCertificateService.deleteCertificate(id);
@DeleteMapping("/delete/{id}") // 该注解表明这个方法用于处理HTTP DELETE请求其请求路径是在类的基础路径/api/certificate基础上添加/delete/{id},其中{id}是路径变量用于接收要删除的证书的IDDELETE请求常用于删除资源这里符合删除指定证书的操作场景
@PreAuthorize("hasAnyAuthority('role_admin')") // 进行权限控制,只有拥有"role_admin"权限的用户才能访问这个方法,确保只有管理员角色的用户可以执行删除证书的操作,保障了证书删除功能的权限安全性
public Result<String> deleteCertificate(@PathVariable("id") Integer id) { // 通过@PathVariable注解获取路径中传入的证书ID参数将其赋值给变量id以便在后续业务逻辑中准确找到对应的证书记录进行删除操作
return iCertificateService.deleteCertificate(id); // 调用iCertificateService接口的deleteCertificate方法由服务层去具体实现删除指定ID的证书的详细业务逻辑比如从数据库中移除对应的证书记录等最后将操作结果封装到Result对象中返回给客户端
}
/**
*
* @param pageNum
* @param pageSize
* @return
* @param pageNum1
* @param pageSize10
* @return ResultIPage<MyCertificateVO>IPageResult
*/
@GetMapping("/paging/my")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
public Result<IPage<MyCertificateVO>> getMyCertificate(@RequestParam(value = "pageNum",required = false, defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize",required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "examName", required = false) String examName){
return iCertificateService.getMyCertificatePaging(pageNum, pageSize,examName);
@GetMapping("/paging/my") // 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/certificate基础上添加/paging/my即/api/certificate/paging/my用于获取分页后的已获证书相关信息符合GET请求获取资源的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')") // 进行权限控制,拥有"role_teacher"、"role_admin"或者"role_student"权限的用户都可以访问这个方法,意味着教师、管理员和学生角色的用户都有权利进行分页查询已获证书的操作
public Result<IPage<MyCertificateVO>> getMyCertificate(@RequestParam(value = "pageNum",required = false, defaultValue = "1") Integer pageNum, // 获取请求中的pageNum参数为可选参数默认值为1用于指定页码
@RequestParam(value = "pageSize",required = false, defaultValue = "10") Integer pageSize, // 获取pageSize参数也是可选参数默认值为10用于指定每页记录数
@RequestParam(value = "examName", required = false) String examName){ // 获取examName参数可选参数可能用于根据考试名称等相关因素来筛选已获证书信息具体筛选逻辑由服务层实现
return iCertificateService.getMyCertificatePaging(pageNum, pageSize,examName); // 调用iCertificateService接口的getMyCertificatePaging方法由服务层去具体实现根据传入的页码、每页记录数以及筛选条件等信息进行分页查询已获证书的详细业务逻辑最后将查询结果封装到Result对象中返回给客户端
}
}
}

@ -1,187 +1,121 @@
package cn.org.alan.exam.controller;
import cn.org.alan.exam.common.result.Result;
import cn.org.alan.exam.model.form.exam.ExamAddForm;
import cn.org.alan.exam.model.form.exam.ExamUpdateForm;
import cn.org.alan.exam.model.form.examquanswer.ExamQuAnswerAddForm;
import cn.org.alan.exam.model.vo.exam.*;
import cn.org.alan.exam.service.IExamService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import jakarta.annotation.Resource;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
package cn.org.alan.exam.controller; // 声明该类所在的包名,用于在项目中对类进行分类组织,清晰地表明此控制器类属于特定的功能模块,方便代码的管理和维护
import cn.org.alan.exam.common.result.Result; // 引入Result类通常用于统一封装业务操作后的结果信息包含操作是否成功的标识以及可能需要返回的数据等内容以标准的格式返回给调用者比如前端应用
import cn.org.alan.exam.model.form.exam.ExamAddForm; // 引入ExamAddForm类这是一个数据模型类用于承载创建考试时前端传递过来的相关信息例如考试名称、考试时间、考试科目等创建考试所需填写的各项内容
import cn.org.alan.exam.model.form.exam.ExamUpdateForm; // 引入ExamUpdateForm类同样是数据模型类用于接收修改考试时前端传来的包含更新后考试相关信息的数据比如修改后的考试时长、考试范围等信息
import cn.org.alan.exam.model.form.examquanswer.ExamQuAnswerAddForm; // 引入ExamQuAnswerAddForm类大概率是用于接收添加答案相关操作时前端传递过来的数据结构比如针对具体题目填写的答案内容等信息
import cn.org.alan.exam.model.vo.exam.*; // 通配符导入exam包下的所有视图对象Value ObjectVO这些VO类通常用于向客户端展示特定格式的与考试相关的数据经过了业务逻辑层的处理和整合方便前端展示和使用
import cn.org.alan.exam.service.IExamService; // 引入IExamService接口定义了与考试管理相关的一系列业务方法涵盖考试的创建、修改、查询、交卷等各种操作的逻辑抽象具体的实现由对应的服务类来完成本控制器类依赖此接口调用相应功能
import com.baomidou.mybatisplus.core.metadata.IPage; // 引入IPage类这是MyBatis Plus框架提供的用于表示分页数据的类型在涉及分页查询考试相关信息的业务逻辑中用于承载分页后的考试数据等内容
import jakarta.annotation.Resource; // 用于进行资源注入的注解作用是将实现了IExamService接口的具体实例注入到当前的ExamController类中使得类中的方法可以便捷地调用对应的服务方法来实现具体的考试管理业务逻辑
import jakarta.validation.constraints.NotBlank; // 引入验证约束注解用于确保对应参数的值不为空不仅不能为null还不能是空白字符串等情况在这里用于对相关参数进行数据合法性验证
import jakarta.validation.constraints.NotNull; // 也是验证约束注解用于保证参数的值不能为空即不能为null用于对参数进行基本的非空验证确保业务逻辑处理时有合理的数据输入
import jakarta.validation.constraints.Pattern; // 此注解用于按照指定的正则表达式模式对参数值进行格式验证,确保传入的参数符合特定的格式要求,增强数据的合法性和规范性
import org.springframework.security.access.prepost.PreAuthorize; // 用于在方法级别进行权限控制的注解,依据配置的权限表达式判断当前用户是否具备相应权限来访问对应的方法,以此保障系统资源只能被授权用户操作,增强系统的安全性和权限管理
import org.springframework.validation.annotation.Validated; // 结合具体的验证规则(可以是默认规则或者像上面引入的分组验证等情况)对传入方法的参数进行数据合法性验证,保证接收到的参数符合业务要求,避免非法数据进入业务逻辑处理流程
import org.springframework.web.bind.annotation.*; // 通配符导入包含了众多Spring Web相关的注解像用于定义请求处理方法的不同请求方式注解如@GetMapping、@PostMapping等以及处理请求路径、请求参数等相关的注解便于在控制器类中构建各种API接口
import java.util.List; // 引入Java标准库中的List接口用于表示列表类型的数据结构在需要返回多个同类型元素如多个题目汇总信息等情况时会用到
/**
*
*
*
* @author Alan
* @since 2024-03-21
*/
@RestController
@RequestMapping("/api/exams")
public class ExamController {
@RestController // 这是Spring框架提供的复合注解兼具@Controller和@ResponseBody的功能。意味着这个类是Spring MVC中的控制器类并且类中方法的返回值默认会直接以JSON等格式响应给客户端无需额外配置视图解析相关操作
@RequestMapping("/api/exams") // 用于给整个控制器类下的所有请求处理方法设置一个公共的请求路径前缀表明此类中所有处理请求的方法对应的URL路径都将以/api/exams开头便于统一管理和组织与考试管理相关的一组API接口路径
public class ExamController { // 定义一个公共的类类名为ExamController作为考试管理相关业务逻辑的处理中心对外提供各类考试管理相关的接口接收并处理对应的HTTP请求
@Resource
private IExamService examService;
@Resource // 使用@Resource注解来进行依赖注入让Spring容器查找并注入一个实现了IExamService接口的实例到当前类中方便后续的方法调用该服务实例所提供的各种考试管理相关业务方法
private IExamService examService; // 声明一个私有成员变量examService其类型为IExamService接口类型通过依赖注入后它将指向对应的服务实现类实例用于在控制器方法中调用具体的考试管理相关业务逻辑方法
/**
*
* @param examAddForm
* @return
* @param examAddFormExamAddForm
* @return ResultResult
*/
@PostMapping
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
public Result<String> createExam(@Validated @RequestBody ExamAddForm examAddForm) {
return examService.createExam(examAddForm);
@PostMapping // 该注解表明这个方法用于处理HTTP POST请求其请求路径就是类级别定义的基础路径/api/exams因为这里没有额外指定路径POST请求常用于向服务器提交数据在此处符合向服务器提交创建考试相关信息的场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')") // 此注解进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,确保只有教师或管理员角色的用户可以执行创建考试的操作,加强了对考试创建功能的权限管理
public Result<String> createExam(@Validated @RequestBody ExamAddForm examAddForm) { // @Validated注解结合@RequestBody注解@RequestBody表示从请求的正文中获取数据并将其转换为ExamAddForm类型的对象@Validated用于对这个转换后的对象依据相关规则进行数据合法性验证只有验证通过的数据才会进入方法内部进行后续的创建考试业务逻辑处理
return examService.createExam(examAddForm); // 方法体内部直接调用通过依赖注入获取的examService接口实例的createExam方法并将经过验证的examAddForm参数传递进去由服务层去具体实现创建考试的详细业务逻辑比如将考试信息保存到数据库中最后将服务层返回的包含创建结果等信息的Result对象直接返回给客户端
}
/**
*
* @param examId
* @return
* @param examIdID便@NotNullID
* @return Result
*/
@GetMapping("/start")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
public Result<String> startExam(@RequestParam("examId") @NotNull Integer examId) {
return examService.startExam(examId);
@GetMapping("/start") // 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/exams基础上添加/start即/api/exams/startGET请求常用于获取资源或触发某个操作这里用于触发开始考试这个操作符合GET请求的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')") // 进行权限控制,拥有"role_teacher"、"role_admin"或者"role_student"权限的用户都能访问这个方法,意味着教师、管理员和学生角色的用户都有权利执行开始考试的操作
public Result<String> startExam(@RequestParam("examId") @NotNull Integer examId) { // 通过@RequestParam注解获取请求中名为"examId"的参数,并通过@NotNull注解确保该参数不能为空将获取到的考试ID传递给服务层以便服务层准确找到对应的考试记录来执行开始考试的业务逻辑
return examService.startExam(examId); // 调用通过依赖注入获取的examService接口实例的startExam方法由服务层去具体实现开始考试的详细业务逻辑比如更新考试状态为正在进行等操作最后将操作结果封装到Result对象中返回给客户端
}
/**
*
* @param examUpdateForm
* @param id
* @return
* @param examUpdateFormExamUpdateForm
* @param idID@NotNull
* @return Result
*/
@PutMapping("/{id}")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
public Result<String> updateExam(@Validated @RequestBody ExamUpdateForm examUpdateForm, @PathVariable("id") @NotNull Integer id) {
return examService.updateExam(examUpdateForm,id);
@PutMapping("/{id}") // 此注解指定这个方法用于处理HTTP PUT请求其请求路径是在类的基础路径/api/exams基础上添加/{id},其中{id}是路径变量用于接收要修改的考试的IDPUT请求常用于更新资源这里符合对指定ID的考试进行修改更新的操作场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')") // 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,确保只有教师或管理员角色的用户可以执行修改考试的操作,保障了考试修改功能的权限安全性
public Result<String> updateExam(@Validated @RequestBody ExamUpdateForm examUpdateForm, @PathVariable("id") @NotNull Integer id) { // @Validated结合@RequestBody对examUpdateForm参数进行数据合法性验证及从请求正文获取数据转换为对应对象@PathVariable注解用于获取路径中的考试ID参数并通过@NotNull保证其非空然后将这些信息传递给服务层进行修改考试的业务逻辑处理
return examService.updateExam(examUpdateForm,id); // 调用examService接口的updateExam方法由服务层去具体实现根据传入的更新信息对指定ID的考试进行修改的详细业务逻辑比如更新数据库中对应考试记录的字段值等最后将操作结果封装到Result对象中返回给客户端
}
/**
*
* @param ids
* @return
* @param ids@Pattern(regexp = "^\\d+(,\\d+)*$|^\\d+$")ID便
* @return Result
*/
@DeleteMapping("/{ids}")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
public Result<String> deleteExam(@PathVariable("ids") @Pattern(regexp = "^\\d+(,\\d+)*$|^\\d+$") String ids) {
return examService.deleteExam(ids);
@DeleteMapping("/{ids}") // 该注解表明这个方法用于处理HTTP DELETE请求其请求路径是在类的基础路径/api/exams基础上添加/{ids},其中{ids}是路径变量用于接收要删除的考试的ID可以是多个用逗号隔开DELETE请求常用于删除资源这里符合删除指定考试的操作场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')") // 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,确保只有教师或管理员角色的用户可以执行删除考试的操作,保障了考试删除功能的权限安全性
public Result<String> deleteExam(@PathVariable("ids") @Pattern(regexp = "^\\d+(,\\d+)*$|^\\d+$") String ids) { // 通过@PathVariable注解获取路径中传入的符合格式要求的考试ID字符串参数将其传递给服务层以便服务层依据这些ID准确找到并删除对应的考试记录
return examService.deleteExam(ids); // 调用examService接口的deleteExam方法由服务层去具体实现删除指定ID的考试的详细业务逻辑比如从数据库中移除对应的考试记录等最后将操作结果封装到Result对象中返回给客户端
}
/**
*
* @param pageNum
* @param pageSize
* @param title
* @return
* @param pageNumpageNum11
* @param pageSize1010
* @param title
* @return ResultIPage<ExamVO>IPageResult
*/
@GetMapping("/paging")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
public Result<IPage<ExamVO>> getPagingExam(@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "title", required = false) String title) {
return examService.getPagingExam(pageNum, pageSize, title);
@GetMapping("/paging") // 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/exams基础上添加/paging即/api/exams/pagingGET请求常用于获取资源这里用于获取分页后的考试列表信息资源的操作符合GET请求的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')") // 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,意味着教师和管理员角色的用户可以执行分页查找考试列表的操作,保障了该功能的权限安全性
public Result<IPage<ExamVO>> getPagingExam(@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum, // 通过@RequestParam注解获取请求中的pageNum参数设置其为可选参数required = false若客户端未传入则使用默认值1方便客户端灵活指定要查询的页码
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize, // 同样通过@RequestParam获取pageSize参数也是可选参数默认值为10用于客户端指定每页显示的记录数量
@RequestParam(value = "title", required = false) String title) { // 获取title参数可选参数用于根据考试标题进行筛选查询
return examService.getPagingExam(pageNum, pageSize, title); // 调用通过依赖注入获取的examService接口实例的getPagingExam方法由服务层去具体实现根据传入的页码、每页记录数以及筛选条件等信息进行分页查询考试列表的详细业务逻辑最后将查询结果封装到Result对象中返回给客户端
}
/**
* id
* @param examId
* @return
*/
@GetMapping("/question/list/{examId}")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
public Result<ExamQuestionListVO> getQuestionList(@PathVariable("examId") @NotBlank Integer examId) {
return examService.getQuestionList(examId);
}
/**
*
* @param examId
* @param questionId
* @return
*/
@GetMapping("/question/single")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
public Result<ExamQuDetailVO> getQuestionSingle(@RequestParam("examId") Integer examId,
@RequestParam("questionId") Integer questionId) {
return examService.getQuestionSingle(examId, questionId);
}
/**
*
* @param examId
* @return
*/
@GetMapping("/collect/{id}")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
public Result<List<ExamQuCollectVO>> getCollect(@PathVariable("id") @NotNull Integer examId) {
return examService.getCollect(examId);
}
/**
*
* @param examId
* @return
*/
@GetMapping("/detail")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
public Result<ExamDetailVO> getDetail(@RequestParam("examId") @NotBlank Integer examId) {
return examService.getDetail(examId);
}
/**
*
* @param pageNum
* @param pageSize
* @return
*/
@GetMapping("/grade")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
public Result<IPage<ExamGradeListVO>> getGradeExamList(@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "title", required = false) String title) {
return examService.getGradeExamList(pageNum,pageSize,title);
}
/**
*
* @param examId
* @return
* @param examId@NotBlankIDIDIDID
* @return ResultIDID
*/
@PutMapping("/cheat/{examId}")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
public Result<Integer> addCheat(@PathVariable("examId") @NotNull Integer examId) {
return examService.addCheat(examId);
@GetMapping("/question/list/{examId}") // 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/exams基础上添加/question/list/{examId},其中{examId}是路径变量用于接收要获取题目ID列表的考试的IDGET请求常用于获取资源这里用于获取特定考试的题目ID列表资源符合GET请求的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')") // 进行权限控制,拥有"role_teacher"、"role_admin"或者"role_student"权限的用户都可以访问这个方法意味着教师、管理员和学生角色的用户都有权利执行获取考试题目ID列表的操作
public Result<ExamQuestionListVO> getQuestionList(@PathVariable("examId") @NotBlank Integer examId) { // 通过@PathVariable注解获取路径中传入的经过验证的考试ID参数将其传递给服务层以便服务层依据该考试ID查找并获取对应的题目ID列表信息用于后续返回给客户端
return examService.getQuestionList(examId); // 调用examService接口的getQuestionList方法由服务层去具体实现根据传入的考试ID查找并获取其对应题目ID列表的详细业务逻辑最后将获取到的题目ID列表数据封装到Result对象中返回给客户端
}
}
/**
*
* @param examQuAnswerForm
* @return
*/
@PostMapping("/full-answer")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
public Result<String> addAnswer(@Validated @RequestBody ExamQuAnswerAddForm examQuAnswerForm) {
return examService.addAnswer(examQuAnswerForm);
}
/**
*
* @param examId
* @return
*/
@GetMapping(value = "/hand-exam/{examId}")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
public Result<ExamQuDetailVO> handleExam(@PathVariable("examId") @NotNull Integer examId) {
return examService.handExam(examId);
}
}
/**
*
* @param examIdID**/

@ -1,107 +1,171 @@
package cn.org.alan.exam.controller;
// 声明该类所在的包名,用于在项目中对类进行分类组织,使代码结构更清晰,表明此控制器类属于特定的“刷题管理”功能模块相关的包
import cn.org.alan.exam.common.result.Result;
// 引入Result类通常用于统一封装业务操作后的结果信息包含操作是否成功的标识以及可能需要返回的数据等内容方便以一致的格式返回给调用者如前端
import cn.org.alan.exam.model.form.ExerciseFillAnswerFrom;
// 引入ExerciseFillAnswerFrom类这是一个数据模型类用于承载与填充答案相关操作时前端传递过来的数据结构比如填写的具体答案内容等信息
import cn.org.alan.exam.model.vo.QuestionVO;
// 引入QuestionVO类这大概率是视图对象Value ObjectVO用于向客户端展示特定格式的试题相关信息经过业务逻辑处理后以适合展示的形式传递给前端
import cn.org.alan.exam.model.vo.exercise.AnswerInfoVO;
// 引入AnswerInfoVO类同样是视图对象类用于展示用户回答详情相关的信息按照一定的格式组织数据方便前端展示和使用
import cn.org.alan.exam.model.vo.exercise.ExerciseRepoVO;
// 引入ExerciseRepoVO类也是视图对象类可能用于向客户端展示可刷题库相关信息比如包含题库名称、简介等展示用的数据
import cn.org.alan.exam.model.vo.exercise.QuestionSheetVO;
// 引入QuestionSheetVO类应该是用于向客户端展示试题相关信息列表的视图对象类例如以列表形式展示的试题基本信息等
import cn.org.alan.exam.service.IExerciseRecordService;
// 引入IExerciseRecordService接口定义了与刷题记录相关的一系列业务方法例如获取试题、填充答案、获取单题详情等操作的逻辑抽象具体的实现由对应的服务类来完成该控制器类会依赖这个接口来调用相应功能
import cn.org.alan.exam.service.IRepoService;
// 引入IRepoService接口定义了与题库相关的业务方法像获取可刷题库列表等操作的逻辑抽象控制器类通过此接口调用对应的服务方法实现相关功能
import com.baomidou.mybatisplus.core.metadata.IPage;
// 引入IPage类这是MyBatis Plus框架提供的用于表示分页数据的类型在涉及分页查询可刷题库等业务逻辑时用于承载分页后的相关数据
import jakarta.annotation.Nullable;
// 引入Nullable注解用于标记对应的参数可以为null值在参数验证等场景中表明该参数允许为空的情况
import jakarta.annotation.Resource;
// 用于进行资源注入的注解在这里的作用是将实现了IExerciseRecordService和IRepoService接口的具体实例注入到当前的ExerciseController类中使得类中的方法可以方便地调用对应的服务方法实现具体业务逻辑
import jakarta.validation.constraints.Max;
// 引入验证约束注解,用于确保对应参数的值不超过指定的最大值,在这里用于对相关参数进行数据合法性验证,限定其取值上限
import jakarta.validation.constraints.Min;
// 引入验证约束注解,用于保证对应参数的值不小于指定的最小值,用于对参数进行取值下限的合法性验证
import org.springframework.security.access.prepost.PreAuthorize;
// 用于在方法级别进行权限控制的注解,根据配置的权限表达式判断当前用户是否有相应权限来访问对应的方法,以此保证系统资源只能被授权用户访问,增强系统的安全性和权限管理
import org.springframework.validation.annotation.Validated;
// 结合具体的验证规则(可以是默认规则或者自定义规则)对传入方法的参数进行数据合法性验证,保证接收到的参数符合业务要求,避免非法数据进入业务逻辑处理流程
import org.springframework.web.bind.annotation.*;
// 通配符导入包含了众多Spring Web相关的注解例如用于定义请求处理方法的不同请求方式注解如@GetMapping、@PostMapping等以及处理请求路径、请求参数等相关的注解方便在控制器类中构建API接口
import java.util.List;
// 引入Java标准库中的List接口用于表示列表类型的数据结构在需要返回多个同类型元素如多个试题信息等情况时会用到
/**
*
*
*
* @Author Alan
* @Version
* @Date 2024/3/25 11:21 AM
*/
@RestController
// 这是Spring框架提供的复合注解兼具@Controller和@ResponseBody的功能。意味着这个类是Spring MVC中的控制器类并且类中方法的返回值默认会直接以JSON等格式响应给客户端无需额外配置视图解析相关操作
@RequestMapping("/api/exercises")
// 用于给整个控制器类下的所有请求处理方法设置一个公共的请求路径前缀表明此类中所有处理请求的方法对应的URL路径都将以/api/exercises开头便于统一管理和组织与刷题管理相关的一组API接口路径
@Validated
// 对整个类中的方法参数启用验证机制,确保传入方法的参数符合相应的验证规则(结合具体的约束注解来实现),保证数据合法性
public class ExerciseController {
@Resource
private IExerciseRecordService iExerciseRecordService;
// 使用@Resource注解来进行依赖注入让Spring容器查找并注入一个实现了IExerciseRecordService接口的实例到当前类中方便后续的方法调用该服务实例所提供的各种刷题记录相关业务方法
@Resource
private IRepoService iRepoService;
// 同样使用@Resource注解进行依赖注入将实现了IRepoService接口的实例注入到当前类中使得类中的方法能够调用对应的题库相关业务方法
/**
* Id
*
* @param repoId Id
* @param quType
* @return
* @param repoId IdIDID
* @param quType @RequestParamrequired = false@Min@Max14@NullablenullID
* @return ResultIDID
*/
@GetMapping("/{repoId}")
// 该注解表明这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/exercises基础上添加/{repoId},其中{repoId}是路径变量用于接收要获取试题ID列表的题库的IDGET请求常用于获取资源这里用于获取特定题库中的试题ID列表资源符合GET请求的使用场景
@PreAuthorize("hasAnyAuthority('role_student','role_teacher','role_admin')")
// 进行权限控制,拥有"role_student"、"role_teacher"或者"role_admin"权限的用户都可以访问这个方法意味着学生、教师和管理员角色的用户都有权利执行获取试题ID列表的操作
public Result<List<QuestionSheetVO>> getQuestion(@PathVariable("repoId") Integer repoId,
// 通过@PathVariable注解获取路径中传入的题库ID参数将其赋值给变量repoId以便在后续业务逻辑中准确找到对应的题库来获取试题ID列表
@Min(value = 1, message = "试题类型最小值应为1")
@Max(value = 4, message = "试题类型最大值应为4")
@Nullable
@RequestParam(value = "quType", required = false) Integer quType) {
// 通过@RequestParam注解获取请求中名为"quType"的参数可选参数若请求中未提供则为null并通过@Min、@Max和@Nullable注解对其进行取值范围及可空性的验证用于筛选特定类型的试题
return iExerciseRecordService.getQuestionSheet(repoId, quType);
// 调用通过依赖注入获取的iExerciseRecordService接口实例的getQuestionSheet方法由服务层去具体实现根据传入的题库ID和试题类型可选获取试题ID列表的详细业务逻辑最后将获取到的试题ID列表数据封装到Result对象中返回给客户端
}
/**
*
*
* @param exerciseFillAnswerFrom
* @return
* @param exerciseFillAnswerFrom ExerciseFillAnswerFrom
* @return Result
*/
@PostMapping("/fillAnswer")
// 表明这个方法用于处理HTTP POST请求其请求路径是在类的基础路径/api/exercises基础上添加/fillAnswer即/api/exercises/fillAnswerPOST请求常用于向服务器提交数据在此处符合向服务器提交填充答案相关信息的场景
@PreAuthorize("hasAnyAuthority('role_student','role_teacher','role_admin')")
// 进行权限控制,拥有"role_student"、"role_teacher"或者"role_admin"权限的用户都可以访问这个方法,意味着学生、教师和管理员角色的用户都有权利执行填充答案的操作
public Result<QuestionVO> fillAnswer(@RequestBody ExerciseFillAnswerFrom exerciseFillAnswerFrom) {
// @RequestBody表示从请求的正文中获取数据并将其转换为ExerciseFillAnswerFrom类型的对象以便获取前端提交的填充答案相关信息用于后续业务逻辑处理
return iExerciseRecordService.fillAnswer(exerciseFillAnswerFrom);
// 调用通过依赖注入获取的iExerciseRecordService接口实例的fillAnswer方法由服务层去具体实现填充答案以及获取相应试题信息可能经过处理比如判断答案正确性等的详细业务逻辑最后将获取到的试题信息数据封装到Result对象中返回给客户端
}
/**
*
*
* @param pageNum
* @param pageSize
* @param title
* @return
* @param pageNum pageNum11
* @param pageSize 1010
* @param title
* @return ResultIPage<ExerciseRepoVO>IPageResult
*/
@GetMapping("/getRepo")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/exercises基础上添加/getRepo即/api/exercises/getRepoGET请求常用于获取资源这里用于获取分页后的可刷题库列表信息资源的操作符合GET请求的使用场景
@PreAuthorize("hasAnyAuthority('role_student','role_teacher','role_admin')")
// 进行权限控制,拥有"role_student"、"role_teacher"或者"role_admin"权限的用户都可以访问这个方法,意味着学生、教师和管理员角色的用户都有权利执行分页获取可刷题库列表的操作
public Result<IPage<ExerciseRepoVO>> getRepo(
@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum,
// 通过@RequestParam注解获取请求中的pageNum参数设置其为可选参数required = false若客户端未传入则使用默认值1方便客户端灵活指定要查询的页码
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize,
// 同样通过@RequestParam获取pageSize参数也是可选参数默认值为10用于客户端指定每页显示的记录数量
@RequestParam(value = "title", required = false) String title) {
// 获取title参数可选参数用于根据题库名称进行筛选查询
return iRepoService.getRepo(pageNum, pageSize, title);
// 调用通过依赖注入获取的iRepoService接口实例的getRepo方法由服务层去具体实现根据传入的页码、每页记录数以及筛选条件等信息进行分页查询可刷题库列表的详细业务逻辑最后将查询结果封装到Result对象中返回给客户端
}
/**
*
* @param id id
* @return
* @param id idID
* @return Result
*/
@GetMapping("/question/{id}")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/exercises基础上添加/question/{id},其中{id}是路径变量用于接收要获取详情的试题的IDGET请求常用于获取资源这里用于获取特定试题的详情信息资源符合GET请求的使用场景
@PreAuthorize("hasAnyAuthority('role_student','role_teacher','role_admin')")
// 进行权限控制,拥有"role_student"、"role_teacher"或者"role_admin"权限的用户都可以访问这个方法,意味着学生、教师和管理员角色的用户都有权利执行获取单题详情的操作
public Result<QuestionVO> getSingle(@PathVariable("id")Integer id){
// 通过@PathVariable注解获取路径中传入的试题ID参数将其赋值给变量id以便在后续业务逻辑中准确找到对应的试题记录来获取其详情信息
return iExerciseRecordService.getSingle(id);
// 调用通过依赖注入获取的iExerciseRecordService接口实例的getSingle方法由服务层去具体实现根据传入的试题ID获取其详细信息不包含答案的详细业务逻辑最后将获取到的试题详情信息封装到Result对象中返回给客户端
}
/**
*
* @param
* @return
* @param repoId IdID
* @param quId idIDID
* @return Result
*/
@GetMapping("/answerInfo/{repoId}/{quId}")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/exercises基础上添加/answerInfo/{repoId}/{quId},其中{repoId}和{quId}是路径变量分别用于接收题库ID和试题IDGET请求常用于获取资源这里用于获取特定试题的用户回答详情资源符合GET请求的使用场景
@PreAuthorize("hasAnyAuthority('role_student','role_teacher','role_admin')")
// 进行权限控制,拥有"role_student"、"role_teacher"或者"role_admin"权限的用户都可以访问这个方法,意味着学生、教师和管理员角色的用户都有权利执行获取用户回答详情的操作
public Result<AnswerInfoVO> getAnswerInfo(@PathVariable("repoId")Integer repoId, @PathVariable("quId")Integer quId){
// 通过@PathVariable注解分别获取路径中传入的题库ID和试题ID参数将其赋值给变量repoId和quId以便在后续业务逻辑中准确找到对应的用户回答记录来获取其详细信息
return iExerciseRecordService.getAnswerInfo(repoId,quId);
// 调用通过依赖注入获取的iExerciseRecordService接口实例的getAnswerInfo方法由服务层去具体实现根据传入的题库ID和试题ID获取对应的用户回答详情的详细业务逻辑最后将获取到的用户回答详情信息封装到Result对象中返回给客户端
}
}
}

@ -1,100 +1,150 @@
package cn.org.alan.exam.controller;
// 声明该类所在的包名,用于在项目中对类进行分类组织,清晰表明此控制器类属于特定的功能模块,方便代码管理与维护,这里是和班级管理相关的控制器类所在的包
import cn.org.alan.exam.common.result.Result;
// 引入Result类通常用于统一封装业务操作后的结果信息包含操作是否成功的标识以及可能需要返回的数据等内容以便以标准格式返回给调用者比如前端应用
import cn.org.alan.exam.model.form.GradeForm;
// 引入GradeForm类这是一个数据模型类用于承载与班级相关操作如新增、修改班级时前端传递过来的班级信息数据例如班级名称、班级描述等内容
import cn.org.alan.exam.model.vo.GradeVO;
// 引入GradeVO类大概率是视图对象Value ObjectVO用于向客户端展示特定格式的班级相关信息经过业务逻辑层处理整合后以适合展示的数据形式传递给前端
import cn.org.alan.exam.service.IGradeService;
// 引入IGradeService接口定义了与班级管理相关的一系列业务方法像班级的查询、新增、修改、删除等操作的逻辑抽象具体实现由对应的服务类来完成本控制器类依赖此接口调用相应功能
import com.baomidou.mybatisplus.core.metadata.IPage;
// 引入IPage类这是MyBatis Plus框架提供的用于表示分页数据的类型在涉及分页查询班级相关信息的业务逻辑中用于承载分页后的班级数据等内容
import jakarta.annotation.Resource;
// 用于进行资源注入的注解作用是将实现了IGradeService接口的具体实例注入到当前的GradeController类中使得类中的方法可以便捷地调用对应的服务方法实现具体业务逻辑
import jakarta.validation.constraints.NotBlank;
// 引入验证约束注解用于确保对应参数的值不为空不仅不能为null还不能是空白字符串等情况在这里用于对相关参数进行数据合法性验证保证传入的数据符合业务要求
import jakarta.validation.constraints.NotNull;
// 也是验证约束注解用于保证参数的值不能为空即不能为null用于对参数进行基本的非空验证避免因空值导致业务逻辑出错
import org.springframework.security.access.prepost.PreAuthorize;
// 用于在方法级别进行权限控制的注解,依据配置的权限表达式判断当前用户是否具备相应权限来访问对应的方法,以此保障系统资源只能被授权用户操作,增强系统的安全性和权限管理
import org.springframework.validation.annotation.Validated;
// 结合具体的验证规则(可以是默认规则或者自定义规则等情况)对传入方法的参数进行数据合法性验证,保证接收到的参数符合业务要求,防止非法数据进入业务逻辑处理流程
import org.springframework.web.bind.annotation.*;
// 通配符导入包含了众多Spring Web相关的注解像用于定义请求处理方法的不同请求方式注解如@GetMapping、@PostMapping等以及处理请求路径、请求参数等相关的注解便于在控制器类中构建各种API接口
import java.util.List;
// 引入Java标准库中的List接口用于表示列表类型的数据结构在需要返回多个同类型元素如获取所有班级列表时返回多个班级信息时会用到
/**
*
* 退
*
* @author Alan
* @since 2024-03-21
*/
@RestController
// 这是Spring框架提供的复合注解兼具@Controller和@ResponseBody的功能。意味着这个类是Spring MVC中的控制器类并且类中方法的返回值默认会直接以JSON等格式响应给客户端无需额外配置视图解析相关操作
@RequestMapping("/api/grades")
// 用于给整个控制器类下的所有请求处理方法设置一个公共的请求路径前缀表明此类中所有处理请求的方法对应的URL路径都将以/api/grades开头便于统一管理和组织与班级管理相关的一组API接口路径
public class GradeController {
// 定义一个公共的类作为班级管理相关业务逻辑的处理中心对外提供各类班级管理相关的接口接收并处理对应的HTTP请求
@Resource
private IGradeService gradeService;
// 使用@Resource注解来进行依赖注入让Spring容器查找并注入一个实现了IGradeService接口的实例到当前类中后续的方法就能调用该服务实例所提供的各种班级管理相关业务方法
/**
*
* @param pageNum
* @param pageSize
* @param gradeName
* @return
* @param pageNumpageNum11
* @param pageSize1010
* @param gradeName
* @return ResultIPage<GradeVO>IPageResult
*/
@GetMapping("/paging")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/grades基础上添加/paging即/api/grades/pagingGET请求常用于获取资源这里用于获取分页后的班级列表信息资源的操作符合GET请求的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,意味着教师和管理员角色的用户可以执行分页查询班级的操作,保障了该功能的权限安全性
public Result<IPage<GradeVO>> getGrade(@RequestParam(value = "pageNum",required = false, defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize",required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "gradeName",required = false) String gradeName) {
// 通过@RequestParam注解获取请求中的pageNum参数设置其为可选参数required = false若客户端未传入则使用默认值1方便客户端灵活指定要查询的页码同样获取pageSize参数默认值为10获取gradeName参数用于筛选查询
return gradeService.getPaging(pageNum, pageSize, gradeName);
// 调用通过依赖注入获取的gradeService接口实例的getPaging方法由服务层去具体实现根据传入的页码、每页记录数以及筛选条件等信息进行分页查询班级的详细业务逻辑最后将查询结果封装到Result对象中返回给客户端
}
/**
*
* @param gradeForm
* @return
* @param gradeFormGradeForm
* @return ResultResult
*/
@PostMapping("/add")
// 该注解表明这个方法用于处理HTTP POST请求其请求路径是在类的基础路径/api/grades基础上添加/add即/api/grades/addPOST请求常用于向服务器提交数据在此处符合向服务器提交新增班级相关信息的场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 此注解进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,确保只有教师或管理员角色的用户可以执行新增班级的操作,加强了对班级新增功能的权限管理
public Result<String> addGrade(@Validated @RequestBody GradeForm gradeForm) {
// @Validated注解结合@RequestBody注解@RequestBody表示从请求的正文中获取数据并将其转换为GradeForm类型的对象@Validated用于对这个转换后的对象依据相关规则进行数据合法性验证只有验证通过的数据才会进入方法内部进行后续的新增班级业务逻辑处理
return gradeService.addGrade(gradeForm);
// 方法体内部直接调用通过依赖注入获取的gradeService接口实例的addGrade方法并将经过验证的gradeForm参数传递进去由服务层去具体实现新增班级的详细业务逻辑比如将班级信息保存到数据库中最后将服务层返回的包含新增结果等信息的Result对象直接返回给客户端
}
/**
*
* @param id
* @param gradeForm
* @return
* @param idID@NotNull
* @param gradeFormGradeForm
* @return Result
*/
@PutMapping("/update/{id}")
// 此注解指定这个方法用于处理HTTP PUT请求其请求路径是在类的基础路径/api/grades基础上添加/update/{id},其中{id}是路径变量用于接收要修改的班级的IDPUT请求常用于更新资源这里符合对指定ID的班级进行修改更新的操作场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,确保只有教师或管理员角色的用户可以执行修改班级的操作,保障了班级修改功能的权限安全性
public Result<String> updateGrade(@PathVariable("id") @NotNull Integer id,@Validated @RequestBody GradeForm gradeForm) {
// @PathVariable注解用于获取路径中的班级ID参数并通过@NotNull保证其非空@Validated结合@RequestBody对gradeForm参数进行数据合法性验证及从请求正文获取数据转换为对应对象然后将这些信息传递给服务层进行修改班级的业务逻辑处理
return gradeService.updateGrade(id, gradeForm);
// 调用gradeService接口的updateGrade方法由服务层去具体实现根据传入的更新信息对指定ID的班级进行修改的详细业务逻辑比如更新数据库中对应班级记录的字段值等最后将操作结果封装到Result对象中返回给客户端
}
/**
*
* @param id
* @return
* @param idID@NotNull
* @return Result
*/
@DeleteMapping("/delete/{id}")
// 该注解表明这个方法用于处理HTTP DELETE请求其请求路径是在类的基础路径/api/grades基础上添加/delete/{id},其中{id}是路径变量用于接收要删除的班级的IDDELETE请求常用于删除资源这里符合删除指定班级的操作场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,确保只有教师或管理员角色的用户可以执行删除班级的操作,保障了班级删除功能的权限安全性
public Result<String> deleteGrade(@PathVariable("id") @NotNull Integer id) {
// 通过@PathVariable注解获取路径中传入的班级ID参数并通过@NotNull确保其非空将其传递给服务层以便服务层依据该ID准确找到并删除对应的班级记录
return gradeService.deleteGrade(id);
// 调用gradeService接口的deleteGrade方法由服务层去具体实现删除指定ID的班级的详细业务逻辑比如从数据库中移除对应的班级记录等最后将操作结果封装到Result对象中返回给客户端
}
/**
* 退
* @param ids
* @return
* @param ids@NotBlank退ID
* @return Result退
*/
@PatchMapping("/remove/{ids}")
// 此注解表明这个方法用于处理HTTP PATCH请求其请求路径是在类的基础路径/api/grades基础上添加/remove/{ids},其中{ids}是路径变量用于接收要退出班级的相关标识信息PATCH请求常用于对资源进行部分更新操作这里符合对班级成员退出班级这一操作的语义可理解为对班级成员关系这一资源的部分修改
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_studnet')")
// 进行权限控制,拥有"role_teacher"、"role_admin"或者"role_studnet"权限的用户都可以访问这个方法,意味着教师、管理员和学生角色的用户都有权利执行退出班级的操作
public Result<String> removeUserGrade(@PathVariable("ids") @NotBlank String ids) {
// 通过@PathVariable注解获取路径中传入的经过验证的班级相关标识参数将其传递给服务层以便服务层依据这些信息执行成员退出班级的业务逻辑
return gradeService.removeUserGrade(ids);
// 调用gradeService接口的removeUserGrade方法由服务层去具体实现根据传入的班级相关标识信息处理成员退出班级的详细业务逻辑比如更新数据库中班级成员关系表等操作最后将操作结果封装到Result对象中返回给客户端
}
/**
*
* @return
*/
@GetMapping("/list")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
public Result<List<GradeVO>> getAllGrade(){
return gradeService.getAllGrade();
}
}
/**
*
* @return Result
*/
@GetMapping("/list")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/grades基础上添加/list即/api/grades/listGET请求常用于获取资源这里用于获取所有班级列表资源的操作符合GET请求的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,意味着教师和管理员角色的用户可以执行获取所有班级列表的操作,保障了该功能的权限安全性
public Result<List<GradeVO>> getAllGrade(){
return gradeService.getAllGrade();
// 调用gradeService接口的getAllGrade方法由服务层去具体实现获取所有班级信息的详细业务逻辑比如从数据库中查询所有班级记录并转换为对应的视图对象列表等操作最后将获取到的班级列表数据封装到Result对象中返回给客户端
}
}

@ -1,91 +1,138 @@
package cn.org.alan.exam.controller;
// 声明该类所在的包名,用于在项目中对类进行分类组织,表明此控制器类属于特定的“公告管理”功能模块相关的包,方便代码管理和维护
import cn.org.alan.exam.common.result.Result;
// 引入Result类通常用于统一封装业务操作后的结果信息包含操作是否成功的标识以及可能需要返回的数据等内容以一致的格式返回给调用者如前端
import cn.org.alan.exam.model.form.NoticeForm;
// 引入NoticeForm类这是一个数据模型类用于承载添加、修改公告等操作时前端传递过来的与公告相关的信息例如公告标题、内容、发布时间等具体的数据内容
import cn.org.alan.exam.model.vo.NoticeVO;
// 引入NoticeVO类大概率是视图对象Value ObjectVO用于向客户端展示特定格式的公告相关信息经过业务逻辑处理后以适合展示的形式传递给前端
import cn.org.alan.exam.service.INoticeService;
// 引入INoticeService接口定义了与公告管理相关的一系列业务方法例如公告的添加、删除、修改、查询等操作的逻辑抽象具体的实现由对应的服务类来完成该控制器类会依赖这个接口来调用相应功能
import cn.org.alan.exam.util.JwtUtil;
// 引入JwtUtil类从名字推测它可能是用于处理JSON Web TokenJWT相关操作的工具类比如解析token获取用户信息、验证token有效性等不过在此控制器类中暂时未看到具体使用它的地方可能在服务层等其他地方会用到
import com.baomidou.mybatisplus.core.metadata.IPage;
// 引入IPage类这是MyBatis Plus框架提供的用于表示分页数据的类型在涉及分页查询公告相关信息的业务逻辑时用于承载分页后的公告数据等内容
import jakarta.annotation.Resource;
// 用于进行资源注入的注解在这里的作用是将实现了INoticeService接口的具体实例注入到当前的NoticeController类中使得类中的方法可以方便地调用对应的服务方法实现具体业务逻辑
import jakarta.servlet.http.HttpServletRequest;
// 引入HttpServletRequest类它代表了客户端发送过来的HTTP请求对象在一些需要获取请求相关信息的场景比如根据请求中的token获取用户权限等虽然此处未体现具体使用下会用到
import jakarta.validation.constraints.NotBlank;
// 引入验证约束注解用于确保对应参数的值不为空不仅不能为null还不能是空白字符串等情况在这里用于对相关参数进行数据合法性验证保证传入的数据符合业务要求
import org.springframework.security.access.prepost.PreAuthorize;
// 用于在方法级别进行权限控制的注解,根据配置的权限表达式判断当前用户是否有相应权限来访问对应的方法,以此保证系统资源只能被授权用户访问,增强系统的安全性和权限管理
import org.springframework.validation.annotation.Validated;
// 结合具体的验证规则(可以是默认规则或者自定义规则)对传入方法的参数进行数据合法性验证,保证接收到的参数符合业务要求,避免非法数据进入业务逻辑处理流程
import org.springframework.web.bind.annotation.*;
// 通配符导入包含了众多Spring Web相关的注解例如用于定义请求处理方法的不同请求方式注解如@GetMapping、@PostMapping等以及处理请求路径、请求参数等相关的注解方便在控制器类中构建API接口
/**
*
*
*
* @author Alan
* @since 2024-03-21
*/
@RestController
// 这是Spring框架提供的复合注解兼具@Controller和@ResponseBody的功能。意味着这个类是Spring MVC中的控制器类并且类中方法的返回值默认会直接以JSON等格式响应给客户端无需额外配置视图解析相关操作
@RequestMapping("/api/notices")
// 用于给整个控制器类下的所有请求处理方法设置一个公共的请求路径前缀表明此类中所有处理请求的方法对应的URL路径都将以/api/notices开头便于统一管理和组织与公告管理相关的一组API接口路径
public class NoticeController {
@Resource
// 使用@Resource注解来进行依赖注入让Spring容器查找并注入一个实现了INoticeService接口的实例到当前类中方便后续的方法调用该服务实例所提供的各种公告管理相关业务方法
private INoticeService noticeService;
/**
*
* @param noticeForm
* @return
* @param noticeFormNoticeForm
* @return Result
*/
@PostMapping
// 该注解表明这个方法用于处理HTTP POST请求其请求路径就是类级别定义的基础路径/api/notices因为这里没有额外指定路径POST请求常用于向服务器提交数据在此处符合向服务器提交添加公告相关信息的场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,确保只有教师或管理员角色的用户可以执行添加公告的操作,加强了对公告添加功能的权限管理
public Result<String> addNotice(@Validated @RequestBody NoticeForm noticeForm) {
// @Validated注解结合@RequestBody注解@RequestBody表示从请求的正文中获取数据并将其转换为NoticeForm类型的对象@Validated用于对这个转换后的对象依据相关规则进行数据合法性验证只有验证通过的数据才会进入方法内部进行后续的添加公告业务逻辑处理
return noticeService.addNotice( noticeForm);
// 方法体内部直接调用通过依赖注入获取的noticeService接口实例的addNotice方法并将经过验证的noticeForm参数传递进去由服务层去具体实现添加公告的详细业务逻辑比如将公告信息保存到数据库中最后将服务层返回的包含添加结果等信息的Result对象直接返回给客户端
}
/**
*
* @param ids
* @return
* @param ids@NotBlankIDID
* @return Result
*/
@DeleteMapping("/{ids}")
// 该注解表明这个方法用于处理HTTP DELETE请求其请求路径是在类的基础路径/api/notices基础上添加/{ids},其中{ids}是路径变量用于接收要删除的公告的相关标识信息DELETE请求常用于删除资源这里符合删除指定公告的操作场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,确保只有教师或管理员角色的用户可以执行删除公告的操作,保障了公告删除功能的权限安全性
public Result<String> deleteNotice(@PathVariable("ids") @NotBlank String ids) {
// 通过@PathVariable注解获取路径中传入的经过验证的公告相关标识参数将其传递给服务层以便服务层依据这些信息执行删除对应的公告记录的业务逻辑
return noticeService.deleteNotice(ids);
// 调用noticeService接口的deleteNotice方法由服务层去具体实现删除指定标识的公告的详细业务逻辑比如从数据库中移除对应的公告记录等最后将操作结果封装到Result对象中返回给客户端
}
/**
*
* @param id
* @param noticeForm
* @return
* @param id@NotBlank
* @param noticeFormNoticeForm
* @return Result
*/
@PutMapping("/{id}")
// 此注解指定这个方法用于处理HTTP PUT请求其请求路径是在类的基础路径/api/notices基础上添加/{id},其中{id}是路径变量用于接收要修改的公告的IDPUT请求常用于更新资源这里符合对指定ID的公告进行修改更新的操作场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,确保只有教师或管理员角色的用户可以执行修改公告的操作,保障了公告修改功能的权限安全性
public Result<String> updateNotice(@PathVariable("id") @NotBlank String id,@Validated @RequestBody NoticeForm noticeForm) {
// @PathVariable注解用于获取路径中的公告ID参数并通过@NotBlank保证其非空@Validated结合@RequestBody对noticeForm参数进行数据合法性验证及从请求正文获取数据转换为对应对象然后将这些信息传递给服务层进行修改公告的业务逻辑处理
return noticeService.updateNotice(id, noticeForm);
// 调用noticeService接口的updateNotice方法由服务层去具体实现根据传入的更新信息对指定ID的公告进行修改的详细业务逻辑比如更新数据库中对应公告记录的字段值等最后将操作结果封装到Result对象中返回给客户端
}
/**
*
* @param pageNum
* @param pageSize
* @param title
* @return
* @param pageNumpageNum11
* @param pageSize1010
* @param title
* @return ResultIPage<NoticeVO>IPageResult
*/
@GetMapping("/paging")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/notices基础上添加/paging即/api/notices/pagingGET请求常用于获取资源这里用于获取分页后的公告列表信息资源的操作符合GET请求的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,意味着教师和管理员角色的用户可以执行分页查找公告的操作,保障了该功能的权限安全性
public Result<IPage<NoticeVO>> getNotice(@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "title", required = false) String title) {
// 通过@RequestParam注解获取请求中的pageNum参数设置其为可选参数required = false若客户端未传入则使用默认值1方便客户端灵活指定要查询的页码同样获取pageSize参数默认值为10获取title参数用于根据公告标题筛选查询
return noticeService.getNotice( pageNum, pageSize, title);
// 调用通过依赖注入获取的noticeService接口实例的getNotice方法由服务层去具体实现根据传入的页码、每页记录数以及筛选条件等信息进行分页查询公告的详细业务逻辑最后将查询结果封装到Result对象中返回给客户端
}
/**
*
* @param pageNum
* @param pageSize
* @return
* @param pageNum1
* @param pageSize10
* @return ResultIPage<NoticeVO>IPageResult
*/
@GetMapping("/new")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/notices基础上添加/new即/api/notices/newGET请求常用于获取资源这里用于获取最新公告信息资源的操作符合GET请求的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
// 进行权限控制,拥有"role_teacher"、"role_admin"或者"role_student"权限的用户都可以访问这个方法,意味着教师、管理员和学生角色的用户都有权利执行获取最新公告的操作
public Result<IPage<NoticeVO>> getNewNotice(@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize){
// 通过@RequestParam注解获取请求中的pageNum和pageSize参数分别用于指定页码和每页记录数若未传入则使用默认值方便客户端灵活获取最新公告信息
return noticeService.getNewNotice(pageNum,pageSize);
// 调用通过依赖注入获取的noticeService接口实例的getNewNotice方法由服务层去具体实现根据传入的页码和每页记录数获取最新公告信息的详细业务逻辑比如按照发布时间等条件筛选出最新的公告并进行分页处理等最后将查询结果封装到Result对象中返回给客户端
}
}
}

@ -1,116 +1,175 @@
package cn.org.alan.exam.controller;
// 声明该类所在的包名,用于在项目中对类进行分类组织,表明此控制器类属于特定的“试题管理”功能模块相关的包,方便代码管理与维护
import cn.org.alan.exam.common.group.QuestionGroup;
// 引入QuestionGroup类可能是用于对试题相关操作进行分组验证的类比如针对试题添加、修改等不同操作按照特定规则进行分组验证确保数据符合相应业务场景下的合法性要求
import cn.org.alan.exam.common.result.Result;
// 引入Result类通常用于统一封装业务操作后的结果信息包含操作是否成功的标识以及可能需要返回的数据等内容以一致的格式返回给调用者如前端
import cn.org.alan.exam.model.form.question.QuestionFrom;
// 引入QuestionFrom类这是一个数据模型类用于承载添加、修改等试题相关操作时前端传递过来的试题信息例如试题内容、答案、分值等具体的数据内容
import cn.org.alan.exam.model.vo.QuestionVO;
// 引入QuestionVO类大概率是视图对象Value ObjectVO用于向客户端展示特定格式的试题相关信息经过业务逻辑处理后以适合展示的形式传递给前端
import cn.org.alan.exam.service.IQuestionService;
// 引入IQuestionService接口定义了与试题管理相关的一系列业务方法例如试题的添加、删除、查询、修改以及导入等操作的逻辑抽象具体的实现由对应的服务类来完成该控制器类会依赖这个接口来调用相应功能
import com.baomidou.mybatisplus.core.metadata.IPage;
// 引入IPage类这是MyBatis Plus框架提供的用于表示分页数据的类型在涉及分页查询试题相关信息的业务逻辑时用于承载分页后的试题数据等内容
import jakarta.annotation.Resource;
// 用于进行资源注入的注解在这里的作用是将实现了IQuestionService接口的具体实例注入到当前的QuestionController类中使得类中的方法可以方便地调用对应的服务方法实现具体业务逻辑
import org.springframework.security.access.prepost.PreAuthorize;
// 用于在方法级别进行权限控制的注解,根据配置的权限表达式判断当前用户是否有相应权限来访问对应的方法,以此保证系统资源只能被授权用户访问,增强系统的安全性和权限管理
import org.springframework.validation.annotation.Validated;
// 结合具体的验证规则(可以是默认规则或者像这里指定的分组验证规则)对传入方法的参数进行数据合法性验证,保证接收到的参数符合业务要求,避免非法数据进入业务逻辑处理流程
import org.springframework.web.bind.annotation.*;
// 通配符导入包含了众多Spring Web相关的注解例如用于定义请求处理方法的不同请求方式注解如@GetMapping、@PostMapping等以及处理请求路径、请求参数等相关的注解方便在控制器类中构建API接口
import org.springframework.web.multipart.MultipartFile;
// 引入MultipartFile类用于处理文件上传相关操作在这里主要用于接收前端上传的Excel文件批量导入试题时以及图片文件上传图片操作时等情况
/**
*
*
*
* @author WeiJin
* @since 2024-03-21
*/
@RestController
// 这是Spring框架提供的复合注解兼具@Controller和@ResponseBody的功能。意味着这个类是Spring MVC中的控制器类并且类中方法的返回值默认会直接以JSON等格式响应给客户端无需额外配置视图解析相关操作
@RequestMapping("/api/questions")
// 用于给整个控制器类下的所有请求处理方法设置一个公共的请求路径前缀表明此类中所有处理请求的方法对应的URL路径都将以/api/questions开头便于统一管理和组织与试题管理相关的一组API接口路径
public class QuestionController {
@Resource
// 使用@Resource注解来进行依赖注入让Spring容器查找并注入一个实现了IQuestionService接口的实例到当前类中方便后续的方法调用该服务实例所提供的各种试题管理相关业务方法
private IQuestionService iQuestionService;
/**
*
* @param questionFrom
* @return
* @param questionFrom QuestionFromQuestionGroup.QuestionAddGroup.class
* @return Result
*/
@PostMapping("/single")
// 该注解表明这个方法用于处理HTTP POST请求其请求路径是在类级别定义的基础路径/api/questions基础上添加/single用于区分其他试题相关操作路径POST请求常用于向服务器提交数据在此处符合向服务器提交单题添加相关信息的场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,确保只有教师或管理员角色的用户可以执行单题添加的操作,加强了对试题添加功能的权限管理
public Result<String> addSingleQuestion(@Validated(QuestionGroup.QuestionAddGroup.class) @RequestBody QuestionFrom questionFrom) {
// @Validated(QuestionGroup.QuestionAddGroup.class)结合@RequestBody注解@RequestBody表示从请求的正文中获取数据并将其转换为QuestionFrom类型的对象@Validated按照指定的分组验证规则对这个转换后的对象进行数据合法性验证只有验证通过的数据才会进入方法内部进行后续的单题添加业务逻辑处理
return iQuestionService.addSingleQuestion(questionFrom);
// 方法体内部直接调用通过依赖注入获取的iQuestionService接口实例的addSingleQuestion方法并将经过验证的questionFrom参数传递进去由服务层去具体实现添加单题的详细业务逻辑比如将试题信息保存到数据库中最后将服务层返回的包含添加结果等信息的Result对象直接返回给客户端
}
/**
*
* @param ids id
* @return
* @param ids idID
* @return Result
*/
@DeleteMapping("/batch/{ids}")
// 该注解表明这个方法用于处理HTTP DELETE请求其请求路径是在类的基础路径/api/questions基础上添加/batch/{ids},其中{ids}是路径变量用于接收要批量删除的试题的相关标识信息DELETE请求常用于删除资源这里符合批量删除指定试题的操作场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,确保只有教师或管理员角色的用户可以执行批量删除试题的操作,保障了试题删除功能的权限安全性
public Result<String> deleteBatchQuestion(@PathVariable("ids") String ids) {
// 通过@PathVariable注解获取路径中传入的要批量删除的试题相关标识参数将其传递给服务层以便服务层依据这些信息执行批量删除对应的试题记录的业务逻辑
return iQuestionService.deleteBatchByIds(ids);
// 调用iQuestionService接口的deleteBatchByIds方法由服务层去具体实现根据传入的试题标识批量删除试题的详细业务逻辑比如从数据库中移除对应的多条试题记录等最后将操作结果封装到Result对象中返回给客户端
}
/**
*
* @param pageNum
* @param pageSize
* @param content
* @param repoId id
* @param type
* @return
* @param pageNum pageNum11
* @param pageSize 1010
* @param content
* @param repoId idID
* @param type 便
* @return ResultIPage<QuestionVO>IPageResult
*/
@GetMapping("/paging")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/questions基础上添加/paging即/api/questions/pagingGET请求常用于获取资源这里用于获取分页后的试题列表信息资源的操作符合GET请求的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,意味着教师和管理员角色的用户可以执行分页查询试题的操作,保障了该功能的权限安全性
public Result<IPage<QuestionVO>> pagingQuestion(@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "content", required = false) String content,
@RequestParam(value = "repoId", required = false) Integer repoId,
@RequestParam(value = "type", required = false) Integer type) {
// 通过@RequestParam注解获取请求中的pageNum参数设置其为可选参数required = false若客户端未传入则使用默认值1方便客户端灵活指定要查询的页码同样获取pageSize参数默认值为10获取content、repoId、type参数分别用于按试题名、题库ID、试题类型进行筛选查询
return iQuestionService.pagingQuestion(pageNum, pageSize, content, type, repoId);
// 调用通过依赖注入获取的iQuestionService接口实例的pagingQuestion方法由服务层去具体实现根据传入的页码、每页记录数以及各种筛选条件等信息进行分页查询试题的详细业务逻辑最后将查询结果封装到Result对象中返回给客户端
}
/**
* id
* @param id id
* @return
* @param id idID
* @return Result
*/
@GetMapping("/single/{id}")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/questions基础上添加/single/{id},其中{id}是路径变量用于接收要获取详情的试题的IDGET请求常用于获取资源这里用于获取特定试题的详情信息资源符合GET请求的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,意味着教师和管理员角色的用户可以执行获取单题详情的操作,保障了该功能的权限安全性
public Result<QuestionVO> querySingle(@PathVariable("id") Integer id) {
// 通过@PathVariable注解获取路径中传入的试题ID参数将其传递给服务层以便服务层依据该ID准确找到对应的试题记录并获取其详细信息
return iQuestionService.querySingle(id);
// 调用iQuestionService接口的querySingle方法由服务层去具体实现根据传入的试题ID获取其详细信息的详细业务逻辑比如从数据库中查询对应试题记录的各个字段信息等最后将获取到的试题详情信息封装到Result对象中返回给客户端
}
/**
*
* @param id Id
* @param questionFrom
* @return
* @param id IdID
* @param questionFrom QuestionFrom
* @return Result
*/
@PutMapping("/{id}")
// 此注解指定这个方法用于处理HTTP PUT请求其请求路径是在类的基础路径/api/questions基础上添加/{id},其中{id}是路径变量用于接收要修改的试题的IDPUT请求常用于更新资源这里符合对指定ID的试题进行修改更新的操作场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,确保只有教师或管理员角色的用户可以执行修改试题的操作,保障了试题修改功能的权限安全性
public Result<String> updateQuestion(@PathVariable("id") Integer id, @RequestBody QuestionFrom questionFrom) {
// 通过@PathVariable注解获取路径中传入的试题ID参数将其赋值给变量id@RequestBody表示从请求正文中获取数据并转换为QuestionFrom类型的对象用于接收前端传来的修改试题的相关信息然后将试题ID设置到questionFrom对象中确保修改操作能准确对应到相应试题记录
questionFrom.setId(id);
return iQuestionService.updateQuestion(questionFrom);
// 调用iQuestionService接口的updateQuestion方法由服务层去具体实现根据传入的更新信息对指定ID的试题进行修改的详细业务逻辑比如更新数据库中对应试题记录的字段值等最后将操作结果封装到Result对象中返回给客户端
}
/**
*
* @param id Id
* @param file Excel
* @return
* @param id IdID便
* @param file ExcelMultipartFileExcel
* @return Result
*/
@PostMapping("/import/{id}")
// 表明这个方法用于处理HTTP POST请求其请求路径是在类级别定义的基础路径/api/questions基础上添加/import/{id},其中{id}是路径变量用于接收要导入试题的题库IDPOST请求常用于向服务器提交数据在此处符合向服务器提交批量导入试题相关信息包含Excel文件及题库ID的场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,确保只有教师或管理员角色的用户可以执行批量导入试题的操作,保障了试题导入功能的权限管理
public Result<String> importQuestion(@PathVariable("id") Integer id, @RequestParam("file") MultipartFile file) {
// 通过@PathVariable注解获取路径中传入的题库ID参数将其赋值给变量id通过@RequestParam("file")注解获取前端上传的文件将其赋值给变量file以便服务层依据这些信息进行批量导入试题到指定题库的业务逻辑处理
return iQuestionService.importQuestion(id,file);
// 调用iQuestionService接口的importQuestion方法由服务层去具体实现根据传入的题库ID和上传的Excel文件解析并批量导入试题到对应题库的详细业务逻辑比如读取文件内容、转换数据格式、插入数据库等操作最后将操作结果封装到Result对象中返回给客户端
}
/**
*
* @param file
* @return
* @param file MultipartFile
* @return Result便访
*/
@PostMapping("/uploadImage")
// 该注解表明这个方法用于处理HTTP POST请求其请求路径是
// @PreAuthorize注解用于在方法级别进行权限控制。
// 此处配置的"hasAnyAuthority('role_teacher','role_admin')"表示只有具备"role_teacher"(教师角色)或者"role_admin"管理员角色权限的用户才能够访问这个uploadImage方法。
// 通过这种方式保障了该图片上传功能只能被授权的特定角色用户操作,增强了系统的安全性和权限管理。
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 这个方法用于处理图片上传的业务逻辑,接收前端上传的图片文件,最终返回图片上传后的相关结果信息。
// 方法返回值类型为Result<String>Result通常用于统一封装业务操作后的结果信息这里的<String>表示结果中可能包含的具体数据是字符串类型,比如可能是上传后图片的存储地址等相关信息。
// @RequestPart("file")注解用于获取前端以multipart/form-data格式上传的文件部分名称为"file"的文件将其绑定到MultipartFile类型的参数file上以便后续在方法中进行处理比如将图片保存到服务器指定位置等操作。
public Result<String> uploadImage(@RequestPart("file") MultipartFile file){
// 方法内部直接调用通过依赖注入获取的iQuestionService接口实例的uploadImage方法并将接收到的MultipartFile类型的file参数传递进去。
// 由服务层也就是iQuestionService对应的具体实现类去具体实现图片上传的详细业务逻辑比如把图片存储到服务器磁盘的某个位置、生成对应的访问地址等操作最后将服务层返回的包含图片上传结果以及相关信息如上传后的地址等的Result对象直接返回给客户端。
return iQuestionService.uploadImage(file);
}
}

@ -1,81 +1,130 @@
package cn.org.alan.exam.controller;
// 声明该类所在的包名,用于在项目中对类进行分类组织,表明此控制器类属于特定的“考试记录”功能模块相关的包,方便代码管理和维护
import cn.org.alan.exam.common.result.Result;
// 引入Result类通常用于统一封装业务操作后的结果信息包含操作是否成功的标识以及可能需要返回的数据等内容以标准格式返回给调用者比如前端应用
import cn.org.alan.exam.model.vo.record.ExamRecordDetailVO;
// 引入ExamRecordDetailVO类这是一个视图对象Value ObjectVO用于向客户端展示特定格式的考试记录详细信息比如包含考试中每道题的答题情况、得分等详细内容方便前端展示和使用
import cn.org.alan.exam.model.vo.record.ExamRecordVO;
// 引入ExamRecordVO类同样是视图对象类用于展示考试记录的基本信息例如考试名称、考试时间、考生信息等内容以一种适合前端展示的形式传递数据
import cn.org.alan.exam.model.vo.record.ExerciseRecordDetailVO;
// 引入ExerciseRecordDetailVO类也是视图对象类用于呈现刷题记录的详细信息像刷题过程中每题的作答情况、所用时间等具体的细节信息便于前端展示给用户查看
import cn.org.alan.exam.model.vo.record.ExerciseRecordVO;
// 引入ExerciseRecordVO类用于向客户端展示刷题记录的基本信息例如刷题的题库名称、刷题时长、刷题的用户等相关基本情况的信息
import cn.org.alan.exam.service.IExerciseRecordService;
// 引入IExerciseRecordService接口定义了与考试记录、刷题记录相关的一系列业务方法像记录的分页查询、详情查询等操作的逻辑抽象具体的实现由对应的服务类来完成本控制器类依赖此接口调用相应功能
import com.baomidou.mybatisplus.core.metadata.IPage;
// 引入IPage类这是MyBatis Plus框架提供的用于表示分页数据的类型在涉及分页查询考试记录、刷题记录相关信息的业务逻辑中用于承载分页后的记录数据等内容
import jakarta.annotation.Resource;
// 用于进行资源注入的注解作用是将实现了IExerciseRecordService接口的具体实例注入到当前的RecordController类中使得类中的方法可以便捷地调用对应的服务方法来实现具体业务逻辑
import org.springframework.security.access.prepost.PreAuthorize;
// 用于在方法级别进行权限控制的注解,依据配置的权限表达式判断当前用户是否具备相应权限来访问对应的方法,以此保障系统资源只能被授权用户操作,增强系统的安全性和权限管理
import org.springframework.web.bind.annotation.GetMapping;
// 这是Spring Web相关的注解用于标识该方法处理HTTP GET请求GET请求常用于获取资源在这里符合获取考试记录、刷题记录相关信息资源的操作场景
import org.springframework.web.bind.annotation.RequestMapping;
// 用于给整个控制器类下的所有请求处理方法设置一个公共的请求路径前缀表明此类中所有处理请求的方法对应的URL路径都将以/api/records开头便于统一管理和组织与考试记录相关的一组API接口路径
import org.springframework.web.bind.annotation.RequestParam;
// 用于获取请求中的参数,通过指定参数名、是否必填以及默认值等信息,将请求中的对应参数绑定到方法的参数上,方便后续业务逻辑使用这些参数进行相应处理
import org.springframework.web.bind.annotation.RestController;
// 这是Spring框架提供的复合注解兼具@Controller和@ResponseBody的功能。意味着这个类是Spring MVC中的控制器类并且类中方法的返回值默认会直接以JSON等格式响应给客户端无需额外配置视图解析相关操作
import java.util.List;
// 引入Java标准库中的List接口用于表示列表类型的数据结构在需要返回多个同类型元素如查询到的多条考试记录详情等情况时会用到
/**
*
*
*
* @Author Alan
* @Version
* @Date 2024/3/25 11:22 AM
*/
@RestController
// 标注此类为Spring MVC中的控制器类且方法返回值默认以JSON等格式响应给客户端方便与前端交互
@RequestMapping("/api/records")
// 为该控制器类下的所有请求处理方法设置公共的请求路径前缀,后续具体方法的路径在此基础上扩展
public class RecordController {
@Resource
// 使用@Resource注解来进行依赖注入让Spring容器查找并注入一个实现了IExerciseRecordService接口的实例到当前类中方便后续调用相关业务方法
private IExerciseRecordService exerciseRecordService;
/**
*
* @param pageNum
* @param pageSize
* @return
* @param pageNumpageNum11
* @param pageSize1010
* @param examName
* @return ResultIPage<ExamRecordVO>IPageResult
*/
@GetMapping("/exam/paging")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/records基础上添加/exam/paging即/api/records/exam/paging用于获取已考试试卷的分页记录信息符合GET请求获取资源的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
// 进行权限控制,拥有"role_teacher"、"role_admin"或者"role_student"权限的用户都可以访问这个方法,意味着教师、管理员和学生角色的用户都有权利执行分页查询已考试试卷记录的操作
public Result<IPage<ExamRecordVO>> getExamRecordPage(@RequestParam(value = "pageNum",required = false, defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize",required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "pageSize",required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "examName", required = false) String examName){
// 通过@RequestParam注解分别获取请求中的pageNum、pageSize、examName参数若客户端未传入pageNum和pageSize则分别使用默认值1和10examName用于筛选查询条件
return exerciseRecordService.getExamRecordPage(pageNum,pageSize,examName);
// 调用通过依赖注入获取的exerciseRecordService接口实例的getExamRecordPage方法由服务层去具体实现根据传入的页码、每页记录数以及考试名称筛选条件等信息进行分页查询已考试试卷记录的详细业务逻辑最后将查询结果封装到Result对象中返回给客户端
}
/**
*
* @param examId
* @return
* @param examIdID
* @return ResultList<ExamRecordDetailVO>
*/
@GetMapping("/exam/detail")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/records基础上添加/exam/detail即/api/records/exam/detail用于获取特定试卷的详细信息符合GET请求获取资源的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
// 进行权限控制,拥有"role_teacher"、"role_admin"或者"role_student"权限的用户都可以访问这个方法,意味着教师、管理员和学生角色的用户都有权利执行查询试卷详情的操作
public Result<List<ExamRecordDetailVO>> getExamRecordDetail(@RequestParam("examId") Integer examId){
// 通过@RequestParam注解获取请求中名为"examId"的参数将其赋值给examId变量以便服务层依据该试卷ID准确找到对应的试卷记录并获取其详细信息
return exerciseRecordService.getExamRecordDetail(examId);
// 调用exerciseRecordService接口的getExamRecordDetail方法由服务层去具体实现根据传入的试卷ID获取其详细信息包含每题答题情况等的详细业务逻辑最后将获取到的试卷详情信息封装到Result对象中返回给客户端
}
/**
*
* @param pageNum
* @param pageSize
* @return
* @param pageNum1
* @param pageSize1010
* @param repoName
* @return ResultIPage<ExerciseRecordVO>IPageResult
*/
@GetMapping("/exercise/paging")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/records基础上添加/exercise/paging即/api/records/exercise/paging用于获取已考试刷题记录的分页信息符合GET请求获取资源的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
// 进行权限控制,拥有"role_teacher"、"role_admin"或者"role_student"权限的用户都可以访问这个方法,意味着教师、管理员和学生角色的用户都有权利执行分页查询已考试刷题记录的操作
public Result<IPage<ExerciseRecordVO>> getExerciseRecordPage(@RequestParam(value = "pageNum",required = false, defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize",required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "pageSize",required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "repoName", required = false) String repoName){
// 通过@RequestParam注解获取请求中的pageNum、pageSize、repoName参数若客户端未传入pageNum和pageSize则分别使用默认值1和10repoName用于按刷题的题库名称进行筛选查询
return exerciseRecordService.getExerciseRecordPage(pageNum,pageSize,repoName);
// 调用exerciseRecordService接口的实例的getExerciseRecordPage方法由服务层去具体实现根据传入的页码、每页记录数以及题库名称筛选条件等信息进行分页查询已考试刷题记录的详细业务逻辑最后将查询结果封装到Result对象中返回给客户端
}
/**
*
* @param exerciseId
* @return
* @param exerciseIdIDID
* @return ResultList<ExerciseRecordDetailVO>
*/
@GetMapping("/exercise/detail")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/records基础上添加/exercise/detail即/api/records/exercise/detail用于获取特定刷题记录的详细信息符合GET请求获取资源的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
// 进行权限控制,拥有"role_teacher"、"role_admin"或者"role_student"权限的用户都可以访问这个方法,意味着教师、管理员和学生角色的用户都有权利执行查询刷题详情的操作
public Result<List<ExerciseRecordDetailVO>> getExerciseRecordDetail(@RequestParam("repoId") Integer exerciseId){
// 通过@RequestParam注解获取请求中名为"repoId"的参数此处参数名可能不太准确也许应该和前面定义的exerciseId参数名统一便于理解和维护将其赋值给exerciseId变量以便服务层依据该ID准确找到对应的刷题记录并获取其详细信息
return exerciseRecordService.getExerciseRecordDetail(exerciseId);
// 调用exerciseRecordService接口的getExerciseRecordDetail方法由服务层去具体实现根据传入的刷题记录ID获取其详细信息包含每题答题情况等的详细业务逻辑最后将获取到的刷题详情信息封装到Result对象中返回给客户端
}
}
}

@ -1,96 +1,140 @@
package cn.org.alan.exam.controller;
// 声明该类所在的包名,用于在项目中对类进行分类组织,表明此控制器类属于特定的“题库管理”功能模块相关的包,方便代码管理与维护
import cn.org.alan.exam.common.result.Result;
// 引入Result类通常用于统一封装业务操作后的结果信息包含操作是否成功的标识以及可能需要返回的数据等内容以一致的格式返回给调用者如前端
import cn.org.alan.exam.model.entity.Repo;
// 引入Repo类这是一个实体类对应数据库中存储题库相关信息的表结构包含了如题库的各种属性例如题库名称、创建人、创建时间等信息用于在业务逻辑中承载和传递与题库相关的数据
import cn.org.alan.exam.model.vo.repo.RepoListVO;
// 引入RepoListVO类大概率是视图对象Value ObjectVO用于向客户端展示特定格式的题库列表相关信息例如可能只包含题库ID和题库名称等关键信息方便前端展示和使用
import cn.org.alan.exam.model.vo.repo.RepoVO;
// 引入RepoVO类同样是视图对象类用于向客户端展示更详细的题库相关信息经过业务逻辑处理后以适合展示的形式传递给前端包含的信息可能比RepoListVO更丰富全面
import cn.org.alan.exam.service.IRepoService;
// 引入IRepoService接口定义了与题库管理相关的一系列业务方法例如题库的添加、修改、删除、查询等操作的逻辑抽象具体的实现由对应的服务类来完成该控制器类会依赖这个接口来调用相应功能
import com.baomidou.mybatisplus.core.metadata.IPage;
// 引入IPage类这是MyBatis Plus框架提供的用于表示分页数据的类型在涉及分页查询题库相关信息的业务逻辑时用于承载分页后的题库数据等内容
import jakarta.annotation.Resource;
// 用于进行资源注入的注解在这里的作用是将实现了IRepoService接口的具体实例注入到当前的RepoController类中使得类中的方法可以方便地调用对应的服务方法实现具体业务逻辑
import org.springframework.security.access.prepost.PreAuthorize;
// 用于在方法级别进行权限控制的注解,根据配置的权限表达式判断当前用户是否有相应权限来访问对应的方法,以此保证系统资源只能被授权用户访问,增强系统的安全性和权限管理
import org.springframework.validation.annotation.Validated;
// 结合具体的验证规则(可以是默认规则或者自定义规则)对传入方法的参数进行数据合法性验证,保证接收到的参数符合业务要求,避免非法数据进入业务逻辑处理流程
import org.springframework.web.bind.annotation.*;
// 通配符导入包含了众多Spring Web相关的注解例如用于定义请求处理方法的不同请求方式注解如@GetMapping、@PostMapping等以及处理请求路径、请求参数等相关的注解方便在控制器类中构建API接口
import java.util.List;
// 引入Java标准库中的List接口用于表示列表类型的数据结构在需要返回多个同类型元素如多个题库的信息列表等情况时会用到
/**
*
*
*
* @author WeiJin
* @since 2024-03-21
*/
@RestController
// 这是Spring框架提供的复合注解兼具@Controller和@ResponseBody的功能。意味着这个类是Spring MVC中的控制器类并且类中方法的返回值默认会直接以JSON等格式响应给客户端无需额外配置视图解析相关操作
@RequestMapping("/api/repo")
// 用于给整个控制器类下的所有请求处理方法设置一个公共的请求路径前缀表明此类中所有处理请求的方法对应的URL路径都将以/api/repo开头便于统一管理和组织与题库管理相关的一组API接口路径
public class RepoController {
@Resource
// 使用@Resource注解来进行依赖注入让Spring容器查找并注入一个实现了IRepoService接口的实例到当前类中方便后续的方法调用该服务实例所提供的各种题库管理相关业务方法
private IRepoService iRepoService;
/**
*
*
* @param repo
* @return
* @param repo Repo
* @return Result
*/
@PostMapping
// 该注解表明这个方法用于处理HTTP POST请求其请求路径就是类级别定义的基础路径/api/repo因为这里没有额外指定路径POST请求常用于向服务器提交数据在此处符合向服务器提交添加题库相关信息的场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"(教师角色)或者"role_admin"(管理员角色)权限的用户才能访问这个方法,确保只有教师或管理员角色的用户可以执行添加题库的操作,加强了对题库添加功能的权限管理
public Result<String> addRepo(@Validated @RequestBody Repo repo) {
//从token获取用户id放入创建人id属性
// @Validated注解结合@RequestBody注解@RequestBody表示从请求的正文中获取数据并将其转换为Repo类型的对象@Validated用于对这个转换后的对象依据相关规则进行数据合法性验证只有验证通过的数据才会进入方法内部进行后续的添加题库业务逻辑处理
//从token获取用户id放入创建人id属性此处应该是在服务层或者更底层的代码逻辑中实现从请求携带的token通常用于用户认证授权里解析出当前操作的用户ID并将其设置到传入的repo对象的创建人ID属性上确保添加的题库能关联到正确的创建人
return iRepoService.addRepo(repo);
// 方法体内部直接调用通过依赖注入获取的iRepoService接口实例的addRepo方法并将经过验证且设置好创建人ID的repo参数传递进去由服务层去具体实现添加题库的详细业务逻辑比如将题库信息保存到数据库中最后将服务层返回的包含添加结果等信息的Result对象直接返回给客户端
}
/**
*
*
* @param repo
* @return
* @param repo Repo
* @return Result
*/
@PutMapping("/{id}")
// 此注解指定这个方法用于处理HTTP PUT请求其请求路径是在类的基础路径/api/repo基础上添加/{id},其中{id}是路径变量用于接收要修改的题库的IDPUT请求常用于更新资源这里符合对指定ID的题库进行修改更新的操作场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,确保只有教师或管理员角色的用户可以执行修改题库的操作,保障了题库修改功能的权限安全性
public Result<String> updateRepo(@Validated @RequestBody Repo repo, @PathVariable("id") Integer id) {
// @Validated结合@RequestBody对repo参数进行数据合法性验证及从请求正文获取数据转换为对应对象@PathVariable注解用于获取路径中的题库ID参数将其赋值给变量id以便服务层依据该ID准确找到对应的题库记录进行修改操作
return iRepoService.updateRepo(repo, id);
// 调用iRepoService接口的updateRepo方法由服务层去具体实现根据传入的更新信息对指定ID的题库进行修改的详细业务逻辑比如更新数据库中对应题库记录的字段值等最后将操作结果封装到Result对象中返回给客户端
}
/**
* id
*
* @param id id
* @return
* @param id idID
* @return Result
*/
@DeleteMapping("/{id}")
// 该注解表明这个方法用于处理HTTP DELETE请求其请求路径是在类的基础路径/api/repo基础上添加/{id},其中{id}是路径变量用于接收要删除的题库的IDDELETE请求常用于删除资源这里符合删除指定题库的操作场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,确保只有教师或管理员角色的用户可以执行删除题库的操作,保障了题库删除功能的权限安全性
public Result<String> deleteRepoById(@PathVariable("id") Integer id) {
// 通过@PathVariable注解获取路径中传入的题库ID参数将其传递给服务层以便服务层依据这个ID准确找到并删除对应的题库记录
return iRepoService.deleteRepoById(id);
// 调用iRepoService接口的deleteRepoById方法由服务层去具体实现删除指定ID的题库的详细业务逻辑比如从数据库中移除对应的题库记录等最后将操作结果封装到Result对象中返回给客户端
}
/**
* id
* @param repoTitle
* @return
* @param repoTitle
* @return ResultIDList<RepoListVO>
*/
@GetMapping("/list")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/repo基础上添加/list即/api/repo/listGET请求常用于获取资源这里用于获取题库ID和名称列表信息资源的操作符合GET请求的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法意味着教师和管理员角色的用户可以执行获取题库ID和名称列表的操作保障了该功能的权限安全性
public Result<List<RepoListVO>> getRepoList(@RequestParam(value = "repoTitle",required = false) String repoTitle) {
// 通过@RequestParam注解获取请求中的repoTitle参数设置其为可选参数required = false方便客户端根据实际需求决定是否传入此参数进行筛选查询
return iRepoService.getRepoList(repoTitle);
// 调用iRepoService接口的getRepoList方法由服务层去具体实现根据传入的题库名称可选获取相应的题库ID和名称列表信息的详细业务逻辑比如按照教师或管理员权限进行不同的数据筛选等最后将获取到的列表数据封装到Result对象中返回给客户端
}
/**
*
*
* @param pageNum
* @param pageSize
* @param title
* @return
* @param pageNum pageNum11
* @param pageSize 1010
* @param title
* @return ResultIPage<RepoVO>IPageResult
*/
@GetMapping("/paging")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/repo基础上添加/paging即/api/repo/pagingGET请求常用于获取资源这里用于获取分页后的题库列表信息资源的操作符合GET请求的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,意味着教师和管理员角色的用户可以执行分页查询题库的操作,保障了该功能的权限安全性
public Result<IPage<RepoVO>> pagingRepo(@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "title", required = false) String title) {
// 通过@RequestParam注解获取请求中的pageNum参数设置其为可选参数required = false若客户端未传入则使用默认值1方便客户端灵活指定要查询的页码同样获取pageSize参数默认值为10获取title参数用于根据题库名称筛选查询
return iRepoService.pagingRepo(pageNum, pageSize, title);
// 调用iRepoService接口的pagingRepo方法由服务层去具体实现根据传入的页码、每页记录数以及筛选条件等信息进行分页查询题库的详细业务逻辑最后将查询结果封装到Result对象中返回给客户端
}
}
}

@ -1,99 +1,144 @@
package cn.org.alan.exam.controller;
// 声明该类所在的包名,用于在项目中对类进行分类组织,表明此控制器类属于特定的“成绩管理”功能模块相关的包,方便代码管理与维护
import cn.org.alan.exam.common.result.Result;
// 引入Result类通常用于统一封装业务操作后的结果信息包含操作是否成功的标识以及可能需要返回的数据等内容以标准格式返回给调用者比如前端应用
import cn.org.alan.exam.model.vo.score.GradeScoreVO;
// 引入GradeScoreVO类这是一个视图对象Value ObjectVO用于向客户端展示特定格式的班级成绩相关分析信息比如班级整体考试成绩的平均分、各分数段人数分布等内容方便前端展示和使用
import cn.org.alan.exam.model.vo.score.QuestionAnalyseVO;
// 引入QuestionAnalyseVO类同样是视图对象类用于呈现某道试题在考试中的作答情况分析信息例如该题的正确率、各选项选择人数等具体的分析数据便于前端展示给教师等查看分析试题情况
import cn.org.alan.exam.model.vo.score.UserScoreVO;
// 引入UserScoreVO类也是视图对象类用于向客户端展示学生个人的考试成绩相关信息像学生的姓名、考试得分、排名等具体的成绩情况内容
import cn.org.alan.exam.service.IStatService;
// 引入IStatService接口可能定义了一些与成绩统计相关的通用业务方法或者其他辅助性的统计逻辑操作不过在此控制器类中暂时未看到直接调用它的地方也许在其他相关类中会被调用协作实现功能
import cn.org.alan.exam.service.IExamQuAnswerService;
// 引入IExamQuAnswerService接口定义了与考试中试题作答情况分析相关的一系列业务方法例如获取某题的作答详情分析等操作的逻辑抽象具体的实现由对应的服务类来完成本控制器类会依赖此接口调用相应功能
import cn.org.alan.exam.service.IUserExamsScoreService;
// 引入IUserExamsScoreService接口定义了与用户考试成绩查询、成绩信息分页获取以及成绩导出等相关的一系列业务方法具体的实现由对应的服务类来完成该控制器类中的多个方法依赖此接口调用相应功能
import com.baomidou.mybatisplus.core.metadata.IPage;
// 引入IPage类这是MyBatis Plus框架提供的用于表示分页数据的类型在涉及分页查询成绩相关信息的业务逻辑中用于承载分页后的成绩数据等内容
import jakarta.annotation.Resource;
// 用于进行资源注入的注解作用是将实现了相关服务接口如IStatService、IUserExamsScoreService、IExamQuAnswerService的具体实例注入到当前的ScoreController类中使得类中的方法可以便捷地调用对应的服务方法来实现具体业务逻辑
import jakarta.servlet.http.HttpServletResponse;
// 引入HttpServletResponse类它代表了服务器对客户端的HTTP响应对象在成绩导出功能中会使用到用于设置响应的相关属性比如响应头信息、输出流等以便将成绩数据以合适的格式如Excel等返回给客户端进行下载
import org.springframework.security.access.prepost.PreAuthorize;
// 用于在方法级别进行权限控制的注解,依据配置的权限表达式判断当前用户是否具备相应权限来访问对应的方法,以此保障系统资源只能被授权用户操作,增强系统的安全性和权限管理
import org.springframework.web.bind.annotation.*;
// 通配符导入包含了众多Spring Web相关的注解例如用于定义请求处理方法的不同请求方式注解如@GetMapping、@PostMapping等以及处理请求路径、请求参数等相关的注解方便在控制器类中构建API接口
/**
*
*
*
* @Author WeiJin
* @Version
* @Date 2024/3/25 11:19 AM
*/
@RestController
// 这是Spring框架提供的复合注解兼具@Controller和@ResponseBody的功能。意味着这个类是Spring MVC中的控制器类并且类中方法的返回值默认会直接以JSON等格式响应给客户端无需额外配置视图解析相关操作
@RequestMapping("/api/score")
// 用于给整个控制器类下的所有请求处理方法设置一个公共的请求路径前缀表明此类中所有处理请求的方法对应的URL路径都将以/api/score开头便于统一管理和组织与成绩管理相关的一组API接口路径
public class ScoreController {
@Resource
// 使用@Resource注解来进行依赖注入让Spring容器查找并注入实现了IStatService接口的具体实例到当前类中虽然当前类中暂时未看到直接调用它的地方但可能在后续业务逻辑扩展或者其他相关功能实现中会用到
private IStatService iStatService;
@Resource
// 同样使用@Resource注解注入实现了IUserExamsScoreService接口的实例后续多个与成绩查询、成绩分析、成绩导出等相关的方法会依赖这个服务实例来调用具体业务逻辑方法
private IUserExamsScoreService iUserExamsScoreService;
@Resource
// 注入实现了IExamQuAnswerService接口的实例用于调用与试题作答情况分析相关的业务方法比如获取某题的作答详情分析等操作
private IExamQuAnswerService iExamQuAnswerService;
/**
*
* @param pageNum
* @param pageSize
* @param gradeId Id
* @param examId Id
* @param realName
* @return
* @param pageNumpageNum11
* @param pageSize1010
* @param gradeIdID便
* @param examIdID便
* @param realName
* @return ResultIPage<UserScoreVO>IPageResult
*/
@GetMapping("/paging")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/score基础上添加/paging即/api/score/paging用于获取分页后的成绩信息符合GET请求获取资源的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,拥有"role_teacher"(教师角色)或者"role_admin"(管理员角色)权限的用户都可以访问这个方法,意味着教师和管理员角色的用户有权利执行分页查询成绩信息的操作
public Result<IPage<UserScoreVO>> pagingScore(@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "gradeId") Integer gradeId,
@RequestParam(value = "examId") Integer examId,
@RequestParam(value = "realName", required = false) String realName) {
// 通过@RequestParam注解分别获取请求中的pageNum、pageSize、gradeId、examId、realName参数若客户端未传入pageNum和pageSize则分别使用默认值1和10gradeId、examId用于筛选特定班级和考试的成绩realName用于按学生姓名进一步筛选
return iUserExamsScoreService.pagingScore(pageNum, pageSize, gradeId, examId, realName);
// 调用通过依赖注入获取的iUserExamsScoreService接口实例的pagingScore方法由服务层去具体实现根据传入的页码、每页记录数以及班级ID、考试ID、学生姓名等筛选条件进行分页查询成绩信息的详细业务逻辑最后将查询结果封装到Result对象中返回给客户端
}
/**
*
* @param examId id
* @param questionId id
* @return
* @param examIdID
* @param questionIdIDexamId
* @return ResultQuestionAnalyseVO
*/
@GetMapping("/question/{examId}/{questionId}")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/score基础上添加/question/{examId}/{questionId},其中{examId}和{questionId}是路径变量用于接收要分析的考试和试题的ID符合GET请求获取资源这里是获取试题作答情况资源的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,意味着教师和管理员角色的用户有权利执行获取试题作答情况分析的操作
public Result<QuestionAnalyseVO> questionAnalyse(@PathVariable("examId") Integer examId,
@PathVariable("questionId") Integer questionId) {
// 通过@PathVariable注解分别获取路径中的考试ID和试题ID参数将其赋值给对应的examId和questionId变量以便服务层依据这两个ID准确找到对应的考试和试题记录进而获取试题的作答情况分析信息
return iExamQuAnswerService.questionAnalyse(examId, questionId);
// 调用iExamQuAnswerService接口的questionAnalyse方法由服务层去具体实现根据传入的考试ID和试题ID获取该试题在对应考试中的作答情况分析比如各选项选择人数、正确率等的详细业务逻辑最后将获取到的分析信息封装到Result对象中返回给客户端
}
/**
*
* @param pageNum
* @param pageSize
* @param examTitle
* @return
* @param pageNum1
* @param pageSize1010
* @param examTitle便
* @param gradeIdID便
* @return ResultIPage<GradeScoreVO>IPageResult
*/
@GetMapping("/getExamScore")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/score基础上添加/getExamScore即/api/score/getExamScore用于获取班级考试情况分析信息符合GET请求获取资源的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,意味着教师和管理员角色的用户有权利执行根据班级分析考试情况的操作
public Result<IPage<GradeScoreVO>> getExamScoreInfo(
@RequestParam(value = "pageNum",required = false,defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize",required = false,defaultValue = "10") Integer pageSize,
@RequestParam(value = "examTitle",required = false) String examTitle,
@RequestParam(value = "gradeId" ,required = false) Integer gradeId){
@RequestParam(value = "gradeId",required = false) Integer gradeId){
// 通过@RequestParam注解获取请求中的pageNum、pageSize、examTitle、gradeId参数若客户端未传入pageNum和pageSize则分别使用默认值1和10examTitle用于按考试名称筛选gradeId用于按班级筛选分析考试情况
return iUserExamsScoreService.getExamScoreInfo(pageNum,pageSize,examTitle,gradeId);
// 调用iUserExamsScoreService接口的getExamScoreInfo方法由服务层去具体实现根据传入的页码、每页记录数以及考试名称、班级ID等筛选条件进行分页查询并分析班级考试情况比如班级平均分、各分数段人数等的详细业务逻辑最后将查询分析结果封装到Result对象中返回给客户端
}
/**
*
* @param response
* @param examId id
* @param gradeId id
* @param responseHttpServletResponseHTTP使Excel便
* @param examIdID
* @param gradeIdID便
*/
@GetMapping("/export/{examId}/{gradeId}")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/score基础上添加/export/{examId}/{gradeId},其中{examId}和{gradeId}是路径变量用于接收要导出成绩的考试和班级的ID符合GET请求获取资源这里是获取成绩数据并导出资源的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,意味着教师和管理员角色的用户有权利执行成绩导出的操作
public void scoreExport(HttpServletResponse response,@PathVariable("examId") Integer examId, @PathVariable("gradeId") Integer gradeId) {
// 通过@PathVariable注解获取路径中的考试ID和班级ID参数将其赋值给对应的examId和gradeId变量以便服务层依据这两个ID准确找到对应的考试和班级记录进而获取相关成绩数据进行导出操作
iUserExamsScoreService.exportScores(response,examId,gradeId);
// 调用iUserExamsScoreService接口的exportScores方法由服务层去具体实现根据传入的HttpServletResponse对象以及考试ID、班级ID将相应的成绩数据按照一定格式如Excel进行组织并通过响应对象返回给客户端使得客户端可以下载成绩文件的详细业务逻辑
}
}
}

@ -1,80 +1,110 @@
package cn.org.alan.exam.controller;
// 声明该类所在的包名,用于在项目中对类进行分类组织,表明此控制器类属于特定的“统计管理”功能模块相关的包,方便代码管理与维护
import cn.org.alan.exam.common.result.Result;
// 引入Result类通常用于统一封装业务操作后的结果信息包含操作是否成功的标识以及可能需要返回的数据等内容以标准格式返回给调用者比如前端应用
import cn.org.alan.exam.model.vo.stat.AllStatsVO;
// 引入AllStatsVO类这是一个视图对象Value ObjectVO用于向客户端展示特定格式的所有相关统计信息汇总结果例如可能包含班级数量、试卷数量、试题数量等综合统计数据方便前端展示和使用
import cn.org.alan.exam.model.vo.stat.DailyVO;
// 引入DailyVO类同样是视图对象类用于呈现每日相关的统计信息具体包含的内容需看其类内部定义可能是每日新增的各类数据量、每日活跃用户数等与日常统计相关的数据便于前端展示给用户查看
import cn.org.alan.exam.model.vo.stat.GradeExamVO;
// 引入GradeExamVO类也是视图对象类用于向客户端展示各班级试卷相关的统计信息比如每个班级已创建的试卷数量、已完成考试的试卷数量等班级与试卷维度的统计情况内容
import cn.org.alan.exam.model.vo.stat.GradeStudentVO;
// 引入GradeStudentVO类用于向客户端展示各班级学生人数相关的统计信息像每个班级的学生总数、不同状态如活跃、未活跃等具体看业务定义的学生数量等班级与学生人数维度的统计数据
import cn.org.alan.exam.service.IStatService;
// 引入IStatService接口定义了与统计管理相关的一系列业务方法例如获取各班级学生人数统计、各班试卷统计、所有数据总量统计以及日常统计等操作的逻辑抽象具体的实现由对应的服务类来完成本控制器类依赖此接口调用相应功能
import jakarta.annotation.Resource;
// 用于进行资源注入的注解作用是将实现了IStatService接口的具体实例注入到当前的StatController类中使得类中的方法可以便捷地调用对应的服务方法来实现具体业务逻辑
import org.springframework.security.access.prepost.PreAuthorize;
// 用于在方法级别进行权限控制的注解,依据配置的权限表达式判断当前用户是否具备相应权限来访问对应的方法,以此保障系统资源只能被授权用户操作,增强系统的安全性和权限管理
import org.springframework.web.bind.annotation.GetMapping;
// 这是Spring Web相关的注解用于标识该方法处理HTTP GET请求GET请求常用于获取资源在这里符合获取各种统计信息资源的操作场景
import org.springframework.web.bind.annotation.RequestMapping;
// 用于给整个控制器类下的所有请求处理方法设置一个公共的请求路径前缀表明此类中所有处理请求的方法对应的URL路径都将以/api/stat开头便于统一管理和组织与统计管理相关的一组API接口路径
import org.springframework.web.bind.annotation.RestController;
// 这是Spring框架提供的复合注解兼具@Controller和@ResponseBody的功能。意味着这个类是Spring MVC中的控制器类并且类中方法的返回值默认会直接以JSON等格式响应给客户端无需额外配置视图解析相关操作
import java.util.List;
// 引入Java标准库中的List接口用于表示列表类型的数据结构在需要返回多个同类型元素如多个班级的统计信息列表等情况时会用到
/**
*
*
*
* @Author Alan
* @Version
* @Date 2024/3/25 11:22 AM
*/
@RestController
// 标注此类为Spring MVC中的控制器类且方法返回值默认以JSON等格式响应给客户端方便与前端交互
@RequestMapping("/api/stat")
// 为该控制器类下的所有请求处理方法设置公共的请求路径前缀,后续具体方法的路径在此基础上扩展
public class StatController {
@Resource
// 使用@Resource注解来进行依赖注入让Spring容器查找并注入一个实现了IStatService接口的实例到当前类中方便后续调用相关业务方法
private IStatService statService;
/**
*
* @return
* @return ResultList<GradeStudentVO>
*/
@GetMapping("/student")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/stat基础上添加/student即/api/stat/student用于获取各班级人数统计信息符合GET请求获取资源的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,拥有"role_teacher"(教师角色)或者"role_admin"(管理员角色)权限的用户都可以访问这个方法,意味着教师和管理员角色的用户有权利执行获取各班级人数统计的操作
public Result<List<GradeStudentVO>> getStudentGradeCount() {
// 方法内部直接调用通过依赖注入获取的statService接口实例的getStudentGradeCount方法由服务层去具体实现获取各班级人数统计信息的详细业务逻辑比如从数据库中查询各班级的学生数量等相关数据并封装成相应的视图对象列表最后将查询结果封装到Result对象中返回给客户端
return statService.getStudentGradeCount();
}
/**
*
* @return
* @return ResultList<GradeExamVO>
*/
@GetMapping("/exam")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/stat基础上添加/exam即/api/stat/exam用于获取各班试卷统计信息符合GET请求获取资源的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,拥有"role_teacher"或者"role_admin"权限的用户都可以访问这个方法,意味着教师和管理员角色的用户有权利执行获取各班试卷统计的操作
public Result<List<GradeExamVO>> getExamGradeCount() {
// 调用statService接口的getExamGradeCount方法由服务层去具体实现获取各班试卷统计信息的详细业务逻辑比如查询每个班级对应的试卷数量等相关数据并整理成相应的视图对象列表最后将结果封装到Result对象中返回给客户端
return statService.getExamGradeCount();
}
/**
*
*
* @return
* @return ResultAllStatsVO
*/
@GetMapping("/allCounts")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/stat基础上添加/allCounts即/api/stat/allCounts用于获取所有班级、试卷、试题数量的综合统计信息符合GET请求获取资源的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,拥有"role_teacher"或者"role_admin"权限的用户都可以访问这个方法,意味着教师和管理员角色的用户有权利执行获取整体数据数量统计的操作
public Result<AllStatsVO> getAllCount(){
// 调用statService接口的getAllCount方法由服务层去具体实现统计所有班级、试卷、试题数量的详细业务逻辑比如分别查询班级总数、试卷总数、试题总数等数据并整合到对应的视图对象中最后将其封装到Result对象中返回给客户端
return statService.getAllCount();
}
/**
* DailyVO
* @return ResultList<DailyVO>
*/
@GetMapping("/daily")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/stat基础上添加/daily即/api/stat/daily用于获取日常相关统计信息符合GET请求获取资源的使用场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
// 进行权限控制,拥有"role_teacher"(教师角色)、"role_admin"(管理员角色)或者"role_student"(学生角色)权限的用户都可以访问这个方法,意味着教师、管理员和学生角色的用户有权利执行获取日常统计信息的操作
public Result<List<DailyVO>> getDaily(){
// 调用statService接口的getDaily方法由服务层去具体实现获取日常相关统计信息的详细业务逻辑比如查询每日新增的各类数据量等相关数据并整理成相应的视图对象列表最后将结果封装到Result对象中返回给客户端
return statService.getDaily();
}
}
}

@ -1,76 +1,112 @@
package cn.org.alan.exam.controller;
// 声明该类所在的包名,用于在项目中对类进行分类组织,表明此控制器类属于特定的“错题本管理”功能模块相关的包,方便代码管理和维护
import cn.org.alan.exam.common.result.Result;
// 引入Result类通常用于统一封装业务操作后的结果信息包含操作是否成功的标识以及可能需要返回的数据等内容以标准格式返回给调用者比如前端应用便于告知客户端对应业务操作的执行情况及相关返回数据。
import cn.org.alan.exam.model.form.userbook.ReUserBookForm;
// 引入ReUserBookForm类这是一个数据模型类用于承载与错题本相关操作比如填充答案操作时前端传递过来的特定格式的数据可能包含错题的一些原始信息、要填写的答案等具体内容方便在业务逻辑中进行数据传递和处理。
import cn.org.alan.exam.model.vo.userbook.*;
// 引入多个以“userbook”命名空间下的视图对象Value ObjectVO这些类用于向客户端展示特定格式的错题本相关信息不同的VO类对应不同的业务场景比如展示错题分页信息、错题ID列表信息、单题详细信息等方便前端按照合适的格式展示数据给用户查看。
import cn.org.alan.exam.service.IUserBookService;
// 引入IUserBookService接口定义了与错题本管理相关的一系列业务方法例如错题的分页查询、错题ID列表查询、单题查询以及答案填充等操作的逻辑抽象具体的实现由对应的服务类来完成该控制器类依赖这个接口来调用相应功能。
import com.baomidou.mybatisplus.core.metadata.IPage;
// 引入IPage类这是MyBatis Plus框架提供的用于表示分页数据的类型在涉及分页查询错题本相关信息的业务逻辑时用于承载分页后的错题数据等内容方便对分页数据进行统一的处理和传递。
import jakarta.annotation.Resource;
// 用于进行资源注入的注解作用是将实现了IUserBookService接口的具体实例注入到当前的UserBookController类中使得类中的方法可以便捷地调用对应的服务方法来实现具体业务逻辑。
import org.springframework.security.access.prepost.PreAuthorize;
// 用于在方法级别进行权限控制的注解,依据配置的权限表达式判断当前用户是否具备相应权限来访问对应的方法,以此保障系统资源只能被授权用户操作,增强系统的安全性和权限管理,这里限定了不同操作对应的有权限的角色。
import org.springframework.web.bind.annotation.*;
// 通配符导入包含了众多Spring Web相关的注解例如用于定义请求处理方法的不同请求方式注解如@GetMapping、@PostMapping等以及处理请求路径、请求参数等相关的注解方便在控制器类中构建API接口处理来自客户端的各种请求。
import java.util.List;
// 引入Java标准库中的List接口用于表示列表类型的数据结构在需要返回多个同类型元素如多个错题的ID列表等情况时会用到便于对一组数据进行统一操作和传递。
/**
*
* ID
*
* @author Alan
* @since 2024-03-21
*/
@RestController
// 这是Spring框架提供的复合注解兼具@Controller和@ResponseBody的功能。意味着这个类是Spring MVC中的控制器类并且类中方法的返回值默认会直接以JSON等格式响应给客户端无需额外配置视图解析相关操作方便与前端进行数据交互。
@RequestMapping("/api/userbooks")
// 用于给整个控制器类下的所有请求处理方法设置一个公共的请求路径前缀表明此类中所有处理请求的方法对应的URL路径都将以/api/userbooks开头便于统一管理和组织与错题本管理相关的一组API接口路径。
public class UserBookController {
@Resource
// 使用@Resource注解来进行依赖注入让Spring容器查找并注入一个实现了IUserBookService接口的实例到当前类中方便后续调用相关业务方法使得业务逻辑能够顺利执行。
private IUserBookService userBookService;
/**
*
* @param pageNum
* @param pageSize
* @param examName
* @return
* @param pageNumpageNum11便
* @param pageSize1010
* @param examName便
* @return ResultIPage<UserPageBookVO>IPageResult便使
*/
@GetMapping("/paging")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/userbooks基础上添加/paging即/api/userbooks/paging用于获取分页后的错题考试信息符合GET请求获取资源的使用场景便于客户端发起获取错题数据的请求。
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
// 进行权限控制,拥有"role_teacher"(教师角色)、"role_admin"(管理员角色)或者"role_student"(学生角色)权限的用户都可以访问这个方法,意味着教师、管理员和学生角色的用户有权利执行分页查询错题考试的操作,确保有权限的用户才能获取相关数据。
public Result<IPage<UserPageBookVO>> getPage(@RequestParam(value = "pageNum",required = false, defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize",required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "examName",required = false) String examName){
// 通过@RequestParam注解分别获取请求中的pageNum、pageSize、examName参数若客户端未传入pageNum和pageSize则分别使用默认值1和10examName用于筛选查询条件将获取到的参数传递给服务层进行后续处理。
return userBookService.getPage(pageNum,pageSize,examName);
// 调用通过依赖注入获取的userBookService接口实例的getPage方法由服务层去具体实现根据传入的页码、每页记录数以及考试名称筛选条件等信息进行分页查询错题考试信息的详细业务逻辑比如从数据库中查询符合条件的错题数据并进行分页处理最后将查询结果封装到Result对象中返回给客户端。
}
/**
* id
* @param examId
* @return
* @param examIdIDID便便
* @return ResultIDIDList<ReUserExamBookVO>便ID
*/
@GetMapping("/question/list/{examId}")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/userbooks基础上添加/question/list/{examId},其中{examId}是路径变量用于接收要查询错题ID列表对应的考试的ID符合GET请求获取资源这里是获取错题ID列表资源的使用场景便于客户端根据考试ID来请求获取错题ID。
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
// 进行权限控制,拥有"role_teacher"、"role_admin"或者"role_student"权限的用户都可以访问这个方法意味着教师、管理员和学生角色的用户有权利执行查询错题本错题ID列表的操作确保相应权限的用户才能获取该数据。
public Result<List<ReUserExamBookVO>> getReUserExamBook(@PathVariable("examId") Integer examId){
// 通过@PathVariable注解获取路径中传入的考试ID参数将其赋值给examId变量以便服务层依据该ID准确找到对应的考试记录进而获取该考试下错题本中的错题ID列表信息。
return userBookService.getReUserExamBook(examId);
// 调用userBookService接口的getReUserExamBook方法由服务层去具体实现根据传入的考试ID获取错题本中错题ID列表的详细业务逻辑比如从数据库中查询对应考试下的错题ID并整理成列表最后将获取到的列表数据封装到Result对象中返回给客户端。
}
/**
*
* @param quId
* @return
* @param quIdID便
* @return ResultBookOneQuVO便
*/
@GetMapping("/question/single/{quId}")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/userbooks基础上添加/question/single/{quId},其中{quId}是路径变量用于接收要查询的错题的ID符合GET请求获取资源这里是获取单题详细信息资源的使用场景便于客户端根据错题ID请求获取单题详情。
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
// 进行权限控制,拥有"role_teacher"、"role_admin"或者"role_student"权限的用户都可以访问这个方法,意味着教师、管理员和学生角色的用户有权利执行查询单题的操作,确保相应权限的用户才能获取单题详情数据。
public Result<BookOneQuVO> getBookOne(@PathVariable("quId") Integer quId){
// 通过@PathVariable注解获取路径中传入的错题ID参数将其赋值给quId变量以便服务层依据该ID准确找到对应的错题记录进而获取其详细信息。
return userBookService.getBookOne(quId);
// 调用userBookService接口的getBookOne方法由服务层去具体实现根据传入的错题ID获取该错题详细信息的详细业务逻辑比如从数据库中查询对应错题的各项详细数据并封装到对应的视图对象中最后将获取到的单题详情信息封装到Result对象中返回给客户端。
}
/**
*
* @param reUserBookForm
* @return
* @param reUserBookFormReUserBookFormID便
* @return ResultAddBookAnswerVO便
*/
@PostMapping("/full-book")
// 该注解表明这个方法用于处理HTTP POST请求其请求路径是在类的基础路径/api/userbooks基础上添加/full-book用于区分其他错题本相关操作路径POST请求常用于向服务器提交数据在此处符合向服务器提交填充错题答案相关信息的场景便于客户端将填写的答案数据发送给服务器。
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
// 进行权限控制,拥有"role_teacher"、"role_admin"或者"role_student"权限的用户都可以访问这个方法,意味着教师、管理员和学生角色的用户有权利执行填充错题答案的操作,确保有权限的用户才能进行该操作。
public Result<AddBookAnswerVO> addBookAnswer(@RequestBody ReUserBookForm reUserBookForm){
// @RequestBody注解表示从请求的正文中获取数据并将其转换为ReUserBookForm类型的对象以便获取前端传递过来的填充答案相关的完整数据信息传递给服务层进行后续处理。
return userBookService.addBookAnswer(reUserBookForm);
// 调用userBookService接口的addBookAnswer方法由服务层去具体实现根据传入的填充答案相关信息进行答案填充的详细业务逻辑比如更新数据库中错题的答案字段等操作最后将操作结果以及可能的相关反馈数据封装到Result对象中返回给客户端。
}
}
}

@ -1,136 +1,193 @@
package cn.org.alan.exam.controller;
// 声明该类所在的包名,用于在项目中对类进行分类组织,表明此控制器类属于特定的“用户管理”功能模块相关的包,方便代码管理与维护
import cn.org.alan.exam.common.result.Result;
// 引入Result类通常用于统一封装业务操作后的结果信息包含操作是否成功的标识以及可能需要返回的数据等内容以标准格式返回给调用者比如前端应用方便告知客户端业务操作的执行情况及对应返回数据情况。
import cn.org.alan.exam.common.group.UserGroup;
// 引入UserGroup类大概率是用于对用户相关操作进行分组验证的类比如针对用户创建、密码修改等不同操作按照特定规则进行分组验证确保传入的数据符合相应业务场景下的合法性要求增强数据校验的准确性和针对性。
import cn.org.alan.exam.model.form.UserForm;
// 引入UserForm类这是一个数据模型类用于承载用户相关操作如创建用户、修改密码等操作时前端传递过来的用户信息像用户名、真实姓名、角色ID等具体的数据内容方便在业务逻辑中传递和处理这些数据。
import cn.org.alan.exam.model.vo.UserVO;
// 引入UserVO类这是一个视图对象Value ObjectVO用于向客户端展示特定格式的用户相关信息例如用户的基本信息、权限信息等经过整理和格式化后适合前端展示的数据便于前端展示给用户查看。
import cn.org.alan.exam.service.IUserService;
// 引入IUserService接口定义了与用户管理相关的一系列业务方法例如获取用户登录信息、创建用户、修改密码、删除用户、分页查询用户信息等操作的逻辑抽象具体的实现由对应的服务类来完成该控制器类依赖这个接口来调用相应功能。
import cn.org.alan.exam.util.AliOSSUtil;
// 引入AliOSSUtil类从名称推测可能是用于与阿里云对象存储服务OSS进行交互的工具类也许在用户上传头像等涉及文件存储的操作中会使用到方便进行文件的上传、管理等相关操作。
import cn.org.alan.exam.util.SecurityUtil;
// 引入SecurityUtil类应该是与安全相关的工具类可能用于处理用户认证、授权或者密码加密等安全相关的逻辑辅助保障系统的安全性在获取用户登录信息、修改密码等操作中或许会发挥作用。
import com.baomidou.mybatisplus.core.metadata.IPage;
// 引入IPage类这是MyBatis Plus框架提供的用于表示分页数据的类型在涉及分页查询用户信息等业务逻辑时用于承载分页后的用户数据等内容便于对分页数据进行统一的处理和传递。
import jakarta.annotation.Resource;
// 用于进行资源注入的注解作用是将实现了IUserService接口的具体实例注入到当前的UserController类中使得类中的方法可以便捷地调用对应的服务方法来实现具体业务逻辑确保业务功能的正常实现。
import org.springframework.security.access.prepost.PreAuthorize;
// 用于在方法级别进行权限控制的注解,依据配置的权限表达式判断当前用户是否具备相应权限来访问对应的方法,以此保障系统资源只能被授权用户操作,增强系统的安全性和权限管理,这里针对不同的用户操作明确了有权限执行的角色。
import org.springframework.validation.annotation.Validated;
// 结合具体的验证规则由UserGroup中不同分组定义对传入方法的参数进行数据合法性验证保证接收到的参数符合业务要求避免非法数据进入业务逻辑处理流程提高数据质量和业务操作的准确性。
import org.springframework.web.bind.annotation.*;
// 通配符导入包含了众多Spring Web相关的注解例如用于定义请求处理方法的不同请求方式注解如@GetMapping、@PostMapping等以及处理请求路径、请求参数等相关的注解方便在控制器类中构建API接口处理来自客户端的各种请求。
import org.springframework.web.multipart.MultipartFile;
// 引入MultipartFile类用于处理文件上传相关操作在用户上传头像、Excel导入用户数据等功能中会使用到方便接收前端上传的文件内容。
import java.util.Objects;
// 引入Objects类它提供了一些用于操作对象的实用方法虽然在这里暂时未看到直接使用它的地方但在一些涉及对象比较、判空等操作时可能会用到。
/**
*
*
* @Author WeiJin
* @Version 1.0
* @Date 2024/3/25 15:50
*/
//用户管理
// 这里是对该类功能的简单描述,表明这个类主要负责处理与用户相关的各种管理操作,比如获取用户登录信息、创建用户、修改密码、删除用户、分页查询用户信息以及涉及用户的一些特殊操作(如加入班级、上传头像等)。
//@Author WeiJin
//@Version 1.0
//@Date 2024/3/25 15:50
//
@RestController
// 这是Spring框架提供的复合注解兼具@Controller和@ResponseBody的功能。意味着这个类是Spring MVC中的控制器类并且类中方法的返回值默认会直接以JSON等格式响应给客户端无需额外配置视图解析相关操作方便与前端进行数据交互。
@RequestMapping("/api/user")
// 用于给整个控制器类下的所有请求处理方法设置一个公共的请求路径前缀表明此类中所有处理请求的方法对应的URL路径都将以/api/user开头便于统一管理和组织与用户管理相关的一组API接口路径。
public class UserController {
@Resource
// 使用@Resource注解来进行依赖注入让Spring容器查找并注入一个实现了IUserService接口的实例到当前类中方便后续调用相关业务方法确保业务逻辑能够顺利执行。
private IUserService iUserService;
/**
*
*
* @return
*/
// 获取用户登录信息
// @return 响应结果返回一个Result类型的对象用于告知客户端获取用户登录信息操作是否成功以及可能附带的相关提示信息等其内部封装了操作结果标识以及获取到的用户登录相关信息以UserVO形式等内容便于前端展示给当前登录用户查看自身信息。
@GetMapping("/info")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/user基础上添加/info即/api/user/info用于获取用户登录信息符合GET请求获取资源的使用场景便于客户端发起获取自身登录信息的请求。
@PreAuthorize("hasAnyAuthority('role_student','role_teacher','role_admin')")
// 进行权限控制,拥有"role_student"(学生角色)、"role_teacher"(教师角色)或者"role_admin"(管理员角色)权限的用户都可以访问这个方法,意味着只要登录的这几种角色的用户都有权利获取自己的登录信息,确保信息的安全性和访问权限的合理性。
public Result<UserVO> info() {
// 方法内部直接调用通过依赖注入获取的iUserService接口实例的info方法由服务层去具体实现获取用户登录信息的详细业务逻辑比如从数据库中查询当前登录用户的基本信息、权限信息等相关数据并封装成对应的视图对象最后将查询结果封装到Result对象中返回给客户端。
return iUserService.info();
}
/**
*
*
* @param userForm [id]
* @return
*/
//创建用户,教师只能创建学生,管理员可以创建教师和学生
//@param userForm 请求参数,用户名、真实姓名[、角色id]这是一个UserForm类型的参数用于承载前端传来的创建用户时需要填写的各种详细信息像用户名、真实姓名是必填的基本信息角色ID根据不同创建者教师或管理员有不同的可选值范围并且会依据UserGroup.CreateUserGroup.class指定的验证规则进行数据合法性验证确保传入的数据符合创建用户的要求。
// @return 响应结果返回一个Result类型的对象用于告知客户端创建用户操作是否成功以及可能附带的相关提示信息等其内部封装了操作结果标识以及其他有关创建用户的反馈信息等内容便于前端知晓创建操作是否成功及获取相应提示。
@PostMapping
// 该注解表明这个方法用于处理HTTP POST请求其请求路径就是类级别定义的基础路径/api/user因为这里没有额外指定路径POST请求常用于向服务器提交数据在此处符合向服务器提交创建用户相关信息的场景便于客户端将创建用户的信息发送给服务器。
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"(教师角色)或者"role_admin"(管理员角色)权限的用户才能访问这个方法,意味着只有这两种角色的用户有权利创建新用户,确保用户创建操作的权限安全性,防止非法创建用户。
public Result<String> createUser(@Validated(UserGroup.CreateUserGroup.class) @RequestBody UserForm userForm) {
// @Validated结合@RequestBody注解@RequestBody表示从请求的正文中获取数据并将其转换为UserForm类型的对象@Validated按照指定的创建用户分组验证规则对这个转换后的对象进行数据合法性验证只有验证通过的数据才会进入方法内部进行后续的创建用户业务逻辑处理。
return iUserService.createUser(userForm);
// 调用iUserService接口的createUser方法由服务层去具体实现创建用户的详细业务逻辑比如将用户信息保存到数据库中根据教师或管理员角色的不同限制创建不同类型的用户教师只能创建学生管理员可创建教师和学生最后将服务层返回的包含创建结果等信息的Result对象直接返回给客户端。
}
/**
*
*
* @param userForm
* @return
*/
// 用户修改密码
//@param userForm 入参这是一个UserForm类型的参数用于承载前端传来的修改密码时需要填写的相关信息例如原密码、新密码等内容并且会依据UserGroup.UpdatePasswordGroup.class指定的验证规则进行数据合法性验证保证传入的数据符合密码修改的业务要求。
// @return 响应结果返回一个Result类型的对象用于告知客户端修改密码操作是否成功以及可能附带的相关提示信息等其内部封装了操作结果标识以及其他与密码修改相关的反馈信息等内容便于前端知晓修改操作是否成功及获取相应提示。
@PutMapping
// 此注解指定这个方法用于处理HTTP PUT请求其请求路径就是类的基础路径/api/userPUT请求常用于更新资源这里符合用户更新自己密码的操作场景便于客户端向服务器提交密码修改相关信息进行密码更新。
@PreAuthorize("hasAnyAuthority('role_student','role_teacher','role_admin')")
// 进行权限控制,拥有"role_student"、"role_teacher"或者"role_admin"权限的用户都可以访问这个方法,意味着这几种角色的用户都有权利修改自己的密码,确保用户对自身密码管理的权限合理性。
public Result<String> updatePassword(@Validated(UserGroup.UpdatePasswordGroup.class) @RequestBody UserForm userForm) {
// 通过@Validated结合@RequestBody对userForm参数进行数据合法性验证及从请求正文获取数据转换为对应对象只有符合密码修改验证规则的数据才进入后续业务逻辑处理。
return iUserService.updatePassword(userForm);
// 调用iUserService接口的updatePassword方法由服务层去具体实现根据传入的修改密码相关信息进行密码更新的详细业务逻辑比如验证原密码是否正确、更新数据库中用户的密码字段等操作最后将操作结果封装到Result对象中返回给客户端。
}
/**
*
*
* @param ids ids
* @return
*/
// 批量删除用户
//@param ids 字符串ids这是一个字符串类型的参数用于指定要批量删除的用户的相关标识可能是多个用户ID以某种格式拼接比如逗号分隔等情况通过这个参数可以准确找到对应的用户记录进行批量删除操作方便进行批量操作的信息传递。
// @return 相应结果返回一个Result类型的对象用于告知客户端批量删除用户操作是否成功以及可能的相关提示信息等其内部封装了操作结果标识以及其他相关的反馈信息等内容便于前端知晓删除操作是否成功及获取相应提示。
@DeleteMapping("/{ids}")
// 该注解表明这个方法用于处理HTTP DELETE请求其请求路径是在类的基础路径/api/user基础上添加/{ids},其中{ids}是路径变量用于接收要批量删除的用户的相关标识信息DELETE请求常用于删除资源这里符合批量删除指定用户的操作场景便于客户端根据用户标识发起删除请求。
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法,意味着只有教师或管理员角色的用户有权利执行批量删除用户的操作,确保删除用户操作的权限安全性,防止非法删除用户数据。
public Result<String> deleteBatchByIds(@PathVariable("ids") String ids) {
// 通过@PathVariable注解获取路径中传入的要批量删除的用户相关标识参数将其传递给服务层以便服务层依据这些信息执行批量删除对应的用户记录的业务逻辑。
return iUserService.deleteBatchByIds(ids);
// 调用iUserService接口的deleteBatchByIds方法由服务层去具体实现根据传入的用户标识批量删除用户的详细业务逻辑比如从数据库中移除对应的多条用户记录等最后将操作结果封装到Result对象中返回给客户端。
}
/**
* Excel
*
* @param file
* @return
*/
// Excel导入用户数据
//@param file 文件这是一个MultipartFile类型的参数用于接收前端上传的包含用户数据的Excel文件服务层会对该文件进行解析等操作来批量导入用户数据到系统中方便批量添加用户信息提高添加效率。
// @return 响应结果返回一个Result类型的对象用于告知客户端Excel导入用户数据操作是否成功以及可能附带的相关提示信息等其内部封装了操作结果标识以及其他与导入操作相关的反馈信息等内容便于前端知晓导入是否成功及获取相应提示。
@PostMapping("/import")
// 表明这个方法用于处理HTTP POST请求其请求路径是在类级别定义的基础路径/api/user基础上添加/import用于区分其他用户管理相关操作路径POST请求常用于向服务器提交数据在此处符合向服务器提交Excel文件用于导入用户数据的场景便于客户端上传文件进行导入操作。
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"或者"role_admin"权限的用户才能访问这个方法意味着只有教师或管理员角色的用户有权利执行Excel导入用户数据的操作确保导入操作的权限安全性防止非法导入数据。
public Result<String> importUsers(@RequestParam("file") MultipartFile file) {
// 通过@RequestParam("file")注解获取前端上传的文件将其赋值给file变量以便服务层依据该文件进行后续的用户数据解析和导入业务逻辑处理。
return iUserService.importUsers(file);
// 调用iUserService接口的importUsers方法由服务层去具体实现根据传入的Excel文件解析并导入用户数据的详细业务逻辑比如读取文件内容、转换数据格式、插入数据库等操作最后将操作结果封装到Result对象中返回给客户端。
}
/**
*
*
* @param code
* @return
*/
// 用户加入班级,只有学生才能加入班级
// @param code 班级口令,这是一个字符串类型的参数,用于指定要加入班级的口令信息,学生通过输入正确的班级口令来申请加入对应的班级,方便进行班级加入的验证操作,确保只有知道口令的学生能加入相应班级。
// @return 响应返回一个Result类型的对象用于告知客户端用户加入班级操作是否成功以及可能附带的相关提示信息等其内部封装了操作结果标识以及其他与班级加入相关的反馈信息等内容便于前端知晓加入操作是否成功及获取相应提示。
@PutMapping("/grade/join")
// 表示这个方法用于处理HTTP PUT请求其请求路径是在类的基础路径/api/user基础上添加/grade/joinPUT请求常用于更新资源这里符合学生更新自己班级归属即加入班级的操作场景便于客户端向服务器提交班级口令等信息进行班级加入操作。
@PreAuthorize("hasAnyAuthority('role_student')")
// 进行权限控制,只有拥有"role_student"(学生角色)权限的用户才能访问这个方法,意味着只有学生角色的用户有权利执行加入班级的操作,确保班级加入操作的权限合理性,防止其他角色非法操作班级成员信息。
public Result<String> joinGrade(@RequestParam("code") String code) {
// 通过@RequestParam("code")注解获取请求中名为"code"的班级口令参数将其赋值给code变量以便服务层依据该口令信息进行后续的验证和班级加入业务逻辑处理。
return iUserService.joinGrade(code);
// 调用iUserService接口的joinGrade方法由服务层去具体实现根据传入的班级口令进行验证并将学生加入相应班级的详细业务逻辑比如验证口令是否正确、更新数据库中用户与班级的关联关系等操作最后将操作结果封装到Result对象中返回给客户端。
}
/**
*
*
* @param pageNum
* @param pageSize
* @param gradeId Id
* @param realName
* @return
*/
// @param pageNum 页码是一个整数类型的参数用于指定要查询的分页数据的页码比如pageNum为1表示查询第一页的数据客户端可以根据需要传入相应页码来获取不同页的用户信息该参数为可选参数若未传入则默认值为1方便客户端灵活控制查询的页面位置以获取期望页面的用户数据展示。
//@param pageSize 每页记录数同样是整数类型参数用于设定每页显示的用户记录数量默认值为10表示如果客户端没有指定每页显示多少条记录就按照每页10条来进行分页查询便于控制每页展示的用户信息量满足不同展示需求。
// @param gradeId 班级Id是一个整数类型的可选参数用于根据班级的唯一标识符进行筛选查询通过传入班级ID可获取该班级下的用户信息方便教师、管理员按班级维度查看用户情况若不传入该参数则可能查询所有班级的用户信息具体看服务层实现逻辑
// @param realName 真实姓名,是一个字符串类型的可选参数,用于根据用户的真实姓名进行模糊查询等筛选操作,客户端可以传入用户真实姓名的部分或全部内容来查找符合条件的用户信息,便于精准查找特定用户或符合姓名特征的一批用户信息。
//@return 响应结果返回一个Result类型的对象其中封装了IPage<UserVO>类型的数据IPage用于承载分页后的用户实体对应的视图对象数据整体通过Result返回给客户端告知分页查询的结果以及相应的用户数据信息方便前端展示和使用查询到的用户分页数据。
@GetMapping("/paging")
// 表示这个方法用于处理HTTP GET请求其请求路径是在类的基础路径/api/user基础上添加/paging即/api/user/paging用于获取分页后的用户信息符合GET请求获取资源的使用场景便于客户端发起按条件分页查询用户数据的请求。
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,拥有"role_teacher"(教师角色)或者"role_admin"(管理员角色)权限的用户都可以访问这个方法,意味着教师和管理员角色的用户有权利执行分页查询用户信息的操作,确保有权限的用户才能获取相关数据,保障数据访问的安全性和合理性。
public Result<IPage<UserVO>> pagingUser(@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "gradeId", required = false) Integer gradeId,
@RequestParam(value = "realName", required = false) String realName) {
// 通过@RequestParam注解分别获取请求中的pageNum、pageSize、gradeId、realName参数若客户端未传入pageNum和pageSize则分别使用默认值1和10gradeId用于按班级筛选realName用于按真实姓名筛选将获取到的参数传递给服务层进行后续的分页查询逻辑处理。
return iUserService.pagingUser(pageNum, pageSize, gradeId, realName);
// 调用通过依赖注入获取的iUserService接口实例的pagingUser方法由服务层去具体实现根据传入的页码、每页记录数以及班级ID、真实姓名等筛选条件进行分页查询用户信息的详细业务逻辑比如从数据库中按照条件筛选并查询用户数据进行分页处理后封装成对应的视图对象最后将查询结果封装到Result对象中返回给客户端。
}
/**
*
*
* @param file
* @return
*/
///用户上传头像
// @param file 文件这是一个MultipartFile类型的参数用于接收前端上传的用户头像文件MultipartFile类型方便处理文件上传相关操作可获取文件的各种属性如文件名、文件内容等以便后续进行头像文件的保存、处理等业务逻辑。
// @return 响应结果返回一个Result类型的对象用于告知客户端用户上传头像操作是否成功以及可能附带的相关提示信息等其内部封装了操作结果标识以及其他与头像上传相关的反馈信息比如头像保存后的访问地址等内容具体看业务逻辑等内容便于前端知晓上传操作是否成功及获取相应提示。
@PutMapping("/uploadAvatar")
// 此注解指定这个方法用于处理HTTP PUT请求其请求路径是在类的基础路径/api/user基础上添加/uploadAvatarPUT请求常用于更新资源这里符合用户更新自己头像即上传新头像覆盖旧头像的操作场景便于客户端向服务器提交头像文件进行更新操作。
@PreAuthorize("hasAnyAuthority('role_student','role_teacher','role_admin')")
// 进行权限控制,拥有"role_student"(学生角色)、"role_teacher"(教师角色)或者"role_admin"(管理员角色)权限的用户都可以访问这个方法,意味着这几种角色的用户都有权利执行上传头像的操作,确保有权限的用户才能进行头像更新,保障头像管理的权限合理性。
public Result<String> uploadAvatar(@RequestPart("file") MultipartFile file) {
// 通过@RequestPart("file")注解获取前端上传的文件并将其赋值给file变量以便服务层依据该文件进行后续的头像文件保存、处理等业务逻辑比如将文件存储到指定位置更新数据库中用户头像相关的记录等操作。
return iUserService.uploadAvatar(file);
// 调用iUserService接口的uploadAvatar方法由服务层去具体实现根据传入的头像文件进行上传处理的详细业务逻辑例如利用相关工具类可能是前面引入的AliOSSUtil等将文件上传到存储服务获取文件存储后的相关信息最后将操作结果以及相关反馈数据封装到Result对象中返回给客户端。
}
}
}

@ -1,11 +1,23 @@
package cn.org.alan.exam.converter;
// 声明该接口所在的包名用于在项目中对类和接口进行分类组织表明此接口属于特定的“exam”项目下与数据转换相关的功能模块所在的包方便代码管理和维护。
import cn.org.alan.exam.model.entity.Certificate;
// 引入Certificate实体类这个类通常对应数据库中的一张表结构用于持久化存储证书相关的信息包含如证书名称、颁发机构、有效期等具体的属性字段代表了证书在系统中的实际数据存储形式。
import cn.org.alan.exam.model.form.CertificateForm;
// 引入CertificateForm类这是一种数据传输对象Data Transfer ObjectDTO形式的类一般用于在前端与后端之间或者不同业务层之间传递数据它承载着与证书相关的输入信息比如用户在创建或更新证书时填写的证书名称等数据方便进行数据的交互和传递。
import org.mapstruct.Mapper;
// 引入Mapstruct框架提供的Mapper注解用于标识这个接口是一个映射器接口Mapstruct会根据接口中定义的方法和映射规则在编译期自动生成对应的实现类来实现不同对象之间属性的转换简化了对象转换的代码编写工作。
import org.mapstruct.Mapping;
// 引入Mapstruct框架提供的Mapping注解用于在对象转换方法中明确指定源对象和目标对象之间具体属性的映射关系比如指定某个源属性对应到目标对象的哪个属性上使得属性的转换更加精确和可控。
import org.mapstruct.Mappings;
// 引入Mapstruct框架提供的Mappings注解它是一个容器注解用于包含多个@Mapping注解当需要定义多个属性的映射关系时就可以使用它来统一管理这些映射规则使代码结构更清晰。
import org.springframework.stereotype.Component;
// 引入Spring框架的@Component注解用于将这个接口标记为一个Spring组件这样Spring容器在进行组件扫描时就能发现并管理它便于后续在需要的地方通过依赖注入等方式使用该接口对应的功能。
/**
* @ Author JinXi
@ -13,11 +25,15 @@ import org.springframework.stereotype.Component;
* @ Date 2024/5/11 14:40
*/
@Component
// 将该接口标记为Spring组件使其能被Spring容器管理从而可以利用Spring的依赖注入等功能在其他类中方便地使用这个接口所定义的对象转换功能。
@Mapper(componentModel="spring")
// 使用@Mapper注解声明这是一个映射器接口并指定componentModel属性为"spring"告知Mapstruct生成的实现类要以Spring组件的形式进行管理方便与Spring框架集成使其可以像普通的Spring Bean一样被注入和使用。
public interface CertificateConverter {
@Mappings({
@Mapping(target = "certificateName",source = "certificateName")
// 使用@Mappings注解包裹@Mapping注解来定义属性的映射规则。这里的@Mapping注解表示将源对象CertificateForm类型中的"certificateName"属性值映射到目标对象Certificate类型的"certificateName"属性上,也就是在进行对象转换时,同名的证书名称属性会进行对应赋值。
})
Certificate fromToEntity(CertificateForm certificateForm);
}
// 定义了一个名为fromToEntity的方法它接收一个CertificateForm类型的参数certificateForm作用是根据定义好的映射规则上面的@Mappings注解中指定的将CertificateForm对象转换为Certificate实体对象方便在业务逻辑中例如将前端传入的证书相关表单数据转换为可持久化存储到数据库的实体对象形式。
}

@ -1,16 +1,35 @@
package cn.org.alan.exam.converter;
// 声明该接口所在的包名用于在项目中对类和接口进行分类组织表明此接口属于特定的“exam”项目下与数据转换相关的功能模块所在的包方便代码管理与维护。
import cn.org.alan.exam.model.entity.Exam;
// 引入Exam实体类它对应数据库中存储考试相关信息的表结构包含如考试名称、考试时间、考试总分等各种与考试相关的属性代表了考试在系统中的实际数据存储形式是业务操作中涉及考试数据持久化的核心对象。
import cn.org.alan.exam.model.entity.ExamQuestion;
// 引入ExamQuestion实体类用于表示考试题目相关的实体信息比如题目内容、题目类型选择题、填空题等、所属考试等属性体现了考试题目在数据库中的存储结构和相关关联关系与考试的具体题目内容相关。
import cn.org.alan.exam.model.entity.Option;
// 引入Option实体类通常用于表示题目选项相关的实体信息比如选择题的各个选项内容、选项是否正确等属性是针对有选项的题目如选择题在数据库中的具体数据存储形式与题目选项相关的数据操作有关。
import cn.org.alan.exam.model.form.exam.ExamAddForm;
// 引入ExamAddForm类这是一种数据传输对象Data Transfer ObjectDTO形式的类主要用于在前端与后端之间或者不同业务层之间传递数据它承载着添加考试时前端输入的各种信息像考试名称、考试时长等添加考试需要填写的数据方便进行数据的交互和传递专用于考试添加场景。
import cn.org.alan.exam.model.form.exam.ExamUpdateForm;
// 引入ExamUpdateForm类同样是数据传输对象DTO用于承载更新考试信息时前端传入的相关数据例如修改后的考试名称、时间等内容为考试信息更新操作提供要修改的数据来源专用于考试更新场景。
import cn.org.alan.exam.model.vo.exam.*;
// 引入多个以“exam”命名空间下的视图对象Value ObjectVO这些类用于向客户端展示特定格式的考试相关信息不同的VO类对应不同的业务场景比如展示考试分页信息、考试详情信息、题目信息等方便前端按照合适的格式展示数据给用户查看。
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
// 引入MyBatis Plus框架提供的Page类它用于表示分页数据的结构包含了分页相关的信息如页码、每页记录数、总记录数等以及具体的数据列表在涉及分页查询考试相关信息并转换展示格式时会用到这个类型。
import org.mapstruct.Mapper;
// 引入Mapstruct框架提供的Mapper注解用于标识这个接口是一个映射器接口Mapstruct会根据接口中定义的方法和映射规则在编译期自动生成对应的实现类来实现不同对象之间属性的转换简化了对象转换的代码编写工作提高开发效率。
import org.springframework.stereotype.Component;
// 引入Spring框架的@Component注解用于将这个接口标记为一个Spring组件这样Spring容器在进行组件扫描时就能发现并管理它便于后续在需要的地方通过依赖注入等方式使用该接口对应的功能实现组件化管理。
import java.util.List;
// 引入Java标准库中的List接口用于表示列表类型的数据结构在需要返回多个同类型元素如多个考试题目信息列表、多个选项信息列表等情况时会用到便于对一组数据进行统一操作和传递。
/**
* @Author Alan
@ -18,24 +37,35 @@ import java.util.List;
* @Date 2024/4/1 3:18 PM
*/
@Component
// 将该接口标记为Spring组件使其能被Spring容器管理从而可以利用Spring的依赖注入等功能在其他类中方便地使用这个接口所定义的对象转换功能便于集成到整个Spring项目体系中。
@Mapper(componentModel="spring")
// 使用@Mapper注解声明这是一个映射器接口并指定componentModel属性为"spring"告知Mapstruct生成的实现类要以Spring组件的形式进行管理方便与Spring框架集成使其可以像普通的Spring Bean一样被注入和使用实现对象转换功能与Spring的无缝对接。
public interface ExamConverter {
Page<ExamVO> pageEntityToVo(Page<Exam> examPage);
// 定义了一个名为pageEntityToVo的方法它接收一个Page<Exam>类型的参数examPage作用是将包含Exam实体对象的分页数据结构MyBatis Plus中的Page类型里面包含了考试相关的实体数据以及分页信息按照一定的映射规则转换为包含ExamVO视图对象的分页数据结构以便将考试相关信息以适合前端展示的格式通过ExamVO来定义展示形式返回给客户端用于分页展示考试信息的场景。
Exam formToEntity(ExamUpdateForm examUpdateForm);
// 定义了一个名为formToEntity的方法它接收一个ExamUpdateForm类型的参数examUpdateForm功能是依据定义好的映射规则由Mapstruct自动生成实现类时根据对象属性名等规则确定也可手动配置更精确的映射将用于更新考试信息的ExamUpdateForm对象转换为Exam实体对象方便后续将更新后的考试数据持久化到数据库中适用于考试信息更新的业务逻辑场景。
Exam formToEntity(ExamAddForm examAddForm);
// 同样是一个将数据传输对象转换为实体对象的方法接收ExamAddForm类型的参数examAddForm作用是把包含添加考试所需信息的ExamAddForm对象转换为Exam实体对象以便将前端传来的添加考试信息保存到数据库中对应考试创建添加的业务操作场景。
List<ExamDetailRespVO> listEntityToExamDetailRespVO(List<ExamQuestion> examQuestion);
// 定义了一个方法接收一个List<ExamQuestion>类型的参数examQuestion即考试题目实体对象的列表目的是按照特定的映射规则将这些考试题目实体对象转换为ExamDetailRespVO视图对象列表用于将考试题目相关信息以适合前端展示的格式由ExamDetailRespVO定义进行展示比如在展示考试详情中的题目列表时会用到这个转换。
ExamDetailVO examToExamDetailVO(Exam exam);
// 该方法接收一个Exam实体对象作为参数exam用于将Exam实体按照相应的映射规则转换为ExamDetailVO视图对象ExamDetailVO通常会包含更详细、更适合前端展示的考试相关信息相比于原始的Exam实体对象比如考试的详细配置、关联信息等常用于向客户端展示考试详细情况的业务场景。
ExamGradeListVO entityToExamGradeListVO(Exam exam);
// 接收Exam实体对象exam按照一定的映射逻辑将其转换为ExamGradeListVO视图对象ExamGradeListVO大概率是用于展示考试与班级等相关关联信息比如某个班级参与此次考试的情况等具体看其内部定义的格式方便在涉及班级与考试关联展示的业务场景中使用。
ExamQuestionVO examQuestionEntityToVO(ExamQuestion examQuestion);
// 接收ExamQuestion实体对象examQuestion将其转换为ExamQuestionVO视图对象把考试题目实体的相关数据以适合前端展示的格式由ExamQuestionVO定义进行转换比如对题目内容、题目类型等属性进行整理和格式化便于前端展示具体的题目信息。
List<ExamQuestionVO> examQuestionListEntityToVO(List<ExamQuestion> examQuestion);
// 接收考试题目实体对象列表examQuestion把列表中的每个ExamQuestion实体按照相应映射规则转换为ExamQuestionVO视图对象并最终返回一个由这些ExamQuestionVO对象组成的列表用于批量将考试题目实体转换为适合前端展示的题目视图对象列表比如在展示一场考试的所有题目时会用到这个转换。
List<OptionVO> opListEntityToVO(List<Option> examQuestion);
}
// 这里参数名可能不太准确应该是List<Option> options更合适些它接收一个选项实体对象列表examQuestion实际应为表示选项的List<Option>类型作用是将这些选项实体对象按照特定映射规则转换为OptionVO视图对象列表用于将题目选项相关信息以适合前端展示的格式由OptionVO定义进行展示例如展示选择题的各个选项内容等情况。
}

@ -1,13 +1,29 @@
package cn.org.alan.exam.converter;
// 声明该接口所在的包名用于在项目中对类和接口进行分类组织表明此接口属于特定的“exam”项目下与数据转换相关的功能模块所在的包方便代码管理与维护让代码结构更清晰易于查找和理解不同功能模块的代码。
import cn.org.alan.exam.model.entity.ExerciseRecord;
// 引入ExerciseRecord实体类它通常对应数据库中存储练习记录相关信息的表结构包含了如练习的题目ID、用户作答情况、练习时间等具体属性代表了练习记录在系统中的实际数据存储形式是业务操作中涉及练习记录数据持久化的关键对象。
import cn.org.alan.exam.model.form.ExerciseFillAnswerFrom;
// 引入ExerciseFillAnswerFrom类这是一种数据传输对象Data Transfer ObjectDTO形式的类一般用于在前端与后端之间或者不同业务层之间传递数据它承载着用户填写练习答案相关的输入信息像题目ID、作答的答案内容等具体数据方便进行数据的交互和传递为后续处理练习作答相关业务提供数据来源。
import cn.org.alan.exam.model.vo.QuestionVO;
// 引入QuestionVO类这是一个视图对象Value ObjectVO用于向客户端展示特定格式的题目相关信息比如题目内容、题目类型、所属章节等经过整理和格式化后适合前端展示的数据便于前端展示给用户查看题目详情等情况。
import cn.org.alan.exam.model.vo.exercise.AnswerInfoVO;
// 引入AnswerInfoVO类同样是视图对象VO大概率用于向客户端展示答案相关的详细信息例如答案的正确性、解析内容、得分情况等具体的与答案有关的展示数据方便在前端呈现给用户查看作答后的反馈信息。
import org.mapstruct.Mapper;
// 引入Mapstruct框架提供的Mapper注解用于标识这个接口是一个映射器接口Mapstruct会根据接口中定义的方法和映射规则在编译期自动生成对应的实现类来实现不同对象之间属性的转换这样开发者就无需手动编写大量繁琐的对象属性赋值代码简化了对象转换的开发流程提高效率。
import org.mapstruct.Mapping;
// 引入Mapstruct框架提供的Mapping注解用于在对象转换方法中明确指定源对象和目标对象之间具体属性的映射关系也就是定义了从源对象的哪个属性取值赋给目标对象的哪个属性使得属性的转换更加精确、可控符合业务逻辑要求。
import org.mapstruct.Mappings;
// 引入Mapstruct框架提供的Mappings注解它是一个容器注解用于包含多个@Mapping注解当需要定义多个属性的映射关系时使用它来统一管理这些映射规则使代码结构更清晰一目了然地看到所有的属性映射配置情况。
import org.springframework.stereotype.Component;
// 引入Spring框架的@Component注解用于将这个接口标记为一个Spring组件这样Spring容器在进行组件扫描时就能发现并管理它便于后续在需要的地方通过依赖注入等方式使用该接口对应的功能实现了组件化管理让接口能更好地融入Spring框架的整体架构中。
/**
* @Author WeiJin
@ -15,13 +31,19 @@ import org.springframework.stereotype.Component;
* @Date 2024/5/6 10:15
*/
@Component
// 将该接口标记为Spring组件使其可以被Spring容器纳入管理范围进而可以利用Spring的依赖注入机制在其他需要进行对象转换操作的类中方便地注入并使用这个接口所定义的转换功能增强了代码的可维护性和扩展性。
@Mapper(componentModel="spring")
// 使用@Mapper注解声明这是一个映射器接口并指定componentModel属性为"spring"告知Mapstruct生成的实现类要以Spring组件的形式进行管理方便与Spring框架集成让生成的实现类可以像普通的Spring Bean一样参与到整个项目的依赖注入和生命周期管理中便于在业务逻辑中灵活调用。
public interface ExerciseConverter {
@Mappings({
@Mapping(source = "quId",target = "questionId"),
@Mapping(source = "quType",target = "questionType")
})
// 使用@Mappings注解包裹着两个@Mapping注解来详细定义属性的映射规则。第一个@Mapping注解指定将源对象ExerciseFillAnswerFrom类型中的"quId"属性值映射到目标对象ExerciseRecord类型的"questionId"属性上也就是在进行从填写练习答案的表单数据转换为练习记录实体数据时把对应的题目ID进行传递赋值第二个@Mapping注解表示将源对象中的"quType"属性值映射到目标对象的"questionType"属性,同样是为了在转换过程中正确传递题目类型信息,确保数据的一致性和准确性。
ExerciseRecord fromToEntity(ExerciseFillAnswerFrom exerciseFillAnswerFrom);
// 定义了一个名为fromToEntity的方法它接收一个ExerciseFillAnswerFrom类型的参数exerciseFillAnswerFrom其作用是根据上面定义好的映射规则通过@Mappings注解指定的将ExerciseFillAnswerFrom对象转换为ExerciseRecord实体对象方便后续将用户填写的练习答案相关信息以符合数据库存储要求的实体形式ExerciseRecord持久化保存到数据库中应用于处理练习作答记录保存的业务场景。
AnswerInfoVO quVOToAnswerInfoVO(QuestionVO questionVO);
}
// 定义了一个名为quVOToAnswerInfoVO的方法它接收一个QuestionVO类型的参数questionVO功能是按照某种可能是基于属性名称匹配等默认规则或者未来可扩展的自定义规则具体看业务需求和实现情况映射逻辑将QuestionVO对象转换为AnswerInfoVO对象目的是把题目相关信息通过QuestionVO展示的转换为答案相关的详细信息通过AnswerInfoVO展示以便在前端展示给用户更全面的关于答案的情况如答案解析、正确性判断等常用于展示作答反馈信息的业务场景。
}

@ -1,13 +1,26 @@
package cn.org.alan.exam.converter;
// 声明该接口所在的包名用于在项目中对类和接口进行分类组织表明此接口属于特定的“exam”项目下与数据转换相关的功能模块所在的包方便代码管理与维护使得不同功能的代码能各归其位便于查找和理解整个项目的代码结构。
import cn.org.alan.exam.model.entity.Grade;
// 引入Grade实体类它对应数据库中存储班级相关信息的表结构包含如班级名称、班级编号、所属年级等具体的属性字段代表了班级在系统中的实际数据存储形式是业务操作中涉及班级数据持久化以及相关业务逻辑处理的核心数据对象。
import cn.org.alan.exam.model.form.GradeForm;
// 引入GradeForm类这是一种数据传输对象Data Transfer ObjectDTO形式的类主要用于在前端与后端之间或者不同业务层之间传递数据它承载着与班级相关操作比如创建班级、更新班级信息时前端输入的各种信息像班级名称、班主任等需要填写的数据方便进行数据的交互和传递为后续业务处理提供数据基础。
import cn.org.alan.exam.model.vo.GradeVO;
// 引入GradeVO类这是一个视图对象Value ObjectVO用于向客户端展示特定格式的班级相关信息例如班级名称、班级人数、班级平均成绩如果有相关业务关联的话等经过整理和格式化后适合前端展示的数据便于前端展示给用户查看班级的具体情况。
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
// 引入MyBatis Plus框架提供的Page类它用于表示分页数据的结构包含了分页相关的信息如页码、每页记录数、总记录数等以及具体的数据列表在涉及分页查询班级相关信息并转换展示格式时会用到这个类型方便对分页数据进行统一管理和操作。
import org.mapstruct.Mapper;
// 引入Mapstruct框架提供的Mapper注解用于标识这个接口是一个映射器接口Mapstruct会根据接口中定义的方法和映射规则在编译期自动生成对应的实现类来实现不同对象之间属性的转换这样开发者无需手动编写大量重复的对象属性赋值代码简化了对象转换相关的开发工作提高了代码开发效率。
import org.springframework.stereotype.Component;
// 引入Spring框架的@Component注解用于将这个接口标记为一个Spring组件使得Spring容器在进行组件扫描时能够发现并管理它便于后续在需要的地方通过依赖注入等方式使用该接口对应的功能实现了组件化管理让这个接口能更好地融入到整个Spring项目的体系架构中。
import java.util.List;
// 引入Java标准库中的List接口用于表示列表类型的数据结构在需要返回多个同类型元素如多个班级的VO列表、多个班级实体列表等情况时会用到便于对一组数据进行统一的操作和传递符合常见的数据处理需求。
/**
* @Author Alan
@ -15,14 +28,20 @@ import java.util.List;
* @Date 2024/3/28 2:03 PM
*/
@Component
// 将该接口标记为Spring组件目的是让Spring容器管理这个接口进而可以在其他类中通过Spring的依赖注入机制方便地获取该接口的实例调用其定义的对象转换方法以此增强代码的可维护性和扩展性使代码模块之间的耦合度更低。
@Mapper(componentModel="spring")
// 使用@Mapper注解声明这是一个映射器接口并指定componentModel属性为"spring"告知Mapstruct生成的实现类要以Spring组件的形式进行管理方便与Spring框架集成使得生成的实现类可以像普通的Spring Bean一样参与到整个项目的依赖注入、生命周期管理等流程中便于在业务逻辑中灵活运用。
public interface GradeConverter {
Page<GradeVO> pageEntityToVo(Page<Grade> page);
// 定义了一个名为pageEntityToVo的方法它接收一个Page<Grade>类型的参数page这里的Page<Grade>表示包含了班级Grade实体对象的分页数据结构由MyBatis Plus的Page类型承载包含了班级实体以及分页相关的信息。该方法的作用是按照一定的映射规则将这个包含班级实体的分页数据转换为包含GradeVO视图对象的分页数据结构以便把班级相关信息以适合前端展示的格式通过GradeVO来定义展示形式返回给客户端常用于分页展示班级信息的业务场景例如在班级列表分页查询结果展示时进行数据转换。
Grade formToEntity(GradeForm gradeForm);
// 定义了名为formToEntity的方法它接收一个GradeForm类型的参数gradeForm功能是依据一定的映射规则由Mapstruct自动生成实现类时根据对象属性名等规则确定也可手动配置更精确的映射将用于传递班级相关操作数据的GradeForm对象转换为Grade实体对象方便后续将前端传来的班级创建或更新等操作的信息保存到数据库中对应班级信息创建或更新的业务操作场景确保数据能正确持久化。
List<GradeVO> listEntityToVo(List<Grade> page);
GradeVO GradeToGradeVO(Grade grade);
// 定义的这个方法接收一个List<Grade>类型的参数page也就是班级实体对象的列表其目的是按照特定的映射规则将这些班级实体对象转换为GradeVO视图对象列表用于将班级相关信息以适合前端展示的格式由GradeVO定义进行批量展示比如在展示多个班级的基本信息时把从数据库查询出来的班级实体列表转换为适合前端展示的视图对象列表后返回给前端进行展示。
}
GradeVO GradeToGradeVO(Grade grade);
// 该方法接收一个Grade实体对象作为参数grade作用是将单个的Grade实体按照相应的映射规则转换为GradeVO视图对象GradeVO通常包含了更符合前端展示需求的班级相关信息相比于原始的Grade实体对象比如对班级名称、人数等属性进行整理和格式化便于前端更友好地展示单个班级的详细情况常用于在需要展示特定班级详细信息的业务场景中。
}

@ -1,12 +1,26 @@
package cn.org.alan.exam.converter;
// 声明该接口所在的包名用于在项目中对类和接口进行分类组织表明此接口属于特定的“exam”项目下与数据转换相关的功能模块所在的包便于清晰划分代码结构让不同功能的代码归类存放方便后续的维护、查找以及理解整个项目代码逻辑。
import cn.org.alan.exam.model.entity.Notice;
// 引入Notice实体类它对应数据库中存储通知相关信息的表结构包含像通知标题、通知内容、发布时间、发布人等具体的属性字段代表了通知在系统中的实际数据存储形式是业务操作中涉及通知数据持久化以及围绕通知展开的各类业务逻辑处理的基础数据对象。
import cn.org.alan.exam.model.form.NoticeForm;
// 引入NoticeForm类这是一种数据传输对象Data Transfer ObjectDTO形式的类主要用于在前端与后端之间或者不同业务层之间传递数据它承载着创建或更新通知时前端输入的各种信息例如通知的标题、详细内容等需要填写的数据方便进行数据的交互和传递为后续的通知相关业务处理提供数据来源。
import cn.org.alan.exam.model.vo.GradeVO;
// 引入GradeVO类这是一个视图对象Value ObjectVO通常用于向客户端展示班级相关的特定格式信息虽然在这里看起来与通知转换功能没有直接关联但可能在整个项目的更广泛场景下存在关联或者是误引入具体需看项目实际情况比如班级名称、班级人数、班级平均成绩等经过整理和格式化后适合前端展示的数据便于前端展示给用户查看班级的详细情况。
import cn.org.alan.exam.model.vo.NoticeVO;
// 引入NoticeVO类这是一个视图对象VO用于向客户端展示特定格式的通知相关信息例如对通知标题、内容进行格式化处理可能添加一些样式、截取合适长度等还有可能包含发布时间的友好展示格式等经过整理后的适合前端展示的数据便于前端以更好的形式呈现通知给用户查看。
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
// 引入MyBatis Plus框架提供的Page类它用于表示分页数据的结构包含了分页相关的信息如页码、每页记录数、总记录数等以及具体的数据列表在涉及分页查询通知相关信息并转换展示格式时会用到这个类型方便对分页的通知数据进行统一管理以及后续的展示格式转换操作。
import org.mapstruct.Mapper;
// 引入Mapstruct框架提供的Mapper注解用于标识这个接口是一个映射器接口Mapstruct会根据接口中定义的方法和映射规则在编译期自动生成对应的实现类来实现不同对象之间属性的转换这样就避免了开发者手动编写大量繁琐的对象属性赋值代码极大地简化了对象转换相关的开发工作提高了代码开发效率。
import org.springframework.stereotype.Component;
// 引入Spring框架的@Component注解用于将这个接口标记为一个Spring组件使得Spring容器在进行组件扫描时能够发现并管理它便于后续在需要的地方通过依赖注入等方式使用该接口对应的功能实现了组件化管理让这个接口能更好地融入到整个Spring项目的体系架构中方便与其他Spring组件协作完成业务功能。
/**
* @Author Alan
@ -14,12 +28,17 @@ import org.springframework.stereotype.Component;
* @Date 2024/3/28 11:04 PM
*/
@Component
// 将该接口标记为Spring组件意味着Spring容器会对其进行管理其他类可以通过Spring的依赖注入机制获取这个接口的实例进而调用接口中定义的方法来完成对象转换操作这样有助于降低代码之间的耦合度增强代码的可维护性和可扩展性使项目的代码结构更加清晰合理。
@Mapper(componentModel="spring")
// 使用@Mapper注解声明这是一个映射器接口并指定componentModel属性为"spring"告知Mapstruct生成的实现类要以Spring组件的形式进行管理方便与Spring框架集成使得生成的实现类可以像普通的Spring Bean一样参与到整个项目的依赖注入、生命周期管理等流程中便于在业务逻辑中灵活地运用这些转换功能。
public interface NoticeConverter {
Notice formToEntity(NoticeForm noticeForm);
// 定义了一个名为formToEntity的方法它接收一个NoticeForm类型的参数noticeForm其功能是依据一定的映射规则这些规则由Mapstruct根据对象属性名等默认情况确定也可以手动配置更精确的映射关系将承载前端输入通知相关数据的NoticeForm对象转换为Notice实体对象方便后续将前端传来的创建或更新通知的信息保存到数据库中对应通知创建或更新的业务操作场景确保通知数据能正确地持久化存储。
Page<NoticeVO> pageEntityToVo(Page<Notice> noticePage);
// 定义的这个方法接收一个Page<Notice>类型的参数noticePage这里的Page<Notice>表示包含了通知Notice实体对象的分页数据结构由MyBatis Plus的Page类型承载包含了通知实体以及分页相关的信息。该方法的作用是按照特定的映射规则将这个包含通知实体的分页数据转换为包含NoticeVO视图对象的分页数据结构以便把通知相关信息以适合前端展示的格式通过NoticeVO来定义展示形式返回给客户端常用于分页展示通知信息的业务场景例如在通知列表分页查询结果展示时进行数据转换操作。
NoticeVO NoticeToNoticeVO(Notice notice);
}
// 该方法接收一个Notice实体对象作为参数notice其作用是将单个的Notice实体按照相应的映射规则转换为NoticeVO视图对象NoticeVO通常包含了更符合前端展示需求的通知相关信息相比于原始的Notice实体对象比如对通知标题、内容进行更友好的格式化处理添加发布时间的合适展示格式等便于前端更美观、清晰地展示单个通知的详细情况常用于在需要展示特定通知详细信息的业务场景中。
}

@ -1,7 +1,11 @@
package cn.org.alan.exam.converter;
// 声明该接口所在的包名用于在项目中对类和接口进行分类组织表明此接口属于特定的“exam”项目下与数据转换相关的功能模块所在的包方便代码管理与维护使代码按照功能模块划分存放易于后续开发人员查找、理解以及扩展相关代码功能。
import org.mapstruct.Mapper;
// 引入Mapstruct框架提供的Mapper注解用于标识这个接口是一个映射器接口。在Mapstruct框架中通过定义这样的接口并添加相应的映射规则后续可在接口方法上通过相关注解来定义具体属性映射关系等它会在编译期自动生成对应的实现类实现不同对象之间属性的转换功能从而避免开发者手动编写大量重复且容易出错的对象属性赋值代码提高开发效率和代码质量。
import org.springframework.stereotype.Component;
// 引入Spring框架的@Component注解用于将这个接口标记为一个Spring组件。当Spring容器进行组件扫描时能够识别并管理被该注解标记的接口或类进而可以利用Spring的依赖注入等功能在其他需要使用该接口功能的地方便捷地获取其实例并调用相关方法使得代码能够更好地遵循Spring框架的组件化开发模式增强代码的可维护性和扩展性。
/**
* @Author WeiJin
@ -9,7 +13,9 @@ import org.springframework.stereotype.Component;
* @Date 2024/4/11 14:20
*/
@Component
// 将该接口标记为Spring组件这样做的目的是让Spring容器管理这个接口意味着在整个Spring项目的运行环境中该接口可以参与到依赖注入等机制里。例如其他的业务逻辑类如果需要使用这个接口所定义的数据转换功能就可以通过Spring的依赖注入方式轻松获取到该接口的实例然后调用相应的转换方法有助于构建低耦合、易维护的代码结构。
@Mapper(componentModel="spring")
// 使用@Mapper注解声明这是一个映射器接口并指定componentModel属性为"spring"。这个配置告知Mapstruct框架生成的对应实现类要以Spring组件的形式进行管理使其能够与Spring框架无缝集成。生成的实现类就如同普通的Spring Bean一样可以遵循Spring的生命周期管理、配置注入等规则方便在基于Spring的项目中灵活运用比如在不同的业务层之间进行对象数据转换操作时很容易就能获取并使用这个接口对应的转换功能。
public interface OptionConverter {
}
// 这里定义了一个空的接口虽然目前没有定义具体的方法但从接口名称以及所在的“converter”包通常用于数据转换相关功能可以推测它大概率是用于定义与选项Option可能是题目选项相关的数据对象具体需结合项目中其他相关类来看相关的数据转换功能。后续开发人员可以根据具体的业务需求在此接口中添加相应的方法并通过Mapstruct的相关注解来定义方法中涉及的对象属性之间的映射关系从而实现将一种与选项相关的数据对象转换为另一种数据对象的功能比如将前端传来的选项相关表单数据转换为可持久化存储的实体对象数据或者将实体对象中的选项数据转换为适合前端展示的视图对象数据等。
}

@ -1,16 +1,32 @@
package cn.org.alan.exam.converter;
// 声明该接口所在的包名用于在项目中对类和接口进行分类组织表明此接口属于特定的“exam”项目下与数据转换相关的功能模块所在的包方便代码管理与维护使得不同功能模块的代码在项目中条理清晰便于查找和理解代码的整体结构与逻辑。
import cn.org.alan.exam.model.entity.Question;
// 引入Question实体类它对应数据库中存储题目相关信息的表结构包含像题目内容、题目类型如选择题、填空题、简答题等、所属知识点、难度系数等具体的属性字段代表了题目在系统中的实际数据存储形式是业务操作中涉及题目数据持久化以及围绕题目展开的各类业务逻辑处理的核心数据对象。
import cn.org.alan.exam.model.form.question.QuestionFrom;
// 引入QuestionFrom类这是一种数据传输对象Data Transfer ObjectDTO形式的类主要用于在前端与后端之间或者不同业务层之间传递数据它承载着创建或更新题目时前端输入的各种信息例如题目内容、选项如果是选择题的话、答案等需要填写的数据方便进行数据的交互和传递为后续的题目相关业务处理提供数据来源。
import cn.org.alan.exam.model.vo.GradeVO;
// 引入GradeVO类这是一个视图对象Value ObjectVO通常用于向客户端展示班级相关的特定格式信息在这里看起来与题目转换功能没有直接关联但可能在整个项目更广泛的业务场景下存在关联比如题目统计可能按班级维度展示等情况像班级名称、班级人数、班级平均成绩等经过整理和格式化后适合前端展示的数据便于前端展示给用户查看班级的详细情况。
import cn.org.alan.exam.model.vo.QuestionVO;
// 引入QuestionVO类这是一个视图对象VO用于向客户端展示特定格式的题目相关信息例如对题目内容进行格式化处理可能添加一些样式、截取合适长度等展示题目类型的友好描述以及关联的知识点等经过整理后的适合前端展示的数据便于前端以更好的形式呈现题目给用户查看。
import cn.org.alan.exam.model.vo.exercise.QuestionSheetVO;
// 引入QuestionSheetVO类同样是一个视图对象VO大概率用于在练习相关场景下向客户端展示题目信息的特定格式可能包含题目在练习中的序号、题目内容以及与练习答题相关的一些特定属性等方便在练习功能模块中展示题目给用户进行作答等操作。
import org.mapstruct.Mapper;
// 引入Mapstruct框架提供的Mapper注解用于标识这个接口是一个映射器接口。Mapstruct会依据接口中定义的方法以及所添加的映射规则通过@Mapping等注解来指定具体属性的映射关系在编译期自动生成对应的实现类以此来实现不同对象之间属性的转换这样就大大简化了开发者手动编写对象属性赋值代码的工作量提高了开发效率和代码的准确性。
import org.mapstruct.Mapping;
// 引入Mapstruct框架提供的Mapping注解用于在对象转换方法中明确指定源对象和目标对象之间具体属性的映射关系也就是定义了从源对象的哪个属性取值赋给目标对象的哪个属性使得属性的转换更加精确、可控符合业务逻辑要求确保转换后的数据符合预期的展示或存储需求。
import org.springframework.stereotype.Component;
// 引入Spring框架的@Component注解用于将这个接口标记为一个Spring组件使得Spring容器在进行组件扫描时能够发现并管理它便于后续在需要的地方通过依赖注入等方式使用该接口对应的功能实现了组件化管理让这个接口能更好地融入到整个Spring项目的体系架构中方便与其他Spring组件协作完成业务功能。
import java.util.List;
// 引入Java标准库中的List接口用于表示列表类型的数据结构在需要返回多个同类型元素如多个题目信息的视图对象列表、多个实体对象列表等情况时会用到便于对一组数据进行统一的操作和传递符合常见的数据处理和展示需求。
/**
* @Author WeiJin
@ -18,16 +34,24 @@ import java.util.List;
* @Date 2024/4/1 15:46
*/
@Component
// 将该接口标记为Spring组件意味着Spring容器会对其进行管理其他类可以通过Spring的依赖注入机制获取这个接口的实例进而调用接口中定义的方法来完成对象转换操作。这样做有助于降低代码之间的耦合度增强代码的可维护性和可扩展性使项目的代码结构更加清晰合理方便在不同的业务场景中灵活运用该接口提供的数据转换功能。
@Mapper(componentModel = "spring")
// 使用@Mapper注解声明这是一个映射器接口并指定componentModel属性为"spring"告知Mapstruct生成的实现类要以Spring组件的形式进行管理方便与Spring框架集成使得生成的实现类可以像普通的Spring Bean一样参与到整个项目的依赖注入、生命周期管理等流程中便于在业务逻辑中灵活地运用这些转换功能与整个Spring项目的开发模式相契合。
public interface QuestionConverter {
@Mapping(target = "repoId",source = "repoId")
// 使用@Mapping注解来定义属性的映射规则这里表示将源对象QuestionFrom类型中的"repoId"属性值映射到目标对象Question类型的"repoId"属性上目的是在把前端传来的题目相关表单数据通过QuestionFrom承载转换为可持久化存储的题目实体数据Question类型确保"repoId"这个特定属性的值能正确传递和赋值,保证数据的一致性和完整性,以符合业务逻辑对于该属性的要求。
Question fromToEntity(QuestionFrom questionFrom);
// 定义了一个名为fromToEntity的方法它接收一个QuestionFrom类型的参数questionFrom其功能是依据上面定义好的映射规则通过@Mapping注解指定的以及Mapstruct根据对象属性名等默认情况确定的其他映射关系将QuestionFrom对象转换为Question实体对象方便后续将前端传来的创建或更新题目的信息保存到数据库中对应题目创建或更新的业务操作场景确保题目数据能正确地持久化存储。
List<QuestionSheetVO> listEntityToVO(List<Question> questions);
// 定义的这个方法接收一个List<Question>类型的参数questions也就是题目实体对象的列表。其目的是按照特定的映射规则可能由Mapstruct根据属性名等默认匹配也可后续添加更多@Mapping注解来精确配置将这些题目实体对象转换为QuestionSheetVO视图对象列表用于将题目相关信息以适合在练习相关场景下前端展示的格式由QuestionSheetVO定义进行批量展示比如在展示练习试卷中的所有题目时把从数据库查询出来的题目实体列表转换为适合前端展示的视图对象列表后返回给前端进行展示。
@Mapping(target = "quId",source = "id")
// 使用@Mapping注解指定属性映射规则这里表示将源对象Question类型中的"id"属性值映射到目标对象QuestionSheetVO类型的"quId"属性上,在将题目实体数据转换为适合在练习场景展示的视图对象数据时,保证题目唯一标识符(以不同属性名在不同对象中体现)能正确对应赋值,使得展示的数据能准确关联到对应的题目实体,符合数据展示和业务逻辑要求。
QuestionSheetVO entityToVO(Question question);
// 该方法接收一个Question实体对象作为参数question其作用是将单个的Question实体按照相应的映射规则转换为QuestionSheetVO视图对象QuestionSheetVO通常包含了更符合在练习场景下前端展示需求的题目相关信息相比于原始的Question实体对象比如对题目内容进行适合练习展示的格式化处理添加练习相关的序号等属性便于前端更友好地展示单个题目在练习中的情况常用于在练习功能模块中展示具体题目的业务场景中。
QuestionVO QuestionToQuestionVO(Question question);
}
// 该方法接收一个Question实体对象作为参数question作用是将Question实体按照一定的映射规则同样可由Mapstruct默认确定或后续精确配置转换为QuestionVO视图对象QuestionVO会对题目实体中的相关信息进行整理和格式化以更适合前端一般性展示题目详情的格式呈现出来比如展示题目详细内容、题目类型、所属知识点等信息便于前端更全面、清晰地展示单个题目的详细情况常用于在需要展示特定题目详细信息的各种业务场景中。
}

@ -1,14 +1,32 @@
package cn.org.alan.exam.converter;
// 声明该接口所在的包名用于在项目中对类和接口进行分类组织表明此接口属于特定的“exam”项目下与数据转换相关的功能模块所在的包方便代码管理与维护使代码结构更清晰不同功能的代码能各归其位便于后续开发人员理解和扩展相关功能。
import cn.org.alan.exam.model.entity.Exam;
// 引入Exam实体类它对应数据库中存储考试相关信息的表结构包含像考试名称、考试时间、考试总分、考试的题目集合等具体的属性字段代表了考试在系统中的实际数据存储形式是业务操作中涉及考试数据持久化以及围绕考试展开的各类业务逻辑处理的核心数据对象之一不过在这里暂时未看到直接与当前转换功能相关的使用可能在更广泛的项目业务关联中有作用。
import cn.org.alan.exam.model.entity.Grade;
// 引入Grade实体类对应数据库里存储班级相关信息的表结构涵盖班级名称、班级编号、所属年级等属性是班级数据在系统中的实体表现形式同样在这里暂时未体现与当前转换功能的直接关联但在整个项目大环境下可能涉及到关联查询、统计等业务场景中会有关联使用。
import cn.org.alan.exam.model.entity.Repo;
// 引入Repo实体类推测它是用于存储某种资源库相关信息的实体类比如题库等资源相关的数据存储结构包含如资源的标识、名称、内容等属性代表了对应资源在系统中的实际存储状态是当前这个数据转换功能中涉及到的关键数据源实体后续要对其进行转换操作。
import cn.org.alan.exam.model.vo.GradeVO;
// 引入GradeVO类这是一个视图对象Value ObjectVO用于向客户端展示特定格式的班级相关信息例如班级名称、班级人数、班级平均成绩等经过整理和格式化后适合前端展示的数据便于前端展示给用户查看班级的详细情况在这里可能与整体的记录相关展示场景有一定关联但暂时未看到直接参与当前转换逻辑的体现。
import cn.org.alan.exam.model.vo.record.ExamRecordVO;
// 引入ExamRecordVO类是一个视图对象VO大概率用于向客户端展示考试记录相关的详细信息比如考生在某场考试中的得分、答题情况、考试时间等具体的考试记录相关展示数据方便前端呈现给用户查看考试相关的历史记录情况在整个项目关于考试记录展示的业务场景中发挥作用。
import cn.org.alan.exam.model.vo.record.ExerciseRecordVO;
// 引入ExerciseRecordVO类同样是视图对象VO应该是用于向客户端展示练习记录相关的特定格式信息例如练习的题目、用户作答情况、练习的时长等与练习记录有关的详细数据便于前端展示给用户查看练习相关的历史情况是当前要转换得到的目标视图对象类型用于展示练习记录相关内容。
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
// 引入MyBatis Plus框架提供的Page类它用于表示分页数据的结构包含了分页相关的信息如页码、每页记录数、总记录数等以及具体的数据列表在涉及分页查询资源此处是Repo资源相关信息并转换展示格式时会用到这个类型方便对分页数据进行统一管理以及后续的展示格式转换操作使得数据展示更有条理符合常见的分页展示需求。
import org.mapstruct.Mapper;
// 引入Mapstruct框架提供的Mapper注解用于标识这个接口是一个映射器接口。Mapstruct会根据接口中定义的方法以及所添加的映射规则通过类似@Mapping注解等方式来指定具体属性的映射关系不过此处暂时未看到更多细化规则在编译期自动生成对应的实现类以此来实现不同对象之间属性的转换避免了开发者手动编写大量繁琐的对象属性赋值代码提高开发效率。
import org.springframework.stereotype.Component;
// 引入Spring框架的@Component注解用于将这个接口标记为一个Spring组件使得Spring容器在进行组件扫描时能够发现并管理它便于后续在需要的地方通过依赖注入等方式使用该接口对应的功能实现了组件化管理让这个接口能更好地融入到整个Spring项目的体系架构中方便与其他Spring组件协作完成业务功能例如在需要进行数据转换的业务逻辑层方便地注入并调用这个接口的方法。
/**
* @Author Alan
@ -16,9 +34,11 @@ import org.springframework.stereotype.Component;
* @Date 2024/4/30 11:39 AM
*/
@Component
// 将该接口标记为Spring组件意味着Spring容器会对其进行管理这样在其他业务逻辑类中如果需要使用这个接口所定义的数据转换功能就可以通过Spring的依赖注入机制轻松获取到该接口的实例然后调用相应的转换方法有助于构建低耦合、易维护的代码结构使得各个功能模块之间的依赖关系更清晰明了。
@Mapper(componentModel = "spring")
// 使用@Mapper注解声明这是一个映射器接口并指定componentModel属性为"spring"告知Mapstruct生成的实现类要以Spring组件的形式进行管理方便与Spring框架集成使得生成的实现类可以像普通的Spring Bean一样参与到整个项目的依赖注入、生命周期管理等流程中便于在业务逻辑中灵活地运用这些转换功能与整个Spring项目的运行环境和开发模式相契合方便在不同业务场景下按需调用数据转换逻辑。
public interface RecordConverter {
Page<ExerciseRecordVO> pageRepoEntityToVo(Page<Repo> page);
}
// 定义了一个名为pageRepoEntityToVo的方法它接收一个Page<Repo>类型的参数page这里的Page<Repo>表示包含了Repo实体对象的分页数据结构由MyBatis Plus的Page类型承载包含了资源库实体以及分页相关的信息。该方法的作用是按照特定的映射规则目前代码中未详细展示可能后续在生成的实现类中根据对象属性名等默认匹配或者额外配置来确定将这个包含资源库实体的分页数据转换为包含ExerciseRecordVO视图对象的分页数据结构以便把资源库相关信息以适合前端展示练习记录的格式通过ExerciseRecordVO来定义展示形式返回给客户端常用于分页展示与练习记录相关的信息基于资源库数据转换而来的业务场景例如在展示基于某个资源库生成的练习记录列表分页查询结果时进行这样的数据转换操作。
}

@ -1,11 +1,20 @@
package cn.org.alan.exam.converter;
// 声明该接口所在的包名用于在项目中对类和接口进行分类组织表明此接口属于特定的“exam”项目下与数据转换相关的功能模块所在的包方便代码管理与维护使得代码按照功能模块清晰划分便于后续开发人员查找、理解和扩展相关功能代码。
import cn.org.alan.exam.model.entity.Repo;
// 引入Repo实体类它对应数据库中存储资源库相关信息的表结构包含诸如资源库名称、资源库类型例如是题库、素材库等具体类型具体依业务而定、创建者、创建时间等具体的属性字段代表了资源库在系统中的实际数据存储形式是业务操作中涉及资源库数据持久化以及围绕资源库展开的各类业务逻辑处理的核心数据对象是后续进行数据转换操作的数据源。
import cn.org.alan.exam.model.vo.repo.RepoVO;
// 引入RepoVO类这是一个视图对象Value ObjectVO用于向客户端展示特定格式的资源库相关信息例如对资源库名称进行格式化展示可能添加一些样式、截取合适长度等展示资源库类型的友好描述以及包含一些适合前端展示的关联信息如资源数量等具体看业务需求等经过整理后的适合前端展示的数据便于前端以更好的形式呈现资源库信息给用户查看是数据转换后要生成的目标展示对象类型。
import org.mapstruct.Mapper;
// 引入Mapstruct框架提供的Mapper注解用于标识这个接口是一个映射器接口。Mapstruct会依据接口中定义的方法以及后续可能添加的映射规则通过相关注解指定具体属性的映射关系当前代码里暂时没有更详细配置在编译期自动生成对应的实现类以此来实现不同对象之间属性的转换这样就避免了开发者手动编写大量繁琐的对象属性赋值代码提高了代码开发效率和准确性让数据转换逻辑更加清晰、易于维护。
import org.springframework.stereotype.Component;
// 引入Spring框架的@Component注解用于将这个接口标记为一个Spring组件使得Spring容器在进行组件扫描时能够发现并管理它便于后续在需要的地方通过依赖注入等方式使用该接口对应的功能实现了组件化管理让这个接口能更好地融入到整个Spring项目的体系架构中方便与其他Spring组件协作完成业务功能例如在业务逻辑层需要进行资源库数据转换操作时能方便地注入并调用这个接口的方法。
import java.util.List;
// 引入Java标准库中的List接口用于表示列表类型的数据结构在需要返回多个同类型元素如多个资源库信息的视图对象列表、多个资源库实体对象列表等情况时会用到便于对一组数据进行统一的操作和传递符合这里将多个资源库实体转换为对应视图对象列表的需求方便进行批量数据转换及展示相关操作。
/**
* @Author WeiJin
@ -13,9 +22,11 @@ import java.util.List;
* @Date 2024/3/28 20:21
*/
@Component
// 将该接口标记为Spring组件意味着Spring容器会对其进行管理其他类可以通过Spring的依赖注入机制获取这个接口的实例进而调用接口中定义的方法来完成对象转换操作这样有助于降低代码之间的耦合度增强代码的可维护性和可扩展性使得整个项目的代码结构更加清晰合理方便在不同的业务场景中灵活运用该接口提供的数据转换功能。
@Mapper(componentModel = "spring")
// 使用@Mapper注解声明这是一个映射器接口并指定componentModel属性为"spring"告知Mapstruct生成的实现类要以Spring组件的形式进行管理方便与Spring框架集成使得生成的实现类可以像普通的Spring Bean一样参与到整个项目的依赖注入、生命周期管理等流程中便于在业务逻辑中灵活地运用这些转换功能使其与整个Spring项目的开发模式相契合便于在项目中按需调用数据转换逻辑。
public interface RepoConverter {
List<RepoVO> listEntityToVo(List<Repo> list);
}
// 定义了一个名为listEntityToVo的方法它接收一个List<Repo>类型的参数list也就是资源库实体对象的列表。其作用是按照特定的映射规则可能由Mapstruct根据对象属性名等默认匹配来确定也可以后续通过添加相关注解来进一步精确配置将这些资源库实体对象转换为RepoVO视图对象列表用于将资源库相关信息以适合前端展示的格式由RepoVO定义进行批量展示比如在前端展示多个资源库的基本信息或查询结果时把从数据库查询出来的资源库实体列表转换为适合前端展示的视图对象列表后返回给前端进行展示满足前端对资源库数据展示的需求对应资源库数据批量转换并展示的业务场景。
}

@ -1,12 +1,20 @@
package cn.org.alan.exam.converter;
// 声明该接口所在的包名用于在项目中对类和接口进行分类组织表明此接口属于特定的“exam”项目下与数据转换相关的功能模块所在的包方便代码管理与维护让不同功能的代码能按模块划分清晰明了便于后续开发人员查找、理解以及对相关功能进行扩展。
import cn.org.alan.exam.model.entity.UserBook;
// 引入UserBook实体类它对应数据库中存储用户错题本相关信息的表结构包含像错题本对应的用户标识、错题的相关记录例如错题的题目编号、答错次数等具体与错题相关的数据具体依业务而定等属性字段代表了用户错题本在系统中的实际数据存储形式是业务操作中涉及错题本数据持久化以及围绕错题本展开的各类业务逻辑处理的核心数据对象也是后续进行数据转换操作的数据源头。
import cn.org.alan.exam.model.vo.userbook.ReUserExamBookVO;
// 引入ReUserExamBookVO类这是一个视图对象Value ObjectVO用于向客户端展示特定格式的用户错题本中与考试错题相关的信息例如错题对应的考试名称、错题在该考试中的具体呈现形式可能包含题干、正确答案等部分展示信息具体看业务需求等经过整理和格式化后适合前端展示的数据便于前端展示给用户查看错题本里考试错题的详细情况是要通过数据转换生成的目标展示对象类型。
import org.mapstruct.Mapper;
// 引入Mapstruct框架提供的Mapper注解用于标识这个接口是一个映射器接口。Mapstruct会根据接口中定义的方法以及后续可能设定的映射规则可通过相关注解来指定具体属性的映射关系不过当前代码里暂时没有更详细配置在编译期自动生成对应的实现类以此来实现不同对象之间属性的转换这样就避免了开发者手动编写大量繁琐的对象属性赋值代码提高了代码开发效率以及数据转换逻辑的可维护性。
import org.springframework.stereotype.Component;
// 引入Spring框架的@Component注解用于将这个接口标记为一个Spring组件使得Spring容器在进行组件扫描时能够发现并管理它便于后续在需要的地方通过依赖注入等方式使用该接口对应的功能实现了组件化管理让这个接口能更好地融入到整个Spring项目的体系架构中方便与其他Spring组件协作完成业务功能例如在业务逻辑层处理错题本数据展示相关业务时能便捷地注入并调用这个接口的方法来进行数据转换操作。
import java.util.List;
// 引入Java标准库中的List接口用于表示列表类型的数据结构在需要返回多个同类型元素如多个错题本相关的视图对象列表、多个错题本实体对象列表等情况时会用到便于对一组数据进行统一的操作和传递符合这里将多个用户错题本实体转换为对应视图对象列表的业务场景需求方便进行批量的数据转换及展示操作。
/**
* @Author Alan
@ -14,9 +22,11 @@ import java.util.List;
* @Date 2024/3/29 15:51
*/
@Component
// 将该接口标记为Spring组件意味着Spring容器会对其进行管理其他类可以通过Spring的依赖注入机制获取这个接口的实例进而调用接口中定义的方法来完成对象转换操作这有助于降低代码之间的耦合度增强代码的可维护性和可扩展性使得整个项目的代码结构更加清晰合理便于在不同的业务场景中灵活运用该接口提供的数据转换功能。
@Mapper(componentModel = "spring")
// 使用@Mapper注解声明这是一个映射器接口并指定componentModel属性为"spring"告知Mapstruct生成的实现类要以Spring组件的形式进行管理方便与Spring框架集成使得生成的实现类可以像普通的Spring Bean一样参与到整个项目的依赖注入、生命周期管理等流程中便于在业务逻辑中灵活地运用这些转换功能使其与整个Spring项目的开发模式相契合方便在项目中按需调用数据转换逻辑。
public interface UserBookConverter {
List<ReUserExamBookVO> listEntityToVo(List<UserBook> list);
}
// 定义了一个名为listEntityToVo的方法它接收一个List<UserBook>类型的参数list也就是用户错题本实体对象的列表。其作用是按照特定的映射规则可能由Mapstruct根据对象属性名等默认匹配情况来确定也可以后续通过添加相应注解来进一步精确配置将这些用户错题本实体对象转换为ReUserExamBookVO视图对象列表用于将用户错题本相关信息以适合前端展示的格式由ReUserExamBookVO定义进行批量展示比如在前端展示用户错题本里与考试相关的错题信息列表时把从数据库查询出来的用户错题本实体列表转换为适合前端展示的视图对象列表后返回给前端进行展示满足前端对用户错题本数据展示的业务需求对应着将用户错题本实体数据转换为适合前端查看的视图数据的批量转换及展示业务场景。
}

@ -1,12 +1,20 @@
package cn.org.alan.exam.converter;
// 声明该接口所在的包名用于在项目中对类和接口进行分类组织表明此接口属于特定的“exam”项目下与数据转换相关的功能模块所在的包方便代码管理与维护使代码按照功能模块清晰划分便于后续开发人员查找、理解和扩展相关功能代码。
import cn.org.alan.exam.model.entity.User;
// 引入User实体类它对应数据库中存储用户相关信息的表结构包含像用户名、密码、用户角色、用户个人信息如姓名、性别、联系方式等具体依业务而定等具体的属性字段代表了用户在系统中的实际数据存储形式是业务操作中涉及用户数据持久化以及围绕用户展开的各类业务逻辑处理的核心数据对象也是后续进行数据转换操作的目标存储对象类型。
import cn.org.alan.exam.model.form.UserForm;
// 引入UserForm类这是一种数据传输对象Data Transfer ObjectDTO形式的类主要用于在前端与后端之间或者不同业务层之间传递数据它承载着创建或更新用户时前端输入的各种信息例如用户名、密码、确认密码、用户角色选择等需要填写的数据方便进行数据的交互和传递为后续的用户相关业务处理提供数据来源是进行数据转换的数据源对象类型。
import org.mapstruct.Mapper;
// 引入Mapstruct框架提供的Mapper注解用于标识这个接口是一个映射器接口。Mapstruct会依据接口中定义的方法以及后续可能添加的映射规则通过相关注解指定具体属性的映射关系当前代码里暂时没有更详细配置在编译期自动生成对应的实现类以此来实现不同对象之间属性的转换这样就避免了开发者手动编写大量繁琐的对象属性赋值代码提高了代码开发效率以及数据转换逻辑的可维护性。
import org.springframework.stereotype.Component;
// 引入Spring框架的@Component注解用于将这个接口标记为一个Spring组件使得Spring容器在进行组件扫描时能够发现并管理它便于后续在需要的地方通过依赖注入等方式使用该接口对应的功能实现了组件化管理让这个接口能更好地融入到整个Spring项目的体系架构中方便与其他Spring组件协作完成业务功能例如在业务逻辑层处理用户数据创建、更新等业务时能便捷地注入并调用这个接口的方法来进行数据转换操作。
import java.util.List;
// 引入Java标准库中的List接口用于表示列表类型的数据结构在需要返回多个同类型元素如多个用户实体对象列表、多个用户表单数据对象列表等情况时会用到便于对一组数据进行统一的操作和传递符合这里将多个用户表单数据转换为对应用户实体对象列表或者单个用户表单数据转换为单个用户实体对象的批量与单个处理的业务场景需求方便进行数据转换及后续操作。
/**
* @Author WeiJin
@ -14,11 +22,14 @@ import java.util.List;
* @Date 2024/3/29 15:51
*/
@Component
// 将该接口标记为Spring组件意味着Spring容器会对其进行管理其他类可以通过Spring的依赖注入机制获取这个接口的实例进而调用接口中定义的方法来完成对象转换操作这有助于降低代码之间的耦合度增强代码的可维护性和可扩展性使得整个项目的代码结构更加清晰合理便于在不同的业务场景中灵活运用该接口提供的数据转换功能。
@Mapper(componentModel = "spring")
// 使用@Mapper注解声明这是一个映射器接口并指定componentModel属性为"spring"告知Mapstruct生成的实现类要以Spring组件的形式进行管理方便与Spring框架集成使得生成的实现类可以像普通的Spring Bean一样参与到整个项目的依赖注入、生命周期管理等流程中便于在业务逻辑中灵活地运用这些转换功能使其与整个Spring项目的开发模式相契合方便在项目中按需调用数据转换逻辑。
public interface UserConverter {
User fromToEntity(UserForm userForm);
// 定义了一个名为fromToEntity的方法它接收一个UserForm类型的参数userForm其功能是依据一定的映射规则这些规则可能由Mapstruct根据对象属性名等默认情况确定也可以手动配置更精确的映射关系将承载前端输入用户相关数据的UserForm对象转换为User实体对象方便后续将前端传来的创建或更新用户的信息保存到数据库中对应着用户创建或更新的业务操作场景确保用户数据能正确地持久化存储将前端传来的符合展示和交互格式的数据转换为符合数据库存储要求的实体格式数据。
List<User> listFromToEntity(List<UserForm> list);
}
// 定义的这个方法接收一个List<UserForm>类型的参数list也就是用户表单数据对象的列表。其目的是按照特定的映射规则同样可能由Mapstruct根据对象属性名等默认匹配也可后续添加更多精确配置来完善将这些用户表单数据对象批量转换为User实体对象列表用于将前端传来的批量用户创建或更新相关的表单数据如批量导入用户信息等场景转换为可持久化存储的用户实体对象列表便于后续统一进行数据库插入或更新操作满足批量处理用户数据转换及存储的业务需求。
}

@ -3,14 +3,20 @@ package cn.org.alan.exam.mapper;
import cn.org.alan.exam.model.entity.Certificate;
import cn.org.alan.exam.model.form.CertificateForm;
import cn.org.alan.exam.model.vo.certificate.MyCertificateVO;
// 导入MyCertificateVO视图对象类用于封装返回给前端的数据
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
// 导入MyBatis-Plus的BaseMapper接口提供基本的CRUD操作
import com.baomidou.mybatisplus.core.metadata.IPage;
// 导入IPage接口用于分页查询
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
// 导入Page类是IPage接口的一个实现用于分页查询
import org.apache.ibatis.annotations.Param;
// 导入Param注解用于MyBatis方法参数注解便于XML配置文件中引用
import org.springframework.stereotype.Repository;
// 导入Repository注解用于标识当前类是一个持久层DAO层的组件
import java.util.List;
// 导入List接口用于处理集合数据
/**
* <p>
@ -21,13 +27,19 @@ import java.util.List;
* @since 2024-03-21
*/
@Repository
// 使用Repository注解将当前接口标识为Spring管理的组件
public interface CertificateMapper extends BaseMapper<Certificate> {
// 声明CertificateMapper接口继承自BaseMapper<Certificate>,提供基本的证书数据操作
// 自定义查询方法根据分页信息、用户ID、考试名称查询用户的证书信息并返回Page<MyCertificateVO>对象
Page<MyCertificateVO> selectMyCertificate(Page<MyCertificateVO> myCertificateVOPage, Integer pageNum, Integer pageSize, Integer userId,String examName);
// 自定义统计方法根据用户ID、证书名称、认证单位统计符合条件的证书数量
int countByCondition(Integer userId, String certificateName, String certificationUnit);
// 自定义分页查询方法根据用户ID、证书名称、认证单位、偏移量、页面大小查询证书ID列表
List<Integer> selectCertificateIdsPage(Integer userId, String certificateName, String certificationUnit, Integer offset,Integer pageSize);
// 自定义批量查询方法根据证书ID列表批量查询证书信息
List<Certificate> batchSelectByIds(List<Integer> missIds);
}
}

@ -1,24 +1,25 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 指定当前接口所在的包路径
import cn.org.alan.exam.model.entity.CertificateUser;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.org.alan.exam.model.entity.CertificateUser; // 导入CertificateUser实体类代表用户证书关联的数据模型
import com.baomidou.mybatisplus.core.mapper.BaseMapper; // 导入MyBatis-Plus的BaseMapper接口提供基本的CRUD操作
import java.util.List;
import java.util.List; // 导入List接口用于处理集合数据
/**
* <p>
* Mapper
* Mapper CertificateUser
* </p>
*
* @author WeiJin
* @since 2024-03-21
* @author WeiJin // 声明当前接口的作者
* @since 2024-03-21 // 声明当前接口自哪个版本开始可用
*/
public interface CertificateUserMapper extends BaseMapper<CertificateUser> {
public interface CertificateUserMapper extends BaseMapper<CertificateUser> { // 声明CertificateUserMapper接口继承自BaseMapper<CertificateUser>提供基本的CertificateUser数据操作
/**
* id
* @param userIds id
* @return
*/
Integer deleteByUserIds(List<Integer> userIds);
}
*
* @param userIds idID
* @return
*/ // 方法注释,描述方法的功能、参数和返回值
Integer deleteByUserIds(List<Integer> userIds); // 自定义删除方法根据用户ID列表批量删除证书用户关联记录
}

@ -1,50 +1,71 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 指定当前接口所在的包路径
import cn.org.alan.exam.model.entity.Exam;
import cn.org.alan.exam.model.entity.ExamGrade;
import cn.org.alan.exam.model.vo.exam.ExamGradeListVO;
import cn.org.alan.exam.model.vo.score.GradeScoreVO;
import cn.org.alan.exam.model.vo.stat.GradeExamVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import cn.org.alan.exam.model.entity.Exam; // 导入Exam实体类本接口未直接使用可能是遗留或用于其他目的
import cn.org.alan.exam.model.entity.ExamGrade; // 导入ExamGrade实体类代表考试与班级的关联数据模型
import cn.org.alan.exam.model.vo.exam.ExamGradeListVO; // 导入ExamGradeListVO视图对象类用于封装考试班级列表的数据
import cn.org.alan.exam.model.vo.score.GradeScoreVO; // 导入GradeScoreVO视图对象类用于封装班级分数的数据
import cn.org.alan.exam.model.vo.stat.GradeExamVO; // 导入GradeExamVO视图对象类本接口未直接使用可能是遗留或用于其他目的
import com.baomidou.mybatisplus.core.mapper.BaseMapper; // 导入MyBatis-Plus的BaseMapper接口提供基本的CRUD操作
import com.baomidou.mybatisplus.core.metadata.IPage; // 导入IPage接口用于分页查询
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; // 导入Page类本接口未直接使用IPage的实现类但通常不直接引用
import org.apache.ibatis.annotations.Param; // 导入Param注解用于MyBatis方法参数注解
import java.util.List;
import java.util.List; // 导入List接口用于处理集合数据
/**
* <p>
* Mapper
* Mapper ExamGrade
* </p>
*
* @author WeiJin
* @since 2024-03-21
* @author WeiJin // 声明当前接口的作者
* @since 2024-03-21 // 声明当前接口自哪个版本开始可用
*/
public interface ExamGradeMapper extends BaseMapper<ExamGrade> {
public interface ExamGradeMapper extends BaseMapper<ExamGrade> { // 声明ExamGradeMapper接口继承自BaseMapper<ExamGrade>提供基本的ExamGrade数据操作
// 自定义方法为指定考试ID添加多个班级ID的关联记录
Integer addExamGrade(Integer id, List<Integer> gradeIds);
// 自定义方法根据考试ID删除考试与班级的关联记录
Integer delExamGrade(Integer id);
/**
*
* @param page
* @param roleId
* @return
*
* @param page
* @param examTitle
* @param userId ID
* @param gradeId ID
* @param roleId ID
* @return
*/
IPage<GradeScoreVO> getExamGrade(IPage<GradeScoreVO> page,String examTitle,Integer userId,Integer gradeId,Integer roleId);
IPage<GradeScoreVO> getExamGrade(IPage<GradeScoreVO> page, String examTitle, Integer userId, Integer gradeId, Integer roleId);
/**
* id
* @param id id
* @return
* ID
*
* @param id ID
* @return
*/
Integer selectClassSize(Integer id);
/**
*
*
* @param examPage
* @param userId ID
* @param title
* @return
*/
IPage<ExamGradeListVO> selectClassExam(IPage<ExamGradeListVO> examPage, Integer userId, String title);
IPage<ExamGradeListVO> selectClassExam(IPage<ExamGradeListVO> examPage, Integer userId,String title);
IPage<ExamGradeListVO> selectAdminClassExam(IPage<ExamGradeListVO> examPage, Integer userId,String title);
/**
*
*
* @param examPage
* @param userId ID
* @param title
* @return
*/
IPage<ExamGradeListVO> selectAdminClassExam(IPage<ExamGradeListVO> examPage, Integer userId, String title);
}
}

@ -1,49 +1,65 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 指定当前接口所在的包路径
import cn.org.alan.exam.model.entity.Exam;
import cn.org.alan.exam.model.vo.answer.AnswerExamVO;
import cn.org.alan.exam.model.vo.record.ExamRecordVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import cn.org.alan.exam.model.entity.Exam; // 导入Exam实体类代表考试数据模型
import cn.org.alan.exam.model.vo.answer.AnswerExamVO; // 导入AnswerExamVO视图对象类用于封装考试答题相关的数据
import cn.org.alan.exam.model.vo.record.ExamRecordVO; // 导入ExamRecordVO视图对象类用于封装考试记录相关的数据
import com.baomidou.mybatisplus.core.mapper.BaseMapper; // 导入MyBatis-Plus的BaseMapper接口提供基本的CRUD操作
import com.baomidou.mybatisplus.core.metadata.IPage; // 导入IPage接口用于分页查询
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; // 导入Page类是IPage接口的一个实现用于分页查询的具体实现
import org.apache.ibatis.annotations.Param; // 导入Param注解用于MyBatis方法参数注解
import org.springframework.stereotype.Repository; // 导入Repository注解本接口未直接使用通常用于标注数据访问层组件
import java.util.List;
import java.util.List; // 导入List接口用于处理集合数据
/**
* Mapper
* Mapper Exam
*
* @author Alan
* @since 2024-03-21
* @author Alan // 声明当前接口的作者
* @since 2024-03-21 // 声明当前接口自哪个版本开始可用
*/
@Repository // 注解通常用于标注数据访问层组件但Mapper接口一般通过MyBatis框架扫描不需要显式添加
public interface ExamMapper extends BaseMapper<Exam> { // 声明ExamMapper接口继承自BaseMapper<Exam>提供基本的Exam数据操作
public interface ExamMapper extends BaseMapper<Exam> {
// 自定义方法根据考试ID列表删除多个考试记录
int deleteExams(List<Integer> examIds);
// 自定义方法根据考试ID列表删除多个考试与班级的关联记录可能是业务逻辑上的关联不一定直接对应数据库表
int deleteExamGrades(List<Integer> examIds);
// 自定义方法根据考试ID列表删除多个考试资源如试题、答案等
int deleteExamRepos(List<Integer> examIds);
// 自定义方法根据考试ID列表删除多个考试与试题的关联记录
int deleteExamQuestions(List<Integer> examIds);
/**
*
*
* @param userIds id
* @return
* @param userIds ID
* @return
*/
Integer deleteByUserIds(List<Integer> userIds);
/**
* id
* ID
*
* @param userId
* @return
* @param page
* @param userId ID
* @param role
* @param examName
* @return
*/
IPage<AnswerExamVO> selectMarkedList(@Param("page") IPage<AnswerExamVO> page, @Param("userId") Integer userId,String role,String examName);
IPage<AnswerExamVO> selectMarkedList(@Param("page") IPage<AnswerExamVO> page, @Param("userId") Integer userId, String role, String examName);
Page<ExamRecordVO> getExamRecordPage(Page<ExamRecordVO> examPage, Integer userId,String examName);
}
/**
*
*
* @param examPage 使Page<ExamRecordVO>
* @param userId ID
* @param examName
* @return
*/
Page<ExamRecordVO> getExamRecordPage(Page<ExamRecordVO> examPage, Integer userId, String examName);
// 注意在实际使用中通常建议统一使用IPage接口作为分页对象的类型以避免具体实现类的依赖。
// 如果MyBatis-Plus的版本或配置允许可以考虑将getExamRecordPage方法的参数和返回值类型改为IPage<ExamRecordVO>。
}

@ -1,45 +1,46 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 指定当前接口所在的包路径
import cn.org.alan.exam.model.entity.ExamQuAnswer;
import cn.org.alan.exam.model.vo.answer.UserAnswerDetailVO;
import cn.org.alan.exam.model.vo.exam.ExamQuAnswerExtVO;
import cn.org.alan.exam.model.vo.score.QuestionAnalyseVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.org.alan.exam.model.entity.ExamQuAnswer; // 导入ExamQuAnswer实体类代表考试问题回答的数据模型
import cn.org.alan.exam.model.vo.answer.UserAnswerDetailVO; // 导入UserAnswerDetailVO视图对象类用于封装用户回答主观题的信息
import cn.org.alan.exam.model.vo.exam.ExamQuAnswerExtVO; // 导入ExamQuAnswerExtVO视图对象类用于扩展封装考试问题回答的信息
import cn.org.alan.exam.model.vo.score.QuestionAnalyseVO; // 导入QuestionAnalyseVO视图对象类用于封装试题分析的信息
import com.baomidou.mybatisplus.core.mapper.BaseMapper; // 导入MyBatis-Plus的BaseMapper接口提供基本的CRUD操作
import java.util.List;
import java.util.List; // 导入List接口用于处理集合数据
/**
* <p>
* Mapper
* Mapper ExamQuAnswer
* </p>
*
* @author WeiJin
* @since 2024-03-21
* @author WeiJin // 声明当前接口的作者
* @since 2024-03-21 // 声明当前接口自哪个版本开始可用
*/
public interface ExamQuAnswerMapper extends BaseMapper<ExamQuAnswer> {
public interface ExamQuAnswerMapper extends BaseMapper<ExamQuAnswer> { // 声明ExamQuAnswerMapper接口继承自BaseMapper<ExamQuAnswer>提供基本的ExamQuAnswer数据操作
List<ExamQuAnswerExtVO> list(String examId, String questionId);
// 自定义方法根据考试ID和试题ID列表查询考试问题回答信息扩展信息
List<ExamQuAnswerExtVO> list(String examId, String questionId); // 注意这里的参数类型通常为Integer除非有特定原因使用String
/**
*
* @param examId id
* @param questionId id
* @return
* @param examId ID
* @param questionId ID
* @return QuestionAnalyseVO
*/
QuestionAnalyseVO questionAnalyse(Integer examId, Integer questionId);
QuestionAnalyseVO questionAnalyse(Integer examId, Integer questionId); // 根据考试ID和试题ID查询试题作答分析信息
/**
*
* @param userIds id
* @return
* @param userIds ID
* @return
*/
Integer deleteByUserIds(List<Integer> userIds);
Integer deleteByUserIds(List<Integer> userIds); // 根据用户ID列表删除对应的考试作答记录
/**
*
* @param userId id
* @param examId id
* @return
* @param userId ID
* @param examId ID
* @return List<UserAnswerDetailVO>
*/
List<UserAnswerDetailVO> selectUserAnswer(Integer userId, Integer examId);
}
List<UserAnswerDetailVO> selectUserAnswer(Integer userId, Integer examId); // 根据用户ID和考试ID查询用户回答主观题的信息
}

@ -1,16 +1,25 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 指定当前接口所在的包路径
import cn.org.alan.exam.model.entity.ExamQuestion;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.org.alan.exam.model.entity.ExamQuestion; // 导入ExamQuestion实体类该类代表考试题目的数据模型
import com.baomidou.mybatisplus.core.mapper.BaseMapper; // 导入MyBatis-Plus的BaseMapper接口提供基本的CRUD操作
import java.util.List;
import java.util.Map;
import java.util.List; // 导入List接口用于处理集合数据
import java.util.Map; // 导入Map接口用于存储键值对数据
/**
* @Author Alan
* @Version
* @Date 2024/4/7 3:49 PM
* @Author Alan // 声明当前接口的作者
* @Version // 版本信息缺失,通常这里会指定接口的版本号或者留空
* @Date 2024/4/7 3:49 PM // 声明当前接口的创建日期和时间
*/
public interface ExamQuestionMapper extends BaseMapper<ExamQuestion> {
public interface ExamQuestionMapper extends BaseMapper<ExamQuestion> { // 声明ExamQuestionMapper接口继承自BaseMapper<ExamQuestion>提供基本的ExamQuestion数据操作
// 自定义方法,用于插入考试题目信息
// 参数说明:
// examId - 考试ID用于标识需要插入题目的考试
// quType - 题目类型,用于区分不同类型的题目(如单选题、多选题、判断题等)
// quScore - 题目分数,表示该题目的分值
// questionIdsAndSorts - 题目ID和排序的列表其中每个Map对象包含题目ID作为键和对应的排序值作为值
int insertQuestion(Integer examId, Integer quType, Integer quScore, List<Map<String, Object>> questionIdsAndSorts);
}
// 方法返回值为int类型通常表示插入操作影响的行数但在这里可能主要用于表示操作是否成功如返回1表示成功0或负数表示失败
// 注意这里的业务逻辑可能需要根据实际情况调整例如题目ID和排序的存储方式、如何确保数据的一致性等
}

@ -1,16 +1,25 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 指定当前Mapper接口所在的包路径
// 导入ExamRepo实体类该类是与数据库表对应的Java对象用于表示考试资源仓库的实体
import cn.org.alan.exam.model.entity.ExamRepo;
// 导入MyBatis-Plus提供的BaseMapper接口它提供了基础的CRUD操作方法
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* Mapper
* Mapper
* </p>
* <p>
* BaseMapperBaseMapper
* CRUD
* </p>
*
* @author WeiJin
* @since 2024-03-21
* @author WeiJin // 声明当前接口的作者
* @since 2024-03-21 // 声明当前接口的创建日期或版本号
*/
public interface ExamRepoMapper extends BaseMapper<ExamRepo> {
public interface ExamRepoMapper extends BaseMapper<ExamRepo> { // 声明ExamRepoMapper接口它继承自BaseMapper<ExamRepo>因此可以对ExamRepo实体进行基础的数据库操作
}
// 由于此接口仅继承了BaseMapper并未定义额外的方法
// 因此这里没有额外的代码需要解释。所有对ExamRepo实体的数据库操作都将通过BaseMapper中定义的方法来完成。
}

@ -1,24 +1,29 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 指定当前Mapper接口所在的包路径
// 导入ExerciseRecord实体类该类是与数据库表对应的Java对象用于表示用户的练习作答记录
import cn.org.alan.exam.model.entity.ExerciseRecord;
// 导入MyBatis-Plus提供的BaseMapper接口它提供了基础的CRUD操作方法
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
// 导入Java的List接口用于处理用户ID的集合
import java.util.List;
/**
* <p>
* Mapper
* Mapper ExerciseRecord
* </p>
*
* @author WeiJin
* @since 2024-03-21
* @author WeiJin // 声明当前接口的作者
* @since 2024-03-21 // 声明当前接口的创建日期或版本号
*/
public interface ExerciseRecordMapper extends BaseMapper<ExerciseRecord> {
public interface ExerciseRecordMapper extends BaseMapper<ExerciseRecord> { // 声明ExerciseRecordMapper接口它继承自BaseMapper<ExerciseRecord>因此可以对ExerciseRecord实体进行基础的数据库操作
/**
*
* @param userIds id
* @return
*
*
* @param userIds IDID
* @return 00
*/
Integer deleteByUserIds(List<Integer> userIds);
}
Integer deleteByUserIds(List<Integer> userIds); // 定义一个自定义方法用于根据用户ID列表删除对应的练习作答记录并返回被影响的记录数
}

@ -1,24 +1,29 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 指定当前Mapper接口所在的包路径
// 导入GradeExercise实体类该类是与数据库表对应的Java对象用于表示用户创建的可供学生练习的题库关联信息
import cn.org.alan.exam.model.entity.GradeExercise;
// 导入MyBatis-Plus提供的BaseMapper接口它提供了基础的CRUD操作方法
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
// 导入Java的List接口用于处理用户ID的集合
import java.util.List;
/**
* <p>
* Mapper
* Mapper GradeExercise
* </p>
*
* @author WeiJin
* @since 2024-03-21
* @author WeiJin // 声明当前接口的作者
* @since 2024-03-21 // 声明当前接口的创建日期或版本号
*/
public interface GradeExerciseMapper extends BaseMapper<GradeExercise> {
public interface GradeExerciseMapper extends BaseMapper<GradeExercise> { // 声明GradeExerciseMapper接口它继承自BaseMapper<GradeExercise>因此可以对GradeExercise实体进行基础的数据库操作
/**
*
* @param userIds id
* @return
*
*
* @param userIds IDID
* @return 00
*/
Integer deleteByUserIds(List<Integer> userIds);
}
Integer deleteByUserIds(List<Integer> userIds); // 定义一个自定义方法用于根据用户ID列表删除对应的题库关联记录并返回被影响的记录数
}

@ -1,32 +1,44 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 指定当前Mapper接口所在的包路径
// 导入Grade实体类该类是与数据库表对应的Java对象用于表示成绩信息
import cn.org.alan.exam.model.entity.Grade;
// 导入GradeVO类这是一个值对象Value Object通常用于封装从数据库查询后需要展示给前端的数据
import cn.org.alan.exam.model.vo.GradeVO;
// 导入MyBatis-Plus提供的BaseMapper接口它提供了基础的CRUD操作方法
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
// 导入MyBatis-Plus分页插件提供的Page类用于实现分页查询功能
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
// 导入Spring框架的Repository注解用于标识这是一个数据访问层的组件
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* <p>
* Mapper
* Mapper Grade
* </p>
*
* @author WeiJin
* @since 2024-03-21
* @author WeiJin // 声明当前接口的作者
* @since 2024-03-21 // 声明当前接口的创建日期或版本号
*/
@Repository
public interface GradeMapper extends BaseMapper<Grade> {
@Repository // 使用@Repository注解标识这是一个数据访问层的组件Spring会将其识别为一个Bean并进行管理
public interface GradeMapper extends BaseMapper<Grade> { // 声明GradeMapper接口它继承自BaseMapper<Grade>因此可以对Grade实体进行基础的数据库操作
// 根据用户ID列表查询对应的成绩ID列表
List<Integer> selectIdsByUserIds(List<Integer> userIds);
// 根据用户ID列表删除对应的成绩记录
Integer deleteByUserId(List<Integer> userIds);
Page<GradeVO> selectGradePage(Page<GradeVO> page, Integer userId , String gradeName, Integer role);
// 分页查询成绩信息返回Page<GradeVO>对象,包含分页信息和查询结果
Page<GradeVO> selectGradePage(Page<GradeVO> page, Integer userId, String gradeName, Integer role);
// 根据用户ID、角色、成绩名称和分页参数偏移量和页面大小查询成绩ID列表用于分页显示
List<Integer> selectGradeIdsPage(Integer userId, Integer role, String gradeName, int offset, Integer pageSize);
// 根据ID列表批量查询成绩信息返回GradeVO对象列表
List<GradeVO> batchSelectByIds(List<Integer> missIds);
// 根据用户ID、成绩名称和角色统计符合条件的成绩记录数
int countByCondition(Integer userId, String gradeName, Integer role);
}
}

@ -1,31 +1,35 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 指定当前Mapper接口所在的包路径
// 导入ManualScore实体类该类是与数据库表对应的Java对象用于表示用户批改的主观题分数信息
import cn.org.alan.exam.model.entity.ManualScore;
// 导入MyBatis-Plus提供的BaseMapper接口它提供了基础的CRUD操作方法
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;
import java.util.List; // 导入Java的List接口用于处理集合数据
/**
* <p>
* Mapper
* Mapper ManualScore
* </p>
*
* @author WeiJin
* @since 2024-03-21
* @author WeiJin // 声明当前接口的作者
* @since 2024-03-21 // 声明当前接口的创建日期或版本号
*/
public interface ManualScoreMapper extends BaseMapper<ManualScore> {
public interface ManualScoreMapper extends BaseMapper<ManualScore> { // 声明ManualScoreMapper接口它继承自BaseMapper<ManualScore>因此可以对ManualScore实体进行基础的数据库操作
/**
*
* @param userIds id
* @return
* ID
*
* @param userIds IDID
* @return 00
*/
Integer deleteByUserIds(List<Integer> userIds);
Integer deleteByUserIds(List<Integer> userIds); // 定义一个自定义方法用于根据用户ID列表删除对应的主观题分数记录并返回被影响的记录数
/**
*
* @param manualScores
* @return
*
*
* @param manualScores
* @return 00
*/
Integer insertList(List<ManualScore> manualScores);
}
Integer insertList(List<ManualScore> manualScores); // 定义一个自定义方法,用于批量添加主观题分数记录到数据库,并返回被影响的记录数
}

@ -1,27 +1,38 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 声明当前Mapper接口所在的包路径
// 导入Grade实体类该类表示学生的成绩信息
import cn.org.alan.exam.model.entity.Grade;
// 导入NoticeGrade实体类该类表示公告与班级的关联信息
import cn.org.alan.exam.model.entity.NoticeGrade;
// 导入MyBatis-Plus提供的BaseMapper接口提供基础的CRUD操作
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;
import java.util.List; // 导入Java的List接口用于处理集合数据
/**
* <p>
* Mapper
* Mapper NoticeGrade
* </p>
*
* @author WeiJin
* @since 2024-03-21
* @author WeiJin // 声明当前接口的作者
* @since 2024-03-21 // 声明当前接口的创建日期或版本号
*/
public interface NoticeGradeMapper extends BaseMapper<NoticeGrade> {
public interface NoticeGradeMapper extends BaseMapper<NoticeGrade> { // 声明NoticeGradeMapper接口继承自BaseMapper<NoticeGrade>提供基础的CRUD操作
/**
* id
* @param noticeIds id
* @return
* ID
*
* @param noticeIds IDID
* @return 00
*/
Integer deleteByNoticeIds(List<Integer> noticeIds);
Integer deleteByNoticeIds(List<Integer> noticeIds); // 定义一个自定义方法用于根据公告ID列表删除对应的公告与班级关联记录并返回被影响的记录数
int addNoticeGrade(Integer noticeId, List<Grade> grades);
}
/**
*
*
* @param noticeId ID
* @param grades
* @return 00
*/
int addNoticeGrade(Integer noticeId, List<Grade> grades); // 定义一个自定义方法,用于添加公告与成绩的关联记录到数据库,并返回被影响的记录数
}

@ -1,45 +1,94 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 指定当前Mapper接口所在的包路径
// 导入Notice实体类表示公告信息
import cn.org.alan.exam.model.entity.Notice;
// 导入NoticeVO值对象类通常用于封装传递给前端的数据可能包含Notice实体类的数据以及其他相关信息
import cn.org.alan.exam.model.vo.NoticeVO;
// 导入MyBatis-Plus提供的BaseMapper接口提供基础的CRUD操作
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
// 导入MyBatis-Plus分页插件提供的Page类用于分页查询
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.util.List;
import java.util.List; // 导入Java的List接口用于处理集合数据
/**
* <p>
* Mapper
* Mapper Notice
* </p>
*
* @author WeiJin
* @since 2024-03-21
* @author WeiJin // 声明当前接口的作者
* @since 2024-03-21 // 声明当前接口的创建日期或版本号
*/
public interface NoticeMapper extends BaseMapper<Notice> {
public interface NoticeMapper extends BaseMapper<Notice> { // 声明NoticeMapper接口继承自BaseMapper<Notice>提供基础的CRUD操作
int removeNotice(List<Integer> noticeIds);
/**
* ID
*
* @param noticeIds IDID
* @return 00
*/
int removeNotice(List<Integer> noticeIds); // 定义一个自定义方法用于根据公告ID列表删除公告记录并返回被影响的记录数
/**
*
* @param userIds id
* @return
*
*
* @param userIds IDID
* @return 00
*/
Integer deleteByUserIds(List<Integer> userIds);
Integer deleteByUserIds(List<Integer> userIds); // 定义一个自定义方法用于根据用户ID列表删除用户创建的公告记录并返回被影响的记录数
/**
* idids
* @param userIds id
* @return
* IDID
*
* @param userIds IDIDID
* @return ID
*/
List<Integer> selectIdsByUserIds(List<Integer> userIds);
List<Integer> selectIdsByUserIds(List<Integer> userIds); // 定义一个自定义方法用于根据用户ID列表查询并返回对应的公告ID列表
Page<NoticeVO> selectNewNoticePage(Page<NoticeVO> page, Integer userId);
/**
*
*
* @param page
* @param userId ID
* @return NoticeVOPage
*/
Page<NoticeVO> selectNewNoticePage(Page<NoticeVO> page, Integer userId); // 定义一个自定义方法用于分页查询并返回用户的新公告信息结果封装在NoticeVO对象中
int countByCondition(Integer userId, String title);
/**
*
*
* @param userId IDID
* @param title
* @return
*/
int countByCondition(Integer userId, String title); // 定义一个自定义方法用于根据用户ID和公告标题统计公告数量并返回结果
List<Integer> selectNoticeIdsPage(Integer userId, String title, int offset, Integer pageSize);
/**
* ID
*
* @param userId IDIDID
* @param title
* @param offset
* @param pageSize
* @return ID
*/
List<Integer> selectNoticeIdsPage(Integer userId, String title, int offset, Integer pageSize); // 定义一个自定义方法用于分页查询并返回符合条件的公告ID列表
List<NoticeVO> batchSelectByIds(List<Integer> missIds);
/**
*
*
* @param missIds IDID
* @return NoticeVO
*/
List<NoticeVO> batchSelectByIds(List<Integer> missIds); // 定义一个自定义方法用于批量查询并返回对应的公告信息结果封装在NoticeVO对象中
List<Integer> selectNewNoticeIdsPage(Integer userId, int offset, Integer pageSize);
}
/**
* ID
*
* @param userId IDID
* @param offset
* @param pageSize
* @return ID
*/
List<Integer> selectNewNoticeIdsPage(Integer userId, int offset, Integer pageSize); // 定义一个自定义方法用于分页查询并返回用户的新公告ID列表
}

@ -1,52 +1,57 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 声明包路径,指明当前接口所在的包
import cn.org.alan.exam.model.entity.Option;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.org.alan.exam.model.entity.Option; // 导入Option实体类代表试题选项的数据模型
import com.baomidou.mybatisplus.core.mapper.BaseMapper; // 导入BaseMapper接口提供基础的CRUD操作
import java.util.List;
import java.util.List; // 导入List接口用于处理集合数据
/**
* <p>
* Mapper
* Mapper Option
* </p>
*
* @author WeiJin
* @since 2024-03-21
* @author WeiJin // 声明接口作者
* @since 2024-03-21 // 声明接口创建日期或版本号
*/
public interface OptionMapper extends BaseMapper<Option> {
public interface OptionMapper extends BaseMapper<Option> { // 声明OptionMapper接口继承自BaseMapper<Option>提供基础的CRUD操作
/**
*
* @param options
* @return
*
*
* @param options Option
* @return
*/
Integer insertBatch(List<Option> options);
Integer insertBatch(List<Option> options); // 定义一个自定义方法,用于批量添加选项到数据库,并返回影响的记录数
/**
* id
* @param quIdList id
* @return
* ID
*
* @param quIdList IDID
* @return
*/
Integer deleteBatchByQuIds(List<Integer> quIdList);
Integer deleteBatchByQuIds(List<Integer> quIdList); // 定义一个自定义方法用于根据试题ID列表批量删除选项并返回影响的记录数
/**
* id
* @param id id
* @return
* ID
*
* @param id ID
* @return List<Option>
*/
List<Option> selectAllByQuestionId(Integer id);
/**
* id
* @param id id
* @return
List<Option> selectAllByQuestionId(Integer id); // 定义一个自定义方法用于根据试题ID查询并返回其所有选项的集合
/**
* ID
*
* @param id ID
* @return List<Option>
*/
List<Option> selectByQuestionId(Integer id);
List<Option> selectByQuestionId(Integer id); // 定义一个自定义方法用于根据试题ID查询并返回其所有选项不包含是否正确信息的集合
/**
* Id
* @param optionIds Id
* @return
* ID
*
* @param optionIds IDID
* @return ID
*/
Integer selectRightCountByIds(List<Integer> optionIds);
}
Integer selectRightCountByIds(List<Integer> optionIds); // 定义一个自定义方法用于根据选项ID列表查询并返回正确的选项个数
}

@ -1,74 +1,114 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 声明包路径包含当前Mapper接口
import cn.org.alan.exam.model.entity.Question;
import cn.org.alan.exam.model.vo.QuestionVO;
import cn.org.alan.exam.model.vo.exercise.QuestionSheetVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import cn.org.alan.exam.model.entity.Question; // 导入Question实体类代表试题的数据模型
import cn.org.alan.exam.model.vo.QuestionVO; // 导入QuestionVO类用于视图层展示试题的详细信息
import cn.org.alan.exam.model.vo.exercise.QuestionSheetVO; // 导入QuestionSheetVO类用于表示用户的试题答题情况
import com.baomidou.mybatisplus.core.mapper.BaseMapper; // 导入BaseMapper接口提供基础的CRUD操作
import com.baomidou.mybatisplus.core.metadata.IPage; // 导入IPage接口用于分页查询
import java.util.List;
import java.util.List; // 导入List接口用于处理集合数据
/**
* <p>
* Mapper
* Mapper Question
* </p>
*
* @author WeiJin
* @since 2024-03-21
* @author WeiJin // 声明接口作者
* @since 2024-03-21 // 声明接口创建日期或版本号
*/
public interface QuestionMapper extends BaseMapper<Question> {
public interface QuestionMapper extends BaseMapper<Question> { // 声明QuestionMapper接口继承自BaseMapper<Question>提供基础的CRUD操作
/**
*
*
* @param page
* @param content
* @param repoId id
* @param type
* @param userId id
* @return
* @param page
* @param content
* @param repoId ID
* @param type
* @param userId ID
* @return IPage<QuestionVO>
*/
IPage<QuestionVO> pagingQuestion(IPage<QuestionVO> page, String content, Integer repoId, Integer type, Integer userId);
/**
* id
* @param id id
* @return
* ID
*
* @param id ID
* @return QuestionVO
*/
QuestionVO selectSingle(Integer id);
/**
*
* @param userIds id
* @return
*
* @param userIds ID
* @return
*/
Integer deleteByUserIds(List<Integer> userIds);
/**
* id
* @param userIds id
* @return
* ID
*
* @param userIds ID
* @return ID
*/
List<Integer> selectIdsByUserIds(List<Integer> userIds);
/**
* Id
* @param repoId Id
* @param quType
* @param userId Id
* @return
* ID
*
* @param repoId ID
* @param quType
* @param userId ID
* @return QuestionSheetVO
*/
List<QuestionSheetVO> selectQuestionSheet(Integer repoId, Integer quType, Integer userId);
/**
* ID
*
* @param id ID
* @return QuestionVOselectSingle
*/
QuestionVO selectDetail(Integer id);
/**
* ID
*
* @param list ID
* @return void
*/
void deleteBatchIdsQu(List<Integer> list);
/**
*
*
* @param userId ID
* @param title
* @param type
* @param repoId ID
* @return
*/
int countByCondition(Integer userId, String title, Integer type, Integer repoId);
/**
* ID
*
* @param userId ID
* @param title
* @param type
* @param repoId ID
* @param offset
* @param pageSize
* @return ID
*/
List<Integer> selectQuestionIdsPage(Integer userId, String title, Integer type, Integer repoId, int offset, Integer pageSize);
/**
* ID
*
* @param missIds ID
* @return QuestionVO
*/
List<QuestionVO> batchSelectByIds(List<Integer> missIds);
}
}

@ -1,30 +1,30 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 定义mapper接口所在的包
import cn.org.alan.exam.model.entity.Repo;
import cn.org.alan.exam.model.vo.repo.RepoListVO;
import cn.org.alan.exam.model.vo.repo.RepoVO;
import cn.org.alan.exam.model.vo.exercise.ExerciseRepoVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import cn.org.alan.exam.model.entity.Repo; // 引入Repo实体类
import cn.org.alan.exam.model.vo.repo.RepoListVO; // 引入RepoListVO视图对象
import cn.org.alan.exam.model.vo.repo.RepoVO; // 引入RepoVO视图对象
import cn.org.alan.exam.model.vo.exercise.ExerciseRepoVO; // 引入ExerciseRepoVO视图对象
import com.baomidou.mybatisplus.core.mapper.BaseMapper; // 引入MyBatis Plus的BaseMapper接口
import com.baomidou.mybatisplus.core.metadata.IPage; // 引入MyBatis Plus的分页接口
import org.apache.ibatis.annotations.Param; // 引入MyBatis的Param注解用于传递参数
import org.springframework.stereotype.Repository; // 引入Spring的Repository注解标记为持久层组件
import java.util.List;
import java.util.List; // 引入Java的List接口
/**
* @author WeiJin
* @since 2024-03-21
*/
@Repository
public interface RepoMapper extends BaseMapper<Repo> {
*/ // 类注释,说明作者和创建时间
@Repository // 标记为Spring的持久层组件
public interface RepoMapper extends BaseMapper<Repo> { // 定义RepoMapper接口继承BaseMapper提供基础的CRUD操作
/**
*
*
* @param page
* @param title
* @param userId
* @return
* @param page
* @param title
* @param userId ID
* @return
*/
IPage<RepoVO> pagingRepo(@Param("page") IPage<RepoVO> page, @Param("title") String title,
@Param("userId") Integer userId);
@ -32,20 +32,27 @@ public interface RepoMapper extends BaseMapper<Repo> {
/**
*
*
* @param userIds id
* @return
* @param userIds id
* @return
*/
Integer deleteByUserIds(List<Integer> userIds);
/**
*
*
* @param page
* @param title
* @return
* @param page
* @param title
* @return ExerciseRepoVO
*/
IPage<ExerciseRepoVO> selectRepo(IPage<ExerciseRepoVO> page,
String title);
/**
* ID
*
* @param repoTitle
* @param userId ID
* @return RepoListVO
*/
List<RepoListVO> selectRepoList(String repoTitle, int userId);
}
}

@ -1,19 +1,25 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 定义RoleMapper接口所在的包路径
import cn.org.alan.exam.model.entity.Role;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.org.alan.exam.model.entity.Role; // 引入Role实体类该类与数据库中的角色表对应
import com.baomidou.mybatisplus.core.mapper.BaseMapper; // 引入MyBatis Plus的BaseMapper接口提供基础的CRUD操作
import java.util.List;
import java.util.List; // 引入Java的List接口用于定义返回结果类型
/**
* <p>
* Mapper
* Mapper Role
* </p>
*
* @author WeiJin
* @since 2024-03-21
* @author WeiJin // 作者信息
* @since 2024-03-21 // 创建时间
*/
public interface RoleMapper extends BaseMapper<Role> {
public interface RoleMapper extends BaseMapper<Role> { // 定义RoleMapper接口继承BaseMapper<Role>提供对Role实体的基础CRUD操作
List<String> selectCodeById(Integer roleId);
}
/**
* ID
*
* @param roleId ID
* @return
*/
List<String> selectCodeById(Integer roleId); // 自定义查询方法根据角色ID返回对应的角色编码列表
}

@ -1,35 +1,40 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 定义StatMapper接口所在的包路径
import cn.org.alan.exam.model.entity.Grade;
import cn.org.alan.exam.model.vo.stat.AllStatsVO;
import cn.org.alan.exam.model.vo.stat.GradeExamVO;
import cn.org.alan.exam.model.vo.stat.GradeStudentVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import cn.org.alan.exam.model.entity.Grade; // 引入Grade实体类代表年级或班级信息
import cn.org.alan.exam.model.vo.stat.AllStatsVO; // 引入AllStatsVO视图对象可能用于其他统计功能当前接口未使用
import cn.org.alan.exam.model.vo.stat.GradeExamVO; // 引入GradeExamVO视图对象用于封装各班试卷数的统计信息
import cn.org.alan.exam.model.vo.stat.GradeStudentVO; // 引入GradeStudentVO视图对象用于封装各班人数的统计信息
import com.baomidou.mybatisplus.core.mapper.BaseMapper; // 引入MyBatis Plus的BaseMapper接口提供基础的CRUD操作
import org.apache.ibatis.annotations.Param; // 引入MyBatis的Param注解用于传递参数
import org.springframework.stereotype.Repository; // 引入Spring的Repository注解标记为持久层组件
import java.util.List;
import java.util.List; // 引入Java的List接口用于定义返回结果类型
/**
* @ Author JinXi
* @ Version 1.0
* @ Date 2024/5/12 14:56
* @ Author JinXi // 作者信息
* @ Version 1.0 // 版本信息
* @ Date 2024/5/12 14:56 // 创建时间
*/
@Repository
public interface StatMapper extends BaseMapper<Grade> {
@Repository // 标记为Spring的持久层组件
public interface StatMapper extends BaseMapper<Grade> { // 定义StatMapper接口继承BaseMapper<Grade>提供对Grade实体的基础CRUD操作
/**
*
* @return
*
* @param roleId ID
* @param id ID
* @return GradeStudentVO
*/
List<GradeStudentVO> StudentGradeCount(@Param("roleId")Integer roleId, Integer id);
List<GradeStudentVO> StudentGradeCount(@Param("roleId") Integer roleId, Integer id);
/**
*
* @return
*
* @param roleId ID
* @param id ID
* @return GradeExamVO
*/
List<GradeExamVO> ExamGradeCount(@Param("roleId") Integer roleId, Integer id);
}
// 注意接口中缺少对AllStatsVO的使用如果后续需要使用应添加相应的方法
}

@ -1,31 +1,46 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 定义UserBookMapper接口所在的包路径
import cn.org.alan.exam.model.entity.UserBook;
import cn.org.alan.exam.model.vo.userbook.UserPageBookVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import cn.org.alan.exam.model.entity.UserBook; // 引入UserBook实体类代表用户的错题本记录
import cn.org.alan.exam.model.vo.userbook.UserPageBookVO; // 引入UserPageBookVO视图对象用于封装分页查询结果
import com.baomidou.mybatisplus.core.mapper.BaseMapper; // 引入MyBatis Plus的BaseMapper接口提供基础的CRUD操作
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; // 引入MyBatis Plus的分页插件Page类用于分页查询
import java.util.List;
import java.util.List; // 引入Java的List接口用于定义返回结果类型或传递参数类型
/**
* <p>
* Mapper
* Mapper UserBook
* </p>
*
* @author WeiJin
* @since 2024-03-21
* @author WeiJin // 作者信息
* @since 2024-03-21 // 创建时间
*/
public interface UserBookMapper extends BaseMapper<UserBook> {
public interface UserBookMapper extends BaseMapper<UserBook> { // 定义UserBookMapper接口继承BaseMapper<UserBook>提供对UserBook实体的基础CRUD操作
/**
*
* @param userIds id
* @return
*
*
* @param userIds ID
* @return
*/
Integer deleteByUserIds(List<Integer> userIds);
Integer deleteByUserIds(List<Integer> userIds); // 自定义删除方法根据用户ID列表删除对应的错题本记录
/**
*
*
* @param userBookArrayList UserBook
* @return
*/
int addUserBookList(List<UserBook> userBookArrayList); // 自定义添加方法,批量添加错题本记录
int addUserBookList(List<UserBook> userBookArrayList);
Page<UserPageBookVO> selectPageVo(Page<UserPageBookVO> page, String examName, Integer userId,Integer role);
}
/**
*
*
* @param page
* @param examName
* @param userId ID
* @param role ID
* @return
*/
Page<UserPageBookVO> selectPageVo(Page<UserPageBookVO> page, String examName, Integer userId, Integer role); // 自定义分页查询方法根据分页信息、考试名称、用户ID和角色ID查询错题本记录
}

@ -1,17 +1,24 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 定义UserDailyLoginDurationMapper接口所在的包路径
import cn.org.alan.exam.model.entity.UserDailyLoginDuration;
import cn.org.alan.exam.model.entity.UserExerciseRecord;
import cn.org.alan.exam.model.vo.stat.DailyVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.org.alan.exam.model.entity.UserDailyLoginDuration; // 引入UserDailyLoginDuration实体类表示用户的每日登录时长记录
import cn.org.alan.exam.model.entity.UserExerciseRecord; // 引入UserExerciseRecord实体类虽然在此接口中未直接使用可能是为了后续扩展或代码未完全展示
import cn.org.alan.exam.model.vo.stat.DailyVO; // 引入DailyVO视图对象用于封装每日统计信息
import com.baomidou.mybatisplus.core.mapper.BaseMapper; // 引入MyBatis Plus的BaseMapper接口提供基础的CRUD操作
import java.util.List;
// 不需要为import语句添加注释
/**
* @Author Alan
* @Version
* @Date 2024/5/28 10:47 PM
* @Author Alan // 作者信息
* @Version // 版本信息(此处未填写具体内容)
* @Date 2024/5/28 10:47 PM // 创建时间
*/
public interface UserDailyLoginDurationMapper extends BaseMapper<UserDailyLoginDuration> {
List<DailyVO> getDaily(Integer userId);
}
public interface UserDailyLoginDurationMapper extends BaseMapper<UserDailyLoginDuration> { // 定义UserDailyLoginDurationMapper接口继承BaseMapper<UserDailyLoginDuration>提供对UserDailyLoginDuration实体的基础CRUD操作
/**
* ID
*
* @param userId ID
* @return DailyVO
*/
List<DailyVO> getDaily(Integer userId); // 自定义查询方法根据用户ID获取用户的每日登录时长统计信息并封装为DailyVO对象列表返回
}

@ -1,66 +1,83 @@
package cn.org.alan.exam.mapper;
package cn.org.alan.exam.mapper; // 定义UserExamsScoreMapper接口所在的包路径
import cn.org.alan.exam.model.entity.UserExamsScore;
import cn.org.alan.exam.model.vo.answer.UncorrectedUserVO;
import cn.org.alan.exam.model.vo.score.ExportScoreVO;
import cn.org.alan.exam.model.vo.score.GradeScoreVO;
import cn.org.alan.exam.model.vo.score.UserScoreVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
// 引入相关的实体类和视图对象
import cn.org.alan.exam.model.entity.UserExamsScore; // 引入UserExamsScore实体类表示用户的考试成绩
import cn.org.alan.exam.model.vo.answer.UncorrectedUserVO; // 引入UncorrectedUserVO视图对象用于封装未考试用户的信息
import cn.org.alan.exam.model.vo.score.ExportScoreVO; // 引入ExportScoreVO视图对象用于导出考试成绩
import cn.org.alan.exam.model.vo.score.GradeScoreVO; // 引入GradeScoreVO视图对象用于封装班级用户的成绩分析信息
import cn.org.alan.exam.model.vo.score.UserScoreVO; // 引入UserScoreVO视图对象用于封装分页查询的用户成绩信息
import java.util.List;
// 引入MyBatis Plus的相关类
import com.baomidou.mybatisplus.core.mapper.BaseMapper; // 引入BaseMapper接口提供基础的CRUD操作
import com.baomidou.mybatisplus.core.metadata.IPage; // 引入IPage接口用于分页查询
import java.util.List; // 引入Java的List接口用于定义返回结果类型或传递参数类型
/**
* <p>
* Mapper
* Mapper UserExamsScore
* </p>
*
* @author WeiJin
* @since 2024-03-21
* @author WeiJin // 作者信息
* @since 2024-03-21 // 创建时间
*/
public interface UserExamsScoreMapper extends BaseMapper<UserExamsScore> {
public interface UserExamsScoreMapper extends BaseMapper<UserExamsScore> { // 定义UserExamsScoreMapper接口继承BaseMapper<UserExamsScore>提供对UserExamsScore实体的基础CRUD操作
/**
*
* @param page
* @param gradeId Id
* @param examTitle
* @param userId Id
* @param roleId Id
* @return
*
* @param page
* @param gradeId Id
* @param examTitle
* @param userId Idnull
* @param roleId Id
* @return
*/
IPage<GradeScoreVO> scoreStatistics(IPage<GradeScoreVO> page ,Integer gradeId, String examTitle,Integer userId,Integer roleId);
IPage<GradeScoreVO> scoreStatistics(IPage<GradeScoreVO> page, Integer gradeId, String examTitle, Integer userId, Integer roleId);
/**
*
*
* @param page
* @param gradeId Id
* @param examId Id
* @param realName
* @return
* @param page
* @param gradeId Id
* @param examId Id
* @param realName null
* @return
*/
IPage<UserScoreVO> pagingScore(IPage<UserScoreVO> page, Integer gradeId, Integer examId, String realName);
/**
*
*
* @param examId Id
* @return
*/
Integer getNumberOfApplicants(Integer examId);
/**
*
*
* @param examId Id
* @return
*/
Integer getCorrectedPaper(Integer examId);
/**
*
*
*
* @param examId id
* @param gradeId id
* @return
* @param examId id
* @param gradeId idnull
* @return idid
*/
List<ExportScoreVO> selectScores(Integer examId, Integer gradeId);
/**
* id
*
* @param page
* @param examId id
* @return
* @param page
* @param examId id
* @param realName null
* @return
*/
IPage<UncorrectedUserVO> uncorrectedUser(IPage<UncorrectedUserVO> page, Integer examId,String realName);
}
IPage<UncorrectedUserVO> uncorrectedUser(IPage<UncorrectedUserVO> page, Integer examId, String realName);
}

@ -1,16 +1,24 @@
// 定义UserExerciseRecordMapper接口所在的包路径
package cn.org.alan.exam.mapper;
// 引入UserExerciseRecord实体类该类表示用户的练习记录
import cn.org.alan.exam.model.entity.UserExerciseRecord;
// 引入MyBatis Plus的BaseMapper接口它提供了基础的CRUD操作
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* Mapper
* Mapper UserExerciseRecord
* </p>
* <p>
* BaseMapper<UserExerciseRecord>UserExerciseRecordCRUD
* </p>
*
* @author WeiJin
* @since 2024-03-21
* @author WeiJin // 作者信息
* @since 2024-03-21 // 接口创建时间
*/
public interface UserExerciseRecordMapper extends BaseMapper<UserExerciseRecord> {
}
// 此处为空因为该接口仅继承了BaseMapper未定义额外的方法。
// 若需要自定义数据库操作方法,可在此处添加。
}

@ -1,34 +1,43 @@
// 定义UserMapper接口所在的包路径
package cn.org.alan.exam.mapper;
// 引入User实体类代表用户信息
import cn.org.alan.exam.model.entity.User;
// 引入ClassCountResult类用于统计和分组的结果
import cn.org.alan.exam.model.form.count.ClassCountResult;
// 引入UserVO类用于视图层展示的用户信息
import cn.org.alan.exam.model.vo.UserVO;
// 引入MyBatis Plus的BaseMapper接口提供基础的CRUD操作
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
// 引入MyBatis Plus的IPage接口用于分页查询
import com.baomidou.mybatisplus.core.metadata.IPage;
// 引入Spring的Repository注解标记这是一个持久层组件
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* <p>
* Mapper
* Mapper User
* </p>
*
* @author WeiJin
* @since 2024-03-21
* @author WeiJin // 作者信息
* @since 2024-03-21 // 接口创建时间
*/
@Repository
public interface UserMapper extends BaseMapper<User> {
@Repository // 标记为Spring的Repository组件
public interface UserMapper extends BaseMapper<User> { // 继承BaseMapper<User>提供User的基础CRUD操作
// 已被注释的方法原意是根据id获取用户列表但通常id应唯一此处逻辑可能有误
// List<User> getText(Integer id);
// 根据用户ID列表删除用户等级信息注意方法名可能不完全准确反映其功能需根据业务逻辑确认
Integer removeUserGrade(List<Integer> userIds);
/**
*
*
* @param list
* @return
* @param list
* @return
*/
Integer insertBatchUser(List<User> list);
@ -36,40 +45,51 @@ public interface UserMapper extends BaseMapper<User> {
*
*
* @param userId id
* @return
* @return UserVO
*/
UserVO info(Integer userId);
/**
* IdId
* IdId
*
* @param classId Id
* @return
* @return Id
*/
List<Integer> selectIdsByClassId(Integer classId);
/**
*
*
* @param page
* @param gradeId Id
* @param realName
* @param roleId Id
* @return
* @param page
* @param gradeId Id
* @param realName
* @param userId IdremoveUserGradeuserIdsId
* @param roleId Id
* @return
*/
IPage<UserVO> pagingUser(IPage<UserVO> page, Integer gradeId, String realName,Integer userId, Integer roleId);
IPage<UserVO> pagingUser(IPage<UserVO> page, Integer gradeId, String realName, Integer userId, Integer roleId);
/**
*
* Id
*
* @param gradeIds id
* @return
*/
Integer removeGradeIdByGradeIds(List<Integer> gradeIds);
/**
* Id
*
* @param roleId Id
* @return IdId
*/
List<ClassCountResult> countAndGroupByGradeAndRoleId(Integer roleId);
/**
* Id
*
* @param userIds Id
* @return
*/
Integer deleteByUserIds(List<Integer> userIds);
}
}

@ -1,6 +1,5 @@
package cn.org.alan.exam.security;
import cn.org.alan.exam.model.entity.User;
import lombok.Data;
import lombok.NoArgsConstructor;
@ -12,59 +11,75 @@ import java.util.Collection;
import java.util.List;
/**
* Spring Security
* Spring Security Spring SecurityUserDetails
* 便
*
* @author haoxr
* @since 3.0.0
*/
// 使用Lombok的 @Data注解自动生成类的Getter、Setter、toString、equals、hashCode等方法简化代码编写
@Data
// 使用Lombok的 @NoArgsConstructor注解自动生成无参构造函数方便对象的创建和初始化
@NoArgsConstructor
public class SysUserDetails implements UserDetails {
// 用于存储用户的权限列表权限类型为SimpleGrantedAuthority它是Spring Security中表示权限的一种简单实现通常包含权限名称如 "ROLE_ADMIN"、"ROLE_USER" 等)
private List<SimpleGrantedAuthority> permissions;
// 关联的实际业务中的用户实体对象,包含了如用户名、密码、用户其他属性等详细信息,用于获取和传递与用户相关的业务数据
private User user;
// 用户名用于在Spring Security认证等流程中作为用户的标识与UserDetails接口中定义的方法对应此处可能后续会根据业务逻辑有具体赋值或使用情况
private String username;
// 构造函数接收一个User对象用于初始化SysUserDetails对象时关联对应的业务用户实体方便后续获取用户相关信息进行认证授权等操作
public SysUserDetails(User user) {
this.user = user;
}
// 实现UserDetails接口的方法用于获取用户的权限集合在Spring Security的授权流程中会调用该方法来判断用户具有哪些权限这里直接返回之前定义的permissions列表
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return permissions;
}
// 设置用户权限的方法,外部可以通过调用该方法来为当前用户对象设置相应的权限列表,方便在用户权限管理等业务场景中更新用户权限信息
public void setPermissions(List<SimpleGrantedAuthority> permissions) {
this.permissions = permissions;
}
// 实现UserDetails接口的方法用于获取用户的密码从关联的User实体对象中获取密码信息并在获取后将User实体中的密码字段设置为空字符串可能出于安全考虑防止密码在后续不必要的场景中被意外获取到然后返回密码
@Override
public String getPassword() {
String myPassword=user.getPassword();
String myPassword = user.getPassword();
user.setPassword("");
return myPassword;
}
// 实现UserDetails接口的方法用于获取用户的用户名这里从关联的User实体对象中获取用户名通常是业务中定义的用户标识字段如账号名等并返回作为Spring Security认证流程中识别用户的重要依据
@Override
public String getUsername() {
return user.getUserName();
}
// 实现UserDetails接口的方法用于判断用户账号是否未过期在Spring Security的认证逻辑中会参考该方法的返回值来确定账号有效性这里直接返回true表示默认账号未过期可根据实际业务需求进行具体逻辑修改
@Override
public boolean isAccountNonExpired() {
return true;
}
// 实现UserDetails接口的方法用于判断用户账号是否未锁定同样在Spring Security的认证检查中起作用返回true表示默认账号未锁定可根据业务规则调整该逻辑比如根据数据库中账号锁定字段等来判断
@Override
public boolean isAccountNonLocked() {
return true;
}
// 实现UserDetails接口的方法用于判断用户的凭证通常就是密码是否未过期此处返回true表示默认凭证未过期若有密码有效期等业务需求可在此处实现相应逻辑判断
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 实现UserDetails接口的方法用于判断用户账号是否启用返回true表示默认用户账号是启用状态可根据业务情况如用户是否被禁用的标识字段等来具体实现该方法的逻辑判断
@Override
public boolean isEnabled() {
return true;
}
}
}

@ -40,177 +40,223 @@ import java.util.stream.Collectors;
*/
@Service
public class GradeServiceImpl extends ServiceImpl<GradeMapper, Grade> implements IGradeService {
// 注入GradeMapper用于操作班级数据的持久层接口
@Resource
private GradeMapper gradeMapper;
// 注入ExamMapper可能用于操作考试相关数据的持久层接口此处从命名推测
@Resource
private ExamMapper examMapper;
// 注入QuestionMapper可能用于操作题目相关数据的持久层接口此处从命名推测
@Resource
private QuestionMapper questionMapper;
// 注入GradeConverter用于不同班级相关对象之间的转换比如表单对象和实体对象等
@Resource
private GradeConverter gradeConverter;
// 注入UserMapper用于操作用户相关数据的持久层接口
@Resource
private UserMapper userMapper;
// 注入IGradeService自身接口注入可能用于调用自身定义的其他服务方法此处从代码结构推测
@Resource
private IGradeService gradeService;
// 注入StringRedisTemplate用于操作Redis字符串类型数据常用于缓存相关操作
@Resource
private StringRedisTemplate stringRedisTemplate;
// 注入CacheClient自定义的缓存客户端用于更方便地进行缓存操作
@Resource
private CacheClient cacheClient;
// 添加班级的方法,事务性操作,成功返回添加成功的提示信息,失败返回相应错误提示
@Override
@Transactional
public Result<String> addGrade(GradeForm gradeForm) {
// 生成班级口令
// 生成班级口令调用工具类方法生成一个长度为18的班级口令
gradeForm.setCode(ClassTokenGenerator.generateClassToken(18));
// 实体转换
// 将GradeForm对象转换为Grade实体对象方便后续持久化操作
Grade grade = gradeConverter.formToEntity(gradeForm);
// 添加数据
// 调用gradeMapper的insert方法将班级数据插入到数据库中返回受影响的行数
int rows = gradeMapper.insert(grade);
// 如果插入操作没有影响到任何行,即插入失败,返回添加失败的结果信息
if (rows == 0) {
return Result.failed("添加失败");
}
// 更新缓存
if (grade.getId() != null) { // 确保ID有效
// 如果是更新操作,先从缓存中移除旧数据,然后重新放入最新的数据
stringRedisTemplate.delete("cache:grade:getPaging:"+grade.getId().toString()); // 删除旧缓存
// 如果插入成功班级数据有了有效的ID这里假设插入后会自动生成ID进行缓存相关操作
if (grade.getId()!= null) { // 确保ID有效
// 如果是更新操作(此处逻辑可能不太准确,实际上是添加操作,但代码结构类似更新时的缓存处理逻辑),先从缓存中移除旧数据
// 根据班级ID删除对应的缓存数据缓存键格式为"cache:grade:getPaging:" + 班级ID
stringRedisTemplate.delete("cache:grade:getPaging:" + grade.getId().toString());
// GradeVO updatedGradeVO = gradeConverter.GradeToGradeVO(grade); // 转换为视图对象
// Map<Integer, GradeVO> map = Map.of(updatedGradeVO.getId(), updatedGradeVO);
// cacheClient.batchPut("cache:grade:getPaging:",map,10L,TimeUnit.MINUTES); // 存储新数据
}
stringRedisTemplate.delete("cache:grade:getAllGrade:"+SecurityUtil.getUserId());
// 根据当前用户ID删除获取所有班级的缓存数据缓存键格式为"cache:grade:getAllGrade:" + 当前用户ID
stringRedisTemplate.delete("cache:grade:getAllGrade:" + SecurityUtil.getUserId());
// 返回添加成功的结果信息
return Result.success("添加成功");
}
// 更新班级信息的方法,事务性操作,成功返回更新成功的提示信息,失败返回相应错误提示
@Override
@Transactional
public Result<String> updateGrade(Integer id, GradeForm gradeForm) {
// 建立更新条件
// 创建一个LambdaUpdateWrapper对象用于构建更新班级信息的条件和要更新的字段
LambdaUpdateWrapper<Grade> gradeUpdateWrapper = new LambdaUpdateWrapper<>();
// 设置要更新的班级名称字段值为传入的gradeForm中的班级名称
gradeUpdateWrapper
.set(Grade::getGradeName, gradeForm.getGradeName())
// 设置更新条件为班级ID等于传入的id参数
.eq(Grade::getId, id);
// 更新班级
// 调用gradeMapper的update方法根据上面构建的更新条件和要更新的字段来更新班级数据,返回受影响的行数
int rows = gradeMapper.update(gradeUpdateWrapper);
// 如果更新操作没有影响到任何行,即更新失败,返回修改失败的结果信息
if (rows == 0) {
return Result.failed("修改失败");
}
// 根据ID获取更新后的班级对象用于后续缓存操作等可能需要获取最新数据来更新缓存等
Grade byId = getById(id);
// 更新缓存
if (byId.getId() != null) { // 确保ID有效
// 如果是更新操作,先从缓存中移除旧数据,然后重新放入最新的数据
stringRedisTemplate.delete("cache:grade:getPaging:"+byId.getId().toString()); // 删除旧缓存
// 如果获取到的班级对象有有效的ID进行缓存相关操作
if (byId.getId()!= null) { // 确保ID有效
// 如果是更新操作,先从缓存中移除旧数据,根据班级ID删除对应的缓存数据缓存键格式为"cache:grade:getPaging:" + 班级ID
stringRedisTemplate.delete("cache:grade:getPaging:" + byId.getId().toString());
// GradeVO updatedGradeVO = gradeConverter.GradeToGradeVO(byId); // 转换为视图对象
// Map<Integer, GradeVO> map = Map.of(updatedGradeVO.getId(), updatedGradeVO);
// cacheClient.batchPut("cache:grade:getPaging:",map,10L,TimeUnit.MINUTES); // 存储新数据
}
stringRedisTemplate.delete("cache:grade:getAllGrade:"+SecurityUtil.getUserId());
// 根据当前用户ID删除获取所有班级的缓存数据缓存键格式为"cache:grade:getAllGrade:" + 当前用户ID
stringRedisTemplate.delete("cache:grade:getAllGrade:" + SecurityUtil.getUserId());
// 返回更新成功的结果信息
return Result.success("修改成功");
}
// 删除班级的方法,事务性操作,成功返回删除成功的提示信息,失败返回相应错误提示
@Override
@Transactional
public Result<String> deleteGrade(Integer id) {
// 删除班级
// 创建一个LambdaUpdateWrapper对象用于构建删除班级的逻辑(这里实际是逻辑删除,设置删除标记字段)
LambdaUpdateWrapper<Grade> gradeLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
gradeLambdaUpdateWrapper.eq(Grade::getId,id)
.set(Grade::getIsDeleted,1);
// 设置更新条件为班级ID等于传入的id参数
gradeLambdaUpdateWrapper.eq(Grade::getId, id)
// 设置班级的删除标记字段假设名为isDeleted为1表示已删除
.set(Grade::getIsDeleted, 1);
// 调用gradeMapper的update方法根据上面构建的更新条件来执行逻辑删除操作返回受影响的行数
int rows = gradeMapper.update(gradeLambdaUpdateWrapper);
// 如果删除操作没有影响到任何行,即删除失败,返回删除失败的结果信息
if (rows == 0) {
return Result.failed("删除失败");
}
// 删除缓存
stringRedisTemplate.delete("cache:grade:getPaging:"+id.toString());
stringRedisTemplate.delete("cache:grade:getAllGrade:"+SecurityUtil.getUserId());
// 根据班级ID删除对应的缓存数据缓存键格式为"cache:grade:getPaging:" + 班级ID
stringRedisTemplate.delete("cache:grade:getPaging:" + id.toString());
// 根据当前用户ID删除获取所有班级的缓存数据缓存键格式为"cache:grade:getAllGrade:" + 当前用户ID
stringRedisTemplate.delete("cache:grade:getAllGrade:" + SecurityUtil.getUserId());
// 返回删除成功的结果信息
return Result.success("删除成功");
}
// 获取班级分页数据的方法,返回包含分页信息和班级视图对象列表的结果
@Override
public Result<IPage<GradeVO>> getPaging(Integer pageNum, Integer pageSize, String gradeName) {
// 初始化角色变量默认为0这里可能根据不同角色有不同的查询逻辑从后续代码推测
Integer role = 0;
if("role_teacher".equals(SecurityUtil.getRole())){
// 如果当前用户角色为"role_teacher"则将角色设置为2具体角色值含义根据业务定义
if ("role_teacher".equals(SecurityUtil.getRole())) {
role = 2;
}
// 查询满足条件的总记录数
int total = gradeMapper.countByCondition(SecurityUtil.getUserId(), gradeName,role); // 假设gradeMapper中实现了根据条件计数的方法
// 计算偏移量
// 调用gradeMapper的countByCondition方法根据当前用户ID、班级名称和角色来统计满足条件的班级总记录数假设gradeMapper中实现了该方法
int total = gradeMapper.countByCondition(SecurityUtil.getUserId(), gradeName, role);
// 计算分页查询的偏移量,根据当前页码和每页显示数量来计算
int offset = (pageNum - 1) * pageSize;
// 查询分页ID列表
List<Integer> gradeIds = gradeMapper.selectGradeIdsPage(SecurityUtil.getUserId(), role ,gradeName, offset, pageSize);
// 调用gradeMapper的selectGradeIdsPage方法根据当前用户ID、角色、班级名称以及偏移量和每页显示数量来查询分页的班级ID列表假设gradeMapper中实现了该方法
List<Integer> gradeIds = gradeMapper.selectGradeIdsPage(SecurityUtil.getUserId(), role, gradeName, offset, pageSize);
// 批量从缓存中获取GradeVO对象
Map<Integer, GradeVO> cachedGradesMap = cacheClient.batchGet("cache:grade:getPaging:",gradeIds, GradeVO.class);
// 使用cacheClient的batchGet方法从缓存中批量获取对应班级ID的GradeVO对象缓存键前缀为"cache:grade:getPaging:"
Map<Integer, GradeVO> cachedGradesMap = cacheClient.batchGet("cache:grade:getPaging:", gradeIds, GradeVO.class);
// 确定未命中的ID列表
// 创建一个列表用于存储在缓存中未命中的班级ID即缓存中不存在对应数据的班级ID
List<Integer> missIds = new ArrayList<>();
// 遍历查询到的班级ID列表检查每个ID在缓存中是否存在如果不存在则添加到未命中ID列表中
for (Integer id : gradeIds) {
if (!cachedGradesMap.containsKey(id)) {
missIds.add(id);
}
}
// 如果有未命中的ID从数据库批量查询并更新缓存
// 如果存在未命中缓存的班级ID即missIds列表不为空
if (!missIds.isEmpty()) {
// 调用gradeMapper的batchSelectByIds方法从数据库中批量查询未命中缓存的班级数据并转换为GradeVO对象列表假设gradeMapper中实现了该方法
List<GradeVO> missedGrades = gradeMapper.batchSelectByIds(missIds);
// 假设GradeVO的ID为getId()使用Collectors.toMap转换
// 使用Java 8的Stream API将GradeVO对象列表转换为以班级ID为键GradeVO对象为值的Map方便后续操作
Map<Integer, GradeVO> missedGradesMap = missedGrades.stream()
.collect(Collectors.toMap(GradeVO::getId, Function.identity()));
// 更新缓存
cacheClient.batchPut("cache:grade:getPaging:",missedGradesMap,10L,TimeUnit.MINUTES);
// 合并缓存结果
// 使用cacheClient的batchPut方法将从数据库中查询到的未命中缓存的班级数据更新缓存缓存有效期为10分钟
cacheClient.batchPut("cache:grade:getPaging:", missedGradesMap, 10L, TimeUnit.MINUTES);
// 将从数据库中查询到的未命中缓存的班级数据合并缓存结果的Map中确保缓存数据的完整性
cachedGradesMap.putAll(missedGradesMap);
}
// 根据ID列表从缓存中获取完整的GradeVO对象列表
// 创建一个列表用于存储最终要返回的完整的GradeVO对象列表初始大小为查询到的班级ID列表的大小
List<GradeVO> finalResult = new ArrayList<>(gradeIds.size());
// 遍历班级ID列表从缓存结果的Map中获取对应的GradeVO对象并添加到最终结果列表中
for (Integer id : gradeIds) {
finalResult.add(cachedGradesMap.get(id));
}
// 构建并返回IPage对象
// 创建一个IPage对象用于封装分页信息和最终的班级视图对象列表设置当前页码、每页显示数量以及总记录数
IPage<GradeVO> resultPage = new Page<>(pageNum, pageSize, Long.valueOf(total));
// 将最终的班级视图对象列表设置到IPage对象中
resultPage.setRecords(finalResult);
// 返回包含分页信息和班级视图对象列表的成功结果信息
return Result.success("查询成功", resultPage);
}
// 移除用户班级关联的方法,成功返回移除成功的提示信息,失败返回相应错误提示
@Override
public Result<String> removeUserGrade(String ids) {
// 字符串转换为列表
// 将传入的以逗号分隔的字符串形式的用户ID列表转换为Integer类型的列表使用Java 8的Stream API进行转换
List<Integer> userIds = Arrays.stream(ids.split(","))
.map(Integer::parseInt)
.toList();
// 移出班级
// 调用userMapper的removeUserGrade方法根据用户ID列表移除用户与班级的关联关系返回受影响的行数假设userMapper中实现了该方法
int rows = userMapper.removeUserGrade(userIds);
// 如果移除操作没有影响到任何行,即移除失败,返回移除失败的结果信息
if (rows == 0) {
return Result.failed("移除失败");
}
userIds.forEach(id->{
stringRedisTemplate.delete("cache:grade:getPaging:"+id.toString());
// 遍历用户ID列表根据每个用户ID删除对应的获取班级分页数据的缓存这里的逻辑可能需要根据实际业务进一步确认是否合理
userIds.forEach(id -> {
stringRedisTemplate.delete("cache:grade:getPaging:" + id.toString());
});
// 返回移除成功的结果信息
return Result.success("移除成功");
}
// 获取所有班级信息的方法,返回包含所有班级视图对象列表的结果
@Override
public Result<List<GradeVO>> getAllGrade() {
// 创建一个匿名内部类实现Function接口用于定义从用户ID获取班级视图对象列表的逻辑
Function<Integer, List> function = new Function<>() {
@Override
public List apply(Integer userId) {
// 创建一个LambdaQueryWrapper对象用于构建查询班级信息的条件这里查询当前用户ID下未删除的班级假设isDeleted字段表示是否删除
LambdaQueryWrapper<Grade> gradeLambdaQueryWrapper = new LambdaQueryWrapper<>();
gradeLambdaQueryWrapper.eq(Grade::getUserId, userId)
.eq(Grade::getIsDeleted,0);
.eq(Grade::getIsDeleted, 0);
// 调用gradeMapper的selectList方法根据上面构建的查询条件获取班级实体对象列表
List<Grade> grades = gradeMapper.selectList(gradeLambdaQueryWrapper);
// 使用gradeConverter的listEntityToVo方法将班级实体对象列表转换为班级视图对象列表
List<GradeVO> gradeVOS = gradeConverter.listEntityToVo(grades);
// 返回班级视图对象列表
return gradeVOS;
}
};
// 使用cacheClient的queryWithPassThrough方法通过缓存穿透机制获取所有班级信息缓存键前缀为"cache:grade:getAllGrade:"传入当前用户ID、期望返回的类型、查询逻辑函数、返回对象类型、缓存有效期等参数
List<GradeVO> result = cacheClient.queryWithPassThrough("cache:grade:getAllGrade:", SecurityUtil.getUserId(), List.class, function, GradeVO.class, 10L, TimeUnit.MINUTES);
if(result==null){
// 如果获取结果为null即查询失败返回查询失败的结果信息
if (result == null) {
return Result.failed("查询失败");
}
// 返回包含所有班级视图对象列表的成功结果信息
return Result.success("查询成功", result);
}
}
}

@ -23,134 +23,177 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* <p>
*
* </p>
*
* @author WeiJin
* @since 2024-03-21
*/
@Service
public class ManualScoreServiceImpl extends ServiceImpl<ManualScoreMapper, ManualScore> implements IManualScoreService {
// 注入ExamMapper用于操作考试相关数据的持久层接口从命名推测其功能
@Resource
private ExamMapper examMapper;
// 注入ExamGradeMapper可能用于操作考试与班级关联相关数据的持久层接口根据命名推测
@Resource
private ExamGradeMapper examGradeMapper;
// 注入UserExamsScoreMapper用于操作用户考试成绩相关数据的持久层接口推测用途
@Resource
private UserExamsScoreMapper userExamsScoreMapper;
// 注入ExamQuAnswerMapper可能用于操作考试题目作答相关数据的持久层接口从名字判断
@Resource
private ExamQuAnswerMapper examQuAnswerMapper;
// 注入ManualScoreMapper用于操作手动评分相关数据的持久层接口对应业务功能
@Resource
private ManualScoreMapper manualScoreMapper;
// 注入CertificateUserMapper可能用于操作证书与用户关联相关数据的持久层接口依据命名猜测
@Resource
private CertificateUserMapper certificateUserMapper;
// 获取用户答题详情的方法根据用户ID和考试ID从数据库获取相关答题详情信息并返回
@Override
public Result<List<UserAnswerDetailVO>> getDetail(Integer userId, Integer examId) {
// 调用examQuAnswerMapper的selectUserAnswer方法传入用户ID和考试ID获取用户答题详情列表
List<UserAnswerDetailVO> list = examQuAnswerMapper.selectUserAnswer(userId, examId);
// 返回包含答题详情列表的成功结果信息这里第一个参数为null可能是预留用于传递其他提示信息等情况
return Result.success(null, list);
}
// 批改答题的方法,事务性操作,用于处理答题批改相关逻辑,包括记录手动评分、更新用户考试记录、颁发证书等操作
@Override
@Transactional
public Result<String> correct(List<CorrectAnswerFrom> correctAnswerFroms) {
// 创建一个ManualScore类型的列表用于存储要插入数据库的手动评分记录初始大小为传入的答题批改表单列表的大小
List<ManualScore> list = new ArrayList<>(correctAnswerFroms.size());
// 创建一个AtomicInteger对象用于原子性地累加手动评分的总分方便在多线程环境下安全地更新总分虽然此处可能并非多线程场景但使用它保证操作的原子性
AtomicInteger manualTotalScore = new AtomicInteger();
// 遍历传入的答题批改表单列表,处理每个答题的批改信息
correctAnswerFroms.forEach(correctAnswerFrom -> {
//获取用户作答信息id
// 创建一个LambdaQueryWrapper对象用于构建查询考试题目作答信息的条件这里只选择作答信息的ID字段
LambdaQueryWrapper<ExamQuAnswer> wrapper = new LambdaQueryWrapper<ExamQuAnswer>()
.select(ExamQuAnswer::getId)
// 设置查询条件为考试ID等于当前答题批改表单中的考试ID
.eq(ExamQuAnswer::getExamId, correctAnswerFrom.getExamId())
// 设置查询条件为用户ID等于当前答题批改表单中的用户ID
.eq(ExamQuAnswer::getUserId, correctAnswerFrom.getUserId())
// 设置查询条件为题目ID等于当前答题批改表单中的题目ID
.eq(ExamQuAnswer::getQuestionId, correctAnswerFrom.getQuestionId());
// 创建一个ManualScore对象用于存储当前答题的手动评分记录信息
ManualScore manualScore = new ManualScore();
// 设置手动评分记录中的考试题目作答信息ID通过查询获取对应的作答信息ID并设置
manualScore.setExamQuAnswerId(examQuAnswerMapper.selectOne(wrapper).getId());
// 设置手动评分记录中的分数,值为当前答题批改表单中的分数
manualScore.setScore(correctAnswerFrom.getScore());
// 将当前手动评分记录添加到列表中,后续批量插入数据库
list.add(manualScore);
// 原子性地累加当前答题的分数到总分中
manualTotalScore.addAndGet(correctAnswerFrom.getScore());
});
// 调用manualScoreMapper的insertList方法将整理好的手动评分记录列表批量插入到数据库中
manualScoreMapper.insertList(list);
//把用户考试记录修改为已批改,并把简答题分数添加进去
// 把用户考试记录修改为已批改,并把简答题分数添加进去
// 获取答题批改表单列表中的第一个表单对象,用于后续构建更新用户考试记录的条件(这里假设所有表单对应的考试和用户是相同的,若不同则逻辑可能需要调整)
CorrectAnswerFrom correctAnswerFrom = correctAnswerFroms.get(0);
// 创建一个LambdaUpdateWrapper对象用于构建更新用户考试成绩记录的条件和要更新的字段
LambdaUpdateWrapper<UserExamsScore> userExamsScoreLambdaUpdateWrapper = new LambdaUpdateWrapper<UserExamsScore>()
// 设置更新条件为考试ID等于当前答题批改表单中的考试ID
.eq(UserExamsScore::getExamId, correctAnswerFrom.getExamId())
// 设置更新条件为用户ID等于当前答题批改表单中的用户ID
.eq(UserExamsScore::getUserId, correctAnswerFrom.getUserId())
// 设置要更新的字段为是否已批改标记为1表示已批改
.set(UserExamsScore::getWhetherMark, 1)
// 使用SQL语句设置更新用户成绩将原用户成绩加上手动评分的总分这里使用了原生SQL语句拼接可能存在SQL注入风险若有安全需求可考虑参数化方式
.setSql("user_score = user_score + " + manualTotalScore.get());
// 调用userExamsScoreMapper的update方法根据构建的更新条件和字段来更新用户考试成绩记录
userExamsScoreMapper.update(userExamsScoreLambdaUpdateWrapper);
//根据该考试是否有证书来给用户颁发对应证书
//判断该考试是否有证书
// 根据该考试是否有证书来给用户颁发对应证书
// 判断该考试是否有证书
LambdaQueryWrapper<Exam> examWrapper = new LambdaQueryWrapper<Exam>()
// 选择查询考试的ID、证书ID以及及格分数字段
.select(Exam::getId, Exam::getCertificateId, Exam::getPassedScore)
// 设置查询条件为考试ID等于当前答题批改表单中的考试ID
.eq(Exam::getId, correctAnswerFrom.getExamId());
// 调用examMapper的selectOne方法根据构建的查询条件获取对应的考试信息对象
Exam exam = examMapper.selectOne(examWrapper);
//不必对exam做非空验证这里一定不为null
if (exam.getCertificateId() != null && exam.getCertificateId() > 0) {
//有证书 获取用户得分
// 不必对exam做非空验证这里一定不为null(此处根据代码逻辑的前置假设,认为该查询必然能获取到数据,如果实际情况可能为空则需要添加空值判断处理)
if (exam.getCertificateId()!= null && exam.getCertificateId() > 0) {
// 有证书 获取用户得分
LambdaQueryWrapper<UserExamsScore> examsScoreWrapper = new LambdaQueryWrapper<UserExamsScore>()
// 选择查询用户考试成绩记录的ID和用户分数字段
.select(UserExamsScore::getId, UserExamsScore::getUserScore)
// 设置查询条件为考试ID等于当前答题批改表单中的考试ID
.eq(UserExamsScore::getExamId, correctAnswerFrom.getExamId())
// 设置查询条件为用户ID等于当前答题批改表单中的用户ID
.eq(UserExamsScore::getUserId, correctAnswerFrom.getUserId());
// 调用userExamsScoreMapper的selectOne方法根据构建的查询条件获取对应的用户考试成绩记录对象
UserExamsScore userExamsScore = userExamsScoreMapper.selectOne(examsScoreWrapper);
//不必对userExamsScore做非空验证这里一定不为null
// 不必对userExamsScore做非空验证这里一定不为null(同样基于前置假设,如果实际可能为空需处理)
if (userExamsScore.getUserScore() >= exam.getPassedScore()) {
//分数合格,判罚证书
// 分数合格,判罚证书(这里可能是“颁发”证书的意思,疑似笔误写为“判罚”,以下按颁发理解)
CertificateUser certificateUser = new CertificateUser();
certificateUser.setUserId(correctAnswerFrom.getUserId());
certificateUser.setExamId(correctAnswerFrom.getExamId());
certificateUser.setCode(ClassTokenGenerator.generateClassToken(18));
certificateUser.setCertificateId(exam.getCertificateId());
// 调用certificateUserMapper的insert方法将生成的证书与用户关联信息插入到数据库中完成证书颁发操作
certificateUserMapper.insert(certificateUser);
}
}
// 返回批改成功的结果信息
return Result.success("批改成功");
}
// 获取考试分页信息的方法,根据页码、每页数量以及考试名称等条件,返回包含相关考试信息的分页结果
@Override
public Result<IPage<AnswerExamVO>> examPage(Integer pageNum, Integer pageSize,String examName) {
public Result<IPage<AnswerExamVO>> examPage(Integer pageNum, Integer pageSize, String examName) {
// 创建一个Page对象用于封装分页相关信息设置当前页码和每页显示数量
Page<AnswerExamVO> page = new Page<>(pageNum, pageSize);
//获取自己创建的考试
List<AnswerExamVO> list = examMapper.selectMarkedList(page, SecurityUtil.getUserId(), SecurityUtil.getRole(),examName).getRecords();
// 调用examMapper的selectMarkedList方法传入分页对象、当前用户ID、当前用户角色以及考试名称获取自己创建的考试信息列表并获取其记录列表这里假设selectMarkedList方法返回的是包含分页信息和记录的对象
List<AnswerExamVO> list = examMapper.selectMarkedList(page, SecurityUtil.getUserId(), SecurityUtil.getRole(), examName).getRecords();
//获取相关信息
// 遍历获取到的考试信息列表,为每个考试信息对象补充相关统计信息
list.forEach(answerExamVO -> {
//需要参加考试人数
// 设置需要参加考试的人数调用examGradeMapper的selectClassSize方法传入考试ID获取班级规模即需要参加考试的人数
answerExamVO.setClassSize(examGradeMapper.selectClassSize(answerExamVO.getExamId()));
//实际参加考试人数
// 设置实际参加考试的人数创建一个LambdaQueryWrapper对象用于构建查询条件查询对应考试的用户考试成绩记录数量即实际参加考试的人数
LambdaQueryWrapper<UserExamsScore> numberWrapper = new LambdaQueryWrapper<UserExamsScore>()
.eq(UserExamsScore::getExamId, answerExamVO.getExamId());
answerExamVO.setNumberOfApplicants(userExamsScoreMapper.selectCount(numberWrapper).intValue());
//已阅人数
// 设置已阅试卷的数量创建一个LambdaQueryWrapper对象用于构建查询条件查询对应考试中已批改是否已批改标记为1的用户考试成绩记录数量即已阅试卷的数量
LambdaQueryWrapper<UserExamsScore> correctedWrapper = new LambdaQueryWrapper<UserExamsScore>()
.eq(UserExamsScore::getWhetherMark, 1)
.eq(UserExamsScore::getExamId,answerExamVO.getExamId());
.eq(UserExamsScore::getExamId, answerExamVO.getExamId());
answerExamVO.setCorrectedPaper(userExamsScoreMapper.selectCount(correctedWrapper).intValue());
});
//移除不需要批改的试卷
// 移除不需要批改的试卷通过流操作过滤出需要批改neededMark为1的考试信息列表重新设置到分页对象的记录列表中
page.setRecords(list.stream()
.filter(answerExamVO -> answerExamVO.getNeededMark() == 1)
.toList());
// 返回包含处理后考试信息分页结果的成功结果信息这里第一个参数为null可能是预留用于传递其他提示信息等情况
return Result.success(null, page);
}
// 获取学生考试分页信息的方法根据页码、每页数量、考试ID以及学生真实姓名等条件返回包含未批改学生考试信息的分页结果
@Override
public Result<IPage<UncorrectedUserVO>> stuExamPage(Integer pageNum, Integer pageSize, Integer examId,String realName) {
public Result<IPage<UncorrectedUserVO>> stuExamPage(Integer pageNum, Integer pageSize, Integer examId, String realName) {
// 创建一个IPage对象用于封装分页相关信息设置当前页码和每页显示数量
IPage<UncorrectedUserVO> page = new Page<>(pageNum, pageSize);
page = userExamsScoreMapper.uncorrectedUser(page, examId,realName);
// 调用userExamsScoreMapper的uncorrectedUser方法传入分页对象、考试ID以及学生真实姓名获取未批改学生考试信息的分页结果并赋值给page对象
page = userExamsScoreMapper.uncorrectedUser(page, examId, realName);
// 返回包含未批改学生考试信息分页结果的成功结果信息这里第一个参数为null可能是预留用于传递其他提示信息等情况
return Result.success(null, page);
}
}
}

@ -1,20 +1,29 @@
package cn.org.alan.exam.service.impl;
// 导入NoticeGradeMapper接口用于操作与通知班级关联相关的数据持久化操作从命名推测功能
import cn.org.alan.exam.mapper.NoticeGradeMapper;
// 导入NoticeGrade实体类代表通知班级相关的实体对象
import cn.org.alan.exam.model.entity.NoticeGrade;
// 导入INoticeGradeService接口定义了通知班级相关的业务服务方法
import cn.org.alan.exam.service.INoticeGradeService;
// 导入ServiceImpl类这是MyBatis Plus提供的基础服务实现类方便进行常见的数据库操作服务实现
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
// 导入Spring的Service注解用于标记该类是一个服务层组件由Spring容器进行管理
import org.springframework.stereotype.Service;
/**
* <p>
*
*
* </p>
*
* @author WeiJin
* @since 2024-03-21
*/
// 使用@Service注解将该类标记为Spring容器管理的服务层组件方便在其他地方通过依赖注入使用
@Service
// 定义NoticeGradeServiceImpl类继承自ServiceImpl<NoticeGradeMapper, NoticeGrade>实现了INoticeGradeService接口
// 这样可以复用ServiceImpl中的通用数据库操作方法并按照INoticeGradeService接口定义实现具体业务逻辑
public class NoticeGradeServiceImpl extends ServiceImpl<NoticeGradeMapper, NoticeGrade> implements INoticeGradeService {
}
// 此处类体为空意味着目前它继承了ServiceImpl中的通用方法还没有额外实现INoticeGradeService接口中特定的业务方法
// 如果后续需要添加如添加通知班级、删除通知班级、查询通知班级等业务逻辑相关的方法,会在这里进行具体实现
}

@ -35,205 +35,266 @@ import java.util.function.Function;
import java.util.stream.Collectors;
/**
*
*
*
* @author Alan
* @since 2024-03-21
*/
@Service
// NoticeServiceImpl类继承自ServiceImpl<NoticeMapper, Notice>实现了INoticeService接口
// 这样可以复用ServiceImpl中的通用数据库操作方法并按照INoticeService接口定义实现具体业务逻辑
public class NoticeServiceImpl extends ServiceImpl<NoticeMapper, Notice> implements INoticeService {
// 注入NoticeMapper用于操作公告数据的持久层接口比如插入、删除、查询公告等操作
@Resource
private NoticeMapper noticeMapper;
// 注入NoticeConverter用于不同公告相关对象之间的转换比如表单对象和实体对象等
@Resource
private NoticeConverter noticeConverter;
// 注入NoticeGradeMapper可能用于操作公告与班级关联相关数据的持久层接口从命名推测其功能
@Resource
private NoticeGradeMapper noticeGradeMapper;
// 注入GradeMapper用于操作班级相关数据的持久层接口可能在公告关联班级等业务中会用到
@Resource
private GradeMapper gradeMapper;
// 注入CacheClient自定义的缓存客户端用于更方便地进行缓存操作如批量获取、批量存入缓存等
@Resource
private CacheClient cacheClient;
// 注入StringRedisTemplate用于操作Redis字符串类型数据常用于缓存相关操作比如删除缓存等
@Resource
private StringRedisTemplate stringRedisTemplate;
// 添加公告的方法,事务性操作,成功返回添加成功的提示信息,失败返回相应错误提示
@Override
@Transactional
public Result<String> addNotice(NoticeForm noticeForm) {
// 设置创建人
// 设置公告的创建人ID为当前登录用户的ID通过SecurityUtil工具类获取当前用户ID
noticeForm.setUserId(SecurityUtil.getUserId());
// 添加公告
// 将NoticeForm对象转换为Notice实体对象方便后续持久化操作使用NoticeConverter进行转换
Notice notice = noticeConverter.formToEntity(noticeForm);
// 调用noticeMapper的insert方法将公告数据插入到数据库中返回受影响的行数即插入操作影响的记录数
int rowsAffected = noticeMapper.insert(notice);
// 如果插入操作没有影响到任何行,即插入失败,返回添加失败的结果信息
if (rowsAffected == 0) {
return Result.failed("添加失败");
}
// 创建一个LambdaQueryWrapper对象用于构建查询班级信息的条件这里查询当前用户创建的班级通过用户ID关联
LambdaQueryWrapper<Grade> gradeLambdaQueryWrapper = new LambdaQueryWrapper<>();
gradeLambdaQueryWrapper.eq(Grade::getUserId,SecurityUtil.getUserId());
gradeLambdaQueryWrapper.eq(Grade::getUserId, SecurityUtil.getUserId());
// 调用gradeMapper的selectList方法根据上面构建的查询条件获取班级实体对象列表即获取当前用户创建的所有班级
List<Grade> grades = gradeMapper.selectList(gradeLambdaQueryWrapper);
// 获取刚插入公告的ID用于后续关联班级等操作假设插入后会自动生成ID
Integer noticeId = notice.getId();
if(grades.isEmpty()){
return Result.failed("您还没有创建班级,请先创建班级后再添加公告尝试");
// 如果当前用户没有创建任何班级,返回相应提示信息,提示先创建班级再添加公告
if (grades.isEmpty()) {
return Result.failed("您还没有创建班级,请先创建班级后再添加公告尝试");
}
int addNoticeGradeRow = noticeGradeMapper.addNoticeGrade(noticeId,grades);
// 调用noticeGradeMapper的addNoticeGrade方法将公告与班级进行关联传入公告ID和班级列表返回受影响的行数假设该方法实现了关联操作并返回影响行数
int addNoticeGradeRow = noticeGradeMapper.addNoticeGrade(noticeId, grades);
// 如果关联操作没有影响到任何行,即关联失败,返回添加失败的结果信息
if (addNoticeGradeRow == 0) {
return Result.failed("添加失败");
}
// 更新缓存
if (notice.getId() != null) { // 确保ID有效
// 如果是更新操作,先从缓存中移除旧数据,然后重新放入最新的数据
stringRedisTemplate.delete("cache:notice:getNotice:"+notice.getId().toString()); // 删除旧缓存
stringRedisTemplate.delete("cache:notice:getNewNotice:"+notice.getId().toString());
// 如果公告插入成功且关联班级等操作也成功公告数据有了有效的ID进行缓存相关操作
if (notice.getId()!= null) { // 确保ID有效
// 如果是更新操作(此处逻辑可能不太准确,实际上是添加操作,但代码结构类似更新时的缓存处理逻辑),先从缓存中移除旧数据
// 根据公告ID删除对应的缓存数据缓存键格式为"cache:notice:getNotice:" + 公告ID
stringRedisTemplate.delete("cache:notice:getNotice:" + notice.getId().toString());
// 根据公告ID删除对应的另一个缓存数据可能是用于获取新公告相关的缓存从命名推测缓存键格式为"cache:notice:getNewNotice:" + 公告ID
stringRedisTemplate.delete("cache:notice:getNewNotice:" + notice.getId().toString());
// NoticeVO updatedNoticeVO = noticeConverter.NoticeToNoticeVO(notice); // 转换为视图对象
// Map<Integer, NoticeVO> map = Map.of(updatedNoticeVO.getId(), updatedNoticeVO);
// cacheClient.batchPut("cache:notice:getNotice:",map,10L,TimeUnit.MINUTES); // 存储新数据
// cacheClient.batchPut("cache:notice:getNewNotice:",map,10L,TimeUnit.MINUTES); // 存储新数据
}
// 返回添加成功的结果信息
return Result.success("添加成功");
}
// 删除公告的方法,事务性操作,成功返回删除成功的提示信息,失败返回相应错误提示
@Override
@Transactional
public Result<String> deleteNotice(String ids) {
// 转换为集合
// 将传入的以逗号分隔的字符串形式的公告ID列表转换为Integer类型的列表使用Java 8的Stream API进行转换
List<Integer> noticeIds = Arrays.stream(ids.split(","))
.map(Integer::parseInt)
.toList();
// 删除公告
// 调用noticeMapper的removeNotice方法根据公告ID列表删除对应的公告数据,返回受影响的行数,即删除操作影响的记录数
int rowsAffected = noticeMapper.removeNotice(noticeIds);
// 如果删除操作没有影响到任何行,即删除失败,返回删除失败的结果信息
if (rowsAffected == 0) {
return Result.failed("删除失败");
}
noticeIds.forEach(id->{
stringRedisTemplate.delete("cache:notice:getNewNotice:"+id);
stringRedisTemplate.delete("cache:notice:getNotice:"+id);
// 遍历公告ID列表根据每个公告ID删除对应的两个缓存数据分别用于获取公告和获取新公告相关的缓存从缓存键命名推测
noticeIds.forEach(id -> {
stringRedisTemplate.delete("cache:notice:getNewNotice:" + id);
stringRedisTemplate.delete("cache:notice:getNotice:" + id);
});
// 返回删除成功的结果信息
return Result.success("删除成功");
}
// 更新公告的方法,事务性操作,成功返回更新成功的提示信息,失败返回相应错误提示
@Override
@Transactional
public Result<String> updateNotice(String id, NoticeForm noticeForm) {
// 创建更新条件
// 创建一个LambdaUpdateWrapper对象用于构建更新公告信息的条件和要更新的字段
LambdaUpdateWrapper<Notice> noticeWrapper = new LambdaUpdateWrapper<>();
// 设置更新条件为公告ID等于传入的id参数
noticeWrapper.eq(Notice::getId, id)
.eq(Notice::getIsDeleted,0)
// 设置更新条件为公告未被删除假设isDeleted字段表示是否删除0表示未删除
.eq(Notice::getIsDeleted, 0)
// 设置要更新的公告内容字段值为传入的noticeForm中的公告内容
.set(Notice::getContent, noticeForm.getContent());
// 更新公告
// 调用noticeMapper的update方法根据上面构建的更新条件和要更新的字段来更新公告数据,返回受影响的行数,即更新操作影响的记录数
int rowsAffected = noticeMapper.update(noticeWrapper);
// 如果更新操作没有影响到任何行,即更新失败,返回修改失败的结果信息
if (rowsAffected == 0) {
return Result.failed("修改失败");
}
// 根据ID获取更新后的公告对象用于后续缓存操作等可能需要获取最新数据来更新缓存等
Notice byId = getById(id);
if (byId.getId() != null) { // 确保ID有效
// 如果是更新操作,先从缓存中移除旧数据,然后重新放入最新的数据
stringRedisTemplate.delete("cache:notice:getNotice:"+byId.getId().toString()); // 删除旧缓存
stringRedisTemplate.delete("cache:notice:getNewNotice:"+byId.getId().toString()); // 删除旧缓存
// 如果获取到的公告对象有有效的ID进行缓存相关操作
if (byId.getId()!= null) { // 确保ID有效
// 如果是更新操作先从缓存中移除旧数据根据公告ID删除对应的缓存数据缓存键格式为"cache:notice:getNotice:" + 公告ID
stringRedisTemplate.delete("cache:notice:getNotice:" + byId.getId().toString());
// 根据公告ID删除对应的另一个缓存数据可能是用于获取新公告相关的缓存从命名推测缓存键格式为"cache:notice:getNewNotice:" + 公告ID
stringRedisTemplate.delete("cache:notice:getNewNotice:" + byId.getId().toString());
// NoticeVO updatedNoticeVO = noticeConverter.NoticeToNoticeVO(byId); // 转换为视图对象
// Map<Integer, NoticeVO> map = Map.of(updatedNoticeVO.getId(), updatedNoticeVO);
// cacheClient.batchPut("cache:notice:getNotice:",map,10L,TimeUnit.MINUTES); // 存储新数据
// cacheClient.batchPut("cache:notice:getNewNotice:",map,10L,TimeUnit.MINUTES); // 存储新数据
}
// 返回更新成功的结果信息
return Result.success("修改成功");
}
// 获取公告分页数据的方法,返回包含分页信息和公告视图对象列表的结果
@Override
public Result<IPage<NoticeVO>> getNotice(Integer pageNum, Integer pageSize, String title) {
// 查询满足条件的总记录数
int total = noticeMapper.countByCondition(SecurityUtil.getUserId(), title); // 假设gradeMapper中实现了根据条件计数的方法
// 计算偏移量
// 调用noticeMapper的countByCondition方法根据当前用户ID和公告标题来统计满足条件的公告总记录数假设noticeMapper中实现了该方法
int total = noticeMapper.countByCondition(SecurityUtil.getUserId(), title);
// 计算分页查询的偏移量,根据当前页码和每页显示数量来计算
int offset = (pageNum - 1) * pageSize;
// 查询分页ID列表
// 调用noticeMapper的selectNoticeIdsPage方法根据当前用户ID、公告标题以及偏移量和每页显示数量来查询分页的公告ID列表假设noticeMapper中实现了该方法
List<Integer> noticeIds = noticeMapper.selectNoticeIdsPage(SecurityUtil.getUserId(), title, offset, pageSize);
// 批量从缓存中获取GradeVO对象
Map<Integer, NoticeVO> cachedGradesMap = cacheClient.batchGet("cache:notice:getNotice:",noticeIds, NoticeVO.class);
// 使用cacheClient的batchGet方法从缓存中批量获取对应公告ID的NoticeVO对象缓存键前缀为"cache:notice:getNotice:"
Map<Integer, NoticeVO> cachedGradesMap = cacheClient.batchGet("cache:notice:getNotice:", noticeIds, NoticeVO.class);
// 确定未命中的ID列表
// 创建一个列表用于存储在缓存中未命中的公告ID即缓存中不存在对应数据的公告ID
List<Integer> missIds = new ArrayList<>();
// 遍历查询到的公告ID列表检查每个ID在缓存中是否存在如果不存在则添加到未命中ID列表中
for (Integer id : noticeIds) {
if (!cachedGradesMap.containsKey(id)) {
missIds.add(id);
}
}
// 如果有未命中的ID从数据库批量查询并更新缓存
// 如果存在未命中缓存的公告ID即missIds列表不为空
if (!missIds.isEmpty()) {
// 调用noticeMapper的batchSelectByIds方法从数据库中批量查询未命中缓存的公告数据并转换为NoticeVO对象列表假设noticeMapper中实现了该方法
List<NoticeVO> missedGrades = noticeMapper.batchSelectByIds(missIds);
// 假设GradeVO的ID为getId()使用Collectors.toMap转换
// 使用Java 8的Stream API将NoticeVO对象列表转换为以公告ID为键NoticeVO对象为值的Map方便后续操作
Map<Integer, NoticeVO> missedGradesMap = missedGrades.stream()
.collect(Collectors.toMap(NoticeVO::getId, Function.identity()));
// 更新缓存
cacheClient.batchPut("cache:notice:getNotice:",missedGradesMap,10L, TimeUnit.MINUTES);
// 合并缓存结果
// 使用cacheClient的batchPut方法将从数据库中查询到的未命中缓存的公告数据更新缓存缓存有效期为10分钟
cacheClient.batchPut("cache:notice:getNotice:", missedGradesMap, 10L, TimeUnit.MINUTES);
// 将从数据库中查询到的未命中缓存的公告数据合并缓存结果的Map中确保缓存数据的完整性
cachedGradesMap.putAll(missedGradesMap);
}
// 根据ID列表从缓存中获取完整的GradeVO对象列表
// 创建一个列表用于存储最终要返回的完整的NoticeVO对象列表初始大小为查询到的公告ID列表的大小
List<NoticeVO> finalResult = new ArrayList<>(noticeIds.size());
// 遍历公告ID列表从缓存结果的Map中获取对应的NoticeVO对象并添加到最终结果列表中
for (Integer id : noticeIds) {
finalResult.add(cachedGradesMap.get(id));
}
// 构建并返回IPage对象
// 创建一个IPage对象用于封装分页信息和最终的公告视图对象列表设置当前页码、每页显示数量以及总记录数
IPage<NoticeVO> resultPage = new Page<>(pageNum, pageSize, Long.valueOf(total));
// 将最终的公告视图对象列表设置到IPage对象中
resultPage.setRecords(finalResult);
// 返回包含分页信息和公告视图对象列表的成功结果信息
return Result.success("查询成功", resultPage);
}
// 获取新公告分页数据的方法返回包含分页信息和公告视图对象列表的结果与getNotice方法逻辑有部分重复可能可进行适当优化抽取公共逻辑
@Override
public Result<IPage<NoticeVO>> getNewNotice(Integer pageNum, Integer pageSize) {
// 创建分页对象
// 创建一个Page对象用于封装分页相关信息设置当前页码和每页显示数量
Page<NoticeVO> page = new Page<>(pageNum, pageSize);
// 学生分页查询公告
page = noticeMapper.selectNewNoticePage(page,SecurityUtil.getUserId());
// 调用noticeMapper的selectNewNoticePage方法传入分页对象和当前用户ID进行学生分页查询公告操作(具体查询逻辑在该方法内部实现,从命名推测是获取新公告相关的分页数据)
page = noticeMapper.selectNewNoticePage(page, SecurityUtil.getUserId());
// return Result.success("查询成功", page);
// 查询满足条件的总记录数
int total = noticeMapper.countByCondition(SecurityUtil.getUserId(), null); // 假设gradeMapper中实现了根据条件计数的方法
// 计算偏移量
// 调用noticeMapper的countByCondition方法根据当前用户ID标题为null可能表示不按标题筛选来统计满足条件的公告总记录数假设noticeMapper中实现了该方法
int total = noticeMapper.countByCondition(SecurityUtil.getUserId(), null);
// 计算分页查询的偏移量,根据当前页码和每页显示数量来计算
int offset = (pageNum - 1) * pageSize;
// 查询分页ID列表
// 调用noticeMapper的selectNewNoticeIdsPage方法根据当前用户ID、偏移量和每页显示数量来查询分页的公告ID列表假设noticeMapper中实现了该方法
List<Integer> noticeIds = noticeMapper.selectNewNoticeIdsPage(SecurityUtil.getUserId(), offset, pageSize);
// 批量从缓存中获取GradeVO对象
Map<Integer, NoticeVO> cachedGradesMap = cacheClient.batchGet("cache:notice:getNewNotice:",noticeIds, NoticeVO.class);
// 使用cacheClient的batchGet方法从缓存中批量获取对应公告ID的NoticeVO对象缓存键前缀为"cache:notice:getNewNotice:"
Map<Integer, NoticeVO> cachedGradesMap = cacheClient.batchGet("cache:notice:getNewNotice:", noticeIds, NoticeVO.class);
// 确定未命中的ID列表
// 创建一个列表用于存储在缓存中未命中的公告ID即缓存中不存在对应数据的公告ID
List<Integer> missIds = new ArrayList<>();
// 遍历查询到的公告ID列表检查每个ID在缓存中是否存在如果不存在则添加到未命中ID列表中
for (Integer id : noticeIds) {
// 遍历noticeIds列表noticeIds可能是存储了通知Notice相关记录的ID集合这里每次循环取出一个通知的ID用于后续的缓存判断等操作。
if (!cachedGradesMap.containsKey(id)) {
// 判断cachedGradesMap推测是一个缓存中存储的已查询到的通知对象的映射键为通知ID值为对应的通知对象这里从命名来看可能是缓存了NoticeVO类型的对象但需结合上下文确定中是否包含当前遍历到的通知ID如果不包含表示该通知ID对应的通知对象在缓存中未找到缓存未命中
missIds.add(id);
// 将未在缓存中找到的通知ID添加到missIds列表中missIds用于收集所有缓存未命中的通知ID后续会根据这些ID从数据库中查询相应的通知信息并更新缓存。
}
}
// 如果有未命中的ID从数据库批量查询并更新缓存
// 如果有未命中的ID从数据库批量查询并更新缓存
if (!missIds.isEmpty()) {
// 判断missIds列表是否为空即是否存在缓存未命中的通知ID如果不为空表示有需要从数据库查询并更新缓存的数据执行以下操作。
List<NoticeVO> missedGrades = noticeMapper.batchSelectByIds(missIds);
// 调用noticeMapper可能是操作通知数据的持久层接口用于与数据库交互的batchSelectByIds方法传入收集到的缓存未命中的通知ID列表missIds从数据库中批量查询对应的通知信息返回的是NoticeVO类型的列表每个NoticeVO对象代表一条通知记录的相关信息存储在missedGrades变量中。
// 假设GradeVO的ID为getId()使用Collectors.toMap转换
Map<Integer, NoticeVO> missedGradesMap = missedGrades.stream()
.collect(Collectors.toMap(NoticeVO::getId, Function.identity()));
// 使用Java 8的Stream API对查询到的通知信息列表missedGrades进行处理将其转换为一个以通知ID为键通知对象NoticeVO为值的Map。Collectors.toMap方法接收两个参数第一个参数NoticeVO::getId表示使用NoticeVO对象的getId方法获取的返回值即通知ID作为键第二个参数Function.identity()表示直接使用原NoticeVO对象作为值这样就构建了方便后续操作的映射关系存储在missedGradesMap变量中。
// 更新缓存
cacheClient.batchPut("cache:notice:getNotice:",missedGradesMap,10L, TimeUnit.MINUTES);
cacheClient.batchPut("cache:notice:getNotice:", missedGradesMap, 10L, TimeUnit.MINUTES);
// 调用cacheClient可能是用于操作缓存的客户端工具类的batchPut方法将构建好的包含未命中通知对象的映射missedGradesMap存储到缓存中缓存的键前缀为 "cache:notice:getNotice:"同时设置缓存的过期时间为10分钟时间单位为分钟TimeUnit.MINUTES这样可以在一定时间内缓存这些通知信息提高后续查询效率。
// 合并缓存结果
cachedGradesMap.putAll(missedGradesMap);
// 将新查询到并存储到missedGradesMap中的通知对象映射合并到cachedGradesMap中使得cachedGradesMap包含了最新的缓存数据无论是之前已存在的缓存数据还是本次新查询并更新的缓存数据保证缓存数据的完整性。
}
// 根据ID列表从缓存中获取完整的GradeVO对象列表
// 根据ID列表从缓存中获取完整的GradeVO对象列表
List<NoticeVO> finalResult = new ArrayList<>(noticeIds.size());
// 创建一个ArrayList对象用于存储最终要返回的通知对象列表初始容量设置为noticeIds的大小这样可以在一定程度上避免列表扩容带来的性能开销提高性能虽然ArrayList会自动扩容但预先指定合适容量可以优化性能
for (Integer id : noticeIds) {
finalResult.add(cachedGradesMap.get(id));
// 再次遍历noticeIds列表从合并后的缓存映射cachedGradesMap中获取每个通知ID对应的通知对象NoticeVO并添加到finalResult列表中这样finalResult就包含了根据传入的通知ID列表从缓存中获取到的完整的通知对象集合。
}
// 构建并返回IPage对象
// 构建并返回IPage对象
IPage<NoticeVO> resultPage = new Page<>(pageNum, pageSize, Long.valueOf(total));
// 创建一个IPage类型的分页对象resultPage使用Page的构造函数进行初始化传入当前页码pageNum、每页显示数量pageSize以及总记录数total这里将total转换为Long类型传入具体total的来源和含义需结合上下文确定用于构建分页相关的数据结构方便前端进行分页展示等操作。
resultPage.setRecords(finalResult);
// 将包含通知对象的finalResult列表设置为分页对象resultPage的记录列表即将查询到的通知数据填充到分页对象中使其符合分页展示的要求包含了分页信息以及具体的数据记录。
return Result.success("查询成功", resultPage);
}
}
// 返回一个表示操作成功的Result结果对象其中包含提示信息“查询成功”以及构建好的分页对象resultPage方便前端等调用者获取查询结果并根据分页信息进行展示和后续处理具体Result对象的结构和使用方式需结合项目中相关定义来看。

@ -1,20 +1,29 @@
package cn.org.alan.exam.service.impl;
// 导入OptionMapper接口它是用于操作题目选项相关数据的持久层接口例如对选项数据进行增删改查等操作从命名推测其功能
import cn.org.alan.exam.mapper.OptionMapper;
// 导入Option实体类代表题目选项相关的实体对象包含了选项的各种属性信息如选项内容、是否正确等
import cn.org.alan.exam.model.entity.Option;
// 导入IOptionService接口该接口定义了题目选项相关的业务服务方法规定了具体有哪些业务操作需要实现
import cn.org.alan.exam.service.IOptionService;
// 导入ServiceImpl类这是MyBatis Plus提供的基础服务实现类它提供了很多通用的数据库操作方法方便在此基础上扩展具体业务逻辑的实现
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
// 导入Spring的Service注解用于标记该类是一个服务层组件Spring容器会对其进行管理使其能够参与依赖注入等相关机制
import org.springframework.stereotype.Service;
/**
* <p>
*
*
* </p>
*
* @author WeiJin
* @since 2024-03-21
*/
// 使用@Service注解将该类标记为Spring容器管理的服务层组件意味着在Spring应用中可以通过依赖注入的方式在其他地方使用这个服务类
@Service
// 定义OptionServiceImpl类它继承自ServiceImpl<OptionMapper, Option>同时实现了IOptionService接口
// 通过继承ServiceImpl可以复用其中已有的通用数据库操作方法再按照IOptionService接口中定义的业务方法来实现具体针对题目选项的业务逻辑
// 目前类体为空,表示暂时没有添加额外的业务方法实现,如果后续有添加选项、删除选项、查询选项等业务逻辑相关的方法,就需要在这里进行具体的编写
public class OptionServiceImpl extends ServiceImpl<OptionMapper, Option> implements IOptionService {
}
}

@ -38,7 +38,7 @@ import java.util.function.Function;
import java.util.stream.Collectors;
/**
*
*
*
* @author WeiJin
* @since 2024-03-21
@ -46,215 +46,292 @@ import java.util.stream.Collectors;
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {
// 注入QuestionConverter用于不同题目相关对象之间的转换比如表单对象和实体对象等
@Resource
private QuestionConverter questionConverter;
// 注入QuestionMapper用于操作题目数据的持久层接口例如插入、查询、更新、删除题目等操作
@Resource
private QuestionMapper questionMapper;
// 注入OptionMapper用于操作题目选项数据的持久层接口像插入、更新、删除题目选项等操作与题目相关联
@Resource
private OptionMapper optionMapper;
// 注入AliOSSUtil可能是用于与阿里云对象存储服务OSS交互的工具类比如上传图片等操作从命名推测
@Resource
private AliOSSUtil aliOSSUtil;
// 注入ExerciseRecordMapper可能用于操作练习记录相关数据的持久层接口推测与用户做题目练习的记录有关
@Resource
private ExerciseRecordMapper exerciseRecordMapper;
// 注入CacheClient自定义的缓存客户端用于更方便地进行缓存操作如批量获取、批量存入缓存等
@Resource
private CacheClient cacheClient;
// 注入StringRedisTemplate用于操作Redis字符串类型数据常用于缓存相关操作比如删除缓存等
@Resource
private StringRedisTemplate stringRedisTemplate;
// 添加单个题目(可能是单选题、多选题、判断题、简答题等不同类型)的方法,事务性操作,成功返回添加成功的提示信息,失败返回相应错误提示
@Override
@Transactional
public Result<String> addSingleQuestion(QuestionFrom questionFrom) {
// 入参校验
// 入参校验,获取题目表单中的选项列表
List<Option> options = questionFrom.getOptions();
// 判断题目类型不是简答题假设题目类型4代表简答题且选项为空或者选项数量小于2时不符合要求返回错误提示
if (questionFrom.getQuType() != 4 && (Objects.isNull(options) || options.size() < 2)) {
return Result.failed("非简答题的试题选项不能少于两个");
}
// 将QuestionFrom对象转换为Question实体对象方便后续持久化操作使用QuestionConverter进行转换
Question question = questionConverter.fromToEntity(questionFrom);
// 调用questionMapper的insert方法将题目数据插入到数据库中
questionMapper.insert(question);
if (question.getQuType() == 4) {
// 简答题添加选项
// 如果题目类型是简答题
if (question.getQuType() == 4) {
// 简答题添加选项,获取题目表单中第一个选项(简答题通常可能只有一个用于填写答案的选项,这里从代码逻辑推测)
Option option = questionFrom.getOptions().get(0);
// 设置该选项所属题目的ID为刚插入题目的ID建立关联
option.setQuId(question.getId());
// 调用optionMapper的insert方法将选项数据插入到数据库中
optionMapper.insert(option);
} else {
// 非简答题添加选项
// 把新建试题获取的id填入选项中
// 遍历题目表单中的所有选项将每个选项所属题目的ID设置为刚插入题目的ID建立关联
options.forEach(option -> {
option.setQuId(question.getId());
});
// 调用optionMapper的insertBatch方法批量将选项数据插入到数据库中
optionMapper.insertBatch(options);
}
// 如果题目插入成功题目数据有了有效的ID这里假设插入后会自动生成ID进行缓存相关操作
if (question.getId() != null) { // 确保ID有效
// 如果是更新操作,先从缓存中移除旧数据,然后重新放入最新的数据
stringRedisTemplate.delete("cache:question:pagingQuestion:"+question.getId().toString()); // 删除旧缓存
//
// 如果是更新操作(此处逻辑可能不太准确,实际上是添加操作,但代码结构类似更新时的缓存处理逻辑),先从缓存中移除旧数据
// 根据题目ID删除对应的缓存数据缓存键格式为"cache:question:pagingQuestion:" + 题目ID
stringRedisTemplate.delete("cache:question:pagingQuestion:" + question.getId().toString());
// QuestionVO questionVO = questionConverter.QuestionToQuestionVO(question); // 转换为视图对象
// Map<Integer, QuestionVO> map = Map.of(questionVO.getId(), questionVO);
// cacheClient.batchPut("cache:question:pagingQuestion:",map,10L,TimeUnit.MINUTES); // 存储新数据
}
// 返回添加成功的结果信息
return Result.success("添加成功");
}
// 批量删除题目根据题目ID列表的方法事务性操作成功返回删除成功的提示信息失败返回相应错误提示
@Override
@Transactional
public Result<String> deleteBatchByIds(String ids) {
// 将传入的以逗号分隔的字符串形式的题目ID列表转换为Integer类型的列表使用Java 8的Stream API进行转换
List<Integer> list = Arrays.stream(ids.split(",")).map(Integer::parseInt).toList();
//删除用户刷题记录表
// 删除用户刷题记录表中与要删除题目相关的记录创建一个LambdaUpdateWrapper对象用于构建删除条件
LambdaUpdateWrapper<ExerciseRecord> updateWrapper =
new LambdaUpdateWrapper<ExerciseRecord>().in(ExerciseRecord::getQuestionId, list);
// 调用exerciseRecordMapper的delete方法根据构建的条件删除相关记录返回受影响的行数
int delete = exerciseRecordMapper.delete(updateWrapper);
// 先删除选项
// 先删除题目对应的选项调用optionMapper的deleteBatchByQuIds方法传入题目ID列表批量删除选项假设该方法实现了批量删除逻辑
optionMapper.deleteBatchByQuIds(list);
// 再删除试题
// 再删除题目调用questionMapper的deleteBatchIdsQu方法传入题目ID列表删除对应的题目假设该方法实现了批量删除逻辑
questionMapper.deleteBatchIdsQu(list);
list.forEach(id->{
stringRedisTemplate.delete("cache:question:pagingQuestion:"+id);
// 遍历题目ID列表根据每个题目ID删除对应的缓存数据缓存键格式为"cache:question:pagingQuestion:" + 题目ID
list.forEach(id -> {
stringRedisTemplate.delete("cache:question:pagingQuestion:" + id);
});
// 返回删除成功的结果信息
return Result.success("删除成功");
}
// 获取题目分页数据的方法根据页码、每页数量、题目标题、题目类型以及题库ID等条件返回包含分页信息和题目视图对象列表的结果
@Override
public Result<IPage<QuestionVO>> pagingQuestion(Integer pageNum, Integer pageSize, String title, Integer type, Integer repoId) {
// 初始化用户ID变量默认为null后续根据用户角色进行赋值
Integer userId = null;
// 如果当前用户角色为"role_teacher"可能代表教师角色具体根据业务定义则将用户ID设置为当前登录用户的ID
if ("role_teacher".equals(SecurityUtil.getRole())) {
userId = SecurityUtil.getUserId();
} else {
// 否则设置为0这里0可能代表其他情况比如游客或者无特定用户身份等具体看业务逻辑
userId = 0;
}
// 查询满足条件的总记录数
int total = questionMapper.countByCondition(userId, title,type,repoId); // 假设gradeMapper中实现了根据条件计数的方法
// 计算偏移量
// 调用questionMapper的countByCondition方法根据用户ID、题目标题、题目类型以及题库ID来统计满足条件的题目总记录数假设questionMapper中实现了该方法
int total = questionMapper.countByCondition(userId, title, type, repoId);
// 计算分页查询的偏移量,根据当前页码和每页显示数量来计算
int offset = (pageNum - 1) * pageSize;
// 查询分页ID列表
List<Integer> quIds = questionMapper.selectQuestionIdsPage(userId, title,type,repoId, offset, pageSize);
// 调用questionMapper的selectQuestionIdsPage方法根据用户ID、题目标题、题目类型、题库ID以及偏移量和每页显示数量来查询分页的题目ID列表假设questionMapper中实现了该方法
List<Integer> quIds = questionMapper.selectQuestionIdsPage(userId, title, type, repoId, offset, pageSize);
// 批量从缓存中获取GradeVO对象
Map<Integer, QuestionVO> cachedQuestionsMap = cacheClient.batchGet("cache:question:pagingQuestion:",quIds, QuestionVO.class);
// 使用cacheClient的batchGet方法从缓存中批量获取对应题目ID的QuestionVO对象缓存键前缀为"cache:question:pagingQuestion:"
Map<Integer, QuestionVO> cachedQuestionsMap = cacheClient.batchGet("cache:question:pagingQuestion:", quIds, QuestionVO.class);
// 确定未命中的ID列表
// 创建一个列表用于存储在缓存中未命中的题目ID即缓存中不存在对应数据的题目ID
List<Integer> missIds = new ArrayList<>();
// 遍历查询到的题目ID列表检查每个ID在缓存中是否存在如果不存在则添加到未命中ID列表中
for (Integer id : quIds) {
if (!cachedQuestionsMap.containsKey(id)) {
missIds.add(id);
}
}
// 如果有未命中的ID从数据库批量查询并更新缓存
// 如果存在未命中缓存的题目ID即missIds列表不为空
if (!missIds.isEmpty()) {
// 调用questionMapper的batchSelectByIds方法从数据库中批量查询未命中缓存的题目数据并转换为QuestionVO对象列表假设questionMapper中实现了该方法
List<QuestionVO> missedGrades = questionMapper.batchSelectByIds(missIds);
// 假设GradeVO的ID为getId()使用Collectors.toMap转换
// 使用Java 8的Stream API将QuestionVO对象列表转换为以题目ID为键QuestionVO对象为值的Map方便后续操作
Map<Integer, QuestionVO> missedGradesMap = missedGrades.stream()
.collect(Collectors.toMap(QuestionVO::getId, Function.identity()));
// 更新缓存
cacheClient.batchPut("cache:question:pagingQuestion:",missedGradesMap,10L, TimeUnit.MINUTES);
// 合并缓存结果
// 使用cacheClient的batchPut方法将从数据库中查询到的未命中缓存的题目数据更新缓存缓存有效期为10分钟
cacheClient.batchPut("cache:question:pagingQuestion:", missedGradesMap, 10L, TimeUnit.MINUTES);
// 将从数据库中查询到的未命中缓存的题目数据合并缓存结果的Map中确保缓存数据的完整性
cachedQuestionsMap.putAll(missedGradesMap);
}
// 根据ID列表从缓存中获取完整的GradeVO对象列表
// 创建一个列表用于存储最终要返回的完整的QuestionVO对象列表初始大小为查询到的题目ID列表的大小
List<QuestionVO> finalResult = new ArrayList<>(quIds.size());
// 遍历题目ID列表从缓存结果的Map中获取对应的QuestionVO对象并添加到最终结果列表中
for (Integer id : quIds) {
finalResult.add(cachedQuestionsMap.get(id));
}
// 构建并返回IPage对象
// 创建一个IPage对象用于封装分页信息和最终的题目视图对象列表设置当前页码、每页显示数量以及总记录数
IPage<QuestionVO> resultPage = new Page<>(pageNum, pageSize, Long.valueOf(total));
// 将最终的题目视图对象列表设置到IPage对象中
resultPage.setRecords(finalResult);
// 返回包含分页信息和题目视图对象列表的成功结果信息这里第一个参数为null可能是预留用于传递其他提示信息等情况
return Result.success(null, resultPage);
}
// 查询单个题目的方法根据题目ID查询并返回对应的题目视图对象返回包含题目视图对象的结果信息
@Override
public Result<QuestionVO> querySingle(Integer id) {
// 调用questionMapper的selectSingle方法传入题目ID查询对应的题目数据并将其包装在成功结果信息中返回
return Result.success(null, questionMapper.selectSingle(id));
}
// 更新题目的方法,事务性操作,成功返回更新成功的提示信息,失败返回相应错误提示
@Override
@Transactional
public Result<String> updateQuestion(QuestionFrom questionFrom) {
// 修改试题
// 修改试题将QuestionFrom对象转换为Question实体对象方便后续更新操作使用QuestionConverter进行转换
Question question = questionConverter.fromToEntity(questionFrom);
// 调用questionMapper的updateById方法根据题目实体对象的ID更新对应的题目数据假设ID是唯一标识且不为空
questionMapper.updateById(question);
// 修改选项
// 修改选项,获取题目表单中的选项列表
List<Option> options = questionFrom.getOptions();
// 遍历题目表单中的每个选项调用optionMapper的updateById方法根据选项实体对象的ID更新对应的选项数据假设ID是唯一标识且不为空
for (Option option : options) {
optionMapper.updateById(option);
}
// 如果题目更新成功题目数据有了有效的ID这里假设更新前后ID不变且不为空进行缓存相关操作
if (question.getId() != null) { // 确保ID有效
// 如果是更新操作,先从缓存中移除旧数据,然后重新放入最新的数据
stringRedisTemplate.delete("cache:question:pagingQuestion:"+question.getId().toString()); // 删除旧缓存
// 如果是更新操作,先从缓存中移除旧数据,根据题目ID删除对应的缓存数据缓存键格式为"cache:question:pagingQuestion:" + 题目ID
stringRedisTemplate.delete("cache:question:pagingQuestion:" + question.getId().toString());
// QuestionVO questionVO = questionConverter.QuestionToQuestionVO(question); // 转换为视图对象
// Map<Integer, QuestionVO> map = Map.of(questionVO.getId(), questionVO);
// cacheClient.batchPut("cache:question:pagingQuestion:",map,10L,TimeUnit.MINUTES); // 存储新数据
}
// 返回更新成功的结果信息
return Result.success("修改成功");
}
// 导入题目从Excel文件导入的方法事务性操作成功返回导入成功的提示信息失败返回相应错误提示使用了异常处理注解来简化异常抛出处理
@SneakyThrows(Exception.class)
@Override
@Transactional
public Result<String> importQuestion(Integer id, MultipartFile file) {
// 检查传入的文件是否是合法的Excel文件调用ExcelUtils工具类的isExcel方法进行判断
if (!ExcelUtils.isExcel(Objects.requireNonNull(file.getOriginalFilename()))) {
return Result.failed("该文件不是一个合法的Excel文件");
}
// 调用ExcelUtils的readMultipartFile方法读取传入的MultipartFile类型的Excel文件并转换为QuestionExcelFrom对象列表假设该方法实现了读取并转换逻辑
List<QuestionExcelFrom> questionExcelFroms = ExcelUtils.readMultipartFile(file, QuestionExcelFrom.class);
// 类型转换
// 进行类型转换将QuestionExcelFrom对象列表转换为QuestionFrom对象列表调用QuestionExcelFrom类中的静态方法进行转换假设该方法实现了转换逻辑
List<QuestionFrom> list = QuestionExcelFrom.converterQuestionFrom(questionExcelFroms);
// 遍历转换后的QuestionFrom对象列表处理每个题目表单对象对应的题目及选项数据插入逻辑
for (QuestionFrom questionFrom : list) {
// 遍历传入的QuestionFrom类型的列表QuestionFrom可能是用于接收前端传递过来的题目相关数据的表单对象从命名推测其功能每次循环处理一个题目相关的数据对象。
Question question = questionConverter.fromToEntity(questionFrom);
// 使用questionConverter可能是一个用于对象转换的工具类将QuestionFrom对象转换为Question实体对象方便后续持久化操作Question实体对象应该是对应数据库中存储题目信息的实体类实例包含了如题目内容、类型等各种属性。
question.setRepoId(id);
// 将题目实体对象的RepoId属性设置为传入的id值推测RepoId可能是用于关联题目所属的题库ID等相关信息具体含义需结合业务逻辑确定
// 添加单题获取Id
questionMapper.insert(question);
// 调用questionMapper可能是操作题目数据的持久层接口用于与数据库交互的insert方法将转换后的题目实体对象插入到数据库中插入成功后数据库会为该题目记录分配一个唯一的ID通常是自增长的主键并且这个ID会回填到question对象中假设使用了相应的数据库框架支持此功能
// 批量添加选项
List<Option> options = questionFrom.getOptions();
// 从QuestionFrom对象中获取其包含的选项列表Option对象应该是代表题目选项相关信息的实体类每个Option实例存储了一个选项的具体内容、是否正确等属性这里获取到的是当前题目对应的所有选项信息列表。
final int[] count = {0};
// 创建一个长度为1的整数数组用于记录选项的序号初始值设为0通过数组的方式可以在Lambda表达式中修改其值类似一种简单的引用传递效果因为Java的Lambda表达式中修改外部局部变量有限制
options.forEach(option -> {
//简答题答案默认给正确
// 简答题答案默认给正确
if (question.getQuType() == 4) {
// 判断当前题目question对象的题目类型QuType属性是否为4推测4代表简答题类型具体需结合业务定义如果是简答题类型则将当前选项的IsRight属性设置为1表示该选项为正确答案对于简答题可能默认只有一个正确答案的设定
option.setIsRight(1);
}
option.setSort(++count[0]);
// 为当前选项设置序号,通过自增操作(++count[0]给选项的Sort属性赋值使其按照顺序依次递增用于表示选项的排列顺序方便后续展示等操作。
option.setQuId(question.getId());
// 将当前选项的QuId属性设置为当前题目的ID即前面插入题目记录后回填到question对象中的ID值建立选项与题目的关联关系表明该选项属于当前这个题目。
});
// 避免简答题没有答案
if (!options.isEmpty()) {
// 判断选项列表是否为空,即确保当前题目有选项信息(虽然简答题可能默认有一个答案选项,但也需要排除没有任何选项数据的情况),如果不为空,则执行以下批量插入选项的操作。
optionMapper.insertBatch(options);
// 调用optionMapper可能是操作选项数据的持久层接口的insertBatch方法将整理好的选项列表批量插入到数据库中实现一次插入多个选项记录的操作提高插入效率。
}
if (question.getId() != null) { // 确保ID有效
// 如果题目实体对象的ID不为空即前面插入题目操作成功获取到了有效的ID执行以下缓存相关的操作可能是为了保证数据一致性更新缓存中的题目数据
// 如果是更新操作,先从缓存中移除旧数据,然后重新放入最新的数据
stringRedisTemplate.delete("cache:question:pagingQuestion:"+question.getId().toString()); // 删除旧缓存
stringRedisTemplate.delete("cache:question:pagingQuestion:" + question.getId().toString()); // 删除旧缓存
// 调用stringRedisTemplate用于操作Redis字符串类型数据的工具类常用于缓存相关操作的delete方法根据构建的缓存键格式为 "cache:question:pagingQuestion:" 加上题目ID的字符串形式删除对应的缓存数据目的是移除旧的题目缓存信息避免缓存数据不一致。
// QuestionVO questionVO = questionConverter.QuestionToQuestionVO(question); // 转换为视图对象
// 注释掉的这行代码原本可能是使用questionConverter前面用于对象转换的工具类将Question实体对象转换为QuestionVO对象推测是用于展示给前端的视图对象包含了前端需要展示的题目相关信息但当前代码中这行被注释掉了可能暂不需要此转换操作或者后续会有其他处理方式。
// Map<Integer, QuestionVO> map = Map.of(questionVO.getId(), questionVO);
// cacheClient.batchPut("cache:question:pagingQuestion:",map,10L,TimeUnit.MINUTES); // 存储新数据
// 注释掉的这行代码原本可能是创建一个只包含当前题目视图对象questionVO的Map以题目ID为键题目视图对象为值可能用于后续缓存存储等操作但同样被注释掉了可能暂不执行此逻辑。
// cacheClient.batchPut("cache:question:pagingQuestion:", map, 10L, TimeUnit.MINUTES); // 存储新数据
// 注释掉的这行代码原本可能是使用cacheClient可能是缓存操作相关的客户端工具类的batchPut方法将包含最新题目视图对象的Map数据存储到缓存中设置缓存过期时间为10分钟以实现缓存更新保证下次获取题目数据时能获取到最新的信息但目前代码中此操作被注释掉了暂不执行缓存存储新数据的逻辑。
}
}
return Result.success("导入成功");
}
// 当遍历完所有题目相关的数据对象并完成相应的插入、关联以及缓存相关操作如果有的话返回一个表示操作成功的Result结果对象其中包含提示信息“导入成功”用于告知调用者题目数据导入操作已顺利完成具体Result对象的结构和使用方式需结合项目中相关定义来看。
@SneakyThrows(IOException.class)
@Override
public Result<String> uploadImage(MultipartFile file) {
if (!aliOSSUtil.isImage(Objects.requireNonNull(file.getOriginalFilename()))) {
return Result.failed("该文件不是常用图片格式(png、jpg、jpeg、bmp)");
}
if (aliOSSUtil.isOverSize(file)) {
return Result.failed("图片大小不能超过50KB");
}
String url = aliOSSUtil.upload(file);
if (StringUtils.isBlank(url)) {
return Result.failed("图片上传失败");
@SneakyThrows(IOException.class)
// 使用 @SneakyThrows注解用于简化异常处理当方法内部抛出IOException异常时会自动将异常向上抛出避免了显式编写try-catch块来处理异常但需要注意在合适的调用层次上进行统一的异常处理这里表示该方法可能会抛出输入输出相关的异常比如文件读取等操作可能出现的异常
@Override
public Result<String> uploadImage (MultipartFile file){
// 实现接口中定义的上传图片方法接收一个MultipartFile类型的文件对象用于上传图片文件该方法的功能是将传入的图片文件上传到指定位置可能是云存储等地方并返回相应的结果信息。
if (!aliOSSUtil.isImage(Objects.requireNonNull(file.getOriginalFilename()))) {
// 调用aliOSSUtil可能是用于与阿里云对象存储服务交互或者进行文件类型判断等相关操作的工具类的isImage方法传入文件的原始文件名通过Objects.requireNonNull确保文件名不为空判断该文件是否是图片格式如果不是图片格式则执行以下返回操作。
return Result.failed("该文件不是常用图片格式(png、jpg、jpeg、bmp)");
// 返回一个表示操作失败的Result结果对象包含提示信息“该文件不是常用图片格式(png、jpg、jpeg、bmp)”告知调用者上传的文件不符合图片格式要求具体Result对象的结构和使用方式需结合项目中相关定义来看。
}
if (aliOSSUtil.isOverSize(file)) {
// 调用aliOSSUtil的isOverSize方法传入文件对象判断该文件大小是否超过了规定的限制从方法名推测是用于判断文件大小是否超出限制的功能如果文件大小超过限制则执行以下返回操作。
return Result.failed("图片大小不能超过50KB");
// 返回一个表示操作失败的Result结果对象包含提示信息“图片大小不能超过50KB”告知调用者上传的图片文件大小不符合要求具体Result对象的结构和使用方式需结合项目中相关定义来看。
}
String url = aliOSSUtil.upload(file);
// 调用aliOSSUtil的upload方法传入文件对象将图片文件上传到指定位置比如阿里云对象存储等地方并返回上传后文件的访问地址如果上传成功存储在url变量中。
if (StringUtils.isBlank(url)) {
// 判断返回的文件访问地址是否为空字符串,如果为空,表示图片上传失败,执行以下返回操作。
return Result.failed("图片上传失败");
// 返回一个表示操作失败的Result结果对象包含提示信息“图片上传失败”告知调用者图片上传过程出现问题具体Result对象的结构和使用方式需结合项目中相关定义来看。
}
return Result.success("图片上传成功", url);
// 如果文件访问地址不为空说明图片上传成功返回一个表示操作成功的Result结果对象包含提示信息“图片上传成功”以及上传后文件的访问地址通过第二个参数传递方便调用者获取图片的访问路径进行后续操作具体Result对象的结构和使用方式需结合项目中相关定义来看。
}
return Result.success("图片上传成功", url);
}
}
}

@ -30,95 +30,128 @@ import java.util.List;
/**
* @author WeiJin
* @since 2024-03-21
* IRepoServiceRepo
*/
@Service
public class RepoServiceImpl extends ServiceImpl<RepoMapper, Repo> implements IRepoService {
// 注入RepoMapper用于操作题库数据的持久层接口比如插入、更新、查询、删除题库记录等操作。
@Resource
private RepoMapper repoMapper;
// 注入QuestionMapper用于操作题目数据的持久层接口可能在涉及题库与题目关联的业务逻辑中使用比如更新题目所属题库等操作。
@Resource
private QuestionMapper questionMapper;
// 注入ExerciseRecordMapper用于操作练习记录数据的持久层接口或许在获取题库相关统计信息如刷题次数等时会用到。
@Resource
private ExerciseRecordMapper exerciseRecordMapper;
// 添加题库的方法将传入的Repo对象插入到数据库中成功返回保存成功的提示信息。
@Override
public Result<String> addRepo(Repo repo) {
// 调用repoMapper的insert方法将传入的Repo对象插入到数据库中执行插入操作。
repoMapper.insert(repo);
// 返回表示保存成功的结果信息,通常可以在前端等地方展示给用户相应提示。
return Result.success("保存成功");
}
// 更新题库信息的方法根据传入的Repo对象和要更新的题库ID更新对应题库的标题信息成功返回修改成功的提示信息。
@Override
public Result<String> updateRepo(Repo repo, Integer id) {
//修改题库
// 创建一个LambdaUpdateWrapper对象用于构建更新题库信息的条件和要更新的字段。
LambdaUpdateWrapper<Repo> updateWrapper = new LambdaUpdateWrapper<Repo>()
.eq(Repo::getId, id).set(Repo::getTitle, repo.getTitle());
// 设置更新条件为题库的ID等于传入的id参数确保更新的是指定的题库记录。
.eq(Repo::getId, id)
// 设置要更新的字段为题库的标题将其更新为传入Repo对象中的标题值。
.set(Repo::getTitle, repo.getTitle());
// 调用repoMapper的update方法根据构建好的更新条件和要更新的字段来更新数据库中的题库记录。
repoMapper.update(updateWrapper);
// 返回表示修改成功的结果信息,方便前端等地方知晓操作结果并展示相应提示给用户。
return Result.success("修改成功");
}
// 删除题库的方法事务性操作先清空题库内所有题目所属的题库ID然后将该题库标记为已删除逻辑删除根据操作结果返回相应提示信息。
@Override
@Transactional
public Result<String> deleteRepoById(Integer id) {
//题库内试题清空所属题库id
// 清空题库内试题所属的题库ID创建一个LambdaUpdateWrapper对象用于构建更新题目信息的条件和要更新的字段。
LambdaUpdateWrapper<Question> wrapper = new LambdaUpdateWrapper<Question>()
// 设置更新条件为题目所属的题库ID等于传入的要删除的题库ID。
.eq(Question::getRepoId, id)
// 设置要更新的字段为题目所属的题库ID将其更新为null表示清空所属题库关系。
.set(Question::getRepoId, null);
// 调用questionMapper的update方法根据构建的条件更新数据库中相关题目的所属题库ID字段。
questionMapper.update(wrapper);
//删除题库
// 创建一个LambdaUpdateWrapper对象用于构建删除题库的逻辑这里采用逻辑删除设置删除标记字段
LambdaUpdateWrapper<Repo> repoLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
repoLambdaUpdateWrapper.eq(Repo::getId,id)
.set(Repo::getIsDeleted,1);
// 设置更新条件为题库的ID等于传入的要删除的题库ID。
repoLambdaUpdateWrapper.eq(Repo::getId, id)
// 设置题库的删除标记字段假设名为isDeleted为1表示已删除。
.set(Repo::getIsDeleted, 1);
// 调用repoMapper的update方法根据构建的条件执行逻辑删除操作返回受影响的行数即实际更新的记录数。
int result = repoMapper.update(repoLambdaUpdateWrapper);
// 如果更新操作影响的行数大于0表示删除操作成功返回删除成功的结果信息。
if (result > 0) {
return Result.success("删除成功");
}
// 如果更新操作影响的行数为0表示删除操作失败返回删除失败的结果信息。
return Result.failed("删除失败");
}
// 获取题库列表的方法,根据当前用户角色(教师或其他角色)以及传入的题库标题模糊查询条件,从数据库中获取相应的题库列表信息,返回包含查询结果的成功提示信息。
@Override
public Result<List<RepoListVO>> getRepoList(String repoTitle) {
List<RepoListVO> list;
// 判断当前用户角色是否为"role_teacher"(通常代表教师角色,具体含义由业务定义)。
if ("role_teacher".equals(SecurityUtil.getRole())) {
// 如果是教师角色调用repoMapper的selectRepoList方法传入题库标题和当前用户ID获取该教师创建的符合标题模糊查询条件的题库列表。
list = repoMapper.selectRepoList(repoTitle, SecurityUtil.getUserId());
} else {
// 如果不是教师角色可能是其他角色比如管理员等调用repoMapper的selectRepoList方法传入题库标题和默认值0具体含义由业务决定可能表示获取所有或公共的题库等情况获取相应的题库列表。
list = repoMapper.selectRepoList(repoTitle, 0);
}
// 返回包含查询到的题库列表信息的成功结果信息,方便前端展示给用户查看。
return Result.success("获取成功", list);
}
// 获取题库分页信息的方法,根据当前用户角色(教师或其他角色)、页码、每页数量以及传入的题库标题模糊查询条件,从数据库中获取相应的分页题库信息,返回包含分页结果的成功提示信息。
@Override
public Result<IPage<RepoVO>> pagingRepo(Integer pageNum, Integer pageSize, String title) {
// 创建一个IPage对象用于封装分页相关信息设置当前页码和每页显示数量。
IPage<RepoVO> page = new Page<>(pageNum, pageSize);
// 判断当前用户角色是否为"role_teacher"(通常代表教师角色,具体含义由业务定义)。
if ("role_teacher".equals(SecurityUtil.getRole())) {
//教师只查询自己的题库
// 如果是教师角色调用repoMapper的pagingRepo方法传入分页对象、题库标题和当前用户ID获取该教师创建的符合标题模糊查询条件的分页题库信息将结果赋值给page对象。
page = repoMapper.pagingRepo(page, title, SecurityUtil.getUserId());
} else {
//管理员可以获取所有题库
// 如果不是教师角色可能是其他角色比如管理员等调用repoMapper的pagingRepo方法传入分页对象、题库标题和默认值0具体含义由业务决定可能表示获取所有或公共的题库等情况获取相应的分页题库信息将结果赋值给page对象。
page = repoMapper.pagingRepo(page, title, 0);
}
// 返回包含分页题库信息的成功结果信息方便前端进行分页展示等操作这里第一个参数为null可能是预留用于传递其他提示信息等情况。
return Result.success(null, page);
}
// 获取包含练习相关信息的题库信息的方法,根据页码、每页数量以及传入的题库标题模糊查询条件,从数据库中获取相应的分页题库信息,并可以进一步填充每个题库的刷题次数等相关练习信息(当前代码中填充刷题次数的逻辑被注释掉了),返回包含分页结果的成功提示信息。
@Override
public Result<IPage<ExerciseRepoVO>> getRepo(Integer pageNum, Integer pageSize, String title) {
// 创建一个IPage对象用于封装分页相关信息设置当前页码和每页显示数量。
IPage<ExerciseRepoVO> page = new Page<>(pageNum, pageSize);
// 调用repoMapper的selectRepo方法传入分页对象和题库标题获取相应的分页题库信息将结果赋值给page对象。
page = repoMapper.selectRepo(page, title);
// page.getRecords().forEach(repoVO -> {
// //填充以刷题数
// LambdaQueryWrapper<ExerciseRecord> wrapper = new LambdaQueryWrapper<ExerciseRecord>().eq(ExerciseRecord::getRepoId, repoVO.getId())
// .eq(ExerciseRecord::getUserId, SecurityUtil.getUserId());
// .eq(ExerciseRecord::getUserId, SecurityUtil.getUserId());
//
// repoVO.setExerciseCount(exerciseRecordMapper.selectCount(wrapper).intValue());
// });
// 返回包含分页题库信息的成功结果信息方便前端进行分页展示等操作这里第一个参数为null可能是预留用于传递其他提示信息等情况。
return Result.success(null, page);
}
}
}

@ -1,20 +1,29 @@
package cn.org.alan.exam.service.impl;
// 导入RoleMapper接口它是用于操作角色Role相关数据的持久层接口比如对角色数据进行增删改查等操作从命名推测其功能
import cn.org.alan.exam.mapper.RoleMapper;
// 导入Role实体类代表角色相关的实体对象包含了角色的各种属性信息例如角色名称、角色权限等
import cn.org.alan.exam.model.entity.Role;
// 导入IRoleService接口该接口定义了角色相关的业务服务方法规定了具体有哪些业务操作需要实现例如获取角色列表、添加角色等
import cn.org.alan.exam.service.IRoleService;
// 导入ServiceImpl类这是MyBatis Plus提供的基础服务实现类它提供了很多通用的数据库操作方法方便在此基础上扩展具体业务逻辑的实现
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
// 导入Spring的Service注解用于标记该类是一个服务层组件Spring容器会对其进行管理使其能够参与依赖注入等相关机制
import org.springframework.stereotype.Service;
/**
* <p>
*
*
* </p>
*
* @author WeiJin
* @since 2024-03-21
*/
// 使用@Service注解将该类标记为Spring容器管理的服务层组件意味着在Spring应用中可以通过依赖注入的方式在其他地方使用这个服务类
@Service
// 定义RoleServiceImpl类它继承自ServiceImpl<RoleMapper, Role>同时实现了IRoleService接口
// 通过继承ServiceImpl可以复用其中已有的通用数据库操作方法再按照IRoleService接口中定义的业务方法来实现具体针对角色的业务逻辑
// 目前类体为空,表示暂时没有添加额外的业务方法实现,如果后续有添加角色、删除角色、查询角色等业务逻辑相关的方法,就需要在这里进行具体的编写
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements IRoleService {
}
}

@ -22,7 +22,7 @@ import java.util.List;
/**
* <p>
*
*
* </p>
*
* @author WeiJin
@ -31,82 +31,117 @@ import java.util.List;
@Service
public class StatServiceImpl extends ServiceImpl<ExamGradeMapper, ExamGrade> implements IStatService {
// 使用 @Resource 注解注入 StatMapper用于操作统计相关数据的持久层接口可能包含各种统计查询方法从命名推测其功能
@Resource
private StatMapper statMapper;
// 注入 GradeMapper用于操作班级相关数据的持久层接口比如查询班级数量等操作在统计班级相关信息时可能会用到
@Resource
private GradeMapper gradeMapper;
// 注入 ExamMapper用于操作考试相关数据的持久层接口例如查询考试数量等操作在统计考试相关信息时可能会用到
@Resource
private ExamMapper examMapper;
// 注入 QuestionMapper用于操作题目相关数据的持久层接口像查询题目数量等操作在统计题目相关信息时可能会用到
@Resource
private QuestionMapper questionMapper;
// 注入 UserDailyLoginDurationMapper用于操作用户每日登录时长相关数据的持久层接口推测在获取每日相关统计信息时会用到
@Resource
private UserDailyLoginDurationMapper userDailyLoginDurationMapper;
/**
*
*
*
* @return
* @return Result
*/
@Override
public Result<List<GradeStudentVO>> getStudentGradeCount() {
//获取班级
// 定义用于存储各班人数统计信息的列表,类型为 GradeStudentVO初始未赋值。
List<GradeStudentVO> gradeStudentVOs;
// 判断当前用户角色是否为 "role_teacher"(通常代表教师角色,具体含义由业务定义)。
if ("role_teacher".equals(SecurityUtil.getRole())) {
// 如果是教师角色,调用 statMapper 的 StudentGradeCount 方法,传入参数 2 和当前用户 ID获取该教师相关的各班人数统计信息。
// 这里的参数 2 具体含义可能由业务逻辑决定,可能是用于区分不同的统计条件或筛选情况等。
gradeStudentVOs = statMapper.StudentGradeCount(2, SecurityUtil.getUserId());
} else {
// 如果不是教师角色(可能是其他角色,比如管理员等),调用 statMapper 的 StudentGradeCount 方法,传入参数 3 和当前用户 ID获取相应的各班人数统计信息。
// 同样,参数 3 的具体含义由业务逻辑确定,用于特定的统计筛选等情况。
gradeStudentVOs = statMapper.StudentGradeCount(3, SecurityUtil.getUserId());
}
// 返回包含各班人数统计信息的成功结果信息,方便前端展示给用户查看,"查询成功" 为提示消息gradeStudentVOs 为具体的数据列表。
return Result.success("查询成功", gradeStudentVOs);
}
/**
*
*
*
* @return
* @return Result
*/
@Override
public Result<List<GradeExamVO>> getExamGradeCount() {
//获取班级
// 定义用于存储各班试卷统计信息的列表,类型为 GradeExamVO初始未赋值。
List<GradeExamVO> gradeExamVOs;
// 判断当前用户角色是否为 "role_teacher"(通常代表教师角色,具体含义由业务定义)。
if ("role_teacher".equals(SecurityUtil.getRole())) {
// 如果是教师角色,调用 statMapper 的 ExamGradeCount 方法,传入参数 2 和当前用户 ID获取该教师相关的各班试卷统计信息。
// 参数 2 的具体含义同上述类似,由业务逻辑决定,用于特定的统计条件或筛选情况。
gradeExamVOs = statMapper.ExamGradeCount(2, SecurityUtil.getUserId());
} else {
// 如果不是教师角色(可能是其他角色,比如管理员等),调用 statMapper 的 ExamGradeCount 方法,传入参数 3 和当前用户 ID获取相应的各班试卷统计信息。
gradeExamVOs = statMapper.ExamGradeCount(3, SecurityUtil.getUserId());
}
// 返回包含各班试卷统计信息的成功结果信息,便于前端展示给用户,"查询成功" 为提示消息gradeExamVOs 为具体的数据列表。
return Result.success("查询成功", gradeExamVOs);
}
/**
*
* AllStatsVO
*
* @return
* @return Result
*/
@Override
public Result<AllStatsVO> getAllCount() {
// 创建一个 AllStatsVO 对象,用于封装所有班级、试卷、试题数量的统计信息,初始各属性值为空。
AllStatsVO allStatsVO = new AllStatsVO();
// 获取当前用户的角色信息,用于后续判断是管理员还是教师角色来进行不同的统计逻辑。
String role = SecurityUtil.getRole();
if("role_admin".equals(role)){
// 判断当前用户角色是否为 "role_admin"(通常代表管理员角色,具体含义由业务定义)。
if ("role_admin".equals(role)) {
// 如果是管理员角色,通过 gradeMapper 的 selectCount 方法,传入 null表示无额外查询条件统计所有班级数量获取班级总数并设置到 allStatsVO 对象中。
allStatsVO.setClassCount(gradeMapper.selectCount(null).intValue());
// 同样,通过 examMapper 的 selectCount 方法,传入 null统计所有考试数量获取考试总数设置到 allStatsVO 对象中。
allStatsVO.setExamCount(examMapper.selectCount(null).intValue());
// 通过 questionMapper 的 selectCount 方法,传入 null统计所有题目数量获取题目总数设置到 allStatsVO 对象中。
allStatsVO.setQuestionCount(questionMapper.selectCount(null).intValue());
}else if("role_teacher".equals(role)){
} else if ("role_teacher".equals(role)) {
// 如果是教师角色,通过 gradeMapper 的 selectCount 方法,传入一个 LambdaQueryWrapper 对象,设置查询条件为班级所属用户 ID 等于当前教师的用户 ID获取该教师创建的班级数量并设置到 allStatsVO 对象中。
allStatsVO.setClassCount(gradeMapper.selectCount(new LambdaQueryWrapper<Grade>().eq(Grade::getUserId,
SecurityUtil.getUserId())).intValue());
SecurityUtil.getUserId()))
.intValue());
// 通过 examMapper 的 selectCount 方法,传入一个 LambdaQueryWrapper 对象,设置查询条件为考试创建者用户 ID 等于当前教师的用户 ID获取该教师创建的考试数量并设置到 allStatsVO 对象中。
allStatsVO.setExamCount(examMapper.selectCount(
new LambdaQueryWrapper<Exam>().eq(Exam::getUserId,SecurityUtil.getUserId())).intValue());
new LambdaQueryWrapper<Exam>().eq(Exam::getUserId, SecurityUtil.getUserId()))
.intValue());
// 通过 questionMapper 的 selectCount 方法,传入一个 LambdaQueryWrapper 对象,设置查询条件为题目创建者用户 ID 等于当前教师的用户 ID获取该教师创建的题目数量并设置到 allStatsVO 对象中。
allStatsVO.setQuestionCount(questionMapper.selectCount(
new LambdaQueryWrapper<Question>().eq(Question::getUserId,SecurityUtil.getUserId())).intValue());
new LambdaQueryWrapper<Question>().eq(Question::getUserId, SecurityUtil.getUserId()))
.intValue());
}
// 返回包含所有统计信息的成功结果信息,方便前端展示给用户查看,"查询成功" 为提示消息allStatsVO 为包含具体统计数据的对象。
return Result.success("查询成功", allStatsVO);
}
/**
* ID
*
* @return Result
*/
@Override
public Result<List<DailyVO>> getDaily() {
// 调用 userDailyLoginDurationMapper 的 getDaily 方法,传入当前用户 ID获取该用户的每日相关统计信息从方法名推测可能是登录时长等相关信息返回一个 DailyVO 类型的列表。
List<DailyVO> daily = userDailyLoginDurationMapper.getDaily(SecurityUtil.getUserId());
return Result.success("请求成功",daily);
// 返回包含每日统计信息的成功结果信息,便于前端展示给用户,"请求成功" 为提示消息daily 为具体的数据列表。
return Result.success("请求成功", daily);
}
}
}

@ -28,145 +28,192 @@ import java.util.*;
import java.util.stream.Collectors;
/**
*
*
*
* @author Alan
* @since 2024-03-21
*/
@Service
public class UserBookServiceImpl extends ServiceImpl<UserBookMapper, UserBook> implements IUserBookService {
// 注入UserBookMapper用于操作错题本数据的持久层接口比如查询错题本记录、插入、删除错题本相关数据等操作。
@Resource
private UserBookMapper userBookMapper;
// 注入QuestionMapper用于操作题目数据的持久层接口可能在获取错题对应的题目详情等操作中使用。
@Resource
private QuestionMapper questionMapper;
// 注入OptionMapper用于操作题目选项数据的持久层接口例如获取题目选项信息等操作与题目相关联
@Resource
private OptionMapper optionMapper;
// 注入ExamQuAnswerMapper可能用于操作考试题目答案相关数据的持久层接口从命名推测其功能
@Resource
private ExamQuAnswerMapper examQuAnswerMapper;
// 注入UserBookConverter用于不同错题本相关对象之间的转换比如实体对象和视图对象等
@Resource
private UserBookConverter userBookConverter;
// 注入IQuestionService这是题目相关的业务服务接口可调用其中已定义的题目相关业务方法如获取题目详情等
@Resource
private IQuestionService questionService;
// 注入IOptionService这是题目选项相关的业务服务接口可调用其中已定义的选项相关业务方法如获取选项详情等
@Resource
private IOptionService optionService;
// 获取错题本分页数据的方法,根据页码、每页数量以及考试名称等条件,返回包含分页信息和错题本视图对象列表的结果。
@Override
public Result<IPage<UserPageBookVO>> getPage(Integer pageNum, Integer pageSize, String examName) {
// 创建一个Page对象用于封装分页相关信息设置当前页码和每页显示数量。
Page<UserPageBookVO> page = new Page<>(pageNum, pageSize);
// 获取当前用户的角色信息,用于后续判断不同角色的逻辑处理(可能涉及不同权限查看不同数据等情况)。
String role = SecurityUtil.getRole();
int roleId = 0;
if("role_admin".equals(role)){
roleId =3;
}else if ("role_teacher".equals(role)){
// 根据用户角色进行不同的角色ID赋值这里假设不同角色有对应的数字标识具体含义由业务定义
if ("role_admin".equals(role)) {
roleId = 3;
} else if ("role_teacher".equals(role)) {
roleId = 2;
}else {
} else {
roleId = 1;
}
Page<UserPageBookVO> userPageBookVOPage = userBookMapper.selectPageVo(page, examName, SecurityUtil.getUserId(),roleId);
// 调用userBookMapper的selectPageVo方法传入分页对象、考试名称、当前用户ID以及角色ID获取符合条件的错题本分页数据具体查询逻辑在该方法内部实现并将结果赋值给userPageBookVOPage对象。
Page<UserPageBookVO> userPageBookVOPage = userBookMapper.selectPageVo(page, examName, SecurityUtil.getUserId(), roleId);
// 返回包含分页信息和错题本视图对象列表的成功结果信息,方便前端进行分页展示等操作,"查询成功" 为提示消息userPageBookVOPage为具体的分页数据对象。
return Result.success("查询成功", userPageBookVOPage);
}
// 根据考试ID获取对应的错题本信息转换为特定视图对象列表的方法返回包含查询结果的成功提示信息。
@Override
public Result<List<ReUserExamBookVO>> getReUserExamBook(Integer examId) {
// 根据考试id查询考试试题表
// 根据考试id查询考试试题表创建一个LambdaQueryWrapper对象用于构建查询错题本记录的条件这里设置考试ID和用户ID作为查询条件。
LambdaQueryWrapper<UserBook> userBookLambdaQueryWrapper = new LambdaQueryWrapper<>();
userBookLambdaQueryWrapper.eq(UserBook::getExamId, examId)
.eq(UserBook::getUserId, SecurityUtil.getUserId());
// 调用userBookMapper的selectList方法根据构建的查询条件获取符合条件的错题本记录列表即UserBook实体对象列表
List<UserBook> userBook = userBookMapper.selectList(userBookLambdaQueryWrapper);
// 实体转换
// 进行实体转换调用userBookConverter的listEntityToVo方法将获取到的UserBook实体对象列表转换为ReUserExamBookVO视图对象列表方便后续返回展示给前端等操作。
List<ReUserExamBookVO> reUserExamBookVOS = userBookConverter.listEntityToVo(userBook);
// 返回包含查询到的错题本视图对象列表的成功结果信息,便于前端展示给用户查看,"查询成功" 为提示消息reUserExamBookVOS为具体的数据列表。
return Result.success("查询成功", reUserExamBookVOS);
}
// 根据题目ID获取错题本中一道题目的详细信息的方法返回包含该题目详细信息的视图对象的成功提示信息如果题目不存在则返回相应错误提示。
@Override
public Result<BookOneQuVO> getBookOne(Integer quId) {
// 创建一个BookOneQuVO对象用于封装要返回的错题本中一道题目的详细信息初始各属性值为空。
BookOneQuVO bookOneQuVO = new BookOneQuVO();
// 问题
// 通过questionService的getById方法调用了Question相关业务服务接口的方法根据题目ID获取对应的题目详细信息Question实体对象用于后续设置错题本中题目详情相关属性。
Question quById = questionService.getById(quId);
if( quById==null){
// 如果获取到的题目实体对象为空,即该题不存在,返回相应的错误提示信息。
if (quById == null) {
return Result.failed("该题不存在");
}
// 基本信息
// 设置错题本中题目视图对象的图片信息属性,取值为从题目实体对象中获取的图片信息(假设题目实体中有保存图片相关字段)。
bookOneQuVO.setImage(quById.getImage());
// 设置错题本中题目视图对象的题目内容属性,取值为从题目实体对象中获取的题目内容。
bookOneQuVO.setContent(quById.getContent());
// 设置错题本中题目视图对象的题目类型属性,取值为从题目实体对象中获取的题目类型(例如单选题、多选题等不同类型的标识)。
bookOneQuVO.setQuType(quById.getQuType());
// 答案列表
// 查询该题目的答案列表创建一个LambdaQueryWrapper对象用于构建查询题目选项的条件这里设置题目ID作为查询条件用于获取该题对应的所有选项信息。
LambdaQueryWrapper<Option> optionLambdaQuery = new LambdaQueryWrapper<>();
optionLambdaQuery.eq(Option::getQuId, quId);
// 调用optionMapper的selectList方法根据构建的查询条件获取符合条件的题目选项列表即Option实体对象列表并设置到错题本中题目视图对象的答案列表属性中。
List<Option> list = optionMapper.selectList(optionLambdaQuery);
bookOneQuVO.setAnswerList(list);
// 返回包含错题本中一道题目的详细信息的成功结果信息,方便前端展示给用户查看,"获取成功" 为提示消息bookOneQuVO为具体的题目详细信息视图对象。
return Result.success("获取成功", bookOneQuVO);
}
// 处理错题本中题目作答情况并返回相应结果的方法,根据传入的错题作答表单信息,判断作答是否正确、移除已答对的错题等操作,并返回对应的结果信息。
@Override
public Result<AddBookAnswerVO> addBookAnswer(ReUserBookForm reUserBookForm) {
// 创建返回视图
// 创建一个AddBookAnswerVO对象用于封装要返回的错题作答相关结果信息初始各属性值为空。
AddBookAnswerVO addBookAnswerVO = new AddBookAnswerVO();
// 未作答
// 判断用户是否未作答如果传入的答案为空字符串通过StringUtils工具类判断则直接返回表示未作答的成功提示信息。
if (StringUtils.isBlank(reUserBookForm.getAnswer())) {
return Result.success("未作答");
}
// 查询试题类型
// 查询试题类型创建一个LambdaQueryWrapper对象用于构建查询题目的条件这里设置题目ID作为查询条件用于获取对应的题目详细信息主要是获取题目类型
LambdaQueryWrapper<Question> QuWrapper = new LambdaQueryWrapper<>();
QuWrapper.eq(Question::getId, reUserBookForm.getQuId());
// 调用questionMapper的selectOne方法根据构建的查询条件获取对应的题目详细信息这里获取到的是Question实体对象因为使用selectOne预期返回单个题目对象
Question qu = questionMapper.selectOne(QuWrapper);
// 获取题目类型(从题目实体对象中获取题目类型字段的值),用于后续根据不同题目类型进行不同的作答判断逻辑。
Integer quType = qu.getQuType();
// 设置试题分析
// 设置试题分析属性,取值为从题目实体对象中获取的试题分析内容(假设题目实体中有保存试题分析相关字段),将其设置到要返回的结果视图对象中。
addBookAnswerVO.setAnalysis(qu.getAnalysis());
// 设置正确答案
// 设置正确答案属性先创建一个LambdaQueryWrapper对象用于构建查询题目选项的条件这里设置题目ID作为查询条件用于获取该题对应的所有选项信息。
LambdaQueryWrapper<Option> opWrapper = new LambdaQueryWrapper<>();
opWrapper.eq(Option::getQuId, reUserBookForm.getQuId());
// 调用optionMapper的selectList方法根据构建的查询条件获取符合条件的题目选项列表即Option实体对象列表
List<Option> options = optionMapper.selectList(opWrapper);
// 初始化一个空字符串,用于后续拼接正确答案(在某些题目类型下可能需要拼接多个正确答案等情况)。
String current = "";
// 创建一个ArrayList用于存储正确答案对应的序号在某些题目类型下可能需要根据序号来判断答案情况等初始为空。
ArrayList<Integer> strings = new ArrayList<>();
// 遍历获取到的题目选项列表找出其中标记为正确答案的选项通过判断选项实体对象中的是否正确标识字段假设为isRight值为1表示正确将其序号添加到strings列表中。
for (Option temp : options) {
if (temp.getIsRight() == 1) {
strings.add(temp.getSort());
}
}
// 将strings列表中的序号转换为字符串列表方便后续拼接等操作使用Java 8的Stream API进行转换。
List<String> stringList = strings.stream().map(String::valueOf).collect(Collectors.toList());
// 使用逗号将字符串列表中的元素拼接成一个字符串,得到正确答案的表示形式(例如对于多选题可能有多个正确答案,用逗号分隔)。
String result = String.join(",", stringList);
if(quType ==4){
// 如果题目类型是简答题假设题目类型4代表简答题设置正确答案属性为题目选项列表中的第一个选项的内容简答题通常可能只有一个用于填写答案的选项这里从代码逻辑推测
if (quType == 4) {
addBookAnswerVO.setRightAnswers(options.get(0).getContent());
}else{
} else {
// 如果不是简答题,设置正确答案属性为前面拼接好的正确答案字符串(包含多个正确答案用逗号分隔的情况等)。
addBookAnswerVO.setRightAnswers(result);
}
// 判断是否正确并移除正确试题
// 根据不同的题目类型进行不同的作答判断逻辑使用Java 17的新特性switch表达式来简化代码结构根据作答情况返回相应的结果信息。
return switch (quType) {
// 题目类型为单选题的情况
case 1 -> {
// 通过optionService的getById方法调用了Option相关业务服务接口的方法根据用户作答的选项ID获取对应的选项详细信息Option实体对象用于判断作答是否正确。
Option byId = optionService.getById(reUserBookForm.getAnswer());
// 如果获取到的选项对象中标记为正确答案通过判断选项实体对象中的是否正确标识字段值为1表示正确说明回答正确进行相应操作。
if (byId.getIsRight() == 1) {
// 创建一个LambdaQueryWrapper对象用于构建删除错题本记录的条件这里设置用户ID、考试ID和题目ID作为查询条件用于定位要删除的错题本记录即已答对的错题从错题本中移除
LambdaQueryWrapper<UserBook> userBookLambdaQueryWrapper = new LambdaQueryWrapper<>();
userBookLambdaQueryWrapper.eq(UserBook::getUserId, SecurityUtil.getUserId())
.eq(UserBook::getExamId, reUserBookForm.getExamId())
.eq(UserBook::getQuId, reUserBookForm.getQuId());
// 调用userBookMapper的delete方法根据构建的条件删除对应的错题本记录返回受影响的行数即实际删除的记录数这里暂时未使用该返回值。
int row = userBookMapper.delete(userBookLambdaQueryWrapper);
// 设置回答正确标识为1表示回答正确将其设置到要返回的结果视图对象中。
addBookAnswerVO.setCorrect(1);
// 使用yield关键字返回表示回答正确且已移出错题本的成功提示信息包含更新后的结果视图对象addBookAnswerVO。
yield Result.success("回答正确,已移出错题本", addBookAnswerVO);
} else {
// 如果选项对象不是正确答案设置回答正确标识为0表示回答错误将其设置到要返回的结果视图对象中。
addBookAnswerVO.setCorrect(0);
// 使用yield关键字返回表示回答错误的成功提示信息包含结果视图对象addBookAnswerVO。
yield Result.success("回答错误", addBookAnswerVO);
}
}
// 题目类型为多选题的情况
case 2 -> {
// 查找正确答案
// 查找正确答案创建一个LambdaQueryWrapper对象用于构建查询正确选项的条件这里设置选项为正确答案通过判断选项实体对象中的是否正确标识字段值为1表示正确以及题目ID作为查询条件用于获取该题对应的所有正确选项信息。
LambdaQueryWrapper<Option> optionWrapper = new LambdaQueryWrapper<>();
optionWrapper.eq(Option::getIsRight, 1)
.eq(Option::getQuId, reUserBookForm.getQuId());
// 调用optionMapper的selectList方法根据构建的查询条件获取符合条件的正确选项列表即Option实体对象列表
List<Option> examQuAnswers = optionMapper.selectList(optionWrapper);
// 解析用户作答
// 解析用户作答将用户传入的以逗号分隔的作答字符串转换为整数列表假设用户作答的是选项的ID用逗号分隔多个选项ID使用Java 8的Stream API进行转换。
List<Integer> quIds = Arrays.stream(reUserBookForm.getAnswer().split(","))
.map(Integer::parseInt)
.toList();
// 判读啊是否正确
// 判读啊是否正确这里可能代码有拼写错误正确应为“判断”以下按照正确逻辑解读注释遍历正确选项列表检查用户作答的选项ID列表中是否包含当前正确选项的ID如果包含则说明回答错误因为多选题需要全部选对才正确
for (Option temp : examQuAnswers) {
boolean containsBanana = quIds.contains(temp.getId());
if (containsBanana) {
yield Result.success("回答错误");
}
}
// 如果遍历完所有正确选项用户作答的选项ID列表都不包含正确选项的ID说明回答正确进行相应操作。
LambdaQueryWrapper<UserBook> userBookWrapper = new LambdaQueryWrapper<>();
userBookWrapper.eq(UserBook::getUserId, SecurityUtil.getUserId())
.eq(UserBook::getExamId, reUserBookForm.getExamId())
@ -175,36 +222,62 @@ public class UserBookServiceImpl extends ServiceImpl<UserBookMapper, UserBook> i
yield Result.success("回答正确");
}
case 3 -> {
// 当满足条件为3时的逻辑分支同样可能对应不同的题型或业务场景下的处理逻辑具体依业务而定
// 通过optionService可能是选项相关的业务服务类的getById方法根据从reUserBookForm对象中获取的答案推测此处答案可能是一个选项ID之类的标识从数据库中获取对应的选项信息存储在byId变量中。
Option byId = optionService.getById(reUserBookForm.getAnswer());
if (byId.getIsRight() == 1) {
// 判断获取到的选项的“是否为正确答案”字段isRight的值是否为1即判断用户选择的这个选项是否是正确答案。
// 如果是正确答案,执行以下操作,准备从错题本中移除对应的题目记录。
// 创建一个LambdaQueryWrapper<UserBook>对象,用于构建删除错题本记录的条件,方式和前面类似,目的是准确找到要删除的错题本记录。
LambdaQueryWrapper<UserBook> userBookLambdaQueryWrapper = new LambdaQueryWrapper<>();
// 添加查询条件设置错题本记录对应的用户ID与通过SecurityUtil获取的当前登录用户的ID相等限定操作的是当前用户的错题本记录。
userBookLambdaQueryWrapper.eq(UserBook::getUserId, SecurityUtil.getUserId())
// 继续添加条件使错题本记录中的考试ID与从reUserBookForm对象中获取的考试ID相等进一步确定要删除的错题本记录范围。
.eq(UserBook::getExamId, reUserBookForm.getExamId())
// 再添加条件让错题本记录中的题目ID与从reUserBookForm对象中获取的题目ID相等确保精准定位到要删除的那条错题本记录。
.eq(UserBook::getQuId, reUserBookForm.getQuId());
// 使用userBookMapper的delete方法传入构建好的删除条件userBookLambdaQueryWrapper从数据库中删除对应的错题本记录实现移除错题本中已答对题目的功能。
userBookMapper.delete(userBookLambdaQueryWrapper);
// 设置addBookAnswerVO可能是用于封装答题相关信息返回给前端等的对象中的correct字段为1表示回答正确具体addBookAnswerVO的结构和使用场景需结合项目整体逻辑来看。
addBookAnswerVO.setCorrect(1);
// 返回一个表示回答正确的Result结果对象同时包含提示信息“回答正确已移除错题本”以及答题相关信息的addBookAnswerVO对象方便前端展示和后续处理。
yield Result.success("回答正确,已移除错题本", addBookAnswerVO);
} else {
// 如果用户选择的选项不是正确答案,执行以下操作。
// 设置addBookAnswerVO中的correct字段为0表示回答错误同样addBookAnswerVO的使用和含义需结合项目整体来看。
addBookAnswerVO.setCorrect(0);
// 返回一个表示回答错误的Result结果对象同时包含答题相关信息的addBookAnswerVO对象方便前端展示和后续处理。
yield Result.success("回答错误", addBookAnswerVO);
}
}
case 4 -> {
// 当满足条件为4时的逻辑分支也是对应特定业务场景下的处理逻辑具体由业务规则决定
if("1".equals(reUserBookForm.getAnswer())){
// 判断用户作答的答案是否为 "1"(具体含义需结合业务来看,可能代表某种特定选择或情况),如果是则执行以下操作,准备从错题本中移除对应的题目记录。
// 创建一个LambdaQueryWrapper<UserBook>对象,用于构建删除错题本记录的条件,和前面类似,用于准确找到要删除的错题本记录。
LambdaQueryWrapper<UserBook> userBookLambdaQueryWrapper = new LambdaQueryWrapper<>();
// 添加查询条件设置错题本记录对应的用户ID与通过SecurityUtil获取的当前登录用户的ID相等确保操作的是当前用户的错题本记录。
userBookLambdaQueryWrapper.eq(UserBook::getUserId, SecurityUtil.getUserId())
// 继续添加条件使错题本记录中的考试ID与从reUserBookForm对象中获取的考试ID相等进一步确定要删除的错题本记录范围。
.eq(UserBook::getExamId, reUserBookForm.getExamId())
// 再添加条件让错题本记录中的题目ID与从reUserBookForm对象中获取的题目ID相等确保精准定位到要删除的那条错题本记录。
.eq(UserBook::getQuId, reUserBookForm.getQuId());
// 使用userBookMapper的delete方法传入构建好的删除条件userBookLambdaQueryWrapper从数据库中删除对应的错题本记录实现移除错题本中已答对题目的功能。
userBookMapper.delete(userBookLambdaQueryWrapper);
// 返回一个表示回答正确的Result结果对象同时包含提示信息“回答正确已移除错题本”以及答题相关信息的addBookAnswerVO对象方便前端展示和后续处理。
yield Result.success("回答正确,已移除错题本", addBookAnswerVO);
}
// 如果用户作答的答案不是 "1",执行以下操作,表示回答错误。
addBookAnswerVO.setCorrect(0);
// 返回一个表示回答错误的Result结果对象同时包含答题相关信息的addBookAnswerVO对象方便前端展示和后续处理。
yield Result.success("回答错误", addBookAnswerVO);
}
default -> {
// 当以上所有case条件都不满足时的默认处理逻辑即处理其他未明确列举的情况。
// 返回一个表示请求错误的Result结果对象包含提示信息“请求错误请联系管理员解决”提示用户出现了不符合预期的情况需要联系管理员进一步排查处理。
yield Result.failed("请求错误,请联系管理员解决");
}
};
}
}
}

@ -1,19 +1,32 @@
package cn.org.alan.exam.service.impl;
// 导入UserDailyLoginDurationMapper接口它是用于操作用户每日登录时长UserDailyLoginDuration相关数据的持久层接口比如对用户每日登录时长数据进行增删改查等操作从命名推测其功能
import cn.org.alan.exam.mapper.UserDailyLoginDurationMapper;
// 导入UserExamsScoreMapper接口它是用于操作用户考试成绩UserExamsScore相关数据的持久层接口可用于对用户考试成绩数据进行相应的数据库操作从命名推测其功能此处虽然当前类未直接使用但可能是在整个项目的相关业务逻辑中存在关联导入的情况
import cn.org.alan.exam.mapper.UserExamsScoreMapper;
// 导入UserDailyLoginDuration实体类代表用户每日登录时长相关的实体对象包含了如登录日期、登录时长等各种属性信息用于存储和传递与用户每日登录时长相关的数据
import cn.org.alan.exam.model.entity.UserDailyLoginDuration;
// 导入UserExamsScore实体类代表用户考试成绩相关的实体对象包含了如考试ID、用户ID、成绩分数等属性信息用于体现用户在不同考试中的成绩情况
import cn.org.alan.exam.model.entity.UserExamsScore;
// 导入IUserDailyLoginDurationService接口该接口定义了用户每日登录时长相关的业务服务方法规定了具体有哪些业务操作需要实现例如获取用户某段时间内的登录时长统计、记录每日登录时长等
import cn.org.alan.exam.service.IUserDailyLoginDurationService;
// 导入IUserExamsScoreService接口该接口定义了用户考试成绩相关的业务服务方法像查询用户特定考试成绩、更新成绩等操作同样虽当前类未直接关联使用但可能在整体业务中有联系
import cn.org.alan.exam.service.IUserExamsScoreService;
// 导入ServiceImpl类这是MyBatis Plus提供的基础服务实现类它提供了很多通用的数据库操作方法方便在此基础上扩展具体业务逻辑的实现减少重复代码编写提高开发效率
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
// 导入Spring的Service注解用于标记该类是一个服务层组件Spring容器会对其进行管理使其能够参与依赖注入等相关机制便于在其他组件中方便地调用该服务类的方法
import org.springframework.stereotype.Service;
/**
* @Author Alan
* @Version
* @Date 2024/5/28 10:46 PM
*
*/
@Service
// 定义UserDailyLoginDurationServiceImpl类它继承自ServiceImpl<UserDailyLoginDurationMapper, UserDailyLoginDuration>同时实现了IUserDailyLoginDurationService接口
// 通过继承ServiceImpl可以复用其中已有的通用数据库操作方法再按照IUserDailyLoginDurationService接口中定义的业务方法来实现具体针对用户每日登录时长的业务逻辑
// 目前类体为空,表示暂时没有添加额外的业务方法实现,如果后续有添加记录用户每日登录时长、查询用户登录时长统计等业务逻辑相关的方法,就需要在这里进行具体的编写
public class UserDailyLoginDurationServiceImpl extends ServiceImpl<UserDailyLoginDurationMapper, UserDailyLoginDuration> implements IUserDailyLoginDurationService {
}
}

@ -24,7 +24,7 @@ import java.util.List;
/**
* <p>
*
*
* </p>
*
* @author WeiJin
@ -32,46 +32,87 @@ import java.util.List;
*/
@Service
public class UserExamsScoreServiceImpl extends ServiceImpl<UserExamsScoreMapper, UserExamsScore> implements IUserExamsScoreService {
// 使用 @Resource 注解注入 UserExamsScoreMapper用于操作用户考试成绩数据的持久层接口比如查询成绩、插入成绩等数据库操作从命名推测其功能
@Resource
private UserExamsScoreMapper userExamsScoreMapper;
// 注入 ExamMapper用于操作考试相关数据的持久层接口例如查询考试名称等操作在获取成绩相关信息时可能会用到
@Resource
private ExamMapper examMapper;
// 注入 ExamGradeMapper用于操作考试成绩等级相关数据的持久层接口可能涉及成绩等级划分等相关业务逻辑具体根据业务定义
@Resource
private ExamGradeMapper examGradeMapper;
/**
* IDID
*
* @param pageNum
* @param pageSize
* @param gradeId IDnull
* @param examId IDnull
* @param realName null
* @return Result
*/
@Override
public Result<IPage<UserScoreVO>> pagingScore(Integer pageNum, Integer pageSize, Integer gradeId, Integer examId, String realName) {
// 创建一个 IPage 对象,用于封装分页相关信息,设置当前页码和每页显示数量。
IPage<UserScoreVO> page = new Page<>(pageNum, pageSize);
// 调用 userExamsScoreMapper 的 pagingScore 方法传入分页对象、班级ID、考试ID以及真实姓名获取符合条件的分页用户成绩信息并将结果赋值给 page 对象(具体查询逻辑在该方法内部实现)。
page = userExamsScoreMapper.pagingScore(page, gradeId, examId, realName);
// 返回包含分页用户成绩信息的成功结果信息方便前端进行分页展示等操作这里第一个参数为null可能是预留用于传递其他提示信息等情况page 为具体的分页数据对象。
return Result.success(null, page);
}
/**
* IDID使 Excel Excel HttpServletResponse
*
* @param response HttpServletResponse Excel
* @param examId ID
* @param gradeId IDnull
*/
@Override
public void exportScores(HttpServletResponse response, Integer examId, Integer gradeId) {
//获取成绩信息
// 获取成绩信息,调用 userExamsScoreMapper 的 selectScores 方法传入考试ID和班级ID从数据库中获取符合条件的成绩数据以 ExportScoreVO 列表形式返回),这些成绩数据将用于生成 Excel 文件。
List<ExportScoreVO> scores = userExamsScoreMapper.selectScores(examId, gradeId);
// 创建一个长度为1的数组用于记录排名序号初始值为0这里通过数组实现类似引用传递的效果方便在Lambda表达式中修改值
final int[] sort = {0};
// 使用 Lambda 表达式遍历成绩列表为每个成绩记录设置排名信息排名序号从1开始自增通过 ++sort[0] 操作实现自增并赋值给排名字段)。
scores.forEach(exportScoreVO -> exportScoreVO.setRanking(++sort[0]));
//获取考试名
// 获取考试名,创建一个 LambdaQueryWrapper 对象用于构建查询考试信息的条件这里设置考试ID作为查询条件并指定只查询考试标题字段通过 select 方法指定),用于获取对应的考试名称(后续用于 Excel 文件的命名等情况)。
LambdaQueryWrapper<Exam> wrapper = new LambdaQueryWrapper<Exam>().eq(Exam::getId, examId).select(Exam::getTitle);
// 调用 examMapper 的 selectOne 方法根据构建的查询条件获取对应的考试信息这里获取到的是Exam实体对象因为使用selectOne预期返回单个考试对象从中获取考试名称。
Exam exam = examMapper.selectOne(wrapper);
//生成表格并响应
// 生成表格并响应,调用 ExcelUtils 工具类的 export 方法,传入 HttpServletResponse 对象、考试名称、成绩数据列表以及成绩数据对应的实体类ExportScoreVO.class该方法内部会将成绩数据生成为 Excel 文件,并通过 response 设置响应头和输出流将 Excel 文件发送给客户端。
ExcelUtils.export(response, exam.getTitle(), scores, ExportScoreVO.class);
}
/**
* ID
*
* @param pageNum
* @param pageSize
* @param examTitle null
* @param gradeId IDnull
* @return Result
*/
@Override
public Result<IPage<GradeScoreVO>> getExamScoreInfo(Integer pageNum, Integer pageSize, String examTitle,Integer gradeId) {
public Result<IPage<GradeScoreVO>> getExamScoreInfo(Integer pageNum, Integer pageSize, String examTitle, Integer gradeId) {
// 创建一个 IPage 对象,用于封装分页相关信息,设置当前页码和每页显示数量。
IPage<GradeScoreVO> page = new Page<>(pageNum, pageSize);
// 判断当前用户角色是否为 "role_teacher"(通常代表教师角色,具体含义由业务定义)。
if ("role_teacher".equals(SecurityUtil.getRole())) {
// 如果是教师角色,调用 userExamsScoreMapper 的 scoreStatistics 方法传入分页对象、班级ID、考试标题、当前用户ID以及角色标识2具体含义由业务逻辑决定可能用于区分不同的统计条件或权限等情况获取符合教师角色相关条件的分页考试成绩信息并将结果赋值给 page 对象(具体查询统计逻辑在该方法内部实现)。
page = userExamsScoreMapper
.scoreStatistics(page,gradeId,examTitle,SecurityUtil.getUserId(),2);
.scoreStatistics(page, gradeId, examTitle, SecurityUtil.getUserId(), 2);
} else {
// 如果不是教师角色(可能是其他角色,比如管理员等),调用 userExamsScoreMapper 的 scoreStatistics 方法传入分页对象、班级ID、考试标题、当前用户ID以及角色标识3同样由业务逻辑确定其含义用于特定的统计筛选等情况获取相应的分页考试成绩信息并将结果赋值给 page 对象。
page = userExamsScoreMapper
.scoreStatistics(page, gradeId, examTitle, SecurityUtil.getUserId(), 3);
}
// 返回包含分页考试成绩信息的成功结果信息,方便前端进行展示等操作,"查询成功" 为提示消息page 为具体的分页数据对象。
return Result.success("查询成功", page);
}
}
}

@ -1,20 +1,29 @@
package cn.org.alan.exam.service.impl;
// 导入UserExerciseRecordMapper接口它是用于操作用户练习记录UserExerciseRecord相关数据的持久层接口可实现诸如对用户练习记录数据进行增删改查等操作从命名推测其功能
import cn.org.alan.exam.mapper.UserExerciseRecordMapper;
// 导入UserExerciseRecord实体类代表用户练习记录相关的实体对象包含了像练习时间、练习题目、练习得分等各种属性信息用于存储和传递与用户练习情况相关的数据
import cn.org.alan.exam.model.entity.UserExerciseRecord;
// 导入IUserExerciseRecordService接口该接口定义了用户练习记录相关的业务服务方法规定了具体有哪些业务操作需要实现例如记录用户每次练习的详细情况、查询用户的练习历史记录等
import cn.org.alan.exam.service.IUserExerciseRecordService;
// 导入ServiceImpl类这是MyBatis Plus提供的基础服务实现类它提供了很多通用的数据库操作方法方便在此基础上扩展具体业务逻辑的实现减少重复编写代码提高开发效率
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
// 导入Spring的Service注解用于标记该类是一个服务层组件Spring容器会对其进行管理使其能够参与依赖注入等相关机制方便在其他地方通过依赖注入的方式使用这个服务类
import org.springframework.stereotype.Service;
/**
* <p>
*
*
* </p>
*
* @author WeiJin
* @since 2024-03-21
*/
// 使用@Service注解将该类标记为Spring容器管理的服务层组件意味着在Spring应用中可以通过依赖注入的方式在其他组件中调用这个服务类的方法
@Service
// 定义UserExerciseRecordServiceImpl类它继承自ServiceImpl<UserExerciseRecordMapper, UserExerciseRecord>同时实现了IUserExerciseRecordService接口
// 通过继承ServiceImpl可以复用其中已有的通用数据库操作方法后续可按照IUserExerciseRecordService接口中定义的业务方法来实现具体针对用户练习记录的业务逻辑
// 目前类体为空,表示暂时没有添加额外的业务方法实现,如果后续有添加记录用户练习情况、查询用户练习记录统计信息等业务逻辑相关的方法,就需要在这里进行具体的编写
public class UserExerciseRecordServiceImpl extends ServiceImpl<UserExerciseRecordMapper, UserExerciseRecord> implements IUserExerciseRecordService {
}
}

@ -31,228 +31,306 @@ import java.util.Arrays;
import java.util.List;
import java.util.Objects;
/**
* @author WeiJin
* @since 2024-03-21
* IUserService
*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
// 注入UserMapper用于操作用户数据的持久层接口比如插入、更新、查询、删除用户记录等操作。
@Resource
private UserMapper userMapper;
// 注入StringRedisTemplate用于操作Redis字符串类型数据常用于缓存相关操作比如删除缓存、存储用户登录相关的token等。
@Resource
private StringRedisTemplate stringRedisTemplate;
// 注入HttpServletRequest用于获取HTTP请求相关信息例如在后续可能用于获取请求中的会话信息等如删除密码修改成功后对应会话的token
@Resource
private HttpServletRequest request;
// 注入UserConverter用于不同用户相关对象之间的转换比如表单对象和实体对象等方便在不同业务逻辑中进行数据格式的适配。
@Resource
private UserConverter userConverter;
// 注入GradeMapper用于操作班级相关数据的持久层接口可能在涉及用户与班级关联的业务逻辑中使用比如查询用户所在班级、添加用户到班级等操作。
@Resource
private GradeMapper gradeMapper;
// 注入CertificateUserMapper可能用于操作用户证书相关数据的持久层接口从命名推测其功能可能涉及用户资质认证等相关业务逻辑
@Resource
private CertificateUserMapper certificateUserMapper;
// 注入ExamMapper用于操作考试相关数据的持久层接口例如查询用户创建的考试、删除用户相关考试记录等操作与用户在考试方面的业务相关
@Resource
private ExamMapper examMapper;
// 注入ExamQuAnswerMapper可能用于操作考试题目答案相关数据的持久层接口推测用于处理用户在考试中作答的题目答案等相关业务逻辑
@Resource
private ExamQuAnswerMapper examQuAnswerMapper;
// 注入ExerciseRecordMapper用于操作用户练习记录相关数据的持久层接口比如删除用户的练习记录等操作与用户练习情况相关
@Resource
private ExerciseRecordMapper exerciseRecordMapper;
// 注入GradeExerciseMapper可能用于操作班级与练习相关关联数据的持久层接口从命名推测其功能也许涉及班级内学生练习相关业务逻辑
@Resource
private GradeExerciseMapper gradeExerciseMapper;
// 注入ManualScoreMapper可能用于操作手动评分相关数据的持久层接口可能用于处理如主观题老师手动给分等相关业务逻辑
@Resource
private ManualScoreMapper manualScoreMapper;
// 注入NoticeMapper用于操作公告相关数据的持久层接口例如查询用户创建的公告、删除用户相关公告记录等操作与系统公告业务相关
@Resource
private NoticeMapper noticeMapper;
// 注入QuestionMapper用于操作题目数据的持久层接口可能在涉及用户创建的题目、删除用户相关题目等业务逻辑中使用。
@Resource
private QuestionMapper questionMapper;
// 注入RepoMapper用于操作题库相关数据的持久层接口比如查询用户创建的题库、删除用户相关题库记录等操作与用户在题库方面的业务相关
@Resource
private RepoMapper repoMapper;
// 注入UserBookMapper用于操作用户错题本相关数据的持久层接口例如删除用户的错题本记录等操作与用户错题本业务相关
@Resource
private UserBookMapper userBookMapper;
// 注入OptionMapper用于操作题目选项数据的持久层接口例如删除用户创建题目对应的选项等操作与题目选项相关且和用户创建题目等业务有关联
@Resource
private OptionMapper optionMapper;
// 注入NoticeGradeMapper可能用于操作公告与班级关联相关数据的持久层接口推测用于处理公告针对哪些班级展示等相关业务逻辑
@Resource
private NoticeGradeMapper noticeGradeMapper;
// 注入IQuestionService这是题目相关的业务服务接口此处用于调用题目相关业务方法如上传头像复用了上传图片的方法这里调用其上传图片功能
@Resource
private IQuestionService iQuestionService;
// 创建用户的方法根据传入的UserForm对象创建新用户进行一些必要的属性设置和验证后插入到数据库中成功返回创建成功的提示信息失败返回相应错误提示。
@Override
public Result<String> createUser(UserForm userForm) {
// 使用BCryptPasswordEncoder对默认密码"123456"进行加密,并设置到用户表单对象的密码字段中,通常新用户会有一个初始密码,后续可自行修改。
userForm.setPassword(new BCryptPasswordEncoder().encode("123456"));
//教师只能创建学生
// 判断当前用户角色是否为"role_teacher"(通常代表教师角色,具体含义由业务定义),如果是教师角色,只能创建学生用户这里假设学生角色的ID为1具体由业务规定设置用户表单对象的角色ID为学生角色ID。
if ("role_teacher".equals(SecurityUtil.getRole())) {
userForm.setRoleId(1);
}
//避免管理员创建用户不传递角色
// 避免管理员创建用户不传递角色信息的情况如果用户表单对象中的角色ID为null或者为0返回提示要求选择用户角色的错误信息。
if (userForm.getRoleId() == null || userForm.getRoleId() == 0) {
return Result.failed("请选择用户角色");
}
// 将UserForm对象转换为User实体对象方便后续持久化操作使用UserConverter进行转换。
User user = userConverter.fromToEntity(userForm);
// 调用userMapper的insert方法将用户数据插入到数据库中执行插入操作。
userMapper.insert(user);
// 返回表示用户创建成功的结果信息,通常可以在前端等地方展示给用户相应提示。
return Result.success("用户创建成功");
}
// 修改用户密码的方法根据传入的UserForm对象中的新旧密码信息进行验证和更新操作密码修改成功后清除Redis中对应的token用于让用户重新登录并返回相应的提示信息验证失败返回错误提示。
@Override
public Result<String> updatePassword(UserForm userForm) {
// 验证两次输入的新密码是否一致,如果不一致,返回两次密码不一致的错误提示信息。
if (!userForm.getNewPassword().equals(userForm.getCheckedPassword())) {
return Result.failed("两次密码不一致");
}
// 获取当前登录用户的ID通过SecurityUtil工具类获取具体获取逻辑在该工具类中实现可能从登录信息等方面获取当前用户的唯一标识ID
Integer userId = SecurityUtil.getUserId();
// 使用BCryptPasswordEncoder验证输入的旧密码是否与数据库中存储的当前用户的密码匹配如果不匹配返回旧密码错误的提示信息。
if (!new BCryptPasswordEncoder()
.matches(userForm.getOriginPassword(),userMapper.selectById(userId).getPassword())) {
.matches(userForm.getOriginPassword(), userMapper.selectById(userId).getPassword())) {
return Result.failed("旧密码错误");
}
//密码加密
// 对新密码进行加密使用BCryptPasswordEncoder将新密码加密后设置到用户表单对象的密码字段中,用于后续更新到数据库中存储加密后的密码。
userForm.setPassword(new BCryptPasswordEncoder().encode(userForm.getNewPassword()));
// 设置用户表单对象的ID为当前登录用户的ID确保更新的是当前登录用户的密码信息。
userForm.setId(userId);
// 调用userMapper的updateById方法根据用户表单对象转换后的用户实体对象通过UserConverter转换来更新数据库中当前用户的密码信息返回受影响的行数即实际更新的记录数。
int updated = userMapper.updateById(userConverter.fromToEntity(userForm));
//密码修改成功清除redis的token让用户重新登录
// 如果更新操作影响的行数大于0表示密码修改成功清除Redis中对应会话的token这里假设token存储在Redis中键为请求会话的ID加上"token"字符串),让用户重新登录,然后返回修改成功并提示重新登录的结果信息。
if (updated > 0) {
stringRedisTemplate.delete(request.getSession().getId() + "token");
return Result.success("修改成功,请重新登录");
}
// 如果更新操作影响的行数为0表示密码修改失败可能是其他原因导致更新未成功返回旧密码错误的提示信息这里可以根据实际情况细化错误提示
return Result.failed("旧密码错误");
}
// 批量删除用户的方法事务性操作根据传入的以逗号分隔的用户ID字符串将对应的用户及其相关联的数据如用户创建的考试、练习记录、题目等多方面相关数据进行删除操作成功返回删除成功的提示信息具体删除逻辑中部分代码被注释掉可按需取消注释启用。
@Override
@Transactional
public Result<String> deleteBatchByIds(String ids) {
// 将传入的以逗号分隔的字符串形式的用户ID列表转换为Integer类型的列表使用Java 8的Stream API进行转换。
List<Integer> userIds = Arrays.stream(ids.split(",")).map(Integer::parseInt).toList();
// //删除用户证书
// //删除用户证书调用certificateUserMapper的deleteByUserIds方法传入用户ID列表批量删除用户证书相关记录假设该方法实现了批量删除逻辑当前代码中此部分被注释掉可按需启用
// certificateUserMapper.deleteByUserIds(userIds);
// //删除用户创建的考试
// //删除用户创建的考试调用examMapper的deleteByUserIds方法传入用户ID列表批量删除用户创建的考试记录同样被注释掉可按需启用
// examMapper.deleteByUserIds(userIds);
// //删除用户考试作答记录
// //删除用户考试作答记录调用examQuAnswerMapper的deleteByUserIds方法传入用户ID列表批量删除用户考试作答记录注释状态可按需启用
// examQuAnswerMapper.deleteByUserIds(userIds);
// //删除用户练习记录
// //删除用户练习记录调用exerciseRecordMapper的deleteByUserIds方法传入用户ID列表批量删除用户练习记录注释部分可按需启用
// exerciseRecordMapper.deleteByUserIds(userIds);
// //清除用户表中的班级id
// //清除用户表中的班级id先调用gradeMapper的selectIdsByUserIds方法传入用户ID列表获取这些用户对应的班级ID列表假设该方法实现了查询逻辑
// List<Integer> gradeIds = gradeMapper.selectIdsByUserIds(userIds);
// //如果获取到的班级ID列表不为空表示有相关班级需要处理调用userMapper的removeGradeIdByGradeIds方法传入班级ID列表清除用户表中对应的班级ID关联假设该方法实现了清除逻辑当前代码中这部分整体被注释掉可按需启用
// if (!gradeIds.isEmpty()) {
// userMapper.removeGradeIdByGradeIds(gradeIds);
// }
//
// //删除用户创建的班级
// //删除用户创建的班级调用gradeMapper的deleteByUserId方法传入用户ID列表批量删除用户创建的班级记录注释部分可按需启用
// gradeMapper.deleteByUserId(userIds);
// //删除用户创建的可供学生练习的题库关联表
// //删除用户创建的可供学生练习的题库关联表调用gradeExerciseMapper的deleteByUserIds方法传入用户ID列表批量删除相关记录注释状态可按需启用
// gradeExerciseMapper.deleteByUserIds(userIds);
// //删除用户批改的主观题分数
// //删除用户批改的主观题分数调用manualScoreMapper的deleteByUserIds方法传入用户ID列表批量删除用户批改主观题分数的相关记录注释内容可按需启用
// manualScoreMapper.deleteByUserIds(userIds);
// //删除公告与班级关联表
// //1.获取公告id
// //删除公告与班级关联表以下是分步骤操作的逻辑先获取公告id再根据公告id删除关联。
// //1.获取公告id调用noticeMapper的selectIdsByUserIds方法传入用户ID列表获取这些用户创建的公告对应的公告ID列表假设该方法实现了查询逻辑当前被注释可按需启用
// List<Integer> noticeIds = noticeMapper.selectIdsByUserIds(userIds);
// //2.删除公告与班级关联
// //2.删除公告与班级关联如果获取到的公告ID列表不为空表示有相关公告需要处理调用noticeGradeMapper的deleteByNoticeIds方法传入公告ID列表删除公告与班级的关联记录假设该方法实现了删除逻辑这部分整体被注释掉可按需启用
// if (!noticeIds.isEmpty()) {
// noticeGradeMapper.deleteByNoticeIds(noticeIds);
// }
// //删除用户创建的公告
// //删除用户创建的公告调用noticeMapper的deleteByUserIds方法传入用户ID列表批量删除用户创建的公告记录注释部分可按需启用
// noticeMapper.deleteByUserIds(userIds);
//
// //删除试题选项
// //1.获取用户创建的试题id
// //删除试题选项先获取用户创建的试题id调用questionMapper的selectIdsByUserIds方法传入用户ID列表获取这些用户创建的试题对应的试题ID列表假设该方法实现了查询逻辑当前代码中处于注释状态可按需启用
// List<Integer> quIds = questionMapper.selectIdsByUserIds(userIds);
// //2.根据试题id列表删除选项
// //2.根据试题id列表删除选项如果获取到的试题ID列表不为空表示有相关试题选项需要处理调用optionMapper的deleteBatchByQuIds方法传入试题ID列表批量删除对应的试题选项假设该方法实现了批量删除逻辑这部分被注释掉可按需启用
// if (!quIds.isEmpty()) {
// optionMapper.deleteBatchByQuIds(quIds);
// }
// //删除用户创建的试题
// //删除用户创建的试题调用questionMapper的deleteByUserIds方法传入用户ID列表批量删除用户创建的试题记录注释内容可按需启用
// questionMapper.deleteByUserIds(userIds);
// //删除用户创建的题库
// //删除用户创建的题库调用repoMapper的deleteByUserIds方法传入用户ID列表批量删除用户创建的题库记录注释部分可按需启用
// repoMapper.deleteByUserIds(userIds);
// //删除用户的错题本
// //删除用户的错题本调用userBookMapper的deleteByUserIds方法传入用户ID列表批量删除用户的错题本记录注释状态可按需启用
// userBookMapper.deleteByUserIds(userIds);
// //删除用户
// //删除用户调用userMapper的deleteByUserIds方法传入用户ID列表删除对应的用户记录返回受影响的行数即实际删除的用户数量。
Integer row = userMapper.deleteByUserIds(userIds);
// //
// 返回删除成功的结果信息目前代码中只是简单返回成功提示实际可根据row的值进一步判断是否真的删除成功等情况进行更准确的提示信息返回比如如果row为0可以返回更详细的失败提示等
return Result.success("删除成功");
}
// 从Excel文件导入用户数据的方法事务性操作先对文件类型进行判断读取文件内容并补充必要的参数然后将用户数据批量插入到数据库中成功返回导入成功的提示信息失败返回相应错误提示使用了异常处理注解来简化异常抛出处理。
@SneakyThrows(Exception.class)
// 使用 @SneakyThrows 注解,当方法内部抛出 Exception 异常时,会自动将异常向上抛出,避免了显式编写 try-catch 块来处理异常(但需要注意在合适的调用层次上进行统一的异常处理),这里表示该方法可能会出现各种异常情况,都统一按此方式处理。
@Override
// 表示重写了接口中定义的方法,说明该方法是实现某个接口规范要求的具体实现逻辑。
@Transactional
// 声明该方法是事务性的,意味着在这个方法执行过程中涉及到的数据库操作(如插入、更新、删除等)会被当作一个整体事务来处理,要么全部成功提交,要么全部回滚,保证数据的一致性和完整性。
public Result<String> importUsers(MultipartFile file) {
//文件类型判断
// 文件类型判断
// 检查传入的文件是否是合法的 Excel 文件xls 或 xlsx 格式),调用 ExcelUtils 工具类的 isExcel 方法进行判断,传入文件的原始文件名(通过 Objects.requireNonNull 确保文件名不为空)。
if (!ExcelUtils.isExcel(Objects.requireNonNull(file.getOriginalFilename()))) {
return Result.failed("文件类型必须是xls或xlsx");
// 如果不是合法的 Excel 文件格式,返回一个表示操作失败的 Result 结果对象,其中包含提示信息“文件类型必须是 xls 或 xlsx”告知调用者文件格式不符合要求具体 Result 对象的结构和使用方式需结合项目中相关定义来看。
return Result.failed("文件类型必须是 xls 或 xlsx");
}
//读取文件
// 读取文件
// 调用 ExcelUtils 工具类的 readMultipartFile 方法,传入文件对象和 UserForm 类信息,读取 Excel 文件内容并转换为 UserForm 类型的列表UserForm 可能是用于接收 Excel 文件中每行数据对应的用户信息的表单对象,存储在 list 变量中。
List<UserForm> list = ExcelUtils.readMultipartFile(file, UserForm.class);
//参数补充
// 参数补充
// 遍历读取到的 UserForm 列表,对每个 UserForm 对象进行一些必要参数的补充设置操作,以下是具体的设置逻辑。
list.forEach(userForm -> {
// 使用 BCryptPasswordEncoderSpring Security 提供的密码加密工具类对默认密码“123456”进行加密并将加密后的密码设置到 UserForm 对象的密码字段中,通常用于为新导入的用户设置初始密码。
userForm.setPassword(new BCryptPasswordEncoder().encode("123456"));
// 调用 DateTimeUtil 工具类的 getDateTime 方法获取当前日期时间信息,并设置到 UserForm 对象的创建时间createTime字段中用于记录用户的创建时间。
userForm.setCreateTime(DateTimeUtil.getDateTime());
// 判断 UserForm 对象中的角色 IDroleId字段是否为 null如果是表示没有指定角色这里默认将角色 ID 设置为 1具体角色 ID 为 1 代表什么角色需结合业务逻辑确定,可能是学生等默认角色)。
if (userForm.getRoleId() == null) {
userForm.setRoleId(1);
}
});
// 判断读取到的用户信息列表的大小是否超过 300如果超过了限制数量返回一个表示操作失败的 Result 结果对象,包含提示信息“表中最多存放 300 条数据”,告知调用者导入的数据量超出了允许范围。
if (list.size() > 300) {
return Result.failed("表中最多存放300条数据");
return Result.failed("表中最多存放 300 条数据");
}
// 调用 userMapper可能是操作用户数据的持久层接口用于与数据库交互的 insertBatchUser 方法,先使用 userConverter可能是用于对象转换的工具类的 listFromToEntity 方法将 UserForm 列表转换为对应的用户实体对象列表,然后将这些用户实体对象批量插入到数据库中,实现批量导入用户的功能。
userMapper.insertBatchUser(userConverter.listFromToEntity(list));
// 如果用户数据批量插入成功,返回一个表示操作成功的 Result 结果对象,包含提示信息“用户导入成功”,告知调用者用户导入操作已顺利完成,具体 Result 对象的结构和使用方式需结合项目中相关定义来看。
return Result.success("用户导入成功");
}
@Override
// 重写接口中定义的方法,用于获取用户信息相关的业务逻辑实现。
public Result<UserVO> info() {
// 调用 userMapper操作用户数据的持久层接口的 info 方法,传入通过 SecurityUtil 工具类获取的当前登录用户的 IDSecurityUtil.getUserId() 获取用户 ID 的具体实现逻辑在该工具类中定义),从数据库中查询并获取当前用户的详细信息,返回的是 UserVO 类型的对象UserVO 可能是用于展示给前端等调用者的用户视图对象,包含了部分用户信息,存储在 userVo 变量中。
UserVO userVo = userMapper.info(SecurityUtil.getUserId());
// 将获取到的用户视图对象 userVo 中的密码字段设置为 null可能出于安全考虑防止将密码信息传递到前端等地方保证用户信息的安全性。
userVo.setPassword(null);
// 返回一个表示操作成功的 Result 结果对象,第二个参数传入处理后的用户视图对象 userVo告知调用者获取用户信息成功并将用户信息传递给调用者具体 Result 对象的结构和使用方式需结合项目中相关定义来看,这里第一个参数传入 null 可能是符合 Result 类的构造函数要求或者业务上不需要额外传递特定的提示信息(具体依项目设定)。
return Result.success(null, userVo);
}
@Override
// 重写接口中定义的方法,用于用户加入班级相关的业务逻辑实现。
public Result<String> joinGrade(String code) {
//获取班级信息
// 获取班级信息
// 创建一个 LambdaQueryWrapper<Grade> 对象用于构建查询班级信息的条件LambdaQueryWrapper 是 MyBatis Plus 提供的方便构建查询条件的工具类,这里用于查询 Grade 类型(可能代表班级实体类)的数据。
LambdaQueryWrapper<Grade> wrapper = new LambdaQueryWrapper<Grade>().eq(Grade::getCode, code);
// 调用 gradeMapper可能是操作班级数据的持久层接口的 selectOne 方法,传入构建好的查询条件 wrapper从数据库中查询符合班级口令code的班级信息返回的是 Grade 类型的对象,代表查询到的班级实体信息,存储在 grade 变量中,如果没有找到匹配的班级则返回 null。
Grade grade = gradeMapper.selectOne(wrapper);
// 判断查询到的班级对象是否为 null如果是表示没有找到对应的班级班级口令不存在返回一个表示操作失败的 Result 结果对象,包含提示信息“班级口令不存在”,告知调用者输入的班级口令有误,无法加入班级,具体 Result 对象的结构和使用方式需结合项目中相关定义来看。
if (Objects.isNull(grade)) {
return Result.failed("班级口令不存在");
}
// 创建一个 User 类型的用户对象,用于后续更新用户与班级关联信息的操作,这里先初始化该对象。
User user = new User();
// 设置用户对象的 ID 为通过 SecurityUtil 工具类获取的当前登录用户的 ID确保操作的是当前登录用户的信息。
user.setId(SecurityUtil.getUserId());
// 设置用户对象的班级 IDgradeId为查询到的班级的 IDgrade.getId()),建立用户与班级的关联关系,准备将当前用户加入到该班级中。
user.setGradeId(grade.getId());
// 调用 userMapper 的 updateById 方法,传入构建好的用户对象,根据用户 ID 更新数据库中对应用户的班级关联信息,返回受影响的行数,即实际更新的记录数,存储在 updated 变量中。
int updated = userMapper.updateById(user);
// 判断更新操作影响的行数是否大于 0如果大于 0表示用户成功加入班级执行以下操作。
if (updated > 0) {
stringRedisTemplate.delete("cache:grade:getPaging:"+SecurityUtil.getUserId().toString());
// 调用 stringRedisTemplate用于操作 Redis 字符串类型数据的工具类,常用于缓存相关操作)的 delete 方法,删除 Redis 中对应缓存键(格式为 "cache:grade:getPaging:" 加上当前登录用户的 ID 的字符串形式)的缓存数据,目的可能是清除与用户班级相关的缓存信息,确保后续获取的班级信息是最新的。
stringRedisTemplate.delete("cache:grade:getPaging:" + SecurityUtil.getUserId().toString());
// 返回一个表示操作成功的 Result 结果对象包含提示信息“加入班级”加上班级名称grade.getGradeName())再加上“成功”,告知调用者已成功加入指定班级,具体 Result 对象的结构和使用方式需结合项目中相关定义来看。
return Result.success("加入班级:" + grade.getGradeName() + "成功");
}
// 如果更新操作影响的行数不大于 0表示用户加入班级操作失败返回一个表示操作失败的 Result 结果对象,包含提示信息“加入失败”,告知调用者加入班级的操作未成功,具体 Result 对象的结构和使用方式需结合项目中相关定义来看。
return Result.failed("加入失败");
}
@Override
// 重写接口中定义的方法,用于实现用户分页查询相关的业务逻辑。
public Result<IPage<UserVO>> pagingUser(Integer pageNum, Integer pageSize, Integer gradeId, String realName) {
// 创建一个 IPage<UserVO> 类型的分页对象 page使用 Page 的构造函数进行初始化,传入当前页码 pageNum 和每页显示数量 pageSize用于构建分页相关的数据结构方便后续设置分页数据以及返回给前端进行分页展示等操作。
IPage<UserVO> page = new Page<>(pageNum, pageSize);
// 判断当前用户的角色(通过 SecurityUtil 工具类的 getRole 方法获取具体获取逻辑在该工具类中定义是否为“role_teacher”可能代表教师角色具体含义由业务定义如果是教师角色执行以下操作。
if ("role_teacher".equals(SecurityUtil.getRole())) {
page = userMapper.pagingUser(page, gradeId, realName, SecurityUtil.getUserId(),1);
// 调用 userMapper 的 pagingUser 方法,传入分页对象 page、班级 IDgradeId、真实姓名realName、当前登录用户的 IDSecurityUtil.getUserId())以及参数 1具体该参数含义需结合业务逻辑确定可能与教师角色下的查询条件等相关根据这些条件查询并设置分页对象 page 中的用户数据信息,用于教师角色下的用户分页查询逻辑。
page = userMapper.pagingUser(page, gradeId, realName, SecurityUtil.getUserId(), 1);
} else {
page = userMapper.pagingUser(page, gradeId, realName, SecurityUtil.getUserId(),null);
// 如果当前用户不是教师角色,执行以下操作,同样调用 userMapper 的 pagingUser 方法,但传入的最后一个参数为 null可能对应非教师角色下不同的查询条件或业务逻辑根据相应条件查询并设置分页对象 page 中的用户数据信息,用于其他角色下的用户分页查询逻辑。
page = userMapper.pagingUser(page, gradeId, realName, SecurityUtil.getUserId(), null);
}
// 返回一个表示操作成功的 Result 结果对象,第二个参数传入构建好的分页对象 page告知调用者用户分页查询成功并将分页查询结果传递给调用者具体 Result 对象的结构和使用方式需结合项目中相关定义来看,这里第一个参数传入 null 可能是符合 Result 类的构造函数要求或者业务上不需要额外传递特定的提示信息(具体依项目设定)。
return Result.success(null, page);
}
@Transactional
// 声明该方法是事务性的,保证方法内涉及的数据库操作作为一个整体事务处理,确保数据一致性和完整性。
@Override
// 重写接口中定义的方法,用于实现用户上传头像相关的业务逻辑。
public Result<String> uploadAvatar(MultipartFile file) {
// 调用 iQuestionService可能是与题目相关的业务服务类这里复用了其上传图片的功能具体原因需结合业务逻辑确定的 uploadImage 方法,传入文件对象,尝试上传用户头像文件,并获取上传操作的结果信息,存储在 result 变量中。
Result<String> result = iQuestionService.uploadImage(file);
// 打印上传操作的结果信息,可能用于调试等目的,查看上传过程中的返回结果情况(在实际生产环境中可能需要根据日志级别等进行合理配置,避免过多不必要的输出)。
System.out.println(result);
// 判断上传结果的状态码(通过 result.getCode() 获取,具体 Result 类中 code 字段的定义和含义需结合项目中相关定义来看)是否为 0如果是 0表示图片上传失败执行以下返回操作。
if (result.getCode() == 0) {
return Result.failed("图片上传失败");
}
// 如果上传结果状态码不为 0表示图片上传成功获取上传后图片的访问地址通过 result.getData() 获取,具体 Result 类中 data 字段用于存储相关数据的定义需结合项目中相关定义来看),存储在 url 变量中。
String url = result.getData();
// 再次打印图片的访问地址,同样可能用于调试等目的,查看获取到的地址是否正确(在实际应用中可根据情况决定是否保留此输出语句)。
System.out.println(url);
// 创建一个 User 类型的用户对象,用于后续更新用户头像信息的操作,这里先初始化该对象。
User user = new User();
// 设置用户对象的 ID 为通过 SecurityUtil 工具类获取的当前登录用户的 ID确保操作的是当前登录用户的头像信息。
user.setId(SecurityUtil.getUserId());
// 设置用户对象的头像字段avatar为获取到的图片访问地址 url准备将用户头像更新为新上传的图片。
user.setAvatar(url);
// 调用 userMapper 的 updateById 方法,传入构建好的用户对象,根据用户 ID 更新数据库中对应用户的头像信息,返回受影响的行数,即实际更新的记录数,判断该行数是否大于 0如果大于 0表示头像更新成功执行以下返回操作。
if (userMapper.updateById(user) > 0) {
// 返回一个表示操作成功的 Result 结果对象,包含提示信息“上传成功”以及上传后图片的访问地址 url告知调用者头像上传成功并将图片地址传递给调用者方便后续使用比如在前端展示用户头像等具体 Result 对象的结构和使用方式需结合项目中相关定义来看。
return Result.success("上传成功", url);
}
// 如果更新操作影响的行数不大于 0表示头像更新失败返回一个表示操作失败的 Result 结果对象,包含提示信息“图片上传失败”,告知调用者头像上传操作未成功,具体 Result 对象的结构和使用方式需结合项目中相关定义来看。
return Result.failed("图片上传失败");
}
}
}

@ -1,15 +1,32 @@
package cn.org.alan.exam.util;
// 定义了该类所属的包名在Java项目中包名用于对类进行分类管理按照功能、模块等因素划分不同的包方便组织代码结构以及避免类名冲突这里表明该 `AliOSSUtil` 类属于 `cn.org.alan.exam.util` 这个包,通常 `util` 包下会放置一些工具类用于提供项目中常用的功能方法从后续代码来看这个类主要是提供了与阿里云对象存储服务OSS相关的操作功能。
import com.aliyun.oss.OSS;
// 导入 `OSS` 接口,它来自 `com.aliyun.oss` 包是阿里云对象存储服务OSS提供的用于操作对象存储的核心接口定义了诸如上传、下载、删除对象等一系列与对象存储相关的操作方法后续代码中会通过其具体实现类来创建对象并调用相应方法实现文件上传到阿里云OSS的功能。
import com.aliyun.oss.OSSClientBuilder;
// 导入 `OSSClientBuilder` 类,同样属于 `com.aliyun.oss` 包,这个类是用于构建 `OSS` 客户端实例的构建器类通过它可以方便地传入相关配置参数如访问端点、访问密钥等来创建一个与阿里云OSS服务进行交互的客户端对象是连接Java代码与阿里云OSS服务的关键构建类使得代码能够基于这个客户端去操作OSS中的存储资源。
import org.springframework.beans.factory.annotation.Value;
// 导入 `Value` 注解,它来自 `org.springframework.beans.factory.annotation` 包在Spring框架中`Value` 注解用于将配置文件中的属性值注入到对应的类成员变量中,通过指定配置文件中属性的键(例如 `${oss.endpoint}`Spring会在运行时读取配置文件并将对应的值赋给被注解的变量实现了配置与代码的解耦方便在不同环境如开发、测试、生产环境下灵活配置相关参数在这里用于注入阿里云OSS相关的配置信息到类的成员变量中。
import org.springframework.stereotype.Component;
// 导入 `@Component` 注解,它是 `org.springframework.stereotype` 包中的注解在Spring框架中使用该注解将一个类标记为Spring组件意味着Spring容器在进行组件扫描时能够发现并管理这个类把它作为一个Bean纳入到Spring的IoC控制反转容器中这样其他地方就可以通过依赖注入等方式使用该类提供的功能便于该工具类在整个Spring项目中与其他组件协作实现将OSS相关操作功能融入项目体系架构中。
import org.springframework.web.multipart.MultipartFile;
// 导入 `MultipartFile` 类型,它来自 `org.springframework.web.multipart` 包在Spring Web框架中`MultipartFile` 用于表示通过HTTP请求上传的文件在这个类的 `upload` 方法中就是接收这样一个表示上传文件的参数然后对其进行处理实现将文件上传到阿里云OSS的功能是处理文件上传功能与Web应用交互的关键类型。
import java.io.IOException;
// 导入 `IOException` 类,属于 `java.io` 包用于处理输入输出操作可能出现的异常情况在代码中像读取文件输入流、关闭OSS客户端等涉及到I/O操作的地方都有可能抛出 `IOException`,通过声明方法抛出该异常或者在 `try-catch` 块中捕获处理它来保证程序在出现I/O相关异常时能有相应的应对机制确保程序的稳定性。
import java.io.InputStream;
// 导入 `InputStream` 类型,来自 `java.io` 包它是Java中用于表示字节输入流的抽象类所有字节输入流类的超类在这里用于获取上传文件对应的输入流以便后续将文件内容通过这个输入流上传到阿里云OSS中是实现文件上传操作中读取文件数据的关键类型。
import java.util.Arrays;
// 导入 `Arrays` 类,属于 `java.util` 包,它提供了一系列用于操作数组的静态方法,在 `isImage` 方法中,使用了它的 `asList` 方法将一个包含常见图片后缀名的数组转换为 `List` 类型,方便后续通过 `List` 的 `contains` 方法来判断给定文件名的后缀是否在常见图片后缀列表中,从而判断文件是否为常见图片格式,简化了数组与列表之间的转换以及判断操作。
import java.util.UUID;
// 导入 `UUID` 类,来自 `java.util` 包,`UUID`(通用唯一识别码)是一种由数字和字母组成的标识符,具有全球唯一性,在代码的 `upload` 方法中,使用 `UUID.randomUUID()` 方法生成一个唯一的标识符然后与原文件名的后缀拼接作为新的文件名这样做的目的是为了避免文件覆盖确保每次上传到OSS的文件都有一个唯一的文件名保证文件存储的唯一性和准确性。
/**
* @author WeiJin
@ -17,38 +34,63 @@ import java.util.UUID;
*/
@Component
public class AliOSSUtil {
// 定义了一个名为 `AliOSSUtil` 的公共类,使用 `@Component` 注解标记它为Spring组件以便被Spring容器管理。这个类主要用于提供与阿里云对象存储服务OSS相关的一些实用功能如文件上传、判断文件格式以及判断文件大小等操作方便在项目中对文件进行存储管理以及相关的前置判断操作。
@Value("${oss.endpoint}")
private String endpoint;
// 使用 `@Value` 注解,将配置文件中键为 `oss.endpoint` 的属性值注入到 `endpoint` 这个私有字符串变量中,`oss.endpoint` 配置项通常代表阿里云OSS服务的访问端点例如 `https://oss-cn-hangzhou.aliyuncs.com` 这样的格式通过注入这个值使得代码能够知道连接到哪个OSS服务端点去进行后续的文件上传等操作实现了配置与代码的分离方便在不同环境下修改配置。
@Value("${oss.access-key-id}")
private String accessKeyId;
// 同样通过 `@Value` 注解,将配置文件中 `oss.access-key-id` 对应的属性值注入到 `accessKeyId` 这个私有变量中,`oss.access-key-id` 是用于在阿里云OSS服务中标识用户身份、进行权限验证的访问密钥ID是访问OSS服务必须的认证信息之一只有提供正确的访问密钥ID和对应的访问密钥秘密下面注入的 `accessKeySecret`才能有权限操作对应的OSS存储空间。
@Value("${oss.access-key-secret}")
private String accessKeySecret;
// 利用 `@Value` 注解将配置文件里 `oss.access-key-secret` 属性对应的字符串值注入到 `accessKeySecret` 这个私有变量中,`oss.access-key-secret` 作为访问密钥秘密,与前面的 `accessKeyId` 一起构成完整的身份认证信息用于向阿里云OSS服务证明操作的合法性保障OSS存储资源的安全性防止未经授权的访问和操作。
@Value("${oss.bucket-name}")
private String bucketName;
// 通过 `@Value` 注解把配置文件中 `oss.bucket-name` 配置项对应的字符串值赋给 `bucketName` 这个私有变量,`oss.bucket-name` 代表的是在阿里云OSS服务中存储空间的名称类似于文件系统中的文件夹概念所有上传到OSS的文件都要存储在特定的存储空间bucket通过指定这个名称代码就能明确将文件上传到哪个具体的存储空间中。
/**
* OSS
*/
public String upload(MultipartFile file) throws IOException {
// 定义了一个公共方法 `upload`,用于将传入的 `MultipartFile` 类型的文件上传到阿里云OSS服务中并且在方法声明中抛出 `IOException` 异常表示在方法执行过程中如果出现与输入输出操作相关的异常比如读取文件输入流失败、网络传输问题等导致的异常情况将由调用者来决定如何处理这个异常可以选择捕获处理或者继续向上层抛出保证了方法在处理文件上传这个I/O密集型操作时的异常处理机制的灵活性。
// 获取上传的文件的输入流
InputStream inputStream = file.getInputStream();
// 通过传入的 `MultipartFile` 文件对象调用其 `getInputStream` 方法,获取到该文件对应的字节输入流对象 `inputStream`这个输入流对象可以用于后续读取文件的内容数据以便将文件内容通过网络传输上传到阿里云OSS中它是实现文件上传操作中读取文件数据的关键步骤只有获取到正确的输入流才能将文件内容按字节发送给OSS服务进行存储。
// 避免文件覆盖
String originalFilename = file.getOriginalFilename();
assert originalFilename != null : "上传文件时获取文件名失败为null";
// 从传入的 `MultipartFile` 文件对象中获取其原始文件名即客户端上传文件时的原始文件名包含后缀名通过获取这个文件名后续可以基于它来生成一个新的唯一文件名达到避免文件覆盖的目的因为如果直接使用原始文件名上传到OSS可能会出现同名文件被覆盖的情况所以需要进行文件名的处理这里先获取原始文件名作为后续操作的基础。
assert originalFilename!= null : "上传文件时获取文件名失败为null";
// 使用断言(`assert`)语句来确保获取到的原始文件名不为 `null`,如果 `originalFilename` 为 `null`,则会抛出 `AssertionError` 异常并显示指定的错误信息 "上传文件时获取文件名失败为null",这是一种简单的防御性编程方式,在开发阶段可以快速发现可能出现的空指针问题,保证后续基于文件名的操作能够正常进行,不过在生产环境中通常断言是可以被禁用的,需要配合合适的日志记录等其他方式来监控这类潜在问题。
String fileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));
// 通过调用 `UUID.randomUUID()` 方法生成一个全球唯一的标识符UUID字符串然后与原始文件名中获取到的后缀名通过 `substring` 方法从原始文件名中截取从最后一个点 `.` 之后的部分,即后缀名部分)进行拼接,得到一个新的文件名 `fileName`这样生成的文件名具有唯一性能够有效避免在上传文件到OSS时出现同名文件覆盖的问题保证每次上传的文件都能以唯一的名称存储在OSS中。
//上传文件到 OSS
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 使用 `OSSClientBuilder` 类的 `build` 方法传入前面注入的OSS服务端点`endpoint`、访问密钥ID`accessKeyId`)以及访问密钥秘密(`accessKeySecret`这三个关键配置参数构建出一个与阿里云OSS服务进行交互的 `OSS` 客户端对象 `ossClient`通过这个客户端对象就能调用OSS提供的各种方法来操作存储资源在这里就是调用 `putObject` 方法将文件上传到指定的存储空间中是实现文件上传到OSS的核心操作步骤创建出可用的客户端对象才能与OSS服务建立连接并进行后续的上传操作。
ossClient.putObject(bucketName, fileName, inputStream);
// 调用 `ossClient`前面创建的OSS客户端对象的 `putObject` 方法将文件上传到阿里云OSS中。传入的参数分别是之前获取到的存储空间名称`bucketName`)、新生成的唯一文件名(`fileName`)以及文件对应的输入流(`inputStream`这个方法会将输入流中的文件内容按照指定的文件名上传到对应的存储空间里实现了将本地文件上传到云端OSS存储的实际操作完成文件上传的核心功能。
//文件访问路径
String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
// 构建一个字符串 `url`用于表示上传到OSS后的文件的访问路径。通过对之前注入的OSS服务端点`endpoint`)进行分割操作(以 `"//"` 为分隔符),取其前面部分(协议部分,如 `https:`)和后面部分(域名及端口等部分,如 `oss-cn-hangzhou.aliyuncs.com`),然后按照一定的格式与存储空间名称(`bucketName`)和文件名(`fileName`进行拼接形成一个完整的文件访问路径这个路径后续可以用于在浏览器或者其他客户端中访问上传到OSS的文件方便用户获取和查看上传的文件内容。
// 关闭ossClient
ossClient.shutdown();
// 调用 `ossClient`OSS客户端对象的 `shutdown` 方法关闭与阿里云OSS服务的连接释放相关资源避免资源泄漏这是良好的编程习惯在完成文件上传等操作后及时关闭不再使用的客户端连接确保系统资源的合理利用和程序的稳定运行尤其是在频繁进行文件上传操作或者长时间运行的应用场景下及时关闭连接尤为重要。
// 把上传到oss的路径返回
return url;
// 将构建好的表示文件在OSS中访问路径的字符串 `url` 返回给调用者这样调用者就可以获取到上传文件在OSS中的具体访问位置方便根据这个路径进行后续的操作比如在网页中展示图片时可以将这个路径设置为 `<img>` 标签的 `src` 属性值,使得用户能够查看上传的图片等,完成文件上传方法并返回相关有用信息的整个流程。
}
@ -59,9 +101,17 @@ public class AliOSSUtil {
* @return
*/
public boolean isImage(String filename) {
// 定义了一个公共方法 `isImage`,用于判断传入的文件名(`filename`)是否代表一个常见的图片格式,返回一个布尔值结果,方便在其他地方(比如文件上传前进行格式验证等场景)调用该方法来快速判断文件是否是图片类型,以此来决定是否允许文件进行后续的操作(如只允许图片文件上传等业务逻辑)。
String lastName = filename.substring(filename.lastIndexOf(".") + 1);
// 通过传入的文件名(`filename`)获取其后缀名,先使用 `lastIndexOf` 方法找到文件名中最后一个点(`.`)的位置,然后使用 `substring` 方法从该位置后一位(即点后面的第一个字符开始)截取字符串,得到文件名的后缀名部分,并存储在 `lastName` 变量中,这是判断文件格式的基础操作,通过获取后缀名来与常见图片格式进行对比,进而确定文件是否为图片格式。
String[] lastnames = {"png", "jpg", "jpeg", "bmp"};
// 创建一个包含常见图片后缀名(如 `png`、`jpg`、`jpeg`、`bmp`)的字符串数组 `lastnames`,用于后续与获取到的文件名后缀进行对比,以此来判断文件是否属于常见图片格式,这里明确列出了常见的几种图片格式,通过将实际文件名后缀与这个预设的常见图片后缀数组进行比较来做出判断。
return Arrays.asList(lastnames).contains(lastName);
// 使用 `Arrays.asList` 方法将前面定义的包含常见图片后缀名的数组 `lastnames` 转换为 `List` 类型(得到一个不可变的列表),然后调用这个列表的 `contains` 方法,传入获取到的文件名后缀(`lastName`),判断该后缀是否在这个常见图片后缀列表中,如果在列表中,则说明文件是常见图片格式,返回 `true`;否则返回 `false`通过这样的方式简洁地实现了判断文件是否为常见图片格式的功能利用了Java集合框架提供的便利方法来进行字符串的包含关系判断。
}
/**
@ -71,6 +121,9 @@ public class AliOSSUtil {
* @return
*/
public boolean isOverSize(MultipartFile file) {
// 定义了一个公共方法 `isOverSize`,用于判断传入的 `MultipartFile` 类型的文件大小是否大于50KB在实际代码中判断的是大于20 * 1024 * 1024字节即20MB可能是代码中的具体业务需求决定的这个大小限制此处解析按文本中的描述来返回一个布尔值结果便于在文件上传等业务场景中对文件大小进行限制和验证比如拒绝过大的文件上传避免占用过多资源或者不符合业务规定等情况。
return file.getSize() > 20 * 1024 * 1024;
// 通过传入的 `MultipartFile` 文件对象调用其 `getSize` 方法获取文件的大小以字节为单位然后与指定的大小这里是20 * 1024 * 1024字节即20MB进行比较如果文件大小大于这个指定值则返回 `true`,表示文件超过了规定大小;否则返回 `false`,通过简单的比较操作实现了对文件大小是否超出限制的判断功能,为文件相关的业务操作提供了大小验证机制。
}
}
}

@ -1,243 +1,426 @@
package cn.org.alan.exam.util;
import cn.hutool.core.util.BooleanUtil;
// 导入 `BooleanUtil` 类,它来自 `cn.hutool.core.util` 包,这个工具类主要用于对 `Boolean` 类型进行一些实用的操作,例如在与 Redis 交互获取锁操作时(如代码中 `tryLock` 方法内),可以使用它来安全地判断返回的 `Boolean` 值是否为 `true`,避免自动拆箱可能导致的空指针异常,方便对布尔类型的结果进行更稳健的处理,使代码在处理这类情况时更加健壮。
import cn.hutool.core.util.StrUtil;
// 导入 `StrUtil` 类,同样来自 `cn.hutool.core.util` 包,它提供了丰富的字符串操作方法。在代码中多处用于判断字符串是否为空(如 `isNotBlank` 和 `isBlank` 方法的使用场景),相较于 Java 原生的字符串判断方式,它提供了更便捷、功能更全面的字符串判空以及其他常用字符串操作功能,帮助更方便地处理和验证缓存数据获取到的 JSON 字符串等相关操作中的字符串情况。
import cn.hutool.json.JSONObject;
// 导入 `JSONObject` 类,属于 `cn.hutool.json` 包,`JSONObject` 可以看作是一个类似 Java 中 `Map` 结构的对象,用于操作 JSON 数据中的键值对信息。在代码里常用于对从 Redis 中获取到的缓存数据(以 JSON 字符串形式存储,解析后就常以 `JSONObject` 形式来操作)进行提取具体属性值、获取 JSON 数组等操作,方便后续将其转换为业务所需的具体 Java 对象或者构建更复杂的数据结构(如分页数据结构等)。
import cn.hutool.json.JSONUtil;
// 导入 `JSONUtil` 类,也是 `cn.hutool.json` 包中的工具类,它提供了一系列用于 JSON 数据与 Java 对象之间相互转换的便捷方法。比如在向 Redis 存储数据时,会使用它将 Java 对象序列化为 JSON 字符串(通过 `toJsonStr` 方法);从 Redis 获取到 JSON 字符串数据后,又会用它将 JSON 字符串反序列化为 Java 对象(通过 `toBean` 方法)或者转换为 `List` 等其他数据结构(通过 `toList` 等相关方法),极大地简化了 JSON 数据在缓存操作中的处理流程。
import cn.org.alan.exam.model.entity.RedisData;
// 导入 `RedisData` 类,它属于项目自定义的 `cn.org.alan.exam.model.entity` 包,这个类应该是专门用于封装在 Redis 中存储的数据以及其相关属性(例如逻辑过期时间等信息)的实体类,使得在缓存操作中可以通过这个统一的实体类来管理和传递缓存相关的数据,方便进行逻辑过期判断以及数据的存储和提取等操作,增强了缓存数据结构的规范性和可维护性。
import cn.org.alan.exam.model.vo.GradeVO;
// 导入 `GradeVO` 类,来自 `cn.org.alan.exam.model.vo` 包,通常 `VO`Value Object值对象是用于在不同层之间传递数据的简单对象这里的 `GradeVO` 大概率是用于表示特定业务场景下(可能与成绩相关等,具体取决于业务含义)的数据传输对象,在缓存操作涉及到与成绩相关的数据获取、存储以及返回等流程中,会使用这个类来明确数据的结构和类型,便于进行类型转换以及符合业务逻辑的操作。
import com.baomidou.mybatisplus.core.metadata.IPage;
// 导入 `IPage` 类,它是 `com.baomidou.mybatisplus.core.metadata` 包中的接口,在 MyBatis Plus 框架中用于表示分页数据的相关信息,包含了如总记录数、每页大小、当前页码以及实际的数据记录列表等分页相关的属性和操作方法。在缓存操作涉及分页数据获取和返回时(比如从 Redis 中获取缓存的分页数据并进行相应处理后返回符合 `IPage` 格式的数据给调用者),会基于这个接口来构建和操作分页数据结构,以满足业务中对分页查询缓存数据的需求。
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
// 导入 `Page` 类,来自 `com.baomidou.mybatisplus.extension.plugins.pagination` 包,它是 `IPage` 接口的一个具体实现类,在代码中用于创建实际的分页对象实例(如在处理缓存中分页数据时,通过 `new Page<>()` 创建分页对象,然后设置相应的分页属性,像记录列表、总记录数等,来构建完整的分页数据结构用于返回符合业务要求的分页数据结果),方便在 MyBatis Plus 框架下进行分页相关的操作以及与缓存操作结合处理分页数据的缓存情况。
import jakarta.annotation.Resource;
// 导入 `Resource` 注解,它来自 `jakarta.annotation` 包(这是 Java EE 规范中的一部分,用于实现依赖注入等功能),在代码中用于告诉 Spring 容器查找并注入相应类型的资源(例如 `StringRedisTemplate` 实例)到对应的字段上,通过这个注解实现了依赖注入的功能,使得代码遵循松耦合的设计原则,方便获取和使用其他组件提供的功能,提高代码的可维护性和扩展性。
import lombok.extern.slf4j.Slf4j;
// 导入 `Slf4j` 注解,来自 `lombok.extern.slf4j` 包Lombok 是一个通过注解来简化 Java 代码编写的工具,`Slf4j` 注解会在编译期自动为这个类生成一个名为“log”的日志记录对象方便在类中使用这个对象来记录各种运行时的信息例如在缓存操作出现异常、缓存命中情况等场景下记录相关日志便于后续的调试和监控工作提升代码在生产环境运行时的可观察性和问题排查能力。
import org.apache.catalina.Pipeline;
// 导入 `Pipeline` 类,它属于 `org.apache.catalina` 包,在 Tomcat 服务器的架构中,`Pipeline` 用于表示一个处理请求的管道包含了一系列的阀门Valve用于对请求进行不同阶段的处理不过在当前展示的缓存相关代码中它可能并没有直接被使用到也许所在的类所在的项目环境中有和 Tomcat 集成等相关情况才引入了这个类,或者只是作为一种通用的依赖引入(但从当前代码逻辑看暂时未体现其作用)。
import org.apache.catalina.connector.Response;
// 导入 `Response` 类,来自 `org.apache.catalina.connector` 包,在 Tomcat 服务器中,`Response` 类用于封装对客户端请求的响应信息,例如设置响应头、响应状态码、响应体内容等,同样在当前缓存相关代码里,它可能没有直接参与到主要的缓存操作逻辑中,也许是所在项目环境基于 Tomcat 相关架构时有潜在的关联或者只是作为一种通用依赖引入(目前从代码逻辑角度未看到其实际作用)。
import org.springframework.data.redis.core.StringRedisTemplate;
// 导入 `StringRedisTemplate` 类,它是 `org.springframework.data.redis.core` 包中的重要类,是 Spring Data Redis 提供的用于操作 Redis 的模板类,专门用于处理 Redis 中字符串类型的数据(实际上 Redis 中存储的数据大多可以以字符串形式来操作,其他复杂结构也常基于字符串序列化来存储)。在代码中通过注入这个模板类的实例后,就可以利用它提供的各种方法(如 `opsForValue` 方法获取操作字符串值的相关操作对象,进而进行数据的存储、查询等操作)来与 Redis 进行交互,实现缓存数据的读写等功能,是整个缓存操作代码与 Redis 进行通信的核心工具类。
import org.springframework.data.redis.core.script.DefaultRedisScript;
// 导入 `DefaultRedisScript` 类,属于 `org.springframework.data.redis.core.script` 包,它是 Spring Data Redis 中用于定义 Redis 脚本的一个默认实现类。在一些复杂的 Redis 操作场景中(比如需要执行自定义的 Lua 脚本实现特定的业务逻辑,像分布式锁的复杂判断和操作等情况),可以通过这个类来创建 Redis 脚本对象,设置脚本内容以及期望的返回类型等信息,然后结合 `StringRedisTemplate` 来执行脚本,实现更灵活、功能更强大的 Redis 操作功能,不过从当前展示的代码来看,可能部分相关使用代码被注释掉了或者暂时未体现完整的使用场景。
import org.springframework.data.redis.core.script.RedisScript;
// 导入 `RedisScript` 接口,来自 `org.springframework.data.redis.core.script` 包,它定义了 Redis 脚本相关的通用行为和属性,`DefaultRedisScript` 就是它的一个具体实现类。通过面向接口编程的方式,使得代码在使用 Redis 脚本时更具灵活性和可扩展性,可以方便地切换不同的脚本实现或者进行统一的脚本操作处理,虽然在当前代码中可能没有全面展示其所有使用方式,但遵循接口规范有助于代码更好地适应未来可能的功能扩展和变化。
import org.springframework.stereotype.Component;
// 导入 `@Component` 注解,它是 `org.springframework.stereotype` 包中的注解,在 Spring 框架中用于将一个类标记为 Spring 组件,意味着 Spring 容器在进行组件扫描时能够发现并管理这个类,将其作为一个 Bean 纳入到 Spring 的 IoCInversion of Control控制反转容器中便于后续在需要的地方通过依赖注入等方式使用该类对应的功能实现了组件化管理让这个类能更好地融入到整个 Spring 项目的体系架构中,方便与其他 Spring 组件协作完成缓存相关的业务功能。
import java.time.LocalDateTime;
// 导入 `LocalDateTime` 类,来自 `java.time` 包,它是 Java 8 引入的新的日期和时间 API 中的一部分,用于表示不带时区信息的日期时间对象,在代码中常用于表示缓存数据的逻辑过期时间(比如在设置逻辑过期的缓存数据时,通过 `LocalDateTime.now()` 获取当前时间并加上一定时长来计算出逻辑过期时间,以此来实现更灵活的缓存过期判断逻辑,区别于 Redis 原生的简单基于时间戳的物理过期机制,提高缓存管理的灵活性和业务适应性)。
import java.util.*;
// 导入 `java.util` 包,这是 Java 标准库中非常基础且常用的一个包,包含了大量用于处理集合(如 `List`、`Map`、`Set` 等)、日期时间(部分旧的日期时间相关类等,不过在 Java 8 后推荐使用新的 `java.time` 包中的类)、随机数生成(`Random` 类用于生成随机数,在代码中用于给缓存过期时间添加一定随机性以避免缓存雪崩等问题)等各种常用功能的类和接口,为代码中的很多操作(比如缓存数据存储时使用 `Map` 结构来整理数据、生成随机的缓存过期时间等情况)提供了基础的工具支持。
import java.util.concurrent.*;
// 导入 `java.util.concurrent` 包,它提供了用于支持并发编程的各种类和接口,例如线程池相关的 `ExecutorService`(代码中创建了固定线程数量的线程池 `CACHE_REBUILD_EXECUTOR` 用于在缓存击穿处理时异步执行缓存重建任务,避免主线程阻塞,提高系统并发处理能力)、`Callable` 和 `Future` 等用于支持更灵活的异步任务执行和结果获取等功能的类,在处理缓存相关操作中涉及到多线程、异步任务(如缓存重建等场景)时会依赖这个包中的相关功能来实现高效、稳定的并发操作。
import java.util.function.Function;
import java.util.stream.Collectors;
// 导入 `Function` 接口,它属于 `java.util.function` 包,是 Java 8 引入的函数式接口之一,用于表示接受一个参数并返回一个结果的函数,在代码中常用于传递数据库查询逻辑(例如在 `queryWithPassThrough` 和 `queryWithLogicalExpire` 等方法中,通过传入 `Function<ID, R>` 类型的参数 `dbFallback`,可以使用 Lambda 表达式或者方法引用来指定当缓存未命中或者缓存数据过期时从数据库中获取对应数据的具体实现方法,体现了函数式编程风格在缓存操作代码中的应用,使得代码更加简洁、灵活且易于扩展)。
import java.util.stream.Collectors;
// 导入 `Collectors` 类,来自 `java.util.stream` 包,它在 Java 8 的 Stream API 中用于对数据流进行各种收集操作,比如将流中的元素收集到 `List`、`Map`、`Set` 等集合类型中(在代码里有多处使用,例如将经过处理后的字符串键通过 `collect(Collectors.toList())` 收集到 `List<String>` 类型的列表中,方便后续批量操作缓存数据时使用,利用 Stream API 和 `Collectors` 类可以更方便、高效地处理集合数据以及进行数据转换和整理等操作)。
/**
* @Author Alan
* @Version
* @Date 2024/6/9 11:03 PM
*/
@Slf4j
// 使用Lombok的@Slf4j注解会在编译期自动为这个类生成一个名为“log”的日志记录对象方便在类中使用这个对象来记录各种运行时的信息例如在缓存操作出现异常、命中情况等场景下记录相关日志便于后续的调试和监控工作。
@Component
// 使用Spring的@Component注解将这个类标记为一个Spring组件使得Spring容器在进行组件扫描时能够发现并管理它便于后续在需要的地方通过依赖注入等方式使用该类对应的功能实现了组件化管理让这个类能更好地融入到整个Spring项目的体系架构中方便与其他Spring组件协作完成缓存相关的业务功能。
public class CacheClient {
// 注入StringRedisTemplate
@Resource
// 使用Jakarta EE中的@Resource注解进行资源注入告诉Spring容器查找并注入一个StringRedisTemplate类型的实例到当前类的这个字段中使得本类可以使用这个已经配置好的StringRedisTemplate对象来与Redis进行交互操作例如进行数据的存储、查询等操作符合依赖注入的开发模式方便获取和使用其他组件提供的功能。
private StringRedisTemplate stringRedisTemplate;
private Random random = new Random();
// 创建一个Random实例用于生成随机数在这里主要是在设置缓存过期时间等操作时添加一定的随机时长以避免大量缓存同时过期引发缓存雪崩等问题通过增加这个随机性可以让缓存过期时间更加分散提高缓存系统的稳定性和可靠性。
// 方法1 解决穿透
public void set(String key, Object value, Long time, TimeUnit unit) {
// 我们往Redis存的时候不能是Object类型我们需要把Object序列化为JSON字符串
// 使用注入的StringRedisTemplate对象的opsForValue方法获取操作字符串值的相关操作对象然后调用其set方法将键值对存入Redis。
// 其中JSONUtil.toJsonStr(value)是利用Hutool工具库中的JSONUtil工具类将传入的Java对象value转换为JSON字符串格式这样才能符合Redis存储要求将数据以合适的格式保存到Redis中time参数表示缓存的时长unit参数表示时长的时间单位例如秒、分钟等用于设置缓存的过期策略确保缓存数据在一定时间后自动失效避免数据长期占用内存空间且能保证数据的时效性。
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
// 方法2 解决击穿 使用逻辑过期 比上面的方法的操作多了一个逻辑过期字段而已
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
// 设置逻辑过期
// 创建一个RedisData对象这个对象应该是项目自定义的用于封装在Redis中存储的数据以及其逻辑过期时间等相关信息的实体类通过它可以更好地管理带有逻辑过期特性的数据存储结构以便后续能方便地判断数据是否逻辑过期实现更灵活的缓存策略。
RedisData redisData = new RedisData();
// 将传入的业务数据value设置到RedisData对象的data属性中这里的设计是将实际要缓存的数据作为一个属性封装在RedisData对象内方便后续统一处理和扩展例如可以在RedisData类中添加更多与缓存相关的元数据等信息。
redisData.setData(value);
// 计算逻辑过期时间通过获取当前时间LocalDateTime.now()并加上指定的时长unit.toSeconds(time)先将传入的时间单位转换为秒数再进行时间相加操作得到一个未来的时间点表示该缓存数据的逻辑过期时间以此来实现逻辑过期的功能区别于Redis原生的基于时间戳的物理过期机制逻辑过期可以在代码层面更灵活地控制缓存数据的有效性判断。
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
// 我们往Redis存的时候不能是Object类型我们需要把Object序列化为JSON字符串
// 同样使用StringRedisTemplate的opsForValue操作对象将数据存入Redis不过这里是将封装好数据和逻辑过期时间的RedisData对象转换为JSON字符串后再存入这样Redis中存储的内容就包含了业务数据以及对应的逻辑过期时间信息方便后续读取出来进行相关的逻辑过期判断等操作。
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
// 方法3 解决穿透
// 返回值不确定,我们要使用泛型,比如<R>R,具体是什么类型由用户传入Class<R> type
public <R, ID, T> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Class<T> voClass, Long time, TimeUnit unit) {
// 定义了一个泛型方法 `queryWithPassThrough`用于处理缓存穿透问题并从缓存Redis或数据库中查询数据并返回。
// `<R, ID, T>` 表示这是一个带有三个泛型参数的泛型方法,`R` 是最终期望返回的数据类型参数,其具体类型由调用者根据业务需求指定,比如可能是某个实体类类型,表示最终要返回给调用者的业务对象类型;`ID` 是用于唯一标识要查询数据的标识信息的类型参数,通常依据业务场景来确定,例如可以是整数类型作为数据库记录的主键等;`T` 则是在处理特定类型(如分页相关类型数据时涉及的列表元素类型等情况)时使用的类型参数,同样由业务场景决定,通过这三个泛型参数使得方法能够灵活适应多种不同类型数据的查询及处理需求。
// `keyPrefix` 参数是一个字符串类型,用于构建缓存数据在 Redis 中的键的前缀部分,按照业务模块、数据分类等规则来设置不同的前缀,有助于对缓存数据进行分类管理,方便准确地定位到特定范围的缓存项,后续它会和具体的标识 `id` 拼接成完整的 Redis 键,用于查找对应的缓存数据。
// `id` 参数就是前面提到的用于唯一标识要查询数据的标识信息,其类型由泛型参数 `ID` 确定,通过它与 `keyPrefix` 拼接生成准确的 Redis 键,以此来指向特定的缓存数据记录,实现对不同个体数据的缓存操作及查询定位。
// `type` 参数是 `Class<R>` 类型,用于明确告知方法期望返回的数据最终应该反序列化为什么具体的 Java 类型(通过传入对应的 `Class` 对象,例如 `User.class` 表示要返回的是 `User` 类型的数据),以便方法内部根据这个类型信息进行正确的反序列化操作,将从缓存或数据库获取到的数据转换为符合业务要求的对象类型后返回给调用者。
// `dbFallback` 参数是一个函数式接口 `Function<ID, R>` 类型,代表了一个接受 `ID` 类型参数并返回 `R` 类型结果的函数,在这里它用于在缓存未命中(即缓存中不存在对应数据)的情况下,从数据库中获取对应的数据的逻辑实现,一般可以通过 Lambda 表达式或者方法引用来传递具体的从数据库查询数据的实现方法,这体现了缓存穿透问题的一种解决策略,当缓存无法提供有效数据时,依靠数据库来获取数据,保证数据的完整性,避免直接返回空值给调用者。
// `voClass` 参数是 `Class<T>` 类型,在处理某些特定复杂数据结构(如分页数据等情况)时,用于指定列表中元素的具体类型,方便将缓存中获取到的 JSON 数据准确地转换为相应类型的列表元素,使得数据在反序列化和后续使用过程中类型是符合业务预期的,增强了方法对不同数据结构处理的灵活性和准确性。
// `time` 参数是 `Long` 类型,用于指定缓存数据的过期时间相关设置,结合 `TimeUnit` 参数来精确确定缓存数据在 Redis 中应该保持有效的时长,合理控制缓存的时效性,既能让缓存数据在一定时间内发挥作用提高查询性能,又能避免数据长期占用内存资源导致内存浪费等问题。
// `TimeUnit` 参数是 `TimeUnit` 类型,它是 Java 中用于表示时间单位的枚举类型(比如 `TimeUnit.SECONDS` 表示秒、`TimeUnit.MINUTES` 表示分钟等),与 `time` 参数配合使用,明确缓存数据的过期时长设置,根据业务场景中数据的更新频率、重要性等因素来选择合适的时间单位和时长进行配置,使得缓存过期时间的设置更贴合实际业务需求。
// 根据传入的键前缀keyPrefix和具体的标识id拼接成完整的Redis键用于后续在Redis中查找对应的缓存数据这种方式方便对不同类型、不同标识的数据进行统一的缓存管理通过键的规则来区分不同的缓存项。
String key = keyPrefix + id;
// 通过简单的字符串拼接操作,将传入的键前缀 `keyPrefix` 和具体的标识 `id``id` 会根据其自身的 `toString` 方法转换为字符串形式参与拼接)组合成一个完整的 Redis 键 `key`,这个键在后续与 Redis 进行交互操作(如查询缓存数据、写入缓存数据等)时作为唯一的标识,依据不同的前缀和标识可以清晰地区分各种不同的缓存数据,便于进行统一的、有条理的缓存管理,确保能够准确地从 Redis 众多缓存数据中定位到目标数据。
// TODO 1. 从Redis查询商铺缓存
// 可以选择Hash结构没问题也能String
// 使用StringRedisTemplate的opsForValue操作对象的get方法根据前面生成的键key从Redis中获取对应的缓存数据获取到的数据是以JSON字符串形式存在因为之前存入时进行了序列化操作如果缓存中不存在该键对应的数据则返回null这里先尝试从缓存中获取数据以利用缓存来提升性能避免每次都直接访问数据库。
String json = stringRedisTemplate.opsForValue().get(key);
// 借助之前注入的 `stringRedisTemplate` 对象(它是 Spring Data Redis 提供的用于操作 Redis 的模板类)的 `opsForValue()` 方法获取操作 Redis 字符串值的相关操作对象,然后调用其 `get` 方法,传入前面生成的完整 Redis 键(`key`),尝试从 Redis 中获取对应的缓存数据。
// 由于在之前向 Redis 存入数据时,通常会先将 Java 对象进行序列化操作转换为 JSON 字符串后再存入(这是符合 Redis 存储字符串数据的要求以及方便后续反序列化还原数据的常见做法),所以这里通过 `get` 方法获取到的数据(如果存在的话)也会是以 JSON 字符串的形式返回。
// 若 Redis 中不存在该键对应的缓存数据,那么 `get` 方法就会返回 `null`,这个返回结果后续会用于判断缓存是否命中,进而依据不同的情况决定下一步的操作逻辑,例如是直接返回数据(缓存命中且数据有效时)、进行额外判断(缓存命中但数据为空值时)还是从数据库获取数据(缓存未命中时)等不同的处理方式,先从缓存中获取数据的目的在于利用缓存来提升整体的数据查询性能,避免每次都直接去访问相对较慢的数据库操作。
// TODO 2. 判断时Redis是否命中
if (StrUtil.isNotBlank(json)) {
// 使用 `StrUtil` 工具类(假设是 `Hutool` 工具库中的字符串工具类,用于方便地进行字符串相关操作和判断)的 `isNotBlank` 方法来判断获取到的 JSON 字符串(`json`)是否不为空。
// `isNotBlank` 方法会判断字符串是否不为 `null` 且长度大于 0 并且不只是包含空白字符(如空格、制表符等),如果满足这个条件,就认为字符串是有实际内容的,也就是缓存命中(获取到了有效的缓存数据),此时按照业务逻辑进入下面根据期望返回的数据类型进行不同处理的流程。
// 如果获取到的JSON字符串不为空即缓存命中需要根据期望返回的数据类型type进行不同的处理因为不同类型的数据在反序列化和返回时可能有不同的操作要求。
if (type.equals(IPage.class)) {
// List<T> dataList = JSONUtil.toList(JSONUtil.parseArray(), voClass);
// IPage<T> page = new Page<>();
// page.setRecords(dataList); // 假设Page类有此方法设置数据列表
// // 设置其他分页属性如total、current等这取决于IPage的具体实
// // 设置其他分页属性如total、current等这取决于IPage的具体实
// return (R) page; // 强制转换为 R 类型返回
// 解析JSON
// 解析JSON使用Hutool的JSONUtil工具类的parseObj方法将获取到的JSON字符串解析为JSONObject对象方便后续从中提取具体的属性值JSONObject可以看作是一个类似Map结构的对象用于操作JSON数据中的键值对信息。
JSONObject jsonObject = JSONUtil.parseObj(json);
// 调用 `JSONUtil` 工具类(假设使用了相关的 JSON 处理工具库,用于方便地进行 JSON 数据与 Java 对象之间的转换操作)的 `parseObj` 方法,传入获取到的 JSON 字符串(`json`),这个方法会将 JSON 字符串解析为 `JSONObject` 类型的对象,`JSONObject` 在功能上类似 Java 中的 `Map`,可以方便地通过键来获取对应的值,便于后续从解析后的 JSON 数据中提取出分页数据相关的各个属性值,比如记录列表、总记录数、每页大小、当前页码等信息,为构建完整的 `IPage` 对象做准备。
// 获取记录列表并转换为List<T>
// 获取记录列表并转换为List<T>通过从解析后的JSONObject对象中获取名为"records"的JSON数组对应IPage中的数据记录列表再利用JSONUtil的toList方法将其转换为指定类型voClass的列表这样就得到了分页数据中的实际记录列表数据用于后续构建IPage对象。
List<T> dataList = JSONUtil.toList(jsonObject.getJSONArray("records"), voClass);
// 首先通过前面解析得到的 `JSONObject` 对象(`jsonObject`)调用 `getJSONArray` 方法获取名为 `"records"` 的 JSON 数组,这个数组对应着分页数据中的实际数据记录部分(在 `IPage` 数据结构中通常会有一个属性用于存储具体的数据记录列表)。
// 然后调用 `JSONUtil` 的 `toList` 方法,传入获取到的 JSON 数组以及指定的类型信息 `voClass`(用于明确列表中元素的具体类型),这个方法会将 JSON 数组中的每个元素按照 `voClass` 指定的类型进行反序列化转换,最终得到一个 `List<T>` 类型的列表对象 `dataList`,这个列表就包含了分页数据中的实际记录数据,后续可以将其设置到 `IPage` 对象中作为其数据记录部分。
// 创建Page对象并设置属性
// 创建Page对象并设置属性创建一个新的Page对象这里假设Page类是MyBatis Plus中用于表示分页数据的类有相应的方法来设置分页相关的属性先将前面获取到的记录列表设置到Page对象中然后分别从解析后的JSONObject对象中获取"total"(总记录数)、"size"(每页大小)、"current"当前页码等属性值并设置到Page对象相应的属性上这样就构建好了一个完整的IPage对象用于返回符合分页数据格式要求的结果。
IPage<T> page = new Page<>();
page.setRecords(dataList); // 设置记录列表
page.setTotal(jsonObject.getInt("total")); // 设置总记录数
page.setSize(jsonObject.getInt("size")); // 设置每页大小
page.setCurrent(jsonObject.getInt("current")); // 设置当前页码
// 创建一个新的 `IPage<T>` 类型的对象 `page`(这里假设 `Page` 类是来自 MyBatis Plus 框架中用于表示分页数据结构的类,它提供了相应的方法来设置和操作分页相关的各种属性)。
// 首先调用 `page` 对象的 `setRecords` 方法,将前面获取并转换得到的包含实际记录数据的 `List<T>` 类型的 `dataList` 设置为 `page` 对象的记录列表属性,这样就填充了分页数据中的具体数据内容部分。
// 接着通过 `jsonObject`(前面解析 JSON 字符串得到的 `JSONObject` 对象)分别调用 `getInt` 方法获取 `"total"`(表示总记录数)、`"size"`(表示每页大小)、`"current"`(表示当前页码)等属性值,并依次调用 `page` 对象的 `setTotal`、`setSize`、`setCurrent` 等相应方法将这些属性值设置到 `page` 对象对应的属性上,通过这样一系列的操作,就构建好了一个完整的符合分页数据格式要求的 `IPage<T>` 对象,最后可以将其强制转换为泛型要求的 `R` 类型并返回给调用者,使得调用者能够获取到符合业务需求的分页数据格式的结果数据,用于后续的业务操作,比如展示分页列表等情况。
return (R) page; // 强制转换为 R 类型返回
} else if(type.equals(List.class)){
// 由于方法期望返回的类型是由泛型参数 `R` 指定的类型,而在这里构建好的是 `IPage<T>` 类型的对象 `page`,所以需要将 `page` 对象强制转换为 `R` 类型后再返回给调用者,这里的强制转换要求 `R` 类型在实际调用时必须是与 `IPage<T>` 类型兼容(比如 `R` 就是 `IPage<T>` 或者是它的父类等情况),否则会在运行时出现类型转换异常,通过这样的返回操作,将符合业务要求和分页数据格式的结果返回给调用者,完成在缓存命中且期望返回类型为 `IPage` 时的数据处理及返回流程。
} else if (type.equals(List.class)) {
// TODO 3. 存在,返回商户信息
// 如果期望返回的数据类型是List直接使用JSONUtil的toList方法将获取到的JSON字符串转换为指定类型voClass的列表然后将其强制转换为泛型要求的类型R并返回这样就将缓存中存储的JSON格式的数据转换为符合业务需求的列表类型数据返回给调用者。
List<T> gradeVOList = JSONUtil.toList(json, voClass);
return (R)gradeVOList;
}else{
// 调用 `JSONUtil` 的 `toList` 方法,传入获取到的 JSON 字符串(`json`)以及指定的类型信息 `voClass`(用于明确列表中元素的具体类型),这个方法会将 JSON 字符串中的数据按照 `voClass` 指定的类型进行反序列化转换,最终得到一个 `List<T>` 类型的列表对象 `gradeVOList`,这个列表就是从缓存中获取并转换后的符合业务需求的列表类型数据,可用于后续直接返回给调用者进行相应的业务操作,比如展示商户信息列表等情况。
return (R) gradeVOList;
// 由于期望返回的类型是由泛型参数 `R` 指定的类型,而在这里得到的是 `List<T>` 类型的 `gradeVOList`,所以需要将 `gradeVOList` 强制转换为 `R` 类型后再返回给调用者,同样这里要求 `R` 类型在实际调用时必须是与 `List<T>` 类型兼容(比如 `R` 就是 `List<T>` 或者是它的父类等情况),否则会出现类型转换异常,通过这样的返回操作,将符合业务要求的列表类型数据返回给调用者,完成在缓存命中且期望返回类型为 `List` 时的数据处理及返回流程。
} else {
// 如果是其他普通的Java对象类型非IPage和List类型则使用JSONUtil的toBean方法将JSON字符串直接反序列化为指定类型type的Java对象然后返回该对象实现从缓存中的JSON数据到Java对象的转换方便后续在业务逻辑中使用该对象进行操作。
return JSONUtil.toBean(json, type);
// 调用 `JSONUtil` 的 `toBean` 方法,传入获取到的 JSON 字符串(`json`)以及期望的业务数据类型(通过泛型参数 `type` 指定,传入对应的 `Class` 对象),这个方法会根据 JSON 字符串的内容以及指定的目标类型结构,将 JSON 数据反序列化为符合 `type` 指定类型的 Java 对象,并直接将其返回给调用者,使得调用者可以获取到符合业务要求的具体业务对象,方便在后续的业务逻辑中使用该对象进行诸如展示、修改、关联其他业务操作等各种操作,完成在缓存命中且期望返回的是普通 Java 对象类型时的数据处理及返回流程。
}
}
// TODO 多判断一步,命中的是否是空值
// 运行到这里说明上面的if没有进去->说明StrUtil.isNotBlank(shopJson)是false ->shopJson两种情况 空白字符串或者null
if (json != null) {
if (json!= null) {
// 不能等于null就一定是一个空字符串
// 如果获取到的json字符串不是null而是一个空字符串说明之前缓存中存储的就是一个空值可能是数据库中对应数据不存在等原因导致写入了空值到缓存此时按照业务逻辑直接返回null表示没有有效的数据可返回给调用者。
return null;
// 当判断获取到的 `json` 字符串不为 `null` 但却是空字符串时,意味着之前缓存中存储的就是一个空值情况,这可能是由于之前在某些情况下(比如首次查询数据时数据库中对应的数据不存在,然后将空值写入了缓存)导致的。按照业务逻辑,在这种情况下直接返回 `null` 给调用者,表示当前没有有效的数据可以提供给调用者使用,调用者可以根据这个返回值在其业务代码中采取相应的措施,例如给用户展示相应的提示信息告知没有找到对应的数据等操作。
}
// TODO 4. 不存在,向数据库进行查询
// 如果缓存中没有命中即获取到的json为null则调用传入的dbFallback函数式接口实现的方法通常是一个Lambda表达式或者具体的方法引用用于从数据库中获取对应的数据传入对应的标识id来查询数据库获取数据将数据库查询的结果存储在变量r中这里体现了缓存穿透的解决思路即当缓存中不存在数据时通过查询数据库来获取数据避免直接返回空值给调用者保证数据的完整性。
R r = dbFallback.apply(id);
// TODO 5. 数据库不存在,返回错误
if (r == null) {
// 将空值写入redis
// 如果从数据库中查询到的数据也是null说明数据库中也不存在对应的数据此时将一个空字符串写入Redis设置一个较短的过期时间如2分钟避免长期占用缓存空间且后续如果数据库有数据更新了可以及时更新缓存这样下次再查询时就不会一直穿透到数据库去查询而是直接从缓存中获取到这个空值返回然后返回null给调用者表示没有获取到有效的数据。
stringRedisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// TODO 6. 存在,写入Redis
// 如果从数据库中查询到了有效数据r不为null先将数据转换为JSON字符串格式以便能够存入Redis中同样是利用JSONUtil的toJsonStr方法进行序列化操作准备将数据存入缓存实现数据的缓存更新方便下次查询时可以直接从缓存中获取数据提高查询性能。
String shopTOJson = JSONUtil.toJsonStr(r);
stringRedisTemplate.opsForValue().
set(key, shopTOJson, time+random.nextInt(20), unit);
set(key, shopTOJson, time + random.nextInt(20), unit);
// TODO 7. 返回最终结果
// 最后将从数据库中获取到的数据(已经序列化并存入缓存了)直接返回给调用者,完成整个查询并缓存数据的流程,使得调用者可以获取到期望的数据进行后续的业务操作。
return r;
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
;
// 创建一个固定线程数量为10的线程池ExecutorService用于后续在缓存击穿处理中当缓存过期需要重建缓存时将缓存重建的任务提交到这个线程池中执行通过使用线程池可以更好地管理线程资源避免频繁创建和销毁线程带来的性能开销并且可以控制并发线程的数量保证系统的稳定性和资源的合理利用这里的缓存重建任务通常是指从数据库中重新获取数据并更新到缓存中的操作通过异步执行可以减少对主线程的阻塞提高系统的响应性能。
// 方法4解决缓存击穿
public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
// 定义了一个泛型方法 `queryWithLogicalExpire`用于从缓存Redis中查询数据并处理逻辑过期的情况。
// `<R, ID>` 表示这是一个泛型方法,其中 `R` 是期望返回的数据类型参数,由调用者根据实际业务需求指定具体类型,比如可能是某个实体类类型,表示要获取的具体业务对象类型;`ID` 则是用于标识要查询数据的唯一标识的类型参数,同样根据业务场景而定,例如可能是整数类型作为数据库记录的主键等,通过这两个泛型参数使得方法可以灵活适应不同类型数据的查询需求。
// `keyPrefix` 参数是一个字符串类型,用于构建缓存数据在 Redis 中的键的前缀部分,通常根据业务模块、数据分类等因素来设置不同的前缀,方便对缓存数据进行分类管理以及准确地定位到特定范围的缓存项,后续会和具体的标识 `id` 拼接成完整的 Redis 键来查找对应的缓存数据。
// `id` 参数就是前面提到的用于唯一标识要查询数据的标识信息,其类型由泛型参数 `ID` 确定,通过它与 `keyPrefix` 拼接能生成准确的 Redis 键,指向特定的缓存数据记录。
// `type` 参数是 `Class<R>` 类型,用于明确告知方法期望返回的数据最终应该反序列化为什么具体的 Java 类型(通过传入对应的 `Class` 对象,例如 `User.class` 表示要返回的是 `User` 类型的数据),以便方法内部根据这个类型信息进行正确的反序列化操作,将从缓存中获取到的数据转换为符合业务要求的对象类型返回给调用者。
// `dbFallback` 参数是一个函数式接口 `Function<ID, R>` 类型,它代表了一个接受 `ID` 类型参数并返回 `R` 类型结果的函数,在这里用于在缓存未命中或者缓存数据过期等情况下,从数据库中获取对应的数据的逻辑实现,通常可以通过 Lambda 表达式或者方法引用来传递具体的从数据库查询数据的实现方法,体现了缓存与数据库交互的一种策略,即当缓存无法提供有效数据时,依靠数据库来获取数据,保证数据的可用性。
// `time` 参数是 `Long` 类型,用于指定缓存数据的过期时间相关设置,具体的含义和使用方式取决于后续的业务逻辑以及与 `TimeUnit` 参数配合来确定缓存数据在多长时间后应该被视为过期(可能是逻辑过期的时间设置等情况),以便对缓存的时效性进行合理控制。
// `TimeUnit` 参数是 `TimeUnit` 类型,它是 Java 中用于表示时间单位的枚举类型(如 `TimeUnit.SECONDS` 表示秒、`TimeUnit.MINUTES` 表示分钟等),与 `time` 参数配合使用,精确地确定缓存数据的过期时长设置,使得缓存过期时间的设置更符合业务场景的实际需求,可根据数据的特性、业务操作频率等因素来选择合适的时间单位和时长进行配置。
String key = keyPrefix + id;
// 根据传入的键前缀(`keyPrefix`)和具体的标识(`id`)拼接成完整的 Redis 键(`key`),用于后续在 Redis 中查找对应的缓存数据。这种通过前缀和具体标识组合成键的方式,方便对不同类型、不同标识的数据进行统一的缓存管理,依据键的规则可以清晰地区分不同的缓存项,确保能够准确地从 Redis 众多缓存数据中定位到目标数据,这是与 Redis 进行数据交互操作的基础步骤,后续的获取缓存、判断缓存状态等操作都依赖这个正确生成的键。
// TODO 1.从redis查询商铺缓存
// 使用StringRedisTemplate的opsForValue操作对象的get方法根据生成的键key从Redis中获取缓存数据以JSON字符串形式存储的之前通过设置逻辑过期的方式存入的数据如果缓存中不存在对应的数据则返回null这里首先尝试从缓存中获取数据以便后续判断缓存是否命中以及是否过期等情况进而采取相应的处理措施。
String json = stringRedisTemplate.opsForValue().get(key);
// 通过之前注入的 `stringRedisTemplate` 对象(它是 Spring Data Redis 提供的用于操作 Redis 的模板类)的 `opsForValue()` 方法获取操作 Redis 字符串值的相关操作对象,然后调用其 `get` 方法,传入前面生成的完整 Redis 键(`key`),尝试从 Redis 中获取对应的缓存数据。
// 由于之前存入 Redis 时(在使用逻辑过期策略存储数据的相关方法中)是将数据转换为 JSON 字符串后存入的,所以这里获取到的数据(如果存在的话)也会是以 JSON 字符串的形式返回,若 Redis 中不存在该键对应的缓存数据,则 `get` 方法会返回 `null`,这个返回结果后续会用于判断缓存是否命中,进而决定下一步的操作逻辑,比如是直接返回数据、进行缓存重建还是从数据库获取数据等不同的处理方式。
// TODO 2. 判断时Redis是否命中
if (StrUtil.isBlank(json)) {
// 使用 `StrUtil` 工具类(假设是 `Hutool` 工具库中的字符串工具类,用于方便地进行字符串相关操作和判断)的 `isBlank` 方法来判断获取到的 JSON 字符串(`json`)是否为空。
// `isBlank` 方法会判断字符串是否为 `null`、空字符串(长度为 0或者只包含空白字符如空格、制表符等如果满足这些情况之一就认为字符串是空白的也就是缓存未命中没有获取到有效的缓存数据此时按照业务逻辑进入下面的处理流程。
// TODO 3.缓存不存在,直接返回空
// 如果获取到的JSON字符串为空即缓存未命中按照业务逻辑直接返回null表示没有从缓存中获取到有效的数据调用者可以根据这个返回值进行相应的处理比如提示用户数据不存在等情况
return null;
// 当判断缓存未命中(`json` 为空)时,直接返回 `null` 给调用者,这符合业务逻辑中对于缓存不存在情况的处理方式,调用者可以根据这个返回值在其业务代码中采取相应的措施,例如给用户展示相应的提示信息,告知用户请求的数据暂时不存在或者需要重新加载等操作,同时也避免了在缓存未命中情况下进行不必要的后续复杂操作,简化了处理流程。
}
// TODO 4.存在,需要先把JSON反序列化为对象
// 使用JSONUtil的toBean方法将获取到的JSON字符串反序列化为RedisData对象因为之前存入Redis时是将包含业务数据和逻辑过期时间等信息的RedisData对象转换为JSON字符串后存入的所以这里需要先反序列化还原出RedisData对象以便后续获取其中的业务数据以及判断逻辑过期时间等操作从缓存数据中提取出关键的信息用于后续的逻辑处理。
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
// 调用 `JSONUtil` 工具类(假设使用了相关的 JSON 处理工具库,用于方便地进行 JSON 数据与 Java 对象之间的转换操作)的 `toBean` 方法,传入获取到的 JSON 字符串(`json`)以及 `RedisData.class`(表示要将 JSON 字符串反序列化为 `RedisData` 类型的对象),这个方法会根据 JSON 字符串的内容以及 `RedisData` 类的结构定义,将 JSON 数据转换为对应的 `RedisData` 类型的 Java 对象,并将其赋值给 `redisData` 变量。
// 因为之前在将数据存入 Redis 时(在相关的设置缓存逻辑过期的方法中),是把包含业务数据和逻辑过期时间等信息的 `RedisData` 对象先转换为 JSON 字符串后再存入的,所以现在从 Redis 获取到数据JSON 字符串形式)后,需要通过这个反序列化操作还原出 `RedisData` 对象,这样才能进一步获取其中封装的业务数据以及逻辑过期时间等关键信息,为后续判断缓存数据是否过期以及提取正确的业务数据返回给调用者等操作做好准备。
// 因为我们在RedisData中设置data属性就是Object类型所以当我们取的时候程序并不知道我们是什么类型我们加一个强转就好了
// 从RedisData对象中获取存储业务数据的data属性由于其类型为Object在实际使用时需要将其强制转换为JSONObject类型这里假设业务数据在存入时是以JSONObject形式封装在RedisData中的方便操作其中的键值对数据以便后续能准确地将其转换为期望的业务数据类型type这里体现了在处理通用数据结构时需要根据实际存储和使用的情况进行类型的转换和适配操作。
JSONObject shopData = (JSONObject) redisData.getData();
// 从前面反序列化得到的 `RedisData` 对象中获取 `data` 属性,由于在 `RedisData` 类的定义中,`data` 属性的类型被设置为 `Object`(这样设计是为了能够存储各种不同类型的业务数据,具有通用性),所以从这个属性获取到的值在程序中默认被当作 `Object` 类型对待。
// 但在实际业务操作中,为了方便后续准确地将其转换为期望的业务数据类型(由泛型参数 `type` 指定的类型),这里假设业务数据在存入时是以 `JSONObject` 形式(一种方便操作键值对数据的 JSON 数据结构表示形式,类似 Java 中的 `Map`,便于获取和设置具体的属性值)封装在 `RedisData` 中的,所以需要将获取到的 `Object` 类型的 `data` 属性值强制转换为 `JSONObject` 类型,并将其赋值给 `shopData` 变量,通过这样的类型转换和适配操作,才能更好地利用其中存储的业务数据进行后续的处理,比如提取具体的业务属性值等操作。
R r = JSONUtil.toBean(shopData, type);
// 调用 `JSONUtil` 的 `toBean` 方法,将前面转换得到的 `JSONObject` 类型的 `shopData`(其中包含了实际的业务数据信息)以及期望的业务数据类型(通过泛型参数 `type` 指定,传入对应的 `Class` 对象)作为参数传入,这个方法会根据 `JSONObject` 中的数据内容以及指定的目标类型结构,将 `shopData` 中的数据反序列化为符合 `type` 指定类型的 Java 对象,并将其赋值给变量 `r`,这样就得到了最终可以在业务中使用的具体业务数据对象,方便后续根据缓存是否过期等情况来决定是直接返回该数据还是进行其他相关操作(如缓存重建后再返回等)。
LocalDateTime expireTime = redisData.getExpireTime();
// 从前面反序列化得到的 `RedisData` 对象中获取 `expireTime` 属性,这个属性存储的是缓存数据的逻辑过期时间(在之前将数据存入 Redis 时,通过设置逻辑过期的相关操作设置了这个时间,它是一个 `LocalDateTime` 类型的时间点,表示缓存数据在逻辑上应该过期的时间),将其赋值给 `expireTime` 变量,以便后续通过与当前时间进行比较,来判断缓存数据是否已经过期,进而决定相应的业务处理逻辑,例如是直接返回数据还是进行缓存重建等操作。
// TODO 5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
// 使用获取到的缓存数据的逻辑过期时间(`expireTime`)与当前时间(通过 `LocalDateTime.now()` 获取当前系统的时间点)进行比较,调用 `isAfter` 方法判断逻辑过期时间是否在当前时间之后。
// 如果 `expireTime` 所表示的时间点在当前时间之后,意味着缓存数据的逻辑过期时间还未到达,即缓存数据还未过期,仍然有效,此时按照业务逻辑进入下面的处理流程,直接将之前反序列化得到的业务数据(`r`)返回给调用者,使得调用者可以使用缓存中的有效数据进行后续业务操作,避免了不必要的数据库查询操作,提高了查询性能。
// TODO 5.1 未过期,返回商铺信息
// 如果缓存数据的逻辑过期时间expireTime在当前时间之后说明缓存数据还未过期仍然有效直接将之前反序列化得到的业务数据r返回给调用者使得调用者可以使用缓存中的有效数据进行后续业务操作避免了不必要的数据库查询操作提高了查询性能。
return r;
// 当判断缓存数据未过期(`expireTime` 在当前时间之后)时,直接将前面已经反序列化得到的符合业务要求的业务数据对象(`r`)返回给调用者,这样调用者就可以使用这个有效的缓存数据在其业务逻辑中进行后续的操作,比如展示数据、进行业务计算等。
// 通过直接返回缓存中的有效数据,避免了再次去数据库中查询相同数据的操作,减少了数据库的访问压力,同时提高了整个数据查询的性能,充分发挥了缓存的作用,使得系统在处理频繁查询相同数据的场景下能够更高效地运行。
}
// TODO 5.2 已过期,需要缓存重建
// 当判断缓存数据已经逻辑过期后,需要进行缓存重建操作,即从数据库中重新获取最新的数据并更新到缓存中,以保证缓存数据的有效性和及时性,同时要考虑并发情况下的缓存一致性等问题,避免多个线程同时重建缓存造成的数据不一致或者资源浪费等情况,下面的步骤就是围绕缓存重建这个过程展开的相关操作。
// TODO 6. 缓存重建
// TODO 6.1 获取互斥锁
// 构建一个用于表示互斥锁的键lockKey其格式通常是固定的这里采用"lock:shop:"加上具体的标识id来构建方便区分不同数据对应的锁避免锁的冲突通过调用tryLock方法尝试获取这个互斥锁
String lockKey = "lock:shop:" + id;
// 构建一个用于表示互斥锁的键lockKey其格式通常是固定的这里采用"lock:shop:"加上具体的标识id来构建方便区分不同数据对应的锁避免锁的冲突通过这个唯一的键来在Redis中标识和操作对应的互斥锁后续基于这个键来进行获取锁、释放锁等操作以实现对缓存重建过程的并发控制保证在同一时刻只有一个线程能够进行缓存重建操作避免多个线程同时重建缓存造成的数据不一致或者资源浪费等情况。
boolean isLock = tryLock(lockKey);
// 调用tryLock方法尝试获取这个互斥锁tryLock方法会与Redis进行交互尝试在Redis中设置一个具有特定键这里就是刚刚构建的lockKey的锁返回一个布尔值表示是否成功获取到锁获取到锁意味着当前线程获得了对缓存重建操作的独占权限后续可以安全地进行缓存重建相关的操作若获取锁失败则说明可能已经有其他线程正在进行缓存重建当前线程需要等待或者采取其他相应的处理策略通过这种方式利用锁机制来协调多个线程对缓存重建的并发访问。
// TODO 6.2 判断是否获取锁成功
// TODO 6.2 判断是否获取锁成功
if (isLock) {
// TODO 6.3 成功获取锁成功应该再次检测Redis缓存是否过期做DoubleCheck如果存在则无序重建缓存
// 如果成功获取到锁isLock为true说明当前线程获得了进行缓存重建的权限接下来需要再次检测Redis缓存是否过期这一步骤被称为“DoubleCheck”双重检查主要目的是防止在获取锁的短暂时间间隔内其他线程已经完成了缓存重建并更新了缓存使得当前线程无需再重复进行缓存重建操作进一步优化性能并保证缓存数据的准确性和一致性。
// 再次从Redis中获取缓存数据对应的JSON字符串这里的键"cache:shop:" + id可能是用于标识该缓存数据在Redis中的存储位置的另一种形式具体取决于项目中的缓存键设计规范通过重新获取数据来进行后续的过期判断等操作确保基于最新的缓存状态来决定是否真正需要进行缓存重建工作。
json = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
// 将获取到的JSON字符串反序列化为RedisData对象与之前从缓存中获取数据并反序列化的操作类似目的是提取出其中包含的业务数据以及逻辑过期时间等关键信息以便后续判断缓存是否过期以及获取正确的业务数据进行返回等操作。
redisData = JSONUtil.toBean(json, RedisData.class);
// 从RedisData对象中获取存储业务数据的data属性并强制转换为JSONObject类型方便后续准确地将其转换为期望的业务数据类型type进行相应的业务操作或者判断操作这是对缓存中存储的数据结构进行适配和提取有效信息的必要步骤。
shopData = (JSONObject) redisData.getData();
// 将从JSONObject中提取出的数据反序列化为期望的业务数据类型type得到最终可以在业务中使用的对象方便后续根据缓存是否过期等情况来决定是直接返回该数据还是进行缓存重建后再返回等操作。
r = JSONUtil.toBean(shopData, type);
expireTime = redisData.getExpireTime();
if (expireTime.isAfter(LocalDateTime.now())) {
// TODO 未过期,返回商铺信息
// 如果缓存数据的逻辑过期时间expireTime在当前时间之后说明在获取锁后再次检查发现缓存数据仍然未过期仍然有效直接将之前反序列化得到的业务数据r返回给调用者使得调用者可以使用缓存中的有效数据进行后续业务操作避免了不必要的数据库查询和缓存重建操作提高了查询性能同时也保证了数据的及时性和准确性。
return r;
}
// TODO 成功,但是缓存过期了,开启独立线程,实现缓存重建(建议使用线程池)
// 当经过双重检查确认缓存确实已经过期后将缓存重建的任务提交到之前创建的固定线程数量为10的线程池CACHE_REBUILD_EXECUTOR中执行通过使用线程池可以实现异步操作避免缓存重建过程阻塞主线程提高系统的并发处理能力和响应性能。
// 这里使用Lambda表达式创建一个Runnable任务在任务内部实现缓存重建的具体逻辑即先从数据库中查询最新的数据再将带有逻辑过期时间设置的数据写入到Redis缓存中完成缓存的更新操作同时在任务执行完毕后无论是否出现异常都需要通过调用unlock方法来释放之前获取的互斥锁以允许其他等待的线程有机会获取锁并进行缓存相关的操作保证锁资源的合理使用和并发控制的有效性。
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// TODO 重建缓存 先查数据库再写入Redis
// 调用传入的dbFallback函数式接口实现的方法通常是一个Lambda表达式或者具体的方法引用用于从数据库中获取对应的数据传入对应的标识id来查询数据库获取最新的数据将数据库查询的结果存储在变量r1中这一步是获取最新的业务数据为后续更新缓存做准备体现了缓存重建时从数据源数据库获取最新数据的关键操作。
R r1 = dbFallback.apply(id);
// TODO 写入缓存要带有逻辑过期
this.setWithLogicalExpire(key, r1, time+random.nextInt(20), unit);
// 调用本类的setWithLogicalExpire方法将从数据库中获取到的最新数据r1按照带有逻辑过期时间的方式写入到Redis缓存中其中设置的过期时间是在传入的固定过期时间time基础上加上一个随机时长random.nextInt(20)),这样做可以进一步分散缓存过期时间,避免大量缓存同时过期引发缓存雪崩等问题,实现更合理、更稳定的缓存更新操作,保证缓存系统的可靠性和数据的时效性。
this.setWithLogicalExpire(key, r1, time + random.nextInt(20), unit);
} catch (Exception e) {
// 如果在缓存重建过程包括数据库查询或者写入Redis操作中出现异常将异常包装在一个RuntimeException中抛出这样可以使得异常能够在合适的地方被捕获和处理例如在调用queryWithLogicalExpire方法的上层业务逻辑中可以根据具体情况进行日志记录、返回错误信息给客户端等操作保证系统在出现异常情况时也能有相应的处理机制维持一定的稳定性。
throw new RuntimeException(e);
} finally {
// 释放锁
// 在缓存重建任务执行完毕后无论是否出现异常都需要调用unlock方法来释放之前获取的互斥锁lockKey对应的锁使得其他等待获取该锁的线程能够有机会获取锁并进行相应的缓存操作保证锁资源的合理循环使用维持并发环境下缓存操作的有序性和正确性。
unlock(lockKey);
}
});
}
// TODO 6.4 失败,返回已经过期的商品信息
// TODO 6.4 失败,返回已经过期的商品信息
// 如果获取锁失败isLock为false说明当前线程没有获得缓存重建的权限可能已经有其他线程正在进行缓存重建操作此时直接将之前已经获取到的虽然已经过期业务数据r返回给调用者这样调用者可以先使用这个过期的数据进行一些展示或者其他不依赖最新数据的操作具体根据业务需求而定同时也避免了当前线程长时间等待或者重复尝试获取锁等不必要的操作在一定程度上保证了系统的响应性能和可用性。
return r;
}
// 拿到锁
private boolean tryLock(String key) {
// setIfAbsent方法就是Redis中的setnx
// 在Redis命令行中的运行结果就是0或者1但是在这的运行结果是true或false但是返回的是Boolean类型封装类
// 解释了Java中通过StringRedisTemplate操作Redis的opsForValue().setIfAbsent方法与Redis原生的SETNX命令是等效的功能SETNX命令SET if Not eXists用于在Redis中设置一个键值对但只有当键不存在时才会设置成功如果键已经存在则设置操作不会执行返回结果用于表示设置操作是否成功。
// 在Redis命令行中的运行结果就是0或者10表示设置失败因为键已存在1表示设置成功但是在Java代码中通过Spring Data Redis操作Redis时其返回的是Boolean类型true表示设置成功false表示设置失败这里的Boolean类型是对底层Redis命令返回结果的一种封装方便在Java代码中进行逻辑判断和处理。
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
// 不建议直接返回:会自动拆箱,有时候会出现空指针
// 如果直接返回flag即直接返回stringRedisTemplate.opsForValue().setIfAbsent的返回结果在Java中会进行自动拆箱操作将Boolean对象转换为基本数据类型boolean当flag为null时例如Redis连接出现问题等异常情况导致返回结果无法正常获取时自动拆箱就会抛出空指针异常所以这里通过调用BooleanUtil.isTrue方法来进行判断并返回避免了自动拆箱可能带来的空指针问题提高代码的健壮性确保返回的结果是经过合理判断和处理后的正确布尔值表示是否成功获取到锁。
return BooleanUtil.isTrue(flag);
}
// 释放锁
private void unlock(String key) {
// 通过调用StringRedisTemplate的delete方法根据传入的键key来删除Redis中对应的锁记录实现释放锁的操作使得其他线程后续可以尝试获取该锁进行相应的缓存操作完成对互斥锁资源在Redis中的删除和释放保证锁资源的正确管理和循环使用维持并发环境下缓存操作的正常秩序。
stringRedisTemplate.delete(key);
}
// public Map<Integer, GradeVO> batchGet(List<Integer> gradeIds, Class<GradeVO> gradeVOClass) {
//
// }
// public Map<Integer, GradeVO> batchGet(List<Integer> gradeIds, Class<GradeVO> gradeVOClass) {
//
// }
/**
* Redis
* RediskeysList<Integer>RedisclazzMapIntegerclazzMap便
* @param keys keyIntegerRediskeyString使
* @param clazz
* @return Map
*/
public <T> Map<Integer, T> batchGet(String prefix, List<Integer> keys, Class<T> clazz) {
// 定义了一个泛型方法 `batchGet`,用于从 Redis 中批量获取缓存对象。
// `<T>` 表示这是一个泛型方法,`T` 是一个类型参数,代表要获取的缓存对象的具体类型,这个类型由调用者在调用该方法时指定,使得方法可以灵活地返回不同类型的缓存对象集合,以适应各种业务场景下对不同类型缓存数据的获取需求。
// `prefix` 参数是一个字符串类型,用于给要获取的 Redis 键添加一个统一的前缀。在实际应用中,可能会根据不同的业务模块、数据分类等设置不同的前缀,通过传入这个前缀,可以准确地定位到属于特定范围的一批缓存键,便于从 Redis 中筛选出期望的缓存数据,增强了缓存管理的灵活性和可区分性。
// `keys` 参数是一个 `List<Integer>` 类型,代表了一组整数类型的键,这些键是用于在 Redis 中标识要获取的缓存数据的标识信息,但由于 Redis 的键要求是字符串类型,所以后续需要对这些整数键进行类型转换处理,使其符合 Redis 的键格式要求,这个列表包含了所有需要批量获取缓存数据对应的键信息,通过遍历这个列表来逐个获取对应的缓存对象。
// `clazz` 参数是 `Class<T>` 类型,它用于指定要获取的缓存对象最终反序列化后的具体类型,通过传入对应的类型信息(例如 `User.class` 表示要获取的缓存对象是 `User` 类型),方法内部就能根据这个类型信息将从 Redis 中获取到的缓存数据(通常是以 JSON 字符串形式存储的)准确地反序列化为期望的 Java 对象类型,方便后续在业务逻辑中直接使用这些对象进行相应的操作,确保获取到的数据类型符合业务要求。
try {
// 使用 `try-catch` 语句块来捕获在批量获取缓存数据过程中可能出现的异常情况,将可能出现异常的代码逻辑放在 `try` 块中执行,一旦发生异常,就会被 `catch` 块捕获并进行相应的处理,这样可以保证程序在遇到异常时不会意外终止,而是按照预设的异常处理逻辑进行应对,维持系统的稳定性,避免因个别异常导致整个系统崩溃或者出现不可预期的行为,提高系统的健壮性。
// 将Integer类型的keys转换为String类型并加上前缀因为Redis的key是字符串
// 使用Java 8的Stream API对传入的整数类型的键列表keys进行操作通过map方法将每个整数键转换为带有指定前缀prefix的字符串形式然后使用collect方法将转换后的字符串键收集到一个新的List<String>中这样就完成了将整数键转换为符合Redis键要求的字符串键的操作方便后续基于这些字符串键从Redis中批量获取缓存数据。
List<String> stringKeys = keys.stream()
.map(key -> prefix + key.toString())
.collect(Collectors.toList());
// 首先调用 `keys` 列表的 `stream()` 方法,将 `List<Integer>` 类型的键列表转换为一个 `Stream<Integer>` 类型的流对象,这样就可以使用流的各种操作方法来处理数据。
// 接着使用 `map` 操作,它会对流中的每个元素(即每个整数键)应用一个给定的函数(这里是通过 Lambda 表达式 `key -> prefix + key.toString()` 表示的函数,其作用是将每个整数键 `key` 转换为带有指定前缀 `prefix` 的字符串形式),实现对每个整数键的类型转换和前缀添加操作,得到一个新的 `Stream<String>` 类型的流,其中每个元素都是符合 Redis 键要求的字符串键形式。
// 最后使用 `collect` 方法结合 `Collectors.toList()` 收集器,将经过 `map` 操作后的流中的所有字符串键元素收集起来,转换为一个 `List<String>` 类型的列表对象 `stringKeys`,这个列表就包含了所有准备好用于从 Redis 中批量获取缓存数据的字符串键,后续就可以基于这些键来与 Redis 进行交互获取对应的缓存内容。
// 执行批量获取操作
// 使用StringRedisTemplate的opsForValue操作对象的multiGet方法传入转换后的字符串键列表stringKeys一次性从Redis中获取多个键对应的缓存数据返回一个List<String>类型的结果列表其中每个元素对应一个键的缓存数据如果键不存在则对应位置为null通过这种批量获取的方式可以减少与Redis的交互次数提高获取缓存数据的效率尤其在需要获取多个缓存数据的场景下性能优势明显。
List<String> values = stringRedisTemplate.opsForValue().multiGet(stringKeys);
// 通过之前注入的 `stringRedisTemplate` 对象(它是 Spring Data Redis 提供的用于操作 Redis 的模板类)的 `opsForValue()` 方法获取操作 Redis 字符串值的相关操作对象,然后调用其 `multiGet` 方法,这个方法用于一次性获取多个键对应的缓存数据。
// 将前面生成的包含所有字符串键的 `stringKeys` 列表作为参数传入 `multiGet` 方法Redis 会根据这些键去查找对应的缓存数据,返回一个 `List<String>` 类型的结果列表 `values`,列表中的每个元素对应着传入的键列表中相应位置键所对应的缓存数据,如果某个键在 Redis 中不存在对应的缓存数据,那么在 `values` 列表中对应位置就会是 `null`,通过这种批量获取的方式,相比逐个获取键对应的缓存数据,可以显著减少与 Redis 的交互次数,降低网络开销等,在需要获取多个缓存数据的场景下能有效提高获取数据的效率,提升系统的整体性能。
Map<Integer, T> resultMap = new HashMap<>();
// 创建一个 `HashMap` 类型的空映射对象 `resultMap`,用于存储最终从 Redis 中批量获取到的缓存数据,键的类型为 `Integer`(对应原始传入的整数类型的键),值的类型为泛型 `T`(即前面通过 `clazz` 参数指定的要获取的缓存对象的类型),通过后续的操作,会将从 Redis 中获取到并反序列化后的缓存对象按照键值对的形式存入这个映射中,最终返回给调用者,方便调用者根据原始的整数键来获取对应的缓存对象进行后续的业务操作。
for (int i = 0; i < stringKeys.size(); i++) {
// 开始一个循环,循环次数由 `stringKeys` 列表的大小决定(也就是前面转换后的字符串键的数量),通过遍历这个列表的索引,依次处理每个键对应的缓存数据,确保能对所有批量获取到的缓存数据进行相应的检查和处理,将有效的缓存数据存入结果映射中,完成批量获取缓存数据并整理的操作流程。
String value = values.get(i);
if (value != null) {
// 反序列化字符串为对象
// 从前面通过 `multiGet` 方法获取到的缓存数据列表 `values` 中,根据当前循环的索引 `i` 获取对应位置的缓存数据(以字符串形式存在,如果对应键在 Redis 中不存在缓存数据,则此处获取到的就是 `null`),将其存储在变量 `value` 中,方便后续判断该缓存数据是否存在以及进行反序列化等操作。
if (value!= null) {
// 如果获取到的某个缓存数据value不为null说明Redis中存在对应键的缓存数据此时调用deserialize方法本类中自定义的用于将JSON字符串反序列化为指定类型对象的方法将缓存数据的JSON字符串value反序列化为期望的类型clazz指定的类型的对象以便后续可以在业务逻辑中直接使用该对象进行操作。
T deserializedObject = deserialize(value, clazz);
// 调用 `deserialize` 方法,传入当前获取到的非空的缓存数据字符串 `value` 和指定的类型信息 `clazz`,这个 `deserialize` 方法(其具体实现应该是根据所采用的序列化/反序列化机制,例如使用 JSON 相关库来将 JSON 字符串转换为指定类型的 Java 对象)会将缓存数据的 JSON 字符串反序列化为符合 `clazz` 指定类型的 Java 对象,并将其存储在变量 `deserializedObject` 中,这个对象就是从 Redis 中获取并成功反序列化后的缓存对象,后续可以将其存入结果映射中用于返回给调用者使用。
// 将反序列化后的对象放入结果Map中键为原始传入的整数类型的键keys.get(i)值为反序列化后的对象这样就构建好了一个包含从Redis中批量获取到的缓存数据的键值对Map方便后续根据键来获取对应的缓存对象进行业务处理。
resultMap.put(keys.get(i), deserializedObject);
// 将前面反序列化得到的缓存对象 `deserializedObject` 存入 `resultMap` 中,键使用原始传入的整数类型的键列表 `keys` 中对应位置的键(通过 `keys.get(i)` 获取),这样就构建好了一个键值对,将从 Redis 中获取到的缓存数据以符合业务要求的键值对形式整理到 `resultMap` 中,随着循环的进行,所有有效的缓存数据都会依次存入这个映射,最终形成一个包含从 Redis 中批量获取到的所有缓存数据的完整映射,方便后续根据原始的整数键来获取对应的缓存对象进行各种业务操作,如展示数据、进行数据处理等。
}
}
return resultMap;
// 在完成对所有批量获取到的缓存数据的处理(将有效的缓存数据反序列化并存入 `resultMap` 中)后,将这个包含了键值对形式的缓存数据的 `resultMap` 返回给调用者,调用者可以根据需要使用这个映射来获取对应的缓存对象,实现了从 Redis 中批量获取缓存数据并转换为合适的业务对象形式返回的功能,满足了批量获取缓存数据用于后续业务操作的业务需求。
} catch (Exception e) {
// 异常处理逻辑,根据需要进行日志记录或抛出异常
// 如果在批量获取缓存数据的过程中出现任何异常例如Redis连接异常、反序列化异常等将异常包装在一个RuntimeException中并抛出同时可以根据具体业务需求在合适的地方比如调用batchGet方法的上层业务逻辑捕获这个异常进行相应的日志记录、返回错误信息给客户端等操作保证系统在出现异常情况时有相应的处理机制维持一定的稳定性和可靠性。
throw new RuntimeException("Failed to batch get from Redis", e);
// 在 `try` 块中如果出现异常,会立即跳转到 `catch` 块中执行这里的代码。将出现的异常 `e` 包装在一个 `RuntimeException` 中,并添加一个自定义的错误信息 `"Failed to batch get from Redis"`,这样做一方面可以在不改变原有异常信息的基础上,添加更明确的业务相关的错误提示,方便后续排查问题时快速定位到是在批量获取 Redis 缓存数据这个环节出现的异常;另一方面,将包装后的异常抛出,使得调用这个 `batchGet` 方法的上层业务逻辑可以捕获到这个异常,然后根据具体业务需求进行相应的处理,比如记录日志、向客户端返回合适的错误信息等操作,通过这种异常处理机制,确保系统在面对各种异常情况时能够有相应的应对措施,维持系统的稳定性和可靠性,避免因异常导致系统出现不可预期的行为或者直接崩溃。
}
}
@ -246,39 +429,77 @@ public class CacheClient {
*/
private static <T> T deserialize(String value, Class<T> clazz) {
// 这里需要根据您的序列化方式来实现例如使用JSON库如Jackson、Gson等
// 指出了当前的反序列化方法只是一个简单的示意,在实际项目中需要根据具体采用的序列化/反序列化库如常用的Jackson、Gson等以及对应的配置和使用方式来准确地实现将JSON字符串反序列化为指定类型对象的功能不同的库有不同的使用方法和特点这里只是简单假设使用了fastjson库进行示意性的反序列化操作。
// 以下仅为示意,实际请使用正确的反序列化逻辑
return JSONUtil.toBean(value, clazz); // 假设使用了fastjson库
// 使用Hutool工具库中的JSONUtil工具类的toBean方法尝试将传入的JSON字符串value反序列化为指定类型clazz的Java对象这是基于假设使用fastjson库来实现反序列化功能的简单示例操作在实际应用中如果使用其他库或者有更复杂的反序列化需求比如包含对象嵌套、特殊数据类型处理等情况则需要相应地调整和完善这个反序列化逻辑确保能够正确地将缓存中的JSON格式数据转换为业务中可用的Java对象。
}
/**
*
* Redis<K><V>RedisexpireTimetimeUnit便
* @param data
* @param <K> String
* @param <V> String
*/
// public <K, V> void batchPut(Map<K, V> data) {
// List<String> allArgs = new ArrayList<>();
// for (Map.Entry<K, V> entry : data.entrySet()) {
// allArgs.add(entry.getKey().toString());
// allArgs.add(JSONUtil.toJsonStr(entry.getValue()));
// }
//
// // 使用MSET命令进行批量插入
// RedisScript<Void> script = new DefaultRedisScript<>("return redis.call('MSET', unpack(ARGV));", Void.class);
// // 将所有参数整合为一个String数组传递给execute方法
// stringRedisTemplate.execute(script, Arrays.asList(""), allArgs.toArray(new String[0]));
// }
// public <K, V> void batchPut(Map<K, V> data) {
// List<String> allArgs = new ArrayList<>();
// for (Map.Entry<K, V> entry : data.entrySet()) {
// allArgs.add(entry.getKey().toString());
// allArgs.add(JSONUtil.toJsonStr(entry.getValue()));
// }
//
// // 使用MSET命令进行批量插入
// RedisScript<Void> script = new DefaultRedisScript<>("return redis.call('MSET', unpack(ARGV));", Void.class);
// // 将所有参数整合为一个String数组传递给execute方法
// stringRedisTemplate.execute(script, Arrays.asList(""), allArgs.toArray(new String[0]));
// }
public <K, V> void batchPut(String prefix, Map<K, V> data, long expireTime, TimeUnit timeUnit) {
// 定义了一个泛型方法 `batchPut`,用于批量将键值对数据放入到 Redis 缓存中。
// `<K, V>` 表示这是支持泛型的方法,其中 `K` 是键的类型参数,`V` 是值的类型参数,意味着可以传入不同类型的键值对映射作为参数,只要它们满足后续代码中的相关要求(例如能够转换为合适的字符串形式用于 Redis 存储等)。
// `prefix` 参数是一个字符串类型,用于给要存入 Redis 的键添加一个统一的前缀,方便在 Redis 中对缓存数据进行分类管理或者区分不同业务模块下的缓存项等,例如可以根据不同功能模块设置不同的前缀,便于后续的查找、清理等操作。
// `data` 参数是一个 `Map<K, V>` 类型,表示要批量存入 Redis 的键值对映射集合,其中键的类型为 `K`,值的类型为 `V`,这个映射集合包含了所有需要缓存的数据内容,通过遍历这个映射来逐个将键值对存入 Redis。
// `expireTime` 参数是一个长整型(`long`),用于指定缓存数据的基础过期时间,即从存入 Redis 开始,经过多长时间后缓存数据应该自动失效,具体的时间单位由 `timeUnit` 参数来明确,这样可以根据业务需求灵活设置缓存的有效期,保证缓存数据的时效性,避免长期占用内存资源同时又能满足一定时间内的重复使用需求。
// `timeUnit` 参数是 `TimeUnit` 类型,它是 Java 中用于表示时间单位的枚举类型(例如可以是秒、分钟、小时、天等),与 `expireTime` 参数配合使用,精确地确定缓存数据的过期时长设置,使得缓存的过期时间设置更加符合业务场景的实际需求,比如可以根据数据的更新频率、重要性等因素来选择合适的时间单位和时长进行设置。
List<String> allArgs = new ArrayList<>();
// 创建一个 `ArrayList` 类型的列表对象 `allArgs`,用于存储后续要传递给 Redis 命令的所有参数。
// 在这里,这些参数主要是由经过处理后的 Redis 键(带有前缀的键)以及对应的值(转换为 JSON 字符串后的缓存值)组成,通过将这些参数收集到一个列表中,方便后续统一传递给相关的 Redis 操作方法,以实现批量插入缓存数据的功能,并且以列表形式组织参数也符合 Redis 某些批量操作命令对参数格式的要求(具体取决于后续实际执行的 Redis 命令实现方式)。
for (Map.Entry<K, V> entry : data.entrySet()) {
// 开始一个循环,遍历传入的 `data` 参数(即 `Map<K, V>` 类型的键值对映射集合)中的每一个键值对元素。
// `Map.Entry<K, V>` 表示键值对映射中的每一个单独的条目(包含一个键和对应的值),`entrySet()` 方法返回由所有这些键值对条目组成的集合,通过遍历这个集合,就能依次获取到每一个键值对,方便后续对每个键值对进行处理,将它们逐个转换为适合 Redis 存储的参数形式并添加到 `allArgs` 列表中,为批量插入缓存数据做准备。
String prefixedKey = prefix + entry.getKey().toString();
// 对于当前遍历到的键值对(`entry`),获取其键(`entry.getKey()`),并将其转换为字符串类型(通过 `toString()` 方法),然后与传入的前缀(`prefix`)进行拼接,得到一个带有前缀的 Redis 键(`prefixedKey`)。
// 这样做的目的是为了给每个键添加统一的前缀标识,符合前面提到的便于在 Redis 中分类管理缓存数据的需求,同时确保生成的键符合 Redis 中键的格式要求Redis 键必须是字符串类型),使得后续可以通过这个完整的键准确地在 Redis 中定位和操作对应的缓存数据。
allArgs.add(prefixedKey);
// 将刚刚生成的带有前缀的 Redis 键(`prefixedKey`)添加到 `allArgs` 列表中,以便后续作为参数传递给 Redis 操作命令,这个键在 Redis 中用于唯一标识要存储的缓存数据,通过不断添加每个键值对对应的键到列表中,逐步构建好批量操作所需的完整参数列表。
allArgs.add(JSONUtil.toJsonStr(entry.getValue()));
// 获取当前遍历到的键值对(`entry`)中的值(`entry.getValue()`),这个值通常是一个 Java 对象(类型为 `V`),由于 Redis 只能存储字符串类型的数据,所以需要使用 `JSONUtil`(假设这里使用的是 `Hutool` 工具库中的 JSON 工具类,用于方便地进行 JSON 相关操作)的 `toJsonStr` 方法将这个 Java 对象转换为 JSON 字符串形式。
// 然后将转换后的 JSON 字符串形式的值添加到 `allArgs` 列表中,与前面添加的键相对应,这样列表中就依次存储了键和对应的值的字符串表示,按照顺序为后续批量插入缓存数据时提供完整且符合要求的参数内容,使得 Redis 能够正确地解析并存储这些键值对数据作为缓存内容。
// 在放置每个值之后立即设置过期时间
// 对于传入的键值对映射data中的每一组键值对先将键转换为带有指定前缀prefix的字符串形式prefixedKey然后将键和对应的值转换后的JSON字符串通过JSONUtil.toJsonStr方法将值序列化为JSON字符串分别添加到一个列表allArgs用于后续在Redis中设置键值对数据。
// 同时使用StringRedisTemplate的opsForValue操作对象的set方法为每个键值对立即设置过期时间设置的过期时间是在传入的固定过期时间expireTime基础上加上一个随机时长1L + random.nextInt(20)),这样做的目的同样是为了
// 在放置每个值之后立即设置过期时间
stringRedisTemplate.opsForValue().set(prefixedKey, JSONUtil.toJsonStr(entry.getValue()), expireTime+(1L+random.nextInt(20)), timeUnit);
// 使用注入的 `stringRedisTemplate` 对象的 `opsForValue()` 方法获取操作字符串值的相关操作对象,然后调用其 `set` 方法向 Redis 中设置键值对数据以及对应的过期时间策略。
// `prefixedKey` 是经过处理后的 Redis 键,它由传入的 `prefix` 和当前遍历到的键值对中的键(通过 `entry.getKey().toString()` 转换为字符串后拼接而成),这个键用于在 Redis 中唯一标识该条缓存数据。
// `JSONUtil.toJsonStr(entry.getValue())` 这部分是利用 `Hutool` 工具库中的 `JSONUtil` 工具类,将当前键值对中的值(其类型为泛型 `V`,通常是一个 Java 对象)转换为 JSON 字符串格式,因为 Redis 存储数据时一般要求数据以字符串形式存储,所以需要先将对象进行序列化操作,使其能正确存入 Redis 中。
// `expireTime+(1L+random.nextInt(20))` 这里是在设置缓存的过期时间,`expireTime` 是传入的基础过期时长,在此基础上加上一个随机的时长(`1L + random.nextInt(20)`,其中 `1L` 表示长整型的数字 `1`,确保后续加法运算结果为长整型,`random.nextInt(20)` 会生成一个 `0` 到 `19` 的随机整数,两者相加后就得到了一个在 `expireTime` 基础上增加了一定随机范围的新的过期时长)。添加随机时长的目的是为了避免大量缓存同时过期引发缓存雪崩问题,通过让缓存过期时间更加分散,提高缓存系统的稳定性和可靠性,使缓存过期的时间分布更加均匀,减轻 Redis 在某一时刻集中处理大量缓存过期的压力。
// `timeUnit` 参数则明确了前面设置的过期时长所使用的时间单位(例如秒、分钟、小时等),它与 `expireTime` 以及随机增加的时长共同确定了该条缓存数据在 Redis 中具体的过期时间设置,保证缓存数据在合适的时间后自动失效,既能满足数据时效性要求,又能合理利用内存资源,避免数据长期占用内存空间。
}
}
// 这个大括号是 `public <K, V> void batchPut(String prefix, Map<K, V> data, long expireTime, TimeUnit timeUnit)` 方法的结束括号,表示该方法中批量向 Redis 放入键值对并设置过期时间的逻辑执行完毕,方法结束后,所有传入的键值对数据都会按照设定的规则逐个存入 Redis 中,并带有相应的过期时间设置,完成批量缓存数据的放置操作。
}
}
// 这个大括号是整个 `CacheClient` 类的结束括号,表示该类的定义结束,类中包含了多个用于缓存操作的方法,如缓存数据的设置、查询(包括解决缓存穿透、击穿等不同场景下的查询方法)以及批量获取、批量放入等功能相关的代码逻辑都在这个类的范围之内,类整体实现了与缓存交互的一系列功能,为项目中的缓存应用提供了相应的操作支持。

@ -1,6 +1,9 @@
package cn.org.alan.exam.util;
import java.security.SecureRandom;
// 引入Java标准库中用于生成安全随机数的类。在很多涉及到安全敏感信息生成的场景中普通的随机数生成器可能存在一定的可预测性风险例如基于简单的算法和固定的种子值来生成随机数容易被攻击者分析规律并猜出后续的随机数情况。
// 而SecureRandom类则不同它借助系统底层提供的更具随机性的源来生成随机数比如在一些操作系统中它可能会利用硬件的随机数生成器像基于物理噪声等产生真正随机的信息源或者经过复杂的算法对系统的多种动态信息进行处理来生成随机数使得每次生成的随机数都具有很强的随机性很难被外界预测。
// 在本案例中生成班级口令是需要保证口令具有足够的不可预测性避免被轻易猜出所以使用SecureRandom类来生成随机数以满足对安全性有要求的这种场景需求。
/**
* @Author Alan
@ -9,19 +12,48 @@ import java.security.SecureRandom;
*/
public class ClassTokenGenerator {
// 定义一个私有静态的字符串常量,用于指定生成班级口令时可选用的字符集合。之所以选择包含所有大写字母("ABCDEFGHIJKLMNOPQRSTUVWXYZ")、小写字母("abcdefghijklmnopqrstuvwxyz")以及数字("0123456789")的这样一个全面的字符集合,是因为这样能极大地丰富口令的组成可能性。
// 从信息论角度来看,字符集合越丰富,可组合出的不同口令数量就越多,其随机性和多样性也就越高。例如,如果只使用数字来生成口令,那么可能的组合数量相对有限,容易被暴力破解等方式猜出。
// 而通过涵盖大小写字母和数字,就可以生成各种各样复杂的口令字符串,大大增加了口令的安全性和复杂性,使得生成的班级口令能够满足作为唯一标识且不易被他人轻易获取或猜到的要求,适用于像在线教育平台等场景中区分不同班级的访问口令等业务需求。
private static final String CHAR_POOL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
/**
*
* @param length
* @return
* length
// 在实际的业务场景中,比如在线教育平台里,不同班级可能需要各自独立的访问口令来保证教学资源的安全性以及班级的独立性,通过这个方法就可以为每个班级动态地生成一个合适的、具有一定安全性的口令。
// 调用者可以根据实际的安全策略、使用场景以及方便记忆等综合因素来合理指定口令的长度,一般来说,长度越长,口令的理论上的安全性就越高,因为随着长度增加,可能的组合数量会呈指数级增长,暴力破解的难度也就越大。
// 不过也要考虑到用户使用的便利性,如果口令过长可能导致用户难以记忆或者输入出错等问题,所以需要在安全性和便利性之间进行权衡,通过合理设置该参数来找到一个平衡点,然后生成符合要求的班级口令用于后续在相关业务逻辑中作为班级的标识口令使用,例如存储在数据库中与班级信息关联,或者展示给班级管理员等相关人员知晓并使用。
* @param length 使便
* @return lengthCHAR_POOL使
*/
public static String generateClassToken(int length) {
// 创建一个SecureRandom实例用于生成安全的随机数。在Java运行环境中SecureRandom的实现机制会尝试利用操作系统提供的最可靠的随机数生成源。
// 例如在某些Linux系统上它可能会调用"/dev/random"或"/dev/urandom"设备文件来获取随机数据("/dev/random"基于环境噪声生成真正的随机数,不过可能会在熵池耗尽时阻塞等待新的随机数据生成;"/dev/urandom"会复用已有的熵池数据,不会阻塞,但随机性稍弱一点,不过也能满足大多数安全需求)。
// 在Windows系统中它会利用系统的加密服务提供的随机数生成功能。总之不同系统底层实现虽有差异但目的都是为了提供高质量、难以预测的随机数确保每次调用它生成随机数时不管是生成用于密码学中的密钥还是像这里生成班级口令中的随机字符选取索引都能基于可靠的随机性使得生成的班级口令具有足够的不可预测性满足安全性要求避免出现口令被轻易猜出的情况。
SecureRandom random = new SecureRandom();
// 创建一个可变的字符串构建器StringBuilder对象用于逐步构建班级口令字符串。通过指定其初始容量为要生成的口令长度length参数这是一种性能优化的考量。
// 当使用StringBuilder进行字符串拼接操作时如果不指定初始容量它内部会有一个默认的初始容量随着不断往里面添加字符当字符数量超过当前容量时它会自动进行扩容操作而扩容操作涉及到内存的重新分配、数据复制等开销可能会影响性能。
// 提前指定初始容量为要生成的口令长度使得StringBuilder内部预先分配好合适大小的内存空间来容纳预计的字符数量在后续循环添加字符过程中大概率可以避免频繁的扩容操作从而提高构建口令字符串的效率使得整个生成口令的过程更加高效尤其是在生成较长口令时这种优化效果会更明显因为这样能减少不必要的内存操作开销更流畅地逐步构建出最终的口令字符串。
StringBuilder tokenBuilder = new StringBuilder(length);
// 开始一个循环循环次数由传入的口令长度参数length决定这意味着会根据指定的口令长度来逐次选取字符最终组成相应长度的口令字符串。
// 每次循环都是生成口令过程中的一个关键步骤,通过循环来逐个确定口令中的每一个字符,从整体上构建出完整的口令字符串,保证口令的长度符合预期要求,并且每个字符都是从预定义的丰富字符池中随机选取的,以此来实现口令的随机性和复杂性,符合口令安全性的设计目标。
for (int i = 0; i < length; i++) {
tokenBuilder.append(CHAR_POOL.charAt(random.nextInt(CHAR_POOL.length())));
// 调用SecureRandom实例的nextInt方法传入字符池CHAR_POOL的长度作为参数这里利用了SecureRandom生成安全随机数的功能来获取一个在0到字符池长度减1范围内的随机整数。
// 之所以要生成在这个范围内的随机数是因为这个随机数将作为索引来从字符池中选取字符字符池的索引是从0开始计数的而长度减1正好对应着最后一个有效字符的索引位置这样生成的随机索引就能覆盖字符池中的每一个字符保证每个字符都有相同的被选中概率实现完全随机的选取过程避免出现某些字符更容易被选中或者遗漏的情况从概率角度保证了口令中字符选取的随机性和均匀性。
// 例如如果字符池长度为62包含大小写字母和数字共62个字符那么生成的随机数就会在0到61这个区间内每次循环都能从这62个字符里等概率地随机选出一个作为口令中的一部分。
int randomIndex = random.nextInt(CHAR_POOL.length());
// 然后通过CHAR_POOL.charAt方法依据生成的随机索引从字符池中取出对应的字符并将其添加到tokenBuilder中。
// charAt方法是Java字符串类String提供的用于获取指定索引位置字符的方法在这里就是根据前面生成的随机索引准确地从字符池字符串中提取出对应的字符然后利用StringBuilder的append方法将这个字符添加到正在构建的口令字符串中。
// 每一次循环执行这个操作,就相当于在口令字符串中逐步添加一个随机字符,经过多次循环,口令字符串就会由一个个随机选取的字符不断拼接起来,最终形成一个完整的、具有足够随机性和复杂性的班级口令字符串,满足作为班级标识口令的安全性要求。
tokenBuilder.append(CHAR_POOL.charAt(randomIndex));
}
// 当循环结束后通过调用tokenBuilder的toString方法将构建好的包含随机字符的可变字符串构建器对象转换为不可变的字符串对象。
// StringBuilder对象在内存中是以一种可变的、高效的方式来存储和操作字符数据的它便于进行字符串的动态拼接等操作但在很多业务场景中最终需要的是一个不可变的、标准的字符串对象来进行传递、存储或者进一步处理等操作。
// toString方法会根据StringBuilder内部存储的字符数据创建一个新的不可变的字符串对象这个新的字符串就是最终生成的班级口令将其返回给调用者以便在业务逻辑中使用该口令比如存储到数据库中作为班级的访问口令、展示给用户等操作使得生成的口令能够顺利融入到整个业务流程中发挥其作为班级标识口令的作用。
return tokenBuilder.toString();
}
}

@ -1,7 +1,5 @@
package cn.org.alan.exam.util;
import cn.org.alan.exam.util.CryptoUtils.Algorithm.Encryption;
import cn.org.alan.exam.util.CryptoUtils.Algorithm.Signing;
import org.apache.commons.lang3.StringUtils;
@ -11,7 +9,6 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
@ -40,7 +37,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* AESDESRSA
* AESDESRSA便
*/
/**
@ -51,311 +48,493 @@ import java.util.concurrent.ConcurrentHashMap;
@Component
public class CryptoUtils {
// 定义默认的字符编码格式为UTF-8用于处理字符串和字节之间的转换操作保证统一的编码标准。
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
// 获取Base64的编码器实例用于将字节数据编码为Base64格式的字符串常用于加密后数据的编码传输等场景。
private static final Encoder BASE64_ENCODER = Base64.getEncoder();
// 获取Base64的解码器实例用于将Base64格式的字符串解码为原始字节数据比如在解密等操作前对密文进行解码。
private static final Decoder BASE64_DECODER = Base64.getDecoder();
// 使用ConcurrentHashMap缓存KeyFactory实例以算法Algorithm类型为键方便后续快速获取相应算法的KeyFactory避免重复创建提高性能并且支持并发访问。
private static final Map<Algorithm, KeyFactory> KEY_FACTORY_CACHE = new ConcurrentHashMap<>();
// 使用HashMap缓存Cipher实例同样以算法Algorithm类型为键用于存储加密、解密操作时对应的Cipher对象减少创建开销但需注意非并发安全情况下的使用场景此处代码中有相应的同步处理机制保证线程安全
private static final Map<Algorithm, Cipher> CIPHER_CACHE = new HashMap<>();
/**
* AESDES
* @param algorithm
* @return
* @throws NoSuchAlgorithmException
* AESDESBase64
*
* @param algorithm AlgorithmAlgorithm.AES_ECB_PKCS5
* @return Base64
* @throws NoSuchAlgorithmException
*/
public static String generateSymmetricKey(Algorithm algorithm) throws NoSuchAlgorithmException {
// 根据指定的算法名称获取对应的KeyGenerator实例用于生成对称密钥。
KeyGenerator generator = KeyGenerator.getInstance(algorithm.getName());
// 使用算法对应的默认密钥长度通过Algorithm中定义的keySize属性获取初始化KeyGenerator准备生成密钥。
generator.init(algorithm.getKeySize());
// 生成对称密钥返回的SecretKey对象包含了实际的密钥数据。
SecretKey secretKey = generator.generateKey();
// 将生成的对称密钥转换为字节数组后使用Base64编码器将其编码为字符串并返回方便存储和传输。
return BASE64_ENCODER.encodeToString(secretKey.getEncoded());
}
/**
* RSADSAPKCS8
* @param algorithm
* @return
* @throws NoSuchAlgorithmException
* RSADSAPKCS8Base64AsymmetricKeyPair
*
* @param algorithm AlgorithmAlgorithm.RSA_ECB_PKCS1
* @return Base64AsymmetricKeyPair
* @throws NoSuchAlgorithmException
*/
public static AsymmetricKeyPair generateAsymmetricKeyPair(Algorithm algorithm) throws NoSuchAlgorithmException {
// 根据指定的算法名称获取对应的KeyPairGenerator实例用于生成非对称密钥对。
KeyPairGenerator generator = KeyPairGenerator.getInstance(algorithm.getName());
// 使用算法对应的默认密钥长度通过Algorithm中定义的keySize属性获取初始化KeyPairGenerator准备生成密钥对。
generator.initialize(algorithm.getKeySize());
// 生成非对称密钥对返回的KeyPair对象包含了公钥和私钥。
KeyPair keyPair = generator.generateKeyPair();
// 将公钥转换为字节数组后使用Base64编码器将其编码为字符串得到公钥的Base64编码表示形式。
String publicKey = BASE64_ENCODER.encodeToString(keyPair.getPublic().getEncoded());
// 将私钥转换为字节数组后使用Base64编码器将其编码为字符串得到私钥的Base64编码表示形式。
String privateKey = BASE64_ENCODER.encodeToString(keyPair.getPrivate().getEncoded());
// 创建AsymmetricKeyPair对象将公钥和私钥的Base64编码字符串传入封装后返回。
return new AsymmetricKeyPair(publicKey, privateKey);
}
/**
* 使RSARSA_ECB_PKCS1encryptAsymmetrically
*
* @param publicKeyText Base64
* @param plainText
* @return Base64
* @throws Exception
*/
public static String encryptByRSA(String publicKeyText, String plainText) throws Exception {
return encryptAsymmetrically(publicKeyText, plainText, Encryption.RSA_ECB_PKCS1);
}
/**
* 使RSARSA_ECB_PKCS1decryptAsymmetrically
*
* @param privateKeyText Base64
* @param ciphertext Base64
* @return
* @throws Exception
*/
public static String decryptByRSA(String privateKeyText, String ciphertext) throws Exception {
return decryptAsymmetrically(privateKeyText, ciphertext, Encryption.RSA_ECB_PKCS1);
}
/**
* SHA1DSA使
* @param privateKeyText
* @param msg
* @return
* @throws Exception
* SHA1DSA使doSign
*
* @param privateKeyText Base64
* @param msg
* @return Base64
* @throws Exception
*/
public static String signBySHA1WithDSA(String privateKeyText, String msg) throws Exception {
return doSign(privateKeyText, msg, Encryption.DSA, Signing.SHA1WithDSA);
}
/**
* SHA1RSA使
* @param privateKeyText
* @param msg
* @return
* @throws Exception
* SHA1RSA使doSign
*
* @param privateKeyText Base64
* @param msg
* @return Base64
* @throws Exception
*/
public static String signBySHA1WithRSA(String privateKeyText, String msg) throws Exception {
return doSign(privateKeyText, msg, Encryption.RSA_ECB_PKCS1, Signing.SHA1WithRSA);
}
/**
* SHA256RSA使
* @param privateKeyText
* @param msg
* @return
* @throws Exception
* SHA256RSA使doSign
*
* @param privateKeyText Base64
* @param msg
* @return Base64
* @throws Exception
*/
public static String signBySHA256WithRSA(String privateKeyText, String msg) throws Exception {
return doSign(privateKeyText, msg, Encryption.RSA_ECB_PKCS1, Signing.SHA256WithRSA);
}
/**
* SHA1DSA
* @param publicKeyText
* @param msg
* @param signatureText
* @return
* @throws Exception
* SHA1DSAdoVerify
*
* @param publicKeyText Base64
* @param msg
* @param signatureText Base64
* @return truefalse
* @throws Exception
*/
public static boolean verifyBySHA1WithDSA(String publicKeyText, String msg, String signatureText) throws Exception {
return doVerify(publicKeyText, msg, signatureText, Encryption.DSA, Signing.SHA1WithDSA);
}
/**
* SHA1RSA
* @param publicKeyText
* @param msg
* @param signatureText
* @return
* @throws Exception
* SHA1RSAdoVerify
*
* @param publicKeyText Base64
* @param msg
* @param signatureText Base64
* @return truefalse
* @throws Exception
*/
public static boolean verifyBySHA1WithRSA(String publicKeyText, String msg, String signatureText) throws Exception {
return doVerify(publicKeyText, msg, signatureText, Encryption.RSA_ECB_PKCS1, Signing.SHA1WithRSA);
}
/**
* SHA256RSA
* @param publicKeyText
* @param msg
* @param signatureText
* @return
* @throws Exception
* SHA256RSAdoVerify
*
* @param publicKeyText Base64
* @param msg
* @param signatureText Base64
* @return truefalse
* @throws Exception
*/
public static boolean verifyBySHA256WithRSA(String publicKeyText, String msg, String signatureText) throws Exception {
public static boolean verifyBySHA256WithRSA(String publicKeyText, String msg, signatureText) throws Exception {
return doVerify(publicKeyText, msg, signatureText, Encryption.RSA_ECB_PKCS1, Signing.SHA256WithRSA);
}
/**
* Base64
*
* @param secretKey Base64
* @param iv CBCCBC使
* @param plainText
* @param algorithm AlgorithmAlgorithm.AES_ECB_PKCS5
* @return Base64
* @throws Exception
*/
public static String encryptSymmetrically(String secretKey, String iv, String plainText, Algorithm algorithm) throws Exception {
// 先将Base64编码的对称密钥解码并重新生成SecretKey实例用于后续的加密操作。
SecretKey key = decodeSymmetricKey(secretKey, algorithm);
// 根据传入的加密向量字符串如果非空解码生成IvParameterSpec实例用于指定加密的初始向量信息CBC模式等需要若为空则设置为null表示不需要初始向量如ECB模式
IvParameterSpec ivParameterSpec = StringUtils.isBlank(iv)? null : decodeIv(iv);
// 将明文字符串按照默认字符编码格式UTF-8转换为字节数组准备进行加密操作。
byte[] plainTextInBytes = plainText.getBytes(DEFAULT_CHARSET);
// 调用transform方法传入算法、加密模式、密钥、加密向量以及明文字节数组执行实际的加密操作得到加密后的字节数组。
byte[] ciphertextInBytes = transform(algorithm, Cipher.ENCRYPT_MODE, key, ivParameterSpec, plainTextInBytes);
// 将加密后的字节数组使用Base64编码器转换为字符串并返回作为最终的加密结果密文
return BASE64_ENCODER.encodeToString(ciphertextInBytes);
}
/**
*
*
* @param secretKey Base64
* @param iv CBCCBC
* @param ciphertext Base64
* @param algorithm AlgorithmAlgorithm.AES_ECB_PKCS5
* @return
* @throws Exception
*/
public static boolean verifyBySHA256WithRSA(String publicKeyText, String msg, String signatureText) throws Exception {
// 调用doVerify方法传入公钥文本publicKeyText、待验证的消息msg、数字签名文本signatureText以及指定的加密算法Encryption.RSA_ECB_PKCS1和签名算法Signing.SHA256WithRSA
// 目的是使用RSA加密算法结合SHA256签名算法来验证给定的数字签名是否与消息匹配最终返回验证结果true表示验证通过false表示验证失败
return doVerify(publicKeyText, msg, signatureText, Encryption.RSA_ECB_PKCS1, Signing.SHA256WithRSA);
}
/**
*
* @param secretKey
* @param iv CBCCBC
* @param plainText
* @param algorithm AESDES
* @return
* @throws Exception
* @param secretKey Base64
* @param iv CBCCBC使CBC
* @param plainText
* @param algorithm AESDESAlgorithm使
* @return Base64便
* @throws Exception
*/
public static String encryptSymmetrically(String secretKey, String iv, String plainText, Algorithm algorithm) throws Exception {
// 调用decodeSymmetricKey方法传入对称密钥secretKey和指定的对称加密算法algorithm将Base64编码的对称密钥解码并重新生成SecretKey实例用于后续的加密操作得到实际可用的对称密钥对象存储在key变量中。
SecretKey key = decodeSymmetricKey(secretKey, algorithm);
IvParameterSpec ivParameterSpec = StringUtils.isBlank(iv) ? null : decodeIv(iv);
// 通过StringUtils.isBlank判断传入的加密向量iv是否为空字符串如果为空则将ivParameterSpec设置为null表示不需要加密向量例如在ECB等不需要加密向量的加密模式下
// 如果不为空则调用decodeIv方法对加密向量进行解码并生成IvParameterSpec实例用于指定加密的初始向量信息在CBC模式等需要的情况下存储在ivParameterSpec变量中。
IvParameterSpec ivParameterSpec = StringUtils.isBlank(iv)? null : decodeIv(iv);
// 将明文字符串plainText按照默认字符编码格式DEFAULT_CHARSET即UTF-8转换为字节数组准备进行加密操作得到的字节数组存储在plainTextInBytes变量中这是因为加密算法通常操作的是字节数据。
byte[] plainTextInBytes = plainText.getBytes(DEFAULT_CHARSET);
// 调用transform方法传入对称加密算法algorithm、加密模式Cipher.ENCRYPT_MODE表示加密操作、生成的对称密钥key、加密向量ivParameterSpec以及明文字节数组plainTextInBytes
// 执行实际的对称加密操作得到加密后的字节数组存储在ciphertextInBytes变量中。
byte[] ciphertextInBytes = transform(algorithm, Cipher.ENCRYPT_MODE, key, ivParameterSpec, plainTextInBytes);
// 将加密后的字节数组ciphertextInBytes使用Base64编码器BASE64_ENCODER转换为字符串即将二进制的加密数据编码为可方便存储和传输的Base64编码字符串形式然后返回该字符串作为最终的加密结果密文
return BASE64_ENCODER.encodeToString(ciphertextInBytes);
}
/**
*
* @param secretKey
* @param iv CBCCBC
* @param ciphertext
* @param algorithm AESDES
* @return
* @throws Exception
* @param secretKey Base64使
* @param iv CBCCBC使
* @param ciphertext Base64
* @param algorithm AESDESAlgorithm使使
* @return
* @throws Exception
*/
public static String decryptSymmetrically(String secretKey, String iv, String ciphertext, Algorithm algorithm) throws Exception {
// 调用decodeSymmetricKey方法传入对称密钥secretKey和指定的对称加密算法algorithm将Base64编码的对称密钥解码并重新生成SecretKey实例用于后续的解密操作得到实际可用的对称密钥对象存储在key变量中。
SecretKey key = decodeSymmetricKey(secretKey, algorithm);
IvParameterSpec ivParameterSpec = StringUtils.isBlank(iv) ? null : decodeIv(iv);
// 通过StringUtils.isBlank判断传入的加密向量iv是否为空字符串如果为空则将ivParameterSpec设置为null表示不需要加密向量对应不需要加密向量的加密模式下的解密情况
// 如果不为空则调用decodeIv方法对加密向量进行解码并生成IvParameterSpec实例用于指定解密时的初始向量信息在CBC模式等需要的情况下存储在ivParameterSpec变量中。
IvParameterSpec ivParameterSpec = StringUtils.isBlank(iv)? null : decodeIv(iv);
// 使用Base64解码器BASE64_DECODER对传入的密文ciphertext进行解码将Base64编码的字符串转换回原始的字节数组得到的字节数组存储在ciphertextInBytes变量中这是因为后续解密操作需要操作字节数据。
byte[] ciphertextInBytes = BASE64_DECODER.decode(ciphertext);
// 调用transform方法传入对称加密算法algorithm、解密模式Cipher.DECRYPT_MODE表示解密操作、生成的对称密钥key、加密向量ivParameterSpec以及密文字节数组ciphertextInBytes
// 执行实际的对称解密操作得到解密后的字节数组存储在plainTextInBytes变量中。
byte[] plainTextInBytes = transform(algorithm, Cipher.DECRYPT_MODE, key, ivParameterSpec, ciphertextInBytes);
// 将解密后的字节数组plainTextInBytes按照默认字符编码格式DEFAULT_CHARSET即UTF-8转换为字符串即将字节数据还原为原始的文本内容然后返回该字符串作为最终的解密结果明文
return new String(plainTextInBytes, DEFAULT_CHARSET);
}
/**
*
* @param publicKeyText
* @param plainText
* @param algorithm
* @return
* @throws Exception
* @param publicKeyText Base64
* @param plainText
* @param algorithm Algorithm使RSA
* @return Base64便
* @throws Exception
*/
public static String encryptAsymmetrically(String publicKeyText, String plainText, Algorithm algorithm) throws Exception {
// 调用regeneratePublicKey方法传入公钥文本publicKeyText和指定的非对称加密算法algorithm将Base64编码的公钥解码并重新生成PublicKey实例用于后续的加密操作得到实际可用的公钥对象存储在publicKey变量中。
PublicKey publicKey = regeneratePublicKey(publicKeyText, algorithm);
// 将明文字符串plainText按照默认字符编码格式DEFAULT_CHARSET即UTF-8转换为字节数组准备进行加密操作得到的字节数组存储在plainTextInBytes变量中因为加密算法通常是基于字节数据进行操作的。
byte[] plainTextInBytes = plainText.getBytes(DEFAULT_CHARSET);
// 调用transform方法传入非对称加密算法algorithm、加密模式Cipher.ENCRYPT_MODE表示加密操作、生成的公钥publicKey以及明文字节数组plainTextInBytes
// 执行实际的非对称加密操作得到加密后的字节数组存储在ciphertextInBytes变量中。
byte[] ciphertextInBytes = transform(algorithm, Cipher.ENCRYPT_MODE, publicKey, plainTextInBytes);
// 将加密后的字节数组ciphertextInBytes使用Base64编码器BASE64_ENCODER转换为字符串即将二进制的加密数据编码为可方便存储和传输的Base64编码字符串形式然后返回该字符串作为最终的加密结果密文
return BASE64_ENCODER.encodeToString(ciphertextInBytes);
}
/**
*
* @param privateKeyText
* @param ciphertext
* @param algorithm
* @return
* @throws Exception
*/
public static String decryptAsymmetrically(String privateKeyText, String ciphertext, Algorithm algorithm) throws Exception {
PrivateKey privateKey = regeneratePrivateKey(privateKeyText, algorithm);
byte[] ciphertextInBytes = BASE64_DECODER.decode(ciphertext);
byte[] plainTextInBytes = transform(algorithm, Cipher.DECRYPT_MODE, privateKey, ciphertextInBytes);
return new String(plainTextInBytes, DEFAULT_CHARSET);
}
/**
*
* @param privateKeyText
* @param ciphertext
* @param algorithm
* @return
* @throws Exception
*/
public static String decryptAsymmetrically(String privateKeyText, String ciphertext, Algorithm algorithm) throws Exception {
// 调用regeneratePrivateKey方法传入私钥文本privateKeyText和指定的非对称加密算法algorithm
// 目的是将Base64编码格式的私钥文本解码并重新生成对应的PrivateKey实例以便后续用于解密操作生成的私钥对象存储在privateKey变量中。
PrivateKey privateKey = regeneratePrivateKey(privateKeyText, algorithm);
// 使用Base64解码器BASE64_DECODER对传入的密文ciphertext进行解码操作将Base64编码的密文字符串转换为原始的字节数组形式
// 得到的字节数组存储在ciphertextInBytes变量中因为后续的解密操作需要基于字节数据进行处理。
byte[] ciphertextInBytes = BASE64_DECODER.decode(ciphertext);
// 调用transform方法传入非对称加密算法algorithm、解密模式Cipher.DECRYPT_MODE表示进行解密操作、刚生成的私钥privateKey以及密文字节数组ciphertextInBytes
// 执行实际的非对称解密操作将密文字节数组还原为原始的明文字节数组得到的明文字节数组存储在plainTextInBytes变量中。
byte[] plainTextInBytes = transform(algorithm, Cipher.DECRYPT_MODE, privateKey, ciphertextInBytes);
// 将解密得到的明文字节数组plainTextInBytes按照默认字符编码格式DEFAULT_CHARSET这里通常是UTF-8转换为字符串形式
// 从而将字节数据还原为原始的文本内容,最终返回这个解密后的字符串作为非对称解密的结果,也就是原始的明文信息。
return new String(plainTextInBytes, DEFAULT_CHARSET);
}
/**
*
* @param privateKeyText
* @param msg
* @param encryptionAlgorithm Algorithm
* @param signatureAlgorithm Algorithm
* @return
* @throws Exception
* @param privateKeyText Base64
* @param msg
* @param encryptionAlgorithm AlgorithmRSA
* @param signatureAlgorithm AlgorithmSHA1WithRSA
* @return Base64便
* @throws Exception
*/
public static String doSign(String privateKeyText, String msg, Algorithm encryptionAlgorithm, Algorithm signatureAlgorithm)
throws Exception {
// 调用regeneratePrivateKey方法传入私钥文本privateKeyText和指定的加密算法encryptionAlgorithm
// 作用是将Base64编码的私钥解码并重新生成对应的PrivateKey实例以便后续利用该私钥进行数字签名操作生成的私钥对象存储在privateKey变量中。
PrivateKey privateKey = regeneratePrivateKey(privateKeyText, encryptionAlgorithm);
// Signature只支持签名算法
// 通过Signature.getInstance方法根据传入的签名算法名称signatureAlgorithm.getName()获取对应的Signature实例
// Signature是Java中用于处理数字签名相关操作的类这里获取到的实例用于后续具体的签名生成操作因为它只支持签名算法相关功能。
Signature signature = Signature.getInstance(signatureAlgorithm.getName());
// 使用获取到的Signature实例调用initSign方法并传入私钥privateKey进行初始化操作
// 此操作是为了告知签名对象后续将使用该私钥来生成数字签名,为签名过程做准备。
signature.initSign(privateKey);
// 调用signature的update方法将需要签名的数据msg按照默认字符编码格式DEFAULT_CHARSET一般为UTF-8转换为字节数组后传入
// 这个操作是将待签名的数据传递给签名对象,以便它基于这些数据和之前初始化的私钥来生成数字签名,类似于告知签名对象要对哪些具体内容进行签名。
signature.update(msg.getBytes(DEFAULT_CHARSET));
// 调用signature的sign方法执行实际的数字签名生成操作该方法会基于前面设置的私钥、待签名数据等信息生成数字签名返回的是字节数组形式的签名结果存储在signatureInBytes变量中。
byte[] signatureInBytes = signature.sign();
// 将生成的字节数组形式的数字签名signatureInBytes使用Base64编码器BASE64_ENCODER进行编码转换为Base64编码字符串形式
// 方便在网络传输等场景中传递该数字签名,最终返回这个编码后的字符串作为生成的数字签名结果。
return BASE64_ENCODER.encodeToString(signatureInBytes);
}
/**
*
* @param publicKeyText
* @param msg
* @param signatureText
* @param encryptionAlgorithm Algorithm
* @param signatureAlgorithm Algorithm
* @return
* @throws Exception
* @param publicKeyText Base64使
* @param msg 使
* @param signatureText Base64使
* @param encryptionAlgorithm Algorithm使
* @param signatureAlgorithm Algorithm使
* @return truefalse
* @throws Exception
*/
public static boolean doVerify(String publicKeyText, String msg, String signatureText, Algorithm encryptionAlgorithm,
Algorithm signatureAlgorithm) throws Exception {
// 调用regeneratePublicKey方法传入公钥文本publicKeyText和指定的加密算法encryptionAlgorithm
// 目的是将Base64编码格式的公钥文本解码并重新生成对应的PublicKey实例以便后续利用该公钥进行数字签名验证操作生成的公钥对象存储在publicKey变量中。
PublicKey publicKey = regeneratePublicKey(publicKeyText, encryptionAlgorithm);
// 通过Signature.getInstance方法根据传入的签名算法名称signatureAlgorithm.getName()获取对应的Signature实例
// 用于后续具体的数字签名验证操作,因为它提供了验证签名有效性的相关功能。
Signature signature = Signature.getInstance(signatureAlgorithm.getName());
// 使用获取到的Signature实例调用initVerify方法并传入公钥publicKey进行初始化操作
// 此操作是为了告知验证对象后续将使用该公钥来验证数字签名,为验证过程做准备。
signature.initVerify(publicKey);
// 调用signature的update方法将需要验证的数据msg按照默认字符编码格式DEFAULT_CHARSET一般为UTF-8转换为字节数组后传入
// 这个操作是将原始数据传递给验证对象,使其基于这些数据和之前初始化的公钥来进行数字签名验证,类似于告知验证对象要验证的原始数据内容是什么。
signature.update(msg.getBytes(DEFAULT_CHARSET));
// 调用signature的verify方法执行实际的数字签名验证操作传入经过Base64解码后的数字签名通过BASE64_DECODER.decode(signatureText)获取原始字节数组形式的签名),
// 该方法会基于前面设置的公钥、原始数据以及传入的数字签名信息进行验证,返回一个布尔值,表示验证是否成功,直接返回这个布尔值作为最终的验证结果。
return signature.verify(BASE64_DECODER.decode(signatureText));
}
/**
* Base64SecretKey
* @param secretKey
* @param algorithm
* @return
*/
private static SecretKey decodeSymmetricKey(String secretKey, Algorithm algorithm) {
byte[] key = BASE64_DECODER.decode(secretKey);
return new SecretKeySpec(key, algorithm.getName());
}
/**
* Base64SecretKey
* @param secretKey
* @param algorithm
* @return
*/
private static SecretKey decodeSymmetricKey(String secretKey, Algorithm algorithm) {
// 使用BASE64_DECODERBase64解码器在类中前面已定义对传入的对称密钥字符串secretKey进行解码操作
// 将Base64编码格式的密钥字符串转换为原始的字节数组形式得到的字节数组存储在key变量中以便后续基于字节数据构建SecretKey对象。
byte[] key = BASE64_DECODER.decode(secretKey);
// 使用SecretKeySpec类根据解码后的字节数组key和指定的算法名称algorithm.getName()创建一个SecretKey实例
// SecretKeySpec实现了SecretKey接口用于包装字节数组形式的密钥数据使其成为符合特定对称加密算法要求的密钥对象最终返回这个SecretKey对象用于后续的对称加密或解密操作等。
return new SecretKeySpec(key, algorithm.getName());
}
private static IvParameterSpec decodeIv(String iv) {
// 使用BASE64_DECODERBase64解码器对传入的加密向量字符串iv进行解码操作
// 将Base64编码格式的加密向量字符串转换为原始的字节数组形式得到的字节数组存储在ivInBytes变量中后续将基于此字节数组构建IvParameterSpec对象用于加密或解密操作在需要加密向量的模式下
byte[] ivInBytes = BASE64_DECODER.decode(iv);
// 使用IvParameterSpec类根据解码后的字节数组ivInBytes创建一个IvParameterSpec实例
// IvParameterSpec用于指定加密或解密操作中使用的初始向量信息在如CBC模式等需要加密向量的情况下最终返回这个IvParameterSpec对象用于相关加密或解密流程。
return new IvParameterSpec(ivInBytes);
}
private static PublicKey regeneratePublicKey(String publicKeyText, Algorithm algorithm)
throws NoSuchAlgorithmException, InvalidKeySpecException {
// 使用BASE64_DECODERBase64解码器对传入的公钥字符串publicKeyText进行解码操作
// 将Base64编码格式的公钥字符串转换为原始的字节数组形式得到的字节数组存储在keyInBytes变量中以便后续基于这些字节数据构建PublicKey对象。
byte[] keyInBytes = BASE64_DECODER.decode(publicKeyText);
// 调用getKeyFactory方法传入指定的算法algorithm获取对应的KeyFactory实例KeyFactory用于根据特定算法创建密钥相关的对象如公钥、私钥等存储在keyFactory变量中。
KeyFactory keyFactory = getKeyFactory(algorithm);
// 公钥必须使用RSAPublicKeySpec或者X509EncodedKeySpec
// 按照Java加密规范要求公钥必须使用RSAPublicKeySpec或者X509EncodedKeySpec来构建这里使用X509EncodedKeySpec类
// 根据解码后的字节数组keyInBytes创建一个公钥规范对象KeySpec用于后续通过KeyFactory生成具体的PublicKey对象此规范定义了公钥的格式和数据内容等信息。
KeySpec publicKeySpec = new X509EncodedKeySpec(keyInBytes);
// 使用获取到的KeyFactory实例keyFactory调用其generatePublic方法并传入公钥规范对象publicKeySpec
// 执行实际的公钥生成操作将符合规范的字节数据转换为可用的PublicKey对象存储在publicKey变量中该PublicKey对象可用于后续的非对称加密、数字签名验证等操作。
PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
// 返回生成的PublicKey对象供调用者在需要使用公钥的地方如加密、验证等操作使用。
return publicKey;
}
private static PrivateKey regeneratePrivateKey(String key, Algorithm algorithm) throws Exception {
// 使用BASE64_DECODERBase64解码器对传入的私钥字符串key进行解码操作
// 将Base64编码格式的私钥字符串转换为原始的字节数组形式得到的字节数组存储在keyInBytes变量中以便后续基于这些字节数据构建PrivateKey对象。
byte[] keyInBytes = BASE64_DECODER.decode(key);
// 调用getKeyFactory方法传入指定的算法algorithm获取对应的KeyFactory实例用于后续创建私钥对象存储在keyFactory变量中。
KeyFactory keyFactory = getKeyFactory(algorithm);
// 私钥必须使用RSAPrivateCrtKeySpec或者PKCS8EncodedKeySpec
// 按照Java加密规范要求私钥必须使用RSAPrivateCrtKeySpec或者PKCS8EncodedKeySpec来构建这里使用PKCS8EncodedKeySpec类
// 根据解码后的字节数组keyInBytes创建一个私钥规范对象KeySpec用于定义私钥的格式和数据内容等信息为生成私钥做准备。
KeySpec privateKeySpec = new PKCS8EncodedKeySpec(keyInBytes);
// 使用获取到的KeyFactory实例keyFactory调用其generatePrivate方法并传入私钥规范对象privateKeySpec
// 执行实际的私钥生成操作将符合规范的字节数据转换为可用的PrivateKey对象存储在privateKey变量中该PrivateKey对象可用于后续的非对称解密、数字签名生成等操作。
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
// 返回生成的PrivateKey对象供调用者在需要使用私钥的地方如解密、签名等操作使用。
return privateKey;
}
private static KeyFactory getKeyFactory(Algorithm algorithm) throws NoSuchAlgorithmException {
// 尝试从KEY_FACTORY_CACHE一个以Algorithm为键存储KeyFactory实例的缓存Map在类中前面已定义中获取指定算法algorithm对应的KeyFactory实例
// 目的是先查看缓存中是否已经存在该算法的KeyFactory如果存在则直接复用避免重复创建提高性能获取到的实例存储在keyFactory变量中。
KeyFactory keyFactory = KEY_FACTORY_CACHE.get(algorithm);
// 判断从缓存中获取到的KeyFactory实例是否为null如果是null表示缓存中还没有该算法对应的KeyFactory实例需要创建新的实例执行以下操作。
if (keyFactory == null) {
// 通过KeyFactory.getInstance方法根据指定算法的名称algorithm.getName()创建一个新的KeyFactory实例
// 此方法会根据Java运行环境中支持的加密算法来查找并创建对应的KeyFactory用于后续创建密钥相关对象公钥、私钥等
keyFactory = KeyFactory.getInstance(algorithm.getName());
// 将新创建的KeyFactory实例放入KEY_FACTORY_CACHE缓存中以算法algorithm为键进行存储方便下次获取时直接使用避免再次创建提高效率。
KEY_FACTORY_CACHE.put(algorithm, keyFactory);
}
// 返回获取到从缓存中或者新创建的的KeyFactory实例供调用者使用例如用于生成公钥、私钥等操作。
return keyFactory;
}
private static byte[] transform(Algorithm algorithm, int mode, Key key, byte[] msg) throws Exception {
// 此方法重载了另一个transform方法这里直接调用另一个带有更多参数的transform方法传入算法algorithm、模式mode、密钥key、null表示加密向量为默认值具体由内部逻辑处理以及消息字节数组msg
// 目的是统一通过另一个更完整参数的方法来处理字节数据的转换(加密或解密等操作),最终返回转换后的字节数组结果。
return transform(algorithm, mode, key, null, msg);
}
private static byte[] transform(Algorithm algorithm, int mode, Key key, IvParameterSpec iv, byte[] msg) throws Exception {
// 尝试从CIPHER_CACHE一个以Algorithm为键存储Cipher实例的缓存Map在类中前面已定义中获取指定算法algorithm对应的Cipher实例
// Cipher实例用于执行加密或解密等实际操作先查看缓存中是否已经存在该算法对应的Cipher如果存在则直接复用避免重复创建减少开销获取到的实例存储在cipher变量中。
Cipher cipher = CIPHER_CACHE.get(algorithm);
// double check减少上下文切换
// 再次检查获取到的Cipher实例是否为null这是一种双重检查机制因为在多线程环境下可能存在多个线程同时发现缓存中没有对应Cipher实例的情况
// 通过再次检查来确保只有一个线程去创建并放入缓存,避免重复创建和不必要的上下文切换开销,提高并发性能。
if (cipher == null) {
// 使用synchronized关键字对CryptoUtils类进行同步锁控制确保在同一时刻只有一个线程能进入以下代码块执行创建Cipher实例并放入缓存的操作避免多线程并发冲突。
synchronized (CryptoUtils.class) {
// 再次检查在进入同步块后缓存中是否已经存在该算法对应的Cipher实例因为可能在等待进入同步块的过程中其他线程已经创建并放入缓存了
// 如果还是null表示确实需要创建新的Cipher实例执行以下操作。
if ((cipher = CIPHER_CACHE.get(algorithm)) == null) {
// 调用determineWhichCipherToUse方法传入指定算法algorithm根据算法相关信息确定并创建对应的Cipher实例
// 该方法内部会根据算法的具体配置如算法名称、转换模式等来创建合适的Cipher对象用于后续的加密或解密操作。
cipher = determineWhichCipherToUse(algorithm);
// 将新创建的Cipher实例放入CIPHER_CACHE缓存中以算法algorithm为键进行存储方便后续获取时直接使用避免再次创建提高效率。
CIPHER_CACHE.put(algorithm, cipher);
}
// 使用获取到从缓存中或者新创建的的Cipher实例调用其init方法传入操作模式mode如加密模式或解密模式等、密钥key以及加密向量iv可能为null取决于具体算法和模式需求
// 对Cipher实例进行初始化操作使其准备好执行后续的加密或解密操作根据传入的模式和密钥等信息来配置其内部状态。
cipher.init(mode, key, iv);
// 调用Cipher实例的doFinal方法传入要处理的消息字节数组msg执行实际的加密或解密操作取决于前面传入的模式是加密还是解密
// 该方法会对消息字节数组进行相应处理,并返回处理后的字节数组结果,也就是加密或解密后的字节数组,最终返回这个结果字节数组供调用者使用。
return cipher.doFinal(msg);
}
}
// 如果第一次获取到的Cipher实例不为null即缓存中已经存在可用的Cipher实例执行以下操作同样进行初始化和执行加密或解密操作。
synchronized (CryptoUtils.class) {
cipher.init(mode, key, iv);
return cipher.doFinal(msg);
}
}
private static Cipher determineWhichCipherToUse(Algorithm algorithm) throws NoSuchAlgorithmException, NoSuchPaddingException {
// 声明一个Cipher类型的变量cipher用于存储后续创建的Cipher实例Cipher类用于执行加密或解密操作这里先进行变量声明后续会根据具体情况创建并赋值。
Cipher cipher;
// 获取传入的Algorithm对象中定义的转换字符串transformation这个转换字符串通常遵循algorithm/mode/padding的格式例如 "AES/CBC/PKCS5Padding"
// 它用于指定加密或解密操作中具体的算法、模式如ECB、CBC等加密模式以及填充方式如PKCS5Padding等等信息存储在transformation变量中。
String transformation = algorithm.getTransformation();
// 官方推荐的transformation使用algorithm/mode/padding组合SunJCE使用ECB作为默认模式使用PKCS5Padding作为默认填充
// 判断获取到的转换字符串transformation是否不为空即是否有指定具体的算法、模式和填充相关信息如果不为空表示有明确的转换要求执行以下操作。
if (StringUtils.isNotEmpty(transformation)) {
// 通过Cipher.getInstance方法传入指定的转换字符串transformation创建一个对应的Cipher实例
// 该方法会根据传入的转换字符串所表示的算法、模式和填充等信息在Java加密框架中查找并创建合适的Cipher对象用于后续的加密或解密操作创建的实例赋值给cipher变量。
cipher = Cipher.getInstance(transformation);
} else {
// 如果转换字符串为空表示没有明确指定具体的转换信息那么只根据算法名称algorithm.getName()来创建Cipher实例
// 此时会使用默认的模式和填充方式根据具体加密框架的默认设置如SunJCE的默认情况来创建Cipher对象同样将创建的实例赋值给cipher变量。
cipher = Cipher.getInstance(algorithm.getName());
}
// 返回创建好的Cipher实例供调用者在后续的加密、解密等操作中使用例如在transform方法中会利用这个Cipher实例来执行具体的字节数据转换操作。
return cipher;
}
/**
* <br/>
* <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#impl">jdk8</a>
* Algorithm便使
*/
public static class Algorithm {
// 定义一个内部接口Encryption用于列举各种加密算法相关的常量配置这些常量都是Algorithm类型的实例每个实例代表一种具体的加密算法及其相关属性。
public interface Encryption {
// 创建一个Algorithm实例表示AES算法在ECB模式下使用PKCS5Padding填充的配置算法名称为 "AES",转换字符串为 "AES/ECB/PKCS5Padding"密钥长度为128位
// 通过这种方式定义了一种具体的加密算法配置,方便在代码中直接引用这个常量来表示该加密算法,后续在加密操作等场景中可以基于此配置来创建对应的加密对象等。
Algorithm AES_ECB_PKCS5 = new Algorithm("AES", "AES/ECB/PKCS5Padding", 128);
Algorithm AES_CBC_PKCS5 = new Algorithm("AES", "AES/CBC/PKCS5Padding", 128);
Algorithm DES_ECB_PKCS5 = new Algorithm("DES", "DES/ECB/PKCS5Padding", 56);
@ -364,23 +543,31 @@ public class CryptoUtils {
Algorithm DSA = new Algorithm("DSA", 1024);
}
// 定义一个内部接口Signing用于列举各种签名算法相关的常量配置同样每个常量都是Algorithm类型的实例代表一种具体的签名算法及其相关属性。
public interface Signing {
Algorithm SHA1WithDSA = new Algorithm("SHA1withDSA", 1024);
Algorithm SHA1WithRSA = new Algorithm("SHA1WithRSA", 2048);
Algorithm SHA256WithRSA = new Algorithm("SHA256WithRSA", 2048);
}
// 使用Lombok的@Getter注解自动生成获取name属性的getter方法name属性用于存储算法的名称例如 "AES"、"RSA" 等,方便外部获取该算法的名称信息。
@Getter
private String name;
// 使用Lombok的@Getter注解自动生成获取transformation属性的getter方法transformation属性用于存储算法对应的转换字符串包含算法、模式和填充等相关信息便于外部获取该算法的具体转换配置。
@Getter
private String transformation;
// 使用Lombok的@Getter注解自动生成获取keySize属性的getter方法keySize属性用于存储算法对应的密钥长度以位为单位例如128、1024等方便外部获取该算法的密钥长度信息。
@Getter
private int keySize;
// 构造函数用于创建Algorithm实例接收算法名称name和密钥长度keySize两个参数调用另一个构造函数this(name, null, keySize))并传递相应参数,
// 主要用于在创建算法实例时只明确算法名称和密钥长度,而不指定具体转换字符串(使用默认的转换配置情况)的场景,比如某些算法只需要名称和密钥长度就能确定基本配置的情况。
public Algorithm(String name, int keySize) {
this(name, null, keySize);
}
// 完整的构造函数用于创建Algorithm实例接收算法名称name、转换字符串transformation和密钥长度keySize三个参数
// 分别将这些参数赋值给对应的实例属性this.name、this.transformation、this.keySize用于初始化一个完整的算法配置对象在定义各种具体算法常量时会调用此构造函数来明确算法的各项属性。
public Algorithm(String name, String transformation, int keySize) {
this.name = name;
this.transformation = transformation;
@ -389,15 +576,18 @@ public class CryptoUtils {
}
// 使用Lombok的@Data注解自动生成该类的常用方法如getter、setter、toString、hashCode和equals等方法方便对类的属性进行操作和对象间的比较等操作。
// 使用Lombok的@NoArgsConstructor注解自动生成无参构造函数方便在某些场景下创建该类的默认实例例如在一些框架进行对象实例化时如果需要无参构造函数的情况。
// 使用Lombok的@AllArgsConstructor注解自动生成全参构造函数接收两个参数publicKey和privateKey用于初始化类的属性方便在创建对象时一次性传入所有必要属性值进行初始化。
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class AsymmetricKeyPair {
// 定义一个字符串类型的属性publicKey用于存储非对称加密中的公钥信息通常是以Base64编码的字符串形式存储公钥内容方便在加密、验证等操作中传递和使用公钥。
private String publicKey;
// 定义一个字符串类型的属性privateKey用于存储非对称加密中的私钥信息同样一般是以Base64编码的字符串形式存储私钥内容用于在解密、签名等操作中传递和使用私钥。
private String privateKey;
}
}
}

@ -1,8 +1,13 @@
package cn.org.alan.exam.util;
import java.time.LocalDate;
// 引入Java 8中用于表示日期不包含时间部分的类它提供了一系列操作日期的方法例如获取年、月、日等信息以及进行日期的比较、计算等操作在这个工具类中用于处理只涉及日期的相关功能比如获取当前日期、将字符串表示的日期转换为LocalDate对象等情况。
import java.time.LocalDateTime;
// 引入Java 8中用于表示日期和时间的类它包含了日期年、月、日以及时间时、分、秒、纳秒信息同样提供了很多便捷的操作方法在本类中用于获取当前日期时间、进行日期时间与字符串之间的转换等操作方便在不同业务场景下对日期时间数据的处理和传递。
import java.time.format.DateTimeFormatter;
// 引入Java 8中用于格式化和解析日期时间对象的类它可以根据指定的日期时间格式模板如"yyyy-MM-dd HH:mm:ss")将日期时间对象转换为对应的字符串表示(格式化操作),也可以将符合格式要求的字符串解析为相应的日期时间对象(解析操作),在这个工具类中是实现日期时间与字符串相互转换的关键类,用于满足不同业务场景下对日期时间格式的需求。
/**
* @Author WeiJin
@ -11,36 +16,55 @@ import java.time.format.DateTimeFormatter;
*/
public class DateTimeUtil {
// 将构造函数私有化,这样外部就不能通过创建这个类的实例来使用它,因为这个工具类中的方法都是静态方法,不需要实例化对象就可以直接调用,通过这种方式可以防止不必要的实例化操作,遵循工具类的设计原则,保证类的使用方式符合预期(仅通过静态方法调用)。
private DateTimeUtil() {
}
// 定义一个私有静态字符串变量,用于指定日期的格式模板,这里的"yyyy-MM-dd"表示年-月-日的格式,例如"2024-03-28"在将LocalDate对象转换为字符串或者将符合此格式的字符串解析为LocalDate对象时会依据这个格式进行操作方便在业务中统一日期的表示格式便于数据的展示、存储和解析等操作。
private static String dataFormat = "yyyy-MM-dd";
// 定义一个私有静态字符串变量,用于指定日期时间的格式模板,"yyyy-MM-dd HH:mm:ss"表示年-月-日 时:分:秒的格式,比如"2024-03-28 13:39:00"在处理LocalDateTime对象与字符串之间的转换时会按照这个格式来进行格式化或者解析操作确保日期时间数据在不同场景下以统一且符合要求的格式呈现和处理。
private static String format = "yyyy-MM-dd HH:mm:ss";
/**
* 使yyyy-MM-dd HH:mm:ss
*
* @return
* "yyyy-MM-dd HH:mm:ss"LocalDateTime便使
* @return "yyyy-MM-dd HH:mm:ss"LocalDateTime使
*/
public static LocalDateTime getDateTime() {
// 首先调用LocalDateTime.now()方法获取当前的日期时间对象这个方法会返回一个表示当前系统时间的LocalDateTime实例包含了当前的年、月、日、时、分、秒、纳秒等信息。
// 然后将获取到的当前日期时间对象LocalDateTime.now()的返回值作为参数传递给datetimeToStr方法这个方法会按照之前定义的"yyyy-MM-dd HH:mm:ss"格式通过DateTimeFormatter.ofPattern(format)指定)将其转换为对应的字符串表示。
// 最后再通过DateTimeFormatter.ofPattern(format)创建的格式化器将前面得到的字符串表示的日期时间再解析回LocalDateTime对象这样做的目的是确保最终返回的LocalDateTime对象的格式是严格符合"yyyy-MM-dd HH:mm:ss"要求的,可能是为了处理一些日期时间对象在创建或者转换过程中格式可能出现不一致等情况,经过这样的格式化再解析操作后得到格式规范的结果并返回。
return LocalDateTime.parse(datetimeToStr(LocalDateTime.now()), DateTimeFormatter.ofPattern(format));
}
// 该方法用于获取当前的日期信息(不包含时间部分),并以指定的"yyyy-MM-dd"格式返回一个LocalDate对象适用于在业务中只关注日期不需要具体时间的场景比如获取某个业务操作对应的日期、查询某一天的数据等情况。
public static LocalDate getDate() {
// 先调用LocalDate.now()方法获取当前的日期对象它会返回一个表示当前系统日期的LocalDate实例仅包含年、月、日信息。
// 接着把获取到的当前日期对象LocalDate.now()的返回值传递给dateToStr方法该方法会按照之前定义的"yyyy-MM-dd"格式通过DateTimeFormatter.ofPattern(dataFormat)指定)将其转换为对应的字符串表示。
// 最后再利用DateTimeFormatter.ofPattern(dataFormat)创建的格式化器把前面得到的字符串表示的日期重新解析为LocalDate对象以此保证返回的LocalDate对象的格式完全符合"yyyy-MM-dd"的规范要求,同样可能是为了确保格式的准确性和一致性,然后将其返回供后续业务使用。
return LocalDate.parse(dateToStr(LocalDate.now()), DateTimeFormatter.ofPattern(dataFormat));
}
/**
* LocalDateTime
*
* @param dateTime
* @return LocalDateTime
* "yyyy-MM-dd HH:mm:ss"JavaLocalDateTime便
* @param dateTime "yyyy-MM-dd HH:mm:ss""2024-03-28 14:20:30"
* @return LocalDateTimeLocalDateTime便使
*/
public static String datetimeToStr(LocalDateTime dateTime) {
// 使用DateTimeFormatter类的ofPattern方法根据之前定义的"yyyy-MM-dd HH:mm:ss"格式模板通过format变量指定创建一个DateTimeFormatter实例这个实例用于将LocalDateTime对象按照指定格式转换为字符串表示。
// 然后调用这个格式化器的format方法将传入的LocalDateTime对象dateTime参数进行格式化操作将其转换为对应的字符串形式并返回这样就实现了从日期时间对象到符合指定格式的字符串的转换方便在需要展示日期时间或者将其存储为字符串格式等场景下使用。
return DateTimeFormatter.ofPattern(format).format(dateTime);
}
// 该方法用于将传入的符合"yyyy-MM-dd"格式的字符串类型的日期信息转换为Java中的LocalDate对象使得可以在业务逻辑中对字符串表示的日期进行相关操作比如与其他日期进行比较、判断是否在某个日期区间内等
public static String dateToStr(LocalDate date) {
// 通过DateTimeFormatter类的ofPattern方法依据之前定义的"yyyy-MM-dd"格式模板由dataFormat变量指定创建一个DateTimeFormatter实例用于按照此格式将LocalDate对象转换为字符串表示。
// 接着调用这个格式化器的format方法把传入的LocalDate对象date参数进行格式化将其转换为对应的字符串形式并返回完成从日期对象到符合指定格式的字符串的转换便于在不同业务场景下对日期数据以字符串形式进行处理如展示给用户、存储到文件等
return DateTimeFormatter.ofPattern(dataFormat).format(date);
}
}
}

@ -1,82 +1,146 @@
package cn.org.alan.exam.util;
import cn.hutool.extra.spring.SpringUtil;
// 引入Hutool工具库中与Spring整合相关的工具类它提供了一些方便在Spring环境下进行操作的方法不过在当前这个类中暂时未看到其具体使用可能在更广泛的项目上下文里用于获取Spring容器中的Bean或者进行一些与Spring框架交互的便捷操作等情况。
import cn.org.alan.exam.model.entity.User;
// 引入项目中定义的User实体类对应数据库中存储用户相关信息的表结构包含如用户名、密码、用户角色等具体的属性字段代表了用户在系统中的实际数据存储形式在这里虽然没有直接看到对其进行复杂操作但在整个基于JWT的认证流程相关业务场景下用户信息是核心数据可能后续会有更多关联使用比如从JWT中解析出用户信息后与数据库中的用户实体进行比对验证等情况
import com.auth0.jwt.JWT;
// 引入Auth0提供的JWT库中的核心类JWT它提供了创建、解析、验证JSON Web TokenJWT等一系列相关操作的静态方法是在Java代码中操作JWT的关键入口类通过它可以方便地构建一个JWT令牌以及进行后续的验证、信息提取等操作。
import com.auth0.jwt.algorithms.Algorithm;
// 引入Auth0的JWT库中用于表示加密算法的类在这里指定了使用的JWT签名算法例如HS256HMAC-SHA256算法用于对JWT进行签名和验证签名的操作确保JWT的完整性和真实性不同的算法有不同的安全性和性能特点HS256是一种常用且较为安全的对称加密算法适用于很多场景。
import com.auth0.jwt.exceptions.JWTVerificationException;
// 引入Auth0的JWT库中表示JWT验证异常的类当对JWT进行验证如校验签名、检查过期时间等操作出现不符合预期的情况时比如签名不匹配、令牌已过期等就会抛出这个异常在代码中通过捕获这个异常来处理验证失败的情况并进行相应的日志记录和返回错误结果等操作保证程序的健壮性。
import com.auth0.jwt.interfaces.Claim;
// 引入Auth0的JWT库中表示JWT声明Claim的接口JWT中的声明是包含在令牌中的一些键值对信息用于携带如用户信息、权限信息等额外的数据通过这个接口可以方便地获取到声明中的具体值根据不同的数据类型进行相应的获取操作在从JWT中提取用户信息、权限列表等操作时会用到它来操作声明相关内容。
import com.auth0.jwt.interfaces.DecodedJWT;
// 引入Auth0的JWT库中表示已解码的JWT对象的接口当使用验证器JWTVerifier对JWT进行验证成功后会得到一个DecodedJWT对象通过这个对象可以方便地获取到JWT中的头部信息、声明信息以及其他相关的元数据等内容是进一步提取JWT中携带具体数据的关键对象在获取用户信息、权限等操作中都依赖它来获取相应的声明数据。
import com.auth0.jwt.interfaces.JWTVerifier;
// 引入Auth0的JWT库中用于构建JWT验证器的接口通过指定加密算法和密钥等信息可以创建一个JWTVerifier对象用于对传入的JWT令牌进行验证如检查签名是否正确、是否过期等确保接收到的JWT是合法有效的在验证token的相关方法中是核心操作对象用于执行验证逻辑。
import com.fasterxml.jackson.databind.ObjectMapper;
// 引入Jackson库的核心类ObjectMapper它提供了强大的功能用于将Java对象转换为JSON字符串序列化以及将JSON字符串转换为Java对象反序列化在这里虽然没有看到明显的序列化或反序列化操作但在更广泛的项目业务场景中可能涉及到对用户信息、权限信息等对象与JSON格式之间的转换操作比如从数据库获取用户信息后转换为JSON格式存储到JWT声明中或者从JWT声明中解析出JSON字符串后再反序列化为Java对象等情况当前类中通过依赖注入获取它以备可能的使用需求。
import jakarta.annotation.Resource;
// 引入Jakarta EE中的Resource注解用于进行资源注入在这里表示将一个ObjectMapper类型的对象注入到当前类中使得当前类可以使用这个已经配置好的ObjectMapper实例来进行JSON相关的操作如果有需要的话方便依赖注入符合Java EE或Spring等框架的开发模式减少对象创建和配置的繁琐过程。
import jakarta.servlet.http.HttpServletRequest;
// 引入用于处理HTTP请求的类它代表了客户端向服务器发送的请求包含了获取请求头、请求参数、请求路径等相关信息的方法在基于JWT的认证相关业务场景中可能会从请求中获取包含JWT令牌的请求头比如常见的Authorization头来进行后续的验证和信息提取操作不过在当前类中暂时未看到直接对它的使用只是在更广泛的项目与Web交互的场景下会涉及到它。
import lombok.SneakyThrows;
// 引入Lombok库提供的一个注解使用这个注解修饰的方法在抛出指定异常类型时编译器会自动帮我们处理异常例如将异常包装在运行时异常中抛出或者其他符合Lombok配置的处理方式在这里用于简化异常处理代码让代码看起来更简洁同时又能保证在出现异常时能有相应的处理机制例如在验证JWT、提取信息等操作出现异常时进行相应的处理避免代码中充斥大量的try-catch块。
import lombok.extern.slf4j.Slf4j;
// 引入Lombok库提供的日志相关注解使用@Slf4j注解后Lombok会在编译期自动为这个类生成一个名为“log”的日志记录对象方便在类中使用这个对象来记录各种运行时的信息如验证JWT失败、获取用户信息失败等情况的错误日志用于辅助排查问题、记录关键操作等无需手动去创建和配置日志记录相关的代码。
import org.springframework.beans.factory.annotation.Value;
// 引入Spring框架的用于注入配置属性值的注解在这里用于将配置文件通常是application.properties或application.yml等配置文件中名为“jwt.secret”的属性值注入到对应的变量secret这个属性值一般是JWT的密钥用于签名和验证JWT保证JWT的安全性通过这种方式方便从配置文件中获取关键的配置参数而无需硬编码在代码中便于配置的修改和管理。
import org.springframework.data.redis.core.RedisTemplate;
// 引入Spring Data Redis框架提供的用于操作Redis的模板类它简化了与Redis数据库进行各种数据类型交互的操作提供了诸如设置键值对、获取值、删除键等一系列便捷的方法不过在当前这个类中暂时未看到其实际使用可能在更广泛的项目与缓存、分布式会话等相关的基于JWT的业务场景中有作用比如将已验证的JWT存储到Redis中进行缓存后续快速验证等情况
import org.springframework.data.redis.core.ValueOperations;
// 引入Spring Data Redis框架中用于操作Redis中简单值类型对应Java中的String等简单类型的操作类它提供了针对Redis中值类型数据的获取、设置等更细化的操作方法同样在当前类中暂时未看到实际使用可能在基于Redis与JWT结合的业务场景中有具体操作需求比如从Redis中获取与JWT相关的缓存信息等情况
import org.springframework.stereotype.Component;
// 引入Spring框架的@Component注解用于将这个类标记为一个Spring组件使得Spring容器在进行组件扫描时能够发现并管理它便于后续在需要的地方通过依赖注入等方式使用该类对应的功能实现了组件化管理让这个类能更好地融入到整个Spring项目的体系架构中方便与其他Spring组件协作完成业务功能例如在其他业务逻辑类中需要创建JWT、验证JWT、获取其中信息等操作时可以方便地注入并调用这个类的相关方法。
import java.util.Date;
// 引入Java标准库中的Date类用于表示日期和时间信息在创建JWT时需要指定签发时间、过期时间等与时间相关的信息就使用这个类来创建对应的时间对象方便在JWT令牌中设置合适的时间戳以控制JWT的有效期等属性。
import java.util.HashMap;
// 引入Java标准库中的HashMap类用于创建键值对形式的集合对象在这里用于构建JWT的头部信息header通过向其中添加“alg”算法、“typ”类型等键值对来指定JWT的相关头部属性符合JWT的规范要求便于后续对JWT的正确处理和识别。
import java.util.List;
// 引入Java标准库中的List接口用于表示列表类型的数据结构在这里用于表示用户的权限列表authList因为用户可能拥有多个权限所以用列表来存储这些权限信息在创建JWT时将权限列表作为声明信息添加到JWT中以及后续从JWT中提取权限列表信息时会用到这个数据结构来传递和处理相关数据。
import java.util.Map;
// 再次引入Java标准库中的Map接口和前面的HashMap类似这里主要是为了通用性在代码中可以根据需要使用不同的Map实现类来构建各种键值对形式的数据集合比如构建JWT头部信息、添加自定义声明等操作都会用到Map结构来组织数据。
/**
* @author WeiJin
* @version 1.0
*/
@SuppressWarnings("all")
// 使用这个注解来抑制所有的编译器警告,一般不建议在正式代码中这样使用,因为可能会掩盖一些潜在的代码问题,但在某些特定情况下(比如暂时不确定某些警告是否会影响功能,或者需要快速忽略一些已知但暂时不想处理的警告等)可能会这样做,这里可能需要后续根据实际情况进一步优化,查看并处理那些被抑制的警告信息。
@Component
// 将该类标记为Spring组件意味着Spring容器会对其进行管理其他类可以通过Spring的依赖注入机制获取这个类的实例进而调用类中定义的创建JWT、验证JWT、获取JWT中相关信息等方法来完成基于JWT的认证和信息提取相关业务操作这样有助于降低代码之间的耦合度增强代码的可维护性和可扩展性使得整个项目的代码结构更加清晰合理方便在不同的业务场景中灵活运用该类提供的JWT相关功能。
@Slf4j
// 使用@Slf4j注解让Lombok在编译期自动为这个类生成名为“log”的日志记录对象以便在类中可以方便地使用这个对象记录各类运行时的日志信息例如在验证JWT失败、获取用户信息或权限信息失败等情况下通过记录错误日志来帮助开发人员排查问题、了解操作执行情况等便于后续的调试和监控工作。
public class JwtUtil {
@Resource
// 使用@Resource注解进行资源注入告诉Spring容器查找并注入一个ObjectMapper类型的实例到当前类的这个字段中使得本类可以使用这个注入的ObjectMapper对象来进行JSON相关的操作如果后续有需要的话比如对用户信息等进行序列化或反序列化操作等情况这种依赖注入的方式符合Spring框架的开发规范方便获取和使用其他组件提供的功能减少了手动创建和配置对象的麻烦。
private ObjectMapper objectMapper;
/**
* jwt
*/
@Value("${jwt.secret}")
// 使用@Value注解将配置文件通常是application.properties或application.yml等配置文件中名为“jwt.secret”的属性值注入到这个变量中这个属性值充当JWT的密钥用于在创建JWT时进行签名操作以及验证JWT时验证签名是否正确密钥的保密性对于JWT的安全性至关重要通过从配置文件获取的方式方便对密钥进行管理和修改避免硬编码在代码中带来的安全风险。
private String secret;
/**
* jwt
*
* @return
* @return JWTJWT
*/
public String createJwt(String userInfo, List<String> authList) {
Date issDate = new Date();//签发时间
Date expireDate = new Date(issDate.getTime() + 1000 * 60 *30);
//定义头部信息
// 创建一个Date对象表示JWT的签发时间即当前时间使用Java标准库中的Date类的无参构造函数来获取当前系统时间对应的Date对象这个时间会记录在JWT中用于后续验证等操作时判断JWT是否在有效期内等情况例如可以检查签发时间到当前时间是否超过了允许的时间范围等。
Date issDate = new Date();
// 计算JWT的过期时间通过获取签发时间issDate的时间戳getTime方法返回从1970年1月1日00:00:00 UTC到当前时间的毫秒数并在此基础上加上一定的时间间隔这里是1000 * 60 * 30毫秒即30分钟来确定过期时间意味着创建的这个JWT令牌在30分钟后将会过期无法再被正常验证和使用这样可以控制JWT的有效时长提高安全性避免令牌长时间有效带来的潜在风险比如被窃取后可长时间使用等情况
Date expireDate = new Date(issDate.getTime() + 1000 * 60 * 30);
// 定义头部信息创建一个HashMap对象来存储JWT头部的相关声明Claims头部信息主要用于指定JWT使用的算法、类型等元数据信息符合JWT的规范要求方便接收方正确识别和处理接收到的JWT令牌。
Map<String, Object> headerClaims = new HashMap<>();
headerClaims.put("alg", "HS256");//算法
headerClaims.put("typ", "JWT");//类型只能是jwt
// 在头部信息的Map中添加一个键值对指定JWT使用的加密算法为“HS256”HMAC-SHA256算法这个算法信息会被包含在JWT头部接收方如服务器端验证时可以根据这个算法来验证JWT的签名是否正确确保JWT的完整性和真实性HS256是一种常用且安全性较高的对称加密算法适用于很多场景。
headerClaims.put("alg", "HS256");
// 在头部信息的Map中添加另一个键值对指定JWT的类型为“JWT”这是固定的标识用于表明这个令牌是符合JSON Web Token规范的方便接收方进行识别和按照相应的规范进行处理确保对JWT的正确解析和验证等操作。
headerClaims.put("typ", "JWT");
// 使用Auth0的JWT库提供的静态创建方法JWT.create()开始构建JWT令牌链式调用一系列的“with”方法来依次添加JWT的各个部分信息如头部信息、签发人、签发时间、过期时间、自定义声明等内容最后通过sign方法使用指定的算法和密钥进行签名生成最终完整的、可用于认证的JWT令牌字符串。
return JWT.create().withHeader(headerClaims)
.withIssuer("wj")//签发人
.withIssuedAt(issDate)//签发时间
.withExpiresAt(expireDate)//过期时间
.withClaim("userInfo", userInfo)//自定义声明
// 指定JWT的签发人Issuer这里简单地设置为“wj”在实际业务中可以根据具体的系统、业务模块等来确定合适的签发人标识签发人信息可以用于在验证等操作中进一步确认JWT的来源合法性等情况例如可以限制只接受特定签发人签发的JWT令牌。
.withIssuer("wj")
// 设置JWT的签发时间Issued At为前面创建的issDate对象所表示的时间将这个时间信息添加到JWT中方便后续验证等操作时参考判断JWT是否在合理的时间范围内有效比如是否过早签发或者已经过期等情况。
.withIssuedAt(issDate)
// 设置JWT的过期时间Expires At为前面计算得到的expireDate对象所表示的时间这样JWT在达到这个过期时间后再进行验证时就会被判定为无效通过这种方式来控制JWT的有效期保障系统的安全性避免令牌长期有效带来的安全隐患。
.withExpiresAt(expireDate)
// 添加一个自定义声明Claim键为“userInfo”值为传入的userInfo字符串这个自定义声明可以用于携带用户的相关信息比如用户名、用户ID等具体信息具体内容根据业务需求而定方便在后续验证通过后从JWT中提取出这些用户信息用于业务逻辑处理例如获取当前登录用户的详细信息等情况。
.withClaim("userInfo", userInfo)
// 再添加一个自定义声明键为“authList”值为传入的authList列表用于携带用户的权限列表信息例如用户具有的角色、操作权限等这样在后续验证JWT成功后可以直接从JWT中获取用户的权限信息便于进行权限判断和业务流程控制决定用户是否有权限执行某些操作等情况。
.withClaim("authList", authList)
// 使用指定的算法Algorithm.HMAC256和密钥secret变量所存储的从配置文件获取的密钥值对构建好的JWT进行签名操作签名后的JWT才是完整有效的接收方可以通过同样的算法和密钥来验证签名是否正确以此确保JWT在传输过程中没有被篡改保证其完整性和真实性最终返回生成的完整的JWT令牌字符串供后续使用如返回给
// 使用HS256算法通过Algorithm.HMAC256指定以及前面通过配置文件注入的密钥secret变量对构建好的JWT进行签名操作。
// 签名的作用是保证JWT的完整性和真实性它会基于JWT的头部、载荷包含各种声明信息以及密钥通过指定的算法生成一个签名值附加到JWT末尾。
// 在后续验证JWT时接收方会使用相同的算法和密钥重新计算签名并与接收到的JWT中的签名进行比对如果一致则说明JWT在传输过程中没有被篡改是完整且可信的最终返回生成的完整的JWT令牌字符串供后续使用比如返回给客户端保存客户端后续请求时携带该令牌用于身份认证等操作
.sign(Algorithm.HMAC256(secret));//使用HS256作为签名SECRET作为密钥
}
/**
* token
*
* @param token
* @return
* JWTtokentruefalse便访
* @param token JWTAuthorization
* @return truefalse
*/
public boolean verifyToken(String token) {
//构建jwt校验器
// 构建jwt校验器通过JWT.require方法并传入使用HS256算法Algorithm.HMAC256和前面获取到的密钥secret来创建一个JWTVerifier对象。
// JWTVerifier对象是用于验证JWT的核心工具它内部封装了验证所需的算法、密钥等信息后续可以使用它对传入的JWT令牌进行验证操作确保接收到的JWT是合法有效的符合预期的签名和有效期等要求。
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret)).build();
try {
// 使用构建好的JWTVerifier对象的verify方法对传入的JWT令牌token参数进行验证该方法会按照内部封装的算法、密钥以及其他验证规则比如检查是否过期等来检查JWT的合法性。
// 如果验证通过意味着JWT的签名正确、未过期且符合其他相关验证条件此时方法正常执行完毕直接返回true表示这个JWT令牌是有效的可以继续后续的业务操作比如获取其中的用户信息、权限信息等
verifier.verify(token);
return true;
} catch (JWTVerificationException e) {
// 如果在验证过程中出现问题例如签名不匹配可能被篡改了、JWT已过期或者其他不符合验证规则的情况就会抛出JWTVerificationException异常。
// 在这里捕获这个异常并通过日志记录使用log对象它是由@Slf4j注解自动生成的用于记录运行时的相关信息输出“校验失败”的错误信息方便后续排查问题了解验证失败的情况然后返回false表示这个JWT令牌验证不通过业务逻辑中可以根据这个返回值进行相应处理比如拒绝客户端的请求等
log.error("校验失败");
return false;
}
@ -84,17 +148,21 @@ public class JwtUtil {
/**
* token
*
* @param token
* @return
* JWTtokenJWTwithClaim("userInfo", userInfo)便
* @param token JWTverifyToken
* @return JWTJWTnull便
*/
public String getUser(String token) {
//构建jwt校验器
// 构建jwt校验器和前面verifyToken方法中一样通过JWT.require方法并传入使用HS256算法Algorithm.HMAC256和密钥secret来创建一个JWTVerifier对象用于后续验证传入的JWT令牌的合法性只有验证通过的JWT才能安全地从中提取信息。
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret)).build();
try {
// 使用构建好的JWTVerifier对象对传入的JWT令牌token参数进行验证验证通过后会返回一个DecodedJWT对象该对象代表了解码后的JWT包含了JWT中的头部信息、各种声明信息等内容是进一步提取具体数据如用户信息、权限信息等的基础对象。
DecodedJWT jwt = verifier.verify(token);
// 通过获取到的DecodedJWT对象的getClaim方法并传入声明的键名"userInfo"对应创建JWT时添加用户信息声明的键获取到对应的声明对象Claim类型再调用asString方法将其转换为字符串类型从而得到之前添加到JWT中的用户信息字符串最终将其返回以便在业务逻辑中使用获取到的用户信息进行后续操作。
return jwt.getClaim("userInfo").asString();
} catch (JWTVerificationException e) {
// 如果在验证JWT或者提取用户信息的过程中出现问题例如JWT验证失败、JWT中不存在名为"userInfo"的声明或者声明内容格式不符合预期等情况就会抛出JWTVerificationException异常。
// 在这里捕获这个异常并通过日志记录使用log对象输出“用户获取失败”的错误信息方便后续排查问题了解获取用户信息失败的情况然后返回null表示没有成功获取到用户信息调用者可以根据这个返回值进行相应处理比如提示用户重新登录等操作
log.error("用户获取失败");
return null;
}
@ -102,20 +170,24 @@ public class JwtUtil {
/**
* token
*
* @param token
* @return
* JWTtokenJWTwithClaim("authList", authList)List<String>便访
* @param token JWTverifyToken
* @return JWTList<String>JWTnull便
*/
public List<String> getAuthList(String token) {
//构建jwt校验器
// 构建jwt校验器再次通过JWT.require方法并传入使用HS256算法Algorithm.HMAC256和密钥secret来创建一个JWTVerifier对象用于对传入的JWT令牌进行验证只有验证通过的JWT才能从中正确提取权限信息。
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret)).build();
try {
// 使用构建好的JWTVerifier对象对传入的JWT令牌token参数进行验证验证通过后得到一个DecodedJWT对象它包含了JWT中的各种信息是进一步提取权限信息的基础对象。
DecodedJWT jwt = verifier.verify(token);
// 通过获取到的DecodedJWT对象的getClaim方法并传入声明的键名"authList"对应创建JWT时添加权限列表声明的键获取到对应的声明对象Claim类型再调用asList方法并指定返回的类型为String.class将声明内容转换为字符串列表类型List<String>从而得到之前添加到JWT中的权限列表信息最终将其返回以便在业务逻辑中依据获取到的权限列表进行后续的权限判断等操作。
return jwt.getClaim("authList").asList(String.class);
} catch (JWTVerificationException e) {
log.error("权限列表+获取失败");
// 如果在验证JWT或者提取权限信息的过程中出现问题例如JWT验证失败、JWT中不存在名为"authList"的声明或者声明内容格式不符合预期等情况就会抛出JWTVerificationException异常。
// 在这里捕获这个异常并通过日志记录使用log对象输出“权限列表获取失败”的错误信息方便后续排查问题了解获取权限信息失败的情况然后返回null表示没有成功获取到权限信息调用者可以根据这个返回值进行相应处理比如提示用户权限不足等操作
log.error("权限列表获取失败");
return null;
}
}
}
}

@ -1,11 +1,19 @@
package cn.org.alan.exam.util;
import jakarta.annotation.Resource;
// 引入Jakarta EE中的Resource注解用于进行资源注入在这里的作用是告诉Spring容器去查找并注入一个符合要求的对象在本类中是StringRedisTemplate类型的对象到对应的字段中使得当前类可以使用这个已经配置好的对象来与Redis进行交互操作符合依赖注入的开发模式方便获取和使用其他组件提供的功能。
import org.springframework.data.redis.core.StringRedisTemplate;
// 引入Spring Data Redis框架提供的用于操作Redis的模板类它简化了与Redis数据库进行字符串类型数据交互的操作提供了诸如设置键值对、获取值、删除键等一系列便捷的方法在本类中用于执行基于Lua脚本的删除操作通过它可以方便地将Lua脚本发送到Redis服务器执行并获取执行结果是与Redis交互的核心工具类。
import org.springframework.data.redis.core.script.DefaultRedisScript;
// 引入Spring Data Redis框架中用于表示默认Redis脚本的类它可以将一段Lua脚本代码进行封装并指定该脚本执行后返回的数据类型在本案例中指定返回Long类型方便后续通过StringRedisTemplate执行这个脚本使得在Java代码中能够方便地利用Redis支持的Lua脚本功能来实现一些复杂的业务逻辑如批量删除符合特定条件的键等操作
import org.springframework.stereotype.Component;
// 引入Spring框架的@Component注解用于将这个类标记为一个Spring组件使得Spring容器在进行组件扫描时能够发现并管理它便于后续在需要的地方通过依赖注入等方式使用该类对应的功能实现了组件化管理让这个类能更好地融入到整个Spring项目的体系架构中方便与其他Spring组件协作完成业务功能例如在其他业务逻辑类中需要对Redis中的键进行批量删除操作时可以方便地注入并调用这个类的相关方法。
import java.util.Collections;
// 引入Java标准库中的Collections类主要用于获取不可变的集合对象在这里使用它的singletonList方法来创建一个只包含单个元素即要传递给Lua脚本的键前缀参数的不可变列表用于在执行Redis脚本时传递参数满足StringRedisTemplate执行脚本时对参数列表的要求。
/**
* @Author Alan
@ -13,14 +21,29 @@ import java.util.Collections;
* @Date 2024/6/10 9:40 AM
*/
@Component
// 将该类标记为Spring组件意味着Spring容器会对其进行管理其他类可以通过Spring的依赖注入机制获取这个类的实例进而调用类中定义的deleteKeysByPrefix方法来完成根据指定前缀删除Redis中相关键的操作这样有助于降低代码之间的耦合度增强代码的可维护性和可扩展性使得整个项目的代码结构更加清晰合理方便在不同的业务场景中灵活运用该类提供的Redis操作功能。
public class RedisUtils {
@Resource
// 使用@Resource注解进行资源注入告诉Spring容器查找并注入一个StringRedisTemplate类型的实例到当前类的这个字段中使得本类可以使用这个注入的StringRedisTemplate对象来与Redis进行交互比如发送脚本执行命令、获取执行结果等操作这种依赖注入的方式遵循Spring框架的开发规范方便获取和使用其他组件提供的功能无需手动去创建和配置相关的Redis操作对象。
private StringRedisTemplate stringRedisTemplate;
/**
* 使LuaKey
* @param keyPrefix "cache:getNewNotice:"
* @param keyPrefix "cache:getNewNotice:"便Redis
*/
public void deleteKeysByPrefix(String keyPrefix) {
// 定义一个包含Lua脚本代码的字符串这段Lua脚本用于实现根据指定前缀删除Redis中相关键的功能。
// 以下是对脚本各部分的详细解释:
// "local keys = redis.call('SCAN', 0, 'MATCH', ARGV[1]..'*', 'COUNT', 1000)"
// 使用Redis的SCAN命令来迭代查找所有匹配特定模式的键。
// 'SCAN'是命令本身0表示从游标位置0开始扫描游标用于分页式地获取结果初始为0后续可根据返回的游标继续获取下一批结果但这里简单地从0开始一次获取
// 'MATCH'表示匹配模式ARGV[1]表示接收的第一个参数在Java代码中传递进来的键前缀并通过'..'操作符将其与'*'连接,意味着匹配以传入的键前缀开头的所有键('*'是通配符,表示任意字符序列)。
// 'COUNT' 1000表示每次扫描最多返回1000个键这是控制每次获取匹配键数量的一个参数避免一次获取过多数据导致内存等问题。
// "local count = 0"在Lua脚本内部定义一个变量count初始化为0用于记录后续删除操作实际删除的键的数量。
// "for i=1,#keys[2],1 do \n" + " redis.call('DEL', keys[2][i]) \n" + " count = count + 1 \n" + "end \n"
// 这是一个循环结构遍历通过SCAN命令获取到的匹配键列表keys[2]表示获取到的结果中的第二个元素即键列表在SCAN命令返回的结果格式中第一个元素是游标第二个元素是键列表
// 对于每个匹配的键使用Redis的DEL命令来删除它并且每删除一个键就将count变量的值加1用于统计删除的键的总数。
// "return count"最后返回实际删除的键的数量以便在Java代码中可以获取到这个删除计数信息了解操作的执行情况。
String luaScript = "local keys = redis.call('SCAN', 0, 'MATCH', ARGV[1]..'*', 'COUNT', 1000)\n" +
"local count = 0\n" +
"for i=1,#keys[2],1 do \n" +
@ -29,10 +52,18 @@ public class RedisUtils {
"end \n" +
"return count";
// 创建一个DefaultRedisScript对象将前面定义的Lua脚本字符串以及指定脚本执行后返回的数据类型Long类型用于接收删除键的数量统计结果传入构造函数对要在Redis中执行的Lua脚本进行封装以便后续通过StringRedisTemplate来执行这个脚本。
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
// 执行Lua脚本并传递前缀作为参数
// 执行Lua脚本并传递前缀作为参数通过StringRedisTemplate的execute方法来实现。
// 参数说明:
// 第一个参数是封装好的DefaultRedisScript对象redisScript它包含了要执行的Lua脚本以及返回值类型信息告诉StringRedisTemplate要执行的具体脚本内容和期望的返回结果类型。
// 第二个参数是传递给脚本的参数列表这里使用Collections.singletonList方法创建了一个只包含单个元素即要删除的键的前缀keyPrefix的不可变列表在Lua脚本中可以通过ARGV[1]来获取这个参数,用于构建匹配键的模式等操作。
// 第三个参数也是键前缀keyPrefix这里的重复传递可能是因为在某些特定的Spring Data Redis内部实现机制或者与Redis交互的场景下需要以这种方式来确保参数能正确传递和被脚本使用具体取决于框架的设计要求。
// 执行该方法后会将Lua脚本发送到Redis服务器执行并返回一个Long类型的结果即删除的键的数量将其存储在deletedCount变量中用于后续了解本次批量删除操作的执行情况。
Long deletedCount = stringRedisTemplate.execute(redisScript, Collections.singletonList(keyPrefix), keyPrefix);
// 打印一条信息到控制台,展示本次根据指定前缀删除键的操作中,实际删除的键的数量以及对应的键前缀,方便在开发、测试或者查看日志时了解操作执行的结果情况,进行相关的调试和监控工作。
System.out.println("Deleted " + deletedCount + " keys with prefix: " + keyPrefix);
}
}
}

@ -1,17 +1,33 @@
package cn.org.alan.exam.util;
//在util的包里
import cn.org.alan.exam.common.result.Result;
// 引入Result类从名称推测它可能是用于封装操作结果的一个通用类包含了如操作是否成功的标识、返回的数据、错误信息等相关属性用于在不同业务逻辑处理后统一返回结果给客户端等调用者是本工具类处理响应时要进行序列化并返回的数据对象类型。
import com.fasterxml.jackson.core.JsonProcessingException;
// 引入Jackson库中表示JSON处理异常的类当使用Jackson的ObjectMapper进行对象到JSON字符串的转换过程中如果出现无法正确处理比如对象的某些属性无法按照JSON规范序列化等情况就会抛出这个异常在本类的方法中需要捕获并处理这个异常以保证程序的健壮性。
import com.fasterxml.jackson.databind.ObjectMapper;
// 引入Jackson库的核心类ObjectMapper它提供了强大的功能用于将Java对象转换为JSON字符串序列化以及将JSON字符串转换为Java对象反序列化在这里主要用于将Result类型的对象序列化为JSON格式的字符串以便后续将其作为响应内容返回给客户端。
import jakarta.annotation.Resource;
// 引入Jakarta EE中的Resource注解用于进行资源注入在这里表示将一个ObjectMapper类型的对象注入到当前类中使得当前类可以使用这个已经配置好的ObjectMapper实例来进行JSON相关的操作方便依赖注入符合Java EE或Spring等框架的开发模式。
import jakarta.servlet.http.HttpServletResponse;
// 引入用于处理HTTP响应的类它代表了服务器对客户端请求做出的响应包含了设置响应头、响应状态码、输出响应内容等相关方法在本类中就是通过它来设置响应的编码格式、内容类型以及输出序列化后的结果数据向客户端返回正确的响应信息。
import lombok.SneakyThrows;
// 引入Lombok库提供的一个注解使用这个注解修饰的方法在抛出指定异常类型时编译器会自动帮我们处理异常例如将异常包装在运行时异常中抛出或者其他符合Lombok配置的处理方式在这里用于简化异常处理代码让代码看起来更简洁同时又能保证在出现异常时能有相应的处理机制。
import org.springframework.stereotype.Component;
// 引入Spring框架的@Component注解用于将这个类标记为一个Spring组件使得Spring容器在进行组件扫描时能够发现并管理它便于后续在需要的地方通过依赖注入等方式使用该类对应的功能实现了组件化管理让这个类能更好地融入到整个Spring项目的体系架构中方便与其他Spring组件协作完成业务功能。
import java.io.IOException;
import java.io.PrintWriter;
// 引入Java标准库中表示输入输出异常的类因为在操作HttpServletResponse的输出流如获取Writer对象以及向其写入内容等操作过程中可能会出现IO相关的异常情况所以需要捕获并处理这个异常保证程序的稳定性。
import java.io.PrintWriter;
// 引入用于向字符输出流写入文本内容的类在这里通过从HttpServletResponse获取对应的PrintWriter对象然后使用它将序列化后的JSON格式的结果数据写入到响应中从而将数据返回给客户端。
/**
* @Author WeiJin
@ -19,19 +35,35 @@ import java.io.PrintWriter;
* @Date 2024/3/25 18:42
*/
@Component
// 将该类标记为Spring组件意味着Spring容器会对其进行管理其他类可以通过Spring的依赖注入机制获取这个类的实例进而调用类中定义的response方法来完成向客户端返回响应结果的操作这样有助于降低代码之间的耦合度增强代码的可维护性和可扩展性使得整个项目的代码结构更加清晰合理方便在不同的业务场景中灵活运用该类提供的响应处理功能。
public class ResponseUtil {
@Resource
// 使用@Resource注解进行资源注入告诉Spring容器查找并注入一个ObjectMapper类型的实例到当前类的这个字段中使得本类可以使用这个注入的ObjectMapper对象来执行对象到JSON字符串的序列化操作这种依赖注入的方式符合Spring框架的开发规范方便获取和使用其他组件提供的功能。
private ObjectMapper objectMapper;
@SneakyThrows({JsonProcessingException.class, IOException.class})
// 使用@SneakyThrows注解来简化异常处理这里指定了要自动处理的异常类型为JsonProcessingExceptionJackson序列化对象时可能出现的异常和IOException在操作响应输出流时可能出现的IO异常编译器会按照Lombok的配置帮助我们处理这些异常让代码更简洁避免了手动编写大量的try-catch块来处理这些异常但实际上在运行时出现异常时依然会有相应的处理逻辑来保证程序不会因为未处理的异常而崩溃。
public void response(HttpServletResponse response, Result result) {
// 使用注入的ObjectMapper对象的writeValueAsString方法将传入的Result类型的对象转换为JSON格式的字符串这个方法会根据Java对象的属性以及Jackson的配置如序列化规则、日期格式等来生成对应的JSON字符串表示以便后续可以将其作为响应内容返回给客户端让客户端能够解析并获取到操作结果相关的信息。
String s = objectMapper.writeValueAsString(result);
// 设置HttpServletResponse对象的字符编码为"UTF-8"确保返回给客户端的响应内容这里是JSON字符串能以正确的字符编码进行传输和解析避免出现中文乱码等字符编码相关的问题因为UTF-8是一种广泛支持且通用的字符编码格式适合在网络传输中使用。
response.setCharacterEncoding("UTF-8");
// 设置HttpServletResponse对象的内容类型为"application/json;charset=utf-8"明确告诉客户端返回的响应数据是JSON格式的并且字符编码是UTF-8这样客户端就能按照正确的格式和编码来解析接收到的响应内容符合HTTP协议中关于响应内容类型和编码声明的规范。
response.setContentType("application/json;charset=utf-8");
// 通过HttpServletResponse对象获取一个PrintWriter对象这个对象用于向HTTP响应的输出流中写入字符数据也就是后续可以使用它将前面序列化得到的JSON字符串写入到响应中以便将数据发送回客户端它是向客户端返回响应内容的关键操作对象。
PrintWriter writer = response.getWriter();
// 使用获取到的PrintWriter对象的write方法将序列化后的JSON字符串变量s所存储的内容写入到HTTP响应的输出流中这样客户端在接收到响应后就能获取到对应的JSON格式的结果数据完成服务器向客户端返回操作结果的核心操作。
writer.write(s);
// 调用PrintWriter对象的flush方法强制将缓冲区中的数据立即输出到客户端确保数据能及时、完整地发送给客户端避免数据因为缓冲区未及时刷新而延迟或丢失的情况发生保证响应数据的完整性和及时性。
writer.flush();
// 调用PrintWriter对象的close方法关闭输出流释放相关的系统资源完成整个向客户端返回响应内容的操作流程同时避免资源泄漏等问题确保程序的资源使用合理且高效。
writer.close();
}
}
}

@ -1,10 +1,20 @@
package cn.org.alan.exam.util;
// 声明该类所在的包名用于在项目中对类进行分类组织表明此工具类属于特定的“exam”项目下的工具类功能模块所在的包方便代码管理与维护使代码按照功能模块清晰划分便于后续开发人员查找、理解和扩展相关功能代码。
import org.apache.tomcat.util.codec.binary.Base64;
// 引入Apache Tomcat提供的Base64编解码工具类这里使用的是其特定的二进制编解码相关的实现用于对字节数组形式的数据进行Base64编码加密结果转换为Base64字符串方便传输和存储等以及解码将接收到的Base64编码数据还原为字节数组以便后续解密操作操作在加密和解密方法中会用到它来处理数据的格式转换。
import org.springframework.stereotype.Component;
// 引入Spring框架的@Component注解用于将这个类标记为一个Spring组件使得Spring容器在进行组件扫描时能够发现并管理它便于后续在需要的地方通过依赖注入等方式使用该类对应的功能实现了组件化管理让这个类能更好地融入到整个Spring项目的体系架构中方便与其他Spring组件协作完成业务功能例如在其他业务逻辑类中需要进行数据加密或解密操作时可以方便地注入并调用这个类的相关加密、解密方法。
import javax.crypto.Cipher;
// 引入Java加密扩展Java Cryptography ExtensionJCE框架中的Cipher类它是用于进行加密和解密操作的核心类通过指定不同的算法、模式以及初始化相关参数等可以利用它来实现对数据的具体加密和解密处理在本类的加密、解密方法中是关键操作类。
import javax.crypto.spec.IvParameterSpec;
// 引入JCE框架中的IvParameterSpec类它用于指定加密算法中使用的初始化向量Initialization VectorIV参数初始化向量在一些加密模式如这里使用的CBC模式下是必需的用于增加加密的安全性和随机性确保每次加密即使对于相同的明文数据也能产生不同的密文结果结合不同的IV在初始化Cipher对象进行加密和解密操作时会用到它来设置相应参数。
import javax.crypto.spec.SecretKeySpec;
// 引入JCE框架中的SecretKeySpec类它用于根据给定的字节数组形式的密钥数据和算法名称创建一个秘密密钥Secret Key对象在本类中就是依据指定的加密密钥key来创建用于加密和解密操作的AES算法对应的密钥对象为Cipher对象的初始化提供必要的密钥参数以便后续进行符合要求的加密和解密操作。
/**
* @Author Alan
@ -12,91 +22,163 @@ import javax.crypto.spec.SecretKeySpec;
* @Date 2024/6/8 9:57 AM
*/
@Component
// 将该类标记为Spring组件意味着Spring容器会对其进行管理其他类可以通过Spring的依赖注入机制获取这个类的实例进而调用类中定义的加密、解密方法来完成相应的数据处理操作这样有助于降低代码之间的耦合度增强代码的可维护性和可扩展性使得整个项目的代码结构更加清晰合理方便在不同的业务场景中灵活运用该类提供的加密、解密功能。
public class SecretUtils {
/***
* keyiv
*/
private static String KEY = "63eeac68cf074c8c";
// 定义一个静态的字符串变量KEY用于存储加密和解密操作所使用的密钥Key这里简单地初始化为一个固定值但按照注释说明实际应用中它可以随机生成以提高加密的安全性不同的密钥会导致相同明文加密出不同的密文当前固定值仅为示例或者临时方便测试使用在生产环境应该采用更安全的随机生成及妥善保管方式。
private static String IV = "63eeac68cf074c8c";
// 定义一个静态的字符串变量IV用于存储加密算法中使用的初始化向量Initialization VectorIV同样这里初始化为一个固定值实际也可随机生成它在特定加密模式如AES的CBC模式下配合密钥一起使用增加加密结果的随机性和安全性避免相同明文每次加密结果都一样当前固定值也是出于类似测试等方便考虑正式场景需改进。
/***
*
* @param data
* @return encrypt
* @param data 便
* @return encryptBase64便使Base64使
*/
public static String encrypt(String data){
// 调用了重载的encrypt方法并传入要加密的数据以及默认的密钥KEY和初始化向量IV将加密操作委托给另一个带有完整参数的encrypt方法来执行这样做可以在外部调用时更方便只需传入要加密的数据即可而内部统一使用完整参数的方法来处理具体加密逻辑提高代码复用性和可维护性。
return encrypt(data, KEY, IV);
}
/***
* param data
* desEncrypt
* param data
* desEncryptdesEncryptKEYIVdesEncryptencrypt便
*/
public static String desEncrypt(String data){
// 该方法用于对传入的加密数据(以字符串形式传入,通常是经过之前加密操作得到的密文数据,例如从数据库中取出的已加密存储的信息或者从网络接收的加密后的数据等)进行解密操作。
// 不过此方法并没有直接进行解密的具体逻辑处理,而是调用了另一个重载的`desEncrypt`方法,并将接收到的要解密的数据(`data`)以及类中预先定义好的默认密钥(`KEY`)和默认初始化向量(`IV`)作为参数传递过去,
// 这样的设计使得外部调用更加简洁方便,只需传入要解密的数据即可,而具体的解密逻辑统一在重载的方法中处理,同时也方便在需要的时候,能以更灵活的方式(如果传入不同的密钥和向量参数)调用那个重载方法来满足多样化的解密需求。
return desEncrypt(data, KEY, IV);
}
/**
*
* @param data
* @param key key
* @param iv iv
* @return
* @param data
* @param key key使KEY使KEY
* @param iv iv使IV
* @return Base64便使
*/
private static String encrypt(String data, String key, String iv){
private static String encrypt(String data, String key, String iv) {
try {
//"算法/模式/补码方式"NoPadding PkcsPadding
// 通过Cipher类的getInstance方法按照指定的"算法/模式/补码方式"字符串来获取一个Cipher实例
// 此处指定的是"AES/CBC/NoPadding"意味着选用高级加密标准AES算法它是一种对称加密算法具有较高的安全性广泛应用于数据加密场景。
// 密码分组链接Cipher Block ChainingCBC模式在此模式下每个明文块在加密前会先与前一个密文块进行异或操作这样可以增加加密的随机性和安全性避免相同的明文块加密后得到相同的密文块情况出现。
// 无填充NoPadding方式表示不对数据进行额外的填充处理不过这要求输入的数据长度需要符合加密算法数据块大小的整数倍要求不同的选择会对加密的特性、安全性以及对输入数据的处理要求等方面产生影响当前这种组合是基于项目的具体需求来确定的比如综合考虑了对数据长度兼容性、整体安全性等多方面因素。
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
// 获取当前所选加密算法AES在CBC模式下中每个数据块的大小单位为字节不同的加密算法以及不同的工作模式下数据块大小可能各不相同
// 知晓这个块大小对于后续处理明文数据长度非常关键,因为要依据它来判断是否需要对明文数据进行长度调整,例如在需要满足特定长度规则(像这里要求是块大小整数倍)的情况下,进行相应的填充或其他处理操作,以确保输入的数据符合加密算法的要求。
int blockSize = cipher.getBlockSize();
// 将传入的要加密的原始明文数据(字符串类型)转换为字节数组形式,方便后续在加密操作中按照字节级别进行处理,
// 这个字节数组的长度就代表了原始明文数据的实际长度(以字节为单位),是后续判断是否需要对数据长度进行调整的基础依据。
byte[] dataBytes = data.getBytes();
// 获取刚才转换得到的明文字节数组的长度,也就是原始明文数据以字节为单位计量的长度值,
// 这个长度值将用于和加密算法要求的数据块大小进行比较,以确定是否需要对明文数据进行额外的处理(如填充操作),来使其满足加密算法对于输入数据长度的规定条件。
int plaintextLength = dataBytes.length;
if (plaintextLength % blockSize != 0) {
// 判断明文字节数组的长度是否不是加密算法数据块大小的整数倍,若不是整数倍,意味着不符合当前加密算法(采用的无填充方式下)对输入数据长度的要求,
// 此时需要进行长度调整,计算出需要补充的字节数,使总长度变为块大小的整数倍,为后续能顺利进行加密操作做准备。
if (plaintextLength % blockSize!= 0) {
// 通过计算加密算法数据块大小与当前明文长度除以块大小所得余数的差值,来确定需要补充的字节数,
// 例如块大小为16字节当前明文长度为18字节18除以16余数为2那么需要补充的字节数就是16 - 2 = 14字节将这个差值加到当前明文长度上就得到了符合要求的调整后长度。
plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
}
// 创建一个新的字节数组,其长度为经过上述调整后的长度(已确保是加密算法数据块大小的整数倍),用于存放处理后的明文数据,
// 新字节数组在创建时其元素初始值会按照字节数组的默认初始化规则被填充通常为0等默认值后续会把原始的明文字节数据复制到这个新数组中若有不够的部分根据前面计算补充的字节数情况按照加密要求这里是NoPadding方式实际可能不需要额外填充具体有意义的内容但长度要符合块大小整数倍要求来处理。
byte[] plaintext = new byte[plaintextLength];
// 使用System.arraycopy方法将原始的明文字节数组dataBytes中的数据复制到新创建的用于存放处理后明文数据的字节数组plaintext
// 复制的起始位置都是从索引0开始复制的字节数量为原始明文字节数组的长度即复制全部已有的原始明文数据内容确保原始明文数据能正确放置到新数组中准备后续的加密操作。
System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
// 根据传入的加密密钥字符串key先将其转换为字节数组形式然后使用"AES"算法名称创建一个SecretKeySpec对象
// 这个SecretKeySpec对象代表了AES加密算法实际所使用的密钥它在后续初始化Cipher对象进行加密操作时会被指定使用以此来确保加密过程是使用正确的密钥对数据进行处理密钥的准确性对于加密结果能否正确解密以及整体安全性至关重要。
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
// 根据传入的初始化向量字符串iv将其转换为字节数组后创建一个IvParameterSpec对象
// 初始化向量Initialization VectorIV在密码分组链接CBC模式下是必需的它用于增加加密的随机性和安全性使得每次即使对相同的明文进行加密只要初始化向量不同得到的密文结果也会不同在初始化Cipher对象时会使用这个对象来设置相应的初始化向量参数与密钥一起共同参与到加密操作当中。
IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
// 使用前面创建好的密钥对象keyspec和初始化向量对象ivspec来初始化Cipher对象将其设置为加密模式Cipher.ENCRYPT_MODE
// 这一步操作相当于告诉Cipher对象接下来要执行的是加密操作并且告知它使用的具体密钥和初始化向量是什么只有经过这样正确的初始化之后Cipher对象才能依据指定的算法、密钥以及向量等参数对准备好的明文数据进行加密处理。
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
// 调用Cipher对象的doFinal方法传入经过前面一系列处理后准备好的明文字节数组plaintext该方法会依据之前初始化好的Cipher对象所设定的加密算法、密钥以及初始化向量等参数
// 执行实际的加密操作对明文字节数组中的数据进行加密处理最终返回加密后的字节数组结果encrypted这个字节数组就是得到的密文数据后续还需要对其进行合适的格式转换等处理以便能够方便地返回给调用者并在实际应用场景如网络传输、存储等中使用。
byte[] encrypted = cipher.doFinal(plaintext);
// return new Base64().encodeToString(encrypted);
// 使用Base64编码工具类的encodeBase64String静态方法将加密后得到的字节数组encrypted转换为Base64编码后的字符串形式
// Base64编码是一种将二进制数据转换为文本格式的编码方式它使得加密后的密文数据可以以文本形式方便地在网络中传输避免了直接使用二进制密文可能带来的兼容性问题例如某些传输协议对二进制数据处理不太友好或者存储时二进制数据可能出现乱码等情况经过编码后的字符串最终会作为加密结果返回给调用者供其在后续的业务逻辑中使用比如将加密后的用户密码存储到数据库中或者传输给其他需要处理加密数据的服务端等场景。
return Base64.encodeBase64String(encrypted);
} catch (Exception e) {
// 如果在加密过程中出现了任何异常情况例如指定的加密算法在当前运行环境中不被支持、传入的密钥格式不符合要求、Cipher对象初始化失败等各类可能导致加密操作无法正常进行的问题
// 通过调用e.printStackTrace()方法打印出异常的堆栈信息这个堆栈信息包含了异常发生的详细调用路径、所在的类和方法以及具体的错误原因等内容方便开发人员在排查问题时准确地定位到异常发生的根源以及相关的代码位置同时返回null值表示此次加密操作失败调用者可以通过判断返回值是否为null来知晓加密是否成功并据此进行相应的处理操作比如向用户提示加密失败等提示信息以便在应用层面进行合理的应对。
e.printStackTrace();
return null;
}
}
/**
*
* @param data
* @param key key
* @param iv iv
* @return
* @param data Base64
* @param key key使使使KEY使
* @param iv ivCBC使使
* @return Base64trim使
*/
private static String desEncrypt(String data, String key, String iv){
private static String desEncrypt(String data, String key, String iv) {
// 此方法是用于执行具体的解密操作的私有静态方法,接收三个参数:
// 参数 `data` 表示要解密的数据它是一个经过加密后且通常以Base64编码的字符串形式传入的密文信息比如从数据库中获取的已加密存储的用户敏感信息或者从网络传输过来的加密数据等。
// 参数 `key` 代表解密所使用的密钥,其类型为字符串,这个密钥需要与加密时使用的密钥完全一致,才能正确地还原出原始的明文内容,若密钥不匹配则无法成功解密。
// 参数 `iv` 是解密操作使用的初始化向量,同样为字符串类型,它也必须和加密过程中使用的初始化向量保持相同,在特定的加密算法模式(此处为 `AES/CBC/NoPadding` 中的CBC模式初始化向量对于正确解密起着重要作用与密钥共同配合来还原出原始信息。
try {
// 由于传入的要解密的数据(`data`是经过Base64编码的字符串形式而实际的解密操作需要字节数组格式的密文数据
// 所以这里使用 `Base64` 解码工具类(通过 `new Base64()` 创建实例后调用 `decode` 方法)对传入的密文字符串进行解码操作,将其还原为原始的字节数组形式,并将结果存储在 `encrypted1` 变量中,
// 这样得到的 `encrypted1` 字节数组就可以作为后续解密操作的输入数据,满足解密算法对输入数据格式的要求,做好解密前的准备工作。
byte[] encrypted1 = new Base64().decode(data);
// 通过 `Cipher` 类的 `getInstance` 方法,按照指定的 "AES/CBC/NoPadding" 算法、模式和补码方式来获取一个 `Cipher` 实例,用于进行解密操作。
// 这里指定的 "AES" 表示使用高级加密标准Advanced Encryption Standard算法它是一种对称加密算法在加密和解密过程中使用相同的密钥。
// "CBC"Cipher Block Chaining密码分组链接模式说明了解密时每个密文块会与前一个密文块以及初始化向量等相关联进行处理以还原出对应的明文块这种模式能增加加密和解密的安全性和随机性。
// "NoPadding" 表示不使用额外的数据填充方式,要求解密时输入的密文数据长度等格式要符合该模式下的特定要求(通常与加密时要求对应,例如长度要是加密算法数据块大小的整数倍等),
// 此处在解密时同样采用与加密一致的配置来获取 `Cipher` 实例,确保解密过程按照与加密相对应的规则来准确还原出原始明文。
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
// 根据传入的解密密钥字符串(`key`),先将其转换为字节数组形式(通过调用 `getBytes` 方法),然后使用 "AES" 算法名称创建一个 `SecretKeySpec` 对象,
// 这个 `SecretKeySpec` 对象代表了 `AES` 解密算法实际所使用的密钥,它封装了密钥的字节数组表示以及对应的算法名称信息,
// 在后续初始化 `Cipher` 对象进行解密操作时,会通过这个对象将具体的密钥参数传递给 `Cipher`,以此确保解密过程使用正确的密钥来还原出原始的明文内容,密钥的准确性对于解密能否成功至关重要,必须与加密时使用的密钥完全相同。
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
// 根据传入的解密初始化向量字符串(`iv`),同样先将其转换为字节数组形式(调用 `getBytes` 方法),然后创建一个 `IvParameterSpec` 对象,
// 初始化向量Initialization VectorIV在 `CBC` 模式下是必不可少的,它与密钥一起参与到解密过程中,用于确保每个密文块能正确地还原为对应的明文块,通过这个 `IvParameterSpec` 对象可以在初始化 `Cipher` 对象时设置相应的初始化向量参数,
// 并且这里传入的初始化向量必须和加密时使用的初始化向量保持一致,才能使解密操作按照正确的逻辑还原出原始明文,保证加密和解密过程的连贯性和正确性。
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());
// 使用前面创建好的密钥对象(`keySpec`)和初始化向量对象(`ivSpec`)来初始化 `Cipher` 对象,将其设置为解密模式(通过传入 `Cipher.DECRYPT_MODE` 常量来指定),
// 这一步操作相当于告知 `Cipher` 对象接下来要执行的是解密操作,并且明确告知它使用的具体密钥和初始化向量是什么,只有经过这样正确的初始化之后,`Cipher` 对象才能依据指定的算法、密钥以及向量等参数,对输入的密文数据(`encrypted1` 字节数组)进行解密处理,以还原出原始的明文信息。
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
// 调用 `Cipher` 对象的 `doFinal` 方法,传入之前经过 `Base64` 解码得到的密文字节数组(`encrypted1`),该方法会依据之前初始化好的 `Cipher` 对象所设定的解密算法、密钥以及初始化向量等参数,
// 执行实际的解密操作,对密文数据进行相应的处理,最终返回解密后的字节数组结果,并将其存储在 `original` 变量中,这个 `original` 字节数组就是还原出来的原始明文数据,不过可能还需要进一步做一些格式上的处理后再返回给调用者,使其更符合业务逻辑中对原始明文数据的预期格式要求。
byte[] original = cipher.doFinal(encrypted1);
// 将解密后得到的字节数组(`original`)转换为字符串形式,通过创建 `String` 对象并传入 `original` 字节数组作为参数来实现这一转换操作,
// 这样就将字节数组表示的原始明文数据变成了可以更方便在业务逻辑中使用的字符串形式。同时,调用 `trim` 方法去除字符串两端可能存在的空白字符(例如空格、制表符等),
// 因为在加密、解密过程或者数据传输、存储等环节中,有可能会意外引入这些多余的空白字符,去除它们后就能得到更干净、准确且符合预期的原始明文内容,最后将处理后的字符串作为解密结果返回给调用者,供后续的业务逻辑(比如展示给用户查看等)使用。
return new String(original).trim();
} catch (Exception e) {
// 如果在解密过程中出现了任何异常情况,例如指定的解密算法在当前运行环境中不被支持(可能缺少相应的加密库等原因)、传入的密钥格式不符合要求(长度不对、包含非法字符等)、
// `Cipher` 对象初始化失败(可能由于密钥或初始化向量参数设置错误等原因)、传入的密文格式不符合解密要求(长度不对、编码错误等)等各类可能导致解密操作无法正常进行的问题,
// 此时通过调用 `e.printStackTrace()` 方法,打印出异常的堆栈信息,这个堆栈信息包含了异常发生的详细调用路径、所在的类和方法以及具体的错误原因等内容,方便开发人员在排查问题时,准确地定位到异常发生的根源以及相关的代码位置,
// 同时返回 `null` 值,表示此次解密操作失败,调用者可以通过判断返回值是否为 `null` 来知晓解密是否成功,并据此进行相应的处理操作,比如向用户提示解密失败等提示信息,以便在应用层面进行合理的应对。
e.printStackTrace();
return null;
}
}
}
}

@ -1,14 +1,27 @@
package cn.org.alan.exam.util;
// 声明该类所在的包名用于在项目中对类进行分类组织表明此工具类属于特定的“exam”项目下的工具类功能模块所在的包方便代码管理与维护使不同功能的代码能各归其位便于查找和理解整个项目的代码结构。
import cn.org.alan.exam.model.entity.User;
// 引入User实体类它对应数据库中存储用户相关信息的表结构包含像用户名、密码、用户角色、用户个人信息如姓名、性别、联系方式等具体依业务而定等具体的属性字段代表了用户在系统中的实际数据存储形式在后续获取用户相关信息如用户ID、所在班级ID等操作时会用到该实体类是获取具体用户数据的关键对象类型。
import cn.org.alan.exam.security.SysUserDetails;
// 引入SysUserDetails类从名称和使用场景推测它可能是对系统用户详细信息进行封装的一个类用于承载当前登录用户的更多细节信息可能包含了User实体以及其他与安全认证相关的额外信息等在Spring Security框架相关的用户信息获取操作中起到承上启下的作用方便从中提取出我们需要的用户基础信息如User实体以此来获取具体的属性值如ID、班级ID等
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
// 引入LambdaQueryWrapper类它是MyBatis Plus框架提供的一个用于构建查询条件的工具类通过Lambda表达式的方式可以更方便、简洁地构建数据库查询条件不过在当前这个工具类中暂时没有看到它的实际使用可能是在项目的其他相关数据库查询操作场景中会用到或者是误引入具体需结合更广泛的项目代码来看
import lombok.extern.java.Log;
import lombok.extern.slf4j.Slf4j;
// 引入Lombok提供的日志相关注解这里使用了@Slf4j它的作用是在编译期自动为该类生成一个名为“log”的日志记录对象方便在类中使用这个对象来记录各种运行时的信息如调试信息、错误信息等用于辅助排查问题、记录关键操作等而无需手动去创建和配置日志记录相关的代码。
import org.springframework.security.core.GrantedAuthority;
// 引入Spring Security框架中的GrantedAuthority接口它用于表示用户被授予的权限信息在获取用户角色等权限相关信息的操作中会用到通过它可以知道当前用户具备哪些具体的权限角色也可看作是一种权限表现形式方便进行权限相关的判断和业务逻辑处理。
import org.springframework.security.core.context.SecurityContextHolder;
// 引入Spring Security框架的SecurityContextHolder类它是用于获取当前安全上下文信息的关键类通过它可以获取到当前登录用户的认证信息如包含用户详细信息的Principal对象等是整个获取当前用户相关信息操作的入口点基于它才能进一步提取出我们需要的用户具体数据如ID、角色、班级ID等
import java.util.List;
// 引入Java标准库中的List接口用于表示列表类型的数据结构在获取当前用户角色信息时会用到因为用户可能拥有多个权限角色这里用列表来存储获取到的权限GrantedAuthority类型集合便于后续从中提取出需要的角色信息进行返回。
/**
* @Author WeiJin
@ -16,36 +29,47 @@ import java.util.List;
* @Date 2024/3/30 0:10
*/
@Slf4j
// 使用@Slf4j注解让Lombok在编译期自动为这个类生成名为“log”的日志记录对象以便在类中可以方便地使用这个对象记录各类运行时的日志信息例如记录获取用户相关信息过程中出现的异常情况等便于后续排查问题。
public class SecurityUtil {
//该类内容
private SecurityUtil(){}
// 将构造函数定义为私有,这是一种常见的设计模式,用于防止外部实例化这个工具类,因为工具类中的方法都是静态的,不需要创建类的实例就可以直接调用,通过把构造函数私有,可以避免不必要的实例化操作,保证工具类的使用方式符合其设计初衷,即只通过类名直接调用静态方法来使用其功能。
/**
* id
* @return id
* @return idID使ID
*/
public static Integer getUserId(){
// 首先通过SecurityContextHolder获取当前的安全上下文信息它里面包含了当前登录用户的认证相关内容这是Spring Security框架提供的获取当前用户信息的标准入口方式。
SysUserDetails user = (SysUserDetails) (SecurityContextHolder.getContext().getAuthentication().getPrincipal());
// 从获取到的认证信息Principal对象这里强转为SysUserDetails类型因为知道其实际封装的是我们需要的这种详细用户信息类型中提取出User对象然后再获取该User对象的ID属性值并返回以此得到当前登录用户的ID方便后续基于该ID进行与用户相关的各种业务操作。
return user.getUser().getId();
//返回值
}
/**
*
* @return
* @return
*/
public static String getRole(){
// 通过SecurityContextHolder获取当前安全上下文信息中的认证对象的权限集合以GrantedAuthority列表形式表示这些权限集合包含了当前用户被授予的所有权限相关信息通过流操作将其转换为列表方便后续处理。
List<? extends GrantedAuthority> list = SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream().toList();
// 从获取到的权限列表中取出第一个元素(这里假设第一个就是我们关心的主要角色信息,具体是否符合实际业务需求需看项目中权限配置和使用方式,可能需要更严谨的处理逻辑来确保获取正确的角色信息),并将其转换为字符串返回,以此获取当前用户的角色信息,用于后续的权限相关业务判断等操作。
return list.get(0).toString();
//返回值
}
/**
* Id
* @return
* @return ID使便
*/
public static Integer getGradeId(){
// 同样先通过SecurityContextHolder获取当前的安全上下文信息然后从中提取出SysUserDetails类型的用户详细信息对象这是获取用户相关具体属性这里是班级ID的前置操作。
SysUserDetails user = (SysUserDetails) (SecurityContextHolder.getContext().getAuthentication().getPrincipal());
// 从提取出的SysUserDetails对象中进一步获取其包含的User对象的班级ID属性值并返回以此得到当前登录用户所在班级的ID便于后续基于班级维度开展相关业务操作。
return user.getUser().getGradeId();
//返回值
}
}
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save