fix(martial): harden mini auth and registration validation

This commit is contained in:
2026-02-16 01:50:51 +08:00
parent 88fef8f6f5
commit 7fba75ec35
5 changed files with 620 additions and 207 deletions
@@ -119,6 +119,7 @@ public class MartialAthleteController extends BladeController {
return R.fail("请先登录"); return R.fail("请先登录");
} }
MartialAthlete target;
if (athlete.getId() != null) { if (athlete.getId() != null) {
MartialAthlete existing = athleteService.getById(athlete.getId()); MartialAthlete existing = athleteService.getById(athlete.getId());
if (existing == null) { if (existing == null) {
@@ -127,14 +128,21 @@ public class MartialAthleteController extends BladeController {
if (!isAdminUser() && !userId.equals(existing.getCreateUser())) { if (!isAdminUser() && !userId.equals(existing.getCreateUser())) {
return R.fail("无权限修改该选手"); return R.fail("无权限修改该选手");
} }
athlete.setCreateUser(existing.getCreateUser()); target = existing;
} else { } else {
athlete.setCreateUser(userId); target = new MartialAthlete();
target.setCreateUser(userId);
target.setRegistrationStatus(0);
target.setCompetitionStatus(0);
} }
log.info("=== 提交选手 === userId: {}, playerName: {}", userId, athlete.getPlayerName()); applyEditableFields(target, athlete);
athlete.setUpdateUser(userId); log.info("=== 提交选手 === userId: {}, playerName: {}", userId, target.getPlayerName());
return R.status(athleteService.saveOrUpdate(athlete)); target.setUpdateUser(userId);
if (target.getId() == null) {
return R.status(athleteService.save(target));
}
return R.status(athleteService.updateById(target));
} }
/** /**
@@ -245,4 +253,27 @@ public class MartialAthleteController extends BladeController {
return AuthUtil.isAdmin() || AuthUtil.isAdministrator(); return AuthUtil.isAdmin() || AuthUtil.isAdministrator();
} }
private void applyEditableFields(MartialAthlete target, MartialAthlete source) {
target.setCompetitionId(source.getCompetitionId());
target.setProjectId(source.getProjectId());
target.setPlayerName(source.getPlayerName());
target.setPlayerNo(source.getPlayerNo());
target.setGender(source.getGender());
target.setAge(source.getAge());
target.setIdCard(source.getIdCard());
target.setIdCardType(source.getIdCardType());
target.setBirthDate(source.getBirthDate());
target.setNation(source.getNation());
target.setContactPhone(source.getContactPhone());
target.setOrganization(source.getOrganization());
target.setOrganizationType(source.getOrganizationType());
target.setTeamName(source.getTeamName());
target.setCategory(source.getCategory());
target.setOrderNum(source.getOrderNum());
target.setIntroduction(source.getIntroduction());
target.setAttachments(source.getAttachments());
target.setPhotoUrl(source.getPhotoUrl());
target.setRemark(source.getRemark());
}
} }
@@ -93,17 +93,36 @@ public class MartialExportController {
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);
String html = template String html = template
.replace("${playerName}", certificate.getPlayerName()) .replace("${playerName}", escapeHtml(certificate.getPlayerName()))
.replace("${competitionName}", certificate.getCompetitionName()) .replace("${competitionName}", escapeHtml(certificate.getCompetitionName()))
.replace("${projectName}", certificate.getProjectName()) .replace("${projectName}", escapeHtml(certificate.getProjectName()))
.replace("${medalName}", certificate.getMedalName()) .replace("${medalName}", escapeHtml(certificate.getMedalName()))
.replace("${medalClass}", certificate.getMedalClass()) .replace("${medalClass}", sanitizeCssClass(certificate.getMedalClass()))
.replace("${organization}", certificate.getOrganization()) .replace("${organization}", escapeHtml(certificate.getOrganization()))
.replace("${issueDate}", certificate.getIssueDate()); .replace("${issueDate}", escapeHtml(certificate.getIssueDate()));
response.setContentType("text/html;charset=UTF-8"); response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(html); response.getWriter().write(html);
} }
private String escapeHtml(String input) {
if (input == null) {
return "";
}
return input
.replace("&", "&")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;");
}
private String sanitizeCssClass(String input) {
if (input == null) {
return "";
}
return input.replaceAll("[^a-zA-Z0-9_-]", "");
}
@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) {
@@ -110,15 +110,50 @@ public class MartialMiniController extends BladeController {
return R.fail("请求体不能为空"); return R.fail("请求体不能为空");
} }
JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken); JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken);
if (authContext == null || authContext.getJudgeId() == null) { if (authContext == null || authContext.getJudgeId() == null || !authContext.isInviteTokenAuth()) {
return R.fail("登录状态无效或已过期"); return R.fail("登录状态无效或已过期");
} }
Long athleteId = parseLong(dto.getAthleteId());
Long competitionId = parseLong(dto.getCompetitionId());
Long projectId = parseLong(dto.getProjectId());
Long venueId = parseLong(dto.getVenueId());
if (athleteId == null || competitionId == null || projectId == null) {
return R.fail("评分参数不完整");
}
if (!hasCompetitionAccess(authContext, competitionId)) {
return R.fail("无权访问该赛事");
}
if (!hasVenueAccess(authContext, venueId)) {
return R.fail("无权访问该场地");
}
if (!hasProjectAccess(authContext, projectId)) {
return R.fail("无权为该项目评分");
}
MartialAthlete athlete = athleteService.getById(athleteId);
if (athlete == null) {
return R.fail("选手不存在");
}
if (!competitionId.equals(athlete.getCompetitionId()) || !projectId.equals(athlete.getProjectId())) {
return R.fail("评分参数与选手信息不匹配");
}
if (!hasAthleteAccess(authContext, athlete)) {
return R.fail("无权访问该选手");
}
Long payloadJudgeId = parseLong(dto.getJudgeId()); Long payloadJudgeId = parseLong(dto.getJudgeId());
if (payloadJudgeId != null && !payloadJudgeId.equals(authContext.getJudgeId())) { if (payloadJudgeId != null && !payloadJudgeId.equals(authContext.getJudgeId())) {
log.warn("评分提交身份字段不匹配,已忽略请求体judgeIdpayloadJudgeId={}, authJudgeId={}", payloadJudgeId, authContext.getJudgeId()); log.warn("评分提交身份字段不匹配,已忽略请求体judgeIdpayloadJudgeId={}, authJudgeId={}", payloadJudgeId, authContext.getJudgeId());
} }
// 向后兼容:保留 judgeId 字段,但强制覆盖为 token 身份 // 向后兼容:保留 judgeId 字段,但强制覆盖为 token 身份
dto.setJudgeId(String.valueOf(authContext.getJudgeId())); dto.setJudgeId(String.valueOf(authContext.getJudgeId()));
// 防止参数篡改:赛事/项目以选手数据和当前上下文为准
dto.setCompetitionId(String.valueOf(athlete.getCompetitionId()));
dto.setProjectId(String.valueOf(athlete.getProjectId()));
if (venueId != null) {
dto.setVenueId(String.valueOf(venueId));
}
return miniScoringService.submitScore(dto); return miniScoringService.submitScore(dto);
} }
@@ -258,7 +293,7 @@ public class MartialMiniController extends BladeController {
if (accessToken != null) { if (accessToken != null) {
MartialJudgeInvite invite = findValidInviteByAccessToken(accessToken); MartialJudgeInvite invite = findValidInviteByAccessToken(accessToken);
if (invite != null && invite.getJudgeId() != null) { if (invite != null && invite.getJudgeId() != null) {
return JudgeAuthContext.fromInvite(invite); return JudgeAuthContext.fromInvite(invite, parseProjectIds(invite.getProjects()));
} }
} }
@@ -311,6 +346,77 @@ public class MartialMiniController extends BladeController {
return invite; return invite;
} }
private List<Long> parseProjectIds(String rawProjects) {
if (Func.isEmpty(rawProjects)) {
return new ArrayList<>();
}
try {
ObjectMapper mapper = new ObjectMapper();
List<Long> ids = mapper.readValue(rawProjects, new TypeReference<List<Long>>() {
});
return ids == null ? new ArrayList<>() : ids.stream()
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
} catch (Exception ignore) {
List<Long> ids = Func.toLongList(rawProjects);
if (ids == null) {
return new ArrayList<>();
}
return ids.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
}
}
private boolean hasCompetitionAccess(JudgeAuthContext authContext, Long competitionId) {
if (competitionId == null) {
return false;
}
if (!authContext.isInviteTokenAuth()) {
return true;
}
return competitionId.equals(authContext.getCompetitionId());
}
private boolean hasVenueAccess(JudgeAuthContext authContext, Long venueId) {
if (venueId == null) {
return true;
}
if (authContext.isGeneralJudge()) {
return true;
}
if (!authContext.isInviteTokenAuth()) {
return true;
}
if (authContext.getVenueId() == null) {
return false;
}
return venueId.equals(authContext.getVenueId());
}
private boolean hasProjectAccess(JudgeAuthContext authContext, Long projectId) {
if (projectId == null) {
return true;
}
if (authContext.isGeneralJudge()) {
return true;
}
List<Long> assignedProjects = authContext.getProjectIds();
if (assignedProjects == null || assignedProjects.isEmpty()) {
return true;
}
return assignedProjects.contains(projectId);
}
private boolean hasAthleteAccess(JudgeAuthContext authContext, MartialAthlete athlete) {
if (athlete == null) {
return false;
}
if (!hasCompetitionAccess(authContext, athlete.getCompetitionId())) {
return false;
}
return hasProjectAccess(authContext, athlete.getProjectId());
}
/** /**
* 获取选手列表(支持分页) * 获取选手列表(支持分页)
* - 裁判员:获取所有选手,标记是否已评分 * - 裁判员:获取所有选手,标记是否已评分
@@ -319,8 +425,10 @@ public class MartialMiniController extends BladeController {
@GetMapping("/score/athletes") @GetMapping("/score/athletes")
@Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)") @Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)")
public R<IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO>> getAthletes( public R<IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO>> getAthletes(
@RequestParam Long judgeId, @RequestHeader(value = "Authorization", required = false) String authorization,
@RequestParam Integer refereeType, @RequestHeader(value = "Blade-Auth", required = false) String bladeAuthToken,
@RequestParam(required = false) Long judgeId,
@RequestParam(required = false) Integer refereeType,
@RequestParam(required = false) Long projectId, @RequestParam(required = false) Long projectId,
@RequestParam(required = false) Long orderId, @RequestParam(required = false) Long orderId,
@RequestParam(required = false) Long venueId, @RequestParam(required = false) Long venueId,
@@ -328,6 +436,27 @@ public class MartialMiniController extends BladeController {
@RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size @RequestParam(defaultValue = "10") Integer size
) { ) {
JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken);
if (authContext == null || authContext.getJudgeId() == null || !authContext.isInviteTokenAuth()) {
return R.fail("登录状态无效或已过期");
}
if (competitionId == null) {
return R.fail("赛事ID不能为空");
}
if (!hasCompetitionAccess(authContext, competitionId)) {
return R.fail("无权访问该赛事");
}
if (venueId == null && !authContext.isGeneralJudge() && authContext.getVenueId() != null) {
venueId = authContext.getVenueId();
}
if (!hasVenueAccess(authContext, venueId)) {
return R.fail("无权访问该场地");
}
if (!hasProjectAccess(authContext, projectId)) {
return R.fail("无权访问该项目");
}
Long currentJudgeId = authContext.getJudgeId();
// 1. 优先从编排表查询选手(按出场顺序) // 1. 优先从编排表查询选手(按出场顺序)
List<MartialAthlete> athletes = getAthletesFromSchedule(competitionId, projectId, venueId); List<MartialAthlete> athletes = getAthletesFromSchedule(competitionId, projectId, venueId);
@@ -335,15 +464,19 @@ public class MartialMiniController extends BladeController {
if (athletes.isEmpty()) { if (athletes.isEmpty()) {
LambdaQueryWrapper<MartialAthlete> athleteQuery = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialAthlete> athleteQuery = new LambdaQueryWrapper<>();
athleteQuery.eq(MartialAthlete::getIsDeleted, 0); athleteQuery.eq(MartialAthlete::getIsDeleted, 0);
if (competitionId != null) { athleteQuery.eq(MartialAthlete::getCompetitionId, competitionId);
athleteQuery.eq(MartialAthlete::getCompetitionId, competitionId);
}
if (projectId != null) { if (projectId != null) {
athleteQuery.eq(MartialAthlete::getProjectId, projectId); athleteQuery.eq(MartialAthlete::getProjectId, projectId);
} }
athleteQuery.orderByAsc(MartialAthlete::getOrderNum); athleteQuery.orderByAsc(MartialAthlete::getOrderNum);
athletes = athleteService.list(athleteQuery); athletes = athleteService.list(athleteQuery);
} }
if (!authContext.isGeneralJudge() && authContext.getProjectIds() != null && !authContext.getProjectIds().isEmpty()) {
List<Long> allowedProjects = authContext.getProjectIds();
athletes = athletes.stream()
.filter(a -> a.getProjectId() != null && allowedProjects.contains(a.getProjectId()))
.collect(Collectors.toList());
}
// 2. 获取该场地所有主裁判的judge_id列表 // 2. 获取该场地所有主裁判的judge_id列表
List<Long> chiefJudgeIds = getChiefJudgeIds(venueId); List<Long> chiefJudgeIds = getChiefJudgeIds(venueId);
@@ -351,6 +484,7 @@ public class MartialMiniController extends BladeController {
// 3. 获取所有评分记录(排除主裁判的评分) // 3. 获取所有评分记录(排除主裁判的评分)
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getIsDeleted, 0); scoreQuery.eq(MartialScore::getIsDeleted, 0);
scoreQuery.eq(MartialScore::getCompetitionId, competitionId);
if (projectId != null) { if (projectId != null) {
scoreQuery.eq(MartialScore::getProjectId, projectId); scoreQuery.eq(MartialScore::getProjectId, projectId);
} }
@@ -373,18 +507,9 @@ public class MartialMiniController extends BladeController {
// 5. 根据裁判类型处理选手列表 // 5. 根据裁判类型处理选手列表
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList; List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList;
filteredList = athletes.stream()
if (refereeType == 1) { .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), currentJudgeId, requiredJudgeCount))
// 主裁判:返回所有选手,前端根据totalScore判断是否显示修改按钮 .collect(Collectors.toList());
filteredList = athletes.stream()
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount))
.collect(java.util.stream.Collectors.toList());
} else {
// 裁判员:返回所有选手,标记是否已评分
filteredList = athletes.stream()
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount))
.collect(java.util.stream.Collectors.toList());
}
// 6. 手动分页 // 6. 手动分页
int total = filteredList.size(); int total = filteredList.size();
@@ -503,7 +628,22 @@ public class MartialMiniController extends BladeController {
*/ */
@GetMapping("/score/detail/{athleteId}") @GetMapping("/score/detail/{athleteId}")
@Operation(summary = "评分详情", description = "查看选手的所有评委评分") @Operation(summary = "评分详情", description = "查看选手的所有评委评分")
public R<MiniScoreDetailVO> getScoreDetail(@PathVariable Long athleteId) { public R<MiniScoreDetailVO> getScoreDetail(
@RequestHeader(value = "Authorization", required = false) String authorization,
@RequestHeader(value = "Blade-Auth", required = false) String bladeAuthToken,
@PathVariable Long athleteId
) {
JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken);
if (authContext == null || authContext.getJudgeId() == null || !authContext.isInviteTokenAuth()) {
return R.fail("登录状态无效或已过期");
}
MartialAthlete athlete = athleteService.getById(athleteId);
if (athlete == null) {
return R.fail("选手不存在");
}
if (!hasAthleteAccess(authContext, athlete)) {
return R.fail("无权访问该选手");
}
MiniScoreDetailVO detail = scoreService.getScoreDetailForMini(athleteId); MiniScoreDetailVO detail = scoreService.getScoreDetailForMini(athleteId);
return R.data(detail); return R.data(detail);
} }
@@ -519,7 +659,7 @@ public class MartialMiniController extends BladeController {
@Valid @RequestBody MiniScoreModifyDTO dto @Valid @RequestBody MiniScoreModifyDTO dto
) { ) {
JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken); JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken);
if (authContext == null || authContext.getJudgeId() == null) { if (authContext == null || authContext.getJudgeId() == null || !authContext.isInviteTokenAuth()) {
return R.fail("登录状态无效或已过期"); return R.fail("登录状态无效或已过期");
} }
if (!authContext.isChiefJudge()) { if (!authContext.isChiefJudge()) {
@@ -530,6 +670,16 @@ public class MartialMiniController extends BladeController {
if (dto.getModifierId() != null && !dto.getModifierId().equals(authContext.getJudgeId())) { if (dto.getModifierId() != null && !dto.getModifierId().equals(authContext.getJudgeId())) {
log.warn("主裁判改分身份字段不匹配,已忽略请求体modifierIdpayloadModifierId={}, authJudgeId={}", dto.getModifierId(), authContext.getJudgeId()); log.warn("主裁判改分身份字段不匹配,已忽略请求体modifierIdpayloadModifierId={}, authJudgeId={}", dto.getModifierId(), authContext.getJudgeId());
} }
if (!hasVenueAccess(authContext, dto.getVenueId())) {
return R.fail("无权访问该场地");
}
MartialAthlete athlete = athleteService.getById(dto.getAthleteId());
if (athlete == null) {
return R.fail("选手不存在");
}
if (!hasAthleteAccess(authContext, athlete)) {
return R.fail("无权访问该选手");
}
// 向后兼容:忽略请求体 modifierId,以 token 身份为准 // 向后兼容:忽略请求体 modifierId,以 token 身份为准
dto.setModifierId(authContext.getJudgeId()); dto.setModifierId(authContext.getJudgeId());
boolean success = scoreService.modifyScoreByAdmin(dto); boolean success = scoreService.modifyScoreByAdmin(dto);
@@ -830,7 +980,7 @@ public class MartialMiniController extends BladeController {
return R.fail("参数错误"); return R.fail("参数错误");
} }
JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken); JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken);
if (authContext == null || authContext.getJudgeId() == null) { if (authContext == null || authContext.getJudgeId() == null || !authContext.isInviteTokenAuth()) {
return R.fail("登录状态无效或已过期"); return R.fail("登录状态无效或已过期");
} }
if (!authContext.isChiefJudge()) { if (!authContext.isChiefJudge()) {
@@ -847,6 +997,16 @@ public class MartialMiniController extends BladeController {
log.warn("主裁判确认身份字段不匹配,已忽略请求体chiefJudgeIdpayloadChiefJudgeId={}, authJudgeId={}", log.warn("主裁判确认身份字段不匹配,已忽略请求体chiefJudgeIdpayloadChiefJudgeId={}, authJudgeId={}",
payloadChiefJudgeId, authContext.getJudgeId()); payloadChiefJudgeId, authContext.getJudgeId());
} }
MartialResult result = resultService.getById(resultId);
if (result == null) {
return R.fail("成绩不存在");
}
if (!hasCompetitionAccess(authContext, result.getCompetitionId())) {
return R.fail("无权访问该赛事");
}
if (!hasVenueAccess(authContext, result.getVenueId())) {
return R.fail("无权访问该场地");
}
boolean success = resultService.confirmByChiefJudge(resultId, authContext.getJudgeId(), dto.getScore(), dto.getNote()); boolean success = resultService.confirmByChiefJudge(resultId, authContext.getJudgeId(), dto.getScore(), dto.getNote());
return success ? R.success("确认成功") : R.fail("确认失败"); return success ? R.success("确认成功") : R.fail("确认失败");
} }
@@ -865,7 +1025,7 @@ public class MartialMiniController extends BladeController {
return R.fail("参数错误"); return R.fail("参数错误");
} }
JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken); JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken);
if (authContext == null || authContext.getJudgeId() == null) { if (authContext == null || authContext.getJudgeId() == null || !authContext.isInviteTokenAuth()) {
return R.fail("登录状态无效或已过期"); return R.fail("登录状态无效或已过期");
} }
if (!authContext.isGeneralJudge()) { if (!authContext.isGeneralJudge()) {
@@ -882,6 +1042,13 @@ public class MartialMiniController extends BladeController {
log.warn("总裁确认身份字段不匹配,已忽略请求体generalJudgeIdpayloadGeneralJudgeId={}, authJudgeId={}", log.warn("总裁确认身份字段不匹配,已忽略请求体generalJudgeIdpayloadGeneralJudgeId={}, authJudgeId={}",
payloadGeneralJudgeId, authContext.getJudgeId()); payloadGeneralJudgeId, authContext.getJudgeId());
} }
MartialResult result = resultService.getById(resultId);
if (result == null) {
return R.fail("成绩不存在");
}
if (!hasCompetitionAccess(authContext, result.getCompetitionId())) {
return R.fail("无权访问该赛事");
}
boolean success = resultService.confirmByGeneralJudge(resultId, authContext.getJudgeId(), dto.getScore(), dto.getNote()); boolean success = resultService.confirmByGeneralJudge(resultId, authContext.getJudgeId(), dto.getScore(), dto.getNote());
return success ? R.success("确认成功") : R.fail("确认失败"); return success ? R.success("确认成功") : R.fail("确认失败");
} }
@@ -891,7 +1058,21 @@ public class MartialMiniController extends BladeController {
*/ */
@GetMapping("/chief/pending") @GetMapping("/chief/pending")
@Operation(summary = "待主裁判确认列表", description = "获取待主裁判确认的成绩列表") @Operation(summary = "待主裁判确认列表", description = "获取待主裁判确认的成绩列表")
public R<List<MartialResult>> getPendingChiefConfirmList(@RequestParam Long venueId) { public R<List<MartialResult>> getPendingChiefConfirmList(
@RequestHeader(value = "Authorization", required = false) String authorization,
@RequestHeader(value = "Blade-Auth", required = false) String bladeAuthToken,
@RequestParam Long venueId
) {
JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken);
if (authContext == null || authContext.getJudgeId() == null || !authContext.isInviteTokenAuth()) {
return R.fail("登录状态无效或已过期");
}
if (!authContext.isChiefJudge()) {
return R.fail("无主裁判权限");
}
if (!hasVenueAccess(authContext, venueId)) {
return R.fail("无权访问该场地");
}
List<MartialResult> list = resultService.getPendingChiefConfirmList(venueId); List<MartialResult> list = resultService.getPendingChiefConfirmList(venueId);
return R.data(list); return R.data(list);
} }
@@ -901,7 +1082,21 @@ public class MartialMiniController extends BladeController {
*/ */
@GetMapping("/general/pending") @GetMapping("/general/pending")
@Operation(summary = "待总裁确认列表", description = "获取待总裁确认的成绩列表(所有场地)") @Operation(summary = "待总裁确认列表", description = "获取待总裁确认的成绩列表(所有场地)")
public R<List<MartialResult>> getPendingGeneralConfirmList(@RequestParam Long competitionId) { public R<List<MartialResult>> getPendingGeneralConfirmList(
@RequestHeader(value = "Authorization", required = false) String authorization,
@RequestHeader(value = "Blade-Auth", required = false) String bladeAuthToken,
@RequestParam Long competitionId
) {
JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken);
if (authContext == null || authContext.getJudgeId() == null || !authContext.isInviteTokenAuth()) {
return R.fail("登录状态无效或已过期");
}
if (!authContext.isGeneralJudge()) {
return R.fail("无总裁权限");
}
if (!hasCompetitionAccess(authContext, competitionId)) {
return R.fail("无权访问该赛事");
}
List<MartialResult> list = resultService.getPendingGeneralConfirmList(competitionId); List<MartialResult> list = resultService.getPendingGeneralConfirmList(competitionId);
return R.data(list); return R.data(list);
} }
@@ -911,7 +1106,21 @@ public class MartialMiniController extends BladeController {
*/ */
@GetMapping("/general/venues") @GetMapping("/general/venues")
@Operation(summary = "获取所有场地", description = "总裁获取比赛的所有场地列表") @Operation(summary = "获取所有场地", description = "总裁获取比赛的所有场地列表")
public R<List<MartialVenue>> getAllVenues(@RequestParam Long competitionId) { public R<List<MartialVenue>> getAllVenues(
@RequestHeader(value = "Authorization", required = false) String authorization,
@RequestHeader(value = "Blade-Auth", required = false) String bladeAuthToken,
@RequestParam Long competitionId
) {
JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken);
if (authContext == null || authContext.getJudgeId() == null || !authContext.isInviteTokenAuth()) {
return R.fail("登录状态无效或已过期");
}
if (!authContext.isGeneralJudge()) {
return R.fail("无总裁权限");
}
if (!hasCompetitionAccess(authContext, competitionId)) {
return R.fail("无权访问该赛事");
}
LambdaQueryWrapper<MartialVenue> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialVenue> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialVenue::getCompetitionId, competitionId); wrapper.eq(MartialVenue::getCompetitionId, competitionId);
wrapper.eq(MartialVenue::getIsDeleted, 0); wrapper.eq(MartialVenue::getIsDeleted, 0);
@@ -925,7 +1134,21 @@ public class MartialMiniController extends BladeController {
*/ */
@GetMapping("/general/confirmed") @GetMapping("/general/confirmed")
@Operation(summary = "已总裁确认列表", description = "获取已总裁确认的成绩列表") @Operation(summary = "已总裁确认列表", description = "获取已总裁确认的成绩列表")
public R<List<MartialResult>> getConfirmedGeneralList(@RequestParam Long competitionId) { public R<List<MartialResult>> getConfirmedGeneralList(
@RequestHeader(value = "Authorization", required = false) String authorization,
@RequestHeader(value = "Blade-Auth", required = false) String bladeAuthToken,
@RequestParam Long competitionId
) {
JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken);
if (authContext == null || authContext.getJudgeId() == null || !authContext.isInviteTokenAuth()) {
return R.fail("登录状态无效或已过期");
}
if (!authContext.isGeneralJudge()) {
return R.fail("无总裁权限");
}
if (!hasCompetitionAccess(authContext, competitionId)) {
return R.fail("无权访问该赛事");
}
List<MartialResult> list = resultService.getConfirmedGeneralList(competitionId); List<MartialResult> list = resultService.getConfirmedGeneralList(competitionId);
return R.data(list); return R.data(list);
} }
@@ -1115,15 +1338,31 @@ public class MartialMiniController extends BladeController {
private final Long judgeId; private final Long judgeId;
private final String role; private final String role;
private final Integer refereeType; private final Integer refereeType;
private final Long competitionId;
private final Long venueId;
private final List<Long> projectIds;
private final boolean inviteTokenAuth;
private JudgeAuthContext(Long judgeId, String role, Integer refereeType) { private JudgeAuthContext(Long judgeId, String role, Integer refereeType, Long competitionId, Long venueId, List<Long> projectIds, boolean inviteTokenAuth) {
this.judgeId = judgeId; this.judgeId = judgeId;
this.role = role; this.role = role;
this.refereeType = refereeType; this.refereeType = refereeType;
this.competitionId = competitionId;
this.venueId = venueId;
this.projectIds = projectIds;
this.inviteTokenAuth = inviteTokenAuth;
} }
static JudgeAuthContext fromInvite(MartialJudgeInvite invite) { static JudgeAuthContext fromInvite(MartialJudgeInvite invite, List<Long> projectIds) {
return new JudgeAuthContext(invite.getJudgeId(), invite.getRole(), invite.getRefereeType()); return new JudgeAuthContext(
invite.getJudgeId(),
invite.getRole(),
invite.getRefereeType(),
invite.getCompetitionId(),
invite.getVenueId(),
projectIds == null ? new ArrayList<>() : projectIds,
true
);
} }
static JudgeAuthContext fromJudge(MartialJudge judge) { static JudgeAuthContext fromJudge(MartialJudge judge) {
@@ -1136,7 +1375,7 @@ public class MartialMiniController extends BladeController {
} else { } else {
role = "judge"; role = "judge";
} }
return new JudgeAuthContext(judge.getId(), role, refereeType); return new JudgeAuthContext(judge.getId(), role, refereeType, null, null, new ArrayList<>(), false);
} }
Long getJudgeId() { Long getJudgeId() {
@@ -1151,6 +1390,22 @@ public class MartialMiniController extends BladeController {
return refereeType; return refereeType;
} }
Long getCompetitionId() {
return competitionId;
}
Long getVenueId() {
return venueId;
}
List<Long> getProjectIds() {
return projectIds;
}
boolean isInviteTokenAuth() {
return inviteTokenAuth;
}
boolean isChiefJudge() { boolean isChiefJudge() {
return "chief_judge".equals(role) || (refereeType != null && refereeType == 1); return "chief_judge".equals(role) || (refereeType != null && refereeType == 1);
} }
@@ -57,6 +57,17 @@ public class MartialRegistrationOrderController extends BladeController {
@PreAuth(RoleConstant.HAS_ROLE_USER) @PreAuth(RoleConstant.HAS_ROLE_USER)
@Operation(summary = "详情", description = "传入ID") @Operation(summary = "详情", description = "传入ID")
public R detail(@RequestParam Long id) { public R detail(@RequestParam Long id) {
Long userId = AuthUtil.getUserId();
if (userId == null || userId <= 0) {
return R.fail("请先登录");
}
MartialRegistrationOrder order = registrationOrderService.getById(id);
if (order == null) {
return R.fail("订单不存在");
}
if (!isAdminUser() && !userId.equals(order.getUserId())) {
return R.fail("仅可查看自己的订单");
}
return R.data(registrationOrderService.getDetailWithRelations(id)); return R.data(registrationOrderService.getDetailWithRelations(id));
} }
@@ -79,187 +90,166 @@ public class MartialRegistrationOrderController extends BladeController {
public R<List<OrganizationStatsVO>> getOrganizationStats(@RequestParam Long competitionId) { public R<List<OrganizationStatsVO>> getOrganizationStats(@RequestParam Long competitionId) {
log.info("获取单位统计: competitionId={}", competitionId); log.info("获取单位统计: competitionId={}", competitionId);
// 1. Get all athletes for this competition
LambdaQueryWrapper<MartialAthlete> athleteWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialAthlete> athleteWrapper = new LambdaQueryWrapper<>();
athleteWrapper.eq(MartialAthlete::getCompetitionId, competitionId) athleteWrapper.eq(MartialAthlete::getCompetitionId, competitionId)
.eq(MartialAthlete::getIsDeleted, 0); .eq(MartialAthlete::getIsDeleted, 0);
List<MartialAthlete> athletes = athleteService.list(athleteWrapper); List<MartialAthlete> athletes = athleteService.list(athleteWrapper);
if (athletes.isEmpty()) { if (athletes.isEmpty()) {
return R.data(new ArrayList<>()); return R.data(new ArrayList<>());
} }
// 2. Get all projects for this competition
Set<Long> projectIds = athletes.stream() Set<Long> projectIds = athletes.stream()
.map(MartialAthlete::getProjectId) .map(MartialAthlete::getProjectId)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
Map<Long, MartialProject> projectMap = projectIds.isEmpty() ? new HashMap<>() :
projectService.listByIds(projectIds).stream()
.collect(Collectors.toMap(MartialProject::getId, p -> p, (a, b) -> a));
final Map<Long, MartialProject> projectMap = new HashMap<>(); Set<String> teamNames = athletes.stream()
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 -> { .filter(a -> {
MartialProject project = projectMap.get(a.getProjectId()); MartialProject project = projectMap.get(a.getProjectId());
return project != null && project.getType() != null && project.getType() == 2; return project != null && project.getType() != null && project.getType() == 2 && Func.isNotBlank(a.getTeamName());
}) })
.map(a -> { .map(MartialAthlete::getTeamName)
// 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()); .collect(Collectors.toSet());
// Get team members Map<String, MartialTeam> teamByName = new HashMap<>();
Map<Long, List<MartialTeamMember>> teamMembersMap = new HashMap<>(); Map<Long, List<MartialTeamMember>> teamMembersByTeamId = new HashMap<>();
if (!teamIds.isEmpty()) { Map<Long, MartialAthlete> memberAthleteById = new HashMap<>();
LambdaQueryWrapper<MartialTeamMember> memberWrapper = new LambdaQueryWrapper<>(); if (!teamNames.isEmpty()) {
memberWrapper.in(MartialTeamMember::getTeamId, teamIds) LambdaQueryWrapper<MartialTeam> teamWrapper = new LambdaQueryWrapper<>();
.eq(MartialTeamMember::getIsDeleted, 0); teamWrapper.in(MartialTeam::getTeamName, teamNames)
List<MartialTeamMember> members = teamMemberMapper.selectList(memberWrapper); .eq(MartialTeam::getIsDeleted, 0);
teamMembersMap = members.stream().collect(Collectors.groupingBy(MartialTeamMember::getTeamId)); List<MartialTeam> teams = teamService.list(teamWrapper);
teamByName = teams.stream().collect(Collectors.toMap(MartialTeam::getTeamName, t -> t, (a, b) -> a));
Set<Long> teamIds = teams.stream().map(MartialTeam::getId).filter(Objects::nonNull).collect(Collectors.toSet());
if (!teamIds.isEmpty()) {
LambdaQueryWrapper<MartialTeamMember> memberWrapper = new LambdaQueryWrapper<>();
memberWrapper.in(MartialTeamMember::getTeamId, teamIds)
.eq(MartialTeamMember::getIsDeleted, 0);
List<MartialTeamMember> members = teamMemberMapper.selectList(memberWrapper);
teamMembersByTeamId = members.stream().collect(Collectors.groupingBy(MartialTeamMember::getTeamId));
Set<Long> memberAthleteIds = members.stream()
.map(MartialTeamMember::getAthleteId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (!memberAthleteIds.isEmpty()) {
LambdaQueryWrapper<MartialAthlete> memberAthleteWrapper = new LambdaQueryWrapper<>();
memberAthleteWrapper.in(MartialAthlete::getId, memberAthleteIds)
.eq(MartialAthlete::getIsDeleted, 0);
memberAthleteById = athleteService.list(memberAthleteWrapper).stream()
.collect(Collectors.toMap(MartialAthlete::getId, a -> a, (a, b) -> a));
}
}
} }
// 4. Group by organization and calculate stats
Map<String, OrganizationStatsVO> orgStatsMap = new LinkedHashMap<>(); Map<String, OrganizationStatsVO> orgStatsMap = new LinkedHashMap<>();
Map<String, Set<String>> orgUniqueIdCards = new HashMap<>();
for (MartialAthlete athlete : athletes) { for (MartialAthlete athlete : athletes) {
String org = athlete.getOrganization(); String org = normalizeOrganization(athlete.getOrganization());
if (org == null || org.isEmpty()) { OrganizationStatsVO stats = orgStatsMap.computeIfAbsent(org, this::initOrganizationStats);
org = "未知单位"; Set<String> uniqueIdCards = orgUniqueIdCards.computeIfAbsent(org, k -> new HashSet<>());
}
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()); MartialProject project = projectMap.get(athlete.getProjectId());
if (project == null) continue; if (project == null) {
continue;
}
// Check if project already counted for this org upsertProjectAmount(stats, project);
boolean projectExists = stats.getProjectAmounts().stream()
.anyMatch(pa -> pa.getProjectId().equals(athlete.getProjectId()));
if (!projectExists) { if (project.getType() == null || project.getType() == 1) {
// Add project amount item collectAthleteGender(stats, uniqueIdCards, athlete);
OrganizationStatsVO.ProjectAmountItem item = new OrganizationStatsVO.ProjectAmountItem(); continue;
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 (Func.isBlank(athlete.getTeamName())) {
if (project.getType() != null && project.getType() == 2) { continue;
stats.setTeamProjectCount(stats.getTeamProjectCount() + 1); }
} else { MartialTeam team = teamByName.get(athlete.getTeamName());
stats.setSingleProjectCount(stats.getSingleProjectCount() + 1); if (team == null) {
} continue;
} else { }
// Update count for existing project List<MartialTeamMember> members = teamMembersByTeamId.getOrDefault(team.getId(), new ArrayList<>());
stats.getProjectAmounts().stream() for (MartialTeamMember member : members) {
.filter(pa -> pa.getProjectId().equals(athlete.getProjectId())) MartialAthlete memberAthlete = memberAthleteById.get(member.getAthleteId());
.findFirst() collectAthleteGender(stats, uniqueIdCards, memberAthlete);
.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 orgStatsMap.values().forEach(stats -> {
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() BigDecimal totalAmount = stats.getProjectAmounts().stream()
.map(OrganizationStatsVO.ProjectAmountItem::getAmount) .map(OrganizationStatsVO.ProjectAmountItem::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add); .reduce(BigDecimal.ZERO, BigDecimal::add);
stats.setTotalAmount(totalAmount); stats.setTotalAmount(totalAmount);
} });
return R.data(new ArrayList<>(orgStatsMap.values())); return R.data(new ArrayList<>(orgStatsMap.values()));
} }
private String normalizeOrganization(String organization) {
return Func.isBlank(organization) ? "未知单位" : organization;
}
private OrganizationStatsVO initOrganizationStats(String organization) {
OrganizationStatsVO vo = new OrganizationStatsVO();
vo.setOrganization(organization);
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;
}
private void upsertProjectAmount(OrganizationStatsVO stats, MartialProject project) {
OrganizationStatsVO.ProjectAmountItem item = stats.getProjectAmounts().stream()
.filter(pa -> pa.getProjectId() != null && pa.getProjectId().equals(project.getId()))
.findFirst()
.orElse(null);
if (item == null) {
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);
}
return;
}
item.setCount(item.getCount() + 1);
item.setAmount(item.getPrice().multiply(BigDecimal.valueOf(item.getCount())));
}
private void collectAthleteGender(OrganizationStatsVO stats, Set<String> uniqueIdCards, MartialAthlete athlete) {
if (athlete == null || Func.isBlank(athlete.getIdCard())) {
return;
}
if (!uniqueIdCards.add(athlete.getIdCard())) {
return;
}
stats.setAthleteCount(stats.getAthleteCount() + 1);
if (athlete.getGender() != null && athlete.getGender() == 1) {
stats.setMaleCount(stats.getMaleCount() + 1);
} else if (athlete.getGender() != null && athlete.getGender() == 2) {
stats.setFemaleCount(stats.getFemaleCount() + 1);
}
}
@PostMapping("/submit") @PostMapping("/submit")
@PreAuth(RoleConstant.HAS_ROLE_USER) @PreAuth(RoleConstant.HAS_ROLE_USER)
@Operation(summary = "提交报名", description = "提交报名订单并关联选手或集体") @Operation(summary = "提交报名", description = "提交报名订单并关联选手或集体")
@@ -295,19 +285,53 @@ public class MartialRegistrationOrderController extends BladeController {
return R.fail("比赛已结束,无法报名"); return R.fail("比赛已结束,无法报名");
} }
Long currentUserId = AuthUtil.getUserId();
if (currentUserId == null || currentUserId <= 0) {
return R.fail("请先登录");
}
// Create order entity // Create order entity
MartialRegistrationOrder order = new MartialRegistrationOrder(); MartialRegistrationOrder order = new MartialRegistrationOrder();
order.setOrderNo(dto.getOrderNo()); order.setOrderNo(dto.getOrderNo());
order.setCompetitionId(dto.getCompetitionId()); order.setCompetitionId(dto.getCompetitionId());
order.setContactPhone(dto.getContactPhone()); order.setContactPhone(dto.getContactPhone());
order.setTotalAmount(dto.getTotalAmount()); order.setTotalAmount(dto.getTotalAmount());
order.setUserId(AuthUtil.getUserId()); order.setUserId(currentUserId);
order.setUserName(AuthUtil.getUserName()); order.setUserName(AuthUtil.getUserName());
// Parse IDs // Parse IDs
List<Long> athleteIds = Func.toLongList(dto.getAthleteIds()); List<Long> athleteIds = Func.toLongList(dto.getAthleteIds());
List<Long> teamIds = Func.toLongList(dto.getTeamIds()); List<Long> teamIds = Func.toLongList(dto.getTeamIds());
List<Long> projectIds = Func.toLongList(dto.getProjectIds()); List<Long> projectIds = Func.toLongList(dto.getProjectIds());
athleteIds = athleteIds == null ? new ArrayList<>() : athleteIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
teamIds = teamIds == null ? new ArrayList<>() : teamIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
projectIds = projectIds == null ? new ArrayList<>() : projectIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
if (projectIds.isEmpty()) {
return R.fail("请选择报名项目");
}
if (athleteIds.isEmpty() && teamIds.isEmpty()) {
return R.fail("请选择报名选手或集体");
}
if (!athleteIds.isEmpty()) {
long ownAthletes = athleteService.lambdaQuery()
.in(MartialAthlete::getId, athleteIds)
.eq(MartialAthlete::getCreateUser, currentUserId)
.eq(MartialAthlete::getIsDeleted, 0)
.count();
if (ownAthletes != athleteIds.size()) {
return R.fail("选手包含非本人数据,无法报名");
}
}
if (!teamIds.isEmpty()) {
long ownTeams = teamService.lambdaQuery()
.in(MartialTeam::getId, teamIds)
.eq(MartialTeam::getCreateUser, currentUserId)
.eq(MartialTeam::getIsDeleted, 0)
.count();
if (ownTeams != teamIds.size()) {
return R.fail("集体包含非本人数据,无法报名");
}
}
// Validate gender restriction for each project // Validate gender restriction for each project
for (Long projectId : projectIds) { for (Long projectId : projectIds) {
@@ -421,7 +445,7 @@ public class MartialRegistrationOrderController extends BladeController {
teamAthlete.setOrganization(team.getTeamName()); teamAthlete.setOrganization(team.getTeamName());
teamAthlete.setRegistrationStatus(1); teamAthlete.setRegistrationStatus(1);
teamAthlete.setCompetitionStatus(0); teamAthlete.setCompetitionStatus(0);
teamAthlete.setCreateUser(AuthUtil.getUserId()); teamAthlete.setCreateUser(currentUserId);
teamAthlete.setCreateTime(new java.util.Date()); teamAthlete.setCreateTime(new java.util.Date());
teamAthlete.setTenantId("000000"); teamAthlete.setTenantId("000000");
teamAthlete.setIsDeleted(0); teamAthlete.setIsDeleted(0);
@@ -472,7 +496,7 @@ public class MartialRegistrationOrderController extends BladeController {
newRecord.setTeamName(existingAthlete.getTeamName()); newRecord.setTeamName(existingAthlete.getTeamName());
newRecord.setRegistrationStatus(1); newRecord.setRegistrationStatus(1);
newRecord.setCompetitionStatus(0); newRecord.setCompetitionStatus(0);
newRecord.setCreateUser(AuthUtil.getUserId()); newRecord.setCreateUser(currentUserId);
newRecord.setCreateTime(new java.util.Date()); newRecord.setCreateTime(new java.util.Date());
newRecord.setTenantId("000000"); newRecord.setTenantId("000000");
newRecord.setIsDeleted(0); newRecord.setIsDeleted(0);
@@ -511,4 +535,8 @@ public class MartialRegistrationOrderController extends BladeController {
return R.status(registrationOrderService.removeByIds(idList)); return R.status(registrationOrderService.removeByIds(idList));
} }
private boolean isAdminUser() {
return AuthUtil.isAdmin() || AuthUtil.isAdministrator();
}
} }
@@ -63,18 +63,61 @@ public class MiniScoringServiceImpl implements IMiniScoringService {
log.warn("评分提交失败:选手不存在,athleteId={}", athleteId); log.warn("评分提交失败:选手不存在,athleteId={}", athleteId);
return R.fail("选手信息不存在"); return R.fail("选手信息不存在");
} }
if (!competitionId.equals(athlete.getCompetitionId()) || !projectId.equals(athlete.getProjectId())) {
log.warn("评分提交失败:赛事或项目与选手不匹配,athleteId={}, payloadCompetitionId={}, athleteCompetitionId={}, payloadProjectId={}, athleteProjectId={}",
athleteId, competitionId, athlete.getCompetitionId(), projectId, athlete.getProjectId());
return R.fail("评分参数与选手信息不匹配");
}
LocalDateTime now = LocalDateTime.now();
LambdaQueryWrapper<MartialJudgeInvite> inviteQuery = new LambdaQueryWrapper<>();
inviteQuery.eq(MartialJudgeInvite::getJudgeId, judgeId);
inviteQuery.eq(MartialJudgeInvite::getCompetitionId, competitionId);
inviteQuery.eq(MartialJudgeInvite::getStatus, 1);
inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
inviteQuery.eq(venueId != null, MartialJudgeInvite::getVenueId, venueId);
inviteQuery.last("LIMIT 1");
MartialJudgeInvite invite = judgeInviteService.getOne(inviteQuery, false);
if (invite == null) {
log.warn("评分提交失败:未找到有效邀请关系,judgeId={}, competitionId={}, venueId={}", judgeId, competitionId, venueId);
return R.fail("无评分权限");
}
if (invite.getTokenExpireTime() == null || !invite.getTokenExpireTime().isAfter(now)) {
log.warn("评分提交失败:邀请token已过期,judgeId={}, inviteId={}", judgeId, invite.getId());
return R.fail("登录状态已过期");
}
if (invite.getVenueId() != null && venueId != null && !invite.getVenueId().equals(venueId)) {
log.warn("评分提交失败:场地越权,judgeId={}, inviteVenueId={}, payloadVenueId={}", judgeId, invite.getVenueId(), venueId);
return R.fail("无权访问该场地");
}
if (!hasProjectPermission(invite, projectId)) {
log.warn("评分提交失败:项目越权,judgeId={}, projectId={}, inviteProjects={}", judgeId, projectId, invite.getProjects());
return R.fail("无权为该项目评分");
}
long existingCount = scoreService.lambdaQuery()
.eq(MartialScore::getAthleteId, athleteId)
.eq(MartialScore::getProjectId, projectId)
.eq(MartialScore::getJudgeId, judgeId)
.eq(MartialScore::getIsDeleted, 0)
.count();
if (existingCount > 0) {
return R.fail("该选手已评分,请勿重复提交");
}
Long finalVenueId = venueId != null ? venueId : invite.getVenueId();
MartialScore score = new MartialScore(); MartialScore score = new MartialScore();
score.setAthleteId(athleteId); score.setAthleteId(athleteId);
score.setJudgeId(judgeId); score.setJudgeId(judgeId);
score.setScore(dto.getScore()); score.setScore(dto.getScore());
score.setProjectId(projectId); score.setProjectId(athlete.getProjectId());
score.setCompetitionId(competitionId); score.setCompetitionId(athlete.getCompetitionId());
score.setVenueId(venueId); score.setVenueId(finalVenueId);
score.setScheduleId(scheduleId); score.setScheduleId(scheduleId);
score.setNote(dto.getNote()); score.setNote(dto.getNote());
score.setScoreTime(LocalDateTime.now()); score.setScoreTime(now);
if (dto.getDeductions() != null && !dto.getDeductions().isEmpty()) { if (dto.getDeductions() != null && !dto.getDeductions().isEmpty()) {
List<Long> deductionIds = dto.getDeductions().stream() List<Long> deductionIds = dto.getDeductions().stream()
@@ -90,7 +133,7 @@ public class MiniScoringServiceImpl implements IMiniScoringService {
if (success) { if (success) {
if (athleteId != null && projectId != null) { if (athleteId != null && projectId != null) {
updateAthleteTotalScore(athleteId, projectId, venueId); updateAthleteTotalScore(athleteId, projectId, finalVenueId);
} }
} }
@@ -211,4 +254,41 @@ public class MiniScoringServiceImpl implements IMiniScoringService {
return null; return null;
} }
} }
private boolean hasProjectPermission(MartialJudgeInvite invite, Long projectId) {
if (projectId == null) {
return false;
}
Integer refereeType = invite.getRefereeType();
String role = invite.getRole();
if ((refereeType != null && refereeType == 3) || "general_judge".equals(role) || "general".equals(role)) {
return true;
}
String projects = invite.getProjects();
if (projects == null || projects.trim().isEmpty()) {
return true;
}
List<Long> allowedProjects = parseProjectIds(projects);
return allowedProjects.isEmpty() || allowedProjects.contains(projectId);
}
private List<Long> parseProjectIds(String rawProjects) {
if (rawProjects == null || rawProjects.trim().isEmpty()) {
return new ArrayList<>();
}
String value = rawProjects.trim();
if (value.startsWith("[") && value.endsWith("]")) {
value = value.substring(1, value.length() - 1);
}
String[] parts = value.split(",");
List<Long> ids = new ArrayList<>();
for (String part : parts) {
Long id = parseLong(part);
if (id != null && !ids.contains(id)) {
ids.add(id);
}
}
return ids;
}
} }