【项目总结】玛嘉环境物联网平台(大三学生独立完成的真实企业外包项目)/网脉通用物联网平台/网脉铁塔监测系统

分类专栏:
项目实战

文章标签:
Java
SSM
原创

前言

写这篇文章的目的主要是对过去做过的项目做一个整理,梳理项目中遇到问题和我当时的解决方案,回顾我做项目的过程,总结经验和教训,以便在来年找实习时有一个较好的思路去展示我做过的项目。

在本文只梳理设计和实践过程中遇到的问题和解决方案,就不具体介绍项目的功能了,如果对项目功能感兴趣的可以看我上传的视频【项目演示】网脉铁塔监测物联网平台

一、概述

1.背景

玛嘉环境物联网平台,是一个当地一个做环保器材企业的真实外包项目,由学院的老师接手。最开始是一个研究生在做的,不过做了三个多月,企业不太满意,暑假时候研究生因为学制的原因到另外一个学校学习,简单来说就是“跑路”了,留下一个烂摊子。

在大二暑假,我和另外几个同学被叫到老师办公室,让我们接手这个项目,一开始以为还算简单,费费劲应该也能做,但是直到接手研究生的项目源码后我忽然觉得项目难度骤升,因为他使用了Jeecg低代码开发平台。对于低代码平台这种东西,大家可能会觉得匪夷所思,认为低代码平台就是给不会开发的人用的,这怎么会难度骤升呢?其实不然,现在的低代码平台很多都无法做到这种特殊需求的定制化开发,一般低代码平台解决的都是那种表单类型开发,直白点就是平常的crud。

可是我们并不是crud啊!

在克服了无数的困难后,我最终把项目完成并部署上线投入使用,现在进入维护阶段。

网脉通用物联网平台

受到低代码的启发,我针对玛嘉环境物联网平台进行通用化改造,试图从物联网领域的工作模式中抽象出一套模型,力求做到人人皆可通过该平台获取自己定制化的物联网服务。

经过探索和开发,最终也完成部分。当时以为做到了通用化,但是后来在一次和物电老师(负责玛嘉环境物联网平台的硬件开发和设计的老师,一个非常好的,也真正有实力的老师)的交谈中,我才发现,由于自身对于物联网认知的局限性,并没有真正达到当初的目标。当时我才意识到,设计一个通用化物联网平台,最大的难点在于没有一个确定的业界统一的标准,这之中有历史原因,也有利益关系。当各个嵌入式设备都有统一的协议,那么物联网设备便能像手机接入互联网这般容易。所谓物联网,无非就是再现一个互联网罢了。

网脉铁塔监测物联网平台

该平台是为了参加服创省赛而写的版本,不过基本上也就是网脉通用物联网的别称,因为之前设计通用物联网时本就是试图设计一个通用的物联网模型,让所有人,不限行业、不限规模地接入平台,获取定制化服务。或许对于真正的通用化还有些距离,不过对于这种赛题还是绰绰有余,我几乎不费吹灰之力就将原来的平台改造成赛题所要求的平台(几乎就是换了个名字),当然也根据赛题要求对通用化物联网平台做了一定的功能补充。

2.三者的关系

三者同出一脉,前者是企业要求的产物,后两者是根据我自己的想法对其的改造,想做出点不一样的东西,最后也确实做出了点东西。

二、设计中遇到的问题与思考

背景

在思考下述问题时,我们必须得结合当时的背景,否则所提的一切设计都是在耍流氓!

如果从当时的视角来看,我面临以下几个问题:

1.项目上手需要时间,就算我也要好久

  • Jeecg的技术栈并未完全了解,难以上手。比如我没学过vue那套,redis平常也没用过;其他同学更不用说了,大抵要学上好久
  • 配置项目环境都花了一个晚上(主要之前没学过前端,node.js这种环境都配了好久)

2.项目设计堪忧

  • 整个UI设计堪忧,同时与业务相关的页面也就那么两三个
  • 导入sql文件,好家伙,43个表,后来我细细梳理后发现,真正与业务相关的表只有2个!!!其他都是jeecg自带的...
  • 项目规范方面有点随意,可能是因为以为只有自己看,没想到有人接手,好在他在关键步骤有注释
  • 表设计(时序数据库)显然没有考虑到未来的变动,比如在设计数据存储时就直接固定六个字段表示(传感量1-6)(这个还是我看源码发现的,项目中还用了反射去写入实体数据类)

3.项目需求不明确

  • 甲方无法专业表述需求,不过这也是外包的通病了
  • 甲方需求时刻都在变,时不时加新的需求
  • 我们只能从甲方天马行空的想象中,确定实际可行的方案,并给出原型以供其参考

4.留的文档很乱,没有真正意义上的设计文档

5.时间紧,任务重 甲方的客户也很急,因为之前开发已经耗费了三个月,甲方希望能先尽快上线系统,满足基本功能即可

1.如何确定需求?

在真实需求沟通中,我发现了两点: 永远不要低估甲方的想象力,他们提出的需求往往特别天马行空,两个字——离谱! 永远不要高估甲方能给的建议,他们对于原型的修改往往提不出有效的建议,一般都是“这里换个颜色,图标大一点”,对于项目本质的东西,他们连理解都理解不了,又如何提出有效的建议呢?

