FreeMarker

FreeMarker

前言

freeMarker在线手册

freeMarker是用来干啥的?

FreeMarker 是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。

if标签

目标变量后面连续两个??判断对象是否为存在(null)

<#if target??>
    xxxx
<# else>
    yyyy
</#if>

判断相等和不等(数字和字符串的判断是一样的)

<#if target == "xxx">xxx</#if>
<#if target !== "xxx">xxx</#if>>

判断集合的长度

<!--必须括起来,否则会报错-->
<#if (fields?size>0) ></#if>
<!--或者-->
<#if fields?size gt 0 ></#if>

使用if判断后端传参和freeMarker中assign定义的变量

<#assign name="张三">
<#if  user.name == name></#if>
<!--错误的写法-->
<#if  user.name == ${name}></#if>

list标签

如何遍历list?如何获取list的索引值?

通过_index就可以获取的索引值,通过.xxx就可以获取lists中元素的xxx属性值。

<#list lists as res>
    <tr>
    	<td>${res_index}</td>
        <td>${res.xxx}</td>
    </tr>	
</#list>

如何获取集合的长度?

${fields?size}

如何遍历map类型的集合?

<#list stuMap?keys as k>
        <tr>
            <#--_index:得到循环的下标,使用方法是在stu后边加"_index",它的值是从0开始-->
            <td>${k_index + 1}</td>
            <td>${stuMap[k].name}</td>
            <td>${stuMap[k].age}</td>
            <td >${stuMap[k].money}</td>
        </tr>
 </#list>

如何跳出循环?

<#list table.columns as c>
    <#if c.isPK>
         <#break>
    </#if>
</#list>

function标签

定义一个方法,让数据表里两位小数

<#setting number_format="computer">
<#function numFormat t>
    <#if t==0>
        <#return "0.00">
    <#else>
        <#return t?string('0.00')>
    </#if>
</#function>
<!-- 调用方法-->
${numFormat(user.salary)}

巧用?

<!--以时间的格式展示 15:23:05-->
${nowDate?time}
<!--以时期的格式展示 2011-4-28(date的格式能够在freemarker.properties文件里配置)-->
${nowDate?date}
<!--获取当前时间-->
${.now?string('yyyy-MM')} 
<!--freemarker数字当超过4为就会用逗号分隔,使用?c可以去除逗号【1,111---1111】-->
${user.salary?c}
<!--保留两位小数,不足自动补0【string是小写的】-->
${num?string('0.00')}
<!--判断字符串是否包含-->
"a,b,c,"?contains("a")  

巧用!

<!--如果为空就赋值为x-->
${num!"x"}

示例一:xhtmlrenderer

xhtmlrenderer工具链接:百度网盘 请输入提取码 提取码:s8yz

该案例展示了在SpringBoot中使用freeMarker模板和xhtmlrenderer生成pdf报告

maven

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
    <version>2.2.7.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.xhtmlrenderer</groupId>
    <artifactId>flying-saucer-pdf</artifactId>
    <version>9.1.12</version>
</dependency>

application.yml配置

#静态pdf配置
pdf:
  template:
    #根路径
    rootPath: /usr/local/freeMarker/upload/static
    #存放pdf的路径
    pdfPath: /pdfFile/html/
    #linux下存放wkhtmltopdf软件的路径
    linuxCmdPath: /usr/local/bin/wkhtmltopdf
    author: 懒鑫人

配置类

写一个配置类用于读取yml文件的配置

@Data
@Configuration
@ConfigurationProperties(prefix="pdf.template")
@PropertySource("classpath:application.yml")
public class PdfTemplateConfig {
	private String rootPath;
	private String pdfPath;
	private String linuxCmdPath;
	private String author;
}

写一个配置类用来创建线程池用于后续的多线程任务

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Configuration
public class ThreadPoolCreate {

	@Bean
	public ThreadPoolExecutor createExecutor() {
		
		ThreadPoolExecutor pool= new ThreadPoolExecutor(2, 6, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(4));
		pool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy  ());
		return pool;
	}
}

