Compare commits

..

No commits in common. 'main' and 'fmy' have entirely different histories.
main ... fmy

@ -1,163 +1,88 @@
package cn.org.alan.exam.common.aop; // 声明当前类所在的包路径,用于组织类结构,避免命名冲突。
import org.aspectj.lang.JoinPoint; // 导入AspectJ的JoinPoint接口代表程序执行过程中的连接点如方法执行点
import org.aspectj.lang.ProceedingJoinPoint; // 导入ProceedingJoinPoint接口用于环绕通知可控制方法执行。
import org.aspectj.lang.Signature; // 导入Signature接口表示被通知方法的签名。
import org.aspectj.lang.annotation.Around; // 导入Around注解用于声明环绕通知。
import org.aspectj.lang.annotation.Aspect; // 导入Aspect注解用于声明一个切面类。
import org.aspectj.lang.annotation.Before; // 导入Before注解用于声明前置通知即方法执行前的操作。
import org.aspectj.lang.annotation.Pointcut; // 导入Pointcut注解用于定义切点即指定拦截哪些方法。
import org.springframework.stereotype.Component; // 导入Spring的Component注解声明该类为Spring组件纳入Spring管理。
import cn.hutool.core.util.RandomUtil; // 导入Hutool工具包中的RandomUtil类用于生成随机数。
import com.alibaba.fastjson.JSONObject; // 导入阿里巴巴Fastjson库中的JSONObject类用于处理JSON数据。
import com.alibaba.fastjson.support.spring.PropertyPreFilters; // 导入Fastjson的PropertyPreFilters类用于Spring环境下JSON属性过滤。
import jakarta.servlet.ServletRequest; // 导入Jakarta Servlet API的ServletRequest接口表示客户端请求。
import jakarta.servlet.ServletResponse; // 导入Jakarta Servlet API的ServletResponse接口表示服务器响应。
import jakarta.servlet.http.HttpServletRequest; // 导入Jakarta Servlet API的HttpServletRequest接口表示HTTP请求。
import org.slf4j.Logger; // 导入SLF4J日志框架的Logger接口用于记录日志。
import org.slf4j.LoggerFactory; // 导入SLF4J日志框架的LoggerFactory类用于获取Logger实例。
import org.slf4j.MDC; // 导入SLF4J的MDC类用于存储与当前线程绑定的诊断信息。
import org.springframework.web.context.request.RequestContextHolder; // 导入Spring的RequestContextHolder类用于持有请求上下文信息。
import org.springframework.web.context.request.ServletRequestAttributes; // 导入Spring的ServletRequestAttributes类用于获取HTTP请求和响应的详细信息。
import org.springframework.web.multipart.MultipartFile; // 导入Spring的MultipartFile接口用于处理文件上传。
package cn.org.alan.exam.common.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import cn.hutool.core.util.RandomUtil;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.PropertyPreFilters;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
/**
* @Author Alan
* @Version
* @Date 2024/5/13 2:33 PM
* @Date 2024/5/13 2:33 PM111
*/
@Aspect
@Component
public class LogAsPect {
// 声明一个私有的静态的Logger对象用于记录日志。通过LoggerFactory根据当前类LogAsPect的类名来获取对应的日志记录器实例
// 后续可以使用这个实例输出不同级别的日志信息如DEBUG、INFO、WARN等具体的日志输出配置例如输出到控制台还是文件、日志级别设置等
// 通常由项目所集成的日志框架如Log4j、Slf4j等结合对应的配置文件来决定。
private final static Logger LOG = LoggerFactory.getLogger(LogAsPect.class);
// 这是一个被注释掉的Around注解配置原本意图可能是匹配cn.org.alan.exam.controller包及其子包下所有类中的所有方法无论参数情况如何
// 但目前处于注释状态,未生效。如果启用,可用于定义一个环绕通知的切点,决定在哪些方法调用前后执行额外逻辑。
private final static Logger LOG = LoggerFactory.getLogger(LogAsPect.class);
// @Around("execution(* cn.org.alan.exam.controller..*.*(..))")
// 使用@Pointcut注解定义一个切点表达式用于指定切面的作用范围。此表达式表示匹配cn.org.alan.exam.controller包及其子包下所有类名以Controller结尾的类中的
// 所有公共方法方法参数不限意味着后续定义的通知Advice将会作用于这些符合条件的方法上比如在这些方法执行前、执行后等添加自定义逻辑。
@Pointcut("execution(public * cn.org.alan.exam.controller..*Controller.*(..))")
public void controllerPointcut() {}
// @Before注解表明这是一个前置通知方法它会在匹配controllerPointcut切点所指定的那些目标方法执行之前被调用
// 在这里可以进行一些准备工作例如记录请求相关的信息等操作joinPoint参数可用于获取目标方法相关的各种信息如方法参数、方法签名等
@Before("controllerPointcut()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 通过RequestContextHolder获取当前请求相关的属性它会尝试获取与当前线程绑定的ServletRequestAttributes对象
// 这个对象包含了与当前请求相关的信息例如HttpServletRequest、HttpServletResponse等此处将其强制转换为ServletRequestAttributes类型。
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
// 从ServletRequestAttributes对象中获取HttpServletRequest对象后续可以通过这个对象获取请求的各种详细信息比如请求的URL、请求方法、请求头信息等。
HttpServletRequest request = attributes.getRequest();
// 获取目标方法的签名信息,包含了方法所在的类、方法名、参数类型等相关信息,通过这个签名对象可以进一步提取出方法相关的具体细节内容。
Signature signature = joinPoint.getSignature();
// 从方法签名中获取方法的名称,后续可用于在日志中记录具体是哪个方法被调用了,方便定位和排查问题。
String name = signature.getName();
// 使用日志记录器输出一条日志信息,表示后续要记录的是一次请求相关信息的开始部分,方便在日志中清晰区分不同请求的记录范围。
LOG.info("------------- 开始 -------------");
// 使用日志记录器输出请求的完整URL地址以及请求的方法例如GET、POST等通过调用HttpServletRequest对象的相关方法获取这些信息并记录到日志中
// 有助于了解请求的来源以及请求的方式,便于后续分析请求流程和排查问题。
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
// 使用日志记录器记录被调用的类名和方法名通过方法签名获取类名使用getDeclaringTypeName方法和前面获取的方法名拼接成字符串记录到日志中
// 这样可以清晰知道具体是哪个控制器类的哪个方法发起了此次请求,方便定位业务逻辑所在的具体位置。
LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
// 使用日志记录器记录客户端的远程IP地址通过HttpServletRequest对象获取请求来源的IP地址并记录到日志中有助于了解请求是从哪个客户端发起的
// 对于安全审计、访问控制以及问题排查等方面都有帮助。
LOG.info("远程地址: {}", request.getRemoteAddr());
// 获取目标方法的所有参数,以数组形式返回,这些参数就是调用目标方法时传入的实际参数,后续可以根据需求对这些参数进行处理、分析或者记录等操作。
Object[] args = joinPoint.getArgs();
// 创建一个新的Object数组其长度与目标方法的参数数组长度相同用于存放经过筛选后的参数初始化为默认值null等取决于对象类型的默认值
// 目的是排除一些不需要详细记录到日志中的参数类型,重新整理参数列表。
Object[] arguments = new Object[args.length];
// 遍历目标方法的参数数组,对每个参数进行类型判断。
for (int i = 0; i < args.length; i++) {
// 如果参数的类型是ServletRequest例如HttpServletRequest等表示请求相关对象、ServletResponse表示响应相关对象
// 或者MultipartFile通常用于处理文件上传的对象则跳过当前参数不将其添加到要记录的参数列表arguments中因为这些类型的对象可能包含大量不必要在日志中展示的信息
// 或者可能涉及到敏感信息等情况,所以进行排除处理。
if (args[i] instanceof ServletRequest
|| args[i] instanceof ServletResponse
|| args[i] instanceof MultipartFile) {
continue;
}
// 如果参数类型不属于上述要排除的类型则将该参数复制到新的参数数组arguments中以便后续记录到日志里展示真正与业务逻辑相关的参数信息。
arguments[i] = args[i];
}
// 创建一个字符串数组,用于指定要排除的属性名称,这里定义了"password"(可能代表密码等敏感信息)和"file"(可能是文件相关的一些不想在日志中暴露的属性),
// 可以根据实际项目中敏感信息的情况进行调整这些属性在将对象转换为JSON字符串记录日志时会被排除掉以保护敏感数据。
String[] excludeProperties = {"password", "file"};
// 创建一个PropertyPreFilters对象这个对象通常用于对对象属性进行过滤处理比如在将对象转换为JSON字符串时按照一定规则排除某些属性
// 它提供了添加过滤器等功能来实现属性的筛选操作。
PropertyPreFilters filters = new PropertyPreFilters();
// 通过PropertyPreFilters对象添加一个简单的属性预过滤器实例后续可以通过这个过滤器实例设置具体要排除的属性等规则用于控制对象属性在转换为JSON字符串时的筛选行为。
PropertyPreFilters.MySimplePropertyPreFilter excludeFilter = filters.addFilter();
// 将前面定义的要排除的属性名称数组添加到过滤器中告诉过滤器在处理对象转换为JSON字符串时需要把这些指定名称的属性排除掉从而实现对敏感属性的过滤
// 保证日志记录中的参数信息既包含了必要的业务相关内容,又不会泄露敏感数据。
excludeFilter.addExcludes(excludeProperties);
// 使用日志记录器记录请求参数信息通过JSONObject将经过筛选后的参数数组arguments转换为JSON字符串进行记录在转换过程中会依据前面设置的excludeFilter过滤器规则
// 排除指定的敏感属性,这样在日志中看到的请求参数就是经过处理后的安全且关键的业务相关参数内容了。
LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludeFilter));
}
// @Around注解定义了一个环绕通知方法环绕通知可以在目标方法执行前、执行后都添加自定义逻辑相比于前置通知和后置通知更加灵活
// 它可以完全控制目标方法的执行过程,比如可以在执行目标方法前进行一些前置操作,执行后进行一些后置操作,并且可以决定目标方法是否执行以及如何处理返回结果等,
// proceedingJoinPoint参数用于在环绕通知中操作目标方法如调用目标方法等以及获取相关信息。
@Around("controllerPointcut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 获取当前系统的时间戳(以毫秒为单位),记录目标方法开始执行的时间,后续通过与方法执行结束后的时间戳做差值运算,可以得到方法执行所耗费的时间,
// 用于性能分析和监控目标方法的执行效率。
long startTime = System.currentTimeMillis();
// 通过proceedingJoinPoint的proceed方法执行被拦截的目标方法这个方法调用会使得目标方法按照正常逻辑执行并且会返回目标方法的执行结果
// 将返回结果保存到result变量中以便后续进行处理如记录返回结果到日志中、对返回结果进行业务逻辑上的进一步操作等
Object result = proceedingJoinPoint.proceed();
// 创建一个字符串数组,用于指定要排除的属性名称,这里同样定义了"password"和"file",与前置通知中保持一致,用于对返回结果中的敏感属性进行过滤,
// 避免将敏感信息记录到日志中,可根据实际项目的敏感数据情况进行调整。
String[] excludeProperties = {"password", "file"};
// 创建一个PropertyPreFilters对象用于对返回结果对象的属性进行过滤处理以便在记录日志时按照规则排除某些属性实现对敏感信息的保护以及日志内容的优化。
PropertyPreFilters filters = new PropertyPreFilters();
// 通过PropertyPreFilters对象添加一个简单的属性预过滤器实例后续通过这个实例设置具体要排除的属性规则用于控制返回结果对象在转换为JSON字符串时的筛选行为。
PropertyPreFilters.MySimplePropertyPreFilter excludeFilter = filters.addFilter();
// 将前面定义的要排除的属性名称数组添加到过滤器中使得过滤器知道在处理返回结果对象转换为JSON字符串时需要把这些指定名称的属性排除掉
// 从而在日志中记录安全的返回结果信息,只展示非敏感且与业务相关的内容。
excludeFilter.addExcludes(excludeProperties);
// 使用日志记录器记录返回结果信息通过JSONObject将返回结果对象result转换为JSON字符串进行记录在转换过程中会依据前面设置的excludeFilter过滤器规则
// 排除指定的敏感属性,这样在日志中看到的返回结果就是经过处理后的安全且关键的业务相关内容了,方便查看方法执行后的返回情况以及进行后续的业务分析。
LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludeFilter));
// 使用日志记录器记录一条日志信息表示目标方法执行结束并通过计算当前系统时间戳与开始记录的时间戳startTime的差值得到目标方法执行所耗费的时间以毫秒为单位
// 将耗时信息记录到日志中,有助于分析目标方法的性能表现,了解其执行效率是否符合预期,对于性能优化和问题排查等方面有重要作用。
LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
// 将目标方法的原始返回结果返回,确保在切面逻辑执行完毕后,调用目标方法的地方能够接收到正确的返回值,维持方法调用链路的正常返回流程,
// 不会因为添加了切面逻辑而改变目标方法原本的返回情况,保证业务逻辑的正确性。
return result;
}
}
}

@ -5,10 +5,8 @@ package cn.org.alan.exam.common.exception;
* @Version 1.022222
* @Date 2024/3/29 21:06
*/
public class AppException extends RuntimeException { // 定义一个名为AppException的自定义异常类它继承自RuntimeException。
public AppException(String msg) { // 定义一个构造方法接收一个字符串参数msg作为异常信息。
super(msg); // 调用父类RuntimeException的构造方法将msg传递给父类以便父类可以处理这个异常信息。
public class AppException extends RuntimeException{
public AppException(String msg){
super(msg);
}
} // 类定义结束。
}

@ -5,13 +5,8 @@ package cn.org.alan.exam.common.group;
* @Version 1.0
* @Date 2024/4/29 15:14
*/
public interface AnswerGroup { // 定义一个公开的接口AnswerGroup。接口是Java中一种抽象类型用于指定一组方法但不提供这些方法的实现。
public interface AnswerGroup {
// 在AnswerGroup接口内部定义了一个嵌套接口CorrectGroup。
// 嵌套接口是定义在另一个接口或类内部的接口。
interface CorrectGroup extends AnswerGroup { // CorrectGroup接口继承了AnswerGroup接口意味着它必须实现或间接实现AnswerGroup接口中声明的所有方法如果有的话
// 但在这个例子中CorrectGroup没有添加任何新的方法或属性。
// 它可能仅用于表示一个特定的、更具体的AnswerGroup类型或子集。
interface CorrectGroup extends AnswerGroup {
}
} // AnswerGroup接口定义结束。
}

@ -5,6 +5,6 @@ package cn.org.alan.exam.common.group;
* @ Version 1.0
* @ Date 2024/5/11 14:45
*/
public interface CertificateGroup { // 定义一个名为CertificateGroup的公共接口
interface CertificateInsertGroup extends CertificateGroup{} // 在CertificateGroup内部定义一个名为CertificateInsertGroup的接口它继承自CertificateGroup
public interface CertificateGroup {
interface CertificateInsertGroup extends CertificateGroup{}
}

@ -5,6 +5,6 @@ package cn.org.alan.exam.common.group;
* @Version 1.0
* @Date 2024/4/1 11:22
*/
public interface QuestionGroup { // 声明一个公共接口 QuestionGroup。这是一个抽象类型用于定义一组问题的共同行为或属性。
interface QuestionAddGroup extends QuestionGroup{} // 在 QuestionGroup 接口内部声明另一个接口 QuestionAddGroup。这个内部接口继承自其外部接口 QuestionGroup。
}
public interface QuestionGroup {
interface QuestionAddGroup extends QuestionGroup{}
}

@ -1,21 +1,21 @@
package cn.org.alan.exam.common.group; // 这行代码声明了一个包路径用于组织Java类、接口等。这个包路径表明当前接口位于项目的cn.org.alan.exam.common.group目录下。
package cn.org.alan.exam.common.group;
/**
* @Author WeiJin // 这行是JavaDoc注释用于说明这个接口的作者。
* @Version 1.0 // 这行JavaDoc注释说明了接口的版本信息。
* @Date 2024/3/29 15:43 // 这行JavaDoc注释记录了接口创建或最后修改的时间。
* @Author WeiJin
* @Version 1.0
* @Date 2024/3/29 15:43
*/
public interface UserGroup { // 声明了一个公共接口UserGroup。这个接口可能是为了定义与用户组相关的某些行为或属性而创建的。
public interface UserGroup {
/**
* // 这行是JavaDoc注释用于说明接下来的接口CreateUserGroup的用途或功能。
*
*/
interface CreateUserGroup extends UserGroup{} // 在UserGroup接口内部声明了一个名为CreateUserGroup的接口它继承自UserGroup接口。这个内部接口可能用于定义用户创建时参数校验的规则或行为。
interface CreateUserGroup extends UserGroup{}
/**
* // 这行是JavaDoc注释用于说明接下来的接口UpdatePasswordGroup的用途或功能。
*
*/
interface UpdatePasswordGroup extends UserGroup{} // 在UserGroup接口内部声明了另一个名为UpdatePasswordGroup的接口它也继承自UserGroup接口。这个内部接口可能用于定义用户修改密码时参数校验的规则或行为。
interface UpdatePasswordGroup extends UserGroup{}
interface RegisterGroup extends UserGroup{} // 在UserGroup接口内部声明了第三个名为RegisterGroup的接口它同样继承自UserGroup接口。这个内部接口可能用于定义用户注册时相关的行为或规则尽管这里没有提供JavaDoc注释来说明其具体用途。
}
interface RegisterGroup extends UserGroup{}
}

@ -1,18 +1,17 @@
package cn.org.alan.exam.common.handler; // 声明类所在的包路径。
// 导入所需的类和接口
import cn.org.alan.exam.util.DateTimeUtil; // 导入自定义的日期时间工具类。
import cn.org.alan.exam.util.SecurityUtil; // 导入自定义的安全工具类。
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; // 导入MyBatis Plus的元数据对象处理器接口。
import lombok.extern.slf4j.Slf4j; // 导入Lombok的日志注解。
import org.apache.ibatis.reflection.MetaObject; // 导入MyBatis的元数据对象接口。
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; // 导入Spring Security的BCrypt密码编码器虽然在这段代码中未使用
import org.springframework.stereotype.Component; // 导入Spring的组件注解。
import java.lang.reflect.Field; // 导入Java反射的Field类。
import java.time.LocalDateTime; // 导入Java 8的日期时间类。
import java.util.Arrays; // 导入Java的Arrays工具类。
import java.util.Objects; // 导入Java的Objects工具类。
package cn.org.alan.exam.common.handler;
import cn.org.alan.exam.util.DateTimeUtil;
import cn.org.alan.exam.util.SecurityUtil;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Objects;
/**
* mybatisPlus
@ -21,37 +20,34 @@ import java.util.Objects; // 导入Java的Objects工具类。
* @Version 1.0
* @Date 2024/3/31 10:00
*/
@Component // 声明该类为Spring组件以便Spring容器可以管理它。
@Slf4j // 使用Lombok的日志注解来自动注入日志对象。
public class FiledFullHandler implements MetaObjectHandler { // 定义一个类实现MyBatis Plus的MetaObjectHandler接口。
@Override // 重写insertFill方法用于在插入操作时自动填充字段。
@Component
@Slf4j
public class FiledFullHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 获取原始对象的类类型。
//没有创建人id就给他自动填充 放属性名而不是字段名
Class<?> clazz = metaObject.getOriginalObject().getClass();
// 获取该类声明的所有字段。
Field[] fields = clazz.getDeclaredFields();
// 使用Java 8的Stream API遍历字段数组。
Arrays.stream(fields).forEach(field -> {
// 填充创建人ID
//填充创建人
if ("userId".equals(field.getName()) && (Objects.isNull(getFieldValByName("userId", metaObject)))) {
// 如果字段名为userId且当前值为null则进行填充。
log.info("user_id字段满足公共字段自动填充规则已填充"); // 记录日志。
// 调用strictInsertFill方法填充userId字段值为SecurityUtil.getUserId()的返回值。
this.strictInsertFill(metaObject, "userId", Integer.class, SecurityUtil.getUserId());
log.info("user_id字段满足公共字段自动填充规则已填充");
this.strictInsertFill(metaObject, "userId", Integer.class, SecurityUtil.getUserId());
}
// 填充创建时间
//填充创建时间
if ("createTime".equals(field.getName()) && (Objects.isNull(getFieldValByName("createTime", metaObject)))) {
// 如果字段名为createTime且当前值为null则进行填充。
log.info("create_time字段满足公共字段自动填充规则已填充"); // 记录日志。
// 调用strictInsertFill方法填充createTime字段值为DateTimeUtil.getDateTime()的返回值。
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, DateTimeUtil.getDateTime());
log.info("create_time字段满足公共字段自动填充规则已填充");
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, DateTimeUtil.getDateTime());
}
});
}
@Override // 重写updateFill方法但在此实现中为空表示在更新操作时不进行自动填充。
@Override
public void updateFill(MetaObject metaObject) {
// 此方法体为空,表示不实现更新时的自动填充逻辑。
}
}
}

