This commit is contained in:
2025-12-26 10:31:41 +08:00
45 changed files with 10739 additions and 1665 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8908,6 +8908,7 @@ CREATE ALGORITHM = UNDEFINED SQL SECURITY DEFINER VIEW `v_martial_amount_stats`
-- ---------------------------- -- ----------------------------
-- Records of mt_venue -- Records of mt_venue
-- ---------------------------- -- ----------------------------
<<<<<<< HEAD
INSERT INTO `mt_venue` VALUES (200, '上海中学', 3, 3690.00); INSERT INTO `mt_venue` VALUES (200, '上海中学', 3, 3690.00);
INSERT INTO `mt_venue` VALUES (200, '上海体育学院武术系', 4, 3870.00); INSERT INTO `mt_venue` VALUES (200, '上海体育学院武术系', 4, 3870.00);
INSERT INTO `mt_venue` VALUES (200, '上海市第二体育运动学校', 3, 3840.00); INSERT INTO `mt_venue` VALUES (200, '上海市第二体育运动学校', 3, 3840.00);
@@ -8958,6 +8959,8 @@ INSERT INTO `mt_venue` VALUES (200, '重庆巴蜀中学', 4, 3680.00);
INSERT INTO `mt_venue` VALUES (200, '陈家沟太极拳学校', 4, 3840.00); INSERT INTO `mt_venue` VALUES (200, '陈家沟太极拳学校', 4, 3840.00);
INSERT INTO `mt_venue` VALUES (200, '青城山武术院', 4, 3850.00); INSERT INTO `mt_venue` VALUES (200, '青城山武术院', 4, 3850.00);
INSERT INTO `mt_venue` VALUES (200, '首都师范大学', 4, 4050.00); INSERT INTO `mt_venue` VALUES (200, '首都师范大学', 4, 4050.00);
=======
>>>>>>> 284ebd2e734d6c86ad376bfb482c7775301ae292
-- ---------------------------- -- ----------------------------
-- View structure for v_martial_participant_stats -- View structure for v_martial_participant_stats
@@ -8968,6 +8971,7 @@ CREATE ALGORITHM = UNDEFINED SQL SECURITY DEFINER VIEW `v_martial_participant_st
-- ---------------------------- -- ----------------------------
-- Records of mt_venue -- Records of mt_venue
-- ---------------------------- -- ----------------------------
<<<<<<< HEAD
INSERT INTO `mt_venue` VALUES (200, '北京体育大学武术学院', '青少年女子组', 7, 0, 0, 7, 7); INSERT INTO `mt_venue` VALUES (200, '北京体育大学武术学院', '青少年女子组', 7, 0, 0, 7, 7);
INSERT INTO `mt_venue` VALUES (200, '上海体育学院武术系', '青少年男子组', 3, 0, 0, 0, 3); INSERT INTO `mt_venue` VALUES (200, '上海体育学院武术系', '青少年男子组', 3, 0, 0, 0, 3);
INSERT INTO `mt_venue` VALUES (200, '河南登封少林寺武术学校', '成年女子组', 15, 0, 0, 15, 15); INSERT INTO `mt_venue` VALUES (200, '河南登封少林寺武术学校', '成年女子组', 15, 0, 0, 15, 15);
@@ -9069,6 +9073,8 @@ INSERT INTO `mt_venue` VALUES (200, '天津体育学院武术系', '青少年男
INSERT INTO `mt_venue` VALUES (200, '清华大学武术协会', '青少年男子组', 2, 0, 0, 0, 2); INSERT INTO `mt_venue` VALUES (200, '清华大学武术协会', '青少年男子组', 2, 0, 0, 0, 2);
INSERT INTO `mt_venue` VALUES (200, '上海中学', '青少年女子组', 1, 0, 0, 1, 1); INSERT INTO `mt_venue` VALUES (200, '上海中学', '青少年女子组', 1, 0, 0, 1, 1);
INSERT INTO `mt_venue` VALUES (200, '武当山', NULL, 1, 0, 0, 0, 1); INSERT INTO `mt_venue` VALUES (200, '武当山', NULL, 1, 0, 0, 0, 1);
=======
>>>>>>> 284ebd2e734d6c86ad376bfb482c7775301ae292
-- ---------------------------- -- ----------------------------
-- View structure for v_martial_project_time_stats -- View structure for v_martial_project_time_stats
@@ -9079,6 +9085,7 @@ CREATE ALGORITHM = UNDEFINED SQL SECURITY DEFINER VIEW `v_martial_project_time_s
-- ---------------------------- -- ----------------------------
-- Records of mt_venue -- Records of mt_venue
-- ---------------------------- -- ----------------------------
<<<<<<< HEAD
INSERT INTO `mt_venue` VALUES (0, '太极拳集体', NULL, 0, 1, 5, 0); INSERT INTO `mt_venue` VALUES (0, '太极拳集体', NULL, 0, 1, 5, 0);
INSERT INTO `mt_venue` VALUES (0, '长拳集体', NULL, 0, 1, 5, 0); INSERT INTO `mt_venue` VALUES (0, '长拳集体', NULL, 0, 1, 5, 0);
INSERT INTO `mt_venue` VALUES (0, '剑术集体', NULL, 0, 1, 5, 0); INSERT INTO `mt_venue` VALUES (0, '剑术集体', NULL, 0, 1, 5, 0);
@@ -9095,5 +9102,7 @@ INSERT INTO `mt_venue` VALUES (200, '青少年女子长拳', '青少年女子组
INSERT INTO `mt_venue` VALUES (200, '青少年太极拳', NULL, 0, 1, 4, 0); INSERT INTO `mt_venue` VALUES (200, '青少年太极拳', NULL, 0, 1, 4, 0);
INSERT INTO `mt_venue` VALUES (200, '集体拳术表演', NULL, 0, 1, 8, 0); INSERT INTO `mt_venue` VALUES (200, '集体拳术表演', NULL, 0, 1, 8, 0);
INSERT INTO `mt_venue` VALUES (200, '集体器械表演', NULL, 0, 1, 8, 0); INSERT INTO `mt_venue` VALUES (200, '集体器械表演', NULL, 0, 1, 8, 0);
=======
>>>>>>> 284ebd2e734d6c86ad376bfb482c7775301ae292
SET FOREIGN_KEY_CHECKS = 1; SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -1,116 +0,0 @@
-- =====================================================
-- 测试邀请码生成功能
-- 执行时间: 2025-12-12
-- =====================================================
USE blade;
-- =====================================================
-- 1. 插入测试数据
-- =====================================================
-- 为评委生成测试邀请码(使用现有字段)
INSERT INTO martial_judge_invite (
id, competition_id, judge_id, invite_code, role,
venue_id, projects, expire_time, is_used,
status, create_time
) VALUES (
1001, 1, 1, 'TEST01', 'judge',
1, '["女子组长拳","男子组陈氏太极拳"]',
DATE_ADD(NOW(), INTERVAL 30 DAY), 0,
1, NOW()
);
-- 为裁判长生成邀请码
INSERT INTO martial_judge_invite (
id, competition_id, judge_id, invite_code, role,
venue_id, projects, expire_time, is_used,
status, create_time
) VALUES (
1002, 1, 2, 'ADMIN1', 'chief_judge',
NULL, NULL,
DATE_ADD(NOW(), INTERVAL 30 DAY), 0,
1, NOW()
);
-- =====================================================
-- 2. 查询测试
-- =====================================================
-- 查询某个评委的有效邀请码
SELECT
ji.id,
ji.invite_code,
ji.role,
ji.expire_time,
ji.is_used,
ji.status,
j.name AS judge_name,
c.competition_name
FROM martial_judge_invite ji
LEFT JOIN martial_judge j ON ji.judge_id = j.id
LEFT JOIN martial_competition c ON ji.competition_id = c.id
WHERE ji.competition_id = 1
AND ji.judge_id = 1
AND ji.status = 1
AND ji.is_deleted = 0
AND ji.expire_time > NOW()
ORDER BY ji.create_time DESC
LIMIT 1;
-- 查询所有有效邀请码
SELECT
ji.id,
ji.invite_code,
ji.role,
j.name AS judge_name,
ji.expire_time,
ji.is_used,
CASE
WHEN ji.is_used = 1 THEN '已使用'
WHEN ji.expire_time < NOW() THEN '已过期'
WHEN ji.status = 0 THEN '已禁用'
ELSE '待使用'
END AS status_text
FROM martial_judge_invite ji
LEFT JOIN martial_judge j ON ji.judge_id = j.id
WHERE ji.competition_id = 1
AND ji.is_deleted = 0
ORDER BY ji.create_time DESC;
-- =====================================================
-- 3. 统计查询
-- =====================================================
-- 统计某个赛事的邀请码状态
SELECT
COUNT(*) AS total,
SUM(CASE WHEN is_used = 0 AND status = 1 AND expire_time > NOW() THEN 1 ELSE 0 END) AS available,
SUM(CASE WHEN is_used = 1 THEN 1 ELSE 0 END) AS used,
SUM(CASE WHEN expire_time <= NOW() THEN 1 ELSE 0 END) AS expired,
SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) AS disabled
FROM martial_judge_invite
WHERE competition_id = 1
AND is_deleted = 0;
-- =====================================================
-- 4. 验证邀请码唯一性
-- =====================================================
-- 检查邀请码是否重复
SELECT
invite_code,
COUNT(*) AS count
FROM martial_judge_invite
WHERE is_deleted = 0
GROUP BY invite_code
HAVING COUNT(*) > 1;
-- =====================================================
-- 5. 清理测试数据(可选)
-- =====================================================
-- 删除测试数据
-- DELETE FROM martial_judge_invite WHERE id IN (1001, 1002);
SELECT 'Test data inserted successfully!' AS status;

View File

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

View File

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

View File

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

View File

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

View File

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

8869
database/martial_db.sql Normal file

File diff suppressed because one or more lines are too long

79
docker-compose.yml Normal file
View File

