JavaEE精英進階課學習筆記《博學谷》

第1章 億可控系統分析與設計

學習目標

  • 瞭解物聯網應用領域及發展現狀
  • 能夠說出億可控的核心功能
  • 能夠畫出億可控的系統架構圖
  • 能夠完成億可控環境的準備並瞭解億可控的功能結構
  • 完成裝置管理相關功能的開發

1.物聯網行業分析

1.1 什麼是物聯網

物聯網(英文:Internet of Things,縮寫:IoT)起源於傳媒領域,是資訊科技產業的第三次革命。物聯網是指通過資訊感測裝置,按約定的協議,將任何物體與網路相連線,物體通過資訊傳播媒介進行資訊交換和通訊,以實現智慧化識別、定位、跟蹤、監管等功能。

在物聯網應用中有三項關鍵技術,分別是感知層、網路傳輸層和應用層。

中國式物聯網定義:

最簡潔明瞭的定義:物聯網(Internet of Things)是一個基於網際網路、傳統電信網等資訊承載體,讓所有能夠被獨立定址的普通物理物件實現互聯互通的網路。它具有普通物件裝置化、自治終端互聯化和普適服務智慧化3個重要特徵。

上圖中出現了四個概念,我們這裡分別解釋一下:

兩化融合是資訊化和工業化的高層次的深度結合, 是指以資訊化帶動工業化、以工業化促進資訊化,走新型工業化道路;兩化融合的核心就是資訊化支撐,追求可持續發展模式。

M2M全稱Machine to Machine,是指資料從一臺終端傳送到另一臺終端,也就是機器與機器的對話。

射頻識別(RFID)是 Radio Frequency Identification 的縮寫。其原理為閱讀器與標籤之間進行非接觸式的資料通訊,達到識別目標的目的。RFID 的應用非常廣泛,典型應用有動物晶片、汽車晶片防盜器、門禁管制、停車場管制、生產線自動化、物料管理。

感測網是感測器網路的簡稱,感測器網路是集計算機通訊、網路、智慧計算、感測器、嵌入式系統、微電子等多個領域交叉綜合的新興學科,它將大量的多種類感測器節點(集感測、採集、處理、收發於一體)組成自治的網路,實現對物理世界的動態智慧協同感知。

從上圖中可以看出,物聯網涵蓋了上邊所提到的四大領域。

“一句式”理解物聯網

把所有物品通過資訊感測裝置與網際網路連線起來,進行資訊交換,即物物相息,以實現智慧化識別和管理。

歷史溯源

物聯網這個概念,中國在1999年提出來的時候叫感測網。中科院早在1999年就啟動了感測網的研究和開發。與其它國家相比,我國的技術研發水平處於世界前列,具有同發優勢和重大影響力。

2005年11月27日,在突尼西亞舉行的資訊社會峰會上,國際電信聯盟(ITU)釋出了《ITU網際網路報告2005:物聯網》,正式提出了物聯網的概念。

2009年8月24日,中國移動總裁王建宙在臺灣公開演講中,也提到了物聯網這個概念。

工信部總工程師朱巨集任在中國工業執行2009年夏季報告會上表示,物聯網是個新概念,到2009年為止還沒有一個約定俗成的,大家公認的概念。他說,總的來說,“物聯網”是指各類感測器和現有的“網際網路”相互銜接的一種新技術。

物聯網是在計算機網際網路的基礎上,利用RFID、無線資料通訊等技術,構造一個覆蓋世界上萬事萬物的“Internet of Things”。在這個網路中,物品(商品)能夠彼此進行“交流”,而無需人的干預。其實質是利用射頻自動識別(RFID)技術,通過計算機網際網路實現物品(商品)的自動識別和資訊的互聯與共享。

物聯網概念的問世,打破了之前的傳統思維。過去的思路一直是將物理基礎設施和IT基礎設施分開,一方面是機場、公路、建築物,另一方面是資料中心,個人電腦、寬頻等。而在物聯網時代,鋼筋混凝土、電纜將與晶片、寬頻整合為統一的基礎設施,在此意義上,基礎設施更像是一塊新的地球。故也有業內人士認為物聯網與智慧電網均是智慧地球的有機構成部分。

1.2 物聯網應用領域

1、智慧家居

智慧家居是利用先進的計算機技術,運用智慧硬體(氦氪wifi、Zigbee、藍芽、NB-iot等),物聯網技術,通訊技術,將與傢俱生活的各種子系統有機的結合起來,通過統籌管理,讓家居生活更舒適,方便,有效,與安全。智慧家居主要包括智慧音箱、智慧燈、智慧插座、智慧鎖、智慧恆溫器、掃地機器人等。

2、智慧交通

智慧交通,是將物聯網、網際網路、雲端計算為代表的智慧感測技術、資訊網路技術、通訊傳輸技術和資料處理技術等有效地整合,並應用到整個交通系統中,在更大的時空範圍內發揮作用的綜合交通體系 [2] 。智慧交通是以智慧路網、智慧出行、智慧裝備、智慧物流、智慧管理為重要內容,以資訊科技高度整合、資訊資源綜合運用為主要特徵的大交通發展新模式。依託迪蒙科技在雲端計算、物聯網、大資料、金融科技等領域的豐富開發經驗和雄厚的技術積累,歷時3年傾力打造的中國目前首家 一款集網約專車、智慧停車、汽車租賃、汽車金融,以及其他智慧出行領域創新商業模式於一體的高階智慧交通整體解決方案 [3] 。

4、智慧電網

智慧電網是在傳統電網的基礎上構建起來的集感測、通訊、計算、決策與控制為一體的綜合數物複合系統,通過獲取電網各層節點資源和裝置的執行狀態,進行分層次的控制管理和電力調配,實現能量流、資訊流和業務流的高度一體化,提高電力系統執行穩定性,以達到最大限度地提高裝置效利用率,提高安全可靠性,節能減排,提高使用者供電質量,提高可再生能源的利用效率。

4、智慧城市

智慧城市就是運用資訊和通訊技術手段感測、分析、整合城市執行核心系統的各項關鍵資訊,從而對包括民生、環保、公共安全、城市服務、工商業活動在內的各種需求做出智慧響應。其實質是利用先進的資訊科技,實現城市智慧式管理和執行,進而為城市中的人創造更美好的生活,促進城市的和諧、可持續成長。

隨著人類社會的不斷髮展,未來城市將承載越來越多的人口。目前,我國正處於城鎮化加速發展的時期,部分地區“城市病”問題日益嚴峻。為解決城市發展難題,實現城市可持續發展,建設智慧城市已成為當今世界城市發展不可逆轉的歷史潮流。

智慧城市的建設在國內外許多地區已經展開,並取得了一系列成果,國內的如智慧上海、智慧雙流;國外如新加坡的“智慧國計劃”、韓國的“U-City計劃”等 。

5、其它領域:智慧汽車、智慧建築、智慧水務、智慧商業、智慧工業、平安城市、智慧農業、智慧安防、智慧醫療等。

1.3 物聯網發展現狀

消費級IOT蓬勃發展,仍處初級階段

物聯網通過相關裝置將物與物、人與人進行聯網。

(1)規模:全球物聯網產業規模自 2008 年500億美元增長至 2018 年僅 1510 億美元,年均複合增速達 11.7%。我國物聯網產業規模2017年達 11500億元,自 2011 年起進一步加速,2009-2017 年均複合增速達 26.9%,我國物聯網發展速度較全球平均水平更快。

(2)滲透:全球物聯網行業滲透率 2013、2017 分別達 12%、29%,提升一倍多,預計2020年有超過 65%企業和組織將應用物聯網產品和方案。近年來,我國物聯網市場規模不斷擴大,2012年的 3650 億元增長到 2017 年的 11605 億元,年複合增長率高達 25%。

2012-2017年我國物聯網市場規模(億元)

全球物聯網滲透率變化

消費級物聯網:仍處於初級階段

消費級IOT預計快速增長。

(1)全球:2017全球消費級IOT硬體銷售額達4859億美元,同比增長29.5%,2015-2017 複合增速達 26.0%。2022 年銷售額望達 15502 億美元,2017-2022 年均複合增速達 26.1%。全球消費級 IOT 市場規模呈現進一步加速的趨勢。

(2)中國大陸: 2017 中國大陸消費級 IOT 硬體銷售額達 1188 億美元,同比增長 30.0%,2015-2017 複合增速達 28.9%。2022年銷售額望達 3118 億美元, 2017-2022 年均複合增速達 21.3%。2017年前因小米等公司的快速發展,中國消費級 IOT 發展整體快於全球平均水平,2017 年後在中國消費級 IOT 仍維持高速發展的狀況下,全球消費級 IOT 將發展更快。(3)連線裝置:全球消費級 IOT 終端數量 2017年達 49 億個, 2015-2017 年均複合增速達 27.7%,預計 2022 年達 153億個, 2017-2022 年均複合增速達 25.4%。 2017 中國消費級 IOT 終端數量佔世界達 26.5%,預計 2022 年佔比提升至 29.4%, 2017-2022 預計複合增速達 28.2%。

全球消費級IOT市場規模:



