This commit is contained in:
819
doc/schedule/archive/schedule-system-design.md
Normal file
819
doc/schedule/archive/schedule-system-design.md
Normal file
@@ -0,0 +1,819 @@
|
||||
# 赛程编排系统设计文档
|
||||
|
||||
## 📋 文档说明
|
||||
|
||||
**版本**: 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
|
||||
**审核人**: 待定
|
||||
**状态**: 设计中
|
||||
Reference in New Issue
Block a user