那么,如何去设计出他们想要的项目呢? 我一直在思考这个问题,从我这次的项目经历来看,我觉得可以分为以下几步:

1.假想你是甲方 永远不要站在设计者的角度去沟通项目需求,假想你就是这家企业的老板,我想开发一个项目让公司达到某方面的效果。

2.抓住甲方真正想要的东西

甲方在描述他的需求时,往往会给一个示例,或者其他网站,就比如我在沟通需求过程中他们就给了一个之前的老网站(也是一个外包项目,不过界面风格一看就是上个世纪的)。记住不要纠结于甲方描述出的图画,因为甲方很多时候也不知道自己想要什么。他们只会对做出来的东西说一句——“诶,这个好,我想要这个功能”。

这就好像我们去理发,其实我们往往不知道自己要理什么发型,然后我可能就随便说了一个我印象里的明星发型,因为我看他很帅,可我们真的是想要这个发型吗?我们只不过想变帅而已,那这个头型真的可以让自己变帅吗?不一定吧。所以真正作为设计师的我们就需要揣测他的意图,不要在意他所描述的东西,但要抓住他为什么要描述这种图画,揣测他的意图,理解他的重点。

要了解甲方真正在意哪个点,他想要这些功能的目的,抓住这些点,多去问一些问题确认。

3.根据这些点,以甲方的视角去设计项目,给甲方描述一下你的方案并解释为什么

根据之前得到的重点,揣测甲方的意图,然后从他的角度出发,凭借你的专业能力,给他描述一下你的设计。

如果你真正get到了甲方的意图和重点,那么你可以发挥你的专业知识,提出切实可行的设计方案,并向他解释你为什么这么设计。

4.转化为需求,和甲方确认

如果甲方觉得不可以,你要明白他不满意的地方在哪,针对其进行修改;如果方案得到甲方同意,那么将其转化为确定需求,和甲方确认。

一定要列出需求清单,不然甲方真的会反复变需求(别问我为什么,因为说多了都是泪啊)

2.如何接手这个设计堪忧的项目?

如何接手这个项目呢?这留给我们一个非常大的难题。

当时给我们有两条路: 1.沿用之前项目

此时甲方也很急,所以希望快速上线,沿用之前的项目好处在于: 理论上可以快速上线,因为我们也不确定新开发一个系统能不能赶得上项目周期。

当然也有很多坏处: 如此糟糕的设计完全满足不了需求,沿用之前的设计只是权宜之计,未来开发只会举步维艰; 留下的技术栈我们需要时间学习,对于我而言也至少需要一周时间; 缺少相应设计文档,很多东西都要读源码。

2.重开一个项目,选用自己熟悉的技术栈,根据用户需求重新设计编写

这种方案好处在于技术栈都是自己熟悉的,也更容易驾驭项目,然后可以完全脱开之前项目的设计,重写一个系统,后期功能开发也会更容易。 但是对于当时那种情况来说,我们并不能确定开发周期,而且当时所组的团队彼此不熟悉(对我来说还好,因为都是实验室成员,而作为实验室负责人的我都认识,也大概了解他们的技术方向),这点是个很大的挑战。

很多时候,计算机领域就是这样,我们不断地做选择题,而这种选择题并不像我们考试那般有一个明确的答案,选择往往是多样的,每个选择都有它的优劣,你甚至根本无法确认选择的优势劣势以及数量,我们只能凭借当时的情景去做出自己认为的最优选择。

当时我选择了沿用之前的技术栈,但设计肯定不能沿用,必须重构!彻底重构!

3.如何进行数据建模?

如何进行数据建模,这是个非常大的问题,我将其拆分成几个子问题

①物联网情境下如何存储数据?

我们知道物联网产生的数据往往具有以下特点:

  • 海量性
  • 关联性
  • 时效性

如果用传统的关系型数据库mysql显然无法很好的满足需求。 其实不应该这么去说,因为这样会有一种先入为主的观念。我们应该这么讲——在现有的数据库产品中,mysql肯定不是存储这种数据的最佳选择。

根据调研收集资料,以及老师提供的数据库推荐中,我找到了一个较为合适的数据库产品——TDengine。它是一款针对物联网场景进行开发的国产开源时序数据库产品,或者说平台(因为集成了很多数据库没有的功能),他对时序数据做了专门的优化,同时减去了一些传统数据库中不必要的功能(比如事务),因此性能在同类产品中算是前列,更重要的是它的文档丰富,很好上手。

②如何根据自身需求在TDengine上进行数据建模?

TDengine的设计比较特殊,有普通表和超级表的概念。对于一般数据的存储,他建议是一个数据采集点一张表,用超级表进行统一管理,这样有很多好处,比如避开事务问题,最大程度的保证单个数据采集点的插入和查询的性能是最优的等等。

以上描述的是TDengine文档中描述的最常用的数据建设计——多列模型

TDengine 支持多列模型,只要物理量是一个数据采集点同时采集的(时间戳一致),这些量就可以作为不同列放在一张超级表里。

除此之外,官方文档还提到了另一种极限的设计方式——单列模型

