SpringBoot + Echars + Thymeleaf 后端转html,pdf

SpringBoot + Echars + Thymeleaf 后端转html,pdf

  • 需求:
    后端定期跑批,生成pdf文件发送给客户。

  • 版本:
    SpringBoot:2.1.4.RELEASE

  • 思路

  1. 后端将echars报表生成jpg,png图片
  2. 通过Thymeleaf 模板生成html
  3. 将html转成pdf文件

1. 需要引用的依赖


        <!-- thymeleaf模板 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- html 转 pdf 需要用的jar -->
        <dependency>
            <groupId>org.xhtmlrenderer</groupId>[添加链接描述](https://pan.baidu.com/s/1lvvdfPT1-Q9PENHq4HO5Ow)
            <artifactId>flying-saucer-pdf</artifactId>
            <version>9.1.6</version>
        </dependency>

2. 后端将echars报表生成jpg

2.1 phantomjs及echarts-convert资料下载

https://pan.baidu.com/s/1lvvdfPT1-Q9PENHq4HO5Ow
提取码:dq3z

2.1.1 phantomjs介绍

可以理解为一个插件,后端听过指令调用该插件将echars报表生成图片。

2.1.2 echarts-convert.js

echars相关js脚本,phantomjs执行需要的引用依赖。

2.2 echars转image.png

2.2.1 EchartsFileService 调用服务
@Service
public class EchartsFileServiceImpl implements EchartsFileService {

    public static final Logger logger = LoggerFactory.getLogger(EchartsFileServiceImpl.class);

    @Autowired
    private EchartsUtil echartsUtil;

    /**
     * @param options json echar图标数据
     * @return
     */
    @Override
    public String creatEchartsFile(String options) {
        try {
            JSON.parseObject(options);
        } catch (Exception e) {
            logger.error("options is not jsonString");
            return null;
        }
        return echartsUtil.generateEChart(options);
    }
}
2.2.2 EchartsUtil 工具生成图片
@Component
public class EchartsUtil {

    public static final Logger logger = LoggerFactory.getLogger(EchartsUtil.class);

    // phantomjs执行器目录:
    @Value("${echars.phantomJs}")
    private String phantomJs;
    // 图片生成的临时文件目录
    @Value("${echars.fileDirectory}")
    private String fileDirectory;
    @Value("${echars.jsPath}")
    private String jsPath;

    // 指令: 执行器路径 echarts-convert.js路径 -infile 数据源 -outfile 输出文件
    private final String EXEC_CMD = "{0} {1} -infile {2} -outfile {3}";


    /**
     * @param options 报表数据源
     * @return 生成文件地址 返回图片生成的路径
     */
    public String generateEChart(String options) {
        // 随机生成文件名
        String wFileName = UUID.randomUUID().toString();
        String imageName = wFileName + ".png";
        String jsonPath = writeFile(options, wFileName);
        // 生成的文件存储路径
        String imagePath = fileDirectory + imageName;
        try {
            File file = new File(jsonPath);     //文件路径
            if (!file.exists()) {
                File dir = new File(file.getParent());
                dir.mkdirs();
                file.createNewFile();
            }
            String cmd = MessageFormat.format(EXEC_CMD, phantomJs, jsPath, jsonPath, imagePath);
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            e.printStackTrace();
            logger.error("error: {}", e);
        }
        logger.info("echars file path: {}", imagePath);
        return imagePath;
    }

    /*
     * options:json数据
     * wFileName:文件输出路径
     * 生成phantomjs生成图片的数据文件
     * return .json文件全路径
     */
    public String writeFile(String options, String wFileName) {
        /* option写入文本文件 用于执行命令*/
        String wjsonPath = fileDirectory + wFileName + ".json";
        BufferedWriter out = null;
        try {
            File jsonFile = new File(wjsonPath);
            if (!jsonFile.exists()) {
                File dir = new File(jsonFile.getParent());
                dir.mkdirs();
                jsonFile.createNewFile();
            }
            out = new BufferedWriter(new FileWriter(jsonFile));
            out.write(options);
            out.flush(); // 把缓存区内容压入文件
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null) {
                    out.close(); // 最后关闭文件
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return wjsonPath;
    }


}

2.2.3 application.propertyies
## echars图片生成相关插件路径配置
# phantomJs 执行路径
zifisense.echars.phantomJs=xxx\\phantomjs\\phantomjs-2.1.1-windows\\bin\\phantomjs.exe
# echar生成文件目录后最\\或者是/不可缺少
zifisense.echars.fileDirectory=C:\\Users\\Desktop\\model\\
# echarts-convert.js文件路径
zifisense.echars.jsPath=xxx\\echarts-convert\\echarts-convert.js

3. Thymeleaf 模板生成html,pdf

3.1.1 ConstantConfigurations 相关配置信息

/**
 * Thymeleaf配置参数 参数配置化对象
 */
@Configuration
@ConfigurationProperties(
        prefix = "constant"
)
@Data
public class ConstantConfigurations {

    // html文件生成默认路径
    private String indexStorage;

    // 指定模板解析器模板文件路径前缀,根路径resources
    private String resolverPrefix = "/config/templates/model/";

    // 指定模板解析器模板文件后缀,根路径resources
    private String resolverSuffix = ".html";

}

3.1.2 ThymeleafService 转html pdf


@Service
public class ThymeleafServiceImpl implements ThymeleafService {

    public static final Logger logger = LoggerFactory.getLogger(ThymeleafServiceImpl.class);

    /**
     * sifisense 参数配置化对象
     */
    @Autowired
    private ConstantConfigurations ConstantConfigurations ;

    /**
     * 模板引擎
     */
    @Autowired
    private TemplateEngine templateEngine;


    /**
     * 创建出一个name.html文件
     *
     * @param templateName 模板名称
     * @param name         生成name.xml
     * @param map          模板映射参数
     */
    @Override
    public String createHtml(String templateName, String name, Map<String, Object> map) {
        PrintWriter writer = null;
        try {
            SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
            // 1. 创建模板解析目录解析器
            Set<ITemplateResolver> templateResolvers = templateEngine.getTemplateResolvers();

            // 无配置模板解析路径,则代码配置
            if(!templateResolvers.iterator().hasNext()) {
                // 2. 创建模板解析器 并设置相关属性
                resolver.setPrefix(ConstantConfigurations .getResolverPrefix());
                resolver.setSuffix(ConstantConfigurations .getResolverSuffix());
                // 不允许重复设置 否则会报错
                templateEngine.setTemplateResolver(resolver);
            }
            // 2. 模板上下文 主要存储Model参数
            Context context = new Context();
            if (map.size() > 0) {
                context.setVariables(map);
            }

            // 3. 创建输出文件
            File folder = new File(ConstantConfigurations .getIndexStorage(), name + ".html");

            //如果文件不存在,直接创建
            if (!folder.exists()) {
                folder.createNewFile();
            }

            // 5. 获取输出目标文件输出流
            writer = new PrintWriter(folder, "UTF-8");

            // 6. 生成静态模板参数1:template模板名称  参数2:上下文对象  参数3:目标文件输出流
            templateEngine.process(templateName, context, writer);
            logger.info("http path: {}", folder.getAbsolutePath());
            // 返回生成文件路径
            return folder.getAbsolutePath();
        } catch (IOException e) {
            logger.error("createHtml error {}", ExceptionUtil.getStackTrace(e));
        } finally {
            // flush输出流并关闭
            if (writer != null) {
                writer.flush();
                writer.close();
            }
        }
        return "";
    }


    /**
     * 根据html生成PDF
     *
     * @param html html内容
     * @param file 输出pdf文件的路径
     * @throws DocumentException
     * @throws IOException
     */
    @Override
    public void htmlToPdf(String html, File file) {
        /**
         * 切记 css 要定义在head 里,否则解析失败
         * css 要定义字体
         * 例如宋体style="font-family:SimSun"用simsun.ttc
         */
        if (!file.exists()) {
            try {
                if (file.getParentFile() != null && !file.getParentFile().exists()) {
                    file.getParentFile().mkdirs();
                }
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        logger.info("开始根据html生成pdf,html={}", html);
        OutputStream out = null;
        try {
            out = new FileOutputStream(file);
            ITextRenderer renderer = new ITextRenderer();
            // 携带图片,将图片标签转换为itext自己的图片对象
            renderer.getSharedContext().setReplacedElementFactory(new PdfBase64ImgReplacedElementFactory());
            renderer.getSharedContext().getTextRenderer().setSmoothingThreshold(0);
            // 解决中文支持问题
            ITextFontResolver fontResolver = renderer.getFontResolver();
            // 设置语言包文件  //设置字体,否则不支持中文,在html中使用字体,html{ font-family: SimSun;}
            fontResolver.addFont("config/templates/model/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
            // 如果是本地图片使用 file:,这里指定图片的父级目录。html上写相对路径,
            // renderer.getSharedContext().setBaseURL("file:/E:/img/")
            // 处理图片
            //renderer.getSharedContext().setBaseURL(IMG_SAVE_URL);
            renderer.layout();
            renderer.createPDF(out);
            out.flush();
            logger.info("pdf生成成功");
        } catch (DocumentException e) {
            logger.error("pdf生成失败,cause--->" + e.getMessage());
        } catch (IOException e) {
            logger.error("pdf生成失败,cause--->" + e.getMessage());
        } finally {
            try {
                if (null != out) {
                    out.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }



    @Override
    public void htmlToPdf(String htmlPath, String outPath) {

        /**
         * 切记 css 要定义在head 里,否则解析失败
         * css 要定义字体
         * 例如宋体style="font-family:SimSun"用simsun.ttc
         */
        if (StringUtils.isBlank(outPath)) {
            logger.info("pdf 输出路径为null");
            return;
        }
        String bufferHtml = readHtmlFile(htmlPath);

        File file = new File(outPath);
        if (!file.exists()) {
            try {
                if (file.getParentFile() != null && !file.getParentFile().exists()) {
                    file.getParentFile().mkdirs();
                }
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        logger.info("开始根据html生成pdf,html={}", htmlPath);
        OutputStream out = null;
        try {
            out = new FileOutputStream(file);
            ITextRenderer renderer = new ITextRenderer();
            // 携带图片,将图片标签转换为itext自己的图片对象
            renderer.getSharedContext().setReplacedElementFactory(new PdfBase64ImgReplacedElementFactory());
            renderer.getSharedContext().getTextRenderer().setSmoothingThreshold(0);
            // 解决中文支持问题
            ITextFontResolver fontResolver = renderer.getFontResolver();
            // 设置语言包文件  //设置字体,否则不支持中文,在html中使用字体,html{ font-family: SimSun;}
            fontResolver.addFont("config/templates/model/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
            renderer.setDocumentFromString(bufferHtml);
            // 如果是本地图片使用 file:,这里指定图片的父级目录。html上写相对路径,
            // renderer.getSharedContext().setBaseURL("file:/E:/img/")
            // 处理图片
            //renderer.getSharedContext().setBaseURL(IMG_SAVE_URL);
            renderer.layout();
            renderer.createPDF(out);
            out.flush();
            logger.info("pdf生成成功");
        } catch (DocumentException e) {
            logger.error("pdf生成失败,cause--->" + e.getMessage());
        } catch (IOException e) {
            logger.error("pdf生成失败,cause--->" + e.getMessage());
        } finally {
            try {
                if (null != out) {
                    out.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * 删除id.html
     *
     * @param id
     */
    @Override
    public void deleteHtml(String id) {

    }


    public String readHtmlFile(String htmlPath) {
        File file = new File(htmlPath);
        if (!file.exists()) {
            logger.error("html file is not exists, file path: {}", htmlPath);
            // 文件不存在直接返回
            return "";
        }
        BufferedReader reader = null;
        StringBuffer sbf = new StringBuffer();
        try {
            reader = new BufferedReader(new FileReader(file));
            String tempStr;
            while ((tempStr = reader.readLine()) != null) {
                sbf.append(tempStr);
            }
            reader.close();
            return sbf.toString();
        } catch (IOException e) {
            logger.error("error:", e);
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    logger.error("error:", e);
                }
            }
        }
        return sbf.toString();


    }


}

3.1.3 ImgBase64Util

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;

import javax.xml.bind.DatatypeConverter;

import org.apache.commons.lang.StringUtils;

public class ImgBase64Util {

    public static void main(String[] args) throws Exception {

        //本地图片地址
        String url = "C:/Users/Administrator/Desktop/628947887489084892.jpg";
        //在线图片地址
        String string = "http://bpic.588ku.com//element_origin_min_pic/17/03/03/7bf4480888f35addcf2ce942701c728a.jpg";
/*
        String str = Base64Utils.ImageToBase64ByLocal(url);

        String ste = Base64Utils.ImageToBase64ByOnline(string);

        System.out.println(str);

        Base64Utils.Base64ToImage(str,"C:/Users/Administrator/Desktop/test1.jpg");

        Base64Utils.Base64ToImage(ste, "C:/Users/Administrator/Desktop/test2.jpg");*/
    }

    /**
     * 本地图片转换成base64字符串
     *
     * @param imgFile 图片本地路径
     * @return
     */
    public static String ImageToBase64ByLocal(String imgFile) {// 将图片文件转化为字节数组字符串,并对其进行Base64编码处理


        InputStream in = null;
        byte[] data = null;

        // 读取图片字节数组
        try {
            in = new FileInputStream(imgFile);

            data = new byte[in.available()];
            in.read(data);

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return DatatypeConverter.printBase64Binary(data);
    }


    /**
     * 在线图片转换成base64字符串
     *
     * @param imgURL 图片线上路径
     * @return
     */
    public static String ImageToBase64ByOnline(String imgURL) {
        ByteArrayOutputStream data = new ByteArrayOutputStream();
        InputStream is = null;
        try {
            // 创建URL
            URL url = new URL(imgURL);
            byte[] by = new byte[1024];
            // 创建链接
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(5000);
            is = conn.getInputStream();
            // 将内容读取内存中
            int len = -1;
            while ((len = is.read(by)) != -1) {
                data.write(by, 0, len);
            }
            // 关闭流

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return DatatypeConverter.printBase64Binary(data.toByteArray());
//        // 对字节数组Base64编码
//        BASE64Encoder encoder = new BASE64Encoder();
//        return encoder.encode(data.toByteArray());
    }


    /**
     * base64字符串转换成图片
     *
     * @param imgStr      base64字符串
     * @param imgFilePath 图片存放路径
     * @return
     */
    public static boolean Base64ToImage(String imgStr, String imgFilePath) { // 对字节数组字符串进行Base64解码并生成图片

        if (StringUtils.isEmpty(imgStr)) // 图像数据为空
            return false;

        OutputStream out = null;
        try {

            byte[] b = DatatypeConverter.parseBase64Binary(imgStr);
            for (int i = 0; i < b.length; ++i) {
                if (b[i] < 0) {// 调整异常数据
                    b[i] += 256;
                }
            }

            out = new FileOutputStream(imgFilePath);
            out.write(b);
            out.flush();
            return true;
        } catch (Exception e) {
            return false;
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

}


3.1.4 PdfBase64ImgReplacedElementFactory html转pdf图片处理自定义工厂

public class PdfBase64ImgReplacedElementFactory implements ReplacedElementFactory {

    /**
     * * 实现createReplacedElement 替换html中的Img标签
     * *
     * * @param c 上下文
     * * @param box 盒子
     * * @param uac 回调
     * * @param cssWidth css宽
     * * @param cssHeight css高
     * * @return ReplacedElement
     */
    @Override
    public ReplacedElement createReplacedElement(LayoutContext c, BlockBox box, UserAgentCallback uac, int cssWidth, int cssHeight) {
        // 遍历所有标签
        Element e = box.getElement();
        if (e == null) {
            return null;
        }
        String nodeName = e.getNodeName();
        // 找到所有img标签
        if (nodeName.equals("img")) {
            String attribute = e.getAttribute("src");
            FSImage fsImage;
            try {
                // 生成itext图像
                fsImage = buildImage(attribute, uac);
            } catch (BadElementException e1) {
                fsImage = null;
            } catch (IOException e1) {
                fsImage = null;
            }
            if (fsImage != null) {
                // 对图像进行缩放
                if (cssWidth != -1 || cssHeight != -1) {
                    fsImage.scale(cssWidth, cssHeight);
                }
                return new ITextImageElement(fsImage);
            }
        }
        return null;
    }

    /**
     * 编解码base64并生成itext图像
     */
    protected FSImage buildImage(String srcAttr, UserAgentCallback uac) throws IOException,
            BadElementException {
        FSImage fiImg = null;
        //图片的src要为src="data:image/jpg;base64,{图片的base64code}"这种base64格式
        if (srcAttr.toLowerCase().startsWith("data:image/")) {
            String base64Code = srcAttr.substring(srcAttr.indexOf("base64,") + "base64,".length(), srcAttr.length());
            // 解码
            byte[] decodedBytes = Base64.decode(base64Code);
            fiImg = new ITextFSImage(Image.getInstance(decodedBytes));
        } else {
            fiImg = uac.getImageResource(srcAttr).getImage();
        }
        return fiImg;
    }

    @Override
    public void reset() {
    }

    @Override
    public void remove(Element arg0) {
    }

    @Override
    public void setFormSubmissionListener(FormSubmissionListener arg0) {
    }
}

3.1.7 EcharTemplate.html 模板

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>Spring Boot中使用ECharts</title>


    <style>
         html {
            font-family: SimSun;
        }
        .margin_auto {
            margin: 0 auto;
        }

        .title {
            height: 100px;
            width: 100px;
            text-align: center;
            line-height: 100px;
            margin: 0 auto;
        }

        table.gridtable {
            margin: auto;
            font-family: verdana, arial, sans-serif;
            font-size: 11px;
            color: #333333;
            border-width: 1px;
            border-color: #666666;
            border-collapse: collapse;
        }

        table.gridtable th {
            font-family: SimSun;
            border-width: 1px;
            padding: 8px;
            border-style: solid;
            border-color: #666666;
            background-color: #dedede;
        }

        table.gridtable td {
            font-family: SimSun;
            border-width: 1px;
            padding: 8px;
            border-style: solid;
            border-color: #666666;
            background-color: #ffffff;
        }

    </style>
</head>
<body>


<div>
    <div class="title" th:text="${title}"></div>
    <div class="margin_auto">
        <img th:src="@{${imageUrl}}" style="width: 600px;height:400px;"></img>
    </div>
</div>

<dev style="text-align: center;">

    <table class="gridtable" width='80%' border='1'>

        <tr th:each="td: ${tdList}">
            <td th:text="${td.A}"></td>
            <td th:text="${td.B}"></td>
            <td th:text="${td.C}"></td>
        </tr>
    </table>
</dev>



</body>


</html>

3.1.6 application.properties

## thymeleaf配置
# 开发过程关闭缓存,默认是开启的
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:config/templates/model/
spring.thymeleaf.suffix=.html
spring.thymeleaf.encoding=utf-8
spring.thymeleaf.mode=HTML
spring.thymeleaf.servlet.content-type=text/html
# H5模板生成的路径
constant.indexStorage=C:\\Users\\Desktop\\model

4. 测试接口

  1. TestController1
@RestController
@RequestMapping("/test1")
public class TestController1 {

    @Autowired
    private EchartsFileService echartsFileService;
    @Autowired
    private ThymeleafService thymeleafService;

    @RequestMapping("/create")
    public String test() {
        Map<String, Object> map = loadModel();
        String htmlPath = thymeleafService.createHtml("EcharTemplate", "pdf", map);
        // 生成pdf
        thymeleafService.htmlToPdf(htmlPath, map.get("pdf_path").toString());
        return map.get("pdf_path").toString();
    }

    /**
     * 模拟测试数据
     *
     * @return
     */
    public Map<String, Object> loadModel() {

        String options = "{\n" +
                "    tooltip: {\n" +
                "        trigger: 'item'\n" +
                "    },\n" +
                "    legend: {\n" +
                "        top: '5%',\n" +
                "        left: 'center'\n" +
                "    },\n" +
                "    series: [\n" +
                "        {\n" +
                "            name: '访问来源',\n" +
                "            type: 'pie',\n" +
                "            radius: ['40%', '70%'],\n" +
                "            avoidLabelOverlap: false,\n" +
                "            itemStyle: {\n" +
                "                borderRadius: 10,\n" +
                "                borderColor: '#fff',\n" +
                "                borderWidth: 2\n" +
                "            },\n" +
                "            label: {\n" +
                "                show: false,\n" +
                "                position: 'center'\n" +
                "            },\n" +
                "            emphasis: {\n" +
                "                label: {\n" +
                "                    show: true,\n" +
                "                    fontSize: '40',\n" +
                "                    fontWeight: 'bold'\n" +
                "                }\n" +
                "            },\n" +
                "            labelLine: {\n" +
                "                show: false\n" +
                "            },\n" +
                "            data: [\n" +
                "                {value: 1048, name: '搜索引擎'},\n" +
                "                {value: 735, name: '直接访问'},\n" +
                "                {value: 580, name: '邮件营销'},\n" +
                "                {value: 484, name: '联盟广告'},\n" +
                "                {value: 300, name: '视频广告'}\n" +
                "            ]\n" +
                "        }\n" +
                "    ]\n" +
                "}";
        // 创建图片
        String imagePath = echartsFileService.creatEchartsFile(options);
        while(true){
            File file = new File(imagePath);
            if(file.exists()) {
                break;
            }
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
            }
        }
        List<TdBean> tbList = new ArrayList<>();
        int listSize = 1000;
        for(int i = 0 ; i < listSize; i++) {
            TdBean tb = new TdBean();
            tb.setA("A" + i);
            tb.setB("B" + i);
            tb.setC("C" + i);
            tbList.add(tb);
        }
        Map<String, Object> map = new HashMap<>();
        map.put("tdList", tbList);
        map.put("name", "小朋友");
        map.put("age", 18);
        map.put("email", "xiaopengyou@qq.com");
        // 需要将图片转base64
        map.put("imageUrl", "data:image/png;base64," + ImgBase64Util.ImageToBase64ByLocal(imagePath));
        map.put("pdf_path", imagePath.substring(0, imagePath.lastIndexOf(".")) + ".pdf");
        return map;
    }

}
  1. 结果
    在这里插入图片描述

5 补充

在linux 或是 ubuntu 中文乱码问题解决

在centos中执行:yum install bitmap-fonts bitmap-fonts-cjk

在ubuntu中执行:sudo apt-get install xfonts-wqy
  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值