|
@@ -1,29 +1,19 @@
|
|
|
package org.dromara.system.service.impl;
|
|
package org.dromara.system.service.impl;
|
|
|
|
|
|
|
|
import cn.hutool.core.collection.CollectionUtil;
|
|
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.query.LambdaQueryWrapper;
|
|
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
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.core.toolkit.Wrappers;
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import com.google.zxing.BarcodeFormat;
|
|
import com.google.zxing.BarcodeFormat;
|
|
|
import com.google.zxing.EncodeHintType;
|
|
import com.google.zxing.EncodeHintType;
|
|
|
-import com.google.zxing.MultiFormatWriter;
|
|
|
|
|
import com.google.zxing.common.BitMatrix;
|
|
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.annotation.Resource;
|
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
|
-import jakarta.validation.constraints.NotNull;
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
-import org.apache.commons.io.IOUtils;
|
|
|
|
|
import org.apache.commons.io.output.ByteArrayOutputStream;
|
|
import org.apache.commons.io.output.ByteArrayOutputStream;
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
import org.apache.poi.ss.usermodel.*;
|
|
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.PageQuery;
|
|
|
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
|
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.GameAthlete;
|
|
|
|
|
import org.dromara.system.domain.GameEvent;
|
|
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.bo.*;
|
|
|
import org.dromara.system.domain.constant.GameEventConstant;
|
|
import org.dromara.system.domain.constant.GameEventConstant;
|
|
|
import org.dromara.system.domain.vo.*;
|
|
import org.dromara.system.domain.vo.*;
|
|
|
-import org.dromara.system.mapper.GameAthleteMapper;
|
|
|
|
|
import org.dromara.system.mapper.GameEventMapper;
|
|
import org.dromara.system.mapper.GameEventMapper;
|
|
|
import org.dromara.system.config.FileUploadConfig;
|
|
import org.dromara.system.config.FileUploadConfig;
|
|
|
import org.dromara.system.service.*;
|
|
import org.dromara.system.service.*;
|
|
@@ -65,12 +51,9 @@ import java.awt.Color;
|
|
|
import java.awt.Font;
|
|
import java.awt.Font;
|
|
|
import java.awt.image.BufferedImage;
|
|
import java.awt.image.BufferedImage;
|
|
|
import java.io.*;
|
|
import java.io.*;
|
|
|
-import java.nio.file.Files;
|
|
|
|
|
-import java.nio.file.Paths;
|
|
|
|
|
import java.time.Duration;
|
|
import java.time.Duration;
|
|
|
import java.util.*;
|
|
import java.util.*;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
-import java.util.concurrent.CompletableFuture;
|
|
|
|
|
import java.util.concurrent.ExecutorService;
|
|
import java.util.concurrent.ExecutorService;
|
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.Executors;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.TimeUnit;
|
|
@@ -541,8 +524,6 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
/**
|
|
/**
|
|
|
* 统计赛事个数
|
|
* 统计赛事个数
|
|
|
* 0:所有 1:未开始 2:进行中 3:已结束
|
|
* 0:所有 1:未开始 2:进行中 3:已结束
|
|
|
- *
|
|
|
|
|
- * @return
|
|
|
|
|
*/
|
|
*/
|
|
|
@Override
|
|
@Override
|
|
|
public Long countGameEvent(Long type) {
|
|
public Long countGameEvent(Long type) {
|
|
@@ -551,9 +532,6 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 获取默认赛事的号码对照表
|
|
* 获取默认赛事的号码对照表
|
|
|
- *
|
|
|
|
|
- * @param eventId
|
|
|
|
|
- * @return
|
|
|
|
|
*/
|
|
*/
|
|
|
@Override
|
|
@Override
|
|
|
public List<AthleteNumberTableVO> getNumberTable(Long eventId) {
|
|
public List<AthleteNumberTableVO> getNumberTable(Long eventId) {
|
|
@@ -590,19 +568,8 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
return numberTable;
|
|
return numberTable;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 使用poi生成号码对照表
|
|
|
|
|
- *
|
|
|
|
|
- * @param response
|
|
|
|
|
- * @param eventId
|
|
|
|
|
- */
|
|
|
|
|
/**
|
|
/**
|
|
|
* 使用poi生成号码对照表
|
|
* 使用poi生成号码对照表
|
|
|
- *
|
|
|
|
|
- * @param request
|
|
|
|
|
- * @param response
|
|
|
|
|
- * @param eventId
|
|
|
|
|
*/
|
|
*/
|
|
|
@Override
|
|
@Override
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
@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 {
|
|
private static byte[] generateQRCode(String data, int width, int height) throws Exception {
|
|
|
|
|
+ // 使用QRCodeWriter而不是MultiFormatWriter,以便更好地控制二维码生成
|
|
|
|
|
+ QRCodeWriter qrCodeWriter = new QRCodeWriter();
|
|
|
|
|
+
|
|
|
Map<EncodeHintType, Object> hints = new HashMap<>();
|
|
Map<EncodeHintType, Object> hints = new HashMap<>();
|
|
|
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
|
|
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 x = 0; x < width; x++) {
|
|
|
for (int y = 0; y < height; y++) {
|
|
for (int y = 0; y < height; y++) {
|
|
|
- // 如果该位置是背景,则设置为完全透明;如果是数据点,则保持不透明
|
|
|
|
|
|
|
+ // 黑色数据点,透明背景
|
|
|
int color = matrix.get(x, y) ? 0xFF000000 : 0x00000000;
|
|
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();
|
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
|
- ImageIO.write(image, "png", out);
|
|
|
|
|
|
|
+ ImageIO.write(qrImage, "png", out);
|
|
|
return out.toByteArray();
|
|
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();
|
|
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文件(直接在内存中生成,不保存单个图片文件)
|
|
// 创建ZIP文件(直接在内存中生成,不保存单个图片文件)
|
|
|
String zipFilePath = resultDir + "参赛证_" + taskId + ".zip";
|
|
String zipFilePath = resultDir + "参赛证_" + taskId + ".zip";
|
|
|
createZipFileFromMemory(templateImage, athleteVoList, bibParam, zipFilePath, taskId);
|
|
createZipFileFromMemory(templateImage, athleteVoList, bibParam, zipFilePath, taskId);
|
|
@@ -1632,15 +1137,28 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
*/
|
|
*/
|
|
|
private byte[] generateSingleBibFromTemplate(byte[] templateImage, GameAthleteVo athlete, GenerateBibBo bibParam) {
|
|
private byte[] generateSingleBibFromTemplate(byte[] templateImage, GameAthleteVo athlete, GenerateBibBo bibParam) {
|
|
|
try {
|
|
try {
|
|
|
|
|
+ // 检查模板图片数据是否为空
|
|
|
|
|
+ if (templateImage == null || templateImage.length == 0) {
|
|
|
|
|
+ log.error("模板图片数据为空");
|
|
|
|
|
+ throw new RuntimeException("模板图片数据为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("模板图片数据长度: {} bytes", templateImage.length);
|
|
|
|
|
+
|
|
|
// 使用BufferedImage处理模版图片
|
|
// 使用BufferedImage处理模版图片
|
|
|
BufferedImage originalTemplate = ImageIO.read(new ByteArrayInputStream(templateImage));
|
|
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 targetWidth = bibParam.getCanvasWidth() != null ? bibParam.getCanvasWidth() : originalTemplate.getWidth();
|
|
|
int targetHeight = bibParam.getCanvasHeight() != null ? bibParam.getCanvasHeight() : originalTemplate.getHeight();
|
|
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();
|
|
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_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
|
|
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
|
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);
|
|
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());
|
|
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;
|
|
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);
|
|
g2d.setFont(font);
|
|
|
|
|
|
|
|
// 设置颜色
|
|
// 设置颜色
|
|
|
Color color = new Color(bibParam.getFontColor());
|
|
Color color = new Color(bibParam.getFontColor());
|
|
|
g2d.setColor(color);
|
|
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();
|
|
FontMetrics fm = g2d.getFontMetrics();
|
|
|
int textWidth = fm.stringWidth(number);
|
|
int textWidth = fm.stringWidth(number);
|
|
@@ -1713,9 +1252,22 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
String qrData = generateQRCodeData(athlete);
|
|
String qrData = generateQRCodeData(athlete);
|
|
|
log.info("生成二维码数据: {}", qrData);
|
|
log.info("生成二维码数据: {}", qrData);
|
|
|
|
|
|
|
|
- // 生成二维码图片 - 与前端保持一致的初始大小
|
|
|
|
|
|
|
+ // 确定最终绘制的二维码尺寸
|
|
|
int initialQrSize = 32;
|
|
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));
|
|
BufferedImage qrImage = ImageIO.read(new ByteArrayInputStream(qrCodeBytes));
|
|
|
|
|
|
|
|
// 使用前端传递的像素坐标
|
|
// 使用前端传递的像素坐标
|
|
@@ -1726,26 +1278,15 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
log.info("使用前端传递的二维码坐标: ({}, {})", x, y);
|
|
log.info("使用前端传递的二维码坐标: ({}, {})", x, y);
|
|
|
} else {
|
|
} else {
|
|
|
// 使用默认位置(右下角)
|
|
// 使用默认位置(右下角)
|
|
|
- x = canvasWidth - (initialQrSize * 3) - 10;
|
|
|
|
|
- y = canvasHeight - (initialQrSize * 3) - 10;
|
|
|
|
|
|
|
+ x = canvasWidth - scaledWidth - 10;
|
|
|
|
|
+ y = canvasHeight - scaledHeight - 10;
|
|
|
log.info("使用默认二维码位置: ({}, {})", x, y);
|
|
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;
|
|
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实现,用于处理内存中的字节数组
|
|
* 自定义MultipartFile实现,用于处理内存中的字节数组
|
|
|
*/
|
|
*/
|