1.脚手架

随着国内IT事业的兴起,越来越多的小伙伴也投身到了开发这个相对高薪的行业来。很多同学进入的方式都是零基础通过培训或者看视频自学,在工作一两年后,发现由于自己的基础太薄弱,想进一步提高自己的能力变得非常 困难。

现在市面上的视频教程,主要有一下两类:第一类是纯理论的,比如框架、算法、虚拟机等;另一类是Demo级别的项目,如各大培训机构的项目课程。从业这么多年,学习了大量的视频教程,也跟很多毕业三年左右的程序员做过交流,但一直没发现一套特别好的教程,能让小伙伴们从零基础一直到高级进阶,持续得到学习。在工作中,他们也反馈,视频教程各种高大上的技术堆砌,而在实际开发中呢,大部分技术都没有用到,就算是用,也完全不是像教程中那么用的。在面试中,你跟夸夸其谈十分钟,面试官一句,请问你在项目中是怎么用这个技术的,在使用的时候有什么问题?遇到这样的问题,大部分同学们都直接熄火,完全不知所措,为啥呢,因为他在项目中根本没有用过这个技术,只是看了文档、视频,只是跟着教程做了Demo。而技术跟业务如何结合,这应该是大部分同学在工作中最薄弱的环节。甚至,有些同学会唯技术论,面对公司的业务,会去抱怨公司用的技术不新,认为业务不重要。但我要纠正的是,技术的出现本来就是为业务服务的,离开业务谈技术那就是耍流氓。

早在两年前,我就萌生了这样的想法,既然国内的环境造成了面试修地球,上班拧螺丝的情况,那么我能不能结合我自己做的真实项目,脱敏后给大家分享出来,让各位同学能有一个真实的项目环境去边学边提高,所以,也就有了《从0开始用Java做"智慧农业物联网"》课程的诞生。

学习本课程的基本要求:有Java基础,学习过Spring,SpringMVC,Mybatis框架,做过简单项目以上的同学均可学习。但并不代表,本课程就是个入门教程,对于有开发经验的同学们,物联网这个行业是个朝阳产业,也可以说是未来十年发展的蓝海。那我相信,有实体经济支撑的行业绝不会像互联网行业有那样大的泡沫,也绝不会让你工作的没有安全感。对于有志于从事物联网相关工作的同学,对于想从各方面提升自己的各位同学们来说,本课程也非常的适合你们去学习。

我从12年就开始接触物联网项目,做过智慧猪场、智慧农场、猪联网、云医疗等相关项目,也做过互联网行业,兜兜转转一圈下来,自认为还是积累了很多的经验和教训,那我都会在我课程中对这样的知识有所分享。

同时,在这个课程中,我既是产品,又是设计,还是开发,还是运维,也是客户,我会从一个项目的全生命周期给大家进行介绍,并且都有相关的落地方案。这样,同学们的眼光就会有所延展,不会仅仅局限在开发这一亩三分地上。

本课程的亮点:我只能说,亮点很多,很多。。。

核心如下:产品经理眼中的产品、数据库设计、如何把一个需求变成代码、如何跟物联网设备交互、如何上线一个项目、在需求变更后如何做到不跟产品撕逼。。。

看到了吧,我不会只让你知道那些高大上的技术点,我向你们学会的是如何做一个完美的产品!

在这个课程中,你会看到太多太多跟其他教程不同的地方,你看到的不是Demo级别的案例,你看到的绝对是一个工业级别的实现方案。我也希望通过这个课程,同学们能进一步升华自己的视界,你所站的高度,你看问题的不同角度,将决定未来你成长的上限。

对于本课程的成长忠告:本课程的录制会持续很长时间,是的,你没看错,是很长时间,目前我的预期,起码是在年底之前不会结束,为什么要这样安排,因为我想要分享的内容实在太多,我也不想对课程进行拆分,也不想草草结束,所以,你所能得到的收获一定会足够大,他没有终点。。。

最终达到的效果:在业务中学技术点,通过技术点让业务变得更优美!

希望,能通过专门课程,你收获的不仅仅只有知识,还可能是未来路上的一个好朋友,谢谢大家!

1.物联网行业前景-政府重视

阿里云有IoT-万物互联

发展前景-新西兰技术移民

8.环境搭建之脚手架

备选:小白(java基础,懂javaweb开发的常见框架ssm,ssh),接触我们的项目,进行学习。对于git你是不懂的,会有git入门,svn入门;介绍网上的搭建好的源码管理平台github,gitee(码云);

课程的特点就是全面,所有的知识点只要是你希望学习的,我们都可以进行介绍。介绍的风格一定是贴近实战的,我讲的跟网上的教程可能会有不同,但是我讲的一定是我在项目中使用过的方式。我指的项目指的是实战。

我讲的不一定是高大上的,但一定是最实用的。符合现阶段项目的技术栈,包含人员对新技术的掌握程度。

脚手架:https://gitee.com/zhang.w/boot-backend

在从版本控制库中拉代码:

​ 1.安装配置jdk环境,maven环境,本地git软件,idea

​ 2.注册码云的账号

​ 3.通过开发工具导出项目

​ 4.进行项目的构建,是一个标准的SPringBoot项目

9.生产级别的SpringBoot项目入门

​ 1.导入脚手架后需要找他的使用文档和sql脚本

​ 2.打开数据库管理软件,导入脚本

​ 3.运行main函数

10.依赖版本控制-pom文件介绍

​ SpringBoot项目的特点是约定大于配置,没有太多的配置文件,还是有一些的,而且SpringBoot会维护一个配置bean,之前spring开发基本都是xml配置,在SpringBoot中,简化了xml配置,并不是说没有了,如果你需要配置,可以使用配置bean。这一讲的配置主要是maven的pom文件。maven’用来做jar包管理,对应我们项目开发的通用组件,来看一下有哪些jar包?

spring-boot-starter-parent起步依赖,SpringBoot有两个大版本,1.X,2.X,这两个版本有一些区别,有一些类在升级到2.X的时候去掉了,我们开发用的最新版本,建议你升级到最新

11.项目中applicaiton.yml配置文件详细讲解

YAML Ain’t a Markup Language

springboot项目的配置文件不再采用xml的方式,而是使用yml格式,当前springboot的配置也有另外一种格式,applicaiton.properties,但是后一种格式可读性太差,不建议使用

12.完成第一个增删改查

代码生成器:从dao-service-controller-html-js-css

前端用的layui

需要有数据库的表,只要有数据库的表,就能生成代码,生成完的代码是可以直接运行的

代码生成器生成完毕后,需要把各个文件拷贝到对应的目录中去

虽然代码生成啦,但是你看不到效果,因为我们的权限框架shiro做了控制,必须对shiro进行配置才能找到对应的功能

13.权限配置-Shiro配置入门

系统设置-菜单-添加一个新的菜单

系统设置-角色-配置当前用户是否拥有访问这个菜单的权限

此时因为ehcache生效了,每次用户登录会从数据库把当前用户拥有的角色权限都取出来,放在缓存里,当你修改了权限后,他不会更新缓存,所以需要手动退出登录,再登录一次,让ehcache中的缓存变成最新的

2.Redis部分知识概述

2.1.2Redis在项目中的地位及使用场景剖析

2.1.3Redis安装-win-linux-mac

​ windows安装教程:https://www.cnblogs.com/tommy-huang/p/6093813.html

​ windows版本下载:https://github.com/MicrosoftArchive/redis/releases

​ linux安装建议使用yum方式:https://www.cnblogs.com/rslai/p/8249812.html

​ linux编译方式安装:https://www.cnblogs.com/wuguiyunwei/p/7026315.html

​ mac安装:跟linux安装一样

2.1.4Redis客户端命令行redis-cli操作

​ https://www.cnblogs.com/kongzhongqijing/p/6867960.html

​ https://www.runoob.com/redis/redis-commands.html

如何连接redis服务器:redis-cli ip地址 端口号 连接的是集群需要单独配置参数-c

最大的使用场景就是在运维

2.1.5Java连接Redis-Jedis简介

​ 原文:https://blog.csdn.net/yz357823669/article/details/78752950

​ 源码:https://github.com/RisingSunYZ/test_redis

Redis基于Java的客户端SDK收集

2.1.6RedisPlus图形化客户端-支持集群的访问

​ 开源地址:https://gitee.com/MaxBill/RedisPlus

​ 下载地址:https://pan.baidu.com/s/1ETwWnEj4rbsE1S3GlYHlWg

2.1.7Redis跟SpringBoot整合-注解方式使用Redis

2.1.8Redis跟SpringBoot整合-RedisTemplate使用Redis

2.1.9.1使用Redis实现一个分布式锁

2.1.9.2使用Redis实现一个分布式锁操作演示

2.1.10Redis高可用方案-哨兵模式-SpringBoot整合

哨兵是用来放哨的,能实时监控我们redis集群的状态,保证redis服务器不会挂掉

​ 搭建:https://blog.csdn.net/Zer01ne/article/details/83010407

Redis哨兵配置的作用是可以发现redis的主从节点的位置,以及他们当前运行的状态

一主二从三哨兵模式,三个哨兵保证了哨兵本身是高可用的,再去监控其他redis的状态

​ 整合:https://blog.csdn.net/Mars13889146832/article/details/79534981

通过JedisSentinelPool获取到Jedis,对外提供缓存服务

2.1.10Redis高可用方案-RedisCluster-SpringBoot整合

​ 搭建:https://www.cnblogs.com/wuxl360/p/5920330.html

redis-trib.rb  create  --replicas  1  192.168.31.245:7000 192.168.31.245:7001  192.168.31.245:7002 192.168.31.210:7003  192.168.31.210:7004  192.168.31.210:7005

​ 整合:https://blog.csdn.net/qq_30905661/article/details/80859407

2.1.11Redis高可用方案-云上的服务

阿里云,腾讯云,华为云,AWS

云服务商都提供了对于Redis的解决方案,就是可以直接花钱买安装好Redis的服务器,并且对外提供服务

云服务商的机器的稳定性要比自建机房更可靠

2.1.12Redis高可用方案-公私混合云

随着对混合云了解的增多,人们不难发现,看似完美的混合云也有诸多痛点。

一是如何统一管理多个不同的云平台。在混合云场景下,企业常常采用多个公有云或私有云平台,这样不仅造成基础设施资源池多样化,还使得异构资源环境面临着同时管理物理机、虚拟化的局面。此外,合适的管理工具的缺乏,也给平台管理人员带来非常大的压力和工作量。

二是如何更好进行云网融合。对于用户而言,当前混合云业务面临的最大问题之一便是云计算资源和网络资源的申请、计费、运维处于彼此割裂状态,这在一定程度上也影响了用户的体验。

三是如何开展混合云的相关应用。混合云并不是简单的把公有云和私有云堆砌在一起,而是通过两者的碰撞,产生1+1>2的价值。可以说,混合云就像打太极拳,将公有云、私有云打通,使两者无缝衔接,让数据在云中按需流动。例如,如何实现虚拟资源在异构虚拟化资源池之间迁移效率的最大化是混合云应用时需要考虑的问题之一。

2.1.13Redis在生产中不得不重视的几个运维问题

Redis 运维的故障有哪些?如何排查?

常见的运维故障

  • 使用 keys * 把库堵死,——建议使用别名把这个命令改名
  • 超过内存使用后,部分数据被删除——这个有删除策略的,选择适合自己的即可
  • 没开持久化,却重启了实例,数据全掉——记得非缓存的信息需要打开持久化
  • RDB 的持久化需要 vm.overcommit_memory=1,否则会持久化失败
  • 没有持久化情况下,主从,主重启太快,从还没认为主挂的情况下,从会清空自己的数据——人为重启主节点前,先关闭从节点的同步

排查方法

  • 了解清楚业务数据流是怎么样的流向

  • 结合 Redis 监控查看 QPS、缓存命中率、内存使用率等信息

  • 确认机器层面的资源是否有异常

  • 故障时及时上机,使用 redis-cli monitor 打印出操作日志,然后分析(事后分析此条失效)

  • 和研发沟通,确认是否有大 Key 在堵塞(大 Key 也可以在日常的巡检中获得)

  • 和组内同事沟通,确实是否有误操作

  • 和运维同事、研发一起排查流量是否正常,是否存在被刷的情况

  • 更多的排查需要对线上系统的分析。

Redis 性能优化

  • 根据不同业务选择数据类型,有必要时对数据结构进行审核,减少数据冗余
  • 精简键名和键值,控制键值的大小
  • 使用前缀管理好 key,防止key的重名导致value被覆盖
  • 使用 scan 代替 keys,将遍历 Redis DB 中所有 key 的操作放到客户端来做
  • 避免使用 O(N) 复杂度的命令
  • 配置使用 ziplist 来优化 list
  • 合理配置 maxmemory
  • 数据量大的情况,做好 key 和 value 的压缩
  • 利用管道,批量处理命令
  • 根据不同业务选择短链接或者长链接
  • 定期使用 redis-cli --big-keys 检测大 Key

2.1.14Redis面试题详解

面试不能脱离业务聊架构,这是耍流氓。

一定要结合你的业务场景,原则就是面试官问的每一个问题都要说,我在开发过程中遇到了这样的问题,是如何解决的?如果某个问题不会,直接就说在生产中没有遇到过,但是我可以说一下思路。

建议大家看这个面试题,但是不要背,准备一点实际的业务

3.系统设计

4.数据库设计

​ 表设计,有些表及它中间包含的数据都是系统运行所必须的,每个系统都差不多,在脚手架工程中一般都会包含。这些表的结构大致不会变化,而且跟你使用的开发语言无关。

​ 表的命名是有规范的,通常来说,根据前缀区别不同的模块,表名一般是下划线。

​ 介绍这样的一些基础表:

sys_file_info:id是文件的md5,这样可以保证同一个文件只会被上传一次,程序在上传的时候如果没有上传成功,并且是因为数据库的主键冲突,这时就要根据该文件的md5值去数据库查询,把已经存在的文件的path或url取出来返回给上传方。path和url的区别,主要是因为系统设计了两套上传逻辑,分别是本地上传和云存储。

sys_logs:日志,日志表会记录一些比较重要的操作。使用日志表的方式比较特殊,用到了spring的aop面向切面的思想,不需要你手动去new,只需要通过切面加自定义的注解,你的某个方法如果想要被记录日志,可以直接在方法上面加一个日志注解,这样,当这个方法被调用的时候,就会顺便往日志表中插一条记录。我们线上的项目的日志打印功能通常都是关闭的,因为磁盘空间永远不够,如果你以debug的方式在线上启动项目,就我们这个业务量不是很大的系统,每天产生的日志文件大概有7GB,我们买的阿里云服务器,磁盘通常都不会太大,当你有一天发现你的系统突然无法访问,也没用被别人攻击,一定去检查一下,是否你的磁盘满了。而且日志表的作用很多,也可以记录一些跟业务相关的比较中亚的信息,例如,我们项目中的物联网采集设备,在开机的时候会发送一条启动成功的信息,每个半分钟会发送一条心跳信息,但这些信息我们在项目里面没有必要去处理,但是有的时候系统会不稳定,需要查看这些日志,来判断是因为什么原因导致系统出问题。开始的时候,是通过开发人员后台查询打印出来的的日志判断是否正常,这样非常不方便,后面开发了一个功能,把这些启动信息,心跳信息都存在日志表中,客户可以在界面直接查看设备是否正常。