@ -1,115 +1,167 @@
package cn.org.alan.exam.common.handler; // 声明类所在的包路径。
// 导入所需的类和接口
import cn.org.alan.exam.common.exception.AppException; // 导入自定义的应用异常类。
import cn.org.alan.exam.common.result.Result; // 导入自定义的响应结果类。
import jakarta.validation.ConstraintViolationException; // 导入Jakarta Bean Validation的约束违反异常类。
import lombok.extern.slf4j.Slf4j; // 导入Lombok的日志注解。
import org.springframework.dao.DuplicateKeyException; // 导入Spring的数据库主键冲突异常类。
import org.springframework.http.converter.HttpMessageNotReadableException; // 导入Spring的HTTP消息无法解析异常类。
import org.springframework.security.access.AccessDeniedException; // 导入Spring Security的访问拒绝异常类。
import org.springframework.web.bind.MethodArgumentNotValidException; // 导入Spring MVC的方法参数校验异常类。
import org.springframework.web.bind.MissingServletRequestParameterException; // 导入Spring MVC的请求参数缺失异常类。
import org.springframework.web.bind.annotation.ExceptionHandler; // 导入Spring MVC的异常处理方法注解。
import org.springframework.web.bind.annotation.RestControllerAdvice; // 导入Spring MVC的控制器增强注解。
import org.springframework.web.multipart.MaxUploadSizeExceededException; // 导入Spring MVC的文件上传大小超限异常类。
import org.springframework.web.multipart.MultipartException; // 导入Spring MVC的文件上传异常基类虽然在此代码中未直接使用
import org.springframework.web.multipart.support.MissingServletRequestPartException; // 导入Spring MVC的文件部分缺失异常类。
import java.lang.reflect.InvocationTargetException; // 导入Java反射的调用目标异常类虽然在此代码中未使用
import java.sql.SQLIntegrityConstraintViolationException; // 导入Java SQL的异常类表示SQL完整性约束违反。
@RestControllerAdvice // 声明该类为Spring MVC的控制器增强类用于全局异常处理。
@Slf4j // 使用Lombok的日志注解来自动注入日志对象。
public class GlobalExceptionHandler { // 定义一个全局异常处理器类。
// 处理自定义异常的方法
@ExceptionHandler(AppException.class) // 指定该方法处理AppException类型的异常。
package cn.org.alan.exam.common.handler;
/**
* @Author WeiJin
* @Version 1.0
* @Date 2024/3/29 16:10
*/
import cn.org.alan.exam.common.exception.AppException;
import cn.org.alan.exam.common.result.Result;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import java.lang.reflect.InvocationTargetException;
import java.sql.SQLIntegrityConstraintViolationException;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
*
*
* @param e
* @return
*/
@ExceptionHandler(AppException.class)
public Result<String> handleAppException(AppException e) {
log.error(e.getMessage(), e.getClass()); // 记录异常日志。
return Result.failed(e.getLocalizedMessage()); // 返回自定义的响应结果,表示失败,并携带异常消息。
log.error(e.getMessage(), e.getClass());
return Result.failed(e.getLocalizedMessage());
}
// 处理方法参数校验异常的方法
@ExceptionHandler(MethodArgumentNotValidException.class) // 指定该方法处理MethodArgumentNotValidException类型的异常。
/**
*
*
* @param e
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error(e.getMessage(), e.getClass()); // 记录异常日志。
// 获取校验失败的第一个错误信息,并返回自定义的响应结果。
log.error(e.getMessage(), e.getClass());
String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
return Result.failed(message);
}
// 处理SQL完整性约束违反异常的方法
@ExceptionHandler(SQLIntegrityConstraintViolationException.class) // 指定该方法处理SQLIntegrityConstraintViolationException类型的异常。
/**
*
*
* @param e
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public Result<String> handleSQLIntegrityConstraintViolationException(SQLIntegrityConstraintViolationException e) {
log.error(e.getMessage(), e.getClass()); // 记录异常日志。
// 返回自定义的响应结果,表示失败,并携带固定的错误信息“重复”。
log.error(e.getMessage(), e.getClass());
return Result.failed("重复");
}
// 处理HTTP消息无法解析异常的方法
@ExceptionHandler(HttpMessageNotReadableException.class) // 指定该方法处理HttpMessageNotReadableException类型的异常。
/**
*
*
* @param e
* @return
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public Result<String> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
log.error(e.getMessage(), e.getClass()); // 记录异常日志。
// 返回自定义的响应结果,表示失败,并携带固定的错误信息“请求参数无法解析”。
log.error(e.getMessage(), e.getClass());
return Result.failed("请求参数无法解析");
}
// 处理请求参数缺失异常的方法
@ExceptionHandler(MissingServletRequestParameterException.class) // 指定该方法处理MissingServletRequestParameterException类型的异常。
/**
*
*
* @param e
* @return
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public Result<String> handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
log.error(e.getMessage(), e.getClass()); // 记录异常日志。
// 返回自定义的响应结果,表示失败,并携带缺失的参数名作为错误信息。
log.error(e.getMessage(), e.getClass());
return Result.failed(e.getParameterName() + "为必填项");
}
// 处理主键冲突异常的方法
@ExceptionHandler(DuplicateKeyException.class) // 指定该方法处理DuplicateKeyException类型的异常。
/**
*
*
* @param e
* @return
*/
@ExceptionHandler(DuplicateKeyException.class)
public Result<String> handleDuplicateKeyException(DuplicateKeyException e) {
// 从异常消息中提取主键名,并记录异常日志。
String name = e.getMessage().split(":")[2].split(" ")[3];
log.error(e.getMessage(), e.getClass());
// 返回自定义的响应结果,表示失败,并携带主键冲突的错误信息。
return Result.failed("主键冲突" + name + "已存在");
}
// 处理无权限访问异常的方法
@ExceptionHandler(AccessDeniedException.class) // 指定该方法处理AccessDeniedException类型的异常。
/**
* 访
*
* @param e
* @return
*/
@ExceptionHandler(AccessDeniedException.class)
public Result<String> handleAccessDeniedException(AccessDeniedException e) {
log.error(e.getMessage(), e.getClass()); // 记录异常日志。
// 返回自定义的响应结果,表示失败,并携带固定的错误信息“你没有该资源的访问权限”。
log.error(e.getMessage(), e.getClass());
return Result.failed("你没有该资源的访问权限");
}
// 处理文件上传大小超限异常的方法
@ExceptionHandler(MaxUploadSizeExceededException.class) // 指定该方法处理MaxUploadSizeExceededException类型的异常。
/**
*
*
* @param e
* @return
*/
@ExceptionHandler(MaxUploadSizeExceededException.class)
public Result<String> handlerMaxUploadSizeExceededException(MaxUploadSizeExceededException e) {
log.error(e.getMessage(), e.getClass()); // 记录异常日志。
// 返回自定义的响应结果表示失败并携带固定的错误信息“文件太大最大上传5MB”。
log.error(e.getMessage(), e.getClass());
return Result.failed("文件太大最大上传5MB");
}
// 处理文件部分缺失异常的方法
@ExceptionHandler(MissingServletRequestPartException.class) // 指定该方法处理MissingServletRequestPartException类型的异常。
/**
*
*
* @param e
* @return
*/
@ExceptionHandler(MissingServletRequestPartException.class)
public Result<String> handlerMissingServletRequestPartException(MissingServletRequestPartException e) {
log.error(e.getMessage(), e.getClass()); // 记录异常日志。
// 返回自定义的响应结果,表示失败,并携带固定的错误信息“没有获取到文件”。
log.error(e.getMessage(), e.getClass());
return Result.failed("没有获取到文件");
}
// 处理约束违反异常的方法
@ExceptionHandler(ConstraintViolationException.class) // 指定该方法处理ConstraintViolationException类型的异常。
/**
*
*
* @param e
* @return
*/
@ExceptionHandler(ConstraintViolationException.class)
public Result<String> handleConstraintViolationException(ConstraintViolationException e) {
log.error(e.getMessage(), e.getClass()); // 记录异常日志。
// 返回自定义的响应结果,表示失败,并携带异常的完整消息。
log.error(e.getMessage(), e.getClass());
return Result.failed(e.getMessage());
}
// 处理其他未捕获异常的方法
@ExceptionHandler(Exception.class) // 指定该方法处理所有其他类型的异常。
/**
*
*
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public Result<String> handleException(Exception e) {
log.error(e.getMessage(), e.getClass(), e.getCause()); // 记录异常日志,包括异常原因。
// 返回自定义的响应结果,表示失败,并携带固定的错误信息“未知异常”。
log.error(e.getMessage(), e.getClass(), e.getCause());
return Result.failed("未知异常");
}
}
}

@ -1,35 +1,29 @@
package cn.org.alan.exam.common.result; // 指定当前类所在的包路径
package cn.org.alan.exam.common.result;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;
// 导入所需的Spring框架和其他Java标准库类
import org.springframework.core.MethodParameter; // 提供对方法参数(如方法中的输入参数)的反射访问
import org.springframework.http.HttpInputMessage; // 表示HTTP请求体的输入消息
import org.springframework.http.HttpStatus; // HTTP状态码枚举
import org.springframework.http.converter.HttpMessageConverter; // HTTP消息转换器接口用于在HTTP请求和响应之间进行数据转换
import org.springframework.util.StreamUtils; // 提供对输入流和输出流操作的实用方法
import org.springframework.web.bind.annotation.ControllerAdvice; // 控制器增强器,用于全局配置控制器行为
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter; // 请求体增强适配器,用于在请求体绑定到控制器方法参数之前和之后进行增强处理
import java.io.ByteArrayInputStream; // 字节数组输入流
import java.io.IOException; // 输入输出异常
import java.io.InputStream; // 输入流接口
import java.lang.reflect.Type; // Java类型信息接口
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
/**
* @Author Alan // 类的作者
* @Version // 版本信息(此处未指定)
* @Date 2024/6/8 11:47 AM // 类的创建日期
* @Author Alan
* @Version222
* @Date 2024/6/8 11:47 AM111
*/
// 使用@ControllerAdvice注解标记此类为控制器增强器
@ControllerAdvice
public class DecryptRequestBodyAdvice extends RequestBodyAdviceAdapter {
// 重写supports方法用于判断当前增强器是否适用于指定的方法参数、目标类型和转换器类型
// 此处返回false表示默认不应用此增强器实际使用中需要根据需求判断是否支持
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return false; // 暂时不支持任何情况,需要根据具体需求进行实现
return false;
}
}
//
// @Override
// public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {

@ -1,63 +1,53 @@
package cn.org.alan.exam.common.result; // 定义当前类所在的包路径
package cn.org.alan.exam.common.result;
// 导入Lombok库的@Data注解用于自动生成getter、setter、toString等方法
import lombok.Data;
// 导入Java标准库中的Serializable接口用于序列化对象
import java.io.Serializable;
/**
* // 类注释,说明这是一个用于统一响应的结构体类
*
*
* @author haoxr // 类的作者
* @since 2022/1/30 // 类的创建日期或版本
* @author haoxr
* @since 2022/1/30
**/
@Data // 使用Lombok的@Data注解自动生成getter、setter等方法
public class Result<T> implements Serializable { // 定义一个泛型类Result实现Serializable接口
@Data
public class Result<T> implements Serializable {
// 定义一个私有成员变量code用于存储响应码
private Integer code;
// 定义一个泛型成员变量data用于存储响应数据
private T data;
// 定义一个私有成员变量msg用于存储响应消息
private String msg;
// 定义一个静态泛型方法success无参数版本默认返回成功响应无数据
public static <T> Result<T> success() {
return success(null); // 调用另一个success方法传入null作为数据
return success(null);
}
// 定义一个静态泛型方法success带有msg和data参数用于返回成功响应
public static <T> Result<T> success(String msg,T data) {
Result<T> result = new Result<>(); // 创建Result对象
result.setCode(1); // 设置响应码为1通常表示成功
result.setMsg(msg); // 设置响应消息
result.setData(data); // 设置响应数据
return result; // 返回Result对象
Result<T> result = new Result<>();
result.setCode(1);
result.setMsg(msg);
result.setData(data);
return result;
}
// 定义一个静态泛型方法success只带有msg参数用于返回成功响应无数据
public static <T> Result<T> success(String msg) {
Result<T> result = new Result<>(); // 创建Result对象
result.setCode(1); // 设置响应码为1通常表示成功
result.setMsg(msg); // 设置响应消息
result.setData(null); // 设置响应数据为null
return result; // 返回Result对象
Result<T> result = new Result<>();
result.setCode(1);
result.setMsg(msg);
result.setData(null);
return result;
}
// 定义一个静态泛型方法failed带有msg参数用于返回失败响应
public static <T> Result<T> failed(String msg) {
return result(0,msg , null); // 调用私有方法result传入失败响应码、消息和null作为数据
return result(0,msg , null);
}
// 定义一个私有静态泛型方法result带有code、msg和data参数用于创建并返回Result对象
private static <T> Result<T> result(Integer code, String msg, T data) {
Result<T> result = new Result<>(); // 创建Result对象
result.setCode(code); // 设置响应码
result.setData(data); // 设置响应数据
result.setMsg(msg); // 设置响应消息
return result; // 返回Result对象
Result<T> result = new Result<>();
result.setCode(code);
result.setData(data);
result.setMsg(msg);
return result;
}
}
}

@ -1,22 +1,12 @@
package cn.org.alan.exam.config; // 定义当前类所在的包路径
package cn.org.alan.exam.config;
// 导入Spring Boot的FilterRegistrationBean类用于注册过滤器
import org.springframework.boot.web.servlet.FilterRegistrationBean;
// 导入Spring的@Bean和@Configuration注解用于定义配置类和Bean
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// 导入Spring Web的CorsConfiguration类用于配置CORS跨源资源共享
import org.springframework.web.cors.CorsConfiguration;
// 导入Spring Web的UrlBasedCorsConfigurationSource类用于基于URL路径的CORS配置源
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
// 导入Spring Web的CorsFilter类用于实现CORS过滤功能
import org.springframework.web.filter.CorsFilter;
// 导入Java的Collections工具类用于操作集合
import java.util.Collections;
/**
@ -25,33 +15,28 @@ import java.util.Collections;
* @author Alan
* @since 2023/4/17
*/
@Configuration // 标识这是一个Spring配置类
@Configuration
public class CorsConfig {
@Bean // 定义一个Bean方法用于创建并返回FilterRegistrationBean对象
@Bean
public FilterRegistrationBean filterRegistrationBean() {
CorsConfiguration corsConfiguration = new CorsConfiguration(); // 创建CorsConfiguration对象用于配置CORS
CorsConfiguration corsConfiguration = new CorsConfiguration();
//1.允许任何来源
corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); // 设置允许的来源模式为任意模式
corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*"));
//2.允许任何请求头
corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); // 允许所有的请求头
corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
//3.允许任何方法
corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); // 允许所有的HTTP方法
corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
//4.允许凭证
corsConfiguration.setAllowCredentials(true); // 允许客户端发送凭证如Cookies
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // 创建UrlBasedCorsConfigurationSource对象用于基于URL路径的CORS配置
source.registerCorsConfiguration("/**", corsConfiguration); // 对所有路径("/**"应用上述CORS配置
corsConfiguration.setAllowCredentials(true);
CorsFilter corsFilter = new CorsFilter(source); // 创建CorsFilter对象并传入配置源
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
CorsFilter corsFilter = new CorsFilter(source);
FilterRegistrationBean<CorsFilter> filterRegistrationBean = new FilterRegistrationBean<>(corsFilter); // 创建FilterRegistrationBean对象并传入CorsFilter
filterRegistrationBean.setOrder(-101); // 设置过滤器的顺序确保它在Spring Security过滤器之前执行Spring Security的默认顺序是-100
FilterRegistrationBean<CorsFilter> filterRegistrationBean=new FilterRegistrationBean<>(corsFilter);
filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可
return filterRegistrationBean; // 返回配置好的FilterRegistrationBean对象
return filterRegistrationBean;
}
}

@ -1,56 +1,38 @@
package cn.org.alan.exam.config; // 定义当前类所在的包路径
package cn.org.alan.exam.config;
// 导入自定义的FiledFullHandler类可能用于处理MyBatis Plus中的字段自动填充
import cn.org.alan.exam.common.handler.FiledFullHandler;
// 导入MyBatis Plus的DbType枚举用于指定数据库类型
import com.baomidou.mybatisplus.annotation.DbType;
// 导入MyBatis Plus的GlobalConfig类用于全局配置
import com.baomidou.mybatisplus.core.config.GlobalConfig;
// 导入MyBatis Plus的MybatisPlusInterceptor类用于配置拦截器
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
// 导入MyBatis Plus的分页插件内部拦截器PaginationInnerInterceptor
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
// 导入Jakarta EE的@Resource注解用于注入依赖
import jakarta.annotation.Resource;
// 导入MyBatis的@MapperScan注解用于扫描Mapper接口所在的包
import org.mybatis.spring.annotation.MapperScan;
// 导入Spring的@Bean和@Configuration注解用于定义配置类和Bean
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis Plus
*
* @Author Alan
* @Version
* @Version
* @Date 2024/3/28 3:57 PM
*/
@Configuration // 标识这是一个Spring配置类
@MapperScan("cn.org.alan.exam.mapper") // 指定Mapper接口所在的包路径
@Configuration
@MapperScan("cn.org.alan.exam.mapper")
public class MybatisPlusConfig {
@Resource // 注入自定义的FieldFullHandler用于处理字段自动填充
@Resource
private FiledFullHandler filedFullHandler;
@Bean // 定义一个Bean方法用于创建并返回MybatisPlusInterceptor对象
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 创建MybatisPlusInterceptor对象
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 添加分页插件内部拦截器并指定数据库类型为MySQL
//添加元数据对象处理器此注释实际上是对下一行代码的说明但下一行代码是return语句因此注释放在了这里
return interceptor; // 返回配置好的MybatisPlusInterceptor对象
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new
PaginationInnerInterceptor(DbType.MYSQL));
//添加元数据对象处理器
return interceptor;
}
@Bean // 定义一个Bean方法用于创建并返回GlobalConfig对象
@Bean
public GlobalConfig globalConfig() {
GlobalConfig config = new GlobalConfig(); // 创建GlobalConfig对象
config.setMetaObjectHandler(filedFullHandler); // 设置元数据对象处理器,用于处理字段自动填充
return config; // 返回配置好的GlobalConfig对象
GlobalConfig config = new GlobalConfig();
config.setMetaObjectHandler(filedFullHandler);
return config;
}
}
}

