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.缺点
这个功能只能将公共训练的题单同步到班级内,每次都是新的,无法将原来一模一样的题单进行更新,好在做题记录都是通用的。
后面的文章再写一篇同步题库的。