|
@@ -34,6 +34,7 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
|
|
import org.dromara.common.redis.utils.RedisUtils;
|
|
import org.dromara.common.redis.utils.RedisUtils;
|
|
import org.dromara.system.domain.GameEvent;
|
|
import org.dromara.system.domain.GameEvent;
|
|
import org.dromara.system.domain.GameEventGroup;
|
|
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.GameAthleteBo;
|
|
import org.dromara.system.domain.bo.GameEventBo;
|
|
import org.dromara.system.domain.bo.GameEventBo;
|
|
import org.dromara.system.domain.bo.GameTeamBo;
|
|
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.domain.vo.*;
|
|
import org.dromara.system.mapper.GameEventMapper;
|
|
import org.dromara.system.mapper.GameEventMapper;
|
|
import org.dromara.system.service.*;
|
|
import org.dromara.system.service.*;
|
|
|
|
+import org.jetbrains.annotations.NotNull;
|
|
import org.springframework.context.annotation.Lazy;
|
|
import org.springframework.context.annotation.Lazy;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
@@ -52,8 +54,11 @@ import javax.imageio.ImageIO;
|
|
import java.awt.image.BufferedImage;
|
|
import java.awt.image.BufferedImage;
|
|
import java.io.*;
|
|
import java.io.*;
|
|
import java.util.*;
|
|
import java.util.*;
|
|
|
|
+import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Collectors;
|
|
|
|
+import java.util.zip.ZipEntry;
|
|
|
|
+import java.util.zip.ZipOutputStream;
|
|
|
|
|
|
/**
|
|
/**
|
|
* 赛事基本信息Service业务层处理
|
|
* 赛事基本信息Service业务层处理
|
|
@@ -616,8 +621,25 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
Map<Long, String> teamNameMap,
|
|
Map<Long, String> teamNameMap,
|
|
Map<Long, String> projectMap,
|
|
Map<Long, String> projectMap,
|
|
GenerateBibBo bibParam) {
|
|
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 {
|
|
try {
|
|
- // 提取布局参数
|
|
|
|
|
|
+ // 2. 提取参数
|
|
Double logoX = bibParam.getLogoX();
|
|
Double logoX = bibParam.getLogoX();
|
|
Double logoY = bibParam.getLogoY();
|
|
Double logoY = bibParam.getLogoY();
|
|
Double qRCodeX = bibParam.getQRCodeX();
|
|
Double qRCodeX = bibParam.getQRCodeX();
|
|
@@ -626,158 +648,161 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
Integer fontSize = bibParam.getFontSize();
|
|
Integer fontSize = bibParam.getFontSize();
|
|
Integer fontColor = bibParam.getFontColor();
|
|
Integer fontColor = bibParam.getFontColor();
|
|
|
|
|
|
- // 设置默认值
|
|
|
|
|
|
+ // 2.1 设置默认值及获取字体及颜色
|
|
if (fontSize == null) fontSize = 14;
|
|
if (fontSize == null) fontSize = 14;
|
|
BaseColor textColor = parseColor(fontColor);
|
|
BaseColor textColor = parseColor(fontColor);
|
|
BaseFont baseFont = getChineseFont(fontName);
|
|
BaseFont baseFont = getChineseFont(fontName);
|
|
|
|
|
|
- // 读取背景图
|
|
|
|
- Image bgImage = Image.getInstance(backgroundImage.getBytes());
|
|
|
|
|
|
+ // 3. 读取背景图和 logo
|
|
|
|
+ Image bgImage = Image.getInstance(backgroundImageBytes);
|
|
float pageWidth = bgImage.getWidth();
|
|
float pageWidth = bgImage.getWidth();
|
|
float pageHeight = bgImage.getHeight();
|
|
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);
|
|
img.scaleToFit(80, 80);
|
|
-
|
|
|
|
- // 直接使用前端传递的坐标值,更精确的位置调整
|
|
|
|
float logoPositionX = logoX != null ? logoX.floatValue() : 50f;
|
|
float logoPositionX = logoX != null ? logoX.floatValue() : 50f;
|
|
float logoPositionY = logoY != null ? logoY.floatValue() : pageHeight - 130f;
|
|
float logoPositionY = logoY != null ? logoY.floatValue() : pageHeight - 130f;
|
|
-
|
|
|
|
- // 更精确的位置调整
|
|
|
|
- logoPositionX += 10; // 向右偏移10pt
|
|
|
|
- logoPositionY -= 50; // 向下偏移50pt(与编号重合)
|
|
|
|
-
|
|
|
|
|
|
+ logoPositionX += 10; // 微调
|
|
|
|
+ logoPositionY -= 50;
|
|
img.setAbsolutePosition(logoPositionX, logoPositionY);
|
|
img.setAbsolutePosition(logoPositionX, logoPositionY);
|
|
cb.addImage(img);
|
|
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.closeEntry();
|
|
}
|
|
}
|
|
-
|
|
|
|
- // 强制刷新输出流
|
|
|
|
zos.flush();
|
|
zos.flush();
|
|
- response.flushBuffer();
|
|
|
|
-
|
|
|
|
- } catch (Exception e) {
|
|
|
|
- throw new RuntimeException("生成号码布 ZIP 失败", e);
|
|
|
|
}
|
|
}
|
|
|
|
+
|
|
} catch (Exception 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) {
|
|
private static void addText(PdfContentByte cb, BaseFont font, int size, BaseColor color, float x, float y, String text) {
|
|
cb.beginText();
|
|
cb.beginText();
|