中國消費級IOT市場規模:



全球及中國IOT終端數量:

2.億可控需求分析

2.1 需求概述

​ 億可控作為一箇中臺,對裝置執行狀況進行實時線上監測、預警,不做業務相關的功能。

​ 核心功能列表:

​ (1)報文資料採集與指標解析 :整個系統的資料來源是通過接收裝置傳送過來的報文訊息,在系統中定義主題和訊息內容欄位的指標資料為過濾條件,從而對訊息進行收集和分析。

​ (2)報警監控 : 通過和系統中定義的各種告警級別資料進行對比,一旦發現觸發到告警級別的訊息,就會通過和告警關聯配置的webhook來將告警資訊透傳到其它系統

​ (3)GPS定位監控 :採集每臺裝置的GPS定位,並提供裝置位置查詢功能。

​ (4)資料看板 : 提供豐富的自定義資料看板。

2.2 業務架構圖

從上圖我們可以看到,真個系統從業務上分為6大功能模組:圖形監控模組、資料詳情展示模組、看板管理模組、裝置管理模組、報警管理模組、系統管理模組。

2.3 核心業務描述

產品原型地址:

https://app.mockplus.cn/run/prototype/yYVLQlJ-YN6/JhE4uVilt/4nw_LQ8n7

詳見資源提供的《億可控PRD文件》

3.億可控系統架構

3.1 系統架構圖

整個系統的技術架構圖如下:

預製資料將放入MySQL裡進行儲存,裝置上報的指標資料包括告警資料將存入influxDB中,裝置的地理位置資訊資料存入到ES中以便後期搜尋。為了提高系統的執行穩定性,有些頻繁訪問的資料儲存在redis中,因為考慮到裝置上報的資料是非常頻繁的,如果單單隻依靠MySQL資料庫的話,會很容易將MySQL伺服器的CPU的佔用率搞到100%,從而會引發整個系統的崩潰無法使用。

一些基本的配置放入到了consul的配置中心,考慮到系統的橫向擴充套件能力,將整個系統基於Consul做註冊中心來搭組建一個微服務。

3.2 資料庫設計

mysql資料庫有5個表:

管理員表tb_admin

列名 資料型別 說明
id int 表主鍵id,自增
login_name varchar(50) 登入賬號
password varchar(60) 密碼
type tinyint 型別 1:超級管理員 2:普通使用者 目前作為保留欄位
board varchar(50) 看板列表

指標配置表tb_quota

列名 資料型別 說明
id int 表主鍵id
name varchar(50) 指標名稱
unit varchar(20) 指標單位
subject varchar(50) 報文主題
value_key varchar(50) 指標值欄位
sn_key varchar(50) 裝置識別碼欄位
webhook varchar(1000) web鉤子
value_type varchar(10) 指標欄位型別,Double、Inteter、Boolean
reference_value varchar(100) 參考值

報警配置表tb_alarm

列名 資料型別 說明
id int 表主鍵id,自增
name varchar(50) 報警指標名稱
quota_id int 關聯指標名稱
operator varchar(10) 運算子
threshold int 報警閾值
level int 報警級別 1:一般 2:嚴重
cycle int 沉默週期(以分鐘為單位)
webhook varchar(1000) web鉤子地址

面板配置表tb_board

列名 資料型別 說明
id int 表主鍵id,自增
admin_id int 管理員id
name varchar(50) 看板名稱
quota varchar(100) 指標
device varchar(100) 裝置
system tinyint 是否是系統看板
disable tinyint 是否不顯示

GPS配置表tb_gps

列名 資料型別 說明
id bigint 表主鍵id
subject varchar(50) 報文主題
sn_key varchar(50) 裝置識別碼欄位
type tinyint 型別(單欄位、雙欄位)
value_key varchar(50) 經緯度欄位
separation varchar(10) 經緯度分隔符
longitude varchar(20) 經度欄位
latitude varchar(20) 維度欄位

4.基礎程式碼解析

4.1 環境準備

4.1.1 載入虛擬機器映象

使用課程配套的虛擬機器映象。

網路連線建議使用NAT模式。

本課程講義中提供的程式碼,192.168.200.128為宿主機IP,如果你載入映象後不是此IP請自行調整。

已安裝好docker環境,並已拉取了所需映象,開箱即用。

4.1.2 MySQL建庫建表

連線虛擬機器的mysql ,使用者名稱root ,密碼root123

建立資料庫ykk,建立表

