日程定时开关机管理页面联调

parent 95bcc1ca
......@@ -376,6 +376,14 @@ async function integrateAcDevices() {
return;
}
// 查询未处理的故障设备信息
const faultDevices = await deviceModel.findAll({
where: { device_type: '空调', device_state: 2 },
raw: true,
});
console.log(`[空调设备集成] 共 ${faultDevices.length} 个故障设备`);
let deviceNomalIds = [];
// 分页拉取所有内机数据
const rows = await fetchAllPages((page, limit) => getIndoorUnitList({ page, limit }));
console.log(`[空调设备集成] 共获取 ${rows.length} 条内机数据`);
......@@ -393,6 +401,19 @@ async function integrateAcDevices() {
const existing = await deviceModel.findOne({ where: { device_id: deviceId } });
if (existing) {
skipCount++;
// 更新设备状态
const newState = item.state ?? 0;
if (existing.device_state !== newState) {
await deviceModel.update(
{ device_state: newState, updated_at: new Date() },
{ where: { id: existing.id } },
);
}
// 校验故障设备是否已恢复
if (faultDevices.find(d => d.device_id === deviceId) && newState === 0) {
console.log(`[空调设备集成] 故障设备 ${deviceId} 已恢复`);
deviceNomalIds.push(deviceId);
}
continue;
}
......@@ -424,9 +445,24 @@ async function integrateAcDevices() {
device_type: deviceType,
device_name: `${deviceName}-${deviceId}`,
control_params: AC_CONTROL_PARAMS,
device_state: item.state ?? 0
});
insertCount++;
}
// 根据deviceNomalIds更新故障信息为正常
if (deviceNomalIds.length > 0) {
const faultModel = mysqlModelMap['device_fault'];
if (faultModel) {
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
const now = new Date();
await faultModel.update(
{ status: 2, resolved_time: now, updated_at: now },
{ where: { device_id: { [Op.in]: deviceNomalIds }, status: { [Op.ne]: 2 } } },
);
console.log(`[空调设备集成] 批量解决 ${deviceNomalIds.length} 台设备故障`);
}
}
console.log(`[空调设备集成] 完成,新增 ${insertCount} 条,跳过 ${skipCount} 条已存在`);
}
......
......@@ -8,6 +8,15 @@ import { controlIndoorUnit } from "./feiyiClient";
import { getRegionList } from "./region";
import { startSchedule, stopSchedule } from "../config/schedule";
/** device_state 值常量 */
const DEVICE_STATE = { NORMAL: 0, HARDWARE_FAULT: 1, COMM_FAULT: 2, OFFLINE: 3 } as const;
const OFFLINE_STATES = [DEVICE_STATE.COMM_FAULT, DEVICE_STATE.OFFLINE] as number[];
/** 判断设备是否离线(仅依据 device_state) */
function isDeviceOffline(deviceState: number): boolean {
return OFFLINE_STATES.includes(deviceState);
}
/**
* 联动去重缓存:记录每个区域上次下发的控制状态,避免重复调用第三方接口
* key: regionKey, value: 上次下发的控制参数
......@@ -551,11 +560,11 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
if (regionGroup) {
acParams.region_key = { "%in%": regionKeys };
}
let acDevices = await selectDataListByParam(TABLENAME.设备表, acParams, ["device_id"]);
let acDevices = await selectDataListByParam(TABLENAME.设备表, acParams, ["device_id", "device_state"]);
let acDeviceIds = acDevices.data.map((d: any) => d.device_id);
let acCount = acDeviceIds.length;
let acOfflineCount = 0;
let acOfflineCount = acDevices.data.filter((d: any) => isDeviceOffline(d.device_state ?? 0)).length;
let acOnlineCount = acCount - acOfflineCount;
let acFaultRecords = await selectDataListByParam(
TABLENAME.设备故障表,
......@@ -601,24 +610,6 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
acOnlineTrend.push({ time: `${i}:00`, value: val ? ((val.online / val.total) * 100).toFixed(2) : '0' });
}
// 2.3 设备监测情况:在线/离线(根据最后数据时间是否超过30分钟)
let deviceLatestMap = new Map();
for (let devId of acDeviceIds) {
let rec = await selectDataListByParam(TABLENAME.设备数据表,
{ device_id: devId, "%orderDesc%": "device_time", "%limit%": 1 },
["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: power==='on' });
} else {
deviceLatestMap.set(devId, { lastTime: null, isOnline: false });
}
}
acOnlineCount = Array.from(deviceLatestMap.values()).filter(v => v.isOnline).length;
acOfflineCount = acCount - acOnlineCount;
}
// 2.4. 空调设备在线趋势图数据
......@@ -635,11 +626,11 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
if (regionGroup) {
fanParams.region_key = { "%in%": regionKeys };
}
let fanDevices = await selectDataListByParam(TABLENAME.设备表, fanParams, ["device_id"]);
let fanDevices = await selectDataListByParam(TABLENAME.设备表, fanParams, ["device_id", "device_state"]);
let fanDeviceIds = fanDevices.data.map((d: any) => d.device_id);
let fanCount = fanDeviceIds.length;
let fanOfflineCount = 0;
let fanOfflineCount = fanDevices.data.filter((d: any) => isDeviceOffline(d.device_state ?? 0)).length;
let fanOnlineCount = fanCount - fanOfflineCount;
let fanFaultRecords = await selectDataListByParam(
TABLENAME.设备故障表,
......@@ -701,30 +692,12 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
// 设备监控总数据
let devices = await selectDataListByParam(TABLENAME.设备表, {
region_key: { "%in%": regionKeys }
}, ["device_id"]);
}, ["device_id", "device_state"]);
let deviceIds = devices.data.map((d: any) => d.device_id);
let deviceCount = deviceIds.length;
let deviceOfflineCount = 0;
let deviceOfflineCount = devices.data.filter((d: any) => isDeviceOffline(d.device_state ?? 0)).length;
let deviceOnlineCount = deviceCount - deviceOfflineCount;
// 设备监测情况:在线/离线(根据最后数据时间是否超过30分钟)
let deviceLatestMap = new Map();
for (let devId of deviceIds) {
let rec = await selectDataListByParam(TABLENAME.设备数据表,
{ device_id: devId, "%orderDesc%": "device_time", "%limit%": 1 },
["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: power === 'on' });
} else {
deviceLatestMap.set(devId, { lastTime: null, isOnline: false });
}
}
deviceOnlineCount = Array.from(deviceLatestMap.values()).filter(v => v.isOnline).length;
deviceOfflineCount = deviceCount - deviceOnlineCount;
let deviceFaultRecords = await selectDataListByParam(
TABLENAME.设备故障表,
{ status: { "%ne%": 2 }, device_id: { "%in%": deviceIds } },
......
......@@ -26,13 +26,14 @@ export async function getScheduleList(params: {
pageSize?: number;
startDate?: string;
endDate?: string;
regionKey?: number;
deviceIds?: any;
regionKeys?: any;
}): Promise<any> {
const pageNumber = params.pageNumber || 1;
const pageSize = params.pageSize || 10;
const { startDate, endDate, regionKey } = params;
const { startDate, endDate, deviceIds, regionKeys } = params;
console.log(`[日程列表] page=${pageNumber}, size=${pageSize}, startDate=${startDate}, endDate=${endDate}, regionKey=${regionKey}`);
console.log(`[日程列表] page=${pageNumber}, size=${pageSize}, startDate=${startDate}, endDate=${endDate}, deviceIds=${deviceIds}, regionKeys=${regionKeys}`);
// 构建日程表查询条件
const scheduleWhere: any = { "%orderDesc%": "id" };
......@@ -47,10 +48,15 @@ export async function getScheduleList(params: {
// 如果按区域过滤,先查出该区域下的 schedule_id 集合
let filteredScheduleIds: number[] | null = null;
if (regionKey) {
const sdRes = await selectDataListByParam(TABLENAME.日程设备关联表, {
region_key: regionKey,
}, ["schedule_id"]);
let paramAnys: any = {};
if (deviceIds && deviceIds.length) {
paramAnys.device_id = { "%in%": deviceIds };
}
// if (regionKeys && regionKeys.length) {
// paramAnys.region_key = { "%in%": regionKeys };
// }
if ((deviceIds && deviceIds.length) || (regionKeys && regionKeys.length)) {
const sdRes = await selectDataListByParam(TABLENAME.日程设备关联表, paramAnys, ["schedule_id"]);
filteredScheduleIds = (sdRes.data || []).map((r: any) => r.schedule_id);
if (filteredScheduleIds.length === 0) {
return { total: 0, list: [] };
......@@ -59,9 +65,9 @@ export async function getScheduleList(params: {
}
// 分页查询
const columns = ["id", "title", "ac_controls", "schedule_time", "begin_validity", "end_validity", "created_at"];
const listRes = await selectDataListToPageByParam(
TABLENAME.日程表, scheduleWhere, columns, pageNumber, pageSize
const columns = ["id", "title", "ac_control", "schedule_time", "begin_validity", "end_validity", "created_at"];
const listRes = await selectDataListByParam(
TABLENAME.日程表, scheduleWhere, columns
);
const rows: any[] = listRes.data || [];
......@@ -121,7 +127,7 @@ export async function getScheduleList(params: {
return {
id: row.id,
title: row.title,
acControls: row.ac_controls,
acControl: row.ac_control,
scheduleTime: row.schedule_time || "",
validity,
deviceCount: devices.length,
......@@ -144,20 +150,21 @@ export async function getScheduleList(params: {
export async function addSchedule(params: {
title: string;
schedule_time: string;
ac_controls: string;
ac_control: string;
begin_validity?: string;
end_validity?: string;
deviceIds: string[];
regionKeys: number[];
}): Promise<any> {
const { title, schedule_time, ac_controls, begin_validity, end_validity, regionKeys } = params;
const { title, schedule_time, ac_control, begin_validity, end_validity, deviceIds, regionKeys } = params;
console.log(`[新增日程] title=${title}, time=${schedule_time}, action=${ac_controls}, regions=${regionKeys}`);
console.log(`[新增日程] title=${title}, time=${schedule_time}, action=${ac_control}, devices=${deviceIds}`);
// 1. 插入 schedule 主表
const scheduleRes = await addData(TABLENAME.日程表, {
await addData(TABLENAME.日程表, {
title,
schedule_time,
ac_controls,
ac_control,
begin_validity: begin_validity || null,
end_validity: end_validity || null,
});
......@@ -166,7 +173,7 @@ export async function addSchedule(params: {
const inserted = await selectDataListByParam(TABLENAME.日程表, {
title,
schedule_time,
ac_controls,
ac_control,
"%orderDesc%": "id",
"%limit%": 1,
}, ["id"]);
......@@ -178,10 +185,16 @@ export async function addSchedule(params: {
}
// 3. 查这些区域下的所有空调设备
const deviceRes = await selectDataListByParam(TABLENAME.设备表, {
region_key: { "%in%": regionKeys },
let deviceParams:any = {
device_type: "空调",
}, ["device_id", "region_key"]);
};
if (deviceIds && deviceIds.length) {
deviceParams.device_id = { "%in%": deviceIds };
}
if (regionKeys && regionKeys.length) {
deviceParams.region_key = { "%in%": regionKeys };
}
const deviceRes = await selectDataListByParam(TABLENAME.设备表, deviceParams, ["device_id", "region_key"]);
const devices = deviceRes.data || [];
......@@ -194,7 +207,7 @@ export async function addSchedule(params: {
}));
await addData(TABLENAME.日程设备关联表, sdRecords);
console.log(`[新增日程] 关联设备 ${sdRecords.length} 条`);
} else {
} else if (!regionKeys && !regionKeys.length) {
// 无设备时也插入一条区域占位记录(允许区域下暂时无设备)
for (const rk of regionKeys) {
await addData(TABLENAME.日程设备关联表, {
......@@ -241,7 +254,7 @@ export async function getScheduleDetail(scheduleId: number): Promise<any> {
id: row.id,
title: row.title,
scheduleTime: row.schedule_time || "",
acControls: row.ac_controls,
acControl: row.ac_control,
beginValidity: row.begin_validity
? (typeof row.begin_validity === "string" ? row.begin_validity.split(" ")[0] : formatLocalTime(new Date(row.begin_validity)).split(" ")[0])
: "",
......@@ -262,12 +275,13 @@ export async function editSchedule(params: {
scheduleId: number;
title: string;
schedule_time: string;
ac_controls: string;
ac_control: string;
begin_validity?: string;
end_validity?: string;
deviceIds: string[];
regionKeys: number[];
}): Promise<any> {
const { scheduleId, title, schedule_time, ac_controls, begin_validity, end_validity, regionKeys } = params;
const { scheduleId, title, schedule_time, ac_control, begin_validity, end_validity, deviceIds, regionKeys } = params;
console.log(`[编辑日程] id=${scheduleId}, title=${title}`);
......@@ -275,7 +289,7 @@ export async function editSchedule(params: {
await updateManyData(TABLENAME.日程表, { id: scheduleId }, {
title,
schedule_time,
ac_controls,
ac_control,
begin_validity: begin_validity || null,
end_validity: end_validity || null,
});
......@@ -284,10 +298,16 @@ export async function editSchedule(params: {
await delData(TABLENAME.日程设备关联表, { schedule_id: scheduleId });
// 3. 查区域下的空调设备
const deviceRes = await selectDataListByParam(TABLENAME.设备表, {
region_key: { "%in%": regionKeys },
let deviceParams:any = {
device_type: "空调",
}, ["device_id", "region_key"]);
};
if (deviceIds && deviceIds.length) {
deviceParams.device_id = { "%in%": deviceIds };
}
if (regionKeys && regionKeys.length) {
deviceParams.region_key = { "%in%": regionKeys };
}
const deviceRes = await selectDataListByParam(TABLENAME.设备表, deviceParams, ["device_id", "region_key"]);
const devices = deviceRes.data || [];
// 4. 重建关联
......@@ -299,7 +319,7 @@ export async function editSchedule(params: {
}));
await addData(TABLENAME.日程设备关联表, sdRecords);
console.log(`[编辑日程] 重建关联设备 ${sdRecords.length} 条`);
} else {
} else if (!regionKeys && !regionKeys.length) {
for (const rk of regionKeys) {
await addData(TABLENAME.日程设备关联表, {
schedule_id: scheduleId,
......
......@@ -163,6 +163,12 @@ export const TablesConfig = [
allowNull: true,
comment: '该设备支持的参数定义,JSON格式{"power":["on","off"]}'
},
device_state: {
type: DataTypes.TINYINT,
allowNull: false,
defaultValue: 0,
comment: '设备状态:0=正常 1=硬件故障 2=通信故障 3=离线'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
......@@ -356,10 +362,10 @@ export const TablesConfig = [
},
schedule_date: {
type: DataTypes.DATE,
allowNull: false,
allowNull: true,
comment: '日期'
},
ac_controls: {
ac_control: {
type: Sequelize.STRING(50),
allowNull: false,
comment: '操作状态:off关on开'
......
......@@ -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 { eccReqParamater } from "../util/verificationParam";
export function setRouter(httpServer) {
/** 获取周报告JSON数据 */
......@@ -79,9 +80,10 @@ 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,
startDate: req.query && req.query.startDate?req.query.startDate:'',
endDate: req.query && req.query.endDate?req.query.endDate:'',
deviceIds: req.query?.deviceIds ? req.query.deviceIds : undefined,
regionKeys: req.query?.regionKeys ? req.query.regionKeys : undefined,
});
res.success(data);
}
......@@ -108,25 +110,28 @@ async function getScheduleDetail(req, res) {
/**
* POST /api/qdm/admin/schedule/add
* 新增日程
* body: title, scheduleTime, acControls, beginValidity, endValidity, regionKeys
* body: title, scheduleTime, acControl, 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");
let reqConf = { title: 'String', scheduleTime: 'String', acControl: 'String', beginValidity: 'Date', endValidity: 'Date', deviceIds: 'Array', regionKeys: 'Array' };
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");
return;
}
if (!regionKeys || !Array.isArray(regionKeys) || regionKeys.length === 0) {
if ((!deviceIds && !regionKeys) || (!deviceIds.length && !regionKeys.length)) {
res.fail("至少选择一个区域");
return;
}
const data = await scheduleBiz.addSchedule({
title,
schedule_time: scheduleTime,
ac_controls: acControls,
ac_control: acControl,
begin_validity: beginValidity || undefined,
end_validity: endValidity || undefined,
regionKeys,
deviceIds: deviceIds ?? [],
regionKeys: regionKeys ?? []
});
res.success(data);
}
......@@ -134,26 +139,29 @@ async function addSchedule(req, res) {
/**
* POST /api/qdm/admin/schedule/edit
* 编辑日程
* body: scheduleId, title, scheduleTime, acControls, beginValidity, endValidity, regionKeys
* body: scheduleId, title, scheduleTime, acControl, 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");
let reqConf = { scheduleId: 'Number', title: 'String', scheduleTime: 'String', acControl: 'String', beginValidity: 'Date', endValidity: 'Date', deviceIds: 'Array', regionKeys: 'Array' };
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");
return;
}
if (!regionKeys || !Array.isArray(regionKeys) || regionKeys.length === 0) {
if ((!deviceIds && !regionKeys) || (!deviceIds.length && !regionKeys.length)) {
res.fail("至少选择一个区域");
return;
}
const data = await scheduleBiz.editSchedule({
scheduleId: Number(scheduleId),
scheduleId,
title,
schedule_time: scheduleTime,
ac_controls: acControls,
ac_control: acControl,
begin_validity: beginValidity || undefined,
end_validity: endValidity || undefined,
regionKeys,
deviceIds: deviceIds ?? [],
regionKeys: regionKeys ?? []
});
res.success(data);
}
......
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