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

1857 lines
60 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 武术赛事编排系统 - 完整技术方案
> **文档版本**: 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. **使用指南**: 详细的操作步骤和常见问题解决
希望这份文档能帮助您全面理解编排系统的实现原理和使用方法。如有任何疑问,欢迎随时咨询!
---
**文档结束**