create table if not exists tb_admin
(
id int auto_increment
primary key,
login_name varchar(50) null comment '登入名',
password varchar(60) null comment '密碼',
type tinyint null comment '型別 1超級管理員 0普通使用者',
board varchar(50) null comment '看板'
); create table if not exists tb_alarm
(
id int auto_increment comment 'id'
primary key,
name varchar(50) null comment '報警名稱',
quota_id int null comment '指標id',
operator varchar(10) null comment '運算子',
threshold int null comment '報警閾值',
level int null comment '報警級別 1一般 2嚴重',
cycle int null comment '沉默週期(分鐘)',
webhook varchar(1000) null comment 'web鉤子',
constraint tb_alarm_name_uindex
unique (name)
); create table if not exists tb_board
(
id int auto_increment comment 'id'
primary key,
admin_id int default 1 null comment '管理員id',
name varchar(50) null comment '看板名稱',
quota varchar(100) default '0' null comment '指標(趨勢時設定)',
device varchar(100) null comment '裝置(累計)',
`system` tinyint default 0 null comment '是否是系統看板',
disable tinyint default 0 null comment '是否不顯示',
constraint tb_board_name_uindex
unique (name)
); create table if not exists tb_gps
(
id int not null comment 'id'
primary key,
subject varchar(50) null comment '主題',
sn_key varchar(50) null comment '裝置識別碼欄位',
single_field tinyint null comment '型別(單欄位、雙欄位)',
value_key varchar(50) null comment '經緯度欄位',
separation varchar(10) null comment '經緯度分隔符',
longitude varchar(20) null comment '經度欄位',
latitude varchar(20) null comment '維度欄位',
constraint tb_gps_subject_uindex
unique (subject)
); create table if not exists tb_quota
(
id int auto_increment comment 'id'
primary key,
name varchar(50) null comment '指標名稱',
unit varchar(20) null comment '指標單位',
subject varchar(50) null comment '報文主題',
value_key varchar(50) null comment '指標值欄位',
sn_key varchar(50) null comment '裝置識別碼欄位',
webhook varchar(1000) null comment 'web鉤子',
value_type varchar(10) null comment '指標欄位型別,Double、Inteter、Boolean',
reference_value varchar(100) null comment '參考值',
constraint tb_quota_name_uindex
unique (name)
);

4.1.3 Consul新增配置

(1)進入Consul

開啟瀏覽器,輸入地址 http://192.168.200.128:8500/

(2)建立配置 key為  config/backend-service/data value如下

spring:
datasource:
url: jdbc:mysql://192.168.200.128:3306/ykk?useUnicode=true&autoReconnect=true&autoReconnectForPools=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root123
driver-class-name: com.mysql.jdbc.Driver
redis:
host: 192.168.200.128
port: 6379
database: 0
lettuce:
pool:
max-active: 10
max-wait: -1
max-idle: 5
min-idle: 1
shutdown-timeout: 100
timeout: 1000
password:

4.2 工程結構解析

專案主體框架截圖如下:

目前專案主要分為兩個部分:ykk-common和ykk-backend。

ykk-common模組存放系統的一些基礎通用性定義,包括通用異常定義、資料庫聯接定義、還有一些常量定義。

ykk-backend模組是我們後臺邏輯的實現程式碼,裡面按照具體的功能實現拆分到了具體的包裡。

4.3 核心程式碼解析

4.3.1 使用者登入與JWT校驗

(1)使用者登入業務邏輯

package com.yikekong.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.base.Strings;
import com.yikekong.entity.AdminEntity;
import com.yikekong.mapper.AdminMapper;
import com.yikekong.service.AdminService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service; @Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper,AdminEntity> implements AdminService{
@Override
public Integer login(String loginName, String password) {
if(Strings.isNullOrEmpty(loginName) || Strings.isNullOrEmpty(password)){
return -1;
}
QueryWrapper<AdminEntity> queryWrapper = new QueryWrapper<>();
queryWrapper
.lambda()
.eq(AdminEntity::getLoginName,loginName);
AdminEntity adminEntity = this.getOne(queryWrapper);
if(adminEntity == null)
return -1; BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
if(passwordEncoder.matches(password,adminEntity.getPassword())){
return adminEntity.getId();
} return -1;
}
}

(2)使用者登入控制器類

@RestController
public class AdminController{
@Autowired
private AdminService adminService; @PostMapping("/login")
public LoginResultVO login(@RequestBody AdminVO admin){
LoginResultVO result = new LoginResultVO();
Integer adminId = adminService.login(admin.getLoginName(),admin.getPassword());
if(adminId < 0){
result.setLoginSuccess(false);
return result;
}
result.setAdminId(adminId);
String token = JwtUtil.createJWT(adminId);
result.setToken(token);
result.setLoginSuccess(true); return result;
}
}

