Browse Source

feat:导出号码布zip、生成二维码、增加权限校验

wenkai 6 days ago
parent
commit
7f7a8a6cab

+ 26 - 4
ruoyi-modules/ruoyi-game-event/pom.xml

@@ -120,11 +120,33 @@
             <groupId>org.dromara</groupId>
             <artifactId>ruoyi-system</artifactId>
         </dependency>
+        <!-- iText -->
         <dependency>
-            <groupId>org.javassist</groupId>
-            <artifactId>javassist</artifactId>
-            <version>3.29.2-GA</version>
+            <groupId>com.itextpdf</groupId>
+            <artifactId>itextpdf</artifactId>
+            <version>5.5.13.2</version>
+        </dependency>
+        <!-- ZXing -->
+        <dependency>
+            <groupId>com.google.zxing</groupId>
+            <artifactId>core</artifactId>
+            <version>3.4.1</version>
+        </dependency>
+        <!-- ZXing -->
+        <dependency>
+            <groupId>com.google.zxing</groupId>
+            <artifactId>javase</artifactId>
+            <version>3.4.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-compress</artifactId>
+            <version>1.21</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.11.0</version>
         </dependency>
     </dependencies>
-
 </project>

+ 19 - 19
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/GameEventController.java

@@ -1,30 +1,31 @@
 package org.dromara.system.controller;
 
-import java.util.List;
-import java.util.Map;
-
-import lombok.RequiredArgsConstructor;
-import jakarta.servlet.http.HttpServletResponse;
-import jakarta.validation.constraints.*;
 import cn.dev33.satoken.annotation.SaCheckPermission;
-import org.dromara.common.core.exception.ServiceException;
-import org.dromara.common.redis.utils.RedisUtils;
-import org.dromara.system.domain.constant.GameEventConstant;
-import org.springframework.web.bind.annotation.*;
-import org.springframework.validation.annotation.Validated;
-import org.dromara.common.idempotent.annotation.RepeatSubmit;
-import org.dromara.common.log.annotation.Log;
-import org.dromara.common.web.core.BaseController;
-import org.dromara.common.mybatis.core.page.PageQuery;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
 import org.dromara.common.core.domain.R;
+import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.validate.AddGroup;
 import org.dromara.common.core.validate.EditGroup;
-import org.dromara.common.log.enums.BusinessType;
 import org.dromara.common.excel.utils.ExcelUtil;
-import org.dromara.system.domain.vo.GameEventVo;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.web.core.BaseController;
 import org.dromara.system.domain.bo.GameEventBo;
+import org.dromara.system.domain.constant.GameEventConstant;
+import org.dromara.system.domain.vo.GameEventVo;
 import org.dromara.system.service.IGameEventService;
-import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
 
 /**
  * 赛事基本信息
@@ -152,5 +153,4 @@ public class GameEventController extends BaseController {
         return R.ok(gameEventService.countGameEvent(type));
     }
 
-
 }

+ 1 - 4
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/MarkdownController.java

@@ -1,7 +1,6 @@
 package org.dromara.system.controller;
 
 import cn.dev33.satoken.annotation.SaCheckPermission;
-import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 import lombok.RequiredArgsConstructor;
 import org.dromara.common.core.domain.R;
@@ -17,8 +16,6 @@ import org.dromara.system.service.IAppEventMdService;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.List;
-
 /**
  * 赛事富文本
  *
@@ -62,7 +59,7 @@ public class MarkdownController extends BaseController {
     /**
      * 编辑移动端富文本
      */
-    @SaCheckPermission("system:eventMd:edit")
+    @SaCheckPermission("system:gameEvent:writeArticle")
     @Log(title = "移动端富文本", businessType = BusinessType.INSERT)
     @RepeatSubmit()
     @PostMapping()

+ 21 - 7
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/NumberController.java

@@ -1,22 +1,21 @@
 package org.dromara.system.controller;
 
 
+import cn.dev33.satoken.annotation.SaCheckPermission;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.system.domain.bo.GenerateBibBo;
 import org.dromara.system.domain.constant.GameEventConstant;
 import org.dromara.system.domain.vo.AthleteNumberTableVO;
 import org.dromara.system.service.IGameEventService;
 import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
-import java.util.Map;
 
 @Slf4j
 @Validated
@@ -40,13 +39,28 @@ public class NumberController {
 
 
     /**
-     *
-     * @return key:队伍名称 value:队伍运动员
+     * 导出号码对照表
      */
+    @SaCheckPermission("system:gameEvent:numberExport")
     @PostMapping("/export")
     public void exportNumberTable(HttpServletRequest request, HttpServletResponse response) {
         Object cacheObject = RedisUtils.getCacheObject(GameEventConstant.DEFAULT_EVENT_ID);
         Long eventId = Long.valueOf(cacheObject.toString());
         gameEventService.exportNumberTable(request, response, eventId);
     }
+
+
+    /**
+     * 生成号码布
+     *
+     */
+    @SaCheckPermission("system:gameEvent:numberBib")
+    @PostMapping("/generateBib")
+    public void generateNumberBib(HttpServletResponse response,
+                                  @RequestPart("bgImage") MultipartFile bgImage,
+                                  @RequestPart("logo") MultipartFile logo,
+                                  GenerateBibBo bibParam) {
+        gameEventService.generateNumberBib(response, bgImage, logo, bibParam);
+    }
+
 }

