Compare commits

..

57 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
116 changed files with 3957 additions and 617 deletions

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}"]

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

@@ -6254,6 +6254,7 @@ CREATE TABLE `martial_project` (
`project_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '项目名称', `project_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '项目名称',
`project_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '项目编码', `project_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '项目编码',
`category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '组别(男子组/女子组)', `category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '组别(男子组/女子组)',
`event_type` int(0) NULL DEFAULT NULL COMMENT '项目类型: 1-套路 2-散打 3-器械 4-对练',
`type` int(0) NULL DEFAULT 1 COMMENT '类型(1-个人,2-双人,3-集体)', `type` int(0) NULL DEFAULT 1 COMMENT '类型(1-个人,2-双人,3-集体)',
`min_participants` int(0) NULL DEFAULT 1 COMMENT '最少参赛人数', `min_participants` int(0) NULL DEFAULT 1 COMMENT '最少参赛人数',
`max_participants` int(0) NULL DEFAULT 1 COMMENT '最多参赛人数', `max_participants` int(0) NULL DEFAULT 1 COMMENT '最多参赛人数',
@@ -6262,6 +6263,8 @@ CREATE TABLE `martial_project` (
`gender_limit` int(0) NULL DEFAULT 0 COMMENT '性别限制(0-不限,1-仅男,2-仅女)', `gender_limit` int(0) NULL DEFAULT 0 COMMENT '性别限制(0-不限,1-仅男,2-仅女)',
`estimated_duration` int(0) NULL DEFAULT 5 COMMENT '预估时长(分钟)', `estimated_duration` int(0) NULL DEFAULT 5 COMMENT '预估时长(分钟)',
`price` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '报名费用', `price` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '报名费用',
`registration_start_time` datetime(0) NULL DEFAULT NULL COMMENT '报名开始时间',
`registration_end_time` datetime(0) NULL DEFAULT NULL COMMENT '报名结束时间',
`difficulty_coefficient` decimal(5, 2) NULL DEFAULT 1.00 COMMENT '难度系数', `difficulty_coefficient` decimal(5, 2) NULL DEFAULT 1.00 COMMENT '难度系数',
`registration_deadline` datetime(0) NULL DEFAULT NULL COMMENT '报名截止时间', `registration_deadline` datetime(0) NULL DEFAULT NULL COMMENT '报名截止时间',
`description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '项目描述', `description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '项目描述',

View File

@@ -1,5 +1,3 @@
version: '3.8'
services: services:
# MySQL 数据库 # MySQL 数据库
mysql: mysql:
@@ -42,21 +40,61 @@ services:
networks: networks:
- martial-network - 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: martial-api:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile.quick
container_name: martial-api container_name: martial-api
restart: always restart: always
environment: environment:
SPRING_PROFILE: dev SPRING_PROFILE: dev
JAVA_OPTS: "-Xms512m -Xmx1024m -XX:+UseG1GC" 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_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_USERNAME: root
SPRING_DATASOURCE_PASSWORD: 123456 SPRING_DATASOURCE_PASSWORD: 123456
# 覆盖 Redis 连接配置
SPRING_DATA_REDIS_HOST: redis SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379 SPRING_DATA_REDIS_PORT: 6379
SPRING_DATA_REDIS_PASSWORD: 123456 SPRING_DATA_REDIS_PASSWORD: 123456

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)

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

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

@@ -5,6 +5,7 @@ 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;
@@ -22,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 = "参赛选手接口")
@@ -55,8 +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) {
athlete.setCreateUser(AuthUtil.getUserId()); Long userId = AuthUtil.getUserId();
athlete.setUpdateUser(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

@@ -15,6 +15,7 @@ import org.springblade.core.tool.utils.Func;
import org.springblade.modules.martial.pojo.entity.MartialAthlete; 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.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.springblade.modules.system.pojo.entity.User;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -42,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);
} }
@@ -54,11 +60,9 @@ public class MartialCompetitionController extends BladeController {
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(); List<MartialCompetition> pagelist = pages.getRecords();
for (MartialCompetition martialCompetition : pagelist) { for (MartialCompetition martialCompetition : pagelist) {
Long cnt = martialAthleteService.count(Wrappers.<MartialAthlete>query().lambda() // Count distinct participants by id_card
.eq(MartialAthlete::getCompetitionId, martialCompetition.getId()) Long cnt = ((MartialAthleteMapper) martialAthleteService.getBaseMapper()).countDistinctParticipants(martialCompetition.getId());
.eq(MartialAthlete::getIsDeleted, 0) martialCompetition.setTotalParticipants(cnt != null ? cnt.intValue() : 0);
);
martialCompetition.setTotalParticipants(cnt.intValue());
} }
return R.data(pages); return R.data(pages);
} }
@@ -68,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

@@ -52,7 +52,10 @@ public class MartialDeductionItemController extends BladeController {
List<MartialDeductionItem> deductionItems = pages.getRecords(); List<MartialDeductionItem> deductionItems = pages.getRecords();
for (MartialDeductionItem item : deductionItems) { for (MartialDeductionItem item : deductionItems) {
MartialProject project = martialProjectService.getById(item.getProjectId()); MartialProject project = martialProjectService.getById(item.getProjectId());
if (project != null) {
item.setProjectName(project.getProjectName()); item.setProjectName(project.getProjectName());
item.setCompetitionId(project.getCompetitionId());
}
} }
return R.data(pages); return R.data(pages);
} }

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