(3)登入校驗

httpfilter包裡AuthFilter是我們jwt的過濾器,主要來校驗jwt token,該類的實現如下:

package com.yikekong.httpfilter;

import org.elasticsearch.common.Strings;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component; import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException; @Component
@WebFilter(urlPatterns = "/*",filterName = "authFilter")
public class AuthFilter implements Filter{
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)servletRequest;
HttpServletResponse resp = (HttpServletResponse)servletResponse;
String path = ((HttpServletRequest) servletRequest).getServletPath();
//如果訪問的是login介面,不進行jwt token校驗
if(path.equals("/login")){
filterChain.doFilter(servletRequest, servletResponse);
return;
}
String authToken = ((HttpServletRequest) servletRequest).getHeader("Authorization");
//如何header中不存在Authorization的值,直接返回校驗失敗
if(Strings.isNullOrEmpty(authToken)){
((HttpServletResponse) servletResponse).setStatus(HttpStatus.UNAUTHORIZED.value());
return;
} try {
JwtUtil.parseJWT(authToken);
} catch (Exception e) {
//jwt校驗失敗,返回
((HttpServletResponse) servletResponse).setStatus(HttpStatus.UNAUTHORIZED.value());
return;
} filterChain.doFilter(servletRequest, servletResponse);
}
}

4.3.2 指標管理-建立指標

QuotaController的create方法用於建立指標

/**
* 建立指標
* @param vo
* @return
*/
@PostMapping
public boolean create(@RequestBody QuotaVO vo){
QuotaEntity quotaEntity = new QuotaEntity();
BeanUtils.copyProperties(vo,quotaEntity);
return quotaService.save(quotaEntity);
}

此方法接收的vo類,是前端的封裝檢視物件。有很多時候,前端傳遞過來的資料與我們後端資料庫對應的不一定完全一致,所以我們通常的做法是建立一個單獨的vo類,用於與前端進行資料的傳輸。這樣如果前端傳遞的資料物件傳送結構變化,並不會影響到後端資料庫結構。

BeanUtils.copyProperties(vo,quotaEntity); 用於物件資料的拷貝,如果兩個物件有相同的屬性,會自動複製屬性,這樣可以避免在程式碼中出現大量的setter方法。

5. 裝置管理

5.1 裝置新增

5.1.1 需求分析

在億可控系統中,我們不能也不需要從系統介面中新增裝置。裝置的新增,是在億可控接收到裝置發過來的報文,解析後儲存的。由於物聯網類的應用所使用的裝置數量可能非常龐大,而對這部分資料的讀寫頻率又很頻繁,所以我們使用elasticsearch作為裝置的資料庫。

5.1.2 索引庫結構設計

裝置庫 device

列名 資料型別 說明
deviceId keyword 裝置編號
alarm boolean 是否告警
alarmName keyword 告警名稱
level integer 告警級別
online boolean 是否線上
status boolean 開關
tag keyword 標籤

5.1.3 程式碼實現