但还有一种极限的设计,单列模型,每个采集的物理量都单独建表,因此每种类型的物理量都单独建立一超级表。比如电流、电压、相位,就建三张超级表。

接下来我们看一下我当时的需求:

从和客户的交谈中,我发现客户的期望是想做出一个可以在线管理他们的环保设备,他们给出的老的系统也是这么做的。

这也可能是之前研究生设计固定的六个字段存储的原因,可能当时的甲方认为最多不超过六个。

但是我也发现了,他们的设备并不固定,几乎所有的设备传感器都是外购的,也就是说他们并没有独立的生产能力;所有的硬件设施也是临时挑选,这就意味着他们后期很可能再次更换设备传感器,而且类型和数量并不固定。

总的来说,当时的情况有以下几点:

  • 一台设备的传感器类型和数量不固定
  • 每个传感器所包含的传感量不固定,比如温湿度传感器能测温度和湿度两个传感量
  • 每台设备还有开关量这种概念(这个一开始没说清楚,导致设计并未考虑到,当然后期做了补救)

如果设计者是你,你会如何设计?

我们来捋一捋单列模型和多列模型的优缺点:

多列模型

就是把一台设备的所有传感量存储在一张表,或者把同一个传感器的传感量存储在一张表里。如果是前者,会陷入到研究生的那种设计中,我到底该设计几个字段,是六个吗?你真的能确定甲方不会加到六个以上吗?(实际上现在他们已经加到了16个,很庆幸当初没沿用之前的设计);如果是后者,你要确定多少个超级表呢?每加一个传感器类型就加一个吗?你确定加的完吗?尤其是在设备传感器还都确定的情况下。

当然这也有好处,就是它的存储空间小,速度也快(同一时间上传的数据只需写入一次即可)。

单列模型

每个传感量一张表,我只需要创建固定的八张不同类型的表即可。优点是灵活,能很好满足用户的情景,缺点是效率低。

综合下来,我选择了单列模型去进行数据建模,因为单列模型不仅很好满足用户需求,同时公司规模不大,注定不可能有过多(10000以上)的设备接入系统。与其去追求那可忽略不计的性能,不如选择切实可行的设计方案来极大提升系统的灵活性。

③如何抽象出物联网设备的通用模型?

以下所讲大概率不全面,以下设计都是根据我接触到的设备来说的。

我们先梳理一下现实中的物理模型是怎么样的 以我的甲方举例,他们所做的是对环保设备加装各种传感器,比如污水处理可能会有流量,温湿度,光氧这种,同时利用继电器进行设备控制,其物理模型如下:

在这里插入图片描述

其实上图已经有了建模的味道,现实中的所表现出的往往会更复杂,更难寻找规律。

首先我们明确一台设备有哪些东西:

传感器(数据收集器)

这里的传感器并不一定是我们严格意义上的传感器,这里的传感器更贴切的叫法应该叫数据收集器,用于收集各种数据,比如电流传感器,温湿度传感器,gps定位等等。

控制器

控制器是可以对设备进行控制的装置,目前我接触的一般是继电器控制(甲方采用的方案),肯定还有其他控制设备的装置,所以我把它叫做控制器而不叫做继电器。

根据物理模型,我们转化为我们需要的数据模型。

首先设计出的数据模型需要符合以下几点:

  • 能够灵活配置,比如传感量、传感器、控制器的数量、含义等信息可以自定义
  • 对于普通用户而言不能暴露太多复杂度
  • 对于开发人员而言可以灵活配置设备实现高度可自定义

于是我设计出以下数据模型:

在这里插入图片描述

先统一解释一下各个概念:

设备:这里的设备和物理意义上的设备相似,是一种虚拟设备,你可以把你监控的设备当做一台设备,比如生产机床;也可以是某些非常规意义上的设备,比如你可以把一个房间当做一台设备。这里的设备更准确的说是一种虚拟的集合,它可能赋予了特定的现实意义,或者仅仅是为了方便管理某些传感器和控制器。

传感量:传感器测出来的数据量。比如温湿度传感器测出来的温度、湿度等,一般用于表示设备的某种信息。

传感器(数据收集器):这里的传感器并不一定是我们严格意义上的传感器,这里的传感器更贴切的叫法应该叫数据收集器,用于收集各种数据,比如电流传感器,温湿度传感器,gps定位等等。传感器除了需要自定一些基本信息外,最重要的是需要绑定传感量,这样传感器上传的数据才能存储到对应的传感量中。

控制器:控制器是可以对设备进行控制的装置,目前我接触的一般是继电器控制(甲方采用的方案),肯定还有其他控制设备的装置,所以我把它叫做控制器而不叫做继电器。

传感器类型:为了进一步抽象的产物,比如定义某个型号的传感器为某种类型(因为一个型号的传感器可以有同一种解析方式,同样的传感量绑定规则),传感器类型可以自定义

传感量类型:为了进一步抽象的产物,主要规定了传感量的存储方式、单位等。比如温度就是一个传感量类型,这种类型可以自定义。