@ -1,49 +1,37 @@
package cn.org.alan.exam.config; // 声明当前类所在的包路径为cn.org.alan.exam.config
package cn.org.alan.exam.config;
// 导入Spring的@Bean和@Configuration注解用于定义配置类和Bean
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// 导入Spring Data Redis的RedisConnectionFactory接口用于提供Redis连接
import org.springframework.data.redis.connection.RedisConnectionFactory;
// 导入Spring Data Redis的RedisTemplate类用于操作Redis数据
import org.springframework.data.redis.core.RedisTemplate;
// 导入Spring Data Redis的序列化器类用于对象与Redis存储之间的转换
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis
*
* @Author Alan
* @Version
* @Version
* @Date 2024/6/9 11:07 PM
*/
@Configuration // 标识这是一个Spring配置类
@Configuration
public class RedisConfig {
@Bean // 定义一个Bean方法用于创建并返回RedisTemplate对象
@Bean
public RedisTemplate redisTemplateInit(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>(); // 创建RedisTemplate对象键类型为String值类型为Object
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory); // 设置Redis连接工厂用于提供Redis连接
// 设置序列化Key的实例化对象使用StringRedisSerializer来序列化键确保键以字符串形式存储
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置序列化Key的实例化对象
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 设置序列化Value的实例化对象使用GenericJackson2JsonRedisSerializer来序列化值支持将对象序列化为JSON字符串存储
//设置序列化Value的实例化对象
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
/**
* Hash
* Hash
*
* Hash
*/
redisTemplate.setHashKeySerializer(new StringRedisSerializer()); // 设置Hash键的序列化器为StringRedisSerializer
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); // 设置Hash值的序列化器为GenericJackson2JsonRedisSerializer
return redisTemplate; // 返回配置好的RedisTemplate对象
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
}
}

@ -1,30 +1,25 @@
package cn.org.alan.exam.config; // 声明当前类所在的包路径
package cn.org.alan.exam.config;
import cn.org.alan.exam.filter.VerifyTokenFilter;
import cn.org.alan.exam.common.result.Result;
import cn.org.alan.exam.util.ResponseUtil;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
// 导入项目中自定义的类
import cn.org.alan.exam.filter.VerifyTokenFilter; // 导入自定义的Token验证过滤器
import cn.org.alan.exam.common.result.Result; // 导入自定义的响应结果类
import cn.org.alan.exam.util.ResponseUtil; // 导入自定义的响应工具类
// 导入Jakarta EE的注解
import jakarta.annotation.Resource; // 用于注入依赖
// 导入Lombok库中的注解
import lombok.RequiredArgsConstructor; // 用于生成构造器
// 导入Spring框架的注解和类
import org.springframework.context.annotation.Bean; // 用于定义Bean
import org.springframework.context.annotation.Configuration; // 用于标识配置类
import org.springframework.security.authentication.AuthenticationManager; // 用于认证管理
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; // 用于配置认证
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; // 启用方法级别的安全配置
import org.springframework.security.config.annotation.web.builders.HttpSecurity; // 用于配置HTTP安全
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; // 启用Web安全配置
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; // 用于自定义Web安全配置
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; // 抽象HTTP配置器
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; // BCrypt密码编码器
import org.springframework.security.crypto.password.PasswordEncoder; // 密码编码器接口
import org.springframework.security.web.SecurityFilterChain; // 安全过滤器链
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; // 用户名密码认证过滤器
/**
* Spring Security
@ -32,54 +27,51 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
* @author Alan
* @since 2024/4/17
*/
@Configuration // 标识这是一个Spring配置类
@EnableWebSecurity // 启用Spring Security的Web安全配置
@EnableMethodSecurity // 启用方法级别的安全配置
@RequiredArgsConstructor // 使用Lombok生成构造器自动注入final或@NonNull标注的字段
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
@Resource // 注入ResponseUtil依赖用于自定义响应处理
@Resource
private ResponseUtil responseUtil;
@Resource // 注入VerifyTokenFilter依赖用于自定义Token验证
@Resource
private VerifyTokenFilter verifyTokenFilter;
@Bean // 定义一个Bean方法用于创建并返回SecurityFilterChain对象
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(request -> { // 配置请求授权规则
// 放开对/api/auths/**路径的访问,无需认证
http.authorizeHttpRequests(request -> {
//放开认证
request.requestMatchers("/api/auths/**").permitAll();
// 配置/api/auths/logout路径需要认证才能访问
request.requestMatchers("/api/auths/logout").authenticated();
// 所有其他请求都需要认证才能访问
//所有请求的授权都需要认证
request.anyRequest().authenticated();
});
// 关闭默认的表单登录功能,使用自定义登录和退出逻辑
//关闭表单功能,使用自定义登录和退出
http.formLogin(AbstractHttpConfigurer::disable);
// 配置拒绝访问处理器,当访问被拒绝时执行自定义的响应处理
// 配置拒绝访问处理器
http.exceptionHandling(exceptionHandling -> exceptionHandling
.accessDeniedHandler((request, response, accessDeniedException) ->
responseUtil.response(response, Result.failed("你没有该资源的访问权限")))); // 使用ResponseUtil工具类返回自定义的响应结果
// 在UsernamePasswordAuthenticationFilter之前添加自定义的Token验证过滤器
responseUtil.response(response, Result.failed("你没有该资源的访问权限"))));
// 配置请求拦截前处理器验证token
http.addFilterBefore(verifyTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 关闭CSRF保护允许跨域请求
// 放开跨域请求
http.csrf(AbstractHttpConfigurer::disable);
return http.build(); // 构建并返回SecurityFilterChain对象
return http.build();
}
/**
*
*/
@Bean // 定义一个Bean方法用于创建并返回WebSecurityCustomizer对象
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring() // 配置不需要经过安全过滤器的路径
.requestMatchers( // 指定放行的路径
return (web) -> web.ignoring()
.requestMatchers(
// "/api/auths/**",
"/webjars/**",
"/doc.html",
"/swagger-resources/**",
@ -94,9 +86,9 @@ public class SecurityConfig {
/**
*
*/
@Bean // 定义一个Bean方法用于创建并返回PasswordEncoder对象
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 使用BCrypt算法进行密码编码
return new BCryptPasswordEncoder();
}
/**
@ -104,8 +96,8 @@ public class SecurityConfig {
*
* @param authenticationConfiguration
*/
@Bean // 定义一个Bean方法用于创建并返回AuthenticationManager对象
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager(); // 从认证配置中获取AuthenticationManager对象
return authenticationConfiguration.getAuthenticationManager();
}
}
}

@ -1,79 +1,67 @@
package cn.org.alan.exam.config;
// 定义包路径,便于组织代码和类
// import 语句被注释掉了,这些原本是用来导入其他类或接口的
// import com.zaxxer.hikari.HikariDataSource;
// 导入HikariDataSource类它是一个高性能的JDBC连接池
// import org.apache.shardingsphere.api.config.masterslave.LoadBalanceStrategyConfiguration;
// 导入负载均衡策略配置类
// import org.apache.shardingsphere.api.config.masterslave.MasterSlaveRuleConfiguration;
// 导入主从规则配置类
// import org.apache.shardingsphere.api.config.sharding.ShardingRuleConfiguration;
// 导入分片规则配置类
// import org.apache.shardingsphere.shardingjdbc.api.ShardingDataSourceFactory;
// 导入ShardingDataSourceFactory类用于创建分片数据源
// import org.springframework.context.annotation.Bean;
// 导入Bean注解用于定义Spring管理的bean
// import org.springframework.context.annotation.Configuration;
// 导入Configuration注解表示这是一个配置类
import javax.sql.DataSource; // 导入DataSource接口它是JDBC的API用于表示数据库连接池
import java.sql.SQLException; // 导入SQLException类用于处理SQL异常
import java.util.HashMap; // 导入HashMap类用于存储键值对数据
import java.util.List; // 导入List接口用于表示一个有序的集合
import java.util.Map; // 导入Map接口用于表示键值对的集合
import java.util.Properties; // 导入Properties类用于表示一组持久的属性
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* @Author Alan
* @Version
* @Version
* @Date 2024/6/11 4:21 PM
*/
// @Configuration
// 使用@Configuration注解标记这个类为配置类Spring容器会基于此类生成Bean定义和服务请求
public class ShardingsphereConfig {
// 这个方法被注释掉了它原本用于创建一个HikariDataSource实例并配置数据库连接信息
// public DataSource hikariDataSource (String username, String pwd, String className, String url){
// HikariDataSource hikariDataSource1=new HikariDataSource();
// hikariDataSource1.setUsername(username); // 设置数据库用户名
// hikariDataSource1.setPassword(pwd); // 设置数据库密码
// hikariDataSource1.setDriverClassName(className); // 设置JDBC驱动类名
// hikariDataSource1.setJdbcUrl(url); // 设置数据库连接URL
// return hikariDataSource1; // 返回配置好的数据源
// hikariDataSource1.setUsername(username);
// hikariDataSource1.setPassword(pwd);
// hikariDataSource1.setDriverClassName(className);
// hikariDataSource1.setJdbcUrl(url);
// return hikariDataSource1;
// }
//
// @Bean
// 这个方法被注释掉了它原本用于创建一个ShardingRuleConfiguration实例并配置分片规则
// public ShardingRuleConfiguration shardingRuleConfiguration(){
// ShardingRuleConfiguration config=new ShardingRuleConfiguration(); // 创建分片规则配置实例
// LoadBalanceStrategyConfiguration loadConfiguration=new LoadBalanceStrategyConfiguration("round_robin"); // 创建负载均衡策略配置实例,使用轮询算法
// ShardingRuleConfiguration config=new ShardingRuleConfiguration();
// LoadBalanceStrategyConfiguration loadConfiguration=new LoadBalanceStrategyConfiguration("round_robin");
// MasterSlaveRuleConfiguration configuration=new MasterSlaveRuleConfiguration("dataSource",
// "master", List.of("slave"), // 配置主从数据源和从数据源列表
// loadConfiguration // 设置负载均衡策略
// "master", List.of("slave"),
// loadConfiguration
// );
// config.setMasterSlaveRuleConfigs(List.of(configuration)); // 将主从规则配置添加到分片规则配置中
// return config; // 返回配置好的分片规则
// config.setMasterSlaveRuleConfigs(List.of(configuration));
// return config;
// }
//
//
// @Bean
// 这个方法被注释掉了,它原本用于创建一个分片数据源
// public DataSource shardingDataSource(ShardingRuleConfiguration configuration){
// DataSource masterDataSource = hikariDataSource("root", "123456", "com.mysql.cj.jdbc.Driver", "jdbc:mysql://47.109.94.143:9506/db_exam"); // 创建主数据源
// DataSource slaveDataSource = hikariDataSource("root", "123456", "com.mysql.cj.jdbc.Driver", "jdbc:mysql://47.109.94.143:9507/db_exam"); // 创建从数据源
// DataSource masterDataSource = hikariDataSource("root", "123456", "com.mysql.cj.jdbc.Driver", "jdbc:mysql://47.109.94.143:9506/db_exam");
// DataSource slaveDataSource = hikariDataSource("root", "123456", "com.mysql.cj.jdbc.Driver", "jdbc:mysql://47.109.94.143:9507/db_exam");
//
// Map<String, DataSource> dataSourceMap=new HashMap<>(); // 创建一个数据源映射,用于存储主从数据源
// dataSourceMap.put("master",masterDataSource); // 将主数据源添加到映射中
// dataSourceMap.put("slave",slaveDataSource); // 将从数据源添加到映射中
// Map<String, DataSource> dataSourceMap=new HashMap<>();
// dataSourceMap.put("master",masterDataSource);
// dataSourceMap.put("slave",slaveDataSource);
//
// DataSource shardingDataSource= null; // 声明分片数据源变量
// Properties props=new Properties(); // 创建属性对象用于配置Sharding-JDBC的属性
// props.put("sql.show",true); // 设置属性用于开启SQL显示功能
// DataSource shardingDataSource= null;
// Properties props=new Properties();
// props.put("sql.show",true);
// try {
// shardingDataSource= ShardingDataSourceFactory.createDataSource(dataSourceMap,configuration,props); // 创建分片数据源
// shardingDataSource= ShardingDataSourceFactory.createDataSource(dataSourceMap,configuration,props);
// } catch (SQLException e) {
// throw new RuntimeException(e); // 如果创建数据源时发生异常,则抛出运行时异常
// throw new RuntimeException(e);
// }
// return shardingDataSource; // 返回配置好的分片数据源
// return shardingDataSource;
// }
}
}

@ -1,21 +1,19 @@
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;
// 引入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接口用于表示列表类型在这里用于处理返回多个数据元素的情况
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;
/**
*
@ -24,44 +22,44 @@ import java.util.List; // 引入Java标准库中的List接口用于表示列
* @Version
* @Date 2024/3/25 11:20 AM
*/
@RestController // 这是Spring框架提供的复合注解结合了@Controller和@ResponseBody的功能意味着这个类是一个Spring MVC中的控制器类并且类中的方法返回值会直接以JSON等格式响应给客户端默认情况下无需额外配置视图解析等相关内容
@RequestMapping("/api/answers") // 用于给整个控制器类的所有请求处理方法定义一个基础的请求路径前缀表明该控制器类中所有处理请求的方法的URL路径都将以/api/answers开头方便对一组相关的API进行统一的路径管理
public class AnswerController { // 定义一个名为AnswerController的公共类作为控制器来处理相关业务逻辑请求
@RestController
@RequestMapping("/api/answers")
public class AnswerController {
@Resource // 使用@Resource注解将一个实现了IManualScoreService接口的实例注入到当前的AnswerController类中使得后续方法可以调用该服务接口所定义的业务逻辑方法
private IManualScoreService manualScoreService; // 声明一个私有变量manualScoreService类型为IManualScoreService用于后续调用相关服务方法实现具体业务逻辑
@Resource
private IManualScoreService manualScoreService;
/**
*
* @return
*/
@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查询答卷详细信息的业务逻辑
@GetMapping("/detail")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
public Result<List<UserAnswerDetailVO>> getDetail(@RequestParam Integer userId,
@RequestParam Integer examId) {
return manualScoreService.getDetail(userId, examId);
}
/**
*
* @return
*/
@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可能用于表示批改试卷操作的一些相关反馈信息比如批改是否成功等简单提示
@PutMapping("/correct")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
public Result<String> Correct(@RequestBody @Validated(AnswerGroup.CorrectGroup.class) List<CorrectAnswerFrom> correctAnswerFroms) {
return manualScoreService.correct(correctAnswerFroms);
}
/**
*
* @return
*/
@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类型的分页数据
@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);
}
/**
@ -71,12 +69,12 @@ public class AnswerController { // 定义一个名为AnswerController的公共
* @param examId
* @return
*/
@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>>类型,由服务层去实现具体根据给定条件查询待批阅用户信息的业务逻辑,并将结果封装在合适的对象中返回给客户端
@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);
}
}
}

@ -1,90 +1,95 @@
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接口用于存储键值对形式的数据结构常用于传递多个相关参数、存储配置信息等场景在当前代码里尚未看到具体运用的地方
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;
/**
*
*
*
* @Author WeiJin
* @Version
* @Date 2024/3/25 11:05 AM
*/
@RestController // 这是Spring框架提供的复合注解兼具@Controller和@ResponseBody的功能。意味着这个类是Spring MVC中的控制器类并且类中方法的返回值默认会直接以JSON等格式响应给客户端无需额外配置视图解析相关操作
@RequestMapping("/api/auths") // 用于给整个控制器类下的所有请求处理方法设置一个公共的请求路径前缀表明此类中所有处理请求的方法对应的URL路径都将以/api/auths开头便于统一管理和组织与权限认证相关的一组API接口路径
public class AuthController { // 定义一个公共的类类名为AuthController作为权限管理相关业务逻辑的处理中心对外提供各种权限认证相关的接口接收和处理对应的HTTP请求
@RestController
@RequestMapping("/api/auths")
public class AuthController {
@Resource // 使用@Resource注解来进行依赖注入目的是让Spring容器查找并注入一个实现了IAuthService接口的实例到当前类中这样后续的方法就能方便地调用该服务实例所提供的各种权限认证相关的业务方法
private IAuthService iAuthService; // 声明一个私有成员变量iAuthService其类型为IAuthService接口类型通过依赖注入后它将指向对应的服务实现类实例用于在控制器方法中调用具体的权限认证相关业务逻辑方法
@Resource
private IAuthService iAuthService;
/**
*
* @param request requestsessionIdsessionsessionId
* @param user LoginFormLoginForm
* @return tokentokentoken
* @param request requestsessionId
* @param user
* @return token
*/
@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对象直接返回给客户端
@PostMapping("/login")
public Result<String> login(HttpServletRequest request,
@Validated @RequestBody LoginForm loginForm) {
return iAuthService.login(request,loginForm);
}
/**
*
* @param request requestsessionsession访
* @return ResultResult
* @param request requestsession
* @return
*/
@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对象中返回给客户端告知注销操作的执行情况
@DeleteMapping("/logout")
public Result<String> logout(HttpServletRequest request) {
return iAuthService.logout(request);
}
/**
*
* @param request requestsessionIdsessionsessionsessionId
* @param userForm userFormUserFormUserGroup.RegisterGroup.class
* @return Result
* @param request requestsessionId
* @param userForm
* @return
*/
@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对象中返回给客户端告知注册是否成功以及可能的提示信息等情况
@PostMapping("/register")
public Result<String> register(HttpServletRequest request,
@RequestBody @Validated(UserGroup.RegisterGroup.class) UserForm userForm) {
return iAuthService.register(request, userForm);
}
/**
*
* @param request requestsessionIdsession便sessionId
* @param response responseHTTPimage/png
* @param request requestsessionId
* @param response 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直接向客户端返回图片验证码数据
@GetMapping("/captcha")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) {
iAuthService.getCaptcha(request, response);
}
/**
*
* @param request requestsessionIdsessionrequestsessionIdsession
* @param code @PathVariablesession
* @return ResultResult
* @param request requestsessionId
* @param code
* @return
*/
@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("/verifyCode/{code}")
public Result<String> verifyCode(HttpServletRequest request, @PathVariable("code") String code) {
return iAuthService.verifyCode(request, code);
}
@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对象中返回给客户端告知用户状态跟踪的相关情况
@PostMapping("/track-presence")
public Result<String> trackPresence(HttpServletRequest request) {
return iAuthService.sendHeartbeat(request);
}
}
}

