Procházet zdrojové kódy

refactor(game-event): 优化参赛证生成功能并清理无用代码

- 移除未使用的导入和类,包括Pdf相关、文件操作和异步处理类
- 简化二维码生成逻辑,使用QRCodeWriter替代MultiFormatWriter
- 优化参赛证模板处理,改进图像绘制和尺寸调整逻辑
- 修复Logo绘制功能,添加文件存在性检查和坐标处理优化
- 移除已注释的无用代码和方法,提升代码可读性
- 改进错误处理和日志记录,增强系统稳定性
zhou před 3 měsíci
rodič
revize
968c1fa9b9

+ 6 - 2
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java

@@ -99,8 +99,12 @@ public class OssClient {
                     .connectionTimeout(Duration.ofSeconds(60))
                     .writeTimeout(Duration.ofMinutes(5))
                     .readTimeout(Duration.ofMinutes(5))
-                    .maxConcurrency(50)
-                    .maxPendingConnectionAcquires(100)
+                    // 增加最大并发连接数
+                    .maxConcurrency(100)
+                    // 增加最大待处理连接获取数
+                    .maxPendingConnectionAcquires(200)
+                    // 增加连接获取超时时间
+                    .connectionAcquisitionTimeout(Duration.ofSeconds(30))
                     .build())
                 .build();
 

+ 42 - 19
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/NumberController.java

