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
437999e3
Commit
437999e3
authored
Aug 28, 2026
by
PC-20251223ZVQQ\Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
问题修复
parent
456f47ee
Show whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
1738 additions
and
90 deletions
+1738
-90
IAQ.html
public/IAQ.html
+24
-21
serverConfig.xml
serverConfig.xml
+2
-1
dataIntegration.ts
src/biz/dataIntegration.ts
+5
-0
device.ts
src/biz/device.ts
+217
-7
deviceFault.ts
src/biz/deviceFault.ts
+1
-0
deviceLinkage.ts
src/biz/deviceLinkage.ts
+370
-0
feiyiClient.ts
src/biz/feiyiClient.ts
+388
-3
mqttClient.ts
src/biz/mqttClient.ts
+0
-7
region.ts
src/biz/region.ts
+1
-1
report.ts
src/biz/report.ts
+28
-11
running.ts
src/biz/running.ts
+113
-20
schedule.ts
src/biz/schedule.ts
+4
-4
dbEnum.ts
src/config/dbEnum.ts
+3
-1
mysqlTableConfig.ts
src/config/mysqlTableConfig.ts
+155
-1
findData.ts
src/data/findData.ts
+1
-0
main.ts
src/main.ts
+1
-1
watch.ts
src/middleware/watch.ts
+8
-0
admin.ts
src/routers/admin.ts
+264
-12
device.ts
src/routers/device.ts
+39
-0
feiyiClient.ts
src/routers/feiyiClient.ts
+114
-0
No files found.
public/IAQ.html
View file @
437999e3
...
...
@@ -4,8 +4,10 @@
<meta
charset=
"UTF-8"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
>
<title>
智能环境态势感知系统
</title>
<script
src=
"https://unpkg.com/vue@3/dist/vue.global.js"
></script>
<script
src=
"https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"
></script>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> -->
<script
src=
"./js/vue.global.js"
></script>
<script
src=
"./js/echarts.min.js"
></script>
<style>
/* ============ CSS 自定义属性(主题色) ============ */
:root
{
...
...
@@ -331,7 +333,7 @@
.gauge-right
{
flex
:
2
;
display
:
grid
;
grid-template-columns
:
repeat
(
4
,
1
fr
);
grid-template-columns
:
repeat
(
3
,
1
fr
);
grid-template-rows
:
repeat
(
2
,
1
fr
);
gap
:
6px
;
}
...
...
@@ -423,7 +425,7 @@
height
:
264px
;
}
.gauge-right
{
grid-template-columns
:
repeat
(
4
,
1
fr
);
grid-template-columns
:
repeat
(
3
,
1
fr
);
}
.gauge-small-item
.chart-container
{
height
:
165px
;
...
...
@@ -536,12 +538,12 @@
<th>
CO₂
</th>
<th>
PM2.5
</th>
<th>
PM10
</th>
<
th>
TVOC
</th
>
<
!-- <th>TVOC</th> --
>
<th>
温度
</th>
<th>
湿度
</th>
<th>
甲醛
</th>
<
th>
气压
</th
>
<
th>
光照
</th
>
<
!-- <th>气压</th> --
>
<
!-- <th>光照</th> --
>
</tr>
</thead>
<tbody>
...
...
@@ -552,12 +554,12 @@
<td>
{{ row.co2 }}
</td>
<td>
{{ row.pm2_5 }}
</td>
<td>
{{ row.pm10 }}
</td>
<
td>
{{ row.tvoc }}
</td
>
<
!-- <td>{{ row.tvoc }}</td> --
>
<td>
{{ row.temperature }}
</td>
<td>
{{ row.humidity }}
</td>
<td>
{{ row.hcho }}
</td>
<
td>
{{ row.pressure }}
</td
>
<
td>
{{ row.light_level }}
</td
>
<
!-- <td>{{ row.pressure }}</td> --
>
<
!-- <td>{{ row.light_level }}</td> --
>
</tr>
</tbody>
</table>
...
...
@@ -587,18 +589,18 @@
setup
()
{
// ======== 指标配置(静态) ========
const
INDICATOR_META
=
{
pm2_5
:
{
label
:
'PM2.5'
,
unit
:
'µg/m³'
,
max
:
100
,
decimals
:
0
},
co2
:
{
label
:
'CO₂'
,
unit
:
'ppm'
,
max
:
2500
,
decimals
:
0
},
pm2_5
:
{
label
:
'PM2.5'
,
unit
:
'µg/m³'
,
max
:
100
,
decimals
:
0
},
tvoc
:
{
label
:
'TVOC'
,
unit
:
'mg/m³'
,
max
:
3.0
,
decimals
:
2
},
temperature
:
{
label
:
'温度'
,
unit
:
'°C'
,
max
:
50
,
decimals
:
1
},
humidity
:
{
label
:
'湿度'
,
unit
:
'%RH'
,
max
:
100
,
decimals
:
1
},
hcho
:
{
label
:
'甲醛'
,
unit
:
'mg/m³'
,
max
:
0.2
,
decimals
:
2
},
pm10
:
{
label
:
'PM10'
,
unit
:
'µg/m³'
,
max
:
150
,
decimals
:
0
},
pressure
:
{
label
:
'气压'
,
unit
:
'hPa'
,
max
:
1100
,
decimals
:
1
},
light_level
:
{
label
:
'光照'
,
unit
:
'lux'
,
max
:
2000
,
decimals
:
0
},
//
pressure: { label: '气压', unit: 'hPa', max: 1100, decimals: 1 },
//
light_level: { label: '光照', unit: 'lux', max: 2000, decimals: 0 },
};
// 表格列映射:API dataList 索引 → 前端 key
const
TABLE_KEY_MAP
=
[
'co2'
,
'pm10'
,
'pm2_5'
,
'hcho'
,
'tvoc'
,
'temperature'
,
'humidity'
,
'pressure'
,
'light_level'
];
const
TABLE_KEY_MAP
=
[
'co2'
,
'pm10'
,
'pm2_5'
,
'hcho'
,
'tvoc'
,
'temperature'
,
'humidity'
];
//, 'pressure', 'light_level'
// ======== 响应式数据 ========
const
indicators
=
ref
([]);
...
...
@@ -607,7 +609,7 @@
const
deviceId
=
ref
(
'---'
);
const
apiDeviceNames
=
ref
([]);
// 从 /api/qdm/device/environment 返回的设备名列表
const
loading
=
ref
(
true
);
const
selectedKey
=
ref
(
'
pm2_5
'
);
const
selectedKey
=
ref
(
'
co2
'
);
const
currentTime
=
ref
(
''
);
const
deviceTree
=
ref
([]);
// 设备树 [{key,value,sub:[{key,value,sub:[{key,value,sub:[]}]}]}]
const
selectedFilters
=
ref
([]);
// 已确认的筛选 [{label,regionGroup?,regionType?,deviceId?,nodeType,nodeKey}]
...
...
@@ -680,7 +682,7 @@
function
renderIndicators
(
apiIndicators
,
areaInfo
)
{
const
list
=
[];
const
order
=
[
'
pm2_5'
,
'co2'
,
'tvoc'
,
'temperature'
,
'humidity'
,
'hcho'
,
'pm10'
,
'pressure'
,
'light_level'
];
const
order
=
[
'
co2'
,
'pm2_5'
,
'temperature'
,
'tvoc'
,
'humidity'
,
'hcho'
,
'pm10'
];
// , 'pressure', 'light_level'
const
valueMap
=
{};
const
qualityMap
=
{};
(
apiIndicators
||
[]).
forEach
(
api
=>
{
...
...
@@ -692,7 +694,7 @@
if
(
!
meta
)
return
;
list
.
push
({
key
,
label
:
meta
.
label
,
unit
:
meta
.
unit
,
value
:
valueMap
[
key
]
!=
null
?
valueMap
[
key
]
:
0
,
value
:
key
===
'tvoc'
?
(
valueMap
[
key
]
!=
null
?
valueMap
[
key
]
>
1
?
valueMap
[
key
]
-
1
:
valueMap
[
key
]
:
0
)
:
valueMap
[
key
]
!=
null
?
valueMap
[
key
]
:
0
,
max
:
meta
.
max
,
decimals
:
meta
.
decimals
,
quality
:
qualityMap
[
key
]
||
null
,
});
...
...
@@ -960,13 +962,14 @@
const
STATUS_ORANGE
=
'#d98c3a'
;
// 暖调铜橙
const
STATUS_RED
=
'#d1483a'
;
// 暖调砖红
function
getStatus
(
ind
)
{
const
idx
=
ind
.
key
;
const
q
=
ind
.
quality
;
if
(
!
q
)
return
[
'—'
,
'#999'
];
switch
(
q
)
{
case
'优'
:
return
[
'
优
'
,
STATUS_GREEN
];
case
'良'
:
return
[
'
良'
,
STATUS_BLUE
];
case
'中'
:
return
[
'中
'
,
STATUS_ORANGE
];
case
'差'
:
return
[
'差'
,
STATUS_RED
];
case
'优'
:
return
[
'
正常
'
,
STATUS_GREEN
];
case
'良'
:
return
[
'
正常'
,
STATUS_GREEN
];
case
'中'
:
return
idx
===
'tvoc'
?
[
'正常'
,
STATUS_GREEN
]
:
[
'一般
'
,
STATUS_ORANGE
];
case
'差'
:
return
idx
===
'tvoc'
?
[
'正常'
,
STATUS_GREEN
]
:
[
'一般'
,
STATUS_ORANGE
];
case
'有人'
:
return
[
'有人'
,
STATUS_GREEN
];
case
'无感应'
:
return
[
'无感应'
,
'#999'
];
default
:
return
[
q
,
'#999'
];
...
...
serverConfig.xml
View file @
437999e3
...
...
@@ -17,7 +17,8 @@
<dataBase>beermuseumplatform</dataBase> -->
</mysqldb>
<mqttsrv>
<mqttHost>
10.100.48.16
</mqttHost>
<!-- <mqttHost>10.100.48.16</mqttHost> -->
<mqttHost>
192.168.125.88
</mqttHost>
<mqttPort>
1883
</mqttPort>
<mqttTopic1>
/milesight/uplink/2026
</mqttTopic1>
<mqttTopic2>
/milesight/uplink/2026_crowd
</mqttTopic2>
...
...
src/biz/dataIntegration.ts
View file @
437999e3
...
...
@@ -7,6 +7,7 @@ import {
getIndoorUnitAlarmErrorHis
,
getFaultCodeInfo
,
getFaultTypeName
,
saveRoomExpensesToExpansion
,
}
from
"./feiyiClient"
;
import
{
mysqlModelMap
}
from
"../model/sqlModelBind"
;
import
moment
from
"moment"
;
...
...
@@ -39,6 +40,8 @@ function getOuterAddress(indoorUnitAddressFull: string): string | null {
/**
* 从 Excel 文件读取电表设备编号 → 外机地址 的映射表
*
*/
function
loadMeterOuterAddrMap
():
Map
<
string
,
string
>
{
const
map
=
new
Map
<
string
,
string
>
();
...
...
@@ -540,6 +543,8 @@ export async function deviceData() {
try
{
await
integrateAcDeviceData
();
await
integrateMeterDeviceData
();
// 电费数据集成:拉取当天房间电费日使用详情,按 deviceTime 去重后写入 device_expansion
await
saveRoomExpensesToExpansion
(
getTodayDateStr
(),
'电能监测'
);
console
.
log
(
'[设备数据集成] 完成'
);
}
catch
(
err
)
{
console
.
error
(
'[设备数据集成] 执行异常:'
,
err
);
...
...
src/biz/device.ts
View file @
437999e3
...
...
@@ -765,6 +765,203 @@ export async function getEnvQualityPop(regionGroup?: string, regionType?: string
}
/**
* 运行分析 - 离线设备弹窗
* 按区域(regionGroup/regionType)查询离线设备(通信故障=2 / 离线=3),
* 返回设备编码、设备名、最后更新时间、区域key、区域名称
*/
export
async
function
getOfflineDeviceList
(
regionGroup
?:
string
,
regionType
?:
string
)
{
// 1. 查询区域
const
regionWhere
:
any
=
{};
if
(
regionGroup
)
regionWhere
.
groups
=
regionGroup
;
if
(
regionType
)
regionWhere
.
type
=
regionType
;
const
regionResult
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{
...
regionWhere
,
"%orderAsc%"
:
"sort_order"
},
[
"id"
,
"name"
,
"type"
,
"groups"
]);
const
regions
=
regionResult
.
data
||
[];
if
(
regions
.
length
===
0
)
return
{
total
:
0
,
dataList
:
[]
};
const
regionMap
:
{
[
id
:
number
]:
any
}
=
{};
regions
.
forEach
((
r
:
any
)
=>
{
regionMap
[
r
.
id
]
=
r
;
});
const
regionKeys
=
regions
.
map
((
r
:
any
)
=>
r
.
id
);
// 2. 查询离线设备
const
deviceResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
region_key
:
{
"%in%"
:
regionKeys
},
device_state
:
{
"%in%"
:
[
2
,
3
]
}
},
[
"device_id"
,
"device_name"
,
"region_key"
]);
const
devices
=
deviceResult
.
data
||
[];
if
(
devices
.
length
===
0
)
return
{
total
:
0
,
dataList
:
[]
};
// 3. 并行取每台设备最后上报时间
const
lastUpdateMap
:
{
[
devId
:
string
]:
string
}
=
{};
await
Promise
.
all
(
devices
.
map
(
async
(
d
:
any
)
=>
{
lastUpdateMap
[
d
.
device_id
]
=
await
getDeviceLastUpdateTime
(
d
.
device_id
);
}));
// 4. 组装返回
const
dataList
=
devices
.
map
((
d
:
any
)
=>
{
const
region
=
regionMap
[
d
.
region_key
];
return
{
deviceId
:
d
.
device_id
,
deviceName
:
d
.
device_name
||
d
.
device_id
,
lastUpdateTime
:
lastUpdateMap
[
d
.
device_id
],
regionKey
:
d
.
region_key
,
regionName
:
region
?
[
region
.
groups
,
region
.
type
,
region
.
name
].
filter
(
Boolean
).
join
(
'_'
)
:
''
};
});
return
{
total
:
dataList
.
length
,
dataList
};
}
/**
* 运行分析 - 故障设备弹窗
* 按区域(regionGroup/regionType)查询存在未解决故障(status != 2)的设备,
* 返回设备编码、设备名、最后更新时间、区域key、区域名称
*/
export
async
function
getFaultDeviceList
(
regionGroup
?:
string
,
regionType
?:
string
)
{
// 1. 查询区域
const
regionWhere
:
any
=
{};
if
(
regionGroup
)
regionWhere
.
groups
=
regionGroup
;
if
(
regionType
)
regionWhere
.
type
=
regionType
;
const
regionResult
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{
...
regionWhere
,
"%orderAsc%"
:
"sort_order"
},
[
"id"
,
"name"
,
"type"
,
"groups"
]);
const
regions
=
regionResult
.
data
||
[];
if
(
regions
.
length
===
0
)
return
{
total
:
0
,
dataList
:
[]
};
const
regionMap
:
{
[
id
:
number
]:
any
}
=
{};
regions
.
forEach
((
r
:
any
)
=>
{
regionMap
[
r
.
id
]
=
r
;
});
const
regionKeys
=
regions
.
map
((
r
:
any
)
=>
r
.
id
);
// 2. 查询区域内所有设备
const
deviceResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
region_key
:
{
"%in%"
:
regionKeys
}
},
[
"device_id"
,
"device_name"
,
"region_key"
]);
const
devices
=
deviceResult
.
data
||
[];
const
deviceIds
=
devices
.
map
((
d
:
any
)
=>
d
.
device_id
);
if
(
deviceIds
.
length
===
0
)
return
{
total
:
0
,
dataList
:
[]
};
// 3. 查询未解决的故障记录,按设备去重
const
faultResult
=
await
selectDataListByParam
(
TABLENAME
.
设备故障表
,
{
status
:
{
"%ne%"
:
2
},
device_id
:
{
"%in%"
:
deviceIds
}
},
[
"device_id"
]);
const
faultDeviceIdSet
=
new
Set
((
faultResult
.
data
||
[]).
map
((
f
:
any
)
=>
f
.
device_id
));
const
faultDevices
=
devices
.
filter
((
d
:
any
)
=>
faultDeviceIdSet
.
has
(
d
.
device_id
));
if
(
faultDevices
.
length
===
0
)
return
{
total
:
0
,
dataList
:
[]
};
// 4. 并行取每台设备最后上报时间
const
lastUpdateMap
:
{
[
devId
:
string
]:
string
}
=
{};
await
Promise
.
all
(
faultDevices
.
map
(
async
(
d
:
any
)
=>
{
lastUpdateMap
[
d
.
device_id
]
=
await
getDeviceLastUpdateTime
(
d
.
device_id
);
}));
// 5. 组装返回
const
dataList
=
faultDevices
.
map
((
d
:
any
)
=>
{
const
region
=
regionMap
[
d
.
region_key
];
return
{
deviceId
:
d
.
device_id
,
deviceName
:
d
.
device_name
||
d
.
device_id
,
lastUpdateTime
:
lastUpdateMap
[
d
.
device_id
],
regionKey
:
d
.
region_key
,
regionName
:
region
?
[
region
.
groups
,
region
.
type
,
region
.
name
].
filter
(
Boolean
).
join
(
'_'
)
:
''
};
});
return
{
total
:
dataList
.
length
,
dataList
};
}
/**
* 运行分析 - 设备能耗监控弹窗
* 展示各个区域昨日和今日能耗(用电量)
* 数据来源:device_expansion 区域电表电费数据(saveRoomExpensesToExpansion 写入,
* original_data 含 usePower 用电量、electricityUsageFee 电费、deviceTime 毫秒时间戳)
* @param regionGroup 区域分组
* @param regionType 区域类型
*/
export
async
function
getDeviceEnergyList
(
regionGroup
?:
string
,
regionType
?:
string
)
{
// 1. 查询区域
const
regionWhere
:
any
=
{};
if
(
regionGroup
)
regionWhere
.
groups
=
regionGroup
;
if
(
regionType
)
regionWhere
.
type
=
regionType
;
const
regionResult
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{
...
regionWhere
,
"%orderAsc%"
:
"sort_order"
},
[
"id"
,
"name"
,
"type"
,
"groups"
]);
const
regions
=
regionResult
.
data
||
[];
if
(
regions
.
length
===
0
)
return
{
total
:
0
,
dataList
:
[]
};
const
regionMap
:
{
[
id
:
number
]:
any
}
=
{};
regions
.
forEach
((
r
:
any
)
=>
{
regionMap
[
r
.
id
]
=
r
;
});
const
regionKeys
=
regions
.
map
((
r
:
any
)
=>
r
.
id
);
// 2. 查询区域电表电费扩展数据(device_expansion)
const
expansionResult
=
await
selectDataListByParam
(
TABLENAME
.
设备扩展表
,
{
region_key
:
{
"%in%"
:
regionKeys
}
},
[
"region_key"
,
"original_data"
]);
const
expansions
=
expansionResult
.
data
||
[];
if
(
expansions
.
length
===
0
)
return
{
total
:
0
,
dataList
:
[]
};
// 3. 时间边界:今日 0 点 / 昨日 0 点(deviceTime 为毫秒时间戳)
const
todayStart
=
new
Date
();
todayStart
.
setHours
(
0
,
0
,
0
,
0
);
const
todayMs
=
todayStart
.
getTime
();
const
yesterdayMs
=
todayMs
-
24
*
60
*
60
*
1000
;
const
tomorrowMs
=
todayMs
+
24
*
60
*
60
*
1000
;
// 4. 按区域聚合昨日/今日用电量(usePower)
const
agg
:
{
[
rk
:
number
]:
{
yesterdayEnergy
:
number
;
todayEnergy
:
number
}
}
=
{};
for
(
const
exp
of
expansions
)
{
const
row
=
exp
.
original_data
;
if
(
!
row
||
row
.
usePower
===
undefined
||
row
.
usePower
===
null
)
continue
;
const
deviceTime
=
Number
(
row
.
deviceTime
);
if
(
!
deviceTime
||
isNaN
(
deviceTime
))
continue
;
// 只统计昨日和今日时间窗内,未来数据忽略
if
(
deviceTime
<
yesterdayMs
||
deviceTime
>=
tomorrowMs
)
continue
;
if
(
!
agg
[
exp
.
region_key
])
agg
[
exp
.
region_key
]
=
{
yesterdayEnergy
:
0
,
todayEnergy
:
0
};
// 电表电量可能出现微小负差值,按 0 处理
const
energy
=
Math
.
max
(
0
,
Number
(
row
.
usePower
)
||
0
);
if
(
deviceTime
>=
todayMs
)
{
agg
[
exp
.
region_key
].
todayEnergy
+=
energy
;
}
else
{
agg
[
exp
.
region_key
].
yesterdayEnergy
+=
energy
;
}
}
// 5. 组装返回(仅返回有能耗数据的区域)
const
dataList
=
Object
.
keys
(
agg
)
.
map
((
rkStr
)
=>
Number
(
rkStr
))
.
filter
((
rk
)
=>
regionMap
[
rk
])
.
map
((
rk
)
=>
{
const
region
=
regionMap
[
rk
];
return
{
regionKey
:
rk
,
regionName
:
[
region
.
groups
,
region
.
type
,
region
.
name
].
filter
(
Boolean
).
join
(
'_'
),
yesterdayEnergy
:
Math
.
round
(
agg
[
rk
].
yesterdayEnergy
*
100
)
/
100
,
todayEnergy
:
Math
.
round
(
agg
[
rk
].
todayEnergy
*
100
)
/
100
};
})
.
sort
((
a
,
b
)
=>
a
.
regionKey
-
b
.
regionKey
);
return
{
total
:
dataList
.
length
,
dataList
};
}
/**
* 获取设备最后数据上报时间(device_data 最新一条)
* 优先取 device_time,无则取 received_time;无上报记录返回空字符串
*/
async
function
getDeviceLastUpdateTime
(
deviceId
:
string
):
Promise
<
string
>
{
try
{
const
dataResult
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
deviceId
,
"%orderDesc%"
:
"received_time"
,
"%limit%"
:
1
},
[
"device_time"
,
"received_time"
]);
const
rec
=
dataResult
.
data
?.[
0
];
if
(
!
rec
)
return
""
;
const
raw
=
rec
.
device_time
||
rec
.
received_time
;
if
(
!
raw
)
return
""
;
const
d
=
raw
instanceof
Date
?
raw
:
new
Date
(
raw
);
return
isNaN
(
d
.
getTime
())
?
""
:
formatLocalTime
(
d
);
}
catch
(
e
)
{
return
""
;
}
}
/**
* 获取设备列表
* @param params
*/
...
...
@@ -969,15 +1166,15 @@ export async function editAcDeviceControl(params: {
setTemp
?:
number
;
fanSpeed
?:
string
;
deviceMode
?:
string
;
lock
?:
string
;
lock
ed
?:
string
;
}):
Promise
<
{
success
:
boolean
;
message
:
string
}
>
{
const
{
deviceId
,
power
,
setTemp
,
fanSpeed
,
deviceMode
,
lock
}
=
params
;
const
{
deviceId
,
power
,
setTemp
,
fanSpeed
,
deviceMode
,
lock
ed
}
=
params
;
// 1. 校验设备是否存在
let
device
=
await
selectOneDataByParam
(
TABLENAME
.
设备表
,
{
device_id
:
deviceId
},
[
"id"
,
"device_id"
,
"device_name"
]
[
"id"
,
"device_id"
,
"device_
type"
,
"device_
name"
]
);
if
(
!
device
.
data
||
!
device
.
data
.
id
)
{
throw
new
BizError
(
ERRORENUM
.
参数错误
,
`设备
${
deviceId
}
未注册`
);
...
...
@@ -990,8 +1187,15 @@ export async function editAcDeviceControl(params: {
"openApiAction"
:
"control"
,
};
if
(
setTemp
!=
null
)
acParams
.
tempSet
=
setTemp
;
if
(
fanSpeed
)
acParams
.
fanSpeed
=
acDeviceKeyMap
[
fanSpeed
];
acParams
.
fanSpeed
=
fanSpeed
?
acDeviceKeyMap
[
fanSpeed
]
:
2
;
// 默认中风
if
(
deviceMode
)
acParams
.
workMode
=
acDeviceKeyMap
[
deviceMode
];
// 锁定参数(语义: locked='on'=锁定开启=已锁定, locked='off'=锁定关闭=未锁定)
if
(
locked
!==
undefined
&&
locked
!==
''
)
{
const
lockVal
=
locked
===
'on'
?
1
:
0
;
acParams
.
onOffLock
=
lockVal
;
acParams
.
workModeLock
=
lockVal
;
acParams
.
tempSetLock
=
lockVal
;
}
// 3. 调用飞奕控制接口
const
result
=
await
controlIndoorUnit
(
acParams
);
...
...
@@ -1000,7 +1204,7 @@ export async function editAcDeviceControl(params: {
const
now
=
new
Date
();
const
logEntry
=
{
device_id
:
deviceId
,
device_type
:
'空调'
,
device_type
:
device
.
data
.
device_type
,
operation_content
:
{
option
:
'设备管理编辑'
,
device_id
:
deviceId
,
...
...
@@ -1059,7 +1263,7 @@ export async function batchEditDevices(params: {
}
// 1. 构建设备查询条件
let
whereParam
:
any
=
{
device_type
:
'空调'
};
let
whereParam
:
any
=
{
device_type
:
{
"%in%"
:
[
"空调"
,
"新风"
]
}
};
whereParam
.
region_key
=
{
"%in%"
:
regionKeys
};
if
(
deviceName
)
{
...
...
@@ -1143,6 +1347,7 @@ export async function batchEditDevices(params: {
acParams
.
tempSet
=
edits
.
setTemp
;
}
if
(
edits
.
locked
!==
undefined
)
{
// 语义: locked='on'=锁定, locked='off'=解锁
const
lockVal
=
edits
.
locked
===
'on'
?
1
:
0
;
acParams
.
onOffLock
=
lockVal
;
acParams
.
workModeLock
=
lockVal
;
...
...
@@ -1290,6 +1495,7 @@ export async function batchEditLocalDevices(params: {
export
async
function
getDeviceLogList
(
params
:
{
pageNumber
?:
number
;
pageSize
?:
number
;
deviceId
?:
string
;
deviceName
?:
string
;
// 设备名称(模糊匹配,通过 device 表关联)
status
?:
number
;
// 操作状态:0失败 1成功
})
{
...
...
@@ -1320,6 +1526,9 @@ export async function getDeviceLogList(params: {
}
queryParam
.
device_id
=
{
"%in%"
:
deviceIds
};
}
if
(
params
.
deviceId
)
{
queryParam
.
device_id
=
params
.
deviceId
;
}
// 分页查询,按创建时间倒序
const
result
=
await
selectPaginatedDataWithOrder
(
...
...
@@ -1360,6 +1569,6 @@ export async function getDeviceLogList(params: {
return
{
list
,
pagination
:
result
.
pagination
,
total
:
result
.
total
,
};
}
\ No newline at end of file
src/biz/deviceFault.ts
View file @
437999e3
...
...
@@ -79,6 +79,7 @@ export async function getDeviceFaultList(params: AlertWorkOrderQueryParams) {
return
{
list
,
total
:
result
.
total
,
pagination
:
result
.
pagination
,
};
}
src/biz/deviceLinkage.ts
0 → 100644
View file @
437999e3
/**
* 设备联动调节 业务逻辑(后台管理配置联动参数)
*
* 联动逻辑与 running.ts 中写死的温度联动(calcAcTempSet)保持一致,阈值改为可配置:
* - 制冷(夏季):temperature >= threshold_max → setting_max;中间区间 → setting_mid;< threshold_min → setting_min
* - 制热(冬季):temperature > threshold_max → setting_max;中间区间 → setting_mid;<= threshold_min → setting_min
* - 新风:CO2 联动(threshold_max=1000 → 高风,threshold_min=800 → 低风/待机)
*/
import
{
selectDataListByParam
}
from
"../data/findData"
;
import
{
selectPaginatedDataWithOrder
}
from
"../data/findData"
;
import
{
addData
}
from
"../data/addData"
;
import
{
delData
}
from
"../data/delData"
;
import
{
updateManyData
}
from
"../data/updateData"
;
import
{
TABLENAME
}
from
"../config/dbEnum"
;
/** 联动类型可选值 */
const
LINKAGE_TYPES
=
[
"制冷"
,
"制热"
,
"新风"
];
/** 格式化为本地时间字符串 YYYY-MM-DD HH:mm:ss */
function
formatLocalTime
(
d
:
Date
):
string
{
const
pad
=
(
n
:
number
)
=>
String
(
n
).
padStart
(
2
,
"0"
);
return
`
${
d
.
getFullYear
()}
-
${
pad
(
d
.
getMonth
()
+
1
)}
-
${
pad
(
d
.
getDate
())}
${
pad
(
d
.
getHours
())}
:
${
pad
(
d
.
getMinutes
())}
:
${
pad
(
d
.
getSeconds
())}
`
;
}
// ======================= 联动参数列表 =======================
/**
* 查询设备联动参数列表(分页)
* query: pageNumber, pageSize, deviceName, linkageType
*/
export
async
function
getLinkageList
(
params
:
{
pageNumber
?:
number
;
pageSize
?:
number
;
deviceId
?:
string
;
// 设备编号
deviceName
?:
string
;
// 设备名称(模糊匹配,通过 device 表关联)
linkageType
?:
string
;
// 联动类型:制冷/制热/新风
}):
Promise
<
any
>
{
const
pageNumber
=
params
.
pageNumber
||
1
;
const
pageSize
=
params
.
pageSize
||
10
;
const
queryParam
:
any
=
{};
// 联动类型筛选
if
(
params
.
linkageType
)
{
queryParam
.
linkage_type
=
params
.
linkageType
;
}
// 设备名称筛选:先通过 device 表查出匹配的 device_id 列表
if
(
params
.
deviceName
)
{
const
deviceResult
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_name
:
{
"%like%"
:
params
.
deviceName
}
},
[
"device_id"
]
);
const
deviceIds
=
(
deviceResult
.
data
as
any
[]).
map
((
d
:
any
)
=>
d
.
device_id
);
if
(
deviceIds
.
length
===
0
)
{
return
{
list
:
[],
pagination
:
{
page
:
pageNumber
,
pageSize
,
total
:
0
,
totalPages
:
0
}
};
}
queryParam
.
device_id
=
{
"%in%"
:
deviceIds
};
}
if
(
params
.
deviceId
)
{
queryParam
.
device_id
=
params
.
deviceId
;
}
// 分页查询,按创建时间倒序
const
result
=
await
selectPaginatedDataWithOrder
(
TABLENAME
.
设备联动调节表
,
queryParam
,
[
"id"
,
"device_id"
,
"linkage_type"
,
"threshold_max"
,
"threshold_min"
,
"threshold_mid"
,
"setting_max"
,
"setting_min"
,
"setting_mid"
,
"divide_method"
,
"begin_validity"
,
"end_validity"
,
"created_at"
,
"updated_at"
],
pageNumber
,
pageSize
,
[[
"id"
,
"DESC"
]],
);
// 补充设备名称映射
const
rows
=
result
.
data
as
any
[];
const
linkageDeviceIds
=
Array
.
from
(
new
Set
(
rows
.
map
((
item
:
any
)
=>
item
.
device_id
)));
let
deviceNameMap
:
Record
<
string
,
string
>
=
{};
if
(
linkageDeviceIds
.
length
>
0
)
{
const
devRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_id
:
{
"%in%"
:
linkageDeviceIds
}
},
[
"device_id"
,
"device_name"
]
);
for
(
const
d
of
(
devRes
.
data
as
any
[]))
{
deviceNameMap
[
d
.
device_id
]
=
d
.
device_name
||
''
;
}
}
// 映射展示字段
const
list
=
rows
.
map
((
item
:
any
)
=>
({
id
:
item
.
id
,
deviceId
:
item
.
device_id
,
deviceName
:
deviceNameMap
[
item
.
device_id
]
||
''
,
linkageType
:
item
.
linkage_type
||
''
,
thresholdMax
:
item
.
threshold_max
??
''
,
thresholdMin
:
item
.
threshold_min
??
''
,
thresholdMid
:
item
.
threshold_mid
??
''
,
settingMax
:
item
.
setting_max
??
''
,
settingMin
:
item
.
setting_min
??
''
,
settingMid
:
item
.
setting_mid
??
''
,
divideMethod
:
item
.
divide_method
||
''
,
beginValidity
:
item
.
begin_validity
?
(
typeof
item
.
begin_validity
===
"string"
?
item
.
begin_validity
:
formatLocalTime
(
new
Date
(
item
.
begin_validity
)))
:
""
,
endValidity
:
item
.
end_validity
?
(
typeof
item
.
end_validity
===
"string"
?
item
.
end_validity
:
formatLocalTime
(
new
Date
(
item
.
end_validity
)))
:
""
,
createdAt
:
item
.
created_at
?
(
typeof
item
.
created_at
===
"string"
?
item
.
created_at
:
formatLocalTime
(
new
Date
(
item
.
created_at
)))
:
""
,
updatedAt
:
item
.
updated_at
?
(
typeof
item
.
updated_at
===
"string"
?
item
.
updated_at
:
formatLocalTime
(
new
Date
(
item
.
updated_at
)))
:
""
,
}));
return
{
total
:
result
.
total
,
list
};
}
// ======================= 联动参数详情 =======================
/**
* 查询单条设备联动参数详情
* @param id 联动参数主键
*/
export
async
function
getLinkageDetail
(
id
:
number
):
Promise
<
any
>
{
if
(
!
id
)
{
return
{
isSuccess
:
false
,
msg
:
"缺少必填参数:id"
};
}
const
res
=
await
selectDataListByParam
(
TABLENAME
.
设备联动调节表
,
{
id
},
[
"id"
,
"device_id"
,
"linkage_type"
,
"threshold_max"
,
"threshold_min"
,
"threshold_mid"
,
"setting_max"
,
"setting_min"
,
"setting_mid"
,
"divide_method"
,
"begin_validity"
,
"end_validity"
,
"created_at"
,
"updated_at"
]
);
const
rows
:
any
[]
=
res
.
data
||
[];
if
(
rows
.
length
===
0
)
{
return
{
isSuccess
:
false
,
msg
:
`联动参数
${
id
}
不存在`
};
}
const
item
=
rows
[
0
];
// 补充设备名称
let
deviceName
=
''
;
const
devRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_id
:
item
.
device_id
},
[
"device_id"
,
"device_name"
]
);
if
((
devRes
.
data
as
any
[]).
length
>
0
)
{
deviceName
=
(
devRes
.
data
as
any
[])[
0
].
device_name
||
''
;
}
return
{
isSuccess
:
true
,
data
:
{
id
:
item
.
id
,
deviceId
:
item
.
device_id
,
deviceName
,
linkageType
:
item
.
linkage_type
||
''
,
thresholdMax
:
item
.
threshold_max
??
''
,
thresholdMin
:
item
.
threshold_min
??
''
,
thresholdMid
:
item
.
threshold_mid
??
''
,
settingMax
:
item
.
setting_max
??
''
,
settingMin
:
item
.
setting_min
??
''
,
settingMid
:
item
.
setting_mid
??
''
,
divideMethod
:
item
.
divide_method
||
''
,
beginValidity
:
item
.
begin_validity
?
(
typeof
item
.
begin_validity
===
"string"
?
item
.
begin_validity
:
formatLocalTime
(
new
Date
(
item
.
begin_validity
)))
:
""
,
endValidity
:
item
.
end_validity
?
(
typeof
item
.
end_validity
===
"string"
?
item
.
end_validity
:
formatLocalTime
(
new
Date
(
item
.
end_validity
)))
:
""
,
createdAt
:
item
.
created_at
?
(
typeof
item
.
created_at
===
"string"
?
item
.
created_at
:
formatLocalTime
(
new
Date
(
item
.
created_at
)))
:
""
,
updatedAt
:
item
.
updated_at
?
(
typeof
item
.
updated_at
===
"string"
?
item
.
updated_at
:
formatLocalTime
(
new
Date
(
item
.
updated_at
)))
:
""
,
},
};
}
// ======================= 新增联动参数 =======================
/**
* 新增设备联动参数
* body: deviceId, linkageType, thresholdMax, thresholdMin, thresholdMid,
* settingMax, settingMin, settingMid, divideMethod, beginValidity, endValidity
*/
export
async
function
addLinkage
(
params
:
{
deviceId
:
string
;
linkageType
:
string
;
thresholdMax
?:
string
;
thresholdMin
?:
string
;
thresholdMid
?:
string
;
settingMax
?:
string
;
settingMin
?:
string
;
settingMid
?:
string
;
divideMethod
?:
string
;
beginValidity
?:
string
;
endValidity
?:
string
;
}):
Promise
<
any
>
{
const
{
deviceId
,
linkageType
}
=
params
;
if
(
!
deviceId
)
{
return
{
isSuccess
:
false
,
msg
:
"缺少必填参数:deviceId"
};
}
if
(
!
linkageType
||
!
LINKAGE_TYPES
.
includes
(
linkageType
))
{
return
{
isSuccess
:
false
,
msg
:
"linkageType 只能为:制冷/制热/新风"
};
}
// 校验设备存在
const
devRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_id
:
deviceId
},
[
"device_id"
]);
if
(
!
(
devRes
.
data
as
any
[]).
length
)
{
return
{
isSuccess
:
false
,
msg
:
`设备
${
deviceId
}
不存在`
};
}
await
addData
(
TABLENAME
.
设备联动调节表
,
{
device_id
:
deviceId
,
linkage_type
:
linkageType
,
threshold_max
:
params
.
thresholdMax
??
null
,
threshold_min
:
params
.
thresholdMin
??
null
,
threshold_mid
:
params
.
thresholdMid
??
null
,
setting_max
:
params
.
settingMax
??
null
,
setting_min
:
params
.
settingMin
??
null
,
setting_mid
:
params
.
settingMid
??
null
,
divide_method
:
params
.
divideMethod
??
null
,
begin_validity
:
params
.
beginValidity
||
null
,
end_validity
:
params
.
endValidity
||
null
,
});
console
.
log
(
`[新增联动参数] device=
${
deviceId
}
, type=
${
linkageType
}
`
);
return
{
isSuccess
:
true
};
}
// ======================= 批量新增联动参数 =======================
/**
* 批量新增设备联动参数(为多台设备配置相同联动规则)
* body: deviceIds(数组), linkageType, thresholdMax, thresholdMin, thresholdMid,
* settingMax, settingMin, settingMid, divideMethod, beginValidity, endValidity
*/
export
async
function
batchAddLinkage
(
params
:
{
deviceIds
:
string
[];
linkageType
:
string
;
thresholdMax
?:
string
;
thresholdMin
?:
string
;
thresholdMid
?:
string
;
settingMax
?:
string
;
settingMin
?:
string
;
settingMid
?:
string
;
divideMethod
?:
string
;
beginValidity
?:
string
;
endValidity
?:
string
;
}):
Promise
<
any
>
{
const
{
deviceIds
,
linkageType
}
=
params
;
if
(
!
Array
.
isArray
(
deviceIds
)
||
deviceIds
.
length
===
0
)
{
return
{
isSuccess
:
false
,
msg
:
"缺少必填参数:deviceIds(数组)"
};
}
if
(
!
linkageType
||
!
LINKAGE_TYPES
.
includes
(
linkageType
))
{
return
{
isSuccess
:
false
,
msg
:
"linkageType 只能为:制冷/制热/新风"
};
}
// 去重
const
ids
=
Array
.
from
(
new
Set
(
deviceIds
));
// 校验设备均存在
const
devRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_id
:
{
"%in%"
:
ids
}
},
[
"device_id"
]
);
const
existSet
=
new
Set
((
devRes
.
data
as
any
[]).
map
((
d
:
any
)
=>
d
.
device_id
));
const
notExist
=
ids
.
filter
((
id
)
=>
!
existSet
.
has
(
id
));
if
(
notExist
.
length
>
0
)
{
return
{
isSuccess
:
false
,
msg
:
`以下设备不存在:
${
notExist
.
join
(
", "
)}
`
};
}
// 批量插入(addData 内部走 bulkCreate)
const
rows
=
ids
.
map
((
deviceId
)
=>
({
device_id
:
deviceId
,
linkage_type
:
linkageType
,
threshold_max
:
params
.
thresholdMax
??
null
,
threshold_min
:
params
.
thresholdMin
??
null
,
threshold_mid
:
params
.
thresholdMid
??
null
,
setting_max
:
params
.
settingMax
??
null
,
setting_min
:
params
.
settingMin
??
null
,
setting_mid
:
params
.
settingMid
??
null
,
divide_method
:
params
.
divideMethod
??
null
,
begin_validity
:
params
.
beginValidity
||
null
,
end_validity
:
params
.
endValidity
||
null
,
}));
await
addData
(
TABLENAME
.
设备联动调节表
,
rows
);
console
.
log
(
`[批量新增联动参数] count=
${
ids
.
length
}
, type=
${
linkageType
}
, ids=
${
ids
.
join
(
","
)}
`
);
return
{
isSuccess
:
true
,
count
:
ids
.
length
};
}
// ======================= 编辑联动参数 =======================
/**
* 编辑设备联动参数
* body: id + 待更新字段(同新增)
*/
export
async
function
editLinkage
(
params
:
{
id
:
number
;
deviceId
?:
string
;
linkageType
?:
string
;
thresholdMax
?:
string
;
thresholdMin
?:
string
;
thresholdMid
?:
string
;
settingMax
?:
string
;
settingMin
?:
string
;
settingMid
?:
string
;
divideMethod
?:
string
;
beginValidity
?:
string
;
endValidity
?:
string
;
}):
Promise
<
any
>
{
const
{
id
}
=
params
;
if
(
!
id
)
{
return
{
isSuccess
:
false
,
msg
:
"缺少必填参数:id"
};
}
const
dbEdits
:
any
=
{};
if
(
params
.
deviceId
!==
undefined
)
dbEdits
.
device_id
=
params
.
deviceId
;
if
(
params
.
linkageType
!==
undefined
)
{
if
(
!
LINKAGE_TYPES
.
includes
(
params
.
linkageType
))
{
return
{
isSuccess
:
false
,
msg
:
"linkageType 只能为:制冷/制热/新风"
};
}
dbEdits
.
linkage_type
=
params
.
linkageType
;
}
if
(
params
.
thresholdMax
!==
undefined
)
dbEdits
.
threshold_max
=
params
.
thresholdMax
;
if
(
params
.
thresholdMin
!==
undefined
)
dbEdits
.
threshold_min
=
params
.
thresholdMin
;
if
(
params
.
thresholdMid
!==
undefined
)
dbEdits
.
threshold_mid
=
params
.
thresholdMid
;
if
(
params
.
settingMax
!==
undefined
)
dbEdits
.
setting_max
=
params
.
settingMax
;
if
(
params
.
settingMin
!==
undefined
)
dbEdits
.
setting_min
=
params
.
settingMin
;
if
(
params
.
settingMid
!==
undefined
)
dbEdits
.
setting_mid
=
params
.
settingMid
;
if
(
params
.
divideMethod
!==
undefined
)
dbEdits
.
divide_method
=
params
.
divideMethod
;
if
(
params
.
beginValidity
!==
undefined
)
dbEdits
.
begin_validity
=
params
.
beginValidity
||
null
;
if
(
params
.
endValidity
!==
undefined
)
dbEdits
.
end_validity
=
params
.
endValidity
||
null
;
if
(
Object
.
keys
(
dbEdits
).
length
===
0
)
{
return
{
isSuccess
:
false
,
msg
:
"没有需要更新的字段"
};
}
await
updateManyData
(
TABLENAME
.
设备联动调节表
,
{
id
},
dbEdits
);
console
.
log
(
`[编辑联动参数] id=
${
id
}
, edits=
${
JSON
.
stringify
(
dbEdits
)}
`
);
return
{
isSuccess
:
true
};
}
// ======================= 删除联动参数 =======================
/**
* 删除设备联动参数
* body: id
*/
export
async
function
deleteLinkage
(
id
:
number
):
Promise
<
any
>
{
if
(
!
id
)
{
return
{
isSuccess
:
false
,
msg
:
"缺少必填参数:id"
};
}
await
delData
(
TABLENAME
.
设备联动调节表
,
{
id
});
console
.
log
(
`[删除联动参数] id=
${
id
}
`
);
return
{
isSuccess
:
true
};
}
src/biz/feiyiClient.ts
View file @
437999e3
...
...
@@ -4,10 +4,13 @@
*/
import
*
as
crypto
from
'crypto'
;
import
path
from
'path'
;
import
XLSX
from
'xlsx'
;
import
{
post
,
get
}
from
'../util/request'
;
import
{
BizError
}
from
'../util/bizError'
;
import
{
ERRORENUM
}
from
'../config/errorEnum'
;
import
{
addData
}
from
'../data/addData'
;
import
{
selectDataListByParam
}
from
'../data/findData'
;
import
{
TABLENAME
}
from
'../config/dbEnum'
;
// 全局变量缓存 token 和过期时间
...
...
@@ -137,6 +140,8 @@ const PATH_DESC_MAP: Record<string, string> = {
'/openApi/electricMeterState/list'
:
'查询抄表记录'
,
'/openApi/indoorUnit/event/switch'
:
'查询内机开关机记录'
,
'/openApi/indoorUnit/alarm/errorHis'
:
'查询内机故障记录'
,
'/openApi/indoorUnit/meter/expenses'
:
'查询单个内机分摊电量和电费'
,
'/openApi/indoorUnit/meter/expenses/list'
:
'查询房间电费日使用详情'
,
};
/**
...
...
@@ -302,18 +307,27 @@ export async function getIndoorUnitList(params: any): Promise<any> {
* @param deviceId
* @returns
*/
// 飞奕内机原始值 → 前端展示值映射
// workMode: 1制冷 2制热 3送风 4除湿;onOff: 0关 1开;fanSpeed: 1高风(超强) 2中风(强) 4低风(弱)
const
acModeNameMap
:
{
[
key
:
number
]:
string
}
=
{
1
:
"制冷"
,
2
:
"制热"
,
3
:
"送风"
,
4
:
"除湿"
};
const
acPowerNameMap
:
{
[
key
:
number
]:
string
}
=
{
0
:
"off"
,
1
:
"on"
};
const
acFanNameMap
:
{
[
key
:
number
]:
string
}
=
{
1
:
"超强"
,
2
:
"强"
,
4
:
"弱"
};
export
async
function
getIndoorUnitStatus
(
deviceId
:
string
):
Promise
<
any
>
{
const
path
=
'/openApi/indoorUnit/list'
;
const
body
=
{
indoorUnitAddressFull
:
deviceId
};
const
indoorUnits
=
await
feiYiPost
(
path
,
body
);
let
indoorUnit
=
indoorUnits
.
data
?.
rows
?.[
0
]
||
null
;
// result: {"mode": "制冷", "power": "on", "maxTemp": 0, "minTemp": 0, "setTemp": 23, "fanSpeed": "超强", "roomTemp": 0, "autoControl": "生效"}
let
result
:
any
=
{};
result
.
mode
=
indoorUnit
.
workMode
;
result
.
power
=
indoorUnit
.
onOff
;
result
.
fanSpeed
=
indoorUnit
.
fanSpeed
;
result
.
mode
=
acModeNameMap
[
indoorUnit
.
workMode
]
??
indoorUnit
.
workMode
;
result
.
power
=
acPowerNameMap
[
indoorUnit
.
onOff
]
??
indoorUnit
.
onOff
;
result
.
fanSpeed
=
acFanNameMap
[
indoorUnit
.
fanSpeed
]
??
indoorUnit
.
fanSpeed
;
result
.
setTemp
=
indoorUnit
.
tempSet
;
result
.
minTemp
=
indoorUnit
.
tempSetLo
;
result
.
maxTemp
=
indoorUnit
.
tempSetHi
;
return
result
;
}
...
...
@@ -446,6 +460,70 @@ export async function getMaterStateList(params: any): Promise<any> {
}
/**
* 单个内机分摊电量和电费
* 查询指定内机(或房间)在某一日的分摊用电量、电费、损耗等
* @param params { page, limit, roomId, indoorUnitId, indoorUnitAddressFull, occurrenceTime(yyyyMMdd) }
* @returns result.data { page, limit, total, rows: [{ indoorUnitId, roomId, floorId, buildingId, roomName, floorName, buildingName, tenantUserName, electricityPrice(电价), usePower(用电量), electricityUsageFee(电费), energyLoss(损耗电量), lossEnergyFee(损耗电费), deviceTime }] }
{
"code": "00000",
"message": "success",
"data": {
"page": 1,
"limit": 10,
"total": 9,
"rows": [{
"indoorUnitId": "97485639955775488", //内机ID
"roomId": "97474792772468736", //房间ID
"electricityPrice": 1.000, //电价
"usePower": 17457.00, //用电量
"electricityUsageFee": 17457.00, //电费
"energyLoss": 0.00, //损耗电量
"lossEnergyFee": 0.00, //损耗电费
"deviceTime": 1704423600000 //设备时间
}]
},
"timestamp": 1714465179333
}
*/
export
async
function
getIndoorUnitExpenses
(
params
:
any
):
Promise
<
any
>
{
const
path
=
'/openApi/indoorUnit/meter/expenses'
;
const
body
=
{
...
params
};
const
result
=
await
feiYiPost
(
path
,
body
);
return
result
.
data
;
}
/**
* 房间电费日使用详情
* 按房间/楼层/楼栋维度查询某日的用电量和电费
* @param params { page, limit, roomIds, floorIds, buildingIds, occurrenceTime(yyyyMMdd) }
* @returns result.data { page, limit, total, rows: [{ buildingId, floorId, roomId, usePower(房间用电量), electricityUsageFee(房间电费), deviceTime }] }
{
"code": "00000",
"message": "success",
"data": {
"page": 1,
"limit": 5,
"total": 9730,
"rows": [{
"buildingId": "97474832886792192", //楼栋ID
"floorId": "97474833251696640", //楼层ID
"roomId": "97474833612406784", //房间ID
"usePower": 114.761593, //房间用电量
"electricityUsageFee": 114.761593, //房间电费
"deviceTime": 1706544000000 //记录时间
}]
},
"timestamp": 1713255660803
}
*/
export
async
function
getRoomExpensesList
(
params
:
any
):
Promise
<
any
>
{
const
path
=
'/openApi/indoorUnit/meter/expenses/list'
;
const
body
=
{
...
params
};
const
result
=
await
feiYiPost
(
path
,
body
);
return
result
.
data
;
}
/**
* 内机开关机记录查询
* @param params
* @returns
...
...
@@ -528,6 +606,154 @@ export async function getFaultCodeInfo(brandCode: string, errorCode: string): Pr
}
/**
* 读取 res/电表对应区域.xlsx,构建 房间ID(空调内机所属房间) → 电表设备编号 的映射
* 说明:该文件是电表与区域的原始映射数据,比 device 表更全面(一台电表对应多个房间/内机)
* 列结构:电表设备编号 | 空调外机地址 | 空调内机地址 | 空调内机所属房间 | 区域名称
* 已验证:一个房间唯一对应一台电表(roomId → 电表设备编号 1:1)
*/
function
loadMeterRoomMap
():
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
roomId
=
row
[
'空调内机所属房间'
];
if
(
meterCode
&&
roomId
)
{
map
.
set
(
String
(
roomId
),
String
(
meterCode
));
}
}
console
.
log
(
`[工具] 电表对应区域.xlsx 读取完成,共
${
map
.
size
}
条 房间→电表 映射`
);
}
catch
(
err
)
{
console
.
error
(
'[工具] 读取电表对应区域.xlsx 失败:'
,
err
);
}
return
map
;
}
/**
* 将"房间电费日使用详情"写入设备扩展表 device_expansion
* 逻辑:
* 1. 调用 getRoomExpensesList 分页拉取指定日期的房间电费数据
* 2. 通过 region.room_id 关联本地区域,得到 region_key
* 3. 匹配电表设备:优先用 电表对应区域.xlsx(原始数据)的 房间→电表 映射;
* 若 xlsx 读取失败/为空,回退按区域查 device 表中 device_type 匹配的设备
* 4. 每条电费记录 → 匹配到的电表设备写一条 device_expansion
* original_data 形如 { power: 114.761593, electricityUsageFee: 114.761593, deviceTime: 1706544000000 }
* @param occurrenceTime 日期 yyyyMMdd(必填)
* @param deviceType 设备类型,默认"电能监测"
* @returns { total, success, skip } 处理统计
*/
export
async
function
saveRoomExpensesToExpansion
(
occurrenceTime
:
string
,
deviceType
:
string
=
'电能监测'
):
Promise
<
any
>
{
if
(
!
occurrenceTime
)
{
throw
new
BizError
(
ERRORENUM
.
参数错误
,
"日期 occurrenceTime 不能为空(格式 yyyyMMdd)"
);
}
// 1. 分页拉取房间电费日使用详情
const
pageSize
=
100
;
const
allRows
:
any
[]
=
[];
let
page
=
1
;
while
(
true
)
{
const
res
:
any
=
await
getRoomExpensesList
({
page
,
limit
:
pageSize
,
occurrenceTime
});
const
rows
=
res
?.
rows
||
[];
allRows
.
push
(...
rows
);
const
total
=
res
?.
total
||
allRows
.
length
;
if
(
allRows
.
length
>=
total
||
rows
.
length
===
0
)
break
;
page
++
;
}
console
.
log
(
`拉取房间电费记录
${
allRows
.
length
}
条(日期
${
occurrenceTime
}
)`
);
// 2. 通过 region.room_id 关联本地区域,一次查全部 region
const
roomIds
=
[...
new
Set
(
allRows
.
map
((
r
:
any
)
=>
r
.
roomId
))];
const
regions
=
roomIds
.
length
>
0
?
(
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{
room_id
:
{
"%in%"
:
roomIds
}
},
[
"id"
,
"room_id"
])).
data
||
[]
:
[];
const
roomToRegionKey
=
new
Map
<
string
,
number
>
();
regions
.
forEach
((
rg
:
any
)
=>
{
if
(
rg
.
room_id
)
roomToRegionKey
.
set
(
String
(
rg
.
room_id
),
rg
.
id
);
});
// 3. 电表设备匹配:优先用 电表对应区域.xlsx(原始数据,比 device 表更全面)
// 该文件是"电表 → 区域/房间"的原始映射,一个房间唯一对应一台电表
const
roomToMeterMap
=
loadMeterRoomMap
();
// 3.1 回退方案:xlsx 读取失败或为空时,按区域查 device 表中 device_type 匹配的设备
let
deviceRows
:
any
[]
=
[];
if
(
roomToMeterMap
.
size
>
0
)
{
deviceRows
=
[...
new
Set
(
roomToMeterMap
.
values
())].
map
((
id
)
=>
({
device_id
:
id
}));
}
else
{
const
fbRegionKeys
=
[...
roomToRegionKey
.
values
()];
deviceRows
=
fbRegionKeys
.
length
>
0
?
(
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
region_key
:
{
"%in%"
:
fbRegionKeys
},
device_type
:
deviceType
,
},
[
"device_id"
,
"region_key"
])).
data
||
[]
:
[];
}
// 4. 去重基准:查 device_expansion 中该设备类型已有记录(按 device_id + region_key + deviceTime 判重,避免定时任务重复插入)
const
matchedDeviceIds
=
[...
new
Set
(
deviceRows
.
map
((
d
:
any
)
=>
d
.
device_id
))];
const
regionKeys
=
[...
roomToRegionKey
.
values
()];
const
existingKeys
=
new
Set
<
string
>
();
if
(
matchedDeviceIds
.
length
>
0
)
{
const
where
:
any
=
{
device_id
:
{
"%in%"
:
matchedDeviceIds
},
device_type
:
deviceType
,
};
if
(
regionKeys
.
length
>
0
)
{
where
.
region_key
=
{
"%in%"
:
regionKeys
};
}
const
existingRows
=
(
await
selectDataListByParam
(
TABLENAME
.
设备扩展表
,
where
,
[
"device_id"
,
"region_key"
,
"original_data"
])).
data
||
[];
existingRows
.
forEach
((
r
:
any
)
=>
{
const
dt
=
r
.
original_data
&&
r
.
original_data
.
deviceTime
;
if
(
dt
!==
undefined
&&
dt
!==
null
)
{
existingKeys
.
add
(
`
${
r
.
device_id
}
|
${
r
.
region_key
}
|
${
dt
}
`
);
}
});
}
// 5. 组装 device_expansion 数据
const
expansionData
:
any
[]
=
[];
let
skip
=
0
;
allRows
.
forEach
((
row
:
any
)
=>
{
const
regionKey
=
roomToRegionKey
.
get
(
String
(
row
.
roomId
));
if
(
regionKey
===
undefined
)
{
skip
++
;
return
;
}
let
devices
:
any
[];
if
(
roomToMeterMap
.
size
>
0
)
{
// 优先:按 xlsx 映射精确匹配该房间对应的电表
const
meterId
=
roomToMeterMap
.
get
(
String
(
row
.
roomId
));
devices
=
meterId
?
[{
device_id
:
meterId
}]
:
[];
}
else
{
// 回退:该区域下所有匹配设备
devices
=
deviceRows
.
filter
((
d
:
any
)
=>
Number
(
d
.
region_key
)
===
Number
(
regionKey
));
}
if
(
devices
.
length
===
0
)
{
skip
++
;
return
;
}
devices
.
forEach
((
dev
:
any
)
=>
{
// 已存在相同设备 + 相同区域 + 相同时间点的记录则跳过
if
(
row
.
deviceTime
!==
undefined
&&
row
.
deviceTime
!==
null
&&
existingKeys
.
has
(
`
${
dev
.
device_id
}
|
${
regionKey
}
|
${
row
.
deviceTime
}
`
))
{
skip
++
;
return
;
}
expansionData
.
push
({
device_id
:
dev
.
device_id
,
region_key
:
regionKey
,
device_type
:
deviceType
,
original_data
:
row
,
cateted_at
:
new
Date
(),
});
});
});
if
(
expansionData
.
length
>
0
)
{
await
addData
(
TABLENAME
.
设备扩展表
,
expansionData
);
}
console
.
log
(
`写入设备扩展表
${
expansionData
.
length
}
条,跳过
${
skip
}
条(未匹配到电表或已存在)`
);
return
{
total
:
allRows
.
length
,
success
:
expansionData
.
length
,
skip
};
}
/**
* 故障类型映射(中文 -> 数字)
*/
let
faultTypeMap
:
{
[
key
:
string
]:
number
}
=
{
...
...
@@ -557,3 +783,161 @@ export function getFaultTypeName(alarmType: number | string): string {
const
num
=
typeof
alarmType
===
'string'
?
parseInt
(
alarmType
,
10
)
:
alarmType
;
return
getFaultTypeReverseMap
().
get
(
num
)
||
String
(
alarmType
);
}
/**
* 机房群控接口配置
* 文档:机房群控对外接口文档说明.docx
* 注意:该接口与上方飞奕云平台(m.achelp.cn)是两套独立接口/凭证:
* - 请求方式 GET,签名头 appkey/timestamp/signature
* - 签名规则 md5(ak + sk + timestamp),32 位小写
* sk(密钥)由接口提供商提供,请通过环境变量 FEIYI_ROOM_SECRET_KEY 注入;
* 未配置时暂用飞奕云平台 appSecret,若签名失败请与提供商确认 sk。
*/
const
ROOM_GROUP_CONFIG
=
{
// 默认生产环境 http://iot.achelp.cn;测试环境 http://iot-test.achelp.cn/feiyiiot/openapi/call
baseUrl
:
process
.
env
.
FEIYI_ROOM_BASE_URL
||
'http://iot.achelp.cn/feiyiiot/openapi/call'
,
appKey
:
process
.
env
.
FEIYI_ROOM_APP_KEY
||
'ak-vfpgVsdpIY882UYb'
,
// ak-TdFr2ftcRFhciGKi
secretKey
:
process
.
env
.
FEIYI_ROOM_SECRET_KEY
||
'6TUE271OlHeoxTQwjUYyAWSZIJnAsrlP'
,
};
/** 机房群控各接口标识 path */
const
ROOM_GROUP_PATH
=
{
deviceData
:
'tXCV5jlc'
,
// 获取设备最新参数数据(GET)
gatewayInfo
:
'vEkQTL8a'
,
// 获取网关信息(GET)
writeParams
:
'vHXQ9VL1'
,
// 参数写入(POST)
};
/**
* 机房群控接口签名:md5(ak + sk + timestamp),取 32 位小写
* @param timestamp 当前 UNIX 时间戳(毫秒)
*/
function
generateRoomGroupSign
(
timestamp
:
string
):
string
{
const
raw
=
ROOM_GROUP_CONFIG
.
appKey
+
ROOM_GROUP_CONFIG
.
secretKey
+
timestamp
;
return
crypto
.
createHash
(
'md5'
).
update
(
raw
).
digest
(
'hex'
);
// digest('hex') 默认小写
}
/** 生成机房群控接口签名请求头 */
function
buildRoomGroupHeaders
()
{
const
timestamp
=
Date
.
now
().
toString
();
return
{
appkey
:
ROOM_GROUP_CONFIG
.
appKey
,
timestamp
:
timestamp
,
signature
:
generateRoomGroupSign
(
timestamp
),
};
}
/**
* 机房群控通用请求:签名头 + 请求 + 数据接入日志 + code 校验
* @param path 接口标识(见 ROOM_GROUP_PATH)
* @param method 请求方式 GET / POST
* @param params 请求参数(GET 拼 query,POST 放 body)
* @param requestDesc 接口描述(写入数据接入日志)
* @returns 接口返回的 result 字段
*/
async
function
requestRoomGroup
(
path
:
string
,
method
:
'GET'
|
'POST'
,
params
:
any
,
requestDesc
:
string
):
Promise
<
any
>
{
const
url
=
`
${
ROOM_GROUP_CONFIG
.
baseUrl
}
/
${
path
}
`
;
const
headers
=
buildRoomGroupHeaders
();
const
requestTime
=
new
Date
();
// 记录日志(成功或失败共用)
const
logData
=
(
responseData
:
any
,
status
:
number
,
responseStatus
:
string
,
responseTime
:
Date
)
=>
{
addData
(
TABLENAME
.
数据接入日志表
,
{
partner_name
:
'飞奕机房群控'
,
request_url
:
url
,
request_desc
:
requestDesc
,
request_params
:
params
,
response_data
:
responseData
,
status
:
status
,
response_status
:
responseStatus
,
request_time
:
requestTime
,
response_time
:
responseTime
,
}).
catch
(
e
=>
console
.
error
(
'写入数据接入日志失败:'
,
e
));
};
let
result
:
any
;
try
{
if
(
method
===
'POST'
)
{
result
=
await
post
(
url
,
params
,
headers
);
}
else
{
result
=
await
get
(
url
,
params
,
headers
);
}
}
catch
(
err
)
{
const
responseTime
=
new
Date
();
let
responseStatus
=
'请求报错'
;
if
(
err
.
code
===
'ETIMEDOUT'
||
err
.
code
===
'ECONNABORTED'
||
err
.
message
?.
includes
(
'timeout'
))
{
responseStatus
=
'连接超时'
;
}
else
if
(
err
.
code
===
'ECONNREFUSED'
||
err
.
code
===
'ENOTFOUND'
)
{
responseStatus
=
'连接失败'
;
}
logData
(
null
,
0
,
responseStatus
,
responseTime
);
throw
new
BizError
(
ERRORENUM
.
网络错误
,
`
${
requestDesc
}
失败:
${
err
.
message
}
`
);
}
const
responseTime
=
new
Date
();
// 文档:success 是否成功,code 状态码(200 成功,500 异常)
if
(
!
result
||
result
.
code
!==
200
)
{
logData
(
result
,
0
,
'请求报错'
,
responseTime
);
throw
new
BizError
(
ERRORENUM
.
第三方接口错误
,
`
${
requestDesc
}
失败:
${
result
?.
message
||
'未知错误'
}
`);
}
logData(result, 1, '正常', responseTime);
return result.result ?? null;
}
/**
* 获取机房群控设备最新参数数据
* 根据网关编号和设备地址,获取指定设备下所有参数的最新数据
* @param gwSn 网关编号(可选)
* @param deviceAddr 设备地址
* @returns result 数组,如 [{ name: "仅风冷室内温度", value: "24.0" }, { name: "蓄冰出水温度设定点读取值", value: "25.0" }]
*/
export async function getRoomGroupDeviceData(gwSn: string, deviceAddr: string): Promise<any> {
if (!deviceAddr) {
throw new BizError(ERRORENUM.参数错误, "设备地址不能为空");
}
// gwSn 可选:有则携带,无则只按设备地址查询
const query: any = { deviceAddr };
if (gwSn) query.gwSn = gwSn;
return requestRoomGroup(ROOM_GROUP_PATH.deviceData, 'GET', query, '获取机房群控设备最新参数数据');
}
/**
* 获取机房群控网关信息
* 根据项目ID,获取该项目下的网关列表
* @param projectId 项目ID
* @returns result 数组,如 [{ onlineStatus: 1, id: 56951, gwSn: "8654f9b2...", projectId: "...", gwName: "青啤博物馆" }]
*/
export async function getRoomGroupGatewayInfo(projectId: string): Promise<any> {
if (!projectId) {
projectId = "410203244837601280"; // 默认值
}
return requestRoomGroup(ROOM_GROUP_PATH.gatewayInfo, 'GET', { projectId }, '获取机房群控网关信息');
}
/**
* 机房群控参数写入
* 向指定设备写入点位参数(key 为点位号,value 为写入值)
* @param addr 设备地址
* @param gwId 网关ID(可选)
* @param projectId 项目ID
* @param params 写入参数 JSON,如 { "1": 0.0 }
* @returns result(写入成功时一般为 null)
*/
export async function setRoomGroupParams(addr: string, gwId: number | string | undefined, projectId: string, params: object): Promise<any> {
if (!addr) {
throw new BizError(ERRORENUM.参数错误, "设备地址不能为空");
}
if (!projectId) {
throw new BizError(ERRORENUM.参数错误, "项目ID不能为空");
}
if (!params || typeof params !== 'object' || Array.isArray(params)) {
throw new BizError(ERRORENUM.参数错误, "写入参数 params 必须为 JSON 对象");
}
const body: any = { addr, projectId, params };
if (gwId !== undefined && gwId !== null && gwId !== '') body.gwId = gwId;
return requestRoomGroup(ROOM_GROUP_PATH.writeParams, 'POST', body, '机房群控参数写入');
}
\ No newline at end of file
src/biz/mqttClient.ts
View file @
437999e3
...
...
@@ -3,7 +3,6 @@ import { addData } from "../data/addData";
import
{
selectOneDataByParam
}
from
"../data/findData"
;
import
{
controlAcByEnvironment
}
from
"./running"
;
import
{
systemConfig
}
from
"../config/serverConfig"
;
import
{
isInOffPeriod
}
from
"./scheduleStatus"
;
/**
* 处理接收到的环境设备数据,并存入数据库
...
...
@@ -112,12 +111,6 @@ export async function processDeviceData(message) {
console
.
log
(
`数据入库成功: 设备
${
devEUI
}
`
);
writeLog
(
1
,
'正常'
);
// 动态判断设备是否处于关闭/休眠时段(基于日程开关机时间,默认 23:00-05:00)
const
regionKey
=
deviceRow
?.
data
?.
region_key
||
0
;
if
(
await
isInOffPeriod
(
devEUI
,
regionKey
))
{
console
.
log
(
`设备
${
devEUI
}
当前处于关闭/休眠时段,跳过空调自控`
);
return
;
}
// 根据regionKey查询区域表,若包含文创,则禁用空调控制
if
(
deviceRow
.
data
&&
deviceRow
.
data
.
region_key
)
{
const
regionRow
=
await
selectOneDataByParam
(
...
...
src/biz/region.ts
View file @
437999e3
...
...
@@ -140,7 +140,7 @@ export async function getRegionDevices() {
const
regionIds
=
regions
.
map
((
r
:
any
)
=>
r
.
id
);
const
deviceList
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
region_key
:
{
"%in%"
:
regionIds
},
device_type
:
"空调"
},
{
region_key
:
{
"%in%"
:
regionIds
},
device_type
:
{
"%in%"
:
[
"空调"
,
"新风"
]
}
},
[
"device_id"
,
"device_name"
,
"region_key"
,
"control_params"
],
);
const
devices
:
any
[]
=
deviceList
.
data
||
[];
...
...
src/biz/report.ts
View file @
437999e3
...
...
@@ -50,11 +50,16 @@ function formatDateStr(date: Date): string {
/** 计算最近七天的起止时间(今天往前推6天) */
function
calcWeekRange
(
weekOffset
:
number
=
0
):
{
start
:
Date
;
end
:
Date
;
timer
:
string
}
{
const
now
=
new
Date
();
// 临时调整为周三~周二周期:定位到最近一个周二
const
dayOfWeek
=
now
.
getDay
();
// 0=周日, 1=周一, ..., 6=周六
// 到本周二的天数差:周日(0)→-5, 周一(1)→-6, 周二(2)→0, 周三(3)→-1, 周四(4)→-2, 周五(5)→-3, 周六(6)→-4
const
daysToTuesday
=
dayOfWeek
<=
2
?
-
(
6
-
dayOfWeek
)
:
-
(
dayOfWeek
-
2
);
// 再往前推 weekOffset 周
const
end
=
new
Date
(
now
);
end
.
setDate
(
now
.
getDate
()
+
weekOffset
*
7
);
end
.
setDate
(
now
.
getDate
()
+
daysToTuesday
+
weekOffset
*
7
);
end
.
setHours
(
23
,
59
,
59
,
999
);
const
start
=
new
Date
(
now
);
start
.
setDate
(
now
.
getDate
()
-
6
+
weekOffset
*
7
);
const
start
=
new
Date
(
end
);
start
.
setDate
(
end
.
getDate
()
-
6
);
start
.
setHours
(
0
,
0
,
0
,
0
);
const
timer
=
`
${
formatChineseDate
(
start
)}
—
${
formatChineseDate
(
end
)}
`
;
...
...
@@ -586,8 +591,11 @@ function yesterdayStr(dateStr: string): string {
/**
* 查询指定日期的每小时最高温度
* 优化:使用范围查询替代 DATE() 函数,让索引生效
*/
async
function
getHourlyMaxTemp
(
dateStr
:
string
):
Promise
<
Record
<
number
,
number
>>
{
const
startDate
=
`
${
dateStr
}
00:00:00`
;
const
endDate
=
`
${
dateStr
}
23:59:59`
;
const
sql
=
`
SELECT
HOUR(dd.received_time) AS hour,
...
...
@@ -595,11 +603,12 @@ async function getHourlyMaxTemp(dateStr: string): Promise<Record<number, number>
FROM device_data dd
INNER JOIN device d ON dd.device_id = d.device_id
WHERE d.device_type = '环境监测'
AND DATE(dd.received_time) = :date
AND dd.received_time >= :startDate
AND dd.received_time <= :endDate
GROUP BY HOUR(dd.received_time)
ORDER BY HOUR(dd.received_time)
`
;
const
{
data
}
=
await
selectDataListBySql
(
sql
,
{
date
:
dateStr
});
const
{
data
}
=
await
selectDataListBySql
(
sql
,
{
startDate
,
endDate
});
const
map
:
Record
<
number
,
number
>
=
{};
for
(
const
row
of
(
data
as
any
[]))
{
map
[
row
.
hour
]
=
Number
(
row
.
max_temp
)
||
0
;
...
...
@@ -609,8 +618,11 @@ async function getHourlyMaxTemp(dateStr: string): Promise<Record<number, number>
/**
* 查询指定日期的每小时客流总量
* 优化:使用范围查询替代 DATE() 函数,让索引生效
*/
async
function
getHourlyVisitorFlow
(
dateStr
:
string
):
Promise
<
Record
<
number
,
number
>>
{
const
startDate
=
`
${
dateStr
}
00:00:00`
;
const
endDate
=
`
${
dateStr
}
23:59:59`
;
const
sql
=
`
SELECT
HOUR(dd.received_time) AS hour,
...
...
@@ -618,11 +630,12 @@ async function getHourlyVisitorFlow(dateStr: string): Promise<Record<number, num
FROM device_data dd
INNER JOIN device d ON dd.device_id = d.device_id
WHERE d.device_type = '客流传感器'
AND DATE(dd.received_time) = :date
AND dd.received_time >= :startDate
AND dd.received_time <= :endDate
GROUP BY HOUR(dd.received_time)
ORDER BY HOUR(dd.received_time)
`
;
const
{
data
}
=
await
selectDataListBySql
(
sql
,
{
date
:
dateStr
});
const
{
data
}
=
await
selectDataListBySql
(
sql
,
{
startDate
,
endDate
});
const
map
:
Record
<
number
,
number
>
=
{};
for
(
const
row
of
(
data
as
any
[]))
{
map
[
row
.
hour
]
=
Number
(
row
.
total_in
)
||
0
;
...
...
@@ -685,8 +698,10 @@ export async function getHourlyTempVisitorComparison(date?: string) {
/**
* 查询近 N 天的每日最高温度
* 优化:使用预计算日期范围替代 DATE_SUB + 范围查询让索引生效
*/
async
function
getDailyMaxTemp
(
days
:
number
):
Promise
<
Record
<
string
,
number
>>
{
const
startDate
=
formatDate
(
new
Date
(
Date
.
now
()
-
days
*
86400000
));
const
sql
=
`
SELECT
DATE(dd.received_time) AS date,
...
...
@@ -694,11 +709,11 @@ async function getDailyMaxTemp(days: number): Promise<Record<string, number>> {
FROM device_data dd
INNER JOIN device d ON dd.device_id = d.device_id
WHERE d.device_type = '环境监测'
AND dd.received_time >=
DATE_SUB(CURDATE(), INTERVAL :days DAY)
AND dd.received_time >=
:startDate
GROUP BY DATE(dd.received_time)
ORDER BY DATE(dd.received_time)
`
;
const
{
data
}
=
await
selectDataListBySql
(
sql
,
{
days
});
const
{
data
}
=
await
selectDataListBySql
(
sql
,
{
startDate
});
const
map
:
Record
<
string
,
number
>
=
{};
for
(
const
row
of
(
data
as
any
[]))
{
map
[
row
.
date
]
=
Number
(
row
.
max_temp
)
||
0
;
...
...
@@ -708,8 +723,10 @@ async function getDailyMaxTemp(days: number): Promise<Record<string, number>> {
/**
* 查询近 N 天的每日客流总量
* 优化:使用预计算日期范围替代 DATE_SUB + 范围查询让索引生效
*/
async
function
getDailyTotalVisitor
(
days
:
number
):
Promise
<
Record
<
string
,
number
>>
{
const
startDate
=
formatDate
(
new
Date
(
Date
.
now
()
-
days
*
86400000
));
const
sql
=
`
SELECT
DATE(dd.received_time) AS date,
...
...
@@ -717,11 +734,11 @@ async function getDailyTotalVisitor(days: number): Promise<Record<string, number
FROM device_data dd
INNER JOIN device d ON dd.device_id = d.device_id
WHERE d.device_type = '客流传感器'
AND dd.received_time >=
DATE_SUB(CURDATE(), INTERVAL :days DAY)
AND dd.received_time >=
:startDate
GROUP BY DATE(dd.received_time)
ORDER BY DATE(dd.received_time)
`
;
const
{
data
}
=
await
selectDataListBySql
(
sql
,
{
days
});
const
{
data
}
=
await
selectDataListBySql
(
sql
,
{
startDate
});
const
map
:
Record
<
string
,
number
>
=
{};
for
(
const
row
of
(
data
as
any
[]))
{
map
[
row
.
date
]
=
Number
(
row
.
total_in
)
||
0
;
...
...
src/biz/running.ts
View file @
437999e3
...
...
@@ -3,10 +3,12 @@ import { TABLENAME } from "../config/dbEnum";
import
{
ERRORENUM
}
from
"../config/errorEnum"
;
import
{
selectOneDataByParam
,
selectDataListByParam
}
from
"../data/findData"
;
import
{
addData
}
from
"../data/addData"
;
import
{
updateManyData
}
from
"../data/updateData"
;
import
{
BizError
}
from
"../util/bizError"
;
import
{
controlIndoorUnit
,
getIndoorUnitStatus
}
from
"./feiyiClient"
;
import
{
getRegionList
}
from
"./region"
;
import
{
startSchedule
,
stopSchedule
}
from
"../config/schedule"
;
import
{
isInOffPeriod
}
from
"./scheduleStatus"
;
/** device_state 值常量 */
const
DEVICE_STATE
=
{
NORMAL
:
0
,
HARDWARE_FAULT
:
1
,
COMM_FAULT
:
2
,
OFFLINE
:
3
}
as
const
;
...
...
@@ -21,7 +23,7 @@ function isDeviceOffline(deviceState: number): boolean {
* 联动去重缓存:记录每个区域上次下发的控制状态,避免重复调用第三方接口
* key: regionKey, value: 上次下发的控制参数
*/
const
envControlCache
:
Map
<
string
,
{
tempSet
?:
number
;
workMode
?:
number
;
fanSpeed
?:
number
;
onOff
?:
number
;
maxTemp
?:
number
;
minTemp
?:
number
;
co2Action
?:
string
}
>
=
new
Map
();
const
envControlCache
:
Map
<
string
,
{
tempSet
?:
number
;
workMode
?:
number
;
fanSpeed
?:
number
;
onOff
?:
number
;
maxTemp
?:
number
;
minTemp
?:
number
;
co2Action
?:
string
;
deviceCount
?:
number
}
>
=
new
Map
();
/**
* 将 Date 对象格式化为本地时间字符串 'YYYY-MM-DD HH:mm:ss'
...
...
@@ -924,7 +926,7 @@ export async function getAcRunningPop(regionKey: number, deviceId: string) {
let
devices
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_id
:
deviceId
},
// region_key: regionKey,
[
"device_id"
,
"device_type"
,
"device_state"
,
"device_name"
,
"control_params"
]
[
"device_id"
,
"device_type"
,
"device_state"
,
"device_name"
,
"control_params"
,
"linkage_start"
]
);
if
(
!
devices
.
data
.
length
)
{
throw
new
BizError
(
ERRORENUM
.
未找到数据
,
`设备
${
deviceId
}
在区域
${
regionKey
}
中未找到`
);
...
...
@@ -936,6 +938,16 @@ export async function getAcRunningPop(regionKey: number, deviceId: string) {
// 通过飞奕内机列表获取实时设备数据
let
detailData
=
await
getIndoorUnitStatus
(
deviceId
);
detailData
.
fanSpeed
=
detailData
&&
detailData
.
fanSpeed
?
acDeviceKeyMap
[
detailData
.
fanSpeed
]
:
2
;
// 查询当前生效的联动配置:命中则用联动表上下限覆盖飞奕默认上下限,未命中保持飞奕默认
const
linkage
=
await
findEffectiveLinkage
(
deviceId
,
deviceType
);
if
(
linkage
)
{
if
(
linkage
.
threshold_max
!==
null
&&
linkage
.
threshold_max
!==
undefined
&&
linkage
.
threshold_max
!==
''
)
{
detailData
.
maxTemp
=
Number
(
linkage
.
threshold_max
);
}
if
(
linkage
.
threshold_min
!==
null
&&
linkage
.
threshold_min
!==
undefined
&&
linkage
.
threshold_min
!==
''
)
{
detailData
.
minTemp
=
Number
(
linkage
.
threshold_min
);
}
}
detailData
.
autoControl
=
devices
&&
devices
.
data
[
0
].
linkage_start
&&
devices
.
data
[
0
].
linkage_start
===
"on"
?
'生效'
:
'解除'
;
return
{
deviceFault
:
{
...
...
@@ -970,20 +982,52 @@ export async function controlAcRunning(params: {}) {
let
mode
=
params
[
"mode"
]
||
"制冷"
;
let
power
=
params
[
"power"
]
||
'on'
;
let
setTemp
=
params
[
"setTemp"
];
let
maxTemp
=
params
[
"maxTemp"
]
||
28
;
// 默认锁定
温度上限
let
minTemp
=
params
[
"minTemp"
]
||
16
;
// 默认锁定
温度下限
let
autoControl
=
params
[
"autoControl"
];
let
fanSpeed
=
params
[
"fanSpeed"
];
let
maxTemp
=
params
[
"maxTemp"
]
;
// 联动
温度上限
let
minTemp
=
params
[
"minTemp"
]
;
// 联动
温度下限
let
autoControl
=
params
[
"autoControl"
];
// 联动是否生效
let
fanSpeed
=
params
[
"fanSpeed"
]
&&
params
[
"fanSpeed"
]
!==
"Error"
?
Number
(
params
[
"fanSpeed"
])
:
1
;
// 1. 通过设备信息,获取内机地址
let
device
=
await
selectOneDataByParam
(
TABLENAME
.
设备表
,
{
device_id
:
deviceId
},
[
"id"
,
"device_id"
,
"region_key"
,
"device_name"
,
"device_ad"
,
"control_params"
]
[
"id"
,
"device_id"
,
"region_key"
,
"device_name"
,
"device_ad"
,
"control_params"
,
"device_type"
,
"linkage_start"
]
);
if
(
!
device
.
data
||
!
device
.
data
.
id
)
{
throw
new
BizError
(
ERRORENUM
.
未找到数据
,
`设备
${
deviceId
}
未注册`
);
}
const
deviceType
=
device
.
data
.
device_type
||
''
;
// 1.1 联动开关状态写入 device.linkage_start(autoControl:生效→on,解除→off)
if
(
autoControl
!==
undefined
&&
autoControl
!==
null
&&
autoControl
!==
''
)
{
const
linkageStart
=
autoControl
===
'生效'
?
'on'
:
'off'
;
await
updateManyData
(
TABLENAME
.
设备表
,
{
device_id
:
deviceId
},
{
linkage_start
:
linkageStart
});
}
// 1.2 联动温度上下限写入 device_linkage:与 acPop 同样方式查找,找到则更新,找不到则新增
const
hasMax
=
maxTemp
!==
undefined
&&
maxTemp
!==
null
&&
maxTemp
!==
''
;
const
hasMin
=
minTemp
!==
undefined
&&
minTemp
!==
null
&&
minTemp
!==
''
;
if
(
hasMax
||
hasMin
)
{
const
linkage
=
await
findEffectiveLinkage
(
deviceId
,
deviceType
);
if
(
linkage
)
{
const
edits
:
any
=
{};
if
(
hasMax
)
edits
.
threshold_max
=
String
(
maxTemp
);
if
(
hasMin
)
edits
.
threshold_min
=
String
(
minTemp
);
await
updateManyData
(
TABLENAME
.
设备联动调节表
,
{
id
:
linkage
.
id
},
edits
);
}
else
{
// 无生效配置:新增一条(有效期留空=长期生效)
const
season
=
getSeason
();
let
linkageType
=
''
;
if
(
deviceType
.
includes
(
'新风'
))
linkageType
=
'新风'
;
else
if
(
deviceType
.
includes
(
'空调'
))
linkageType
=
season
===
'summer'
?
'制冷'
:
'制热'
;
await
addData
(
TABLENAME
.
设备联动调节表
,
{
device_id
:
deviceId
,
linkage_type
:
linkageType
||
'制冷'
,
threshold_max
:
hasMax
?
String
(
maxTemp
)
:
null
,
threshold_min
:
hasMin
?
String
(
minTemp
)
:
null
,
});
}
}
// 2. 接入空调控制API,更新空调设备信息
let
acPrarms
=
{
"indoorUnitAddressFull"
:
[
deviceId
],
...
...
@@ -991,13 +1035,13 @@ export async function controlAcRunning(params: {}) {
"onOff"
:
acDeviceKeyMap
[
power
],
"tempSet"
:
Number
(
setTemp
),
"fanSpeed"
:
acDeviceKeyMap
[
acFanSpeedKeyMap
[
fanSpeed
]],
"onOffLock"
:
acDeviceKeyMap
[
autoControl
],
"workModeLock"
:
acDeviceKeyMap
[
autoControl
],
"tempSetLock"
:
acDeviceKeyMap
[
autoControl
],
"tempSetLo"
:
Number
(
minTemp
),
"tempSetHi"
:
Number
(
maxTemp
),
//
"onOffLock": acDeviceKeyMap[autoControl],
//
"workModeLock": acDeviceKeyMap[autoControl],
//
"tempSetLock": acDeviceKeyMap[autoControl],
//
"tempSetLo": Number(minTemp),
//
"tempSetHi": Number(maxTemp),
// "openApiAction": autoControl === "生效" ? "control" : "lock"
"openApiAction"
:
"control"
// TODO:临时处理
"openApiAction"
:
"control"
};
const
result
=
await
controlIndoorUnit
(
acPrarms
);
// 手动控制日志
...
...
@@ -1015,9 +1059,6 @@ export async function controlAcRunning(params: {}) {
power
:
power
,
setTemp
:
setTemp
,
fanSpeed
:
fanSpeed
,
autoControl
:
autoControl
,
maxTemp
:
maxTemp
,
minTemp
:
minTemp
,
success
:
result
.
success
,
message
:
result
.
message
||
(
result
.
success
?
'控制成功'
:
'控制失败'
),
},
...
...
@@ -1092,6 +1133,46 @@ function getSeason(): 'summer' | 'winter' {
return
(
month
>=
5
&&
month
<=
9
)
?
'summer'
:
'winter'
;
}
/**
* 查找设备当前生效的联动配置(device_linkage 表)
* 规则:
* 1. 先过滤有效期:begin_validity 为空或 <= 当前时间,且 end_validity 为空或 >= 当前时间
* 2. 有效期内仅 1 条则直接返回
* 3. 多条时根据设备类型 + 当前季节匹配联动类型(空调:夏→制冷/冬→制热;新风→新风)
* 4. 无匹配返回 null
* @param deviceId 设备编号
* @param deviceType 设备类型(空调/新风等)
*/
async
function
findEffectiveLinkage
(
deviceId
:
string
,
deviceType
:
string
):
Promise
<
any
|
null
>
{
const
res
=
await
selectDataListByParam
(
TABLENAME
.
设备联动调节表
,
{
device_id
:
deviceId
},
[
"id"
,
"device_id"
,
"linkage_type"
,
"threshold_max"
,
"threshold_min"
,
"begin_validity"
,
"end_validity"
]
);
const
rows
:
any
[]
=
res
.
data
||
[];
if
(
rows
.
length
===
0
)
return
null
;
const
now
=
new
Date
();
// 过滤有效期内的配置
const
valid
=
rows
.
filter
((
r
:
any
)
=>
{
const
begin
=
r
.
begin_validity
?
new
Date
(
r
.
begin_validity
)
:
null
;
const
end
=
r
.
end_validity
?
new
Date
(
r
.
end_validity
)
:
null
;
if
(
begin
&&
begin
>
now
)
return
false
;
if
(
end
&&
end
<
now
)
return
false
;
return
true
;
});
if
(
valid
.
length
===
0
)
return
null
;
if
(
valid
.
length
===
1
)
return
valid
[
0
];
// 多条:根据设备类型 + 当前季节匹配联动类型
const
season
=
getSeason
();
let
targetType
:
string
|
null
=
null
;
if
(
deviceType
.
includes
(
'新风'
))
targetType
=
'新风'
;
else
if
(
deviceType
.
includes
(
'空调'
))
targetType
=
season
===
'summer'
?
'制冷'
:
'制热'
;
const
matched
=
valid
.
find
((
r
:
any
)
=>
targetType
&&
r
.
linkage_type
===
targetType
);
return
matched
||
null
;
}
// 根据IAQ温度计算空调设定温度
/**
* 根据文档 Slide 39 三段式温度段,计算联动控制参数
...
...
@@ -1148,22 +1229,28 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
linkage_start
:
'on'
},
[
"id"
,
"device_name"
,
"device_id"
,
"device_ad"
,
"device_type"
]);
console
.
log
(
`若是新风设备,判断联动是否开启
${
fanDevices
.
data
.
length
}
`
);
// 逐台按设备自身日程过滤:处于关闭/休眠时段的新风/空调不参与联动
if
(
fanDevices
.
data
.
length
>
0
)
{
const
fanOffFlags
=
await
Promise
.
all
(
fanDevices
.
data
.
map
((
fd
:
any
)
=>
isInOffPeriod
(
fd
.
device_id
,
regionKey
)));
fanDevices
=
{
data
:
fanDevices
.
data
.
filter
((
_
:
any
,
i
:
number
)
=>
!
fanOffFlags
[
i
])
};
console
.
log
(
`新风联动设备按日程过滤后
${
fanDevices
.
data
.
length
}
台参与控制`
);
}
if
(
fanDevices
.
data
.
length
>
0
)
{
// CO2联动:高→送风模式(3)+高风(1),低→不传workMode(不切换模式)+低风(4)
// const fanWorkMode: number | undefined = fanParams.action === 'high' ? 3 : undefined;
const
fanWorkMode
=
1
;
const
fanSpeedVal
=
fanParams
.
action
===
'high'
?
1
:
4
;
// 去重
// 去重
(含参与控制的设备数,设备因日程被过滤导致数量变化时不跳过)
const
fanCacheKey
=
`fan_
${
regionKey
}
`
;
const
lastFanState
=
envControlCache
.
get
(
fanCacheKey
);
if
(
lastFanState
&&
lastFanState
.
co2Action
===
fanParams
.
action
&&
lastFanState
.
workMode
===
fanWorkMode
&&
lastFanState
.
fanSpeed
===
fanSpeedVal
)
{
if
(
lastFanState
&&
lastFanState
.
co2Action
===
fanParams
.
action
&&
lastFanState
.
workMode
===
fanWorkMode
&&
lastFanState
.
fanSpeed
===
fanSpeedVal
&&
lastFanState
.
deviceCount
===
fanDevices
.
data
.
length
)
{
console
.
log
(
`新风联动跳过(参数未变): CO2
${
co2
}
ppm,
${
fanParams
.
action
===
'high'
?
'送风高风'
:
'低风待机'
}
, 区域
${
regionKey
}
`
);
}
else
{
let
fanControlRes
=
await
controlAcRunningApi
(
fanDevices
,
fanWorkMode
,
undefined
,
fanSpeedVal
,
1
);
// 更新缓存(含 workMode/fanSpeed 用于去重)
envControlCache
.
set
(
fanCacheKey
,
{
co2Action
:
fanParams
.
action
,
workMode
:
fanWorkMode
,
fanSpeed
:
fanSpeedVal
});
// 更新缓存(含 workMode/fanSpeed
/deviceCount
用于去重)
envControlCache
.
set
(
fanCacheKey
,
{
co2Action
:
fanParams
.
action
,
workMode
:
fanWorkMode
,
fanSpeed
:
fanSpeedVal
,
deviceCount
:
fanDevices
.
data
.
length
});
let
temp
=
calcAcTempSet
(
temperature
,
season
);
...
...
@@ -1205,6 +1292,12 @@ console.log(`若是空调设备,判断温度是否为空 ${temperature}`);
linkage_start
:
'on'
},
[
"id"
,
"device_name"
,
"device_id"
,
"device_ad"
,
"control_params"
]);
console
.
log
(
`若是空调设备,判断联动是否开启
${
acDevices
.
data
.
length
}
`
);
// 逐台按设备自身日程过滤:处于关闭/休眠时段的空调不参与联动
if
(
acDevices
.
data
.
length
>
0
)
{
const
acOffFlags
=
await
Promise
.
all
(
acDevices
.
data
.
map
((
ad
:
any
)
=>
isInOffPeriod
(
ad
.
device_id
,
regionKey
)));
acDevices
=
{
data
:
acDevices
.
data
.
filter
((
_
:
any
,
i
:
number
)
=>
!
acOffFlags
[
i
])
};
console
.
log
(
`空调联动设备按日程过滤后
${
acDevices
.
data
.
length
}
台参与控制`
);
}
if
(
acDevices
.
data
.
length
>
0
)
{
const
{
tempSet
,
fanSpeed
,
onOff
}
=
calcAcTempSet
(
temperature
,
season
);
const
workMode
=
season
===
'summer'
?
1
:
2
;
// 1制冷 2制热
...
...
src/biz/schedule.ts
View file @
437999e3
...
...
@@ -191,14 +191,14 @@ export async function addSchedule(params: {
if
(
deviceIds
&&
deviceIds
.
length
)
{
// 有具体设备时,按 deviceIds 查,取出它们的 device_id + region_key
const
deviceRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_type
:
"空调"
,
device_type
:
{
"%in%"
:
[
"空调"
,
"新风"
]
}
,
device_id
:
{
"%in%"
:
deviceIds
},
},
[
"device_id"
,
"region_key"
]);
devices
=
deviceRes
.
data
||
[];
}
else
if
(
regionKeys
&&
regionKeys
.
length
)
{
// 仅传区域时,查该区域下所有空调
const
deviceRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_type
:
"空调"
,
device_type
:
{
"%in%"
:
[
"空调"
,
"新风"
]
}
,
region_key
:
{
"%in%"
:
regionKeys
},
},
[
"device_id"
,
"region_key"
]);
devices
=
deviceRes
.
data
||
[];
...
...
@@ -308,13 +308,13 @@ export async function editSchedule(params: {
let
devices
:
any
[]
=
[];
if
(
deviceIds
&&
deviceIds
.
length
)
{
const
deviceRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_type
:
"空调"
,
device_type
:
{
"%in%"
:
[
"空调"
,
"新风"
]
}
,
device_id
:
{
"%in%"
:
deviceIds
},
},
[
"device_id"
,
"region_key"
]);
devices
=
deviceRes
.
data
||
[];
}
else
if
(
regionKeys
&&
regionKeys
.
length
)
{
const
deviceRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_type
:
"空调"
,
device_type
:
{
"%in%"
:
[
"空调"
,
"新风"
]
}
,
region_key
:
{
"%in%"
:
regionKeys
},
},
[
"device_id"
,
"region_key"
]);
devices
=
deviceRes
.
data
||
[];
...
...
src/config/dbEnum.ts
View file @
437999e3
...
...
@@ -11,7 +11,9 @@ export enum TABLENAME {
设备日志表
=
'device_logs'
,
日程表
=
'schedule'
,
日程设备关联表
=
'schedule_device'
,
数据接入日志表
=
'data_access_log'
数据接入日志表
=
'data_access_log'
,
设备联动调节表
=
'device_linkage'
,
设备扩展表
=
'device_expansion'
};
/**
...
...
src/config/mysqlTableConfig.ts
View file @
437999e3
...
...
@@ -43,6 +43,11 @@ export const TablesConfig = [
}
},
association
:
[
],
indexes
:
[
{
fields
:
[
'device_id'
]
},
{
fields
:
[
'received_time'
]
},
{
fields
:
[
'device_id'
,
'received_time'
]
},
]
},
// 设备故障表
...
...
@@ -192,7 +197,13 @@ export const TablesConfig = [
},
association
:
[
{
type
:
"hasMany"
,
target
:
"device_data"
,
foreignKey
:
"device_id"
},
{
type
:
"hasMany"
,
target
:
"device_fault"
,
foreignKey
:
"device_id"
}
{
type
:
"hasMany"
,
target
:
"device_fault"
,
foreignKey
:
"device_id"
},
{
type
:
"hasMany"
,
target
:
"device_linkage"
,
foreignKey
:
"device_id"
},
{
type
:
"hasMany"
,
target
:
"device_expansion"
,
foreignKey
:
"device_id"
}
],
indexes
:
[
{
fields
:
[
'device_type'
]
},
{
fields
:
[
'device_type'
,
'device_id'
]
},
]
},
// 人员信息表
...
...
@@ -538,5 +549,148 @@ export const TablesConfig = [
{
type
:
"hasMany"
,
target
:
"region"
,
foreignKey
:
"region_key"
}
]
},
// 设备联动调节表
{
tableNameCn
:
'设备联动调节表'
,
tableName
:
'device_linkage'
,
schema
:
{
id
:
{
type
:
DataTypes
.
BIGINT
,
allowNull
:
false
,
primaryKey
:
true
,
autoIncrement
:
true
,
comment
:
'自增主键'
},
device_id
:
{
type
:
DataTypes
.
STRING
(
50
),
allowNull
:
false
,
comment
:
'设备编号,关联device.device_id'
},
linkage_type
:
{
type
:
DataTypes
.
STRING
(
20
),
allowNull
:
false
,
comment
:
'联动类型:制冷/制热/新风'
},
threshold_max
:
{
type
:
DataTypes
.
STRING
(
50
),
allowNull
:
true
,
comment
:
'阈值上限,如"26"'
},
threshold_min
:
{
type
:
DataTypes
.
STRING
(
50
),
allowNull
:
true
,
comment
:
'阈值下限,如"22"'
},
threshold_mid
:
{
type
:
DataTypes
.
STRING
(
50
),
allowNull
:
true
,
comment
:
'中间阈值,如"24"'
},
setting_max
:
{
type
:
DataTypes
.
STRING
(
50
),
allowNull
:
true
,
comment
:
'上限设置:高于阈值上限时的设定值'
},
setting_min
:
{
type
:
DataTypes
.
STRING
(
50
),
allowNull
:
true
,
comment
:
'下限设置:低于阈值下限时的设定值'
},
setting_mid
:
{
type
:
DataTypes
.
STRING
(
50
),
allowNull
:
true
,
comment
:
'中间设置:处于阈值区间内的设定值'
},
divide_method
:
{
type
:
DataTypes
.
STRING
(
50
),
allowNull
:
true
,
comment
:
'划分方式,如"按温度三段式"'
},
begin_validity
:
{
type
:
DataTypes
.
DATE
,
allowNull
:
true
,
comment
:
'有效期起,不设置则为空'
},
end_validity
:
{
type
:
DataTypes
.
DATE
,
allowNull
:
true
,
comment
:
'有效期止,不设置则为空'
},
created_at
:
{
type
:
DataTypes
.
DATE
,
allowNull
:
false
,
defaultValue
:
DataTypes
.
NOW
,
comment
:
'创建时间'
},
updated_at
:
{
type
:
DataTypes
.
DATE
,
allowNull
:
false
,
defaultValue
:
DataTypes
.
NOW
,
comment
:
'修改时间'
}
},
association
:
[],
indexes
:
[
{
fields
:
[
'device_id'
]
},
{
fields
:
[
'linkage_type'
]
},
{
fields
:
[
'device_id'
,
'linkage_type'
]
},
]
},
// 设备扩展表
{
tableNameCn
:
'设备扩展表'
,
tableName
:
'device_expansion'
,
schema
:
{
id
:
{
type
:
DataTypes
.
BIGINT
,
allowNull
:
false
,
primaryKey
:
true
,
autoIncrement
:
true
,
comment
:
'自增主键'
},
device_id
:
{
type
:
DataTypes
.
STRING
(
50
),
allowNull
:
false
,
comment
:
'设备ID,关联device.device_id'
},
region_key
:
{
type
:
DataTypes
.
BIGINT
,
allowNull
:
false
,
comment
:
'区域编号,关联region.id,表示设备所属区域'
},
device_type
:
{
type
:
DataTypes
.
STRING
(
50
),
allowNull
:
false
,
comment
:
'设备类型,如"电能监测、电表"'
},
original_data
:
{
type
:
DataTypes
.
JSON
,
allowNull
:
true
,
comment
:
'原始扩展数据,如{usePower:114.76,electricityUsageFee:114.76}'
},
cateted_at
:
{
type
:
DataTypes
.
DATE
,
allowNull
:
false
,
defaultValue
:
DataTypes
.
NOW
,
comment
:
'创建时间'
},
updated_at
:
{
type
:
DataTypes
.
DATE
,
allowNull
:
false
,
defaultValue
:
DataTypes
.
NOW
,
comment
:
'修改时间'
}
},
association
:
[
{
type
:
"hasMany"
,
target
:
"device"
,
foreignKey
:
"device_id"
},
{
type
:
"hasMany"
,
target
:
"region"
,
foreignKey
:
"region_key"
}
],
indexes
:
[
{
fields
:
[
'device_id'
]
},
{
fields
:
[
'region_key'
]
},
{
fields
:
[
'device_type'
]
},
{
fields
:
[
'device_id'
,
'device_type'
]
},
]
},
];
src/data/findData.ts
View file @
437999e3
...
...
@@ -434,6 +434,7 @@ export async function selectPaginatedDataWithOrder( tableName:string, param:Quer
return
{
data
:
result
.
rows
,
total
:
result
.
count
,
pagination
:
{
page
:
pageNumber
,
pageSize
,
...
...
src/main.ts
View file @
437999e3
...
...
@@ -19,7 +19,7 @@ async function lanuch() {
/**创建http服务 */
httpServer
.
createServer
(
systemConfig
.
port
);
/**启动MQTT订阅服务 */
await
mqttSrv
.
startMqttClient
();
//
await mqttSrv.startMqttClient();
/**启动定时任务 */
startSchedule
();
/**启动预警工单定时任务(10分钟) */
...
...
src/middleware/watch.ts
View file @
437999e3
...
...
@@ -23,3 +23,11 @@ function success(data) {
this
.
res
.
send
(
resultPack
);
}
/**
* 中间件错误返回方法
* @param msg
*/
function
error
(
msg
)
{
this
.
res
.
send
({
success
:
false
,
msg
,
code
:
400
});
}
src/routers/admin.ts
View file @
437999e3
...
...
@@ -8,9 +8,36 @@ import * as regionBiz from "../biz/region";
import
*
as
scheduleBiz
from
"../biz/schedule"
;
import
*
as
deviceBiz
from
"../biz/device"
;
import
*
as
alertWorkOrderBiz
from
"../biz/deviceFault"
;
import
*
as
deviceLinkageBiz
from
"../biz/deviceLinkage"
;
import
{
reloadScheduleTasks
}
from
"../biz/scheduleExecutor"
;
import
{
eccReqParamater
}
from
"../util/verificationParam"
;
/** 校验分页参数,不通过则直接返回错误 */
function
validatePagination
(
pageNumber
:
number
,
pageSize
:
number
,
res
:
any
):
boolean
{
if
(
!
Number
.
isInteger
(
pageNumber
)
||
pageNumber
<
1
)
{
res
.
success
({
success
:
false
,
msg
:
"pageNumber 必须为大于等于 1 的整数"
,
code
:
400
});
return
false
;
}
if
(
!
Number
.
isInteger
(
pageSize
)
||
pageSize
<
1
)
{
res
.
success
({
success
:
false
,
msg
:
"pageSize 必须为大于等于 1 的整数"
,
code
:
400
});
return
false
;
}
return
true
;
}
/** 校验日期格式 yyyy-MM-dd,不通过则直接返回错误 */
function
validateDateParam
(
dateStr
:
string
|
undefined
,
res
:
any
):
boolean
{
if
(
!
dateStr
||
dateStr
.
trim
()
===
''
)
{
res
.
success
({
success
:
false
,
msg
:
"date 参数不能为空"
,
code
:
400
});
return
false
;
}
if
(
!
/^
\d{4}
-
\d{2}
-
\d{2}
$/
.
test
(
dateStr
.
trim
()))
{
res
.
success
({
success
:
false
,
msg
:
"date 格式错误,应为 yyyy-MM-dd"
,
code
:
400
});
return
false
;
}
return
true
;
}
export
function
setRouter
(
httpServer
)
{
/** 获取周报告JSON数据 */
httpServer
.
get
(
"/api/qdm/report/weekly/data"
,
asyncHandler
(
getReportData
));
...
...
@@ -51,6 +78,19 @@ export function setRouter(httpServer) {
httpServer
.
get
(
"/api/qdm/admin/fault/list"
,
asyncHandler
(
getDeviceFaultList
));
/** 日志管理 - 分页列表 */
httpServer
.
get
(
"/api/qdm/admin/device/logs"
,
asyncHandler
(
getDeviceLogList
));
/** 设备联动参数 - 分页列表 */
httpServer
.
get
(
"/api/qdm/admin/linkage/list"
,
asyncHandler
(
getLinkageList
));
/** 设备联动参数 - 详情 */
httpServer
.
get
(
"/api/qdm/admin/linkage/detail"
,
asyncHandler
(
getLinkageDetail
));
/** 设备联动参数 - 新增 */
httpServer
.
post
(
"/api/qdm/admin/linkage/add"
,
asyncHandler
(
addLinkage
));
/** 设备联动参数 - 批量新增 */
httpServer
.
post
(
"/api/qdm/admin/linkage/add_batch"
,
asyncHandler
(
batchAddLinkage
));
/** 设备联动参数 - 编辑 */
httpServer
.
post
(
"/api/qdm/admin/linkage/edit"
,
asyncHandler
(
editLinkage
));
/** 设备联动参数 - 删除 */
httpServer
.
post
(
"/api/qdm/admin/linkage/delete"
,
asyncHandler
(
deleteLinkage
));
}
/**
...
...
@@ -100,9 +140,13 @@ async function getRegionDevices(req, res) {
* query: pageNumber, pageSize, startDate, endDate, regionKey
*/
async
function
getScheduleList
(
req
,
res
)
{
const
pageNumber
=
Number
(
req
.
query
?.
pageNumber
)
||
1
;
const
pageSize
=
Number
(
req
.
query
?.
pageSize
)
||
10
;
if
(
!
validatePagination
(
pageNumber
,
pageSize
,
res
))
return
;
const
data
=
await
scheduleBiz
.
getScheduleList
({
pageNumber
:
Number
(
req
.
query
?.
pageNumber
)
||
1
,
pageSize
:
Number
(
req
.
query
?.
pageSize
)
||
10
,
pageNumber
,
pageSize
,
startDate
:
req
.
query
&&
req
.
query
.
startDate
?
req
.
query
.
startDate
:
''
,
endDate
:
req
.
query
&&
req
.
query
.
endDate
?
req
.
query
.
endDate
:
''
,
deviceIds
:
req
.
query
?.
deviceIds
?
req
.
query
.
deviceIds
:
undefined
,
...
...
@@ -236,14 +280,14 @@ async function getDeviceList(req, res) {
* body: { deviceId, power, setTemp, fanSpeed, deviceMode }
*/
async
function
editDevice
(
req
,
res
)
{
let
reqConf
=
{
deviceId
:
'String'
,
power
:
'String'
,
setTemp
:
'Number'
,
fanSpeed
:
'String'
,
deviceMode
:
'String'
,
lock
:
'String'
};
const
NotMustHaveKeys
=
[
"fanSpeed"
,
"deviceMode"
,
"lock"
];
let
{
deviceId
,
power
,
setTemp
,
fanSpeed
,
deviceMode
,
lock
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
let
reqConf
=
{
deviceId
:
'String'
,
power
:
'String'
,
setTemp
:
'Number'
,
fanSpeed
:
'String'
,
deviceMode
:
'String'
,
lock
ed
:
'String'
};
const
NotMustHaveKeys
=
[
"fanSpeed"
,
"deviceMode"
,
"lock
ed
"
];
let
{
deviceId
,
power
,
setTemp
,
fanSpeed
,
deviceMode
,
lock
ed
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
if
(
!
deviceId
||
!
power
)
{
res
.
error
(
"缺少必填参数:deviceId, power"
);
return
;
}
const
data
=
await
deviceBiz
.
editAcDeviceControl
({
deviceId
,
power
,
setTemp
,
fanSpeed
,
deviceMode
,
lock
});
const
data
=
await
deviceBiz
.
editAcDeviceControl
({
deviceId
,
power
,
setTemp
,
fanSpeed
,
deviceMode
,
lock
ed
});
res
.
success
(
data
);
}
...
...
@@ -379,9 +423,13 @@ async function batchEditLocalDevices(req, res) {
* query: pageNumber, pageSize, level, fault_code, device_id, startTime, endTime
*/
async
function
getDeviceFaultList
(
req
,
res
)
{
const
pageNumber
=
Number
(
req
.
query
?.
pageNumber
)
||
1
;
const
pageSize
=
Number
(
req
.
query
?.
pageSize
)
||
10
;
if
(
!
validatePagination
(
pageNumber
,
pageSize
,
res
))
return
;
const
data
=
await
alertWorkOrderBiz
.
getDeviceFaultList
({
pageNumber
:
Number
(
req
.
query
?.
pageNumber
)
||
1
,
pageSize
:
Number
(
req
.
query
?.
pageSize
)
||
10
,
pageNumber
,
pageSize
,
level
:
req
.
query
?.
level
?
Number
(
req
.
query
.
level
)
:
undefined
,
fault_code
:
req
.
query
?.
fault_code
||
undefined
,
device_id
:
req
.
query
?.
device_id
||
undefined
,
...
...
@@ -399,9 +447,14 @@ async function getDeviceFaultList(req, res) {
* query: pageNumber, pageSize, deviceName(设备名称模糊匹配), status(0失败 1成功)
*/
async
function
getDeviceLogList
(
req
,
res
)
{
const
pageNumber
=
Number
(
req
.
query
?.
pageNumber
)
||
1
;
const
pageSize
=
Number
(
req
.
query
?.
pageSize
)
||
10
;
if
(
!
validatePagination
(
pageNumber
,
pageSize
,
res
))
return
;
const
data
=
await
deviceBiz
.
getDeviceLogList
({
pageNumber
:
Number
(
req
.
query
?.
pageNumber
)
||
1
,
pageSize
:
Number
(
req
.
query
?.
pageSize
)
||
10
,
pageNumber
,
pageSize
,
deviceId
:
req
.
query
?.
deviceId
||
undefined
,
deviceName
:
req
.
query
?.
deviceName
||
undefined
,
status
:
req
.
query
?.
status
!==
undefined
&&
req
.
query
.
status
!==
''
?
Number
(
req
.
query
.
status
)
:
undefined
,
});
...
...
@@ -415,7 +468,12 @@ async function getDeviceLogList(req, res) {
* 每小时最高温度和人流展示与对比(今天 vs 昨天)
*/
async
function
getHourlyAnalysis
(
req
,
res
)
{
const
data
=
await
reportBiz
.
getHourlyTempVisitorComparison
(
req
.
query
?.
date
);
const
date
=
req
.
query
?.
date
;
if
(
date
!==
undefined
)
{
// 传了 date 就校验格式
if
(
!
validateDateParam
(
date
,
res
))
return
;
}
const
data
=
await
reportBiz
.
getHourlyTempVisitorComparison
(
date
);
res
.
success
(
data
);
}
...
...
@@ -424,7 +482,201 @@ async function getHourlyAnalysis(req, res) {
* 每天最高温度和人流展示与对比(近 N 天趋势)
*/
async
function
getDailyAnalysis
(
req
,
res
)
{
const
days
=
Number
(
req
.
query
?.
days
)
||
7
;
const
rawDays
=
req
.
query
?.
days
;
let
days
:
number
;
if
(
rawDays
===
undefined
||
rawDays
===
''
||
rawDays
===
null
)
{
days
=
7
;
}
else
{
days
=
Number
(
rawDays
);
if
(
!
Number
.
isInteger
(
days
)
||
days
<
1
)
{
res
.
success
({
success
:
false
,
msg
:
"days 必须为大于等于 1 的整数"
,
code
:
400
});
return
;
}
}
if
(
days
>
365
)
{
days
=
365
;
}
const
data
=
await
reportBiz
.
getDailyTempVisitorComparison
(
days
);
res
.
success
(
data
);
}
// ======================= 设备联动参数管理 =======================
/**
* GET /api/qdm/admin/linkage/list
* 设备联动参数分页列表
* query: pageNumber, pageSize, deviceName(设备名称模糊匹配), linkageType(制冷/制热/新风)
*/
async
function
getLinkageList
(
req
,
res
)
{
const
data
=
await
deviceLinkageBiz
.
getLinkageList
({
pageNumber
:
Number
(
req
.
query
?.
pageNumber
)
||
1
,
pageSize
:
Number
(
req
.
query
?.
pageSize
)
||
10
,
deviceId
:
req
.
query
?.
deviceId
||
undefined
,
deviceName
:
req
.
query
?.
deviceName
||
undefined
,
linkageType
:
req
.
query
?.
linkageType
||
undefined
,
});
res
.
success
(
data
);
}
/**
* GET /api/qdm/admin/linkage/detail?id=1
* 设备联动参数详情
* query: id
*/
async
function
getLinkageDetail
(
req
,
res
)
{
const
id
=
Number
(
req
.
query
?.
id
);
if
(
!
id
)
{
res
.
error
(
"缺少必填参数:id"
);
return
;
}
const
data
=
await
deviceLinkageBiz
.
getLinkageDetail
(
id
);
if
(
data
.
isSuccess
)
{
res
.
success
(
data
.
data
);
}
else
{
res
.
error
(
data
.
msg
);
}
}
/**
* POST /api/qdm/admin/linkage/add
* 新增设备联动参数
* body: deviceId, linkageType, thresholdMax, thresholdMin, thresholdMid,
* settingMax, settingMin, settingMid, divideMethod, beginValidity, endValidity
*/
async
function
addLinkage
(
req
,
res
)
{
let
reqConf
=
{
deviceId
:
'String'
,
linkageType
:
'String'
,
thresholdMax
:
'String'
,
thresholdMin
:
'String'
,
thresholdMid
:
'String'
,
settingMax
:
'String'
,
settingMin
:
'String'
,
settingMid
:
'String'
,
divideMethod
:
'String'
,
beginValidity
:
'Date'
,
endValidity
:
'Date'
,
};
const
NotMustHaveKeys
=
[
"thresholdMax"
,
"thresholdMin"
,
"thresholdMid"
,
"settingMax"
,
"settingMin"
,
"settingMid"
,
"divideMethod"
,
"beginValidity"
,
"endValidity"
];
let
{
deviceId
,
linkageType
,
thresholdMax
,
thresholdMin
,
thresholdMid
,
settingMax
,
settingMin
,
settingMid
,
divideMethod
,
beginValidity
,
endValidity
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
if
(
!
deviceId
||
!
linkageType
)
{
res
.
error
(
"缺少必填参数:deviceId, linkageType"
);
return
;
}
const
data
=
await
deviceLinkageBiz
.
addLinkage
({
deviceId
,
linkageType
,
thresholdMax
:
thresholdMax
??
undefined
,
thresholdMin
:
thresholdMin
??
undefined
,
thresholdMid
:
thresholdMid
??
undefined
,
settingMax
:
settingMax
??
undefined
,
settingMin
:
settingMin
??
undefined
,
settingMid
:
settingMid
??
undefined
,
divideMethod
:
divideMethod
??
undefined
,
beginValidity
:
beginValidity
??
undefined
,
endValidity
:
endValidity
??
undefined
,
});
res
.
success
(
data
);
}
/**
* POST /api/qdm/admin/linkage/add_batch
* 批量新增设备联动参数(为多台设备配置相同规则)
* body: deviceIds(数组), linkageType, thresholdMax, thresholdMin, thresholdMid,
* settingMax, settingMin, settingMid, divideMethod, beginValidity, endValidity
*/
async
function
batchAddLinkage
(
req
,
res
)
{
let
reqConf
=
{
deviceIds
:
'Array'
,
linkageType
:
'String'
,
thresholdMax
:
'String'
,
thresholdMin
:
'String'
,
thresholdMid
:
'String'
,
settingMax
:
'String'
,
settingMin
:
'String'
,
settingMid
:
'String'
,
divideMethod
:
'String'
,
beginValidity
:
'Date'
,
endValidity
:
'Date'
,
};
const
NotMustHaveKeys
=
[
"thresholdMax"
,
"thresholdMin"
,
"thresholdMid"
,
"settingMax"
,
"settingMin"
,
"settingMid"
,
"divideMethod"
,
"beginValidity"
,
"endValidity"
];
let
{
deviceIds
,
linkageType
,
thresholdMax
,
thresholdMin
,
thresholdMid
,
settingMax
,
settingMin
,
settingMid
,
divideMethod
,
beginValidity
,
endValidity
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
if
(
!
deviceIds
||
!
Array
.
isArray
(
deviceIds
)
||
deviceIds
.
length
===
0
||
!
linkageType
)
{
res
.
error
(
"缺少必填参数:deviceIds, linkageType"
);
return
;
}
const
data
=
await
deviceLinkageBiz
.
batchAddLinkage
({
deviceIds
,
linkageType
,
thresholdMax
:
thresholdMax
??
undefined
,
thresholdMin
:
thresholdMin
??
undefined
,
thresholdMid
:
thresholdMid
??
undefined
,
settingMax
:
settingMax
??
undefined
,
settingMin
:
settingMin
??
undefined
,
settingMid
:
settingMid
??
undefined
,
divideMethod
:
divideMethod
??
undefined
,
beginValidity
:
beginValidity
??
undefined
,
endValidity
:
endValidity
??
undefined
,
});
res
.
success
(
data
);
}
/**
* POST /api/qdm/admin/linkage/edit
* 编辑设备联动参数
* body: id + 待更新字段(同新增)
*/
async
function
editLinkage
(
req
,
res
)
{
let
reqConf
=
{
id
:
'String'
,
deviceId
:
'String'
,
linkageType
:
'String'
,
thresholdMax
:
'String'
,
thresholdMin
:
'String'
,
thresholdMid
:
'String'
,
settingMax
:
'String'
,
settingMin
:
'String'
,
settingMid
:
'String'
,
divideMethod
:
'String'
,
beginValidity
:
'Date'
,
endValidity
:
'Date'
,
};
const
NotMustHaveKeys
=
[
"deviceId"
,
"linkageType"
,
"thresholdMax"
,
"thresholdMin"
,
"thresholdMid"
,
"settingMax"
,
"settingMin"
,
"settingMid"
,
"divideMethod"
,
"beginValidity"
,
"endValidity"
];
let
{
id
,
deviceId
,
linkageType
,
thresholdMax
,
thresholdMin
,
thresholdMid
,
settingMax
,
settingMin
,
settingMid
,
divideMethod
,
beginValidity
,
endValidity
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
if
(
!
id
)
{
res
.
error
(
"缺少必填参数:id"
);
return
;
}
const
data
=
await
deviceLinkageBiz
.
editLinkage
({
id
:
Number
(
id
),
deviceId
:
deviceId
??
undefined
,
linkageType
:
linkageType
??
undefined
,
thresholdMax
:
thresholdMax
??
undefined
,
thresholdMin
:
thresholdMin
??
undefined
,
thresholdMid
:
thresholdMid
??
undefined
,
settingMax
:
settingMax
??
undefined
,
settingMin
:
settingMin
??
undefined
,
settingMid
:
settingMid
??
undefined
,
divideMethod
:
divideMethod
??
undefined
,
beginValidity
:
beginValidity
??
undefined
,
endValidity
:
endValidity
??
undefined
,
});
res
.
success
(
data
);
}
/**
* POST /api/qdm/admin/linkage/delete
* 删除设备联动参数
* body: id
*/
async
function
deleteLinkage
(
req
,
res
)
{
const
{
id
}
=
req
.
body
||
{};
if
(
!
id
)
{
res
.
error
(
"缺少必填参数:id"
);
return
;
}
const
data
=
await
deviceLinkageBiz
.
deleteLinkage
(
Number
(
id
));
res
.
success
(
data
);
}
src/routers/device.ts
View file @
437999e3
...
...
@@ -34,6 +34,12 @@ export function setRouter(httpServer) {
httpServer
.
post
(
'/api/qdm/run/monitor/acRun'
,
asyncHandler
(
controlAcRunning
));
/** 运行分析 - 环境质量弹窗 */
httpServer
.
post
(
'/api/qdm/run/analysis/envPop'
,
asyncHandler
(
getRunAnalysisEnvPop
));
/** 运行分析 - 离线设备弹窗 */
httpServer
.
post
(
'/api/qdm/run/analysis/offlineDevicePop'
,
asyncHandler
(
getOfflineDevicePop
));
/** 运行分析 - 故障设备弹窗 */
httpServer
.
post
(
'/api/qdm/run/analysis/faultDevicePop'
,
asyncHandler
(
getFaultDevicePop
));
/** 运行分析 - 设备能耗监控弹窗 */
httpServer
.
post
(
'/api/qdm/run/analysis/deviceEnergyPop'
,
asyncHandler
(
getDeviceEnergyPop
));
/** 启动数据集成 */
httpServer
.
post
(
'/api/qdm/run/start/schedule'
,
asyncHandler
(
startSchedule
));
...
...
@@ -201,6 +207,39 @@ async function getRunAnalysisEnvPop(req, res) {
}
/**
* 运行分析 - 离线设备弹窗
*/
async
function
getOfflineDevicePop
(
req
,
res
)
{
let
reqConf
=
{
regionGroup
:
'String'
,
regionType
:
'String'
};
const
NotMustHaveKeys
=
[
"regionGroup"
,
"regionType"
];
let
{
regionGroup
,
regionType
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
const
result
=
await
deviceBiz
.
getOfflineDeviceList
(
regionGroup
,
regionType
);
res
.
success
(
result
);
}
/**
* 运行分析 - 故障设备弹窗
*/
async
function
getFaultDevicePop
(
req
,
res
)
{
let
reqConf
=
{
regionGroup
:
'String'
,
regionType
:
'String'
};
const
NotMustHaveKeys
=
[
"regionGroup"
,
"regionType"
];
let
{
regionGroup
,
regionType
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
const
result
=
await
deviceBiz
.
getFaultDeviceList
(
regionGroup
,
regionType
);
res
.
success
(
result
);
}
/**
* 运行分析 - 设备能耗监控弹窗(各区域昨日/今日能耗)
*/
async
function
getDeviceEnergyPop
(
req
,
res
)
{
let
reqConf
=
{
regionGroup
:
'String'
,
regionType
:
'String'
};
const
NotMustHaveKeys
=
[
"regionGroup"
,
"regionType"
];
let
{
regionGroup
,
regionType
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
const
result
=
await
deviceBiz
.
getDeviceEnergyList
(
regionGroup
,
regionType
);
res
.
success
(
result
);
}
/**
* 本月累计用电(按天)
*/
async
function
getMonthlyEnergy
(
req
,
res
)
{
...
...
src/routers/feiyiClient.ts
View file @
437999e3
...
...
@@ -14,8 +14,22 @@ export function setRouter(httpServer) {
httpServer
.
post
(
'/api/qdm/feiyi/indoorUnit/list'
,
asyncHandler
(
getFeiyiIndoorUnitList
));
/** 空调内机控制 */
httpServer
.
post
(
'/api/qdm/feiyi/indoorUnit/control'
,
asyncHandler
(
getFeiyiIndoorUnitControl
));
/** 单个内机分摊电量和电费 */
httpServer
.
post
(
'/api/qdm/feiyi/indoorUnit/expenses'
,
asyncHandler
(
getIndoorUnitExpensesApi
));
/** 房间电费日使用详情 */
httpServer
.
post
(
'/api/qdm/feiyi/indoorUnit/expenses/list'
,
asyncHandler
(
getRoomExpensesListApi
));
/** 电费数据写入设备扩展表 */
httpServer
.
post
(
'/api/qdm/feiyi/indoorUnit/expenses/save'
,
asyncHandler
(
saveRoomExpensesToExpansionApi
));
/** 机房群控设备最新参数数据(测试接口) */
httpServer
.
get
(
'/api/qdm/feiyi/roomGroup/data'
,
asyncHandler
(
getRoomGroupDeviceDataApi
));
/** 机房群控获取网关信息(测试接口) */
httpServer
.
get
(
'/api/qdm/feiyi/roomGroup/gatewayInfo'
,
asyncHandler
(
getRoomGroupGatewayInfoApi
));
/** 机房群控参数写入(测试接口) */
httpServer
.
post
(
'/api/qdm/feiyi/roomGroup/writeParams'
,
asyncHandler
(
setRoomGroupParamsApi
));
}
// =============================================== 控制系统 ==================================================
/**
* 网关设备列表
...
...
@@ -81,4 +95,104 @@ async function getFeiyiIndoorUnitControl(req, res) {
res
.
success
(
result
);
}
/**
* 单个内机分摊电量和电费
* @param page 当前页码(选填)
* @param limit 每页条数(选填)
* @param roomId 房间ID(选填)
* @param indoorUnitId 内机ID(选填)
* @param indoorUnitAddressFull 内机地址(选填)
* @param occurrenceTime 日期 yyyyMMdd(必填)
*/
async
function
getIndoorUnitExpensesApi
(
req
,
res
)
{
let
reqConf
=
{
page
:
'Number'
,
limit
:
'Number'
,
roomId
:
'String'
,
indoorUnitId
:
'String'
,
indoorUnitAddressFull
:
'String'
,
occurrenceTime
:
'String'
,
};
const
NotMustHaveKeys
=
[
"page"
,
"limit"
,
"roomId"
,
"indoorUnitId"
,
"indoorUnitAddressFull"
];
let
params
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
const
result
=
await
feiyiClientBiz
.
getIndoorUnitExpenses
(
params
);
res
.
success
(
result
);
}
/**
* 房间电费日使用详情
* @param page 当前页码(选填)
* @param limit 每页条数(选填)
* @param roomIds 房间ID集合(选填)
* @param floorIds 楼层ID集合(选填)
* @param buildingIds 楼栋ID集合(选填)
* @param occurrenceTime 日期 yyyyMMdd(必填)
*/
async
function
getRoomExpensesListApi
(
req
,
res
)
{
let
reqConf
=
{
page
:
'Number'
,
limit
:
'Number'
,
roomIds
:
'Array'
,
floorIds
:
'Array'
,
buildingIds
:
'Array'
,
occurrenceTime
:
'String'
,
};
const
NotMustHaveKeys
=
[
"page"
,
"limit"
,
"roomIds"
,
"floorIds"
,
"buildingIds"
];
let
params
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
const
result
=
await
feiyiClientBiz
.
getRoomExpensesList
(
params
);
res
.
success
(
result
);
}
/**
* 电费数据写入设备扩展表 device_expansion
* 拉取"房间电费日使用详情",按房间(region)映射到本地设备,写入 device_expansion
* @param occurrenceTime 日期 yyyyMMdd(必填)
* @param deviceType 写入的设备类型,默认"电能监测"
*/
async
function
saveRoomExpensesToExpansionApi
(
req
,
res
)
{
let
reqConf
=
{
occurrenceTime
:
'String'
,
deviceType
:
'String'
};
const
NotMustHaveKeys
=
[
"deviceType"
];
let
params
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
const
result
=
await
feiyiClientBiz
.
saveRoomExpensesToExpansion
(
params
.
occurrenceTime
,
params
.
deviceType
||
'电能监测'
);
res
.
success
(
result
);
}
// =============================================== 机房系统 ==================================================
/**
* 机房群控设备最新参数数据
* @param gwSn 网关编号(可选)
* @param deviceAddr 设备地址
*/
async
function
getRoomGroupDeviceDataApi
(
req
,
res
)
{
let
reqConf
=
{
gwSn
:
'String'
,
deviceAddr
:
'String'
};
const
NotMustHaveKeys
=
[
"gwSn"
];
let
params
=
eccReqParamater
(
reqConf
,
req
.
query
,
NotMustHaveKeys
);
const
result
=
await
feiyiClientBiz
.
getRoomGroupDeviceData
(
params
.
gwSn
,
params
.
deviceAddr
);
res
.
success
(
result
);
}
/**
* 机房群控获取网关信息
* @param projectId 项目ID
*/
async
function
getRoomGroupGatewayInfoApi
(
req
,
res
)
{
let
reqConf
=
{
projectId
:
'String'
};
const
NotMustHaveKeys
:
string
[]
=
[];
let
params
=
eccReqParamater
(
reqConf
,
req
.
query
,
NotMustHaveKeys
);
const
result
=
await
feiyiClientBiz
.
getRoomGroupGatewayInfo
(
params
.
projectId
);
res
.
success
(
result
);
}
/**
* 机房群控参数写入
* @param addr 设备地址
* @param gwId 网关ID(可选)
* @param projectId 项目ID
* @param params 写入参数 JSON,如 { "1": 0.0 }
*/
async
function
setRoomGroupParamsApi
(
req
,
res
)
{
let
reqConf
=
{
addr
:
'String'
,
gwId
:
'Number'
,
projectId
:
'String'
,
params
:
'Object'
};
const
NotMustHaveKeys
=
[
"gwId"
];
let
params
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
const
result
=
await
feiyiClientBiz
.
setRoomGroupParams
(
params
.
addr
,
params
.
gwId
,
params
.
projectId
,
params
.
params
);
res
.
success
(
result
);
}
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