微服务项目->在线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

【AI 风向标】一文讲清:大模型的上下文窗口 200k 到底指的是什么?

【AI 风向标】一文讲清:大模型的上下文窗口 200k 到底指的是什么?

本文原创作者:姚瑞南 AI-agent 大模型运营专家,先后任职于美团、猎聘等中大厂AI训练专家和智能运营专家岗;多年人工智能行业智能产品运营及大模型落地经验,拥有AI外呼方向国家专利与PMP项目管理证书。(转载需经授权)    目录 一、先给结论 二、什么是 Token?(通俗版) 三、Token ≈ 多大文本?给你一个直觉 四、为什么不是“文件大小”? 五、200k / 1M 上下文窗口意味着什么? 六、常见支持上下文 Token 的模型(示例) 七、一个非常重要但常被忽略的点 最近经常看到宣传说: “上下文窗口突破 200k,甚至 1M” 很多人第一反应是: 👉 这是 字符数?文件大小?还是几百 MB 的文档? 答案其实很简单,但也最容易被误解。 一、先给结论

By Ne0inhk
人工智能、机器学习和深度学习,其实不是一回事

人工智能、机器学习和深度学习,其实不是一回事

一、人工智能、机器学习与深度学习的真正区别 在当今科技领域,我们经常听到人工智能、机器学习和深度学习这三个词。它们虽然相关,但含义不同。 1.1 人工智能 人工智能是计算机科学的一个分支,旨在研究如何合成与分析能够像人一样行动的计算主体。简单来说,AI 的目标是利用计算机来模拟甚至替代人类大脑的功能。 一个理想的 AI 系统通常具备以下特征:像人一样思考、像人一样行动、理性地思考与行动。 1.2 机器学习 机器学习是实现人工智能的一种途径。它的核心定义是:赋予计算机在没有被显式编程的情况下进行学习的能力。 与传统的基于规则的编程不同,机器学习不依赖程序员手写每一条逻辑指令,而是通过算法让机器从大量数据中寻找规律,从而对新的数据产生预测或判断。 1.3 深度学习 深度学习是机器学习的一种特殊方法,也称为深度神经网络。它受人类大脑结构的启发,通过设计多层的神经元网络结构,来模拟万事万物的特征表示。 1.4 三者之间的层级关系 厘清这三者的关系对于初学者至关重要。人工智能 AI是最宏大的概念,包含了所有让机器变聪明的技术。机器学习 ML是 AI

By Ne0inhk
小智AI烧录/ESP32切换目标板报错的处理 Failed to set target esp32s3:non zero exit code 2

小智AI烧录/ESP32切换目标板报错的处理 Failed to set target esp32s3:non zero exit code 2

这一篇解决这个报错,这是在烧录esp32s3等系列芯片常发生的问题,具体表现是报以上的错,或者删除build后成功切换到esp32s3了,但是此时又打不开齿轮的config界面了。 配置 原码版本是1.8.3,不建议用最新的原码,然后ESP-IDF的版本是5.5.1,串口选UART,自行选择端口COMX,初始时目标板是型号是esp32,上图是解决后的截图。 下载ESP-IDF5.5.1(完成可跳过) 先打开ESP-IDF,如果在左侧栏没找到就点击三个点。 然后点开“Advanced”,点击配置扩展 等待一会,出现这个页面选择ADVANCED 在version项选择5.5.1 然后选择安装路径(大约1.8G),点击configureTools即可,要等待很久,然后会有一个下载tools的界面,正常下载即可。 注意,在下载期间不要切换网络,断了就白下了,需要删除文件夹重新下。 解决 # 1. 打开 ESP-IDF PowerShell(不是普通PowerShell!) ctrl+shift+

By Ne0inhk