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

This commit is contained in:
2025-12-12 05:13:10 +08:00
parent 1c981a2fb7
commit 7aa6545cbb
82 changed files with 8495 additions and 28 deletions

View File

@@ -0,0 +1,96 @@
package org.springblade.job.processor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.modules.martial.service.IMartialScheduleArrangeService;
import org.springframework.stereotype.Component;
import tech.powerjob.worker.core.processor.ProcessResult;
import tech.powerjob.worker.core.processor.TaskContext;
import tech.powerjob.worker.core.processor.sdk.BasicProcessor;
import tech.powerjob.worker.log.OmsLogger;
import java.util.List;
/**
* 赛程自动编排定时任务处理器
* <p>
* 任务说明:
* 1. 每10分钟执行一次自动编排
* 2. 查询所有未锁定的赛事(schedule_status != 2)
* 3. 对每个赛事执行自动编排算法
* 4. 更新编排状态和最后编排时间
* <p>
* 配置方式:
* 在PowerJob控制台配置定时任务:
* - 任务名称: 赛程自动编排
* - 执行类型: BASIC
* - 处理器: org.springblade.job.processor.ScheduleAutoArrangeProcessor
* - Cron表达式: 0 * /10 * * * ? (每10分钟执行一次)
* - 最大实例数: 1 (避免并发)
*
* @author BladeX
**/
@Slf4j
@Component
@RequiredArgsConstructor
public class ScheduleAutoArrangeProcessor implements BasicProcessor {
private final IMartialScheduleArrangeService scheduleArrangeService;
@Override
public ProcessResult process(TaskContext context) {
OmsLogger omsLogger = context.getOmsLogger();
omsLogger.info("赛程自动编排任务开始执行...");
try {
// 1. 查询所有未锁定的赛事
List<Long> unlockedCompetitions = scheduleArrangeService.getUnlockedCompetitions();
if (unlockedCompetitions.isEmpty()) {
omsLogger.info("没有需要编排的赛事");
return new ProcessResult(true, "没有需要编排的赛事");
}
omsLogger.info("找到 {} 个需要编排的赛事: {}", unlockedCompetitions.size(), unlockedCompetitions);
// 2. 对每个赛事执行自动编排
int successCount = 0;
int failCount = 0;
StringBuilder errorMsg = new StringBuilder();
for (Long competitionId : unlockedCompetitions) {
try {
omsLogger.info("开始编排赛事: {}", competitionId);
scheduleArrangeService.autoArrange(competitionId);
successCount++;
omsLogger.info("赛事 {} 编排成功", competitionId);
} catch (Exception e) {
failCount++;
String error = String.format("赛事 %d 编排失败: %s", competitionId, e.getMessage());
omsLogger.error(error, e);
errorMsg.append(error).append("; ");
}
}
// 3. 返回执行结果
String result = String.format("自动编排任务完成. 成功: %d, 失败: %d. %s",
successCount, failCount, errorMsg.toString());
omsLogger.info(result);
// 如果有失败的,返回部分成功
if (failCount > 0) {
return new ProcessResult(true, result);
}
return new ProcessResult(true, result);
} catch (Exception e) {
String errorMsg = "赛程自动编排任务执行失败: " + e.getMessage();
omsLogger.error(errorMsg, e);
log.error(errorMsg, e);
return new ProcessResult(false, errorMsg);
}
}
}

View File

