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

微服务项目->在线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 { @Autowired private 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 mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.bite.system.mapper.exam.ExamMapper"> <select resultType="com.bite.system.model.exam.vo.ExamVO"> SELECT te.exam_id, te.title, te.start_time, te.end_time, te.create_time, ts.nick_name as create_name, te.status FROM tb_exam te left join tb_sys_user ts on te.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 BY te.create_time DESC </select> </mapper>
请求测试

前端代码开发

页面半成品代码
<template> <el-form inline="true"> <el-form-item label="创建日期"> <el-date-picker v-model="datetimeRange" 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 :data="examList"> <el-table-column prop="title" label="竞赛标题"/> <el-table-column prop="startTime" label="竞赛开始时间" /> <el-table-column prop="endTime" label="竞赛结束时间" /> <el-table-column label="是否开赛"> <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" 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" label="创建用户" /> <el-table-column prop="createTime" label="创建时间" /> <el-table-column label="操作"> <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)">删除 </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 = 1 getExamList() } function onReset() { params.pageNum = 1 params.pageSize = 10 params.title = '' params.startTime = '' params.endTime = '' datetimeRange.value.length = 0 getExamList() }

因为时间范围是一个数组的形式,但是后端需要的是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.rows total.value = result.total }

增加竞赛

一、不包含题目的竞赛

后端代码
Controller
 @RestController @RequestMapping("/exam") public class ExamController extends BaseController { @Autowired private 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和创建时间和创建人

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

 @Override public 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> <div> <!-- 竞赛信息模块 --> <div> <!-- 标题 --> <div> <span>{{ type === 'edit' ? '编辑竞赛' : '添加竞赛' }}</span> <span @click="goBack">返回</span> </div> <!-- 基本信息 --> <div> <div> <div> <div>竞赛名称</div> <div> <el-input v-model="formExam.title" placeholder="请填写竞赛名称"></el-input> </div> </div> </div> <div> <div> <div>竞赛周期</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> <div> <el-button type="primary" plain @click="saveBaseInfo">保存</el-button> </div> </div> </div> </div> <!-- 添加竞赛题目 --> <div> <el-button :icon="Plus" type="text" @click="addQuestion()"> 添加题目 </el-button> <el-table :data="formExam.examQuestionList"> <el-table-column prop="questionId" label="题目id" /> <el-table-column prop="title" :show-overflow-tooltip="true" label="题目标题" /> <el-table-column prop="difficulty" label="题目难度"> <template #default="{ row }"> <div v-if="row.difficulty === 1">简单</div> <div v-if="row.difficulty === 2">中等</div> <div v-if="row.difficulty === 3">困难</div> </template> </el-table-column> <el-table-column label="操作"> <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> <div>选择竞赛题目</div> <el-form inline="true"> <el-form-item label="题目难度"> <selector v-model="params.difficulty"></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">简单</div> <div v-if="row.difficulty === 2">中等</div> <div v-if="row.difficulty === 3">困难</div> </template> </el-table-column> </el-table> <!-- 分页区域 --> <div> <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 type="primary" plain @click="submitSelectQuestion">提交</el-button> </div> </div> </el-dialog> </div> <!-- 提交任务区域 --> <div> <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 { @Autowired private 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
 @Override public 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.rows total.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 = false ElMessage.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 

 @Override public 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.examId console.log(examId) if (examId) { formExam.examId = examId const examDetail =await getExamDetailService(examId) Object.assign(formExam, examDetail.data) formExam.examDate = [examDetail.data.startTime, examDetail.data.endTime] } }

竞赛编辑

竞赛基本信息编辑

后端代码
Controller
DTO
Service
 @Override public 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
 @Override public 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.examId console.log(examId) if (examId) { formExam.examId = examId getExamDetailById(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 = false getExamDetailById(formExam.examId) ElMessage.success('竞赛题目添加成功') }

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

已;作为分隔符

@Override public 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 mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.bite.system.mapper.question.QuestionMapper"> <select resultType="com.bite.system.model.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>

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

然后修改前端代码

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

竞赛删除

后端代码

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

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

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

竞赛发布与撤销发布

后端代码

 @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)); }
 @Override public 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); } @Override public 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端竞赛管理结束

Read more

【大模型实战篇】基于Claude MCP协议的智能体落地示例

【大模型实战篇】基于Claude MCP协议的智能体落地示例

1. 背景         之前我们在《MCP(Model Context Protocol) 大模型智能体第一个开源标准协议》一文中,介绍了MCP的概念,虽然了解了其概念、架构、解决的问题,但还缺少具体的示例,来帮助进一步理解整套MCP框架如何落地。         今天我们基于claude的官方例子--获取天气预报【1】,来理解MCP落地的整条链路。 2. MCP示例         该案例是构建一个简单的MCP天气预报服务器,并将其连接到主机,即Claude for Desktop。从基本设置开始,然后逐步发展到更复杂的使用场景。         大模型虽然能力非常强,但其弊端就是内容是过时的,这里的过时不是说内容很旧,只是表达内容具有非实时性。比如没有获取天气预报和严重天气警报的能力。因此我们将使用MCP来解决这一问题。         构建一个服务器,该服务器提供两个工具:获取警报(get-alerts)和获取预报(get-forecast)。然后,将该服务器连接到MCP主机(在本例中为Claude for Desktop)。         首先我们配置下环

By Ne0inhk
AI Agent新范式:FastGPT+MCP协议实现工具增强型智能体构建

AI Agent新范式:FastGPT+MCP协议实现工具增强型智能体构建

AI Agent新范式:FastGPT+MCP协议实现工具增强型智能体构建 作者:高瑞冬 本文目录 * AI Agent新范式:FastGPT+MCP协议实现工具增强型智能体构建 * 一、MCP协议简介 * 二、创建MCP工具集 * 1. 获取MCP服务地址 * 2. 在FastGPT中创建MCP工具集 * 三、测试MCP工具 * 四、AI模型调用MCP工具 * 1. 调用单个工具 * 2. 调用整个工具集 * 五、私有化部署支持 * 1. 环境准备 * 2. 修改docker-compose.yml文件 * 3. 修改FastGPT配置 * 4. 重启服务 * 六、使用MCP-Proxy集成多个MCP服务 * 1. MCP-Proxy简介 * 2. 安装MCP-Proxy * 3. 配置MCP-Proxy * 4. 将MCP-Proxy与FastGPT集成 * 5. 高级配置

By Ne0inhk
基于腾讯云HAI + DeepSeek快速设计自己的个人网页

基于腾讯云HAI + DeepSeek快速设计自己的个人网页

前言:通过结合腾讯云HAI 强大的云端运算能力与DeepSeek先进的 AI技术,本文介绍高效、便捷且低成本的设计一个自己的个人网页。你将了解到如何轻松绕过常见的技术阻碍,在腾讯云HAI平台上快速部署DeepSeek模型,仅需简单几步,就能获取一个包含个人简介、技能特长、项目经历及联系方式等核心板块的响应式网页。 目录 一、DeepSeek模型部署在腾讯云HAI 二、设计个人网页 一、DeepSeek模型部署在腾讯云HAI 把 DeepSeek 模型部署于腾讯云 HAI,用户便能避开官网访问限制,直接依托腾讯云 HAI 的超强算力运行 DeepSeek-R1 等模型。这一举措不仅降低了技术门槛,还缩短了部署时间,削减了成本。尤为关键的是,凭借 HAI 平台灵活且可扩展的特性,用户能够依据自身特定需求定制专属解决方案,进而更出色地适配特定业务场景,满足各类技术要求 。 点击访问腾讯云HAI控制台地址: 算力管理 - 高性能应用服务 - 控制台 腾讯云高性能应用服务HAI已支持DeepSeek-R1模型预装环境和CPU算力,只需简单的几步就能调用DeepSeek - R1

By Ne0inhk
AI革命先锋:DeepSeek与蓝耘通义万相2.1的无缝融合引领行业智能化变革

AI革命先锋:DeepSeek与蓝耘通义万相2.1的无缝融合引领行业智能化变革

云边有个稻草人-ZEEKLOG博客 目录 引言 一、什么是DeepSeek? 1.1 DeepSeek平台概述 1.2 DeepSeek的核心功能与技术 二、蓝耘通义万相2.1概述 2.1 蓝耘科技简介 2.2 蓝耘通义万相2.1的功能与优势 1. 全链条智能化解决方案 2. 强大的数据处理能力 3. 高效的模型训练与优化 4. 自动化推理与部署 5. 行业专用解决方案 三、蓝耘通义万相2.1与DeepSeek的对比分析 3.1 核心区别 3.2 结合使用的优势 四、蓝耘注册流程 五、DeepSeek与蓝耘通义万相2.1的集成应用 5.1 集成应用场景 1. 智能医疗诊断

By Ne0inhk