提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- [1. 竞赛新增](#1. 竞赛新增)
-
- [1.1 竞赛基本信息增加-后端开发](#1.1 竞赛基本信息增加-后端开发)
- [1.2 竞赛新增题目-后端](#1.2 竞赛新增题目-后端)
- [1.3 竞赛基本信息-前端](#1.3 竞赛基本信息-前端)
- [1.4 竞赛新增题目-前端](#1.4 竞赛新增题目-前端)
- [2. 竞赛编辑](#2. 竞赛编辑)
-
- [2.1 竞赛详情-后端](#2.1 竞赛详情-后端)
- [2.2 竞赛详情-前端](#2.2 竞赛详情-前端)
- [2.3 竞赛基本信息编辑-后端](#2.3 竞赛基本信息编辑-后端)
- [2.4 竞赛基本信息编辑](#2.4 竞赛基本信息编辑)
- [2.5 题目信息编辑](#2.5 题目信息编辑)
- [2.6 竞赛题目信息编辑-前端](#2.6 竞赛题目信息编辑-前端)
- 总结
前言
1. 竞赛新增
1.1 竞赛基本信息增加-后端开发
可以添加没有题目的竞赛,后期来添加题目
但是没有题目的竞赛不能发布,可以保存,保存的题目可以在列表看到
然后是竞赛的开始时间必须在当前时间以后
然后是结束时间必须在开始时间之后,还有就是竞赛名称不要一样
java
@Data
public class ExamAddDTO {
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;
}
DTO字段加上 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
意思就是前端可以传入字符串类型的事件
VO加上 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
意思就是返回给前端的时间是字符串类型
java
EXAM_TITLE_HAVE_EXITED(3201,"竞赛标题重复"),
START_TIME_BEFORE_NOW_TIME(3202,"开始时间在当前时间之前"),
START_TIME_BEFORE_END_TIME(3203,"开始时间在结束时间之后");
java
@Override
public int add(ExamAddDTO examAddDTO) {
//校验竞赛名字是否重复
List<Exam> exams = examMapper.selectList(new LambdaQueryWrapper<Exam>().eq(Exam::getTitle, examAddDTO.getTitle()));
if(CollectionUtil.isNotEmpty(exams)){
throw new ServiceException(ResultCode.EXAM_TITLE_HAVE_EXITED);
}
if(examAddDTO.getStartTime().isBefore(LocalDateTime.now())){
throw new ServiceException(ResultCode.START_TIME_BEFORE_NOW_TIME);
}
if(examAddDTO.getEndTime().isBefore(examAddDTO.getStartTime())){
throw new ServiceException(ResultCode.START_TIME_BEFORE_END_TIME);
}
Exam exam = new Exam();
BeanUtil.copyProperties(examAddDTO,exam);
examMapper.insert(exam);
return 0;
}
1.2 竞赛新增题目-后端
新增竞赛可以新增竞赛基本信息,没有题目
也可以新增基本信息,有题目,反正必须要有基本信息
题目可以后续去增加

添加题目之前,要先保存基本信息add,先生成不包含题目的竞赛
然后再新增题目,插入表exam_question
这样就可以利用上一个生成的接口了
添加题目成功以后,可以点击提交,就添加成功了,然后可以点击暂不发布的按钮,就可以了

先选择的题目,题目顺序靠前,或者order较小
java
@Data
public class ExamQuestionAddDTO {
private Long examId;
private LinkedHashSet<Long> questionIds;
}
这里我们用LinkedHashSet来存储questionId
因为List不能去重,questionId是不能重复的
因为Set是无序的,不会按照我们前端传入的顺序来操作,所以也不用这个
java
@Override
public boolean questionAdd(ExamQuestionAddDTO examQuestionAddDTO) {
//先看竞赛Id存不存在
Exam exam = getExamById(examQuestionAddDTO.getExamId());
//然后是看题目Id是否正确
LinkedHashSet<Long> questionIds = examQuestionAddDTO.getQuestionIds();
int count =1;
for(Long questionId : questionIds){
//先查询这个ID存不存在
Question question = questionMapper.selectById(questionId);
if(question == null){
throw new ServiceException(ResultCode.QUESTION_ID_NOT_EXIST);
}
ExamQuestion examQuestion = new ExamQuestion();
examQuestion.setExamId(exam.getExamId());
examQuestion.setQuestionId(questionId);
examQuestion.setQuestionOrder(count++);
examQuestionMapper.insert(examQuestion);
}
return true;
}
但是这样写有问题
第一个问题就是会频繁的访问数据库
第二个问题就是有一个questionId不存在的时候,整个流程都应该取消,但是这个无法实现
所以我们可以使用mybatisPlus的整体id查询方法或者整体插入的方法
mybatisPlus的selectByIds方法就是根据一系列id来查询,只用访问一次数据库可就可以了
然后是整体插入,这个方法不在BaseMapper中,
整体插入是saveBatch方法,我们可以双击shift+shift,来搜索这个方法

然后是往下找实现的方法
这个是抽象类,还不行
最后找到这个实现类
方法就在这里,我们只需要继承这个类,然后就可以使用方法了
java
public class ExamServiceImpl extends ServiceImpl<ExamQuestionMapper,ExamQuestion> implements IExamService {
或者这样写也是可以的
java
public class ExamServiceImpl extends ServiceImpl<BaseMapper<ExamQuestion>,ExamQuestion> implements IExamService {

saveBatch这个方法就是对一个集合直接进行插入了
java
@Override
public boolean questionAdd(ExamQuestionAddDTO examQuestionAddDTO) {
//先看竞赛Id存不存在
Exam exam = getExamById(examQuestionAddDTO.getExamId());
//然后是看题目Id是否正确
LinkedHashSet<Long> questionIds = examQuestionAddDTO.getQuestionIds();
List<Question> questionList = questionMapper.selectByIds(questionIds);
if(CollectionUtil.isEmpty(questionList)){
return true;
}
if(questionList.size()<questionIds.size()){
throw new ServiceException(ResultCode.QUESTION_ID_NOT_EXIST);
}
return saveExamQuestionList(questionIds, exam);
}
private boolean saveExamQuestionList(LinkedHashSet<Long> questionIds, Exam exam) {
List<ExamQuestion> examQuestionList = new ArrayList<>();
int count =1;
for(Long questionId : questionIds){
//先查询这个ID存不存在
Question question = questionMapper.selectById(questionId);
if(question == null){
throw new ServiceException(ResultCode.QUESTION_ID_NOT_EXIST);
}
ExamQuestion examQuestion = new ExamQuestion();
examQuestion.setExamId(exam.getExamId());
examQuestion.setQuestionId(questionId);
examQuestion.setQuestionOrder(count++);
examQuestionList.add(examQuestion);
}
return saveBatch(examQuestionList);
}
private Exam getExamById(Long examId) {
Exam exam = examMapper.selectById(examId);
if(exam == null){
throw new ServiceException(ResultCode.EXAM_ID_NOT_EXIST);
}
return exam;
}
这样就OK了
1.3 竞赛基本信息-前端
在exam.vue中
java
function onAddExam(){
router.push("/oj/layout/updateExam")
}
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 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: ''
})
</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>
java
<span class="base-title">{{ type === 'edit' ? '编辑竞赛' : '添加竞赛' }}</span>
这个是由路由的参数决定的
java
const type = useRoute().query.type
这个可以获得路由的参数
java
function goBack(){
router.go(-1)
}
这个是返回上一级
java
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])
}
}
await examAddService(fd)
ElMessage.success('基本信息保存成功')
}
这里的时间没有转为字符串,但是会自动转为字符串的,因为Json中没有Date类型
java
export function examAddService(params = {}) {
return service({
url: "/exam/add",
method: "post",
data: params,
});
}
这样就成功了
1.4 竞赛新增题目-前端

java
<el-button class="exam-add-question" :icon="Plus" type="text" @click="addQuestion()">
添加题目
</el-button>
添加题目,会先把题目数据加载到弹出框中,然后用表格的形式展示数据
java
<el-dialog v-model="dialogVisible">
java
examMapper.insert(exam) ;
return exam.getExamId();
修改后端,保存成功以后,要返回竞赛Id
java
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])
}
}
const res = await examAddService(fd)
formExam.examId = res.data
ElMessage.success('基本信息保存成功')
}
java
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
}
}
然后就可以获取题目列表数据了,都是一样的
然后是搜索,分页那些功能
java
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()
}

