运行分析展示数据问题修复、日程管理定时开关机任务创建和环境质量弹窗接口

parent e110cac7
......@@ -418,7 +418,8 @@ export async function getEnvironmentDevices() {
}
const regionResult = await selectDataListByParam(TABLENAME.区域表, {
id: { "%in%": regionKeys }
id: { "%in%": regionKeys },
"%orderAsc%": "sort_order",
}, ["id", "name", "type", "groups"]);
const regions = regionResult.data || [];
......@@ -569,6 +570,7 @@ export async function getEnvironmentData(regionGroup: string, regionType: string
if (deviceRegionKeys.length > 0) {
const regionResult = await selectDataListByParam(TABLENAME.区域表, {
id: { "%in%": deviceRegionKeys },
"%orderAsc%": "sort_order",
}, ["id", "name", "type", "groups"]);
(regionResult.data || []).forEach((r: any) => {
regionNameMap[r.id] = r.name || '';
......@@ -622,4 +624,131 @@ export async function getEnvironmentData(regionGroup: string, regionType: string
};
return { indicators, records, areaInfo };
}
/**
* CO2 线性超标描述(仅一般/差级别调用)
* 偏高:1000~1500 超标:1500~2500 严重超标:>2500
*/
function getCo2ExceedLabel(co2: number): string | null {
if (co2 > 2500) return '二氧化碳严重超标';
if (co2 > 1500) return '二氧化碳超标';
if (co2 >= 1000) return '二氧化碳偏高';
return null;
}
/**
* PM2.5 线性超标描述(仅一般/差级别调用)
* 偏高:50~75 超标:75~95 严重超标:>95
*/
function getPm25ExceedLabel(pm25: number): string | null {
if (pm25 > 95) return 'PM2.5严重超标';
if (pm25 > 75) return 'PM2.5超标';
if (pm25 > 50) return 'PM2.5偏高';
return null;
}
/**
* 运行分析 - 环境质量弹窗
* 按区域查询环境监测设备最新数据,返回超标区域(一般/差)。
* 质量等级与 todayEnvironmental 一致,优秀/良好不返回。
*/
export async function getEnvQualityPop(regionGroup?: string, regionType?: string) {
// 1. 查询区域
const regionWhere: any = {};
if (regionGroup) regionWhere.groups = regionGroup;
if (regionType) regionWhere.type = regionType;
const regionResult = await selectDataListByParam(TABLENAME.区域表, { ...regionWhere, "%orderAsc%": "sort_order" }, ["id", "name", "type", "groups"]);
const regions = regionResult.data || [];
if (regions.length === 0) return { total: 0, dataList: [] };
const regionMap: { [id: number]: any } = {};
regions.forEach((r: any) => { regionMap[r.id] = r; });
const regionKeys = regions.map((r: any) => r.id);
// 2. 查询环境监测设备
const deviceResult = await selectDataListByParam(TABLENAME.设备表, {
region_key: { "%in%": regionKeys },
device_type: "环境监测"
}, ["device_id", "region_key"]);
const devices = deviceResult.data || [];
// 3. 按 region_key 分组
const regionDevices: { [rk: number]: string[] } = {};
devices.forEach((d: any) => {
if (!regionDevices[d.region_key]) regionDevices[d.region_key] = [];
regionDevices[d.region_key].push(d.device_id);
});
const dataList: any[] = [];
const todayStart = getTodayStart();
// 4. 并行处理有设备的区域
await Promise.all(Object.keys(regionDevices).map(async (rkStr) => {
const rk = Number(rkStr);
const deviceIds = regionDevices[rk];
const region = regionMap[rk];
if (!region || deviceIds.length === 0) return;
// 仅查询今天的数据,按 device_time 降序,每个设备最多取一条
const dataResult = await selectDataListByParam(TABLENAME.设备数据表, {
device_id: { "%in%": deviceIds },
device_time: { "%gte%": todayStart },
"%orderDesc%": "device_time",
"%limit%": deviceIds.length
}, ["device_id", "data"]);
const records = dataResult.data || [];
if (records.length === 0) return;
// 每个设备取最新一条,算均值
const deviceLatest: { [devId: string]: any } = {};
records.forEach((r: any) => {
if (!deviceLatest[r.device_id]) deviceLatest[r.device_id] = r.data;
});
const dataArr = Object.values(deviceLatest);
const co2Avg = Math.round(dataArr.reduce((s, d: any) => s + (d.co2 ?? 0), 0) / dataArr.length);
const pm25Avg = Math.round(dataArr.reduce((s, d: any) => s + (d.pm2_5 ?? 0), 0) / dataArr.length);
// 质量等级判定(与 todayEnvironmental 一致)
let quality: string;
if (co2Avg < 800 && pm25Avg <= 35) {
quality = '优秀';
} else if (co2Avg < 1000 && pm25Avg <= 45) {
quality = '优秀';
} else if (co2Avg <= 1000 && pm25Avg <= 50) {
quality = '良好';
} else if (co2Avg > 1000 && co2Avg < 1500 && pm25Avg > 50 && pm25Avg < 75) {
quality = '一般';
} else {
quality = '差';
}
// 仅返回一般/差
if (quality === '优秀' || quality === '良好') return;
// 构建超标内容
const exceedItems: string[] = [];
const co2Label = getCo2ExceedLabel(co2Avg);
const pm25Label = getPm25ExceedLabel(pm25Avg);
if (co2Label) exceedItems.push(co2Label);
if (pm25Label) exceedItems.push(pm25Label);
const qualityContent = exceedItems.length > 0
? `环境质量${quality}${exceedItems.join('、')}`
: `环境质量${quality}`;
dataList.push({
indexs: 0,
regionName: [region.groups, region.type, region.name].filter(Boolean).join('_'),
co2: co2Avg,
pm25: pm25Avg,
qualityContent
});
}));
// 5. 排序并分配序号
dataList.sort((a, b) => a.regionName.localeCompare(b.regionName, 'zh'));
dataList.forEach((item, i) => { item.indexs = i + 1; });
return { total: dataList.length, dataList };
}
\ No newline at end of file
import { selectDataListByParam } from "../data/findData";
import { TABLENAME } from "../config/dbEnum";
import { updateManyData } from "../data/updateData";
/** 楼栋排序基础值(间隔100,便于中间插入) */
const BUILDING_BASE: Record<string, number> = {
'A馆': 100,
'B馆': 200,
'C馆': 300,
};
/** 楼层排序偏移值 */
const FLOOR_OFFSET: Record<string, number> = {
'一层': 1,
'二层': 2,
'三层': 3,
'四层': 4,
};
/**
* 根据楼栋和楼层计算 sort_order 值
* 排序规则:A馆 > B馆 > C馆 > 其它;一层 > 二层 > 三层 > 四层 > 其它
*/
export function computeSortOrder(groups: string, type: string): number {
const buildingBase = BUILDING_BASE[groups] ?? 900;
const floorOffset = FLOOR_OFFSET[type] ?? 99;
return buildingBase + floorOffset;
}
/**
* 刷新所有区域的 sort_order(系统启动时执行)
* 遍历所有区域,根据 groups + type 重新计算并更新 sort_order
*/
export async function refreshRegionSortOrder(): Promise<void> {
const regionList = await selectDataListByParam(
TABLENAME.区域表,
{},
["id", "groups", "type", "sort_order"],
);
const regions = regionList.data || [];
let updatedCount = 0;
for (const r of regions) {
const newVal = computeSortOrder(r.groups, r.type);
if (r.sort_order !== newVal) {
await updateManyData(TABLENAME.区域表, { id: r.id }, { sort_order: newVal });
updatedCount++;
}
}
console.log(`sort_order 刷新完成,共更新 ${updatedCount} 条区域记录`);
}
/**
* 获取楼栋区域列表(供前端下拉选择)
......
......@@ -24,7 +24,7 @@ function formatLocalTime(date: Date): string {
/** 并行查询三馆区域,按 groups 分组 */
async function queryHallRegions(): Promise<Map<string, number[]>> {
const results = await Promise.all(
HALL_NAMES.map(g => selectDataListByParam(TABLENAME.区域表, { groups: g }, ["id"]))
HALL_NAMES.map(g => selectDataListByParam(TABLENAME.区域表, { groups: g, "%orderAsc%": "sort_order" }, ["id"]))
);
const hallRegionMap = new Map<string, number[]>();
for (let i = 0; i < HALL_NAMES.length; i++) {
......@@ -112,7 +112,7 @@ async function queryHallDeviceGroup(regionIds: number[]): Promise<HallDeviceGrou
// 并行查询该馆设备和区域名称
const [allDevRes, regionRes] = await Promise.all([
selectDataListByParam(TABLENAME.设备表, { region_key: { "%in%": regionIds } }, ["device_id", "device_name", "device_type", "region_key"]),
selectDataListByParam(TABLENAME.区域表, { id: { "%in%": regionIds } }, ["id", "name"]),
selectDataListByParam(TABLENAME.区域表, { id: { "%in%": regionIds }, "%orderAsc%": "sort_order" }, ["id", "name"]),
]);
const nameMap = new Map<number, string>();
......
......@@ -43,7 +43,7 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
let resultAny: any = { };
// 根据区域类型和名称筛选设备ID列表(如果提供了区域信息)
let regionParam: any = { groups: regionGroup };
let regionParam: any = { groups: regionGroup, "%orderAsc%": "sort_order" };
if (regionType) {
// regionParam.type = regionTypeKeyMap[regionType];
regionParam.type = regionType;
......@@ -381,7 +381,7 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
}
if (regionKeySet.size > 0) {
const regionsRes = await selectDataListByParam(TABLENAME.区域表,
{ id: { "%in%": [...regionKeySet] } },
{ id: { "%in%": [...regionKeySet] }, "%orderAsc%": "sort_order" },
["id", "name"]);
const regionNameMap = new Map<string, string>();
for (const r of regionsRes.data) {
......@@ -422,10 +422,10 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
let regionInfos: any = [];
if (regionKeys.length === 0) {
// 6.1.1 若没有选择区域,则查询区域表获取所有区域类型
regionInfos = await selectDataListByParam(TABLENAME.区域表, {}, ["id", "room_id", "name", "type", "groups"]);
regionInfos = await selectDataListByParam(TABLENAME.区域表, { "%orderAsc%": "sort_order" }, ["id", "room_id", "name", "type", "groups"]);
} else {
// 6.1.2 若选择了区域,则查询选择的区域
regionInfos = await selectDataListByParam(TABLENAME.区域表, { id: { "%in%": regionKeys } }, ["id", "room_id", "name", "type", "groups"]);
regionInfos = await selectDataListByParam(TABLENAME.区域表, { id: { "%in%": regionKeys }, "%orderAsc%": "sort_order" }, ["id", "room_id", "name", "type", "groups"]);
}
// 6.2 循环区域查询客流传感器设备,获取过去24小时每个区域的客流量
......@@ -472,6 +472,7 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
if (!regionCustomerMap.has(regionName)) {
regionCustomerMap.set(regionName, netVisitor);
} else {
totalNum = regionCustomerMap.get(regionName);
regionCustomerMap.set(regionName, totalNum + netVisitor);
}
}
......@@ -487,7 +488,7 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
// 计算每个区域的饱和度(客流量/区域最大承载量),并转换成表格
for (let [regionName, customerNum] of regionCustomerMap.entries()) {
let regionMaxCustomer = regionMaxCustomerMap.get(regionName) || 1000; // 默认最大承载量为1000
let saturation = `${((customerNum / regionMaxCustomer) * 100).toFixed(2)}%`;
let saturation = `${((customerNum / regionMaxCustomer) * 100)>100?100:((customerNum / regionMaxCustomer) * 100).toFixed(2)}%`;
regionCustomerTable.push({ regionName, customerNum, saturation });
todayCustomer += customerNum;
}
......@@ -684,7 +685,7 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
// 运行监控总数据(一天)
resultAny.mainRunningManagement = {
totalCustomerNumber: totalRegionCustomerNum,
totalDeviceOnlineRate: (acOnlineCount/acCount*100).toFixed(1),
totalDeviceOnlineRate: (acOnlineCount/acCount*100)>=100?'100':(acOnlineCount/acCount*100).toFixed(1),
// totalElectricity: last24hRunningEnergy.toFixed(0).slice(0, 5),
totalElectricity: last24hRunningEnergy.toFixed(0),
};
......
......@@ -102,6 +102,7 @@ export async function getScheduleList(params: {
if (regionKeySet.size > 0) {
const regionRes = await selectDataListByParam(TABLENAME.区域表, {
id: { "%in%": [...regionKeySet] },
"%orderAsc%": "sort_order",
}, ["id", "name"]);
for (const r of (regionRes.data || [])) {
regionNameMap[r.id] = r.name;
......
......@@ -4,6 +4,8 @@ import * as mqttSrv from "./service/mqttInit";
import { initMysqlModel } from "./model/sqlModelBind";
import { httpServer } from "./net/http_server";
import { startSchedule, startAlertSchedule, startFaultStatusSchedule } from "./config/schedule";
import { initScheduleTasks } from "./biz/scheduleExecutor";
import { refreshRegionSortOrder } from "./biz/region";
async function lanuch() {
......@@ -12,6 +14,8 @@ async function lanuch() {
/**初始化sql */
await mysqlDB.initMysqlDB();
await initMysqlModel();
/**刷新区域排序 */
await refreshRegionSortOrder();
/**创建http服务 */
httpServer.createServer(systemConfig.port);
/**启动MQTT订阅服务 */
......@@ -22,6 +26,8 @@ async function lanuch() {
startAlertSchedule();
/**启动故障状态更新定时任务(5分钟) */
startFaultStatusSchedule();
/**启动日程执行器 */
await initScheduleTasks();
console.log('This indicates that the server is started successfully.');
......
......@@ -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 { reloadScheduleTasks } from "../biz/scheduleExecutor";
import { eccReqParamater } from "../util/verificationParam";
export function setRouter(httpServer) {
......@@ -134,6 +135,8 @@ async function addSchedule(req, res) {
regionKeys: regionKeys ?? []
});
res.success(data);
// 新增日程后重载执行器
reloadScheduleTasks().catch((err) => console.error("[日程重载] 新增后重载失败:", err));
}
/**
......@@ -164,6 +167,8 @@ async function editSchedule(req, res) {
regionKeys: regionKeys ?? []
});
res.success(data);
// 编辑日程后重载执行器
reloadScheduleTasks().catch((err) => console.error("[日程重载] 编辑后重载失败:", err));
}
/**
......@@ -179,4 +184,6 @@ async function deleteSchedule(req, res) {
}
const data = await scheduleBiz.deleteSchedule(Number(scheduleId));
res.success(data);
// 删除日程后重载执行器
reloadScheduleTasks().catch((err) => console.error("[日程重载] 删除后重载失败:", err));
}
......@@ -32,15 +32,17 @@ export function setRouter(httpServer) {
httpServer.post('/api/qdm/run/monitor/acPop', asyncHandler(getAcRunningPop));
/** 空调开关 */
httpServer.post('/api/qdm/run/monitor/acRun', asyncHandler(controlAcRunning));
/** 运行分析 - 环境质量弹窗 */
httpServer.post('/api/qdm/run/analysis/envPop', asyncHandler(getRunAnalysisEnvPop));
/** 启动数据集成 */
httpServer.post('/api/qdm/run/start/schedule', asyncHandler(startSchedule));
/** 停止数据集成 */
httpServer.post('/api/qdm/run/stop/schedule', asyncHandler(stopSchedule));
/** 获取环境监测设备 */
/** IAQ-获取环境监测设备 */
httpServer.post('/api/qdm/device/envDevice', asyncHandler(getEnvironmentDevices));
/** 获取环境监测数据 */
/** IAQ-获取环境监测数据 */
httpServer.post('/api/qdm/device/environment', asyncHandler(getEnvironmentData));
}
......@@ -184,6 +186,17 @@ async function getEnvironmentData(req, res) {
res.success(result);
}
/**
* 运行分析 - 环境质量弹窗
*/
async function getRunAnalysisEnvPop(req, res) {
let reqConf = { regionGroup: 'String', regionType: 'String' };
const NotMustHaveKeys = ["regionGroup", "regionType"];
let { regionGroup, regionType } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await deviceBiz.getEnvQualityPop(regionGroup, regionType);
res.success(result);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment