重构项4: 添加出场顺序显示功能

后端:
- 新增LineupGroupVO和LineupParticipantVO类
- 在MartialMiniController中添加/schedule/status和/schedule/lineup接口
- 注入MartialScheduleStatusMapper和MartialScheduleGroupMapper

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
2026-01-08 15:42:47 +08:00
parent e0d3572e34
commit 496537ceef
16 changed files with 763 additions and 134 deletions

View File

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

View File

@@ -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.MiniAthleteScoreVO;
import org.springblade.modules.martial.pojo.vo.MiniLoginVO; import org.springblade.modules.martial.pojo.vo.MiniLoginVO;
import org.springblade.modules.martial.pojo.vo.MiniScoreDetailVO; import org.springblade.modules.martial.pojo.vo.MiniScoreDetailVO;
import org.springblade.modules.martial.pojo.vo.LineupGroupVO;
import org.springblade.modules.martial.pojo.vo.LineupParticipantVO;
import org.springblade.modules.martial.pojo.vo.ScheduleGroupDetailVO;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import org.springblade.modules.martial.service.*; import org.springblade.modules.martial.service.*;
import org.springblade.modules.martial.pojo.dto.ChiefJudgeConfirmDTO; import org.springblade.modules.martial.pojo.dto.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.MartialVenue;
import org.springblade.modules.martial.pojo.entity.MartialResult; import org.springblade.modules.martial.pojo.entity.MartialResult;
import org.springblade.core.redis.cache.BladeRedis; import org.springblade.core.redis.cache.BladeRedis;
import org.springblade.modules.martial.mapper.MartialScheduleStatusMapper;
import org.springblade.modules.martial.mapper.MartialScheduleGroupMapper;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -34,6 +39,8 @@ import java.math.RoundingMode;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
@@ -60,6 +67,8 @@ public class MartialMiniController extends BladeController {
private final IMartialScoreService scoreService; private final IMartialScoreService scoreService;
private final BladeRedis bladeRedis; private final BladeRedis bladeRedis;
private final IMartialResultService resultService; private final IMartialResultService resultService;
private final MartialScheduleStatusMapper scheduleStatusMapper;
private final MartialScheduleGroupMapper scheduleGroupMapper;
// Redis缓存key前缀 // Redis缓存key前缀
private static final String MINI_LOGIN_CACHE_PREFIX = "mini:login:"; private static final String MINI_LOGIN_CACHE_PREFIX = "mini:login:";
@@ -817,4 +826,145 @@ public class MartialMiniController extends BladeController {
return R.data(list); return R.data(list);
} }
// ========== 出场顺序相关 API ==========
/**
* 获取编排状态
*/
@GetMapping("/schedule/status")
@Operation(summary = "获取编排状态", description = "检查赛事编排是否完成")
public R<Map<String, Object>> getScheduleStatus(@RequestParam Long competitionId) {
Map<String, Object> result = new HashMap<>();
LambdaQueryWrapper<MartialScheduleStatus> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialScheduleStatus::getCompetitionId, competitionId);
wrapper.eq(MartialScheduleStatus::getIsDeleted, 0);
wrapper.last("LIMIT 1");
MartialScheduleStatus status = scheduleStatusMapper.selectOne(wrapper);
if (status == null) {
result.put("isCompleted", false);
result.put("scheduleStatus", 0);
result.put("statusText", "未编排");
return R.data(result);
}
boolean isCompleted = status.getScheduleStatus() != null && status.getScheduleStatus() == 2;
result.put("isCompleted", isCompleted);
result.put("scheduleStatus", status.getScheduleStatus());
result.put("statusText", getScheduleStatusText(status.getScheduleStatus()));
result.put("totalGroups", status.getTotalGroups());
result.put("totalParticipants", status.getTotalParticipants());
result.put("lockedTime", status.getLockedTime());
return R.data(result);
}
private String getScheduleStatusText(Integer status) {
if (status == null) return "未编排";
switch (status) {
case 0: return "未编排";
case 1: return "编排中";
case 2: return "已锁定";
default: return "未知";
}
}
/**
* 获取出场顺序
*/
@GetMapping("/schedule/lineup")
@Operation(summary = "获取出场顺序", description = "获取已编排的出场顺序列表")
public R<Map<String, Object>> getLineup(
@RequestParam Long competitionId,
@RequestParam(required = false) Long projectId
) {
Map<String, Object> result = new HashMap<>();
// 使用现有mapper查询编排详情
List<ScheduleGroupDetailVO> details = scheduleGroupMapper.selectScheduleGroupDetails(competitionId);
if (details == null || details.isEmpty()) {
result.put("groups", new ArrayList<>());
return R.data(result);
}
// 按项目过滤
if (projectId != null) {
// 需要通过groupName或其他字段判断项目这里先获取项目名
MartialProject project = projectService.getById(projectId);
if (project != null) {
String projectName = project.getProjectName();
details = details.stream()
.filter(d -> d.getGroupName() != null && d.getGroupName().contains(projectName))
.collect(Collectors.toList());
}
}
// 转换为LineupGroupVO格式
Map<Long, LineupGroupVO> groupMap = new HashMap<>();
for (ScheduleGroupDetailVO detail : details) {
Long groupId = detail.getGroupId();
LineupGroupVO group = groupMap.get(groupId);
if (group == null) {
group = new LineupGroupVO();
group.setGroupId(groupId);
group.setGroupName(detail.getGroupName());
group.setCategory(detail.getCategory());
group.setVenueName(detail.getVenueName());
group.setTimeSlot(detail.getTimeSlot());
group.setTableNo(generateTableNo(detail));
group.setParticipants(new ArrayList<>());
groupMap.put(groupId, group);
}
// 添加参赛者
if (detail.getParticipantId() != null) {
LineupParticipantVO participant = new LineupParticipantVO();
participant.setId(detail.getParticipantId());
participant.setOrder(detail.getPerformanceOrder() != null ? detail.getPerformanceOrder() : group.getParticipants().size() + 1);
participant.setPlayerName(detail.getPlayerName());
participant.setOrganization(detail.getOrganization());
participant.setStatus(detail.getScheduleStatus() != null ? detail.getScheduleStatus() : "waiting");
group.getParticipants().add(participant);
}
}
result.put("groups", new ArrayList<>(groupMap.values()));
return R.data(result);
}
/**
* 生成表号: 场地(1位) + 时段(1位) + 序号(2位)
*/
private String generateTableNo(ScheduleGroupDetailVO detail) {
// 场地编号简单取第一个数字或默认1
int venueNo = 1;
if (detail.getVenueName() != null) {
String venueName = detail.getVenueName();
for (char c : venueName.toCharArray()) {
if (Character.isDigit(c)) {
venueNo = Character.getNumericValue(c);
break;
}
}
}
// 时段:上午=1, 下午=2
int period = 1;
if (detail.getTimeSlot() != null) {
try {
int hour = Integer.parseInt(detail.getTimeSlot().split(":")[0]);
period = hour < 12 ? 1 : 2;
} catch (Exception e) {
// ignore
}
}
// 序号使用displayOrder或默认1
int orderNo = detail.getDisplayOrder() != null ? detail.getDisplayOrder() : 1;
return String.format("%d%d%02d", venueNo, period, orderNo);
}
} }

