ruoyi项目导出PDF

需求背景

客户要求项目报表支持导出PDF文件,并线下领导签字再上传已签名的PDF扫描件。

解决方案

1.使用flying-saucer-pdf+thymeleaf

这种方式需要编写html,每个报表都需要重新编写html对表头及数据格式等。

2.使用itextpdf开源组件

开发支持注解方式导出PDF(类似ruoyi导出excel的方式),只需要bean对象编写需要导出字段的注解。

这里选择使用itextpdf。

代码组成

1.PDF字段注解PdfField.java

java 复制代码
package com.asiadb.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * PDF字段注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PdfField {

    /**
     * 字段名称(标题)
     */
    String name() default "";

    /**
     * 字段顺序
     */
    int sort() default 0;

    /**
     * 日期格式
     */
    String dateFormat() default "";

    /**
     * 字典类型
     */
    String dictType() default "";

    /**
     * 是否忽略该字段
     */
    boolean ignore() default false;

    /**
     * 字段宽度(百分比)
     */
    int width() default 10;

    /**
     * 对齐方式:left, center, right
     */
    String align() default "left";

    /**
     * 字体颜色(支持CSS颜色值,如:#FF0000, red, rgb(255,0,0))
     */
    String color() default "";

    /**
     * 条件颜色配置(JSON格式)
     * 示例:[{"value":"1","color":"#FF0000"},{"value":"2","color":"#00FF00"}]
     */
    String conditionalColor() default "";

    /**
     * 背景颜色
     */
    String backgroundColor() default "";
}

2.PDF表格注解PdfSheet.java

java 复制代码
package com.asiadb.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * PDF表格注解
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PdfSheet {

    /**
     * 表格标题
     */
    String title() default "";

    /**
     * 表头高度
     */
    int headerHeight() default 20;

    /**
     * 行高
     */
    int rowHeight() default 15;

    /**
     * 是否显示序号列
     */
    boolean showIndex() default true;

    /**
     * 序号列名称
     */
    String indexName() default "序号";
    /**
     * 页面方向:portrait(纵向), landscape(横向)
     */
    String orientation() default "portrait";

    /**
     * 页面尺寸:A4, A3, LETTER等
     */
    String pageSize() default "A4";

    /**
     * 页面边距(毫米)
     */
    float marginLeft() default 20;
    float marginRight() default 20;
    float marginTop() default 20;
    float marginBottom() default 20;
}

3.PDF导出工具类

PdfUtils.java

java 复制代码
package com.asiadb.common.utils.pdf;

import com.asiadb.common.utils.StringUtils;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.io.font.PdfEncodings;
import com.itextpdf.io.font.FontProgram;
import com.itextpdf.io.font.FontProgramFactory;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Table;
import com.itextpdf.layout.element.Cell;
import com.itextpdf.layout.properties.UnitValue;

import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.List;
import java.util.UUID;

/**
 * PDF导出工具类
 */
public class PdfUtils {

    // 字体路径配置
    private static final String[] FONT_PATHS = {
            "fonts/simsun.ttf",                    // 项目resources下的字体
            "fonts/simhei.ttf",                    // 黑体
            "fonts/msyh.ttf",                      // 微软雅黑
            "fonts/msyhbd.ttf",                    // 微软雅黑粗体
            "fonts/simsunb.ttf"                    // 宋体粗体
    };

    // 系统字体路径
    private static final String[] SYSTEM_FONT_PATHS = {
            "C:/Windows/Fonts/simsun.ttc",         // Windows宋体
            "C:/Windows/Fonts/simhei.ttf",         // Windows黑体
            "C:/Windows/Fonts/msyh.ttc",           // Windows微软雅黑
            "C:/Windows/Fonts/msyhbd.ttc",         // Windows微软雅黑粗体
            "/System/Library/Fonts/PingFang.ttc",  // Mac苹方字体
            "/System/Library/Fonts/STHeiti Light.ttc", // Mac华文黑体
            "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc" // Linux文泉驿微米黑
    };

    private static PdfFont chineseFont = null;

    /**
     * 初始化中文字体 - 修复字体加载逻辑
     */
    private static synchronized PdfFont getChineseFont() throws IOException {
        if (chineseFont == null) {
            // 1. 首先尝试从资源文件加载
            for (String fontPath : FONT_PATHS) {
                try {
                    System.out.println("尝试加载资源字体: " + fontPath);
                    try (java.io.InputStream is = PdfUtils.class.getClassLoader().getResourceAsStream(fontPath)) {
                        if (is != null) {
                            byte[] fontBytes = is.readAllBytes();
                            // 使用正确的方法创建字体
                            chineseFont = PdfFontFactory.createFont(fontBytes, PdfEncodings.IDENTITY_H);
                            System.out.println("成功加载资源字体: " + fontPath);
                            return chineseFont;
                        }
                    }
                } catch (Exception e) {
                    System.out.println("无法加载资源字体: " + fontPath + ", 错误: " + e.getMessage());
                }
            }

            // 2. 尝试从系统字体加载
            for (String fontPath : SYSTEM_FONT_PATHS) {
                try {
                    java.io.File fontFile = new java.io.File(fontPath);
                    if (fontFile.exists()) {
                        // 使用正确的方法创建字体
                        chineseFont = PdfFontFactory.createFont(fontPath, PdfEncodings.IDENTITY_H);
                        System.out.println("成功加载系统字体: " + fontPath);
                        return chineseFont;
                    }
                } catch (Exception e) {
                    // 继续尝试下一个字体
                    System.out.println("无法加载系统字体: " + fontPath + ", 错误: " + e.getMessage());
                }
            }

            // 3. 尝试使用iText Asian字体(如果依赖已添加)
            try {
                // 使用iText Asian字体包中的字体
                chineseFont = PdfFontFactory.createFont("STSong-Light", "UniGB-UCS2-H");
                System.out.println("成功加载iText Asian字体: STSong-Light");
                return chineseFont;
            } catch (Exception e) {
                System.out.println("无法加载iText Asian字体: " + e.getMessage());
            }

            // 4. 最后尝试使用默认字体(可能不支持中文)
            try {
                chineseFont = PdfFontFactory.createFont("Helvetica", PdfEncodings.IDENTITY_H);
                System.out.println("使用Helvetica字体(可能不支持中文)");
            } catch (Exception e) {
                System.err.println("警告:无法加载任何中文字体,将使用默认字体");
                chineseFont = PdfFontFactory.createFont();
            }
        }
        return chineseFont;
    }

    /**
     * 编码文件名
     */
    public static String encodingFilename(String filename) {
        filename = UUID.randomUUID().toString() + "_" + filename;
        return filename;
    }

    /**
     * 导出PDF到响应流 - 修复的主要方法
     */
    public static void exportPdf(HttpServletResponse response, String fileName, String content) throws IOException {
        response.setContentType("application/pdf");
        response.setCharacterEncoding("utf-8");
        fileName = encodingFilename(fileName);
        fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".pdf");
        response.setHeader("download-filename", fileName + ".pdf");

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        // 设置HTML转换器属性
        com.itextpdf.html2pdf.ConverterProperties properties = new com.itextpdf.html2pdf.ConverterProperties();

