|
@@ -14,16 +14,14 @@ import org.apache.poi.ss.util.CellRangeAddress;
|
|
|
import org.apache.poi.xssf.usermodel.XSSFSheet;
|
|
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
|
|
import org.dromara.common.core.utils.ObjectUtils;
|
|
|
-import org.dromara.system.domain.GameTeam;
|
|
|
+import org.dromara.common.core.utils.StringUtils;
|
|
|
+import org.dromara.system.domain.*;
|
|
|
import org.dromara.system.domain.bo.EnrollBo;
|
|
|
import org.dromara.system.domain.bo.GameAthleteBo;
|
|
|
import org.dromara.system.domain.bo.GameTeamBo;
|
|
|
-import org.dromara.system.domain.vo.EnrollProjectVo;
|
|
|
-import org.dromara.system.domain.vo.GameTeamVo;
|
|
|
-import org.dromara.system.service.IEnrollService;
|
|
|
-import org.dromara.system.service.IGameAthleteService;
|
|
|
-import org.dromara.system.service.IGameEventProjectService;
|
|
|
-import org.dromara.system.service.IGameTeamService;
|
|
|
+import org.dromara.system.domain.vo.*;
|
|
|
+import org.dromara.system.service.*;
|
|
|
+import org.dromara.system.utils.GenderDictUtils;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
@@ -44,6 +42,7 @@ public class IEnrollServiceImpl implements IEnrollService {
|
|
|
private final IGameEventProjectService gameEventProjectService;
|
|
|
private final IGameTeamService gameTeamService;
|
|
|
private final IGameAthleteService gameAthleteService;
|
|
|
+ private final IGameEventService gameEventService;
|
|
|
|
|
|
/**
|
|
|
* 使用poi生成报名表模板
|
|
@@ -68,7 +67,7 @@ public class IEnrollServiceImpl implements IEnrollService {
|
|
|
|
|
|
if (!projectMap.isEmpty()) {
|
|
|
// 3. 渲染分类
|
|
|
- int currentColumnIndex = 6;
|
|
|
+ int currentColumnIndex = 7;
|
|
|
Row row = sheet.getRow(1);
|
|
|
if (row == null) {
|
|
|
row = sheet.createRow(1);
|
|
@@ -115,7 +114,7 @@ public class IEnrollServiceImpl implements IEnrollService {
|
|
|
}
|
|
|
|
|
|
// 4. 渲染项目(在第4行,索引为3)
|
|
|
- currentColumnIndex = 6;
|
|
|
+ currentColumnIndex = 7;
|
|
|
Row projectRow = sheet.getRow(2);
|
|
|
if (projectRow == null) {
|
|
|
projectRow = sheet.createRow(2);
|
|
@@ -286,8 +285,8 @@ public class IEnrollServiceImpl implements IEnrollService {
|
|
|
int lastCellIndex = findValidLastColumnIndex(headerRow);
|
|
|
List<String> projectNames = new ArrayList<>();
|
|
|
|
|
|
- // 从第6列(F列,索引5)开始收集项目名称
|
|
|
- for (int i = 6; i < lastCellIndex; i++) {
|
|
|
+ // 从第7列(G列,索引6)开始收集项目名称(因为新增了号码列)
|
|
|
+ for (int i = 7; i < lastCellIndex; i++) {
|
|
|
Cell cell = headerRow.getCell(i);
|
|
|
if (cell != null && cell.getStringCellValue() != null && !cell.getStringCellValue().trim().isEmpty()) {
|
|
|
projectNames.add(cell.getStringCellValue().trim());
|
|
@@ -307,46 +306,54 @@ public class IEnrollServiceImpl implements IEnrollService {
|
|
|
EnrollProjectVo enroll = new EnrollProjectVo();
|
|
|
Map<String, Boolean> selections = new LinkedHashMap<>(); // 保持顺序
|
|
|
|
|
|
- // A列:姓名
|
|
|
- Cell nameCell = row.getCell(0);
|
|
|
+ // A列:号码
|
|
|
+ Cell codeCell = row.getCell(0);
|
|
|
+ if (codeCell != null) {
|
|
|
+ enroll.setAthleteCode(getCellValueAsString(codeCell));
|
|
|
+ }
|
|
|
+
|
|
|
+ // B列:姓名
|
|
|
+ Cell nameCell = row.getCell(1);
|
|
|
if (nameCell != null) {
|
|
|
enroll.setName(getCellValueAsString(nameCell));
|
|
|
}
|
|
|
|
|
|
- // B列:性别
|
|
|
- Cell sexCell = row.getCell(1);
|
|
|
+ // C列:性别
|
|
|
+ Cell sexCell = row.getCell(2);
|
|
|
if (sexCell != null) {
|
|
|
- enroll.setSex(getCellValueAsString(sexCell));
|
|
|
+ String sexText = getCellValueAsString(sexCell);
|
|
|
+ String sexValue = GenderDictUtils.convertGenderToDictValue(sexText);
|
|
|
+ enroll.setSex(sexValue);
|
|
|
}
|
|
|
|
|
|
- // C列:年龄
|
|
|
- Cell ageCell = row.getCell(2);
|
|
|
+ // D列:年龄
|
|
|
+ Cell ageCell = row.getCell(3);
|
|
|
if (ageCell != null) {
|
|
|
enroll.setAge(getCellValueAsString(ageCell));
|
|
|
}
|
|
|
|
|
|
- // D列:队伍名称
|
|
|
- Cell teamCell = row.getCell(3);
|
|
|
+ // E列:队伍名称
|
|
|
+ Cell teamCell = row.getCell(4);
|
|
|
if (teamCell != null) {
|
|
|
enroll.setTeamName(getCellValueAsString(teamCell));
|
|
|
}
|
|
|
|
|
|
- // E列:领队
|
|
|
- Cell leaderCell = row.getCell(4);
|
|
|
+ // F列:领队
|
|
|
+ Cell leaderCell = row.getCell(5);
|
|
|
if (leaderCell != null) {
|
|
|
enroll.setLeader(getCellValueAsString(leaderCell));
|
|
|
}
|
|
|
|
|
|
- // F列:联系方式
|
|
|
- Cell phoneCell = row.getCell(5);
|
|
|
+ // G列:联系方式
|
|
|
+ Cell phoneCell = row.getCell(6);
|
|
|
if (phoneCell != null) {
|
|
|
enroll.setPhone(getCellValueAsString(phoneCell));
|
|
|
}
|
|
|
|
|
|
- // 从第6列开始读取项目名称
|
|
|
- for (int j = 6; j < lastCellIndex; j++) {
|
|
|
+ // 从第7列开始读取项目名称
|
|
|
+ for (int j = 7; j < lastCellIndex; j++) {
|
|
|
Cell cell = row.getCell(j);
|
|
|
- String projectName = projectNames.get(j - 6); // 对应项目名
|
|
|
+ String projectName = projectNames.get(j - 7); // 对应项目名
|
|
|
boolean selected = isCellSelected(cell);
|
|
|
selections.put(projectName, selected);
|
|
|
}
|
|
@@ -357,11 +364,9 @@ public class IEnrollServiceImpl implements IEnrollService {
|
|
|
}
|
|
|
return enrolls;
|
|
|
} catch (Exception e) {
|
|
|
-
|
|
|
- } finally {
|
|
|
-
|
|
|
+ log.error("解析Excel文件失败", e);
|
|
|
+ return List.of();
|
|
|
}
|
|
|
- return List.of();
|
|
|
}
|
|
|
|
|
|
// 辅助方法:将 Cell 转为字符串
|
|
@@ -460,87 +465,6 @@ public class IEnrollServiceImpl implements IEnrollService {
|
|
|
* @param eventId
|
|
|
* @return
|
|
|
*/
|
|
|
- // @Transactional(rollbackFor = Exception.class)
|
|
|
- // public boolean saveEnrollData(List<EnrollProjectVo> dataList, Long eventId) {
|
|
|
- // // 1. 根据队伍分类成Map
|
|
|
- // Map<String, List<EnrollProjectVo>> groupedByTeam = dataList.stream()
|
|
|
- // .collect(Collectors.groupingBy(EnrollProjectVo::getTeamName));
|
|
|
- // // 查询当前赛事下的项目 名称和id映射关系
|
|
|
- // Map<String, Long> projectList = gameEventProjectService.mapProjectAndProjectId(eventId);
|
|
|
- // // 1.2 根据队伍生成号码段
|
|
|
- // Map<String, String> numberRanges = new HashMap<>();
|
|
|
- // Map<String, AtomicInteger> currentNumbers = new HashMap<>(); // 记录每个队伍当前分配到的号码
|
|
|
- // AtomicInteger teamIndex = new AtomicInteger(1);
|
|
|
- // Snowflake snowflake = IdUtil.createSnowflake(1, 1);
|
|
|
- //
|
|
|
- // for (Map.Entry<String, List<EnrollProjectVo>> entry : groupedByTeam.entrySet()) {
|
|
|
- // String teamName = entry.getKey();
|
|
|
- // String range = generateNumberRange(dataList.size(), teamIndex.getAndIncrement());
|
|
|
- // numberRanges.put(teamName, range);
|
|
|
- //
|
|
|
- // // 解析起始号码作为初始值
|
|
|
- // int startNum = Integer.parseInt(range.split("-")[0]);
|
|
|
- // currentNumbers.put(teamName, new AtomicInteger(startNum));
|
|
|
- // }
|
|
|
- //
|
|
|
- // // 2. 保存参赛队伍 & 队员
|
|
|
- // for (Map.Entry<String, List<EnrollProjectVo>> entry : groupedByTeam.entrySet()) {
|
|
|
- // String teamName = entry.getKey();
|
|
|
- // List<EnrollProjectVo> athletes = entry.getValue();
|
|
|
- // GameTeamBo gameTeamBo = new GameTeamBo();
|
|
|
- // gameTeamBo.setTeamId(snowflake.nextId());
|
|
|
- // Long teamId = gameTeamBo.getTeamId();
|
|
|
- // // 获取该队伍的当前编号计数器
|
|
|
- // AtomicInteger currentNumber = currentNumbers.get(teamName);
|
|
|
- // int width = (dataList.size() > 100) ? 5 : 4; // 决定格式化宽度
|
|
|
- // List<Long> athletesId = new ArrayList<>();
|
|
|
- // // 3. 保存参赛队员
|
|
|
- // for (EnrollProjectVo enrollInfo : athletes) {
|
|
|
- // GameAthleteBo gameAthleteBo = new GameAthleteBo();
|
|
|
- // gameAthleteBo.setEventId(eventId);
|
|
|
- // gameAthleteBo.setTeamId(teamId);
|
|
|
- // gameAthleteBo.setTeamName(teamName);
|
|
|
- // // 分配编号:从当前计数器获取并递增
|
|
|
- // int assignedNumber = currentNumber.getAndIncrement();
|
|
|
- // String formattedCode = String.format("%0" + width + "d", assignedNumber);
|
|
|
- // gameAthleteBo.setAthleteCode(formattedCode);
|
|
|
- //
|
|
|
- // gameAthleteBo.setName(enrollInfo.getName());
|
|
|
- // gameAthleteBo.setGender(enrollInfo.getSex());
|
|
|
- // gameAthleteBo.setAge(Long.valueOf(enrollInfo.getAge()));
|
|
|
- // gameAthleteBo.setPhone(enrollInfo.getPhone());
|
|
|
- // gameAthleteBo.setUnit(teamName);
|
|
|
- // Map<String, Boolean> selectProjects = enrollInfo.getProjectSelections();
|
|
|
- // //查询出对应的赛事id
|
|
|
- // List<Long> selectionProjectIds = new ArrayList<>();
|
|
|
- // for (Map.Entry<String, Boolean> selectProject : selectProjects.entrySet()) {
|
|
|
- // Long projectId = projectList.get(selectProject.getKey());
|
|
|
- // if (ObjectUtils.isNotEmpty(projectId)) {
|
|
|
- // selectionProjectIds.add(projectId);
|
|
|
- // }
|
|
|
- // }
|
|
|
- // gameAthleteBo.setProjectValue(JSONUtil.toJsonStr(selectionProjectIds));
|
|
|
- //
|
|
|
- // gameAthleteBo.setStatus("0");
|
|
|
- //
|
|
|
- // gameAthleteService.insertByBo(gameAthleteBo);
|
|
|
- // athletesId.add(gameAthleteBo.getAthleteId());
|
|
|
- // }
|
|
|
- //
|
|
|
- // gameTeamBo.setEventId(eventId);
|
|
|
- // gameTeamBo.setTeamName(teamName);
|
|
|
- // // gameTeamBo.setTeamCode("");
|
|
|
- // gameTeamBo.setLeader(athletes.get(0).getLeader());
|
|
|
- // gameTeamBo.setAthleteValue(JSONUtil.toJsonStr(athletesId));
|
|
|
- // gameTeamBo.setAthleteNum(Long.valueOf(athletes.size()));
|
|
|
- // gameTeamBo.setNumberRange(numberRanges.get(teamName));
|
|
|
- // gameTeamBo.setStatus("0");
|
|
|
- // gameTeamService.insertByBo(gameTeamBo);
|
|
|
- //
|
|
|
- //
|
|
|
- // }
|
|
|
- // return true;
|
|
|
- // }
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
public boolean saveEnrollData(List<EnrollProjectVo> dataList, Long eventId) {
|
|
|
// 1. 根据队伍分类成Map
|
|
@@ -603,8 +527,13 @@ public class IEnrollServiceImpl implements IEnrollService {
|
|
|
// 成功获取到有效编号,续着分配
|
|
|
currentNumber = new AtomicInteger(maxNumber + 1);
|
|
|
}
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ log.warn("解析队伍{}的号码段失败: {},从号码段起始开始分配", teamName, numberRange, e);
|
|
|
+ String[] rangeParts = numberRange.split("-");
|
|
|
+ int startNumber = Integer.valueOf(rangeParts[0]);
|
|
|
+ currentNumber = new AtomicInteger(startNumber);
|
|
|
} catch (Exception e) {
|
|
|
- // 出现异常,从号码段起始开始分配
|
|
|
+ log.error("查询队伍{}的最大队员编号时发生异常", teamName, e);
|
|
|
String[] rangeParts = numberRange.split("-");
|
|
|
int startNumber = Integer.valueOf(rangeParts[0]);
|
|
|
currentNumber = new AtomicInteger(startNumber);
|
|
@@ -614,14 +543,39 @@ public class IEnrollServiceImpl implements IEnrollService {
|
|
|
isNewTeam = true;
|
|
|
teamId = snowflake.nextId();
|
|
|
|
|
|
- // 为新队伍生成300个号码段
|
|
|
- int teamIndex = teamIndexCounter.getAndIncrement();
|
|
|
- numberRange = generateNumberRange(dataList.size(), teamIndex);
|
|
|
-
|
|
|
- // 解析号码段起始号码
|
|
|
- String[] rangeParts = numberRange.split("-");
|
|
|
- int startNumber = Integer.valueOf(rangeParts[0]);
|
|
|
- currentNumber = new AtomicInteger(startNumber);
|
|
|
+ // 检查是否有填写的号码
|
|
|
+ List<String> providedCodes = athletes.stream()
|
|
|
+ .map(EnrollProjectVo::getAthleteCode)
|
|
|
+ .filter(StringUtils::isNotBlank)
|
|
|
+ .map(code -> code.trim())
|
|
|
+ .filter(code -> StringUtils.isNotBlank(code))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ if (!providedCodes.isEmpty()) {
|
|
|
+ // 有填写的号码,使用ASCII值比较来确定号码段
|
|
|
+ numberRange = generateCustomNumberRangeWithChars(providedCodes, eventId, null);
|
|
|
+
|
|
|
+ if (numberRange != null) {
|
|
|
+ // 成功生成自定义号码段
|
|
|
+ currentNumber = null; // 对于字符号码,不使用数字计数器
|
|
|
+ log.info("队伍 {} 使用自定义字符号码段: {}", teamName, numberRange);
|
|
|
+ } else {
|
|
|
+ // 生成失败,使用默认方式
|
|
|
+ int teamIndex = teamIndexCounter.getAndIncrement();
|
|
|
+ numberRange = generateNumberRange(dataList.size(), teamIndex);
|
|
|
+ String[] rangeParts = numberRange.split("-");
|
|
|
+ int startNumber = Integer.valueOf(rangeParts[0]);
|
|
|
+ currentNumber = new AtomicInteger(startNumber);
|
|
|
+ log.warn("队伍 {} 自定义号码段生成失败,使用默认号码段: {}", teamName, numberRange);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 没有填写的号码,使用默认的300号码段
|
|
|
+ int teamIndex = teamIndexCounter.getAndIncrement();
|
|
|
+ numberRange = generateNumberRange(dataList.size(), teamIndex);
|
|
|
+ String[] rangeParts = numberRange.split("-");
|
|
|
+ int startNumber = Integer.valueOf(rangeParts[0]);
|
|
|
+ currentNumber = new AtomicInteger(startNumber);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// 保存队员信息
|
|
@@ -654,6 +608,49 @@ public class IEnrollServiceImpl implements IEnrollService {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 检查字符号码段是否与现有队伍冲突
|
|
|
+ *
|
|
|
+ * @param minCode 最小号码
|
|
|
+ * @param maxCode 最大号码
|
|
|
+ * @param eventId 赛事ID
|
|
|
+ * @param excludeTeamId 排除的队伍ID
|
|
|
+ * @return 是否冲突
|
|
|
+ */
|
|
|
+ private boolean isNumberRangeConflictWithChars(String minCode, String maxCode, Long eventId, Long excludeTeamId) {
|
|
|
+ List<GameTeam> existingTeams = gameTeamService.queryTeamByEventId(eventId);
|
|
|
+
|
|
|
+ for (GameTeam team : existingTeams) {
|
|
|
+ // 排除当前队伍
|
|
|
+ if (excludeTeamId != null && team.getTeamId().equals(excludeTeamId)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (StringUtils.isNotBlank(team.getNumberRange())) {
|
|
|
+ String[] rangeParts = team.getNumberRange().split("-");
|
|
|
+ if (rangeParts.length == 2) {
|
|
|
+ String existingMin = rangeParts[0];
|
|
|
+ String existingMax = rangeParts[1];
|
|
|
+
|
|
|
+ // 使用ASCII比较检查重叠
|
|
|
+ if (isRangeOverlapWithChars(minCode, maxCode, existingMin, existingMax)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查两个号码段是否有重叠
|
|
|
+ */
|
|
|
+ private boolean isRangeOverlapWithChars(String min1, String max1, String min2, String max2) {
|
|
|
+ // 如果max1 < min2 或 max2 < min1,则没有重叠
|
|
|
+ return !(compareCodesByAscii(max1, min2) < 0 || compareCodesByAscii(max2, min1) < 0);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 生成号码段
|
|
|
* 规则:
|
|
@@ -679,21 +676,258 @@ public class IEnrollServiceImpl implements IEnrollService {
|
|
|
return startStr + "-" + endStr;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 生成基于填写号码的号码段(支持字符)
|
|
|
+ *
|
|
|
+ * @param providedCodes 填写的号码列表
|
|
|
+ * @param eventId 赛事ID
|
|
|
+ * @param excludeTeamId 排除的队伍ID
|
|
|
+ * @return 号码段字符串
|
|
|
+ */
|
|
|
+ private String generateCustomNumberRangeWithChars(List<String> providedCodes, Long eventId, Long excludeTeamId) {
|
|
|
+ if (providedCodes.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 过滤和验证号码
|
|
|
+ List<String> validCodes = providedCodes.stream()
|
|
|
+ .filter(StringUtils::isNotBlank)
|
|
|
+ .map(String::trim)
|
|
|
+ .filter(this::isValidAthleteCode)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ if (validCodes.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按ASCII值排序
|
|
|
+ List<String> sortedCodes = sortCodesByAscii(validCodes);
|
|
|
+ String minCode = sortedCodes.get(0);
|
|
|
+ String maxCode = sortedCodes.get(sortedCodes.size() - 1);
|
|
|
+
|
|
|
+ // 生成号码段
|
|
|
+ String numberRange = generateRangeFromCodes(minCode, maxCode);
|
|
|
+
|
|
|
+ // 检查是否与现有号码段冲突
|
|
|
+ if (isNumberRangeConflictWithChars(minCode, maxCode, eventId, excludeTeamId)) {
|
|
|
+ // 如果冲突,尝试扩展范围
|
|
|
+ numberRange = expandRangeToAvoidConflict(minCode, maxCode, eventId, excludeTeamId);
|
|
|
+ if (numberRange == null) {
|
|
|
+ log.warn("无法为填写的号码生成合适的号码段,使用默认方式");
|
|
|
+ return null; // 返回null,让调用方使用默认方式
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("队伍使用自定义号码段: {} (基于填写的号码: {}-{})",
|
|
|
+ numberRange, minCode, maxCode);
|
|
|
+
|
|
|
+ return numberRange;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据最小和最大号码生成号码段
|
|
|
+ *
|
|
|
+ * @param minCode 最小号码
|
|
|
+ * @param maxCode 最大号码
|
|
|
+ * @return 号码段字符串
|
|
|
+ */
|
|
|
+ private String generateRangeFromCodes(String minCode, String maxCode) {
|
|
|
+ // 简单的实现:直接使用最小和最大号码
|
|
|
+ return minCode + "-" + maxCode;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通过前缀扩展号码段
|
|
|
+ */
|
|
|
+ private String expandByPrefix(String minCode, String maxCode, String prefix) {
|
|
|
+ return prefix + minCode + "-" + prefix + maxCode;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通过后缀扩展号码段
|
|
|
+ */
|
|
|
+ private String expandBySuffix(String minCode, String maxCode, String suffix) {
|
|
|
+ return minCode + suffix + "-" + maxCode + suffix;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通过填充扩展号码段
|
|
|
+ */
|
|
|
+ private String expandByPadding(String minCode, String maxCode, String padding) {
|
|
|
+ // 确保最小长度一致
|
|
|
+ int maxLength = Math.max(minCode.length(), maxCode.length());
|
|
|
+ String paddedMin = String.format("%-" + maxLength + "s", minCode).replace(' ', padding.charAt(0));
|
|
|
+ String paddedMax = String.format("%-" + maxLength + "s", maxCode).replace(' ', padding.charAt(0));
|
|
|
+
|
|
|
+ return paddedMin + "-" + paddedMax;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 扩展号码段以避免冲突
|
|
|
+ *
|
|
|
+ * @param minCode 最小号码
|
|
|
+ * @param maxCode 最大号码
|
|
|
+ * @param eventId 赛事ID
|
|
|
+ * @param excludeTeamId 排除的队伍ID
|
|
|
+ * @return 扩展后的号码段
|
|
|
+ */
|
|
|
+ private String expandRangeToAvoidConflict(String minCode, String maxCode, Long eventId, Long excludeTeamId) {
|
|
|
+ // 尝试不同的扩展策略
|
|
|
+ String[] prefixes = {"A", "B", "C", "D", "E"};
|
|
|
+ String[] suffixes = {"A", "B", "C", "D", "E"};
|
|
|
+
|
|
|
+ // 1. 尝试前缀扩展
|
|
|
+ for (String prefix : prefixes) {
|
|
|
+ String expandedRange = expandByPrefix(minCode, maxCode, prefix);
|
|
|
+ if (!isNumberRangeConflictWithChars(
|
|
|
+ expandedRange.split("-")[0], expandedRange.split("-")[1], eventId, excludeTeamId)) {
|
|
|
+ return expandedRange;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 尝试后缀扩展
|
|
|
+ for (String suffix : suffixes) {
|
|
|
+ String expandedRange = expandBySuffix(minCode, maxCode, suffix);
|
|
|
+ if (!isNumberRangeConflictWithChars(
|
|
|
+ expandedRange.split("-")[0], expandedRange.split("-")[1], eventId, excludeTeamId)) {
|
|
|
+ return expandedRange;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 尝试数字扩展
|
|
|
+ for (int i = 1; i <= 9; i++) {
|
|
|
+ String expandedRange = expandByPrefix(minCode, maxCode, String.valueOf(i));
|
|
|
+ if (!isNumberRangeConflictWithChars(
|
|
|
+ expandedRange.split("-")[0], expandedRange.split("-")[1], eventId, excludeTeamId)) {
|
|
|
+ return expandedRange;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 比较两个号码的ASCII值大小
|
|
|
+ *
|
|
|
+ * @param code1 号码1
|
|
|
+ * @param code2 号码2
|
|
|
+ * @return 比较结果:负数表示code1<code2,0表示相等,正数表示code1>code2
|
|
|
+ */
|
|
|
+ private int compareCodesByAscii(String code1, String code2) {
|
|
|
+ if (code1 == null && code2 == null) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ if (code1 == null) {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ if (code2 == null) {
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按字符逐个比较ASCII值
|
|
|
+ int minLength = Math.min(code1.length(), code2.length());
|
|
|
+
|
|
|
+ for (int i = 0; i < minLength; i++) {
|
|
|
+ char char1 = code1.charAt(i);
|
|
|
+ char char2 = code2.charAt(i);
|
|
|
+
|
|
|
+ if (char1 != char2) {
|
|
|
+ return char1 - char2; // 返回ASCII值差
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果前面字符都相同,比较长度
|
|
|
+ return code1.length() - code2.length();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 对号码列表按ASCII值排序
|
|
|
+ *
|
|
|
+ * @param codes 号码列表
|
|
|
+ * @return 排序后的号码列表
|
|
|
+ */
|
|
|
+ private List<String> sortCodesByAscii(List<String> codes) {
|
|
|
+ return codes.stream()
|
|
|
+ .filter(StringUtils::isNotBlank)
|
|
|
+ .map(String::trim)
|
|
|
+ .sorted(this::compareCodesByAscii)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 验证运动员号码格式(支持字符)
|
|
|
+ *
|
|
|
+ * @param athleteCode 运动员号码
|
|
|
+ * @return 是否有效
|
|
|
+ */
|
|
|
+ private boolean isValidAthleteCode(String athleteCode) {
|
|
|
+ if (StringUtils.isBlank(athleteCode)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ String trimmedCode = athleteCode.trim();
|
|
|
+
|
|
|
+ // 检查长度
|
|
|
+ if (trimmedCode.length() < 1 || trimmedCode.length() > 15) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 允许字母、数字、连字符、下划线、点号
|
|
|
+ if (!trimmedCode.matches("^[A-Za-z0-9\\-_\\.]+$")) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否包含连续的特殊字符
|
|
|
+ if (trimmedCode.matches(".*[\\-_\\.]{2,}.*")) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 创建运动员BO对象
|
|
|
*/
|
|
|
- private GameAthleteBo createAthleteBo(EnrollProjectVo enrollInfo, Long eventId, Long teamId,
|
|
|
- String teamName, AtomicInteger currentNumber, int width,
|
|
|
- Map<String, Long> projectList) {
|
|
|
+ private GameAthleteBo createAthleteBo(EnrollProjectVo enrollInfo,
|
|
|
+ Long eventId, Long teamId,
|
|
|
+ String teamName,
|
|
|
+ AtomicInteger currentNumber, int width,
|
|
|
+ Map<String, Long> projectList) {
|
|
|
GameAthleteBo gameAthleteBo = new GameAthleteBo();
|
|
|
gameAthleteBo.setEventId(eventId);
|
|
|
gameAthleteBo.setTeamId(teamId);
|
|
|
gameAthleteBo.setTeamName(teamName);
|
|
|
|
|
|
- // 分配编号
|
|
|
- int assignedNumber = currentNumber.getAndIncrement();
|
|
|
- String formattedCode = String.format("%0" + width + "d", assignedNumber);
|
|
|
- gameAthleteBo.setAthleteCode(formattedCode);
|
|
|
+ // 号码处理逻辑
|
|
|
+ String athleteCode;
|
|
|
+ if (StringUtils.isNotBlank(enrollInfo.getAthleteCode())) {
|
|
|
+ // 如果Excel中有号码,使用Excel中的号码
|
|
|
+ athleteCode = enrollInfo.getAthleteCode().trim();
|
|
|
+
|
|
|
+ // 验证号码格式
|
|
|
+ if (!isValidAthleteCode(athleteCode)) {
|
|
|
+ log.warn("运动员{}的号码格式不正确: {},将使用自动生成的号码", enrollInfo.getName(), athleteCode);
|
|
|
+ // 如果号码格式不正确,使用自动生成的号码
|
|
|
+ if (currentNumber != null) {
|
|
|
+ int assignedNumber = currentNumber.getAndIncrement();
|
|
|
+ athleteCode = String.format("%0" + width + "d", assignedNumber);
|
|
|
+ } else {
|
|
|
+ // 字符号码段情况下,生成一个基于序号的号码
|
|
|
+ athleteCode = "AUTO_" + (System.currentTimeMillis() % 10000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 如果Excel中没有号码,自动生成
|
|
|
+ if (currentNumber != null) {
|
|
|
+ int assignedNumber = currentNumber.getAndIncrement();
|
|
|
+ athleteCode = String.format("%0" + width + "d", assignedNumber);
|
|
|
+ } else {
|
|
|
+ // 字符号码段情况下,生成一个基于序号的号码
|
|
|
+ athleteCode = "AUTO_" + (System.currentTimeMillis() % 10000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ gameAthleteBo.setAthleteCode(athleteCode);
|
|
|
|
|
|
gameAthleteBo.setName(enrollInfo.getName());
|
|
|
gameAthleteBo.setGender(enrollInfo.getSex());
|
|
@@ -754,4 +988,289 @@ public class IEnrollServiceImpl implements IEnrollService {
|
|
|
}
|
|
|
|
|
|
// endregion 辅助方法
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查号码是否已存在
|
|
|
+ *
|
|
|
+ * @param athleteCode 运动员号码
|
|
|
+ * @param eventId 赛事ID
|
|
|
+ * @return 是否已存在
|
|
|
+ */
|
|
|
+ private boolean isAthleteCodeExists(String athleteCode, Long eventId) {
|
|
|
+ if (StringUtils.isBlank(athleteCode)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ GameAthleteBo bo = new GameAthleteBo();
|
|
|
+ bo.setEventId(eventId);
|
|
|
+ bo.setAthleteCode(athleteCode);
|
|
|
+
|
|
|
+ List<GameAthleteVo> existingAthletes = gameAthleteService.queryList(bo);
|
|
|
+ return existingAthletes.size() > 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 校验基本数据完整性
|
|
|
+ */
|
|
|
+ private List<EnrollValidationError> validateBasicData(EnrollProjectVo enroll, int rowIndex) {
|
|
|
+ List<EnrollValidationError> errors = new ArrayList<>();
|
|
|
+
|
|
|
+ if (StringUtils.isBlank(enroll.getName())) {
|
|
|
+ EnrollValidationError error = new EnrollValidationError();
|
|
|
+ error.setRowIndex(rowIndex + 1);
|
|
|
+ error.setErrorType("MISSING_NAME");
|
|
|
+ error.setErrorMessage("运动员姓名不能为空");
|
|
|
+ errors.add(error);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (StringUtils.isBlank(enroll.getTeamName())) {
|
|
|
+ EnrollValidationError error = new EnrollValidationError();
|
|
|
+ error.setRowIndex(rowIndex + 1);
|
|
|
+ error.setAthleteName(enroll.getName());
|
|
|
+ error.setErrorType("MISSING_TEAM");
|
|
|
+ error.setErrorMessage("队伍名称不能为空");
|
|
|
+ errors.add(error);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 性别校验 - 使用字典工具验证
|
|
|
+ if (StringUtils.isBlank(enroll.getSex())) {
|
|
|
+ EnrollValidationError error = new EnrollValidationError();
|
|
|
+ error.setRowIndex(rowIndex + 1);
|
|
|
+ error.setAthleteName(enroll.getName());
|
|
|
+ error.setTeamName(enroll.getTeamName());
|
|
|
+ error.setErrorType("MISSING_GENDER");
|
|
|
+ error.setErrorMessage("性别不能为空");
|
|
|
+ errors.add(error);
|
|
|
+ } else if (!GenderDictUtils.isValidGender(enroll.getSex())) {
|
|
|
+ EnrollValidationError error = new EnrollValidationError();
|
|
|
+ error.setRowIndex(rowIndex + 1);
|
|
|
+ error.setAthleteName(enroll.getName());
|
|
|
+ error.setTeamName(enroll.getTeamName());
|
|
|
+ error.setErrorType("INVALID_GENDER");
|
|
|
+ error.setErrorMessage("性别值无效,请填写'男'、'女'、'1'或'2'");
|
|
|
+ errors.add(error);
|
|
|
+ }
|
|
|
+
|
|
|
+ return errors;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public EnrollValidationResult validateEnrollData(List<EnrollProjectVo> enrollList, Long eventId) {
|
|
|
+ EnrollValidationResult result = new EnrollValidationResult();
|
|
|
+ result.setValid(true);
|
|
|
+ result.setErrors(new ArrayList<>());
|
|
|
+ result.setValidData(new ArrayList<>());
|
|
|
+
|
|
|
+ if (enrollList == null || enrollList.isEmpty()) {
|
|
|
+ log.warn("导入数据为空,赛事ID: {}", eventId);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (eventId == null) {
|
|
|
+ log.error("赛事ID不能为空");
|
|
|
+ throw new IllegalArgumentException("赛事ID不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 1. 获取赛事配置:每人限报项目数
|
|
|
+ Integer maxProjectsPerPerson = getMaxProjectsPerPerson(eventId);
|
|
|
+
|
|
|
+ // 2. 获取项目限制信息
|
|
|
+ Map<String, Integer> projectLimits = getProjectLimits(eventId);
|
|
|
+
|
|
|
+ // 3. 统计每个项目的当前报名人数
|
|
|
+ Map<String, Integer> currentProjectCounts = getCurrentProjectCounts(eventId);
|
|
|
+
|
|
|
+ // 4. 校验每个运动员
|
|
|
+ for (int i = 0; i < enrollList.size(); i++) {
|
|
|
+ EnrollProjectVo enroll = enrollList.get(i);
|
|
|
+ // 先进行基本数据校验
|
|
|
+ List<EnrollValidationError> basicErrors = validateBasicData(enroll, i);
|
|
|
+ if (!basicErrors.isEmpty()) {
|
|
|
+ result.setValid(false);
|
|
|
+ result.getErrors().addAll(basicErrors);
|
|
|
+ continue; // 基本数据有问题,跳过后续校验
|
|
|
+ }
|
|
|
+
|
|
|
+ List<EnrollValidationError> athleteErrors = validateAthlete(eventId, enroll, i,
|
|
|
+ maxProjectsPerPerson, projectLimits, currentProjectCounts);
|
|
|
+
|
|
|
+ if (athleteErrors.isEmpty()) {
|
|
|
+ result.getValidData().add(enroll);
|
|
|
+ // 更新项目计数(模拟保存后的状态)
|
|
|
+ updateProjectCounts(enroll, currentProjectCounts);
|
|
|
+ } else {
|
|
|
+ result.setValid(false);
|
|
|
+ result.getErrors().addAll(athleteErrors);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("校验导入数据时发生异常,赛事ID: {}", eventId, e);
|
|
|
+ result.setValid(false);
|
|
|
+ EnrollValidationError error = new EnrollValidationError();
|
|
|
+ error.setErrorType("SYSTEM_ERROR");
|
|
|
+ error.setErrorMessage("系统校验异常:" + e.getMessage());
|
|
|
+ result.getErrors().add(error);
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public EnrollImportResult importDataWithValidation(MultipartFile file, Long eventId) {
|
|
|
+ // 1. 解析数据
|
|
|
+ List<EnrollProjectVo> enrollList = parseData(file);
|
|
|
+
|
|
|
+ // 2. 校验数据
|
|
|
+ EnrollValidationResult validationResult = validateEnrollData(enrollList, eventId);
|
|
|
+
|
|
|
+ // 3. 保存有效数据
|
|
|
+ boolean saveSuccess = false;
|
|
|
+ if (!validationResult.getValidData().isEmpty()) {
|
|
|
+ saveSuccess = saveEnrollData(validationResult.getValidData(), eventId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 返回结果
|
|
|
+ EnrollImportResult result = new EnrollImportResult();
|
|
|
+ result.setSuccess(saveSuccess);
|
|
|
+ result.setValidationResult(validationResult);
|
|
|
+ result.setTotalCount(enrollList.size());
|
|
|
+ result.setValidCount(validationResult.getValidData().size());
|
|
|
+ result.setErrorCount(validationResult.getErrors().size());
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取每人限报项目数
|
|
|
+ */
|
|
|
+ private Integer getMaxProjectsPerPerson(Long eventId) {
|
|
|
+ GameEventVo event = gameEventService.queryById(eventId);
|
|
|
+ return event.getLimitApplication() != null ? event.getLimitApplication() : 0; // 默认0个
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取项目限制信息
|
|
|
+ */
|
|
|
+ private Map<String, Integer> getProjectLimits(Long eventId) {
|
|
|
+ List<GameEventProjectVo> projects = gameEventProjectService.queryListByEventId(eventId);
|
|
|
+ return projects.stream()
|
|
|
+ .filter(p -> p.getLimitPerson() != null && p.getLimitPerson() > 0)
|
|
|
+ .collect(Collectors.toMap(
|
|
|
+ GameEventProjectVo::getProjectName,
|
|
|
+ GameEventProjectVo::getLimitPerson
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取当前项目报名人数
|
|
|
+ */
|
|
|
+ private Map<String, Integer> getCurrentProjectCounts(Long eventId) {
|
|
|
+ GameAthleteBo bo = new GameAthleteBo();
|
|
|
+ bo.setEventId(eventId);
|
|
|
+ List<GameAthleteVo> athletes = gameAthleteService.queryList(bo);
|
|
|
+ Map<String, Integer> counts = new HashMap<>();
|
|
|
+
|
|
|
+ for (GameAthleteVo athlete : athletes) {
|
|
|
+ if (StringUtils.isNotBlank(athlete.getProjectValue())) {
|
|
|
+ List<Long> projectIds = JSONUtil.toList(athlete.getProjectValue(), Long.class);
|
|
|
+ for (Long projectId : projectIds) {
|
|
|
+ GameEventProjectVo project = gameEventProjectService.queryById(projectId);
|
|
|
+ if (project != null) {
|
|
|
+ counts.merge(project.getProjectName(), 1, Integer::sum);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return counts;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 校验单个运动员
|
|
|
+ */
|
|
|
+ private List<EnrollValidationError> validateAthlete(Long eventId, EnrollProjectVo enroll, int rowIndex,
|
|
|
+ Integer maxProjectsPerPerson, Map<String, Integer> projectLimits,
|
|
|
+ Map<String, Integer> currentProjectCounts) {
|
|
|
+
|
|
|
+ List<EnrollValidationError> errors = new ArrayList<>();
|
|
|
+
|
|
|
+ // 号码冲突检查
|
|
|
+ if (StringUtils.isNotBlank(enroll.getAthleteCode())) {
|
|
|
+ if (isAthleteCodeExists(enroll.getAthleteCode(), eventId)) {
|
|
|
+ EnrollValidationError error = new EnrollValidationError();
|
|
|
+ error.setRowIndex(rowIndex + 1);
|
|
|
+ error.setAthleteName(enroll.getName());
|
|
|
+ error.setTeamName(enroll.getTeamName());
|
|
|
+ error.setErrorType("DUPLICATE_ATHLETE_CODE");
|
|
|
+ error.setErrorMessage(String.format("运动员号码'%s'已存在", enroll.getAthleteCode()));
|
|
|
+ errors.add(error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查项目选择数据是否为空
|
|
|
+ if (enroll.getProjectSelections() == null || enroll.getProjectSelections().isEmpty()) {
|
|
|
+ EnrollValidationError error = new EnrollValidationError();
|
|
|
+ error.setRowIndex(rowIndex + 1);
|
|
|
+ error.setAthleteName(enroll.getName());
|
|
|
+ error.setTeamName(enroll.getTeamName());
|
|
|
+ error.setErrorType("NO_PROJECT_SELECTED");
|
|
|
+ error.setErrorMessage("该运动员未选择任何项目");
|
|
|
+ errors.add(error);
|
|
|
+ return errors; // 如果没有选择项目,直接返回
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 校验每人限报项目数
|
|
|
+ long selectedProjectCount = enroll.getProjectSelections().values().stream()
|
|
|
+ .mapToLong(selected -> selected ? 1 : 0)
|
|
|
+ .sum();
|
|
|
+
|
|
|
+ if (maxProjectsPerPerson > 0 && selectedProjectCount > maxProjectsPerPerson) {
|
|
|
+ EnrollValidationError error = new EnrollValidationError();
|
|
|
+ error.setRowIndex(rowIndex + 1); // 从1开始计数
|
|
|
+ error.setAthleteName(enroll.getName());
|
|
|
+ error.setTeamName(enroll.getTeamName());
|
|
|
+ error.setErrorType("PERSON_LIMIT_EXCEEDED");
|
|
|
+ error.setErrorMessage(String.format("该运动员选择了%d个项目,超出每人限报%d个项目",
|
|
|
+ selectedProjectCount, maxProjectsPerPerson));
|
|
|
+ errors.add(error);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 校验项目限报人数
|
|
|
+ for (Map.Entry<String, Boolean> entry : enroll.getProjectSelections().entrySet()) {
|
|
|
+ if (entry.getValue()) { // 如果选择了该项目
|
|
|
+ String projectName = entry.getKey();
|
|
|
+ Integer projectLimit = projectLimits.get(projectName);
|
|
|
+
|
|
|
+ if (projectLimit != null && projectLimit > 0) {
|
|
|
+ int currentCount = currentProjectCounts.getOrDefault(projectName, 0);
|
|
|
+ if (currentCount >= projectLimit) {
|
|
|
+ EnrollValidationError error = new EnrollValidationError();
|
|
|
+ error.setRowIndex(rowIndex + 1);
|
|
|
+ error.setAthleteName(enroll.getName());
|
|
|
+ error.setTeamName(enroll.getTeamName());
|
|
|
+ error.setErrorType("PROJECT_LIMIT_EXCEEDED");
|
|
|
+ error.setProjectName(projectName);
|
|
|
+ error.setErrorMessage(String.format("项目'%s'报名人数已达上限%d人",
|
|
|
+ projectName, projectLimit));
|
|
|
+ errors.add(error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return errors;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新项目计数(模拟保存后的状态)
|
|
|
+ */
|
|
|
+ private void updateProjectCounts(EnrollProjectVo enroll, Map<String, Integer> currentProjectCounts) {
|
|
|
+ for (Map.Entry<String, Boolean> entry : enroll.getProjectSelections().entrySet()) {
|
|
|
+ if (entry.getValue()) {
|
|
|
+ String projectName = entry.getKey();
|
|
|
+ currentProjectCounts.merge(projectName, 1, Integer::sum);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|