@@ -10,6 +10,7 @@ import org.springblade.core.mp.support.Query;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.modules.martial.pojo.entity.MartialAthlete;
import org.springblade.modules.martial.pojo.vo.MartialAthleteVO;
import org.springblade.modules.martial.service.IMartialAthleteService;
import org.springframework.web.bind.annotation.*;
@@ -37,12 +38,12 @@ public class MartialAthleteController extends BladeController {
}
/**
* 分页列表
* 分页列表(包含关联字段)
*/
@GetMapping("/list")
@Operation(summary = "分页列表", description = "分页查询")
public R<IPage<MartialAthlete>> list(MartialAthlete athlete, Query query) {
IPage<MartialAthlete> pages = athleteService.page(Condition.getPage(query), Condition.getQueryWrapper(athlete));
public R<IPage<MartialAthleteVO>> list(MartialAthlete athlete, Query query) {
IPage<MartialAthleteVO> pages = athleteService.selectAthleteVOPage(Condition.getPage(query), athlete);
return R.data(pages);
}

View File

@@ -0,0 +1,143 @@
package org.springblade.modules.martial.controller;
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.modules.martial.pojo.entity.MartialCompetitionRulesAttachment;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionRulesChapter;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionRulesContent;
import org.springblade.modules.martial.pojo.vo.MartialCompetitionRulesVO;
import org.springblade.modules.martial.service.IMartialCompetitionRulesService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 赛事规程 控制器
*
* @author BladeX
*/
@RestController
@AllArgsConstructor
@RequestMapping("/martial/competition/rules")
@Tag(name = "赛事规程管理", description = "赛事规程管理接口")
public class MartialCompetitionRulesController extends BladeController {
private final IMartialCompetitionRulesService rulesService;
/**
* 获取赛事规程(小程序端)
*/
@GetMapping("")
@Operation(summary = "获取赛事规程", description = "小程序端获取规程信息")
public R<MartialCompetitionRulesVO> getRules(@RequestParam Long competitionId) {
MartialCompetitionRulesVO rules = rulesService.getRulesByCompetitionId(competitionId);
return R.data(rules);
}
// ==================== 附件管理 ====================
/**
* 获取附件列表
*/
@GetMapping("/attachment/list")
@Operation(summary = "获取附件列表", description = "管理端获取附件列表")
public R<List<MartialCompetitionRulesAttachment>> getAttachmentList(@RequestParam Long competitionId) {
List<MartialCompetitionRulesAttachment> list = rulesService.getAttachmentList(competitionId);
return R.data(list);
}
/**
* 保存附件
*/
@PostMapping("/attachment/save")
@Operation(summary = "保存附件", description = "新增或修改附件")
public R saveAttachment(@RequestBody MartialCompetitionRulesAttachment attachment) {
return R.status(rulesService.saveAttachment(attachment));
}
/**
* 删除附件
*/
@PostMapping("/attachment/remove")
@Operation(summary = "删除附件", description = "传入附件ID")
public R removeAttachment(@RequestParam Long id) {
return R.status(rulesService.removeAttachment(id));
}
// ==================== 章节管理 ====================
/**
* 获取章节列表
*/
@GetMapping("/chapter/list")
@Operation(summary = "获取章节列表", description = "管理端获取章节列表")
public R<List<MartialCompetitionRulesChapter>> getChapterList(@RequestParam Long competitionId) {
List<MartialCompetitionRulesChapter> list = rulesService.getChapterList(competitionId);
return R.data(list);
}
/**
* 保存章节
*/
@PostMapping("/chapter/save")
@Operation(summary = "保存章节", description = "新增或修改章节")
public R saveChapter(@RequestBody MartialCompetitionRulesChapter chapter) {
return R.status(rulesService.saveChapter(chapter));
}
/**
* 删除章节
*/
@PostMapping("/chapter/remove")
@Operation(summary = "删除章节", description = "传入章节ID")
public R removeChapter(@RequestParam Long id) {
return R.status(rulesService.removeChapter(id));
}
// ==================== 章节内容管理 ====================
/**
* 获取章节内容列表
*/
@GetMapping("/content/list")
@Operation(summary = "获取章节内容列表", description = "管理端获取章节内容")
public R<List<MartialCompetitionRulesContent>> getContentList(@RequestParam Long chapterId) {
List<MartialCompetitionRulesContent> list = rulesService.getContentList(chapterId);
return R.data(list);
}
/**
* 保存章节内容
*/
@PostMapping("/content/save")
@Operation(summary = "保存章节内容", description = "新增或修改章节内容")
public R saveContent(@RequestBody MartialCompetitionRulesContent content) {
return R.status(rulesService.saveContent(content));
}
/**
* 批量保存章节内容
*/
@PostMapping("/content/batch-save")
@Operation(summary = "批量保存章节内容", description = "批量保存章节内容")
public R batchSaveContents(@RequestBody Map<String, Object> params) {
Long chapterId = Long.valueOf(params.get("chapterId").toString());
@SuppressWarnings("unchecked")
List<String> contents = (List<String>) params.get("contents");
return R.status(rulesService.batchSaveContents(chapterId, contents));
}
/**
* 删除章节内容
*/
@PostMapping("/content/remove")
@Operation(summary = "删除章节内容", description = "传入内容ID")
public R removeContent(@RequestParam Long id) {
return R.status(rulesService.removeContent(id));
}
}

View File

@@ -10,9 +10,12 @@ 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.MartialJudgeInvite;
import org.springblade.modules.martial.pojo.vo.MartialJudgeInviteVO;
import org.springblade.modules.martial.service.IMartialJudgeInviteService;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 裁判邀请码 控制器
*
@@ -37,12 +40,12 @@ public class MartialJudgeInviteController extends BladeController {
}
/**
* 分页列表
* 分页列表(关联裁判信息)
*/
@GetMapping("/list")
@Operation(summary = "分页列表", description = "分页查询")
public R<IPage<MartialJudgeInvite>> list(MartialJudgeInvite judgeInvite, Query query) {
IPage<MartialJudgeInvite> pages = judgeInviteService.page(Condition.getPage(query), Condition.getQueryWrapper(judgeInvite));
@Operation(summary = "分页列表", description = "分页查询,关联裁判信息")
public R<IPage<MartialJudgeInviteVO>> list(MartialJudgeInvite judgeInvite, Query query) {
IPage<MartialJudgeInviteVO> pages = judgeInviteService.selectJudgeInvitePage(judgeInvite, query);
return R.data(pages);
}
@@ -64,4 +67,14 @@ public class MartialJudgeInviteController extends BladeController {
return R.status(judgeInviteService.removeByIds(Func.toLongList(ids)));
}
/**
* 获取邀请统计信息
*/
@GetMapping("/statistics")
@Operation(summary = "邀请统计", description = "传入赛事ID")
public R<Map<String, Object>> statistics(@RequestParam Long competitionId) {
Map<String, Object> statistics = judgeInviteService.getInviteStatistics(competitionId);
return R.data(statistics);
}
}

View File

@@ -77,7 +77,7 @@ public class MartialMiniController extends BladeController {
}
// 4. 验证比赛编码
if (!competition.getCode().equals(dto.getMatchCode())) {
if (!competition.getCompetitionCode().equals(dto.getMatchCode())) {
return R.fail("比赛编码不匹配");
}
@@ -111,13 +111,13 @@ public class MartialMiniController extends BladeController {
vo.setToken(token);
vo.setUserRole("chief_judge".equals(invite.getRole()) ? "admin" : "pub");
vo.setMatchId(competition.getId());
vo.setMatchName(competition.getName());
vo.setMatchTime(competition.getStartTime() != null ?
competition.getStartTime().toString() : "");
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.getName() : null);
vo.setVenueName(venue != null ? venue.getVenueName() : null);
vo.setProjects(projects);
return R.data(vo);
@@ -210,7 +210,7 @@ public class MartialMiniController extends BladeController {
projects = projectList.stream().map(project -> {
MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo();
info.setProjectId(project.getId());
info.setProjectName(project.getName());
info.setProjectName(project.getProjectName());
return info;
}).collect(Collectors.toList());
}
@@ -228,7 +228,7 @@ public class MartialMiniController extends BladeController {
projects = projectList.stream().map(project -> {
MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo();
info.setProjectId(project.getId());
info.setProjectName(project.getName());
info.setProjectName(project.getProjectName());
return info;
}).collect(Collectors.toList());
}

View File

@@ -0,0 +1,121 @@
package org.springblade.modules.martial.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.secure.BladeUser;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.R;
import org.springblade.modules.martial.pojo.dto.MoveScheduleGroupDTO;
import org.springblade.modules.martial.pojo.dto.SaveScheduleDraftDTO;
import org.springblade.modules.martial.pojo.dto.ScheduleResultDTO;
import org.springblade.modules.martial.service.IMartialScheduleArrangeService;
import org.springblade.modules.martial.service.IMartialScheduleService;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 赛程自动编排 控制器
*
* @author BladeX
*/
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/martial/schedule")
@Tag(name = "赛程编排管理", description = "赛程自动编排接口")
public class MartialScheduleArrangeController extends BladeController {
private final IMartialScheduleArrangeService scheduleArrangeService;
private final IMartialScheduleService scheduleService;
/**
* 获取编排结果
*/
@GetMapping("/result")
@Operation(summary = "获取编排结果", description = "传入赛事ID")
public R<ScheduleResultDTO> getScheduleResult(@RequestParam Long competitionId) {
try {
ScheduleResultDTO result = scheduleService.getScheduleResult(competitionId);
return R.data(result);
} catch (Exception e) {
log.error("获取编排结果失败, competitionId: {}", competitionId, e);
return R.fail("获取编排结果失败: " + e.getMessage());
}
}
/**
* 保存编排草稿
*/
@PostMapping("/save-draft")
@Operation(summary = "保存编排草稿", description = "传入编排草稿数据")
public R saveDraftSchedule(@RequestBody SaveScheduleDraftDTO dto) {
try {
boolean success = scheduleService.saveDraftSchedule(dto);
return success ? R.success("草稿保存成功") : R.fail("草稿保存失败");
} catch (Exception e) {
log.error("保存编排草稿失败", e);
return R.fail("保存编排草稿失败: " + e.getMessage());
}
}
/**
* 完成编排并锁定
*/
@PostMapping("/save-and-lock")
@Operation(summary = "完成编排并锁定", description = "传入赛事ID")
public R saveAndLock(@RequestBody SaveScheduleDraftDTO dto) {
try {
// 获取当前登录用户
BladeUser user = AuthUtil.getUser();
String userId = user != null ? user.getUserName() : "system";
boolean success = scheduleService.saveAndLockSchedule(dto.getCompetitionId());
if (success) {
// 调用原有的锁定逻辑
scheduleArrangeService.saveAndLock(dto.getCompetitionId(), userId);
return R.success("编排已完成并锁定");
} else {
return R.fail("编排锁定失败");
}
} catch (Exception e) {
log.error("保存并锁定编排失败", e);
return R.fail("保存并锁定编排失败: " + e.getMessage());
}
}
/**
* 手动触发自动编排(测试用)
*/
@PostMapping("/auto-arrange")
@Operation(summary = "手动触发自动编排", description = "传入赛事ID,仅用于测试")
public R autoArrange(@RequestBody Map<String, Object> params) {
try {
Long competitionId = Long.valueOf(String.valueOf(params.get("competitionId")));
scheduleArrangeService.autoArrange(competitionId);
return R.success("自动编排完成");
} catch (Exception e) {
log.error("自动编排失败", e);
return R.fail("自动编排失败: " + e.getMessage());
}
}
/**
* 移动赛程分组
*/
@PostMapping("/move-group")
@Operation(summary = "移动赛程分组", description = "将分组移动到指定场地和时间段")
public R moveGroup(@RequestBody MoveScheduleGroupDTO dto) {
try {
boolean success = scheduleService.moveScheduleGroup(dto);
return success ? R.success("分组移动成功") : R.fail("分组移动失败");
} catch (Exception e) {
log.error("移动分组失败", e);
return R.fail("移动分组失败: " + e.getMessage());
}
}
}

View File

@@ -9,6 +9,8 @@ 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.dto.SaveScheduleDraftDTO;
import org.springblade.modules.martial.pojo.dto.ScheduleResultDTO;
import org.springblade.modules.martial.pojo.entity.MartialSchedule;
import org.springblade.modules.martial.service.IMartialScheduleService;
import org.springframework.web.bind.annotation.*;

View File

@@ -1,7 +1,10 @@
package org.springblade.modules.martial.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Param;
import org.springblade.modules.martial.pojo.entity.MartialAthlete;
import org.springblade.modules.martial.pojo.vo.MartialAthleteVO;
/**
* Athlete Mapper 接口
@@ -10,4 +13,13 @@ import org.springblade.modules.martial.pojo.entity.MartialAthlete;
*/
public interface MartialAthleteMapper extends BaseMapper<MartialAthlete> {
/**
* 分页查询参赛选手(包含关联字段)
*
* @param page 分页对象
* @param athlete 查询条件
* @return 参赛选手VO分页数据
*/
IPage<MartialAthleteVO> selectAthleteVOPage(IPage<MartialAthleteVO> page, @Param("athlete") MartialAthlete athlete);
}

View File

@@ -2,4 +2,44 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.modules.martial.mapper.MartialAthleteMapper">
<!-- 分页查询参赛选手(包含关联字段) -->
<select id="selectAthleteVOPage" resultType="org.springblade.modules.martial.pojo.vo.MartialAthleteVO">
SELECT
a.*,
c.competition_name as competitionName,
p.project_name as projectName
FROM martial_athlete a
LEFT JOIN martial_competition c ON a.competition_id = c.id AND c.is_deleted = 0
LEFT JOIN martial_project p ON a.project_id = p.id AND p.is_deleted = 0
WHERE a.is_deleted = 0
<if test="athlete.competitionId != null">
AND a.competition_id = #{athlete.competitionId}
</if>
<if test="athlete.projectId != null">
AND a.project_id = #{athlete.projectId}
</if>
<if test="athlete.playerName != null and athlete.playerName != ''">
AND a.player_name LIKE CONCAT('%', #{athlete.playerName}, '%')
</if>
<if test="athlete.playerNo != null and athlete.playerNo != ''">
AND a.player_no = #{athlete.playerNo}
</if>
<if test="athlete.gender != null">
AND a.gender = #{athlete.gender}
</if>
<if test="athlete.organization != null and athlete.organization != ''">
AND a.organization LIKE CONCAT('%', #{athlete.organization}, '%')
</if>
<if test="athlete.category != null and athlete.category != ''">
AND a.category = #{athlete.category}
</if>
<if test="athlete.registrationStatus != null">
AND a.registration_status = #{athlete.registrationStatus}
</if>
<if test="athlete.competitionStatus != null">
AND a.competition_status = #{athlete.competitionStatus}
</if>
ORDER BY a.create_time DESC
</select>
</mapper>

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.MartialCompetitionRulesAttachment;
/**
* 赛事规程附件 Mapper 接口
*
* @author BladeX
*/
public interface MartialCompetitionRulesAttachmentMapper extends BaseMapper<MartialCompetitionRulesAttachment> {
}

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.MartialCompetitionRulesChapter;
/**
* 赛事规程章节 Mapper 接口
*
* @author BladeX
*/
public interface MartialCompetitionRulesChapterMapper extends BaseMapper<MartialCompetitionRulesChapter> {
}

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.MartialCompetitionRulesContent;
/**
* 赛事规程内容 Mapper 接口
*
* @author BladeX
*/
public interface MartialCompetitionRulesContentMapper extends BaseMapper<MartialCompetitionRulesContent> {
}

View File

@@ -1,7 +1,10 @@
package org.springblade.modules.martial.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Param;
import org.springblade.modules.martial.pojo.entity.MartialJudgeInvite;
import org.springblade.modules.martial.pojo.vo.MartialJudgeInviteVO;
/**
* JudgeInvite Mapper 接口
@@ -10,4 +13,13 @@ import org.springblade.modules.martial.pojo.entity.MartialJudgeInvite;
*/
public interface MartialJudgeInviteMapper extends BaseMapper<MartialJudgeInvite> {
/**
* 分页查询裁判邀请列表(关联裁判信息)
*
* @param page 分页对象
* @param judgeInvite 查询条件
* @return 裁判邀请VO分页列表
*/
IPage<MartialJudgeInviteVO> selectJudgeInvitePage(IPage<MartialJudgeInviteVO> page, @Param("judgeInvite") MartialJudgeInvite judgeInvite);
}

View File

@@ -2,4 +2,99 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.modules.martial.mapper.MartialJudgeInviteMapper">
<!-- 裁判邀请VO结果映射 -->
<resultMap id="judgeInviteVOResultMap" type="org.springblade.modules.martial.pojo.vo.MartialJudgeInviteVO">
<id column="id" property="id"/>
<result column="competition_id" property="competitionId"/>
<result column="judge_id" property="judgeId"/>
<result column="invite_code" property="inviteCode"/>
<result column="role" property="role"/>
<result column="venue_id" property="venueId"/>
<result column="projects" property="projects"/>
<result column="expire_time" property="expireTime"/>
<result column="is_used" property="isUsed"/>
<result column="use_time" property="useTime"/>
<result column="device_info" property="deviceInfo"/>
<result column="login_ip" property="loginIp"/>
<result column="access_token" property="accessToken"/>
<result column="token_expire_time" property="tokenExpireTime"/>
<result column="invite_status" property="inviteStatus"/>
<result column="invite_time" property="inviteTime"/>
<result column="reply_time" property="replyTime"/>
<result column="reply_note" property="replyNote"/>
<result column="contact_phone" property="contactPhone"/>
<result column="contact_email" property="contactEmail"/>
<result column="invite_message" property="inviteMessage"/>
<result column="cancel_reason" property="cancelReason"/>
<!-- 关联的裁判信息 -->
<result column="judge_name" property="judgeName"/>
<result column="judge_level" property="judgeLevel"/>
<!-- 关联的赛事信息 -->
<result column="competition_name" property="competitionName"/>
<!-- 基础字段 -->
<result column="create_user" property="createUser"/>
<result column="create_dept" property="createDept"/>
<result column="create_time" property="createTime"/>
<result column="update_user" property="updateUser"/>
<result column="update_time" property="updateTime"/>
<result column="status" property="status"/>
<result column="is_deleted" property="isDeleted"/>
</resultMap>
<!-- 分页查询裁判邀请列表(关联裁判信息) -->
<select id="selectJudgeInvitePage" resultMap="judgeInviteVOResultMap">
SELECT
ji.id,
ji.competition_id,
ji.judge_id,
ji.invite_code,
ji.role,
ji.venue_id,
ji.projects,
ji.expire_time,
ji.is_used,
ji.use_time,
ji.device_info,
ji.login_ip,
ji.access_token,
ji.token_expire_time,
ji.invite_status,
ji.invite_time,
ji.reply_time,
ji.reply_note,
ji.contact_phone,
ji.contact_email,
ji.invite_message,
ji.cancel_reason,
ji.create_user,
ji.create_dept,
ji.create_time,
ji.update_user,
ji.update_time,
ji.status,
ji.is_deleted,
j.name AS judge_name,
j.level AS judge_level,
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.is_deleted = 0
<if test="judgeInvite.competitionId != null">
AND ji.competition_id = #{judgeInvite.competitionId}
</if>
<if test="judgeInvite.inviteStatus != null">
AND ji.invite_status = #{judgeInvite.inviteStatus}
</if>
<if test="judgeInvite.judgeName != null and judgeInvite.judgeName != ''">
AND j.name LIKE CONCAT('%', #{judgeInvite.judgeName}, '%')
</if>
<if test="judgeInvite.judgeLevel != null and judgeInvite.judgeLevel != ''">
AND j.level = #{judgeInvite.judgeLevel}
</if>
ORDER BY ji.create_time DESC
</select>
</mapper>

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.MartialScheduleDetail;
/**
* 赛程编排明细 Mapper 接口
*
* @author BladeX
*/
public interface MartialScheduleDetailMapper extends BaseMapper<MartialScheduleDetail> {
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.modules.martial.mapper.MartialScheduleDetailMapper">
</mapper>

View File

@@ -0,0 +1,41 @@
/*
* 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.apache.ibatis.annotations.Param;
import org.springblade.modules.martial.pojo.entity.MartialScheduleGroup;
import org.springblade.modules.martial.pojo.vo.ScheduleGroupDetailVO;
import java.util.List;
/**
* 赛程编排分组 Mapper 接口
*
* @author BladeX
*/
public interface MartialScheduleGroupMapper extends BaseMapper<MartialScheduleGroup> {
/**
* 查询赛程编排的完整详情一次性JOIN查询优化性能
*
* @param competitionId 比赛ID
* @return 分组详情列表
*/
List<ScheduleGroupDetailVO> selectScheduleGroupDetails(@Param("competitionId") Long competitionId);
}

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.modules.martial.mapper.MartialScheduleGroupMapper">
<!-- 优化的一次性JOIN查询获取完整的赛程编排数据 -->
<select id="selectScheduleGroupDetails" resultType="org.springblade.modules.martial.pojo.vo.ScheduleGroupDetailVO">
SELECT
g.id AS groupId,
g.group_name AS groupName,
g.category AS category,
g.project_type AS projectType,
g.total_teams AS totalTeams,
g.total_participants AS totalParticipants,
g.display_order AS displayOrder,
d.id AS detailId,
d.venue_id AS venueId,
d.venue_name AS venueName,
d.time_slot AS timeSlot,
d.time_slot_index AS timeSlotIndex,
p.id AS participantId,
p.organization AS organization,
p.check_in_status AS checkInStatus,
p.schedule_status AS scheduleStatus,
p.performance_order AS performanceOrder
FROM
martial_schedule_group g
LEFT JOIN
martial_schedule_detail d ON g.id = d.schedule_group_id AND d.is_deleted = 0
LEFT JOIN
martial_schedule_participant p ON g.id = p.schedule_group_id AND p.is_deleted = 0
WHERE
g.competition_id = #{competitionId}
AND g.is_deleted = 0
ORDER BY
g.display_order ASC,
p.performance_order ASC
</select>
</mapper>

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.MartialScheduleParticipant;
/**
* 赛程编排参赛者关联 Mapper 接口
*
* @author BladeX
*/
public interface MartialScheduleParticipantMapper extends BaseMapper<MartialScheduleParticipant> {
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.modules.martial.mapper.MartialScheduleParticipantMapper">
</mapper>

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.MartialScheduleStatus;
/**
* 赛程编排状态 Mapper 接口
*
* @author BladeX
*/
public interface MartialScheduleStatusMapper extends BaseMapper<MartialScheduleStatus> {
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.modules.martial.mapper.MartialScheduleStatusMapper">
</mapper>

View File

@@ -0,0 +1,80 @@
package org.springblade.modules.martial.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 竞赛分组DTO
*
* @author BladeX
*/
@Data
@Schema(description = "竞赛分组DTO")
public class CompetitionGroupDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 分组ID
*/
@Schema(description = "分组ID")
private Long id;
/**
* 分组标题
*/
@Schema(description = "分组标题")
private String title;
/**
* 类型:集体/单人/双人
*/
@Schema(description = "类型:集体/单人/双人")
private String type;
/**
* 队伍数量
*/
@Schema(description = "队伍数量")
private String count;
/**
* 分组编号
*/
@Schema(description = "分组编号")
private String code;
/**
* 当前所属场地ID
*/
@Schema(description = "当前所属场地ID")
private Long venueId;
/**
* 场地名称
*/
@Schema(description = "场地名称")
private String venueName;
/**
* 时间段
*/
@Schema(description = "时间段")
private String timeSlot;
/**
* 时间段索引
*/
@Schema(description = "时间段索引")
private Integer timeSlotIndex;
/**
* 参赛人员列表
*/
@Schema(description = "参赛人员列表")
private List<ParticipantDTO> participants;
}

View File

@@ -0,0 +1,33 @@
package org.springblade.modules.martial.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 移动赛程分组DTO
*
* @author BladeX
*/
@Data
@Schema(description = "移动赛程分组请求")
public class MoveScheduleGroupDTO {
/**
* 分组ID
*/
@Schema(description = "分组ID")
private Long groupId;
/**
* 目标场地ID
*/
@Schema(description = "目标场地ID")
private Long targetVenueId;
/**
* 目标时间段索引
*/
@Schema(description = "目标时间段索引(0=第1天上午,1=第1天下午,2=第2天上午...)")
private Integer targetTimeSlotIndex;
}

View File

@@ -0,0 +1,43 @@
package org.springblade.modules.martial.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 参赛人员DTO
*
* @author BladeX
*/
@Data
@Schema(description = "参赛人员DTO")
public class ParticipantDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 参赛人员ID
*/
@Schema(description = "参赛人员ID")
private Long id;
/**
* 学校/单位
*/
@Schema(description = "学校/单位")
private String schoolUnit;
/**
* 状态:未签到/已签到/异常
*/
@Schema(description = "状态:未签到/已签到/异常")
private String status;
/**
* 排序
*/
@Schema(description = "排序")
private Integer sortOrder;
}

View File

@@ -0,0 +1,38 @@
package org.springblade.modules.martial.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 保存编排草稿DTO
*
* @author BladeX
*/
@Data
@Schema(description = "保存编排草稿DTO")
public class SaveScheduleDraftDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 赛事ID
*/
@Schema(description = "赛事ID")
private Long competitionId;
/**
* 是否为草稿
*/
@Schema(description = "是否为草稿")
private Boolean isDraft;
/**
* 竞赛分组数据
*/
@Schema(description = "竞赛分组数据")
private List<CompetitionGroupDTO> competitionGroups;
}

View File

@@ -0,0 +1,38 @@
package org.springblade.modules.martial.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 赛程编排结果DTO
*
* @author BladeX
*/
@Data
@Schema(description = "赛程编排结果DTO")
public class ScheduleResultDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 是否为草稿状态
*/
@Schema(description = "是否为草稿状态")
private Boolean isDraft;
/**
* 是否已完成编排
*/
@Schema(description = "是否已完成编排")
private Boolean isCompleted;
/**
* 竞赛分组列表
*/
@Schema(description = "竞赛分组列表")
private List<CompetitionGroupDTO> competitionGroups;
}

View File

@@ -0,0 +1,80 @@
/*
* 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_rules_attachment")
@Schema(description = "赛事规程附件")
public class MartialCompetitionRulesAttachment extends TenantEntity {
private static final long serialVersionUID = 1L;
/**
* 赛事ID
*/
@Schema(description = "赛事ID")
private Long competitionId;
/**
* 文件名称
*/
@Schema(description = "文件名称")
private String fileName;
/**
* 文件URL
*/
@Schema(description = "文件URL")
private String fileUrl;
/**
* 文件大小(字节)
*/
@Schema(description = "文件大小(字节)")
private Long fileSize;
/**
* 文件类型
*/
@Schema(description = "文件类型pdf/doc/docx/xls/xlsx等")
private String fileType;
/**
* 排序序号
*/
@Schema(description = "排序序号")
private Integer orderNum;
/**
* 状态1-启用 0-禁用)
*/
@Schema(description = "状态1-启用 0-禁用)")
private Integer status;
}

View File

@@ -0,0 +1,68 @@
/*
* 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_rules_chapter")
@Schema(description = "赛事规程章节")
public class MartialCompetitionRulesChapter extends TenantEntity {
private static final long serialVersionUID = 1L;
/**
* 赛事ID
*/
@Schema(description = "赛事ID")
private Long competitionId;
/**
* 章节编号
*/
@Schema(description = "章节编号(如:第一章)")
private String chapterNumber;
/**
* 章节标题
*/
@Schema(description = "章节标题")
private String title;
/**
* 排序序号
*/
@Schema(description = "排序序号")
private Integer orderNum;
/**
* 状态1-启用 0-禁用)
*/
@Schema(description = "状态1-启用 0-禁用)")
private Integer status;
}

View File

@@ -0,0 +1,62 @@
/*
* 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_rules_content")
@Schema(description = "赛事规程内容")
public class MartialCompetitionRulesContent extends TenantEntity {
private static final long serialVersionUID = 1L;
/**
* 章节ID
*/
@Schema(description = "章节ID")
private Long chapterId;
/**
* 规程内容
*/
@Schema(description = "规程内容")
private String content;
/**
* 排序序号
*/
@Schema(description = "排序序号")
private Integer orderNum;
/**
* 状态1-启用 0-禁用)
*/
@Schema(description = "状态1-启用 0-禁用)")
private Integer status;
}

View File

@@ -115,4 +115,52 @@ public class MartialJudgeInvite extends TenantEntity {
@Schema(description = "token过期时间")
private LocalDateTime tokenExpireTime;
/**
* 邀请状态(0-待回复,1-已接受,2-已拒绝,3-已取消)
*/
@Schema(description = "邀请状态")
private Integer inviteStatus;
/**
* 邀请时间
*/
@Schema(description = "邀请时间")
private LocalDateTime inviteTime;
/**
* 回复时间
*/
@Schema(description = "回复时间")
private LocalDateTime replyTime;
/**
* 回复备注
*/
@Schema(description = "回复备注")
private String replyNote;
/**
* 联系电话
*/
@Schema(description = "联系电话")
private String contactPhone;
/**
* 联系邮箱
*/
@Schema(description = "联系邮箱")
private String contactEmail;
/**
* 邀请消息
*/
@Schema(description = "邀请消息")
private String inviteMessage;
/**
* 取消原因
*/
@Schema(description = "取消原因")
private String cancelReason;
}

View File

@@ -0,0 +1,119 @@
/*
* 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;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 赛程编排明细实体类(场地时间段分配)
*
* @author BladeX
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("martial_schedule_detail")
@Schema(description = "赛程编排明细(场地时间段分配)")
public class MartialScheduleDetail extends TenantEntity {
private static final long serialVersionUID = 1L;
/**
* 分组ID
*/
@Schema(description = "分组ID")
private Long scheduleGroupId;
/**
* 赛事ID
*/
@Schema(description = "赛事ID")
private Long competitionId;
/**
* 场地ID
*/
@Schema(description = "场地ID")
private Long venueId;
/**
* 场地名称
*/
@Schema(description = "场地名称")
private String venueName;
/**
* 比赛日期
*/
@Schema(description = "比赛日期")
private LocalDate scheduleDate;
/**
* 时间段(morning/afternoon)
*/
@Schema(description = "时间段(morning/afternoon)")
private String timePeriod;
/**
* 时间点(08:30/13:30)
*/
@Schema(description = "时间点(08:30/13:30)")
private String timeSlot;
/**
* 时间段索引(0=第1天上午,1=第1天下午,2=第2天上午,...)
*/
@Schema(description = "时间段索引")
private Integer timeSlotIndex;
/**
* 预计开始时间
*/
@Schema(description = "预计开始时间")
private LocalDateTime estimatedStartTime;
/**
* 预计结束时间
*/
@Schema(description = "预计结束时间")
private LocalDateTime estimatedEndTime;
/**
* 预计时长(分钟)
*/
@Schema(description = "预计时长(分钟)")
private Integer estimatedDuration;
/**
* 参赛人数
*/
@Schema(description = "参赛人数")
private Integer participantCount;
/**
* 场内顺序
*/
@Schema(description = "场内顺序")
private Integer sortOrder;
}