        // 设置字符编码
        properties.setCharset("UTF-8");

        // 创建字体提供者
        com.itextpdf.html2pdf.resolver.font.DefaultFontProvider fontProvider =
                new com.itextpdf.html2pdf.resolver.font.DefaultFontProvider();

        // 获取中文字体
        PdfFont font = getChineseFont();
        if (font != null) {
            try {
                fontProvider.addFont(font.getFontProgram(), PdfEncodings.IDENTITY_H);
                System.out.println("已添加字体到字体提供者: " + font.getFontProgram().getFontNames().getFontName());
            } catch (Exception e) {
                System.err.println("添加字体到字体提供者失败: " + e.getMessage());
            }
        }

        // 尝试添加其他可能的字体
        for (String fontPath : SYSTEM_FONT_PATHS) {
            try {
                java.io.File fontFile = new java.io.File(fontPath);
                if (fontFile.exists()) {
                    fontProvider.addFont(fontFile.getAbsolutePath());
                    System.out.println("字体提供者添加系统字体: " + fontPath);
                }
            } catch (Exception e) {
                // 忽略错误,继续尝试下一个
            }
        }

        properties.setFontProvider(fontProvider);

        // 构建完整的HTML,确保CSS正确
        String htmlWithFont = "<!DOCTYPE html>" +
                "<html lang='zh-CN'>" +
                "<head>" +
                "    <meta charset='UTF-8'>" +
                "    <meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>" +
                "    <style>" +
                "        @font-face {" +
                "            font-family: 'ChineseFont';" +
                "            src: local('SimSun'), local('Microsoft YaHei');" +
                "        }" +
                "        body {" +
                "            font-family: 'ChineseFont', 'SimSun', 'Microsoft YaHei', 'STSong', sans-serif;" +
                "            font-size: 12pt;" +
                "            line-height: 1.5;" +
                "        }" +
                "        * {" +
                "            font-family: inherit;" +
                "        }" +
                "        table {" +
                "            border-collapse: collapse;" +
                "            width: 100%;" +
                "        }" +
                "        th, td {" +
                "            border: 1px solid #ddd;" +
                "            padding: 8px;" +
                "            text-align: left;" +
                "        }" +
                "        th {" +
                "            background-color: #f2f2f2;" +
                "            font-weight: bold;" +
                "        }" +
                "    </style>" +
                "</head>" +
                "<body>" + content + "</body>" +
                "</html>";

        try {
            HtmlConverter.convertToPdf(htmlWithFont, baos, properties);
            System.out.println("PDF生成成功");
        } catch (Exception e) {
            System.err.println("PDF生成失败: " + e.getMessage());
            e.printStackTrace();
            // 尝试使用简化版本
            exportPdfSimple(response, fileName.replace(".pdf", ""), content);
            return;
        }

