跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Java大前端java算法

在线 OJ 系统竞赛管理模块开发实战 (Java-Spring)

综述由AI生成在线 OJ 系统竞赛管理模块涵盖数据建模、列表查询、竞赛创建及状态流转。通过 Java-Spring Boot 构建后端服务,利用 MyBatis Plus 处理复杂查询与批量操作;前端采用 Vue 配合 Element Plus 实现交互。重点解决长整型 ID 序列化精度问题、竞赛生命周期状态校验(未发布/已发布/已结束)以及题目关联逻辑。实现了从基础信息保存到题目动态添加的全流程管理,确保数据一致性与业务规则约束。

王者发布于 2026/3/21更新于 2026/6/820 浏览
在线 OJ 系统竞赛管理模块开发实战 (Java-Spring)

数据模型设计

竞赛管理的核心在于状态流转与题目关联。我们设计了 tb_exam 存储竞赛基础信息,tb_exam_question 维护竞赛与题目的多对多关系。

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  comment ,
    update_by  unsigned comment ,
    update_time datetime comment ,
    (exam_question_id)
);
not null
'创建时间'
bigint
'更新人'
'更新时间'
primary key

列表查询实现

后端接口

列表页需要支持分页、标题模糊搜索以及时间范围筛选。Controller 层直接调用 Service 返回标准表格数据。

@RestController
@RequestMapping("/exam")
public class ExamController extends BaseController {
    @Autowired
    private IExamService examService;

    // 获取竞赛列表
    @GetMapping("/list")
    public TableDataInfo list(ExamQueryDTO examQueryDTO) {
        return getDataTable(examService.list(examQueryDTO));
    }
}

请求参数继承通用的分页对象 PageDomain,并补充业务查询字段。返回值 VO 中需要注意 ID 的序列化问题。由于雪花算法生成的 ID 可能超过 JavaScript Number 的安全整数范围,我们将 Long 类型转为 String 处理。

@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ExamVO {
    @JsonSerialize(using = ToStringSerializer.class)
    private Long examId;
    private String title;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime startTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime endTime;
    private Integer status;
    private String createName; // 通过联表查询获取创建人昵称
    private LocalDateTime createTime;
}

Mapper 层使用 XML 编写复杂查询,利用 MyBatis 的动态 SQL 处理可选条件。

<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 &lt;= #{endTime}
            </if>
        </where>
        ORDER BY te.create_time DESC
    </select>
</mapper>

前端交互

前端使用 Element Plus 的日期范围选择器收集时间区间。注意,组件返回的是 Date 数组,需转换为 ISO 字符串传给后端。

<template>
  <el-form inline="true">
    <el-form-item label="竞赛名称">
      <el-input v-model="params.title" placeholder="请输入竞赛名称" />
    </el-form-item>
    <el-form-item label="创建日期">
      <el-date-picker 
        v-model="datetimeRange" 
        type="datetimerange" 
        range-separator="至" 
        start-placeholder="开始日期" 
        end-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>
  <!-- 表格展示略 -->
</template>

<script setup>
// 时间处理逻辑
async function getExamList() {
  if (datetimeRange.value[0] instanceof Date) {
    params.startTime = datetimeRange.value[0].toISOString();
  }
  if (datetimeRange.value[1] instanceof Date) {
    params.endTime = datetimeRange.value[1].toISOString();
  }
  const result = await getExamListService(params);
  examList.value = result.rows;
  total.value = result.total;
}
</script>

创建竞赛流程

竞赛创建分为两步:先保存基本信息生成竞赛 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);
    }
}

关联题目

题目选择通过弹窗展示题库列表,支持多选。提交时需传入竞赛 ID 和题目 ID 集合。

@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),则提示用户先保存。选中题目后,将 ID 集合封装为 DTO 发送请求。

详情编辑与删除

详情查看

详情页需要聚合竞赛信息与题目列表。题目列表只需展示 ID、标题、难度,无需冗余信息。

