diff --git a/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta b/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta index 05f6351..a222be5 100644 Binary files a/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta and b/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta differ diff --git a/minio_data/.minio.sys/buckets/.usage-cache.bin.bkp/xl.meta b/minio_data/.minio.sys/buckets/.usage-cache.bin.bkp/xl.meta index 52e3f9a..44bb2bf 100644 Binary files a/minio_data/.minio.sys/buckets/.usage-cache.bin.bkp/xl.meta and b/minio_data/.minio.sys/buckets/.usage-cache.bin.bkp/xl.meta differ diff --git a/minio_data/.minio.sys/buckets/.usage-cache.bin/xl.meta b/minio_data/.minio.sys/buckets/.usage-cache.bin/xl.meta index c0ca47f..5d743d5 100644 Binary files a/minio_data/.minio.sys/buckets/.usage-cache.bin/xl.meta and b/minio_data/.minio.sys/buckets/.usage-cache.bin/xl.meta differ diff --git a/minio_data/.minio.sys/buckets/.usage.json/xl.meta b/minio_data/.minio.sys/buckets/.usage.json/xl.meta index 6cf0705..9c2ab0e 100644 Binary files a/minio_data/.minio.sys/buckets/.usage.json/xl.meta and b/minio_data/.minio.sys/buckets/.usage.json/xl.meta differ diff --git a/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin.bkp/xl.meta b/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin.bkp/xl.meta index 8e176c6..f70f5be 100644 Binary files a/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin.bkp/xl.meta and b/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin.bkp/xl.meta differ diff --git a/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin/xl.meta b/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin/xl.meta index d1639ad..ddd7e8c 100644 Binary files a/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin/xl.meta and b/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin/xl.meta differ diff --git a/minio_data/.minio.sys/buckets/assets/.usage-cache.bin.bkp/xl.meta b/minio_data/.minio.sys/buckets/assets/.usage-cache.bin.bkp/xl.meta index 1394540..7d13aa9 100644 Binary files a/minio_data/.minio.sys/buckets/assets/.usage-cache.bin.bkp/xl.meta and b/minio_data/.minio.sys/buckets/assets/.usage-cache.bin.bkp/xl.meta differ diff --git a/minio_data/.minio.sys/buckets/assets/.usage-cache.bin/xl.meta b/minio_data/.minio.sys/buckets/assets/.usage-cache.bin/xl.meta index 02961a5..8482067 100644 Binary files a/minio_data/.minio.sys/buckets/assets/.usage-cache.bin/xl.meta and b/minio_data/.minio.sys/buckets/assets/.usage-cache.bin/xl.meta differ diff --git a/src/main/java/org/springblade/modules/martial/controller/MartialExportController.java b/src/main/java/org/springblade/modules/martial/controller/MartialExportController.java index 21ac3dd..dff95ad 100644 --- a/src/main/java/org/springblade/modules/martial/controller/MartialExportController.java +++ b/src/main/java/org/springblade/modules/martial/controller/MartialExportController.java @@ -10,26 +10,19 @@ import org.springblade.core.tool.utils.DateUtil; import org.springblade.modules.martial.excel.AthleteExportExcel; import org.springblade.modules.martial.excel.ResultExportExcel; import org.springblade.modules.martial.excel.ScheduleExportExcel; +import org.springblade.modules.martial.excel.ScheduleExportExcel2; import org.springblade.modules.martial.pojo.vo.CertificateVO; import org.springblade.modules.martial.service.IMartialAthleteService; import org.springblade.modules.martial.service.IMartialResultService; import org.springblade.modules.martial.service.IMartialScheduleService; import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.ModelAndView; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.HashMap; import java.util.List; -import java.util.Map; -/** - * 导出打印 控制器 - * - * @author BladeX - */ @RestController @AllArgsConstructor @RequestMapping("/martial/export") @@ -40,67 +33,47 @@ public class MartialExportController { private final IMartialAthleteService athleteService; private final IMartialScheduleService scheduleService; - /** - * Task 3.1: 导出成绩单 - */ @GetMapping("/results") @Operation(summary = "导出成绩单", description = "导出指定赛事或项目的成绩单Excel") - public void exportResults( - @RequestParam Long competitionId, - @RequestParam(required = false) Long projectId, - HttpServletResponse response - ) { + public void exportResults(@RequestParam Long competitionId, @RequestParam(required = false) Long projectId, HttpServletResponse response) { List list = resultService.exportResults(competitionId, projectId); String fileName = "成绩单_" + DateUtil.today(); String sheetName = projectId != null ? "项目成绩单" : "全部成绩"; ExcelUtil.export(response, fileName, sheetName, list, ResultExportExcel.class); } - /** - * Task 3.2: 导出运动员名单 - */ @GetMapping("/athletes") @Operation(summary = "导出运动员名单", description = "导出指定赛事的运动员名单Excel") - public void exportAthletes( - @RequestParam Long competitionId, - HttpServletResponse response - ) { + public void exportAthletes(@RequestParam Long competitionId, HttpServletResponse response) { List list = athleteService.exportAthletes(competitionId); String fileName = "运动员名单_" + DateUtil.today(); ExcelUtil.export(response, fileName, "运动员名单", list, AthleteExportExcel.class); } - /** - * Task 3.3: 导出赛程表 - */ @GetMapping("/schedule") @Operation(summary = "导出赛程表", description = "导出指定赛事的赛程安排Excel") - public void exportSchedule( - @RequestParam Long competitionId, - HttpServletResponse response - ) { + public void exportSchedule(@RequestParam Long competitionId, HttpServletResponse response) { List list = scheduleService.exportSchedule(competitionId); String fileName = "赛程表_" + DateUtil.today(); ExcelUtil.export(response, fileName, "赛程安排", list, ScheduleExportExcel.class); } - /** - * Task 3.4: 生成单个证书(HTML格式) - */ - @GetMapping("/certificate/{resultId}") - @Operation(summary = "生成证书", description = "生成获奖证书HTML页面,可打印为PDF") - public void generateCertificate( - @PathVariable Long resultId, - HttpServletResponse response - ) throws IOException { - // 1. 获取证书数据 - CertificateVO certificate = resultService.generateCertificateData(resultId); + @GetMapping("/schedule2") + @Operation(summary = "导出赛程表-模板2", description = "按场地导出比赛时间表格式的赛程安排") + public void exportScheduleTemplate2(@RequestParam Long competitionId, @RequestParam(required = false) Long venueId, + @RequestParam(required = false) String venueName, @RequestParam(required = false) String timeSlot, HttpServletResponse response) { + List list = scheduleService.exportScheduleTemplate2(competitionId, venueId); + String fileName = "比赛时间_" + (venueName != null ? venueName : "全部场地") + "_" + DateUtil.today(); + String sheetName = (venueName != null ? venueName : "全部场地") + (timeSlot != null ? "_" + timeSlot : ""); + ExcelUtil.export(response, fileName, sheetName, list, ScheduleExportExcel2.class); + } - // 2. 读取HTML模板 + @GetMapping("/certificate/{resultId}") + @Operation(summary = "生成证书", description = "生成获奖证书HTML页面") + public void generateCertificate(@PathVariable Long resultId, HttpServletResponse response) throws IOException { + CertificateVO certificate = resultService.generateCertificateData(resultId); Path templatePath = Path.of("src/main/resources/templates/certificate/certificate.html"); String template = Files.readString(templatePath, StandardCharsets.UTF_8); - - // 3. 替换模板变量 String html = template .replace("${playerName}", certificate.getPlayerName()) .replace("${competitionName}", certificate.getCompetitionName()) @@ -109,15 +82,10 @@ public class MartialExportController { .replace("${medalClass}", certificate.getMedalClass()) .replace("${organization}", certificate.getOrganization()) .replace("${issueDate}", certificate.getIssueDate()); - - // 4. 返回HTML response.setContentType("text/html;charset=UTF-8"); response.getWriter().write(html); } - /** - * Task 3.4: 批量生成证书数据 - */ @GetMapping("/certificates/batch") @Operation(summary = "批量生成证书数据", description = "批量获取项目获奖选手的证书数据") public R> batchGenerateCertificates(@RequestParam Long projectId) { @@ -125,14 +93,10 @@ public class MartialExportController { return R.data(certificates); } - /** - * Task 3.4: 获取单个证书数据(JSON格式) - */ @GetMapping("/certificate/data/{resultId}") - @Operation(summary = "获取证书数据", description = "获取证书数据(JSON格式),供前端渲染") + @Operation(summary = "获取证书数据", description = "获取证书数据(JSON格式)") public R getCertificateData(@PathVariable Long resultId) { CertificateVO certificate = resultService.generateCertificateData(resultId); return R.data(certificate); } - } diff --git a/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java b/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java index a87bc25..83a7e81 100644 --- a/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java +++ b/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java @@ -19,6 +19,9 @@ import org.springblade.modules.martial.pojo.vo.MiniAthleteAdminVO; import org.springblade.modules.martial.pojo.vo.MiniAthleteScoreVO; import org.springblade.modules.martial.pojo.vo.MiniLoginVO; import org.springblade.modules.martial.pojo.vo.MiniScoreDetailVO; +import org.springblade.modules.martial.pojo.vo.LineupGroupVO; +import org.springblade.modules.martial.pojo.vo.LineupParticipantVO; +import org.springblade.modules.martial.pojo.vo.ScheduleGroupDetailVO; import com.alibaba.fastjson.JSON; import org.springblade.modules.martial.service.*; import org.springblade.modules.martial.pojo.dto.ChiefJudgeConfirmDTO; @@ -27,6 +30,8 @@ import org.springblade.modules.martial.pojo.entity.MtVenue; import org.springblade.modules.martial.pojo.entity.MartialVenue; import org.springblade.modules.martial.pojo.entity.MartialResult; import org.springblade.core.redis.cache.BladeRedis; +import org.springblade.modules.martial.mapper.MartialScheduleStatusMapper; +import org.springblade.modules.martial.mapper.MartialScheduleGroupMapper; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; @@ -34,6 +39,8 @@ import java.math.RoundingMode; import java.time.LocalDateTime; import java.time.Duration; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import java.util.List; import java.util.Objects; import java.util.UUID; @@ -60,6 +67,8 @@ public class MartialMiniController extends BladeController { private final IMartialScoreService scoreService; private final BladeRedis bladeRedis; private final IMartialResultService resultService; + private final MartialScheduleStatusMapper scheduleStatusMapper; + private final MartialScheduleGroupMapper scheduleGroupMapper; // Redis缓存key前缀 private static final String MINI_LOGIN_CACHE_PREFIX = "mini:login:"; @@ -817,4 +826,145 @@ public class MartialMiniController extends BladeController { return R.data(list); } + // ========== 出场顺序相关 API ========== + + /** + * 获取编排状态 + */ + @GetMapping("/schedule/status") + @Operation(summary = "获取编排状态", description = "检查赛事编排是否完成") + public R> getScheduleStatus(@RequestParam Long competitionId) { + Map result = new HashMap<>(); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(MartialScheduleStatus::getCompetitionId, competitionId); + wrapper.eq(MartialScheduleStatus::getIsDeleted, 0); + wrapper.last("LIMIT 1"); + MartialScheduleStatus status = scheduleStatusMapper.selectOne(wrapper); + + if (status == null) { + result.put("isCompleted", false); + result.put("scheduleStatus", 0); + result.put("statusText", "未编排"); + return R.data(result); + } + + boolean isCompleted = status.getScheduleStatus() != null && status.getScheduleStatus() == 2; + result.put("isCompleted", isCompleted); + result.put("scheduleStatus", status.getScheduleStatus()); + result.put("statusText", getScheduleStatusText(status.getScheduleStatus())); + result.put("totalGroups", status.getTotalGroups()); + result.put("totalParticipants", status.getTotalParticipants()); + result.put("lockedTime", status.getLockedTime()); + + return R.data(result); + } + + private String getScheduleStatusText(Integer status) { + if (status == null) return "未编排"; + switch (status) { + case 0: return "未编排"; + case 1: return "编排中"; + case 2: return "已锁定"; + default: return "未知"; + } + } + + /** + * 获取出场顺序 + */ + @GetMapping("/schedule/lineup") + @Operation(summary = "获取出场顺序", description = "获取已编排的出场顺序列表") + public R> getLineup( + @RequestParam Long competitionId, + @RequestParam(required = false) Long projectId + ) { + Map result = new HashMap<>(); + + // 使用现有mapper查询编排详情 + List details = scheduleGroupMapper.selectScheduleGroupDetails(competitionId); + + if (details == null || details.isEmpty()) { + result.put("groups", new ArrayList<>()); + return R.data(result); + } + + // 按项目过滤 + if (projectId != null) { + // 需要通过groupName或其他字段判断项目,这里先获取项目名 + MartialProject project = projectService.getById(projectId); + if (project != null) { + String projectName = project.getProjectName(); + details = details.stream() + .filter(d -> d.getGroupName() != null && d.getGroupName().contains(projectName)) + .collect(Collectors.toList()); + } + } + + // 转换为LineupGroupVO格式 + Map groupMap = new HashMap<>(); + for (ScheduleGroupDetailVO detail : details) { + Long groupId = detail.getGroupId(); + LineupGroupVO group = groupMap.get(groupId); + if (group == null) { + group = new LineupGroupVO(); + group.setGroupId(groupId); + group.setGroupName(detail.getGroupName()); + group.setCategory(detail.getCategory()); + group.setVenueName(detail.getVenueName()); + group.setTimeSlot(detail.getTimeSlot()); + group.setTableNo(generateTableNo(detail)); + group.setParticipants(new ArrayList<>()); + groupMap.put(groupId, group); + } + + // 添加参赛者 + if (detail.getParticipantId() != null) { + LineupParticipantVO participant = new LineupParticipantVO(); + participant.setId(detail.getParticipantId()); + participant.setOrder(detail.getPerformanceOrder() != null ? detail.getPerformanceOrder() : group.getParticipants().size() + 1); + participant.setPlayerName(detail.getPlayerName()); + participant.setOrganization(detail.getOrganization()); + participant.setStatus(detail.getScheduleStatus() != null ? detail.getScheduleStatus() : "waiting"); + group.getParticipants().add(participant); + } + } + + result.put("groups", new ArrayList<>(groupMap.values())); + return R.data(result); + } + + /** + * 生成表号: 场地(1位) + 时段(1位) + 序号(2位) + */ + private String generateTableNo(ScheduleGroupDetailVO detail) { + // 场地编号(简单取第一个数字或默认1) + int venueNo = 1; + if (detail.getVenueName() != null) { + String venueName = detail.getVenueName(); + for (char c : venueName.toCharArray()) { + if (Character.isDigit(c)) { + venueNo = Character.getNumericValue(c); + break; + } + } + } + + // 时段:上午=1, 下午=2 + int period = 1; + if (detail.getTimeSlot() != null) { + try { + int hour = Integer.parseInt(detail.getTimeSlot().split(":")[0]); + period = hour < 12 ? 1 : 2; + } catch (Exception e) { + // ignore + } + } + + // 序号:使用displayOrder或默认1 + int orderNo = detail.getDisplayOrder() != null ? detail.getDisplayOrder() : 1; + + return String.format("%d%d%02d", venueNo, period, orderNo); + } + } diff --git a/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java.backup b/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java.backup index fcd1a79..a87bc25 100644 --- a/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java.backup +++ b/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java.backup @@ -21,12 +21,21 @@ import org.springblade.modules.martial.pojo.vo.MiniLoginVO; import org.springblade.modules.martial.pojo.vo.MiniScoreDetailVO; import com.alibaba.fastjson.JSON; import org.springblade.modules.martial.service.*; +import org.springblade.modules.martial.pojo.dto.ChiefJudgeConfirmDTO; +import org.springblade.modules.martial.pojo.dto.GeneralJudgeConfirmDTO; +import org.springblade.modules.martial.pojo.entity.MtVenue; +import org.springblade.modules.martial.pojo.entity.MartialVenue; +import org.springblade.modules.martial.pojo.entity.MartialResult; +import org.springblade.core.redis.cache.BladeRedis; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDateTime; +import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; @@ -45,9 +54,17 @@ public class MartialMiniController extends BladeController { private final IMartialJudgeService judgeService; private final IMartialCompetitionService competitionService; private final IMartialVenueService venueService; + private final IMtVenueService mtVenueService; private final IMartialProjectService projectService; private final IMartialAthleteService athleteService; private final IMartialScoreService scoreService; + private final BladeRedis bladeRedis; + private final IMartialResultService resultService; + + // Redis缓存key前缀 + private static final String MINI_LOGIN_CACHE_PREFIX = "mini:login:"; + // 登录缓存过期时间(7天) + private static final Duration LOGIN_CACHE_EXPIRE = Duration.ofDays(7); /** * 登录验证 @@ -91,26 +108,55 @@ public class MartialMiniController extends BladeController { invite.setDeviceInfo(dto.getDeviceInfo()); judgeInviteService.updateById(invite); - MartialVenue venue = null; + // 从 martial_venue 表获取场地信息 + MartialVenue martialVenue = null; if (invite.getVenueId() != null) { - venue = venueService.getById(invite.getVenueId()); + martialVenue = venueService.getById(invite.getVenueId()); } - List projects = parseProjects(invite.getProjects()); + // 获取项目列表:总裁判看所有项目,其他裁判根据场地获取项目 + List projects = new ArrayList<>(); + Integer refereeTypeVal = invite.getRefereeType(); + String roleVal = invite.getRole(); + boolean isGeneralJudge = (refereeTypeVal != null && refereeTypeVal == 3) + || "general_judge".equals(roleVal) || "general".equals(roleVal); + + if (isGeneralJudge) { + // 总裁判看所有项目 + projects = getAllProjectsByCompetition(competition.getId()); + } else if (Func.isNotEmpty(invite.getProjects())) { + projects = parseProjects(invite.getProjects()); + } else if (invite.getVenueId() != null) { + // 未指定项目,根据场地获取项目;如果场地没有项目则返回空列表 + projects = getProjectsByVenue(invite.getVenueId()); + } + // 如果没有场地,projects保持为空列表 MiniLoginVO vo = new MiniLoginVO(); vo.setToken(token); - vo.setUserRole("chief_judge".equals(invite.getRole()) ? "admin" : "pub"); + String role = invite.getRole(); + Integer refereeType = invite.getRefereeType(); + if ("general_judge".equals(role) || "general".equals(role) || (refereeType != null && refereeType == 3)) { + vo.setUserRole("general"); + } else if ("chief_judge".equals(role) || (refereeType != null && refereeType == 1)) { + vo.setUserRole("admin"); + } else { + vo.setUserRole("pub"); + } vo.setMatchId(competition.getId()); vo.setMatchName(competition.getCompetitionName()); vo.setMatchTime(competition.getCompetitionStartTime() != null ? competition.getCompetitionStartTime().toString() : ""); vo.setJudgeId(judge.getId()); vo.setJudgeName(judge.getName()); - vo.setVenueId(venue != null ? venue.getId() : null); - vo.setVenueName(venue != null ? venue.getVenueName() : null); + vo.setVenueId(martialVenue != null ? martialVenue.getId() : null); + vo.setVenueName(martialVenue != null ? martialVenue.getVenueName() : null); vo.setProjects(projects); + // 将登录信息缓存到Redis(服务重启后仍然有效) + String cacheKey = MINI_LOGIN_CACHE_PREFIX + token; + bladeRedis.setEx(cacheKey, vo, LOGIN_CACHE_EXPIRE); + return R.data(vo); } @@ -152,8 +198,136 @@ public class MartialMiniController extends BladeController { } boolean success = scoreService.save(score); + + // 评分保存成功后,计算并更新选手总分 + if (success) { + Long athleteId = parseLong(dto.getAthleteId()); + Long projectId = parseLong(dto.getProjectId()); + Long venueId = parseLong(dto.getVenueId()); + if (athleteId != null && projectId != null) { + updateAthleteTotalScore(athleteId, projectId, venueId); + } + } + return success ? R.success("评分提交成功") : R.fail("评分提交失败"); } + + /** + * 计算并更新选手总分 + * 总分算法:去掉一个最高分和一个最低分,取剩余分数的平均值 + * 特殊情况:裁判数量<3时,直接取平均分 + * 只有所有裁判都评分完成后才更新总分 + */ + private void updateAthleteTotalScore(Long athleteId, Long projectId, Long venueId) { + try { + // 1. 查询该场地的裁判员数量 + int requiredJudgeCount = getRequiredJudgeCount(venueId); + + // 2. 获取主裁判ID列表 + List chiefJudgeIds = getChiefJudgeIds(venueId); + + // 3. 查询该选手在该项目的所有评分(排除主裁判的评分) + LambdaQueryWrapper scoreQuery = new LambdaQueryWrapper<>(); + scoreQuery.eq(MartialScore::getAthleteId, athleteId); + scoreQuery.eq(MartialScore::getProjectId, projectId); + scoreQuery.eq(MartialScore::getIsDeleted, 0); + // 排除主裁判的所有评分(包括普通评分和修改记录) + if (!chiefJudgeIds.isEmpty()) { + scoreQuery.notIn(MartialScore::getJudgeId, chiefJudgeIds); + } + List scores = scoreService.list(scoreQuery); + + // 4. 判断是否所有裁判都已评分 + if (scores == null || scores.isEmpty()) { + return; + } + + // 如果配置了裁判数量,检查是否评分完成 + if (requiredJudgeCount > 0 && scores.size() < requiredJudgeCount) { + // 未完成评分,清空总分 + MartialAthlete athlete = athleteService.getById(athleteId); + if (athlete != null && athlete.getTotalScore() != null) { + athlete.setTotalScore(null); + athleteService.updateById(athlete); + } + return; + } + + // 4. 计算总分(去掉最高最低分取平均) + BigDecimal totalScore = calculateTotalScore(scores); + + // 5. 更新选手总分 + if (totalScore != null) { + MartialAthlete athlete = athleteService.getById(athleteId); + if (athlete != null) { + athlete.setTotalScore(totalScore); + athleteService.updateById(athlete); + } + } + } catch (Exception e) { + // 记录错误但不影响评分提交 + e.printStackTrace(); + } + } + + /** + * 获取项目应评分的裁判数量(裁判员,不包括主裁判) + * 按项目过滤:检查 projects JSON 字段是否包含该项目ID + */ + private int getRequiredJudgeCount(Long venueId) { + if (venueId == null || venueId <= 0) { + return 0; + } + LambdaQueryWrapper judgeQuery = new LambdaQueryWrapper<>(); + judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0); + judgeQuery.eq(MartialJudgeInvite::getVenueId, venueId); + judgeQuery.eq(MartialJudgeInvite::getRefereeType, 2); // Only count referees (type=2), exclude chief judge (type=1) and general judge (type=3) + List judges = judgeInviteService.list(judgeQuery); + // Use distinct judge_id to count unique judges + return (int) judges.stream() + .map(MartialJudgeInvite::getJudgeId) + .filter(Objects::nonNull) + .distinct() + .count(); + } + + /** + * 计算总分 + * 算法:去掉一个最高分和一个最低分,取剩余分数的平均值 + * 特殊情况:裁判数量<3时,直接取平均分 + */ + private BigDecimal calculateTotalScore(List scores) { + if (scores == null || scores.isEmpty()) { + return null; + } + + // 提取所有分数并排序 + List scoreValues = scores.stream() + .map(MartialScore::getScore) + .filter(Objects::nonNull) + .sorted() + .collect(Collectors.toList()); + + int count = scoreValues.size(); + if (count == 0) { + return null; + } + + if (count < 3) { + // 裁判数量<3,直接取平均分 + BigDecimal sum = scoreValues.stream() + .reduce(BigDecimal.ZERO, BigDecimal::add); + return sum.divide(new BigDecimal(count), 3, RoundingMode.HALF_UP); + } + + // 去掉最高分和最低分(已排序,去掉第一个和最后一个) + List middleScores = scoreValues.subList(1, count - 1); + + // 计算平均分 + BigDecimal sum = middleScores.stream() + .reduce(BigDecimal.ZERO, BigDecimal::add); + return sum.divide(new BigDecimal(middleScores.size()), 3, RoundingMode.HALF_UP); + } /** * 安全地将String转换为Long @@ -171,8 +345,8 @@ public class MartialMiniController extends BladeController { /** * 获取选手列表(支持分页) - * - 普通裁判:获取所有选手,标记是否已评分 - * - 裁判长:获取已有评分的选手列表 + * - 裁判员:获取所有选手,标记是否已评分 + * - 主裁判:获取所有裁判员都评分完成的选手列表 */ @GetMapping("/score/athletes") @Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)") @@ -181,6 +355,7 @@ public class MartialMiniController extends BladeController { @RequestParam Integer refereeType, @RequestParam(required = false) Long projectId, @RequestParam(required = false) Long venueId, + @RequestParam(required = false) Long competitionId, @RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "10") Integer size ) { @@ -188,6 +363,11 @@ public class MartialMiniController extends BladeController { LambdaQueryWrapper athleteQuery = new LambdaQueryWrapper<>(); athleteQuery.eq(MartialAthlete::getIsDeleted, 0); + // 按比赛ID过滤(重要:确保只显示当前比赛的选手) + if (competitionId != null) { + athleteQuery.eq(MartialAthlete::getCompetitionId, competitionId); + } + if (projectId != null) { athleteQuery.eq(MartialAthlete::getProjectId, projectId); } @@ -196,35 +376,48 @@ public class MartialMiniController extends BladeController { List athletes = athleteService.list(athleteQuery); - // 2. 获取所有评分记录 + // 2. 获取该场地所有主裁判的judge_id列表 + List chiefJudgeIds = getChiefJudgeIds(venueId); + + // 3. 获取所有评分记录(排除主裁判的评分) LambdaQueryWrapper scoreQuery = new LambdaQueryWrapper<>(); scoreQuery.eq(MartialScore::getIsDeleted, 0); + if (projectId != null) { + scoreQuery.eq(MartialScore::getProjectId, projectId); + } + // 添加场地过滤 + if (venueId != null && venueId > 0) { + scoreQuery.eq(MartialScore::getVenueId, venueId); + } + // 排除主裁判的评分 + if (!chiefJudgeIds.isEmpty()) { + scoreQuery.notIn(MartialScore::getJudgeId, chiefJudgeIds); + } List allScores = scoreService.list(scoreQuery); // 按选手ID分组统计评分 java.util.Map> scoresByAthlete = allScores.stream() .collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId)); - // 3. 根据裁判类型处理选手列表 + // 4. 获取该场地的应评裁判数量 + int requiredJudgeCount = getRequiredJudgeCount(venueId); + + // 5. 根据裁判类型处理选手列表 List filteredList; if (refereeType == 1) { - // 裁判长:返回已有评分的选手 + // 主裁判:返回所有选手,前端根据totalScore判断是否显示修改按钮 filteredList = athletes.stream() - .filter(athlete -> { - List scores = scoresByAthlete.get(athlete.getId()); - return scores != null && !scores.isEmpty(); - }) - .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId)) + .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount)) .collect(java.util.stream.Collectors.toList()); } else { - // 普通裁判:返回所有选手,标记是否已评分 + // 裁判员:返回所有选手,标记是否已评分 filteredList = athletes.stream() - .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId)) + .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount)) .collect(java.util.stream.Collectors.toList()); } - // 4. 手动分页 + // 6. 手动分页 int total = filteredList.size(); int fromIndex = (current - 1) * size; int toIndex = Math.min(fromIndex + size, total); @@ -236,13 +429,31 @@ public class MartialMiniController extends BladeController { pageRecords = filteredList.subList(fromIndex, toIndex); } - // 5. 构建分页结果 + // 7. 构建分页结果 IPage page = new Page<>(current, size, total); page.setRecords(pageRecords); return R.data(page); } + /** + * 获取场地所有主裁判的judge_id列表 + */ + private List getChiefJudgeIds(Long venueId) { + if (venueId == null) { + return new ArrayList<>(); + } + LambdaQueryWrapper judgeQuery = new LambdaQueryWrapper<>(); + judgeQuery.eq(MartialJudgeInvite::getVenueId, venueId); + judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0); + judgeQuery.eq(MartialJudgeInvite::getRole, "chief_judge"); + List chiefJudges = judgeInviteService.list(judgeQuery); + return chiefJudges.stream() + .map(MartialJudgeInvite::getJudgeId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + /** * 获取评分详情 */ @@ -254,10 +465,10 @@ public class MartialMiniController extends BladeController { } /** - * 修改评分(裁判长) + * 修改评分(主裁判) */ @PutMapping("/score/modify") - @Operation(summary = "修改评分", description = "裁判长修改选手总分") + @Operation(summary = "修改评分", description = "主裁判修改选手总分") public R modifyScore(@RequestBody MiniScoreModifyDTO dto) { boolean success = scoreService.modifyScoreByAdmin(dto); return success ? R.success("修改成功") : R.fail("修改失败"); @@ -268,26 +479,107 @@ public class MartialMiniController extends BladeController { */ @PostMapping("/logout") @Operation(summary = "退出登录", description = "清除登录状态") - public R logout() { + public R logout(@RequestHeader(value = "Authorization", required = false) String token) { + // 从Redis删除登录缓存 + if (token != null && !token.isEmpty()) { + String cacheKey = MINI_LOGIN_CACHE_PREFIX + token; + bladeRedis.del(cacheKey); + } return R.success("退出成功"); } /** - * Token验证 + * Token验证(从Redis恢复登录状态) */ @GetMapping("/verify") - @Operation(summary = "Token验证", description = "验证当前token是否有效") - public R verify() { - return R.success("Token有效"); + @Operation(summary = "Token验证", description = "验证token并返回登录信息,支持服务重启后恢复登录状态") + public R verify(@RequestHeader(value = "Authorization", required = false) String token) { + if (token == null || token.isEmpty()) { + return R.fail("Token不能为空"); + } + + // 从Redis获取登录信息 + String cacheKey = MINI_LOGIN_CACHE_PREFIX + token; + MiniLoginVO loginInfo = bladeRedis.get(cacheKey); + + if (loginInfo != null) { + // 刷新缓存过期时间 + bladeRedis.setEx(cacheKey, loginInfo, LOGIN_CACHE_EXPIRE); + return R.data(loginInfo); + } + + // Redis中没有,尝试从数据库恢复 + LambdaQueryWrapper inviteQuery = new LambdaQueryWrapper<>(); + inviteQuery.eq(MartialJudgeInvite::getAccessToken, token); + inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0); + MartialJudgeInvite invite = judgeInviteService.getOne(inviteQuery); + + if (invite == null) { + return R.fail("Token无效"); + } + + if (invite.getTokenExpireTime() != null && invite.getTokenExpireTime().isBefore(LocalDateTime.now())) { + return R.fail("Token已过期"); + } + + // 重建登录信息 + MartialCompetition competition = competitionService.getById(invite.getCompetitionId()); + MartialJudge judge = judgeService.getById(invite.getJudgeId()); + MartialVenue martialVenue = invite.getVenueId() != null ? venueService.getById(invite.getVenueId()) : null; + // 获取项目列表:总裁判看所有项目,其他裁判根据场地获取项目 + List projects = new ArrayList<>(); + Integer refereeTypeVal = invite.getRefereeType(); + String roleVal = invite.getRole(); + boolean isGeneralJudge = (refereeTypeVal != null && refereeTypeVal == 3) + || "general_judge".equals(roleVal) || "general".equals(roleVal); + + if (isGeneralJudge) { + // 总裁判看所有项目 + projects = getAllProjectsByCompetition(competition.getId()); + } else if (Func.isNotEmpty(invite.getProjects())) { + projects = parseProjects(invite.getProjects()); + } else if (invite.getVenueId() != null) { + // 未指定项目,根据场地获取项目;如果场地没有项目则返回空列表 + projects = getProjectsByVenue(invite.getVenueId()); + } + // 如果没有场地,projects保持为空列表 + + MiniLoginVO vo = new MiniLoginVO(); + vo.setToken(token); + String role = invite.getRole(); + Integer refereeType = invite.getRefereeType(); + if ("general_judge".equals(role) || "general".equals(role) || (refereeType != null && refereeType == 3)) { + vo.setUserRole("general"); + } else if ("chief_judge".equals(role) || (refereeType != null && refereeType == 1)) { + vo.setUserRole("admin"); + } else { + vo.setUserRole("pub"); + } + vo.setMatchId(competition != null ? competition.getId() : null); + vo.setMatchName(competition != null ? competition.getCompetitionName() : null); + vo.setMatchTime(competition != null && competition.getCompetitionStartTime() != null ? + competition.getCompetitionStartTime().toString() : ""); + vo.setJudgeId(judge != null ? judge.getId() : null); + vo.setJudgeName(judge != null ? judge.getName() : null); + vo.setVenueId(martialVenue != null ? martialVenue.getId() : null); + vo.setVenueName(martialVenue != null ? martialVenue.getVenueName() : null); + vo.setProjects(projects); + + // 重新缓存到Redis + bladeRedis.setEx(cacheKey, vo, LOGIN_CACHE_EXPIRE); + + return R.data(vo); } /** * 转换选手实体为VO + * 新增:只有评分完成时才显示总分 */ private org.springblade.modules.martial.pojo.vo.MiniAthleteListVO convertToAthleteListVO( MartialAthlete athlete, List scores, - Long currentJudgeId) { + Long currentJudgeId, + int requiredJudgeCount) { org.springblade.modules.martial.pojo.vo.MiniAthleteListVO vo = new org.springblade.modules.martial.pojo.vo.MiniAthleteListVO(); vo.setAthleteId(athlete.getId()); vo.setName(athlete.getPlayerName()); @@ -296,7 +588,9 @@ public class MartialMiniController extends BladeController { vo.setTeam(athlete.getTeamName()); vo.setOrderNum(athlete.getOrderNum()); vo.setCompetitionStatus(athlete.getCompetitionStatus()); - vo.setTotalScore(athlete.getTotalScore()); + + // 设置应评分裁判数量 + vo.setRequiredJudgeCount(requiredJudgeCount); // 设置项目名称 if (athlete.getProjectId() != null) { @@ -307,8 +601,10 @@ public class MartialMiniController extends BladeController { } // 设置评分状态 + int scoredCount = 0; if (scores != null && !scores.isEmpty()) { - vo.setScoredJudgeCount(scores.size()); + scoredCount = scores.size(); + vo.setScoredJudgeCount(scoredCount); // 查找当前裁判的评分 MartialScore myScore = scores.stream() @@ -326,6 +622,23 @@ public class MartialMiniController extends BladeController { vo.setScored(false); vo.setScoredJudgeCount(0); } + + // 判断评分是否完成(所有裁判都已评分) + boolean scoringComplete = false; + if (requiredJudgeCount > 0) { + scoringComplete = scoredCount >= requiredJudgeCount; + } else { + // 如果没有配置裁判数量,只要有评分就算完成 + scoringComplete = scoredCount > 0; + } + vo.setScoringComplete(scoringComplete); + + // 只有评分完成时才显示总分 + if (scoringComplete) { + vo.setTotalScore(athlete.getTotalScore()); + } else { + vo.setTotalScore(null); + } return vo; } @@ -378,4 +691,130 @@ public class MartialMiniController extends BladeController { return projects; } + + /** + * 获取比赛的所有项目 + */ + private List getAllProjectsByCompetition(Long competitionId) { + List projects = new ArrayList<>(); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(MartialProject::getCompetitionId, competitionId); + wrapper.eq(MartialProject::getIsDeleted, 0); + + List projectList = projectService.list(wrapper); + + if (Func.isNotEmpty(projectList)) { + projects = projectList.stream().map(project -> { + MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo(); + info.setProjectId(project.getId()); + info.setProjectName(project.getProjectName()); + return info; + }).collect(Collectors.toList()); + } + + return projects; + } + + /** + * 根据场地获取项目列表 + */ + private List getProjectsByVenue(Long venueId) { + List projects = new ArrayList<>(); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(MartialProject::getVenueId, venueId); + wrapper.eq(MartialProject::getIsDeleted, 0); + + List projectList = projectService.list(wrapper); + + if (Func.isNotEmpty(projectList)) { + projects = projectList.stream().map(project -> { + MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo(); + info.setProjectId(project.getId()); + info.setProjectName(project.getProjectName()); + return info; + }).collect(Collectors.toList()); + } + + return projects; + } + + + // ========== 三级裁判评分流程 API ========== + + /** + * 主裁判确认/修改分数 + */ + @PostMapping("/chief/confirm") + @Operation(summary = "主裁判确认分数", description = "主裁判确认或修改选手分数") + public R confirmByChiefJudge(@RequestBody ChiefJudgeConfirmDTO dto) { + Long resultId = parseLong(dto.getResultId()); + Long chiefJudgeId = parseLong(dto.getChiefJudgeId()); + if (resultId == null || chiefJudgeId == null) { + return R.fail("参数错误"); + } + boolean success = resultService.confirmByChiefJudge(resultId, chiefJudgeId, dto.getScore(), dto.getNote()); + return success ? R.success("确认成功") : R.fail("确认失败"); + } + + /** + * 总裁确认/修改分数 + */ + @PostMapping("/general/confirm") + @Operation(summary = "总裁确认分数", description = "总裁确认或修改选手分数") + public R confirmByGeneralJudge(@RequestBody GeneralJudgeConfirmDTO dto) { + Long resultId = parseLong(dto.getResultId()); + Long generalJudgeId = parseLong(dto.getGeneralJudgeId()); + if (resultId == null || generalJudgeId == null) { + return R.fail("参数错误"); + } + boolean success = resultService.confirmByGeneralJudge(resultId, generalJudgeId, dto.getScore(), dto.getNote()); + return success ? R.success("确认成功") : R.fail("确认失败"); + } + + /** + * 获取待主裁判确认的成绩列表 + */ + @GetMapping("/chief/pending") + @Operation(summary = "待主裁判确认列表", description = "获取待主裁判确认的成绩列表") + public R> getPendingChiefConfirmList(@RequestParam Long venueId) { + List list = resultService.getPendingChiefConfirmList(venueId); + return R.data(list); + } + + /** + * 获取待总裁确认的成绩列表 + */ + @GetMapping("/general/pending") + @Operation(summary = "待总裁确认列表", description = "获取待总裁确认的成绩列表(所有场地)") + public R> getPendingGeneralConfirmList(@RequestParam Long competitionId) { + List list = resultService.getPendingGeneralConfirmList(competitionId); + return R.data(list); + } + + /** + * 获取所有场地列表(总裁用) + */ + @GetMapping("/general/venues") + @Operation(summary = "获取所有场地", description = "总裁获取比赛的所有场地列表") + public R> getAllVenues(@RequestParam Long competitionId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(MartialVenue::getCompetitionId, competitionId); + wrapper.eq(MartialVenue::getIsDeleted, 0); + wrapper.orderByAsc(MartialVenue::getVenueName); + List venues = venueService.list(wrapper); + return R.data(venues); + } + + /** + * 获取已总裁确认的成绩列表 + */ + @GetMapping("/general/confirmed") + @Operation(summary = "已总裁确认列表", description = "获取已总裁确认的成绩列表") + public R> getConfirmedGeneralList(@RequestParam Long competitionId) { + List list = resultService.getConfirmedGeneralList(competitionId); + return R.data(list); + } + } diff --git a/src/main/java/org/springblade/modules/martial/excel/ScheduleExportExcel2.java b/src/main/java/org/springblade/modules/martial/excel/ScheduleExportExcel2.java new file mode 100644 index 0000000..17cd8af --- /dev/null +++ b/src/main/java/org/springblade/modules/martial/excel/ScheduleExportExcel2.java @@ -0,0 +1,47 @@ +package org.springblade.modules.martial.excel; + +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.annotation.write.style.ColumnWidth; +import com.alibaba.excel.annotation.write.style.ContentRowHeight; +import com.alibaba.excel.annotation.write.style.HeadRowHeight; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * Schedule export Excel - Template 2 (Competition Schedule Format) + * Format: Sequence, Project, Participants, Groups, Time, Table Number + */ +@Data +@ColumnWidth(12) +@HeadRowHeight(25) +@ContentRowHeight(20) +public class ScheduleExportExcel2 implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + @ExcelProperty("序号") + @ColumnWidth(8) + private Integer sequenceNo; + + @ExcelProperty("项目") + @ColumnWidth(25) + private String projectName; + + @ExcelProperty("人数") + @ColumnWidth(8) + private Integer participantCount; + + @ExcelProperty("组数") + @ColumnWidth(8) + private Integer groupCount; + + @ExcelProperty("时间") + @ColumnWidth(10) + private Integer durationMinutes; + + @ExcelProperty("表号") + @ColumnWidth(10) + private String tableNo; +} diff --git a/src/main/java/org/springblade/modules/martial/pojo/vo/LineupGroupVO.java b/src/main/java/org/springblade/modules/martial/pojo/vo/LineupGroupVO.java new file mode 100644 index 0000000..6b3e2e1 --- /dev/null +++ b/src/main/java/org/springblade/modules/martial/pojo/vo/LineupGroupVO.java @@ -0,0 +1,22 @@ +package org.springblade.modules.martial.pojo.vo; + +import lombok.Data; +import java.io.Serializable; +import java.util.List; + +/** + * 出场顺序分组VO + */ +@Data +public class LineupGroupVO implements Serializable { + private static final long serialVersionUID = 1L; + + private Long groupId; + private String groupName; + private String projectName; + private String category; + private String venueName; + private String timeSlot; + private String tableNo; + private List participants; +} diff --git a/src/main/java/org/springblade/modules/martial/pojo/vo/LineupParticipantVO.java b/src/main/java/org/springblade/modules/martial/pojo/vo/LineupParticipantVO.java new file mode 100644 index 0000000..7ae5a98 --- /dev/null +++ b/src/main/java/org/springblade/modules/martial/pojo/vo/LineupParticipantVO.java @@ -0,0 +1,18 @@ +package org.springblade.modules.martial.pojo.vo; + +import lombok.Data; +import java.io.Serializable; + +/** + * 出场顺序参赛者VO + */ +@Data +public class LineupParticipantVO implements Serializable { + private static final long serialVersionUID = 1L; + + private Long id; + private Integer order; + private String playerName; + private String organization; + private String status; +} diff --git a/src/main/java/org/springblade/modules/martial/service/IMartialScheduleService.java b/src/main/java/org/springblade/modules/martial/service/IMartialScheduleService.java index b739955..363892c 100644 --- a/src/main/java/org/springblade/modules/martial/service/IMartialScheduleService.java +++ b/src/main/java/org/springblade/modules/martial/service/IMartialScheduleService.java @@ -2,6 +2,7 @@ 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.excel.ScheduleExportExcel2; import org.springblade.modules.martial.pojo.dto.MoveScheduleGroupDTO; import org.springblade.modules.martial.pojo.dto.SaveScheduleDraftDTO; import org.springblade.modules.martial.pojo.dto.ScheduleResultDTO; @@ -10,74 +11,27 @@ import org.springblade.modules.martial.pojo.entity.MartialSchedule; import java.util.List; /** - * Schedule 服务类 - * - * @author BladeX + * Schedule Service */ public interface IMartialScheduleService extends IService { - /** - * Task 3.3: 导出赛程表 - */ List exportSchedule(Long competitionId); - /** - * 获取赛程编排结果 - * @param competitionId 赛事ID - * @return 赛程编排结果 - */ + List exportScheduleTemplate2(Long competitionId, Long venueId); + 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); - /** - * 获取调度数据 - * @param competitionId 赛事ID - * @param venueId 场地ID - * @param timeSlotIndex 时间段索引 - * @return 调度数据 - */ org.springblade.modules.martial.pojo.vo.DispatchDataVO getDispatchData(Long competitionId, Long venueId, Integer timeSlotIndex); - /** - * 调整出场顺序 - * @param dto 调整请求数据 - * @return 是否成功 - */ boolean adjustOrder(org.springblade.modules.martial.pojo.dto.AdjustOrderDTO dto); - /** - * 批量保存调度 - * @param dto 保存调度数据 - * @return 是否成功 - */ boolean saveDispatch(org.springblade.modules.martial.pojo.dto.SaveDispatchDTO dto); - /** - * 更新参赛者签到状态 - * @param participantId 参赛者ID - * @param status 状态:未签到/已签到/异常 - * @return 是否成功 - */ boolean updateParticipantCheckInStatus(Long participantId, String status); - } diff --git a/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleServiceImpl.java b/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleServiceImpl.java index 4c2f160..03f0337 100644 --- a/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleServiceImpl.java +++ b/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleServiceImpl.java @@ -3,6 +3,7 @@ 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.excel.ScheduleExportExcel2; import org.springblade.modules.martial.mapper.MartialScheduleDetailMapper; import org.springblade.modules.martial.mapper.MartialScheduleGroupMapper; import org.springblade.modules.martial.mapper.MartialScheduleParticipantMapper; @@ -136,6 +137,40 @@ public class MartialScheduleServiceImpl extends ServiceImpl exportScheduleTemplate2(Long competitionId, Long venueId) { + List exportList = new ArrayList<>(); + List details = scheduleGroupMapper.selectScheduleGroupDetails(competitionId); + if (details.isEmpty()) { + return exportList; + } + if (venueId != null) { + details = details.stream().filter(d -> venueId.equals(d.getVenueId())).collect(Collectors.toList()); + } + Map> groupMap = details.stream() + .collect(Collectors.groupingBy(ScheduleGroupDetailVO::getGroupId)); + List sortedGroupIds = details.stream() + .collect(Collectors.toMap(ScheduleGroupDetailVO::getGroupId, d -> d.getDisplayOrder() != null ? d.getDisplayOrder() : 999, (a, b) -> a)) + .entrySet().stream().sorted(Map.Entry.comparingByValue()).map(Map.Entry::getKey).collect(Collectors.toList()); + int sequenceNo = 1; + int tableNoBase = 1101; + for (Long groupId : sortedGroupIds) { + List groupDetails = groupMap.get(groupId); + if (groupDetails == null || groupDetails.isEmpty()) continue; + ScheduleGroupDetailVO firstDetail = groupDetails.get(0); + long participantCount = groupDetails.stream().filter(d -> d.getParticipantId() != null).count(); + int durationMinutes = (int) (participantCount * 4); + ScheduleExportExcel2 excel = new ScheduleExportExcel2(); + excel.setSequenceNo(sequenceNo++); + excel.setProjectName(firstDetail.getGroupName()); + excel.setParticipantCount((int) participantCount); + excel.setGroupCount(1); + excel.setDurationMinutes(durationMinutes); + excel.setTableNo(String.valueOf(tableNoBase++)); + exportList.add(excel); + } + return exportList; + } /** * 获取赛程编排结果(优化版本:使用单次JOIN查询)