        response.getOutputStream().write(baos.toByteArray());
        response.getOutputStream().flush();
    }

    /**
     * 生成表格PDF - 修复表格中文显示
     */
    public static void exportTablePdf(HttpServletResponse response, String fileName,
                                      List<String> headers, List<List<String>> data) throws IOException {
        response.setContentType("application/pdf");
        response.setCharacterEncoding("utf-8");
        fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".pdf");

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PdfWriter writer = new PdfWriter(baos);
        PdfDocument pdf = new PdfDocument(writer);
        Document document = new Document(pdf);

        // 设置中文字体 - 使用IDENTITY_H编码
        PdfFont font = getChineseFont();
        document.setFont(font);

        // 添加标题
        document.add(new Paragraph(fileName)
                .setBold()
                .setFontSize(16)
                .setFont(font));

        // 创建表格
        Table table = new Table(UnitValue.createPercentArray(headers.size()));
        table.setWidth(UnitValue.createPercentValue(100));

        // 添加表头
        for (String header : headers) {
            Cell cell = new Cell()
                    .add(new Paragraph(header)
                            .setBold()
                            .setFont(font));
            table.addHeaderCell(cell);
        }

        // 添加数据
        for (List<String> row : data) {
            for (String cellData : row) {
                Cell cell = new Cell()
                        .add(new Paragraph(StringUtils.isNotNull(cellData) ? cellData : "")
                                .setFont(font));
                table.addCell(cell);
            }
        }

        document.add(table);
        document.close();

        response.getOutputStream().write(baos.toByteArray());
        response.getOutputStream().flush();
    }

    /**
     * 生成HTML内容PDF
     */
    public static byte[] generatePdfFromHtml(String html) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        // 设置转换属性
        com.itextpdf.html2pdf.ConverterProperties properties = new com.itextpdf.html2pdf.ConverterProperties();
        properties.setCharset("UTF-8");

        // 创建字体提供者
        com.itextpdf.html2pdf.resolver.font.DefaultFontProvider fontProvider =
                new com.itextpdf.html2pdf.resolver.font.DefaultFontProvider();

        // 添加中文字体
        PdfFont font = getChineseFont();
        if (font != null) {
            fontProvider.addFont(font.getFontProgram(), PdfEncodings.IDENTITY_H);
        }

        properties.setFontProvider(fontProvider);

        // 包装HTML确保字符集和字体
        String htmlWithFont = "<!DOCTYPE html>" +
                "<html lang='zh-CN'>" +
                "<head>" +
                "    <meta charset='UTF-8'>" +
                "    <meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>" +
                "    <style>" +
                "        body { font-family: 'SimSun', 'Microsoft YaHei', 'STSong', sans-serif; }" +
                "        * { font-family: inherit; }" +
                "    </style>" +
                "</head>" +
                "<body>" + html + "</body>" +
                "</html>";

        HtmlConverter.convertToPdf(htmlWithFont, baos, properties);
        return baos.toByteArray();
    }

    /**
     * 设置自定义字体
     */
    public static void setChineseFont(PdfFont font) {
        chineseFont = font;
    }

    /**
     * 从资源文件加载字体 - 修复的方法
     */
    public static PdfFont loadFontFromResource(String resourcePath) throws IOException {
        try (java.io.InputStream is = PdfUtils.class.getClassLoader().getResourceAsStream(resourcePath)) {
            if (is != null) {
                byte[] fontBytes = is.readAllBytes();
                return PdfFontFactory.createFont(fontBytes, PdfEncodings.IDENTITY_H);
            }
        }
        return null;
    }

    /**
     * 简化版本 - 直接使用系统字体
     */
    public static void exportPdfSimple(HttpServletResponse response, String fileName, String content) throws IOException {
        response.setContentType("application/pdf");
        response.setCharacterEncoding("utf-8");
        fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".pdf");

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        // 创建字体提供者
        com.itextpdf.html2pdf.resolver.font.DefaultFontProvider fontProvider =
                new com.itextpdf.html2pdf.resolver.font.DefaultFontProvider();

        // 尝试直接添加字体文件
        for (String fontFile : SYSTEM_FONT_PATHS) {
            try {
                java.io.File file = new java.io.File(fontFile);
                if (file.exists()) {
                    fontProvider.addFont(fontFile);
                    System.out.println("成功添加字体文件: " + fontFile);
                }
            } catch (Exception e) {
                // 忽略错误
            }
        }

        com.itextpdf.html2pdf.ConverterProperties properties = new com.itextpdf.html2pdf.ConverterProperties();
        properties.setFontProvider(fontProvider);
        properties.setCharset("UTF-8");

        // 确保HTML有正确的字符集声明
        String html = "<!DOCTYPE html>" +
                "<html lang='zh-CN'>" +
                "<head>" +
                "    <meta charset='UTF-8'>" +
                "    <meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>" +
                "    <style>" +
                "        body { font-family: 'SimSun', 'Microsoft YaHei', sans-serif; }" +
                "    </style>" +
                "</head>" +
                "<body>" + content + "</body>" +
                "</html>";

        HtmlConverter.convertToPdf(html, baos, properties);

        response.getOutputStream().write(baos.toByteArray());
        response.getOutputStream().flush();
    }

    /**
     * 测试方法:检查字体是否可用
     */
    public static boolean testChineseFont() {
        try {
            PdfFont font = getChineseFont();
            if (font != null) {
                System.out.println("字体名称: " + font.getFontProgram().getFontNames().getFontName());
                System.out.println("支持中文: " + font.isEmbedded());
                return true;
            }
        } catch (Exception e) {
            System.err.println("测试字体失败: " + e.getMessage());
        }
        return false;
    }

    /**
     * 清理字体缓存
     */
    public static void clearFontCache() {
        chineseFont = null;
        System.out.println("已清理字体缓存");
    }

    /**
     * 替代方案:使用更简单的方法创建字体
     */
    public static PdfFont createChineseFontSimple() throws IOException {
        // 方法1:尝试使用系统字体
        try {
            return PdfFontFactory.createFont("C:/Windows/Fonts/simsun.ttc", PdfEncodings.IDENTITY_H);
        } catch (Exception e1) {
            // 方法2:尝试从资源加载
            try (java.io.InputStream is = PdfUtils.class.getClassLoader().getResourceAsStream("fonts/simsun.ttf")) {
                if (is != null) {
                    byte[] fontBytes = is.readAllBytes();
                    return PdfFontFactory.createFont(fontBytes, PdfEncodings.IDENTITY_H);
                }
            } catch (Exception e2) {
                // 方法3:使用默认字体
                return PdfFontFactory.createFont();
            }
        }
        return PdfFontFactory.createFont();
    }

    /**
     * 使用iText Asian字体的方法(需要添加itext-asian依赖)
     */
    public static PdfFont createAsianFont() throws IOException {
        try {
            // 如果添加了itext-asian依赖,可以使用这些字体
            return PdfFontFactory.createFont("STSong-Light", "UniGB-UCS2-H");
        } catch (Exception e) {
            System.out.println("无法加载Asian字体,尝试其他方法: " + e.getMessage());
            return getChineseFont();
        }
    }

    /**
     * 修复字体问题的终极方案
     */
    public static void exportPdfFixed(HttpServletResponse response, String fileName, String content) throws IOException {
        response.setContentType("application/pdf");
        response.setCharacterEncoding("utf-8");
        fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".pdf");

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        // 方法1:使用HTML转换器,确保字体正确
        com.itextpdf.html2pdf.ConverterProperties properties = new com.itextpdf.html2pdf.ConverterProperties();
        properties.setCharset("UTF-8");

        // 创建字体提供者
        com.itextpdf.html2pdf.resolver.font.DefaultFontProvider fontProvider =
                new com.itextpdf.html2pdf.resolver.font.DefaultFontProvider();

        // 尝试添加多种字体
        boolean fontAdded = false;

        // 1. 尝试添加系统字体
        String[] fontFiles = {
                "C:/Windows/Fonts/simsun.ttc",
                "C:/Windows/Fonts/simhei.ttf",
                "C:/Windows/Fonts/msyh.ttc",
                "C:/Windows/Fonts/msyhbd.ttc",
                "/System/Library/Fonts/PingFang.ttc",
                "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"
        };

        for (String fontFile : fontFiles) {
            try {
                java.io.File file = new java.io.File(fontFile);
                if (file.exists()) {
                    fontProvider.addFont(fontFile);
                    System.out.println("成功添加字体: " + fontFile);
                    fontAdded = true;
                    break;
                }
            } catch (Exception e) {
                // 忽略错误,继续尝试下一个
            }
        }

        // 2. 如果系统字体失败,尝试从资源加载
        if (!fontAdded) {
            try {
                java.io.InputStream is = PdfUtils.class.getClassLoader().getResourceAsStream("fonts/simsun.ttf");
                if (is != null) {
                    byte[] fontBytes = is.readAllBytes();
                    fontProvider.addFont(fontBytes, PdfEncodings.IDENTITY_H);
                    System.out.println("成功从资源加载字体");
                    fontAdded = true;
                }
            } catch (Exception e) {
                System.out.println("从资源加载字体失败: " + e.getMessage());
            }
        }

        properties.setFontProvider(fontProvider);

        // 构建完整的HTML,包含CSS字体设置
        String html = "<!DOCTYPE html>" +
                "<html>" +
                "<head>" +
                "    <meta charset=\"UTF-8\">" +
                "    <style>" +
                "        @font-face {" +
                "            font-family: 'MyChineseFont';" +
                "            src: local('SimSun'), local('Microsoft YaHei');" +
                "        }" +
                "        body {" +
                "            font-family: 'MyChineseFont', 'SimSun', 'Microsoft YaHei', sans-serif;" +
                "            font-size: 14px;" +
                "            line-height: 1.6;" +
                "        }" +
                "        h1, h2, h3, h4, h5, h6 {" +
                "            font-family: inherit;" +
                "        }" +
                "        table {" +
                "            border-collapse: collapse;" +
                "            width: 100%;" +
                "        }" +
                "        th, td {" +
                "            border: 1px solid #ddd;" +
                "            padding: 8px;" +
                "        }" +
                "        th {" +
                "            background-color: #f2f2f2;" +
                "        }" +
                "    </style>" +
                "</head>" +
                "<body>" + content + "</body>" +
                "</html>";

        try {
            HtmlConverter.convertToPdf(html, baos, properties);
            System.out.println("PDF生成成功");
        } catch (Exception e) {
            System.err.println("HTML转换失败: " + e.getMessage());
            // 回退方案:使用直接PDF创建
            baos = createPdfDirectly(content);
        }

        response.getOutputStream().write(baos.toByteArray());
        response.getOutputStream().flush();
    }

    /**
     * 直接创建PDF的回退方案
     */
    private static ByteArrayOutputStream createPdfDirectly(String content) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PdfWriter writer = new PdfWriter(baos);
        PdfDocument pdf = new PdfDocument(writer);
        Document document = new Document(pdf);

        // 尝试获取中文字体
        PdfFont font = null;
        try {
            font = getChineseFont();
        } catch (Exception e) {
            // 如果获取失败,使用默认字体
            font = PdfFontFactory.createFont();
        }

        document.setFont(font);

        // 将内容按段落分割
        String[] paragraphs = content.split("\n");
        for (String paragraph : paragraphs) {
            if (paragraph.trim().length() > 0) {
                document.add(new Paragraph(paragraph).setFont(font));
            }
        }

        document.close();
        return baos;
    }

    /**
     * 推荐的解决方案:使用iText Asian字体包
     * 需要添加依赖: com.itextpdf:itext-asian:7.2.5
     */
    public static void exportPdfWithAsianFont(HttpServletResponse response, String fileName, String content) throws IOException {
        response.setContentType("application/pdf");
        response.setCharacterEncoding("utf-8");
        fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".pdf");

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        com.itextpdf.html2pdf.ConverterProperties properties = new com.itextpdf.html2pdf.ConverterProperties();
        properties.setCharset("UTF-8");

        // 创建字体提供者
        com.itextpdf.html2pdf.resolver.font.DefaultFontProvider fontProvider =
                new com.itextpdf.html2pdf.resolver.font.DefaultFontProvider();

        try {
            // 方法1:尝试使用iText Asian字体
            fontProvider.addFont("STSong-Light");
            System.out.println("使用STSong-Light字体");
        } catch (Exception e1) {
            try {
                // 方法2:尝试使用系统字体
                String fontPath = "C:/Windows/Fonts/simsun.ttc";
                java.io.File fontFile = new java.io.File(fontPath);
                if (fontFile.exists()) {
                    fontProvider.addFont(fontPath);
                    System.out.println("使用系统字体: " + fontPath);
                }
            } catch (Exception e2) {
                // 方法3:从资源加载
                try (java.io.InputStream is = PdfUtils.class.getClassLoader().getResourceAsStream("fonts/simsun.ttf")) {
                    if (is != null) {
                        byte[] fontBytes = is.readAllBytes();
                        fontProvider.addFont(fontBytes, PdfEncodings.IDENTITY_H);
                        System.out.println("从资源加载字体");
                    }
                } catch (Exception e3) {
                    System.err.println("所有字体加载方法都失败了");
                }
            }
        }

        properties.setFontProvider(fontProvider);

        // 构建HTML
        String html = "<!DOCTYPE html>" +
                "<html>" +
                "<head>" +
                "    <meta charset=\"UTF-8\">" +
                "    <style>" +
                "        body {" +
                "            font-family: 'STSong-Light', 'SimSun', 'Microsoft YaHei', sans-serif;" +
                "            font-size: 12pt;" +
                "        }" +
                "    </style>" +
                "</head>" +
                "<body>" + content + "</body>" +
                "</html>";

        HtmlConverter.convertToPdf(html, baos, properties);

        response.getOutputStream().write(baos.toByteArray());
        response.getOutputStream().flush();
    }

    /**
     * 最简单的解决方案:确保有字体文件在resources/fonts目录下
     */
    public static void exportPdfSimpleFixed(HttpServletResponse response, String fileName, String content) throws IOException {
        response.setContentType("application/pdf");
        response.setCharacterEncoding("utf-8");
        fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".pdf");

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        com.itextpdf.html2pdf.ConverterProperties properties = new com.itextpdf.html2pdf.ConverterProperties();
        properties.setCharset("UTF-8");

        // 创建字体提供者
        com.itextpdf.html2pdf.resolver.font.DefaultFontProvider fontProvider =
                new com.itextpdf.html2pdf.resolver.font.DefaultFontProvider();

        // 确保有字体文件在resources/fonts目录下
        // 1. 下载一个中文字体文件(如simsun.ttf)
        // 2. 放在项目的src/main/resources/fonts/目录下

        try (java.io.InputStream is = PdfUtils.class.getClassLoader().getResourceAsStream("fonts/simsun.ttf")) {
            if (is != null) {
                byte[] fontBytes = is.readAllBytes();
                fontProvider.addFont(fontBytes, PdfEncodings.IDENTITY_H);
                System.out.println("成功加载simsun.ttf字体");
            } else {
                System.err.println("找不到字体文件: fonts/simsun.ttf");
                // 尝试其他字体
                try (java.io.InputStream is2 = PdfUtils.class.getClassLoader().getResourceAsStream("fonts/msyh.ttf")) {
                    if (is2 != null) {
                        byte[] fontBytes = is2.readAllBytes();
                        fontProvider.addFont(fontBytes, PdfEncodings.IDENTITY_H);
                        System.out.println("成功加载msyh.ttf字体");
                    }
                }
            }
        }

        properties.setFontProvider(fontProvider);

        String html = "<!DOCTYPE html>" +
                "<html>" +
                "<head>" +
                "    <meta charset=\"UTF-8\">" +
                "    <style>" +
                "        body { font-family: 'SimSun', sans-serif; }" +
                "    </style>" +
                "</head>" +
                "<body>" + content + "</body>" +
                "</html>";

        HtmlConverter.convertToPdf(html, baos, properties);

        response.getOutputStream().write(baos.toByteArray());
        response.getOutputStream().flush();
    }
}