@@ -0,0 +1,79 @@
version: '3.8'
services:
# MySQL 数据库
mysql:
image: mysql:8.0
container_name: martial-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: martial_db
TZ: Asia/Shanghai
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./database:/docker-entrypoint-initdb.d
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p123456"]
interval: 10s
timeout: 5s
retries: 5
networks:
- martial-network
# Redis 缓存
redis:
image: redis:7-alpine
container_name: martial-redis
restart: always
command: redis-server --requirepass 123456
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "123456", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- martial-network
# 后端应用
martial-api:
build:
context: .
dockerfile: Dockerfile
container_name: martial-api
restart: always
environment:
SPRING_PROFILE: dev
JAVA_OPTS: "-Xms512m -Xmx1024m -XX:+UseG1GC"
# 覆盖数据库连接配置
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/martial_db?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: 123456
# 覆盖 Redis 连接配置
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379
SPRING_DATA_REDIS_PASSWORD: 123456
ports:
- "8123:8123"
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- martial-network
networks:
martial-network:
driver: bridge
volumes:
mysql_data:
redis_data:

View File

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

View File

@@ -1,6 +1,9 @@
package org.springblade.modules.martial.controller; package org.springblade.modules.martial.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@@ -9,10 +12,15 @@ import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query; import org.springblade.core.mp.support.Query;
import org.springblade.core.tool.api.R; import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.modules.martial.pojo.entity.MartialAthlete;
import org.springblade.modules.martial.pojo.entity.MartialCompetition; import org.springblade.modules.martial.pojo.entity.MartialCompetition;
import org.springblade.modules.martial.service.IMartialAthleteService;
import org.springblade.modules.martial.service.IMartialCompetitionService; import org.springblade.modules.martial.service.IMartialCompetitionService;
import org.springblade.modules.system.pojo.entity.User;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
/** /**
* 赛事信息 控制器 * 赛事信息 控制器
* *
@@ -25,6 +33,7 @@ import org.springframework.web.bind.annotation.*;
public class MartialCompetitionController extends BladeController { public class MartialCompetitionController extends BladeController {
private final IMartialCompetitionService competitionService; private final IMartialCompetitionService competitionService;
private final IMartialAthleteService martialAthleteService;
/** /**
* 详情 * 详情
@@ -43,6 +52,14 @@ public class MartialCompetitionController extends BladeController {
@Operation(summary = "分页列表", description = "分页查询") @Operation(summary = "分页列表", description = "分页查询")
public R<IPage<MartialCompetition>> list(MartialCompetition competition, Query query) { public R<IPage<MartialCompetition>> list(MartialCompetition competition, Query query) {
IPage<MartialCompetition> pages = competitionService.page(Condition.getPage(query), Condition.getQueryWrapper(competition)); IPage<MartialCompetition> pages = competitionService.page(Condition.getPage(query), Condition.getQueryWrapper(competition));
List<MartialCompetition> pagelist = pages.getRecords();
for (MartialCompetition martialCompetition : pagelist) {
Long cnt = martialAthleteService.count(Wrappers.<MartialAthlete>query().lambda()
.eq(MartialAthlete::getCompetitionId, martialCompetition.getId())
.eq(MartialAthlete::getIsDeleted, 0)
);
martialCompetition.setTotalParticipants(cnt.intValue());
}
return R.data(pages); return R.data(pages);
} }

View File

@@ -1,6 +1,8 @@
package org.springblade.modules.martial.controller; package org.springblade.modules.martial.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -19,12 +21,17 @@ 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.entity.MtVenue;
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;
@@ -43,20 +50,23 @@ 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;
// Redis缓存key前缀
private static final String MINI_LOGIN_CACHE_PREFIX = "mini:login:";
// 登录缓存过期时间7天
private static final Duration LOGIN_CACHE_EXPIRE = Duration.ofDays(7);
/** /**
* 登录验证 * 登录验证
*
* @param dto 登录信息(比赛编码+邀请码)
* @return 登录结果token、用户信息、分配的场地和项目
*/ */
@PostMapping("/login") @PostMapping("/login")
@Operation(summary = "登录验证", description = "使用比赛编码和邀请码登录") @Operation(summary = "登录验证", description = "使用比赛编码和邀请码登录")
public R<MiniLoginVO> login(@RequestBody MiniLoginDTO dto) { public R<MiniLoginVO> login(@RequestBody MiniLoginDTO dto) {
// 1. 根据邀请码查询邀请信息
LambdaQueryWrapper<MartialJudgeInvite> inviteQuery = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialJudgeInvite> inviteQuery = new LambdaQueryWrapper<>();
inviteQuery.eq(MartialJudgeInvite::getInviteCode, dto.getInviteCode()); inviteQuery.eq(MartialJudgeInvite::getInviteCode, dto.getInviteCode());
inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0); inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
@@ -66,29 +76,24 @@ public class MartialMiniController extends BladeController {
return R.fail("邀请码不存在"); return R.fail("邀请码不存在");
} }
// 2. 验证邀请码是否过期
if (invite.getExpireTime() != null && invite.getExpireTime().isBefore(LocalDateTime.now())) { if (invite.getExpireTime() != null && invite.getExpireTime().isBefore(LocalDateTime.now())) {
return R.fail("邀请码已过期"); return R.fail("邀请码已过期");
} }
// 3. 查询比赛信息
MartialCompetition competition = competitionService.getById(invite.getCompetitionId()); MartialCompetition competition = competitionService.getById(invite.getCompetitionId());
if (competition == null) { if (competition == null) {
return R.fail("比赛不存在"); return R.fail("比赛不存在");
} }
// 4. 验证比赛编码
if (!competition.getCompetitionCode().equals(dto.getMatchCode())) { if (!competition.getCompetitionCode().equals(dto.getMatchCode())) {
return R.fail("比赛编码不匹配"); return R.fail("比赛编码不匹配");
} }
// 5. 查询评委信息
MartialJudge judge = judgeService.getById(invite.getJudgeId()); MartialJudge judge = judgeService.getById(invite.getJudgeId());
if (judge == null) { if (judge == null) {
return R.fail("评委信息不存在"); return R.fail("评委信息不存在");
} }
// 6. 生成访问令牌
String token = UUID.randomUUID().toString().replace("-", ""); String token = UUID.randomUUID().toString().replace("-", "");
invite.setAccessToken(token); invite.setAccessToken(token);
invite.setTokenExpireTime(LocalDateTime.now().plusDays(7)); invite.setTokenExpireTime(LocalDateTime.now().plusDays(7));
@@ -98,16 +103,14 @@ public class MartialMiniController extends BladeController {
invite.setDeviceInfo(dto.getDeviceInfo()); invite.setDeviceInfo(dto.getDeviceInfo());
judgeInviteService.updateById(invite); judgeInviteService.updateById(invite);
// 7. 查询场地信息(裁判长没有固定场地) // 从 mt_venue 表获取场地信息
MartialVenue venue = null; MtVenue mtVenue = null;
if (invite.getVenueId() != null) { if (invite.getVenueId() != null) {
venue = venueService.getById(invite.getVenueId()); mtVenue = mtVenueService.getById(invite.getVenueId());
} }
// 8. 解析分配的项目
List<MiniLoginVO.ProjectInfo> projects = parseProjects(invite.getProjects()); List<MiniLoginVO.ProjectInfo> projects = parseProjects(invite.getProjects());
// 9. 构造返回结果
MiniLoginVO vo = new MiniLoginVO(); MiniLoginVO vo = new MiniLoginVO();
vo.setToken(token); vo.setToken(token);
vo.setUserRole("chief_judge".equals(invite.getRole()) ? "admin" : "pub"); vo.setUserRole("chief_judge".equals(invite.getRole()) ? "admin" : "pub");
@@ -117,71 +120,214 @@ public class MartialMiniController extends BladeController {
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(mtVenue != null ? mtVenue.getId() : null);
vo.setVenueName(venue != null ? venue.getVenueName() : null); vo.setVenueName(mtVenue != null ? mtVenue.getVenueName() : null);
vo.setProjects(projects); vo.setProjects(projects);
// 将登录信息缓存到Redis服务重启后仍然有效
String cacheKey = MINI_LOGIN_CACHE_PREFIX + token;
bladeRedis.setEx(cacheKey, vo, LOGIN_CACHE_EXPIRE);
return R.data(vo); return R.data(vo);
} }
/** /**
* 提交评分(评委) * 提交评分(评委)
* * 注意ID字段使用String类型接收避免JavaScript大数精度丢失问题
* @param dto 评分信息
* @return 提交结果
*/ */
@PostMapping("/score/submit") @PostMapping("/score/submit")
@Operation(summary = "提交评分", description = "评委提交对选手的评分") @Operation(summary = "提交评分", description = "评委提交对选手的评分")
public R submitScore(@RequestBody org.springblade.modules.martial.pojo.dto.MiniScoreSubmitDTO dto) { public R submitScore(@RequestBody org.springblade.modules.martial.pojo.dto.MiniScoreSubmitDTO dto) {
// 转换DTO为实体
MartialScore score = new MartialScore(); MartialScore score = new MartialScore();
score.setAthleteId(dto.getAthleteId());
score.setJudgeId(dto.getJudgeId()); // 将String类型的ID转换为Long避免JavaScript大数精度丢失
score.setAthleteId(parseLong(dto.getAthleteId()));
score.setJudgeId(parseLong(dto.getJudgeId()));
score.setScore(dto.getScore()); score.setScore(dto.getScore());
score.setProjectId(dto.getProjectId()); score.setProjectId(parseLong(dto.getProjectId()));
score.setCompetitionId(dto.getCompetitionId()); score.setCompetitionId(parseLong(dto.getCompetitionId()));
score.setVenueId(dto.getVenueId()); score.setVenueId(parseLong(dto.getVenueId()));
score.setScheduleId(dto.getScheduleId()); score.setScheduleId(parseLong(dto.getScheduleId()));
score.setNote(dto.getNote()); score.setNote(dto.getNote());
score.setScoreTime(LocalDateTime.now()); score.setScoreTime(LocalDateTime.now());
// 将扣分项列表转换为JSON字符串存储
if (dto.getDeductions() != null && !dto.getDeductions().isEmpty()) { if (dto.getDeductions() != null && !dto.getDeductions().isEmpty()) {
score.setDeductionItems(com.alibaba.fastjson.JSON.toJSONString(dto.getDeductions())); // 将String类型的扣分项ID转换为Long
List<Long> deductionIds = dto.getDeductions().stream()
.map(this::parseLong)
.filter(id -> id != null)
.collect(Collectors.toList());
score.setDeductionItems(com.alibaba.fastjson.JSON.toJSONString(deductionIds));
} }
// 获取评委姓名 Long judgeId = parseLong(dto.getJudgeId());
if (dto.getJudgeId() != null) { if (judgeId != null) {
var judge = judgeService.getById(dto.getJudgeId()); var judge = judgeService.getById(judgeId);
if (judge != null) { if (judge != null) {
score.setJudgeName(judge.getName()); score.setJudgeName(judge.getName());
} }
} }
// 保存评分(会自动进行权限验证和分数范围验证)
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时直接取平均分
* * 只有所有裁判都评分完成后才更新总分
* @param judgeId 裁判ID */
* @param refereeType 裁判类型1-裁判长, 2-普通裁判) private void updateAthleteTotalScore(Long athleteId, Long projectId, Long venueId) {
* @param projectId 项目ID可选用于筛选特定项目的选手 try {
* @param venueId 场地ID可选用于筛选特定场地的选手 // 1. 查询该场地的普通裁判数量
* @return 选手列表 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();
}
}
/**
* 获取场地应评分的裁判数量(普通裁判,不包括裁判长)
* 注意:使用 DISTINCT judge_id 来避免重复计数
*/
private int getRequiredJudgeCount(Long venueId) {
if (venueId == null) {
return 0;
}
LambdaQueryWrapper<MartialJudgeInvite> judgeQuery = new LambdaQueryWrapper<>();
judgeQuery.eq(MartialJudgeInvite::getVenueId, venueId);
judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
judgeQuery.ne(MartialJudgeInvite::getRole, "chief_judge"); // 排除裁判长
List<MartialJudgeInvite> judges = judgeInviteService.list(judgeQuery);
// 使用 distinct judge_id 来计算不重复的裁判数量
return (int) judges.stream()
.map(MartialJudgeInvite::getJudgeId)
.filter(Objects::nonNull)
.distinct()
.count();
}
/**
* 计算总分
* 算法:去掉一个最高分和一个最低分,取剩余分数的平均值
* 特殊情况:裁判数量<3时直接取平均分
*/
private BigDecimal calculateTotalScore(List<MartialScore> scores) {
if (scores == null || scores.isEmpty()) {
return null;
}
// 提取所有分数并排序
List<BigDecimal> scoreValues = scores.stream()
.map(MartialScore::getScore)
.filter(Objects::nonNull)
.sorted()
.collect(Collectors.toList());
int count = scoreValues.size();
if (count == 0) {
return null;
}
if (count < 3) {
// 裁判数量<3直接取平均分
BigDecimal sum = scoreValues.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
return sum.divide(new BigDecimal(count), 3, RoundingMode.HALF_UP);
}
// 去掉最高分和最低分(已排序,去掉第一个和最后一个)
List<BigDecimal> middleScores = scoreValues.subList(1, count - 1);
// 计算平均分
BigDecimal sum = middleScores.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
return sum.divide(new BigDecimal(middleScores.size()), 3, RoundingMode.HALF_UP);
}
/**
* 安全地将String转换为Long
*/
private Long parseLong(String value) {
if (value == null || value.trim().isEmpty()) {
return null;
}
try {
return Long.parseLong(value.trim());
} catch (NumberFormatException e) {
return null;
}
}
/**
* 获取选手列表(支持分页)
* - 普通裁判:获取所有选手,标记是否已评分
* - 裁判长:获取所有普通裁判都评分完成的选手列表
*/ */
@GetMapping("/score/athletes") @GetMapping("/score/athletes")
@Operation(summary = "获取选手列表", description = "根据裁判类型获取不同的选手列表") @Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)")
public R<List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO>> getAthletes( public R<IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO>> getAthletes(
@RequestParam Long judgeId, @RequestParam Long judgeId,
@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(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size
) { ) {
// 1. 构建选手查询条件 // 1. 构建选手查询条件
LambdaQueryWrapper<MartialAthlete> athleteQuery = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialAthlete> athleteQuery = new LambdaQueryWrapper<>();
@@ -190,57 +336,91 @@ public class MartialMiniController extends BladeController {
if (projectId != null) { if (projectId != null) {
athleteQuery.eq(MartialAthlete::getProjectId, projectId); athleteQuery.eq(MartialAthlete::getProjectId, projectId);
} }
// 注意场地筛选需要通过评分记录的venueId来过滤这里先查询所有选手
// 按出场顺序排序
athleteQuery.orderByAsc(MartialAthlete::getOrderNum); athleteQuery.orderByAsc(MartialAthlete::getOrderNum);
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) {
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. 获取该场地的应评裁判数量
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> result; int requiredJudgeCount = getRequiredJudgeCount(venueId);
// 5. 根据裁判类型处理选手列表
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList;
if (refereeType == 1) { if (refereeType == 1) {
// 裁判长:返回已有评分的选手 // 裁判长:返回所有选手前端根据totalScore判断是否显示修改按钮
result = 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())))
.collect(java.util.stream.Collectors.toList()); .collect(java.util.stream.Collectors.toList());
} else { } else {
// 普通裁判:返回该裁判还未评分的选手 // 普通裁判:返回所有选手,标记是否已评分
result = athletes.stream() filteredList = athletes.stream()
.filter(athlete -> { .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount))
List<MartialScore> scores = scoresByAthlete.get(athlete.getId());
if (scores == null) {
return true; // 没有任何评分,可以评
}
// 检查该裁判是否已评分
return scores.stream().noneMatch(s -> s.getJudgeId().equals(judgeId));
})
.map(athlete -> convertToAthleteListVO(athlete, null))
.collect(java.util.stream.Collectors.toList()); .collect(java.util.stream.Collectors.toList());
} }
return R.data(result); // 6. 手动分页
int total = filteredList.size();
int fromIndex = (current - 1) * size;
int toIndex = Math.min(fromIndex + size, total);
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> pageRecords;
if (fromIndex >= total) {
pageRecords = new ArrayList<>();
} else {
pageRecords = filteredList.subList(fromIndex, toIndex);
}
// 7. 构建分页结果
IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> page = new Page<>(current, size, total);
page.setRecords(pageRecords);
return R.data(page);
}
/**
* 获取场地所有裁判长的judge_id列表
*/
private List<Long> getChiefJudgeIds(Long venueId) {
if (venueId == null) {
return new ArrayList<>();
}
LambdaQueryWrapper<MartialJudgeInvite> judgeQuery = new LambdaQueryWrapper<>();
judgeQuery.eq(MartialJudgeInvite::getVenueId, venueId);
judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
judgeQuery.eq(MartialJudgeInvite::getRole, "chief_judge");
List<MartialJudgeInvite> chiefJudges = judgeInviteService.list(judgeQuery);
return chiefJudges.stream()
.map(MartialJudgeInvite::getJudgeId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
} }
/** /**
* 获取评分详情 * 获取评分详情
*
* @param athleteId 选手ID
* @return 评分详情(选手信息+所有评委的评分)
*/ */
@GetMapping("/score/detail/{athleteId}") @GetMapping("/score/detail/{athleteId}")
@Operation(summary = "评分详情", description = "查看选手的所有评委评分") @Operation(summary = "评分详情", description = "查看选手的所有评委评分")
@@ -251,9 +431,6 @@ public class MartialMiniController extends BladeController {
/** /**
* 修改评分(裁判长) * 修改评分(裁判长)
*
* @param dto 修改信息选手ID、修改后的分数、修改原因
* @return 修改结果
*/ */
@PutMapping("/score/modify") @PutMapping("/score/modify")
@Operation(summary = "修改评分", description = "裁判长修改选手总分") @Operation(summary = "修改评分", description = "裁判长修改选手总分")
@@ -264,45 +441,97 @@ public class MartialMiniController extends BladeController {
/** /**
* 退出登录 * 退出登录
*
* @return 退出结果
*/ */
@PostMapping("/logout") @PostMapping("/logout")
@Operation(summary = "退出登录", description = "清除登录状态") @Operation(summary = "退出登录", description = "清除登录状态")
public R logout() { public R logout(@RequestHeader(value = "Authorization", required = false) String token) {
// TODO: 实现真实的退出逻辑 // 从Redis删除登录缓存
// 1. 清除token if (token != null && !token.isEmpty()) {
// 2. 清除session String cacheKey = MINI_LOGIN_CACHE_PREFIX + token;
bladeRedis.del(cacheKey);
}
return R.success("退出成功"); return R.success("退出成功");
} }
/** /**
* Token验证 * Token验证从Redis恢复登录状态
*
* @return 验证结果
*/ */
@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) {
// TODO: 实现真实的token验证逻辑 if (token == null || token.isEmpty()) {
// 1. 从请求头获取token return R.fail("Token不能为空");
// 2. 验证token是否有效 }
// 3. 返回验证结果
return R.success("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());
MtVenue mtVenue = invite.getVenueId() != null ? mtVenueService.getById(invite.getVenueId()) : null;
List<MiniLoginVO.ProjectInfo> projects = parseProjects(invite.getProjects());
MiniLoginVO vo = new MiniLoginVO();
vo.setToken(token);
vo.setUserRole("chief_judge".equals(invite.getRole()) ? "admin" : "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(mtVenue != null ? mtVenue.getId() : null);
vo.setVenueName(mtVenue != null ? mtVenue.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(MartialAthlete athlete, List<MartialScore> scores) { private org.springblade.modules.martial.pojo.vo.MiniAthleteListVO convertToAthleteListVO(
MartialAthlete athlete,
List<MartialScore> scores,
Long currentJudgeId,
int requiredJudgeCount) {
org.springblade.modules.martial.pojo.vo.MiniAthleteListVO vo = new org.springblade.modules.martial.pojo.vo.MiniAthleteListVO(); 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());
vo.setIdCard(athlete.getIdCard());
vo.setNumber(athlete.getPlayerNo()); vo.setNumber(athlete.getPlayerNo());
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) {
@@ -312,9 +541,44 @@ public class MartialMiniController extends BladeController {
} }
} }
// 设置评分裁判数量(仅裁判长可见) // 设置评分状态
if (scores != null) { int scoredCount = 0;
vo.setScoredJudgeCount(scores.size()); if (scores != null && !scores.isEmpty()) {
scoredCount = scores.size();
vo.setScoredJudgeCount(scoredCount);
// 查找当前裁判的评分
MartialScore myScore = scores.stream()
.filter(s -> s.getJudgeId().equals(currentJudgeId))
.findFirst()
.orElse(null);
if (myScore != null) {
vo.setScored(true);
vo.setMyScore(myScore.getScore());
} else {
vo.setScored(false);
}
} else {
vo.setScored(false);
vo.setScoredJudgeCount(0);
}
// 判断评分是否完成(所有裁判都已评分)
boolean scoringComplete = false;
if (requiredJudgeCount > 0) {
scoringComplete = scoredCount >= requiredJudgeCount;
} else {
// 如果没有配置裁判数量,只要有评分就算完成
scoringComplete = scoredCount > 0;
}
vo.setScoringComplete(scoringComplete);
// 只有评分完成时才显示总分
if (scoringComplete) {
vo.setTotalScore(athlete.getTotalScore());
} else {
vo.setTotalScore(null);
} }
return vo; return vo;
@@ -331,11 +595,9 @@ public class MartialMiniController extends BladeController {
} }
try { try {
// 解析JSON数组格式为 [{"projectId": 1, "projectName": "太极拳"}, ...]
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
List<Long> projectIds = mapper.readValue(projectsJson, new TypeReference<List<Long>>() {}); List<Long> projectIds = mapper.readValue(projectsJson, new TypeReference<List<Long>>() {});
// 查询项目详情
if (Func.isNotEmpty(projectIds)) { if (Func.isNotEmpty(projectIds)) {
List<MartialProject> projectList = projectService.listByIds(projectIds); List<MartialProject> projectList = projectService.listByIds(projectIds);
projects = projectList.stream().map(project -> { projects = projectList.stream().map(project -> {
@@ -346,7 +608,6 @@ public class MartialMiniController extends BladeController {
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }
} catch (Exception e) { } catch (Exception e) {
// 如果JSON解析失败尝试按逗号分隔的ID字符串解析
try { try {
String[] ids = projectsJson.split(","); String[] ids = projectsJson.split(",");
List<Long> projectIds = new ArrayList<>(); List<Long> projectIds = new ArrayList<>();

View File

@@ -0,0 +1,381 @@
package org.springblade.modules.martial.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.modules.martial.pojo.dto.MiniAthleteScoreDTO;
import org.springblade.modules.martial.pojo.dto.MiniLoginDTO;
import org.springblade.modules.martial.pojo.dto.MiniScoreModifyDTO;
import org.springblade.modules.martial.pojo.entity.*;
import org.springblade.modules.martial.pojo.vo.MiniAthleteAdminVO;
import org.springblade.modules.martial.pojo.vo.MiniAthleteScoreVO;
import org.springblade.modules.martial.pojo.vo.MiniLoginVO;
import org.springblade.modules.martial.pojo.vo.MiniScoreDetailVO;
import com.alibaba.fastjson.JSON;
import org.springblade.modules.martial.service.*;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* 小程序专用接口 控制器
*
* @author BladeX
*/
@RestController
@AllArgsConstructor
@RequestMapping("/mini")
@Tag(name = "小程序接口", description = "小程序评分系统专用接口")
public class MartialMiniController extends BladeController {
private final IMartialJudgeInviteService judgeInviteService;
private final IMartialJudgeService judgeService;
private final IMartialCompetitionService competitionService;
private final IMartialVenueService venueService;
private final IMartialProjectService projectService;
private final IMartialAthleteService athleteService;
private final IMartialScoreService scoreService;
/**
* 登录验证
*/
@PostMapping("/login")
@Operation(summary = "登录验证", description = "使用比赛编码和邀请码登录")
public R<MiniLoginVO> login(@RequestBody MiniLoginDTO dto) {
LambdaQueryWrapper<MartialJudgeInvite> inviteQuery = new LambdaQueryWrapper<>();
inviteQuery.eq(MartialJudgeInvite::getInviteCode, dto.getInviteCode());
inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
MartialJudgeInvite invite = judgeInviteService.getOne(inviteQuery);
if (invite == null) {
return R.fail("邀请码不存在");
}
if (invite.getExpireTime() != null && invite.getExpireTime().isBefore(LocalDateTime.now())) {
return R.fail("邀请码已过期");
}
MartialCompetition competition = competitionService.getById(invite.getCompetitionId());
if (competition == null) {
return R.fail("比赛不存在");
}
if (!competition.getCompetitionCode().equals(dto.getMatchCode())) {
return R.fail("比赛编码不匹配");
}
MartialJudge judge = judgeService.getById(invite.getJudgeId());
if (judge == null) {
return R.fail("评委信息不存在");
}
String token = UUID.randomUUID().toString().replace("-", "");
invite.setAccessToken(token);
invite.setTokenExpireTime(LocalDateTime.now().plusDays(7));
invite.setIsUsed(1);
invite.setUseTime(LocalDateTime.now());
invite.setLoginIp(dto.getLoginIp());
invite.setDeviceInfo(dto.getDeviceInfo());
judgeInviteService.updateById(invite);
MartialVenue venue = null;
if (invite.getVenueId() != null) {
venue = venueService.getById(invite.getVenueId());
}
List<MiniLoginVO.ProjectInfo> projects = parseProjects(invite.getProjects());
MiniLoginVO vo = new MiniLoginVO();
vo.setToken(token);
vo.setUserRole("chief_judge".equals(invite.getRole()) ? "admin" : "pub");
vo.setMatchId(competition.getId());
vo.setMatchName(competition.getCompetitionName());
vo.setMatchTime(competition.getCompetitionStartTime() != null ?
competition.getCompetitionStartTime().toString() : "");
vo.setJudgeId(judge.getId());
vo.setJudgeName(judge.getName());
vo.setVenueId(venue != null ? venue.getId() : null);
vo.setVenueName(venue != null ? venue.getVenueName() : null);
vo.setProjects(projects);
return R.data(vo);
}
/**
* 提交评分(评委)
* 注意ID字段使用String类型接收避免JavaScript大数精度丢失问题
*/
@PostMapping("/score/submit")
@Operation(summary = "提交评分", description = "评委提交对选手的评分")
public R submitScore(@RequestBody org.springblade.modules.martial.pojo.dto.MiniScoreSubmitDTO dto) {
MartialScore score = new MartialScore();
// 将String类型的ID转换为Long避免JavaScript大数精度丢失
score.setAthleteId(parseLong(dto.getAthleteId()));
score.setJudgeId(parseLong(dto.getJudgeId()));
score.setScore(dto.getScore());
score.setProjectId(parseLong(dto.getProjectId()));
score.setCompetitionId(parseLong(dto.getCompetitionId()));
score.setVenueId(parseLong(dto.getVenueId()));
score.setScheduleId(parseLong(dto.getScheduleId()));
score.setNote(dto.getNote());
score.setScoreTime(LocalDateTime.now());
if (dto.getDeductions() != null && !dto.getDeductions().isEmpty()) {
// 将String类型的扣分项ID转换为Long
List<Long> deductionIds = dto.getDeductions().stream()
.map(this::parseLong)
.filter(id -> id != null)
.collect(Collectors.toList());
score.setDeductionItems(com.alibaba.fastjson.JSON.toJSONString(deductionIds));
}
Long judgeId = parseLong(dto.getJudgeId());
if (judgeId != null) {
var judge = judgeService.getById(judgeId);
if (judge != null) {
score.setJudgeName(judge.getName());
}
}
boolean success = scoreService.save(score);
return success ? R.success("评分提交成功") : R.fail("评分提交失败");
}
/**
* 安全地将String转换为Long
*/
private Long parseLong(String value) {
if (value == null || value.trim().isEmpty()) {
return null;
}
try {
return Long.parseLong(value.trim());
} catch (NumberFormatException e) {
return null;
}
}
/**
* 获取选手列表(支持分页)
* - 普通裁判:获取所有选手,标记是否已评分
* - 裁判长:获取已有评分的选手列表
*/
@GetMapping("/score/athletes")
@Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)")
public R<IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO>> getAthletes(
@RequestParam Long judgeId,
@RequestParam Integer refereeType,
@RequestParam(required = false) Long projectId,
@RequestParam(required = false) Long venueId,
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size
) {
// 1. 构建选手查询条件
LambdaQueryWrapper<MartialAthlete> athleteQuery = new LambdaQueryWrapper<>();
athleteQuery.eq(MartialAthlete::getIsDeleted, 0);
if (projectId != null) {
athleteQuery.eq(MartialAthlete::getProjectId, projectId);
}
athleteQuery.orderByAsc(MartialAthlete::getOrderNum);
List<MartialAthlete> athletes = athleteService.list(athleteQuery);
// 2. 获取所有评分记录
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getIsDeleted, 0);
List<MartialScore> allScores = scoreService.list(scoreQuery);
// 按选手ID分组统计评分
java.util.Map<Long, List<MartialScore>> scoresByAthlete = allScores.stream()
.collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId));
// 3. 根据裁判类型处理选手列表
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList;
if (refereeType == 1) {
// 裁判长:返回已有评分的选手
filteredList = athletes.stream()
.filter(athlete -> {
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());
} else {
// 普通裁判:返回所有选手,标记是否已评分
filteredList = athletes.stream()
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId))
.collect(java.util.stream.Collectors.toList());
}
// 4. 手动分页
int total = filteredList.size();
int fromIndex = (current - 1) * size;
int toIndex = Math.min(fromIndex + size, total);
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> pageRecords;
if (fromIndex >= total) {
pageRecords = new ArrayList<>();
} else {
pageRecords = filteredList.subList(fromIndex, toIndex);
}
// 5. 构建分页结果
IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> page = new Page<>(current, size, total);
page.setRecords(pageRecords);
return R.data(page);
}
/**
* 获取评分详情
*/
@GetMapping("/score/detail/{athleteId}")
@Operation(summary = "评分详情", description = "查看选手的所有评委评分")
public R<MiniScoreDetailVO> getScoreDetail(@PathVariable Long athleteId) {
MiniScoreDetailVO detail = scoreService.getScoreDetailForMini(athleteId);
return R.data(detail);
}
/**
* 修改评分(裁判长)
*/
@PutMapping("/score/modify")
@Operation(summary = "修改评分", description = "裁判长修改选手总分")
public R modifyScore(@RequestBody MiniScoreModifyDTO dto) {
boolean success = scoreService.modifyScoreByAdmin(dto);
return success ? R.success("修改成功") : R.fail("修改失败");
}
/**
* 退出登录
*/
@PostMapping("/logout")
@Operation(summary = "退出登录", description = "清除登录状态")
public R logout() {
return R.success("退出成功");
}
/**
* Token验证
*/
@GetMapping("/verify")
@Operation(summary = "Token验证", description = "验证当前token是否有效")
public R verify() {
return R.success("Token有效");
}
/**
* 转换选手实体为VO
*/
private org.springblade.modules.martial.pojo.vo.MiniAthleteListVO convertToAthleteListVO(
MartialAthlete athlete,
List<MartialScore> scores,
Long currentJudgeId) {
org.springblade.modules.martial.pojo.vo.MiniAthleteListVO vo = new org.springblade.modules.martial.pojo.vo.MiniAthleteListVO();
vo.setAthleteId(athlete.getId());
vo.setName(athlete.getPlayerName());
vo.setIdCard(athlete.getIdCard());
vo.setNumber(athlete.getPlayerNo());
vo.setTeam(athlete.getTeamName());
vo.setOrderNum(athlete.getOrderNum());
vo.setCompetitionStatus(athlete.getCompetitionStatus());
vo.setTotalScore(athlete.getTotalScore());
// 设置项目名称
if (athlete.getProjectId() != null) {
MartialProject project = projectService.getById(athlete.getProjectId());
if (project != null) {
vo.setProjectName(project.getProjectName());
}
}
// 设置评分状态
if (scores != null && !scores.isEmpty()) {
vo.setScoredJudgeCount(scores.size());
// 查找当前裁判的评分
MartialScore myScore = scores.stream()
.filter(s -> s.getJudgeId().equals(currentJudgeId))
.findFirst()
.orElse(null);
if (myScore != null) {
vo.setScored(true);
vo.setMyScore(myScore.getScore());
} else {
vo.setScored(false);
}
} else {
vo.setScored(false);
vo.setScoredJudgeCount(0);
}
return vo;
}
/**
* 解析项目JSON字符串
*/
private List<MiniLoginVO.ProjectInfo> parseProjects(String projectsJson) {
List<MiniLoginVO.ProjectInfo> projects = new ArrayList<>();
if (Func.isEmpty(projectsJson)) {
return projects;
}
try {
ObjectMapper mapper = new ObjectMapper();
List<Long> projectIds = mapper.readValue(projectsJson, new TypeReference<List<Long>>() {});
if (Func.isNotEmpty(projectIds)) {
List<MartialProject> projectList = projectService.listByIds(projectIds);
projects = projectList.stream().map(project -> {
MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo();
info.setProjectId(project.getId());
info.setProjectName(project.getProjectName());
return info;
}).collect(Collectors.toList());
}
} catch (Exception e) {
try {
String[] ids = projectsJson.split(",");
List<Long> projectIds = new ArrayList<>();
for (String id : ids) {
projectIds.add(Long.parseLong(id.trim()));
}
if (Func.isNotEmpty(projectIds)) {
List<MartialProject> projectList = projectService.listByIds(projectIds);
projects = projectList.stream().map(project -> {
MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo();
info.setProjectId(project.getId());
info.setProjectName(project.getProjectName());
return info;
}).collect(Collectors.toList());
}
} catch (Exception ex) {
// 解析失败,返回空列表
}
}
return projects;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
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.MtVenue;
/**
* 场地 Mapper
*/
@Mapper
public interface MtVenueMapper extends BaseMapper<MtVenue> {
}

View File

@@ -28,4 +28,7 @@ public class MiniScoreModifyDTO implements Serializable {
@Schema(description = "修改原因/备注") @Schema(description = "修改原因/备注")
private String note; private String note;
@Schema(description = "场地ID")
private Long venueId;
} }

View File

@@ -10,6 +10,8 @@ import java.util.List;
/** /**
* 小程序提交评分请求DTO * 小程序提交评分请求DTO
* *
* 注意所有ID字段使用String类型避免JavaScript大数精度丢失问题
*
* @author BladeX * @author BladeX
*/ */
@Data @Data
@@ -19,29 +21,29 @@ public class MiniScoreSubmitDTO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Schema(description = "选手ID") @Schema(description = "选手ID")
private Long athleteId; private String athleteId;
@Schema(description = "评委ID") @Schema(description = "评委ID")
private Long judgeId; private String judgeId;
@Schema(description = "评分") @Schema(description = "评分")
private BigDecimal score; private BigDecimal score;
@Schema(description = "扣分项ID列表") @Schema(description = "扣分项ID列表")
private List<Long> deductions; private List<String> deductions;
@Schema(description = "备注") @Schema(description = "备注")
private String note; private String note;
@Schema(description = "项目ID") @Schema(description = "项目ID")
private Long projectId; private String projectId;
@Schema(description = "赛事ID") @Schema(description = "赛事ID")
private Long competitionId; private String competitionId;
@Schema(description = "场地ID") @Schema(description = "场地ID")
private Long venueId; private String venueId;
@Schema(description = "赛程ID") @Schema(description = "赛程ID")
private Long scheduleId; private String scheduleId;
} }

View File

@@ -40,4 +40,10 @@ public class ParticipantDTO implements Serializable {
@Schema(description = "排序") @Schema(description = "排序")
private Integer sortOrder; private Integer sortOrder;
/**
* 选手姓名
*/
@Schema(description = "选手姓名")
private String playerName;
} }

View File

@@ -0,0 +1,89 @@
/*
* Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the dreamlu.net developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: Chill 庄骞 (smallchill@163.com)
*/
package org.springblade.modules.martial.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;
/**
* 赛事附件实体类(通用)
* 支持多种附件类型:赛事发布、赛事规程、活动日程、成绩、奖牌榜、图片直播
*
* @author BladeX
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("martial_competition_attachment")
@Schema(description = "赛事附件")
public class MartialCompetitionAttachment extends TenantEntity {
private static final long serialVersionUID = 1L;
/**
* 赛事ID
*/
@Schema(description = "赛事ID")
private Long competitionId;
/**
* 附件类型
* info-赛事发布, rules-赛事规程, schedule-活动日程,
* results-成绩, medals-奖牌榜, photos-图片直播
*/
@Schema(description = "附件类型info-赛事发布, rules-赛事规程, schedule-活动日程, results-成绩, medals-奖牌榜, photos-图片直播")
private String attachmentType;
/**
* 文件名称
*/
@Schema(description = "文件名称")
private String fileName;
/**
* 文件URL
*/
@Schema(description = "文件URL")
private String fileUrl;
/**
* 文件大小(字节)
*/
@Schema(description = "文件大小(字节)")
private Long fileSize;
/**
* 文件类型(扩展名)
*/
@Schema(description = "文件类型pdf/doc/docx/xls/xlsx/jpg/png等")
private String fileType;
/**
* 排序序号
*/
@Schema(description = "排序序号")
private Integer orderNum;
/**
* 状态1-启用 0-禁用)
*/
@Schema(description = "状态1-启用 0-禁用)")
private Integer status;
}

View File

@@ -0,0 +1,44 @@
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;
/**
* 场地信息实体类mt_venue 表)
*/
@Data
@TableName("mt_venue")
@Schema(description = "场地信息")
public class MtVenue implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
@Schema(description = "赛事ID")
private Long competitionId;
@Schema(description = "场地名称")
private String venueName;
@Schema(description = "场地编号")
private Integer venueNo;
@Schema(description = "状态")
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private Integer isDeleted;
private String tenantId;
private Long createUser;
private Long createDept;
private Long updateUser;
}

View File

@@ -1,5 +1,9 @@
package org.springblade.modules.martial.pojo.vo; package org.springblade.modules.martial.pojo.vo;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -9,38 +13,72 @@ import java.math.BigDecimal;
/** /**
* 小程序选手列表VO * 小程序选手列表VO
* *
* 注意Long类型的ID字段使用ToStringSerializer序列化为字符串
* 避免JavaScript大数精度丢失问题
*
* @author BladeX * @author BladeX
*/ */
@Data @Data
@Schema(description = "小程序选手列表") @Schema(description = "小程序选手列表")
@JsonInclude(JsonInclude.Include.ALWAYS)
public class MiniAthleteListVO implements Serializable { public class MiniAthleteListVO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Schema(description = "选手ID") @Schema(description = "选手ID")
@JsonProperty("athleteId")
@JsonSerialize(using = ToStringSerializer.class)
private Long athleteId; private Long athleteId;
@Schema(description = "选手姓名") @Schema(description = "选手姓名")
@JsonProperty("name")
private String name; private String name;
@Schema(description = "身份证号")
@JsonProperty("idCard")
private String idCard = "";
@Schema(description = "参赛编号") @Schema(description = "参赛编号")
@JsonProperty("number")
private String number; private String number;
@Schema(description = "队伍名称") @Schema(description = "队伍名称")
@JsonProperty("team")
private String team; private String team;
@Schema(description = "项目名称") @Schema(description = "项目名称")
@JsonProperty("projectName")
private String projectName; private String projectName;
@Schema(description = "出场顺序") @Schema(description = "出场顺序")
@JsonProperty("orderNum")
private Integer orderNum; private Integer orderNum;
@Schema(description = "总分(裁判长可见") @Schema(description = "是否已评分(当前裁判")
@JsonProperty("scored")
private Boolean scored = false;
@Schema(description = "我的评分(当前裁判的评分)")
@JsonProperty("myScore")
private BigDecimal myScore;
@Schema(description = "总分(只有所有裁判评分完成后才显示)")
@JsonProperty("totalScore")
private BigDecimal totalScore; private BigDecimal totalScore;
@Schema(description = "已评分裁判数量(裁判长可见)") @Schema(description = "已评分裁判数量")
@JsonProperty("scoredJudgeCount")
private Integer scoredJudgeCount; private Integer scoredJudgeCount;
@Schema(description = "应评分裁判总数")
@JsonProperty("requiredJudgeCount")
private Integer requiredJudgeCount;
@Schema(description = "评分是否完成(所有裁判都已评分)")
@JsonProperty("scoringComplete")
private Boolean scoringComplete = false;
@Schema(description = "比赛状态0-待出场,1-进行中,2-已完成)") @Schema(description = "比赛状态0-待出场,1-进行中,2-已完成)")
@JsonProperty("competitionStatus")
private Integer competitionStatus; private Integer competitionStatus;
} }

View File

@@ -29,10 +29,16 @@ public class ScheduleGroupDetailVO implements Serializable {
private String timeSlot; private String timeSlot;
private Integer timeSlotIndex; // 时间段索引(0=第1天上午,1=第1天下午,2=第2天上午,...) private Integer timeSlotIndex; // 时间段索引(0=第1天上午,1=第1天下午,2=第2天上午,...)
// === 比赛日期 ===
private String scheduleDate;
// === 参赛者信息 === // === 参赛者信息 ===
private Long participantId; private Long participantId;
private String organization; private String organization;
private String checkInStatus; private String checkInStatus;
private String scheduleStatus; private String scheduleStatus;
private Integer performanceOrder; private Integer performanceOrder;
// === 选手姓名 ===
private String playerName;
} }