@ -1,96 +1,97 @@
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接口
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.*;
/**
*
*
*
*
* @author zsx
* @since 2024-04-1
*/
@RestController // 这是Spring框架提供的复合注解兼具@Controller和@ResponseBody的功能。意味着这个类是Spring MVC中的控制器类并且类中方法的返回值默认会直接以JSON等格式响应给客户端无需额外配置视图解析相关操作
@RequestMapping("/api/certificate") // 用于给整个控制器类下的所有请求处理方法设置一个公共的请求路径前缀表明此类中所有处理请求的方法对应的URL路径都将以/api/certificate开头便于统一管理和组织与证书管理相关的一组API接口路径
public class CertificateController { // 定义一个公共的类类名为CertificateController作为证书管理相关业务逻辑的处理中心对外提供各种证书管理相关的接口接收和处理对应的HTTP请求
@RestController
@RequestMapping("/api/certificate")
public class CertificateController {
@Resource // 使用@Resource注解来进行依赖注入目的是让Spring容器查找并注入一个实现了ICertificateService接口的实例到当前类中这样后续的方法就能方便地调用该服务实例所提供的各种证书管理相关的业务方法
private ICertificateService iCertificateService; // 声明一个私有成员变量iCertificateService其类型为ICertificateService接口类型通过依赖注入后它将指向对应的服务实现类实例用于在控制器方法中调用具体的证书管理相关业务逻辑方法
@Resource
private ICertificateService iCertificateService;
/**
*
* @param certificateForm CertificateForm
* @return Result
* @param certificateForm
* @return
*/
@PostMapping // 该注解表明这个方法用于处理HTTP POST请求其请求路径就是类级别定义的基础路径/api/certificate因为这里没有额外指定路径POST请求常用于向服务器提交数据在此处符合向服务器提交证书添加相关信息的场景
@PreAuthorize("hasAnyAuthority('role_admin')") // 此注解进行权限控制,只有拥有"role_admin"权限的用户才能访问这个方法,确保只有管理员角色的用户可以执行添加证书的操作,增强了系统对证书添加功能的权限管理
@PostMapping
@PreAuthorize("hasAnyAuthority('role_admin')")
public Result<String> addCertificate(@RequestBody @Validated(CertificateGroup.CertificateInsertGroup.class)
CertificateForm certificateForm) { // @RequestBody表示从请求的正文中获取数据并将其转换为CertificateForm类型的对象@Validated结合指定的分组类CertificateGroup.CertificateInsertGroup.class对这个转换后的对象进行数据合法性验证只有验证通过的数据才会进入方法内部进行后续的添加证书业务逻辑处理。这里验证可能涉及到证书必填字段是否填写、格式是否正确等方面
//从token获取用户id放入创建人id属性这里应该是在具体的业务逻辑中通过解析用户登录的token获取当前操作的用户ID并将其设置到证书的创建人ID属性上以便记录证书是由谁创建的后续便于追溯等操作
return iCertificateService.addCertificate(certificateForm); // 方法体内部直接调用通过依赖注入获取的iCertificateService接口实例的addCertificate方法并将经过验证的certificateForm参数传递进去由服务层去具体实现添加证书的详细业务逻辑比如将证书信息保存到数据库中最后将服务层返回的包含添加结果等信息的Result对象直接返回给客户端
CertificateForm certificateForm) {
//从token获取用户id放入创建人id属性
return iCertificateService.addCertificate(certificateForm);
}
/**
*
* @param pageNum pageNum1
* @param pageSize 1010
* @param certificateName
* @param certificationUnit
* @return ResultIPage<Certificate>IPageResult
* @param pageNum
* @param pageSize
* @param certificateName
* @param certificationUnit
* @return
*/
@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对象中返回给客户端
@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);
}
/**
*
* @param id idID
* @param certificateForm CertificateForm
* @return Result
* @param id id
* @param certificateForm
* @return
*/
@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对象中返回给客户端
@PutMapping("/{id}")
@PreAuthorize("hasAnyAuthority('role_admin')")
public Result<String> updateCertificate(@PathVariable("id") Integer id,
@RequestBody CertificateForm certificateForm) {
certificateForm.setId(id);
return iCertificateService.updateCertificate(certificateForm);
}
/**
*
* @param id idID
* @return Result
* @param id id
* @return
*/
@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对象中返回给客户端
@DeleteMapping("/delete/{id}")
@PreAuthorize("hasAnyAuthority('role_admin')")
public Result<String> deleteCertificate(@PathVariable("id") Integer id) {
return iCertificateService.deleteCertificate(id);
}
/**
*
* @param pageNum1
* @param pageSize10
* @return ResultIPage<MyCertificateVO>IPageResult
* @param pageNum
* @param pageSize
* @return
*/
@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对象中返回给客户端
@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);
}
}
}

@ -1,121 +1,187 @@
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接口用于表示列表类型的数据结构在需要返回多个同类型元素如多个题目汇总信息等情况时会用到
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;
/**
*
*
*
* @author Alan
* @since 2024-03-21
*/
@RestController // 这是Spring框架提供的复合注解兼具@Controller和@ResponseBody的功能。意味着这个类是Spring MVC中的控制器类并且类中方法的返回值默认会直接以JSON等格式响应给客户端无需额外配置视图解析相关操作
@RequestMapping("/api/exams") // 用于给整个控制器类下的所有请求处理方法设置一个公共的请求路径前缀表明此类中所有处理请求的方法对应的URL路径都将以/api/exams开头便于统一管理和组织与考试管理相关的一组API接口路径
public class ExamController { // 定义一个公共的类类名为ExamController作为考试管理相关业务逻辑的处理中心对外提供各类考试管理相关的接口接收并处理对应的HTTP请求
@RestController
@RequestMapping("/api/exams")
public class ExamController {
@Resource // 使用@Resource注解来进行依赖注入让Spring容器查找并注入一个实现了IExamService接口的实例到当前类中方便后续的方法调用该服务实例所提供的各种考试管理相关业务方法
private IExamService examService; // 声明一个私有成员变量examService其类型为IExamService接口类型通过依赖注入后它将指向对应的服务实现类实例用于在控制器方法中调用具体的考试管理相关业务逻辑方法
@Resource
private IExamService examService;
/**
*
* @param examAddFormExamAddForm
* @return ResultResult
* @param examAddForm
* @return
*/
@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对象直接返回给客户端
@PostMapping
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
public Result<String> createExam(@Validated @RequestBody ExamAddForm examAddForm) {
return examService.createExam(examAddForm);
}
/**
*
* @param examIdID便@NotNullID
* @return Result
* @param examId
* @return
*/
@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对象中返回给客户端
@GetMapping("/start")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
public Result<String> startExam(@RequestParam("examId") @NotNull Integer examId) {
return examService.startExam(examId);
}
/**
*
* @param examUpdateFormExamUpdateForm
* @param idID@NotNull
* @return Result
* @param examUpdateForm
* @param id
* @return
*/
@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对象中返回给客户端
@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);
}
/**
*
* @param ids@Pattern(regexp = "^\\d+(,\\d+)*$|^\\d+$")ID便
* @return Result
* @param ids
* @return
*/
@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对象中返回给客户端
@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);
}
/**
*
* @param pageNumpageNum11
* @param pageSize1010
* @param title
* @return ResultIPage<ExamVO>IPageResult
* @param pageNum
* @param pageSize
* @param title
* @return
*/
@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对象中返回给客户端
@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);
}
/**
* id
* @param examId@NotBlankIDIDIDID
* @return ResultIDID
* @param examId
* @return
*/
@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对象中返回给客户端
@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 examIdID**/
/**
*
* @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
*/
@PutMapping("/cheat/{examId}")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin','role_student')")
public Result<Integer> addCheat(@PathVariable("examId") @NotNull Integer examId) {
return examService.addCheat(examId);
}
/**
*
* @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);
}
}

@ -1,171 +1,107 @@
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 IdIDID
* @param quType @RequestParamrequired = false@Min@Max14@NullablenullID
* @return ResultIDID
* @param repoId Id
* @param quType
* @return
*/
@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 ExerciseFillAnswerFrom
* @return Result
* @param exerciseFillAnswerFrom
* @return
*/
@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 pageNum11
* @param pageSize 1010
* @param title
* @return ResultIPage<ExerciseRepoVO>IPageResult
* @param pageNum
* @param pageSize
* @param title
* @return
*/
@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 idID
* @return Result
* @param id id
* @return
*/
@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 repoId IdID
* @param quId idIDID
* @return Result
* @param
* @return
*/
@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,150 +1,100 @@
package cn.org.alan.exam.controller;
// 声明该类所在的包名,用于在项目中对类进行分类组织,清晰表明此控制器类属于特定的功能模块,方便代码管理与维护,这里是和班级管理相关的控制器类所在的包
import cn.org.alan.exam.common.result.Result;
// 引入Result类通常用于统一封装业务操作后的结果信息包含操作是否成功的标识以及可能需要返回的数据等内容以便以标准格式返回给调用者比如前端应用
import cn.org.alan.exam.common.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 pageNumpageNum11
* @param pageSize1010
* @param gradeName
* @return ResultIPage<GradeVO>IPageResult
* @param pageNum
* @param pageSize
* @param gradeName
* @return
*/
@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 gradeFormGradeForm
* @return ResultResult
* @param gradeForm
* @return
*/
@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 idID@NotNull
* @param gradeFormGradeForm
* @return Result
* @param id
* @param gradeForm
* @return
*/
@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 idID@NotNull
* @return Result
* @param id
* @return
*/
@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@NotBlank退ID
* @return Result退
* @param ids
* @return
*/
@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 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对象中返回给客户端
}
}
/**
*
* @return
*/
@GetMapping("/list")
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
public Result<List<GradeVO>> getAllGrade(){
return gradeService.getAllGrade();
}
}

@ -1,138 +1,91 @@
package cn.org.alan.exam.controller;
// 声明该类所在的包名,用于在项目中对类进行分类组织,表明此控制器类属于特定的“公告管理”功能模块相关的包,方便代码管理和维护
import cn.org.alan.exam.common.result.Result;
// 引入Result类通常用于统一封装业务操作后的结果信息包含操作是否成功的标识以及可能需要返回的数据等内容以一致的格式返回给调用者如前端
import cn.org.alan.exam.common.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 noticeFormNoticeForm
* @return Result
* @param noticeForm
* @return
*/
@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@NotBlankIDID
* @return Result
* @param ids
* @return
*/
@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@NotBlank
* @param noticeFormNoticeForm
* @return Result
* @param id
* @param noticeForm
* @return
*/
@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 pageNumpageNum11
* @param pageSize1010
* @param title
* @return ResultIPage<NoticeVO>IPageResult
* @param pageNum
* @param pageSize
* @param title
* @return
*/
@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 pageNum1
* @param pageSize10
* @return ResultIPage<NoticeVO>IPageResult
* @param pageNum
* @param pageSize
* @return
*/
@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,175 +1,116 @@
package cn.org.alan.exam.controller;
// 声明该类所在的包名,用于在项目中对类进行分类组织,表明此控制器类属于特定的“试题管理”功能模块相关的包,方便代码管理与维护
import cn.org.alan.exam.common.group.QuestionGroup;
// 引入QuestionGroup类可能是用于对试题相关操作进行分组验证的类比如针对试题添加、修改等不同操作按照特定规则进行分组验证确保数据符合相应业务场景下的合法性要求
import cn.org.alan.exam.common.group.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 QuestionFromQuestionGroup.QuestionAddGroup.class
* @return Result
* @param questionFrom
* @return
*/
@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 idID
* @return Result
* @param ids id
* @return
*/
@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 pageNum11
* @param pageSize 1010
* @param content
* @param repoId idID
* @param type 便
* @return ResultIPage<QuestionVO>IPageResult
* @param pageNum
* @param pageSize
* @param content
* @param repoId id
* @param type
* @return
*/
@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 idID
* @return Result
* @param id id
* @return
*/
@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 IdID
* @param questionFrom QuestionFrom
* @return Result
* @param id Id
* @param questionFrom
* @return
*/
@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 IdID便
* @param file ExcelMultipartFileExcel
* @return Result
* @param id Id
* @param file Excel
* @return
*/
@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 MultipartFile
* @return Result便访
* @param file
* @return
*/
@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,130 +1,81 @@
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 pageNumpageNum11
* @param pageSize1010
* @param examName
* @return ResultIPage<ExamRecordVO>IPageResult
* @param pageNum
* @param pageSize
* @return
*/
@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 examIdID
* @return ResultList<ExamRecordDetailVO>
* @param examId
* @return
*/
@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 pageNum1
* @param pageSize1010
* @param repoName
* @return ResultIPage<ExerciseRecordVO>IPageResult
* @param pageNum
* @param pageSize
* @return
*/
@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 exerciseIdIDID
* @return ResultList<ExerciseRecordDetailVO>
* @param exerciseId
* @return
*/
@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,140 +1,96 @@
package cn.org.alan.exam.controller;
// 声明该类所在的包名,用于在项目中对类进行分类组织,表明此控制器类属于特定的“题库管理”功能模块相关的包,方便代码管理与维护
import cn.org.alan.exam.common.result.Result;
// 引入Result类通常用于统一封装业务操作后的结果信息包含操作是否成功的标识以及可能需要返回的数据等内容以一致的格式返回给调用者如前端
import cn.org.alan.exam.common.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 Repo
* @return Result
* @param repo
* @return
*/
@PostMapping
// 该注解表明这个方法用于处理HTTP POST请求其请求路径就是类级别定义的基础路径/api/repo因为这里没有额外指定路径POST请求常用于向服务器提交数据在此处符合向服务器提交添加题库相关信息的场景
@PreAuthorize("hasAnyAuthority('role_teacher','role_admin')")
// 进行权限控制,只有拥有"role_teacher"(教师角色)或者"role_admin"(管理员角色)权限的用户才能访问这个方法,确保只有教师或管理员角色的用户可以执行添加题库的操作,加强了对题库添加功能的权限管理
public Result<String> addRepo(@Validated @RequestBody Repo repo) {
// @Validated注解结合@RequestBody注解@RequestBody表示从请求的正文中获取数据并将其转换为Repo类型的对象@Validated用于对这个转换后的对象依据相关规则进行数据合法性验证只有验证通过的数据才会进入方法内部进行后续的添加题库业务逻辑处理
//从token获取用户id放入创建人id属性此处应该是在服务层或者更底层的代码逻辑中实现从请求携带的token通常用于用户认证授权里解析出当前操作的用户ID并将其设置到传入的repo对象的创建人ID属性上确保添加的题库能关联到正确的创建人
//从token获取用户id放入创建人id属性
return iRepoService.addRepo(repo);
// 方法体内部直接调用通过依赖注入获取的iRepoService接口实例的addRepo方法并将经过验证且设置好创建人ID的repo参数传递进去由服务层去具体实现添加题库的详细业务逻辑比如将题库信息保存到数据库中最后将服务层返回的包含添加结果等信息的Result对象直接返回给客户端
}
/**
*
*
* @param repo Repo
* @return Result
* @param repo
* @return
*/
@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 idID
* @return Result
* @param id id
* @return
*/
@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 ResultIDList<RepoListVO>
* @param repoTitle
* @return
*/
@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 pageNum11
* @param pageSize 1010
* @param title
* @return ResultIPage<RepoVO>IPageResult
* @param pageNum
* @param pageSize
* @param title
* @return
*/
@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,144 +1,99 @@
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 pageNumpageNum11
* @param pageSize1010
* @param gradeIdID便
* @param examIdID便
* @param realName
* @return ResultIPage<UserScoreVO>IPageResult
* @param pageNum
* @param pageSize
* @param gradeId Id
* @param examId Id
* @param realName
* @return
*/
@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 examIdID
* @param questionIdIDexamId
* @return ResultQuestionAnalyseVO
* @param examId id
* @param questionId id
* @return
*/
@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 pageNum1
* @param pageSize1010
* @param examTitle便
* @param gradeIdID便
* @return ResultIPage<GradeScoreVO>IPageResult
* @param pageNum
* @param pageSize
* @param examTitle
* @return
*/
@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注解获取请求中的pageNum、pageSize、examTitle、gradeId参数若客户端未传入pageNum和pageSize则分别使用默认值1和10examTitle用于按考试名称筛选gradeId用于按班级筛选分析考试情况
@RequestParam(value = "gradeId" ,required = false) Integer gradeId){
return iUserExamsScoreService.getExamScoreInfo(pageNum,pageSize,examTitle,gradeId);
// 调用iUserExamsScoreService接口的getExamScoreInfo方法由服务层去具体实现根据传入的页码、每页记录数以及考试名称、班级ID等筛选条件进行分页查询并分析班级考试情况比如班级平均分、各分数段人数等的详细业务逻辑最后将查询分析结果封装到Result对象中返回给客户端
}
/**
*
* @param responseHttpServletResponseHTTP使Excel便
* @param examIdID
* @param gradeIdID便
* @param response
* @param examId id
* @param gradeId id
*/
@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,110 +1,80 @@
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 ResultList<GradeStudentVO>
* @return
*/
@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 ResultList<GradeExamVO>
* @return
*/
@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 ResultAllStatsVO
* @return
*/
@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,112 +1,76 @@
package cn.org.alan.exam.controller;
// 声明该类所在的包名,用于在项目中对类进行分类组织,表明此控制器类属于特定的“错题本管理”功能模块相关的包,方便代码管理和维护
import cn.org.alan.exam.common.result.Result;
// 引入Result类通常用于统一封装业务操作后的结果信息包含操作是否成功的标识以及可能需要返回的数据等内容以标准格式返回给调用者比如前端应用便于告知客户端对应业务操作的执行情况及相关返回数据。
import cn.org.alan.exam.common.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 pageNumpageNum11便
* @param pageSize1010
* @param examName便
* @return ResultIPage<UserPageBookVO>IPageResult便使
* @param pageNum
* @param pageSize
* @param examName
* @return
*/
@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 examIdIDID便便
* @return ResultIDIDList<ReUserExamBookVO>便ID
* @param examId
* @return
*/
@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 quIdID便
* @return ResultBookOneQuVO便
* @param quId
* @return
*/
@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 reUserBookFormReUserBookFormID便
* @return ResultAddBookAnswerVO便
* @param reUserBookForm
* @return
*/
@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,193 +1,136 @@
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 响应结果返回一个Result类型的对象用于告知客户端获取用户登录信息操作是否成功以及可能附带的相关提示信息等其内部封装了操作结果标识以及获取到的用户登录相关信息以UserVO形式等内容便于前端展示给当前登录用户查看自身信息。
/**
*
*
* @return
*/
@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]这是一个UserForm类型的参数用于承载前端传来的创建用户时需要填写的各种详细信息像用户名、真实姓名是必填的基本信息角色ID根据不同创建者教师或管理员有不同的可选值范围并且会依据UserGroup.CreateUserGroup.class指定的验证规则进行数据合法性验证确保传入的数据符合创建用户的要求。
// @return 响应结果返回一个Result类型的对象用于告知客户端创建用户操作是否成功以及可能附带的相关提示信息等其内部封装了操作结果标识以及其他有关创建用户的反馈信息等内容便于前端知晓创建操作是否成功及获取相应提示。
/**
*
*
* @param userForm [id]
* @return
*/
@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 入参这是一个UserForm类型的参数用于承载前端传来的修改密码时需要填写的相关信息例如原密码、新密码等内容并且会依据UserGroup.UpdatePasswordGroup.class指定的验证规则进行数据合法性验证保证传入的数据符合密码修改的业务要求。
// @return 响应结果返回一个Result类型的对象用于告知客户端修改密码操作是否成功以及可能附带的相关提示信息等其内部封装了操作结果标识以及其他与密码修改相关的反馈信息等内容便于前端知晓修改操作是否成功及获取相应提示。
/**
*
*
* @param userForm
* @return
*/
@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这是一个字符串类型的参数用于指定要批量删除的用户的相关标识可能是多个用户ID以某种格式拼接比如逗号分隔等情况通过这个参数可以准确找到对应的用户记录进行批量删除操作方便进行批量操作的信息传递。
// @return 相应结果返回一个Result类型的对象用于告知客户端批量删除用户操作是否成功以及可能的相关提示信息等其内部封装了操作结果标识以及其他相关的反馈信息等内容便于前端知晓删除操作是否成功及获取相应提示。
/**
*
*
* @param ids ids
* @return
*/
@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 文件这是一个MultipartFile类型的参数用于接收前端上传的包含用户数据的Excel文件服务层会对该文件进行解析等操作来批量导入用户数据到系统中方便批量添加用户信息提高添加效率。
// @return 响应结果返回一个Result类型的对象用于告知客户端Excel导入用户数据操作是否成功以及可能附带的相关提示信息等其内部封装了操作结果标识以及其他与导入操作相关的反馈信息等内容便于前端知晓导入是否成功及获取相应提示。
/**
* Excel
*
* @param file
* @return
*/
@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 响应返回一个Result类型的对象用于告知客户端用户加入班级操作是否成功以及可能附带的相关提示信息等其内部封装了操作结果标识以及其他与班级加入相关的反馈信息等内容便于前端知晓加入操作是否成功及获取相应提示。
/**
*
*
* @param code
* @return
*/
@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 页码是一个整数类型的参数用于指定要查询的分页数据的页码比如pageNum为1表示查询第一页的数据客户端可以根据需要传入相应页码来获取不同页的用户信息该参数为可选参数若未传入则默认值为1方便客户端灵活控制查询的页面位置以获取期望页面的用户数据展示。
//@param pageSize 每页记录数同样是整数类型参数用于设定每页显示的用户记录数量默认值为10表示如果客户端没有指定每页显示多少条记录就按照每页10条来进行分页查询便于控制每页展示的用户信息量满足不同展示需求。
// @param gradeId 班级Id是一个整数类型的可选参数用于根据班级的唯一标识符进行筛选查询通过传入班级ID可获取该班级下的用户信息方便教师、管理员按班级维度查看用户情况若不传入该参数则可能查询所有班级的用户信息具体看服务层实现逻辑
// @param realName 真实姓名,是一个字符串类型的可选参数,用于根据用户的真实姓名进行模糊查询等筛选操作,客户端可以传入用户真实姓名的部分或全部内容来查找符合条件的用户信息,便于精准查找特定用户或符合姓名特征的一批用户信息。
//@return 响应结果返回一个Result类型的对象其中封装了IPage<UserVO>类型的数据IPage用于承载分页后的用户实体对应的视图对象数据整体通过Result返回给客户端告知分页查询的结果以及相应的用户数据信息方便前端展示和使用查询到的用户分页数据。
/**
*
*
* @param pageNum
* @param pageSize
* @param gradeId Id
* @param realName
* @return
*/
@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 文件这是一个MultipartFile类型的参数用于接收前端上传的用户头像文件MultipartFile类型方便处理文件上传相关操作可获取文件的各种属性如文件名、文件内容等以便后续进行头像文件的保存、处理等业务逻辑。
// @return 响应结果返回一个Result类型的对象用于告知客户端用户上传头像操作是否成功以及可能附带的相关提示信息等其内部封装了操作结果标识以及其他与头像上传相关的反馈信息比如头像保存后的访问地址等内容具体看业务逻辑等内容便于前端知晓上传操作是否成功及获取相应提示。
/**
*
*
* @param file
* @return
*/
@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,23 +1,11 @@
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
@ -25,15 +13,11 @@ 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,35 +1,16 @@
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
@ -37,35 +18,24 @@ 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,29 +1,13 @@
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
@ -31,19 +15,13 @@ 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,26 +1,13 @@
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
@ -28,20 +15,14 @@ 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);
// 定义的这个方法接收一个List<Grade>类型的参数page也就是班级实体对象的列表其目的是按照特定的映射规则将这些班级实体对象转换为GradeVO视图对象列表用于将班级相关信息以适合前端展示的格式由GradeVO定义进行批量展示比如在展示多个班级的基本信息时把从数据库查询出来的班级实体列表转换为适合前端展示的视图对象列表后返回给前端进行展示。
GradeVO GradeToGradeVO(Grade grade);
// 该方法接收一个Grade实体对象作为参数grade作用是将单个的Grade实体按照相应的映射规则转换为GradeVO视图对象GradeVO通常包含了更符合前端展示需求的班级相关信息相比于原始的Grade实体对象比如对班级名称、人数等属性进行整理和格式化便于前端更友好地展示单个班级的详细情况常用于在需要展示特定班级详细信息的业务场景中。
}
}

