|
@@ -1,30 +1,58 @@
|
|
|
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.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.constant.GameEventConstant;
|
|
|
-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.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.*;
|
|
|
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.springframework.transaction.annotation.Transactional;
|
|
|
+import org.springframework.util.CollectionUtils;
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
|
-import java.util.List;
|
|
|
-import java.util.Map;
|
|
|
-import java.util.Collection;
|
|
|
+import javax.imageio.ImageIO;
|
|
|
+import java.awt.image.BufferedImage;
|
|
|
+import java.io.IOException;
|
|
|
+import java.util.*;
|
|
|
+import java.util.concurrent.atomic.AtomicLong;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
/**
|
|
@@ -34,11 +62,18 @@ import java.util.stream.Collectors;
|
|
|
* @date 2025-07-30
|
|
|
*/
|
|
|
@Slf4j
|
|
|
-@RequiredArgsConstructor
|
|
|
@Service
|
|
|
public class GameEventServiceImpl implements IGameEventService {
|
|
|
|
|
|
- private final GameEventMapper baseMapper;
|
|
|
+ @Resource
|
|
|
+ private GameEventMapper baseMapper;
|
|
|
+ @Resource
|
|
|
+ private IGameTeamService gameTeamService;
|
|
|
+ @Lazy
|
|
|
+ @Resource
|
|
|
+ private IGameAthleteService gameAthleteService;
|
|
|
+ @Resource
|
|
|
+ private IGameEventProjectService gameEventProjectService;
|
|
|
|
|
|
/**
|
|
|
* 查询赛事基本信息
|
|
@@ -191,12 +226,8 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
*/
|
|
|
@Override
|
|
|
public Map<String, Long> getEventIdNameMap() {
|
|
|
- List<GameEvent> idNameList = this.baseMapper.selectList(
|
|
|
- new LambdaQueryWrapper<GameEvent>()
|
|
|
- .select(GameEvent::getEventId, GameEvent::getEventName)
|
|
|
- );
|
|
|
- Map<String, Long> idNameMap = idNameList.stream()
|
|
|
- .collect(Collectors.toMap(GameEvent::getEventName, GameEvent::getEventId));
|
|
|
+ List<GameEvent> idNameList = this.baseMapper.selectList(new LambdaQueryWrapper<GameEvent>().select(GameEvent::getEventId, GameEvent::getEventName));
|
|
|
+ Map<String, Long> idNameMap = idNameList.stream().collect(Collectors.toMap(GameEvent::getEventName, GameEvent::getEventId));
|
|
|
return idNameMap;
|
|
|
}
|
|
|
|
|
@@ -253,8 +284,7 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
public int updateEventDefault(GameEventBo bo) {
|
|
|
GameEvent event = MapstructUtils.convert(bo, GameEvent.class);
|
|
|
// 先将所有赛事设置为非默认
|
|
|
- int row = baseMapper.update(null, new LambdaUpdateWrapper<GameEvent>()
|
|
|
- .set(GameEvent::getIsDefault, "1"));
|
|
|
+ int row = baseMapper.update(null, new LambdaUpdateWrapper<GameEvent>().set(GameEvent::getIsDefault, "1"));
|
|
|
// 再将指定赛事设置为默认
|
|
|
row += baseMapper.updateById(event);
|
|
|
if (row > 0) {
|
|
@@ -274,12 +304,521 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
*/
|
|
|
@Override
|
|
|
public Long countGameEvent(Long type) {
|
|
|
- return this.baseMapper.selectCount(
|
|
|
- Wrappers.lambdaQuery(GameEvent.class)
|
|
|
- .apply(type == 0, "1=1")
|
|
|
- .apply(type == 1, "start_time > now()")
|
|
|
- .apply(type == 2, "start_time <= now() and end_time >= now()")
|
|
|
- .apply(type == 3, "end_time < now()")
|
|
|
+ return this.baseMapper.selectCount(Wrappers.lambdaQuery(GameEvent.class).apply(type == 0, "1=1").apply(type == 1, "start_time > now()").apply(type == 2, "start_time <= now() and end_time >= now()").apply(type == 3, "end_time < now()"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取默认赛事的号码对照表
|
|
|
+ *
|
|
|
+ * @param eventId
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public List<AthleteNumberTableVO> getNumberTable(Long eventId) {
|
|
|
+ //1.查询当前赛事下的所有队伍
|
|
|
+ GameTeamBo bo = new GameTeamBo();
|
|
|
+ bo.setEventId(eventId);
|
|
|
+ List<GameTeamVo> gameTeam = gameTeamService.queryList(bo);
|
|
|
+ //2.查询当前赛事下所有运动员
|
|
|
+ GameAthleteBo gameAthleteBo = new GameAthleteBo();
|
|
|
+ gameAthleteBo.setEventId(eventId);
|
|
|
+ Map<Long, GameAthleteVo> athleteVoMap = gameAthleteService.queryList(gameAthleteBo).stream().collect(Collectors.toMap(GameAthleteVo::getAthleteId, v -> v));
|
|
|
+ //3.组装map
|
|
|
+ List<AthleteNumberTableVO> numberTable = new ArrayList<>();
|
|
|
+ for (GameTeamVo vo : gameTeam) {
|
|
|
+ List<AthleteCodeVo> athleteCodeVos = new ArrayList<>();
|
|
|
+ AtomicLong memberCount = new AtomicLong(0);
|
|
|
+ AthleteNumberTableVO athleteNumberTableVO = new AthleteNumberTableVO();
|
|
|
+ vo.getAthleteList().forEach(athleteId -> {
|
|
|
+ GameAthleteVo athleteVo = athleteVoMap.get(Long.valueOf(athleteId));
|
|
|
+ if (athleteVo != null) {
|
|
|
+ AthleteCodeVo athleteCodeVo = new AthleteCodeVo();
|
|
|
+ athleteCodeVo.setId(memberCount.incrementAndGet());
|
|
|
+ athleteCodeVo.setCode(athleteVo.getAthleteCode());
|
|
|
+ athleteCodeVo.setName(athleteVo.getName());
|
|
|
+ athleteCodeVos.add(athleteCodeVo);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ athleteNumberTableVO.setNumberRange(vo.getNumberRange());
|
|
|
+ athleteNumberTableVO.setTeamName(vo.getTeamName());
|
|
|
+ athleteNumberTableVO.setMemberCount(memberCount.get());
|
|
|
+ athleteNumberTableVO.setAthleteCodeVos(athleteCodeVos);
|
|
|
+ numberTable.add(athleteNumberTableVO);
|
|
|
+ }
|
|
|
+ return numberTable;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 使用poi生成号码对照表
|
|
|
+ *
|
|
|
+ * @param response
|
|
|
+ * @param eventId
|
|
|
+ */
|
|
|
+ /**
|
|
|
+ * 使用poi生成号码对照表
|
|
|
+ *
|
|
|
+ * @param request
|
|
|
+ * @param response
|
|
|
+ * @param eventId
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public void exportNumberTable(HttpServletRequest request, HttpServletResponse response, Long eventId) {
|
|
|
+
|
|
|
+ // 获取当前赛事的队伍信息
|
|
|
+ List<AthleteNumberTableVO> numberTable = this.getNumberTable(eventId);
|
|
|
+ if (CollectionUtils.isEmpty(numberTable)) {
|
|
|
+ throw new ServiceException("该赛事无队伍数据");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 创建Excel表格
|
|
|
+ Workbook wb = new XSSFWorkbook();
|
|
|
+
|
|
|
+ // 1.2 创建公共样式:边框 + 居中 + 加粗
|
|
|
+ CellStyle style = wb.createCellStyle();
|
|
|
+ style.setBorderTop(BorderStyle.THIN);
|
|
|
+ style.setBorderBottom(BorderStyle.THIN);
|
|
|
+ style.setBorderLeft(BorderStyle.THIN);
|
|
|
+ style.setBorderRight(BorderStyle.THIN);
|
|
|
+ style.setTopBorderColor(IndexedColors.BLACK.getIndex());
|
|
|
+ style.setBottomBorderColor(IndexedColors.BLACK.getIndex());
|
|
|
+ style.setLeftBorderColor(IndexedColors.BLACK.getIndex());
|
|
|
+ style.setRightBorderColor(IndexedColors.BLACK.getIndex());
|
|
|
+ style.setAlignment(HorizontalAlignment.CENTER);
|
|
|
+ style.setVerticalAlignment(VerticalAlignment.CENTER);
|
|
|
+ // 字体加粗
|
|
|
+ org.apache.poi.ss.usermodel.Font font = wb.createFont();
|
|
|
+ font.setBold(true);
|
|
|
+ style.setFont(font);
|
|
|
+
|
|
|
+ // 创建带浅灰色背景的样式(用于序号行)
|
|
|
+ CellStyle backgroundFillStyle = wb.createCellStyle();
|
|
|
+ backgroundFillStyle.cloneStyleFrom(style);
|
|
|
+ backgroundFillStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
|
|
|
+ backgroundFillStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
|
|
+
|
|
|
+ // 定义1-8列的样式(当前统一使用style,未来可差异化)
|
|
|
+ CellStyle[] columnStyles = new CellStyle[8];
|
|
|
+ for (int i = 0; i < 8; i++) {
|
|
|
+ columnStyles[i] = style;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ for (AthleteNumberTableVO teamNumberTable : numberTable) {
|
|
|
+ if (teamNumberTable.getMemberCount() == 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // 2. 遍历队伍信息,为每个队伍创建一个工作簿
|
|
|
+ Sheet sheet = wb.createSheet(teamNumberTable.getTeamName());
|
|
|
+ // 3. 按照规定的格式渲染
|
|
|
+ // 3.1 创建表头(号码对照表) 第一行 合并8个单元格 即0行7列
|
|
|
+ Row headerTitleRow = sheet.createRow(0);
|
|
|
+ Cell headerTitleCell = headerTitleRow.createCell(0);
|
|
|
+ headerTitleCell.setCellValue("号码对照表");
|
|
|
+ CellRangeAddress headerTitleRegion = new CellRangeAddress(0, 0, 0, 7);
|
|
|
+ sheet.addMergedRegion(headerTitleRegion);
|
|
|
+ // 应用样式到合并区域的所有单元格
|
|
|
+ for (int i = headerTitleRegion.getFirstColumn(); i <= headerTitleRegion.getLastColumn(); i++) {
|
|
|
+ Cell cell = headerTitleRow.getCell(i, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
|
|
|
+ cell.setCellStyle(backgroundFillStyle);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3.2 创建表头(队伍名称、人数、号码段) 第二行,按照指定格式:AB列合并、C列空白、DE列合并、F列空白、GH列合并
|
|
|
+ Row teamInfoRow = sheet.createRow(1);
|
|
|
+
|
|
|
+ // 设置队伍名称,并合并AB列(0-1列)
|
|
|
+ Cell teamNameInfoCell = teamInfoRow.createCell(0);
|
|
|
+ teamNameInfoCell.setCellValue(teamNumberTable.getTeamName());
|
|
|
+ CellRangeAddress teamNameInfoRegion = new CellRangeAddress(1, 1, 0, 1); // 合并AB列
|
|
|
+ sheet.addMergedRegion(teamNameInfoRegion);
|
|
|
+ teamNameInfoCell.setCellStyle(style); // 应用样式
|
|
|
+
|
|
|
+ // C列保持空白,但需创建单元格并应用样式
|
|
|
+ Cell cBlankCell = teamInfoRow.createCell(2);
|
|
|
+ cBlankCell.setCellStyle(style); // 应用样式
|
|
|
+
|
|
|
+ // 设置人数,并合并DE列(3-4列)
|
|
|
+ Cell teamMemberInfoCell = teamInfoRow.createCell(3);
|
|
|
+ teamMemberInfoCell.setCellValue(teamNumberTable.getMemberCount() + "人");
|
|
|
+ CellRangeAddress teamMemberInfoRegion = new CellRangeAddress(1, 1, 3, 4); // 合并DE列
|
|
|
+ sheet.addMergedRegion(teamMemberInfoRegion);
|
|
|
+ teamMemberInfoCell.setCellStyle(style); // 应用样式
|
|
|
+
|
|
|
+ // F列保持空白,但需创建单元格并应用样式
|
|
|
+ Cell fBlankCell = teamInfoRow.createCell(5);
|
|
|
+ fBlankCell.setCellStyle(style); // 应用样式
|
|
|
+
|
|
|
+ // 设置号码段,并合并GH列(6-7列)
|
|
|
+ Cell teamNumberInfoCell = teamInfoRow.createCell(6);
|
|
|
+ teamNumberInfoCell.setCellValue(teamNumberTable.getNumberRange());
|
|
|
+ CellRangeAddress teamNumberInfoRegion = new CellRangeAddress(1, 1, 6, 7); // 合并GH列
|
|
|
+ sheet.addMergedRegion(teamNumberInfoRegion);
|
|
|
+ Cell teamNumberInfoCell1 = teamInfoRow.createCell(7);
|
|
|
+ teamNumberInfoCell.setCellStyle(style); // 应用样式
|
|
|
+ teamNumberInfoCell1.setCellStyle(style); // 应用样式
|
|
|
+
|
|
|
+ // 确保所有列都应用了样式
|
|
|
+ for (int i = 0; i <= 7; i++) {
|
|
|
+ Cell cell = teamInfoRow.getCell(i, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
|
|
|
+ if (cell != null && cell.getCellStyle() == null) { // 如果当前单元格没有样式,则应用默认样式
|
|
|
+ cell.setCellStyle(style);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3.3 渲染运动员数据(序号、号码、姓名),每8人为一组,每组3行
|
|
|
+ AtomicLong cellCount = new AtomicLong(0);
|
|
|
+ List<AthleteCodeVo> athletes = teamNumberTable.getAthleteCodeVos();
|
|
|
+
|
|
|
+ for (AthleteCodeVo athlete : athletes) {
|
|
|
+ int groupIndex = Math.toIntExact(cellCount.get() / 8); // 第几组
|
|
|
+ int colIndex = Math.toIntExact(cellCount.get() % 8); // 第几列 (0-7)
|
|
|
+
|
|
|
+ int idRowIdx = groupIndex * 3 + 2; // 序号行索引
|
|
|
+ int codeRowIdx = idRowIdx + 1; // 号码行索引
|
|
|
+ int nameRowIdx = idRowIdx + 2; // 姓名行索引
|
|
|
+
|
|
|
+ // 获取或创建行
|
|
|
+ Row idRow = sheet.getRow(idRowIdx);
|
|
|
+ if (idRow == null) idRow = sheet.createRow(idRowIdx);
|
|
|
+
|
|
|
+ Row codeRow = sheet.getRow(codeRowIdx);
|
|
|
+ if (codeRow == null) codeRow = sheet.createRow(codeRowIdx);
|
|
|
+
|
|
|
+ Row nameRow = sheet.getRow(nameRowIdx);
|
|
|
+ if (nameRow == null) nameRow = sheet.createRow(nameRowIdx);
|
|
|
+
|
|
|
+ // 创建并设置序号单元格(带灰色背景)
|
|
|
+ Cell idCell = idRow.createCell(colIndex);
|
|
|
+ idCell.setCellValue(athlete.getId());
|
|
|
+ idCell.setCellStyle(backgroundFillStyle);
|
|
|
+
|
|
|
+ // 创建并设置号码单元格
|
|
|
+ Cell codeCell = codeRow.createCell(colIndex);
|
|
|
+ codeCell.setCellValue(athlete.getCode());
|
|
|
+ codeCell.setCellStyle(columnStyles[colIndex]);
|
|
|
+
|
|
|
+ // 创建并设置姓名单元格
|
|
|
+ Cell nameCell = nameRow.createCell(colIndex);
|
|
|
+ nameCell.setCellValue(athlete.getName());
|
|
|
+ nameCell.setCellStyle(columnStyles[colIndex]);
|
|
|
+
|
|
|
+ cellCount.incrementAndGet();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 补全最后一组的空列(确保1-8列都有样式)
|
|
|
+ if (!athletes.isEmpty()) {
|
|
|
+ int lastGroupIndex = Math.toIntExact((cellCount.get() - 1) / 8);
|
|
|
+ int lastFilledCol = Math.toIntExact((cellCount.get() - 1) % 8);
|
|
|
+
|
|
|
+ if (lastFilledCol < 7) {
|
|
|
+ int idRowIdx = lastGroupIndex * 3 + 2;
|
|
|
+ int codeRowIdx = idRowIdx + 1;
|
|
|
+ int nameRowIdx = idRowIdx + 2;
|
|
|
+
|
|
|
+ Row idRow = sheet.getRow(idRowIdx);
|
|
|
+ Row codeRow = sheet.getRow(codeRowIdx);
|
|
|
+ Row nameRow = sheet.getRow(nameRowIdx);
|
|
|
+
|
|
|
+ for (int col = lastFilledCol + 1; col < 8; col++) {
|
|
|
+ // 补序号(空但有背景)
|
|
|
+ Cell idCell = idRow.createCell(col);
|
|
|
+ idCell.setCellStyle(backgroundFillStyle);
|
|
|
+
|
|
|
+ // 补号码(空)
|
|
|
+ Cell codeCell = codeRow.createCell(col);
|
|
|
+ codeCell.setCellStyle(columnStyles[col]);
|
|
|
+
|
|
|
+ // 补姓名(空)
|
|
|
+ Cell nameCell = nameRow.createCell(col);
|
|
|
+ nameCell.setCellStyle(columnStyles[col]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 自动调整列宽
|
|
|
+ for (int i = 0; i < 8; i++) {
|
|
|
+ sheet.setColumnWidth(i, 14 * 256);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 渲染完所有队伍信息后返回Excel文件
|
|
|
+ response.reset();
|
|
|
+ response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
|
+ response.setCharacterEncoding("UTF-8");
|
|
|
+ String fileName = "号码对照表.xlsx";
|
|
|
+ response.setHeader("Content-Disposition", "attachment; filename=" + new String(fileName.getBytes("utf-8"), "ISO8859-1"));
|
|
|
+ String origin = request.getHeader("Origin");
|
|
|
+ if (origin != null) {
|
|
|
+ response.addHeader("Access-Control-Allow-Origin", origin);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 写入输出流
|
|
|
+ wb.write(response.getOutputStream());
|
|
|
+
|
|
|
+ } catch (IOException e) {
|
|
|
+ log.error("导出号码对照表异常:", e);
|
|
|
+ throw new ServiceException("导出失败:" + e.getMessage());
|
|
|
+ } finally {
|
|
|
+ try {
|
|
|
+ wb.close();
|
|
|
+ } catch (IOException e) {
|
|
|
+ log.error("关闭Workbook失败:", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @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);
|
|
|
+ switch (fontName) {
|
|
|
+ case "simhei":
|
|
|
+ return BaseFont.createFont("classpath:fonts/simhei.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
|
|
|
+ case "simsun":
|
|
|
+ return BaseFont.createFont("classpath:fonts/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
|
|
|
+ default:
|
|
|
+ return BaseFont.createFont("classpath:fonts/msyhbd.ttc", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|