zigbee物联网开发平台(工业物联网)

1.概述鉴于ZigBee技术适合用于数据采集系统的的特点,提出了基于ZigBee的数据采集系统的设计方案,着重探讨ZigBee节点的硬件设计及其组网设计.并详细讨论了基于CC2530芯片的数据采集节点的硬件设计方案,组网设计中的协调器建立网络、节点加入网络的设计方法,以及数据采集系统的软件设计方法.最后通过采集ZigBee网络传感器数据的实验,证明该方案能取得良好的通信效果.

大家好,又见面了,我是你们的朋友全栈君。

1.概述
鉴于ZigBee技术适合用于数据采集系统的的特点, 提出了基于ZigBee的数据采集系统的设计方案, 着重探讨ZigBee节点的硬件设计及其组网设计. 并详细讨论了基于CC2530芯片的数据采集节点的硬件设计方案, 组网设计中的协调器建立网络、节点加入网络的设计方法, 以及数据采集系统的软件设计方法. 最后通过采集ZigBee网络传感器数据的实验, 证明该方案能取得良好的通信效果.
1.1 系统描述
利用ZigBee传感器网络、网关、服务器实现简单的数据采集系统。项目中把使用了三种传感器,分别是:温湿度传感器,烟雾传感器,光敏传感器。终端节点传感器采集到数据之后发送给协调器之后,由协调器通过串口将数据发至给电脑客户端,最后电脑客户端将串口发送上来的传感器数据使用套接字封装成http格式后通过post方式发送到服务端,并且存储到数据库中。客户端通过访问服务器,获取数据展示出来。
1.2 系统结构介绍
这里写图片描述