这样就可以了
然后是选择题目提交了

java
<el-table-column type="selection"></el-table-column>
加上了这个就可以多选了

这个是勾选小框框的时候要触发的事件
java
<el-table :data="questionList" @select="handleRowSelect">
java
const questionIdSet = ref([])
function handleRowSelect(selection) {
questionIdSet.value = []
selection.forEach(element => {
questionIdSet.value.push(element.questionId)
});
}
selection就是多选的集合,只要多选变了,就会触发这个事件
java
<el-button class="question-select-submit" type="primary" plain
@click="submitSelectQuestion">提交</el-button>
然后是提交了
java
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('竞赛题目添加成功')
}
其实reactive分装Json和formData分装Json都是一样的格式吗,都是一样的效果
只不过以后formData还可以分装文件
这样就可以了
java
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,
});
}
但是还要注意一下,就是后端返回的Long,会截断的
所以要改一下·
java
examMapper.insert(exam) ;
return exam.getExamId().toString();
这样就OK了
还有就是这个多选框是自动区分选择顺序的
2. 竞赛编辑
2.1 竞赛详情-后端
java
@Data
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;
List<QuestionVO> questionVOList;
}
虽然需要返回的题目列表没有QuestionVO那么多属性,但是后面我们可以设置不返回的
java
@GetMapping("/detail")
public R<ExamDetailVO> detail(Long examId){
log.info("获取竞赛详细信息,examId:{}",examId);
return R.ok(iExamService.detail(examId));
}
java
@Override
public ExamDetailVO detail(Long examId) {
Exam exam = getExamById(examId);
ExamDetailVO examDetailVO = new ExamDetailVO();
BeanUtil.copyProperties(exam,examDetailVO);
//先根据examId,查出所有的题目,在查出所有的questionId
List<ExamQuestion> examQuestionList = examQuestionMapper.selectList(
new LambdaQueryWrapper<ExamQuestion>()
.select(ExamQuestion::getQuestionId)
.orderByAsc(ExamQuestion::getQuestionOrder)
.eq(ExamQuestion::getExamId, examId));
List<Long> questionIdList = examQuestionList.stream().map(ExamQuestion::getQuestionId).toList();
if(CollectionUtil.isEmpty(questionIdList)){
return examDetailVO;
}
//在查出所有的question
List<Question> questionList = questionMapper.selectList
(new LambdaQueryWrapper<Question>()
.select(Question::getQuestionId,Question::getTitle,Question::getDifficulty)
.in(Question::getQuestionId, questionIdList));
List<QuestionVO> questionVOList = BeanUtil.copyToList(questionList, QuestionVO.class);
examDetailVO.setQuestionVOList(questionVOList);
return examDetailVO;
}
java
BeanUtil.copyProperties
这个是拷贝对象的属性,直接拷贝就是了,不用管谁大谁小,就是拷贝相同名字的字段
BeanUtil.copyToList是拷贝数组,第二个参数是拷贝目的的元素类型,第一个参数是源数组
然后用select就可以指定要查出哪些数据了,像那些前端不用的createName和time就不用select了,就不用返回给前端了
然后就可以测试了
先测试没有题目的竞赛

