微服务的编程测评系统16-用户答题

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

文章目录

  • 前言
  • [1. 用户答题](#1. 用户答题)
    • [1.1 用户刷题](#1.1 用户刷题)
    • [1.2 详细信息](#1.2 详细信息)
    • [1.3 上一题下一题](#1.3 上一题下一题)
    • [1.4 竞赛中答题-获取第一个题目](#1.4 竞赛中答题-获取第一个题目)
    • [1.4 竞赛中答题-题目切换](#1.4 竞赛中答题-题目切换)
    • [1.4 竞赛中答题-提交代码](#1.4 竞赛中答题-提交代码)
  • [2. 判题功能](#2. 判题功能)
    • [2.1 逻辑梳理](#2.1 逻辑梳理)
  • 总结

前言

1. 用户答题

1.1 用户刷题

三个答题--》刷题,竞赛已经开赛报名竞赛已经结束练习竞赛

刷题:先根据questionId获取详细信息

1.2 详细信息

后端:

java 复制代码
    @GetMapping("/detail")
    public  R<QuestionDetailVO> detail(Long questionId){
        log.info("获取竞赛详细信息,questionId:{}",questionId);
        return R.ok(questionService.detail(questionId));
    }
java 复制代码
@Data
public class QuestionDetailVO extends QuestionVO{
    private Long timeLimit;

    private Long spaceLimit;

    private String content;

    private String defaultCode;
}

我们先去es中获取数据,如果没有数据的话,就从数据库中获取,并刷新es

java 复制代码
    @Override
    public QuestionDetailVO detail(Long questionId) {
        QuestionES questionES = questionRepository.findById(questionId).orElse(null);
        QuestionDetailVO questionDetailVO = new QuestionDetailVO();
        if(questionES!=null){
            BeanUtil.copyProperties(questionES,questionDetailVO);
            return questionDetailVO;
        }
        Question question = questionMapper.selectById(questionId);
        if(question==null){
            throw new ServiceException(ResultCode.QUESTION_ID_NOT_EXIST);
        }
        BeanUtil.copyProperties(question,questionDetailVO);
        refreshQuestionEs();
        return questionDetailVO;
    }

这样就可以了

前端:要先引入编译器的组件

java 复制代码
npm install ace-builds@1.4.13

question.vue中

java 复制代码
function goQuestTest(questionId) {
  router.push(`/c-oj/anwser?questionId=${questionId}`)
}
java 复制代码
export function questionDetailService(questionId) {
  return service({
    url: "/question/detail",
    method: "get",
    params: {questionId},
  });
}
java 复制代码
<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>精选题库</span>
            </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 warning">请先提交代码</span>
                                </div>
                            </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 router from "@/router";
import { getQuestionListService } from "@/apis/question";
function goBack() {
    router.go(-1);
}



</script>
java 复制代码
const questionDetail  = reactive({});
let questionId = useRoute().query.questionId;
const defaultCodeRef = ref();//加载到编辑器中
async function getQuestionDetail(){
    const res = await questionDetailService(questionId);
    Object.assign(questionDetail,res.data);
    defaultCodeRef.value.setAceCode(questionDetail.defaultCode)
}
getQuestionDetail()

这样就成功了,我们就不拷贝css代码了

1.3 上一题下一题

java 复制代码
    @GetMapping("/preQuestion")
    public  R<String> preQuestion(Long questionId){
        log.info("获取该题目的上一题ID,questionId:{}",questionId);
        return R.ok(questionService.preQuestion(questionId));
    }

    @GetMapping("/nextQuestion")
    public  R<String> nextQuestion(Long questionId){
        log.info("获取该题目的下一题ID,questionId:{}",questionId);
        return R.ok(questionService.nextQuestion(questionId));
    }

我们将题目的排列顺序存储到redis中,所以先从redis中获取,没有就更新数据

在redisService中增加方法

java 复制代码
    public <T>  Long indexOfForList(final  String key ,T value){
        return redisTemplate.opsForList().indexOf(key,value);
    }

    public <T>  T indexForList(final  String key ,long index,Class<T> clazz ){
        Object o = redisTemplate.opsForList().index(key, index);
        return  JSON.parseObject(String.valueOf(o), clazz);
    }

在增加一个QuestionCacheManager来操作redis

java 复制代码
@Component
public class QuestionCacheManager {
    @Autowired
    private RedisService redisService;


    @Autowired
    private QuestionMapper questionMapper;

    public Long getListSize() {
        return redisService.getListSize(CacheConstants.QUESTION_ORDER_LIST);
    }

    public void refreshQuestionOrderListCache() {
        List<Question> questionList = questionMapper
                .selectList(new LambdaQueryWrapper<Question>().orderByDesc(Question::getCreateTime));
        if(CollectionUtil.isEmpty(questionList)){
            return;
        }
        List<Long> list = questionList.stream().map(Question::getQuestionId).toList();
        redisService.rightPushAll(CacheConstants.QUESTION_ORDER_LIST,list);
    }

    public Long preQuestion(Long questionId) {
        Long index = redisService.indexOfForList(CacheConstants.QUESTION_ORDER_LIST, questionId);
        if(index==0){
            throw new ServiceException(ResultCode.FIRST_QUESTION);
        }
        return redisService.indexForList(CacheConstants.QUESTION_ORDER_LIST,index-1, Long.class);
    }

    public Long nextQuestion(Long questionId) {
        Long index = redisService.indexOfForList(CacheConstants.QUESTION_ORDER_LIST, questionId);
        Long total = redisService.getListSize(CacheConstants.QUESTION_ORDER_LIST);
        if(index==total-1){
            throw new ServiceException(ResultCode.FINALLY_QUESTION);
        }
        return redisService.indexForList(CacheConstants.QUESTION_ORDER_LIST,index+1, Long.class);
    }
}
java 复制代码
    @Override
    public String preQuestion(Long questionId) {
        Long listSize = questionCacheManager.getListSize();
        if(listSize==null||listSize==0){
            questionCacheManager.refreshQuestionOrderListCache();
        }
        return questionCacheManager.preQuestion(questionId).toString();
    }

    @Override
    public String nextQuestion(Long questionId) {
        Long listSize = questionCacheManager.getListSize();
        if(listSize==null||listSize==0){
            questionCacheManager.refreshQuestionOrderListCache();
        }
        return questionCacheManager.nextQuestion(questionId).toString();
    }

这样就成功了

然后是前端

java 复制代码
export function preQuestionService(questionId) {
  return service({
    url: "/question/preQuestion",
    method: "get",
    params: {questionId},
  });
}

export function nextQuestionService(questionId) {
  return service({
    url: "/question/nextQuestion",
    method: "get",
    params: {questionId},
  });
}
java 复制代码
async function preQuestion(){
    const res = await preQuestionService(questionId);
    questionId = res.data;
    await getQuestionDetail()
}

async function nextQuestion(){
    const res = await nextQuestionService(questionId);
    questionId = res.data;
    await getQuestionDetail()
}

然后就是管理员新增题目和删除题目的时候,要从redis中删除序列

java 复制代码
@Component
public class QuestionCacheManager {
    @Autowired
    private RedisService redisService;

    public void addQuestionOrderUpdate(Long questionId){
        redisService.leftPushForList(CacheConstants.QUESTION_ORDER_LIST,questionId);
    }

    public void deleteQuestionOrderUpdate(Long questionId){
        redisService.removeForList(CacheConstants.QUESTION_ORDER_LIST,questionId);
    }
}

这样就OK了

1.4 竞赛中答题-获取第一个题目

前面1.1~1.3都是用户自己刷题

第一个是在未完赛中答题

一个是在已经结束的竞赛中答题

前端页面都是一样的,只有一个不一样,就是竞赛里面有倒计时,还有提交竞赛按钮,还有竞赛标题

我们先设计从竞赛中获取第一个题目---》用redis存储竞赛的题目顺序--》和上面设计的redis存储练习题目顺序是一样的

java 复制代码
    @GetMapping("/getFirstQuestion")
    public R<String> getFirstQuestion(Long examId){
        log.info("获取竞赛的第一个题目ID:examId:{}",examId);
        return R.ok(examService.getFirstQuestion(examId));
    }
java 复制代码
    public static final String EXAM_QUESTION_ORDER_LIST_EXAMID = "exam:question:order:list:";

在ExamCacheManager增加方法

java 复制代码
    /// 竞赛中的题目顺序缓存
    public Long getExamQuestionOrderListSize(Long examId) {
        return redisService.getListSize(getExamQuestionOrderListKey(examId));
    }

    private String getExamQuestionOrderListKey(Long examId) {
        return CacheConstants.EXAM_QUESTION_ORDER_LIST_EXAMID + examId;
    }

    public void refreshExamQuestionOrderListCache(Long examId) {
        List<ExamQuestion> examQuestionList = examQuestionMapper.selectList(new LambdaQueryWrapper<ExamQuestion>()
                .eq(ExamQuestion::getExamId, examId).orderByAsc(ExamQuestion::getQuestionOrder));
        List<Long> questionIdList = examQuestionList.stream().map(ExamQuestion::getQuestionId).toList();
        redisService.rightPushAll(getExamQuestionOrderListKey(examId),questionIdList);
        long timesExpiredTime = ChronoUnit.SECONDS.between(LocalDateTime.now()
                ,LocalDateTime.now().plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0));
        redisService.expire(getExamQuestionOrderListKey(examId),timesExpiredTime,TimeUnit.SECONDS);
    }

    public Long getFirstExamQuestion(Long examId) {
        return redisService.indexForList(getExamQuestionOrderListKey(examId),0,Long.class);
    }

refreshExamQuestionOrderListCache中把exam中的questionList的redis设置了有效时间为当天,因为一个比赛一般就是只有当天,而且这样可以节省redis内存

java 复制代码
    @Override
    public String getFirstQuestion(Long examId) {
        Long listSize = examCacheManager.getExamQuestionOrderListSize(examId);
        if(listSize==null||listSize==0){
            examCacheManager.refreshExamQuestionOrderListCache(examId);
        }
        return examCacheManager.getFirstExamQuestion(examId).toString();
    }

这样我们就成功了

然后就是redis的删除那些了

就是在竞赛中增加题目,要修改redis吗

注意这个在竞赛中获取题目列表在redis中,的前提是这个竞赛已经发布了的,然后就是发布的竞赛不能进行增加删除题目,所以redis中的内容不用修改

撤消发布也,没有必要删除缓存

因为撤消发布了说明还没有开始竞赛,那么就不会产生缓存,只有用户访问第一个竞赛才会产生缓存

然后就是前端代码了

java 复制代码
function goExam(exam) {
  router.push(`/c-oj/anwser?examId=${exam.examId}&examTitle=${exam.title}&examEndTime=${exam.endTime}`)
}
java 复制代码
                <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)" />

倒计时我们用的是elementplus中的 Statistic统计组件

value就是目标时间,就是endTime

finish就是倒计时结束事件

使用new Date(examEndTime)的原因是examEndTime的格式不对,可以用examEndTime来格式化一个时间对象

java 复制代码
export function getFirstExamQuestionService(examId) {
  return service({
    url: "/exam/getFirstQuestion",
    method: "get",
    params: {examId},
  });
}
java 复制代码
let examId = useRoute().query.examId;
let examTitle = useRoute().query.examTitle;
let examEndTime = useRoute().query.examEndTime;
async function getQuestionDetail() {
    if(examId){
        const res2 = await getFirstExamQuestionService(examId);
        questionId = res2.data;
    }
    const res = await questionDetailService(questionId);
    Object.assign(questionDetail, res.data);
    defaultCodeRef.value.setAceCode(questionDetail.defaultCode)
}
function handleCountdownFinish(){
    ElMessage.info("竞赛时间结束!!")
    router.push("/c-oj/home/exam")
}

这样就成功了

1.4 竞赛中答题-题目切换

java 复制代码
    @GetMapping("/preQuestion")
    public R<String> preQuestion(Long examId,Long questionId){
        log.info("获取竞赛中的题目的上一题ID:examId:{},questionId:{}",examId,questionId);
        return R.ok(examService.preQuestion(examId,questionId));
    }

    @GetMapping("/nextQuestion")
    public R<String> nextQuestion(Long examId,Long questionId){
        log.info("获取竞赛中的题目的下一题ID:examId:{},questionId:{}",examId,questionId);
        return R.ok(examService.nextQuestion(examId,questionId));
    }
java 复制代码
    @Override
    public String preQuestion(Long examId, Long questionId) {
        checkExamQuestionListCache(examId);
        return examCacheManager.examPreQuestion(examId,questionId).toString();
    }
    
    @Override
    public String nextQuestion(Long examId, Long questionId) {
        checkExamQuestionListCache(examId);
        return examCacheManager.examNextQuestion(examId,questionId).toString();
    }

    private void checkExamQuestionListCache(Long examId) {
        Long listSize = examCacheManager.getExamQuestionOrderListSize(examId);
        if(listSize==null||listSize==0){
            examCacheManager.refreshExamQuestionOrderListCache(examId);
        }
    }
java 复制代码
    public Long examPreQuestion(Long examId, Long questionId) {
        Long index = redisService.indexOfForList(getExamQuestionOrderListKey(examId), questionId);
        if(index==0){
            throw new ServiceException(ResultCode.FIRST_QUESTION);
        }
        return redisService.indexForList(getExamQuestionOrderListKey(examId),index-1, Long.class);
    }


    public Long examNextQuestion(Long examId, Long questionId) {
        Long index = redisService.indexOfForList(getExamQuestionOrderListKey(examId), questionId);
        Long total = redisService.getListSize(getExamQuestionOrderListKey(examId));
        if(index==total-1){
            throw new ServiceException(ResultCode.FINALLY_QUESTION);
        }
        return redisService.indexForList(getExamQuestionOrderListKey(examId),index+1, Long.class);
    }

然后是前端代码

java 复制代码
export function examPreQuestionService(examId,questionId) {
  return service({
    url: "/exam/preQuestion",
    method: "get",
    params: {examId,questionId},
  });
}

export function examNextQuestionService(examId,questionId) {
  return service({
    url: "/exam/nextQuestion",
    method: "get",
    params: {examId,questionId},
  });
}
java 复制代码
async function preQuestion() {
    if (examId) {
        const res = await examPreQuestionService(examId,questionId);
        questionId = res.data;
    } else {
        const res = await preQuestionService(questionId);
        questionId = res.data;
    }
    await getQuestionDetail()
}

async function nextQuestion() {
    if (examId) {
        const res = await examNextQuestionService(examId,questionId);
        questionId = res.data;
    } else {
        const res = await nextQuestionService(questionId);
        questionId = res.data;
    }
    await getQuestionDetail()
}

这样成功了

1.4 竞赛中答题-提交代码

先把提交的代码存起来---》新增

java 复制代码
@Data
public class SubmitQuestionDTO {
    private Long examId;  //可选

    private Long questionId;

    private Integer programType;  // (0: java  1:cpp 2: golang)

    private String userCode;

}
java 复制代码
@AllArgsConstructor
@Getter
public enum ProgramType {
    JAVA(0,"java"),
    CPP(1,"c++"),
    GO(2,"go");
    
    private final Integer value;
    private final String msg;
}
java 复制代码
@AllArgsConstructor
@Getter
public enum SubmitQuestionResult {

    PASS(1,"提交成功"),
    ERR(0,"运行失败");
    
    private final Integer value;
    private final String msg;
}
java 复制代码
@Data
public class SubmitQuestionOneResultVO {
    private String input;
    private String output;//实际输出
    private String expectOutput;//期望输出
}
java 复制代码
@Data
public class SubmitQuestionVO {
    private Integer result;//0表示失败,1表示成功
    private String msg;//表示失败的错误信息
    List<SubmitQuestionOneResultVO> submitQuestionOneResultVOList;
}
java 复制代码
    @PostMapping("/submitQuestion")
    public R<SubmitQuestionVO> submitQuestion(@RequestBody SubmitQuestionDTO submitQuestionDTO){
        log.info("用户提交题目代码,submitQuestionDTO:{}",submitQuestionDTO);
//        return R.ok(userService.submitQuestion(submitQuestionDTO));
        return null;
    }

2. 判题功能

sumitQuestion接口就是判题功能的实现

2.1 逻辑梳理

先对programType进行判断

userCode是不能直接执行的

所以我们要有main函数,然后还要有入参,questionId去查询入参

input就是main函数的入参,入参就是从json里面得到的,output就是expectOutput

用questionId来查询

所以我们要把main函数和function拼好

我们用javac来编译,成功就执行java指令,失败的话,终止逻辑,返回原因

然后用java来执行,成功的话,继续执行后续逻辑,失败的话就返回原因

然后是题目答案的比对,与json的output进行比对,比对一致,正确,比对失败,返回错误原因

然后还要在代码中比对时间限制,和空间限制,把实际的时间空间和期望的比对,小于等于期望值就符合要求了,不符合要求就返回错误原因

对于用户的答题结果,无论失败成功都要存储

分数的话,难题和简单题的分数应该不一样

docker就可以完成--》隔离的容器完成代码,资源使用,是相互隔离的,不会互相干扰,可以限制文件的访问权限,通过java来操作docker

判题逻辑--》单独一个微服务--judge

judge就只是判题,不会操作数据库,es啥的

friend执行完之后,再去调用judge服务,服务间调用--》openfeign

总结