PdfExportUtil.java

java 复制代码
package com.asiadb.common.utils.pdf;

import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.asiadb.common.annotation.PdfField;
import com.asiadb.common.annotation.PdfSheet;
import com.asiadb.common.utils.DateUtils;
import com.asiadb.common.utils.DictUtils;
import com.asiadb.common.utils.MessageUtils;
import com.asiadb.common.utils.StringUtils;
import com.asiadb.common.utils.reflect.ReflectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * PDF导出工具类(支持注解)
 */
public class PdfExportUtil {

    private static final Logger log = LoggerFactory.getLogger(PdfExportUtil.class);

    // 缓存类字段信息
    private static final Map<Class<?>, List<FieldInfo>> FIELD_CACHE = new ConcurrentHashMap<>();

    // 缓存条件颜色配置
    private static final Map<String, List<ColorCondition>> COLOR_CONDITION_CACHE = new ConcurrentHashMap<>();

    private static final String DATEFORMAT = "00000000000000000";
    /**
     * 颜色条件配置
     */
    private static class ColorCondition {
        private String value;
        private String color;
        private String backgroundColor;

        public ColorCondition(String value, String color, String backgroundColor) {
            this.value = value;
            this.color = color;
            this.backgroundColor = backgroundColor;
        }

        public String getValue() {
            return value;
        }

        public String getColor() {
            return color;
        }

        public String getBackgroundColor() {
            return backgroundColor;
        }
    }