这里我们题目列表为空,那么就不要返回list了,直接忽略掉
空的数据就不要返回给前端了
java
@Data
@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;
List<QuestionVO> examQuestionList;
}
加上注解@JsonInclude(JsonInclude.Include.NON_NULL),就表示字段为null的就不会返回给前端了
但是数组为[],还是会返回的,数组为[],与数组为null,不是一回事
这样就OK了
然后是测试有题目的竞赛
因为select中没有createname和createTime字段,所以是null,但是数据库中可不是null
然后就是因为前端不需要这两个字段,所以没有加入select,所以就不用返回给前端看了,因为是null,而且也不需要
java
@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;
}
这样就OK了
2.2 竞赛详情-前端
点击编辑按钮,进入编辑竞赛界面,然后要先展示详细信息
在exam.vue中
java
<el-button v-if="isNotStartExam(row) && row.status == 0" type="text" @click="onEdit(row.examId)">编辑
java
function onAddExam(){
router.push("/oj/layout/updateExam?type=add")
}
function onEdit(examId){
// router.push("/oj/layout/updateExam?type=edit&examId=${examId}")
router.push(`/oj/layout/updateExam?type=edit&examId=${examId}`)
}
如果要使$符号起作用的话,那么就不能用双引号,就只能用esc下面的引号
java
export function getExamDetailService(examId) {
return service({
url: "/exam/detail",
method: "get",
params: { examId },
});
}
java
const formExam = reactive({
examId: '',
title: '',
examDate: ''
})
async function getExamDetail(){
const examId = useRoute().query.examId
if(examId){
const res = await getExamDetailService(examId);
Object.assign(formExam,res.data)
formExam.examId = examId
formExam.examDate = [res.data.startTime,res.data.endTime]
}
}
getExamDetail()
前端的数据类型不是固定的,是比较浮动的,是不严格的,所以Object.assign就会把res.data的所有属性都赋值给formExam了
还有就是有一点,就是在获取竞赛列表的时候,后端没有返回竞赛id
java
@Data
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;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
private String createName;
}
记得加 @JsonSerialize(using = ToStringSerializer.class)
不然又会截断
这样就成功了
2.3 竞赛基本信息编辑-后端
有两个部分的编辑,一个是竞赛基本信息的编辑,一个题目信息的编辑,就是添加或者删除,题目本身信息是无法修改的
java
@Data
public class ExamEditDTO extends ExamAddDTO{
private Long examId;
}
java
@PutMapping("/edit")
public R<Void> edit(@RequestBody ExamEditDTO examEditDTO){
log.info("编辑题目基本信息examEditDTO:{}",examEditDTO);
return toR(iExamService.edit(examEditDTO));
}
java
@Override
public int edit(ExamEditDTO examEditDTO) {
Exam exam = getExamById(examEditDTO.getExamId());
checkExamEditOrAddDTO(examEditDTO,examEditDTO.getExamId());
exam.setTitle(examEditDTO.getTitle());
exam.setStartTime(examEditDTO.getStartTime());
exam.setEndTime(examEditDTO.getEndTime());
return examMapper.updateById(exam);
}
private void checkExamEditOrAddDTO(ExamAddDTO examInfo,Long examId){
List<Exam> exams = examMapper.selectList(new LambdaQueryWrapper<Exam>()
.eq(Exam::getTitle, examInfo.getTitle())
.ne(examId!=null,Exam::getExamId,examId)
);
if(CollectionUtil.isNotEmpty(exams)){
throw new ServiceException(ResultCode.EXAM_TITLE_HAVE_EXITED);
}
if(examInfo.getStartTime().isBefore(LocalDateTime.now())){
throw new ServiceException(ResultCode.START_TIME_BEFORE_NOW_TIME);
}
if(examInfo.getEndTime().isBefore(examInfo.getStartTime())){
throw new ServiceException(ResultCode.START_TIME_BEFORE_END_TIME);
}
}
对于add基本信息,要检查标题是否与其他竞赛重复
而对于edit的话,同样也要检查是否重复,但是检查的时候,一定要排除掉自己,如果examId不为null,说明是edit,那么就要ne来排除自己,如果examId为null,说明是add,那么就不用排除自己