View File

@@ -0,0 +1,98 @@
/*
* 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_schedule_group")
@Schema(description = "赛程编排分组")
public class MartialScheduleGroup extends TenantEntity {
private static final long serialVersionUID = 1L;
/**
* 赛事ID
*/
@Schema(description = "赛事ID")
private Long competitionId;
/**
* 分组名称(如:太极拳男组)
*/
@Schema(description = "分组名称")
private String groupName;
/**
* 项目ID
*/
@Schema(description = "项目ID")
private Long projectId;
/**
* 项目名称
*/
@Schema(description = "项目名称")
private String projectName;
/**
* 组别(成年组、少年组等)
*/
@Schema(description = "组别")
private String category;
/**
* 项目类型(1=个人 2=集体)
*/
@Schema(description = "项目类型(1=个人 2=集体)")
private Integer projectType;
/**
* 显示顺序(集体项目优先,数字越小越靠前)
*/
@Schema(description = "显示顺序")
private Integer displayOrder;
/**
* 总参赛人数
*/
@Schema(description = "总参赛人数")
private Integer totalParticipants;
/**
* 总队伍数(仅集体项目)
*/
@Schema(description = "总队伍数")
private Integer totalTeams;
/**
* 预计时长(分钟)
*/
@Schema(description = "预计时长(分钟)")
private Integer estimatedDuration;
}

View File

@@ -0,0 +1,98 @@
/*
* 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_schedule_participant")
@Schema(description = "赛程编排参赛者关联")
public class MartialScheduleParticipant extends TenantEntity {
private static final long serialVersionUID = 1L;
/**
* 编排明细ID
*/
@Schema(description = "编排明细ID")
private Long scheduleDetailId;
/**
* 分组ID
*/
@Schema(description = "分组ID")
private Long scheduleGroupId;
/**
* 参赛者ID(关联martial_athlete表)
*/
@Schema(description = "参赛者ID")
private Long participantId;
/**
* 单位名称
*/
@Schema(description = "单位名称")
private String organization;
/**
* 选手姓名
*/
@Schema(description = "选手姓名")
private String playerName;
/**
* 项目名称
*/
@Schema(description = "项目名称")
private String projectName;
/**
* 组别
*/
@Schema(description = "组别")
private String category;
/**
* 出场顺序
*/
@Schema(description = "出场顺序")
private Integer performanceOrder;
/**
* 签到状态:未签到/已签到/异常
*/
@Schema(description = "签到状态:未签到/已签到/异常")
private String checkInStatus;
/**
* 编排状态draft/completed
*/
@Schema(description = "编排状态draft/completed")
private String scheduleStatus;
}

View File

@@ -0,0 +1,82 @@
/*
* 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;
import java.time.LocalDateTime;
/**
* 赛程编排状态实体类
*
* @author BladeX
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("martial_schedule_status")
@Schema(description = "赛程编排状态")
public class MartialScheduleStatus extends TenantEntity {
private static final long serialVersionUID = 1L;
/**
* 赛事ID(唯一)
*/
@Schema(description = "赛事ID")
private Long competitionId;
/**
* 编排状态(0=未编排 1=编排中 2=已保存锁定)
*/
@Schema(description = "编排状态(0=未编排 1=编排中 2=已保存锁定)")
private Integer scheduleStatus;
/**
* 最后自动编排时间
*/
@Schema(description = "最后自动编排时间")
private LocalDateTime lastAutoScheduleTime;
/**
* 锁定时间
*/
@Schema(description = "锁定时间")
private LocalDateTime lockedTime;
/**
* 锁定人
*/
@Schema(description = "锁定人")
private String lockedBy;
/**
* 总分组数
*/
@Schema(description = "总分组数")
private Integer totalGroups;
/**
* 总参赛人数
*/
@Schema(description = "总参赛人数")
private Integer totalParticipants;
}