(1)建立索引庫(開啟kibana建立 http://192.168.200.128:5601/

PUT /devices
{
"mappings": {
"properties": {
"deviceId": {
"type": "keyword"
},
"alarm": {
"type": "boolean"
},
"alarmName": {
"type": "keyword"
},
"level": {
"type": "integer"
},
"online": {
"type": "boolean"
},
"status": {
"type": "boolean"
},
"tag": {
"type": "keyword"
}
}
}
}

(2)pom.xml新增配置

<!--es相關依賴-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.7.1</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.7.1</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>7.7.1</version>
</dependency>
<!--es相關依賴結束-->

(3)在配置檔案中新增配置,以下配置新增到spring節點下


elasticsearch:
rest:
uris: http://192.168.200.128:9200

(4)建立包com.yikekong.dto , 建立用於封裝裝置的DTO類

package com.yikekong.dto;

import lombok.Data;

import java.io.Serializable;

/**
* 裝置DTO
*/
@Data
public class DeviceDTO implements Serializable { private String deviceId;//裝置編號 private Boolean alarm;// 是否告警 private String alarmName;//告警名稱 private Integer level;//告警級別 private Boolean online;//是否線上 private String tag;// 標籤 private Boolean status;//開關狀態 }

(5)建立com.yikekong.es包,包下建立ESRepository類,並編寫新增裝置的方法

package com.yikekong.es;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.yikekong.dto.DeviceDTO;
import com.yikekong.util.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map; @Component
@Slf4j
public class ESRepository{ @Autowired
private RestHighLevelClient restHighLevelClient; /**
* 新增裝置
* @param deviceDTO
*/
public void addDevices(DeviceDTO deviceDTO){
if(deviceDTO==null ) return;
if(deviceDTO.getDeviceId()==null) return;
IndexRequest request=new IndexRequest("devices");
try {
String json = JsonUtil.serialize(deviceDTO);
Map map = JsonUtil.getByJson(json, Map.class);
request.source(map);
request.id(deviceDTO.getDeviceId());
restHighLevelClient.index(request, RequestOptions.DEFAULT);
} catch (JsonProcessingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
log.error("裝置添加發生異常");
}
}
}

5.1.4 單元測試

編寫單元測試

import com.yikekong.YkkApplication;
import com.yikekong.dto.DeviceDTO;
import com.yikekong.es.ESRepository;
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; @SpringBootTest(classes = YkkApplication.class)
@RunWith(SpringRunner.class)
public class EsTest { @Autowired
private ESRepository esRepository; @Test
public void testAdd(){
DeviceDTO deviceDTO=new DeviceDTO();
deviceDTO.setDeviceId("123456");
deviceDTO.setStatus(true);
deviceDTO.setAlarm(false);
deviceDTO.setLevel(0);
deviceDTO.setAlarmName("");
deviceDTO.setOnline(true);
deviceDTO.setTag("");
esRepository.addDevices(deviceDTO);
}
}

查詢資料,驗證執行結果

GET devices/_search
{
"query": {
"match_all": {}
}
}

5.2 根據裝置ID查詢裝置

5.2.1 需求分析

根據id從elasticsearch中查詢裝置資訊。在之後的報文解析的邏輯中需要呼叫此方法來實現裝置的查詢。

5.2.2 程式碼實現

ESRepository類新增方法

/**
* 根據裝置id 查詢裝置
* @param deviceId 裝置id
* @return
*/
public DeviceDTO searchDeviceById(String deviceId){
SearchRequest searchRequest=new SearchRequest("devices");
SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.termQuery("_id",deviceId));
searchRequest.source(searchSourceBuilder);
try {
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); SearchHits hits = searchResponse.getHits();
long hitsCount = hits.getTotalHits().value;
if(hitsCount<=0) return null;
DeviceDTO deviceDTO=null;
for(SearchHit hit:hits){
String hitResult = hit.getSourceAsString();
deviceDTO=JsonUtil.getByJson(hitResult,DeviceDTO.class );
deviceDTO.setDeviceId(deviceId);
break;
}
return deviceDTO; } catch (IOException e) {
e.printStackTrace();
log.error("查詢裝置異常");
return null;
}
}

5.2.3 單元測試

編寫單元測試方法,驗證程式碼是否正確

@Test
public void testSearchById(){ DeviceDTO deviceDTO = esRepository.searchDeviceById("123456");
try {
String json = JsonUtil.serialize(deviceDTO);
System.out.println(json); } catch (JsonProcessingException e) {
e.printStackTrace();
} }

5.3 設定裝置狀態

5.3.1 需求分析

當我們不需要接收某裝置的報文,可以將其關閉。已經關閉的裝置對接收的報文指標不做任何處理。

5.3.2 API 介面

5.3.3 程式碼實現

(1)ESRepository類新增方法實現對裝置的開與關。

/**
* 更新裝置狀態
* @param deviceId
* @param status
* @return
*/
public boolean updateStatus(String deviceId,Boolean status){
UpdateRequest updateRequest=new UpdateRequest("devices",deviceId)
.doc( "status",status );
try {
restHighLevelClient.update( updateRequest,RequestOptions.DEFAULT );
return true;
} catch (IOException e) {
e.printStackTrace();
log.error("更新裝置狀態出錯");
return false;
}
}

(2)DeviceService新增方法定義

/**
* 更改裝置狀態
* @param deviceId
* @param status
* @return
*/
boolean setStatus(String deviceId, Boolean status);

DeviceServiceImpl實現方法

@Autowired
private ESRepository esRepository; @Override
public boolean setStatus(String deviceId, Boolean status) {
DeviceDTO deviceDTO = findDevice(deviceId);
if( deviceDTO==null ) return false;
return esRepository.updateStatus(deviceId,status);
} /**
* 根據裝置id查詢裝置
* @param deviceId
* @return
*/
private DeviceDTO findDevice(String deviceId){
DeviceDTO deviceDTO = esRepository.searchDeviceById(deviceId);
return deviceDTO;
}

(3)DeviceController新增方法

/**
* 設定狀態的介面
* @param deviceVO
* @return
*/
@PutMapping("/status")
public boolean setStatus(@RequestBody DeviceVO deviceVO){
return deviceService.setStatus(deviceVO.getSn(),deviceVO.getStatus());
}