@@ -224,4 +224,24 @@ public class MartialJudgeInviteController extends BladeController {
// 使用EasyExcel或POI导出 // 使用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

@@ -19,11 +19,19 @@ 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 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.MtVenue;
import org.springblade.modules.martial.pojo.entity.MartialVenue; 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.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;
@@ -31,6 +39,8 @@ import java.math.RoundingMode;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.Duration; 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.Objects;
import java.util.UUID; import java.util.UUID;
@@ -56,6 +66,9 @@ public class MartialMiniController extends BladeController {
private final IMartialAthleteService athleteService; private final IMartialAthleteService athleteService;
private final IMartialScoreService scoreService; private final IMartialScoreService scoreService;
private final BladeRedis bladeRedis; private final BladeRedis bladeRedis;
private final IMartialResultService resultService;
private final MartialScheduleStatusMapper scheduleStatusMapper;
private final MartialScheduleGroupMapper scheduleGroupMapper;
// Redis缓存key前缀 // Redis缓存key前缀
private static final String MINI_LOGIN_CACHE_PREFIX = "mini:login:"; private static final String MINI_LOGIN_CACHE_PREFIX = "mini:login:";
@@ -110,11 +123,35 @@ public class MartialMiniController extends BladeController {
martialVenue = venueService.getById(invite.getVenueId()); martialVenue = venueService.getById(invite.getVenueId());
} }
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保持为空列表
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 ?
@@ -192,18 +229,18 @@ public class MartialMiniController extends BladeController {
*/ */
private void updateAthleteTotalScore(Long athleteId, Long projectId, Long venueId) { private void updateAthleteTotalScore(Long athleteId, Long projectId, Long venueId) {
try { try {
// 1. 查询该场地的普通裁判数量 // 1. 查询该场地的裁判数量
int requiredJudgeCount = getRequiredJudgeCount(projectId); int requiredJudgeCount = getRequiredJudgeCount(venueId);
// 2. 获取裁判ID列表 // 2. 获取裁判ID列表
List<Long> chiefJudgeIds = getChiefJudgeIds(venueId); List<Long> chiefJudgeIds = getChiefJudgeIds(venueId);
// 3. 查询该选手在该项目的所有评分(排除裁判的评分) // 3. 查询该选手在该项目的所有评分(排除裁判的评分)
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getAthleteId, athleteId); scoreQuery.eq(MartialScore::getAthleteId, athleteId);
scoreQuery.eq(MartialScore::getProjectId, projectId); scoreQuery.eq(MartialScore::getProjectId, projectId);
scoreQuery.eq(MartialScore::getIsDeleted, 0); scoreQuery.eq(MartialScore::getIsDeleted, 0);
// 排除裁判的所有评分(包括普通评分和修改记录) // 排除裁判的所有评分(包括普通评分和修改记录)
if (!chiefJudgeIds.isEmpty()) { if (!chiefJudgeIds.isEmpty()) {
scoreQuery.notIn(MartialScore::getJudgeId, chiefJudgeIds); scoreQuery.notIn(MartialScore::getJudgeId, chiefJudgeIds);
} }
@@ -243,20 +280,19 @@ public class MartialMiniController extends BladeController {
} }
/** /**
* 获取项目应评分的裁判数量(普通裁判,不包括裁判 * 获取项目应评分的裁判数量(裁判,不包括裁判)
* 按项目过滤:检查 projects JSON 字段是否包含该项目ID * 按项目过滤:检查 projects JSON 字段是否包含该项目ID
*/ */
private int getRequiredJudgeCount(Long projectId) { private int getRequiredJudgeCount(Long venueId) {
if (projectId == null) { if (venueId == null || venueId <= 0) {
return 0; return 0;
} }
LambdaQueryWrapper<MartialJudgeInvite> judgeQuery = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialJudgeInvite> judgeQuery = new LambdaQueryWrapper<>();
judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0); judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
judgeQuery.ne(MartialJudgeInvite::getRole, "chief_judge"); // 排除裁判长 judgeQuery.eq(MartialJudgeInvite::getVenueId, venueId);
// 按项目过滤projects字段包含该项目ID judgeQuery.eq(MartialJudgeInvite::getRefereeType, 2); // Only count referees (type=2), exclude chief judge (type=1) and general judge (type=3)
judgeQuery.like(MartialJudgeInvite::getProjects, projectId.toString());
List<MartialJudgeInvite> judges = judgeInviteService.list(judgeQuery); List<MartialJudgeInvite> judges = judgeInviteService.list(judgeQuery);
// 使用 distinct judge_id 来计算不重复的裁判数量 // Use distinct judge_id to count unique judges
return (int) judges.stream() return (int) judges.stream()
.map(MartialJudgeInvite::getJudgeId) .map(MartialJudgeInvite::getJudgeId)
.filter(Objects::nonNull) .filter(Objects::nonNull)
@@ -318,8 +354,8 @@ public class MartialMiniController extends BladeController {
/** /**
* 获取选手列表(支持分页) * 获取选手列表(支持分页)
* - 普通裁判:获取所有选手,标记是否已评分 * - 裁判:获取所有选手,标记是否已评分
* - 裁判:获取所有普通裁判都评分完成的选手列表 * - 裁判:获取所有裁判都评分完成的选手列表
*/ */
@GetMapping("/score/athletes") @GetMapping("/score/athletes")
@Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)") @Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)")
@@ -328,6 +364,7 @@ public class MartialMiniController extends BladeController {
@RequestParam Integer refereeType, @RequestParam Integer refereeType,
@RequestParam(required = false) Long projectId, @RequestParam(required = false) Long projectId,
@RequestParam(required = false) Long venueId, @RequestParam(required = false) Long venueId,
@RequestParam(required = false) Long competitionId,
@RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size @RequestParam(defaultValue = "10") Integer size
) { ) {
@@ -335,6 +372,11 @@ public class MartialMiniController extends BladeController {
LambdaQueryWrapper<MartialAthlete> athleteQuery = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialAthlete> athleteQuery = new LambdaQueryWrapper<>();
athleteQuery.eq(MartialAthlete::getIsDeleted, 0); athleteQuery.eq(MartialAthlete::getIsDeleted, 0);
// 按比赛ID过滤重要确保只显示当前比赛的选手
if (competitionId != null) {
athleteQuery.eq(MartialAthlete::getCompetitionId, competitionId);
}
if (projectId != null) { if (projectId != null) {
athleteQuery.eq(MartialAthlete::getProjectId, projectId); athleteQuery.eq(MartialAthlete::getProjectId, projectId);
} }
@@ -343,10 +385,10 @@ public class MartialMiniController extends BladeController {
List<MartialAthlete> athletes = athleteService.list(athleteQuery); List<MartialAthlete> athletes = athleteService.list(athleteQuery);
// 2. 获取该场地所有裁判的judge_id列表 // 2. 获取该场地所有裁判的judge_id列表
List<Long> chiefJudgeIds = getChiefJudgeIds(venueId); List<Long> chiefJudgeIds = getChiefJudgeIds(venueId);
// 3. 获取所有评分记录(排除裁判的评分) // 3. 获取所有评分记录(排除裁判的评分)
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getIsDeleted, 0); scoreQuery.eq(MartialScore::getIsDeleted, 0);
if (projectId != null) { if (projectId != null) {
@@ -356,7 +398,7 @@ public class MartialMiniController extends BladeController {
if (venueId != null && venueId > 0) { if (venueId != null && venueId > 0) {
scoreQuery.eq(MartialScore::getVenueId, venueId); scoreQuery.eq(MartialScore::getVenueId, venueId);
} }
// 排除裁判的评分 // 排除裁判的评分
if (!chiefJudgeIds.isEmpty()) { if (!chiefJudgeIds.isEmpty()) {
scoreQuery.notIn(MartialScore::getJudgeId, chiefJudgeIds); scoreQuery.notIn(MartialScore::getJudgeId, chiefJudgeIds);
} }
@@ -367,18 +409,18 @@ public class MartialMiniController extends BladeController {
.collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId)); .collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId));
// 4. 获取该场地的应评裁判数量 // 4. 获取该场地的应评裁判数量
int requiredJudgeCount = getRequiredJudgeCount(projectId); int requiredJudgeCount = getRequiredJudgeCount(venueId);
// 5. 根据裁判类型处理选手列表 // 5. 根据裁判类型处理选手列表
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList; List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList;
if (refereeType == 1) { if (refereeType == 1) {
// 裁判返回所有选手前端根据totalScore判断是否显示修改按钮 // 裁判返回所有选手前端根据totalScore判断是否显示修改按钮
filteredList = athletes.stream() filteredList = athletes.stream()
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount)) .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount))
.collect(java.util.stream.Collectors.toList()); .collect(java.util.stream.Collectors.toList());
} else { } else {
// 普通裁判:返回所有选手,标记是否已评分 // 裁判:返回所有选手,标记是否已评分
filteredList = athletes.stream() filteredList = athletes.stream()
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount)) .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount))
.collect(java.util.stream.Collectors.toList()); .collect(java.util.stream.Collectors.toList());
@@ -404,7 +446,7 @@ public class MartialMiniController extends BladeController {
} }
/** /**
* 获取场地所有裁判的judge_id列表 * 获取场地所有裁判的judge_id列表
*/ */
private List<Long> getChiefJudgeIds(Long venueId) { private List<Long> getChiefJudgeIds(Long venueId) {
if (venueId == null) { if (venueId == null) {
@@ -432,10 +474,10 @@ public class MartialMiniController extends BladeController {
} }
/** /**
* 修改评分(裁判 * 修改评分(裁判)
*/ */
@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("修改失败");
@@ -493,11 +535,35 @@ public class MartialMiniController extends BladeController {
MartialCompetition competition = competitionService.getById(invite.getCompetitionId()); MartialCompetition competition = competitionService.getById(invite.getCompetitionId());
MartialJudge judge = judgeService.getById(invite.getJudgeId()); MartialJudge judge = judgeService.getById(invite.getJudgeId());
MartialVenue martialVenue = invite.getVenueId() != null ? venueService.getById(invite.getVenueId()) : null; MartialVenue martialVenue = invite.getVenueId() != null ? venueService.getById(invite.getVenueId()) : null;
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保持为空列表
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 != null ? competition.getId() : null); vo.setMatchId(competition != null ? competition.getId() : null);
vo.setMatchName(competition != null ? competition.getCompetitionName() : null); vo.setMatchName(competition != null ? competition.getCompetitionName() : null);
vo.setMatchTime(competition != null && competition.getCompetitionStartTime() != null ? vo.setMatchTime(competition != null && competition.getCompetitionStartTime() != null ?
@@ -634,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

@@ -21,12 +21,21 @@ 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 com.alibaba.fastjson.JSON; 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.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.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;
@@ -45,9 +54,17 @@ 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;
// Redis缓存key前缀
private static final String MINI_LOGIN_CACHE_PREFIX = "mini:login:";
// 登录缓存过期时间7天
private static final Duration LOGIN_CACHE_EXPIRE = Duration.ofDays(7);
/** /**
* 登录验证 * 登录验证
@@ -91,26 +108,55 @@ public class MartialMiniController extends BladeController {
invite.setDeviceInfo(dto.getDeviceInfo()); invite.setDeviceInfo(dto.getDeviceInfo());
judgeInviteService.updateById(invite); judgeInviteService.updateById(invite);
MartialVenue venue = null; // 从 martial_venue 表获取场地信息
MartialVenue martialVenue = null;
if (invite.getVenueId() != null) { if (invite.getVenueId() != null) {
venue = venueService.getById(invite.getVenueId()); martialVenue = venueService.getById(invite.getVenueId());
} }
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保持为空列表
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);
} }
@@ -152,9 +198,137 @@ public class MartialMiniController extends BladeController {
} }
boolean success = scoreService.save(score); 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("评分提交失败"); 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 * 安全地将String转换为Long
*/ */
@@ -171,8 +345,8 @@ public class MartialMiniController extends BladeController {
/** /**
* 获取选手列表(支持分页) * 获取选手列表(支持分页)
* - 普通裁判:获取所有选手,标记是否已评分 * - 裁判:获取所有选手,标记是否已评分
* - 裁判:获取已有评分的选手列表 * - 裁判:获取所有裁判员都评分完成的选手列表
*/ */
@GetMapping("/score/athletes") @GetMapping("/score/athletes")
@Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)") @Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)")
@@ -181,6 +355,7 @@ public class MartialMiniController extends BladeController {
@RequestParam Integer refereeType, @RequestParam Integer refereeType,
@RequestParam(required = false) Long projectId, @RequestParam(required = false) Long projectId,
@RequestParam(required = false) Long venueId, @RequestParam(required = false) Long venueId,
@RequestParam(required = false) Long competitionId,
@RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size @RequestParam(defaultValue = "10") Integer size
) { ) {
@@ -188,6 +363,11 @@ public class MartialMiniController extends BladeController {
LambdaQueryWrapper<MartialAthlete> athleteQuery = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialAthlete> athleteQuery = new LambdaQueryWrapper<>();
athleteQuery.eq(MartialAthlete::getIsDeleted, 0); athleteQuery.eq(MartialAthlete::getIsDeleted, 0);
// 按比赛ID过滤重要确保只显示当前比赛的选手
if (competitionId != null) {
athleteQuery.eq(MartialAthlete::getCompetitionId, competitionId);
}
if (projectId != null) { if (projectId != null) {
athleteQuery.eq(MartialAthlete::getProjectId, projectId); athleteQuery.eq(MartialAthlete::getProjectId, projectId);
} }
@@ -196,35 +376,48 @@ public class MartialMiniController extends BladeController {
List<MartialAthlete> athletes = athleteService.list(athleteQuery); List<MartialAthlete> athletes = athleteService.list(athleteQuery);
// 2. 获取所有评分记录 // 2. 获取该场地所有主裁判的judge_id列表
List<Long> chiefJudgeIds = getChiefJudgeIds(venueId);
// 3. 获取所有评分记录(排除主裁判的评分)
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getIsDeleted, 0); 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); List<MartialScore> allScores = scoreService.list(scoreQuery);
// 按选手ID分组统计评分 // 按选手ID分组统计评分
java.util.Map<Long, List<MartialScore>> scoresByAthlete = allScores.stream() java.util.Map<Long, List<MartialScore>> scoresByAthlete = allScores.stream()
.collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId)); .collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId));
// 3. 根据裁判类型处理选手列表 // 4. 获取该场地的应评裁判数量
int requiredJudgeCount = getRequiredJudgeCount(venueId);
// 5. 根据裁判类型处理选手列表
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList; List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList;
if (refereeType == 1) { if (refereeType == 1) {
// 裁判:返回已有评分的选手 // 裁判:返回所有选手前端根据totalScore判断是否显示修改按钮
filteredList = athletes.stream() filteredList = athletes.stream()
.filter(athlete -> { .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount))
List<MartialScore> scores = scoresByAthlete.get(athlete.getId());
return scores != null && !scores.isEmpty();
})
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId))
.collect(java.util.stream.Collectors.toList()); .collect(java.util.stream.Collectors.toList());
} else { } else {
// 普通裁判:返回所有选手,标记是否已评分 // 裁判:返回所有选手,标记是否已评分
filteredList = athletes.stream() filteredList = athletes.stream()
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId)) .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount))
.collect(java.util.stream.Collectors.toList()); .collect(java.util.stream.Collectors.toList());
} }
// 4. 手动分页 // 6. 手动分页
int total = filteredList.size(); int total = filteredList.size();
int fromIndex = (current - 1) * size; int fromIndex = (current - 1) * size;
int toIndex = Math.min(fromIndex + size, total); int toIndex = Math.min(fromIndex + size, total);
@@ -236,13 +429,31 @@ public class MartialMiniController extends BladeController {
pageRecords = filteredList.subList(fromIndex, toIndex); pageRecords = filteredList.subList(fromIndex, toIndex);
} }
// 5. 构建分页结果 // 7. 构建分页结果
IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> page = new Page<>(current, size, total); IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> page = new Page<>(current, size, total);
page.setRecords(pageRecords); page.setRecords(pageRecords);
return R.data(page); 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());
}
/** /**
* 获取评分详情 * 获取评分详情
*/ */
@@ -254,10 +465,10 @@ public class MartialMiniController extends BladeController {
} }
/** /**
* 修改评分(裁判 * 修改评分(裁判)
*/ */
@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("修改失败");
@@ -268,26 +479,107 @@ public class MartialMiniController extends BladeController {
*/ */
@PostMapping("/logout") @PostMapping("/logout")
@Operation(summary = "退出登录", description = "清除登录状态") @Operation(summary = "退出登录", description = "清除登录状态")
public R logout() { 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("退出成功"); return R.success("退出成功");
} }
/** /**
* Token验证 * Token验证从Redis恢复登录状态
*/ */
@GetMapping("/verify") @GetMapping("/verify")
@Operation(summary = "Token验证", description = "验证当前token是否有效") @Operation(summary = "Token验证", description = "验证token并返回登录信息,支持服务重启后恢复登录状态")
public R verify() { public R<MiniLoginVO> verify(@RequestHeader(value = "Authorization", required = false) String token) {
return R.success("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 * 转换选手实体为VO
* 新增:只有评分完成时才显示总分
*/ */
private org.springblade.modules.martial.pojo.vo.MiniAthleteListVO convertToAthleteListVO( private org.springblade.modules.martial.pojo.vo.MiniAthleteListVO convertToAthleteListVO(
MartialAthlete athlete, MartialAthlete athlete,
List<MartialScore> scores, List<MartialScore> scores,
Long currentJudgeId) { Long currentJudgeId,
int requiredJudgeCount) {
org.springblade.modules.martial.pojo.vo.MiniAthleteListVO vo = new org.springblade.modules.martial.pojo.vo.MiniAthleteListVO(); org.springblade.modules.martial.pojo.vo.MiniAthleteListVO vo = new org.springblade.modules.martial.pojo.vo.MiniAthleteListVO();
vo.setAthleteId(athlete.getId()); vo.setAthleteId(athlete.getId());
vo.setName(athlete.getPlayerName()); vo.setName(athlete.getPlayerName());
@@ -296,7 +588,9 @@ public class MartialMiniController extends BladeController {
vo.setTeam(athlete.getTeamName()); vo.setTeam(athlete.getTeamName());
vo.setOrderNum(athlete.getOrderNum()); vo.setOrderNum(athlete.getOrderNum());
vo.setCompetitionStatus(athlete.getCompetitionStatus()); vo.setCompetitionStatus(athlete.getCompetitionStatus());
vo.setTotalScore(athlete.getTotalScore());
// 设置应评分裁判数量
vo.setRequiredJudgeCount(requiredJudgeCount);
// 设置项目名称 // 设置项目名称
if (athlete.getProjectId() != null) { if (athlete.getProjectId() != null) {
@@ -307,8 +601,10 @@ public class MartialMiniController extends BladeController {
} }
// 设置评分状态 // 设置评分状态
int scoredCount = 0;
if (scores != null && !scores.isEmpty()) { if (scores != null && !scores.isEmpty()) {
vo.setScoredJudgeCount(scores.size()); scoredCount = scores.size();
vo.setScoredJudgeCount(scoredCount);
// 查找当前裁判的评分 // 查找当前裁判的评分
MartialScore myScore = scores.stream() MartialScore myScore = scores.stream()
@@ -327,6 +623,23 @@ public class MartialMiniController extends BladeController {
vo.setScoredJudgeCount(0); 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; return vo;
} }
@@ -378,4 +691,130 @@ 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);
}
} }

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 detail(@RequestParam Long id) { public R detail(@RequestParam Long id) {
// 返回包含关联数据的完整详情
return R.data(registrationOrderService.getDetailWithRelations(id)); return R.data(registrationOrderService.getDetailWithRelations(id));
} }
/**
* 分页列表
*/
@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 {
@@ -167,4 +185,21 @@ public class MartialScheduleArrangeController extends BladeController {
} }
} }
/**
* 更新参赛者签到状态
*/
@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>

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

@@ -27,6 +27,7 @@
<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="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"/>
@@ -71,17 +72,22 @@
ji.referee_type, 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
LEFT JOIN martial_venue v ON ji.venue_id = v.id
WHERE 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 test="judgeInvite.venueId != null">
AND ji.venue_id = #{judgeInvite.venueId}
</if> </if>
ORDER BY ji.create_time DESC ORDER BY ji.create_time DESC
</select> </select>

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> {
}

View File

@@ -20,9 +20,15 @@ public class BatchGenerateInviteDTO {
@Schema(description = "评委ID列表", required = true) @Schema(description = "评委ID列表", required = true)
private List<Long> judgeIds; private List<Long> judgeIds;
@Schema(description = "角色judge-普通评委chief_judge-裁判") @Schema(description = "角色judge-普通评委chief_judge-裁判")
private String role = "judge"; private String role = "judge";
@Schema(description = "过期天数默认30天") @Schema(description = "过期天数默认30天")
private Integer expireDays = 30; private Integer expireDays = 30;
@Schema(description = "场地ID")
private Long venueId;
@Schema(description = "项目ID列表JSON字符串")
private String projects;
} }

View File

@@ -0,0 +1,26 @@
package org.springblade.modules.martial.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
* 主裁判确认评分DTO
*/
@Data
@Schema(description = "主裁判确认评分DTO")
public class ChiefJudgeConfirmDTO {
@Schema(description = "成绩ID")
private String resultId;
@Schema(description = "主裁判ID")
private String chiefJudgeId;
@Schema(description = "确认/修改后的分数null表示直接确认原分数")
private BigDecimal score;
@Schema(description = "备注")
private String note;
}

View File

@@ -23,6 +23,12 @@ public class CompetitionGroupDTO implements Serializable {
@Schema(description = "分组ID") @Schema(description = "分组ID")
private Long id; private Long id;
/**
* 项目ID
*/
@Schema(description = "项目ID")
private Long projectId;
/** /**
* 分组标题 * 分组标题
*/ */

View File

@@ -0,0 +1,26 @@
package org.springblade.modules.martial.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
* 总裁确认评分DTO
*/
@Data
@Schema(description = "总裁确认评分DTO")
public class GeneralJudgeConfirmDTO {
@Schema(description = "成绩ID")
private String resultId;
@Schema(description = "总裁ID")
private String generalJudgeId;
@Schema(description = "确认/修改后的分数null表示直接确认原分数")
private BigDecimal score;
@Schema(description = "备注")
private String note;
}

View File

@@ -19,7 +19,7 @@ public class GenerateInviteDTO {
@Schema(description = "评委ID", required = true) @Schema(description = "评委ID", required = true)
private Long judgeId; private Long judgeId;
@Schema(description = "角色judge-普通评委chief_judge-裁判", required = true) @Schema(description = "角色judge-普通评委chief_judge-裁判", required = true)
private String role; private String role;
@Schema(description = "分配场地ID普通评委必填") @Schema(description = "分配场地ID普通评委必填")

View File

@@ -20,7 +20,7 @@ public class MiniScoreModifyDTO implements Serializable {
@Schema(description = "选手ID") @Schema(description = "选手ID")
private Long athleteId; private Long athleteId;
@Schema(description = "修改者ID裁判ID") @Schema(description = "修改者ID裁判ID")
private Long modifierId; private Long modifierId;
@Schema(description = "修改后的分数") @Schema(description = "修改后的分数")

View File

@@ -28,6 +28,12 @@ public class ParticipantDTO implements Serializable {
@Schema(description = "学校/单位") @Schema(description = "学校/单位")
private String schoolUnit; private String schoolUnit;
/**
* 队伍名称
*/
@Schema(description = "队伍名称")
private String teamName;
/** /**
* 状态:未签到/已签到/异常 * 状态:未签到/已签到/异常
*/ */

View File

@@ -0,0 +1,33 @@
package org.springblade.modules.martial.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Schema(description = "报名提交数据")
public class RegistrationSubmitDTO {
@Schema(description = "订单号")
private String orderNo;
@Schema(description = "赛事ID")
private Long competitionId;
@Schema(description = "项目ID列表")
private String projectIds;
@Schema(description = "选手ID列表个人项目")
private String athleteIds;
@Schema(description = "集体ID列表集体项目")
private String teamIds;
@Schema(description = "联系电话")
private String contactPhone;
@Schema(description = "总金额")
private BigDecimal totalAmount;
}

View File

@@ -0,0 +1,24 @@
package org.springblade.modules.martial.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(description = "集体提交DTO")
public class TeamSubmitDTO {
@Schema(description = "集体ID更新时必填")
private String teamId;
@Schema(description = "集体名称")
private String teamName;
@Schema(description = "备注")
private String remark;
@Schema(description = "成员ID列表")
private List<Long> memberIds;
}

View File

@@ -0,0 +1,41 @@
package org.springblade.modules.martial.pojo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity;
/**
* Contact entity
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("martial_contact")
@Schema(description = "联系人")
public class MartialContact extends TenantEntity {
private static final long serialVersionUID = 1L;
@Schema(description = "证件类型")
private String idType;
@Schema(description = "姓名")
private String name;
@Schema(description = "证件号码")
private String idCard;
@Schema(description = "手机号码")
private String phone;
@Schema(description = "邮箱")
private String email;
@Schema(description = "地址")
private String address;
@Schema(description = "是否默认联系人")
private Boolean isDefault;
}

View File

@@ -93,4 +93,11 @@ public class MartialDeductionItem extends TenantEntity {
@Schema(description = "项目名称") @Schema(description = "项目名称")
private String projectName; private String projectName;
/**
* 赛事ID
*/
@TableField(exist = false)
@Schema(description = "赛事ID")
private Long competitionId;
} }

View File

@@ -60,7 +60,7 @@ public class MartialJudge extends TenantEntity {
private String idCard; private String idCard;
/** /**
* 裁判类型(1-裁判,2-普通裁判) * 裁判类型(1-裁判,2-裁判)
*/ */
@Schema(description = "裁判类型") @Schema(description = "裁判类型")
private Integer refereeType; private Integer refereeType;

View File

@@ -37,6 +37,14 @@ public class MartialJudgeInvite extends TenantEntity {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
// ========== 角色常量 ==========
/** 裁判员 */
public static final String ROLE_JUDGE = "judge";
/** 主裁判 */
public static final String ROLE_CHIEF_JUDGE = "chief_judge";
/** 总裁(裁判长) */
public static final String ROLE_GENERAL_JUDGE = "general_judge";
/** /**
* 赛事ID * 赛事ID
*/ */
@@ -56,13 +64,13 @@ public class MartialJudgeInvite extends TenantEntity {
private String inviteCode; private String inviteCode;
/** /**
* 角色(judge-普通裁判,chief_judge-裁判) * 角色(judge-裁判员, chief_judge-裁判, general_judge-总裁)
*/ */
@Schema(description = "角色") @Schema(description = "角色")
private String role; private String role;
/** /**
* 分配场地ID * 分配场地ID (总裁时为null表示负责所有场地)
*/ */
@Schema(description = "分配场地ID") @Schema(description = "分配场地ID")
private Long venueId; private Long venueId;
@@ -169,4 +177,25 @@ public class MartialJudgeInvite extends TenantEntity {
@Schema(description = "裁判类型") @Schema(description = "裁判类型")
private Integer refereeType; private Integer refereeType;
/**
* 判断是否为裁判员
*/
public boolean isJudge() {
return ROLE_JUDGE.equals(this.role);
}
/**
* 判断是否为主裁判
*/
public boolean isChiefJudge() {
return ROLE_CHIEF_JUDGE.equals(this.role);
}
/**
* 判断是否为总裁
*/
public boolean isGeneralJudge() {
return ROLE_GENERAL_JUDGE.equals(this.role);
}
} }

View File

@@ -45,6 +45,12 @@ public class MartialProject extends TenantEntity {
@Schema(description = "赛事ID") @Schema(description = "赛事ID")
private Long competitionId; private Long competitionId;
/**
* 所属场地ID
*/
@Schema(description = "所属场地ID")
private Long venueId;
/** /**
* 项目名称 * 项目名称
*/ */
@@ -63,6 +69,13 @@ public class MartialProject extends TenantEntity {
@Schema(description = "组别") @Schema(description = "组别")
private String category; private String category;
/**
* 项目类型(1-套路,2-散打,3-器械,4-对练)
*/
@Schema(description = "项目类型")
@com.baomidou.mybatisplus.annotation.TableField("event_type")
private Integer eventType;
/** /**
* 类型(1-个人,2-双人,3-集体) * 类型(1-个人,2-双人,3-集体)
*/ */
@@ -111,6 +124,18 @@ public class MartialProject extends TenantEntity {
@Schema(description = "报名费用") @Schema(description = "报名费用")
private BigDecimal price; private BigDecimal price;
/**
* 报名开始时间
*/
@Schema(description = "报名开始时间")
private LocalDateTime registrationStartTime;
/**
* 报名结束时间
*/
@Schema(description = "报名结束时间")
private LocalDateTime registrationEndTime;
/** /**
* 难度系数(默认1.00) * 难度系数(默认1.00)
*/ */

View File

@@ -38,6 +38,14 @@ public class MartialResult extends TenantEntity {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
// ========== 评分状态常量 ==========
/** 评分状态:裁判员评分中 */
public static final int SCORE_STATUS_JUDGING = 0;
/** 评分状态:主裁判已确认 */
public static final int SCORE_STATUS_CHIEF_CONFIRMED = 1;
/** 评分状态:总裁已确认 */
public static final int SCORE_STATUS_GENERAL_CONFIRMED = 2;
/** /**
* 赛事ID * 赛事ID
*/ */
@@ -158,4 +166,62 @@ public class MartialResult extends TenantEntity {
@Schema(description = "发布时间") @Schema(description = "发布时间")
private LocalDateTime publishTime; private LocalDateTime publishTime;
// ========== 主裁判确认相关字段 ==========
/**
* 主裁判确认/修改后的分数
*/
@Schema(description = "主裁判确认/修改后的分数")
private BigDecimal chiefJudgeScore;
/**
* 主裁判ID
*/
@Schema(description = "主裁判ID")
private Long chiefJudgeId;
/**
* 主裁判确认时间
*/
@Schema(description = "主裁判确认时间")
private LocalDateTime chiefJudgeTime;
/**
* 主裁判备注
*/
@Schema(description = "主裁判备注")
private String chiefJudgeNote;
// ========== 总裁确认相关字段 ==========
/**
* 总裁确认/修改后的分数
*/
@Schema(description = "总裁确认/修改后的分数")
private BigDecimal generalJudgeScore;
/**
* 总裁ID
*/
@Schema(description = "总裁ID")
private Long generalJudgeId;
/**
* 总裁确认时间
*/
@Schema(description = "总裁确认时间")
private LocalDateTime generalJudgeTime;
/**
* 总裁备注
*/
@Schema(description = "总裁备注")
private String generalJudgeNote;
/**
* 评分状态: 0-裁判员评分中, 1-主裁判已确认, 2-总裁已确认
*/
@Schema(description = "评分状态: 0-裁判员评分中, 1-主裁判已确认, 2-总裁已确认")
private Integer scoreStatus;
} }

View File

@@ -0,0 +1,29 @@
package org.springblade.modules.martial.pojo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity;
/**
* 集体/团队实体类
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("martial_team")
@Schema(description = "集体/团队")
public class MartialTeam extends TenantEntity {
private static final long serialVersionUID = 1L;
@Schema(description = "集体名称")
private String teamName;
@Schema(description = "备注")
private String remark;
@Schema(description = "成员数量")
private Integer memberCount;
}

View File

@@ -0,0 +1,40 @@
package org.springblade.modules.martial.pojo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 集体成员关联实体类
*/
@Data
@TableName("martial_team_member")
@Schema(description = "集体成员关联")
public class MartialTeamMember implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
@Schema(description = "集体ID")
private Long teamId;
@Schema(description = "选手ID")
private Long athleteId;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "是否删除")
private Integer isDeleted;
@Schema(description = "租户ID")
private String tenantId;
}

View File

@@ -53,6 +53,12 @@ public class MartialVenue extends TenantEntity {
@Schema(description = "场地编码") @Schema(description = "场地编码")
private String venueCode; private String venueCode;
/**
* 场地类型(indoor-室内,outdoor-室外)
*/
@Schema(description = "场地类型")
private String venueType;
/** /**
* 容纳人数 * 容纳人数
*/ */

View File

@@ -0,0 +1,22 @@
package org.springblade.modules.martial.pojo.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 出场顺序分组VO
*/
@Data
public class LineupGroupVO implements Serializable {
private static final long serialVersionUID = 1L;
private Long groupId;
private String groupName;
private String projectName;
private String category;
private String venueName;
private String timeSlot;
private String tableNo;
private List<LineupParticipantVO> participants;
}

View File

@@ -0,0 +1,18 @@
package org.springblade.modules.martial.pojo.vo;
import lombok.Data;
import java.io.Serializable;
/**
* 出场顺序参赛者VO
*/
@Data
public class LineupParticipantVO implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private Integer order;
private String playerName;
private String organization;
private String status;
}

View File

@@ -74,4 +74,21 @@ public class MartialJudgeInviteVO extends MartialJudgeInvite {
@Schema(description = "赛事名称") @Schema(description = "赛事名称")
private String competitionName; private String competitionName;
/**
* 场地名称
*/
@Schema(description = "场地名称")
private String venueName;
/**
* 获取场地名称
* 总裁(referee_type=3)显示全部场地
*/
public String getVenueName() {
if (this.getRefereeType() != null && this.getRefereeType() == 3) {
return "全部场地";
}
return venueName;
}
} }

View File

@@ -47,4 +47,35 @@ public class MartialScoreVO extends MartialScore {
@Schema(description = "状态文本") @Schema(description = "状态文本")
private String statusText; private String statusText;
/**
* 主裁判确认分数
*/
@Schema(description = "主裁判确认分数")
private java.math.BigDecimal chiefJudgeScore;
/**
* 评分状态 (0-待评分, 1-裁判已评分, 2-主裁判已确认)
*/
@Schema(description = "评分状态")
private Integer scoreStatus;
/**
* 队伍名称
*/
@Schema(description = "队伍名称")
private String teamName;
/**
* 身份证
*/
@Schema(description = "身份证")
private String idCard;
/**
* 选手编号
*/
@Schema(description = "选手编号")
private String playerNo;
} }

View File

@@ -0,0 +1,33 @@
package org.springblade.modules.martial.pojo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.modules.martial.pojo.entity.MartialTeam;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "集体视图对象")
public class MartialTeamVO extends MartialTeam {
private static final long serialVersionUID = 1L;
@Schema(description = "成员列表")
private List<MemberInfo> members;
@Data
@Schema(description = "成员信息")
public static class MemberInfo {
@Schema(description = "选手ID")
private Long id;
@Schema(description = "选手姓名")
private String name;
@Schema(description = "身份证号")
private String idCard;
}
}

View File

@@ -7,12 +7,12 @@ import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
/** /**
* 小程序选手评分VO裁判视图) * 小程序选手评分VO裁判视图)
* *
* @author BladeX * @author BladeX
*/ */
@Data @Data
@Schema(description = "小程序选手评分信息(裁判") @Schema(description = "小程序选手评分信息(裁判)")
public class MiniAthleteAdminVO implements Serializable { public class MiniAthleteAdminVO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@@ -20,7 +20,7 @@ public class MiniLoginVO implements Serializable {
@Schema(description = "访问令牌") @Schema(description = "访问令牌")
private String token; private String token;
@Schema(description = "用户角色pub-普通评委, admin-裁判") @Schema(description = "用户角色pub-普通评委, admin-裁判")
private String userRole; private String userRole;
@Schema(description = "比赛ID") @Schema(description = "比赛ID")

View File

@@ -25,7 +25,7 @@ public class MiniScoreDetailVO implements Serializable {
@Schema(description = "评委评分列表") @Schema(description = "评委评分列表")
private List<JudgeScore> judgeScores; private List<JudgeScore> judgeScores;
@Schema(description = "裁判修改信息") @Schema(description = "裁判修改信息")
private Modification modification; private Modification modification;
/** /**
@@ -82,10 +82,10 @@ public class MiniScoreDetailVO implements Serializable {
} }
/** /**
* 裁判修改信息内部类 * 裁判修改信息内部类
*/ */
@Data @Data
@Schema(description = "裁判修改信息") @Schema(description = "裁判修改信息")
public static class Modification implements Serializable { public static class Modification implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@@ -0,0 +1,69 @@
package org.springblade.modules.martial.pojo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* Organization Statistics VO
*/
@Data
@Schema(description = "单位统计视图对象")
public class OrganizationStatsVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "单位名称")
private String organization;
@Schema(description = "运动员人数(去重)")
private Integer athleteCount;
@Schema(description = "项目数量")
private Integer projectCount;
@Schema(description = "单人项目数")
private Integer singleProjectCount;
@Schema(description = "集体项目数")
private Integer teamProjectCount;
@Schema(description = "男运动员数")
private Integer maleCount;
@Schema(description = "女运动员数")
private Integer femaleCount;
@Schema(description = "总金额")
private BigDecimal totalAmount;
@Schema(description = "项目金额明细")
private List<ProjectAmountItem> projectAmounts;
@Data
@Schema(description = "项目金额明细")
public static class ProjectAmountItem implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "项目ID")
private Long projectId;
@Schema(description = "项目名称")
private String projectName;
@Schema(description = "项目类型(1=单人,2=集体)")
private Integer projectType;
@Schema(description = "报名人数/集体数")
private Integer count;
@Schema(description = "单价")
private BigDecimal price;
@Schema(description = "小计金额")
private BigDecimal amount;
}
}

View File

@@ -57,7 +57,7 @@ public interface IMartialAthleteService extends IService<MartialAthlete> {
List<MiniAthleteScoreVO> getAthletesWithMyScore(Long judgeId, Long venueId, Long projectId); List<MiniAthleteScoreVO> getAthletesWithMyScore(Long judgeId, Long venueId, Long projectId);
/** /**
* 小程序接口:获取选手列表(裁判 * 小程序接口:获取选手列表(裁判)
* *
* @param competitionId 比赛ID * @param competitionId 比赛ID
* @param venueId 场地ID * @param venueId 场地ID

View File

@@ -16,7 +16,7 @@
*/ */
package org.springblade.modules.martial.service; package org.springblade.modules.martial.service;
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;
@@ -44,7 +44,7 @@ public interface IMartialCompetitionRulesService {
* @param competitionId 赛事ID * @param competitionId 赛事ID
* @return 附件列表 * @return 附件列表
*/ */
List<MartialCompetitionRulesAttachment> getAttachmentList(Long competitionId); List<MartialCompetitionAttachment> getAttachmentList(Long competitionId);
/** /**
* 保存附件 * 保存附件
@@ -52,7 +52,7 @@ public interface IMartialCompetitionRulesService {
* @param attachment 附件信息 * @param attachment 附件信息
* @return 是否成功 * @return 是否成功
*/ */
boolean saveAttachment(MartialCompetitionRulesAttachment attachment); boolean saveAttachment(MartialCompetitionAttachment attachment);
/** /**
* 删除附件 * 删除附件

View File

@@ -0,0 +1,27 @@
package org.springblade.modules.martial.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springblade.modules.martial.pojo.entity.MartialContact;
/**
* Contact Service Interface
*/
public interface IMartialContactService extends IService<MartialContact> {
/**
* Get contact list by user
*/
IPage<MartialContact> getContactList(Long userId, Integer current, Integer size);
/**
* Get contact detail
*/
MartialContact getContactDetail(Long id);
/**
* Save contact with default uniqueness handling
*/
boolean saveContact(MartialContact contact, Long userId);
}

View File

@@ -1,5 +1,6 @@
package org.springblade.modules.martial.service; package org.springblade.modules.martial.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import org.springblade.modules.martial.pojo.entity.MartialRegistrationOrder; import org.springblade.modules.martial.pojo.entity.MartialRegistrationOrder;
import org.springblade.modules.martial.pojo.vo.MartialRegistrationOrderVO; import org.springblade.modules.martial.pojo.vo.MartialRegistrationOrderVO;
@@ -19,4 +20,15 @@ public interface IMartialRegistrationOrderService extends IService<MartialRegist
*/ */
MartialRegistrationOrderVO getDetailWithRelations(Long id); MartialRegistrationOrderVO getDetailWithRelations(Long id);
/**
* 获取报名订单列表(包含关联数据)
*
* @param userId 用户ID
* @param status 状态
* @param current 当前页
* @param size 每页大小
* @return 订单列表VO
*/
IPage<MartialRegistrationOrderVO> getListWithRelations(Long userId, Integer status, Integer current, Integer size);
} }

View File

@@ -70,4 +70,33 @@ public interface IMartialResultService extends IService<MartialResult> {
*/ */
List<CertificateVO> batchGenerateCertificates(Long projectId); List<CertificateVO> batchGenerateCertificates(Long projectId);
// ========== 三级裁判评分流程方法 ==========
/**
* 主裁判确认/修改分数
*/
boolean confirmByChiefJudge(Long resultId, Long chiefJudgeId, BigDecimal score, String note);
/**
* 总裁确认/修改分数
*/
boolean confirmByGeneralJudge(Long resultId, Long generalJudgeId, BigDecimal score, String note);
/**
* 获取待主裁判确认的成绩列表
*/
List<MartialResult> getPendingChiefConfirmList(Long venueId);
/**
* 获取待总裁确认的成绩列表
*/
List<MartialResult> getPendingGeneralConfirmList(Long competitionId);
/**
* 获取已总裁确认的成绩列表
*/
List<MartialResult> getConfirmedGeneralList(Long competitionId);
} }

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