微服务的编程测评系统18-判题功能-Rabbitmq-用户拉黑

发布于:2025-09-01 ⋅ 阅读:(20) ⋅ 点赞:(0)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

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 &gt;= #{currentTime} or update_time &gt;= #{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


网站公告

今日签到

点亮在社区的每一天
去签到