View File

@@ -0,0 +1,41 @@
package org.springblade.modules.martial.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionAttachment;
import java.util.List;
/**
* 赛事附件 服务类
*
* @author BladeX
*/
public interface IMartialCompetitionAttachmentService extends IService<MartialCompetitionAttachment> {
/**
* 根据赛事ID和附件类型获取附件列表
*
* @param competitionId 赛事ID
* @param attachmentType 附件类型
* @return 附件列表
*/
List<MartialCompetitionAttachment> getByCompetitionIdAndType(Long competitionId, String attachmentType);
/**
* 根据赛事ID获取所有附件
*
* @param competitionId 赛事ID
* @return 附件列表
*/
List<MartialCompetitionAttachment> getByCompetitionId(Long competitionId);
/**
* 删除赛事的指定类型附件
*
* @param competitionId 赛事ID
* @param attachmentType 附件类型
* @return 是否成功
*/
boolean removeByCompetitionIdAndType(Long competitionId, String attachmentType);
}

View File

@@ -0,0 +1,10 @@
package org.springblade.modules.martial.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springblade.modules.martial.pojo.entity.MtVenue;
/**
* 场地 Service 接口
*/
public interface IMtVenueService extends IService<MtVenue> {
}

View File

@@ -0,0 +1,48 @@
package org.springblade.modules.martial.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springblade.modules.martial.mapper.MartialCompetitionAttachmentMapper;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionAttachment;
import org.springblade.modules.martial.service.IMartialCompetitionAttachmentService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 赛事附件 服务实现类
*
* @author BladeX
*/
@Service
public class MartialCompetitionAttachmentServiceImpl extends ServiceImpl<MartialCompetitionAttachmentMapper, MartialCompetitionAttachment> implements IMartialCompetitionAttachmentService {
@Override
public List<MartialCompetitionAttachment> getByCompetitionIdAndType(Long competitionId, String attachmentType) {
LambdaQueryWrapper<MartialCompetitionAttachment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialCompetitionAttachment::getCompetitionId, competitionId)
.eq(MartialCompetitionAttachment::getAttachmentType, attachmentType)
.eq(MartialCompetitionAttachment::getStatus, 1)
.orderByAsc(MartialCompetitionAttachment::getOrderNum);
return this.list(wrapper);
}
@Override
public List<MartialCompetitionAttachment> getByCompetitionId(Long competitionId) {
LambdaQueryWrapper<MartialCompetitionAttachment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialCompetitionAttachment::getCompetitionId, competitionId)
.eq(MartialCompetitionAttachment::getStatus, 1)
.orderByAsc(MartialCompetitionAttachment::getAttachmentType)
.orderByAsc(MartialCompetitionAttachment::getOrderNum);
return this.list(wrapper);
}
@Override
public boolean removeByCompetitionIdAndType(Long competitionId, String attachmentType) {
LambdaQueryWrapper<MartialCompetitionAttachment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialCompetitionAttachment::getCompetitionId, competitionId)
.eq(MartialCompetitionAttachment::getAttachmentType, attachmentType);
return this.remove(wrapper);
}
}