+ 33 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/GenerateBibBo.java

@@ -0,0 +1,33 @@
+package org.dromara.system.domain.bo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class GenerateBibBo implements Serializable {
+
+    /**
+     * logo位置
+     */
+    private Double logoX;
+    private Double logoY;
+
+    /**
+     * 二维码位置
+     */
+    private Double qRCodeX;
+    private Double qRCodeY;
+
+    /**
+     * 号码字体样式
+     */
+    private String fontName;
+    private Integer fontSize;
+    private Integer fontColor;
+
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+}

+ 7 - 5
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IGameEventService.java

@@ -2,13 +2,13 @@ package org.dromara.system.service;
 
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
-import org.dromara.common.core.domain.R;
-import org.dromara.system.domain.vo.AthleteCodeVo;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.system.domain.bo.GameEventBo;
+import org.dromara.system.domain.bo.GenerateBibBo;
 import org.dromara.system.domain.vo.AthleteNumberTableVO;
 import org.dromara.system.domain.vo.GameEventVo;
-import org.dromara.system.domain.bo.GameEventBo;
-import org.dromara.common.mybatis.core.page.TableDataInfo;
-import org.dromara.common.mybatis.core.page.PageQuery;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.Collection;
 import java.util.List;
@@ -118,4 +118,6 @@ public interface IGameEventService {
      * @param eventId
      */
     void exportNumberTable(HttpServletRequest request, HttpServletResponse response, Long eventId);
+
+    void generateNumberBib(HttpServletResponse response, MultipartFile bgImage, MultipartFile logo, GenerateBibBo bibParam);
 }

+ 7 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IGameTeamService.java

@@ -108,4 +108,11 @@ public interface IGameTeamService {
     Boolean updateTeamAthletes(Long teamId, List<Long> athleteIds);
 
     Long countByEventId(Long eventId);
+
+    /**
+     * 根据队伍id查询队伍名称映射
+     * @param teamIds
+     * @return
+     */
+    Map<Long, String> queryTeamIdAndName(Set<Long> teamIds);
 }

+ 282 - 24
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameEventServiceImpl.java

@@ -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);
+    }
 }

+ 27 - 12
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameTeamServiceImpl.java

@@ -2,31 +2,29 @@ package org.dromara.system.service.impl;
 
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
-import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
-import org.dromara.common.core.utils.MapstructUtils;
-import org.dromara.common.core.utils.StringUtils;
-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.CollectionUtils;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.redis.utils.RedisUtils;
 import org.dromara.system.domain.GameAthlete;
 import org.dromara.system.domain.GameEvent;
+import org.dromara.system.domain.GameTeam;
+import org.dromara.system.domain.bo.GameTeamBo;
 import org.dromara.system.domain.constant.GameEventConstant;
-import org.dromara.system.domain.vo.GameEventProjectVo;
+import org.dromara.system.domain.vo.GameTeamVo;
 import org.dromara.system.mapper.GameAthleteMapper;
 import org.dromara.system.mapper.GameEventMapper;
-import org.springframework.stereotype.Service;
-import org.dromara.system.domain.bo.GameTeamBo;
-import org.dromara.system.domain.vo.GameTeamVo;
-import org.dromara.system.domain.GameTeam;
 import org.dromara.system.mapper.GameTeamMapper;
 import org.dromara.system.service.IGameTeamService;
+import org.springframework.stereotype.Service;
 
-import javax.swing.text.html.Option;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -350,4 +348,21 @@ public class GameTeamServiceImpl implements IGameTeamService {
                 .eq(GameTeam::getEventId, eventId)
         );
     }
+
+    /**
+     * 根据队伍id查询队伍名称映射
+     *
+     * @param teamIds
+     * @return
+     */
+    @Override
+    public Map<Long, String> queryTeamIdAndName(Set<Long> teamIds) {
+        List<GameTeamVo> list = baseMapper.selectVoList(
+            Wrappers.lambdaQuery(GameTeam.class)
+                .in(GameTeam::getEventId, teamIds)
+        );
+        Map<Long, String> map = list.stream()
+            .collect(Collectors.toMap(GameTeamVo::getTeamId, GameTeamVo::getTeamName));
+        return map;
+    }
 }

+ 13 - 0
本周改动.md

@@ -33,3 +33,16 @@
 <li>添加组别详细信息,如人数、组数、道数等</li>
 <li>实现分组自动计算和时间验证功能</li>
 
+
+<h2>8.21</h2>
+
+<li>增加报名表导入校验-仅允许无报名数据的赛事进行导入</li>
+<li>号码对照表生成</li>
+<li>号码布开发</li>
+<li>优化成绩编辑界面,适配不同项目类型</li>
+<li>重构个人和团体项目的排名和积分计算方法</li>
+
+<h2>8.22</h2>
+
+<li>完成号码布开发</li>
+<li>修复部分bug</li>