实体类

生成的html和pdf模板我们可以将它的信息存储到数据库,这样第二次下载的时候通过数据库就能找到模板路径。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PdfMessage {
    private Integer id;
    private String pdfName;
    private String url;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    Integer id;
    String userName;
    Integer age;
    Integer sex;
    String createTime;
}

mapper

此处省略mapper.xml

@Mapper
@Repository
public interface FreeMarkerMapper {
    //根据id查询pdf信息
    PdfMessage getPdfMessage(Integer pdfId);
    //保存pdf信息
    int savePdfMessage(PdfMessage pdfMessage);
}

service

public interface FreeMarkerService {
    PdfMessage getPdfMessage(Integer pdfId);
}
  1. 查询数据库中是否已有pdf模板的信息

  2. 如果没有或者服务器中模板已经不存在就重新下载模板

  3. 我们这里使用了线程池来执行任务,提高效率

import javax.annotation.Resource;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;

@Service
@Slf4j
public class FreeMarkerServiceImpl implements FreeMarkerService {
    @Resource
    FreeMarkerMapper freeMarkerMapper;
    @Resource
    private ThreadPoolCreate poolCreate;
    @Resource
    private PdfTemplateConfig pdfConfig;

    /**返回一个pdf模板的信息*/
    @Override
    public PdfMessage getPdfMessage(Integer pdfId) {
        PdfMessage pdfMessage=freeMarkerMapper.getPdfMessage(pdfId);
        if (pdfMessage==null||!new File(pdfMessage.getUrl()).exists()){
            pdfMessage=downTemplate(pdfId);
            //将pdf信息存入数据库
            freeMarkerMapper.savePdfMessage(pdfMessage);
        }
       return pdfMessage;
    }

    /**生成pdf和html模板*/
    public PdfMessage downTemplate(Integer pdfId) {
        PdfMessage pdfMessage=new PdfMessage();
        pdfMessage.setPdfName("Freemarker案例"+new Date().getTime());

        //我们通过线程池来执行任务:这样可以提升效率
        ThreadPoolExecutor pool = poolCreate.createExecutor();
        //定义一个map来存储模板需要的数据 ConcurrentHashMap是线程安全的
        Map<String, Object> map = new ConcurrentHashMap();
        map.put("author",pdfConfig.getAuthor());
        //CountDownLatch允许一个或者多个线程去等待其他线程完成操作。接收一个int型参数,表示要等待的工作线程的个数。
        CountDownLatch latch = new CountDownLatch(1);
        PdfTask pdfTask=new PdfTask(map,latch);
        pool.execute(pdfTask);

        try {
            latch.await();
        } catch (Exception e) {
            log.error("企业信息线程池查询错误|" + e.getMessage());
        }

        //生成html
        String html = getHtml(map, pdfId.toString());

        //生成pdf
        String pdfPath = null;
        if (html.equals("")) {
            log.error("静态化企业模板出错!");
        } else {
            pdfPath = createPdf(html, pdfId.toString());
            pdfMessage.setUrl(pdfPath);
        }

        return pdfMessage;
    }