5.3.4 介面測試

(1)執行YkkApplication啟動工程

(2)測試介面。為了方便測試,我們採用vscode的Rest Client外掛來進行測試。指令碼是課程提供的“資料\測試\yikekong.http” ,用vscode開啟,找到以下指令碼

####修改裝置狀態########
PUT http://{{hostname}}:{{port}}/device/status HTTP/1.1
Authorization: {{Authorization}}
Content-Type: {{contentType}} {
"sn":"123456",
"status":true
}

我們可以修改status值後 點選 Send Request 連結進行測試

5.4 設定裝置標籤

5.4.1 需求分析

我們為了方便之後對裝置進行查詢,我們可以對每個裝置設定一個或多個標籤。在前端介面上沒有更新裝置標籤的功能,此功能只對外部系統提供呼叫的介面。

5.4.2 程式碼實現

(1)ESRepository類新增方法

/**
* 更新裝置標籤
* @param deviceId
* @param tags
* @return
*/
public boolean updateDeviceTag(String deviceId,String tags){
UpdateRequest updateRequest=new UpdateRequest("devices",deviceId)
.doc( "tag",tags );
try {
restHighLevelClient.update( updateRequest,RequestOptions.DEFAULT );
return true;
} catch (IOException e) {
e.printStackTrace();
log.error("更新裝置標籤出錯");
return false;
}
}

(2)DeviceService新增方法

/**
* 更新裝置標籤
* @param deviceId
* @param tags
* @return
*/
boolean updateTags(String deviceId,String tags);

DeviceServiceImpl實現方法

@Override
public boolean updateTags(String deviceId, String tags) {
DeviceDTO deviceStatus = findDevice(deviceId);
if(deviceStatus == null) return false;
esRepository.updateDeviceTag(deviceId,tags);
return true;
}

(3)DeviceController新增方法

/**
* 設定標籤的介面
* @param deviceVO
* @return
*/
@PutMapping("/tags")
public boolean setTags(@RequestBody DeviceVO deviceVO){
return deviceService.updateTags(deviceVO.getSn(),deviceVO.getTags());
}

(4)AuthFilter類的doFilter新增程式碼,對tags放行

//tag介面不校驗token
if(path.contains("/device/tags")){
filterChain.doFilter(servletRequest, servletResponse);
return;
}

5.4.3 介面測試

(1)啟動工程

(2)找到以下指令碼,進行測試

####設定裝置標籤############
PUT http://{{hostname}}:{{port}}/device/tags HTTP/1.1
Content-Type: {{contentType}} {
"sn":"123456",
"tags":"學校"
}

5.5 更新裝置告警資訊

5.5.1 需求分析

當裝置傳送過來的報文中的指標資訊達到告警級別,我們應該更新elasticsearch中的更新裝置告警資訊(是否告警、告警級別、告警名稱)

5.5.2 程式碼實現

ESRepository類新增方法

/**
* 更新裝置告警資訊
* @param deviceDTO
* @return
*/
public boolean updateDevicesAlarm(DeviceDTO deviceDTO){
UpdateRequest updateRequest=new UpdateRequest("devices",deviceDTO.getDeviceId())
.doc( "alarm",deviceDTO.getAlarm(),//是否告警
"level",deviceDTO.getLevel(),//告警級別
"alarmName",deviceDTO.getAlarmName() );//告警名稱
try {
restHighLevelClient.update( updateRequest,RequestOptions.DEFAULT );
return true;
} catch (IOException e) {
e.printStackTrace();
log.error("更新裝置告警資訊出錯");
return false;
}
}

5.5.3 單元測試

編寫單元測試,在TestES中新增測試方法

@Test
public void testAlarm(){
DeviceDTO deviceDTO=new DeviceDTO();
deviceDTO.setDeviceId("123456");
deviceDTO.setAlarm(true);
deviceDTO.setLevel(1);
deviceDTO.setAlarmName("溫度過高"); esRepository.updateDevicesAlarm(deviceDTO); }

5.6 更新線上狀態

5.6.1 需求分析

線上狀態是指這個裝置是否線上,如果裝置存在網路故障就會導致裝置離線。億可控系統可以監測裝置的線上和離線狀態

5.6.2 程式碼實現

我們這裡需要在ESRepository類新增方法用於更新線上狀態。

/**
* 更新線上狀態
* @param deviceId
* @param online
* @return
*/
public boolean updateOnline(String deviceId,Boolean online){
UpdateRequest updateRequest=new UpdateRequest("devices",deviceId)
.doc( "online",online );
try {
restHighLevelClient.update( updateRequest,RequestOptions.DEFAULT );
return true;
} catch (IOException e) {
e.printStackTrace();
log.error("更新線上狀態出錯");
return false;
}
}