    /**
     * 字段信息(增加颜色相关属性)
     */
    public static class FieldInfo {
        private Field field;
        private PdfField annotation;
        private int sort;
        private String name;
        private List<ColorCondition> colorConditions; // 条件颜色配置

        public FieldInfo(Field field, PdfField annotation) {
            this.field = field;
            this.annotation = annotation;
            this.sort = annotation.sort();
            try {
                if(StringUtils.isNotEmpty(annotation.name())){
                    this.name = MessageUtils.message(annotation.name());
                }else{
                    this.name = field.getName();
                }
            }catch (Exception e){
                log.error("国际化异常:",e);
            }

            // 解析条件颜色配置
            this.colorConditions = parseColorConditions(annotation);
        }

        public Field getField() {
            return field;
        }

        public PdfField getAnnotation() {
            return annotation;
        }

        public int getSort() {
            return sort;
        }

        public String getName() {
            return name;
        }

        public List<ColorCondition> getColorConditions() {
            return colorConditions;
        }

        /**
         * 解析条件颜色配置
         */
        private List<ColorCondition> parseColorConditions(PdfField annotation) {
            String conditionalColor = annotation.conditionalColor();
            if (StringUtils.isEmpty(conditionalColor)) {
                return Collections.emptyList();
            }

            try {
                String cacheKey = field.getDeclaringClass().getName() + "." + field.getName();
                return COLOR_CONDITION_CACHE.computeIfAbsent(cacheKey, key -> {
                    List<ColorCondition> conditions = new ArrayList<>();
                    JSONArray array = JSONUtil.parseArray(conditionalColor);
                    for (int i = 0; i < array.size(); i++) {
                        JSONObject obj = array.getJSONObject(i);
                        String value = obj.getStr("value");
                        String color = obj.getStr("color");
                        String backgroundColor = obj.getStr("backgroundColor");
                        conditions.add(new ColorCondition(value, color, backgroundColor));
                    }
                    return conditions;
                });
            } catch (Exception e) {
                log.error("解析条件颜色配置失败: {}", conditionalColor, e);
                return Collections.emptyList();
            }
        }

        /**
         * 根据字段值获取颜色样式
         */
        public String getColorStyle(Object fieldValue) {
            if (fieldValue == null) {
                return "";
            }

            StringBuilder style = new StringBuilder();

            // 1. 首先应用条件颜色
            String strValue = fieldValue.toString();
            for (ColorCondition condition : colorConditions) {
                if (strValue.equals(condition.getValue())) {
                    if (StringUtils.isNotEmpty(condition.getColor())) {
                        style.append("color:").append(condition.getColor()).append(";");
                    }
                    if (StringUtils.isNotEmpty(condition.getBackgroundColor())) {
                        style.append("background-color:").append(condition.getBackgroundColor()).append(";");
                    }
                    return style.toString();
                }
            }

            // 2. 应用固定颜色(如果没有条件颜色匹配)
            if (StringUtils.isNotEmpty(annotation.color())) {
                style.append("color:").append(annotation.color()).append(";");
            }
            if (StringUtils.isNotEmpty(annotation.backgroundColor())) {
                style.append("background-color:").append(annotation.backgroundColor()).append(";");
            }

            return style.toString();
        }
    }

