|
@@ -1,49 +1,56 @@
|
|
|
package org.dromara.system.service.impl;
|
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
|
-import jakarta.annotation.PostConstruct;
|
|
|
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+import com.google.zxing.BarcodeFormat;
|
|
|
+import com.google.zxing.EncodeHintType;
|
|
|
+import com.google.zxing.MultiFormatWriter;
|
|
|
+import com.google.zxing.common.BitMatrix;
|
|
|
+import com.itextpdf.text.BaseColor;
|
|
|
+import com.itextpdf.text.Document;
|
|
|
+import com.itextpdf.text.Image;
|
|
|
+import com.itextpdf.text.Rectangle;
|
|
|
+import com.itextpdf.text.pdf.BaseFont;
|
|
|
+import com.itextpdf.text.pdf.PdfContentByte;
|
|
|
+import com.itextpdf.text.pdf.PdfWriter;
|
|
|
import jakarta.annotation.Resource;
|
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.apache.commons.io.output.ByteArrayOutputStream;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
import org.apache.poi.ss.usermodel.*;
|
|
|
import org.apache.poi.ss.util.CellRangeAddress;
|
|
|
-import org.apache.poi.xssf.usermodel.XSSFSheet;
|
|
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
|
|
import org.dromara.common.core.exception.ServiceException;
|
|
|
import org.dromara.common.core.utils.MapstructUtils;
|
|
|
-import org.dromara.common.core.utils.StringUtils;
|
|
|
import org.dromara.common.json.utils.JsonUtils;
|
|
|
-import org.dromara.common.mybatis.core.page.TableDataInfo;
|
|
|
import org.dromara.common.mybatis.core.page.PageQuery;
|
|
|
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
-import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
-import lombok.RequiredArgsConstructor;
|
|
|
-import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.dromara.common.mybatis.core.page.TableDataInfo;
|
|
|
import org.dromara.common.redis.utils.RedisUtils;
|
|
|
+import org.dromara.system.domain.GameEvent;
|
|
|
import org.dromara.system.domain.bo.GameAthleteBo;
|
|
|
+import org.dromara.system.domain.bo.GameEventBo;
|
|
|
import org.dromara.system.domain.bo.GameTeamBo;
|
|
|
+import org.dromara.system.domain.bo.GenerateBibBo;
|
|
|
import org.dromara.system.domain.constant.GameEventConstant;
|
|
|
-import org.dromara.system.domain.vo.AthleteCodeVo;
|
|
|
-import org.dromara.system.domain.vo.AthleteNumberTableVO;
|
|
|
-import org.dromara.system.domain.vo.GameAthleteVo;
|
|
|
-import org.dromara.system.domain.vo.GameTeamVo;
|
|
|
+import org.dromara.system.domain.vo.*;
|
|
|
+import org.dromara.system.mapper.GameEventMapper;
|
|
|
import org.dromara.system.service.IGameAthleteService;
|
|
|
+import org.dromara.system.service.IGameEventProjectService;
|
|
|
+import org.dromara.system.service.IGameEventService;
|
|
|
import org.dromara.system.service.IGameTeamService;
|
|
|
import org.springframework.context.annotation.Lazy;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
-import org.dromara.system.domain.bo.GameEventBo;
|
|
|
-import org.dromara.system.domain.vo.GameEventVo;
|
|
|
-import org.dromara.system.domain.GameEvent;
|
|
|
-import org.dromara.system.mapper.GameEventMapper;
|
|
|
-import org.dromara.system.service.IGameEventService;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
import org.springframework.util.CollectionUtils;
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
|
-import java.io.FileOutputStream;
|
|
|
+import javax.imageio.ImageIO;
|
|
|
+import java.awt.image.BufferedImage;
|
|
|
import java.io.IOException;
|
|
|
-import java.io.InputStream;
|
|
|
-import java.io.OutputStream;
|
|
|
import java.util.*;
|
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
|
import java.util.stream.Collectors;
|
|
@@ -65,6 +72,8 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
@Lazy
|
|
|
@Resource
|
|
|
private IGameAthleteService gameAthleteService;
|
|
|
+ @Resource
|
|
|
+ private IGameEventProjectService gameEventProjectService;
|
|
|
|
|
|
/**
|
|
|
* 查询赛事基本信息
|
|
@@ -379,7 +388,7 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
style.setAlignment(HorizontalAlignment.CENTER);
|
|
|
style.setVerticalAlignment(VerticalAlignment.CENTER);
|
|
|
// 字体加粗
|
|
|
- Font font = wb.createFont();
|
|
|
+ org.apache.poi.ss.usermodel.Font font = wb.createFont();
|
|
|
font.setBold(true);
|
|
|
style.setFont(font);
|
|
|
|
|
@@ -397,7 +406,7 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
|
|
|
try {
|
|
|
for (AthleteNumberTableVO teamNumberTable : numberTable) {
|
|
|
- if(teamNumberTable.getMemberCount()==0){
|
|
|
+ if (teamNumberTable.getMemberCount() == 0) {
|
|
|
continue;
|
|
|
}
|
|
|
// 2. 遍历队伍信息,为每个队伍创建一个工作簿
|
|
@@ -529,7 +538,7 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
|
|
|
// 自动调整列宽
|
|
|
for (int i = 0; i < 8; i++) {
|
|
|
- sheet.setColumnWidth(i,14*256);
|
|
|
+ sheet.setColumnWidth(i, 14 * 256);
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -558,4 +567,253 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void generateNumberBib(HttpServletResponse response, MultipartFile bgImage, MultipartFile logo, GenerateBibBo bibParam) {
|
|
|
+ //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);
|
|
|
+ //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.根据参数生成号码布
|
|
|
+ GameEventVo eventVo = baseMapper.selectVoById(defaultEventId);
|
|
|
+ generateBib(response, bgImage, logo, eventVo.getEventName(), athleteVoList, teamNameMap, projectMap, bibParam);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成号码布并直接通过 HttpServletResponse 返回 ZIP 文件
|
|
|
+ *
|
|
|
+ * @param response HttpServletResponse 对象(用于输出 ZIP)
|
|
|
+ * @param backgroundImage 背景图 (MultipartFile)
|
|
|
+ * @param logo Logo 图片(可选)
|
|
|
+ * @param eventName 赛事名称
|
|
|
+ * @param athleteList 运动员列表
|
|
|
+ * @param teamNameMap 队伍id名称映射
|
|
|
+ * @param projectMap 项目id名称映射
|
|
|
+ * @param bibParam 布局参数(位置、字体等)
|
|
|
+ * @throws Exception
|
|
|
+ */
|
|
|
+ public void generateBib(HttpServletResponse response,
|
|
|
+ MultipartFile backgroundImage,
|
|
|
+ MultipartFile logo,
|
|
|
+ String eventName,
|
|
|
+ List<GameAthleteVo> athleteList,
|
|
|
+ Map<Long, String> teamNameMap,
|
|
|
+ Map<Long, String> projectMap,
|
|
|
+ GenerateBibBo bibParam) {
|
|
|
+ try {
|
|
|
+ // 提取布局参数
|
|
|
+ Double logoX = bibParam.getLogoX();
|
|
|
+ Double logoY = bibParam.getLogoY();
|
|
|
+ Double qRCodeX = bibParam.getQRCodeX();
|
|
|
+ Double qRCodeY = bibParam.getQRCodeY();
|
|
|
+ String fontName = bibParam.getFontName();
|
|
|
+ Integer fontSize = bibParam.getFontSize();
|
|
|
+ Integer fontColor = bibParam.getFontColor();
|
|
|
+
|
|
|
+ // 设置默认值
|
|
|
+ if (fontSize == null) fontSize = 14;
|
|
|
+ BaseColor textColor = parseColor(fontColor);
|
|
|
+ BaseFont baseFont = getChineseFont(fontName);
|
|
|
+
|
|
|
+ // 读取背景图
|
|
|
+ Image bgImage = Image.getInstance(backgroundImage.getBytes());
|
|
|
+ float pageWidth = bgImage.getWidth();
|
|
|
+ float pageHeight = bgImage.getHeight();
|
|
|
+
|
|
|
+ // 提前读取 logo 字节数组
|
|
|
+ byte[] logoBytes = null;
|
|
|
+ if (logo != null && !logo.isEmpty()) {
|
|
|
+ logoBytes = logo.getBytes();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置响应头(返回 ZIP 压缩包)
|
|
|
+ response.reset();
|
|
|
+ response.setContentType("application/zip");
|
|
|
+ response.setHeader("Content-Disposition", "attachment; filename=\"athlete_bibs.zip\"");
|
|
|
+
|
|
|
+ try (java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(response.getOutputStream())) {
|
|
|
+
|
|
|
+ for (GameAthleteVo athlete : athleteList) {
|
|
|
+ // 每个运动员生成一个 PDF
|
|
|
+ ByteArrayOutputStream pdfStream = new ByteArrayOutputStream();
|
|
|
+ Document document = new Document(new Rectangle(pageWidth, pageHeight));
|
|
|
+ PdfWriter writer = PdfWriter.getInstance(document, pdfStream);
|
|
|
+ document.open();
|
|
|
+
|
|
|
+ PdfContentByte cb = writer.getDirectContent();
|
|
|
+
|
|
|
+ // 添加背景图
|
|
|
+ bgImage.setAbsolutePosition(0, 0);
|
|
|
+ document.add(bgImage);
|
|
|
+
|
|
|
+ // 添加Logo - 更精确的位置调整
|
|
|
+ if (logoBytes != null) {
|
|
|
+ try {
|
|
|
+ Image img = Image.getInstance(logoBytes);
|
|
|
+ img.scaleToFit(80, 80);
|
|
|
+
|
|
|
+ // 直接使用前端传递的坐标值,更精确的位置调整
|
|
|
+ float logoPositionX = logoX != null ? logoX.floatValue() : 50f;
|
|
|
+ float logoPositionY = logoY != null ? logoY.floatValue() : pageHeight - 130f;
|
|
|
+
|
|
|
+ // 更精确的位置调整
|
|
|
+ logoPositionX += 10; // 向右偏移10pt
|
|
|
+ logoPositionY -= 50; // 向下偏移50pt(与编号重合)
|
|
|
+
|
|
|
+ img.setAbsolutePosition(logoPositionX, logoPositionY);
|
|
|
+ cb.addImage(img);
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 忽略或记录日志
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算文本宽度和高度
|
|
|
+ float textWidth = baseFont.getWidthPoint(athlete.getAthleteCode(), fontSize);
|
|
|
+ float textHeight = fontSize;
|
|
|
+
|
|
|
+ // 确保文本垂直和水平都居中
|
|
|
+ float textPositionX = (pageWidth - textWidth) / 2; // 水平居中
|
|
|
+ float textPositionY = (pageHeight / 2) + (textHeight / 2); // 垂直居中
|
|
|
+
|
|
|
+ addText(cb, baseFont, fontSize, textColor, textPositionX, textPositionY, athlete.getAthleteCode());
|
|
|
+
|
|
|
+ // 添加赛事名称
|
|
|
+ if (eventName != null && !eventName.trim().isEmpty()) {
|
|
|
+
|
|
|
+ // 设置字体大小和颜色
|
|
|
+ int eventNameFontSize = 64; // 确保与绘制文字时的字体大小一致
|
|
|
+ cb.beginText();
|
|
|
+ cb.setFontAndSize(baseFont, eventNameFontSize);
|
|
|
+ cb.setColorFill(BaseColor.BLACK);
|
|
|
+
|
|
|
+ // 计算文本宽度和X坐标以实现水平居中
|
|
|
+ textWidth = baseFont.getWidthPoint(eventName, eventNameFontSize);
|
|
|
+ float textX = (pageWidth - textWidth) / 2;
|
|
|
+
|
|
|
+ // 设置文本垂直位置使其位于页面顶部,留出一定间距
|
|
|
+ float textY = pageHeight - eventNameFontSize - 10; // 调整这个值以改变顶部间距
|
|
|
+
|
|
|
+ cb.setTextMatrix(textX, textY);
|
|
|
+ cb.showText(eventName);
|
|
|
+ cb.endText();
|
|
|
+ }
|
|
|
+ StringBuilder joinProject = new StringBuilder();
|
|
|
+ athlete.getProjectList().forEach(
|
|
|
+ projectId -> joinProject.append(projectMap.get(Long.valueOf(projectId))).append(" ")
|
|
|
+ );
|
|
|
+
|
|
|
+ // 生成二维码 - 更精确的位置调整
|
|
|
+ String qrData = String.format(
|
|
|
+ """
|
|
|
+ 赛事名称:%s,
|
|
|
+ 运动员序号:%d,
|
|
|
+ 运动员编号:%s,
|
|
|
+ 参与项目:%s,
|
|
|
+ 队伍id:%d,
|
|
|
+ 队伍名称:%s
|
|
|
+ 运动员姓名:%s
|
|
|
+ 性别:%s,
|
|
|
+ 年龄:%d
|
|
|
+ """,
|
|
|
+ eventName, athlete.getAthleteId(), athlete.getAthleteCode(),
|
|
|
+ joinProject.toString(),
|
|
|
+ athlete.getTeamId(), teamNameMap.get(athlete.getTeamId()), athlete.getName(), athlete.getGender(), athlete.getAge()
|
|
|
+ );
|
|
|
+
|
|
|
+ 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());
|
|
|
+ zos.closeEntry();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 强制刷新输出流
|
|
|
+ zos.flush();
|
|
|
+ response.flushBuffer();
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException("生成号码布 ZIP 失败", e);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException(e);
|
|
|
+ } finally {
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 工具方法:添加文本
|
|
|
+ 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 {
|
|
|
+ Map<EncodeHintType, Object> hints = new HashMap<>();
|
|
|
+ hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
|
|
|
+ BitMatrix matrix = new MultiFormatWriter().encode(data, BarcodeFormat.QR_CODE, width, height, hints);
|
|
|
+
|
|
|
+ // 创建BufferedImage并设置透明度
|
|
|
+ BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
|
|
+ for (int x = 0; x < width; x++) {
|
|
|
+ for (int y = 0; y < height; y++) {
|
|
|
+ // 如果该位置是背景,则设置为完全透明;如果是数据点,则保持不透明
|
|
|
+ int color = matrix.get(x, y) ? 0xFF000000 : 0x00000000;
|
|
|
+ image.setRGB(x, y, color);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将BufferedImage写入ByteArrayOutputStream
|
|
|
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
|
+ ImageIO.write(image, "png", out);
|
|
|
+ return out.toByteArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 工具方法:解析颜色 (0xRRGGBB)
|
|
|
+ private static BaseColor parseColor(Integer colorInt) {
|
|
|
+ if (colorInt == null) return BaseColor.BLACK;
|
|
|
+ return new BaseColor(
|
|
|
+ (colorInt >> 16) & 0xFF,
|
|
|
+ (colorInt >> 8) & 0xFF,
|
|
|
+ colorInt & 0xFF
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 工具方法:获取中文字体(推荐将字体文件打包进 resources)
|
|
|
+ private static BaseFont getChineseFont(String fontName) throws Exception {
|
|
|
+ // 方式1:使用系统字体(Windows)
|
|
|
+ return BaseFont.createFont("C:/Windows/Fonts/simhei.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
|
|
|
+
|
|
|
+ // 方式2:使用项目内嵌字体(推荐)
|
|
|
+ // return BaseFont.createFont("classpath:fonts/simhei.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
|
|
|
+ }
|
|
|
}
|