node-red教程 5 函数节点

5.1 函数控件介绍
  函数控件在node-red中是重点,也是难点。由于其功能强大,能做的事情很多,所以它重要;事实上,函数控件中的“函数”一词,翻译为中文“功能”也是可以的。但是,函数是需要直接编写代码的,所以说也是难点。
  
节点帮助
  一个JavaScript函数块,用来处理节点接收到的消息。
  这些消息作为一个名为msg的JavaScript对象传入。
  按照惯例,它会有一个msg.payload属性,包含消息正文。
  函数被期望返回一个message对象(或多个消息对象),但是可以选择不返回任何东西以阻止流。
详细信息
  有关编写函数的更多信息,请参见在线文档https://nodered.org/docs/writing-functions.html

发送消息
  这个函数既可以使用“return”向流中的下一个节点传递消息,也可以调用node.send(messages)。
  它可以返回或是发送
    一个单一的消息对象——传递给连接到第一个输出的节点
    一组消息对象——传递给连接到相应输出的节点
  如果数组的任何元素本身都是一组消息,那么多条消息就会被发送到相应的输出。
  如果返回null,无论是单独还是作为数组的元素,都不会传递任何消息。
日志和错误处理
  要记录任何信息或报告错误,可以使用以下功能:

node.log(“Log message”)
node.warn(“Warning”)
node.error(“Error”)
1
2
3
Catch节点也可以用来处理错误。为了调用Catch节点,将msg作为第二个参数传递给node.error:

5.2 使用函数控件实现多个输出
  前边使用switch控件时我们已经实现了根据不同的条件,来实现多个输出。Switch控件是一种功能控件,而函数控件也是功能控件。甚至可以说,函数控件可以通过编程实现所有功能控件的功能。接下来,我们尝试用函数控件实现多个输出。

5.2.1 最简单的函数控件用法
  拖入一个inject、function与debug,无需任何修改,直接用线连接三个节点。
这里写图片描述
  部署,并点击inject的输入按钮,观察调试窗口。可以看到debug节点打印的调试信息。
这里写图片描述
  你可能会说,什么嘛,不加这个函数节点,直接连接inject与debug节点,不也是这个效果嘛!这个函数节点有什么用?
它的作用就是,把消息原封不动输出。原封不动的输出,也是一种功能,最简单的功能。
我们双击函数节点,来看一下里边的内容。
这里写图片描述
  只有一行代码,return msg,返回消息。
  回过头来,看函数控件的说明信息。用来处理节点接收到的消息,消息作为一个名为msg的对象传入。可以使用“return”向下一个节点传递消息。也就是说,函数控件的输入时msg对象,输出也是msg对象。在输出之前,可以对msg对象进行处理。所谓对象,意思是这是一个msg实例,是具体的数据,不是抽象的,更不是msg跟异性朋友处对象。
  既然可以原封不动返回msg,当然也可以不返回。只要把语句改为return null即可。Null的意思是空值,也可以说没有值。修改代码为:
这里写图片描述
  重新部署,无论现在如何点击inject节点的输入按钮,debug节点都不会打印出任何消息了。

5.2.2 在函数节点中创建并返回多个消息
  函数控件中,除了可以处理输入的msg对象以外,也可以自己定义一些msg对象。另外,说明文件中,有介绍说function可以返回一个对象,或是用数组传递一组对象。我们先尝试自行建立一组对象并传递。
  修改函数节点:取个合适的名称,添加如下代码,并把输出改为两路。

var msg1 = { payload:“first out of output 1” };
var msg2 = { payload:“second out of output 1” };
var msg3 = { payload:“third out of output 1” };
var msg4 = { payload:“only message from output 2” };
return [ [ msg1, msg2, msg3 ], msg4 ];
1
2
3
4
5
这里写图片描述
  拖入两个debug节点,命名,并分别接到函数节点的两路输出去。然后点击inject的按钮,观察调试窗口的现象。
这里写图片描述
这里写图片描述

我们发现,OUT1节点一口气收到了3条消息,OUT2节点收到了一条消息,且消息的内容我们刚刚输入的代码有关系。跟输入的消息没有任何关系,因为输入的是时间戳。
  关注最后一句并对比分析:

return msg;//一个返回值
return [ [ msg1, msg2, msg3 ], msg4 ];//一组返回值
1
2
  可以得出结论,如果想得到多个返回值,需要把返回值放到英文的中括号里。在3.1.3小节里,恰好在inject的内容里,输入数组时,也是放在中括号里。这说明,多个返回值需要放到一个数组中,数组用中括号括起来,用逗号间隔开。而数组元素的顺序,也就是输出对象的顺序,比如刚刚的OUT1收到的是msg1,msg2与msg3,而OUT2收到的msg4。
  另外,虽然名为数组(array),但是其边的元素并不是数字,而是msg对象,所以,更贴切的叫法应该是:对象组。
这里写图片描述
  这个例子代码如下:

[{“id”:“adaac427.ae09a8”,“type”:“function”,“z”:“a1f259d6.8791a8”,“name”:“两路输出”,“func”:“var msg1 = { payload:“first out of output 1” };\nvar msg2 = { payload:“second out of output 1” };\nvar msg3 = { payload:“third out of output 1” };\nvar msg4 = { payload:“only message from output 2” };\nreturn [ [ msg1, msg2, msg3 ], msg4 ];”,“outputs”:2,“noerr”:0,“x”:360,“y”:180,“wires”:[[“6144cc3f.854e34”],[“4f12a300.ec146c”]]},{“id”:“e49b03a7.395d1”,“type”:“inject”,“z”:“a1f259d6.8791a8”,“name”:"",“topic”:"",“payload”:"",“payloadType”:“date”,“repeat”:"",“crontab”:"",“once”:false,“onceDelay”:0.1,“x”:200,“y”:180,“wires”:[[“adaac427.ae09a8”]]},{“id”:“6144cc3f.854e34”,“type”:“debug”,“z”:“a1f259d6.8791a8”,“name”:“OUT1”,“active”:true,“tosidebar”:true,“console”:false,“tostatus”:false,“complete”:“payload”,“x”:510,“y”:160,“wires”:[]},{“id”:“4f12a300.ec146c”,“type”:“debug”,“z”:“a1f259d6.8791a8”,“name”:“OUT2”,“active”:true,“tosidebar”:true,“console”:false,“tostatus”:false,“complete”:“payload”,“x”:510,“y”:220,“wires”:[]}]
1
5.2.3 通过判断语句进行数据分类
  Switch控件可以进行数据的分流,它属于功能控件。函数控件是功能控件里,功能最强的,它可以实现switch控件的功能。
  第3章4节讲解switch控件时,使用过判断数字的例子,这里用函数控件来实现它。
  输入与输出不变,把函数节点替换掉switch节点,并修改代码如下:
这里写图片描述
  分别点击3个inject节点的输入按钮。可以再调试窗口观察到以下现象。
这里写图片描述
  可以看出,所有数据都根据判断条件正确输出,说明我们的任务完成了。代码如下:

