# Task 6: 编排调度功能 **负责人:** Claude Code **优先级:** P3 → P1(用户新需求) **预计工时:** 10天 **状态:** 🟡 设计中 **创建时间:** 2025-11-30 --- ## 📋 需求概述 编排调度功能是赛事组织的核心环节,负责将报名的运动员合理分配到不同的时间段和场地进行比赛。系统需要基于多种约束条件自动生成编排方案,并支持人工微调。 ### 业务流程 ``` 报名完成 → 自动编排 → 人工微调 → 确认发布 → 比赛执行 → 临时调整 ``` --- ## 🎯 功能需求 ### 1. 赛前自动编排(核心功能) #### 1.1 前置条件 - ✅ 报名阶段已完成 - ✅ 所有参赛运动员信息已录入 - ✅ 比赛场地信息已配置 - ✅ 比赛时间段已设定 #### 1.2 输入数据 **比赛基础数据** - 比赛时间段(开始时间、结束时间) - 场地数量及名称 - 项目列表及详细信息 **项目信息** | 字段 | 说明 | 示例 | |------|------|------| | 项目名称 | 比赛项目 | "太极拳"、"长拳" | | 报名单位数量 | 有多少队伍/运动员报名 | 15个队 | | 单次上场单位数 | 一轮比赛几个单位同时上场 | 1个(个人)/ 3个(团体) | | 单场比赛时间 | 包含入场+表演+打分 | 10分钟 | | 项目类型 | 个人/双人/集体 | 集体 | #### 1.3 编排规则(硬约束) **基础规则** 1. ✅ **场地互斥**:同一场地同一时间只能进行一个项目 2. ✅ **运动员互斥**:同一运动员同一时间只能参加一个比赛 3. ✅ **项目聚合**:同类项目尽量安排在连续的时间段(如太极拳放在一起) **优先级规则(软约束)** 1. 🥇 **集体项目优先**:集体项目优先安排 2. 🥈 **时间均衡**:各场地的比赛时间尽量均衡 3. 🥉 **休息时间**:同一运动员的不同项目之间预留休息时间 #### 1.4 输出结果 **预编排表结构** ``` 编排方案ID ├── 时间段1 (09:00-09:30) │ ├── 场地A: 长拳-男子组 (运动员1, 2, 3...) │ ├── 场地B: 太极拳-女子组 (运动员4, 5, 6...) │ └── 场地C: 集体项目 (队伍1, 2...) ├── 时间段2 (09:30-10:00) │ ├── 场地A: 长拳-女子组 │ └── ... └── ... ``` **冲突检测结果** - 运动员时间冲突列表 - 场地超时警告 - 规则违反提示 --- ### 2. 预编排手动微调 #### 2.1 场地间移动 - **功能**:多选一部分运动员,从场地A移动到场地B - **约束检测**: - ✅ 检测目标场地时间冲突 - ✅ 检测运动员时间冲突 - ✅ 实时提示冲突信息 #### 2.2 场地内调整 - **功能**:拖拽调整运动员出场顺序 - **交互方式**:长按拖拽 - **实时反馈**:拖动时显示时间预估 #### 2.3 批量操作 - 批量删除 - 批量复制到其他时间段 - 批量调整时间偏移 --- ### 3. 确定编排结果 #### 3.1 编排文档生成 - **格式**:PDF / Excel - **内容**: - 完整赛程表(按时间顺序) - 场地分配表(按场地分组) - 运动员出场通知单(按队伍/运动员分组) #### 3.2 发布功能 - 上传到官方页面供查看 - 生成公开访问链接 - 支持下载打印 #### 3.3 启动比赛流程 - 基于编排表初始化比赛状态 - 生成签到列表 - 通知相关裁判和运动员 --- ### 4. 比赛中临时调整 #### 4.1 检录长权限 - 查看当前场地编排情况 - 手动调整出场顺序 - 临时替换运动员 #### 4.2 调整范围 - ✅ 当前时间段及未来时间段 - ❌ 不可修改已完成的比赛 #### 4.3 调整记录 - 记录所有调整操作 - 标注调整原因 - 审计日志 --- ## 🗄️ 数据库设计 ### 新增表 #### 1. martial_schedule_plan(编排方案表) ```sql CREATE TABLE martial_schedule_plan ( id BIGINT PRIMARY KEY AUTO_INCREMENT, competition_id BIGINT NOT NULL COMMENT '赛事ID', plan_name VARCHAR(100) COMMENT '方案名称', plan_type TINYINT COMMENT '方案类型: 1-自动生成, 2-手动调整', status TINYINT COMMENT '状态: 0-草稿, 1-已确认, 2-已发布', -- 编排参数 start_time DATETIME COMMENT '比赛开始时间', end_time DATETIME COMMENT '比赛结束时间', venue_count INT COMMENT '场地数量', time_slot_duration INT COMMENT '时间段长度(分钟)', -- 规则配置 rules JSON COMMENT '编排规则配置', -- 统计信息 total_matches INT COMMENT '总场次', conflict_count INT COMMENT '冲突数量', -- 审计字段 created_by BIGINT COMMENT '创建人', approved_by BIGINT COMMENT '审批人', approved_time DATETIME COMMENT '审批时间', published_time DATETIME COMMENT '发布时间', -- 标准字段 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, is_deleted TINYINT DEFAULT 0, INDEX idx_competition (competition_id), INDEX idx_status (status) ) COMMENT='编排方案表'; ``` #### 2. martial_schedule_slot(时间槽表) ```sql CREATE TABLE martial_schedule_slot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plan_id BIGINT NOT NULL COMMENT '编排方案ID', venue_id BIGINT COMMENT '场地ID', -- 时间信息 slot_date DATE COMMENT '比赛日期', start_time TIME COMMENT '开始时间', end_time TIME COMMENT '结束时间', duration INT COMMENT '时长(分钟)', -- 项目信息 project_id BIGINT COMMENT '项目ID', category VARCHAR(50) COMMENT '组别', -- 排序 sort_order INT COMMENT '排序号', -- 状态 status TINYINT COMMENT '状态: 0-未开始, 1-进行中, 2-已完成', -- 标准字段 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, is_deleted TINYINT DEFAULT 0, INDEX idx_plan (plan_id), INDEX idx_venue (venue_id), INDEX idx_time (slot_date, start_time), INDEX idx_project (project_id) ) COMMENT='编排时间槽表'; ``` #### 3. martial_schedule_athlete_slot(运动员-时间槽关联表) ```sql CREATE TABLE martial_schedule_athlete_slot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, slot_id BIGINT NOT NULL COMMENT '时间槽ID', athlete_id BIGINT NOT NULL COMMENT '运动员ID', -- 出场信息 appearance_order INT COMMENT '出场顺序', estimated_time TIME COMMENT '预计出场时间', -- 状态 check_in_status TINYINT COMMENT '签到状态: 0-未签到, 1-已签到', performance_status TINYINT COMMENT '比赛状态: 0-未开始, 1-进行中, 2-已完成', -- 调整记录 is_adjusted TINYINT DEFAULT 0 COMMENT '是否调整过', adjust_note VARCHAR(200) COMMENT '调整备注', -- 标准字段 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, is_deleted TINYINT DEFAULT 0, INDEX idx_slot (slot_id), INDEX idx_athlete (athlete_id), INDEX idx_order (appearance_order), UNIQUE KEY uk_slot_athlete (slot_id, athlete_id) ) COMMENT='运动员时间槽关联表'; ``` #### 4. martial_schedule_conflict(编排冲突记录表) ```sql CREATE TABLE martial_schedule_conflict ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plan_id BIGINT NOT NULL COMMENT '编排方案ID', conflict_type TINYINT COMMENT '冲突类型: 1-时间冲突, 2-场地冲突, 3-规则违反', severity TINYINT COMMENT '严重程度: 1-警告, 2-错误, 3-致命', -- 冲突详情 entity_type VARCHAR(20) COMMENT '实体类型: athlete/venue/slot', entity_id BIGINT COMMENT '实体ID', conflict_description TEXT COMMENT '冲突描述', -- 解决状态 is_resolved TINYINT DEFAULT 0 COMMENT '是否已解决', resolve_method VARCHAR(100) COMMENT '解决方法', -- 标准字段 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, is_deleted TINYINT DEFAULT 0, INDEX idx_plan (plan_id), INDEX idx_type (conflict_type), INDEX idx_resolved (is_resolved) ) COMMENT='编排冲突记录表'; ``` #### 5. martial_schedule_adjustment_log(编排调整日志表) ```sql CREATE TABLE martial_schedule_adjustment_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plan_id BIGINT NOT NULL COMMENT '编排方案ID', -- 操作信息 action_type VARCHAR(20) COMMENT '操作类型: move/swap/delete/insert', operator_id BIGINT COMMENT '操作人ID', operator_name VARCHAR(50) COMMENT '操作人姓名', operator_role VARCHAR(20) COMMENT '操作人角色: admin/referee', -- 变更详情 before_data JSON COMMENT '变更前数据', after_data JSON COMMENT '变更后数据', reason VARCHAR(200) COMMENT '调整原因', -- 时间 action_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', INDEX idx_plan (plan_id), INDEX idx_operator (operator_id), INDEX idx_time (action_time) ) COMMENT='编排调整日志表'; ``` --- ## 🔧 技术实现方案 ### 1. 自动编排算法 #### 1.1 算法选择 - **回溯法(Backtracking)**:适合小规模(< 100场次) - **遗传算法(Genetic Algorithm)**:适合中大规模(100-1000场次) - **约束满足问题(CSP)**:结合启发式搜索 **推荐方案**:分阶段编排 1. **Phase 1**:集体项目优先分配(硬约束) 2. **Phase 2**:个人项目按类别分组分配 3. **Phase 3**:冲突检测与调整 4. **Phase 4**:优化(时间均衡、休息时间) #### 1.2 算法伪代码 ```java public SchedulePlan autoSchedule(Competition competition) { // 1. 数据准备 List projects = loadProjects(competition); List venues = loadVenues(competition); List timeSlots = generateTimeSlots(competition.getStartTime(), competition.getEndTime(), 30); // 30分钟一个时间槽 // 2. 项目排序(集体项目优先) projects.sort((a, b) -> { if (a.isGroupProject() != b.isGroupProject()) { return a.isGroupProject() ? -1 : 1; } return a.getCategory().compareTo(b.getCategory()); }); // 3. 初始化编排表 ScheduleMatrix matrix = new ScheduleMatrix(timeSlots, venues); // 4. 逐个项目分配 for (Project project : projects) { List athletes = getAthletes(project); // 4.1 寻找可用的时间-场地槽 for (TimeSlot time : timeSlots) { for (Venue venue : venues) { if (canAssign(matrix, project, athletes, time, venue)) { assign(matrix, project, athletes, time, venue); break; } } } } // 5. 冲突检测 List conflicts = detectConflicts(matrix); // 6. 冲突解决(尝试调整) if (!conflicts.isEmpty()) { resolveConflicts(matrix, conflicts); } // 7. 优化 optimizeSchedule(matrix); // 8. 保存方案 return savePlan(matrix); } // 检查是否可分配 private boolean canAssign(ScheduleMatrix matrix, Project project, List athletes, TimeSlot time, Venue venue) { // 检查场地是否空闲 if (matrix.isVenueOccupied(venue, time)) { return false; } // 检查运动员是否有冲突 for (Athlete athlete : athletes) { if (matrix.isAthleteOccupied(athlete, time)) { return false; } } // 检查时间是否足够 int requiredMinutes = project.getDuration() * athletes.size(); if (time.getAvailableMinutes() < requiredMinutes) { return false; } return true; } ``` #### 1.3 时间复杂度分析 - **最坏情况**:O(n! × m × k) - n: 项目数 - m: 场地数 - k: 时间槽数 - **优化后**:O(n × m × k × log n) ### 2. 冲突检测机制 #### 2.1 冲突类型 **硬冲突(必须解决)** 1. **运动员时间冲突**:同一运动员被分配到同一时间的不同场地 2. **场地超载**:同一场地同一时间分配了多个项目 **软冲突(警告提示)** 1. **休息时间不足**:运动员连续两场比赛间隔 < 30分钟 2. **场地时间不均**:某个场地使用率过高或过低 3. **项目分散**:同类项目未连续安排 #### 2.2 冲突检测SQL ```sql -- 检测运动员时间冲突 SELECT a.athlete_id, a.name, COUNT(*) as conflict_count, GROUP_CONCAT(s.slot_date, ' ', s.start_time) as conflict_times FROM martial_schedule_athlete_slot sas1 JOIN martial_schedule_athlete_slot sas2 ON sas1.athlete_id = sas2.athlete_id AND sas1.id != sas2.id JOIN martial_schedule_slot s1 ON sas1.slot_id = s1.id JOIN martial_schedule_slot s2 ON sas2.slot_id = s2.id JOIN martial_athlete a ON sas1.athlete_id = a.id WHERE s1.slot_date = s2.slot_date AND s1.start_time < s2.end_time AND s2.start_time < s1.end_time GROUP BY a.athlete_id, a.name HAVING conflict_count > 0; ``` ### 3. 手动调整实现 #### 3.1 场地间移动API ```java /** * 批量移动运动员到其他场地 */ @PostMapping("/schedule/move") public R moveAthletes( @RequestParam List athleteIds, @RequestParam Long fromSlotId, @RequestParam Long toSlotId, @RequestParam String reason ) { // 1. 冲突检测 List conflicts = scheduleService.checkMoveConflicts( athleteIds, fromSlotId, toSlotId ); if (!conflicts.isEmpty()) { return R.fail("存在冲突:" + conflicts); } // 2. 执行移动 boolean success = scheduleService.moveAthletes( athleteIds, fromSlotId, toSlotId ); // 3. 记录日志 scheduleService.logAdjustment("move", athleteIds, reason); return R.status(success); } ``` #### 3.2 拖拽排序API ```java /** * 调整场地内运动员出场顺序 */ @PostMapping("/schedule/reorder") public R reorderAthletes( @RequestParam Long slotId, @RequestBody List newOrder ) { // newOrder: [{athleteId: 1, order: 1}, {athleteId: 2, order: 2}, ...] return R.data(scheduleService.updateAppearanceOrder(slotId, newOrder)); } ``` ### 4. 编排文档导出 #### 4.1 完整赛程表(PDF) ```java /** * 导出完整赛程表 */ public void exportFullSchedule(Long planId, HttpServletResponse response) { SchedulePlan plan = getPlan(planId); // 按时间顺序获取所有时间槽 List slots = scheduleService.getAllSlots(planId); // 生成PDF PDFGenerator.builder() .title(plan.getCompetitionName() + " 完整赛程表") .addSection("时间安排", buildTimeTable(slots)) .addSection("场地分配", buildVenueTable(slots)) .generate(response); } ``` #### 4.2 运动员出场通知单(Excel) ```java /** * 按队伍导出运动员出场通知 */ public void exportAthleteNotice(Long planId, Long teamId) { List schedules = scheduleService.getAthleteSchedulesByTeam(planId, teamId); // 按运动员分组 Map> grouped = schedules.stream().collect(Collectors.groupingBy( AthleteScheduleVO::getAthleteId )); // 生成Excel ExcelUtil.export(response, "运动员出场通知", ...); } ``` --- ## 🧪 测试用例 ### 1. 自动编排测试 #### Test Case 1.1: 基础编排 ```java @Test @DisplayName("测试基础自动编排 - 无冲突场景") void testAutoSchedule_NoConflict() { // Given: 3个项目,2个场地,足够的时间 Competition competition = createCompetition( projects: 3, venues: 2, timeSlots: 10 ); // When: 执行自动编排 SchedulePlan plan = scheduleService.autoSchedule(competition); // Then: 所有项目都被分配,无冲突 assertEquals(3, plan.getAssignedProjectCount()); assertEquals(0, plan.getConflictCount()); } ``` #### Test Case 1.2: 集体项目优先 ```java @Test @DisplayName("测试集体项目优先规则") void testAutoSchedule_GroupProjectFirst() { // Given: 2个集体项目,3个个人项目 List projects = Arrays.asList( createProject("太极拳", ProjectType.INDIVIDUAL), createProject("集体长拳", ProjectType.GROUP), createProject("剑术", ProjectType.INDIVIDUAL), createProject("集体太极", ProjectType.GROUP), createProject("棍术", ProjectType.INDIVIDUAL) ); // When: 自动编排 SchedulePlan plan = scheduleService.autoSchedule(projects); // Then: 集体项目应该在最前面 List slots = plan.getSlots(); assertTrue(slots.get(0).getProject().isGroupProject()); assertTrue(slots.get(1).getProject().isGroupProject()); } ``` ### 2. 冲突检测测试 #### Test Case 2.1: 运动员时间冲突 ```java @Test @DisplayName("测试运动员时间冲突检测") void testConflictDetection_AthleteTimeConflict() { // Given: 同一运动员被分配到两个重叠的时间槽 Athlete athlete = createAthlete("张三"); ScheduleSlot slot1 = createSlot("09:00", "09:30", venueA); ScheduleSlot slot2 = createSlot("09:15", "09:45", venueB); assignAthleteToSlot(athlete, slot1); assignAthleteToSlot(athlete, slot2); // When: 执行冲突检测 List conflicts = scheduleService.detectConflicts(plan); // Then: 应检测到运动员时间冲突 assertEquals(1, conflicts.size()); assertEquals(ConflictType.ATHLETE_TIME_CONFLICT, conflicts.get(0).getType()); } ``` ### 3. 手动调整测试 #### Test Case 3.1: 场地间移动 ```java @Test @DisplayName("测试运动员场地间移动") void testMoveAthletes_BetweenVenues() { // Given: 运动员A在场地1 Athlete athlete = createAthlete("李四"); ScheduleSlot fromSlot = getSlot(venue1, "10:00"); ScheduleSlot toSlot = getSlot(venue2, "10:00"); assignAthleteToSlot(athlete, fromSlot); // When: 移动到场地2 boolean success = scheduleService.moveAthletes( Arrays.asList(athlete.getId()), fromSlot.getId(), toSlot.getId(), "场地调整" ); // Then: 移动成功,记录已更新 assertTrue(success); assertFalse(isAthleteInSlot(athlete, fromSlot)); assertTrue(isAthleteInSlot(athlete, toSlot)); } ``` --- ## 📊 性能指标 ### 1. 编排性能目标 - **小规模**(< 50场次):< 1秒 - **中规模**(50-200场次):< 5秒 - **大规模**(200-500场次):< 30秒 ### 2. 冲突检测性能 - 实时检测:< 100ms - 批量检测:< 1秒 ### 3. 前端交互 - 拖拽响应:< 50ms - 冲突提示:实时 --- ## 🚀 开发计划 ### Week 1: 核心算法(3天) - Day 1: 数据模型设计 + 数据库表创建 - Day 2: 自动编排算法实现 - Day 3: 冲突检测机制 ### Week 2: API开发(4天) - Day 4-5: 编排管理API(CRUD) - Day 6: 手动调整API(移动、排序) - Day 7: 冲突检测API ### Week 3: 导出与测试(3天) - Day 8: 文档导出功能(PDF/Excel) - Day 9-10: 单元测试 + 集成测试 --- ## 🔗 依赖关系 ### 前置依赖 - ✅ MartialProject(项目管理) - ✅ MartialAthlete(运动员管理) - ✅ MartialVenue(场地管理) - ✅ MartialCompetition(赛事管理) ### 后置影响 - → MartialScore(评分依赖编排结果) - → MartialResult(成绩计算依赖编排) --- ## ⚠️ 技术挑战 ### 1. 算法复杂度 - **问题**:大规模编排(500+场次)性能瓶颈 - **解决**:分阶段编排 + 缓存 + 异步处理 ### 2. 实时冲突检测 - **问题**:频繁调整时冲突检测开销大 - **解决**:增量检测 + 防抖 + WebSocket推送 ### 3. 并发调整 - **问题**:多个检录长同时调整 - **解决**:乐观锁 + 版本控制 --- ## 📌 备注 1. **优先级调整**:本功能原为P3(未来规划),现根据用户需求提升至P1 2. **分阶段实现**:先实现核心自动编排,再实现高级优化功能 3. **前端配合**:需要前端实现拖拽交互界面 4. **性能优化**:大规模赛事可能需要后台任务队列处理 --- **下一步行动**: 1. 创建数据库表 2. 实现基础编排算法 3. 开发API接口 4. 编写单元测试