【项目】【在线判题系统】竞赛发布与撤销发布

目录

一、竞赛发布

发布竞赛的前提:竞赛是存在、竞赛中是存在题目的

  1. 在满足发布竞赛前提之后,点击发布竞赛,前端携带竞赛id向后端发起请求。
  2. 后端接收到请求之后,根据竞赛id判断前提是否成立。如果不成立经不成立原因发送给前端。如果成立竞赛的状态字段从0改为1,并且同步到数据库,再将修改结果发送给
  3. 前端接收到后端的响应之后,如果失败,展示失败原因,如果成功,跳转回列表页面,当前的状态变为已发布。C端竞赛列表中要能找到已发布的竞赛。

竞赛撤销发布的前提:竞赛存在 ,竞赛处于未开始状态竞赛撤销发布功能:

  1. 找到要撤销发布的竞赛,点击撤销发布按钮。前端携带者竞赛id向后端发起请求。

  2. 后端接收到请求之后,根据竞赛id判断前提条件是否成立。如果不成立,返回前端不成立的原因。如果成立,将竞赛状态字段再从1变成1.并且同步数据库,再将结果返回给前端。

  3. 前端接收到响应之后,如果失败,展示失败原因。如果成功,B端竞赛列表中当前竞赛状态变为未发布状态。当前竞赛从C端竞赛列表中消失

二、controller

oj-modules/oj-system/src/main/java/com/yj/system/controller/exam/ExamController.java

java 复制代码
    @PutMapping("/publish")
    public R<Void> publish(@NotBlank Long examId) {
        return toR(examService.publish(examId));
    }
    @PutMapping("/cancelPublish")
    public R<Void> cancelPublish(@NotBlank Long examId) {
        return toR(examService.cancelPublish(examId));
    }

三、service

oj-modules/oj-system/src/main/java/com/yj/system/service/exam/IExamService.java接口方法

java 复制代码
    int publish(Long examId);

    int cancelPublish(Long examId);

oj-modules/oj-system/src/main/java/com/yj/system/service/exam/impl/ExamServiceImpl.java实现

java 复制代码
    @Override
    public int publish(Long examId) {
        ExamDO examDO = getExamDO(examId);
        Long count = examQuestionMapper.selectCount(new LambdaQueryWrapper<ExamQuestionDO>()
                .eq(ExamQuestionDO::getExamId, examId)
        );
        if(null == count || 0 >= count) {
            throw new ServiceException(ResultCodeEnum.EXAM_NOT_HAVE_QUESTION);
        }
        examDO.setStatus(Constants.TRUE);
        return examMapper.updateById(examDO);
    }

    @Override
    public int cancelPublish(Long examId) {
        //竞赛存在
        ExamDO examDO = getExamDO(examId);
        //竞赛未开始
        checkExamStarted(examDO);

        examDO.setStatus(Constants.FALSE);
        return examMapper.updateById(examDO);
    }

四、竞赛模块前端代码

oj-vue\oj-fe-b\src\views\Exam.vue

java 复制代码
<template>
  <el-form inline="true">
    <el-form-item label="创建日期">
      <el-date-picker v-model="datetimeRange" style="width: 240px" 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 height="526px" :data="examList">
    <el-table-column prop="title" label="竞赛标题"/>
    <el-table-column prop="startTime" width="180px" label="竞赛开始时间" />
    <el-table-column prop="endTime" width="180px" label="竞赛结束时间" />
    <el-table-column label="是否开赛" width="100px">
      <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" width="100px" 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" width="140px" label="创建用户" />
    <el-table-column prop="createTime" width="180px" label="创建时间" />
    <el-table-column label="操作" width="180px">
      <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)" class="red">删除
        </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'
import { getExamListService, delExamService, publishExamService, cancelPublishExamService } from '@/apis/exam'
import { reactive,ref } from 'vue'
import router from '@/router'

function isNotStartExam(exam) {
  const now = new Date(); //当前时间
  return new Date(exam.startTime) > now
}