[{“id”:“50a42dcc.f47e44”,“type”:“inject”,“z”:“899b4619.356a88”,“name”:"",“topic”:"",“payload”:“100”,“payloadType”:“num”,“repeat”:"",“crontab”:"",“once”:false,“onceDelay”:0.1,“x”:330,“y”:160,“wires”:[[“d616421c.69733”]]},{“id”:“ea00b43e.c61ba8”,“type”:“inject”,“z”:“899b4619.356a88”,“name”:"",“topic”:"",“payload”:“10”,“payloadType”:“num”,“repeat”:"",“crontab”:"",“once”:false,“onceDelay”:0.1,“x”:330,“y”:220,“wires”:[[“d616421c.69733”]]},{“id”:“370ac143.d7c11e”,“type”:“inject”,“z”:“899b4619.356a88”,“name”:"",“topic”:"",“payload”:“1”,“payloadType”:“num”,“repeat”:"",“crontab”:"",“once”:false,“onceDelay”:0.1,“x”:330,“y”:280,“wires”:[[“d616421c.69733”]]},{“id”:“a9434442.ab3e98”,“type”:“debug”,“z”:“899b4619.356a88”,“name”:“OUT1”,“active”:true,“tosidebar”:true,“console”:false,“tostatus”:false,“complete”:“payload”,“x”:750,“y”:160,“wires”:[]},{“id”:“22fba15b.22857e”,“type”:“debug”,“z”:“899b4619.356a88”,“name”:“OUT2”,“active”:true,“tosidebar”:true,“console”:false,“tostatus”:false,“complete”:“payload”,“x”:750,“y”:220,“wires”:[]},{“id”:“82f1829b.b1b8a”,“type”:“debug”,“z”:“899b4619.356a88”,“name”:“OUT3”,“active”:true,“tosidebar”:true,“console”:false,“tostatus”:false,“complete”:“payload”,“x”:750,“y”:280,“wires”:[]},{“id”:“d616421c.69733”,“type”:“function”,“z”:“899b4619.356a88”,“name”:“判断数字”,“func”:“if (msg.payload > 10 ){\n return [msg,null,null];\n}else if(msg.payload === 10 ){\n return [null,msg,null];\n}else{\n return [null,null,msg];\n}”,“outputs”:3,“noerr”:0,“x”:540,“y”:220,“wires”:[[“a9434442.ab3e98”],[“22fba15b.22857e”],[“82f1829b.b1b8a”]]}]
1
  然后看一看通过主题来判断的例子。跟判断数字没有多大的不同,只需要把被判断的条件改为msg.payload就可以。
这里写图片描述
  这段代码出现了一个indexOf()函数,它的作用就是检查一下调用它的字符串,有没有包含括号里的字符(或字符串)。如果包含的话,返回字符在字符串中首次出现的位置。如果没有包含的话,返回-1。所以,if(msg.topic.indexOf(“e”) != -1)在包含的情况下成立。如图配置并部署。
这里写图片描述
  分别输入3个inject节点,结果如下:
这里写图片描述
  这是跟3.4.4小节中,在选择“接受第一条匹配消息后停止”的情况下,结果一样。
  代码如下:

[{“id”:“e98b2a01.0b6228”,“type”:“function”,“z”:“a1f259d6.8791a8”,“name”:“判断主题”,“func”:“if (msg.topic === “apple”) {\n return [ msg, null, null];\n} \nif(msg.topic === “banana”){\n return [ null, msg, null];\n}\nif(msg.topic.indexOf(“e”) != -1){\n return [null,null, msg ];\n}\n”,“outputs”:3,“noerr”:0,“x”:340,“y”:160,“wires”:[[“7cfcaea9.f501f”],[“6997f0e2.7d67d”],[“95d3ab47.6f79f8”]]},{“id”:“77880a51.e8f324”,“type”:“inject”,“z”:“a1f259d6.8791a8”,“name”:"",“topic”:“apple”,“payload”:“apple”,“payloadType”:“str”,“repeat”:"",“crontab”:"",“once”:false,“onceDelay”:0.1,“x”:130,“y”:100,“wires”:[[“e98b2a01.0b6228”]]},{“id”:“98875d47.089c5”,“type”:“inject”,“z”:“a1f259d6.8791a8”,“name”:"",“topic”:“banana”,“payload”:“banana”,“payloadType”:“str”,“repeat”:"",“crontab”:"",“once”:false,“onceDelay”:0.1,“x”:140,“y”:160,“wires”:[[“e98b2a01.0b6228”]]},{“id”:“a5a2ecfd.7f28f”,“type”:“inject”,“z”:“a1f259d6.8791a8”,“name”:"",“topic”:“orange”,“payload”:“orange”,“payloadType”:“str”,“repeat”:"",“crontab”:"",“once”:false,“onceDelay”:0.1,“x”:140,“y”:220,“wires”:[[“e98b2a01.0b6228”]]},{“id”:“7cfcaea9.f501f”,“type”:“debug”,“z”:“a1f259d6.8791a8”,“name”:“OUT1”,“active”:true,“tosidebar”:true,“console”:false,“tostatus”:false,“complete”:“payload”,“x”:530,“y”:100,“wires”:[]},{“id”:“6997f0e2.7d67d”,“type”:“debug”,“z”:“a1f259d6.8791a8”,“name”:“OUT2”,“active”:true,“tosidebar”:true,“console”:false,“tostatus”:false,“complete”:“payload”,“x”:530,“y”:160,“wires”:[]},{“id”:“95d3ab47.6f79f8”,“type”:“debug”,“z”:“a1f259d6.8791a8”,“name”:“OUT3”,“active”:true,“tosidebar”:true,“console”:false,“tostatus”:false,“complete”:“payload”,“x”:530,“y”:220,“wires”:[]}]
1
  思考一下,apple显然是包括了字符“e”的,但是点击apple时,为什么OUT3没有收到apple的数据?
  这是因为,编程的函数有一个特点:见到return以后函数就结束了,即便return之后还有没执行的代码。if (msg.topic === “apple”)成立以后就执行了return,当然不会管有没有包含“e”的判断条件了。
  如果你想实现apple既要输出给OUT1,又要输出给OUT3的话,可以使用一个数组,在满足if (msg.topic === “apple”)时,元素1储存apple;在满足if(msg.topic.indexOf(“e”) != -1)时,元素3存储apple,只使用一次return,来返回数组就可以了。你可以尝试自己实现它。
  后边也会用到函数的send方法。

