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

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

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

王者发布于 2026/3/21更新于 2026/10/691 浏览
在线 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 not null comment '创建时间',
    update_by bigint unsigned comment '更新人',
    update_time datetime comment '更新时间',
    primary key(exam_question_id)
);

列表查询实现

后端接口

列表页需要支持分页、标题模糊搜索以及时间范围筛选。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. 状态管理与发布

更多推荐文章

查看全部
  • 绿联云 NAS 配置 WebDAV 实现公网同步
  • ChatGPT 在企业各管理岗位的应用实践与策略
  • Android 开发春招准备指南:面试题汇总与复习策略
  • 企业级大模型接入全流程:从需求分析到持续优化的八步指南
  • 移动端高颜值登录页实战:动态背景与仿幕布风格
  • Stable Diffusion 技术详解:LoRA 模型与 ControlNet 协同应用
  • Python 核心优势、职业方向与系统学习指南
  • 二分答案专题:木材加工与砍树算法详解
  • SpringBoot 整合 Flink CDC 实时追踪 MySQL 数据变动
  • 分布式文件存储服务设计与实现优化
  • 本地部署开源大模型教程:LangChain + Streamlit + Llama
  • 从零开始实现 LLaMa3 模型:核心架构与代码解析
  • 元境智搭:低代码可视化AR远程协助与巡检平台
  • Python pip 国内镜像源配置指南:清华、阿里云等常用源设置
  • Python 爬虫入门实战:从请求到数据保存
  • Java 使用 org.w3c.dom 解析 XML 文档的创建、读写与遍历
  • C++17:继 C++11 后的实质性增强与核心特性总结
  • Python 算法题解:链表、二叉树、动态规划等
  • Qwen3-Embedding-4B 本地化部署实战:llama.cpp 与 vLLM 方案
  • Dify 工作流集成 TTS:低代码实现语音输出

相关免费在线工具

  • 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