    /**
     * 获取类的字段信息(实时获取,不使用缓存)
     */
    public static List<FieldInfo> getFieldInfos(Class<?> clazz) {
        List<FieldInfo> fieldInfos = new ArrayList<>();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            PdfField annotation = field.getAnnotation(PdfField.class);
            if (annotation != null && !annotation.ignore()) {
                fieldInfos.add(new FieldInfo(field, annotation));
            }
        }
        // 按sort排序
        fieldInfos.sort(Comparator.comparingInt(FieldInfo::getSort));
        return fieldInfos;
    }

    /**
     * 获取表头信息
     */
    public static List<String> getHeaders(Class<?> clazz) {
        PdfSheet sheetAnnotation = clazz.getAnnotation(PdfSheet.class);
        List<String> headers = new ArrayList<>();

        if (sheetAnnotation != null && sheetAnnotation.showIndex()) {
            headers.add(sheetAnnotation.indexName());
        }

        List<FieldInfo> fieldInfos = getFieldInfos(clazz);
        for (FieldInfo fieldInfo : fieldInfos) {
            headers.add(fieldInfo.getName());
        }

        return headers;
    }

    /**
     * 获取数据行
     */
    public static List<List<CellData>> getDataRows(List<?> dataList) {
        if (dataList == null || dataList.isEmpty()) {
            return new ArrayList<>();
        }

        Class<?> clazz = dataList.get(0).getClass();
        PdfSheet sheetAnnotation = clazz.getAnnotation(PdfSheet.class);
        List<FieldInfo> fieldInfos = getFieldInfos(clazz);
        List<List<CellData>> rows = new ArrayList<>();

        for (int i = 0; i < dataList.size(); i++) {
            Object obj = dataList.get(i);
            List<CellData> row = new ArrayList<>();

            // 添加序号
            if (sheetAnnotation != null && sheetAnnotation.showIndex()) {
                CellData indexCell = new CellData(String.valueOf(i + 1), "",fieldInfos.get(0).annotation.align());
                row.add(indexCell);
            }

            // 添加字段数据
            for (FieldInfo fieldInfo : fieldInfos) {
                try {
                    Object value = ReflectUtils.invokeGetter(obj, fieldInfo.getField().getName());
                    String strValue = formatValue(value, fieldInfo.getAnnotation());
                    String style = fieldInfo.getColorStyle(value);
                    row.add(new CellData(strValue, style,fieldInfo.annotation.align()));
                } catch (Exception e) {
                    log.error("获取字段值失败: {}", fieldInfo.getField().getName(), e);
                    row.add(new CellData("", "","left"));
                }
            }

            rows.add(row);
        }

        return rows;
    }

    /**
     * 格式化字段值
     */
    private static String formatValue(Object value, PdfField annotation) {
        if (value == null) {
            return "";
        }

        // 日期格式化
        if (StringUtils.isNotEmpty(annotation.dateFormat())) {
            String format = DateUtils.YYYYMMDDHHMMSS_SSS;
            if (value instanceof Long) {
                value = String.valueOf(value);
            }
            if (value instanceof String) {
                String dateStr =String.valueOf(value);
                dateStr=dateStr+DATEFORMAT.substring(0, DATEFORMAT.length() - dateStr.length());
                Date dateTime = DateUtils.dateTime(format, dateStr);
                return DateUtils.parseDateToStr(annotation.dateFormat(), dateTime);
            } else if (value instanceof Date) {
                DateUtils.parseDateToStr(annotation.dateFormat(), (Date) value);
            }
        }

        // 字典转换(这里需要根据实际情况实现字典转换)
        if (StringUtils.isNotEmpty(annotation.dictType())) {
            // 这里可以调用字典服务进行转换
            try {
                return DictUtils.getDictLabel(annotation.dictType(), value.toString());
            }catch (Exception e) {
                return value.toString();
            }
        }

        // 其他类型处理
        if (value instanceof Date) {
            return DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, (Date) value);
        } else if (value instanceof BigDecimal) {
            return ((BigDecimal) value).toPlainString();
        } else if (value instanceof Boolean) {
            return (Boolean) value ? "是" : "否";
        }

        return value.toString();
    }

    public static class CellData {
        private String value;
        private String style;
        private String align;

        public CellData(String value, String style,String align) {
            this.value = value;
            this.style = style;
            this.align = align;
        }

        public String getValue() {
            return value;
        }

        public String getStyle() {
            return style;
        }
        public String getAlign() {
            return align;
        }
    }

    /**
     * 生成HTML表格
     */
    public static String generateTableHtml(List<?> dataList) {
        String nodatamsg;
        try{
            nodatamsg = MessageUtils.message("common.noData");
        }catch (Exception e){
            nodatamsg = "暂无数据";
        }
        if (dataList == null || dataList.isEmpty()) {
            return "<html><body><p>"+nodatamsg+"</p></body></html>";
        }

        Class<?> clazz = dataList.get(0).getClass();
        PdfSheet sheetAnnotation = clazz.getAnnotation(PdfSheet.class);
        String title;
        try {
            if(sheetAnnotation != null){
                title = MessageUtils.message(sheetAnnotation.title());
            }else{
                title = "数据报表";
            }
        }catch (Exception e){
            log.error("国际化异常:",e);
            title = "数据报表";
        }

        List<String> headers = getHeaders(clazz);
        List<List<CellData>> data = getDataRows(dataList);

        return buildHtmlTable(title, headers, data);
    }

    /**
     * 构建HTML表格
     */
    private static String buildHtmlTable(String title, List<String> headers, List<List<CellData>> data) {
        StringBuilder html = new StringBuilder();

        // 计算列宽
        int colCount = headers.size();
        int colWidth = 100 / colCount;

        html.append("<!DOCTYPE html>")
                .append("<html>")
                .append("<head>")
                .append("<meta charset=\"UTF-8\">")
                .append("<style>")
                .append("body { font-family: 'SimSun', Arial, sans-serif; margin: 20px; }")
                .append(".title { text-align: center; font-size: 18px; font-weight: bold; margin-bottom: 20px; }")
                .append(".table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 12px; }")
                .append(".table th { background-color: #f8f9fa; border: 1px solid #dee2e6; padding: 8px; text-align: center; font-weight: bold; }")
                .append(".table td { border: 1px solid #dee2e6; padding: 6px; }")
                .append(".text-left { text-align: left; }")
                .append(".text-center { text-align: center; }")
                .append(".text-right { text-align: right; }")
                .append(".header-info { margin-bottom: 15px; font-size: 13px; }")
                .append(".footer { margin-top: 30px; text-align: right; font-size: 12px; color: #666; }")
                .append("</style>")
                .append("</head>")
                .append("<body>");

        // 标题
        html.append("<div class=\"title\">").append(title).append("</div>");

        // 生成时间
        html.append("<div class=\"header-info\">")
                .append(MessageUtils.message("common.exportTime")+":").append(DateUtils.getTime())
                .append("</div>");

        // 表格
        html.append("<table class=\"table\">")
                .append("<thead><tr>");

        // 表头
        for (String header : headers) {
            html.append("<th>").append(header).append("</th>");
        }
        html.append("</tr></thead>")
                .append("<tbody>");

        // 数据行
        if (data.isEmpty()) {
            html.append("<tr><td colspan=\"").append(headers.size())
                    .append("\" class=\"text-center\">"+MessageUtils.message("common.noData")+"</td></tr>");
        } else {
            for (int i = 0; i < data.size(); i++) {
                List<CellData> row = data.get(i);
                html.append("<tr>");
                for (CellData cell : row) {
                    String style = cell.getStyle();
                    String alignClass = StringUtils.isNotEmpty(cell.getAlign())?("text-"+cell.getAlign()):"text-left"; // 根据需要从注解获取对齐方式

                    html.append("<td class=\"").append(alignClass).append("\"");
                    if (StringUtils.isNotEmpty(style)) {
                        html.append(" style=\"").append(style).append("\"");
                    }
                    html.append(">")
                            .append(cell.getValue() != null ? cell.getValue() : "")
                            .append("</td>");
                }
                html.append("</tr>");

                // 每50行添加分页(横向时行数可以更多)
                if ((i + 1) % 100 == 0 && i < data.size() - 1) {
                    html.append("</tbody></table></div>")
                            .append("<div class=\"page-break\"></div>")
                            .append("<div class=\"table-container\">")
                            .append("<table class=\"table\">")
                            .append("<thead><tr>");
                    for (String header : headers) {
                        html.append("<th>").append(header).append("</th>");
                    }
                    html.append("</tr></thead><tbody>");
                }
            }
        }

        html.append("</tbody>")
                .append("</table>")
                .append("</div>");

        // 页脚
        html.append("<div class=\"footer\">")
                .append(MessageUtils.message("common.totalCount", data.size()))
                .append("</div>")
                .append("</body>")
                .append("</html>");

        return html.toString();
    }

    /**
     * 获取表格标题
     */
    public static String getSheetTitle(Class<?> clazz) {
        PdfSheet sheetAnnotation = clazz.getAnnotation(PdfSheet.class);
        return sheetAnnotation != null ? sheetAnnotation.title() : "数据报表";
    }

    /**
     * 生成横向表格HTML
     */
    public static String generateLandscapeTableHtml(List<?> dataList) {
        String nodatamsg;
        try{
            nodatamsg = MessageUtils.message("common.noData");
        }catch (Exception e){
            nodatamsg = "暂无数据";
        }
        if (dataList == null || dataList.isEmpty()) {
            return "<html><body><p>"+nodatamsg+"</p></body></html>";
        }

        Class<?> clazz = dataList.get(0).getClass();
        PdfSheet sheetAnnotation = clazz.getAnnotation(PdfSheet.class);
        String title;
        try {
            if(sheetAnnotation != null){
                title = MessageUtils.message(sheetAnnotation.title());
            }else{
                title = "数据报表";
            }
        }catch (Exception e){
            log.error("国际化异常:",e);
            title = "数据报表";
        }

        List<String> headers = getHeaders(clazz);
        List<List<CellData>> data = getDataRows(dataList);

        return buildLandscapeHtmlTable(title, headers, data, sheetAnnotation);
    }

    /**
     * 构建横向HTML表格
     */
    private static String buildLandscapeHtmlTable(String title, List<String> headers,
                                                  List<List<CellData>> data, PdfSheet sheetAnnotation) {
        StringBuilder html = new StringBuilder();

        // 计算列宽(横向时列可以更宽)
        int colCount = headers.size();
        int baseColWidth = 100 / Math.min(colCount, 10); // 横向时限制最大列数

        html.append("<!DOCTYPE html>")
                .append("<html>")
                .append("<head>")
                .append("<meta charset=\"UTF-8\">")
                .append("<style>")
                .append("@page { size: landscape; margin: 1cm; }")
                .append("body { font-family: 'SimSun', Arial, sans-serif; margin: 0; padding: 20px; }")
                .append(".title { text-align: center; font-size: 18px; font-weight: bold; margin-bottom: 20px; }")
                .append(".table-container { width: 100%; overflow-x: auto; }")
                .append(".table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 11px; }")
                .append(".table th { background-color: #f8f9fa; border: 1px solid #dee2e6; padding: 8px; text-align: center; font-weight: bold; min-width: 80px; }")
                .append(".table td { border: 1px solid #dee2e6; padding: 6px; word-wrap: break-word; word-break: break-all; }")
                .append(".text-left { text-align: left; }")
                .append(".text-center { text-align: center; }")
                .append(".text-right { text-align: right; }")
                .append(".header-info { margin-bottom: 15px; font-size: 12px; display: flex; justify-content: space-between; }")
                .append(".footer { margin-top: 30px; text-align: right; font-size: 11px; color: #666; }")
                .append(".page-break { page-break-after: always; }")
                .append("</style>")
                .append("</head>")
                .append("<body>");

        // 标题
        html.append("<div class=\"title\">").append(title).append("</div>");

        // 生成时间
        html.append("<div class=\"header-info\">")
                .append("<div>"+MessageUtils.message("common.exportTime")+":").append(DateUtils.getTime()).append("</div>")
                .append("</div>");

        // 表格容器
        html.append("<div class=\"table-container\">")
                .append("<table class=\"table\">")
                .append("<thead><tr>");

        // 表头
        for (String header : headers) {
            html.append("<th>").append(header).append("</th>");
        }
        html.append("</tr></thead>")
                .append("<tbody>");

        // 数据行
        if (data.isEmpty()) {
            html.append("<tr><td colspan=\"").append(headers.size())
                    .append("\" class=\"text-center\">"+MessageUtils.message("common.noData")+"</td></tr>");
        } else {
            for (int i = 0; i < data.size(); i++) {
                List<CellData> row = data.get(i);
                html.append("<tr>");
                for (CellData cell : row) {
                    String style = cell.getStyle();
                    String alignClass = "text-left";

                    html.append("<td class=\"").append(alignClass).append("\"");
                    if (StringUtils.isNotEmpty(style)) {
                        html.append(" style=\"").append(style).append("\"");
                    }
                    html.append(">")
                            .append(cell.getValue() != null ? cell.getValue() : "")
                            .append("</td>");
                }
                html.append("</tr>");

                // 每50行添加分页(横向时行数可以更多)
                if ((i + 1) % 100 == 0 && i < data.size() - 1) {
                    html.append("</tbody></table></div>")
                            .append("<div class=\"page-break\"></div>")
                            .append("<div class=\"table-container\">")
                            .append("<table class=\"table\">")
                            .append("<thead><tr>");
                    for (String header : headers) {
                        html.append("<th>").append(header).append("</th>");
                    }
                    html.append("</tr></thead><tbody>");
                }
            }
        }

        html.append("</tbody>")
                .append("</table>")
                .append("</div>");

        // 页脚
        html.append("<div class=\"footer\">")
                .append(MessageUtils.message("common.totalCount", data.size()))
                .append("</div>")
                .append("</body>")
                .append("</html>");

        return html.toString();
    }

    /**
     * 根据注解判断是否需要横向导出
     */
    public static boolean isLandscapeExport(Class<?> clazz) {
        PdfSheet sheetAnnotation = clazz.getAnnotation(PdfSheet.class);
        return sheetAnnotation != null && "landscape".equalsIgnoreCase(sheetAnnotation.orientation());
    }

}