权限相关表:不管用什么权限框架,表结构基本一样《用户user、角色role、权限permission》,以及他们之间的多对多映射关系表。在系统中使用非常灵活,对系统的扩展帮助非常大。通过对权限的灵活控制,可以实现很多你想要的业务逻辑,而不需要写一行代码。

stb_area:全国区域规划表

sys_ys7_account:系统要使用其他第三方的系统的一些功能,这时候第三方的系统也需要账号密码登录,为了让我们的系统能顺利地交付给客户使用,这样的第三方系统的账号信息我也可以通过界面维护

t_dict:数据字典表,性别、状态、民族、国籍、组织机构

t_mail ,t_mail_to:系统用来发送邮件的

weixin_*:是跟公众号开发相关的

定时任务表:QRTZ_*:这是使用定时框架quartz所需要的表,通常来说,在网上找的资料,使用quartz框架实现定时任务,不需要数据库表。quartz的表结构的非常重要的作用就在于可以实现分布式的定时任务。

例如:有十个相同的项目被部署,每个项目中都有一段相同的定时任务逻辑,那如果在到达这个执行时间时,这些任务都应该被执行吗?没有必要所有的任务都执行,只需要有一个任务执行成功就可以了,所以我们把任务执行的状态信息保存起来,刚你要去执行某一个任务的时候,先去查看这个状态,如果是 可以执行的状态,那就执行,并且把状态修改为不可执行。其他任务来的时候一看状态不可执行,就继续进行下一次等待。其实,这就是分布式锁,只不过是通过数据库的方式实现的。

https://blog.csdn.net/jiangyu1013/article/details/79882868

t_job:也是quartz框架中需要的一张表,这张表可以记录当前系统所有的定时任务执行情况,并且有可视化的界面进行维护。你可以去动态的指定某个方法什么时候被执行,以及他执行的次数。

Ps:定时任务框架还有SpringTask,我们在项目中也用到了,但没有相关表被使用。

4.8.物联网业务相关表

基地:农场

通知:

设备相关

设计思路:分了两个大的阶段,第一阶段,我们做的是沙盘演示功能,沙盘上面的传感器可以记录温度,湿度,风速,风向等信息,可以进行远程控制,可以显示每个设备的近一段时间的数据折线图。因为这些信息的度量单位,数值上的差异是很大的,统计信息需要根据不同的设备分别进行,而且任务要求时间比较急,客户的意思是先做一个简单的出来(此处客户给我挖了坑),所以我把这些信息都单独建了表,把采集过来的信息根据设备类型直接入库,然后再去做后续的显示,参加了展览会,取得了很大的成功(客户获得了数十个订单);第二阶段,需求变化比较大,因为通过分析发现,采集来的数据除了数值和单位不同,其他信息都是相同的,所以我就进行了数据表的合并,放到了设备采集表中进行统一记录,但是并没有把原来的分表删除,因为考虑到数据量很大的情况下,对应每种设备还是可以单独记录的。

设计中需要注意的问题:对于需求的变更,因为不知道客户需要的到底是什么,会出现某个功能可能已经开发的差不多了,又需要多加字段,造成开发量的增加。还有因为设计的时候不可能考虑的很周到,必然会出现一些字段的变化,也会造成工作量的增加。大家在进行工作量预估的时候,一定要把这些因素都考虑进去。

4.9.数据库管理软件-Navicat使用

Navicat的版本比较多,我们通常会使用可以连接多种数据库的版本Premium。

在公司里面需要连接oracle,mysql,sqlserver,采用这个客户端比较方便。

通过逆向数据库到模型的功能,可以查看表的设计图,如果表与表之间有外键,此处可以看到他们的关联关系,类似于PowerDesinger的界面

这个工具也能帮助我们执行excel数据的导入导出,生成报表

5.业务模块

数据的采集需要物联网设备与web后台进行交互,交互的方式需要一个中介,中介的作用是一个消息队列,可以接收物联网设备的传递过来的信息,并且转发给web设备。中介的实现方式很多种,可以自己搭建对应的平台,也可以采用第三方的平台,从技术实现的难易程度和成本控制上来说,花钱采用第三方的是最经济实惠的。而从技术选型上来说,我们跟设备端的工程师确定共同的一个平台。一期采用的是开发快平台,负责数据采集后的传递,他的功能很强大,在数据传递中不需要考虑粘包,拆包问题。二期准备把平台换成阿里云的,因为阿里云的更稳定,更可靠的。平台本身不会对数据库做任何的处理,只负责消息的收发,硬件程序员将采集的数据发送到平台,而web端对对应的端口进行监听,监听到数据就进行处理,否则一直等待。

任意的第三方平台的接入,考虑的最多的就是对于多语言的支持能力,对于问题的解决速度,以及api的更新要保持节奏。

开发快平台:http://www.kaifakuai.com/

找到到开发者指南:http://developer.kaifakuai.com/

找到开发者工具:微信,A-SDK,HAM,L-SDK,S-SDK

S-SDK 开发向导:http://developer.kaifakuai.com/ssdk/SSDK-Tutorial.html

5.2.2.传感器与开发快交互

很多的云平台都会有一些活动,赠送一些免费的开发板,让你使用它的平台。接下来去购买响应的智能感应设备,这些设备包含温湿度传感器,土壤温湿度、酸碱度、电导率传感器,风速风向传感器,光照传感器,购买回来之后可以安装到开发板上进行测试,每个传感器,板子都有对应的使用说明书,《 485型温湿度变送器使用说明书》,参数的说明,对设备的安装教程,配置软件安装及使用。对于开发人员来说,通信协议非常关键。常见问题及解决办法。

我们的板子需要一台能联网的设备向外发送数据,可以采用安卓系统

传感器安装板子上,板子再去跟安卓系统交互,安卓通过有线或无线信号把数据发送出去,发送到开发快的平台上。

5.2.3.Web程序与开发快交互

安卓的操作需要下载使用A-SDK,具体操作方式见操作手册

Java的操作:

​ 1)下载jar包,并且把他安装在本地的maven仓库

maven安装jar包到本地
mvn install:install-file -DgroupId=com.beidouapp -DartifactId=SSDK -Dversion=4.0.2.0 -Dfile=/Users/szz/IdeaProjects/intelligent_agriculture1/intelligent_agriculture/src/main/resources/lib/SSDK-Release-4.0.2.0.jar -Dpackaging=jar

​ 2)根据手册

BaseConfig.java存放开发快的参数的

这些参数都需要大家自己注册一个账号,会免费提供给你

DOMAIN:这个开发快的平台ip

AppKey

SecretKey

UID

RECEIVE_UID

申请uid:就是一个添加用户的

ETClientAddUser.java用来添加uid

5.2.5.S-SDK开发快开发向导指引

5.2.6.S-SDK跟SpringBoot的整合

1)把开发快提供的api的main函数变成SpringBoot的bean

2)SpringBoot的配置文件

3)导入开发快依赖jar包

注意:现在是自己发自己收,实际上要跟开发快另一端的硬件做交互

​ 硬件发程序收:需要一个一直存在的监听器

​ 程序发硬件收:需要一个控制硬件的入门按钮

5.3.1.沙盘演示及重点知识提要

​ 硬件发送传感器的数据,程序接收到数据进行解码处理,存入数据库,并且在页面上做演示

​ 程序发送远程控制的指令,硬件接收到指令,并且执行相应的操作

需要学习的内容:如何去设计硬件与程序之间的通信协议、页面布局、对硬件传递过来的数据进行解码、对解码的数据处理并存入数据库、再从数据库把数据查询出来按照规定的格式发送到前端、前端接收到数据并且用echarts渲染出来、程序给硬件发指令;

5.3.2.LayUI入门-一个很丑的沙盘控制页面布局

脚手架工程用的前端框架就是layui,需要对layui做入门

建议大家还是采用bootstrap进行布局

对于苹果的手机的自适应问题,后续再讲

5.3.4.如何去设计硬件与程序之间的通信协议

硬件跟程序之间通信就是传递的byte数组,所以需要定义规则进行解析,这里的规则指的就是协议。

协议是由硬件工程师跟软件开发一起制定的

农业项目通信格式(沙盘专用),沙盘采购的传感器跟后续使用的传感器不是一个厂商,所以协议需要单独定制

5.3.5.沙盘数据库表的设计

同一个数据库中,不同的传感器设计出来的表结构除了单位不同,其他都是类似的,同学们可以会考虑把这些数据都放在一张表中,但请不要这样做,因为随着数据量的增加,迟早都要进行数据库的分库分表,为了业务的扩展性,不要放到一起。数据采集15秒就会上报一次。数据库的记录会增长的很快。

5.3.6.对硬件传递过来的数据进行解码(难点)

1)硬件传递过来的数据是byte数组

2)需要我们转换成16进制,

3)然后按通信规则进行解析

demo其实消息在发送前已经转换成了16进制数,所有发送出去的已经是16进制了。接收端只需要new String就能转换回16进制了。

开发快的demo提供了byte[]转16进制String的方法toChangeByHex

但是,我们在开发中,硬件工程师并没有把byte[]转换为16进制,所以,我们在接收到消息后,需要先调用toChangeByHex方法进行转换,然后再进行解析

16进制数打印出来显示的是10进制,但是协议中定义的是0x格式,你可以在程序中定义0x25这样的16进制,但是打印出来也是10进制

System.out.println(0x7b);//打印结果是123

5.3.7.对解码的数据处理并存入数据库

GY39协议说明书

我们定义的协议integers数组的第三个元素指定了我们发送来的数据类型

0x45:温度,气压,湿度,海拔

0x15:光照

跟硬件通信的时候必须遵守协议,协议可以自己定义,但一般传感器的厂商会提供一份解析的文档,通过文档,你就清楚地知道传递过来的数据的每一位分别代表什么。也会告诉你哪几位是什么传感器的值,而且会告诉你计算公式,通过计算公式,在程序里面进行计算,解析,最终存到数据库。

5.3.8.从数据库把数据查询出来按照规定的格式发送到前端

什么叫规定的格式?我怎么知道规定的格式是什么?

根据原型图,发现页面需要显示温度,湿度,pm2.5,气压四种传感器的折线图,折线图包含x轴和y轴,通常x轴代表时间段,每十分钟一个数据,y轴就是需要显示的数据。为了让页面显示十分钟一次,代表十分钟会采集一次数据。

需求深化的挖掘:可以给页面提供一些数据,这些数据的采集间隔是十分钟。页面不可能把设备所有时间段的数据都显示出来,只能显示最近一段时间,什么叫最近?

这个最近需要跟产品进行沟通,看到底在页面显示几条,注意美观度。我们的项目中显示了十条。该怎么给前端显示十条数据呢?

怎么写sql语句?

只需要提供当前时间点之前的10条数据即可

@GetMapping("/findAll")
@ApiOperation(value = "获取最近10个温度数据")
public List<Temperature> findAll() {System.out.println("异步定时访问了数据库:"+new Date());return temperatureDao.findAll();
}

SELECT

FROM
( SELECT * FROM t_temperature t ORDER BY t.ddatetime DESC LIMIT 10 ) t2
ORDER BY
t2.ddatetime

里层表示我们要去从结果集中取最近时间段的10条数据,desc对时间进行逆序排列,limit 10表示只取十条

外层对取出来的逆序数据进行正排

111 20 2018-10-21 00:00:21 11111 2018-10-21 00:00:21 2018-10-21 00:00:21
112 20 2018-10-21 00:00:51 11111 2018-10-21 00:00:51 2018-10-21 00:00:51
113 20 2018-10-21 00:01:06 11111 2018-10-21 00:01:06 2018-10-21 00:01:06
114 20 2018-10-21 00:01:36 11111 2018-10-21 00:01:36 2018-10-21 00:01:36
115 20 2018-10-21 00:01:51 11111 2018-10-21 00:01:51 2018-10-21 00:01:51
116 20 2018-10-21 00:02:06 11111 2018-10-21 00:02:06 2018-10-21 00:02:06
117 20 2018-10-21 00:02:22 11111 2018-10-21 00:02:22 2018-10-21 00:02:22
118 20 2018-10-21 00:02:37 11111 2018-10-21 00:02:37 2018-10-21 00:02:37
119 20 2018-10-21 00:02:52 11111 2018-10-21 00:02:52 2018-10-21 00:02:52
121 25 2018-12-22 23:57:34 11111 2018-12-29 09:28:30 2018-12-29 09:28:30

有了结果集直接发送到前端,在前端进行显示即可

5.3.9.前端接收到数据并且用echarts渲染出来

1)前端发送ajax请求,从后端把数据取回来

​ $.get(url1, null, a, “json”);

数据如何在页面不刷新的情况下自动更新,需要一个定时任务,定时去后端拿数据

var intvalTime=1500000000;//全局定时任务时间,一般情况下设置为10分钟去后台取一次数据

//重复执行某个方法
window.setInterval("$.get(url1, null,a,'json');", intvalTime);

2)要对数据进行处理,处理成echarts需要的格式

ECharts,一个使用 JavaScript 实现的开源可视化库,可以流畅的运行在 PC 和移动设备上,兼容当前绝大部分浏览器(IE8/9/10/11,Chrome,Firefox,Safari等),底层依赖轻量级的矢量图形库 ZRender,提供直观,交互丰富,可高度个性化定制的数据可视化图表。

https://echarts.baidu.com/

3)把数据交给echarts,进行展示

<!--温度-->
<script type="text/javascript">var intvalTime=1500000000;//全局定时任务时间,一般情况下设置为10分钟去后台取一次数据var option1 = {tooltip: {trigger: 'axis'},title: {text: '温度(℃)' //标题},grid: {left: '3%',right: '7%',bottom: '3%',containLabel: true},toolbox: {show: true,feature: {mark: {show: true},dataView: {show: true, readOnly: false},magicType: {show: true, type: ['line', 'bar']},restore: {show: true},saveAsImage: {show: true}}},calculable: true,xAxis: {type: 'category',boundaryGap: false,data: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']},yAxis: {type: 'value',axisLabel: {formatter: '{value}'}},series: [{name: '湿度',type: 'line',stack: '总量',smooth: true,data: [12, 13, 40, 43, 59, 63, 71, 82, 83, 90],markPoint: {data: [{type: 'max', name: '最大值'},{type: 'min', name: '最小值'}]},markLine: {data: [{type: 'average', name: '平均值'}]}}]};// 基于准备好的dom,初始化echarts实例var temperatureChart = echarts.init(document.getElementById('temperatureChartMain'));// 使用刚指定的配置项和数据显示图表。temperatureChart.setOption(option1);var url1 = "../temperatures/findAll";var times1 = [];    //时间数组(实际用来盛放X轴坐标值)var temperatures = [];    //温度数组(实际用来盛放Y坐标值)var a = function (data, status) {if (data != null) {times1 = [];temperatures = [];for (var i = 0; i < data.length; i++) {times1.push(data[i].timeStr);temperatures.push(data[i].t);}//之前option中legend和 XAxis的data,series 为空,所以现在将数据填充进去temperatureChart.setOption({        //加载数据图表xAxis: {data: times1},series: [{type: 'line',data: temperatures}]});}};//第一次打开页面去调用,a代表回调函数$.get(url1, null, a, "json");//重复执行某个方法window.setInterval("$.get(url1, null,a,'json');", intvalTime);</script>
<!--温度-->

