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
daed81cb
Commit
daed81cb
authored
Aug 11, 2026
by
PC-20251223ZVQQ\Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
联动日程设置
parent
77a71622
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
378 additions
and
61 deletions
+378
-61
device.ts
src/biz/device.ts
+2
-1
deviceFault.ts
src/biz/deviceFault.ts
+84
-0
feiyiClient.ts
src/biz/feiyiClient.ts
+1
-0
mqttClient.ts
src/biz/mqttClient.ts
+5
-4
running.ts
src/biz/running.ts
+27
-21
schedule.ts
src/biz/schedule.ts
+35
-24
scheduleExecutor.ts
src/biz/scheduleExecutor.ts
+1
-1
scheduleStatus.ts
src/biz/scheduleStatus.ts
+190
-0
main.ts
src/main.ts
+1
-1
admin.ts
src/routers/admin.ts
+32
-9
No files found.
src/biz/device.ts
View file @
daed81cb
...
...
@@ -969,8 +969,9 @@ export async function editAcDeviceControl(params: {
setTemp
?:
number
;
fanSpeed
?:
string
;
deviceMode
?:
string
;
lock
?:
string
;
}):
Promise
<
{
success
:
boolean
;
message
:
string
}
>
{
const
{
deviceId
,
power
,
setTemp
,
fanSpeed
,
deviceMode
}
=
params
;
const
{
deviceId
,
power
,
setTemp
,
fanSpeed
,
deviceMode
,
lock
}
=
params
;
// 1. 校验设备是否存在
let
device
=
await
selectOneDataByParam
(
...
...
src/biz/deviceFault.ts
0 → 100644
View file @
daed81cb
import
{
selectPaginatedDataWithOrder
}
from
"../data/findData"
;
import
{
TABLENAME
}
from
"../config/dbEnum"
;
/** 故障等级映射 */
const
levelMap
:
Record
<
number
,
string
>
=
{
1
:
'一级'
,
2
:
'二级'
,
3
:
'三级'
,
};
/** 处理状态映射 */
const
statusMap
:
Record
<
number
,
string
>
=
{
0
:
'未处理'
,
1
:
'处理中'
,
2
:
'已解决'
,
};
export
interface
AlertWorkOrderQueryParams
{
pageNumber
?:
number
;
pageSize
?:
number
;
level
?:
number
;
// 故障等级:1/2/3
fault_code
?:
string
;
// 故障代码(模糊匹配)
device_id
?:
string
;
// 设备ID(模糊匹配)
startTime
?:
string
;
// 时间范围起始 'YYYY-MM-DD HH:mm:ss'
endTime
?:
string
;
// 时间范围截止 'YYYY-MM-DD HH:mm:ss'
}
/**
* 预警工单列表查询(分页)
*/
export
async
function
getDeviceFaultList
(
params
:
AlertWorkOrderQueryParams
)
{
const
pageNumber
=
params
.
pageNumber
||
1
;
const
pageSize
=
params
.
pageSize
||
10
;
// 构建查询条件
const
queryParam
:
any
=
{};
if
(
params
.
level
!==
undefined
&&
params
.
level
!==
null
)
{
queryParam
.
level
=
params
.
level
;
}
if
(
params
.
fault_code
)
{
queryParam
.
fault_code
=
{
"%like%"
:
params
.
fault_code
};
}
if
(
params
.
device_id
)
{
queryParam
.
device_id
=
{
"%like%"
:
params
.
device_id
};
}
if
(
params
.
startTime
||
params
.
endTime
)
{
const
start
=
params
.
startTime
||
'1970-01-01 00:00:00'
;
const
end
=
params
.
endTime
||
'2099-12-31 23:59:59'
;
queryParam
.
occurred_time
=
{
"%between%"
:
[
start
,
end
]
};
}
// 分页查询,按创建时间倒序
const
result
=
await
selectPaginatedDataWithOrder
(
TABLENAME
.
设备故障表
,
{
...
queryParam
},
[
"id"
,
"device_id"
,
"fault_type"
,
"fault_code"
,
"fault_description"
,
"occurred_time"
,
"level"
,
"status"
,
"created_at"
],
pageNumber
,
pageSize
,
[[
"created_at"
,
"DESC"
]],
);
// 映射中文名
const
list
=
(
result
.
data
as
any
[]).
map
((
item
:
any
)
=>
({
id
:
item
.
id
,
device_id
:
item
.
device_id
||
''
,
fault_type
:
item
.
fault_type
||
''
,
fault_code
:
item
.
fault_code
||
''
,
fault_description
:
item
.
fault_description
||
''
,
occurred_time
:
item
.
occurred_time
||
''
,
created_at
:
item
.
created_at
||
''
,
level
:
item
.
level
,
levelName
:
levelMap
[
item
.
level
]
||
'未知'
,
status
:
item
.
status
,
statusName
:
statusMap
[
item
.
status
]
||
'未知'
,
order_address
:
''
,
// 预留:工单地址,todo
order_send
:
'on'
,
// 预留:工单推送,默认 on,todo
}));
return
{
list
,
pagination
:
result
.
pagination
,
};
}
src/biz/feiyiClient.ts
View file @
daed81cb
...
...
@@ -304,6 +304,7 @@ export async function getIndoorUnitList(params: any): Promise<any> {
export
async
function
controlIndoorUnit
(
params
:
any
):
Promise
<
any
>
{
const
path
=
'/openApi/indoorUnit/control'
;
if
(
!
params
.
indoorUnitAddressFull
||
!
Array
.
isArray
(
params
.
indoorUnitAddressFull
)
||
params
.
indoorUnitAddressFull
.
length
===
0
)
{
console
.
error
(
'控制内机: 设备编号不能为空'
);
throw
new
BizError
(
ERRORENUM
.
参数错误
,
"设备编号不能为空"
);
}
const
body
=
{
...
params
};
...
...
src/biz/mqttClient.ts
View file @
daed81cb
...
...
@@ -3,6 +3,7 @@ import { addData } from "../data/addData";
import
{
selectOneDataByParam
}
from
"../data/findData"
;
import
{
controlAcByEnvironment
}
from
"./running"
;
import
{
systemConfig
}
from
"../config/serverConfig"
;
import
{
isInOffPeriod
}
from
"./scheduleStatus"
;
/**
* 处理接收到的环境设备数据,并存入数据库
...
...
@@ -111,10 +112,10 @@ export async function processDeviceData(message) {
console
.
log
(
`数据入库成功: 设备
${
devEUI
}
`
);
writeLog
(
1
,
'正常'
);
//
晚上 23:00 ~ 次日 05:00 不执行空调自控(休眠时段
)
const
currentHour
=
new
Date
().
getHours
()
;
if
(
currentHour
>=
23
||
currentHour
<
5
)
{
console
.
log
(
`
当前时间
${
currentHour
}
:00,处于休眠时段(23:00-05:00)
,跳过空调自控`
);
//
动态判断设备是否处于关闭/休眠时段(基于日程开关机时间,默认 23:00-05:00
)
const
regionKey
=
deviceRow
?.
data
?.
region_key
||
0
;
if
(
await
isInOffPeriod
(
devEUI
,
regionKey
)
)
{
console
.
log
(
`
设备
${
devEUI
}
当前处于关闭/休眠时段
,跳过空调自控`
);
return
;
}
// 根据regionKey查询区域表,若包含文创,则禁用空调控制
...
...
src/biz/running.ts
View file @
daed81cb
...
...
@@ -473,7 +473,7 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
const
sortedFaults
:
any
[]
=
Array
.
isArray
(
ordersRes
.
data
)
?
[...
ordersRes
.
data
]
:
[];
let
deviceRegionMap
=
new
Map
<
string
,
string
>
();
if
(
regionType
&&
sortedFaults
.
length
>
0
)
{
//
if (regionType && sortedFaults.length > 0) {
const
faultUniqueDeviceIds
=
[...
new
Set
(
sortedFaults
.
map
((
f
:
any
)
=>
f
.
device_id
))];
const
devicesRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_id
:
{
"%in%"
:
faultUniqueDeviceIds
}
},
...
...
@@ -484,13 +484,15 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
deviceRegionKeyMap
.
set
(
d
.
device_id
,
d
.
region_key
);
if
(
d
.
region_key
)
regionKeySet
.
add
(
d
.
region_key
);
}
let
deviceOrderAddressMap
=
new
Map
<
string
,
string
>
();
if
(
regionKeySet
.
size
>
0
)
{
const
regionsRes
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{
id
:
{
"%in%"
:
[...
regionKeySet
]
},
"%orderAsc%"
:
"sort_order"
},
[
"id"
,
"name"
]);
[
"id"
,
"name"
,
"groups"
,
"type"
]);
const
regionNameMap
=
new
Map
<
string
,
string
>
();
for
(
const
r
of
regionsRes
.
data
)
{
regionNameMap
.
set
(
String
(
r
.
id
),
r
.
name
);
deviceOrderAddressMap
.
set
(
String
(
r
.
id
),
`
${
r
.
groups
||
''
}
_
${
r
.
type
||
''
}
`
);
}
for
(
const
[
deviceId
,
regionKey
]
of
deviceRegionKeyMap
)
{
deviceRegionMap
.
set
(
deviceId
,
regionNameMap
.
get
(
regionKey
)
||
''
);
...
...
@@ -504,14 +506,18 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
alertAddress
:
deviceRegionMap
.
get
(
f
.
device_id
)
||
''
,
device_id
:
f
.
device_id
||
''
,
faultCode
:
f
.
fault_code
||
''
,
order_address
:
deviceOrderAddressMap
.
get
(
deviceRegionKeyMap
.
get
(
f
.
device_id
)?.
toString
())
||
''
,
order_send
:
'on'
,
// TODO: 后续需要实现工单推送的实际逻辑
}));
}
else
{
alertWorkOrders
=
sortedFaults
.
map
((
f
:
any
)
=>
({
riskContent
:
f
.
fault_description
||
f
.
fault_type
||
''
,
alertTime
:
f
.
occurred_time
||
''
,
status
:
faultStatusTextMap
[
f
.
status
]
||
'未知'
,
}));
}
// } else {
// alertWorkOrders = sortedFaults.map((f: any) => ({
// riskContent: f.fault_description || f.fault_type || '',
// alertTime: f.occurred_time || '',
// status: faultStatusTextMap[f.status] || '未知',
// order_address: '',
// order_send: 'on', // TODO: 后续需要实现工单推送的实际逻辑
// }));
// }
}
catch
(
err
)
{
console
.
error
(
"预警监控数据查询失败,使用空数据降级:"
,
err
);
}
...
...
@@ -1050,13 +1056,13 @@ function calcAcTempSet(temperature: number, season: 'summer' | 'winter'): { temp
if
(
season
===
'summer'
)
{
// 夏季制冷:温度越高,设定越低
if
(
temperature
>
24
)
return
{
tempSet
:
16
,
fanSpeed
:
1
,
onOff
:
1
};
// 未达需求:高风制冷
if
(
temperature
>=
21
)
return
{
tempSet
:
18
,
fanSpeed
:
2
,
onOff
:
1
};
// 舒适区:四舍五入21℃待机 Math.round(temperature) === 21 ? 0 : 1
return
{
tempSet
:
19
,
fanSpeed
:
4
,
onOff
:
1
};
// 已达需求:低风维持
if
(
temperature
>=
21
)
return
{
tempSet
:
18
,
fanSpeed
:
1
,
onOff
:
1
};
// 舒适区:四舍五入21℃待机 Math.round(temperature) === 21 ? 0 : 1
return
{
tempSet
:
19
,
fanSpeed
:
1
,
onOff
:
1
};
// 已达需求:低风维持
}
else
{
// 冬季制热:温度越低,设定越高
if
(
temperature
<
18
)
return
{
tempSet
:
21
,
fanSpeed
:
1
,
onOff
:
1
};
// 未达需求:高风制热
if
(
temperature
<=
21
)
return
{
tempSet
:
20
,
fanSpeed
:
4
,
onOff
:
1
};
// 舒适区:四舍五入20℃待机 Math.round(temperature) === 20 ? 0 : 1
return
{
tempSet
:
18
,
fanSpeed
:
4
,
onOff
:
1
};
// 已达需求:低风维持
if
(
temperature
<=
21
)
return
{
tempSet
:
20
,
fanSpeed
:
1
,
onOff
:
1
};
// 舒适区:四舍五入20℃待机 Math.round(temperature) === 20 ? 0 : 1
return
{
tempSet
:
18
,
fanSpeed
:
1
,
onOff
:
1
};
// 已达需求:低风维持
}
}
...
...
@@ -1080,7 +1086,7 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
const
season
=
getSeason
();
const
temperature
=
data
.
temperature
;
const
co2
=
data
.
co2
;
console
.
log
(
`若是环境设备,则解析环境设备数据,判断是否需要联动
${
envDevices
.
data
.
length
}
`
);
// 若是环境设备,则解析环境设备数据,判断是否需要联动
if
(
envDevices
.
data
.
length
)
{
let
regionKey
=
envDevices
.
data
[
0
].
region_key
;
...
...
@@ -1096,7 +1102,7 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
device_type
:
{
"%in%"
:
[
"新风"
,
"空调"
]
},
linkage_start
:
'on'
},
[
"id"
,
"device_name"
,
"device_id"
,
"device_ad"
,
"device_type"
]);
console
.
log
(
`若是新风设备,判断联动是否开启
${
fanDevices
.
data
.
length
}
`
);
if
(
fanDevices
.
data
.
length
>
0
)
{
// CO2联动:高→送风模式(3)+高风(1),低→不传workMode(不切换模式)+低风(4)
// const fanWorkMode: number | undefined = fanParams.action === 'high' ? 3 : undefined;
...
...
@@ -1145,7 +1151,7 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
}
}
}
console
.
log
(
`若是空调设备,判断温度是否为空
${
temperature
}
`
);
// ===== 空调联动 =====
if
(
temperature
!=
null
)
{
let
acDevices
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
...
...
@@ -1153,7 +1159,7 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
device_type
:
"空调"
,
linkage_start
:
'on'
},
[
"id"
,
"device_name"
,
"device_id"
,
"device_ad"
,
"control_params"
]);
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制热
...
...
@@ -1179,9 +1185,9 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
// 去重:若本次计算的控制参数与上次相同,跳过调用
const
cacheKey
=
`ac_
${
regionKey
}
`
;
const
lastState
=
envControlCache
.
get
(
cacheKey
);
if
(
lastState
&&
lastState
.
tempSet
===
tempSet
&&
lastState
.
workMode
===
workMode
&&
lastState
.
fanSpeed
===
fanSpeed
&&
lastState
.
onOff
===
onOff
&&
lastState
.
maxTemp
===
maxTemp
&&
lastState
.
minTemp
===
minTemp
)
{
console
.
log
(
`空调联动跳过(控制参数未变):
${
modeName
}
模式, 设定
${
tempSet
}
℃, 风速
${
fanSpeed
===
1
?
'高'
:
'低'
}
风,
${
onOff
?
'开机'
:
'待机'
}
, 区域
${
regionKey
}
`
);
}
else
{
//
if (lastState && lastState.tempSet === tempSet && lastState.workMode === workMode && lastState.fanSpeed === fanSpeed && lastState.onOff === onOff && lastState.maxTemp === maxTemp && lastState.minTemp === minTemp) {
//
console.log(`空调联动跳过(控制参数未变): ${modeName}模式, 设定${tempSet}℃, 风速${fanSpeed === 1 ? '高' : '低'}风, ${onOff ? '开机' : '待机'}, 区域 ${regionKey}`);
//
} else {
let
controlRes
=
await
controlAcRunningApi
(
acDevices
,
workMode
,
tempSet
,
fanSpeed
,
onOff
,
maxTemp
,
minTemp
);
// 更新缓存
...
...
@@ -1213,7 +1219,7 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
}
else
{
console
.
error
(
`空调联动(
${
modeName
}
)失败:
${
controlRes
.
message
}
`
);
}
}
//
}
}
}
}
...
...
src/biz/schedule.ts
View file @
daed81cb
...
...
@@ -7,6 +7,7 @@ import { addData } from "../data/addData";
import
{
delData
}
from
"../data/delData"
;
import
{
updateManyData
}
from
"../data/updateData"
;
import
{
TABLENAME
}
from
"../config/dbEnum"
;
import
{
clearOffPeriodCache
}
from
"./scheduleStatus"
;
// ======================= 公共工具 =======================
...
...
@@ -185,19 +186,23 @@ export async function addSchedule(params: {
return
{
isSuccess
:
false
,
msg
:
"日程创建失败"
};
}
// 3. 查这些区域下的所有空调设备
let
deviceParams
:
any
=
{
device_type
:
"空调"
,
};
// 3. 查空调设备(优先 deviceIds,补充 regionKeys 维度)
let
devices
:
any
[]
=
[];
if
(
deviceIds
&&
deviceIds
.
length
)
{
deviceParams
.
device_id
=
{
"%in%"
:
deviceIds
};
}
if
(
regionKeys
&&
regionKeys
.
length
)
{
deviceParams
.
region_key
=
{
"%in%"
:
regionKeys
};
// 有具体设备时,按 deviceIds 查,取出它们的 device_id + region_key
const
deviceRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_type
:
"空调"
,
device_id
:
{
"%in%"
:
deviceIds
},
},
[
"device_id"
,
"region_key"
]);
devices
=
deviceRes
.
data
||
[];
}
else
if
(
regionKeys
&&
regionKeys
.
length
)
{
// 仅传区域时,查该区域下所有空调
const
deviceRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_type
:
"空调"
,
region_key
:
{
"%in%"
:
regionKeys
},
},
[
"device_id"
,
"region_key"
]);
devices
=
deviceRes
.
data
||
[];
}
const
deviceRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
deviceParams
,
[
"device_id"
,
"region_key"
]);
const
devices
=
deviceRes
.
data
||
[];
// 4. 批量插入 schedule_device 关联记录
if
(
devices
.
length
>
0
)
{
...
...
@@ -208,8 +213,8 @@ export async function addSchedule(params: {
}));
await
addData
(
TABLENAME
.
日程设备关联表
,
sdRecords
);
console
.
log
(
`[新增日程] 关联设备
${
sdRecords
.
length
}
条`
);
}
else
if
(
!
regionKeys
&&
!
regionKeys
.
length
)
{
// 无设备
时也插入一条区域占位记录(允许区域下暂时无设备)
}
else
if
(
regionKeys
&&
regionKeys
.
length
)
{
// 无设备
但传了区域时,插入区域占位记录
for
(
const
rk
of
regionKeys
)
{
await
addData
(
TABLENAME
.
日程设备关联表
,
{
schedule_id
:
scheduleId
,
...
...
@@ -220,6 +225,7 @@ export async function addSchedule(params: {
console
.
log
(
`[新增日程] 区域
${
regionKeys
.
length
}
个,暂无设备`
);
}
clearOffPeriodCache
();
// 新增日程后清空开关机时段缓存
return
{
isSuccess
:
true
,
scheduleId
};
}
...
...
@@ -298,18 +304,21 @@ export async function editSchedule(params: {
// 2. 删除旧的关联记录
await
delData
(
TABLENAME
.
日程设备关联表
,
{
schedule_id
:
scheduleId
});
// 3. 查区域下的空调设备
let
deviceParams
:
any
=
{
device_type
:
"空调"
,
};
// 3. 查空调设备(优先 deviceIds,补充区域维度)
let
devices
:
any
[]
=
[];
if
(
deviceIds
&&
deviceIds
.
length
)
{
deviceParams
.
device_id
=
{
"%in%"
:
deviceIds
};
}
if
(
regionKeys
&&
regionKeys
.
length
)
{
deviceParams
.
region_key
=
{
"%in%"
:
regionKeys
};
const
deviceRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_type
:
"空调"
,
device_id
:
{
"%in%"
:
deviceIds
},
},
[
"device_id"
,
"region_key"
]);
devices
=
deviceRes
.
data
||
[];
}
else
if
(
regionKeys
&&
regionKeys
.
length
)
{
const
deviceRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_type
:
"空调"
,
region_key
:
{
"%in%"
:
regionKeys
},
},
[
"device_id"
,
"region_key"
]);
devices
=
deviceRes
.
data
||
[];
}
const
deviceRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
deviceParams
,
[
"device_id"
,
"region_key"
]);
const
devices
=
deviceRes
.
data
||
[];
// 4. 重建关联
if
(
devices
.
length
>
0
)
{
...
...
@@ -320,7 +329,7 @@ export async function editSchedule(params: {
}));
await
addData
(
TABLENAME
.
日程设备关联表
,
sdRecords
);
console
.
log
(
`[编辑日程] 重建关联设备
${
sdRecords
.
length
}
条`
);
}
else
if
(
!
regionKeys
&&
!
regionKeys
.
length
)
{
}
else
if
(
regionKeys
&&
regionKeys
.
length
)
{
for
(
const
rk
of
regionKeys
)
{
await
addData
(
TABLENAME
.
日程设备关联表
,
{
schedule_id
:
scheduleId
,
...
...
@@ -331,6 +340,7 @@ export async function editSchedule(params: {
console
.
log
(
`[编辑日程] 区域
${
regionKeys
.
length
}
个,暂无设备`
);
}
clearOffPeriodCache
();
// 编辑日程后清空开关机时段缓存
return
{
isSuccess
:
true
};
}
...
...
@@ -348,5 +358,6 @@ export async function deleteSchedule(scheduleId: number): Promise<any> {
// 2. 删除主记录
await
delData
(
TABLENAME
.
日程表
,
{
id
:
scheduleId
});
clearOffPeriodCache
();
// 删除日程后清空开关机时段缓存
return
{
isSuccess
:
true
};
}
src/biz/scheduleExecutor.ts
View file @
daed81cb
...
...
@@ -179,7 +179,7 @@ async function executeControl(
scheduleId
,
time
:
formatLocalTime
(
now
),
acControl
,
params
:
{
...
params
,
indoorUnitAddressFull
:
undefined
},
params
:
{
...
params
,
indoorUnitAddressFull
:
deviceIds
.
join
(
","
)
},
success
:
result
.
success
,
message
:
result
.
message
,
},
...
...
src/biz/scheduleStatus.ts
0 → 100644
View file @
daed81cb
import
{
TABLENAME
}
from
"../config/dbEnum"
;
import
{
selectDataListByParam
}
from
"../data/findData"
;
/** 默认关机时间 23:00(分钟数) */
const
DEFAULT_OFF_MINUTES
=
23
*
60
;
/** 默认开机时间 05:00(分钟数) */
const
DEFAULT_ON_MINUTES
=
5
*
60
;
/**
* 将 HH:mm:ss 或 HH:mm 格式的时间转为分钟数
*/
function
timeToMinutes
(
timeStr
:
string
):
number
{
const
parts
=
timeStr
.
split
(
':'
).
map
(
Number
);
return
parts
[
0
]
*
60
+
(
parts
[
1
]
||
0
);
}
// ===== 内存缓存 =====
interface
CacheEntry
{
result
:
boolean
;
expireAt
:
number
;
// 过期时间戳 ms
}
const
offPeriodCache
=
new
Map
<
string
,
CacheEntry
>
();
const
CACHE_TTL_MS
=
5
*
60
*
1000
;
// 5 分钟
function
getCacheKey
(
deviceId
:
string
,
regionKey
:
number
|
string
):
string
{
return
`
${
deviceId
}
_
${
regionKey
}
`
;
}
/** 清空所有缓存 */
export
function
clearOffPeriodCache
():
void
{
offPeriodCache
.
clear
();
console
.
log
(
`[scheduleStatus] 缓存已清空`
);
}
/** 清空指定设备的缓存 */
export
function
clearOffPeriodCacheForDevice
(
deviceId
:
string
):
void
{
const
prefix
=
`
${
deviceId
}
_`
;
for
(
const
key
of
offPeriodCache
.
keys
())
{
if
(
key
.
startsWith
(
prefix
))
{
offPeriodCache
.
delete
(
key
);
}
}
}
/**
* 判断设备当前是否处于"关闭/休眠时段"
*
* 逻辑:
* 1. 查 schedule_device 表,获取设备关联的日程 ID(按 device_id 或 region_key 匹配)
* 2. 查 schedule 表,获取 ac_control=on/off 且设置了 schedule_time 的日程
* 3. 若日程中没有 on 时间 → 用默认 05:00;没有 off 时间 → 用默认 23:00
* 4. 找到当前时间点之前最近的一次操作(on/off),若为 off 则处于关闭时段
* 5. 结果缓存 5 分钟
*
* @returns true 表示当前处于关闭时段,应跳过联动
*/
export
async
function
isInOffPeriod
(
deviceId
:
string
,
regionKey
:
number
):
Promise
<
boolean
>
{
const
cacheKey
=
getCacheKey
(
deviceId
,
regionKey
);
const
cached
=
offPeriodCache
.
get
(
cacheKey
);
// 缓存命中且未过期
if
(
cached
&&
cached
.
expireAt
>
Date
.
now
())
{
return
cached
.
result
;
}
const
now
=
new
Date
();
const
nowMinutes
=
now
.
getHours
()
*
60
+
now
.
getMinutes
();
try
{
// 1. 查 schedule_device:匹配 device_id 或 (region_key 且 device_id 为空,即区域级日程)
const
sdResult
=
await
selectDataListByParam
(
TABLENAME
.
日程设备关联表
,
{
"%or%"
:
[
{
device_id
:
deviceId
},
{
region_key
:
regionKey
,
device_id
:
""
},
],
},
[
"schedule_id"
],
);
const
scheduleIds
=
[...
new
Set
((
sdResult
.
data
as
any
[]).
map
((
d
:
any
)
=>
d
.
schedule_id
))];
// 2. 无关联日程 → 使用默认 23:00-05:00 判断
if
(
scheduleIds
.
length
===
0
)
{
const
result
=
nowMinutes
>=
DEFAULT_OFF_MINUTES
||
nowMinutes
<
DEFAULT_ON_MINUTES
;
offPeriodCache
.
set
(
cacheKey
,
{
result
,
expireAt
:
Date
.
now
()
+
CACHE_TTL_MS
});
console
.
log
(
`[scheduleStatus] 设备
${
deviceId
}
无关联日程,使用默认时段(23:00-05:00),当前
${
result
?
'关闭'
:
'开启'
}
时段`
,
);
return
result
;
}
// 3. 查 schedule 表:有 schedule_time 且 ac_control 为 on/off
const
schResult
=
await
selectDataListByParam
(
TABLENAME
.
日程表
,
{
id
:
{
"%in%"
:
scheduleIds
},
schedule_time
:
{
"%ne%"
:
null
},
ac_control
:
{
"%in%"
:
[
"on"
,
"off"
]
},
},
[
"ac_control"
,
"schedule_time"
],
);
const
schedules
=
schResult
.
data
as
any
[];
let
hasOnSchedule
=
false
;
let
hasOffSchedule
=
false
;
let
latestOffBeforeNow
=
-
1
;
// 今天当前时间之前最近的 off,不跨天时
let
latestOnBeforeNow
=
-
1
;
const
allOffMinutes
:
number
[]
=
[];
const
allOnMinutes
:
number
[]
=
[];
for
(
const
s
of
schedules
)
{
const
minutes
=
timeToMinutes
(
s
.
schedule_time
);
if
(
s
.
ac_control
===
"off"
)
{
hasOffSchedule
=
true
;
allOffMinutes
.
push
(
minutes
);
if
(
minutes
<=
nowMinutes
&&
minutes
>
latestOffBeforeNow
)
{
latestOffBeforeNow
=
minutes
;
}
}
else
if
(
s
.
ac_control
===
"on"
)
{
hasOnSchedule
=
true
;
allOnMinutes
.
push
(
minutes
);
if
(
minutes
<=
nowMinutes
&&
minutes
>
latestOnBeforeNow
)
{
latestOnBeforeNow
=
minutes
;
}
}
}
// 4. 构建完整的 on/off 时间点列表(含默认值兜底)
const
actions
:
{
time
:
number
;
type
:
"on"
|
"off"
}[]
=
[];
// 加入日程中的时间点
for
(
const
min
of
allOffMinutes
)
{
actions
.
push
({
time
:
min
,
type
:
"off"
});
}
for
(
const
min
of
allOnMinutes
)
{
actions
.
push
({
time
:
min
,
type
:
"on"
});
}
// 兜底:日程中没有 on → 默认 05:00 开机
if
(
!
hasOnSchedule
)
{
actions
.
push
({
time
:
DEFAULT_ON_MINUTES
,
type
:
"on"
});
}
// 兜底:日程中没有 off → 默认 23:00 关机
if
(
!
hasOffSchedule
)
{
actions
.
push
({
time
:
DEFAULT_OFF_MINUTES
,
type
:
"off"
});
}
// 按时间排序
actions
.
sort
((
a
,
b
)
=>
a
.
time
-
b
.
time
);
// 5. 找到当前时间之前最近的一次操作
// 如果今天没有 ≤ nowMinutes 的操作,说明所有操作都在未来,
// 取昨天最后一个操作(即 actions 中时间最大的那个,视为跨天)
let
lastAction
:
{
time
:
number
;
type
:
"on"
|
"off"
}
|
null
=
null
;
for
(
let
i
=
actions
.
length
-
1
;
i
>=
0
;
i
--
)
{
if
(
actions
[
i
].
time
<=
nowMinutes
)
{
lastAction
=
actions
[
i
];
break
;
}
}
if
(
lastAction
===
null
)
{
// 所有操作都在当前时间之后 → 取昨天的最后一个操作
// 最后一条就是时间最大的,视为昨天执行的操作
const
yesterdayAction
=
actions
[
actions
.
length
-
1
];
const
result
=
yesterdayAction
.
type
===
"off"
;
offPeriodCache
.
set
(
cacheKey
,
{
result
,
expireAt
:
Date
.
now
()
+
CACHE_TTL_MS
});
const
offTime
=
Math
.
floor
(
yesterdayAction
.
time
/
60
);
console
.
log
(
`[scheduleStatus] 设备
${
deviceId
}
跨天,昨天最后操作为
${
yesterdayAction
.
type
}
(
${
offTime
}
:00),当前
${
result
?
'关闭'
:
'开启'
}
时段`
,
);
return
result
;
}
const
result
=
lastAction
.
type
===
"off"
;
offPeriodCache
.
set
(
cacheKey
,
{
result
,
expireAt
:
Date
.
now
()
+
CACHE_TTL_MS
});
const
actionHour
=
Math
.
floor
(
lastAction
.
time
/
60
);
console
.
log
(
`[scheduleStatus] 设备
${
deviceId
}
最近操作为
${
lastAction
.
type
}
(
${
actionHour
}
:00),当前
${
result
?
'关闭'
:
'开启'
}
时段`
,
);
return
result
;
}
catch
(
error
)
{
console
.
error
(
`[scheduleStatus] 查询设备
${
deviceId
}
日程失败,降级为默认 23-05:`
,
error
);
const
result
=
nowMinutes
>=
DEFAULT_OFF_MINUTES
||
nowMinutes
<
DEFAULT_ON_MINUTES
;
return
result
;
}
}
src/main.ts
View file @
daed81cb
...
...
@@ -27,7 +27,7 @@ async function lanuch() {
/**启动故障状态更新定时任务(5分钟) */
startFaultStatusSchedule
();
/**启动日程执行器 */
//
await initScheduleTasks();
await
initScheduleTasks
();
console
.
log
(
'This indicates that the server is started successfully.'
);
...
...
src/routers/admin.ts
View file @
daed81cb
...
...
@@ -7,6 +7,7 @@ import * as reportBiz from "../biz/report";
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
{
reloadScheduleTasks
}
from
"../biz/scheduleExecutor"
;
import
{
eccReqParamater
}
from
"../util/verificationParam"
;
...
...
@@ -42,6 +43,8 @@ export function setRouter(httpServer) {
/** 设备管理 - 批量编辑设备信息 */
httpServer
.
post
(
"/api/qdm/admin/device/editDevice_batch"
,
asyncHandler
(
batchEditLocalDevices
));
/** 预警工单列表 */
httpServer
.
get
(
"/api/qdm/admin/fault/list"
,
asyncHandler
(
getDeviceFaultList
));
}
/**
...
...
@@ -227,14 +230,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'
};
const
NotMustHaveKeys
=
[
"fanSpeed"
,
"deviceMode"
];
let
{
deviceId
,
power
,
setTemp
,
fanSpeed
,
deviceMode
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
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
);
if
(
!
deviceId
||
!
power
)
{
res
.
error
(
"缺少必填参数:deviceId, power"
);
return
;
}
const
data
=
await
deviceBiz
.
editAcDeviceControl
({
deviceId
,
power
,
setTemp
,
fanSpeed
,
deviceMode
});
const
data
=
await
deviceBiz
.
editAcDeviceControl
({
deviceId
,
power
,
setTemp
,
fanSpeed
,
deviceMode
,
lock
});
res
.
success
(
data
);
}
...
...
@@ -303,7 +306,7 @@ async function batchEditDevices(req, res) {
/**
* POST /api/qdm/admin/device/editDevice
* 设备管理 - 编辑单台设备表信息
* body: { deviceId, deviceName?, regionKey?, deviceType?,
controlParams
?, deviceState? }
* body: { deviceId, deviceName?, regionKey?, deviceType?,
linkageStart
?, deviceState? }
*/
async
function
editLocalDevice
(
req
,
res
)
{
let
reqConf
=
{
...
...
@@ -324,7 +327,7 @@ async function editLocalDevice(req, res) {
const
hasEdit
=
deviceName
!==
undefined
||
regionKey
!==
undefined
||
deviceType
!==
undefined
||
deviceState
!==
undefined
||
linkageStart
!==
undefined
;
if
(
!
hasEdit
)
{
res
.
error
(
"至少需要提供一个编辑字段(deviceName/regionKey/deviceType/
controlParams
/deviceState)"
);
res
.
error
(
"至少需要提供一个编辑字段(deviceName/regionKey/deviceType/
linkageStart
/deviceState)"
);
return
;
}
const
data
=
await
deviceBiz
.
editLocalDevice
({
deviceId
,
deviceName
,
regionKey
,
deviceType
,
deviceState
,
linkageStart
});
...
...
@@ -334,7 +337,7 @@ async function editLocalDevice(req, res) {
/**
* POST /api/qdm/admin/device/editDevice_batch
* 设备管理 - 批量编辑设备表信息
* body: { regionKeys?, deviceName?, edits: { deviceName?, regionKey?, deviceType?,
controlParams
?, deviceState? } }
* body: { regionKeys?, deviceName?, edits: { deviceName?, regionKey?, deviceType?,
linkageStart
?, deviceState? } }
*/
async
function
batchEditLocalDevices
(
req
,
res
)
{
let
reqConf
=
{
...
...
@@ -352,12 +355,32 @@ async function batchEditLocalDevices(req, res) {
res
.
error
(
"至少需要提供 regionKeys 或 deviceName 筛选条件"
);
return
;
}
const
validEditKeys
=
[
"deviceName"
,
"regionKey"
,
"deviceType"
,
"
controlParams"
,
"deviceState
"
];
const
validEditKeys
=
[
"deviceName"
,
"regionKey"
,
"deviceType"
,
"
deviceState"
,
"linkageStart
"
];
const
hasEdit
=
validEditKeys
.
some
(
k
=>
edits
[
k
]
!==
undefined
);
if
(
!
hasEdit
)
{
res
.
error
(
"edits 中至少需要一个编辑字段(deviceName/regionKey/deviceType/
controlParams
/deviceState)"
);
res
.
error
(
"edits 中至少需要一个编辑字段(deviceName/regionKey/deviceType/
linkageStart
/deviceState)"
);
return
;
}
const
data
=
await
deviceBiz
.
batchEditLocalDevices
({
regionKeys
,
deviceName
,
edits
});
res
.
success
(
data
);
}
// ======================= 预警工单 =======================
/**
* GET /api/qdm/admin/alertWorkOrder/list
* 预警工单列表(分页)
* query: pageNumber, pageSize, level, fault_code, device_id, startTime, endTime
*/
async
function
getDeviceFaultList
(
req
,
res
)
{
const
data
=
await
alertWorkOrderBiz
.
getDeviceFaultList
({
pageNumber
:
Number
(
req
.
query
?.
pageNumber
)
||
1
,
pageSize
:
Number
(
req
.
query
?.
pageSize
)
||
10
,
level
:
req
.
query
?.
level
?
Number
(
req
.
query
.
level
)
:
undefined
,
fault_code
:
req
.
query
?.
fault_code
||
undefined
,
device_id
:
req
.
query
?.
device_id
||
undefined
,
startTime
:
req
.
query
?.
startTime
||
undefined
,
endTime
:
req
.
query
?.
endTime
||
undefined
,
});
res
.
success
(
data
);
}
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