目录

一、竞赛发布
发布竞赛的前提:竞赛是存在、竞赛中是存在题目的
- 在满足发布竞赛前提之后,点击发布竞赛,前端携带竞赛id向后端发起请求。
- 后端接收到请求之后,根据竞赛id判断前提是否成立。如果不成立经不成立原因发送给前端。如果成立竞赛的状态字段从0改为1,并且同步到数据库,再将修改结果发送给
- 前端接收到后端的响应之后,如果失败,展示失败原因,如果成功,跳转回列表页面,当前的状态变为已发布。C端竞赛列表中要能找到已发布的竞赛。
竞赛撤销发布的前提:竞赛存在 ,竞赛处于未开始状态竞赛撤销发布功能:
-
找到要撤销发布的竞赛,点击撤销发布按钮。前端携带者竞赛id向后端发起请求。
-
后端接收到请求之后,根据竞赛id判断前提条件是否成立。如果不成立,返回前端不成立的原因。如果成立,将竞赛状态字段再从1变成1.并且同步数据库,再将结果返回给前端。
-
前端接收到响应之后,如果失败,展示失败原因。如果成功,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>