提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
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 {
@Bean
public Queue workQueue() {
return new Queue(RabbitMQConstants.OJ_WORK_QUEUE, true);
}
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
然后是生产者和消费者
friend就是生产者–》发送判题
judge是消费者–》判题
在friend中
@Component
@Slf4j
public class JudgeProducer {
@Autowired
private 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 {
@Autowired
private 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));
}
@Override
public 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;///容器名称前缀
@Bean
public DockerClient createDockerClient(){
DefaultDockerClientConfig clientConfig = DefaultDockerClientConfig.createDefaultConfigBuilder()
.withDockerHost(dockerHost)
.build();
DockerClient dockerClient = DockerClientBuilder
.getInstance(clientConfig)
.withDockerCmdExecFactory(new NettyDockerCmdExecFactory())
.build();
return dockerClient;
}
@Bean
public 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: localhost
port: 5672
username: admin
password: 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));
}
@Override
public 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 mapper
PUBLIC "-//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">
SELECT
submit_id,
pass,
exe_message,
case_judge_res
FROM
tb_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;
}
}
@Override
public 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.examEndTime
console.log('examTitle: ', examTitle)
async function getQuestionDetail() {
if (examId && (questionId == null || questionId == '')) {
const eqrs = await getFirstExamQuestionService(examId)
questionId = eqrs.data
console.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 currentTime
function startPolling() {
stopPolling(); // 停止之前的轮询
pollingInterval.value = setInterval(() => {
getQuestionResult();
}, 2000); // 每隔2秒请求一次
}
function stopPolling() {
if (pollingInterval.value) {
clearInterval(pollingInterval.value);
pollingInterval.value = null;
}
}
async function submitQuestion() {
submitDTO.examId = examId
submitDTO.questionId = questionId
await userSubmitService(submitDTO)
currentTime = new Date().toLocaleString();
userQuestionResultVO.value.pass = 3
startPolling()
}
async function getQuestionResult() {
const res = await getQuestionResultService(submitDTO.examId, submitDTO.questionId, currentTime)
userQuestionResultVO.value = res.data
if (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 {
@Autowired
private 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