微服务的编程测评系统16-用户答题
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 1. 用户答题
- 1.1 用户刷题
- 1.2 详细信息
- 1.3 上一题下一题
- 1.4 竞赛中答题-获取第一个题目
- 1.4 竞赛中答题-题目切换
- 1.4 竞赛中答题-提交代码
- 2. 判题功能
- 2.1 逻辑梳理
- 总结
前言
1. 用户答题
1.1 用户刷题
三个答题–》刷题,竞赛已经开赛报名竞赛已经结束练习竞赛
刷题:先根据questionId获取详细信息
1.2 详细信息
后端:
@GetMapping("/detail")public R<QuestionDetailVO> detail(Long questionId){log.info("获取竞赛详细信息,questionId:{}",questionId);return R.ok(questionService.detail(questionId));}
@Data
public class QuestionDetailVO extends QuestionVO{private Long timeLimit;private Long spaceLimit;private String content;private String defaultCode;
}
我们先去es中获取数据,如果没有数据的话,就从数据库中获取,并刷新es
@Overridepublic 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;}
这样就可以了
前端:要先引入编译器的组件
npm install ace-builds@1.4.13
question.vue中
function goQuestTest(questionId) {router.push(`/c-oj/anwser?questionId=${questionId}`)
}
export function questionDetailService(questionId) {return service({url: "/question/detail",method: "get",params: {questionId},});
}
<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>
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 上一题下一题
@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中增加方法
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
@Component
public class QuestionCacheManager {@Autowiredprivate RedisService redisService;@Autowiredprivate 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);}
}
@Overridepublic String preQuestion(Long questionId) {Long listSize = questionCacheManager.getListSize();if(listSize==null||listSize==0){questionCacheManager.refreshQuestionOrderListCache();}return questionCacheManager.preQuestion(questionId).toString();}@Overridepublic String nextQuestion(Long questionId) {Long listSize = questionCacheManager.getListSize();if(listSize==null||listSize==0){questionCacheManager.refreshQuestionOrderListCache();}return questionCacheManager.nextQuestion(questionId).toString();}
这样就成功了
然后是前端
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},});
}
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中删除序列
@Component
public class QuestionCacheManager {@Autowiredprivate 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存储练习题目顺序是一样的
@GetMapping("/getFirstQuestion")public R<String> getFirstQuestion(Long examId){log.info("获取竞赛的第一个题目ID:examId:{}",examId);return R.ok(examService.getFirstQuestion(examId));}
public static final String EXAM_QUESTION_ORDER_LIST_EXAMID = "exam:question:order:list:";
在ExamCacheManager增加方法
/// 竞赛中的题目顺序缓存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内存
@Overridepublic 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中的内容不用修改
撤消发布也,没有必要删除缓存
因为撤消发布了说明还没有开始竞赛,那么就不会产生缓存,只有用户访问第一个竞赛才会产生缓存
然后就是前端代码了
function goExam(exam) {router.push(`/c-oj/anwser?examId=${exam.examId}&examTitle=${exam.title}&examEndTime=${exam.endTime}`)
}
<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来格式化一个时间对象
export function getFirstExamQuestionService(examId) {return service({url: "/exam/getFirstQuestion",method: "get",params: {examId},});
}
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 竞赛中答题-题目切换
@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));}
@Overridepublic String preQuestion(Long examId, Long questionId) {checkExamQuestionListCache(examId);return examCacheManager.examPreQuestion(examId,questionId).toString();}@Overridepublic 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);}}
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);}
然后是前端代码
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},});
}
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 竞赛中答题-提交代码
先把提交的代码存起来—》新增
@Data
public class SubmitQuestionDTO {private Long examId; //可选private Long questionId;private Integer programType; // (0: java 1:cpp 2: golang)private String userCode;}
@AllArgsConstructor
@Getter
public enum ProgramType {JAVA(0,"java"),CPP(1,"c++"),GO(2,"go");private final Integer value;private final String msg;
}
@AllArgsConstructor
@Getter
public enum SubmitQuestionResult {PASS(1,"提交成功"),ERR(0,"运行失败");private final Integer value;private final String msg;
}
@Data
public class SubmitQuestionOneResultVO {private String input;private String output;//实际输出private String expectOutput;//期望输出
}
@Data
public class SubmitQuestionVO {private Integer result;//0表示失败,1表示成功private String msg;//表示失败的错误信息List<SubmitQuestionOneResultVO> submitQuestionOneResultVOList;
}
@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