当前位置: 首页 > news >正文

微服务项目->在线oj系统(Java-Spring)--竞赛管理

表结构创建

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:已发布',
-- exam_question 这个竞赛下所有的题目都存进来并且用&分隔开 10
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 {@Autowiredprivate IExamService examService;//exam/list@GetMapping("/list")public TableDataInfo list(ExamQueryDTO examQueryDTO) {return getDataTable(examService.list(examQueryDTO));}
}
前端传入参数(DTO)

因为需要查询,所以需要传入查询条件(标题,开始时间,结束时间)

因为是分页查询,所以需要继承之前写的pageDomain

返回值类型:TableDataInfo和之前的题库列表一样

Service

首先是之前使用的分页插件的使用,然后就是调用mapper进行查询

前端返回值(VO)

注解的作用:

它会将 Long 类型的 examId 先转换为字符串,然后再进行序列化。

(由于雪花算法产生的值会超过Long的范围)

在 JavaScript 等语言中,JavaScript 的 Number 类型在处理非常大的整数时,可能会出现精度丢失的情况 。将 Java 中的 Long 类型(尤其是比较大的 Long 值)序列化为字符串,可以避免在前端处理时出现的精度问题。

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 是 Jackson 库中的注解,用于指定 LocalDateTime 类型的 endTime 字段在序列化为 JSON 时的日期时间格式,这里设置为 “年 - 月 - 日 时:分: 秒” 的格式,能让日期时间数据在 JSON 传输时按照指定格式呈现,方便前后端对日期时间格式的统一处理。

因为我们数据库中的数据为int类型,但是我们可以通过多表查询,将管理员匿称返回给前端,所以用String类型

Mapper

由于我们这里查询条件比较繁琐,所以我们这里使用xml方式来解决

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bite.system.mapper.exam.ExamMapper"><select id="selectExamList" resultType="com.bite.system.model.exam.vo.ExamVO">SELECTte.exam_id,te.title,te.start_time,te.end_time,te.create_time,ts.nick_name as create_name,te.statusFROMtb_exam teleft jointb_sys_user tsonte.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 BYte.create_time DESC</select>
</mapper>
请求测试

前端代码开发

页面半成品代码
<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'
function isNotStartExam(exam) {const now = new Date(); //当前时间return new Date(exam.startTime) > now
}
</script>

<el-date-picker> 是日期时间范围选择器组件:

这是一个判断,如果已经开始则为已开赛,否则未开赛

根据status的值不同去展示是否发布

如果已经开赛并且没到开始时间可以撤销发布,否则已经开赛不允许修改

分页功能

查询重置功能
function onSearch() {params.pageNum = 1getExamList()
}function onReset() {params.pageNum = 1params.pageSize = 10params.title = ''params.startTime = ''params.endTime = ''datetimeRange.value.length = 0getExamList()
}

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

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.rowstotal.value = result.total
}

增加竞赛

一、不包含题目的竞赛

后端代码
Controller

@RestController
@RequestMapping("/exam")
public class ExamController extends BaseController {@Autowiredprivate IExamService examService;//exam/list@GetMapping("/list")public TableDataInfo list(ExamQueryDTO examQueryDTO) {return getDataTable(examService.list(examQueryDTO));}@PostMapping("/add")public R<String> add(@RequestBody ExamAddDTO examAddDTO) {return R.ok(examService.add(examAddDTO));}
}
DTO

Service

首先我们需要判断竞赛标题是否重复,竞赛开始时间和结束时间判断

然后将DTO中内容复制给Exam类

原因:Exam类继承了BeanEntity类以及使用了雪花算法,可以提高id和创建时间和创建人

由于判断会被多次使用,所以我们将其提出为一个方法

 @Overridepublic 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) {//1、竞赛标题是否重复进行判断   2、竞赛开始、结束时间进行判断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,为后面的增加题目做准备

前端代码

模板代码

<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 } 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: ''
})
// 返回
function goBack() {router.go(-1)
}
const params = reactive({pageNum: 1,pageSize: 10,difficulty: '',title: ''
})</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>

api请求

import service from '@/utils/request'export function getExamListService(params) {return service({url: "/exam/list",method: "get",params,});
}export function examAddService(params = {}) {return service({url: "/exam/add",method: "post",data: params,});
}export function addExamQuestionService(params = {}) {return service({url: "/exam/question/add",method: "post",data: params,});
}

代码分析

基本信息

这里是我们之前学的输入框双向绑定,以及时间框

goBack()是点击想要事件,返回上一级路由

保存按键

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.添加一个题目即可存在这个竞赛中,不用害怕突然退出导致的重新添加

后端代码
 Controller
@RestController
@RequestMapping("/exam")
public class ExamController extends BaseController {@Autowiredprivate IExamService examService;//exam/list@GetMapping("/list")public TableDataInfo list(ExamQueryDTO examQueryDTO) {return getDataTable(examService.list(examQueryDTO));}@PostMapping("/add")public R<String> add(@RequestBody ExamAddDTO examAddDTO) {return R.ok(examService.add(examAddDTO));}
}
Service
 @Overridepublic 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);}
DTO
@Getter
@Setter
public class ExamQuestAddDTO {private Long examId;private LinkedHashSet<Long> questionIdSet;
}

需要传入竞赛id和题目id集合

细节分析:

首先判断这个竞赛是否存在,如果存在则返回Exam,否则抛出异常资源不存在

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

检查一下竞赛是否开启,如果开启了则不能进行添加

获得问题列表,如果没有题目,则直接返回即可

通过问题ids进行批量查找,如果有找不到的题目,则直接抛出异常,资源不存在

这个方法是批量进行插入操作,将问题批量插入

