跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
Java大前端java算法

基于 Java-Spring 的在线 OJ 系统竞赛管理功能开发

在线 OJ 系统竞赛管理模块涵盖数据库设计、后端接口及前端交互。后端采用 Java Spring Boot 结合 MyBatis Plus,实现竞赛列表查询、新增、编辑、删除及发布状态管理。前端基于 Vue 与 Element Plus 构建,支持时间范围筛选、分页展示及题目关联操作。核心逻辑包括竞赛时间校验、题目批量插入、已发布状态锁定及雪花算法 ID 处理。通过前后端分离架构完成竞赛全生命周期管理功能。

城市逃兵发布于 2026/3/16更新于 2026/4/263 浏览
基于 Java-Spring 的在线 OJ 系统竞赛管理功能开发

在线 OJ 系统竞赛管理模块

表结构创建

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
@RestController
@RequestMapping("/exam")
public class ExamController extends BaseController {
    @Autowired
    private IExamService examService;

    // exam/list
    @GetMapping("/list")
    public TableDataInfo list(ExamQueryDTO examQueryDTO) {
        return getDataTable(examService.list(examQueryDTO));
    }
}
DTO

前端传入参数需要包含查询条件(标题,开始时间,结束时间)。因为是分页查询,所以需要继承之前写的 pageDomain。

Service

使用分页插件调用 mapper 进行查询。

VO

返回值类型与题库列表一致。注解作用说明:

  • @JsonSerialize(using = ToStringSerializer.class):将 Long 类型的 examId 先转换为字符串,然后再进行序列化。由于雪花算法产生的值会超过 Long 的范围,在 JavaScript 等语言中处理非常大的整数时可能出现精度丢失,转为字符串可避免此问题。
  • @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss"):指定 LocalDateTime 类型的 endTime 字段在序列化为 JSON 时的日期时间格式,方便前后端统一处理。
  • 数据库数据为 int 类型,但通过多表查询可将管理员昵称返回给前端,所以 VO 中使用 String 类型。
Mapper

由于查询条件比较繁琐,使用 XML 方式解决。

<?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 &lt;= #{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>

逻辑说明:

  • <el-date-picker> 是日期时间范围选择器组件。
  • isNotStartExam 判断函数:如果当前时间晚于竞赛开始时间则为已开赛,否则未开赛。
  • 根据 status 的值不同去展示是否发布。
  • 如果已经开赛并且没到开始时间可以撤销发布,否则已经开赛不允许修改。
  • 分页功能支持调整每页条数和跳转。
查询重置功能
function onSearch() {
  params.pageNum = 1
  getExamList()
}
function onReset() {
  params.pageNum = 1
  params.pageSize = 10
  params.title = ''
  params.startTime = ''
  params.endTime = ''
  datetimeRange.value.length = 0
  getExamList()
}

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
}

因为时间范围是一个数组的形式,但是后端需要的是 2 个参数(开始时间和结束时间),所以需要单独赋值,而其他的因为双向绑定,所以不需要。

增加竞赛

一、不包含题目的竞赛

后端代码
Controller
@RestController
@RequestMapping("/exam")
public class ExamController extends BaseController {
    @Autowired
    private IExamService examService;

    @PostMapping("/add")
    public R<String> add(@RequestBody ExamAddDTO examAddDTO) {
        return R.ok(examService.add(examAddDTO));
    }
}
Service

首先我们需要判断竞赛标题是否重复,竞赛开始时间和结束时间判断。然后将 DTO 中内容复制给 Exam 类。

@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);
    }
}

原因:Exam 类继承了 BeanEntity 类以及使用了雪花算法,可以提高 id 和创建时间和创建人。最后返回竞赛 ID,为后面的增加题目做准备。

前端代码

模板代码包含竞赛信息模块、添加竞赛题目模块、题目配置模块及提交任务区域。API 请求封装了获取列表、新增竞赛、添加竞赛题目等服务。

基本信息保存按键逻辑:

async function saveBaseInfo() {
  const fd = new FormData()
  for (let key in formExam) {
    if (key === 'examDate') {
      fd.append('startTime', formExam.examDate[0]);
      fd.append('endTime', formExam.examDate[1]);
    } else {
      fd.append(key, formExam[key])
    }
  }
  await examAddService(fd)
  ElMessage.success('基本信息保存成功')
}

二、包含题目的竞赛