    /**生成静态的html*/
    public String getHtml(Map<String, Object> map, String taxId) {
        String charSet = "UTF-8";
        //html模板保存路径
        String htmlRootPath = pdfConfig.getRootPath() + pdfConfig.getPdfPath();
        //html生成时间
        String pdfDate = '/' + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
        map.put("pdfDate", pdfDate.substring(1));
        //根据月份生成保存html的目录
        File packagePath = new File(htmlRootPath + pdfDate + '/');
        if (!packagePath.exists()) {
            packagePath.mkdirs();
        }
        //Configuration负责模版(Template)实例的创建以及缓存
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
        //模板目录  你想使用ClassLoader的方式来加载模版的时候,你就可以使用这种方式,这种方式将会调用来寻找模版文件
        cfg.setClassForTemplateLoading(this.getClass(), "/templates");
        cfg.setDefaultEncoding(charSet);
        String newsHtmlPath = null;
        try {
            //获得模板
            Template newsTemplate = cfg.getTemplate("pdfTemplate.ftl");
            //生成的内容
            newsHtmlPath = packagePath.getAbsolutePath() + '/' + taxId.trim() + ".html";
            File htmlFile = new File(newsHtmlPath);
            if (!htmlFile.exists()) {
                boolean flag = htmlFile.createNewFile();
                if (!flag)
                    log.info("模板生成失败!");
            }
            Writer wr = new OutputStreamWriter(new FileOutputStream(htmlFile), charSet);
            newsTemplate.process(map, wr);
            wr.flush();
            wr.close();

        } catch (Exception e) {
            log.error("模板静态化出错", e);
        }
        return newsHtmlPath;
    }

    /**生成静态的pdf*/
    public String createPdf(String htmlPath, String leftFooter) {
        final String htmlStr = ".html";
        final String pdfStr = ".pdf";
        StringBuilder cmdStr = new StringBuilder();
        cmdStr.append("--enable-local-file-access").append(" ")
                .append("--footer-left " + leftFooter).append(" ")
                .append("--footer-right 第[page]页/共[topage]页 --footer-font-size 8").append(" ")
                .append("--footer-line --footer-spacing 2 --page-size A4").append(" ")
                .append(htmlPath).append(" ")
                .append(htmlPath.replace(htmlStr, pdfStr));
        Runtime runtime = Runtime.getRuntime();
        String osName = System.getProperty("os.name").toLowerCase();
        Process process = null;
        String pdfPath = null;
        String cmd = null;
        try {
            if (osName.indexOf("windows") > -1) {
                cmd = "E:\\wkhtmltopdf\\bin\\wkhtmltopdf.exe " + cmdStr.toString();
            } else {
                cmd = pdfConfig.getLinuxCmdPath() + " " + cmdStr.toString();
            }

            process = runtime.exec(cmd);
            ProcessInputThread errInput = new ProcessInputThread(process.getErrorStream(), "err");
            ProcessInputThread infoInput = new ProcessInputThread(process.getInputStream(), "info");
            errInput.start();
            infoInput.start();
            process.waitFor();
            pdfPath = htmlPath.replace(htmlStr, pdfStr);

        } catch (Exception e) {
            log.error(leftFooter + "--企业报告PDF转化出错", e);
        }

        return pdfPath;

    }

    /**pdf下载进程*/
    @Data
    @EqualsAndHashCode(callSuper = true)
    @AllArgsConstructor
    class ProcessInputThread extends Thread {
        InputStream is;
        String type;

        @Override
        public void run() {
            try (InputStreamReader inRead = new InputStreamReader(getIs(), StandardCharsets.UTF_8);
                 BufferedReader bfread = new BufferedReader(inRead)) {

                String len = null;

                if ("info".equals(getType())) {
                    while ((len = bfread.readLine()) != null)
                        log.info(len);
                } else {
                    while ((len = bfread.readLine()) != null)
                        log.warn(len);
                }

            } catch (Exception e) {
                log.error("PDF转化进程中死锁错误" + e.getMessage());
            }
        }
    }

    @Data
    @AllArgsConstructor
    static class PdfTask implements Runnable, Serializable{
        private static final long serialVersionUID = 112L;
        private transient Map<String, Object> map;
        private transient CountDownLatch latch;

        @Override
        public void run() {
            List<User>  users=new ArrayList<>();
            User user=new User();
            for (int i = 0; i < 4; i++) {
                user.setId(i+1);
                user.setAge(i+18);
                user.setUserName("测试"+i);
                users.add(user);
            }
            map.put("pdfTask",users);
            latch.countDown();
        }
    }
}