5.3.10.程序给硬件发指令实现对传感器的远程控制

1)前端向后端发请求

开关

<tr style="margin: 0 auto;text-align:center"><td><form class="layui-form" action="" name="temperatureForm"><div class="layui-form-item"><label class="layui-form-label"><i class="layui-icon layui-icon-set"style="font-size: 25px; color: #1E9FFF;"></i>&nbsp;灌溉泵</label><input lay-filter="alert_shade_net" type="checkbox" name="alert_shade_net"lay-skin="switch" lay-text="开启|关闭" id="alert_shade_net"></div></form></td>
</tr>
//使用layui的form组件
layui.use('form', function () {var form = layui.form;});
//监听遮光网传感器开关
form.on('switch(alert_shade_net)', function (data) {var alert_value = this.checked ? '1' : '2';$.ajax({type: 'post',url: '/remote_control/switch/shade_net/' + alert_value,beforeSend: function () {index_wx = layer.msg('正在切换中,请稍候', {icon: 16, time: false, shade: 0.8});},error: function (data) {layer.msg('数据异常,操作失败!');},success: function (data) {data = JSON.parse(data);if (data.code == "1") {setTimeout(function () {layer.msg('操作成功');}, 2000);} else {layer.msg(data.message);}},dataType: 'html'});
});

2)后端controller接收到请求

3)根据协议,不同的指令执行不同的操作

1-8分别代表四个传感器的开关指令,正确的做法需要定义协议

4)把指令发送到开发快

@Autowired
private ISDK sdk;@LogAnnotation
@ApiOperation(value = "遮阳网开关")
@PostMapping("/switch/shade_net/{alert_value}")
public ResponseInfo switchShadeNet(@PathVariable Long alert_value) {System.out.println("遮阳网控制:"+alert_value);byte[] msg ={1};if (alert_value==1L){msg=new byte[]{1};}else if(alert_value==2L){msg=new byte[]{2};}System.out.println(msg);final CountDownLatch cdl3 = new CountDownLatch(1);sdk.chatTo(EtContextConfig.RECEIVE_UID, msg, new ICallback<Void>() {@Overridepublic void onSuccess(Void aVoid) {System.out.println("~~~~~~~~~chatTo(" + EtContextConfig.RECEIVE_UID + ")成功");cdl3.countDown();}@Overridepublic void onFailure(Throwable ex) {System.err.println("*********chatTo(" + EtContextConfig.RECEIVE_UID + ")失败. " + ex.getLocalizedMessage());cdl3.countDown();}});return new ResponseInfo("1", "遮阳网操作成功!");
}

5)传感器接收到开发快的指令,执行对应的操作

5.4.1.数据采集模块业务介绍

5.4.2.用户管理功能配置演示

使用管理员可以给当前系统增加用户,增加用户的时候可以给用户指定对应的角色。对于系统来说,现在有三种角色,超级管理员admin,县级管理员user,基地管理员farmer

在开发阶段,按照超级管理员的角色把所有的功能都开发出来,然后在系统设置中的角色页面,可以定制不同的角色能访问到的功能,可以做到方法级别的细粒度的权限控制。

5.4.3.基地数据库表设计

从原型可以得到基地表的部分字段id,地区,基地名称,备注,操作,有这些信息远远不够

根据生活常识,地区应该拆分成省市区三个字段,农场的类型,负责人,手机,基地的建立时间,修改时间

根据添加基地的原型我们发现,常识能得到的字段都有体现。

5.4.4.使用代码生成器完成基地管理功能

有了数据库的表结构,可以非常方便地使用代码生成器完成基本字段的增删改查,包括前端所有的页面

对于特殊的字段:下拉,文本框,时间,文件上传

5.4.5.省市区三级联动插件city-picker的使用

下载:https://github.com/tshi0912/city-picker/releases

使用教程:https://blog.csdn.net/qq_43652509/article/details/84568998

<link rel="stylesheet" type="text/css" href="../../js/city-picker/city-picker.css">
<script type="text/javascript" src="../../js/libs/jquery-2.1.1.min.js"></script>
<script src="../../js/city-picker/city-picker.data.js"></script>
<script src="../../js/city-picker/city-picker.js"></script>
div class='col-md-10'><input class='form-control' placeholder='请选择基地的成立时间' type='text' name='createTime'id='createTime'data-bv-notempty='true'>
</div>
//初始化city-picker省市区三级联动
$("#city-picker1").citypicker();

5.4.6.LayUI中时间控件的使用

https://www.layui.com/doc/modules/laydate.html

<div class='col-md-10'><input class='form-control' placeholder='请选择基地的成立时间' type='text' name='createTime'id='createTime'data-bv-notempty='true'>
</div>
layui.use(['laydate'], function () {var laydate = layui.laydate;laydate.render({elem: '#createTime',type: 'date',//range:true,//开启左右面板,可以进行日期范围的选择//trigger: 'mouseover',//定义鼠标悬停时弹出控件theme: '#393D49',//主题颜色calendar: true//是否显示公历节日});
});

5.4.7.下拉菜单和文本域

此处的下拉,使用的是select+option的方式实现,对于选项固定,不会经常发生变化,不需要从数据库取值的情况,可以采用这样的方案。

layui并没有实现动态生成下拉菜单的功能,只能做静态。对于目前这个需求是满足,如果是动态,我们项目中会采用angularjs动态加载。

5.4.8.完成基地保存功能

layui的时间组件会把时间封装成这样一个字符串:createTime: “2019-01-23”,后端不认识这样的时间,所以需要特殊处理一下。

对于特殊的字段,需要前端处理:var createTime=new Date(“2019-01-23”).getTime();

formdata.createTime=new Date(formdata.createTime).getTime();
//得到citypicker格式的省市区信息  安徽省/芜湖市/鸠江区
var area = (formdata.position).split("/");
formdata.province = area[0];
formdata.city = area[1];
if (area.length>2) {formdata.district = area[2];
}

5.4.9.基地编辑功能-数据回显

编辑页面需要数据回显,需要把主键用隐藏域的方式一起传递到后台

<input type="hidden" id="id" name="id">

日期需要特殊处理,仿照添加页面

省市区联动也需要特殊处理:

要给这个控件赋值:

$("#city-picker2").citypicker("reset");
$("#city-picker2").citypicker("destroy");$("#city-picker2").citypicker({province: data.province,city: data.city,district: data.district
});

5.4.10.传感器数据库表的设计

t_device

t_device_gather

t_device_log

t_device_type

t_measurement_unit

5.4.11.传感器相关代码生成及基础的增删改查

5.4.12.文件上传功能-本地存储、阿里OSS、七牛云

文件上传有多种方式,需要一张本地的文件表存储文件上传的相关信息,使用脚手架的时候,因为脚手架跟业务无关,我们在我们的业务中,可以参考脚手架提供的这个功能

LayUI图片上传方式:

<button type="button" class="layui-btn" id="uploadBtn"><i class="layui-icon"></i>上传文件
</button>

报错的原因,脚手架对文件上传功能做了改造,导致数据库的字段发生变化

新版本是文件上传多实现

工厂类

@Autowired
private FileServiceFactory fileServiceFactory;
@Configuration
public class FileServiceFactory {private Map<FileSource, FileService> map = new HashMap<>();@Autowiredprivate FileService localFileServiceImpl;@Autowiredprivate FileService aliyunFileServiceImpl;@PostConstructpublic void init() {map.put(FileSource.LOCAL, localFileServiceImpl);map.put(FileSource.ALIYUN, aliyunFileServiceImpl);}public FileService getFileService(String fileSource) {if (StringUtils.isBlank(fileSource)) {return localFileServiceImpl;}return map.get(FileSource.valueOf(fileSource));}

文件上传抽象类

@Slf4j
public abstract class AbstractFileService implements FileService {protected abstract FileDao getFileDao();@Overridepublic FileInfo upload(MultipartFile file) throws Exception {FileInfo fileInfo = FileUtil.getFileInfo(file);FileInfo oldFileInfo = getFileDao().getById(fileInfo.getId());if (oldFileInfo != null) {return oldFileInfo;}if (!fileInfo.getName().contains(".")) {throw new IllegalArgumentException("缺少后缀名");}uploadFile(file, fileInfo);fileInfo.setSource(fileSource().name());// 设置文件来源getFileDao().save(fileInfo);// 将文件信息保存到数据库log.info("上传文件:{}", fileInfo);return fileInfo;}/*** 文件来源* * @return*/protected abstract FileSource fileSource();/*** 上传文件* * @param file* @param fileInfo*/protected abstract void uploadFile(MultipartFile file, FileInfo fileInfo) throws Exception;@Overridepublic void delete(FileInfo fileInfo) {deleteFile(fileInfo);getFileDao().delete(fileInfo.getId());log.info("删除文件:{}", fileInfo);}/*** 删除文件资源* * @param fileInfo* @return*/protected abstract boolean deleteFile(FileInfo fileInfo);
}

本地实现类

/*** 本地存储文件<br>* 该实现文件服务只能部署一台<br>* 如多台机器间能共享到一个目录,即可部署多台* * @author 小威老师**/
@Service("localFileServiceImpl")
public class LocalFileServiceImpl extends AbstractFileService {@Autowiredprivate FileDao fileDao;@Overrideprotected FileDao getFileDao() {return fileDao;}@Value("${file.local.urlPrefix}")private String urlPrefix;/*** 上传文件存储在本地的根路径*/@Value("${file.local.path}")private String localFilePath;@Overrideprotected FileSource fileSource() {return FileSource.LOCAL;}@Overrideprotected void uploadFile(MultipartFile file, FileInfo fileInfo) throws Exception {int index = fileInfo.getName().lastIndexOf(".");// 文件扩展名String fileSuffix = fileInfo.getName().substring(index);String suffix = "/" + LocalDate.now().toString().replace("-", "/") + "/" + fileInfo.getId() + fileSuffix;String path = localFilePath + suffix;String url = urlPrefix + suffix;fileInfo.setPath(path);fileInfo.setUrl(url);FileUtil.saveFile(file, path);}@Overrideprotected boolean deleteFile(FileInfo fileInfo) {return FileUtil.deleteFile(fileInfo.getPath());}
}

阿里云实现类,需要导入oss的pom依赖

/*** 阿里云存储文件* * @author 小威老师**/
@Service("aliyunFileServiceImpl")
public class AliyunFileServiceImpl extends AbstractFileService {@Autowiredprivate FileDao fileDao;@Overrideprotected FileDao getFileDao() {return fileDao;}@Overrideprotected FileSource fileSource() {return FileSource.ALIYUN;}@Autowiredprivate OSSClient ossClient;@Value("${file.aliyun.bucketName}")private String bucketName;@Value("${file.aliyun.domain}")private String domain;@Overrideprotected void uploadFile(MultipartFile file, FileInfo fileInfo) throws Exception {ossClient.putObject(bucketName, fileInfo.getName(), file.getInputStream());fileInfo.setUrl(domain + "/" + fileInfo.getName());}@Overrideprotected boolean deleteFile(FileInfo fileInfo) {ossClient.deleteObject(bucketName, fileInfo.getName());return true;}}

文件上传工具类

public class FileUtil {public static FileInfo getFileInfo(MultipartFile file) throws Exception {String md5 = fileMd5(file.getInputStream());FileInfo fileInfo = new FileInfo();fileInfo.setId(md5);// 将文件的md5设置为文件表的idfileInfo.setName(file.getOriginalFilename());fileInfo.setContentType(file.getContentType());fileInfo.setIsImg(fileInfo.getContentType().startsWith("image/"));fileInfo.setSize(file.getSize());fileInfo.setCreateTime(new Date());return fileInfo;}/*** 文件的md5* * @param inputStream* @return*/public static String fileMd5(InputStream inputStream) {try {return DigestUtils.md5Hex(inputStream);} catch (IOException e) {e.printStackTrace();}return null;}public static String saveFile(MultipartFile file, String path) {try {File targetFile = new File(path);if (targetFile.exists()) {return path;}if (!targetFile.getParentFile().exists()) {targetFile.getParentFile().mkdirs();}file.transferTo(targetFile);return path;} catch (Exception e) {e.printStackTrace();}return null;}public static boolean deleteFile(String pathname) {File file = new File(pathname);if (file.exists()) {boolean flag = file.delete();if (flag) {File[] files = file.getParentFile().listFiles();if (files == null || files.length == 0) {file.getParentFile().delete();}}return flag;}return false;}public static String getPath() {return "/" + LocalDate.now().toString().replace("-", "/") + "/";}/*** 将文本写入文件** @param value* @param path*/public static void saveTextFile(String value, String path) {FileWriter writer = null;try {File file = new File(path);if (!file.getParentFile().exists()) {file.getParentFile().mkdirs();}writer = new FileWriter(file);writer.write(value);writer.flush();} catch (IOException e) {e.printStackTrace();} finally {try {if (writer != null) {writer.close();}} catch (IOException e) {e.printStackTrace();}}}public static String getText(String path) {File file = new File(path);if (!file.exists()) {return null;}try {return getText(new FileInputStream(file));} catch (FileNotFoundException e) {e.printStackTrace();}return null;}public static String getText(InputStream inputStream) {InputStreamReader isr = null;BufferedReader bufferedReader = null;try {isr = new InputStreamReader(inputStream, "utf-8");bufferedReader = new BufferedReader(isr);StringBuilder builder = new StringBuilder();String string;while ((string = bufferedReader.readLine()) != null) {string = string + "\n";builder.append(string);}return builder.toString();} catch (IOException e) {e.printStackTrace();} finally {if (bufferedReader != null) {try {bufferedReader.close();} catch (IOException e) {e.printStackTrace();}}if (isr != null) {try {isr.close();} catch (IOException e) {e.printStackTrace();}}}return null;}}

5.4.13.本地上传流程梳理

1)前端点击按钮进行文件上传

2)controller方法中通过参数fileSource来判断注入哪个文件上传的实现类?

3)fileServiceFactory根据你传入的fileSource帮你生成对应的实现类,如果你不传,就用LocalFileServiceImpl,如果传了,根据类型去map中找

4)给filIInfo添加对应的属性

5)FileUtil.saveFile(file, path);

6)抽象类AbstractFileService调用upload方法把fileInfo中的信息存入了数据库

7)controller最终把fileInfo信息返回给前端

