Files
martial-web/doc/schedule/schedule-complete-guide.md
宅房 5b806e29b7
Some checks failed
continuous-integration/drone/push Build is failing
fix bugs
2025-12-11 16:56:19 +08:00

60 KiB
Raw Permalink Blame History

武术赛事编排系统 - 完整技术方案

文档版本: v1.0 创建日期: 2025-12-10 文档作者: Claude Code 项目名称: 武术赛事管理系统 - 赛程编排模块


📋 目录

  1. 系统概述
  2. 架构设计
  3. 数据库设计
  4. 后端实现
  5. 前端实现
  6. 数据流转
  7. 核心功能
  8. API接口文档
  9. 关键代码解析
  10. 使用指南

1. 系统概述

1.1 功能简介

武术赛事编排系统是一个智能化的赛程编排管理工具,主要功能包括:

  • 自动编排: 根据参赛人员和项目自动生成赛程分组
  • 手动调整: 支持拖拽上下移动、分组移动、异常标记
  • 场地管理: 多场地、多时间段的赛程安排
  • 草稿保存: 支持保存编排草稿,随时恢复
  • 锁定发布: 完成编排后锁定,防止误操作
  • 数据导出: 导出赛程表格供打印使用

1.2 技术栈

前端技术栈:

  • Vue 2.x
  • Element UI
  • Axios
  • Vue Router

后端技术栈:

  • Spring Boot 2.x
  • MyBatis Plus
  • MySQL 8.0
  • Swagger 3.0

2. 架构设计

2.1 系统架构图

┌─────────────────────────────────────────────────────────────┐
│                        前端层 (Vue.js)                        │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐       │
│  │ 编排页面     │  │ 场地管理     │  │ 参赛人员管理  │       │
│  │ schedule/    │  │ venue/       │  │ participant/  │       │
│  │ index.vue    │  │ index.vue    │  │ index.vue     │       │
│  └──────────────┘  └──────────────┘  └──────────────┘       │
└─────────────────────────────────────────────────────────────┘
                            ↓ HTTP/HTTPS
┌─────────────────────────────────────────────────────────────┐
│                     后端层 (Spring Boot)                      │
│  ┌──────────────────────────────────────────────────────┐   │
│  │              Controller 控制器层                      │   │
│  │  - MartialScheduleArrangeController (编排控制器)     │   │
│  │  - MartialScheduleController (赛程控制器)            │   │
│  │  - MartialVenueController (场地控制器)               │   │
│  └──────────────────────────────────────────────────────┘   │
│                            ↓                                  │
│  ┌──────────────────────────────────────────────────────┐   │
│  │               Service 业务逻辑层                      │   │
│  │  - IMartialScheduleService (赛程服务)                │   │
│  │  - IMartialScheduleArrangeService (编排服务)         │   │
│  │  - IMartialVenueService (场地服务)                   │   │
│  └──────────────────────────────────────────────────────┘   │
│                            ↓                                  │
│  ┌──────────────────────────────────────────────────────┐   │
│  │                 Mapper 数据访问层                     │   │
│  │  - MartialScheduleMapper                              │   │
│  │  - MartialScheduleGroupMapper                         │   │
│  │  - MartialScheduleDetailMapper                        │   │
│  │  - MartialScheduleParticipantMapper                   │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                            ↓ JDBC
┌─────────────────────────────────────────────────────────────┐
│                    数据库层 (MySQL 8.0)                       │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  核心表:                                              │   │
│  │  - martial_schedule_group (分组表)                   │   │
│  │  - martial_schedule_detail (明细表)                  │   │
│  │  - martial_schedule_participant (参赛者关联表)       │   │
│  │  - martial_schedule_status (状态表)                  │   │
│  │                                                        │   │
│  │  关联表:                                              │   │
│  │  - martial_competition (赛事表)                      │   │
│  │  - martial_athlete (参赛选手表)                      │   │
│  │  - martial_venue (场地表)                            │   │
│  │  - martial_project (项目表)                          │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

2.2 模块划分

2.2.1 前端模块

src/views/martial/schedule/
├── index.vue                    # 编排主页面
└── components/
    ├── CompetitionGroupCard.vue # 竞赛分组卡片 (未实现)
    ├── VenueSelector.vue        # 场地选择器 (未实现)
    └── ExceptionDialog.vue      # 异常组对话框 (未实现)

src/api/martial/
├── activitySchedule.js          # 编排API接口
├── venue.js                     # 场地API接口
└── competition.js               # 赛事API接口

2.2.2 后端模块

org.springblade.modules.martial/
├── controller/
│   ├── MartialScheduleArrangeController.java   # 编排控制器
│   ├── MartialScheduleController.java          # 赛程控制器
│   └── MartialVenueController.java             # 场地控制器
├── service/
│   ├── IMartialScheduleService.java            # 赛程服务接口
│   ├── IMartialScheduleArrangeService.java     # 编排服务接口
│   └── impl/
│       ├── MartialScheduleServiceImpl.java     # 赛程服务实现
│       └── MartialScheduleArrangeServiceImpl.java # 编排服务实现
├── mapper/
│   ├── MartialScheduleGroupMapper.java         # 分组Mapper
│   ├── MartialScheduleDetailMapper.java        # 明细Mapper
│   └── MartialScheduleParticipantMapper.java   # 参赛者Mapper
└── pojo/
    ├── dto/
    │   ├── ScheduleResultDTO.java              # 编排结果DTO
    │   ├── CompetitionGroupDTO.java            # 竞赛分组DTO
    │   ├── ParticipantDTO.java                 # 参赛者DTO
    │   └── SaveScheduleDraftDTO.java           # 保存草稿DTO
    └── entity/
        ├── MartialScheduleGroup.java           # 分组实体
        ├── MartialScheduleDetail.java          # 明细实体
        ├── MartialScheduleParticipant.java     # 参赛者实体
        └── MartialScheduleStatus.java          # 状态实体

3. 数据库设计

3.1 核心表设计

3.1.1 赛程编排分组表 (martial_schedule_group)

用途: 存储赛程的分组信息(按项目和组别划分)

CREATE TABLE `martial_schedule_group` (
  `id` bigint(0) NOT NULL COMMENT '主键ID',
  `competition_id` bigint(0) NOT NULL COMMENT '赛事ID',
  `group_name` varchar(200) NOT NULL COMMENT '分组名称(如:太极拳男组)',
  `project_id` bigint(0) NOT NULL COMMENT '项目ID',
  `project_name` varchar(100) DEFAULT NULL COMMENT '项目名称',
  `category` varchar(50) DEFAULT NULL COMMENT '组别(成年组、少年组等)',
  `project_type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '项目类型(1=个人 2=集体)',
  `display_order` int(0) NOT NULL DEFAULT 0 COMMENT '显示顺序',
  `total_participants` int(0) DEFAULT 0 COMMENT '总参赛人数',
  `total_teams` int(0) DEFAULT 0 COMMENT '总队伍数(仅集体项目)',
  `estimated_duration` int(0) DEFAULT 0 COMMENT '预计时长(分钟)',
  PRIMARY KEY (`id`),
  INDEX `idx_competition` (`competition_id`),
  INDEX `idx_project` (`project_id`)
) COMMENT '赛程编排分组表';

关键字段说明:

  • group_name: 分组的显示名称,如"太极拳-成年男子组"
  • project_type: 区分个人项目(1)和集体项目(2)
  • display_order: 控制分组的显示顺序,集体项目优先
  • total_teams: 集体项目按队伍计数个人项目此字段为0

3.1.2 赛程编排明细表 (martial_schedule_detail)

用途: 存储分组与场地、时间段的关联关系

CREATE TABLE `martial_schedule_detail` (
  `id` bigint(0) NOT NULL COMMENT '主键ID',
  `schedule_group_id` bigint(0) NOT NULL COMMENT '分组ID',
  `competition_id` bigint(0) NOT NULL COMMENT '赛事ID',
  `venue_id` bigint(0) NOT NULL COMMENT '场地ID',
  `venue_name` varchar(100) DEFAULT NULL COMMENT '场地名称',
  `schedule_date` date NOT NULL COMMENT '比赛日期',
  `time_period` varchar(20) NOT NULL COMMENT '时间段(morning/afternoon)',
  `time_slot` varchar(20) NOT NULL COMMENT '时间点(08:30/13:30)',
  `estimated_start_time` datetime DEFAULT NULL COMMENT '预计开始时间',
  `estimated_end_time` datetime DEFAULT NULL COMMENT '预计结束时间',
  `participant_count` int(0) DEFAULT 0 COMMENT '参赛人数',
  `sort_order` int(0) DEFAULT 0 COMMENT '场内顺序',
  PRIMARY KEY (`id`),
  INDEX `idx_group` (`schedule_group_id`),
  INDEX `idx_venue_time` (`venue_id`, `schedule_date`, `time_slot`)
) COMMENT '赛程编排明细表';

关键字段说明:

  • schedule_group_id: 关联到分组表
  • venue_id: 指定该分组在哪个场地比赛
  • time_slot: 时间点,如"08:30"、"13:30"
  • sort_order: 同一场地同一时间段内的顺序

3.1.3 赛程编排参赛者关联表 (martial_schedule_participant)

用途: 存储参赛者与赛程明细的关联,以及出场顺序

CREATE TABLE `martial_schedule_participant` (
  `id` bigint(0) NOT NULL COMMENT '主键ID',
  `schedule_detail_id` bigint(0) NOT NULL COMMENT '编排明细ID',
  `schedule_group_id` bigint(0) NOT NULL COMMENT '分组ID',
  `participant_id` bigint(0) NOT NULL COMMENT '参赛者ID(关联martial_athlete表)',
  `organization` varchar(200) DEFAULT NULL COMMENT '单位名称',
  `player_name` varchar(100) DEFAULT NULL COMMENT '选手姓名',
  `project_name` varchar(100) DEFAULT NULL COMMENT '项目名称',
  `category` varchar(50) DEFAULT NULL COMMENT '组别',
  `performance_order` int(0) DEFAULT 0 COMMENT '出场顺序',
  PRIMARY KEY (`id`),
  INDEX `idx_detail` (`schedule_detail_id`),
  INDEX `idx_group` (`schedule_group_id`),
  INDEX `idx_participant` (`participant_id`)
) COMMENT '赛程编排参赛者关联表';

关键字段说明:

  • participant_id: 关联到 martial_athlete 表
  • organization: 冗余存储单位名称,提高查询效率
  • performance_order: 出场顺序,前端可以调整

3.1.4 赛程编排状态表 (martial_schedule_status)

用途: 记录每个赛事的编排状态和锁定信息

CREATE TABLE `martial_schedule_status` (
  `id` bigint(0) NOT NULL COMMENT '主键ID',
  `competition_id` bigint(0) NOT NULL UNIQUE COMMENT '赛事ID(唯一)',
  `schedule_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '编排状态(0=未编排 1=编排中 2=已保存锁定)',
  `last_auto_schedule_time` datetime DEFAULT NULL COMMENT '最后自动编排时间',
  `locked_time` datetime DEFAULT NULL COMMENT '锁定时间',
  `locked_by` varchar(100) DEFAULT NULL COMMENT '锁定人',
  `total_groups` int(0) DEFAULT 0 COMMENT '总分组数',
  `total_participants` int(0) DEFAULT 0 COMMENT '总参赛人数',
  PRIMARY KEY (`id`),
  UNIQUE INDEX `uk_competition` (`competition_id`),
  INDEX `idx_schedule_status` (`schedule_status`)
) COMMENT '赛程编排状态表';

关键字段说明:

  • schedule_status: 0=未编排, 1=有草稿, 2=已锁定发布
  • locked_by: 记录谁锁定了编排
  • locked_time: 锁定时间,用于审计

3.2 表关系图

martial_competition (赛事表)
    ↓ 1:1
martial_schedule_status (状态表)
    ↓ 1:N
martial_schedule_group (分组表)
    ↓ 1:N
martial_schedule_detail (明细表)
    ↓ 1:N
martial_schedule_participant (参赛者表)
    ↓ N:1
martial_athlete (选手表)

3.3 关联表

martial_athlete (参赛选手表) - 节选

CREATE TABLE `martial_athlete` (
  `id` bigint(0) NOT NULL COMMENT '主键ID',
  `order_id` bigint(0) NOT NULL COMMENT '订单ID',
  `competition_id` bigint(0) NOT NULL COMMENT '赛事ID',
  `project_id` bigint(0) COMMENT '项目ID',
  `player_name` varchar(50) NOT NULL COMMENT '选手姓名',
  `organization` varchar(200) COMMENT '所属单位',
  `category` varchar(50) COMMENT '组别',
  `team_name` varchar(100) COMMENT '队伍名称',
  PRIMARY KEY (`id`)
) COMMENT '参赛选手表';

martial_venue (场地表) - 节选

CREATE TABLE `martial_venue` (
  `id` bigint(0) NOT NULL COMMENT '主键ID',
  `competition_id` bigint(0) NOT NULL COMMENT '赛事ID',
  `venue_name` varchar(100) NOT NULL COMMENT '场地名称',
  `capacity` int(0) COMMENT '容纳人数',
  `location` varchar(200) COMMENT '位置',
  PRIMARY KEY (`id`)
) COMMENT '场地表';

4. 后端实现

4.1 Controller 层