resource/templates/pdfTemplate.ftl

  1. 模板数据来源于service中提供的map

  2. 模板还演示了在模板中如何使用echarts图表

  3. 因为在yml中配置了根路径,所以echars需要依赖和图片我们要存放到

    • usr\local\freeMarker\upload\static\pdfFile\html\static\js\echarts.min.js

    • usr\local\freeMarker\upload\static\pdfFile\html\static\img\custom-gauge-panel.png

    • 图片和依赖百度云链接

    • 提取码:o4b8

<!DOCTYPE html>
<html>
<head lang="en">
    <title>freeMarker模板</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <style>
        #container {
            font-family: "microsoft yahei", serif;
            width: 800px;
            margin: 0 auto;
            text-align: center;
            font-size: 14px;
        }
        p {
            font-size: 20px;
            font-weight: bold;
        }
        table {
            width: 800px;
            border-collapse: collapse;
            text-align: left;
            margin-bottom: 20px;
        }
        table caption{
            text-align: left;
            font-size: 16px;
            padding-bottom: 2px;
            font-weight: bold;
        }
        table tr {
            width: 100%;
            padding: 5px 0;
            page-break-before: always;
            page-break-after: always;
            page-break-inside: avoid;
        }
        table td {
            border: 1px black solid;
        }
        table .title_table_tip {
            width: 120px;
            background-color: rgba(200, 205, 210, 0.52);
            text-align: center;
        }
        .user_message,
        .echarts_img
        {
            page-break-inside: avoid;
            page-break-before: left;
        }
        .first_page_center {
            width: 380px;
            text-align: left;
            font-size: 20px;
            margin: 0 auto 360px;
        }
        .first_page_center div {
            margin-bottom: 20px;
        }
    </style>
    <script type="text/javascript" src="../static/js/echarts.min.js"></script>