这样就成功了
2.4 竞赛基本信息编辑
java
export function editExamService(params = {}) {
return service({
url: "/exam/edit",
method: "put",
data: params,
});
}
修改个人基本信息就是点击保存按钮
java
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])
}
}
fd.forEach((value,key)=>{
console.log("key:",key,"value:",value)
})
if(formExam.examId){
await editExamService(fd)
}else{
const res = await examAddService(fd)
formExam.examId = res.data
}
ElMessage.success('基本信息保存成功')
}
这样就成功了
2.5 题目信息编辑
就是添加或者删除题目
添加题目已经开发过了
然后就是开发删除功能了
后端删除题目的时候,第一要看竞赛是否存在,然后是这个竞赛有没有这个题目
前端只需要传题目id和竞赛id就可以了
还有就是在开始竞赛的时候,不能删除题目
就是开始时间在当前时间之前的时候,说明竞赛已经开始了
还有就是在竞赛开始的时候,,也不能添加题目
还有在竞赛开始的时候,也不能编辑竞赛基本信息
或者在竞赛结束的时候,也不能修改的
java
@DeleteMapping("/question/delete")
public R<Void> questionDelete(Long examId,Long questionId){
log.info("在竞赛中删除题目examId:{},questionId:{}",examId,questionId);
return toR(iExamService.questionDelete(examId,questionId));
}
java
@Override
public int questionDelete(Long examId, Long questionId) {
Exam exam = getExamById(examId);
if(LocalDateTime.now().isAfter(exam.getStartTime())){
//说明竞赛已经开始或者结束
throw new ServiceException(ResultCode.EXAM_HAVE_STARED);
}
return examQuestionMapper.delete(new LambdaQueryWrapper<ExamQuestion>()
.eq(ExamQuestion::getQuestionId, questionId)
.eq(ExamQuestion::getExamId, examId)
);
}
这样就成功了