View File

@@ -744,7 +744,7 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
participant.setScheduleDetailId(detailId); participant.setScheduleDetailId(detailId);
participant.setScheduleGroupId(groupId); participant.setScheduleGroupId(groupId);
participant.setParticipantId(athlete.getId()); participant.setParticipantId(athlete.getId());
participant.setOrganization(athlete.getOrganization()); participant.setOrganization(athlete.getTeamName() != null && !athlete.getTeamName().isEmpty() ? athlete.getTeamName() : athlete.getOrganization());
participant.setPlayerName(athlete.getPlayerName()); participant.setPlayerName(athlete.getPlayerName());
participant.setProjectName(groupData.getGroupName()); participant.setProjectName(groupData.getGroupName());
participant.setCategory(athlete.getCategory()); participant.setCategory(athlete.getCategory());

View File

@@ -60,77 +60,69 @@ public class MartialScheduleServiceImpl extends ServiceImpl<MartialScheduleMappe
*/ */
@Override @Override
public List<ScheduleExportExcel> exportSchedule(Long competitionId) { public List<ScheduleExportExcel> exportSchedule(Long competitionId) {
// 1. 查询该赛事的所有赛程
List<MartialSchedule> schedules = this.list(
new QueryWrapper<MartialSchedule>()
.eq("competition_id", competitionId)
.eq("is_deleted", 0)
.orderByAsc("schedule_date", "start_time")
);
List<ScheduleExportExcel> exportList = new ArrayList<>(); List<ScheduleExportExcel> exportList = new ArrayList<>();
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 2. 遍历每个赛程 // 使用与 getScheduleResult 相同的数据源
for (MartialSchedule schedule : schedules) { List<ScheduleGroupDetailVO> details = scheduleGroupMapper.selectScheduleGroupDetails(competitionId);
// 3. 获取该赛程的所有运动员
List<MartialScheduleAthlete> scheduleAthletes = scheduleAthleteService.list(
new QueryWrapper<MartialScheduleAthlete>()
.eq("schedule_id", schedule.getId())
.eq("is_deleted", 0)
.orderByAsc("order_num")
);
// 4. 获取项目和场地信息(一次查询,避免重复) if (details.isEmpty()) {
MartialProject project = schedule.getProjectId() != null return exportList;
? projectService.getById(schedule.getProjectId()) }
: null;
MartialVenue venue = schedule.getVenueId() != null
? venueService.getById(schedule.getVenueId())
: null;
// 5. 如果没有运动员,创建一条基础记录 // 按分组ID分组然后按 displayOrder 排序
if (scheduleAthletes.isEmpty()) { Map<Long, List<ScheduleGroupDetailVO>> groupMap = details.stream()
.collect(Collectors.groupingBy(ScheduleGroupDetailVO::getGroupId));
// 获取分组的排序顺序
List<Long> sortedGroupIds = details.stream()
.collect(Collectors.toMap(
ScheduleGroupDetailVO::getGroupId,
d -> d.getDisplayOrder() != null ? d.getDisplayOrder() : 999,
(a, b) -> a
))
.entrySet().stream()
.sorted(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.collect(Collectors.toList());
// 按排序顺序遍历分组
for (Long groupId : sortedGroupIds) {
List<ScheduleGroupDetailVO> groupDetails = groupMap.get(groupId);
if (groupDetails == null || groupDetails.isEmpty()) {
continue;
}
ScheduleGroupDetailVO firstDetail = groupDetails.get(0);
// 过滤出有参赛者的记录,并按出场顺序排序
List<ScheduleGroupDetailVO> participants = groupDetails.stream()
.filter(d -> d.getParticipantId() != null)
.sorted(Comparator.comparing(d -> d.getPerformanceOrder() != null ? d.getPerformanceOrder() : 999))
.collect(Collectors.toList());
if (participants.isEmpty()) {
// 没有参赛者,创建一条基础记录
ScheduleExportExcel excel = new ScheduleExportExcel(); ScheduleExportExcel excel = new ScheduleExportExcel();
excel.setScheduleDate(schedule.getScheduleDate() != null excel.setScheduleDate(firstDetail.getScheduleDate() != null ? firstDetail.getScheduleDate() : "");
? schedule.getScheduleDate().format(dateFormatter) excel.setTimeSlot(firstDetail.getTimeSlot());
: ""); excel.setVenueName(firstDetail.getVenueName());
excel.setTimeSlot(schedule.getTimeSlot()); excel.setProjectName(firstDetail.getGroupName());
excel.setVenueName(venue != null ? venue.getVenueName() : ""); excel.setCategory(firstDetail.getCategory());
excel.setProjectName(project != null ? project.getProjectName() : ""); excel.setStatus("completed".equals(firstDetail.getScheduleStatus()) ? "已完成" : "草稿");
excel.setCategory(schedule.getGroupTitle());
excel.setStatus(schedule.getIsConfirmed() != null && schedule.getIsConfirmed() == 1
? "已确认" : "未确认");
exportList.add(excel); exportList.add(excel);
} else { } else {
// 6. 为每个运动员创建导出记录 // 为每个参赛者创建导出记录
for (MartialScheduleAthlete scheduleAthlete : scheduleAthletes) { for (ScheduleGroupDetailVO detail : participants) {
MartialAthlete athlete = athleteService.getById(scheduleAthlete.getAthleteId());
if (athlete == null) {
continue;
}
ScheduleExportExcel excel = new ScheduleExportExcel(); ScheduleExportExcel excel = new ScheduleExportExcel();
excel.setScheduleDate(schedule.getScheduleDate() != null excel.setScheduleDate(detail.getScheduleDate() != null ? detail.getScheduleDate() : "");
? schedule.getScheduleDate().format(dateFormatter) excel.setTimeSlot(detail.getTimeSlot());
: ""); excel.setVenueName(detail.getVenueName());
excel.setTimeSlot(schedule.getTimeSlot()); excel.setProjectName(detail.getGroupName());
excel.setVenueName(venue != null ? venue.getVenueName() : ""); excel.setCategory(detail.getCategory());
excel.setProjectName(project != null ? project.getProjectName() : ""); excel.setAthleteName(detail.getPlayerName());
excel.setCategory(schedule.getGroupTitle()); excel.setTeamName(detail.getOrganization());
excel.setAthleteName(athlete.getPlayerName()); excel.setSortOrder(detail.getPerformanceOrder());
excel.setTeamName(athlete.getTeamName()); excel.setStatus(detail.getCheckInStatus() != null ? detail.getCheckInStatus() : "未签到");
excel.setSortOrder(scheduleAthlete.getOrderNum());
// 状态转换
if (scheduleAthlete.getIsCompleted() != null && scheduleAthlete.getIsCompleted() == 1) {
excel.setStatus("已完赛");
} else if (schedule.getIsConfirmed() != null && schedule.getIsConfirmed() == 1) {
excel.setStatus("已确认");
} else {
excel.setStatus("待确认");
}
exportList.add(excel); exportList.add(excel);
} }
} }
@@ -224,6 +216,7 @@ public class MartialScheduleServiceImpl extends ServiceImpl<MartialScheduleMappe
dto.setSchoolUnit(d.getOrganization()); dto.setSchoolUnit(d.getOrganization());
dto.setStatus(d.getCheckInStatus() != null ? d.getCheckInStatus() : "未签到"); dto.setStatus(d.getCheckInStatus() != null ? d.getCheckInStatus() : "未签到");
dto.setSortOrder(d.getPerformanceOrder()); dto.setSortOrder(d.getPerformanceOrder());
dto.setPlayerName(d.getPlayerName());
return dto; return dto;
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());

View File

@@ -285,7 +285,13 @@ public class MartialScoreServiceImpl extends ServiceImpl<MartialScoreMapper, Mar
// 注意:这里假设修改记录存储在选手的 totalScore 和一个额外的字段中 // 注意:这里假设修改记录存储在选手的 totalScore 和一个额外的字段中
// 由于 MartialAthlete 实体没有 originalScore 字段,我们查找修改过的评分记录 // 由于 MartialAthlete 实体没有 originalScore 字段,我们查找修改过的评分记录
MartialScore modifiedScore = scores.stream() MartialScore modifiedScore = scores.stream()
.filter(s -> s.getOriginalScore() != null && s.getModifyReason() != null) .filter(s -> s.getOriginalScore() != null)
.sorted((a, b) -> {
if (a.getModifyTime() == null && b.getModifyTime() == null) return 0;
if (a.getModifyTime() == null) return 1;
if (b.getModifyTime() == null) return -1;
return b.getModifyTime().compareTo(a.getModifyTime());
})
.findFirst() .findFirst()
.orElse(null); .orElse(null);
@@ -326,42 +332,77 @@ public class MartialScoreServiceImpl extends ServiceImpl<MartialScoreMapper, Mar
throw new ServiceException("选手不存在"); throw new ServiceException("选手不存在");
} }
// 2. 验证分数范围 // 2. 验证分数基本范围
if (!validateScore(dto.getModifiedScore())) { if (!validateScore(dto.getModifiedScore())) {
throw new ServiceException("修改后的分数必须在5.000-10.000之间"); throw new ServiceException("修改后的分数必须在5.000-10.000之间");
} }
// 3. 保存原始总分(如果是第一次修改) // 3. 查找是否已存在裁判长的修改记录
BigDecimal originalTotalScore = athlete.getTotalScore(); LambdaQueryWrapper<MartialScore> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(MartialScore::getAthleteId, dto.getAthleteId())
.like(MartialScore::getJudgeName, "裁判长修改");
MartialScore existingRecord = this.getOne(queryWrapper);
// 4. 更新选手总分 // 4. 确定原始计算总分(用于范围验证)
// 如果已有修改记录,使用记录中保存的原始分数;否则使用当前总分
BigDecimal originalCalculatedScore;
if (existingRecord != null && existingRecord.getOriginalScore() != null) {
// 已有修改记录,使用记录中的原始计算总分
originalCalculatedScore = existingRecord.getOriginalScore();
} else {
// 首次修改,当前总分就是原始计算总分
originalCalculatedScore = athlete.getTotalScore();
}
// 5. 验证修改范围基于原始计算总分±0.050
if (originalCalculatedScore != null) {
BigDecimal minAllowed = originalCalculatedScore.subtract(new BigDecimal("0.050"));
BigDecimal maxAllowed = originalCalculatedScore.add(new BigDecimal("0.050"));
if (dto.getModifiedScore().compareTo(minAllowed) < 0 || dto.getModifiedScore().compareTo(maxAllowed) > 0) {
throw new ServiceException("修改分数只能在原始计算总分(" + originalCalculatedScore + ")±0.050范围内");
}
}
// 6. 更新选手总分
athlete.setTotalScore(dto.getModifiedScore()); athlete.setTotalScore(dto.getModifiedScore());
boolean athleteUpdated = athleteService.updateById(athlete); boolean athleteUpdated = athleteService.updateById(athlete);
// 5. 记录修改日志(可以新增一条特殊的评分记录,或更新现有记录) boolean recordSaved;
// 这里选择创建一条新的修改记录 if (existingRecord != null) {
MartialScore modificationRecord = new MartialScore(); // 7a. 更新现有的裁判长修改记录(保持原始计算总分不变)
modificationRecord.setCompetitionId(athlete.getCompetitionId()); existingRecord.setScore(dto.getModifiedScore());
modificationRecord.setAthleteId(athlete.getId()); // originalScore 保持不变,始终是系统计算的原始总分
modificationRecord.setProjectId(athlete.getProjectId()); existingRecord.setModifyReason(dto.getNote());
modificationRecord.setJudgeId(dto.getModifierId()); existingRecord.setModifyTime(LocalDateTime.now());
modificationRecord.setScore(dto.getModifiedScore()); // 直接使用 baseMapper.updateById 绕过 Service 层的状态检查,裁判长可以无限次修改
modificationRecord.setOriginalScore(originalTotalScore); recordSaved = this.baseMapper.updateById(existingRecord) > 0;
modificationRecord.setModifyReason(dto.getNote()); log.info("裁判长更新评分记录 - 选手ID:{}, 姓名:{}, 原始计算总分:{}, 修改后总分:{}, 修改原因:{}",
modificationRecord.setModifyTime(LocalDateTime.now()); athlete.getId(), athlete.getPlayerName(), originalCalculatedScore, dto.getModifiedScore(), dto.getNote());
modificationRecord.setScoreTime(LocalDateTime.now()); } else {
// 7b. 创建新的裁判长修改记录
MartialScore modificationRecord = new MartialScore();
modificationRecord.setCompetitionId(athlete.getCompetitionId());
modificationRecord.setAthleteId(athlete.getId());
modificationRecord.setProjectId(athlete.getProjectId());
modificationRecord.setVenueId(dto.getVenueId()); // 设置场地ID
modificationRecord.setJudgeId(dto.getModifierId());
modificationRecord.setScore(dto.getModifiedScore());
modificationRecord.setOriginalScore(originalCalculatedScore); // 保存原始计算总分
modificationRecord.setModifyReason(dto.getNote());
modificationRecord.setModifyTime(LocalDateTime.now());
modificationRecord.setScoreTime(LocalDateTime.now());
// 查询修改者信息 // 查询修改者信息
MartialJudge modifier = judgeService.getById(dto.getModifierId()); MartialJudge modifier = judgeService.getById(dto.getModifierId());
if (modifier != null) { if (modifier != null) {
modificationRecord.setJudgeName(modifier.getName() + "(裁判长修改)"); modificationRecord.setJudgeName(modifier.getName() + "(裁判长修改)");
}
recordSaved = this.save(modificationRecord);
log.info("裁判长新增评分记录 - 选手ID:{}, 姓名:{}, 原始计算总分:{}, 修改后总分:{}, 修改原因:{}",
athlete.getId(), athlete.getPlayerName(), originalCalculatedScore, dto.getModifiedScore(), dto.getNote());
} }
boolean recordSaved = this.save(modificationRecord);
log.info("裁判长修改评分 - 选手ID:{}, 姓名:{}, 原始总分:{}, 修改后总分:{}, 修改原因:{}",
athlete.getId(), athlete.getPlayerName(), originalTotalScore, dto.getModifiedScore(), dto.getNote());
return athleteUpdated && recordSaved; return athleteUpdated && recordSaved;
} }

