Freemarker常用基本命令

freemarker包括下面几个基本命令

if,else,elseif指令
switch,case,default,break指令
list,break指令
include指令
import 指令
noparse指令
compress指令
escape,noescape指令
assign 指令
global 指令
local 指令
setting 指令
用户自定义指令(<@...>)
macro,nested,return 指令
function,return 指令
flush 指令
stop 指令
ftl 指令
t,lt,rt 指令
nt 指令
attempt,recover 指令
visit,recurse,fallback 指令

常用技巧之一

1 截取字符串
有的时候我们在页面中不需要显示那么长的字符串,比如新闻标题,这样用下面的例子就可以自定义显示的长度

<#if title.content?length lt 8>
           <a href>${title.content?default("")}</a>
      <#else>
           <a href title="${title.content}">${title.content[0..3]?default("")}...</a>
</#if>

意思就是如果这个字符串的长度小于8,那么就正常显示,反之则取4位
注意:常用的比较运算符

=(==):判断两个值是否相等
!=:不相等
>(gt):判断左边是否大于右边
>=(gte):
<(lt):
<=(lte):

2 连接字符串

${"Hello," + user + "!"} //输出结果为:hello,swiftlet.net!

3 日期格式和boolean类型,转化为string类型

${lastUpdate?string("yyyy-MM-dd HH:mm:ss")}      
输出结果如下:
2003-04-08 21:24:44
<#assign foo=true/>
${foo?string("yes","no")} //输出结果:yes

4 排序

升序用sort_by()
<#list list?sort_by("字段") as x>
</#list>
降序用sort_by()?reverse
<#list list?sort_by("字段")?reverse as x>
</#list>

5 去空格
${xx?trim}
6 数值精度控制

    mX:小数部分最小X位。
    MX:小数部分最大X位。
    <#assign x=2.582/>
    <#assign y=4/>
    #{x; M2}//2.58
    #{y; M2}//4
    #{x; m1M2}//2.58
    #{y; m1M2}//4.0

7 内置函数

html:字符串中所有的特殊HTML字符都需要用实体引用来代替(比如<代替&lt;)
cap_first:字符串的第一个字母变为大写形式
lower_case:字符串的小写形式
upper_case:字符串的大写形式
trim:去掉字符串首尾的空格
序列使用的内建函数:
size:序列中元素的个数
数字使用的内建函数:
int:数字的整数部分(比如-1.9?int就是-1)9>.空值运算符
length:字符串的长度
string :把其他格式的数据,转化为string类型
${test?html}
${test?upper_case?html}
假设字符串test存储”Tom & Jerry”,那么输出为:
Tom &amp; Jerry
TOM &amp; JERRY
${seasons?size}
${seasons[1]?cap_first}
${"horse"?cap_first}

假设seasons存储了序列"winter", “spring”, “summer”, “autumn”,那么上面的输出将会是:
4
Spring
Horse

8 顶层变量
所谓顶层变量就是直接放在数据模型中的值。
Map root = new HashMap();
root.put(“name”,“admin”);//name是一个顶层对象
对于顶层变量,直接使用${variableName}来输出变量值

9 集合连接运算符
集合连接运算是将两个集合连接成一个新的集合,连接集合的运算符是’+’.
<#list [“一”,“二”,“三”] + [“四”,“五”,“六”] as x>
${x}
</#list>
//输出结果如下:
一二三四五六
10 算术运算符

取整运算
<#assign x=5>
${(x/2)?>int}//2
${1.1?int}//1
${1.999?int} //1
${-1.1?int}//-1

常用技巧之二

1 list、break指令
<#list sequence as item>

</#list>
tem_index:当前变量的索引值.
item_has_next:是否存在下一个对象.
<#list [“星期一”,“星期二”,“星期三”,“星期四”,“星期五”,“星期六”] as x>
x i n d e x + 1 . {x_index + 1}. xindex+1.{x}
<#if x_has_next>,</#if>
<#if x=“星期四”><#break></#if>
</#list>
输出结果:
1.星期一,
2.星期二,
3.星期三,
4.兴趣四,

2 import指令
<#import path as mapObject>
path:指定要被导入的模板文件.
mapObject:是一个Map对象.
意思:将path路径中的变量都放在mapObject中.
例子:<#import “/lib/common.ftl” as com>