上述设计中,我抽象出了很多概念,比如传感器类型、传感量、控制量、传感器类型、传感量类型。这些东西一方面是为了更好地抽离物联网领域的各个元素,建立出一个统一模型;另一方面是为了方便将展示层和配置层分离开来,在给用户展示时,用户肯定不想了解这个温度是从哪个传感器测出来,他们只想看到这台设备的这些指标,比如温度、湿度;同时他们并不想知道这些控制量(比如设备开关、风扇档速)是由继电器控制的还是由其他装置控制的,他们只想知道这些控制量可以控制设备,至于如何控制这不关用户的事情。

抽离这些元素可以更好的展示设备信息,既能兼顾开发人员对设备模型的自由配置,又能对普通用户屏蔽部分复杂性。

④如何进行数据库表设计?

设计思路有了,那么具体实施就相对容易了。

首先我们有两个数据库(数据量不大的情况下,暂时不考虑分库分表情况),一个是mysql数据库,一个是TDengine数据库。

mysql数据库主要存储业务数据,可以建立如下表:设备信息表、传感量表、传感器表、传感量类型表、传感器类型表、控制器表、控制量表

TDengine数据库主要存储时序数据,对应到此场景中就是传感量的值,我们可以根据值的类型确定几个超级表,然后每新增一个传感量时就根据其数据类型(float、int、string这种)来创建对应的普通表,每次写入数据时,写入其传感量表即可。

4.如何与设备通讯?

①设备如何与系统连接

这其实是物联网一个非常大的问题,在这里我们采用了业界已有的解决方案。 首先是MQTT协议,

MQTT(消息队列遥测传输)是ISO 标准(ISO/IEC PRF 20922)下基于发布/订阅范式的消息协议。它工作在TCP/IP协议族上,是为硬件性能低下的远程设备以及网络状况糟糕的情况下而设计的发布/订阅型消息协议,为此,它需要一个消息中间件 。 MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。MQTT协议是轻量、简单、开放和易于实现的,这些特点使它适用范围非常广泛。在很多情况下,包括受限的环境中,如:机器与机器(M2M)通信和物联网(IoT)。其在,通过卫星链路通信传感器、偶尔拨号的医疗设备、智能家居、及一些小型化设备中已广泛使用。

然后我们采用了开源物联网 MQTT 消息服务器EMQ X

EMQ X (Erlang/Enterprise/Elastic MQTT Broker) 是基于 Erlang/OTP 平台开发的开源物联网 MQTT 消息服务器。

Erlang/OTP是出色的软实时 (Soft-Realtime)、低延时 (Low-Latency)、分布式 (Distributed)的语言平台。

MQTT 是轻量的 (Lightweight)、发布订阅模式 (PubSub) 的物联网消息协议。

EMQ X 设计目标是实现高可靠,并支持承载海量物联网终端的MQTT连接,支持在海量物联网设备间低延时消息路由:

  • 稳定承载大规模的 MQTT 客户端连接,单服务器节点支持50万到100万连接。
  • 分布式节点集群,快速低延时的消息路由,单集群支持1000万规模的路由。 消息服务器内扩展,支持定制多种认证方式、高效存储消息到后端数据库。
  • 完整物联网协议支持,MQTT、MQTT-SN、CoAP、LwM2M、WebSocket 或私有协议支持。

至于平台与EMQ X消息服务器的连接我们采用MQTT客户端 eclipse paho去实现。

大致如下:

在这里插入图片描述

这里我们仅仅是采用直接连接的方式,主要原因是我当时刚接触这东西,不太了解。其实更优的方案应该是EMQ服务将消息发送到消息中间件中,比如RabbitMQ,然后再由系统接收,这样可以提高系统吞吐量。当然了,在直接连接的时候也用了SpringBoot的监听器实现了异步处理,性能也还过得去。

②如何判断设备状态

这里最优的设计应该是直接利用EMQ X的设备下线功能的,不过当时由于EMQ服务器是由物电老师负责的,我不清楚它的功能,所以自己想了个简单巧妙的方法来判断设备状态: 在每次解析数据后,系统会将数据刷入对应的redis缓存(实时数据)中,设置这个缓存生命时长为五分钟。每次前端获取实时数据都会去这个缓存里取。 如果五分钟内设备没有更新数据(包括gps)时,该缓存就失效了,此时如果前端发来请求获取设备数据再去缓存里找就找不到了,与此同时会更新设备状态,发送离线消息。

这种方法虽然巧妙,但是有个问题,离线消息发送不及时。

③如何解析完全不一致的设备数据

设备可以连接了,也可以判断设备状态了,那么我们如何解析数据呢?

或许这个问题比较抽象,我们再具体一点, 比如现在有以下传感器:

在这里插入图片描述

别急,还有这种:

在这里插入图片描述

你以为完了吗?还有呢:

在这里插入图片描述

其实这就是我一开始面对的情况——解析格式复杂多样,种类不确定。

面对上述情况你会如何解决?

你可能会想到以下几种方案:

  • 每个传感器类型写一套解析代码 反问:你确定写的完吗?
  • 对上述的解析方式进行抽象,写出通用的解析代码 反问:你确定你能高度概括如此异构的解析方式?
  • 对每种方式进行分析,进行归纳汇总,写出几种通用的解析代码 反问:系统如何识别传感器数据该用哪种解析方式?

