Forráskód Böngészése

perf:生产号码布采用异步

wenkai 3 napja
szülő
commit
b972d4d4c6

+ 6 - 0
ruoyi-admin/pom.xml

@@ -75,6 +75,12 @@
             <artifactId>ruoyi-job</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-caffeine</artifactId>
+            <version>5.4.1</version>
+        </dependency>
+
         <!-- 代码生成-->
         <dependency>
             <groupId>org.dromara</groupId>

+ 0 - 1
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/CacheUtils.java

@@ -57,5 +57,4 @@ public class CacheUtils {
     public static void clear(String cacheNames) {
         CACHE_MANAGER.getCache(cacheNames).clear();
     }
-
 }

+ 1 - 4
ruoyi-modules/ruoyi-game-event/pom.xml

@@ -98,6 +98,7 @@
             <groupId>org.dromara</groupId>
             <artifactId>ruoyi-common-websocket</artifactId>
         </dependency>
+
         <dependency>
             <groupId>org.apache.poi</groupId>
             <artifactId>poi</artifactId>
@@ -112,10 +113,6 @@
             <groupId>org.dromara</groupId>
             <artifactId>ruoyi-common-sse</artifactId>
         </dependency>
-        <dependency>
-            <groupId>org.dromara</groupId>
-            <artifactId>ruoyi-common-core</artifactId>
-        </dependency>
         <dependency>
             <groupId>org.dromara</groupId>
             <artifactId>ruoyi-system</artifactId>

+ 12 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/PdfEntry.java

@@ -0,0 +1,12 @@
+package org.dromara.system.domain;
+
+import lombok.Data;
+import lombok.Getter;
+import lombok.Setter;
+
+@Data
+@Getter
+public class PdfEntry {
+    private final String fileName;
+    private final byte[] pdfBytes;
+}

+ 2 - 2
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameEventGroupServiceImpl.java

@@ -211,9 +211,9 @@ public class GameEventGroupServiceImpl implements IGameEventGroupService {
      */
     @Override
     public GameEventGroup queryByEventId(Long defaultEventId) {
-        return baseMapper.selectOne(
+        return baseMapper.selectList(
             Wrappers.lambdaQuery(GameEventGroup.class)
                 .eq(GameEventGroup::getEventId, defaultEventId)
-        );
+        ).get(0);
     }
 }

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

@@ -34,6 +34,7 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.redis.utils.RedisUtils;
 import org.dromara.system.domain.GameEvent;
 import org.dromara.system.domain.GameEventGroup;
+import org.dromara.system.domain.PdfEntry;
 import org.dromara.system.domain.bo.GameAthleteBo;
 import org.dromara.system.domain.bo.GameEventBo;
 import org.dromara.system.domain.bo.GameTeamBo;
@@ -42,6 +43,7 @@ import org.dromara.system.domain.constant.GameEventConstant;
 import org.dromara.system.domain.vo.*;
 import org.dromara.system.mapper.GameEventMapper;
 import org.dromara.system.service.*;