const params = reactive({
  pageNum: 1,
  pageSize: 10,
  startTime:'',
  endTime:'',
  title: ''
})

const examList = ref([])
const total = ref(0)
const datetimeRange = ref([])

async function getExamList() {
  if (datetimeRange.value[0] instanceof Date) {
    params.startTime = formatDateTime(datetimeRange.value[0])
  }
  if (datetimeRange.value[1] instanceof Date) {
    params.endTime = formatDateTime(datetimeRange.value[1])
  }
  const result = await getExamListService(params)
  examList.value = result.rows
  total.value = result.total
}

function formatDateTime(date) {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  const hours = String(date.getHours()).padStart(2, '0');
  const minutes = String(date.getMinutes()).padStart(2, '0');
  const seconds = String(date.getSeconds()).padStart(2, '0');
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}

getExamList()

function handleSizeChange(newSize) {
  params.pageNum = 1
  getExamList()
}

function handleCurrentChange(newPage) {
  getExamList()
}

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()
}

function onAddExam() {
  //跳转到新的页面上   
  router.push("/oj/layout/updateExam?type=add")
}

async function onEdit(examId) {
  // await getExamDetailService(examId)
  router.push(`/oj/layout/updateExam?type=edit&examId=${examId}`)
  ///oj/layout/updateExam?type=edit&examId=${examId}
  ///oj/layout/updateExam?type=edit&examId=123
}

async function onDelete(examId) {
  await delExamService(examId)
  params.pageNum = 1
  getExamList()
}

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

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

</script>

oj-vue\oj-fe-b\src\views\UpdateExam.vue

java 复制代码
<template>
  <div class="add-exam-component-box">
    <div class="add-exam-component">
      <!-- 竞赛信息模块 -->
      <div class="exam-base-info-box">
        <!-- 标题 -->
        <div class="exam-base-title">
          <span class="base-title">{{ type === 'edit' ? '编辑竞赛' : '添加竞赛' }}</span>
          <span class="go-back" @click="goBack">返回</span>
        </div>
        <!-- 基本信息 -->
        <div class="exam-base-info">
          <div class="group-box">
            <div class="group-item">
              <div class="item-label required">竞赛名称</div>
              <div>
                <el-input v-model="formExam.title" style="width:420px" placeholder="请填写竞赛名称"></el-input>
              </div>
            </div>
          </div>
          <div class="group-box">
            <div class="group-item">
              <div class="item-label required">竞赛周期</div>
              <div>
                <el-date-picker v-model="formExam.examDate" :disabledDate="disabledDate" type="datetimerange"
                  start-placeholder="竞赛开始时间" end-placeholder="竞赛结束时间" value-format="YYYY-MM-DD HH:mm:ss" />
              </div>
            </div>
          </div>
          <div class="group-box">
            <div class="group-item">
              <el-button class="exam-base-info-button" type="primary" plain @click="saveBaseInfo">保存</el-button>
            </div>
          </div>
        </div>
      </div>
      <!-- 添加竞赛题目 -->
      <div class="exam-select-question-box">
        <el-button class="exam-add-question" :icon="Plus" type="text" @click="addQuestion()">
          添加题目
        </el-button>
        <el-table height="136px" :data="formExam.examQuestionList" class="question-select-list">
          <el-table-column prop="questionId" width="180px" label="题目id" />
          <el-table-column prop="title" :show-overflow-tooltip="true" label="题目标题" />
          <el-table-column prop="difficulty" width="80px" label="题目难度">
            <template #default="{ row }">
              <div v-if="row.difficulty === 1" style="color:#3EC8FF;">简单</div>
              <div v-if="row.difficulty === 2" style="color:#FE7909;">中等</div>
              <div v-if="row.difficulty === 3" style="color:#FD4C40;">困难</div>
            </template>
          </el-table-column>
          <el-table-column label="操作" width="80px">
            <template #default="{ row }">
              <el-button circle type="text" @click="deleteExamQuestion(formExam.examId, row.questionId)">
                删除
              </el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
      <!-- 题目配置模块 题目列表勾选加序号 -->
      <div>
        <el-dialog v-model="dialogVisible">
          <div class="exam-list-box">
            <div class="exam-list-title required">选择竞赛题目</div>
            <el-form inline="true">
              <el-form-item label="题目难度">
                <selector v-model="params.difficulty" style="width: 120px;"></selector>
              </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-form-item>

            </el-form>
            <!-- 题目列表 -->
            <el-table :data="questionList" @select="handleRowSelect">
              <el-table-column type="selection"></el-table-column>
              <el-table-column prop="questionId" label="题目id" />
              <el-table-column prop="title" label="题目标题" />
              <el-table-column prop="difficulty" label="题目难度">
                <template #default="{ row }">
                  <div v-if="row.difficulty === 1" style="color:#3EC8FF;">简单</div>
                  <div v-if="row.difficulty === 2" style="color:#FE7909;">中等</div>
                  <div v-if="row.difficulty === 3" style="color:#FD4C40;">困难</div>
                </template>
              </el-table-column>
            </el-table>
            <!-- 分页区域 -->
            <div class="exam-question-list-button">
              <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="[1, 5, 10, 15, 20]" @size-change="handleSizeChange"
                @current-change="handleCurrentChange" />
              <el-button class="question-select-submit" type="primary" plain
                @click="submitSelectQuestion">提交</el-button>
            </div>
          </div>
        </el-dialog>
      </div>

      <!-- 提交任务区域 -->
      <div class="submit-box absolute">
        <el-button type="info" plain @click="goBack">取消</el-button>
        <el-button type="primary" plain @click="publishExam">发布竞赛</el-button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { examAddService, addExamQuestionService, getExamDetailService, editExamService, delExamQuestionService, publishExamService} from "@/apis/exam"
