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
77a71622
Commit
77a71622
authored
Aug 06, 2026
by
PC-20251223ZVQQ\Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
联动修改
parent
027f2027
Show whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
1092 additions
and
100 deletions
+1092
-100
.gitignore
.gitignore
+1
-0
serverConfig.xml
serverConfig.xml
+3
-0
device.ts
src/biz/device.ts
+527
-2
feiyiClient.ts
src/biz/feiyiClient.ts
+85
-5
mqttClient.ts
src/biz/mqttClient.ts
+48
-2
report.ts
src/biz/report.ts
+25
-13
running.ts
src/biz/running.ts
+129
-64
dbEnum.ts
src/config/dbEnum.ts
+2
-1
mysqlTableConfig.ts
src/config/mysqlTableConfig.ts
+82
-0
main.ts
src/main.ts
+4
-4
sqlModelBind.ts
src/model/sqlModelBind.ts
+5
-2
admin.ts
src/routers/admin.ts
+181
-7
No files found.
.gitignore
View file @
77a71622
...
...
@@ -3,6 +3,7 @@
/node_modules
/test
/public
/scripts
/logs
/video
/files
...
...
serverConfig.xml
View file @
77a71622
...
...
@@ -24,4 +24,7 @@
<username>
admin
</username>
<password>
admin123
</password>
</mqttsrv>
<controller>
<is_use>
true
</is_use>
</controller>
</config>
src/biz/device.ts
View file @
77a71622
import
{
selectOneDataByParam
,
selectDataListByParam
}
from
"../data/findData"
;
import
{
selectOneDataByParam
,
selectDataListByParam
,
selectDataCountByParam
,
selectDataListToPageByParam
}
from
"../data/findData"
;
import
{
TABLENAME
}
from
"../config/dbEnum"
;
import
{
getMySqlMs
}
from
"../tools/systemTools"
;
import
{
BizError
}
from
"../util/bizError"
;
import
{
ERRORENUM
}
from
"../config/errorEnum"
;
import
{
addData
}
from
"../data/addData"
;
import
{
updateManyData
}
from
"../data/updateData"
;
import
{
controlAcByEnvironment
}
from
"./running"
;
import
{
controlAcByEnvironment
,
acDeviceKeyMap
,
formatLocalTime
}
from
"./running"
;
import
{
getIndoorUnitList
,
controlIndoorUnit
}
from
"./feiyiClient"
;
// ======= 模块级缓存(低频变化的数据) =======
let
deviceTreeCache
:
{
data
:
any
;
timestamp
:
number
}
|
null
=
null
;
...
...
@@ -762,3 +763,526 @@ export async function getEnvQualityPop(regionGroup?: string, regionType?: string
return
{
total
:
dataList
.
length
,
dataList
};
}
/**
* 获取设备列表
* @param params
*/
export
async
function
getDeviceList
(
params
:
{
pageNumber
?:
number
;
pageSize
?:
number
;
regionKeys
?:
any
;
deviceName
?:
string
;
power
?:
string
;
state
?:
string
;
deviceMode
?:
string
;
})
{
const
pageNumber
=
params
.
pageNumber
||
1
;
const
pageSize
=
params
.
pageSize
||
10
;
// 解析 regionKeys(兼容字符串和数组格式)
let
regionKeys
:
string
[]
=
[];
if
(
params
.
regionKeys
)
{
if
(
typeof
params
.
regionKeys
===
'string'
)
{
regionKeys
=
params
.
regionKeys
.
split
(
','
).
map
((
k
:
string
)
=>
k
.
trim
()).
filter
(
Boolean
);
}
else
if
(
Array
.
isArray
(
params
.
regionKeys
))
{
regionKeys
=
params
.
regionKeys
;
}
}
// 1. 构建查询参数,只查空调设备,排除 region_key=0 的设备
let
paramAnys
:
any
=
{
device_type
:
'空调'
};
if
(
regionKeys
.
length
)
{
paramAnys
.
region_key
=
{
"%in%"
:
regionKeys
};
}
else
{
paramAnys
.
region_key
=
{
"%ne%"
:
0
};
}
// 2. deviceName 模糊搜索
if
(
params
.
deviceName
)
{
paramAnys
.
device_name
=
{
"%like%"
:
params
.
deviceName
};
}
// 3. state 映射:全部/正常/告警/离线
const
stateMap
:
Record
<
string
,
number
[]
>
=
{
'正常'
:
[
0
],
'告警'
:
[
1
,
2
],
'离线'
:
[
3
],
};
if
(
params
.
state
&&
stateMap
[
params
.
state
])
{
paramAnys
.
device_state
=
{
"%in%"
:
stateMap
[
params
.
state
]
};
}
// 4. 判断是否需要飞奕后置过滤
const
powerMap
:
Record
<
string
,
number
>
=
{
'开'
:
1
,
'关'
:
0
};
const
modeMap
:
Record
<
string
,
number
>
=
{
'制冷'
:
1
,
'制热'
:
2
,
'通风'
:
3
,
'除湿'
:
4
};
const
needFeiyiFilter
=
params
.
power
||
params
.
deviceMode
;
const
targetOnOff
=
params
.
power
?
powerMap
[
params
.
power
]
:
undefined
;
const
targetMode
=
params
.
deviceMode
?
modeMap
[
params
.
deviceMode
]
:
undefined
;
if
(
needFeiyiFilter
)
{
// ==== 飞奕筛选模式:查全量设备,后置过滤再内存分页 ====
const
deviceRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
paramAnys
,
[
"device_id"
,
"region_key"
,
"device_state"
,
"linkage_start"
]
);
let
devices
=
(
deviceRes
.
data
||
[])
as
any
[];
let
filteredRows
:
any
[]
=
[];
const
fanSpeedMap
:
Record
<
number
,
string
>
=
{
1
:
'高风'
,
2
:
'中风'
,
4
:
'低风'
};
const
workModeMap
:
Record
<
number
,
string
>
=
{
1
:
'制冷'
,
2
:
'制热'
,
3
:
'送风'
,
4
:
'除湿'
};
const
deviceStateMap
:
Record
<
number
,
string
>
=
{
0
:
'正常'
,
1
:
'硬件故障'
,
2
:
'通信故障'
,
3
:
'离线'
};
const
pad
=
(
n
:
number
)
=>
String
(
n
).
padStart
(
2
,
'0'
);
const
now
=
new
Date
();
const
updateTime
=
`
${
now
.
getFullYear
()}
-
${
pad
(
now
.
getMonth
()
+
1
)}
-
${
pad
(
now
.
getDate
())}
${
pad
(
now
.
getHours
())}
:
${
pad
(
now
.
getMinutes
())}
:
${
pad
(
now
.
getSeconds
())}
`
;
if
(
devices
.
length
>
0
)
{
// 查区域信息
const
regionIdSet
=
[...
new
Set
(
devices
.
map
((
d
:
any
)
=>
d
.
region_key
).
filter
(
Boolean
))];
let
regionMap
:
Map
<
string
,
any
>
=
new
Map
();
if
(
regionIdSet
.
length
)
{
const
regionRes
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{
id
:
{
"%in%"
:
regionIdSet
}
},
[
"id"
,
"groups"
,
"type"
,
"name"
]
);
(
regionRes
.
data
as
any
[]).
forEach
((
r
:
any
)
=>
regionMap
.
set
(
String
(
r
.
id
),
r
));
}
// 逐台调飞奕,收集符合条件的
for
(
const
device
of
devices
)
{
let
feiyiRow
:
any
=
{};
try
{
const
result
=
await
getIndoorUnitList
({
indoorUnitAddressFull
:
device
.
device_id
,
page
:
1
,
limit
:
1
});
feiyiRow
=
result
.
rows
[
0
]
||
{};
}
catch
(
e
)
{
console
.
error
(
`[getDeviceList] 获取设备
${
device
.
device_id
}
飞奕数据失败:`
,
e
);
}
// 飞奕筛选
if
(
targetOnOff
!==
undefined
&&
feiyiRow
.
onOff
!==
targetOnOff
)
continue
;
if
(
targetMode
!==
undefined
&&
feiyiRow
.
workMode
!==
targetMode
)
continue
;
const
region
=
regionMap
.
get
(
String
(
device
.
region_key
));
filteredRows
.
push
({
deviceId
:
device
.
device_id
,
regionName
:
region
?
`
${
region
.
groups
||
''
}
_
${
region
.
type
||
''
}
_
${
region
.
name
||
''
}
`
:
''
,
power
:
feiyiRow
.
onOff
===
1
?
'开'
:
'关'
,
setTemp
:
feiyiRow
.
tempSet
??
''
,
fanSpeed
:
fanSpeedMap
[
feiyiRow
.
fanSpeed
]
||
String
(
feiyiRow
.
fanSpeed
||
''
),
deviceMode
:
workModeMap
[
feiyiRow
.
workMode
]
||
String
(
feiyiRow
.
workMode
||
''
),
state
:
deviceStateMap
[
device
.
device_state
]
??
String
(
device
.
device_state
??
''
),
linkageStart
:
device
.
linkage_start
??
'on'
,
updateTime
,
});
}
}
const
total
=
filteredRows
.
length
;
const
startIdx
=
(
pageNumber
-
1
)
*
pageSize
;
const
rows
=
filteredRows
.
slice
(
startIdx
,
startIdx
+
pageSize
);
return
{
total
,
rows
};
}
else
{
// ==== 无飞奕筛选:DB 层分页(原逻辑) ====
// 查询总数
const
countRes
=
await
selectDataCountByParam
(
TABLENAME
.
设备表
,
paramAnys
);
const
total
=
countRes
.
data
as
number
;
if
(
total
===
0
)
{
return
{
total
:
0
,
rows
:
[]
};
}
// 分页查询设备
const
deviceRes
=
await
selectDataListToPageByParam
(
TABLENAME
.
设备表
,
paramAnys
,
[
"device_id"
,
"region_key"
,
"device_state"
,
"linkage_start"
],
pageNumber
,
pageSize
);
const
devices
=
deviceRes
.
data
as
any
[];
if
(
!
devices
.
length
)
{
return
{
total
:
0
,
rows
:
[]
};
}
// 查询这批设备对应的区域信息(region_key 对应 region.id)
const
regionIdSet
=
[...
new
Set
(
devices
.
map
((
d
:
any
)
=>
d
.
region_key
).
filter
(
Boolean
))];
let
regionMap
:
Map
<
string
,
any
>
=
new
Map
();
if
(
regionIdSet
.
length
)
{
const
regionRes
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{
id
:
{
"%in%"
:
regionIdSet
}
},
[
"id"
,
"groups"
,
"type"
,
"name"
]
);
(
regionRes
.
data
as
any
[]).
forEach
((
r
:
any
)
=>
regionMap
.
set
(
String
(
r
.
id
),
r
));
}
// 逐台串行调用飞奕接口获取实时数据
const
feiyiResults
:
any
[]
=
[];
for
(
const
device
of
devices
)
{
try
{
const
result
=
await
getIndoorUnitList
({
indoorUnitAddressFull
:
device
.
device_id
,
page
:
1
,
limit
:
1
});
feiyiResults
.
push
(
result
.
rows
[
0
]
||
{});
}
catch
(
e
)
{
console
.
error
(
`[getDeviceList] 获取设备
${
device
.
device_id
}
飞奕数据失败:`
,
e
);
feiyiResults
.
push
({
});
}
}
// 组装返回数据
const
fanSpeedMap
:
Record
<
number
,
string
>
=
{
1
:
'高风'
,
2
:
'中风'
,
4
:
'低风'
};
const
workModeMap
:
Record
<
number
,
string
>
=
{
1
:
'制冷'
,
2
:
'制热'
,
3
:
'送风'
,
4
:
'除湿'
};
const
deviceStateMap
:
Record
<
number
,
string
>
=
{
0
:
'正常'
,
1
:
'硬件故障'
,
2
:
'通信故障'
,
3
:
'离线'
};
const
pad
=
(
n
:
number
)
=>
String
(
n
).
padStart
(
2
,
'0'
);
const
now
=
new
Date
();
const
updateTime
=
`
${
now
.
getFullYear
()}
-
${
pad
(
now
.
getMonth
()
+
1
)}
-
${
pad
(
now
.
getDate
())}
${
pad
(
now
.
getHours
())}
:
${
pad
(
now
.
getMinutes
())}
:
${
pad
(
now
.
getSeconds
())}
`
;
const
rows
=
devices
.
map
((
device
:
any
,
index
:
number
)
=>
{
const
region
=
regionMap
.
get
(
String
(
device
.
region_key
));
const
row
=
feiyiResults
[
index
]
||
{};
return
{
deviceId
:
device
.
device_id
,
regionName
:
region
?
`
${
region
.
groups
||
''
}
_
${
region
.
type
||
''
}
_
${
region
.
name
||
''
}
`
:
''
,
power
:
row
.
onOff
===
1
?
'开'
:
'关'
,
setTemp
:
row
.
tempSet
??
''
,
fanSpeed
:
fanSpeedMap
[
row
.
fanSpeed
]
||
String
(
row
.
fanSpeed
||
''
),
deviceMode
:
workModeMap
[
row
.
workMode
]
||
String
(
row
.
workMode
||
''
),
state
:
deviceStateMap
[
device
.
device_state
]
??
String
(
device
.
device_state
??
''
),
linkageStart
:
device
.
linkage_start
??
'on'
,
updateTime
,
};
});
return
{
total
,
rows
};
}
}
/**
* 设备管理 - 编辑空调控制参数(调用飞奕接口 + 记录日志)
* @returns { success: boolean, message: string }
*/
export
async
function
editAcDeviceControl
(
params
:
{
deviceId
:
string
;
power
:
string
;
setTemp
?:
number
;
fanSpeed
?:
string
;
deviceMode
?:
string
;
}):
Promise
<
{
success
:
boolean
;
message
:
string
}
>
{
const
{
deviceId
,
power
,
setTemp
,
fanSpeed
,
deviceMode
}
=
params
;
// 1. 校验设备是否存在
let
device
=
await
selectOneDataByParam
(
TABLENAME
.
设备表
,
{
device_id
:
deviceId
},
[
"id"
,
"device_id"
,
"device_name"
]
);
if
(
!
device
.
data
||
!
device
.
data
.
id
)
{
throw
new
BizError
(
ERRORENUM
.
参数错误
,
`设备
${
deviceId
}
未注册`
);
}
// 2. 构建飞奕控制参数
let
acParams
:
any
=
{
"indoorUnitAddressFull"
:
[
deviceId
],
"onOff"
:
acDeviceKeyMap
[
power
]
??
1
,
"openApiAction"
:
"control"
,
};
if
(
setTemp
!=
null
)
acParams
.
tempSet
=
setTemp
;
if
(
fanSpeed
)
acParams
.
fanSpeed
=
acDeviceKeyMap
[
fanSpeed
];
if
(
deviceMode
)
acParams
.
workMode
=
acDeviceKeyMap
[
deviceMode
];
// 3. 调用飞奕控制接口
const
result
=
await
controlIndoorUnit
(
acParams
);
// 4. 记录操作日志
const
now
=
new
Date
();
const
logEntry
=
{
device_id
:
deviceId
,
device_type
:
'空调'
,
operation_content
:
{
option
:
'设备管理编辑'
,
device_id
:
deviceId
,
time
:
formatLocalTime
(
now
),
power
:
power
,
setTemp
:
setTemp
,
fanSpeed
:
fanSpeed
,
deviceMode
:
deviceMode
,
success
:
result
.
success
,
message
:
result
.
data
||
(
result
.
success
?
'控制成功'
:
'控制失败'
),
},
status
:
result
.
success
?
1
:
0
,
created_at
:
now
,
updated_at
:
now
,
};
await
addData
(
TABLENAME
.
设备日志表
,
[
logEntry
]);
if
(
result
.
success
)
{
return
{
success
:
true
,
message
:
'控制成功'
};
}
else
{
throw
new
BizError
(
ERRORENUM
.
操作失败
,
result
.
data
||
'控制空调设备失败'
);
}
}
/**
* 设备管理 - 批量编辑设备
* 根据筛选条件查询符合条件的设备,批量修改 power / setTemp
* @returns { total, successCount, failCount, details }
*/
export
async
function
batchEditDevices
(
params
:
{
regionKeys
:
number
[];
deviceName
?:
string
;
power
?:
string
;
state
?:
string
;
deviceMode
?:
string
;
startDate
?:
string
;
endDate
?:
string
;
edits
:
{
power
?:
string
;
setTemp
?:
number
;
linkageStart
?:
string
;
};
}):
Promise
<
{
total
:
number
;
successCount
:
number
;
failCount
:
number
;
details
:
any
[]
}
>
{
const
{
regionKeys
,
deviceName
,
power
,
state
,
deviceMode
,
startDate
,
endDate
,
edits
}
=
params
;
// 校验必填参数
if
(
!
regionKeys
||
!
Array
.
isArray
(
regionKeys
)
||
regionKeys
.
length
===
0
)
{
throw
new
BizError
(
ERRORENUM
.
参数错误
,
"regionKeys 不能为空"
);
}
if
(
!
edits
||
typeof
edits
!==
'object'
)
{
throw
new
BizError
(
ERRORENUM
.
参数错误
,
"edits 不能为空"
);
}
const
hasEdit
=
edits
.
power
!==
undefined
||
edits
.
setTemp
!==
undefined
||
edits
.
linkageStart
!==
undefined
;
if
(
!
hasEdit
)
{
throw
new
BizError
(
ERRORENUM
.
参数错误
,
"edits 中至少需要 power / setTemp / linkageStart 其中之一"
);
}
// 1. 构建设备查询条件
let
whereParam
:
any
=
{
device_type
:
'空调'
};
whereParam
.
region_key
=
{
"%in%"
:
regionKeys
};
if
(
deviceName
)
{
whereParam
.
device_name
=
{
"%like%"
:
deviceName
};
}
// state 筛选(正常/告警/离线 → device_state)
const
stateMap
:
Record
<
string
,
number
[]
>
=
{
'正常'
:
[
0
],
'告警'
:
[
1
,
2
],
'离线'
:
[
3
],
};
if
(
state
&&
stateMap
[
state
])
{
whereParam
.
device_state
=
{
"%in%"
:
stateMap
[
state
]
};
}
// 日期范围筛选
if
(
startDate
)
{
whereParam
.
created_at
=
{
...(
whereParam
.
created_at
||
{}),
"%gte%"
:
startDate
+
' 00:00:00'
};
}
if
(
endDate
)
{
whereParam
.
created_at
=
{
...(
whereParam
.
created_at
||
{}),
"%lte%"
:
endDate
+
' 23:59:59'
};
}
// 2. 查询符合条件的设备
const
deviceRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
whereParam
,
[
"id"
,
"device_id"
,
"region_key"
,
"device_name"
,
"device_state"
,
"created_at"
]
);
let
devices
=
(
deviceRes
.
data
||
[])
as
any
[];
// 3. 飞奕后置过滤:power / deviceMode 需要调飞奕接口逐台确认
const
powerMap
:
Record
<
string
,
number
>
=
{
'开'
:
1
,
'关'
:
0
};
const
modeMap
:
Record
<
string
,
number
>
=
{
'制冷'
:
1
,
'制热'
:
2
,
'通风'
:
3
,
'除湿'
:
4
,
'自动'
:
0
};
const
needFeiyiFilter
=
!!
(
power
||
deviceMode
);
const
targetOnOff
=
power
?
powerMap
[
power
]
:
undefined
;
const
targetMode
=
deviceMode
?
modeMap
[
deviceMode
]
:
undefined
;
if
(
needFeiyiFilter
&&
devices
.
length
>
0
)
{
let
filteredDevices
:
any
[]
=
[];
for
(
const
dev
of
devices
)
{
try
{
const
feiyiRes
=
await
getIndoorUnitList
({
indoorUnitAddressFull
:
dev
.
device_id
,
page
:
1
,
limit
:
1
});
const
row
=
(
feiyiRes
&&
feiyiRes
.
rows
&&
feiyiRes
.
rows
[
0
])
?
feiyiRes
.
rows
[
0
]
:
{};
if
(
targetOnOff
!==
undefined
&&
row
.
onOff
!==
targetOnOff
)
continue
;
if
(
targetMode
!==
undefined
&&
row
.
workMode
!==
targetMode
)
continue
;
filteredDevices
.
push
(
dev
);
}
catch
(
e
)
{
console
.
error
(
`[batchEditDevices] 获取设备
${
dev
.
device_id
}
飞奕数据失败:`
,
e
);
// 无飞奕数据时默认跳过
}
}
devices
=
filteredDevices
;
}
const
total
=
devices
.
length
;
if
(
total
===
0
)
{
return
{
total
:
0
,
successCount
:
0
,
failCount
:
0
,
details
:
[]
};
}
const
details
:
any
[]
=
[];
let
successCount
=
0
;
let
failCount
=
0
;
// 4. 处理 linkageStart 编辑(纯数据库更新,一次性批量更新)
if
(
edits
.
linkageStart
!==
undefined
)
{
try
{
const
deviceIds
=
devices
.
map
(
d
=>
d
.
device_id
);
await
updateManyData
(
TABLENAME
.
设备表
,
{
device_id
:
{
"%in%"
:
deviceIds
}
},
{
linkage_start
:
edits
.
linkageStart
});
console
.
log
(
`[batchEditDevices] 批量设置联动为
${
edits
.
linkageStart
}
,涉及
${
deviceIds
.
length
}
台设备`
);
}
catch
(
e
:
any
)
{
console
.
error
(
'[batchEditDevices] 批量更新 linkage_start 失败:'
,
e
);
throw
new
BizError
(
ERRORENUM
.
操作失败
,
`批量更新联动状态失败:
${
e
.
message
}
`
);
}
}
// 5. 处理 power / setTemp 编辑(逐台调用飞奕接口)
const
needAcControl
=
edits
.
power
!==
undefined
||
edits
.
setTemp
!==
undefined
;
if
(
needAcControl
)
{
const
now
=
new
Date
();
for
(
const
dev
of
devices
)
{
try
{
let
acParams
:
any
=
{
"indoorUnitAddressFull"
:
[
dev
.
device_id
],
"openApiAction"
:
"control"
,
};
if
(
edits
.
power
!==
undefined
)
{
acParams
.
onOff
=
powerMap
[
edits
.
power
]
??
1
;
}
if
(
edits
.
setTemp
!==
undefined
)
{
acParams
.
tempSet
=
edits
.
setTemp
;
}
const
result
=
await
controlIndoorUnit
(
acParams
);
// 记录操作日志
const
logEntry
=
{
device_id
:
dev
.
device_id
,
device_type
:
'空调'
,
operation_content
:
{
option
:
'批量编辑'
,
device_id
:
dev
.
device_id
,
time
:
formatLocalTime
(
now
),
power
:
edits
.
power
,
setTemp
:
edits
.
setTemp
,
linkageStart
:
edits
.
linkageStart
,
success
:
result
.
success
,
message
:
result
.
data
||
(
result
.
success
?
'控制成功'
:
'控制失败'
),
},
status
:
result
.
success
?
1
:
0
,
created_at
:
now
,
updated_at
:
now
,
};
await
addData
(
TABLENAME
.
设备日志表
,
[
logEntry
]);
if
(
result
.
success
)
{
successCount
++
;
details
.
push
({
deviceId
:
dev
.
device_id
,
success
:
true
});
}
else
{
failCount
++
;
details
.
push
({
deviceId
:
dev
.
device_id
,
success
:
false
,
message
:
result
.
data
||
'控制失败'
});
}
}
catch
(
e
:
any
)
{
failCount
++
;
details
.
push
({
deviceId
:
dev
.
device_id
,
success
:
false
,
message
:
e
.
message
||
'未知错误'
});
console
.
error
(
`[batchEditDevices] 控制设备
${
dev
.
device_id
}
失败:`
,
e
);
}
}
}
else
{
// 仅 linkageStart 修改,全部成功
successCount
=
total
;
for
(
const
dev
of
devices
)
{
details
.
push
({
deviceId
:
dev
.
device_id
,
success
:
true
});
}
}
return
{
total
,
successCount
,
failCount
,
details
};
}
// ======= 设备表本地信息编辑(不涉及飞奕API) =======
/**
* 编辑单台设备的表信息
* @param params deviceId + 可编辑字段(至少一个)
*/
export
async
function
editLocalDevice
(
params
:
{
deviceId
:
string
;
deviceName
?:
string
;
regionKey
?:
number
;
deviceType
?:
string
;
deviceState
?:
string
;
linkageStart
?:
string
;
})
{
const
{
deviceId
,
deviceName
,
regionKey
,
deviceType
,
deviceState
,
linkageStart
}
=
params
;
// 校验设备存在
const
deviceRes
=
await
selectOneDataByParam
(
TABLENAME
.
设备表
,
{
device_id
:
deviceId
});
if
(
!
deviceRes
.
data
)
{
throw
new
BizError
(
ERRORENUM
.
参数错误
,
`设备
${
deviceId
}
不存在`
);
}
// 构建更新字段(驼峰 → 数据库字段)
const
dbEdits
:
any
=
{};
if
(
deviceName
!==
undefined
)
dbEdits
.
device_name
=
deviceName
;
if
(
regionKey
!==
undefined
)
dbEdits
.
region_key
=
regionKey
;
if
(
deviceType
!==
undefined
)
dbEdits
.
device_type
=
deviceType
;
if
(
deviceState
!==
undefined
)
dbEdits
.
device_state
=
deviceState
;
if
(
linkageStart
!==
undefined
)
dbEdits
.
linkage_start
=
linkageStart
;
if
(
Object
.
keys
(
dbEdits
).
length
===
0
)
{
throw
new
BizError
(
ERRORENUM
.
参数错误
,
"至少需要提供一个编辑字段"
);
}
await
updateManyData
(
TABLENAME
.
设备表
,
{
device_id
:
deviceId
},
dbEdits
);
console
.
log
(
`[editLocalDevice] 更新设备
${
deviceId
}
:`
,
dbEdits
);
return
{
isSuccess
:
true
,
deviceId
};
}
/**
* 批量编辑设备表信息
* @param params regionKeys/deviceName 筛选条件 + edits 编辑内容
*/
export
async
function
batchEditLocalDevices
(
params
:
{
regionKeys
?:
string
[];
deviceName
?:
string
;
edits
:
{
deviceName
?:
string
;
regionKey
?:
number
;
deviceType
?:
string
;
controlParams
?:
string
;
deviceState
?:
string
;
};
})
{
const
{
regionKeys
,
deviceName
,
edits
}
=
params
;
// 构建查询条件
const
where
:
any
=
{};
if
(
regionKeys
&&
regionKeys
.
length
>
0
)
{
where
.
region_key
=
{
"%in%"
:
regionKeys
};
}
if
(
deviceName
)
{
where
.
device_name
=
{
"%like%"
:
deviceName
};
}
if
(
Object
.
keys
(
where
).
length
===
0
)
{
throw
new
BizError
(
ERRORENUM
.
参数错误
,
"至少需要提供 regionKeys 或 deviceName 筛选条件"
);
}
// 构建更新字段
const
dbEdits
:
any
=
{};
if
(
edits
.
deviceName
!==
undefined
)
dbEdits
.
device_name
=
edits
.
deviceName
;
if
(
edits
.
regionKey
!==
undefined
)
dbEdits
.
region_key
=
edits
.
regionKey
;
if
(
edits
.
deviceType
!==
undefined
)
dbEdits
.
device_type
=
edits
.
deviceType
;
if
(
edits
.
controlParams
!==
undefined
)
dbEdits
.
control_params
=
edits
.
controlParams
;
if
(
edits
.
deviceState
!==
undefined
)
dbEdits
.
device_state
=
edits
.
deviceState
;
if
(
Object
.
keys
(
dbEdits
).
length
===
0
)
{
throw
new
BizError
(
ERRORENUM
.
参数错误
,
"edits 中至少需要一个编辑字段"
);
}
await
updateManyData
(
TABLENAME
.
设备表
,
where
,
dbEdits
);
console
.
log
(
`[batchEditLocalDevices] 批量更新,条件:`
,
where
,
"更新内容:"
,
dbEdits
);
return
{
isSuccess
:
true
};
}
\ No newline at end of file
src/biz/feiyiClient.ts
View file @
77a71622
...
...
@@ -7,6 +7,8 @@ import * as crypto from 'crypto';
import
{
post
,
get
}
from
'../util/request'
;
import
{
BizError
}
from
'../util/bizError'
;
import
{
ERRORENUM
}
from
'../config/errorEnum'
;
import
{
addData
}
from
'../data/addData'
;
import
{
TABLENAME
}
from
'../config/dbEnum'
;
// 全局变量缓存 token 和过期时间
let
feiYiToken
=
""
;
...
...
@@ -124,6 +126,20 @@ export async function getFeiYiToken(): Promise<string> {
}
/**
* 接口路径 -> 中文描述映射
*/
const
PATH_DESC_MAP
:
Record
<
string
,
string
>
=
{
'/openApi/gw/page'
:
'查询网关设备列表'
,
'/openApi/indoorUnit/list'
:
'查询内机列表'
,
'/openApi/indoorUnit/control'
:
'控制内机开关'
,
'/openApi/building/tree'
:
'查询建筑物树结构'
,
'/openApi/electricMeter/getAll'
:
'查询电表列表'
,
'/openApi/electricMeterState/list'
:
'查询抄表记录'
,
'/openApi/indoorUnit/event/switch'
:
'查询内机开关机记录'
,
'/openApi/indoorUnit/alarm/errorHis'
:
'查询内机故障记录'
,
};
/**
* 带 Token 的 POST 请求封装
* 自动获取/刷新 token
* @param path 接口路径(相对路径)
...
...
@@ -131,32 +147,96 @@ export async function getFeiYiToken(): Promise<string> {
* @param retry 是否重试(用于 token 过期重试)
*/
async
function
feiYiPost
(
path
:
string
,
body
:
any
,
retry
:
boolean
=
true
):
Promise
<
any
>
{
const
url
=
`
${
FEIYI_CONFIG
.
baseUrl
}${
path
}
`
;
const
requestTime
=
new
Date
();
// 获取 token
const
token
=
await
getFeiYiToken
();
const
url
=
`
${
FEIYI_CONFIG
.
baseUrl
}${
path
}
`
;
const
headers
=
{
'Authorization'
:
`Bearer
${
token
}
`
,
'Content-Type'
:
'application/json'
,
};
let
result
:
any
;
let
responseStatus
:
string
;
let
responseTime
:
Date
;
let
dbStatus
:
number
;
try
{
result
=
await
post
(
url
,
body
,
headers
);
responseTime
=
new
Date
();
console
.
log
(
`请求
${
path
}
成功,响应:`
,
result
?.
code
);
if
(
result
.
code
===
'00000'
)
{
dbStatus
=
1
;
responseStatus
=
'正常'
;
}
else
{
dbStatus
=
0
;
responseStatus
=
'请求报错'
;
}
}
catch
(
err
)
{
responseTime
=
new
Date
();
dbStatus
=
0
;
// 区分连接超时和其他网络错误
if
(
err
.
code
===
'ETIMEDOUT'
||
err
.
code
===
'ECONNABORTED'
||
err
.
message
?.
includes
(
'timeout'
))
{
responseStatus
=
'连接超时'
;
}
else
if
(
err
.
code
===
'ECONNREFUSED'
||
err
.
code
===
'ENOTFOUND'
)
{
responseStatus
=
'连接失败'
;
}
else
{
responseStatus
=
'请求报错'
;
}
// 记录日志(网络异常,无响应数据)
addData
(
TABLENAME
.
数据接入日志表
,
{
partner_name
:
'飞奕'
,
request_url
:
url
,
request_desc
:
PATH_DESC_MAP
[
path
]
||
''
,
request_params
:
body
,
response_data
:
null
,
status
:
dbStatus
,
response_status
:
responseStatus
,
request_time
:
requestTime
,
response_time
:
responseTime
,
}).
catch
(
e
=>
console
.
error
(
'写入数据接入日志失败:'
,
e
));
throw
new
BizError
(
ERRORENUM
.
网络错误
,
`请求
${
path
}
失败:
${
err
.
message
}
`
);
}
// 如果 token 过期(示例错误码可能为 token 过期,文档未明确,假设 code 为 401 或其他)
// 这里简单判断如果 code 不为 '00000' 且包含 token 相关错误,则重试一次
// 如果 token 过期,重试一次
if
(
result
.
code
!==
'00000'
)
{
if
(
retry
&&
(
result
.
code
===
'401'
||
result
.
message
?.
includes
(
'token'
)))
{
// 可选:清除 token 缓存,重新获取
return
feiYiPost
(
path
,
body
,
false
);
}
throw
new
BizError
(
ERRORENUM
.
第三方接口错误
,
`接口
${
path
}
调用失败:
${
result
.
message
}
`
);
// || JSON.stringify(result)
// 非 token 错误,记录日志后抛异常
addData
(
TABLENAME
.
数据接入日志表
,
{
partner_name
:
'飞奕'
,
request_url
:
url
,
request_desc
:
PATH_DESC_MAP
[
path
]
||
''
,
request_params
:
body
,
response_data
:
result
,
status
:
dbStatus
,
response_status
:
responseStatus
,
request_time
:
requestTime
,
response_time
:
responseTime
,
}).
catch
(
e
=>
console
.
error
(
'写入数据接入日志失败:'
,
e
));
throw
new
BizError
(
ERRORENUM
.
第三方接口错误
,
`接口
${
path
}
调用失败:
${
result
.
message
}
`
);
}
// 成功也记录日志
addData
(
TABLENAME
.
数据接入日志表
,
{
partner_name
:
'飞奕'
,
request_url
:
url
,
request_desc
:
PATH_DESC_MAP
[
path
]
||
''
,
request_params
:
body
,
response_data
:
result
,
status
:
dbStatus
,
response_status
:
responseStatus
,
request_time
:
requestTime
,
response_time
:
responseTime
,
}).
catch
(
e
=>
console
.
error
(
'写入数据接入日志失败:'
,
e
));
return
result
;
}
...
...
src/biz/mqttClient.ts
View file @
77a71622
...
...
@@ -2,6 +2,7 @@ import { TABLENAME } from "../config/dbEnum";
import
{
addData
}
from
"../data/addData"
;
import
{
selectOneDataByParam
}
from
"../data/findData"
;
import
{
controlAcByEnvironment
}
from
"./running"
;
import
{
systemConfig
}
from
"../config/serverConfig"
;
/**
* 处理接收到的环境设备数据,并存入数据库
...
...
@@ -26,14 +27,32 @@ import { controlAcByEnvironment } from "./running";
* }
*/
export
async
function
processDeviceData
(
message
)
{
const
requestTime
=
new
Date
();
const
msgStr
=
message
.
toString
();
const
topic
=
systemConfig
.
mqttsrv
.
topic_env
;
const
writeLog
=
(
status
:
number
,
responseStatus
:
string
,
responseData
?:
any
)
=>
{
addData
(
TABLENAME
.
数据接入日志表
,
{
partner_name
:
'星纵物联'
,
request_url
:
topic
,
request_desc
:
'环境监测数据推送'
,
request_params
:
msgStr
,
response_data
:
responseData
??
null
,
status
,
response_status
:
responseStatus
,
request_time
:
requestTime
,
response_time
:
new
Date
(),
}).
catch
(
e
=>
console
.
error
(
'写入数据接入日志失败:'
,
e
));
};
try
{
// 解析JSON消息
const
msgStr
=
message
.
toString
();
let
data
:
any
;
try
{
data
=
JSON
.
parse
(
msgStr
);
}
catch
(
e
)
{
console
.
error
(
'消息不是合法JSON,跳过:'
,
msgStr
);
writeLog
(
0
,
'消息格式错误'
);
return
;
}
console
.
log
(
'收到环境数据:'
,
data
.
length
);
...
...
@@ -42,6 +61,7 @@ export async function processDeviceData(message) {
const
devEUI
=
data
.
devEUI
?
data
.
devEUI
.
toUpperCase
()
:
null
;
if
(
!
devEUI
)
{
console
.
warn
(
'消息缺少devEUI字段,跳过'
);
writeLog
(
0
,
'缺少devEUI'
);
return
;
}
const
gatewayTime
=
data
.
gatewayTime
?
new
Date
(
data
.
gatewayTime
)
:
new
Date
();
...
...
@@ -59,6 +79,7 @@ export async function processDeviceData(message) {
);
if
(
!
deviceRow
&&
!
deviceRow
.
data
)
{
console
.
warn
(
`设备不存在,跳过入库:
${
devEUI
}
`
);
writeLog
(
0
,
'设备不存在'
);
return
;
}
...
...
@@ -88,6 +109,7 @@ export async function processDeviceData(message) {
);
console
.
log
(
`数据入库成功: 设备
${
devEUI
}
`
);
writeLog
(
1
,
'正常'
);
// 晚上 23:00 ~ 次日 05:00 不执行空调自控(休眠时段)
const
currentHour
=
new
Date
().
getHours
();
...
...
@@ -114,6 +136,7 @@ export async function processDeviceData(message) {
}
catch
(
error
)
{
console
.
error
(
'数据处理失败:'
,
error
);
writeLog
(
0
,
'数据处理异常'
);
}
}
...
...
@@ -143,14 +166,32 @@ interface CustomerTriggerItem {
* // region_trigger_data: { region_count_data: [ { region: 1, region_name: 'Region1', region_uuid: '...', total: { current_total: 9 } } ] }
*/
export
async
function
customerDeviceData
(
message
)
{
const
requestTime
=
new
Date
();
const
msgStr
=
message
.
toString
();
const
topic
=
systemConfig
.
mqttsrv
.
topic_crowd
;
const
writeLog
=
(
status
:
number
,
responseStatus
:
string
,
responseData
?:
any
)
=>
{
addData
(
TABLENAME
.
数据接入日志表
,
{
partner_name
:
'星纵物联'
,
request_url
:
topic
,
request_desc
:
'客流数据推送'
,
request_params
:
msgStr
,
response_data
:
responseData
??
null
,
status
,
response_status
:
responseStatus
,
request_time
:
requestTime
,
response_time
:
new
Date
(),
}).
catch
(
e
=>
console
.
error
(
'写入数据接入日志失败:'
,
e
));
};
try
{
// 解析JSON消息
const
msgStr
=
message
.
toString
();
let
data
:
any
;
try
{
data
=
JSON
.
parse
(
msgStr
);
}
catch
(
e
)
{
console
.
error
(
'消息不是合法JSON,跳过:'
,
msgStr
);
writeLog
(
0
,
'消息格式错误'
);
return
;
}
console
.
log
(
'收到客流数据:'
,
msgStr
);
...
...
@@ -159,11 +200,13 @@ export async function customerDeviceData(message) {
const
deviceInfo
=
data
.
device_info
;
if
(
!
deviceInfo
)
{
console
.
warn
(
'消息缺少device_info字段,跳过'
);
writeLog
(
0
,
'缺少device_info'
);
return
;
}
const
deviceSn
=
deviceInfo
.
device_sn
?
deviceInfo
.
device_sn
.
toUpperCase
()
:
null
;
// 设备序列号,转大写作为设备标识
if
(
!
deviceSn
)
{
console
.
warn
(
'消息缺少device_info.device_sn字段,跳过'
);
writeLog
(
0
,
'缺少device_sn'
);
return
;
}
const
deviceMac
=
deviceInfo
.
device_mac
;
...
...
@@ -206,6 +249,7 @@ export async function customerDeviceData(message) {
if
(
triggerItems
.
length
===
0
)
{
console
.
warn
(
'消息缺少客流线路数据,跳过'
);
writeLog
(
0
,
'缺少客流线路数据'
);
return
;
}
...
...
@@ -254,9 +298,11 @@ export async function customerDeviceData(message) {
}
console
.
log
(
`客流数据处理完成: 设备
${
deviceSn
}
, 共入库
${
triggerItems
.
length
}
条数据`
);
writeLog
(
1
,
'正常'
);
}
catch
(
error
)
{
console
.
error
(
'客流数据处理失败:'
,
error
);
writeLog
(
0
,
'数据处理异常'
);
}
}
...
...
src/biz/report.ts
View file @
77a71622
...
...
@@ -219,7 +219,13 @@ function calcMeterDeviceDaily(records: any[]): number {
* @param weekOffset 0=最近七天(默认), -1=再往前七天, ...
*/
export
async
function
getWeeklyReportData
(
weekOffset
:
number
=
0
):
Promise
<
any
>
{
const
{
start
:
monday
,
end
:
sunday
,
timer
}
=
calcWeekRange
(
weekOffset
);
// ==== 固定查询 7月29日 ~ 8月4日 ====
// const { start: monday, end: sunday, timer } = calcWeekRange(weekOffset);
const
start
=
new
Date
(
2026
,
6
,
29
,
0
,
0
,
0
,
0
);
// 7月29日 00:00
const
end
=
new
Date
(
2026
,
7
,
4
,
23
,
59
,
59
,
999
);
// 8月4日 23:59
const
monday
=
start
;
const
sunday
=
end
;
const
timer
=
`
${
formatChineseDate
(
monday
)}
—
${
formatChineseDate
(
sunday
)}
`
;
console
.
log
(
`[周报告JSON] 查询数据:
${
timer
}
`
);
// ===== 第一步:并行查询区域 + 各馆设备分组 =====
...
...
@@ -394,12 +400,15 @@ const ENV_INDICATOR_KEYS = [
*/
export
async
function
getEnvIndicatorsWeeklyAvg
():
Promise
<
any
>
{
// 1. 计算七天时间范围
const
now
=
new
Date
();
const
end
=
new
Date
(
now
);
end
.
setHours
(
23
,
59
,
59
,
999
);
const
start
=
new
Date
(
now
);
start
.
setDate
(
now
.
getDate
()
-
6
);
start
.
setHours
(
0
,
0
,
0
,
0
);
// ==== 固定查询 7月29日 ~ 8月4日 ====
// const now = new Date();
// const end = new Date(now);
// end.setHours(23, 59, 59, 999);
// const start = new Date(now);
// start.setDate(now.getDate() - 6);
// start.setHours(0, 0, 0, 0);
const
start
=
new
Date
(
2026
,
6
,
29
,
0
,
0
,
0
,
0
);
// 7月29日 00:00
const
end
=
new
Date
(
2026
,
7
,
4
,
23
,
59
,
59
,
999
);
// 8月4日 23:59
const
from
=
formatLocalTime
(
start
);
const
to
=
formatLocalTime
(
end
);
...
...
@@ -486,12 +495,15 @@ const LEVEL_MAP: { [key: number]: string } = { 1: "一级", 2: "二级", 3: "三
* 每组展示:故障代码、风险内容、报警时间、处理状态、报警设备
*/
export
async
function
getFaultInfoLast7Days
():
Promise
<
any
>
{
const
now
=
new
Date
();
const
end
=
new
Date
(
now
);
end
.
setHours
(
23
,
59
,
59
,
999
);
const
start
=
new
Date
(
now
);
start
.
setDate
(
now
.
getDate
()
-
6
);
start
.
setHours
(
0
,
0
,
0
,
0
);
// ==== 固定查询 7月29日 ~ 8月4日 ====
// const now = new Date();
// const end = new Date(now);
// end.setHours(23, 59, 59, 999);
// const start = new Date(now);
// start.setDate(now.getDate() - 6);
// start.setHours(0, 0, 0, 0);
const
start
=
new
Date
(
2026
,
6
,
29
,
0
,
0
,
0
,
0
);
// 7月29日 00:00
const
end
=
new
Date
(
2026
,
7
,
4
,
23
,
59
,
59
,
999
);
// 8月4日 23:59
const
from
=
formatLocalTime
(
start
);
const
to
=
formatLocalTime
(
end
);
...
...
src/biz/running.ts
View file @
77a71622
...
...
@@ -27,7 +27,7 @@ const envControlCache: Map<string, { tempSet?: number; workMode?: number; fanSpe
* 将 Date 对象格式化为本地时间字符串 'YYYY-MM-DD HH:mm:ss'
* 替代 toISOString() 避免 UTC 时区偏移问题
*/
function
formatLocalTime
(
date
:
Date
):
string
{
export
function
formatLocalTime
(
date
:
Date
):
string
{
const
pad
=
(
n
:
number
)
=>
String
(
n
).
padStart
(
2
,
'0'
);
return
`
${
date
.
getFullYear
()}
-
${
pad
(
date
.
getMonth
()
+
1
)}
-
${
pad
(
date
.
getDate
())}
${
pad
(
date
.
getHours
())}
:
${
pad
(
date
.
getMinutes
())}
:
${
pad
(
date
.
getSeconds
())}
`
;
}
...
...
@@ -68,6 +68,21 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
let
currentMonthStart
=
new
Date
();
currentMonthStart
.
setDate
(
1
);
currentMonthStart
.
setHours
(
0
,
0
,
0
,
0
);
let
lastMonthStart
=
new
Date
();
lastMonthStart
.
setMonth
(
lastMonthStart
.
getMonth
()
-
1
);
lastMonthStart
.
setDate
(
1
);
lastMonthStart
.
setHours
(
0
,
0
,
0
,
0
);
let
lastMonthEnd
=
new
Date
(
currentMonthStart
.
getTime
()
-
1
);
// 昨日同时段结束时间(日同比窗口)
let
yesterdaySameTimeEnd
=
new
Date
(
yesterdayStart
);
yesterdaySameTimeEnd
.
setHours
(
nowTime
.
getHours
(),
nowTime
.
getMinutes
(),
nowTime
.
getSeconds
(),
nowTime
.
getMilliseconds
());
// 上月同比截止日(处理月份天数差异)
let
todayDay
=
nowTime
.
getDate
();
let
thisMonthMaxDay
=
new
Date
(
nowTime
.
getFullYear
(),
nowTime
.
getMonth
()
+
1
,
0
).
getDate
();
let
lastMonthMaxDay
=
new
Date
(
nowTime
.
getFullYear
(),
nowTime
.
getMonth
(),
0
).
getDate
();
let
comparisonDay
=
todayDay
;
if
(
todayDay
==
thisMonthMaxDay
&&
thisMonthMaxDay
<
lastMonthMaxDay
)
{
comparisonDay
=
lastMonthMaxDay
;
}
else
{
comparisonDay
=
Math
.
min
(
todayDay
,
lastMonthMaxDay
);
}
let
lastMonthComparisonEnd
=
new
Date
(
nowTime
.
getFullYear
(),
nowTime
.
getMonth
()
-
1
,
comparisonDay
,
nowTime
.
getHours
(),
nowTime
.
getMinutes
(),
nowTime
.
getSeconds
(),
nowTime
.
getMilliseconds
());
let
last24hStart
=
new
Date
(
nowTime
.
getTime
()
-
24
*
3600000
);
let
last7DaysStart
=
new
Date
(
nowTime
.
getTime
()
-
7
*
86400000
);
let
last30DaysStart
=
new
Date
(
nowTime
.
getTime
()
-
30
*
86400000
);
...
...
@@ -85,11 +100,13 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
},
[
"device_id"
,
"data"
,
"device_time"
]);
// 一次遍历,同时产出:4个时段总量 + 4组趋势数据
let
todayTotal
=
0
,
yesterdayTotal
=
0
,
currentMonthTotal
=
0
,
previousMonth
Total
=
0
;
let
todayTotal
=
0
,
yesterdayTotal
=
0
,
yesterdayWindowTotal
=
0
,
currentMonthTotal
=
0
,
previousMonthTotal
=
0
,
previousMonthWindow
Total
=
0
;
let
deviceTodayMap
=
new
Map
<
string
,
number
>
();
let
deviceYesterdayMap
=
new
Map
<
string
,
number
>
();
let
deviceYesterdayWindowMap
=
new
Map
<
string
,
number
>
();
let
deviceMonthMap
=
new
Map
<
string
,
number
>
();
let
devicePrevMonthMap
=
new
Map
<
string
,
number
>
();
let
devicePrevMonthWindowMap
=
new
Map
<
string
,
number
>
();
let
hourlyMap
=
new
Map
<
string
,
number
>
();
let
daily7Map
=
new
Map
<
string
,
number
>
();
let
daily30Map
=
new
Map
<
string
,
number
>
();
...
...
@@ -111,12 +128,18 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
if
(
ts
>=
yesterdayStart
.
getTime
()
&&
ts
<=
yesterdayEnd
.
getTime
())
{
deviceYesterdayMap
.
set
(
deviceId
,
(
deviceYesterdayMap
.
get
(
deviceId
)
||
0
)
+
energy
);
}
if
(
ts
>=
yesterdayStart
.
getTime
()
&&
ts
<=
yesterdaySameTimeEnd
.
getTime
())
{
deviceYesterdayWindowMap
.
set
(
deviceId
,
(
deviceYesterdayWindowMap
.
get
(
deviceId
)
||
0
)
+
energy
);
}
if
(
ts
>=
currentMonthStart
.
getTime
())
{
deviceMonthMap
.
set
(
deviceId
,
(
deviceMonthMap
.
get
(
deviceId
)
||
0
)
+
energy
);
}
if
(
ts
>=
lastMonthStart
.
getTime
()
&&
ts
<=
lastMonthEnd
.
getTime
())
{
devicePrevMonthMap
.
set
(
deviceId
,
(
devicePrevMonthMap
.
get
(
deviceId
)
||
0
)
+
energy
);
}
if
(
ts
>=
lastMonthStart
.
getTime
()
&&
ts
<=
lastMonthComparisonEnd
.
getTime
())
{
devicePrevMonthWindowMap
.
set
(
deviceId
,
(
devicePrevMonthWindowMap
.
get
(
deviceId
)
||
0
)
+
energy
);
}
// --- 趋势分组 ---
if
(
ts
>=
last24hStart
.
getTime
())
{
...
...
@@ -138,8 +161,10 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
// 汇总各时段总量
for
(
let
v
of
deviceTodayMap
.
values
())
todayTotal
+=
v
;
for
(
let
v
of
deviceYesterdayMap
.
values
())
yesterdayTotal
+=
v
;
for
(
let
v
of
deviceYesterdayWindowMap
.
values
())
yesterdayWindowTotal
+=
v
;
for
(
let
v
of
deviceMonthMap
.
values
())
currentMonthTotal
+=
v
;
for
(
let
v
of
devicePrevMonthMap
.
values
())
previousMonthTotal
+=
v
;
for
(
let
v
of
devicePrevMonthWindowMap
.
values
())
previousMonthWindowTotal
+=
v
;
// map 转为排序后的数组
let
hourlyEnergy
:
{
key
:
string
,
value
:
string
}[]
=
[];
...
...
@@ -158,8 +183,8 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
monthlyMap
.
forEach
((
v
,
k
)
=>
monthlyEnergyYear
.
push
({
key
:
k
,
value
:
v
.
toFixed
(
0
)
}));
monthlyEnergyYear
.
sort
((
a
,
b
)
=>
a
.
key
.
localeCompare
(
b
.
key
));
let
dailyYearOnYear
=
yesterday
Total
===
0
?
0
:
(((
todayTotal
-
yesterdayTotal
)
/
yesterday
Total
)
*
100
).
toFixed
(
2
);
let
monthlyYearOnYear
=
previousMonth
Total
===
0
?
0
:
(((
currentMonthTotal
-
previousMonthTotal
)
/
previousMonth
Total
)
*
100
).
toFixed
(
2
);
let
dailyYearOnYear
=
yesterday
WindowTotal
===
0
?
0
:
(((
todayTotal
-
yesterdayWindowTotal
)
/
yesterdayWindow
Total
)
*
100
).
toFixed
(
2
);
let
monthlyYearOnYear
=
previousMonth
WindowTotal
===
0
?
0
:
(((
currentMonthTotal
-
previousMonthWindowTotal
)
/
previousMonthWindow
Total
)
*
100
).
toFixed
(
2
);
// 补全 x 轴坐标的工具函数
function
padTrend
(
trend
:
any
[],
allKeys
:
string
[]):
any
[]
{
...
...
@@ -196,10 +221,12 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
// 1.4. 能耗监控数据和能耗监控趋势图数据
resultAny
.
energyManagement
=
{
todayElectricity
:
todayTotal
.
toFixed
(
0
),
yesterdayElectricity
:
yesterdayTotal
.
toFixed
(
0
),
yesterdayElectricity
:
yesterdayWindowTotal
.
toFixed
(
0
),
yesterdayTotalElectricity
:
yesterdayTotal
.
toFixed
(
0
),
dailyYearOnYear
,
currentMonthElectricity
:
currentMonthTotal
.
toFixed
(
0
),
previousMonthElectricity
:
previousMonthTotal
.
toFixed
(
0
),
previousMonthElectricity
:
previousMonthWindowTotal
.
toFixed
(
0
),
previousMonthTotalElectricity
:
previousMonthTotal
.
toFixed
(
0
),
monthlyYearOnYear
};
resultAny
.
electricityTrend
=
{
...
...
@@ -293,44 +320,52 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
device_time: { "%gte%": formatLocalTime(todayStartEnv), "%lte%": formatLocalTime(nowTime) }
}, ["data", "device_time"]);
let hourlyData = new Map();
const fieldMap: Record<string, string> = { temp: 'temperature', hum: 'humidity', pm: 'pm2_5', co2: 'co2', hcho: 'hcho', lightLevel: 'light_level', pm10: 'pm10', pressure: 'pressure', tvoc: 'tvoc' };
for (let rec of airHistory.data) {
let d = new Date(rec.device_time);
let hourKey = `
$
{
d
.
getHours
()}
时
`;
let data = rec.data;
if (!hourlyData.has(hourKey)) {
hourlyData.set(hourKey, { temp: data.temperature, hum: data.humidity, pm: data.pm2_5, co2: data.co2
, hcho: data.hcho, lightLevel: data.light_level, pm10: data.pm10, pressure: data.pressure, tvoc: data.tvoc });
let cur = hourlyData.get(hourKey);
if (!cur) {
cur = { cnt: 0 };
hourlyData.set(hourKey, cur);
}
cur.cnt++;
for (let field of ['temp', 'hum', 'pm', 'co2', 'hcho', 'lightLevel', 'pm10', 'pressure', 'tvoc']) {
let key = field + 'Sum';
cur[key] = (cur[key] || 0) + (Number(data[fieldMap[field]]) || 0);
}
}
const currentHour = nowTime.getHours();
for (let i = 0; i < 24; i++) {
let key = `
$
{
i
}
时
`;
let val = hourlyData.get(key);
let avg = (sumKey: string) => val?.cnt ? ((val[sumKey] || 0) / val.cnt).toFixed(1) : '0';
if (i <= currentHour) {
temperatureTrend.push({ time: `
$
{
i
}:
00
`, value: val?.
temp?.toString() ??
'0' });
humidityTrend.push({ time: `
$
{
i
}:
00
`, value: val?.
hum?.toString() ??
'0' });
pm25Trend.push({ time: `
$
{
i
}:
00
`, value: val?.
pm?.toString() ??
'0' });
let co2Val = val?.c
o2 ??
0;
temperatureTrend.push({ time: `
$
{
i
}:
00
`, value: val?.
cnt ? avg('tempSum') :
'0' });
humidityTrend.push({ time: `
$
{
i
}:
00
`, value: val?.
cnt ? avg('humSum') :
'0' });
pm25Trend.push({ time: `
$
{
i
}:
00
`, value: val?.
cnt ? avg('pmSum') :
'0' });
let co2Val = val?.c
nt ? ((val['co2Sum'] || 0) / val.cnt) :
0;
co2TrendDetail.push({ time: `
$
{
i
}:
00
`, value: (co2Val / 100).toFixed(1) });
co2Trend.push({ time: `
$
{
i
}
时`
,
value
:
co2Val
.
to
String
(
)
});
co2Trend.push({ time: `
$
{
i
}
时`
,
value
:
co2Val
.
to
Fixed
(
0
)
});
hchoTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
val
?.
hcho
?.
toString
()
??
'0'
});
lightLevelTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
val
?.
lightLevel
?.
toString
()
??
'0'
});
pm10Trend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
val
?.
pm10
?.
toString
()
??
'0'
});
pressureTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
val
?.
pressure
?.
toString
()
??
'0'
});
tvocTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
val
?.
tvoc
?.
toString
()
??
'0'
});
hchoTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
val
?.
cnt
?
avg
(
'hchoSum'
)
:
'0'
});
lightLevelTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
val
?.
cnt
?
avg
(
'lightLevelSum'
)
:
'0'
});
pm10Trend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
val
?.
cnt
?
avg
(
'pm10Sum'
)
:
'0'
});
pressureTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
val
?.
cnt
?
avg
(
'pressureSum'
)
:
'0'
});
tvocTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
val
?.
cnt
?
avg
(
'tvocSum'
)
:
'0'
});
}
else
{
temperatureTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
null
});
humidityTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
null
});
pm25Trend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
null
});
co2TrendDetail
.
push
({
time
:
`
${
i
}
:00`
,
value
:
null
});
co2Trend
.
push
({
time
:
`
${
i
}
时`
,
value
:
null
});
temperatureTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
'0'
});
humidityTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
'0'
});
pm25Trend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
'0'
});
co2TrendDetail
.
push
({
time
:
`
${
i
}
:00`
,
value
:
'0'
});
co2Trend
.
push
({
time
:
`
${
i
}
时`
,
value
:
'0'
});
hchoTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
null
});
lightLevelTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
null
});
pm10Trend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
null
});
pressureTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
null
});
tvocTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
null
});
hchoTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
'0'
});
lightLevelTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
'0'
});
pm10Trend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
'0'
});
pressureTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
'0'
});
tvocTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
'0'
});
}
}
}
...
...
@@ -377,31 +412,29 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
let
alertWorkOrders
:
any
[]
=
[];
try
{
const
todayFilter
=
{
"%gte%"
:
formatLocalTime
(
todayStart
)
};
const
thirtyDaysAgo
=
new
Date
(
nowTime
.
getTime
()
-
30
*
86400000
);
const
timeFilter
=
{
"%gte%"
:
formatLocalTime
(
thirtyDaysAgo
)
};
const
statsRes
=
await
selectDataListByParam
(
TABLENAME
.
设备故障表
,
{
occurred_time
:
timeFilter
},
[
"status"
,
"level"
,
"fault_type"
]);
const
statsFaults
:
any
[]
=
Array
.
isArray
(
statsRes
.
data
)
?
statsRes
.
data
:
[];
const
totalAlerts
=
statsFaults
.
length
;
let
resolved
=
0
,
responded
=
0
,
unresolved
=
0
;
for
(
const
f
of
statsFaults
)
{
if
(
f
.
status
===
2
)
resolved
++
;
else
if
(
f
.
status
===
1
)
responded
++
;
else
if
(
f
.
status
===
0
)
unresolved
++
;
}
alertStatus
=
{
totalAlerts
,
resolved
,
responded
,
unresolved
};
for
(
const
f
of
statsFaults
)
{
const
thirtyDayFilter
=
{
"%gte%"
:
formatLocalTime
(
thirtyDaysAgo
)
};
// alertWorkLevel:查今日三个级别的预警数量
const
levelRes
=
await
selectDataListByParam
(
TABLENAME
.
设备故障表
,
{
occurred_time
:
todayFilter
},
[
"level"
]);
const
levelFaults
:
any
[]
=
Array
.
isArray
(
levelRes
.
data
)
?
levelRes
.
data
:
[];
for
(
const
f
of
levelFaults
)
{
if
(
f
.
level
===
1
)
alertWorkLevel
[
"一级预警"
]
++
;
else
if
(
f
.
level
===
2
)
alertWorkLevel
[
"二级预警"
]
++
;
else
if
(
f
.
level
===
3
)
alertWorkLevel
[
"三级预警"
]
++
;
}
// alertWorkType:查今日各故障类型占比
const
todayTypeRes
=
await
selectDataListByParam
(
TABLENAME
.
设备故障表
,
{
occurred_time
:
todayFilter
},
[
"fault_type"
]);
const
todayTypeFaults
:
any
[]
=
Array
.
isArray
(
todayTypeRes
.
data
)
?
todayTypeRes
.
data
:
[];
const
todayAlertTotal
=
todayTypeFaults
.
length
;
let
faultTypeCountMap
=
new
Map
<
string
,
number
>
();
for
(
const
f
of
stats
Faults
)
{
for
(
const
f
of
todayType
Faults
)
{
const
typeName
=
f
.
fault_type
||
"其他"
;
faultTypeCountMap
.
set
(
typeName
,
(
faultTypeCountMap
.
get
(
typeName
)
||
0
)
+
1
);
}
...
...
@@ -409,13 +442,30 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
alertWorkType
.
push
({
count
,
name
,
proportion
:
to
talAlerts
>
0
?
((
count
/
totalAlerts
)
*
100
).
toFixed
(
0
)
+
'%'
:
'0%'
proportion
:
to
dayAlertTotal
>
0
?
((
count
/
todayAlertTotal
)
*
100
).
toFixed
(
0
)
+
'%'
:
'0%'
});
}
// alertStatus:查近 30 天不同状态的预警
const
statsRes
=
await
selectDataListByParam
(
TABLENAME
.
设备故障表
,
{
occurred_time
:
thirtyDayFilter
},
[
"status"
]);
const
statsFaults
:
any
[]
=
Array
.
isArray
(
statsRes
.
data
)
?
statsRes
.
data
:
[];
const
totalAlerts
=
statsFaults
.
length
;
let
resolved
=
0
,
responded
=
0
,
unresolved
=
0
;
for
(
const
f
of
statsFaults
)
{
if
(
f
.
status
===
2
)
resolved
++
;
else
if
(
f
.
status
===
1
)
responded
++
;
else
if
(
f
.
status
===
0
)
unresolved
++
;
}
alertStatus
=
{
totalAlerts
,
resolved
,
responded
,
unresolved
};
// alertWorkOrders:查近 30 天未处理的预警列表
const
ordersRes
=
await
selectDataListByParam
(
TABLENAME
.
设备故障表
,
{
occurred_time
:
timeFilter
,
occurred_time
:
thirtyDayFilter
,
status
:
0
,
"%orderDesc%"
:
"occurred_time"
,
"%limit%"
:
10
},
...
...
@@ -626,10 +676,9 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
let
acOnlineTrend
:
{
time
:
string
;
value
:
string
}[]
=
[];
if
(
acDeviceIds
.
length
)
{
let
last24h
=
new
Date
(
nowTime
.
getTime
()
-
24
*
3600000
);
let
acHistory
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
"%in%"
:
acDeviceIds
},
device_time
:
{
"%gte%"
:
formatLocalTime
(
last24h
)
}
device_time
:
{
"%gte%"
:
formatLocalTime
(
todayStart
),
"%lte%"
:
formatLocalTime
(
nowTime
)
}
},
[
"device_id"
,
"data"
,
"device_time"
]);
let
hourlyData
=
new
Map
();
for
(
let
rec
of
acHistory
.
data
)
{
...
...
@@ -645,10 +694,15 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
hourlyData
.
set
(
hourKey
,
v
);
}
}
let
currentHour
=
nowTime
.
getHours
();
for
(
let
i
=
0
;
i
<
24
;
i
++
)
{
if
(
i
<=
currentHour
)
{
let
key
=
`
${
i
}
时`
;
let
val
=
hourlyData
.
get
(
key
);
acOnlineTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
val
?
((
val
.
online
/
val
.
total
)
*
100
).
toFixed
(
2
)
:
'0'
});
}
else
{
acOnlineTrend
.
push
({
time
:
`
${
i
}
:00`
,
value
:
'0'
});
}
}
}
return
{
acDeviceIds
,
acCount
,
acOnlineCount
,
acOfflineCount
,
acFaultCount
,
acOnlineTrend
};
...
...
@@ -898,13 +952,14 @@ export async function controlAcRunning(params: {}) {
// 手动控制日志
const
now
=
new
Date
();
const
operationTime
=
new
Date
();
const
operationTimeStr
=
formatLocalTime
(
operationTime
);
const
acLogs
=
[{
device_id
:
deviceId
,
device_type
:
'空调'
,
operation_content
:
{
option
:
'手动控制'
,
device_id
:
deviceId
,
time
:
operationTime
,
time
:
operationTime
Str
,
mode
:
mode
,
power
:
power
,
setTemp
:
setTemp
,
...
...
@@ -994,9 +1049,9 @@ function getSeason(): 'summer' | 'winter' {
function
calcAcTempSet
(
temperature
:
number
,
season
:
'summer'
|
'winter'
):
{
tempSet
:
number
;
fanSpeed
:
number
;
onOff
:
number
}
{
if
(
season
===
'summer'
)
{
// 夏季制冷:温度越高,设定越低
if
(
temperature
>
2
1
)
return
{
tempSet
:
16
,
fanSpeed
:
1
,
onOff
:
1
};
// 未达需求:高风制冷
if
(
temperature
>=
16
)
return
{
tempSet
:
16
,
fanSpeed
:
4
,
onOff
:
1
};
// 舒适区:四舍五入21℃待机 Math.round(temperature) === 21 ? 0 : 1
return
{
tempSet
:
1
8
,
fanSpeed
:
4
,
onOff
:
1
};
// 已达需求:低风维持
if
(
temperature
>
2
4
)
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
:
1
9
,
fanSpeed
:
4
,
onOff
:
1
};
// 已达需求:低风维持
}
else
{
// 冬季制热:温度越低,设定越高
if
(
temperature
<
18
)
return
{
tempSet
:
21
,
fanSpeed
:
1
,
onOff
:
1
};
// 未达需求:高风制热
...
...
@@ -1021,7 +1076,7 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
},
[
"id"
,
"device_name"
,
"region_key"
]);
const
now
=
new
Date
();
const
operationTime
=
now
.
toISOString
().
replace
(
'T'
,
' '
).
slice
(
0
,
19
);
const
operationTime
=
formatLocalTime
(
now
);
const
season
=
getSeason
();
const
temperature
=
data
.
temperature
;
const
co2
=
data
.
co2
;
...
...
@@ -1038,12 +1093,14 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
if
(
fanParams
)
{
let
fanDevices
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
region_key
:
regionKey
,
device_type
:
{
"%in%"
:
[
"新风"
,
"空调"
]
}
device_type
:
{
"%in%"
:
[
"新风"
,
"空调"
]
},
linkage_start
:
'on'
},
[
"id"
,
"device_name"
,
"device_id"
,
"device_ad"
,
"device_type"
]);
if
(
fanDevices
.
data
.
length
>
0
)
{
// CO2联动:高→送风模式(3)+高风(1),低→不传workMode(不切换模式)+低风(4)
const
fanWorkMode
:
number
|
undefined
=
fanParams
.
action
===
'high'
?
3
:
undefined
;
// const fanWorkMode: number | undefined = fanParams.action === 'high' ? 3 : undefined;
const
fanWorkMode
=
1
;
const
fanSpeedVal
=
fanParams
.
action
===
'high'
?
1
:
4
;
// 去重
...
...
@@ -1067,8 +1124,8 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
device_id
:
deviceId
,
time
:
operationTime
,
roomCo2
:
co2
,
mode
:
fanParams
.
action
===
'high'
?
'送风'
:
'低风待机'
,
action
:
fanParams
.
action
===
'high'
?
'送风高风'
:
'低风不切换模式'
,
mode
:
fanParams
.
action
===
'high'
?
'送风
制冷
'
:
'低风待机'
,
action
:
fanParams
.
action
===
'high'
?
'送风
制冷
高风'
:
'低风不切换模式'
,
success
:
fanControlRes
.
success
,
message
:
fanControlRes
.
message
||
(
fanControlRes
.
success
?
'控制成功'
:
'控制失败'
),
setTemp
:
temp
?.
tempSet
,
...
...
@@ -1080,7 +1137,7 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
await
addData
(
TABLENAME
.
设备日志表
,
fanLogs
);
if
(
fanControlRes
.
success
)
{
console
.
log
(
`新风联动成功: CO2
${
co2
}
ppm →
${
fanParams
.
action
===
'high'
?
'送风高风'
:
'低风待机'
}
, 设备
${
deviceId
}
`
);
console
.
log
(
`新风联动成功: CO2
${
co2
}
ppm →
${
fanParams
.
action
===
'high'
?
'送风
制冷
高风'
:
'低风待机'
}
, 设备
${
deviceId
}
`
);
}
else
{
console
.
error
(
`新风联动失败:
${
fanControlRes
.
message
}
`
);
}
...
...
@@ -1093,7 +1150,8 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
if
(
temperature
!=
null
)
{
let
acDevices
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
region_key
:
regionKey
,
device_type
:
"空调"
device_type
:
"空调"
,
linkage_start
:
'on'
},
[
"id"
,
"device_name"
,
"device_id"
,
"device_ad"
,
"control_params"
]);
if
(
acDevices
.
data
.
length
>
0
)
{
...
...
@@ -1180,6 +1238,8 @@ async function controlAcRunningApi(acDevices?: any, workMode?: number, tempSet?:
// workMode 为 undefined 时不传,飞奕不切换模式
if
(
workMode
!=
null
)
{
params
.
workMode
=
workMode
;
}
else
{
params
.
workMode
=
1
;
// 设置默认为制冷模式
}
// 如果传入了设定温度,一并下发
if
(
tempSet
!=
null
)
{
...
...
@@ -1244,7 +1304,7 @@ export async function stopScheduleTask() {
* 1 高风-超强 2 中风-强 4 低风-弱
* 0 不锁定-生效 1 锁定-解除
*/
let
acDeviceKeyMap
:
{
[
key
:
string
]:
number
}
=
{
export
let
acDeviceKeyMap
:
{
[
key
:
string
]:
number
}
=
{
"制冷"
:
1
,
"制热"
:
2
,
"送风"
:
3
,
...
...
@@ -1255,7 +1315,12 @@ let acDeviceKeyMap: { [key: string]: number } = {
"强"
:
2
,
"弱"
:
4
,
"解除"
:
1
,
"生效"
:
0
"生效"
:
0
,
"高风"
:
1
,
"中风"
:
2
,
"低风"
:
4
,
"开"
:
1
,
"关"
:
0
}
/**
...
...
src/config/dbEnum.ts
View file @
77a71622
...
...
@@ -10,7 +10,8 @@ export enum TABLENAME {
用户信息表
=
'user_info'
,
设备日志表
=
'device_logs'
,
日程表
=
'schedule'
,
日程设备关联表
=
'schedule_device'
日程设备关联表
=
'schedule_device'
,
数据接入日志表
=
'data_access_log'
};
/**
...
...
src/config/mysqlTableConfig.ts
View file @
77a71622
import
{
response
}
from
"express"
;
const
{
Sequelize
,
DataTypes
}
=
require
(
'sequelize'
);
export
const
TablesConfig
=
[
...
...
@@ -169,6 +171,12 @@ export const TablesConfig = [
defaultValue
:
0
,
comment
:
'设备状态:0=正常 1=硬件故障 2=通信故障 3=离线'
},
linkage_start
:
{
type
:
DataTypes
.
STRING
(
10
),
allowNull
:
true
,
defaultValue
:
'on'
,
comment
:
'联动启用:on=开 off=关'
},
created_at
:
{
type
:
DataTypes
.
DATE
,
allowNull
:
false
,
...
...
@@ -417,6 +425,80 @@ export const TablesConfig = [
{
type
:
"hasMany"
,
target
:
"schedule_device"
,
foreignKey
:
"schedule_id"
}
]
},
// 数据接入日志表
{
tableNameCn
:
'数据接入日志表'
,
tableName
:
'data_access_log'
,
schema
:
{
id
:
{
type
:
DataTypes
.
BIGINT
,
allowNull
:
false
,
primaryKey
:
true
,
autoIncrement
:
true
,
comment
:
'自增主键'
},
partner_name
:
{
type
:
DataTypes
.
STRING
(
100
),
allowNull
:
false
,
comment
:
'对接方名称,如“飞奕”'
},
request_url
:
{
type
:
DataTypes
.
STRING
(
500
),
allowNull
:
false
,
comment
:
'请求的第三方接口地址'
},
request_desc
:
{
type
:
DataTypes
.
STRING
(
200
),
allowNull
:
true
,
comment
:
'请求描述,如"查询设备列表""控制空调开关"'
},
request_params
:
{
type
:
DataTypes
.
JSON
,
allowNull
:
true
,
comment
:
'请求参数'
},
response_data
:
{
type
:
DataTypes
.
JSON
,
allowNull
:
true
,
comment
:
'第三方返回的数据'
},
status
:
{
type
:
DataTypes
.
TINYINT
,
allowNull
:
false
,
defaultValue
:
1
,
comment
:
'请求状态,0失败1成功'
},
request_time
:
{
type
:
DataTypes
.
DATE
,
allowNull
:
true
,
comment
:
'请求时间'
},
response_time
:
{
type
:
DataTypes
.
DATE
,
allowNull
:
true
,
comment
:
'响应时间'
},
response_status
:
{
type
:
DataTypes
.
STRING
(
100
),
allowNull
:
true
,
comment
:
'响应状态描述,如"连接超时""请求报错""正常"等'
},
created_at
:
{
type
:
DataTypes
.
DATE
,
allowNull
:
false
,
defaultValue
:
DataTypes
.
NOW
,
comment
:
'创建时间'
}
},
association
:
[],
indexes
:
[
{
fields
:
[
'partner_name'
]
},
{
fields
:
[
'status'
]
},
{
fields
:
[
'request_time'
]
},
{
fields
:
[
'partner_name'
,
'status'
]
},
{
fields
:
[
'partner_name'
,
'request_time'
]
},
]
},
// 日程设备关联表
{
tableNameCn
:
'日程设备关联表'
,
...
...
src/main.ts
View file @
77a71622
...
...
@@ -19,13 +19,13 @@ async function lanuch() {
/**创建http服务 */
httpServer
.
createServer
(
systemConfig
.
port
);
/**启动MQTT订阅服务 */
//
await mqttSrv.startMqttClient();
await
mqttSrv
.
startMqttClient
();
/**启动定时任务 */
//
startSchedule();
startSchedule
();
/**启动预警工单定时任务(10分钟) */
//
startAlertSchedule();
startAlertSchedule
();
/**启动故障状态更新定时任务(5分钟) */
//
startFaultStatusSchedule();
startFaultStatusSchedule
();
/**启动日程执行器 */
// await initScheduleTasks();
...
...
src/model/sqlModelBind.ts
View file @
77a71622
...
...
@@ -25,7 +25,7 @@ export async function initMysqlModel() {
/**第一步:初始化所有表 */
for
(
let
i
=
0
;
i
<
TablesConfig
.
length
;
i
++
)
{
let
{
tableName
,
schema
}
=
TablesConfig
[
i
];
let
{
tableName
,
schema
,
indexes
}
=
TablesConfig
[
i
];
if
(
!
tableName
)
{
console
.
warn
(
`⚠️ 第
${
i
}
个表配置缺少 tableName,跳过`
);
...
...
@@ -39,10 +39,13 @@ export async function initMysqlModel() {
console
.
log
(
`🔄 正在初始化表:
${
tableName
}
`
);
let
schemaConf
=
{
let
schemaConf
:
any
=
{
freezeTableName
:
true
,
timestamps
:
false
};
if
(
indexes
&&
Array
.
isArray
(
indexes
)
&&
indexes
.
length
)
{
schemaConf
.
indexes
=
indexes
;
}
try
{
let
model
=
mysqlDB
.
define
(
tableName
,
schema
,
schemaConf
);
...
...
src/routers/admin.ts
View file @
77a71622
...
...
@@ -6,6 +6,7 @@ import asyncHandler from "express-async-handler";
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
{
reloadScheduleTasks
}
from
"../biz/scheduleExecutor"
;
import
{
eccReqParamater
}
from
"../util/verificationParam"
;
...
...
@@ -29,6 +30,18 @@ export function setRouter(httpServer) {
httpServer
.
post
(
"/api/qdm/admin/schedule/edit"
,
asyncHandler
(
editSchedule
));
/** 日程管理删除 */
httpServer
.
post
(
"/api/qdm/admin/schedule/delete"
,
asyncHandler
(
deleteSchedule
));
/** 设备管理列表 */
httpServer
.
get
(
"/api/qdm/admin/device/list"
,
asyncHandler
(
getDeviceList
));
/** 设备管理 - 编辑空调控制参数 */
httpServer
.
post
(
"/api/qdm/admin/device/edit"
,
asyncHandler
(
editDevice
));
/** 设备管理 - 批量编辑设备 */
httpServer
.
post
(
"/api/qdm/admin/device/edit_batch"
,
asyncHandler
(
batchEditDevices
));
/** 设备管理 - 编辑设备信息 */
httpServer
.
post
(
"/api/qdm/admin/device/editDevice"
,
asyncHandler
(
editLocalDevice
));
/** 设备管理 - 批量编辑设备信息 */
httpServer
.
post
(
"/api/qdm/admin/device/editDevice_batch"
,
asyncHandler
(
batchEditLocalDevices
));
}
/**
...
...
@@ -97,12 +110,12 @@ async function getScheduleList(req, res) {
async
function
getScheduleDetail
(
req
,
res
)
{
const
scheduleId
=
Number
(
req
.
query
?.
scheduleId
);
if
(
!
scheduleId
)
{
res
.
fail
(
"缺少参数 scheduleId"
);
res
.
error
(
"缺少参数 scheduleId"
);
return
;
}
const
data
=
await
scheduleBiz
.
getScheduleDetail
(
scheduleId
);
if
(
!
data
)
{
res
.
fail
(
"日程不存在"
);
res
.
error
(
"日程不存在"
);
return
;
}
res
.
success
(
data
);
...
...
@@ -118,11 +131,11 @@ async function addSchedule(req, res) {
const
NotMustHaveKeys
=
[
"deviceIds"
,
"regionKeys"
];
let
{
title
,
scheduleTime
,
acControl
,
beginValidity
,
endValidity
,
deviceIds
,
regionKeys
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
if
(
!
title
||
!
scheduleTime
||
!
acControl
)
{
res
.
fail
(
"缺少必填参数:title, scheduleTime, acControl"
);
res
.
error
(
"缺少必填参数:title, scheduleTime, acControl"
);
return
;
}
if
((
!
deviceIds
&&
!
regionKeys
)
||
(
!
deviceIds
.
length
&&
!
regionKeys
.
length
))
{
res
.
fail
(
"至少选择一个区域"
);
res
.
error
(
"至少选择一个区域"
);
return
;
}
const
data
=
await
scheduleBiz
.
addSchedule
({
...
...
@@ -149,11 +162,11 @@ async function editSchedule(req, res) {
const
NotMustHaveKeys
=
[
"deviceIds"
,
"regionKeys"
];
let
{
scheduleId
,
title
,
scheduleTime
,
acControl
,
beginValidity
,
endValidity
,
deviceIds
,
regionKeys
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
if
(
!
scheduleId
||
!
title
||
!
scheduleTime
||
!
acControl
)
{
res
.
fail
(
"缺少必填参数:title, scheduleTime, acControl"
);
res
.
error
(
"缺少必填参数:title, scheduleTime, acControl"
);
return
;
}
if
((
!
deviceIds
&&
!
regionKeys
)
||
(
!
deviceIds
.
length
&&
!
regionKeys
.
length
))
{
res
.
fail
(
"至少选择一个区域"
);
res
.
error
(
"至少选择一个区域"
);
return
;
}
const
data
=
await
scheduleBiz
.
editSchedule
({
...
...
@@ -179,7 +192,7 @@ async function editSchedule(req, res) {
async
function
deleteSchedule
(
req
,
res
)
{
const
{
scheduleId
}
=
req
.
body
||
{};
if
(
!
scheduleId
)
{
res
.
fail
(
"缺少参数 scheduleId"
);
res
.
error
(
"缺少参数 scheduleId"
);
return
;
}
const
data
=
await
scheduleBiz
.
deleteSchedule
(
Number
(
scheduleId
));
...
...
@@ -187,3 +200,164 @@ async function deleteSchedule(req, res) {
// 删除日程后重载执行器
reloadScheduleTasks
().
catch
((
err
)
=>
console
.
error
(
"[日程重载] 删除后重载失败:"
,
err
));
}
// ======================= 设备管理 =======================
/**
* GET /api/qdm/admin/device/list
* 设备列表
* query: pageNumber, pageSize, regionKeys, deviceName, power, state, deviceMode
*/
async
function
getDeviceList
(
req
,
res
)
{
const
data
=
await
deviceBiz
.
getDeviceList
({
pageNumber
:
Number
(
req
.
query
?.
pageNumber
)
||
1
,
pageSize
:
Number
(
req
.
query
?.
pageSize
)
||
10
,
regionKeys
:
req
.
query
?.
regionKeys
||
undefined
,
deviceName
:
req
.
query
?.
deviceName
||
undefined
,
power
:
req
.
query
?.
power
||
undefined
,
state
:
req
.
query
?.
state
||
undefined
,
deviceMode
:
req
.
query
?.
deviceMode
||
undefined
,
});
res
.
success
(
data
);
}
/**
* POST /api/qdm/admin/device/edit
* 设备管理 - 编辑空调控制参数
* 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
);
if
(
!
deviceId
||
!
power
)
{
res
.
error
(
"缺少必填参数:deviceId, power"
);
return
;
}
const
data
=
await
deviceBiz
.
editAcDeviceControl
({
deviceId
,
power
,
setTemp
,
fanSpeed
,
deviceMode
});
res
.
success
(
data
);
}
/**
* POST /api/qdm/admin/device/batch_edit
* 设备管理 - 批量编辑设备
* body: { regionKeys, deviceName?, power?, state?, deviceMode?, startDate?, endDate?, edits }
* edits: { power?, setTemp?, linkageStart? }
*/
async
function
batchEditDevices
(
req
,
res
)
{
let
reqConf
=
{
regionKeys
:
'Array'
,
deviceName
:
'String'
,
power
:
'String'
,
state
:
'String'
,
deviceMode
:
'String'
,
startDate
:
'String'
,
endDate
:
'String'
,
edits
:
'Object'
,
};
const
NotMustHaveKeys
=
[
"deviceName"
,
"power"
,
"state"
,
"deviceMode"
,
"startDate"
,
"endDate"
];
let
{
regionKeys
,
deviceName
,
power
,
state
,
deviceMode
,
startDate
,
endDate
,
edits
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
if
(
!
regionKeys
||
!
regionKeys
.
length
)
{
res
.
error
(
"缺少必填参数:regionKeys"
);
return
;
}
if
(
!
edits
||
typeof
edits
!==
'object'
)
{
res
.
error
(
"缺少必填参数:edits"
);
return
;
}
if
(
edits
.
power
===
undefined
&&
edits
.
setTemp
===
undefined
&&
edits
.
linkageStart
===
undefined
)
{
res
.
error
(
"edits 中至少需要 power / setTemp / linkageStart 其中之一"
);
return
;
}
// 校验 edits.power 值合法性
if
(
edits
.
power
!==
undefined
&&
!
[
'开'
,
'关'
].
includes
(
edits
.
power
))
{
res
.
error
(
"edits.power 只能为 开 或 关"
);
return
;
}
// 校验 edits.setTemp 值合法性
if
(
edits
.
setTemp
!==
undefined
&&
(
typeof
edits
.
setTemp
!==
'number'
||
edits
.
setTemp
<
16
||
edits
.
setTemp
>
30
))
{
res
.
error
(
"edits.setTemp 必须为 16~30 之间的数值"
);
return
;
}
// 校验 edits.linkageStart 值合法性
if
(
edits
.
linkageStart
!==
undefined
&&
!
[
'on'
,
'off'
].
includes
(
edits
.
linkageStart
))
{
res
.
error
(
"edits.linkageStart 只能为 on 或 off"
);
return
;
}
const
data
=
await
deviceBiz
.
batchEditDevices
({
regionKeys
,
deviceName
:
deviceName
||
undefined
,
power
:
power
||
undefined
,
state
:
state
||
undefined
,
deviceMode
:
deviceMode
||
undefined
,
startDate
:
startDate
||
undefined
,
endDate
:
endDate
||
undefined
,
edits
,
});
res
.
success
(
data
);
}
/**
* POST /api/qdm/admin/device/editDevice
* 设备管理 - 编辑单台设备表信息
* body: { deviceId, deviceName?, regionKey?, deviceType?, controlParams?, deviceState? }
*/
async
function
editLocalDevice
(
req
,
res
)
{
let
reqConf
=
{
deviceId
:
'String'
,
deviceName
:
'String'
,
regionKey
:
'Number'
,
deviceType
:
'String'
,
deviceState
:
'String'
,
linkageStart
:
'String'
,
};
const
NotMustHaveKeys
=
[
"deviceName"
,
"regionKey"
,
"deviceType"
,
"deviceState"
,
"linkageStart"
];
let
{
deviceId
,
deviceName
,
regionKey
,
deviceType
,
deviceState
,
linkageStart
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
if
(
!
deviceId
)
{
res
.
error
(
"缺少必填参数:deviceId"
);
return
;
}
const
hasEdit
=
deviceName
!==
undefined
||
regionKey
!==
undefined
||
deviceType
!==
undefined
||
deviceState
!==
undefined
||
linkageStart
!==
undefined
;
if
(
!
hasEdit
)
{
res
.
error
(
"至少需要提供一个编辑字段(deviceName/regionKey/deviceType/controlParams/deviceState)"
);
return
;
}
const
data
=
await
deviceBiz
.
editLocalDevice
({
deviceId
,
deviceName
,
regionKey
,
deviceType
,
deviceState
,
linkageStart
});
res
.
success
(
data
);
}
/**
* POST /api/qdm/admin/device/editDevice_batch
* 设备管理 - 批量编辑设备表信息
* body: { regionKeys?, deviceName?, edits: { deviceName?, regionKey?, deviceType?, controlParams?, deviceState? } }
*/
async
function
batchEditLocalDevices
(
req
,
res
)
{
let
reqConf
=
{
regionKeys
:
'Array'
,
deviceName
:
'String'
,
edits
:
'Object'
,
};
const
NotMustHaveKeys
=
[
"regionKeys"
,
"deviceName"
];
let
{
regionKeys
,
deviceName
,
edits
}
=
eccReqParamater
(
reqConf
,
req
.
body
,
NotMustHaveKeys
);
if
(
!
edits
||
typeof
edits
!==
'object'
)
{
res
.
error
(
"缺少必填参数:edits"
);
return
;
}
if
((
!
regionKeys
||
regionKeys
.
length
===
0
)
&&
!
deviceName
)
{
res
.
error
(
"至少需要提供 regionKeys 或 deviceName 筛选条件"
);
return
;
}
const
validEditKeys
=
[
"deviceName"
,
"regionKey"
,
"deviceType"
,
"controlParams"
,
"deviceState"
];
const
hasEdit
=
validEditKeys
.
some
(
k
=>
edits
[
k
]
!==
undefined
);
if
(
!
hasEdit
)
{
res
.
error
(
"edits 中至少需要一个编辑字段(deviceName/regionKey/deviceType/controlParams/deviceState)"
);
return
;
}
const
data
=
await
deviceBiz
.
batchEditLocalDevices
({
regionKeys
,
deviceName
,
edits
});
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