820 lines
28 KiB
Markdown
820 lines
28 KiB
Markdown
# 赛程编排系统设计文档
|
||
|
||
## 📋 文档说明
|
||
|
||
**版本**: v2.0
|
||
**创建日期**: 2025-12-08
|
||
**最后更新**: 2025-12-08
|
||
**状态**: 设计阶段
|
||
|
||
---
|
||
|
||
## 1. 业务需求概述
|
||
|
||
### 1.1 核心需求
|
||
|
||
武术赛事管理系统需要实现**自动赛程编排功能**,将参赛者智能分配到不同的场地和时间段,确保比赛有序进行。
|
||
|
||
### 1.2 关键特性
|
||
|
||
- ✅ **后端自动编排**:使用Java后端定时任务自动编排,前端只负责展示
|
||
- ✅ **集体优先原则**:集体项目优先编排,个人项目随后
|
||
- ✅ **负载均衡**:均匀分配到所有场地和时间段
|
||
- ✅ **定时刷新**:每10分钟自动重新编排(未保存状态)
|
||
- ✅ **手动调整**:支持用户手动调整编排结果
|
||
- ✅ **锁定机制**:保存后锁定,不再自动编排
|
||
|
||
---
|
||
|
||
## 2. 业务规则
|
||
|
||
### 2.1 项目类型
|
||
|
||
#### 集体项目(type=2)
|
||
- **定义**:多人一场表演
|
||
- **时长**:约5分钟/场
|
||
- **场地占用**:独占整个场地
|
||
- **示例**:太极拳男组(泰州太极拳小学:张三、李四、王五、小红、小花)
|
||
- **分组规则**:按"项目+组别"分组,同一分组内按单位列出
|
||
|
||
#### 个人项目(type=1)
|
||
- **定义**:单人表演
|
||
- **时长**:约1分钟/人
|
||
- **场地占用**:场地可同时容纳6人
|
||
- **示例**:太极拳个人男组(台州太极拳馆:洪坚立;泰州太极拳小学:李四)
|
||
- **分组规则**:按"项目+组别"分组,不限人数
|
||
|
||
### 2.2 时间段划分
|
||
|
||
```
|
||
每天分为两个时间段:
|
||
- 上午场:08:30 - 11:30(180分钟,预留30分钟机动)
|
||
- 下午场:13:30 - 17:30(240分钟,预留30分钟机动)
|
||
|
||
实际可用时间:
|
||
- 上午:150分钟(扣除间隔时间)
|
||
- 下午:210分钟(扣除间隔时间)
|
||
|
||
间隔时间:每场比赛间隔1-2分钟(选手准备)
|
||
```
|
||
|
||
### 2.3 编排优先级
|
||
|
||
```
|
||
优先级排序:
|
||
1. 集体项目(type=2)
|
||
2. 个人项目(type=1)
|
||
|
||
同类型内部排序:
|
||
- 按项目ID升序
|
||
- 按组别(category)排序
|
||
- 按报名时间先后
|
||
```
|
||
|
||
### 2.4 分配策略
|
||
|
||
#### 场地分配
|
||
- **集体项目**:每个分组独占一个场地时间段
|
||
- **个人项目**:每个场地时间段可容纳多个分组(按6人/批次计算)
|
||
|
||
#### 时间段分配
|
||
- **负载均衡**:优先填充负载较轻的时间段
|
||
- **连续性**:同一项目的多个分组尽量安排在相邻时间段
|
||
- **容量检查**:确保不超过时间段容量
|
||
|
||
#### 计算公式
|
||
```
|
||
集体项目占用时长 = 队伍数 × 5分钟 + (队伍数-1) × 2分钟间隔
|
||
个人项目占用时长 = ⌈人数/6⌉ × (6分钟 + 2分钟间隔)
|
||
|
||
场地时间段容量:
|
||
- 上午:150分钟
|
||
- 下午:210分钟
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 数据库设计
|
||
|
||
### 3.1 编排主表
|
||
|
||
```sql
|
||
CREATE TABLE `martial_schedule_group` (
|
||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||
`competition_id` bigint(20) NOT NULL COMMENT '赛事ID',
|
||
`group_name` varchar(200) NOT NULL COMMENT '分组名称:太极拳男组',
|
||
`project_id` bigint(20) 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(11) NOT NULL DEFAULT '0' COMMENT '显示顺序(集体优先)',
|
||
`total_participants` int(11) DEFAULT '0' COMMENT '总参赛人数',
|
||
`total_teams` int(11) DEFAULT '0' COMMENT '总队伍数(集体项目)',
|
||
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (`id`),
|
||
KEY `idx_competition` (`competition_id`),
|
||
KEY `idx_project` (`project_id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='赛程编排分组表';
|
||
```
|
||
|
||
### 3.2 编排明细表(场地时间段分配)
|
||
|
||
```sql
|
||
CREATE TABLE `martial_schedule_detail` (
|
||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||
`schedule_group_id` bigint(20) NOT NULL COMMENT '分组ID',
|
||
`competition_id` bigint(20) NOT NULL COMMENT '赛事ID',
|
||
`venue_id` bigint(20) 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 '预计结束时间',
|
||
`estimated_duration` int(11) DEFAULT '0' COMMENT '预计时长(分钟)',
|
||
`participant_count` int(11) DEFAULT '0' COMMENT '参赛人数',
|
||
`sort_order` int(11) DEFAULT '0' COMMENT '场内顺序',
|
||
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (`id`),
|
||
KEY `idx_group` (`schedule_group_id`),
|
||
KEY `idx_venue_time` (`venue_id`, `schedule_date`, `time_slot`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='赛程编排明细表';
|
||
```
|
||
|
||
### 3.3 参赛者关联表
|
||
|
||
```sql
|
||
CREATE TABLE `martial_schedule_participant` (
|
||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||
`schedule_detail_id` bigint(20) NOT NULL COMMENT '编排明细ID',
|
||
`schedule_group_id` bigint(20) NOT NULL COMMENT '分组ID',
|
||
`participant_id` bigint(20) NOT NULL COMMENT '参赛者ID',
|
||
`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(11) DEFAULT '0' COMMENT '出场顺序',
|
||
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (`id`),
|
||
KEY `idx_detail` (`schedule_detail_id`),
|
||
KEY `idx_participant` (`participant_id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='赛程编排参赛者关联表';
|
||
```
|
||
|
||
### 3.4 编排状态表
|
||
|
||
```sql
|
||
CREATE TABLE `martial_schedule_status` (
|
||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||
`competition_id` bigint(20) 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(11) DEFAULT '0' COMMENT '总分组数',
|
||
`total_participants` int(11) DEFAULT '0' COMMENT '总参赛人数',
|
||
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (`id`),
|
||
UNIQUE KEY `uk_competition` (`competition_id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='赛程编排状态表';
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 后端编排算法设计
|
||
|
||
### 4.1 算法流程
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ 定时任务:每10分钟执行一次 │
|
||
└─────────────────┬───────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────┐
|
||
│ 1. 检查赛事状态 │
|
||
│ - 如果已锁定(status=2),跳过 │
|
||
│ - 如果未开始,继续 │
|
||
└─────────────────┬───────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────┐
|
||
│ 2. 加载数据 │
|
||
│ - 赛事信息(开始/结束时间) │
|
||
│ - 场地列表 │
|
||
│ - 参赛者列表 │
|
||
└─────────────────┬───────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────┐
|
||
│ 3. 生成时间段网格 │
|
||
│ - 计算比赛天数 │
|
||
│ - 生成所有时间段(上午/下午) │
|
||
└─────────────────┬───────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────┐
|
||
│ 4. 自动分组 │
|
||
│ - 集体项目按"项目+组别"分组 │
|
||
│ - 个人项目按"项目+组别"分组 │
|
||
│ - 集体项目排在前面 │
|
||
└─────────────────┬───────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────┐
|
||
│ 5. 分配场地和时间段(负载均衡) │
|
||
│ - 初始化所有场地×时间段的负载 │
|
||
│ - 按时长降序处理分组 │
|
||
│ - 贪心算法:选择负载最小的位置 │
|
||
└─────────────────┬───────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────┐
|
||
│ 6. 保存到数据库 │
|
||
│ - 清空旧的编排数据 │
|
||
│ - 插入新的编排结果 │
|
||
│ - 更新编排状态 │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
### 4.2 核心算法伪代码
|
||
|
||
#### 4.2.1 自动分组算法
|
||
|
||
```java
|
||
public List<ScheduleGroup> autoGroupParticipants(List<Participant> participants) {
|
||
List<ScheduleGroup> groups = new ArrayList<>();
|
||
int displayOrder = 1;
|
||
|
||
// 1. 分离集体和个人项目
|
||
List<Participant> teamParticipants = participants.stream()
|
||
.filter(p -> p.getProjectType() == 2)
|
||
.collect(Collectors.toList());
|
||
|
||
List<Participant> individualParticipants = participants.stream()
|
||
.filter(p -> p.getProjectType() == 1)
|
||
.collect(Collectors.toList());
|
||
|
||
// 2. 集体项目分组:按"项目ID_组别"分组
|
||
Map<String, List<Participant>> teamGroupMap = teamParticipants.stream()
|
||
.collect(Collectors.groupingBy(p ->
|
||
p.getProjectId() + "_" + p.getCategory()
|
||
));
|
||
|
||
for (Map.Entry<String, List<Participant>> entry : teamGroupMap.entrySet()) {
|
||
List<Participant> members = entry.getValue();
|
||
Participant first = members.get(0);
|
||
|
||
// 统计队伍数(按单位分组)
|
||
long teamCount = members.stream()
|
||
.map(Participant::getOrganization)
|
||
.distinct()
|
||
.count();
|
||
|
||
ScheduleGroup group = new ScheduleGroup();
|
||
group.setGroupName(first.getProjectName() + " " + first.getCategory());
|
||
group.setProjectId(first.getProjectId());
|
||
group.setProjectType(2);
|
||
group.setDisplayOrder(displayOrder++);
|
||
group.setTotalParticipants(members.size());
|
||
group.setTotalTeams((int) teamCount);
|
||
group.setParticipants(members);
|
||
|
||
// 计算预计时长:队伍数 × 5分钟 + 间隔时间
|
||
int duration = (int) teamCount * 5 + ((int) teamCount - 1) * 2;
|
||
group.setEstimatedDuration(duration);
|
||
|
||
groups.add(group);
|
||
}
|
||
|
||
// 3. 个人项目分组:按"项目ID_组别"分组
|
||
Map<String, List<Participant>> individualGroupMap = individualParticipants.stream()
|
||
.collect(Collectors.groupingBy(p ->
|
||
p.getProjectId() + "_" + p.getCategory()
|
||
));
|
||
|
||
for (Map.Entry<String, List<Participant>> entry : individualGroupMap.entrySet()) {
|
||
List<Participant> members = entry.getValue();
|
||
Participant first = members.get(0);
|
||
|
||
ScheduleGroup group = new ScheduleGroup();
|
||
group.setGroupName(first.getProjectName() + " " + first.getCategory());
|
||
group.setProjectId(first.getProjectId());
|
||
group.setProjectType(1);
|
||
group.setDisplayOrder(displayOrder++);
|
||
group.setTotalParticipants(members.size());
|
||
group.setParticipants(members);
|
||
|
||
// 计算预计时长:人数/6(向上取整)× (6分钟 + 2分钟间隔)
|
||
int batches = (int) Math.ceil(members.size() / 6.0);
|
||
int duration = batches * 8;
|
||
group.setEstimatedDuration(duration);
|
||
|
||
groups.add(group);
|
||
}
|
||
|
||
return groups;
|
||
}
|
||
```
|
||
|
||
#### 4.2.2 场地时间段分配算法(负载均衡)
|
||
|
||
```java
|
||
public void assignVenueAndTimeSlot(List<ScheduleGroup> groups,
|
||
List<Venue> venues,
|
||
List<TimeSlot> timeSlots) {
|
||
|
||
// 1. 初始化负载表(场地 × 时间段)
|
||
Map<String, Integer> loadMap = new HashMap<>();
|
||
for (Venue venue : venues) {
|
||
for (TimeSlot timeSlot : timeSlots) {
|
||
String key = venue.getId() + "_" + timeSlot.getKey();
|
||
loadMap.put(key, 0);
|
||
}
|
||
}
|
||
|
||
// 2. 获取时间段容量
|
||
Map<String, Integer> capacityMap = new HashMap<>();
|
||
for (TimeSlot timeSlot : timeSlots) {
|
||
int capacity = timeSlot.getPeriod().equals("morning") ? 150 : 210;
|
||
capacityMap.put(timeSlot.getKey(), capacity);
|
||
}
|
||
|
||
// 3. 按预计时长降序排序(先安排时间长的)
|
||
groups.sort((a, b) -> b.getEstimatedDuration() - a.getEstimatedDuration());
|
||
|
||
// 4. 贪心算法分配
|
||
for (ScheduleGroup group : groups) {
|
||
String bestKey = null;
|
||
int minLoad = Integer.MAX_VALUE;
|
||
|
||
// 遍历所有场地×时间段组合
|
||
for (Venue venue : venues) {
|
||
for (TimeSlot timeSlot : timeSlots) {
|
||
String key = venue.getId() + "_" + timeSlot.getKey();
|
||
int currentLoad = loadMap.get(key);
|
||
int capacity = capacityMap.get(timeSlot.getKey());
|
||
|
||
// 检查容量是否足够
|
||
if (currentLoad + group.getEstimatedDuration() <= capacity) {
|
||
if (currentLoad < minLoad) {
|
||
minLoad = currentLoad;
|
||
bestKey = key;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 分配到最佳位置
|
||
if (bestKey != null) {
|
||
String[] parts = bestKey.split("_");
|
||
long venueId = Long.parseLong(parts[0]);
|
||
String timeSlotKey = parts[1];
|
||
|
||
group.setVenueId(venueId);
|
||
group.setTimeSlotKey(timeSlotKey);
|
||
|
||
// 更新负载
|
||
loadMap.put(bestKey, loadMap.get(bestKey) + group.getEstimatedDuration());
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.3 算法复杂度分析
|
||
|
||
- **自动分组算法**: O(n),n为参赛者数量
|
||
- **场地分配算法**: O(g × v × t),g为分组数,v为场地数,t为时间段数
|
||
- **总体复杂度**: O(n + g×v×t)
|
||
|
||
对于1000人,5个场地,10个时间段:
|
||
- 分组: O(1000) ≈ 1ms
|
||
- 分配: O(100×5×10) = O(5000) ≈ 5ms
|
||
- **总耗时**: < 10ms
|
||
|
||
---
|
||
|
||
## 5. 前端展示设计
|
||
|
||
### 5.1 页面布局
|
||
|
||
```
|
||
┌────────────────────────────────────────────────────────────┐
|
||
│ 编排 - 郑州协会全国运动大赛 [返回] │
|
||
└────────────────────────────────────────────────────────────┘
|
||
┌────────────────────────────────────────────────────────────┐
|
||
│ [竞赛分组] [场地] │
|
||
└────────────────────────────────────────────────────────────┘
|
||
|
||
┌────────────────────────────────────────────────────────────┐
|
||
│ 竞赛分组内容区 │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ 1. 太极拳男组 集体 2队 2组 1101 │ │
|
||
│ │ ┌─────────────────────────────────────────────┐ │ │
|
||
│ │ │ 1. 少林寺武校 │ │ │
|
||
│ │ │ [场A 2025-11-06 08:30] [场A 2025-11-06 13:30] ...│
|
||
│ │ │ 2. 洛阳武校 │ │ │
|
||
│ │ │ [场B 2025-11-06 08:30] [场B 2025-11-06 13:30] ...│
|
||
│ │ └─────────────────────────────────────────────┘ │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ 2. 长拳个人男组 个人 3人 1个A 1102 │ │
|
||
│ │ ┌─────────────────────────────────────────────┐ │ │
|
||
│ │ │ 1. 少林寺武校 张三 │ │ │
|
||
│ │ │ [场A 2025-11-06 08:30] │ │ │
|
||
│ │ │ 2. 洛阳武校 李四 │ │ │
|
||
│ │ │ [场A 2025-11-06 08:30] │ │ │
|
||
│ │ │ 3. 少林寺武校 王五 │ │ │
|
||
│ │ │ [场B 2025-11-06 13:30] │ │ │
|
||
│ │ └─────────────────────────────────────────────┘ │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
└────────────────────────────────────────────────────────────┘
|
||
|
||
┌────────────────────────────────────────────────────────────┐
|
||
│ [保存编排] │
|
||
└────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 5.2 数据结构
|
||
|
||
```javascript
|
||
// 前端数据结构
|
||
{
|
||
competitionInfo: {
|
||
competitionId: 200,
|
||
competitionName: "郑州协会全国运动大赛",
|
||
startDate: "2025-11-06",
|
||
endDate: "2025-11-10"
|
||
},
|
||
|
||
scheduleGroups: [
|
||
{
|
||
id: 1,
|
||
groupName: "太极拳男组",
|
||
projectType: 2, // 集体
|
||
displayOrder: 1,
|
||
totalParticipants: 10,
|
||
totalTeams: 2,
|
||
|
||
// 按单位组织的参赛者(集体项目)
|
||
organizationGroups: [
|
||
{
|
||
organization: "少林寺武校",
|
||
participants: [
|
||
{ id: 1, playerName: "张三", ... },
|
||
{ id: 2, playerName: "李四", ... }
|
||
],
|
||
scheduleDetails: [
|
||
{
|
||
venueId: 1,
|
||
venueName: "场A",
|
||
scheduleDate: "2025-11-06",
|
||
timePeriod: "morning",
|
||
timeSlot: "08:30"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
organization: "洛阳武校",
|
||
participants: [...],
|
||
scheduleDetails: [...]
|
||
}
|
||
]
|
||
},
|
||
|
||
{
|
||
id: 2,
|
||
groupName: "长拳个人男组",
|
||
projectType: 1, // 个人
|
||
displayOrder: 2,
|
||
totalParticipants: 3,
|
||
|
||
// 个人项目直接列出参赛者
|
||
participants: [
|
||
{
|
||
id: 10,
|
||
organization: "少林寺武校",
|
||
playerName: "张三",
|
||
scheduleDetail: {
|
||
venueId: 1,
|
||
venueName: "场A",
|
||
scheduleDate: "2025-11-06",
|
||
timePeriod: "morning",
|
||
timeSlot: "08:30"
|
||
}
|
||
},
|
||
{
|
||
id: 11,
|
||
organization: "洛阳武校",
|
||
playerName: "李四",
|
||
scheduleDetail: {...}
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 5.3 场地按钮点击交互
|
||
|
||
当用户点击某个场地时间段按钮时:
|
||
|
||
```javascript
|
||
handleVenueTimeClick(participant, scheduleDetail) {
|
||
// 弹出对话框显示该时间段该场地的详细信息
|
||
this.$alert(`
|
||
<h3>场地详情</h3>
|
||
<p>场地: ${scheduleDetail.venueName}</p>
|
||
<p>时间: ${scheduleDetail.scheduleDate} ${scheduleDetail.timeSlot}</p>
|
||
<p>参赛者: ${participant.organization} - ${participant.playerName}</p>
|
||
<p>项目: ${participant.projectName}</p>
|
||
`, '场地时间段详情', {
|
||
dangerouslyUseHTMLString: true
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 后端定时任务设计
|
||
|
||
### 6.1 定时任务配置
|
||
|
||
```java
|
||
@Component
|
||
@EnableScheduling
|
||
public class ScheduleAutoArrangeTask {
|
||
|
||
@Autowired
|
||
private IScheduleService scheduleService;
|
||
|
||
/**
|
||
* 每10分钟执行一次自动编排
|
||
* cron: 0 */10 * * * ?
|
||
*/
|
||
@Scheduled(cron = "0 */10 * * * ?")
|
||
public void autoArrangeSchedule() {
|
||
log.info("开始执行自动编排任务...");
|
||
|
||
try {
|
||
// 查询所有未锁定的赛事
|
||
List<Long> competitionIds = scheduleService.getUnlockedCompetitions();
|
||
|
||
for (Long competitionId : competitionIds) {
|
||
try {
|
||
// 执行自动编排
|
||
scheduleService.autoArrange(competitionId);
|
||
log.info("赛事[{}]自动编排完成", competitionId);
|
||
} catch (Exception e) {
|
||
log.error("赛事[{}]自动编排失败", competitionId, e);
|
||
}
|
||
}
|
||
|
||
} catch (Exception e) {
|
||
log.error("自动编排任务执行失败", e);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 6.2 编排服务接口
|
||
|
||
```java
|
||
public interface IScheduleService {
|
||
|
||
/**
|
||
* 自动编排
|
||
* @param competitionId 赛事ID
|
||
*/
|
||
void autoArrange(Long competitionId);
|
||
|
||
/**
|
||
* 获取未锁定的赛事列表
|
||
* @return 赛事ID列表
|
||
*/
|
||
List<Long> getUnlockedCompetitions();
|
||
|
||
/**
|
||
* 保存编排(锁定)
|
||
* @param competitionId 赛事ID
|
||
* @param userId 用户ID
|
||
*/
|
||
void saveAndLock(Long competitionId, String userId);
|
||
|
||
/**
|
||
* 获取编排结果
|
||
* @param competitionId 赛事ID
|
||
* @return 编排数据
|
||
*/
|
||
ScheduleResult getScheduleResult(Long competitionId);
|
||
|
||
/**
|
||
* 手动调整编排
|
||
* @param adjustRequest 调整请求
|
||
*/
|
||
void adjustSchedule(ScheduleAdjustRequest adjustRequest);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. API接口设计
|
||
|
||
### 7.1 获取编排结果
|
||
|
||
```
|
||
GET /api/martial/schedule/result/{competitionId}
|
||
|
||
Response:
|
||
{
|
||
"code": 200,
|
||
"msg": "success",
|
||
"data": {
|
||
"competitionId": 200,
|
||
"scheduleStatus": 1, // 0=未编排 1=编排中 2=已锁定
|
||
"lastAutoScheduleTime": "2025-11-06 10:00:00",
|
||
"totalGroups": 45,
|
||
"totalParticipants": 1100,
|
||
"scheduleGroups": [
|
||
{
|
||
"id": 1,
|
||
"groupName": "太极拳男组",
|
||
"projectType": 2,
|
||
"displayOrder": 1,
|
||
"organizationGroups": [...]
|
||
},
|
||
...
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.2 保存并锁定编排
|
||
|
||
```
|
||
POST /api/martial/schedule/save-and-lock
|
||
|
||
Request:
|
||
{
|
||
"competitionId": 200,
|
||
"userId": "admin"
|
||
}
|
||
|
||
Response:
|
||
{
|
||
"code": 200,
|
||
"msg": "编排已保存并锁定"
|
||
}
|
||
```
|
||
|
||
### 7.3 手动调整编排
|
||
|
||
```
|
||
POST /api/martial/schedule/adjust
|
||
|
||
Request:
|
||
{
|
||
"competitionId": 200,
|
||
"participantId": 123,
|
||
"targetVenueId": 2,
|
||
"targetDate": "2025-11-06",
|
||
"targetTimeSlot": "13:30"
|
||
}
|
||
|
||
Response:
|
||
{
|
||
"code": 200,
|
||
"msg": "调整成功"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 测试数据设计
|
||
|
||
### 8.1 集体项目测试数据
|
||
|
||
需要生成100个集体项目的参赛队伍:
|
||
|
||
```
|
||
项目分布:
|
||
- 太极拳(集体):20个单位
|
||
- 长拳(集体):20个单位
|
||
- 剑术(集体):20个单位
|
||
- 刀术(集体):20个单位
|
||
- 棍术(集体):20个单位
|
||
|
||
每个单位5人,共100个队伍,500人
|
||
```
|
||
|
||
### 8.2 测试数据总计
|
||
|
||
```
|
||
原有个人项目:1000人
|
||
新增集体项目:500人(100个队伍)
|
||
总计:1500人
|
||
|
||
预计分组:
|
||
- 集体项目分组:约20个(按项目+组别)
|
||
- 个人项目分组:约25个
|
||
- 总计:约45个分组
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 技术实现要点
|
||
|
||
### 9.1 后端技术栈
|
||
|
||
- **Spring Boot**: 2.x
|
||
- **MyBatis-Plus**: 数据访问
|
||
- **Quartz**: 定时任务调度
|
||
- **Redis**: 编排结果缓存(可选)
|
||
|
||
### 9.2 前端技术栈
|
||
|
||
- **Vue 3**: 前端框架
|
||
- **Element Plus**: UI组件
|
||
- **Axios**: HTTP请求
|
||
|
||
### 9.3 性能优化
|
||
|
||
1. **批量查询**:一次性加载所有参赛者
|
||
2. **结果缓存**:编排结果缓存10分钟
|
||
3. **增量编排**:只对新增参赛者进行增量编排(可选)
|
||
4. **索引优化**:场地、时间段联合索引
|
||
|
||
---
|
||
|
||
## 10. 实施计划
|
||
|
||
### 阶段1:数据库和测试数据(第1天)
|
||
- ✅ 创建数据库表
|
||
- ✅ 生成集体项目测试数据
|
||
- ✅ 验证数据完整性
|
||
|
||
### 阶段2:后端编排算法(第2-3天)
|
||
- ⏳ 实现自动分组算法
|
||
- ⏳ 实现场地时间段分配算法
|
||
- ⏳ 实现定时任务
|
||
- ⏳ 单元测试
|
||
|
||
### 阶段3:后端API接口(第4天)
|
||
- ⏳ 获取编排结果接口
|
||
- ⏳ 保存锁定接口
|
||
- ⏳ 手动调整接口
|
||
|
||
### 阶段4:前端展示页面(第5-6天)
|
||
- ⏳ 修改页面布局
|
||
- ⏳ 实现集体/个人不同展示
|
||
- ⏳ 实现场地时间段按钮点击
|
||
- ⏳ 集成后端API
|
||
|
||
### 阶段5:测试和优化(第7天)
|
||
- ⏳ 功能测试
|
||
- ⏳ 性能测试
|
||
- ⏳ 用户验收测试
|
||
|
||
---
|
||
|
||
## 11. 风险和注意事项
|
||
|
||
### 11.1 容量不足风险
|
||
|
||
**风险**:参赛人数过多,所有场地时间段容量不足
|
||
|
||
**解决方案**:
|
||
- 编排前进行容量校验
|
||
- 提示用户增加比赛天数或场地
|
||
- 自动建议最少需要的天数
|
||
|
||
### 11.2 数据一致性
|
||
|
||
**风险**:定时任务执行时用户正在查看页面
|
||
|
||
**解决方案**:
|
||
- 前端轮询检查编排时间戳
|
||
- 如有更新,提示用户刷新
|
||
- 锁定状态下不再自动编排
|
||
|
||
### 11.3 并发冲突
|
||
|
||
**风险**:多个定时任务同时执行
|
||
|
||
**解决方案**:
|
||
- 使用分布式锁(Redis)
|
||
- 数据库乐观锁
|
||
- 任务执行状态标记
|
||
|
||
---
|
||
|
||
**文档版本**: v2.0
|
||
**创建人**: Claude Code
|
||
**审核人**: 待定
|
||
**状态**: 设计中
|