遗留问题,返回了的url无法将图片显示出来?

5.4.14.本地上传后根据图片的url无法访问图片的bug解决

在yml中配置
path代表文件在本地存储的位置
urlPrefix代表能通过浏览器访问的url服务器,通常是一个nginx服务器
拷贝的这个path目录就是你本地文件服务器的目录,配置文件服务器file:local:path: /Users/szz/IdeaProjects/cloud-service/localFileprefix: /staticsurlPrefix: http://localhost:9001/files

备注,通过SpringBoot的tomcat容器应该也是可以访问,需要大家自己研究

5.4.15.阿里云OSS-对象存储流程梳理演示

​ 海量、安全、低成本、高可靠的云存储服务,提供99.9999999999%的数据可靠性。使用RESTful API 可以在互联网任何位置存储和访问,容量和处理能力弹性扩展,多种存储类型供选择全面优化存储成本。

创建oss的桶时(bucket),选择公共读,默认是私有的,因为我们上传的图片是不需要鉴权就可以访问

oss的配置类

import com.aliyun.oss.OSSClient;
import com.aliyun.oss.model.PutObjectResult;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.net.URL;
import java.util.Date;/*** 阿里云配置* * @author 小威老师**/
@Configuration
public class AliyunConfig {@Value("${file.aliyun.endpoint}")private String endpoint;@Value("${file.aliyun.accessKeyId}")private String accessKeyId;@Value("${file.aliyun.accessKeySecret}")private String accessKeySecret;/*** 阿里云文件存储client* */@Beanpublic OSSClient ossClient() {OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret);return ossClient;}public static void main(String[] args) throws FileNotFoundException {OSSClient ossClient = new OSSClient("oss-cn-beijing.aliyuncs.com", "LTAI3jTQMjLamd0v", "aOR1ZFUoJCKmiSUUQopZcwZDu0uei6");InputStream inputStream = new FileInputStream("D://ssfw.sql");ossClient.putObject("topwulian", "upload/" + "ss11fw.sql", inputStream);Date expiration = new Date(new Date().getTime() + 3600l * 1000 * 24 * 365 * 10);// 生成URLURL url = ossClient.generatePresignedUrl("topwulian", "upload/" + "ss11fw.sql", expiration);System.out.println(url);}}
@Autowired
private OSSClient ossClient;@Value("${file.aliyun.bucketName}")
private String bucketName;
@Value("${file.aliyun.domain}")
private String domain;@Override
protected void uploadFile(MultipartFile file, FileInfo fileInfo) throws Exception {ossClient.putObject(bucketName, fileInfo.getName(), file.getInputStream());fileInfo.setUrl(domain + "/" + fileInfo.getName());
}@Override
protected boolean deleteFile(FileInfo fileInfo) {ossClient.deleteObject(bucketName, fileInfo.getName());return true;
}

参考:https://blog.csdn.net/xinanrusu/article/details/52847134

前端新增设备的时候,需要上传一张设备图片,默认如果不传参数,会存到本地,传参就存到oss。

5.4.16.AngularJS入门

对于页面上的一些特殊功能,如下拉框,layui是不支持动态加载的,可以使用jquery或者原生js来实现

Js动态生成下拉框:https://blog.csdn.net/weixin_44578470/article/details/86566297

也可以采用前端框架去更方便地实现这一功能,使用前端框架可以极大地减少代码量

目前给大家介绍Angularjs,目前最老的Angularjs依然在维护,版本1.5.8。至于Angular从2开始,版本不向下兼容,而且目前企业在用的,依然是1.几的版本比较多,而vue的设计完全参考了Angularjs1.几。

而学习Angularjs的目的是为vue学习打基础,他的特性双向数据绑定,路由机制,指令,模块化,MVC,在vue中依然适用。

入门的方式建议从菜鸟教程网进行学习:https://www.runoob.com/angularjs/angularjs-tutorial.html

5.4.16.使用AngularJS生成动态的下拉框

1)

2)

3)js

<script  type="text/javascript">var app=angular.module('ia',[]);app.controller('addDeviceController',function ($scope,$http) {//获取设备类型列表,获取用户列表,获取农场列表$scope.getDeviceTypeList=function () {$http.get('/deviceTypes/all').then(function (response) {$scope.deviceTypeList=response.data.deviceTypeList;$scope.farmList=response.data.farmList;$scope.userList=response.data.userList;});}$scope.device={installTime:''}$scope.add=function () {var formdata = $("#form").serializeObject();//处理一下日期字符串,不然后台无法封装$scope.device.installTime=new Date(formdata.installTime).getTime();$http.post('/devices',$scope.device).then(function (response) {location.href = "deviceList.html";})}});
</script>

4)后端

@GetMapping("/all")
@ApiOperation(value = "全部的设备类型列表")
public Map<String,Object> getAllDeviceType(){List<User> userList = userDao.list(null, null, null);List<Farm> farmList = farmDao.list(null, null, null);List<DeviceType> deviceTypeList = deviceTypeDao.getAllDeviceType();Map<String,Object> map=new HashMap<>();map.put("userList",userList);map.put("farmList",farmList);map.put("deviceTypeList",deviceTypeList);return map;
}

5)html页面

<div class='form-group'><label class='col-md-2 control-label'>设备类型</label><div class='col-md-10'><select name="typeId" ng-model="device.typeId" ng-options="deviceType.id as deviceType.name for deviceType in deviceTypeList" lay-verify="required" class='form-control input-sm' data-bv-notempty='true'><option value="">请选择一个设备类型</option></select></div>
</div>
<div class='form-group'><label class='col-md-2 control-label'>农场位置</label><div class='col-md-10'><select name="farmId" ng-model="device.farmId" ng-options="farm.id as farm.name for farm in farmList" class='form-control input-sm'><option value="">请选择您的农场</option></select></div>
</div>
<div class='form-group'><label class='col-md-2 control-label'>拥有者</label><div class='col-md-10'><select name="userId" lay-verify="required" ng-model="device.userId" ng-options="user.id as user.username for user in userList" class='form-control input-sm' data-bv-notempty='true'  data-bv-notempty-message='用户不能为空'><option value="">请选择您的主人</option></select></div>
</div>

5.4.17.前端提交日期到后台接收的几种处理方式总结

其实是springmvc的一个问题,springmvc对于日期的参数封装是有要求的,如果你的日期是2019-05-30,后端使用Date createTIme遍历接收,springmvc默认会报错,有几种解决方案

1)改前端,前端提交的日期格式变为2019/05/30

2)也是改前端,$scope.device.installTime=new Date(formdata.installTime).getTime();

3)改后端,使用注解@JsonFormat和@DateTimeFormat的作用:https://blog.csdn.net/wddbq/article/details/79632534

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date installTime;

4)改后端,配置全局的过滤器,当接收的参数是时间时,就按规定的规则转换,说实话,这种方案用的很少,尤其在springboot项目中,本人看了多个案例,也配置了多次,发现很不方便,而且会有性能问题,应该去局部处理:https://blog.csdn.net/chenxidong9/article/details/82865786

(全局性的修改)

application.yml中修改默认的配置,修改字符串的格式:spring.jackson.date-formate = yyyy-MM-dd HH:mm:ss

这样修改后会有8小时的时差,还需要如下的配置,修改时区spring.jackson.time-zone = GMT+8

学习这么多种解决方案的目的,是为了应对变幻莫测的开发生涯,你会遇到不同的场景,不同的同事,需要你去灵活运用。

一个好的程序员,对于同一个问题,至少应该掌握三种以上的解决方案,这才是合格的。

5.4.19.设备管理“卡片式页面”的制作:本页面的制作时间对于大家来说可能需要2个小时,甚至更长

数据库的一些字段,如果说没想到的字段先放着不管,后续做项目用到了可以随时做数据库的变更

layui的布局,跟bootstrap布局方式一致,都是栅格化处理

https://www.layui.com/doc/element/layout.html

对于页面布局来说,对于大部分的后端开发程序员来说比较困难,但是如果你自己做项目,这些布局也都是需要自己搞定的,因为对于客户来说,他认为你能帮他完成这个项目。我的理解,一个合格的程序员,应该是前后端通吃,包括切图设计。对于大公司来说,岗位会划分地比较细,但是对于大部分人来说,可能需要在工作中承担的更多。

5.4.20.设备管理“卡片式页面”数据展示

根据不同的设备分类,分别去把该分类下在线离线状态的设备数量统计出来,并且在前端页面做展示

依然采用Angularjs进行开发

1)导入js

2)定body为Angularjs的作用域

3)初始化调用一下加载数据的函数 ng-init=“devicesState()”

4)发送一个ajax请求,通过$http的对象,注意,要使用这个对象,需要进行注入

app.controller('deviceManageController',function ($scope,$http) {
$http.get('../../devices/devicesState').then(function (response) {

5)从后端拿到数据后(设备列表和摄像头的列表),进行统计的操作,分别计算在线和离线设备的数量

当然这个不是最优选择,最优选择应该是在后端通过sql查询把数据统计出来然后直接给前端返回,留给大家当成作业完成。

6)完善后端的接口,调用dao查询数据,封装到map中返回

7)注意,如果前端需要的数据是从不同表中查询出来的,应该在后端封装到一个方法中进行返回,否则前端会发送多次的ajax请求,影响性能

5.4.21.小老弟,有考虑过物联网项目为啥不采用前后端分离开发的原因吗?

大家的普遍看法,为什么不采用前后端分离开发,觉得没有分离的项目很low。前后端分离的项目也就这几年比较多,也比较火,之前我们的开发,都是一个web工程,开发完毕后统一部署到Tomcat中。

原因非常简单,我们不是互联网项目,我们没有独立的运维,我们自己也不是运维,我们最终是要把产品交付给客户的,而客户通常没有自己的技术团队。物联网项目通常是传统行业的有钱人有想法去做,他又不懂互联网公司的氛围。

需要教会客户如果简单部署,我们不参与后期维护。即使维护,后期也是需要收费的。因为客户不懂技术,有一点点小问题都会找你,相对来说,维护成本特别高。所以,我就设计为整体运行的项目。

当然进行前后端拆分也是比较容易实现的,在农业电商项目中,我们就会采用这样的方案去实现。

为了不增加运维成本,你可能有专门的一个人一天啥也不干,需要给客户解决问题。

尽量让运维简单化,能部署一个项目解决绝对不把他弄的很复杂。项目不可能购买很多的服务器资源去支持某一个客户。

如果你自己的团队专门做产品,那我非常建议你进行前后端分离开发。但是从节约成本的角度出发,前端工程师不要招太多,我们主要采用的方式是后端开发参与简单前端的代码编写,复杂功能交给前端工程师。

希望大家能从成本最小化的角度去考虑问题。不要任性,不要认为只是个敲代码的,别的东西与你无关!

人终究是会成长的。

5.4.22.我的基地页面涉及的表设计修改

农情通知表t_notice:上级可以下发通知,自己的基地也可以发布一些预警消息

​ 有一个status的状态字段,该字段去表明是否已读。

农情通知阅读人表t_notice_read:可以知道哪些人读过了该通知

农事活动表:放在农事活动的功能再讲

设备日志表t_device_log:可以记录下设备采集到的异常数据,并且提供对外报警的功能

5.4.23.我的基地页面制作

5.2.24.我的基地基本信息展示

我的基地页面很复杂,也需要大量的业务数据数据支持,正常的开发顺序是先去讲各自模块,然后整合到这个页面中

1)

2)对于html页面进行了数据的绑定

3)getFarmInfo()这个方法就是当打开页面被初始化调用的

4)后端

GetMapping("/info/{noticeRows}/{deviceLogRows}")
@ApiOperation(value = "我的基地页面显示各种详情")
public FarmDto farmInfo(@PathVariable Integer noticeRows,@PathVariable Integer deviceLogRows){FarmDto farmDto=new FarmDto();User user = (User) SecurityUtils.getSubject().getPrincipal();user.setPassword(null);farmDto.setUser(user);//农情通知,显示最近条数,由前端传入List<Notice> noticeList = noticeDao.recentlyNotice(0, noticeRows);//设备报警List<DeviceLog> deviceLogList = deviceLogDao.deviceNotice(0, deviceLogRows, user.getId());//根据用户id获取拥有的基地,在farm_user表中配置了用户和基地的关系//前端默认显示用户拥有的第一个基地,可以通过下拉列表来切换基地//设备信息//基地id默认就是当前拥有的第一个基地,根据前端传递过来的farmId进行切换//根据用户获取拥有的农场Long farmId = null;List<Farm> farmList = farmDao.listByUserId(user.getId());if (farmList != null && farmList.size() > 0) {Farm farm = farmList.get(0);farmId=farm.getId();farmDto.setFarm(farm);List<Device> deviceList = deviceDao.getByFarmId(farmId);//当前基地的设备实时数据List<DeviceGather> deviceGathers=deviceService.getRealTimeDataByFarmId(farmId);//当前基地的摄像头列表List<Vedio> vedioList=vedioDao.getVedioListByFarmId(farmId);farmDto.setDeviceList(deviceList);farmDto.setRealTimeDataList(deviceGathers);farmDto.setVedioList(vedioList);}farmDto.setRecentlyNoticeList(noticeList);farmDto.setDeviceNoticeList(deviceLogList);return farmDto;
}
public class FarmDto{User user;Farm farm;//农情通知List<Notice> recentlyNoticeList;//设备报警List<DeviceLog> deviceNoticeList;//当前农场的设备列表信息List<Device> deviceList;//设备实时数据List<DeviceGather> realTimeDataList;//摄像头列表List<Vedio> vedioList;List<Farm> farmList;Map<Long,List<DeviceGather>> deviceGatherMap=new HashMap<>();

在权限框架shiro中如何获取当前登录的用户信息?

​ User user = (User) SecurityUtils.getSubject().getPrincipal();

5.4.25.农情通知基本增删改查制作-富文本编辑器的使用

<textarea class="layui-textarea" id="demo" style="display: none">
</textarea>
var pro = window.location.protocol;
var host = window.location.host;
var domain = pro + "//" + host;var layedit, index;
layui.use(['layedit','upload'], function(){layedit = layui.layedit;layedit.set({uploadImage: {url: '/files/layui?domain=' + domain,type: 'post'}});index = layedit.build('demo');
});

富文本编辑的技术有很多种,UEditor(百度),KindEditor,CKEditor,Froala Editor

不要看技术这么多,其实实现方案都是类似的,他们的原理都是可以把一个文本域渲染成一个富文本的编辑框,使用步骤如下:

1)导入js,以及css,img

2)在页面定义一个文本域,并起个id

3)在js代码中根据不同方案的文档写不同的代码进行渲染

4)如果涉及到了图片的上传,这时就设置一下图片上传的路径

5.4.26.我的基地页面中的农情通知展示

