瀏覽代碼

fix: freemark 生成pdf

gufj 5 月之前
父節點
當前提交
6f0bdab79f

+ 22 - 28
bound-link-api/blink-base/pom.xml

@@ -281,32 +281,24 @@
             <artifactId>snakeyaml</artifactId>
         </dependency>
 
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-mail</artifactId>
-        </dependency>
-
         <dependency>
             <groupId>org.jsoup</groupId>
             <artifactId>jsoup</artifactId>
             <version>1.18.1</version>
         </dependency>
-
-        <dependency>
-            <groupId>org.freemarker</groupId>
-            <artifactId>freemarker</artifactId>
-        </dependency>
-
+        <!--    druid    -->
         <dependency>
             <groupId>com.alibaba</groupId>
             <artifactId>druid-spring-boot-starter</artifactId>
             <version>1.2.16</version>
         </dependency>
+        <!--    dingtalk    -->
         <dependency>
             <groupId>com.aliyun</groupId>
             <artifactId>dingtalk</artifactId>
             <version>2.1.14</version>
         </dependency>
+
         <dependency>
             <groupId>com.dingtalk.api</groupId>
             <artifactId>dingtalk</artifactId>
@@ -315,27 +307,13 @@
             <systemPath>${project.basedir}/src/main/resources/lib/taobao-sdk-java-auto_1479188381469-20220309.jar
             </systemPath>
         </dependency>
+        <!--    minio    -->
         <dependency>
             <groupId>io.minio</groupId>
             <artifactId>minio</artifactId>
             <version>8.3.4</version>
         </dependency>
-
-        <dependency>
-            <groupId>org.freemarker</groupId>
-            <artifactId>freemarker</artifactId>
-            <version>2.3.32</version>
-        </dependency>
-        <dependency>
-            <groupId>com.itextpdf.tool</groupId>
-            <artifactId>xmlworker</artifactId>
-            <version>5.5.11</version>
-        </dependency>
-        <dependency>
-            <groupId>org.xhtmlrenderer</groupId>
-            <artifactId>flying-saucer-pdf-itext5</artifactId>
-            <version>9.1.18</version>
-        </dependency>
+        <!--    mapstruct    -->
         <dependency>
             <groupId>org.mapstruct</groupId>
             <artifactId>mapstruct</artifactId>
@@ -351,7 +329,7 @@
             <artifactId>liquibase-core</artifactId>
             <version>4.31.0</version>
         </dependency>
-
+        <!--飞书-->
         <dependency>
             <groupId>com.larksuite.oapi</groupId>
             <artifactId>oapi-sdk</artifactId>
@@ -373,5 +351,21 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-freemarker</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>org.xhtmlrenderer</groupId>
+            <artifactId>flying-saucer-pdf-itext5</artifactId>
+            <version>9.1.22</version>
+        </dependency>
+        <dependency>
+            <groupId>com.itextpdf</groupId>
+            <artifactId>itext-asian</artifactId>
+            <version>5.2.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.itextpdf.tool</groupId>
+            <artifactId>xmlworker</artifactId>
+            <version>5.5.11</version>
+        </dependency>
     </dependencies>
 </project>

+ 20 - 0
bound-link-api/blink-base/src/main/java/com/cloud/sa/base/module/support/pdf/FreemarkerConfiguration.java

@@ -0,0 +1,20 @@
+package com.cloud.sa.base.module.support.pdf;
+
+import freemarker.template.Configuration;
+
+/**
+ * @author gufj
+ * @date 2025-05-18
+ */
+public class FreemarkerConfiguration {
+    private static Configuration config;
+    static {
+        config = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
+        config.setClassForTemplateLoading(FreemarkerConfiguration.class, "/templates/");
+        config.setClassicCompatible(true);
+    }
+
+    public static Configuration getConfiguration() {
+        return config;
+    }
+}

+ 271 - 0
bound-link-api/blink-base/src/main/java/com/cloud/sa/base/module/support/pdf/PDFBuilder.java

