60 KiB
武术赛事编排系统 - 完整技术方案
文档版本: v1.0 创建日期: 2025-12-10 文档作者: Claude Code 项目名称: 武术赛事管理系统 - 赛程编排模块
📋 目录
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;
}
数据流程:
- 从数据库一次性JOIN查询所有相关数据
- 在内存中按分组ID进行分组
- 检查编排状态(草稿 or 已完成)
- 构建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_id和time_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
})
}
}
优点:
- 数据驱动:当
selectedVenueId或selectedTime改变时,自动重新计算 - 性能优化: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 进入编排页面
- 登录系统
- 进入"赛事管理"模块
- 选择一个赛事,点击"编排"按钮
- 系统自动跳转到编排页面,URL格式:
/schedule/index?competitionId=1&orderId=123
10.1.2 查看编排数据
- 页面加载后,自动显示编排数据
- 如果是首次编排,后端会自动生成初始编排(通过定时任务)
- 如果之前保存过草稿,会加载草稿数据
10.1.3 调整编排
选择场地和时间:
- 点击顶部的场地按钮(如"一号场地")
- 点击时间段按钮(如"2025年6月25日 上午8:30")
- 下方表格自动显示该场地+时间段的分组
调整参赛者顺序:
- 在分组表格中,点击"上移"或"下移"按钮
- 参赛者的出场顺序会立即改变
移动分组:
- 点击分组右侧的"移动"按钮
- 在弹出的对话框中选择目标场地和时间段
- 点击"确定",分组会被移动到新的场地和时间
标记异常:
- 对于未签到的参赛者,点击"异常"按钮
- 该参赛者会被标记为异常状态
- 点击右上角的"异常组"按钮,可以查看所有异常参赛者
10.1.4 保存草稿
- 调整完成后,点击底部的"保存草稿"按钮
- 系统会保存当前的编排状态
- 下次进入时,会自动加载草稿
10.1.5 完成编排
- 确认编排无误后,点击"完成编排"按钮
- 在确认对话框中点击"确定"
- 系统会锁定编排,禁止后续修改
- 页面所有操作按钮变为禁用状态
- 底部显示"导出"按钮,可以导出赛程表
10.2 常见问题
10.2.1 为什么编排数据为空?
可能原因:
- 后端还没有执行自动编排
- 该赛事没有参赛人员
- 该赛事没有配置场地
解决方法:
- 检查赛事是否有参赛人员(进入"参赛人员"页面)
- 检查赛事是否有场地(进入"场地管理"页面)
- 手动触发自动编排(调用
/api/martial/schedule/auto-arrange接口)
10.2.2 为什么无法编辑?
可能原因:
- 编排已被锁定(
isScheduleCompleted = true)
解决方法:
- 联系管理员解锁编排(需要在数据库中修改
martial_schedule_status表的schedule_status字段为 0 或 1)
10.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 |
总结
本文档详细介绍了武术赛事编排系统的完整技术实现,包括:
- 架构设计: 前后端分离,清晰的模块划分
- 数据库设计: 4张核心表,支持灵活的编排调整
- 后端实现: Spring Boot + MyBatis Plus,优化的SQL查询
- 前端实现: Vue2 + Element UI,响应式的数据驱动
- 核心功能: 场地过滤、顺序调整、分组移动、异常标记、草稿保存、锁定发布
- 数据流转: 完整的请求-响应流程
- 使用指南: 详细的操作步骤和常见问题解决
希望这份文档能帮助您全面理解编排系统的实现原理和使用方法。如有任何疑问,欢迎随时咨询!
文档结束