|
|
@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.extension.toolkit.Db;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.dromara.common.core.utils.StringUtils;
|
|
|
+import org.dromara.common.redis.utils.RedisUtils;
|
|
|
import org.dromara.system.domain.*;
|
|
|
import org.dromara.system.domain.bo.*;
|
|
|
import org.dromara.system.domain.constant.ProjectClassification;
|
|
|
@@ -22,13 +23,18 @@ import org.dromara.system.service.IGameScoreService;
|
|
|
import org.dromara.system.service.IGameRankGroupService;
|
|
|
import org.dromara.system.service.ISysDictTypeService;
|
|
|
import org.dromara.system.service.app.IToClientService;
|
|
|
+import org.redisson.api.RLock;
|
|
|
import org.springframework.scheduling.annotation.Async;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
+import org.dromara.common.core.utils.SpringUtils;
|
|
|
+import org.springframework.transaction.support.TransactionSynchronization;
|
|
|
+import org.springframework.transaction.support.TransactionSynchronizationManager;
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
import java.math.RoundingMode;
|
|
|
import java.util.*;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
@Slf4j
|
|
|
@@ -64,11 +70,10 @@ public class ToClientServiceImpl implements IToClientService {
|
|
|
|
|
|
/** 登录成功后,返回裁判相关信息 */
|
|
|
List<Long> projectIds = JSONUtil.toList(referee.getProjectList(), Long.class);
|
|
|
- if (CollectionUtils.isNotEmpty(projectIds)){
|
|
|
+ if (CollectionUtils.isNotEmpty(projectIds)) {
|
|
|
List<GameEventProject> list = Db.list(Wrappers.lambdaQuery(GameEventProject.class)
|
|
|
- .select(GameEventProject::getProjectName)
|
|
|
- .in(GameEventProject::getProjectId, projectIds)
|
|
|
- );
|
|
|
+ .select(GameEventProject::getProjectName)
|
|
|
+ .in(GameEventProject::getProjectId, projectIds));
|
|
|
List<String> names = list.stream().map(GameEventProject::getProjectName).toList();
|
|
|
vo.getRefereeInfo().setProjectNames(names);
|
|
|
}
|
|
|
@@ -88,8 +93,7 @@ public class ToClientServiceImpl implements IToClientService {
|
|
|
// 1. 获取或创建赛事
|
|
|
GameEvent event = gameEventMapper.selectOne(Wrappers.lambdaQuery(GameEvent.class)
|
|
|
.eq(bo.getId() != null, GameEvent::getEventId, bo.getId())
|
|
|
- .eq(GameEvent::getEventCode, bo.getBianhao())
|
|
|
- );
|
|
|
+ .eq(GameEvent::getEventCode, bo.getBianhao()));
|
|
|
if (event == null) {
|
|
|
event = new GameEvent();
|
|
|
event.setEventCode(bo.getBianhao());
|
|
|
@@ -180,40 +184,10 @@ public class ToClientServiceImpl implements IToClientService {
|
|
|
if (event == null) {
|
|
|
throw new RuntimeException("赛事不存在");
|
|
|
}
|
|
|
- // 异步执行删除操作
|
|
|
- asyncRemoveEventData(eventId);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 异步删除赛事关联数据
|
|
|
- */
|
|
|
- @Async
|
|
|
- @Transactional(rollbackFor = Exception.class)
|
|
|
- public void asyncRemoveEventData(Long eventId) {
|
|
|
- log.info("开始异步删除赛事数据,eventId: {}", eventId);
|
|
|
- long startTime = System.currentTimeMillis();
|
|
|
- try {
|
|
|
- // 删除关联的组别
|
|
|
- Db.remove(Wrappers.lambdaQuery(GameRankGroup.class)
|
|
|
- .eq(GameRankGroup::getEventId, eventId));
|
|
|
- // 删除关联的配置信息
|
|
|
- Db.remove(Wrappers.lambdaQuery(GameEventConfig.class)
|
|
|
- .eq(GameEventConfig::getEventId, eventId));
|
|
|
- // 删除关联的成绩信息
|
|
|
- Db.remove(Wrappers.lambdaQuery(GameScore.class)
|
|
|
- .eq(GameScore::getEventId, eventId));
|
|
|
- // 删除关联的项目
|
|
|
- int projectCount = projectMapper.delete(Wrappers.lambdaQuery(GameEventProject.class)
|
|
|
- .eq(GameEventProject::getEventId, eventId));
|
|
|
- log.info("删除项目完成,eventId: {}, 删除数量: {}", eventId, projectCount);
|
|
|
- // 删除赛事
|
|
|
- gameEventMapper.deleteById(eventId);
|
|
|
- long endTime = System.currentTimeMillis();
|
|
|
- log.info("赛事数据删除完成,eventId: {}, 耗时: {}ms", eventId, (endTime - startTime));
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("异步删除赛事数据失败,eventId: {}", eventId, e);
|
|
|
- throw new RuntimeException("删除赛事数据失败: " + e.getMessage());
|
|
|
- }
|
|
|
+ // 仅删除主表赛事。对于关联的从表数据(如成绩、明细等),由于业务查询均以赛事存在为前提,
|
|
|
+ // 赛事删除后关联数据已天然不可见。直接删除主表即可规避大量从表逻辑删除引发的锁表和性能瓶颈。
|
|
|
+ gameEventMapper.deleteById(eventId);
|
|
|
+ log.info("赛事主表删除成功,eventId: {}", eventId);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -222,11 +196,10 @@ public class ToClientServiceImpl implements IToClientService {
|
|
|
@Override
|
|
|
public List<GameAppEvent> getEventList(Long refereeId) {
|
|
|
List<GameEvent> res = gameEventMapper.selectList(Wrappers.lambdaQuery(GameEvent.class)
|
|
|
- .orderByDesc(GameEvent::getCreateTime)
|
|
|
+ .orderByDesc(GameEvent::getCreateTime)
|
|
|
.apply("event_id in (select event_id from game_referee where referee_id = {0})", refereeId)
|
|
|
- .select(GameEvent::getEventId, GameEvent::getEventCode, GameEvent::getEventName,
|
|
|
- GameEvent::getCreateTime)
|
|
|
- );
|
|
|
+ .select(GameEvent::getEventId, GameEvent::getEventCode, GameEvent::getEventName,
|
|
|
+ GameEvent::getCreateTime));
|
|
|
return res.isEmpty() ? new ArrayList<>() : res.stream().map(e -> {
|
|
|
GameAppEvent appEvent = BeanUtil.copyProperties(e, GameAppEvent.class);
|
|
|
appEvent.setId(e.getEventId());
|
|
|
@@ -242,25 +215,23 @@ public class ToClientServiceImpl implements IToClientService {
|
|
|
@Override
|
|
|
public List<GameAppProject> getProjectList(Long eventId, Long refereeId) {
|
|
|
String list = Db.getObj(Wrappers.lambdaQuery(GameReferee.class)
|
|
|
- .eq(GameReferee::getRefereeId, refereeId)
|
|
|
- .select(GameReferee::getProjectList), GameReferee::getProjectList
|
|
|
- );
|
|
|
+ .eq(GameReferee::getRefereeId, refereeId)
|
|
|
+ .select(GameReferee::getProjectList), GameReferee::getProjectList);
|
|
|
List<Long> projectIds = JSONUtil.toList(list, Long.class);
|
|
|
- if (projectIds.isEmpty()){
|
|
|
+ if (projectIds.isEmpty()) {
|
|
|
return new ArrayList<>();
|
|
|
}
|
|
|
List<SysDictDataVo> dictDataVos = dictService.selectDictDataByType("game_project_type");
|
|
|
HashMap<String, String> typeMaps = new HashMap<>();
|
|
|
- if (!dictDataVos.isEmpty()){
|
|
|
+ if (!dictDataVos.isEmpty()) {
|
|
|
dictDataVos.forEach(d -> typeMaps.put(d.getDictValue(), d.getDictLabel()));
|
|
|
}
|
|
|
List<GameEventProject> res = projectMapper.selectList(Wrappers.lambdaQuery(GameEventProject.class)
|
|
|
- .eq(GameEventProject::getEventId, eventId)
|
|
|
+ .eq(GameEventProject::getEventId, eventId)
|
|
|
.in(GameEventProject::getProjectId, projectIds)
|
|
|
- .select(GameEventProject::getProjectId, GameEventProject::getProjectType,
|
|
|
- GameEventProject::getClassification, GameEventProject::getProjectName,
|
|
|
- GameEventProject::getStatus)
|
|
|
- );
|
|
|
+ .select(GameEventProject::getProjectId, GameEventProject::getProjectType,
|
|
|
+ GameEventProject::getClassification, GameEventProject::getProjectName,
|
|
|
+ GameEventProject::getStatus));
|
|
|
return res.isEmpty() ? new ArrayList<>() : res.stream().map(p -> {
|
|
|
GameAppProject appProject = BeanUtil.copyProperties(p, GameAppProject.class);
|
|
|
appProject.setId(p.getEventId());
|
|
|
@@ -290,8 +261,7 @@ public class ToClientServiceImpl implements IToClientService {
|
|
|
// 批量查询字典数据,避免多次调用 Redis/MySQL 交互
|
|
|
Map<String, List<SysDictDataVo>> dictMap = dictService.selectDictDataByTypes(List.of(
|
|
|
"game_project_type", "game_score_type", "game_order_type",
|
|
|
- "sys_group_sex", "game_stage", "game_round"
|
|
|
- ));
|
|
|
+ "sys_group_sex", "game_stage", "game_round"));
|
|
|
|
|
|
vo.setType(getDictLabelFromMap(dictMap, "game_project_type", vo.getType()));
|
|
|
vo.setChengjiType(getDictLabelFromMap(dictMap, "game_score_type", vo.getChengjiType()));
|
|
|
@@ -349,7 +319,7 @@ public class ToClientServiceImpl implements IToClientService {
|
|
|
if (StringUtils.isNotBlank(item.getScore()) && vo.getChengjiType().contains("计时")) {
|
|
|
item.setScore(convertDecimalToTimeScore(item.getScore(), vo.getGuize()));
|
|
|
}
|
|
|
- if (StringUtils.isNotBlank(item.getYongshi())){
|
|
|
+ if (StringUtils.isNotBlank(item.getYongshi())) {
|
|
|
item.setYongshi(convertDecimalToTimeScore(item.getYongshi(), "0"));
|
|
|
}
|
|
|
// 格式化明细成绩
|
|
|
@@ -462,12 +432,13 @@ public class ToClientServiceImpl implements IToClientService {
|
|
|
|
|
|
// 校验成绩是否为空
|
|
|
for (ScoreSubmitItemBo item : items) {
|
|
|
- String displayName = StringUtils.isNotBlank(item.getName()) ? item.getName() :
|
|
|
- (StringUtils.isNotBlank(item.getNum()) ? item.getNum() : "未知选手");
|
|
|
+ String displayName = StringUtils.isNotBlank(item.getName()) ? item.getName()
|
|
|
+ : (StringUtils.isNotBlank(item.getNum()) ? item.getNum() : "未知选手");
|
|
|
if (CollectionUtils.isNotEmpty(item.getChengji())) {
|
|
|
for (ScoreSubmitDetailBo detail : item.getChengji()) {
|
|
|
if (detail.getNum() == null) {
|
|
|
- String attemptStr = detail.getAttemptIndex() != null ? "第 " + detail.getAttemptIndex() + " 轮" : "明细";
|
|
|
+ String attemptStr = detail.getAttemptIndex() != null ? "第 " + detail.getAttemptIndex() + " 轮"
|
|
|
+ : "明细";
|
|
|
throw new RuntimeException("提交失败:" + displayName + " 的" + attemptStr + "成绩不能为空");
|
|
|
}
|
|
|
}
|
|
|
@@ -599,7 +570,22 @@ public class ToClientServiceImpl implements IToClientService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- gameScoreService.updateScoreAndRecalculate(scoreBo);
|
|
|
+ // 仅写入数据库,暂不重新计算排名
|
|
|
+ gameScoreService.saveOrUpdateScoreOnly(scoreBo);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 注册事务同步器:在当前事务成功提交后,触发异步单次排名计算
|
|
|
+ if (TransactionSynchronizationManager.isActualTransactionActive()) {
|
|
|
+ TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
|
|
+ @Override
|
|
|
+ public void afterCommit() {
|
|
|
+ // 事务提交后,调用异步排名计算
|
|
|
+ SpringUtils.getBean(IToClientService.class).asyncRecalculateRankings(eventId, projectId);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // 如果当前没有活跃事务,则直接同步计算(预防非事务环境)
|
|
|
+ gameScoreService.recalculateRankingsAndPoints(eventId, projectId);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -642,20 +628,22 @@ public class ToClientServiceImpl implements IToClientService {
|
|
|
|
|
|
list.forEach(item -> item.setChengji(detailMap.get(item.getTeamId())));
|
|
|
} else {
|
|
|
- // 个人项目:按 athleteId 分组
|
|
|
+ // 个人项目:优先按 athleteId (即 detailVo.id) 分组,同时生成按 scoreId 的分组以便匹配
|
|
|
detailMap = allDetails.stream()
|
|
|
.filter(d -> d.getAthleteId() != null)
|
|
|
.map(this::convertToDetailVo)
|
|
|
.collect(Collectors.groupingBy(ScoreSheetDetailVo::getId));
|
|
|
|
|
|
+ Map<Long, List<ScoreSheetDetailVo>> scoreDetailMap = allDetails.stream()
|
|
|
+ .filter(d -> d.getScoreId() != null)
|
|
|
+ .map(this::convertToDetailVo)
|
|
|
+ .collect(Collectors.groupingBy(ScoreSheetDetailVo::getScoreId));
|
|
|
+
|
|
|
list.forEach(item -> {
|
|
|
List<ScoreSheetDetailVo> details = detailMap.get(item.getId());
|
|
|
- if (details == null && item.getScoreId() != null) {
|
|
|
- // 兜底:按 scoreId 匹配
|
|
|
- details = allDetails.stream()
|
|
|
- .filter(d -> Objects.equals(d.getScoreId(), item.getScoreId()))
|
|
|
- .map(this::convertToDetailVo)
|
|
|
- .toList();
|
|
|
+ if (CollectionUtils.isEmpty(details) && item.getScoreId() != null) {
|
|
|
+ // 兜底:处理部分仅关联 scoreId 的旧数据
|
|
|
+ details = scoreDetailMap.get(item.getScoreId());
|
|
|
}
|
|
|
item.setChengji(details);
|
|
|
});
|
|
|
@@ -830,4 +818,29 @@ public class ToClientServiceImpl implements IToClientService {
|
|
|
public List<SysDictDataVo> selectDictDataByType(String dictType) {
|
|
|
return dictService.selectDictDataByType(dictType);
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 异步重新计算项目排名和积分
|
|
|
+ */
|
|
|
+ @Async
|
|
|
+ @Override
|
|
|
+ public void asyncRecalculateRankings(Long eventId, Long projectId) {
|
|
|
+ String lockKey = "project:rank:lock:" + projectId;
|
|
|
+ RLock lock = RedisUtils.getClient().getLock(lockKey);
|
|
|
+ try {
|
|
|
+ // 尝试获取锁,最多等待 10 秒,上锁后 30 秒自动解锁(防死锁)
|
|
|
+ if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
|
|
|
+ gameScoreService.recalculateRankingsAndPoints(eventId, projectId);
|
|
|
+ } else {
|
|
|
+ log.warn("未能获取项目 {} 的排名计算锁,跳过或放弃本次计算", projectId);
|
|
|
+ }
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ log.error("获取项目 {} 排名锁被中断", projectId, e);
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ } finally {
|
|
|
+ if (lock.isHeldByCurrentThread()) {
|
|
|
+ lock.unlock();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|