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
d5c0f8ef
Commit
d5c0f8ef
authored
Sep 20, 2026
by
PC-20251223ZVQQ\Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
客流体验环境指标弹窗
parent
44f637ff
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
628 additions
and
347 deletions
+628
-347
dst数据.xlsx
docs/dst数据.xlsx
+0
-0
区域坐标分布.xlsx
res/区域坐标分布.xlsx
+0
-0
customs.ts
src/biz/customs.ts
+334
-226
dataIntegration.ts
src/biz/dataIntegration.ts
+40
-12
device.ts
src/biz/device.ts
+10
-4
running.ts
src/biz/running.ts
+121
-89
users.ts
src/biz/users.ts
+3
-3
businessEnum.ts
src/config/businessEnum.ts
+11
-0
mysqlTableConfig.ts
src/config/mysqlTableConfig.ts
+10
-0
sqlModelBind.ts
src/model/sqlModelBind.ts
+7
-2
admin.ts
src/routers/admin.ts
+12
-10
customs.ts
src/routers/customs.ts
+22
-1
public.ts
src/routers/public.ts
+2
-0
running.ts
src/routers/running.ts
+12
-0
ttlCache.ts
src/util/ttlCache.ts
+43
-0
tsconfig.json
tsconfig.json
+1
-0
No files found.
docs/dst数据.xlsx
0 → 100644
View file @
d5c0f8ef
File added
res/区域坐标分布.xlsx
View file @
d5c0f8ef
No preview for this file type
src/biz/customs.ts
View file @
d5c0f8ef
...
@@ -33,6 +33,7 @@ import {
...
@@ -33,6 +33,7 @@ import {
assessLightEnvironment
,
assessSoundEnvironment
,
assessLightEnvironment
,
assessSoundEnvironment
,
calcComplianceRate
,
calcOverallComfort
,
calcComplianceRate
,
calcOverallComfort
,
}
from
"./customsIndicators"
;
}
from
"./customsIndicators"
;
import
{
dasb
}
from
"tencentcloud-sdk-nodejs"
;
const
Op
=
Sequelize
.
Op
;
const
Op
=
Sequelize
.
Op
;
...
@@ -49,16 +50,22 @@ function formatLocalDateTime(d: Date): string {
...
@@ -49,16 +50,22 @@ function formatLocalDateTime(d: Date): string {
}
}
/**
/**
* 从 IEQ 传感器获取最新的环境数据
* 【性能优化】客流体验分析公共数据加载
* 使用子查询绕过 ONLY_FULL_GROUP_BY 限制
* 原先 7 个 helper 各自重复执行:IEQ 设备表查询 ×6、近24h device_data 全量 ×4、
* "每设备最新数据"子查询 ×2、region 名称查询 ×1,且全部串行。
* 现收敛为:一次设备表查询 → 3 个互不依赖的查询并行(region 名称 / 近24h记录 / 每设备最新)。
* 返回 null 表示当前没有 IEQ 传感器设备(调用方按空数据展示)。
*/
*/
async
function
getLatestIEQData
():
Promise
<
{
async
function
fetchCustomsBaseData
():
Promise
<
{
temperature
:
number
;
humidity
:
number
;
ieqDevices
:
any
[];
co2
:
number
;
pm25
:
number
;
tvoc
:
number
;
records24h
:
any
[];
a31001
:
number
;
lightLevel
:
number
;
lightUniformity
:
number
;
a14263
:
number
;
latestData
:
any
[];
pm10
?:
number
;
pressure
?:
number
;
regionNameMap
:
Record
<
string
,
string
>
;
}
>
{
regionAvailable
:
boolean
;
}
|
null
>
{
const
deviceModel
=
mysqlModelMap
[
'device'
];
const
deviceModel
=
mysqlModelMap
[
'device'
];
const
dataModel
=
mysqlModelMap
[
'device_data'
];
const
regionModel
=
mysqlModelMap
[
'region'
];
if
(
!
deviceModel
)
{
if
(
!
deviceModel
)
{
throw
new
Error
(
'device 表模型未初始化'
);
throw
new
Error
(
'device 表模型未初始化'
);
}
}
...
@@ -70,33 +77,68 @@ async function getLatestIEQData(): Promise<{
...
@@ -70,33 +77,68 @@ async function getLatestIEQData(): Promise<{
raw
:
true
,
raw
:
true
,
})
as
any
[];
})
as
any
[];
if
(
!
ieqDevices
.
length
)
{
if
(
!
ieqDevices
.
length
)
return
null
;
return
{
temperature
:
0
,
humidity
:
0
,
co2
:
0
,
pm25
:
0
,
const
deviceIds
=
ieqDevices
.
map
((
d
:
any
)
=>
d
.
device_id
);
tvoc
:
0
,
a31001
:
0
,
lightLevel
:
0
,
lightUniformity
:
0
,
a14263
:
0
,
const
regionKeys
=
[...
new
Set
(
ieqDevices
.
map
((
d
:
any
)
=>
d
.
region_key
).
filter
(
Boolean
))];
};
const
now
=
new
Date
();
const
yesterday
=
new
Date
(
now
.
getTime
()
-
24
*
60
*
60
*
1000
);
// 三个互不依赖的查询并行执行
const
[
regions
,
records24h
,
latestData
]
=
await
Promise
.
all
([
// 1) 区域名称映射
(
regionModel
&&
regionKeys
.
length
)
?
regionModel
.
findAll
({
where
:
{
region_key
:
{
[
Op
.
in
]:
regionKeys
}
},
attributes
:
[
'region_key'
,
'region_name'
],
raw
:
true
,
})
as
any
[]
:
Promise
.
resolve
([]),
// 2) 近 24h 全部 IEQ 明细(原 4 个 trend + maxNoise 各自全量查询的合并)
(
dataModel
?
dataModel
.
findAll
({
attributes
:
[
'device_data'
,
'device_time'
],
where
:
{
device_id
:
{
[
Op
.
in
]:
deviceIds
},
device_time
:
{
[
Op
.
gte
]:
yesterday
},
},
order
:
[[
'device_time'
,
'ASC'
]],
raw
:
true
,
})
:
Promise
.
resolve
([]))
as
any
[],
// 3) 每台设备的最新数据(子查询绕过 ONLY_FULL_GROUP_BY;热湿/空气/光/声现状与区域数据共用)
mysqlDB
.
query
(
`
SELECT dd.device_id, dd.device_data
FROM device_data dd
INNER JOIN (
SELECT device_id, MAX(device_time) AS max_time
FROM device_data
WHERE device_id IN (
${
deviceIds
.
map
(()
=>
'?'
).
join
(
','
)}
)
GROUP BY device_id
) latest ON dd.device_id = latest.device_id AND dd.device_time = latest.max_time
`
,
{
replacements
:
deviceIds
,
type
:
Sequelize
.
QueryTypes
.
SELECT
,
})
as
any
[],
]);
const
regionNameMap
:
Record
<
string
,
string
>
=
{};
for
(
const
r
of
regions
)
{
regionNameMap
[
r
.
region_key
]
=
r
.
region_name
;
}
}
const
deviceIds
=
ieqDevices
.
map
(
d
=>
d
.
device_id
);
return
{
ieqDevices
,
records24h
,
latestData
,
regionNameMap
,
regionAvailable
:
!!
regionModel
};
}
// 使用子查询获取每台设备的最新数据,避免 ONLY_FULL_GROUP_BY 报错
const
placeholders
=
deviceIds
.
map
(()
=>
'?'
).
join
(
','
);
/** IEQ 汇总后的传感器数据(pm10/pressure 为可选,设备可能不上报) */
const
sql
=
`
type
SensorSummary
=
{
SELECT dd.device_id, dd.device_data
temperature
:
number
;
humidity
:
number
;
co2
:
number
;
pm25
:
number
;
tvoc
:
number
;
FROM device_data dd
a31001
:
number
;
lightLevel
:
number
;
lightUniformity
:
number
;
a14263
:
number
;
INNER JOIN (
pm10
?:
number
;
pressure
?:
number
;
SELECT device_id, MAX(device_time) AS max_time
};
FROM device_data
WHERE device_id IN (
${
placeholders
}
)
GROUP BY device_id
) latest ON dd.device_id = latest.device_id AND dd.device_time = latest.max_time
`
;
const
latestData
=
await
mysqlDB
.
query
(
sql
,
{
replacements
:
deviceIds
,
type
:
Sequelize
.
QueryTypes
.
SELECT
,
})
as
any
[];
// 汇总所有设备的最新数据
/** 汇总所有设备的最新数据(原 getLatestIEQData 聚合逻辑,行为不变) */
function
summarizeLatestData
(
latestData
:
any
[]):
SensorSummary
{
const
sums
:
Record
<
string
,
number
>
=
{};
const
sums
:
Record
<
string
,
number
>
=
{};
const
fieldCounts
:
Record
<
string
,
number
>
=
{};
const
fieldCounts
:
Record
<
string
,
number
>
=
{};
let
minLight
:
number
|
null
=
null
;
let
minLight
:
number
|
null
=
null
;
...
@@ -154,35 +196,8 @@ async function getLatestIEQData(): Promise<{
...
@@ -154,35 +196,8 @@ async function getLatestIEQData(): Promise<{
};
};
}
}
/**
/** 近 24 小时 PMV 趋势(原 getPMVTrend 聚合逻辑,行为不变) */
* 获取近 24 小时的 PMV 趋势数据(每小时一个点)
function
buildPMVTrend
(
records
:
any
[]):
Array
<
{
key
:
string
;
value
:
string
}
>
{
*/
async
function
getPMVTrend
():
Promise
<
Array
<
{
key
:
string
;
value
:
string
}
>>
{
const
dataModel
=
mysqlModelMap
[
'device_data'
];
const
deviceModel
=
mysqlModelMap
[
'device'
];
if
(
!
dataModel
||
!
deviceModel
)
return
[];
const
ieqDevices
=
await
deviceModel
.
findAll
({
where
:
{
device_type
:
'IEQ传感器'
},
attributes
:
[
'device_id'
],
raw
:
true
,
})
as
any
[];
const
deviceIds
=
ieqDevices
.
map
(
d
=>
d
.
device_id
);
if
(
!
deviceIds
.
length
)
return
[];
const
now
=
new
Date
();
const
yesterday
=
new
Date
(
now
.
getTime
()
-
24
*
60
*
60
*
1000
);
const
records
=
await
dataModel
.
findAll
({
attributes
:
[
'device_data'
,
'device_time'
],
where
:
{
device_id
:
{
[
Op
.
in
]:
deviceIds
},
device_time
:
{
[
Op
.
gte
]:
yesterday
},
},
order
:
[[
'device_time'
,
'ASC'
]],
raw
:
true
,
})
as
any
[];
// 按小时分组取平均
// 按小时分组取平均
const
hourlyMap
:
Record
<
string
,
{
tempSum
:
number
;
humSum
:
number
;
count
:
number
}
>
=
{};
const
hourlyMap
:
Record
<
string
,
{
tempSum
:
number
;
humSum
:
number
;
count
:
number
}
>
=
{};
for
(
const
r
of
records
)
{
for
(
const
r
of
records
)
{
...
@@ -209,34 +224,8 @@ async function getPMVTrend(): Promise<Array<{ key: string; value: string }>> {
...
@@ -209,34 +224,8 @@ async function getPMVTrend(): Promise<Array<{ key: string; value: string }>> {
});
});
}
}
/**
/** 近 24 小时空气质量趋势(原 getAirQualityTrend 聚合逻辑,行为不变) */
* 获取近 24 小时的空气质量趋势
function
buildAirQualityTrend
(
records
:
any
[]):
Array
<
{
key
:
string
;
co2
:
string
;
pm25
:
string
}
>
{
*/
async
function
getAirQualityTrend
():
Promise
<
Array
<
{
key
:
string
;
co2
:
string
;
pm25
:
string
}
>>
{
const
dataModel
=
mysqlModelMap
[
'device_data'
];
const
deviceModel
=
mysqlModelMap
[
'device'
];
if
(
!
dataModel
||
!
deviceModel
)
return
[];
const
ieqDevices
=
await
deviceModel
.
findAll
({
where
:
{
device_type
:
'IEQ传感器'
},
attributes
:
[
'device_id'
],
raw
:
true
,
})
as
any
[];
const
deviceIds
=
ieqDevices
.
map
(
d
=>
d
.
device_id
);
if
(
!
deviceIds
.
length
)
return
[];
const
yesterday
=
new
Date
(
Date
.
now
()
-
24
*
60
*
60
*
1000
);
const
records
=
await
dataModel
.
findAll
({
attributes
:
[
'device_data'
,
'device_time'
],
where
:
{
device_id
:
{
[
Op
.
in
]:
deviceIds
},
device_time
:
{
[
Op
.
gte
]:
yesterday
},
},
order
:
[[
'device_time'
,
'ASC'
]],
raw
:
true
,
})
as
any
[];
const
hourlyMap
:
Record
<
string
,
{
co2Sum
:
number
;
pm25Sum
:
number
;
count
:
number
}
>
=
{};
const
hourlyMap
:
Record
<
string
,
{
co2Sum
:
number
;
pm25Sum
:
number
;
count
:
number
}
>
=
{};
for
(
const
r
of
records
)
{
for
(
const
r
of
records
)
{
if
(
!
r
.
device_time
||
!
r
.
device_data
)
continue
;
if
(
!
r
.
device_time
||
!
r
.
device_data
)
continue
;
...
@@ -260,55 +249,45 @@ async function getAirQualityTrend(): Promise<Array<{ key: string; co2: string; p
...
@@ -260,55 +249,45 @@ async function getAirQualityTrend(): Promise<Array<{ key: string; co2: string; p
}));
}));
}
}
/**
/** 近 24 小时某字段的每小时均值趋势(原 getFieldTrend 聚合逻辑,行为不变) */
* 获取各区域的最新环境数据
function
buildFieldTrend
(
records
:
any
[],
field
:
string
):
Array
<
{
key
:
string
;
value
:
string
}
>
{
*/
const
hourlyMap
:
Record
<
string
,
{
sum
:
number
;
count
:
number
}
>
=
{};
async
function
getRegionData
():
Promise
<
Array
<
{
for
(
const
r
of
records
)
{
regionName
:
string
;
pmv
:
string
;
co2
:
string
;
lux
:
string
;
dB
:
string
;
zhssd
:
string
;
zb
:
string
;
if
(
!
r
.
device_time
||
!
r
.
device_data
)
continue
;
}
>>
{
const
key
=
formatHourKey
(
new
Date
(
r
.
device_time
));
const
deviceModel
=
mysqlModelMap
[
'device'
];
const
val
=
r
.
device_data
[
field
];
const
dataModel
=
mysqlModelMap
[
'device_data'
];
if
(
val
===
undefined
||
val
===
null
||
typeof
val
!==
'number'
)
continue
;
const
regionModel
=
mysqlModelMap
[
'region'
];
if
(
!
deviceModel
||
!
dataModel
||
!
regionModel
)
return
[];
// 查询所有 IEQ 设备及其区域信息
if
(
!
hourlyMap
[
key
])
hourlyMap
[
key
]
=
{
sum
:
0
,
count
:
0
};
const
ieqDevices
=
await
deviceModel
.
findAll
({
hourlyMap
[
key
].
sum
+=
val
;
where
:
{
device_type
:
'IEQ传感器'
},
hourlyMap
[
key
].
count
++
;
attributes
:
[
'device_id'
,
'region_key'
],
}
raw
:
true
,
})
as
any
[];
if
(
!
ieqDevices
.
length
)
return
[];
return
Object
.
entries
(
hourlyMap
)
.
sort
(([
a
],
[
b
])
=>
a
.
localeCompare
(
b
))
.
map
(([
key
,
val
])
=>
({
key
,
value
:
(
val
.
sum
/
val
.
count
).
toFixed
(
2
),
}));
}
// 查询区域名称
/** 近 24 小时某字段的最大值(原 getFieldMax24h 聚合逻辑,行为不变) */
const
regionKeys
=
[...
new
Set
(
ieqDevices
.
map
(
d
=>
d
.
region_key
).
filter
(
Boolean
))];
function
buildFieldMax24h
(
records
:
any
[],
field
:
string
):
number
{
const
regions
=
await
regionModel
.
findAll
({
let
maxVal
=
0
;
where
:
{
region_key
:
{
[
Op
.
in
]:
regionKeys
}
},
for
(
const
r
of
records
)
{
attributes
:
[
'region_key'
,
'region_name'
],
if
(
!
r
.
device_data
)
continue
;
raw
:
true
,
const
val
=
r
.
device_data
[
field
];
})
as
any
[];
if
(
val
!==
undefined
&&
val
!==
null
&&
typeof
val
===
'number'
&&
val
>
maxVal
)
{
const
regionNameMap
:
Record
<
string
,
string
>
=
{};
maxVal
=
val
;
for
(
const
r
of
regions
)
{
}
regionNameMap
[
r
.
region_key
]
=
r
.
region_name
;
}
}
return
Math
.
round
(
maxVal
*
100
)
/
100
;
}
// 按区域分组查询最新数据(使用子查询避免 ONLY_FULL_GROUP_BY)
/** 各区域最新环境数据聚合(原 getRegionData 聚合逻辑,行为不变) */
const
deviceIds
=
ieqDevices
.
map
(
d
=>
d
.
device_id
);
function
buildRegionData
(
latestData
:
any
[],
ieqDevices
:
any
[],
regionNameMap
:
Record
<
string
,
string
>
):
Array
<
{
const
placeholders
=
deviceIds
.
map
(()
=>
'?'
).
join
(
','
);
regionName
:
string
;
pmv
:
string
;
co2
:
string
;
lux
:
string
;
dB
:
string
;
zhssd
:
string
;
zb
:
string
;
const
latestData
=
await
mysqlDB
.
query
(
`
}
>
{
SELECT dd.device_id, dd.device_data
FROM device_data dd
INNER JOIN (
SELECT device_id, MAX(device_time) AS max_time
FROM device_data
WHERE device_id IN (
${
placeholders
}
)
GROUP BY device_id
) latest ON dd.device_id = latest.device_id AND dd.device_time = latest.max_time
`
,
{
replacements
:
deviceIds
,
type
:
Sequelize
.
QueryTypes
.
SELECT
,
})
as
any
[];
// 按 region_key 汇总
// 按 region_key 汇总
const
regionAgg
:
Record
<
string
,
{
temp
:
number
[];
hum
:
number
[];
co2
:
number
[];
lux
:
number
[];
a14263
:
number
[]
}
>
=
{};
const
regionAgg
:
Record
<
string
,
{
temp
:
number
[];
hum
:
number
[];
co2
:
number
[];
lux
:
number
[];
a14263
:
number
[]
}
>
=
{};
const
deviceRegionMap
:
Record
<
string
,
string
>
=
{};
const
deviceRegionMap
:
Record
<
string
,
string
>
=
{};
...
@@ -362,103 +341,36 @@ async function getRegionData(): Promise<Array<{
...
@@ -362,103 +341,36 @@ async function getRegionData(): Promise<Array<{
}
}
/**
/**
* 获取近 24 小时某字段的每小时均值趋势
* 客流体验分析 - 返回大屏所需的所有真实指标数据
*/
* 【性能优化】共享一次设备/明细/最新数据查询后全部改为内存聚合,DB 往返由 13 次串行降为 4 次(前 3 次并行)
async
function
getFieldTrend
(
field
:
string
):
Promise
<
Array
<
{
key
:
string
;
value
:
string
}
>>
{
const
dataModel
=
mysqlModelMap
[
'device_data'
];
const
deviceModel
=
mysqlModelMap
[
'device'
];
if
(
!
dataModel
||
!
deviceModel
)
return
[];
const
ieqDevices
=
await
deviceModel
.
findAll
({
where
:
{
device_type
:
'IEQ传感器'
},
attributes
:
[
'device_id'
],
raw
:
true
,
})
as
any
[];
const
deviceIds
=
ieqDevices
.
map
(
d
=>
d
.
device_id
);
if
(
!
deviceIds
.
length
)
return
[];
const
yesterday
=
new
Date
(
Date
.
now
()
-
24
*
60
*
60
*
1000
);
const
records
=
await
dataModel
.
findAll
({
attributes
:
[
'device_data'
,
'device_time'
],
where
:
{
device_id
:
{
[
Op
.
in
]:
deviceIds
},
device_time
:
{
[
Op
.
gte
]:
yesterday
},
},
order
:
[[
'device_time'
,
'ASC'
]],
raw
:
true
,
})
as
any
[];
const
hourlyMap
:
Record
<
string
,
{
sum
:
number
;
count
:
number
}
>
=
{};
for
(
const
r
of
records
)
{
if
(
!
r
.
device_time
||
!
r
.
device_data
)
continue
;
const
key
=
formatHourKey
(
new
Date
(
r
.
device_time
));
const
val
=
r
.
device_data
[
field
];
if
(
val
===
undefined
||
val
===
null
||
typeof
val
!==
'number'
)
continue
;
if
(
!
hourlyMap
[
key
])
hourlyMap
[
key
]
=
{
sum
:
0
,
count
:
0
};
hourlyMap
[
key
].
sum
+=
val
;
hourlyMap
[
key
].
count
++
;
}
return
Object
.
entries
(
hourlyMap
)
.
sort
(([
a
],
[
b
])
=>
a
.
localeCompare
(
b
))
.
map
(([
key
,
val
])
=>
({
key
,
value
:
(
val
.
sum
/
val
.
count
).
toFixed
(
2
),
}));
}
/**
* 获取近 24 小时某字段的最大值
*/
*/
async
function
getFieldMax24h
(
field
:
string
):
Promise
<
number
>
{
export
async
function
getCustomsAnalysis
()
{
const
dataModel
=
mysqlModelMap
[
'device_data'
];
const
base
=
await
fetchCustomsBaseData
();
const
deviceModel
=
mysqlModelMap
[
'device'
];
if
(
!
dataModel
||
!
deviceModel
)
return
0
;
const
ieqDevices
=
await
deviceModel
.
findAll
({
where
:
{
device_type
:
'IEQ传感器'
},
attributes
:
[
'device_id'
],
raw
:
true
,
})
as
any
[];
const
deviceIds
=
ieqDevices
.
map
(
d
=>
d
.
device_id
);
if
(
!
deviceIds
.
length
)
return
0
;
const
yesterday
=
new
Date
(
Date
.
now
()
-
24
*
60
*
60
*
1000
);
const
records
=
await
dataModel
.
findAll
({
// IEQ 设备为空时的兜底(与改造前各 helper 空数据处理一致)
attributes
:
[
'device_data'
,
'device_time'
],
const
emptySensor
:
SensorSummary
=
{
where
:
{
temperature
:
0
,
humidity
:
0
,
co2
:
0
,
pm25
:
0
,
device_id
:
{
[
Op
.
in
]:
deviceIds
},
tvoc
:
0
,
a31001
:
0
,
lightLevel
:
0
,
lightUniformity
:
0
,
a14263
:
0
,
device_time
:
{
[
Op
.
gte
]:
yesterday
},
};
},
order
:
[[
'device_time'
,
'ASC'
]],
raw
:
true
,
})
as
any
[];
let
maxVal
=
0
;
let
sensorData
:
SensorSummary
=
emptySensor
;
for
(
const
r
of
records
)
{
let
regionData
:
any
[]
=
[];
if
(
!
r
.
device_data
)
continue
;
let
pmvTrend
:
any
[]
=
[];
const
val
=
r
.
device_data
[
field
];
let
airTrend
:
any
[]
=
[];
if
(
val
!==
undefined
&&
val
!==
null
&&
typeof
val
===
'number'
&&
val
>
maxVal
)
{
let
lightTrend
:
any
[]
=
[];
maxVal
=
val
;
let
noiseTrend
:
any
[]
=
[];
}
let
maxNoise
=
0
;
if
(
base
)
{
const
{
ieqDevices
,
records24h
,
latestData
,
regionNameMap
,
regionAvailable
}
=
base
;
sensorData
=
summarizeLatestData
(
latestData
);
pmvTrend
=
buildPMVTrend
(
records24h
);
airTrend
=
buildAirQualityTrend
(
records24h
);
lightTrend
=
buildFieldTrend
(
records24h
,
'lightLevel'
);
noiseTrend
=
buildFieldTrend
(
records24h
,
'a14263'
);
maxNoise
=
buildFieldMax24h
(
records24h
,
'a14263'
);
regionData
=
regionAvailable
?
buildRegionData
(
latestData
,
ieqDevices
,
regionNameMap
)
:
[];
}
}
return
Math
.
round
(
maxVal
*
100
)
/
100
;
}
/**
* 客流体验分析 - 返回大屏所需的所有真实指标数据
*/
export
async
function
getCustomsAnalysis
()
{
const
sensorData
=
await
getLatestIEQData
();
const
pmvTrend
=
await
getPMVTrend
();
const
airTrend
=
await
getAirQualityTrend
();
const
lightTrend
=
await
getFieldTrend
(
'lightLevel'
);
const
noiseTrend
=
await
getFieldTrend
(
'a14263'
);
const
maxNoise
=
await
getFieldMax24h
(
'a14263'
);
const
regionData
=
await
getRegionData
();
// 计算光环境区域达标率(照度 ≥ 200 lx 的区域占比)
// 计算光环境区域达标率(照度 ≥ 200 lx 的区域占比)
const
lightRegions
=
regionData
.
filter
(
r
=>
{
const
lightRegions
=
regionData
.
filter
(
r
=>
{
...
@@ -556,4 +468,199 @@ export async function getCustomsAnalysis() {
...
@@ -556,4 +468,199 @@ export async function getCustomsAnalysis() {
shjdbl
:
calcComplianceRate
(
sound
.
passed
,
sound
.
total
),
shjdbl
:
calcComplianceRate
(
sound
.
passed
,
sound
.
total
),
},
},
};
};
}
/**
* 客流体验环境指标弹窗
* @param env 环境类型:rshj、kqpz、ghj、shj、ALL
* @res
*/
export
async
function
getCustomsEnvPopup
(
env
:
string
)
{
let
result
:
any
=
{};
switch
(
env
)
{
case
'rshj'
:
// 热湿环境指标弹窗:温度/湿度取国标舒适区间(展厅),PMV/PPD 取 ISO 7730
result
=
{
"base"
:
{
"温度"
:
"18℃ ≤ x ≤ 28℃"
,
"湿度"
:
"40%RH ≤ x ≤ 70%RH"
,
"PMV"
:
"-0.5 ≤ x ≤ 0.5"
,
"PPD"
:
"x ≤ 20%"
},
"formulas"
:
[
{
"indicator"
:
"PMV"
,
"purpose"
:
"热湿环境 - 热舒适"
,
"formula"
:
"基于 ISO 7730 人体热平衡方程,迭代求解后保留 2 位小数"
,
"note"
:
"输入空气温度、湿度、辐射温度、风速、代谢率、服装热阻等参数"
},
{
"indicator"
:
"PPD"
,
"purpose"
:
"热湿环境 - 不满意率"
,
"formula"
:
"PPD = 100 - 95 × e^(-0.03353 × PMV⁴ - 0.2179 × PMV²),保留 1 位小数"
,
"note"
:
"由 PMV 计算得出,预测人群对热环境的不满意百分数"
}
],
"standards"
:
[
{
"indicator"
:
"温度"
,
"standard"
:
"国标舒适区间(展厅)"
},
{
"indicator"
:
"湿度"
,
"standard"
:
"国标舒适区间(展厅)"
},
{
"indicator"
:
"PMV"
,
"standard"
:
"ISO 7730 舒适区"
},
{
"indicator"
:
"PPD"
,
"standard"
:
"ISO 7730 预测不满意率限值"
}
]
};
break
;
case
'kqpz'
:
// 空气品质指标弹窗:依据 GB/T 18883
result
=
{
"base"
:
{
"CO₂"
:
"0 ≤ x ≤ 1000 ppm"
,
"PM2.5"
:
"0 ≤ x ≤ 75 μg/m³"
,
"TVOC"
:
"0 ≤ x ≤ 0.6 mg/m³"
,
"甲醛"
:
"0 ≤ x ≤ 0.1 mg/m³"
},
"formulas"
:
[],
"standards"
:
[
{
"indicator"
:
"CO₂"
,
"standard"
:
"GB/T 18883"
},
{
"indicator"
:
"PM2.5"
,
"standard"
:
"GB/T 18883"
},
{
"indicator"
:
"TVOC"
,
"standard"
:
"GB/T 18883"
},
{
"indicator"
:
"甲醛"
,
"standard"
:
"GB/T 18883(取甲醛传感器 a31001 实测值)"
}
]
};
break
;
case
'ghj'
:
// 光环境指标弹窗:照度取展厅标准,UGR 为眩光评价数
result
=
{
"base"
:
{
"光照"
:
"200 ≤ x ≤ 2000 lx"
,
"UGR"
:
"x ≤ 22"
},
"formulas"
:
[
{
"indicator"
:
"UGR"
,
"purpose"
:
"光环境 - 眩光值"
,
"formula"
:
"简化估算模型:由照度与照度均匀度计算(照度越低 / 均匀度越差则越高;无光照视为 25;结果钳制在 10~28,保留 1 位小数)"
,
"note"
:
"UGR 为统一眩光评价数,用于展厅眩光限值判定"
}
],
"standards"
:
[
{
"indicator"
:
"照度"
,
"standard"
:
"展厅 ≥ 200 lx"
},
{
"indicator"
:
"UGR"
,
"standard"
:
"展厅眩光限值 ≤ 22"
}
]
};
break
;
case
'shj'
:
// 声环境指标弹窗:声级取展厅限值,NR 为噪声评价数
result
=
{
"base"
:
{
"音量"
:
"0 ≤ x ≤ 60 dB(A)"
,
"NR"
:
"x ≤ 40"
,
"峰值"
:
"x > 65 触发告警(仅告警,不计入达标率)"
},
"formulas"
:
[
{
"indicator"
:
"NR"
,
"purpose"
:
"声环境 - 噪声评价数"
,
"formula"
:
"NR ≈ max(0, dB(A) - 5),保留 1 位小数"
,
"note"
:
"NR 由 A 声级简化估算;达标判定 min ≤ 实测值 ≤ max 即达标"
}
],
"standards"
:
[
{
"indicator"
:
"声级"
,
"standard"
:
"展厅 ≤ 60 dB(A)"
},
{
"indicator"
:
"NR"
,
"standard"
:
"办公场所 NR 限值 ≤ 40"
},
{
"indicator"
:
"噪音 24h 峰值"
,
"standard"
:
"24h 峰值告警阈值 65 dB(A)"
}
]
};
break
;
case
'ALL'
:
// 综合环境品质弹窗:四维度达标率加权 → 综合评分 → 等级分档(对应 analysis.sjhz)
result
=
{
"base"
:
{
"综合评分"
:
"综合评分 ≥ 90 → 优;75 ≤ 评分 < 90 → 良好;60 ≤ 评分 < 75 → 一般;评分 < 60 → 差"
,
"热湿环境"
:
"热湿达标项数 ÷ 4 × 100%(温度、湿度、PMV、PPD)"
,
"空气品质"
:
"空气达标项数 ÷ 4 × 100%(CO₂、PM2.5、TVOC、甲醛)"
,
"光环境"
:
"照度 ≥ 200 lx 区域数 ÷ 区域总数 × 100%"
,
"声环境"
:
"声环境达标项数 ÷ 2 × 100%(声级、NR)"
},
"formulas"
:
[
{
"indicator"
:
"综合环境品质评分"
,
"purpose"
:
"综合环境品质 - 加权评分"
,
"formula"
:
"score = round((热湿率×0.3 + 空气率×0.3 + 光率×0.2 + 声率×0.2) × 100)"
,
"note"
:
"权重:热湿 0.3 / 空气 0.3 / 光 0.2 / 声 0.2;各维达标率传 0~1 小数"
},
{
"indicator"
:
"rshjdbl / kqpzdbl / shjdbl"
,
"purpose"
:
"各维达标率(指标项数口径)"
,
"formula"
:
"Math.round(达标项数 ÷ 总项数 × 100) + '%'"
,
"note"
:
"热湿固定 4 项、空气固定 4 项、声环境固定 2 项;总数为 0 时返回 0%"
},
{
"indicator"
:
"ghjdbl"
,
"purpose"
:
"光环境达标率(区域数口径)"
,
"formula"
:
"照度 ≥ 200 lx 区域数 ÷ 区域总数 × 100%,保留 1 位小数"
,
"note"
:
"ghj.lux 字段显示值即此占比;区域总数为 0 时返回 0%"
}
],
"standards"
:
[
{
"indicator"
:
"优"
,
"standard"
:
"综合评分 score ≥ 90"
},
{
"indicator"
:
"良好"
,
"standard"
:
"75 ≤ 综合评分 < 90"
},
{
"indicator"
:
"一般"
,
"standard"
:
"60 ≤ 综合评分 < 75"
},
{
"indicator"
:
"差"
,
"standard"
:
"综合评分 < 60"
}
]
};
break
;
}
return
{
success
:
true
,
data
:
result
};
}
}
\ No newline at end of file
src/biz/dataIntegration.ts
View file @
d5c0f8ef
...
@@ -19,7 +19,7 @@ import { mysqlModelMap } from "../model/sqlModelBind";
...
@@ -19,7 +19,7 @@ import { mysqlModelMap } from "../model/sqlModelBind";
import
{
acLinkageControl
}
from
"./acLinkage"
;
import
{
acLinkageControl
}
from
"./acLinkage"
;
import
{
systemConfig
}
from
"../config/serverConfig"
;
import
{
systemConfig
}
from
"../config/serverConfig"
;
import
Sequelize
from
"sequelize"
;
import
Sequelize
from
"sequelize"
;
import
{
DEVICE_STATE
,
WORK_MODE_TEXT_MAP
,
FAN_SPEED_TEXT_MAP
}
from
"../config/businessEnum"
;
import
{
DEVICE_STATE
,
WORK_MODE_TEXT_MAP
,
FAN_SPEED_TEXT_MAP
,
REGION_NAME
}
from
"../config/businessEnum"
;
import
moment
from
"moment"
;
import
moment
from
"moment"
;
import
XLSX
from
"xlsx"
;
import
XLSX
from
"xlsx"
;
import
path
from
"path"
;
import
path
from
"path"
;
...
@@ -1230,34 +1230,62 @@ export async function diqinRegion() {
...
@@ -1230,34 +1230,62 @@ export async function diqinRegion() {
const
stations
=
detail
?.
stations
||
[];
const
stations
=
detail
?.
stations
||
[];
for
(
const
st
of
stations
)
{
for
(
const
st
of
stations
)
{
if
(
!
st
.
id
||
!
st
.
name
)
continue
;
if
(
!
st
.
id
||
!
st
.
name
)
continue
;
// 转换后的统一区域名:迪勤监测点与飞奕区域(房间)为同一物理房间时,
// 用飞奕侧 region_name(如 大会议室→亚马逊)入库,使其能与飞奕已建区域按名合并,
// 避免同一房间在 region 表生成两条记录(未配置映射的监测点保持迪勤原始名)。
const
mappedName
:
string
=
(
REGION_NAME
as
Record
<
string
,
string
>
)[
st
.
name
]
||
st
.
name
;
// 当前迪勤监测点 id 是否已有入库记录
let
stExisting
=
await
regionModel
.
findOne
({
where
:
{
region_key
:
st
.
id
}
});
let
stExisting
=
await
regionModel
.
findOne
({
where
:
{
region_key
:
st
.
id
}
});
// mappedName 被谁占用(飞奕已建区域,或历史遗留的迪勤冗余记录)
const
nameOwner
=
await
regionModel
.
findOne
({
where
:
stExisting
?
{
region_name
:
mappedName
,
id
:
{
[
Op
.
ne
]:
stExisting
.
id
}
}
:
{
region_name
:
mappedName
},
});
if
(
stExisting
)
{
if
(
stExisting
)
{
if
(
stExisting
.
region_name
!==
st
.
name
if
(
nameOwner
)
{
// 撞名:存量里该监测点曾以原始名建过记录(stExisting),而 mappedName
// 已归属 nameOwner(通常是飞奕房间)→ 把迪勤id并入 nameOwner 后清理冗余记录
if
(
!
nameOwner
.
region_ad
||
nameOwner
.
region_ad
!==
st
.
id
)
{
await
regionModel
.
update
(
{
region_ad
:
st
.
id
,
updated_at
:
new
Date
()
},
{
where
:
{
id
:
nameOwner
.
id
}
},
);
stUpdate
++
;
console
.
log
(
`[迪勤区域集成] 监测点撞名合并:
${
st
.
name
}
(
${
st
.
id
}
) →
${
mappedName
}
(
${
nameOwner
.
region_key
}
)`
);
}
await
regionModel
.
destroy
({
where
:
{
id
:
stExisting
.
id
}
});
stUpdate
++
;
}
else
if
(
stExisting
.
region_name
!==
mappedName
||
stExisting
.
parent_key
!==
regionKey
||
stExisting
.
parent_key
!==
regionKey
||
stExisting
.
region_type
!==
'监测点'
)
{
||
stExisting
.
region_type
!==
'监测点'
)
{
// 无撞名:按需更新为转换后的名称/归属
await
regionModel
.
update
(
await
regionModel
.
update
(
{
region_name
:
st
.
n
ame
,
parent_key
:
regionKey
,
region_type
:
'监测点'
,
updated_at
:
new
Date
()
},
{
region_name
:
mappedN
ame
,
parent_key
:
regionKey
,
region_type
:
'监测点'
,
updated_at
:
new
Date
()
},
{
where
:
{
id
:
stExisting
.
id
}
},
{
where
:
{
id
:
stExisting
.
id
}
},
);
);
stUpdate
++
;
stUpdate
++
;
}
}
}
else
{
}
else
{
// 按名称查是否存在同名区域(飞奕或其他平台已创建)
if
(
nameOwner
)
{
stExisting
=
await
regionModel
.
findOne
({
where
:
{
region_name
:
st
.
name
}
});
// 同名区域已存在(飞奕或其他平台已创建),将迪勤内部ID写入 region_ad,不新建
if
(
stExisting
)
{
if
(
!
nameOwner
.
region_ad
||
nameOwner
.
region_ad
!==
st
.
id
)
{
// 同名区域已存在,将迪勤内部ID写入 region_ad
if
(
!
stExisting
.
region_ad
||
stExisting
.
region_ad
!==
st
.
id
)
{
await
regionModel
.
update
(
await
regionModel
.
update
(
{
region_ad
:
st
.
id
,
updated_at
:
new
Date
()
},
{
region_ad
:
st
.
id
,
updated_at
:
new
Date
()
},
{
where
:
{
id
:
stExisting
.
id
}
},
{
where
:
{
id
:
nameOwner
.
id
}
},
);
);
stUpdate
++
;
stUpdate
++
;
console
.
log
(
`[迪勤区域集成] 同名监测点补充 region_ad:
${
st
.
name
}
→
${
st
.
id
}
`
);
console
.
log
(
`[迪勤区域集成] 同名监测点补充 region_ad:
${
st
.
name
}
→
${
mappedName
}
(
${
st
.
id
}
)
`
);
}
}
}
else
{
}
else
{
await
regionModel
.
create
({
await
regionModel
.
create
({
region_key
:
st
.
id
,
region_key
:
st
.
id
,
region_name
:
st
.
n
ame
,
region_name
:
mappedN
ame
,
parent_id
:
null
,
parent_id
:
null
,
parent_key
:
regionKey
,
parent_key
:
regionKey
,
region_type
:
'监测点'
,
region_type
:
'监测点'
,
...
@@ -1318,7 +1346,7 @@ export async function diqinDevice() {
...
@@ -1318,7 +1346,7 @@ export async function diqinDevice() {
if
(
!
deviceId
)
continue
;
if
(
!
deviceId
)
continue
;
const
deviceType
=
DIQIN_DEVICE_TYPE_MAP
[
ds
.
data_source_model_name
];
const
deviceType
=
DIQIN_DEVICE_TYPE_MAP
[
ds
.
data_source_model_name
];
const
deviceName
=
ds
.
name
||
ds
.
identifier
||
deviceId
;
const
deviceName
=
`IEQ-
${
ds
.
name
||
ds
.
identifier
||
deviceId
}
`
;
const
existing
=
await
deviceModel
.
findOne
({
where
:
{
device_id
:
deviceId
}
});
const
existing
=
await
deviceModel
.
findOne
({
where
:
{
device_id
:
deviceId
}
});
if
(
existing
)
{
if
(
existing
)
{
...
...
src/biz/device.ts
View file @
d5c0f8ef
...
@@ -325,6 +325,7 @@ export async function editAcDeviceControl(params: {
...
@@ -325,6 +325,7 @@ export async function editAcDeviceControl(params: {
*/
*/
export
async
function
batchEditDevices
(
params
:
{
export
async
function
batchEditDevices
(
params
:
{
regionKeys
:
number
[];
regionKeys
:
number
[];
deviceIds
:
string
[];
deviceName
?:
string
;
deviceName
?:
string
;
power
?:
string
;
power
?:
string
;
state
?:
string
;
state
?:
string
;
...
@@ -337,11 +338,11 @@ export async function batchEditDevices(params: {
...
@@ -337,11 +338,11 @@ export async function batchEditDevices(params: {
linkageStart
?:
string
;
linkageStart
?:
string
;
};
};
}):
Promise
<
{
total
:
number
;
successCount
:
number
;
failCount
:
number
;
details
:
any
[]
}
>
{
}):
Promise
<
{
total
:
number
;
successCount
:
number
;
failCount
:
number
;
details
:
any
[]
}
>
{
const
{
regionKeys
,
deviceName
,
power
,
state
,
deviceMode
,
startDate
,
endDate
,
edits
}
=
params
;
const
{
regionKeys
,
device
Ids
,
device
Name
,
power
,
state
,
deviceMode
,
startDate
,
endDate
,
edits
}
=
params
;
// 校验必填参数
// 校验必填参数
if
(
!
regionKeys
||
!
Array
.
isArray
(
regionKeys
)
||
regionKeys
.
length
===
0
)
{
if
(
(
!
regionKeys
||
!
Array
.
isArray
(
regionKeys
)
||
regionKeys
.
length
===
0
)
&&
(
!
deviceIds
||
!
Array
.
isArray
(
deviceIds
)
||
deviceIds
.
length
===
0
)
)
{
throw
new
BizError
(
ERRORENUM
.
参数错误
,
"regionKeys
不能
为空"
);
throw
new
BizError
(
ERRORENUM
.
参数错误
,
"regionKeys
和 deviceIds 不能同时
为空"
);
}
}
if
(
!
edits
||
typeof
edits
!==
'object'
)
{
if
(
!
edits
||
typeof
edits
!==
'object'
)
{
throw
new
BizError
(
ERRORENUM
.
参数错误
,
"edits 不能为空"
);
throw
new
BizError
(
ERRORENUM
.
参数错误
,
"edits 不能为空"
);
...
@@ -353,7 +354,12 @@ export async function batchEditDevices(params: {
...
@@ -353,7 +354,12 @@ export async function batchEditDevices(params: {
// 1. 构建设备查询条件
// 1. 构建设备查询条件
let
whereParam
:
any
=
{
device_type
:
'空调'
};
let
whereParam
:
any
=
{
device_type
:
'空调'
};
whereParam
.
region_key
=
{
"%in%"
:
regionKeys
};
if
(
regionKeys
&&
regionKeys
.
length
>
0
)
{
whereParam
.
region_key
=
{
"%in%"
:
regionKeys
};
}
if
(
deviceIds
&&
deviceIds
.
length
>
0
)
{
whereParam
.
device_id
=
{
"%in%"
:
deviceIds
};
}
if
(
deviceName
)
{
if
(
deviceName
)
{
whereParam
.
device_name
=
{
"%like%"
:
deviceName
};
whereParam
.
device_name
=
{
"%like%"
:
deviceName
};
...
...
src/biz/running.ts
View file @
d5c0f8ef
...
@@ -6,7 +6,9 @@ import { BizError } from '../util/bizError';
...
@@ -6,7 +6,9 @@ import { BizError } from '../util/bizError';
import
{
ERRORENUM
}
from
'../config/errorEnum'
;
import
{
ERRORENUM
}
from
'../config/errorEnum'
;
import
{
TABLENAME
}
from
'../config/dbEnum'
;
import
{
TABLENAME
}
from
'../config/dbEnum'
;
import
{
DEVICE_STATE
,
powerMap
,
workModeMap
,
fanSpeedMap
,
lockMap
,
acPopFanSpeedMap
}
from
'../config/businessEnum'
;
import
{
DEVICE_STATE
,
powerMap
,
workModeMap
,
fanSpeedMap
,
lockMap
,
acPopFanSpeedMap
}
from
'../config/businessEnum'
;
import
{
selectDataListByParam
,
selectOneDataByParam
,
selectDataCountByParam
}
from
'../data/findData'
;
import
{
selectDataListByParam
,
selectOneDataByParam
}
from
'../data/findData'
;
import
Sequelize
from
'sequelize'
;
import
{
mysqlDB
}
from
'../db/mysqlInit'
;
import
{
controlIndoorUnit
,
getIndoorUnitStatus
}
from
'./feiyiClient'
;
import
{
controlIndoorUnit
,
getIndoorUnitStatus
}
from
'./feiyiClient'
;
import
{
INDICATOR_NAME_MAP
,
ENV_INDICATOR_KEYS
}
from
'../config/businessEnum'
;
import
{
INDICATOR_NAME_MAP
,
ENV_INDICATOR_KEYS
}
from
'../config/businessEnum'
;
import
{
import
{
...
@@ -249,27 +251,26 @@ async function getDevicesByRegion(regionKey: string, columns: string[]): Promise
...
@@ -249,27 +251,26 @@ async function getDevicesByRegion(regionKey: string, columns: string[]): Promise
async
function
getLatestDeviceDataMap
(
deviceIds
:
string
[],
startTime
:
string
):
Promise
<
Map
<
string
,
any
>>
{
async
function
getLatestDeviceDataMap
(
deviceIds
:
string
[],
startTime
:
string
):
Promise
<
Map
<
string
,
any
>>
{
if
(
deviceIds
.
length
===
0
)
return
new
Map
();
if
(
deviceIds
.
length
===
0
)
return
new
Map
();
// 逐台设备单独查询最新一条(limit=1 + device_time 降序),保证每台设备都取到各自最新的数据。
// 【性能优化】由"每台设备单独 limit=1 查询"改为单条 SQL 子查询取每台设备最新一条:
// 不能合并成 IN + limit=N:多台设备数据量不均时,最新 N 条可能被数据密集的设备占满,其他设备取不到。
// 用 MAX(device_time) 分组保证每台设备都取到各自最新数据(与逐台 limit=1 语义一致),
// DB 往返由 N 次降为 1 次。
const
rows
=
await
mysqlDB
.
query
(
`SELECT dd.device_id, dd.device_data
FROM
${
TABLENAME
.
设备数据表
}
dd
INNER JOIN (
SELECT device_id, MAX(device_time) AS max_time
FROM
${
TABLENAME
.
设备数据表
}
WHERE device_id IN (
${
deviceIds
.
map
(()
=>
'?'
).
join
(
','
)}
)
AND device_time >= ?
GROUP BY device_id
) latest ON dd.device_id = latest.device_id AND dd.device_time = latest.max_time`
,
{
replacements
:
[...
deviceIds
,
startTime
],
type
:
Sequelize
.
QueryTypes
.
SELECT
}
)
as
any
[];
const
latestMap
=
new
Map
<
string
,
any
>
();
const
latestMap
=
new
Map
<
string
,
any
>
();
await
Promise
.
all
(
rows
.
forEach
((
row
:
any
)
=>
{
deviceIds
.
map
(
async
(
deviceId
)
=>
{
latestMap
.
set
(
row
.
device_id
,
parseDeviceData
(
row
.
device_data
));
const
dataResult
=
await
selectDataListByParam
(
});
TABLENAME
.
设备数据表
,
{
device_id
:
deviceId
,
device_time
:
{
'%gte%'
:
startTime
},
'%orderDesc%'
:
'device_time'
,
'%limit%'
:
1
,
},
[
'device_id'
,
'device_data'
]
);
const
row
=
(
dataResult
.
data
||
[])[
0
];
if
(
row
)
{
latestMap
.
set
(
row
.
device_id
,
parseDeviceData
(
row
.
device_data
));
}
})
);
return
latestMap
;
return
latestMap
;
}
}
...
@@ -289,14 +290,18 @@ const ELECTRICITY_TREND_DAYS = 14;
...
@@ -289,14 +290,18 @@ const ELECTRICITY_TREND_DAYS = 14;
/**
/**
* 查询电表设备在指定时间范围内的电量总和
* 查询电表设备在指定时间范围内的电量总和
* 【性能优化】支持外部传入电表设备 ID 列表,避免多次调用时重复查询设备表
*/
*/
async
function
getElectricitySumInRange
(
startTime
:
string
,
endTime
:
string
):
Promise
<
number
>
{
async
function
getElectricitySumInRange
(
startTime
:
string
,
endTime
:
string
,
meterDeviceIds
?:
string
[]):
Promise
<
number
>
{
const
deviceResult
=
await
selectDataListByParam
(
let
deviceIds
=
meterDeviceIds
;
TABLENAME
.
设备表
,
if
(
!
deviceIds
)
{
{
device_type
:
'电表'
},
const
deviceResult
=
await
selectDataListByParam
(
[
'device_id'
]
TABLENAME
.
设备表
,
);
{
device_type
:
'电表'
},
const
deviceIds
=
(
deviceResult
.
data
||
[]).
map
((
d
:
any
)
=>
d
.
device_id
);
[
'device_id'
]
);
deviceIds
=
(
deviceResult
.
data
||
[]).
map
((
d
:
any
)
=>
d
.
device_id
);
}
if
(
deviceIds
.
length
===
0
)
return
0
;
if
(
deviceIds
.
length
===
0
)
return
0
;
const
dataResult
=
await
selectDataListByParam
(
const
dataResult
=
await
selectDataListByParam
(
...
@@ -322,19 +327,23 @@ async function getElectricitySumInRange(startTime: string, endTime: string): Pro
...
@@ -322,19 +327,23 @@ async function getElectricitySumInRange(startTime: string, endTime: string): Pro
* @param startTime 开始时间
* @param startTime 开始时间
* @param endTime 结束时间
* @param endTime 结束时间
* @param groupBy 'hour' | 'day' | 'month'
* @param groupBy 'hour' | 'day' | 'month'
* @param
keyFormatter 时间 key 格式化函数
* @param
meterDeviceIds 电表设备 ID 列表(可选,外部传入可避免重复查询设备表)
*/
*/
async
function
getElectricityTrendData
(
async
function
getElectricityTrendData
(
startTime
:
string
,
startTime
:
string
,
endTime
:
string
,
endTime
:
string
,
groupBy
:
'hour'
|
'day'
|
'month'
,
groupBy
:
'hour'
|
'day'
|
'month'
,
meterDeviceIds
?:
string
[],
):
Promise
<
{
key
:
string
;
value
:
number
}[]
>
{
):
Promise
<
{
key
:
string
;
value
:
number
}[]
>
{
const
deviceResult
=
await
selectDataListByParam
(
let
deviceIds
=
meterDeviceIds
;
TABLENAME
.
设备表
,
if
(
!
deviceIds
)
{
{
device_type
:
'电表'
},
const
deviceResult
=
await
selectDataListByParam
(
[
'device_id'
]
TABLENAME
.
设备表
,
);
{
device_type
:
'电表'
},
const
deviceIds
=
(
deviceResult
.
data
||
[]).
map
((
d
:
any
)
=>
d
.
device_id
);
[
'device_id'
]
);
deviceIds
=
(
deviceResult
.
data
||
[]).
map
((
d
:
any
)
=>
d
.
device_id
);
}
if
(
deviceIds
.
length
===
0
)
return
[];
if
(
deviceIds
.
length
===
0
)
return
[];
const
dataResult
=
await
selectDataListByParam
(
const
dataResult
=
await
selectDataListByParam
(
...
@@ -408,10 +417,22 @@ export async function getRunAnalysis() {
...
@@ -408,10 +417,22 @@ export async function getRunAnalysis() {
// 上月同期:上月 1 号 → 对标日(短月/本月月底自动兜底到上月最后一天)
// 上月同期:上月 1 号 → 对标日(短月/本月月底自动兜底到上月最后一天)
const
lastMonthSameTimeEnd
=
getLastMonthSameTimeEnd
();
const
lastMonthSameTimeEnd
=
getLastMonthSameTimeEnd
();
const
jrnh
=
await
getElectricitySumInRange
(
todayStart
,
todayEnd
);
// 【性能优化】电表设备一次查询,供能耗统计/趋势/区域统计共用(原先 9 次重复查设备表 → 1 次)
const
zrnh
=
await
getElectricitySumInRange
(
yesterdayStart
,
yesterdaySameTimeEnd
);
const
meterDevicesResult
=
await
selectDataListByParam
(
const
bynh
=
await
getElectricitySumInRange
(
monthStart
,
todayEnd
);
TABLENAME
.
设备表
,
const
synh
=
await
getElectricitySumInRange
(
lastMonthStart
,
lastMonthSameTimeEnd
);
{
device_type
:
'电表'
},
[
'device_id'
,
'region_key'
]
);
const
meterDevices
=
meterDevicesResult
.
data
||
[];
const
meterDeviceIds
=
meterDevices
.
map
((
d
:
any
)
=>
d
.
device_id
);
// 【性能优化】四个时段的能耗统计并行执行(原先串行)
const
[
jrnh
,
zrnh
,
bynh
,
synh
]
=
await
Promise
.
all
([
getElectricitySumInRange
(
todayStart
,
todayEnd
,
meterDeviceIds
),
getElectricitySumInRange
(
yesterdayStart
,
yesterdaySameTimeEnd
,
meterDeviceIds
),
getElectricitySumInRange
(
monthStart
,
todayEnd
,
meterDeviceIds
),
getElectricitySumInRange
(
lastMonthStart
,
lastMonthSameTimeEnd
,
meterDeviceIds
),
]);
const
zrtb
=
calcCompareRate
(
jrnh
,
zrnh
);
const
zrtb
=
calcCompareRate
(
jrnh
,
zrnh
);
const
sytb
=
calcCompareRate
(
bynh
,
synh
);
const
sytb
=
calcCompareRate
(
bynh
,
synh
);
...
@@ -459,10 +480,13 @@ export async function getRunAnalysis() {
...
@@ -459,10 +480,13 @@ export async function getRunAnalysis() {
const
last30dStart
=
getDaysAgoStart
(
29
);
const
last30dStart
=
getDaysAgoStart
(
29
);
const
lastYearStart
=
getMonthsAgoStart
(
12
);
const
lastYearStart
=
getMonthsAgoStart
(
12
);
const
last24HoursData
=
await
getElectricityTrendData
(
last24hStart
,
formatTime
(
now24h
),
'hour'
);
// 【性能优化】四个时段趋势并行执行(电表设备复用上方一次查询结果)
const
last7DaysData
=
await
getElectricityTrendData
(
last7dStart
,
getTodayEnd
(),
'day'
);
const
[
last24HoursData
,
last7DaysData
,
last30DaysData
,
lastYearData
]
=
await
Promise
.
all
([
const
last30DaysData
=
await
getElectricityTrendData
(
last30dStart
,
getTodayEnd
(),
'day'
);
getElectricityTrendData
(
last24hStart
,
formatTime
(
now24h
),
'hour'
,
meterDeviceIds
),
const
lastYearData
=
await
getElectricityTrendData
(
lastYearStart
,
getTodayEnd
(),
'month'
);
getElectricityTrendData
(
last7dStart
,
getTodayEnd
(),
'day'
,
meterDeviceIds
),
getElectricityTrendData
(
last30dStart
,
getTodayEnd
(),
'day'
,
meterDeviceIds
),
getElectricityTrendData
(
lastYearStart
,
getTodayEnd
(),
'month'
,
meterDeviceIds
),
]);
// 转为字符串 value(与 mock 一致)
// 转为字符串 value(与 mock 一致)
const
stringifyValues
=
(
arr
:
{
key
:
string
;
value
:
number
}[])
=>
const
stringifyValues
=
(
arr
:
{
key
:
string
;
value
:
number
}[])
=>
...
@@ -547,14 +571,7 @@ export async function getRunAnalysis() {
...
@@ -547,14 +571,7 @@ export async function getRunAnalysis() {
const
allRegionKeys
=
new
Set
<
string
>
();
const
allRegionKeys
=
new
Set
<
string
>
();
regionDeviceCountMap
.
forEach
((
_
,
rk
)
=>
allRegionKeys
.
add
(
rk
));
regionDeviceCountMap
.
forEach
((
_
,
rk
)
=>
allRegionKeys
.
add
(
rk
));
// 查询所有电表设备(含 region_key,兼容存量旧电表)
// 电表设备已在上方能耗统计前一次性查询(meterDevices / meterDeviceIds)
const
meterDevicesResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_type
:
'电表'
},
[
'device_id'
,
'region_key'
]
);
const
meterDevices
=
meterDevicesResult
.
data
||
[];
const
meterDeviceIds
=
meterDevices
.
map
((
d
:
any
)
=>
d
.
device_id
);
// 构建电表→区域映射(一对多):优先读 region_device_rel 关联表,存量旧电表 fallback 到 device.region_key
// 构建电表→区域映射(一对多):优先读 region_device_rel 关联表,存量旧电表 fallback 到 device.region_key
const
meterRegionMap
=
new
Map
<
string
,
Set
<
string
>>
();
const
meterRegionMap
=
new
Map
<
string
,
Set
<
string
>>
();
...
@@ -774,14 +791,26 @@ export async function getRunAnalysis() {
...
@@ -774,14 +791,26 @@ export async function getRunAnalysis() {
====== MOCK DATA END ====== */
====== MOCK DATA END ====== */
// ---- 真实查询 ----
// ---- 真实查询 ----
const
resolvedCount
=
await
selectDataCountByParam
(
TABLENAME
.
设备故障表
,
{
fault_status
:
'已处理'
});
// 【性能优化+正确性】fault_status 为 INTEGER(0=未处理, 1=处理中, 2=已处理):
const
respondedCount
=
await
selectDataCountByParam
(
TABLENAME
.
设备故障表
,
{
fault_status
:
'处理中'
});
// 原先用中文条件与 INTEGER 列比较会被 MySQL 隐式转为 0,导致三个计数相同;
const
unresolvedCount
=
await
selectDataCountByParam
(
TABLENAME
.
设备故障表
,
{
fault_status
:
'未处理'
});
// 现改为一次 GROUP BY 按真实状态统计,三次全表 count 合并为一次。
const
statusCountRows
=
await
mysqlDB
.
query
(
`SELECT fault_status, COUNT(*) AS cnt FROM
${
TABLENAME
.
设备故障表
}
GROUP BY fault_status`
,
{
type
:
Sequelize
.
QueryTypes
.
SELECT
}
)
as
any
[];
const
statusCountMap
=
new
Map
<
number
,
number
>
();
statusCountRows
.
forEach
((
r
:
any
)
=>
{
const
st
=
typeof
r
.
fault_status
===
'string'
?
Number
(
r
.
fault_status
)
:
r
.
fault_status
;
statusCountMap
.
set
(
st
,
Number
(
r
.
cnt
)
||
0
);
});
const
unresolvedCount
=
statusCountMap
.
get
(
0
)
||
0
;
// 未处理
const
respondedCount
=
statusCountMap
.
get
(
1
)
||
0
;
// 处理中
const
resolvedCount
=
statusCountMap
.
get
(
2
)
||
0
;
// 已处理
let
alertStatus
=
{
let
alertStatus
=
{
resolved
:
typeof
resolvedCount
.
data
===
'number'
?
resolvedCount
.
data
:
0
,
resolved
:
resolvedCount
,
responded
:
typeof
respondedCount
.
data
===
'number'
?
respondedCount
.
data
:
0
,
responded
:
respondedCount
,
unresolved
:
typeof
unresolvedCount
.
data
===
'number'
?
unresolvedCount
.
data
:
0
,
unresolved
:
unresolvedCount
,
};
};
// 2、预警工单列表
// 2、预警工单列表
...
@@ -797,10 +826,8 @@ export async function getRunAnalysis() {
...
@@ -797,10 +826,8 @@ export async function getRunAnalysis() {
const
faultListResult
=
await
selectDataListByParam
(
const
faultListResult
=
await
selectDataListByParam
(
TABLENAME
.
设备故障表
,
TABLENAME
.
设备故障表
,
{
{
'%or%'
:
[
// fault_status 为 INTEGER:0=未处理, 1=处理中(原中文条件会被隐式转为 0)
{
fault_status
:
'未处理'
},
fault_status
:
{
'%in%'
:
[
0
,
1
]
},
{
fault_status
:
'处理中'
}
],
'%orderDesc%'
:
'fault_time'
,
'%orderDesc%'
:
'fault_time'
,
'%limit%'
:
20
,
'%limit%'
:
20
,
},
},
...
@@ -1983,19 +2010,21 @@ export async function getRunMonitoring() {
...
@@ -1983,19 +2010,21 @@ export async function getRunMonitoring() {
// ---- 真实查询 ----
// ---- 真实查询 ----
// 查询全历史故障记录,按 fault_code 分组统计
// 查询全历史故障记录,按 fault_code 分组统计
const
allFaultsResult
=
await
selectDataListByParam
(
// 【性能优化】故障类型统计下沉 SQL 端 GROUP BY,避免全历史故障明细拉取到内存再分组
TABLENAME
.
设备故障表
,
const
faultCodeRows
=
await
mysqlDB
.
query
(
{},
`SELECT COALESCE(fault_code, '未知') AS fault_code, COUNT(*) AS fault_count
[
'fault_code'
]
FROM
${
TABLENAME
.
设备故障表
}
);
GROUP BY COALESCE(fault_code, '未知')`
,
const
allFaults
=
allFaultsResult
.
data
||
[];
{
type
:
Sequelize
.
QueryTypes
.
SELECT
}
)
as
any
[];
const
faultCodeCountMap
=
new
Map
<
string
,
number
>
();
const
faultCodeCountMap
=
new
Map
<
string
,
number
>
();
let
totalFaultCount
=
0
;
let
totalFaultCount
=
0
;
allFault
s
.
forEach
((
f
:
any
)
=>
{
faultCodeRow
s
.
forEach
((
f
:
any
)
=>
{
const
code
=
f
.
fault_code
||
'未知'
;
const
code
=
f
.
fault_code
||
'未知'
;
faultCodeCountMap
.
set
(
code
,
(
faultCodeCountMap
.
get
(
code
)
||
0
)
+
1
);
const
cnt
=
Number
(
f
.
fault_count
)
||
0
;
totalFaultCount
++
;
faultCodeCountMap
.
set
(
code
,
(
faultCodeCountMap
.
get
(
code
)
||
0
)
+
cnt
);
totalFaultCount
+=
cnt
;
});
});
const
gzflzb
=
Array
.
from
(
faultCodeCountMap
.
entries
())
const
gzflzb
=
Array
.
from
(
faultCodeCountMap
.
entries
())
...
@@ -2024,18 +2053,27 @@ export async function getRunMonitoring() {
...
@@ -2024,18 +2053,27 @@ export async function getRunMonitoring() {
====== MOCK DATA END ====== */
====== MOCK DATA END ====== */
// ---- 真实查询 ----
// ---- 真实查询 ----
const
todoCount
=
await
selectDataCountByParam
(
TABLENAME
.
设备故障表
,
{
fault_status
:
'未处理'
});
// 【性能优化+正确性】fault_status 为 INTEGER(0=未处理, 1=处理中, 2=已处理):
const
doingCount
=
await
selectDataCountByParam
(
TABLENAME
.
设备故障表
,
{
fault_status
:
'处理中'
});
// 原先用中文条件与 INTEGER 列比较会被 MySQL 隐式转为 0,导致三个计数相同,
const
doneCount
=
await
selectDataCountByParam
(
TABLENAME
.
设备故障表
,
{
fault_status
:
'已处理'
});
// 现改为一次 GROUP BY 按真实状态统计,三次全表 count 合并为一次。
const
statusCountRows
=
await
mysqlDB
.
query
(
`SELECT fault_status, COUNT(*) AS cnt FROM
${
TABLENAME
.
设备故障表
}
GROUP BY fault_status`
,
{
type
:
Sequelize
.
QueryTypes
.
SELECT
}
)
as
any
[];
const
faultStatusCountMap
=
new
Map
<
number
,
number
>
();
statusCountRows
.
forEach
((
r
:
any
)
=>
{
const
st
=
typeof
r
.
fault_status
===
'string'
?
Number
(
r
.
fault_status
)
:
r
.
fault_status
;
faultStatusCountMap
.
set
(
st
,
Number
(
r
.
cnt
)
||
0
);
});
const
todoCount
=
faultStatusCountMap
.
get
(
0
)
||
0
;
// 未处理
const
doingCount
=
faultStatusCountMap
.
get
(
1
)
||
0
;
// 处理中
const
doneCount
=
faultStatusCountMap
.
get
(
2
)
||
0
;
// 已处理
// 查询未处理和处理中的工单列表(含设备信息)
// 查询未处理和处理中的工单列表(含设备信息)
const
pendingFaultsResult
=
await
selectDataListByParam
(
const
pendingFaultsResult
=
await
selectDataListByParam
(
TABLENAME
.
设备故障表
,
TABLENAME
.
设备故障表
,
{
{
'%or%'
:
[
fault_status
:
{
'%in%'
:
[
0
,
1
]
},
{
fault_status
:
'未处理'
},
{
fault_status
:
'处理中'
}
],
'%orderDesc%'
:
'fault_time'
,
'%orderDesc%'
:
'fault_time'
,
'%limit%'
:
20
,
'%limit%'
:
20
,
},
},
...
@@ -2051,25 +2089,19 @@ export async function getRunMonitoring() {
...
@@ -2051,25 +2089,19 @@ export async function getRunMonitoring() {
deviceRegionMap
.
set
(
d
.
device_id
,
regionNameMap
.
get
(
d
.
region_key
)
||
''
);
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
)
=>
({
const
ywczztDataList
=
pendingFaults
.
map
((
f
:
any
)
=>
({
faultDescription
:
f
.
fault_type
||
'未知故障'
,
faultDescription
:
f
.
fault_type
||
'未知故障'
,
occurredTime
:
f
.
fault_time
?
formatTime
(
f
.
fault_time
)
:
''
,
occurredTime
:
f
.
fault_time
?
formatTime
(
f
.
fault_time
)
:
''
,
regionName
:
deviceRegionMap
.
get
(
f
.
device_id
)
||
''
,
regionName
:
deviceRegionMap
.
get
(
f
.
device_id
)
||
''
,
deviceId
:
f
.
device_id
,
deviceId
:
f
.
device_id
,
status
:
faultStatusMap
[
f
.
fault_status
]
??
0
,
// fault_status 本身即 0=未处理 1=处理中 2=已处理,直接透传
status
:
typeof
f
.
fault_status
===
'number'
?
f
.
fault_status
:
Number
(
f
.
fault_status
)
||
0
,
}));
}));
let
ywczzt
=
{
let
ywczzt
=
{
"todo"
:
t
ypeof
todoCount
.
data
===
'number'
?
todoCount
.
data
:
0
,
"todo"
:
t
odoCount
,
"doing"
:
typeof
doingCount
.
data
===
'number'
?
doingCount
.
data
:
0
,
"doing"
:
doingCount
,
"done"
:
typeof
doneCount
.
data
===
'number'
?
doneCount
.
data
:
0
,
"done"
:
doneCount
,
"titleList"
:
[
"风险内容"
,
"告警时间"
,
"告警位置"
,
"设备编号"
,
"状态"
],
"titleList"
:
[
"风险内容"
,
"告警时间"
,
"告警位置"
,
"设备编号"
,
"状态"
],
"dataList"
:
ywczztDataList
,
"dataList"
:
ywczztDataList
,
};
};
...
@@ -2487,7 +2519,7 @@ export async function controlAcRunning(params: {}) {
...
@@ -2487,7 +2519,7 @@ export async function controlAcRunning(params: {}) {
updated_at
:
now
,
updated_at
:
now
,
}];
}];
await
addData
(
TABLENAME
.
设备日志表
,
acLogs
);
await
addData
(
TABLENAME
.
设备日志表
,
acLogs
);
return
await
controlIndoorUnit
(
acPrarms
)
;
return
result
;
}
}
...
...
src/biz/users.ts
View file @
d5c0f8ef
...
@@ -21,14 +21,14 @@ const USER_COLUMNS = ['login_id', 'name', 'phone', 'role', 'department', 'status
...
@@ -21,14 +21,14 @@ const USER_COLUMNS = ['login_id', 'name', 'phone', 'role', 'department', 'status
* @param pwd
* @param pwd
*/
*/
export
async
function
registerUser
(
loginId
:
string
,
pwd
:
string
,
name
:
string
)
{
export
async
function
registerUser
(
loginId
:
string
,
pwd
:
string
,
name
:
string
)
{
let
filesList
=
[
"login
I
d"
,
"id"
,
"pwd"
,
"name"
];
let
filesList
=
[
"login
_i
d"
,
"id"
,
"pwd"
,
"name"
];
let
adminUserInfo
:
any
=
await
selectOneDataByParam
(
TABLENAME
.
用户信息表
,
{
loginId
},
filesList
);
let
adminUserInfo
:
any
=
await
selectOneDataByParam
(
TABLENAME
.
用户信息表
,
{
login
_id
:
login
Id
},
filesList
);
if
(
!
adminUserInfo
)
{
if
(
!
adminUserInfo
)
{
throw
new
BizError
(
ERRORENUM
.
用户已存在
);
throw
new
BizError
(
ERRORENUM
.
用户已存在
);
}
}
await
addData
(
TABLENAME
.
用户信息表
,
{
loginId
,
pwd
:
getPwdMd5
(
loginId
,
pwd
),
name
:
name
});
await
addData
(
TABLENAME
.
用户信息表
,
{
login
_id
:
login
Id
,
pwd
:
getPwdMd5
(
loginId
,
pwd
),
name
:
name
});
return
{
success
:
true
};
return
{
success
:
true
};
}
}
...
...
src/config/businessEnum.ts
View file @
d5c0f8ef
...
@@ -88,6 +88,17 @@ export enum REGION_TYPE {
...
@@ -88,6 +88,17 @@ export enum REGION_TYPE {
监测点
=
'监测点'
,
监测点
=
'监测点'
,
}
}
/**
* 区域名称映射
*/
export
enum
REGION_NAME
{
/** 迪勤 - 飞奕 */
大会议室
=
'亚马逊'
,
大办公室
=
'环球港'
,
小办公室
=
'综合办'
,
仓库
=
'仓库408'
,
}
// ==================== 关联相关枚举 ====================
// ==================== 关联相关枚举 ====================
/**
/**
...
...
src/config/mysqlTableConfig.ts
View file @
d5c0f8ef
...
@@ -334,6 +334,11 @@ export const TablesConfig = [
...
@@ -334,6 +334,11 @@ export const TablesConfig = [
comment
:
'修改时间'
comment
:
'修改时间'
}
}
},
},
// 大表查询索引:device_id + device_time 组合覆盖“按设备查时间区间”,device_time 单独覆盖跨设备按时间区间查询
indexes
:
[
{
name
:
'idx_device_data_device_id_time'
,
fields
:
[
'device_id'
,
'device_time'
]
},
{
name
:
'idx_device_data_device_time'
,
fields
:
[
'device_time'
]
}
],
association
:
[
association
:
[
{
{
type
:
'belongsTo'
,
type
:
'belongsTo'
,
...
@@ -597,6 +602,11 @@ export const TablesConfig = [
...
@@ -597,6 +602,11 @@ export const TablesConfig = [
comment
:
'修改时间'
comment
:
'修改时间'
}
}
},
},
// 故障统计/列表查询索引:device_id 覆盖单设备维度,fault_status + fault_time 覆盖状态计数与最新故障排序
indexes
:
[
{
name
:
'idx_device_fault_device_id'
,
fields
:
[
'device_id'
]
},
{
name
:
'idx_device_fault_status_time'
,
fields
:
[
'fault_status'
,
'fault_time'
]
}
],
association
:
[
association
:
[
{
{
type
:
'belongsTo'
,
type
:
'belongsTo'
,
...
...
src/model/sqlModelBind.ts
View file @
d5c0f8ef
...
@@ -25,7 +25,7 @@ export async function initMysqlModel() {
...
@@ -25,7 +25,7 @@ export async function initMysqlModel() {
/**第一步:初始化所有表 */
/**第一步:初始化所有表 */
for
(
let
i
=
0
;
i
<
TablesConfig
.
length
;
i
++
)
{
for
(
let
i
=
0
;
i
<
TablesConfig
.
length
;
i
++
)
{
let
{
tableName
,
schema
}
=
TablesConfig
[
i
];
let
{
tableName
,
schema
,
indexes
}
=
TablesConfig
[
i
];
if
(
!
tableName
)
{
if
(
!
tableName
)
{
console
.
warn
(
`⚠️ 第
${
i
}
个表配置缺少 tableName,跳过`
);
console
.
warn
(
`⚠️ 第
${
i
}
个表配置缺少 tableName,跳过`
);
...
@@ -39,11 +39,16 @@ export async function initMysqlModel() {
...
@@ -39,11 +39,16 @@ export async function initMysqlModel() {
console
.
log
(
`🔄 正在初始化表:
${
tableName
}
`
);
console
.
log
(
`🔄 正在初始化表:
${
tableName
}
`
);
let
schemaConf
=
{
let
schemaConf
:
any
=
{
freezeTableName
:
true
,
freezeTableName
:
true
,
timestamps
:
false
timestamps
:
false
};
};
// 表级索引(新环境 sync 建表时生效;存量库需人工执行对应 DDL)
if
(
indexes
&&
indexes
.
length
>
0
)
{
schemaConf
.
indexes
=
indexes
;
}
try
{
try
{
let
model
=
mysqlDB
.
define
(
tableName
,
schema
,
schemaConf
);
let
model
=
mysqlDB
.
define
(
tableName
,
schema
,
schemaConf
);
mysqlModelMap
[
tableName
]
=
await
model
.
sync
({});
mysqlModelMap
[
tableName
]
=
await
model
.
sync
({});
...
...
src/routers/admin.ts
View file @
d5c0f8ef
...
@@ -352,6 +352,7 @@ async function editDevice(req, res) {
...
@@ -352,6 +352,7 @@ async function editDevice(req, res) {
async
function
batchEditDevices
(
req
,
res
)
{
async
function
batchEditDevices
(
req
,
res
)
{
let
reqConf
=
{
let
reqConf
=
{
regionKeys
:
'Array'
,
regionKeys
:
'Array'
,
deviceIds
:
'Array'
,
deviceName
:
'String'
,
deviceName
:
'String'
,
power
:
'String'
,
power
:
'String'
,
state
:
'String'
,
state
:
'String'
,
...
@@ -360,12 +361,12 @@ async function batchEditDevices(req, res) {
...
@@ -360,12 +361,12 @@ async function batchEditDevices(req, res) {
endDate
:
'String'
,
endDate
:
'String'
,
edits
:
'Object'
,
edits
:
'Object'
,
};
};
const
NotMustHaveKeys
=
[
"deviceName"
,
"power"
,
"state"
,
"deviceMode"
,
"startDate"
,
"endDate"
];
const
NotMustHaveKeys
=
[
"
regionKeys"
,
"deviceIds"
,
"
deviceName"
,
"power"
,
"state"
,
"deviceMode"
,
"startDate"
,
"endDate"
];
let
{
regionKeys
,
deviceName
,
power
,
state
,
deviceMode
,
startDate
,
endDate
,
edits
}
=
let
{
regionKeys
,
device
Ids
,
device
Name
,
power
,
state
,
deviceMode
,
startDate
,
endDate
,
edits
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
if
(
!
regionKeys
||
!
regionKeys
.
length
)
{
if
(
(
!
regionKeys
||
!
regionKeys
.
length
)
&&
(
!
deviceIds
||
!
deviceIds
.
length
)
)
{
res
.
success
({
success
:
false
,
msg
:
"缺少必填参数:regionKeys"
});
res
.
success
({
success
:
false
,
msg
:
"缺少必填参数:regionKeys
或 deviceIds
"
});
return
;
return
;
}
}
if
(
!
edits
||
typeof
edits
!==
'object'
)
{
if
(
!
edits
||
typeof
edits
!==
'object'
)
{
...
@@ -394,6 +395,7 @@ async function batchEditDevices(req, res) {
...
@@ -394,6 +395,7 @@ async function batchEditDevices(req, res) {
const
data
=
await
deviceBiz
.
batchEditDevices
({
const
data
=
await
deviceBiz
.
batchEditDevices
({
regionKeys
,
regionKeys
,
deviceIds
:
deviceIds
,
deviceName
:
deviceName
||
undefined
,
deviceName
:
deviceName
||
undefined
,
power
:
power
||
undefined
,
power
:
power
||
undefined
,
state
:
state
||
undefined
,
state
:
state
||
undefined
,
...
@@ -533,14 +535,14 @@ async function getLinkageList(req, res) {
...
@@ -533,14 +535,14 @@ async function getLinkageList(req, res) {
async
function
getLinkageDetail
(
req
,
res
)
{
async
function
getLinkageDetail
(
req
,
res
)
{
const
id
=
Number
(
req
.
query
?.
id
);
const
id
=
Number
(
req
.
query
?.
id
);
if
(
!
id
)
{
if
(
!
id
)
{
res
.
error
(
"缺少必填参数:id"
);
res
.
success
({
success
:
false
,
msg
:
"缺少必填参数:id"
}
);
return
;
return
;
}
}
const
data
=
await
deviceLinkageBiz
.
getLinkageDetail
(
id
);
const
data
=
await
deviceLinkageBiz
.
getLinkageDetail
(
id
);
if
(
data
.
isSuccess
)
{
if
(
data
.
isSuccess
)
{
res
.
success
(
data
.
data
);
res
.
success
(
data
.
data
);
}
else
{
}
else
{
res
.
error
(
data
.
msg
);
res
.
success
({
success
:
false
,
msg
:
data
.
msg
}
);
}
}
}
}
...
@@ -571,7 +573,7 @@ async function addLinkage(req, res) {
...
@@ -571,7 +573,7 @@ async function addLinkage(req, res) {
let
{
deviceId
,
linkageType
,
thresholdMax
,
thresholdMin
,
thresholdMid
,
settingMax
,
settingMin
,
settingMid
,
fanSpeedMax
,
fanSpeedMid
,
fanSpeedMin
,
divideMethod
,
beginValidity
,
endValidity
}
=
let
{
deviceId
,
linkageType
,
thresholdMax
,
thresholdMin
,
thresholdMid
,
settingMax
,
settingMin
,
settingMid
,
fanSpeedMax
,
fanSpeedMid
,
fanSpeedMin
,
divideMethod
,
beginValidity
,
endValidity
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
if
(
!
deviceId
||
!
linkageType
)
{
if
(
!
deviceId
||
!
linkageType
)
{
res
.
error
(
"缺少必填参数:deviceId, linkageType"
);
res
.
success
({
success
:
false
,
msg
:
"缺少必填参数:deviceId, linkageType"
}
);
return
;
return
;
}
}
const
data
=
await
deviceLinkageBiz
.
addLinkage
({
const
data
=
await
deviceLinkageBiz
.
addLinkage
({
...
@@ -620,7 +622,7 @@ async function batchAddLinkage(req, res) {
...
@@ -620,7 +622,7 @@ async function batchAddLinkage(req, res) {
let
{
deviceIds
,
linkageType
,
thresholdMax
,
thresholdMin
,
thresholdMid
,
settingMax
,
settingMin
,
settingMid
,
fanSpeedMax
,
fanSpeedMid
,
fanSpeedMin
,
divideMethod
,
beginValidity
,
endValidity
}
=
let
{
deviceIds
,
linkageType
,
thresholdMax
,
thresholdMin
,
thresholdMid
,
settingMax
,
settingMin
,
settingMid
,
fanSpeedMax
,
fanSpeedMid
,
fanSpeedMin
,
divideMethod
,
beginValidity
,
endValidity
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
if
(
!
deviceIds
||
!
Array
.
isArray
(
deviceIds
)
||
deviceIds
.
length
===
0
||
!
linkageType
)
{
if
(
!
deviceIds
||
!
Array
.
isArray
(
deviceIds
)
||
deviceIds
.
length
===
0
||
!
linkageType
)
{
res
.
error
(
"缺少必填参数:deviceIds, linkageType"
);
res
.
success
({
success
:
false
,
msg
:
"缺少必填参数:deviceIds, linkageType"
}
);
return
;
return
;
}
}
const
data
=
await
deviceLinkageBiz
.
batchAddLinkage
({
const
data
=
await
deviceLinkageBiz
.
batchAddLinkage
({
...
@@ -669,7 +671,7 @@ async function editLinkage(req, res) {
...
@@ -669,7 +671,7 @@ async function editLinkage(req, res) {
const
NotMustHaveKeys
=
[
"deviceId"
,
"linkageType"
,
"thresholdMax"
,
"thresholdMin"
,
"thresholdMid"
,
"settingMax"
,
"settingMin"
,
"settingMid"
,
"fanSpeedMax"
,
"fanSpeedMid"
,
"fanSpeedMin"
,
"isUse"
,
"divideMethod"
,
"beginValidity"
,
"endValidity"
];
const
NotMustHaveKeys
=
[
"deviceId"
,
"linkageType"
,
"thresholdMax"
,
"thresholdMin"
,
"thresholdMid"
,
"settingMax"
,
"settingMin"
,
"settingMid"
,
"fanSpeedMax"
,
"fanSpeedMid"
,
"fanSpeedMin"
,
"isUse"
,
"divideMethod"
,
"beginValidity"
,
"endValidity"
];
let
{
id
,
deviceId
,
linkageType
,
thresholdMax
,
thresholdMin
,
thresholdMid
,
settingMax
,
settingMin
,
settingMid
,
fanSpeedMax
,
fanSpeedMid
,
fanSpeedMin
,
isUse
,
divideMethod
,
beginValidity
,
endValidity
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
let
{
id
,
deviceId
,
linkageType
,
thresholdMax
,
thresholdMin
,
thresholdMid
,
settingMax
,
settingMin
,
settingMid
,
fanSpeedMax
,
fanSpeedMid
,
fanSpeedMin
,
isUse
,
divideMethod
,
beginValidity
,
endValidity
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
if
(
!
id
)
{
if
(
!
id
)
{
res
.
error
(
"缺少必填参数:id"
);
res
.
success
({
success
:
false
,
msg
:
"缺少必填参数:id"
}
);
return
;
return
;
}
}
const
data
=
await
deviceLinkageBiz
.
editLinkage
({
const
data
=
await
deviceLinkageBiz
.
editLinkage
({
...
@@ -701,7 +703,7 @@ async function editLinkage(req, res) {
...
@@ -701,7 +703,7 @@ async function editLinkage(req, res) {
async
function
deleteLinkage
(
req
,
res
)
{
async
function
deleteLinkage
(
req
,
res
)
{
const
{
id
}
=
req
.
body
||
{};
const
{
id
}
=
req
.
body
||
{};
if
(
!
id
)
{
if
(
!
id
)
{
res
.
error
(
"缺少必填参数:id"
);
res
.
success
({
success
:
false
,
msg
:
"缺少必填参数:id"
}
);
return
;
return
;
}
}
const
data
=
await
deviceLinkageBiz
.
deleteLinkage
(
Number
(
id
));
const
data
=
await
deviceLinkageBiz
.
deleteLinkage
(
Number
(
id
));
...
...
src/routers/customs.ts
View file @
d5c0f8ef
...
@@ -4,18 +4,38 @@
...
@@ -4,18 +4,38 @@
import
asyncHandler
from
'express-async-handler'
;
import
asyncHandler
from
'express-async-handler'
;
import
*
as
customsBiz
from
'../biz/customs'
;
import
*
as
customsBiz
from
'../biz/customs'
;
import
{
eccReqParamater
}
from
'../util/verificationParam'
;
import
{
eccReqParamater
}
from
'../util/verificationParam'
;
import
{
getCache
,
setCache
}
from
'../util/ttlCache'
;
export
function
setRouter
(
httpServer
)
{
export
function
setRouter
(
httpServer
)
{
/** 客流体验分析 */
/** 客流体验分析 */
httpServer
.
post
(
'/api/dq/customs/analysis'
,
asyncHandler
(
getCustomsAnalysis
));
httpServer
.
post
(
'/api/dq/customs/analysis'
,
asyncHandler
(
getCustomsAnalysis
));
/** 客流体验环境指标弹窗 */
httpServer
.
post
(
'/api/dq/customs/env_popup'
,
asyncHandler
(
getCustomsEnvPopup
));
}
}
/**
/**
* 客流体验分析
* 客流体验分析
* 大屏只读接口:加 30s TTL 缓存兜底,避免高频轮询重复打库
*/
*/
async
function
getCustomsAnalysis
(
req
,
res
)
{
const
CACHE_KEY
=
'dq:customs:analysis'
;
const
CACHE_TTL
=
30
*
1000
;
async
function
getCustomsAnalysis
(
req
,
res
)
{
const
{
}
=
req
.
body
;
const
{
}
=
req
.
body
;
const
cached
=
getCache
(
CACHE_KEY
);
if
(
cached
)
return
res
.
success
(
cached
);
const
result
=
await
customsBiz
.
getCustomsAnalysis
();
const
result
=
await
customsBiz
.
getCustomsAnalysis
();
setCache
(
CACHE_KEY
,
result
,
CACHE_TTL
);
res
.
success
(
result
);
}
/**
* 客流体验环境指标弹窗
* @param req
* @param res
*/
async
function
getCustomsEnvPopup
(
req
,
res
)
{
const
{
env
}
=
req
.
body
;
const
result
=
await
customsBiz
.
getCustomsEnvPopup
(
env
);
res
.
success
(
result
);
res
.
success
(
result
);
}
}
\ No newline at end of file
src/routers/public.ts
View file @
d5c0f8ef
...
@@ -8,6 +8,8 @@ const config = {
...
@@ -8,6 +8,8 @@ const config = {
"/api/public/deviceType"
:
enumBiz
.
DEVICE_TYPE
,
//设备类型
"/api/public/deviceType"
:
enumBiz
.
DEVICE_TYPE
,
//设备类型
"/api/public/deviceState"
:
enumBiz
.
DEVICE_STATE
,
//设备状态
"/api/public/deviceState"
:
enumBiz
.
DEVICE_STATE
,
//设备状态
"/api/public/regionType"
:
enumBiz
.
REGION_TYPE
,
//区域类型
"/api/public/regionType"
:
enumBiz
.
REGION_TYPE
,
//区域类型
"/api/public/fanSpeed"
:
enumBiz
.
fanSpeedMap
,
//风速
"/api/public/workMode"
:
enumBiz
.
workModeMap
,
//模式
}
}
export
function
setRouter
(
httpServer
)
{
export
function
setRouter
(
httpServer
)
{
...
...
src/routers/running.ts
View file @
d5c0f8ef
...
@@ -4,6 +4,7 @@
...
@@ -4,6 +4,7 @@
import
asyncHandler
from
'express-async-handler'
;
import
asyncHandler
from
'express-async-handler'
;
import
*
as
runningBiz
from
'../biz/running'
;
import
*
as
runningBiz
from
'../biz/running'
;
import
{
eccReqParamater
}
from
'../util/verificationParam'
;
import
{
eccReqParamater
}
from
'../util/verificationParam'
;
import
{
getCache
,
setCache
}
from
'../util/ttlCache'
;
export
function
setRouter
(
httpServer
)
{
export
function
setRouter
(
httpServer
)
{
...
@@ -29,10 +30,16 @@ export function setRouter(httpServer) {
...
@@ -29,10 +30,16 @@ export function setRouter(httpServer) {
/**
/**
* 运行分析
* 运行分析
* 大屏只读接口:加 30s TTL 缓存兜底,避免高频轮询重复打库
*/
*/
const
CACHE_TTL
=
30
*
1000
;
async
function
getRunAnalysis
(
req
,
res
)
{
async
function
getRunAnalysis
(
req
,
res
)
{
const
{
}
=
req
.
body
;
const
{
}
=
req
.
body
;
const
CACHE_KEY
=
'dq:run:analysis'
;
const
cached
=
getCache
(
CACHE_KEY
);
if
(
cached
)
return
res
.
success
(
cached
);
const
result
=
await
runningBiz
.
getRunAnalysis
();
const
result
=
await
runningBiz
.
getRunAnalysis
();
setCache
(
CACHE_KEY
,
result
,
CACHE_TTL
);
res
.
success
(
result
);
res
.
success
(
result
);
}
}
...
@@ -67,10 +74,15 @@ async function getRunEnvironmental(req, res) {
...
@@ -67,10 +74,15 @@ async function getRunEnvironmental(req, res) {
/**
/**
* 智能监控
* 智能监控
* 大屏只读接口:加 30s TTL 缓存兜底,避免高频轮询重复打库
*/
*/
async
function
getRunMonitoring
(
req
,
res
)
{
async
function
getRunMonitoring
(
req
,
res
)
{
const
{
}
=
req
.
body
;
const
{
}
=
req
.
body
;
const
CACHE_KEY
=
'dq:run:monitoring'
;
const
cached
=
getCache
(
CACHE_KEY
);
if
(
cached
)
return
res
.
success
(
cached
);
const
result
=
await
runningBiz
.
getRunMonitoring
();
const
result
=
await
runningBiz
.
getRunMonitoring
();
setCache
(
CACHE_KEY
,
result
,
CACHE_TTL
);
res
.
success
(
result
);
res
.
success
(
result
);
}
}
...
...
src/util/ttlCache.ts
0 → 100644
View file @
d5c0f8ef
/**
* 进程内 TTL 缓存(用于大屏只读分析接口防抖)
* 注意:仅适合无参或参数极少的只读接口;服务重启后缓存失效。
*/
interface
CacheItem
{
expire
:
number
;
data
:
any
;
}
const
DEFAULT_TTL
=
30
*
1000
;
// 默认 30 秒
const
MAX_SIZE
=
200
;
// 缓存条目上限,防止异常 key 膨胀
const
store
=
new
Map
<
string
,
CacheItem
>
();
export
function
getCache
<
T
=
any
>
(
key
:
string
):
T
|
undefined
{
const
item
=
store
.
get
(
key
);
if
(
!
item
)
return
undefined
;
if
(
item
.
expire
<=
Date
.
now
())
{
store
.
delete
(
key
);
return
undefined
;
}
return
item
.
data
;
}
export
function
setCache
(
key
:
string
,
data
:
any
,
ttlMs
:
number
=
DEFAULT_TTL
)
{
if
(
store
.
size
>=
MAX_SIZE
)
{
// 简单淘汰:优先清掉已过期项;都没有过期则整体清空(极少发生)
const
now
=
Date
.
now
();
for
(
const
[
k
,
v
]
of
store
)
{
if
(
v
.
expire
<=
now
)
{
store
.
delete
(
k
);
break
;
}
}
if
(
store
.
size
>=
MAX_SIZE
)
store
.
clear
();
}
store
.
set
(
key
,
{
expire
:
Date
.
now
()
+
ttlMs
,
data
});
}
/** 主动失效(例如数据被写入后需要立刻反映的场景) */
export
function
delCache
(
key
:
string
)
{
store
.
delete
(
key
);
}
tsconfig.json
View file @
d5c0f8ef
...
@@ -10,6 +10,7 @@
...
@@ -10,6 +10,7 @@
"strict"
:
false
,
"strict"
:
false
,
"noImplicitAny"
:
false
,
"noImplicitAny"
:
false
,
"strictNullChecks"
:
false
,
"strictNullChecks"
:
false
,
"skipLibCheck"
:
true
,
"types"
:
[
"node"
]
"types"
:
[
"node"
]
},
},
"exclude"
:
[
"exclude"
:
[
...
...
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