@ -1,26 +1,12 @@
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
@ -28,17 +14,12 @@ 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,11 +1,7 @@
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
@ -13,9 +9,7 @@ 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,32 +1,16 @@
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
@ -34,24 +18,16 @@ 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,32 +1,14 @@
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
@ -34,11 +16,9 @@ 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,20 +1,11 @@
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
@ -22,11 +13,9 @@ 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,20 +1,12 @@
package cn.org.alan.exam.converter;
// 声明该接口所在的包名用于在项目中对类和接口进行分类组织表明此接口属于特定的“exam”项目下与数据转换相关的功能模块所在的包方便代码管理与维护让不同功能的代码能按模块划分清晰明了便于后续开发人员查找、理解以及对相关功能进行扩展。
import cn.org.alan.exam.model.entity.UserBook;
// 引入UserBook实体类它对应数据库中存储用户错题本相关信息的表结构包含像错题本对应的用户标识、错题的相关记录例如错题的题目编号、答错次数等具体与错题相关的数据具体依业务而定等属性字段代表了用户错题本在系统中的实际数据存储形式是业务操作中涉及错题本数据持久化以及围绕错题本展开的各类业务逻辑处理的核心数据对象也是后续进行数据转换操作的数据源头。
import cn.org.alan.exam.model.entity.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
@ -22,11 +14,9 @@ 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,20 +1,12 @@
package cn.org.alan.exam.converter;
// 声明该接口所在的包名用于在项目中对类和接口进行分类组织表明此接口属于特定的“exam”项目下与数据转换相关的功能模块所在的包方便代码管理与维护使代码按照功能模块清晰划分便于后续开发人员查找、理解和扩展相关功能代码。
import cn.org.alan.exam.model.entity.User;
// 引入User实体类它对应数据库中存储用户相关信息的表结构包含像用户名、密码、用户角色、用户个人信息如姓名、性别、联系方式等具体依业务而定等具体的属性字段代表了用户在系统中的实际数据存储形式是业务操作中涉及用户数据持久化以及围绕用户展开的各类业务逻辑处理的核心数据对象也是后续进行数据转换操作的目标存储对象类型。
import cn.org.alan.exam.model.entity.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
@ -22,14 +14,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 UserConverter {
User fromToEntity(UserForm userForm);
// 定义了一个名为fromToEntity的方法它接收一个UserForm类型的参数userForm其功能是依据一定的映射规则这些规则可能由Mapstruct根据对象属性名等默认情况确定也可以手动配置更精确的映射关系将承载前端输入用户相关数据的UserForm对象转换为User实体对象方便后续将前端传来的创建或更新用户的信息保存到数据库中对应着用户创建或更新的业务操作场景确保用户数据能正确地持久化存储将前端传来的符合展示和交互格式的数据转换为符合数据库存储要求的实体格式数据。
List<User> listFromToEntity(List<UserForm> list);
// 定义的这个方法接收一个List<UserForm>类型的参数list也就是用户表单数据对象的列表。其目的是按照特定的映射规则同样可能由Mapstruct根据对象属性名等默认匹配也可后续添加更多精确配置来完善将这些用户表单数据对象批量转换为User实体对象列表用于将前端传来的批量用户创建或更新相关的表单数据如批量导入用户信息等场景转换为可持久化存储的用户实体对象列表便于后续统一进行数据库插入或更新操作满足批量处理用户数据转换及存储的业务需求。
}
}

@ -1,7 +1,10 @@
package cn.org.alan.exam.mapper;
// 指定当前类所在的包路径
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视图对象类用于封装返回给前端的数据
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

