|
@@ -41,17 +41,25 @@ import org.dromara.system.domain.bo.*;
|
|
|
import org.dromara.system.domain.constant.GameEventConstant;
|
|
|
import org.dromara.system.domain.vo.*;
|
|
|
import org.dromara.system.mapper.GameEventMapper;
|
|
|
+import org.dromara.system.config.FileUploadConfig;
|
|
|
import org.dromara.system.service.*;
|
|
|
import org.springframework.context.annotation.Lazy;
|
|
|
+import org.springframework.scheduling.annotation.Async;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
import org.springframework.util.CollectionUtils;
|
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
|
import javax.imageio.ImageIO;
|
|
|
+import java.awt.*;
|
|
|
+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.util.*;
|
|
|
+import java.util.List;
|
|
|
import java.util.concurrent.CompletableFuture;
|
|
|
import java.util.concurrent.ExecutorService;
|
|
|
import java.util.concurrent.Executors;
|
|
@@ -81,6 +89,10 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
private IGameEventProjectService gameEventProjectService;
|
|
|
@Resource
|
|
|
private IGameEventGroupService gameEventGroupService;
|
|
|
+ @Resource
|
|
|
+ private IGameBibTaskService gameBibTaskService;
|
|
|
+ @Resource
|
|
|
+ private FileUploadConfig fileUploadConfig;
|
|
|
private static final ExecutorService PDF_GENERATION_EXECUTOR =
|
|
|
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
|
|
|
// 常见图片类型的文件头(前几个字节)
|
|
@@ -92,6 +104,11 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
"4D4D002A", // TIFF (big endian)
|
|
|
"424D" // BMP
|
|
|
};
|
|
|
+
|
|
|
+ // 预览框尺寸(固定)
|
|
|
+ private static final int previewWidth = 600;
|
|
|
+ private static final int previewHeight = 400;
|
|
|
+
|
|
|
/**
|
|
|
* 查询赛事基本信息
|
|
|
*
|
|
@@ -733,9 +750,12 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
if (finalLogoImageBytes != null && logoX != null && logoY != null) {
|
|
|
try {
|
|
|
Image img = Image.getInstance(finalLogoImageBytes);
|
|
|
- img.scaleToFit(80, 80);
|
|
|
- float logoPositionX = logoX.floatValue();
|
|
|
- float logoPositionY = logoY.floatValue();
|
|
|
+ // 应用缩放参数
|
|
|
+ Double logoScale = bibParam.getLogoScale() != null ? bibParam.getLogoScale() : 1.0;
|
|
|
+ img.scaleToFit(80 * logoScale.floatValue(), 80 * logoScale.floatValue());
|
|
|
+ // 将百分比坐标转换为PDF像素坐标,并翻转Y轴
|
|
|
+ float logoPositionX = (float) (logoX * pageWidth / 100.0);
|
|
|
+ float logoPositionY = (float) (pageHeight - (logoY * pageHeight / 100.0));
|
|
|
img.setAbsolutePosition(logoPositionX, logoPositionY);
|
|
|
cb.addImage(img);
|
|
|
// log.debug("成功添加Logo到PDF");
|
|
@@ -744,37 +764,121 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 添加号码(居中)
|
|
|
- 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());
|
|
|
+ // 添加号码(使用前端传递的位置参数)
|
|
|
+ Double numberScale = bibParam.getNumberScale() != null ? bibParam.getNumberScale() : 1.0;
|
|
|
+ int scaledFontSize = (int) (finalFontSize * 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 = eventName;
|
|
|
+ if (eventNameToShow == null || eventNameToShow.trim().isEmpty()) {
|
|
|
+ eventNameToShow = "参赛证"; // 默认赛事名称
|
|
|
+ }
|
|
|
+ log.debug("同步生成 - 赛事名称检查 - eventName: '{}', 最终显示: '{}'", eventName, eventNameToShow);
|
|
|
|
|
|
- // 添加赛事名称(在3:2横屏比例下调整位置)
|
|
|
- if (eventName != null && !eventName.trim().isEmpty()) {
|
|
|
- int eventNameFontSize = Math.min(64, (int) (pageHeight * 0.08)); // 根据页面高度调整字体大小
|
|
|
+ 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(eventName, eventNameFontSize);
|
|
|
- float textX = (pageWidth - textWidth2) / 2;
|
|
|
- float textY = pageHeight - eventNameFontSize - 20; // 增加边距
|
|
|
+ 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(eventName);
|
|
|
+ cb.showText(eventNameToShow);
|
|
|
cb.endText();
|
|
|
}
|
|
|
|
|
|
- // 生成二维码(在3:2横屏比例下调整尺寸)
|
|
|
+ // 生成二维码(使用前端传递的位置和缩放参数)
|
|
|
if (qRCodeX != null && qRCodeY != null) {
|
|
|
- String qrDataStr = getQrDataStr(eventName, groupName, teamNameMap, projectMap, athlete);
|
|
|
- int qrSize = Math.min(150, (int) (Math.min(pageWidth, pageHeight) * 0.15)); // 根据页面尺寸调整二维码大小
|
|
|
- byte[] qrBytes = generateQRCode(qrDataStr, qrSize, qrSize);
|
|
|
- Image qrImage = Image.getInstance(qrBytes);
|
|
|
- float qrX = qRCodeX.floatValue();
|
|
|
- float qrY = qRCodeY.floatValue();
|
|
|
- qrImage.setAbsolutePosition(qrX, qrY);
|
|
|
- cb.addImage(qrImage);
|
|
|
+ try {
|
|
|
+ String qrDataStr = getQrDataStr(eventName, groupName, teamNameMap, projectMap, athlete);
|
|
|
+ Double barcodeScale = bibParam.getBarcodeScale() != null ? bibParam.getBarcodeScale() : 1.0;
|
|
|
+ int baseQrSize = Math.min(150, (int) (Math.min(pageWidth, pageHeight) * 0.15));
|
|
|
+ int qrSize = (int) (baseQrSize * barcodeScale.floatValue());
|
|
|
+
|
|
|
+ // 将百分比坐标转换为PDF像素坐标,并翻转Y轴
|
|
|
+ float qrX = (float) (qRCodeX * pageWidth / 100.0);
|
|
|
+ float qrY = (float) (pageHeight - (qRCodeY * 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);
|
|
|
+ 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(), qRCodeX, qRCodeY);
|
|
|
+
|
|
|
+ // 即使位置参数为空,也生成二维码在默认位置
|
|
|
+ try {
|
|
|
+ String qrDataStr = getQrDataStr(eventName, groupName, teamNameMap, projectMap, athlete);
|
|
|
+ Double barcodeScale = bibParam.getBarcodeScale() != null ? bibParam.getBarcodeScale() : 1.0;
|
|
|
+ int baseQrSize = Math.min(150, (int) (Math.min(pageWidth, pageHeight) * 0.15));
|
|
|
+ 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();
|
|
@@ -908,6 +1012,10 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
// 方式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";
|
|
@@ -1144,4 +1252,619 @@ 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 bgImageObj = Image.getInstance(backgroundImageBytes);
|
|
|
+ float pageWidth = bgImageObj.getWidth();
|
|
|
+ float pageHeight = bgImageObj.getHeight();
|
|
|
+
|
|
|
+ // 设置字体和颜色,提供默认值
|
|
|
+ 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.setAbsolutePosition(0, 0);
|
|
|
+ document.add(bg);
|
|
|
+
|
|
|
+ // 添加Logo(如果存在)
|
|
|
+ if (logoImageBytes != null && bibParam.getLogoX() != null && bibParam.getLogoY() != null) {
|
|
|
+ try {
|
|
|
+ Image img = Image.getInstance(logoImageBytes);
|
|
|
+ // 应用缩放参数
|
|
|
+ Double logoScale = bibParam.getLogoScale() != null ? bibParam.getLogoScale() : 1.0;
|
|
|
+ img.scaleToFit(80 * logoScale.floatValue(), 80 * logoScale.floatValue());
|
|
|
+ // 将百分比坐标转换为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 = Math.min(150, (int) (Math.min(pageWidth, pageHeight) * 0.15));
|
|
|
+ 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 = Math.min(150, (int) (Math.min(pageWidth, pageHeight) * 0.15));
|
|
|
+ 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());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 基于画布模版生成参赛证(异步)
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ @Async
|
|
|
+ public void generateBibFromTemplateAsync(Long taskId, Long eventId, byte[] templateImage, 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 = generateBibFromTemplateFiles(taskId, eventId, templateImage, bibParam, athleteVoList);
|
|
|
+
|
|
|
+ // 完成任务
|
|
|
+ gameBibTaskService.completeTask(taskId, resultFilePath);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("基于模版生成参赛证失败,taskId: {}", taskId, e);
|
|
|
+ gameBibTaskService.updateTaskStatus(taskId, "3", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 基于画布模版生成参赛证文件
|
|
|
+ */
|
|
|
+ private String generateBibFromTemplateFiles(Long taskId, Long eventId, byte[] templateImage,
|
|
|
+ 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());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ int totalCount = athleteVoList.size();
|
|
|
+ int completedCount = 0;
|
|
|
+
|
|
|
+ // 为每个运动员生成参赛证图片
|
|
|
+ for (GameAthleteVo athlete : athleteVoList) {
|
|
|
+ try {
|
|
|
+ // 基于模版生成单个参赛证图片
|
|
|
+ byte[] bibImage = generateSingleBibFromTemplate(templateImage, athlete, bibParam);
|
|
|
+
|
|
|
+ // 保存图片文件
|
|
|
+ String fileName = athlete.getAthleteCode() + "_" + athlete.getName() + ".png";
|
|
|
+ String filePath = resultDir + fileName;
|
|
|
+ Files.write(Paths.get(filePath), bibImage);
|
|
|
+
|
|
|
+ completedCount++;
|
|
|
+ int progress = (completedCount * 100) / totalCount;
|
|
|
+ gameBibTaskService.updateTaskProgress(taskId, progress, completedCount);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("生成运动员参赛证失败: {}", athlete.getName(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建ZIP文件
|
|
|
+ String zipFilePath = resultDir + "参赛证_" + taskId + ".zip";
|
|
|
+ createZipFile(resultDir, zipFilePath);
|
|
|
+ log.info("ZIP文件创建完成: {}", zipFilePath);
|
|
|
+
|
|
|
+ return zipFilePath;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("基于模版生成参赛证文件失败", e);
|
|
|
+ throw new RuntimeException("生成参赛证文件失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 基于模版生成单个参赛证图片
|
|
|
+ */
|
|
|
+ private byte[] generateSingleBibFromTemplate(byte[] templateImage, GameAthleteVo athlete, GenerateBibBo bibParam) {
|
|
|
+ try {
|
|
|
+ // 使用BufferedImage处理模版图片
|
|
|
+ BufferedImage template = ImageIO.read(new ByteArrayInputStream(templateImage));
|
|
|
+ Graphics2D g2d = template.createGraphics();
|
|
|
+
|
|
|
+ // 设置抗锯齿
|
|
|
+ g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
|
|
+ g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
|
|
+
|
|
|
+ // 绘制号码
|
|
|
+ drawNumberOnTemplate(g2d, athlete.getAthleteCode().toString(), bibParam, template.getWidth(), template.getHeight());
|
|
|
+
|
|
|
+ // 绘制二维码
|
|
|
+ drawQRCodeOnTemplate(g2d, athlete, bibParam, template.getWidth(), template.getHeight());
|
|
|
+
|
|
|
+ g2d.dispose();
|
|
|
+
|
|
|
+ // 转换为字节数组
|
|
|
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
|
+ ImageIO.write(template, "PNG", baos);
|
|
|
+ return baos.toByteArray();
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("基于模版生成单个参赛证失败", e);
|
|
|
+ throw new RuntimeException("生成参赛证失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 在模版上绘制号码
|
|
|
+ */
|
|
|
+ private void drawNumberOnTemplate(Graphics2D g2d, String number, GenerateBibBo bibParam, int canvasWidth, int canvasHeight) {
|
|
|
+ try {
|
|
|
+ // 计算号码位置(基于百分比)
|
|
|
+ int x = (int) (canvasWidth * bibParam.getNumberX() / 100);
|
|
|
+ int y = (int) (canvasHeight * bibParam.getNumberY() / 100);
|
|
|
+
|
|
|
+ // 设置字体(考虑前端预览的相对比例)
|
|
|
+ int baseFontSize = bibParam.getFontSize();
|
|
|
+ // 前端预览时号码字体被限制为最大56px,相对于预览框600x400的比例
|
|
|
+ final int previewMaxFontSize = 56;
|
|
|
+
|
|
|
+ // 计算预览时的相对比例(相对于预览框的宽度)
|
|
|
+ double previewRelativeFontSize = (double) previewMaxFontSize / previewWidth;
|
|
|
+
|
|
|
+ // 计算实际图片中字体应该占的相对比例
|
|
|
+ double actualRelativeFontSize = previewRelativeFontSize * canvasWidth;
|
|
|
+
|
|
|
+ // 计算字体的实际缩放比例
|
|
|
+ double fontScale = Math.min(1.0, actualRelativeFontSize / baseFontSize);
|
|
|
+ int fontSize = (int) (baseFontSize * fontScale);
|
|
|
+ Font font = new Font(bibParam.getFontName(), Font.BOLD, fontSize);
|
|
|
+ g2d.setFont(font);
|
|
|
+
|
|
|
+ // 设置颜色
|
|
|
+ Color color = new Color(bibParam.getFontColor());
|
|
|
+ g2d.setColor(color);
|
|
|
+
|
|
|
+ // 绘制号码
|
|
|
+ FontMetrics fm = g2d.getFontMetrics();
|
|
|
+ int textWidth = fm.stringWidth(number);
|
|
|
+ int textHeight = fm.getHeight();
|
|
|
+
|
|
|
+ // 居中绘制
|
|
|
+ g2d.drawString(number, x - textWidth / 2, y + textHeight / 4);
|
|
|
+ log.info("号码绘制完成 - 位置: ({}, {}), 基础字体: {}, 预览相对比例: {}, 实际相对大小: {}, 预览缩放: {}, 最终字体: {}", x, y, baseFontSize, previewRelativeFontSize, actualRelativeFontSize, fontScale, fontSize);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("绘制号码失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 在模版上绘制二维码
|
|
|
+ */
|
|
|
+ private void drawQRCodeOnTemplate(Graphics2D g2d, GameAthleteVo athlete, GenerateBibBo bibParam, int canvasWidth, int canvasHeight) {
|
|
|
+ try {
|
|
|
+ // 生成二维码数据
|
|
|
+ String qrData = generateQRCodeData(athlete);
|
|
|
+ log.info("生成二维码数据: {}", qrData);
|
|
|
+
|
|
|
+ // 生成二维码图片
|
|
|
+ byte[] qrCodeBytes = generateQRCode(qrData, 100, 100);
|
|
|
+ BufferedImage qrImage = ImageIO.read(new ByteArrayInputStream(qrCodeBytes));
|
|
|
+
|
|
|
+ // 计算二维码位置(从百分比坐标转换为实际图片坐标)
|
|
|
+ Double qrX = bibParam.getQRCodeX();
|
|
|
+ Double qrY = bibParam.getQRCodeY();
|
|
|
+
|
|
|
+ int x, y;
|
|
|
+ if (qrX != null && qrY != null) {
|
|
|
+ // 将百分比坐标转换为实际图片像素坐标
|
|
|
+ x = (int) (qrX * canvasWidth / 100.0);
|
|
|
+ y = (int) (qrY * canvasHeight / 100.0);
|
|
|
+ log.info("二维码坐标转换 - 百分比坐标: ({}, {}), 实际图片坐标: ({}, {})",
|
|
|
+ qrX, qrY, x, y);
|
|
|
+ } else {
|
|
|
+ // 使用默认位置(右下角)
|
|
|
+ x = (int) (88.33 * canvasWidth / 100.0); // 88.33%
|
|
|
+ y = (int) (67.5 * canvasHeight / 100.0); // 67.5%
|
|
|
+ log.info("使用默认二维码位置: ({}, {})", x, y);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 应用缩放(考虑前端预览的相对比例和用户设置的缩放)
|
|
|
+ Double barcodeScale = bibParam.getBarcodeScale() != null ? bibParam.getBarcodeScale() : 1.0;
|
|
|
+
|
|
|
+ // 前端预览时二维码是32x32的SVG图标,相对于预览框600x400的比例
|
|
|
+ final int previewQRSize = 32; // 前端SVG图标大小
|
|
|
+
|
|
|
+ // 计算预览时的相对比例(相对于预览框的宽度)
|
|
|
+ double previewRelativeSize = (double) previewQRSize / previewWidth;
|
|
|
+
|
|
|
+ // 计算实际图片中二维码应该占的相对比例
|
|
|
+ double actualRelativeSize = previewRelativeSize * canvasWidth;
|
|
|
+
|
|
|
+ // 计算二维码的实际缩放比例
|
|
|
+ double previewScale = Math.min(1.0, actualRelativeSize / Math.max(qrImage.getWidth(), qrImage.getHeight()));
|
|
|
+
|
|
|
+ // 计算最终缩放:预览缩放 × 用户缩放
|
|
|
+ double finalScale = previewScale * barcodeScale;
|
|
|
+ int scaledWidth = (int) (qrImage.getWidth() * finalScale);
|
|
|
+ int scaledHeight = (int) (qrImage.getHeight() * finalScale);
|
|
|
+
|
|
|
+ // 边界检查,确保二维码不会超出图片范围
|
|
|
+ if (x < 0) x = 0;
|
|
|
+ if (y < 0) y = 0;
|
|
|
+ if (x + scaledWidth > canvasWidth) x = canvasWidth - scaledWidth;
|
|
|
+ if (y + scaledHeight > canvasHeight) y = canvasHeight - scaledHeight;
|
|
|
+
|
|
|
+ // 绘制二维码
|
|
|
+ g2d.drawImage(qrImage, x, y, scaledWidth, scaledHeight, null);
|
|
|
+ log.info("二维码绘制完成 - 位置: ({}, {}), 尺寸: {}x{}, 预览相对比例: {}, 实际相对大小: {}, 预览缩放: {}, 用户缩放: {}, 最终缩放: {}, 画布尺寸: {}x{}",
|
|
|
+ x, y, scaledWidth, scaledHeight, previewRelativeSize, actualRelativeSize, previewScale, barcodeScale, finalScale, canvasWidth, canvasHeight);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("绘制二维码失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成二维码数据
|
|
|
+ */
|
|
|
+ private String generateQRCodeData(GameAthleteVo athlete) {
|
|
|
+ return String.format("运动员编号: %s, 姓名: %s, 队伍: %s",
|
|
|
+ athlete.getAthleteCode(),
|
|
|
+ athlete.getName(),
|
|
|
+ athlete.getTeamName() != null ? athlete.getTeamName() : "未知队伍");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建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();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
}
|