5.3 使用函数控件处理数据
  上一小节使用函数控件进行判断与多个输出的情况下,函数控件可能还没有switch好用。但是在这一节,函数控件的优势就能完全体现出来。接下来使用函数控件可以进行数据的处理。

5.3.1 单个数字的计算
  接下来尝试用函数节点对数字进行计算。拖入inject节点,并把输入的内容改为数字;拖入debug节点用来观察现象,拖入函数节点并做如下修改:
这里写图片描述
  其中,var newMsg = {payload: msg.payload * 2 }语句的意思是,新建一个对象,名为newMsg,它有payload属性,并且payload的值为msg.payload的两倍。最后返回的是newMsg,也就是新建的这个对象。
这里写图片描述
  部署并运行,可以看到现象是,调试窗口显示的数值是输入的两倍。说明函数节点的功能实现了。
这里写图片描述
  我们知道,消息是通过msg对象传过来的,可不可以不新建一个对象,直接修改msg.payload呢?当然是可以的。
  将函数节点内的代码修改为

msg.payload = msg.payload*2;
return msg;
1
2
  部署,可以发现结果一样正确。由于这个代码实际的处理顺序是从右向左,也就是msg.payload先乘以2,再赋值给msg.payload。出于方便理解的角度,我们总是用一个变量来暂时储存一下msg.payload计算过的值。代码修改为:

var temp = msg.payload*2;
msg.payload = temp;
return msg;
1
2
3
  这段程序保存如下:

[{“id”:“d5052172.91827”,“type”:“inject”,“z”:“a1f259d6.8791a8”,“name”:"",“topic”:"",“payload”:“2333”,“payloadType”:“num”,“repeat”:"",“crontab”:"",“once”:false,“onceDelay”:0.1,“x”:140,“y”:180,“wires”:[[“ecfadc58.b48d4”]]},{“id”:“ecfadc58.b48d4”,“type”:“function”,“z”:“a1f259d6.8791a8”,“name”:“乘以二”,“func”:“var temp = msg.payload*2;\nmsg.payload = temp;\nreturn msg;”,“outputs”:1,“noerr”:0,“x”:310,“y”:180,“wires”:[[“811f3cdb.bf65b”]]},{“id”:“811f3cdb.bf65b”,“type”:“debug”,“z”:“a1f259d6.8791a8”,“name”:"",“active”:true,“tosidebar”:true,“console”:false,“tostatus”:false,“complete”:“false”,“x”:520,“y”:180,“wires”:[]}]
1
  你可以自己来试一下除法该怎么应用,将计算的代码分别修改并观察结果。

var temp = msg.payload%100;
var temp = msg.payload/100;
1
2
5.3.2 使用数组进行数据的截取与组装
  通常,在实际通信的时候不会只传递一个数组,信息的传递依赖于数组,数组中不同位置的数字也会有不同的含义,例如,第一个数字表示ID,第二个数字表示数组长度,第三个数字表示温度等等,一般会有一个通信协议来规定。所以,如何取出有用的数据,或者按要求把数据进行组装很重要。
  现在,假如收到了一串数据,数据共16位,我们只用到后8位,前边的8位都不要了,如何来操作?