4.PDF导出服务接口

IPdfExportService.java

java 复制代码
package com.asiadb.common.core.service;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;

/**
 * PDF导出服务接口
 */
public interface IPdfExportService<T> {

    /**
     * 导出数据为PDF
     */
    void exportPdf(HttpServletResponse response, String fileName, List<?> dataList) throws IOException;

    void exportLandscapePdf(HttpServletResponse response, String fileName, List<?> dataList) throws IOException;

    /**
     * 导出数据为PDF(带自定义参数)
     */
    void exportPdf(HttpServletResponse response, String fileName, List<?> dataList, Map<String, Object> params) throws IOException;

    /**
     * 生成PDF字节数组
     */
    byte[] generatePdfBytes(List<?> dataList) throws IOException;

    /**
     * 生成PDF字节数组(带自定义参数)
     */
    byte[] generatePdfBytes(List<?> dataList, Map<String, Object> params) throws IOException;

    String generateHtmlContent(List<T> dataList, Map<String, Object> params);

    String customizeHtml(String originalHtml, Map<String, Object> params);

    String generateStyledHtml(List<T> dataList, String customCss);
}

AbstractPdfExportService.java

java 复制代码
package com.asiadb.common.core.service;

import com.asiadb.common.core.service.IPdfExportService;
import com.asiadb.common.utils.pdf.PdfExportUtil;
import com.asiadb.common.utils.pdf.PdfUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;

/**
 * 抽象PDF导出服务(支持注解)
 */
@Service
public abstract class AbstractPdfExportService<T> implements IPdfExportService<T> {

    @Override
    public void exportPdf(HttpServletResponse response, String fileName, List<?> dataList) throws IOException {
        exportPdf(response, fileName, dataList, null);
    }

    @Override
    public void exportLandscapePdf(HttpServletResponse response, String fileName, List<?> dataList) throws IOException {
        String htmlContent = generateLandscapeHtmlContent((List<T>) dataList, null);
        PdfUtils.exportPdf(response, fileName, htmlContent);
    }

    @Override
    public void exportPdf(HttpServletResponse response, String fileName, List<?> dataList, Map<String, Object> params) throws IOException {
        String htmlContent = generateHtmlContent((List<T>) dataList, params);
        PdfUtils.exportPdf(response, fileName, htmlContent);
    }

    @Override
    public byte[] generatePdfBytes(List<?> dataList) throws IOException {
        return generatePdfBytes(dataList, null);
    }

    @Override
    public byte[] generatePdfBytes(List<?> dataList, Map<String, Object> params) throws IOException {
        String htmlContent = generateHtmlContent((List<T>) dataList, params);
        return PdfUtils.generatePdfFromHtml(htmlContent);
    }

    /**
     * 生成HTML内容(默认实现使用注解)
     */
    @Override
    public String generateHtmlContent(List<T> dataList, Map<String, Object> params) {
        // 使用注解方式生成HTML
        String html = PdfExportUtil.generateTableHtml(dataList);

        // 如果子类需要自定义,可以重写此方法
        if (params != null) {
            // 可以在这里处理额外的参数
            html = customizeHtml(html, params);
        }

        return html;
    }