View File

@@ -53,12 +53,6 @@ public class MartialVenue extends TenantEntity {
@Schema(description = "场地编码")
private String venueCode;
/**
* 场地位置
*/
@Schema(description = "场地位置")
private String location;
/**
* 容纳人数
*/
@@ -66,9 +60,21 @@ public class MartialVenue extends TenantEntity {
private Integer capacity;
/**
* 设施说明
* 位置/地点
*/
@Schema(description = "设施说明")
@Schema(description = "位置")
private String location;
/**
* 场地设施
*/
@Schema(description = "场地设施")
private String facilities;
/**
* 状态(0-禁用,1-启用)
*/
@Schema(description = "状态")
private Integer status;
}

View File

@@ -0,0 +1,129 @@
/*
* 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.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 赛事规程视图对象
*
* @author BladeX
*/
@Data
@Schema(description = "赛事规程")
public class MartialCompetitionRulesVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 赛事ID
*/
@Schema(description = "赛事ID")
private Long competitionId;
/**
* 赛事名称
*/
@Schema(description = "赛事名称")
private String competitionName;
/**
* 附件列表
*/
@Schema(description = "附件列表")
private List<AttachmentVO> attachments;
/**
* 章节列表
*/
@Schema(description = "章节列表")
private List<ChapterVO> chapters;
/**
* 附件视图对象
*/
@Data
@Schema(description = "附件信息")
public static class AttachmentVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "附件ID")
private Long id;
@Schema(description = "文件名称")
private String name;
@Schema(description = "文件名称(别名)")
private String fileName;
@Schema(description = "文件URL")
private String url;
@Schema(description = "文件URL别名")
private String fileUrl;
@Schema(description = "文件大小(字节)")
private Long size;
@Schema(description = "文件大小(别名)")
private Long fileSize;
@Schema(description = "文件类型")
private String fileType;
@Schema(description = "上传时间")
private String uploadTime;
}
/**
* 章节视图对象
*/
@Data
@Schema(description = "章节信息")
public static class ChapterVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "章节ID")
private Long id;
@Schema(description = "章节编号")
private String chapterNumber;
@Schema(description = "章节编号(别名)")
private String number;
@Schema(description = "章节标题")
private String title;
@Schema(description = "章节标题(别名)")
private String name;
@Schema(description = "排序序号")
private Integer order;
@Schema(description = "章节内容列表")
private List<String> contents;
@Schema(description = "章节内容列表(别名)")
private List<String> items;
}
}

View File

@@ -0,0 +1,77 @@
package org.springblade.modules.martial.pojo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.modules.martial.pojo.entity.MartialJudgeInvite;
import java.time.LocalDateTime;
/**
* 裁判邀请码视图对象
* 包含裁判的详细信息
*
* @author BladeX
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "裁判邀请码视图对象")
public class MartialJudgeInviteVO extends MartialJudgeInvite {
private static final long serialVersionUID = 1L;
/**
* 裁判姓名
*/
@Schema(description = "裁判姓名")
private String judgeName;
/**
* 裁判等级
*/
@Schema(description = "裁判等级")
private String judgeLevel;
/**
* 联系电话
*/
@Schema(description = "联系电话")
private String contactPhone;
/**
* 联系邮箱
*/
@Schema(description = "联系邮箱")
private String contactEmail;
/**
* 邀请状态(0-待回复,1-已接受,2-已拒绝,3-已取消)
*/
@Schema(description = "邀请状态")
private Integer inviteStatus;
/**
* 邀请时间
*/
@Schema(description = "邀请时间")
private LocalDateTime inviteTime;
/**
* 回复时间
*/
@Schema(description = "回复时间")
private LocalDateTime replyTime;
/**
* 回复备注
*/
@Schema(description = "回复备注")
private String replyNote;
/**
* 赛事名称
*/
@Schema(description = "赛事名称")
private String competitionName;
}

View File

@@ -0,0 +1,38 @@
package org.springblade.modules.martial.pojo.vo;
import lombok.Data;
import java.io.Serializable;
/**
* 赛程编排分组详情VO用于优化查询
*
* @author BladeX
*/
@Data
public class ScheduleGroupDetailVO implements Serializable {
private static final long serialVersionUID = 1L;
// === 分组信息 ===
private Long groupId;
private String groupName;
private String category;
private Integer projectType;
private Integer totalTeams;
private Integer totalParticipants;
private Integer displayOrder;
// === 编排明细信息 ===
private Long detailId;
private Long venueId;
private String venueName;
private String timeSlot;
private Integer timeSlotIndex; // 时间段索引(0=第1天上午,1=第1天下午,2=第2天上午,...)
// === 参赛者信息 ===
private Long participantId;
private String organization;
private String checkInStatus;
private String scheduleStatus;
private Integer performanceOrder;
}

View File

@@ -0,0 +1,122 @@
/*
* 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.service;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionRulesAttachment;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionRulesChapter;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionRulesContent;
import org.springblade.modules.martial.pojo.vo.MartialCompetitionRulesVO;
import java.util.List;
/**
* 赛事规程服务类
*
* @author BladeX
*/
public interface IMartialCompetitionRulesService {
/**
* 获取赛事规程(小程序端)
*
* @param competitionId 赛事ID
* @return 规程信息
*/
MartialCompetitionRulesVO getRulesByCompetitionId(Long competitionId);
/**
* 获取附件列表
*
* @param competitionId 赛事ID
* @return 附件列表
*/
List<MartialCompetitionRulesAttachment> getAttachmentList(Long competitionId);
/**
* 保存附件
*
* @param attachment 附件信息
* @return 是否成功
*/
boolean saveAttachment(MartialCompetitionRulesAttachment attachment);
/**
* 删除附件
*
* @param id 附件ID
* @return 是否成功
*/
boolean removeAttachment(Long id);
/**
* 获取章节列表
*
* @param competitionId 赛事ID
* @return 章节列表
*/
List<MartialCompetitionRulesChapter> getChapterList(Long competitionId);
/**
* 保存章节
*
* @param chapter 章节信息
* @return 是否成功
*/
boolean saveChapter(MartialCompetitionRulesChapter chapter);
/**
* 删除章节
*
* @param id 章节ID
* @return 是否成功
*/
boolean removeChapter(Long id);
/**
* 获取章节内容列表
*
* @param chapterId 章节ID
* @return 内容列表
*/
List<MartialCompetitionRulesContent> getContentList(Long chapterId);
/**
* 保存章节内容
*
* @param content 内容信息
* @return 是否成功
*/
boolean saveContent(MartialCompetitionRulesContent content);
/**
* 删除章节内容
*
* @param id 内容ID
* @return 是否成功
*/
boolean removeContent(Long id);
/**
* 批量保存章节内容
*
* @param chapterId 章节ID
* @param contents 内容列表
* @return 是否成功
*/
boolean batchSaveContents(Long chapterId, List<String> contents);
}

View File

@@ -1,7 +1,12 @@
package org.springblade.modules.martial.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springblade.core.mp.support.Query;
import org.springblade.modules.martial.pojo.entity.MartialJudgeInvite;
import org.springblade.modules.martial.pojo.vo.MartialJudgeInviteVO;
import java.util.Map;
/**
* JudgeInvite 服务类
@@ -10,4 +15,21 @@ import org.springblade.modules.martial.pojo.entity.MartialJudgeInvite;
*/
public interface IMartialJudgeInviteService extends IService<MartialJudgeInvite> {
/**
* 分页查询裁判邀请列表(关联裁判信息)
*
* @param judgeInvite 查询条件
* @param query 分页参数
* @return 裁判邀请VO分页列表
*/
IPage<MartialJudgeInviteVO> selectJudgeInvitePage(MartialJudgeInvite judgeInvite, Query query);
/**
* 获取邀请统计信息
*
* @param competitionId 赛事ID
* @return 统计信息
*/
Map<String, Object> getInviteStatistics(Long competitionId);
}

View File

@@ -0,0 +1,55 @@
/*
* 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.service;
import java.util.List;
import java.util.Map;
/**
* 赛程自动编排服务接口
*
* @author BladeX
*/
public interface IMartialScheduleArrangeService {
/**
* 自动编排赛程
* @param competitionId 赛事ID
*/
void autoArrange(Long competitionId);
/**
* 获取未锁定的赛事列表
* @return 赛事ID列表
*/
List<Long> getUnlockedCompetitions();
/**
* 保存并锁定编排
* @param competitionId 赛事ID
* @param userId 用户ID
*/
void saveAndLock(Long competitionId, String userId);
/**
* 获取编排结果
* @param competitionId 赛事ID
* @return 编排数据
*/
Map<String, Object> getScheduleResult(Long competitionId);
}

View File

@@ -2,6 +2,9 @@ package org.springblade.modules.martial.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springblade.modules.martial.excel.ScheduleExportExcel;
import org.springblade.modules.martial.pojo.dto.MoveScheduleGroupDTO;
import org.springblade.modules.martial.pojo.dto.SaveScheduleDraftDTO;
import org.springblade.modules.martial.pojo.dto.ScheduleResultDTO;
import org.springblade.modules.martial.pojo.entity.MartialSchedule;
import java.util.List;
@@ -18,4 +21,32 @@ public interface IMartialScheduleService extends IService<MartialSchedule> {
*/
List<ScheduleExportExcel> exportSchedule(Long competitionId);
/**
* 获取赛程编排结果
* @param competitionId 赛事ID
* @return 赛程编排结果
*/
ScheduleResultDTO getScheduleResult(Long competitionId);
/**
* 保存编排草稿
* @param dto 编排草稿数据
* @return 是否成功
*/
boolean saveDraftSchedule(SaveScheduleDraftDTO dto);
/**
* 完成编排并锁定
* @param competitionId 赛事ID
* @return 是否成功
*/
boolean saveAndLockSchedule(Long competitionId);
/**
* 移动赛程分组到指定场地和时间段
* @param dto 移动请求数据
* @return 是否成功
*/
boolean moveScheduleGroup(MoveScheduleGroupDTO dto);
}

View File