View File

@@ -21,12 +21,21 @@ import org.springblade.modules.martial.pojo.vo.MiniLoginVO;
import org.springblade.modules.martial.pojo.vo.MiniScoreDetailVO; import org.springblade.modules.martial.pojo.vo.MiniScoreDetailVO;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import org.springblade.modules.martial.service.*; import org.springblade.modules.martial.service.*;
import org.springblade.modules.martial.pojo.dto.ChiefJudgeConfirmDTO;
import org.springblade.modules.martial.pojo.dto.GeneralJudgeConfirmDTO;
import org.springblade.modules.martial.pojo.entity.MtVenue;
import org.springblade.modules.martial.pojo.entity.MartialVenue;
import org.springblade.modules.martial.pojo.entity.MartialResult;
import org.springblade.core.redis.cache.BladeRedis;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -45,9 +54,17 @@ public class MartialMiniController extends BladeController {
private final IMartialJudgeService judgeService; private final IMartialJudgeService judgeService;
private final IMartialCompetitionService competitionService; private final IMartialCompetitionService competitionService;
private final IMartialVenueService venueService; private final IMartialVenueService venueService;
private final IMtVenueService mtVenueService;
private final IMartialProjectService projectService; private final IMartialProjectService projectService;
private final IMartialAthleteService athleteService; private final IMartialAthleteService athleteService;
private final IMartialScoreService scoreService; private final IMartialScoreService scoreService;
private final BladeRedis bladeRedis;
private final IMartialResultService resultService;
// Redis缓存key前缀
private static final String MINI_LOGIN_CACHE_PREFIX = "mini:login:";
// 登录缓存过期时间7天
private static final Duration LOGIN_CACHE_EXPIRE = Duration.ofDays(7);
/** /**
* 登录验证 * 登录验证
@@ -91,26 +108,55 @@ public class MartialMiniController extends BladeController {
invite.setDeviceInfo(dto.getDeviceInfo()); invite.setDeviceInfo(dto.getDeviceInfo());
judgeInviteService.updateById(invite); judgeInviteService.updateById(invite);
MartialVenue venue = null; // 从 martial_venue 表获取场地信息
MartialVenue martialVenue = null;
if (invite.getVenueId() != null) { if (invite.getVenueId() != null) {
venue = venueService.getById(invite.getVenueId()); martialVenue = venueService.getById(invite.getVenueId());
} }
List<MiniLoginVO.ProjectInfo> projects = parseProjects(invite.getProjects()); // 获取项目列表:总裁判看所有项目,其他裁判根据场地获取项目
List<MiniLoginVO.ProjectInfo> projects = new ArrayList<>();
Integer refereeTypeVal = invite.getRefereeType();
String roleVal = invite.getRole();
boolean isGeneralJudge = (refereeTypeVal != null && refereeTypeVal == 3)
|| "general_judge".equals(roleVal) || "general".equals(roleVal);
if (isGeneralJudge) {
// 总裁判看所有项目
projects = getAllProjectsByCompetition(competition.getId());
} else if (Func.isNotEmpty(invite.getProjects())) {
projects = parseProjects(invite.getProjects());
} else if (invite.getVenueId() != null) {
// 未指定项目,根据场地获取项目;如果场地没有项目则返回空列表
projects = getProjectsByVenue(invite.getVenueId());
}
// 如果没有场地projects保持为空列表
MiniLoginVO vo = new MiniLoginVO(); MiniLoginVO vo = new MiniLoginVO();
vo.setToken(token); vo.setToken(token);
vo.setUserRole("chief_judge".equals(invite.getRole()) ? "admin" : "pub"); String role = invite.getRole();
Integer refereeType = invite.getRefereeType();
if ("general_judge".equals(role) || "general".equals(role) || (refereeType != null && refereeType == 3)) {
vo.setUserRole("general");
} else if ("chief_judge".equals(role) || (refereeType != null && refereeType == 1)) {
vo.setUserRole("admin");
} else {
vo.setUserRole("pub");
}
vo.setMatchId(competition.getId()); vo.setMatchId(competition.getId());
vo.setMatchName(competition.getCompetitionName()); vo.setMatchName(competition.getCompetitionName());
vo.setMatchTime(competition.getCompetitionStartTime() != null ? vo.setMatchTime(competition.getCompetitionStartTime() != null ?
competition.getCompetitionStartTime().toString() : ""); competition.getCompetitionStartTime().toString() : "");
vo.setJudgeId(judge.getId()); vo.setJudgeId(judge.getId());
vo.setJudgeName(judge.getName()); vo.setJudgeName(judge.getName());
vo.setVenueId(venue != null ? venue.getId() : null); vo.setVenueId(martialVenue != null ? martialVenue.getId() : null);
vo.setVenueName(venue != null ? venue.getVenueName() : null); vo.setVenueName(martialVenue != null ? martialVenue.getVenueName() : null);
vo.setProjects(projects); vo.setProjects(projects);
// 将登录信息缓存到Redis服务重启后仍然有效
String cacheKey = MINI_LOGIN_CACHE_PREFIX + token;
bladeRedis.setEx(cacheKey, vo, LOGIN_CACHE_EXPIRE);
return R.data(vo); return R.data(vo);
} }
@@ -152,9 +198,137 @@ public class MartialMiniController extends BladeController {
} }
boolean success = scoreService.save(score); boolean success = scoreService.save(score);
// 评分保存成功后,计算并更新选手总分
if (success) {
Long athleteId = parseLong(dto.getAthleteId());
Long projectId = parseLong(dto.getProjectId());
Long venueId = parseLong(dto.getVenueId());
if (athleteId != null && projectId != null) {
updateAthleteTotalScore(athleteId, projectId, venueId);
}
}
return success ? R.success("评分提交成功") : R.fail("评分提交失败"); return success ? R.success("评分提交成功") : R.fail("评分提交失败");
} }
/**
* 计算并更新选手总分
* 总分算法:去掉一个最高分和一个最低分,取剩余分数的平均值
* 特殊情况:裁判数量<3时直接取平均分
* 只有所有裁判都评分完成后才更新总分
*/
private void updateAthleteTotalScore(Long athleteId, Long projectId, Long venueId) {
try {
// 1. 查询该场地的裁判员数量
int requiredJudgeCount = getRequiredJudgeCount(venueId);
// 2. 获取主裁判ID列表
List<Long> chiefJudgeIds = getChiefJudgeIds(venueId);
// 3. 查询该选手在该项目的所有评分(排除主裁判的评分)
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getAthleteId, athleteId);
scoreQuery.eq(MartialScore::getProjectId, projectId);
scoreQuery.eq(MartialScore::getIsDeleted, 0);
// 排除主裁判的所有评分(包括普通评分和修改记录)
if (!chiefJudgeIds.isEmpty()) {
scoreQuery.notIn(MartialScore::getJudgeId, chiefJudgeIds);
}
List<MartialScore> scores = scoreService.list(scoreQuery);
// 4. 判断是否所有裁判都已评分
if (scores == null || scores.isEmpty()) {
return;
}
// 如果配置了裁判数量,检查是否评分完成
if (requiredJudgeCount > 0 && scores.size() < requiredJudgeCount) {
// 未完成评分,清空总分
MartialAthlete athlete = athleteService.getById(athleteId);
if (athlete != null && athlete.getTotalScore() != null) {
athlete.setTotalScore(null);
athleteService.updateById(athlete);
}
return;
}
// 4. 计算总分(去掉最高最低分取平均)
BigDecimal totalScore = calculateTotalScore(scores);
// 5. 更新选手总分
if (totalScore != null) {
MartialAthlete athlete = athleteService.getById(athleteId);
if (athlete != null) {
athlete.setTotalScore(totalScore);
athleteService.updateById(athlete);
}
}
} catch (Exception e) {
// 记录错误但不影响评分提交
e.printStackTrace();
}
}
/**
* 获取项目应评分的裁判数量(裁判员,不包括主裁判)
* 按项目过滤:检查 projects JSON 字段是否包含该项目ID
*/
private int getRequiredJudgeCount(Long venueId) {
if (venueId == null || venueId <= 0) {
return 0;
}
LambdaQueryWrapper<MartialJudgeInvite> judgeQuery = new LambdaQueryWrapper<>();
judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
judgeQuery.eq(MartialJudgeInvite::getVenueId, venueId);
judgeQuery.eq(MartialJudgeInvite::getRefereeType, 2); // Only count referees (type=2), exclude chief judge (type=1) and general judge (type=3)
List<MartialJudgeInvite> judges = judgeInviteService.list(judgeQuery);
// Use distinct judge_id to count unique judges
return (int) judges.stream()
.map(MartialJudgeInvite::getJudgeId)
.filter(Objects::nonNull)
.distinct()
.count();
}
/**
* 计算总分
* 算法:去掉一个最高分和一个最低分,取剩余分数的平均值
* 特殊情况:裁判数量<3时直接取平均分
*/
private BigDecimal calculateTotalScore(List<MartialScore> scores) {
if (scores == null || scores.isEmpty()) {
return null;
}
// 提取所有分数并排序
List<BigDecimal> scoreValues = scores.stream()
.map(MartialScore::getScore)
.filter(Objects::nonNull)
.sorted()
.collect(Collectors.toList());
int count = scoreValues.size();
if (count == 0) {
return null;
}
if (count < 3) {
// 裁判数量<3直接取平均分
BigDecimal sum = scoreValues.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
return sum.divide(new BigDecimal(count), 3, RoundingMode.HALF_UP);
}
// 去掉最高分和最低分(已排序,去掉第一个和最后一个)
List<BigDecimal> middleScores = scoreValues.subList(1, count - 1);
// 计算平均分
BigDecimal sum = middleScores.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
return sum.divide(new BigDecimal(middleScores.size()), 3, RoundingMode.HALF_UP);
}
/** /**
* 安全地将String转换为Long * 安全地将String转换为Long
*/ */
@@ -171,8 +345,8 @@ public class MartialMiniController extends BladeController {
/** /**
* 获取选手列表(支持分页) * 获取选手列表(支持分页)
* - 普通裁判:获取所有选手,标记是否已评分 * - 裁判:获取所有选手,标记是否已评分
* - 裁判:获取已有评分的选手列表 * - 裁判:获取所有裁判员都评分完成的选手列表
*/ */
@GetMapping("/score/athletes") @GetMapping("/score/athletes")
@Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)") @Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)")
@@ -181,6 +355,7 @@ public class MartialMiniController extends BladeController {
@RequestParam Integer refereeType, @RequestParam Integer refereeType,
@RequestParam(required = false) Long projectId, @RequestParam(required = false) Long projectId,
@RequestParam(required = false) Long venueId, @RequestParam(required = false) Long venueId,
@RequestParam(required = false) Long competitionId,
@RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size @RequestParam(defaultValue = "10") Integer size
) { ) {
@@ -188,6 +363,11 @@ public class MartialMiniController extends BladeController {
LambdaQueryWrapper<MartialAthlete> athleteQuery = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialAthlete> athleteQuery = new LambdaQueryWrapper<>();
athleteQuery.eq(MartialAthlete::getIsDeleted, 0); athleteQuery.eq(MartialAthlete::getIsDeleted, 0);
// 按比赛ID过滤重要确保只显示当前比赛的选手
if (competitionId != null) {
athleteQuery.eq(MartialAthlete::getCompetitionId, competitionId);
}
if (projectId != null) { if (projectId != null) {
athleteQuery.eq(MartialAthlete::getProjectId, projectId); athleteQuery.eq(MartialAthlete::getProjectId, projectId);
} }
@@ -196,35 +376,48 @@ public class MartialMiniController extends BladeController {
List<MartialAthlete> athletes = athleteService.list(athleteQuery); List<MartialAthlete> athletes = athleteService.list(athleteQuery);
// 2. 获取所有评分记录 // 2. 获取该场地所有主裁判的judge_id列表
List<Long> chiefJudgeIds = getChiefJudgeIds(venueId);
// 3. 获取所有评分记录(排除主裁判的评分)
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getIsDeleted, 0); scoreQuery.eq(MartialScore::getIsDeleted, 0);
if (projectId != null) {
scoreQuery.eq(MartialScore::getProjectId, projectId);
}
// 添加场地过滤
if (venueId != null && venueId > 0) {
scoreQuery.eq(MartialScore::getVenueId, venueId);
}
// 排除主裁判的评分
if (!chiefJudgeIds.isEmpty()) {
scoreQuery.notIn(MartialScore::getJudgeId, chiefJudgeIds);
}
List<MartialScore> allScores = scoreService.list(scoreQuery); List<MartialScore> allScores = scoreService.list(scoreQuery);
// 按选手ID分组统计评分 // 按选手ID分组统计评分
java.util.Map<Long, List<MartialScore>> scoresByAthlete = allScores.stream() java.util.Map<Long, List<MartialScore>> scoresByAthlete = allScores.stream()
.collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId)); .collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId));
// 3. 根据裁判类型处理选手列表 // 4. 获取该场地的应评裁判数量
int requiredJudgeCount = getRequiredJudgeCount(venueId);
// 5. 根据裁判类型处理选手列表
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList; List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList;
if (refereeType == 1) { if (refereeType == 1) {
// 裁判:返回已有评分的选手 // 裁判:返回所有选手前端根据totalScore判断是否显示修改按钮
filteredList = athletes.stream() filteredList = athletes.stream()
.filter(athlete -> { .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount))
List<MartialScore> scores = scoresByAthlete.get(athlete.getId());
return scores != null && !scores.isEmpty();
})
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId))
.collect(java.util.stream.Collectors.toList()); .collect(java.util.stream.Collectors.toList());
} else { } else {
// 普通裁判:返回所有选手,标记是否已评分 // 裁判:返回所有选手,标记是否已评分
filteredList = athletes.stream() filteredList = athletes.stream()
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId)) .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount))
.collect(java.util.stream.Collectors.toList()); .collect(java.util.stream.Collectors.toList());
} }
// 4. 手动分页 // 6. 手动分页
int total = filteredList.size(); int total = filteredList.size();
int fromIndex = (current - 1) * size; int fromIndex = (current - 1) * size;
int toIndex = Math.min(fromIndex + size, total); int toIndex = Math.min(fromIndex + size, total);
@@ -236,13 +429,31 @@ public class MartialMiniController extends BladeController {
pageRecords = filteredList.subList(fromIndex, toIndex); pageRecords = filteredList.subList(fromIndex, toIndex);
} }
// 5. 构建分页结果 // 7. 构建分页结果
IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> page = new Page<>(current, size, total); IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> page = new Page<>(current, size, total);
page.setRecords(pageRecords); page.setRecords(pageRecords);
return R.data(page); return R.data(page);
} }
/**
* 获取场地所有主裁判的judge_id列表
*/
private List<Long> getChiefJudgeIds(Long venueId) {
if (venueId == null) {
return new ArrayList<>();
}
LambdaQueryWrapper<MartialJudgeInvite> judgeQuery = new LambdaQueryWrapper<>();
judgeQuery.eq(MartialJudgeInvite::getVenueId, venueId);
judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
judgeQuery.eq(MartialJudgeInvite::getRole, "chief_judge");
List<MartialJudgeInvite> chiefJudges = judgeInviteService.list(judgeQuery);
return chiefJudges.stream()
.map(MartialJudgeInvite::getJudgeId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
/** /**
* 获取评分详情 * 获取评分详情
*/ */
@@ -254,10 +465,10 @@ public class MartialMiniController extends BladeController {
} }
/** /**
* 修改评分(裁判 * 修改评分(裁判)
*/ */
@PutMapping("/score/modify") @PutMapping("/score/modify")
@Operation(summary = "修改评分", description = "裁判修改选手总分") @Operation(summary = "修改评分", description = "裁判修改选手总分")
public R modifyScore(@RequestBody MiniScoreModifyDTO dto) { public R modifyScore(@RequestBody MiniScoreModifyDTO dto) {
boolean success = scoreService.modifyScoreByAdmin(dto); boolean success = scoreService.modifyScoreByAdmin(dto);
return success ? R.success("修改成功") : R.fail("修改失败"); return success ? R.success("修改成功") : R.fail("修改失败");
@@ -268,26 +479,107 @@ public class MartialMiniController extends BladeController {
*/ */
@PostMapping("/logout") @PostMapping("/logout")
@Operation(summary = "退出登录", description = "清除登录状态") @Operation(summary = "退出登录", description = "清除登录状态")
public R logout() { public R logout(@RequestHeader(value = "Authorization", required = false) String token) {
// 从Redis删除登录缓存
if (token != null && !token.isEmpty()) {
String cacheKey = MINI_LOGIN_CACHE_PREFIX + token;
bladeRedis.del(cacheKey);
}
return R.success("退出成功"); return R.success("退出成功");
} }
/** /**
* Token验证 * Token验证从Redis恢复登录状态
*/ */
@GetMapping("/verify") @GetMapping("/verify")
@Operation(summary = "Token验证", description = "验证当前token是否有效") @Operation(summary = "Token验证", description = "验证token并返回登录信息,支持服务重启后恢复登录状态")
public R verify() { public R<MiniLoginVO> verify(@RequestHeader(value = "Authorization", required = false) String token) {
return R.success("Token有效"); if (token == null || token.isEmpty()) {
return R.fail("Token不能为空");
}
// 从Redis获取登录信息
String cacheKey = MINI_LOGIN_CACHE_PREFIX + token;
MiniLoginVO loginInfo = bladeRedis.get(cacheKey);
if (loginInfo != null) {
// 刷新缓存过期时间
bladeRedis.setEx(cacheKey, loginInfo, LOGIN_CACHE_EXPIRE);
return R.data(loginInfo);
}
// Redis中没有尝试从数据库恢复
LambdaQueryWrapper<MartialJudgeInvite> inviteQuery = new LambdaQueryWrapper<>();
inviteQuery.eq(MartialJudgeInvite::getAccessToken, token);
inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
MartialJudgeInvite invite = judgeInviteService.getOne(inviteQuery);
if (invite == null) {
return R.fail("Token无效");
}
if (invite.getTokenExpireTime() != null && invite.getTokenExpireTime().isBefore(LocalDateTime.now())) {
return R.fail("Token已过期");
}
// 重建登录信息
MartialCompetition competition = competitionService.getById(invite.getCompetitionId());
MartialJudge judge = judgeService.getById(invite.getJudgeId());
MartialVenue martialVenue = invite.getVenueId() != null ? venueService.getById(invite.getVenueId()) : null;
// 获取项目列表:总裁判看所有项目,其他裁判根据场地获取项目
List<MiniLoginVO.ProjectInfo> projects = new ArrayList<>();
Integer refereeTypeVal = invite.getRefereeType();
String roleVal = invite.getRole();
boolean isGeneralJudge = (refereeTypeVal != null && refereeTypeVal == 3)
|| "general_judge".equals(roleVal) || "general".equals(roleVal);
if (isGeneralJudge) {
// 总裁判看所有项目
projects = getAllProjectsByCompetition(competition.getId());
} else if (Func.isNotEmpty(invite.getProjects())) {
projects = parseProjects(invite.getProjects());
} else if (invite.getVenueId() != null) {
// 未指定项目,根据场地获取项目;如果场地没有项目则返回空列表
projects = getProjectsByVenue(invite.getVenueId());
}
// 如果没有场地projects保持为空列表
MiniLoginVO vo = new MiniLoginVO();
vo.setToken(token);
String role = invite.getRole();
Integer refereeType = invite.getRefereeType();
if ("general_judge".equals(role) || "general".equals(role) || (refereeType != null && refereeType == 3)) {
vo.setUserRole("general");
} else if ("chief_judge".equals(role) || (refereeType != null && refereeType == 1)) {
vo.setUserRole("admin");
} else {
vo.setUserRole("pub");
}
vo.setMatchId(competition != null ? competition.getId() : null);
vo.setMatchName(competition != null ? competition.getCompetitionName() : null);
vo.setMatchTime(competition != null && competition.getCompetitionStartTime() != null ?
competition.getCompetitionStartTime().toString() : "");
vo.setJudgeId(judge != null ? judge.getId() : null);
vo.setJudgeName(judge != null ? judge.getName() : null);
vo.setVenueId(martialVenue != null ? martialVenue.getId() : null);
vo.setVenueName(martialVenue != null ? martialVenue.getVenueName() : null);
vo.setProjects(projects);
// 重新缓存到Redis
bladeRedis.setEx(cacheKey, vo, LOGIN_CACHE_EXPIRE);
return R.data(vo);
} }
/** /**
* 转换选手实体为VO * 转换选手实体为VO
* 新增:只有评分完成时才显示总分
*/ */
private org.springblade.modules.martial.pojo.vo.MiniAthleteListVO convertToAthleteListVO( private org.springblade.modules.martial.pojo.vo.MiniAthleteListVO convertToAthleteListVO(
MartialAthlete athlete, MartialAthlete athlete,
List<MartialScore> scores, List<MartialScore> scores,
Long currentJudgeId) { Long currentJudgeId,
int requiredJudgeCount) {
org.springblade.modules.martial.pojo.vo.MiniAthleteListVO vo = new org.springblade.modules.martial.pojo.vo.MiniAthleteListVO(); org.springblade.modules.martial.pojo.vo.MiniAthleteListVO vo = new org.springblade.modules.martial.pojo.vo.MiniAthleteListVO();
vo.setAthleteId(athlete.getId()); vo.setAthleteId(athlete.getId());
vo.setName(athlete.getPlayerName()); vo.setName(athlete.getPlayerName());
@@ -296,7 +588,9 @@ public class MartialMiniController extends BladeController {
vo.setTeam(athlete.getTeamName()); vo.setTeam(athlete.getTeamName());
vo.setOrderNum(athlete.getOrderNum()); vo.setOrderNum(athlete.getOrderNum());
vo.setCompetitionStatus(athlete.getCompetitionStatus()); vo.setCompetitionStatus(athlete.getCompetitionStatus());
vo.setTotalScore(athlete.getTotalScore());
// 设置应评分裁判数量
vo.setRequiredJudgeCount(requiredJudgeCount);
// 设置项目名称 // 设置项目名称
if (athlete.getProjectId() != null) { if (athlete.getProjectId() != null) {
@@ -307,8 +601,10 @@ public class MartialMiniController extends BladeController {
} }
// 设置评分状态 // 设置评分状态
int scoredCount = 0;
if (scores != null && !scores.isEmpty()) { if (scores != null && !scores.isEmpty()) {
vo.setScoredJudgeCount(scores.size()); scoredCount = scores.size();
vo.setScoredJudgeCount(scoredCount);
// 查找当前裁判的评分 // 查找当前裁判的评分
MartialScore myScore = scores.stream() MartialScore myScore = scores.stream()
@@ -327,6 +623,23 @@ public class MartialMiniController extends BladeController {
vo.setScoredJudgeCount(0); vo.setScoredJudgeCount(0);
} }
// 判断评分是否完成(所有裁判都已评分)
boolean scoringComplete = false;
if (requiredJudgeCount > 0) {
scoringComplete = scoredCount >= requiredJudgeCount;
} else {
// 如果没有配置裁判数量,只要有评分就算完成
scoringComplete = scoredCount > 0;
}
vo.setScoringComplete(scoringComplete);
// 只有评分完成时才显示总分
if (scoringComplete) {
vo.setTotalScore(athlete.getTotalScore());
} else {
vo.setTotalScore(null);
}
return vo; return vo;
} }
@@ -378,4 +691,130 @@ public class MartialMiniController extends BladeController {
return projects; return projects;
} }
/**
* 获取比赛的所有项目
*/
private List<MiniLoginVO.ProjectInfo> getAllProjectsByCompetition(Long competitionId) {
List<MiniLoginVO.ProjectInfo> projects = new ArrayList<>();
LambdaQueryWrapper<MartialProject> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialProject::getCompetitionId, competitionId);
wrapper.eq(MartialProject::getIsDeleted, 0);
List<MartialProject> projectList = projectService.list(wrapper);
if (Func.isNotEmpty(projectList)) {
projects = projectList.stream().map(project -> {
MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo();
info.setProjectId(project.getId());
info.setProjectName(project.getProjectName());
return info;
}).collect(Collectors.toList());
}
return projects;
}
/**
* 根据场地获取项目列表
*/
private List<MiniLoginVO.ProjectInfo> getProjectsByVenue(Long venueId) {
List<MiniLoginVO.ProjectInfo> projects = new ArrayList<>();
LambdaQueryWrapper<MartialProject> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialProject::getVenueId, venueId);
wrapper.eq(MartialProject::getIsDeleted, 0);
List<MartialProject> projectList = projectService.list(wrapper);
if (Func.isNotEmpty(projectList)) {
projects = projectList.stream().map(project -> {
MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo();
info.setProjectId(project.getId());
info.setProjectName(project.getProjectName());
return info;
}).collect(Collectors.toList());
}
return projects;
}
// ========== 三级裁判评分流程 API ==========
/**
* 主裁判确认/修改分数
*/
@PostMapping("/chief/confirm")
@Operation(summary = "主裁判确认分数", description = "主裁判确认或修改选手分数")
public R confirmByChiefJudge(@RequestBody ChiefJudgeConfirmDTO dto) {
Long resultId = parseLong(dto.getResultId());
Long chiefJudgeId = parseLong(dto.getChiefJudgeId());
if (resultId == null || chiefJudgeId == null) {
return R.fail("参数错误");
}
boolean success = resultService.confirmByChiefJudge(resultId, chiefJudgeId, dto.getScore(), dto.getNote());
return success ? R.success("确认成功") : R.fail("确认失败");
}
/**
* 总裁确认/修改分数
*/
@PostMapping("/general/confirm")
@Operation(summary = "总裁确认分数", description = "总裁确认或修改选手分数")
public R confirmByGeneralJudge(@RequestBody GeneralJudgeConfirmDTO dto) {
Long resultId = parseLong(dto.getResultId());
Long generalJudgeId = parseLong(dto.getGeneralJudgeId());
if (resultId == null || generalJudgeId == null) {
return R.fail("参数错误");
}
boolean success = resultService.confirmByGeneralJudge(resultId, generalJudgeId, dto.getScore(), dto.getNote());
return success ? R.success("确认成功") : R.fail("确认失败");
}
/**
* 获取待主裁判确认的成绩列表
*/
@GetMapping("/chief/pending")
@Operation(summary = "待主裁判确认列表", description = "获取待主裁判确认的成绩列表")
public R<List<MartialResult>> getPendingChiefConfirmList(@RequestParam Long venueId) {
List<MartialResult> list = resultService.getPendingChiefConfirmList(venueId);
return R.data(list);
}
/**
* 获取待总裁确认的成绩列表
*/
@GetMapping("/general/pending")
@Operation(summary = "待总裁确认列表", description = "获取待总裁确认的成绩列表(所有场地)")
public R<List<MartialResult>> getPendingGeneralConfirmList(@RequestParam Long competitionId) {
List<MartialResult> list = resultService.getPendingGeneralConfirmList(competitionId);
return R.data(list);
}
/**
* 获取所有场地列表(总裁用)
*/
@GetMapping("/general/venues")
@Operation(summary = "获取所有场地", description = "总裁获取比赛的所有场地列表")
public R<List<MartialVenue>> getAllVenues(@RequestParam Long competitionId) {
LambdaQueryWrapper<MartialVenue> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialVenue::getCompetitionId, competitionId);
wrapper.eq(MartialVenue::getIsDeleted, 0);
wrapper.orderByAsc(MartialVenue::getVenueName);
List<MartialVenue> venues = venueService.list(wrapper);
return R.data(venues);
}
/**
* 获取已总裁确认的成绩列表
*/
@GetMapping("/general/confirmed")
@Operation(summary = "已总裁确认列表", description = "获取已总裁确认的成绩列表")
public R<List<MartialResult>> getConfirmedGeneralList(@RequestParam Long competitionId) {
List<MartialResult> list = resultService.getConfirmedGeneralList(competitionId);
return R.data(list);
}
} }

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package org.springblade.modules.martial.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import org.springblade.modules.martial.excel.ScheduleExportExcel; import org.springblade.modules.martial.excel.ScheduleExportExcel;
import org.springblade.modules.martial.excel.ScheduleExportExcel2;
import org.springblade.modules.martial.pojo.dto.MoveScheduleGroupDTO; import org.springblade.modules.martial.pojo.dto.MoveScheduleGroupDTO;
import org.springblade.modules.martial.pojo.dto.SaveScheduleDraftDTO; import org.springblade.modules.martial.pojo.dto.SaveScheduleDraftDTO;
import org.springblade.modules.martial.pojo.dto.ScheduleResultDTO; import org.springblade.modules.martial.pojo.dto.ScheduleResultDTO;
@@ -10,74 +11,27 @@ import org.springblade.modules.martial.pojo.entity.MartialSchedule;
import java.util.List; import java.util.List;
/** /**
* Schedule 服务类 * Schedule Service
*
* @author BladeX
*/ */
public interface IMartialScheduleService extends IService<MartialSchedule> { public interface IMartialScheduleService extends IService<MartialSchedule> {
/**
* Task 3.3: 导出赛程表
*/
List<ScheduleExportExcel> exportSchedule(Long competitionId); List<ScheduleExportExcel> exportSchedule(Long competitionId);
/** List<ScheduleExportExcel2> exportScheduleTemplate2(Long competitionId, Long venueId);
* 获取赛程编排结果
* @param competitionId 赛事ID
* @return 赛程编排结果
*/
ScheduleResultDTO getScheduleResult(Long competitionId); ScheduleResultDTO getScheduleResult(Long competitionId);
/**
* 保存编排草稿
* @param dto 编排草稿数据
* @return 是否成功
*/
boolean saveDraftSchedule(SaveScheduleDraftDTO dto); boolean saveDraftSchedule(SaveScheduleDraftDTO dto);
/**
* 完成编排并锁定
* @param competitionId 赛事ID
* @return 是否成功
*/
boolean saveAndLockSchedule(Long competitionId); boolean saveAndLockSchedule(Long competitionId);
/**
* 移动赛程分组到指定场地和时间段
* @param dto 移动请求数据
* @return 是否成功
*/
boolean moveScheduleGroup(MoveScheduleGroupDTO dto); 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); 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); boolean adjustOrder(org.springblade.modules.martial.pojo.dto.AdjustOrderDTO dto);
/**
* 批量保存调度
* @param dto 保存调度数据
* @return 是否成功
*/
boolean saveDispatch(org.springblade.modules.martial.pojo.dto.SaveDispatchDTO dto); boolean saveDispatch(org.springblade.modules.martial.pojo.dto.SaveDispatchDTO dto);
/**
* 更新参赛者签到状态
* @param participantId 参赛者ID
* @param status 状态:未签到/已签到/异常
* @return 是否成功
*/
boolean updateParticipantCheckInStatus(Long participantId, String status); boolean updateParticipantCheckInStatus(Long participantId, String status);
} }