import { getQuestionListService } from "@/apis/question"
import Selector from "@/components/QuestionSelector.vue"
import router from '@/router'
import { reactive, ref } from "vue"
import { Plus } from '@element-plus/icons-vue'
import { useRoute } from 'vue-router';

const type = useRoute().query.type
const formExam = reactive({
  examId: '',
  title: '',
  examDate: ''
})

const params = reactive({
  pageNum: 1,
  pageSize: 10,
  difficulty: '',
  title: ''
})


// 返回
function goBack() {
  router.go(-1)
}

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 if (key !== 'startTime' && key !== 'endTime') {
      fd.append(key, formExam[key])
    }
  }
  if (formExam.examId) {
    //编辑
    await editExamService(fd)
  } else {
    const addRes = await examAddService(fd)
    formExam.examId = addRes.data
  }
  ElMessage.success('基本信息保存成功')
}

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

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

function handleSizeChange() {
  params.pageNum = 1
  getQuestionList()
}

function handleCurrentChange() {
  getQuestionList()
}


function onSearch() {
  params.pageNum = 1
  getQuestionList()
}

function onReset() {
  params.pageNum = 1
  params.pageSize = 10
  params.title = ''
  params.difficulty = ''
  getQuestionList()
}

async function publishExam() {
  await publishExamService(formExam.examId)
  router.push("/oj/layout/exam")
}

const questionIdSet = ref([])

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);
  getExamDetailById(formExam.examId)
  dialogVisible.value = false
  ElMessage.success('竞赛题目添加成功')
}

async function getExamDetail() {
  const examId = useRoute().query.examId
  if (examId) {
    formExam.examId = examId
    getExamDetailById(examId)
  }
}
getExamDetail()

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]
}

</script>

<style lang="scss" scoped>
.add-exam-component-box {
  height: 100%;
  overflow: hidden;
  position: relative;
}