@@ -0,0 +1,217 @@
/*
* 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.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.modules.martial.mapper.MartialCompetitionMapper;
import org.springblade.modules.martial.mapper.MartialCompetitionRulesAttachmentMapper;
import org.springblade.modules.martial.mapper.MartialCompetitionRulesChapterMapper;
import org.springblade.modules.martial.mapper.MartialCompetitionRulesContentMapper;
import org.springblade.modules.martial.pojo.entity.MartialCompetition;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionRulesAttachment;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionRulesChapter;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionRulesContent;
import org.springblade.modules.martial.pojo.vo.MartialCompetitionRulesVO;
import org.springblade.modules.martial.service.IMartialCompetitionRulesService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 赛事规程服务实现类
*
* @author BladeX
*/
@Service
@RequiredArgsConstructor
public class MartialCompetitionRulesServiceImpl implements IMartialCompetitionRulesService {
private final MartialCompetitionMapper competitionMapper;
private final MartialCompetitionRulesAttachmentMapper attachmentMapper;
private final MartialCompetitionRulesChapterMapper chapterMapper;
private final MartialCompetitionRulesContentMapper contentMapper;
@Override
public MartialCompetitionRulesVO getRulesByCompetitionId(Long competitionId) {
MartialCompetitionRulesVO vo = new MartialCompetitionRulesVO();
vo.setCompetitionId(competitionId);
// 获取赛事信息
MartialCompetition competition = competitionMapper.selectById(competitionId);
if (competition != null) {
vo.setCompetitionName(competition.getCompetitionName());
}
// 获取附件列表
List<MartialCompetitionRulesAttachment> attachments = getAttachmentList(competitionId);
List<MartialCompetitionRulesVO.AttachmentVO> attachmentVOList = attachments.stream()
.map(this::convertToAttachmentVO)
.collect(Collectors.toList());
vo.setAttachments(attachmentVOList);
// 获取章节列表
List<MartialCompetitionRulesChapter> chapters = getChapterList(competitionId);
List<MartialCompetitionRulesVO.ChapterVO> chapterVOList = new ArrayList<>();
for (MartialCompetitionRulesChapter chapter : chapters) {
MartialCompetitionRulesVO.ChapterVO chapterVO = convertToChapterVO(chapter);
// 获取章节内容
List<MartialCompetitionRulesContent> contents = getContentList(chapter.getId());
List<String> contentList = contents.stream()
.map(MartialCompetitionRulesContent::getContent)
.collect(Collectors.toList());
chapterVO.setContents(contentList);
chapterVO.setItems(contentList); // 别名
chapterVOList.add(chapterVO);
}
vo.setChapters(chapterVOList);
return vo;
}
@Override
public List<MartialCompetitionRulesAttachment> getAttachmentList(Long competitionId) {
LambdaQueryWrapper<MartialCompetitionRulesAttachment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialCompetitionRulesAttachment::getCompetitionId, competitionId)
.eq(MartialCompetitionRulesAttachment::getStatus, 1)
.orderByAsc(MartialCompetitionRulesAttachment::getOrderNum);
return attachmentMapper.selectList(wrapper);
}
@Override
public boolean saveAttachment(MartialCompetitionRulesAttachment attachment) {
if (attachment.getId() == null) {
return attachmentMapper.insert(attachment) > 0;
} else {
return attachmentMapper.updateById(attachment) > 0;
}
}
@Override
public boolean removeAttachment(Long id) {
return attachmentMapper.deleteById(id) > 0;
}
@Override
public List<MartialCompetitionRulesChapter> getChapterList(Long competitionId) {
LambdaQueryWrapper<MartialCompetitionRulesChapter> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialCompetitionRulesChapter::getCompetitionId, competitionId)
.eq(MartialCompetitionRulesChapter::getStatus, 1)
.orderByAsc(MartialCompetitionRulesChapter::getOrderNum);
return chapterMapper.selectList(wrapper);
}
@Override
public boolean saveChapter(MartialCompetitionRulesChapter chapter) {
if (chapter.getId() == null) {
return chapterMapper.insert(chapter) > 0;
} else {
return chapterMapper.updateById(chapter) > 0;
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean removeChapter(Long id) {
// 删除章节下的所有内容
LambdaQueryWrapper<MartialCompetitionRulesContent> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialCompetitionRulesContent::getChapterId, id);
contentMapper.delete(wrapper);
// 删除章节
return chapterMapper.deleteById(id) > 0;
}
@Override
public List<MartialCompetitionRulesContent> getContentList(Long chapterId) {
LambdaQueryWrapper<MartialCompetitionRulesContent> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialCompetitionRulesContent::getChapterId, chapterId)
.eq(MartialCompetitionRulesContent::getStatus, 1)
.orderByAsc(MartialCompetitionRulesContent::getOrderNum);
return contentMapper.selectList(wrapper);
}
@Override
public boolean saveContent(MartialCompetitionRulesContent content) {
if (content.getId() == null) {
return contentMapper.insert(content) > 0;
} else {
return contentMapper.updateById(content) > 0;
}
}
@Override
public boolean removeContent(Long id) {
return contentMapper.deleteById(id) > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean batchSaveContents(Long chapterId, List<String> contents) {
// 先删除原有内容
LambdaQueryWrapper<MartialCompetitionRulesContent> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialCompetitionRulesContent::getChapterId, chapterId);
contentMapper.delete(wrapper);
// 批量插入新内容
for (int i = 0; i < contents.size(); i++) {
MartialCompetitionRulesContent content = new MartialCompetitionRulesContent();
content.setChapterId(chapterId);
content.setContent(contents.get(i));
content.setOrderNum(i + 1);
content.setStatus(1);
contentMapper.insert(content);
}
return true;
}
/**
* 转换为附件VO
*/
private MartialCompetitionRulesVO.AttachmentVO convertToAttachmentVO(MartialCompetitionRulesAttachment attachment) {
MartialCompetitionRulesVO.AttachmentVO vo = new MartialCompetitionRulesVO.AttachmentVO();
vo.setId(attachment.getId());
vo.setName(attachment.getFileName());
vo.setFileName(attachment.getFileName());
vo.setUrl(attachment.getFileUrl());
vo.setFileUrl(attachment.getFileUrl());
vo.setSize(attachment.getFileSize());
vo.setFileSize(attachment.getFileSize());
vo.setFileType(attachment.getFileType());
vo.setUploadTime(DateUtil.format(attachment.getCreateTime(), DateUtil.PATTERN_DATETIME));
return vo;
}
/**
* 转换为章节VO
*/
private MartialCompetitionRulesVO.ChapterVO convertToChapterVO(MartialCompetitionRulesChapter chapter) {
MartialCompetitionRulesVO.ChapterVO vo = new MartialCompetitionRulesVO.ChapterVO();
vo.setId(chapter.getId());
vo.setChapterNumber(chapter.getChapterNumber());
vo.setNumber(chapter.getChapterNumber());
vo.setTitle(chapter.getTitle());
vo.setName(chapter.getTitle());
vo.setOrder(chapter.getOrderNum());
return vo;
}
}

View File

@@ -1,11 +1,20 @@
package org.springblade.modules.martial.service.impl;
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.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.modules.martial.pojo.entity.MartialJudgeInvite;
import org.springblade.modules.martial.mapper.MartialJudgeInviteMapper;
import org.springblade.modules.martial.pojo.vo.MartialJudgeInviteVO;
import org.springblade.modules.martial.service.IMartialJudgeInviteService;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* JudgeInvite 服务实现类
*
@@ -14,4 +23,45 @@ import org.springframework.stereotype.Service;
@Service
public class MartialJudgeInviteServiceImpl extends ServiceImpl<MartialJudgeInviteMapper, MartialJudgeInvite> implements IMartialJudgeInviteService {
@Override
public IPage<MartialJudgeInviteVO> selectJudgeInvitePage(MartialJudgeInvite judgeInvite, Query query) {
IPage<MartialJudgeInviteVO> page = Condition.getPage(query);
return baseMapper.selectJudgeInvitePage(page, judgeInvite);
}
@Override
public Map<String, Object> getInviteStatistics(Long competitionId) {
Map<String, Object> statistics = new HashMap<>();
LambdaQueryWrapper<MartialJudgeInvite> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialJudgeInvite::getCompetitionId, competitionId);
// 总邀请数
long totalInvites = this.count(wrapper);
statistics.put("totalInvites", totalInvites);
// 待回复数量
LambdaQueryWrapper<MartialJudgeInvite> pendingWrapper = new LambdaQueryWrapper<>();
pendingWrapper.eq(MartialJudgeInvite::getCompetitionId, competitionId)
.eq(MartialJudgeInvite::getInviteStatus, 0);
long pendingCount = this.count(pendingWrapper);
statistics.put("pendingCount", pendingCount);
// 已接受数量
LambdaQueryWrapper<MartialJudgeInvite> acceptedWrapper = new LambdaQueryWrapper<>();
acceptedWrapper.eq(MartialJudgeInvite::getCompetitionId, competitionId)
.eq(MartialJudgeInvite::getInviteStatus, 1);
long acceptedCount = this.count(acceptedWrapper);
statistics.put("acceptedCount", acceptedCount);
// 已拒绝数量
LambdaQueryWrapper<MartialJudgeInvite> rejectedWrapper = new LambdaQueryWrapper<>();
rejectedWrapper.eq(MartialJudgeInvite::getCompetitionId, competitionId)
.eq(MartialJudgeInvite::getInviteStatus, 2);
long rejectedCount = this.count(rejectedWrapper);
statistics.put("rejectedCount", rejectedCount);
return statistics;
}
}

View File

