Compare commits

...

76 Commits

Author SHA1 Message Date
df7efac819 fix(schedule): 修复集体项目类型显示为双人的问题
- 修改MartialScheduleServiceImpl中type=2的显示文本从双人改为集体
- 保持与前端项目管理页面的类型定义一致(1=单人,2=集体)
2026-01-09 12:58:28 +08:00
559dea702a feat: 项目编码自动生成
- 新增项目时自动生成编码,格式: C{赛事ID}-P{序号}
- 移除手动输入项目编码

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 17:19:23 +08:00
c40ca5b35b fix: 修复联系人默认唯一性问题 & 添加单位统计API
- 问题1: 设置默认联系人时自动取消其他默认联系人
- 问题3: 新增 /organization-stats API 按单位统计运动员、项目、金额

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 16:08:31 +08:00
742272026b 重构项2: 优化编排规则
变更点1 - 使用项目maxParticipants:
- 优先使用项目配置的单位容纳人数进行分组
- 未配置时回退到全局配置(35人)
- 添加日志记录使用的容纳人数

变更点2 - 实现集体优先策略:
- 集体项目(projectType=2)优先于单人项目(projectType=1)
- 同类型项目按预计时长降序排列
- 添加排序结果日志便于调试

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 15:50:21 +08:00
496537ceef 重构项4: 添加出场顺序显示功能
后端:
- 新增LineupGroupVO和LineupParticipantVO类
- 在MartialMiniController中添加/schedule/status和/schedule/lineup接口
- 注入MartialScheduleStatusMapper和MartialScheduleGroupMapper

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 15:42:47 +08:00
e0d3572e34 feat: add score VO with deduction items and player number assignment
- Add selectScoreVOPage for score list with deduction items text
- Add chiefJudgeScore and scoreStatus fields to MartialScoreVO
- Add player number assignment in saveAndLockSchedule method

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-07 14:56:25 +08:00
a262ca9279 fix: 修复安全配置,移除/**通配符放行
- 移除skip-url中的/**通配符,恢复接口认证
- 添加必要的公开接口放行路径
- 修复AuthUtil.getUserId()返回-1的问题

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-07 13:05:46 +08:00
b94ac501de feat: 编排保存时同步更新项目的venue_id
- 在saveDraftSchedule方法中添加同步逻辑
- 保存编排详情后自动更新martial_project.venue_id
- 保持编排系统和项目管理的数据一致性

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-07 11:55:03 +08:00
ea50330a5d fix: 场地无项目时返回空列表而非所有项目
- 修改login和refreshLoginInfo方法中的项目获取逻辑
- 当场地没有关联项目时返回空列表
- 初始化projects变量为空ArrayList

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-07 11:31:27 +08:00
e3f158985a fix: 添加@Slf4j注解修复编译错误
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-07 11:05:49 +08:00
eefe7167ee fix: 报名时检查是否已存在相同选手记录,避免重复创建
- 在提交报名时,先检查是否已存在相同选手+比赛+项目的记录
- 如果存在则更新订单ID,而不是创建新记录
- 解决添加选手后再报名导致重复记录的问题

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-06 16:24:23 +08:00
550802a029 feat: 添加场地类型(venueType)字段支持
- MartialVenue实体添加venueType字段
- 支持室内/室外场地类型区分

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-06 15:18:00 +08:00
ac44bd45fa fix(deduction): 修复扣分项编辑时赛事ID未携带的问题
- 实体类添加competitionId字段
- Controller查询时从关联项目获取competitionId
- 修复project为null时的空指针异常
2026-01-06 14:56:07 +08:00
8193baf314 fix: 添加报名时间和比赛结束时间校验
- 报名时检查报名时间是否在有效范围内
- 报名时检查比赛是否已结束
- 如果比赛已结束,返回错误提示

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-05 17:50:38 +08:00
3af34506ba fix(mini): ensure general judge sees all projects regardless of venue
- Add check for refereeType == 3 or role == general_judge before filtering by venue
- General judges now always get all projects for the competition
- Prevents issue where general judge assigned to a venue would see no projects

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-05 16:38:30 +08:00
55ccf08246 fix(mini): 根据场地获取项目列表,解决同一项目显示在多个场地的问题
- 在MartialProject实体添加venueId字段
- 数据库martial_project表添加venue_id列
- 修改MartialMiniController:当裁判未指定项目时,根据venue_id获取该场地的项目
- 新增getProjectsByVenue方法

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-05 15:51:38 +08:00
29e9fb4e0a feat: add contact management and judge project assignment features
- Add MartialContact entity, mapper, service, and controller for contact management
- Add updateProjects endpoint to MartialJudgeInviteController for project assignment
- Fix MartialRegistrationOrderController for multi-project registration
- Update MartialJudgeInviteVO with projects field

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-05 15:10:27 +08:00
9fa5eb46df fix(score): 按场地统计裁判数量而非按项目 2025-12-31 17:32:13 +08:00
d3c7dccf05 fix(mini): 修复裁判类型判断逻辑 - refereeType=1为主裁判 2025-12-31 16:55:22 +08:00
370cdc8e1e feat(judgeInvite): 支持按场地过滤裁判邀请列表 2025-12-31 16:20:29 +08:00
e70dbd1144 fix(athlete): 选手列表过滤掉集体报名记录 2025-12-31 15:56:24 +08:00
760b7d0039 feat(registration): 报名成功后自动确认选手状态 2025-12-31 15:47:04 +08:00
e50b71a13d fix(registration): 根据赛事时间动态计算报名状态
- 1: 待开始 (赛事未开始)
- 2: 进行中 (赛事进行中)
- 3: 已结束 (赛事已结束)
2025-12-31 15:10:37 +08:00
e1bf9a4351 fix(registration): 查询选手时过滤已删除记录 2025-12-31 14:42:09 +08:00
2f9fbbb2aa fix(schedule): 修复集体项目类型显示为单人的问题
- 修改autoGroupParticipants方法中的projectType判断逻辑
- type=2(双人)或type=3(集体)都映射为projectType=2(集体)
- 之前只处理了type=3的情况,导致type=2的集体项目被错误标记为单人
2025-12-31 14:15:26 +08:00
f45fee050e feat(registration): 支持集体项目报名
- DTO添加teamIds字段接收集体ID列表
- Controller处理集体报名逻辑
- 为每个集体创建martial_athlete记录用于编排
2025-12-31 13:48:04 +08:00
18895dcb76 fix(team): 修复编辑集体变成新增的问题
- 将DTO中的id字段改为teamId,避免uni-app对id字段的特殊处理
- 使用String类型接收teamId,避免JavaScript大数精度丢失
- 添加日志记录便于调试
2025-12-31 13:09:43 +08:00
89962c69e6 feat(team): 添加集体编辑功能 2025-12-31 11:51:12 +08:00
45758108a8 fix(team): 修复删除选手后集体信息未同步更新的问题
- 删除选手时级联删除集体成员关系
- 集体列表动态计算有效成员数(排除已删除选手)
- 集体详情过滤已删除的选手
2025-12-31 11:37:37 +08:00
19e3d94a33 fix(team): 修复集体列表不显示的问题,设置createUser字段 2025-12-31 11:16:30 +08:00
7fae2f0ff8 fix(competition): 修复赛事详情页面报名人数显示为0的问题 2025-12-31 11:07:14 +08:00
fe5ddfa253 fix(mini): 修复裁判员角色判断逻辑
- 修复role和referee_type不一致导致的权限问题
- 裁判员(role=judge)应该只能评分,不能修改
- 主裁判(role=chief_judge)才能修改评分
2025-12-30 18:06:25 +08:00
c7038a5883 feat(team): 添加集体/团队管理功能
- 创建martial_team和martial_team_member表
- 添加MartialTeam和MartialTeamMember实体类
- 添加MartialTeamController提供集体CRUD接口
- 支持集体成员关联管理
2025-12-30 18:02:00 +08:00
87a05df04f fix(registration): 修复我的报名列表信息显示不全问题
- 修改list接口返回VO而非Entity
- 添加getListWithRelations方法批量加载关联数据
- 返回赛事名称、地点、时间、项目名称、选手名称等完整信息
- 优化批量查询减少数据库访问次数
2025-12-30 17:43:45 +08:00
b7ad819a29 fix(schedule): 完成编排时允许空分组状态 2025-12-30 12:59:08 +08:00
6db9a1e51d fix(schedule): 保存草稿时设置time_period默认值 2025-12-30 12:06:31 +08:00
0539152dbb fix(schedule): 保存草稿时设置默认日期,避免schedule_date为空导致插入失败 2025-12-30 11:53:34 +08:00
c7058b8b07 feat(schedule): 无编排数据时自动从项目和选手表生成初始分组
- 修改 getScheduleResult 方法,当没有编排数据时调用 generateInitialScheduleResult
- 新增 generateInitialScheduleResult 方法,从项目和选手表生成初始分组
- CompetitionGroupDTO 添加 projectId 字段
- ParticipantDTO 添加 teamName 字段
- 用户进入编排页面可直接看到选手数据,无需先执行自动编排
2025-12-30 11:10:01 +08:00
16b55adf81 feat(schedule): 添加赛程配置API,支持动态时间段配置
- 添加 GET /martial/schedule/config API 暴露 ScheduleConfig
- 返回 morningStartTime, afternoonStartTime 等配置
- 更新 docker-compose.yml 使用 Dockerfile.quick
2025-12-30 10:51:04 +08:00
0b5fc9fb71 feat: improve schedule auto-arrange functionality
- Add ScheduleConfig for configurable schedule parameters
- Add updateParticipantCheckInStatus API for exception status persistence
- Use configurable thresholds for group splitting and capacity warning
- Add @Slf4j to MartialScheduleServiceImpl
2025-12-29 15:07:52 +08:00
86e4580e5d feat: add multi-stage Dockerfile for full build with martial-tool
- Add Dockerfile.fullbuild: multi-stage build that compiles martial-tool
- Rename Dockerfile to Dockerfile.quick for quick builds (pre-built jar)
- Update docker-compose.yml to use parent directory context
- Update README with new deployment instructions
2025-12-29 14:34:54 +08:00
47d0b70a9c docs: 更新README,简化内容并更新域名配置 2025-12-29 14:17:09 +08:00
105e457f7c docs: README添加数据库迁移文档链接 2025-12-29 14:11:21 +08:00
d583bdc5c8 feat: 集成Flyway数据库迁移工具
- 添加Flyway依赖到pom.xml
- 配置application.yml启用Flyway
- 创建迁移脚本目录db/migration
- 添加V1基线脚本和V2项目字段迁移脚本
- 添加DATABASE_MIGRATION.md使用文档
2025-12-29 14:07:27 +08:00
07845f3a4f fix: 同步martial_project表结构,添加event_type和报名时间字段 2025-12-29 13:53:23 +08:00
ec2382b447 feat: 添加项目报名开始时间和结束时间字段 2025-12-29 12:04:36 +08:00
bcba649b02 裁判邀请列表添加负责场地字段
- MartialJudgeInviteVO: 添加venueName字段
- MartialJudgeInviteMapper.xml: 关联查询场地名称

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-29 10:32:28 +08:00
a19baf3907 feat: 添加项目类型(eventType)字段支持
- 在MartialProject实体类中添加eventType字段
- 在MartialProjectController中添加eventType查询支持
- 项目类型: 1-套路, 2-散打, 3-器械, 4-对练
2025-12-28 19:02:44 +08:00
301bb7a227 feat: 项目列表支持模糊查询
- 项目名称支持模糊查询
- 分组类别支持模糊查询
- 赛事ID和参赛类型精确匹配
- 添加排序(按sort_order升序,create_time降序)

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-28 17:20:10 +08:00
fdd346b27f fix: default venue_id to first venue when null or invalid
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-28 16:20:11 +08:00
1d5ac896dd feat: 添加已确认成绩列表API
- 新增 /mini/general/confirmed 接口
- MartialResultServiceImpl 添加 getConfirmedGeneralList 方法
- 支持总裁页面同时显示待确认和已确认成绩

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-28 16:09:41 +08:00
aab66f79fe feat: 添加三级裁判评分系统(裁判员→主裁判→总裁)
- 新增总裁(裁判长)角色支持,referee_type=3
- MartialResult实体添加主裁判/总裁确认字段和score_status状态
- MartialJudgeInvite实体添加角色常量和判断方法
- MartialMiniController添加三级确认API和登录角色判断
- MartialResultServiceImpl实现三级确认业务逻辑
- MartialScoreServiceImpl主裁判确认时同步martial_result表

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-28 15:49:11 +08:00
491c8db26c chore: 添加 minio_data 到 gitignore 2025-12-28 13:44:56 +08:00
4a2071ddda refactor: 裁判角色名称修改 - 裁判长→主裁判, 普通裁判→裁判员
- 修改所有Java文件中的注释和Schema描述
- 更新MartialScoreServiceImpl中的评分修改记录名称

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 11:37:11 +08:00
559e97b672 feat(mini): 裁判未指定项目时自动获取比赛所有项目
- login和refreshToken接口:如果invite.projects为空,自动获取该比赛的所有项目
- 新增getAllProjectsByCompetition方法查询比赛所有项目
- 支持裁判负责整个场地所有项目的需求

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 11:11:29 +08:00
35a5369e81 feat: 批量导入裁判时根据refereeType自动设置角色
- batchGenerateInviteCode方法查询裁判的refereeType
- refereeType=1 自动设置为chief_judge(裁判长)
- 其他情况设置为judge(普通裁判)

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 16:04:46 +08:00
dca5e5050f fix: 修复批量导入裁判时venueId和projects参数丢失问题
- BatchGenerateInviteDTO添加venueId和projects字段
- batchGenerateInviteCode方法传递venueId和projects给generateDto
- MartialMiniController添加competitionId参数过滤选手
- 新增RegistrationSubmitDTO

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 15:44:29 +08:00
67908a4dd0 Merge branch 'main' of git.waypeak.work:martial/martial-master 2025-12-26 10:32:03 +08:00
0c9322c510 fix bugs 2025-12-26 10:31:41 +08:00
7c1b9de6b4 fix: 修复登录和评分查询的场地ID问题
1. 登录API使用martial_venue表替代mt_venue表查询场地信息
2. 评分查询时过滤无效的venueId(null或<=0)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-25 20:28:33 +08:00
284ebd2e73 fix: 修复裁判评分列表数据不一致问题
- MartialMiniController: getAthletes方法添加venueId过滤
- MiniScoreModifyDTO: 添加venueId字段
- MartialScoreServiceImpl: modifyScoreByAdmin方法设置venueId

问题原因:
1. 后端查询评分记录时缺少场地过滤
2. 裁判长修改评分时未设置venue_id
导致不同场地的裁判看到混乱的数据
2025-12-25 10:55:19 +08:00
e7b8a1c59d fix: optimize schedule query and add scheduleDate field
- Add scheduleDate field to ScheduleGroupDetailVO
- Fix schedule date format in mapper XML
- Optimize schedule service implementation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 17:01:54 +08:00
432ccb606c 修复导出赛程表:使用正确的数据源
- 从 martial_schedule_group 表获取编排数据
- 使用与 getScheduleResult 相同的数据源
- 按分组顺序和出场顺序导出

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 14:10:21 +08:00
ffbe511f34 feat: 裁判长页面显示所有选手
- 修改后端逻辑:裁判长返回所有选手,不再只返回评分完成的
- 前端根据 totalScore 判断是否显示修改按钮
- 未完成评分的选手显示评分中...提示

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 10:31:37 +08:00
4c93027028 fix: 裁判长无限次修改总分功能优化
Some checks are pending
continuous-integration/drone/push Build is pending
- 使用 baseMapper.updateById() 绕过 Service 层状态检查
- 允许裁判长在原始计算总分 ±0.050 范围内无限次修改
- 每次修改都基于原始计算总分验证范围

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 23:37:39 +08:00
abb1391b2f fix: 优化评分系统总分显示逻辑
Some checks are pending
continuous-integration/drone/push Build is pending
1. 修复 updateAthleteTotalScore 方法,使用 getChiefJudgeIds() 排除裁判长的所有评分
2. 修复 getRequiredJudgeCount 方法,使用 distinct 去重统计普通裁判数量
3. 新增 scoringComplete、scoredJudgeCount、requiredJudgeCount 字段
4. 总分只在所有普通裁判评分完成后才显示
5. 总分算法:去掉最高最低分取平均,裁判数<3时直接取平均

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 23:09:31 +08:00
1d6c3d9df5 裁判长修改分数功能优化
1. 限制修改范围为原始分数±0.050
2. 修改记录改为更新而非新增(避免重复记录)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-21 15:22:19 +08:00
cc4a01ea28 fix: 修复评分后总分显示为-1的问题
问题根因:
1. submitScore方法只保存评分记录,未计算更新选手总分
2. BladeX框架将null的Number类型序列化为-1

修复内容:
- 添加updateAthleteTotalScore方法,评分后计算平均分并更新选手总分
- 添加parseLong方法,安全地将String转换为Long(解决JS大数精度问题)
- MiniScoreSubmitDTO的ID字段改为String类型
- MiniAthleteListVO的athleteId添加ToStringSerializer序列化

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 18:29:25 +08:00
0f0beaf62e chore: 整理数据库文件和Docker配置
Some checks failed
continuous-integration/drone/push Build was killed
- 更新Dockerfile
- 整合数据库SQL文件为martial_db.sql
- 添加docker-compose.yml
- 清理临时SQL脚本

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 14:43:48 +08:00
3ae441c044 fix: 添加 @JsonProperty 注解确保 JSON 字段名正确序列化
Some checks failed
continuous-integration/drone/push Build is failing
- MiniAthleteListVO 添加 @JsonProperty 注解,确保字段名与前端一致
- 添加 @JsonInclude(ALWAYS) 确保所有字段都输出
- 添加 idCard 字段支持身份证号显示

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 23:08:16 +08:00
ab290d1aa2 fix: 修复 martial_db.sql 中错误的 mt_venue INSERT 语句
Some checks failed
continuous-integration/drone/push Build is failing
问题: SQL文件中包含167条错误的 INSERT INTO mt_venue 语句
- 这些数据实际上是视图查询结果,被错误地插入到 mt_venue 表
- mt_venue 表有12列,但 INSERT 语句只有4-8个值,导致导入失败

修复: 移除所有错误的 mt_venue INSERT 语句
2025-12-18 12:27:44 +08:00
4e487b76b7 feat: 添加图形验证码接口和扣分项排序功能
All checks were successful
continuous-integration/drone/push Build is passing
新增功能:
- 添加图形验证码接口 GET /oauth/captcha,返回验证码图片和key
- 添加扣分项排序接口 POST /update-order
- 新增数据库完整备份 martial_db.sql

技术细节:
- CaptchaController: 新增 getCaptcha() 方法,生成4位验证码,Redis缓存5分钟
- MartialDeductionItemController: 新增 updateOrder() 批量更新排序
- IMartialDeductionItemService/Impl: 新增排序服务方法
2025-12-18 12:15:25 +08:00
ec26191a5f 最新提交
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-18 11:55:35 +08:00
f6c019e520 fix bugs
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-14 17:38:15 +08:00
4b530dd6be fix bugs
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-12 17:19:16 +08:00
1ca0f6a7f6 fix bugs
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-12 13:49:00 +08:00
171 changed files with 27240 additions and 2024 deletions

View File

@@ -27,7 +27,14 @@
"Bash(python -m json.tool:*)", "Bash(python -m json.tool:*)",
"Bash(\"/d/Program Files/mysql-8.0.32-winx64/bin/mysql\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"\nSELECT \n TABLE_NAME,\n CASE WHEN SUM(COLUMN_NAME = ''status'') > 0 THEN ''✓'' ELSE ''✗'' END AS has_status\nFROM information_schema.COLUMNS \nWHERE TABLE_SCHEMA = ''martial_db'' \n AND TABLE_NAME IN (''martial_athlete'', ''martial_live_update'', ''martial_result'', ''martial_schedule_athlete'')\nGROUP BY TABLE_NAME\nORDER BY TABLE_NAME;\n\")", "Bash(\"/d/Program Files/mysql-8.0.32-winx64/bin/mysql\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"\nSELECT \n TABLE_NAME,\n CASE WHEN SUM(COLUMN_NAME = ''status'') > 0 THEN ''✓'' ELSE ''✗'' END AS has_status\nFROM information_schema.COLUMNS \nWHERE TABLE_SCHEMA = ''martial_db'' \n AND TABLE_NAME IN (''martial_athlete'', ''martial_live_update'', ''martial_result'', ''martial_schedule_athlete'')\nGROUP BY TABLE_NAME\nORDER BY TABLE_NAME;\n\")",
"Bash(git add:*)", "Bash(git add:*)",
"Bash(git commit -m \"$(cat <<''EOF''\nMerge remote-tracking branch ''origin/main''\n\n解决目录重组冲突:\n- doc/ → docs/ (文档目录重命名)\n- doc/sql/ → database/ (数据库脚本目录重组)\n- doc/script/ → scripts/ (脚本目录重组)\n\n保留本地新增的武术比赛系统文件:\n- docs/sql/mysql/martial-*.sql (4个数据库脚本)\n- docs/后端开发完成报告.md\n- docs/数据库字段检查报告.md \n- docs/问题修复报告.md\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\")" "Bash(git commit -m \"$(cat <<''EOF''\nMerge remote-tracking branch ''origin/main''\n\n解决目录重组冲突:\n- doc/ → docs/ (文档目录重命名)\n- doc/sql/ → database/ (数据库脚本目录重组)\n- doc/script/ → scripts/ (脚本目录重组)\n\n保留本地新增的武术比赛系统文件:\n- docs/sql/mysql/martial-*.sql (4个数据库脚本)\n- docs/后端开发完成报告.md\n- docs/数据库字段检查报告.md \n- docs/问题修复报告.md\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\")",
"Bash(\"/d/Program Files/mysql-8.0.32-winx64/bin/mysql\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"DESC martial_schedule_participant;\")",
"Bash(\"/d/Program Files/mysql-8.0.32-winx64/bin/mysql\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"SHOW CREATE TABLE martial_schedule_participant\\\\G\")",
"Bash(\"/d/Program Files/mysql-8.0.32-winx64/bin/mysql\" -h localhost -P 3306 -u root -p123456 -e \"DROP DATABASE IF EXISTS martial_db; CREATE DATABASE martial_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;\":*)",
"Bash(\"/d/Program Files/mysql-8.0.32-winx64/bin/mysql\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"DESC mt_venue;\")",
"Bash(grep:*)",
"Bash(\"/d/Program Files/mysql-8.0.32-winx64/bin/mysql\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"DESC martial_competition_rules_attachment;\")",
"Bash(\"/d/Program Files/mysql-8.0.32-winx64/bin/mysql\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"SELECT COUNT\\(*\\) FROM martial_competition_rules_attachment WHERE is_deleted = 0;\")"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

3
.gitignore vendored
View File

@@ -39,3 +39,6 @@ PORT_FORWARD.md
QUICKSTART.md QUICKSTART.md
SERVICE_CONFIG.md SERVICE_CONFIG.md
nul nul
# MinIO 运行时数据
minio_data/

50
Dockerfile.fullbuild Normal file
View File

@@ -0,0 +1,50 @@
# ============================================
# 武术赛事管理系统 - 完整构建 Dockerfile
# 包含 martial-tool 编译 + martial-master 编译
# ============================================
# 构建阶段:使用 Maven + JDK 镜像
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
# 复制 martial-toolBladeX 框架)
COPY martial-tool /build/martial-tool
# 编译 martial-tool 并安装到本地仓库
RUN cd /build/martial-tool && \
mvn clean install -DskipTests -q
# 复制 martial-master后端项目
COPY martial-master /build/martial-master
# 编译 martial-master
RUN cd /build/martial-master && \
mvn clean package -DskipTests -q
# ============================================
# 运行阶段:使用轻量级 JRE 镜像
# ============================================
FROM eclipse-temurin:17-jre-jammy
LABEL maintainer="JohnSion"
LABEL description="武术比赛管理系统后端服务"
WORKDIR /app
# 从构建阶段复制 JAR 文件
COPY --from=builder /build/martial-master/target/blade-api.jar /app/blade-api.jar
# 暴露端口
EXPOSE 8123
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8123/actuator/health || exit 1
# JVM 参数配置
ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC"
ENV SPRING_PROFILE="dev"
# 启动命令
CMD ["sh", "-c", "java ${JAVA_OPTS} -jar /app/blade-api.jar --spring.profiles.active=${SPRING_PROFILE}"]

View File

@@ -1,15 +1,3 @@
# 多阶段构建:编译阶段
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
# 复制主项目源码
COPY pom.xml .
COPY src ./src
# 编译项目(在 Drone 中已经编译好,这里只是复制)
RUN mkdir -p target
# 运行阶段:使用轻量级 JRE 镜像 # 运行阶段:使用轻量级 JRE 镜像
FROM eclipse-temurin:17-jre-jammy FROM eclipse-temurin:17-jre-jammy

423
README.md
View File

@@ -2,348 +2,145 @@
基于 BladeX 4.0.1 企业级框架构建的武术比赛管理系统后端服务。 基于 BladeX 4.0.1 企业级框架构建的武术比赛管理系统后端服务。
## 🌐 在线访问 ## 在线访问
- **生产环境 API**: https://martial-api.johnsion.club | 服务 | 地址 | 说明 |
- **API 文档**: https://martial-doc.johnsion.club |------|------|------|
- **前端系统**: https://martial.johnsion.club | 后端 API | https://martial-api.aitisai.com | Spring Boot 服务 |
- **CI/CD 管理**: https://martial-ci.johnsion.club | 管理后台 | https://martial-admin.aitisai.com | Web 管理端 |
| 用户端 | https://martial.aitisai.com | 报名小程序 H5 |
| 裁判端 | https://martial-mini.aitisai.com | 裁判评分小程序 |
| OSS 存储 | https://martial-oss.aitisai.com | MinIO 对象存储 |
| MinIO 控制台 | https://martial-minio.aitisai.com | MinIO 管理界面 |
## 📦 技术栈 ## 技术栈
- **框架**: Spring Boot 3.2.4 - **框架**: Spring Boot 3.2.4 + BladeX 4.0.1
- **语言**: Java 17 - **语言**: Java 17
- **数据库**: MySQL 8.0 + Redis 7
- **ORM**: MyBatis-Plus - **ORM**: MyBatis-Plus
- **数据库**: MySQL 8.0 - **数据库迁移**: Flyway
- **缓存**: Redis 7 - **对象存储**: MinIO
- **API 文档**: Knife4j (Swagger) - **反向代理**: Caddy
- **企业框架**: BladeX 4.0.1 RELEASE - **容器化**: Docker Compose
## 📁 项目结构 ## 快速开始
### 环境要求
- Docker & Docker Compose
### 一键部署(推荐)
确保目录结构如下:
```
martial/
├── martial-tool/ # BladeX 框架(必需)
├── martial-master/ # 后端项目
├── martial-web/ # 管理后台前端
├── martial-mini/ # 用户端小程序
└── martial-admin-mini/ # 裁判端小程序
```
```bash
cd martial/martial-master
# 首次部署完整构建约5-6分钟
docker compose up -d
# 查看构建日志
docker compose logs -f martial-api
# 查看服务状态
docker compose ps
```
服务启动后:
- API 服务: http://localhost:8123
- API 文档: http://localhost:8123/doc.html
### 快速构建(开发迭代)
如果已经手动编译过 JAR可以使用快速构建
```bash
# 先编译 martial-tool首次
cd ../martial-tool && mvn clean install -DskipTests
# 编译 martial-master
cd ../martial-master && mvn clean package -DskipTests
# 使用快速构建 Dockerfile
docker compose build martial-api --build-arg DOCKERFILE=Dockerfile.quick
```
## 项目结构
``` ```
martial-master/ martial-master/
├── src/main/java/org/springblade/ ├── src/main/java/org/springblade/
│ ├── Application.java # 主启动类 │ ├── modules/martial/ # 武术比赛核心业务
│ ├── common/ # 公共工具和配置 │ ├── controller/ # 接口控制器
│ ├── modules/ # 业务模块 │ ├── service/ # 业务逻辑
│ │ ├── auth/ # 认证授权 │ │ ├── mapper/ # 数据访问
│ │ ── system/ # 系统管理 │ │ ── pojo/ # 实体类
│ ├── resource/ # 资源管理 └── ... # BladeX 框架模块
│ │ ├── desk/ # 工作台
│ │ ├── develop/ # 代码生成
│ │ └── martial/ # ⭐ 武术比赛业务(核心)
│ └── job/ # 定时任务
├── src/main/resources/ ├── src/main/resources/
│ ├── application.yml # 主配置 │ ├── application.yml # 主配置
│ ├── application-dev.yml # 开发环境 │ ├── application-dev.yml # 开发环境
│ ├── application-test.yml # 测试环境 │ ├── application-prod.yml # 生产环境
│ └── application-prod.yml # 生产环境 │ └── db/migration/ # Flyway 迁移脚本
├── database/ # 数据库脚本 ├── database/ # 数据库初始化脚本
│ ├── bladex/ # BladeX 框架表
│ ├── flowable/ # 工作流表
│ ├── martial-db/ # 武术业务表
│ └── upgrade/ # 升级脚本
├── docs/ # 项目文档 ├── docs/ # 项目文档
│ ├── README.md # 文档索引 ├── docker-compose.yml # Docker 编排配置
│ ├── 架构说明.md # 架构设计 ├── Dockerfile.fullbuild # 完整构建(含 martial-tool
│ ├── 前后端架构说明.md # 前后端交互 └── Dockerfile.quick # 快速构建(需预编译 JAR
│ ├── 开发指南.md # 开发规范
│ └── CI-CD部署总结.md # 部署文档
├── scripts/ # 运维脚本
│ ├── docker/ # Docker 部署
│ └── fatjar/ # JAR 启动脚本
├── .drone.yml # CI/CD 配置
├── Dockerfile # Docker 镜像构建
└── CLAUDE.md # 项目完整说明
``` ```
## 🚀 快速开始 ## Docker Compose 服务
### 环境要求 | 服务 | 端口 | 说明 |
|------|------|------|
| martial-api | 8123 | 后端 API 服务 |
| martial-mysql | 3306 | MySQL 数据库 |
| martial-redis | 6379 | Redis 缓存 |
| minio | 9000/9001 | 对象存储 |
- **JDK**: 17+ ## 数据库迁移
- **Maven**: 3.8+
- **MySQL**: 8.0+
- **Redis**: 6.0+
### 本地开发 项目使用 Flyway 管理数据库版本,应用启动时自动执行迁移。
**添加新迁移:**
```bash ```bash
# 1. 克隆项目 # 在 src/main/resources/db/migration/ 创建脚本
git clone https://git.waypeak.work/martial/martial-master.git # 命名规范: V{版本号}__{描述}.sql
cd martial-master # 示例: V3__add_new_table.sql
# 2. 编译 BladeX 框架(首次必须)
cd /path/to/martial-tool
mvn clean install -DskipTests
# 3. 编译并运行
cd /path/to/martial-master
mvn clean package -DskipTests -Dmaven.test.skip=true
mvn spring-boot:run
# 4. 访问应用
# API: http://localhost:8123
# 文档: http://localhost:8123/doc.html
``` ```
详细说明请参考:[CLAUDE.md](./CLAUDE.md) 详细说明[docs/DATABASE_MIGRATION.md](./docs/DATABASE_MIGRATION.md)
## 🔄 自动化部署 ## 开发文档
### CI/CD 架构 | 文档 | 说明 |
|------|------|
| [CLAUDE.md](./CLAUDE.md) | 项目完整说明 |
| [docs/开发指南.md](./docs/开发指南.md) | 开发规范 |
| [docs/架构说明.md](./docs/架构说明.md) | 架构设计 |
| [docs/DATABASE_MIGRATION.md](./docs/DATABASE_MIGRATION.md) | 数据库迁移指南 |
本项目已配置 Drone CI/CD 实现代码推送后的全自动编译、部署流程。 ## 相关仓库
``` | 仓库 | 说明 |
开发者 Push 代码 |------|------|
| [martial-master](https://git.waypeak.work/martial/martial-master) | 后端 API |
Gitea 仓库git.waypeak.work | [martial-web](https://git.waypeak.work/martial/martial-web) | 管理后台前端 |
↓ [Webhook 触发] | [martial-mini](https://git.waypeak.work/martial/martial-mini) | 用户端小程序 |
Drone CI Servermartial-ci.johnsion.club | [martial-admin-mini](https://git.waypeak.work/martial/martial-admin-mini) | 裁判端小程序 |
↓ [Runner 执行]
编译 BladeX 框架 → 编译后端项目 → 构建 Docker 镜像 → 部署容器 → 健康检查
生产服务器部署完成martial-api.johnsion.club
```
### 部署流程 ## 许可协议
**日常开发(不触发部署):** 本项目基于 **BladeX 商业框架** 构建,需遵守 [BladeX 商业授权许可协议](https://license.bladex.cn)。
```bash
# 1. 切换到开发分支
git checkout dev
# 2. 修改代码并提交
git add .
git commit -m "feat: 添加新功能"
git push origin dev
# ✅ 推送到 dev 分支不会触发自动部署
```
**发布到生产环境:**
```bash
# 1. 合并开发分支到 main
git checkout main
git merge dev
# 2. 推送到 main 分支(自动触发部署)
git push origin main
# 3. 查看部署进度
# 访问 Drone UI: https://martial-ci.johnsion.club
# 或等待约 5-6 分钟后直接访问生产环境
```
### 部署步骤(全自动)
1. **编译完整项目**约4-5分钟
- 克隆 BladeX 框架代码martial-tool
- 编译框架并安装到 Maven 本地仓库
- 编译后端项目martial-master
- 生成 blade-api.jar约236MB
2. **构建 Docker 镜像**约1分钟
- 基于 eclipse-temurin:17-jre-alpine
- 复制 JAR 文件和配置
- 构建轻量化镜像
3. **部署到生产环境**约30秒
- 停止旧容器
- 启动新容器
- 连接数据库和 Redis
4. **健康检查**约45秒
- 等待 Spring Boot 应用完全启动
- 检查健康端点: `/actuator/health`
- 验证部署成功
**总耗时:** 约 6-7 分钟
### 访问地址
**部署完成后:**
- 后端 API: https://martial-api.johnsion.club
- API 文档: https://martial-doc.johnsion.club
- 健康检查: https://martial-api.johnsion.club/actuator/health
- 前端系统: https://martial.johnsion.club
**CI/CD 管理:**
- Drone UI: https://martial-ci.johnsion.club
### 部署配置
**生产服务器:**
- MySQL 8.0 (Docker 容器)
- Redis 7 (Docker 容器)
- Docker Network: martial_martial-network
**环境变量配置在 docker-compose.yml**
```yaml
SPRING_PROFILE: prod
JAVA_OPTS: "-Xms512m -Xmx1024m"
```
### 故障排查
**查看部署日志:**
```bash
# Drone 构建日志
访问: https://martial-ci.johnsion.club
# 应用日志
ssh root@154.30.6.21
docker logs -f martial-backend
```
**检查服务状态:**
```bash
# 查看容器状态
docker ps | grep martial
# 查看健康状态
curl https://martial-api.johnsion.club/actuator/health
# 重启服务
cd /app/martial && docker-compose restart backend
```
详细部署文档请参考:[docs/CI-CD部署总结.md](./docs/CI-CD部署总结.md)
## 📚 开发文档
- **[CLAUDE.md](./CLAUDE.md)** - 项目完整说明、构建命令、技术栈
- **[docs/README.md](./docs/README.md)** - 文档索引和快速导航
- **[docs/架构说明.md](./docs/架构说明.md)** - BladeX 架构设计说明
- **[docs/前后端架构说明.md](./docs/前后端架构说明.md)** - 前后端分离架构
- **[docs/开发指南.md](./docs/开发指南.md)** - 开发规范和最佳实践
- **[docs/CI-CD部署总结.md](./docs/CI-CD部署总结.md)** - CI/CD 配置和运维
## 🗄️ 数据库
**连接信息(生产环境):**
- Host: 容器内使用 `martial-mysql`
- Port: 3306
- Database: martial_db
- Username: root
- Password: WtcSecure901faf1ac4d32e2bPwd
**数据库脚本:**
- BladeX 框架表: `database/bladex/bladex.mysql.all.create.sql`
- Flowable 工作流表: `database/flowable/flowable.mysql.all.create.sql`
- 武术业务表: `database/martial-db/martial_db.sql`
## 🔧 配置说明
**配置文件优先级:**
```
application.yml (基础配置)
application-{profile}.yml (环境配置)
环境变量 (Docker 容器配置)
```
**环境切换:**
```bash
# 开发环境
mvn spring-boot:run -Dspring-boot.run.profiles=dev
# 测试环境
java -jar blade-api.jar --spring.profiles.active=test
# 生产环境Docker
SPRING_PROFILE=prod
```
## 🔐 安全配置
- **Token 认证**: 无状态 Token 机制
- **多租户隔离**: 基于 tenant_id 的数据隔离
- **权限控制**: RBAC 角色权限体系
- **SQL 监控**: Druid 数据库连接池监控
- **API 文档**: 生产环境可配置访问控制
## 📊 监控和管理
- **API 文档**: https://martial-doc.johnsion.club
- **Druid 监控**: https://martial-api.johnsion.club/druid
- 用户名: blade
- 密码: 1qaz@WSX
- **健康检查**: https://martial-api.johnsion.club/actuator/health
- **CI/CD 管理**: https://martial-ci.johnsion.club
## 🤝 贡献指南
1. Fork 本仓库
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'feat: Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 提交 Pull Request
**提交规范:**
```
feat: 新功能
fix: 修复 Bug
docs: 文档更新
style: 代码格式调整
refactor: 重构
perf: 性能优化
test: 测试相关
chore: 构建/工具配置
```
## 👥 开发团队
- **开发者**: JohnSion
- **AI 助手**: Claude Code
- **基础框架**: BladeX 4.0.1 (上海布雷德科技有限公司)
## 📄 许可协议
### BladeX 商业授权
本项目基于 **BladeX 商业框架** 构建,需遵守以下协议:
#### 版权声明
- BladeX 是一个商业化软件,系列产品知识产权归**上海布雷德科技有限公司**独立所有
- 您一旦开始复制、下载、安装或者使用本产品,即被视为完全理解并接受本协议的各项条款
- 更多详情请看:[BladeX商业授权许可协议](https://license.bladex.cn)
#### 授权范围
- **专业版**:只可用于**个人学习**及**个人私活**项目,不可用于公司或团队,不可泄露给任何第三方
- **企业版**:可用于**企业名下**的任何项目,企业版员工在**未购买**专业版授权前,只授权开发**所在授权企业名下**的项目,**不得将BladeX用于个人私活**
- **共同遵守**若甲方需要您提供项目源码则需代为甲方购买BladeX企业授权甲方购买后续的所有项目都无需再次购买授权
#### 商用权益
- ✔️ 遵守[商业协议](https://license.bladex.cn)的前提下将BladeX系列产品用于授权范围内的商用项目并上线运营
- ✔️ 遵守[商业协议](https://license.bladex.cn)的前提下,不限制项目数,不限制服务器数
- ✔️ 遵守[商业协议](https://license.bladex.cn)的前提下,将自行编写的业务代码申请软件著作权
#### 何为侵权
- ❌ 不遵守商业协议,私自销售商业源码
- ❌ 以任何理由将BladeX源码用于申请软件著作权
- ❌ 将商业源码以任何途径任何理由泄露给未授权的单位或个人
- ❌ 开发完毕项目没有为甲方购买企业授权向甲方提供了BladeX代码
- ❌ 基于BladeX拓展研发与BladeX有竞争关系的衍生框架并将其开源或销售
#### 侵权后果
- 情节较轻:第一次发现警告处理
- 情节较重:封禁账号,踢出商业群,并保留追究法律责任的权利
- 情节严重:与本地律师事务所合作,以公司名义起诉侵犯计算机软件著作权
#### 技术支持
- **答疑时间**: 工作日 9:00 ~ 17:00周末、节假日休息
- **技术社区**: https://sns.bladex.cn
- **官方QQ**: 1272154962
--- ---
**最后更新**: 2025-11-30 **最后更新**: 2024-12-29
**项目版本**: 4.0.1 RELEASE
**部署环境**: Docker + Drone CI/CD

View File

@@ -1,53 +0,0 @@
-- ================================================================
-- 【紧急修复】场地表字段缺失问题 - 直接复制执行此脚本
-- 问题Unknown column 'max_capacity' in 'field list'
-- 解决:重建 martial_venue 表,包含所有必需字段
-- 日期2025-12-06
-- ================================================================
-- 使用正确的数据库
USE martial_db;
-- 删除旧表(如果有重要数据,请先备份!)
DROP TABLE IF EXISTS `martial_venue`;
-- 创建新表,包含完整字段
CREATE TABLE `martial_venue` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(12) DEFAULT '000000' COMMENT '租户ID',
`competition_id` bigint(20) NOT NULL COMMENT '赛事ID',
`venue_name` varchar(100) NOT NULL COMMENT '场地名称',
`venue_code` varchar(50) DEFAULT NULL COMMENT '场地编码',
`max_capacity` int(11) DEFAULT 100 COMMENT '最大容纳人数',
`location` varchar(200) DEFAULT NULL COMMENT '位置/地点',
`description` varchar(500) DEFAULT NULL COMMENT '场地描述',
`facilities` varchar(500) DEFAULT NULL COMMENT '场地设施',
`sort_order` int(11) DEFAULT 0 COMMENT '排序',
`status` int(2) DEFAULT 1 COMMENT '状态(0-禁用,1-启用)',
`create_user` bigint(20) DEFAULT NULL COMMENT '创建人',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`is_deleted` int(2) DEFAULT 0 COMMENT '是否已删除',
PRIMARY KEY (`id`),
KEY `idx_competition_id` (`competition_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='场地信息表';
-- 验证表已创建成功
DESC martial_venue;
-- 检查 max_capacity 字段
SELECT '✓ martial_venue 表已成功重建,包含 max_capacity 字段' AS ;
-- 显示所有字段
SELECT
COLUMN_NAME AS ,
COLUMN_TYPE AS ,
COLUMN_DEFAULT AS ,
COLUMN_COMMENT AS
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'martial_db'
AND TABLE_NAME = 'martial_venue'
ORDER BY ORDINAL_POSITION;

View File

@@ -1,73 +0,0 @@
-- 检查赛事基础数据是否完整
USE martial_db;
-- 1. 检查赛事信息
SELECT
'赛事信息' AS '检查项',
COUNT(*) AS '记录数'
FROM martial_competition
WHERE id = 200;
-- 2. 检查参赛者数据
SELECT
'参赛者数据' AS '检查项',
COUNT(*) AS '记录数'
FROM martial_athlete
WHERE competition_id = 200;
-- 3. 检查场地数据
SELECT
'场地数据' AS '检查项',
COUNT(*) AS '记录数'
FROM martial_venue
WHERE competition_id = 200;
-- 4. 检查项目数据
SELECT
'项目数据' AS '检查项',
COUNT(*) AS '记录数'
FROM martial_project
WHERE id IN (
SELECT DISTINCT project_id
FROM martial_athlete
WHERE competition_id = 200
);
-- 5. 检查赛事时间配置
SELECT
id AS '赛事ID',
competition_name AS '赛事名称',
competition_start_time AS '开始时间',
competition_end_time AS '结束时间',
CASE
WHEN competition_start_time IS NULL THEN '⚠ 未配置'
WHEN competition_end_time IS NULL THEN '⚠ 未配置'
ELSE '✓ 已配置'
END AS '时间配置状态'
FROM martial_competition
WHERE id = 200;
-- 6. 详细检查参赛者项目分布
SELECT
p.project_name AS '项目名称',
p.type AS '项目类型(1=个人,2=双人,3=集体)',
COUNT(*) AS '参赛人数'
FROM martial_athlete a
LEFT JOIN martial_project p ON a.project_id = p.id
WHERE a.competition_id = 200
GROUP BY p.id, p.project_name, p.type
ORDER BY p.type, p.project_name;
-- 7. 检查场地详情
SELECT
id AS '场地ID',
venue_name AS '场地名称',
venue_type AS '场地类型',
capacity AS '容量'
FROM martial_venue
WHERE competition_id = 200;
-- 总结
SELECT
'数据检查完成' AS '状态',
NOW() AS '检查时间';

View File

@@ -1,26 +0,0 @@
-- ================================================================
-- 场地表结构检查和修复脚本
-- 用途:检查 martial_venue 表是否存在 max_capacity 字段,如果不存在则添加
-- 日期2025-12-06
-- ================================================================
-- 检查表是否存在
SELECT
TABLE_NAME,
CASE
WHEN TABLE_NAME IS NOT NULL THEN '表存在'
ELSE '表不存在'
END AS status
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'martial_venue';
-- 查看当前表结构
DESC martial_venue;
-- 查看所有字段
SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_DEFAULT, IS_NULLABLE, COLUMN_COMMENT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'martial_venue'
ORDER BY ORDINAL_POSITION;

View File

@@ -1,85 +0,0 @@
-- ================================================================
-- 清理所有测试数据脚本
-- 用途:清空所有业务数据,保留表结构
-- 日期2025-12-06
-- 警告:此脚本会删除所有业务数据,请谨慎使用!
-- ================================================================
-- 设置外键检查为0允许删除有外键关联的数据
SET FOREIGN_KEY_CHECKS = 0;
-- 1. 清空赛事相关表
-- ================================================================
TRUNCATE TABLE `martial_competition`;
TRUNCATE TABLE `martial_banner`;
-- 2. 清空项目相关表
-- ================================================================
TRUNCATE TABLE `martial_project`;
-- 3. 清空场地相关表
-- ================================================================
TRUNCATE TABLE `martial_venue`;
-- 4. 清空参赛者/运动员相关表
-- ================================================================
TRUNCATE TABLE `martial_athlete`;
TRUNCATE TABLE `martial_participant`;
-- 5. 清空报名订单相关表
-- ================================================================
TRUNCATE TABLE `martial_registration_order`;
-- 6. 清空裁判相关表
-- ================================================================
TRUNCATE TABLE `martial_referee`;
-- 7. 清空成绩相关表
-- ================================================================
TRUNCATE TABLE `martial_score`;
-- 8. 清空赛程编排相关表(如果存在)
-- ================================================================
-- TRUNCATE TABLE `martial_schedule`;
-- TRUNCATE TABLE `martial_schedule_detail`;
-- 9. 清空信息发布相关表
-- ================================================================
TRUNCATE TABLE `martial_info_publish`;
-- 重新启用外键检查
SET FOREIGN_KEY_CHECKS = 1;
-- ================================================================
-- 验证清理结果
-- ================================================================
SELECT
'赛事数据' AS ,
COUNT(*) AS
FROM martial_competition
UNION ALL
SELECT '项目数据', COUNT(*) FROM martial_project
UNION ALL
SELECT '场地数据', COUNT(*) FROM martial_venue
UNION ALL
SELECT '参赛者数据', COUNT(*) FROM martial_athlete
UNION ALL
SELECT '报名订单数据', COUNT(*) FROM martial_registration_order
UNION ALL
SELECT '裁判数据', COUNT(*) FROM martial_referee
UNION ALL
SELECT '成绩数据', COUNT(*) FROM martial_score
UNION ALL
SELECT '信息发布数据', COUNT(*) FROM martial_info_publish;
-- ================================================================
-- 清理完成
-- ================================================================
-- 所有业务数据已清空,表结构保留
-- 您现在可以重新测试完整的业务流程:
-- 1. 创建赛事
-- 2. 配置场地
-- 3. 创建项目
-- 4. 添加参赛者
-- 5. 进行编排
-- ================================================================

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

@@ -1,26 +0,0 @@
-- 调试检查脚本
USE martial_db;
-- 检查参赛者的project_id是否都有对应的项目
SELECT
'检查参赛者项目关联' AS check_item,
a.id,
a.project_id,
a.player_name,
p.id AS project_exists,
p.project_name,
p.type AS project_type
FROM martial_athlete a
LEFT JOIN martial_project p ON a.project_id = p.id
WHERE a.competition_id = 200
LIMIT 10;
-- 检查是否有参赛者的project_id为NULL或找不到对应项目
SELECT
'检查异常数据' AS check_item,
COUNT(*) AS total_athletes,
SUM(CASE WHEN project_id IS NULL THEN 1 ELSE 0 END) AS null_project_id,
SUM(CASE WHEN p.id IS NULL THEN 1 ELSE 0 END) AS project_not_found
FROM martial_athlete a
LEFT JOIN martial_project p ON a.project_id = p.id
WHERE a.competition_id = 200;

View File

@@ -1,93 +0,0 @@
@echo off
REM =============================================
REM 赛程编排系统数据库部署脚本
REM =============================================
echo.
echo ========================================
echo 赛程编排系统 - 数据库部署工具
echo ========================================
echo.
REM 检查MySQL是否安装
where mysql >nul 2>&1
if %errorlevel% neq 0 (
echo [错误] 未找到MySQL命令请确保MySQL已安装并添加到系统PATH
echo.
echo 常见MySQL安装路径:
echo - C:\Program Files\MySQL\MySQL Server 8.0\bin
echo - C:\xampp\mysql\bin
echo.
pause
exit /b 1
)
echo [1/3] 检测到MySQL...
REM 设置数据库信息
set DB_NAME=martial_db
set SCRIPT_PATH=%~dp0deploy_schedule_tables.sql
echo [2/3] 准备执行SQL脚本...
echo 数据库: %DB_NAME%
echo 脚本: %SCRIPT_PATH%
echo.
REM 提示用户输入密码
echo 请输入MySQL root密码 (如果没有密码直接按回车):
set /p MYSQL_PWD=密码:
echo.
echo [3/3] 正在执行SQL脚本...
echo.
REM 执行SQL脚本
if "%MYSQL_PWD%"=="" (
mysql -u root %DB_NAME% < "%SCRIPT_PATH%"
) else (
mysql -u root -p%MYSQL_PWD% %DB_NAME% < "%SCRIPT_PATH%"
)
if %errorlevel% equ 0 (
echo.
echo ========================================
echo ✓ 数据库表创建成功!
echo ========================================
echo.
echo 已创建以下4张表:
echo 1. martial_schedule_group - 赛程编排分组表
echo 2. martial_schedule_detail - 赛程编排明细表
echo 3. martial_schedule_participant - 参赛者关联表
echo 4. martial_schedule_status - 编排状态表
echo.
echo 下一步:
echo 1. 导入测试数据 (可选)
echo cd ..\..\..
echo cd martial-web\test-data
echo mysql -u root -p%MYSQL_PWD% martial_db ^< create_100_team_participants.sql
echo.
echo 2. 启动后端服务
echo cd martial-master
echo mvn spring-boot:run
echo.
echo 3. 访问前端页面
echo http://localhost:3000/martial/schedule?competitionId=200
echo.
) else (
echo.
echo ========================================
echo ✗ 数据库表创建失败!
echo ========================================
echo.
echo 可能的原因:
echo 1. 数据库 %DB_NAME% 不存在
echo 2. MySQL密码错误
echo 3. 权限不足
echo.
echo 解决方法:
echo 1. 先创建数据库: CREATE DATABASE martial_db;
echo 2. 检查MySQL密码是否正确
echo 3. 确保用户有CREATE TABLE权限
echo.
)
pause

View File

@@ -1,159 +0,0 @@
-- =============================================
-- 武术赛事赛程编排系统 - 数据库表创建脚本(带数据库选择)
-- =============================================
-- 创建日期: 2025-12-09
-- 版本: v1.1
-- 说明: 自动选择正确的数据库并创建赛程编排相关的4张核心表
-- =============================================
-- 选择数据库(根据实际情况修改)
USE martial_db;
-- 检查表是否已存在,如果存在则删除(可选,生产环境请注释掉)
-- DROP TABLE IF EXISTS martial_schedule_participant;
-- DROP TABLE IF EXISTS martial_schedule_detail;
-- DROP TABLE IF EXISTS martial_schedule_group;
-- DROP TABLE IF EXISTS martial_schedule_status;
-- 1. 赛程编排分组表
CREATE TABLE IF NOT EXISTS `martial_schedule_group` (
`id` bigint(0) NOT NULL COMMENT '主键ID',
`competition_id` bigint(0) NOT NULL COMMENT '赛事ID',
`group_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '分组名称(如:太极拳男组)',
`project_id` bigint(0) NOT NULL COMMENT '项目ID',
`project_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '项目名称',
`category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组别(成年组、少年组等)',
`project_type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '项目类型(1=个人 2=集体)',
`display_order` int(0) NOT NULL DEFAULT 0 COMMENT '显示顺序(集体项目优先,数字越小越靠前)',
`total_participants` int(0) NULL DEFAULT 0 COMMENT '总参赛人数',
`total_teams` int(0) NULL DEFAULT 0 COMMENT '总队伍数(仅集体项目)',
`estimated_duration` int(0) NULL DEFAULT 0 COMMENT '预计时长(分钟)',
`create_user` bigint(0) NULL DEFAULT NULL,
`create_dept` bigint(0) NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`update_user` bigint(0) NULL DEFAULT NULL,
`update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
`status` int(0) NULL DEFAULT 1 COMMENT '状态(1-启用,2-禁用)',
`is_deleted` int(0) NULL DEFAULT 0,
`tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '000000',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_competition` (`competition_id`) USING BTREE,
INDEX `idx_project` (`project_id`) USING BTREE,
INDEX `idx_display_order` (`display_order`) USING BTREE,
INDEX `idx_tenant` (`tenant_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '赛程编排分组表' ROW_FORMAT = Dynamic;
-- 2. 赛程编排明细表(场地时间段分配)
CREATE TABLE IF NOT EXISTS `martial_schedule_detail` (
`id` bigint(0) NOT NULL COMMENT '主键ID',
`schedule_group_id` bigint(0) NOT NULL COMMENT '分组ID',
`competition_id` bigint(0) NOT NULL COMMENT '赛事ID',
`venue_id` bigint(0) NOT NULL COMMENT '场地ID',
`venue_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '场地名称',
`schedule_date` date NOT NULL COMMENT '比赛日期',
`time_period` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '时间段(morning/afternoon)',
`time_slot` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '时间点(08:30/13:30)',
`estimated_start_time` datetime(0) NULL DEFAULT NULL COMMENT '预计开始时间',
`estimated_end_time` datetime(0) NULL DEFAULT NULL COMMENT '预计结束时间',
`estimated_duration` int(0) NULL DEFAULT 0 COMMENT '预计时长(分钟)',
`participant_count` int(0) NULL DEFAULT 0 COMMENT '参赛人数',
`sort_order` int(0) NULL DEFAULT 0 COMMENT '场内顺序',
`create_user` bigint(0) NULL DEFAULT NULL,
`create_dept` bigint(0) NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`update_user` bigint(0) NULL DEFAULT NULL,
`update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
`status` int(0) NULL DEFAULT 1 COMMENT '状态(1-未开始,2-进行中,3-已完成)',
`is_deleted` int(0) NULL DEFAULT 0,
`tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '000000',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_group` (`schedule_group_id`) USING BTREE,
INDEX `idx_competition` (`competition_id`) USING BTREE,
INDEX `idx_venue_time` (`venue_id`, `schedule_date`, `time_slot`) USING BTREE,
INDEX `idx_tenant` (`tenant_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '赛程编排明细表(场地时间段分配)' ROW_FORMAT = Dynamic;
-- 3. 赛程编排参赛者关联表
CREATE TABLE IF NOT EXISTS `martial_schedule_participant` (
`id` bigint(0) NOT NULL COMMENT '主键ID',
`schedule_detail_id` bigint(0) NOT NULL COMMENT '编排明细ID',
`schedule_group_id` bigint(0) NOT NULL COMMENT '分组ID',
`participant_id` bigint(0) NOT NULL COMMENT '参赛者ID(关联martial_athlete表)',
`organization` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '单位名称',
`player_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '选手姓名',
`project_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '项目名称',
`category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组别',
`performance_order` int(0) NULL DEFAULT 0 COMMENT '出场顺序',
`create_user` bigint(0) NULL DEFAULT NULL,
`create_dept` bigint(0) NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`update_user` bigint(0) NULL DEFAULT NULL,
`update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
`status` int(0) NULL DEFAULT 1 COMMENT '状态(1-待出场,2-已出场)',
`is_deleted` int(0) NULL DEFAULT 0,
`tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '000000',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_detail` (`schedule_detail_id`) USING BTREE,
INDEX `idx_group` (`schedule_group_id`) USING BTREE,
INDEX `idx_participant` (`participant_id`) USING BTREE,
INDEX `idx_tenant` (`tenant_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '赛程编排参赛者关联表' ROW_FORMAT = Dynamic;
-- 4. 赛程编排状态表
CREATE TABLE IF NOT EXISTS `martial_schedule_status` (
`id` bigint(0) NOT NULL COMMENT '主键ID',
`competition_id` bigint(0) NOT NULL COMMENT '赛事ID(唯一)',
`schedule_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '编排状态(0=未编排 1=编排中 2=已保存锁定)',
`last_auto_schedule_time` datetime(0) NULL DEFAULT NULL COMMENT '最后自动编排时间',
`locked_time` datetime(0) NULL DEFAULT NULL COMMENT '锁定时间',
`locked_by` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '锁定人',
`total_groups` int(0) NULL DEFAULT 0 COMMENT '总分组数',
`total_participants` int(0) NULL DEFAULT 0 COMMENT '总参赛人数',
`create_user` bigint(0) NULL DEFAULT NULL,
`create_dept` bigint(0) NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`update_user` bigint(0) NULL DEFAULT NULL,
`update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
`status` int(0) NULL DEFAULT 1 COMMENT '状态(1-启用,2-禁用)',
`is_deleted` int(0) NULL DEFAULT 0,
`tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '000000',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_competition` (`competition_id`) USING BTREE,
INDEX `idx_tenant` (`tenant_id`) USING BTREE,
INDEX `idx_schedule_status` (`schedule_status`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '赛程编排状态表' ROW_FORMAT = Dynamic;
-- 验证表是否创建成功
SELECT
'表创建完成' AS message,
COUNT(*) AS table_count
FROM information_schema.tables
WHERE table_schema = 'martial_db'
AND table_name IN (
'martial_schedule_group',
'martial_schedule_detail',
'martial_schedule_participant',
'martial_schedule_status'
);
-- =============================================
-- 使用说明
-- =============================================
--
-- 1. 确认数据库名称
-- 如果你的数据库名称不是 martial_db,请修改第9行的 USE 语句
--
-- 2. 执行脚本
-- 方式1: 在MySQL客户端中直接执行
-- mysql -u root -p < deploy_schedule_tables.sql
--
-- 方式2: 在数据库管理工具中执行(Navicat/DBeaver等)
--
-- 3. 验证
-- 执行完成后应该看到 "table_count = 4" 的结果
--
-- 4. 下一步
-- 执行测试数据导入脚本:
-- mysql -u root -p martial_db < martial-web/test-data/create_100_team_participants.sql
--
-- =============================================

View File

@@ -1,19 +0,0 @@
-- ================================================================
-- 修复参赛选手表 order_id 字段约束
-- 问题Field 'order_id' doesn't have a default value
-- 解决:允许 order_id 为 NULL支持直接添加参赛选手无需订单
-- 日期2025-12-06
-- ================================================================
-- 使用正确的数据库
USE martial_db;
-- 修改 order_id 字段,允许为 NULL
ALTER TABLE martial_athlete
MODIFY COLUMN order_id bigint(20) NULL DEFAULT NULL COMMENT '订单ID';
-- 验证修改
DESC martial_athlete;
-- 显示修改结果
SELECT '✓ order_id 字段已修改为可空' AS ;

View File

@@ -1,40 +0,0 @@
-- ================================================================
-- 场地表字段修复脚本
-- 用途:为 martial_venue 表添加缺失的 max_capacity 字段
-- 问题Error: Unknown column 'max_capacity' in 'field list'
-- 日期2025-12-06
-- ================================================================
-- 方案1直接 DROP 表并重新创建(如果表中没有重要数据)
-- 如果表中有数据请跳过此步骤使用方案2
DROP TABLE IF EXISTS `martial_venue`;
CREATE TABLE `martial_venue` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(12) DEFAULT '000000' COMMENT '租户ID',
`competition_id` bigint(20) NOT NULL COMMENT '赛事ID',
`venue_name` varchar(100) NOT NULL COMMENT '场地名称',
`venue_code` varchar(50) DEFAULT NULL COMMENT '场地编码',
`max_capacity` int(11) DEFAULT 100 COMMENT '最大容纳人数',
`location` varchar(200) DEFAULT NULL COMMENT '位置/地点',
`description` varchar(500) DEFAULT NULL COMMENT '场地描述',
`facilities` varchar(500) DEFAULT NULL COMMENT '场地设施',
`sort_order` int(11) DEFAULT 0 COMMENT '排序',
`status` int(2) DEFAULT 1 COMMENT '状态(0-禁用,1-启用)',
`create_user` bigint(20) DEFAULT NULL COMMENT '创建人',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`is_deleted` int(2) DEFAULT 0 COMMENT '是否已删除',
PRIMARY KEY (`id`),
KEY `idx_competition_id` (`competition_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='场地信息表';
-- ================================================================
-- 验证表结构
-- ================================================================
DESC martial_venue;
SELECT '场地表已重新创建,包含 max_capacity 字段' AS result;

View File

@@ -1,131 +0,0 @@
-- =============================================
-- 赛程编排系统 - 完整测试数据初始化
-- =============================================
USE martial_db;
-- 1. 确保赛事存在并配置了时间
UPDATE martial_competition
SET
competition_start_time = '2025-11-06 08:00:00',
competition_end_time = '2025-11-08 18:00:00'
WHERE id = 200;
-- 检查赛事是否存在
SELECT
'1. 检查赛事' AS step,
CASE
WHEN COUNT(*) > 0 THEN CONCAT('✓ 赛事ID=200存在, 名称: ', MAX(competition_name))
ELSE '✗ 赛事ID=200不存在,请先创建赛事'
END AS result
FROM martial_competition
WHERE id = 200;
-- 2. 创建场地数据(如果不存在)
INSERT IGNORE INTO martial_venue (id, competition_id, venue_name, venue_type, capacity, create_time, is_deleted)
VALUES
(1, 200, '一号场地', '主场地', 100, NOW(), 0),
(2, 200, '二号场地', '副场地', 100, NOW(), 0),
(3, 200, '三号场地', '副场地', 100, NOW(), 0),
(4, 200, '四号场地', '副场地', 100, NOW(), 0);
SELECT
'2. 检查场地' AS step,
CONCAT('✓ 已有 ', COUNT(*), ' 个场地') AS result
FROM martial_venue
WHERE competition_id = 200 AND is_deleted = 0;
-- 3. 创建项目数据(如果不存在)
INSERT IGNORE INTO martial_project (id, project_name, type, category, estimated_duration, create_time)
VALUES
(1001, '太极拳集体', 3, '成年组', 5, NOW()),
(1002, '长拳集体', 3, '成年组', 5, NOW()),
(1003, '剑术集体', 3, '成年组', 5, NOW()),
(1004, '刀术集体', 3, '成年组', 5, NOW()),
(1005, '棍术集体', 3, '少年组', 5, NOW());
SELECT
'3. 检查项目' AS step,
CONCAT('✓ 已有 ', COUNT(*), ' 个项目') AS result
FROM martial_project
WHERE id BETWEEN 1001 AND 1005;
-- 4. 创建测试参赛者数据(少量测试数据)
DELETE FROM martial_athlete WHERE competition_id = 200;
INSERT INTO martial_athlete (
competition_id, project_id, organization, team_name,
player_name, gender, age, phone, category, create_time, is_deleted
)
VALUES
-- 太极拳集体 - 队伍1: 少林寺武校 (5人)
(200, 1001, '少林寺武校', '少林寺武校', '张明远', '', 25, '13800001001', '成年组', NOW(), 0),
(200, 1001, '少林寺武校', '少林寺武校', '李华强', '', 26, '13800001002', '成年组', NOW(), 0),
(200, 1001, '少林寺武校', '少林寺武校', '王建国', '', 24, '13800001003', '成年组', NOW(), 0),
(200, 1001, '少林寺武校', '少林寺武校', '赵小明', '', 23, '13800001004', '成年组', NOW(), 0),
(200, 1001, '少林寺武校', '少林寺武校', '刘德华', '', 27, '13800001005', '成年组', NOW(), 0),
-- 太极拳集体 - 队伍2: 武当派 (5人)
(200, 1001, '武当派', '武当派', '陈剑锋', '', 28, '13800001011', '成年组', NOW(), 0),
(200, 1001, '武当派', '武当派', '周杰伦', '', 25, '13800001012', '成年组', NOW(), 0),
(200, 1001, '武当派', '武当派', '吴彦祖', '', 26, '13800001013', '成年组', NOW(), 0),
(200, 1001, '武当派', '武当派', '郑伊健', '', 24, '13800001014', '成年组', NOW(), 0),
(200, 1001, '武当派', '武当派', '谢霆锋', '', 27, '13800001015', '成年组', NOW(), 0),
-- 长拳集体 - 队伍1: 峨眉派 (5人)
(200, 1002, '峨眉派', '峨眉派', '小龙女', '', 22, '13800002001', '成年组', NOW(), 0),
(200, 1002, '峨眉派', '峨眉派', '黄蓉', '', 23, '13800002002', '成年组', NOW(), 0),
(200, 1002, '峨眉派', '峨眉派', '赵敏', '', 24, '13800002003', '成年组', NOW(), 0),
(200, 1002, '峨眉派', '峨眉派', '周芷若', '', 22, '13800002004', '成年组', NOW(), 0),
(200, 1002, '峨眉派', '峨眉派', '任盈盈', '', 23, '13800002005', '成年组', NOW(), 0),
-- 长拳集体 - 队伍2: 华山派 (5人)
(200, 1002, '华山派', '华山派', '令狐冲', '', 27, '13800002011', '成年组', NOW(), 0),
(200, 1002, '华山派', '华山派', '风清扬', '', 28, '13800002012', '成年组', NOW(), 0),
(200, 1002, '华山派', '华山派', '岳不群', '', 29, '13800002013', '成年组', NOW(), 0),
(200, 1002, '华山派', '华山派', '宁中则', '', 26, '13800002014', '成年组', NOW(), 0),
(200, 1002, '华山派', '华山派', '岳灵珊', '', 24, '13800002015', '成年组', NOW(), 0);
SELECT
'4. 检查参赛者' AS step,
CONCAT('✓ 已有 ', COUNT(*), ' 个参赛者 (', COUNT(DISTINCT organization), ' 个队伍)') AS result
FROM martial_athlete
WHERE competition_id = 200 AND is_deleted = 0;
-- 5. 清空旧的编排数据(如果有)
DELETE FROM martial_schedule_participant WHERE schedule_group_id IN (
SELECT id FROM martial_schedule_group WHERE competition_id = 200
);
DELETE FROM martial_schedule_detail WHERE competition_id = 200;
DELETE FROM martial_schedule_group WHERE competition_id = 200;
DELETE FROM martial_schedule_status WHERE competition_id = 200;
SELECT '5. 清理旧数据' AS step, '✓ 已清空旧的编排数据' AS result;
-- 6. 最终验证
SELECT
'6. 数据完整性检查' AS step,
CONCAT(
'✓ 赛事: ', (SELECT COUNT(*) FROM martial_competition WHERE id = 200),
', 场地: ', (SELECT COUNT(*) FROM martial_venue WHERE competition_id = 200 AND is_deleted = 0),
', 项目: ', (SELECT COUNT(*) FROM martial_project WHERE id BETWEEN 1001 AND 1005),
', 参赛者: ', (SELECT COUNT(*) FROM martial_athlete WHERE competition_id = 200 AND is_deleted = 0)
) AS result;
-- 7. 检查赛事时间配置
SELECT
'7. 赛事时间配置' AS step,
CONCAT(
'开始: ', IFNULL(competition_start_time, '未配置'),
', 结束: ', IFNULL(competition_end_time, '未配置')
) AS result
FROM martial_competition
WHERE id = 200;
SELECT
'========================================' AS '',
'✓ 测试数据初始化完成!' AS result,
'========================================' AS '';
SELECT
'下一步: 测试API' AS action,
'curl -X POST http://localhost:8123/martial/schedule/auto-arrange -H "Content-Type: application/json" -d "{\"competitionId\": 200}"' AS command;

View File

@@ -1,82 +0,0 @@
-- =====================================================
-- 插入测试裁判邀请数据
-- 执行时间: 2025-12-12
-- =====================================================
USE blade;
-- 首先确保有测试赛事数据
-- 假设已经有赛事ID为 1 的数据
-- 首先确保有测试裁判数据
-- 插入测试裁判(如果不存在)
INSERT IGNORE INTO martial_judge (id, name, gender, phone, id_card, referee_type, level, specialty, create_time, update_time, status, is_deleted)
VALUES
(1, '张三', 1, '13800138001', '110101199001011234', 2, '国家级', '太极拳', NOW(), NOW(), 1, 0),
(2, '李四', 1, '13800138002', '110101199002021234', 2, '一级', '长拳', NOW(), NOW(), 1, 0),
(3, '王五', 2, '13800138003', '110101199003031234', 2, '二级', '剑术', NOW(), NOW(), 1, 0),
(4, '赵六', 1, '13800138004', '110101199004041234', 1, '国家级', '刀术', NOW(), NOW(), 1, 0),
(5, '钱七', 2, '13800138005', '110101199005051234', 2, '三级', '棍术', NOW(), NOW(), 1, 0);
-- 插入测试邀请数据
INSERT INTO martial_judge_invite (
id,
competition_id,
judge_id,
invite_code,
role,
invite_status,
invite_time,
reply_time,
reply_note,
contact_phone,
contact_email,
invite_message,
expire_time,
is_used,
create_time,
update_time,
status,
is_deleted
)
VALUES
-- 待回复的邀请
(1, 1, 1, 'INV2025001', 'judge', 0, NOW(), NULL, NULL, '13800138001', 'zhangsan@example.com', '诚邀您担任本次武术比赛的裁判', DATE_ADD(NOW(), INTERVAL 30 DAY), 0, NOW(), NOW(), 1, 0),
(2, 1, 2, 'INV2025002', 'judge', 0, NOW(), NULL, NULL, '13800138002', 'lisi@example.com', '诚邀您担任本次武术比赛的裁判', DATE_ADD(NOW(), INTERVAL 30 DAY), 0, NOW(), NOW(), 1, 0),
-- 已接受的邀请
(3, 1, 3, 'INV2025003', 'judge', 1, DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY), '很荣幸能参加,我会准时到场', '13800138003', 'wangwu@example.com', '诚邀您担任本次武术比赛的裁判', DATE_ADD(NOW(), INTERVAL 30 DAY), 1, DATE_SUB(NOW(), INTERVAL 2 DAY), NOW(), 1, 0),
(4, 1, 4, 'INV2025004', 'chief_judge', 1, DATE_SUB(NOW(), INTERVAL 3 DAY), DATE_SUB(NOW(), INTERVAL 2 DAY), '感谢邀请,我会认真履行裁判长职责', '13800138004', 'zhaoliu@example.com', '诚邀您担任本次武术比赛的裁判长', DATE_ADD(NOW(), INTERVAL 30 DAY), 1, DATE_SUB(NOW(), INTERVAL 3 DAY), NOW(), 1, 0),
-- 已拒绝的邀请
(5, 1, 5, 'INV2025005', 'judge', 2, DATE_SUB(NOW(), INTERVAL 5 DAY), DATE_SUB(NOW(), INTERVAL 4 DAY), '非常抱歉,那段时间有其他安排', '13800138005', 'qianqi@example.com', '诚邀您担任本次武术比赛的裁判', DATE_ADD(NOW(), INTERVAL 30 DAY), 0, DATE_SUB(NOW(), INTERVAL 5 DAY), NOW(), 1, 0);
-- 验证插入结果
SELECT
ji.id,
ji.invite_code,
j.name AS judge_name,
j.level AS judge_level,
ji.contact_phone,
ji.contact_email,
ji.invite_status,
CASE ji.invite_status
WHEN 0 THEN '待回复'
WHEN 1 THEN '已接受'
WHEN 2 THEN '已拒绝'
WHEN 3 THEN '已取消'
ELSE '未知'
END AS status_text,
ji.invite_time,
ji.reply_time,
ji.reply_note
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.id;
SELECT 'Test data inserted successfully!' AS status;

View File

@@ -0,0 +1,37 @@
-- 赛事通用附件表
-- 支持多种附件类型:赛事发布(info)、赛事规程(rules)、活动日程(schedule)、成绩(results)、奖牌榜(medals)、图片直播(photos)
DROP TABLE IF EXISTS `martial_competition_attachment`;
CREATE TABLE `martial_competition_attachment` (
`id` bigint NOT NULL COMMENT '主键ID',
`tenant_id` varchar(12) DEFAULT '000000' COMMENT '租户ID',
`competition_id` bigint NOT NULL COMMENT '赛事ID',
`attachment_type` varchar(20) NOT NULL COMMENT '附件类型info-赛事发布, rules-赛事规程, schedule-活动日程, results-成绩, medals-奖牌榜, photos-图片直播',
`file_name` varchar(255) NOT NULL COMMENT '文件名称',
`file_url` varchar(500) NOT NULL COMMENT '文件URL',
`file_size` bigint DEFAULT NULL COMMENT '文件大小(字节)',
`file_type` varchar(20) DEFAULT NULL COMMENT '文件类型pdf/doc/docx/xls/xlsx/jpg/png等',
`order_num` int DEFAULT 0 COMMENT '排序序号',
`status` int DEFAULT 1 COMMENT '状态1-启用 0-禁用)',
`create_user` bigint DEFAULT NULL COMMENT '创建人',
`create_dept` bigint DEFAULT NULL COMMENT '创建部门',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_user` bigint DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` int DEFAULT 0 COMMENT '是否已删除0-否 1-是)',
PRIMARY KEY (`id`),
KEY `idx_competition_id` (`competition_id`),
KEY `idx_attachment_type` (`attachment_type`),
KEY `idx_competition_type` (`competition_id`, `attachment_type`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='赛事通用附件表';
-- 插入测试数据假设赛事ID为1
INSERT INTO `martial_competition_attachment` (`id`, `tenant_id`, `competition_id`, `attachment_type`, `file_name`, `file_url`, `file_size`, `file_type`, `order_num`, `status`) VALUES
(1, '000000', 1, 'info', '2025年郑州武术大赛通知.pdf', 'http://example.com/files/notice.pdf', 1258291, 'pdf', 1, 1),
(2, '000000', 1, 'rules', '2025年郑州武术大赛竞赛规程.pdf', 'http://example.com/files/rules.pdf', 2621440, 'pdf', 1, 1),
(3, '000000', 1, 'rules', '参赛报名表.pdf', 'http://example.com/files/form.pdf', 163840, 'pdf', 2, 1),
(4, '000000', 1, 'schedule', '比赛日程安排表.pdf', 'http://example.com/files/schedule.pdf', 911360, 'pdf', 1, 1),
(5, '000000', 1, 'results', '比赛成绩公告.pdf', 'http://example.com/files/results.pdf', 1887436, 'pdf', 1, 1),
(6, '000000', 1, 'medals', '奖牌榜统计.pdf', 'http://example.com/files/medals.pdf', 532480, 'pdf', 1, 1),
(7, '000000', 1, 'photos', '比赛精彩瞬间.pdf', 'http://example.com/files/photos.pdf', 16357785, 'pdf', 1, 1);

File diff suppressed because one or more lines are too long

View File

@@ -1,82 +0,0 @@
@echo off
REM =============================================
REM 赛程编排系统 - 数据库升级脚本
REM =============================================
echo.
echo ========================================
echo 赛程编排系统 - 数据库升级工具
echo ========================================
echo.
echo 说明: 此脚本会创建新的4张表,不会影响现有数据
echo - martial_schedule_group
echo - martial_schedule_detail
echo - martial_schedule_participant
echo - martial_schedule_status
echo.
REM 检查MySQL
where mysql >nul 2>&1
if %errorlevel% neq 0 (
echo [错误] 未找到MySQL命令
echo.
echo 请使用以下方法之一:
echo 方法1: 在Navicat/DBeaver中打开并执行 upgrade_schedule_system.sql
echo 方法2: 将MySQL添加到系统PATH后重新运行此脚本
echo.
pause
exit /b 1
)
set DB_NAME=martial_db
set SCRIPT_PATH=%~dp0upgrade_schedule_system.sql
echo [1/2] 检测到MySQL...
echo.
echo 请输入MySQL root密码 (无密码直接回车):
set /p MYSQL_PWD=密码:
echo.
echo [2/2] 正在执行升级脚本...
echo.
if "%MYSQL_PWD%"=="" (
mysql -u root %DB_NAME% < "%SCRIPT_PATH%"
) else (
mysql -u root -p%MYSQL_PWD% %DB_NAME% < "%SCRIPT_PATH%"
)
if %errorlevel% equ 0 (
echo.
echo ========================================
echo ✓ 数据库升级成功!
echo ========================================
echo.
echo 已创建/检查以下表:
echo [新] martial_schedule_group - 赛程编排分组表
echo [新] martial_schedule_detail - 赛程编排明细表
echo [新] martial_schedule_participant - 参赛者关联表
echo [新] martial_schedule_status - 编排状态表
echo.
echo [旧] martial_schedule - 保留(如果存在)
echo [旧] martial_schedule_athlete - 保留(如果存在)
echo.
echo 下一步:
echo 1. 重启后端服务以使新表生效
echo 2. 访问前端页面测试:
echo http://localhost:3000/martial/schedule?competitionId=200
echo.
) else (
echo.
echo ========================================
echo ✗ 升级失败!
echo ========================================
echo.
echo 请检查:
echo 1. 数据库 martial_db 是否存在
echo 2. MySQL密码是否正确
echo 3. 用户是否有CREATE TABLE权限
echo.
)
pause

View File

@@ -1,75 +0,0 @@
-- =====================================================
-- 升级 martial_judge_invite 表
-- 添加邀请状态、时间、联系方式等字段
-- 执行时间: 2025-12-12
-- =====================================================
USE blade;
-- 检查表是否存在
SELECT 'Checking martial_judge_invite table...' AS status;
-- 添加邀请状态字段
ALTER TABLE martial_judge_invite
ADD COLUMN IF NOT EXISTS invite_status INT DEFAULT 0 COMMENT '邀请状态(0-待回复,1-已接受,2-已拒绝,3-已取消)';
-- 添加邀请时间字段
ALTER TABLE martial_judge_invite
ADD COLUMN IF NOT EXISTS invite_time DATETIME COMMENT '邀请时间';
-- 添加回复时间字段
ALTER TABLE martial_judge_invite
ADD COLUMN IF NOT EXISTS reply_time DATETIME COMMENT '回复时间';
-- 添加回复备注字段
ALTER TABLE martial_judge_invite
ADD COLUMN IF NOT EXISTS reply_note VARCHAR(500) COMMENT '回复备注';
-- 添加联系电话字段
ALTER TABLE martial_judge_invite
ADD COLUMN IF NOT EXISTS contact_phone VARCHAR(20) COMMENT '联系电话';
-- 添加联系邮箱字段
ALTER TABLE martial_judge_invite
ADD COLUMN IF NOT EXISTS contact_email VARCHAR(100) COMMENT '联系邮箱';
-- 添加邀请消息字段
ALTER TABLE martial_judge_invite
ADD COLUMN IF NOT EXISTS invite_message VARCHAR(1000) COMMENT '邀请消息';
-- 添加取消原因字段
ALTER TABLE martial_judge_invite
ADD COLUMN IF NOT EXISTS cancel_reason VARCHAR(500) COMMENT '取消原因';
-- 为邀请状态字段添加索引
ALTER TABLE martial_judge_invite
ADD INDEX IF NOT EXISTS idx_invite_status (invite_status);
-- 为赛事ID和邀请状态组合添加索引
ALTER TABLE martial_judge_invite
ADD INDEX IF NOT EXISTS idx_competition_status (competition_id, invite_status);
-- 验证字段是否添加成功
SELECT
COLUMN_NAME,
COLUMN_TYPE,
COLUMN_COMMENT
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
TABLE_SCHEMA = 'blade'
AND TABLE_NAME = 'martial_judge_invite'
AND COLUMN_NAME IN (
'invite_status',
'invite_time',
'reply_time',
'reply_note',
'contact_phone',
'contact_email',
'invite_message',
'cancel_reason'
)
ORDER BY
ORDINAL_POSITION;
SELECT 'Upgrade completed successfully!' AS status;

View File

@@ -1,179 +0,0 @@
-- =============================================
-- 赛程编排系统 - 增量升级脚本
-- =============================================
-- 说明: 检查并创建缺失的表,不影响现有数据
-- 版本: v1.1
-- 日期: 2025-12-09
-- =============================================
USE martial_db;
-- 检查当前已有的表
SELECT
table_name,
'已存在' AS status
FROM information_schema.tables
WHERE table_schema = 'martial_db'
AND table_name LIKE 'martial_schedule%';
-- =============================================
-- 创建新表(仅当不存在时)
-- =============================================
-- 1. 赛程编排分组表
CREATE TABLE IF NOT EXISTS `martial_schedule_group` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`competition_id` bigint(20) NOT NULL COMMENT '赛事ID',
`group_name` varchar(200) NOT NULL COMMENT '分组名称(如:太极拳男组)',
`project_id` bigint(20) NOT NULL COMMENT '项目ID',
`project_name` varchar(100) DEFAULT NULL COMMENT '项目名称',
`category` varchar(50) DEFAULT NULL COMMENT '组别(成年组、少年组等)',
`project_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '项目类型(1=个人 2=集体)',
`display_order` int(11) NOT NULL DEFAULT '0' COMMENT '显示顺序(集体项目优先,数字越小越靠前)',
`total_participants` int(11) DEFAULT '0' COMMENT '总参赛人数',
`total_teams` int(11) DEFAULT '0' COMMENT '总队伍数(仅集体项目)',
`estimated_duration` int(11) DEFAULT '0' COMMENT '预计时长(分钟)',
`create_user` bigint(20) DEFAULT NULL,
`create_dept` bigint(20) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_user` bigint(20) DEFAULT NULL,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`status` int(11) DEFAULT '1' COMMENT '状态(1-启用,2-禁用)',
`is_deleted` int(11) DEFAULT '0',
`tenant_id` varchar(12) DEFAULT '000000',
PRIMARY KEY (`id`),
KEY `idx_competition` (`competition_id`),
KEY `idx_project` (`project_id`),
KEY `idx_display_order` (`display_order`),
KEY `idx_tenant` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='赛程编排分组表';
-- 2. 赛程编排明细表(场地时间段分配)
CREATE TABLE IF NOT EXISTS `martial_schedule_detail` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`schedule_group_id` bigint(20) NOT NULL COMMENT '分组ID',
`competition_id` bigint(20) NOT NULL COMMENT '赛事ID',
`venue_id` bigint(20) NOT NULL COMMENT '场地ID',
`venue_name` varchar(100) DEFAULT NULL COMMENT '场地名称',
`schedule_date` date NOT NULL COMMENT '比赛日期',
`time_period` varchar(20) NOT NULL COMMENT '时间段(morning/afternoon)',
`time_slot` varchar(20) NOT NULL COMMENT '时间点(08:30/13:30)',
`estimated_start_time` datetime DEFAULT NULL COMMENT '预计开始时间',
`estimated_end_time` datetime DEFAULT NULL COMMENT '预计结束时间',
`estimated_duration` int(11) DEFAULT '0' COMMENT '预计时长(分钟)',
`participant_count` int(11) DEFAULT '0' COMMENT '参赛人数',
`sort_order` int(11) DEFAULT '0' COMMENT '场内顺序',
`create_user` bigint(20) DEFAULT NULL,
`create_dept` bigint(20) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_user` bigint(20) DEFAULT NULL,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`status` int(11) DEFAULT '1' COMMENT '状态(1-未开始,2-进行中,3-已完成)',
`is_deleted` int(11) DEFAULT '0',
`tenant_id` varchar(12) DEFAULT '000000',
PRIMARY KEY (`id`),
KEY `idx_group` (`schedule_group_id`),
KEY `idx_competition` (`competition_id`),
KEY `idx_venue_time` (`venue_id`,`schedule_date`,`time_slot`),
KEY `idx_tenant` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='赛程编排明细表(场地时间段分配)';
-- 3. 赛程编排参赛者关联表
CREATE TABLE IF NOT EXISTS `martial_schedule_participant` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`schedule_detail_id` bigint(20) NOT NULL COMMENT '编排明细ID',
`schedule_group_id` bigint(20) NOT NULL COMMENT '分组ID',
`participant_id` bigint(20) NOT NULL COMMENT '参赛者ID(关联martial_athlete表)',
`organization` varchar(200) DEFAULT NULL COMMENT '单位名称',
`player_name` varchar(100) DEFAULT NULL COMMENT '选手姓名',
`project_name` varchar(100) DEFAULT NULL COMMENT '项目名称',
`category` varchar(50) DEFAULT NULL COMMENT '组别',
`performance_order` int(11) DEFAULT '0' COMMENT '出场顺序',
`create_user` bigint(20) DEFAULT NULL,
`create_dept` bigint(20) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_user` bigint(20) DEFAULT NULL,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`status` int(11) DEFAULT '1' COMMENT '状态(1-待出场,2-已出场)',
`is_deleted` int(11) DEFAULT '0',
`tenant_id` varchar(12) DEFAULT '000000',
PRIMARY KEY (`id`),
KEY `idx_detail` (`schedule_detail_id`),
KEY `idx_group` (`schedule_group_id`),
KEY `idx_participant` (`participant_id`),
KEY `idx_tenant` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='赛程编排参赛者关联表';
-- 4. 赛程编排状态表
CREATE TABLE IF NOT EXISTS `martial_schedule_status` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`competition_id` bigint(20) NOT NULL COMMENT '赛事ID(唯一)',
`schedule_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '编排状态(0=未编排 1=编排中 2=已保存锁定)',
`last_auto_schedule_time` datetime DEFAULT NULL COMMENT '最后自动编排时间',
`locked_time` datetime DEFAULT NULL COMMENT '锁定时间',
`locked_by` varchar(100) DEFAULT NULL COMMENT '锁定人',
`total_groups` int(11) DEFAULT '0' COMMENT '总分组数',
`total_participants` int(11) DEFAULT '0' COMMENT '总参赛人数',
`create_user` bigint(20) DEFAULT NULL,
`create_dept` bigint(20) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_user` bigint(20) DEFAULT NULL,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`status` int(11) DEFAULT '1' COMMENT '状态(1-启用,2-禁用)',
`is_deleted` int(11) DEFAULT '0',
`tenant_id` varchar(12) DEFAULT '000000',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_competition` (`competition_id`),
KEY `idx_tenant` (`tenant_id`),
KEY `idx_schedule_status` (`schedule_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='赛程编排状态表';
-- =============================================
-- 验证结果
-- =============================================
SELECT
'升级完成' AS message,
COUNT(*) AS new_tables_count
FROM information_schema.tables
WHERE table_schema = 'martial_db'
AND table_name IN (
'martial_schedule_group',
'martial_schedule_detail',
'martial_schedule_participant',
'martial_schedule_status'
);
-- 显示所有赛程相关表
SELECT
table_name,
table_comment,
CASE
WHEN table_name IN ('martial_schedule_group', 'martial_schedule_detail',
'martial_schedule_participant', 'martial_schedule_status')
THEN '新系统'
ELSE '旧系统'
END AS system_version
FROM information_schema.tables
WHERE table_schema = 'martial_db'
AND table_name LIKE 'martial_schedule%'
ORDER BY system_version DESC, table_name;
-- =============================================
-- 说明
-- =============================================
--
-- 执行结果说明:
-- 1. 如果 new_tables_count = 4说明4张新表全部创建成功
-- 2. 如果 new_tables_count < 4说明部分表已存在或创建失败
-- 3. 最后一个查询会显示所有赛程相关表及其所属系统版本
--
-- 新旧系统对比:
-- - 旧系统: martial_schedule, martial_schedule_athlete (可能存在)
-- - 新系统: martial_schedule_group, martial_schedule_detail,
-- martial_schedule_participant, martial_schedule_status
--
-- 两个系统可以共存,不会互相影响
-- 新系统由后端Service层代码使用
--
-- =============================================

View File

@@ -1,113 +0,0 @@
-- ================================================================
-- 赛事编排智能化升级 SQL 脚本
-- 用途:支持智能编排算法(场地容纳人数 + 项目时长限制)
-- 日期2025-12-06
-- ================================================================
-- 1. 创建场地信息表(如果不存在)
-- ================================================================
-- 注意:使用 capacity 字段名以匹配现有数据库表结构
CREATE TABLE IF NOT EXISTS `martial_venue` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(12) DEFAULT '000000' COMMENT '租户ID',
`competition_id` bigint(20) NOT NULL COMMENT '赛事ID',
`venue_name` varchar(100) NOT NULL COMMENT '场地名称',
`venue_code` varchar(50) DEFAULT NULL COMMENT '场地编码',
`location` varchar(200) DEFAULT NULL COMMENT '位置/地点',
`capacity` int(11) DEFAULT 100 COMMENT '容纳人数',
`facilities` varchar(500) DEFAULT NULL COMMENT '场地设施',
`status` int(2) DEFAULT 1 COMMENT '状态(0-禁用,1-启用)',
`create_user` bigint(20) DEFAULT NULL COMMENT '创建人',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`is_deleted` int(2) DEFAULT 0 COMMENT '是否已删除',
PRIMARY KEY (`id`),
KEY `idx_competition_id` (`competition_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='场地信息表';
-- 2. 确保 martial_project 表有 estimated_duration 字段
-- ================================================================
-- 检查字段是否存在,不存在则添加
SET @col_exists = 0;
SELECT COUNT(*) INTO @col_exists
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'martial_project'
AND COLUMN_NAME = 'estimated_duration';
SET @sql = IF(@col_exists = 0,
'ALTER TABLE martial_project ADD COLUMN estimated_duration int(11) DEFAULT 5 COMMENT ''预估时长(分钟)'' AFTER max_participants',
'SELECT ''estimated_duration column already exists'' AS info'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 3. 插入测试数据(仅用于开发测试)
-- ================================================================
-- 为赛事 ID=100 插入场地数据
INSERT INTO `martial_venue` (`competition_id`, `venue_name`, `venue_code`, `capacity`, `location`, `facilities`) VALUES
(100, '一号场地', 'VENUE_01', 50, '体育馆一楼东侧', '主会场,配备专业武术地毯,适合集体项目'),
(100, '二号场地', 'VENUE_02', 50, '体育馆一楼西侧', '次会场,配备专业武术地毯,适合集体项目'),
(100, '三号场地', 'VENUE_03', 30, '体育馆二楼东侧', '小型场地,适合个人项目'),
(100, '四号场地', 'VENUE_04', 30, '体育馆二楼西侧', '小型场地,适合个人项目')
ON DUPLICATE KEY UPDATE
venue_name = VALUES(venue_name),
capacity = VALUES(capacity),
location = VALUES(location),
facilities = VALUES(facilities);
-- 4. 更新现有项目的预估时长如果为NULL或0
-- ================================================================
UPDATE martial_project
SET estimated_duration = CASE
WHEN project_name LIKE '%太极%' THEN 5
WHEN project_name LIKE '%长拳%' THEN 5
WHEN project_name LIKE '%剑%' THEN 4
WHEN project_name LIKE '%刀%' THEN 4
WHEN project_name LIKE '%棍%' THEN 6
WHEN project_name LIKE '%枪%' THEN 6
ELSE 5
END
WHERE estimated_duration IS NULL OR estimated_duration = 0;
-- 5. 创建视图:场地使用统计(可选)
-- ================================================================
CREATE OR REPLACE VIEW v_venue_usage_stats AS
SELECT
v.id AS venue_id,
v.competition_id,
v.venue_name,
v.max_capacity,
COUNT(DISTINCT s.group_id) AS assigned_groups,
SUM(s.participant_count) AS total_participants,
SUM(s.estimated_duration) AS total_duration,
v.max_capacity - IFNULL(SUM(s.participant_count), 0) AS remaining_capacity
FROM martial_venue v
LEFT JOIN (
-- 这里假设将来会有 martial_schedule 表来存储编排结果
SELECT
venue_id,
group_id,
COUNT(*) AS participant_count,
SUM(estimated_duration) AS estimated_duration
FROM martial_schedule_detail
WHERE is_deleted = 0
GROUP BY venue_id, group_id
) s ON v.id = s.venue_id
WHERE v.is_deleted = 0
GROUP BY v.id, v.competition_id, v.venue_name, v.max_capacity;
-- ================================================================
-- 脚本执行完成
-- ================================================================
-- 说明:
-- 1. 场地表已创建,支持最大容纳人数配置
-- 2. 项目表 estimated_duration 字段已确保存在
-- 3. 测试数据已插入赛事ID=100
-- 4. 现有项目的预估时长已更新为合理默认值
-- ================================================================

View File

@@ -1,101 +0,0 @@
-- =============================================
-- 验证赛程编排系统表创建情况
-- =============================================
USE martial_db;
-- 1. 检查所有赛程相关表
SELECT
table_name AS '表名',
table_comment AS '说明',
CASE
WHEN table_name IN ('martial_schedule_group', 'martial_schedule_detail',
'martial_schedule_participant', 'martial_schedule_status')
THEN '✓ 新系统'
ELSE '旧系统'
END AS '系统版本',
table_rows AS '记录数'
FROM information_schema.tables
WHERE table_schema = 'martial_db'
AND table_name LIKE 'martial_schedule%'
ORDER BY
CASE
WHEN table_name IN ('martial_schedule_group', 'martial_schedule_detail',
'martial_schedule_participant', 'martial_schedule_status')
THEN 1
ELSE 2
END,
table_name;
-- 2. 验证新系统4张表是否全部创建
SELECT
CASE
WHEN COUNT(*) = 4 THEN '✓ 新系统表创建成功! 共4张表已就绪'
ELSE CONCAT('⚠ 警告: 只创建了 ', COUNT(*), ' 张表,应该是4张')
END AS '创建状态'
FROM information_schema.tables
WHERE table_schema = 'martial_db'
AND table_name IN (
'martial_schedule_group',
'martial_schedule_detail',
'martial_schedule_participant',
'martial_schedule_status'
);
-- 3. 检查各表的字段数量
SELECT
table_name AS '表名',
COUNT(*) AS '字段数'
FROM information_schema.columns
WHERE table_schema = 'martial_db'
AND table_name IN (
'martial_schedule_group',
'martial_schedule_detail',
'martial_schedule_participant',
'martial_schedule_status'
)
GROUP BY table_name
ORDER BY table_name;
-- 4. 检查索引创建情况
SELECT
table_name AS '表名',
COUNT(DISTINCT index_name) AS '索引数量',
GROUP_CONCAT(DISTINCT index_name ORDER BY index_name) AS '索引列表'
FROM information_schema.statistics
WHERE table_schema = 'martial_db'
AND table_name IN (
'martial_schedule_group',
'martial_schedule_detail',
'martial_schedule_participant',
'martial_schedule_status'
)
GROUP BY table_name
ORDER BY table_name;
-- 5. 检查是否有数据(应该为空,因为是新表)
SELECT
'martial_schedule_group' AS '表名',
COUNT(*) AS '记录数'
FROM martial_schedule_group
UNION ALL
SELECT
'martial_schedule_detail',
COUNT(*)
FROM martial_schedule_detail
UNION ALL
SELECT
'martial_schedule_participant',
COUNT(*)
FROM martial_schedule_participant
UNION ALL
SELECT
'martial_schedule_status',
COUNT(*)
FROM martial_schedule_status;
-- 6. 显示最终状态
SELECT
'🎉 数据库升级完成!' AS '状态',
DATABASE() AS '当前数据库',
NOW() AS '验证时间';

8872
database/martial_db.sql Normal file

File diff suppressed because one or more lines are too long

117
docker-compose.yml Normal file
View File

@@ -0,0 +1,117 @@
services:
# MySQL 数据库
mysql:
image: mysql:8.0
container_name: martial-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: martial_db
TZ: Asia/Shanghai
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./database:/docker-entrypoint-initdb.d
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p123456"]
interval: 10s
timeout: 5s
retries: 5
networks:
- martial-network
# Redis 缓存
redis:
image: redis:7-alpine
container_name: martial-redis
restart: always
command: redis-server --requirepass 123456
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "123456", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- martial-network
# MinIO 对象存储
minio:
image: minio/minio:RELEASE.2024-12-18T13-15-44Z
container_name: minio
environment:
MINIO_ROOT_USER: "JohnSion"
MINIO_ROOT_PASSWORD: "v!*BTket4oagDdw"
TZ: "Asia/Shanghai"
command: server /data --console-address ":9001"
volumes:
- ./minio_data:/data
ports:
- "9000:9000"
- "9001:9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:9000/minio/health/live"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- martial-network
# MinIO 初始化 - 创建桶和设置策略
minio-init:
image: minio/mc:latest
depends_on:
minio:
condition: service_healthy
entrypoint: >
sh -c "
mc alias set local http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD} &&
mc mb -p local/assets || true &&
mc anonymous set download local/assets || true
"
environment:
MINIO_ROOT_USER: "JohnSion"
MINIO_ROOT_PASSWORD: "v!*BTket4oagDdw"
restart: "no"
networks:
- martial-network
# 后端应用(完整构建模式)
martial-api:
build:
context: .
dockerfile: Dockerfile.quick
container_name: martial-api
restart: always
environment:
SPRING_PROFILE: dev
JAVA_OPTS: "-Xms512m -Xmx1024m -XX:+UseG1GC"
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/martial_db?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: 123456
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379
SPRING_DATA_REDIS_PASSWORD: 123456
ports:
- "8123:8123"
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- martial-network
networks:
martial-network:
driver: bridge
volumes:
mysql_data:
redis_data:

224
docs/DATABASE_MIGRATION.md Normal file
View File

@@ -0,0 +1,224 @@
# 数据库迁移指南
本项目使用 **Flyway** 进行数据库版本管理和自动迁移。
## 概述
Flyway 是一个数据库迁移工具,它能够:
- 自动追踪数据库版本
- 按顺序执行迁移脚本
- 确保团队成员的数据库结构一致
- 支持回滚和修复
## 工作原理
1. 应用启动时Flyway 自动扫描 `src/main/resources/db/migration` 目录
2. 检查 `flyway_schema_history` 表,确定已执行的版本
3. 按版本号顺序执行未运行的迁移脚本
4. 记录执行结果到历史表
## 迁移脚本命名规范
```
V{版本号}__{描述}.sql
```
### 命名规则
| 规则 | 说明 | 示例 |
|------|------|------|
| 前缀 | 必须以 `V` 开头(大写) | V1, V2, V10 |
| 版本号 | 数字,支持小数点 | 1, 2, 2.1, 10 |
| 分隔符 | **两个下划线** | `__` |
| 描述 | 用下划线连接单词 | add_user_table |
| 后缀 | 必须是 `.sql` | .sql |
### 正确示例
```
V1__baseline.sql # 基线版本
V2__add_project_fields.sql # 添加项目字段
V3__create_order_table.sql # 创建订单表
V4__add_index_to_user.sql # 添加用户索引
V4.1__fix_user_column_type.sql # 修复用户列类型(小版本)
V10__major_refactor.sql # 大版本重构
```
### 错误示例
```
v1__init.sql # 错误v 应该大写
V1_init.sql # 错误:只有一个下划线
V1-init.sql # 错误:使用了连字符
V1__init.SQL # 错误:后缀应该小写
init.sql # 错误:缺少版本前缀
```
## 如何添加新的迁移
### 步骤 1确定版本号
查看当前最新版本:
```bash
ls src/main/resources/db/migration/
```
新版本号 = 最新版本号 + 1
### 步骤 2创建迁移脚本
`src/main/resources/db/migration/` 目录创建新文件:
```sql
-- =====================================================
-- 迁移脚本: [描述]
-- 版本: V{版本号}
-- 描述: [详细说明]
-- 日期: YYYY-MM-DD
-- =====================================================
-- 你的 SQL 语句
ALTER TABLE xxx ADD COLUMN yyy VARCHAR(100);
```
### 步骤 3测试迁移
本地启动应用,观察日志:
```
Flyway Community Edition 9.x.x
Successfully validated 3 migrations
Current version of schema: 2
Migrating schema to version 3 - create_order_table
Successfully applied 1 migration
```
### 步骤 4提交代码
```bash
git add src/main/resources/db/migration/V3__xxx.sql
git commit -m "db: 添加xxx迁移脚本"
git push
```
## 最佳实践
### 1. 幂等性脚本
编写可重复执行的脚本,避免重复执行报错:
```sql
-- 添加列(如果不存在)
SET @exist := (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'your_table'
AND column_name = 'new_column');
SET @sql := IF(@exist = 0,
'ALTER TABLE your_table ADD COLUMN new_column VARCHAR(100)',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
```
### 2. 不要修改已执行的脚本
一旦迁移脚本被执行(已提交到版本库),**永远不要修改它**。
如果需要修复,创建新的迁移脚本:
```
V3__create_table.sql # 已执行,有错误
V4__fix_v3_error.sql # 新建脚本修复错误
```
### 3. 小步迁移
每个迁移脚本只做一件事:
- V2__add_user_email.sql
- V3__add_user_phone.sql
- 不要: V2__add_user_email_and_phone_and_address.sql
### 4. 添加注释
```sql
-- =====================================================
-- 迁移脚本: 添加用户邮箱字段
-- 版本: V5
-- 描述: 为用户表添加邮箱字段,用于接收通知
-- 作者: 张三
-- 日期: 2024-12-29
-- 关联需求: JIRA-123
-- =====================================================
```
### 5. 备份数据
生产环境执行迁移前,务必备份数据库:
```bash
mysqldump -u root -p martial_db > backup_$(date +%Y%m%d).sql
```
## 常见问题
### Q1: 迁移失败怎么办?
1. 查看错误日志,定位问题
2. 修复数据库中的问题(手动)
3. 修复迁移脚本
4. 执行 Flyway repair如需要
### Q2: 如何跳过某个版本?
不建议跳过版本。如果必须跳过,可以创建空脚本:
```sql
-- V3__placeholder.sql
-- 此版本跳过
SELECT 1;
```
### Q3: 多人开发版本冲突怎么办?
使用日期时间作为版本号前缀:
```
V20241229001__add_field.sql
V20241229002__fix_bug.sql
```
### Q4: 如何查看迁移历史?
```sql
SELECT * FROM flyway_schema_history ORDER BY installed_rank;
```
## 目录结构
```
src/main/resources/
└── db/
└── migration/
├── V1__baseline.sql # 基线版本
├── V2__add_project_fields.sql # 添加项目字段
└── V3__xxx.sql # 后续迁移...
```
## 配置说明
application.yml 中的 Flyway 配置:
```yaml
spring:
flyway:
enabled: true # 启用 Flyway
locations: classpath:db/migration # 迁移脚本位置
table: flyway_schema_history # 版本历史表名
baseline-version: 0 # 基线版本号
baseline-on-migrate: true # 自动执行基线
validate-on-migrate: true # 校验迁移脚本
encoding: UTF-8 # 脚本编码
out-of-order: false # 禁止乱序执行
clean-disabled: true # 禁用清理(生产安全)
```
## 参考资料
- [Flyway 官方文档](https://flywaydb.org/documentation/)
- [Spring Boot Flyway 集成](https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.data-initialization.migration-tool.flyway)

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,59 @@
-- =====================================================
-- 武术比赛管理系统 - 补充裁判邀请表字段
-- 添加实体类中存在但数据库表缺失的字段
-- Date: 2025-12-12
-- =====================================================
USE martial_db;
-- =====================================================
-- martial_judge_invite (裁判邀请码表) - 添加缺失字段
-- =====================================================
-- 添加 invite_status 字段
ALTER TABLE martial_judge_invite
ADD COLUMN invite_status int DEFAULT 0 COMMENT '邀请状态(0-待回复,1-已接受,2-已拒绝,3-已取消)' AFTER token_expire_time;
-- 添加 invite_time 字段
ALTER TABLE martial_judge_invite
ADD COLUMN invite_time datetime DEFAULT NULL COMMENT '邀请时间' AFTER invite_status;
-- 添加 reply_time 字段
ALTER TABLE martial_judge_invite
ADD COLUMN reply_time datetime DEFAULT NULL COMMENT '回复时间' AFTER invite_time;
-- 添加 reply_note 字段
ALTER TABLE martial_judge_invite
ADD COLUMN reply_note varchar(500) DEFAULT NULL COMMENT '回复备注' AFTER reply_time;
-- 添加 contact_phone 字段
ALTER TABLE martial_judge_invite
ADD COLUMN contact_phone varchar(20) DEFAULT NULL COMMENT '联系电话' AFTER reply_note;
-- 添加 contact_email 字段
ALTER TABLE martial_judge_invite
ADD COLUMN contact_email varchar(100) DEFAULT NULL COMMENT '联系邮箱' AFTER contact_phone;
-- 添加 invite_message 字段
ALTER TABLE martial_judge_invite
ADD COLUMN invite_message varchar(1000) DEFAULT NULL COMMENT '邀请消息' AFTER contact_email;
-- 添加 cancel_reason 字段
ALTER TABLE martial_judge_invite
ADD COLUMN cancel_reason varchar(500) DEFAULT NULL COMMENT '取消原因' AFTER invite_message;
-- =====================================================
-- 验证修改
-- =====================================================
SELECT '=== 裁判邀请表字段补充完成 ===' AS status;
-- 查看表结构
SHOW COLUMNS FROM martial_judge_invite;
-- 统计字段数量
SELECT
'martial_judge_invite 字段数:' AS info,
COUNT(*) AS count
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='martial_db'
AND TABLE_NAME='martial_judge_invite';

View File

@@ -0,0 +1,147 @@
-- =====================================================
-- 武术比赛管理系统 - 菜单数据
-- 添加武术比赛管理相关菜单
-- Date: 2025-12-12
-- =====================================================
-- 注意请根据实际情况调整菜单ID避免与现有菜单冲突
-- 建议先查询当前最大菜单ID: SELECT MAX(id) FROM blade_menu;
USE bladex;
-- =====================================================
-- 1. 武术比赛管理 - 一级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2000000, 0, 'martial', '武术比赛', 'menu', '/martial', 'iconfont icon-quanxian', 1, 1, 0, 1, '武术比赛管理系统', 0);
-- =====================================================
-- 2. 赛事管理 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2001000, 2000000, 'martial:competition', '赛事管理', 'menu', '/martial/competition/list', 'iconfont icon-rizhi', 1, 1, 0, 1, '赛事信息管理', 0);
-- =====================================================
-- 3. 报名管理 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2002000, 2000000, 'martial:registration', '报名详情', 'menu', '/martial/registration/detail', 'iconfont icon-wenben', 2, 1, 0, 1, '报名信息管理', 0);
-- =====================================================
-- 4. 订单管理 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2003000, 2000000, 'martial:order', '订单管理', 'menu', '/martial/order/list', 'iconfont icon-caidan', 3, 1, 0, 1, '订单信息管理', 0);
-- =====================================================
-- 5. 参赛选手管理 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2004000, 2000000, 'martial:participant', '参赛选手管理', 'menu', '/martial/participant/list', 'iconfont icon-icon-', 4, 1, 0, 1, '参赛选手信息管理', 0);
-- =====================================================
-- 6. 项目管理 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2005000, 2000000, 'martial:project', '项目管理', 'menu', '/martial/project/list', 'iconfont icon-liebiao', 5, 1, 0, 1, '比赛项目管理', 0);
-- =====================================================
-- 7. 评委管理 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2006000, 2000000, 'martial:referee', '评委管理', 'menu', '/martial/referee/list', 'iconfont icon-quanxian', 6, 1, 0, 1, '评委信息管理', 0);
-- =====================================================
-- 8. 裁判邀请 - 二级菜单 ⭐ 重点
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2007000, 2000000, 'martial:judgeInvite', '裁判邀请', 'menu', '/martial/judgeInvite/list', 'iconfont icon-email', 7, 1, 0, 1, '裁判邀请码管理', 0);
-- =====================================================
-- 9. 裁判分配 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2008000, 2000000, 'martial:judgeProject', '裁判分配', 'menu', '/martial/judgeProject/list', 'iconfont icon-quanxian', 8, 1, 0, 1, '裁判项目分配', 0);
-- =====================================================
-- 10. 评分管理 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2009000, 2000000, 'martial:score', '评分管理', 'menu', '/martial/score/index', 'iconfont icon-icon-', 9, 1, 0, 1, '评分记录管理', 0);
-- =====================================================
-- 11. 扣分项管理 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2010000, 2000000, 'martial:deduction', '扣分项管理', 'menu', '/martial/deduction/list', 'iconfont icon-icon-', 10, 1, 0, 1, '扣分项配置管理', 0);
-- =====================================================
-- 12. 成绩管理 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2011000, 2000000, 'martial:result', '成绩管理', 'menu', '/martial/result/list', 'iconfont icon-icon-', 11, 1, 0, 1, '成绩统计管理', 0);
-- =====================================================
-- 13. 赛程计划 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2012000, 2000000, 'martial:schedulePlan', '赛程计划', 'menu', '/martial/schedulePlan/list', 'iconfont icon-riqi', 12, 1, 0, 1, '赛程安排管理', 0);
-- =====================================================
-- 14. 选手关联 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2013000, 2000000, 'martial:scheduleAthlete', '选手关联', 'menu', '/martial/scheduleAthlete/list', 'iconfont icon-icon-', 13, 1, 0, 1, '赛程选手关联', 0);
-- =====================================================
-- 15. 轮播图管理 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2014000, 2000000, 'martial:banner', '轮播图管理', 'menu', '/martial/banner/index', 'iconfont icon-tupian', 14, 1, 0, 1, '轮播图配置', 0);
-- =====================================================
-- 16. 直播管理 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2015000, 2000000, 'martial:live', '直播管理', 'menu', '/martial/live/list', 'iconfont icon-icon-', 15, 1, 0, 1, '直播信息管理', 0);
-- =====================================================
-- 17. 信息发布 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2016000, 2000000, 'martial:info', '信息发布', 'menu', '/martial/info/list', 'iconfont icon-wenben', 16, 1, 0, 1, '信息发布管理', 0);
-- =====================================================
-- 18. 异常事件 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2017000, 2000000, 'martial:exception', '异常事件', 'menu', '/martial/exception/list', 'iconfont icon-icon-', 17, 1, 0, 1, '异常事件管理', 0);
-- =====================================================
-- 19. 活动日程 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2018000, 2000000, 'martial:activity', '活动日程', 'menu', '/martial/activity/list', 'iconfont icon-riqi', 18, 1, 0, 1, '活动日程管理', 0);
-- =====================================================
-- 20. 赛事规程管理 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2019000, 2000000, 'martial:rules', '赛事规程管理', 'menu', '/martial/rules/index', 'iconfont icon-wenben', 19, 1, 0, 1, '赛事规程文件管理', 0);
-- =====================================================
-- 21. 导出中心 - 二级菜单
-- =====================================================
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`)
VALUES (2020000, 2000000, 'martial:export', '导出中心', 'menu', '/martial/export/index', 'iconfont icon-icon-', 20, 1, 0, 1, '数据导出中心', 0);
-- =====================================================
-- 验证插入
-- =====================================================
SELECT '=== 菜单数据插入完成 ===' AS status;
-- 查看插入的菜单
SELECT id, parent_id, name, path, sort
FROM blade_menu
WHERE id >= 2000000 AND id <= 2020000
ORDER BY id;

91
init-judge-project.sql Normal file
View File

@@ -0,0 +1,91 @@
-- ============================================
-- 初始化裁判-项目关联数据
-- 用于解决"您没有权限给该项目打分"的问题
-- ============================================
-- 说明:
-- 1. 这个脚本会为所有裁判分配所有项目的评分权限
-- 2. 如果需要更精细的权限控制,请根据实际情况修改
-- 3. 执行前请确保 martial_judge 和 martial_project 表中已有数据
-- 清空现有的裁判-项目关联(可选)
-- TRUNCATE TABLE martial_judge_project;
-- 方案1为所有裁判分配所有项目适用于测试环境
INSERT INTO martial_judge_project (
competition_id,
judge_id,
project_id,
assign_time,
status,
is_deleted,
create_time,
update_time
)
SELECT
j.competition_id,
j.id AS judge_id,
p.id AS project_id,
NOW() AS assign_time,
1 AS status,
0 AS is_deleted,
NOW() AS create_time,
NOW() AS update_time
FROM martial_judge j
CROSS JOIN martial_project p
WHERE j.is_deleted = 0
AND p.is_deleted = 0
AND NOT EXISTS (
SELECT 1 FROM martial_judge_project jp
WHERE jp.judge_id = j.id
AND jp.project_id = p.id
AND jp.is_deleted = 0
);
-- 方案2为特定裁判分配特定项目适用于生产环境
-- 示例为裁判ID=456分配项目ID=5的权限
/*
INSERT INTO martial_judge_project (
competition_id,
judge_id,
project_id,
assign_time,
status,
is_deleted,
create_time,
update_time
) VALUES (
200, -- 比赛ID
456, -- 裁判ID
5, -- 项目ID
NOW(),
1,
0,
NOW(),
NOW()
);
*/
-- 验证数据
SELECT
jp.id,
j.name AS judge_name,
p.project_name,
jp.status,
jp.assign_time
FROM martial_judge_project jp
LEFT JOIN martial_judge j ON jp.judge_id = j.id
LEFT JOIN martial_project p ON jp.project_id = p.id
WHERE jp.is_deleted = 0
ORDER BY jp.judge_id, jp.project_id;
-- 查看每个裁判分配的项目数量
SELECT
j.id AS judge_id,
j.name AS judge_name,
COUNT(jp.id) AS project_count
FROM martial_judge j
LEFT JOIN martial_judge_project jp ON j.id = jp.judge_id AND jp.is_deleted = 0
WHERE j.is_deleted = 0
GROUP BY j.id, j.name
ORDER BY j.id;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
{"version":"1","format":"xl-single","id":"7aa712c5-97fa-4608-aafd-5e91b82dcaaa","xl":{"version":"3","this":"a0620b80-1f59-4689-8995-4d5bcde4044d","sets":[["a0620b80-1f59-4689-8995-4d5bcde4044d"]],"distributionAlgo":"SIPMOD+PARITY"}}