其实我选择的是第三种,那么问题来了,系统如何识别传感器数据该用哪种解析方式?

当时我使用了一个之前设计中常用的手法——抽离不确定性。我抽离出一个概念——解析规则。

解析规则,顾名思义,就是规定了如何解析数据。它是一串字符串,用事先规定好的语言去确定一套解析规则,让程序看到这串字符串就知道如何去解析数据。

举个例子,在我之前展示的温湿度传感器和风速传感器中,它们数据解析规则很像,比如起始地址都是0x00 0x00,每个传感量的数据长度都是0x00 0x01,其规定的含义也类似,那么我们完全可以将其归为一类(实际上和老师沟通后确实是的,这类传感器都有类似的格式)。那么我们可以定义以下解析规则:

A--通用模拟量(Analog)

每个分隔符之间的数字表示一个传感量的解析方式,第一位表示长度,第二位表示精度。例如A-21-21,两个“-21”表示有2个传感器数据,其中的“2”表示有2个字节,“1”表示精度(除以10^1)

对于后一种继电器数据我们可以另外再定义一种解析规则:

B--通用开关量(Binary)

格式后面跟一串二进制数字,0表示该位无效,1表示该位有效

B-00001111,表示低4位有效

B-0000110100001111

这种解析规则的好处就在于它是完全可以穷举完的,这也意味着我们完全可以事先编写几套固定的解析代码,然后在设置传感器类型时把解析规则配置。

在这里插入图片描述

④如何做到反向控制

有了之前mqtt连接的基础,那么我们很容易设计出反向控制的方案。

我们只需规定一个事件,当用户点击了控制量的开关,那么系统就会向EMQ消息服务器发送一条消息,然后订阅该消息的设备便会接收到消息,此时设备做出相应的变化。

在这里插入图片描述

比如,我点击了设备温控开关(一个控制量),那么系统就会向EMQ服务器发布对应的消息事件,由于我们是用继电器实现的,那么我们需要发送对应消息内容就是控制串的16位crc冗余校验码。在EMQ服务器接受到消息后会转发给对应的设备,设备做出响应。

事实上,实践遇到的问题会更复杂,这个会在工程实践中讲。这也从侧面看出设计只是理论上的,那些所谓完美的设计在实践中也会出现各种各样的问题。

5.如何设计物联网领域的通用解决方案

在玛嘉环境物联网平台设计之初,我便考虑到了各种可能的情况,比如设备传感器不固定,型号不固定等等,而我也根据当时的物理模型抽象出一套较为通用的物联网模型。

在这里插入图片描述

根据这套模型,我实现了企业的需求,同时系统本身就具有一种通用性,这种通用性不仅体现在低代码开发平台jeecg,更体现在项目之初的各种通用性设计。

至此,只要符合接入规则的设备便能通过简单配置接入平台,此时我们可以提供诸如查看设备状态,查看设备历史数据,对设备进行反向控制等基本服务。

有了这些后,我萌生了一个大胆的想法——能不能开发一个适用于物联网的通用平台,和那些低代码平台做的事情一样,提供一些通用物联网服务,让任何人,不限行业、不限规模地接入平台,获取到定制化的物联网服务呢?

于是我开发了第二个版本——通用物联网平台。

①设备服务

该版本最重要的功能就是可以提供各种定制化的服务,比如设备资源预约服务,针对的就是诸如停车场、图书馆这种需要资源预约的场景;又比如数据预测服务,可以根据已有的历史数据预测未来的数据变化,适用于铁塔监测、设备监测这种需要预测未来的场景;再比如自动报警服务....

最重要的是这些服务本身可以自由配置,效果图如下:

在这里插入图片描述

这样就实现定制化的服务需求,不同行业的人可以根据自己需要配置获取自己想要的服务。

②多种运营模式

我们可以设想一下,这样做就相当于在各种物联网平台上又做了一层抽象,构建出一套通用的物联网模型。如果有企业或者个人想要监控设备,获取对应的物联网服务,只需接入平台即可(事实上,这种需求很多,比如一些制造业就需要信息化,而他们最想做的事情就是监控自己的设备,而他们又不想单独开发一个平台,那么接入已有的平台便是最好的选择)

我们可以设置企业账号,个人账号,管理自己的设备,为不同用户提供不同的服务。

但是,一个令人沮丧的消息是——阿里云Iot实际上也就在做类似的事情,这也是我将其写的差不多的时候才发现的

三、工程实践中遇到的问题

1.团队问题

①技术水平技术不一

团队成员技术水平不一,当初在接到任务时,也分了工,比如我负责整个项目的设计和把控项目进度,以及后端的核心数据解析,其他人负责诸如前端开发,后端业务等等。

但是问题也随之出现了,正如我之前担心的那样,团队成员工程实践能力差距太大了,我布置一个任务,有时一周都没个解决方案,或者呈现出的东西离我想要的效果差距很大。

②沟通成本

再加上一个很麻烦的问题——沟通成本,每次布置任务都需要开会花时间讲清楚。他们也会遇到很多问题,有时候就来问我,原本我是想分化问题来减轻负担,但往往问题又抛回给我,我如果一一解答,那还不如我自己去做。