为什么要先保存后新增?

  1. 为了防止太多没有竞赛名字的题目集合存在,导致最后不知道到底是哪个。
  2. 添加一个题目即可存在这个竞赛中,不用害怕突然退出导致的重新添加。
后端代码
Service
@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);
}

private Exam getExam(Long examId) {
    Exam exam = examMapper.selectById(examId);
    if (exam == null) {
        throw new ServiceException(ResultCode.FAILED_NOT_EXISTS);
    }
    return exam;
}

细节分析:

  • 首先判断这个竞赛是否存在,如果存在则返回 Exam,否则抛出异常资源不存在。
  • 检查一下竞赛是否开启,如果开启了则不能进行添加。
  • 获得问题列表,如果没有题目,则直接返回即可。
  • 通过问题 ids 进行批量查找,如果有找不到的题目,则直接抛出异常,资源不存在。
  • 批量插入操作,将问题批量插入 tb_exam_question 中(竞赛 id,题目 id,题目顺序)。先将数据统一存在一个列表里面,然后一起插入,但是因为 mybatis-plus 中没有对应的批量插入方法,所以我们继承其他类提高的 saveBatch 方法。
前端代码

点击添加题目后会弹出弹框,将之前的题目列表在一个弹框中展示。

实现添加点击事件:

const dialogVisible = ref(false)
function addQuestion() {
  if (formExam.examId === null || formExam.examId === '') {
    ElMessage.error('请先保存竞赛基本信息')
  } else {
    getQuestionList()
    dialogVisible.value = true
  }
}

async function getQuestionList() {
  const result = await getQuestionListService(params)
  console.log(result)
  questionList.value = result.rows
  total.value = result.total
}

多选框处理所选择的题目:

function handleRowSelect(selection) {
  questionIdSet.value = []
  selection.forEach(element => {
    questionIdSet.value.push(element.questionId)
  });
}

提交逻辑:

async function submitSelectQuestion() {
  if (questionIdSet.value && questionIdSet.value.length < 1) {
    ElMessage.error('请先选择要提交的题目')
    return false
  }
  const examQ = reactive({ examId: formExam.examId, questionIdSet: questionIdSet.value })
  console.log(examQ)
  await addExamQuestionService(examQ);
  dialogVisible.value = false
  ElMessage.success('竞赛题目添加成功')
}

竞赛详情

后端代码

Controller
@GetMapping("/detail")
public R<ExamDetailVO> detail(Long examId) {
    return R.ok(examService.detail(examId));
}
VO
@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ExamDetailVO {
    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 List<QuestionVO> examQuestionList;
}

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class QuestionVO {
    @JsonSerialize(using = ToStringSerializer.class)
    private Long questionId;
    private String title;
    private Integer difficulty;
    private String createName;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
}
Service
@Override
public ExamDetailVO detail(Long examId) {
    ExamDetailVO examDetailVO = new ExamDetailVO();
    Exam exam = getExam(examId);
    BeanUtil.copyProperties(exam, examDetailVO);
    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));
    List<QuestionVO> questionVOList = new ArrayList<>();
    questionVOList = BeanUtil.copyToList(questionList, QuestionVO.class);
    examDetailVO.setExamQuestionList(questionVOList);
    return examDetailVO;
}

这里总的来说就是从一堆表里面通过关系去查询结果,然后将内容进行截断赋值给要返回的类型。

前端代码

创建请求函数。点击编辑按钮的时候,会进行路由,为了携带 examId 以及 type,我们在路由上面携带。

进来之后,首先从路由上获得 examId,然后对 formExam 的竞赛 ID 进行赋值。注意:由于后端是分为开始时间和结束时间,而前端只有一个 examDate,所以需要特殊处理。

async function getExamDetail() {
  const examId = useRoute().query.examId
  console.log(examId)
  if (examId) {
    formExam.examId = examId
    const examDetail = await getExamDetailService(examId)
    Object.assign(formExam, examDetail.data)
    formExam.examDate = [examDetail.data.startTime, examDetail.data.endTime]
  }
}

竞赛编辑

竞赛基本信息编辑

后端代码
Controller
@PostMapping("/edit")
public R<Void> edit(@RequestBody ExamEditDTO examEditDTO) {
    return toR(examService.edit(examEditDTO));
}
Service
@Override
public int edit(ExamEditDTO examEditDTO) {
    Exam exam = getExam(examEditDTO.getExamId());
    checkExam(exam);
    checkExamSaveParams(examEditDTO, examEditDTO.getExamId());
    exam.setTitle(examEditDTO.getTitle());
    exam.setStartTime(examEditDTO.getStartTime());
    exam.setEndTime(examEditDTO.getEndTime());
    return examMapper.updateById(exam);
}