Binary file not shown.

10
pom.xml
View File

@@ -228,6 +228,16 @@
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- Flyway 数据库迁移 -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -48,7 +48,7 @@ public class BladeConfiguration implements WebMvcConfigurer {
*/ */
@Override @Override
public void addCorsMappings(CorsRegistry registry) { public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/cors/**") registry.addMapping("/**")
.allowedOriginPatterns("*") .allowedOriginPatterns("*")
.allowedHeaders("*") .allowedHeaders("*")
.allowedMethods("*") .allowedMethods("*")

View File

@@ -0,0 +1,167 @@
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;
/**
* 获取图形验证码
*/
@GetMapping("/oauth/captcha")
@ApiOperationSupport(order = 1)
@Operation(summary = "获取图形验证码", description = "返回验证码图片和key")
public R<java.util.Map<String, String>> getCaptcha() {
// 生成唯一key
String key = java.util.UUID.randomUUID().toString().replace("-", "");
// 生成4位随机验证码
String code = generateCode(4);
// 存储验证码到Redis有效期5分钟
String cacheKey = CacheNames.CAPTCHA_KEY + key;
bladeRedis.setEx(cacheKey, code.toLowerCase(), Duration.ofMinutes(5));
// 生成验证码图片简单的base64图片
String image = generateCaptchaImage(code);
// 返回结果
java.util.Map<String, String> result = new java.util.HashMap<>();
result.put("key", key);
result.put("image", image);
return R.data(result);
}
/**
* 发送短信验证码
*/
@PostMapping("/send")
@ApiOperationSupport(order = 2)
@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();
}
/**
* 生成验证码图片Base64格式
* 使用 SVG 格式生成验证码,避免字体依赖问题
*
* @param code 验证码文本
* @return Base64编码的图片
*/
private String generateCaptchaImage(String code) {
Random random = new Random();
StringBuilder svg = new StringBuilder();
svg.append("<svg xmlns='http://www.w3.org/2000/svg' width='120' height='40'>");
// 背景
svg.append("<rect width='120' height='40' fill='#f8f9fa'/>");
// 绘制验证码字符
for (int i = 0; i < code.length(); i++) {
char c = code.charAt(i);
int x = 15 + i * 25;
int y = 25 + random.nextInt(5) - 2;
int rotate = random.nextInt(30) - 15;
// 随机颜色
String color = String.format("#%02x%02x%02x",
random.nextInt(100),
random.nextInt(100),
random.nextInt(100));
svg.append(String.format(
"<text x='%d' y='%d' font-size='28' font-weight='bold' fill='%s' transform='rotate(%d %d %d)'>%c</text>",
x, y, color, rotate, x, y, c
));
}
// 添加干扰线
for (int i = 0; i < 3; i++) {
int x1 = random.nextInt(120);
int y1 = random.nextInt(40);
int x2 = random.nextInt(120);
int y2 = random.nextInt(40);
String color = String.format("#%02x%02x%02x",
random.nextInt(200) + 50,
random.nextInt(200) + 50,
random.nextInt(200) + 50);
svg.append(String.format(
"<line x1='%d' y1='%d' x2='%d' y2='%d' stroke='%s' stroke-width='1'/>",
x1, y1, x2, y2, color
));
}
svg.append("</svg>");
// 转换为 Base64
return "data:image/svg+xml;base64," + java.util.Base64.getEncoder().encodeToString(svg.toString().getBytes());
}
}