View File

@@ -0,0 +1,374 @@
package org.springblade.modules.martial.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.modules.martial.pojo.dto.MiniScoreModifyDTO;
import org.springblade.modules.martial.pojo.entity.MartialAthlete;
import org.springblade.modules.martial.pojo.entity.MartialJudge;
import org.springblade.modules.martial.pojo.entity.MartialScore;
import org.springblade.modules.martial.mapper.MartialScoreMapper;
import org.springblade.modules.martial.pojo.vo.MiniScoreDetailVO;
import org.springblade.modules.martial.service.IMartialAthleteService;
import org.springblade.modules.martial.service.IMartialJudgeProjectService;
import org.springblade.modules.martial.service.IMartialJudgeService;
import org.springblade.modules.martial.service.IMartialScoreService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* Score 服务实现类
*
* @author BladeX
*/
@Slf4j
@Service
public class MartialScoreServiceImpl extends ServiceImpl<MartialScoreMapper, MartialScore> implements IMartialScoreService {
@Autowired
private IMartialJudgeProjectService judgeProjectService;
@Autowired
private IMartialAthleteService athleteService;
@Autowired
private IMartialJudgeService judgeService;
/** 最低分 */
private static final BigDecimal MIN_SCORE = new BigDecimal("5.000");
/** 最高分 */
private static final BigDecimal MAX_SCORE = new BigDecimal("10.000");
/** 异常分数偏差阈值(偏离平均分超过此值报警) */
private static final BigDecimal ANOMALY_THRESHOLD = new BigDecimal("1.000");
/**
* Task 2.2: 验证分数范围
*
* @param score 分数
* @return 是否有效
*/
public boolean validateScore(BigDecimal score) {
if (score == null) {
return false;
}
return score.compareTo(MIN_SCORE) >= 0 && score.compareTo(MAX_SCORE) <= 0;
}
/**
* Task 2.2 & 2.5: 保存评分(带验证和权限检查)
*
* @param score 评分记录
* @return 是否成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean save(MartialScore score) {
// Task 2.5: 权限验证 - 裁判只能给被分配的项目打分
// 注意:如果 martial_judge_project 表中没有数据,可以临时注释掉权限验证
boolean hasPermission = judgeProjectService.hasPermission(score.getJudgeId(), score.getProjectId());
if (!hasPermission) {
log.warn("⚠️ 权限验证失败 - 裁判ID:{}, 项目ID:{} (如果是测试环境,请在 martial_judge_project 表中添加关联记录)",
score.getJudgeId(), score.getProjectId());
// 临时允许通过,但记录警告日志
// 生产环境请取消注释下面这行
// throw new ServiceException("您没有权限给该项目打分");
}
// Task 2.2: 验证分数范围
if (!validateScore(score.getScore())) {
throw new ServiceException("分数必须在5.000-10.000之间");
}
// Task 2.3: 检查异常分数
checkAnomalyScore(score);
return super.save(score);
}
/**
* Task 2.5: 更新评分(禁止修改已提交的成绩)
*
* @param score 评分记录
* @return 是否成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateById(MartialScore score) {
// 检查原记录状态
MartialScore existing = this.getById(score.getId());
if (existing == null) {
throw new ServiceException("评分记录不存在");
}
// Task 2.5: 已提交的成绩不能修改status=1表示正常已提交
if (existing.getStatus() != null && existing.getStatus() == 1) {
log.error("❌ 禁止修改 - 评分ID:{}, 裁判:{}, 状态:已提交",
score.getId(), existing.getJudgeName());
throw new ServiceException("已提交的评分不能修改");
}
// Task 2.5: 权限验证
if (!judgeProjectService.hasPermission(score.getJudgeId(), score.getProjectId())) {
throw new ServiceException("您没有权限修改该项目的评分");
}
// 验证分数范围
if (!validateScore(score.getScore())) {
throw new ServiceException("分数必须在5.000-10.000之间");
}
// 标记为已修改
score.setStatus(2);
return super.updateById(score);
}
/**
* Task 2.3: 检测异常分数
*
* @param newScore 新评分
*/
public void checkAnomalyScore(MartialScore newScore) {
// 获取同一运动员的其他裁判评分
List<MartialScore> scores = this.list(
new QueryWrapper<MartialScore>()
.eq("athlete_id", newScore.getAthleteId())
.eq("project_id", newScore.getProjectId())
.ne("judge_id", newScore.getJudgeId())
.eq("is_deleted", 0)
);
if (scores.size() < 2) {
return; // 评分数量不足,无法判断
}
// 计算其他裁判的平均分
BigDecimal avgScore = scores.stream()
.map(MartialScore::getScore)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.divide(new BigDecimal(scores.size()), 3, RoundingMode.HALF_UP);
// 判断偏差
BigDecimal diff = newScore.getScore().subtract(avgScore).abs();
if (diff.compareTo(ANOMALY_THRESHOLD) > 0) {
// 偏差超过阈值,记录警告
log.warn("⚠️ 异常评分检测 - 裁判:{}(ID:{}), 运动员ID:{}, 评分:{}, 其他裁判平均分:{}, 偏差:{}",
newScore.getJudgeName(),
newScore.getJudgeId(),
newScore.getAthleteId(),
newScore.getScore(),
avgScore,
diff);
}
}
/**
* Task 2.3: 获取异常评分列表
*
* @param athleteId 运动员ID
* @param projectId 项目ID
* @return 异常评分列表
*/
public List<MartialScore> getAnomalyScores(Long athleteId, Long projectId) {
// 获取该运动员的所有评分
List<MartialScore> scores = this.list(
new QueryWrapper<MartialScore>()
.eq("athlete_id", athleteId)
.eq("project_id", projectId)
.eq("is_deleted", 0)
.orderByDesc("score")
);
if (scores.size() < 3) {
return List.of(); // 评分数量不足,无异常
}
// 计算平均分
BigDecimal avgScore = scores.stream()
.map(MartialScore::getScore)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.divide(new BigDecimal(scores.size()), 3, RoundingMode.HALF_UP);
// 筛选偏差大于阈值的评分
return scores.stream()
.filter(score -> {
BigDecimal diff = score.getScore().subtract(avgScore).abs();
return diff.compareTo(ANOMALY_THRESHOLD) > 0;
})
.toList();
}
/**
* Task 2.2: 批量验证评分
*
* @param athleteId 运动员ID
* @param projectId 项目ID
* @return 验证结果
*/
public boolean validateScores(Long athleteId, Long projectId) {
List<MartialScore> scores = this.list(
new QueryWrapper<MartialScore>()
.eq("athlete_id", athleteId)
.eq("project_id", projectId)
.eq("is_deleted", 0)
);
if (scores.isEmpty()) {
return false;
}
for (MartialScore score : scores) {
if (!validateScore(score.getScore())) {
log.error("分数验证失败 - 裁判:{}, 分数:{}", score.getJudgeName(), score.getScore());
return false;
}
}
return true;
}
/**
* 小程序接口:获取评分详情
*
* @param athleteId 选手ID
* @return 评分详情(选手信息+所有评委评分+修改记录)
*/
@Override
public MiniScoreDetailVO getScoreDetailForMini(Long athleteId) {
MiniScoreDetailVO vo = new MiniScoreDetailVO();
// 1. 查询选手信息
MartialAthlete athlete = athleteService.getById(athleteId);
if (athlete == null) {
throw new ServiceException("选手不存在");
}
MiniScoreDetailVO.AthleteInfo athleteInfo = new MiniScoreDetailVO.AthleteInfo();
athleteInfo.setAthleteId(athlete.getId());
athleteInfo.setName(athlete.getPlayerName());
athleteInfo.setIdCard(athlete.getIdCard());
athleteInfo.setTeam(athlete.getTeamName());
athleteInfo.setNumber(athlete.getPlayerNo());
athleteInfo.setTotalScore(athlete.getTotalScore());
vo.setAthleteInfo(athleteInfo);
// 2. 查询所有评委的评分
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getAthleteId, athleteId);
scoreQuery.eq(MartialScore::getIsDeleted, 0);
scoreQuery.orderByAsc(MartialScore::getScoreTime);
List<MartialScore> scores = this.list(scoreQuery);
List<MiniScoreDetailVO.JudgeScore> judgeScores = scores.stream().map(score -> {
MiniScoreDetailVO.JudgeScore judgeScore = new MiniScoreDetailVO.JudgeScore();
judgeScore.setJudgeId(score.getJudgeId());
judgeScore.setJudgeName(score.getJudgeName());
judgeScore.setScore(score.getScore());
judgeScore.setScoreTime(score.getScoreTime());
judgeScore.setNote(score.getNote());
return judgeScore;
}).collect(Collectors.toList());
vo.setJudgeScores(judgeScores);
// 3. 查询裁判长修改记录(检查选手的 originalScore 字段)
// 注意:这里假设修改记录存储在选手的 totalScore 和一个额外的字段中
// 由于 MartialAthlete 实体没有 originalScore 字段,我们查找修改过的评分记录
MartialScore modifiedScore = scores.stream()
.filter(s -> s.getOriginalScore() != null)
.sorted((a, b) -> {
if (a.getModifyTime() == null && b.getModifyTime() == null) return 0;
if (a.getModifyTime() == null) return 1;
if (b.getModifyTime() == null) return -1;
return b.getModifyTime().compareTo(a.getModifyTime());
})
.findFirst()
.orElse(null);
if (modifiedScore != null) {
MiniScoreDetailVO.Modification modification = new MiniScoreDetailVO.Modification();
modification.setOriginalScore(modifiedScore.getOriginalScore());
modification.setModifiedScore(modifiedScore.getScore());
modification.setModifierId(modifiedScore.getUpdateUser());
modification.setModifyReason(modifiedScore.getModifyReason());
modification.setModifyTime(modifiedScore.getModifyTime());
// 查询修改者姓名
if (modifiedScore.getUpdateUser() != null) {
MartialJudge modifier = judgeService.getById(modifiedScore.getUpdateUser());
if (modifier != null) {
modification.setModifierName(modifier.getName());
}
}
vo.setModification(modification);
}
return vo;
}
/**
* 小程序接口:修改评分(裁判长)
*
* @param dto 修改信息
* @return 修改成功/失败
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean modifyScoreByAdmin(MiniScoreModifyDTO dto) {
// 1. 查询选手信息
MartialAthlete athlete = athleteService.getById(dto.getAthleteId());
if (athlete == null) {
throw new ServiceException("选手不存在");
}
// 2. 验证分数范围
if (!validateScore(dto.getModifiedScore())) {
throw new ServiceException("修改后的分数必须在5.000-10.000之间");
}
// 3. 保存原始总分(如果是第一次修改)
BigDecimal originalTotalScore = athlete.getTotalScore();
// 4. 更新选手总分
athlete.setTotalScore(dto.getModifiedScore());
boolean athleteUpdated = athleteService.updateById(athlete);
// 5. 记录修改日志(可以新增一条特殊的评分记录,或更新现有记录)
// 这里选择创建一条新的修改记录
MartialScore modificationRecord = new MartialScore();
modificationRecord.setCompetitionId(athlete.getCompetitionId());
modificationRecord.setAthleteId(athlete.getId());
modificationRecord.setProjectId(athlete.getProjectId());
modificationRecord.setJudgeId(dto.getModifierId());
modificationRecord.setScore(dto.getModifiedScore());
modificationRecord.setOriginalScore(originalTotalScore);
modificationRecord.setModifyReason(dto.getNote());
modificationRecord.setModifyTime(LocalDateTime.now());
modificationRecord.setScoreTime(LocalDateTime.now());
// 查询修改者信息
MartialJudge modifier = judgeService.getById(dto.getModifierId());
if (modifier != null) {
modificationRecord.setJudgeName(modifier.getName() + "(裁判长修改)");
}
boolean recordSaved = this.save(modificationRecord);
log.info("裁判长修改评分 - 选手ID:{}, 姓名:{}, 原始总分:{}, 修改后总分:{}, 修改原因:{}",
athlete.getId(), athlete.getPlayerName(), originalTotalScore, dto.getModifiedScore(), dto.getNote());
return athleteUpdated && recordSaved;
}
}

View File

@@ -0,0 +1,14 @@
package org.springblade.modules.martial.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springblade.modules.martial.mapper.MtVenueMapper;
import org.springblade.modules.martial.pojo.entity.MtVenue;
import org.springblade.modules.martial.service.IMtVenueService;
import org.springframework.stereotype.Service;
/**
* 场地 Service 实现类
*/
@Service
public class MtVenueServiceImpl extends ServiceImpl<MtVenueMapper, MtVenue> implements IMtVenueService {
}