.exam-list-box {
  background: #fff;
  padding: 20px 24px;

  .question-select-submit {
    margin-left: 0;
    margin-top: 20px;
    width: 100%;
  }

  .exam-list-title {
    font-size: 14px;
    color: rgba(0, 0, 0, 0.85);
    position: relative;
    padding: 15px 20px;
    padding-top: 0;

    &.required::before {
      position: absolute;
      content: '*';
      font-size: 20px;
      color: red;
      left: 10px;
    }
  }
}

.add-exam-component {
  width: 100%;
  background: #fff;
  padding-bottom: 120px;
  overflow-y: auto;
  box-sizing: border-box;
  height: calc(100vh - 50px);
  margin-top: -10px;

  .exam-select-question-box {

    background: #fff;
    border-bottom: 1px solid #fff;
    border-radius: 2px;
    width: 100%;

    .exam-add-question {
      font-size: 14px;
      float: right;
      margin: 10px 20px 5px 0;
    }

    .question-select-list {
      margin: 0 0 20px 0;
      height: 200px;
    }
  }

  .exam-base-info-box {
    background: #fff;
    border-bottom: 1px solid #fff;
    border-radius: 2px;
    margin-bottom: 10px;
    width: 100%;
    box-sizing: border-box;

    .exam-base-title {
      width: 100%;
      box-sizing: border-box;
      height: 52px;
      border-bottom: 1px solid #e9e9e9;
      display: flex;
      justify-content: space-between;
      align-items: center;

      .base-title {
        font-size: 16px;
        font-weight: 500;
        color: #333333;
      }

      .go-back {
        color: #999;
        cursor: pointer;
      }
    }

    .exam-base-info {
      box-sizing: border-box;
      border-bottom: 1px solid #e9e9e9;
    }

    .mesage-list-content {
      box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.1);
      background-color: rgba(255, 255, 255, 1);
      border-radius: 10px;
      width: 1200px;
      margin-top: 20px;
    }
  }

  .group-box {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: calc(100% - 64px);
    margin: 24px 0;

    .group-item {
      display: flex;
      align-items: center;
      width: 100%;

      .exam-base-info-button {
        margin-left: 104px;
        width: 420px;
      }

      .item-label {
        font-size: 14px;
        font-weight: 400;
        width: 94px;
        text-align: left;
        color: rgba(0, 0, 0, 0.85);
        position: relative;
        padding-left: 10px;

        &.required::before {
          position: absolute;
          content: '*';
          font-size: 20px;
          color: red;
          left: 0px;
          top: -2px;
        }
      }
    }
  }

  .submit-box {
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent;

    &.absolute {
      position: absolute;
      width: calc(100% - 48px);
      bottom: 0;
      background: #fff;
      z-index: 999;
    }
  }
}
</style>

<style>
.w-e-text-container {
  min-height: 142px;
}
</style>
相关推荐
lemon_sjdk4 小时前
从一段真实的 Electron IM 前端代码,看高级前端的工程化修养
前端·electron·状态模式·im
Nebula_g4 小时前
云禾外卖补充—项目整体后端运行逻辑-对流程的更深层次理解
java·开发语言·网络·数据库·状态模式·javase
Wang's Blog13 小时前
Java 项目实战: 交友项目:登录链路回顾与今日任务地图
java·开发语言·状态模式·交友
Java后端的Ai之路15 小时前
14、ReactJS - 路由(Router)
前端·react.js·状态模式
星恒随风16 小时前
Linux线程概念详解:从分页存储、页表、MMU与TLB,深入理解线程和进程的区别与联系
linux·运维·笔记·学习·状态模式
溪语流沙1 天前
【Web全栈进阶】前端对接新契约:信封解包 + 统一错误处理
前端·vue.js·python·状态模式·fastapi
积流成海1 天前
JSON 格式化:语法校验与错误定位实践
json·状态模式·盘子工具站
鸽鸽程序猿2 天前
【项目】【在线判题系统】竞赛新增
状态模式
const_qiu2 天前
第 2.5 章 2.5.6:前端登录、Token 无感续签与路由守卫
java·前端·人工智能·状态模式