View File

@@ -3,6 +3,7 @@ package org.springblade.modules.martial.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springblade.modules.martial.excel.ScheduleExportExcel; import org.springblade.modules.martial.excel.ScheduleExportExcel;
import org.springblade.modules.martial.excel.ScheduleExportExcel2;
import org.springblade.modules.martial.mapper.MartialScheduleDetailMapper; import org.springblade.modules.martial.mapper.MartialScheduleDetailMapper;
import org.springblade.modules.martial.mapper.MartialScheduleGroupMapper; import org.springblade.modules.martial.mapper.MartialScheduleGroupMapper;
import org.springblade.modules.martial.mapper.MartialScheduleParticipantMapper; import org.springblade.modules.martial.mapper.MartialScheduleParticipantMapper;
@@ -136,6 +137,40 @@ public class MartialScheduleServiceImpl extends ServiceImpl<MartialScheduleMappe
return exportList; return exportList;
} }
@Override
public List<ScheduleExportExcel2> exportScheduleTemplate2(Long competitionId, Long venueId) {
List<ScheduleExportExcel2> exportList = new ArrayList<>();
List<ScheduleGroupDetailVO> 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<Long, List<ScheduleGroupDetailVO>> groupMap = details.stream()
.collect(Collectors.groupingBy(ScheduleGroupDetailVO::getGroupId));
List<Long> sortedGroupIds = details.stream()
.collect(Collectors.toMap(ScheduleGroupDetailVO::getGroupId, d -> d.getDisplayOrder() != null ? d.getDisplayOrder() : 999, (a, b) -> a))
.entrySet().stream().sorted(Map.Entry.comparingByValue()).map(Map.Entry::getKey).collect(Collectors.toList());
int sequenceNo = 1;
int tableNoBase = 1101;
for (Long groupId : sortedGroupIds) {
List<ScheduleGroupDetailVO> 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查询 * 获取赛程编排结果优化版本使用单次JOIN查询