fix bugs
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-12-12 13:49:00 +08:00
parent 7aa6545cbb
commit 1ca0f6a7f6
23 changed files with 2695 additions and 3 deletions

View 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;

View 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;

View 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)
祝使用愉快!✨

View 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.**用户体验**:清晰的按钮状态和操作提示
现在可以开始测试新的调度流程了!🚀

View 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)

View 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.**用户体验**:提供上移/下移按钮,操作简单直观
现在可以开始测试调度功能了!🎉

View File

@@ -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();
}
}

View File

@@ -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);
}
} }

View File

@@ -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());
}
}
} }

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
} }

View File

@@ -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);
} }

View File

@@ -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);
}
} }

View File

@@ -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;
}
} }

View File

@@ -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);
}
} }

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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