这里写图片描述
  通过分析可以得知:

ARR2[0] = ARR1[8];
ARR2[1] = ARR1[9];
……
ARR2[7] = ARR1[15];
1
2
3
4
  但是写代码的时候一般不会这么复制粘贴。这是循环操作,用for语句可以增加效率。

var temp = new Buffer([0,0,0,0,0,0,0,0]);
for(var i = 0;i <8 ;i++)
temp[i] = msg.payload[i+8];
msg.payload = temp;
return msg;
1
2
3
4
5
  我们新建一个temp数组,有8个元素并初始化为0;
  通过for循环,把msg.payload[i+8]的值赋给temp[i],i可以是0到7。
  然后板temp作为msg的payload返回。
  接下来添加inject与debug节点搭一个测试环境。Inject节点内容为数组。
这里写图片描述
  函数节点内容
这里写图片描述
  整个流程如下
这里写图片描述

[{“id”:“d5052172.91827”,“type”:“inject”,“z”:“a1f259d6.8791a8”,“name”:"",“topic”:“数组输入”,“payload”:"[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]",“payloadType”:“bin”,“repeat”:"",“crontab”:"",“once”:false,“onceDelay”:0.1,“x”:140,“y”:180,“wires”:[[“37f18063.f0bdd”]]},{“id”:“811f3cdb.bf65b”,“type”:“debug”,“z”:“a1f259d6.8791a8”,“name”:"",“active”:true,“tosidebar”:true,“console”:false,“tostatus”:false,“complete”:“false”,“x”:490,“y”:180,“wires”:[]},{“id”:“37f18063.f0bdd”,“type”:“function”,“z”:“a1f259d6.8791a8”,“name”:“数据截断”,“func”:“var temp = new Buffer([0,0,0,0,0,0,0,0]);\nfor(var i = 0;i <8 ;i++)\ntemp[i] = msg.payload[i+8];\nmsg.payload = temp;\nreturn msg;\n”,“outputs”:1,“noerr”:0,“x”:300,“y”:180,“wires”:[[“811f3cdb.bf65b”]]}]
1
  部署并观察调试窗口如下
这里写图片描述
  说明经过函数节点以后,我们成功截取了数组的后8位。
  接下来进行数据的组装。需求是,我们的原始数据是8位的,通信协议要求的数据是16位的,我们要把原始数据放在通信数据的后8位。逻辑跟刚刚的组装正好相反。
这里写图片描述
  以下是函数节点的代码

var temp = new Buffer([170,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0]);
for(var i = 0;i <8 ;i++)
temp[8+i] = msg.payload[i];
msg.payload = temp;
return msg;
1
2
3
4
5
  Inject节点的设置与现象,就请你自行导入流来观察吧。