1)如何查看后端返回的数据的结构?

2)在angularjs的代码中如何定义全局变量提供给页面显示?

​ $scope.recentlyNoticeList = response.data.recentlyNoticeList;

3)如何使用layui的不同样式对行显示不同的颜色?

<td><i class="layui-icon layui-icon-praise"></i><span class="{{titleColorCssClass[$index]}}">{{recentlyNotice.title}}</span></td>

想要显示不同的颜色,只需要给class赋予不同的值,在layui中有三个值:‘first’, ‘second’, ‘third’

先在js中定义了一个颜色的数组:$scope.titleColorCssClass = [‘first’, ‘second’, ‘third’];

然后页面上根据遍历出来的每一行的索引$index去把数组的值赋予class

4)angularjs的元素遍历

<tr ng-repeat="deviceNotice in deviceNoticeList"><td><i class="layui-icon layui-icon-praise"></i><span class="{{titleColorCssClass[$index]}}">[{{deviceNotice.deviceName}}]{{deviceNotice.content}}</span></td><td><i class="layui-icon layui-icon-log">{{deviceNotice.createTime}}</i></td>
</tr>

通过ng-repeat进行遍历,in后面指的是你的数据列表,in前面的就是遍历的每一个对象

可以使用{{}}去显示对象中的每一个属性,可以使用$index显示列表的索引

5.4.27.传感器设备类型

注意点:原型中没有出现的一些基础数据,也是需要进行维护的,比如我们打开添加设备页面,发现需要进行设备类型的选择,所以我们需要维护这张表

5.4.28.传感器阈值设定

这也是传感器的一个基础数据功能,我们可以在系统中设置传感器采集到的数据的最小最大值,如果超出这个范围,我们就认为是一个极端恶劣天气,需要给基地管理人员及时报警(短信,电话,邮件),并且将异常信息记录到设备日志表中

<body ng-app="ia" ng-controller="deviceThresholdController" ng-init="devices()">。。。  <table id="dt-table" class="table table-striped table-bordered table-hover" style="width:100%"><thead><tr><th>设备ID</th><th>设备序列号</th><th>设备名称</th><th>最小阈值</th><th>最大阈值</th><th>更改</th></tr></thead><tbody><tr ng-repeat="device in deviceList"><th>{{device.id}}</th><th>{{device.sn}}</th><th>{{device.name}}</th><th><input type="text" ng-model="device.thresholdMin"></th><th><input type="text" ng-model="device.thresholdMax"></th><th><button class="btn btn-primary" type="submit" ng-click="updateDeviceThreshold(device)"><i class="fa fa-save"></i> 阈值修改</button></th></tr></tbody></table>
<table id="dt-table" class="table table-striped table-bordered table-hover" style="width:100%"><thead><tr>
<th>设备ID</th>
<th>设备序列号</th>
<th>设备名称</th><th>最小阈值</th><th>最大阈值</th><th>更改</th></tr></thead><tbody><tr ng-repeat="device in deviceList"><th>{{device.id}}</th><th>{{device.sn}}</th><th>{{device.name}}</th><th><input type="text" ng-model="device.thresholdMin"></th><th><input type="text" ng-model="device.thresholdMax"></th><th><button class="btn btn-primary" type="submit" ng-click="updateDeviceThreshold(device)"><i class="fa fa-save"></i> 阈值修改</button></th></tr></tbody></table>

angularjs使用步骤梳理:

1)导入js

2)在html代码中指定一个作用域,这个作用域的目的是为了能让angularjs操作页面中的元素

3)ng-init 页面加载的时候会执行里面的内容,可以是函数,也可以是表达式,多个内容用分号隔开

4)ng-repeat="device in deviceList"是遍历一个集合,可以取到每个对象中的属性

5)

6)在js代码中定义作用域

7)$scope.deviceList代表是 一个全局变量,可以在页面中自由使用

5.4.29.数据采集功能说明

跟沙盘的 数据采集不同,传感器变了;在目前的采集中,需要考虑性能问题,而沙盘不需要,因为沙盘只做演示。

首先采集到的数据不会直接处理,而是通过消息队列发送到队列中排队,然后再去依次消费

其次,对于采集来的数据,需要给他设置单位、阈值,这些数据我们不会从数据库比对,我们需要放入缓存,当采集来数据的时候,我们就跟缓存中的数据进行比较

再次,当数据库的数据做了修改,需要同时把缓存中的数据一起更新

5.4.30.数据采集接口设计

​ 通信协议跟沙盘的协议是不同的,因为采购了新的传感器,跟之前的是不同品牌的,导致需要重新定义接口。所以要求各位,在项目调研阶段,就需要把所需要的传感器版本定义下来,不然后期项目开发中或上线后,再去更改协议是非常困难的,因为每个厂家的协议完全不同。

我们的协议都是由硬件工程师定义的,因为硬件都是他去采购安装调试的。

通信协议有:传感器数据上传、状态上传、文件/图片上传、后台控制指令、后台主动获取设备状态

5.4.31.数据采集表结构设计

通过原型可以了解到应该有哪些字段?

t_device_gather设备采集表

​ 对于设备相关的信息,我们在本表中进行了冗余,因为设备的信息在采集表中总是会用到,不要去进行连表查询,而且我也把设备信息都存到了redis中,在采集到数据后,会从redis中拿到相关设备的详细信息

t_device_log设备数据异常日志表

5.4.32.数据采集设备端实现介绍

硬件分两大部分,一部分是传感器,一部分是控制器,他们通过基站或者AP网关与通信管理服务器(开发快)进行交互,软件端也与开发快进行交互,得到数据存入数据库,也可以发送控制指令。数据库的采集数据可以在云平台进行显示,为后续的农业专家系统分析提供信息支持。

5.4.33.数据采集Java端解码-同步实现方案

传感器端工程师会按照之前定义的协议去上传数据,我们接收到开发快发送来的数据后,需要按照指定的规则去存储,采集到数据后,立即将数据解析并且存入数据库,这叫同步实现方案,优点是逻辑简单,不会引入太多复杂的技术,就是对数据流做操作,缺点是操作数据库的动作比较耗时,如果说传感器很多,会造成数据拥堵,甚至于服务的不可以。

前置要求:首先要把开发快平台调通,请参考前面给大家演示的案例

@Override
public void onMessage(String topic, byte[] payload) {try {System.out.println("~~~~~~~~~~~~~topic=" + topic + ". payload=" + new String(payload,"UTF-8"));} catch (UnsupportedEncodingException e) {e.printStackTrace();}// 此处编写您的业务代码,注意不要在此处出现耗时操作,如果消息量大时,会占用服务器内存且出现阻塞超时等问题。//7b3007160000000000022fbf58d9saveData(payload);//此处采集到的数据,直接发送到消息队列中,由消息队列处理存入数据库的操作//jmsMessagingTemplate.convertAndSend(QUEUE_GATHER, payload);
}

采集来的数据需要进行计算才能存到数据库

if (strings[27].startsWith("f")) {basicData = ((Integer.valueOf(strings[27] + strings[28], 16).shortValue()) / 256 - 1);
} else {basicData = Integer.valueOf(strings[27] + strings[28], 16);
}

其实数据如果是两位,并且会有正负,需要计算,因为数据是16进制的,需要进行判断是正数还是负数,如果是负数有负数的处理逻辑,正数有正数的处理逻辑

5.4.34.对数据的解析演示

把字符串变成两位两位的字符串数组

public static String[] handleStr2ArrStr(String message){//去空格message = message.replace(" ", "");//先判断数据是否完整,完整可以正确处理,如果不完整尝试进行处理,看是否能解析一部分//先转换成char数组char[] chars = message.toCharArray();List<String> list = new ArrayList<>();String tmp = "";for (int i = 0; i < chars.length; i++) {if (i%2==0&&i>0) {//System.out.println(tmp);list.add(tmp);tmp = "";}tmp+=chars[i]+"";if (i==chars.length-1) {list.add(tmp);}}System.out.println();String[] strings = list.toArray(new String[list.size()]);return strings;}

把字符串数组变成整型数组,变完是16进制的,debug看到的是10进制的

public static Integer[] handleStr2ArrInt(String[] strings){Integer[] integers =new Integer[strings.length];for (int i = 0; i < strings.length; i++) {integers[i]=Integer.valueOf(strings[i],16);}return integers;
}

5.4.35.数据采集瓶颈分析及优化思路剖析

1)开发快在onMessage方法中有一行提示,// 此处编写您的业务代码,注意不要在此处出现耗时操作,如果消息量大时,会占用服务器内存且出现阻塞超时等问题。

目前我们的实现方案需要在此处操作数据库,这是一个耗时的操作。当农场越来越多的时候,传感器也越来越多,那这是此处就是一个瓶颈,需要优化

2)代码逻辑对于消息的处理是写死的,是硬编码的,不利于扩展,需要优化

3)我们方法体中会对消息进行解析,并且每一个传感器的数据都会访问dao,并且存到数据库,对于同一批数据,我们是每个传感器单独保存的,其实对于一批数据来说,可以使用批量导入,需要优化

4)我们的传感器数据在存到数据库的时候,需要读取传感器的信息和单位信息,这些信息也需要从数据库取,但数据量太大,会造成数据库的极大压力,需要优化

强调一下:一定是先实现功能再去考虑系统的优化问题,不要过度设计,过度优化,要结合实际场景

5.4.36.数据采集Java端解码-使用消息队列异步实现方案

思路:让获取消息的方法不要去处理此消息,而是把这个方法当成一个消息的中转站,消息到来后,直接转发出去,在其他地方对这个消息进行处理。

此处发送消息的方案会采用消息队列来实现,“消息”是在两台计算机间传送的数据单位,消息在传输过程中,需要一个容器来保存他,这个容器叫消息队列

Java常用消息队列原理介绍及性能对比

ActiveMQ:基于java的jms协议

RabbitMQ:基于amqp协议

Kafka:在大数据领域用的较多,因为他处理消息的速度特别特别快

Rocketmq:

选型最后总结:

如果我们系统中已经有选择 Kafka,或者 RabbitMq,并且完全可以满足现在的业务,建议就不用重复去增加和造轮子。
可以在 Kafka 和 RabbitMq 中选择一个适合自己团队和业务的,这个才是最重要的。但是毋庸置疑现阶段,综合考虑没有第三选择。

5.4.37.ActiveMQ入门-发送消息机制的介绍

消息队列的几种角色:消息队列是系统之间发消息的,会有消息的发送者(生产者),会有消息的接受者(消费者),还有一个用来存储并且发送消息的容器,这个容易叫消息队列

ActiveMQ发消息的方式有两种:Queue(点对点),Topic(广播)

点对点的模型一条消息只能被一个消费者消费

对于topic模式,一条消息经过topic处理后,可以被多个消息者消费

5.4.38.ActiveMQ入门-ActiveMQ环境搭建

http://activemq.apache.org/从阿帕奇的官方下载,ActiveMQ的环境搭建依赖于JDK,建议使用1.8

解压缩就能用,执行bin文件夹下面的可执行文件

➜ ~ cd /Users/szz/Downloads/apache-activemq-5.15.9/bin/macosx

➜ macosx ./activemq start
Starting ActiveMQ Broker…

可以打开它的管理界面http://localhost:8161/ 账号密码都是admin

搭建ActiveMQ时用windows电脑可能出现的问题,电脑中安装了jdk1.7和1.8,导致ActiveMQ启动的时候找到了1.7的jdk,那这是不兼容的会报错,最好的办法就是把1.7卸载。如果你对jdk1.7不能卸载,请把1.7的文件夹复制一份,然后去控制面板进行卸载,卸载完毕后解压缩1.7,这样就可以保留了。

还可能出现的问题,是你的计算机名字有中划线“-”,这时也会报错,解决办法把计算机名字改掉

5.4.39.ActiveMQ入门-ActiveMQ跟SpringBoot整合发送接收Queue

很多教程中,会介绍原生demo,原生demo在实际开发中不会用到,而且代码量比较大,而且不好理解,也没什么用,所以我们课程不介绍。我们的课程主打实战,所以只会介绍跟SpringBoot整合的方案。

新建一个maven项目,导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.ia</groupId><artifactId>springboot-jms</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.4.RELEASE</version><relativePath/></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-activemq</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

在发送时,只需要指定发送目标的字符串即可,不需要Destination这个对象,直接用字符串。

package jms;import javax.jms.Destination;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Component;/*** @Author: szz* @Date: 2019/6/8 下午7:57* @Version 1.0*/
@Component
public class Producer {@AutowiredJmsMessagingTemplate jmsTemplate;public void sendMessage(String destination,String message) {jmsTemplate.convertAndSend(destination,message);}
}
package jms;import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;/*** @Author: szz* @Date: 2019/6/8 下午7:59* @Version 1.0*/
@Component
public class Consumer {@JmsListener(destination = "mytest.queue")public void receiveQueue(String text){System.out.println(text);}
}
package jms;import javax.jms.Destination;import org.apache.activemq.command.ActiveMQQueue;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;/*** @Author: szz* @Date: 2019/6/8 下午8:02* @Version 1.0*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestJms {@Autowiredprivate Producer producer;@Testpublic void sendMessage()throws  Exception{producer.sendMessage("mytest.queue","你好,ActiveMQ");}
}

application.yml

spring:activemq:broker-url: tcp://localhost:61616in-memory: true
package jms;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/*** @Author: szz* @Date: 2019/6/8 下午8:01* @Version 1.0*/
@SpringBootApplication
public class App {public static void main(String[] args) {SpringApplication.run(App.class,args);}
}

5.4.40.ActiveMQ跟SpringBoot整合的双向队列

@JmsListener(destination = "mytest.queue")
@SendTo("out.queue")
public String receiveQueue(String text){System.out.println(text);return "消息已收到,over";
}
@JmsListener(destination = "out.queue")
public void consumerMessage(String message){System.out.println(message);}

5.4.41.ActiveMQ入门-ActiveMQ跟SpringBoot整合发送接收Topic

默认情况下@JmsListener不能监听topic,需要添加工厂类