3 宏的基本用法
例如:

<#macro greet>
<font size="+2"> Hello JOE!</font>
</#macro>
使用时:
<@greet></@greet>
如果没有体内容也可以用
<@greet />
可以在宏定义之后定义参数,宏参数是局部变量,只在宏定义中有效。如:
<#macro greet person>
<font size="+2"> Hello ${person}!</font>
</#macro>
使用时:
<@greet person="emma"> and <@greet person="LEO">
输出为:
<font size="+2"> Hello emma!</font>
<font size="+2"> Hello LEO!</font>

注意:宏的参数是FTL表达式,所以,person=emma和上面的例子中具有不同的意义,这意味着将变量emma的值传给person,这个值可能是任意一种数据类型,甚至是一个复杂的表达式。
宏可以有多个参数,使用时参数的次序是无关的,但是只能使用宏中定义的参数,并且对所有参数赋值。如:
<#macro greet person color>
Hello ${person}!
</#macro>
使用时:
<@greet color=“black” person=“emma” />正确
<@greet person=“emma” />错误,color没有赋值,此时,如果在定义宏时为color定义缺省值<#macro greet person color=“black”>这样的话,这个使用方法就是正确的。

FreeMarker缓存处理
上周我面试了一个人,无意中我问了一个问题:freemarker加载模板文件的缓存策略是什么呢?很遗憾,面试者没有回答出来。后来,我告诉面试者,学习一个框架,不仅仅是要明白这个框架如何使用,还要了解一下框架的底层实现。本文主要是给大家说一下FreeMarker的缓存实现部分。本文的内容大部分来自于互联网,要想更深入的了解与掌握
FreeMarker缓存处理,我个人觉得还是要深入研读源码,然后再看看网上的分析以加深印象,这样的学习效果是最好的。
FreeMarker 的缓存处理主要用于模版文件的缓存,一般来讲,模版文件改动不会很频繁,在一个流量非常大的网站中,如果频繁的读取模版文件对系统的负担还是很重的,因此 FreeMarker 通过将模版文件的内容进行缓存,来降低模版文件读取的频次,降低系统的负载。
当处理某个模版时,FreeMarker直接从缓存中返回对应的 Template 对象,并有一个默认的机制来保证该模版对象是跟模版文件同步的。如果使用的时候 FreemarkerServlet 时,有一个配置项template_update_delay用来指定更新模版文件的间隔时间,相当于多长时间检测一下是否有必要重新加载模版文件,0 表示每次都重新加载,否则为多少毫秒钟检测一下模版是否更改。
FreeMarker定义了一个统一的缓存处理接口CacheStorage,默认的实现是 MruCacheStorage 最近最少使用的缓存策略。一般情况下,很少需要对缓存进行扩展处理。您可以通过下面的代码指定最大缓存的模版数:

1 cfg.setCacheStorage(new freemarker.cache.MruCacheStorage(20, 250))
其中第一个参数是最大的强引用对象数,第二个为最大的弱引用对象数。这两个值FreeMarker默认的是0和 Integer.MAX_VALUE,表明模版缓存数是无限的。

freemarker空值的处理
FreeMarker的变量必须赋值,否则就会抛出异常。而对于FreeMarker来说,null值和不存在的变量是完全一样的,因为FreeMarker无法理解null值。FreeMarker提供两个运算符来避免空值:
(1)!运算符:指定缺失变量的默认值;
(2)??运算符:判断变量是否存在。
!运算符有两种用法:variable!或variable!defaultValue。第一种用法不给变量指定默认值,表明默认值是空字符串、长度为0的集合、或长度为0的Map对象。
注意:使用!运算符指定默认值并不要求默认值的类型和变量类型相同。下面是一个小例子:
<#-- ${sss}没有定义这个变量,会报异常!–>
${sss!} <#–没有定义这个变量,默认值是空字符串!–>
${sss!“abc”} <#–没有定义这个变量,默认值是字符串abc!–>
??运算符返回布尔值,如:variable??,如果变量存在,返回true,否则返回false。一般情况下与if指令共同使用。将它和if指令合并,如下面的例子:如果user变量不存在的话将会忽略整个问候代码段:
<#if user??>

Welcome ${user}!

