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
8eb38c38
Commit
8eb38c38
authored
Jul 22, 2026
by
PC-20251223ZVQQ\Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
接口开发
parent
92e486c8
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
3853 additions
and
407 deletions
+3853
-407
区域坐标分布.xlsx
res/区域坐标分布.xlsx
+0
-0
serverConfig.xml
serverConfig.xml
+10
-0
dataIntegration.ts
src/biz/dataIntegration.ts
+1477
-0
diqinClient.ts
src/biz/diqinClient.ts
+324
-11
feiyiClient.ts
src/biz/feiyiClient.ts
+93
-16
region.ts
src/biz/region.ts
+101
-31
running.ts
src/biz/running.ts
+1653
-338
clientEnum.ts
src/config/clientEnum.ts
+6
-2
dbEnum.ts
src/config/dbEnum.ts
+4
-0
mysqlTableConfig.ts
src/config/mysqlTableConfig.ts
+21
-9
schedule.ts
src/config/schedule.ts
+157
-0
main.ts
src/main.ts
+7
-0
No files found.
res/区域坐标分布.xlsx
View file @
8eb38c38
No preview for this file type
serverConfig.xml
View file @
8eb38c38
...
@@ -16,4 +16,14 @@
...
@@ -16,4 +16,14 @@
<mysqlPwd>root</mysqlPwd>
<mysqlPwd>root</mysqlPwd>
<dataBase>dst_technology_platform</dataBase> -->
<dataBase>dst_technology_platform</dataBase> -->
</mysqldb>
</mysqldb>
<feiyi>
<feiyiAppKey>
0
</feiyiAppKey>
<feiyiSecretKey>
0
</feiyiSecretKey>
<feiyiBaseUrl>
https://m.achelp.cn/open
</feiyiBaseUrl>
</feiyi>
<diqin>
<diqinAppKey>
178428005410049
</diqinAppKey>
<diqinSecretKey>
pGAII3TfpdrJ4zjrPncERwtt
</diqinSecretKey>
<diqinBaseUrl>
https://airiccc.com
</diqinBaseUrl>
</diqin>
</config>
</config>
src/biz/dataIntegration.ts
0 → 100644
View file @
8eb38c38
import
{
getRegionTree
,
getIndoorUnitList
,
getElectricMaterList
,
getIndoorUnitStateList
,
getMaterStateList
,
getIndoorUnitAlarmErrorHis
,
getFaultCodeInfo
,
getFaultTypeName
,
}
from
"./feiyiClient"
;
import
{
getLocations
,
getLocationDetail
,
getDataSources
,
getDataSourceDetail
,
}
from
"./diqinClient"
;
import
{
mysqlModelMap
}
from
"../model/sqlModelBind"
;
import
Sequelize
from
"sequelize"
;
import
moment
from
"moment"
;
import
XLSX
from
"xlsx"
;
import
path
from
"path"
;
const
Op
=
Sequelize
.
Op
;
/** 飞奕品牌编码,默认 "1"=日立 */
const
FEIYI_BRAND_CODE
=
"1"
;
// ==================== 迪勤:设备类型映射配置 ====================
/**
* 迪勤 data_source_model_name → 本系统 device_type 映射表
* 初始仅 IEQ → IEQ传感器
* ORI PRO / ORI API 跳过不处理
*/
const
DIQIN_DEVICE_TYPE_MAP
:
Record
<
string
,
string
>
=
{
'IEQ'
:
'IEQ传感器'
,
};
// ==================== 工具函数 ====================
/**
* 获取今天的日期字符串,格式 YYYYMMDD(使用本地时区,避免时区问题)
*/
function
getTodayDateStr
():
string
{
return
moment
().
format
(
'YYYYMMDD'
);
}
/**
* 从内机四段地址(如 "2-1-0-1")提取外机地址("2-1-0")
*/
function
getOuterAddress
(
indoorUnitAddressFull
:
string
):
string
|
null
{
if
(
!
indoorUnitAddressFull
)
return
null
;
const
parts
=
indoorUnitAddressFull
.
split
(
'-'
);
if
(
parts
.
length
>=
3
)
{
return
parts
.
slice
(
0
,
3
).
join
(
'-'
);
}
return
null
;
}
/**
* 秒级时间比较:itemTime > maxTime(精度对齐,避免 MySQL DATETIME 秒级与接口毫秒级不一致导致重复插入)
* @returns true 表示 itemTime 比 maxTime 更新,应该保留
*/
function
isNewerThan
(
itemTime
:
Date
|
null
,
maxTime
:
Date
|
null
|
undefined
):
boolean
{
if
(
maxTime
===
undefined
)
return
true
;
// 新设备,无历史数据
if
(
maxTime
===
null
||
!
itemTime
)
return
true
;
const
itemSec
=
Math
.
floor
(
itemTime
.
getTime
()
/
1000
);
const
maxSec
=
Math
.
floor
(
maxTime
.
getTime
()
/
1000
);
return
itemSec
>
maxSec
;
}
/**
* 批量查询设备在各表中的最大 device_time 并构建 Map
*/
async
function
buildMaxTimeMap
(
deviceDataModel
:
any
,
deviceIds
:
string
[],
):
Promise
<
Map
<
string
,
Date
|
null
>>
{
const
map
=
new
Map
<
string
,
Date
|
null
>
();
if
(
deviceIds
.
length
===
0
)
return
map
;
const
results
=
await
deviceDataModel
.
findAll
({
attributes
:
[
'device_id'
,
[
deviceDataModel
.
sequelize
.
fn
(
'MAX'
,
deviceDataModel
.
sequelize
.
col
(
'device_time'
)),
'max_time'
],
],
where
:
{
device_id
:
deviceIds
},
group
:
[
'device_id'
],
raw
:
true
,
});
for
(
const
r
of
results
as
any
[])
{
map
.
set
(
r
.
device_id
,
r
.
max_time
?
new
Date
(
r
.
max_time
)
:
null
);
}
return
map
;
}
/**
* 从 Excel 文件读取电表设备编号 → 外机地址 的映射表
*/
function
loadMeterOuterAddrMap
():
Map
<
string
,
string
>
{
const
map
=
new
Map
<
string
,
string
>
();
try
{
const
filePath
=
path
.
resolve
(
__dirname
,
'../../res/电表对应区域.xlsx'
);
const
workbook
=
XLSX
.
readFile
(
filePath
);
const
sheet
=
workbook
.
Sheets
[
workbook
.
SheetNames
[
0
]];
const
rows
:
any
[]
=
XLSX
.
utils
.
sheet_to_json
(
sheet
);
for
(
const
row
of
rows
)
{
const
meterCode
=
row
[
'电表设备编号'
];
const
outerAddr
=
row
[
'空调外机地址'
];
if
(
meterCode
&&
outerAddr
)
{
map
.
set
(
String
(
meterCode
),
String
(
outerAddr
));
}
}
console
.
log
(
`[工具] 电表对应区域.xlsx 读取完成,共
${
map
.
size
}
条映射`
);
}
catch
(
err
)
{
console
.
error
(
'[工具] 读取电表对应区域.xlsx 失败:'
,
err
);
}
return
map
;
}
// ==================== 分页拉取辅助函数 ====================
/**
* 分页拉取所有数据(通用)
* @param fetchFn 分页查询函数,入参 { page, limit, ...extraParams },返回 { page, limit, total, rows }
* @param extraParams 额外的查询参数,会透传每次调用
* @param limit 每页条数,默认 100
*/
async
function
fetchAllPages
(
fetchFn
:
(
page
:
number
,
limit
:
number
,
extraParams
:
Record
<
string
,
any
>
)
=>
Promise
<
any
>
,
extraParams
:
Record
<
string
,
any
>
=
{},
limit
:
number
=
100
,
):
Promise
<
any
[]
>
{
const
allRows
:
any
[]
=
[];
let
page
=
1
;
let
hasMore
=
true
;
while
(
hasMore
)
{
const
result
=
await
fetchFn
(
page
,
limit
,
extraParams
);
if
(
!
result
||
!
result
.
rows
||
result
.
rows
.
length
===
0
)
{
hasMore
=
false
;
break
;
}
allRows
.
push
(...
result
.
rows
);
// 判断是否还有下一页
if
(
page
*
limit
>=
result
.
total
)
{
hasMore
=
false
;
}
else
{
page
++
;
}
}
return
allRows
;
}
// ==================== 区域集成 ====================
/**
* 递归遍历建筑物树,提取所有房间节点(buildingType === 3)
* 同时追溯父节点获取楼层和楼栋信息
*/
function
extractRoomsFromTree
(
tree
:
any
[],
parentFloor
:
{
floorId
:
string
;
floorName
:
string
}
|
null
=
null
,
parentBuilding
:
{
buildingId
:
string
;
buildingName
:
string
}
|
null
=
null
,
):
Array
<
{
roomId
:
string
;
roomName
:
string
;
floorId
:
string
;
floorName
:
string
;
buildingId
:
string
;
buildingName
:
string
;
}
>
{
const
rooms
:
any
[]
=
[];
for
(
const
node
of
tree
)
{
const
buildingType
=
node
.
buildingType
;
const
buildingId
=
node
.
buildingId
;
const
buildingName
=
node
.
buildingName
;
const
children
=
node
.
children
||
[];
if
(
buildingType
===
1
)
{
// 楼栋节点
const
building
=
{
buildingId
,
buildingName
};
rooms
.
push
(...
extractRoomsFromTree
(
children
,
null
,
building
));
}
else
if
(
buildingType
===
2
)
{
// 楼层节点
const
floor
=
{
floorId
:
buildingId
,
floorName
:
buildingName
};
rooms
.
push
(...
extractRoomsFromTree
(
children
,
floor
,
parentBuilding
));
}
else
if
(
buildingType
===
3
)
{
// 房间节点
rooms
.
push
({
roomId
:
buildingId
,
roomName
:
buildingName
,
floorId
:
parentFloor
?.
floorId
||
''
,
floorName
:
parentFloor
?.
floorName
||
''
,
buildingId
:
parentBuilding
?.
buildingId
||
''
,
buildingName
:
parentBuilding
?.
buildingName
||
''
,
});
}
}
return
rooms
;
}
/**
* 数据集成-区域集成
* 按最小节点(房间)平铺集成,一条数据对应一个房间
*/
export
async
function
region
()
{
console
.
log
(
'[区域集成] 开始执行...'
);
try
{
const
regionModel
=
mysqlModelMap
[
'region'
];
if
(
!
regionModel
)
{
console
.
error
(
'[区域集成] region 表模型未初始化,跳过'
);
return
;
}
// 1. 获取建筑物树结构
const
treeData
=
await
getRegionTree
();
if
(
!
treeData
||
!
Array
.
isArray
(
treeData
))
{
console
.
warn
(
'[区域集成] 未获取到建筑物树数据'
);
return
;
}
// 2. 提取所有房间节点
const
rooms
=
extractRoomsFromTree
(
treeData
);
console
.
log
(
`[区域集成] 共提取到
${
rooms
.
length
}
个房间节点`
);
// 3. 逐房间检查并插入
let
insertCount
=
0
;
let
skipCount
=
0
;
for
(
const
room
of
rooms
)
{
// 校验是否已存在(通过 room_id)
const
existing
=
await
regionModel
.
findOne
({
where
:
{
room_id
:
room
.
roomId
}
});
if
(
existing
)
{
skipCount
++
;
continue
;
}
// 插入新区域记录
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 对应楼栋名称
sort_order
:
0
,
});
insertCount
++
;
}
console
.
log
(
`[区域集成] 完成,新增
${
insertCount
}
条,跳过
${
skipCount
}
条已存在`
);
// 2. 一次性查询所有 region,后续两步共用内存数据,避免 DB 读写时序问题
const
allRegions
=
await
regionModel
.
findAll
({
raw
:
true
})
as
any
[];
// 3. 同步空调内机 → 反写 region.address
const
updatedRegions
=
await
syncRegionAddressByAcDevices
(
regionModel
,
allRegions
);
// 4. 同步电表设备 region_key(复用上一步已更新 address 的内存数据)
await
syncMeterDeviceRegionKey
(
updatedRegions
);
// 5. 同步飞奕设备 → region_device_rel 关联表
await
feiyiRegionDeviceRel
();
}
catch
(
err
)
{
console
.
error
(
'[区域集成] 执行异常:'
,
err
);
}
}
// ==================== 区域-设备关联同步 ====================
/**
* 通过空调内机信息反写 region 表的 address 字段(外机地址)
* 逻辑:拉取所有空调内机 → 通过 roomId 匹配 region → 用内机地址前三位反写 region.address
*/
async
function
syncRegionAddressByAcDevices
(
regionModel
:
any
,
allRegions
:
any
[])
{
console
.
log
(
'[同步region.address] 开始...'
);
try
{
// 拉取所有空调内机
const
rows
=
await
fetchAllPages
((
page
,
limit
)
=>
getIndoorUnitList
({
page
,
limit
}));
console
.
log
(
`[同步region.address] 共获取
${
rows
.
length
}
条空调内机`
);
// 构建 roomId → region 映射(使用外部传入的 allRegions)
const
roomIdToRegion
=
new
Map
<
string
,
any
>
();
for
(
const
r
of
allRegions
)
{
if
(
r
.
room_id
)
{
roomIdToRegion
.
set
(
r
.
room_id
,
r
);
}
}
let
updateCount
=
0
;
for
(
const
item
of
rows
)
{
if
(
!
item
.
roomId
||
!
item
.
indoorUnitAddressFull
)
continue
;
const
region
=
roomIdToRegion
.
get
(
item
.
roomId
);
if
(
!
region
)
continue
;
// 用内机四段地址前三位作为外机地址
const
outerAddr
=
getOuterAddress
(
item
.
indoorUnitAddressFull
);
if
(
!
outerAddr
)
continue
;
// address 为空或值不同时才更新
if
(
region
.
address
===
outerAddr
)
continue
;
await
regionModel
.
update
(
{
address
:
outerAddr
},
{
where
:
{
id
:
region
.
id
}
},
);
// 同步更新内存中的值,后续 syncMeterDeviceRegionKey 可直接使用
region
.
address
=
outerAddr
;
updateCount
++
;
}
console
.
log
(
`[同步region.address] 完成,更新
${
updateCount
}
条`
);
return
allRegions
;
// 返回已更新 address 的内存数据
}
catch
(
err
)
{
console
.
error
(
'[同步region.address] 执行异常:'
,
err
);
return
allRegions
;
// 异常时仍返回原始数据,不阻塞后续流程
}
}
/**
* 通过 Excel 映射 + region.address 同步电表设备的 region_key
* 逻辑:Excel(电表设备编号→外机地址) → region.address → region.id → 更新 device.region_key
*/
async
function
syncMeterDeviceRegionKey
(
allRegions
:
any
[])
{
console
.
log
(
'[同步电表region_key] 开始...'
);
try
{
const
deviceModel
=
mysqlModelMap
[
'device'
];
if
(
!
deviceModel
)
{
console
.
error
(
'[同步电表region_key] device 表模型未初始化,跳过'
);
return
;
}
// 1. 读取 Excel 映射:电表设备编号 → 外机地址
const
meterOuterMap
=
loadMeterOuterAddrMap
();
if
(
meterOuterMap
.
size
===
0
)
{
console
.
warn
(
'[同步电表region_key] Excel 映射为空,跳过'
);
return
;
}
// 2. 使用外部传入的 allRegions 构建 address → regionKey 映射(复用已更新 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
);
}
}
console
.
log
(
`[同步电表region_key] region.address 映射共
${
addrToRegionKey
.
size
}
条`
);
// 3. 查询所有电表设备
const
meterDevices
=
await
deviceModel
.
findAll
({
where
:
{
device_type
:
'电能监测'
},
raw
:
true
,
});
console
.
log
(
`[同步电表region_key] 共
${
meterDevices
.
length
}
个电表设备`
);
// 4. 逐一匹配更新
let
updateCount
=
0
;
let
missCount
=
0
;
for
(
const
device
of
meterDevices
as
any
[])
{
const
outerAddr
=
meterOuterMap
.
get
(
device
.
device_id
);
if
(
!
outerAddr
)
{
missCount
++
;
continue
;
}
const
regionKey
=
addrToRegionKey
.
get
(
outerAddr
);
if
(
!
regionKey
)
{
missCount
++
;
continue
;
}
// region_key 已正确则跳过
if
(
device
.
region_key
===
regionKey
)
continue
;
await
deviceModel
.
update
(
{
region_key
:
regionKey
},
{
where
:
{
id
:
device
.
id
}
},
);
updateCount
++
;
}
console
.
log
(
`[同步电表region_key] 完成,更新
${
updateCount
}
条,未匹配
${
missCount
}
条`
);
}
catch
(
err
)
{
console
.
error
(
'[同步电表region_key] 执行异常:'
,
err
);
}
}
// ==================== 设备集成 ====================
/** 空调控制参数模板(按设备数据结构.md) */
const
AC_CONTROL_PARAMS
=
{
power
:
[
"on"
,
"off"
],
mode
:
[
"制冷"
,
"制热"
,
"送风"
,
"除湿"
],
fanSpeed
:
[
"自动"
,
"弱"
,
"强"
,
"超强"
],
setTemp
:
"16~30"
,
roomTemp
:
"-20~50"
,
maxTemp
:
"20~40"
,
minTemp
:
"-10~20"
,
autoControl
:
[
"生效"
,
"解除"
],
};
/** 电表控制参数模板(按设备数据结构.md) */
const
METER_CONTROL_PARAMS
=
{
current
:
"0~9999"
,
voltage
:
"0~500"
,
power
:
"0~99999"
,
energy
:
"0~999999"
,
switch
:
[
"closed"
,
"open"
],
};
/**
* 数据集成-设备集成
* 包含:空调设备集成 + 电表设备集成
*/
export
async
function
device
()
{
console
.
log
(
'[设备集成] 开始执行...'
);
try
{
await
integrateAcDevices
();
await
integrateMeterDevices
();
console
.
log
(
'[设备集成] 完成'
);
}
catch
(
err
)
{
console
.
error
(
'[设备集成] 执行异常:'
,
err
);
}
}
/**
* 空调设备集成
* device_id = indoorUnitAddressFull
* device_ad = indoorUnitId
*/
async
function
integrateAcDevices
()
{
console
.
log
(
'[空调设备集成] 开始...'
);
const
deviceModel
=
mysqlModelMap
[
'device'
];
if
(
!
deviceModel
)
{
console
.
error
(
'[空调设备集成] device 表模型未初始化,跳过'
);
return
;
}
// 查询未处理的故障设备信息
const
faultDevices
=
await
deviceModel
.
findAll
({
where
:
{
device_type
:
'空调'
,
device_state
:
2
},
raw
:
true
,
});
console
.
log
(
`[空调设备集成] 共
${
faultDevices
.
length
}
个故障设备`
);
let
deviceNomalIds
=
[];
// 分页拉取所有内机数据
const
rows
=
await
fetchAllPages
((
page
,
limit
)
=>
getIndoorUnitList
({
page
,
limit
}));
console
.
log
(
`[空调设备集成] 共获取
${
rows
.
length
}
条内机数据`
);
let
insertCount
=
0
;
let
skipCount
=
0
;
for
(
const
item
of
rows
)
{
const
deviceId
=
item
.
indoorUnitAddressFull
;
// device_id 用四段地址
if
(
!
deviceId
)
{
skipCount
++
;
continue
;
}
// 校验是否已存在
const
existing
=
await
deviceModel
.
findOne
({
where
:
{
device_id
:
deviceId
}
});
if
(
existing
)
{
skipCount
++
;
// 更新设备状态
const
newState
=
item
.
state
??
0
;
if
(
existing
.
device_state
!==
newState
)
{
await
deviceModel
.
update
(
{
device_state
:
newState
,
updated_at
:
new
Date
()
},
{
where
:
{
id
:
existing
.
id
}
},
);
}
// 校验故障设备是否已恢复
if
(
faultDevices
.
find
(
d
=>
d
.
device_id
===
deviceId
)
&&
newState
===
0
)
{
console
.
log
(
`[空调设备集成] 故障设备
${
deviceId
}
已恢复`
);
deviceNomalIds
.
push
(
deviceId
);
}
continue
;
}
let
deviceType
=
'空调'
;
let
deviceName
=
'空调内机'
;
// 查找对应的 region_key
let
regionKey
:
string
=
''
;
if
(
item
.
roomId
)
{
const
regionModel
=
mysqlModelMap
[
'region'
];
if
(
regionModel
)
{
const
region
=
await
regionModel
.
findOne
({
where
:
{
room_id
:
item
.
roomId
}
});
if
(
region
)
{
regionKey
=
region
.
region_key
;
// 如果区域地址包含新风,则空调视为新风机
if
(
region
.
name
&&
region
.
name
.
includes
(
'新风'
))
{
deviceType
=
'新风'
;
deviceName
=
'新风机'
;
}
}
}
}
// 插入新设备
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_state
:
item
.
state
??
0
});
insertCount
++
;
}
// 根据deviceNomalIds更新故障信息为正常
if
(
deviceNomalIds
.
length
>
0
)
{
const
faultModel
=
mysqlModelMap
[
'device_fault'
];
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
}
}
},
);
console
.
log
(
`[空调设备集成] 批量解决
${
deviceNomalIds
.
length
}
台设备故障`
);
}
}
console
.
log
(
`[空调设备集成] 完成,新增
${
insertCount
}
条,跳过
${
skipCount
}
条已存在`
);
}
/**
* 电表设备集成
* device_id = gatewayCode
* device_ad = meterId
*/
async
function
integrateMeterDevices
()
{
console
.
log
(
'[电表设备集成] 开始...'
);
const
deviceModel
=
mysqlModelMap
[
'device'
];
if
(
!
deviceModel
)
{
console
.
error
(
'[电表设备集成] device 表模型未初始化,跳过'
);
return
;
}
// 分页拉取所有电表数据
const
rows
=
await
fetchAllPages
((
page
,
limit
)
=>
getElectricMaterList
({
page
,
limit
}));
console
.
log
(
`[电表设备集成] 共获取
${
rows
.
length
}
条电表数据`
);
let
insertCount
=
0
;
let
skipCount
=
0
;
for
(
const
item
of
rows
)
{
const
deviceId
=
item
.
gatewayCode
;
// device_id 用 gatewayCode
if
(
!
deviceId
)
{
skipCount
++
;
continue
;
}
// 校验是否已存在
const
existing
=
await
deviceModel
.
findOne
({
where
:
{
device_id
:
deviceId
}
});
if
(
existing
)
{
skipCount
++
;
continue
;
}
// 插入新设备
await
deviceModel
.
create
({
device_id
:
deviceId
,
device_ad
:
item
.
meterId
||
''
,
region_key
:
''
,
// 电表暂无区域关联,后续由 syncMeterDeviceRegionKey 回填
device_type
:
'电能监测'
,
device_name
:
`电表-
${
item
.
meterComId
||
item
.
meterId
||
''
}
`
,
control_params
:
METER_CONTROL_PARAMS
,
});
insertCount
++
;
}
console
.
log
(
`[电表设备集成] 完成,新增
${
insertCount
}
条,跳过
${
skipCount
}
条已存在`
);
}
// ==================== 设备数据集成 ====================
/** 工作模式映射:1=制冷 2=制热 3=送风 4=除湿 */
const
WORK_MODE_MAP
:
Record
<
number
,
string
>
=
{
1
:
'制冷'
,
2
:
'制热'
,
3
:
'送风'
,
4
:
'除湿'
,
};
/** 风速映射:1=高风 2=中风 4=低风 */
const
FAN_SPEED_MAP
:
Record
<
number
,
string
>
=
{
1
:
'超强'
,
2
:
'强'
,
4
:
'弱'
,
};
/**
* 数据集成-空调内机、电表设备数据集成
*/
export
async
function
deviceData
()
{
console
.
log
(
'[设备数据集成] 开始执行...'
);
try
{
await
integrateAcDeviceData
();
await
integrateMeterDeviceData
();
console
.
log
(
'[设备数据集成] 完成'
);
}
catch
(
err
)
{
console
.
error
(
'[设备数据集成] 执行异常:'
,
err
);
}
}
/**
* 空调设备数据集成(开关机记录)
* 用 indoorUnitAddressFull 匹配 device 表的 device_id
*/
async
function
integrateAcDeviceData
()
{
console
.
log
(
'[空调设备数据集成] 开始...'
);
const
deviceModel
=
mysqlModelMap
[
'device'
];
const
deviceDataModel
=
mysqlModelMap
[
'device_data'
];
if
(
!
deviceModel
||
!
deviceDataModel
)
{
console
.
error
(
'[空调设备数据集成] device 或 device_data 表模型未初始化,跳过'
);
return
;
}
// 1. 分页拉取当天内机开关机记录(使用 occurrenceTime 过滤当天数据)
const
todayDate
=
getTodayDateStr
();
const
rows
=
await
fetchAllPages
(
(
page
,
limit
,
extraParams
)
=>
getIndoorUnitStateList
({
page
,
limit
,
occurrenceTime
:
extraParams
.
occurrenceTime
}),
{
occurrenceTime
:
todayDate
},
);
console
.
log
(
`[空调设备数据集成] 共获取
${
rows
.
length
}
条开关机记录`
);
if
(
rows
.
length
===
0
)
{
console
.
log
(
'[空调设备数据集成] 当天无开关机记录,跳过'
);
return
;
}
// 2. 提取所有涉及的 device_id(去重)
const
deviceIdSet
=
new
Set
<
string
>
();
for
(
const
item
of
rows
)
{
if
(
item
.
indoorUnitAddressFull
)
{
deviceIdSet
.
add
(
item
.
indoorUnitAddressFull
);
}
}
const
deviceIds
=
Array
.
from
(
deviceIdSet
);
// 3. 一次 SQL GROUP BY 查询最大 device_time
const
maxTimeMap
=
await
buildMaxTimeMap
(
deviceDataModel
,
deviceIds
);
// 4. 内存过滤:保留 device_time > 该设备最大时间的记录
const
toInsert
:
any
[]
=
[];
let
skipCount
=
0
;
for
(
const
item
of
rows
)
{
const
deviceId
=
item
.
indoorUnitAddressFull
;
if
(
!
deviceId
||
!
deviceIdSet
.
has
(
deviceId
))
{
skipCount
++
;
continue
;
}
const
itemTime
=
item
.
createTime
?
new
Date
(
item
.
createTime
)
:
null
;
if
(
!
isNewerThan
(
itemTime
,
maxTimeMap
.
get
(
deviceId
)))
{
skipCount
++
;
continue
;
}
// 构造数据(按设备数据结构.md 空调设备数据格式)
const
data
=
{
power
:
item
.
onOff
===
1
?
'on'
:
'off'
,
mode
:
WORK_MODE_MAP
[
item
.
workMode
]
||
'未知'
,
fanSpeed
:
FAN_SPEED_MAP
[
item
.
fanSpeed
]
||
'自动'
,
setTemp
:
item
.
tempSet
||
0
,
roomTemp
:
item
.
roomTemp
||
0
,
maxTemp
:
item
.
tempSetHi
||
28
,
minTemp
:
item
.
tempSetLo
||
16
,
autoControl
:
'生效'
,
};
toInsert
.
push
({
device_id
:
deviceId
,
data
:
data
,
received_time
:
new
Date
(),
device_time
:
itemTime
,
});
}
// TODO: 考虑是否需要在插入前对数据按 device_time 排序,避免数据库存储顺序混乱
// 5. 批量插入
if
(
toInsert
.
length
>
0
)
{
await
deviceDataModel
.
bulkCreate
(
toInsert
);
}
console
.
log
(
`[空调设备数据集成] 完成,新增
${
toInsert
.
length
}
条,跳过
${
skipCount
}
条`
);
}
/**
* 电表设备数据集成(抄表记录)
* 用 gatewayCode 匹配 device 表的 device_id
*/
async
function
integrateMeterDeviceData
()
{
console
.
log
(
'[电表设备数据集成] 开始...'
);
const
deviceModel
=
mysqlModelMap
[
'device'
];
const
deviceDataModel
=
mysqlModelMap
[
'device_data'
];
if
(
!
deviceModel
||
!
deviceDataModel
)
{
console
.
error
(
'[电表设备数据集成] device 或 device_data 表模型未初始化,跳过'
);
return
;
}
// 1. 分页拉取当天抄表记录(使用 occurrenceTime 过滤当天数据)
const
todayDate
=
getTodayDateStr
();
const
rows
=
await
fetchAllPages
(
(
page
,
limit
,
extraParams
)
=>
getMaterStateList
({
page
,
limit
,
occurrenceTime
:
extraParams
.
occurrenceTime
}),
{
occurrenceTime
:
todayDate
},
);
console
.
log
(
`[电表设备数据集成] 共获取
${
rows
.
length
}
条抄表记录`
);
if
(
rows
.
length
===
0
)
{
console
.
log
(
'[电表设备数据集成] 当天无抄表记录,跳过'
);
return
;
}
// 2. 提取所有涉及的 device_id(去重)
const
deviceIdSet
=
new
Set
<
string
>
();
for
(
const
item
of
rows
)
{
if
(
item
.
gatewayCode
)
{
deviceIdSet
.
add
(
item
.
gatewayCode
);
}
}
const
deviceIds
=
Array
.
from
(
deviceIdSet
);
// 3. 一次 SQL GROUP BY 查询最大 device_time
const
maxTimeMap
=
await
buildMaxTimeMap
(
deviceDataModel
,
deviceIds
);
// 4. 内存过滤:保留 device_time > 该设备最大时间的记录
const
toInsert
:
any
[]
=
[];
let
skipCount
=
0
;
for
(
const
item
of
rows
)
{
const
deviceId
=
item
.
gatewayCode
;
if
(
!
deviceId
)
{
skipCount
++
;
continue
;
}
const
itemTime
=
item
.
deviceTime
?
new
Date
(
item
.
deviceTime
)
:
null
;
if
(
!
isNewerThan
(
itemTime
,
maxTimeMap
.
get
(
deviceId
)))
{
skipCount
++
;
continue
;
}
// 构造数据(按设备数据结构.md 电表设备数据格式)
const
data
=
{
current
:
item
.
powerConsumptionAfterTransformationRatio
||
0
,
voltage
:
0
,
power
:
item
.
ammeterReadingAfterTransformationRatio
||
0
,
energy
:
item
.
ammeterReadingAfterTransformationRatio
||
0
,
switch
:
'open'
,
};
toInsert
.
push
({
device_id
:
deviceId
,
data
:
data
,
received_time
:
new
Date
(),
device_time
:
itemTime
,
});
}
// TODO: 考虑是否需要在插入前对数据按 device_time 排序,避免数据库存储顺序混乱
// 5. 批量插入
if
(
toInsert
.
length
>
0
)
{
await
deviceDataModel
.
bulkCreate
(
toInsert
);
}
console
.
log
(
`[电表设备数据集成] 完成,新增
${
toInsert
.
length
}
条,跳过
${
skipCount
}
条`
);
}
// ==================== 暂无对应接口的集成函数(保留空实现) ====================
/**
* 数据集成-九合一环境设备数据集成
*/
export
async
function
airDeviceData
()
{
// 暂无对应接口 -> MQTT
}
/**
* 数据集成-客流监测设备数据集成
*/
export
async
function
customerDeviceData
()
{
// 暂无对应接口 -> MQTT
}
// ==================== 预警工单集成(飞奕故障记录) ====================
/**
* 数据集成-预警工单数据集成
* 从飞奕云平台拉取最近 100 条空调内机故障记录,按 indoorUnitAlarmId 去重后写入 device_fault 表
*/
export
async
function
alertWorkData
()
{
console
.
log
(
'[预警工单集成] 开始...'
);
const
deviceModel
=
mysqlModelMap
[
'device'
];
const
faultModel
=
mysqlModelMap
[
'device_fault'
];
if
(
!
deviceModel
||
!
faultModel
)
{
console
.
error
(
'[预警工单集成] device 或 device_fault 模型未初始化,跳过'
);
return
;
}
try
{
// 1. 拉取最近 100 条故障记录(不再限制当天)
const
result
=
await
getIndoorUnitAlarmErrorHis
({
page
:
1
,
limit
:
100
});
const
rows
:
any
[]
=
result
?.
rows
||
[];
console
.
log
(
`[预警工单集成] 获取
${
rows
.
length
}
条故障记录`
);
if
(
rows
.
length
===
0
)
{
console
.
log
(
'[预警工单集成] 无故障记录,跳过'
);
return
;
}
// 2. 收集所有 indoorUnitAlarmId,批量查已有故障记录(去重)
const
alarmIds
=
rows
.
map
((
r
:
any
)
=>
r
.
indoorUnitAlarmId
).
filter
(
Boolean
);
if
(
alarmIds
.
length
===
0
)
{
console
.
log
(
'[预警工单集成] 所有记录缺少 indoorUnitAlarmId,跳过'
);
return
;
}
const
existingFaults
=
await
faultModel
.
findAll
({
where
:
{
fault_origin_id
:
{
[
Op
.
in
]:
alarmIds
}
},
attributes
:
[
'fault_origin_id'
],
raw
:
true
,
});
const
existingIds
=
new
Set
((
existingFaults
as
any
[]).
map
((
f
:
any
)
=>
f
.
fault_origin_id
));
// 3. 收集 indoorUnitAddressFull,批量查设备确认已注册
const
addrList
=
rows
.
map
((
r
:
any
)
=>
r
.
indoorUnitAddressFull
).
filter
(
Boolean
);
const
devices
=
await
deviceModel
.
findAll
({
where
:
{
device_id
:
addrList
},
attributes
:
[
'device_id'
],
raw
:
true
,
});
const
registeredDeviceIds
=
new
Set
((
devices
as
any
[]).
map
((
d
:
any
)
=>
d
.
device_id
));
// 4. 收集所有故障码,批量查询故障详情(缓存去重)
const
alarmCodes
=
[...
new
Set
(
rows
.
map
((
r
:
any
)
=>
r
.
alarmCode
).
filter
(
Boolean
))]
as
string
[];
const
faultInfoMap
=
new
Map
<
string
,
{
errorInfo
:
string
;
scheme
:
string
|
null
}
>
();
if
(
alarmCodes
.
length
>
0
)
{
console
.
log
(
`[预警工单集成] 查询
${
alarmCodes
.
length
}
个不同故障码的详情...`
);
const
infoResults
=
await
Promise
.
all
(
alarmCodes
.
map
(
async
(
code
)
=>
{
const
info
=
await
getFaultCodeInfo
(
FEIYI_BRAND_CODE
,
code
);
return
{
code
,
info
};
})
);
for
(
const
{
code
,
info
}
of
infoResults
)
{
faultInfoMap
.
set
(
code
,
info
);
}
}
// 5. 过滤 + 组装待插入数据
const
toInsert
:
any
[]
=
[];
let
skipCount
=
0
;
for
(
const
r
of
rows
as
any
[])
{
// 去重:已存在的记录跳过
if
(
!
r
.
indoorUnitAlarmId
||
existingIds
.
has
(
r
.
indoorUnitAlarmId
))
{
skipCount
++
;
continue
;
}
// 设备未注册跳过
const
deviceId
=
r
.
indoorUnitAddressFull
;
if
(
!
deviceId
||
!
registeredDeviceIds
.
has
(
deviceId
))
{
skipCount
++
;
continue
;
}
// 获取故障详情
const
faultInfo
=
faultInfoMap
.
get
(
r
.
alarmCode
);
const
faultFound
=
faultInfo
&&
faultInfo
.
errorInfo
!==
r
.
alarmCode
&&
faultInfo
.
errorInfo
!==
'查询失败'
;
const
errorMsg
=
faultFound
?
faultInfo
.
errorInfo
:
'未找到主内机'
;
toInsert
.
push
({
device_id
:
deviceId
,
fault_type
:
getFaultTypeName
(
r
.
alarmType
),
fault_code
:
r
.
alarmCode
||
''
,
fault_description
:
errorMsg
,
fault_origin_id
:
r
.
indoorUnitAlarmId
,
occurred_time
:
r
.
deviceTime
?
new
Date
(
r
.
deviceTime
)
:
new
Date
(),
level
:
2
,
status
:
0
,
});
}
// 6. 批量写入
if
(
toInsert
.
length
>
0
)
{
await
faultModel
.
bulkCreate
(
toInsert
);
}
console
.
log
(
`[预警工单集成] 完成,新增
${
toInsert
.
length
}
条,跳过
${
skipCount
}
条(去重/未注册)`
);
}
catch
(
err
)
{
console
.
error
(
'[预警工单集成] 执行异常:'
,
err
);
}
}
// ==================== 飞奕:区域设备关联同步 ====================
/**
* 数据集成-飞奕区域设备关联同步
* 从 device 表中读取飞奕设备已绑定的 region_key,同步写入 region_device_rel 表
* 涵盖空调设备和电表设备
* 同步策略:先删后插,删除本批次涉及设备的全部旧关联,再插入新关联
*/
export
async
function
feiyiRegionDeviceRel
()
{
console
.
log
(
'[飞奕区域设备关联] 开始执行...'
);
try
{
const
relModel
=
mysqlModelMap
[
'region_device_rel'
];
const
deviceModel
=
mysqlModelMap
[
'device'
];
if
(
!
relModel
||
!
deviceModel
)
{
console
.
error
(
'[飞奕区域设备关联] region_device_rel 或 device 表模型未初始化,跳过'
);
return
;
}
// 1. 查询所有已绑定 region_key 的飞奕设备(空调 + 电表)
const
devices
=
await
deviceModel
.
findAll
({
where
:
{
device_type
:
[
'空调'
,
'新风'
,
'电能监测'
],
region_key
:
{
[
Op
.
ne
]:
null
},
},
attributes
:
[
'device_id'
,
'region_key'
,
'device_type'
],
raw
:
true
,
})
as
any
[];
// 过滤掉 region_key 为空或为 "0" 的记录(兼容历史数据)
const
validDevices
=
devices
.
filter
(
(
d
:
any
)
=>
d
.
region_key
&&
String
(
d
.
region_key
).
trim
()
!==
''
&&
String
(
d
.
region_key
)
!==
'0'
);
console
.
log
(
`[飞奕区域设备关联] 共
${
devices
.
length
}
个飞奕设备,有效关联
${
validDevices
.
length
}
个`
);
if
(
validDevices
.
length
===
0
)
{
console
.
log
(
'[飞奕区域设备关联] 无有效关联设备,跳过'
);
return
;
}
// 2. 收集涉及的所有 device_id
const
targetDeviceIds
=
validDevices
.
map
((
d
:
any
)
=>
d
.
device_id
);
// 3. 先删:清除这些设备的全部旧关联
const
deleted
=
await
relModel
.
destroy
({
where
:
{
device_id
:
{
[
Op
.
in
]:
targetDeviceIds
}
},
});
console
.
log
(
`[飞奕区域设备关联] 清除旧关联
${
deleted
}
条`
);
// 4. 后插:批量写入新关联
const
newRels
=
validDevices
.
map
((
d
:
any
)
=>
({
region_key
:
String
(
d
.
region_key
),
device_id
:
d
.
device_id
,
relation_type
:
'region_to_device'
,
}));
await
relModel
.
bulkCreate
(
newRels
);
console
.
log
(
`[飞奕区域设备关联] 完成,新关联
${
newRels
.
length
}
条`
);
}
catch
(
err
)
{
console
.
error
(
'[飞奕区域设备关联] 执行异常:'
,
err
);
}
}
// ==================== 迪勤:数据集成 ====================
/**
* 迪勤 reading 数组 → 设备数据 JSON 对象
* 将迪勤的 [{ indicator, value }] 格式转为 { key: value } 的平面对象
* 特殊映射:light → lightLevel(与系统现有字段名对齐)
*/
function
parseDiqinReading
(
reading
:
Array
<
{
indicator
:
string
;
value
:
number
}
>
|
undefined
):
Record
<
string
,
number
>
{
if
(
!
reading
||
!
Array
.
isArray
(
reading
))
return
{};
const
result
:
Record
<
string
,
number
>
=
{};
for
(
const
item
of
reading
)
{
const
key
=
item
.
indicator
===
'light'
?
'lightLevel'
:
item
.
indicator
;
result
[
key
]
=
item
.
value
;
}
return
result
;
}
/**
* 数据集成-迪勤区域集成
* 将迪勤平台的场所(location)同步到 region 表
* region_key = location.id,无父节点
*/
export
async
function
diqinRegion
()
{
console
.
log
(
'[迪勤区域集成] 开始执行...'
);
try
{
const
regionModel
=
mysqlModelMap
[
'region'
];
if
(
!
regionModel
)
{
console
.
error
(
'[迪勤区域集成] region 表模型未初始化,跳过'
);
return
;
}
// 1. 获取所有场所
const
locations
=
await
getLocations
();
if
(
!
locations
||
!
Array
.
isArray
(
locations
))
{
console
.
warn
(
'[迪勤区域集成] 未获取到场所数据'
);
return
;
}
console
.
log
(
`[迪勤区域集成] 共获取
${
locations
.
length
}
个场所`
);
// 2. 逐一 upsert
let
insertCount
=
0
;
let
updateCount
=
0
;
for
(
const
loc
of
locations
)
{
const
regionKey
=
loc
.
id
;
if
(
!
regionKey
)
continue
;
const
existing
=
await
regionModel
.
findOne
({
where
:
{
region_key
:
regionKey
}
});
if
(
existing
)
{
// 更新名称(如有变化)
if
(
existing
.
region_name
!==
loc
.
name
)
{
await
regionModel
.
update
(
{
region_name
:
loc
.
name
,
updated_at
:
new
Date
()
},
{
where
:
{
id
:
existing
.
id
}
},
);
updateCount
++
;
}
}
else
{
await
regionModel
.
create
({
region_key
:
regionKey
,
region_name
:
loc
.
name
,
parent_id
:
null
,
parent_key
:
null
,
region_type
:
null
,
sort_order
:
0
,
});
insertCount
++
;
}
}
console
.
log
(
`[迪勤区域集成] 完成,新增
${
insertCount
}
条,更新
${
updateCount
}
条`
);
}
catch
(
err
)
{
console
.
error
(
'[迪勤区域集成] 执行异常:'
,
err
);
}
}
/**
* 数据集成-迪勤设备集成
* 将迪勤平台的设备(data_source)同步到 device 表
* 仅处理 DIQIN_DEVICE_TYPE_MAP 中配置的型号(目前仅 IEQ)
* ORI PRO / ORI API 跳过
*/
export
async
function
diqinDevice
()
{
console
.
log
(
'[迪勤设备集成] 开始执行...'
);
try
{
const
deviceModel
=
mysqlModelMap
[
'device'
];
if
(
!
deviceModel
)
{
console
.
error
(
'[迪勤设备集成] device 表模型未初始化,跳过'
);
return
;
}
// 1. 获取所有设备
const
dataSources
=
await
getDataSources
();
if
(
!
dataSources
||
!
Array
.
isArray
(
dataSources
))
{
console
.
warn
(
'[迪勤设备集成] 未获取到设备数据'
);
return
;
}
console
.
log
(
`[迪勤设备集成] 共获取
${
dataSources
.
length
}
个设备`
);
// 2. 过滤:仅处理已配置映射的型号
const
validSources
=
dataSources
.
filter
(
(
ds
:
any
)
=>
DIQIN_DEVICE_TYPE_MAP
[
ds
.
data_source_model_name
]
);
const
skippedCount
=
dataSources
.
length
-
validSources
.
length
;
if
(
skippedCount
>
0
)
{
console
.
log
(
`[迪勤设备集成] 跳过
${
skippedCount
}
个非目标型号设备(ORI PRO / ORI API 等)`
);
}
// 3. 逐一 upsert
let
insertCount
=
0
;
let
updateCount
=
0
;
for
(
const
ds
of
validSources
)
{
const
deviceId
=
ds
.
id
;
if
(
!
deviceId
)
continue
;
const
deviceType
=
DIQIN_DEVICE_TYPE_MAP
[
ds
.
data_source_model_name
];
const
deviceName
=
ds
.
name
||
ds
.
identifier
||
deviceId
;
const
existing
=
await
deviceModel
.
findOne
({
where
:
{
device_id
:
deviceId
}
});
if
(
existing
)
{
// 更新名称和类型(如有变化)
let
needUpdate
=
false
;
if
(
existing
.
device_name
!==
deviceName
)
{
existing
.
device_name
=
deviceName
;
needUpdate
=
true
;
}
if
(
existing
.
device_type
!==
deviceType
)
{
existing
.
device_type
=
deviceType
;
needUpdate
=
true
;
}
if
(
needUpdate
)
{
await
deviceModel
.
update
(
{
device_name
:
deviceName
,
device_type
:
deviceType
,
updated_at
:
new
Date
()
},
{
where
:
{
id
:
existing
.
id
}
},
);
updateCount
++
;
}
}
else
{
await
deviceModel
.
create
({
device_id
:
deviceId
,
region_key
:
null
,
device_type
:
deviceType
,
device_name
:
deviceName
,
device_state
:
1
,
// 默认在线
});
insertCount
++
;
}
}
console
.
log
(
`[迪勤设备集成] 完成,新增
${
insertCount
}
条,更新
${
updateCount
}
条`
);
}
catch
(
err
)
{
console
.
error
(
'[迪勤设备集成] 执行异常:'
,
err
);
}
}
/**
* 数据集成-迪勤区域设备关联同步
* 匹配逻辑:station.name 包含 data_source.name → 建立 region_device_rel 关联
* 同步策略:先删后插,删除本批次设备的全部旧关联,再插入新关联
*/
export
async
function
diqinRegionDeviceRel
()
{
console
.
log
(
'[迪勤区域设备关联] 开始执行...'
);
try
{
const
relModel
=
mysqlModelMap
[
'region_device_rel'
];
if
(
!
relModel
)
{
console
.
error
(
'[迪勤区域设备关联] region_device_rel 表模型未初始化,跳过'
);
return
;
}
// 1. 获取所有 IEQ 设备
const
dataSources
=
await
getDataSources
();
if
(
!
dataSources
||
!
Array
.
isArray
(
dataSources
))
return
;
const
ieqSources
=
dataSources
.
filter
(
(
ds
:
any
)
=>
DIQIN_DEVICE_TYPE_MAP
[
ds
.
data_source_model_name
]
);
if
(
ieqSources
.
length
===
0
)
{
console
.
log
(
'[迪勤区域设备关联] 无 IEQ 设备,跳过'
);
return
;
}
console
.
log
(
`[迪勤区域设备关联] IEQ 设备共
${
ieqSources
.
length
}
个`
);
// 2. 获取所有场所及其监测点
// 构建 stationName → locationId 映射
const
locations
=
await
getLocations
();
if
(
!
locations
||
!
Array
.
isArray
(
locations
))
return
;
const
stationLocMap
=
new
Map
<
string
,
string
>
();
// stationName → locationId
for
(
const
loc
of
locations
)
{
try
{
const
detail
=
await
getLocationDetail
(
loc
.
id
);
const
stations
=
detail
?.
stations
||
[];
for
(
const
st
of
stations
)
{
if
(
st
.
name
)
{
stationLocMap
.
set
(
st
.
name
,
loc
.
id
);
}
}
}
catch
(
err
)
{
console
.
warn
(
`[迪勤区域设备关联] 获取场所
${
loc
.
name
}
明细失败:`
,
err
.
message
);
}
}
console
.
log
(
`[迪勤区域设备关联] stationLocMap 共
${
stationLocMap
.
size
}
条`
);
// 3. 匹配:station.name 包含 data_source.name → 建立关联
const
newRels
:
Array
<
{
region_key
:
string
;
device_id
:
string
}
>
=
[];
const
ieqDeviceIds
:
string
[]
=
[];
for
(
const
ds
of
ieqSources
)
{
const
dsName
=
ds
.
name
||
ds
.
identifier
;
if
(
!
dsName
)
continue
;
ieqDeviceIds
.
push
(
ds
.
id
);
// 遍历所有 station,找包含 device name 的
for
(
const
[
stationName
,
locationId
]
of
stationLocMap
)
{
if
(
stationName
.
includes
(
dsName
))
{
newRels
.
push
({
region_key
:
locationId
,
device_id
:
ds
.
id
});
break
;
// 一个设备只匹配第一个命中的 station
}
}
}
console
.
log
(
`[迪勤区域设备关联] 匹配到
${
newRels
.
length
}
条关联`
);
// 4. 先删后插:删除这批 IEQ 设备的全部旧关联
if
(
ieqDeviceIds
.
length
>
0
)
{
const
deleted
=
await
relModel
.
destroy
({
where
:
{
device_id
:
{
[
Op
.
in
]:
ieqDeviceIds
}
},
});
console
.
log
(
`[迪勤区域设备关联] 清除旧关联
${
deleted
}
条`
);
}
// 5. 批量插入新关联
if
(
newRels
.
length
>
0
)
{
await
relModel
.
bulkCreate
(
newRels
.
map
(
r
=>
({
region_key
:
r
.
region_key
,
device_id
:
r
.
device_id
,
relation_type
:
'region_to_device'
,
})),
);
}
console
.
log
(
`[迪勤区域设备关联] 完成,新关联
${
newRels
.
length
}
条`
);
}
catch
(
err
)
{
console
.
error
(
'[迪勤区域设备关联] 执行异常:'
,
err
);
}
}
/**
* 数据集成-迪勤设备数据同步
* 对每个 IEQ 设备,取最新一条 reading 数据写入 device_data 表
* 使用每个设备自身的 reading_time 作为 device_time
* 增量策略:只插入比该设备已有最新 record 更新的数据
*/
export
async
function
diqinDeviceData
()
{
console
.
log
(
'[迪勤设备数据集成] 开始执行...'
);
try
{
const
deviceDataModel
=
mysqlModelMap
[
'device_data'
];
if
(
!
deviceDataModel
)
{
console
.
error
(
'[迪勤设备数据集成] device_data 表模型未初始化,跳过'
);
return
;
}
// 1. 获取所有 IEQ 设备
const
dataSources
=
await
getDataSources
();
if
(
!
dataSources
||
!
Array
.
isArray
(
dataSources
))
return
;
const
ieqSources
=
dataSources
.
filter
(
(
ds
:
any
)
=>
DIQIN_DEVICE_TYPE_MAP
[
ds
.
data_source_model_name
]
);
if
(
ieqSources
.
length
===
0
)
{
console
.
log
(
'[迪勤设备数据集成] 无 IEQ 设备,跳过'
);
return
;
}
// 2. 批量查询这些设备在 device_data 表中的最大 device_time
const
ieqDeviceIds
=
ieqSources
.
map
((
ds
:
any
)
=>
ds
.
id
);
const
maxTimeMap
=
await
buildMaxTimeMap
(
deviceDataModel
,
ieqDeviceIds
);
// 3. 逐个设备获取最新明细并判断增量
const
toInsert
:
any
[]
=
[];
let
skipCount
=
0
;
for
(
const
ds
of
ieqSources
)
{
const
deviceId
=
ds
.
id
;
if
(
!
deviceId
)
continue
;
let
detail
:
any
;
try
{
detail
=
await
getDataSourceDetail
(
deviceId
);
}
catch
(
err
)
{
console
.
warn
(
`[迪勤设备数据集成] 获取设备
${
deviceId
}
明细失败:`
,
err
.
message
);
skipCount
++
;
continue
;
}
if
(
!
detail
||
!
detail
.
reading
||
detail
.
reading
.
length
===
0
)
{
skipCount
++
;
continue
;
}
// reading_time 格式:"2026-07-20T14:05:32.000+08:00"
const
readingTime
=
detail
.
reading_time
?
new
Date
(
detail
.
reading_time
)
:
new
Date
();
if
(
!
isNewerThan
(
readingTime
,
maxTimeMap
.
get
(
deviceId
)))
{
skipCount
++
;
continue
;
}
const
data
=
parseDiqinReading
(
detail
.
reading
);
toInsert
.
push
({
device_id
:
deviceId
,
device_data
:
data
,
device_time
:
readingTime
,
created_at
:
new
Date
(),
updated_at
:
new
Date
(),
});
}
// 4. 批量插入
if
(
toInsert
.
length
>
0
)
{
await
deviceDataModel
.
bulkCreate
(
toInsert
);
}
console
.
log
(
`[迪勤设备数据集成] 完成,新增
${
toInsert
.
length
}
条,跳过
${
skipCount
}
条`
);
}
catch
(
err
)
{
console
.
error
(
'[迪勤设备数据集成] 执行异常:'
,
err
);
}
}
/**
* 数据集成-迪勤设备 region_key 回填
* 通过 region_device_rel 关联表,将匹配到的 region_key 回填到 device 表作为冗余字段
*/
export
async
function
diqinDeviceRegionKey
()
{
console
.
log
(
'[迪勤设备region_key回填] 开始...'
);
try
{
const
deviceModel
=
mysqlModelMap
[
'device'
];
const
relModel
=
mysqlModelMap
[
'region_device_rel'
];
if
(
!
deviceModel
||
!
relModel
)
{
console
.
error
(
'[迪勤设备region_key回填] device 或 region_device_rel 表模型未初始化,跳过'
);
return
;
}
// 1. 查询所有迪勤 IEQ 设备
const
ieqDevices
=
await
deviceModel
.
findAll
({
where
:
{
device_type
:
'IEQ传感器'
},
attributes
:
[
'id'
,
'device_id'
,
'region_key'
],
raw
:
true
,
})
as
any
[];
if
(
ieqDevices
.
length
===
0
)
{
console
.
log
(
'[迪勤设备region_key回填] 无 IEQ 设备,跳过'
);
return
;
}
console
.
log
(
`[迪勤设备region_key回填] IEQ 设备共
${
ieqDevices
.
length
}
个`
);
// 2. 批量查询这些设备在 region_device_rel 中的关联
const
ieqDeviceIds
=
ieqDevices
.
map
((
d
:
any
)
=>
d
.
device_id
);
const
rels
=
await
relModel
.
findAll
({
where
:
{
device_id
:
{
[
Op
.
in
]:
ieqDeviceIds
}
},
attributes
:
[
'device_id'
,
'region_key'
],
raw
:
true
,
})
as
any
[];
// 构建 deviceId → region_key 映射(一个设备可能关联多个区域,取第一个)
const
deviceRegionMap
=
new
Map
<
string
,
string
>
();
for
(
const
rel
of
rels
)
{
if
(
!
deviceRegionMap
.
has
(
rel
.
device_id
)
&&
rel
.
region_key
)
{
deviceRegionMap
.
set
(
rel
.
device_id
,
String
(
rel
.
region_key
));
}
}
console
.
log
(
`[迪勤设备region_key回填] region_device_rel 中匹配到
${
deviceRegionMap
.
size
}
个设备`
);
// 3. 比对并更新 device.region_key
let
updateCount
=
0
;
for
(
const
device
of
ieqDevices
)
{
const
targetKey
=
deviceRegionMap
.
get
(
device
.
device_id
)
||
''
;
if
(
device
.
region_key
===
targetKey
)
continue
;
await
deviceModel
.
update
(
{
region_key
:
targetKey
||
null
,
updated_at
:
new
Date
()
},
{
where
:
{
id
:
device
.
id
}
},
);
updateCount
++
;
}
console
.
log
(
`[迪勤设备region_key回填] 完成,更新
${
updateCount
}
条`
);
}
catch
(
err
)
{
console
.
error
(
'[迪勤设备region_key回填] 执行异常:'
,
err
);
}
}
/**
* 迪勤数据全量同步入口
* 按顺序执行:区域 → 设备 → 区域设备关联 → 设备region_key回填 → 设备数据
*/
export
async
function
diqinSyncAll
()
{
console
.
log
(
'[迪勤全量同步] 开始...'
);
await
diqinRegion
();
await
diqinDevice
();
await
diqinRegionDeviceRel
();
await
diqinDeviceRegionKey
();
await
diqinDeviceData
();
console
.
log
(
'[迪勤全量同步] 完成'
);
}
// ==================== 故障状态自动更新 ====================
/**
* 故障状态自动更新
* 故障驱动模式:拉取所有未处理/处理中的故障,其设备一次拉回故障时间之后的全部开机记录,
* 在内存中比对,状态匹配的批量更新为"已解决"。
* 始终发 2 条 SQL,与故障数无关;不设时间兜底,服务宕机重启后也能补全。
*/
export
async
function
processFaultStatus
()
{
console
.
log
(
'[故障状态更新] 开始...'
);
const
faultModel
=
mysqlModelMap
[
'device_fault'
];
const
deviceDataModel
=
mysqlModelMap
[
'device_data'
];
if
(
!
faultModel
||
!
deviceDataModel
)
{
console
.
error
(
'[故障状态更新] device_fault 或 device_data 模型未初始化,跳过'
);
return
;
}
try
{
// ① 一次查出所有未处理/处理中的故障
const
activeFaults
=
await
faultModel
.
findAll
({
where
:
{
status
:
{
[
Op
.
in
]:
[
0
,
1
]
}
},
attributes
:
[
'id'
,
'device_id'
,
'occurred_time'
],
raw
:
true
,
});
console
.
log
(
`[故障状态更新] 共
${
activeFaults
.
length
}
条活跃故障`
);
if
(
activeFaults
.
length
===
0
)
{
console
.
log
(
'[故障状态更新] 无活跃故障,跳过'
);
return
;
}
// ② 去重 device_id + 建立每个设备的最早故障时间映射
// deviceFaultTimeMap: 每个设备最早的活跃故障发生时间,用于内存中精确过滤
// globalMinOccurredTime: 全局最早故障时间,仅作为 SQL 查询范围下限
const
deviceSet
=
new
Set
<
string
>
();
let
globalMinOccurredTime
:
Date
|
null
=
null
;
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
;
if
(
!
t
)
continue
;
// 全局最早时间(SQL 查询范围)
if
(
!
globalMinOccurredTime
||
t
<
globalMinOccurredTime
)
{
globalMinOccurredTime
=
t
;
}
// 每个设备的最早故障时间(内存过滤用)
const
existing
=
deviceFaultTimeMap
.
get
(
f
.
device_id
);
if
(
!
existing
||
t
<
existing
)
{
deviceFaultTimeMap
.
set
(
f
.
device_id
,
t
);
}
}
const
deviceIds
=
[...
deviceSet
];
// ③ 一次拉回所有相关设备的运行记录(全局最早故障时间之后)
// 不做时间兜底,宕机重启后也能覆盖全部历史
const
deviceDataRecords
=
await
deviceDataModel
.
findAll
({
where
:
{
device_id
:
{
[
Op
.
in
]:
deviceIds
},
...(
globalMinOccurredTime
?
{
device_time
:
{
[
Op
.
gte
]:
globalMinOccurredTime
}
}
:
{}),
},
attributes
:
[
'device_id'
,
'data'
,
'device_time'
],
order
:
[[
'device_id'
,
'ASC'
],
[
'device_time'
,
'ASC'
]],
raw
:
true
,
});
console
.
log
(
`[故障状态更新] 共
${
deviceDataRecords
.
length
}
条相关设备数据记录`
);
// ④ 内存过滤:只保留各设备最早故障时间之后的开机记录(power='on')
// 关键修复:不以全局最早时间过滤,而是用每个设备自己的最早故障时间,
// 避免将故障发生前的开机记录纳入比对,导致永远无法判定"已解决"
const
powerOnTimesMap
=
new
Map
<
string
,
Date
[]
>
();
for
(
const
r
of
deviceDataRecords
as
any
[])
{
let
power
:
string
|
undefined
;
const
rawData
=
r
.
data
;
if
(
typeof
rawData
===
'string'
)
{
try
{
power
=
JSON
.
parse
(
rawData
).
power
;
}
catch
{
continue
;
}
}
else
if
(
rawData
&&
typeof
rawData
===
'object'
)
{
power
=
rawData
.
power
;
}
if
(
power
!==
'on'
)
continue
;
const
dt
=
r
.
device_time
?
new
Date
(
r
.
device_time
)
:
null
;
if
(
!
dt
)
continue
;
// 只保留该设备最早故障时间之后的记录
const
deviceFaultTime
=
deviceFaultTimeMap
.
get
(
r
.
device_id
);
if
(
deviceFaultTime
&&
dt
<=
deviceFaultTime
)
continue
;
if
(
!
powerOnTimesMap
.
has
(
r
.
device_id
))
{
powerOnTimesMap
.
set
(
r
.
device_id
,
[]);
}
powerOnTimesMap
.
get
(
r
.
device_id
)
!
.
push
(
dt
);
}
// ⑤ 逐条故障比对:查找该故障发生时间之后最早的开机记录
// 关键修复:不再拿"全量最早开机时间"与故障时间对比,
// 而是在该设备故障后出现的开机记录中找第一条,确保 isAfter 判定正确
const
toUpdate
:
{
id
:
number
;
resolved_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
;
if
(
!
occurredTime
)
continue
;
// 数据已按 device_time ASC 排序,找第一条在故障时间之后的记录
const
resolvedTime
=
powerOnTimes
.
find
(
dt
=>
dt
>
occurredTime
);
if
(
resolvedTime
)
{
toUpdate
.
push
({
id
:
fault
.
id
,
resolved_time
:
resolvedTime
});
}
}
if
(
toUpdate
.
length
===
0
)
{
console
.
log
(
'[故障状态更新] 无符合条件的故障需更新'
);
return
;
}
// ⑥ 逐条更新 status=2
for
(
const
item
of
toUpdate
)
{
await
faultModel
.
update
(
{
status
:
2
,
resolved_time
:
item
.
resolved_time
,
updated_at
:
new
Date
()
},
{
where
:
{
id
:
item
.
id
}
},
);
}
console
.
log
(
`[故障状态更新] 完成,共处理
${
toUpdate
.
length
}
条故障`
);
}
catch
(
err
)
{
console
.
error
(
'[故障状态更新] 执行异常:'
,
err
);
}
}
src/biz/diqinClient.ts
View file @
8eb38c38
/**
/**
* 迪勤云平台 API 客户端
* 迪勤云平台 API 客户端
* 参考 feiyiClient.ts 的编码风格:async/await、BizError、清晰的注释
*/
*/
import
*
as
crypto
from
'crypto'
;
import
*
as
crypto
from
'crypto'
;
import
{
pos
t
}
from
'../util/request'
;
import
{
ge
t
}
from
'../util/request'
;
import
{
BizError
}
from
'../util/bizError'
;
import
{
BizError
}
from
'../util/bizError'
;
import
{
ERRORENUM
}
from
'../config/errorEnum'
;
import
{
ERRORENUM
}
from
'../config/errorEnum'
;
import
{
DIQIN
}
from
'../config/clientEnum'
;
import
{
systemConfig
}
from
'../config/serverConfig'
;
// 全局变量缓存 token 和过期时间
let
diQinToken
=
""
;
let
expireTime
=
0
;
let
isFetchingToken
=
false
;
let
refreshTokenPromise
:
Promise
<
string
>
|
null
=
null
;
/**
* 获取迪勤云平台配置(从 serverConfig.xml 读取)
*/
function
getDiQinConfig
()
{
const
config
=
systemConfig
.
diqin
;
if
(
!
config
||
!
config
.
appKey
||
!
config
.
secretKey
)
{
throw
new
BizError
(
ERRORENUM
.
参数错误
,
'迪勤云平台配置缺失,请检查 serverConfig.xml 中 <diqin> 节点'
);
}
return
{
baseUrl
:
config
.
baseUrl
||
'https://airiccc.com'
,
appKey
:
config
.
appKey
,
secretKey
:
config
.
secretKey
,
apiVersion
:
'2.0'
,
};
}
/**
* 格式化时间戳(北京时间 yyyy-MM-dd HH:mm:ss)
* TODO: 时区问题暂时忽略,后续需要确保服务器时区为北京时间或做转换
* @param date 日期对象,默认当前时间
*/
function
formatTimestamp
(
date
:
Date
=
new
Date
()):
string
{
const
year
=
date
.
getFullYear
();
const
month
=
String
(
date
.
getMonth
()
+
1
).
padStart
(
2
,
'0'
);
const
day
=
String
(
date
.
getDate
()).
padStart
(
2
,
'0'
);
const
hours
=
String
(
date
.
getHours
()).
padStart
(
2
,
'0'
);
const
minutes
=
String
(
date
.
getMinutes
()).
padStart
(
2
,
'0'
);
const
seconds
=
String
(
date
.
getSeconds
()).
padStart
(
2
,
'0'
);
return
`
${
year
}
-
${
month
}
-
${
day
}
${
hours
}
:
${
minutes
}
:
${
seconds
}
`
;
}
/**
* 生成迪勤签名
* 算法:
* 1. 除 sign 外,按 key 升序排列所有参数
* 2. key+value 纯拼接(无 =、& 分隔符!)
* 3. 末尾追加 secretKey
* 4. MD5 并转大写
* @param params 请求参数对象(不含 sign)
* @param secretKey 密钥
* @returns 大写 MD5 签名字符串
*/
function
generateDiQinSign
(
params
:
Record
<
string
,
any
>
,
secretKey
:
string
):
string
{
// 1. 过滤 sign,按 key 升序排序
const
sortedKeys
=
Object
.
keys
(
params
)
.
filter
(
key
=>
key
!==
'sign'
)
.
sort
();
// 2. key+value 纯拼接
const
signStr
=
sortedKeys
.
map
(
key
=>
`
${
key
}${
params
[
key
]}
`
).
join
(
''
);
// 3. 末尾追加 secretKey
const
stringSignTemp
=
signStr
+
secretKey
;
console
.
log
(
'迪勤签名原始串:'
,
stringSignTemp
);
// 4. MD5 并转大写
return
crypto
.
createHash
(
'md5'
).
update
(
stringSignTemp
).
digest
(
'hex'
).
toUpperCase
();
}
/**
* 获取迪勤云平台 access_token
* 参照飞奕 getFeiYiToken() 的缓存策略:token 有效期内直接返回缓存
* @returns {Promise<string>} access_token
* @throws 如果请求失败或响应码非 10000,则抛出 BizError
*/
export
async
function
getDiQinToken
():
Promise
<
string
>
{
// 校验 token 是否有效,有效则直接返回
const
now
=
Date
.
now
();
if
(
diQinToken
&&
expireTime
>
now
)
{
console
.
log
(
`使用缓存迪勤 token,剩余有效时间:
${(
expireTime
-
now
)
/
1000
}
秒`
);
return
diQinToken
;
}
// 避免并发重复获取 token
if
(
isFetchingToken
&&
refreshTokenPromise
)
{
console
.
log
(
'已有正在进行的 token 刷新请求,等待结果...'
);
return
refreshTokenPromise
;
}
isFetchingToken
=
true
;
refreshTokenPromise
=
(
async
()
=>
{
try
{
const
config
=
getDiQinConfig
();
const
url
=
`
${
config
.
baseUrl
}${
DIQIN
.
获取
Token
}
`
;
// 构建请求参数
const
t
=
formatTimestamp
();
const
queryParams
:
Record
<
string
,
any
>
=
{
app_key
:
config
.
appKey
,
t
:
t
,
v
:
config
.
apiVersion
,
};
// 生成签名
const
sign
=
generateDiQinSign
(
queryParams
,
config
.
secretKey
);
queryParams
.
sign
=
sign
;
console
.
log
(
'获取迪勤 token 请求参数:'
,
queryParams
);
let
result
:
any
;
try
{
result
=
await
get
(
url
,
queryParams
);
}
catch
(
err
)
{
throw
new
BizError
(
ERRORENUM
.
网络错误
,
`获取迪勤 token 网络失败:
${
err
.
message
}
`
);
}
console
.
log
(
'获取迪勤 token 响应:'
,
result
);
// 检查响应:文档中返回 { meta: { code, message }, data: { token, expired_at } }
if
(
!
result
||
!
result
.
meta
||
result
.
meta
.
code
!==
10000
)
{
throw
new
BizError
(
ERRORENUM
.
第三方接口错误
,
`获取迪勤 token 失败:
${
result
?.
meta
?.
message
||
'未知错误'
}
`);
}
const token = result.data?.token;
if (!token) {
throw new BizError(ERRORENUM.第三方接口错误, '迪勤响应中未包含 token');
}
// 更新全局 token 和过期时间
diQinToken = token;
// Token 有效期策略:参考飞奕
if (result.data.expired_at) {
// 有明确过期时间,使用该时间
const expiredAt = new Date(result.data.expired_at).getTime();
// 提前 5 分钟刷新,避免临界过期
expireTime = expiredAt - 5 * 60 * 1000;
} else {
// 未开启有效期(expired_at 为 null),默认 2 小时
expireTime = now + 2 * 60 * 60 * 1000;
}
console.log(`
获取新迪勤
token
:
$
{
token
},
过期时间
:
$
{
new
Date
(
expireTime
).
toISOString
()}
`
);
return
token
;
}
finally
{
isFetchingToken
=
false
;
refreshTokenPromise
=
null
;
}
})();
return
refreshTokenPromise
;
}
/**
* 带 Token 的 GET 请求封装
* 自动获取/刷新 token,自动附加公共参数和签名
* @param path 接口路径(相对路径,如 /v2/locations)
* @param query 额外的查询参数(业务参数)
* @param retry 是否重试(用于 token 过期重试)
*/
async
function
diQinGet
(
path
:
string
,
query
:
Record
<
string
,
any
>
=
{},
retry
:
boolean
=
true
):
Promise
<
any
>
{
const
config
=
getDiQinConfig
();
// 获取 token
const
token
=
await
getDiQinToken
();
const
url
=
`
${
config
.
baseUrl
}${
path
}
`
;
const
headers
=
{
'Authorization'
:
`
${
token
}
`
,
};
// 拼接公共参数
const
t
=
formatTimestamp
();
const
publicParams
:
Record
<
string
,
any
>
=
{
app_key
:
config
.
appKey
,
t
:
t
,
v
:
config
.
apiVersion
,
};
// 合并业务参数
const
allParams
=
{
...
publicParams
,
...
query
};
// 生成签名
const
sign
=
generateDiQinSign
(
allParams
,
config
.
secretKey
);
allParams
.
sign
=
sign
;
console
.
log
(
`请求
${
path
}
参数:`
,
allParams
);
let
result
:
any
;
try
{
result
=
await
get
(
url
,
allParams
,
headers
);
console
.
log
(
`请求
${
path
}
成功,响应:`
,
result
);
}
catch
(
err
)
{
throw
new
BizError
(
ERRORENUM
.
网络错误
,
`请求
${
path
}
失败:
${
err
.
message
}
`
);
}
// 检查响应码
if
(
!
result
||
!
result
.
meta
||
result
.
meta
.
code
!==
10000
)
{
// Token 过期重试一次
const
errCode
=
result
?.
meta
?.
code
;
if
(
retry
&&
(
errCode
===
401
||
errCode
===
10001
||
(
result
?.
meta
?.
message
||
''
).
includes
(
'token'
)))
{
console
.
log
(
'迪勤 token 可能已过期,清除缓存重试...'
);
diQinToken
=
""
;
expireTime
=
0
;
return
diQinGet
(
path
,
query
,
false
);
}
throw
new
BizError
(
ERRORENUM
.
第三方接口错误
,
`迪勤接口
${
path
}
调用失败:
${
result
?.
meta
?.
message
||
JSON
.
stringify
(
result
)}
`);
}
return result;
}
// ======================== 业务接口 ========================
/**
/**
* 获取网关设备列表(分页)
* 获取场所列表
* @returns 场所数据
*/
export async function getLocations(): Promise<any> {
const path = DIQIN.获取区域信息;
const result = await diQinGet(path);
return result.data;
}
/**
* 获取场所明细(含实时数据)
* @param locationId 场所 ID
* @returns 场所明细数据
*/
export async function getLocationDetail(locationId: string): Promise<any> {
if (!locationId) throw new BizError(ERRORENUM.参数错误, '场所 ID 不能为空');
const path = `
$
{
DIQIN
.
获取区域明细信息
}
$
{
locationId
}
`;
const result = await diQinGet(path);
return result.data;
}
/**
* 获取监测点明细
* @param stationId 监测点 ID
* @returns 监测点明细数据
*/
export async function getStationDetail(stationId: string): Promise<any> {
if (!stationId) throw new BizError(ERRORENUM.参数错误, '监测点 ID 不能为空');
const path = `
$
{
DIQIN
.
获取监测点明细
}
$
{
stationId
}
`;
const result = await diQinGet(path);
return result.data;
}
/**
* 获取设备列表
* @returns 设备列表数据
*/
export async function getDataSources(): Promise<any> {
const path = DIQIN.获取设备列表;
const result = await diQinGet(path);
return result.data;
}
/**
* 获取设备明细(含实时数据)
* @param dataSourceId 设备 ID
* @returns 设备明细数据
*/
export async function getDataSourceDetail(dataSourceId: string): Promise<any> {
if (!dataSourceId) throw new BizError(ERRORENUM.参数错误, '设备 ID 不能为空');
const path = `
$
{
DIQIN
.
获取设备明细
}
$
{
dataSourceId
}
`;
const result = await diQinGet(path);
return result.data;
}
/**
* 获取历史数据
* @param params 查询参数,包含:
* - m: 指标类型(如 temperature、humidity、pm25 等)
* - id: 场所/监测点/设备 ID(location_id / station_id / data_source_id)
* - type: ID 类型(location / station / datasource)
* - start: 开始时间
* - end: 结束时间
* @returns 历史数据
*/
export async function getReadings(params: Record<string, any>): Promise<any> {
const path = DIQIN.获取历史数据;
const result = await diQinGet(path, params);
return result.data;
}
/**
* 获取图表数据
* @param params 查询参数,包含:
* - m: 指标类型
* - id: ID
* - indicator: 指标
* - start: 开始时间
* - end: 结束时间
* @returns 图表数据
*/
export async function getGraphs(params: Record<string, any>): Promise<any> {
const path = DIQIN.获取图表数据;
const result = await diQinGet(path, params);
return result.data;
}
/**
* 获取环境设备数据(综合接口,用于调试入口)
* 组合调用 getDataSources + getDataSourceDetail,返回设备列表及其明细数据
* @param page 当前页码,默认 1
* @param page 当前页码,默认 1
* @param limit 每页条数,默认 10
* @param limit 每页条数,默认 10
* @returns
网关
设备数据,包含 rows、total 等
* @returns 设备数据,包含 rows、total 等
*/
*/
export async function processDeviceData(page: number = 1, limit: number = 10): Promise<any> {
export async function processDeviceData(page: number = 1, limit: number = 10): Promise<any> {
const
path
=
''
;
// 获取所有设备列表
const
body
=
{
page
,
limit
}
;
const
dataSources = await getDataSources()
;
const
result
=
{
data
:
{}};
if (!dataSources || !Array.isArray(dataSources)) {
return
result
.
data
;
return { page, limit, total: 0, rows: [] }
;
}
}
export
async
function
getToken
()
{
// 分页
const total = dataSources.length;
const start = (page - 1) * limit;
const end = start + limit;
const pagedItems = dataSources.slice(start, end);
}
// 获取每个设备的明细数据
\ No newline at end of file
const rows = [];
for (const item of pagedItems) {
try {
const detail = await getDataSourceDetail(item.id || item.data_source_id);
rows.push({
...item,
detail: detail,
});
} catch (err) {
console.log(`
获取设备
$
{
item
.
id
||
item
.
data_source_id
}
明细失败
:
`, err.message);
rows.push(item);
}
}
return { page, limit, total, rows };
}
src/biz/feiyiClient.ts
View file @
8eb38c38
...
@@ -4,27 +4,27 @@
...
@@ -4,27 +4,27 @@
*/
*/
import
*
as
crypto
from
'crypto'
;
import
*
as
crypto
from
'crypto'
;
import
{
post
}
from
'../util/request'
;
import
{
get
,
post
}
from
'../util/request'
;
import
{
BizError
}
from
'../util/bizError'
;
import
{
BizError
}
from
'../util/bizError'
;
import
{
ERRORENUM
}
from
'../config/errorEnum'
;
import
{
ERRORENUM
}
from
'../config/errorEnum'
;
import
{
FEIYI
}
from
'../config/clientEnum'
;
import
{
FEIYI
}
from
'../config/clientEnum'
;
import
{
systemConfig
}
from
'../config/serverConfig'
;
// 全局变量缓存 token 和过期时间
// 全局变量缓存 token 和过期时间
let
feiYiToken
=
""
;
let
feiYiToken
=
""
;
let
expireTime
=
0
;
let
expireTime
=
0
;
let
getTokenTime
=
0
;
let
isFetchingToken
=
false
;
// 避免并发获取 token 的标志位
let
refreshTokenPromise
:
Promise
<
string
>
|
null
=
null
;
// 用于避免并发刷新 token 的 Promise
/**
/**
*
飞奕云平台配置(建议从环境变量或配置文件中读取
)
*
获取飞奕云平台配置(从 serverConfig.xml 读取,兜底使用环境变量或默认值
)
*/
*/
const
FEIYI_CONFIG
=
{
function
getFeiYiConfig
()
{
baseUrl
:
'https://m.achelp.cn/open'
,
const
config
=
systemConfig
.
feiyi
;
clientId
:
process
.
env
.
FEIYI_CLIENT_ID
||
'419636996764598272'
,
// 我方提供的 clientId
return
{
secretKey
:
process
.
env
.
FEIYI_SECRET_KEY
||
'02fc75446b4544b3a02444a2b5f9be2b'
,
// 我方提供的 appSecret
baseUrl
:
(
config
&&
config
.
baseUrl
)
||
process
.
env
.
FEIYI_BASE_URL
||
'https://m.achelp.cn/open'
,
};
clientId
:
(
config
&&
config
.
appKey
)
||
process
.
env
.
FEIYI_CLIENT_ID
||
'419636996764598272'
,
secretKey
:
(
config
&&
config
.
secretKey
)
||
process
.
env
.
FEIYI_SECRET_KEY
||
'02fc75446b4544b3a02444a2b5f9be2b'
,
};
}
/**
/**
* 生成随机字符串 nonce
* 生成随机字符串 nonce
...
@@ -70,7 +70,8 @@ function generateSign(params: Record<string, any>, timestamp: string, nonce: str
...
@@ -70,7 +70,8 @@ function generateSign(params: Record<string, any>, timestamp: string, nonce: str
* @throws 如果请求失败或响应码非 00000,则抛出 BizError
* @throws 如果请求失败或响应码非 00000,则抛出 BizError
*/
*/
export
async
function
getFeiYiToken
():
Promise
<
string
>
{
export
async
function
getFeiYiToken
():
Promise
<
string
>
{
const
url
=
`
${
FEIYI_CONFIG
.
baseUrl
}${
FEIYI
.
获取
Token
}
`
;
const
feiyiConfig
=
getFeiYiConfig
();
const
url
=
`
${
feiyiConfig
.
baseUrl
}${
FEIYI
.
获取
Token
}
`
;
// 校验token是否有效,有效则直接返回
// 校验token是否有效,有效则直接返回
const
now
=
Date
.
now
();
const
now
=
Date
.
now
();
...
@@ -82,12 +83,12 @@ export async function getFeiYiToken(): Promise<string> {
...
@@ -82,12 +83,12 @@ export async function getFeiYiToken(): Promise<string> {
// 根据文档,获取 token 的请求参数
// 根据文档,获取 token 的请求参数
const
params
=
{
const
params
=
{
AccessKey
:
''
,
// 文档要求传空串
AccessKey
:
''
,
// 文档要求传空串
clientId
:
FEIYI_CONFIG
.
clientId
clientId
:
feiyiConfig
.
clientId
};
};
// 生成签名
// 生成签名
const
timestamp
=
Date
.
now
().
toString
();
// 毫秒时间戳
const
timestamp
=
Date
.
now
().
toString
();
// 毫秒时间戳
const
nonce
=
generateNonce
();
const
nonce
=
generateNonce
();
const
sign
=
generateSign
(
params
,
timestamp
,
nonce
,
FEIYI_CONFIG
.
secretKey
);
const
sign
=
generateSign
(
params
,
timestamp
,
nonce
,
feiyiConfig
.
secretKey
);
console
.
log
(
'MD5加密签名:'
,
sign
);
console
.
log
(
'MD5加密签名:'
,
sign
);
const
requestBody
=
{
const
requestBody
=
{
...
@@ -116,7 +117,6 @@ export async function getFeiYiToken(): Promise<string> {
...
@@ -116,7 +117,6 @@ export async function getFeiYiToken(): Promise<string> {
} else {
} else {
// 更新全局 token 和过期时间
// 更新全局 token 和过期时间
feiYiToken = accessToken;
feiYiToken = accessToken;
getTokenTime = now;
// 默认 2分钟 过期,如果响应中包含 expires_in 字段,则使用该值(单位毫秒)
// 默认 2分钟 过期,如果响应中包含 expires_in 字段,则使用该值(单位毫秒)
expireTime = now + (result.data.expires_in ? result.data.expires_in : 2 * 60 * 1000);
expireTime = now + (result.data.expires_in ? result.data.expires_in : 2 * 60 * 1000);
console.log(`
获取新
token
:
$
{
accessToken
}
, 过期时间:
${
new
Date
(
expireTime
).
toISOString
()}
`
);
console.log(`
获取新
token
:
$
{
accessToken
}
, 过期时间:
${
new
Date
(
expireTime
).
toISOString
()}
`
);
...
@@ -135,7 +135,8 @@ async function feiYiPost(path: string, body: any, retry: boolean = true): Promis
...
@@ -135,7 +135,8 @@ async function feiYiPost(path: string, body: any, retry: boolean = true): Promis
// 获取 token
// 获取 token
const
token
=
await
getFeiYiToken
();
const
token
=
await
getFeiYiToken
();
const
url
=
`
${
FEIYI_CONFIG
.
baseUrl
}${
path
}
`
;
const
feiyiConfig
=
getFeiYiConfig
();
const
url
=
`
${
feiyiConfig
.
baseUrl
}${
path
}
`
;
const
headers
=
{
const
headers
=
{
'Authorization'
:
`Bearer
${
token
}
`
,
'Authorization'
:
`Bearer
${
token
}
`
,
'Content-Type'
:
'application/json'
,
'Content-Type'
:
'application/json'
,
...
@@ -380,3 +381,79 @@ export async function getIndoorUnitStateList(params: any): Promise<any> {
...
@@ -380,3 +381,79 @@ export async function getIndoorUnitStateList(params: any): Promise<any> {
const
result
=
await
feiYiPost
(
path
,
body
);
const
result
=
await
feiYiPost
(
path
,
body
);
return
result
.
data
;
return
result
.
data
;
}
}
/**
* 空调内机故障记录分页查询
* @param params { page, limit, roomIds?, indoorUnitAddressFull?, alarmType?, occurrenceTime? }
* @returns data 部分,包含 page, limit, total, rows
*/
export
async
function
getIndoorUnitAlarmErrorHis
(
params
:
any
):
Promise
<
any
>
{
const
path
=
'/openApi/indoorUnit/alarm/errorHis'
;
const
body
=
{
...
params
};
const
result
=
await
feiYiPost
(
path
,
body
);
return
result
.
data
;
}
/**
* 故障码查询故障详情
* 根据品牌编码和错误编码查询故障描述和解决方案
* 接口文档:空调故障码对接服务与解析流程.pdf
* @param brandCode 品牌编码,如 "1"=日立
* @param errorCode 错误编码,如 "LOST"
* @returns { errorInfo: string, scheme: string | null } 故障信息描述、解决方案
*/
export
async
function
getFaultCodeInfo
(
brandCode
:
string
,
errorCode
:
string
):
Promise
<
{
errorInfo
:
string
;
scheme
:
string
|
null
}
>
{
const
url
=
'https://errorcode.feiyikj.cn/prod-api/feiyi/app/errorCode/info'
;
const
query
=
{
brandCode
,
errorCode
};
let
result
:
any
;
try
{
result
=
await
get
(
url
,
query
);
}
catch
(
err
)
{
console
.
error
(
`[故障码查询] 请求失败 brandCode=
${
brandCode
}
errorCode=
${
errorCode
}
:`
,
err
);
return
{
errorInfo
:
'查询失败'
,
scheme
:
null
};
}
if
(
!
result
||
result
.
code
!==
200
)
{
console
.
error
(
`[故障码查询] 接口返回异常 brandCode=
${
brandCode
}
errorCode=
${
errorCode
}
:`
,
result
);
return
{
errorInfo
:
'查询失败'
,
scheme
:
null
};
}
const
data
=
result
.
data
||
[];
if
(
data
.
length
===
0
)
{
console
.
warn
(
`[故障码查询] 未查到故障码信息 brandCode=
${
brandCode
}
errorCode=
${
errorCode
}
`
);
return
{
errorInfo
:
errorCode
,
scheme
:
null
};
}
return
{
errorInfo
:
data
[
0
].
errorInfo
||
errorCode
,
scheme
:
data
[
0
].
scheme
||
null
,
};
}
/**
* 故障类型映射(中文 -> 数字)
*/
let
faultTypeMap
:
{
[
key
:
string
]:
number
}
=
{
"内机故障"
:
5
,
"未绑定房间"
:
15
};
/** 数字 -> 中文的反向映射 */
let
faultTypeReverseMap
:
Map
<
number
,
string
>
|
null
=
null
;
/** 构建数字->中文的反向映射 */
function
getFaultTypeReverseMap
():
Map
<
number
,
string
>
{
if
(
faultTypeReverseMap
)
return
faultTypeReverseMap
;
faultTypeReverseMap
=
new
Map
();
for
(
const
[
cn
,
num
]
of
Object
.
entries
(
faultTypeMap
))
{
faultTypeReverseMap
.
set
(
num
,
cn
);
}
return
faultTypeReverseMap
;
}
/**
* 根据 alarmType 数字获取中文故障类型名称
* @param alarmType 故障类型数字
* @returns 中文名称,未匹配时返回原数字字符串
*/
export
function
getFaultTypeName
(
alarmType
:
number
|
string
):
string
{
const
num
=
typeof
alarmType
===
'string'
?
parseInt
(
alarmType
,
10
)
:
alarmType
;
return
getFaultTypeReverseMap
().
get
(
num
)
||
String
(
alarmType
);
}
src/biz/region.ts
View file @
8eb38c38
...
@@ -46,6 +46,72 @@ export async function readRegionAxis(gatewayPage: number) {
...
@@ -46,6 +46,72 @@ export async function readRegionAxis(gatewayPage: number) {
return
result
;
return
result
;
}
}
/**
* 动态查询:根据 regionName→regionKey 映射,从数据库查询各区域下的设备及最新功率状态
* @returns 格式与 mock 数据一致: { regionName: [{deviceId, deviceType, power}, ...] }
*/
async
function
buildDeviceMapDynamic
(
regionMap
:
{
[
regionName
:
string
]:
string
}):
Promise
<
{
[
regionName
:
string
]:
any
[]
}
>
{
const
deviceMap
:
{
[
regionName
:
string
]:
any
[]
}
=
{};
// 1. 收集所有 region_key
const
regionKeys
=
Object
.
values
(
regionMap
)
as
string
[];
if
(
regionKeys
.
length
===
0
)
return
deviceMap
;
// 2. 反向映射:regionKey → regionName
const
regionKeyToName
:
{
[
key
:
string
]:
string
}
=
{};
for
(
const
name
in
regionMap
)
{
regionKeyToName
[
regionMap
[
name
]]
=
name
;
}
// 3. 批量查询这些区域下所有的设备
const
deviceResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
region_key
:
{
'%in%'
:
regionKeys
}
},
[
'device_id'
,
'region_key'
,
'device_type'
],
);
const
devices
=
(
deviceResult
.
data
||
[])
as
any
[];
// 4. 批量查询这些设备的最新一条 device_data,提取 power 状态
const
deviceIds
=
devices
.
map
((
d
)
=>
d
.
device_id
).
filter
(
Boolean
);
const
powerMap
=
new
Map
<
string
,
string
>
();
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
);
const
parsed
=
typeof
r
.
device_data
===
'string'
?
JSON
.
parse
(
r
.
device_data
)
:
r
.
device_data
;
powerMap
.
set
(
r
.
device_id
,
parsed
?.
power
||
'on'
);
}
});
}
// 5. 组装 deviceMap(按 regionName 分组,格式对齐 mock 数据)
for
(
const
device
of
devices
)
{
const
regionName
=
regionKeyToName
[
device
.
region_key
];
if
(
!
regionName
)
continue
;
if
(
!
deviceMap
[
regionName
])
{
deviceMap
[
regionName
]
=
[];
}
deviceMap
[
regionName
].
push
({
deviceId
:
device
.
device_id
,
deviceType
:
device
.
device_type
,
power
:
powerMap
.
get
(
device
.
device_id
)
||
'on'
,
});
}
return
deviceMap
;
}
async
function
readSheet
(
sheet
:
any
[][],
gatewayPage
:
number
)
{
async
function
readSheet
(
sheet
:
any
[][],
gatewayPage
:
number
)
{
// 取出所有区域名称,查询区域表获取区域数据
// 取出所有区域名称,查询区域表获取区域数据
let
regionList
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{});
let
regionList
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{});
...
@@ -59,40 +125,44 @@ async function readSheet(sheet: any[][], gatewayPage: number) {
...
@@ -59,40 +125,44 @@ async function readSheet(sheet: any[][], gatewayPage: number) {
// "前台": "0001",
// "前台": "0001",
// "卫生间": "3000"
// "卫生间": "3000"
// }
// }
let
regionMap
=
regionList
.
data
.
map
((
bean
)
=>
{
let
regionMap
:
any
=
{};
regionList
.
data
.
map
((
bean
)
=>
{
regionMap
[
bean
.
regionName
]
=
bean
.
regionKey
regionMap
[
bean
.
regionName
]
=
bean
.
regionKey
});
});
// 根据区域信息查询所有环境设备,通过环境设备查询最新环境指标
// 根据区域信息查询所有环境设备,通过环境设备查询最新环境指标
let
deviceMap
=
{
// ====== MOCK 数据(注释保留作为格式参考) ======
"会议室"
:
[
// let deviceMap = {
{
"deviceId"
:
"AC1"
,
"deviceType"
:
"空调"
,
"power"
:
"off"
},
// "会议室": [
{
"deviceId"
:
"Z1"
,
"deviceType"
:
"照明"
,
"power"
:
"on"
}
// { "deviceId": "AC1", "deviceType": "空调", "power": "off" },
],
// { "deviceId": "Z1", "deviceType": "照明", "power": "on" }
"公共办公区"
:
[
// ],
{
"deviceId"
:
"AC2"
,
"deviceType"
:
"空调"
,
"power"
:
"on"
},
// "公共办公区": [
{
"deviceId"
:
"Z12"
,
"deviceType"
:
"照明"
,
"power"
:
"on"
},
// { "deviceId": "AC2", "deviceType": "空调", "power": "on" },
{
"deviceId"
:
"F1"
,
"deviceType"
:
"排风"
,
"power"
:
"on"
},
// { "deviceId": "Z12", "deviceType": "照明", "power": "on" },
{
"deviceId"
:
"E1"
,
"deviceType"
:
"IEQ传感器"
,
"power"
:
"on"
},
// { "deviceId": "F1", "deviceType": "排风", "power": "on" },
{
"deviceId"
:
"Y1"
,
"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": "AC3", "deviceType": "空调", "power": "on" },
{
"deviceId"
:
"F2"
,
"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": "AC11", "deviceType": "空调", "power": "off" }],
"传感器生产间"
:
[{
"deviceId"
:
"AC13"
,
"deviceType"
:
"空调"
,
"power"
:
"off"
}],
// "CEO办公室": [{ "deviceId": "AC12", "deviceType": "空调", "power": "off" }],
"卫生间"
:
[
// "传感器生产间": [{ "deviceId": "AC13", "deviceType": "空调", "power": "off" }],
{
"deviceId"
:
"S1"
,
"deviceType"
:
"摄像头"
,
"power"
:
"on"
},
// "卫生间": [
{
"deviceId"
:
"E100"
,
"deviceType"
:
"IEQ传感器"
,
"power"
:
"on"
},
// { "deviceId": "S1", "deviceType": "摄像头", "power": "on" },
{
"deviceId"
:
"X1"
,
"deviceType"
:
"音响"
,
"power"
:
"on"
}
// { "deviceId": "E100", "deviceType": "IEQ传感器", "power": "on" },
],
// { "deviceId": "X1", "deviceType": "音响", "power": "on" }
"前台"
:
[
// ],
{
"deviceId"
:
"S0"
,
"deviceType"
:
"摄像头"
,
"power"
:
"on"
},
// "前台": [
{
"deviceId"
:
"T1"
,
"deviceType"
:
"人流监测"
,
"power"
:
"on"
}
// { "deviceId": "S0", "deviceType": "摄像头", "power": "on" },
]
// { "deviceId": "T1", "deviceType": "人流监测", "power": "on" }
}
// ]
// }
// ====== 动态查询:从数据库获取真实设备及最新状态 ======
const
deviceMap
=
await
buildDeviceMapDynamic
(
regionMap
);
// 循环区域,根据各区域环境指标计算各区域的环境质量得出优良中差四个值
// 循环区域,根据各区域环境指标计算各区域的环境质量得出优良中差四个值
let
sheetData
=
await
planaryArrayBecomeOfBlockData
(
sheet
);
let
sheetData
=
await
planaryArrayBecomeOfBlockData
(
sheet
);
let
result
=
[];
let
result
=
[];
...
...
src/biz/running.ts
View file @
8eb38c38
...
@@ -2,23 +2,393 @@
...
@@ -2,23 +2,393 @@
* 运行分析页面
* 运行分析页面
*/
*/
import
*
as
crypto
from
'crypto'
;
import
{
post
}
from
'../util/request'
;
import
{
BizError
}
from
'../util/bizError'
;
import
{
BizError
}
from
'../util/bizError'
;
import
{
ERRORENUM
}
from
'../config/errorEnum'
;
import
{
ERRORENUM
}
from
'../config/errorEnum'
;
import
{
max
}
from
'moment'
;
import
{
title
}
from
'process'
;
import
{
TABLENAME
}
from
'../config/dbEnum'
;
import
{
TABLENAME
}
from
'../config/dbEnum'
;
import
{
selectDataListByParam
,
selectOneDataByParam
}
from
'../data/findData'
;
import
{
selectDataListByParam
,
selectOneDataByParam
,
selectDataCountByParam
}
from
'../data/findData'
;
import
{
controlIndoorUnit
}
from
'./feiyiClient'
;
import
{
controlIndoorUnit
}
from
'./feiyiClient'
;
import
{
formatTime
,
getTodayStart
,
getTodayEnd
,
getYesterdayStart
,
getYesterdayEnd
,
getMonthStart
,
getLastMonthStart
,
getLastMonthEnd
,
getHoursAgo
,
getDaysAgoStart
,
getMonthsAgoStart
,
calcCompareRate
,
formatHourKey
,
formatDayKey
,
formatMonthKey
,
}
from
'../util/dateUtils'
;
// ==================== 常量定义 ====================
const
deviceTypeList
:
string
[]
=
[
"IEQ传感器"
,
"烟感器"
,
"摄像头"
,
"空调"
,
"音响"
,
"照明"
,
"等离子"
,
"人流监测"
,
"电表"
,
"排风"
,
];
/**
* 空调中文到Key的映射
* 1 制冷 2 制热 3 送风 4 除湿
* 0 关-off 1 开-on
* 1 高风-超强 2 中风-强 4 低风-弱
* 0 不锁定-生效 1 锁定-解除
*/
const
acDeviceKeyMap
:
{
[
key
:
string
]:
number
}
=
{
"制冷"
:
1
,
"制热"
:
2
,
"送风"
:
3
,
"除湿"
:
4
,
"on"
:
1
,
"off"
:
0
,
"超强"
:
1
,
"强"
:
2
,
"弱"
:
4
,
"解除"
:
1
,
"生效"
:
0
,
};
// ==================== 质量判定函数(从 device.ts 移植,适配新系统) ====================
/** 温度质量等级(夏/冬季模式) */
function
getTemperatureQuality
(
value
:
number
):
string
{
const
month
=
new
Date
().
getMonth
()
+
1
;
const
isWinter
=
month
===
12
||
month
<=
2
;
if
(
isWinter
)
{
if
(
value
===
20
)
return
'优'
;
if
(
value
>=
18
&&
value
<=
21
)
return
'良'
;
if
(
value
>=
14
&&
value
<=
27
)
return
'中'
;
return
'差'
;
}
else
{
if
(
value
===
24
)
return
'优'
;
if
(
value
>=
22
&&
value
<=
26
)
return
'良'
;
if
(
value
>=
18
&&
value
<=
32
)
return
'中'
;
return
'差'
;
}
}
const
QUALITY_THRESHOLDS
:
{
[
key
:
string
]:
{
you
:
[
number
,
number
];
liang
:
[
number
,
number
];
zhong
:
[
number
,
number
]
}
}
=
{
co2
:
{
you
:
[
0
,
800
],
liang
:
[
800
,
1000
],
zhong
:
[
1000
,
1500
]
},
pm10
:
{
you
:
[
0
,
50
],
liang
:
[
50
,
150
],
zhong
:
[
150
,
250
]
},
pm2_5
:
{
you
:
[
0
,
35
],
liang
:
[
35
,
75
],
zhong
:
[
75
,
150
]
},
hcho
:
{
you
:
[
0
,
0.03
],
liang
:
[
0.03
,
0.08
],
zhong
:
[
0.08
,
0.10
]
},
tvoc
:
{
you
:
[
0
,
0.3
],
liang
:
[
0.3
,
0.6
],
zhong
:
[
0.6
,
1.0
]
},
o3
:
{
you
:
[
0
,
0.05
],
liang
:
[
0.05
,
0.1
],
zhong
:
[
0.1
,
0.16
]
},
light_level
:
{
you
:
[
300
,
500
],
liang
:
[
100
,
300
],
zhong
:
[
50
,
100
]
},
};
/** 质量判定 */
function
getQualityLevel
(
key
:
string
,
value
:
number
):
string
|
null
{
if
(
value
===
null
||
value
===
undefined
)
return
null
;
if
(
key
===
'temperature'
)
return
getTemperatureQuality
(
value
);
if
(
key
===
'humidity'
)
{
if
(
value
>=
40
&&
value
<=
60
)
return
'优'
;
if
((
value
>=
30
&&
value
<
40
)
||
(
value
>
60
&&
value
<=
70
))
return
'良'
;
if
((
value
>=
20
&&
value
<
30
)
||
(
value
>
70
&&
value
<=
80
))
return
'中'
;
return
'差'
;
}
if
(
key
===
'pressure'
)
{
if
(
value
>=
1010
&&
value
<=
1020
)
return
'优'
;
if
((
value
>=
1000
&&
value
<
1010
)
||
(
value
>
1020
&&
value
<=
1030
))
return
'良'
;
if
((
value
>=
990
&&
value
<
1000
)
||
(
value
>
1030
&&
value
<=
1040
))
return
'中'
;
return
'差'
;
}
const
t
=
QUALITY_THRESHOLDS
[
key
];
if
(
!
t
)
return
null
;
if
(
value
>=
t
.
you
[
0
]
&&
value
<=
t
.
you
[
1
])
return
'优'
;
if
(
value
>=
t
.
liang
[
0
]
&&
value
<=
t
.
liang
[
1
])
return
'良'
;
if
(
value
>=
t
.
zhong
[
0
]
&&
value
<=
t
.
zhong
[
1
])
return
'中'
;
return
'差'
;
}
/**
* 获取综合环境质量等级字符串(优秀/良好/一般/差)
* 基于 CO2 + PM2.5 的判定逻辑
*/
function
getOverallQuality
(
co2
:
number
,
pm25
:
number
):
string
{
if
(
co2
<
800
&&
pm25
<=
35
)
return
'优秀'
;
if
(
co2
<
1000
&&
pm25
<=
45
)
return
'优秀'
;
if
(
co2
<=
1000
&&
pm25
<=
50
)
return
'良好'
;
if
(
co2
>
1000
&&
co2
<
1500
&&
pm25
>
50
&&
pm25
<
75
)
return
'一般'
;
if
(
co2
>=
1500
||
pm25
>=
75
)
return
'差'
;
// 其余情况粗暴判定
if
(
co2
>
1000
||
pm25
>
50
)
return
'一般'
;
return
'良好'
;
}
// ==================== 环境监测相关辅助 ====================
/** 环境指标 key 映射:device_data JSON 中的 key → 展示用的 key */
const
ENV_INDICATOR_KEYS
=
[
'temperature'
,
'humidity'
,
'pm25'
,
'co2'
,
'hcho'
,
'lightLevel'
,
'pm10'
,
'pressure'
,
'tvoc'
];
/**
* 查询所有 "空气质量监测" 类型设备的最新一条 device_data
* @returns Map<device_id, { data: JSON, device_time: string }>
*/
async
function
getLatestEnvDeviceData
():
Promise
<
Map
<
string
,
any
>>
{
const
deviceResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_type
:
'IEQ传感器'
},
[
'device_id'
,
'device_name'
,
'region_key'
]
);
const
devices
=
deviceResult
.
data
||
[];
if
(
devices
.
length
===
0
)
return
new
Map
();
const
deviceIds
=
devices
.
map
((
d
:
any
)
=>
d
.
device_id
);
// 获取今天所有数据,按时间降序,每个设备取第一条
const
todayStart
=
getTodayStart
();
const
dataResult
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
'%in%'
:
deviceIds
},
device_time
:
{
'%gte%'
:
todayStart
},
'%orderDesc%'
:
'device_time'
,
},
[
'device_id'
,
'device_data'
,
'device_time'
]
);
const
records
=
dataResult
.
data
||
[];
const
latestMap
=
new
Map
<
string
,
any
>
();
// 因为已按时间降序,每个 device_id 第一条就是最新
records
.
forEach
((
r
:
any
)
=>
{
if
(
!
latestMap
.
has
(
r
.
device_id
))
{
const
parsed
=
parseDeviceData
(
r
.
device_data
);
latestMap
.
set
(
r
.
device_id
,
{
data
:
parsed
,
device_time
:
r
.
device_time
});
}
});
return
latestMap
;
}
/**
* 查询指定时间范围内 "空气质量监测" 设备的 device_data,按小时聚合取均值
* @returns Map<hourKey, Map<indicatorKey, avgValue>>
*/
async
function
getHourlyEnvData
(
startTime
:
string
,
endTime
:
string
):
Promise
<
Map
<
string
,
Map
<
string
,
number
>>>
{
const
deviceResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_type
:
'IEQ传感器'
},
[
'device_id'
]
);
const
deviceIds
=
(
deviceResult
.
data
||
[]).
map
((
d
:
any
)
=>
d
.
device_id
);
if
(
deviceIds
.
length
===
0
)
return
new
Map
();
const
dataResult
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
'%in%'
:
deviceIds
},
device_time
:
{
'%gte%'
:
startTime
,
'%lte%'
:
endTime
},
'%orderAsc%'
:
'device_time'
,
},
[
'device_data'
,
'device_time'
]
);
const
records
=
dataResult
.
data
||
[];
const
hourlyMap
=
new
Map
<
string
,
Map
<
string
,
{
sum
:
number
;
count
:
number
}
>>
();
records
.
forEach
((
r
:
any
)
=>
{
const
t
=
new
Date
(
r
.
device_time
);
const
hourKey
=
`
${
String
(
t
.
getHours
()).
padStart
(
2
,
'0'
)}
:00`
;
const
parsed
=
parseDeviceData
(
r
.
device_data
);
if
(
!
hourlyMap
.
has
(
hourKey
))
{
hourlyMap
.
set
(
hourKey
,
new
Map
());
}
const
hourData
=
hourlyMap
.
get
(
hourKey
)
!
;
ENV_INDICATOR_KEYS
.
forEach
(
key
=>
{
const
val
=
parsed
[
key
];
if
(
val
!==
undefined
&&
val
!==
null
&&
typeof
val
===
'number'
)
{
if
(
!
hourData
.
has
(
key
))
hourData
.
set
(
key
,
{
sum
:
0
,
count
:
0
});
const
acc
=
hourData
.
get
(
key
)
!
;
acc
.
sum
+=
val
;
acc
.
count
+=
1
;
}
});
});
// 转换为均值
const
result
=
new
Map
<
string
,
Map
<
string
,
number
>>
();
hourlyMap
.
forEach
((
indicatorMap
,
hourKey
)
=>
{
const
avgMap
=
new
Map
<
string
,
number
>
();
indicatorMap
.
forEach
((
acc
,
key
)
=>
{
avgMap
.
set
(
key
,
Math
.
round
((
acc
.
sum
/
acc
.
count
)
*
100
)
/
100
);
});
result
.
set
(
hourKey
,
avgMap
);
});
return
result
;
}
// ==================== 辅助:查询工具函数 ====================
/** 根据 regionKey 查询区域名称 */
async
function
getRegionName
(
regionKey
:
string
):
Promise
<
string
>
{
const
regionResult
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{
region_key
:
regionKey
},
[
'region_key'
,
'region_name'
]
);
const
regionInfo
=
(
regionResult
.
data
||
[])[
0
];
return
regionInfo
?
regionInfo
.
region_name
:
''
;
}
/** 根据 regionKey 查询该区域下的设备列表 */
async
function
getDevicesByRegion
(
regionKey
:
string
,
columns
:
string
[]):
Promise
<
any
[]
>
{
const
deviceResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
region_key
:
regionKey
},
columns
);
return
deviceResult
.
data
||
[];
}
/** 查询指定设备列表在当天的最新一条 device_data(去重取第一条),返回 Map<device_id, parsed_data> */
async
function
getLatestDeviceDataMap
(
deviceIds
:
string
[],
startTime
:
string
):
Promise
<
Map
<
string
,
any
>>
{
if
(
deviceIds
.
length
===
0
)
return
new
Map
();
const
dataResult
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
'%in%'
:
deviceIds
},
device_time
:
{
'%gte%'
:
startTime
},
'%orderDesc%'
:
'device_time'
,
'%limit%'
:
deviceIds
.
length
,
},
[
'device_id'
,
'device_data'
]
);
const
latestMap
=
new
Map
<
string
,
any
>
();
(
dataResult
.
data
||
[]).
forEach
((
r
:
any
)
=>
{
if
(
!
latestMap
.
has
(
r
.
device_id
))
{
latestMap
.
set
(
r
.
device_id
,
parseDeviceData
(
r
.
device_data
));
}
});
return
latestMap
;
}
// ==================== 通用数据解析辅助 ====================
/** 安全解析 device_data 字段(可能是 JSON 字符串或已解析的对象) */
function
parseDeviceData
(
raw
:
any
):
any
{
if
(
!
raw
)
return
null
;
return
typeof
raw
===
'string'
?
JSON
.
parse
(
raw
)
:
raw
;
}
// TODO: 电表数据接入后,确认 device_data JSON 中电量字段的 key,目前暂定 'current'
const
ELECTRICITY_KEY
=
'current'
;
/**
* 查询电表设备在指定时间范围内的电量总和
*/
async
function
getElectricitySumInRange
(
startTime
:
string
,
endTime
:
string
):
Promise
<
number
>
{
const
deviceResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_type
:
'电表'
},
[
'device_id'
]
);
const
deviceIds
=
(
deviceResult
.
data
||
[]).
map
((
d
:
any
)
=>
d
.
device_id
);
if
(
deviceIds
.
length
===
0
)
return
0
;
const
dataResult
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
'%in%'
:
deviceIds
},
device_time
:
{
'%gte%'
:
startTime
,
'%lte%'
:
endTime
},
},
[
'device_data'
]
);
let
total
=
0
;
(
dataResult
.
data
||
[]).
forEach
((
r
:
any
)
=>
{
const
parsed
=
parseDeviceData
(
r
.
device_data
);
const
val
=
parsed
[
ELECTRICITY_KEY
];
if
(
typeof
val
===
'number'
)
total
+=
val
;
});
return
Math
.
round
(
total
*
100
)
/
100
;
}
/**
* 查询电表设备在指定时间范围内的分时间段电量列表
* @param startTime 开始时间
* @param endTime 结束时间
* @param groupBy 'hour' | 'day' | 'month'
* @param keyFormatter 时间 key 格式化函数
*/
async
function
getElectricityTrendData
(
startTime
:
string
,
endTime
:
string
,
groupBy
:
'hour'
|
'day'
|
'month'
,
):
Promise
<
{
key
:
string
;
value
:
number
}[]
>
{
const
deviceResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_type
:
'电表'
},
[
'device_id'
]
);
const
deviceIds
=
(
deviceResult
.
data
||
[]).
map
((
d
:
any
)
=>
d
.
device_id
);
if
(
deviceIds
.
length
===
0
)
return
[];
const
dataResult
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
'%in%'
:
deviceIds
},
device_time
:
{
'%gte%'
:
startTime
,
'%lte%'
:
endTime
},
'%orderAsc%'
:
'device_time'
,
},
[
'device_data'
,
'device_time'
]
);
const
records
=
dataResult
.
data
||
[];
const
bucketMap
=
new
Map
<
string
,
number
>
();
records
.
forEach
((
r
:
any
)
=>
{
const
t
=
new
Date
(
r
.
device_time
);
let
bucketKey
:
string
;
if
(
groupBy
===
'hour'
)
{
bucketKey
=
formatHourKey
(
t
);
}
else
if
(
groupBy
===
'day'
)
{
bucketKey
=
formatDayKey
(
t
);
}
else
{
bucketKey
=
formatMonthKey
(
t
);
}
const
parsed
=
parseDeviceData
(
r
.
device_data
);
const
val
=
parsed
[
ELECTRICITY_KEY
];
if
(
typeof
val
===
'number'
)
{
bucketMap
.
set
(
bucketKey
,
(
bucketMap
.
get
(
bucketKey
)
||
0
)
+
val
);
}
});
return
Array
.
from
(
bucketMap
.
entries
())
.
map
(([
key
,
value
])
=>
({
key
,
value
:
Math
.
round
(
value
*
100
)
/
100
}))
.
sort
((
a
,
b
)
=>
a
.
key
.
localeCompare
(
b
.
key
));
}
// ==================== 核心接口 ====================
/**
/**
* 运行分析
* 运行分析
* @returns 大屏所需的所有指标数据
* @returns 大屏所需的所有指标数据
*/
*/
export
async
function
getRunAnalysis
()
{
export
async
function
getRunAnalysis
()
{
// ========================================================================
// 一、能耗管理与趋势
// 一、能耗管理与趋势
// ========================================================================
// 1、能耗管理
// 1、能耗管理
/* ====== MOCK DATA(保留,勿删) ======
let energyManagement = {
let energyManagement = {
"jrnh": "16212",
"jrnh": "16212",
"zrnh": "13829",
"zrnh": "13829",
...
@@ -27,54 +397,96 @@ export async function getRunAnalysis() {
...
@@ -27,54 +397,96 @@ export async function getRunAnalysis() {
"synh": "0",
"synh": "0",
"sytb": "0"
"sytb": "0"
};
};
====== MOCK DATA END ====== */
// ---- 真实查询 ----
const
todayStart
=
getTodayStart
();
const
todayEnd
=
getTodayEnd
();
const
yesterdayStart
=
getYesterdayStart
();
const
yesterdayEnd
=
getYesterdayEnd
();
const
monthStart
=
getMonthStart
();
const
lastMonthStart
=
getLastMonthStart
();
const
lastMonthEnd
=
getLastMonthEnd
();
const
jrnh
=
await
getElectricitySumInRange
(
todayStart
,
todayEnd
);
const
zrnh
=
await
getElectricitySumInRange
(
yesterdayStart
,
yesterdayEnd
);
const
bynh
=
await
getElectricitySumInRange
(
monthStart
,
todayEnd
);
const
synh
=
await
getElectricitySumInRange
(
lastMonthStart
,
lastMonthEnd
);
const
zrtb
=
calcCompareRate
(
jrnh
,
zrnh
);
const
sytb
=
calcCompareRate
(
bynh
,
synh
);
let
energyManagement
=
{
"jrnh"
:
String
(
Math
.
round
(
jrnh
)),
"zrnh"
:
String
(
Math
.
round
(
zrnh
)),
"zrtb"
:
zrtb
,
"bynh"
:
String
(
Math
.
round
(
bynh
)),
"synh"
:
String
(
Math
.
round
(
synh
)),
"sytb"
:
sytb
};
// ========================================================================
// 2、能耗趋势
// 2、能耗趋势
/* ====== MOCK DATA(保留,勿删) ======
let electricityTrend = {
let electricityTrend = {
"last24HoursMax": 11258,
"last24HoursMax": 11258,
"last24Hours": [
"last24Hours": [
{
{ "key": "2026-06-28 17时", "value": "10429" },
"key"
:
"2026-06-28 17时"
,
{ "key": "2026-06-28 18时", "value": "10483" }
"value"
:
"10429"
},
{
"key"
:
"2026-06-28 18时"
,
"value"
:
"10483"
}
],
],
"last7DaysMax": 18601,
"last7DaysMax": 18601,
"last7Days": [
"last7Days": [
{
{ "key": "2026-06-23", "value": "13636" },
"key"
:
"2026-06-23"
,
{ "key": "2026-06-24", "value": "15402" }
"value"
:
"13636"
},
{
"key"
:
"2026-06-24"
,
"value"
:
"15402"
}
],
],
"last30DaysMax": 18601,
"last30DaysMax": 18601,
"last30Days": [
"last30Days": [
{
{ "key": "2026-05-31", "value": "0" },
"key"
:
"2026-05-31"
,
{ "key": "2026-06-01", "value": "0" }
"value"
:
"0"
},
{
"key"
:
"2026-06-01"
,
"value"
:
"0"
}
],
],
"lastYearMax": 21128,
"lastYearMax": 21128,
"lastYear": [
"lastYear": [
{
{ "key": "2025-07", "value": "0" },
"key"
:
"2025-07"
,
{ "key": "2025-08", "value": "0" }
"value"
:
"0"
},
{
"key"
:
"2025-08"
,
"value"
:
"0"
}
]
]
};
};
====== MOCK DATA END ====== */
// ---- 真实查询 ----
const
now24h
=
new
Date
();
const
last24hStart
=
getHoursAgo
(
24
);
const
last7dStart
=
getDaysAgoStart
(
7
);
const
last30dStart
=
getDaysAgoStart
(
30
);
const
lastYearStart
=
getMonthsAgoStart
(
12
);
const
last24HoursData
=
await
getElectricityTrendData
(
last24hStart
,
formatTime
(
now24h
),
'hour'
);
const
last7DaysData
=
await
getElectricityTrendData
(
last7dStart
,
getTodayEnd
(),
'day'
);
const
last30DaysData
=
await
getElectricityTrendData
(
last30dStart
,
getTodayEnd
(),
'day'
);
const
lastYearData
=
await
getElectricityTrendData
(
lastYearStart
,
getTodayEnd
(),
'month'
);
// 转为字符串 value(与 mock 一致)
const
stringifyValues
=
(
arr
:
{
key
:
string
;
value
:
number
}[])
=>
arr
.
map
(
item
=>
({
key
:
item
.
key
,
value
:
String
(
item
.
value
)
}));
const
last24hValues
=
last24HoursData
.
map
(
i
=>
i
.
value
);
const
last7dValues
=
last7DaysData
.
map
(
i
=>
i
.
value
);
const
last30dValues
=
last30DaysData
.
map
(
i
=>
i
.
value
);
const
lastYearValues
=
lastYearData
.
map
(
i
=>
i
.
value
);
let
electricityTrend
=
{
"last24HoursMax"
:
last24hValues
.
length
>
0
?
Math
.
max
(...
last24hValues
)
:
0
,
"last24Hours"
:
stringifyValues
(
last24HoursData
),
"last7DaysMax"
:
last7dValues
.
length
>
0
?
Math
.
max
(...
last7dValues
)
:
0
,
"last7Days"
:
stringifyValues
(
last7DaysData
),
"last30DaysMax"
:
last30dValues
.
length
>
0
?
Math
.
max
(...
last30dValues
)
:
0
,
"last30Days"
:
stringifyValues
(
last30DaysData
),
"lastYearMax"
:
lastYearValues
.
length
>
0
?
Math
.
max
(...
lastYearValues
)
:
0
,
"lastYear"
:
stringifyValues
(
lastYearData
),
};
// ========================================================================
// 二、设备耗电量监控
// 二、设备耗电量监控
/* ====== MOCK DATA(保留,勿删) ======
let deviceTypeMap = new Map();
let deviceTypeMap = new Map();
let deviceElectricityMap = new Map();
let deviceElectricityMap = new Map();
let deviceElectricityDatas = deviceTypeList.map((deviceType) => {
let deviceElectricityDatas = deviceTypeList.map((deviceType) => {
...
@@ -88,15 +500,92 @@ export async function getRunAnalysis() {
...
@@ -88,15 +500,92 @@ export async function getRunAnalysis() {
titleList: ["设备","数量","耗电情况"],
titleList: ["设备","数量","耗电情况"],
dataList: deviceElectricityDatas
dataList: deviceElectricityDatas
};
};
====== MOCK DATA END ====== */
// ---- 真实查询 ----
// 查询所有设备,按 device_type 分组统计数量
const
allDevicesResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{},
[
'device_id'
,
'device_type'
]
);
const
allDevices
=
allDevicesResult
.
data
||
[];
// 按类型统计设备数量
const
deviceTypeCountMap
=
new
Map
<
string
,
number
>
();
allDevices
.
forEach
((
d
:
any
)
=>
{
const
dt
=
d
.
device_type
;
deviceTypeCountMap
.
set
(
dt
,
(
deviceTypeCountMap
.
get
(
dt
)
||
0
)
+
1
);
});
// 查询当日各电表设备的总电量
const
meterDevicesResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_type
:
'电表'
},
[
'device_id'
]
);
const
meterDeviceIds
=
(
meterDevicesResult
.
data
||
[]).
map
((
d
:
any
)
=>
d
.
device_id
);
// 按设备类型统计耗电:仅电表类型有数据
let
deviceElectricityMap
=
new
Map
<
string
,
number
>
();
if
(
meterDeviceIds
.
length
>
0
)
{
const
meterDataResult
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
'%in%'
:
meterDeviceIds
},
device_time
:
{
'%gte%'
:
todayStart
},
},
[
'device_data'
]
);
let
meterTotal
=
0
;
(
meterDataResult
.
data
||
[]).
forEach
((
r
:
any
)
=>
{
const
parsed
=
parseDeviceData
(
r
.
device_data
);
const
val
=
parsed
[
ELECTRICITY_KEY
];
if
(
typeof
val
===
'number'
)
meterTotal
+=
val
;
});
deviceElectricityMap
.
set
(
'电表'
,
Math
.
round
(
meterTotal
*
100
)
/
100
);
}
let
deviceElectricityDatas
=
deviceTypeList
.
map
((
deviceType
)
=>
{
return
{
"deviceType"
:
deviceType
,
"deviceCount"
:
deviceTypeCountMap
.
get
(
deviceType
)
||
0
,
"electricity"
:
deviceElectricityMap
.
get
(
deviceType
)
||
0
}
});
let
deviceElectricity
=
{
titleList
:
[
"设备"
,
"数量"
,
"耗电情况"
],
dataList
:
deviceElectricityDatas
};
// ========================================================================
// 三、设备监测情况与趋势
// 三、设备监测情况与趋势
// 1、设备监测情况
// 1、设备监测情况
/* ====== MOCK DATA(保留,勿删) ======
let deviceMonitor = {
let deviceMonitor = {
total: 100,
total: 100,
online: 80,
online: 80,
offline: 15,
offline: 15,
fault: 5,
fault: 5,
};
};
====== MOCK DATA END ====== */
// ---- 真实查询 ----
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
;
let
deviceMonitor
=
{
total
:
total
,
online
:
onlineTotal
,
offline
:
offlineTotal
,
fault
:
faultTotal
,
};
// 2、设备监测趋势
// 2、设备监测趋势
/* ====== MOCK DATA(保留,勿删) ======
let deviceMonitorTrend = [{
let deviceMonitorTrend = [{
key: "2026-06-28 07时",
key: "2026-06-28 07时",
value: 80
value: 80
...
@@ -104,22 +593,103 @@ export async function getRunAnalysis() {
...
@@ -104,22 +593,103 @@ export async function getRunAnalysis() {
key: "2026-06-28 17时",
key: "2026-06-28 17时",
value: 75
value: 75
}];
}];
====== MOCK DATA END ====== */
// ---- 真实查询 ----
// 趋势基于 device_data 表:每小时有数据上报的设备数代表在线数
const
trend24hStart
=
getHoursAgo
(
24
);
const
trendEnd
=
formatTime
(
new
Date
());
const
envDevicesResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{},
[
'device_id'
]
);
const
allDeviceIds
=
(
envDevicesResult
.
data
||
[]).
map
((
d
:
any
)
=>
d
.
device_id
);
let
trendHourly
:
{
key
:
string
;
value
:
number
}[]
=
[];
if
(
allDeviceIds
.
length
>
0
)
{
const
trendDataResult
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
'%in%'
:
allDeviceIds
},
device_time
:
{
'%gte%'
:
trend24hStart
,
'%lte%'
:
trendEnd
},
'%orderAsc%'
:
'device_time'
,
},
[
'device_id'
,
'device_time'
]
);
const
hourlyDevices
=
new
Map
<
string
,
Set
<
string
>>
();
(
trendDataResult
.
data
||
[]).
forEach
((
r
:
any
)
=>
{
const
t
=
new
Date
(
r
.
device_time
);
const
key
=
formatHourKey
(
t
);
if
(
!
hourlyDevices
.
has
(
key
))
hourlyDevices
.
set
(
key
,
new
Set
());
hourlyDevices
.
get
(
key
)
!
.
add
(
r
.
device_id
);
});
trendHourly
=
Array
.
from
(
hourlyDevices
.
entries
())
.
map
(([
key
,
devices
])
=>
({
key
,
value
:
devices
.
size
}))
.
sort
((
a
,
b
)
=>
a
.
key
.
localeCompare
(
b
.
key
));
}
let
deviceMonitorTrend
=
trendHourly
;
// ========================================================================
// 四、预警工单处理
// 四、预警工单处理
// 1、预警处理状态
// 1、预警处理状态
/* ====== MOCK DATA(保留,勿删) ======
let alertStatus = {
let alertStatus = {
resolved: 1,
resolved: 1,
responded: 1,
responded: 1,
unresolved: 1
unresolved: 1
};
};
====== MOCK DATA END ====== */
// ---- 真实查询 ----
const
resolvedCount
=
await
selectDataCountByParam
(
TABLENAME
.
设备故障表
,
{
fault_status
:
'已处理'
});
const
respondedCount
=
await
selectDataCountByParam
(
TABLENAME
.
设备故障表
,
{
fault_status
:
'处理中'
});
const
unresolvedCount
=
await
selectDataCountByParam
(
TABLENAME
.
设备故障表
,
{
fault_status
:
'未处理'
});
let
alertStatus
=
{
resolved
:
typeof
resolvedCount
.
data
===
'number'
?
resolvedCount
.
data
:
0
,
responded
:
typeof
respondedCount
.
data
===
'number'
?
respondedCount
.
data
:
0
,
unresolved
:
typeof
unresolvedCount
.
data
===
'number'
?
unresolvedCount
.
data
:
0
,
};
// 2、预警工单列表
// 2、预警工单列表
/* ====== MOCK DATA(保留,勿删) ======
let alertWorkOrders = [
let alertWorkOrders = [
{ riskContent: "空调压缩机异常", alertTime: "26/02/01", status: "已解决" },
{ riskContent: "空调压缩机异常", alertTime: "26/02/01", status: "已解决" },
{ riskContent: "温度传感器离线", alertTime: "26/02/01", status: "已响应" },
{ riskContent: "温度传感器离线", alertTime: "26/02/01", status: "已响应" },
{ riskContent: "湿度超标", alertTime: "26/02/02", status: "未处理" }
{ riskContent: "湿度超标", alertTime: "26/02/02", status: "未处理" }
];
];
====== MOCK DATA END ====== */
// ---- 真实查询 ----
const
faultListResult
=
await
selectDataListByParam
(
TABLENAME
.
设备故障表
,
{
'%or%'
:
[
{
fault_status
:
'未处理'
},
{
fault_status
:
'处理中'
}
],
'%orderDesc%'
:
'fault_time'
,
'%limit%'
:
20
,
},
[
'fault_type'
,
'fault_time'
,
'fault_status'
,
'device_id'
]
);
let
alertWorkOrders
=
(
faultListResult
.
data
||
[]).
map
((
f
:
any
)
=>
({
riskContent
:
f
.
fault_type
||
'未知故障'
,
alertTime
:
f
.
fault_time
?
formatTime
(
f
.
fault_time
).
split
(
' '
)[
0
].
replace
(
/-/g
,
'/'
).
slice
(
2
)
:
''
,
status
:
f
.
fault_status
,
}));
// ========================================================================
// 五、空气质量环境监控
// 五、空气质量环境监控
// 1、空气质量指数和趋势
// 1、空气质量指数和趋势
/* ====== MOCK DATA(保留,勿删) ======
let environmentalQuality = {
let environmentalQuality = {
qualityIndex: {
qualityIndex: {
current: "优",
current: "优",
...
@@ -135,103 +705,314 @@ export async function getRunAnalysis() {
...
@@ -135,103 +705,314 @@ export async function getRunAnalysis() {
value: 79
value: 79
}]
}]
};
};
====== MOCK DATA END ====== */
// ---- 真实查询 ----
const
latestEnvData
=
await
getLatestEnvDeviceData
();
let
avgCo2
=
0
;
let
avgPm25
=
0
;
let
envDataCount
=
0
;
latestEnvData
.
forEach
((
item
)
=>
{
const
d
=
item
.
data
;
if
(
d
)
{
if
(
typeof
d
.
co2
===
'number'
)
{
avgCo2
+=
d
.
co2
;
envDataCount
++
;
}
if
(
typeof
d
.
pm2_5
===
'number'
)
avgPm25
+=
d
[
'pm2_5'
];
}
});
if
(
envDataCount
>
0
)
avgCo2
=
Math
.
round
(
avgCo2
/
envDataCount
);
if
(
latestEnvData
.
size
>
0
)
avgPm25
=
Math
.
round
(
avgPm25
/
latestEnvData
.
size
);
const
overallQuality
=
getOverallQuality
(
avgCo2
,
avgPm25
);
// 空气质量指数(基于 CO2 的粗估:0~2000 映射到 0~100 的倒数)
const
qualityIndex
=
Math
.
max
(
0
,
Math
.
min
(
100
,
Math
.
round
((
1
-
avgCo2
/
2000
)
*
100
)));
// 按小时聚合的历史数据(当日)
const
hourlyEnvData
=
await
getHourlyEnvData
(
todayStart
,
todayEnd
);
const
iaqQualityTrend
:
{
key
:
string
;
value
:
number
}[]
=
[];
hourlyEnvData
.
forEach
((
indicatorMap
,
hourKey
)
=>
{
const
co2
=
indicatorMap
.
get
(
'co2'
)
||
0
;
const
q
=
Math
.
max
(
0
,
Math
.
min
(
100
,
Math
.
round
((
1
-
co2
/
2000
)
*
100
)));
iaqQualityTrend
.
push
({
key
:
formatHourKey
(
new
Date
(
`
${
todayStart
.
split
(
' '
)[
0
]}
${
hourKey
}
`
)),
value
:
q
,
});
});
iaqQualityTrend
.
sort
((
a
,
b
)
=>
a
.
key
.
localeCompare
(
b
.
key
));
let
environmentalQuality
=
{
qualityIndex
:
{
current
:
overallQuality
===
'优秀'
?
'优'
:
(
overallQuality
===
'良好'
?
'良'
:
overallQuality
),
max
:
100
,
min
:
0
,
index
:
qualityIndex
,
},
iaqQualityTrend
:
iaqQualityTrend
,
};
// 2、环境数据监测
// 2、环境数据监测
/* ====== MOCK DATA(保留,勿删) ======
let environmentalTrend = {
let environmentalTrend = {
"todayEnvironmental": "优秀",
"todayEnvironmental": "优秀",
"currentTemperature": "24.5℃",
"currentTemperature": "24.5℃",
"currentHumidity": "55%",
"currentHumidity": "55%",
"currentPm25": "0μg/m³",
"currentPm25": "0μg/m³",
"currentCo2": "800ppm",
"currentCo2": "800ppm",
"temperatureTrend"
:
[
"temperatureTrend": [{ "time": "0:00", "value": "0" }],
{
"humidityTrend": [{ "time": "0:00", "value": "0" }],
"time"
:
"0:00"
,
"pm25Trend": [{ "time": "0:00", "value": "0" }],
"value"
:
"0"
"co2Trend": [{ "time": "0:00", "value": "0" }],
}
],
"humidityTrend"
:
[
{
"time"
:
"0:00"
,
"value"
:
"0"
}
],
"pm25Trend"
:
[
{
"time"
:
"0:00"
,
"value"
:
"0"
}
],
"co2Trend"
:
[
{
"time"
:
"0时"
,
"value"
:
"0"
}
],
"qualityIndex": 500,
"qualityIndex": 500,
"currentHcho": 0,
"currentHcho": 0,
"currentLightLevel": 0,
"currentLightLevel": 0,
"currentPm10": 0,
"currentPm10": 0,
"currentPressure": 0,
"currentPressure": 0,
"currentTvoc": 2,
"currentTvoc": 2,
"hchoTrend"
:
[
"hchoTrend": [{ "time": "0:00", "value": "0" }],
{
"lightLevelTrend": [{ "time": "0:00", "value": "0" }],
"time"
:
"0:00"
,
"pm10Trend": [{ "time": "0:00", "value": "0" }],
"value"
:
"0"
"pressureTrend": [{ "time": "0:00", "value": "0" }],
}
"tvocTrend": [{ "time": "0:00", "value": "0" }]
],
};
"lightLevelTrend"
:
[
====== MOCK DATA END ====== */
{
"time"
:
"0:00"
,
// ---- 真实查询 ----
"value"
:
"0"
// 计算各指标当前均值
}
const
allIndicatorValues
=
new
Map
<
string
,
{
sum
:
number
;
count
:
number
}
>
();
],
latestEnvData
.
forEach
((
item
)
=>
{
"pm10Trend"
:
[
const
d
=
item
.
data
;
{
if
(
!
d
)
return
;
"time"
:
"0:00"
,
ENV_INDICATOR_KEYS
.
forEach
(
key
=>
{
"value"
:
"0"
const
val
=
d
[
key
];
}
if
(
typeof
val
===
'number'
)
{
],
if
(
!
allIndicatorValues
.
has
(
key
))
allIndicatorValues
.
set
(
key
,
{
sum
:
0
,
count
:
0
});
"pressureTrend"
:
[
const
acc
=
allIndicatorValues
.
get
(
key
)
!
;
{
acc
.
sum
+=
val
;
"time"
:
"0:00"
,
acc
.
count
+=
1
;
"value"
:
"0"
}
],
"tvocTrend"
:
[
{
"time"
:
"0:00"
,
"value"
:
"0"
}
}
]
});
});
const
getAvg
=
(
key
:
string
):
number
=>
{
const
acc
=
allIndicatorValues
.
get
(
key
);
if
(
!
acc
||
acc
.
count
===
0
)
return
0
;
return
Math
.
round
((
acc
.
sum
/
acc
.
count
)
*
100
)
/
100
;
};
const
currentTemperature
=
getAvg
(
'temperature'
);
const
currentHumidity
=
getAvg
(
'humidity'
);
const
currentPm25
=
getAvg
(
'pm25'
);
const
currentCo2
=
getAvg
(
'co2'
);
const
currentHcho
=
getAvg
(
'hcho'
);
const
currentLightLevel
=
getAvg
(
'lightLevel'
);
const
currentPm10
=
getAvg
(
'pm10'
);
const
currentPressure
=
getAvg
(
'pressure'
);
const
currentTvoc
=
getAvg
(
'tvoc'
);
// 构建各指标趋势(按小时,time 格式 "0:00")
const
buildTrend
=
(
indicatorKey
:
string
):
{
time
:
string
;
value
:
string
}[]
=>
{
const
trend
:
{
time
:
string
;
value
:
string
}[]
=
[];
const
hours
=
Array
.
from
({
length
:
24
},
(
_
,
i
)
=>
String
(
i
).
padStart
(
2
,
'0'
)
+
':00'
);
hours
.
forEach
(
h
=>
{
const
hourData
=
hourlyEnvData
.
get
(
h
);
const
val
=
hourData
?
(
hourData
.
get
(
indicatorKey
)
||
0
)
:
0
;
trend
.
push
({
time
:
h
,
value
:
String
(
val
)
});
});
return
trend
;
};
let
environmentalTrend
=
{
"todayEnvironmental"
:
overallQuality
,
"currentTemperature"
:
currentTemperature
+
'℃'
,
"currentHumidity"
:
currentHumidity
+
'%'
,
"currentPm25"
:
currentPm25
+
'μg/m³'
,
"currentCo2"
:
currentCo2
+
'ppm'
,
"temperatureTrend"
:
buildTrend
(
'temperature'
),
"humidityTrend"
:
buildTrend
(
'humidity'
),
"pm25Trend"
:
buildTrend
(
'pm25'
),
"co2Trend"
:
buildTrend
(
'co2'
),
"qualityIndex"
:
qualityIndex
,
"currentHcho"
:
currentHcho
,
"currentLightLevel"
:
currentLightLevel
,
"currentPm10"
:
currentPm10
,
"currentPressure"
:
currentPressure
,
"currentTvoc"
:
currentTvoc
,
"hchoTrend"
:
buildTrend
(
'hcho'
),
"lightLevelTrend"
:
buildTrend
(
'lightLevel'
),
"pm10Trend"
:
buildTrend
(
'pm10'
),
"pressureTrend"
:
buildTrend
(
'pressure'
),
"tvocTrend"
:
buildTrend
(
'tvoc'
),
};
};
// ========================================================================
// 六、机房环境功率监控
// 六、机房环境功率监控
// 1、机房环境监控
// 1、机房环境监控
let
compRoomEnvironmental
=
[{
/* ====== MOCK DATA(保留,勿删) ======
index
:
1
,
let compRoomEnvironmental: { index: number; key: string; value: string }[] = [{
key
:
"温度"
,
index: 1, key: "温度", value: "24.5℃",
value
:
"24.5℃"
,
},{
},{
index
:
2
,
index: 2, key: "湿度", value: "55%",
key
:
"湿度"
,
value
:
"55%"
,
},{
},{
index
:
3
,
index: 3, key: "配置", value: "烟感",
key
:
"配置"
,
value
:
"烟感"
,
},{
},{
index
:
4
,
index: 4, key: "常开", value: "水浸",
key
:
"常开"
,
value
:
"水浸"
,
}];
}];
====== MOCK DATA END ====== */
// ---- 真实查询 ----
// 查询 region_type = '机房' 的区域
const
serverRoomResult
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{
region_type
:
'机房'
},
[
'id'
,
'region_key'
,
'region_name'
]
);
const
serverRegions
=
serverRoomResult
.
data
||
[];
let
compRoomEnvironmental
:
{
index
:
number
;
key
:
string
;
value
:
string
}[];
if
(
serverRegions
.
length
>
0
)
{
const
serverRegionKeys
=
serverRegions
.
map
((
r
:
any
)
=>
r
.
region_key
);
// 查询机房区域下的 IEQ传感器 设备(环境监测设备)
const
serverDeviceResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
region_key
:
{
'%in%'
:
serverRegionKeys
},
device_type
:
'IEQ传感器'
,
},
[
'device_id'
]
);
const
serverDeviceIds
=
(
serverDeviceResult
.
data
||
[]).
map
((
d
:
any
)
=>
d
.
device_id
);
if
(
serverDeviceIds
.
length
>
0
)
{
const
srDataResult
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
'%in%'
:
serverDeviceIds
},
device_time
:
{
'%gte%'
:
todayStart
},
'%orderDesc%'
:
'device_time'
,
'%limit%'
:
serverDeviceIds
.
length
,
},
[
'device_id'
,
'device_data'
]
);
const
srRecords
=
srDataResult
.
data
||
[];
const
srLatest
:
Map
<
string
,
any
>
=
new
Map
();
srRecords
.
forEach
((
r
:
any
)
=>
{
if
(
!
srLatest
.
has
(
r
.
device_id
))
{
srLatest
.
set
(
r
.
device_id
,
parseDeviceData
(
r
.
device_data
));
}
});
// 汇总四个指标均值:温度、湿度、二氧化碳、PM2.5
let
srTempSum
=
0
,
srHumSum
=
0
,
srCo2Sum
=
0
,
srPm25Sum
=
0
,
srCount
=
0
;
srLatest
.
forEach
((
d
)
=>
{
if
(
d
)
{
srTempSum
+=
(
d
.
temperature
||
0
);
srHumSum
+=
(
d
.
humidity
||
0
);
srCo2Sum
+=
(
d
.
co2
||
0
);
srPm25Sum
+=
(
d
.
pm25
||
0
);
srCount
++
;
}
});
const
srAvgTemp
=
srCount
>
0
?
(
srTempSum
/
srCount
).
toFixed
(
1
)
:
'0'
;
const
srAvgHum
=
srCount
>
0
?
(
srHumSum
/
srCount
).
toFixed
(
1
)
:
'0'
;
const
srAvgCo2
=
srCount
>
0
?
Math
.
round
(
srCo2Sum
/
srCount
).
toString
()
:
'0'
;
const
srAvgPm25
=
srCount
>
0
?
Math
.
round
(
srPm25Sum
/
srCount
).
toString
()
:
'0'
;
compRoomEnvironmental
=
[
{
index
:
1
,
key
:
'温度'
,
value
:
srAvgTemp
+
'℃'
},
{
index
:
2
,
key
:
'湿度'
,
value
:
srAvgHum
+
'%'
},
{
index
:
3
,
key
:
'二氧化碳'
,
value
:
srAvgCo2
+
'ppm'
},
{
index
:
4
,
key
:
'PM2.5'
,
value
:
srAvgPm25
+
'μg/m³'
},
];
}
else
{
compRoomEnvironmental
=
[
{
index
:
1
,
key
:
'温度'
,
value
:
'N/A'
},
{
index
:
2
,
key
:
'湿度'
,
value
:
'N/A'
},
{
index
:
3
,
key
:
'二氧化碳'
,
value
:
'N/A'
},
{
index
:
4
,
key
:
'PM2.5'
,
value
:
'N/A'
},
];
}
}
else
{
compRoomEnvironmental
=
[
{
index
:
1
,
key
:
'温度'
,
value
:
'N/A'
},
{
index
:
2
,
key
:
'湿度'
,
value
:
'N/A'
},
{
index
:
3
,
key
:
'二氧化碳'
,
value
:
'N/A'
},
{
index
:
4
,
key
:
'PM2.5'
,
value
:
'N/A'
},
];
}
// 2、机房功率监控
// 2、机房功率监控
let
compRoomPower
=
[{
// TODO: 待机房环境监控逻辑确认后,同步恢复真实查询
key
:
"2026-06-28 07时"
,
let
compRoomPower
:
{
key
:
string
;
value
:
number
}[]
=
[{
value
:
200
key
:
"2026-06-28 07时"
,
value
:
200
},{
},{
key
:
"2026-06-28 17时"
,
key
:
"2026-06-28 17时"
,
value
:
400
value
:
400
}];
}];
/* ====== 真实查询逻辑(暂存,待确认后启用) ======
if (serverRegions.length > 0) {
const serverRegionKeys = serverRegions.map((r: any) => r.region_key);
const srMeterResult = await selectDataListByParam(
TABLENAME.设备表,
{
region_key: { '%in%': serverRegionKeys },
device_type: '电表',
},
['device_id']
);
const srMeterIds = (srMeterResult.data || []).map((d: any) => d.device_id);
if (srMeterIds.length > 0) {
const srPowerResult = await selectDataListByParam(
TABLENAME.设备数据表,
{
device_id: { '%in%': srMeterIds },
device_time: { '%gte%': getHoursAgo(24) },
'%orderAsc%': 'device_time',
},
['device_data', 'device_time']
);
const srPowerHourly = new Map<string, number>();
(srPowerResult.data || []).forEach((r: any) => {
const t = new Date(r.device_time);
const key = formatHourKey(t);
const parsed = parseDeviceData(r.device_data);
const val = parsed[ELECTRICITY_KEY];
if (typeof val === 'number') {
srPowerHourly.set(key, (srPowerHourly.get(key) || 0) + val);
}
});
compRoomPower = Array.from(srPowerHourly.entries())
.map(([key, value]) => ({ key, value: Math.round(value) }))
.sort((a, b) => a.key.localeCompare(b.key));
}
}
====== 真实查询逻辑 END ====== */
// ========================================================================
// 七、设备汇总
// 七、设备汇总
/* ====== MOCK DATA(保留,勿删) ======
let summaryData = {"runningDevices":83,"offlineDevices":0,"normalRate":"100.00%","faultDevices":0}
let summaryData = {"runningDevices":83,"offlineDevices":0,"normalRate":"100.00%","faultDevices":0}
====== 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
normalRate
=
total
>
0
?
((
runningDevices
/
total
)
*
100
).
toFixed
(
2
)
+
'%'
:
'0.00%'
;
let
summaryData
=
{
"runningDevices"
:
runningDevices
,
"offlineDevices"
:
offlineDevicesSummary
,
"normalRate"
:
normalRate
,
"faultDevices"
:
faultDevices
};
// 返回所有指标监控信息
// 返回所有指标监控信息
return
{
return
{
nhgl
:
energyManagement
,
nhgl
:
energyManagement
,
...
@@ -251,9 +1032,11 @@ export async function getRunAnalysis() {
...
@@ -251,9 +1032,11 @@ export async function getRunAnalysis() {
/**
/**
* 运行分析弹窗
* 运行分析弹窗
* @param regionKey 区域标识
* @returns 区域设备的指标数据
* @returns 区域设备的指标数据
*/
*/
export
async
function
getAnalysisPopup
()
{
export
async
function
getAnalysisPopup
(
regionKey
:
string
)
{
/* ====== MOCK DATA(保留,勿删) ======
return {
return {
"regionName": "汇报厅",
"regionName": "汇报厅",
"sbztjc": {
"sbztjc": {
...
@@ -277,13 +1060,123 @@ export async function getAnalysisPopup() {
...
@@ -277,13 +1060,123 @@ export async function getAnalysisPopup() {
}
}
]
]
};
};
====== MOCK DATA END ====== */
// ---- 真实查询 ----
const
todayStart
=
getTodayStart
();
const
todayEnd
=
getTodayEnd
();
const
trendStart
=
getHoursAgo
(
24
);
// 查询区域信息和设备列表
const
regionName
=
await
getRegionName
(
regionKey
);
const
devices
=
await
getDevicesByRegion
(
regionKey
,
[
'device_id'
,
'device_type'
,
'device_state'
]);
const
deviceIds
=
devices
.
map
((
d
:
any
)
=>
d
.
device_id
);
// 设备状态监测(总数/在线/离线/故障)
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
;
let
sbztjc
=
{
"total"
:
total
,
"online"
:
online
,
"offline"
:
offline
,
"fault"
:
fault
};
// 设备监测趋势(24小时内每小时在线/离线/故障数量)
let
sbztjcqs
:
{
key
:
string
;
online
:
number
;
offline
:
number
;
fault
:
number
}[]
=
[];
if
(
deviceIds
.
length
>
0
)
{
const
trendDataResult
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
'%in%'
:
deviceIds
},
device_time
:
{
'%gte%'
:
trendStart
,
'%lte%'
:
todayEnd
},
'%orderAsc%'
:
'device_time'
,
},
[
'device_id'
,
'device_time'
]
);
const
records
=
trendDataResult
.
data
||
[];
// 按小时聚合:每个小时有哪些设备上报了数据
const
hourlyDeviceSet
=
new
Map
<
string
,
Set
<
string
>>
();
records
.
forEach
((
r
:
any
)
=>
{
const
t
=
new
Date
(
r
.
device_time
);
const
h
=
String
(
t
.
getHours
()).
padStart
(
2
,
'0'
);
const
hourKey
=
todayStart
.
split
(
' '
)[
0
]
+
' '
+
h
+
':00'
;
if
(
!
hourlyDeviceSet
.
has
(
hourKey
))
hourlyDeviceSet
.
set
(
hourKey
,
new
Set
());
hourlyDeviceSet
.
get
(
hourKey
)
!
.
add
(
r
.
device_id
);
});
// 预计算各设备的状态(静态,不随时间变化)
const
deviceStateMap
=
new
Map
<
string
,
number
>
();
devices
.
forEach
((
d
:
any
)
=>
deviceStateMap
.
set
(
d
.
device_id
,
d
.
device_state
));
hourlyDeviceSet
.
forEach
((
reportedDeviceSet
,
hourKey
)
=>
{
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
++
;
else
if
(
!
reported
)
hOffline
++
;
// 未上报也算离线
});
sbztjcqs
.
push
({
key
:
hourKey
,
online
:
hOnline
,
offline
:
hOffline
,
fault
:
hFault
,
});
});
}
sbztjcqs
.
sort
((
a
,
b
)
=>
a
.
key
.
localeCompare
(
b
.
key
));
// 能耗监测(该区域电表24小时趋势)
let
nhjc
:
{
key
:
string
;
value
:
string
}[]
=
[];
if
(
deviceIds
.
length
>
0
)
{
const
meterIds
=
devices
.
filter
((
d
:
any
)
=>
d
.
device_type
===
'电表'
).
map
((
d
:
any
)
=>
d
.
device_id
);
if
(
meterIds
.
length
>
0
)
{
const
meterDataResult
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
'%in%'
:
meterIds
},
device_time
:
{
'%gte%'
:
trendStart
,
'%lte%'
:
todayEnd
},
'%orderAsc%'
:
'device_time'
,
},
[
'device_data'
,
'device_time'
]
);
const
hourlySum
=
new
Map
<
string
,
number
>
();
(
meterDataResult
.
data
||
[]).
forEach
((
r
:
any
)
=>
{
const
t
=
new
Date
(
r
.
device_time
);
const
h
=
String
(
t
.
getHours
()).
padStart
(
2
,
'0'
);
const
hourKey
=
todayStart
.
split
(
' '
)[
0
]
+
' '
+
h
+
':00'
;
const
parsed
=
parseDeviceData
(
r
.
device_data
);
const
val
=
parsed
[
ELECTRICITY_KEY
];
if
(
typeof
val
===
'number'
)
{
hourlySum
.
set
(
hourKey
,
(
hourlySum
.
get
(
hourKey
)
||
0
)
+
val
);
}
});
nhjc
=
Array
.
from
(
hourlySum
.
entries
())
.
map
(([
key
,
value
])
=>
({
key
,
value
:
String
(
Math
.
round
(
value
))
}))
.
sort
((
a
,
b
)
=>
a
.
key
.
localeCompare
(
b
.
key
));
}
}
return
{
"regionName"
:
regionName
,
"sbztjc"
:
sbztjc
,
"sbztjcqs"
:
sbztjcqs
,
"nhjc"
:
nhjc
};
}
}
/**
/**
* 环境态势感知
* 环境态势感知
(所有区域汇总)
* @returns 今日环境监测数据
* @returns 今日环境监测数据
*/
*/
export
async
function
getRunEnvironmental
()
{
export
async
function
getRunEnvironmental
()
{
/* ====== MOCK DATA(保留,勿删) ======
// 一、环境数据总览
// 一、环境数据总览
let hjsjzl = {
let hjsjzl = {
"temperature": {
"temperature": {
...
@@ -337,52 +1230,10 @@ export async function getRunEnvironmental() {
...
@@ -337,52 +1230,10 @@ export async function getRunEnvironmental() {
// 二、环境参数趋势对比分析
// 二、环境参数趋势对比分析
let hjqsdbfx = {
let hjqsdbfx = {
"wd": {
"wd": {
"jr"
:
[{
"jr": [{"key": "06:00","value": 16},{"key": "12:00","value": 26},{"key": "18:00","value": 18}],
"key"
:
"06:00"
,
"zr": [{"key": "06:00","value": 18},{"key": "12:00","value": 24},{"key": "18:00","value": 20}],
"value"
:
16
"bz": [{"key": "2026-07-06","value": 25},{"key": "2026-07-07","value": 22},{"key": "2026-07-08","value": 24},{"key": "2026-07-09","value": 23}],
},
{
"by": [{"key": "2026-07-06","value": 25},{"key": "2026-07-07","value": 22},{"key": "2026-07-08","value": 24},{"key": "2026-07-09","value": 23}],
"key"
:
"12:00"
,
"value"
:
26
},
{
"key"
:
"18:00"
,
"value"
:
18
}],
"zr"
:
[{
"key"
:
"06:00"
,
"value"
:
18
},
{
"key"
:
"12:00"
,
"value"
:
24
},
{
"key"
:
"18:00"
,
"value"
:
20
}],
"bz"
:
[{
"key"
:
"2026-07-06"
,
"value"
:
25
},
{
"key"
:
"2026-07-07"
,
"value"
:
22
},
{
"key"
:
"2026-07-08"
,
"value"
:
24
},
{
"key"
:
"2026-07-09"
,
"value"
:
23
}],
"by"
:
[{
"key"
:
"2026-07-06"
,
"value"
:
25
},
{
"key"
:
"2026-07-07"
,
"value"
:
22
},
{
"key"
:
"2026-07-08"
,
"value"
:
24
},
{
"key"
:
"2026-07-09"
,
"value"
:
23
}],
},
},
"sd": {},
"sd": {},
"pm25": {},
"pm25": {},
...
@@ -391,52 +1242,10 @@ export async function getRunEnvironmental() {
...
@@ -391,52 +1242,10 @@ export async function getRunEnvironmental() {
"hcho": {},
"hcho": {},
"hyl": {},
"hyl": {},
"gz": {
"gz": {
"jr"
:
[{
"jr": [{"key": "06:00","value": 0},{"key": "12:00","value": 1},{"key": "18:00","value": 0}],
"key"
:
"06:00"
,
"zr": [{"key": "06:00","value": 0},{"key": "12:00","value": 1},{"key": "18:00","value": 1}],
"value"
:
0
"bz": [{"key": "2026-07-06","value": 0.2},{"key": "2026-07-07","value": 0.8},{"key": "2026-07-08","value": 0.5},{"key": "2026-07-09","value": 0.6}],
},
{
"by": [{"key": "2026-07-06","value": 0.2},{"key": "2026-07-07","value": 0.8},{"key": "2026-07-08","value": 0.5},{"key": "2026-07-09","value": 0.6}],
"key"
:
"12:00"
,
"value"
:
1
},
{
"key"
:
"18:00"
,
"value"
:
0
}],
"zr"
:
[{
"key"
:
"06:00"
,
"value"
:
0
},
{
"key"
:
"12:00"
,
"value"
:
1
},
{
"key"
:
"18:00"
,
"value"
:
1
}],
"bz"
:
[{
"key"
:
"2026-07-06"
,
"value"
:
0.2
},
{
"key"
:
"2026-07-07"
,
"value"
:
0.8
},
{
"key"
:
"2026-07-08"
,
"value"
:
0.5
},
{
"key"
:
"2026-07-09"
,
"value"
:
0.6
}],
"by"
:
[{
"key"
:
"2026-07-06"
,
"value"
:
0.2
},
{
"key"
:
"2026-07-07"
,
"value"
:
0.8
},
{
"key"
:
"2026-07-08"
,
"value"
:
0.5
},
{
"key"
:
"2026-07-09"
,
"value"
:
0.6
}],
},
},
"zy": {},
"zy": {},
};
};
...
@@ -444,53 +1253,312 @@ export async function getRunEnvironmental() {
...
@@ -444,53 +1253,312 @@ export async function getRunEnvironmental() {
let cjsbjcmx = {
let cjsbjcmx = {
"titleList": ["场景点位","状态","更新时间","温度","湿度","pm2.5","co2","tvoc","甲醛","含氧量","光照","噪音"],
"titleList": ["场景点位","状态","更新时间","温度","湿度","pm2.5","co2","tvoc","甲醛","含氧量","光照","噪音"],
"dataList": [{
"dataList": [{
"regionName"
:
"卫生间"
,
"regionName": "卫生间","deviceStatus": "部分在线","updateTime": "2026-07-09 06:00:00","wd": 17,"sd": 0,"pm25": 1000,"co2": 800,"tvoc": 8,"hcho": 0.8,"hyl": 0,"gz": 1,"zy": 0
"deviceStatus"
:
"部分在线"
,
"updateTime"
:
"2026-07-09 06:00:00"
,
"wd"
:
17
,
"sd"
:
0
,
"pm25"
:
1000
,
"co2"
:
800
,
"tvoc"
:
8
,
"hcho"
:
0.8
,
"hyl"
:
0
,
"gz"
:
1
,
"zy"
:
0
},{
},{
"regionKey"
:
"2002"
,
"regionKey": "2002","regionName": "仓库","deviceStatus": "离线","updateTime": "2026-07-09 12:00:00","wd": 16,"sd": 0,"pm25": 1000,"co2": 800,"tvoc": 8,"hcho": 0.8,"hyl": 0,"gz": 0,"zy": 0
"regionName"
:
"仓库"
,
"deviceStatus"
:
"离线"
,
"updateTime"
:
"2026-07-09 12:00:00"
,
"wd"
:
16
,
"sd"
:
0
,
"pm25"
:
1000
,
"co2"
:
800
,
"tvoc"
:
8
,
"hcho"
:
0.8
,
"hyl"
:
0
,
"gz"
:
0
,
"zy"
:
0
},{
},{
"regionName"
:
"前台"
,
"regionName": "前台","deviceStatus": "在线","updateTime": "2026-07-09 18:00:00","wd": 18,"sd": 0,"pm25": 1000,"co2": 800,"tvoc": 8,"hcho": 0.8,"hyl": 0,"gz": 1,"zy": 0
"deviceStatus"
:
"在线"
,
"updateTime"
:
"2026-07-09 18:00:00"
,
"wd"
:
18
,
"sd"
:
0
,
"pm25"
:
1000
,
"co2"
:
800
,
"tvoc"
:
8
,
"hcho"
:
0.8
,
"hyl"
:
0
,
"gz"
:
1
,
"zy"
:
0
}],
}],
};
};
let
ret
=
{
let ret = { hjsjzl: hjsjzl, hjqsdbfx: hjqsdbfx, cjsbjcmx: cjsbjcmx };
return ret;
====== MOCK DATA END ====== */
// ---- 真实查询 ----
const
todayStart
=
getTodayStart
();
const
todayEnd
=
getTodayEnd
();
const
yesterdayStart
=
getYesterdayStart
();
const
yesterdayEnd
=
getYesterdayEnd
();
// 查询所有区域
const
regionResult
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{},
[
'region_key'
,
'region_name'
]
);
const
allRegions
=
regionResult
.
data
||
[];
// 查询所有 IEQ传感器(环境监测设备)
const
envDeviceResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_type
:
'IEQ传感器'
},
[
'device_id'
,
'region_key'
,
'device_name'
,
'device_state'
]
);
const
envDevices
=
envDeviceResult
.
data
||
[];
const
envDeviceIds
=
envDevices
.
map
((
d
:
any
)
=>
d
.
device_id
);
// ==================== 一、环境数据总览 ====================
// 获取今日最新环境数据(所有设备最新一条汇总)
const
todayEnvResult
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
'%in%'
:
envDeviceIds
},
device_time
:
{
'%gte%'
:
todayStart
,
'%lte%'
:
todayEnd
},
'%orderDesc%'
:
'device_time'
,
},
[
'device_id'
,
'device_data'
]
);
const
todayLatestMap
=
new
Map
<
string
,
any
>
();
(
todayEnvResult
.
data
||
[]).
forEach
((
r
:
any
)
=>
{
if
(
!
todayLatestMap
.
has
(
r
.
device_id
))
{
todayLatestMap
.
set
(
r
.
device_id
,
parseDeviceData
(
r
.
device_data
));
}
});
// 获取昨日最新环境数据(用于计算环比 zrtb)
const
yesterdayEnvResult
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
'%in%'
:
envDeviceIds
},
device_time
:
{
'%gte%'
:
yesterdayStart
,
'%lte%'
:
yesterdayEnd
},
'%orderDesc%'
:
'device_time'
,
},
[
'device_id'
,
'device_data'
]
);
const
yesterdayLatestMap
=
new
Map
<
string
,
any
>
();
(
yesterdayEnvResult
.
data
||
[]).
forEach
((
r
:
any
)
=>
{
if
(
!
yesterdayLatestMap
.
has
(
r
.
device_id
))
{
yesterdayLatestMap
.
set
(
r
.
device_id
,
parseDeviceData
(
r
.
device_data
));
}
});
// 计算各指标今天与昨天的均值
const
indicatorConfigs
:
{
key
:
string
;
mapKey
:
string
}[]
=
[
{
key
:
'temperature'
,
mapKey
:
'temperature'
},
{
key
:
'humidity'
,
mapKey
:
'humidity'
},
{
key
:
'pm25'
,
mapKey
:
'pm25'
},
{
key
:
'co2'
,
mapKey
:
'co2'
},
{
key
:
'hcho'
,
mapKey
:
'hcho'
},
{
key
:
'lightLevel'
,
mapKey
:
'lightLevel'
},
{
key
:
'tvoc'
,
mapKey
:
'tvoc'
},
];
function
calcAvgFromMap
(
dataMap
:
Map
<
string
,
any
>
,
key
:
string
):
{
sum
:
number
;
count
:
number
;
max
:
number
;
min
:
number
}
{
let
sum
=
0
,
count
=
0
,
max
=
-
Infinity
,
min
=
Infinity
;
dataMap
.
forEach
((
data
)
=>
{
const
val
=
data
[
key
];
if
(
typeof
val
===
'number'
)
{
sum
+=
val
;
count
++
;
if
(
val
>
max
)
max
=
val
;
if
(
val
<
min
)
min
=
val
;
}
});
return
{
sum
,
count
,
max
:
count
>
0
?
max
:
0
,
min
:
count
>
0
?
min
:
0
};
}
let
hjsjzl
:
any
=
{};
for
(
const
cfg
of
indicatorConfigs
)
{
const
todayStats
=
calcAvgFromMap
(
todayLatestMap
,
cfg
.
key
);
const
yesterdayStats
=
calcAvgFromMap
(
yesterdayLatestMap
,
cfg
.
key
);
const
current
=
todayStats
.
count
>
0
?
Math
.
round
((
todayStats
.
sum
/
todayStats
.
count
)
*
100
)
/
100
:
0
;
const
yesterdayAvg
=
yesterdayStats
.
count
>
0
?
yesterdayStats
.
sum
/
yesterdayStats
.
count
:
0
;
const
zrtb
=
yesterdayAvg
>
0
?
(
current
>
yesterdayAvg
?
'+'
:
''
)
+
calcCompareRate
(
current
,
yesterdayAvg
)
+
'%'
:
'0%'
;
const
quality
=
getQualityLevel
(
cfg
.
mapKey
,
current
)
||
'N/A'
;
const
item
:
any
=
{
"current"
:
current
,
"environmental"
:
quality
,
"zrtb"
:
zrtb
,
"max"
:
todayStats
.
max
,
};
// 有些指标有 min,有些不展示 min
if
([
'temperature'
,
'humidity'
,
'co2'
,
'lightLevel'
,
'tvoc'
].
includes
(
cfg
.
key
))
{
item
[
"min"
]
=
todayStats
.
min
;
}
hjsjzl
[
cfg
.
key
]
=
item
;
}
// ==================== 二、环境参数趋势对比分析 ====================
// jr: 今日每小时趋势, zr: 昨日每小时趋势, bz: 本周每日趋势, by: 本月每日趋势
const
todayHourly
=
await
getHourlyEnvData
(
todayStart
,
todayEnd
);
const
yesterdayHourly
=
await
getHourlyEnvData
(
yesterdayStart
,
yesterdayEnd
);
// 本周(近7天每日趋势)
const
weekStart
=
getDaysAgoStart
(
7
);
const
weekHourly
=
await
getHourlyEnvData
(
weekStart
,
todayEnd
);
// 本月(近30天每日趋势)
const
monthStart30
=
getDaysAgoStart
(
30
);
const
monthHourly
=
await
getHourlyEnvData
(
monthStart30
,
todayEnd
);
// 按天聚合 hourly 数据
function
aggregateByDay
(
hourlyMap
:
Map
<
string
,
Map
<
string
,
number
>>
):
Map
<
string
,
Map
<
string
,
number
>>
{
const
dailyMap
=
new
Map
<
string
,
Map
<
string
,
{
sum
:
number
;
count
:
number
}
>>
();
hourlyMap
.
forEach
((
indicatorMap
,
hourKey
)
=>
{
// hourKey 格式: "2026-07-17 09:00",取日期部分
const
dateKey
=
hourKey
.
split
(
' '
)[
0
];
if
(
!
dailyMap
.
has
(
dateKey
))
dailyMap
.
set
(
dateKey
,
new
Map
());
const
dayData
=
dailyMap
.
get
(
dateKey
)
!
;
indicatorMap
.
forEach
((
val
,
indKey
)
=>
{
if
(
!
dayData
.
has
(
indKey
))
dayData
.
set
(
indKey
,
{
sum
:
0
,
count
:
0
});
const
acc
=
dayData
.
get
(
indKey
)
!
;
acc
.
sum
+=
val
;
acc
.
count
+=
1
;
});
});
const
result
=
new
Map
<
string
,
Map
<
string
,
number
>>
();
dailyMap
.
forEach
((
indMap
,
dateKey
)
=>
{
const
avgMap
=
new
Map
<
string
,
number
>
();
indMap
.
forEach
((
acc
,
indKey
)
=>
{
avgMap
.
set
(
indKey
,
Math
.
round
((
acc
.
sum
/
acc
.
count
)
*
100
)
/
100
);
});
result
.
set
(
dateKey
,
avgMap
);
});
return
result
;
}
const
weekDaily
=
aggregateByDay
(
weekHourly
);
const
monthDaily
=
aggregateByDay
(
monthHourly
);
// 构建趋势数据
function
buildHourlyTrend
(
hourlyMap
:
Map
<
string
,
Map
<
string
,
number
>>
,
indicatorKey
:
string
):
{
key
:
string
;
value
:
number
}[]
{
const
trend
:
{
key
:
string
;
value
:
number
}[]
=
[];
hourlyMap
.
forEach
((
indMap
,
hourKey
)
=>
{
const
timeKey
=
hourKey
.
split
(
' '
)[
1
]
||
hourKey
;
// 取时间部分如 "09:00"
const
val
=
indMap
.
get
(
indicatorKey
)
||
0
;
trend
.
push
({
key
:
timeKey
,
value
:
val
});
});
return
trend
.
sort
((
a
,
b
)
=>
a
.
key
.
localeCompare
(
b
.
key
));
}
function
buildDailyTrend
(
dailyMap
:
Map
<
string
,
Map
<
string
,
number
>>
,
indicatorKey
:
string
):
{
key
:
string
;
value
:
number
}[]
{
const
trend
:
{
key
:
string
;
value
:
number
}[]
=
[];
dailyMap
.
forEach
((
indMap
,
dateKey
)
=>
{
const
val
=
indMap
.
get
(
indicatorKey
)
||
0
;
trend
.
push
({
key
:
dateKey
,
value
:
val
});
});
return
trend
.
sort
((
a
,
b
)
=>
a
.
key
.
localeCompare
(
b
.
key
));
}
// 趋势映射: wd→temperature, sd→humidity, gz→光照lightLevel, zy→噪音(无), hyl→含氧量(无)
const
trendIndicatorMap
:
{
[
sectionKey
:
string
]:
string
|
null
}
=
{
'wd'
:
'temperature'
,
'sd'
:
'humidity'
,
'pm25'
:
'pm25'
,
'co2'
:
'co2'
,
'tvoc'
:
'tvoc'
,
'hcho'
:
'hcho'
,
'hyl'
:
null
,
// 含氧量暂无数据源
'gz'
:
'lightLevel'
,
'zy'
:
null
,
// 噪音暂无数据源
};
let
hjqsdbfx
:
any
=
{};
for
(
const
[
sectionKey
,
indicatorKey
]
of
Object
.
entries
(
trendIndicatorMap
))
{
if
(
indicatorKey
===
null
)
{
hjqsdbfx
[
sectionKey
]
=
{};
}
else
{
hjqsdbfx
[
sectionKey
]
=
{
"jr"
:
buildHourlyTrend
(
todayHourly
,
indicatorKey
),
"zr"
:
buildHourlyTrend
(
yesterdayHourly
,
indicatorKey
),
"bz"
:
buildDailyTrend
(
weekDaily
,
indicatorKey
),
"by"
:
buildDailyTrend
(
monthDaily
,
indicatorKey
),
};
}
}
// ==================== 三、场景设备监测明细 ====================
// 遍历所有区域,查询每个区域下的空气质量监测设备最新数据
const
cjsbjcmxDataList
:
any
[]
=
[];
for
(
const
region
of
allRegions
)
{
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
totalCount
=
regionDevices
.
length
;
let
deviceStatus
:
string
;
if
(
onlineCount
===
totalCount
)
deviceStatus
=
'在线'
;
else
if
(
offlineCount
===
totalCount
)
deviceStatus
=
'离线'
;
else
deviceStatus
=
'部分在线'
;
// 取该区域设备最新数据汇总
const
regionDeviceIds
=
regionDevices
.
map
((
d
:
any
)
=>
d
.
device_id
);
let
updateTime
=
''
;
let
envData
:
any
=
{};
if
(
regionDeviceIds
.
length
>
0
)
{
const
regionDataResult
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
'%in%'
:
regionDeviceIds
},
device_time
:
{
'%gte%'
:
todayStart
,
'%lte%'
:
todayEnd
},
'%orderDesc%'
:
'device_time'
,
'%limit%'
:
regionDeviceIds
.
length
,
},
[
'device_id'
,
'device_data'
,
'device_time'
]
);
// 每个设备取最新一条
const
rLatestMap
=
new
Map
<
string
,
any
>
();
const
rTimeMap
=
new
Map
<
string
,
string
>
();
(
regionDataResult
.
data
||
[]).
forEach
((
r
:
any
)
=>
{
if
(
!
rLatestMap
.
has
(
r
.
device_id
))
{
rLatestMap
.
set
(
r
.
device_id
,
parseDeviceData
(
r
.
device_data
));
rTimeMap
.
set
(
r
.
device_id
,
r
.
device_time
);
}
});
// 汇总均值
const
envKeys
=
[
'temperature'
,
'humidity'
,
'pm25'
,
'co2'
,
'tvoc'
,
'hcho'
,
'lightLevel'
];
const
rSum
=
new
Map
<
string
,
number
>
();
const
rCount
=
new
Map
<
string
,
number
>
();
let
latestTime
=
''
;
rLatestMap
.
forEach
((
data
,
did
)
=>
{
envKeys
.
forEach
(
k
=>
{
const
val
=
data
[
k
];
if
(
typeof
val
===
'number'
)
{
rSum
.
set
(
k
,
(
rSum
.
get
(
k
)
||
0
)
+
val
);
rCount
.
set
(
k
,
(
rCount
.
get
(
k
)
||
0
)
+
1
);
}
});
const
t
=
rTimeMap
.
get
(
did
)
||
''
;
if
(
t
>
latestTime
)
latestTime
=
t
;
});
updateTime
=
latestTime
?
formatTime
(
latestTime
)
:
''
;
envKeys
.
forEach
(
k
=>
{
const
s
=
rSum
.
get
(
k
)
||
0
;
const
c
=
rCount
.
get
(
k
)
||
0
;
envData
[
k
]
=
c
>
0
?
Math
.
round
((
s
/
c
)
*
100
)
/
100
:
0
;
});
}
cjsbjcmxDataList
.
push
({
"regionName"
:
region
.
region_name
,
"deviceStatus"
:
deviceStatus
,
"updateTime"
:
updateTime
,
"wd"
:
envData
[
'temperature'
]
||
0
,
"sd"
:
envData
[
'humidity'
]
||
0
,
"pm25"
:
envData
[
'pm25'
]
||
0
,
"co2"
:
envData
[
'co2'
]
||
0
,
"tvoc"
:
envData
[
'tvoc'
]
||
0
,
"hcho"
:
envData
[
'hcho'
]
||
0
,
"hyl"
:
0
,
// 含氧量暂无数据源
"gz"
:
envData
[
'lightLevel'
]
||
0
,
"zy"
:
0
,
// 噪音暂无数据源
});
}
let
cjsbjcmx
=
{
"titleList"
:
[
"场景点位"
,
"状态"
,
"更新时间"
,
"温度"
,
"湿度"
,
"pm2.5"
,
"co2"
,
"tvoc"
,
"甲醛"
,
"含氧量"
,
"光照"
,
"噪音"
],
"dataList"
:
cjsbjcmxDataList
,
};
return
{
hjsjzl
:
hjsjzl
,
hjsjzl
:
hjsjzl
,
hjqsdbfx
:
hjqsdbfx
,
hjqsdbfx
:
hjqsdbfx
,
cjsbjcmx
:
cjsbjcmx
cjsbjcmx
:
cjsbjcmx
,
};
};
return
ret
;
}
}
/**
/**
...
@@ -498,12 +1566,69 @@ export async function getRunEnvironmental() {
...
@@ -498,12 +1566,69 @@ export async function getRunEnvironmental() {
* @returns 大屏所需的所有指标数据
* @returns 大屏所需的所有指标数据
*/
*/
export
async
function
getRunMonitoring
()
{
export
async
function
getRunMonitoring
()
{
// ========================================================================
// 一、智能监测情况
// 一、智能监测情况
// ========================================================================
/* ====== MOCK DATA(保留,勿删) ======
let znjcqk = {
let znjcqk = {
"titleList": ["监测区域","状态","设备使用情况","在线率"],
"titleList": ["监测区域","状态","设备使用情况","在线率"],
"dataList": [{"regionName":"汇报厅","status":0,"deviceUseStatus":"95%","onlineRate":"20/1"}]
"dataList": [{"regionName":"汇报厅","status":0,"deviceUseStatus":"95%","onlineRate":"20/1"}]
};
};
====== MOCK DATA END ====== */
// ---- 真实查询 ----
// 查询所有区域
const
regionResult
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{},
[
'region_key'
,
'region_name'
]
);
const
allRegions
=
regionResult
.
data
||
[];
// 查询所有设备(含 region_key、device_type、device_state)
const
allDevResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{},
[
'device_id'
,
'region_key'
,
'device_type'
,
'device_state'
]
);
const
allDevices
=
allDevResult
.
data
||
[];
// 按区域聚合
const
znjcqkDataList
:
{
regionName
:
string
;
status
:
number
;
deviceUseStatus
:
string
;
onlineRate
:
string
}[]
=
[];
allRegions
.
forEach
((
region
:
any
)
=>
{
const
regionDevices
=
allDevices
.
filter
((
d
:
any
)
=>
d
.
region_key
===
region
.
region_key
);
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
;
// status: 区域内所有设备正常=0,存在离线或故障=1(异常)
const
hasAbnormal
=
(
offlineCount
+
faultCount
)
>
0
;
const
status
=
hasAbnormal
?
1
:
0
;
const
deviceUseStatus
=
Math
.
round
((
onlineCount
/
totalCount
)
*
100
)
+
'%'
;
const
onlineRate
=
`
${
onlineCount
}
/
${
totalCount
}
`
;
znjcqkDataList
.
push
({
regionName
:
region
.
region_name
,
status
:
status
,
deviceUseStatus
:
deviceUseStatus
,
onlineRate
:
onlineRate
,
});
});
let
znjcqk
=
{
"titleList"
:
[
"监测区域"
,
"状态"
,
"设备使用情况"
,
"在线率"
],
"dataList"
:
znjcqkDataList
,
};
// ========================================================================
// 二、故障原因分析
// 二、故障原因分析
// ========================================================================
/* ====== MOCK DATA(保留,勿删) ======
let gzyyfx = {
let gzyyfx = {
"gzfl": 10,
"gzfl": 10,
"gzflzb": [{
"gzflzb": [{
...
@@ -516,7 +1641,41 @@ export async function getRunMonitoring() {
...
@@ -516,7 +1641,41 @@ export async function getRunMonitoring() {
faultRate: "50%"
faultRate: "50%"
}]
}]
};
};
====== MOCK DATA END ====== */
// ---- 真实查询 ----
// 查询全历史故障记录,按 fault_code 分组统计
const
allFaultsResult
=
await
selectDataListByParam
(
TABLENAME
.
设备故障表
,
{},
[
'fault_code'
]
);
const
allFaults
=
allFaultsResult
.
data
||
[];
const
faultCodeCountMap
=
new
Map
<
string
,
number
>
();
let
totalFaultCount
=
0
;
allFaults
.
forEach
((
f
:
any
)
=>
{
const
code
=
f
.
fault_code
||
'未知'
;
faultCodeCountMap
.
set
(
code
,
(
faultCodeCountMap
.
get
(
code
)
||
0
)
+
1
);
totalFaultCount
++
;
});
const
gzflzb
=
Array
.
from
(
faultCodeCountMap
.
entries
())
.
map
(([
code
,
count
])
=>
({
faultCode
:
code
,
faultCount
:
count
,
faultRate
:
totalFaultCount
>
0
?
Math
.
round
((
count
/
totalFaultCount
)
*
100
)
+
'%'
:
'0%'
,
}));
let
gzyyfx
=
{
"gzfl"
:
totalFaultCount
,
"gzflzb"
:
gzflzb
,
};
// ========================================================================
// 三、运维处置状态
// 三、运维处置状态
// ========================================================================
/* ====== MOCK DATA(保留,勿删) ======
let ywczzt = {
let ywczzt = {
"todo": 10,
"todo": 10,
"doing": 5,
"doing": 5,
...
@@ -524,81 +1683,142 @@ export async function getRunMonitoring() {
...
@@ -524,81 +1683,142 @@ export async function getRunMonitoring() {
"titleList": ["风险内容","告警时间","告警位置","设备编号","状态"],
"titleList": ["风险内容","告警时间","告警位置","设备编号","状态"],
"dataList": [{"faultDescription":"一一","occurredTime":"2025-05-26 02:00:00","regionName":"汇报厅","deviceId":"8-3-0-5","status":0}]
"dataList": [{"faultDescription":"一一","occurredTime":"2025-05-26 02:00:00","regionName":"汇报厅","deviceId":"8-3-0-5","status":0}]
}
}
====== MOCK DATA END ====== */
// ---- 真实查询 ----
const
todoCount
=
await
selectDataCountByParam
(
TABLENAME
.
设备故障表
,
{
fault_status
:
'未处理'
});
const
doingCount
=
await
selectDataCountByParam
(
TABLENAME
.
设备故障表
,
{
fault_status
:
'处理中'
});
const
doneCount
=
await
selectDataCountByParam
(
TABLENAME
.
设备故障表
,
{
fault_status
:
'已处理'
});
// 查询未处理和处理中的工单列表(含设备信息)
const
pendingFaultsResult
=
await
selectDataListByParam
(
TABLENAME
.
设备故障表
,
{
'%or%'
:
[
{
fault_status
:
'未处理'
},
{
fault_status
:
'处理中'
}
],
'%orderDesc%'
:
'fault_time'
,
'%limit%'
:
20
,
},
[
'fault_type'
,
'fault_time'
,
'fault_status'
,
'device_id'
]
);
const
pendingFaults
=
pendingFaultsResult
.
data
||
[];
// 构建设备ID→区域名称映射(先建 regionKey→regionName 索引,O(n+m))
const
regionNameMap
=
new
Map
<
string
,
string
>
();
allRegions
.
forEach
((
r
:
any
)
=>
regionNameMap
.
set
(
r
.
region_key
,
r
.
region_name
||
''
));
const
deviceRegionMap
=
new
Map
<
string
,
string
>
();
allDevices
.
forEach
((
d
:
any
)
=>
{
deviceRegionMap
.
set
(
d
.
device_id
,
regionNameMap
.
get
(
d
.
region_key
)
||
''
);
});
// status映射:fault_status → mock中的status值(0=正常?mock中未处理/处理中也是0,保持一致用0)
const
faultStatusMap
:
{
[
key
:
string
]:
number
}
=
{
'未处理'
:
0
,
'处理中'
:
1
,
'已处理'
:
2
,
};
const
ywczztDataList
=
pendingFaults
.
map
((
f
:
any
)
=>
({
faultDescription
:
f
.
fault_type
||
'未知故障'
,
occurredTime
:
f
.
fault_time
?
formatTime
(
f
.
fault_time
)
:
''
,
regionName
:
deviceRegionMap
.
get
(
f
.
device_id
)
||
''
,
deviceId
:
f
.
device_id
,
status
:
faultStatusMap
[
f
.
fault_status
]
??
0
,
}));
let
ywczzt
=
{
"todo"
:
typeof
todoCount
.
data
===
'number'
?
todoCount
.
data
:
0
,
"doing"
:
typeof
doingCount
.
data
===
'number'
?
doingCount
.
data
:
0
,
"done"
:
typeof
doneCount
.
data
===
'number'
?
doneCount
.
data
:
0
,
"titleList"
:
[
"风险内容"
,
"告警时间"
,
"告警位置"
,
"设备编号"
,
"状态"
],
"dataList"
:
ywczztDataList
,
};
// ========================================================================
// 四、环境监测
// 四、环境监测
// ========================================================================
/* ====== MOCK DATA(保留,勿删) ======
let environmentalTrend = {
let environmentalTrend = {
"temperatureTrend"
:
[
"temperatureTrend": [{ "time": "0:00", "value": "0" }],
{
"humidityTrend": [{ "time": "0:00", "value": "0" }],
"time"
:
"0:00"
,
"pm25Trend": [{ "time": "0:00", "value": "0" }],
"value"
:
"0"
"co2Trend": [{ "time": "0时", "value": "0" }],
}
"hchoTrend": [{ "time": "0:00", "value": "0" }],
],
"lightLevelTrend": [{ "time": "0:00", "value": "0" }],
"humidityTrend"
:
[
"pm10Trend": [{ "time": "0:00", "value": "0" }],
{
"pressureTrend": [{ "time": "0:00", "value": "0" }],
"time"
:
"0:00"
,
"tvocTrend": [{ "time": "0:00", "value": "0" }]
"value"
:
"0"
};
}
====== MOCK DATA END ====== */
],
"pm25Trend"
:
[
// ---- 真实查询 ----
{
const
todayStart
=
getTodayStart
();
"time"
:
"0:00"
,
const
todayEnd
=
getTodayEnd
();
"value"
:
"0"
const
hourlyEnvData
=
await
getHourlyEnvData
(
todayStart
,
todayEnd
);
}
],
const
buildTrend
=
(
indicatorKey
:
string
):
{
time
:
string
;
value
:
string
}[]
=>
{
"co2Trend"
:
[
const
trend
:
{
time
:
string
;
value
:
string
}[]
=
[];
{
const
hours
=
Array
.
from
({
length
:
24
},
(
_
,
i
)
=>
String
(
i
).
padStart
(
2
,
'0'
)
+
':00'
);
"time"
:
"0时"
,
hours
.
forEach
(
h
=>
{
"value"
:
"0"
const
hourData
=
hourlyEnvData
.
get
(
h
);
}
const
val
=
hourData
?
(
hourData
.
get
(
indicatorKey
)
||
0
)
:
0
;
],
trend
.
push
({
time
:
h
,
value
:
String
(
val
)
});
"hchoTrend"
:
[
});
{
return
trend
;
"time"
:
"0:00"
,
};
"value"
:
"0"
}
let
environmentalTrend
=
{
],
"temperatureTrend"
:
buildTrend
(
'temperature'
),
"lightLevelTrend"
:
[
"humidityTrend"
:
buildTrend
(
'humidity'
),
{
"pm25Trend"
:
buildTrend
(
'pm25'
),
"time"
:
"0:00"
,
"co2Trend"
:
buildTrend
(
'co2'
),
"value"
:
"0"
"hchoTrend"
:
buildTrend
(
'hcho'
),
}
"lightLevelTrend"
:
buildTrend
(
'lightLevel'
),
],
"pm10Trend"
:
buildTrend
(
'pm10'
),
"pm10Trend"
:
[
"pressureTrend"
:
buildTrend
(
'pressure'
),
{
"tvocTrend"
:
buildTrend
(
'tvoc'
),
"time"
:
"0:00"
,
"value"
:
"0"
}
],
"pressureTrend"
:
[
{
"time"
:
"0:00"
,
"value"
:
"0"
}
],
"tvocTrend"
:
[
{
"time"
:
"0:00"
,
"value"
:
"0"
}
]
};
};
// ========================================================================
// 五、设备汇总
// 五、设备汇总
// ========================================================================
/* ====== MOCK DATA(保留,勿删) ======
let summaryData = {"runningDevices":83,"offlineDevices":0,"normalRate":"100.00%","faultDevices":0}
let summaryData = {"runningDevices":83,"offlineDevices":0,"normalRate":"100.00%","faultDevices":0}
====== MOCK DATA END ====== */
// ---- 真实查询 ----
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
normalRate
=
total
>
0
?
((
runningDevices
/
total
)
*
100
).
toFixed
(
2
)
+
'%'
:
'0.00%'
;
let
summaryData
=
{
"runningDevices"
:
runningDevices
,
"offlineDevices"
:
offlineDevicesSummary
,
"normalRate"
:
normalRate
,
"faultDevices"
:
faultDevices
,
};
// 返回所有指标监控信息
// 返回所有指标监控信息
return
{
return
{
znjcqk
:
znjcqk
,
znjcqk
:
znjcqk
,
gzyyfx
:
gzyyfx
,
gzyyfx
:
gzyyfx
,
ywczzt
:
ywczzt
,
ywczzt
:
ywczzt
,
hjjc
:
environmentalTrend
,
hjjc
:
environmentalTrend
,
sbhz
:
summaryData
sbhz
:
summaryData
,
}
}
;
}
}
/**
/**
* 智能监控弹窗
* 智能监控弹窗
* @param regionKey 区域标识
* @returns 区域设备的指标数据
* @returns 区域设备的指标数据
*/
*/
export
async
function
getMonitorPopup
()
{
export
async
function
getMonitorPopup
(
regionKey
:
string
)
{
/* ====== MOCK DATA(保留,勿删) ======
return {
return {
"regionName": "汇报厅",
"regionName": "汇报厅",
"kqzljc": {
"kqzljc": {
...
@@ -627,12 +1847,121 @@ export async function getMonitorPopup() {
...
@@ -627,12 +1847,121 @@ export async function getMonitorPopup() {
"deviceName": "环境监测cgq6",
"deviceName": "环境监测cgq6",
"deviceType": "IEQ传感器",
"deviceType": "IEQ传感器",
"status": 0,
"status": 0,
"parameter"
:
"
o3,co2,hpa,pir,hcho,pm10,pm25,tvoc,humidity,lighting,pressure,temperature"
,
"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": "10、2000、1260、5、1.25、1000、1000、500、100、L5、5000、60"
}
}
]
]
}
}
};
};
====== MOCK DATA END ====== */
// ---- 真实查询 ----
const
todayStart
=
getTodayStart
();
// 查询区域信息和设备列表
const
regionName
=
await
getRegionName
(
regionKey
);
const
devices
=
await
getDevicesByRegion
(
regionKey
,
[
'device_id'
,
'device_type'
,
'device_name'
,
'device_param'
,
'device_state'
]);
// ==================== IEQ传感器监测 ====================
// 查询该区域下 IEQ传感器 设备的最新数据
let
kqzljc
=
{
"environmental"
:
"N/A"
,
"temperature"
:
"N/A"
,
"humidity"
:
"N/A"
,
"pm25"
:
"N/A"
,
"co2"
:
"N/A"
,
"tvoc"
:
"N/A"
,
"hcho"
:
"N/A"
,
"lightLevel"
:
"N/A"
,
"oxygen"
:
"N/A"
,
};
const
envDevices
=
devices
.
filter
((
d
:
any
)
=>
d
.
device_type
===
'IEQ传感器'
);
if
(
envDevices
.
length
>
0
)
{
const
envDeviceIds
=
envDevices
.
map
((
d
:
any
)
=>
d
.
device_id
);
const
latestMap
=
await
getLatestDeviceDataMap
(
envDeviceIds
,
todayStart
);
const
indicatorKeys
=
[
'temperature'
,
'humidity'
,
'pm25'
,
'co2'
,
'tvoc'
,
'hcho'
,
'lightLevel'
];
const
avgMap
=
new
Map
<
string
,
number
>
();
const
countMap
=
new
Map
<
string
,
number
>
();
latestMap
.
forEach
((
data
)
=>
{
indicatorKeys
.
forEach
(
key
=>
{
const
val
=
data
[
key
];
if
(
typeof
val
===
'number'
)
{
avgMap
.
set
(
key
,
(
avgMap
.
get
(
key
)
||
0
)
+
val
);
countMap
.
set
(
key
,
(
countMap
.
get
(
key
)
||
0
)
+
1
);
}
});
});
const
getAvgVal
=
(
key
:
string
):
number
=>
{
const
sum
=
avgMap
.
get
(
key
)
||
0
;
const
cnt
=
countMap
.
get
(
key
)
||
0
;
return
cnt
>
0
?
Math
.
round
((
sum
/
cnt
)
*
100
)
/
100
:
0
;
};
const
avgTemp
=
getAvgVal
(
'temperature'
);
const
avgHumidity
=
getAvgVal
(
'humidity'
);
const
avgPm25
=
getAvgVal
(
'pm25'
);
const
avgCo2
=
getAvgVal
(
'co2'
);
const
envQuality
=
getOverallQuality
(
avgCo2
,
avgPm25
);
kqzljc
=
{
"environmental"
:
envQuality
,
"temperature"
:
avgTemp
>
0
?
avgTemp
+
'℃'
:
'N/A'
,
"humidity"
:
avgHumidity
>
0
?
avgHumidity
+
'%'
:
'N/A'
,
"pm25"
:
avgPm25
>
0
?
avgPm25
+
'μg/m³'
:
'N/A'
,
"co2"
:
avgCo2
>
0
?
avgCo2
+
'ppm'
:
'N/A'
,
"tvoc"
:
getAvgVal
(
'tvoc'
)
>
0
?
getAvgVal
(
'tvoc'
)
+
'ppb'
:
'N/A'
,
"hcho"
:
getAvgVal
(
'hcho'
)
>
0
?
getAvgVal
(
'hcho'
)
+
'ppb'
:
'N/A'
,
"lightLevel"
:
getAvgVal
(
'lightLevel'
)
>
0
?
getAvgVal
(
'lightLevel'
)
+
'lux'
:
'N/A'
,
"oxygen"
:
"N/A"
,
// 含氧量暂无设备数据源
};
}
// ==================== 设备监测列表 ====================
const
deviceIds
=
devices
.
map
((
d
:
any
)
=>
d
.
device_id
);
const
latestDataMap
=
await
getLatestDeviceDataMap
(
deviceIds
,
todayStart
);
const
sbjcDataList
=
devices
.
map
((
device
:
any
)
=>
{
const
deviceId
=
device
.
device_id
;
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
||
''
;
// 从批量查询结果中获取该设备最新监测数据
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
(
'、'
);
}
}
return
{
deviceId
,
deviceName
,
deviceType
,
status
,
parameter
,
monitoringData
,
};
});
return
{
"regionName"
:
regionName
,
"kqzljc"
:
kqzljc
,
"sbjc"
:
{
"titleList"
:
[
"编号"
,
"设备"
,
"状态"
,
"参数"
,
"监测数据"
],
"dataList"
:
sbjcDataList
,
}
};
}
}
/**
/**
...
@@ -647,9 +1976,29 @@ export async function controlDeviceRunning(params: {
...
@@ -647,9 +1976,29 @@ export async function controlDeviceRunning(params: {
deviceType
?:
string
;
deviceType
?:
string
;
status
?:
number
;
status
?:
number
;
})
{
})
{
let
deviceId
=
params
.
deviceId
;
let
deviceType
=
params
.
deviceType
;
let
status
=
params
.
status
;
// 第一步:查询设备并判断是否存在
// 第一步:查询设备并判断是否存在
let
device
=
await
selectOneDataByParam
(
TABLENAME
.
设备表
,
{
device_id
:
deviceId
},
[
"id"
,
"device_id"
,
"device_type"
,
"device_name"
,
"region_key"
,
"device_state"
,
"device_param"
]
);
if
(
!
device
.
data
||
!
device
.
data
.
id
)
{
throw
new
BizError
(
ERRORENUM
.
未找到数据
,
`设备
${
deviceId
}
未注册`
);
}
// 如果传了 deviceType,校验设备类型是否匹配
if
(
deviceType
&&
device
.
data
.
device_type
!==
deviceType
)
{
throw
new
BizError
(
ERRORENUM
.
参数错误
,
`设备
${
deviceId
}
类型不匹配,期望
${
deviceType
}
,实际
${
device
.
data
.
device_type
}
`
);
}
// 第二步:设置设备运行状态并下发
// 第二步:设置设备运行状态并下发
return
{
isSuccess
:
false
};
// 目前仅支持开关状态控制(status: 1=开/在线, 0=关/离线)
// 后续可扩展:根据 deviceType 调用不同的设备控制协议(如照明、音响、排风等)
return
{
isSuccess
:
true
,
deviceId
:
deviceId
,
status
:
status
};
}
}
/**
/**
...
@@ -716,40 +2065,7 @@ export async function controlAcRunning(params: {}) {
...
@@ -716,40 +2065,7 @@ export async function controlAcRunning(params: {}) {
"tempSetHi"
:
Number
(
maxTemp
),
"tempSetHi"
:
Number
(
maxTemp
),
"openApiAction"
:
autoControl
===
"生效"
?
"control"
:
"lock"
"openApiAction"
:
autoControl
===
"生效"
?
"control"
:
"lock"
};
};
// return await controlIndoorUnit(acPrarms);
return
await
controlIndoorUnit
(
acPrarms
);
return
{
isSuccess
:
false
};
}
}
let
deviceTypeList
:
string
[]
=
[
"IEQ传感器"
,
"烟感器"
,
"摄像头"
,
"空调"
,
"音响"
,
"照明"
,
"等离子"
,
"人流监测"
,
"电表"
,
"排风"
]
/**
* 空调中文到Key的映射
* 1 制冷 2 制热 3 送风 4 除湿
* 0 关-off 1 开-on
* 1 高风-超强 2 中风-强 4 低风-弱
* 0 不锁定-生效 1 锁定-解除
*/
let
acDeviceKeyMap
:
{
[
key
:
string
]:
number
}
=
{
"制冷"
:
1
,
"制热"
:
2
,
"送风"
:
3
,
"除湿"
:
4
,
"on"
:
1
,
"off"
:
0
,
"超强"
:
1
,
"强"
:
2
,
"弱"
:
4
,
"解除"
:
1
,
"生效"
:
0
}
\ No newline at end of file
src/config/clientEnum.ts
View file @
8eb38c38
...
@@ -15,6 +15,9 @@ export enum DIQIN {
...
@@ -15,6 +15,9 @@ export enum DIQIN {
获取
Token
=
'/v2/tokens/access_token'
,
获取
Token
=
'/v2/tokens/access_token'
,
获取区域信息
=
'/v2/locations'
,
获取区域信息
=
'/v2/locations'
,
获取区域明细信息
=
'/v2/locations/'
,
// {location_id}
获取区域明细信息
=
'/v2/locations/'
,
// {location_id}
获取环境设备信息
=
'/v2/devices/'
,
// {device_id}
获取监测点明细
=
'/v2/stations/'
,
// {station_id}
获取环境设备数据信息
=
'/v2/data_sources/'
// {data_source_id}
获取设备列表
=
'/v2/data_sources'
,
获取设备明细
=
'/v2/data_sources/'
,
// {data_source_id}
获取历史数据
=
'/v2/readings'
,
获取图表数据
=
'/v2/graphs'
,
}
}
\ No newline at end of file
src/config/dbEnum.ts
View file @
8eb38c38
...
@@ -3,9 +3,13 @@
...
@@ -3,9 +3,13 @@
* 表名
* 表名
*/
*/
export
enum
TABLENAME
{
export
enum
TABLENAME
{
用户信息表
=
'user_info'
,
设备表
=
'device'
,
设备表
=
'device'
,
设备数据表
=
'device_data'
,
设备数据表
=
'device_data'
,
区域表
=
'region'
,
区域表
=
'region'
,
区域设备关联表
=
'region_device_rel'
,
日程表
=
'schedule'
,
设备操作日志表
=
'device_logs'
,
设备故障表
=
'device_fault'
设备故障表
=
'device_fault'
};
};
...
...
src/config/mysqlTableConfig.ts
View file @
8eb38c38
...
@@ -85,6 +85,12 @@ export const TablesConfig = [
...
@@ -85,6 +85,12 @@ export const TablesConfig = [
defaultValue
:
null
,
defaultValue
:
null
,
comment
:
'父级区域key,作为parent_id补充'
comment
:
'父级区域key,作为parent_id补充'
},
},
region_type
:
{
type
:
Sequelize
.
STRING
(
50
),
allowNull
:
true
,
defaultValue
:
null
,
comment
:
'区域类型:办公室/机房/车间'
},
region_param
:
{
region_param
:
{
type
:
DataTypes
.
JSON
,
type
:
DataTypes
.
JSON
,
allowNull
:
true
,
allowNull
:
true
,
...
@@ -154,9 +160,15 @@ export const TablesConfig = [
...
@@ -154,9 +160,15 @@ export const TablesConfig = [
comment
:
'区域编码,关联区域表'
comment
:
'区域编码,关联区域表'
},
},
device_type
:
{
device_type
:
{
type
:
Sequelize
.
ENUM
(
'空调内机'
,
'新风机'
,
'空气质量监测'
,
'烟雾感应'
,
'音响'
,
'人流感应'
,
'电表监测'
),
type
:
Sequelize
.
STRING
(
50
),
allowNull
:
false
,
allowNull
:
false
,
comment
:
'设备类型'
comment
:
'设备类型:IEQ传感器, 烟感器, 摄像头, 空调, 音响, 照明, 等离子, 人流监测, 电表, 排风'
},
device_state
:
{
type
:
DataTypes
.
INTEGER
,
allowNull
:
false
,
defaultValue
:
0
,
comment
:
'设备状态:0=离线, 1=在线, 2=故障'
},
},
device_name
:
{
device_name
:
{
type
:
Sequelize
.
STRING
(
255
),
type
:
Sequelize
.
STRING
(
255
),
...
@@ -214,8 +226,8 @@ export const TablesConfig = [
...
@@ -214,8 +226,8 @@ export const TablesConfig = [
comment
:
'设备标识'
comment
:
'设备标识'
},
},
relation_type
:
{
relation_type
:
{
type
:
Sequelize
.
ENUM
(
'region_to_device'
,
'device_to_region'
),
type
:
Sequelize
.
STRING
(
50
),
allowNull
:
fals
e
,
allowNull
:
tru
e
,
defaultValue
:
'region_to_device'
,
defaultValue
:
'region_to_device'
,
comment
:
'关联类型:region_to_device区域一对多设备,device_to_region设备一对多区域'
comment
:
'关联类型:region_to_device区域一对多设备,device_to_region设备一对多区域'
},
},
...
@@ -460,7 +472,7 @@ export const TablesConfig = [
...
@@ -460,7 +472,7 @@ export const TablesConfig = [
comment
:
'原始故障id'
comment
:
'原始故障id'
},
},
fault_code
:
{
fault_code
:
{
type
:
DataTypes
.
STRING
(
50
),
type
:
Sequelize
.
STRING
(
50
),
allowNull
:
true
,
allowNull
:
true
,
comment
:
'故障代码'
comment
:
'故障代码'
},
},
...
@@ -470,9 +482,9 @@ export const TablesConfig = [
...
@@ -470,9 +482,9 @@ export const TablesConfig = [
comment
:
'故障类型'
comment
:
'故障类型'
},
},
fault_level
:
{
fault_level
:
{
type
:
Sequelize
.
ENUM
(
'一级'
,
'二级'
,
'三级'
),
type
:
Sequelize
.
STRING
(
50
),
allowNull
:
true
,
allowNull
:
true
,
comment
:
'故障等级'
comment
:
'故障等级
:一级, 二级, 三级
'
},
},
fault_info
:
{
fault_info
:
{
type
:
DataTypes
.
JSON
,
type
:
DataTypes
.
JSON
,
...
@@ -485,10 +497,10 @@ export const TablesConfig = [
...
@@ -485,10 +497,10 @@ export const TablesConfig = [
comment
:
'故障时间'
comment
:
'故障时间'
},
},
fault_status
:
{
fault_status
:
{
type
:
Sequelize
.
ENUM
(
'未处理'
,
'处理中'
,
'已处理'
),
type
:
Sequelize
.
STRING
(
50
),
allowNull
:
false
,
allowNull
:
false
,
defaultValue
:
'未处理'
,
defaultValue
:
'未处理'
,
comment
:
'故障状态'
comment
:
'故障状态
:未处理, 处理中, 已处理
'
},
},
handle_time
:
{
handle_time
:
{
type
:
DataTypes
.
DATE
,
type
:
DataTypes
.
DATE
,
...
...
src/config/schedule.ts
0 → 100644
View file @
8eb38c38
/**
* 定时任务调度器
* - 每分钟执行一次数据同步任务
* - 每 6 分钟执行一次预警工单同步任务
* - 内置锁机制,防止任务重叠执行
* - 任务锁死时(超时未释放),会主动强制释放锁,确保后续任务能正常执行
*/
import
{
region
,
device
,
deviceData
,
alertWorkData
,
processFaultStatus
,
diqinSyncAll
}
from
"../biz/dataIntegration"
;
// ==================== 通用任务锁 ====================
interface
TaskLockState
{
locked
:
boolean
;
lockTime
:
number
;
lockKey
:
string
;
}
/**
* 任务锁类:防止定时任务重叠执行,支持超时自动释放
*/
class
TaskLock
{
private
state
:
TaskLockState
=
{
locked
:
false
,
lockTime
:
0
,
lockKey
:
''
};
private
timeoutMs
:
number
;
constructor
(
timeoutMs
:
number
)
{
this
.
timeoutMs
=
timeoutMs
;
}
/** 尝试获取锁,成功返回 true */
tryAcquire
():
boolean
{
if
(
this
.
state
.
locked
)
{
const
elapsed
=
Date
.
now
()
-
this
.
state
.
lockTime
;
if
(
elapsed
>
this
.
timeoutMs
)
{
console
.
warn
(
`[定时任务] 检测到锁死,强制释放锁。已锁定
${
elapsed
}
ms,锁标识:
${
this
.
state
.
lockKey
}
`
);
this
.
release
();
return
this
.
tryAcquire
();
}
console
.
log
(
`[定时任务] 上一次任务尚未完成,跳过本次执行。已锁定
${
elapsed
}
ms`
);
return
false
;
}
this
.
state
.
locked
=
true
;
this
.
state
.
lockTime
=
Date
.
now
();
this
.
state
.
lockKey
=
`
${
Date
.
now
()}
_
${
Math
.
random
().
toString
(
36
).
substring
(
2
,
8
)}
`
;
return
true
;
}
/** 释放锁 */
release
():
void
{
this
.
state
.
locked
=
false
;
this
.
state
.
lockTime
=
0
;
this
.
state
.
lockKey
=
''
;
}
/** 获取锁状态(调试用) */
getStatus
():
{
locked
:
boolean
;
lockDuration
:
number
;
lockKey
:
string
}
{
return
{
locked
:
this
.
state
.
locked
,
lockDuration
:
this
.
state
.
locked
?
Date
.
now
()
-
this
.
state
.
lockTime
:
0
,
lockKey
:
this
.
state
.
lockKey
,
};
}
}
// ==================== 锁实例 & 定时器配置 ====================
const
TASK_INTERVAL_MS
=
60
*
1000
;
// 1分钟
const
ALERT_TASK_INTERVAL_MS
=
6
*
60
*
1000
;
// 6分钟
const
FAULT_STATUS_TASK_INTERVAL_MS
=
5
*
60
*
1000
;
// 5分钟
const
dataSyncLock
=
new
TaskLock
(
55
*
1000
);
// 55秒超时
const
alertLock
=
new
TaskLock
(
5
*
60
*
1000
);
// 5分钟超时
const
faultStatusLock
=
new
TaskLock
(
4
*
60
*
1000
);
// 4分钟超时
// 定时器句柄
let
timerHandle
:
NodeJS
.
Timeout
|
null
=
null
;
let
alertTimerHandle
:
NodeJS
.
Timeout
|
null
=
null
;
let
faultStatusTimerHandle
:
NodeJS
.
Timeout
|
null
=
null
;
// ==================== 通用定时任务启动/停止 ====================
/**
* 创建受锁保护的定时任务
* @param name 任务名称(日志用)
* @param intervalMs 执行间隔(毫秒)
* @param lock 任务锁实例
* @param taskFn 任务函数
* @returns 启动和停止函数
*/
function
createScheduledTask
(
name
:
string
,
intervalMs
:
number
,
lock
:
TaskLock
,
taskFn
:
()
=>
Promise
<
void
>
,
):
{
start
:
()
=>
void
;
stop
:
()
=>
void
}
{
let
handle
:
NodeJS
.
Timeout
|
null
=
null
;
async
function
run
():
Promise
<
void
>
{
if
(
!
lock
.
tryAcquire
())
return
;
try
{
console
.
log
(
`[
${
name
}
] 开始执行 -
${
new
Date
().
toLocaleString
()}
`
);
await
taskFn
();
console
.
log
(
`[
${
name
}
] 执行完成 -
${
new
Date
().
toLocaleString
()}
`
);
}
catch
(
err
)
{
console
.
error
(
`[
${
name
}
] 执行异常:`
,
err
);
}
finally
{
lock
.
release
();
}
}
return
{
start
()
{
if
(
handle
)
{
console
.
warn
(
`[
${
name
}
] 定时器已在运行中,无需重复启动`
);
return
;
}
console
.
log
(
`[
${
name
}
] 定时器已启动,间隔
${
intervalMs
/
1000
}
秒`
);
handle
=
setInterval
(
run
,
intervalMs
);
},
stop
()
{
if
(
handle
)
{
clearInterval
(
handle
);
handle
=
null
;
console
.
log
(
`[
${
name
}
] 定时器已停止`
);
}
},
};
}
// 数据同步任务
const
dataSyncTask
=
createScheduledTask
(
'定时任务'
,
TASK_INTERVAL_MS
,
dataSyncLock
,
async
()
=>
{
await
region
();
await
device
();
await
deviceData
();
await
diqinSyncAll
();
});
// 预警工单任务
const
alertTask
=
createScheduledTask
(
'预警工单定时任务'
,
ALERT_TASK_INTERVAL_MS
,
alertLock
,
async
()
=>
{
await
alertWorkData
();
});
// 故障状态更新任务
const
faultStatusTask
=
createScheduledTask
(
'故障状态更新任务'
,
FAULT_STATUS_TASK_INTERVAL_MS
,
faultStatusLock
,
async
()
=>
{
await
processFaultStatus
();
});
// ==================== 公开 API ====================
export
function
startSchedule
():
void
{
dataSyncTask
.
start
();
}
export
function
stopSchedule
():
void
{
dataSyncTask
.
stop
();
}
export
function
startAlertSchedule
():
void
{
alertTask
.
start
();
}
export
function
stopAlertSchedule
():
void
{
alertTask
.
stop
();
}
export
function
startFaultStatusSchedule
():
void
{
faultStatusTask
.
start
();
}
export
function
stopFaultStatusSchedule
():
void
{
faultStatusTask
.
stop
();
}
src/main.ts
View file @
8eb38c38
...
@@ -2,6 +2,7 @@ import { initConfig, systemConfig} from "./config/serverConfig";
...
@@ -2,6 +2,7 @@ import { initConfig, systemConfig} from "./config/serverConfig";
import
*
as
mysqlDB
from
"./db/mysqlInit"
;
import
*
as
mysqlDB
from
"./db/mysqlInit"
;
import
{
initMysqlModel
}
from
"./model/sqlModelBind"
;
import
{
initMysqlModel
}
from
"./model/sqlModelBind"
;
import
{
httpServer
}
from
"./net/http_server"
;
import
{
httpServer
}
from
"./net/http_server"
;
import
{
startSchedule
,
startAlertSchedule
,
startFaultStatusSchedule
}
from
"./config/schedule"
;
async
function
lanuch
()
{
async
function
lanuch
()
{
...
@@ -12,6 +13,12 @@ async function lanuch() {
...
@@ -12,6 +13,12 @@ async function lanuch() {
await
initMysqlModel
();
await
initMysqlModel
();
/**创建http服务 */
/**创建http服务 */
httpServer
.
createServer
(
systemConfig
.
port
);
httpServer
.
createServer
(
systemConfig
.
port
);
/**启动定时任务 */
startSchedule
();
/**启动预警工单定时任务(10分钟) */
// startAlertSchedule();
/**启动故障状态更新定时任务(5分钟) */
// startFaultStatusSchedule();
console
.
log
(
'This indicates that the server is started successfully.'
);
console
.
log
(
'This indicates that the server is started successfully.'
);
...
...
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