package jms;import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.command.ActiveMQTopic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;import javax.jms.ConnectionFactory;@Configuration
@EnableJms //启用jms功能
public class ActiveMqConfig {//如果要使用topic类型的消息,则需要配置该bean@Bean("jmsTopicListenerContainerFactory")public JmsListenerContainerFactory jmsTopicListenerContainerFactory(ConnectionFactory connectionFactory){DefaultJmsListenerContainerFactory factory= new DefaultJmsListenerContainerFactory();factory.setConnectionFactory(connectionFactory);factory.setPubSubDomain(true); //这里必须设置为true,false则表示是queue类型return factory;}}
public void sendMessageTopic(ActiveMQTopic destination,String message) {jmsTemplate.convertAndSend(destination,"这是一个广播");
}
@JmsListener(destination = "mytest.topic",containerFactory = "jmsTopicListenerContainerFactory")
public void receiveToic(String text){System.out.println(text);
}
@Test
public void sendMessageTopic()throws  Exception{ActiveMQTopic destination = new ActiveMQTopic("mytest.topic");producer.sendMessageTopic(destination,"你好,ActiveMQ");
}

刚才出现了一个问题,在topic消费后也返回了一个提示信息,但是报错了

对于点对点消费者来说, 我可以给发送者回一条消息,告诉他我收到消息了,但是对于topic,发送者对接受者是否接受到消息不感兴趣,所以在发送者如果回送消息的时候会报错。

最后一个比较重要的知识点:对于queue来说,我们的消息发送出去,会一直保存在服务器上,等待消费者消费他,如果此时没有消费者在线,会一直等待,会把消息做持久化处理,保存在硬盘上,如果消费者上线,消费者就能监听到这条消息并进行消费。

对于topic来说,我一条消息发送出去,可能会有多个消费者去消费,如果这个时候消费者不在线,默认情况下,等消费者上线,他也无法收到这条消息,相当于这条消息丢失了。

也可以配置topic的持久化,保证消费者不在线的情况下,也能收到这条消息,那这跟queue的机制就类似了。

特殊情况:如果消息发送失败怎么办?一直失败怎么办?

消息的重试机制,死信队列

5.4.42.RabbitMQ入门-环境搭建

为何用消息队列?

用于业务解耦、分布式事务最终一致性、广播、错峰流控等等。

RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。

参考资料: 消息队列之 RabbitMQ:https://www.jianshu.com/p/79ca08116d57

Erlang不能错过的盛宴:https://www.cnblogs.com/xuan52rock/p/4597300.html

Erlang是一种面向并发(Concurrency Oriented),面向消息(Message Oriented)的函数式(Functional)编程语言。

Erlang应用场景是分布式产品,网络服务器,客户端,等各种应用环境。

RabbitMQ在windows安装时先需要安装Erlang语言的运行环境。

Mac中RabbitMQ的安装:

1)安装brew,在命令窗口执行:

/usr/bin/ruby -e “$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)”

安装过程中提示文件夹不存在,需要按回车新建,接着输入密码即可,下载较慢,需等待下

安装完毕后执行brew update 更新brew

2)brew install rabbitmq

注意: rabbitmq的安装目录: /usr/local/Cellar/rabbitmq/版本号

3)启动:进入到目录执行:

/usr/local/Cellar/rabbitmq/3.7.7_1/sbin/rabbitmq-server

启动可能会出错,出错的原因是mq产品他们的端口都是一致的,启动多种产品会导致端口冲突

4)RabbitMQ 启动插件

待RabbitMQ 的启动完毕之后,另起终端进入

cd /Users/lidong/javaEE/rabbitmq_server-版本号/sbin

sudo ./rabbitmq-plugins enable rabbitmq_management(执行一次以后不用再次执行)

5)登陆管理界面

浏览器输入:http://localhost:15672/

账号密码初始默认都为guest,登录后可新建一个账号密码进行管理

5.4.43.RabbitMQ入门-发送消息机制的介绍

几个重要概念

1)交换机Exchange:接收消息,并且把消息转发到不同的队列

2)绑定Binding:用于交换机和消息队列的关联

3)Routingkey:路由key,其实就是消息的名称

Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。

direct:消息中的路由键(routing key)如果和 Binding 中的 binding key 一致,是一种点对点方式

fanout:每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。

topic:topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配。

5.4.45.RabbitMQ跟SpringBoot整合

https://www.jianshu.com/p/0d400d30936b

5.4.46.大数据神器Kafka入门

Kafka安装依赖jdk,运行依赖zookeeper

MAC下Kafka安装和启动:https://www.jianshu.com/p/dd2578d47ff6

kafka主要用于大数据量,一般在公司用rabbitmq会比较,如果是教学,activemq用的多,但是他们的用法基本类似,

导入pom依赖,配置配置文件,在项目中注入xxxTemplate,调用方法发送,@xxListener接收消息

5.4.47.RocketMQ入门

Rocketmq原理&最佳实践:https://www.jianshu.com/p/2838890f3284

Rocketmq相比于Rabbitmq、kafka具有主要优势特性有:
• 支持事务型消息(消息发送和DB操作保持两方的最终一致性,rabbitmq和kafka不支持)
• 支持结合rocketmq的多个系统之间数据最终一致性(多方事务,二方事务是前提)
• 支持18个级别的延迟消息(rabbitmq和kafka不支持)
• 支持指定次数和时间间隔的失败消息重发(kafka不支持,rabbitmq需要手动确认)
• 支持consumer端tag过滤,减少不必要的网络传输(rabbitmq和kafka不支持)
• 支持重复消费(rabbitmq不支持,kafka支持)

RocketMQ 安装详细说明:https://blog.csdn.net/wangmx1993328/article/details/81536168

5.4.48.数据采集Java端解码-使用消息队列异步最终选型及实现

我们最终的选型使用的是activemq,使用它的主要原因是因为搭建比较方便,activemq依赖于jdk

rabbitmq需要依赖于erlang,需要在服务器安装erlang环境

kafka依赖于zookeeper,启动比较麻烦,而且没有用户友好的界面

综合考虑,产品需要交付给客户,而客户没有对应的运维能力,只能是交付给客户一个最简单的方案

5.4.49.频繁使用设备信息存在的问题及定时任务模块的引入

目前数据采集功能已经完成了,每次采集后的数据需要存到数据库,这时需要获取对应的设备相关的信息,会每采集一条数据,就要把他对应的设备信息查询出来,而我的采集频率是相对较高的,而设备信息通常不会改变,这里就是他的性能瓶颈。另外,我们采集到的数据需要进行甄别,要对异常数据进行报警,这就涉及到,对采集到的数据,需要判断他的最小最大值,而目前每个仪器的最小最大阈值是存在数据库的,这样每次比对一样的需要这些信息。这里的问题就是,我们需要频繁使用设备信息(包含了阈值),如果每次都去数据库拿,会影响性能。此时就要把数据缓存起来,会缓存到Redis中。但是谁去缓存?以及什么时候去缓存?这是两个问题

1)谁去缓存的问题?当设备新增的时候,就把设备信息存到redis,当增删改的时候,把缓存中的数据删掉。当下次再使用这条数据的时候,先从redis中取,如果取到了,直接使用,如果没取到,就去数据库取,并且把这个数据存到redis,这也是大部分情况下的比较好的做法。

2)什么时候去缓存?

对于我们这个业务,其实还有一种处理方案,我们可以把缓存的任务交给程序自动完成,这时可以使用定时框架,由定时框架去帮我们做缓存。程序依然从缓存中拿设备信息,而定时任务会定时从数据库中将设备信息取出来,同步到缓存中。此种方案可以避免对业务代码的侵入,对于已经完成的模块,我们不会去动他。

讲解一下定时任务框架

5.4.50.定时任务框架入门