@@ -0,0 +1,271 @@
+package com.cloud.sa.base.module.support.pdf;
+
+import com.itextpdf.text.*;
+import com.itextpdf.text.Font;
+import com.itextpdf.text.Image;
+import com.itextpdf.text.Rectangle;
+import com.itextpdf.text.pdf.*;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * @author gufj
+ * @date 2025-05-18
+ */
+public class PDFBuilder extends PdfPageEventHelper {
+    /**
+     * 页眉
+     */
+    public String header = "";
+
+    /**
+     * 文档字体大小,页腳页眉最好和文本大小一致
+     */
+    public int presentFontSize = 10;
+
+    /**
+     * 文档页面大小,最好前面传入,否則默认为A4
+     */
+    public Rectangle pageSize = PageSize.A4;
+
+    /**
+     * 模板
+     */
+    public PdfTemplate total;
+
+    /**
+     * 基础字体对象
+     */
+    public BaseFont bf = null;
+
+    /**
+     * 利用基础字体生成的字体对象,一般用于生成中文文字
+     */
+    public Font fontDetail = null;
+    /**
+     * 水印文件
+     */
+    private boolean watermark = true;
+    /**
+     * 是否展示页脚页码信息
+     */
+    private boolean isHeaderFooter = false;
+
+    /**
+     * 左上角logo
+     */
+    private String plate;
+
+    /**
+     * 字体
+     */
+    private String fontName = "STSong-Light";
+
+    /**
+     * 编码
+     */
+    private String encoding = "UniGB-UCS2-H";
+    /**
+     * @param header
+     * @param presentFontSize
+     * @param pageSize
+     * @param watermark
+     */
+    public PDFBuilder(String header, int presentFontSize, Rectangle pageSize, boolean watermark,
+                      boolean isHeaderFooter, String plate) {
+        this.header = header;
+        this.presentFontSize = presentFontSize;
+        this.pageSize = pageSize;
+        this.watermark = watermark;
+        this.isHeaderFooter = isHeaderFooter;
+        this.plate = plate;
+    }
+
+    /**
+     * 文档打开时创建模板
+     */
+    @Override
+    public void onOpenDocument(PdfWriter writer, Document document) {
+        /**
+         * 共 页 的矩形的长宽高
+         */
+        total = writer.getDirectContent().createTemplate(50, 50);
+    }
+
+    /**
+     * 关闭每页的时候添加页眉页腳和水印
+     */
+    @Override
+    public void onEndPage(PdfWriter writer, Document document) {
+        /**
+         * 添加分页
+         */
+        if (isHeaderFooter) {
+            this.addPage(writer, document);
+        }
+        /**
+         * 添加水印
+         */
+        if (watermark) {
+            this.addWatermark(writer, document);
+        }
+    }
+
+    /**
+     * 分页
+     *
+     * @param writer
+     * @param document
+     */
+    public void addPage(PdfWriter writer, Document document) {
+        try {
+            if (bf == null) {
+                bf = BaseFont.createFont(fontName, encoding, BaseFont.NOT_EMBEDDED);
+            }
+            if (fontDetail == null) {
+                /**
+                 * 数据字体好
+                 */
+                fontDetail = new Font(bf, presentFontSize, Font.NORMAL);
+                fontDetail.setColor(BaseColor.BLACK);
+            }
+        } catch (Exception e) {
+            //  log.error("", e);
+        }
+
+        /**
+         * 写入页眉
+         */
+        ColumnText.showTextAligned(writer.getDirectContent(), Element.ALIGN_LEFT,
+                new Phrase(header, fontDetail), document.left(), document.top() + 10, 0);
+
+        Image img;
+        try {
+            String logoPath = "templates/" + plate + "Logo.png";
+            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+            InputStream inputStream = classLoader.getResourceAsStream(logoPath);
+
+            int n;
+            byte[] buffer = new byte[4096];
+            ByteArrayOutputStream output = new ByteArrayOutputStream();
+            while (-1 != (n = inputStream.read(buffer))) {
+                output.write(buffer, 0, n);
+            }
+            img = Image.getInstance(output.toByteArray());
+
+        } catch (BadElementException e) {
+            throw new RuntimeException(e);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        img.setAlignment(Image.ALIGN_RIGHT);
+        img.setWidthPercentage(80);
+        img.scaleToFit(50, 50);
+        img.setAbsolutePosition(document.right() - 50, document.top() + 10);
+        try {
+            writer.getDirectContent().addImage(img);
+        } catch (DocumentException e) {
+            throw new RuntimeException(e);
+        }
+        // 页眉加下划线
+        PdfPTable tableHeader = new PdfPTable(1);
+        tableHeader.setTotalWidth(PageSize.A4.getWidth() - 60);
+        PdfPCell pCell = new PdfPCell();
+        pCell.setBorderWidthBottom(0.3f);
+        tableHeader.addCell(pCell);
+        tableHeader.writeSelectedRows(0, -1, 30, 810, writer.getDirectContent());
+
+        /**
+         * 写入页脚(分页信息)
+         */
+        int pageS = writer.getPageNumber();
+        String foot1 = "第 " + pageS + " 页 / 共";
+        Phrase footer = new Phrase(foot1, fontDetail);
+
+        /**
+         * 计算前半部分的foot1的長度,后面好定位最后一部分的'Y页'这两字的x轴坐标,字体长度也要计算进去 = len
+         */
+        float len = bf.getWidthPoint(foot1, presentFontSize);
+
+        /**
+         * 拿到当前的PdfContentByte
+         */
+        PdfContentByte cb = writer.getDirectContent();
+
+        ColumnText.showTextAligned(cb, Element.ALIGN_CENTER, footer,
+                (document.rightMargin() + document.right() + document.leftMargin() - document.left() - len)
+                        / 2.0F + 20F,
+                document.bottom() - 25, 0);
+
+        cb.addTemplate(total,
+                (document.rightMargin() + document.right() + document.leftMargin() - document.left()) / 2.0F
+                        + 20F,
+                document.bottom() - 25);
+    }
+
+
+    /**
+     * 添加水印
+     *
+     * @param writer
+     */
+    public void addWatermark(PdfWriter writer, Document document) {
+        // 加入水印
+        PdfContentByte waterMark = writer.getDirectContent();
+        Rectangle pageSize = writer.getPageSize();
+        // 开始设置水印
+        waterMark.beginText();
+        // 设置水印透明度
+        PdfGState gs = new PdfGState();
+        // 设置填充字体不透明度为0.4f
+        gs.setFillOpacity(0.2f);
+
+        JLabel label = new JLabel();
+        FontMetrics metrics;
+        int textH;
+        int textW;
+        label.setText("无限畅联");
+        metrics = label.getFontMetrics(label.getFont());
+        textH = metrics.getHeight();
+        textW = metrics.stringWidth(label.getText());
+        try {
+            // 设置水印字体参数及大小 (字体参数,字体编码格式,是否将字体信息嵌入到pdf中(一般不需要嵌入),字体大小)
+            waterMark.setFontAndSize(BaseFont.createFont(fontName, encoding, BaseFont.NOT_EMBEDDED), 20);
+            // 设置透明度
+            waterMark.setGState(gs);
+            for (int height = textH + textH * 5; height < pageSize.getHeight(); height = height + textH * 5) {
+                if (height + textH * 5 < pageSize.getHeight()) {
+                    for (int width = textW; width < pageSize.getWidth() * 1.5 + textW; width = width + textW * 3) {
+                        waterMark.showTextAligned(Element.ALIGN_LEFT, "无限畅联", width - textW, height - textH, 30);
+                    }
+                }
+            }
+            // 设置水印颜色
+            waterMark.setColorFill(BaseColor.LIGHT_GRAY);
+            //结束设置
+            waterMark.endText();
+            waterMark.stroke();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (DocumentException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public void onCloseDocument(PdfWriter writer, Document document) {
+        if (isHeaderFooter) {
+            total.beginText();
+            total.setFontAndSize(bf, presentFontSize);
+            total.setColorFill(BaseColor.GRAY);
+            String foot2 = " " + (writer.getPageNumber()) + " 页";
+            total.showText(foot2);
+            total.endText();
+            total.closePath();
+        }
+    }
+}

+ 216 - 0
bound-link-api/blink-base/src/main/java/com/cloud/sa/base/module/support/pdf/PDFCommonGenerator.java

@@ -0,0 +1,216 @@
+package com.cloud.sa.base.module.support.pdf;
+
+import com.itextpdf.text.Document;
+import com.itextpdf.text.Font;
+import com.itextpdf.text.Image;
+import com.itextpdf.text.Rectangle;
+import com.itextpdf.text.pdf.BaseFont;
+import com.itextpdf.text.pdf.PdfWriter;
+import com.itextpdf.tool.xml.Pipeline;
+import com.itextpdf.tool.xml.XMLWorker;
+import com.itextpdf.tool.xml.XMLWorkerFontProvider;
+import com.itextpdf.tool.xml.XMLWorkerHelper;
+import com.itextpdf.tool.xml.html.CssAppliersImpl;
+import com.itextpdf.tool.xml.html.Tags;
+import com.itextpdf.tool.xml.net.FileRetrieve;
+import com.itextpdf.tool.xml.net.ReadingProcessor;
+import com.itextpdf.tool.xml.parser.XMLParser;
+import com.itextpdf.tool.xml.pipeline.css.CSSResolver;
+import com.itextpdf.tool.xml.pipeline.css.CssResolverPipeline;
+import com.itextpdf.tool.xml.pipeline.end.PdfWriterPipeline;
+import com.itextpdf.tool.xml.pipeline.html.AbstractImageProvider;
+import com.itextpdf.tool.xml.pipeline.html.HtmlPipeline;
+import com.itextpdf.tool.xml.pipeline.html.HtmlPipelineContext;
+import com.itextpdf.tool.xml.pipeline.html.ImageProvider;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.util.StringUtils;
+
+import java.io.*;
+import java.nio.charset.Charset;
+import java.util.Map;
+
+/**
+ * @author gufj
+ * @date 2025-05-18
+ */
+public class PDFCommonGenerator {
+
+    private static final Logger logger = LoggerFactory.getLogger(PDFCommonGenerator.class);
+
+    /**
+     * 生成html
+     *
+     * @param template
+     * @param variables
+     * @return
+     * @throws Exception
+     */
+    public static String htmlGenerate(String template, Map<String, Object> variables)
+            throws Exception {
+        Configuration config = FreemarkerConfiguration.getConfiguration();
+        Template tp = config.getTemplate(template, "UTF-8");
+        StringWriter stringWriter = new StringWriter();
+        BufferedWriter writer = new BufferedWriter(stringWriter);
+        tp.process(variables, writer);
+        String htmlStr = stringWriter.toString();
+        writer.flush();
+        writer.close();
+        return htmlStr;
+    }
+
+    /**
+     * 生成pdf
+     *
+     * @param htmlTemplate
+     * @param dataMap
+     * @param targetPdf
+     * @param pageSize
+     * @param header
+     * @param isFooter
+     * @param watermark
+     * @throws Exception
+     */
+    public static void pdfGeneratePlus(String htmlTemplate, Map<String, Object> dataMap,
+                                       String targetPdf, Rectangle pageSize, String header, boolean isFooter, boolean watermark, String plate)
+            throws Exception {
+        /**
+         * 根据freemarker模板生成html
+         */
+        String htmlStr = htmlGenerate(htmlTemplate, dataMap);
+        File outFile = new File("E:\\boundlink\\BoundLink-Plate\\bound-link-api\\blink-start\\src\\main\\resources\\templates\\report.html");
+        BufferedWriter output = null;
+        try {
+            output = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFile), "UTF-8"));
+            output.write(htmlStr);
+        } catch (
+                IOException e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                output.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        String charsetName = Charset.defaultCharset().toString();
+        Document document = new Document(pageSize);
+
+        OutputStream out = new FileOutputStream(targetPdf);
+        //设置边距
+        // document.setMargins(30, 30, 30, 30);
+        PdfWriter writer = PdfWriter.getInstance(document, out);
+        /**
+         * 添加页码
+         */
+        PDFBuilder builder = new PDFBuilder(header, 10, pageSize, watermark, isFooter, plate);
+        writer.setPageEvent(builder);
+        document.open();
+        /**
+         * html內容解析
+         */
+        HtmlPipelineContext htmlContext =
+                new HtmlPipelineContext(new CssAppliersImpl(new XMLWorkerFontProvider() {
+                    @Override
+                    public Font getFont(String fontname, String encoding, float size, final int style) {
+                        Font font = null;
+                        try {
+                            font = new Font(BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED), size, style);
+                        } catch (Exception e) {
+                            logger.error("pdfGeneratePlus 异常:", e);
+                        }
+                        return font;
+                    }
+                })) {
+                    @Override
+                    public HtmlPipelineContext clone() throws CloneNotSupportedException {
+                        HtmlPipelineContext context = super.clone();
+                        ImageProvider imageProvider = this.getImageProvider();
+                        context.setImageProvider(imageProvider);
+                        return context;
+                    }
+                };
+        /**
+         * 图片解析
+         */
+        htmlContext.setImageProvider(new AbstractImageProvider() {
+            String rootPath = "";
+
+            @Override
+            public String getImageRootPath() {
+                return rootPath;
+            }
+
+            @Override
+            public Image retrieve(String src) {
+                if (StringUtils.isEmpty(src)) {
+                    return null;
+                }
+                try {
+                    if (src.startsWith("http")) {
+                        return Image.getInstance(src);
+                    }
+                    Image image = Image.getInstance(new File(src).toURI().toString());
+                    if (image != null) {
+                        store(src, image);
+                        return image;
+                    }
+                } catch (Exception e) {
+                    logger.error("", e);
+                }
+                return super.retrieve(src);
+            }
+        });
+        htmlContext.setAcceptUnknown(true).autoBookmark(true)
+                .setTagFactory(Tags.getHtmlTagProcessorFactory());
+
+
+        /**
+         * css解析
+         */
+        CSSResolver cssResolver = XMLWorkerHelper.getInstance().getDefaultCssResolver(true);
+        cssResolver.setFileRetrieve(new FileRetrieve() {
+            @Override
+            public void processFromStream(InputStream in, ReadingProcessor processor) throws IOException {
+                try (InputStreamReader reader = new InputStreamReader(in, charsetName)) {
+                    int i = -1;
+                    while (-1 != (i = reader.read())) {
+                        processor.process(i);
+                    }
+                } catch (Throwable e) {
+                }
+            }
+
+            /**
+             * 解析href
+             */
+            @Override
+            public void processFromHref(String href, ReadingProcessor processor) throws IOException {
+                InputStream is = new ByteArrayInputStream(href.getBytes());
+                try {
+                    InputStreamReader reader = new InputStreamReader(is, charsetName);
+                    int i = -1;
+                    while (-1 != (i = reader.read())) {
+                        processor.process(i);
+                    }
+                } catch (Exception e) {
+                    logger.error("", e);
+                }
+
+            }
+        });
+
+        HtmlPipeline htmlPipeline =
+                new HtmlPipeline(htmlContext, new PdfWriterPipeline(document, writer));
+        Pipeline<?> pipeline = new CssResolverPipeline(cssResolver, htmlPipeline);
+        XMLWorker worker = null;
+        worker = new XMLWorker(pipeline, true);
+        XMLParser parser = new XMLParser(true, worker, Charset.forName(charsetName));
+        try (InputStream inputStream = new ByteArrayInputStream(htmlStr.getBytes())) {
+            parser.parse(inputStream, Charset.forName(charsetName));
+        }
+        document.close();
+    }
+}

+ 1 - 0
bound-link-api/blink-base/src/main/resources/dev/sa-base.yaml

@@ -106,6 +106,7 @@ spring:
       suffix: .html # 模版后缀名 默认为ftl
       charset: UTF-8 # 文件编码
       template-loader-path: classpath:/templates/  # 存放模板的文件夹,以resource文件夹为相对路径
+
   # json序列化相关配置
   jackson:
     serialization:

+ 26 - 1
bound-link-api/blink-management/src/main/java/com/cloud/sa/management/controller/BlinkCustomerController.java

@@ -1,18 +1,25 @@
 package com.cloud.sa.management.controller;
 
+import com.cloud.sa.base.common.annoation.NoNeedLogin;
 import com.cloud.sa.base.common.domain.PageResult;
 import com.cloud.sa.base.common.domain.ResponseDTO;
-import com.cloud.sa.management.domain.dto.BlinkCustomerDTO;
+import com.cloud.sa.base.module.support.pdf.PDFCommonGenerator;
 import com.cloud.sa.management.domain.dto.BlinkBizFileDTO;
+import com.cloud.sa.management.domain.dto.BlinkCustomerDTO;
 import com.cloud.sa.management.domain.qry.BlinkBizFileQry;
 import com.cloud.sa.management.domain.qry.BlinkCustomerQry;
 import com.cloud.sa.management.service.IBlinkCustomerService;
+import com.itextpdf.text.PageSize;
+import freemarker.template.Configuration;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import javax.validation.Valid;
+import java.util.HashMap;
+import java.util.Map;
 
 
 /**
@@ -25,6 +32,8 @@ public class BlinkCustomerController {
 
     @Resource
     private IBlinkCustomerService customerService;
+    @Resource
+    private Configuration configuration;
 
     @Operation(summary = "新增客户")
     @PostMapping("/supports/customer/create")
@@ -62,4 +71,20 @@ public class BlinkCustomerController {
     public ResponseDTO<PageResult<BlinkBizFileDTO>> customerFileQueryPage(@RequestBody BlinkBizFileQry qry) {
         return customerService.bizFileQueryPage(qry);
     }
+
+    @NoNeedLogin
+    @RequestMapping("/pdf/preview")
+    public ResponseEntity<byte[]> downloadPdfWithFixedHeaderAndFooter() throws Exception {
+        Map<String, Object> dataMap = new HashMap<String, Object>();
+        dataMap.put("generationDate", "1");
+        dataMap.put("memberName","Nick Liu");
+        dataMap.put("memberAddress","Nanshan District, Shenzhen city, Guangdong Province");
+        dataMap.put("accountNo","88888888888888");
+        dataMap.put("bankName","ICBC");
+        dataMap.put("bankSwiftCode","ABCDEFG");
+        dataMap.put("bankAddress","Shenzhen city of Guangdong Province");
+        dataMap.put("countryName","China");
+        PDFCommonGenerator.pdfGeneratePlus("report.html", dataMap, "D:\\Downloads\\CALIBRI\\1.pdf", PageSize.A4, "123", true, true, "wx");
+        return null;
+    }
 }

+ 124 - 0
bound-link-api/blink-start/src/main/resources/templates/report.html

@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8" />
+  <title>Running Headers and Footers</title>
+  <style>
+    @page {
+      size: A4;
+      margin: 40mm 10mm 50mm 10mm;
+
+      @top-left {
+        content: element(headerLeft);
+      }
+
+      @bottom-center {
+        content: element(footerCenter);
+      }
+    }
+
+    * {
+      padding: 0;
+      margin: 0;
+    }
+
+    body {
+      font-family: Calibri, serif;
+    }
+
+    .headerLeft {
+      position: running(headerLeft);
+    }
+
+    .titleWrapper > div {
+      margin: 2px 0;
+    }
+
+    .footerCenter {
+      text-align: center;
+      position: running(footerCenter);
+    }
+
+    .footerTipsWrapper {
+      color: #C1A97D;
+      margin-top: 10px;
+      border-top: 2px solid #EFE7DA;
+    }
+
+    .footerTipsWrapper > div {
+      font-size: 12px;
+      margin-top: 12px;
+    }
+
+    .contentWrapper {
+      margin-top: -10px;
+    }
+
+    .paddingWrapper {
+      padding: 10px;
+    }
+
+    .accountIntroduction {
+      margin-top: 60px;
+      background-color: #EFE7DA;
+      border: 1px solid #EFE7DA;
+      border-radius: 10px;
+    }
+
+    .accountDetailsWrapper {
+      margin-top: 50px;
+      border: 3px solid #EFE7DA;
+      border-radius: 10px;
+    }
+
+    .subTitle {
+      font-weight: bold;
+      border-bottom: 2px solid #EFE7DA;
+      padding-bottom: 10px;
+    }
+
+    .accountDetails > div {
+      margin-top: 8px;
+    }
+  </style>
+</head>
+<body>
+<div class="headerLeft paddingWrapper">
+</div>
+<div class="footerCenter">
+  <div class="footerLogoWrapper"></div>
+  <div class="footerTipsWrapper">
+    <div>www.aletaplanet.com | account@aletaplanet.com</div>
+    <div>MPHK Management Company Limited | Suite 615, 6/F, Ocean Centre, Harbour City, Tsim Sha Tsui, Tsim Sha Tsui, Kowloon |<br/>
+      License No.: 21-10-03068
+    </div>
+  </div>
+</div>
+
+<div class="contentWrapper">
+  <div class="titleWrapper paddingWrapper">
+    <div><b>Proof of Account Details</b></div>
+    <div>Generated on: 1</div>
+  </div>
+  <div class="tips paddingWrapper">To whom it may concern,</div>
+  <div class="accountIntroduction paddingWrapper">
+    <div><b>Personal account of Nick Liu</b></div>
+    <div style="margin-top: 10px;word-break: break-word">
+      This letter confirms the below account details allow Nick Liu residing at Nanshan District, Shenzhen city, Guangdong Province to receive payments into his/ her AP-1 Account:
+    </div>
+  </div>
+  <div class="accountDetailsWrapper paddingWrapper">
+    <div class="subTitle">Business account details</div>
+    <div class="accountDetails">
+      <div>Account Name: Nick Liu</div>
+      <div>Account Number: 88888888888888</div>
+      <div>Bank Name: ICBC</div>
+      <div>Bank SWIFT/BIC: ABCDEFG</div>
+      <div>Bank Country: China</div>
+      <div>Bank Address: Shenzhen city of Guangdong Province</div>
+    </div>
+  </div>
+</div>
+</body>
+</html>
+

二進制
bound-link-api/blink-start/src/main/resources/templates/wxLogo.png