③自身水平受限

当时我很多问题也并没有明确的解决思路,往往能给的建议也很有限,以我当时的技术水平,无法做到统筹全局。因为很多问题我自己都需要一一去探寻,并没有一个明确的解决思路。

眼看着企业那边不断催促,老师也不断问我们这边的项目状况,我最终在完成自身的任务的情况下,去着手解决其他问题,尝试最快满足企业基本需求。

最终项目部署上线并逐步更新迭代,逐渐演变成个人项目。

2.部署问题小记

在遇到一些问题时,我习惯把解决问题的过程记录下来,方便后面回顾总结,以下是当时部署遇到的问题小记:


安装宝塔面板,链接phpmyadmin后分别报502和403的错误,发现是因为php没下,后来又因为php版本过高重新下了个低版本的,后来又报错,原因是phpmyadmin版本过低,最后将两者重新安装到最新版本后使用正常

执行sql文件发现因为开发时用的是mysql8,而生成环境用的是mysql5.7,sql无法执行,改了一下sql最终执行成功

前端找不到依赖,折腾两三小时后通过下载淘宝的cnpm,并执行cnpm install解决

后端打成jar包出现问题,折腾了一会后加了个SpringBoot的打包插件(一开始jar包找不到,发现是版本没配),打包后发现报包已损坏的信息,然后根据网上做的去弄结果还是不行,折腾了好久发现按照文档说的打包方式就可以了,之前是因为多模块的项目只打包了子模块,后面几次是因为上传没有上传完整就开始java -jar执行了

运行后发现后台报错,连接出问题,原因是生产环境的配置有问题,修改后并且同时修改了环境(包括redis和mysql的配置)

后来又报错说没有QRTZ_LOCKS这个表,一开始以为真没有然后又执行了一遍sql文件,发现是有的,但是是小写,我就怀疑是不是mysql5是区分大小写的(开发环境中的mysql8是不区分的),在网上搜索证实了我的猜测,最终改了mysql的配置文件解决问题

按照步骤配置Nginx后发现网站无法访问,我尝试不复制dist文件夹而是复制文件夹里的文件,还是不可以,搜了之后发现问题在于宝塔面板没有绑定相对应的域名,绑定后可以正常访问

但是前后端都部署后发现前端页面虽然可以正常访问,但是验证码404,说明前端访问后端失败,仔细检查后端运行的日志文件发现能够正常运行,一开始我怀疑后端路径出问题了,所以打开相应的端口访问,虽然报404,但是说明后端已经部署上去了。接着我怀疑是前端配置的问题,仔细检查发现并没什么问题,然后开始上网找资料,尝试着将localhost改成了对应的ip地址,惊奇的发现可以访问了!(按理来说localhost应该也可以的才对,因为我部署前后端都是在一台服务器上的,而且mysql的路径也是localhost)

原因还没深究,总之弄完已经晚上10点了.....

后记

前后端虽然打通,但是外网访问后端报404,包括前端访问后端的swagger文档也是如此,我尝试修改前端里的文件,把后台接口的地址改为127.0.0.1,即自己访问自己,居然可以访问了。这说明访问127.0.0.1是可以访问后端,后端没有问题,但是外网访问不行

这里我估计是被防火墙拦截掉了,拦截掉也好,反正也是单机部署,省事,就这样了。

3.前端依赖导入问题

前端导入依赖依旧没有导入完整,试遍了网上很多很多方法,包括删了重新安装,更改仓库,用npm、yarn亦或者cnpm,重装vue,这些都没用,出现各种问题。仔细查看前端运行的提示,发现它缺少依赖,我试着导入相应的依赖,可是还有另外一些依赖没导入。于是我开始查看package.json里的dependencys,发现这些依赖都来自于devDependency,于是乎我就怀疑yarn install时并没有把这些依赖安装进去。但是根据百度得到的信息来看yarn install是可以把所有依赖都导入的。偶然间我发现了npm的配置production会导致这个原因,查看自己的配置确实是true,所以应该是因为这个原因导致yarn并没有将devDependency依赖导入。解决 npm 无法安装 devDependencies 下的依赖包的问题 - dkvirus的个人空间 - OSCHINA - 中文开源技术交流社区,于是更改后可以正常运行。不过引入sanding业务文件后发现依然报错,估计是因为有些依赖也没导入

image-20210809110559491

原来学长还有一些文件放在其他地方...

4.多数据源切换问题

这个问题的背景是这样的: 因为平台涉及到TDengine和mysql两个数据库,所以在业务处理中难免会操作这两个数据库。因为mysql和TDengine都是服务JDBC规范的,所以当时采取的解决方案是用dynamic-datasource-spring-boot-starter的多数据库源的启动器来控制多个数据源。

一开始用的还算正常,当时用了jeecg的Java增强功能实现传感量增加,业务逻辑要求操作mysql库和TDengine,这时,问题就出现了。

以下是当时的问题记录: 操作了同一个数据库源,

估计是增强时已经有数据注入,这时再用@DS可能会有一些问题

但是业务需要在增加传感量的时候创建相应的TDengine的表

解决方法有两个:

1.继续用在线开发,利用增强Java方式继续开发,不过数据源切换是个问题,需要另外写jdbc来解决