</#if>
关于多级访问的变量,比如animals.python.price,书写代码:animals.python.price!0,仅当animals.python存在而仅仅最后一个子变量price可能不存在(这种情况下我们假设价格是0)。如果animals或者python不存在,那么模板处理过程将会以“未定义的变量”错误而停止。为了防止这种情况的发生,可以这样来书写代码(animals.python.price)!0。这种情况下当animals或python不存在时表达式的结果仍然是0。对于??也是同样用来的处理这种逻辑的:animals.python.price??对比(animals.python.price)??来看。

freemarker数据类型
freemarker的数据类型主要包括下面几类:
字符串类型
定义字符串可以使用双引号和单引号,例如:
<#assign temp = “some text” />
或者
<#assign temp = ‘some text’ />
这两种形式是相等的。字符串中可以使用转义字符""。如果字符串内有大量的特殊字符,则可以在引号的前面加上一个字母r,则字符串内的所有字符都将直接输出。例如:“It’s “quoted”” 或者 r"C:\raw\string"
数字类型
输入不带引号的数字就可以直接指定一个数字,必须使用点作为小数的分隔符而不能是其他的分组分隔符。可以使用-或+来表明符号(+是多余的)。科学记数法暂不支持使用(1E3就是错误的),而且也不能在小数点之前不写0(.5也是错误的)。
哈希表类型
键和值成对出现并以冒号分隔,最外面使用花括号。看这个例子:
<#assign temp = {“name”:“green mouse”, “price”:150} />
注意到名字和值都是表达式,但是用来检索的名字就必须是字符串类型的。
序列类型
指定一个序列,使用逗号来分隔其中的每个子变量,然后把整个列表放到方括号中。例如:
<#assign nums=[1,2,3,4,5,77,8,99]/>
使用list指令将序列输出,如下所示:
<#list nums as num>
${num}
</#list>
还可以采用数字范围定义了一个连续的序列,例如:
<#assign nums=12…99/>
这种方式定义的序列的内容是12到99。总之,使用数字范围也可以表示一个数字集合,如1…5等同于集合[1,2, 3, 4, 5];同样也可以用5…1来表示[5, 4, 3, 2, 1]。
时间类型
FreeMarker支持date、time、datetime三种类型,这三种类型的值无法直接指定,通常需要借助字符串的date、time、datetime三个内建函数进行转换才可以:
<#assign test1 = “2009-01-22”?date(“yyyy-MM-dd”) />;
<#assign test2 =“16:34:43”?time(“HH:mm:ss”) />
<#assign test2 = “2009-01-22 17:23:45”?datetime(“yyyy-MM-dd HH:mm:ss”) />
布尔类型
直接使用true或false,不使用引号。例如:<#assign temp = true />

freemarker的配置简介
在freemarker启动的过程中,参与配置功能的类主要有四个:Configurable,Configuration,Template和Environment。下面给大家简单介绍一下这里类的特点,内容主要是出自freemarker的源码,而且这些英文比较简单,稍微耐心一点就可以读懂的。
Configurable简介

This is a common superclass of {@link freemarker.template.Configuration},{@link freemarker.template.Template}, and {@link Environment} classes.
It provides settings that are common to each of them. FreeMarker uses a three-level setting hierarchy - the return value of every setting getter method on <code>Configurable</code> objects inherits its value from its parent <code>Configurable</code> object, unless explicitly overridden by a call to a corresponding setter method on the object itself. The parent of an 
<code>Environment</code> object is a <code>Template</code> object, the parent of a <code>Template</code> object is a <code>Configuration</code> object.

Configuration简介

The main entry point into the FreeMarker API; encapsulates the configuration settings of FreeMarker, also serves as a central template-loading and caching service.
This class is meant to be used in a singleton pattern. That is, you create an instance of this at the beginning of the application life-cycle, set its {@link #setSetting(String, String) configuration settings} there (either with the setter methods like {@link #setTemplateLoader(TemplateLoader)} or by loading a {@code .properties} file), and then use that single instance everywhere in your application. Frequently re-creating {@link Configuration} is a typical and grave mistake from performance standpoint, as the {@link Configuration} holds the template cache, and often also the class introspection cache, which then will be lost. (Note that, naturally,having multiple long-lived instances,like one per component that internally uses FreeMarker is fine.)  
The basic usage pattern is like:
// Where the application is initialized; in general you do this ONLY ONCE in the application life-cycle!
Configuration cfg = new Configuration(VERSION_X_Y_Z));
// Where X, Y, Z enables the not-100%-backward-compatible fixes introduced in
// FreeMarker version X.Y.Z  and earlier (see {@link #Configuration(Version)}).
cfg.setSomeSetting(...);
cfg.setOtherSetting(...);