</head>
<body>
<div id="container">
    <#--第一部分:报告封面页-->
    <div class="page_header" style=" text-align: center">
        <div style="height: 300px;"></div>
        <p style="margin-bottom: 140px;font-size: 48px;">FreeMarker模板的使用</p>
        <div class="first_page_center">
            <div>
                <span>评估日期:</span>
                <span>${pdfDate}</span>
            </div>
            <div>
                <span>作者:</span>
                <span>${author}</span>
            </div>
        </div>
    </div>
    <#--第二部分: 用户信息-->
    <div class="user_message">
        <p>一、用户信息</p>
        <table>
            <caption>基本信息</caption>
            <tr>
                <td class="title_table_tip">编号</td>
                <td class="title_table_tip">用户名</td>
                <td class="title_table_tip">年龄</td>
            </tr>
            <#list pdfTask as pt>
                <tr>
                    <td class="collapse_td">${pt.id}</td>
                    <td class="collapse_td">${pt.userName}</td>
                    <td class="collapse_td">${pt.age}</td>
                </tr>
            </#list>
        </table>
    </div>
    <#--第三部分:echart图-->
    <div class="echarts_img">
        <div id="echarts_img_demo" style="height: 200px;width: 800px"></div>
        <script type="text/javascript">
            //初始化图案
            var myChart =  echarts.init(document.getElementById('echarts_img_demo'), null, {
                renderer: 'canvas',
                useDirtyRect: false
            });
            var _panelImageURL = '../static/img/custom-gauge-panel.png';
            var app = {};
            var option;
            //echart底层通过_valOnRadianMax/valOnRadian计算环形图的占比
            var _valOnRadianMax = 100;
            //设置外部圆直径
            var _outerRadius = 80;
            //设置中间圆直径
            var _innerRadius = 70;
            var _pointerInnerRadius = 40;
            //设置内部圆直径
            var _insidePanelRadius = 60;
            var _currentDataIndex = 0;
            function renderItem(params, api) {
                var valOnRadian = 70;
                var coords = api.coord([api.value(0), valOnRadian]);
                var polarEndRadian = coords[3];
                var imageStyle = {
                    image: _panelImageURL,
                    x: params.coordSys.cx - _outerRadius,
                    y: params.coordSys.cy - _outerRadius,
                    width: _outerRadius * 2,
                    height: _outerRadius * 2
                };
                return {
                    type: 'group',
                    children: [
                        {
                            type: 'image',
                            style: imageStyle,
                            clipPath: {
                                type: 'sector',
                                shape: {
                                    cx: params.coordSys.cx,
                                    cy: params.coordSys.cy,
                                    r: _outerRadius,
                                    r0: _innerRadius,
                                    startAngle: 0,
                                    endAngle: -polarEndRadian,
                                    transition: 'endAngle',
                                    enterFrom: {endAngle: 0}
                                }
                            }
                        },
                        {
                            type: 'image',
                            style: imageStyle,
                            clipPath: {
                                type: 'polygon',
                                shape: {
                                    points: makePionterPoints(params, polarEndRadian)
                                },
                                extra: {
                                    polarEndRadian: polarEndRadian,
                                    transition: 'polarEndRadian',
                                    enterFrom: {polarEndRadian: 0}
                                },
                                during: function (apiDuring) {
                                    apiDuring.setShape(
                                        'points',
                                        makePionterPoints(params, apiDuring.getExtra('polarEndRadian'))
                                    );
                                }
                            }
                        },
                        {
                            type: 'circle',
                            shape: {
                                cx: params.coordSys.cx,
                                cy: params.coordSys.cy,
                                r: _insidePanelRadius
                            },
                            style: {
                                fill: '#fff',
                                shadowBlur: 25,
                                shadowOffsetX: 0,
                                shadowOffsetY: 0,
                                shadowColor: 'rgba(76,107,167,0.4)'
                            }
                        },
                        {
                            type: 'text',
                            style: {
                                text: makeText(70),
                                fontSize: 16,
                                fontWeight: 700,
                                x: params.coordSys.cx,
                                y: params.coordSys.cy,
                                fill: 'rgb(0,50,190)',
                                align: 'center',
                                verticalAlign: 'middle',
                                enterFrom: {opacity: 0}
                            }
                        }
                    ]
                };
            }
            function convertToPolarPoint(renderItemParams, radius, radian) {
                return [
                    Math.cos(radian) * radius + renderItemParams.coordSys.cx,
                    -Math.sin(radian) * radius + renderItemParams.coordSys.cy
                ];
            }
            function makePionterPoints(renderItemParams, polarEndRadian) {
                return [
                    convertToPolarPoint(renderItemParams, _outerRadius, polarEndRadian),
                    convertToPolarPoint(
                        renderItemParams,
                        _outerRadius,
                        polarEndRadian + Math.PI * 0.03
                    ),
                    convertToPolarPoint(renderItemParams, _pointerInnerRadius, polarEndRadian)
                ];
            }
            function makeText(valOnRadian) {
                return "" + valOnRadian;
            }
            option = {
                animation:false,
                dataset: {
                    source: [[1, 190]]
                },
                tooltip: {
                    //取消鼠标悬浮时的提示框
                    show: false
                },
                angleAxis: {
                    type: 'value',
                    startAngle: 0,
                    show: false,
                    min: 0,
                    max: _valOnRadianMax
                },
                radiusAxis: {
                    type: 'value',
                    show: false
                },
                polar: {},
                series: [
                    {
                        type: 'custom',
                        radius: '50%',
                        coordinateSystem: 'polar',
                        renderItem: renderItem,
                    }
                ],
            };
            if (option && typeof option === 'object') {
                myChart.setOption(option);
            }
        </script>
    </div>
</div>
</body>
</html>

controller类

  1. 拿到服务器中pdf模板报告的地址,根据模板下载报告

  2. 下载的报告加上水印

@RestController
@RequestMapping("/free")
@Slf4j
public class FreeMarKerController {
    @Resource
    private PdfTemplateConfig pdfConfig;
    @Resource
    FreeMarkerService freeMarkerService;

