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.缺点

这个功能只能将公共训练的题单同步到班级内,每次都是新的,无法将原来一模一样的题单进行更新,好在做题记录都是通用的。

后面的文章再写一篇同步题库的。

分类: OnlineJudge