定时任务有很多种,比如大家闹钟。学习了程序开发后,对于数据库有定时器(定时执行某条sql语句)。对于操作系统来说也有定时器,比如可以设置定时关机,假如你使用的是linux,那这时可能会做数据库的定时备份。对于js来说,也有定时器(http://caibaojian.com/javascript-timer.html)。对于java来说,当然也有他的定时器Timer(jdk自带),但我们一般会使用比较成熟的框架(Quartz,SpringTask),对于项目来说,这两个框架都用到了,而脚手架工程已经内置好了Quartz定时器,你可以通过简单的界面操作,设置某个类中的方法被定时执行(定时收发邮件)。对于一些业务上的操作,我在这里采用的是SpringTask的方案。

对于定时任务(不涉及到具体的实现方案)来说,他里面最重要的就是定时的规则该怎么指定?

定时规则是什么?定时任务要在何时被执行,或者多长时间重复执行一次

指定定时规则的表述可以使用表达式来设置,跟正则表达式类似(用于匹配字符串),而我们的时间表达式用于匹配时间,如果表达式跟时间能匹配,那这时定时任务就会被执行。

这种在时间圈中用的表达式叫Cron表达式,Cron(克龙)是一个时间单位,代表百万年,这就代表了Cron能对时间进行非常精准,大范围的控制。而Cron表达式对于编程来说,他是通用的,他可以应用于不同的系统,应用于不同的语言。**在各种情况下略微有所不同,**但是大体上是不会改变的。

5.4.51.Cron表达式

其实就是一种规则,这种规则该如何书写,如何去跟时间匹配呢?

https://www.cnblogs.com/javahr/p/8318728.html

Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式:

(1) Seconds Minutes Hours DayofMonth Month DayofWeek Year

(2)Seconds Minutes Hours DayofMonth Month DayofWeek

而对于SpringTask和Quartz框架来说,只能使用第二种不带年份的

而对于linux来说,里面叫crontab,他是可以使用年份的,在数据库的定时备份部分会给大家讲解

一、结构

corn从左到右(用空格隔开):秒 分 小时 月份中的日期 月份 星期中的日期 年份

二、各字段的含义

字段 允许值 允许的特殊字符
秒(Seconds) 0~59的整数 , - * / 四个字符
分(Minutes 0~59的整数 , - * / 四个字符
小时(Hours 0~23的整数 , - * / 四个字符
日期(DayofMonth 1~31的整数(但是你需要考虑你月的天数) ,- * ? / L W C 八个字符
月份(Month 1~12的整数或者 JAN-DEC , - * / 四个字符
星期(DayofWeek 1~7的整数或者 SUN-SAT (1=SUN) , - * ? / L C # 八个字符
年(可选,留空)(Year 1970~2099 , - * / 四个字符

注意事项:

每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是:

(1):表示匹配该域的任意值。假如在Minutes域使用, 即表示每分钟都会触发事件。

(2)?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?, 其中最后一位只能用?,而不能使用*,如果使用*表示不管星期几都会触发,实际上并不是这样。

举例:对于月份和星期他们是有冲突的,定义了15号 星期三触发,这是有问题的,15号不一定是星期三,我们的规则是必须要同时满足才能被执行,因此这两个域不能同时存在,当指定其中一个域的值的时候,另外一个必须为问号。

(3)-:表示范围。例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次

(4)/:表示起始时间开始触发,然后每隔固定时间触发一次。例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次.

(5),:表示列出枚举值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。

(6)L:表示最后,只能出现在DayofWeek和DayofMonth域。如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。

(7)W:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份 。工作日下发工作计划或发邮件

(8)LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。

(9)#:用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三。

三、常用表达式例子

(1)0 0 2 1 * ? * 表示在每月的1日的凌晨2点调整任务

(2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业

(3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作

(4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点

(5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时

(6)0 0 12 ? * WED 表示每个星期三中午12点

(7)0 0 12 * * ? 每天中午12点触发

(8)0 15 10 ? * * 每天上午10:15触发

(9)0 15 10 * * ? 每天上午10:15触发

(10)0 15 10 * * ? * 每天上午10:15触发

(11)0 15 10 * * ? 2005 2005年的每天上午10:15触发

(12)0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发

(13)0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发

(14)0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发

(15)0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发

(16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发

(17)0 15 10 ? * MON-FRI 周一至周五的上午10:15触发

(18)0 15 10 15 * ? 每月15日上午10:15触发

(19)0 15 10 L * ? 每月最后一日的上午10:15触发

(20)0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发

(21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发

(22)0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发

注:

(1)有些子表达式能包含一些范围或列表

例如:子表达式(天(星期))可以为 “MON-FRI”,“MON,WED,FRI”,“MON-WED,SAT”

“*”字符代表所有可能的值

因此,“”在子表达式(月)里表示每个月的含义,“”在子表达式(天(星期))表示星期的每一天
  “/”字符用来指定数值的增量
  例如:在子表达式(分钟)里的“0/15”表示从第0分钟开始,每15分钟
在子表达式(分钟)里的“3/20”表示从第3分钟开始,每20分钟(它和“3,23,43”)的含义一样
  “?”字符仅被用于天(月)和天(星期)两个子表达式,表示不指定值
  当2个子表达式其中之一被指定了值以后,为了避免冲突,需要将另一个子表达式的值设为“?”

“L” 字符仅被用于天(月)和天(星期)两个子表达式,它是单词“last”的缩写
  但是它在两个子表达式里的含义是不同的。
  在天(月)子表达式中,“L”表示一个月的最后一天
  在天(星期)自表达式中,“L”表示一个星期的最后一天,也就是SAT

如果在“L”前有具体的内容,它就具有其他的含义了

例如:“6L”表示这个月的倒数第6天,“FRIL”表示这个月的最一个星期五
  注意:在使用“L”参数时,不要指定列表或范围,因为这会导致问题

接下来要讲的,规则这么多,表达式这么复杂,记不住怎么办?凉拌

神器:在线的表达式生成器http://cron.qqe2.com/

5.4.52.使用SpringTask定时获取传感器设备信息并缓存到Redis

1)整合:略。因为在Spring-context包中已经整合过了

2)使用:写一个方法,然后加上规则,他就会被定时执行

/*** 项目启动后从数据库将各设备的信息和阈值查询出来放入redis* 五分钟一次*/
//0 */5 * * * ?
@Scheduled(cron = "0 */5 * * * ?")
public void getDeviceThresholdFromRedis() {//保证redis中是最新的数据,需要定时删除redisTemplate.delete("deviceThreshold");Set deviceThreshold = redisTemplate.opsForHash().keys("deviceThreshold");if (deviceThreshold.size() < 1) {List<Device> deviceList = deviceDao.list(null, null, null);for (Device device : deviceList) {redisTemplate.opsForHash().put("deviceThreshold",device.getId(),device);}}System.out.println(deviceThreshold.size());
}

3)注意事项:因为考虑到客户的部署环境比较复杂,redis没有做高可用,可能会挂掉

5.4.53.数据采集提升性能篇-使用Mybatis的批量操作api导入数据

每次发送过来的数据,会包含多个传感器的值,每个传感器的数据都要存到数据库,如果每次都调用dao去保存,那性能比较低,应该进行优化,要用到mybatis的批量操作

通常大家都会使用mybatis的 标签去执行批量的insert操作,这种做法是有问题的,foreach标签中在做字符串的拼接的时候,对于字符串的最大长度是有限制的,如果你的数据特别多,这时拼接会报错,一般的培训机构或网上的教程都不会指明这一点。应该采用另外的办法,就是mybatis的batch操作。

网上的例子

@Autowired
private SqlSessionFactory sqlSessionFactory;@Transactional(rollbackFor = Exception.class)
@Override
public void batchTest() {SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);CountryMapper mapper = sqlSession.getMapper(CountryMapper.class);List<Country> countries = mapper.selectAll();for (int i = 0; i < countries.size(); i++) {Country country = countries.get(i);country.setCountryname(country.getCountryname() + "Test");mapper.updateByPrimaryKey(country);//每 50 条提交一次if((i + 1) % 50 == 0){sqlSession.flushStatements();}}sqlSession.flushStatements();
}

自己的流程:

1)注入工厂类

//批量插入
@Autowired
private SqlSessionFactory sqlSessionFactory;

2)根据工厂类得到批量操作的session:指定这个参数ExecutorType.BATCH

SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);

3)执行你的 dao的sava操作

deviceGatherDao.save(deviceGather);//。。。很多的save

4)执行提交

sqlSession.flushStatements();

5.4.54.智能监测页面功能分析及布局设计

页面上的每一部分数据都需要从数据库查询出来并且进行显示

5.4.55.监测站信息展示

<body ng-app="ia" ng-controller="envMoniterController" ng-init="getFarmInfo();getRealDataChart()">
<div class="tubox-list my-bigfont"><span><strong>设备ID号:</strong>{{device.sn}}</span><span><strong>设备状态:</strong>{{state[device.state]}}</span><span><strong>软件版本:</strong>V{{device.softwareVersion}}</span><span><strong>Tel:</strong><addr title="phone">{{farm.telephone}}</addr></span>
</div>
FarmDto farmDto=new FarmDto();
User user = (User) SecurityUtils.getSubject().getPrincipal();
user.setPassword(null);
farmDto.setUser(user);
//..
List<Farm> farmList = farmDao.listByUserId(user.getId());

5.4.56.实时数据前端设计

<div class="layui-col-md12"><div class="layui-card"><div class="layui-card-header">实时数据</div><div class="layui-card-body"><div class="layui-carousel layadmin-carousel my-height1 layadmin-shortcut changeheight"><div carousel-item><ul class="layui-row layui-col-space10" id="realData1"><li class="layui-col-xs3 {{isSelectData(realTimeData)?'cardSelected':''}}" name="dataCard" id="{{realTimeData.deviceId}}" ng-repeat="realTimeData in realTimeDataList"><a ng-click="selectData(realTimeData)"><i class="layui-icon {{layuiCssClass[$index]}}"></i><cite class="nonowrap">{{realTimeData.measureUnitType}}:{{realTimeData.basicData}}{{realTimeData.measurementUnitName}}</cite></a></li></ul></div></div></div></div>
</div>
$scope.layuiCssClass=['layui-icon-console','layui-icon-chart','layui-icon-template-1','layui-icon-find-fill','layui-icon-survey','layui-icon-user','layui-icon-set','layui-icon-about','layui-icon-camera-fill','layui-icon-website','layui-icon-about'];
//设备实时数据
$scope.realTimeDataList=response.data.realTimeDataList;
//如果设备是从我的基地页面跳过来的,那就将我的基地点击的数据变成默认值
if ($scope.actveDeviceId!=undefined) {for (var i=0;i<$scope.realTimeDataList.length;i++) {if ($scope.realTimeDataList[i].deviceId==$scope.actveDeviceId) {$scope.realTimeData=$scope.realTimeDataList[i];break;}}
}

样式建议还是找个专业的前端来设计

5.4.57.实时数据后端数据封装

获取当前农场的所有传感器的最新的值,跟传感器一起封装发到前端

controller

//当前基地的设备实时数据
List<DeviceGather> deviceGathers=deviceService.getRealTimeDataByFarmId(farmId);
/*** 获取采集到的实时数据,根据每个不同的农场分别进行返回* 页面需要将农场的id传过来* 每个农场的设备很多,此处应该获取所有设备的最新采集数据封装并且返回* @param farmId* @return*/
public List<DeviceGather> getRealTimeDataByFarmId(Long farmId) {List<DeviceGather> deviceGathers = new ArrayList<>();//根据农场id获取该农场的所有设备List<Device> deviceList = deviceDao.getByFarmId(farmId);for (Device device : deviceList) {//从deviceGather表中根据设备的id查询每个设备的最后一条采集记录//主键倒序取一条记录即可 ORDER BY id DESC LIMIT 1List<DeviceGather> deviceGatherList = deviceGatherDao.getByDeviceId(device.getId(),1);if (deviceGatherList!=null&&deviceGatherList.size()>0){//封装数据返回deviceGathers.add(deviceGatherList.get(0));}}return  deviceGathers;
}

dao

//ORDER BY id DESC LIMIT 1
@Select("select * from t_device_gather where deviceId=#{deviceId} ORDER BY id DESC LIMIT #{rows}")
List<DeviceGather> getByDeviceId(@Param("deviceId") Long deviceId,@Param("rows") Integer rows);

5.4.58.ECharts组件入门

开发中经常会遇到需要在页面显示报表:折线图,饼图,柱状图,能生成这些报表的前端框架很多,JCharts

课程中使用echarts:https://echarts.baidu.com/

option = {xAxis: {type: 'category',data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']},yAxis: {type: 'value'},series: [{data: [1820, 932, 901, 1934, 1290, 1330, 1320],type: 'line'}]
};

不管用的是哪个框架都需要横轴和纵轴数据,我们要做的就是去数据库把数据按指定的格式查询出来,然后在前端进行显示即可

5.4.59.环境数据趋势图功能介绍

5.4.60.趋势图前端设计

layui跟echarts是可以进行整合的,我项目就做了整合,但是不建议做过多的整合,因为这样就耦合到一起了,如果你换用其他的ui框架,那你的代码不能复用。

<div class="layui-col-md12" id="echartsbug"><div class="layui-card"><!--<div class="layui-card-header">监测趋势图</div>--><form name="form" class="layui-form" action="" lay-filter="component-form-element" ng-click="changeTime()"><div class="layui-form-item"><label class="layui-form-label">趋势图:</label><div class="layui-input-block"><input type="radio" name="timeUnit" value="MONTH" title="月"><input type="radio" name="timeUnit" value="DAY" title="天"><input type="radio" name="timeUnit" value="HOUR" title="时" checked></div></div></form><div class="layui-card-body"><div class="layui-carousel layadmin-carousel my-height1 layadmin-dataview" data-anim="fade" lay-filter="LAY-index-dataview"><div carousel-item id="realDataView"></div><div></div><div></div></div></div></div>
</div>
 //可以单选实时数据,并高亮显示,将折线图同步进行变化$scope.selectData=function(realTimeData){$scope.realTimeData=realTimeData;//将折线图的默认数据清空option.xAxis.data=[];angular.forEach(option.series,function (data,index) {option.series[index].data=[];});option.legend.data=[];var i=0;angular.forEach($scope.responseData,function(deviceDataList,indexI){angular.forEach(deviceDataList,function (deviceData, indexJ) {if (i===0){option.xAxis.data.push(deviceData.hour);option.legend.data.push(realTimeData.measureUnitType)}if (realTimeData.deviceId== deviceData.deviceId) {option.series[i].data.push(deviceData.avg);return;}});i++;if (i>deviceDataList.length) {return;}})realDataViewChart.setOption(option);}$scope.isSelectData=function(realTimeData){if (realTimeData == $scope.realTimeData) {return true;} else {return false;}}//按年,月,日分别进行显示$scope.changeTime=function(){$scope.timeUnit=document.form.timeUnit.value;$scope.getRealDataChart($scope.timeUnit);}$scope.getRealDataChart=function (timeUnit) {//通过地址栏传递参数$scope.actveDeviceId= $location.search()['deviceId'];//获取参数值$http.get('../../deviceGathers/echartsShow?timeUnit='+timeUnit).then(function (response) {//将折线图的默认数据清空option.xAxis.data=[];angular.forEach(option.series,function (data,index) {option.series[index].data=[];});$scope.responseData=response.data;//console.log($scope.responseData);var i=0;angular.forEach(response.data,function(deviceDataList,indexI){if ($scope.actveDeviceId != undefined) {angular.forEach(deviceDataList,function (deviceData, indexJ) {//console.log(deviceData)//x轴只需要赋值一次if (i===0){option.xAxis.data.push(deviceData.hour);}if ($scope.actveDeviceId == deviceData.deviceId) {option.series[i].data.push(deviceData.avg);return;}});} else {angular.forEach(deviceDataList,function (deviceData, indexJ) {//console.log(deviceData)//x轴只需要赋值一次if (i===0){option.xAxis.data.push(deviceData.hour);}option.series[i].data.push(deviceData.avg);});}i++;if (i>deviceDataList.length) {return;}})//console.log(option)realDataViewChart.setOption(option);})}});
</script><!--折线图-->
<script type="text/javascript">option = {tooltip: {trigger: 'axis'},legend: {data:['空气温度','空气湿度','光照','土壤温度','土壤湿度','电导率','PH值'],x: 'left',y: 'top'},grid: {left: '3%',right: '5%',bottom: '3%',containLabel: true},toolbox: {feature: {// mark: {show: true},// dataView: {show: true, readOnly: false},magicType: {show: true, type: ['line', 'bar']},// restore: {show: true},saveAsImage: {show: true}}},xAxis: {type: 'category',boundaryGap: false,data: ['00:00','01:00','02:00','03:00','04:00','05:00','06:00','07:00','08:00','09:00','10:00','11:00','12:00','13:00','14:00','15:00','16:00','17:00','18:00','19:00','20:00','21:00','22:00','23:00']},yAxis: {type: 'value'},series: [{name:'空气温度',type:'line',stack: '总量',data:[120, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210]},{name:'空气湿度',type:'line',stack: '总量',data:[220, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210]},{name:'光照',type:'line',stack: '总量',data:[320, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210]},{name:'土壤温度',type:'line',stack: '总量',data:[420, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210]},{name:'土壤湿度',type:'line',stack: '总量',data:[520, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210]},{name:'电导率',type:'line',stack: '总量',data:[620, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210]},{name:'PH值',type:'line',stack: '总量',data:[720, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210,120, 132, 101, 134, 90, 230, 210]}]};// 基于准备好的dom,初始化echarts实例var realDataViewChart = echarts.init(document.getElementById('realDataView'));
</script>
<!--温度-->

折线图的架构是相对很固定的,唯二变化的就是x轴和y轴的数据,分别对应于代码中的xAxis:data yAxis:series:data

5.4.61.趋势图后端数据封装

controller

@RequestMapping("/echartsShow")
@ApiOperation(value = "echarts数据展示,要求按年月日")
public Map<Long,List<DeviceGatherCharts> > echartsShow(PageTableRequest pageTableRequest){User user = (User) SecurityUtils.getSubject().getPrincipal();user.setPassword(null);//按照farm和设备分别封装数据,一个农场的一个设备为一组数据return deviceService.echartsShow(user.getId(),pageTableRequest);
}

service

/*** 返回当前农场所拥有的所有设备的采集数据* @param userId* @param pageTableRequest* @return*/
@Override
public Map<Long,List<DeviceGatherCharts> >  echartsShow(Long userId, PageTableRequest pageTableRequest) {String timeUnit = (String) pageTableRequest.getParams().get("timeUnit");if (StringUtils.isEmpty(timeUnit)||"undefined".equals(timeUnit)) {pageTableRequest.getParams().put("timeUnit","HOUR");//前端不传,按小时显示 MONTH YEAR}Map<Long,List<DeviceGatherCharts>> viewChartsData=new HashMap<>();List<Farm> farmList = farmDao.listByUserId(userId);String createTimeRangeStr ="";if (farmList != null && farmList.size() > 0) {//TODO 将用户拥有的第一个农场查询出来,这里是有问题的,需要优化,应该修改为根据前端传递过来的基地id进行切换Farm farm = farmList.get(0);Long farmId=farm.getId();List<Device> deviceList = deviceDao.getByFarmId(farmId);for (Device device : deviceList) {//根据每个设备分别查询对应的数据pageTableRequest.getParams().put("deviceId",device.getId());List<DeviceGatherCharts> DeviceGatherChartsList=deviceGatherDao.echartsShow(pageTableRequest);viewChartsData.put(device.getId(),DeviceGatherChartsList);}}return viewChartsData;
}
List<DeviceGatherCharts> echartsShow(PageTableRequest pageTableRequest);
<select id="echartsShow" resultType="com.topwulian.dto.DeviceGatherCharts">SELECT ${params.timeUnit}( e.createTime ) AS HOUR,ROUND(avg( e.basicData ),1) AS avg,deviceIdFROMt_device_gather eWHEREdeviceId=${params.deviceId}<if test="params.timeUnit=='HOUR'">and to_days(e.createTime) = to_days(now())</if><if test="params.timeUnit!='HOUR'">and to_days(e.createTime) &lt;= to_days(now())</if>GROUP BY${params.timeUnit} ( e.createTime )ORDER BY${params.timeUnit} ( e.createTime );
</select>

sql语句统计规定的时间单位内某个设备单一时间点的平均值

5.4.62.趋势图数据展示流程梳理

1)页面定义渲染的区域,通常就是一个div

2)需要向后端发请求

3)后端接收到请求,分别根据年月日三个维度返回不同的封装数据

4)首先根据用户id得到对应的农场

5)根据农场得到对应的设备

6)根据设备id得到设备对应的时间段内的数据(写sql)

7)在前端js中进行处理

8)把返回的数据封装到xy轴

5.4.63.历史数据下载功能介绍

对于每个设备采集到的数据,我们都希望可以直接下载保存下来,通常的做法就是保存成excel表格

5.4.64.Java的Excel导出方案介绍

Apache POI方案可以对数据导出成excel表格,大部分的教程也都是基于poi进行的,但是基于poi他的api比较复杂

现在流行的一个方案就是对poi进行封装,把api的细节屏蔽,直接跟实体类进行映射,映射到表格中对应的行,导出数据

easypoi:http://easypoi.mydoc.io/

<dependency><groupId>cn.afterturn</groupId><artifactId>easypoi-base</artifactId><version>3.2.0</version>
</dependency>
<dependency><groupId>cn.afterturn</groupId><artifactId>easypoi-web</artifactId><version>3.2.0</version>
</dependency>
<dependency><groupId>cn.afterturn</groupId><artifactId>easypoi-annotation</artifactId><version>3.2.0</version>
</dependency>

现在已经有了springboot起步依赖版本,可以自行去github寻找

开发的主要思路是利用他的demo进行,https://gitee.com/lemur/easypoi-test

阿里开源的easyexcel

<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>{latestVersion}</version>
</dependency>

https://blog.csdn.net/jiangjiandecsd/article/details/81115622

easyexcel和easypoi最大的区别:easypoi对于poi的封装,所以他的依赖jar包非常大,有5M多

而easyexcel是对poi进行了重写,jar小,而且修正了oom的问题,建议使用easyexcel

但是easyexcel他的功能没有easypoi强大

这两个开源都比较活跃

5.4.65.EasyPOI的入门

开发思路就是导包,新建实体类,在实体类上面加上Excel的注解,注解中包含excel表格的属性:颜色,宽高,是否合并单元格

构建excel的流程:

​ 1)新建一个excel文件

​ 2)在excel文件中新建sheet

​ 3)在sheet中新建一行Row

​ 4)在row中插入数据cell

5.4.66.项目中使用EasyPOI完成监控设备历史数据的导出

1)在页面定义一个按钮,当点击按钮时,把设备id,时间范围传递到后台

2)后天接收到请求及参数,进行处理

3)使用easypoi导出数据

