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

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

---

## 📋 目录

1. [系统概述](#系统概述)
2. [架构设计](#架构设计)
3. [数据库设计](#数据库设计)
4. [后端实现](#后端实现)
5. [前端实现](#前端实现)
6. [数据流转](#数据流转)
7. [核心功能](#核心功能)
8. [API接口文档](#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)

**用途**: 存储赛程的分组信息（按项目和组别划分）

```sql
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)

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

```sql
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)

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

```sql
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)

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

```sql
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 (参赛选手表) - 节选

```sql
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 (场地表) - 节选

```sql
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`

**主要接口**:

```java
@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

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

**实现逻辑**:

```java
@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

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

**实现逻辑**:

```java
@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`

```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 页面布局

```vue
<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 核心数据结构

```javascript
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 加载编排数据

```javascript
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 保存草稿

```javascript
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 上移/下移操作

```javascript
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 标记异常

```javascript
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`

```javascript
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 查询编排数据

```sql
-- 一次性查询所有相关数据
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 保存草稿数据

```sql
-- 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 场地和时间段过滤

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

**实现方式**:

```javascript
// 计算属性：根据选中的场地和时间段过滤
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 参赛者顺序调整

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

**实现方式**:

```javascript
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 分组移动

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

**实现流程**:

```javascript
// 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_id` 和 `time_slot` 字段

### 7.4 异常标记

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

**实现流程**:

```javascript
// 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 草稿保存

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

**实现流程**:

```javascript
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('草稿保存成功')
}
```

**后端处理**:
```java
@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 完成编排

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

**实现流程**:

```javascript
// 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('完成编排失败')
  }
}
```

**后端处理**:
```java
@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 |

**响应示例**:

```json
{
  "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`

**请求体**:

```json
{
  "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
        }
      ]
    }
  ]
}
```

**响应示例**:

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

### 8.3 完成编排并锁定

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

**请求体**:

```json
{
  "competitionId": 1
}
```

**响应示例**:

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

### 8.4 获取场地列表

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

**请求参数**:

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

**响应示例**:

```json
{
  "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 |

**响应示例**:

```json
{
  "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

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

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

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

**优点**:
- 数据驱动：当 `selectedVenueId` 或 `selectedTime` 改变时，自动重新计算
- 性能优化：Vue的计算属性有缓存机制
- 代码简洁：模板直接使用 `filteredCompetitionGroups`

### 9.2 生成时间段列表

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

```javascript
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 保存草稿的数据转换

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

```javascript
// 前端数据结构
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格式。

```java
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 前端调试

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

#### 10.3.2 后端调试

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

#### 10.3.3 数据库调试

```sql
-- 查看编排状态
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 相关文档链接

- [赛事管理系统整体设计文档](./system-design.md)
- [自动编排算法文档](./auto-arrange-algorithm.md)
- [数据库设计文档](./database-design.md)
- [API接口文档](./api-documentation.md)
- [前端开发规范](./frontend-standards.md)

### 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. **使用指南**: 详细的操作步骤和常见问题解决

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

---

**文档结束**