[{“id”:“d5052172.91827”,“type”:“inject”,“z”:“a1f259d6.8791a8”,“name”:"",“topic”:“数组输入”,“payload”:"[0,1,2,3,4,5,6,7]",“payloadType”:“bin”,“repeat”:"",“crontab”:"",“once”:false,“onceDelay”:0.1,“x”:140,“y”:180,“wires”:[[“37f18063.f0bdd”]]},{“id”:“811f3cdb.bf65b”,“type”:“debug”,“z”:“a1f259d6.8791a8”,“name”:"",“active”:true,“tosidebar”:true,“console”:false,“tostatus”:false,“complete”:“false”,“x”:490,“y”:180,“wires”:[]},{“id”:“37f18063.f0bdd”,“type”:“function”,“z”:“a1f259d6.8791a8”,“name”:“数据截断”,“func”:“var temp = new Buffer([170,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0]); \nfor(var i = 0;i <8 ;i++)\ntemp[8+i] = msg.payload[i];\nmsg.payload = temp;\nreturn msg;\n”,“outputs”:1,“noerr”:0,“x”:300,“y”:180,“wires”:[[“811f3cdb.bf65b”]]}]
1
5.3.3 模拟温度数值的计算
  由于通信方式的限制,数据在发送时,常常要转换为8位的字符来发送。在数字电路里,只有高电平和低电平两种情况,所以数据在发送时,就只能使用1和0个数字。这个1或0,称之为1位。通常,8位可以传输一个字符。2的8次方是256,所以小于256的数字,可以用8位来传输。而大于或等于256的数字,就要分成2个8位传输了。另外,8位的数据可以用两个十六进制的数来表示,比如0101 1010可以表示为0x5a,所以通常会用十六进制来表示通信协议中的数。
  例如,我们现在想传输十进制的数字14859,大于256,所以要分包传送。十进制转化为二进制是0011 1010 0000 1011,在通过硬件发送数据时,如果不考虑校验或者起始位以及高位还是地位在前,就只考虑数字的发送,那么总线上的电平可能是低低高高,高低高低,低低低低,高低高高。把这个数字写成十六进制,是0x3a与0x0b,或0x3a0b,0x用来表示这是个十六进制的数字。0x3049可以通过这种方式换算成10进制:数字拆成单个字符来分析,a到f的数对应10到15,在第几位上,就乘以几个16。0x3a0b = 3×16×16×16+10×16×16+11=14859。或者直接使用16进制进行计算,0x3a0b=0x3a×256+0x0b
  当然,通信协议是可以进行特殊的规定,例如表示4位的温度,34.56°C,要通过十六进制的数字进行分包发送,就是0Xd80/100。我们现在规定,十六进制的温度高位×256+低位=温度×100。接下来尝试解析温度的数据。
  我们使用inject节点输入数组[0xd,0x80],由于inject节点里不支持直接输入十六进制的数字,所以输入[13,128]。这两个数组的值是一样的。
这里写图片描述
  函数节点进行如下修改
这里写图片描述
  连线并部署,然后点击inject节点的输入按钮,可以看到调试窗口输出的正是我们期望的结果:34.56。
这里写图片描述
  代码如下:

[{“id”:“d5052172.91827”,“type”:“inject”,“z”:“a1f259d6.8791a8”,“name”:"",“topic”:“数组输入”,“payload”:"[13,128]",“payloadType”:“bin”,“repeat”:"",“crontab”:"",“once”:false,“onceDelay”:0.1,“x”:140,“y”:180,“wires”:[[“37f18063.f0bdd”]]},{“id”:“811f3cdb.bf65b”,“type”:“debug”,“z”:“a1f259d6.8791a8”,“name”:"",“active”:true,“tosidebar”:true,“console”:false,“tostatus”:false,“complete”:“false”,“x”:490,“y”:180,“wires”:[]},{“id”:“37f18063.f0bdd”,“type”:“function”,“z”:“a1f259d6.8791a8”,“name”:“温度计算”,“func”:“var temp = (msg.payload[0]*256+msg.payload[1])/100;\nmsg.payload = temp;\nreturn msg;\n”,“outputs”:1,“noerr”:0,“x”:300,“y”:180,“wires”:[[“811f3cdb.bf65b”]]}]
1
  可以再添加一个节点,把数字34.56转为字符34.56摄氏度。
这里写图片描述

这里写图片描述

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Node-RED是工业网物联网的重要组成部分,我最开始接触Node-RED,也算是一个偶然的机会吧,上班后领导安排我的第一个任务就是调研一下Node-RED,我之后上网查了一下,那个时候网上相对于Node-RED的资料也比较少,只知道它是IBM公司的一个开源项目。直到最近,发现许多大公司的产品都支持Node-RED,比如西门子公司的IoT2000,研华公司的WISE PaaS 网关,美国OPTO 22等设备中都安装了Node-RED,表明它在工业物联网和控制中已经广泛应用了。 那么工业物联网为什么要用它?它又处于工业物联网那个层次?它具有哪些特性?它帮助物联网解决了什么问题?为什么说它是柔性动态可重构的解决方案呢? ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 本门课程,老师将带领你从Node-RED的发展,工业物联网定位开始讲解,并带领着大家进行手把手安装Node-RED,实际操作演练Node-RED,并搭建一个物联网小平台,给大家带来更好的学习效果。  ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 为了能够让小伙伴们快速了解本门课程的结构,本门课程从以下几个方面展开:Node-RED入门Node-RED安装与配置Node-RED教学实战Node-RED的优势与不足Node-RED能为我们带来什么Node-RED总结与展望

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值