View File

@@ -0,0 +1,59 @@
package org.springblade.modules.martial.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 赛程编排配置
*/
@Data
@Component
@ConfigurationProperties(prefix = "martial.schedule")
public class ScheduleConfig {
/**
* 个人项目每组最大人数(超过此数量将自动拆分)
*/
private int maxPeoplePerGroup = 35;
/**
* 拆分后每组目标人数
*/
private int targetPeoplePerGroup = 33;
/**
* 默认每人比赛时长(分钟)
*/
private int defaultDurationPerPerson = 5;
/**
* 容量利用率警告阈值(百分比)
*/
private int capacityWarningThreshold = 90;
/**
* 是否允许容量超载(超载时仍继续编排)
*/
private boolean allowOverload = false;
/**
* 上午时段开始时间
*/
private String morningStartTime = "08:00";
/**
* 上午时段结束时间
*/
private String morningEndTime = "12:00";
/**
* 下午时段开始时间
*/
private String afternoonStartTime = "14:00";
/**
* 下午时段结束时间
*/
private String afternoonEndTime = "18:00";
}

View File

@@ -1,12 +1,15 @@
package org.springblade.modules.martial.controller; package org.springblade.modules.martial.controller;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.qiniu.util.Auth;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.boot.ctrl.BladeController; import org.springblade.core.boot.ctrl.BladeController;
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.core.secure.utils.AuthUtil;
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.entity.MartialAthlete; import org.springblade.modules.martial.pojo.entity.MartialAthlete;
@@ -20,6 +23,7 @@ import org.springframework.web.bind.annotation.*;
* @author BladeX * @author BladeX
*/ */
@RestController @RestController
@Slf4j
@AllArgsConstructor @AllArgsConstructor
@RequestMapping("/martial/athlete") @RequestMapping("/martial/athlete")
@Tag(name = "参赛选手管理", description = "参赛选手接口") @Tag(name = "参赛选手管理", description = "参赛选手接口")
@@ -53,6 +57,13 @@ public class MartialAthleteController extends BladeController {
@PostMapping("/submit") @PostMapping("/submit")
@Operation(summary = "新增或修改", description = "传入实体") @Operation(summary = "新增或修改", description = "传入实体")
public R submit(@RequestBody MartialAthlete athlete) { public R submit(@RequestBody MartialAthlete athlete) {
Long userId = AuthUtil.getUserId();
log.info("=== 提交选手 === userId: {}, playerName: {}", userId, athlete.getPlayerName());
// Only set createUser for new records (when id is null)
if (athlete.getId() == null) {
athlete.setCreateUser(userId);
}
athlete.setUpdateUser(userId);
return R.status(athleteService.saveOrUpdate(athlete)); return R.status(athleteService.saveOrUpdate(athlete));
} }

View File

@@ -0,0 +1,127 @@
package org.springblade.modules.martial.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionAttachment;
import org.springblade.modules.martial.service.IMartialCompetitionAttachmentService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 赛事附件 控制器
*
* @author BladeX
*/
@RestController
@AllArgsConstructor
@RequestMapping("/martial/competition/attachment")
@Tag(name = "赛事附件管理", description = "赛事附件管理接口")
public class MartialCompetitionAttachmentController extends BladeController {
private final IMartialCompetitionAttachmentService attachmentService;
/**
* 详情
*/
@GetMapping("/detail")
@Operation(summary = "详情", description = "传入ID")
public R<MartialCompetitionAttachment> detail(@RequestParam Long id) {
MartialCompetitionAttachment detail = attachmentService.getById(id);
return R.data(detail);
}
/**
* 分页列表
*/
@GetMapping("/list")
@Operation(summary = "分页列表", description = "分页查询")
public R<IPage<MartialCompetitionAttachment>> list(MartialCompetitionAttachment attachment, Query query) {
IPage<MartialCompetitionAttachment> pages = attachmentService.page(Condition.getPage(query), Condition.getQueryWrapper(attachment));
return R.data(pages);
}
/**
* 根据赛事ID和类型获取附件列表
*/
@GetMapping("/getByType")
@Operation(summary = "根据赛事ID和类型获取附件列表", description = "传入赛事ID和附件类型")
public R<List<MartialCompetitionAttachment>> getByType(
@RequestParam Long competitionId,
@RequestParam String attachmentType) {
List<MartialCompetitionAttachment> list = attachmentService.getByCompetitionIdAndType(competitionId, attachmentType);
return R.data(list);
}
/**
* 根据赛事ID获取所有附件
*/
@GetMapping("/getByCompetition")
@Operation(summary = "根据赛事ID获取所有附件", description = "传入赛事ID")
public R<List<MartialCompetitionAttachment>> getByCompetition(@RequestParam Long competitionId) {
List<MartialCompetitionAttachment> list = attachmentService.getByCompetitionId(competitionId);
return R.data(list);
}
/**
* 新增或修改
*/
@PostMapping("/submit")
@Operation(summary = "新增或修改", description = "传入实体")
public R submit(@RequestBody MartialCompetitionAttachment attachment) {
// 设置默认状态为启用
if (attachment.getStatus() == null) {
attachment.setStatus(1);
}
// 设置默认排序
if (attachment.getOrderNum() == null) {
attachment.setOrderNum(0);
}
return R.status(attachmentService.saveOrUpdate(attachment));
}
/**
* 批量保存附件
*/
@PostMapping("/batchSubmit")
@Operation(summary = "批量保存附件", description = "传入附件列表")
public R batchSubmit(@RequestBody List<MartialCompetitionAttachment> attachments) {
for (MartialCompetitionAttachment attachment : attachments) {
if (attachment.getStatus() == null) {
attachment.setStatus(1);
}
if (attachment.getOrderNum() == null) {
attachment.setOrderNum(0);
}
}
return R.status(attachmentService.saveOrUpdateBatch(attachments));
}
/**
* 删除
*/
@PostMapping("/remove")
@Operation(summary = "删除", description = "传入ID")
public R remove(@RequestParam String ids) {
return R.status(attachmentService.removeByIds(Func.toLongList(ids)));
}
/**
* 删除赛事的指定类型附件
*/
@PostMapping("/removeByType")
@Operation(summary = "删除赛事的指定类型附件", description = "传入赛事ID和附件类型")
public R removeByType(
@RequestParam Long competitionId,
@RequestParam String attachmentType) {
return R.status(attachmentService.removeByCompetitionIdAndType(competitionId, attachmentType));
}
}

View File

@@ -1,6 +1,9 @@
package org.springblade.modules.martial.controller; package org.springblade.modules.martial.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@@ -9,10 +12,16 @@ 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.entity.MartialAthlete;
import org.springblade.modules.martial.pojo.entity.MartialCompetition; import org.springblade.modules.martial.pojo.entity.MartialCompetition;
import org.springblade.modules.martial.service.IMartialAthleteService;
import org.springblade.modules.martial.mapper.MartialAthleteMapper;
import org.springblade.modules.martial.service.IMartialCompetitionService; import org.springblade.modules.martial.service.IMartialCompetitionService;
import org.springblade.modules.system.pojo.entity.User;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
/** /**
* 赛事信息 控制器 * 赛事信息 控制器
* *
@@ -25,6 +34,7 @@ import org.springframework.web.bind.annotation.*;
public class MartialCompetitionController extends BladeController { public class MartialCompetitionController extends BladeController {
private final IMartialCompetitionService competitionService; private final IMartialCompetitionService competitionService;
private final IMartialAthleteService martialAthleteService;
/** /**
* 详情 * 详情
@@ -33,6 +43,11 @@ public class MartialCompetitionController extends BladeController {
@Operation(summary = "详情", description = "传入ID") @Operation(summary = "详情", description = "传入ID")
public R<MartialCompetition> detail(@RequestParam Long id) { public R<MartialCompetition> detail(@RequestParam Long id) {
MartialCompetition detail = competitionService.getById(id); MartialCompetition detail = competitionService.getById(id);
if (detail != null) {
// Count distinct participants by id_card
Long cnt = ((MartialAthleteMapper) martialAthleteService.getBaseMapper()).countDistinctParticipants(detail.getId());
detail.setTotalParticipants(cnt != null ? cnt.intValue() : 0);
}
return R.data(detail); return R.data(detail);
} }
@@ -43,6 +58,12 @@ public class MartialCompetitionController extends BladeController {
@Operation(summary = "分页列表", description = "分页查询") @Operation(summary = "分页列表", description = "分页查询")
public R<IPage<MartialCompetition>> list(MartialCompetition competition, Query query) { public R<IPage<MartialCompetition>> list(MartialCompetition competition, Query query) {
IPage<MartialCompetition> pages = competitionService.page(Condition.getPage(query), Condition.getQueryWrapper(competition)); IPage<MartialCompetition> pages = competitionService.page(Condition.getPage(query), Condition.getQueryWrapper(competition));
List<MartialCompetition> pagelist = pages.getRecords();
for (MartialCompetition martialCompetition : pagelist) {
// Count distinct participants by id_card
Long cnt = ((MartialAthleteMapper) martialAthleteService.getBaseMapper()).countDistinctParticipants(martialCompetition.getId());
martialCompetition.setTotalParticipants(cnt != null ? cnt.intValue() : 0);
}
return R.data(pages); return R.data(pages);
} }
@@ -51,8 +72,9 @@ public class MartialCompetitionController extends BladeController {
*/ */
@PostMapping("/submit") @PostMapping("/submit")
@Operation(summary = "新增或修改", description = "传入实体") @Operation(summary = "新增或修改", description = "传入实体")
public R submit(@RequestBody MartialCompetition competition) { public R<MartialCompetition> submit(@RequestBody MartialCompetition competition) {
return R.status(competitionService.saveOrUpdate(competition)); competitionService.saveOrUpdate(competition);
return R.data(competition);
} }
/** /**

View File

@@ -5,7 +5,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController; import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.tool.api.R; import org.springblade.core.tool.api.R;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionRulesAttachment; import org.springblade.modules.martial.pojo.entity.MartialCompetitionAttachment;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionRulesChapter; import org.springblade.modules.martial.pojo.entity.MartialCompetitionRulesChapter;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionRulesContent; import org.springblade.modules.martial.pojo.entity.MartialCompetitionRulesContent;
import org.springblade.modules.martial.pojo.vo.MartialCompetitionRulesVO; import org.springblade.modules.martial.pojo.vo.MartialCompetitionRulesVO;
@@ -45,8 +45,8 @@ public class MartialCompetitionRulesController extends BladeController {
*/ */
@GetMapping("/attachment/list") @GetMapping("/attachment/list")
@Operation(summary = "获取附件列表", description = "管理端获取附件列表") @Operation(summary = "获取附件列表", description = "管理端获取附件列表")
public R<List<MartialCompetitionRulesAttachment>> getAttachmentList(@RequestParam Long competitionId) { public R<List<MartialCompetitionAttachment>> getAttachmentList(@RequestParam Long competitionId) {
List<MartialCompetitionRulesAttachment> list = rulesService.getAttachmentList(competitionId); List<MartialCompetitionAttachment> list = rulesService.getAttachmentList(competitionId);
return R.data(list); return R.data(list);
} }
@@ -55,7 +55,7 @@ public class MartialCompetitionRulesController extends BladeController {
*/ */
@PostMapping("/attachment/save") @PostMapping("/attachment/save")
@Operation(summary = "保存附件", description = "新增或修改附件") @Operation(summary = "保存附件", description = "新增或修改附件")
public R saveAttachment(@RequestBody MartialCompetitionRulesAttachment attachment) { public R saveAttachment(@RequestBody MartialCompetitionAttachment attachment) {
return R.status(rulesService.saveAttachment(attachment)); return R.status(rulesService.saveAttachment(attachment));
} }

View File

@@ -0,0 +1,56 @@
package org.springblade.modules.martial.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.modules.martial.pojo.entity.MartialContact;
import org.springblade.modules.martial.service.IMartialContactService;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/martial/contact")
@Tag(name = "联系人管理", description = "联系人接口")
public class MartialContactController extends BladeController {
private final IMartialContactService contactService;
@GetMapping("/list")
@Operation(summary = "分页列表", description = "获取当前用户的联系人列表")
public R<IPage<MartialContact>> list(Query query) {
Long userId = AuthUtil.getUserId();
IPage<MartialContact> pages = contactService.getContactList(userId, query.getCurrent(), query.getSize());
return R.data(pages);
}
@GetMapping("/detail")
@Operation(summary = "详情", description = "获取联系人详情")
public R<MartialContact> detail(@RequestParam Long id) {
return R.data(contactService.getContactDetail(id));
}
@PostMapping("/submit")
@Operation(summary = "保存", description = "新增或修改联系人")
public R<Boolean> submit(@RequestBody MartialContact contact) {
Long userId = AuthUtil.getUserId();
log.info("Contact submit - id: {}, name: {}, userId: {}, isDefault: {}",
contact.getId(), contact.getName(), userId, contact.getIsDefault());
return R.data(contactService.saveContact(contact, userId));
}
@PostMapping("/remove")
@Operation(summary = "删除", description = "删除联系人")
public R<Boolean> remove(@RequestParam String ids) {
return R.data(contactService.removeByIds(Func.toLongList(ids)));
}
}

View File

@@ -10,9 +10,13 @@ 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.entity.MartialDeductionItem; import org.springblade.modules.martial.pojo.entity.MartialDeductionItem;
import org.springblade.modules.martial.pojo.entity.MartialProject;
import org.springblade.modules.martial.service.IMartialDeductionItemService; import org.springblade.modules.martial.service.IMartialDeductionItemService;
import org.springblade.modules.martial.service.IMartialProjectService;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
/** /**
* 扣分项配置 控制器 * 扣分项配置 控制器
* *
@@ -20,12 +24,14 @@ import org.springframework.web.bind.annotation.*;
*/ */
@RestController @RestController
@AllArgsConstructor @AllArgsConstructor
@RequestMapping("/martial/deductionItem") @RequestMapping("/blade-martial/deductionItem")
@Tag(name = "扣分项配置管理", description = "扣分项配置接口") @Tag(name = "扣分项配置管理", description = "扣分项配置接口")
public class MartialDeductionItemController extends BladeController { public class MartialDeductionItemController extends BladeController {
private final IMartialDeductionItemService deductionItemService; private final IMartialDeductionItemService deductionItemService;
private final IMartialProjectService martialProjectService;
/** /**
* 详情 * 详情
*/ */
@@ -43,6 +49,14 @@ public class MartialDeductionItemController extends BladeController {
@Operation(summary = "分页列表", description = "分页查询") @Operation(summary = "分页列表", description = "分页查询")
public R<IPage<MartialDeductionItem>> list(MartialDeductionItem deductionItem, Query query) { public R<IPage<MartialDeductionItem>> list(MartialDeductionItem deductionItem, Query query) {
IPage<MartialDeductionItem> pages = deductionItemService.page(Condition.getPage(query), Condition.getQueryWrapper(deductionItem)); IPage<MartialDeductionItem> pages = deductionItemService.page(Condition.getPage(query), Condition.getQueryWrapper(deductionItem));
List<MartialDeductionItem> deductionItems = pages.getRecords();
for (MartialDeductionItem item : deductionItems) {
MartialProject project = martialProjectService.getById(item.getProjectId());
if (project != null) {
item.setProjectName(project.getProjectName());
item.setCompetitionId(project.getCompetitionId());
}
}
return R.data(pages); return R.data(pages);
} }
@@ -64,4 +78,13 @@ public class MartialDeductionItemController extends BladeController {
return R.status(deductionItemService.removeByIds(Func.toLongList(ids))); return R.status(deductionItemService.removeByIds(Func.toLongList(ids)));
} }
/**
* 更新排序
*/
@PostMapping("/update-order")
@Operation(summary = "更新排序", description = "传入排序数据")
public R updateOrder(@RequestBody List<MartialDeductionItem> sortData) {
return R.status(deductionItemService.updateOrder(sortData));
}
} }

View File

@@ -10,26 +10,19 @@ import org.springblade.core.tool.utils.DateUtil;
import org.springblade.modules.martial.excel.AthleteExportExcel; import org.springblade.modules.martial.excel.AthleteExportExcel;
import org.springblade.modules.martial.excel.ResultExportExcel; import org.springblade.modules.martial.excel.ResultExportExcel;
import org.springblade.modules.martial.excel.ScheduleExportExcel; import org.springblade.modules.martial.excel.ScheduleExportExcel;
import org.springblade.modules.martial.excel.ScheduleExportExcel2;
import org.springblade.modules.martial.pojo.vo.CertificateVO; import org.springblade.modules.martial.pojo.vo.CertificateVO;
import org.springblade.modules.martial.service.IMartialAthleteService; import org.springblade.modules.martial.service.IMartialAthleteService;
import org.springblade.modules.martial.service.IMartialResultService; import org.springblade.modules.martial.service.IMartialResultService;
import org.springblade.modules.martial.service.IMartialScheduleService; import org.springblade.modules.martial.service.IMartialScheduleService;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/**
* 导出打印 控制器
*
* @author BladeX
*/
@RestController @RestController
@AllArgsConstructor @AllArgsConstructor
@RequestMapping("/martial/export") @RequestMapping("/martial/export")
@@ -40,67 +33,47 @@ public class MartialExportController {
private final IMartialAthleteService athleteService; private final IMartialAthleteService athleteService;
private final IMartialScheduleService scheduleService; private final IMartialScheduleService scheduleService;
/**
* Task 3.1: 导出成绩单
*/
@GetMapping("/results") @GetMapping("/results")
@Operation(summary = "导出成绩单", description = "导出指定赛事或项目的成绩单Excel") @Operation(summary = "导出成绩单", description = "导出指定赛事或项目的成绩单Excel")
public void exportResults( public void exportResults(@RequestParam Long competitionId, @RequestParam(required = false) Long projectId, HttpServletResponse response) {
@RequestParam Long competitionId,
@RequestParam(required = false) Long projectId,
HttpServletResponse response
) {
List<ResultExportExcel> list = resultService.exportResults(competitionId, projectId); List<ResultExportExcel> list = resultService.exportResults(competitionId, projectId);
String fileName = "成绩单_" + DateUtil.today(); String fileName = "成绩单_" + DateUtil.today();
String sheetName = projectId != null ? "项目成绩单" : "全部成绩"; String sheetName = projectId != null ? "项目成绩单" : "全部成绩";
ExcelUtil.export(response, fileName, sheetName, list, ResultExportExcel.class); ExcelUtil.export(response, fileName, sheetName, list, ResultExportExcel.class);
} }
/**
* Task 3.2: 导出运动员名单
*/
@GetMapping("/athletes") @GetMapping("/athletes")
@Operation(summary = "导出运动员名单", description = "导出指定赛事的运动员名单Excel") @Operation(summary = "导出运动员名单", description = "导出指定赛事的运动员名单Excel")
public void exportAthletes( public void exportAthletes(@RequestParam Long competitionId, HttpServletResponse response) {
@RequestParam Long competitionId,
HttpServletResponse response
) {
List<AthleteExportExcel> list = athleteService.exportAthletes(competitionId); List<AthleteExportExcel> list = athleteService.exportAthletes(competitionId);
String fileName = "运动员名单_" + DateUtil.today(); String fileName = "运动员名单_" + DateUtil.today();
ExcelUtil.export(response, fileName, "运动员名单", list, AthleteExportExcel.class); ExcelUtil.export(response, fileName, "运动员名单", list, AthleteExportExcel.class);
} }
/**
* Task 3.3: 导出赛程表
*/
@GetMapping("/schedule") @GetMapping("/schedule")
@Operation(summary = "导出赛程表", description = "导出指定赛事的赛程安排Excel") @Operation(summary = "导出赛程表", description = "导出指定赛事的赛程安排Excel")
public void exportSchedule( public void exportSchedule(@RequestParam Long competitionId, HttpServletResponse response) {
@RequestParam Long competitionId,
HttpServletResponse response
) {
List<ScheduleExportExcel> list = scheduleService.exportSchedule(competitionId); List<ScheduleExportExcel> list = scheduleService.exportSchedule(competitionId);
String fileName = "赛程表_" + DateUtil.today(); String fileName = "赛程表_" + DateUtil.today();
ExcelUtil.export(response, fileName, "赛程安排", list, ScheduleExportExcel.class); ExcelUtil.export(response, fileName, "赛程安排", list, ScheduleExportExcel.class);
} }
/** @GetMapping("/schedule2")
* Task 3.4: 生成单个证书HTML格式 @Operation(summary = "导出赛程表-模板2", description = "按场地导出比赛时间表格式的赛程安排")
*/ public void exportScheduleTemplate2(@RequestParam Long competitionId, @RequestParam(required = false) Long venueId,
@GetMapping("/certificate/{resultId}") @RequestParam(required = false) String venueName, @RequestParam(required = false) String timeSlot, HttpServletResponse response) {
@Operation(summary = "生成证书", description = "生成获奖证书HTML页面可打印为PDF") List<ScheduleExportExcel2> list = scheduleService.exportScheduleTemplate2(competitionId, venueId);
public void generateCertificate( String fileName = "比赛时间_" + (venueName != null ? venueName : "全部场地") + "_" + DateUtil.today();
@PathVariable Long resultId, String sheetName = (venueName != null ? venueName : "全部场地") + (timeSlot != null ? "_" + timeSlot : "");
HttpServletResponse response ExcelUtil.export(response, fileName, sheetName, list, ScheduleExportExcel2.class);
) throws IOException { }
// 1. 获取证书数据
CertificateVO certificate = resultService.generateCertificateData(resultId);
// 2. 读取HTML模板 @GetMapping("/certificate/{resultId}")
@Operation(summary = "生成证书", description = "生成获奖证书HTML页面")
public void generateCertificate(@PathVariable Long resultId, HttpServletResponse response) throws IOException {
CertificateVO certificate = resultService.generateCertificateData(resultId);
Path templatePath = Path.of("src/main/resources/templates/certificate/certificate.html"); Path templatePath = Path.of("src/main/resources/templates/certificate/certificate.html");
String template = Files.readString(templatePath, StandardCharsets.UTF_8); String template = Files.readString(templatePath, StandardCharsets.UTF_8);
// 3. 替换模板变量
String html = template String html = template
.replace("${playerName}", certificate.getPlayerName()) .replace("${playerName}", certificate.getPlayerName())
.replace("${competitionName}", certificate.getCompetitionName()) .replace("${competitionName}", certificate.getCompetitionName())
@@ -109,15 +82,10 @@ public class MartialExportController {
.replace("${medalClass}", certificate.getMedalClass()) .replace("${medalClass}", certificate.getMedalClass())
.replace("${organization}", certificate.getOrganization()) .replace("${organization}", certificate.getOrganization())
.replace("${issueDate}", certificate.getIssueDate()); .replace("${issueDate}", certificate.getIssueDate());
// 4. 返回HTML
response.setContentType("text/html;charset=UTF-8"); response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(html); response.getWriter().write(html);
} }
/**
* Task 3.4: 批量生成证书数据
*/
@GetMapping("/certificates/batch") @GetMapping("/certificates/batch")
@Operation(summary = "批量生成证书数据", description = "批量获取项目获奖选手的证书数据") @Operation(summary = "批量生成证书数据", description = "批量获取项目获奖选手的证书数据")
public R<List<CertificateVO>> batchGenerateCertificates(@RequestParam Long projectId) { public R<List<CertificateVO>> batchGenerateCertificates(@RequestParam Long projectId) {
@@ -125,14 +93,10 @@ public class MartialExportController {
return R.data(certificates); return R.data(certificates);
} }
/**
* Task 3.4: 获取单个证书数据JSON格式
*/
@GetMapping("/certificate/data/{resultId}") @GetMapping("/certificate/data/{resultId}")
@Operation(summary = "获取证书数据", description = "获取证书数据JSON格式,供前端渲染") @Operation(summary = "获取证书数据", description = "获取证书数据JSON格式")
public R<CertificateVO> getCertificateData(@PathVariable Long resultId) { public R<CertificateVO> getCertificateData(@PathVariable Long resultId) {
CertificateVO certificate = resultService.generateCertificateData(resultId); CertificateVO certificate = resultService.generateCertificateData(resultId);
return R.data(certificate); return R.data(certificate);
} }
} }

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,168 @@ 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);
}
/**
* 发送邀请
*/
@PostMapping("/send")
@Operation(summary = "发送邀请", description = "向评委发送邀请")
public R sendInvite(@RequestBody MartialJudgeInvite judgeInvite) {
// TODO: 实现邮件/短信发送逻辑
judgeInvite.setInviteStatus(0); // 待回复
judgeInvite.setInviteTime(java.time.LocalDateTime.now());
return R.status(judgeInviteService.saveOrUpdate(judgeInvite));
}
/**
* 重发邀请
*/
@PostMapping("/resend/{inviteId}")
@Operation(summary = "重发邀请", description = "重新发送邀请")
public R resendInvite(@PathVariable Long inviteId) {
MartialJudgeInvite invite = judgeInviteService.getById(inviteId);
if (invite == null) {
return R.fail("邀请记录不存在");
}
// TODO: 实现邮件/短信重发逻辑
invite.setInviteTime(java.time.LocalDateTime.now());
return R.status(judgeInviteService.updateById(invite));
}
/**
* 取消邀请
*/
@PostMapping("/cancel/{inviteId}")
@Operation(summary = "取消邀请", description = "取消邀请")
public R cancelInvite(@PathVariable Long inviteId, @RequestParam(required = false) String reason) {
MartialJudgeInvite invite = judgeInviteService.getById(inviteId);
if (invite == null) {
return R.fail("邀请记录不存在");
}
invite.setInviteStatus(3); // 已取消
invite.setCancelReason(reason);
return R.status(judgeInviteService.updateById(invite));
}
/**
* 确认邀请
*/
@PostMapping("/confirm/{inviteId}")
@Operation(summary = "确认邀请", description = "确认接受邀请")
public R confirmInvite(@PathVariable Long inviteId) {
MartialJudgeInvite invite = judgeInviteService.getById(inviteId);
if (invite == null) {
return R.fail("邀请记录不存在");
}
if (invite.getInviteStatus() != 1) {
return R.fail("只能确认已接受的邀请");
}
// TODO: 实现确认逻辑(如分配场地、项目等)
return R.success("确认成功");
}
/**
* 发送提醒
*/
@PostMapping("/reminder/{inviteId}")
@Operation(summary = "发送提醒", description = "提醒评委回复邀请")
public R sendReminder(@PathVariable Long inviteId, @RequestParam(required = false) String message) {
MartialJudgeInvite invite = judgeInviteService.getById(inviteId);
if (invite == null) {
return R.fail("邀请记录不存在");
}
// TODO: 实现提醒发送逻辑(邮件/短信)
return R.success("提醒发送成功");
}
/**
* 从评委库导入
*/
@PostMapping("/import/pool")
@Operation(summary = "从评委库导入", description = "从评委库批量导入评委")
public R importFromPool(@RequestParam Long competitionId, @RequestParam String judgeIds) {
// TODO: 实现从评委库导入逻辑
List<Long> ids = Func.toLongList(judgeIds);
// 为每个评委生成邀请码
return R.success("导入成功");
}
/**
* 导出邀请数据
*/
@GetMapping("/export")
@Operation(summary = "导出邀请数据", description = "导出邀请数据为Excel")
public void exportInvites(MartialJudgeInvite judgeInvite) {
// TODO: 实现Excel导出逻辑
// 使用EasyExcel或POI导出
}
/**
* 更新邀请的项目分配
*/
@PutMapping("/updateProjects")
@Operation(summary = "更新项目分配", description = "更新裁判邀请的项目分配")
public R updateProjects(@RequestBody java.util.Map<String, Object> params) {
Long inviteId = Long.valueOf(params.get("inviteId").toString());
String projects = params.get("projects").toString();
MartialJudgeInvite invite = judgeInviteService.getById(inviteId);
if (invite == null) {
return R.fail("邀请记录不存在");
}
invite.setProjects(projects);
boolean success = judgeInviteService.updateById(invite);
return success ? R.success("更新成功") : R.fail("更新失败");
}
} }

