wenkai 2 долоо хоног өмнө
parent
commit
857170865d

+ 6 - 34
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/EnrollController.java

@@ -4,6 +4,7 @@ import cn.idev.excel.EasyExcel;
 import cn.idev.excel.annotation.ExcelProperty;
 import cn.idev.excel.context.WriteContext;
 import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -42,39 +43,9 @@ public class EnrollController {
 
     private final IEnrollService enrollService;
 
-    // @PostMapping("/importTemplate")
-    // public void importTemplate(HttpServletResponse response) throws IOException {
-    //     // 获取动态项目(赛事项目)
-    //     Map<String, ArrayList<String>> projectMap = gameEventProjectService.mapProjectTypeAndProject();
-    //     List<String> project = new ArrayList<>();
-    //     for (Map.Entry<String, ArrayList<String>> entry : projectMap.entrySet()) {
-    //         project.addAll(entry.getValue());
-    //     }
-    //     // 反射构建属性
-    //     Class<EnrollProjectVo> enrollClass = EnrollProjectVo.class;
-    //     String var = "var";
-    //     AtomicInteger count = new AtomicInteger(1);
-    //     for (String p : project) {
-    //         try {
-    //             Field newField = enrollClass.getDeclaredField(var + count.addAndGet(1));
-    //             //给字段添加 @ExcelProperty 注解,value为p
-    //         } catch (NoSuchFieldException e) {
-    //             log.error("字段不存在:", p);
-    //         }
-    //     }
-    //
-    //     // 创建Excel工作簿
-    //
-    //
-    //     // 输出文件到客户端
-    //     ServletOutputStream out = response.getOutputStream();
-    //     out.flush();
-    //     out.close();
-    // }
-
     @PostMapping("/importTemplate")
-    public void importTemplate(HttpServletResponse response, Long eventId) throws IOException {
-        enrollService.downloadTemplate(response, eventId);
+    public void importTemplate(HttpServletRequest request, HttpServletResponse response, Long eventId) throws IOException {
+        enrollService.downloadTemplateForPoi(request, response, eventId);
     }
 
 
@@ -82,9 +53,10 @@ public class EnrollController {
      * 导入报名表
      */
     @PostMapping("/importData/{eventId}")
-    public ResponseEntity<Map<String, Object>> importData(
+    public R<String> importData(
         @RequestParam("file") MultipartFile file,
         @PathVariable Long eventId) {
-        return enrollService.importData(file, eventId);
+        Boolean result = enrollService.importDataForPoi(file, eventId);
+        return R.ok(result ? "导入成功" : "导入失败");
     }
 }

+ 5 - 7
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IEnrollService.java

@@ -1,5 +1,6 @@
 package org.dromara.system.service;
 
+import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.multipart.MultipartFile;
@@ -9,20 +10,17 @@ import java.util.Map;
 public interface IEnrollService {
 
     /**
-     * 下载报名表模板
-     *
+     * 使用poi生成报名表模板
      * @param response
      * @param eventId
      */
-    void downloadTemplate(HttpServletResponse response, Long eventId);
-
+    void downloadTemplateForPoi(HttpServletRequest request, HttpServletResponse response, Long eventId);
 
     /**
-     * 导入报名表
-     *
+     * 使用poi导入报名表
      * @param file
      * @param eventId
      * @return
      */
-    ResponseEntity<Map<String, Object>> importData(MultipartFile file, Long eventId);
+    Boolean importDataForPoi(MultipartFile file, Long eventId);
 }

+ 361 - 258
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/IEnrollServiceImpl.java

@@ -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));
+        }
     }
 }

BIN
ruoyi-modules/ruoyi-game-event/src/main/resources/template/报名表模板.xlsx