Compare commits

...

4 Commits

Author SHA1 Message Date
559dea702a feat: 项目编码自动生成
- 新增项目时自动生成编码,格式: C{赛事ID}-P{序号}
- 移除手动输入项目编码

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 17:19:23 +08:00
c40ca5b35b fix: 修复联系人默认唯一性问题 & 添加单位统计API
- 问题1: 设置默认联系人时自动取消其他默认联系人
- 问题3: 新增 /organization-stats API 按单位统计运动员、项目、金额

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 16:08:31 +08:00
742272026b 重构项2: 优化编排规则
变更点1 - 使用项目maxParticipants:
- 优先使用项目配置的单位容纳人数进行分组
- 未配置时回退到全局配置(35人)
- 添加日志记录使用的容纳人数

变更点2 - 实现集体优先策略:
- 集体项目(projectType=2)优先于单人项目(projectType=1)
- 同类型项目按预计时长降序排列
- 添加排序结果日志便于调试

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 15:50:21 +08:00
496537ceef 重构项4: 添加出场顺序显示功能
后端:
- 新增LineupGroupVO和LineupParticipantVO类
- 在MartialMiniController中添加/schedule/status和/schedule/lineup接口
- 注入MartialScheduleStatusMapper和MartialScheduleGroupMapper

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 15:42:47 +08:00
23 changed files with 1112 additions and 151 deletions

View File

