0.前言
HOJ默认是没有同步班级题单和同步班级题库的功能的,有时候用起来觉得不方便,于是就增加一个。
先来看实现的效果:
1.修改方式
一共三个步骤:
第一步:复制公共题单信息,插入到training表。
第二步:插入mapping_training_category表数据。
第三步:在training_problem表中插入数据。
原作者已经有题单插入的代码了,我们参考一下,主要使用它生成的id,修改一下原方法,让它可以返回id。
@Transactional(rollbackFor = Exception.class) public Long addTraining(TrainingDTO trainingDto) throws StatusForbiddenException, StatusNotFoundException, StatusFailException { trainingValidator.validateTraining(trainingDto.getTraining()); AccountProfile userRolesVo = (AccountProfile) SecurityUtils.getSubject().getPrincipal(); boolean isRoot = SecurityUtils.getSubject().hasRole("root"); Long gid = trainingDto.getTraining().getGid(); if (gid == null){ throw new StatusForbiddenException("添加失败,训练所属的团队ID不可为空!"); } Group group = groupEntityService.getById(gid); if (group == null || group.getStatus() == 1 && !isRoot) { throw new StatusNotFoundException("添加训练失败,该团队不存在或已被封禁!"); } if (!isRoot && !groupValidator.isGroupAdmin(userRolesVo.getUid(), gid)) { throw new StatusForbiddenException("对不起,您无权限操作!"); } trainingDto.getTraining().setIsGroup(true); Training training = trainingDto.getTraining(); trainingEntityService.save(training); TrainingCategory trainingCategory = trainingDto.getTrainingCategory(); if (trainingCategory.getGid() != null && !Objects.equals(trainingCategory.getGid(), gid)) { throw new StatusForbiddenException("对不起,您无权限操作!"); } if (trainingCategory.getId() == null) { try { trainingCategory.setGid(gid); trainingCategoryEntityService.save(trainingCategory); } catch (Exception ignored) { QueryWrapper<TrainingCategory> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("name", trainingCategory.getName()); trainingCategory = trainingCategoryEntityService.getOne(queryWrapper, false); } } boolean isOk = mappingTrainingCategoryEntityService.save(new MappingTrainingCategory() .setTid(training.getId()) .setCid(trainingCategory.getId())); if (!isOk) { throw new StatusFailException("添加失败!"); } return training.getId(); }
传入的参数有两个,第一个就是班级/团队的id。
前端设计的一个子组件:
// 将公共题单的某一些题单同步到某个班级的题单中 <template> <div> <el-card> <el-button icon="el-icon-attract" size="mini" @click.native="syncProblem()" type="primary" > {{$t('m.Sync_Train')}} </el-button> <vxe-table border="inner" :data="trainingList" ref="xTable" align="center" @checkbox-change="handleSelectionChange" @checkbox-all="handlechangeAll" > <vxe-table-column type="checkbox" width="60"></vxe-table-column> <vxe-table-column field="title" :title="$t('m.Title')" align="center" ></vxe-table-column> <vxe-table-column field="categoryName" :title="$t('m.Category')" align="center" ></vxe-table-column> <vxe-table-column field="problemCount" :title="$t('m.Problem_Number')" align="center" ></vxe-table-column> </vxe-table> <Pagination :total="total" :page-size="limit" @on-change="currentChange" :current.sync="currentPage" @on-page-size-change="onPageSizeChange" :layout="'prev, pager, next, sizes'" ></Pagination> </el-card> </div> </template> <script> import api from "@/common/api"; import Pagination from "@/components/oj/common/Pagination"; import mMessage from "@/common/message"; export default { name: "SyncGroupList", components: { Pagination, mMessage, }, props: { // 班级/团队id groupID: { type: Number, default: null, }, }, data() { return { loading: false, //默认查询,权限只能是公开的 query: { keyword: "", categoryId: null, auth: "Public", }, total: 0, currentPage: 1, limit: 10, trainingList: [], //训练列表 selectedTrain:[], //选中的 }; }, methods: { init() { console.log("sync init"); this.getTrainingList(); }, // 获取所有题单列表 getTrainingList() { console.log("getTrainingList"); console.log(this.groupID); this.loading = true; api.getTrainingList(this.currentPage, this.limit, this.query).then( (res) => { console.log(res); this.trainingList = res.data.data.records; this.total = res.data.data.total; this.loading = false; }, (err) => { this.loading = false; } ); }, currentChange(page) { this.currentPage = page; this.getTrainingList(); }, onPageSizeChange(pageSize) { this.limit = pageSize; this.getTrainingList(); }, handleSelectionChange({ records }){ this.selectedTrain = []; for (let num = 0; num < records.length; num++) { this.selectedTrain.push(records[num].id); } }, //全选 handlechangeAll(){ let trainList = this.$refs.xTable.getCheckboxRecords(); this.selectedTrain = []; for (let num = 0; num < trainList.length; num++) { this.selectedTrain.push(trainList[num].id); } }, //选中同步题目 syncProblem(){ // console.log(this.selectedTrain) if(this.selectedTrain.length==0){ mMessage.warning("没有选中任何题单"); }else{ let params={ groupID:this.groupID, //要同步到哪个班级 selectedTrain:this.selectedTrain //同步哪些题单 } console.log(params) api.syncPublicTrainToGroupTrain(params).then( (res) => { console.log(res); if(res.data.msg=="success"){ mMessage.success(this.$i18n.t('m.Add_Success')); } this.syncGroupListClose() }, (err) => { mMessage.error(this.$i18n.t('m.Sync_Error')) } ); } }, syncGroupListClose(){ console.log("syncGroupListClose") this.$emit('syncGroupListClose'); } }, }; </script>
然后在父组件中引用:
<template> <el-card> <div class="filter-row"> <el-row> <el-col :md="3" :xs="24"> <span class="title">{{ $t("m.Group_Training") }}</span> </el-col> <!-- 创建团队新题单 --> <el-col :md="18" :xs="24" v-if=" (isSuperAdmin || isGroupAdmin) && !problemPage && !editProblemPage " > <el-button v-if="!editPage" :type="createPage ? 'warning' : 'primary'" size="small" @click="handleCreatePage" :icon="createPage ? 'el-icon-back' : 'el-icon-plus'" >{{ createPage ? $t("m.Back_To_Admin_Training_List") : $t("m.Create") }}</el-button > <!-- 新增主题库同步到班级题库 --> <el-button v-if="!editPage && !createPage" :type="createPage ? 'warning' : 'primary'" size="small" @click="handleTrainingToGroup" :icon="createPage ? 'el-icon-back' : 'el-icon-plus'" >{{ $t("m.Training_To_Group") }}</el-button > <el-button v-if="editPage && adminPage" type="warning" size="small" @click="handleEditPage" icon="el-icon-back" >{{ $t("m.Back_To_Admin_Training_List") }}</el-button > <el-button :type="adminPage ? 'danger' : 'success'" v-if="!editPage && !createPage" size="small" @click="handleAdminPage" :icon="adminPage ? 'el-icon-back' : 'el-icon-s-opportunity'" >{{ adminPage ? $t("m.Back_To_Training_List") : $t("m.Training_Admin") }}</el-button > </el-col> <el-col :md="18" :xs="24" v-else-if=" (isSuperAdmin || isGroupAdmin) && problemPage && !editProblemPage " > <el-button type="primary" size="small" @click="publicPage = true" icon="el-icon-plus" >{{ $t("m.Add_From_Public_Problem") }}</el-button > <el-button type="success" size="small" @click="handleGroupPage" icon="el-icon-plus" >{{ $t("m.Add_From_Group_Problem") }}</el-button > <el-button type="warning" size="small" @click="handleProblemPage(null)" icon="el-icon-back" >{{ $t("m.Back_To_Admin_Training_List") }}</el-button > </el-col> <el-col :md="18" :xs="24" v-else-if="(isSuperAdmin || isGroupAdmin) && editProblemPage" > <el-button type="primary" size="small" @click="handleEditProblemPage" icon="el-icon-back" >{{ $t("m.Back_Admin_Training_Problem_List") }}</el-button >` </el-col> </el-row> </div> <template v-if="!adminPage && !createPage && !problemPage"> <vxe-table border="inner" stripe auto-resize highlight-hover-row :data="trainingList" :loading="loading" align="center" @cell-click="goGroupTraining" > <vxe-table-column field="rank" :title="$t('m.Number')" min-width="60" show-overflow > </vxe-table-column> <vxe-table-column field="title" :title="$t('m.Title')" min-width="200" align="center" > </vxe-table-column> <vxe-table-column field="auth" :title="$t('m.Auth')" min-width="100" align="center" > <template v-slot="{ row }"> <el-tag :type="TRAINING_TYPE[row.auth]['color']" effect="dark"> {{ $t("m.Training_" + row.auth) }} </el-tag> </template> </vxe-table-column> <vxe-table-column field="categoryName" :title="$t('m.Category')" min-width="130" align="center" > <template v-slot="{ row }"> <el-tag size="large" :style=" 'background-color: #fff; color: ' + row.categoryColor + '; border-color: ' + row.categoryColor + ';' " >{{ row.categoryName }}</el-tag > </template> </vxe-table-column> <vxe-table-column field="acCount" :title="$t('m.Progress')" min-width="120" align="center" > <template v-slot="{ row }"> <span> <el-tooltip effect="dark" :content="row.acCount + '/' + row.problemCount" placement="top" > <el-progress :text-inside="true" :stroke-width="20" :percentage="getPassingRate(row.acCount, row.problemCount)" ></el-progress> </el-tooltip> </span> </template> </vxe-table-column> <vxe-table-column field="problemCount" :title="$t('m.Problem_Number')" min-width="70" align="center" > </vxe-table-column> <vxe-table-column field="author" :title="$t('m.Author')" min-width="130" align="center" show-overflow > </vxe-table-column> <vxe-table-column field="gmtModified" :title="$t('m.Recent_Update')" min-width="96" align="center" show-overflow > <template v-slot="{ row }"> <span> <el-tooltip :content="row.gmtModified | localtime" placement="top" > <span>{{ row.gmtModified | fromNow }}</span> </el-tooltip> </span> </template> </vxe-table-column> </vxe-table> <Pagination :total="total" :page-size="limit" @on-change="currentChange" :current.sync="currentPage" @on-page-size-change="onPageSizeChange" :layout="'prev, pager, next, sizes'" ></Pagination> </template> <TrainingList ref="trainingList" v-if="adminPage && !createPage && !problemPage" @handleEditPage="handleEditPage" @currentChange="currentChange" @handleProblemPage="handleProblemPage" ></TrainingList> <TrainingProblemList v-if="problemPage" :trainingId="trainingId" @currentChangeProblem="currentChangeProblem" @handleEditProblemPage="handleEditProblemPage" ref="trainingProblemList" > </TrainingProblemList> <Training v-if="createPage && !editPage && !problemPage" mode="add" :title="$t('m.Create_Training')" apiMethod="addGroupTraining" @handleCreatePage="handleCreatePage" @currentChange="currentChange" ></Training> <!-- 团队添加公共题目 --> <el-dialog :title="$t('m.Add_Training_Problem')" width="90%" :visible.sync="publicPage" :close-on-click-modal="false" > <AddPublicProblem v-if="publicPage" :trainingId="trainingId" apiMethod="getGroupTrainingProblemList" @currentChangeProblem="currentChangeProblem" ref="addPublicProblem" ></AddPublicProblem> </el-dialog> <!-- 团队添加团队题目 --> <el-dialog :title="$t('m.Add_Training_Problem')" width="350px" :visible.sync="groupPage" :close-on-click-modal="false" > <AddGroupProblem :trainingId="trainingId" @currentChangeProblem="currentChangeProblem" @handleGroupPage="handleGroupPage" ></AddGroupProblem> </el-dialog> <!-- 打开公共题单弹窗 --> <el-dialog :title="$t('m.Training_To_Group')" width="850px" :visible.sync="groupListPage" :close-on-click-modal="true" > <SyncGroupList :groupID="groupID" @syncGroupListClose="syncGroupListClose" ref="SyncGroupListChild" > </SyncGroupList> </el-dialog> </el-card> </template> <script> import { mapGetters } from "vuex"; import { TRAINING_TYPE } from "@/common/constants"; import Pagination from "@/components/oj/common/Pagination"; import TrainingList from "@/components/oj/group/TrainingList"; import Training from "@/components/oj/group/Training"; import TrainingProblemList from "@/components/oj/group/TrainingProblemList"; import AddPublicProblem from "@/components/oj/group/AddPublicProblem.vue"; import AddGroupProblem from "@/components/oj/group/AddGroupProblem.vue"; import SyncGroupList from "@/components/oj/group/SyncGroupList.vue"; import api from "@/common/api"; export default { name: "GroupTrainingList", components: { Pagination, TrainingList, Training, TrainingProblemList, AddPublicProblem, AddGroupProblem, SyncGroupList, }, data() { return { total: 0, currentPage: 1, limit: 10, trainingList: [], TRAINING_TYPE: {}, loading: false, adminPage: false, createPage: false, editPage: false, problemPage: false, publicPage: false, groupPage: false, groupListPage: false, //打开同步题单弹窗 editProblemPage: false, trainingId: null, groupID:"", //团队/班级id }; }, mounted() { this.TRAINING_TYPE = Object.assign({}, TRAINING_TYPE); this.init(); }, methods: { init() { this.groupID=this.$route.params.groupID this.getGroupTrainingList(); }, onPageSizeChange(pageSize) { this.limit = pageSize; this.init(); }, currentChange(page) { this.currentPage = page; this.init(); }, currentChangeProblem() { this.$refs.trainingProblemList.currentChange(1); }, getGroupTrainingList() { this.trainingList = []; this.loading = true; api .getGroupTrainingList( this.currentPage, this.limit, this.$route.params.groupID ) .then( (res) => { this.trainingList = res.data.data.records; console.log("团队this.trainingList ", this.trainingList); this.total = res.data.data.total; this.loading = false; }, (err) => { this.loading = false; } ); }, goGroupTraining(event) { this.$router.push({ name: "GroupTrainingDetails", params: { trainingID: event.row.id, groupID: this.$route.params.groupID, }, }); }, handleCreatePage() { this.createPage = !this.createPage; }, // 公共训练同步至班级 handleTrainingToGroup() { console.log("公共训练同步至班级"); this.groupListPage = true; setTimeout(() => { this.$refs.SyncGroupListChild.getTrainingList(); }); }, //关闭弹窗 syncGroupListClose(){ this.groupListPage=false this.getGroupTrainingList() }, handleEditPage() { this.editPage = !this.editPage; this.$refs.trainingList.editPage = this.editPage; }, handleAdminPage() { this.adminPage = !this.adminPage; this.createPage = false; this.editPage = false; }, handleProblemPage(trainingId) { this.trainingId = trainingId; this.problemPage = !this.problemPage; }, handleGroupPage() { this.groupPage = !this.groupPage; }, handleEditProblemPage() { this.editProblemPage = !this.editProblemPage; this.$refs.trainingProblemList.editPage = this.editProblemPage; }, getPassingRate(ac, total) { if (!total) { return 0; } return ((ac / total) * 100).toFixed(2); }, }, computed: { ...mapGetters(["isAuthenticated", "isSuperAdmin", "isGroupAdmin"]), }, }; </script> <style scoped> .title { font-size: 20px; vertical-align: middle; float: left; } .filter-row { margin-bottom: 5px; text-align: center; } @media screen and (max-width: 768px) { .filter-row span { margin-left: 5px; margin-right: 5px; } } @media screen and (min-width: 768px) { .filter-row span { margin-left: 10px; margin-right: 10px; } } </style>
后台的实现也比较简单,原作者留下了很多现成的接口,直接使用就行。
String groupID=jsonObject.getStr("groupID"); //要同步到哪个班级 JSONArray trainingList= jsonObject.getJSONArray("selectedTrain"); //题单列表(id) //循环遍历每一个题单数据 for(Object o:trainingList){ Long tid = Long.valueOf(o.toString()); //获取当前训练 Training training = trainingEntityService.getById(tid); if (training == null) { throw new StatusNotFoundException("该训练不存在!"); } training.setGid(Long.valueOf(groupID)); //获取训练的类别 QueryWrapper<MappingTrainingCategory> mappingTrainingCategoryQueryWrapper = new QueryWrapper<>(); mappingTrainingCategoryQueryWrapper.eq("tid", tid); MappingTrainingCategory mappingTrainingCategory = mappingTrainingCategoryEntityService.getOne(mappingTrainingCategoryQueryWrapper); TrainingCategory trainingCategory = null; if (mappingTrainingCategory != null) { trainingCategory = trainingCategoryEntityService.getById(mappingTrainingCategory.getCid()); } TrainingDTO trainingDto = new TrainingDTO(); trainingDto.setTraining(training); trainingDto.setTrainingCategory(trainingCategory); Long newTrainId = groupTrainingManager.addTraining(trainingDto); //新的训练id //往新题单里插入题目 trainingProblemMapper.getOldProblemAndInsertNew(tid,newTrainId); }
大部分都是现成的,只有getOldProblemAndInsertNew这个是自己写的,也很简单:
<select id="getOldProblemAndInsertNew"> insert into training_problem(tid,pid,display_id) SELECT #{newTrainId},pid,display_id from training_problem WHERE tid=#{oldTrainId} </select>
这里的oldTrainId对应上面的tid。
3.缺点
这个功能只能将公共训练的题单同步到班级内,每次都是新的,无法将原来一模一样的题单进行更新,好在做题记录都是通用的。
后面的文章再写一篇同步题库的。