+import org.jetbrains.annotations.NotNull;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -52,8 +54,11 @@ import javax.imageio.ImageIO;
 import java.awt.image.BufferedImage;
 import java.io.*;
 import java.util.*;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
 
 /**
  * 赛事基本信息Service业务层处理
@@ -616,8 +621,25 @@ public class GameEventServiceImpl implements IGameEventService {
                             Map<Long, String> teamNameMap,
                             Map<Long, String> projectMap,
                             GenerateBibBo bibParam) {
+
+        byte[] backgroundImageBytes;
+        byte[] logoImageBytes = null;
+        //1.读取图片文件
+        try {
+            if (backgroundImage == null || backgroundImage.isEmpty()) {
+                throw new IllegalArgumentException("背景图不能为空");
+            }
+            backgroundImageBytes = backgroundImage.getBytes();
+
+            if (logo != null && !logo.isEmpty()) {
+                logoImageBytes = logo.getBytes();
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("读取上传文件失败", e);
+        }
+
         try {
-            // 提取布局参数
+            // 2. 提取参数
             Double logoX = bibParam.getLogoX();
             Double logoY = bibParam.getLogoY();
             Double qRCodeX = bibParam.getQRCodeX();
@@ -626,158 +648,161 @@ public class GameEventServiceImpl implements IGameEventService {
             Integer fontSize = bibParam.getFontSize();
             Integer fontColor = bibParam.getFontColor();
 
-            // 设置默认值
+            // 2.1 设置默认值及获取字体及颜色
             if (fontSize == null) fontSize = 14;
             BaseColor textColor = parseColor(fontColor);
             BaseFont baseFont = getChineseFont(fontName);
 
-            // 读取背景图
-            Image bgImage = Image.getInstance(backgroundImage.getBytes());
+            // 3. 读取背景图和 logo
+            Image bgImage = Image.getInstance(backgroundImageBytes);
             float pageWidth = bgImage.getWidth();
             float pageHeight = bgImage.getHeight();
 
-            // 提前读取 logo 字节数组
-            byte[] logoBytes = null;
-            if (logo != null && !logo.isEmpty()) {
-                logoBytes = logo.getBytes();
-            }
-
-            // 设置响应头(返回 ZIP 压缩包)
-            response.reset();
-            response.setContentType("application/zip");
-            response.setHeader("Content-Disposition", "attachment; filename=\"athlete_bibs.zip\"");
-
-            try (java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(response.getOutputStream())) {
-
-                for (GameAthleteVo athlete : athleteList) {
-                    // 每个运动员生成一个 PDF
-                    ByteArrayOutputStream pdfStream = new ByteArrayOutputStream();
-                    Document document = new Document(new Rectangle(pageWidth, pageHeight));
-                    PdfWriter writer = PdfWriter.getInstance(document, pdfStream);
-                    document.open();
-
-                    PdfContentByte cb = writer.getDirectContent();
-
-                    // 添加背景图
-                    bgImage.setAbsolutePosition(0, 0);
-                    document.add(bgImage);
-
-                    // 添加Logo - 更精确的位置调整
-                    if (logoBytes != null) {
-                        try {
-                            Image img = Image.getInstance(logoBytes);
+            Integer finalFontSize = fontSize;
+            // 4. 并行生成所有 PDF
+            byte[] finalLogoImageBytes = logoImageBytes;
+            List<CompletableFuture<PdfEntry>> pdfFutures = athleteList.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.setAbsolutePosition(0, 0);
+                        document.add(bg);
+
+                        // 添加 Logo(如果存在)
+                        if (finalLogoImageBytes != null) {
+                            Image img = Image.getInstance(finalLogoImageBytes);
                             img.scaleToFit(80, 80);
-
-                            // 直接使用前端传递的坐标值,更精确的位置调整
                             float logoPositionX = logoX != null ? logoX.floatValue() : 50f;
                             float logoPositionY = logoY != null ? logoY.floatValue() : pageHeight - 130f;
-
-                            // 更精确的位置调整
-                            logoPositionX += 10; // 向右偏移10pt
-                            logoPositionY -= 50; // 向下偏移50pt(与编号重合)
-
+                            logoPositionX += 10;  // 微调
+                            logoPositionY -= 50;
                             img.setAbsolutePosition(logoPositionX, logoPositionY);
                             cb.addImage(img);
-                        } catch (Exception e) {
-                            // 忽略或记录日志
                         }
-                    }
-
-                    // 计算文本宽度和高度
-                    float textWidth = baseFont.getWidthPoint(athlete.getAthleteCode(), fontSize);
-                    float textHeight = fontSize;
-
-                    // 确保文本垂直和水平都居中
-                    float textPositionX = (pageWidth - textWidth) / 2; // 水平居中
-                    float textPositionY = (pageHeight / 2) + (textHeight / 2); // 垂直居中
 
-                    addText(cb, baseFont, fontSize, textColor, textPositionX, textPositionY, athlete.getAthleteCode());
-
-                    // 添加赛事名称
-                    if (eventName != null && !eventName.trim().isEmpty()) {
+                        // 添加号码(居中)
+                        float textWidth = baseFont.getWidthPoint(athlete.getAthleteCode(), finalFontSize);
+                        float textHeight = finalFontSize;
+                        float textPositionX = (pageWidth - textWidth) / 2;
+                        float textPositionY = (pageHeight / 2) + (textHeight / 2);
+                        addText(cb, baseFont, finalFontSize, textColor, textPositionX, textPositionY, athlete.getAthleteCode());
+
+                        // 添加赛事名称
+                        if (eventName != null && !eventName.trim().isEmpty()) {
+                            int eventNameFontSize = 64;
+                            cb.beginText();
+                            cb.setFontAndSize(baseFont, eventNameFontSize);
+                            cb.setColorFill(BaseColor.BLACK);
+                            float textWidth2 = baseFont.getWidthPoint(eventName, eventNameFontSize);
+                            float textX = (pageWidth - textWidth2) / 2;
+                            float textY = pageHeight - eventNameFontSize - 10;
+                            cb.setTextMatrix(textX, textY);
+                            cb.showText(eventName);
+                            cb.endText();
+                        }
 
-                        // 设置字体大小和颜色
-                        int eventNameFontSize = 64; // 确保与绘制文字时的字体大小一致
-                        cb.beginText();
-                        cb.setFontAndSize(baseFont, eventNameFontSize);
-                        cb.setColorFill(BaseColor.BLACK);
+                        // 生成二维码
+                        String qrDataStr = getQrDataStr(eventName, groupName, teamNameMap, projectMap, athlete);
+                        byte[] qrBytes = generateQRCode(qrDataStr, 150, 150);
+                        Image qrImage = Image.getInstance(qrBytes);
+                        float qrX = qRCodeX != null ? qRCodeX.floatValue() : pageWidth - 170f;
+                        float qrY = qRCodeY != null ? qRCodeY.floatValue() : 50f;
+                        qrX += 15;
+                        qrY -= 55;
+                        qrImage.setAbsolutePosition(qrX, qrY);
+                        cb.addImage(qrImage);
+
+                        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) {
+                        // 建议记录日志
+                        System.err.println("生成 PDF 失败: " + athlete.getName() + " - " + e.getMessage());
+                        throw new RuntimeException("生成 PDF 失败: " + athlete.getName(), e);
+                    }
+                }))
+              .toList();
 
-                        // 计算文本宽度和X坐标以实现水平居中
-                        textWidth = baseFont.getWidthPoint(eventName, eventNameFontSize);
-                        float textX = (pageWidth - textWidth) / 2;
+            // 等待所有任务完成
+            List<PdfEntry> pdfEntries = pdfFutures.stream()
+                .map(CompletableFuture::join)
+                .toList();
 
-                        // 设置文本垂直位置使其位于页面顶部,留出一定间距
-                        float textY = pageHeight - eventNameFontSize - 10; // 调整这个值以改变顶部间距
+            // 4. 设置响应头,开始写 ZIP
+            response.setContentType("application/zip");
+            response.setHeader("Content-Disposition", "attachment; filename=\"athlete_bibs.zip\"");
 
-                        cb.setTextMatrix(textX, textY);
-                        cb.showText(eventName);
-                        cb.endText();
-                    }
-                    StringBuilder joinProject = new StringBuilder();
-                    StringJoiner joiner = new StringJoiner("、"); // 指定分隔符
-                    athlete.getProjectList().forEach(projectId -> {
-                        String projectName = projectMap.get(Long.valueOf(projectId));
-                        if (projectName != null) {
-                            joiner.add(projectName);
-                        }
-                    });
-
-                    // 生成二维码
-                    String qrData = String.format(
-                        """
-                                {
-                                    赛事名称:%s,
-                                    运动员序号:%d,
-                                    运动员编号:%s,
-                                    参与项目:%s,
-                                    队伍名称:%s,
-                                    运动员姓名:%s,
-                                    性别:%s,
-                                    年龄:%d
-                                    组别:%s
-                                }
-                            """,
-                        eventName, athlete.getAthleteId(), athlete.getAthleteCode(),
-                        joinProject, teamNameMap.get(athlete.getTeamId()), athlete.getName(),
-                        athlete.getGender(), athlete.getAge(), groupName
-                    );
-
-                    byte[] qrBytes = generateQRCode(qrData, 150, 150);
-                    Image qrImage = Image.getInstance(qrBytes);
-
-                    // 直接使用前端传递的坐标值,更精确的位置调整
-                    float qrX = qRCodeX != null ? qRCodeX.floatValue() : pageWidth - 170f;
-                    float qrY = qRCodeY != null ? qRCodeY.floatValue() : 50f;
-
-                    // 更精确的位置调整
-                    qrX += 15; // 向右偏移15pt
-                    qrY -= 55; // 向下偏移55pt(与编号重合)
-
-                    qrImage.setAbsolutePosition(qrX, qrY);
-                    cb.addImage(qrImage);
-
-                    document.close();
-
-                    // 写入 ZIP:文件名为 bib_号码_姓名.pdf
-                    String fileName = String.format("bib_%s_%s.pdf", athlete.getAthleteCode(), athlete.getName());
-                    zos.putNextEntry(new java.util.zip.ZipEntry(fileName));
-                    zos.write(pdfStream.toByteArray());
+            // 5. 写入 ZIP 文件
+            try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
+                for (PdfEntry entry : pdfEntries) {
+                    zos.putNextEntry(new ZipEntry(entry.getFileName()));
+                    zos.write(entry.getPdfBytes());
                     zos.closeEntry();
                 }
-
-                // 强制刷新输出流
                 zos.flush();
-                response.flushBuffer();
-
-            } catch (Exception e) {
-                throw new RuntimeException("生成号码布 ZIP 失败", e);
             }
+
         } catch (Exception e) {
-            throw new RuntimeException(e);
-        } finally {
+            throw new RuntimeException("批量生成号码布失败", e);
         }
     }
 
+
+    /**
+     * 生成二维码数据
+     * @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) {
+        //处理参加项目
+        StringBuilder joinProject = new StringBuilder();
+        StringJoiner joiner = new StringJoiner("、"); // 指定分隔符
+        athlete.getProjectList().forEach(projectId -> {
+            String projectName = projectMap.get(Long.valueOf(projectId));
+            if (projectName != null) {
+                joiner.add(projectName);
+            }
+        });
+
+        // 生成二维码
+        String qrData = String.format(
+            """
+                    {
+                        赛事名称:%s,
+                        运动员序号:%d,
+                        运动员编号:%s,
+                        参与项目:%s,
+                        队伍名称:%s,
+                        运动员姓名:%s,
+                        性别:%s,
+                        年龄:%d
+                        组别:%s
+                    }
+                """,
+            eventName, athlete.getAthleteId(), athlete.getAthleteCode(),
+            joinProject, teamNameMap.get(athlete.getTeamId()), athlete.getName(),
+            athlete.getGender(), athlete.getAge(), groupName
+        );
+        return qrData;
+    }
+
     // 工具方法:添加文本
     private static void addText(PdfContentByte cb, BaseFont font, int size, BaseColor color, float x, float y, String text) {
         cb.beginText();

+ 0 - 1
ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/AlipayBillTask.java

@@ -38,5 +38,4 @@ public class AlipayBillTask {
         SnailJobLog.REMOTE.info("上下文: {}", jobArgs.getWfContext());
         return ExecuteResult.success(billDto);
     }
-
 }