View File

@@ -1,6 +1,8 @@
package org.springblade.modules.martial.controller; package org.springblade.modules.martial.controller;
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.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -17,13 +19,30 @@ import org.springblade.modules.martial.pojo.vo.MiniAthleteAdminVO;
import org.springblade.modules.martial.pojo.vo.MiniAthleteScoreVO; import org.springblade.modules.martial.pojo.vo.MiniAthleteScoreVO;
import org.springblade.modules.martial.pojo.vo.MiniLoginVO; import org.springblade.modules.martial.pojo.vo.MiniLoginVO;
import org.springblade.modules.martial.pojo.vo.MiniScoreDetailVO; import org.springblade.modules.martial.pojo.vo.MiniScoreDetailVO;
import org.springblade.modules.martial.pojo.vo.LineupGroupVO;
import org.springblade.modules.martial.pojo.vo.LineupParticipantVO;
import org.springblade.modules.martial.pojo.vo.ScheduleGroupDetailVO;
import com.alibaba.fastjson.JSON;
import org.springblade.modules.martial.service.*; import org.springblade.modules.martial.service.*;
import org.springblade.modules.martial.pojo.dto.ChiefJudgeConfirmDTO;
import org.springblade.modules.martial.pojo.dto.GeneralJudgeConfirmDTO;
import org.springblade.modules.martial.pojo.entity.MtVenue;
import org.springblade.modules.martial.pojo.entity.MartialVenue;
import org.springblade.modules.martial.pojo.entity.MartialResult;
import org.springblade.core.redis.cache.BladeRedis;
import org.springblade.modules.martial.mapper.MartialScheduleStatusMapper;
import org.springblade.modules.martial.mapper.MartialScheduleGroupMapper;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -34,7 +53,7 @@ import java.util.stream.Collectors;
*/ */
@RestController @RestController
@AllArgsConstructor @AllArgsConstructor
@RequestMapping("/api/mini") @RequestMapping("/mini")
@Tag(name = "小程序接口", description = "小程序评分系统专用接口") @Tag(name = "小程序接口", description = "小程序评分系统专用接口")
public class MartialMiniController extends BladeController { public class MartialMiniController extends BladeController {
@@ -42,20 +61,26 @@ public class MartialMiniController extends BladeController {
private final IMartialJudgeService judgeService; private final IMartialJudgeService judgeService;
private final IMartialCompetitionService competitionService; private final IMartialCompetitionService competitionService;
private final IMartialVenueService venueService; private final IMartialVenueService venueService;
private final IMtVenueService mtVenueService;
private final IMartialProjectService projectService; private final IMartialProjectService projectService;
private final IMartialAthleteService athleteService; private final IMartialAthleteService athleteService;
private final IMartialScoreService scoreService; private final IMartialScoreService scoreService;
private final BladeRedis bladeRedis;
private final IMartialResultService resultService;
private final MartialScheduleStatusMapper scheduleStatusMapper;
private final MartialScheduleGroupMapper scheduleGroupMapper;
// Redis缓存key前缀
private static final String MINI_LOGIN_CACHE_PREFIX = "mini:login:";
// 登录缓存过期时间7天
private static final Duration LOGIN_CACHE_EXPIRE = Duration.ofDays(7);
/** /**
* 登录验证 * 登录验证
*
* @param dto 登录信息(比赛编码+邀请码)
* @return 登录结果token、用户信息、分配的场地和项目
*/ */
@PostMapping("/login") @PostMapping("/login")
@Operation(summary = "登录验证", description = "使用比赛编码和邀请码登录") @Operation(summary = "登录验证", description = "使用比赛编码和邀请码登录")
public R<MiniLoginVO> login(@RequestBody MiniLoginDTO dto) { public R<MiniLoginVO> login(@RequestBody MiniLoginDTO dto) {
// 1. 根据邀请码查询邀请信息
LambdaQueryWrapper<MartialJudgeInvite> inviteQuery = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialJudgeInvite> inviteQuery = new LambdaQueryWrapper<>();
inviteQuery.eq(MartialJudgeInvite::getInviteCode, dto.getInviteCode()); inviteQuery.eq(MartialJudgeInvite::getInviteCode, dto.getInviteCode());
inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0); inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
@@ -65,29 +90,24 @@ public class MartialMiniController extends BladeController {
return R.fail("邀请码不存在"); return R.fail("邀请码不存在");
} }
// 2. 验证邀请码是否过期
if (invite.getExpireTime() != null && invite.getExpireTime().isBefore(LocalDateTime.now())) { if (invite.getExpireTime() != null && invite.getExpireTime().isBefore(LocalDateTime.now())) {
return R.fail("邀请码已过期"); return R.fail("邀请码已过期");
} }
// 3. 查询比赛信息
MartialCompetition competition = competitionService.getById(invite.getCompetitionId()); MartialCompetition competition = competitionService.getById(invite.getCompetitionId());
if (competition == null) { if (competition == null) {
return R.fail("比赛不存在"); return R.fail("比赛不存在");
} }
// 4. 验证比赛编码
if (!competition.getCompetitionCode().equals(dto.getMatchCode())) { if (!competition.getCompetitionCode().equals(dto.getMatchCode())) {
return R.fail("比赛编码不匹配"); return R.fail("比赛编码不匹配");
} }
// 5. 查询评委信息
MartialJudge judge = judgeService.getById(invite.getJudgeId()); MartialJudge judge = judgeService.getById(invite.getJudgeId());
if (judge == null) { if (judge == null) {
return R.fail("评委信息不存在"); return R.fail("评委信息不存在");
} }
// 6. 生成访问令牌
String token = UUID.randomUUID().toString().replace("-", ""); String token = UUID.randomUUID().toString().replace("-", "");
invite.setAccessToken(token); invite.setAccessToken(token);
invite.setTokenExpireTime(LocalDateTime.now().plusDays(7)); invite.setTokenExpireTime(LocalDateTime.now().plusDays(7));
@@ -97,77 +117,354 @@ public class MartialMiniController extends BladeController {
invite.setDeviceInfo(dto.getDeviceInfo()); invite.setDeviceInfo(dto.getDeviceInfo());
judgeInviteService.updateById(invite); judgeInviteService.updateById(invite);
// 7. 查询场地信息(裁判长没有固定场地) // 从 martial_venue 表获取场地信息
MartialVenue venue = null; MartialVenue martialVenue = null;
if (invite.getVenueId() != null) { if (invite.getVenueId() != null) {
venue = venueService.getById(invite.getVenueId()); martialVenue = venueService.getById(invite.getVenueId());
} }
// 8. 解析分配的项目 // 获取项目列表:总裁判看所有项目,其他裁判根据场地获取项目
List<MiniLoginVO.ProjectInfo> projects = parseProjects(invite.getProjects()); List<MiniLoginVO.ProjectInfo> projects = new ArrayList<>();
Integer refereeTypeVal = invite.getRefereeType();
String roleVal = invite.getRole();
boolean isGeneralJudge = (refereeTypeVal != null && refereeTypeVal == 3)
|| "general_judge".equals(roleVal) || "general".equals(roleVal);
if (isGeneralJudge) {
// 总裁判看所有项目
projects = getAllProjectsByCompetition(competition.getId());
} else if (Func.isNotEmpty(invite.getProjects())) {
projects = parseProjects(invite.getProjects());
} else if (invite.getVenueId() != null) {
// 未指定项目,根据场地获取项目;如果场地没有项目则返回空列表
projects = getProjectsByVenue(invite.getVenueId());
}
// 如果没有场地projects保持为空列表
// 9. 构造返回结果
MiniLoginVO vo = new MiniLoginVO(); MiniLoginVO vo = new MiniLoginVO();
vo.setToken(token); vo.setToken(token);
vo.setUserRole("chief_judge".equals(invite.getRole()) ? "admin" : "pub"); String role = invite.getRole();
Integer refereeType = invite.getRefereeType();
if ("general_judge".equals(role) || "general".equals(role) || (refereeType != null && refereeType == 3)) {
vo.setUserRole("general");
} else if ("chief_judge".equals(role) || (refereeType != null && refereeType == 1)) {
vo.setUserRole("admin");
} else {
vo.setUserRole("pub");
}
vo.setMatchId(competition.getId()); vo.setMatchId(competition.getId());
vo.setMatchName(competition.getCompetitionName()); vo.setMatchName(competition.getCompetitionName());
vo.setMatchTime(competition.getCompetitionStartTime() != null ? vo.setMatchTime(competition.getCompetitionStartTime() != null ?
competition.getCompetitionStartTime().toString() : ""); competition.getCompetitionStartTime().toString() : "");
vo.setJudgeId(judge.getId()); vo.setJudgeId(judge.getId());
vo.setJudgeName(judge.getName()); vo.setJudgeName(judge.getName());
vo.setVenueId(venue != null ? venue.getId() : null); vo.setVenueId(martialVenue != null ? martialVenue.getId() : null);
vo.setVenueName(venue != null ? venue.getVenueName() : null); vo.setVenueName(martialVenue != null ? martialVenue.getVenueName() : null);
vo.setProjects(projects); vo.setProjects(projects);
// 将登录信息缓存到Redis服务重启后仍然有效
String cacheKey = MINI_LOGIN_CACHE_PREFIX + token;
bladeRedis.setEx(cacheKey, vo, LOGIN_CACHE_EXPIRE);
return R.data(vo); return R.data(vo);
} }
/** /**
* 获取我的选手列表(普通评委) * 提交评分(评委)
* * 注意ID字段使用String类型接收避免JavaScript大数精度丢失问题
* @param judgeId 评委ID
* @param venueId 场地ID
* @param projectId 项目ID
* @return 选手列表(含评分状态)
*/ */
@GetMapping("/athletes") @PostMapping("/score/submit")
@Operation(summary = "选手列表(普通评委)", description = "获取分配的选手列表") @Operation(summary = "提交评分", description = "评委提交对选手的评分")
public R<List<MiniAthleteScoreVO>> getMyAthletes( public R submitScore(@RequestBody org.springblade.modules.martial.pojo.dto.MiniScoreSubmitDTO dto) {
@RequestParam Long judgeId, MartialScore score = new MartialScore();
@RequestParam Long venueId,
@RequestParam Long projectId // 将String类型的ID转换为Long避免JavaScript大数精度丢失
) { score.setAthleteId(parseLong(dto.getAthleteId()));
List<MiniAthleteScoreVO> result = athleteService.getAthletesWithMyScore( score.setJudgeId(parseLong(dto.getJudgeId()));
judgeId, venueId, projectId); score.setScore(dto.getScore());
return R.data(result); score.setProjectId(parseLong(dto.getProjectId()));
score.setCompetitionId(parseLong(dto.getCompetitionId()));
score.setVenueId(parseLong(dto.getVenueId()));
score.setScheduleId(parseLong(dto.getScheduleId()));
score.setNote(dto.getNote());
score.setScoreTime(LocalDateTime.now());
if (dto.getDeductions() != null && !dto.getDeductions().isEmpty()) {
// 将String类型的扣分项ID转换为Long
List<Long> deductionIds = dto.getDeductions().stream()
.map(this::parseLong)
.filter(id -> id != null)
.collect(Collectors.toList());
score.setDeductionItems(com.alibaba.fastjson.JSON.toJSONString(deductionIds));
}
Long judgeId = parseLong(dto.getJudgeId());
if (judgeId != null) {
var judge = judgeService.getById(judgeId);
if (judge != null) {
score.setJudgeName(judge.getName());
}
}
boolean success = scoreService.save(score);
// 评分保存成功后,计算并更新选手总分
if (success) {
Long athleteId = parseLong(dto.getAthleteId());
Long projectId = parseLong(dto.getProjectId());
Long venueId = parseLong(dto.getVenueId());
if (athleteId != null && projectId != null) {
updateAthleteTotalScore(athleteId, projectId, venueId);
}
}
return success ? R.success("评分提交成功") : R.fail("评分提交失败");
} }
/** /**
* 获取选手列表(裁判长) * 计算并更新选手总分
* * 总分算法:去掉一个最高分和一个最低分,取剩余分数的平均值
* @param competitionId 比赛ID * 特殊情况:裁判数量<3时直接取平均分
* @param venueId 场地ID * 只有所有裁判都评分完成后才更新总分
* @param projectId 项目ID
* @return 选手列表(含评分统计)
*/ */
@GetMapping("/athletes/admin") private void updateAthleteTotalScore(Long athleteId, Long projectId, Long venueId) {
@Operation(summary = "选手列表(裁判长)", description = "裁判长查看所有选手") try {
public R<List<MiniAthleteAdminVO>> getAthletesForAdmin( // 1. 查询该场地的裁判员数量
@RequestParam Long competitionId, int requiredJudgeCount = getRequiredJudgeCount(venueId);
@RequestParam Long venueId,
@RequestParam Long projectId // 2. 获取主裁判ID列表
List<Long> chiefJudgeIds = getChiefJudgeIds(venueId);
// 3. 查询该选手在该项目的所有评分(排除主裁判的评分)
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getAthleteId, athleteId);
scoreQuery.eq(MartialScore::getProjectId, projectId);
scoreQuery.eq(MartialScore::getIsDeleted, 0);
// 排除主裁判的所有评分(包括普通评分和修改记录)
if (!chiefJudgeIds.isEmpty()) {
scoreQuery.notIn(MartialScore::getJudgeId, chiefJudgeIds);
}
List<MartialScore> scores = scoreService.list(scoreQuery);
// 4. 判断是否所有裁判都已评分
if (scores == null || scores.isEmpty()) {
return;
}
// 如果配置了裁判数量,检查是否评分完成
if (requiredJudgeCount > 0 && scores.size() < requiredJudgeCount) {
// 未完成评分,清空总分
MartialAthlete athlete = athleteService.getById(athleteId);
if (athlete != null && athlete.getTotalScore() != null) {
athlete.setTotalScore(null);
athleteService.updateById(athlete);
}
return;
}
// 4. 计算总分(去掉最高最低分取平均)
BigDecimal totalScore = calculateTotalScore(scores);
// 5. 更新选手总分
if (totalScore != null) {
MartialAthlete athlete = athleteService.getById(athleteId);
if (athlete != null) {
athlete.setTotalScore(totalScore);
athleteService.updateById(athlete);
}
}
} catch (Exception e) {
// 记录错误但不影响评分提交
e.printStackTrace();
}
}
/**
* 获取项目应评分的裁判数量(裁判员,不包括主裁判)
* 按项目过滤:检查 projects JSON 字段是否包含该项目ID
*/
private int getRequiredJudgeCount(Long venueId) {
if (venueId == null || venueId <= 0) {
return 0;
}
LambdaQueryWrapper<MartialJudgeInvite> judgeQuery = new LambdaQueryWrapper<>();
judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
judgeQuery.eq(MartialJudgeInvite::getVenueId, venueId);
judgeQuery.eq(MartialJudgeInvite::getRefereeType, 2); // Only count referees (type=2), exclude chief judge (type=1) and general judge (type=3)
List<MartialJudgeInvite> judges = judgeInviteService.list(judgeQuery);
// Use distinct judge_id to count unique judges
return (int) judges.stream()
.map(MartialJudgeInvite::getJudgeId)
.filter(Objects::nonNull)
.distinct()
.count();
}
/**
* 计算总分
* 算法:去掉一个最高分和一个最低分,取剩余分数的平均值
* 特殊情况:裁判数量<3时直接取平均分
*/
private BigDecimal calculateTotalScore(List<MartialScore> scores) {
if (scores == null || scores.isEmpty()) {
return null;
}
// 提取所有分数并排序
List<BigDecimal> scoreValues = scores.stream()
.map(MartialScore::getScore)
.filter(Objects::nonNull)
.sorted()
.collect(Collectors.toList());
int count = scoreValues.size();
if (count == 0) {
return null;
}
if (count < 3) {
// 裁判数量<3直接取平均分
BigDecimal sum = scoreValues.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
return sum.divide(new BigDecimal(count), 3, RoundingMode.HALF_UP);
}
// 去掉最高分和最低分(已排序,去掉第一个和最后一个)
List<BigDecimal> middleScores = scoreValues.subList(1, count - 1);
// 计算平均分
BigDecimal sum = middleScores.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
return sum.divide(new BigDecimal(middleScores.size()), 3, RoundingMode.HALF_UP);
}
/**
* 安全地将String转换为Long
*/
private Long parseLong(String value) {
if (value == null || value.trim().isEmpty()) {
return null;
}
try {
return Long.parseLong(value.trim());
} catch (NumberFormatException e) {
return null;
}
}
/**
* 获取选手列表(支持分页)
* - 裁判员:获取所有选手,标记是否已评分
* - 主裁判:获取所有裁判员都评分完成的选手列表
*/
@GetMapping("/score/athletes")
@Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)")
public R<IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO>> getAthletes(
@RequestParam Long judgeId,
@RequestParam Integer refereeType,
@RequestParam(required = false) Long projectId,
@RequestParam(required = false) Long venueId,
@RequestParam(required = false) Long competitionId,
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size
) { ) {
List<MiniAthleteAdminVO> result = athleteService.getAthletesForAdmin( // 1. 构建选手查询条件
competitionId, venueId, projectId); LambdaQueryWrapper<MartialAthlete> athleteQuery = new LambdaQueryWrapper<>();
return R.data(result); athleteQuery.eq(MartialAthlete::getIsDeleted, 0);
// 按比赛ID过滤重要确保只显示当前比赛的选手
if (competitionId != null) {
athleteQuery.eq(MartialAthlete::getCompetitionId, competitionId);
}
if (projectId != null) {
athleteQuery.eq(MartialAthlete::getProjectId, projectId);
}
athleteQuery.orderByAsc(MartialAthlete::getOrderNum);
List<MartialAthlete> athletes = athleteService.list(athleteQuery);
// 2. 获取该场地所有主裁判的judge_id列表
List<Long> chiefJudgeIds = getChiefJudgeIds(venueId);
// 3. 获取所有评分记录(排除主裁判的评分)
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getIsDeleted, 0);
if (projectId != null) {
scoreQuery.eq(MartialScore::getProjectId, projectId);
}
// 添加场地过滤
if (venueId != null && venueId > 0) {
scoreQuery.eq(MartialScore::getVenueId, venueId);
}
// 排除主裁判的评分
if (!chiefJudgeIds.isEmpty()) {
scoreQuery.notIn(MartialScore::getJudgeId, chiefJudgeIds);
}
List<MartialScore> allScores = scoreService.list(scoreQuery);
// 按选手ID分组统计评分
java.util.Map<Long, List<MartialScore>> scoresByAthlete = allScores.stream()
.collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId));
// 4. 获取该场地的应评裁判数量
int requiredJudgeCount = getRequiredJudgeCount(venueId);
// 5. 根据裁判类型处理选手列表
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList;
if (refereeType == 1) {
// 主裁判返回所有选手前端根据totalScore判断是否显示修改按钮
filteredList = athletes.stream()
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount))
.collect(java.util.stream.Collectors.toList());
} else {
// 裁判员:返回所有选手,标记是否已评分
filteredList = athletes.stream()
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount))
.collect(java.util.stream.Collectors.toList());
}
// 6. 手动分页
int total = filteredList.size();
int fromIndex = (current - 1) * size;
int toIndex = Math.min(fromIndex + size, total);
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> pageRecords;
if (fromIndex >= total) {
pageRecords = new ArrayList<>();
} else {
pageRecords = filteredList.subList(fromIndex, toIndex);
}
// 7. 构建分页结果
IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> page = new Page<>(current, size, total);
page.setRecords(pageRecords);
return R.data(page);
}
/**
* 获取场地所有主裁判的judge_id列表
*/
private List<Long> getChiefJudgeIds(Long venueId) {
if (venueId == null) {
return new ArrayList<>();
}
LambdaQueryWrapper<MartialJudgeInvite> judgeQuery = new LambdaQueryWrapper<>();
judgeQuery.eq(MartialJudgeInvite::getVenueId, venueId);
judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
judgeQuery.eq(MartialJudgeInvite::getRole, "chief_judge");
List<MartialJudgeInvite> chiefJudges = judgeInviteService.list(judgeQuery);
return chiefJudges.stream()
.map(MartialJudgeInvite::getJudgeId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
} }
/** /**
* 获取评分详情 * 获取评分详情
*
* @param athleteId 选手ID
* @return 评分详情(选手信息+所有评委的评分)
*/ */
@GetMapping("/score/detail/{athleteId}") @GetMapping("/score/detail/{athleteId}")
@Operation(summary = "评分详情", description = "查看选手的所有评委评分") @Operation(summary = "评分详情", description = "查看选手的所有评委评分")
@@ -177,18 +474,184 @@ public class MartialMiniController extends BladeController {
} }
/** /**
* 修改评分(裁判 * 修改评分(裁判)
*
* @param dto 修改信息选手ID、修改后的分数、修改原因
* @return 修改结果
*/ */
@PutMapping("/score/modify") @PutMapping("/score/modify")
@Operation(summary = "修改评分", description = "裁判修改选手总分") @Operation(summary = "修改评分", description = "裁判修改选手总分")
public R modifyScore(@RequestBody MiniScoreModifyDTO dto) { public R modifyScore(@RequestBody MiniScoreModifyDTO dto) {
boolean success = scoreService.modifyScoreByAdmin(dto); boolean success = scoreService.modifyScoreByAdmin(dto);
return success ? R.success("修改成功") : R.fail("修改失败"); return success ? R.success("修改成功") : R.fail("修改失败");
} }
/**
* 退出登录
*/
@PostMapping("/logout")
@Operation(summary = "退出登录", description = "清除登录状态")
public R logout(@RequestHeader(value = "Authorization", required = false) String token) {
// 从Redis删除登录缓存
if (token != null && !token.isEmpty()) {
String cacheKey = MINI_LOGIN_CACHE_PREFIX + token;
bladeRedis.del(cacheKey);
}
return R.success("退出成功");
}
/**
* Token验证从Redis恢复登录状态
*/
@GetMapping("/verify")
@Operation(summary = "Token验证", description = "验证token并返回登录信息支持服务重启后恢复登录状态")
public R<MiniLoginVO> verify(@RequestHeader(value = "Authorization", required = false) String token) {
if (token == null || token.isEmpty()) {
return R.fail("Token不能为空");
}
// 从Redis获取登录信息
String cacheKey = MINI_LOGIN_CACHE_PREFIX + token;
MiniLoginVO loginInfo = bladeRedis.get(cacheKey);
if (loginInfo != null) {
// 刷新缓存过期时间
bladeRedis.setEx(cacheKey, loginInfo, LOGIN_CACHE_EXPIRE);
return R.data(loginInfo);
}
// Redis中没有尝试从数据库恢复
LambdaQueryWrapper<MartialJudgeInvite> inviteQuery = new LambdaQueryWrapper<>();
inviteQuery.eq(MartialJudgeInvite::getAccessToken, token);
inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
MartialJudgeInvite invite = judgeInviteService.getOne(inviteQuery);
if (invite == null) {
return R.fail("Token无效");
}
if (invite.getTokenExpireTime() != null && invite.getTokenExpireTime().isBefore(LocalDateTime.now())) {
return R.fail("Token已过期");
}
// 重建登录信息
MartialCompetition competition = competitionService.getById(invite.getCompetitionId());
MartialJudge judge = judgeService.getById(invite.getJudgeId());
MartialVenue martialVenue = invite.getVenueId() != null ? venueService.getById(invite.getVenueId()) : null;
// 获取项目列表:总裁判看所有项目,其他裁判根据场地获取项目
List<MiniLoginVO.ProjectInfo> projects = new ArrayList<>();
Integer refereeTypeVal = invite.getRefereeType();
String roleVal = invite.getRole();
boolean isGeneralJudge = (refereeTypeVal != null && refereeTypeVal == 3)
|| "general_judge".equals(roleVal) || "general".equals(roleVal);
if (isGeneralJudge) {
// 总裁判看所有项目
projects = getAllProjectsByCompetition(competition.getId());
} else if (Func.isNotEmpty(invite.getProjects())) {
projects = parseProjects(invite.getProjects());
} else if (invite.getVenueId() != null) {
// 未指定项目,根据场地获取项目;如果场地没有项目则返回空列表
projects = getProjectsByVenue(invite.getVenueId());
}
// 如果没有场地projects保持为空列表
MiniLoginVO vo = new MiniLoginVO();
vo.setToken(token);
String role = invite.getRole();
Integer refereeType = invite.getRefereeType();
if ("general_judge".equals(role) || "general".equals(role) || (refereeType != null && refereeType == 3)) {
vo.setUserRole("general");
} else if ("chief_judge".equals(role) || (refereeType != null && refereeType == 1)) {
vo.setUserRole("admin");
} else {
vo.setUserRole("pub");
}
vo.setMatchId(competition != null ? competition.getId() : null);
vo.setMatchName(competition != null ? competition.getCompetitionName() : null);
vo.setMatchTime(competition != null && competition.getCompetitionStartTime() != null ?
competition.getCompetitionStartTime().toString() : "");
vo.setJudgeId(judge != null ? judge.getId() : null);
vo.setJudgeName(judge != null ? judge.getName() : null);
vo.setVenueId(martialVenue != null ? martialVenue.getId() : null);
vo.setVenueName(martialVenue != null ? martialVenue.getVenueName() : null);
vo.setProjects(projects);
// 重新缓存到Redis
bladeRedis.setEx(cacheKey, vo, LOGIN_CACHE_EXPIRE);
return R.data(vo);
}
/**
* 转换选手实体为VO
* 新增:只有评分完成时才显示总分
*/
private org.springblade.modules.martial.pojo.vo.MiniAthleteListVO convertToAthleteListVO(
MartialAthlete athlete,
List<MartialScore> scores,
Long currentJudgeId,
int requiredJudgeCount) {
org.springblade.modules.martial.pojo.vo.MiniAthleteListVO vo = new org.springblade.modules.martial.pojo.vo.MiniAthleteListVO();
vo.setAthleteId(athlete.getId());
vo.setName(athlete.getPlayerName());
vo.setIdCard(athlete.getIdCard());
vo.setNumber(athlete.getPlayerNo());
vo.setTeam(athlete.getTeamName());
vo.setOrderNum(athlete.getOrderNum());
vo.setCompetitionStatus(athlete.getCompetitionStatus());
// 设置应评分裁判数量
vo.setRequiredJudgeCount(requiredJudgeCount);
// 设置项目名称
if (athlete.getProjectId() != null) {
MartialProject project = projectService.getById(athlete.getProjectId());
if (project != null) {
vo.setProjectName(project.getProjectName());
}
}
// 设置评分状态
int scoredCount = 0;
if (scores != null && !scores.isEmpty()) {
scoredCount = scores.size();
vo.setScoredJudgeCount(scoredCount);
// 查找当前裁判的评分
MartialScore myScore = scores.stream()
.filter(s -> s.getJudgeId().equals(currentJudgeId))
.findFirst()
.orElse(null);
if (myScore != null) {
vo.setScored(true);
vo.setMyScore(myScore.getScore());
} else {
vo.setScored(false);
}
} else {
vo.setScored(false);
vo.setScoredJudgeCount(0);
}
// 判断评分是否完成(所有裁判都已评分)
boolean scoringComplete = false;
if (requiredJudgeCount > 0) {
scoringComplete = scoredCount >= requiredJudgeCount;
} else {
// 如果没有配置裁判数量,只要有评分就算完成
scoringComplete = scoredCount > 0;
}
vo.setScoringComplete(scoringComplete);
// 只有评分完成时才显示总分
if (scoringComplete) {
vo.setTotalScore(athlete.getTotalScore());
} else {
vo.setTotalScore(null);
}
return vo;
}
/** /**
* 解析项目JSON字符串 * 解析项目JSON字符串
*/ */
@@ -200,11 +663,9 @@ public class MartialMiniController extends BladeController {
} }
try { try {
// 解析JSON数组格式为 [{"projectId": 1, "projectName": "太极拳"}, ...]
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
List<Long> projectIds = mapper.readValue(projectsJson, new TypeReference<List<Long>>() {}); List<Long> projectIds = mapper.readValue(projectsJson, new TypeReference<List<Long>>() {});
// 查询项目详情
if (Func.isNotEmpty(projectIds)) { if (Func.isNotEmpty(projectIds)) {
List<MartialProject> projectList = projectService.listByIds(projectIds); List<MartialProject> projectList = projectService.listByIds(projectIds);
projects = projectList.stream().map(project -> { projects = projectList.stream().map(project -> {
@@ -215,7 +676,6 @@ public class MartialMiniController extends BladeController {
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }
} catch (Exception e) { } catch (Exception e) {
// 如果JSON解析失败尝试按逗号分隔的ID字符串解析
try { try {
String[] ids = projectsJson.split(","); String[] ids = projectsJson.split(",");
List<Long> projectIds = new ArrayList<>(); List<Long> projectIds = new ArrayList<>();
@@ -240,4 +700,271 @@ public class MartialMiniController extends BladeController {
return projects; return projects;
} }
/**
* 获取比赛的所有项目
*/
private List<MiniLoginVO.ProjectInfo> getAllProjectsByCompetition(Long competitionId) {
List<MiniLoginVO.ProjectInfo> projects = new ArrayList<>();
LambdaQueryWrapper<MartialProject> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialProject::getCompetitionId, competitionId);
wrapper.eq(MartialProject::getIsDeleted, 0);
List<MartialProject> projectList = projectService.list(wrapper);
if (Func.isNotEmpty(projectList)) {
projects = projectList.stream().map(project -> {
MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo();
info.setProjectId(project.getId());
info.setProjectName(project.getProjectName());
return info;
}).collect(Collectors.toList());
}
return projects;
}
/**
* 根据场地获取项目列表
*/
private List<MiniLoginVO.ProjectInfo> getProjectsByVenue(Long venueId) {
List<MiniLoginVO.ProjectInfo> projects = new ArrayList<>();
LambdaQueryWrapper<MartialProject> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialProject::getVenueId, venueId);
wrapper.eq(MartialProject::getIsDeleted, 0);
List<MartialProject> projectList = projectService.list(wrapper);
if (Func.isNotEmpty(projectList)) {
projects = projectList.stream().map(project -> {
MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo();
info.setProjectId(project.getId());
info.setProjectName(project.getProjectName());
return info;
}).collect(Collectors.toList());
}
return projects;
}
// ========== 三级裁判评分流程 API ==========
/**
* 主裁判确认/修改分数
*/
@PostMapping("/chief/confirm")
@Operation(summary = "主裁判确认分数", description = "主裁判确认或修改选手分数")
public R confirmByChiefJudge(@RequestBody ChiefJudgeConfirmDTO dto) {
Long resultId = parseLong(dto.getResultId());
Long chiefJudgeId = parseLong(dto.getChiefJudgeId());
if (resultId == null || chiefJudgeId == null) {
return R.fail("参数错误");
}
boolean success = resultService.confirmByChiefJudge(resultId, chiefJudgeId, dto.getScore(), dto.getNote());
return success ? R.success("确认成功") : R.fail("确认失败");
}
/**
* 总裁确认/修改分数
*/
@PostMapping("/general/confirm")
@Operation(summary = "总裁确认分数", description = "总裁确认或修改选手分数")
public R confirmByGeneralJudge(@RequestBody GeneralJudgeConfirmDTO dto) {
Long resultId = parseLong(dto.getResultId());
Long generalJudgeId = parseLong(dto.getGeneralJudgeId());
if (resultId == null || generalJudgeId == null) {
return R.fail("参数错误");
}
boolean success = resultService.confirmByGeneralJudge(resultId, generalJudgeId, dto.getScore(), dto.getNote());
return success ? R.success("确认成功") : R.fail("确认失败");
}
/**
* 获取待主裁判确认的成绩列表
*/
@GetMapping("/chief/pending")
@Operation(summary = "待主裁判确认列表", description = "获取待主裁判确认的成绩列表")
public R<List<MartialResult>> getPendingChiefConfirmList(@RequestParam Long venueId) {
List<MartialResult> list = resultService.getPendingChiefConfirmList(venueId);
return R.data(list);
}
/**
* 获取待总裁确认的成绩列表
*/
@GetMapping("/general/pending")
@Operation(summary = "待总裁确认列表", description = "获取待总裁确认的成绩列表(所有场地)")
public R<List<MartialResult>> getPendingGeneralConfirmList(@RequestParam Long competitionId) {
List<MartialResult> list = resultService.getPendingGeneralConfirmList(competitionId);
return R.data(list);
}
/**
* 获取所有场地列表(总裁用)
*/
@GetMapping("/general/venues")
@Operation(summary = "获取所有场地", description = "总裁获取比赛的所有场地列表")
public R<List<MartialVenue>> getAllVenues(@RequestParam Long competitionId) {
LambdaQueryWrapper<MartialVenue> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialVenue::getCompetitionId, competitionId);
wrapper.eq(MartialVenue::getIsDeleted, 0);
wrapper.orderByAsc(MartialVenue::getVenueName);
List<MartialVenue> venues = venueService.list(wrapper);
return R.data(venues);
}
/**
* 获取已总裁确认的成绩列表
*/
@GetMapping("/general/confirmed")
@Operation(summary = "已总裁确认列表", description = "获取已总裁确认的成绩列表")
public R<List<MartialResult>> getConfirmedGeneralList(@RequestParam Long competitionId) {
List<MartialResult> list = resultService.getConfirmedGeneralList(competitionId);
return R.data(list);
}
// ========== 出场顺序相关 API ==========
/**
* 获取编排状态
*/
@GetMapping("/schedule/status")
@Operation(summary = "获取编排状态", description = "检查赛事编排是否完成")
public R<Map<String, Object>> getScheduleStatus(@RequestParam Long competitionId) {
Map<String, Object> result = new HashMap<>();
LambdaQueryWrapper<MartialScheduleStatus> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialScheduleStatus::getCompetitionId, competitionId);
wrapper.eq(MartialScheduleStatus::getIsDeleted, 0);
wrapper.last("LIMIT 1");
MartialScheduleStatus status = scheduleStatusMapper.selectOne(wrapper);
if (status == null) {
result.put("isCompleted", false);
result.put("scheduleStatus", 0);
result.put("statusText", "未编排");
return R.data(result);
}
boolean isCompleted = status.getScheduleStatus() != null && status.getScheduleStatus() == 2;
result.put("isCompleted", isCompleted);
result.put("scheduleStatus", status.getScheduleStatus());
result.put("statusText", getScheduleStatusText(status.getScheduleStatus()));
result.put("totalGroups", status.getTotalGroups());
result.put("totalParticipants", status.getTotalParticipants());
result.put("lockedTime", status.getLockedTime());
return R.data(result);
}
private String getScheduleStatusText(Integer status) {
if (status == null) return "未编排";
switch (status) {
case 0: return "未编排";
case 1: return "编排中";
case 2: return "已锁定";
default: return "未知";
}
}
/**
* 获取出场顺序
*/
@GetMapping("/schedule/lineup")
@Operation(summary = "获取出场顺序", description = "获取已编排的出场顺序列表")
public R<Map<String, Object>> getLineup(
@RequestParam Long competitionId,
@RequestParam(required = false) Long projectId
) {
Map<String, Object> result = new HashMap<>();
// 使用现有mapper查询编排详情
List<ScheduleGroupDetailVO> details = scheduleGroupMapper.selectScheduleGroupDetails(competitionId);
if (details == null || details.isEmpty()) {
result.put("groups", new ArrayList<>());
return R.data(result);
}
// 按项目过滤
if (projectId != null) {
// 需要通过groupName或其他字段判断项目这里先获取项目名
MartialProject project = projectService.getById(projectId);
if (project != null) {
String projectName = project.getProjectName();
details = details.stream()
.filter(d -> d.getGroupName() != null && d.getGroupName().contains(projectName))
.collect(Collectors.toList());
}
}
// 转换为LineupGroupVO格式
Map<Long, LineupGroupVO> groupMap = new HashMap<>();
for (ScheduleGroupDetailVO detail : details) {
Long groupId = detail.getGroupId();
LineupGroupVO group = groupMap.get(groupId);
if (group == null) {
group = new LineupGroupVO();
group.setGroupId(groupId);
group.setGroupName(detail.getGroupName());
group.setCategory(detail.getCategory());
group.setVenueName(detail.getVenueName());
group.setTimeSlot(detail.getTimeSlot());
group.setTableNo(generateTableNo(detail));
group.setParticipants(new ArrayList<>());
groupMap.put(groupId, group);
}
// 添加参赛者
if (detail.getParticipantId() != null) {
LineupParticipantVO participant = new LineupParticipantVO();
participant.setId(detail.getParticipantId());
participant.setOrder(detail.getPerformanceOrder() != null ? detail.getPerformanceOrder() : group.getParticipants().size() + 1);
participant.setPlayerName(detail.getPlayerName());
participant.setOrganization(detail.getOrganization());
participant.setStatus(detail.getScheduleStatus() != null ? detail.getScheduleStatus() : "waiting");
group.getParticipants().add(participant);
}
}
result.put("groups", new ArrayList<>(groupMap.values()));
return R.data(result);
}
/**
* 生成表号: 场地(1位) + 时段(1位) + 序号(2位)
*/
private String generateTableNo(ScheduleGroupDetailVO detail) {
// 场地编号简单取第一个数字或默认1
int venueNo = 1;
if (detail.getVenueName() != null) {
String venueName = detail.getVenueName();
for (char c : venueName.toCharArray()) {
if (Character.isDigit(c)) {
venueNo = Character.getNumericValue(c);
break;
}
}
}
// 时段:上午=1, 下午=2
int period = 1;
if (detail.getTimeSlot() != null) {
try {
int hour = Integer.parseInt(detail.getTimeSlot().split(":")[0]);
period = hour < 12 ? 1 : 2;
} catch (Exception e) {
// ignore
}
}
// 序号使用displayOrder或默认1
int orderNo = detail.getDisplayOrder() != null ? detail.getDisplayOrder() : 1;
return String.format("%d%d%02d", venueNo, period, orderNo);
}
} }

View File

@@ -0,0 +1,820 @@
package org.springblade.modules.martial.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.modules.martial.pojo.dto.MiniAthleteScoreDTO;
import org.springblade.modules.martial.pojo.dto.MiniLoginDTO;
import org.springblade.modules.martial.pojo.dto.MiniScoreModifyDTO;
import org.springblade.modules.martial.pojo.entity.*;
import org.springblade.modules.martial.pojo.vo.MiniAthleteAdminVO;
import org.springblade.modules.martial.pojo.vo.MiniAthleteScoreVO;
import org.springblade.modules.martial.pojo.vo.MiniLoginVO;
import org.springblade.modules.martial.pojo.vo.MiniScoreDetailVO;
import com.alibaba.fastjson.JSON;
import org.springblade.modules.martial.service.*;
import org.springblade.modules.martial.pojo.dto.ChiefJudgeConfirmDTO;
import org.springblade.modules.martial.pojo.dto.GeneralJudgeConfirmDTO;
import org.springblade.modules.martial.pojo.entity.MtVenue;
import org.springblade.modules.martial.pojo.entity.MartialVenue;
import org.springblade.modules.martial.pojo.entity.MartialResult;
import org.springblade.core.redis.cache.BladeRedis;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* 小程序专用接口 控制器
*
* @author BladeX
*/
@RestController
@AllArgsConstructor
@RequestMapping("/mini")
@Tag(name = "小程序接口", description = "小程序评分系统专用接口")
public class MartialMiniController extends BladeController {
private final IMartialJudgeInviteService judgeInviteService;
private final IMartialJudgeService judgeService;
private final IMartialCompetitionService competitionService;
private final IMartialVenueService venueService;
private final IMtVenueService mtVenueService;
private final IMartialProjectService projectService;
private final IMartialAthleteService athleteService;
private final IMartialScoreService scoreService;
private final BladeRedis bladeRedis;
private final IMartialResultService resultService;
// Redis缓存key前缀
private static final String MINI_LOGIN_CACHE_PREFIX = "mini:login:";
// 登录缓存过期时间7天
private static final Duration LOGIN_CACHE_EXPIRE = Duration.ofDays(7);
/**
* 登录验证
*/
@PostMapping("/login")
@Operation(summary = "登录验证", description = "使用比赛编码和邀请码登录")
public R<MiniLoginVO> login(@RequestBody MiniLoginDTO dto) {
LambdaQueryWrapper<MartialJudgeInvite> inviteQuery = new LambdaQueryWrapper<>();
inviteQuery.eq(MartialJudgeInvite::getInviteCode, dto.getInviteCode());
inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
MartialJudgeInvite invite = judgeInviteService.getOne(inviteQuery);
if (invite == null) {
return R.fail("邀请码不存在");
}
if (invite.getExpireTime() != null && invite.getExpireTime().isBefore(LocalDateTime.now())) {
return R.fail("邀请码已过期");
}
MartialCompetition competition = competitionService.getById(invite.getCompetitionId());
if (competition == null) {
return R.fail("比赛不存在");
}
if (!competition.getCompetitionCode().equals(dto.getMatchCode())) {
return R.fail("比赛编码不匹配");
}
MartialJudge judge = judgeService.getById(invite.getJudgeId());
if (judge == null) {
return R.fail("评委信息不存在");
}
String token = UUID.randomUUID().toString().replace("-", "");
invite.setAccessToken(token);
invite.setTokenExpireTime(LocalDateTime.now().plusDays(7));
invite.setIsUsed(1);
invite.setUseTime(LocalDateTime.now());
invite.setLoginIp(dto.getLoginIp());
invite.setDeviceInfo(dto.getDeviceInfo());
judgeInviteService.updateById(invite);
// 从 martial_venue 表获取场地信息
MartialVenue martialVenue = null;
if (invite.getVenueId() != null) {
martialVenue = venueService.getById(invite.getVenueId());
}
// 获取项目列表:总裁判看所有项目,其他裁判根据场地获取项目
List<MiniLoginVO.ProjectInfo> projects = new ArrayList<>();
Integer refereeTypeVal = invite.getRefereeType();
String roleVal = invite.getRole();
boolean isGeneralJudge = (refereeTypeVal != null && refereeTypeVal == 3)
|| "general_judge".equals(roleVal) || "general".equals(roleVal);
if (isGeneralJudge) {
// 总裁判看所有项目
projects = getAllProjectsByCompetition(competition.getId());
} else if (Func.isNotEmpty(invite.getProjects())) {
projects = parseProjects(invite.getProjects());
} else if (invite.getVenueId() != null) {
// 未指定项目,根据场地获取项目;如果场地没有项目则返回空列表
projects = getProjectsByVenue(invite.getVenueId());
}
// 如果没有场地projects保持为空列表
MiniLoginVO vo = new MiniLoginVO();
vo.setToken(token);
String role = invite.getRole();
Integer refereeType = invite.getRefereeType();
if ("general_judge".equals(role) || "general".equals(role) || (refereeType != null && refereeType == 3)) {
vo.setUserRole("general");
} else if ("chief_judge".equals(role) || (refereeType != null && refereeType == 1)) {
vo.setUserRole("admin");
} else {
vo.setUserRole("pub");
}
vo.setMatchId(competition.getId());
vo.setMatchName(competition.getCompetitionName());
vo.setMatchTime(competition.getCompetitionStartTime() != null ?
competition.getCompetitionStartTime().toString() : "");
vo.setJudgeId(judge.getId());
vo.setJudgeName(judge.getName());
vo.setVenueId(martialVenue != null ? martialVenue.getId() : null);
vo.setVenueName(martialVenue != null ? martialVenue.getVenueName() : null);
vo.setProjects(projects);
// 将登录信息缓存到Redis服务重启后仍然有效
String cacheKey = MINI_LOGIN_CACHE_PREFIX + token;
bladeRedis.setEx(cacheKey, vo, LOGIN_CACHE_EXPIRE);
return R.data(vo);
}
/**
* 提交评分(评委)
* 注意ID字段使用String类型接收避免JavaScript大数精度丢失问题
*/
@PostMapping("/score/submit")
@Operation(summary = "提交评分", description = "评委提交对选手的评分")
public R submitScore(@RequestBody org.springblade.modules.martial.pojo.dto.MiniScoreSubmitDTO dto) {
MartialScore score = new MartialScore();
// 将String类型的ID转换为Long避免JavaScript大数精度丢失
score.setAthleteId(parseLong(dto.getAthleteId()));
score.setJudgeId(parseLong(dto.getJudgeId()));
score.setScore(dto.getScore());
score.setProjectId(parseLong(dto.getProjectId()));
score.setCompetitionId(parseLong(dto.getCompetitionId()));
score.setVenueId(parseLong(dto.getVenueId()));
score.setScheduleId(parseLong(dto.getScheduleId()));
score.setNote(dto.getNote());
score.setScoreTime(LocalDateTime.now());
if (dto.getDeductions() != null && !dto.getDeductions().isEmpty()) {
// 将String类型的扣分项ID转换为Long
List<Long> deductionIds = dto.getDeductions().stream()
.map(this::parseLong)
.filter(id -> id != null)
.collect(Collectors.toList());
score.setDeductionItems(com.alibaba.fastjson.JSON.toJSONString(deductionIds));
}
Long judgeId = parseLong(dto.getJudgeId());
if (judgeId != null) {
var judge = judgeService.getById(judgeId);
if (judge != null) {
score.setJudgeName(judge.getName());
}
}
boolean success = scoreService.save(score);
// 评分保存成功后,计算并更新选手总分
if (success) {
Long athleteId = parseLong(dto.getAthleteId());
Long projectId = parseLong(dto.getProjectId());
Long venueId = parseLong(dto.getVenueId());
if (athleteId != null && projectId != null) {
updateAthleteTotalScore(athleteId, projectId, venueId);
}
}
return success ? R.success("评分提交成功") : R.fail("评分提交失败");
}
/**
* 计算并更新选手总分
* 总分算法:去掉一个最高分和一个最低分,取剩余分数的平均值
* 特殊情况:裁判数量<3时直接取平均分
* 只有所有裁判都评分完成后才更新总分
*/
private void updateAthleteTotalScore(Long athleteId, Long projectId, Long venueId) {
try {
// 1. 查询该场地的裁判员数量
int requiredJudgeCount = getRequiredJudgeCount(venueId);
// 2. 获取主裁判ID列表
List<Long> chiefJudgeIds = getChiefJudgeIds(venueId);
// 3. 查询该选手在该项目的所有评分(排除主裁判的评分)
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getAthleteId, athleteId);
scoreQuery.eq(MartialScore::getProjectId, projectId);
scoreQuery.eq(MartialScore::getIsDeleted, 0);
// 排除主裁判的所有评分(包括普通评分和修改记录)
if (!chiefJudgeIds.isEmpty()) {
scoreQuery.notIn(MartialScore::getJudgeId, chiefJudgeIds);
}
List<MartialScore> scores = scoreService.list(scoreQuery);
// 4. 判断是否所有裁判都已评分
if (scores == null || scores.isEmpty()) {
return;
}
// 如果配置了裁判数量,检查是否评分完成
if (requiredJudgeCount > 0 && scores.size() < requiredJudgeCount) {
// 未完成评分,清空总分
MartialAthlete athlete = athleteService.getById(athleteId);
if (athlete != null && athlete.getTotalScore() != null) {
athlete.setTotalScore(null);
athleteService.updateById(athlete);
}
return;
}
// 4. 计算总分(去掉最高最低分取平均)
BigDecimal totalScore = calculateTotalScore(scores);
// 5. 更新选手总分
if (totalScore != null) {
MartialAthlete athlete = athleteService.getById(athleteId);
if (athlete != null) {
athlete.setTotalScore(totalScore);
athleteService.updateById(athlete);
}
}
} catch (Exception e) {
// 记录错误但不影响评分提交
e.printStackTrace();
}
}
/**
* 获取项目应评分的裁判数量(裁判员,不包括主裁判)
* 按项目过滤:检查 projects JSON 字段是否包含该项目ID
*/
private int getRequiredJudgeCount(Long venueId) {
if (venueId == null || venueId <= 0) {
return 0;
}
LambdaQueryWrapper<MartialJudgeInvite> judgeQuery = new LambdaQueryWrapper<>();
judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
judgeQuery.eq(MartialJudgeInvite::getVenueId, venueId);
judgeQuery.eq(MartialJudgeInvite::getRefereeType, 2); // Only count referees (type=2), exclude chief judge (type=1) and general judge (type=3)
List<MartialJudgeInvite> judges = judgeInviteService.list(judgeQuery);
// Use distinct judge_id to count unique judges
return (int) judges.stream()
.map(MartialJudgeInvite::getJudgeId)
.filter(Objects::nonNull)
.distinct()
.count();
}
/**
* 计算总分
* 算法:去掉一个最高分和一个最低分,取剩余分数的平均值
* 特殊情况:裁判数量<3时直接取平均分
*/
private BigDecimal calculateTotalScore(List<MartialScore> scores) {
if (scores == null || scores.isEmpty()) {
return null;
}
// 提取所有分数并排序
List<BigDecimal> scoreValues = scores.stream()
.map(MartialScore::getScore)
.filter(Objects::nonNull)
.sorted()
.collect(Collectors.toList());
int count = scoreValues.size();
if (count == 0) {
return null;
}
if (count < 3) {
// 裁判数量<3直接取平均分
BigDecimal sum = scoreValues.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
return sum.divide(new BigDecimal(count), 3, RoundingMode.HALF_UP);
}
// 去掉最高分和最低分(已排序,去掉第一个和最后一个)
List<BigDecimal> middleScores = scoreValues.subList(1, count - 1);
// 计算平均分
BigDecimal sum = middleScores.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
return sum.divide(new BigDecimal(middleScores.size()), 3, RoundingMode.HALF_UP);
}
/**
* 安全地将String转换为Long
*/
private Long parseLong(String value) {
if (value == null || value.trim().isEmpty()) {
return null;
}
try {
return Long.parseLong(value.trim());
} catch (NumberFormatException e) {
return null;
}
}
/**
* 获取选手列表(支持分页)
* - 裁判员:获取所有选手,标记是否已评分
* - 主裁判:获取所有裁判员都评分完成的选手列表
*/
@GetMapping("/score/athletes")
@Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)")
public R<IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO>> getAthletes(
@RequestParam Long judgeId,
@RequestParam Integer refereeType,
@RequestParam(required = false) Long projectId,
@RequestParam(required = false) Long venueId,
@RequestParam(required = false) Long competitionId,
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size
) {
// 1. 构建选手查询条件
LambdaQueryWrapper<MartialAthlete> athleteQuery = new LambdaQueryWrapper<>();
athleteQuery.eq(MartialAthlete::getIsDeleted, 0);
// 按比赛ID过滤重要确保只显示当前比赛的选手
if (competitionId != null) {
athleteQuery.eq(MartialAthlete::getCompetitionId, competitionId);
}
if (projectId != null) {
athleteQuery.eq(MartialAthlete::getProjectId, projectId);
}
athleteQuery.orderByAsc(MartialAthlete::getOrderNum);
List<MartialAthlete> athletes = athleteService.list(athleteQuery);
// 2. 获取该场地所有主裁判的judge_id列表
List<Long> chiefJudgeIds = getChiefJudgeIds(venueId);
// 3. 获取所有评分记录(排除主裁判的评分)
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getIsDeleted, 0);
if (projectId != null) {
scoreQuery.eq(MartialScore::getProjectId, projectId);
}
// 添加场地过滤
if (venueId != null && venueId > 0) {
scoreQuery.eq(MartialScore::getVenueId, venueId);
}
// 排除主裁判的评分
if (!chiefJudgeIds.isEmpty()) {
scoreQuery.notIn(MartialScore::getJudgeId, chiefJudgeIds);
}
List<MartialScore> allScores = scoreService.list(scoreQuery);
// 按选手ID分组统计评分
java.util.Map<Long, List<MartialScore>> scoresByAthlete = allScores.stream()
.collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId));
// 4. 获取该场地的应评裁判数量
int requiredJudgeCount = getRequiredJudgeCount(venueId);
// 5. 根据裁判类型处理选手列表
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList;
if (refereeType == 1) {
// 主裁判返回所有选手前端根据totalScore判断是否显示修改按钮
filteredList = athletes.stream()
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount))
.collect(java.util.stream.Collectors.toList());
} else {
// 裁判员:返回所有选手,标记是否已评分
filteredList = athletes.stream()
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount))
.collect(java.util.stream.Collectors.toList());
}
// 6. 手动分页
int total = filteredList.size();
int fromIndex = (current - 1) * size;
int toIndex = Math.min(fromIndex + size, total);
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> pageRecords;
if (fromIndex >= total) {
pageRecords = new ArrayList<>();
} else {
pageRecords = filteredList.subList(fromIndex, toIndex);
}
// 7. 构建分页结果
IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> page = new Page<>(current, size, total);
page.setRecords(pageRecords);
return R.data(page);
}
/**
* 获取场地所有主裁判的judge_id列表
*/
private List<Long> getChiefJudgeIds(Long venueId) {
if (venueId == null) {
return new ArrayList<>();
}
LambdaQueryWrapper<MartialJudgeInvite> judgeQuery = new LambdaQueryWrapper<>();
judgeQuery.eq(MartialJudgeInvite::getVenueId, venueId);
judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
judgeQuery.eq(MartialJudgeInvite::getRole, "chief_judge");
List<MartialJudgeInvite> chiefJudges = judgeInviteService.list(judgeQuery);
return chiefJudges.stream()
.map(MartialJudgeInvite::getJudgeId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
/**
* 获取评分详情
*/
@GetMapping("/score/detail/{athleteId}")
@Operation(summary = "评分详情", description = "查看选手的所有评委评分")
public R<MiniScoreDetailVO> getScoreDetail(@PathVariable Long athleteId) {
MiniScoreDetailVO detail = scoreService.getScoreDetailForMini(athleteId);
return R.data(detail);
}
/**
* 修改评分(主裁判)
*/
@PutMapping("/score/modify")
@Operation(summary = "修改评分", description = "主裁判修改选手总分")
public R modifyScore(@RequestBody MiniScoreModifyDTO dto) {
boolean success = scoreService.modifyScoreByAdmin(dto);
return success ? R.success("修改成功") : R.fail("修改失败");
}
/**
* 退出登录
*/
@PostMapping("/logout")
@Operation(summary = "退出登录", description = "清除登录状态")
public R logout(@RequestHeader(value = "Authorization", required = false) String token) {
// 从Redis删除登录缓存
if (token != null && !token.isEmpty()) {
String cacheKey = MINI_LOGIN_CACHE_PREFIX + token;
bladeRedis.del(cacheKey);
}
return R.success("退出成功");
}
/**
* Token验证从Redis恢复登录状态
*/
@GetMapping("/verify")
@Operation(summary = "Token验证", description = "验证token并返回登录信息支持服务重启后恢复登录状态")
public R<MiniLoginVO> verify(@RequestHeader(value = "Authorization", required = false) String token) {
if (token == null || token.isEmpty()) {
return R.fail("Token不能为空");
}
// 从Redis获取登录信息
String cacheKey = MINI_LOGIN_CACHE_PREFIX + token;
MiniLoginVO loginInfo = bladeRedis.get(cacheKey);
if (loginInfo != null) {
// 刷新缓存过期时间
bladeRedis.setEx(cacheKey, loginInfo, LOGIN_CACHE_EXPIRE);
return R.data(loginInfo);
}
// Redis中没有尝试从数据库恢复
LambdaQueryWrapper<MartialJudgeInvite> inviteQuery = new LambdaQueryWrapper<>();
inviteQuery.eq(MartialJudgeInvite::getAccessToken, token);
inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
MartialJudgeInvite invite = judgeInviteService.getOne(inviteQuery);
if (invite == null) {
return R.fail("Token无效");
}
if (invite.getTokenExpireTime() != null && invite.getTokenExpireTime().isBefore(LocalDateTime.now())) {
return R.fail("Token已过期");
}
// 重建登录信息
MartialCompetition competition = competitionService.getById(invite.getCompetitionId());
MartialJudge judge = judgeService.getById(invite.getJudgeId());
MartialVenue martialVenue = invite.getVenueId() != null ? venueService.getById(invite.getVenueId()) : null;
// 获取项目列表:总裁判看所有项目,其他裁判根据场地获取项目
List<MiniLoginVO.ProjectInfo> projects = new ArrayList<>();
Integer refereeTypeVal = invite.getRefereeType();
String roleVal = invite.getRole();
boolean isGeneralJudge = (refereeTypeVal != null && refereeTypeVal == 3)
|| "general_judge".equals(roleVal) || "general".equals(roleVal);
if (isGeneralJudge) {
// 总裁判看所有项目
projects = getAllProjectsByCompetition(competition.getId());
} else if (Func.isNotEmpty(invite.getProjects())) {
projects = parseProjects(invite.getProjects());
} else if (invite.getVenueId() != null) {
// 未指定项目,根据场地获取项目;如果场地没有项目则返回空列表
projects = getProjectsByVenue(invite.getVenueId());
}
// 如果没有场地projects保持为空列表
MiniLoginVO vo = new MiniLoginVO();
vo.setToken(token);
String role = invite.getRole();
Integer refereeType = invite.getRefereeType();
if ("general_judge".equals(role) || "general".equals(role) || (refereeType != null && refereeType == 3)) {
vo.setUserRole("general");
} else if ("chief_judge".equals(role) || (refereeType != null && refereeType == 1)) {
vo.setUserRole("admin");
} else {
vo.setUserRole("pub");
}
vo.setMatchId(competition != null ? competition.getId() : null);
vo.setMatchName(competition != null ? competition.getCompetitionName() : null);
vo.setMatchTime(competition != null && competition.getCompetitionStartTime() != null ?
competition.getCompetitionStartTime().toString() : "");
vo.setJudgeId(judge != null ? judge.getId() : null);
vo.setJudgeName(judge != null ? judge.getName() : null);
vo.setVenueId(martialVenue != null ? martialVenue.getId() : null);
vo.setVenueName(martialVenue != null ? martialVenue.getVenueName() : null);
vo.setProjects(projects);
// 重新缓存到Redis
bladeRedis.setEx(cacheKey, vo, LOGIN_CACHE_EXPIRE);
return R.data(vo);
}
/**
* 转换选手实体为VO
* 新增:只有评分完成时才显示总分
*/
private org.springblade.modules.martial.pojo.vo.MiniAthleteListVO convertToAthleteListVO(
MartialAthlete athlete,
List<MartialScore> scores,
Long currentJudgeId,
int requiredJudgeCount) {
org.springblade.modules.martial.pojo.vo.MiniAthleteListVO vo = new org.springblade.modules.martial.pojo.vo.MiniAthleteListVO();
vo.setAthleteId(athlete.getId());
vo.setName(athlete.getPlayerName());
vo.setIdCard(athlete.getIdCard());
vo.setNumber(athlete.getPlayerNo());
vo.setTeam(athlete.getTeamName());
vo.setOrderNum(athlete.getOrderNum());
vo.setCompetitionStatus(athlete.getCompetitionStatus());
// 设置应评分裁判数量
vo.setRequiredJudgeCount(requiredJudgeCount);
// 设置项目名称
if (athlete.getProjectId() != null) {
MartialProject project = projectService.getById(athlete.getProjectId());
if (project != null) {
vo.setProjectName(project.getProjectName());
}
}
// 设置评分状态
int scoredCount = 0;
if (scores != null && !scores.isEmpty()) {
scoredCount = scores.size();
vo.setScoredJudgeCount(scoredCount);
// 查找当前裁判的评分
MartialScore myScore = scores.stream()
.filter(s -> s.getJudgeId().equals(currentJudgeId))
.findFirst()
.orElse(null);
if (myScore != null) {
vo.setScored(true);
vo.setMyScore(myScore.getScore());
} else {
vo.setScored(false);
}
} else {
vo.setScored(false);
vo.setScoredJudgeCount(0);
}
// 判断评分是否完成(所有裁判都已评分)
boolean scoringComplete = false;
if (requiredJudgeCount > 0) {
scoringComplete = scoredCount >= requiredJudgeCount;
} else {
// 如果没有配置裁判数量,只要有评分就算完成
scoringComplete = scoredCount > 0;
}
vo.setScoringComplete(scoringComplete);
// 只有评分完成时才显示总分
if (scoringComplete) {
vo.setTotalScore(athlete.getTotalScore());
} else {
vo.setTotalScore(null);
}
return vo;
}
/**
* 解析项目JSON字符串
*/
private List<MiniLoginVO.ProjectInfo> parseProjects(String projectsJson) {
List<MiniLoginVO.ProjectInfo> projects = new ArrayList<>();
if (Func.isEmpty(projectsJson)) {
return projects;
}
try {
ObjectMapper mapper = new ObjectMapper();
List<Long> projectIds = mapper.readValue(projectsJson, new TypeReference<List<Long>>() {});
if (Func.isNotEmpty(projectIds)) {
List<MartialProject> projectList = projectService.listByIds(projectIds);
projects = projectList.stream().map(project -> {
MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo();
info.setProjectId(project.getId());
info.setProjectName(project.getProjectName());
return info;
}).collect(Collectors.toList());
}
} catch (Exception e) {
try {
String[] ids = projectsJson.split(",");
List<Long> projectIds = new ArrayList<>();
for (String id : ids) {
projectIds.add(Long.parseLong(id.trim()));
}
if (Func.isNotEmpty(projectIds)) {
List<MartialProject> projectList = projectService.listByIds(projectIds);
projects = projectList.stream().map(project -> {
MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo();
info.setProjectId(project.getId());
info.setProjectName(project.getProjectName());
return info;
}).collect(Collectors.toList());
}
} catch (Exception ex) {
// 解析失败,返回空列表
}
}
return projects;
}
/**
* 获取比赛的所有项目
*/
private List<MiniLoginVO.ProjectInfo> getAllProjectsByCompetition(Long competitionId) {
List<MiniLoginVO.ProjectInfo> projects = new ArrayList<>();
LambdaQueryWrapper<MartialProject> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialProject::getCompetitionId, competitionId);
wrapper.eq(MartialProject::getIsDeleted, 0);
List<MartialProject> projectList = projectService.list(wrapper);
if (Func.isNotEmpty(projectList)) {
projects = projectList.stream().map(project -> {
MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo();
info.setProjectId(project.getId());
info.setProjectName(project.getProjectName());
return info;
}).collect(Collectors.toList());
}
return projects;
}
/**
* 根据场地获取项目列表
*/
private List<MiniLoginVO.ProjectInfo> getProjectsByVenue(Long venueId) {
List<MiniLoginVO.ProjectInfo> projects = new ArrayList<>();
LambdaQueryWrapper<MartialProject> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialProject::getVenueId, venueId);
wrapper.eq(MartialProject::getIsDeleted, 0);
List<MartialProject> projectList = projectService.list(wrapper);
if (Func.isNotEmpty(projectList)) {
projects = projectList.stream().map(project -> {
MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo();
info.setProjectId(project.getId());
info.setProjectName(project.getProjectName());
return info;
}).collect(Collectors.toList());
}
return projects;
}
// ========== 三级裁判评分流程 API ==========
/**
* 主裁判确认/修改分数
*/
@PostMapping("/chief/confirm")
@Operation(summary = "主裁判确认分数", description = "主裁判确认或修改选手分数")
public R confirmByChiefJudge(@RequestBody ChiefJudgeConfirmDTO dto) {
Long resultId = parseLong(dto.getResultId());
Long chiefJudgeId = parseLong(dto.getChiefJudgeId());
if (resultId == null || chiefJudgeId == null) {
return R.fail("参数错误");
}
boolean success = resultService.confirmByChiefJudge(resultId, chiefJudgeId, dto.getScore(), dto.getNote());
return success ? R.success("确认成功") : R.fail("确认失败");
}
/**
* 总裁确认/修改分数
*/
@PostMapping("/general/confirm")
@Operation(summary = "总裁确认分数", description = "总裁确认或修改选手分数")
public R confirmByGeneralJudge(@RequestBody GeneralJudgeConfirmDTO dto) {
Long resultId = parseLong(dto.getResultId());
Long generalJudgeId = parseLong(dto.getGeneralJudgeId());
if (resultId == null || generalJudgeId == null) {
return R.fail("参数错误");
}
boolean success = resultService.confirmByGeneralJudge(resultId, generalJudgeId, dto.getScore(), dto.getNote());
return success ? R.success("确认成功") : R.fail("确认失败");
}
/**
* 获取待主裁判确认的成绩列表
*/
@GetMapping("/chief/pending")
@Operation(summary = "待主裁判确认列表", description = "获取待主裁判确认的成绩列表")
public R<List<MartialResult>> getPendingChiefConfirmList(@RequestParam Long venueId) {
List<MartialResult> list = resultService.getPendingChiefConfirmList(venueId);
return R.data(list);
}
/**
* 获取待总裁确认的成绩列表
*/
@GetMapping("/general/pending")
@Operation(summary = "待总裁确认列表", description = "获取待总裁确认的成绩列表(所有场地)")
public R<List<MartialResult>> getPendingGeneralConfirmList(@RequestParam Long competitionId) {
List<MartialResult> list = resultService.getPendingGeneralConfirmList(competitionId);
return R.data(list);
}
/**
* 获取所有场地列表(总裁用)
*/
@GetMapping("/general/venues")
@Operation(summary = "获取所有场地", description = "总裁获取比赛的所有场地列表")
public R<List<MartialVenue>> getAllVenues(@RequestParam Long competitionId) {
LambdaQueryWrapper<MartialVenue> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialVenue::getCompetitionId, competitionId);
wrapper.eq(MartialVenue::getIsDeleted, 0);
wrapper.orderByAsc(MartialVenue::getVenueName);
List<MartialVenue> venues = venueService.list(wrapper);
return R.data(venues);
}
/**
* 获取已总裁确认的成绩列表
*/
@GetMapping("/general/confirmed")
@Operation(summary = "已总裁确认列表", description = "获取已总裁确认的成绩列表")
public R<List<MartialResult>> getConfirmedGeneralList(@RequestParam Long competitionId) {
List<MartialResult> list = resultService.getConfirmedGeneralList(competitionId);
return R.data(list);
}
}