相同竞赛 id 的时候可以同样的标题,但不同竞赛 ID 标题不能相同。

竞赛题目信息编辑

后端代码
Controller
@DeleteMapping("/question/delete")
public R<Void> questionDelete(Long examId, Long questionId) {
    return toR(examService.questionDelete(examId, questionId));
}
Service
@Override
public int questionDelete(Long examId, Long questionId) {
    Exam exam = getExam(examId);
    checkExam(exam);
    if (Contants.TRUE.equals(exam.getStatus())) {
        throw new ServiceException(ResultCode.EXAM_IS_PUBLISH);
    }
    return examQuestionMapper.delete(new LambdaQueryWrapper<ExamQuestion>()
        .eq(ExamQuestion::getExamId, examId)
        .eq(ExamQuestion::getQuestionId, questionId));
}

详细分析:

  • 首先查看这个竞赛是否存在。
  • 因为在比赛开始后,我们不能进行删除题目操作,所以检查是否已经开始。
  • 判断是否已经开赛,如果已经开赛则不能修改(双重保险)。
  • 去删除 tb_exam_question 中竞赛 id 相同且题目 Id 相同的数据。
前端代码
async function deleteExamQuestion(examId, questionId) {
  await delExamQuestionService(examId, questionId)
  getExamDetailById(examId)
  ElMessage.success('竞赛题目删除成功')
}

async function getExamDetailById(examId) {
  const examDetail = await getExamDetailService(examId)
  formExam.examQuestionList = []
  Object.assign(formExam, examDetail.data)
  formExam.examDate = [examDetail.data.startTime, examDetail.data.endTime]
}

注意:由于当我们删除最后一个题目的时候,我们会导致 examQuestionList 为空,导致赋值的时候无法找到,所以我们需要提前设置一下。

由于添加题目之后也需要重新请求详细信息,所以也修改代码:

async function submitSelectQuestion() {
  if (questionIdSet.value && questionIdSet.value.length < 1) {
    ElMessage.error('请先选择要提交的题目')
    return false
  }
  const examQ = reactive({ examId: formExam.examId, questionIdSet: questionIdSet.value })
  console.log(examQ)
  await addExamQuestionService(examQ);
  dialogVisible.value = false
  getExamDetailById(formExam.examId)
  ElMessage.success('竞赛题目添加成功')
}

我们现在发现,我们添加题目之后,我们点击添加题目之后还是会显示出来,这对用户不友好,所以我们继续修改后端代码和前端代码。

以分号作为分隔符:

@Override
public List<QuestionVO> list(QuestionQueryDTO questionQueryDTO) {
    String excludeIdStr = questionQueryDTO.getExcludeIdStr();
    if (StrUtil.isNotEmpty(excludeIdStr)) {
        String[] excludeIdArr = excludeIdStr.split(Contants.SPLIT_SEM);
        Set<Long> excludeIdSet = Arrays.stream(excludeIdArr)
            .map(Long::valueOf)
            .collect(Collectors.toSet());
        questionQueryDTO.setExcludeIdSet(excludeIdSet);
    }
    PageHelper.startPage(questionQueryDTO.getPageNum(), questionQueryDTO.getPageSize());
    return questionMapper.selectQuestionList(questionQueryDTO);
}

得到前端的参数后,先将 excludeIdStr 按照分隔符进行分割为数组,然后将数组转为 Set 类型的数据,最后将参数赋值给 DTO 进行数据库查询。

修改之前的 xml 文件:

<?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.question.QuestionMapper">
    <select resultType="com.bite.system.model.question.vo.QuestionVO">
        SELECT tq.question_id, tq.title, tq.difficulty, ts.nick_name as create_name, tq.create_time
        FROM tb_question tq
        LEFT JOIN tb_sys_user ts ON tq.create_by = ts.user_id
        <where>
            <if test="difficulty !=null ">
                AND difficulty = #{difficulty}
            </if>
            <if test="title !=null and title !='' ">
                AND title LIKE CONCAT('%',#{title},'%')
            </if>
            <if test="excludeIdSet !=null and !excludeIdSet.isEmpty()">
                <foreach collection="excludeIdSet" open=" AND tq.question_id NOT IN( " close=" ) " item="id" separator=",">
                    #{id}
                </foreach>
            </if>
        </where>
        ORDER BY create_time DESC
    </select>
</mapper>

这样我们搜索的时候就可以排除我们集合中的 questionId。

然后修改前端代码,我们进行查询前先查找已经选择的题目 id,然后进行查找。

竞赛删除

后端代码

Controller
@DeleteMapping("/delete")
public R<Void> delete(Long examId) {
    return toR(examService.delete(examId));
}
Service

我们这里只需要保证是在开始之前进行删除即可,调用数据库删除竞赛里面的问题,然后删除竞赛。

@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);
}
前端代码
async function onDelete(examId) {
  await delExamService(examId)
  params.pageNum = 1
  getExamList()
}

