|
@@ -2,15 +2,25 @@ package org.dromara.system.service.impl;
|
|
|
|
|
|
import cn.hutool.json.JSONUtil;
|
|
|
import cn.idev.excel.annotation.ExcelProperty;
|
|
|
+import jakarta.servlet.http.HttpServletRequest;
|
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import lombok.val;
|
|
|
+import org.apache.poi.ss.usermodel.*;
|
|
|
+import org.apache.poi.ss.util.CellRangeAddress;
|
|
|
+import org.apache.poi.xssf.usermodel.XSSFSheet;
|
|
|
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
|
|
+import org.dromara.system.controller.TestPoi;
|
|
|
+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.service.IEnrollService;
|
|
|
+import org.dromara.system.service.IGameAthleteService;
|
|
|
import org.dromara.system.service.IGameEventProjectService;
|
|
|
import org.dromara.system.service.IGameTeamService;
|
|
|
+import org.springframework.core.io.Resource;
|
|
|
+import org.springframework.core.io.ResourceLoader;
|
|
|
import org.springframework.http.ResponseEntity;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
@@ -22,6 +32,7 @@ import java.net.URLEncoder;
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
import java.util.*;
|
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
@Slf4j
|
|
@@ -31,236 +42,324 @@ public class IEnrollServiceImpl implements IEnrollService {
|
|
|
|
|
|
private final IGameEventProjectService gameEventProjectService;
|
|
|
private final IGameTeamService gameTeamService;
|
|
|
- private final ConcurrentHashMap<Long, Class> classMap = new ConcurrentHashMap<>();
|
|
|
- private final ConcurrentHashMap<Long, List<String>> projectsMap = new ConcurrentHashMap<>();
|
|
|
+ private final IGameAthleteService gameAthleteService;
|
|
|
|
|
|
/**
|
|
|
- * 下载报名表模板
|
|
|
+ * 使用poi生成报名表模板
|
|
|
*
|
|
|
* @param response
|
|
|
* @param eventId
|
|
|
*/
|
|
|
@Override
|
|
|
- public void downloadTemplate(HttpServletResponse response, Long eventId) {
|
|
|
- // 1. 获取默认赛事动态项目(赛事项目)
|
|
|
- Map<String, List<String>> projectMap = gameEventProjectService.mapProjectTypeAndProject(eventId);
|
|
|
- List<String> projects = new ArrayList<>();
|
|
|
- for (List<String> list : projectMap.values()) {
|
|
|
- projects.addAll(list);
|
|
|
- }
|
|
|
- projectsMap.put(eventId, projects);
|
|
|
-
|
|
|
- // 2. 构建表头行(CSV 用逗号分隔)
|
|
|
- List<String> headers = new ArrayList<>();
|
|
|
- // 反射获取属性的注解value值
|
|
|
- Class<EnrollProjectVo> enrollClass = EnrollProjectVo.class;
|
|
|
- Field[] declaredFields = enrollClass.getDeclaredFields();
|
|
|
- for (Field field : declaredFields) {
|
|
|
- field.setAccessible(true);
|
|
|
- // 检查字段是否含有 ExcelProperty 注解
|
|
|
- if (field.isAnnotationPresent(ExcelProperty.class)) {
|
|
|
- ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);
|
|
|
- // 获取注解的 value 值
|
|
|
- String[] value = annotation.value();
|
|
|
- log.error("注解值:{}", Arrays.toString(value));
|
|
|
- headers.add(value[0]);
|
|
|
- }
|
|
|
- }
|
|
|
- classMap.put(eventId, enrollClass);
|
|
|
- headers.addAll(projects);
|
|
|
-
|
|
|
- // 3. 使用 UTF-8 编码的 CSV,兼容 Excel 打开
|
|
|
- String fileName = URLEncoder.encode("报名导入模板", StandardCharsets.UTF_8) + ".csv";
|
|
|
-
|
|
|
- response.setContentType("text/csv; charset=UTF-8");
|
|
|
- response.setCharacterEncoding("UTF-8");
|
|
|
- response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + fileName);
|
|
|
-
|
|
|
- // 4. 使用原始 IO 流输出 CSV
|
|
|
- try (OutputStream os = response.getOutputStream();
|
|
|
- OutputStreamWriter writer = new OutputStreamWriter(os, StandardCharsets.UTF_8)) {
|
|
|
+ public void downloadTemplateForPoi(HttpServletRequest request, HttpServletResponse response, Long eventId) {
|
|
|
+ //1.加载Excel模板文件
|
|
|
+ String template = "template/enroll_template.xlsx";
|
|
|
+ XSSFSheet sheet = null;
|
|
|
+ try (
|
|
|
+ InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(template);
|
|
|
+ XSSFWorkbook xwb = new XSSFWorkbook(inputStream)) {
|
|
|
+ sheet = xwb.getSheetAt(0);
|
|
|
+ assert sheet != null;
|
|
|
+ //7列3行开始横着渲染 excel表对应6行2列
|
|
|
+ // 2. 获取赛事动态项目(赛事项目)
|
|
|
+ Map<String, List<String>> projectMap = gameEventProjectService.mapProjectTypeAndProject(eventId);
|
|
|
|
|
|
- // 写入 BOM(可选):让 Excel 正确识别 UTF-8
|
|
|
- writer.write('\uFEFF'); // UTF-8 BOM
|
|
|
+ // 3. 渲染分类
|
|
|
+ int currentColumnIndex = 6;
|
|
|
+ Row row = sheet.getRow(1);
|
|
|
+ if (row == null) {
|
|
|
+ row = sheet.createRow(1);
|
|
|
+ }
|
|
|
|
|
|
- // 写入表头(用逗号连接)
|
|
|
- writer.write(String.join(",", headers));
|
|
|
- writer.write("\n"); // 换行
|
|
|
+ for (Map.Entry<String, List<String>> entry : projectMap.entrySet()) {
|
|
|
+ String categoryName = entry.getKey();
|
|
|
+ List<String> projectList = entry.getValue();
|
|
|
+ int projectCount = projectList.size();
|
|
|
+ // 3.1 创建单元格并设置分类名称
|
|
|
+ Cell cell = row.createCell(currentColumnIndex);
|
|
|
+ cell.setCellValue(categoryName);
|
|
|
+
|
|
|
+ // 3.2 合并单元格:从 currentColumnIndex 开始,合并 projectCount 个单元格
|
|
|
+ int lastColumnIndex = currentColumnIndex + projectCount - 1;
|
|
|
+ CellRangeAddress region = new CellRangeAddress(1, 1, currentColumnIndex, lastColumnIndex);
|
|
|
+ sheet.addMergedRegion(region);
|
|
|
+
|
|
|
+ // 3.3 创建样式:边框 + 居中 + 加粗
|
|
|
+ CellStyle style = xwb.createCellStyle();
|
|
|
+ style.setBorderTop(BorderStyle.THIN);
|
|
|
+ style.setBorderBottom(BorderStyle.THIN);
|
|
|
+ style.setBorderLeft(BorderStyle.THIN);
|
|
|
+ style.setBorderRight(BorderStyle.THIN);
|
|
|
+ style.setTopBorderColor(IndexedColors.BLACK.getIndex());
|
|
|
+ style.setBottomBorderColor(IndexedColors.BLACK.getIndex());
|
|
|
+ style.setLeftBorderColor(IndexedColors.BLACK.getIndex());
|
|
|
+ style.setRightBorderColor(IndexedColors.BLACK.getIndex());
|
|
|
+
|
|
|
+ style.setAlignment(HorizontalAlignment.CENTER);
|
|
|
+ style.setVerticalAlignment(VerticalAlignment.CENTER);
|
|
|
+
|
|
|
+ // 字体加粗
|
|
|
+ Font font = xwb.createFont();
|
|
|
+ font.setBold(true);
|
|
|
+ style.setFont(font);
|
|
|
+
|
|
|
+ // 应用样式
|
|
|
+ cell.setCellStyle(style);
|
|
|
+
|
|
|
+ // 3.4 更新起始列:为下一个分类腾出位置
|
|
|
+ currentColumnIndex = lastColumnIndex + 1;
|
|
|
+ }
|
|
|
|
|
|
- // 可选:写入一行空数据作为示例
|
|
|
- String emptyRow = String.join(",", Collections.nCopies(headers.size(), ""));
|
|
|
- writer.write(emptyRow);
|
|
|
- writer.write("\n");
|
|
|
+ // 4. 渲染项目(在第4行,索引为3)
|
|
|
+ currentColumnIndex = 6;
|
|
|
+ Row projectRow = sheet.getRow(2);
|
|
|
+ if (projectRow == null) {
|
|
|
+ projectRow = sheet.createRow(2);
|
|
|
+ }
|
|
|
+ for (Map.Entry<String, List<String>> entry : projectMap.entrySet()) {
|
|
|
+ List<String> projectList = entry.getValue();
|
|
|
+ // 4.1 遍历当前分类下的每个项目
|
|
|
+ for (String projectName : projectList) {
|
|
|
+ Cell cell = projectRow.createCell(currentColumnIndex);
|
|
|
+ cell.setCellValue(projectName);
|
|
|
+
|
|
|
+ // 3.3 创建样式:边框 + 居中 + 加粗
|
|
|
+ CellStyle style = xwb.createCellStyle();
|
|
|
+ style.setBorderTop(BorderStyle.THIN);
|
|
|
+ style.setBorderBottom(BorderStyle.THIN);
|
|
|
+ style.setBorderLeft(BorderStyle.THIN);
|
|
|
+ style.setBorderRight(BorderStyle.THIN);
|
|
|
+ style.setTopBorderColor(IndexedColors.BLACK.getIndex());
|
|
|
+ style.setBottomBorderColor(IndexedColors.BLACK.getIndex());
|
|
|
+ style.setLeftBorderColor(IndexedColors.BLACK.getIndex());
|
|
|
+ style.setRightBorderColor(IndexedColors.BLACK.getIndex());
|
|
|
+
|
|
|
+ style.setAlignment(HorizontalAlignment.CENTER);
|
|
|
+ style.setVerticalAlignment(VerticalAlignment.CENTER);
|
|
|
+
|
|
|
+ // 字体加粗
|
|
|
+ Font font = xwb.createFont();
|
|
|
+ font.setBold(true);
|
|
|
+ style.setFont(font);
|
|
|
+
|
|
|
+ // 应用样式
|
|
|
+ cell.setCellStyle(style);
|
|
|
+
|
|
|
+ // 移动到下一列
|
|
|
+ currentColumnIndex++;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 强制刷新
|
|
|
- writer.flush();
|
|
|
+ OutputStream outputStream = response.getOutputStream();
|
|
|
+ // 清空response
|
|
|
+ response.reset();
|
|
|
+ response.setContentType("application/msexcel");//设置生成的文件类型
|
|
|
+ response.setCharacterEncoding("UTF-8");//设置文件头编码方式和文件名
|
|
|
+ response.setHeader("Content-Disposition", "attachment; "
|
|
|
+ + " filename=" + new String("报名表模板.xlsx".getBytes("utf-8"), "ISO8859-1"));
|
|
|
+ String origin = request.getHeader("Origin");
|
|
|
+ response.addHeader("Access-Control-Allow-Origin", origin);
|
|
|
+ xwb.write(outputStream);
|
|
|
} catch (IOException e) {
|
|
|
- e.printStackTrace();
|
|
|
+ log.error("下载Excel模板异常,异常信息为:【{}】", e.getMessage(), e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 导入报名表
|
|
|
+ * 使用poi导入报名表
|
|
|
*
|
|
|
* @param file
|
|
|
* @param eventId
|
|
|
* @return
|
|
|
*/
|
|
|
@Override
|
|
|
- public ResponseEntity<Map<String, Object>> importData(MultipartFile file, Long eventId) {
|
|
|
-
|
|
|
- Map<String, Object> result = new HashMap<>();
|
|
|
- List<String> errors = new ArrayList<>();
|
|
|
-
|
|
|
- if (file.isEmpty()) {
|
|
|
- errors.add("上传文件不能为空");
|
|
|
- result.put("success", false);
|
|
|
- result.put("errors", errors);
|
|
|
- return ResponseEntity.badRequest().body(result);
|
|
|
- }
|
|
|
-
|
|
|
- try (InputStream inputStream = file.getInputStream();
|
|
|
- BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
|
|
-
|
|
|
- // 1. 读取第一行:表头
|
|
|
- String headerLine = reader.readLine();
|
|
|
- if (headerLine == null) {
|
|
|
- errors.add("文件为空");
|
|
|
- result.put("success", false);
|
|
|
- result.put("errors", errors);
|
|
|
- return ResponseEntity.badRequest().body(result);
|
|
|
- }
|
|
|
-
|
|
|
- // 去除 BOM(如果存在)
|
|
|
- if (headerLine.startsWith("\uFEFF")) {
|
|
|
- headerLine = headerLine.substring(1);
|
|
|
- }
|
|
|
+ public Boolean importDataForPoi(MultipartFile file, Long eventId) {
|
|
|
+ //1.解析报名信息
|
|
|
+ List<EnrollProjectVo> enrollList = parseData(file);
|
|
|
+ //2.保存报名信息
|
|
|
+ this.saveEnrollData(enrollList, eventId);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
|
|
|
- String[] headers = headerLine.split(",", -1); // -1 保留空字段
|
|
|
+ private List<EnrollProjectVo> parseData(MultipartFile file) {
|
|
|
+ try {
|
|
|
+ List<EnrollProjectVo> enrolls = new ArrayList<>();
|
|
|
+ try (InputStream inputStream = file.getInputStream();
|
|
|
+ Workbook workbook = new XSSFWorkbook(inputStream)) {
|
|
|
+
|
|
|
+ Sheet sheet = workbook.getSheetAt(0);
|
|
|
+
|
|
|
+ // 获取第一行(表头),用于获取项目名称
|
|
|
+ Row headerRow = sheet.getRow(2);
|
|
|
+ // 动态确定有效最大列数(去除表头末尾的空列)
|
|
|
+ int lastCellIndex = findValidLastColumnIndex(headerRow);
|
|
|
+ List<String> projectNames = new ArrayList<>();
|
|
|
+
|
|
|
+ // 从第6列(F列,索引5)开始收集项目名称
|
|
|
+ for (int i = 6; i < lastCellIndex; i++) {
|
|
|
+ Cell cell = headerRow.getCell(i);
|
|
|
+ if (cell != null && cell.getStringCellValue() != null && !cell.getStringCellValue().trim().isEmpty()) {
|
|
|
+ projectNames.add(cell.getStringCellValue().trim());
|
|
|
+ } else {
|
|
|
+ projectNames.add("项目_" + i); // 防止空标题
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 添加调试日志,输出实际读取到的表头
|
|
|
- log.info("实际读取到的表头: {}", Arrays.toString(headers));
|
|
|
- log.info("表头行原始内容: '{}'", headerLine);
|
|
|
+ // 从第3行开始读数据
|
|
|
+ for (int i = 3; i <= sheet.getLastRowNum(); i++) {
|
|
|
+ Row row = sheet.getRow(i);
|
|
|
+ if (row == null) continue;
|
|
|
+ if (isRowEmpty(row, 0, lastCellIndex)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
- // 2. 获取动态项目列(从数据库或服务中)
|
|
|
- Map<String, List<String>> projectMap = gameEventProjectService.mapProjectTypeAndProject(eventId);
|
|
|
- List<String> validProjects = new ArrayList<>();
|
|
|
- for (List<String> list : projectMap.values()) {
|
|
|
- validProjects.addAll(list);
|
|
|
- }
|
|
|
+ EnrollProjectVo enroll = new EnrollProjectVo();
|
|
|
+ Map<String, Boolean> selections = new LinkedHashMap<>(); // 保持顺序
|
|
|
|
|
|
- // 3. 验证表头是否合法
|
|
|
- Set<String> expectedHeaders = new LinkedHashSet<>();
|
|
|
- // 添加固定字段(来自 EnrollProjectVo)
|
|
|
- Field[] fields = EnrollProjectVo.class.getDeclaredFields();
|
|
|
- for (Field field : fields) {
|
|
|
- if (field.isAnnotationPresent(ExcelProperty.class)) {
|
|
|
- ExcelProperty prop = field.getAnnotation(ExcelProperty.class);
|
|
|
- expectedHeaders.add(prop.value()[0]);
|
|
|
- }
|
|
|
- }
|
|
|
- expectedHeaders.addAll(validProjects); // 加上动态项目
|
|
|
+ // A列:姓名
|
|
|
+ Cell nameCell = row.getCell(0);
|
|
|
+ if (nameCell != null) {
|
|
|
+ enroll.setName(getCellValueAsString(nameCell));
|
|
|
+ }
|
|
|
|
|
|
- Set<String> actualHeaders = new LinkedHashSet<>(Arrays.asList(headers));
|
|
|
+ // B列:性别
|
|
|
+ Cell sexCell = row.getCell(1);
|
|
|
+ if (sexCell != null) {
|
|
|
+ enroll.setSex(getCellValueAsString(sexCell));
|
|
|
+ }
|
|
|
|
|
|
- // 改进表头验证逻辑
|
|
|
- if (!actualHeaders.equals(expectedHeaders)) {
|
|
|
- log.error("表头不匹配,期望: {}, 实际: {}", expectedHeaders, actualHeaders);
|
|
|
+ // C列:年龄
|
|
|
+ Cell ageCell = row.getCell(2);
|
|
|
+ if (ageCell != null) {
|
|
|
+ enroll.setAge(getCellValueAsString(ageCell));
|
|
|
+ }
|
|
|
|
|
|
- // 检查是否完全不匹配(可能文件格式错误)
|
|
|
- boolean hasAnyMatch = false;
|
|
|
- for (String actualHeader : actualHeaders) {
|
|
|
- if (expectedHeaders.contains(actualHeader)) {
|
|
|
- hasAnyMatch = true;
|
|
|
- break;
|
|
|
+ // D列:队伍名称
|
|
|
+ Cell teamCell = row.getCell(3);
|
|
|
+ if (teamCell != null) {
|
|
|
+ enroll.setTeamName(getCellValueAsString(teamCell));
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- if (!hasAnyMatch) {
|
|
|
- errors.add("上传的文件格式不正确,请使用系统生成的导入模板");
|
|
|
- errors.add("期望的表头: " + String.join(", ", expectedHeaders));
|
|
|
- errors.add("实际的表头: " + String.join(", ", actualHeaders));
|
|
|
- result.put("success", false);
|
|
|
- result.put("errors", errors);
|
|
|
- return ResponseEntity.badRequest().body(result);
|
|
|
- }
|
|
|
+ // E列:领队
|
|
|
+ Cell leaderCell = row.getCell(4);
|
|
|
+ if (leaderCell != null) {
|
|
|
+ enroll.setLeader(getCellValueAsString(leaderCell));
|
|
|
+ }
|
|
|
|
|
|
- // 如果部分匹配,给出详细的差异信息
|
|
|
- Set<String> missingHeaders = new LinkedHashSet<>(expectedHeaders);
|
|
|
- missingHeaders.removeAll(actualHeaders);
|
|
|
- Set<String> extraHeaders = new LinkedHashSet<>(actualHeaders);
|
|
|
- extraHeaders.removeAll(expectedHeaders);
|
|
|
+ // F列:联系方式
|
|
|
+ Cell phoneCell = row.getCell(5);
|
|
|
+ if (phoneCell != null) {
|
|
|
+ enroll.setPhone(getCellValueAsString(phoneCell));
|
|
|
+ }
|
|
|
|
|
|
- if (!missingHeaders.isEmpty()) {
|
|
|
- errors.add("缺少必要的表头字段: " + String.join(", ", missingHeaders));
|
|
|
- }
|
|
|
- if (!extraHeaders.isEmpty()) {
|
|
|
- log.warn("包含额外的表头字段: {}", String.join(", ", extraHeaders));
|
|
|
- }
|
|
|
+ // 从第6列开始读取项目名称
|
|
|
+ for (int j = 6; j < lastCellIndex; j++) {
|
|
|
+ Cell cell = row.getCell(j);
|
|
|
+ String projectName = projectNames.get(j - 6); // 对应项目名
|
|
|
+ boolean selected = isCellSelected(cell);
|
|
|
+ selections.put(projectName, selected);
|
|
|
+ }
|
|
|
|
|
|
- if (!errors.isEmpty()) {
|
|
|
- result.put("success", false);
|
|
|
- result.put("errors", errors);
|
|
|
- return ResponseEntity.badRequest().body(result);
|
|
|
+ enroll.setProjectSelections(selections);
|
|
|
+ enrolls.add(enroll);
|
|
|
}
|
|
|
}
|
|
|
+ return enrolls;
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
|
- // 4. 逐行读取数据
|
|
|
- String line;
|
|
|
- int lineNumber = 2; // 数据从第2行开始(第1行是表头)
|
|
|
- List<EnrollProjectVo> dataList = new ArrayList<>();
|
|
|
+ } finally {
|
|
|
|
|
|
- while ((line = reader.readLine()) != null) {
|
|
|
- line = line.trim();
|
|
|
- if (line.isEmpty()) continue;
|
|
|
+ }
|
|
|
+ return List.of();
|
|
|
+ }
|
|
|
|
|
|
- String[] values = line.split(",", -1); // 保持空值
|
|
|
- if (values.length != headers.length) {
|
|
|
- errors.add("第" + lineNumber + "行数据列数不匹配,期望" + headers.length + "列,实际" + values.length + "列");
|
|
|
- lineNumber++;
|
|
|
- continue;
|
|
|
- }
|
|
|
+ // 辅助方法:将 Cell 转为字符串
|
|
|
+ private static String getCellValueAsString(Cell cell) {
|
|
|
+ if (cell == null) return "";
|
|
|
|
|
|
- // 映射到 EnrollProjectVo
|
|
|
- EnrollProjectVo vo = mapRowToVo(headers, values, validProjects);
|
|
|
- if (vo != null) {
|
|
|
- dataList.add(vo);
|
|
|
+ switch (cell.getCellType()) {
|
|
|
+ case Cell.CELL_TYPE_STRING:
|
|
|
+ return cell.getStringCellValue().trim();
|
|
|
+ case Cell.CELL_TYPE_NUMERIC:
|
|
|
+ if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) {
|
|
|
+ return cell.getDateCellValue().toString();
|
|
|
} else {
|
|
|
- errors.add("第" + lineNumber + "行数据格式错误");
|
|
|
+ return String.valueOf((int) cell.getNumericCellValue()); // 或者保留小数用 double
|
|
|
}
|
|
|
+ case Cell.CELL_TYPE_BOOLEAN:
|
|
|
+ return String.valueOf(cell.getBooleanCellValue());
|
|
|
+ default:
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- lineNumber++;
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * 判断单元格是否表示“已选择”
|
|
|
+ * 支持:是、yes、true、1、✔、✅、√ 等
|
|
|
+ */
|
|
|
+ private static boolean isCellSelected(Cell cell) {
|
|
|
+ if (cell == null) return false;
|
|
|
|
|
|
- // 5. 数据校验 & 保存
|
|
|
- if (!errors.isEmpty()) {
|
|
|
- result.put("success", false);
|
|
|
- result.put("errors", errors);
|
|
|
- return ResponseEntity.badRequest().body(result);
|
|
|
- }
|
|
|
- log.info("读取到的数据:{}", dataList);
|
|
|
- // 调用 Service 层保存数据
|
|
|
- boolean saveSuccess = this.saveEnrollData(dataList, eventId);
|
|
|
- if (!saveSuccess) {
|
|
|
- result.put("success", false);
|
|
|
- result.put("errors", Arrays.asList("保存数据失败,请重试"));
|
|
|
- return ResponseEntity.status(500).body(result);
|
|
|
- }
|
|
|
+ // 先检查单元格类型
|
|
|
+ if (cell.getCellType() == Cell.CELL_TYPE_BOOLEAN) {
|
|
|
+ return cell.getBooleanCellValue();
|
|
|
+ }
|
|
|
+ if (cell.getCellType() == Cell.CELL_TYPE_NUMERIC) {
|
|
|
+ return cell.getNumericCellValue() == 1;
|
|
|
+ }
|
|
|
|
|
|
- log.info("成功读取的数据条数: {}", dataList.size());
|
|
|
+ String value = getCellValueAsString(cell).trim().toLowerCase();
|
|
|
+ return !value.isEmpty() &&
|
|
|
+ (value.equals("是") || value.equals("yes") || value.equals("true") ||
|
|
|
+ value.equals("1") || value.contains("✔") || value.contains("✅") ||
|
|
|
+ value.contains("√") || value.equals("×"));
|
|
|
+ }
|
|
|
|
|
|
- result.put("success", true);
|
|
|
- result.put("message", "导入成功,共导入 " + dataList.size() + " 条记录");
|
|
|
- result.put("data", dataList);
|
|
|
- return ResponseEntity.ok(result);
|
|
|
+ /**
|
|
|
+ * 判断某行在指定范围内是否为空(无有效数据)
|
|
|
+ *
|
|
|
+ * @param row 行对象
|
|
|
+ * @param startCol 起始列索引
|
|
|
+ * @param endCol 结束列索引(不包含)
|
|
|
+ * @return true 表示该行为空
|
|
|
+ */
|
|
|
+ private static boolean isRowEmpty(Row row, int startCol, int endCol) {
|
|
|
+ if (row == null) return true;
|
|
|
+
|
|
|
+ for (int i = startCol; i < endCol; i++) {
|
|
|
+ Cell cell = row.getCell(i);
|
|
|
+ if (cell != null) {
|
|
|
+ // 如果单元格不为null,检查其是否有非空值
|
|
|
+ String value = getCellValueAsString(cell);
|
|
|
+ if (value != null && !value.trim().isEmpty()) {
|
|
|
+ return false; // 有有效数据
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true; // 所有列都为空
|
|
|
+ }
|
|
|
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("导入数据时发生异常", e);
|
|
|
- errors.add("文件解析失败: " + e.getMessage());
|
|
|
- result.put("success", false);
|
|
|
- result.put("errors", errors);
|
|
|
- return ResponseEntity.status(500).body(result);
|
|
|
+ /**
|
|
|
+ * 找到行中最后一个“有效”单元格的索引(从右往左找第一个非空单元格)
|
|
|
+ *
|
|
|
+ * @param row 行对象
|
|
|
+ * @return 有效最大列索引(不包含),即 getLastCellNum() 的合理值
|
|
|
+ */
|
|
|
+ private static int findValidLastColumnIndex(Row row) {
|
|
|
+ if (row == null) return 0;
|
|
|
+
|
|
|
+ int lastCellNum = row.getLastCellNum(); // 物理最后一列
|
|
|
+ if (lastCellNum <= 0) return 0;
|
|
|
+
|
|
|
+ // 从右往左扫描,找到第一个非空单元格
|
|
|
+ for (int i = lastCellNum - 1; i >= 0; i--) {
|
|
|
+ Cell cell = row.getCell(i);
|
|
|
+ String value = getCellValueAsString(cell);
|
|
|
+ if (value != null && !value.trim().isEmpty()) {
|
|
|
+ return i + 1; // 返回有效列数(索引+1)
|
|
|
+ }
|
|
|
}
|
|
|
+ return 0; // 全为空
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -272,100 +371,104 @@ public class IEnrollServiceImpl implements IEnrollService {
|
|
|
*/
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
public boolean saveEnrollData(List<EnrollProjectVo> dataList, Long eventId) {
|
|
|
- // - 读取到的数据:[EnrollProjectVo(name=林啊啊啊, teamName=废物队,
|
|
|
- // sex=男, age=20, leader=少爷, phone=34567890,
|
|
|
- // ProjectSelections={4*400米接力=false, 4*100米接力=true, 跳高=true})]
|
|
|
- // 1.根据队伍生成号码段 五个一组
|
|
|
- // 1.2 根据队伍分类成Map
|
|
|
+ // 1. 根据队伍分类成Map
|
|
|
Map<String, List<EnrollProjectVo>> groupedByTeam = dataList.stream()
|
|
|
.collect(Collectors.groupingBy(EnrollProjectVo::getTeamName));
|
|
|
- // 1.3 根据队伍生成号码段(生成一个map key:teamName value:队员数量)
|
|
|
- Map<String, Long> teamCountMap = dataList.stream()
|
|
|
- .collect(Collectors.groupingBy(
|
|
|
- EnrollProjectVo::getTeamName,
|
|
|
- Collectors.counting()
|
|
|
- ));
|
|
|
- // 2.校验是否存在项目(如果项目不存在就无视)
|
|
|
- List<String> projects = projectsMap.get(eventId);
|
|
|
- // 3.保存参赛队员
|
|
|
- // 4.保存参赛队伍
|
|
|
+
|
|
|
+ // 1.2 根据队伍生成号码段
|
|
|
+ Map<String, String> numberRanges = new HashMap<>();
|
|
|
+ Map<String, AtomicInteger> currentNumbers = new HashMap<>(); // 记录每个队伍当前分配到的号码
|
|
|
+ AtomicInteger teamIndex = new AtomicInteger(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 key = entry.getKey();
|
|
|
- List<EnrollProjectVo> value = entry.getValue();
|
|
|
+ String teamName = entry.getKey();
|
|
|
+ List<EnrollProjectVo> athletes = entry.getValue();
|
|
|
+
|
|
|
GameTeamBo gameTeamBo = new GameTeamBo();
|
|
|
gameTeamBo.setEventId(eventId);
|
|
|
- gameTeamBo.setTeamName(key);
|
|
|
- // gameTeamBo.setTeamCode();
|
|
|
- gameTeamBo.setLeader(value.get(0).getLeader());
|
|
|
- gameTeamBo.setAthleteValue(JSONUtil.toJsonStr(value.stream().map(EnrollProjectVo::getName).collect(Collectors.toList())));
|
|
|
- gameTeamBo.setAthleteNum(Long.valueOf(value.size()));
|
|
|
- // gameTeamBo.setNumberRange();
|
|
|
+ gameTeamBo.setTeamName(teamName);
|
|
|
+ // gameTeamBo.setTeamCode("");
|
|
|
+ gameTeamBo.setLeader(athletes.get(0).getLeader());
|
|
|
+ gameTeamBo.setAthleteValue(JSONUtil.toJsonStr(athletes.stream().map(EnrollProjectVo::getName).collect(Collectors.toList())));
|
|
|
+ gameTeamBo.setAthleteNum(Long.valueOf(athletes.size()));
|
|
|
+ gameTeamBo.setNumberRange(numberRanges.get(teamName));
|
|
|
gameTeamBo.setStatus("0");
|
|
|
gameTeamService.insertByBo(gameTeamBo);
|
|
|
+
|
|
|
+ Long teamId = gameTeamBo.getTeamId();
|
|
|
+
|
|
|
+ // 获取该队伍的当前编号计数器
|
|
|
+ AtomicInteger currentNumber = currentNumbers.get(teamName);
|
|
|
+ int width = (dataList.size() > 100) ? 5 : 4; // 决定格式化宽度
|
|
|
+
|
|
|
+ // 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);
|
|
|
+ gameAthleteBo.setProjectValue(JSONUtil.toJsonStr(enrollInfo.getProjectSelections()));
|
|
|
+ gameAthleteBo.setStatus("0");
|
|
|
+
|
|
|
+ gameAthleteService.insertByBo(gameAthleteBo);
|
|
|
+ }
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
|
|
|
/**
|
|
|
- * 映射
|
|
|
+ * 生成号码段
|
|
|
+ * 需求:
|
|
|
+ * 不超100人,一般4位。0101-0199;超过100人,一般5位,01001-01101;
|
|
|
+ * 根据队伍分配,最多不超过300个
|
|
|
+ * 规则:
|
|
|
+ * 1. 按照队伍分类,每个队伍生成一个号码段
|
|
|
+ * 2. 直接给每个队伍分配300个号码,多余的预留起来,防止后续有新的人加入该队伍参赛,预留号码可直接分配给他
|
|
|
+ * 3. 号码从1开始递增,高位填充0
|
|
|
*
|
|
|
- * @param headers
|
|
|
- * @param values
|
|
|
- * @param validProjects
|
|
|
- * @return
|
|
|
+ * @param member 人数
|
|
|
+ * @param team 当前是第几队
|
|
|
+ * @return 号码段字符串,如 "0001-0300" 或 "00001-00300"
|
|
|
*/
|
|
|
- private EnrollProjectVo mapRowToVo(String[] headers, String[] values, List<String> validProjects) {
|
|
|
- EnrollProjectVo vo = new EnrollProjectVo();
|
|
|
+ public static String generateNumberRange(int member, int team) {
|
|
|
+ // 每个队伍固定分配300个号码
|
|
|
+ int start = 1 + (team - 1) * 300;
|
|
|
+ int end = 300 * team;
|
|
|
|
|
|
- try {
|
|
|
- for (int i = 0; i < headers.length; i++) {
|
|
|
- String header = headers[i];
|
|
|
- String value = i < values.length ? values[i].trim() : "";
|
|
|
-
|
|
|
- // 判断是否是固定字段
|
|
|
- boolean matched = false;
|
|
|
- Field[] fields = EnrollProjectVo.class.getDeclaredFields();
|
|
|
- for (Field field : fields) {
|
|
|
- if (field.isAnnotationPresent(ExcelProperty.class)) {
|
|
|
- ExcelProperty prop = field.getAnnotation(ExcelProperty.class);
|
|
|
- if (prop.value()[0].equals(header)) {
|
|
|
- field.setAccessible(true);
|
|
|
- // 简单类型转换(可扩展)
|
|
|
- if (field.getType() == String.class) {
|
|
|
- field.set(vo, value.isEmpty() ? null : value);
|
|
|
- } else if (field.getType() == Integer.class || field.getType() == int.class) {
|
|
|
- field.set(vo, value.isEmpty() ? null : Integer.valueOf(value));
|
|
|
- } else if (field.getType() == Long.class || field.getType() == long.class) {
|
|
|
- field.set(vo, value.isEmpty() ? null : Long.valueOf(value));
|
|
|
- }
|
|
|
- matched = true;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ // 判断使用4位还是5位
|
|
|
+ int width = (member > 100) ? 5 : 4;
|
|
|
|
|
|
- // 如果不是固定字段,则视为“项目报名”字段
|
|
|
- if (!matched && validProjects.contains(header)) {
|
|
|
- // 假设 value 是“是/否”、“1/0”、“√”等
|
|
|
- boolean enrolled = isTrueValue(value);
|
|
|
- // 使用 map 或其他结构存储项目报名状态
|
|
|
- if (vo.getProjectSelections() == null) {
|
|
|
- vo.setProjectSelections(new HashMap<>());
|
|
|
- }
|
|
|
- vo.getProjectSelections().put(header, enrolled);
|
|
|
- }
|
|
|
- }
|
|
|
- return vo;
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("映射行数据失败", e);
|
|
|
- return null;
|
|
|
- }
|
|
|
- }
|
|
|
+ String startStr = String.format("%0" + width + "d", start);
|
|
|
+ String endStr = String.format("%0" + width + "d", end);
|
|
|
|
|
|
+ return startStr + "-" + endStr;
|
|
|
+ }
|
|
|
|
|
|
- // 判断是否为“真值”
|
|
|
- private boolean isTrueValue(String val) {
|
|
|
- return Arrays.asList("是", "√", "1", "true", "报名", "已报").contains(val);
|
|
|
+ public static void main(String[] args) {
|
|
|
+ for (int i = 1; i <= 10; i++) {
|
|
|
+ System.out.println(generateNumberRange(100, i));
|
|
|
+ }
|
|
|
}
|
|
|
}
|