@ -1,325 +1,264 @@
package cn.org.alan.exam.service.impl; // 声明包路径
import cn.hutool.captcha.CaptchaUtil; // 引入Hutool工具类库中的验证码工具
import cn.hutool.captcha.LineCaptcha; // 引入Hutool工具类库中的线性验证码
import cn.org.alan.exam.common.result.Result; // 引入项目中的通用返回结果类
import cn.org.alan.exam.converter.UserConverter; // 引入用户转换器
import cn.org.alan.exam.mapper.RoleMapper; // 引入角色Mapper
import cn.org.alan.exam.mapper.UserDailyLoginDurationMapper; // 引入用户每日登录时长Mapper
import cn.org.alan.exam.mapper.UserMapper; // 引入用户Mapper
import cn.org.alan.exam.model.entity.User; // 引入用户实体类
import cn.org.alan.exam.model.entity.UserDailyLoginDuration; // 引入用户每日登录时长实体类
import cn.org.alan.exam.model.form.Auth.LoginForm; // 引入登录表单类
import cn.org.alan.exam.model.form.UserForm; // 引入用户表单类
import cn.org.alan.exam.security.SysUserDetails; // 引入系统用户详情类
import cn.org.alan.exam.service.IAuthService; // 引入认证服务接口
import cn.org.alan.exam.util.DateTimeUtil; // 引入日期时间工具类
import cn.org.alan.exam.util.JwtUtil; // 引入JWT工具类
import cn.org.alan.exam.util.SecretUtils; // 引入加密工具类
import cn.org.alan.exam.util.SecurityUtil; // 引入安全工具类
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; // 引入MyBatis Plus查询条件包装类
import com.baomidou.mybatisplus.core.toolkit.StringUtils; // 引入MyBatis Plus字符串工具类
import com.fasterxml.jackson.core.JsonProcessingException; // 引入Jackson JSON处理异常类
import com.fasterxml.jackson.databind.ObjectMapper; // 引入Jackson对象映射器
import jakarta.annotation.Resource; // 引入Jakarta注解的依赖注入注解
import jakarta.servlet.ServletOutputStream; // 引入Servlet输出流类
import jakarta.servlet.http.HttpServletRequest; // 引入Servlet HTTP请求类
import jakarta.servlet.http.HttpServletResponse; // 引入Servlet HTTP响应类
import jakarta.servlet.http.HttpSession; // 引入Servlet会话类
import lombok.SneakyThrows; // 引入Lombok异常处理注解
import org.springframework.data.redis.core.StringRedisTemplate; // 引入Spring Data Redis字符串模板类
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; // 引入Spring Security用户名密码认证令牌类
import org.springframework.security.core.authority.SimpleGrantedAuthority; // 引入Spring Security简单权限授权类
import org.springframework.security.core.context.SecurityContextHolder; // 引入Spring Security安全上下文持有者类
import org.springframework.security.core.userdetails.UsernameNotFoundException; // 引入Spring Security用户名未找到异常类
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; // 引入Spring Security BCrypt密码编码器类
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; // 引入Spring Security Web认证详情源类
import org.springframework.stereotype.Service; // 引入Spring服务注解
import java.io.IOException; // 引入IO异常类
import java.time.Duration; // 引入Java时间库中的持续时间类
import java.time.LocalDate; // 引入Java时间库中的本地日期类
import java.time.LocalDateTime; // 引入Java时间库中的本地日期时间类
import java.time.ZoneOffset; // 引入Java时间库中的时区偏移量类
import java.util.ArrayList; // 引入Java集合框架中的ArrayList类
import java.util.List; // 引入Java集合框架中的List接口
import java.util.Objects; // 引入Java工具类中的Objects类
import java.util.concurrent.TimeUnit; // 引入Java并发库中的时间单位枚举
package cn.org.alan.exam.service.impl;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha;
import cn.org.alan.exam.common.result.Result;
import cn.org.alan.exam.converter.UserConverter;
import cn.org.alan.exam.mapper.RoleMapper;
import cn.org.alan.exam.mapper.UserDailyLoginDurationMapper;
import cn.org.alan.exam.mapper.UserMapper;
import cn.org.alan.exam.model.entity.User;
import cn.org.alan.exam.model.entity.UserDailyLoginDuration;
import cn.org.alan.exam.model.form.Auth.LoginForm;
import cn.org.alan.exam.model.form.UserForm;
import cn.org.alan.exam.security.SysUserDetails;
import cn.org.alan.exam.service.IAuthService;
import cn.org.alan.exam.util.DateTimeUtil;
import cn.org.alan.exam.util.JwtUtil;
import cn.org.alan.exam.util.SecretUtils;
import cn.org.alan.exam.util.SecurityUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.SneakyThrows;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* @Author Alan
* @Version
* @Date 2024/3/28 1:33 PM
*/
@Service // 声明这是一个Spring服务组件
public class AuthServiceImpl implements IAuthService { // 实现认证服务接口
private static final String HEARTBEAT_KEY_PREFIX = "user:heartbeat:"; // 定义心跳键前缀
private static final long HEARTBEAT_INTERVAL_MILLIS = 10 * 60 * 1000; // 定义心跳间隔时间为10分钟
@Service
public class AuthServiceImpl implements IAuthService {
private static final String HEARTBEAT_KEY_PREFIX = "user:heartbeat:";
private static final long HEARTBEAT_INTERVAL_MILLIS = 10 * 60 * 1000; // 10分钟
@Resource // 自动注入StringRedisTemplate对象
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource // 自动注入UserMapper对象
@Resource
private UserMapper userMapper;
@Resource // 自动注入RoleMapper对象
@Resource
private RoleMapper roleMapper;
@Resource // 自动注入UserConverter对象
@Resource
private UserConverter userConverter;
@Resource // 自动注入ObjectMapper对象
@Resource
private ObjectMapper objectMapper;
@Resource // 自动注入JwtUtil对象
@Resource
private JwtUtil jwtUtil;
@Resource // 自动注入UserDailyLoginDurationMapper对象
@Resource
private UserDailyLoginDurationMapper userDailyLoginDurationMapper;
/**
*
* @param request HTTP
* @param loginForm
* @return JWT
* @param request
* @param loginForm
* @return
*/
@SneakyThrows(JsonProcessingException.class) // 声明可能抛出的异常
@Override // 重写父类或接口中的方法
@SneakyThrows(JsonProcessingException.class)
@Override
public Result<String> login(HttpServletRequest request, LoginForm loginForm) {
// 先判断用户是否通过校验(如验证码校验)
// 先判断用户是否通过校验
String s = stringRedisTemplate.opsForValue().get("isVerifyCode" + request.getSession().getId());
if (StringUtils.isBlank(s)) { // 如果验证码校验未通过
return Result.failed("请先验证验证码"); // 返回错误信息
if (StringUtils.isBlank(s)) {
return Result.failed("请先验证验证码");
}
// 根据用户名获取用户信息
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); // 创建查询条件包装对象
wrapper.eq(User::getUserName, loginForm.getUsername()); // 设置查询条件为用户名等于登录表单中的用户名
User user = userMapper.selectOne(wrapper); // 执行查询并获取用户对象
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName, loginForm.getUsername());
User user = userMapper.selectOne(wrapper);
// 判读用户名是否存在
if (Objects.isNull(user)) { // 如果用户不存在
throw new UsernameNotFoundException("该用户不存在"); // 抛出用户名未找到异常
if (Objects.isNull(user)) {
throw new UsernameNotFoundException("该用户不存在");
}
if(user.getIsDeleted() == 1){ // 如果用户已被注销
return Result.failed("该用户已注销"); // 返回错误信息
if(user.getIsDeleted() == 1){
return Result.failed("该用户已注销");
}
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); // 创建BCrypt密码编码器对象
if (!encoder.matches(SecretUtils.desEncrypt(loginForm.getPassword()), user.getPassword())) { // 如果密码不匹配
return Result.failed("密码错误"); // 返回错误信息
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
if (!encoder.matches(SecretUtils.desEncrypt(loginForm.getPassword()), user.getPassword())) {
return Result.failed("密码错误");
}
user.setPassword(null); // 清空用户密码(出于安全考虑)
user.setPassword(null);
// 根据用户Id获取权限
List<String> permissions = roleMapper.selectCodeById(user.getRoleId()); // 获取用户角色对应的权限列表
List<String> permissions = roleMapper.selectCodeById(user.getRoleId());
// 数据库获取的权限是字符串Spring Security需要实现GrantedAuthority接口类型所以这里做一个类型转换
// 数据库获取的权限是字符串springSecurity需要实现GrantedAuthority接口类型所有这里做一个类型转换
List<SimpleGrantedAuthority> userPermissions = permissions.stream()
.map(permission -> new SimpleGrantedAuthority("role_" + permission)).toList(); // 将权限字符串转换为SimpleGrantedAuthority对象列表
.map(permission -> new SimpleGrantedAuthority("role_" + permission)).toList();
// 创建一个sysUserDetails对象该类实现了UserDetails接口
SysUserDetails sysUserDetails = new SysUserDetails(user); // 使用用户对象创建系统用户详情对象
SysUserDetails sysUserDetails = new SysUserDetails(user);
// 把转型后的权限放进sysUserDetails对象
sysUserDetails.setPermissions(userPermissions); // 设置用户权限
// 将用户信息转为字符串(此行代码缺少具体操作,可能是后续代码被省略或遗漏)
sysUserDetails.setPermissions(userPermissions);
// 将用户信息转为字符串
String userInfo = objectMapper.writeValueAsString(user);
// 将user对象转换为JSON字符串用于JWT载荷部分。
String token = jwtUtil.createJwt(userInfo, userPermissions.stream().map(String::valueOf).toList());
// 使用JWT工具创建JWT令牌载荷为用户信息和用户权限列表转换为字符串列表
// 把token放到redis中
// 把token放到redis中
stringRedisTemplate.opsForValue().set("token" + request.getSession().getId(), token, 30, TimeUnit.MINUTES);
// 将生成的JWT令牌存储到Redis中键为"token"加上session ID有效期为30分钟。
// 封装用户的身份信息,为后续的身份验证和授权操作提供必要的输入
// 创建UsernamePasswordAuthenticationToken 参数:用户信息,密码,权限列表
// 封装用户的身份信息,为后续的身份验证和授权操作提供必要的输入
// 创建UsernamePasswordAuthenticationToken 参数:用户信息,密码,权限列表
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(sysUserDetails, user.getPassword(), userPermissions);
// 创建一个UsernamePasswordAuthenticationToken对象包含用户详细信息、密码通常不存储明文密码此处可能是示例和权限列表。
// 可选添加Web认证细节
// 可选添加Web认证细节
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 为认证令牌添加Web认证细节如远程地址、会话ID等。
// 用户信息存放进上下文
// 用户信息存放进上下文
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
// 将认证令牌设置到安全上下文SecurityContextHolder供后续的身份验证和授权操作使用。
//用户信息放入
// 清除redis通过校验表示
// 清除redis通过校验表示
stringRedisTemplate.delete("isVerifyCode" + request.getSession().getId());
// 删除Redis中标记验证码已验证的键避免重复使用。
return Result.success("登录成功", token);
// 返回登录成功的结果和JWT令牌。
}
@Override
public Result<String> logout(HttpServletRequest request) {
// 重写logout方法。
// 清除session
HttpSession session = request.getSession(false);
// 获取当前请求的会话如果不存在则返回null。
@Override
public Result<String> logout(HttpServletRequest request) {
if (session != null) {
// 清除redis
stringRedisTemplate.delete("token" + session.getId());
// 删除Redis中存储的JWT令牌。
session.invalidate();
// 使会话失效。
}
// 清除session
HttpSession session = request.getSession(false);
return Result.success("退出成功");
// 返回退出成功的结果。
if (session != null) {
// 清除redis
stringRedisTemplate.delete("token" + session.getId());
session.invalidate();
}
@SneakyThrows(IOException.class)
@Override
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) {
// 重写getCaptcha方法用于生成验证码。
// 生成线性图形验证码的静态方法,参数:图片宽,图片高,验证码字符个数 干扰个数
LineCaptcha captcha = CaptchaUtil
.createLineCaptcha(200, 100, 4, 300);
// 创建一个线性图形验证码。
// 把验证码存放进redis
// 获取验证码
String code = captcha.getCode();
// 获取生成的验证码字符串。
String key = "code" + request.getSession().getId();
// 构造Redis键包含会话ID。
stringRedisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES);
// 将验证码存储到Redis中有效期为5分钟。
// 把图片响应到输出流
response.setContentType("image/jpeg");
// 设置响应内容类型为JPEG图像。
ServletOutputStream os = response.getOutputStream();
// 获取响应的输出流。
captcha.write(os);
// 将验证码图片写入输出流。
os.close();
// 关闭输出流。
return Result.success("退出成功");
}
@SneakyThrows(IOException.class)
@Override
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) {
// 生成线性图形验证码的静态方法,参数:图片宽,图片高,验证码字符个数 干扰个数
LineCaptcha captcha = CaptchaUtil
.createLineCaptcha(200, 100, 4, 300);
// 把验证码存放进redis
// 获取验证码
String code = captcha.getCode();
String key = "code" + request.getSession().getId();
stringRedisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES);
// 把图片响应到输出流
response.setContentType("image/jpeg");
ServletOutputStream os = response.getOutputStream();
captcha.write(os);
os.close();
}
@Override
public Result<String> verifyCode(HttpServletRequest request, String code) {
String key = "code" + request.getSession().getId();
String rightCode = stringRedisTemplate.opsForValue().get(key);
if (StringUtils.isBlank(rightCode)) {
return Result.failed("验证码已过期");
}
@Override
public Result<String> verifyCode(HttpServletRequest request, String code) {
// 重写verifyCode方法用于验证用户输入的验证码。
String key = "code" + request.getSession().getId();
// 构造Redis键包含会话ID。
String rightCode = stringRedisTemplate.opsForValue().get(key);
// 从Redis中获取正确的验证码。
if (StringUtils.isBlank(rightCode)) {
return Result.failed("验证码已过期");
}
// 如果Redis中没有验证码则返回验证码已过期的结果。
if (!rightCode.equalsIgnoreCase(code)) {
return Result.failed("验证码错误");
}
// 如果用户输入的验证码与Redis中的不匹配则返回验证码错误的结果。
// 验证码校验后redis清除验证码避免重复使用
stringRedisTemplate.delete(key);
// 删除Redis中的验证码。
// 验证码校验后redis存入校验成功避免用户登录和注册时不验证验证码直接提交
stringRedisTemplate.opsForValue().set("isVerifyCode" + request.getSession().getId(), "1", 5, TimeUnit.MINUTES);
// 在Redis中标记验证码已验证有效期为5分钟。
return Result.success("验证码校验成功");
// 返回验证码校验成功的结果。
if (!rightCode.equalsIgnoreCase(code)) {
return Result.failed("验证码错误");
}
// 验证码校验后redis清除验证码避免重复使用
stringRedisTemplate.delete(key);
// 验证码校验后redis存入校验成功避免用户登录和注册时不验证验证码直接提交
stringRedisTemplate.opsForValue().set("isVerifyCode" + request.getSession().getId(), "1", 5, TimeUnit.MINUTES);
return Result.success("验证码校验成功");
}
@Override
public Result<String> register(HttpServletRequest request, UserForm userForm) {
// 重写register方法用于用户注册。
String s = stringRedisTemplate.opsForValue().get("isVerifyCode" + request.getSession().getId());
// 检查Redis中是否有验证码已验证的标记。
if (StringUtils.isBlank(s)) {
return Result.failed("请先验证验证码");
}
// 如果没有,则返回请先验证验证码的结果。
if (!SecretUtils.desEncrypt(userForm.getPassword()).equals(SecretUtils.desEncrypt(userForm.getCheckedPassword()))) {
return Result.failed("两次密码不一致");
}
// 验证用户输入的两次密码是否一致。
User user = userConverter.fromToEntity(userForm);
// 将用户表单转换为用户实体。
@Override
public Result<String> register(HttpServletRequest request, UserForm userForm) {
user.setPassword(new BCryptPasswordEncoder().encode(SecretUtils.desEncrypt(user.getPassword())));
// 对用户密码进行加密。
user.setRoleId(1);
// 设置用户角色ID此处为示例通常根据用户选择或系统策略设置
userMapper.insert(user);
// 将用户实体插入数据库。
// 注册成功把redis的是否通过校验验证码删除防止用户注册后立马登录还可以使用
stringRedisTemplate.delete("isVerifyCode" + request.getSession().getId());
// 删除Redis中验证码已验证的标记防止重复注册。
return Result.success("注册成功");
// 返回注册成功的结果。
String s = stringRedisTemplate.opsForValue().get("isVerifyCode" + request.getSession().getId());
if (StringUtils.isBlank(s)) {
return Result.failed("请先验证验证码");
}
if (!SecretUtils.desEncrypt(userForm.getPassword()).equals(SecretUtils.desEncrypt(userForm.getCheckedPassword()))) {
return Result.failed("两次密码不一致");
}
User user = userConverter.fromToEntity(userForm);
/**
*
*
* @return
*/
@Override
@SneakyThrows(value = JsonProcessingException.class)
public Result<String> sendHeartbeat(HttpServletRequest request) {
// 重写sendHeartbeat方法用于用户发送心跳以更新最后活跃时间。
String key = HEARTBEAT_KEY_PREFIX + SecurityUtil.getUserId();
// 构造Redis键包含用户ID和前缀。
String lastHeartbeatStr = stringRedisTemplate.opsForValue().getAndDelete(key);
// 获取并删除Redis中存储的上一次心跳时间。
LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
// 获取当前时间UTC时区
stringRedisTemplate.opsForValue().set(key, now.toString());
// 将当前时间存储到Redis中作为新的心跳时间。
if (lastHeartbeatStr != null) {
LocalDateTime lastHeartbeat = LocalDateTime.parse(lastHeartbeatStr);
// 解析上一次心跳时间。
Duration durationSinceLastHeartbeat = Duration.between(lastHeartbeat, LocalDateTime.now(ZoneOffset.UTC));
// 计算上一次心跳到现在的持续时间。
LocalDate date = DateTimeUtil.getDate();
// 获取当前日期。
// 实现累加逻辑,比如更新数据库中的记录
LambdaQueryWrapper<UserDailyLoginDuration> userDailyLoginDurationLambdaQueryWrapper = new LambdaQueryWrapper<>();
// 构造查询条件。
userDailyLoginDurationLambdaQueryWrapper.eq(UserDailyLoginDuration::getUserId,SecurityUtil.getUserId())
.eq(UserDailyLoginDuration::getLoginDate, date);
// 设置查询条件为用户ID和登录日期。
user.setPassword(new BCryptPasswordEncoder().encode(SecretUtils.desEncrypt(user.getPassword())));
user.setRoleId(1);
userMapper.insert(user);
// 注册成功把redis的是否通过校验验证码删除防止用户注册后立马登录还可以使用
stringRedisTemplate.delete("isVerifyCode" + request.getSession().getId());
return Result.success("注册成功");
}
List<UserDailyLoginDuration> userDailyLoginDurations =
userDailyLoginDurationMapper.selectList(userDailyLoginDurationLambdaQueryWrapper);
// 查询用户当日的登录时长记录。
if(userDailyLoginDurations.isEmpty()){
UserDailyLoginDuration userDailyLoginDuration = new UserDailyLoginDuration();
// 如果没有记录,则创建新记录。
userDailyLoginDuration.setUserId(SecurityUtil.getUserId());
userDailyLoginDuration.setLoginDate(date);
userDailyLoginDuration.setTotalSeconds(0);
userDailyLoginDurationMapper.insert(userDailyLoginDuration);
// 设置用户ID、登录日期和初始时长并插入数据库。
}else {
UserDailyLoginDuration userDailyLoginDuration = new UserDailyLoginDuration();
// 如果有记录
/**
*
*
* @return
*/
@Override
@SneakyThrows(value = JsonProcessingException.class)
public Result<String> sendHeartbeat(HttpServletRequest request) {
String key = HEARTBEAT_KEY_PREFIX + SecurityUtil.getUserId();
String lastHeartbeatStr = stringRedisTemplate.opsForValue().getAndDelete(key);
LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
stringRedisTemplate.opsForValue().set(key, now.toString());
if (lastHeartbeatStr != null) {
LocalDateTime lastHeartbeat = LocalDateTime.parse(lastHeartbeatStr);
Duration durationSinceLastHeartbeat = Duration.between(lastHeartbeat, LocalDateTime.now(ZoneOffset.UTC));
LocalDate date = DateTimeUtil.getDate();
// 实现累加逻辑,比如更新数据库中的记录
LambdaQueryWrapper<UserDailyLoginDuration> userDailyLoginDurationLambdaQueryWrapper = new LambdaQueryWrapper<>();
userDailyLoginDurationLambdaQueryWrapper.eq(UserDailyLoginDuration::getUserId,SecurityUtil.getUserId())
.eq(UserDailyLoginDuration::getLoginDate, date);
List<UserDailyLoginDuration> userDailyLoginDurations =
userDailyLoginDurationMapper.selectList(userDailyLoginDurationLambdaQueryWrapper);
if(userDailyLoginDurations.isEmpty()){
UserDailyLoginDuration userDailyLoginDuration = new UserDailyLoginDuration();
userDailyLoginDuration.setUserId(SecurityUtil.getUserId());
userDailyLoginDuration.setLoginDate(date);
userDailyLoginDuration.setTotalSeconds(0);
userDailyLoginDurationMapper.insert(userDailyLoginDuration);
}else {
UserDailyLoginDuration userDailyLoginDuration = new UserDailyLoginDuration();
userDailyLoginDuration.setTotalSeconds(userDailyLoginDurations.get(0)
.getTotalSeconds()+(int)durationSinceLastHeartbeat.getSeconds());
userDailyLoginDuration.setId(userDailyLoginDurations.get(0).getId());
userDailyLoginDurationMapper.updateById(userDailyLoginDuration);
}
}
ArrayList<String> permissions = new ArrayList<>();
permissions.add(SecurityUtil.getRole());
SysUserDetails principal = (SysUserDetails) (SecurityContextHolder.getContext().getAuthentication().getPrincipal());
User user = principal.getUser();
String string = objectMapper.writeValueAsString(user);
String jwt = jwtUtil.createJwt(string, permissions);
stringRedisTemplate.opsForValue().set("token" + request.getSession().getId(), jwt, 30, TimeUnit.MINUTES);
return Result.success("请求成功",jwt);
}
}

@ -1,92 +1,36 @@
package cn.org.alan.exam.service.impl;
// 指定包名,组织代码结构
import cn.org.alan.exam.common.result.Result;
// 导入通用结果类,用于封装返回结果
import cn.org.alan.exam.converter.CertificateConverter;
// 导入转换器类,用于表单对象与实体对象之间的转换
import cn.org.alan.exam.mapper.CertificateMapper;
// 导入Mapper接口用于数据库操作
import cn.org.alan.exam.model.entity.Certificate;
// 导入实体类,对应数据库中的表
import cn.org.alan.exam.model.form.CertificateForm;
// 导入表单类,用于接收前端传递的数据
import cn.org.alan.exam.model.vo.GradeVO;
// 导入视图对象类,用于封装返回给前端的数据
import cn.org.alan.exam.model.vo.NoticeVO;
// 导入通知视图对象类
import cn.org.alan.exam.model.vo.certificate.MyCertificateVO;
// 导入我的证书视图对象类
import cn.org.alan.exam.service.ICertificateService;
// 导入服务接口,定义服务层方法
import cn.org.alan.exam.util.CacheClient;
// 导入缓存客户端,用于操作缓存
import cn.org.alan.exam.util.DateTimeUtil;
// 导入日期时间工具类
import cn.org.alan.exam.util.SecurityUtil;
// 导入安全工具类,用于获取当前用户信息
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
// 导入LambdaQueryWrapper用于构建查询条件
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
// 导入LambdaUpdateWrapper用于构建更新条件
import com.baomidou.mybatisplus.core.metadata.IPage;
// 导入分页接口
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
// 导入字符串工具类
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
// 导入Page类实现IPage接口用于分页查询
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
// 导入ServiceImpl类简化服务层实现
import jakarta.annotation.Resource;
// 导入资源注入注解
import org.springframework.data.redis.core.StringRedisTemplate;
// 导入Redis字符串模板类用于操作Redis字符串数据
import org.springframework.stereotype.Service;
// 导入服务注解,用于声明服务类
import org.springframework.transaction.annotation.Transactional;
// 导入事务注解,用于声明事务方法
import java.util.ArrayList;
// 导入ArrayList类用于创建动态数组
import java.util.List;
// 导入List接口用于声明集合
import java.util.Map;
// 导入Map接口用于声明键值对集合
import java.util.Objects;
// 导入Objects类包含一些实用方法如非空判断
import java.util.concurrent.TimeUnit;
// 导入TimeUnit枚举用于指定时间单位
import java.util.function.Function;
// 导入Function接口用于声明函数式接口
import java.util.stream.Collectors;
// 导入Collectors类用于声明收集器进行集合的聚合操作
/**
* <p>
@ -97,48 +41,36 @@ import java.util.stream.Collectors;
* @since 2024-03-21
*/
@Service
// 声明这是一个服务类Spring会自动管理这个类的实例
public class CertificateServiceImpl extends ServiceImpl<CertificateMapper, Certificate> implements ICertificateService {
@Resource
// 注入CertificateMapper用于数据库操作
private CertificateMapper certificateMapper;
@Resource
// 注入CertificateConverter用于表单对象与实体对象之间的转换
private CertificateConverter certificateConverter;
@Resource
// 注入CacheClient用于操作缓存
private CacheClient cacheClient;
@Resource
// 注入StringRedisTemplate用于操作Redis字符串数据
private StringRedisTemplate stringRedisTemplate;
//新增证书
@Override
@Transactional
// 声明这是一个事务方法,保证方法中的数据库操作要么全部成功,要么全部回滚
public Result<String> addCertificate(CertificateForm certificateForm) {
Certificate certificate = certificateConverter.fromToEntity(certificateForm);
// 将表单对象转换为实体对象
//自动生成时间
int insertRows = certificateMapper.insert(certificate);
// 调用Mapper的insert方法将实体对象插入数据库返回插入的行数
if (insertRows > 0) {
if (certificate.getId() != null) { // 确保ID有效
// 如果是更新操作这里逻辑可能有误新增操作通常不会检查ID,先从缓存中移除旧数据,然后重新放入最新的数据
// 如果是更新操作,先从缓存中移除旧数据,然后重新放入最新的数据
stringRedisTemplate.delete("cache:certificate:pagingCertificate:"+certificate.getId().toString()); // 删除旧缓存
// 注释的代码是假设要存储新数据到缓存,但在这里并未执行
// Map<Integer, Certificate> map = Map.of(certificate.getId(), certificate);
// cacheClient.batchPut("cache:certificate:pagingCertificate:",map,10L,TimeUnit.MINUTES); // 存储新数据
}
return Result.success("添加证书成功");
// 返回成功结果
} else {
return Result.failed("添加证书失败");
// 返回失败结果
}
}
@ -147,20 +79,15 @@ public class CertificateServiceImpl extends ServiceImpl<CertificateMapper, Certi
public Result<IPage<Certificate>> pagingCertificate(Integer pageNum, Integer pageSize, String certificateName,
String certificationUnit) {
// 查询满足条件的总记录数
int total = certificateMapper.countByCondition(SecurityUtil.getUserId(), certificateName,certificationUnit);
// 调用Mapper的countByCondition方法根据条件计数获取总记录数
int total = certificateMapper.countByCondition(SecurityUtil.getUserId(), certificateName,certificationUnit); // 假设gradeMapper中实现了根据条件计数的方法
// 计算偏移量
int offset = (pageNum - 1) * pageSize;
// 根据页码和页面大小计算偏移量
// 查询分页ID列表
List<Integer> certificateIds = certificateMapper.selectCertificateIdsPage(SecurityUtil.getUserId(), certificateName,certificationUnit, offset, pageSize);
// 调用Mapper的selectCertificateIdsPage方法根据条件分页查询ID列表
// 批量从缓存中获取Certificate对象
// 批量从缓存中获取GradeVO对象
Map<Integer, Certificate> cachedGradesMap = cacheClient.batchGet("cache:certificate:pagingCertificate:",certificateIds, Certificate.class);
// 从缓存中获取ID列表对应的Certificate对象
// 确定未命中的ID列表
List<Integer> missIds = new ArrayList<>();
@ -169,49 +96,75 @@ public class CertificateServiceImpl extends ServiceImpl<CertificateMapper, Certi
missIds.add(id);
}
}
// 遍历ID列表找出未缓存的ID
// 如果有未命中的ID从数据库批量查询并更新缓存
if (!missIds.isEmpty()) {
List<Certificate> missedGrades = certificateMapper.batchSelectByIds(missIds);
// 从数据库查询未缓存的Certificate对象
// 使用Collectors.toMap转换构建Map
// 假设GradeVO的ID为getId()使用Collectors.toMap转换
Map<Integer, Certificate> missedGradesMap = missedGrades.stream()
.collect(Collectors.toMap(Certificate::getId, Function.identity()));
// 将查询结果转换为Map
// 更新缓存
cacheClient.batchPut("cache:certificate:pagingCertificate:",missedGradesMap,10L, TimeUnit.MINUTES);
// 将查询结果存入缓存
// 合并缓存结果
cachedGradesMap.putAll(missedGradesMap);
// 将新查询的结果合并到原来的Map中
}
// 根据ID列表从缓存中获取完整的Certificate对象列表
// 根据ID列表从缓存中获取完整的GradeVO对象列表
List<Certificate> finalResult = new ArrayList<>(certificateIds.size());
for (Integer id : certificateIds) {
finalResult.add(cachedGradesMap.get(id));
}
// 根据ID列表从合并后的Map中获取Certificate对象构建结果列表
// 构建并返回IPage对象
IPage<Certificate> resultPage = new Page<>(pageNum, pageSize, Long.valueOf(total));
resultPage.setRecords(finalResult);
// 创建分页对象,设置页码、页面大小、总记录数和结果列表
return Result.success("查询成功", resultPage);
// 返回成功结果,包含分页对象
}
@Override
@Transactional
public Result<String> updateCertificate(CertificateForm certificateForm) {
Certificate certificate = certificateConverter.fromToEntity(certificateForm);
// 将表单对象转换为实体对象
// 调用mapper方法更新证书
int affectedRows = certificateMapper.updateById(certificate);
// 调用Mapper的updateById方法根据ID更新实体对象返回
if (affectedRows > 0) {
if (certificate.getId() != null) { // 确保ID有效
// 如果是更新操作,先从缓存中移除旧数据,然后重新放入最新的数据
stringRedisTemplate.delete("cache:certificate:pagingCertificate:"+certificate.getId().toString()); // 删除旧缓存
// Map<Integer, Certificate> map = Map.of(certificate.getId(), certificate);
// cacheClient.batchPut("cache:certificate:pagingCertificate:",map,10L,TimeUnit.MINUTES); // 存储新数据
}
return Result.success("修改证书成功");
} else {
return Result.failed("修改证书失败");
}
}
@Override
@Transactional
public Result<String> deleteCertificate(Integer id) {
LambdaUpdateWrapper<Certificate> certificateLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
certificateLambdaUpdateWrapper.eq(Certificate::getId,id)
.set(Certificate::getIsDeleted,1);
int affectedRows = certificateMapper.update(certificateLambdaUpdateWrapper);
if (affectedRows > 0) {
stringRedisTemplate.delete("cache:certificate:pagingCertificate:"+id);
return Result.success("删除证书成功");
} else {
return Result.failed("删除证书失败");
}
}
@Override
public Result<IPage<MyCertificateVO>> getMyCertificatePaging(Integer pageNum, Integer pageSize,String examName) {
Page<MyCertificateVO> myCertificateVOPage = new Page<>();
myCertificateVOPage = certificateMapper.selectMyCertificate(myCertificateVOPage,pageNum,pageSize, SecurityUtil.getUserId(),examName);
return Result.success("查询成功",myCertificateVOPage);
}
}

@ -1,26 +1,10 @@
package cn.org.alan.exam.service.impl;
// 这行代码声明了该类所在的包名,表明这个类属于"cn.org.alan.exam.service.impl"包,包名通常按照一定的层次结构来组织代码,方便管理和区分不同功能模块的类,
// 在这里可以看出是该项目中与考试相关的服务实现类所在的包,后续其他类可以通过这个包名来引用该类等操作。
import cn.org.alan.exam.mapper.CertificateUserMapper;
// 导入"cn.org.alan.exam.mapper"包下的CertificateUserMapper类从类名推测这个Mapper类是用于操作CertificateUser证书用户相关的实体类可能对应数据库中的表
// 的数据库持久层接口,通过导入它可以在当前类中使用其定义的方法来进行与证书用户数据相关的数据库操作,比如插入、查询等操作。
import cn.org.alan.exam.model.entity.CertificateUser;
// 导入"cn.org.alan.exam.model.entity"包下的CertificateUser类这个类就是代表证书用户相关的实体类定义了与证书用户相关的各种属性比如用户ID、证书ID等
// 它通常与数据库中的表结构相对应,在代码中用于承载和传递证书用户相关的数据,方便进行业务逻辑处理以及与数据库的交互操作。
import cn.org.alan.exam.service.ICertificateUserService;
// 导入"cn.org.alan.exam.service"包下的ICertificateUserService接口这个接口应该是定义了关于证书用户相关服务的一系列方法签名比如获取证书用户信息、添加证书用户等方法的声明
// 当前类实现了这个接口,意味着要按照接口中定义的规范来实现具体的业务逻辑方法,保证服务层对证书用户操作的一致性和规范性。
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
// 导入MyBatis Plus框架中的ServiceImpl类它是一个基础的服务实现类提供了很多通用的数据库操作相关的方法实现比如基于实体类和对应的Mapper接口进行常见的增删改查操作等
// 在这里继承它可以复用其已有的功能代码,减少重复开发,方便快速构建针对特定实体类的服务实现类,只需要关注具体业务逻辑特有的部分即可。
import org.springframework.stereotype.Service;
// 导入Spring框架中的@Service注解这个注解用于标识当前类是一个服务层Service Layer的组件告诉Spring容器这个类是一个业务逻辑处理的服务类
// Spring容器会对其进行管理例如进行依赖注入等操作使得该类可以方便地在整个项目中被其他组件调用参与到业务流程中。
/**
* <p>
@ -30,19 +14,7 @@ import org.springframework.stereotype.Service;
* @author WeiJin
* @since 2024-03-21
*/
// 以上是JavaDoc格式的注释用于对类进行简要的说明这里说明这个类是一个服务实现类不过具体实现的服务相关内容没有详细描述
// 通常可以在这里进一步补充类的功能、作用等详细信息,方便后续开发人员查看和理解代码。
@Service
// 使用@Service注解将当前类标记为Spring框架中的服务组件让Spring容器识别并管理它这样在其他地方比如控制层等就可以通过依赖注入的方式来使用这个服务类提供的方法了。
public class CertificateUserServiceImpl extends ServiceImpl<CertificateUserMapper, CertificateUser> implements ICertificateUserService {
// 定义了一个名为CertificateUserServiceImpl的类它继承自MyBatis Plus框架中的ServiceImpl类
// 并指定了泛型参数为CertificateUserMapper用于操作证书用户数据的持久层接口和CertificateUser证书用户相关的实体类
// 这样就继承了ServiceImpl类中针对CertificateUser实体类的通用数据库操作方法实现比如可以直接调用父类的save、update等方法来操作数据库中的证书用户数据。
// 同时这个类还实现了ICertificateUserService接口意味着它需要按照该接口中定义的方法签名来实现具体的业务逻辑方法
// 从而提供符合接口规范的证书用户相关服务,确保在整个项目中,对于证书用户服务的调用具有统一的接口和行为表现,方便进行代码的维护和扩展。
// 不过目前这个类中没有添加额外的自定义方法实现(可能后续会根据业务需求添加具体的业务逻辑代码),只是通过继承和实现接口构建了一个基础的服务实现类框架,
// 复用了已有框架提供的功能来处理与证书用户相关的数据库操作和服务提供相关事宜。
}

@ -1,38 +1,17 @@
package cn.org.alan.exam.service.impl;
// 声明包名,用于组织类文件
import cn.org.alan.exam.common.result.Result;
// 导入Result类用于封装返回结果
import cn.org.alan.exam.mapper.ExamQuAnswerMapper;
// 导入Mapper接口用于数据库操作
import cn.org.alan.exam.model.entity.ExamQuAnswer;
// 导入实体类,对应数据库表
import cn.org.alan.exam.model.vo.score.QuestionAnalyseVO;
// 导入VO类用于封装返回给前端的数据
import cn.org.alan.exam.service.IExamQuAnswerService;
// 导入服务接口,定义业务逻辑
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
// 导入MyBatis Plus的QueryWrapper类用于构建查询条件
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
// 导入ServiceImpl类用于简化服务类的实现
import jakarta.annotation.Resource;
// 导入Resource注解用于注入依赖
import org.springframework.stereotype.Service;
// 导入Service注解用于声明服务类
import java.text.DecimalFormat;
// 导入DecimalFormat类用于格式化数字
import java.util.List;
// 导入List接口用于处理集合数据
/**
* <p>
@ -43,31 +22,19 @@ import java.util.List;
* @since 2024-03-21
*/
@Service
// 使用Service注解声明这是一个服务类Spring会自动管理这个类的实例
public class ExamQuAnswerServiceImpl extends ServiceImpl<ExamQuAnswerMapper, ExamQuAnswer> implements IExamQuAnswerService {
@Resource
// 使用Resource注解注入ExamQuAnswerMapper的实例用于数据库操作
private ExamQuAnswerMapper examQuAnswerMapper;
@Override
// 重写IExamQuAnswerService接口中的questionAnalyse方法
public Result<QuestionAnalyseVO> questionAnalyse(Integer examId, Integer questionId) {
QuestionAnalyseVO questionAnalyseVO = examQuAnswerMapper.questionAnalyse(examId, questionId);
// 调用Mapper接口的questionAnalyse方法获取题目分析数据
//正确率保留两位小数
DecimalFormat format = new DecimalFormat("#.00");
// 创建DecimalFormat对象用于格式化数字为保留两位小数的字符串
String strAccuracy = format.format(questionAnalyseVO.getRightCount() / questionAnalyseVO.getTotalCount());
// 计算正确率并格式化为保留两位小数的字符串
questionAnalyseVO.setAccuracy(Double.parseDouble(strAccuracy));
// 将格式化后的正确率字符串转换为Double类型并设置到VO对象中
return Result.success(null, questionAnalyseVO);
// 封装返回结果,返回成功信息和题目分析数据
}
}
}

@ -1,33 +1,10 @@
package cn.org.alan.exam.service.impl;
// 这行代码定义了该类所属的包名,表明这个类位于"cn.org.alan.exam.service.impl"这个包结构下。
// 在Java项目中包用于对类进行分类管理按照功能、模块等逻辑将相关的类组织在一起方便代码的维护、查找以及避免类名冲突等这里就是存放考试相关服务实现类的包。
import cn.org.alan.exam.mapper.ExamRepoMapper;
// 导入"cn.org.alan.exam.mapper"包下的ExamRepoMapper类。从类名可以推测ExamRepoMapper是一个数据持久层的映射器Mapper接口
// 它的作用是定义了一系列用于操作ExamRepo考试资源库相关实体类大概率对应数据库中的表实体对应数据库表的方法比如增删改查等操作方法的声明
// 导入它后就能在当前类中调用其相关方法与数据库中的考试资源库数据进行交互了。
import cn.org.alan.exam.model.entity.ExamRepo;
// 导入"cn.org.alan.exam.model.entity"包下的ExamRepo类这个类代表考试资源库相关的实体类它里面定义了和考试资源库对应的各种属性
// 例如可能包含资源库的名称、描述、包含的资源类型等属性,并且这些属性通常与数据库中存储考试资源库信息的表结构中的字段是一一对应的,
// 在代码中主要用于承载和传递考试资源库相关的数据,方便后续业务逻辑围绕这些数据进行处理以及与数据库之间的交互操作。
import cn.org.alan.exam.service.IExamRepoService;
// 导入"cn.org.alan.exam.service"包下的IExamRepoService接口。这个接口是定义了关于考试资源库相关服务的统一规范
// 里面声明了一系列针对考试资源库业务操作的方法,比如获取资源库列表、添加资源库、更新资源库信息等方法签名,
// 当前类实现了这个接口,那就意味着需要按照接口里规定的方法签名来具体实现相应的业务逻辑,以此保证在整个项目中,
// 对考试资源库服务的调用都遵循统一的接口标准,便于不同模块之间协同以及代码的后续扩展和维护。
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
// 导入MyBatis Plus框架提供的ServiceImpl类。它是一个非常实用的基础服务实现类封装了很多通用的、基于数据库操作的功能实现
// 比如基于给定的实体类和对应的Mapper接口它已经实现好了像常见的插入save、更新update、删除delete以及根据条件查询select等方法
// 在这里让当前类继承它,就能复用这些已经写好的通用数据库操作逻辑,我们只需要关注本类涉及的考试资源库业务逻辑中特有的部分就可以了,
// 这样可以大大减少代码的重复编写,提高开发效率。
import org.springframework.stereotype.Service;
// 导入Spring框架中的@Service注解类。这个注解用于向Spring容器表明当前类是一个服务层Service Layer的组件
// Spring容器在进行组件扫描时识别到这个注解就会把该类纳入管理范围例如会为其进行依赖注入等操作
// 使得这个类可以方便地在整个项目的其他地方被调用,参与到具体的业务流程中,实现业务逻辑的解耦和复用。
/**
* <p>
@ -37,21 +14,7 @@ import org.springframework.stereotype.Service;
* @author WeiJin
* @since 2024-03-21
*/
// 这是JavaDoc形式的注释部分用于对类做一个简单的介绍说明这里只是简单提及是服务实现类
// 其实可以更详细地描述下这个类针对考试资源库具体实现了哪些业务功能等信息,方便后续查看代码的开发人员快速理解类的作用。
@Service
// 使用@Service注解标记当前类告诉Spring容器这是一个需要被管理的服务类。
// 有了这个注解Spring容器会按照配置对这个类进行实例化、依赖注入等操作使其成为整个Spring应用上下文ApplicationContext中的一个组件
// 这样其他的组件(比如控制层的类等)就可以通过依赖注入的方式获取并使用这个服务类所提供的功能了。
public class ExamRepoServiceImpl extends ServiceImpl<ExamRepoMapper, ExamRepo> implements IExamRepoService {
// 定义了一个名为ExamRepoServiceImpl的公共类它继承自MyBatis Plus框架的ServiceImpl类
// 并指定了泛型参数为ExamRepoMapper前面导入的用于操作考试资源库数据的Mapper接口和ExamRepo考试资源库相关的实体类
// 通过这种继承方式当前类自动获得了ServiceImpl类中针对ExamRepo实体类的那些通用的数据库操作方法实现
// 比如可以直接调用父类中定义好的插入、更新、查询等方法来操作数据库里的考试资源库相关数据,减少了自行编写数据库操作代码的工作量。
// 同时这个类还实现了IExamRepoService接口这就要求它必须按照IExamRepoService接口里声明的各个方法签名来具体实现对应的业务逻辑
// 不过目前这个类里暂时没有添加额外的自定义方法实现代码(可能后续根据业务需求会添加具体的考试资源库相关业务逻辑代码),
// 现阶段只是搭建了一个符合框架规范的基础服务实现类的框架结构,借助已有的框架功能来处理与考试资源库相关的数据库操作以及服务提供相关事务。
}

@ -1,32 +1,15 @@
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
@ -34,63 +17,38 @@ 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();
// 从传入的 `MultipartFile` 文件对象中获取其原始文件名即客户端上传文件时的原始文件名包含后缀名通过获取这个文件名后续可以基于它来生成一个新的唯一文件名达到避免文件覆盖的目的因为如果直接使用原始文件名上传到OSS可能会出现同名文件被覆盖的情况所以需要进行文件名的处理这里先获取原始文件名作为后续操作的基础。
assert originalFilename!= null : "上传文件时获取文件名失败为null";
// 使用断言(`assert`)语句来确保获取到的原始文件名不为 `null`,如果 `originalFilename` 为 `null`,则会抛出 `AssertionError` 异常并显示指定的错误信息 "上传文件时获取文件名失败为null",这是一种简单的防御性编程方式,在开发阶段可以快速发现可能出现的空指针问题,保证后续基于文件名的操作能够正常进行,不过在生产环境中通常断言是可以被禁用的,需要配合合适的日志记录等其他方式来监控这类潜在问题。
assert originalFilename != null : "上传文件时获取文件名失败为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` 属性值,使得用户能够查看上传的图片等,完成文件上传方法并返回相关有用信息的整个流程。
}
@ -101,17 +59,9 @@ 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集合框架提供的便利方法来进行字符串的包含关系判断。
}
/**
@ -121,9 +71,6 @@ 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,426 +1,243 @@
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;
// 导入 `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使用Hutool的JSONUtil工具类的parseObj方法将获取到的JSON字符串解析为JSONObject对象方便后续从中提取具体的属性值JSONObject可以看作是一个类似Map结构的对象用于操作JSON数据中的键值对信息。
// 解析JSON
JSONObject jsonObject = JSONUtil.parseObj(json);
// 调用 `JSONUtil` 工具类(假设使用了相关的 JSON 处理工具库,用于方便地进行 JSON 数据与 Java 对象之间的转换操作)的 `parseObj` 方法,传入获取到的 JSON 字符串(`json`),这个方法会将 JSON 字符串解析为 `JSONObject` 类型的对象,`JSONObject` 在功能上类似 Java 中的 `Map`,可以方便地通过键来获取对应的值,便于后续从解析后的 JSON 数据中提取出分页数据相关的各个属性值,比如记录列表、总记录数、每页大小、当前页码等信息,为构建完整的 `IPage` 对象做准备。
// 获取记录列表并转换为List<T>通过从解析后的JSONObject对象中获取名为"records"的JSON数组对应IPage中的数据记录列表再利用JSONUtil的toList方法将其转换为指定类型voClass的列表这样就得到了分页数据中的实际记录列表数据用于后续构建IPage对象。
// 获取记录列表并转换为List<T>
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类是MyBatis Plus中用于表示分页数据的类有相应的方法来设置分页相关的属性先将前面获取到的记录列表设置到Page对象中然后分别从解析后的JSONObject对象中获取"total"(总记录数)、"size"(每页大小)、"current"当前页码等属性值并设置到Page对象相应的属性上这样就构建好了一个完整的IPage对象用于返回符合分页数据格式要求的结果。
// 创建Page对象并设置属性
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 类型返回
// 由于方法期望返回的类型是由泛型参数 `R` 指定的类型,而在这里构建好的是 `IPage<T>` 类型的对象 `page`,所以需要将 `page` 对象强制转换为 `R` 类型后再返回给调用者,这里的强制转换要求 `R` 类型在实际调用时必须是与 `IPage<T>` 类型兼容(比如 `R` 就是 `IPage<T>` 或者是它的父类等情况),否则会在运行时出现类型转换异常,通过这样的返回操作,将符合业务要求和分页数据格式的结果返回给调用者,完成在缓存命中且期望返回类型为 `IPage` 时的数据处理及返回流程。
} else if (type.equals(List.class)) {
} else if(type.equals(List.class)){
// TODO 3. 存在,返回商户信息
// 如果期望返回的数据类型是List直接使用JSONUtil的toList方法将获取到的JSON字符串转换为指定类型voClass的列表然后将其强制转换为泛型要求的类型R并返回这样就将缓存中存储的JSON格式的数据转换为符合业务需求的列表类型数据返回给调用者。
List<T> gradeVOList = JSONUtil.toList(json, voClass);
// 调用 `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 (R)gradeVOList;
}else{
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) {
// 如果成功获取到锁isLock为true说明当前线程获得了进行缓存重建的权限接下来需要再次检测Redis缓存是否过期这一步骤被称为“DoubleCheck”双重检查主要目的是防止在获取锁的短暂时间间隔内其他线程已经完成了缓存重建并更新了缓存使得当前线程无需再重复进行缓存重建操作进一步优化性能并保证缓存数据的准确性和一致性。
// 再次从Redis中获取缓存数据对应的JSON字符串这里的键"cache:shop:" + id可能是用于标识该缓存数据在Redis中的存储位置的另一种形式具体取决于项目中的缓存键设计规范通过重新获取数据来进行后续的过期判断等操作确保基于最新的缓存状态来决定是否真正需要进行缓存重建工作。
// TODO 6.3 成功获取锁成功应该再次检测Redis缓存是否过期做DoubleCheck如果存在则无序重建缓存
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())) {
// 如果缓存数据的逻辑过期时间expireTime在当前时间之后说明在获取锁后再次检查发现缓存数据仍然未过期仍然有效直接将之前反序列化得到的业务数据r返回给调用者使得调用者可以使用缓存中的有效数据进行后续业务操作避免了不必要的数据库查询和缓存重建操作提高了查询性能同时也保证了数据的及时性和准确性。
// TODO 未过期,返回商铺信息
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 写入缓存要带有逻辑过期
// 调用本类的setWithLogicalExpire方法将从数据库中获取到的最新数据r1按照带有逻辑过期时间的方式写入到Redis缓存中其中设置的过期时间是在传入的固定过期时间time基础上加上一个随机时长random.nextInt(20)),这样做可以进一步分散缓存过期时间,避免大量缓存同时过期引发缓存雪崩等问题,实现更合理、更稳定的缓存更新操作,保证缓存系统的可靠性和数据的时效性。
this.setWithLogicalExpire(key, r1, time + random.nextInt(20), unit);
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 失败,返回已经过期的商品信息
// 如果获取锁失败isLock为false说明当前线程没有获得缓存重建的权限可能已经有其他线程正在进行缓存重建操作此时直接将之前已经获取到的虽然已经过期业务数据r返回给调用者这样调用者可以先使用这个过期的数据进行一些展示或者其他不依赖最新数据的操作具体根据业务需求而定同时也避免了当前线程长时间等待或者重复尝试获取锁等不必要的操作在一定程度上保证了系统的响应性能和可用性。
// TODO 6.4 失败,返回已经过期的商品信息
return r;
}
// 拿到锁
private boolean tryLock(String key) {
// setIfAbsent方法就是Redis中的setnx
// 解释了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代码中进行逻辑判断和处理。
// 在Redis命令行中的运行结果就是0或者1但是在这的运行结果是true或false但是返回的是Boolean类型封装类
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);
// 从前面通过 `multiGet` 方法获取到的缓存数据列表 `values` 中,根据当前循环的索引 `i` 获取对应位置的缓存数据(以字符串形式存在,如果对应键在 Redis 中不存在缓存数据,则此处获取到的就是 `null`),将其存储在变量 `value` 中,方便后续判断该缓存数据是否存在以及进行反序列化等操作。
if (value!= null) {
// 如果获取到的某个缓存数据value不为null说明Redis中存在对应键的缓存数据此时调用deserialize方法本类中自定义的用于将JSON字符串反序列化为指定类型对象的方法将缓存数据的JSON字符串value反序列化为期望的类型clazz指定的类型的对象以便后续可以在业务逻辑中直接使用该对象进行操作。
if (value != null) {
// 反序列化字符串为对象
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` 方法的上层业务逻辑可以捕获到这个异常,然后根据具体业务需求进行相应的处理,比如记录日志、向客户端返回合适的错误信息等操作,通过这种异常处理机制,确保系统在面对各种异常情况时能够有相应的应对措施,维持系统的稳定性和可靠性,避免因异常导致系统出现不可预期的行为或者直接崩溃。
}
}
@ -429,77 +246,39 @@ 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,9 +1,6 @@
package cn.org.alan.exam.util;
import java.security.SecureRandom;
// 引入Java标准库中用于生成安全随机数的类。在很多涉及到安全敏感信息生成的场景中普通的随机数生成器可能存在一定的可预测性风险例如基于简单的算法和固定的种子值来生成随机数容易被攻击者分析规律并猜出后续的随机数情况。
// 而SecureRandom类则不同它借助系统底层提供的更具随机性的源来生成随机数比如在一些操作系统中它可能会利用硬件的随机数生成器像基于物理噪声等产生真正随机的信息源或者经过复杂的算法对系统的多种动态信息进行处理来生成随机数使得每次生成的随机数都具有很强的随机性很难被外界预测。
// 在本案例中生成班级口令是需要保证口令具有足够的不可预测性避免被轻易猜出所以使用SecureRandom类来生成随机数以满足对安全性有要求的这种场景需求。
/**
* @Author Alan
@ -12,48 +9,19 @@ import java.security.SecureRandom;
*/
public class ClassTokenGenerator {
// 定义一个私有静态的字符串常量,用于指定生成班级口令时可选用的字符集合。之所以选择包含所有大写字母("ABCDEFGHIJKLMNOPQRSTUVWXYZ")、小写字母("abcdefghijklmnopqrstuvwxyz")以及数字("0123456789")的这样一个全面的字符集合,是因为这样能极大地丰富口令的组成可能性。
// 从信息论角度来看,字符集合越丰富,可组合出的不同口令数量就越多,其随机性和多样性也就越高。例如,如果只使用数字来生成口令,那么可能的组合数量相对有限,容易被暴力破解等方式猜出。
// 而通过涵盖大小写字母和数字,就可以生成各种各样复杂的口令字符串,大大增加了口令的安全性和复杂性,使得生成的班级口令能够满足作为唯一标识且不易被他人轻易获取或猜到的要求,适用于像在线教育平台等场景中区分不同班级的访问口令等业务需求。
private static final String CHAR_POOL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
/**
*
* length
// 在实际的业务场景中,比如在线教育平台里,不同班级可能需要各自独立的访问口令来保证教学资源的安全性以及班级的独立性,通过这个方法就可以为每个班级动态地生成一个合适的、具有一定安全性的口令。
// 调用者可以根据实际的安全策略、使用场景以及方便记忆等综合因素来合理指定口令的长度,一般来说,长度越长,口令的理论上的安全性就越高,因为随着长度增加,可能的组合数量会呈指数级增长,暴力破解的难度也就越大。
// 不过也要考虑到用户使用的便利性,如果口令过长可能导致用户难以记忆或者输入出错等问题,所以需要在安全性和便利性之间进行权衡,通过合理设置该参数来找到一个平衡点,然后生成符合要求的班级口令用于后续在相关业务逻辑中作为班级的标识口令使用,例如存储在数据库中与班级信息关联,或者展示给班级管理员等相关人员知晓并使用。
* @param length 使便
* @return lengthCHAR_POOL使
* @param length
* @return
*/
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++) {
// 调用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.append(CHAR_POOL.charAt(random.nextInt(CHAR_POOL.length())));
}
// 当循环结束后通过调用tokenBuilder的toString方法将构建好的包含随机字符的可变字符串构建器对象转换为不可变的字符串对象。
// StringBuilder对象在内存中是以一种可变的、高效的方式来存储和操作字符数据的它便于进行字符串的动态拼接等操作但在很多业务场景中最终需要的是一个不可变的、标准的字符串对象来进行传递、存储或者进一步处理等操作。
// toString方法会根据StringBuilder内部存储的字符数据创建一个新的不可变的字符串对象这个新的字符串就是最终生成的班级口令将其返回给调用者以便在业务逻辑中使用该口令比如存储到数据库中作为班级的访问口令、展示给用户等操作使得生成的口令能够顺利融入到整个业务流程中发挥其作为班级标识口令的作用。
return tokenBuilder.toString();
}
}

