Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Q
qingdaoMuseumPlatform
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
qingdaoMuseumPlatform
Commits
d6c9a9f9
Commit
d6c9a9f9
authored
Jun 11, 2026
by
PC-20251223ZVQQ\Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
数据集成-电表关联区域;运行分析客流监控和空气质量优化
parent
ae21dd9b
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
209 additions
and
17 deletions
+209
-17
package-lock.json
package-lock.json
+0
-0
package.json
package.json
+1
-0
电表对应区域.xlsx
res/电表对应区域.xlsx
+0
-0
dataIntegration.ts
src/biz/dataIntegration.ts
+164
-0
running.ts
src/biz/running.ts
+40
-17
mysqlTableConfig.ts
src/config/mysqlTableConfig.ts
+4
-0
No files found.
package-lock.json
View file @
d6c9a9f9
This diff is collapsed.
Click to expand it.
package.json
View file @
d6c9a9f9
...
...
@@ -31,6 +31,7 @@
"
tencentcloud-sdk-nodejs
"
:
"^4.0.562"
,
"
winston
"
:
"^3.17.0"
,
"
ws
"
:
"^5.2.2"
,
"
xlsx
"
:
"^0.18.5"
,
"
xml2js
"
:
"^0.4.23"
,
"
xmlrpc
"
:
"^1.3.2"
},
...
...
res/电表对应区域.xlsx
0 → 100644
View file @
d6c9a9f9
File added
src/biz/dataIntegration.ts
View file @
d6c9a9f9
...
...
@@ -7,6 +7,8 @@ import {
}
from
"./feiyiClient"
;
import
{
mysqlModelMap
}
from
"../model/sqlModelBind"
;
import
moment
from
"moment"
;
import
XLSX
from
"xlsx"
;
import
path
from
"path"
;
// ==================== 工具函数 ====================
...
...
@@ -17,6 +19,42 @@ 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
;
}
/**
* 从 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
;
}
// ==================== 分页拉取辅助函数 ====================
/**
...
...
@@ -150,11 +188,137 @@ export async function region() {
}
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
);
}
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 → regionId 映射(复用已更新 address 的内存数据)
const
addrToRegionId
=
new
Map
<
string
,
number
>
();
for
(
const
r
of
allRegions
)
{
if
(
r
.
address
)
{
addrToRegionId
.
set
(
r
.
address
,
r
.
id
);
}
}
console
.
log
(
`[同步电表region_key] region.address 映射共
${
addrToRegionId
.
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
regionId
=
addrToRegionId
.
get
(
outerAddr
);
if
(
!
regionId
)
{
missCount
++
;
continue
;
}
// region_key 已正确则跳过
if
(
device
.
region_key
===
regionId
)
continue
;
await
deviceModel
.
update
(
{
region_key
:
regionId
},
{
where
:
{
id
:
device
.
id
}
},
);
updateCount
++
;
}
console
.
log
(
`[同步电表region_key] 完成,更新
${
updateCount
}
条,未匹配
${
missCount
}
条`
);
}
catch
(
err
)
{
console
.
error
(
'[同步电表region_key] 执行异常:'
,
err
);
}
}
// ==================== 设备集成 ====================
/** 空调控制参数模板(按设备数据结构.md) */
...
...
src/biz/running.ts
View file @
d6c9a9f9
...
...
@@ -128,13 +128,17 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
let
airQualityDevices
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
airParams
,
[
"device_id"
]);
let
airDeviceIds
=
airQualityDevices
.
data
.
map
(
d
=>
d
.
device_id
);
let
qualityIndex
=
300
;
// 今日环境质量
let
todayEnvironmental
=
'优秀'
;
let
currentCo2
=
0
;
// 当前二氧化氮
let
co2Trend
=
[];
let
currentCo2
=
'0ppm'
;
let
currentTemperature
=
'0℃'
;
let
currentTemperature
=
'0℃'
;
// 当前温度
let
temperatureTrend
=
[];
let
currentHumidity
=
'0%'
;
let
currentHumidity
=
'0%'
;
// 当前湿度
let
humidityTrend
=
[];
let
currentPm25
=
'0μg/m³'
;
let
currentPm25
=
0
;
// 当前PM2.5
let
pm25Trend
=
[];
let
co2TrendDetail
=
[];
...
...
@@ -149,8 +153,8 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
let
airData
=
latestAir
.
data
[
0
].
data
;
currentTemperature
=
`
${
airData
.
temperature
??
0
}
℃
`;
currentHumidity = `
$
{
airData
.
humidity
??
0
}
%
`;
currentPm25 =
`
$
{
airData
.
pm25
??
0
}
μ
g
/
m
³
`
;
currentCo2 =
`
$
{
airData
.
co2
??
0
}
ppm
`
;
currentPm25 =
airData.pm25
;
currentCo2 =
airData.co2 ?? 0
;
qualityIndex = 500 - (airData.pm25 ?? 0);
}
// 过去24小时趋势
...
...
@@ -179,12 +183,27 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
co2Trend.push({ time: `
$
{
i
}
时`
,
value
:
co2Val
.
toString
()
});
}
}
// 计算今日环境质量
if
(
currentCo2
&&
currentPm25
)
{
// 优秀:CO2< 800ppm; PM2.5≤35; 甲醛≤0.055
todayEnvironmental
=
currentCo2
<
800
&&
currentPm25
<=
35
?
'优秀'
:
// 优秀:CO2< 1000ppm; PM2.5≤45; TVOC≤0.4
currentCo2
<
1000
&&
currentPm25
<=
45
?
'优秀'
:
// 良好:CO2≤ 1000ppm; PM2.5≤50; 甲醛≤0.08
currentCo2
<=
1000
&&
currentPm25
<=
50
?
'良好'
:
// 一般:CO2:1000~1500; PM2.5:50~75
currentCo2
>
1000
&&
currentCo2
<
1500
&&
currentPm25
>
50
&&
currentPm25
<
75
?
'一般'
:
'差'
}
// 4.2. 环境监测趋势图数据
resultAny
.
environmentalTrend
=
{
todayEnvironmental
:
todayEnvironmental
,
currentTemperature
:
currentTemperature
,
currentHumidity
:
currentHumidity
,
currentPm25: currentPm25,
currentCo2: currentCo2,
currentPm25
:
currentPm25
+
'μg/m³'
,
currentCo2
:
currentCo2
+
'ppm'
,
temperatureTrend
:
temperatureTrend
,
humidityTrend
:
humidityTrend
,
pm25Trend
:
pm25Trend
,
...
...
@@ -234,10 +253,10 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
let
regionInfos
:
any
=
[];
if
(
regionKeys
.
length
===
0
)
{
// 6.1.1 若没有选择区域,则查询区域表获取所有区域类型
regionInfos = await selectDataListByParam(TABLENAME.区域表, {}, ["id", "
name", "type
"]);
regionInfos
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{},
[
"id"
,
"
room_id"
,
"name"
,
"type"
,
"groups
"
]);
}
else
{
// 6.1.2 若选择了区域,则查询选择的区域
regionInfos = await selectDataListByParam(TABLENAME.区域表, { id: { "%in%": regionKeys } }, ["id", "
name", "type
"]);
regionInfos
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{
id
:
{
"%in%"
:
regionKeys
}
},
[
"id"
,
"
room_id"
,
"name"
,
"type"
,
"groups
"
]);
}
// 6.2 循环区域查询客流传感器设备,获取过去24小时每个区域的客流量
...
...
@@ -246,12 +265,20 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
let
regionCustomerMap
=
new
Map
();
// {regionName: customerNum}
let
regionCustomerTable
=
[];
// [{regionName: A馆, customerNum: 5000, saturation: '80%'}]
for
(
let
regionInfo
of
regionInfos
.
data
)
{
// 区分楼栋与楼层
let
regionName
=
regionKeys
.
length
===
0
?
regionInfo
.
groups
:
regionInfo
.
type
;
// 如果是按区域类型统计,则最大承载量取该类型下所有区域的总和;如果是按区域名称统计,则取该区域的最大承载量
let
maxCapacity
=
regionKeys
.
length
===
0
?
regionMaxCustomerMap
.
get
(
regionName
)
:
regionInfo
.
max_capacity
;
regionMaxCustomerMap
.
set
(
regionName
,
maxCapacity
);
// 区域最大承载量
// 初始化客流量
let
totalNum
=
regionCustomerMap
.
get
(
regionName
)
?
regionCustomerMap
.
get
(
regionName
)
:
0
;
let
customerDevices
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_type
:
"客流传感器"
,
region_key
:
regionInfo
.
id
},
[
"device_type"
,
"device_id"
]);
let
customerDeviceIds
=
customerDevices
.
data
.
map
((
d
:
any
)
=>
d
.
device_id
);
// 获取今日的客流量数据
if (customerDeviceIds) {
if
(
customerDeviceIds
&&
customerDeviceIds
.
length
>
0
)
{
let
customerDeviceDatas
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
"%in%"
:
customerDeviceIds
},
device_time
:
{
"%gte%"
:
formatLocalTime
(
todayStart
),
"%lte%"
:
formatLocalTime
(
todayEnd
)
},
...
...
@@ -263,19 +290,15 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
let
data
=
rec
.
data
;
if
(
data
.
presence
===
'进'
)
{
totalRegionCustomerNum
+=
1
;
// 区分楼栋与楼层
let regionName = regionKeys.length === 0 ? regionInfo.type : regionInfo.name;
if
(
!
regionCustomerMap
.
has
(
regionName
))
{
regionCustomerMap
.
set
(
regionName
,
1
);
// 如果是按区域类型统计,则最大承载量取该类型下所有区域的总和;如果是按区域名称统计,则取该区域的最大承载量
let maxCapacity = regionKeys.length === 0 ? regionMaxCustomerMap.get(regionName) : regionInfo.max_capacity;
regionMaxCustomerMap.set(regionName, maxCapacity); // 区域最大承载量
}
else
{
let totalNum = regionCustomerMap.get(regionName);
regionCustomerMap
.
set
(
regionName
,
totalNum
+
1
);
}
}
}
}
else
{
regionCustomerMap
.
set
(
regionName
,
totalNum
);
}
}
// 计算每个区域的饱和度(客流量/区域最大承载量),并转换成表格
...
...
src/config/mysqlTableConfig.ts
View file @
d6c9a9f9
...
...
@@ -257,6 +257,10 @@ export const TablesConfig = [
defaultValue
:
0
,
comment
:
'排序序号'
},
address
:
{
type
:
DataTypes
.
STRING
(
100
),
comment
:
'外机地址'
},
created_at
:
{
type
:
DataTypes
.
DATE
,
allowNull
:
false
,
...
...
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