这个方法是批量进行插入操tb_exam_questionxam_question中插入数据(竞赛id,题目id,题目顺序)----》》先将数据统一存在一个列表里面,然后一起插入,但是因为mybatis-plus中没有对应的批量插入方法,所以我们继承其他类提高的savaBatch方法

第一个参数是要操作是数据库,第二个参数是数据库里面参数的类型

前端代码

在点击添加题目后会弹出这样的一个弹框

我们仔细一看,可以去Element*中查找可得

就是将我们之前的题目列表在一个弹框中展示

下面实现添加点击事件

async function getQuestionList() {const result = await getQuestionListService(params)console.log(result)questionList.value = result.rowstotal.value = result.total
}
const dialogVisible = ref(false)
function addQuestion() {if (formExam.examId === null || formExam.examId === '') {ElMessage.error('请先保存竞赛基本信息')} else {getQuestionList()dialogVisible.value = true}
}

由于这里需要判断是否以及保存(examId),所以我们保存的时候需要进行赋值

多选框

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 = falseElMessage.success('竞赛题目添加成功')
}

竞赛详情

后端代码

Controller
    @GetMapping("/detail")public R<ExamDetailVO> detail(Long examId) {return R.ok(examService.detail(examId));}
VO

我们这里需要竞赛标题,竞赛的开始时间和结束时间,以及问题列表(由于只需要问题id,难度,标题)所以使用QuestionVO

@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

我们首先通过examId来获得竞赛,然后将竞赛内容复制给返回值

然后通过竞赛id进行查询竞赛题目的题目id并根据order进行排序

之后通过题目id进行查询问题列表,将查询到的列表复制给questionVOList来进行返回值处理

最后把questionVOList赋值给examDetailVO 

 @Overridepublic 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进行赋值(为了不要点击保存就可以使用)获得返回值,对formExam进行赋值

注意:由于后端是分为开始时间和结束时间,而前端只有一个examDate,所以我们这里特殊处理

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

竞赛编辑

竞赛基本信息编辑

后端代码
Controller

DTO

Service
  @Overridepublic 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
  @Overridepublic 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 getExamDetail() {const examId = useRoute().query.examIdconsole.log(examId)if (examId) {formExam.examId = examIdgetExamDetailById(examId)}
}

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

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

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

已;作为分隔符

@Overridepublic 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<Long>类型的数据,最后将参数赋值给DTO进行数据库查询

这里修改之前的xml文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bite.system.mapper.question.QuestionMapper"><select id="selectQuestionList" resultType="com.bite.system.model.question.vo.QuestionVO">SELECTtq.question_id,tq.title,tq.difficulty,ts.nick_name as create_name,tq.create_timeFROMtb_question tqleft jointb_sys_user tsontq.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 BYcreate_time DESC</select>
</mapper>

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

然后修改前端代码

我们进行查询前先查找已经选择的题目id,然后进行查找

竞赛删除

后端代码

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

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

  @Overridepublic 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 = 1getExamList()
}

竞赛发布与撤销发布

后端代码

 @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));}
 @Overridepublic int publish(Long examId) {Exam exam = getExam(examId);if (exam.getEndTime().isBefore(LocalDateTime.now())) {throw new ServiceException(ResultCode.EXAM_IS_FINISH);}//select count(0) from tb_exam_question where exam_id = #{examId}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);}@Overridepublic 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);}

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

前端代码

Exam.vue

import{ publishExamService,cancelPublishExamService} from '../apis/exam'
async function publishExam(examId) {await publishExamService(examId)getExamList()
}async function cancelPublishExam(examId) {await cancelPublishExamService(examId)getExamList()
}

update.vue

我们在新增竞赛的时候发现,当我们新增页面或者之前没有选择题目的竞赛的时候,我们点击新增题目会报错,这是因为examQuestionList为null

至此我们B端竞赛管理结束

http://www.dtcms.com/a/438038.html

相关文章:

  • 苏州市吴江太湖新城建设局网站网站模版建设教程
  • 【AI Design】如何利用 Paraflow 从创意到产品设计规范
  • 360免费建站网址是什么深圳网站推广哪家好
  • 【Linux系列】并发世界的基石:透彻理解 Linux 进程 — 进程概念
  • Spring AI alibaba 工具调用
  • 机器学习基础入门(第三篇):监督学习详解与经典算法
  • 做产品的淘宝客网站网站建设的素材处理方式
  • 【专业词典】FAST
  • 诸城网站建设wordpress退出维护
  • 预约记录自动关联功能测试
  • 进程“悄悄话”函数——`socketpair`
  • QT肝8天14--编辑用户
  • Redis Zset的底层秘密:跳表(Skip List)的精妙设计
  • 广州金融网站建设2017网站开发语言排名
  • C++ priority_queue优先级队列
  • Kafka 授权与 ACL 深入实践
  • 西宁市住房和城乡建设局网站做一个个人网站
  • 瑞安做网站多少钱东莞网站建设找谁
  • 谷歌云+Apache Airflow,数据处理自动化的强力武器
  • 小红书自动化运营:智能体+RPA自动化+MCP实现采集仿写和自动发布
  • 网站域名和网站网址建筑培训网 江苏
  • 定制开发开源AI智能名片S2B2C商城小程序的会员制运营研究——以“老铁用户”培养为核心目标
  • 【aigc】chrome-devtools-mcp怎么玩?
  • 从《Life of A Pixel》来看Chrome的渲染机制
  • 【项目实战 Day9】springboot + vue 苍穹外卖系统(用户端订单模块 + 商家端订单管理模块 完结)
  • Mac 安装Neo4j教程
  • blender 解决shift快捷键和中英切换重复的问题
  • 网站动态图怎么做阳明拍卖公司网站
  • 01_Docker 部署 Ollama 模型(支持 NVIDIA GPU)
  • 苏州新区网站制作wordpress视频格式