竞赛发布与撤销发布

后端代码

@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));
}

@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);
}

@Override
public int cancelPublish(Long examId) {
    Exam exam = getExam(examId);
    checkExam(exam);
    if (exam.getEndTime().isBefore(LocalDateTime.now())) {
        throw new ServiceException(ResultCode.EXAM_IS_FINISH);
    }
    exam.setStatus(Contants.FALSE);
    return examMapper.updateById(exam);
}

这里逻辑简单不做多余讲解。

前端代码

import { publishExamService, cancelPublishExamService } from '../apis/exam'

async function publishExam(examId) {
  await publishExamService(examId)
  getExamList()
}

async function cancelPublishExam(examId) {
  await cancelPublishExamService(examId)
  getExamList()
}

在新增竞赛的时候发现,当我们新增页面或者之前没有选择题目的竞赛的时候,我们点击新增题目会报错,这是因为 examQuestionList 为 null。至此 B 端竞赛管理功能开发完成。

目录

  1. 在线 OJ 系统竞赛管理模块
  2. 表结构创建
  3. 竞赛列表
  4. 后端代码开发
  5. Controller
  6. DTO
  7. Service
  8. VO
  9. Mapper
  10. 前端代码开发
  11. 页面半成品代码
  12. 查询重置功能
  13. 增加竞赛
  14. 一、不包含题目的竞赛
  15. 后端代码
  16. Controller
  17. Service
  18. 前端代码
  19. 二、包含题目的竞赛
  20. 后端代码
  21. Service
  22. 前端代码
  23. 竞赛详情
  24. 后端代码
  25. Controller
  26. VO
  27. Service
  28. 前端代码
  29. 竞赛编辑
  30. 竞赛基本信息编辑
  31. 后端代码
  32. Controller
  33. Service
  34. 竞赛题目信息编辑
  35. 后端代码
  36. Controller
  37. Service
  38. 前端代码
  39. 竞赛删除
  40. 后端代码
  41. Controller
  42. Service
  43. 前端代码
  44. 竞赛发布与撤销发布
  45. 后端代码
  46. 前端代码
  • 💰 8折买阿里云服务器限时8折了解详情
  • 💰 8折买阿里云服务器限时8折购买
  • 🦞 5分钟部署阿里云小龙虾了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 双指针算法实战:三角形个数与多数之和
  • DeepSeek 各版本详解与优缺点对比
  • Ubuntu 配置 Samba 实现跨系统文件共享
  • 手机端运行 Stable Diffusion 的开源方案与使用指南
  • Llama-2-7b 昇腾 NPU 测评:性能数据、场景适配与硬件选型
  • VSCode 中 Git 推送与拉取的详细操作指南
  • 使用 Python 实现微信自动回复功能
  • OpenClaw 本地 AI 助手部署与飞书对接指南
  • 2024 年转行 AI 产品经理的行业趋势与准备指南
  • WebAgent 详解与实战:利用开源 AI 智能体进行产品与竞品市场调研
  • Spring Cloud Alibaba 微服务架构详解
  • Python 在 CentOS 系统上的安装、配置与部署深度指南
  • Linux 环境变量详解:从底层原理到实战操作
  • AIGC 产品经理核心能力模型与从 0 到 1 落地实战
  • GitHub 启用双因素身份验证(2FA)配置指南:TOTP.app 动态验证码设置
  • Rokid 灵珠平台搭建旅游 AR 智能体实战
  • 二叉树算法实战:美国血统与深度宽度计算
  • Stable Diffusion 结合 Redream 实现现实影像转二次元动画
  • 无需 OCR 的 PDF 多模态 RAG 方案:ColQwen2、Qwen2.5 与 Weaviate 实战
  • Google Stitch 实战指南:从自然语言生成 UI 到原型迭代

相关免费在线工具

  • 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