2.生成代码,自己去写业务,这样可以解决问题,但是在频繁的需求变动下,这种方式不方便,而有些效果代码生成器无法帮你生成

两种都试了,但是最后还是用第一种开发下去

一开始遇到无法获取数据源的问题,原本打算自己去创建,但是想着项目里已经有这个数据库连接池了,只是无法获取到。于是我开始看mybatisplus的相关源码,最终发现动态数据源被注册到了容器中,名字为dataSource

image-20210812204450961

image-20210812204430898

image-20210812204327345

最后通过获取这个DataSource获取TDengine的数据库连接池,然后根据此封装了个工具类

最终顺利通过测试,不过由于jeecg并没有提供批量删除的Java增强,所以批量删除传感量的时候,TDengine数据库将不会清除这些表

总结一下问题:问题出在多数据源切换,在一个方法中使用多个数据源无法使用dynamic-datasource-spring-boot-starter,需要另外编写JDBC代码去操作数据库,不过单独为此去创建数据源显然不太合适,因为在容器中已经存在了相应的数据库连接池,问题在于我怎么获取到它。通过阅读源码发现,动态数据源被注册到了容器中,名字为dataSource,最后通过根据名称注入来获取到了这个对象。

现在去看文档,发现其实是有数据库源切换的功能的,不过文档要收费!!!

5.redis缓存问题

当时想着对热点数据比如设备实时数据、设备信息等进行缓存,这个操作虽然提高了系统性能,但是与此同时引入了缓存一致性问题。当某些业务操作涉及增删改时,需要同时修改缓存和数据库,否则业务数据则会不一致。为此我特意封装了redis的缓存操作类来方便编码。

6.mybatis拼接sql语句问题

拼接高级查询出现问题,试了各种方法,不断调试,把拼接的sql去TDengine里运行,甚至想自己写jdbc,不过我突然想到#和$的区别,为了防止sql注入,#是通过jdbc的preparedStatement来实现的,如果把拼接的sql条件放进去会多个“,而且容易造成sql注入的字符会被转义。

mybatis $和#问题,之前感觉差不多,无非就是是否预编译的问题,但随着做项目后发现两者大有不同。对于$没有预编译(对应jdbc中Statement),是直接以执行的,适合用于自定义sql语句。而#它帮我们做了处理,比如对于字符串、时间等数据类型加了"‘"单引号。虽然真正写过jdbc的代码,也深知其麻烦,也了解它一些坑,但是用了封装后的工具,虽然这些麻烦已经看不见了,但是因为自己不了解mybatis的处理,也会踩一些坑。

7.redis使用map的问题

原先想着map可以集中管理,统计也方便,但是后来发现使用map不能单独设置键值的有效时间,这就导致很多原本应该失效的数据依然存在。所以最后统一都改成用前缀的方式。

8.设备定位出错

当时出现这个bug是产品上线后,然后企业那边发消息过来说有设备定位到了非洲,当时我在上课也没注意,下课后上线去看发现一切正常,这就很诡异了。

后来发现,我在部署调试的时候往往没有任何问题,但是在过了几天后,问题出现了——部分设备定位到了非洲,而这个问题也不是一直存在,往往忽隐忽现。

image-20210913174018546

而我们知道改bug的前提是复现bug,而这bug跟个幽灵似的,令人头疼。

当时我也对另一个现象很好奇,为什么gps异常的设备都会定位在非洲,于是我开始搜索,最后发现这个地方确实有些特殊——在火星坐标系中,GPS(0,0)正是这个点。

同时我还发现一个现象,异常的设备都是下线状态,于是我开始了猜测,是不是因为设备被关时上传了个(0,0)坐标?

为了验证这种猜测,我开始检索服务器的日志,发现设备确实上传了(0,0)坐标。

好了,问题找到了,是因为设备误上传了(0,0)坐标,可这貌似也不是软件层面上的问题啊!

没办法,问题还是要解决的,于是我对(0,0)坐标做了特殊的处理——不理它。

其实后面又出现了同样的问题,排查发现,它上传的(0,0)坐标并不一定是(0,0),而可能是(0.000,0.000)。 我当时的心情是这样的

在这里插入图片描述

于是我又对逻辑做了修改,将解析字符串先统一转化成浮点数,然后再比较,最后才解决了问题。

9.vue异步数据props传值问题

以下是当时的问题记录: 由于是异步数据,所以数据一开始是默认值,但是等异步数据来了之后就会有值,但是当你把异步数据放在data对象的子属性的子属性时便会失效(响应式)。

此时我犯了一个错误,便是指针赋值,其实props也是把对象引用传了过去,如果对象属性会变,那么它也会变(实际上就是引用误用)。解决方案就是把父对象给他或者创建一个对象,其属性便是真正的数据,而我们把该对象引用传递即可

监听问题,正常情况下,如果一个属性原本存在,直接重新赋值是可以监听到的。但是如果对象没有该属性,而你想手动通过自己去设置来增加新的属性值,这种情况下vue是监听不到的!!!

如果想被监听到时,可以通过 this.$set(this.obj,'num',3)的方式进行属性新增。


