核心业务流程
自定义上传题目流程:
用户答题流程:
AI 创建题目流程:
时序图:
架构设计
在对登录用户的权限进行判断时,不再通过条件判断,编写一大串代码去实现,可以通过写一个Java注解,如
package com.yupi.qidada.annotation;import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;/*** 权限校验**/ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AuthCheck {/*** 必须有某个角色** @return*/String mustRole() default "";}
这是一个Java注解,用于权限验证。它有两个属性:mustRole和excludeRole。mustRole表示必须有某个角色才能访问该方法,excludeRole表示没有某个角色才能访问该方法。这个注解可以应用于方法上,用于限制访问权限。
实现原理:
- 使用@Target和@Retention注解来定义注解的作用范围和生命周期。@Target(ElementType.METHOD)表示这个注解只能应用于方法上,@Retention(RetentionPolicy.RUNTIME)表示这个注解在运行时可用。
- 使用@interface关键字定义一个名为AuthCheck的注解,包含两个属性:mustRole和excludeRole。
- 在AuthCheck注解中,使用default关键字为mustRole属性设置一个默认值。
- 使用ElementType.METHOD表示这个注解只能应用于方法上。
- 使用RetentionPolicy.RUNTIME表示这个注解在运行时可用。
用途:
这个注解可以用于权限验证,限制访问权限。例如,在一个方法上添加@AuthCheck(mustRole="admin"),表示只有具有admin角色的用户才能访问该方法。
package com.yupi.qidada.aop;import com.yupi.qidada.annotation.AuthCheck; import com.yupi.qidada.common.ErrorCode; import com.yupi.qidada.exception.BusinessException; import com.yupi.qidada.model.entity.User; import com.yupi.qidada.model.enums.UserRoleEnum; import com.yupi.qidada.service.UserService; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes;import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest;/*** 权限校验 AOP**/ @Aspect @Component public class AuthInterceptor {@Resourceprivate UserService userService;/*** 执行拦截** @param joinPoint* @param authCheck* @return*/@Around("@annotation(authCheck)")public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {String mustRole = authCheck.mustRole();RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();// 当前登录用户User loginUser = userService.getLoginUser(request);UserRoleEnum mustRoleEnum = UserRoleEnum.getEnumByValue(mustRole);// 不需要权限,放行if (mustRoleEnum == null) {return joinPoint.proceed();}// 必须有该权限才通过UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(loginUser.getUserRole());if (userRoleEnum == null) {throw new BusinessException(ErrorCode.NO_AUTH_ERROR);}// 如果被封号,直接拒绝if (UserRoleEnum.BAN.equals(userRoleEnum)) {throw new BusinessException(ErrorCode.NO_AUTH_ERROR);}// 必须有管理员权限if (UserRoleEnum.ADMIN.equals(mustRoleEnum)) {// 用户没有管理员权限,拒绝if (!UserRoleEnum.ADMIN.equals(userRoleEnum)) {throw new BusinessException(ErrorCode.NO_AUTH_ERROR);}}// 通过权限校验,放行return joinPoint.proceed();} }
这是一个基于Spring AOP(面向切面编程)的权限校验拦截器。它主要用于在方法执行前,对请求进行权限校验。
1. 首先,定义了一个名为`AuthInterceptor`的类,并使用`@Aspect`和`@Component`注解进行标注,表示这是一个切面类,并且会自动被Spring容器管理。
2. 在`AuthInterceptor`类中,定义了一个名为`doInterceptor`的方法,该方法使用`@Around`注解进行标注,表示这是一个环绕通知,会在目标方法执行前后进行拦截。
3. 在`doInterceptor`方法中,首先获取`AuthCheck`注解中的`mustRole`属性值,然后通过`UserService`获取当前登录用户的信息。
4. 判断`mustRole`属性值是否为空,如果为空,则表示不需要权限,直接放行。
5. 如果`mustRole`属性值不为空,则需要进行权限校验。首先判断当前登录用户的角色是否与`mustRole`属性值相等,如果相等,则表示用户具有该权限,放行。
6. 如果当前登录用户的角色与`mustRole`属性值不相等,则判断用户是否被封号。如果被封号,则抛出`BusinessException`异常。
7. 如果用户没有被封号,则判断用户是否具有管理员权限。如果具有管理员权限,则放行。
8. 如果用户不具有管理员权限,则抛出`BusinessException`异常。
示例:
/*** 应用审核* @param reviewRequest* @param request* @return*/ @PostMapping("/review") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse<Boolean> doAppReview(@RequestBody ReviewRequest reviewRequest, HttpServletRequest request) {ThrowUtils.throwIf(reviewRequest == null, ErrorCode.PARAMS_ERROR);Long id = reviewRequest.getId();Integer reviewStatus = reviewRequest.getReviewStatus();// 校验ReviewStatusEnum reviewStatusEnum = ReviewStatusEnum.getEnumByValue(reviewStatus);if (id == null || reviewStatusEnum == null) {throw new BusinessException(ErrorCode.PARAMS_ERROR);}// 判断是否存在App oldApp = appService.getById(id);ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR);// 已是该状态if (oldApp.getReviewStatus().equals(reviewStatus)) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "请勿重复审核");}// 更新审核状态User loginUser = userService.getLoginUser(request);App app = new App();app.setId(id);app.setReviewStatus(reviewStatus);app.setReviewerId(loginUser.getId());app.setReviewTime(new Date());boolean result = appService.updateById(app);ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);return ResultUtils.success(true); }
全局执行器
为了简化外部调用,需要根据不同的应用类别和评分策略,选择对应的策略执行,因此需要一个全局执行器。
2 种实现方式:
1)编程式,在内部计算选用何种策略:
@Service @Deprecated public class ScoringStrategyContext {@Resourceprivate CustomScoreScoringStrategy customScoreScoringStrategy;@Resourceprivate CustomTestScoringStrategy customTestScoringStrategy;/*** 评分** @param choiceList* @param app* @return* @throws Exception*/public UserAnswer doScore(List<String> choiceList, App app) throws Exception {AppTypeEnum appTypeEnum = AppTypeEnum.getEnumByValue(app.getAppType());AppScoringStrategyEnum appScoringStrategyEnum = AppScoringStrategyEnum.getEnumByValue(app.getScoringStrategy());if (appTypeEnum == null || appScoringStrategyEnum == null) {throw new BusinessException(ErrorCode.SYSTEM_ERROR, "应用配置有误,未找到匹配的策略");}// 根据不同的应用类别和评分策略,选择对应的策略执行switch (appTypeEnum) {case SCORE:switch (appScoringStrategyEnum) {case CUSTOM:return customScoreScoringStrategy.doScore(choiceList, app);case AI:break;}break;case TEST:switch (appScoringStrategyEnum) {case CUSTOM:return customTestScoringStrategy.doScore(choiceList, app);case AI:break;}break;}throw new BusinessException(ErrorCode.SYSTEM_ERROR, "应用配置有误,未找到匹配的策略");} }
优点是直观清晰,缺点是不利于扩展和维护。
2)声明式,在每个策略类中通过接口声明对应的生效条件,适合比较规律的策略选取场景。
接口:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Component public @interface ScoringStrategyConfig {int appType();int scoringStrategy(); }
给策略实现类补充注解:
@ScoringStrategyConfig(appType = 0, scoringStrategy = 0)
全局执行器:
@Service public class ScoringStrategyExecutor {// 策略列表 @Resourceprivate List<ScoringStrategy> scoringStrategyList;/*** 评分** @param choiceList* @param app* @return* @throws Exception*/public UserAnswer doScore(List<String> choiceList, App app) throws Exception {Integer appType = app.getAppType();Integer appScoringStrategy = app.getScoringStrategy();if (appType == null || appScoringStrategy == null) {throw new BusinessException(ErrorCode.SYSTEM_ERROR, "应用配置有误,未找到匹配的策略");}// 根据注解获取策略for (ScoringStrategy strategy : scoringStrategyList) {if (strategy.getClass().isAnnotationPresent(ScoringStrategyConfig.class)) {ScoringStrategyConfig scoringStrategyConfig = strategy.getClass().getAnnotation(ScoringStrategyConfig.class);if (scoringStrategyConfig.appType() == appType && scoringStrategyConfig.scoringStrategy() == appScoringStrategy) {return strategy.doScore(choiceList, app);}}}throw new BusinessException(ErrorCode.SYSTEM_ERROR, "应用配置有误,未找到匹配的策略");} }
因为用了 ScoringStrategyConfig 注解,所以这个实现类被加上了 component 注解,因此可以被spring 管理扫描到。 然后 @Resoure 注入的时候,会通过 ScoringStrategy 类型找到所有实现 ScoringStrategy 接口的实现类