图1:系统结构图
如图1所示,本系统主要有传感器节点、协调器、网关、服务器、客户端四个部分组成。传感器部分采集温湿度,可燃气,光照数据。通过使用Zstack协议栈来进行zigbee传感器节点组网,使用c语言来控制zigbee终端节点io口采集到传感器数据,协调器再将数据通过串口上传到网关。网关部分从串口接收来自协调器发来的传感器数据,并以及解析从串口收到的数据,并且把数据封装成http格式通过post方式可以实现远程发送数据到服务器。客户端使用python从服务器获取数据,设计图形界面用于数据展示。
1.3 软硬件开发平台
软件:
VMware Workstation(linux OS)
TCP&UDP测试工具(测试)
Configure Virtual Serial Port Driver(测试)
IAR Embedded Workbench(CC2530 传感器)
sublime(C 网关)
IntelliJ IDEA 2017.1.1(JAVA 服务器)
JetBrains CLion 2016.3.4(Python 显示)
xampp(数据库)
串口调试助手(测试)
硬件:温湿度传感器、光照传感器、可燃气传感器、CC2530开发板*4
1.4 实现功能
通过传感器采集环境数据,并将采集到的数据进行处理后在客户端做展现,客户端可以查询某一日下的数据变化情况。
2 系统设计方案
数据格式:
• FF00 01 0600 1A 27
• FF00: 包标志 2Byte
• 01: 传感器类型 1Byte
• 0600: 短地址 2Byte
• 1A27: 传感器数据 1A:温度26 27:湿度39
• 传感器类型数据大小
• 温湿度 PACKET_DHT11_DATA_SIZE 2byte
• 光敏 PACKET_PHOTORESUSTANCE_DATA_SIZE 1byte
• 可燃气体 PACKET_COMBUSTIBLEGAS_DATA_SIZE 1byte
3 系统具体实现 (项目整体实现细节、各个模块的流程,关键代码)
传感器部分:
整体流程:
这里写图片描述
传感器节点接入协调器网络,封装传感器数据

 if ( event & MY_REPORT_EVT )      // MY_REPORT_EVT事件触发处理
  {
    if ( appState == APP_REPORT ) 
    {
      pData[PACKET_FLAG_OFFSET] = PACKET_L_FLAG;
      pData[PACKET_FLAG_OFFSET + 1] = PACKET_H_FLAG;
      pData[PACKET_SENSOR_SHORT_ADDR_OFFSET] = (NLME_GetShortAddr() & 0xff);
      pData[PACKET_SENSOR_SHORT_ADDR_OFFSET + 1] = (NLME_GetShortAddr() >> 8);
#if PACKET_SENSOR_TYPE == PACKET_DHT11
    dht11_update(); 
    pData[PACKET_SENSOR_TYPE_OFFSET] = PACKET_DHT11;
    pData[PACKET_SENSOR_DATA_OFFSET] = dht11_temp();
    pData[PACKET_SENSOR_DATA_OFFSET + 1] = dht11_humidity();
#elif PACKET_SENSOR_TYPE == PACKET_PHOTORESUSTANCE
    pData[PACKET_SENSOR_TYPE_OFFSET] = PACKET_PHOTORESUSTANCE;
    pData[PACKET_SENSOR_DATA_OFFSET] = HalAdcRead (HAL_ADC_CHN_AIN1,HAL_ADC_RESOLUTION_8);
#else
    pData[PACKET_SENSOR_TYPE_OFFSET] = PACKET_COMBUSTIBLEGAS;
    pData[PACKET_SENSOR_DATA_OFFSET] = HalAdcRead (HAL_ADC_CHN_AIN1,HAL_ADC_RESOLUTION_8);

上传数据包结构
【包标志】【传感器类型】【短地址】【数据】
包标志 2Byte FF00
传感器类型
温湿度 2byte 传感器类型编号 01
光敏 1byte 传感器类型编号 02
可燃气体 1byte 传感器类型编号 03
将数据包发送给协调器

#if PACKET_SENSOR_TYPE == PACKET_DHT11
      zb_SendDataRequest( 0xFFFE, SENSOR_REPORT_CMD_ID, PACKET_BASE_SIZE + PACKET_DHT11_DATA_SIZE, pData, 0, AF_TX_OPTIONS_NONE, 0 );
#elif PACKET_SENSOR_TYPE == PACKET_PHOTORESUSTANCE
      zb_SendDataRequest( 0xFFFE, SENSOR_REPORT_CMD_ID, PACKET_BASE_SIZE + PACKET_PHOTORESUSTANCE_DATA_SIZE, pData, 0, AF_TX_OPTIONS_NONE, 0 );
#else
      zb_SendDataRequest( 0xFFFE, SENSOR_REPORT_CMD_ID, PACKET_BASE_SIZE + PACKET_COMBUSTIBLEGAS_DATA_SIZE, pData, 0, AF_TX_OPTIONS_NONE, 0 );

协调器接收到数据包通过串口上传

void zb_ReceiveDataIndication( uint16 source, uint16 command, uint16 len, uint8 *pData  )
{ 
  HalUARTWrite(HAL_UART_PORT_0, pData, len);
}

协调器接收到网关发送的命令做出命令判断后将其发送给传感器节点


  while (Hal_UART_RxBufLen(port))		//计算并返回接收缓冲区的长度
  {
    len = HalUARTRead (port, pBuf, RX_BUF_LEN);
    HalUARTWrite (port, pBuf, len);
    if(len >= PACKET_BASE_SIZE 
       && pBuf[PACKET_FLAG_OFFSET] == PACKET_L_FLAG 
         && pBuf[PACKET_FLAG_OFFSET + 1] == PACKET_H_FLAG)
    {
      zb_SendDataRequest( *(uint16 *)(pBuf + PACKET_SENSOR_SHORT_ADDR_OFFSET), OPERATION_CMD_ID, len, pBuf, 0, AF_TX_OPTIONS_NONE, 0 );
    }
  }


命令数据包结构
【包标志】【操作指令】【对应传感器短地址】
包标志 2Byte FF00
操作指令 1Byte
01 LED开启
02 LED关闭
03 LED状态反转
04 LED闪烁
对应传感器短地址 2Byte
传感器节点接收到协调器控制数据包并解析做出相应的反应

void zb_ReceiveDataIndication( uint16 source, uint16 command, uint16 len, uint8 *pData  )
{
  if(len >= PACKET_BASE_SIZE 
     && pData[PACKET_FLAG_OFFSET] == PACKET_L_FLAG 
     && pData[PACKET_FLAG_OFFSET + 1] == PACKET_H_FLAG
     && *(uint16 *)(pData + PACKET_SENSOR_SHORT_ADDR_OFFSET) == NLME_GetShortAddr())
  {
    switch(pData[PACKET_OPERATION_TYPE_OFFSET])
    {
      case PACKET_ON:
        HalLedSet( HAL_LED_1, HAL_LED_MODE_ON );
        break;
      case PACKET_OFF:
        HalLedSet( HAL_LED_1, HAL_LED_MODE_OFF );
        break;
      case PACKET_TOGGLE:
        HalLedSet( HAL_LED_1, HAL_LED_MODE_TOGGLE );
        break;
      case PACKET_BLINK:
        HalLedBlink(HAL_LED_1, 0, 50, 500);
        break;
      default:
        break;
      }
  }
}


网关部分:
程序流程:
这里写图片描述
关键代码:

//serial part串口部分
    	cssl_t *ser;     
    	cssl_start();
    	ser=cssl_open(dev_path,callback,0,38400,8,0,1);   
    	if (!ser){
        		printf("%s\n",cssl_geterrormsg());
    	}
    	printf("Serial connection successful, DEV = %s\n", dev_path);

//create socket
建立socket 建立一个sock连接

sockfd = socket( AF_INET,SOCK_STREAM,0 );

	if ( sockfd < 0 )
	{
		fprintf( stderr, "socket:%s\n", strerror(errno));
		exit(1);		
	}
	else{
		printf("success to connect the service\n");
	}

	struct sockaddr_in sockaddr;
	//设置连接信息结构
	memset( &sockaddr,0,sizeof(sockaddr) );
	sockaddr.sin_family = AF_INET;
	sockaddr.sin_port = htons ( atoi (port) );
	inet_pton(AF_INET,ip,&sockaddr.sin_addr.s_addr);
	socklen_t len = sizeof(sockaddr);
	//连接到远端服务器 
	if( connect(sockfd,(struct sockaddr*)&sockaddr,len) <0 ){
		fprintf(stderr, "connect: %s\n", strerror(errno) );
		exit(1);
	}

回调函数,打印串口收到的数据的信息,在这个方法中对串口收到的数据进行解析,并且调用通过socket发送数据给服务器

static void callback(int id, uint8_t *buf,int length)    
{
    int i;
    char dtype[2],data[50];
    printf("the date from serial:%s\n", buf);
    printf("\nread serial success:  length = %d\n", length); 
    sprintf(dtype,"%d",buf[2]);
    switch(dtype[0]){
	case '1':
		sprintf(data,"wendu=%d&shidu=%d",buf[5],buf[6]);
		puts(data);
		sendDatawenshidu(data,strlen(data));
		break;
	case '2':
		sprintf(data,"gz=%d",buf[5]);
		puts(data);
		sendDataguangzhao(data,strlen(data));
		break;
	case '3':
		sprintf(data,"gs=%d",buf[5]);
		puts(data);
		sendDataGas(data,strlen(data));
		break;
	default:
		puts("the date is error"); 
		break;
	}
}

把数据封装成http格式,并且通过post方式发送传感器数据

void sendDatawenshidu(uint8_t *buf,int length)
{
    printf("buf:%s\n",buf);
    //接口
    char page[] = "addWendu";  
    char content[4096];  
    char content_page[50];  
    char content_host[50];  
    char content_len[50]; 
    //1请求行
sprintf(content_page,"POST /%s HTTP/1.1\r\n",page);  

//把数据封装成http格式,并且通过post方式发送光照值数据
void sendDataguangzhao(uint8_t *buf,int length)
{
    printf("buf:%s\n",buf);
    char page[] = "Guangzhao";  
    char content[4096];  
    char content_page[50];  
    char content_len[50];  
    char content_host[50]; 
    sprintf(content_page,"POST /%s HTTP/1.1\r\n",page);  
    sprintf(content_host,"HOST: %s:%d\r\n",ip,port);  
    char content_type[] = "Content-Type: application/x-www-form-urlencoded\r\n";  
    sprintf(content_len,"Content-Length: %d\r\n\r\n",strlen(buf));  
    sprintf(content,"%s%s%s%s%s",content_page,content_host,content_type,content_len,buf);  
    send(sockfd,content,strlen(content),0);  
}
//把数据封装成http格式,并且通过post方式发送可燃气体值数据
void sendDataGas(uint8_t *buf,int length)
{
    printf("buf:%s\n",buf);
    char page[] = "Gas";  
    char content[4096];  
    char content_page[50];  
    char content_host[50];
    char content_len[50]; 
    sprintf(content_page,"POST /%s HTTP/1.1\r\n",page);       
    sprintf(content_host,"HOST: %s:%d\r\n",ip,port);  
    char content_type[] = "Content-Type: application/x-www-form-urlencoded\r\n";   
    sprintf(content_len,"Content-Length: %d\r\n\r\n",strlen(buf));  
    sprintf(content,"%s%s%s%s%s",content_page,content_host,content_type,content_len,buf); 
    send(sockfd,content,strlen(content),0);  
}

一个HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据4个部分组成,下图给出了请求报文的一般格式。
    /*2.请求头部
    /Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机。
    */
    sprintf(content_host,"HOST: %s:%d\r\n",ip,port);  

 /*4.请求数据
    /请求数据不在GET方法中使用,而是在POST方法中使用。
    /POST方法适用于需要客户填写表单的场合。与请求数据相
    /关的最常使用的请求头是Content-Type和Content-Length。
    */
    char content_type[] = "Content-Type: application/x-www-form-urlencoded\r\n";  
    sprintf(content_len,"Content-Length: %d\r\n\r\n",strlen(buf));  
    sprintf(content,"%s%s%s%s%s",content_page,content_host,content_type,content_len,buf);  
    send(sockfd,content,strlen(content),0);  

}

服务端:
设计方案
服务端用maven整合springboot框架搭建项目,数据库使用mysql,代码主要分为四个层,POJO层、Dao层、Service层、Controller层。

POJO层:该层包含三个实体类,温湿度封装类、光照值封装类、可燃气体封装类;
Dao层:负责与数据库交互,提供数据源;
Service层:该层主要负责管理事务;
Controller层;负责提供json数据接口。
主要代码:

@RequestMapping(value = "/findAllShidu")
public Map<String,Object> findAllShidu(){
    Map<String,Object> wenduMap = new HashMap<String,Object>();
    List<Wendu> list = wenduService.findAllShidu();
    wenduMap.put("data",list);
    return wenduMap;
}
@RequestMapping(value = "/addWendu")
public Map<String,Object> addWendu(Double wendu,Double shidu) {
    Map<String, Object> wenduMap = new HashMap<String, Object>();
    System.out.print(wendu);
    try {
      wenduService.saveWendu(wendu,shidu);
    } catch (Exception e) {
        wenduMap.put("status", "fail");
        return wenduMap;
    }
    wenduMap.put("status", "success");
    return wenduMap;
}

Web显示端
主要代码:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8">
        <link rel="icon" href="https://static.jianshukeji.com/highcharts/images/favicon.ico">
        <meta name="viewport"  content="width=device-width,initial-scale=1">
        <script src="https://img.hcharts.cn/jquery/jquery-1.8.3.min.js"></script>
        <script src="https://img.hcharts.cn/highcharts/highcharts.js"></script>
        <script src="https://img.hcharts.cn/highcharts/modules/exporting.js"></script>
        <script src="https://img.hcharts.cn/highcharts-plugins/highcharts-zh_CN.js"></script>
    </head>
    <body>
        <div id="container" style="min-width:400px;height:400px"></div>
        <script>
		var wendu=0;
			$(function(){
	getallRepairOrder();
	
	function getallRepairOrder(){
		$.ajax({
			url:"http://localhost:8080/findGas",
			type:"post",
			async:true,
			success:function(data){
				//oOrderContainer.text("");
				//回调
				 wendu=data.data[0][0];			
				//alert(data.data[0][0]);
				}
		});
		setTimeout(getallRepairOrder, 1000);
	
	}
	});
			Highcharts.setOptions({
                global: {
                    useUTC: false
                }
            });
            function activeLastPointToolip(chart) {
                var points = chart.series[0].points;
                chart.tooltip.refresh(points[points.length -1]);
            }
            $('#container').highcharts({
                chart: {
                    type: 'spline',
                    animation: Highcharts.svg, // don't animate in old IE
                    marginRight: 10,
                    events: {
                        load: function () {
                            // set up the updating of the chart each second
                            var series = this.series[0],
                                chart = this;
                            setInterval(function () {
                                var x = (new Date()).getTime(), // current time
                                    y = wendu;
                                series.addPoint([x, y], true, true);
                                activeLastPointToolip(chart)
                            }, 1000);
                        }
                    }
                },
                title: {
                    text: '可燃气体实时数据'
                },
                xAxis: {
                    type: 'datetime',
                    tickPixelInterval: 150
                },
                yAxis: {
                    title: {
                        text: '可燃气体值 %'
                    },
                    plotLines: [{
                        value: 0,
                        width: 1,
                        color: '#808080'
                    }]
                },
                tooltip: {
                    formatter: function () {
                        return '<b>' + this.series.name + '</b><br/>' +
                            Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', this.x) + '<br/>' +
                            Highcharts.numberFormat(this.y, 2);
                    }
                },
                legend: {
                    enabled: false
                },
                exporting: {
                    enabled: false
                },
                series: [{
                    name: '初始数据',
                    data: (function () {
                        // generate an array of random data
                        var data = [],
                            time = (new Date()).getTime(),
                            i;
                        for (i = -19; i <= 0; i += 1) {
                            data.push({
                                x: time + i * 1000,
                                y: 0
                            });
                        }
                        return data;
                    }())
                }]
            }, function(c) {
                activeLastPointToolip(c)
            });
	
        </script>
    </body>

</html>

客户端可视化展示部分:
主要流程:
这里写图片描述

数据访问:

def get_data(url,date):
    reponse = urllib.request.urlopen(url)
    html = reponse.read().decode('utf-8')
    PullData= json.loads(html)
    DataArry = PullData['data']
    xar,yar=[],[]
    for each in DataArry:

        if len(each)>1:
            y,x=each
            x=datetime.strptime(x,'%Y-%m-%d %H:%M:%S')
            if x>date and x<(date+timedelta(days=1)):
                xar.append(x)
                yar.append(float(y)
    return xar,yar
参数:
url:访问数据的接口。
对接口进行封装:
wendu_url = 'http://localhost:8080/findAllWendu'
shidu_url = 'http://localhost:8080/findAllShidu'
gas_url = 'http://localhost:8080/findGas'
sun_url='http://localhost:8080/findGuangzhao'
data = [('温度', wendu_url), ('湿度', shidu_url), ('可燃气', gas_url),('光照',sun_url)]
date:对其他日期进行过滤,可以查询特定日期下的数据变化情况。
对数据绘图:
drawPic.f.clf()
drawPic.a=drawPic.f.add_subplot(111)
x,y=get_data(url,sampleCount)
drawPic.a.set_xlim(min(x), max(x))
drawPic.a.plot(x,y,color='r',label='wendu',)
xlabels = drawPic.a.get_xticklabels()
for label in drawPic.a.xaxis.get_ticklabels():
    label.set_fontsize(5)
for label in drawPic.a.yaxis.get_ticklabels():
    label.set_fontsize(5)
for xl in xlabels:
    xl.set_rotation(45)
drawPic.a.set_title('Demo: DATA Display')
drawPic.a.set_xlabel(sampleCount.date())
drawPic.canvas.show()

图形界面:

drawPic.canvas = FigureCanvasTkAgg(drawPic.f, master=root)

drawPic.canvas.show()

drawPic.canvas.get_tk_widget().grid(row=0, columnspan=3)
var = StringVar()
var.set(1)
count = 0
for each in data:
    Radiobutton(root, text=each[0], variable=var, value=each[1]).grid(row=1, column=count, sticky=W)
    count += 1
Label(root,text='请输入查询的日期(Y-m-d):').grid(row=2,column=0)
#单选框,选择可以传入某一接口
inputEntry=Entry(root)
inputEntry.grid(row=2,column=1)
inputEntry.insert(0,'2017-6-12')
Button(root,text='画图',command=drawPic).grid(row=2,column=2,columnspan=3)
#command=drwaPic 按下Button执行drwaPic函数绘图

4 系统调试及运行测试截图
这里写图片描述
上图是网关运行截图
这里写图片描述
上图是服务器运行截图
这里写图片描述
上图是web显示截图
这里写图片描述
上图是python可视化窗口端截图
5 结束语
提出了一种基于ZigBee的数据采集系统的设计方案. 在实验中搭建了温度采集的ZigBee网络, 通信良好, 并实现了该系统的功能. ZigBee网络的覆盖范围一般为几十米, 但是可以通过网关与其他网络(例如WiFi、以太网)进行连接, 从而达到扩大网络覆盖范围或远程监控的目的, 这将使得ZigBee优良特性得到更好的体现, 使得它有更广阔的应用前景.
项目源码地址:https://github.com/Wlain/Environment-Data-Acquisition-System-of-Internet-of-Things-Based-on-ZigBee.git

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/127842.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • MySQL的binlog日志详解

    binlog就是binarylog,二进制日志文件,这个文件记录了MySQL所有的DML操作。通过binlog日志我们可以做数据恢复,增量备份,主主复制和主从复制等等。对于开发者可能对binlog并不怎么关注,但是对于运维或者架构人员来讲是非常重要的。MySQL5.7这个版本默认是不开启binlog日志的,具体的开启方式可以查看https://blog.csdn.net/king_kgh…

  • HTML 表格表单代码实例(个人简介表)

    HTML 表格表单代码实例(个人简介表)<!DOCTYPEhtml><htmllang=”en”><head><metacharset=”UTF-8″><metahttp-equiv=”X-UA-Compatible”content=”IE=edge”><metaname=”viewport”content=”width=device-width,initial-scale=1.0″><title&…

  • 微信小程序api

    微信小程序api1.api1.概述小程序开发框架提供丰富的微信原生API,可以方便的调起微信提供的能力,如获取用户信息,本地存储,支付功能等2.api分类监听api约定以on开头的API用了来监听某个事件是否触发同步api约定,以Sync结尾的API都是同步API,直接接取函数返回的的结果既可,不需要等待异步api大多数API都是异步API,如wx.request,wx.login等,异步api方法主体是object结构,都有success/faill/complete几乎所有的异步api都支

  • C++中string append函数的使用与字符串拼接「建议收藏」

    C++中string append函数的使用与字符串拼接「建议收藏」常用的函数原型:basic_string&amp;amp;amp;append(constbasic_string&amp;amp;amp;str);basic_string&amp;amp;amp;append(constchar*str);basic_string&amp;amp;amp;append(constbasic_string&amp;amp;amp;str,size_typeindex,size

  • 手把手教你获取x信本地数据库(利用Sqlcipher查看)

    手把手教你获取x信本地数据库(利用Sqlcipher查看)最近一直在研究Xposed等一些hook框架,进行学习做一些demo,这次就正好拿x信练练手,学习学习,也可以学习x信手机本地数据库的表结构设计等。好,废话不多说,直接开干。前提是你的电脑已经安装了adb相关程序,如果你本身就是安卓开发者,想必肯定安装了AndroidStudio,那里直接就有adb相关程序。其次,你准备一台已经root过的安卓手机(要安装的有使用过的x信应用,不

    2022年10月22日
  • 最佳路径分析_最佳路径选择

    最佳路径分析_最佳路径选择最佳路径分析(作者:吴东梅,撰写时间:2019年1月17日)学习了GIS技术,让我们对地理信息系统有了一定的了解,也让我们知道了我们平时常用的导航系统里面的导航最佳路径里面的一些奥妙。现在就让我们了解一下如何制出最佳路径的方法并应用在我们自己的项目中吧!1、 先在SuperMapiDesktop9D把你的地图打开,然后点击上方导航条上面的交通分析,里面有一个拓扑构网,鼠标移到拓扑构网上…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号