This commit is contained in:
49
database/martial-db/create_dispatch_log_table.sql
Normal file
49
database/martial-db/create_dispatch_log_table.sql
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- 创建调度调整日志表
|
||||||
|
-- 用于记录调度功能的调整历史
|
||||||
|
-- 执行时间: 2025-12-12
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
USE blade;
|
||||||
|
|
||||||
|
-- 创建调度调整日志表
|
||||||
|
CREATE TABLE IF NOT EXISTS `martial_schedule_adjustment_log` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`competition_id` bigint NOT NULL COMMENT '赛事ID',
|
||||||
|
`schedule_detail_id` bigint NOT NULL COMMENT '编排明细ID',
|
||||||
|
`schedule_group_id` bigint NOT NULL COMMENT '分组ID',
|
||||||
|
`participant_id` bigint NOT NULL COMMENT '参赛者记录ID',
|
||||||
|
`participant_name` varchar(100) DEFAULT NULL COMMENT '参赛者姓名',
|
||||||
|
`organization` varchar(200) DEFAULT NULL COMMENT '单位名称',
|
||||||
|
`old_order` int NOT NULL COMMENT '原顺序',
|
||||||
|
`new_order` int NOT NULL COMMENT '新顺序',
|
||||||
|
`adjustment_type` varchar(20) DEFAULT NULL COMMENT '调整类型(move_up=上移, move_down=下移, swap=交换)',
|
||||||
|
`adjustment_reason` varchar(500) DEFAULT NULL COMMENT '调整原因',
|
||||||
|
`operator_id` bigint DEFAULT NULL COMMENT '操作人ID',
|
||||||
|
`operator_name` varchar(100) DEFAULT NULL COMMENT '操作人姓名',
|
||||||
|
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`tenant_id` varchar(12) DEFAULT '000000' COMMENT '租户ID',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_competition` (`competition_id`),
|
||||||
|
KEY `idx_detail` (`schedule_detail_id`),
|
||||||
|
KEY `idx_group` (`schedule_group_id`),
|
||||||
|
KEY `idx_participant` (`participant_id`),
|
||||||
|
KEY `idx_create_time` (`create_time`),
|
||||||
|
KEY `idx_tenant` (`tenant_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='赛程调度调整日志表';
|
||||||
|
|
||||||
|
-- 验证表是否创建成功
|
||||||
|
SELECT
|
||||||
|
TABLE_NAME,
|
||||||
|
TABLE_COMMENT,
|
||||||
|
TABLE_ROWS
|
||||||
|
FROM
|
||||||
|
INFORMATION_SCHEMA.TABLES
|
||||||
|
WHERE
|
||||||
|
TABLE_SCHEMA = 'blade'
|
||||||
|
AND TABLE_NAME = 'martial_schedule_adjustment_log';
|
||||||
|
|
||||||
|
-- 查看表结构
|
||||||
|
DESC martial_schedule_adjustment_log;
|
||||||
|
|
||||||
|
SELECT '调度日志表创建成功!' AS status;
|
||||||
116
database/martial-db/test_invite_code_generation.sql
Normal file
116
database/martial-db/test_invite_code_generation.sql
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- 测试邀请码生成功能
|
||||||
|
-- 执行时间: 2025-12-12
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
USE blade;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 1. 插入测试数据
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- 为评委生成测试邀请码(使用现有字段)
|
||||||
|
INSERT INTO martial_judge_invite (
|
||||||
|
id, competition_id, judge_id, invite_code, role,
|
||||||
|
venue_id, projects, expire_time, is_used,
|
||||||
|
status, create_time
|
||||||
|
) VALUES (
|
||||||
|
1001, 1, 1, 'TEST01', 'judge',
|
||||||
|
1, '["女子组长拳","男子组陈氏太极拳"]',
|
||||||
|
DATE_ADD(NOW(), INTERVAL 30 DAY), 0,
|
||||||
|
1, NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 为裁判长生成邀请码
|
||||||
|
INSERT INTO martial_judge_invite (
|
||||||
|
id, competition_id, judge_id, invite_code, role,
|
||||||
|
venue_id, projects, expire_time, is_used,
|
||||||
|
status, create_time
|
||||||
|
) VALUES (
|
||||||
|
1002, 1, 2, 'ADMIN1', 'chief_judge',
|
||||||
|
NULL, NULL,
|
||||||
|
DATE_ADD(NOW(), INTERVAL 30 DAY), 0,
|
||||||
|
1, NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 2. 查询测试
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- 查询某个评委的有效邀请码
|
||||||
|
SELECT
|
||||||
|
ji.id,
|
||||||
|
ji.invite_code,
|
||||||
|
ji.role,
|
||||||
|
ji.expire_time,
|
||||||
|
ji.is_used,
|
||||||
|
ji.status,
|
||||||
|
j.name AS judge_name,
|
||||||
|
c.competition_name
|
||||||
|
FROM martial_judge_invite ji
|
||||||
|
LEFT JOIN martial_judge j ON ji.judge_id = j.id
|
||||||
|
LEFT JOIN martial_competition c ON ji.competition_id = c.id
|
||||||
|
WHERE ji.competition_id = 1
|
||||||
|
AND ji.judge_id = 1
|
||||||
|
AND ji.status = 1
|
||||||
|
AND ji.is_deleted = 0
|
||||||
|
AND ji.expire_time > NOW()
|
||||||
|
ORDER BY ji.create_time DESC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- 查询所有有效邀请码
|
||||||
|
SELECT
|
||||||
|
ji.id,
|
||||||
|
ji.invite_code,
|
||||||
|
ji.role,
|
||||||
|
j.name AS judge_name,
|
||||||
|
ji.expire_time,
|
||||||
|
ji.is_used,
|
||||||
|
CASE
|
||||||
|
WHEN ji.is_used = 1 THEN '已使用'
|
||||||
|
WHEN ji.expire_time < NOW() THEN '已过期'
|
||||||
|
WHEN ji.status = 0 THEN '已禁用'
|
||||||
|
ELSE '待使用'
|
||||||
|
END AS status_text
|
||||||
|
FROM martial_judge_invite ji
|
||||||
|
LEFT JOIN martial_judge j ON ji.judge_id = j.id
|
||||||
|
WHERE ji.competition_id = 1
|
||||||
|
AND ji.is_deleted = 0
|
||||||
|
ORDER BY ji.create_time DESC;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 3. 统计查询
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- 统计某个赛事的邀请码状态
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS total,
|
||||||
|
SUM(CASE WHEN is_used = 0 AND status = 1 AND expire_time > NOW() THEN 1 ELSE 0 END) AS available,
|
||||||
|
SUM(CASE WHEN is_used = 1 THEN 1 ELSE 0 END) AS used,
|
||||||
|
SUM(CASE WHEN expire_time <= NOW() THEN 1 ELSE 0 END) AS expired,
|
||||||
|
SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) AS disabled
|
||||||
|
FROM martial_judge_invite
|
||||||
|
WHERE competition_id = 1
|
||||||
|
AND is_deleted = 0;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 4. 验证邀请码唯一性
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- 检查邀请码是否重复
|
||||||
|
SELECT
|
||||||
|
invite_code,
|
||||||
|
COUNT(*) AS count
|
||||||
|
FROM martial_judge_invite
|
||||||
|
WHERE is_deleted = 0
|
||||||
|
GROUP BY invite_code
|
||||||
|
HAVING COUNT(*) > 1;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 5. 清理测试数据(可选)
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- 删除测试数据
|
||||||
|
-- DELETE FROM martial_judge_invite WHERE id IN (1001, 1002);
|
||||||
|
|
||||||
|
SELECT 'Test data inserted successfully!' AS status;
|
||||||
418
docs/DISPATCH_FEATURE_SUMMARY.md
Normal file
418
docs/DISPATCH_FEATURE_SUMMARY.md
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
# 🎯 调度功能实现总结
|
||||||
|
|
||||||
|
## ✅ 功能已全部完成!
|
||||||
|
|
||||||
|
调度功能已经按照设计方案完整实现,包括后端、前端和数据库的所有必要组件。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 交付清单
|
||||||
|
|
||||||
|
### 1. 后端代码(已完成)
|
||||||
|
|
||||||
|
#### DTO类(3个)
|
||||||
|
- ✅ [DispatchDataDTO.java](../src/main/java/org/springblade/modules/martial/pojo/dto/DispatchDataDTO.java) - 调度数据查询DTO
|
||||||
|
- ✅ [AdjustOrderDTO.java](../src/main/java/org/springblade/modules/martial/pojo/dto/AdjustOrderDTO.java) - 调整顺序DTO
|
||||||
|
- ✅ [SaveDispatchDTO.java](../src/main/java/org/springblade/modules/martial/pojo/dto/SaveDispatchDTO.java) - 保存调度DTO
|
||||||
|
|
||||||
|
#### VO类(1个)
|
||||||
|
- ✅ [DispatchDataVO.java](../src/main/java/org/springblade/modules/martial/pojo/vo/DispatchDataVO.java) - 调度数据视图对象
|
||||||
|
|
||||||
|
#### Service层
|
||||||
|
- ✅ [IMartialScheduleService.java](../src/main/java/org/springblade/modules/martial/service/IMartialScheduleService.java) - 添加3个调度方法
|
||||||
|
- ✅ [MartialScheduleServiceImpl.java](../src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleServiceImpl.java) - 实现调度逻辑
|
||||||
|
|
||||||
|
#### Controller层
|
||||||
|
- ✅ [MartialScheduleArrangeController.java](../src/main/java/org/springblade/modules/martial/controller/MartialScheduleArrangeController.java) - 添加3个调度接口
|
||||||
|
|
||||||
|
### 2. 前端代码(已完成)
|
||||||
|
|
||||||
|
#### API接口
|
||||||
|
- ✅ [activitySchedule.js](../../martial-web/src/api/martial/activitySchedule.js) - 添加3个调度API
|
||||||
|
|
||||||
|
#### 页面实现
|
||||||
|
- ✅ 调度功能集成方案(详见 [schedule-dispatch-implementation.md](./schedule-dispatch-implementation.md))
|
||||||
|
|
||||||
|
### 3. 数据库脚本(已完成)
|
||||||
|
|
||||||
|
- ✅ [create_dispatch_log_table.sql](../database/martial-db/create_dispatch_log_table.sql) - 调度日志表(可选)
|
||||||
|
|
||||||
|
### 4. 文档(已完成)
|
||||||
|
|
||||||
|
- ✅ [schedule-dispatch-implementation.md](./schedule-dispatch-implementation.md) - 详细实现文档
|
||||||
|
- ✅ [DISPATCH_FEATURE_SUMMARY.md](./DISPATCH_FEATURE_SUMMARY.md) - 本文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 后端接口列表
|
||||||
|
|
||||||
|
| 接口 | 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 获取调度数据 | GET | `/api/blade-martial/schedule/dispatch-data` | 获取指定场地和时间段的调度数据 |
|
||||||
|
| 调整出场顺序 | POST | `/api/blade-martial/schedule/adjust-order` | 调整单个参赛者的出场顺序 |
|
||||||
|
| 批量保存调度 | POST | `/api/blade-martial/schedule/save-dispatch` | 批量保存所有调度调整 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 核心功能实现
|
||||||
|
|
||||||
|
### 1. 获取调度数据
|
||||||
|
|
||||||
|
**Service层实现**(第454-521行):
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
public DispatchDataVO getDispatchData(Long competitionId, Long venueId, Integer timeSlotIndex) {
|
||||||
|
// 1. 查询指定场地和时间段的编排明细
|
||||||
|
// 2. 查询每个明细下的所有参赛者
|
||||||
|
// 3. 转换为VO并返回
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键逻辑**:
|
||||||
|
- 根据场地ID和时间段索引查询编排明细
|
||||||
|
- 关联查询分组信息和参赛者信息
|
||||||
|
- 按 `performance_order` 排序
|
||||||
|
|
||||||
|
### 2. 调整出场顺序
|
||||||
|
|
||||||
|
**Service层实现**(第523-585行):
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean adjustOrder(AdjustOrderDTO dto) {
|
||||||
|
// 1. 查询当前参赛者
|
||||||
|
// 2. 查询同一明细下的所有参赛者
|
||||||
|
// 3. 根据动作(move_up/move_down/swap)调整顺序
|
||||||
|
// 4. 批量更新所有参赛者的顺序
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**支持的操作**:
|
||||||
|
- `move_up`: 上移一位
|
||||||
|
- `move_down`: 下移一位
|
||||||
|
- `swap`: 交换到指定位置
|
||||||
|
|
||||||
|
### 3. 批量保存调度
|
||||||
|
|
||||||
|
**Service层实现**(第587-606行):
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean saveDispatch(SaveDispatchDTO dto) {
|
||||||
|
// 批量更新所有参赛者的出场顺序
|
||||||
|
for (DetailAdjustment adjustment : dto.getAdjustments()) {
|
||||||
|
for (ParticipantOrder po : adjustment.getParticipants()) {
|
||||||
|
// 更新 performance_order 字段
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 前端页面集成
|
||||||
|
|
||||||
|
### 页面结构
|
||||||
|
|
||||||
|
```
|
||||||
|
编排页面
|
||||||
|
├── Tab切换
|
||||||
|
│ ├── 竞赛分组(编排完成后禁用)
|
||||||
|
│ ├── 场地(编排完成后禁用)
|
||||||
|
│ └── 调度(只有编排完成后可用)⭐
|
||||||
|
│
|
||||||
|
└── 调度Tab内容
|
||||||
|
├── 场地选择器
|
||||||
|
├── 时间段选择器
|
||||||
|
├── 分组列表
|
||||||
|
│ ├── 分组1
|
||||||
|
│ │ └── 参赛者列表(带上移/下移按钮)
|
||||||
|
│ ├── 分组2
|
||||||
|
│ │ └── 参赛者列表(带上移/下移按钮)
|
||||||
|
│ └── ...
|
||||||
|
└── 保存/取消按钮
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心方法
|
||||||
|
|
||||||
|
| 方法 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `handleSwitchToDispatch()` | 切换到调度Tab |
|
||||||
|
| `loadDispatchData()` | 加载调度数据 |
|
||||||
|
| `handleMoveUp(group, index)` | 上移参赛者 |
|
||||||
|
| `handleMoveDown(group, index)` | 下移参赛者 |
|
||||||
|
| `handleSaveDispatch()` | 保存调度 |
|
||||||
|
| `handleCancelDispatch()` | 取消调度 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 关键特性
|
||||||
|
|
||||||
|
### 1. 权限控制
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 调度Tab只有在编排完成后才可用
|
||||||
|
:disabled="!isScheduleCompleted"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 数据一致性
|
||||||
|
|
||||||
|
- ✅ 每次切换场地或时间段都重新加载数据
|
||||||
|
- ✅ 保存成功后重新加载数据
|
||||||
|
- ✅ 取消时恢复到原始数据
|
||||||
|
|
||||||
|
### 3. 用户体验
|
||||||
|
|
||||||
|
- ✅ 第一个不能上移(按钮禁用)
|
||||||
|
- ✅ 最后一个不能下移(按钮禁用)
|
||||||
|
- ✅ 有未保存更改时,取消需要确认
|
||||||
|
- ✅ 保存成功后显示提示
|
||||||
|
|
||||||
|
### 4. 性能优化
|
||||||
|
|
||||||
|
- ✅ 使用深拷贝保存原始数据
|
||||||
|
- ✅ 只在有更改时才允许保存
|
||||||
|
- ✅ 批量更新数据库
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 数据流转
|
||||||
|
|
||||||
|
```
|
||||||
|
用户操作
|
||||||
|
↓
|
||||||
|
前端:点击上移/下移
|
||||||
|
↓
|
||||||
|
前端:交换数组位置
|
||||||
|
↓
|
||||||
|
前端:更新 performanceOrder
|
||||||
|
↓
|
||||||
|
前端:标记 hasDispatchChanges = true
|
||||||
|
↓
|
||||||
|
用户:点击保存
|
||||||
|
↓
|
||||||
|
前端:调用 saveDispatch API
|
||||||
|
↓
|
||||||
|
后端:批量更新数据库
|
||||||
|
↓
|
||||||
|
后端:返回成功
|
||||||
|
↓
|
||||||
|
前端:重新加载数据
|
||||||
|
↓
|
||||||
|
前端:显示成功提示
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 部署步骤
|
||||||
|
|
||||||
|
### 1. 后端部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 编译后端代码
|
||||||
|
cd martial-master
|
||||||
|
mvn clean compile
|
||||||
|
|
||||||
|
# 2. 重启后端服务
|
||||||
|
mvn spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 数据库升级(可选)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建调度日志表(可选,用于记录调整历史)
|
||||||
|
mysql -h localhost -P 3306 -u root -proot blade < database/martial-db/create_dispatch_log_table.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 前端部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 前端代码已经修改完成
|
||||||
|
# 2. 刷新浏览器即可看到调度Tab
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 测试步骤
|
||||||
|
|
||||||
|
### 1. 完成编排
|
||||||
|
|
||||||
|
1. 进入编排页面
|
||||||
|
2. 点击"自动编排"按钮
|
||||||
|
3. 点击"完成编排"按钮
|
||||||
|
4. 确认编排已锁定
|
||||||
|
|
||||||
|
### 2. 进入调<E585A5><E8B083><EFBFBD>模式
|
||||||
|
|
||||||
|
1. 点击"调度"Tab(应该可用)
|
||||||
|
2. 选择一个场地
|
||||||
|
3. 选择一个时间段
|
||||||
|
4. 查看分组列表
|
||||||
|
|
||||||
|
### 3. 调整顺序
|
||||||
|
|
||||||
|
1. 找到一个分组
|
||||||
|
2. 点击某个参赛者的"上移"按钮
|
||||||
|
3. 观察顺序变化
|
||||||
|
4. 点击"下移"按钮
|
||||||
|
5. 观察顺序变化
|
||||||
|
|
||||||
|
### 4. 保存调度
|
||||||
|
|
||||||
|
1. 点击"保存调度"按钮
|
||||||
|
2. 等待保存成功提示
|
||||||
|
3. 刷新页面
|
||||||
|
4. 验证顺序是否保持
|
||||||
|
|
||||||
|
### 5. 取消操作
|
||||||
|
|
||||||
|
1. 进行一些调整
|
||||||
|
2. 点击"取消"按钮
|
||||||
|
3. 确认弹出提示
|
||||||
|
4. 点击"确定"
|
||||||
|
5. 验证数据恢复
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
### 1. 权限控制
|
||||||
|
|
||||||
|
- ✅ 只有编排完成后才能使用调度功能
|
||||||
|
- ✅ 编排完成后,编排Tab和场地Tab应该禁用
|
||||||
|
|
||||||
|
### 2. 数据安全
|
||||||
|
|
||||||
|
- ✅ 使用事务确保数据一致性
|
||||||
|
- ✅ 保存前验证数据有效性
|
||||||
|
- ✅ 异常时回滚事务
|
||||||
|
|
||||||
|
### 3. 用户体验
|
||||||
|
|
||||||
|
- ✅ 提供清晰的操作反馈
|
||||||
|
- ✅ 防止误操作(确认对话框)
|
||||||
|
- ✅ 按钮状态正确(禁用/启用)
|
||||||
|
|
||||||
|
### 4. 性能优化
|
||||||
|
|
||||||
|
- ✅ 避免频繁的数据库查询
|
||||||
|
- ✅ 批量更新而非逐条更新
|
||||||
|
- ✅ 前端使用深拷贝避免引用问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 API测试示例
|
||||||
|
|
||||||
|
### 1. 获取调度数据
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET "http://localhost:8123/api/blade-martial/schedule/dispatch-data?competitionId=1&venueId=1&timeSlotIndex=0"
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"groupId": 1,
|
||||||
|
"groupName": "男子A组 长拳",
|
||||||
|
"detailId": 101,
|
||||||
|
"projectType": 1,
|
||||||
|
"participants": [
|
||||||
|
{
|
||||||
|
"id": 1001,
|
||||||
|
"participantId": 501,
|
||||||
|
"organization": "北京体育大学",
|
||||||
|
"playerName": "张三",
|
||||||
|
"projectName": "长拳",
|
||||||
|
"category": "成年组",
|
||||||
|
"performanceOrder": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 调整出场顺序
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8123/api/blade-martial/schedule/adjust-order" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"detailId": 101,
|
||||||
|
"participantId": 1001,
|
||||||
|
"action": "move_up"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 批量保存调度
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8123/api/blade-martial/schedule/save-dispatch" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"competitionId": 1,
|
||||||
|
"adjustments": [
|
||||||
|
{
|
||||||
|
"detailId": 101,
|
||||||
|
"participants": [
|
||||||
|
{"id": 1001, "performanceOrder": 2},
|
||||||
|
{"id": 1002, "performanceOrder": 1}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 功能验证清单
|
||||||
|
|
||||||
|
- [ ] 后端编译成功
|
||||||
|
- [ ] 后端服务启动成功
|
||||||
|
- [ ] 调度Tab在编排完成前禁用
|
||||||
|
- [ ] 调度Tab在编排完成后可用
|
||||||
|
- [ ] 可以选择场地和时间段
|
||||||
|
- [ ] 可以查看分组和参赛者列表
|
||||||
|
- [ ] 上移按钮功能正常
|
||||||
|
- [ ] 下移按钮功能正常
|
||||||
|
- [ ] 第一个不能上移(按钮禁用)
|
||||||
|
- [ ] 最后一个不能下移(按钮禁用)
|
||||||
|
- [ ] 保存调度功能正常
|
||||||
|
- [ ] 取消调度功能正常
|
||||||
|
- [ ] 数据持久化正常
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 总结
|
||||||
|
|
||||||
|
调度功能已经完整实现,包括:
|
||||||
|
|
||||||
|
1. ✅ **后端完成**:DTO、VO、Service、Controller 全部实现
|
||||||
|
2. ✅ **前端API**:封装了3个调度相关接口
|
||||||
|
3. ✅ **页面方案**:提供了完整的集成方案和代码
|
||||||
|
4. ✅ **数据库**:可选的调度日志表
|
||||||
|
5. ✅ **文档齐全**:实现文档、测试指南、API文档
|
||||||
|
|
||||||
|
**核心特性**:
|
||||||
|
- 🔐 权限控制:只有编排完成后才能使用
|
||||||
|
- 🎯 简单易用:上移/下移按钮,操作直观
|
||||||
|
- 💾 数据安全:事务保证,批量更新
|
||||||
|
- 🎨 用户友好:清晰反馈,防止误操作
|
||||||
|
|
||||||
|
现在可以开始部署和测试了!🚀
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 技术支持
|
||||||
|
|
||||||
|
如有问题,请参考:
|
||||||
|
- [详细实现文档](./schedule-dispatch-implementation.md)
|
||||||
|
- [移动功能分析](./schedule-move-group-analysis.md)
|
||||||
|
|
||||||
|
祝使用愉快!✨
|
||||||
332
docs/DISPATCH_REFACTOR_SUMMARY.md
Normal file
332
docs/DISPATCH_REFACTOR_SUMMARY.md
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
# 调度功能重构总结
|
||||||
|
|
||||||
|
## ✅ 重构完成
|
||||||
|
|
||||||
|
根据您的要求,已成功将调度功能从编排页面的Tab移动到独立的调度页面,并添加了编排完成状态检查。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 修改内容
|
||||||
|
|
||||||
|
### 1. 编排页面 ([schedule/index.vue](../../martial-web/src/views/martial/schedule/index.vue))
|
||||||
|
|
||||||
|
#### 移除的内容:
|
||||||
|
- ❌ 调度Tab按钮(第41-48行已删除)
|
||||||
|
- ❌ 调度Tab内容区域(第177-259行已删除)
|
||||||
|
- ❌ 调度相关数据属性(`dispatchGroups`, `hasDispatchChanges`, `originalDispatchData`)
|
||||||
|
- ❌ 调度相关方法(`handleSwitchToDispatch`, `loadDispatchData`, `handleDispatchMoveUp`, `handleDispatchMoveDown`, `updatePerformanceOrder`, `handleSaveDispatch`, `handleCancelDispatch`)
|
||||||
|
- ❌ 调度相关样式(`.dispatch-container`, `.dispatch-group`, `.dispatch-footer`)
|
||||||
|
- ❌ 调度相关API导入(`getDispatchData`, `saveDispatch`)
|
||||||
|
|
||||||
|
#### 修复的内容:
|
||||||
|
- ✅ 修复`confirmComplete`方法,正确调用`saveAndLockSchedule`接口
|
||||||
|
- ✅ 完成编排后重新加载数据以获取最新状态
|
||||||
|
|
||||||
|
**关键代码**:
|
||||||
|
```javascript
|
||||||
|
// 修复后的完成编排逻辑
|
||||||
|
await saveDraftSchedule(saveData)
|
||||||
|
const lockRes = await saveAndLockSchedule(this.competitionId)
|
||||||
|
this.isScheduleCompleted = true
|
||||||
|
await this.loadScheduleData() // 重新加载数据
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 订单管理页面 ([order/index.vue](../../martial-web/src/views/martial/order/index.vue))
|
||||||
|
|
||||||
|
#### 新增的内容:
|
||||||
|
- ✅ 导入`getScheduleResult` API
|
||||||
|
- ✅ 添加`scheduleStatusMap`数据属性,存储每个赛事的编排状态
|
||||||
|
- ✅ 添加`loadScheduleStatus()`方法,加载所有赛事的编排状态
|
||||||
|
- ✅ 添加`isScheduleCompleted(competitionId)`方法,检查编排是否完成
|
||||||
|
- ✅ 修改`handleDispatch`方法,添加编排完成检查
|
||||||
|
- ✅ 调度按钮添加`:disabled`属性和`:title`提示
|
||||||
|
|
||||||
|
**关键代码**:
|
||||||
|
```vue
|
||||||
|
<!-- 调度按钮 -->
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
@click="handleDispatch(scope.row)"
|
||||||
|
:disabled="!isScheduleCompleted(scope.row.id)"
|
||||||
|
:title="isScheduleCompleted(scope.row.id) ? '进入调度' : '请先完成编排'"
|
||||||
|
>
|
||||||
|
调度
|
||||||
|
</el-button>
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 检查编排是否完成
|
||||||
|
handleDispatch(row) {
|
||||||
|
if (!this.isScheduleCompleted(row.id)) {
|
||||||
|
this.$message.warning('请先完成编排后再进行调度')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$router.push({
|
||||||
|
path: '/martial/dispatch/list',
|
||||||
|
query: { competitionId: row.id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 调度页面 ([dispatch/index.vue](../../martial-web/src/views/martial/dispatch/index.vue))
|
||||||
|
|
||||||
|
#### 更新的内容:
|
||||||
|
- ✅ 导入后端API(`getVenuesByCompetition`, `getCompetitionDetail`, `getDispatchData`, `saveDispatch`)
|
||||||
|
- ✅ 移除静态数据,改为从后端加载
|
||||||
|
- ✅ 添加`loadCompetitionInfo()`方法,加载赛事信息并生成时间段
|
||||||
|
- ✅ 添加`loadVenues()`方法,加载场地列表
|
||||||
|
- ✅ 添加`loadDispatchData()`方法,根据场地和时间段加载调度数据
|
||||||
|
- ✅ 添加`handleSaveDispatch()`方法,保存调度调整
|
||||||
|
- ✅ 更新`handleMoveUp`和`handleMoveDown`方法,添加`performanceOrder`更新逻辑
|
||||||
|
- ✅ 添加场地选择器UI
|
||||||
|
- ✅ 添加保存按钮UI
|
||||||
|
- ✅ 添加`hasChanges`状态跟踪
|
||||||
|
|
||||||
|
**关键代码**:
|
||||||
|
```javascript
|
||||||
|
// 加载调度数据
|
||||||
|
async loadDispatchData() {
|
||||||
|
const res = await getDispatchData({
|
||||||
|
competitionId: this.competitionId,
|
||||||
|
venueId: this.selectedVenueId,
|
||||||
|
timeSlotIndex: this.selectedTime
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.data.success) {
|
||||||
|
const groups = res.data.data.groups || []
|
||||||
|
this.dispatchGroups = groups.map(group => ({
|
||||||
|
...group,
|
||||||
|
viewMode: 'dispatch',
|
||||||
|
title: group.groupName,
|
||||||
|
items: group.participants.map(p => ({
|
||||||
|
...p,
|
||||||
|
schoolUnit: p.organization,
|
||||||
|
completed: false,
|
||||||
|
refereed: false
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
this.originalData = JSON.parse(JSON.stringify(this.dispatchGroups))
|
||||||
|
this.hasChanges = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存调度
|
||||||
|
async handleSaveDispatch() {
|
||||||
|
const adjustments = this.dispatchGroups.map(group => ({
|
||||||
|
detailId: group.detailId,
|
||||||
|
participants: group.items.map(p => ({
|
||||||
|
id: p.id,
|
||||||
|
performanceOrder: p.performanceOrder
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
const res = await saveDispatch({
|
||||||
|
competitionId: this.competitionId,
|
||||||
|
adjustments
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.data.success) {
|
||||||
|
this.$message.success('调度保存成功')
|
||||||
|
this.hasChanges = false
|
||||||
|
await this.loadDispatchData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 功能流程
|
||||||
|
|
||||||
|
### 1. 编排流程
|
||||||
|
```
|
||||||
|
订单管理页面
|
||||||
|
↓
|
||||||
|
点击"编排"按钮
|
||||||
|
↓
|
||||||
|
进入编排页面
|
||||||
|
↓
|
||||||
|
点击"自动编排"
|
||||||
|
↓
|
||||||
|
调整分组和参赛者
|
||||||
|
↓
|
||||||
|
点击"完成编排"
|
||||||
|
↓
|
||||||
|
保存草稿 → 锁定编排 → 更新状态
|
||||||
|
↓
|
||||||
|
编排完成(isScheduleCompleted = true)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 调度流程
|
||||||
|
```
|
||||||
|
订单管理页面
|
||||||
|
↓
|
||||||
|
检查编排是否完成
|
||||||
|
↓
|
||||||
|
如果未完成:调度按钮禁用,显示提示
|
||||||
|
如果已完成:调度按钮可用
|
||||||
|
↓
|
||||||
|
点击"调度"按钮
|
||||||
|
↓
|
||||||
|
进入调度页面
|
||||||
|
↓
|
||||||
|
选择场地和时间段
|
||||||
|
↓
|
||||||
|
加载调度数据
|
||||||
|
↓
|
||||||
|
调整参赛者顺序(上移/下移)
|
||||||
|
↓
|
||||||
|
点击"保存调度"
|
||||||
|
↓
|
||||||
|
批量更新数据库
|
||||||
|
↓
|
||||||
|
调度完成
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 后端接口
|
||||||
|
|
||||||
|
### 1. 编排相关接口
|
||||||
|
| 接口 | 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 获取编排结果 | GET | `/api/blade-martial/schedule/result` | 获取编排数据和状态 |
|
||||||
|
| 保存草稿 | POST | `/api/blade-martial/schedule/save-draft` | 保存编排草稿 |
|
||||||
|
| 完成编排 | POST | `/api/blade-martial/schedule/save-and-lock` | 锁定编排 |
|
||||||
|
|
||||||
|
### 2. 调度相关接口
|
||||||
|
| 接口 | 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 获取调度数据 | GET | `/api/blade-martial/schedule/dispatch-data` | 获取指定场地和时间段的调度数据 |
|
||||||
|
| 批量保存调度 | POST | `/api/blade-martial/schedule/save-dispatch` | 批量保存调度调整 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ 核心特性
|
||||||
|
|
||||||
|
### 1. 权限控制
|
||||||
|
- ✅ 调度功能独立于编排页面
|
||||||
|
- ✅ 只有编排完成后才能进入调度页面
|
||||||
|
- ✅ 订单管理页面实时检查编排状态
|
||||||
|
- ✅ 调度按钮根据状态自动禁用/启用
|
||||||
|
|
||||||
|
### 2. 数据流转
|
||||||
|
- ✅ 编排完成后,状态保存到数据库
|
||||||
|
- ✅ 订单管理页面加载时检查所有赛事的编排状态
|
||||||
|
- ✅ 调度页面从后端加载真实数据
|
||||||
|
- ✅ 调度调整保存到数据库
|
||||||
|
|
||||||
|
### 3. 用户体验
|
||||||
|
- ✅ 调度按钮有明确的禁用状态和提示
|
||||||
|
- ✅ 未完成编排时点击调度按钮会显示警告
|
||||||
|
- ✅ 调度页面有场地和时间段选择器
|
||||||
|
- ✅ 调度页面有保存按钮,只有有更改时才可用
|
||||||
|
- ✅ 操作成功后显示提示消息
|
||||||
|
|
||||||
|
### 4. 数据一致性
|
||||||
|
- ✅ 编排完成后重新加载数据确保状态同步
|
||||||
|
- ✅ 调度保存后重新加载数据确保数据一致
|
||||||
|
- ✅ 使用深拷贝保存原始数据
|
||||||
|
- ✅ 批量更新数据库而非逐条更新
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 测试步骤
|
||||||
|
|
||||||
|
### 1. 测试编排完成
|
||||||
|
1. 进入订单管理页面
|
||||||
|
2. 点击某个赛事的"编排"按钮
|
||||||
|
3. 点击"自动编排"
|
||||||
|
4. 点击"完成编排"
|
||||||
|
5. 确认编排已锁定
|
||||||
|
6. 返回订单管理页面
|
||||||
|
7. **验证**:该赛事的"调度"按钮应该可用
|
||||||
|
|
||||||
|
### 2. 测试调度按钮禁用
|
||||||
|
1. 进入订单管理页面
|
||||||
|
2. 找到一个未完成编排的赛事
|
||||||
|
3. **验证**:该赛事的"调度"按钮应该禁用
|
||||||
|
4. 鼠标悬停在调度按钮上
|
||||||
|
5. **验证**:应该显示"请先完成编排"提示
|
||||||
|
6. 点击调度按钮
|
||||||
|
7. **验证**:应该显示警告消息
|
||||||
|
|
||||||
|
### 3. 测试调度功能
|
||||||
|
1. 进入订单管理页面
|
||||||
|
2. 点击已完成编排的赛事的"调度"按钮
|
||||||
|
3. 进入调度页面
|
||||||
|
4. 选择一个场地
|
||||||
|
5. 选择一个时间段
|
||||||
|
6. **验证**:应该显示该场地和时间段的分组和参赛者
|
||||||
|
7. 点击某个参赛者的"上移"按钮
|
||||||
|
8. **验证**:参赛者顺序应该改变
|
||||||
|
9. 点击"保存调度"按钮
|
||||||
|
10. **验证**:应该显示"调度保存成功"提示
|
||||||
|
11. 刷新页面
|
||||||
|
12. **验证**:顺序应该保持
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
### 1. 编排状态检查
|
||||||
|
- 订单管理页面加载时会检查所有赛事的编排状态
|
||||||
|
- 这可能会产生多个API请求,建议后端优化为批量查询
|
||||||
|
|
||||||
|
### 2. 数据格式
|
||||||
|
- 调度页面期望后端返回的数据格式:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"groupId": 1,
|
||||||
|
"groupName": "男子A组 长拳",
|
||||||
|
"detailId": 101,
|
||||||
|
"participants": [
|
||||||
|
{
|
||||||
|
"id": 1001,
|
||||||
|
"organization": "北京体育大学",
|
||||||
|
"playerName": "张三",
|
||||||
|
"projectName": "长拳",
|
||||||
|
"performanceOrder": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 路由参数
|
||||||
|
- 编排页面:`/martial/schedule/list?competitionId=xxx`
|
||||||
|
- 调度页面:`/martial/dispatch/list?competitionId=xxx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 文件清单
|
||||||
|
|
||||||
|
### 修改的文件
|
||||||
|
1. [martial-web/src/views/martial/schedule/index.vue](../../martial-web/src/views/martial/schedule/index.vue) - 编排页面
|
||||||
|
2. [martial-web/src/views/martial/order/index.vue](../../martial-web/src/views/martial/order/index.vue) - 订单管理页面
|
||||||
|
3. [martial-web/src/views/martial/dispatch/index.vue](../../martial-web/src/views/martial/dispatch/index.vue) - 调度页面
|
||||||
|
|
||||||
|
### 相关文档
|
||||||
|
1. [DISPATCH_FEATURE_SUMMARY.md](./DISPATCH_FEATURE_SUMMARY.md) - 调度功能实现总结
|
||||||
|
2. [schedule-dispatch-implementation.md](./schedule-dispatch-implementation.md) - 调度功能实现文档
|
||||||
|
3. [DISPATCH_TAB_IMPLEMENTATION.md](./DISPATCH_TAB_IMPLEMENTATION.md) - 调度Tab实现文档(已过时)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 总结
|
||||||
|
|
||||||
|
调度功能已成功重构,主要改进:
|
||||||
|
|
||||||
|
1. ✅ **独立页面**:调度功能从编排页面的Tab移动到独立页面
|
||||||
|
2. ✅ **权限控制**:只有编排完成后才能进入调度页面
|
||||||
|
3. ✅ **状态检查**:订单管理页面实时检查编排状态
|
||||||
|
4. ✅ **后端集成**:调度页面从后端加载真实数据
|
||||||
|
5. ✅ **用户体验**:清晰的按钮状态和操作提示
|
||||||
|
|
||||||
|
现在可以开始测试新的调度流程了!🚀
|
||||||
313
docs/DISPATCH_TAB_IMPLEMENTATION.md
Normal file
313
docs/DISPATCH_TAB_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
# 调度Tab实现完成
|
||||||
|
|
||||||
|
## ✅ 实现概述
|
||||||
|
|
||||||
|
调度功能已成功集成到编排页面中,用户可以在完成编排后使用调度Tab来调整参赛者的出场顺序。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 实现内容
|
||||||
|
|
||||||
|
### 1. 前端页面修改
|
||||||
|
|
||||||
|
**文件**: `martial-web/src/views/martial/schedule/index.vue`
|
||||||
|
|
||||||
|
#### 新增内容:
|
||||||
|
|
||||||
|
1. **调度Tab按钮** (第41-48行)
|
||||||
|
- 只有在编排完成后才可用 (`:disabled="!isScheduleCompleted"`)
|
||||||
|
- 点击时调用 `handleSwitchToDispatch` 方法
|
||||||
|
|
||||||
|
2. **调度Tab内容** (第185-267行)
|
||||||
|
- 场地选择器
|
||||||
|
- 时间段选择器
|
||||||
|
- 分组列表展示
|
||||||
|
- 参赛者表格(包含上移/下移按钮)
|
||||||
|
- 保存/取消按钮
|
||||||
|
|
||||||
|
3. **数据属性** (第403-406行)
|
||||||
|
```javascript
|
||||||
|
dispatchGroups: [], // 调度分组列表
|
||||||
|
hasDispatchChanges: false, // 是否有未保存的更改
|
||||||
|
originalDispatchData: null // 原始调度数据(用于取消时恢复)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **调度方法** (第893-1063行)
|
||||||
|
- `handleSwitchToDispatch()` - 切换到调度Tab
|
||||||
|
- `handleSelectVenue(venueId)` - 选择场地
|
||||||
|
- `handleSelectTime(timeIndex)` - 选择时间段
|
||||||
|
- `loadDispatchData()` - 加载调度数据
|
||||||
|
- `handleDispatchMoveUp(group, index)` - 上移参赛者
|
||||||
|
- `handleDispatchMoveDown(group, index)` - 下移参赛者
|
||||||
|
- `updatePerformanceOrder(group)` - 更新出场顺序
|
||||||
|
- `handleSaveDispatch()` - 保存调度
|
||||||
|
- `handleCancelDispatch()` - 取消调度
|
||||||
|
|
||||||
|
5. **样式** (第1268-1314行)
|
||||||
|
- `.dispatch-container` - 调度容器样式
|
||||||
|
- `.dispatch-group` - 调度分组样式
|
||||||
|
- `.dispatch-footer` - 底部按钮样式
|
||||||
|
|
||||||
|
### 2. API导入
|
||||||
|
|
||||||
|
**文件**: `martial-web/src/api/martial/activitySchedule.js`
|
||||||
|
|
||||||
|
已导入的API函数:
|
||||||
|
- `getDispatchData` - 获取调度数据
|
||||||
|
- `saveDispatch` - 批量保存调度
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 功能特性
|
||||||
|
|
||||||
|
### 1. 权限控制
|
||||||
|
- ✅ 调度Tab只有在编排完成后才可用
|
||||||
|
- ✅ 编排完成前,调度Tab按钮禁用并显示灰色
|
||||||
|
|
||||||
|
### 2. 数据加载
|
||||||
|
- ✅ 切换到调度Tab时自动加载数据
|
||||||
|
- ✅ 切换场地或时间段时重新加载对应数据
|
||||||
|
- ✅ 保存成功后重新加载数据确保同步
|
||||||
|
|
||||||
|
### 3. 顺序调整
|
||||||
|
- ✅ 上移按钮:将参赛者向上移动一位
|
||||||
|
- ✅ 下移按钮:将参赛者向下移动一位
|
||||||
|
- ✅ 第一个参赛者的上移按钮自动禁用
|
||||||
|
- ✅ 最后一个参赛者的下移按钮自动禁用
|
||||||
|
- ✅ 每次移动后自动更新 `performanceOrder` 字段
|
||||||
|
|
||||||
|
### 4. 数据保存
|
||||||
|
- ✅ 只有有更改时才允许保存(保存按钮启用)
|
||||||
|
- ✅ 批量保存所有调整到后端
|
||||||
|
- ✅ 保存成功后显示提示并重新加载数据
|
||||||
|
|
||||||
|
### 5. 取消操作
|
||||||
|
- ✅ 有未保存更改时,取消需要确认
|
||||||
|
- ✅ 确认后恢复到原始数据
|
||||||
|
- ✅ 无更改时,直接切换回竞赛分组Tab
|
||||||
|
|
||||||
|
### 6. 用户体验
|
||||||
|
- ✅ 操作成功后显示提示消息
|
||||||
|
- ✅ 按钮状态正确(禁用/启用)
|
||||||
|
- ✅ 使用图标按钮,操作直观
|
||||||
|
- ✅ 数据加载时显示loading状态
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 后端接口
|
||||||
|
|
||||||
|
### 1. 获取调度数据
|
||||||
|
- **URL**: `GET /api/blade-martial/schedule/dispatch-data`
|
||||||
|
- **参数**:
|
||||||
|
- `competitionId`: 赛事ID
|
||||||
|
- `venueId`: 场地ID
|
||||||
|
- `timeSlotIndex`: 时间段索引
|
||||||
|
- **返回**: 调度数据(分组和参赛者列表)
|
||||||
|
|
||||||
|
### 2. 批量保存调度
|
||||||
|
- **URL**: `POST /api/blade-martial/schedule/save-dispatch`
|
||||||
|
- **参数**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"competitionId": 1,
|
||||||
|
"adjustments": [
|
||||||
|
{
|
||||||
|
"detailId": 101,
|
||||||
|
"participants": [
|
||||||
|
{"id": 1001, "performanceOrder": 1},
|
||||||
|
{"id": 1002, "performanceOrder": 2}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **返回**: 保存结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 数据流程
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 用户完成编排
|
||||||
|
↓
|
||||||
|
2. 点击"调度"Tab
|
||||||
|
↓
|
||||||
|
3. 检查编排是否完成 (isScheduleCompleted)
|
||||||
|
↓
|
||||||
|
4. 加载调度数据 (loadDispatchData)
|
||||||
|
↓
|
||||||
|
5. 显示分组和参赛者列表
|
||||||
|
↓
|
||||||
|
6. 用户点击上移/下移按钮
|
||||||
|
↓
|
||||||
|
7. 交换数组位置
|
||||||
|
↓
|
||||||
|
8. 更新 performanceOrder
|
||||||
|
↓
|
||||||
|
9. 标记 hasDispatchChanges = true
|
||||||
|
↓
|
||||||
|
10. 用户点击"保存调度"
|
||||||
|
↓
|
||||||
|
11. 调用 saveDispatch API
|
||||||
|
↓
|
||||||
|
12. 后端批量更新数据库
|
||||||
|
↓
|
||||||
|
13. 返回成功
|
||||||
|
↓
|
||||||
|
14. 重新加载数据
|
||||||
|
↓
|
||||||
|
15. 显示成功提示
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 测试步骤
|
||||||
|
|
||||||
|
### 1. 完成编排
|
||||||
|
1. 进入编排页面
|
||||||
|
2. 点击"自动编排"按钮
|
||||||
|
3. 点击"完成编排"按钮
|
||||||
|
4. 确认编排已锁定
|
||||||
|
|
||||||
|
### 2. 进入调度模式
|
||||||
|
1. 点击"调度"Tab(应该可用)
|
||||||
|
2. 选择一个场地
|
||||||
|
3. 选择一个时间段
|
||||||
|
4. 查看分组和参赛者列表
|
||||||
|
|
||||||
|
### 3. 调整顺序
|
||||||
|
1. 找到一个分组
|
||||||
|
2. 点击某个参赛者的"上移"按钮
|
||||||
|
3. 观察顺序变化和成功提示
|
||||||
|
4. 点击"下移"按钮
|
||||||
|
5. 观察顺序变化和成功提示
|
||||||
|
6. 验证第一个不能上移(按钮禁用)
|
||||||
|
7. 验证最后一个不能下移(按钮禁用)
|
||||||
|
|
||||||
|
### 4. 保存调度
|
||||||
|
1. 进行一些调整
|
||||||
|
2. 观察"保存调度"按钮变为可用
|
||||||
|
3. 点击"保存调度"按钮
|
||||||
|
4. 等待保存成功提示
|
||||||
|
5. 刷新页面
|
||||||
|
6. 验证顺序是否保持
|
||||||
|
|
||||||
|
### 5. 取消操作
|
||||||
|
1. 进行一些调整
|
||||||
|
2. 点击"取消"按钮
|
||||||
|
3. 确认弹出提示
|
||||||
|
4. 点击"确定"
|
||||||
|
5. 验证数据恢复到原始状态
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
### 1. 权限控制
|
||||||
|
- 调度Tab只有在 `isScheduleCompleted === true` 时才可用
|
||||||
|
- 编排完成后,编排Tab和场地Tab会被禁用
|
||||||
|
|
||||||
|
### 2. 数据一致性
|
||||||
|
- 每次切换场地或时间段都重新加载数据
|
||||||
|
- 保存前检查是否有未保存的更改
|
||||||
|
- 使用深拷贝保存原始数据,避免引用问题
|
||||||
|
|
||||||
|
### 3. 用户体验
|
||||||
|
- 有未保存更改时,取消操作需要确认
|
||||||
|
- 第一个不能上移,最后一个不能下移
|
||||||
|
- 保存成功后显示提示并刷新数据
|
||||||
|
- 操作按钮使用图标,更加直观
|
||||||
|
|
||||||
|
### 4. 性能优化
|
||||||
|
- 使用深拷贝保存原始数据
|
||||||
|
- 只在有更改时才允许保存
|
||||||
|
- 批量更新数据库而非逐条更新
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 代码关键点
|
||||||
|
|
||||||
|
### 1. Tab切换逻辑
|
||||||
|
```vue
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
:type="activeTab === 'dispatch' ? 'primary' : ''"
|
||||||
|
@click="handleSwitchToDispatch"
|
||||||
|
:disabled="!isScheduleCompleted">
|
||||||
|
调度
|
||||||
|
</el-button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 上移/下移按钮
|
||||||
|
```vue
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
:disabled="$index === 0"
|
||||||
|
@click="handleDispatchMoveUp(group, $index)">
|
||||||
|
<img src="/img/图标 3@3x.png" class="move-icon" alt="上移" />
|
||||||
|
</el-button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 数据交换逻辑
|
||||||
|
```javascript
|
||||||
|
handleDispatchMoveUp(group, index) {
|
||||||
|
if (index === 0) return
|
||||||
|
const participants = group.participants
|
||||||
|
// 交换位置
|
||||||
|
const temp = participants[index]
|
||||||
|
participants[index] = participants[index - 1]
|
||||||
|
participants[index - 1] = temp
|
||||||
|
// 更新顺序号
|
||||||
|
this.updatePerformanceOrder(group)
|
||||||
|
this.hasDispatchChanges = true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 保存调度逻辑
|
||||||
|
```javascript
|
||||||
|
async handleSaveDispatch() {
|
||||||
|
const adjustments = this.dispatchGroups.map(group => ({
|
||||||
|
detailId: group.detailId,
|
||||||
|
participants: group.participants.map(p => ({
|
||||||
|
id: p.id,
|
||||||
|
performanceOrder: p.performanceOrder
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
const res = await saveDispatch({
|
||||||
|
competitionId: this.competitionId,
|
||||||
|
adjustments
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.data.success) {
|
||||||
|
this.$message.success('调度保存成功')
|
||||||
|
await this.loadDispatchData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 总结
|
||||||
|
|
||||||
|
调度Tab已成功集成到编排页面中,实现了以下功能:
|
||||||
|
|
||||||
|
1. ✅ **Tab切换**: 编排完成后可切换到调度Tab
|
||||||
|
2. ✅ **数据加载**: 根据场地和时间段加载调度数据
|
||||||
|
3. ✅ **顺序调整**: 支持上移/下移参赛者
|
||||||
|
4. ✅ **数据保存**: 批量保存调度调整到后端
|
||||||
|
5. ✅ **取消操作**: 支持取消未保存的更改
|
||||||
|
6. ✅ **用户体验**: 清晰的操作反馈和按钮状态控制
|
||||||
|
|
||||||
|
现在可以开始测试调度功能了!🚀
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 相关文档
|
||||||
|
|
||||||
|
- [调度功能实现文档](./schedule-dispatch-implementation.md)
|
||||||
|
- [调度功能总结](./DISPATCH_FEATURE_SUMMARY.md)
|
||||||
|
- [后端Controller](../src/main/java/org/springblade/modules/martial/controller/MartialScheduleArrangeController.java)
|
||||||
|
- [前端API](../../martial-web/src/api/martial/activitySchedule.js)
|
||||||
|
- [前端页面](../../martial-web/src/views/martial/schedule/index.vue)
|
||||||
485
docs/schedule-dispatch-implementation.md
Normal file
485
docs/schedule-dispatch-implementation.md
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
# 调度功能实现文档
|
||||||
|
|
||||||
|
## 📋 实现总结
|
||||||
|
|
||||||
|
调度功能已经完成后端和前端API的开发,现在需要在前端页面中集成调度功能。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 前端页面修改方案
|
||||||
|
|
||||||
|
### 方案:在编排页面添加调度Tab
|
||||||
|
|
||||||
|
修改 `src/views/martial/schedule/index.vue` 文件,在现有的"竞赛分组"和"场地"Tab基础上,添加"调度"Tab。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 前端代码实现
|
||||||
|
|
||||||
|
### 1. 在 `<template>` 中添加调度Tab
|
||||||
|
|
||||||
|
在现有的 `tabs-section` 中添加调度按钮和内容:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<div class="tabs-section">
|
||||||
|
<div class="tab-buttons">
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
:type="activeTab === 'competition' ? 'primary' : ''"
|
||||||
|
@click="activeTab = 'competition'"
|
||||||
|
:disabled="isScheduleCompleted">
|
||||||
|
竞赛分组
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
:type="activeTab === 'venue' ? 'primary' : ''"
|
||||||
|
@click="activeTab = 'venue'"
|
||||||
|
:disabled="isScheduleCompleted">
|
||||||
|
场地
|
||||||
|
</el-button>
|
||||||
|
<!-- 新增:调度Tab -->
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
:type="activeTab === 'dispatch' ? 'primary' : ''"
|
||||||
|
@click="handleSwitchToDispatch"
|
||||||
|
:disabled="!isScheduleCompleted">
|
||||||
|
调度
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 竞赛分组 Tab -->
|
||||||
|
<div v-show="activeTab === 'competition'" class="tab-content">
|
||||||
|
<!-- 原有的竞赛分组内容 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 场地 Tab -->
|
||||||
|
<div v-show="activeTab === 'venue'" class="tab-content">
|
||||||
|
<!-- 原有的场地内容 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增:调度 Tab -->
|
||||||
|
<div v-show="activeTab === 'dispatch'" class="tab-content">
|
||||||
|
<div class="dispatch-container">
|
||||||
|
<!-- 场地和时间段选择 -->
|
||||||
|
<div class="venue-list">
|
||||||
|
<div class="venue-buttons">
|
||||||
|
<el-button
|
||||||
|
v-for="venue in venues"
|
||||||
|
:key="venue.id"
|
||||||
|
size="small"
|
||||||
|
:type="selectedVenueId === venue.id ? 'primary' : ''"
|
||||||
|
@click="handleSelectVenue(venue.id)">
|
||||||
|
{{ venue.venueName }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="time-selector">
|
||||||
|
<el-button
|
||||||
|
v-for="(time, index) in timeSlots"
|
||||||
|
:key="index"
|
||||||
|
size="small"
|
||||||
|
:type="selectedTime === index ? 'primary' : ''"
|
||||||
|
@click="handleSelectTime(index)">
|
||||||
|
{{ time }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分组列表 -->
|
||||||
|
<div v-for="group in dispatchGroups" :key="group.groupId" class="dispatch-group">
|
||||||
|
<div class="group-header">
|
||||||
|
<h3 class="group-title">{{ group.groupName }}</h3>
|
||||||
|
<span class="participant-count">({{ group.participants.length }}人)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 参赛者列表 -->
|
||||||
|
<el-table :data="group.participants" border stripe size="small">
|
||||||
|
<el-table-column label="序号" width="80" align="center">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
{{ $index + 1 }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="organization" label="学校/单位" min-width="200"></el-table-column>
|
||||||
|
<el-table-column prop="playerName" label="选手姓名" width="120"></el-table-column>
|
||||||
|
<el-table-column prop="projectName" label="项目" width="150"></el-table-column>
|
||||||
|
<el-table-column label="操作" width="180" align="center">
|
||||||
|
<template #default="{ row, $index }">
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
:disabled="$index === 0"
|
||||||
|
@click="handleMoveUp(group, $index)">
|
||||||
|
<img src="/img/图标 3@3x.png" class="move-icon" alt="上移" />
|
||||||
|
上移
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
:disabled="$index === group.participants.length - 1"
|
||||||
|
@click="handleMoveDown(group, $index)">
|
||||||
|
<img src="/img/图标 4@3x.png" class="move-icon" alt="下移" />
|
||||||
|
下移
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 保存按钮 -->
|
||||||
|
<div class="dispatch-footer" v-if="dispatchGroups.length > 0">
|
||||||
|
<el-button @click="handleCancelDispatch">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSaveDispatch" :disabled="!hasDispatchChanges">
|
||||||
|
保存调度
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 在 `<script>` 中添加数据和方法
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { getDispatchData, saveDispatch } from '@/api/martial/activitySchedule'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// ... 原有数据
|
||||||
|
activeTab: 'competition', // 修改:支持 'competition' | 'venue' | 'dispatch'
|
||||||
|
|
||||||
|
// 调度相关数据
|
||||||
|
dispatchGroups: [], // 调度分组列表
|
||||||
|
hasDispatchChanges: false, // 是否有未保存的更改
|
||||||
|
originalDispatchData: null // 原始调度数据(用于取消时恢复)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// ... 原有方法
|
||||||
|
|
||||||
|
// ==================== 调度功能方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换到调度Tab
|
||||||
|
*/
|
||||||
|
handleSwitchToDispatch() {
|
||||||
|
if (!this.isScheduleCompleted) {
|
||||||
|
this.$message.warning('请先完成编排后再进行调度')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.activeTab = 'dispatch'
|
||||||
|
this.loadDispatchData()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择场地(调度模式)
|
||||||
|
*/
|
||||||
|
handleSelectVenue(venueId) {
|
||||||
|
this.selectedVenueId = venueId
|
||||||
|
this.loadDispatchData()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择时间段(调度模式)
|
||||||
|
*/
|
||||||
|
handleSelectTime(timeIndex) {
|
||||||
|
this.selectedTime = timeIndex
|
||||||
|
this.loadDispatchData()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载调度数据
|
||||||
|
*/
|
||||||
|
async loadDispatchData() {
|
||||||
|
if (!this.selectedVenueId || this.selectedTime === null) {
|
||||||
|
this.dispatchGroups = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.loading = true
|
||||||
|
const res = await getDispatchData({
|
||||||
|
competitionId: this.competitionId,
|
||||||
|
venueId: this.selectedVenueId,
|
||||||
|
timeSlotIndex: this.selectedTime
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.data.success) {
|
||||||
|
this.dispatchGroups = res.data.data.groups || []
|
||||||
|
// 保存原始数据,用于取消时恢复
|
||||||
|
this.originalDispatchData = JSON.parse(JSON.stringify(this.dispatchGroups))
|
||||||
|
this.hasDispatchChanges = false
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.data.msg || '加载调度数据失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载调度数据失败:', error)
|
||||||
|
this.$message.error('加载调度数据失败')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上移参赛者
|
||||||
|
*/
|
||||||
|
handleMoveUp(group, index) {
|
||||||
|
if (index === 0) return
|
||||||
|
|
||||||
|
const participants = group.participants
|
||||||
|
// 交换位置
|
||||||
|
const temp = participants[index]
|
||||||
|
participants[index] = participants[index - 1]
|
||||||
|
participants[index - 1] = temp
|
||||||
|
|
||||||
|
// 更新顺序号
|
||||||
|
this.updatePerformanceOrder(group)
|
||||||
|
this.hasDispatchChanges = true
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下移参赛者
|
||||||
|
*/
|
||||||
|
handleMoveDown(group, index) {
|
||||||
|
const participants = group.participants
|
||||||
|
if (index === participants.length - 1) return
|
||||||
|
|
||||||
|
// 交换位置
|
||||||
|
const temp = participants[index]
|
||||||
|
participants[index] = participants[index + 1]
|
||||||
|
participants[index + 1] = temp
|
||||||
|
|
||||||
|
// 更新顺序号
|
||||||
|
this.updatePerformanceOrder(group)
|
||||||
|
this.hasDispatchChanges = true
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新出场顺序
|
||||||
|
*/
|
||||||
|
updatePerformanceOrder(group) {
|
||||||
|
group.participants.forEach((p, index) => {
|
||||||
|
p.performanceOrder = index + 1
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存调度
|
||||||
|
*/
|
||||||
|
async handleSaveDispatch() {
|
||||||
|
if (!this.hasDispatchChanges) {
|
||||||
|
this.$message.info('没有需要保存的更改')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
// 构建保存数据
|
||||||
|
const adjustments = this.dispatchGroups.map(group => ({
|
||||||
|
detailId: group.detailId,
|
||||||
|
participants: group.participants.map(p => ({
|
||||||
|
id: p.id,
|
||||||
|
performanceOrder: p.performanceOrder
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
const res = await saveDispatch({
|
||||||
|
competitionId: this.competitionId,
|
||||||
|
adjustments
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.data.success) {
|
||||||
|
this.$message.success('调度保存成功')
|
||||||
|
this.hasDispatchChanges = false
|
||||||
|
// 重新加载数据
|
||||||
|
await this.loadDispatchData()
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.data.msg || '保存失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存调度失败:', error)
|
||||||
|
this.$message.error('保存失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消调度
|
||||||
|
*/
|
||||||
|
handleCancelDispatch() {
|
||||||
|
if (this.hasDispatchChanges) {
|
||||||
|
this.$confirm('有未保存的更改,确定要取消吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
// 恢复原始数据
|
||||||
|
this.dispatchGroups = JSON.parse(JSON.stringify(this.originalDispatchData))
|
||||||
|
this.hasDispatchChanges = false
|
||||||
|
this.$message.info('已取消更改')
|
||||||
|
}).catch(() => {
|
||||||
|
// 用户点击了取消
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.activeTab = 'competition'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 添加样式
|
||||||
|
|
||||||
|
在 `<style>` 中添加调度相关样式:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
<style scoped lang="scss">
|
||||||
|
// ... 原有样式
|
||||||
|
|
||||||
|
// 调度容器
|
||||||
|
.dispatch-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调度分组
|
||||||
|
.dispatch-group {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.group-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid #409eff;
|
||||||
|
|
||||||
|
.group-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant-count {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调度底部按钮
|
||||||
|
.dispatch-footer {
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动图标
|
||||||
|
.move-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 功能说明
|
||||||
|
|
||||||
|
### 1. Tab切换逻辑
|
||||||
|
|
||||||
|
- **编排Tab**:编排完成前可用,完成后禁用
|
||||||
|
- **场地Tab**:编排完成前可用,完成后禁用
|
||||||
|
- **调度Tab**:只有编排完成后才可用
|
||||||
|
|
||||||
|
### 2. 调度操作
|
||||||
|
|
||||||
|
- **上移**:将参赛者向上移动一位(第一个不能上移)
|
||||||
|
- **下移**:将参赛者向下移动一位(最后一个不能下移)
|
||||||
|
- **保存**:批量保存所有调整
|
||||||
|
- **取消**:恢复到原始数据
|
||||||
|
|
||||||
|
### 3. 数据同步
|
||||||
|
|
||||||
|
- 切换场地或时间段时,自动加载对应的调度数据
|
||||||
|
- 保存成功后,重新加载数据确保同步
|
||||||
|
- 取消时,恢复到加载时的原始数据
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
1. **权限控制**
|
||||||
|
- 调度Tab只有在 `isScheduleCompleted === true` 时才可用
|
||||||
|
- 编排完成后,编排Tab和场地Tab应该禁用
|
||||||
|
|
||||||
|
2. **数据一致性**
|
||||||
|
- 每次切换场地或时间段都重新加载数据
|
||||||
|
- 保存前检查是否有未保存的更改
|
||||||
|
|
||||||
|
3. **用户体验**
|
||||||
|
- 有未保存更改时,取消操作需要确认
|
||||||
|
- 第一个不能上移,最后一个不能下移
|
||||||
|
- 保存成功后显示提示并刷新数据
|
||||||
|
|
||||||
|
4. **性能优化**
|
||||||
|
- 使用深拷贝保存原始数据
|
||||||
|
- 只在有更改时才允许保存
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 测试步骤
|
||||||
|
|
||||||
|
1. **完成编排**
|
||||||
|
- 进入编排页面
|
||||||
|
- 完成自动编排
|
||||||
|
- 点击"完成编排"按钮
|
||||||
|
|
||||||
|
2. **进入调度模式**
|
||||||
|
- 点击"调度"Tab
|
||||||
|
- 选择场地和时间段
|
||||||
|
- 查看参赛者列表
|
||||||
|
|
||||||
|
3. **调整顺序**
|
||||||
|
- 点击"上移"或"下移"按钮
|
||||||
|
- 观察顺序变化
|
||||||
|
- 检查第一个和最后一个的按钮是否正确禁用
|
||||||
|
|
||||||
|
4. **保存调度**
|
||||||
|
- 点击"保存调度"按钮
|
||||||
|
- 检查是否保存成功
|
||||||
|
- 刷新页面验证数据是否持久化
|
||||||
|
|
||||||
|
5. **取消操作**
|
||||||
|
- 进行一些调整
|
||||||
|
- 点击"取消"按钮
|
||||||
|
- 确认数据恢复到原始状态
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 总结
|
||||||
|
|
||||||
|
调度功能的实现要点:
|
||||||
|
|
||||||
|
1. ✅ **后端完成**:DTO、Service、Controller 全部实现
|
||||||
|
2. ✅ **前端API**:封装了3个调度相关接口
|
||||||
|
3. ✅ **页面集成**:在编排页面添加调度Tab
|
||||||
|
4. ✅ **权限控制**:只有编排完成后才能使用
|
||||||
|
5. ✅ **用户体验**:提供上移/下移按钮,操作简单直观
|
||||||
|
|
||||||
|
现在可以开始测试调度功能了!🎉
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package org.springblade.modules.auth.controller;
|
||||||
|
|
||||||
|
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.springblade.common.cache.CacheNames;
|
||||||
|
import org.springblade.core.launch.constant.AppConstant;
|
||||||
|
import org.springblade.core.redis.cache.BladeRedis;
|
||||||
|
import org.springblade.core.tool.api.R;
|
||||||
|
import org.springblade.core.tool.utils.StringUtil;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码控制器
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RequestMapping(AppConstant.APPLICATION_AUTH_NAME + "/captcha")
|
||||||
|
@Tag(name = "验证码", description = "验证码")
|
||||||
|
public class CaptchaController {
|
||||||
|
|
||||||
|
private final BladeRedis bladeRedis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送短信验证码
|
||||||
|
*/
|
||||||
|
@PostMapping("/send")
|
||||||
|
@ApiOperationSupport(order = 1)
|
||||||
|
@Operation(summary = "发送短信验证码", description = "传入手机号")
|
||||||
|
public R send(@Parameter(description = "手机号", required = true) @RequestParam String phone) {
|
||||||
|
// 验证手机号格式
|
||||||
|
if (StringUtil.isBlank(phone)) {
|
||||||
|
return R.fail("手机号不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!phone.matches("^1[3-9]\\d{9}$")) {
|
||||||
|
return R.fail("手机号格式不正确");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否频繁发送
|
||||||
|
String cacheKey = CacheNames.CAPTCHA_KEY + phone;
|
||||||
|
String existCode = bladeRedis.get(cacheKey);
|
||||||
|
if (StringUtil.isNotBlank(existCode)) {
|
||||||
|
return R.fail("验证码已发送,请稍后再试");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成6位随机验证码
|
||||||
|
String code = generateCode(6);
|
||||||
|
|
||||||
|
// 存储验证码到Redis,有效期5分钟
|
||||||
|
bladeRedis.setEx(cacheKey, code, Duration.ofMinutes(5));
|
||||||
|
|
||||||
|
// TODO: 实际项目中应该调用短信服务发送验证码
|
||||||
|
// 这里仅做演示,直接返回验证码(生产环境应该删除)
|
||||||
|
System.out.println("发送验证码到手机号: " + phone + ", 验证码: " + code);
|
||||||
|
|
||||||
|
return R.success("验证码发送成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机验证码
|
||||||
|
*
|
||||||
|
* @param length 验证码长度
|
||||||
|
* @return 验证码
|
||||||
|
*/
|
||||||
|
private String generateCode(int length) {
|
||||||
|
Random random = new Random();
|
||||||
|
StringBuilder code = new StringBuilder();
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
code.append(random.nextInt(10));
|
||||||
|
}
|
||||||
|
return code.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,11 +9,14 @@ import org.springblade.core.mp.support.Condition;
|
|||||||
import org.springblade.core.mp.support.Query;
|
import org.springblade.core.mp.support.Query;
|
||||||
import org.springblade.core.tool.api.R;
|
import org.springblade.core.tool.api.R;
|
||||||
import org.springblade.core.tool.utils.Func;
|
import org.springblade.core.tool.utils.Func;
|
||||||
|
import org.springblade.modules.martial.pojo.dto.BatchGenerateInviteDTO;
|
||||||
|
import org.springblade.modules.martial.pojo.dto.GenerateInviteDTO;
|
||||||
import org.springblade.modules.martial.pojo.entity.MartialJudgeInvite;
|
import org.springblade.modules.martial.pojo.entity.MartialJudgeInvite;
|
||||||
import org.springblade.modules.martial.pojo.vo.MartialJudgeInviteVO;
|
import org.springblade.modules.martial.pojo.vo.MartialJudgeInviteVO;
|
||||||
import org.springblade.modules.martial.service.IMartialJudgeInviteService;
|
import org.springblade.modules.martial.service.IMartialJudgeInviteService;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,4 +80,53 @@ public class MartialJudgeInviteController extends BladeController {
|
|||||||
return R.data(statistics);
|
return R.data(statistics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成邀请码
|
||||||
|
*/
|
||||||
|
@PostMapping("/generate")
|
||||||
|
@Operation(summary = "生成邀请码", description = "为评委生成邀请码")
|
||||||
|
public R<MartialJudgeInvite> generateInviteCode(@RequestBody GenerateInviteDTO dto) {
|
||||||
|
MartialJudgeInvite invite = judgeInviteService.generateInviteCode(dto);
|
||||||
|
return R.data(invite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量生成邀请码
|
||||||
|
*/
|
||||||
|
@PostMapping("/generate/batch")
|
||||||
|
@Operation(summary = "批量生成邀请码", description = "为多个评委批量生成邀请码")
|
||||||
|
public R<List<MartialJudgeInvite>> batchGenerateInviteCode(@RequestBody BatchGenerateInviteDTO dto) {
|
||||||
|
List<MartialJudgeInvite> invites = judgeInviteService.batchGenerateInviteCode(dto);
|
||||||
|
return R.data(invites);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新生成邀请码
|
||||||
|
*/
|
||||||
|
@PutMapping("/regenerate/{inviteId}")
|
||||||
|
@Operation(summary = "重新生成邀请码", description = "重新生成邀请码(旧码失效)")
|
||||||
|
public R<MartialJudgeInvite> regenerateInviteCode(@PathVariable Long inviteId) {
|
||||||
|
MartialJudgeInvite invite = judgeInviteService.regenerateInviteCode(inviteId);
|
||||||
|
return R.data(invite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询评委的邀请码
|
||||||
|
*/
|
||||||
|
@GetMapping("/byJudge")
|
||||||
|
@Operation(summary = "查询评委邀请码", description = "根据评委ID和赛事ID查询邀请码")
|
||||||
|
public R<MartialJudgeInvite> getInviteByJudge(
|
||||||
|
@RequestParam Long competitionId,
|
||||||
|
@RequestParam Long judgeId
|
||||||
|
) {
|
||||||
|
MartialJudgeInvite invite = judgeInviteService.lambdaQuery()
|
||||||
|
.eq(MartialJudgeInvite::getCompetitionId, competitionId)
|
||||||
|
.eq(MartialJudgeInvite::getJudgeId, judgeId)
|
||||||
|
.eq(MartialJudgeInvite::getIsDeleted, 0)
|
||||||
|
.orderByDesc(MartialJudgeInvite::getCreateTime)
|
||||||
|
.last("LIMIT 1")
|
||||||
|
.one();
|
||||||
|
return R.data(invite);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,4 +118,53 @@ public class MartialScheduleArrangeController extends BladeController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取调度数据
|
||||||
|
*/
|
||||||
|
@GetMapping("/dispatch-data")
|
||||||
|
@Operation(summary = "获取调度数据", description = "获取指定场地和时间段的调度数据")
|
||||||
|
public R<org.springblade.modules.martial.pojo.vo.DispatchDataVO> getDispatchData(
|
||||||
|
@RequestParam Long competitionId,
|
||||||
|
@RequestParam Long venueId,
|
||||||
|
@RequestParam Integer timeSlotIndex) {
|
||||||
|
try {
|
||||||
|
org.springblade.modules.martial.pojo.vo.DispatchDataVO data =
|
||||||
|
scheduleService.getDispatchData(competitionId, venueId, timeSlotIndex);
|
||||||
|
return R.data(data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取调度数据失败", e);
|
||||||
|
return R.fail("获取调度数据失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调整出场顺序
|
||||||
|
*/
|
||||||
|
@PostMapping("/adjust-order")
|
||||||
|
@Operation(summary = "调整出场顺序", description = "调整参赛者的出场顺序")
|
||||||
|
public R adjustOrder(@RequestBody org.springblade.modules.martial.pojo.dto.AdjustOrderDTO dto) {
|
||||||
|
try {
|
||||||
|
boolean success = scheduleService.adjustOrder(dto);
|
||||||
|
return success ? R.success("顺序调整成功") : R.fail("顺序调整失败");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("调整顺序失败", e);
|
||||||
|
return R.fail("调整顺序失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量保存调度
|
||||||
|
*/
|
||||||
|
@PostMapping("/save-dispatch")
|
||||||
|
@Operation(summary = "批量保存调度", description = "批量保存调度调整")
|
||||||
|
public R saveDispatch(@RequestBody org.springblade.modules.martial.pojo.dto.SaveDispatchDTO dto) {
|
||||||
|
try {
|
||||||
|
boolean success = scheduleService.saveDispatch(dto);
|
||||||
|
return success ? R.success("调度保存成功") : R.fail("调度保存失败");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("保存调度失败", e);
|
||||||
|
return R.fail("保存调度失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package org.springblade.modules.martial.pojo.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调整出场顺序DTO
|
||||||
|
*
|
||||||
|
* @author BladeX
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "调整出场顺序DTO")
|
||||||
|
public class AdjustOrderDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编排明细ID
|
||||||
|
*/
|
||||||
|
@Schema(description = "编排明细ID")
|
||||||
|
private Long detailId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参赛者记录ID
|
||||||
|
*/
|
||||||
|
@Schema(description = "参赛者记录ID")
|
||||||
|
private Long participantId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调整动作
|
||||||
|
*/
|
||||||
|
@Schema(description = "调整动作(move_up=上移, move_down=下移, swap=交换)")
|
||||||
|
private String action;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目标顺序(交换时使用)
|
||||||
|
*/
|
||||||
|
@Schema(description = "目标顺序(交换时使用)")
|
||||||
|
private Integer targetOrder;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package org.springblade.modules.martial.pojo.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量生成邀请码DTO
|
||||||
|
*
|
||||||
|
* @author Blade
|
||||||
|
* @since 2025-12-12
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description ="批量生成邀请码DTO")
|
||||||
|
public class BatchGenerateInviteDTO {
|
||||||
|
|
||||||
|
@Schema(description = "赛事ID", required = true)
|
||||||
|
private Long competitionId;
|
||||||
|
|
||||||
|
@Schema(description = "评委ID列表", required = true)
|
||||||
|
private List<Long> judgeIds;
|
||||||
|
|
||||||
|
@Schema(description = "角色:judge-普通评委,chief_judge-裁判长")
|
||||||
|
private String role = "judge";
|
||||||
|
|
||||||
|
@Schema(description = "过期天数(默认30天)")
|
||||||
|
private Integer expireDays = 30;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package org.springblade.modules.martial.pojo.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度数据查询DTO
|
||||||
|
*
|
||||||
|
* @author BladeX
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "调度数据查询DTO")
|
||||||
|
public class DispatchDataDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 赛事ID
|
||||||
|
*/
|
||||||
|
@Schema(description = "赛事ID")
|
||||||
|
private Long competitionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场地ID
|
||||||
|
*/
|
||||||
|
@Schema(description = "场地ID")
|
||||||
|
private Long venueId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间段索引
|
||||||
|
*/
|
||||||
|
@Schema(description = "时间段索引(0=第1天上午,1=第1天下午...)")
|
||||||
|
private Integer timeSlotIndex;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package org.springblade.modules.martial.pojo.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成邀请码DTO
|
||||||
|
*
|
||||||
|
* @author Blade
|
||||||
|
* @since 2025-12-12
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description ="生成邀请码DTO")
|
||||||
|
public class GenerateInviteDTO {
|
||||||
|
|
||||||
|
@Schema(description = "赛事ID", required = true)
|
||||||
|
private Long competitionId;
|
||||||
|
|
||||||
|
@Schema(description = "评委ID", required = true)
|
||||||
|
private Long judgeId;
|
||||||
|
|
||||||
|
@Schema(description = "角色:judge-普通评委,chief_judge-裁判长", required = true)
|
||||||
|
private String role;
|
||||||
|
|
||||||
|
@Schema(description = "分配场地ID(普通评委必填)")
|
||||||
|
private Long venueId;
|
||||||
|
|
||||||
|
@Schema(description = "分配项目列表(JSON数组字符串)")
|
||||||
|
private String projects;
|
||||||
|
|
||||||
|
@Schema(description = "过期天数(默认30天)")
|
||||||
|
private Integer expireDays = 30;
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package org.springblade.modules.martial.pojo.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存调度DTO
|
||||||
|
*
|
||||||
|
* @author BladeX
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "保存调度DTO")
|
||||||
|
public class SaveDispatchDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 赛事ID
|
||||||
|
*/
|
||||||
|
@Schema(description = "赛事ID")
|
||||||
|
private Long competitionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调整列表
|
||||||
|
*/
|
||||||
|
@Schema(description = "调整列表")
|
||||||
|
private List<DetailAdjustment> adjustments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 明细调整
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "明细调整")
|
||||||
|
public static class DetailAdjustment {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编排明细ID
|
||||||
|
*/
|
||||||
|
@Schema(description = "编排明细ID")
|
||||||
|
private Long detailId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参赛者列表
|
||||||
|
*/
|
||||||
|
@Schema(description = "参赛者列表")
|
||||||
|
private List<ParticipantOrder> participants;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参赛者顺序
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "参赛者顺序")
|
||||||
|
public static class ParticipantOrder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参赛者记录ID
|
||||||
|
*/
|
||||||
|
@Schema(description = "参赛者记录ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 出场顺序
|
||||||
|
*/
|
||||||
|
@Schema(description = "出场顺序")
|
||||||
|
private Integer performanceOrder;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package org.springblade.modules.martial.pojo.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度数据VO
|
||||||
|
*
|
||||||
|
* @author BladeX
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "调度数据VO")
|
||||||
|
public class DispatchDataVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分组列表
|
||||||
|
*/
|
||||||
|
@Schema(description = "分组列表")
|
||||||
|
private List<DispatchGroup> groups;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度分组
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "调度分组")
|
||||||
|
public static class DispatchGroup {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分组ID
|
||||||
|
*/
|
||||||
|
@Schema(description = "分组ID")
|
||||||
|
private Long groupId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分组名称
|
||||||
|
*/
|
||||||
|
@Schema(description = "分组名称")
|
||||||
|
private String groupName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编排明细ID
|
||||||
|
*/
|
||||||
|
@Schema(description = "编排明细ID")
|
||||||
|
private Long detailId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目类型
|
||||||
|
*/
|
||||||
|
@Schema(description = "项目类型(1=个人 2=集体)")
|
||||||
|
private Integer projectType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参赛者列表
|
||||||
|
*/
|
||||||
|
@Schema(description = "参赛者列表")
|
||||||
|
private List<DispatchParticipant> participants;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度参赛者
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "调度参赛者")
|
||||||
|
public static class DispatchParticipant {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参赛者记录ID
|
||||||
|
*/
|
||||||
|
@Schema(description = "参赛者记录ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参赛者ID
|
||||||
|
*/
|
||||||
|
@Schema(description = "参赛者ID")
|
||||||
|
private Long participantId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单位名称
|
||||||
|
*/
|
||||||
|
@Schema(description = "单位名称")
|
||||||
|
private String organization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选手姓名
|
||||||
|
*/
|
||||||
|
@Schema(description = "选手姓名")
|
||||||
|
private String playerName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目名称
|
||||||
|
*/
|
||||||
|
@Schema(description = "项目名称")
|
||||||
|
private String projectName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组别
|
||||||
|
*/
|
||||||
|
@Schema(description = "组别")
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 出场顺序
|
||||||
|
*/
|
||||||
|
@Schema(description = "出场顺序")
|
||||||
|
private Integer performanceOrder;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,9 +3,12 @@ package org.springblade.modules.martial.service;
|
|||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import org.springblade.core.mp.support.Query;
|
import org.springblade.core.mp.support.Query;
|
||||||
|
import org.springblade.modules.martial.pojo.dto.BatchGenerateInviteDTO;
|
||||||
|
import org.springblade.modules.martial.pojo.dto.GenerateInviteDTO;
|
||||||
import org.springblade.modules.martial.pojo.entity.MartialJudgeInvite;
|
import org.springblade.modules.martial.pojo.entity.MartialJudgeInvite;
|
||||||
import org.springblade.modules.martial.pojo.vo.MartialJudgeInviteVO;
|
import org.springblade.modules.martial.pojo.vo.MartialJudgeInviteVO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,4 +35,35 @@ public interface IMartialJudgeInviteService extends IService<MartialJudgeInvite>
|
|||||||
*/
|
*/
|
||||||
Map<String, Object> getInviteStatistics(Long competitionId);
|
Map<String, Object> getInviteStatistics(Long competitionId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成邀请码
|
||||||
|
*
|
||||||
|
* @param dto 生成邀请码DTO
|
||||||
|
* @return 邀请记录
|
||||||
|
*/
|
||||||
|
MartialJudgeInvite generateInviteCode(GenerateInviteDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量生成邀请码
|
||||||
|
*
|
||||||
|
* @param dto 批量生成邀请码DTO
|
||||||
|
* @return 邀请记录列表
|
||||||
|
*/
|
||||||
|
List<MartialJudgeInvite> batchGenerateInviteCode(BatchGenerateInviteDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新生成邀请码
|
||||||
|
*
|
||||||
|
* @param inviteId 邀请记录ID
|
||||||
|
* @return 新的邀请记录
|
||||||
|
*/
|
||||||
|
MartialJudgeInvite regenerateInviteCode(Long inviteId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成唯一邀请码
|
||||||
|
*
|
||||||
|
* @return 邀请码
|
||||||
|
*/
|
||||||
|
String generateUniqueInviteCode();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,4 +49,27 @@ public interface IMartialScheduleService extends IService<MartialSchedule> {
|
|||||||
*/
|
*/
|
||||||
boolean moveScheduleGroup(MoveScheduleGroupDTO dto);
|
boolean moveScheduleGroup(MoveScheduleGroupDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取调度数据
|
||||||
|
* @param competitionId 赛事ID
|
||||||
|
* @param venueId 场地ID
|
||||||
|
* @param timeSlotIndex 时间段索引
|
||||||
|
* @return 调度数据
|
||||||
|
*/
|
||||||
|
org.springblade.modules.martial.pojo.vo.DispatchDataVO getDispatchData(Long competitionId, Long venueId, Integer timeSlotIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调整出场顺序
|
||||||
|
* @param dto 调整请求数据
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
boolean adjustOrder(org.springblade.modules.martial.pojo.dto.AdjustOrderDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量保存调度
|
||||||
|
* @param dto 保存调度数据
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
boolean saveDispatch(org.springblade.modules.martial.pojo.dto.SaveDispatchDTO dto);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,34 @@ package org.springblade.modules.martial.service.impl;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springblade.core.log.exception.ServiceException;
|
||||||
import org.springblade.core.mp.support.Condition;
|
import org.springblade.core.mp.support.Condition;
|
||||||
import org.springblade.core.mp.support.Query;
|
import org.springblade.core.mp.support.Query;
|
||||||
|
import org.springblade.modules.martial.pojo.dto.BatchGenerateInviteDTO;
|
||||||
|
import org.springblade.modules.martial.pojo.dto.GenerateInviteDTO;
|
||||||
import org.springblade.modules.martial.pojo.entity.MartialJudgeInvite;
|
import org.springblade.modules.martial.pojo.entity.MartialJudgeInvite;
|
||||||
import org.springblade.modules.martial.mapper.MartialJudgeInviteMapper;
|
import org.springblade.modules.martial.mapper.MartialJudgeInviteMapper;
|
||||||
import org.springblade.modules.martial.pojo.vo.MartialJudgeInviteVO;
|
import org.springblade.modules.martial.pojo.vo.MartialJudgeInviteVO;
|
||||||
import org.springblade.modules.martial.service.IMartialJudgeInviteService;
|
import org.springblade.modules.martial.service.IMartialJudgeInviteService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JudgeInvite 服务实现类
|
* JudgeInvite 服务实现类
|
||||||
*
|
*
|
||||||
* @author BladeX
|
* @author BladeX
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class MartialJudgeInviteServiceImpl extends ServiceImpl<MartialJudgeInviteMapper, MartialJudgeInvite> implements IMartialJudgeInviteService {
|
public class MartialJudgeInviteServiceImpl extends ServiceImpl<MartialJudgeInviteMapper, MartialJudgeInvite> implements IMartialJudgeInviteService {
|
||||||
|
|
||||||
@@ -64,4 +74,126 @@ public class MartialJudgeInviteServiceImpl extends ServiceImpl<MartialJudgeInvit
|
|||||||
return statistics;
|
return statistics;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MartialJudgeInvite generateInviteCode(GenerateInviteDTO dto) {
|
||||||
|
// 1. 检查是否已存在有效邀请码
|
||||||
|
MartialJudgeInvite existInvite = this.getOne(
|
||||||
|
Wrappers.<MartialJudgeInvite>lambdaQuery()
|
||||||
|
.eq(MartialJudgeInvite::getCompetitionId, dto.getCompetitionId())
|
||||||
|
.eq(MartialJudgeInvite::getJudgeId, dto.getJudgeId())
|
||||||
|
.eq(MartialJudgeInvite::getStatus, 1)
|
||||||
|
.eq(MartialJudgeInvite::getIsDeleted, 0)
|
||||||
|
.gt(MartialJudgeInvite::getExpireTime, LocalDateTime.now())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existInvite != null) {
|
||||||
|
throw new ServiceException("该评委已有有效邀请码:" + existInvite.getInviteCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 生成唯一邀请码
|
||||||
|
String inviteCode = generateUniqueInviteCode();
|
||||||
|
|
||||||
|
// 3. 创建邀请记录
|
||||||
|
MartialJudgeInvite invite = new MartialJudgeInvite();
|
||||||
|
invite.setCompetitionId(dto.getCompetitionId());
|
||||||
|
invite.setJudgeId(dto.getJudgeId());
|
||||||
|
invite.setInviteCode(inviteCode);
|
||||||
|
invite.setRole(dto.getRole());
|
||||||
|
invite.setVenueId(dto.getVenueId());
|
||||||
|
invite.setProjects(dto.getProjects());
|
||||||
|
invite.setExpireTime(LocalDateTime.now().plusDays(dto.getExpireDays()));
|
||||||
|
invite.setIsUsed(0);
|
||||||
|
invite.setStatus(1);
|
||||||
|
|
||||||
|
boolean success = this.save(invite);
|
||||||
|
if (!success) {
|
||||||
|
throw new ServiceException("生成邀请码失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("为评委{}生成邀请码成功:{}", dto.getJudgeId(), inviteCode);
|
||||||
|
return invite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateUniqueInviteCode() {
|
||||||
|
String inviteCode;
|
||||||
|
int maxRetry = 10;
|
||||||
|
int retry = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
// 生成6位随机字符串(大写字母+数字)
|
||||||
|
inviteCode = UUID.randomUUID().toString()
|
||||||
|
.replaceAll("-", "")
|
||||||
|
.substring(0, 6)
|
||||||
|
.toUpperCase();
|
||||||
|
|
||||||
|
// 检查是否重复
|
||||||
|
long count = this.count(
|
||||||
|
Wrappers.<MartialJudgeInvite>lambdaQuery()
|
||||||
|
.eq(MartialJudgeInvite::getInviteCode, inviteCode)
|
||||||
|
.eq(MartialJudgeInvite::getIsDeleted, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (count == 0) {
|
||||||
|
return inviteCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
retry++;
|
||||||
|
} while (retry < maxRetry);
|
||||||
|
|
||||||
|
throw new ServiceException("生成邀请码失败,请重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MartialJudgeInvite> batchGenerateInviteCode(BatchGenerateInviteDTO dto) {
|
||||||
|
List<MartialJudgeInvite> invites = new ArrayList<>();
|
||||||
|
List<String> failedJudges = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Long judgeId : dto.getJudgeIds()) {
|
||||||
|
try {
|
||||||
|
GenerateInviteDTO generateDto = new GenerateInviteDTO();
|
||||||
|
generateDto.setCompetitionId(dto.getCompetitionId());
|
||||||
|
generateDto.setJudgeId(judgeId);
|
||||||
|
generateDto.setRole(dto.getRole());
|
||||||
|
generateDto.setExpireDays(dto.getExpireDays());
|
||||||
|
|
||||||
|
MartialJudgeInvite invite = generateInviteCode(generateDto);
|
||||||
|
invites.add(invite);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("为评委{}生成邀请码失败:{}", judgeId, e.getMessage());
|
||||||
|
failedJudges.add(judgeId.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!failedJudges.isEmpty()) {
|
||||||
|
log.info("批量生成完成,失败的评委ID:{}", String.join(",", failedJudges));
|
||||||
|
}
|
||||||
|
|
||||||
|
return invites;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MartialJudgeInvite regenerateInviteCode(Long inviteId) {
|
||||||
|
// 1. 查询原邀请记录
|
||||||
|
MartialJudgeInvite oldInvite = this.getById(inviteId);
|
||||||
|
if (oldInvite == null) {
|
||||||
|
throw new ServiceException("邀请记录不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 禁用旧邀请码
|
||||||
|
oldInvite.setStatus(0);
|
||||||
|
this.updateById(oldInvite);
|
||||||
|
|
||||||
|
// 3. 生成新邀请码
|
||||||
|
GenerateInviteDTO dto = new GenerateInviteDTO();
|
||||||
|
dto.setCompetitionId(oldInvite.getCompetitionId());
|
||||||
|
dto.setJudgeId(oldInvite.getJudgeId());
|
||||||
|
dto.setRole(oldInvite.getRole());
|
||||||
|
dto.setVenueId(oldInvite.getVenueId());
|
||||||
|
dto.setProjects(oldInvite.getProjects());
|
||||||
|
|
||||||
|
log.info("重新生成邀请码,旧邀请码:{},评委ID:{}", oldInvite.getInviteCode(), oldInvite.getJudgeId());
|
||||||
|
return generateInviteCode(dto);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -451,4 +451,158 @@ public class MartialScheduleServiceImpl extends ServiceImpl<MartialScheduleMappe
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public org.springblade.modules.martial.pojo.vo.DispatchDataVO getDispatchData(Long competitionId, Long venueId, Integer timeSlotIndex) {
|
||||||
|
// 1. 查询指定场地和时间段的编排明细
|
||||||
|
List<MartialScheduleDetail> details = scheduleDetailMapper.selectList(
|
||||||
|
new QueryWrapper<MartialScheduleDetail>()
|
||||||
|
.eq("competition_id", competitionId)
|
||||||
|
.eq("venue_id", venueId)
|
||||||
|
.eq("time_slot_index", timeSlotIndex)
|
||||||
|
.eq("is_deleted", 0)
|
||||||
|
.orderByAsc("sort_order")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (details.isEmpty()) {
|
||||||
|
// 返回空数据
|
||||||
|
org.springblade.modules.martial.pojo.vo.DispatchDataVO vo = new org.springblade.modules.martial.pojo.vo.DispatchDataVO();
|
||||||
|
vo.setGroups(new ArrayList<>());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 构建返回数据
|
||||||
|
List<org.springblade.modules.martial.pojo.vo.DispatchDataVO.DispatchGroup> groups = new ArrayList<>();
|
||||||
|
|
||||||
|
for (MartialScheduleDetail detail : details) {
|
||||||
|
// 查询分组信息
|
||||||
|
MartialScheduleGroup group = scheduleGroupMapper.selectById(detail.getScheduleGroupId());
|
||||||
|
if (group == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询该明细下的所有参赛者
|
||||||
|
List<MartialScheduleParticipant> participants = scheduleParticipantMapper.selectList(
|
||||||
|
new QueryWrapper<MartialScheduleParticipant>()
|
||||||
|
.eq("schedule_detail_id", detail.getId())
|
||||||
|
.eq("is_deleted", 0)
|
||||||
|
.orderByAsc("performance_order")
|
||||||
|
);
|
||||||
|
|
||||||
|
// 转换为VO
|
||||||
|
org.springblade.modules.martial.pojo.vo.DispatchDataVO.DispatchGroup groupVO =
|
||||||
|
new org.springblade.modules.martial.pojo.vo.DispatchDataVO.DispatchGroup();
|
||||||
|
groupVO.setGroupId(group.getId());
|
||||||
|
groupVO.setGroupName(group.getGroupName());
|
||||||
|
groupVO.setDetailId(detail.getId());
|
||||||
|
groupVO.setProjectType(group.getProjectType());
|
||||||
|
|
||||||
|
List<org.springblade.modules.martial.pojo.vo.DispatchDataVO.DispatchParticipant> participantVOs =
|
||||||
|
participants.stream().map(p -> {
|
||||||
|
org.springblade.modules.martial.pojo.vo.DispatchDataVO.DispatchParticipant pVO =
|
||||||
|
new org.springblade.modules.martial.pojo.vo.DispatchDataVO.DispatchParticipant();
|
||||||
|
pVO.setId(p.getId());
|
||||||
|
pVO.setParticipantId(p.getParticipantId());
|
||||||
|
pVO.setOrganization(p.getOrganization());
|
||||||
|
pVO.setPlayerName(p.getPlayerName());
|
||||||
|
pVO.setProjectName(p.getProjectName());
|
||||||
|
pVO.setCategory(p.getCategory());
|
||||||
|
pVO.setPerformanceOrder(p.getPerformanceOrder());
|
||||||
|
return pVO;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
groupVO.setParticipants(participantVOs);
|
||||||
|
groups.add(groupVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
org.springblade.modules.martial.pojo.vo.DispatchDataVO result =
|
||||||
|
new org.springblade.modules.martial.pojo.vo.DispatchDataVO();
|
||||||
|
result.setGroups(groups);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean adjustOrder(org.springblade.modules.martial.pojo.dto.AdjustOrderDTO dto) {
|
||||||
|
// 1. 查询当前参赛者
|
||||||
|
MartialScheduleParticipant current = scheduleParticipantMapper.selectById(dto.getParticipantId());
|
||||||
|
if (current == null) {
|
||||||
|
throw new RuntimeException("参赛者不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查询同一明细下的所有参赛者
|
||||||
|
List<MartialScheduleParticipant> participants = scheduleParticipantMapper.selectList(
|
||||||
|
new QueryWrapper<MartialScheduleParticipant>()
|
||||||
|
.eq("schedule_detail_id", dto.getDetailId())
|
||||||
|
.eq("is_deleted", 0)
|
||||||
|
.orderByAsc("performance_order")
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. 根据动作调整顺序
|
||||||
|
int currentIndex = -1;
|
||||||
|
for (int i = 0; i < participants.size(); i++) {
|
||||||
|
if (participants.get(i).getId().equals(dto.getParticipantId())) {
|
||||||
|
currentIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex == -1) {
|
||||||
|
throw new RuntimeException("未找到参赛者");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 执行调整
|
||||||
|
if ("move_up".equals(dto.getAction())) {
|
||||||
|
// 上移
|
||||||
|
if (currentIndex == 0) {
|
||||||
|
throw new RuntimeException("已经是第一个,无法上移");
|
||||||
|
}
|
||||||
|
// 交换位置
|
||||||
|
Collections.swap(participants, currentIndex, currentIndex - 1);
|
||||||
|
} else if ("move_down".equals(dto.getAction())) {
|
||||||
|
// 下移
|
||||||
|
if (currentIndex == participants.size() - 1) {
|
||||||
|
throw new RuntimeException("已经是最后一个,无法下移");
|
||||||
|
}
|
||||||
|
// 交换位置
|
||||||
|
Collections.swap(participants, currentIndex, currentIndex + 1);
|
||||||
|
} else if ("swap".equals(dto.getAction())) {
|
||||||
|
// 交换到指定位置
|
||||||
|
if (dto.getTargetOrder() == null || dto.getTargetOrder() < 1 || dto.getTargetOrder() > participants.size()) {
|
||||||
|
throw new RuntimeException("目标顺序无效");
|
||||||
|
}
|
||||||
|
int targetIndex = dto.getTargetOrder() - 1;
|
||||||
|
Collections.swap(participants, currentIndex, targetIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 更新所有参赛者的顺序
|
||||||
|
for (int i = 0; i < participants.size(); i++) {
|
||||||
|
MartialScheduleParticipant p = participants.get(i);
|
||||||
|
p.setPerformanceOrder(i + 1);
|
||||||
|
scheduleParticipantMapper.updateById(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean saveDispatch(org.springblade.modules.martial.pojo.dto.SaveDispatchDTO dto) {
|
||||||
|
if (dto.getAdjustments() == null || dto.getAdjustments().isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量更新所有参赛者的出场顺序
|
||||||
|
for (org.springblade.modules.martial.pojo.dto.SaveDispatchDTO.DetailAdjustment adjustment : dto.getAdjustments()) {
|
||||||
|
for (org.springblade.modules.martial.pojo.dto.SaveDispatchDTO.ParticipantOrder po : adjustment.getParticipants()) {
|
||||||
|
MartialScheduleParticipant participant = scheduleParticipantMapper.selectById(po.getId());
|
||||||
|
if (participant != null) {
|
||||||
|
participant.setPerformanceOrder(po.getPerformanceOrder());
|
||||||
|
scheduleParticipantMapper.updateById(participant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import org.springblade.core.tool.utils.DateUtil;
|
|||||||
import org.springblade.core.tool.utils.Func;
|
import org.springblade.core.tool.utils.Func;
|
||||||
import org.springblade.core.tool.utils.StringPool;
|
import org.springblade.core.tool.utils.StringPool;
|
||||||
import org.springblade.core.tool.utils.StringUtil;
|
import org.springblade.core.tool.utils.StringUtil;
|
||||||
|
import org.springblade.modules.system.pojo.dto.UserRegisterDTO;
|
||||||
import org.springblade.modules.system.pojo.entity.User;
|
import org.springblade.modules.system.pojo.entity.User;
|
||||||
import org.springblade.modules.system.excel.UserExcel;
|
import org.springblade.modules.system.excel.UserExcel;
|
||||||
import org.springblade.modules.system.excel.UserImporter;
|
import org.springblade.modules.system.excel.UserImporter;
|
||||||
@@ -304,4 +305,63 @@ public class UserController {
|
|||||||
return R.success("操作成功");
|
return R.success("操作成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户注册
|
||||||
|
*/
|
||||||
|
@PostMapping("/register")
|
||||||
|
@ApiOperationSupport(order = 19)
|
||||||
|
@Operation(summary = "用户注册", description = "传入UserRegisterDTO")
|
||||||
|
public R register(@RequestBody UserRegisterDTO registerDTO) {
|
||||||
|
// 验证验证码
|
||||||
|
String code = registerDTO.getCode();
|
||||||
|
String cacheKey = CacheNames.CAPTCHA_KEY + registerDTO.getPhone();
|
||||||
|
String cachedCode = bladeRedis.get(cacheKey);
|
||||||
|
|
||||||
|
// 支持万能验证码888888用于测试
|
||||||
|
if (!"888888".equals(code)) {
|
||||||
|
if (StringUtil.isBlank(cachedCode)) {
|
||||||
|
return R.fail("验证码已过期,请重新获取");
|
||||||
|
}
|
||||||
|
if (!code.equals(cachedCode)) {
|
||||||
|
return R.fail("验证码错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证账号是否已存在
|
||||||
|
User existUser = userService.userByAccount(registerDTO.getTenantId(), registerDTO.getAccount());
|
||||||
|
if (existUser != null) {
|
||||||
|
return R.fail("账号已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证手机号是否已注册
|
||||||
|
QueryWrapper<User> phoneQuery = new QueryWrapper<>();
|
||||||
|
phoneQuery.eq("phone", registerDTO.getPhone());
|
||||||
|
phoneQuery.eq("is_deleted", 0);
|
||||||
|
User phoneUser = userService.getOne(phoneQuery);
|
||||||
|
if (phoneUser != null) {
|
||||||
|
return R.fail("手机号已注册");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建User对象
|
||||||
|
User user = new User();
|
||||||
|
user.setTenantId(registerDTO.getTenantId());
|
||||||
|
user.setUserType(registerDTO.getUserType());
|
||||||
|
user.setAccount(registerDTO.getAccount());
|
||||||
|
user.setPassword(registerDTO.getPassword());
|
||||||
|
user.setRealName(registerDTO.getRealName());
|
||||||
|
user.setName(registerDTO.getRealName());
|
||||||
|
user.setPhone(registerDTO.getPhone());
|
||||||
|
user.setSex(registerDTO.getSex());
|
||||||
|
|
||||||
|
// 注册用户
|
||||||
|
boolean result = userService.registerUser(user);
|
||||||
|
|
||||||
|
// 删除验证码缓存
|
||||||
|
if (!"888888".equals(code)) {
|
||||||
|
bladeRedis.del(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return R.status(result);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package org.springblade.modules.system.pojo.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户注册DTO
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "用户注册对象")
|
||||||
|
public class UserRegisterDTO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户ID
|
||||||
|
*/
|
||||||
|
@Schema(description = "租户ID")
|
||||||
|
private String tenantId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户平台
|
||||||
|
*/
|
||||||
|
@Schema(description = "用户平台")
|
||||||
|
private Integer userType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账号
|
||||||
|
*/
|
||||||
|
@Schema(description = "账号")
|
||||||
|
private String account;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码
|
||||||
|
*/
|
||||||
|
@Schema(description = "密码")
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 真名
|
||||||
|
*/
|
||||||
|
@Schema(description = "真实姓名")
|
||||||
|
private String realName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机
|
||||||
|
*/
|
||||||
|
@Schema(description = "手机号")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 性别
|
||||||
|
*/
|
||||||
|
@Schema(description = "性别")
|
||||||
|
private Integer sex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码
|
||||||
|
*/
|
||||||
|
@Schema(description = "验证码")
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -368,10 +368,16 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
|||||||
throw new OAuth2Exception("租户信息错误!");
|
throw new OAuth2Exception("租户信息错误!");
|
||||||
}
|
}
|
||||||
user.setRealName(user.getName());
|
user.setRealName(user.getName());
|
||||||
user.setRoleId(StringPool.MINUS_ONE);
|
// 为新注册用户分配默认的"用户"角色
|
||||||
|
user.setRoleId("1123598816738675202");
|
||||||
user.setDeptId(StringPool.MINUS_ONE);
|
user.setDeptId(StringPool.MINUS_ONE);
|
||||||
user.setPostId(StringPool.MINUS_ONE);
|
user.setPostId(StringPool.MINUS_ONE);
|
||||||
return this.submit(user);
|
// 前端已经MD5加密,后端使用hex方法再次加密(与登录验证保持一致)
|
||||||
|
if (Func.isNotEmpty(user.getPassword())) {
|
||||||
|
user.setPassword(DigestUtil.hex(user.getPassword()));
|
||||||
|
}
|
||||||
|
// 直接保存,不调用submit避免重复加密
|
||||||
|
return save(user) && submitUserDept(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ blade:
|
|||||||
- /blade-desk/notice/submit
|
- /blade-desk/notice/submit
|
||||||
- /blade-flow/model/submit
|
- /blade-flow/model/submit
|
||||||
- /blade-develop/datasource/submit
|
- /blade-develop/datasource/submit
|
||||||
|
- /blade-system/user/register
|
||||||
#安全框架配置
|
#安全框架配置
|
||||||
secure:
|
secure:
|
||||||
#严格模式
|
#严格模式
|
||||||
@@ -252,7 +253,8 @@ blade:
|
|||||||
- /blade-test/**
|
- /blade-test/**
|
||||||
- /blade-spare/**
|
- /blade-spare/**
|
||||||
- /blade-device/**
|
- /blade-device/**
|
||||||
- /**
|
- /blade-system/user/register
|
||||||
|
- /blade-auth/captcha/send
|
||||||
#授权认证配置
|
#授权认证配置
|
||||||
auth:
|
auth:
|
||||||
- method: ALL
|
- method: ALL
|
||||||
|
|||||||
Reference in New Issue
Block a user