@@ -14,8 +14,6 @@ import org.springblade.modules.martial.pojo.entity.MartialContact;
import org.springblade.modules.martial.service.IMartialContactService; import org.springblade.modules.martial.service.IMartialContactService;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.Date;
@Slf4j @Slf4j
@RestController @RestController
@AllArgsConstructor @AllArgsConstructor
@@ -43,16 +41,10 @@ public class MartialContactController extends BladeController {
@Operation(summary = "保存", description = "新增或修改联系人") @Operation(summary = "保存", description = "新增或修改联系人")
public R<Boolean> submit(@RequestBody MartialContact contact) { public R<Boolean> submit(@RequestBody MartialContact contact) {
Long userId = AuthUtil.getUserId(); Long userId = AuthUtil.getUserId();
log.info("Contact submit - id: {}, name: {}, userId: {}", contact.getId(), contact.getName(), userId); log.info("Contact submit - id: {}, name: {}, userId: {}, isDefault: {}",
contact.getId(), contact.getName(), userId, contact.getIsDefault());
if (contact.getId() == null) { return R.data(contactService.saveContact(contact, userId));
contact.setCreateUser(userId);
contact.setCreateTime(new Date());
}
contact.setUpdateUser(userId);
contact.setUpdateTime(new Date());
return R.data(contactService.saveOrUpdate(contact));
} }
@PostMapping("/remove") @PostMapping("/remove")

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

@@ -1,5 +1,6 @@
package org.springblade.modules.martial.controller; package org.springblade.modules.martial.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -78,9 +79,32 @@ public class MartialProjectController extends BladeController {
@PostMapping("/submit") @PostMapping("/submit")
@Operation(summary = "新增或修改", description = "传入实体") @Operation(summary = "新增或修改", description = "传入实体")
public R submit(@RequestBody MartialProject project) { public R submit(@RequestBody MartialProject project) {
// Auto-generate project code for new projects
if (project.getId() == null && StringUtil.isBlank(project.getProjectCode())) {
String projectCode = generateProjectCode(project.getCompetitionId());
project.setProjectCode(projectCode);
}
return R.status(projectService.saveOrUpdate(project)); return R.status(projectService.saveOrUpdate(project));
} }
/**
* Generate project code: competition prefix + sequence number
* Format: C{competitionId}-P{sequence}, e.g., C1-P001
*/
private String generateProjectCode(Long competitionId) {
if (competitionId == null) {
return "P" + System.currentTimeMillis();
}
// Count existing projects for this competition
LambdaQueryWrapper<MartialProject> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialProject::getCompetitionId, competitionId);
long count = projectService.count(wrapper);
// Generate code: C{competitionId}-P{sequence}
return String.format("C%d-P%03d", competitionId, count + 1);
}
/** /**
* 删除 * 删除
*/ */

View File

@@ -13,21 +13,26 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.modules.martial.pojo.entity.MartialAthlete; import org.springblade.modules.martial.pojo.entity.MartialAthlete;
import org.springblade.modules.martial.pojo.entity.MartialCompetition; import org.springblade.modules.martial.pojo.entity.MartialCompetition;
import org.springblade.modules.martial.pojo.entity.MartialProject;
import org.springblade.modules.martial.pojo.entity.MartialRegistrationOrder; import org.springblade.modules.martial.pojo.entity.MartialRegistrationOrder;
import org.springblade.modules.martial.pojo.entity.MartialTeam; import org.springblade.modules.martial.pojo.entity.MartialTeam;
import org.springblade.modules.martial.pojo.entity.MartialTeamMember; import org.springblade.modules.martial.pojo.entity.MartialTeamMember;
import org.springblade.modules.martial.pojo.dto.RegistrationSubmitDTO; import org.springblade.modules.martial.pojo.dto.RegistrationSubmitDTO;
import org.springblade.modules.martial.pojo.vo.MartialRegistrationOrderVO; import org.springblade.modules.martial.pojo.vo.MartialRegistrationOrderVO;
import org.springblade.modules.martial.pojo.vo.OrganizationStatsVO;
import org.springblade.modules.martial.service.IMartialAthleteService; import org.springblade.modules.martial.service.IMartialAthleteService;
import org.springblade.modules.martial.service.IMartialCompetitionService; import org.springblade.modules.martial.service.IMartialCompetitionService;
import org.springblade.modules.martial.service.IMartialProjectService;
import org.springblade.modules.martial.service.IMartialRegistrationOrderService; import org.springblade.modules.martial.service.IMartialRegistrationOrderService;
import org.springblade.modules.martial.service.IMartialTeamService; import org.springblade.modules.martial.service.IMartialTeamService;
import org.springblade.modules.martial.mapper.MartialTeamMemberMapper; import org.springblade.modules.martial.mapper.MartialTeamMemberMapper;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.*;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@RestController @RestController
@@ -40,6 +45,7 @@ public class MartialRegistrationOrderController extends BladeController {
private final IMartialAthleteService athleteService; private final IMartialAthleteService athleteService;
private final IMartialTeamService teamService; private final IMartialTeamService teamService;
private final IMartialCompetitionService competitionService; private final IMartialCompetitionService competitionService;
private final IMartialProjectService projectService;
private final MartialTeamMemberMapper teamMemberMapper; private final MartialTeamMemberMapper teamMemberMapper;
@GetMapping("/detail") @GetMapping("/detail")
@@ -60,6 +66,192 @@ public class MartialRegistrationOrderController extends BladeController {
return R.data(pages); return R.data(pages);
} }
@GetMapping("/organization-stats")
@Operation(summary = "单位统计", description = "按单位统计运动员、项目、金额")
public R<List<OrganizationStatsVO>> getOrganizationStats(@RequestParam Long competitionId) {
log.info("获取单位统计: competitionId={}", competitionId);
// 1. Get all athletes for this competition
LambdaQueryWrapper<MartialAthlete> athleteWrapper = new LambdaQueryWrapper<>();
athleteWrapper.eq(MartialAthlete::getCompetitionId, competitionId)
.eq(MartialAthlete::getIsDeleted, 0);
List<MartialAthlete> athletes = athleteService.list(athleteWrapper);
if (athletes.isEmpty()) {
return R.data(new ArrayList<>());
}
// 2. Get all projects for this competition
Set<Long> projectIds = athletes.stream()
.map(MartialAthlete::getProjectId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
final Map<Long, MartialProject> projectMap = new HashMap<>();
if (!projectIds.isEmpty()) {
List<MartialProject> projects = projectService.listByIds(projectIds);
projectMap.putAll(projects.stream().collect(Collectors.toMap(MartialProject::getId, p -> p)));
}
// 3. Get team members for team projects
Set<Long> teamIds = athletes.stream()
.filter(a -> {
MartialProject project = projectMap.get(a.getProjectId());
return project != null && project.getType() != null && project.getType() == 2;
})
.map(a -> {
// Try to get team ID from team table by team name
LambdaQueryWrapper<MartialTeam> teamWrapper = new LambdaQueryWrapper<>();
teamWrapper.eq(MartialTeam::getTeamName, a.getTeamName())
.eq(MartialTeam::getIsDeleted, 0)
.last("LIMIT 1");
MartialTeam team = teamService.getOne(teamWrapper, false);
return team != null ? team.getId() : null;
})
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// Get team members
Map<Long, List<MartialTeamMember>> teamMembersMap = new HashMap<>();
if (!teamIds.isEmpty()) {
LambdaQueryWrapper<MartialTeamMember> memberWrapper = new LambdaQueryWrapper<>();
memberWrapper.in(MartialTeamMember::getTeamId, teamIds)
.eq(MartialTeamMember::getIsDeleted, 0);
List<MartialTeamMember> members = teamMemberMapper.selectList(memberWrapper);
teamMembersMap = members.stream().collect(Collectors.groupingBy(MartialTeamMember::getTeamId));
}
// 4. Group by organization and calculate stats
Map<String, OrganizationStatsVO> orgStatsMap = new LinkedHashMap<>();
for (MartialAthlete athlete : athletes) {
String org = athlete.getOrganization();
if (org == null || org.isEmpty()) {
org = "未知单位";
}
OrganizationStatsVO stats = orgStatsMap.computeIfAbsent(org, k -> {
OrganizationStatsVO vo = new OrganizationStatsVO();
vo.setOrganization(k);
vo.setAthleteCount(0);
vo.setProjectCount(0);
vo.setSingleProjectCount(0);
vo.setTeamProjectCount(0);
vo.setMaleCount(0);
vo.setFemaleCount(0);
vo.setTotalAmount(BigDecimal.ZERO);
vo.setProjectAmounts(new ArrayList<>());
return vo;
});
MartialProject project = projectMap.get(athlete.getProjectId());
if (project == null) continue;
// Check if project already counted for this org
boolean projectExists = stats.getProjectAmounts().stream()
.anyMatch(pa -> pa.getProjectId().equals(athlete.getProjectId()));
if (!projectExists) {
// Add project amount item
OrganizationStatsVO.ProjectAmountItem item = new OrganizationStatsVO.ProjectAmountItem();
item.setProjectId(project.getId());
item.setProjectName(project.getProjectName());
item.setProjectType(project.getType());
item.setCount(1);
item.setPrice(project.getPrice() != null ? project.getPrice() : BigDecimal.ZERO);
item.setAmount(item.getPrice());
stats.getProjectAmounts().add(item);
stats.setProjectCount(stats.getProjectCount() + 1);
if (project.getType() != null && project.getType() == 2) {
stats.setTeamProjectCount(stats.getTeamProjectCount() + 1);
} else {
stats.setSingleProjectCount(stats.getSingleProjectCount() + 1);
}
} else {
// Update count for existing project
stats.getProjectAmounts().stream()
.filter(pa -> pa.getProjectId().equals(athlete.getProjectId()))
.findFirst()
.ifPresent(pa -> {
pa.setCount(pa.getCount() + 1);
pa.setAmount(pa.getPrice().multiply(BigDecimal.valueOf(pa.getCount())));
});
}
}
// 5. Calculate unique athletes and gender counts per organization
for (Map.Entry<String, OrganizationStatsVO> entry : orgStatsMap.entrySet()) {
String org = entry.getKey();
OrganizationStatsVO stats = entry.getValue();
// Get all athletes for this org
Set<String> uniqueIdCards = new HashSet<>();
int maleCount = 0;
int femaleCount = 0;
for (MartialAthlete athlete : athletes) {
String athleteOrg = athlete.getOrganization();
if (athleteOrg == null || athleteOrg.isEmpty()) athleteOrg = "未知单位";
if (!athleteOrg.equals(org)) continue;
MartialProject project = projectMap.get(athlete.getProjectId());
if (project == null) continue;
// For individual projects, count the athlete
if (project.getType() == null || project.getType() == 1) {
String idCard = athlete.getIdCard();
if (idCard != null && !idCard.isEmpty() && !uniqueIdCards.contains(idCard)) {
uniqueIdCards.add(idCard);
if (athlete.getGender() != null && athlete.getGender() == 1) {
maleCount++;
} else if (athlete.getGender() != null && athlete.getGender() == 2) {
femaleCount++;
}
}
} else {
// For team projects, count team members
String teamName = athlete.getTeamName();
if (teamName != null) {
LambdaQueryWrapper<MartialTeam> teamWrapper = new LambdaQueryWrapper<>();
teamWrapper.eq(MartialTeam::getTeamName, teamName)
.eq(MartialTeam::getIsDeleted, 0)
.last("LIMIT 1");
MartialTeam team = teamService.getOne(teamWrapper, false);
if (team != null && teamMembersMap.containsKey(team.getId())) {
for (MartialTeamMember member : teamMembersMap.get(team.getId())) {
MartialAthlete memberAthlete = athleteService.getById(member.getAthleteId());
if (memberAthlete != null) {
String idCard = memberAthlete.getIdCard();
if (idCard != null && !idCard.isEmpty() && !uniqueIdCards.contains(idCard)) {
uniqueIdCards.add(idCard);
if (memberAthlete.getGender() != null && memberAthlete.getGender() == 1) {
maleCount++;
} else if (memberAthlete.getGender() != null && memberAthlete.getGender() == 2) {
femaleCount++;
}
}
}
}
}
}
}
}
stats.setAthleteCount(uniqueIdCards.size());
stats.setMaleCount(maleCount);
stats.setFemaleCount(femaleCount);
// Calculate total amount
BigDecimal totalAmount = stats.getProjectAmounts().stream()
.map(OrganizationStatsVO.ProjectAmountItem::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
stats.setTotalAmount(totalAmount);
}
return R.data(new ArrayList<>(orgStatsMap.values()));
}
@PostMapping("/submit") @PostMapping("/submit")
@Operation(summary = "提交报名", description = "提交报名订单并关联选手或集体") @Operation(summary = "提交报名", description = "提交报名订单并关联选手或集体")
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)

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

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

View File

@@ -19,4 +19,9 @@ public interface IMartialContactService extends IService<MartialContact> {
*/ */
MartialContact getContactDetail(Long id); MartialContact getContactDetail(Long id);
/**
* Save contact with default uniqueness handling
*/
boolean saveContact(MartialContact contact, Long userId);
} }

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

@@ -1,6 +1,7 @@
package org.springblade.modules.martial.service.impl; package org.springblade.modules.martial.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -10,6 +11,9 @@ import org.springblade.modules.martial.mapper.MartialContactMapper;
import org.springblade.modules.martial.pojo.entity.MartialContact; import org.springblade.modules.martial.pojo.entity.MartialContact;
import org.springblade.modules.martial.service.IMartialContactService; import org.springblade.modules.martial.service.IMartialContactService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
/** /**
* Contact Service Implementation * Contact Service Implementation
@@ -35,4 +39,32 @@ public class MartialContactServiceImpl extends ServiceImpl<MartialContactMapper,
return this.getById(id); return this.getById(id);
} }
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveContact(MartialContact contact, Long userId) {
// If setting as default, clear other defaults first
if (Boolean.TRUE.equals(contact.getIsDefault())) {
LambdaUpdateWrapper<MartialContact> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(MartialContact::getCreateUser, userId)
.eq(MartialContact::getIsDeleted, 0)
.set(MartialContact::getIsDefault, false);
// Exclude current contact if it's an update
if (contact.getId() != null) {
updateWrapper.ne(MartialContact::getId, contact.getId());
}
this.update(updateWrapper);
log.info("Cleared default status for user {}'s other contacts", userId);
}
// Set audit fields
if (contact.getId() == null) {
contact.setCreateUser(userId);
contact.setCreateTime(new Date());
}
contact.setUpdateUser(userId);
contact.setUpdateTime(new Date());
return this.saveOrUpdate(contact);
}
} }

View File

@@ -458,10 +458,14 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
} }
// 自动拆分大组:如果人数过多,拆分成多个小组 // 自动拆分大组:如果人数过多,拆分成多个小组
// 改进策略:根据人数动态拆分,确保能充分利用所有时间槽 // 优先使用项目配置的单位容纳人数(maxParticipants)
// 目标:让分组数量接近可用时间槽数量,实现均匀分配
int maxPeoplePerGroup; int maxPeoplePerGroup;
if (members.size() <= scheduleConfig.getMaxPeoplePerGroup()) { Integer projectMax = project.getMaxParticipants();
if (projectMax != null && projectMax > 0) {
// 使用项目配置的单位容纳人数
maxPeoplePerGroup = projectMax;
log.debug("项目 '{}' 使用自定义容纳人数: {}", projectName, projectMax);
} else if (members.size() <= scheduleConfig.getMaxPeoplePerGroup()) {
// 35人以内不拆分 // 35人以内不拆分
maxPeoplePerGroup = members.size(); maxPeoplePerGroup = members.size();
} else { } else {
@@ -585,8 +589,22 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
log.info("总共初始化了 {} 个场地×时间段组合", slots.size()); log.info("总共初始化了 {} 个场地×时间段组合", slots.size());
// 按预计时长降序排序(先安排时间长的) // 排序策略: 集体项目(projectType=2)优先,同类型按预计时长降序
groups.sort((a, b) -> b.getEstimatedDuration() - a.getEstimatedDuration()); groups.sort((a, b) -> {
// 1. 集体项目优先于单人项目
int aType = a.getProjectType() != null ? a.getProjectType() : 1;
int bType = b.getProjectType() != null ? b.getProjectType() : 1;
if (aType != bType) {
return bType - aType; // 2(集体) > 1(单人)
}
// 2. 同类型按预计时长降序(先安排时间长的)
int aDuration = a.getEstimatedDuration() != null ? a.getEstimatedDuration() : 0;
int bDuration = b.getEstimatedDuration() != null ? b.getEstimatedDuration() : 0;
return bDuration - aDuration;
});
log.info("排序后分组顺序(集体优先): {}",
groups.stream().map(g -> g.getGroupName() + "(type=" + g.getProjectType() + ")").collect(java.util.stream.Collectors.joining(", ")));
// 使用轮询算法进行均匀分配 // 使用轮询算法进行均匀分配
int assignedCount = 0; int assignedCount = 0;

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查询