@ -1,13 +1,8 @@
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
@ -16,55 +11,36 @@ 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
* "yyyy-MM-dd HH:mm:ss"LocalDateTime便使
* @return "yyyy-MM-dd HH:mm:ss"LocalDateTime使
*
* @return
*/
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
* "yyyy-MM-dd HH:mm:ss"JavaLocalDateTime便
* @param dateTime "yyyy-MM-dd HH:mm:ss""2024-03-28 14:20:30"
* @return LocalDateTimeLocalDateTime便使
*
* @param dateTime
* @return LocalDateTime
*/
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,146 +1,82 @@
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 JWTJWT
* @return
*/
public String createJwt(String userInfo, List<String> authList) {
// 创建一个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令牌。
Date issDate = new Date();//签发时间
Date expireDate = new Date(issDate.getTime() + 1000 * 60 *30);
//定义头部信息
Map<String, Object> headerClaims = new HashMap<>();
// 在头部信息的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令牌字符串。
headerClaims.put("alg", "HS256");//算法
headerClaims.put("typ", "JWT");//类型只能是jwt
return JWT.create().withHeader(headerClaims)
// 指定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中获取用户的权限信息便于进行权限判断和业务流程控制决定用户是否有权限执行某些操作等情况。
.withIssuer("wj")//签发人
.withIssuedAt(issDate)//签发时间
.withExpiresAt(expireDate)//过期时间
.withClaim("userInfo", userInfo)//自定义声明
.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
* JWTtokentruefalse便访
* @param token JWTAuthorization
* @return truefalse
*
* @param token
* @return
*/
public boolean verifyToken(String token) {
// 构建jwt校验器通过JWT.require方法并传入使用HS256算法Algorithm.HMAC256和前面获取到的密钥secret来创建一个JWTVerifier对象。
// JWTVerifier对象是用于验证JWT的核心工具它内部封装了验证所需的算法、密钥等信息后续可以使用它对传入的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;
}
@ -148,21 +84,17 @@ public class JwtUtil {
/**
* token
* JWTtokenJWTwithClaim("userInfo", userInfo)便
* @param token JWTverifyToken
* @return JWTJWTnull便
*
* @param token
* @return
*/
public String getUser(String token) {
// 构建jwt校验器和前面verifyToken方法中一样通过JWT.require方法并传入使用HS256算法Algorithm.HMAC256和密钥secret来创建一个JWTVerifier对象用于后续验证传入的JWT令牌的合法性只有验证通过的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;
}
@ -170,24 +102,20 @@ public class JwtUtil {
/**
* token
* JWTtokenJWTwithClaim("authList", authList)List<String>便访
* @param token JWTverifyToken
* @return JWTList<String>JWTnull便
*
* @param token
* @return
*/
public List<String> getAuthList(String token) {
// 构建jwt校验器再次通过JWT.require方法并传入使用HS256算法Algorithm.HMAC256和密钥secret来创建一个JWTVerifier对象用于对传入的JWT令牌进行验证只有验证通过的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) {
// 如果在验证JWT或者提取权限信息的过程中出现问题例如JWT验证失败、JWT中不存在名为"authList"的声明或者声明内容格式不符合预期等情况就会抛出JWTVerificationException异常。
// 在这里捕获这个异常并通过日志记录使用log对象输出“权限列表获取失败”的错误信息方便后续排查问题了解获取权限信息失败的情况然后返回null表示没有成功获取到权限信息调用者可以根据这个返回值进行相应处理比如提示用户权限不足等操作
log.error("权限列表获取失败");
log.error("权限列表+获取失败");
return null;
}
}
}
}