    /**
     * 生成HTML内容(默认实现使用注解)
     */
    public String generateLandscapeHtmlContent(List<T> dataList, Map<String, Object> params) {
        // 使用注解方式生成HTML
        String html = PdfExportUtil.generateLandscapeTableHtml(dataList);

        // 如果子类需要自定义,可以重写此方法
        if (params != null) {
            // 可以在这里处理额外的参数
            html = customizeHtml(html, params);
        }

        return html;
    }

    /**
     * 自定义HTML(子类可重写)
     */
    @Override
    public String customizeHtml(String originalHtml, Map<String, Object> params) {
        return originalHtml;
    }

    /**
     * 生成带自定义样式的HTML
     */
    @Override
    public String generateStyledHtml(List<T> dataList, String customCss) {
        String html = PdfExportUtil.generateTableHtml(dataList);

        if (StringUtils.isNotEmpty(customCss)) {
            // 替换或添加自定义样式
            html = html.replace("</style>", customCss + "</style>");
        }

        return html;
    }
}

使用

1.报表需求实现service

ReportReconciliationPdfService.java

java 复制代码
package com.asiadb.report.service;

import com.asiadb.common.core.service.AbstractPdfExportService;
import com.asiadb.report.domain.ReportReconciliation;
import org.springframework.stereotype.Service;

/**
 * @ClassName ReportReconciliationPdfService
 * @Description TODO
 * @Author 
 * @Date 2025/12/30 14:46
 * @Version 1.0
 **/
@Service
public class ReportReconciliationPdfService  extends AbstractPdfExportService<ReportReconciliation> {

}

2.bean注解

ReportReconciliation.java

java 复制代码
package com.asiadb.report.domain;

import com.asiadb.common.annotation.Excel;
import com.asiadb.common.annotation.PdfField;
import com.asiadb.common.annotation.PdfSheet;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;

/**
 * <p>
 * 对账报表
 * </p>
 *
 * @author author
 * @since 2025-12-26
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_report_reconciliation")
@PdfSheet(
        title = "export.ReportReconciliation.sheetname",
        orientation = "landscape",  // 关键:设置为横向
        pageSize = "A4",           // A4横向
        showIndex = false,
        marginLeft = 15,
        marginRight = 15,
        marginTop = 20,
        marginBottom = 20
)
public class ReportReconciliation implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * ID
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 日期
     */
    @TableField("batch_date")
    @Excel(name = "export.ReportReconciliation.batchDate",dateFormat = "yyyy-MM-dd")
    @PdfField(name = "export.ReportReconciliation.batchDate", sort = 1,dateFormat = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private String batchDate;

    /**
     * 公式
     */
    @TableField("equation")
    @Excel(name = "export.ReportReconciliation.equation")
    @PdfField(name = "export.ReportReconciliation.equation", sort = 2)
    private String equation;

    /**
     * 数值
     */
    @TableField("data")
    @Excel(name = "export.ReportReconciliation.data")
    @PdfField(name = "export.ReportReconciliation.data", sort = 3)
    private String data;

    /**
     * 结果
     */
    @TableField("result")
    @Excel(name = "export.ReportReconciliation.result")
    @PdfField(name = "export.ReportReconciliation.result", sort = 4)
    private String result;


    private String type;

    @Excel(name = "export.ReportReconciliation.resultType")
    @PdfField(name = "export.ReportReconciliation.resultType", dictType = "recon_resulttype",conditionalColor="[{\"value\":\"0\",\"color\":\"#67C23A\"},{\"value\":\"1\",\"color\":\"#F56C6C\"}]", sort = 5)
    @TableField(exist = false)
    private String resultType;


}

3.Controller类

ReportReconciliationController.java

java 复制代码
@ApiOperation("导出对账仪表盘pdf")
    @PreAuthorize("@ss.hasPermi('reportCenter:reportReconciliation:export')")
    @Log(title = "导出发对账仪表盘pdf", businessType = BusinessType.EXPORT)
    @GetMapping("/exportpdf")
    public void exportpdf(String batchDate, HttpServletResponse response)
    {
        List<ReportReconciliation> list = reportReconciliationService.list(getQueryWrapper(new ReportReconciliation().setBatchDate(batchDate)));
        processReconciliationResults(list);
        String fileName = MessageUtils.message("export.ReportReconciliation.sheetname");

        try {
            reportReconciliationPdfService.exportLandscapePdf(response, fileName, list);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

4.前端

download.js

javascript 复制代码
exportPdf(url) {
    return new Promise((resolve, reject) => {
      var _url = baseURL + url;
      axios({
        method: 'get',
        url: _url,
        responseType: 'blob',
        headers: {
          'Authorization': 'Bearer ' + getToken(),
          "signtimestamp": new Date().getTime(),
          "browserid": localStorage.getItem('browserId'),
          "Accept-Language": store.getters.language,
        },
      }).then(res => {
        try {
          const blob = new Blob([res.data], { type: 'application/pdf' });
          const filename = decodeURIComponent(res.headers['download-filename'] || 'download.pdf');
          saveAs(blob, filename);
          resolve({
            success: true,
            filename: filename,
            blob: blob
          });
        } catch (error) {
          reject(new Error('保存文件失败'));
        }
      }).catch(error => {
        console.error('下载PDF失败:', error);
        // 处理不同的错误类型
        let errorMessage = '下载失败';
        reject(new Error(errorMessage));
      });
    });
  },

vue中按钮使用

javascript 复制代码
// 导出PDF
    async handleExportPdf() {
      try {
        // 显示确认对话框
        await this.$modal.confirm(this.$t(this.config.confirmExportContent));
        this.exportpdfLoading = true;
        await this.$download.exportPdf(
          "/reportCenter/reportReconciliation/exportpdf?batchDate=" +
          this.config.queryParams.batchDate
        );
        // 下载成功
      } catch (error) {
        // 用户取消确认对话框
        if (error === 'cancel' || error?.message?.includes('cancel')) {
          return;
        }
        // 下载失败
        console.error('导出PDF失败:', error);
      } finally {
        // 无论成功失败,都关闭loading
        this.exportpdfLoading = false;
      }
    },

导出结果展示

相关推荐
qinyia14 小时前
WisdomSSH解决硬盘直通给飞牛系统时控制器无法绑定的问题
java·linux·服务器
kkoral14 小时前
RuoYi AI 框架部署操作指南
java·ai·ruoyi
TDengine (老段)14 小时前
TDengine JAVA 语言连接器入门指南
java·大数据·开发语言·数据库·python·时序数据库·tdengine
yaoxin52112314 小时前
282. Java Stream API - 从 Collection 或 Iterator 创建 Stream
java
indexsunny14 小时前
互联网大厂Java面试实战:Spring Boot、微服务与Kafka在电商场景中的应用
java·spring boot·redis·junit·kafka·mockito·microservices
悟能不能悟14 小时前
openfeign 返回void和ResponseEntity的区别
java
C雨后彩虹14 小时前
ReentrantLock 源码解析:AQS 核心原理
java·reentrantlock·lock