4.1.1 MartialScheduleArrangeController

位置: org.springblade.modules.martial.controller.MartialScheduleArrangeController

主要接口:

@RestController
@RequestMapping("/martial/schedule")
public class MartialScheduleArrangeController {

    /**
     * 获取编排结果
     * GET /api/martial/schedule/result?competitionId=1
     */
    @GetMapping("/result")
    public R<ScheduleResultDTO> getScheduleResult(@RequestParam Long competitionId);

    /**
     * 保存编排草稿
     * POST /api/martial/schedule/save-draft
     */
    @PostMapping("/save-draft")
    public R saveDraftSchedule(@RequestBody SaveScheduleDraftDTO dto);

    /**
     * 完成编排并锁定
     * POST /api/martial/schedule/save-and-lock
     */
    @PostMapping("/save-and-lock")
    public R saveAndLock(@RequestBody SaveScheduleDraftDTO dto);

    /**
     * 手动触发自动编排(测试用)
     * POST /api/martial/schedule/auto-arrange
     */
    @PostMapping("/auto-arrange")
    public R autoArrange(@RequestBody Map<String, Object> params);
}

4.2 Service 层

4.2.1 核心方法getScheduleResult

功能: 获取赛程编排结果,返回前端展示数据

实现逻辑:

@Override
public ScheduleResultDTO getScheduleResult(Long competitionId) {
    ScheduleResultDTO result = new ScheduleResultDTO();

    // 1. 使用优化的JOIN查询获取所有数据
    List<ScheduleGroupDetailVO> details = scheduleGroupMapper
        .selectScheduleGroupDetails(competitionId);

    if (details.isEmpty()) {
        // 没有数据,返回空结果
        result.setIsDraft(true);
        result.setIsCompleted(false);
        result.setCompetitionGroups(new ArrayList<>());
        return result;
    }

    // 2. 按分组ID分组数据
    Map<Long, List<ScheduleGroupDetailVO>> groupMap = details.stream()
        .collect(Collectors.groupingBy(ScheduleGroupDetailVO::getGroupId));

    // 3. 检查编排状态
    boolean isCompleted = details.stream()
        .anyMatch(d -> "completed".equals(d.getScheduleStatus()));

    result.setIsCompleted(isCompleted);
    result.setIsDraft(!isCompleted);

    // 4. 组装数据
    List<CompetitionGroupDTO> groupDTOs = new ArrayList<>();
    for (Map.Entry<Long, List<ScheduleGroupDetailVO>> entry : groupMap.entrySet()) {
        CompetitionGroupDTO groupDTO = buildCompetitionGroupDTO(entry.getValue());
        groupDTOs.add(groupDTO);
    }

    result.setCompetitionGroups(groupDTOs);
    return result;
}

数据流程:

  1. 从数据库一次性JOIN查询所有相关数据
  2. 在内存中按分组ID进行分组
  3. 检查编排状态(草稿 or 已完成)
  4. 构建DTO对象返回给前端

4.2.2 核心方法saveDraftSchedule

功能: 保存编排草稿,支持用户调整后保存

实现逻辑:

@Override
@Transactional
public boolean saveDraftSchedule(SaveScheduleDraftDTO dto) {
    Long competitionId = dto.getCompetitionId();

    // 1. 更新或插入状态表
    MartialScheduleStatus status = getOrCreateStatus(competitionId);
    status.setScheduleStatus(1); // 1 = 草稿状态
    updateScheduleStatus(status);

    // 2. 删除旧的编排数据(如果存在)
    deleteOldScheduleData(competitionId);

    // 3. 保存新的编排数据
    List<CompetitionGroupDTO> groups = dto.getCompetitionGroups();
    for (CompetitionGroupDTO group : groups) {
        // 保存分组
        MartialScheduleGroup scheduleGroup = convertToEntity(group);
        scheduleGroupMapper.insert(scheduleGroup);

        // 保存明细
        MartialScheduleDetail detail = buildDetail(group, scheduleGroup.getId());
        scheduleDetailMapper.insert(detail);

        // 保存参赛者
        for (ParticipantDTO participant : group.getParticipants()) {
            MartialScheduleParticipant sp = buildParticipant(
                participant, detail.getId(), scheduleGroup.getId()
            );
            scheduleParticipantMapper.insert(sp);
        }
    }

    return true;
}

4.3 Mapper 层

4.3.1 关键SQL查询

位置: MartialScheduleGroupMapper.xml

<select id="selectScheduleGroupDetails" resultType="ScheduleGroupDetailVO">
    SELECT
        sg.id AS group_id,
        sg.group_name,
        sg.category,
        sg.project_type,
        sg.total_participants,
        sg.total_teams,
        sg.display_order,

        sd.id AS detail_id,
        sd.venue_id,
        sd.venue_name,
        sd.time_slot,
        sd.schedule_date,

        sp.id AS participant_id,
        sp.organization,
        sp.player_name,
        sp.performance_order,
        sp.status AS check_in_status,

        ss.schedule_status
    FROM martial_schedule_group sg
    LEFT JOIN martial_schedule_detail sd ON sg.id = sd.schedule_group_id
    LEFT JOIN martial_schedule_participant sp ON sd.id = sp.schedule_detail_id
    LEFT JOIN martial_schedule_status ss ON sg.competition_id = ss.competition_id
    WHERE sg.competition_id = #{competitionId}
      AND sg.is_deleted = 0
    ORDER BY sg.display_order, sp.performance_order
</select>

优化说明:

  • 使用LEFT JOIN一次性查询所有关联数据
  • 避免了N+1查询问题
  • 在Service层进行内存分组提高性能

5. 前端实现

5.1 页面结构

文件位置: src/views/martial/schedule/index.vue

5.1.1 页面布局