@Override
public ExamDetailVO detail(Long examId) {
    ExamDetailVO examDetailVO = new ExamDetailVO();
    Exam exam = getExam(examId);
    BeanUtil.copyProperties(exam, examDetailVO);

    // 按顺序获取题目 ID
    List<ExamQuestion> examQuestionList = examQuestionMapper.selectList(
        new LambdaQueryWrapper<ExamQuestion>()
            .select(ExamQuestion::getQuestionId)
            .eq(ExamQuestion::getExamId, examId)
            .orderByAsc(ExamQuestion::getQuestionOrder)
    );
    
    if (CollectionUtil.isEmpty(examQuestionList)) return examDetailVO;

    List<Long> questionIdList = examQuestionList.stream()
        .map(ExamQuestion::getQuestionId).toList();
    
    List<Question> questionList = questionMapper.selectList(
        new LambdaQueryWrapper<Question>()
            .select(Question::getQuestionId, Question::getTitle, Question::getDifficulty)
            .in(Question::getQuestionId, questionIdList)
    );
    
    examDetailVO.setExamQuestionList(BeanUtil.copyToList(questionList, QuestionVO.class));
    return examDetailVO;
}

编辑与删除

编辑时同样需要校验标题唯一性(排除自身)和时间逻辑。删除操作必须确保竞赛未发布且未开始。

@Override
public int delete(Long examId) {
    Exam exam = getExam(examId);
    // 已发布或已结束不可删除
    if (Contants.TRUE.equals(exam.getStatus())) {
        throw new ServiceException(ResultCode.EXAM_IS_PUBLISH);
    }
    checkExam(exam);
    // 级联删除关联题目
    examQuestionMapper.delete(new LambdaQueryWrapper<ExamQuestion>().eq(ExamQuestion::getExamId, examId));
    return examMapper.deleteById(exam);
}

前端在删除题目时,会重新拉取详情以刷新列表。为了避免重复显示已选题目,我们在查询题库时增加排除逻辑,将已关联的题目 ID 从搜索结果中过滤掉。

<!-- 题库查询排除已选 ID -->
<if test="excludeIdSet !=null and !excludeIdSet.isEmpty()">
    <foreach collection="excludeIdSet" open=" AND tq.question_id NOT IN( " close=" ) " item="id" separator=",">
        #{id}
    </foreach>
</if>

状态管理与发布

竞赛的生命周期包含未发布、已发布、已结束三种状态。发布前必须校验是否已关联题目,且结束时间未过。

@Override
public int publish(Long examId) {
    Exam exam = getExam(examId);
    if (exam.getEndTime().isBefore(LocalDateTime.now())) {
        throw new ServiceException(ResultCode.EXAM_IS_FINISH);
    }
    Long count = examQuestionMapper.selectCount(
        new LambdaQueryWrapper<ExamQuestion>().eq(ExamQuestion::getExamId, examId)
    );
    if (count == null || count <= 0) {
        throw new ServiceException(ResultCode.EXAM_NOT_HAS_QUESTION);
    }
    exam.setStatus(Contants.TRUE);
    return examMapper.updateById(exam);
}

撤销发布逻辑类似,仅将状态置回未发布即可。前端根据状态动态控制操作按钮的显隐,例如已开赛状态下禁止编辑或删除。

目录

  1. 数据模型设计
  2. 列表查询实现
  3. 后端接口
  4. 前端交互
  5. 创建竞赛流程
  6. 基础信息保存
  7. 关联题目
  8. 详情编辑与删除
  9. 详情查看
  10. 编辑与删除
  11. 状态管理与发布
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • eBay 商品数据采集实战:Python 接入 IPIDEA 网页抓取 API
  • 基于 DeepSeek-V3.1 的 MATLAB 本地 AI 编程工具实战
  • OpenClaw Web Search 工具配置与官方搜索渠道详解
  • LeetCode 398. Random Pick Index 解题思路与 Python 实现
  • 谷歌 Gemini API 快速入门与 LangChain 调用指南
  • DICOM 标准详解:文件解析、Java/Python 库与 AI 应用
  • voidImageViewer:轻量级图像查看工具,GIF/WEBP 动画支持
  • Python 调用高德地图 MCP 服务查询天气示例
  • Spring Boot 整合 Neo4j 图数据库项目实战详解
  • Git clone 速度慢:配置国内镜像、浅克隆的优化方案
  • Whisper 语音识别本地化部署实战指南
  • 机器学习十大核心算法原理与 Python 实现
  • 基于 Dify 和 LangBot 搭建飞书智能体机器人
  • 高性能 Go 缓存库 Ristretto:从算法原理到生产级架构实践
  • 前端面试核心知识点全解析
  • 前端精确数字运算方案:使用 BigNumber.js 解决 JavaScript 精度问题
  • 结构化思维:ChatGPT 如何实现高效信息管理
  • Python 实现草榴论坛磁力链接抓取示例
  • 基于 openJiuwen 记忆库构建 AI 职业匹配智能体
  • 大模型提示工程实战指南:掌握提问驱动 AI

相关免费在线工具

  • 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