@@ -0,0 +1,933 @@
/*
* 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.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.springblade.core.tool.utils.Func;
import org.springblade.modules.martial.mapper.*;
import org.springblade.modules.martial.pojo.entity.*;
import org.springblade.modules.martial.service.IMartialScheduleArrangeService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* 赛程自动编排服务实现类
*
* @author BladeX
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrangeService {
private final MartialScheduleStatusMapper scheduleStatusMapper;
private final MartialScheduleGroupMapper scheduleGroupMapper;
private final MartialScheduleDetailMapper scheduleDetailMapper;
private final MartialScheduleParticipantMapper scheduleParticipantMapper;
private final MartialAthleteMapper athleteMapper;
private final MartialCompetitionMapper competitionMapper;
private final MartialVenueMapper venueMapper;
private final MartialProjectMapper projectMapper;
@Override
public List<Long> getUnlockedCompetitions() {
// 查询所有未锁定的赛事(schedule_status != 2)
LambdaQueryWrapper<MartialScheduleStatus> wrapper = new LambdaQueryWrapper<>();
wrapper.ne(MartialScheduleStatus::getScheduleStatus, 2)
.eq(MartialScheduleStatus::getIsDeleted, 0);
List<MartialScheduleStatus> statusList = scheduleStatusMapper.selectList(wrapper);
return statusList.stream()
.map(MartialScheduleStatus::getCompetitionId)
.collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void autoArrange(Long competitionId) {
log.info("开始自动编排赛程, competitionId: {}", competitionId);
try {
// 1. 检查赛事状态
MartialScheduleStatus status = getOrCreateScheduleStatus(competitionId);
if (status.getScheduleStatus() == 2) {
log.info("赛事已锁定,跳过自动编排, competitionId: {}", competitionId);
return;
}
// 2. 加载赛事信息
MartialCompetition competition = competitionMapper.selectById(competitionId);
if (competition == null) {
log.error("赛事不存在, competitionId: {}", competitionId);
return;
}
// 3. 加载场地列表
List<MartialVenue> venues = loadVenues(competitionId);
if (venues.isEmpty()) {
log.warn("赛事没有配置场地, competitionId: {}", competitionId);
return;
}
// 4. 加载参赛者列表
List<MartialAthlete> athletes = loadAthletes(competitionId);
if (athletes.isEmpty()) {
log.warn("赛事没有参赛者, competitionId: {}", competitionId);
return;
}
// 5. 生成时间段网格
List<TimeSlot> timeSlots = generateTimeSlots(competition);
// 6. 自动分组
List<ScheduleGroupData> groups = autoGroupParticipants(athletes);
// 7. 验证容量是否足够
validateCapacity(groups, venues, timeSlots);
// 8. 分配场地和时间段(轮询均匀分配)
assignVenueAndTimeSlot(groups, venues, timeSlots);
// 8. 清空旧的编排数据
clearOldScheduleData(competitionId);
// 9. 保存新的编排结果
saveScheduleData(competitionId, groups);
// 10. 更新编排状态
status.setScheduleStatus(1);
status.setLastAutoScheduleTime(LocalDateTime.now());
status.setTotalGroups(groups.size());
status.setTotalParticipants(athletes.size());
status.setUpdateTime(new Date());
scheduleStatusMapper.updateById(status);
log.info("自动编排完成, competitionId: {}, 分组数: {}, 参赛人数: {}",
competitionId, groups.size(), athletes.size());
} catch (Exception e) {
log.error("自动编排失败, competitionId: {}", competitionId, e);
throw new RuntimeException("自动编排失败: " + e.getMessage(), e);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveAndLock(Long competitionId, String userId) {
log.info("保存并锁定编排, competitionId: {}, userId: {}", competitionId, userId);
MartialScheduleStatus status = getOrCreateScheduleStatus(competitionId);
status.setScheduleStatus(2);
status.setLockedTime(LocalDateTime.now());
status.setLockedBy(userId);
status.setUpdateTime(new Date());
scheduleStatusMapper.updateById(status);
log.info("编排已保存并锁定, competitionId: {}", competitionId);
}
@Override
public Map<String, Object> getScheduleResult(Long competitionId) {
// 获取编排状态
MartialScheduleStatus status = getOrCreateScheduleStatus(competitionId);
// 获取分组列表
LambdaQueryWrapper<MartialScheduleGroup> groupWrapper = new LambdaQueryWrapper<>();
groupWrapper.eq(MartialScheduleGroup::getCompetitionId, competitionId)
.eq(MartialScheduleGroup::getIsDeleted, 0)
.orderByAsc(MartialScheduleGroup::getDisplayOrder);
List<MartialScheduleGroup> groups = scheduleGroupMapper.selectList(groupWrapper);
// 构建前端需要的数据结构
List<Map<String, Object>> scheduleGroups = new ArrayList<>();
for (MartialScheduleGroup group : groups) {
Map<String, Object> groupData = new HashMap<>();
groupData.put("id", group.getId());
groupData.put("groupName", group.getGroupName());
groupData.put("projectType", group.getProjectType());
groupData.put("displayOrder", group.getDisplayOrder());
groupData.put("totalParticipants", group.getTotalParticipants());
groupData.put("totalTeams", group.getTotalTeams());
// 获取该分组的场地时间段详情
LambdaQueryWrapper<MartialScheduleDetail> detailWrapper = new LambdaQueryWrapper<>();
detailWrapper.eq(MartialScheduleDetail::getScheduleGroupId, group.getId())
.eq(MartialScheduleDetail::getIsDeleted, 0);
List<MartialScheduleDetail> details = scheduleDetailMapper.selectList(detailWrapper);
List<Map<String, Object>> scheduleDetails = new ArrayList<>();
for (MartialScheduleDetail detail : details) {
Map<String, Object> detailData = new HashMap<>();
detailData.put("venueId", detail.getVenueId());
detailData.put("venueName", detail.getVenueName());
detailData.put("scheduleDate", detail.getScheduleDate());
detailData.put("timeSlot", detail.getTimeSlot());
detailData.put("timePeriod", detail.getTimePeriod());
scheduleDetails.add(detailData);
}
groupData.put("scheduleDetails", scheduleDetails);
// 获取该分组的参赛者
LambdaQueryWrapper<MartialScheduleParticipant> participantWrapper = new LambdaQueryWrapper<>();
participantWrapper.eq(MartialScheduleParticipant::getScheduleGroupId, group.getId())
.eq(MartialScheduleParticipant::getIsDeleted, 0);
List<MartialScheduleParticipant> participants = scheduleParticipantMapper.selectList(participantWrapper);
if (group.getProjectType() == 2) {
// 集体项目:按单位分组
Map<String, List<MartialScheduleParticipant>> orgGroupMap = participants.stream()
.collect(Collectors.groupingBy(MartialScheduleParticipant::getOrganization));
List<Map<String, Object>> organizationGroups = new ArrayList<>();
for (Map.Entry<String, List<MartialScheduleParticipant>> entry : orgGroupMap.entrySet()) {
Map<String, Object> orgGroup = new HashMap<>();
orgGroup.put("organization", entry.getKey());
List<Map<String, Object>> orgParticipants = new ArrayList<>();
for (MartialScheduleParticipant p : entry.getValue()) {
Map<String, Object> pData = new HashMap<>();
pData.put("playerName", p.getPlayerName());
orgParticipants.add(pData);
}
orgGroup.put("participants", orgParticipants);
// 获取该单位的场地时间段
orgGroup.put("scheduleDetails", scheduleDetails);
organizationGroups.add(orgGroup);
}
groupData.put("organizationGroups", organizationGroups);
} else {
// 个人项目:直接列出参赛者
List<Map<String, Object>> individualParticipants = new ArrayList<>();
for (MartialScheduleParticipant p : participants) {
Map<String, Object> pData = new HashMap<>();
pData.put("id", p.getParticipantId());
pData.put("organization", p.getOrganization());
pData.put("playerName", p.getPlayerName());
// 获取该参赛者的场地时间段
LambdaQueryWrapper<MartialScheduleDetail> pDetailWrapper = new LambdaQueryWrapper<>();
pDetailWrapper.eq(MartialScheduleDetail::getId, p.getScheduleDetailId())
.eq(MartialScheduleDetail::getIsDeleted, 0)
.last("LIMIT 1");
MartialScheduleDetail pDetail = scheduleDetailMapper.selectOne(pDetailWrapper);
if (pDetail != null) {
Map<String, Object> scheduleDetail = new HashMap<>();
scheduleDetail.put("venueId", pDetail.getVenueId());
scheduleDetail.put("venueName", pDetail.getVenueName());
scheduleDetail.put("scheduleDate", pDetail.getScheduleDate());
scheduleDetail.put("timeSlot", pDetail.getTimeSlot());
pData.put("scheduleDetail", scheduleDetail);
}
individualParticipants.add(pData);
}
groupData.put("participants", individualParticipants);
}
scheduleGroups.add(groupData);
}
// 构建返回结果
Map<String, Object> result = new HashMap<>();
result.put("scheduleStatus", status.getScheduleStatus());
result.put("lastAutoScheduleTime", status.getLastAutoScheduleTime());
result.put("totalGroups", status.getTotalGroups());
result.put("totalParticipants", status.getTotalParticipants());
result.put("scheduleGroups", scheduleGroups);
return result;
}
// ==================== 私有辅助方法 ====================
private MartialScheduleStatus getOrCreateScheduleStatus(Long competitionId) {
LambdaQueryWrapper<MartialScheduleStatus> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialScheduleStatus::getCompetitionId, competitionId)
.eq(MartialScheduleStatus::getIsDeleted, 0)
.last("LIMIT 1");
MartialScheduleStatus status = scheduleStatusMapper.selectOne(wrapper);
if (status == null) {
status = new MartialScheduleStatus();
status.setCompetitionId(competitionId);
status.setScheduleStatus(0);
status.setTotalGroups(0);
status.setTotalParticipants(0);
status.setCreateTime(new Date());
scheduleStatusMapper.insert(status);
}
return status;
}
private List<MartialVenue> loadVenues(Long competitionId) {
LambdaQueryWrapper<MartialVenue> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialVenue::getCompetitionId, competitionId)
.eq(MartialVenue::getIsDeleted, 0);
return venueMapper.selectList(wrapper);
}
private List<MartialAthlete> loadAthletes(Long competitionId) {
LambdaQueryWrapper<MartialAthlete> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialAthlete::getCompetitionId, competitionId)
.eq(MartialAthlete::getIsDeleted, 0);
return athleteMapper.selectList(wrapper);
}
private List<TimeSlot> generateTimeSlots(MartialCompetition competition) {
List<TimeSlot> timeSlots = new ArrayList<>();
LocalDateTime startTime = competition.getCompetitionStartTime();
LocalDateTime endTime = competition.getCompetitionEndTime();
if (startTime == null || endTime == null) {
log.warn("赛事时间信息不完整, 使用默认时间段");
return timeSlots;
}
LocalDate currentDate = startTime.toLocalDate();
LocalDate endDate = endTime.toLocalDate();
while (!currentDate.isAfter(endDate)) {
// 上午时段 (08:00-12:00, 共240分钟)
TimeSlot morning = new TimeSlot();
morning.setDate(currentDate);
morning.setPeriod("morning");
morning.setStartTime("08:30");
morning.setCapacity(480); // 上午480分钟(8小时,允许并发/灵活安排)
timeSlots.add(morning);
// 下午时段 (13:00-18:00, 共300分钟)
TimeSlot afternoon = new TimeSlot();
afternoon.setDate(currentDate);
afternoon.setPeriod("afternoon");
afternoon.setStartTime("13:30");
afternoon.setCapacity(480); // 下午480分钟(8小时,允许并发/灵活安排)
timeSlots.add(afternoon);
currentDate = currentDate.plusDays(1);
}
return timeSlots;
}
private List<ScheduleGroupData> autoGroupParticipants(List<MartialAthlete> athletes) {
List<ScheduleGroupData> groups = new ArrayList<>();
int displayOrder = 1;
// 先加载所有项目信息(用于获取项目类型和名称)
Set<Long> projectIds = athletes.stream()
.map(MartialAthlete::getProjectId)
.filter(id -> id != null)
.collect(Collectors.toSet());
Map<Long, MartialProject> projectMap = new HashMap<>();
for (Long projectId : projectIds) {
MartialProject project = projectMapper.selectById(projectId);
if (project != null) {
projectMap.put(projectId, project);
}
}
// 分离集体和个人项目(根据项目表的type字段: 1=个人, 2=双人, 3=集体)
List<MartialAthlete> teamAthletes = athletes.stream()
.filter(a -> {
MartialProject project = projectMap.get(a.getProjectId());
return project != null && (project.getType() == 2 || project.getType() == 3);
})
.collect(Collectors.toList());
List<MartialAthlete> individualAthletes = athletes.stream()
.filter(a -> {
MartialProject project = projectMap.get(a.getProjectId());
return project != null && project.getType() == 1;
})
.collect(Collectors.toList());
// 集体项目分组:按"项目ID_组别"分组
Map<String, List<MartialAthlete>> teamGroupMap = teamAthletes.stream()
.collect(Collectors.groupingBy(a ->
a.getProjectId() + "_" + Func.toStr(a.getCategory(), "未分组")
));
for (Map.Entry<String, List<MartialAthlete>> entry : teamGroupMap.entrySet()) {
List<MartialAthlete> members = entry.getValue();
if (members.isEmpty()) continue;
MartialAthlete first = members.get(0);
MartialProject project = projectMap.get(first.getProjectId());
// 统计队伍数(按单位分组)
long teamCount = members.stream()
.map(MartialAthlete::getOrganization)
.filter(org -> org != null && !org.isEmpty())
.distinct()
.count();
// 跳过没有项目信息的分组
if (project == null) {
log.warn("项目不存在, projectId: {}, 跳过该分组", first.getProjectId());
continue;
}
ScheduleGroupData group = new ScheduleGroupData();
String projectName = project.getProjectName();
group.setGroupName(projectName + " " + (first.getCategory() != null ? first.getCategory() : "未分组"));
group.setProjectId(first.getProjectId());
group.setProjectType(project.getType() == 3 ? 2 : 1); // type=3映射为projectType=2(集体)
group.setDisplayOrder(displayOrder++);
group.setTotalParticipants(members.size());
group.setTotalTeams((int) teamCount);
group.setAthletes(members);
// 计算预计时长:使用项目的 estimatedDuration
// 如果项目设置了时长,使用 队伍数 × 项目时长
// 否则使用默认计算: 队伍数 × 5分钟 + 间隔时间
int duration;
if (project.getEstimatedDuration() != null && project.getEstimatedDuration() > 0) {
duration = (int) teamCount * project.getEstimatedDuration();
log.debug("集体项目 '{}': 使用项目时长 {}分钟/队, {}队, 总计{}分钟",
projectName, project.getEstimatedDuration(), teamCount, duration);
} else {
duration = (int) teamCount * 5 + Math.max(0, (int) teamCount - 1) * 2;
log.debug("集体项目 '{}': 使用默认时长, {}队, 总计{}分钟", projectName, teamCount, duration);
}
group.setEstimatedDuration(duration);
groups.add(group);
}
// 个人项目分组:按"项目ID_组别"分组
Map<String, List<MartialAthlete>> individualGroupMap = individualAthletes.stream()
.collect(Collectors.groupingBy(a ->
a.getProjectId() + "_" + Func.toStr(a.getCategory(), "未分组")
));
for (Map.Entry<String, List<MartialAthlete>> entry : individualGroupMap.entrySet()) {
List<MartialAthlete> members = entry.getValue();
if (members.isEmpty()) continue;
MartialAthlete first = members.get(0);
MartialProject project = projectMap.get(first.getProjectId());
// 跳过没有项目信息的分组
if (project == null) {
log.warn("项目不存在, projectId: {}, 跳过该分组", first.getProjectId());
continue;
}
String projectName = project.getProjectName();
String categoryName = first.getCategory() != null ? first.getCategory() : "未分组";
// 计算单人时长
int durationPerPerson = 5; // 默认5分钟/人
if (project.getEstimatedDuration() != null && project.getEstimatedDuration() > 0) {
durationPerPerson = project.getEstimatedDuration();
}
// 自动拆分大组:如果人数过多,拆分成多个小组
// 改进策略:根据人数动态拆分,确保能充分利用所有时间槽
// 目标:让分组数量接近可用时间槽数量,实现均匀分配
int maxPeoplePerGroup;
if (members.size() <= 35) {
// 35人以内不拆分
maxPeoplePerGroup = members.size();
} else {
// 超过35人,按每组30-35人拆分
maxPeoplePerGroup = 33; // 每组33人左右
}
if (members.size() <= maxPeoplePerGroup) {
// 人数不多,不需要拆分
ScheduleGroupData group = new ScheduleGroupData();
group.setGroupName(projectName + " " + categoryName);
group.setProjectId(first.getProjectId());
group.setProjectType(1);
group.setDisplayOrder(displayOrder++);
group.setTotalParticipants(members.size());
group.setAthletes(members);
int duration = members.size() * durationPerPerson;
group.setEstimatedDuration(duration);
log.debug("个人项目 '{}': {}人, {}分钟/人, 总计{}分钟",
projectName + " " + categoryName, members.size(), durationPerPerson, duration);
groups.add(group);
} else {
// 人数太多,需要拆分成多个小组
int totalPeople = members.size();
int numSubGroups = (int) Math.ceil((double) totalPeople / maxPeoplePerGroup);
log.info("个人项目 '{}' 有{}人,将拆分成{}个小组(每组最多{}人)",
projectName + " " + categoryName, totalPeople, numSubGroups, maxPeoplePerGroup);
for (int i = 0; i < numSubGroups; i++) {
int startIdx = i * maxPeoplePerGroup;
int endIdx = Math.min(startIdx + maxPeoplePerGroup, totalPeople);
List<MartialAthlete> subGroupMembers = members.subList(startIdx, endIdx);
ScheduleGroupData group = new ScheduleGroupData();
group.setGroupName(projectName + " " + categoryName + "" + (i + 1) + "");
group.setProjectId(first.getProjectId());
group.setProjectType(1);
group.setDisplayOrder(displayOrder++);
group.setTotalParticipants(subGroupMembers.size());
group.setAthletes(new ArrayList<>(subGroupMembers));
int duration = subGroupMembers.size() * durationPerPerson;
group.setEstimatedDuration(duration);
log.debug(" 拆分子组 '第{}组': {}人, 总计{}分钟",
i + 1, subGroupMembers.size(), duration);
groups.add(group);
}
}
}
return groups;
}
/**
* 验证时间窗口容量是否足够容纳所有分组
*/
private void validateCapacity(List<ScheduleGroupData> groups,
List<MartialVenue> venues,
List<TimeSlot> timeSlots) {
// 计算总需求时长
int totalDuration = groups.stream()
.mapToInt(ScheduleGroupData::getEstimatedDuration)
.sum();
// 计算总可用容量
int totalCapacity = venues.size() * timeSlots.size() * (timeSlots.isEmpty() ? 0 : timeSlots.get(0).getCapacity());
log.info("=== 容量验证 ===");
log.info("分组总需求时长: {} 分钟", totalDuration);
log.info("总可用容量: {} 分钟 ({}个场地 × {}个时段 × {}分钟/时段)",
totalCapacity, venues.size(), timeSlots.size(),
timeSlots.isEmpty() ? 0 : timeSlots.get(0).getCapacity());
if (totalDuration > totalCapacity) {
String errorMsg = String.format(
"时间窗口容量不足! 需要 %d 分钟, 但只有 %d 分钟可用 (缺口: %d 分钟)",
totalDuration, totalCapacity, totalDuration - totalCapacity
);
log.error(errorMsg);
throw new RuntimeException(errorMsg);
}
double utilizationRate = totalCapacity > 0 ? (totalDuration * 100.0 / totalCapacity) : 0;
log.info("预计容量利用率: {}%", (int)utilizationRate);
if (utilizationRate > 90) {
log.warn("⚠️ 容量利用率超过90%,可能导致分配困难,建议增加场地或延长比赛时间");
}
}
private void assignVenueAndTimeSlot(List<ScheduleGroupData> groups,
List<MartialVenue> venues,
List<TimeSlot> timeSlots) {
log.info("=== 开始分配场地和时间段 ===");
log.info("场地数量: {}, 时间段数量: {}, 分组数量: {}", venues.size(), timeSlots.size(), groups.size());
// 创建所有槽位(场地 × 时间段组合)
List<SlotInfo> slots = new ArrayList<>();
int timeSlotIndex = 0; // 时间段索引,对应前端的 timeSlotIndex
for (TimeSlot timeSlot : timeSlots) { // 先遍历时间段
for (MartialVenue venue : venues) { // 再遍历场地
SlotInfo slot = new SlotInfo();
slot.timeSlotIndex = timeSlotIndex; // 同一时间段的所有场地,共享相同的 timeSlotIndex
slot.venueId = venue.getId();
slot.venueName = venue.getVenueName();
slot.date = timeSlot.getDate();
slot.timeSlot = timeSlot.getStartTime();
slot.period = timeSlot.getPeriod();
slot.capacity = timeSlot.getCapacity();
slot.currentLoad = 0;
slots.add(slot);
}
timeSlotIndex++; // 每个时间段结束后,索引+1
}
log.info("总共初始化了 {} 个场地×时间段组合", slots.size());
// 按预计时长降序排序(先安排时间长的)
groups.sort((a, b) -> b.getEstimatedDuration() - a.getEstimatedDuration());
// 使用轮询算法进行均匀分配
int assignedCount = 0;
int currentSlotIndex = 0;
for (ScheduleGroupData group : groups) {
SlotInfo bestSlot = null;
int attempts = 0;
int maxAttempts = slots.size();
// 第一轮:从当前槽位开始轮询,寻找第一个容量足够的槽位
int startIndex = currentSlotIndex;
while (attempts < maxAttempts) {
SlotInfo slot = slots.get(currentSlotIndex);
// 检查容量是否足够
if (slot.currentLoad + group.getEstimatedDuration() <= slot.capacity) {
bestSlot = slot;
break;
}
// 尝试下一个槽位
currentSlotIndex = (currentSlotIndex + 1) % slots.size();
attempts++;
}
// 第二轮:如果没有找到容量足够的槽位,选择负载最小的槽位(允许超载)
if (bestSlot == null) {
log.warn("分组 '{}' 需要{}分钟,超过单个槽位容量{}分钟,将选择负载最小的槽位(允许超载)",
group.getGroupName(), group.getEstimatedDuration(), slots.get(0).capacity);
bestSlot = slots.stream()
.min((a, b) -> Integer.compare(a.currentLoad, b.currentLoad))
.orElse(slots.get(0));
// 设置currentSlotIndex到bestSlot的位置
for (int i = 0; i < slots.size(); i++) {
if (slots.get(i) == bestSlot) {
currentSlotIndex = i;
break;
}
}
}
// 分配到选定的槽位
group.setAssignedVenueId(bestSlot.venueId);
group.setAssignedVenueName(bestSlot.venueName);
group.setAssignedDate(bestSlot.date);
group.setAssignedTimeSlot(bestSlot.timeSlot);
group.setAssignedTimeSlotIndex(bestSlot.timeSlotIndex); // 保存时间段索引
group.setAssignedTimePeriod(bestSlot.period);
// 更新槽位负载
bestSlot.currentLoad += group.getEstimatedDuration();
assignedCount++;
log.info("分组 '{}' 分配到: 场地ID={}, 场地名={}, 日期={}, 时间段={}, 预计时长={}分钟, 新负载={}分钟 ({}%)",
group.getGroupName(), bestSlot.venueId, bestSlot.venueName, bestSlot.date, bestSlot.timeSlot,
group.getEstimatedDuration(), bestSlot.currentLoad,
(int)(bestSlot.currentLoad * 100.0 / bestSlot.capacity));
// 移动到下一个槽位(轮询)
currentSlotIndex = (currentSlotIndex + 1) % slots.size();
}
log.info("=== 分配完成: {}/{} 个分组成功分配 ===", assignedCount, groups.size());
// 输出每个槽位的负载情况
log.info("=== 各槽位负载统计 ===");
for (SlotInfo slot : slots) {
if (slot.currentLoad > 0) {
log.info("场地={}, 日期={}, 时段={}, 负载={}/{}分钟 ({}%)",
slot.venueName, slot.date, slot.timeSlot, slot.currentLoad, slot.capacity,
(int)(slot.currentLoad * 100.0 / slot.capacity));
}
}
}
// 槽位信息内部类
private static class SlotInfo {
int timeSlotIndex; // 时间段索引 (0=第1天上午, 1=第1天下午, 2=第2天上午, ...)
Long venueId;
String venueName;
LocalDate date;
String timeSlot;
String period;
int capacity;
int currentLoad;
}
private void clearOldScheduleData(Long competitionId) {
// 删除旧的编排数据
LambdaQueryWrapper<MartialScheduleGroup> groupWrapper = new LambdaQueryWrapper<>();
groupWrapper.eq(MartialScheduleGroup::getCompetitionId, competitionId);
// 先查询出所有分组ID,然后再删除
List<Long> groupIds = scheduleGroupMapper.selectList(groupWrapper).stream()
.map(MartialScheduleGroup::getId)
.collect(Collectors.toList());
// 删除参赛者关联(必须在删除分组之前)
if (groupIds != null && !groupIds.isEmpty()) {
LambdaQueryWrapper<MartialScheduleParticipant> participantWrapper = new LambdaQueryWrapper<>();
participantWrapper.in(MartialScheduleParticipant::getScheduleGroupId, groupIds);
scheduleParticipantMapper.delete(participantWrapper);
}
// 删除场地时间段详情
LambdaQueryWrapper<MartialScheduleDetail> detailWrapper = new LambdaQueryWrapper<>();
detailWrapper.eq(MartialScheduleDetail::getCompetitionId, competitionId);
scheduleDetailMapper.delete(detailWrapper);
// 最后删除分组
scheduleGroupMapper.delete(groupWrapper);
}
private void saveScheduleData(Long competitionId, List<ScheduleGroupData> groups) {
for (ScheduleGroupData groupData : groups) {
// 保存分组
MartialScheduleGroup group = new MartialScheduleGroup();
group.setCompetitionId(competitionId);
group.setGroupName(groupData.getGroupName());
group.setProjectId(groupData.getProjectId());
group.setProjectName(groupData.getGroupName());
group.setProjectType(groupData.getProjectType());
group.setDisplayOrder(groupData.getDisplayOrder());
group.setTotalParticipants(groupData.getTotalParticipants());
group.setTotalTeams(groupData.getTotalTeams());
group.setEstimatedDuration(groupData.getEstimatedDuration());
group.setCreateTime(new Date());
scheduleGroupMapper.insert(group);
Long groupId = group.getId();
// 保存场地时间段详情
if (groupData.getAssignedVenueId() != null) {
MartialScheduleDetail detail = new MartialScheduleDetail();
detail.setScheduleGroupId(groupId);
detail.setCompetitionId(competitionId);
detail.setVenueId(groupData.getAssignedVenueId());
detail.setVenueName(groupData.getAssignedVenueName());
detail.setScheduleDate(groupData.getAssignedDate());
detail.setTimePeriod(groupData.getAssignedTimePeriod());
detail.setTimeSlot(groupData.getAssignedTimeSlot());
detail.setTimeSlotIndex(groupData.getAssignedTimeSlotIndex()); // 保存时间段索引
detail.setEstimatedDuration(groupData.getEstimatedDuration());
detail.setParticipantCount(groupData.getTotalParticipants());
detail.setCreateTime(new Date());
scheduleDetailMapper.insert(detail);
Long detailId = detail.getId();
// 保存参赛者关联
int order = 1;
for (MartialAthlete athlete : groupData.getAthletes()) {
MartialScheduleParticipant participant = new MartialScheduleParticipant();
participant.setScheduleDetailId(detailId);
participant.setScheduleGroupId(groupId);
participant.setParticipantId(athlete.getId());
participant.setOrganization(athlete.getOrganization());
participant.setPlayerName(athlete.getPlayerName());
participant.setProjectName(groupData.getGroupName());
participant.setCategory(athlete.getCategory());
participant.setPerformanceOrder(order++);
participant.setCreateTime(new Date());
scheduleParticipantMapper.insert(participant);
}
}
}
}
// ==================== 内部数据类 ====================
private static class TimeSlot {
private LocalDate date;
private String period; // morning/afternoon
private String startTime; // 08:30/13:30
private Integer capacity; // 容量(分钟)
// Getters and Setters
public LocalDate getDate() {
return date;
}
public void setDate(LocalDate date) {
this.date = date;
}
public String getPeriod() {
return period;
}
public void setPeriod(String period) {
this.period = period;
}
public String getStartTime() {
return startTime;
}
public void setStartTime(String startTime) {
this.startTime = startTime;
}
public Integer getCapacity() {
return capacity;
}
public void setCapacity(Integer capacity) {
this.capacity = capacity;
}
}
private static class ScheduleGroupData {
private String groupName;
private Long projectId;
private Integer projectType;
private Integer displayOrder;
private Integer totalParticipants;
private Integer totalTeams;
private Integer estimatedDuration;
private List<MartialAthlete> athletes;
// 分配结果
private Long assignedVenueId;
private String assignedVenueName;
private LocalDate assignedDate;
private String assignedTimePeriod;
private String assignedTimeSlot;
private Integer assignedTimeSlotIndex; // 时间段索引
// Getters and Setters
public String getGroupName() {
return groupName;
}
public void setGroupName(String groupName) {
this.groupName = groupName;
}
public Long getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
this.projectId = projectId;
}
public Integer getProjectType() {
return projectType;
}
public void setProjectType(Integer projectType) {
this.projectType = projectType;
}
public Integer getDisplayOrder() {
return displayOrder;
}
public void setDisplayOrder(Integer displayOrder) {
this.displayOrder = displayOrder;
}
public Integer getTotalParticipants() {
return totalParticipants;
}
public void setTotalParticipants(Integer totalParticipants) {
this.totalParticipants = totalParticipants;
}
public Integer getTotalTeams() {
return totalTeams;
}
public void setTotalTeams(Integer totalTeams) {
this.totalTeams = totalTeams;
}
public Integer getEstimatedDuration() {
return estimatedDuration;
}
public void setEstimatedDuration(Integer estimatedDuration) {
this.estimatedDuration = estimatedDuration;
}
public List<MartialAthlete> getAthletes() {
return athletes;
}
public void setAthletes(List<MartialAthlete> athletes) {
this.athletes = athletes;
}
public Long getAssignedVenueId() {
return assignedVenueId;
}
public void setAssignedVenueId(Long assignedVenueId) {
this.assignedVenueId = assignedVenueId;
}
public String getAssignedVenueName() {
return assignedVenueName;
}
public void setAssignedVenueName(String assignedVenueName) {
this.assignedVenueName = assignedVenueName;
}
public LocalDate getAssignedDate() {
return assignedDate;
}
public void setAssignedDate(LocalDate assignedDate) {
this.assignedDate = assignedDate;
}
public String getAssignedTimePeriod() {
return assignedTimePeriod;
}
public void setAssignedTimePeriod(String assignedTimePeriod) {
this.assignedTimePeriod = assignedTimePeriod;
}
public String getAssignedTimeSlot() {
return assignedTimeSlot;
}
public void setAssignedTimeSlot(String assignedTimeSlot) {
this.assignedTimeSlot = assignedTimeSlot;
}
public Integer getAssignedTimeSlotIndex() {
return assignedTimeSlotIndex;
}
public void setAssignedTimeSlotIndex(Integer assignedTimeSlotIndex) {
this.assignedTimeSlotIndex = assignedTimeSlotIndex;
}
}
}