View File

@@ -1,5 +1,7 @@
package org.springblade.modules.martial.controller; package org.springblade.modules.martial.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -9,6 +11,7 @@ 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.core.tool.utils.StringUtil;
import org.springblade.modules.martial.pojo.entity.MartialProject; import org.springblade.modules.martial.pojo.entity.MartialProject;
import org.springblade.modules.martial.service.IMartialProjectService; import org.springblade.modules.martial.service.IMartialProjectService;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -42,7 +45,31 @@ public class MartialProjectController extends BladeController {
@GetMapping("/list") @GetMapping("/list")
@Operation(summary = "分页列表", description = "分页查询") @Operation(summary = "分页列表", description = "分页查询")
public R<IPage<MartialProject>> list(MartialProject project, Query query) { public R<IPage<MartialProject>> list(MartialProject project, Query query) {
IPage<MartialProject> pages = projectService.page(Condition.getPage(query), Condition.getQueryWrapper(project)); QueryWrapper<MartialProject> queryWrapper = new QueryWrapper<>();
// 赛事ID精确匹配
if (project.getCompetitionId() != null) {
queryWrapper.eq("competition_id", project.getCompetitionId());
}
// 项目名称模糊查询
if (StringUtil.isNotBlank(project.getProjectName())) {
queryWrapper.like("project_name", project.getProjectName());
}
// 分组类别模糊查询
if (StringUtil.isNotBlank(project.getCategory())) {
queryWrapper.like("category", project.getCategory());
}
// 项目类型精确匹配
if (project.getEventType() != null) {
queryWrapper.eq("event_type", project.getEventType());
}
// 参赛类型精确匹配
if (project.getType() != null) {
queryWrapper.eq("type", project.getType());
}
// 按排序字段和创建时间排序
queryWrapper.orderByAsc("sort_order").orderByDesc("create_time");
IPage<MartialProject> pages = projectService.page(Condition.getPage(query), queryWrapper);
return R.data(pages); return R.data(pages);
} }
@@ -52,9 +79,32 @@ public class MartialProjectController extends BladeController {
@PostMapping("/submit") @PostMapping("/submit")
@Operation(summary = "新增或修改", description = "传入实体") @Operation(summary = "新增或修改", description = "传入实体")
public R submit(@RequestBody MartialProject project) { public R submit(@RequestBody MartialProject project) {
// Auto-generate project code for new projects
if (project.getId() == null && StringUtil.isBlank(project.getProjectCode())) {
String projectCode = generateProjectCode(project.getCompetitionId());
project.setProjectCode(projectCode);
}
return R.status(projectService.saveOrUpdate(project)); return R.status(projectService.saveOrUpdate(project));
} }
/**
* Generate project code: competition prefix + sequence number
* Format: C{competitionId}-P{sequence}, e.g., C1-P001
*/
private String generateProjectCode(Long competitionId) {
if (competitionId == null) {
return "P" + System.currentTimeMillis();
}
// Count existing projects for this competition
LambdaQueryWrapper<MartialProject> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialProject::getCompetitionId, competitionId);
long count = projectService.count(wrapper);
// Generate code: C{competitionId}-P{sequence}
return String.format("C%d-P%03d", competitionId, count + 1);
}
/** /**
* 删除 * 删除
*/ */

View File

@@ -1,23 +1,40 @@
package org.springblade.modules.martial.controller; package org.springblade.modules.martial.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.boot.ctrl.BladeController; import org.springblade.core.boot.ctrl.BladeController;
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.secure.utils.AuthUtil;
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.entity.MartialAthlete;
import org.springblade.modules.martial.pojo.entity.MartialCompetition;
import org.springblade.modules.martial.pojo.entity.MartialProject;
import org.springblade.modules.martial.pojo.entity.MartialRegistrationOrder; import org.springblade.modules.martial.pojo.entity.MartialRegistrationOrder;
import org.springblade.modules.martial.pojo.entity.MartialTeam;
import org.springblade.modules.martial.pojo.entity.MartialTeamMember;
import org.springblade.modules.martial.pojo.dto.RegistrationSubmitDTO;
import org.springblade.modules.martial.pojo.vo.MartialRegistrationOrderVO;
import org.springblade.modules.martial.pojo.vo.OrganizationStatsVO;
import org.springblade.modules.martial.service.IMartialAthleteService;
import org.springblade.modules.martial.service.IMartialCompetitionService;
import org.springblade.modules.martial.service.IMartialProjectService;
import org.springblade.modules.martial.service.IMartialRegistrationOrderService; import org.springblade.modules.martial.service.IMartialRegistrationOrderService;
import org.springblade.modules.martial.service.IMartialTeamService;
import org.springblade.modules.martial.mapper.MartialTeamMemberMapper;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
/** import java.math.BigDecimal;
* 报名订单 控制器 import java.time.LocalDateTime;
* import java.util.*;
* @author BladeX import java.util.stream.Collectors;
*/
@Slf4j
@RestController @RestController
@AllArgsConstructor @AllArgsConstructor
@RequestMapping("/martial/registrationOrder") @RequestMapping("/martial/registrationOrder")
@@ -25,39 +42,372 @@ import org.springframework.web.bind.annotation.*;
public class MartialRegistrationOrderController extends BladeController { public class MartialRegistrationOrderController extends BladeController {
private final IMartialRegistrationOrderService registrationOrderService; private final IMartialRegistrationOrderService registrationOrderService;
private final IMartialAthleteService athleteService;
private final IMartialTeamService teamService;
private final IMartialCompetitionService competitionService;
private final IMartialProjectService projectService;
private final MartialTeamMemberMapper teamMemberMapper;
/**
* 详情
*/
@GetMapping("/detail") @GetMapping("/detail")
@Operation(summary = "详情", description = "传入ID") @Operation(summary = "详情", description = "传入ID")
public R<MartialRegistrationOrder> detail(@RequestParam Long id) { public R detail(@RequestParam Long id) {
MartialRegistrationOrder detail = registrationOrderService.getById(id); return R.data(registrationOrderService.getDetailWithRelations(id));
return R.data(detail);
} }
/**
* 分页列表
*/
@GetMapping("/list") @GetMapping("/list")
@Operation(summary = "分页列表", description = "分页查询") @Operation(summary = "分页列表", description = "分页查询当前用户的报名记录")
public R<IPage<MartialRegistrationOrder>> list(MartialRegistrationOrder registrationOrder, Query query) { public R<IPage<MartialRegistrationOrderVO>> list(MartialRegistrationOrder registrationOrder, Query query) {
IPage<MartialRegistrationOrder> pages = registrationOrderService.page(Condition.getPage(query), Condition.getQueryWrapper(registrationOrder)); Long userId = AuthUtil.getUserId();
Integer status = registrationOrder.getStatus();
IPage<MartialRegistrationOrderVO> pages = registrationOrderService.getListWithRelations(
userId, status, query.getCurrent(), query.getSize());
return R.data(pages); return R.data(pages);
} }
/** @GetMapping("/organization-stats")
* 新增或修改 @Operation(summary = "单位统计", description = "按单位统计运动员、项目、金额")
*/ public R<List<OrganizationStatsVO>> getOrganizationStats(@RequestParam Long competitionId) {
@PostMapping("/submit") log.info("获取单位统计: competitionId={}", competitionId);
@Operation(summary = "新增或修改", description = "传入实体")
public R submit(@RequestBody MartialRegistrationOrder registrationOrder) { // 1. Get all athletes for this competition
return R.status(registrationOrderService.saveOrUpdate(registrationOrder)); LambdaQueryWrapper<MartialAthlete> athleteWrapper = new LambdaQueryWrapper<>();
athleteWrapper.eq(MartialAthlete::getCompetitionId, competitionId)
.eq(MartialAthlete::getIsDeleted, 0);
List<MartialAthlete> athletes = athleteService.list(athleteWrapper);
if (athletes.isEmpty()) {
return R.data(new ArrayList<>());
}
// 2. Get all projects for this competition
Set<Long> projectIds = athletes.stream()
.map(MartialAthlete::getProjectId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
final Map<Long, MartialProject> projectMap = new HashMap<>();
if (!projectIds.isEmpty()) {
List<MartialProject> projects = projectService.listByIds(projectIds);
projectMap.putAll(projects.stream().collect(Collectors.toMap(MartialProject::getId, p -> p)));
}
// 3. Get team members for team projects
Set<Long> teamIds = athletes.stream()
.filter(a -> {
MartialProject project = projectMap.get(a.getProjectId());
return project != null && project.getType() != null && project.getType() == 2;
})
.map(a -> {
// Try to get team ID from team table by team name
LambdaQueryWrapper<MartialTeam> teamWrapper = new LambdaQueryWrapper<>();
teamWrapper.eq(MartialTeam::getTeamName, a.getTeamName())
.eq(MartialTeam::getIsDeleted, 0)
.last("LIMIT 1");
MartialTeam team = teamService.getOne(teamWrapper, false);
return team != null ? team.getId() : null;
})
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// Get team members
Map<Long, List<MartialTeamMember>> teamMembersMap = new HashMap<>();
if (!teamIds.isEmpty()) {
LambdaQueryWrapper<MartialTeamMember> memberWrapper = new LambdaQueryWrapper<>();
memberWrapper.in(MartialTeamMember::getTeamId, teamIds)
.eq(MartialTeamMember::getIsDeleted, 0);
List<MartialTeamMember> members = teamMemberMapper.selectList(memberWrapper);
teamMembersMap = members.stream().collect(Collectors.groupingBy(MartialTeamMember::getTeamId));
}
// 4. Group by organization and calculate stats
Map<String, OrganizationStatsVO> orgStatsMap = new LinkedHashMap<>();
for (MartialAthlete athlete : athletes) {
String org = athlete.getOrganization();
if (org == null || org.isEmpty()) {
org = "未知单位";
}
OrganizationStatsVO stats = orgStatsMap.computeIfAbsent(org, k -> {
OrganizationStatsVO vo = new OrganizationStatsVO();
vo.setOrganization(k);
vo.setAthleteCount(0);
vo.setProjectCount(0);
vo.setSingleProjectCount(0);
vo.setTeamProjectCount(0);
vo.setMaleCount(0);
vo.setFemaleCount(0);
vo.setTotalAmount(BigDecimal.ZERO);
vo.setProjectAmounts(new ArrayList<>());
return vo;
});
MartialProject project = projectMap.get(athlete.getProjectId());
if (project == null) continue;
// Check if project already counted for this org
boolean projectExists = stats.getProjectAmounts().stream()
.anyMatch(pa -> pa.getProjectId().equals(athlete.getProjectId()));
if (!projectExists) {
// Add project amount item
OrganizationStatsVO.ProjectAmountItem item = new OrganizationStatsVO.ProjectAmountItem();
item.setProjectId(project.getId());
item.setProjectName(project.getProjectName());
item.setProjectType(project.getType());
item.setCount(1);
item.setPrice(project.getPrice() != null ? project.getPrice() : BigDecimal.ZERO);
item.setAmount(item.getPrice());
stats.getProjectAmounts().add(item);
stats.setProjectCount(stats.getProjectCount() + 1);
if (project.getType() != null && project.getType() == 2) {
stats.setTeamProjectCount(stats.getTeamProjectCount() + 1);
} else {
stats.setSingleProjectCount(stats.getSingleProjectCount() + 1);
}
} else {
// Update count for existing project
stats.getProjectAmounts().stream()
.filter(pa -> pa.getProjectId().equals(athlete.getProjectId()))
.findFirst()
.ifPresent(pa -> {
pa.setCount(pa.getCount() + 1);
pa.setAmount(pa.getPrice().multiply(BigDecimal.valueOf(pa.getCount())));
});
}
}
// 5. Calculate unique athletes and gender counts per organization
for (Map.Entry<String, OrganizationStatsVO> entry : orgStatsMap.entrySet()) {
String org = entry.getKey();
OrganizationStatsVO stats = entry.getValue();
// Get all athletes for this org
Set<String> uniqueIdCards = new HashSet<>();
int maleCount = 0;
int femaleCount = 0;
for (MartialAthlete athlete : athletes) {
String athleteOrg = athlete.getOrganization();
if (athleteOrg == null || athleteOrg.isEmpty()) athleteOrg = "未知单位";
if (!athleteOrg.equals(org)) continue;
MartialProject project = projectMap.get(athlete.getProjectId());
if (project == null) continue;
// For individual projects, count the athlete
if (project.getType() == null || project.getType() == 1) {
String idCard = athlete.getIdCard();
if (idCard != null && !idCard.isEmpty() && !uniqueIdCards.contains(idCard)) {
uniqueIdCards.add(idCard);
if (athlete.getGender() != null && athlete.getGender() == 1) {
maleCount++;
} else if (athlete.getGender() != null && athlete.getGender() == 2) {
femaleCount++;
}
}
} else {
// For team projects, count team members
String teamName = athlete.getTeamName();
if (teamName != null) {
LambdaQueryWrapper<MartialTeam> teamWrapper = new LambdaQueryWrapper<>();
teamWrapper.eq(MartialTeam::getTeamName, teamName)
.eq(MartialTeam::getIsDeleted, 0)
.last("LIMIT 1");
MartialTeam team = teamService.getOne(teamWrapper, false);
if (team != null && teamMembersMap.containsKey(team.getId())) {
for (MartialTeamMember member : teamMembersMap.get(team.getId())) {
MartialAthlete memberAthlete = athleteService.getById(member.getAthleteId());
if (memberAthlete != null) {
String idCard = memberAthlete.getIdCard();
if (idCard != null && !idCard.isEmpty() && !uniqueIdCards.contains(idCard)) {
uniqueIdCards.add(idCard);
if (memberAthlete.getGender() != null && memberAthlete.getGender() == 1) {
maleCount++;
} else if (memberAthlete.getGender() != null && memberAthlete.getGender() == 2) {
femaleCount++;
}
}
}
}
}
}
}
}
stats.setAthleteCount(uniqueIdCards.size());
stats.setMaleCount(maleCount);
stats.setFemaleCount(femaleCount);
// Calculate total amount
BigDecimal totalAmount = stats.getProjectAmounts().stream()
.map(OrganizationStatsVO.ProjectAmountItem::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
stats.setTotalAmount(totalAmount);
}
return R.data(new ArrayList<>(orgStatsMap.values()));
}
@PostMapping("/submit")
@Operation(summary = "提交报名", description = "提交报名订单并关联选手或集体")
@Transactional(rollbackFor = Exception.class)
public R submit(@RequestBody RegistrationSubmitDTO dto) {
log.info("=== 提交报名订单 ===");
log.info("订单号: {}", dto.getOrderNo());
log.info("赛事ID: {}", dto.getCompetitionId());
log.info("项目IDs: {}", dto.getProjectIds());
log.info("选手IDs: {}", dto.getAthleteIds());
log.info("集体IDs: {}", dto.getTeamIds());
log.info("联系电话: {}", dto.getContactPhone());
log.info("总金额: {}", dto.getTotalAmount());
// Validate competition exists and check registration/competition time
MartialCompetition competition = competitionService.getById(dto.getCompetitionId());
if (competition == null) {
return R.fail("赛事不存在");
}
LocalDateTime now = LocalDateTime.now();
// Check if registration time is valid
if (competition.getRegistrationStartTime() != null && now.isBefore(competition.getRegistrationStartTime())) {
return R.fail("报名尚未开始");
}
if (competition.getRegistrationEndTime() != null && now.isAfter(competition.getRegistrationEndTime())) {
return R.fail("报名已结束");
}
// Check if competition has ended
if (competition.getCompetitionEndTime() != null && now.isAfter(competition.getCompetitionEndTime())) {
return R.fail("比赛已结束,无法报名");
}
// Create order entity
MartialRegistrationOrder order = new MartialRegistrationOrder();
order.setOrderNo(dto.getOrderNo());
order.setCompetitionId(dto.getCompetitionId());
order.setContactPhone(dto.getContactPhone());
order.setTotalAmount(dto.getTotalAmount());
order.setUserId(AuthUtil.getUserId());
order.setUserName(AuthUtil.getUserName());
// Parse IDs
List<Long> athleteIds = Func.toLongList(dto.getAthleteIds());
List<Long> teamIds = Func.toLongList(dto.getTeamIds());
List<Long> projectIds = Func.toLongList(dto.getProjectIds());
// Determine if this is a team registration
boolean isTeamRegistration = !teamIds.isEmpty();
if (isTeamRegistration) {
order.setTotalParticipants(teamIds.size());
} else {
order.setTotalParticipants(athleteIds.size());
}
// Save order
boolean saved = registrationOrderService.save(order);
if (!saved) {
return R.fail("创建订单失败");
}
Long orderId = order.getId();
log.info("订单创建成功订单ID: {}", orderId);
if (isTeamRegistration) {
// Handle team registration - create record for each team and each project
for (Long teamId : teamIds) {
MartialTeam team = teamService.getById(teamId);
if (team == null) {
log.warn("集体不存在: {}", teamId);
continue;
}
// Create a record for each project
for (Long projectId : projectIds) {
MartialAthlete teamAthlete = new MartialAthlete();
teamAthlete.setOrderId(orderId);
teamAthlete.setCompetitionId(dto.getCompetitionId());
teamAthlete.setProjectId(projectId);
teamAthlete.setTeamName(team.getTeamName());
teamAthlete.setPlayerName(team.getTeamName());
teamAthlete.setOrganization(team.getTeamName());
teamAthlete.setRegistrationStatus(1);
teamAthlete.setCompetitionStatus(0);
teamAthlete.setCreateUser(AuthUtil.getUserId());
teamAthlete.setCreateTime(new java.util.Date());
teamAthlete.setTenantId("000000");
teamAthlete.setIsDeleted(0);
teamAthlete.setStatus(1);
athleteService.save(teamAthlete);
log.info("创建集体参赛记录: teamName={}, projectId={}", team.getTeamName(), projectId);
}
}
} else {
// Handle individual registration - create record for each athlete and each project
for (Long athleteId : athleteIds) {
MartialAthlete existingAthlete = athleteService.getById(athleteId);
if (existingAthlete == null) {
log.warn("选手不存在: {}", athleteId);
continue;
}
// Create a record for each project
for (Long projectId : projectIds) {
// Check if record already exists for this athlete + competition + project
LambdaQueryWrapper<MartialAthlete> checkWrapper = new LambdaQueryWrapper<>();
checkWrapper.eq(MartialAthlete::getCompetitionId, dto.getCompetitionId())
.eq(MartialAthlete::getProjectId, projectId)
.eq(MartialAthlete::getIdCard, existingAthlete.getIdCard())
.eq(MartialAthlete::getIsDeleted, 0);
MartialAthlete existingRecord = athleteService.getOne(checkWrapper, false);
if (existingRecord != null) {
// Update existing record with order info
existingRecord.setOrderId(orderId);
existingRecord.setRegistrationStatus(1);
existingRecord.setUpdateUser(AuthUtil.getUserId());
existingRecord.setUpdateTime(new java.util.Date());
athleteService.updateById(existingRecord);
log.info("更新已存在的选手参赛记录: playerName={}, projectId={}", existingAthlete.getPlayerName(), projectId);
} else {
// Create new record
MartialAthlete newRecord = new MartialAthlete();
newRecord.setOrderId(orderId);
newRecord.setCompetitionId(dto.getCompetitionId());
newRecord.setProjectId(projectId);
newRecord.setPlayerName(existingAthlete.getPlayerName());
newRecord.setGender(existingAthlete.getGender());
newRecord.setIdCard(existingAthlete.getIdCard());
newRecord.setIdCardType(existingAthlete.getIdCardType());
newRecord.setBirthDate(existingAthlete.getBirthDate());
newRecord.setAge(existingAthlete.getAge());
newRecord.setContactPhone(existingAthlete.getContactPhone());
newRecord.setOrganization(existingAthlete.getOrganization());
newRecord.setTeamName(existingAthlete.getTeamName());
newRecord.setRegistrationStatus(1);
newRecord.setCompetitionStatus(0);
newRecord.setCreateUser(AuthUtil.getUserId());
newRecord.setCreateTime(new java.util.Date());
newRecord.setTenantId("000000");
newRecord.setIsDeleted(0);
newRecord.setStatus(1);
athleteService.save(newRecord);
log.info("创建选手参赛记录: playerName={}, projectId={}", existingAthlete.getPlayerName(), projectId);
}
}
}
}
return R.data(order);
} }
/**
* 删除
*/
@PostMapping("/remove") @PostMapping("/remove")
@Operation(summary = "删除", description = "传入ID") @Operation(summary = "删除", description = "传入ID")
public R remove(@RequestParam String ids) { public R remove(@RequestParam String ids) {

View File

@@ -8,6 +8,7 @@ import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.secure.BladeUser; import org.springblade.core.secure.BladeUser;
import org.springblade.core.secure.utils.AuthUtil; import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.R; import org.springblade.core.tool.api.R;
import org.springblade.modules.martial.config.ScheduleConfig;
import org.springblade.modules.martial.pojo.dto.MoveScheduleGroupDTO; import org.springblade.modules.martial.pojo.dto.MoveScheduleGroupDTO;
import org.springblade.modules.martial.pojo.dto.SaveScheduleDraftDTO; import org.springblade.modules.martial.pojo.dto.SaveScheduleDraftDTO;
import org.springblade.modules.martial.pojo.dto.ScheduleResultDTO; import org.springblade.modules.martial.pojo.dto.ScheduleResultDTO;
@@ -15,6 +16,7 @@ import org.springblade.modules.martial.service.IMartialScheduleArrangeService;
import org.springblade.modules.martial.service.IMartialScheduleService; import org.springblade.modules.martial.service.IMartialScheduleService;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
@@ -31,6 +33,24 @@ public class MartialScheduleArrangeController extends BladeController {
private final IMartialScheduleArrangeService scheduleArrangeService; private final IMartialScheduleArrangeService scheduleArrangeService;
private final IMartialScheduleService scheduleService; private final IMartialScheduleService scheduleService;
private final ScheduleConfig scheduleConfig;
/**
* 获取赛程配置
*/
@GetMapping("/config")
@Operation(summary = "获取赛程配置", description = "获取赛程编排的时间配置")
public R<Map<String, Object>> getScheduleConfig() {
Map<String, Object> config = new HashMap<>();
config.put("morningStartTime", scheduleConfig.getMorningStartTime());
config.put("morningEndTime", scheduleConfig.getMorningEndTime());
config.put("afternoonStartTime", scheduleConfig.getAfternoonStartTime());
config.put("afternoonEndTime", scheduleConfig.getAfternoonEndTime());
config.put("maxPeoplePerGroup", scheduleConfig.getMaxPeoplePerGroup());
config.put("targetPeoplePerGroup", scheduleConfig.getTargetPeoplePerGroup());
config.put("defaultDurationPerPerson", scheduleConfig.getDefaultDurationPerPerson());
return R.data(config);
}
/** /**
* 获取编排结果 * 获取编排结果
@@ -69,13 +89,11 @@ public class MartialScheduleArrangeController extends BladeController {
@Operation(summary = "完成编排并锁定", description = "传入赛事ID") @Operation(summary = "完成编排并锁定", description = "传入赛事ID")
public R saveAndLock(@RequestBody SaveScheduleDraftDTO dto) { public R saveAndLock(@RequestBody SaveScheduleDraftDTO dto) {
try { try {
// 获取当前登录用户
BladeUser user = AuthUtil.getUser(); BladeUser user = AuthUtil.getUser();
String userId = user != null ? user.getUserName() : "system"; String userId = user != null ? user.getUserName() : "system";
boolean success = scheduleService.saveAndLockSchedule(dto.getCompetitionId()); boolean success = scheduleService.saveAndLockSchedule(dto.getCompetitionId());
if (success) { if (success) {
// 调用原有的锁定逻辑
scheduleArrangeService.saveAndLock(dto.getCompetitionId(), userId); scheduleArrangeService.saveAndLock(dto.getCompetitionId(), userId);
return R.success("编排已完成并锁定"); return R.success("编排已完成并锁定");
} else { } else {
@@ -118,4 +136,70 @@ 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());
}
}
/**
* 更新参赛者签到状态
*/
@PostMapping("/update-check-in-status")
@Operation(summary = "更新签到状态", description = "更新参赛者签到状态:未签到/已签到/异常")
public R updateCheckInStatus(@RequestBody java.util.Map<String, Object> params) {
try {
Long participantId = Long.valueOf(String.valueOf(params.get("participantId")));
String status = String.valueOf(params.get("status"));
boolean success = scheduleService.updateParticipantCheckInStatus(participantId, status);
return success ? R.success("状态更新成功") : R.fail("状态更新失败");
} catch (Exception e) {
log.error("更新签到状态失败", e);
return R.fail("更新签到状态失败: " + e.getMessage());
}
}
} }

View File

@@ -10,6 +10,7 @@ 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.entity.MartialScore; import org.springblade.modules.martial.pojo.entity.MartialScore;
import org.springblade.modules.martial.pojo.vo.MartialScoreVO;
import org.springblade.modules.martial.service.IMartialScoreService; import org.springblade.modules.martial.service.IMartialScoreService;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -43,8 +44,8 @@ public class MartialScoreController extends BladeController {
*/ */
@GetMapping("/list") @GetMapping("/list")
@Operation(summary = "分页列表", description = "分页查询") @Operation(summary = "分页列表", description = "分页查询")
public R<IPage<MartialScore>> list(MartialScore score, Query query) { public R<IPage<MartialScoreVO>> list(MartialScore score, Query query) {
IPage<MartialScore> pages = scoreService.page(Condition.getPage(query), Condition.getQueryWrapper(score)); IPage<MartialScoreVO> pages = scoreService.selectScoreVOPage(Condition.getPage(query), score);
return R.data(pages); return R.data(pages);
} }

View File

@@ -0,0 +1,70 @@
package org.springblade.modules.martial.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.modules.martial.pojo.dto.TeamSubmitDTO;
import org.springblade.modules.martial.pojo.entity.MartialTeam;
import org.springblade.modules.martial.pojo.vo.MartialTeamVO;
import org.springblade.modules.martial.service.IMartialTeamService;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/martial/team")
@Tag(name = "集体管理", description = "集体/团队接口")
public class MartialTeamController extends BladeController {
private final IMartialTeamService teamService;
@GetMapping("/list")
@Operation(summary = "分页列表", description = "获取当前用户的集体列表")
public R<IPage<MartialTeamVO>> list(Query query) {
Long userId = AuthUtil.getUserId();
IPage<MartialTeamVO> pages = teamService.getTeamList(userId, query.getCurrent(), query.getSize());
return R.data(pages);
}
@GetMapping("/detail")
@Operation(summary = "详情", description = "获取集体详情")
public R<MartialTeamVO> detail(@RequestParam Long id) {
return R.data(teamService.getTeamDetail(id));
}
@PostMapping("/submit")
@Operation(summary = "保存", description = "新增或修改集体")
public R<Boolean> submit(@RequestBody TeamSubmitDTO dto) {
log.info("Team submit - teamId: {}, teamName: {}, memberIds: {}", dto.getTeamId(), dto.getTeamName(), dto.getMemberIds());
MartialTeam team = new MartialTeam();
team.setTeamName(dto.getTeamName());
team.setRemark(dto.getRemark());
boolean result;
if (StringUtil.isNotBlank(dto.getTeamId())) {
Long teamId = Long.parseLong(dto.getTeamId());
team.setId(teamId);
log.info("Updating team with id: {}", teamId);
result = teamService.updateTeamWithMembers(team, dto.getMemberIds());
} else {
log.info("Creating new team");
result = teamService.saveTeamWithMembers(team, dto.getMemberIds());
}
return R.data(result);
}
@PostMapping("/remove")
@Operation(summary = "删除", description = "删除集体")
public R<Boolean> remove(@RequestParam Long id) {
return R.data(teamService.removeTeamWithMembers(id));
}
}

View File

@@ -0,0 +1,47 @@
package org.springblade.modules.martial.excel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* Schedule export Excel - Template 2 (Competition Schedule Format)
* Format: Sequence, Project, Participants, Groups, Time, Table Number
*/
@Data
@ColumnWidth(12)
@HeadRowHeight(25)
@ContentRowHeight(20)
public class ScheduleExportExcel2 implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty("序号")
@ColumnWidth(8)
private Integer sequenceNo;
@ExcelProperty("项目")
@ColumnWidth(25)
private String projectName;
@ExcelProperty("人数")
@ColumnWidth(8)
private Integer participantCount;
@ExcelProperty("组数")
@ColumnWidth(8)
private Integer groupCount;
@ExcelProperty("时间")
@ColumnWidth(10)
private Integer durationMinutes;
@ExcelProperty("表号")
@ColumnWidth(10)
private String tableNo;
}

View File

@@ -22,4 +22,10 @@ public interface MartialAthleteMapper extends BaseMapper<MartialAthlete> {
*/ */
IPage<MartialAthleteVO> selectAthleteVOPage(IPage<MartialAthleteVO> page, @Param("athlete") MartialAthlete athlete); IPage<MartialAthleteVO> selectAthleteVOPage(IPage<MartialAthleteVO> page, @Param("athlete") MartialAthlete athlete);
/**
* Count distinct participants by id_card for a competition
*/
@org.apache.ibatis.annotations.Select("SELECT COUNT(DISTINCT id_card) FROM martial_athlete WHERE competition_id = #{competitionId} AND is_deleted = 0")
Long countDistinctParticipants(@Param("competitionId") Long competitionId);
} }

View File

@@ -12,6 +12,7 @@
LEFT JOIN martial_competition c ON a.competition_id = c.id AND c.is_deleted = 0 LEFT JOIN martial_competition c ON a.competition_id = c.id AND c.is_deleted = 0
LEFT JOIN martial_project p ON a.project_id = p.id AND p.is_deleted = 0 LEFT JOIN martial_project p ON a.project_id = p.id AND p.is_deleted = 0
WHERE a.is_deleted = 0 WHERE a.is_deleted = 0
AND (a.team_name IS NULL OR a.player_name != a.team_name)
<if test="athlete.competitionId != null"> <if test="athlete.competitionId != null">
AND a.competition_id = #{athlete.competitionId} AND a.competition_id = #{athlete.competitionId}
</if> </if>
@@ -39,6 +40,9 @@
<if test="athlete.competitionStatus != null"> <if test="athlete.competitionStatus != null">
AND a.competition_status = #{athlete.competitionStatus} AND a.competition_status = #{athlete.competitionStatus}
</if> </if>
<if test="athlete.createUser != null">
AND a.create_user = #{athlete.createUser}
</if>
ORDER BY a.create_time DESC ORDER BY a.create_time DESC
</select> </select>

View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the dreamlu.net developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: Chill 庄骞 (smallchill@163.com)
*/
package org.springblade.modules.martial.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionAttachment;
/**
* 赛事附件 Mapper 接口
*
* @author BladeX
*/
public interface MartialCompetitionAttachmentMapper extends BaseMapper<MartialCompetitionAttachment> {
}

View File

@@ -0,0 +1,13 @@
package org.springblade.modules.martial.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.modules.martial.pojo.entity.MartialContact;
/**
* Contact Mapper
*/
@Mapper
public interface MartialContactMapper extends BaseMapper<MartialContact> {
}

View File

@@ -26,12 +26,8 @@
<result column="contact_email" property="contactEmail"/> <result column="contact_email" property="contactEmail"/>
<result column="invite_message" property="inviteMessage"/> <result column="invite_message" property="inviteMessage"/>
<result column="cancel_reason" property="cancelReason"/> <result column="cancel_reason" property="cancelReason"/>
<!-- 关联的裁判信息 -->
<result column="judge_name" property="judgeName"/>
<result column="judge_level" property="judgeLevel"/>
<!-- 关联的赛事信息 -->
<result column="competition_name" property="competitionName"/> <result column="competition_name" property="competitionName"/>
<!-- 基础字段 --> <result column="venue_name" property="venueName"/>
<result column="create_user" property="createUser"/> <result column="create_user" property="createUser"/>
<result column="create_dept" property="createDept"/> <result column="create_dept" property="createDept"/>
<result column="create_time" property="createTime"/> <result column="create_time" property="createTime"/>
@@ -73,26 +69,25 @@
ji.update_time, ji.update_time,
ji.status, ji.status,
ji.is_deleted, ji.is_deleted,
ji.referee_type,
j.name AS judge_name, j.name AS judge_name,
j.level AS judge_level, j.level AS judge_level,
c.competition_name c.competition_name,
v.venue_name
FROM FROM
martial_judge_invite ji martial_judge_invite ji
LEFT JOIN martial_judge j ON ji.judge_id = j.id LEFT JOIN martial_judge j ON ji.judge_id = j.id
LEFT JOIN martial_competition c ON ji.competition_id = c.id LEFT JOIN martial_competition c ON ji.competition_id = c.id
WHERE LEFT JOIN martial_venue v ON ji.venue_id = v.id
ji.is_deleted = 0 WHERE ji.is_deleted = 0
<if test="judgeInvite.competitionId != null"> <if test="judgeInvite.competitionId != null">
AND ji.competition_id = #{judgeInvite.competitionId} AND ji.competition_id = #{judgeInvite.competitionId}
</if> </if>
<if test="judgeInvite.inviteStatus != null"> <if test="judgeInvite.inviteStatus != null">
AND ji.invite_status = #{judgeInvite.inviteStatus} AND ji.invite_status = #{judgeInvite.inviteStatus}
</if> </if>
<if test="judgeInvite.judgeName != null and judgeInvite.judgeName != ''"> <if test="judgeInvite.venueId != null">
AND j.name LIKE CONCAT('%', #{judgeInvite.judgeName}, '%') AND ji.venue_id = #{judgeInvite.venueId}
</if>
<if test="judgeInvite.judgeLevel != null and judgeInvite.judgeLevel != ''">
AND j.level = #{judgeInvite.judgeLevel}
</if> </if>
ORDER BY ji.create_time DESC ORDER BY ji.create_time DESC
</select> </select>

View File

@@ -17,11 +17,13 @@
d.venue_name AS venueName, d.venue_name AS venueName,
d.time_slot AS timeSlot, d.time_slot AS timeSlot,
d.time_slot_index AS timeSlotIndex, d.time_slot_index AS timeSlotIndex,
d.schedule_date AS scheduleDate,
p.id AS participantId, p.id AS participantId,
p.organization AS organization, p.organization AS organization,
p.check_in_status AS checkInStatus, p.check_in_status AS checkInStatus,
p.schedule_status AS scheduleStatus, p.schedule_status AS scheduleStatus,
p.performance_order AS performanceOrder p.performance_order AS performanceOrder,
p.player_name AS playerName
FROM FROM
martial_schedule_group g martial_schedule_group g
LEFT JOIN LEFT JOIN

View File

@@ -1,7 +1,10 @@
package org.springblade.modules.martial.mapper; package org.springblade.modules.martial.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Param;
import org.springblade.modules.martial.pojo.entity.MartialScore; import org.springblade.modules.martial.pojo.entity.MartialScore;
import org.springblade.modules.martial.pojo.vo.MartialScoreVO;
/** /**
* Score Mapper 接口 * Score Mapper 接口
@@ -10,4 +13,6 @@ import org.springblade.modules.martial.pojo.entity.MartialScore;
*/ */
public interface MartialScoreMapper extends BaseMapper<MartialScore> { public interface MartialScoreMapper extends BaseMapper<MartialScore> {
IPage<MartialScoreVO> selectScoreVOPage(IPage<MartialScoreVO> page, @Param("score") MartialScore score);
} }

View File

@@ -2,4 +2,43 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.modules.martial.mapper.MartialScoreMapper"> <mapper namespace="org.springblade.modules.martial.mapper.MartialScoreMapper">
<select id="selectScoreVOPage" resultType="org.springblade.modules.martial.pojo.vo.MartialScoreVO">
SELECT
s.*,
a.player_name as playerName,
a.team_name as teamName,
a.id_card as idCard,
a.player_no as playerNo,
p.project_name as projectName,
v.venue_name as venueName,
r.chief_judge_score as chiefJudgeScore,
r.score_status as scoreStatus,
(SELECT GROUP_CONCAT(d.item_name SEPARATOR ', ')
FROM martial_deduction_item d
WHERE FIND_IN_SET(d.id, REPLACE(REPLACE(s.deduction_items, '[', ''), ']', ''))
) as deductionItemsText
FROM martial_score s
LEFT JOIN martial_athlete a ON s.athlete_id = a.id AND a.is_deleted = 0
LEFT JOIN martial_project p ON s.project_id = p.id AND p.is_deleted = 0
LEFT JOIN martial_venue v ON s.venue_id = v.id AND v.is_deleted = 0
LEFT JOIN martial_result r ON s.athlete_id = r.athlete_id AND s.project_id = r.project_id AND r.is_deleted = 0
WHERE s.is_deleted = 0
<if test="score.competitionId != null">
AND s.competition_id = #{score.competitionId}
</if>
<if test="score.athleteId != null">
AND s.athlete_id = #{score.athleteId}
</if>
<if test="score.projectId != null">
AND s.project_id = #{score.projectId}
</if>
<if test="score.judgeId != null">
AND s.judge_id = #{score.judgeId}
</if>
<if test="score.venueId != null">
AND s.venue_id = #{score.venueId}
</if>
ORDER BY s.create_time DESC
</select>
</mapper> </mapper>

View File

@@ -0,0 +1,7 @@
package org.springblade.modules.martial.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springblade.modules.martial.pojo.entity.MartialTeam;
public interface MartialTeamMapper extends BaseMapper<MartialTeam> {
}

View File

@@ -0,0 +1,7 @@
package org.springblade.modules.martial.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springblade.modules.martial.pojo.entity.MartialTeamMember;
public interface MartialTeamMemberMapper extends BaseMapper<MartialTeamMember> {
}

Some files were not shown because too many files have changed in this diff Show More