Template简介

Stores an already parsed template, ready to be processed (rendered) for unlimited times, possibly from multiple threads.
Typically, you will use {@link Configuration#getTemplate(String)} to create/get {@link Template} objects, so you don't construct them directly. But you can also construct a template from a {@link Reader} or a {@link String} that contains the template source code. But then it's important to know that while the resulting {@link Template} is efficient for later processing, creating a new {@link Template} itself is relatively expensive. So try to re-use {@link Template} objects if possible.{@link Configuration#getTemplate(String)} does that (caching {@link Template}-s) for you, but the constructor of course doesn't, so it's up to you to solve then.
Objects of this class meant to be handled as immutable and thus thread-safe. However, it has some setter methods for changing FreeMarker settings. Those must not be used while the template is being processed, or if the template object is already accessible from multiple threads.

Environment简介

Object that represents the runtime environment during template processing. For every invocation of a <tt>Template.process()</tt> method, a new instance
of this object is created, and then discarded when <tt>process()</tt> returns.
This object stores the set of temporary variables created by the template,the value of settings set by the template, the reference to the data model root,etc. Everything that is needed to fulfill the template processing job.
Data models that need to access the Environment object that represents the template processing on the current thread can use the {@link #getCurrentEnvironment()} method.
If you need to modify or read this object before or after the process call, use {@link Template#createProcessingEnvironment(Object rootMap, Writer out, ObjectWrapper wrapper)}

常用技巧之三

freemarker模板解析过程
例如:一个freemarker表达式 ${hello} ,会被解析成三个部分,分别是

${hello} 前面和后面的body标签,在freemarker中被定义为TextBlock,中间的变量定义为DollarVariable。那么目前的结构也就是RootExpression = TextBlock DollarVariable TextBlock。解释器一进来将会对RootExpression进行解析,RootExpression将会依次调用TextBlock DollarVariable TextBlock进行解析。不同类型将会做不同操作,根据传进来的Context参数进行相应赋值并输出等。 当Template启动解释时,由Environment进入调用根元素的访问动作,根元素会依次访问所包含的TemplateElement,直到所有叶子节点访问完成,这些访问动作是通过调用Environment的visit方法控制,Environment做些相关必要操作,再根据访问的节点类型调用相应节点的访问操作。当访问到包含需要解释器的元素节点时,则会启动解释器做解释操作,根据Expression类型,调用getStringValue,并传入参数Environment,相应类型的表达式根据Environment解释得到输入字符串的值,返回并写到响应流,即解释完成。 **freemarker内建函数介绍** Sequence的内置函数
1.sequence?first 返回sequence的第一个值。
2.sequence?last 返回sequence的最后一个值。
3.sequence?reverse 将sequence的现有顺序反转,即倒序排序
4.sequence?size 返回sequence的大小
5.sequence?sort 将sequence中的对象转化为字符串后顺序排序
6.sequence?sort_by(value) 按sequence中对象的属性value进行排序

注意:Sequence不能为null
Hash的内置函数
1.hash?keys 返回hash里的所有key,返回结果为sequence
2.hash?values 返回hash里的所有value,返回结果为sequence
操作字符串内置函数

1.substring(start,end)从一个字符串中截取子串
start:截取子串开始的索引,start必须大于等于0,小于等于end
end: 截取子串的长度,end必须大于等于0,小于等于字符串长度,如果省略该参数,默认为字符串长度。
2.cap_first 将字符串中的第一个单词的首字母变为大写。
3.uncap_first将字符串中的第一个单词的首字母变为小写。
4.capitalize将字符串中的所有单词的首字母变为大写
5.date,time,datetime将字符串转换为日期
注意:如果指定的字符串格式不正确将引发错误
6.ends_with 判断某个字符串是否由某个子串结尾,返回布尔值
注意:布尔值必须转换为字符串才能输出
7.html 用于将字符串中的<、>、&和"替换为对应得<>&quot:&amp
8.index_of(substring,start)在字符串中查找某个子串,返回找到子串的第一个字符的索引,如果没有找到子串,则返回-1。
Start参数用于指定从字符串的那个索引处开始搜索,start为数字值。
如果start大于字符串长度,则start取值等于字符串长度,如果start小于0,则start取值为0。
9.length返回字符串的长度
10.lower_case将字符串转为小写
11.upper_case将字符串转为大写
12.contains 判断字符中是否包含某个子串。返回布尔值
注意:布尔值必须转换为字符串才能输出
13.number将字符串转换为数字
14.replace用于将字符串中的一部分从左到右替换为另外的字符串。
15.split使用指定的分隔符将一个字符串拆分为一组字符串
16.trim 删除字符串首尾空格

操作数字内置函数
1.c 用于将数字转换为字符串
2.string用于将数字转换为字符串
Freemarker中预订义了三种数字格式:number,currency(货币)和percent(百分比)其中number为默认的数字格式转换
操作布尔值内置函数
string用于将布尔值转换为字符串输出
true转为"true",false转换为"false"
foo?string(“yes”,“no”)如果布尔值是true,那么返回"yes",否则返回no

freemarker日志实现过程分析
freemarker有自己的log类,这是一个抽象类,具体的日志打印委托给classpath里面合适的日志jar包来执行,寻找合适日志jar的查找顺序是:Apache Log4J, Apache Avalon LogKit, JDK log。如果一个合适的日志实现类都没有找到,日志功能将被抑制,并会使用System.err打印出错误提示信息。
如果我们想自己指定使用的日志类型,那么可以通过:
Loger.selectLoggerLibrary(int library);
注意:一定要在freemarker初始化阶段进行设置,在调用任何freemarker api之前进行设置,否则freemarker将会与默认的日志实现进行绑定,从而自己指定的日志修改将不会起到作用。
Freemarker中大于号>的使用

在Freemarker中,比较数据的大小时候,要注意大于号(>)的使用。如果不注意,程序就会发生异常信息,或者使用gt符号。

总结一下:
使用>=和>的时候有一点小问题。FreeMarker解释>的时候可以把它当作FTL标签的结束符。为了避免这种问题,不得不将表达式放到括号内:<#if (x > y) >,另外,可以使用lt代替<,lte代替<=,gt代替>,gte代替>=。由于历史遗留的原因,FTL也支持\lt,\lte,\gt和\gte,使用他们和使用不带反斜杠的效果一样。

序列的重点知识小结

(1)序列的默认值为[]
看下面的例子:

<#if (winnersList![])?size gt 0>
    <table class="winner_table" border="0" cellspacing="0" cellpadding="0">
        <tr>
            <th class="bdr_gray">中奖账号</th>
            <th>猜测差值</th>
        </tr>
    <#list winnersList as list>
        <tr>
            <td class="bdr_gray">${list.accountId!""}</td>
            <td>${list.deviation!""}</td>
        </tr>
    </#list>
    </table>
</#if>

说明:在上面例子中,winnersList默认为[],它的内建函数为size

(2)序列的连接:
可以将两个序列连接成一个新的序列,连接序列的运算符是’+’,见下面的例子:

<#list ["一","二","三"] + ["四","五","六"] as x>
    ${x}
</#list>

输出结果如下:
一二三四五六

(3)序列的切分:
举个例子看序列的切分应用场景:有的时候我们在页面中不需要显示那么长的字符串,比如新闻标题,这样用下面的例子就可以自定义显示的长度
<#if title.content?length lt 8>
KaTeX parse error: Expected 'EOF', got '#' at position 40: …")}</a> <#̲else> …{title.content}">${title.content[0…3]?default("")}
</#if>
上面例子的作用是:如果这个字符串的长度小于8,那么就正常显示,反之则取4位。
序列的切分要注意下面两点:
从FreeMarker 2.3.3版本以后lastindex才能省略。
如果试图访问一个序列首变量之前的项或末变量之后的项将会引起错误,模板的执行也会中断。

(4)子序列的定义:
序列中的项是表达式,那么也可以这样做:[2 + 2, [1, 2, 3, 4], “what”],其中第一个子变量是数字4,第二个子变量是一个序列,第三个子变量是字符串"what"。

(5)数字序列的定义:
第一种定义序列的方式:
<#assign nums=[1,2,3,4,5,77,8,99]/>
使用list指令将序列输出,
<#list nums as num>
${num}
</#list>
第二种定义序列的方式
定义了一个连续的序列,
<#assign nums=12…99/>
这种方式定义的序列的内容是12到99
说明:
从上面的例子可以看出,序列也可以用start…end定义存储数字范围的序列,这里的start和end是处理数字值表达式,比如2…5和[2, 3, 4, 5]是相同的,但是使用前者会更有效率(内存占用少而且速度快)。可以看出前者也没有使用方括号,这样也可以用来定义递减的数字范围,比如5…2。(此外,还可以省略end,只需5…即可,但这样序列默认包含5,6,7,8等递增量直到无穷大)。

(6)判断序列是否包含某个元素
如果要判断序列中是否包含某个指定的元素,可以使用序列的内建函数seq_contains。
注:seq_contains这个内建函数从FreeMarker 2.3.1 版本开始可用。而在2.3 版本中不存在。
<#–声明一个序列,包含若干个元素–>
<#assign x = [“red”, 16, “blue”, “cyan”]>
<#–使用seq_contains判断序列中的元素是否存在–>
“blue”: ${x?seq_contains(“blue”)?string(“yes”, “no”)}
“yellow”: ${x?seq_contains(“yellow”)?string(“yes”, “no”)}
16: ${x?seq_contains(16)?string(“yes”, “no”)}
“16”: ${x?seq_contains(“16”)?string(“yes”, “no”)}
输出结果:
“blue”: yes
“yellow”: no
16: yes
“16”: no
附:seq_前缀在这个内建函数中是需要的,用来和contains 区分开。contains函数用来在字符串中查找子串(因为变量可以同时当作字符串和序列)。
StringTemplateLoader的用法
作为一个模板框架,freemarker的功能还是很强大的。在模板处理方面,freemarker有多种形式,最常见的方式是将模板文件放在一个统一的文件夹下面,如下形式:
Configuration cfg = new Configuration();
cfg.setDirectoryForTemplateLoading(new File(“templates”));
如果我想把模板存放到数据库中,可以实现吗?答案是肯定的。在这里可以使用StringTemplateLoader来加载模板内容。主要的代码实现如下所示:
Configuration cfg = new Configuration();
StringTemplateLoader stringLoader = new StringTemplateLoader();
String templateContent=“hello ${name}!”;
stringLoader.putTemplate(“myTemplate”,templateContent);
cfg.setTemplateLoader(stringLoader);
Template template = cfg.getTemplate(“myTemplate”,“utf-8”);

freemarker报错的处理方案
freemarker文件如果出错,网站的前台页面会报出很明显的错误-焦黄的背景,血红的文字,很不利于用户体验的。如何修改这个问题呢?
首先需要在struts.xml配置文件里添加下面一行代码:

<constant name="struts.freemarker.manager.classname" value="net.swiftlet.freemarker.MyFreemarkerManager" />

接着新建MyFreemarkerManager类,如下所示:

public class MyFreemarkerManager extends org.apache.struts2.views.freemarker.FreemarkerManager
{
    private static final Logger LOG = LoggerFactory.getLogger(MyFreemarkerManager.class);
    public void init(ServletContext servletContext) throws TemplateException
    {
        config = createConfiguration(servletContext);
        config.setTemplateExceptionHandler(TemplateExceptionHandler.IGNORE_HANDLER);
        contentType = DEFAULT_CONTENT_TYPE;
        wrapper = createObjectWrapper(servletContext);
        if (LOG.isDebugEnabled())
        {
            LOG.debug("Using object wrapper of class " + wrapper.getClass().getName());
        }
        config.setObjectWrapper(wrapper);
        templatePath = servletContext.getInitParameter(INITPARAM_TEMPLATE_PATH);
        if (templatePath == null)
        {
            templatePath = servletContext.getInitParameter("templatePath");
        }
        configureTemplateLoader(createTemplateLoader(servletContext, templatePath));
        loadSettings(servletContext);
    }
}
  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值