# 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<Project> projects = loadProjects(competition);
    List<Venue> venues = loadVenues(competition);
    List<TimeSlot> 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<Athlete> 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<Conflict> 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<Athlete> 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<Boolean> moveAthletes(
    @RequestParam List<Long> athleteIds,
    @RequestParam Long fromSlotId,
    @RequestParam Long toSlotId,
    @RequestParam String reason
) {
    // 1. 冲突检测
    List<Conflict> 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<Boolean> reorderAthletes(
    @RequestParam Long slotId,
    @RequestBody List<AthleteOrder> 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<ScheduleSlot> 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<AthleteScheduleVO> schedules =
        scheduleService.getAthleteSchedulesByTeam(planId, teamId);

    // 按运动员分组
    Map<Long, List<AthleteScheduleVO>> 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<Project> 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<ScheduleSlot> 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<Conflict> 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. 编写单元测试