View File

@@ -3,15 +3,25 @@ package org.springblade.modules.martial.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springblade.modules.martial.excel.ScheduleExportExcel;
import org.springblade.modules.martial.mapper.MartialScheduleDetailMapper;
import org.springblade.modules.martial.mapper.MartialScheduleGroupMapper;
import org.springblade.modules.martial.mapper.MartialScheduleParticipantMapper;
import org.springblade.modules.martial.pojo.dto.CompetitionGroupDTO;
import org.springblade.modules.martial.pojo.dto.ParticipantDTO;
import org.springblade.modules.martial.pojo.dto.SaveScheduleDraftDTO;
import org.springblade.modules.martial.pojo.dto.ScheduleResultDTO;
import org.springblade.modules.martial.pojo.entity.*;
import org.springblade.modules.martial.pojo.vo.ScheduleGroupDetailVO;
import org.springblade.modules.martial.mapper.MartialScheduleMapper;
import org.springblade.modules.martial.service.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
/**
* Schedule 服务实现类
@@ -33,6 +43,15 @@ public class MartialScheduleServiceImpl extends ServiceImpl<MartialScheduleMappe
@Autowired
private IMartialVenueService venueService;
@Autowired
private MartialScheduleGroupMapper scheduleGroupMapper;
@Autowired
private MartialScheduleDetailMapper scheduleDetailMapper;
@Autowired
private MartialScheduleParticipantMapper scheduleParticipantMapper;
/**
* Task 3.3: 导出赛程表
*
@@ -120,4 +139,316 @@ public class MartialScheduleServiceImpl extends ServiceImpl<MartialScheduleMappe
return exportList;
}
/**
* 获取赛程编排结果优化版本使用单次JOIN查询
*
* @param competitionId 赛事ID
* @return 赛程编排结果
*/
@Override
public ScheduleResultDTO getScheduleResult(Long competitionId) {
ScheduleResultDTO result = new ScheduleResultDTO();
// 使用优化的一次性JOIN查询获取所有数据
List<ScheduleGroupDetailVO> details = scheduleGroupMapper.selectScheduleGroupDetails(competitionId);
if (details.isEmpty()) {
result.setIsDraft(true);
result.setIsCompleted(false);
result.setCompetitionGroups(new ArrayList<>());
return result;
}
// 按分组ID分组数据
Map<Long, List<ScheduleGroupDetailVO>> groupMap = details.stream()
.collect(Collectors.groupingBy(ScheduleGroupDetailVO::getGroupId));
// 检查编排状态
boolean isCompleted = details.stream()
.anyMatch(d -> "completed".equals(d.getScheduleStatus()));
boolean isDraft = !isCompleted;
result.setIsDraft(isDraft);
result.setIsCompleted(isCompleted);
// 组装数据
List<CompetitionGroupDTO> groupDTOs = new ArrayList<>();
for (Map.Entry<Long, List<ScheduleGroupDetailVO>> entry : groupMap.entrySet()) {
List<ScheduleGroupDetailVO> groupDetails = entry.getValue();
if (groupDetails.isEmpty()) {
continue;
}
// 获取第一条记录作为分组信息(同一分组的记录,分组信息是相同的)
ScheduleGroupDetailVO firstDetail = groupDetails.get(0);
CompetitionGroupDTO groupDTO = new CompetitionGroupDTO();
groupDTO.setId(firstDetail.getGroupId());
groupDTO.setTitle(firstDetail.getGroupName());
groupDTO.setCode(firstDetail.getCategory());
// 设置类型
if (firstDetail.getProjectType() != null) {
switch (firstDetail.getProjectType()) {
case 1:
groupDTO.setType("单人");
break;
case 2:
groupDTO.setType("集体");
break;
default:
groupDTO.setType("其他");
break;
}
}
// 设置队伍数量
if (firstDetail.getTotalTeams() != null && firstDetail.getTotalTeams() > 0) {
groupDTO.setCount(firstDetail.getTotalTeams() + "");
} else if (firstDetail.getTotalParticipants() != null) {
groupDTO.setCount(firstDetail.getTotalParticipants() + "");
}
// 设置场地和时间段信息
groupDTO.setVenueId(firstDetail.getVenueId());
groupDTO.setVenueName(firstDetail.getVenueName());
groupDTO.setTimeSlot(firstDetail.getTimeSlot());
groupDTO.setTimeSlotIndex(firstDetail.getTimeSlotIndex() != null ? firstDetail.getTimeSlotIndex() : 0); // 直接从数据库读取
// 获取参赛者列表
List<ParticipantDTO> participantDTOs = groupDetails.stream()
.filter(d -> d.getParticipantId() != null) // 过滤掉没有参赛者的记录
.map(d -> {
ParticipantDTO dto = new ParticipantDTO();
dto.setId(d.getParticipantId());
dto.setSchoolUnit(d.getOrganization());
dto.setStatus(d.getCheckInStatus() != null ? d.getCheckInStatus() : "未签到");
dto.setSortOrder(d.getPerformanceOrder());
return dto;
})
.collect(Collectors.toList());
groupDTO.setParticipants(participantDTOs);
groupDTOs.add(groupDTO);
}
// 按 displayOrder 排序
groupDTOs.sort(Comparator.comparing(g -> {
ScheduleGroupDetailVO detail = details.stream()
.filter(d -> d.getGroupId().equals(g.getId()))
.findFirst()
.orElse(null);
return detail != null ? detail.getDisplayOrder() : 999;
}));
result.setCompetitionGroups(groupDTOs);
return result;
}
/**
* 保存编排草稿
*
* @param dto 编排草稿数据
* @return 是否成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveDraftSchedule(SaveScheduleDraftDTO dto) {
if (dto.getCompetitionGroups() == null || dto.getCompetitionGroups().isEmpty()) {
return false;
}
for (CompetitionGroupDTO groupDTO : dto.getCompetitionGroups()) {
// 1. 更新或创建编排明细
MartialScheduleDetail detail = scheduleDetailMapper.selectOne(
new QueryWrapper<MartialScheduleDetail>()
.eq("schedule_group_id", groupDTO.getId())
.eq("is_deleted", 0)
.last("LIMIT 1")
);
if (detail == null) {
detail = new MartialScheduleDetail();
detail.setScheduleGroupId(groupDTO.getId());
detail.setCompetitionId(dto.getCompetitionId());
}
detail.setVenueId(groupDTO.getVenueId());
detail.setVenueName(groupDTO.getVenueName());
detail.setTimeSlot(groupDTO.getTimeSlot());
// 解析日期
if (groupDTO.getTimeSlot() != null && groupDTO.getTimeSlot().contains("")) {
try {
String dateStr = groupDTO.getTimeSlot().split(" ")[0];
dateStr = dateStr.replace("", "-").replace("", "-").replace("", "");
detail.setScheduleDate(LocalDate.parse(dateStr));
} catch (Exception e) {
// 日期解析失败,忽略
}
}
if (detail.getId() == null) {
scheduleDetailMapper.insert(detail);
} else {
scheduleDetailMapper.updateById(detail);
}
// 2. 更新参赛者信息
if (groupDTO.getParticipants() != null) {
for (ParticipantDTO participantDTO : groupDTO.getParticipants()) {
MartialScheduleParticipant participant = scheduleParticipantMapper.selectById(participantDTO.getId());
if (participant != null) {
participant.setCheckInStatus(participantDTO.getStatus());
participant.setPerformanceOrder(participantDTO.getSortOrder());
participant.setScheduleStatus("draft");
scheduleParticipantMapper.updateById(participant);
}
}
}
}
return true;
}
/**
* 完成编排并锁定
*
* @param competitionId 赛事ID
* @return 是否成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveAndLockSchedule(Long competitionId) {
// 1. 查询所有分组
List<MartialScheduleGroup> groups = scheduleGroupMapper.selectList(
new QueryWrapper<MartialScheduleGroup>()
.eq("competition_id", competitionId)
.eq("is_deleted", 0)
);
if (groups.isEmpty()) {
return false;
}
// 2. 获取所有分组ID
List<Long> groupIds = groups.stream().map(MartialScheduleGroup::getId).collect(Collectors.toList());
// 3. 更新所有参赛者的编排状态为completed
List<MartialScheduleParticipant> participants = scheduleParticipantMapper.selectList(
new QueryWrapper<MartialScheduleParticipant>()
.in("schedule_group_id", groupIds)
.eq("is_deleted", 0)
);
for (MartialScheduleParticipant participant : participants) {
participant.setScheduleStatus("completed");
scheduleParticipantMapper.updateById(participant);
}
return true;
}
/**
* 根据时间段字符串计算 timeSlotIndex
* 按照时间从早到晚分配索引: 08:30=0, 13:30=1, 其他时间按字符串顺序排序
*
* @param timeSlot 时间段字符串 (如 "08:30", "13:30")
* @return 时间段索引
*/
private int calculateTimeSlotIndex(String timeSlot) {
if (timeSlot == null || timeSlot.trim().isEmpty()) {
return 0;
}
// 标准时间段映射
switch (timeSlot.trim()) {
case "08:30":
return 0; // 上午场
case "13:30":
return 1; // 下午场
default:
// 其他时间段,尝试解析并分配索引
// 如果是上午(< 12:00)返回0,下午(>= 12:00)返回1
try {
String[] parts = timeSlot.trim().split(":");
if (parts.length >= 1) {
int hour = Integer.parseInt(parts[0]);
return hour < 12 ? 0 : 1;
}
} catch (Exception e) {
// 解析失败,返回默认值
}
return 0;
}
}
/**
* 移动赛程分组到指定场地和时间段
*
* @param dto 移动请求数据
* @return 是否成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean moveScheduleGroup(org.springblade.modules.martial.pojo.dto.MoveScheduleGroupDTO dto) {
// 1. 查询分组信息
MartialScheduleGroup group = scheduleGroupMapper.selectById(dto.getGroupId());
if (group == null) {
throw new RuntimeException("分组不存在");
}
// 2. 查询该分组的详情记录
List<MartialScheduleDetail> details = scheduleDetailMapper.selectList(
new QueryWrapper<MartialScheduleDetail>()
.eq("schedule_group_id", dto.getGroupId())
.eq("is_deleted", 0)
);
if (details.isEmpty()) {
throw new RuntimeException("分组详情不存在");
}
// 3. 查询目标场地信息
MartialVenue targetVenue = venueService.getById(dto.getTargetVenueId());
if (targetVenue == null) {
throw new RuntimeException("目标场地不存在");
}
// 4. 根据时间段索引计算日期和时间
// 假设: 0=第1天上午, 1=第1天下午, 2=第2天上午, 3=第2天下午...
// 需要从赛事信息中获取起始日期
int dayOffset = dto.getTargetTimeSlotIndex() / 2; // 每天2个时段
boolean isAfternoon = dto.getTargetTimeSlotIndex() % 2 == 1;
String timeSlot = isAfternoon ? "13:30" : "08:30";
// 获取赛事起始日期(从第一个detail中获取)
LocalDate baseDate = details.get(0).getScheduleDate();
if (baseDate == null) {
throw new RuntimeException("无法确定赛事起始日期");
}
// 计算目标日期(从起始日期开始,加上dayOffset天的偏移)
// 如果当前detail的日期早于base date,需要调整
LocalDate minDate = details.stream()
.map(MartialScheduleDetail::getScheduleDate)
.filter(Objects::nonNull)
.min(LocalDate::compareTo)
.orElse(baseDate);
LocalDate targetDate = minDate.plusDays(dayOffset);
// 5. 更新所有detail记录
for (MartialScheduleDetail detail : details) {
detail.setVenueId(dto.getTargetVenueId());
detail.setVenueName(targetVenue.getVenueName());
detail.setScheduleDate(targetDate);
detail.setTimeSlot(timeSlot);
detail.setTimeSlotIndex(dto.getTargetTimeSlotIndex());
scheduleDetailMapper.updateById(detail);
}
return true;
}
}