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

用 10% GPU 跑通万亿参数 RL!马骁腾拆解万亿参数大模型的后训练实战

用 10% GPU 跑通万亿参数 RL!马骁腾拆解万亿参数大模型的后训练实战

整理 | 梦依丹 出品 | ZEEKLOG(ID:ZEEKLOGnews) 左手是提示词的工程化约束,右手是 Context Learning 的自我进化。 在 OpenAI 新发布的《Prompt guidance for GPT-5.4》中,反复提到了 Prompt Contracts(提示词合约)。要求开发者像编写代码一样,严谨地定义 Agent 的输入边界、输出格式与工具调用逻辑,进而换取 AI 行为的确定性。 但在现实操作中,谁又能日复一日地去维护那些冗长、脆弱的“提示词代码”? 真正的 Agent,不应只靠阅读 Context Engineering,更应该具备 Context Learning 的能力。 为此,在 4 月 17-18

By Ne0inhk
当OpenClaw引爆全网,谁来解决企业AI Agent的“落地焦虑”?

当OpenClaw引爆全网,谁来解决企业AI Agent的“落地焦虑”?

2026 年 3 月,开源 AI Agent 框架 OpenClaw 在 GitHub 上的星标突破28万,并一度超越 React,成为 GitHub 最受关注的软件项目之一。短时间内,开发者利用它构建了大量实验性应用:从全栈开发辅助,到自动化营销脚本,再到桌面操作自动化,AI Agent 的能力边界正在迅速被拓展。 这股热潮也带动了另一个趋势——本地部署与算力硬件需求的快速增长。越来越多开发者尝试在个人设备或企业服务器上运行 Agent 系统,以获得更高的控制权和数据安全性。 从表面上看,AI Agent 似乎正从“概念验证”走向更广泛的开发实践。但在企业环境中,情况却没有想象中乐观。当企业负责人开始追问—— “它能直接解决我的业务问题吗?” 很多演示级产品仍难以给出令人满意的答案。 如何让 Agent 真正融入企业既有系统、适配复杂业务流程,正成为大模型产业落地必须跨越的一道门槛。 与此同时,中国不同城市的产业结构差异明显:互联网、

By Ne0inhk
二手平台出现OpenClaw卸载服务,299元可上门“帮卸”;2026年春招AI人才身价暴涨:平均月薪超6万;Meta辟谣亚历山大·王离职 | 极客头条

二手平台出现OpenClaw卸载服务,299元可上门“帮卸”;2026年春招AI人才身价暴涨:平均月薪超6万;Meta辟谣亚历山大·王离职 | 极客头条

「极客头条」—— 技术人员的新闻圈! ZEEKLOG 的读者朋友们好,「极客头条」来啦,快来看今天都有哪些值得我们技术人关注的重要新闻吧。(投稿或寻求报道:[email protected]) 整理 | 苏宓 出品 | ZEEKLOG(ID:ZEEKLOGnews) 一分钟速览新闻点! * 微信员工辟谣“小龙虾可自动发红包”:不要以讹传讹 * 蚂蚁集团启动春招,超 70% 为 AI 相关岗位 * 受贿 208 万!拼多多一员工被抓 * 2026 年春招 AI 人才身价暴涨: 平均月薪超 6 万元 * 二手平台出现 OpenClaw 上门卸载服务 * 权限太高,国家互联网应急中心发布 OpenClaw 安全应用的风险提示 * 字节豆包内测 AI 电商功能:无需跳转抖音,日活用户数超

By Ne0inhk
遭“美国政府封杀”后,Anthropic正式提起诉讼!

遭“美国政府封杀”后,Anthropic正式提起诉讼!

整理 | 苏宓 出品 | ZEEKLOG(ID:ZEEKLOGnews) 据路透社报道,当地时间周一,AI 初创公司 Anthropic 正式对美国国防部及特朗普政府提起诉讼,抗议五角大楼将其列为“国家安全供应链风险”主体的决定。 Anthropic 在向美国加州北区地方法院提交的诉讼文件中表示,这一认定“史无前例且非法”,已对公司造成“不可挽回的损害”。公司希望法院撤销该决定,并指示联邦机构停止执行相关认定。 划定 AI 应用红线,双方观点不一 正如我们此前报道,这场争端的核心在于 Anthropic 为其核心 AI 模型 Claude 设定的两条技术使用红线,与美国国防部的使用需求发生根本冲突。 此前,Anthropic 曾与五角大楼签署一份价值最高可达 2 亿美元的合作合同,Claude 也成为少数被纳入美国机密网络环境进行测试的 AI 系统之一。 对此,Anthropic 一直坚持两条底线: * Claude 等技术不得被用于对美国民众的大规模国内监控;

By Ne0inhk