然后还有一个问题就是
以前选过的题目,但是在添加题目那里还是会显示出来,可能会导致数据库重复添加
所以的修改一下获取题目列表的接口
就是前端可以传入已经添加的题目id,这样后端就不要返回这些id了
java
@Getter
@Setter
public class QuestionQueryDTO extends PageQueryDTO {
private String title;
private Integer difficulty;
private String excludeIdStr;
private Set<Long> excludeIdSet;
}
get请求的DTO有集合参数的话,一般会把集合里面的元素拿出来,然后弄成一个字符串,然后传给后端,多个元素的话,拼成一个字符串,那么就要有分隔符,就是一个特殊字符,比如分号
这就是get请求处理集合参数的方法
拼接的字符串,就放入excludeIdStr里面,后端处理之后,把生成的LongId放入excludeIdSet
java
public class Constants {
public static final String SPLIT_SEM = ";";
}
java
@Override
public List<QuestionVO> list(QuestionQueryDTO questionQueryDTO) {
String excludeIdStr = questionQueryDTO.getExcludeIdStr();
if(StrUtil.isNotEmpty(excludeIdStr)){
//说明是在竞赛中查询题目列表
String[] idString = excludeIdStr.split(Constants.SPLIT_SEM);
//将字符串的id变为Long
Set<Long> collect = Arrays.stream(idString).map(Long::valueOf).collect(Collectors.toSet());
questionQueryDTO.setExcludeIdSet(collect);
}
PageHelper.startPage(questionQueryDTO.getPageNum(), questionQueryDTO.getPageSize());
return questionMapper.selectQuestionList(questionQueryDTO);
}
StrUtil.isNotEmpty是专门对String类型判空
Arrays.stream(idString).map(Long::valueOf).collect(Collectors.toSet());是用流的方式来转换
java
<?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.ck.system.mapper.question.QuestionMapper">
<select id="selectQuestionList" resultType="com.ck.system.domain.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>

这样就OK了
2.6 竞赛题目信息编辑-前端
java
export function deleteExamQuestionService(examId,questionId) {
return service({
url: "/exam/question/delete",
method: "delete",
params: { examId ,questionId},
});
}
java
async function deleteExamQuestion(examId,questionId){
await deleteExamQuestionService(examId,questionId);
getExamDetail()
ElMessage.success("删除题目成功")
}
但是有一个问题,就是
getExamDetail中
Object.assign(formExam,res.data)中
如果questionList为null,会直接就不返回这个字段了
然后就不会把空的list赋值给questionList了,然后list里面的数据就是上一次只有一个的数据
所以我们在赋值之前把它弄为空
java
//获取竞赛详细信息
async function getExamDetail(){
const examId = useRoute().query.examId
console.log("examId",examId)
if(examId){
const res = await getExamDetailService(examId);
formExam.examQuestionList = []
Object.assign(formExam,res.data)
console.log("formExam",formExam)
formExam.examId = examId
formExam.examDate = [res.data.startTime,res.data.endTime]
console.log("formExam",formExam)
}
}
注意不能直接在deleteExamQuestion或者addExamQuestion中调用getExamDetail方法
因为
useRoute() 是 Vue Router 提供的组合式 API,只能在 Vue 组件的 setup() 函数或
所以useRoute()只能在js里面使用,或者在函数里面使用,但是这个函数必须是在js里面会马上调用的,所以我们弄一个新的方法
就可以了
java
async function getExamDetailByExamId(examId){
const res = await getExamDetailService(examId);
formExam.examQuestionList = []
Object.assign(formExam,res.data)
formExam.examId = examId
formExam.examDate = [res.data.startTime,res.data.endTime]
}
java
async function deleteExamQuestion(examId,questionId){
await deleteExamQuestionService(examId,questionId);
getExamDetailByExamId(examId)
ElMessage.success("删除题目成功")
}
java
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("questionIdSet",questionIdSet.value)
console.log(examQ)
await addExamQuestionService(examQ);
dialogVisible.value = false
getExamDetailByExamId(formExam.examId)
ElMessage.success('竞赛题目添加成功')
}
这样就OK了