@@ -301,7 +301,7 @@ public class NumberController {
             g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
 
             // 将原始背景图片拉伸/缩放到目标画布尺寸
-            // g2d.drawImage(originalBgImage, 0, 0, targetWidth, targetHeight, null);
+            g2d.drawImage(originalBgImage, 0, 0, targetWidth, targetHeight, null);
             log.info("背景图片已调整到目标尺寸: {}x{}", targetWidth, targetHeight);
 
             // 绘制Logo(如果存在)
@@ -339,7 +339,26 @@ public class NumberController {
      */
     private void drawLogoOnTemplate(Graphics2D g2d, String logoImagePath, GenerateBibBo bibParam, int canvasWidth, int canvasHeight) {
         try {
-            BufferedImage logoImage = ImageIO.read(new File(logoImagePath));
+            // 检查Logo图片路径是否为空
+            if (logoImagePath == null || logoImagePath.isEmpty()) {
+                log.info("Logo图片路径为空,跳过Logo绘制");
+                return;
+            }
+
+            // 检查Logo文件是否存在
+            File logoFile = new File(logoImagePath);
+            if (!logoFile.exists()) {
+                log.warn("Logo图片文件不存在: {}", logoImagePath);
+                return;
+            }
+
+            // 读取Logo图片
+            BufferedImage logoImage = ImageIO.read(logoFile);
+            if (logoImage == null) {
+                log.warn("无法加载Logo图片: {}", logoImagePath);
+                return;
+            }
+
             log.info("Logo图片尺寸: {}x{}", logoImage.getWidth(), logoImage.getHeight());
 
             // 计算Logo位置(直接使用前端传入的百分比坐标)
@@ -348,10 +367,10 @@ public class NumberController {
 
             int x, y;
             if (logoX != null && logoY != null) {
-                // 将百分比坐标转换为实际图片像素坐标
-                x = (int) (logoX * canvasWidth / 100.0);
-                y = (int) (logoY * canvasHeight / 100.0);
-                log.info("Logo坐标转换 - 百分比坐标: ({}, {}), 实际图片坐标: ({}, {})",
+                // 直接使用前端传入的像素坐标
+                x = logoX.intValue();
+                y = logoY.intValue();
+                log.info("Logo坐标转换 - 前端传入像素坐标: ({}, {}), 实际图片坐标: ({}, {})",
                     logoX, logoY, x, y);
             } else {
                 // 使用默认位置(左上角)
@@ -368,13 +387,11 @@ public class NumberController {
                 scaledHeight = bibParam.getLogoHeight();
                 log.info("使用前端计算的Logo尺寸: {}x{}", scaledWidth, scaledHeight);
             }
-            // else {
-            //     // 回退到原始缩放逻辑(兼容旧版本)
-            //     Double logoScale = bibParam.getLogoScale() != null ? bibParam.getLogoScale() : 1.0;
-            //     scaledWidth = (int) (logoImage.getWidth() * logoScale);
-            //     scaledHeight = (int) (logoImage.getHeight() * logoScale);
-            //     log.warn("使用回退缩放逻辑,Logo尺寸: {}x{}", scaledWidth, scaledHeight);
-            // }
+
+            // 实现前端的translateX(-50%)水平居中效果
+            if (logoX != null) {
+                x = x - scaledWidth / 2;
+            }
 
             // 边界检查,确保Logo不会超出图片范围
             if (x < 0) x = 0;
@@ -402,14 +419,20 @@ public class NumberController {
             int y = bibParam.getEventY() != null ? bibParam.getEventY().intValue() : canvasHeight / 4;
 
             // 使用前端传递的字体大小
-            int fontSize = bibParam.getEventFontSize() != null ? bibParam.getEventFontSize() : 28;
-
-            // 使用固定字体 "黑体" 与前端保持一致
-            Font font = new Font("黑体", Font.BOLD, fontSize);
+            int fontSize = bibParam.getEventFontSize() != null ? bibParam.getEventFontSize() : 36;
+
+            // 使用前端传递的字体名称,否则使用默认字体 "SimHei"
+            String fontName = bibParam.getFontName();
+            if (fontName == null) {
+                fontName = "SimHei";
+            } else if ("simhei".equalsIgnoreCase(fontName)) {
+                fontName = "SimHei";
+            }
+            Font font = new Font(fontName, Font.BOLD, fontSize);
             g2d.setFont(font);
 
-            // 设置颜色 - 与前端保持一致(固定黑色)
-            Color color = Color.BLACK;
+            // 设置颜色
+            Color color = new Color(bibParam.getFontColor());
             g2d.setColor(color);
 
             // 绘制赛事名称

+ 90 - 625
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameEventServiceImpl.java

@@ -1,29 +1,19 @@
 package org.dromara.system.service.impl;
 
 import cn.hutool.core.collection.CollectionUtil;
-import cn.hutool.core.img.FontUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
-import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.google.zxing.BarcodeFormat;
 import com.google.zxing.EncodeHintType;
-import com.google.zxing.MultiFormatWriter;
 import com.google.zxing.common.BitMatrix;
-import com.itextpdf.text.BaseColor;
-import com.itextpdf.text.Document;
-import com.itextpdf.text.Image;
-import com.itextpdf.text.Rectangle;
-import com.itextpdf.text.pdf.BaseFont;
-import com.itextpdf.text.pdf.PdfContentByte;
-import com.itextpdf.text.pdf.PdfWriter;
+import com.google.zxing.qrcode.QRCodeWriter;
+import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
 import jakarta.annotation.Resource;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
-import jakarta.validation.constraints.NotNull;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.io.IOUtils;
 import org.apache.commons.io.output.ByteArrayOutputStream;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.poi.ss.usermodel.*;
@@ -35,14 +25,10 @@ import org.dromara.common.json.utils.JsonUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.redis.utils.RedisUtils;
-import org.dromara.system.domain.GameAthlete;
 import org.dromara.system.domain.GameEvent;
-import org.dromara.system.domain.GameEventGroup;
-import org.dromara.system.domain.PdfEntry;
 import org.dromara.system.domain.bo.*;
 import org.dromara.system.domain.constant.GameEventConstant;
 import org.dromara.system.domain.vo.*;
-import org.dromara.system.mapper.GameAthleteMapper;
 import org.dromara.system.mapper.GameEventMapper;
 import org.dromara.system.config.FileUploadConfig;
 import org.dromara.system.service.*;
@@ -65,12 +51,9 @@ import java.awt.Color;
 import java.awt.Font;
 import java.awt.image.BufferedImage;
 import java.io.*;
-import java.nio.file.Files;
-import java.nio.file.Paths;
 import java.time.Duration;
 import java.util.*;
 import java.util.List;
-import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
@@ -541,8 +524,6 @@ public class GameEventServiceImpl implements IGameEventService {
     /**
      * 统计赛事个数
      * 0:所有 1:未开始 2:进行中 3:已结束
-     *
-     * @return
      */
     @Override
     public Long countGameEvent(Long type) {
@@ -551,9 +532,6 @@ public class GameEventServiceImpl implements IGameEventService {
 
     /**
      * 获取默认赛事的号码对照表
-     *
-     * @param eventId
-     * @return
      */
     @Override
     public List<AthleteNumberTableVO> getNumberTable(Long eventId) {
@@ -590,19 +568,8 @@ public class GameEventServiceImpl implements IGameEventService {
         return numberTable;
     }
 
-
-    /**
-     * 使用poi生成号码对照表
-     *
-     * @param response
-     * @param eventId
-     */
     /**
      * 使用poi生成号码对照表
-     *
-     * @param request
-     * @param response
-     * @param eventId
      */
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -810,190 +777,48 @@ public class GameEventServiceImpl implements IGameEventService {
         }
     }
 
-//     @Override
-//     public void generateNumberBib(HttpServletResponse response,
-//                                   MultipartFile bgImage, MultipartFile logo,
-//                                   GenerateBibBo bibParam) {
-//         if (bgImage!=null&&!isImage(bgImage)) {
-//             throw new ServiceException("背景图不是图片格式");
-//         }
-//         if (logo!=null&&!isImage(logo)) {
-//             throw new ServiceException("logo不是图片格式");
-//         }
-//         //1.查询当前赛事所有队员数据
-//         GameAthleteBo gameAthleteBo = new GameAthleteBo();
-//         Object cacheObject = RedisUtils.getCacheObject(GameEventConstant.DEFAULT_EVENT_ID);
-//         Long defaultEventId = Long.valueOf(cacheObject.toString());
-//         gameAthleteBo.setEventId(defaultEventId);
-//         List<GameAthleteVo> athleteVoList = gameAthleteService.queryList(gameAthleteBo);
-//         if(CollectionUtil.isEmpty(athleteVoList)){
-//             throw new ServiceException("当前赛事没有队员数据,无法生成号码布");
-//         }
-//         //2.提前查询队员队伍名称缓存
-//         Set<Long> teamIds = athleteVoList.stream()
-//             .map(GameAthleteVo::getTeamId)
-//             .collect(Collectors.toSet());
-//         Map<Long, String> teamNameMap = gameTeamService.queryTeamIdAndName(teamIds);
-//         //3.查询赛事所有项目缓存
-//         Map<Long, String> projectMap = gameEventProjectService.queryListByEventId(defaultEventId)
-//             .stream().collect(Collectors.toMap(GameEventProjectVo::getProjectId, GameEventProjectVo::getProjectName));
-//         //4.查询赛事组别
-//         GameEventGroup gameEventGroup = gameEventGroupService.queryByEventId(defaultEventId);
-//         //5.根据参数生成号码布
-// //        GameEventVo eventVo = baseMapper.selectVoById(defaultEventId);
-// //        generateBib(response, bgImage, logo, eventVo.getEventName(), gameEventGroup.getGroupName(), athleteVoList, teamNameMap, projectMap, bibParam);
-//         generateBib(response, bgImage, logo, bibParam.getEventName(), gameEventGroup.getGroupName(), athleteVoList, teamNameMap, projectMap, bibParam);
-//     }
-
-    /**
-     * 判断是否为图片
-     * @param file
-     * @return
-     */
-    public static boolean isImage(MultipartFile file) {
-        if (file == null || file.isEmpty()) {
-            return false;
-        }
-
-        try (InputStream is = file.getInputStream()) {
-            byte[] header = new byte[8]; // 读取前8字节足够判断大部分图片
-            int bytesRead = is.read(header);
-            if (bytesRead < 4) {
-                return false;
-            }
-
-            StringBuilder hexHeader = new StringBuilder();
-            for (int i = 0; i < bytesRead; i++) {
-                hexHeader.append(String.format("%02X", header[i] & 0xFF));
-            }
-
-            String headerStr = hexHeader.toString();
-            for (String prefix : IMAGE_HEADER_PREFIXES) {
-                if (headerStr.startsWith(prefix)) {
-                    return true;
-                }
-            }
-
-            return false;
-
-        } catch (IOException e) {
-            return false;
-        }
-    }
-    
-    /**
-     * 生成二维码数据
-     *
-     * @param eventName
-     * @param groupName
-     * @param teamNameMap
-     * @param projectMap
-     * @param athlete
-     * @return
-     */
-    @NotNull
-    private static String getQrDataStr(String eventName, String groupName, Map<Long, String> teamNameMap, Map<Long, String> projectMap, GameAthleteVo athlete) {
-        try {
-            // 处理参加项目
-            StringJoiner joiner = new StringJoiner("、"); // 指定分隔符
-            athlete.getProjectList().forEach(projectId -> {
-                String projectName = projectMap.getOrDefault(Long.valueOf(projectId),"");
-                if (projectName != null && !projectName.isEmpty()) {
-                    joiner.add(projectName);
-                }
-            });
-            String xiangmu = joiner.toString();
-
-            // 获取运动员姓名并进行URL编码
-            String encodedName = java.net.URLEncoder.encode(athlete.getName(), "UTF-8");
-
-            // 获取赛事ID(从athlete对象中获取)
-            Long shId = athlete.getEventId();
-
-            // 生成符合要求格式的URL查询参数字符串
-            String qrData = String.format(
-                "id=%s&name=%s&gender=%s&shId=%d&shName=%s&dwName=%s&xiangmu=%s",
-                athlete.getAthleteCode(), // 队员号码(唯一ID)
-                encodedName, // 队员姓名(URL编码)
-                athlete.getGender(), // 性别
-                shId, // 赛事ID
-                eventName, // 赛事名称
-                teamNameMap.getOrDefault(athlete.getTeamId(), ""), // 队伍名称
-                xiangmu // 参与项目
-            );
-
-            return qrData;
-        } catch (Exception e) {
-            // 如果编码失败,返回基本信息
-            log.error("生成二维码数据失败(编码失败): {}", e.getMessage(), e);
-            return String.format("id=%s&name=%s", athlete.getAthleteCode(), athlete.getName());
-        }
-    }
-
-    // 工具方法:添加文本
-    private static void addText(PdfContentByte cb, BaseFont font, int size, BaseColor color, float x, float y, String text) {
-        cb.beginText();
-        cb.setFontAndSize(font, size);
-        cb.setColorFill(color);
-        cb.setTextMatrix(x, y);
-        cb.showText(text);
-        cb.endText();
-    }
-
     // 工具方法:生成二维码
     private static byte[] generateQRCode(String data, int width, int height) throws Exception {
+        // 使用QRCodeWriter而不是MultiFormatWriter,以便更好地控制二维码生成
+        QRCodeWriter qrCodeWriter = new QRCodeWriter();
+        
         Map<EncodeHintType, Object> hints = new HashMap<>();
         hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
-        BitMatrix matrix = new MultiFormatWriter().encode(data, BarcodeFormat.QR_CODE, width, height, hints);
-
-        // 创建BufferedImage并设置透明度
-        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+        // 设置纠错级别为L(最小纠错,最大数据容量)
+        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
+        // 设置边距为0,确保二维码内容填满指定尺寸
+        hints.put(EncodeHintType.MARGIN, 0);
+        
+        // 计算合适的二维码版本,确保所有二维码使用相同的版本
+        // 版本5对应37x37模块,足够容纳所有运动员信息
+        // 使用QRCodeWriter.encode方法直接生成指定尺寸的矩阵
+        BitMatrix matrix = qrCodeWriter.encode(
+            data, 
+            BarcodeFormat.QR_CODE, 
+            width, 
+            height, 
+            hints
+        );
+        
+        // 创建最终尺寸的图像
+        BufferedImage qrImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+        
+        // 直接在最终尺寸的图像上绘制二维码数据
+        // 确保每个像素都被正确设置,避免缩放差异
         for (int x = 0; x < width; x++) {
             for (int y = 0; y < height; y++) {
-                // 如果该位置是背景,则设置为完全透明;如果是数据点,则保持不透明
+                // 黑色数据点,透明背景
                 int color = matrix.get(x, y) ? 0xFF000000 : 0x00000000;
-                image.setRGB(x, y, color);
+                qrImage.setRGB(x, y, color);
             }
         }
-
-        // 将BufferedImage写入ByteArrayOutputStream
+        
+        // 将图像写入ByteArrayOutputStream
         ByteArrayOutputStream out = new ByteArrayOutputStream();
-        ImageIO.write(image, "png", out);
+        ImageIO.write(qrImage, "png", out);
         return out.toByteArray();
     }
 
-    // 工具方法:解析颜色 (0xRRGGBB)
-    private static BaseColor parseColor(Integer colorInt) {
-        if (colorInt == null) return BaseColor.BLACK;
-        return new BaseColor(
-            (colorInt >> 16) & 0xFF,
-            (colorInt >> 8) & 0xFF,
-            colorInt & 0xFF
-        );
-    }
-
-    // 工具方法:获取中文字体(推荐将字体文件打包进 resources)
-    private static BaseFont getChineseFont(String fontName) throws Exception {
-        // 方式1:使用系统字体(Windows)
-        // return BaseFont.createFont("C:/Windows/Fonts/simhei.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
-        String filePath;
-        // 处理null值,使用默认字体
-        if (fontName == null) {
-            fontName = "yahei";
-        }
-        switch (fontName) {
-            case "simhei" -> filePath = "fonts/simhei.ttf";
-            case "simsun" -> filePath = "fonts/simsun.ttf";
-            default -> filePath = "fonts/yahei.ttf";
-        }
-        try (InputStream in = FontUtil.class.getClassLoader().getResourceAsStream(filePath)) {
-            if (in == null) throw new FileNotFoundException(filePath);
-            File temp = File.createTempFile("font_", ".ttf");
-            temp.deleteOnExit();
-            IOUtils.copy(in, new FileOutputStream(temp));
-            return BaseFont.createFont(temp.getAbsolutePath(), BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
-        }
-    }
 
     /**
      * 获取赛事项目进度信息
@@ -1218,323 +1043,6 @@ public class GameEventServiceImpl implements IGameEventService {
         return earliestGroupTime != null ? earliestGroupTime : project.getStartTime();
     }
 
-    /**
-     * 异步生成参赛证
-     *
-     * @param taskId        任务ID
-     * @param eventId       赛事ID
-     * @param bgImagePath   背景图片路径
-     * @param logoImagePath Logo图片路径
-     * @param bibParam      参赛证参数
-     */
-    // @Override
-    // @Async
-    // public void generateNumberBibAsync(Long taskId, Long eventId, String bgImagePath,
-    //                                  String logoImagePath, GenerateBibBo bibParam) {
-    //     try {
-    //         // 更新任务状态为运行中
-    //         gameBibTaskService.updateTaskStatus(taskId, "0", null);
-
-    //         // 查询运动员数据
-    //         GameAthleteBo gameAthleteBo = new GameAthleteBo();
-    //         gameAthleteBo.setEventId(eventId);
-    //         List<GameAthleteVo> athleteVoList = gameAthleteService.queryList(gameAthleteBo);
-
-    //         if (CollectionUtil.isEmpty(athleteVoList)) {
-    //             gameBibTaskService.updateTaskStatus(taskId, "3", "当前赛事没有队员数据");
-    //             return;
-    //         }
-
-    //         // 更新总数量
-    //         gameBibTaskService.updateTaskProgress(taskId, 0, 0);
-
-    //         // 生成参赛证文件
-    //         String resultFilePath = generateBibFilesAsync(taskId, eventId, bgImagePath, logoImagePath, bibParam, athleteVoList);
-
-    //         // 完成任务
-    //         gameBibTaskService.completeTask(taskId, resultFilePath);
-
-    //     } catch (Exception e) {
-    //         log.error("参赛证生成失败,taskId: {}", taskId, e);
-    //         gameBibTaskService.updateTaskStatus(taskId, "3", e.getMessage());
-    //     }
-    // }
-
-    /**
-     * 异步生成参赛证文件
-     */
-    // private String generateBibFilesAsync(Long taskId, Long eventId, String bgImagePath, String logoImagePath,
-    //                                    GenerateBibBo bibParam, List<GameAthleteVo> athleteVoList) {
-    //     try {
-    //         // 创建结果目录
-    //         String resultDir = fileUploadConfig.getBibResultPath() + taskId + File.separator;
-    //         File resultDirFile = new File(resultDir);
-    //         if (!resultDirFile.exists()) {
-    //             boolean created = resultDirFile.mkdirs();
-    //             if (!created) {
-    //                 throw new RuntimeException("无法创建结果目录: " + resultDirFile.getAbsolutePath());
-    //             }
-    //         }
-
-    //         // 图片路径已经传入,直接使用
-
-    //         // 查询队伍名称缓存
-    //         Set<Long> teamIds = athleteVoList.stream()
-    //             .map(GameAthleteVo::getTeamId)
-    //             .collect(Collectors.toSet());
-    //         Map<Long, String> teamNameMap = gameTeamService.queryTeamIdAndName(teamIds);
-
-    //         // 查询赛事所有项目缓存
-    //         Map<Long, String> projectMap = gameEventProjectService.queryListByEventId(eventId)
-    //             .stream().collect(Collectors.toMap(GameEventProjectVo::getProjectId, GameEventProjectVo::getProjectName));
-
-    //         // 查询赛事组别
-    //         GameEventGroup gameEventGroup = gameEventGroupService.queryByEventId(eventId);
-
-    //         // 生成PDF文件
-    //         List<PdfEntry> pdfEntries = generatePdfEntries(bgImagePath, logoImagePath, bibParam, athleteVoList, teamNameMap, projectMap, gameEventGroup.getGroupName());
-
-    //         // 创建ZIP文件
-    //         String zipFilePath = resultDir + "athlete_bibs.zip";
-    //         try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFilePath))) {
-    //             for (PdfEntry entry : pdfEntries) {
-    //                 zos.putNextEntry(new ZipEntry(entry.getFileName()));
-    //                 zos.write(entry.getPdfBytes());
-    //                 zos.closeEntry();
-    //             }
-    //             zos.flush();
-    //         }
-
-    //         return zipFilePath;
-
-    //     } catch (Exception e) {
-    //         log.error("生成参赛证文件失败,taskId: {}", taskId, e);
-    //         throw new RuntimeException("生成参赛证文件失败: " + e.getMessage());
-    //     }
-    // }
-
-
-    /**
-     * 生成PDF条目列表
-     */
-    // private List<PdfEntry> generatePdfEntries(String bgImagePath, String logoImagePath,
-    //                                         GenerateBibBo bibParam, List<GameAthleteVo> athleteVoList,
-    //                                         Map<Long, String> teamNameMap, Map<Long, String> projectMap,
-    //                                         String groupName) {
-    //     try {
-    //         // 从文件路径读取图片
-    //         byte[] backgroundImageBytes = Files.readAllBytes(Paths.get(bgImagePath));
-    //         byte[] logoImageBytes = logoImagePath != null ? Files.readAllBytes(Paths.get(logoImagePath)) : null;
-
-    //         // 读取背景图
-    //         Image originalBgImageObj = Image.getInstance(backgroundImageBytes);
-    //         float originalWidth = originalBgImageObj.getWidth();
-    //         float originalHeight = originalBgImageObj.getHeight();
-
-    //         // 获取目标画布尺寸
-    //         float pageWidth = bibParam.getCanvasWidth() != null ? bibParam.getCanvasWidth().floatValue() : originalWidth;
-    //         float pageHeight = bibParam.getCanvasHeight() != null ? bibParam.getCanvasHeight().floatValue() : originalHeight;
-
-    //         log.info("异步生成 - 原始背景图片尺寸: {}x{}, 目标画布尺寸: {}x{}", originalWidth, originalHeight, pageWidth, pageHeight);
-
-    //         // 设置字体和颜色,提供默认值
-    //         String fontName = bibParam.getFontName() != null ? bibParam.getFontName() : "yahei";
-    //         BaseFont baseFont = getChineseFont(fontName);
-    //         BaseColor textColor = parseColor(bibParam.getFontColor());
-    //         Integer fontSize = bibParam.getFontSize() != null ? bibParam.getFontSize() : 14;
-
-    //         // 并行生成所有PDF
-    //         List<CompletableFuture<PdfEntry>> pdfFutures = athleteVoList.parallelStream()
-    //             .map(athlete -> CompletableFuture.supplyAsync(() -> {
-    //                 try {
-    //                     ByteArrayOutputStream pdfStream = new ByteArrayOutputStream();
-    //                     Document document = new Document(new Rectangle(pageWidth, pageHeight));
-    //                     PdfWriter writer = PdfWriter.getInstance(document, pdfStream);
-    //                     document.open();
-    //                     PdfContentByte cb = writer.getDirectContent();
-
-    //                     // 添加背景图(缩放到目标尺寸)
-    //                     Image bg = Image.getInstance(backgroundImageBytes);
-    //                     bg.scaleToFit(pageWidth, pageHeight);
-    //                     bg.setAbsolutePosition(0, 0);
-    //                     document.add(bg);
-
-    //                     // 添加Logo(如果存在)
-    //                     if (logoImageBytes != null && bibParam.getLogoX() != null && bibParam.getLogoY() != null) {
-    //                         try {
-    //                             Image img = Image.getInstance(logoImageBytes);
-    //                             // 使用传入的Logo尺寸(前端已计算好最终尺寸)
-    //                             float scaledWidth, scaledHeight;
-    //                             if (bibParam.getLogoWidth() != null && bibParam.getLogoHeight() != null) {
-    //                                 // 使用前端传入的Logo尺寸(已包含所有缩放计算)
-    //                                 scaledWidth = bibParam.getLogoWidth().floatValue();
-    //                                 scaledHeight = bibParam.getLogoHeight().floatValue();
-    //                                 log.debug("异步生成 - 使用前端计算的Logo尺寸: {}x{}", scaledWidth, scaledHeight);
-    //                             } else {
-    //                                 // 回退到原始缩放逻辑(兼容旧版本)
-    //                                 Double logoScale = bibParam.getLogoScale() != null ? bibParam.getLogoScale() : 1.0;
-    //                                 scaledWidth = img.getWidth() * logoScale.floatValue();
-    //                                 scaledHeight = img.getHeight() * logoScale.floatValue();
-    //                                 log.warn("异步生成 - 使用回退缩放逻辑,Logo尺寸: {}x{}", scaledWidth, scaledHeight);
-    //                             }
-    //                             img.scaleToFit(scaledWidth, scaledHeight);
-    //                             // 将百分比坐标转换为PDF像素坐标,并翻转Y轴
-    //                             float logoPositionX = (float) (bibParam.getLogoX() * pageWidth / 100.0);
-    //                             float logoPositionY = (float) (pageHeight - (bibParam.getLogoY() * pageHeight / 100.0));
-    //                             img.setAbsolutePosition(logoPositionX, logoPositionY);
-    //                             cb.addImage(img);
-    //                         } catch (Exception e) {
-    //                             log.warn("添加Logo到PDF失败,跳过Logo处理: {}", e.getMessage());
-    //                         }
-    //                     }
-
-    //                     // 添加号码(使用前端传递的位置参数)
-    //                     Double numberScale = bibParam.getNumberScale() != null ? bibParam.getNumberScale() : 1.0;
-    //                     int scaledFontSize = (int) (fontSize * numberScale.floatValue());
-    //                     float textWidth = baseFont.getWidthPoint(athlete.getAthleteCode(), scaledFontSize);
-    //                     float textHeight = scaledFontSize;
-
-    //                     // 使用前端传递的位置参数(百分比转换为像素)
-    //                     float textPositionX, textPositionY;
-    //                     if (bibParam.getNumberX() != null && bibParam.getNumberY() != null) {
-    //                         textPositionX = (pageWidth * bibParam.getNumberX().floatValue() / 100) - (textWidth / 2);
-    //                         // PDF Y轴从下到上递增,需要翻转Y坐标
-    //                         textPositionY = pageHeight - (pageHeight * bibParam.getNumberY().floatValue() / 100) + (textHeight / 2);
-    //                     } else {
-    //                         // 默认居中
-    //                         textPositionX = (pageWidth - textWidth) / 2;
-    //                         textPositionY = (pageHeight / 2) + (textHeight / 2);
-    //                     }
-    //                     addText(cb, baseFont, scaledFontSize, textColor, textPositionX, textPositionY, athlete.getAthleteCode());
-
-    //                     // 添加赛事名称(使用前端传递的位置参数)
-    //                     String eventNameToShow = bibParam.getEventName();
-    //                     if (eventNameToShow == null || eventNameToShow.trim().isEmpty()) {
-    //                         eventNameToShow = "参赛证"; // 默认赛事名称
-    //                     }
-    //                     log.debug("赛事名称检查 - eventName: '{}', 最终显示: '{}'", bibParam.getEventName(), eventNameToShow);
-
-    //                     if (eventNameToShow != null && !eventNameToShow.trim().isEmpty()) {
-    //                         Double eventScale = bibParam.getEventScale() != null ? bibParam.getEventScale() : 1.0;
-    //                         int baseEventNameFontSize = Math.min(64, (int) (pageHeight * 0.08));
-    //                         int eventNameFontSize = (int) (baseEventNameFontSize * eventScale.floatValue());
-    //                         cb.beginText();
-    //                         cb.setFontAndSize(baseFont, eventNameFontSize);
-    //                         cb.setColorFill(BaseColor.BLACK);
-    //                         float textWidth2 = baseFont.getWidthPoint(eventNameToShow, eventNameFontSize);
-
-    //                         // 使用前端传递的位置参数(百分比转换为像素)
-    //                         float textX, textY;
-    //                         if (bibParam.getEventX() != null && bibParam.getEventY() != null) {
-    //                             textX = (pageWidth * bibParam.getEventX().floatValue() / 100) - (textWidth2 / 2);
-    //                             // PDF Y轴从下到上递增,需要翻转Y坐标
-    //                             textY = pageHeight - (pageHeight * bibParam.getEventY().floatValue() / 100) + (eventNameFontSize / 2);
-    //                         } else {
-    //                             // 默认位置(顶部居中)
-    //                             textX = (pageWidth - textWidth2) / 2;
-    //                             textY = pageHeight - eventNameFontSize - 20;
-    //                         }
-    //                         cb.setTextMatrix(textX, textY);
-    //                         cb.showText(eventNameToShow);
-    //                         cb.endText();
-    //                     }
-
-    //                     // 生成二维码(使用前端传递的位置和缩放参数)
-    //                     log.debug("二维码参数检查 - qRCodeX: {}, qRCodeY: {}, 是否为空: {}",
-    //                         bibParam.getQRCodeX(), bibParam.getQRCodeY(),
-    //                         bibParam.getQRCodeX() == null || bibParam.getQRCodeY() == null);
-    //                     if (bibParam.getQRCodeX() != null && bibParam.getQRCodeY() != null) {
-    //                         try {
-    //                             String qrDataStr = getQrDataStr(bibParam.getEventName(), groupName, teamNameMap, projectMap, athlete);
-    //                             Double barcodeScale = bibParam.getBarcodeScale() != null ? bibParam.getBarcodeScale() : 1.0;
-    //                             int baseQrSize = 100; // 固定基础尺寸
-    //                             int qrSize = (int) (baseQrSize * barcodeScale.floatValue());
-
-    //                             // 将百分比坐标转换为PDF像素坐标,并翻转Y轴
-    //                             float qrX = (float) (bibParam.getQRCodeX() * pageWidth / 100.0);
-    //                             float qrY = (float) (pageHeight - (bibParam.getQRCodeY() * pageHeight / 100.0));
-
-    //                             // 检查坐标是否在页面范围内
-    //                             if (qrX >= 0 && qrY >= 0 && qrX + qrSize <= pageWidth && qrY + qrSize <= pageHeight) {
-    //                                 log.debug("生成二维码 - 位置: ({}, {}), 大小: {}, 数据: {}",
-    //                                     qrX, qrY, qrSize, qrDataStr);
-
-    //                                 byte[] qrBytes = generateQRCode(qrDataStr, qrSize, qrSize);
-    //                                 Image qrImage = Image.getInstance(qrBytes);
-    //                                 // 二维码位置不需要翻转Y轴,因为它是图片元素
-    //                                 qrImage.setAbsolutePosition(qrX, qrY);
-    //                                 cb.addImage(qrImage);
-    //                             } else {
-    //                                 log.warn("二维码位置超出页面范围 - 位置: ({}, {}), 大小: {}, 页面尺寸: {}x{}",
-    //                                     qrX, qrY, qrSize, pageWidth, pageHeight);
-    //                                 // 使用默认位置(右下角)
-    //                                 float defaultX = pageWidth - qrSize - 20;
-    //                                 float defaultY = 20;
-    //                                 log.debug("使用默认二维码位置: ({}, {})", defaultX, defaultY);
-
-    //                                 byte[] qrBytes = generateQRCode(qrDataStr, qrSize, qrSize);
-    //                                 Image qrImage = Image.getInstance(qrBytes);
-    //                                 qrImage.setAbsolutePosition(defaultX, defaultY);
-    //                                 cb.addImage(qrImage);
-    //                             }
-
-    //                             log.debug("二维码添加成功 - 运动员: {}", athlete.getName());
-    //                         } catch (Exception e) {
-    //                             log.error("生成二维码失败 - 运动员: {}, 错误: {}", athlete.getName(), e.getMessage(), e);
-    //                         }
-    //                     } else {
-    //                         log.warn("二维码位置参数为空 - 运动员: {}, qRCodeX: {}, qRCodeY: {}",
-    //                             athlete.getName(), bibParam.getQRCodeX(), bibParam.getQRCodeY());
-
-    //                         // 即使位置参数为空,也生成二维码在默认位置
-    //                         try {
-    //                             String qrDataStr = getQrDataStr(bibParam.getEventName(), groupName, teamNameMap, projectMap, athlete);
-    //                             Double barcodeScale = bibParam.getBarcodeScale() != null ? bibParam.getBarcodeScale() : 1.0;
-    //                             int baseQrSize = 100; // 固定基础尺寸
-    //                             int qrSize = (int) (baseQrSize * barcodeScale.floatValue());
-
-    //                             // 使用默认位置(右下角)
-    //                             float defaultX = pageWidth - qrSize - 20;
-    //                             float defaultY = 20;
-    //                             log.debug("使用默认二维码位置: ({}, {})", defaultX, defaultY);
-
-    //                             byte[] qrBytes = generateQRCode(qrDataStr, qrSize, qrSize);
-    //                             Image qrImage = Image.getInstance(qrBytes);
-    //                             qrImage.setAbsolutePosition(defaultX, defaultY);
-    //                             cb.addImage(qrImage);
-
-    //                             log.debug("默认位置二维码添加成功 - 运动员: {}", athlete.getName());
-    //                         } catch (Exception e) {
-    //                             log.error("生成默认位置二维码失败 - 运动员: {}, 错误: {}", athlete.getName(), e.getMessage(), e);
-    //                         }
-    //                     }
-
-    //                     document.close();
-    //                     byte[] pdfBytes = pdfStream.toByteArray();
-
-    //                     // 构造安全的文件名
-    //                     String safeName = athlete.getName().replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5]", "_");
-    //                     String fileName = String.format("%s_%s.pdf", athlete.getAthleteCode(), safeName);
-    //                     return new PdfEntry(fileName, pdfBytes);
-
-    //                 } catch (Exception e) {
-    //                     log.error("生成PDF失败: {} - {}", athlete.getName(), e.getMessage());
-    //                     throw new RuntimeException("生成PDF失败: " + athlete.getName(), e);
-    //                 }
-    //             }, PDF_GENERATION_EXECUTOR))
-    //             .toList();
-
-    //         // 等待所有任务完成
-    //         return pdfFutures.stream()
-    //             .map(CompletableFuture::join)
-    //             .toList();
-
-    //     } catch (Exception e) {
-    //         log.error("生成PDF条目失败", e);
-    //         throw new RuntimeException("生成PDF条目失败: " + e.getMessage());
-    //     }
-    // }
-
     /**
      * 基于画布模版生成参赛证(异步)
      */
@@ -1583,9 +1091,6 @@ public class GameEventServiceImpl implements IGameEventService {
                 }
             }
 
-            int totalCount = athleteVoList.size();
-            int completedCount = 0;
-
             // 创建ZIP文件(直接在内存中生成,不保存单个图片文件)
             String zipFilePath = resultDir + "参赛证_" + taskId + ".zip";
             createZipFileFromMemory(templateImage, athleteVoList, bibParam, zipFilePath, taskId);
@@ -1632,15 +1137,28 @@ public class GameEventServiceImpl implements IGameEventService {
      */
     private byte[] generateSingleBibFromTemplate(byte[] templateImage, GameAthleteVo athlete, GenerateBibBo bibParam) {
         try {
+            // 检查模板图片数据是否为空
+            if (templateImage == null || templateImage.length == 0) {
+                log.error("模板图片数据为空");
+                throw new RuntimeException("模板图片数据为空");
+            }
+            log.info("模板图片数据长度: {} bytes", templateImage.length);
+
             // 使用BufferedImage处理模版图片
             BufferedImage originalTemplate = ImageIO.read(new ByteArrayInputStream(templateImage));
+            if (originalTemplate == null) {
+                log.error("无法加载模板图片,ImageIO.read返回null");
+                throw new RuntimeException("无法加载模板图片");
+            }
+            log.info("模板图片原始尺寸: {}x{}", originalTemplate.getWidth(), originalTemplate.getHeight());
 
             // 获取目标画布尺寸
             int targetWidth = bibParam.getCanvasWidth() != null ? bibParam.getCanvasWidth() : originalTemplate.getWidth();
             int targetHeight = bibParam.getCanvasHeight() != null ? bibParam.getCanvasHeight() : originalTemplate.getHeight();
+            log.info("目标画布尺寸: {}x{}", targetWidth, targetHeight);
 
             // 创建目标尺寸的画布
-            BufferedImage template = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
+            BufferedImage template = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_ARGB);
             Graphics2D g2d = template.createGraphics();
 
             // 设置抗锯齿
@@ -1648,12 +1166,20 @@ public class GameEventServiceImpl implements IGameEventService {
             g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
             g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
 
+            // 填充背景色为白色(防止透明背景显示为黑色)
+            g2d.setColor(Color.WHITE);
+            g2d.fillRect(0, 0, targetWidth, targetHeight);
+            log.info("已填充白色背景");
+
             // 将原始模版图片拉伸/缩放到目标画布尺寸
-            g2d.drawImage(originalTemplate, 0, 0, targetWidth, targetHeight, null);
+            boolean drawResult = g2d.drawImage(originalTemplate, 0, 0, targetWidth, targetHeight, null);
+            if (!drawResult) {
+                log.error("绘制模板图片失败,drawImage返回false");
+            }
             log.info("模版图片已调整到目标尺寸: {}x{}", targetWidth, targetHeight);
 
             // 绘制号码
-            drawNumberOnTemplate(g2d, athlete.getAthleteCode().toString(), bibParam, template.getWidth(), template.getHeight());
+            drawNumberOnTemplate(g2d, athlete.getAthleteCode(), bibParam, template.getWidth(), template.getHeight());
 
             // 绘制二维码
             drawQRCodeOnTemplate(g2d, athlete, bibParam, template.getWidth(), template.getHeight());
@@ -1682,13 +1208,26 @@ public class GameEventServiceImpl implements IGameEventService {
 
             // 使用前端传递的字体大小
             int fontSize = bibParam.getNumberFontSize() != null ? bibParam.getNumberFontSize() : 36;
-            Font font = new Font(bibParam.getFontName(), Font.BOLD, fontSize);
+
+            // 确保字体名称匹配系统可用字体
+            String fontName = bibParam.getFontName();
+            if (fontName == null) {
+                fontName = "SimHei";
+            } else if ("simhei".equalsIgnoreCase(fontName)) {
+                fontName = "SimHei";
+            }
+
+            Font font = new Font(fontName, Font.BOLD, fontSize);
             g2d.setFont(font);
 
             // 设置颜色
             Color color = new Color(bibParam.getFontColor());
             g2d.setColor(color);
 
+            // 启用抗锯齿以获得更平滑的字体渲染
+            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+            g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+
             // 绘制号码
             FontMetrics fm = g2d.getFontMetrics();
             int textWidth = fm.stringWidth(number);
@@ -1713,9 +1252,22 @@ public class GameEventServiceImpl implements IGameEventService {
             String qrData = generateQRCodeData(athlete);
             log.info("生成二维码数据: {}", qrData);
 
-            // 生成二维码图片 - 与前端保持一致的初始大小
+            // 确定最终绘制的二维码尺寸
             int initialQrSize = 32;
-            byte[] qrCodeBytes = generateQRCode(qrData, initialQrSize, initialQrSize);
+            int scaledWidth, scaledHeight;
+            if (bibParam.getQRCodeWidth() != null && bibParam.getQRCodeHeight() != null) {
+                scaledWidth = bibParam.getQRCodeWidth();
+                scaledHeight = bibParam.getQRCodeHeight();
+                log.info("使用前端传递的二维码尺寸: {}x{}", scaledWidth, scaledHeight);
+            } else {
+                // 使用默认缩放
+                scaledWidth = initialQrSize * 3;
+                scaledHeight = initialQrSize * 3;
+                log.info("使用默认二维码尺寸: {}x{}", scaledWidth, scaledHeight);
+            }
+            
+            // 直接使用最终尺寸生成二维码,避免二次缩放
+            byte[] qrCodeBytes = generateQRCode(qrData, scaledWidth, scaledHeight);
             BufferedImage qrImage = ImageIO.read(new ByteArrayInputStream(qrCodeBytes));
 
             // 使用前端传递的像素坐标
@@ -1726,26 +1278,15 @@ public class GameEventServiceImpl implements IGameEventService {
                 log.info("使用前端传递的二维码坐标: ({}, {})", x, y);
             } else {
                 // 使用默认位置(右下角)
-                x = canvasWidth - (initialQrSize * 3) - 10;
-                y = canvasHeight - (initialQrSize * 3) - 10;
+                x = canvasWidth - scaledWidth - 10;
+                y = canvasHeight - scaledHeight - 10;
                 log.info("使用默认二维码位置: ({}, {})", x, y);
             }
 
-            // 使用前端传递的二维码尺寸
-            int scaledWidth, scaledHeight;
-            if (bibParam.getQRCodeWidth() != null && bibParam.getQRCodeHeight() != null) {
-                scaledWidth = bibParam.getQRCodeWidth();
-                scaledHeight = bibParam.getQRCodeHeight();
-                log.info("使用前端传递的二维码尺寸: {}x{}", scaledWidth, scaledHeight);
-            } else {
-                // 使用默认缩放
-                scaledWidth = initialQrSize * 3;
-                scaledHeight = initialQrSize * 3;
-                log.info("使用默认二维码尺寸: {}x{}", scaledWidth, scaledHeight);
-            }
-
-            // 应用与前端相同的水平居中偏移 (transform: translateX(-50%))
-            x = x - scaledWidth / 2;
+            // 前端传递的是直接使用的坐标(已验证不需要转换)
+            // 保持原始坐标不变,避免位置偏移
+            // x = x - scaledWidth / 2;
+            // y = y - scaledHeight / 2;
 
             // 边界检查,确保二维码不会超出图片范围
             if (x < 0) x = 0;
@@ -1866,82 +1407,6 @@ public class GameEventServiceImpl implements IGameEventService {
         }
     }
 
-    /**
-     * 创建ZIP文件(从已存在的文件)
-     */
-    private void createZipFile(String sourceDir, String zipFilePath) {
-        try (FileOutputStream fos = new FileOutputStream(zipFilePath);
-             ZipOutputStream zos = new ZipOutputStream(fos)) {
-
-            File sourceFile = new File(sourceDir);
-            log.info("开始创建ZIP文件 - 源目录: {}, ZIP文件: {}", sourceDir, zipFilePath);
-
-            if (!sourceFile.exists()) {
-                throw new RuntimeException("源目录不存在: " + sourceDir);
-            }
-
-            File[] files = sourceFile.listFiles();
-            if (files == null || files.length == 0) {
-                log.warn("源目录为空: {}", sourceDir);
-                return;
-            }
-
-            log.info("找到 {} 个文件需要打包", files.length);
-
-            // 只添加PNG文件,不包含ZIP文件本身
-            for (File file : files) {
-                if (file.isFile() && file.getName().toLowerCase().endsWith(".png")) {
-                    try (FileInputStream fis = new FileInputStream(file)) {
-                        ZipEntry zipEntry = new ZipEntry(file.getName());
-                        zos.putNextEntry(zipEntry);
-
-                        byte[] buffer = new byte[1024];
-                        int length;
-                        while ((length = fis.read(buffer)) > 0) {
-                            zos.write(buffer, 0, length);
-                        }
-
-                        zos.closeEntry();
-                        log.debug("添加文件到ZIP: {}", file.getName());
-                    }
-                }
-            }
-
-            log.info("ZIP文件创建成功: {}", zipFilePath);
-
-        } catch (Exception e) {
-            log.error("创建ZIP文件失败", e);
-            throw new RuntimeException("创建ZIP文件失败: " + e.getMessage());
-        }
-    }
-
-    /**
-     * 递归添加文件到ZIP
-     */
-    private void addFileToZip(File file, String fileName, ZipOutputStream zos) throws IOException {
-        if (file.isDirectory()) {
-            File[] files = file.listFiles();
-            if (files != null) {
-                for (File subFile : files) {
-                    addFileToZip(subFile, fileName + File.separator + subFile.getName(), zos);
-                }
-            }
-        } else {
-            try (FileInputStream fis = new FileInputStream(file)) {
-                ZipEntry zipEntry = new ZipEntry(fileName);
-                zos.putNextEntry(zipEntry);
-
-                byte[] buffer = new byte[1024];
-                int length;
-                while ((length = fis.read(buffer)) > 0) {
-                    zos.write(buffer, 0, length);
-                }
-
-                zos.closeEntry();
-            }
-        }
-    }
-
     /**
      * 自定义MultipartFile实现,用于处理内存中的字节数组
      */