    /**
     * 根据url地址下载pdf文件
     */
    @GetMapping("/downPdf/{pdfId}" )
    public void downPdf(@PathVariable(required = true) Integer pdfId, HttpServletResponse response) {
        /**
         * 1.为了效率更高:我们采用的是先提前将pdf生成好上传到服务器。
         * 2.然后我们再通过上传到服务器的地址来下载文件
         * 真实开发中我们可以通过定时任务将资源生成下载上传到服务器,然后通过id查找到对应的pdf路径进行下载
         */
        PdfMessage pdfMessage = freeMarkerService.getPdfMessage(pdfId);

        //将作者名作为水印
        String watermark =pdfConfig.getAuthor();
        //获取到生成pdf模板的路径
        String pdfPath = pdfMessage.getUrl();
        //判断当前程序是在什么服务器上运行  linux/windows
        String osName = System.getProperty("os.name").toLowerCase();
        String fileName;
        //根据不同的服务器来制定文件名的命名方式
        if (osName.contains("windows")) {
            fileName = pdfPath.substring(pdfPath.lastIndexOf('\\'));
        } else {
            fileName = pdfPath.substring(pdfPath.lastIndexOf('/'));
        }

        //通过 PdfReader 读取pdf 文件
        PdfReader read = null;
        //将内容添加到PDF文件
        PdfStamper stamper = null;
        try {
            response.setHeader("Content-Disposition", "attachment;fileName=\"" + URLEncoder.encode(fileName, "utf-8") + "\"");
            response.addHeader("Content-Type", "application/pdf;charset=UTF-8");
            read = new PdfReader(pdfPath);
            stamper = new PdfStamper(read, response.getOutputStream());
            int pageTotal = read.getNumberOfPages();
            //PdfContentByte可以进行文本的绝对定位和字体的加粗、导入图片、画指定位置线条、创建下一页。
            PdfContentByte content;
            //设置字体
            BaseFont base = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.EMBEDDED);
            //添加水印
            PdfGState gs = new PdfGState();
            for (int i = 1; i <= pageTotal; i++) {
                //内容下方添加水印
                content = stamper.getUnderContent(i);
                //设置透明度
                gs.setFillOpacity(0.5f);
                //加载文本
                content.beginText();
                content.setColorFill(BaseColor.LIGHT_GRAY);
                content.setFontAndSize(base, 30);
                //设置文本绝对坐标
                content.setTextMatrix(70, 200);
                //添加文字水印
                content.showTextAligned(Element.ALIGN_CENTER, watermark, 300, 150, 55);
                //接收设置
                content.endText();
            }
        } catch (DocumentException | IOException e) {
            log.error("导出PDF出错,请联系管理员", e);
        } finally {
            try {
                if (stamper != null) {
                    stamper.close();
                }
                if (read != null) {
                    read.close();
                }
            } catch (DocumentException | IOException e) {
                log.error("导出PDF数据流关闭出错", e);
            }
        }
    }
}

注意事项

  1. 如果我们是使用wkhtmltopdf生成pdf的话,freeMarker中就不能够使用ES6语法(如let关键子和Map集合等),否则会报错。

  2. 我们在freeMarker中使用echars的时候,最好关闭动态效果,不然可能会因为加载延迟导致图片加载不出来。

    option={
        animation:false,//echarts图标关闭动态效果
    }
  3. 我们可以通过 cmd wkhtmltopdf\bin 回车 ,然后通过下面命令来看我们的html模板能否成功转换成pdf【将1.html转换成1.pdf】

    wkhtmltopdf\bin>wkhtmltopdf.exe 1.html 1.pdf
    Loading pages (1/6)
    Warning: Failed to load https://raw.githubusercontent.com/apache/echarts-website/asf-site/examples/data/asset/img/custom-gauge-panel.png (ignore)
    Counting pages (2/6)
    Resolving links (4/6)
    Loading headers and footers (5/6)
    Printing pages (6/6)
    Done
  4. 在将模板转换为pdf的时候,freeMarker中的容器必须要有宽和高,否则html模板能够显示出来内容,pdf显示不出来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值