fix bugs
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2025-12-11 16:56:19 +08:00
parent ab69968bda
commit 5b806e29b7
45 changed files with 13744 additions and 6364 deletions

View 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:30180分钟预留30分钟机动
- 下午场13:30 - 17:30240分钟预留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
**审核人**: 待定
**状态**: 设计中