微服务的编程测评系统18-判题功能-Rabbitmq-用户拉黑
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 1. 判题功能
- 1.1 Rabbitmq
- 1.2 Rabbitmq判题结果的获取
- 1.3 热榜排行
- 2. 用户拉黑功能-用户行为限制
- 总结
前言
1. 判题功能
1.1 Rabbitmq
docker pull rabbitmq:3.8.30-management
先安装
启动容器
docker run -d --name oj-rabbit-dev -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 rabbitmq:3.8.30-management
指定用户名密码为admin
启⽤管理插件:
rabbitmq-plugins enable rabbitmq_management
在容器里面执行
然后点击端口号15672就进入管理页面了
<!--rabbitmq-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
引入–》建立一个Rabbitmq包
维护rabbitMQ常量,在core中
public class RabbitMQConstants {public static final String OJ_WORK_QUEUE = "oj-work-queue";
}
然后是Rabbitmq的基本配置
messageConverter是一个json转化器,就是把对象和json之间转换
@Configuration
public class RabbitConfig {@Beanpublic Queue workQueue() {return new Queue(RabbitMQConstants.OJ_WORK_QUEUE, true);}@Beanpublic MessageConverter messageConverter() {return new Jackson2JsonMessageConverter();}
}
然后是生产者和消费者
friend就是生产者–》发送判题
judge是消费者–》判题
在friend中
@Component
@Slf4j
public class JudgeProducer {@Autowiredprivate RabbitTemplate rabbitTemplate;public void produceMsg(JudgeSubmitDTO judgeSubmitDTO) {try {rabbitTemplate.convertAndSend(RabbitMQConstants.OJ_WORK_QUEUE,judgeSubmitDTO);} catch (Exception e) {log.error("⽣产者发送消息异常", e);throw new ServiceException(ResultCode.FAILED_RABBIT_PRODUCE);}}
}
这个就是生产者,把消息发送到队列
然后是judge消费者
@Slf4j
@Component
public class JudgeConsumer {@Autowiredprivate IJudgeService judgeService;@RabbitListener(queues = RabbitMQConstants.OJ_WORK_QUEUE)public void consume(JudgeSubmitDTO judgeSubmitDTO) {log.info("收到消息为: {}", judgeSubmitDTO);judgeService.doJudgeJavaCode(judgeSubmitDTO);}
}
现在开始使用
现在我们是生产者和消费者
那么在friend中就不用手动调用judge服务了
直接给mq就可以了
消费者自己知道消费
我们给UserQuestionController写一个新的接口
@PostMapping("/rabbit/submitQuestion")public R<Void> rabbitSubmitQuestion(@RequestBody SubmitQuestionDTO submitQuestionDTO){log.info("用户提交题目代码,rabbitSubmitQuestion:{}",submitQuestionDTO);return toR(userQuestionService.rabbitSubmitQuestion(submitQuestionDTO));}
@Overridepublic boolean rabbitSubmitQuestion(SubmitQuestionDTO submitQuestionDTO) {Integer programType = submitQuestionDTO.getProgramType();if(ProgramType.JAVA.getValue().equals(programType)){JudgeSubmitDTO judgeSubmitDTO = makeJudgeSubmitDTO(submitQuestionDTO);judgeProducer.produceMsg(judgeSubmitDTO);return true;}throw new ServiceException(ResultCode.PROGRAM_TYPE_ERR);}
这样就可以了但是没有返回值呢–》先不管
然后测试一下
@Configuration
public class DockerSandBoxPoolConfig {@Value("${sandbox.docker.host:tcp://localhost:2375}")private String dockerHost;@Value("${sandbox.docker.image:openjdk:8-jdk-alpine}")private String sandboxImage;//镜像名称@Value("${sandbox.docker.volume:/usr/share/java}")private String volumeDir;//挂载目录@Value("${sandbox.limit.memory:100000000}")private Long memoryLimit;@Value("${sandbox.limit.memory-swap:100000000}")private Long memorySwapLimit;@Value("${sandbox.limit.cpu:1}")private Long cpuLimit;@Value("${sandbox.docker.pool.size:4}")private int poolSize;@Value("${sandbox.docker.name-prefix:oj-sandbox-jdk}")private String containerNamePrefix;///容器名称前缀@Beanpublic DockerClient createDockerClient(){DefaultDockerClientConfig clientConfig = DefaultDockerClientConfig.createDefaultConfigBuilder().withDockerHost(dockerHost).build();DockerClient dockerClient = DockerClientBuilder.getInstance(clientConfig).withDockerCmdExecFactory(new NettyDockerCmdExecFactory()).build();return dockerClient;}@Beanpublic DockerSandBoxPool createDockerSandBoxPool(DockerClient dockerClient){DockerSandBoxPool dockerSandBoxPool = new DockerSandBoxPool(dockerClient,sandboxImage,volumeDir,memoryLimit,memorySwapLimit,cpuLimit,poolSize,containerNamePrefix);dockerSandBoxPool.initDockerPool();return dockerSandBoxPool;}
}
这个容器池改造一下,就是dockerClient
记得还要加入mq的配置,在两个服务中
spring:rabbitmq:host: localhostport: 5672username: adminpassword: admin
先不启动judge服务–》可以看到在队列的消息
然后就可以测试了
这样就成功了
1.2 Rabbitmq判题结果的获取
我们在提供一个接口,专门用来查询判题结果—》查询数据库可以–》但是这个判题结果不是马上就能获取出来的,因为判题是需要时间的
—》搞一个定时器每隔一段时间获取结果
然后就是前端传递的参数还要有一个判题时间的参数,因为万一这次是二次提交,那么第二次判题还没结束(第一次的结果也没有删除),那么就可能获取到第一次的判题结果
有一个时间的话,就只需要这个判题时间之后的判题结果就可以了
@GetMapping("/exe/result")public R<UserQuestionResultVO> exeResult( Long questionId ,Long examId,String currentTime){log.info("定时查询判题结果,questionID:{},examId:{},currentTime:{}",questionId,examId,currentTime);return R.ok(userQuestionService.exeResult(questionId,examId,currentTime));}
@Overridepublic UserQuestionResultVO exeResult(Long questionId, Long examId, String currentTime) {Long userId = ThreadLocalUtil.get(Constants.USER_ID, Long.class);UserSubmit userSubmit = userSubmitMapper.selectCurrentUserSubmit(userId,questionId,examId,currentTime);return null;}
然后是xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ck.friend.mapper.user.UserSubmitMapper"><select id="selectCurrentUserSubmit" resultType="com.ck.friend.domain.user.UserSubmit">SELECTsubmit_id,pass,exe_message,case_judge_resFROMtb_user_submit<where><if test="questionId != null">AND question_id = #{questionId}</if><if test="examId !=null ">AND exam_id = #{examId}</if><if test="examId == null ">AND exam_id is null</if><if test="userId !=null ">AND user_id = #{userId}</if><if test="currentTime !=null and currentTime !='' ">AND (create_time >= #{currentTime} or update_time >= #{currentTime})</if></where></select></mapper>
case_judge_res是返回的结果信息,就是判题的输出结果—》input,output,exeOutput
但是数据库没有设计这个字段----》增加一下
private String caseJudgeRes;
把输出结果转化成json来存储
我们在judge服务的insertUserSubmit方法中增加
userSubmit.setCaseJudgeRes(JSON.toJSONString(userQuestionResultVO.getUserExeResultList()));
其中用的是fastJson的JSON
这样就存储成功了
@Getter
public enum QuestionResType {ERROR(0), //未通过PASS(1), //通过UN_SUBMIT(2), //未提交IN_JUDGE(3); // 系统判题中private Integer value;QuestionResType(Integer value) {this.value = value;}
}
@Overridepublic UserQuestionResultVO exeResult(Long questionId, Long examId, String currentTime) {Long userId = ThreadLocalUtil.get(Constants.USER_ID, Long.class);UserSubmit userSubmit = userSubmitMapper.selectCurrentUserSubmit(userId,questionId,examId,currentTime);UserQuestionResultVO userQuestionResultVO = new UserQuestionResultVO();if(userSubmit==null){userQuestionResultVO.setPass(QuestionResType.IN_JUDGE.getValue());}else{userQuestionResultVO.setExeMessage(userSubmit.getExeMessage());userQuestionResultVO.setScore(userSubmit.getScore());userQuestionResultVO.setPass(userSubmit.getPass());if(StrUtil.isNotEmpty(userSubmit.getCaseJudgeRes())){userQuestionResultVO.setUserExeResultList(JSON.parseArray(userSubmit.getCaseJudgeRes(), UserExeResult.class));}}return userQuestionResultVO;}
这样就可以了
然后
定时任务是在前端实现的
export function getQuestionResultService(examId, questionId, currentTime) {return service({url: "/user/question/exe/result",method: "get",params: { examId, questionId, currentTime }});
}
export function userSubmitService(params = {}) {return service({url: "/user/question/rabbit/submitQuestion",method: "post",data: params,});
}
<template><div class="page praticle-page flex-col"><div class="box_1 flex-row"><div class="group_1 "><img class="label_4" src="@/assets/ide/liebiao.png" /><span>{{ examTitle ? examTitle : 精选题库 }}</span><el-countdown v-if="examEndTime && new Date() < new Date(examEndTime)" class="exam-time-countdown"@finish="handleCountdownFinish" title="距离竞赛结束还有:" :value="new Date(examEndTime)" /></div><div class="group_2"><el-button type="primary" plain @click="submitQuestion">提交代码</el-button></div><span class="ide-back" @click="goBack()">返回</span></div><div class="box_8 flex-col"><div class="group_12 flex-row justify-between"><div class="image-wrapper_1 flex-row"><img class="thumbnail_2" src="@/assets/ide/xiaobiaoti.png" /><div class="question-nav"><span>题目描述</span></div><div class="question-nav" @click="preQuestion"><el-icon><span>上一题</span><ArrowLeft /></el-icon></div><div class="question-nav" @click="nextQuestion"><el-icon><ArrowRight /><span>下一题</span></el-icon></div></div><div class="image-wrapper_2 flex-row"><img class="image_1" src="@/assets/ide/daima.png" />代码</div></div><div class="group_13 flex-row justify-between"><div class="box_3 flex-col"><span class="question-title">{{ questionDetail.title }}</span><span class="question-limit"><div v-if="questionDetail.difficulty === 1">题目难度:简单 时间限制:{{ questionDetail.timeLimit }} ms 空间限制:{{questionDetail.spaceLimit }} 字节</div><div v-if="questionDetail.difficulty === 2">题目难度:中等 时间限制:{{ questionDetail.timeLimit }} ms 空间限制:{{questionDetail.spaceLimit }} 字节</div><div v-if="questionDetail.difficulty === 3">题目难度:困难 时间限制:{{ questionDetail.timeLimit }} ms 空间限制:{{questionDetail.spaceLimit }} 字节</div></span><span class="question-content" v-html="questionDetail.content"></span></div><div class="group_14 flex-col"><div class="group_8 flex-col"><codeEditor ref="defaultCodeRef" @update:value="handleEditorContent"></codeEditor></div><div class="code-result flex-row"><img class="code-result-image" src="@/assets/ide/codeResult.png" /><span class="code-result-content">执行结果</span></div><div class="group_15 flex-row"><div class="section_1 flex-row"><div class="section_3 flex-col"><div class="text-wrapper_2 flex-row justify-between"><span class="text_1 red" v-if="userQuestionResultVO.pass === 0">未通过</span><span class="text_1 success" v-if="userQuestionResultVO.pass === 1">通过</span><span class="text_1 warning" v-if="userQuestionResultVO.pass === 2">请先执行代码</span><span class="text_1 info" v-if="userQuestionResultVO.pass === 3">系统正在处理您的代码,请稍后</span></div><span class="error-text" v-if="userQuestionResultVO.pass === 0">异常信息:{{userQuestionResultVO.exeMessage }}</span><el-table v-if="userQuestionResultVO.userExeResultList && userQuestionResultVO.userExeResultList.length > 0":data="userQuestionResultVO.userExeResultList"><el-table-column prop="input" label="输入" /><el-table-column prop="output" label="预期结果" /><el-table-column prop="exeOutput" label="实际输出" /></el-table></div></div></div></div></div></div></div>
</template><script setup>
import { reactive, ref } from "vue"
import codeEditor from "@/components/CodeEditor.vue"
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
import { useRoute } from "vue-router"
import { questionDetailService, preQuestionService, nextQuestionService, getQuestionResultService } from "@/apis/question"
import router from "@/router"
import { examNextQuestionService, examPreQuestionService, getFirstExamQuestionService } from "@/apis/exam"
import { ElMessage } from "element-plus"
import { userSubmitService } from "@/apis/user"function goBack() {router.go(-1);
}
const questionDetail = reactive({})
const defaultCodeRef = ref()let questionId = useRoute().query.questionId
let examId = useRoute().query.examId
let examTitle = useRoute().query.examTitle
let examEndTime = useRoute().query.examEndTimeconsole.log('examTitle: ', examTitle)async function getQuestionDetail() {if (examId && (questionId == null || questionId == '')) {const eqrs = await getFirstExamQuestionService(examId)questionId = eqrs.dataconsole.log('qId: ', questionId)}const res = await questionDetailService(questionId)Object.assign(questionDetail, res.data)defaultCodeRef.value.setAceCode(questionDetail.defaultCode)
}
getQuestionDetail()async function preQuestion() {if (examId) {//竞赛中上一题的逻辑 需要提供一个竞赛中获取上一题的接口const res = await examPreQuestionService(examId, questionId)questionId = res.data} else {const res = await preQuestionService(questionId)questionId = res.data}getQuestionDetail()
}async function nextQuestion() {if (examId) {//竞赛中下一题的逻辑 需要提供一个竞赛中获取下一题的接口const res = await examNextQuestionService(examId, questionId)questionId = res.data} else {const res = await nextQuestionService(questionId)questionId = res.data}getQuestionDetail()
}function handleCountdownFinish() {ElMessage.info('竞赛已经结束了哦')router.push('/c-oj/home/exam')
}const submitDTO = reactive({examId:'',questionId:'',programType: 0,userCode: ''
})function handleEditorContent(content) {submitDTO.userCode = content
}const userQuestionResultVO = ref({pass: 2, //默认值为2,未提交代码exeMessage: '',userExeResultList: [],
})const pollingInterval = ref(null);
let currentTimefunction startPolling() {stopPolling(); // 停止之前的轮询pollingInterval.value = setInterval(() => {getQuestionResult();}, 2000); // 每隔2秒请求一次
}function stopPolling() {if (pollingInterval.value) {clearInterval(pollingInterval.value);pollingInterval.value = null;}
}async function submitQuestion() {submitDTO.examId = examIdsubmitDTO.questionId = questionIdawait userSubmitService(submitDTO)currentTime = new Date().toLocaleString();userQuestionResultVO.value.pass = 3startPolling()
}async function getQuestionResult() {const res = await getQuestionResultService(submitDTO.examId, submitDTO.questionId, currentTime)userQuestionResultVO.value = res.dataif (userQuestionResultVO.value.pass !== 3) {stopPolling();}
}</script>
然后就成功了,可以测试了
如果我们没有使用容器池的话,就会很慢
前端就会调用定时器一直访问这个接口
还有一个我的错误就是,friend中没有把examId传递给judge,代码要改一下
1.3 热榜排行
热榜排行就是被提交次数最多的题目–》用户提交表–》然后展示排名前五前十–》redis查询—》分页查询–》差不到的话,就去数据库,然后同步数据到redis—》排名一直会变–》后端定时任务–》凌晨统计或者频率快点都是可以的–》可以在redis中存储questionId的list,然后去es查询title
2. 用户拉黑功能-用户行为限制
接口我们已经写了,然后是权限的限制
—》比如:拉黑的用户不能报名竞赛,不能开始答题等等,只能浏览页面,不能操作,很多的接口都要限制住权限—》AOP–》多个接口会重复调用判断权限–》注解来
先导入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
我们来创建一个注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckUserStatus {}
这个就是我们的自定义注解
然后创建一个切面类
@Aspect
@Component
public class UserStatusCheckAspect {@Autowiredprivate UserCacheManager userCacheManager;@Before(value = "@annotation(com.ck.friend.aspect.CheckUserStatus)")public void before(JoinPoint point){Long userId = ThreadLocalUtil.get(Constants.USER_ID, Long.class);UserVO user = userCacheManager.getUserById(userId);if (user == null) {throw new ServiceException(ResultCode.FAILED_USER_NOT_EXISTS);}if (Objects.equals(user.getStatus(), Constants.FALSE)) {throw new ServiceException(ResultCode.FAILED_USER_BANNED);}}
}
注解Aspect的作用就是标识这是一个切面类
注解Before加在方法before上面的意思就是标识这个方法是一个前置统治类型的方法
—》代表这个方法会在目标方法之前执行,操作之前验证权限—》所以用注解Before
所以现在就变成了加自定义注解CheckUserStatus—》自动执行before方法
所以注解加在哪个方法上面就会执行before
比如加在报名竞赛的方法上面
@CheckUserStatus@PostMapping("/enter")@Operation(description = "用户报名竞赛")public R<Void> enter(@RequestBody ExamDto examDto, @RequestHeader(HttpConstants.AUTHENTICATION) String token){log.info("用户报名竞赛:examDto:{},token:{}",examDto,token);return toR(userExamService.enter(examDto.getExamId(),token));}
因为用户数据以前就存在数据库中了
所以我们可以直接从缓存中获取
然后是把用户数据存入缓存的时机是访问个人中心,我们记得要把status字段存入缓存
然后是修改status的时候,还要修改缓存,status不能删除,不能增加,所以只能修改缓存了,查询已经写好了
缓存的增删查改
因为这个用户数据的缓存是设计了过期时间的,所以在getUserById的时候,缓存里面没有,从数据库中获取,并刷新
所以修改缓存的时候,如果缓存都过期了,那么就不用修改了
没有过期的话,就要更新了
public void updateStatus(Long userId,Integer status ) {//刷新用户缓存String userKey = getUserKey(userId);User user = redisService.getCacheObject(userKey, User.class);if(user==null){return;}user.setStatus(status);redisService.setCacheObject(userKey, user);//设置用户缓存有效期为10分钟redisService.expire(userKey, CacheConstants.USER_DETAIL_EXP, TimeUnit.MINUTES);}
在UserCacheManager增加如上方法
这样就OK了
这样就成功了
总结
如果有显示的问题的话,就编译一下,compile