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
95bcc1ca
Commit
95bcc1ca
authored
Jul 10, 2026
by
PC-20251223ZVQQ\Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
日程管理接口
parent
18e87f61
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
924 additions
and
46 deletions
+924
-46
电表对应区域.xlsx
res/电表对应区域.xlsx
+0
-0
mqttClient.ts
src/biz/mqttClient.ts
+5
-1
region.ts
src/biz/region.ts
+74
-0
report.ts
src/biz/report.ts
+199
-15
running.ts
src/biz/running.ts
+23
-6
schedule.ts
src/biz/schedule.ts
+331
-0
dbEnum.ts
src/config/dbEnum.ts
+3
-1
mysqlTableConfig.ts
src/config/mysqlTableConfig.ts
+113
-0
admin.ts
src/routers/admin.ts
+174
-0
report.ts
src/routers/report.ts
+0
-21
router.ts
src/routers/router.ts
+2
-2
No files found.
res/电表对应区域.xlsx
View file @
95bcc1ca
No preview for this file type
src/biz/mqttClient.ts
View file @
95bcc1ca
...
...
@@ -96,7 +96,11 @@ export async function processDeviceData(message) {
[
"name"
]
);
// 调用空调控制(非文创区域才允许自动控温)
if
(
regionRow
?.
data
&&
!
regionRow
.
data
.
name
?.
includes
(
"文创"
))
{
if
(
regionRow
?.
data
&&
!
regionRow
.
data
.
name
?.
includes
(
"文创"
)
&&
!
regionRow
.
data
.
name
?.
includes
(
"原料厅"
)
&&
!
regionRow
.
data
.
name
?.
includes
(
"社会责任厅"
)
&&
!
regionRow
.
data
.
name
?.
includes
(
"新包装"
)
)
{
await
controlAcByEnvironment
(
devEUI
,
data
);
}
}
...
...
src/biz/region.ts
View file @
95bcc1ca
...
...
@@ -69,4 +69,77 @@ export async function getRegionAllList( regionGroup: string ) {
return
{
list
:
regionMap
};
}
/**
* 获取区域设备树
* @returns 区域树
*/
export
async
function
getRegionDevices
()
{
// 查询所有区域
let
where
:
any
=
{
"%orderAsc%"
:
"sort_order"
};
let
regionList
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
where
,
[
"id"
,
"room_id"
,
"name"
,
"type"
,
"groups"
],
);
const
regions
:
any
[]
=
regionList
.
data
||
[];
if
(
regions
.
length
===
0
)
return
[];
// 通过区域查询所有空调设备
const
regionIds
=
regions
.
map
((
r
:
any
)
=>
r
.
id
);
const
deviceList
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
region_key
:
{
"%in%"
:
regionIds
},
device_type
:
"空调"
},
[
"device_id"
,
"device_name"
,
"region_key"
,
"control_params"
],
);
const
devices
:
any
[]
=
deviceList
.
data
||
[];
// 将设备列表转成map形式:{regionKey:[{设备1},{设备2}]}
const
deviceMap
:
{
[
regionKey
:
number
]:
any
[]
}
=
{};
for
(
const
d
of
devices
)
{
const
rk
=
d
.
region_key
;
if
(
!
deviceMap
[
rk
])
deviceMap
[
rk
]
=
[];
deviceMap
[
rk
].
push
({
deviceId
:
d
.
device_id
,
deviceName
:
d
.
device_name
||
d
.
device_id
,
controlParams
:
d
.
control_params
||
null
,
});
}
// 循环区域,将区域转成树结构,并通过设备map将设备加入区域
const
groupMap
:
{
[
group
:
string
]:
{
key
:
string
;
value
:
string
;
typeMap
:
{
[
type
:
string
]:
{
key
:
string
;
value
:
string
;
sub
:
any
[]
}
}
}
}
=
{};
for
(
const
r
of
regions
)
{
const
groupName
=
r
.
groups
;
const
typeName
=
r
.
type
;
if
(
!
groupMap
[
groupName
])
{
groupMap
[
groupName
]
=
{
key
:
groupName
,
value
:
groupName
,
typeMap
:
{}
};
}
if
(
!
groupMap
[
groupName
].
typeMap
[
typeName
])
{
groupMap
[
groupName
].
typeMap
[
typeName
]
=
{
key
:
typeName
,
value
:
typeName
,
sub
:
[]
};
}
groupMap
[
groupName
].
typeMap
[
typeName
].
sub
.
push
({
regionKey
:
r
.
id
,
roomId
:
r
.
room_id
,
regionName
:
r
.
name
,
devices
:
deviceMap
[
r
.
id
]
||
[],
});
}
// 返回区域树
const
result
:
any
[]
=
[];
const
sortedGroups
=
Object
.
keys
(
groupMap
).
sort
();
for
(
const
groupName
of
sortedGroups
)
{
const
group
=
groupMap
[
groupName
];
const
typeArr
:
any
[]
=
[];
const
sortedTypes
=
Object
.
keys
(
group
.
typeMap
).
sort
();
for
(
const
typeName
of
sortedTypes
)
{
typeArr
.
push
(
group
.
typeMap
[
typeName
]);
}
result
.
push
({
key
:
group
.
key
,
value
:
group
.
value
,
sub
:
typeArr
});
}
return
result
;
}
\ No newline at end of file
src/biz/report.ts
View file @
95bcc1ca
/**
* 周报告模块
* - 查询
上周
各馆(A/B/C馆)的设备监测数据
* - 查询
最近七天
各馆(A/B/C馆)的设备监测数据
* - 提供周报告 JSON 数据接口
*/
...
...
@@ -47,20 +47,18 @@ function formatDateStr(date: Date): string {
return
`
${
date
.
getFullYear
()}
-
${
pad
(
date
.
getMonth
()
+
1
)}
-
${
pad
(
date
.
getDate
())}
`
;
}
/** 计算
指定周的起止时间(周一~周日
) */
/** 计算
最近七天的起止时间(今天往前推6天
) */
function
calcWeekRange
(
weekOffset
:
number
=
0
):
{
start
:
Date
;
end
:
Date
;
timer
:
string
}
{
const
now
=
new
Date
();
const
dayOfWeek
=
now
.
getDay
();
const
daysToMonday
=
dayOfWeek
===
0
?
8
:
dayOfWeek
+
6
;
// 回溯到上周一
const
monday
=
new
Date
(
now
);
monday
.
setDate
(
now
.
getDate
()
-
daysToMonday
+
weekOffset
*
7
);
monday
.
setHours
(
0
,
0
,
0
,
0
);
const
sunday
=
new
Date
(
monday
);
sunday
.
setDate
(
monday
.
getDate
()
+
6
);
sunday
.
setHours
(
23
,
59
,
59
,
999
);
const
timer
=
`
${
formatChineseDate
(
monday
)}
—
${
formatChineseDate
(
sunday
)}
`
;
return
{
start
:
monday
,
end
:
sunday
,
timer
};
const
end
=
new
Date
(
now
);
end
.
setDate
(
now
.
getDate
()
+
weekOffset
*
7
);
end
.
setHours
(
23
,
59
,
59
,
999
);
const
start
=
new
Date
(
now
);
start
.
setDate
(
now
.
getDate
()
-
6
+
weekOffset
*
7
);
start
.
setHours
(
0
,
0
,
0
,
0
);
const
timer
=
`
${
formatChineseDate
(
start
)}
—
${
formatChineseDate
(
end
)}
`
;
return
{
start
,
end
,
timer
};
}
/** 格式化数字,不足时返回 "——" */
...
...
@@ -217,8 +215,8 @@ function calcMeterDeviceDaily(records: any[]): number {
}
/**
* 查询周报告 JSON 数据
* @param weekOffset 0=
上周(默认), -1=前一周
, ...
* 查询周报告 JSON 数据
(最近七天)
* @param weekOffset 0=
最近七天(默认), -1=再往前七天
, ...
*/
export
async
function
getWeeklyReportData
(
weekOffset
:
number
=
0
):
Promise
<
any
>
{
const
{
start
:
monday
,
end
:
sunday
,
timer
}
=
calcWeekRange
(
weekOffset
);
...
...
@@ -374,3 +372,189 @@ export async function getWeeklyReportData(weekOffset: number = 0): Promise<any>
return
{
timer
,
dataTotal
,
runDatas
};
}
// ======================= 九合一环境监测指标 7 天均值 =======================
/** 九合一核心指标(去掉 pir 人体感应 和 o3 臭氧) */
const
ENV_INDICATOR_KEYS
=
[
{
key
:
"co2"
,
name
:
"二氧化碳"
,
unit
:
"ppm"
},
{
key
:
"pm10"
,
name
:
"PM10"
,
unit
:
"μg/m³"
},
{
key
:
"pm2_5"
,
name
:
"PM2.5"
,
unit
:
"μg/m³"
},
{
key
:
"hcho"
,
name
:
"甲醛"
,
unit
:
"mg/m³"
},
{
key
:
"tvoc"
,
name
:
"TVOC"
,
unit
:
"mg/m³"
},
{
key
:
"temperature"
,
name
:
"温度"
,
unit
:
"℃"
},
{
key
:
"humidity"
,
name
:
"湿度"
,
unit
:
"%"
},
{
key
:
"pressure"
,
name
:
"气压"
,
unit
:
"hPa"
},
{
key
:
"light_level"
,
name
:
"光照度"
,
unit
:
"lux"
},
];
/**
* 查询九合一环境监测指标的 7 天均值(A馆 / B馆 / C馆)
* 对每个馆的环境监测设备,取近 7 天所有上报数据的算术平均
*/
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
);
const
from
=
formatLocalTime
(
start
);
const
to
=
formatLocalTime
(
end
);
const
timer
=
`
${
formatChineseDate
(
start
)}
—
${
formatChineseDate
(
end
)}
`
;
console
.
log
(
`[九合一七日均值] 查询范围:
${
timer
}
`
);
// 2. 并行查询三馆区域
const
hallRegionMap
=
await
queryHallRegions
();
// 3. 并行查询三馆的环境监测设备
const
hallDeviceGroups
=
await
Promise
.
all
(
HALL_NAMES
.
map
(
h
=>
queryHallDeviceGroup
(
hallRegionMap
.
get
(
h
)
||
[]))
);
// 4. 并行查询三馆近 7 天的设备数据
const
hallResults
=
await
Promise
.
all
(
hallDeviceGroups
.
map
(
async
(
group
)
=>
{
const
envDeviceIds
=
group
.
envDevices
.
map
(
d
=>
d
.
device_id
);
if
(
envDeviceIds
.
length
===
0
)
return
null
;
const
dayDataRes
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
{
"%in%"
:
envDeviceIds
},
device_time
:
{
"%gte%"
:
from
,
"%lte%"
:
to
},
},
[
"device_id"
,
"data"
]);
return
dayDataRes
.
data
||
[];
})
);
// 5. 计算每个馆的各指标均值
const
result
:
any
=
{
timer
};
for
(
let
idx
=
0
;
idx
<
HALL_NAMES
.
length
;
idx
++
)
{
const
hallName
=
HALL_NAMES
[
idx
];
const
records
=
hallResults
[
idx
];
const
indicators
:
any
=
{};
if
(
!
records
||
records
.
length
===
0
)
{
// 无数据填 null
for
(
const
ik
of
ENV_INDICATOR_KEYS
)
{
indicators
[
ik
.
key
]
=
{
name
:
ik
.
name
,
value
:
null
,
unit
:
ik
.
unit
};
}
}
else
{
// 统计各指标累加值和计数
const
sums
:
{
[
key
:
string
]:
number
}
=
{};
const
counts
:
{
[
key
:
string
]:
number
}
=
{};
for
(
const
ik
of
ENV_INDICATOR_KEYS
)
{
sums
[
ik
.
key
]
=
0
;
counts
[
ik
.
key
]
=
0
;
}
for
(
const
rec
of
records
)
{
const
d
=
rec
.
data
||
{};
for
(
const
ik
of
ENV_INDICATOR_KEYS
)
{
const
v
=
d
[
ik
.
key
];
if
(
v
!==
undefined
&&
v
!==
null
&&
typeof
v
===
"number"
)
{
sums
[
ik
.
key
]
+=
v
;
counts
[
ik
.
key
]
++
;
}
}
}
for
(
const
ik
of
ENV_INDICATOR_KEYS
)
{
const
avg
=
counts
[
ik
.
key
]
>
0
?
Math
.
round
((
sums
[
ik
.
key
]
/
counts
[
ik
.
key
])
*
100
)
/
100
:
null
;
indicators
[
ik
.
key
]
=
{
name
:
ik
.
name
,
value
:
avg
,
unit
:
ik
.
unit
};
}
}
result
[
hallName
]
=
{
deviceCount
:
hallDeviceGroups
[
idx
].
envDevices
.
length
,
recordCount
:
records
?.
length
||
0
,
indicators
};
}
console
.
log
(
`[九合一七日均值] 完成`
);
return
result
;
}
// ======================= 近 7 天故障信息(按 fault_code 分组) =======================
const
STATUS_MAP
:
{
[
key
:
number
]:
string
}
=
{
0
:
"未处理"
,
1
:
"处理中"
,
2
:
"已解决"
};
const
LEVEL_MAP
:
{
[
key
:
number
]:
string
}
=
{
1
:
"一级"
,
2
:
"二级"
,
3
:
"三级"
};
/**
* 查询近 7 天故障信息,按 fault_code 分组
* 每组展示:故障代码、风险内容、报警时间、处理状态、报警设备
*/
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
);
const
from
=
formatLocalTime
(
start
);
const
to
=
formatLocalTime
(
end
);
const
timer
=
`
${
formatChineseDate
(
start
)}
—
${
formatChineseDate
(
end
)}
`
;
console
.
log
(
`[近7天故障] 查询范围:
${
timer
}
`
);
// 1. 查询近 7 天故障记录(按发生时间倒序)
const
faultRes
=
await
selectDataListByParam
(
TABLENAME
.
设备故障表
,
{
occurred_time
:
{
"%gte%"
:
from
,
"%lte%"
:
to
},
"%orderDesc%"
:
"occurred_time"
,
});
const
faultRecords
=
faultRes
.
data
||
[];
if
(
faultRecords
.
length
===
0
)
{
return
{
timer
,
totalCount
:
0
,
groups
:
[]
};
}
// 2. 收集所有 device_id,查询设备名称
const
deviceIds
=
[...
new
Set
(
faultRecords
.
map
((
f
:
any
)
=>
f
.
device_id
).
filter
(
Boolean
))];
let
deviceNameMap
:
Map
<
string
,
string
>
=
new
Map
();
if
(
deviceIds
.
length
>
0
)
{
const
devRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
device_id
:
{
"%in%"
:
deviceIds
},
},
[
"device_id"
,
"device_name"
]);
for
(
const
d
of
(
devRes
.
data
||
[]))
{
deviceNameMap
.
set
(
d
.
device_id
,
d
.
device_name
||
d
.
device_id
);
}
}
// 3. 按 fault_code 分组
const
groupMap
:
{
[
code
:
string
]:
any
[]
}
=
{};
for
(
const
f
of
faultRecords
)
{
const
code
=
f
.
fault_code
||
"未分类"
;
if
(
!
groupMap
[
code
])
groupMap
[
code
]
=
[];
groupMap
[
code
].
push
({
id
:
f
.
id
,
faultType
:
f
.
fault_type
||
""
,
faultCode
:
code
,
faultDesc
:
f
.
fault_description
||
""
,
alarmTime
:
f
.
occurred_time
?
(
typeof
f
.
occurred_time
===
"string"
?
f
.
occurred_time
:
formatLocalTime
(
new
Date
(
f
.
occurred_time
)))
:
""
,
status
:
f
.
status
??
0
,
statusText
:
STATUS_MAP
[
f
.
status
]
??
"未知"
,
level
:
f
.
level
??
0
,
levelText
:
LEVEL_MAP
[
f
.
level
]
??
"未知"
,
deviceId
:
f
.
device_id
,
deviceName
:
deviceNameMap
.
get
(
f
.
device_id
)
||
f
.
device_id
||
""
,
});
}
// 4. 构建分组结果
const
groups
=
Object
.
entries
(
groupMap
)
.
sort
((
a
,
b
)
=>
b
[
1
].
length
-
a
[
1
].
length
)
// 按故障数量降序
.
map
(([
code
,
items
])
=>
({
faultCode
:
code
,
count
:
items
.
length
,
items
,
}));
const
totalCount
=
faultRecords
.
length
;
console
.
log
(
`[近7天故障] 共
${
totalCount
}
条,
${
groupMap
.
size
}
种故障码`
);
return
{
timer
,
totalCount
,
groupCount
:
Object
.
keys
(
groupMap
).
length
,
groups
};
}
src/biz/running.ts
View file @
95bcc1ca
...
...
@@ -606,11 +606,13 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
for
(
let
devId
of
acDeviceIds
)
{
let
rec
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
devId
,
"%orderDesc%"
:
"device_time"
,
"%limit%"
:
1
},
[
"device_time"
]);
[
"device_time"
,
"data"
]);
if
(
rec
.
data
.
length
)
{
let
lastTime
=
new
Date
(
rec
.
data
[
0
].
device_time
);
let
lastData
=
rec
.
data
[
0
].
data
;
let
power
=
lastData
.
power
??
''
;
let
diffMinutes
=
(
nowTime
.
getTime
()
-
lastTime
.
getTime
())
/
60000
;
deviceLatestMap
.
set
(
devId
,
{
lastTime
,
isOnline
:
diffMinutes
<=
30
});
deviceLatestMap
.
set
(
devId
,
{
lastTime
,
isOnline
:
power
===
'on'
});
}
else
{
deviceLatestMap
.
set
(
devId
,
{
lastTime
:
null
,
isOnline
:
false
});
}
...
...
@@ -710,11 +712,13 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
for
(
let
devId
of
deviceIds
)
{
let
rec
=
await
selectDataListByParam
(
TABLENAME
.
设备数据表
,
{
device_id
:
devId
,
"%orderDesc%"
:
"device_time"
,
"%limit%"
:
1
},
[
"device_time"
]);
[
"device_time"
,
"data"
]);
if
(
rec
.
data
.
length
)
{
let
lastTime
=
new
Date
(
rec
.
data
[
0
].
device_time
);
let
lastData
=
rec
.
data
[
0
].
data
;
let
power
=
lastData
.
power
??
''
;
let
diffMinutes
=
(
nowTime
.
getTime
()
-
lastTime
.
getTime
())
/
60000
;
deviceLatestMap
.
set
(
devId
,
{
lastTime
,
isOnline
:
diffMinutes
<=
30
});
deviceLatestMap
.
set
(
devId
,
{
lastTime
,
isOnline
:
power
===
'on'
});
}
else
{
deviceLatestMap
.
set
(
devId
,
{
lastTime
:
null
,
isOnline
:
false
});
}
...
...
@@ -1135,8 +1139,21 @@ async function controlAcRunningApi(acDevices?: any, workMode?: number, tempSet?:
return
controlResults
;
}
/**
* 设置开关机
* @param regionGroup
* @param regionFloor
* @param regionKey
* @param deviceId
* @param powerStatus
*/
export
async
function
scheduledPowerOn
(
regionGroup
:
string
,
regionType
:
string
,
regionKey
:
number
,
deviceId
:
string
,
powerStatus
:
number
)
{
// 根据regionGroup\regionType\regionKey查询区域信息
// 根据deviceId查询空调设备
// 无传参时,查询region表,通过region表查询device表中device_type=空调的数据
// 循环查询的空调设备,组装空调控制需要参数
// 调用空调控制的第三方接口并记录device_log日志表
}
/**
* 数据集成 - 启动定时任务
...
...
src/biz/schedule.ts
0 → 100644
View file @
95bcc1ca
/**
* 日程管理 业务逻辑
*/
import
{
selectDataListByParam
,
selectDataListToPageByParam
,
selectDataCountByParam
}
from
"../data/findData"
;
import
{
addData
}
from
"../data/addData"
;
import
{
delData
}
from
"../data/delData"
;
import
{
updateManyData
}
from
"../data/updateData"
;
import
{
TABLENAME
}
from
"../config/dbEnum"
;
// ======================= 公共工具 =======================
/** 格式化为本地时间字符串 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
())}
`
;
}
// ======================= 日程列表 =======================
/**
* 查询日程列表(分页 + 按有效期/区域过滤)
*/
export
async
function
getScheduleList
(
params
:
{
pageNumber
?:
number
;
pageSize
?:
number
;
startDate
?:
string
;
endDate
?:
string
;
regionKey
?:
number
;
}):
Promise
<
any
>
{
const
pageNumber
=
params
.
pageNumber
||
1
;
const
pageSize
=
params
.
pageSize
||
10
;
const
{
startDate
,
endDate
,
regionKey
}
=
params
;
console
.
log
(
`[日程列表] page=
${
pageNumber
}
, size=
${
pageSize
}
, startDate=
${
startDate
}
, endDate=
${
endDate
}
, regionKey=
${
regionKey
}
`
);
// 构建日程表查询条件
const
scheduleWhere
:
any
=
{
"%orderDesc%"
:
"id"
};
// 有效期过滤:日程有效时间与查询区间有交集
if
(
startDate
)
{
scheduleWhere
.
begin_validity
=
{
"%lte%"
:
startDate
};
}
if
(
endDate
)
{
scheduleWhere
.
end_validity
=
{
"%gte%"
:
endDate
};
}
// 如果按区域过滤,先查出该区域下的 schedule_id 集合
let
filteredScheduleIds
:
number
[]
|
null
=
null
;
if
(
regionKey
)
{
const
sdRes
=
await
selectDataListByParam
(
TABLENAME
.
日程设备关联表
,
{
region_key
:
regionKey
,
},
[
"schedule_id"
]);
filteredScheduleIds
=
(
sdRes
.
data
||
[]).
map
((
r
:
any
)
=>
r
.
schedule_id
);
if
(
filteredScheduleIds
.
length
===
0
)
{
return
{
total
:
0
,
list
:
[]
};
}
scheduleWhere
.
id
=
{
"%in%"
:
filteredScheduleIds
};
}
// 分页查询
const
columns
=
[
"id"
,
"title"
,
"ac_controls"
,
"schedule_time"
,
"begin_validity"
,
"end_validity"
,
"created_at"
];
const
listRes
=
await
selectDataListToPageByParam
(
TABLENAME
.
日程表
,
scheduleWhere
,
columns
,
pageNumber
,
pageSize
);
const
rows
:
any
[]
=
listRes
.
data
||
[];
// 总数
const
countRes
=
await
selectDataCountByParam
(
TABLENAME
.
日程表
,
scheduleWhere
);
const
total
=
countRes
.
data
||
0
;
// 收集所有日程 id
const
scheduleIds
=
rows
.
map
(
r
=>
r
.
id
);
// 批量查关联的设备关联记录
let
deviceMap
:
{
[
sid
:
number
]:
any
[]
}
=
{};
if
(
scheduleIds
.
length
>
0
)
{
const
sdRes
=
await
selectDataListByParam
(
TABLENAME
.
日程设备关联表
,
{
schedule_id
:
{
"%in%"
:
scheduleIds
},
},
[
"schedule_id"
,
"device_id"
,
"region_key"
]);
const
sdRows
=
sdRes
.
data
||
[];
for
(
const
sd
of
sdRows
)
{
if
(
!
deviceMap
[
sd
.
schedule_id
])
deviceMap
[
sd
.
schedule_id
]
=
[];
deviceMap
[
sd
.
schedule_id
].
push
(
sd
);
}
}
// 批量查区域名称(需要去重 region_key)
const
regionKeySet
=
new
Set
<
number
>
();
for
(
const
sid
of
scheduleIds
)
{
for
(
const
sd
of
(
deviceMap
[
sid
]
||
[]))
{
regionKeySet
.
add
(
sd
.
region_key
);
}
}
let
regionNameMap
:
{
[
rk
:
number
]:
string
}
=
{};
if
(
regionKeySet
.
size
>
0
)
{
const
regionRes
=
await
selectDataListByParam
(
TABLENAME
.
区域表
,
{
id
:
{
"%in%"
:
[...
regionKeySet
]
},
},
[
"id"
,
"name"
]);
for
(
const
r
of
(
regionRes
.
data
||
[]))
{
regionNameMap
[
r
.
id
]
=
r
.
name
;
}
}
// 组装返回列表
const
list
=
rows
.
map
(
row
=>
{
const
devices
=
deviceMap
[
row
.
id
]
||
[];
const
regionNames
=
[...
new
Set
(
devices
.
map
((
d
:
any
)
=>
regionNameMap
[
d
.
region_key
]
||
""
))].
filter
(
Boolean
);
let
validity
=
""
;
if
(
row
.
begin_validity
||
row
.
end_validity
)
{
const
from
=
row
.
begin_validity
?
(
typeof
row
.
begin_validity
===
"string"
?
row
.
begin_validity
.
split
(
" "
)[
0
]
:
formatLocalTime
(
new
Date
(
row
.
begin_validity
)).
split
(
" "
)[
0
])
:
""
;
const
to
=
row
.
end_validity
?
(
typeof
row
.
end_validity
===
"string"
?
row
.
end_validity
.
split
(
" "
)[
0
]
:
formatLocalTime
(
new
Date
(
row
.
end_validity
)).
split
(
" "
)[
0
])
:
""
;
validity
=
`
${
from
}
至
${
to
}
`
;
}
return
{
id
:
row
.
id
,
title
:
row
.
title
,
acControls
:
row
.
ac_controls
,
scheduleTime
:
row
.
schedule_time
||
""
,
validity
,
deviceCount
:
devices
.
length
,
regionNames
,
createdAt
:
row
.
created_at
?
(
typeof
row
.
created_at
===
"string"
?
row
.
created_at
:
formatLocalTime
(
new
Date
(
row
.
created_at
)))
:
""
,
};
});
console
.
log
(
`[日程列表] total=
${
total
}
, rows=
${
list
.
length
}
`
);
return
{
total
,
list
};
}
// ======================= 新增日程 =======================
/**
* 新增日程(含区域设备关联)
*/
export
async
function
addSchedule
(
params
:
{
title
:
string
;
schedule_time
:
string
;
ac_controls
:
string
;
begin_validity
?:
string
;
end_validity
?:
string
;
regionKeys
:
number
[];
}):
Promise
<
any
>
{
const
{
title
,
schedule_time
,
ac_controls
,
begin_validity
,
end_validity
,
regionKeys
}
=
params
;
console
.
log
(
`[新增日程] title=
${
title
}
, time=
${
schedule_time
}
, action=
${
ac_controls
}
, regions=
${
regionKeys
}
`
);
// 1. 插入 schedule 主表
const
scheduleRes
=
await
addData
(
TABLENAME
.
日程表
,
{
title
,
schedule_time
,
ac_controls
,
begin_validity
:
begin_validity
||
null
,
end_validity
:
end_validity
||
null
,
});
// 2. 通过 addData 无法直接获取自增 id,需要反向查询
const
inserted
=
await
selectDataListByParam
(
TABLENAME
.
日程表
,
{
title
,
schedule_time
,
ac_controls
,
"%orderDesc%"
:
"id"
,
"%limit%"
:
1
,
},
[
"id"
]);
const
scheduleId
=
(
inserted
.
data
&&
inserted
.
data
[
0
])
?
inserted
.
data
[
0
].
id
:
null
;
if
(
!
scheduleId
)
{
console
.
warn
(
"[新增日程] 无法获取插入的 schedule id"
);
return
{
isSuccess
:
false
,
msg
:
"日程创建失败"
};
}
// 3. 查这些区域下的所有空调设备
const
deviceRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
region_key
:
{
"%in%"
:
regionKeys
},
device_type
:
"空调"
,
},
[
"device_id"
,
"region_key"
]);
const
devices
=
deviceRes
.
data
||
[];
// 4. 批量插入 schedule_device 关联记录
if
(
devices
.
length
>
0
)
{
const
sdRecords
=
devices
.
map
((
d
:
any
)
=>
({
schedule_id
:
scheduleId
,
device_id
:
d
.
device_id
,
region_key
:
d
.
region_key
,
}));
await
addData
(
TABLENAME
.
日程设备关联表
,
sdRecords
);
console
.
log
(
`[新增日程] 关联设备
${
sdRecords
.
length
}
条`
);
}
else
{
// 无设备时也插入一条区域占位记录(允许区域下暂时无设备)
for
(
const
rk
of
regionKeys
)
{
await
addData
(
TABLENAME
.
日程设备关联表
,
{
schedule_id
:
scheduleId
,
device_id
:
""
,
region_key
:
rk
,
});
}
console
.
log
(
`[新增日程] 区域
${
regionKeys
.
length
}
个,暂无设备`
);
}
return
{
isSuccess
:
true
,
scheduleId
};
}
// ======================= 日程详情 =======================
/**
* 获取日程详情(含关联区域/设备)
*/
export
async
function
getScheduleDetail
(
scheduleId
:
number
):
Promise
<
any
>
{
console
.
log
(
`[日程详情] scheduleId=
${
scheduleId
}
`
);
// 1. 查日程主表
const
scheduleRes
=
await
selectDataListByParam
(
TABLENAME
.
日程表
,
{
id
:
scheduleId
,
});
const
rows
=
scheduleRes
.
data
||
[];
if
(
rows
.
length
===
0
)
{
return
null
;
}
const
row
=
rows
[
0
];
// 2. 查关联设备
const
sdRes
=
await
selectDataListByParam
(
TABLENAME
.
日程设备关联表
,
{
schedule_id
:
scheduleId
,
},
[
"device_id"
,
"region_key"
]);
const
sdRows
=
sdRes
.
data
||
[];
const
selectedRegions
=
[...
new
Set
(
sdRows
.
map
((
r
:
any
)
=>
r
.
region_key
))];
const
selectedDevices
=
sdRows
.
filter
((
r
:
any
)
=>
r
.
device_id
).
map
((
r
:
any
)
=>
r
.
device_id
);
return
{
id
:
row
.
id
,
title
:
row
.
title
,
scheduleTime
:
row
.
schedule_time
||
""
,
acControls
:
row
.
ac_controls
,
beginValidity
:
row
.
begin_validity
?
(
typeof
row
.
begin_validity
===
"string"
?
row
.
begin_validity
.
split
(
" "
)[
0
]
:
formatLocalTime
(
new
Date
(
row
.
begin_validity
)).
split
(
" "
)[
0
])
:
""
,
endValidity
:
row
.
end_validity
?
(
typeof
row
.
end_validity
===
"string"
?
row
.
end_validity
.
split
(
" "
)[
0
]
:
formatLocalTime
(
new
Date
(
row
.
end_validity
)).
split
(
" "
)[
0
])
:
""
,
selectedRegions
,
selectedDevices
,
};
}
// ======================= 编辑日程 =======================
/**
* 编辑日程(更新主表 + 重建关联)
*/
export
async
function
editSchedule
(
params
:
{
scheduleId
:
number
;
title
:
string
;
schedule_time
:
string
;
ac_controls
:
string
;
begin_validity
?:
string
;
end_validity
?:
string
;
regionKeys
:
number
[];
}):
Promise
<
any
>
{
const
{
scheduleId
,
title
,
schedule_time
,
ac_controls
,
begin_validity
,
end_validity
,
regionKeys
}
=
params
;
console
.
log
(
`[编辑日程] id=
${
scheduleId
}
, title=
${
title
}
`
);
// 1. 更新 schedule 主表
await
updateManyData
(
TABLENAME
.
日程表
,
{
id
:
scheduleId
},
{
title
,
schedule_time
,
ac_controls
,
begin_validity
:
begin_validity
||
null
,
end_validity
:
end_validity
||
null
,
});
// 2. 删除旧的关联记录
await
delData
(
TABLENAME
.
日程设备关联表
,
{
schedule_id
:
scheduleId
});
// 3. 查区域下的空调设备
const
deviceRes
=
await
selectDataListByParam
(
TABLENAME
.
设备表
,
{
region_key
:
{
"%in%"
:
regionKeys
},
device_type
:
"空调"
,
},
[
"device_id"
,
"region_key"
]);
const
devices
=
deviceRes
.
data
||
[];
// 4. 重建关联
if
(
devices
.
length
>
0
)
{
const
sdRecords
=
devices
.
map
((
d
:
any
)
=>
({
schedule_id
:
scheduleId
,
device_id
:
d
.
device_id
,
region_key
:
d
.
region_key
,
}));
await
addData
(
TABLENAME
.
日程设备关联表
,
sdRecords
);
console
.
log
(
`[编辑日程] 重建关联设备
${
sdRecords
.
length
}
条`
);
}
else
{
for
(
const
rk
of
regionKeys
)
{
await
addData
(
TABLENAME
.
日程设备关联表
,
{
schedule_id
:
scheduleId
,
device_id
:
""
,
region_key
:
rk
,
});
}
console
.
log
(
`[编辑日程] 区域
${
regionKeys
.
length
}
个,暂无设备`
);
}
return
{
isSuccess
:
true
};
}
// ======================= 删除日程 =======================
/**
* 删除日程(含关联记录)
*/
export
async
function
deleteSchedule
(
scheduleId
:
number
):
Promise
<
any
>
{
console
.
log
(
`[删除日程] id=
${
scheduleId
}
`
);
// 1. 删除关联记录
await
delData
(
TABLENAME
.
日程设备关联表
,
{
schedule_id
:
scheduleId
});
// 2. 删除主记录
await
delData
(
TABLENAME
.
日程表
,
{
id
:
scheduleId
});
return
{
isSuccess
:
true
};
}
src/config/dbEnum.ts
View file @
95bcc1ca
...
...
@@ -8,7 +8,9 @@ export enum TABLENAME {
区域表
=
'region'
,
设备故障表
=
'device_fault'
,
用户信息表
=
'user_info'
,
设备日志表
=
'device_logs'
设备日志表
=
'device_logs'
,
日程表
=
'schedule'
,
日程设备关联表
=
'schedule_device'
};
/**
...
...
src/config/mysqlTableConfig.ts
View file @
95bcc1ca
...
...
@@ -337,5 +337,118 @@ export const TablesConfig = [
association
:
[
]
},
// 日程表
{
tableNameCn
:
'日程表'
,
tableName
:
'schedule'
,
schema
:
{
id
:
{
type
:
DataTypes
.
BIGINT
,
allowNull
:
false
,
primaryKey
:
true
,
autoIncrement
:
true
,
comment
:
'自增主键'
},
title
:
{
type
:
Sequelize
.
STRING
(
255
),
allowNull
:
false
,
comment
:
'日程标题'
},
schedule_date
:
{
type
:
DataTypes
.
DATE
,
allowNull
:
false
,
comment
:
'日期'
},
ac_controls
:
{
type
:
Sequelize
.
STRING
(
50
),
allowNull
:
false
,
comment
:
'操作状态:off关on开'
},
todo_item
:
{
type
:
DataTypes
.
JSON
,
allowNull
:
true
,
comment
:
'待办事项,除了开关以外可以设定模式、温度等。例:{"温度":"24.5","模式":"制冷","风速":"低风"}'
},
schedule_time
:
{
type
:
Sequelize
.
STRING
(
50
),
allowNull
:
true
,
comment
:
'定时时间,如08:30:00'
},
set_conditions
:
{
type
:
DataTypes
.
JSON
,
allowNull
:
true
,
comment
:
'设定条件,除了定时以外可以通过条件开启或关闭设备。例:{"温度":"≤26&≥22"}'
},
begin_validity
:
{
type
:
DataTypes
.
DATE
,
allowNull
:
true
,
comment
:
'有效时间起,不设置则为空'
},
end_validity
:
{
type
:
DataTypes
.
DATE
,
allowNull
:
true
,
comment
:
'有效时间止,不设置则为空'
},
outward_time
:
{
type
:
Sequelize
.
STRING
(
200
),
allowNull
:
true
,
comment
:
'有效除外时间,多个通过逗号分隔'
},
created_at
:
{
type
:
DataTypes
.
DATE
,
allowNull
:
false
,
defaultValue
:
Sequelize
.
NOW
,
comment
:
'创建时间'
},
updated_at
:
{
type
:
DataTypes
.
DATE
,
allowNull
:
false
,
defaultValue
:
Sequelize
.
NOW
,
comment
:
'修改时间'
}
},
association
:
[
{
type
:
"hasMany"
,
target
:
"schedule_device"
,
foreignKey
:
"schedule_id"
}
]
},
// 日程设备关联表
{
tableNameCn
:
'日程设备关联表'
,
tableName
:
'schedule_device'
,
schema
:
{
id
:
{
type
:
DataTypes
.
BIGINT
,
allowNull
:
false
,
primaryKey
:
true
,
autoIncrement
:
true
,
comment
:
'自增主键'
},
schedule_id
:
{
type
:
DataTypes
.
BIGINT
,
allowNull
:
false
,
comment
:
'日程ID,关联schedule.id'
},
device_id
:
{
type
:
DataTypes
.
STRING
(
50
),
allowNull
:
false
,
comment
:
'设备ID,关联device.device_id'
},
region_key
:
{
type
:
DataTypes
.
BIGINT
,
allowNull
:
false
,
comment
:
'区域key,关联region.id,表示设备所属区域'
},
created_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"
}
]
},
];
src/routers/admin.ts
0 → 100644
View file @
95bcc1ca
/**
* 后端管理系统 路由
*/
import
asyncHandler
from
"express-async-handler"
;
import
*
as
reportBiz
from
"../biz/report"
;
import
*
as
regionBiz
from
"../biz/region"
;
import
*
as
scheduleBiz
from
"../biz/schedule"
;
export
function
setRouter
(
httpServer
)
{
/** 获取周报告JSON数据 */
httpServer
.
get
(
"/api/qdm/report/weekly/data"
,
asyncHandler
(
getReportData
));
/** 获取九合一环境监测指标 7 天均值(A/B/C馆) */
httpServer
.
get
(
"/api/qdm/report/env/weekly/avg"
,
asyncHandler
(
getEnvWeeklyAvg
));
/** 获取近 7 天故障信息(按 fault_code 分组) */
httpServer
.
get
(
"/api/qdm/report/fault/weekly"
,
asyncHandler
(
getFaultInfo
));
/** 区域设备树 */
httpServer
.
get
(
"/api/qdm/admin/region/device"
,
asyncHandler
(
getRegionDevices
));
/** 日程管理列表 */
httpServer
.
get
(
"/api/qdm/admin/schedule/list"
,
asyncHandler
(
getScheduleList
));
/** 日程管理详情 */
httpServer
.
get
(
"/api/qdm/admin/schedule/detail"
,
asyncHandler
(
getScheduleDetail
));
/** 日程管理新增 */
httpServer
.
post
(
"/api/qdm/admin/schedule/add"
,
asyncHandler
(
addSchedule
));
/** 日程管理编辑 */
httpServer
.
post
(
"/api/qdm/admin/schedule/edit"
,
asyncHandler
(
editSchedule
));
/** 日程管理删除 */
httpServer
.
post
(
"/api/qdm/admin/schedule/delete"
,
asyncHandler
(
deleteSchedule
));
}
/**
* GET /api/qdm/report/weekly/data?weekOffset=0
* 返回周报告 JSON 数据供前端页面渲染
*/
async
function
getReportData
(
req
,
res
)
{
const
weekOffset
=
Number
(
req
.
query
?.
weekOffset
)
||
0
;
const
data
=
await
reportBiz
.
getWeeklyReportData
(
weekOffset
);
res
.
success
(
data
);
}
/**
* GET /api/qdm/report/env/weekly/avg
* 返回九合一环境监测指标 7 天均值(A馆 / B馆 / C馆)
*/
async
function
getEnvWeeklyAvg
(
req
,
res
)
{
const
data
=
await
reportBiz
.
getEnvIndicatorsWeeklyAvg
();
res
.
success
(
data
);
}
/**
* GET /api/qdm/report/fault/weekly
* 返回近 7 天故障信息,按 fault_code 分组
*/
async
function
getFaultInfo
(
req
,
res
)
{
const
data
=
await
reportBiz
.
getFaultInfoLast7Days
();
res
.
success
(
data
);
}
// ======================= 区域设备 =======================
/**
* GET /api/qdm/admin/region/device
* 获取区域设备树(馆 → 层 → 区域 → 设备)
*/
async
function
getRegionDevices
(
req
,
res
)
{
const
data
=
await
regionBiz
.
getRegionDevices
();
res
.
success
(
data
);
}
// ======================= 日程管理 =======================
/**
* GET /api/qdm/admin/schedule/list
* 日程列表(分页 + 有效期 + 区域过滤)
* query: pageNumber, pageSize, startDate, endDate, regionKey
*/
async
function
getScheduleList
(
req
,
res
)
{
const
data
=
await
scheduleBiz
.
getScheduleList
({
pageNumber
:
Number
(
req
.
query
?.
pageNumber
)
||
1
,
pageSize
:
Number
(
req
.
query
?.
pageSize
)
||
10
,
startDate
:
req
.
query
?.
startDate
,
endDate
:
req
.
query
?.
endDate
,
regionKey
:
req
.
query
?.
regionKey
?
Number
(
req
.
query
.
regionKey
)
:
undefined
,
});
res
.
success
(
data
);
}
/**
* GET /api/qdm/admin/schedule/detail
* 日程详情(含关联区域/设备)
* query: scheduleId
*/
async
function
getScheduleDetail
(
req
,
res
)
{
const
scheduleId
=
Number
(
req
.
query
?.
scheduleId
);
if
(
!
scheduleId
)
{
res
.
fail
(
"缺少参数 scheduleId"
);
return
;
}
const
data
=
await
scheduleBiz
.
getScheduleDetail
(
scheduleId
);
if
(
!
data
)
{
res
.
fail
(
"日程不存在"
);
return
;
}
res
.
success
(
data
);
}
/**
* POST /api/qdm/admin/schedule/add
* 新增日程
* body: title, scheduleTime, acControls, beginValidity, endValidity, regionKeys
*/
async
function
addSchedule
(
req
,
res
)
{
const
{
title
,
scheduleTime
,
acControls
,
beginValidity
,
endValidity
,
regionKeys
}
=
req
.
body
||
{};
if
(
!
title
||
!
scheduleTime
||
!
acControls
)
{
res
.
fail
(
"缺少必填参数:title, scheduleTime, acControls"
);
return
;
}
if
(
!
regionKeys
||
!
Array
.
isArray
(
regionKeys
)
||
regionKeys
.
length
===
0
)
{
res
.
fail
(
"至少选择一个区域"
);
return
;
}
const
data
=
await
scheduleBiz
.
addSchedule
({
title
,
schedule_time
:
scheduleTime
,
ac_controls
:
acControls
,
begin_validity
:
beginValidity
||
undefined
,
end_validity
:
endValidity
||
undefined
,
regionKeys
,
});
res
.
success
(
data
);
}
/**
* POST /api/qdm/admin/schedule/edit
* 编辑日程
* body: scheduleId, title, scheduleTime, acControls, beginValidity, endValidity, regionKeys
*/
async
function
editSchedule
(
req
,
res
)
{
const
{
scheduleId
,
title
,
scheduleTime
,
acControls
,
beginValidity
,
endValidity
,
regionKeys
}
=
req
.
body
||
{};
if
(
!
scheduleId
||
!
title
||
!
scheduleTime
||
!
acControls
)
{
res
.
fail
(
"缺少必填参数:scheduleId, title, scheduleTime, acControls"
);
return
;
}
if
(
!
regionKeys
||
!
Array
.
isArray
(
regionKeys
)
||
regionKeys
.
length
===
0
)
{
res
.
fail
(
"至少选择一个区域"
);
return
;
}
const
data
=
await
scheduleBiz
.
editSchedule
({
scheduleId
:
Number
(
scheduleId
),
title
,
schedule_time
:
scheduleTime
,
ac_controls
:
acControls
,
begin_validity
:
beginValidity
||
undefined
,
end_validity
:
endValidity
||
undefined
,
regionKeys
,
});
res
.
success
(
data
);
}
/**
* POST /api/qdm/admin/schedule/delete
* 删除日程
* body: scheduleId
*/
async
function
deleteSchedule
(
req
,
res
)
{
const
{
scheduleId
}
=
req
.
body
||
{};
if
(
!
scheduleId
)
{
res
.
fail
(
"缺少参数 scheduleId"
);
return
;
}
const
data
=
await
scheduleBiz
.
deleteSchedule
(
Number
(
scheduleId
));
res
.
success
(
data
);
}
src/routers/report.ts
deleted
100644 → 0
View file @
18e87f61
/**
* 周报告 API 路由
*/
import
asyncHandler
from
"express-async-handler"
;
import
*
as
reportBiz
from
"../biz/report"
;
export
function
setRouter
(
httpServer
)
{
/** 获取周报告JSON数据 */
httpServer
.
get
(
"/api/qdm/report/weekly/data"
,
asyncHandler
(
getReportData
));
}
/**
* GET /api/qdm/report/weekly/data?weekOffset=0
* 返回周报告 JSON 数据供前端页面渲染
*/
async
function
getReportData
(
req
,
res
)
{
const
weekOffset
=
Number
(
req
.
query
?.
weekOffset
)
||
0
;
const
data
=
await
reportBiz
.
getWeeklyReportData
(
weekOffset
);
res
.
success
(
data
);
}
src/routers/router.ts
View file @
95bcc1ca
...
...
@@ -5,13 +5,13 @@
import
*
as
usersRouter
from
'./users'
;
import
*
as
deviceRouter
from
'./device'
;
import
*
as
feiyiClientRouter
from
'./feiyiClient'
;
import
*
as
reportRouter
from
'./report
'
;
import
*
as
adminRouter
from
'./admin
'
;
export
function
setRouter
(
httpServer
)
{
usersRouter
.
setRouter
(
httpServer
);
deviceRouter
.
setRouter
(
httpServer
);
feiyiClientRouter
.
setRouter
(
httpServer
);
report
Router
.
setRouter
(
httpServer
);
admin
Router
.
setRouter
(
httpServer
);
}
...
...
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