5.6.3 單元測試

編寫單元測試,在TestES中新增測試方法

@Test
public void testOnline(){
esRepository.updateOnline("123456",false);
}

5.7 分頁查詢裝置

5.7.1 需求分析

有兩個頁面需要實現分頁查詢裝置

(1)裝置管理,如下圖效果,需要裝置編號、標籤作為查詢條件分頁查詢

(2)裝置詳情,如下圖效果,需要裝置狀態、標籤、裝置編號作為查詢條件分頁查詢

裝置詳情頁比裝置管理多了一個“裝置狀態”的查詢條件,裝置狀態有四個值:線上(0)、離線(1)、一般告警(2)、嚴重告警(3) 。

為了不讓程式碼冗餘,我們這兩個功能可以用同一個方法實現。

5.7.2 程式碼實現

ESRepository類新增方法

/**
* 分頁查詢裝置
* @param page 頁碼
* @param pageSize 頁大小
* @param deviceId 裝置編號
* @param tags 標籤
* @param state 狀態
* @return
*/
public Pager<DeviceDTO> searchDevice(Long page,Long pageSize,String deviceId,String tags,Integer state){ SearchRequest searchRequest=new SearchRequest("devices");
SearchSourceBuilder sourceBuilder=new SearchSourceBuilder();
//條件查詢
BoolQueryBuilder boolQueryBuilder=QueryBuilders.boolQuery();
//裝置編號
if(!Strings.isNullOrEmpty(deviceId)) {
boolQueryBuilder.must(QueryBuilders.wildcardQuery("deviceId", deviceId + "*"));
}
//標籤
if(!Strings.isNullOrEmpty(tags) ){
boolQueryBuilder.must(QueryBuilders.wildcardQuery("tag","*"+tags+"*"));
}
//狀態(線上狀態和告警狀態) 0:線上 1:離線 2:一般告警 3:嚴重告警
if(state!=null){
if(state.intValue()==0){
boolQueryBuilder.must( QueryBuilders.termQuery("online",true));
}
if(state.intValue()==1){
boolQueryBuilder.must( QueryBuilders.termQuery("online",false));
}
if(state.intValue()==2){
boolQueryBuilder.must( QueryBuilders.termQuery("level",1));
}
if(state.intValue()==3){
boolQueryBuilder.must( QueryBuilders.termQuery("level",2));
}
}
sourceBuilder.query(boolQueryBuilder);
//分頁
sourceBuilder.from( (page.intValue()-1)*pageSize.intValue() );
sourceBuilder.size( pageSize.intValue() );
sourceBuilder.trackTotalHits(true); //排序
sourceBuilder.sort("level", SortOrder.DESC);//告警級別高的排前面
searchRequest.source(sourceBuilder);
try {
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); SearchHits hits = searchResponse.getHits();
SearchHit[] searchHits = hits.getHits();
List<DeviceDTO> devices= Lists.newArrayList();
for(SearchHit hit: searchHits){
String hitResult = hit.getSourceAsString();
DeviceDTO deviceDTO = JsonUtil.getByJson(hitResult, DeviceDTO.class);
devices.add(deviceDTO);
}
Pager<DeviceDTO> pager=new Pager<>( searchResponse.getHits().getTotalHits().value,pageSize );
pager.setItems(devices);
return pager;
} catch (IOException e) {
e.printStackTrace();
log.error("查詢裝置失敗");
return null;
}
}

(2)DeviceService新增方法定義

/**
* 搜尋裝置
* @param page
* @param pageSize
* @param sn
* @param tag
* @return
*/
Pager<DeviceDTO> queryPage(Long page, Long pageSize, String sn, String tag, Integer status);

DeviceServiceImpl實現方法

@Override
public Pager<DeviceDTO> queryPage(Long page, Long pageSize, String sn, String tag, Integer status) {
return esRepository.searchDevice(page,pageSize,sn,tag,status);
}

(3)DeviceController新增方法

/**
* 分頁搜尋裝置
* @param page
* @param pageSize
* @param sn
* @param tag
* @return
*/
@GetMapping
public Pager<DeviceDTO> findPage(@RequestParam(value = "page",required = false,defaultValue = "1") Long page,
@RequestParam(value = "pageSize",required = false,defaultValue = "10") Long pageSize,
@RequestParam(value = "sn",required = false) String sn,
@RequestParam(value = "tag",required = false) String tag){
return deviceService.queryPage(page,pageSize,sn,tag,null);
}