/*** 默认查询最近一个月的数据进行下载* 要按设备类型不同分别下载* 可以根据传入的时间范围进行下载* @param map* @param request* @param response* @param pageTableRequest*/
@RequestMapping("load")
@ApiOperation(value = "历史数据下载")
public void downloadByPoiBaseView(ModelMap map, HttpServletRequest request,HttpServletResponse response,PageTableRequest pageTableRequest) {FarmDto farmDto=new FarmDto();User user = (User) SecurityUtils.getSubject().getPrincipal();user.setPassword(null);farmDto.setUser(user);//设备信息//基地id默认就是当前拥有的第一个基地,如有多个,在切换的时候动态更新数据//如果用户没有基地,那么farmId就为null,数据库也不会返回数据Long farmId = null;//根据用户获取拥有的农场List<Farm> farmList = farmDao.getFarmListByUserId(user.getId());List<DeviceGather> deviceGatherList=null;String createTimeRangeStr ="";if (farmList != null && farmList.size() > 0) {Farm farm = farmList.get(0);//将用户拥有的第一个农场查询出来,这里是有问题的,需要优化farmId=farm.getId();List<Device> deviceList = deviceDao.getByFarmId(farmId);//封装查询条件createTimeRangeStr = (String) pageTableRequest.getParams().get("createTimeRange");if (StringUtils.isNotEmpty(createTimeRangeStr)) {//2019-01-11 - 2019-02-03String[] createTimeRangeStrS = createTimeRangeStr.split(" - ");pageTableRequest.getParams().put("startTime", createTimeRangeStrS[0]);pageTableRequest.getParams().put("endTime", createTimeRangeStrS[1]);}else {//如果没有传时间,就显示最近一个月的String startTime=DateUtil.dateToString(DateUtil.getBeforeByMonth(1));String endTime=DateUtil.dateToString(new Date());pageTableRequest.getParams().put("startTime",startTime);pageTableRequest.getParams().put("endTime", endTime);createTimeRangeStr=startTime+" - "+endTime;}//将分页参数设置为空pageTableRequest.setLimit(null);pageTableRequest.setOffset(null);deviceGatherList=deviceService.getHistoryDataByFarmId(pageTableRequest,farmId);}ExportParams params = new ExportParams("采集数据显示", "概览", ExcelType.XSSF);params.setFreezeCol(2);map.put(NormalExcelConstants.DATA_LIST, deviceGatherList);map.put(NormalExcelConstants.CLASS, DeviceGather.class);map.put(NormalExcelConstants.PARAMS, params);map.put(NormalExcelConstants.FILE_NAME, createTimeRangeStr);PoiBaseView.render(map, request, response, NormalExcelConstants.EASYPOI_EXCEL_VIEW);}

得到历史数据的service

/*** 得到历史采集数据* @param pageTableRequest* @param farmId* @return*/
@Override
public List<DeviceGather> getHistoryDataByFarmId(PageTableRequest pageTableRequest,Long farmId) {List<DeviceGather> deviceGathers = new ArrayList<>();//如果设备id直接传递过来了,那就按设备id直接查询Object deviceIdObj = pageTableRequest.getParams().get("deviceId");if (deviceIdObj != null) {Long deviceId= (Long) deviceIdObj;deviceGathers = deviceGatherDao.list(pageTableRequest.getParams(), null, null);return deviceGathers;}//没有设备id的情况下,根据农场id获取该农场的所有设备,并全部返回List<Device> deviceList = deviceDao.getByFarmId(farmId);for (Device device : deviceList) {//不需要分页参数,并且设置设备idpageTableRequest.getParams().put("deviceId",device.getId());List<DeviceGather> deviceGatherList = deviceGatherDao.list(pageTableRequest.getParams(),null,null);if (deviceGatherList!=null&&deviceGatherList.size()>0){//封装数据返回for (DeviceGather deviceGather : deviceGatherList) {deviceGathers.add(deviceGather);}}}return deviceGathers;
}

实体类

public class DeviceGather extends BaseEntity<Long> {@Excel(name = "设备id")private Integer deviceId;@Excel(name = "设备序列号",width = 20)private String deviceSn;@Excel(name = "设备名称",width = 30)private String deviceName;private String deviceType;@Excel(name = "数据")private Float basicData;private Integer measurementUnitId;@Excel(name = "数据单位")private String measurementUnitName;private String measureUnitType;@Excel(name = "采集时间",format = "yyyy-MM-dd HH:mm:ss", width = 35)private Date gatherTime;

生成excel的代码

ExportParams params = new ExportParams("采集数据显示", "概览", ExcelType.XSSF);
params.setFreezeCol(2);
map.put(NormalExcelConstants.DATA_LIST, deviceGatherList);
map.put(NormalExcelConstants.CLASS, DeviceGather.class);
map.put(NormalExcelConstants.PARAMS, params);
map.put(NormalExcelConstants.FILE_NAME, createTimeRangeStr);
PoiBaseView.render(map, request, response, NormalExcelConstants.EASYPOI_EXCEL_VIEW);

5.5.1.海康威视监控摄像头介绍

业内有很多摄像头厂商,但是从易用性以及api的支持来看,大家在选型时,需要去综合考虑,建议大家采用海康威视的摄像头

https://www.hikvision.com/cn/

我们项目中采购的是网络摄像机https://www.hikvision.com/cn/prlb_1608.html

采购时,注意要选热门设备,要选功能比较强大的,建议采购1000元以上的设备,各种功能比较强大,一定要针对你的应用场景选择自己合适的设备

你需要使用哪些功能,必须都支持,如果某个功能不支持,那就无法进行相应开发

如果是户外,要考虑风吹日晒,防雷击等

    createTimeRangeStr=startTime+" - "+endTime;}//将分页参数设置为空pageTableRequest.setLimit(null);pageTableRequest.setOffset(null);deviceGatherList=deviceService.getHistoryDataByFarmId(pageTableRequest,farmId);}ExportParams params = new ExportParams("采集数据显示", "概览", ExcelType.XSSF);
params.setFreezeCol(2);
map.put(NormalExcelConstants.DATA_LIST, deviceGatherList);
map.put(NormalExcelConstants.CLASS, DeviceGather.class);
map.put(NormalExcelConstants.PARAMS, params);
map.put(NormalExcelConstants.FILE_NAME, createTimeRangeStr);
PoiBaseView.render(map, request, response, NormalExcelConstants.EASYPOI_EXCEL_VIEW);

}


得到历史数据的service```java
/*** 得到历史采集数据* @param pageTableRequest* @param farmId* @return*/
@Override
public List<DeviceGather> getHistoryDataByFarmId(PageTableRequest pageTableRequest,Long farmId) {List<DeviceGather> deviceGathers = new ArrayList<>();//如果设备id直接传递过来了,那就按设备id直接查询Object deviceIdObj = pageTableRequest.getParams().get("deviceId");if (deviceIdObj != null) {Long deviceId= (Long) deviceIdObj;deviceGathers = deviceGatherDao.list(pageTableRequest.getParams(), null, null);return deviceGathers;}//没有设备id的情况下,根据农场id获取该农场的所有设备,并全部返回List<Device> deviceList = deviceDao.getByFarmId(farmId);for (Device device : deviceList) {//不需要分页参数,并且设置设备idpageTableRequest.getParams().put("deviceId",device.getId());List<DeviceGather> deviceGatherList = deviceGatherDao.list(pageTableRequest.getParams(),null,null);if (deviceGatherList!=null&&deviceGatherList.size()>0){//封装数据返回for (DeviceGather deviceGather : deviceGatherList) {deviceGathers.add(deviceGather);}}}return deviceGathers;
}

实体类

public class DeviceGather extends BaseEntity<Long> {@Excel(name = "设备id")private Integer deviceId;@Excel(name = "设备序列号",width = 20)private String deviceSn;@Excel(name = "设备名称",width = 30)private String deviceName;private String deviceType;@Excel(name = "数据")private Float basicData;private Integer measurementUnitId;@Excel(name = "数据单位")private String measurementUnitName;private String measureUnitType;@Excel(name = "采集时间",format = "yyyy-MM-dd HH:mm:ss", width = 35)private Date gatherTime;

生成excel的代码

ExportParams params = new ExportParams("采集数据显示", "概览", ExcelType.XSSF);
params.setFreezeCol(2);
map.put(NormalExcelConstants.DATA_LIST, deviceGatherList);
map.put(NormalExcelConstants.CLASS, DeviceGather.class);
map.put(NormalExcelConstants.PARAMS, params);
map.put(NormalExcelConstants.FILE_NAME, createTimeRangeStr);
PoiBaseView.render(map, request, response, NormalExcelConstants.EASYPOI_EXCEL_VIEW);

5.5.1.海康威视监控摄像头介绍

业内有很多摄像头厂商,但是从易用性以及api的支持来看,大家在选型时,需要去综合考虑,建议大家采用海康威视的摄像头

https://www.hikvision.com/cn/

[外链图片转存中…(img-jZr8O0zS-1654321244611)]

我们项目中采购的是网络摄像机https://www.hikvision.com/cn/prlb_1608.html

采购时,注意要选热门设备,要选功能比较强大的,建议采购1000元以上的设备,各种功能比较强大,一定要针对你的应用场景选择自己合适的设备

你需要使用哪些功能,必须都支持,如果某个功能不支持,那就无法进行相应开发

如果是户外,要考虑风吹日晒,防雷击等

java物联网第三天 智慧农业物联网相关推荐

  1. 物联网平台 源码 智慧农业物联网平台 ,支持mqtt,h ttp,coap协议 java+postgresql,支持集群

    物联网平台 源码 智慧农业物联网平台 ,支持mqtt,h ttp,coap协议 java+postgresql,支持集群. 支持萤石云摄像头接入,云台控制,支持nbiot,lora物联网平台,5g物联 ...

  2. 智慧农业物联网平台建设方案

    本资料来源公开网络,仅供个人学习,请勿商用,如有侵权请联系删除. 智慧农业物联网系统组网图 2.2.1 智能温室组网说明 该组网图演示的为小面积示范区,每个连栋温室为 1个灌溉区域,1个子系统,该子系 ...

  3. 免费开源智慧农业物联网云平台 V3.0.1.2含源码

    一.简介 JINGLI(鲸哩)智能农业物联网云平台,从(设备端-APP端-平台端-管理端)全业务场景包含设备采集系统.监控控制系统.溯源系统.专家系统.仓库系统,大屏系统,开源版本毫无保留给个人及企业 ...

  4. Walker智慧农业物联网云平台(Version:3.0.1)「源码」

    一.简介 Walker智能农业物联网云平台,从(设备端-APP端-平台端-管理端)全业务场景包含设备采集系统.监控控制系统.溯源系统.专家系统.仓库系统,大屏系统,开源版本毫无保留给个人及企业免费使用 ...

  5. 物联网项目(三)平台架构

    物联网项目(三)平台架构 介绍下目前整个软件开发团队的配套成员 技能 人数 android 1 ios 1 前端 1 美工 1 java 2 以上就是我们这个项目的人员搭配,我除了项目上的管理,更多的 ...

  6. 智慧农业物联网系统解决方案

    一.方案背景 随着城市的发展,人们对于生活水准的要求也越来越高,对于食物的品质需求也越来越高,我作为世界农业大国,农业的发展优势慢慢降低,智慧化农业将带来一次新的农业结构改革.农业的根本问题是效率不高 ...

  7. 智慧农业物联网—解决方案

    一.方案背景 随着国家层面对土地集约化经营程度的不断加深,物联网概念在农业生产管理环节内的不断深入,规模化.科学化.数据化的种植方式已经愈发成为行业趋势.对于传统大规模种植而言,大量人力投入.工作效率 ...

  8. 蘑菇云「行空板Python入门教程」第九课-智慧农业物联网系统2

    5G元年的列车早已驶出,人工智能.大数据的浪潮还在涌动,云办公.云问诊成为防疫期间的热词. 现如今,物联网技术正处于时代发展的风口,相较于传统的硬件设备,物联网技术使得各种硬件设备能够通过信息传输设备 ...

  9. 智慧农业物联网云平台方案

    2019独角兽企业重金招聘Python工程师标准>>> 多比智慧农业物联网云平台解决方案结合了最先进的物联网.云计算.传感器.自动控制等, 在浏览器或手机客户端实时显示大棚.大田.温 ...

  10. 产业结盟 跨界共赢 | 新华三成为“中国联通物联网产业联盟” 首批成员

    8月25日 "中国联通物联网生态大会暨中国广州工业互联网国际博览会"盛大开幕,此次大会以"沃联天下.遇见未来"为主题,中国联通与具有代表性的物联网龙头企业一起, ...

最新文章

  1. 【392天】跃迁之路——程序员高效学习方法论探索系列(实验阶段149-2018.03.04)...
  2. ubuntu20.04下安装Docker和NVIDIA Container Toolkit教程
  3. An Invitation to 3-D Vision: From Images to Geometric Models 邀请 3d 视觉从图像的几何模型(免费下载)
  4. c++数据结构中 顺序队列的队首队尾_yiduobo的每日leetcode 622.设计循环队列
  5. python根据文件名获取文件路径_python 查看文件名和文件路径
  6. 结构专业规范大全_2019年一、二级注册结构师专业考试所用的规范、标准、规程...
  7. 【开源项目】基于FFmpeg的PCM数据编码为AAC
  8. 【CDN】域名无法访问,ping不到,tracert不到
  9. RabbitMQ实现生产者发送消息异步confirm
  10. python31001python3_Python310第二个alpha版本最新特性值得关注Python 3100a0 文档
  11. html5新增graph,Qunee for HTML5 - 中文 : Graph组件介绍
  12. blender基本翻译+快捷键
  13. ZCMU--1585: 面试
  14. spring boot酒店会员点餐系统毕业设计源码072005
  15. 详解硬件设计中电容电感磁珠
  16. 虚拟带库 Vistor + TSM 安装 (在家折腾了一个周末)
  17. 穆易天气app代码(一)
  18. 计算机毕业论文进展情况说明,研究生学位论文进展情况 毕业论文的进度和计划安排怎么写~~请详细些~~...
  19. 雷军狂送20亿给员工:网络工程师怎样才能最快体验到大厂待遇?
  20. 国家著作权: DNA 计算公式, 肽展定理公式与 变嘧啶 推导.

热门文章

  1. 制作拨号服务器,如何打造全自动的拨号上网服务器
  2. Mysql DOS界面进入
  3. 计算机组成知识教案,计算机系统的基本组成 教案_
  4. win7适合oracle哪个版本下载,win7系统下载--Windows 7下成功安装ORACLE客户端
  5. MHZ是计算机的什么单位,电脑mhz是什么意思
  6. python 写命令行_一个用python写的用命令行看糗百的小工具
  7. Kafka 之 HW 与 LEO
  8. loss、val_loss与accuracy、val_accuracy含义
  9. oracle分页查询最高效,oracle 分页 高效写法总结
  10. Zookeeper,集群管理之独孤求败