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("请先登录");
}
MartialAthlete target;
if (athlete.getId() != null) {
MartialAthlete existing = athleteService.getById(athlete.getId());
if (existing == null) {
@@ -127,14 +128,21 @@ public class MartialAthleteController extends BladeController {
if (!isAdminUser() && !userId.equals(existing.getCreateUser())) {
return R.fail("无权限修改该选手");
}
athlete.setCreateUser(existing.getCreateUser());
target = existing;
} else {
athlete.setCreateUser(userId);
target = new MartialAthlete();
target.setCreateUser(userId);
target.setRegistrationStatus(0);
target.setCompetitionStatus(0);
}
log.info("=== 提交选手 === userId: {}, playerName: {}", userId, athlete.getPlayerName());
athlete.setUpdateUser(userId);
return R.status(athleteService.saveOrUpdate(athlete));
applyEditableFields(target, athlete);
log.info("=== 提交选手 === userId: {}, playerName: {}", userId, target.getPlayerName());
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();
}
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");
String template = Files.readString(templatePath, StandardCharsets.UTF_8);
String html = template
.replace("${playerName}", certificate.getPlayerName())
.replace("${competitionName}", certificate.getCompetitionName())
.replace("${projectName}", certificate.getProjectName())
.replace("${medalName}", certificate.getMedalName())
.replace("${medalClass}", certificate.getMedalClass())
.replace("${organization}", certificate.getOrganization())
.replace("${issueDate}", certificate.getIssueDate());
.replace("${playerName}", escapeHtml(certificate.getPlayerName()))
.replace("${competitionName}", escapeHtml(certificate.getCompetitionName()))
.replace("${projectName}", escapeHtml(certificate.getProjectName()))
.replace("${medalName}", escapeHtml(certificate.getMedalName()))
.replace("${medalClass}", sanitizeCssClass(certificate.getMedalClass()))
.replace("${organization}", escapeHtml(certificate.getOrganization()))
.replace("${issueDate}", escapeHtml(certificate.getIssueDate()));
response.setContentType("text/html;charset=UTF-8");
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")
@Operation(summary = "批量生成证书数据", description = "批量获取项目获奖选手的证书数据")
public R<List<CertificateVO>> batchGenerateCertificates(@RequestParam Long projectId) {
@@ -110,15 +110,50 @@ public class MartialMiniController extends BladeController {
return R.fail("请求体不能为空");
}
JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken);
if (authContext == null || authContext.getJudgeId() == null) {
if (authContext == null || authContext.getJudgeId() == null || !authContext.isInviteTokenAuth()) {
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());
if (payloadJudgeId != null && !payloadJudgeId.equals(authContext.getJudgeId())) {
log.warn("评分提交身份字段不匹配,已忽略请求体judgeIdpayloadJudgeId={}, authJudgeId={}", payloadJudgeId, authContext.getJudgeId());
}
// 向后兼容:保留 judgeId 字段,但强制覆盖为 token 身份
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);
}
@@ -258,7 +293,7 @@ public class MartialMiniController extends BladeController {
if (accessToken != null) {
MartialJudgeInvite invite = findValidInviteByAccessToken(accessToken);
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;
}
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")
@Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)")
public R<IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO>> getAthletes(
@RequestParam Long judgeId,
@RequestParam Integer refereeType,
@RequestHeader(value = "Authorization", required = false) String authorization,
@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 orderId,
@RequestParam(required = false) Long venueId,
@@ -328,22 +436,47 @@ public class MartialMiniController extends BladeController {
@RequestParam(defaultValue = "1") Integer current,
@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. 优先从编排表查询选手(按出场顺序)
List<MartialAthlete> athletes = getAthletesFromSchedule(competitionId, projectId, venueId);
// 如果编排表没有数据,回退到原始选手表查询
if (athletes.isEmpty()) {
LambdaQueryWrapper<MartialAthlete> athleteQuery = new LambdaQueryWrapper<>();
athleteQuery.eq(MartialAthlete::getIsDeleted, 0);
if (competitionId != null) {
athleteQuery.eq(MartialAthlete::getCompetitionId, competitionId);
}
athleteQuery.eq(MartialAthlete::getCompetitionId, competitionId);
if (projectId != null) {
athleteQuery.eq(MartialAthlete::getProjectId, projectId);
}
athleteQuery.orderByAsc(MartialAthlete::getOrderNum);
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列表
List<Long> chiefJudgeIds = getChiefJudgeIds(venueId);
@@ -351,6 +484,7 @@ public class MartialMiniController extends BladeController {
// 3. 获取所有评分记录(排除主裁判的评分)
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getIsDeleted, 0);
scoreQuery.eq(MartialScore::getCompetitionId, competitionId);
if (projectId != null) {
scoreQuery.eq(MartialScore::getProjectId, projectId);
}
@@ -373,18 +507,9 @@ public class MartialMiniController extends BladeController {
// 5. 根据裁判类型处理选手列表
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList;
if (refereeType == 1) {
// 主裁判:返回所有选手,前端根据totalScore判断是否显示修改按钮
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());
}
filteredList = athletes.stream()
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), currentJudgeId, requiredJudgeCount))
.collect(Collectors.toList());
// 6. 手动分页
int total = filteredList.size();
@@ -503,7 +628,22 @@ public class MartialMiniController extends BladeController {
*/
@GetMapping("/score/detail/{athleteId}")
@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);
return R.data(detail);
}
@@ -519,7 +659,7 @@ public class MartialMiniController extends BladeController {
@Valid @RequestBody MiniScoreModifyDTO dto
) {
JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken);
if (authContext == null || authContext.getJudgeId() == null) {
if (authContext == null || authContext.getJudgeId() == null || !authContext.isInviteTokenAuth()) {
return R.fail("登录状态无效或已过期");
}
if (!authContext.isChiefJudge()) {
@@ -530,6 +670,16 @@ public class MartialMiniController extends BladeController {
if (dto.getModifierId() != null && !dto.getModifierId().equals(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 身份为准
dto.setModifierId(authContext.getJudgeId());
boolean success = scoreService.modifyScoreByAdmin(dto);
@@ -830,7 +980,7 @@ public class MartialMiniController extends BladeController {
return R.fail("参数错误");
}
JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken);
if (authContext == null || authContext.getJudgeId() == null) {
if (authContext == null || authContext.getJudgeId() == null || !authContext.isInviteTokenAuth()) {
return R.fail("登录状态无效或已过期");
}
if (!authContext.isChiefJudge()) {
@@ -847,6 +997,16 @@ public class MartialMiniController extends BladeController {
log.warn("主裁判确认身份字段不匹配,已忽略请求体chiefJudgeIdpayloadChiefJudgeId={}, authJudgeId={}",
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());
return success ? R.success("确认成功") : R.fail("确认失败");
}
@@ -865,7 +1025,7 @@ public class MartialMiniController extends BladeController {
return R.fail("参数错误");
}
JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken);
if (authContext == null || authContext.getJudgeId() == null) {
if (authContext == null || authContext.getJudgeId() == null || !authContext.isInviteTokenAuth()) {
return R.fail("登录状态无效或已过期");
}
if (!authContext.isGeneralJudge()) {
@@ -882,6 +1042,13 @@ public class MartialMiniController extends BladeController {
log.warn("总裁确认身份字段不匹配,已忽略请求体generalJudgeIdpayloadGeneralJudgeId={}, authJudgeId={}",
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());
return success ? R.success("确认成功") : R.fail("确认失败");
}
@@ -891,7 +1058,21 @@ public class MartialMiniController extends BladeController {
*/
@GetMapping("/chief/pending")
@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);
return R.data(list);
}
@@ -901,7 +1082,21 @@ public class MartialMiniController extends BladeController {
*/
@GetMapping("/general/pending")
@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);
return R.data(list);
}
@@ -911,7 +1106,21 @@ public class MartialMiniController extends BladeController {
*/
@GetMapping("/general/venues")
@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<>();
wrapper.eq(MartialVenue::getCompetitionId, competitionId);
wrapper.eq(MartialVenue::getIsDeleted, 0);
@@ -925,7 +1134,21 @@ public class MartialMiniController extends BladeController {
*/
@GetMapping("/general/confirmed")
@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);
return R.data(list);
}
@@ -1115,15 +1338,31 @@ public class MartialMiniController extends BladeController {
private final Long judgeId;
private final String role;
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.role = role;
this.refereeType = refereeType;
this.competitionId = competitionId;
this.venueId = venueId;
this.projectIds = projectIds;
this.inviteTokenAuth = inviteTokenAuth;
}
static JudgeAuthContext fromInvite(MartialJudgeInvite invite) {
return new JudgeAuthContext(invite.getJudgeId(), invite.getRole(), invite.getRefereeType());
static JudgeAuthContext fromInvite(MartialJudgeInvite invite, List<Long> projectIds) {
return new JudgeAuthContext(
invite.getJudgeId(),
invite.getRole(),
invite.getRefereeType(),
invite.getCompetitionId(),
invite.getVenueId(),
projectIds == null ? new ArrayList<>() : projectIds,
true
);
}
static JudgeAuthContext fromJudge(MartialJudge judge) {
@@ -1136,7 +1375,7 @@ public class MartialMiniController extends BladeController {
} else {
role = "judge";
}
return new JudgeAuthContext(judge.getId(), role, refereeType);
return new JudgeAuthContext(judge.getId(), role, refereeType, null, null, new ArrayList<>(), false);
}
Long getJudgeId() {
@@ -1151,6 +1390,22 @@ public class MartialMiniController extends BladeController {
return refereeType;
}
Long getCompetitionId() {
return competitionId;
}
Long getVenueId() {
return venueId;
}
List<Long> getProjectIds() {
return projectIds;
}
boolean isInviteTokenAuth() {
return inviteTokenAuth;
}
boolean isChiefJudge() {
return "chief_judge".equals(role) || (refereeType != null && refereeType == 1);
}
@@ -57,6 +57,17 @@ public class MartialRegistrationOrderController extends BladeController {
@PreAuth(RoleConstant.HAS_ROLE_USER)
@Operation(summary = "详情", description = "传入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));
}
@@ -78,188 +89,167 @@ public class MartialRegistrationOrderController extends BladeController {
@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);
.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()
Map<Long, MartialProject> projectMap = projectIds.isEmpty() ? new HashMap<>() :
projectService.listByIds(projectIds).stream()
.collect(Collectors.toMap(MartialProject::getId, p -> p, (a, b) -> a));
Set<String> teamNames = athletes.stream()
.filter(a -> {
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 -> {
// 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)
.map(MartialAthlete::getTeamName)
.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));
Map<String, MartialTeam> teamByName = new HashMap<>();
Map<Long, List<MartialTeamMember>> teamMembersByTeamId = new HashMap<>();
Map<Long, MartialAthlete> memberAthleteById = new HashMap<>();
if (!teamNames.isEmpty()) {
LambdaQueryWrapper<MartialTeam> teamWrapper = new LambdaQueryWrapper<>();
teamWrapper.in(MartialTeam::getTeamName, teamNames)
.eq(MartialTeam::getIsDeleted, 0);
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, Set<String>> orgUniqueIdCards = new HashMap<>();
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;
});
String org = normalizeOrganization(athlete.getOrganization());
OrganizationStatsVO stats = orgStatsMap.computeIfAbsent(org, this::initOrganizationStats);
Set<String> uniqueIdCards = orgUniqueIdCards.computeIfAbsent(org, k -> new HashSet<>());
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())));
});
if (project == null) {
continue;
}
upsertProjectAmount(stats, project);
if (project.getType() == null || project.getType() == 1) {
collectAthleteGender(stats, uniqueIdCards, athlete);
continue;
}
if (Func.isBlank(athlete.getTeamName())) {
continue;
}
MartialTeam team = teamByName.get(athlete.getTeamName());
if (team == null) {
continue;
}
List<MartialTeamMember> members = teamMembersByTeamId.getOrDefault(team.getId(), new ArrayList<>());
for (MartialTeamMember member : members) {
MartialAthlete memberAthlete = memberAthleteById.get(member.getAthleteId());
collectAthleteGender(stats, uniqueIdCards, memberAthlete);
}
}
// 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
orgStatsMap.values().forEach(stats -> {
BigDecimal totalAmount = stats.getProjectAmounts().stream()
.map(OrganizationStatsVO.ProjectAmountItem::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
stats.setTotalAmount(totalAmount);
}
});
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")
@PreAuth(RoleConstant.HAS_ROLE_USER)
@Operation(summary = "提交报名", description = "提交报名订单并关联选手或集体")
@@ -294,6 +284,11 @@ public class MartialRegistrationOrderController extends BladeController {
if (competition.getCompetitionEndTime() != null && now.isAfter(competition.getCompetitionEndTime())) {
return R.fail("比赛已结束,无法报名");
}
Long currentUserId = AuthUtil.getUserId();
if (currentUserId == null || currentUserId <= 0) {
return R.fail("请先登录");
}
// Create order entity
MartialRegistrationOrder order = new MartialRegistrationOrder();
@@ -301,13 +296,42 @@ public class MartialRegistrationOrderController extends BladeController {
order.setCompetitionId(dto.getCompetitionId());
order.setContactPhone(dto.getContactPhone());
order.setTotalAmount(dto.getTotalAmount());
order.setUserId(AuthUtil.getUserId());
order.setUserId(currentUserId);
order.setUserName(AuthUtil.getUserName());
// Parse IDs
List<Long> athleteIds = Func.toLongList(dto.getAthleteIds());
List<Long> teamIds = Func.toLongList(dto.getTeamIds());
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
for (Long projectId : projectIds) {
@@ -421,7 +445,7 @@ public class MartialRegistrationOrderController extends BladeController {
teamAthlete.setOrganization(team.getTeamName());
teamAthlete.setRegistrationStatus(1);
teamAthlete.setCompetitionStatus(0);
teamAthlete.setCreateUser(AuthUtil.getUserId());
teamAthlete.setCreateUser(currentUserId);
teamAthlete.setCreateTime(new java.util.Date());
teamAthlete.setTenantId("000000");
teamAthlete.setIsDeleted(0);
@@ -472,7 +496,7 @@ public class MartialRegistrationOrderController extends BladeController {
newRecord.setTeamName(existingAthlete.getTeamName());
newRecord.setRegistrationStatus(1);
newRecord.setCompetitionStatus(0);
newRecord.setCreateUser(AuthUtil.getUserId());
newRecord.setCreateUser(currentUserId);
newRecord.setCreateTime(new java.util.Date());
newRecord.setTenantId("000000");
newRecord.setIsDeleted(0);
@@ -511,4 +535,8 @@ public class MartialRegistrationOrderController extends BladeController {
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);
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();
score.setAthleteId(athleteId);
score.setJudgeId(judgeId);
score.setScore(dto.getScore());
score.setProjectId(projectId);
score.setCompetitionId(competitionId);
score.setVenueId(venueId);
score.setProjectId(athlete.getProjectId());
score.setCompetitionId(athlete.getCompetitionId());
score.setVenueId(finalVenueId);
score.setScheduleId(scheduleId);
score.setNote(dto.getNote());
score.setScoreTime(LocalDateTime.now());
score.setScoreTime(now);
if (dto.getDeductions() != null && !dto.getDeductions().isEmpty()) {
List<Long> deductionIds = dto.getDeductions().stream()
@@ -90,7 +133,7 @@ public class MiniScoringServiceImpl implements IMiniScoringService {
if (success) {
if (athleteId != null && projectId != null) {
updateAthleteTotalScore(athleteId, projectId, venueId);
updateAthleteTotalScore(athleteId, projectId, finalVenueId);
}
}
@@ -211,4 +254,41 @@ public class MiniScoringServiceImpl implements IMiniScoringService {
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;
}
}