现在回过去看这个问题,其实涉及vue响应式的原理,由于当时是边学vue边做的项目,所以很多地方都是磕磕碰碰的,尤其是异步处理这块,我因为它纠结了好久。

10.前端服务器数据更新问题

当我更新了一个版本上线时,企业那边看到的依旧是老的版本,网上查询相关博文才发现是因为浏览器有缓存机制。 解决方案一个是修改nignx配置,设置缓存策略为不缓存

#解决前端缓存问题
location = /index.html {
	add_header Cache-Control "no-cache, no-store";
}

想要既能缓存又能在部署时没有问题,需要给静态文件名添加hash值。在webpack中,有些配置能让我们实现持久化缓存。

11.vue数据延迟更新问题

写代码的时候发现数据并没有更新,检索后最终发现问题在于没使用vue.nextTick方法,想当然以为数据会实时改变

Vue.nextTick(callback) 使用原理:

原因是,Vue是异步执行dom更新的,一旦观察到数据变化,Vue就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOm操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。 当你设置 vm.someData = 'new value',DOM 并不会马上更新,而是在异步队列被清除,也就是下一个事件循环开始时执行更新时才会进行必要的DOM更新。如果此时你想要根据更新的 DOM 状态去做某些事情,就会出现问题。。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。

注:在dom操作都要加一层nextTick


上述问题也是因为不熟悉vue原理导致的,想当然的以为数据会实时改变。

12.vue使用echarts问题

在vue中使用echarts时出现了以下异常——echarts.js:3066 Uncaught Error: setOption should not be called during main process

经过多番检索最终解决,以下是当时的问题记录:

跟vue一起用时出现了这个问题,解决了,就是不要让echart实例变成vue的响应式对象:

data() {
  return {
    id: 'chart-xxxxx',
    instance: null,
  };
},
mounted() {
  this.instance = Object.freeze({ chart: echart.init(document.getElementById(this.id)) });
},

tips: 超大对象放入vue 的data前最好freeze下,不然可能出现把整个页面卡住的性能问题,当然,你得明白自己在干啥

经测试,声明并初始化赋值option后,修改option属性,调用setOption方法就会报错。

目前解决的方案就是将其声明在组件内,只声明一次,每次都是重新初始化一个新的option。

如下面这样

option={
  title: {
    text: this.title,
      left: '1%'
  }
}

13.前端异步加载顺序

这个是当时js的多线程的异步操作不太会用记下的,当然最后解决了,去学习了下js的相关知识。

14.vue封装echarts问题

在用vue封装echarts的过程中遇到了很多问题,其中一个就是异步数据安装问题,当数据还没到达时,图表便已经安装。这个问题我也尝试了很多思路。最终在vue-echarts官方文档中找到了答案。

它的方案和我之前写的类似,主动向外提供渲染方法,当数据到达时再主动调用更新。但由于我并不清楚echarts的原理,调用setOption方法时发现图表并没有进行渲染,当我放在图表mounted方法里,图表渲染,不过是数据到达前的图表,其也没进行更新。

事实上,不怕你们笑话,我到现在也没找到vue封装echarts的最佳实践,网上的资料貌似也没给出一个明确的实践方案。目前项目中封装的图表也是经过很多调试最终做出来的,但自问也没有觉得十分优雅,这个问题待我深入理解了vue的原理并熟悉了echarts的使用后,再去研究一下吧。

15.byte和int转化问题

在写设备反向控制时,需要封装一个转16为crc校验码的方法,其中就踩了byte和int转化的坑(好吧,是我自己没注意...)

int转byte要注意位数的截取,byte是8bit,int是32bit,强转要注意后8位。 byte转int要注意高位补1的规则,常常要和0xFF进行&操作,即将高位清0。

16.嵌入式设备单线程传输问题

当我写完反向控制功能后,进行测试,可以成功控制。不过随着测试增多,我也发现,部分命令下发后设备并无反应。和老师沟通后才发现了问题所在——设备控制器(继电器)是单线程,有时候我通过平台的对设备下发控制命令,此时设备可能在上传自身信息,所以此时发的消息就被丢弃了。

为了避免这种情况,同时考虑到用户实际使用情况,我选择在间隔1s发2-3次命令(因为发送的命令实际上是设备的状态,相同命令即时多次被接收,也不会变更设备状态)。不过这样也有问题,就是如果用户短时间内开关,开关状态可能就不是最近一次的状态。为了避免这种情况,我选择在前端加相应提示,限制用户短时间内多次开关设备。

总结

总之,在做这个项目过程我遇到了很多困难和挑战,但是经过不断思考探索,最终也得到了较好的解决,这个过程极大地锻炼了我的项目设计能力和工程实践能力,从这次项目中我也得到了不少有价值的经验教训。我个人对于这次经历还是挺满意的。

上述项目总结其实也有一些值得大家思考的地方,写此文的目的不仅是为了自己的总结,也是给同在路上的人一个参考,提供一个思路。

后记

愿大家以梦为马,不负青春韶华。

与君共勉!

  • 作者:金昊霖
  • 发表时间:2020-7-04
  • 版权声明:自由转载-非商用-非衍生-保留署名(创意共享3.0许可证)
  • 评论

    留言