<template>
  <div class="martial-schedule-container">
    <!-- 头部返回按钮 + 标题 + 异常组按钮 -->
    <div class="page-header">
      <div class="header-left">
        <el-button icon="el-icon-back" @click="goBack">返回</el-button>
        <h2>编排</h2>
      </div>
      <div class="header-right">
        <el-button type="danger" @click="showExceptionDialog">
          异常组 <el-badge :value="exceptionList.length" />
        </el-button>
      </div>
    </div>

    <!-- Tab切换竞赛分组 / 场地 -->
    <div class="tabs-section">
      <el-button :type="activeTab === 'competition' ? 'primary' : ''"
                 @click="activeTab = 'competition'">
        竞赛分组
      </el-button>
      <el-button :type="activeTab === 'venue' ? 'primary' : ''"
                 @click="activeTab = 'venue'">
        场地
      </el-button>
    </div>

    <!-- 竞赛分组Tab -->
    <div v-show="activeTab === 'competition'">
      <!-- 场地选择器 -->
      <div class="venue-list">
        <el-button v-for="venue in venues"
                   :key="venue.id"
                   :type="selectedVenueId === venue.id ? 'primary' : ''"
                   @click="selectedVenueId = venue.id">
          {{ venue.venueName }}
        </el-button>
      </div>

      <!-- 时间段选择器 -->
      <div class="time-selector">
        <el-button v-for="(time, index) in timeSlots"
                   :key="index"
                   :type="selectedTime === index ? 'primary' : ''"
                   @click="selectedTime = index">
          {{ time }}
        </el-button>
      </div>

      <!-- 竞赛分组列表 -->
      <div v-for="group in filteredCompetitionGroups" :key="group.id">
        <div class="group-header">
          <div class="group-info">
            <span>{{ group.title }}</span>
            <span>{{ group.type }}</span>
            <span>{{ group.count }}</span>
          </div>
          <el-button @click="handleMoveGroup(group)">移动</el-button>
        </div>

        <!-- 参赛人员表格 -->
        <el-table :data="group.items">
          <el-table-column label="序号" type="index" />
          <el-table-column prop="schoolUnit" label="学校/单位" />
          <el-table-column prop="status" label="状态">
            <template #default="scope">
              <el-tag :type="getStatusType(scope.row.status)">
                {{ scope.row.status || '未签到' }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="操作">
            <template #default="scope">
              <el-button @click="handleMoveUp(group, scope.$index)">
                上移
              </el-button>
              <el-button @click="handleMoveDown(group, scope.$index)">
                下移
              </el-button>
              <el-button @click="markAsException(group, scope.$index)">
                异常
              </el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
    </div>

    <!-- 底部操作按钮 -->
    <div class="footer-actions">
      <el-button @click="handleSaveDraft" v-if="!isScheduleCompleted">
        保存草稿
      </el-button>
      <el-button type="primary" @click="handleConfirm" v-if="!isScheduleCompleted">
        完成编排
      </el-button>
      <el-button @click="handleExport" v-if="isScheduleCompleted">
        导出
      </el-button>
    </div>
  </div>
</template>

5.2 核心数据结构

export default {
  data() {
    return {
      // 基础信息
      competitionId: null,        // 赛事ID
      orderId: null,              // 订单ID

      // UI状态
      activeTab: 'competition',   // 当前Tab
      selectedTime: 0,            // 选中的时间段索引
      selectedVenueId: null,      // 选中的场地ID
      isScheduleCompleted: false, // 是否已完成编排
      loading: false,             // 加载状态

      // 场地和时间
      venues: [],                 // 场地列表
      timeSlots: [],              // 时间段列表

      // 编排数据
      competitionGroups: [],      // 所有竞赛分组
      exceptionList: [],          // 异常组列表

      // 赛事信息
      competitionInfo: {
        competitionName: '',
        competitionStartTime: '',
        competitionEndTime: ''
      }
    }
  },

  computed: {
    // 根据选中的场地和时间段过滤分组
    filteredCompetitionGroups() {
      if (!this.selectedVenueId || this.selectedTime === null) {
        return []
      }
      return this.competitionGroups.filter(group => {
        return group.venueId === this.selectedVenueId &&
               group.timeSlotIndex === this.selectedTime
      })
    }
  }
}

5.3 核心方法

5.3.1 加载编排数据

async loadScheduleData() {
  try {
    this.loading = true
    const res = await getScheduleResult(this.competitionId)
    const data = res.data?.data

    if (data) {
      this.isScheduleCompleted = data.isCompleted || false

      // 加载竞赛分组数据
      if (data.competitionGroups && data.competitionGroups.length > 0) {
        this.competitionGroups = data.competitionGroups.map(group => ({
          id: group.id,
          title: group.title,
          type: group.type,
          count: group.count,
          code: group.code,
          venueId: group.venueId,
          venueName: group.venueName,
          timeSlot: group.timeSlot,
          timeSlotIndex: group.timeSlotIndex,
          items: (group.participants || []).map(p => ({
            id: p.id,
            schoolUnit: p.schoolUnit,
            status: p.status || '未签到',
            sortOrder: p.sortOrder
          }))
        }))

        // 加载异常组数据
        this.loadExceptionList()

        this.$message.success(data.isDraft ? '已加载草稿数据' : '已加载编排数据')
      } else {
        this.competitionGroups = []
      }
    }
  } catch (err) {
    console.error('加载编排数据失败', err)
    this.$message.error('加载编排数据失败')
  } finally {
    this.loading = false
  }
}

5.3.2 保存草稿

async handleSaveDraft() {
  try {
    this.loading = true

    // 构建保存数据
    const saveData = {
      competitionId: this.competitionId,
      isDraft: true,
      competitionGroups: this.competitionGroups.map(group => ({
        id: group.id,
        title: group.title,
        type: group.type,
        count: group.count,
        code: group.code,
        venueId: group.venueId,
        venueName: group.venueName,
        timeSlot: group.timeSlot,
        timeSlotIndex: group.timeSlotIndex,
        participants: group.items.map((item, index) => ({
          id: item.id,
          schoolUnit: item.schoolUnit,
          status: item.status,
          sortOrder: index + 1
        }))
      }))
    }

    // 调用保存草稿接口
    await saveDraftSchedule(saveData)
    this.$message.success('草稿保存成功')
  } catch (err) {
    console.error('保存草稿失败', err)
    this.$message.error('保存草稿失败')
  } finally {
    this.loading = false
  }
}

5.3.3 上移/下移操作

handleMoveUp(group, itemIndex) {
  if (itemIndex === 0 || this.isScheduleCompleted) return

  // 交换位置
  const temp = group.items[itemIndex]
  group.items.splice(itemIndex, 1)
  group.items.splice(itemIndex - 1, 0, temp)

  this.$message.success('上移成功')
}

handleMoveDown(group, itemIndex) {
  if (itemIndex === group.items.length - 1 || this.isScheduleCompleted) return

  // 交换位置
  const temp = group.items[itemIndex]
  group.items.splice(itemIndex, 1)
  group.items.splice(itemIndex + 1, 0, temp)

  this.$message.success('下移成功')
}

5.3.4 标记异常

markAsException(group, itemIndex) {
  if (this.isScheduleCompleted) {
    this.$message.warning('编排已完成,无法标记异常')
    return
  }

  const item = group.items[itemIndex]

  // 修改状态为异常
  item.status = '异常'

  // 添加到异常组列表
  this.exceptionList.push({
    groupId: group.id,
    groupTitle: group.title,
    participantId: item.id,
    schoolUnit: item.schoolUnit,
    status: '异常'
  })

  this.$message.success(`已将 ${item.schoolUnit} 标记为异常`)
}

5.4 API调用

文件位置: src/api/martial/activitySchedule.js

import request from '@/axios'

/**
 * 获取赛程编排结果
 */
export const getScheduleResult = (competitionId) => {
  return request({
    url: '/api/martial/schedule/result',
    method: 'get',
    params: { competitionId },
    timeout: 30000
  })
}

/**
 * 保存编排草稿
 */
export const saveDraftSchedule = (data) => {
  return request({
    url: '/api/martial/schedule/save-draft',
    method: 'post',
    data
  })
}

/**
 * 保存并锁定赛程编排
 */
export const saveAndLockSchedule = (competitionId) => {
  return request({
    url: '/api/martial/schedule/save-and-lock',
    method: 'post',
    data: { competitionId }
  })
}

6. 数据流转

6.1 完整流程图

┌─────────────────────────────────────────────────────────────┐
│ 第1步用户进入编排页面                                       │
│ /schedule/index?competitionId=1&orderId=123                 │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 第2步前端mounted钩子执行                                    │
│ - loadCompetitionInfo()  加载赛事信息                        │
│ - loadVenues()           加载场地列表                        │
│ - loadScheduleData()     加载编排数据                        │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 第3步后端查询编排数据                                       │
│ GET /api/martial/schedule/result?competitionId=1            │
│                                                              │
│ MartialScheduleServiceImpl.getScheduleResult()              │
│  ├─ 查询 martial_schedule_group                             │
│  ├─ LEFT JOIN martial_schedule_detail                       │
│  ├─ LEFT JOIN martial_schedule_participant                  │
│  ├─ LEFT JOIN martial_schedule_status                       │
│  └─ 组装 ScheduleResultDTO                                  │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 第4步返回数据格式                                           │
│ {                                                            │
│   "isCompleted": false,                                      │
│   "isDraft": true,                                           │
│   "competitionGroups": [                                     │
│     {                                                        │
│       "id": 1001,                                            │
│       "title": "太极拳-成年男子组",                          │
│       "type": "个人",                                        │
│       "count": "20人",                                       │
│       "code": "TJQ-M-A",                                     │
│       "venueId": 1,                                          │
│       "venueName": "一号场地",                               │
│       "timeSlot": "2025年06月25日 上午8:30",                │
│       "timeSlotIndex": 0,                                    │
│       "participants": [                                      │
│         {                                                    │
│           "id": 1000001,                                     │
│           "schoolUnit": "北京体育大学武术学院",              │
│           "status": "未签到",                                │
│           "sortOrder": 1                                     │
│         }                                                    │
│       ]                                                      │
│     }                                                        │
│   ]                                                          │
│ }                                                            │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 第5步前端渲染                                               │
│ - 渲染场地按钮列表                                           │
│ - 渲染时间段按钮列表                                         │
│ - 根据选中的场地和时间段过滤并渲染分组                       │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 第6步用户操作                                               │
│ - 选择场地:点击场地按钮 → 更新selectedVenueId              │
│ - 选择时间:点击时间按钮 → 更新selectedTime                 │
│ - 上移/下移:调整参赛者顺序                                  │
│ - 标记异常:添加到异常组                                     │
│ - 移动分组:更改分组的场地和时间                             │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 第7步保存草稿                                               │
│ POST /api/martial/schedule/save-draft                       │
│ {                                                            │
│   "competitionId": 1,                                        │
│   "isDraft": true,                                           │
│   "competitionGroups": [...]  // 包含所有调整后的数据        │
│ }                                                            │
│                                                              │
│ MartialScheduleServiceImpl.saveDraftSchedule()              │
│  ├─ 更新 martial_schedule_status (status=1)                 │
│  ├─ 删除旧的编排数据                                         │
│  ├─ 插入新的 martial_schedule_group                         │
│  ├─ 插入新的 martial_schedule_detail                        │
│  └─ 插入新的 martial_schedule_participant                   │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 第8步完成编排可选                                       │
│ POST /api/martial/schedule/save-and-lock                    │
│ {                                                            │
│   "competitionId": 1                                         │
│ }                                                            │
│                                                              │
│ MartialScheduleServiceImpl.saveAndLockSchedule()            │
│  ├─ 更新 martial_schedule_status (status=2, locked_time)    │
│  └─ 禁止后续修改                                             │
└─────────────────────────────────────────────────────────────┘

6.2 数据库操作流程

6.2.1 查询编排数据

-- 一次性查询所有相关数据
SELECT
    sg.id AS group_id,
    sg.group_name,
    sg.category,
    sg.project_type,
    sd.venue_id,
    sd.venue_name,
    sd.time_slot,
    sp.id AS participant_id,
    sp.organization,
    sp.performance_order,
    sp.status AS check_in_status
FROM martial_schedule_group sg
LEFT JOIN martial_schedule_detail sd ON sg.id = sd.schedule_group_id
LEFT JOIN martial_schedule_participant sp ON sd.id = sp.schedule_detail_id
WHERE sg.competition_id = 1 AND sg.is_deleted = 0
ORDER BY sg.display_order, sp.performance_order

6.2.2 保存草稿数据

-- Step 1: 更新状态表
UPDATE martial_schedule_status
SET schedule_status = 1,
    last_auto_schedule_time = NOW()
WHERE competition_id = 1;

-- Step 2: 删除旧数据(级联删除)
DELETE FROM martial_schedule_participant
WHERE schedule_detail_id IN (
    SELECT id FROM martial_schedule_detail
    WHERE competition_id = 1
);

DELETE FROM martial_schedule_detail
WHERE schedule_group_id IN (
    SELECT id FROM martial_schedule_group
    WHERE competition_id = 1
);

DELETE FROM martial_schedule_group
WHERE competition_id = 1;

-- Step 3: 插入新数据
INSERT INTO martial_schedule_group (...) VALUES (...);
INSERT INTO martial_schedule_detail (...) VALUES (...);
INSERT INTO martial_schedule_participant (...) VALUES (...);

7. 核心功能

7.1 场地和时间段过滤

功能描述: 用户可以选择不同的场地和时间段,页面自动过滤显示对应的竞赛分组。

实现方式:

// 计算属性:根据选中的场地和时间段过滤
computed: {
  filteredCompetitionGroups() {
    if (!this.selectedVenueId || this.selectedTime === null) {
      return []
    }

    return this.competitionGroups.filter(group => {
      return group.venueId === this.selectedVenueId &&
             group.timeSlotIndex === this.selectedTime
    })
  }
}

// 用户点击场地按钮
<el-button @click="selectedVenueId = venue.id">
  {{ venue.venueName }}
</el-button>

// 用户点击时间按钮
<el-button @click="selectedTime = index">
  {{ time }}
</el-button>

数据存储:

  • venueId: 存储在 martial_schedule_detail 表的 venue_id 字段
  • timeSlotIndex: 根据 time_slot 字段计算得出(如"08:30" → 0, "13:30" → 1

7.2 参赛者顺序调整

功能描述: 用户可以上移或下移参赛者的出场顺序。

实现方式:

handleMoveUp(group, itemIndex) {
  // 边界检查
  if (itemIndex === 0 || this.isScheduleCompleted) return

  // 数组元素交换
  const items = group.items
  const temp = items[itemIndex]
  items.splice(itemIndex, 1)           // 删除当前位置
  items.splice(itemIndex - 1, 0, temp) // 插入到前一个位置

  this.$message.success('上移成功')
}

数据存储:

  • 保存草稿时,遍历 group.items 数组
  • 将数组索引+1作为 performance_order 字段存入数据库
  • 下次加载时按 performance_order 排序

7.3 分组移动

功能描述: 用户可以将整个竞赛分组移动到其他场地或时间段。

实现流程:

// 1. 点击"移动"按钮,打开对话框
handleMoveGroup(group) {
  this.moveGroupIndex = this.competitionGroups.findIndex(g => g.id === group.id)
  this.moveTargetVenueId = group.venueId
  this.moveTargetTimeSlot = group.timeSlotIndex
  this.moveDialogVisible = true
}

// 2. 用户选择目标场地和时间段,点击确定
confirmMoveGroup() {
  const group = this.competitionGroups[this.moveGroupIndex]
  const targetVenue = this.venues.find(v => v.id === this.moveTargetVenueId)

  // 更新分组的场地和时间信息
  group.venueId = this.moveTargetVenueId
  group.venueName = targetVenue.venueName
  group.timeSlotIndex = this.moveTargetTimeSlot
  group.timeSlot = this.timeSlots[this.moveTargetTimeSlot]

  this.$message.success(`已移动到 ${group.venueName} - ${group.timeSlot}`)
  this.moveDialogVisible = false
}

数据存储:

  • 更新 martial_schedule_detail 表的 venue_idtime_slot 字段

7.4 异常标记

功能描述: 对于未签到或有问题的参赛者,可以标记为异常,移到异常组统一管理。

实现流程:

// 1. 标记为异常
markAsException(group, itemIndex) {
  const item = group.items[itemIndex]

  // 修改状态
  item.status = '异常'

  // 添加到异常组列表
  this.exceptionList.push({
    groupId: group.id,
    groupTitle: group.title,
    participantId: item.id,
    schoolUnit: item.schoolUnit,
    status: '异常'
  })

  this.$message.success(`已将 ${item.schoolUnit} 标记为异常`)
}

// 2. 从异常组移除
removeFromException(index) {
  const exceptionItem = this.exceptionList[index]

  // 在分组中找到对应的参赛者,恢复状态
  for (let group of this.competitionGroups) {
    if (group.id === exceptionItem.groupId) {
      for (let item of group.items) {
        if (item.id === exceptionItem.participantId) {
          item.status = '未签到'
          break
        }
      }
      break
    }
  }

  // 从异常列表移除
  this.exceptionList.splice(index, 1)
}

数据存储:

  • martial_schedule_participant 表的 status 字段
  • 前端显示时根据 status 值渲染不同颜色的标签

7.5 草稿保存

功能描述: 用户调整后可以随时保存草稿,下次进入继续编辑。

实现流程:

async handleSaveDraft() {
  // 1. 构建保存数据
  const saveData = {
    competitionId: this.competitionId,
    isDraft: true,
    competitionGroups: this.competitionGroups.map(group => ({
      id: group.id,
      title: group.title,
      type: group.type,
      count: group.count,
      code: group.code,
      venueId: group.venueId,
      venueName: group.venueName,
      timeSlot: group.timeSlot,
      timeSlotIndex: group.timeSlotIndex,
      participants: group.items.map((item, index) => ({
        id: item.id,
        schoolUnit: item.schoolUnit,
        status: item.status,
        sortOrder: index + 1  // 重新计算顺序
      }))
    }))
  }

  // 2. 调用API保存
  await saveDraftSchedule(saveData)
  this.$message.success('草稿保存成功')
}

后端处理:

@Transactional
public boolean saveDraftSchedule(SaveScheduleDraftDTO dto) {
    // 1. 更新状态为"草稿"
    updateScheduleStatus(dto.getCompetitionId(), 1);

    // 2. 删除旧数据
    deleteOldScheduleData(dto.getCompetitionId());

    // 3. 保存新数据
    for (CompetitionGroupDTO group : dto.getCompetitionGroups()) {
        saveScheduleGroup(group);
        saveScheduleDetail(group);
        saveScheduleParticipants(group);
    }

    return true;
}

7.6 完成编排

功能描述: 确认编排无误后,锁定编排,禁止后续修改。

实现流程:

// 1. 点击"完成编排"按钮,弹出确认对话框
handleConfirm() {
  this.confirmDialogVisible = true
}

// 2. 用户确认
async confirmComplete() {
  try {
    // 先保存当前状态
    await this.handleSaveDraft()

    // 再锁定
    await saveAndLockSchedule(this.competitionId)

    this.isScheduleCompleted = true
    this.confirmDialogVisible = false
    this.$message.success('编排已完成并锁定')
  } catch (err) {
    this.$message.error('完成编排失败')
  }
}

后端处理:

@Transactional
public boolean saveAndLockSchedule(Long competitionId) {
    // 更新状态为"已锁定"
    MartialScheduleStatus status = getScheduleStatus(competitionId);
    status.setScheduleStatus(2);  // 2 = 已锁定
    status.setLockedTime(LocalDateTime.now());
    status.setLockedBy(currentUser);
    updateScheduleStatus(status);

    return true;
}

锁定后的限制:

  • 前端:所有操作按钮变为禁用状态 (v-if="!isScheduleCompleted")
  • 后端:保存接口检查状态,如果已锁定则拒绝保存

8. API接口文档

8.1 获取编排结果

接口地址: GET /api/martial/schedule/result

请求参数:

参数名 类型 必填 说明
competitionId Long 赛事ID

响应示例:

{
  "code": 200,
  "success": true,
  "data": {
    "isCompleted": false,
    "isDraft": true,
    "competitionGroups": [
      {
        "id": 1001,
        "title": "太极拳-成年男子组",
        "type": "个人",
        "count": "20人",
        "code": "TJQ-M-A",
        "venueId": 1,
        "venueName": "一号场地",
        "timeSlot": "2025年06月25日 上午8:30",
        "timeSlotIndex": 0,
        "participants": [
          {
            "id": 1000001,
            "schoolUnit": "北京体育大学武术学院",
            "status": "未签到",
            "sortOrder": 1
          },
          {
            "id": 1000002,
            "schoolUnit": "上海体育学院武术系",
            "status": "已签到",
            "sortOrder": 2
          }
        ]
      }
    ]
  },
  "msg": "操作成功"
}

8.2 保存编排草稿

接口地址: POST /api/martial/schedule/save-draft

请求体:

{
  "competitionId": 1,
  "isDraft": true,
  "competitionGroups": [
    {
      "id": 1001,
      "title": "太极拳-成年男子组",
      "type": "个人",
      "count": "20人",
      "code": "TJQ-M-A",
      "venueId": 1,
      "venueName": "一号场地",
      "timeSlot": "2025年06月25日 上午8:30",
      "timeSlotIndex": 0,
      "participants": [
        {
          "id": 1000001,
          "schoolUnit": "北京体育大学武术学院",
          "status": "未签到",
          "sortOrder": 1
        }
      ]
    }
  ]
}

响应示例:

{
  "code": 200,
  "success": true,
  "data": null,
  "msg": "草稿保存成功"
}

8.3 完成编排并锁定

接口地址: POST /api/martial/schedule/save-and-lock

请求体:

{
  "competitionId": 1
}

响应示例:

{
  "code": 200,
  "success": true,
  "data": null,
  "msg": "编排已完成并锁定"
}

8.4 获取场地列表

接口地址: GET /api/martial/venue/list-by-competition

请求参数:

参数名 类型 必填 说明
competitionId Long 赛事ID

响应示例:

{
  "code": 200,
  "success": true,
  "data": {
    "records": [
      {
        "id": 1,
        "venueName": "一号场地",
        "capacity": 500,
        "location": "体育馆1F"
      },
      {
        "id": 2,
        "venueName": "二号场地",
        "capacity": 300,
        "location": "体育馆2F"
      }
    ]
  },
  "msg": "操作成功"
}

8.5 获取赛事详情

接口地址: GET /api/martial/competition/detail

请求参数:

参数名 类型 必填 说明
id Long 赛事ID

响应示例:

{
  "code": 200,
  "success": true,
  "data": {
    "id": 1,
    "competitionName": "2025年全国武术散打锦标赛",
    "competitionStartTime": "2025-06-25 08:00:00",
    "competitionEndTime": "2025-06-27 18:00:00",
    "organizer": "国家体育总局武术运动管理中心",
    "location": "北京市",
    "venue": "国家奥林匹克体育中心"
  },
  "msg": "操作成功"
}

9. 关键代码解析

9.1 计算属性filteredCompetitionGroups

作用: 根据用户选择的场地和时间段,动态过滤竞赛分组。

computed: {
  filteredCompetitionGroups() {
    // 如果没有选择场地或时间,返回空数组
    if (!this.selectedVenueId || this.selectedTime === null) {
      return []
    }

    // 过滤出匹配的分组
    return this.competitionGroups.filter(group => {
      return group.venueId === this.selectedVenueId &&
             group.timeSlotIndex === this.selectedTime
    })
  }
}

优点:

  • 数据驱动:当 selectedVenueIdselectedTime 改变时,自动重新计算
  • 性能优化Vue的计算属性有缓存机制
  • 代码简洁:模板直接使用 filteredCompetitionGroups

9.2 生成时间段列表

作用: 根据赛事的开始和结束时间,自动生成时间段列表。

generateTimeSlots() {
  const startTime = this.competitionInfo.competitionStartTime
  const endTime = this.competitionInfo.competitionEndTime

  const slots = []
  const start = new Date(startTime)
  const end = new Date(endTime)

  // 遍历每一天
  let currentDate = new Date(start)
  while (currentDate <= end) {
    const year = currentDate.getFullYear()
    const month = currentDate.getMonth() + 1
    const day = currentDate.getDate()
    const dateStr = `${year}${month}${day}日`

    // 添加上午时段 8:30
    slots.push(`${dateStr} 上午8:30`)

    // 添加下午时段 13:30
    slots.push(`${dateStr} 下午13:30`)

    // 下一天
    currentDate.setDate(currentDate.getDate() + 1)
  }

  this.timeSlots = slots
}

示例输出:

[
  "2025年6月25日 上午8:30",
  "2025年6月25日 下午13:30",
  "2025年6月26日 上午8:30",
  "2025年6月26日 下午13:30",
  "2025年6月27日 上午8:30",
  "2025年6月27日 下午13:30"
]

9.3 保存草稿的数据转换

作用: 将前端的数据结构转换为后端需要的格式。

// 前端数据结构
this.competitionGroups = [
  {
    id: 1001,
    title: "太极拳-成年男子组",
    items: [
      { id: 1000001, schoolUnit: "北京体育大学", status: "未签到" },
      { id: 1000002, schoolUnit: "上海体育学院", status: "已签到" }
    ]
  }
]

// 转换为后端格式
const saveData = {
  competitionId: this.competitionId,
  isDraft: true,
  competitionGroups: this.competitionGroups.map(group => ({
    id: group.id,
    title: group.title,
    type: group.type,
    count: group.count,
    code: group.code,
    venueId: group.venueId,
    venueName: group.venueName,
    timeSlot: group.timeSlot,
    timeSlotIndex: group.timeSlotIndex,
    participants: group.items.map((item, index) => ({
      id: item.id,
      schoolUnit: item.schoolUnit,
      status: item.status,
      sortOrder: index + 1  // 根据数组顺序重新计算
    }))
  }))
}

关键点:

  • items 数组 → participants 数组
  • 数组索引 → sortOrder 字段
  • 保持其他字段不变

9.4 后端数据组装

作用: 将数据库查询结果组装为前端需要的DTO格式。

public ScheduleResultDTO getScheduleResult(Long competitionId) {
    // 1. 一次性查询所有数据
    List<ScheduleGroupDetailVO> details = scheduleGroupMapper
        .selectScheduleGroupDetails(competitionId);

    // 2. 按分组ID分组
    Map<Long, List<ScheduleGroupDetailVO>> groupMap = details.stream()
        .collect(Collectors.groupingBy(ScheduleGroupDetailVO::getGroupId));

    // 3. 遍历每个分组构建DTO
    List<CompetitionGroupDTO> groupDTOs = new ArrayList<>();
    for (Map.Entry<Long, List<ScheduleGroupDetailVO>> entry : groupMap.entrySet()) {
        List<ScheduleGroupDetailVO> groupDetails = entry.getValue();

        // 取第一条记录的分组信息
        ScheduleGroupDetailVO firstDetail = groupDetails.get(0);

        // 构建分组DTO
        CompetitionGroupDTO groupDTO = new CompetitionGroupDTO();
        groupDTO.setId(firstDetail.getGroupId());
        groupDTO.setTitle(firstDetail.getGroupName());
        groupDTO.setVenueId(firstDetail.getVenueId());
        groupDTO.setTimeSlot(firstDetail.getTimeSlot());

        // 构建参赛者列表
        List<ParticipantDTO> participantDTOs = groupDetails.stream()
            .filter(d -> d.getParticipantId() != null)
            .map(d -> {
                ParticipantDTO dto = new ParticipantDTO();
                dto.setId(d.getParticipantId());
                dto.setSchoolUnit(d.getOrganization());
                dto.setStatus(d.getCheckInStatus());
                dto.setSortOrder(d.getPerformanceOrder());
                return dto;
            })
            .collect(Collectors.toList());

        groupDTO.setParticipants(participantDTOs);
        groupDTOs.add(groupDTO);
    }

    return new ScheduleResultDTO(groupDTOs);
}

性能优化:

  • 使用 JOIN 查询,一次性获取所有数据,避免 N+1 问题
  • 使用 Stream API 进行分组和映射,代码简洁
  • 在内存中完成数据组装,减少数据库访问

10. 使用指南

10.1 管理员操作流程

10.1.1 进入编排页面

  1. 登录系统
  2. 进入"赛事管理"模块
  3. 选择一个赛事,点击"编排"按钮
  4. 系统自动跳转到编排页面URL格式/schedule/index?competitionId=1&orderId=123

10.1.2 查看编排数据

  1. 页面加载后,自动显示编排数据
  2. 如果是首次编排,后端会自动生成初始编排(通过定时任务)
  3. 如果之前保存过草稿,会加载草稿数据

10.1.3 调整编排

选择场地和时间:

  1. 点击顶部的场地按钮(如"一号场地"
  2. 点击时间段按钮(如"2025年6月25日 上午8:30"
  3. 下方表格自动显示该场地+时间段的分组

调整参赛者顺序:

  1. 在分组表格中,点击"上移"或"下移"按钮
  2. 参赛者的出场顺序会立即改变

移动分组:

  1. 点击分组右侧的"移动"按钮
  2. 在弹出的对话框中选择目标场地和时间段
  3. 点击"确定",分组会被移动到新的场地和时间

标记异常:

  1. 对于未签到的参赛者,点击"异常"按钮
  2. 该参赛者会被标记为异常状态
  3. 点击右上角的"异常组"按钮,可以查看所有异常参赛者

10.1.4 保存草稿

  1. 调整完成后,点击底部的"保存草稿"按钮
  2. 系统会保存当前的编排状态
  3. 下次进入时,会自动加载草稿

10.1.5 完成编排

  1. 确认编排无误后,点击"完成编排"按钮
  2. 在确认对话框中点击"确定"
  3. 系统会锁定编排,禁止后续修改
  4. 页面所有操作按钮变为禁用状态
  5. 底部显示"导出"按钮,可以导出赛程表

10.2 常见问题

10.2.1 为什么编排数据为空?

可能原因:

  1. 后端还没有执行自动编排
  2. 该赛事没有参赛人员
  3. 该赛事没有配置场地

解决方法:

  1. 检查赛事是否有参赛人员(进入"参赛人员"页面)
  2. 检查赛事是否有场地(进入"场地管理"页面)
  3. 手动触发自动编排(调用 /api/martial/schedule/auto-arrange 接口)

10.2.2 为什么无法编辑?

可能原因:

  1. 编排已被锁定(isScheduleCompleted = true

解决方法:

  1. 联系管理员解锁编排(需要在数据库中修改 martial_schedule_status 表的 schedule_status 字段为 0 或 1

10.2.3 保存草稿失败怎么办?

可能原因:

  1. 网络问题
  2. 后端服务异常
  3. 数据格式错误

解决方法:

  1. 查看浏览器控制台的错误信息
  2. 查看后端日志
  3. 联系技术支持

10.3 开发调试

10.3.1 前端调试

// 在浏览器控制台执行
console.log('当前选中的场地ID:', this.selectedVenueId)
console.log('当前选中的时间索引:', this.selectedTime)
console.log('所有竞赛分组:', this.competitionGroups)
console.log('过滤后的分组:', this.filteredCompetitionGroups)

10.3.2 后端调试

// 在 MartialScheduleServiceImpl 中添加日志
log.info("查询编排结果, competitionId: {}", competitionId);
log.info("查询到 {} 条记录", details.size());
log.info("分组数量: {}", groupMap.size());

10.3.3 数据库调试

-- 查看编排状态
SELECT * FROM martial_schedule_status WHERE competition_id = 1;

-- 查看分组数据
SELECT * FROM martial_schedule_group WHERE competition_id = 1;

-- 查看明细数据
SELECT * FROM martial_schedule_detail WHERE competition_id = 1;

-- 查看参赛者关联
SELECT * FROM martial_schedule_participant
WHERE schedule_group_id IN (
  SELECT id FROM martial_schedule_group WHERE competition_id = 1
);

-- 完整查询与后端SQL一致
SELECT
    sg.id AS group_id,
    sg.group_name,
    sd.venue_id,
    sd.time_slot,
    sp.organization,
    sp.performance_order
FROM martial_schedule_group sg
LEFT JOIN martial_schedule_detail sd ON sg.id = sd.schedule_group_id
LEFT JOIN martial_schedule_participant sp ON sd.id = sp.schedule_detail_id
WHERE sg.competition_id = 1 AND sg.is_deleted = 0
ORDER BY sg.display_order, sp.performance_order;

11. 附录

11.1 数据字典

11.1.1 编排状态枚举

状态值 状态名称 说明
0 未编排 尚未执行自动编排
1 有草稿 已执行自动编排或用户保存过草稿
2 已锁定 编排已完成并锁定,不可修改

11.1.2 项目类型枚举

类型值 类型名称 说明
1 个人 单人项目
2 集体 团体项目

11.1.3 参赛者状态枚举

状态值 状态名称 标签颜色
未签到 未签到 info (灰色)
已签到 已签到 success (绿色)
异常 异常 danger (红色)

11.2 相关文档链接

11.3 更新日志

版本 日期 更新内容 作者
v1.0 2025-12-10 创建完整技术方案文档 Claude Code

总结

本文档详细介绍了武术赛事编排系统的完整技术实现,包括:

  1. 架构设计: 前后端分离,清晰的模块划分
  2. 数据库设计: 4张核心表支持灵活的编排调整
  3. 后端实现: Spring Boot + MyBatis Plus优化的SQL查询
  4. 前端实现: Vue2 + Element UI响应式的数据驱动
  5. 核心功能: 场地过滤、顺序调整、分组移动、异常标记、草稿保存、锁定发布
  6. 数据流转: 完整的请求-响应流程
  7. 使用指南: 详细的操作步骤和常见问题解决

希望这份文档能帮助您全面理解编排系统的实现原理和使用方法。如有任何疑问,欢迎随时咨询!


文档结束