跳到主要内容
在线 OJ 系统 Java Spring 竞赛管理功能实现 | 极客日志
Java 大前端 java 算法
在线 OJ 系统 Java Spring 竞赛管理功能实现 基于 Java Spring Boot 的在线 OJ 系统竞赛管理模块实现方案。包含数据库表结构设计,涉及竞赛主表与题目关联关系。后端提供竞赛列表查询、新增、编辑、删除及发布状态控制接口,实现标题唯一性校验、时间逻辑判断及雪花算法 ID 处理。前端使用 Vue 框架结合 Element Plus 组件库,实现分页搜索、日期范围选择、题目多选绑定及状态可视化展示。通过 MyBatis XML 优化多表关联查询,解决 Long 类型序列化精度丢失问题。确保竞赛在未发布阶段可自由配置,发布后锁定修改权限,保障业务流程规范与数据安全。
不知所云 发布于 2026/3/21 更新于 2026/9/11 57 浏览表结构创建
create table tb_exam (
exam_id bigint unsigned not null comment '竞赛 id(主键)' ,
title varchar (50 ) not null comment '竞赛标题' ,
start_time datetime not null comment '竞赛开始时间' ,
end_time datetime not null comment '竞赛结束时间' ,
status tinyint not null default '0' comment '是否发布 0:未发布 1:已发布' ,
create_by bigint unsigned not null comment '创建人' ,
create_time datetime not null comment '创建时间' ,
update_by bigint unsigned comment '更新人' ,
update_time datetime comment '更新时间' ,
primary key (exam_id)
);
create table tb_exam_question (
exam_question_id bigint unsigned not null comment '竞赛题目关系 id(主键)' ,
question_id bigint unsigned not null comment '题目 id(主键)' ,
exam_id bigint unsigned not null comment '竞赛 id(主键)' ,
question_order int not null comment '题目顺序' ,
create_by bigint unsigned not null comment '创建人' ,
create_time datetime not null comment '创建时间' ,
update_by bigint unsigned comment ,
update_time datetime comment ,
(exam_question_id)
);
'更新人'
'更新时间'
primary key
竞赛列表
后端代码开发
Controller @RestController
@RequestMapping("/exam")
public class ExamController extends BaseController {
@Autowired
private IExamService examService;
@GetMapping("/list")
public TableDataInfo list (ExamQueryDTO examQueryDTO) {
return getDataTable(examService.list(examQueryDTO));
}
}
前端传入参数需包含查询条件(标题、开始时间、结束时间),并继承分页 Domain。返回值类型为 TableDataInfo。
Service
VO 使用 @JsonSerialize(using = ToStringSerializer.class) 将 Long 类型的 examId 转换为字符串,避免前端 JavaScript 处理大整数时的精度丢失问题。
使用 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 指定日期时间序列化格式。
管理员昵称通过多表查询返回,字段类型设为 String。
Mapper 由于查询条件繁琐,采用 XML 方式编写 SQL。
<?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.bite.system.mapper.exam.ExamMapper" >
<select resultType ="com.bite.system.model.exam.vo.ExamVO" >
SELECT te.exam_id, te.title, te.start_time, te.end_time, te.create_time, ts.nick_name as create_name, te.status
FROM tb_exam te
LEFT JOIN tb_sys_user ts ON te.create_by = ts.user_id
<where >
<if test ="title !=null and title !='' " >
AND te.title LIKE CONCAT('%',#{title},'%')
</if >
<if test ="startTime != null and startTime != '' " >
AND te.start_time >= #{startTime}
</if >
<if test ="endTime != null and endTime != ''" >
AND te.end_time < = #{endTime}
</if >
</where >
ORDER BY te.create_time DESC
</select >
</mapper >
前端代码开发 页面包含搜索表单(日期范围、竞赛名称)、表格展示及操作按钮。状态根据开始时间和发布时间动态展示标签(已开赛/未开赛,已发布/未发布)。
<template>
<el-form inline="true">
<el-form-item label="创建日期">
<el-date-picker v-model="datetimeRange" type="datetimerange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
</el-form-item>
<el-form-item label="竞赛名称">
<el-input v-model="params.title" placeholder="请您输入要搜索的竞赛名称" />
</el-form-item>
<el-form-item>
<el-button @click="onSearch" plain>搜索</el-button>
<el-button @click="onReset" plain type="info">重置</el-button>
<el-button type="primary" :icon="Plus" plain @click="onAddExam">添加竞赛</el-button>
</el-form-item>
</el-form>
<!-- 表格 -->
<el-table :data="examList">
<el-table-column prop="title" label="竞赛标题"/>
<el-table-column prop="startTime" label="竞赛开始时间" />
<el-table-column prop="endTime" label="竞赛结束时间" />
<el-table-column label="是否开赛">
<template #default="{ row }">
<div v-if="!isNotStartExam(row)"><el-tag type="warning">已开赛</el-tag></div>
<div v-else><el-tag type="info">未开赛</el-tag></div>
</template>
</el-table-column>
<el-table-column prop="status" label="是否发布">
<template #default="{ row }">
<div v-if="row.status == 0"><el-tag type="danger">未发布</el-tag></div>
<div v-if="row.status == 1"><el-tag type="success">已发布</el-tag></div>
</template>
</el-table-column>
<el-table-column prop="createName" label="创建用户" />
<el-table-column prop="createTime" label="创建时间" />
<el-table-column label="操作">
<template #default="{ row }">
<el-button v-if="isNotStartExam(row) && row.status == 0" type="text" @click="onEdit(row.examId)">编辑</el-button>
<el-button v-if="isNotStartExam(row) && row.status == 0" type="text" @click="onDelete(row.examId)">删除</el-button>
<el-button v-if="row.status == 1 && isNotStartExam(row)" type="text" @click="cancelPublishExam(row.examId)">撤销发布</el-button>
<el-button v-if="row.status == 0 && isNotStartExam(row)" type="text" @click="publishExam(row.examId)">发布</el-button>
<el-button type="text" v-if="!isNotStartExam(row)">已开赛,不允许操作</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页区域 -->
<el-pagination background size="small" layout="total, sizes, prev, pager, next, jumper" :total="total" v-model:current-page="params.pageNum" v-model:page-size="params.pageSize" :page-sizes="[5, 10, 15, 20]" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</template>
<script setup>
import { Plus } from '@element-plus/icons-vue'
function isNotStartExam(exam) {
const now = new Date();
return new Date(exam.startTime) > now
}
</script>
查询与重置逻辑中,时间范围数组需拆分为两个参数赋值给 DTO。分页数据通过异步请求获取。
增加竞赛
一、不包含题目的竞赛
后端代码 @PostMapping("/add")
public R<String> add (@RequestBody ExamAddDTO examAddDTO) {
return R.ok(examService.add(examAddDTO));
}
校验标题唯一性及时间逻辑(开始时间不早于当前时间,不晚于结束时间)。Exam 类继承 BeanEntity 以利用雪花算法生成 ID。
@Override
public String add (ExamAddDTO examAddDTO) {
checkExamSaveParams(examAddDTO, null );
Exam exam = new Exam ();
BeanUtil.copyProperties(examAddDTO, exam);
examMapper.insert(exam);
return exam.getExamId().toString();
}
private void checkExamSaveParams (ExamAddDTO examSaveDTO, Long examId) {
List<Exam> examList = examMapper.selectList(new LambdaQueryWrapper <Exam>()
.eq(Exam::getTitle, examSaveDTO.getTitle())
.ne(examId != null , Exam::getExamId, examId));
if (CollectionUtil.isNotEmpty(examList)) {
throw new ServiceException (ResultCode.FAILED_ALREADY_EXISTS);
}
if (examSaveDTO.getStartTime().isBefore(LocalDateTime.now())) {
throw new ServiceException (ResultCode.EXAM_START_TIME_BEFORE_CURRENT_TIME);
}
if (examSaveDTO.getStartTime().isAfter(examSaveDTO.getEndTime())) {
throw new ServiceException (ResultCode.EXAM_START_TIME_AFTER_END_TIME);
}
}
前端代码 模板包含基本信息表单(名称、周期)及题目选择模块。保存基本信息后获取 examId,再执行题目添加。
二、包含题目的竞赛 先保存竞赛基础信息,再批量添加题目关联,防止出现无归属的题目集合。
后端代码 校验竞赛存在性及状态(已开启不可添加)。批量查找题目是否存在,若存在则批量插入关联表。
@Override
public boolean questionAdd (ExamQuestAddDTO examQuestAddDTO) {
Exam exam = getExam(examQuestAddDTO.getExamId());
checkExam(exam);
Set<Long> questionIdSet = examQuestAddDTO.getQuestionIdSet();
if (CollectionUtil.isEmpty(questionIdSet)) {
return true ;
}
List<Question> questionList = questionMapper.selectBatchIds(questionIdSet);
if (CollectionUtil.isEmpty(questionList) || questionList.size() < questionIdSet.size()) {
throw new ServiceException (ResultCode.EXAM_QUESTION_NOT_EXISTS);
}
return saveExamQuestion(exam, questionIdSet);
}
前端代码 点击添加题目弹出对话框,复用题目列表组件。多选题目后提交,需确保 examId 已存在。提交后刷新详情列表。
竞赛详情
后端代码 @GetMapping("/detail")
public R<ExamDetailVO> detail (Long examId) {
return R.ok(examService.detail(examId));
}
使用 QuestionVO 返回题目 ID、标题、难度等必要信息。
通过 examId 查询竞赛信息,关联查询题目列表并按顺序排序,最后组装返回对象。
前端代码 从路由参数获取 examId,请求详情接口回填表单。注意时间字段需拆分为日期范围数组。
竞赛编辑
竞赛基本信息编辑 后端校验标题唯一性时排除当前 ID。前端复用新增页面的表单结构。
竞赛题目信息编辑
后端代码 @DeleteMapping("/question/delete")
public R<Void> questionDelete (Long examId, Long questionId) {
return toR(examService.questionDelete(examId, questionId));
}
校验竞赛存在性及状态(已发布不可修改)。删除关联表中对应记录。
前端代码 删除成功后重新请求详情接口刷新列表。初始化时设置空列表以防报错。
为优化体验,在查询题目列表时排除已选题目 ID,避免重复选择。
竞赛删除
后端代码 校验竞赛状态(已发布不可删除)。级联删除关联题目记录,再删除竞赛主记录。
前端代码
竞赛发布与撤销发布
后端代码 @PutMapping("/publish")
public R<Void> publish (Long examId) {
return toR(examService.publish(examId));
}
@PutMapping("/cancelPublish")
public R<Void> cancelPublish (Long examId) {
return toR(examService.cancelPublish(examId));
}
前端代码 调用发布或撤销接口,成功后刷新列表。新增竞赛时需初始化题目列表为空数组,防止空指针异常。
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online