Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
D
dstTechnologyPlatform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
node_server
dstTechnologyPlatform
Commits
b29582fb
Commit
b29582fb
authored
Aug 06, 2026
by
PC-20251223ZVQQ\Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
部署第一版
parent
af021737
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
915 additions
and
231 deletions
+915
-231
区域坐标分布.xlsx
res/区域坐标分布.xlsx
+0
-0
serverConfig.xml
serverConfig.xml
+15
-7
dataIntegration.ts
src/biz/dataIntegration.ts
+318
-82
feiyiClient.ts
src/biz/feiyiClient.ts
+2
-2
region.ts
src/biz/region.ts
+236
-41
running.ts
src/biz/running.ts
+186
-87
businessEnum.ts
src/config/businessEnum.ts
+138
-0
mysqlTableConfig.ts
src/config/mysqlTableConfig.ts
+13
-7
schedule.ts
src/config/schedule.ts
+2
-1
region.ts
src/routers/region.ts
+5
-4
No files found.
res/区域坐标分布.xlsx
View file @
b29582fb
No preview for this file type
serverConfig.xml
View file @
b29582fb
<config>
<port>
13269
</port>
<sign></sign>
<img>
http://127.0.0.1:
13269
</img>
<img>
http://127.0.0.1:
80
</img>
<mysqldb>
<!-- 本地mysql配置 -->
<mysqlHost>
127.0.0.1
</mysqlHost>
<
!-- <
mysqlHost>127.0.0.1</mysqlHost>
<mysqlPort>3306</mysqlPort>
<mysqlUser>root</mysqlUser>
<mysqlPwd>123456</mysqlPwd>
<dataBase>
dst_technology_platform
</dataBase>
<dataBase>dst_technology_platform</dataBase>
-->
<!-- 服务器mysql配置 -->
<!-- <mysqlHost>127.0.0.1</mysqlHost>
<mysqlPort>13306</mysqlPort>
<mysqlUser>root</mysqlUser>
<mysqlPwd>root</mysqlPwd>
<dataBase>dst_technology_platform</dataBase> -->
<!-- dst服务器mysql配置 49.235.185.26 -->
<mysqlHost>
127.0.0.1
</mysqlHost>
<mysqlPort>
3306
</mysqlPort>
<mysqlUser>
deploy
</mysqlUser>
<mysqlPwd>
Qaz123456!
</mysqlPwd>
<dataBase>
dst_technology_platform
</dataBase>
</mysqldb>
<feiyi>
<feiyiAppKey>
0
</feiyiAppKey>
<feiyiSecretKey>
0
</feiyiSecretKey>
<feiyiAppKey>
444940462151958528
</feiyiAppKey>
<feiyiSecretKey>
d30353e191c94ed3a3b1c05345e284b1
</feiyiSecretKey>
<feiyiBaseUrl>
https://m.achelp.cn/open
</feiyiBaseUrl>
</feiyi>
<diqin>
<diqinAppKey>
178428005410049
</diqinAppKey>
<diqinSecretKey>
pGAII3TfpdrJ4zjrPncERwtt
</diqinSecretKey>
<!-- <diqinAppKey>178428005410049</diqinAppKey> -->
<!-- <diqinSecretKey>pGAII3TfpdrJ4zjrPncERwtt</diqinSecretKey> -->
<diqinAppKey>
178486189110050
</diqinAppKey>
<diqinSecretKey>
CQuwrqgtaQhnwgcChB9Ejgtt
</diqinSecretKey>
<diqinBaseUrl>
https://airiccc.com
</diqinBaseUrl>
</diqin>
</config>
src/biz/dataIntegration.ts
View file @
b29582fb
...
...
@@ -16,6 +16,7 @@ import {
}
from
"./diqinClient"
;
import
{
mysqlModelMap
}
from
"../model/sqlModelBind"
;
import
Sequelize
from
"sequelize"
;
import
{
DEVICE_STATE
}
from
"../config/businessEnum"
;
import
moment
from
"moment"
;
import
XLSX
from
"xlsx"
;
import
path
from
"path"
;
...
...
@@ -229,28 +230,54 @@ export async function region() {
// 3. 逐房间检查并插入
let
insertCount
=
0
;
let
skipCount
=
0
;
let
swapCount
=
0
;
for
(
const
room
of
rooms
)
{
// 校验是否已存在(通过 r
oom_id
)
const
existing
=
await
regionModel
.
findOne
({
where
:
{
room_id
:
room
.
roomId
}
});
// 校验是否已存在(通过 r
egion_key
)
let
existing
=
await
regionModel
.
findOne
({
where
:
{
region_key
:
room
.
roomId
}
});
if
(
existing
)
{
skipCount
++
;
continue
;
}
// 按名称查是否存在(处理跨平台同名情况,如迪勤先创建了同名区域)
existing
=
await
regionModel
.
findOne
({
where
:
{
region_name
:
room
.
roomName
}
});
if
(
existing
)
{
// 同名区域已存在,判断 region_key 是否一致
if
(
existing
.
region_key
!==
room
.
roomId
)
{
// 不一致:旧key移入 region_ad,region_key 改为飞奕的 roomId
const
oldKey
=
existing
.
region_key
;
await
regionModel
.
update
(
{
region_key
:
room
.
roomId
,
region_ad
:
oldKey
,
},
{
where
:
{
id
:
existing
.
id
}
},
);
swapCount
++
;
console
.
log
(
`[区域集成] region_key 交换:
${
room
.
roomName
}
(
${
oldKey
}
→
${
room
.
roomId
}
)`
);
}
else
{
skipCount
++
;
}
continue
;
}
// 插入新区域记录
// 楼栋/楼层层次信息存入 region_param JSON
await
regionModel
.
create
({
room_id
:
room
.
roomId
,
name
:
room
.
roomName
,
floor_id
:
room
.
floorId
,
type
:
room
.
floorName
,
// type 对应楼层名称
building_id
:
room
.
buildingId
,
groups
:
room
.
buildingName
,
// groups 对应楼栋名称
region_key
:
room
.
roomId
,
region_name
:
room
.
roomName
,
region_type
:
room
.
floorName
,
region_param
:
{
floorId
:
room
.
floorId
,
buildingId
:
room
.
buildingId
,
buildingName
:
room
.
buildingName
,
},
sort_order
:
0
,
});
insertCount
++
;
}
console
.
log
(
`[区域集成] 完成,新增
${
insertCount
}
条,跳过
${
skipCount
}
条已存在`
);
console
.
log
(
`[区域集成] 完成,新增
${
insertCount
}
条,跳过
${
skipCount
}
条已存在
,交换
${
swapCount
}
条
`
);
// 2. 一次性查询所有 region,后续两步共用内存数据,避免 DB 读写时序问题
const
allRegions
=
await
regionModel
.
findAll
({
raw
:
true
})
as
any
[];
...
...
@@ -282,11 +309,17 @@ async function syncRegionAddressByAcDevices(regionModel: any, allRegions: any[])
const
rows
=
await
fetchAllPages
((
page
,
limit
)
=>
getIndoorUnitList
({
page
,
limit
}));
console
.
log
(
`[同步region.address] 共获取
${
rows
.
length
}
条空调内机`
);
// 构建 roomId → region 映射(使用外部传入的 allRegions)
const
roomIdToRegion
=
new
Map
<
string
,
any
>
();
// 构建 region_key → region 映射(地址存储在 region_param.address 中)
const
keyToRegion
=
new
Map
<
string
,
any
>
();
// 辅助函数:从 region_param 中读取 address
const
getRegionAddress
=
(
r
:
any
):
string
=>
{
if
(
!
r
.
region_param
)
return
''
;
const
param
=
typeof
r
.
region_param
===
'string'
?
JSON
.
parse
(
r
.
region_param
)
:
r
.
region_param
;
return
param
?.
address
||
''
;
};
for
(
const
r
of
allRegions
)
{
if
(
r
.
r
oom_id
)
{
roomIdToRegion
.
set
(
r
.
room_id
,
r
);
if
(
r
.
r
egion_key
)
{
keyToRegion
.
set
(
r
.
region_key
,
r
);
}
}
...
...
@@ -294,7 +327,7 @@ async function syncRegionAddressByAcDevices(regionModel: any, allRegions: any[])
for
(
const
item
of
rows
)
{
if
(
!
item
.
roomId
||
!
item
.
indoorUnitAddressFull
)
continue
;
const
region
=
roomId
ToRegion
.
get
(
item
.
roomId
);
const
region
=
key
ToRegion
.
get
(
item
.
roomId
);
if
(
!
region
)
continue
;
// 用内机四段地址前三位作为外机地址
...
...
@@ -302,14 +335,18 @@ async function syncRegionAddressByAcDevices(regionModel: any, allRegions: any[])
if
(
!
outerAddr
)
continue
;
// address 为空或值不同时才更新
if
(
region
.
address
===
outerAddr
)
continue
;
if
(
getRegionAddress
(
region
)
===
outerAddr
)
continue
;
// 更新 region_param,写入 address
const
param
=
typeof
region
.
region_param
===
'string'
?
JSON
.
parse
(
region
.
region_param
)
:
(
region
.
region_param
||
{});
param
.
address
=
outerAddr
;
await
regionModel
.
update
(
{
address
:
outerAddr
},
{
region_param
:
param
},
{
where
:
{
id
:
region
.
id
}
},
);
// 同步更新内存中的值,后续 syncMeterDeviceRegionKey 可直接使用
region
.
address
=
outerAddr
;
region
.
region_param
=
param
;
updateCount
++
;
}
console
.
log
(
`[同步region.address] 完成,更新
${
updateCount
}
条`
);
...
...
@@ -340,18 +377,25 @@ async function syncMeterDeviceRegionKey(allRegions: any[]) {
return
;
}
// 2. 使用外部传入的 allRegions 构建 address → regionKey 映射(复用已更新 address 的内存数据)
// 2. 使用外部传入的 allRegions 构建 address → regionKey 映射
// address 存储在 region_param JSON 中
const
getRegionAddress
=
(
r
:
any
):
string
=>
{
if
(
!
r
.
region_param
)
return
''
;
const
param
=
typeof
r
.
region_param
===
'string'
?
JSON
.
parse
(
r
.
region_param
)
:
r
.
region_param
;
return
param
?.
address
||
''
;
};
const
addrToRegionKey
=
new
Map
<
string
,
string
>
();
for
(
const
r
of
allRegions
)
{
if
(
r
.
address
&&
r
.
region_key
)
{
addrToRegionKey
.
set
(
r
.
address
,
r
.
region_key
);
const
addr
=
getRegionAddress
(
r
);
if
(
addr
&&
r
.
region_key
)
{
addrToRegionKey
.
set
(
addr
,
r
.
region_key
);
}
}
console
.
log
(
`[同步电表region_key] region.address 映射共
${
addrToRegionKey
.
size
}
条`
);
console
.
log
(
`[同步电表region_key] region
_param
.address 映射共
${
addrToRegionKey
.
size
}
条`
);
// 3. 查询所有电表设备
const
meterDevices
=
await
deviceModel
.
findAll
({
where
:
{
device_type
:
'电
能监测
'
},
where
:
{
device_type
:
'电
表
'
},
raw
:
true
,
});
console
.
log
(
`[同步电表region_key] 共
${
meterDevices
.
length
}
个电表设备`
);
...
...
@@ -426,7 +470,7 @@ export async function device() {
/**
* 空调设备集成
* device_id = indoorUnitAddressFull
*
device_ad = indoorUnitId
*
额外参数(indoorUnitId 等)存储在 device_param JSON 中
*/
async
function
integrateAcDevices
()
{
console
.
log
(
'[空调设备集成] 开始...'
);
...
...
@@ -438,7 +482,7 @@ async function integrateAcDevices() {
// 查询未处理的故障设备信息
const
faultDevices
=
await
deviceModel
.
findAll
({
where
:
{
device_type
:
'空调'
,
device_state
:
2
},
where
:
{
device_type
:
'空调'
,
device_state
:
DEVICE_STATE
.
故障
},
raw
:
true
,
});
console
.
log
(
`[空调设备集成] 共
${
faultDevices
.
length
}
个故障设备`
);
...
...
@@ -485,11 +529,11 @@ async function integrateAcDevices() {
if
(
item
.
roomId
)
{
const
regionModel
=
mysqlModelMap
[
'region'
];
if
(
regionModel
)
{
const
region
=
await
regionModel
.
findOne
({
where
:
{
r
oom_id
:
item
.
roomId
}
});
const
region
=
await
regionModel
.
findOne
({
where
:
{
r
egion_key
:
item
.
roomId
}
});
if
(
region
)
{
regionKey
=
region
.
region_key
;
// 如果区域
地址
包含新风,则空调视为新风机
if
(
region
.
name
&&
region
.
name
.
includes
(
'新风'
))
{
// 如果区域
名称
包含新风,则空调视为新风机
if
(
region
.
region_name
&&
region
.
region_
name
.
includes
(
'新风'
))
{
deviceType
=
'新风'
;
deviceName
=
'新风机'
;
}
...
...
@@ -500,11 +544,10 @@ async function integrateAcDevices() {
// 插入新设备
await
deviceModel
.
create
({
device_id
:
deviceId
,
device_ad
:
item
.
indoorUnitId
||
''
,
region_key
:
regionKey
,
device_type
:
deviceType
,
device_name
:
`
${
deviceName
}
-
${
deviceId
}
`
,
control_params
:
AC_CONTROL_PARAMS
,
device_param
:
{
ad
:
item
.
indoorUnitId
||
''
,
...
AC_CONTROL_PARAMS
}
,
device_state
:
item
.
state
??
0
});
insertCount
++
;
...
...
@@ -515,8 +558,8 @@ async function integrateAcDevices() {
if
(
faultModel
)
{
const
now
=
new
Date
();
await
faultModel
.
update
(
{
status
:
2
,
resolved
_time
:
now
,
updated_at
:
now
},
{
where
:
{
device_id
:
{
[
Op
.
in
]:
deviceNomalIds
},
status
:
{
[
Op
.
ne
]:
2
}
}
},
{
fault_status
:
2
,
handle
_time
:
now
,
updated_at
:
now
},
{
where
:
{
device_id
:
{
[
Op
.
in
]:
deviceNomalIds
},
fault_
status
:
{
[
Op
.
ne
]:
2
}
}
},
);
console
.
log
(
`[空调设备集成] 批量解决
${
deviceNomalIds
.
length
}
台设备故障`
);
}
...
...
@@ -528,7 +571,7 @@ async function integrateAcDevices() {
/**
* 电表设备集成
* device_id = gatewayCode
*
device_ad = meterId
*
额外参数(meterId 等)存储在 device_param JSON 中
*/
async
function
integrateMeterDevices
()
{
console
.
log
(
'[电表设备集成] 开始...'
);
...
...
@@ -561,11 +604,10 @@ async function integrateMeterDevices() {
// 插入新设备
await
deviceModel
.
create
({
device_id
:
deviceId
,
device_ad
:
item
.
meterId
||
''
,
region_key
:
''
,
// 电表暂无区域关联,后续由 syncMeterDeviceRegionKey 回填
device_type
:
'电
能监测
'
,
device_type
:
'电
表
'
,
device_name
:
`电表-
${
item
.
meterComId
||
item
.
meterId
||
''
}
`
,
control_params
:
METER_CONTROL_PARAMS
,
device_param
:
{
meterId
:
item
.
meterId
||
''
,
...
METER_CONTROL_PARAMS
}
,
});
insertCount
++
;
}
...
...
@@ -670,7 +712,7 @@ async function integrateAcDeviceData() {
};
toInsert
.
push
({
device_id
:
deviceId
,
data
:
data
,
d
evice_d
ata
:
data
,
received_time
:
new
Date
(),
device_time
:
itemTime
,
});
...
...
@@ -749,7 +791,7 @@ async function integrateMeterDeviceData() {
};
toInsert
.
push
({
device_id
:
deviceId
,
data
:
data
,
d
evice_d
ata
:
data
,
received_time
:
new
Date
(),
device_time
:
itemTime
,
});
...
...
@@ -873,11 +915,11 @@ export async function alertWorkData() {
device_id
:
deviceId
,
fault_type
:
getFaultTypeName
(
r
.
alarmType
),
fault_code
:
r
.
alarmCode
||
''
,
fault_
description
:
errorMsg
,
fault_
info
:
errorMsg
,
fault_origin_id
:
r
.
indoorUnitAlarmId
,
occurred
_time
:
r
.
deviceTime
?
new
Date
(
r
.
deviceTime
)
:
new
Date
(),
level
:
2
,
status
:
0
,
fault
_time
:
r
.
deviceTime
?
new
Date
(
r
.
deviceTime
)
:
new
Date
(),
fault_
level
:
2
,
fault_
status
:
0
,
});
}
...
...
@@ -913,7 +955,7 @@ export async function feiyiRegionDeviceRel() {
// 1. 查询所有已绑定 region_key 的飞奕设备(空调 + 电表)
const
devices
=
await
deviceModel
.
findAll
({
where
:
{
device_type
:
[
'空调'
,
'新风'
,
'电
能监测
'
],
device_type
:
[
'空调'
,
'新风'
,
'电
表
'
],
region_key
:
{
[
Op
.
ne
]:
null
},
},
attributes
:
[
'device_id'
,
'region_key'
,
'device_type'
],
...
...
@@ -993,36 +1035,98 @@ export async function diqinRegion() {
}
console
.
log
(
`[迪勤区域集成] 共获取
${
locations
.
length
}
个场所`
);
// 2. 逐一 upsert
let
insertCount
=
0
;
let
updateCount
=
0
;
// 2. 逐一 upsert
场所 + 其下监测点
let
locInsert
=
0
,
locUpdate
=
0
;
let
stInsert
=
0
,
stUpdate
=
0
;
for
(
const
loc
of
locations
)
{
const
regionKey
=
loc
.
id
;
if
(
!
regionKey
)
continue
;
const
existing
=
await
regionModel
.
findOne
({
where
:
{
region_key
:
regionKey
}
});
// 2.1 upsert 场所本身
let
existing
=
await
regionModel
.
findOne
({
where
:
{
region_key
:
regionKey
}
});
if
(
existing
)
{
// 更新名称(如有变化)
if
(
existing
.
region_name
!==
loc
.
name
)
{
if
(
existing
.
region_name
!==
loc
.
name
||
existing
.
region_type
!==
'场所'
)
{
await
regionModel
.
update
(
{
region_name
:
loc
.
name
,
updated_at
:
new
Date
()
},
{
region_name
:
loc
.
name
,
region_type
:
'场所'
,
updated_at
:
new
Date
()
},
{
where
:
{
id
:
existing
.
id
}
},
);
updateCount
++
;
locUpdate
++
;
}
}
else
{
await
regionModel
.
create
({
region_key
:
regionKey
,
region_name
:
loc
.
name
,
parent_id
:
null
,
parent_key
:
null
,
region_type
:
null
,
sort_order
:
0
,
});
insertCount
++
;
// 按名称查是否存在同名区域(飞奕或其他平台已创建)
existing
=
await
regionModel
.
findOne
({
where
:
{
region_name
:
loc
.
name
}
});
if
(
existing
)
{
// 同名区域已存在,将迪勤内部ID写入 region_ad
if
(
!
existing
.
region_ad
||
existing
.
region_ad
!==
regionKey
)
{
await
regionModel
.
update
(
{
region_ad
:
regionKey
,
updated_at
:
new
Date
()
},
{
where
:
{
id
:
existing
.
id
}
},
);
locUpdate
++
;
console
.
log
(
`[迪勤区域集成] 同名场所补充 region_ad:
${
loc
.
name
}
→
${
regionKey
}
`
);
}
}
else
{
await
regionModel
.
create
({
region_key
:
regionKey
,
region_name
:
loc
.
name
,
parent_id
:
null
,
parent_key
:
null
,
region_type
:
'场所'
,
sort_order
:
0
,
});
locInsert
++
;
}
}
// 2.2 同步该场所下的监测点(作为子区域)
try
{
const
detail
=
await
getLocationDetail
(
loc
.
id
);
const
stations
=
detail
?.
stations
||
[];
for
(
const
st
of
stations
)
{
if
(
!
st
.
id
||
!
st
.
name
)
continue
;
let
stExisting
=
await
regionModel
.
findOne
({
where
:
{
region_key
:
st
.
id
}
});
if
(
stExisting
)
{
if
(
stExisting
.
region_name
!==
st
.
name
||
stExisting
.
parent_key
!==
regionKey
||
stExisting
.
region_type
!==
'监测点'
)
{
await
regionModel
.
update
(
{
region_name
:
st
.
name
,
parent_key
:
regionKey
,
region_type
:
'监测点'
,
updated_at
:
new
Date
()
},
{
where
:
{
id
:
stExisting
.
id
}
},
);
stUpdate
++
;
}
}
else
{
// 按名称查是否存在同名区域(飞奕或其他平台已创建)
stExisting
=
await
regionModel
.
findOne
({
where
:
{
region_name
:
st
.
name
}
});
if
(
stExisting
)
{
// 同名区域已存在,将迪勤内部ID写入 region_ad
if
(
!
stExisting
.
region_ad
||
stExisting
.
region_ad
!==
st
.
id
)
{
await
regionModel
.
update
(
{
region_ad
:
st
.
id
,
updated_at
:
new
Date
()
},
{
where
:
{
id
:
stExisting
.
id
}
},
);
stUpdate
++
;
console
.
log
(
`[迪勤区域集成] 同名监测点补充 region_ad:
${
st
.
name
}
→
${
st
.
id
}
`
);
}
}
else
{
await
regionModel
.
create
({
region_key
:
st
.
id
,
region_name
:
st
.
name
,
parent_id
:
null
,
parent_key
:
regionKey
,
region_type
:
'监测点'
,
sort_order
:
0
,
});
stInsert
++
;
}
}
}
}
catch
(
err
:
any
)
{
console
.
warn
(
`[迪勤区域集成] 获取场所
${
loc
.
name
}
监测点失败:`
,
err
.
message
);
}
}
console
.
log
(
`[迪勤区域集成] 完成,新增
${
insertCount
}
条,更新
${
updateCount
}
条`
);
console
.
log
(
`[迪勤区域集成] 完成:场所新增
${
locInsert
}
/更新
${
locUpdate
}
,监测点新增
${
stInsert
}
/更新
${
stUpdate
}
`
);
}
catch
(
err
)
{
console
.
error
(
'[迪勤区域集成] 执行异常:'
,
err
);
}
...
...
@@ -1089,7 +1193,7 @@ export async function diqinDevice() {
region_key
:
null
,
device_type
:
deviceType
,
device_name
:
deviceName
,
device_state
:
1
,
// 默认在线
device_state
:
DEVICE_STATE
.
在线
,
// 默认在线
});
insertCount
++
;
}
...
...
@@ -1126,28 +1230,62 @@ export async function diqinRegionDeviceRel() {
}
console
.
log
(
`[迪勤区域设备关联] IEQ 设备共
${
ieqSources
.
length
}
个`
);
// 2. 获取所有场所及其监测点
// 构建 stationName → locationId 映射
// 2. 获取所有场所及其监测点,构建两级映射
// stationRegionKeyMap: stationName → stationId(监测点)
// locationRegionKeyMap: locationId → location region_key(场所,兜底用)
const
locations
=
await
getLocations
();
if
(
!
locations
||
!
Array
.
isArray
(
locations
))
return
;
const
stationLocMap
=
new
Map
<
string
,
string
>
();
// stationName → locationId
const
stationRegionKeyMap
=
new
Map
<
string
,
string
>
();
// stationName → stationId
const
locationRegionKeyMap
=
new
Map
<
string
,
string
>
();
// locationId → location region_key(场所id)
for
(
const
loc
of
locations
)
{
locationRegionKeyMap
.
set
(
loc
.
id
,
loc
.
id
);
try
{
const
detail
=
await
getLocationDetail
(
loc
.
id
);
const
stations
=
detail
?.
stations
||
[];
for
(
const
st
of
stations
)
{
if
(
st
.
name
)
{
station
LocMap
.
set
(
st
.
name
,
loc
.
id
);
if
(
st
.
name
&&
st
.
id
)
{
station
RegionKeyMap
.
set
(
st
.
name
,
st
.
id
);
}
}
}
catch
(
err
)
{
}
catch
(
err
:
any
)
{
console
.
warn
(
`[迪勤区域设备关联] 获取场所
${
loc
.
name
}
明细失败:`
,
err
.
message
);
}
}
console
.
log
(
`[迪勤区域设备关联] stationLocMap 共
${
stationLocMap
.
size
}
条`
);
console
.
log
(
`[迪勤区域设备关联] station 映射
${
stationRegionKeyMap
.
size
}
条, location 映射
${
locationRegionKeyMap
.
size
}
条`
);
// 2.1 收集所有迪勤内部ID,查询 region 表获取实际 region_key
// (处理跨平台同名区域:region_key 可能已被飞奕替换,region_ad 存储了迪勤ID)
const
allDiqinIds
=
[...
new
Set
([
...
stationRegionKeyMap
.
values
(),
...
locationRegionKeyMap
.
keys
(),
])];
const
regionModel
=
mysqlModelMap
[
'region'
];
const
diqinIdToRegionKey
=
new
Map
<
string
,
string
>
();
if
(
regionModel
&&
allDiqinIds
.
length
>
0
)
{
const
regionRecords
=
await
regionModel
.
findAll
({
where
:
{
[
Op
.
or
]:
[
{
region_key
:
{
[
Op
.
in
]:
allDiqinIds
}
},
{
region_ad
:
{
[
Op
.
in
]:
allDiqinIds
}
},
],
},
raw
:
true
,
})
as
any
[];
for
(
const
r
of
regionRecords
)
{
// region_key 匹配迪勤ID(未被飞奕交换的场景)
if
(
r
.
region_key
&&
allDiqinIds
.
includes
(
r
.
region_key
))
{
diqinIdToRegionKey
.
set
(
r
.
region_key
,
r
.
region_key
);
}
// region_ad 匹配迪勤ID(被飞奕交换的场景,用实际的 region_key)
if
(
r
.
region_ad
&&
allDiqinIds
.
includes
(
r
.
region_ad
))
{
diqinIdToRegionKey
.
set
(
r
.
region_ad
,
r
.
region_key
);
}
}
console
.
log
(
`[迪勤区域设备关联] 解析到
${
diqinIdToRegionKey
.
size
}
个迪勤ID→实际region_key映射`
);
}
// 3. 匹配:
station.name 包含 data_source.name → 建立关联
// 3. 匹配:
优先子级 station → 找不到再回退父级 location
const
newRels
:
Array
<
{
region_key
:
string
;
device_id
:
string
}
>
=
[];
const
ieqDeviceIds
:
string
[]
=
[];
...
...
@@ -1156,11 +1294,30 @@ export async function diqinRegionDeviceRel() {
if
(
!
dsName
)
continue
;
ieqDeviceIds
.
push
(
ds
.
id
);
// 遍历所有 station,找包含 device name 的
for
(
const
[
stationName
,
locationId
]
of
stationLocMap
)
{
let
matched
=
false
;
// 3.1 先尝试匹配监测点(子级区域)
for
(
const
[
stationName
,
stationRegionKey
]
of
stationRegionKeyMap
)
{
if
(
stationName
.
includes
(
dsName
))
{
newRels
.
push
({
region_key
:
locationId
,
device_id
:
ds
.
id
});
break
;
// 一个设备只匹配第一个命中的 station
const
resolvedKey
=
diqinIdToRegionKey
.
get
(
stationRegionKey
)
||
stationRegionKey
;
newRels
.
push
({
region_key
:
resolvedKey
,
device_id
:
ds
.
id
});
matched
=
true
;
break
;
}
}
// 3.2 未匹配到子级监测点 → 回退匹配父级场所
if
(
!
matched
)
{
for
(
const
[
locationId
,
locationRegionKey
]
of
locationRegionKeyMap
)
{
// 用 locationId 代表的场所名(从 locations 列表中获取)
const
loc
=
locations
.
find
((
l
:
any
)
=>
l
.
id
===
locationId
);
const
locName
=
loc
?.
name
||
''
;
if
(
locName
.
toLowerCase
().
includes
(
dsName
.
toLowerCase
()))
{
const
resolvedKey
=
diqinIdToRegionKey
.
get
(
locationRegionKey
)
||
locationRegionKey
;
newRels
.
push
({
region_key
:
resolvedKey
,
device_id
:
ds
.
id
});
matched
=
true
;
break
;
}
}
}
}
...
...
@@ -1346,6 +1503,85 @@ export async function diqinSyncAll() {
console
.
log
(
'[迪勤全量同步] 完成'
);
}
// ==================== device.region_key 数据迁移 ====================
/**
* 数据迁移:将 device.region_key 从旧值(region.id 数字)映射为 region.region_key(字符串)
* 覆盖所有设备类型(空调、电表、IEQ传感器)
* 幂等设计:已为正确 region_key 值的记录跳过,可安全重复执行
*/
export
async
function
migrateDeviceRegionKey
()
{
console
.
log
(
'[region_key迁移] 开始...'
);
try
{
const
deviceModel
=
mysqlModelMap
[
'device'
];
const
regionModel
=
mysqlModelMap
[
'region'
];
if
(
!
deviceModel
||
!
regionModel
)
{
console
.
error
(
'[region_key迁移] 模型未初始化,跳过'
);
return
;
}
// 1. 构建 id → region_key 映射
const
allRegions
=
await
regionModel
.
findAll
({
attributes
:
[
'id'
,
'region_key'
],
raw
:
true
,
})
as
any
[];
const
idToRegionKey
=
new
Map
<
number
,
string
>
();
for
(
const
r
of
allRegions
)
{
if
(
r
.
region_key
)
{
idToRegionKey
.
set
(
r
.
id
,
String
(
r
.
region_key
));
}
}
console
.
log
(
`[region_key迁移] region 表共
${
idToRegionKey
.
size
}
条映射`
);
// 2. 查询所有设备
const
allDevices
=
await
deviceModel
.
findAll
({
attributes
:
[
'id'
,
'device_id'
,
'region_key'
,
'device_type'
],
raw
:
true
,
})
as
any
[];
console
.
log
(
`[region_key迁移] device 表共
${
allDevices
.
length
}
条`
);
// 3. 逐条判定并更新
let
updateCount
=
0
;
let
skipCorrect
=
0
;
let
skipNoRegion
=
0
;
for
(
const
d
of
allDevices
)
{
const
currentKey
=
d
.
region_key
;
// 空值/无关联 → 跳过(后续由各同步函数回填)
if
(
!
currentKey
||
currentKey
===
'0'
||
currentKey
===
''
)
{
skipNoRegion
++
;
continue
;
}
const
currentKeyStr
=
String
(
currentKey
);
// 如果当前值包含字母(如 "B1-F1-01"),说明已是正确格式
if
(
/
[
a-zA-Z
]
/
.
test
(
currentKeyStr
))
{
skipCorrect
++
;
continue
;
}
// 纯数字字符串(如 "1", "2"),按 region.id 映射为 region_key
const
numericId
=
Number
(
currentKeyStr
);
const
targetKey
=
idToRegionKey
.
get
(
numericId
);
if
(
!
targetKey
)
{
skipNoRegion
++
;
continue
;
// 映射不到,跳过
}
await
deviceModel
.
update
(
{
region_key
:
targetKey
,
updated_at
:
new
Date
()
},
{
where
:
{
id
:
d
.
id
}
},
);
updateCount
++
;
}
console
.
log
(
`[region_key迁移] 完成:已迁移
${
updateCount
}
条,已正确
${
skipCorrect
}
条,无关联
${
skipNoRegion
}
条`
);
}
catch
(
err
)
{
console
.
error
(
'[region_key迁移] 执行异常:'
,
err
);
}
}
// ==================== 故障状态自动更新 ====================
/**
...
...
@@ -1366,8 +1602,8 @@ export async function processFaultStatus() {
try
{
// ① 一次查出所有未处理/处理中的故障
const
activeFaults
=
await
faultModel
.
findAll
({
where
:
{
status
:
{
[
Op
.
in
]:
[
0
,
1
]
}
},
attributes
:
[
'id'
,
'device_id'
,
'
occurred
_time'
],
where
:
{
fault_
status
:
{
[
Op
.
in
]:
[
0
,
1
]
}
},
attributes
:
[
'id'
,
'device_id'
,
'
fault
_time'
],
raw
:
true
,
});
console
.
log
(
`[故障状态更新] 共
${
activeFaults
.
length
}
条活跃故障`
);
...
...
@@ -1385,7 +1621,7 @@ export async function processFaultStatus() {
const
deviceFaultTimeMap
=
new
Map
<
string
,
Date
>
();
for
(
const
f
of
activeFaults
as
any
[])
{
deviceSet
.
add
(
f
.
device_id
);
const
t
=
f
.
occurred_time
?
new
Date
(
f
.
occurred
_time
)
:
null
;
const
t
=
f
.
fault_time
?
new
Date
(
f
.
fault
_time
)
:
null
;
if
(
!
t
)
continue
;
// 全局最早时间(SQL 查询范围)
if
(
!
globalMinOccurredTime
||
t
<
globalMinOccurredTime
)
{
...
...
@@ -1406,7 +1642,7 @@ export async function processFaultStatus() {
device_id
:
{
[
Op
.
in
]:
deviceIds
},
...(
globalMinOccurredTime
?
{
device_time
:
{
[
Op
.
gte
]:
globalMinOccurredTime
}
}
:
{}),
},
attributes
:
[
'device_id'
,
'data'
,
'device_time'
],
attributes
:
[
'device_id'
,
'd
evice_d
ata'
,
'device_time'
],
order
:
[[
'device_id'
,
'ASC'
],
[
'device_time'
,
'ASC'
]],
raw
:
true
,
});
...
...
@@ -1418,7 +1654,7 @@ export async function processFaultStatus() {
const
powerOnTimesMap
=
new
Map
<
string
,
Date
[]
>
();
for
(
const
r
of
deviceDataRecords
as
any
[])
{
let
power
:
string
|
undefined
;
const
rawData
=
r
.
data
;
const
rawData
=
r
.
d
evice_d
ata
;
if
(
typeof
rawData
===
'string'
)
{
try
{
power
=
JSON
.
parse
(
rawData
).
power
;
}
catch
{
continue
;
}
}
else
if
(
rawData
&&
typeof
rawData
===
'object'
)
{
...
...
@@ -1442,18 +1678,18 @@ export async function processFaultStatus() {
// ⑤ 逐条故障比对:查找该故障发生时间之后最早的开机记录
// 关键修复:不再拿"全量最早开机时间"与故障时间对比,
// 而是在该设备故障后出现的开机记录中找第一条,确保 isAfter 判定正确
const
toUpdate
:
{
id
:
number
;
resolved
_time
:
Date
}[]
=
[];
const
toUpdate
:
{
id
:
number
;
handle
_time
:
Date
}[]
=
[];
for
(
const
fault
of
activeFaults
as
any
[])
{
const
powerOnTimes
=
powerOnTimesMap
.
get
(
fault
.
device_id
);
if
(
!
powerOnTimes
||
powerOnTimes
.
length
===
0
)
continue
;
const
occurredTime
=
fault
.
occurred_time
?
new
Date
(
fault
.
occurred
_time
)
:
null
;
const
occurredTime
=
fault
.
fault_time
?
new
Date
(
fault
.
fault
_time
)
:
null
;
if
(
!
occurredTime
)
continue
;
// 数据已按 device_time ASC 排序,找第一条在故障时间之后的记录
const
resolvedTime
=
powerOnTimes
.
find
(
dt
=>
dt
>
occurredTime
);
if
(
resolvedTime
)
{
toUpdate
.
push
({
id
:
fault
.
id
,
resolved
_time
:
resolvedTime
});
toUpdate
.
push
({
id
:
fault
.
id
,
handle
_time
:
resolvedTime
});
}
}
...
...
@@ -1465,7 +1701,7 @@ export async function processFaultStatus() {
// ⑥ 逐条更新 status=2
for
(
const
item
of
toUpdate
)
{
await
faultModel
.
update
(
{
status
:
2
,
resolved_time
:
item
.
resolved
_time
,
updated_at
:
new
Date
()
},
{
fault_status
:
2
,
handle_time
:
item
.
handle
_time
,
updated_at
:
new
Date
()
},
{
where
:
{
id
:
item
.
id
}
},
);
}
...
...
src/biz/feiyiClient.ts
View file @
b29582fb
...
...
@@ -21,8 +21,8 @@ function getFeiYiConfig() {
const
config
=
systemConfig
.
feiyi
;
return
{
baseUrl
:
(
config
&&
config
.
baseUrl
)
||
process
.
env
.
FEIYI_BASE_URL
||
'https://m.achelp.cn/open'
,
clientId
:
(
config
&&
config
.
appKey
)
||
process
.
env
.
FEIYI_CLIENT_ID
||
'4
19636996764598272
'
,
secretKey
:
(
config
&&
config
.
secretKey
)
||
process
.
env
.
FEIYI_SECRET_KEY
||
'
02fc75446b4544b3a02444a2b5f9be2b
'
,
clientId
:
(
config
&&
config
.
appKey
)
||
process
.
env
.
FEIYI_CLIENT_ID
||
'4
44940462151958528
'
,
secretKey
:
(
config
&&
config
.
secretKey
)
||
process
.
env
.
FEIYI_SECRET_KEY
||
'
d30353e191c94ed3a3b1c05345e284b1
'
,
};
}
...
...
src/biz/region.ts
View file @
b29582fb
...
...
@@ -7,26 +7,31 @@ import * as path from 'path';
import
{
post
}
from
'../util/request'
;
import
{
analysisXml
}
from
'../util/myXML'
;
import
{
planaryArrayBecomeOfBlockData
}
from
'../util/analysisExcel'
;
import
{
Op
,
Sequelize
}
from
'sequelize'
;
import
{
selectDataListByParam
}
from
'../data/findData'
;
import
{
mysqlModelMap
}
from
'../model/sqlModelBind'
;
import
{
TABLENAME
}
from
'../config/dbEnum'
;
import
{
formatTime
}
from
'../util/dateUtils'
;
const
xlsx
=
require
(
'node-xlsx'
);
/**
* 区域位置
* @param gatewayPage 网关页面 1:运行分析 2:智能监控
*
* @returns 区域位置数据
*/
export
async
function
getRegionLocation
(
gatewayPage
:
number
)
{
export
async
function
getRegionLocation
(
gatewayPage
:
number
,
hour
:
number
)
{
// const result = [{"regionKey":"32","regionName":"三十二位","xz":1587,"yz":487,"qualityIndex":"优","gatewayPage":gatewayPage}];
// 读取区域坐标分布.xlsx文件中的表格数据,更新区域表中的区域参数字段
const
result
=
await
readRegionAxis
(
gatewayPage
);
const
result
=
await
readRegionAxis
(
gatewayPage
,
hour
);
return
result
;
}
/**
* 读取区域坐标
*/
export
async
function
readRegionAxis
(
gatewayPage
:
number
)
{
export
async
function
readRegionAxis
(
gatewayPage
:
number
,
hour
:
number
)
{
let
result
=
[];
const
filePath
=
path
.
join
(
__dirname
.
substring
(
0
,
__dirname
.
indexOf
(
"out"
)),
"res"
,
"区域坐标分布.xlsx"
);
...
...
@@ -38,24 +43,103 @@ export async function readRegionAxis(gatewayPage: number) {
}
if
(
gatewayPage
===
1
)
{
let
analysisSheet
=
sheetMap
[
"运行分析"
];
result
=
await
readSheet
(
analysisSheet
,
gatewayPage
);
result
=
await
readSheet
(
analysisSheet
,
gatewayPage
,
0
);
}
else
if
(
gatewayPage
===
2
)
{
let
monitorSheet
=
sheetMap
[
"智能监控"
];
result
=
await
readSheet
(
monitorSheet
,
gatewayPage
);
result
=
await
readSheet
(
monitorSheet
,
gatewayPage
,
hour
);
}
return
result
;
}
/**
* 根据 CO2 和 PM2.5 判断环境质量等级(取两指标中较差的等级)
* 优: CO2 < 1000ppm AND PM2.5 ≤ 45
* 良: CO2 ≤ 1000ppm AND PM2.5 ≤ 50
* 中: CO2 ≤ 1500ppm AND PM2.5 ≤ 75
* 差: 不满足以上任一
* hasCo2/hasPm25 为 false 时忽略该指标
*/
function
judgeQuality
(
co2
:
number
,
pm25
:
number
,
hasCo2
:
boolean
,
hasPm25
:
boolean
):
string
{
if
(
!
hasCo2
&&
!
hasPm25
)
return
'--'
;
const
co2Excellent
=
!
hasCo2
||
co2
<
1000
;
const
co2Good
=
!
hasCo2
||
co2
<=
1000
;
const
co2Normal
=
!
hasCo2
||
co2
<=
1500
;
const
pm25Excellent
=
!
hasPm25
||
pm25
<=
45
;
const
pm25Good
=
!
hasPm25
||
pm25
<=
50
;
const
pm25Normal
=
!
hasPm25
||
pm25
<=
75
;
if
(
co2Excellent
&&
pm25Excellent
)
return
'优'
;
if
(
co2Good
&&
pm25Good
)
return
'良'
;
if
(
co2Normal
&&
pm25Normal
)
return
'中'
;
return
'差'
;
}
/**
* 四指标分级判定(甲醛/O2/NH3/H2S),取最差级别。
* 供 IEQ-EC 异味传感器及后续其他场景复用。
*
* 甲醛 a31001: ≤0.08=1级, 0.08~0.15=2级, 0.15~0.3=3级, >0.3=4级 (GB/T 18883-2022)
* O2 a19001: 19.5~23.5=1级, 18~19.5=2级, <18=3级, >23.5=4级 (室内/作业场景安全分级)
* NH3 a21001: ≤0.20=1级, 0.20~0.5=2级, 0.5~1.0=3级, >1.0=4级 (GB/T 18883-2022)
* H2S a21028: ≤10=1级, 10~20=2级, 20~50=3级, >50=4级 (GBZ 2.1-2019)
*
* @returns '优' | '良' | '中' | '差' | '--'(全部无数据)
*/
function
judgeFourIndicators
(
hcho
:
number
,
o2
:
number
,
nh3
:
number
,
h2s
:
number
,
hasHcho
:
boolean
,
hasO2
:
boolean
,
hasNh3
:
boolean
,
hasH2s
:
boolean
):
string
{
if
(
!
hasHcho
&&
!
hasO2
&&
!
hasNh3
&&
!
hasH2s
)
return
'--'
;
const
delta
:
number
[]
=
[];
// 1=优, 2=良, 3=中, 4=差
if
(
hasHcho
)
{
if
(
hcho
<=
0.08
)
delta
.
push
(
1
);
else
if
(
hcho
<=
0.15
)
delta
.
push
(
2
);
else
if
(
hcho
<=
0.3
)
delta
.
push
(
3
);
else
delta
.
push
(
4
);
}
if
(
hasO2
)
{
if
(
o2
>=
19.5
&&
o2
<=
23.5
)
delta
.
push
(
1
);
else
if
(
o2
>=
18
&&
o2
<
19.5
)
delta
.
push
(
2
);
else
if
(
o2
<
18
)
delta
.
push
(
3
);
else
delta
.
push
(
4
);
}
if
(
hasNh3
)
{
if
(
nh3
<=
0.20
)
delta
.
push
(
1
);
else
if
(
nh3
<=
0.5
)
delta
.
push
(
2
);
else
if
(
nh3
<=
1.0
)
delta
.
push
(
3
);
else
delta
.
push
(
4
);
}
if
(
hasH2s
)
{
if
(
h2s
<=
10
)
delta
.
push
(
1
);
else
if
(
h2s
<=
20
)
delta
.
push
(
2
);
else
if
(
h2s
<=
50
)
delta
.
push
(
3
);
else
delta
.
push
(
4
);
}
const
worst
=
Math
.
max
(...
delta
);
const
levels
=
[
'优'
,
'良'
,
'中'
,
'差'
];
return
levels
[
worst
-
1
];
}
/**
* 动态查询:根据 regionName→regionKey 映射,从数据库查询各区域下的设备及最新功率状态
* @returns 格式与 mock 数据一致: { regionName: [{deviceId, deviceType, power}, ...] }
* 同时提取最新 co2/pm25 数据计算各区域环境质量
* @returns { deviceMap, qualityMap }
*/
async
function
buildDeviceMapDynamic
(
regionMap
:
{
[
regionName
:
string
]:
string
}):
Promise
<
{
[
regionName
:
string
]:
any
[]
}
>
{
async
function
buildDeviceMapDynamic
(
regionMap
:
{
[
regionName
:
string
]:
string
},
ieqTagMap
:
{
[
regionName
:
string
]:
string
},
hour
:
number
,
):
Promise
<
{
deviceMap
:
{
[
regionName
:
string
]:
any
[]
};
qualityMap
:
{
[
regionName
:
string
]:
string
}
}
>
{
const
deviceMap
:
{
[
regionName
:
string
]:
any
[]
}
=
{};
const
qualityMap
:
{
[
regionName
:
string
]:
string
}
=
{};
// 1. 收集所有 region_key
const
regionKeys
=
Object
.
values
(
regionMap
)
as
string
[];
if
(
regionKeys
.
length
===
0
)
return
deviceMap
;
if
(
regionKeys
.
length
===
0
)
return
{
deviceMap
,
qualityMap
}
;
// 2. 反向映射:regionKey → regionName
const
regionKeyToName
:
{
[
key
:
string
]:
string
}
=
{};
...
...
@@ -71,28 +155,72 @@ async function buildDeviceMapDynamic(regionMap: { [regionName: string]: string }
);
const
devices
=
(
deviceResult
.
data
||
[])
as
any
[];
// 4. 批量查询这些设备的最新一条 device_data,提取 power
状态
// 4. 批量查询这些设备的最新一条 device_data,提取 power
、co2、pm25,并保留完整解析数据供虚拟传感器计算
const
deviceIds
=
devices
.
map
((
d
)
=>
d
.
device_id
).
filter
(
Boolean
);
const
powerMap
=
new
Map
<
string
,
string
>
();
const
deviceDataMap
=
new
Map
<
string
,
any
>
();
// device_id → 最新 device_data 解析对象
// 按区域收集 co2 / pm25 值
const
regionEnvMap
:
{
[
regionName
:
string
]:
{
co2List
:
number
[];
pm25List
:
number
[]
}
}
=
{};
for
(
const
name
of
Object
.
keys
(
regionMap
))
{
regionEnvMap
[
name
]
=
{
co2List
:
[],
pm25List
:
[]
};
}
if
(
deviceIds
.
length
>
0
)
{
const
dataResult
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
'%in%'
:
deviceIds
},
'%orderDesc%'
:
'device_time'
,
'%limit%'
:
deviceIds
.
length
,
},
[
'device_id'
,
'device_data'
],
);
// 按 device_time 降序返回,去重取每个设备的第一条(最新)
const
seen
=
new
Set
<
string
>
();
(
dataResult
.
data
||
[]).
forEach
((
r
:
any
)
=>
{
if
(
!
seen
.
has
(
r
.
device_id
))
{
seen
.
add
(
r
.
device_id
);
// 计算时间截断点:当前时间减去 hour 小时(不截断整点)
const
cutoffTime
=
new
Date
(
Date
.
now
()
-
hour
*
3600
*
1000
);
const
cutoffTimeStr
=
formatTime
(
cutoffTime
);
// 查询1:GROUP BY 取每个设备的 device_time 最大值(利用索引,高效)
const
tableModel
=
mysqlModelMap
[
TABLENAME
.
设备数据表
];
const
whereGroup
:
any
=
{
device_id
:
{
[
Op
.
in
]:
deviceIds
}
};
if
(
hour
>
0
)
{
whereGroup
.
device_time
=
{
[
Op
.
lte
]:
cutoffTimeStr
};
}
const
latestRows
=
await
tableModel
.
findAll
({
attributes
:
[
'device_id'
,
[
Sequelize
.
fn
(
'MAX'
,
Sequelize
.
col
(
'device_time'
)),
'latest_time'
],
],
where
:
whereGroup
,
group
:
[
'device_id'
],
raw
:
true
,
})
as
any
[];
// 查询2:按 (device_id, device_time) 精确等值查询完整数据
if
(
latestRows
.
length
>
0
)
{
const
orConditions
=
latestRows
.
map
((
row
:
any
)
=>
({
device_id
:
row
.
device_id
,
device_time
:
row
.
latest_time
,
}));
const
dataResult
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
'%or%'
:
orConditions
},
[
'device_id'
,
'device_data'
],
);
(
dataResult
.
data
||
[]).
forEach
((
r
:
any
)
=>
{
const
parsed
=
typeof
r
.
device_data
===
'string'
?
JSON
.
parse
(
r
.
device_data
)
:
r
.
device_data
;
powerMap
.
set
(
r
.
device_id
,
parsed
?.
power
||
'on'
);
deviceDataMap
.
set
(
r
.
device_id
,
parsed
);
// 提取环境数据
const
rk
=
devices
.
find
(
d
=>
d
.
device_id
===
r
.
device_id
)?.
region_key
;
const
rn
=
rk
?
regionKeyToName
[
rk
]
:
undefined
;
if
(
rn
&&
regionEnvMap
[
rn
])
{
if
(
typeof
parsed
?.
co2
===
'number'
)
regionEnvMap
[
rn
].
co2List
.
push
(
parsed
.
co2
);
if
(
typeof
parsed
?.
pm25
===
'number'
)
regionEnvMap
[
rn
].
pm25List
.
push
(
parsed
.
pm25
);
}
});
}
// 时间窗口内无数据的设备:推 0 值(无数据设为零)
for
(
const
device
of
devices
)
{
if
(
!
deviceDataMap
.
has
(
device
.
device_id
))
{
const
rn
=
regionKeyToName
[
device
.
region_key
];
if
(
rn
&&
regionEnvMap
[
rn
])
{
regionEnvMap
[
rn
].
co2List
.
push
(
0
);
regionEnvMap
[
rn
].
pm25List
.
push
(
0
);
}
}
}
);
}
}
// 5. 组装 deviceMap(按 regionName 分组,格式对齐 mock 数据)
...
...
@@ -102,6 +230,7 @@ async function buildDeviceMapDynamic(regionMap: { [regionName: string]: string }
if
(
!
deviceMap
[
regionName
])
{
deviceMap
[
regionName
]
=
[];
}
deviceMap
[
regionName
].
push
({
deviceId
:
device
.
device_id
,
deviceType
:
device
.
device_type
,
...
...
@@ -109,61 +238,127 @@ async function buildDeviceMapDynamic(regionMap: { [regionName: string]: string }
});
}
return
deviceMap
;
// 5.5 根据 IEQ 标签拆分虚拟传感器(ME=人在, EC=异味)
for
(
const
[
regionName
,
ieqTag
]
of
Object
.
entries
(
ieqTagMap
))
{
if
(
!
ieqTag
||
ieqTag
===
'SD'
)
continue
;
const
tags
=
ieqTag
.
split
(
','
).
map
((
t
:
string
)
=>
t
.
trim
());
if
(
!
deviceMap
[
regionName
])
continue
;
// 找到该区域下的 IEQ 设备
const
ieqDevice
=
deviceMap
[
regionName
].
find
((
d
:
any
)
=>
d
.
deviceType
===
'IEQ传感器'
);
if
(
!
ieqDevice
)
continue
;
const
ieqData
=
deviceDataMap
.
get
(
ieqDevice
.
deviceId
);
// ME: 人在传感器
if
(
tags
.
includes
(
'ME'
))
{
const
mePower
=
ieqData
&&
typeof
ieqData
.
a09527
===
'number'
&&
ieqData
.
a09527
>
0
?
'on'
:
'off'
;
deviceMap
[
regionName
].
push
({
deviceId
:
ieqDevice
.
deviceId
,
deviceType
:
'人在传感器'
,
power
:
mePower
,
});
}
// EC: 异味传感器
if
(
tags
.
includes
(
'EC'
))
{
const
hasHcho
=
ieqData
&&
typeof
ieqData
.
a31001
===
'number'
;
const
hasO2
=
ieqData
&&
typeof
ieqData
.
a19001
===
'number'
;
const
hasNh3
=
ieqData
&&
typeof
ieqData
.
a21001
===
'number'
;
const
hasH2s
=
ieqData
&&
typeof
ieqData
.
a21028
===
'number'
;
const
ecLevel
=
judgeFourIndicators
(
hasHcho
?
ieqData
.
a31001
:
0
,
hasO2
?
ieqData
.
a19001
:
0
,
hasNh3
?
ieqData
.
a21001
:
0
,
hasH2s
?
ieqData
.
a21028
:
0
,
hasHcho
,
hasO2
,
hasNh3
,
hasH2s
,
);
deviceMap
[
regionName
].
push
({
deviceId
:
ieqDevice
.
deviceId
,
deviceType
:
'异味传感器'
,
power
:
ecLevel
===
'优'
?
'off'
:
'on'
,
});
}
}
// 6. 根据各区域环境数据计算空气质量等级
for
(
const
[
name
,
entry
]
of
Object
.
entries
(
regionEnvMap
))
{
const
avgCo2
=
entry
.
co2List
.
length
>
0
?
entry
.
co2List
.
reduce
((
a
,
b
)
=>
a
+
b
,
0
)
/
entry
.
co2List
.
length
:
0
;
const
avgPm25
=
entry
.
pm25List
.
length
>
0
?
entry
.
pm25List
.
reduce
((
a
,
b
)
=>
a
+
b
,
0
)
/
entry
.
pm25List
.
length
:
0
;
qualityMap
[
name
]
=
judgeQuality
(
avgCo2
,
avgPm25
,
entry
.
co2List
.
length
>
0
,
entry
.
pm25List
.
length
>
0
);
}
return
{
deviceMap
,
qualityMap
};
}
async
function
readSheet
(
sheet
:
any
[][],
gatewayPage
:
number
)
{
async
function
readSheet
(
sheet
:
any
[][],
gatewayPage
:
number
,
hour
:
number
)
{
// 取出所有区域名称,查询区域表获取区域数据
let
regionList
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{});
// let regionMap = {
// "会议室": "1001",
// "
公共办公区
": "1002",
// "办公室": "2001",
// "
大
会议室": "1001",
// "
大办公室
": "1002",
// "
小
办公室": "2001",
// "仓库": "2002",
// "CEO办公室": "2003",
// "传感器
生产
间": "3001",
// "前台": "0001",
// "传感器
测试车
间": "3001",
// "前台
休息区
": "0001",
// "卫生间": "3000"
// }
let
regionMap
:
any
=
{};
regionList
.
data
.
map
((
bean
)
=>
{
regionMap
[
bean
.
region
Name
]
=
bean
.
regionK
ey
regionMap
[
bean
.
region
_name
]
=
bean
.
region_k
ey
});
// 根据区域信息查询所有环境设备,通过环境设备查询最新环境指标
// ====== MOCK 数据(注释保留作为格式参考) ======
// let deviceMap = {
// "会议室": [
// "
大
会议室": [
// { "deviceId": "AC1", "deviceType": "空调", "power": "off" },
// { "deviceId": "Z1", "deviceType": "照明", "power": "on" }
// ],
// "
公共办公区
": [
// "
大办公室
": [
// { "deviceId": "AC2", "deviceType": "空调", "power": "on" },
// { "deviceId": "Z12", "deviceType": "照明", "power": "on" },
// { "deviceId": "F1", "deviceType": "排风", "power": "on" },
// { "deviceId": "E1", "deviceType": "IEQ传感器", "power": "on" },
// { "deviceId": "Y1", "deviceType": "烟感器", "power": "on" }
// ],
// "办公室": [
// "
小
办公室": [
// { "deviceId": "AC3", "deviceType": "空调", "power": "on" },
// { "deviceId": "Z2", "deviceType": "照明", "power": "on" },
// { "deviceId": "F2", "deviceType": "排风", "power": "on" }
// ],
// "仓库": [
{ "deviceId": "AC11", "deviceType": "空调", "power": "off" }
],
// "仓库": [],
// "CEO办公室": [{ "deviceId": "AC12", "deviceType": "空调", "power": "off" }],
// "传感器
生产
间": [{ "deviceId": "AC13", "deviceType": "空调", "power": "off" }],
// "传感器
测试车
间": [{ "deviceId": "AC13", "deviceType": "空调", "power": "off" }],
// "卫生间": [
// { "deviceId": "S1", "deviceType": "摄像头", "power": "on" },
// { "deviceId": "E100", "deviceType": "IEQ传感器", "power": "on" },
// { "deviceId": "X1", "deviceType": "音响", "power": "on" }
// ],
// "前台": [
// "前台
休息区
": [
// { "deviceId": "S0", "deviceType": "摄像头", "power": "on" },
// { "deviceId": "T1", "deviceType": "人流监测", "power": "on" }
// ]
// }
// ====== 动态查询:从数据库获取真实设备及最新状态 ======
const
deviceMap
=
await
buildDeviceMapDynamic
(
regionMap
);
// 循环区域,根据各区域环境指标计算各区域的环境质量得出优良中差四个值
// 先从 sheet 中解析 IEQ 列,构建 regionName → IEQ 标签映射
const
ieqTagMap
:
{
[
regionName
:
string
]:
string
}
=
{};
if
(
sheet
&&
sheet
.
length
>
0
)
{
const
headerRow
=
sheet
[
0
]
as
any
[];
const
ieqColIdx
=
headerRow
.
findIndex
((
h
:
any
)
=>
String
(
h
).
trim
()
===
'IEQ'
);
if
(
ieqColIdx
>=
0
)
{
for
(
let
i
=
1
;
i
<
sheet
.
length
;
i
++
)
{
const
row
=
sheet
[
i
]
as
any
[];
const
regionName
=
String
(
row
[
0
]
??
''
).
trim
();
if
(
regionName
&&
row
[
ieqColIdx
]
!=
null
)
{
ieqTagMap
[
regionName
]
=
String
(
row
[
ieqColIdx
]).
trim
();
}
}
}
}
const
{
deviceMap
,
qualityMap
}
=
await
buildDeviceMapDynamic
(
regionMap
,
ieqTagMap
,
hour
);
let
sheetData
=
await
planaryArrayBecomeOfBlockData
(
sheet
);
let
result
=
[];
if
(
sheetData
&&
sheetData
[
0
].
blockData
)
{
...
...
@@ -177,7 +372,7 @@ async function readSheet(sheet: any[][], gatewayPage: number) {
regionName
:
String
(
row
[
index
]
??
""
),
xz
:
row
[
xindex
],
yz
:
row
[
yindex
],
qualityIndex
:
row
[
index
]
?
row
[
xindex
]
<
500
?
"优"
:
row
[
xindex
]
<
1000
?
"良"
:
row
[
xindex
]
<
1200
?
"中"
:
"差"
:
"优"
,
qualityIndex
:
qualityMap
[
row
[
0
]]
??
'优'
,
gatewayPage
:
gatewayPage
,
devices
:
deviceMap
[
row
[
0
]]
}))??
[];
...
...
src/biz/running.ts
View file @
b29582fb
...
...
@@ -5,6 +5,7 @@
import
{
BizError
}
from
'../util/bizError'
;
import
{
ERRORENUM
}
from
'../config/errorEnum'
;
import
{
TABLENAME
}
from
'../config/dbEnum'
;
import
{
DEVICE_STATE
}
from
'../config/businessEnum'
;
import
{
selectDataListByParam
,
selectOneDataByParam
,
selectDataCountByParam
}
from
'../data/findData'
;
import
{
controlIndoorUnit
}
from
'./feiyiClient'
;
import
{
...
...
@@ -25,21 +26,6 @@ import {
formatMonthKey
,
}
from
'../util/dateUtils'
;
// ==================== 常量定义 ====================
const
deviceTypeList
:
string
[]
=
[
"IEQ传感器"
,
"烟感器"
,
"摄像头"
,
"空调"
,
"音响"
,
"照明"
,
"等离子"
,
"人流监测"
,
"电表"
,
"排风"
,
];
/**
* 空调中文到Key的映射
* 1 制冷 2 制热 3 送风 4 除湿
...
...
@@ -134,6 +120,32 @@ function getOverallQuality(co2: number, pm25: number): string {
/** 环境指标 key 映射:device_data JSON 中的 key → 展示用的 key */
const
ENV_INDICATOR_KEYS
=
[
'temperature'
,
'humidity'
,
'pm25'
,
'co2'
,
'hcho'
,
'lightLevel'
,
'pm10'
,
'pressure'
,
'tvoc'
];
/** 指标 key → 中文名映射(涵盖标准指标 + 迪勤编码指标) */
const
INDICATOR_NAME_MAP
:
Record
<
string
,
string
>
=
{
// 标准指标
'temperature'
:
'温度'
,
'humidity'
:
'湿度'
,
'pm25'
:
'PM2.5'
,
'pm10'
:
'PM10'
,
'co2'
:
'二氧化碳'
,
'tvoc'
:
'TVOC'
,
'hcho'
:
'甲醛'
,
'lightLevel'
:
'光照度'
,
'pressure'
:
'气压'
,
// 迪勤编码指标
'a01006'
:
'气压'
,
'a05024'
:
'臭氧'
,
'a09527'
:
'人在检测'
,
'a14186'
:
'PID-TVOC'
,
'a14263'
:
'噪音'
,
'a19001'
:
'氧含量'
,
'a21001'
:
'氨气'
,
'a21004'
:
'二氧化氮'
,
'a21005'
:
'一氧化碳'
,
'a21028'
:
'硫化氢'
,
'a31001'
:
'甲醛'
,
};
/**
* 查询所有 "空气质量监测" 类型设备的最新一条 device_data
* @returns Map<device_id, { data: JSON, device_time: string }>
...
...
@@ -485,77 +497,156 @@ export async function getRunAnalysis() {
};
// ========================================================================
// 二、设备耗电量监控
// 二、设备耗电量监控
(按区域聚合,分四个时间范围)
/* ====== MOCK DATA(保留,勿删) ======
let deviceTypeMap = new Map();
let deviceElectricityMap = new Map();
let deviceElectricityDatas = deviceTypeList.map((deviceType) => {
return {
"deviceType": deviceType,
"deviceCount": deviceTypeMap.get(deviceType) || 0,
"electricity": deviceElectricityMap.get(deviceType) || 0
}
});
let deviceElectricity = {
titleList: ["设备","数量","耗电情况"],
dataList: deviceElectricityDatas
last24Hours: {
titleList: ["区域", "设备数量", "耗电情况", "耗电量(kwh)"],
dataList: [
{ "region": "汇报厅", "deviceCount": 12, "electricityStatus": 52.3, "kwh": 15.8 },
]
},
last7Days: {
titleList: ["区域", "设备数量", "耗电情况", "耗电量(kwh)"],
dataList: [
{ "region": "汇报厅", "deviceCount": 12, "electricityStatus": 50.1, "kwh": 110.5 },
]
},
last30Days: {
titleList: ["区域", "设备数量", "耗电情况", "耗电量(kwh)"],
dataList: [
{ "region": "汇报厅", "deviceCount": 12, "electricityStatus": 48.6, "kwh": 450.2 },
]
},
lastYear: {
titleList: ["区域", "设备数量", "耗电情况", "耗电量(kwh)"],
dataList: [
{ "region": "汇报厅", "deviceCount": 12, "electricityStatus": 45.3, "kwh": 5200.0 },
]
},
};
====== MOCK DATA END ====== */
// ---- 真实查询 ----
// 查询所有设备,按 device_type 分组统计数量
// 查询所有区域
const
regionResult
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{},
[
'region_key'
,
'region_name'
]
);
const
allRegions
:
{
region_key
:
string
;
region_name
:
string
}[]
=
regionResult
.
data
||
[];
const
regionNameMap
=
new
Map
<
string
,
string
>
();
allRegions
.
forEach
(
r
=>
regionNameMap
.
set
(
r
.
region_key
,
r
.
region_name
||
''
));
// 查询所有设备(含 region_key),按区域统计数量
const
allDevicesResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{},
[
'device_id'
,
'
device_typ
e'
]
[
'device_id'
,
'
region_key'
,
'device_stat
e'
]
);
const
allDevices
=
allDevicesResult
.
data
||
[];
// 按
类型
统计设备数量
const
deviceTyp
eCountMap
=
new
Map
<
string
,
number
>
();
// 按
区域
统计设备数量
const
regionDevic
eCountMap
=
new
Map
<
string
,
number
>
();
allDevices
.
forEach
((
d
:
any
)
=>
{
const
dt
=
d
.
device_type
;
deviceTypeCountMap
.
set
(
dt
,
(
deviceTypeCountMap
.
get
(
dt
)
||
0
)
+
1
);
const
rk
=
d
.
region_key
||
''
;
if
(
rk
)
{
regionDeviceCountMap
.
set
(
rk
,
(
regionDeviceCountMap
.
get
(
rk
)
||
0
)
+
1
);
}
});
// 查询当日各电表设备的总电量
// 汇总有设备的区域 key
const
allRegionKeys
=
new
Set
<
string
>
();
regionDeviceCountMap
.
forEach
((
_
,
rk
)
=>
allRegionKeys
.
add
(
rk
));
// 查询所有电表设备(含 region_key)
const
meterDevicesResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_type
:
'电表'
},
[
'device_id'
]
[
'device_id'
,
'region_key'
]
);
const
meterDeviceIds
=
(
meterDevicesResult
.
data
||
[]).
map
((
d
:
any
)
=>
d
.
device_id
);
const
meterDevices
=
meterDevicesResult
.
data
||
[];
const
meterDeviceIds
=
meterDevices
.
map
((
d
:
any
)
=>
d
.
device_id
);
// 构建电表→区域映射
const
meterRegionMap
=
new
Map
<
string
,
string
>
();
meterDevices
.
forEach
((
d
:
any
)
=>
{
meterRegionMap
.
set
(
d
.
device_id
,
d
.
region_key
||
''
);
});
// 按设备类型统计耗电:仅电表类型有数据
let
deviceElectricityMap
=
new
Map
<
string
,
number
>
();
if
(
meterDeviceIds
.
length
>
0
)
{
const
meterDataResult
=
await
selectDataListByParam
(
// 耗电量查询终点:当前时刻(起点复用上方已定义的 last24hStart/last7dStart/last30dStart/lastYearStart)
const
elecNowStr
=
formatTime
(
new
Date
());
/** 查询指定时间范围内各区域的耗电量 */
async
function
queryRegionElectricity
(
startTime
:
string
,
endTime
:
string
):
Promise
<
Map
<
string
,
number
>>
{
const
map
=
new
Map
<
string
,
number
>
();
if
(
meterDeviceIds
.
length
===
0
)
return
map
;
const
result
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
'%in%'
:
meterDeviceIds
},
device_time
:
{
'%gte%'
:
todayStart
},
device_time
:
{
'%gte%'
:
startTime
,
'%lte%'
:
endTime
},
},
[
'device_data'
]
[
'device_
id'
,
'device_
data'
]
);
let
meterTotal
=
0
;
(
meterDataResult
.
data
||
[]).
forEach
((
r
:
any
)
=>
{
(
result
.
data
||
[]).
forEach
((
r
:
any
)
=>
{
const
parsed
=
parseDeviceData
(
r
.
device_data
);
const
val
=
parsed
[
ELECTRICITY_KEY
];
if
(
typeof
val
===
'number'
)
meterTotal
+=
val
;
if
(
typeof
val
===
'number'
)
{
const
rk
=
meterRegionMap
.
get
(
r
.
device_id
)
||
''
;
if
(
rk
)
{
map
.
set
(
rk
,
(
map
.
get
(
rk
)
||
0
)
+
val
);
}
}
});
deviceElectricityMap
.
set
(
'电表'
,
Math
.
round
(
meterTotal
*
100
)
/
100
)
;
return
map
;
}
let
deviceElectricityDatas
=
deviceTypeList
.
map
((
deviceType
)
=>
{
return
{
"deviceType"
:
deviceType
,
"deviceCount"
:
deviceTypeCountMap
.
get
(
deviceType
)
||
0
,
"electricity"
:
deviceElectricityMap
.
get
(
deviceType
)
||
0
}
});
/** 根据耗电量 map 构建 dataList */
function
buildElectricityDataList
(
electricityMap
:
Map
<
string
,
number
>
)
{
let
total
=
0
;
electricityMap
.
forEach
(
v
=>
total
+=
v
);
return
Array
.
from
(
allRegionKeys
).
map
(
rk
=>
{
const
kwh
=
Math
.
round
((
electricityMap
.
get
(
rk
)
||
0
)
*
100
)
/
100
;
const
electricityStatus
=
total
>
0
?
Math
.
round
((
kwh
/
total
)
*
1000
)
/
10
:
0
;
return
{
region
:
regionNameMap
.
get
(
rk
)
||
rk
,
deviceCount
:
regionDeviceCountMap
.
get
(
rk
)
||
0
,
electricityStatus
,
kwh
,
};
});
}
const
electricityTitleList
=
[
"区域"
,
"设备数量"
,
"耗电情况"
,
"耗电量(kwh)"
];
// 分别查询四个时间范围
const
[
last24hMap
,
last7dMap
,
last30dMap
,
lastYearMap
]
=
await
Promise
.
all
([
queryRegionElectricity
(
last24hStart
,
elecNowStr
),
queryRegionElectricity
(
last7dStart
,
elecNowStr
),
queryRegionElectricity
(
last30dStart
,
elecNowStr
),
queryRegionElectricity
(
lastYearStart
,
elecNowStr
),
]);
let
deviceElectricity
=
{
titleList
:
[
"设备"
,
"数量"
,
"耗电情况"
],
dataList
:
deviceElectricityDatas
last24Hours
:
{
titleList
:
electricityTitleList
,
dataList
:
buildElectricityDataList
(
last24hMap
),
},
last7Days
:
{
titleList
:
electricityTitleList
,
dataList
:
buildElectricityDataList
(
last7dMap
),
},
last30Days
:
{
titleList
:
electricityTitleList
,
dataList
:
buildElectricityDataList
(
last30dMap
),
},
lastYear
:
{
titleList
:
electricityTitleList
,
dataList
:
buildElectricityDataList
(
lastYearMap
),
},
};
// ========================================================================
...
...
@@ -573,9 +664,9 @@ export async function getRunAnalysis() {
// ---- 真实查询 ----
const
allDevFull
=
allDevices
;
const
total
=
allDevFull
.
length
;
const
onlineTotal
=
allDevFull
.
filter
((
d
:
any
)
=>
d
.
device_state
===
1
).
length
;
const
offlineTotal
=
allDevFull
.
filter
((
d
:
any
)
=>
d
.
device_state
===
0
).
length
;
const
faultTotal
=
allDevFull
.
filter
((
d
:
any
)
=>
d
.
device_state
===
2
).
length
;
const
onlineTotal
=
allDevFull
.
filter
((
d
:
any
)
=>
d
.
device_state
===
DEVICE_STATE
.
在线
).
length
;
const
offlineTotal
=
allDevFull
.
filter
((
d
:
any
)
=>
d
.
device_state
===
DEVICE_STATE
.
离线
).
length
;
const
faultTotal
=
allDevFull
.
filter
((
d
:
any
)
=>
d
.
device_state
===
DEVICE_STATE
.
故障
).
length
;
let
deviceMonitor
=
{
total
:
total
,
...
...
@@ -1001,9 +1092,9 @@ export async function getRunAnalysis() {
====== MOCK DATA END ====== */
// ---- 真实查询 ----
const
runningDevices
=
allDevFull
.
filter
((
d
:
any
)
=>
d
.
device_state
===
1
).
length
;
const
faultDevices
=
allDevFull
.
filter
((
d
:
any
)
=>
d
.
device_state
===
2
).
length
;
const
offlineDevicesSummary
=
allDevFull
.
filter
((
d
:
any
)
=>
d
.
device_state
===
0
).
length
;
const
runningDevices
=
allDevFull
.
filter
((
d
:
any
)
=>
d
.
device_state
===
DEVICE_STATE
.
在线
).
length
;
const
faultDevices
=
allDevFull
.
filter
((
d
:
any
)
=>
d
.
device_state
===
DEVICE_STATE
.
故障
).
length
;
const
offlineDevicesSummary
=
allDevFull
.
filter
((
d
:
any
)
=>
d
.
device_state
===
DEVICE_STATE
.
离线
).
length
;
const
normalRate
=
total
>
0
?
((
runningDevices
/
total
)
*
100
).
toFixed
(
2
)
+
'%'
:
'0.00%'
;
let
summaryData
=
{
...
...
@@ -1074,9 +1165,9 @@ export async function getAnalysisPopup(regionKey: string) {
// 设备状态监测(总数/在线/离线/故障)
const
total
=
devices
.
length
;
const
online
=
devices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
1
).
length
;
const
offline
=
devices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
0
).
length
;
const
fault
=
devices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
2
).
length
;
const
online
=
devices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
DEVICE_STATE
.
在线
).
length
;
const
offline
=
devices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
DEVICE_STATE
.
离线
).
length
;
const
fault
=
devices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
DEVICE_STATE
.
故障
).
length
;
let
sbztjc
=
{
"total"
:
total
,
...
...
@@ -1117,9 +1208,9 @@ export async function getAnalysisPopup(regionKey: string) {
let
hOnline
=
0
,
hOffline
=
0
,
hFault
=
0
;
devices
.
forEach
((
d
:
any
)
=>
{
const
reported
=
reportedDeviceSet
.
has
(
d
.
device_id
);
if
(
reported
&&
d
.
device_state
===
1
)
hOnline
++
;
else
if
(
d
.
device_state
===
0
)
hOffline
++
;
else
if
(
d
.
device_state
===
2
)
hFault
++
;
if
(
reported
&&
d
.
device_state
===
DEVICE_STATE
.
在线
)
hOnline
++
;
else
if
(
d
.
device_state
===
DEVICE_STATE
.
离线
)
hOffline
++
;
else
if
(
d
.
device_state
===
DEVICE_STATE
.
故障
)
hFault
++
;
else
if
(
!
reported
)
hOffline
++
;
// 未上报也算离线
});
sbztjcqs
.
push
({
...
...
@@ -1471,8 +1562,8 @@ export async function getRunEnvironmental() {
const
regionDevices
=
envDevices
.
filter
((
d
:
any
)
=>
d
.
region_key
===
region
.
region_key
);
if
(
regionDevices
.
length
===
0
)
continue
;
const
onlineCount
=
regionDevices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
1
).
length
;
const
offlineCount
=
regionDevices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
0
).
length
;
const
onlineCount
=
regionDevices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
DEVICE_STATE
.
在线
).
length
;
const
offlineCount
=
regionDevices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
DEVICE_STATE
.
离线
).
length
;
const
totalCount
=
regionDevices
.
length
;
let
deviceStatus
:
string
;
...
...
@@ -1601,9 +1692,9 @@ export async function getRunMonitoring() {
const
totalCount
=
regionDevices
.
length
;
if
(
totalCount
===
0
)
return
;
// 无设备区域跳过
const
onlineCount
=
regionDevices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
1
).
length
;
const
offlineCount
=
regionDevices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
0
).
length
;
const
faultCount
=
regionDevices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
2
).
length
;
const
onlineCount
=
regionDevices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
DEVICE_STATE
.
在线
).
length
;
const
offlineCount
=
regionDevices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
DEVICE_STATE
.
离线
).
length
;
const
faultCount
=
regionDevices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
DEVICE_STATE
.
故障
).
length
;
// status: 区域内所有设备正常=0,存在离线或故障=1(异常)
const
hasAbnormal
=
(
offlineCount
+
faultCount
)
>
0
;
...
...
@@ -1790,9 +1881,9 @@ export async function getRunMonitoring() {
// ---- 真实查询 ----
const
total
=
allDevices
.
length
;
const
runningDevices
=
allDevices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
1
).
length
;
const
offlineDevicesSummary
=
allDevices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
0
).
length
;
const
faultDevices
=
allDevices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
2
).
length
;
const
runningDevices
=
allDevices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
DEVICE_STATE
.
在线
).
length
;
const
offlineDevicesSummary
=
allDevices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
DEVICE_STATE
.
离线
).
length
;
const
faultDevices
=
allDevices
.
filter
((
d
:
any
)
=>
d
.
device_state
===
DEVICE_STATE
.
故障
).
length
;
const
normalRate
=
total
>
0
?
((
runningDevices
/
total
)
*
100
).
toFixed
(
2
)
+
'%'
:
'0.00%'
;
let
summaryData
=
{
...
...
@@ -1840,15 +1931,16 @@ export async function getMonitorPopup(regionKey: string) {
"deviceName": "空调内机8-1-0-2",
"deviceType": "空调",
"status": 0,
"parameter": "8-1-0-2",
//
"parameter": "8-1-0-2",
"monitoringData": ""
},{
"deviceId": "24E124710E466083",
"deviceName": "环境监测cgq6",
"deviceType": "IEQ传感器",
"status": 0,
"parameter": "o3,co2,hpa,pir,hcho,pm10,pm25,tvoc,humidity,lighting,pressure,temperature",
"monitoringData": "10、2000、1260、5、1.25、1000、1000、500、100、L5、5000、60"
// "parameter": "o3,co2,hpa,pir,hcho,pm10,pm25,tvoc,humidity,lighting,pressure,temperature",
// "monitoringData": "10、2000、1260、5、1.25、1000、1000、500、100、L5、5000、60"
"monitoringData": "温度: 26、湿度: 60、人在检测: 0、噪音: 39.5、甲醛: 10"
}
]
}
...
...
@@ -1860,7 +1952,7 @@ export async function getMonitorPopup(regionKey: string) {
// 查询区域信息和设备列表
const
regionName
=
await
getRegionName
(
regionKey
);
const
devices
=
await
getDevicesByRegion
(
regionKey
,
[
'device_id'
,
'device_type'
,
'device_name'
,
'device_
param'
,
'device_
state'
]);
const
devices
=
await
getDevicesByRegion
(
regionKey
,
[
'device_id'
,
'device_type'
,
'device_name'
,
'device_state'
]);
// ==================== IEQ传感器监测 ====================
// 查询该区域下 IEQ传感器 设备的最新数据
...
...
@@ -1930,17 +2022,25 @@ export async function getMonitorPopup(regionKey: string) {
const
deviceName
=
device
.
device_name
||
''
;
const
deviceType
=
device
.
device_type
||
''
;
const
deviceState
=
device
.
device_state
;
//
status
: 在线=0,离线=1,故障=2
const
status
=
deviceState
===
1
?
0
:
(
deviceState
===
0
?
1
:
2
);
const
parameter
=
device
.
device_param
||
''
;
//
deviceState
: 在线=0,离线=1,故障=2
// status: 1 on 0 off
const
status
=
deviceState
===
DEVICE_STATE
.
在线
?
1
:
0
;
// 从批量查询结果中获取该设备最新监测数据
let
monitoringData
=
''
;
const
latestData
=
latestDataMap
.
get
(
deviceId
);
if
(
latestData
)
{
const
vals
=
ENV_INDICATOR_KEYS
.
map
(
k
=>
latestData
[
k
]).
filter
(
v
=>
v
!==
undefined
&&
v
!==
null
);
if
(
vals
.
length
>
0
)
{
monitoringData
=
vals
.
join
(
'、'
);
const
items
:
string
[]
=
[];
for
(
const
key
of
Object
.
keys
(
latestData
))
{
const
val
=
latestData
[
key
];
if
(
val
===
undefined
||
val
===
null
)
continue
;
const
name
=
INDICATOR_NAME_MAP
[
key
];
if
(
name
)
{
items
.
push
(
`
${
name
}
:
${
val
}
`
);
}
}
if
(
items
.
length
>
0
)
{
monitoringData
=
items
.
join
(
'、'
);
}
}
...
...
@@ -1949,7 +2049,6 @@ export async function getMonitorPopup(regionKey: string) {
deviceName
,
deviceType
,
status
,
parameter
,
monitoringData
,
};
});
...
...
@@ -1958,7 +2057,7 @@ export async function getMonitorPopup(regionKey: string) {
"regionName"
:
regionName
,
"kqzljc"
:
kqzljc
,
"sbjc"
:
{
"titleList"
:
[
"编号"
,
"设备"
,
"状态"
,
"
参数"
,
"
监测数据"
],
"titleList"
:
[
"编号"
,
"设备"
,
"状态"
,
"监测数据"
],
"dataList"
:
sbjcDataList
,
}
};
...
...
src/config/businessEnum.ts
0 → 100644
View file @
b29582fb
// ==================== 设备相关枚举 ====================
/**
* 设备类型枚举
*/
export
enum
DEVICE_TYPE
{
/** IEQ传感器(迪勤) */
IEQ
传感器
=
'IEQ传感器'
,
/** 烟感器(规划中) */
烟感器
=
'烟感器'
,
/** 摄像头(规划中) */
摄像头
=
'摄像头'
,
/** 空调内机(飞奕) */
空调
=
'空调'
,
/** 新风机(飞奕,区域名含"新风"时自动判定) */
新风
=
'新风'
,
/** 音响(规划中) */
音响
=
'音响'
,
/** 照明(规划中) */
照明
=
'照明'
,
/** 等离子(规划中) */
等离子
=
'等离子'
,
/** 人流监测(规划中) */
人流监测
=
'人流监测'
,
/** 电表(飞奕) */
电表
=
'电表'
,
/** 排风(规划中) */
排风
=
'排风'
,
}
/**
* 设备状态枚举
*/
export
enum
DEVICE_STATE
{
/** 在线 */
在线
=
0
,
/** 离线 */
离线
=
1
,
/** 故障 */
故障
=
2
,
}
// ==================== 故障相关枚举 ====================
/**
* 故障状态枚举
*/
export
enum
FAULT_STATUS
{
/** 未处理 */
未处理
=
0
,
/** 处理中 */
处理中
=
1
,
/** 已处理 */
已处理
=
2
,
}
/**
* 故障等级枚举
*/
export
enum
FAULT_LEVEL
{
/** 一级(轻微) */
一级
=
1
,
/** 二级(一般) */
二级
=
2
,
/** 三级(严重) */
三级
=
3
,
}
// ==================== 区域相关枚举 ====================
/**
* 区域类型枚举
*/
export
enum
REGION_TYPE
{
/** 楼栋 */
楼栋
=
'楼栋'
,
/** 房间 */
房间
=
'房间'
,
/** 办公室 */
办公室
=
'办公室'
,
/** 机房 */
机房
=
'机房'
,
/** 车间 */
车间
=
'车间'
,
/** 迪勤场所 */
场所
=
'场所'
,
/** 迪勤监测点 */
监测点
=
'监测点'
,
}
// ==================== 关联相关枚举 ====================
/**
* 区域设备关联类型枚举
*/
export
enum
RELATION_TYPE
{
/** 区域一对多设备 */
region_to_device
=
'region_to_device'
,
/** 设备一对多区域 */
device_to_region
=
'device_to_region'
,
}
// ==================== 空调控制相关枚举 ====================
/**
* 工作模式枚举
*/
export
enum
WORK_MODE
{
制冷
=
1
,
制热
=
2
,
送风
=
3
,
除湿
=
4
,
}
/**
* 开关状态枚举
*/
export
enum
POWER_STATE
{
on
=
'on'
,
off
=
'off'
,
}
/**
* 风速枚举
*/
export
enum
FAN_SPEED
{
超强
=
1
,
强
=
2
,
弱
=
4
,
}
/**
* 自动控制状态枚举
*/
export
enum
AUTO_CONTROL
{
生效
=
'生效'
,
解除
=
'解除'
,
}
src/config/mysqlTableConfig.ts
View file @
b29582fb
...
...
@@ -68,6 +68,12 @@ export const TablesConfig = [
unique
:
true
,
comment
:
'区域编码,唯一'
},
region_ad
:
{
type
:
Sequelize
.
STRING
(
100
),
allowNull
:
true
,
defaultValue
:
null
,
comment
:
'备用区域编码,多平台接入同一区域时存储其他平台的region_key'
},
region_name
:
{
type
:
Sequelize
.
STRING
(
255
),
allowNull
:
false
,
...
...
@@ -162,13 +168,13 @@ export const TablesConfig = [
device_type
:
{
type
:
Sequelize
.
STRING
(
50
),
allowNull
:
false
,
comment
:
'设备类型:IEQ传感器, 烟感器, 摄像头, 空调, 音响, 照明, 等离子, 人流监测, 电表, 排风'
comment
:
'设备类型:IEQ传感器, 烟感器, 摄像头, 空调,
新风,
音响, 照明, 等离子, 人流监测, 电表, 排风'
},
device_state
:
{
type
:
DataTypes
.
INTEGER
,
allowNull
:
false
,
defaultValue
:
0
,
comment
:
'设备状态:0=
离线, 1=在
线, 2=故障'
comment
:
'设备状态:0=
在线, 1=离
线, 2=故障'
},
device_name
:
{
type
:
Sequelize
.
STRING
(
255
),
...
...
@@ -482,9 +488,9 @@ export const TablesConfig = [
comment
:
'故障类型'
},
fault_level
:
{
type
:
Sequelize
.
STRING
(
50
)
,
type
:
Sequelize
.
INTEGER
,
allowNull
:
true
,
comment
:
'故障等级:
一级, 二级,
三级'
comment
:
'故障等级:
1=一级, 2=二级, 3=
三级'
},
fault_info
:
{
type
:
DataTypes
.
JSON
,
...
...
@@ -497,10 +503,10 @@ export const TablesConfig = [
comment
:
'故障时间'
},
fault_status
:
{
type
:
Sequelize
.
STRING
(
50
)
,
type
:
Sequelize
.
INTEGER
,
allowNull
:
false
,
defaultValue
:
'未处理'
,
comment
:
'故障状态
:未处理, 处理中,
已处理'
defaultValue
:
0
,
comment
:
'故障状态
: 0=未处理, 1=处理中, 2=
已处理'
},
handle_time
:
{
type
:
DataTypes
.
DATE
,
...
...
src/config/schedule.ts
View file @
b29582fb
...
...
@@ -6,7 +6,7 @@
* - 任务锁死时(超时未释放),会主动强制释放锁,确保后续任务能正常执行
*/
import
{
region
,
device
,
deviceData
,
alertWorkData
,
processFaultStatus
,
diqinSyncAll
}
from
"../biz/dataIntegration"
;
import
{
region
,
device
,
deviceData
,
alertWorkData
,
processFaultStatus
,
diqinSyncAll
,
migrateDeviceRegionKey
}
from
"../biz/dataIntegration"
;
// ==================== 通用任务锁 ====================
...
...
@@ -133,6 +133,7 @@ const dataSyncTask = createScheduledTask('定时任务', TASK_INTERVAL_MS, dataS
await
device
();
await
deviceData
();
await
diqinSyncAll
();
await
migrateDeviceRegionKey
();
});
// 预警工单任务
...
...
src/routers/region.ts
View file @
b29582fb
...
...
@@ -15,9 +15,9 @@ export function setRouter(httpServer) {
* 区域位置
*/
async
function
getRegionLocation
(
req
,
res
)
{
let
reqConf
=
{
gatewayPage
:
'Number'
};
const
NotMustHaveKeys
=
[];
let
{
gatewayPage
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
const
result
=
await
regionBiz
.
getRegionLocation
(
gatewayPage
);
let
reqConf
=
{
gatewayPage
:
'Number'
,
hour
:
'Number'
};
const
NotMustHaveKeys
=
[
'hour'
];
let
{
gatewayPage
,
hour
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
const
result
=
await
regionBiz
.
getRegionLocation
(
gatewayPage
,
hour
);
res
.
success
(
result
);
}
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment