package com.ruoyi.common.utils.poi; import com.alibaba.fastjson.JSON; import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; import com.google.zxing.MultiFormatWriter; import com.google.zxing.WriterException; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.framework.config.RuoYiConfig; import org.apache.commons.collections4.CollectionUtils; import org.apache.poi.util.IOUtils; import org.apache.poi.util.Units; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.imageio.ImageIO; import javax.servlet.http.HttpServletResponse; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; public class QRCodeUtil { private static final Logger log = LoggerFactory.getLogger(QRCodeUtil.class); /** * 导出数据列表 */ private List list; private String title; private static XWPFDocument doc; //CODE_WIDTH:二维码宽度,单位像素(适当提高分辨率,让文字更清晰) private static final int CODE_WIDTH = 400; //CODE_HEIGHT:二维码高度,单位像素 private static final int CODE_HEIGHT = 400; //FONT_WIDTH:字体大小(稍微放大一点) private static final int FONT_WIDTH = 18; //FRONT_COLOR:二维码前景色,0x000000 表示黑色 private static final int FRONT_COLOR = 0x000000; //BACKGROUND_COLOR:二维码背景色,0xFFFFFF 表示白色 //演示用 16 进制表示,和前端页面 CSS 的取色是一样的,注意前后景颜色应该对比明显,如常见的黑白 private static final int BACKGROUND_COLOR = 0xFFFFFF; /** * 实体对象 */ public Class clazz; public QRCodeUtil(Class clazz) { this.clazz = clazz; } private void init(List list, String title) { if (CollectionUtils.isEmpty(list)) { list = new ArrayList<>(); } this.list = list; this.title = title; doc = new XWPFDocument(); } public void exportQRCode(HttpServletResponse response, List list) { exportQRCode(response, list, StringUtils.EMPTY); } public void exportQRCode(HttpServletResponse response, List list, String title) { response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); response.setCharacterEncoding("utf-8"); this.init(list, title); exportQRCode(response); } public void exportQRCode(HttpServletResponse response) { try { writeDoc(); doc.write(response.getOutputStream()); } catch (Exception e) { log.error("导出Word异常{}", e.getMessage()); } finally { IOUtils.closeQuietly(doc); } } public static void createCodeToFile(String content, File codeImgFileSaveDir, String fileName) { try { if (StringUtils.isBlank(content) || StringUtils.isBlank(fileName)) { return; } content = content.trim(); if (codeImgFileSaveDir == null || codeImgFileSaveDir.isFile()) { //二维码图片存在目录为空,默认放在桌面... codeImgFileSaveDir = new File(RuoYiConfig.getQRCodePath()); } if (!codeImgFileSaveDir.exists()) { //二维码图片存在目录不存在,开始创建... codeImgFileSaveDir.mkdirs(); } //核心代码-生成二维码 BufferedImage bufferedImage = getBufferedImage(content); File codeImgFile = new File(codeImgFileSaveDir, fileName); ImageIO.write(bufferedImage, "png", codeImgFile); log.info("二维码图片生成成功:" + codeImgFile.getPath()); } catch (Exception e) { e.printStackTrace(); } } public static void createCodeToStream(String content, ByteArrayOutputStream baos, String topText, String bottomText) { try { if (StringUtils.isBlank(content)) { return; } content = content.trim(); //核心代码-生成二维码 BufferedImage qrCodeImage = getBufferedImage(content); // 计算总高度:顶部文字区域 + 二维码 + 底部文字区域 int topMargin = StringUtils.isNotEmpty(topText) ? 40 : 0; // 顶部文字区域高度 int bottomMargin = 80; // 底部文字区域高度,适当加大以容纳多行文字 int totalHeight = CODE_HEIGHT + topMargin + bottomMargin; // 创建包含顶部和底部文字的组合图片 BufferedImage combinedImage = new BufferedImage(CODE_WIDTH, totalHeight, BufferedImage.TYPE_INT_RGB); Graphics2D g = combinedImage.createGraphics(); // 设置抗锯齿(图像和文字) g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); // 设置背景颜色为白色 g.setColor(Color.WHITE); g.fillRect(0, 0, CODE_WIDTH, totalHeight); // 绘制顶部文字 if (StringUtils.isNotEmpty(topText)) { g.setColor(Color.BLACK); g.setFont(new Font("微软雅黑", Font.PLAIN, FONT_WIDTH)); FontMetrics fm = g.getFontMetrics(); int textWidth = fm.stringWidth(topText); int textX = (CODE_WIDTH - textWidth) / 2; int textY = topMargin - 10; // 文字在顶部区域垂直居中 g.drawString(topText, textX, textY); } // 绘制二维码(向下偏移顶部文字区域的高度) g.drawImage(qrCodeImage, 0, topMargin, null); // 绘制底部文字(支持自动换行) if (StringUtils.isNotEmpty(bottomText)) { g.setColor(Color.BLACK); g.setFont(new Font("微软雅黑", Font.PLAIN, FONT_WIDTH)); FontMetrics fm = g.getFontMetrics(); int maxWidth = CODE_WIDTH - 20; // 留左右 10 像素边距 List lines = new ArrayList<>(); StringBuilder currentLine = new StringBuilder(); for (int i = 0; i < bottomText.length(); i++) { char c = bottomText.charAt(i); currentLine.append(c); if (fm.stringWidth(currentLine.toString()) > maxWidth) { // 超出宽度,回退一个字符作为下一行开始 currentLine.deleteCharAt(currentLine.length() - 1); lines.add(currentLine.toString()); currentLine = new StringBuilder().append(c); } } if (currentLine.length() > 0) { lines.add(currentLine.toString()); } int lineHeight = fm.getHeight(); int startY = topMargin + CODE_HEIGHT + (bottomMargin - lineHeight * lines.size()) / 2 + fm.getAscent(); for (int i = 0; i < lines.size(); i++) { String line = lines.get(i); int textWidth = fm.stringWidth(line); int textX = (CODE_WIDTH - textWidth) / 2; int textY = startY + i * lineHeight; g.drawString(line, textX, textY); } } g.dispose(); ImageIO.write(combinedImage, "png", baos); log.info("二维码图片生成到输出流成功..."); } catch (Exception e) { e.printStackTrace(); } } private static Font loadFont(String fontFilePath, int fontSize) throws IOException, FontFormatException { return Font.createFont(Font.TRUETYPE_FONT, new File(fontFilePath)).deriveFont(Font.PLAIN, fontSize); } //核心代码-生成二维码 private static BufferedImage getBufferedImage(String content) throws WriterException { //com.google.zxing.EncodeHintType:编码提示类型,枚举类型 Map hints = new HashMap<>(); //EncodeHintType.CHARACTER_SET:设置字符编码类型 hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); //EncodeHintType.ERROR_CORRECTION:设置误差校正 //ErrorCorrectionLevel:误差校正等级,L = ~7% correction、M = ~15% correction、Q = ~25% correction、H = ~30% correction //不设置时,默认为 L 等级,等级不一样,生成的图案不同,但扫描的结果是一样的 hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); //EncodeHintType.MARGIN:设置二维码边距,单位像素,值越小,二维码距离四周越近 hints.put(EncodeHintType.MARGIN, 1); MultiFormatWriter multiFormatWriter = new MultiFormatWriter(); BitMatrix bitMatrix = multiFormatWriter.encode(content, BarcodeFormat.QR_CODE, CODE_WIDTH, CODE_HEIGHT, hints); BufferedImage bufferedImage = new BufferedImage(CODE_WIDTH, CODE_HEIGHT, BufferedImage.TYPE_INT_BGR); for (int x = 0; x < CODE_WIDTH; x++) { for (int y = 0; y < CODE_HEIGHT; y++) { bufferedImage.setRGB(x, y, bitMatrix.get(x, y) ? FRONT_COLOR : BACKGROUND_COLOR); } } return bufferedImage; } private void writeDoc() { XWPFParagraph paragraph = doc.createParagraph(); list.forEach(item -> { try { log.info("==================================================" + JSON.toJSONString(item)); //务必重写类的toString方法 ByteArrayOutputStream baos = new ByteArrayOutputStream(); createCodeToStream(JSON.toJSONString(item), baos, title, item.toString()); InputStream is = new ByteArrayInputStream(baos.toByteArray()); paragraph.createRun().addPicture(is, XWPFDocument.PICTURE_TYPE_JPEG, "image.jpg", Units.toEMU(200), Units.toEMU(200)); } catch (Exception e) { e.printStackTrace(); } }); } /** * 生成二维码图片并打包成ZIP文件下载 * * @param response HttpServletResponse * @param list 数据列表 * @param title 标题(可选) */ public void exportQRCodeAsZip(HttpServletResponse response, List list, String title) { response.setContentType("application/zip"); response.setCharacterEncoding("utf-8"); response.setHeader("Content-Disposition", "attachment;"); this.init(list, title); try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) { // 设置压缩级别 zos.setLevel(ZipOutputStream.STORED); int index = 1; for (T item : list) { try { // 生成文件名 String fileName = generateFileName(item, index) + ".png"; index++; // 创建ZIP条目 ZipEntry zipEntry = new ZipEntry(fileName); zos.putNextEntry(zipEntry); // 生成二维码图片并写入ZIP ByteArrayOutputStream baos = new ByteArrayOutputStream(); String content = JSON.toJSONString(item); String bottomText = item.toString(); createCodeToStream(content, baos, title, bottomText); // 将字节数组写入ZIP zos.write(baos.toByteArray()); zos.closeEntry(); baos.close(); log.info("成功生成二维码图片: {}", fileName); } catch (Exception e) { log.error("生成二维码图片失败: {}", e.getMessage()); } } zos.finish(); log.info("ZIP文件生成成功,包含 {} 个二维码图片", list.size()); } catch (Exception e) { log.error("生成ZIP文件异常: {}", e.getMessage()); try { response.reset(); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().println("生成ZIP文件失败: " + e.getMessage()); } catch (IOException ex) { log.error("发送错误信息失败: {}", ex.getMessage()); } } } /** * 生成二维码图片并打包成ZIP文件下载(简化版) * * @param response HttpServletResponse * @param list 数据列表 */ public void exportQRCodeAsZip(HttpServletResponse response, List list) { exportQRCodeAsZip(response, list, StringUtils.EMPTY); } /** * 生成文件名 * * @param item 数据项 * @param index 索引 * @return 文件名 */ private String generateFileName(T item, int index) { // 如果对象有特定的标识字段,可以重写此方法 // 例如:如果对象有name字段,可以返回 item.getName() // 默认使用索引+对象字符串表示 String baseName = StringUtils.substring(item.toString(), 0, 20); // 限制长度 baseName = baseName.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5]", "_"); // 替换特殊字符 return String.format("%03d_%s", index, baseName); } }