@ -1,19 +1,11 @@
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
@ -21,29 +13,14 @@ 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:"便Redis
* @param keyPrefix "cache:getNewNotice:"
*/
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" +
@ -52,18 +29,10 @@ public class RedisUtils {
"end \n" +
"return count";
// 创建一个DefaultRedisScript对象将前面定义的Lua脚本字符串以及指定脚本执行后返回的数据类型Long类型用于接收删除键的数量统计结果传入构造函数对要在Redis中执行的Lua脚本进行封装以便后续通过StringRedisTemplate来执行这个脚本。
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
// 执行Lua脚本并传递前缀作为参数通过StringRedisTemplate的execute方法来实现。
// 参数说明:
// 第一个参数是封装好的DefaultRedisScript对象redisScript它包含了要执行的Lua脚本以及返回值类型信息告诉StringRedisTemplate要执行的具体脚本内容和期望的返回结果类型。
// 第二个参数是传递给脚本的参数列表这里使用Collections.singletonList方法创建了一个只包含单个元素即要删除的键的前缀keyPrefix的不可变列表在Lua脚本中可以通过ARGV[1]来获取这个参数,用于构建匹配键的模式等操作。
// 第三个参数也是键前缀keyPrefix这里的重复传递可能是因为在某些特定的Spring Data Redis内部实现机制或者与Redis交互的场景下需要以这种方式来确保参数能正确传递和被脚本使用具体取决于框架的设计要求。
// 执行该方法后会将Lua脚本发送到Redis服务器执行并返回一个Long类型的结果即删除的键的数量将其存储在deletedCount变量中用于后续了解本次批量删除操作的执行情况。
// 执行Lua脚本并传递前缀作为参数
Long deletedCount = stringRedisTemplate.execute(redisScript, Collections.singletonList(keyPrefix), keyPrefix);
// 打印一条信息到控制台,展示本次根据指定前缀删除键的操作中,实际删除的键的数量以及对应的键前缀,方便在开发、测试或者查看日志时了解操作执行的结果情况,进行相关的调试和监控工作。
System.out.println("Deleted " + deletedCount + " keys with prefix: " + keyPrefix);
}
}
}

@ -1,33 +1,17 @@
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;
// 引入Java标准库中表示输入输出异常的类因为在操作HttpServletResponse的输出流如获取Writer对象以及向其写入内容等操作过程中可能会出现IO相关的异常情况所以需要捕获并处理这个异常保证程序的稳定性。
import java.io.PrintWriter;
// 引入用于向字符输出流写入文本内容的类在这里通过从HttpServletResponse获取对应的PrintWriter对象然后使用它将序列化后的JSON格式的结果数据写入到响应中从而将数据返回给客户端。
/**
* @Author WeiJin
@ -35,35 +19,19 @@ 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,20 +1,10 @@
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
@ -22,163 +12,91 @@ 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 encryptBase64便使Base64使
* @param data
* @return encrypt
*/
public static String encrypt(String data){
// 调用了重载的encrypt方法并传入要加密的数据以及默认的密钥KEY和初始化向量IV将加密操作委托给另一个带有完整参数的encrypt方法来执行这样做可以在外部调用时更方便只需传入要加密的数据即可而内部统一使用完整参数的方法来处理具体加密逻辑提高代码复用性和可维护性。
return encrypt(data, KEY, IV);
}
/***
* param data
* desEncryptdesEncryptKEYIVdesEncryptencrypt便
* param data
* desEncrypt
*/
public static String desEncrypt(String data){
// 该方法用于对传入的加密数据(以字符串形式传入,通常是经过之前加密操作得到的密文数据,例如从数据库中取出的已加密存储的信息或者从网络接收的加密后的数据等)进行解密操作。
// 不过此方法并没有直接进行解密的具体逻辑处理,而是调用了另一个重载的`desEncrypt`方法,并将接收到的要解密的数据(`data`)以及类中预先定义好的默认密钥(`KEY`)和默认初始化向量(`IV`)作为参数传递过去,
// 这样的设计使得外部调用更加简洁方便,只需传入要解密的数据即可,而具体的解密逻辑统一在重载的方法中处理,同时也方便在需要的时候,能以更灵活的方式(如果传入不同的密钥和向量参数)调用那个重载方法来满足多样化的解密需求。
return desEncrypt(data, KEY, IV);
}
/**
*
* @param data
* @param key key使KEY使KEY
* @param iv iv使IV
* @return Base64便使
* @param data
* @param key key
* @param iv iv
* @return
*/
private static String encrypt(String data, String key, String iv) {
private static String encrypt(String data, String key, String iv){
try {
// 通过Cipher类的getInstance方法按照指定的"算法/模式/补码方式"字符串来获取一个Cipher实例
// 此处指定的是"AES/CBC/NoPadding"意味着选用高级加密标准AES算法它是一种对称加密算法具有较高的安全性广泛应用于数据加密场景。
// 密码分组链接Cipher Block ChainingCBC模式在此模式下每个明文块在加密前会先与前一个密文块进行异或操作这样可以增加加密的随机性和安全性避免相同的明文块加密后得到相同的密文块情况出现。
// 无填充NoPadding方式表示不对数据进行额外的填充处理不过这要求输入的数据长度需要符合加密算法数据块大小的整数倍要求不同的选择会对加密的特性、安全性以及对输入数据的处理要求等方面产生影响当前这种组合是基于项目的具体需求来确定的比如综合考虑了对数据长度兼容性、整体安全性等多方面因素。
//"算法/模式/补码方式"NoPadding PkcsPadding
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) {
// 通过计算加密算法数据块大小与当前明文长度除以块大小所得余数的差值,来确定需要补充的字节数,
// 例如块大小为16字节当前明文长度为18字节18除以16余数为2那么需要补充的字节数就是16 - 2 = 14字节将这个差值加到当前明文长度上就得到了符合要求的调整后长度。
if (plaintextLength % blockSize != 0) {
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);
// 使用Base64编码工具类的encodeBase64String静态方法将加密后得到的字节数组encrypted转换为Base64编码后的字符串形式
// Base64编码是一种将二进制数据转换为文本格式的编码方式它使得加密后的密文数据可以以文本形式方便地在网络中传输避免了直接使用二进制密文可能带来的兼容性问题例如某些传输协议对二进制数据处理不太友好或者存储时二进制数据可能出现乱码等情况经过编码后的字符串最终会作为加密结果返回给调用者供其在后续的业务逻辑中使用比如将加密后的用户密码存储到数据库中或者传输给其他需要处理加密数据的服务端等场景。
// return new Base64().encodeToString(encrypted);
return Base64.encodeBase64String(encrypted);
} catch (Exception e) {
// 如果在加密过程中出现了任何异常情况例如指定的加密算法在当前运行环境中不被支持、传入的密钥格式不符合要求、Cipher对象初始化失败等各类可能导致加密操作无法正常进行的问题
// 通过调用e.printStackTrace()方法打印出异常的堆栈信息这个堆栈信息包含了异常发生的详细调用路径、所在的类和方法以及具体的错误原因等内容方便开发人员在排查问题时准确地定位到异常发生的根源以及相关的代码位置同时返回null值表示此次加密操作失败调用者可以通过判断返回值是否为null来知晓加密是否成功并据此进行相应的处理操作比如向用户提示加密失败等提示信息以便在应用层面进行合理的应对。
e.printStackTrace();
return null;
}
}
/**
*
* @param data Base64
* @param key key使使使KEY使
* @param iv ivCBC使使
* @return Base64trim使
* @param data
* @param key key
* @param iv iv
* @return
*/
private static String desEncrypt(String data, String key, String iv) {
// 此方法是用于执行具体的解密操作的私有静态方法,接收三个参数:
// 参数 `data` 表示要解密的数据它是一个经过加密后且通常以Base64编码的字符串形式传入的密文信息比如从数据库中获取的已加密存储的用户敏感信息或者从网络传输过来的加密数据等。
// 参数 `key` 代表解密所使用的密钥,其类型为字符串,这个密钥需要与加密时使用的密钥完全一致,才能正确地还原出原始的明文内容,若密钥不匹配则无法成功解密。
// 参数 `iv` 是解密操作使用的初始化向量,同样为字符串类型,它也必须和加密过程中使用的初始化向量保持相同,在特定的加密算法模式(此处为 `AES/CBC/NoPadding` 中的CBC模式初始化向量对于正确解密起着重要作用与密钥共同配合来还原出原始信息。
private static String desEncrypt(String data, String key, String iv){
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,27 +1,14 @@
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
@ -29,47 +16,36 @@ import java.util.List;
* @Date 2024/3/30 0:10
*/
@Slf4j
// 使用@Slf4j注解让Lombok在编译期自动为这个类生成名为“log”的日志记录对象以便在类中可以方便地使用这个对象记录各类运行时的日志信息例如记录获取用户相关信息过程中出现的异常情况等便于后续排查问题。
public class SecurityUtil {
//该类内容
private SecurityUtil(){}
// 将构造函数定义为私有,这是一种常见的设计模式,用于防止外部实例化这个工具类,因为工具类中的方法都是静态的,不需要创建类的实例就可以直接调用,通过把构造函数私有,可以避免不必要的实例化操作,保证工具类的使用方式符合其设计初衷,即只通过类名直接调用静态方法来使用其功能。
/**
* id
* @return idID使ID
* @return 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 ID使便
* @return
*/
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