提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- [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