微服务的编程测评系统9-竞赛新增-竞赛编辑

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • [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了

总结

相关推荐
智践行1 小时前
ROS2 Jazzy:执行器
架构
飏旎2 小时前
对于前端闭包的详细理解
前端·状态模式
null不是我干的4 小时前
微服务消息队列之RabbitMQ,深入了解
微服务·rabbitmq·java-rabbitmq
●VON4 小时前
重生之我在暑假学习微服务第七天《微服务之服务治理篇》
java·学习·微服务·云原生·nacos·架构·springcloud
贾全4 小时前
Transformer架构全解析:搭建AI的“神经网络大厦“
人工智能·神经网络·ai·语言模型·自然语言处理·架构·transformer
潘锦6 小时前
架构师必备:解决技术问题当从第一性原理开始
架构·cto
kaliarch6 小时前
IaC 管控资源发生属性偏移修正方案
后端·架构·自动化运维
数据智能老司机7 小时前
DevOps 安全与自动化——理解 DevOps 文化与原则
架构·自动化运维·devops
数据智能老司机7 小时前
DevOps 安全与自动化——开发环境搭建
架构·自动化运维·devops