运行分析数据修改

parent daed81cb
import { selectOneDataByParam, selectDataListByParam, selectDataCountByParam, selectDataListToPageByParam } from "../data/findData"; import { selectOneDataByParam, selectDataListByParam, selectDataCountByParam, selectDataListToPageByParam, selectPaginatedDataWithOrder } from "../data/findData";
import { TABLENAME } from "../config/dbEnum"; import { TABLENAME } from "../config/dbEnum";
import { getMySqlMs } from "../tools/systemTools"; import { getMySqlMs } from "../tools/systemTools";
import { BizError } from "../util/bizError"; import { BizError } from "../util/bizError";
...@@ -791,7 +791,7 @@ export async function getDeviceList(params: { ...@@ -791,7 +791,7 @@ export async function getDeviceList(params: {
} }
// 1. 构建查询参数,只查空调设备,排除 region_key=0 的设备 // 1. 构建查询参数,只查空调设备,排除 region_key=0 的设备
let paramAnys: any = { device_type: '空调' }; let paramAnys: any = { device_type: ['空调', '新风'] };
if (regionKeys.length) { if (regionKeys.length) {
paramAnys.region_key = { "%in%": regionKeys }; paramAnys.region_key = { "%in%": regionKeys };
} else { } else {
...@@ -1041,7 +1041,7 @@ export async function batchEditDevices(params: { ...@@ -1041,7 +1041,7 @@ export async function batchEditDevices(params: {
edits: { edits: {
power?: string; power?: string;
setTemp?: number; setTemp?: number;
linkageStart?: string; locked?: string;
}; };
}): Promise<{ total: number; successCount: number; failCount: number; details: any[] }> { }): Promise<{ total: number; successCount: number; failCount: number; details: any[] }> {
const { regionKeys, deviceName, power, state, deviceMode, startDate, endDate, edits } = params; const { regionKeys, deviceName, power, state, deviceMode, startDate, endDate, edits } = params;
...@@ -1053,9 +1053,9 @@ export async function batchEditDevices(params: { ...@@ -1053,9 +1053,9 @@ export async function batchEditDevices(params: {
if (!edits || typeof edits !== 'object') { if (!edits || typeof edits !== 'object') {
throw new BizError(ERRORENUM.参数错误, "edits 不能为空"); throw new BizError(ERRORENUM.参数错误, "edits 不能为空");
} }
const hasEdit = edits.power !== undefined || edits.setTemp !== undefined || edits.linkageStart !== undefined; const hasEdit = edits.power !== undefined || edits.setTemp !== undefined || edits.locked !== undefined;
if (!hasEdit) { if (!hasEdit) {
throw new BizError(ERRORENUM.参数错误, "edits 中至少需要 power / setTemp / linkageStart 其中之一"); throw new BizError(ERRORENUM.参数错误, "edits 中至少需要 power / setTemp / locked 其中之一");
} }
// 1. 构建设备查询条件 // 1. 构建设备查询条件
...@@ -1124,20 +1124,8 @@ export async function batchEditDevices(params: { ...@@ -1124,20 +1124,8 @@ export async function batchEditDevices(params: {
let successCount = 0; let successCount = 0;
let failCount = 0; let failCount = 0;
// 4. 处理 linkageStart 编辑(纯数据库更新,一次性批量更新) // 4. 处理 power / setTemp / locked 编辑(逐台调用飞奕接口)
if (edits.linkageStart !== undefined) { const needAcControl = edits.power !== undefined || edits.setTemp !== undefined || edits.locked !== 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) { if (needAcControl) {
const now = new Date(); const now = new Date();
...@@ -1154,6 +1142,12 @@ export async function batchEditDevices(params: { ...@@ -1154,6 +1142,12 @@ export async function batchEditDevices(params: {
if (edits.setTemp !== undefined) { if (edits.setTemp !== undefined) {
acParams.tempSet = edits.setTemp; acParams.tempSet = edits.setTemp;
} }
if (edits.locked !== undefined) {
const lockVal = edits.locked === 'on' ? 1 : 0;
acParams.onOffLock = lockVal;
acParams.workModeLock = lockVal;
acParams.tempSetLock = lockVal;
}
const result = await controlIndoorUnit(acParams); const result = await controlIndoorUnit(acParams);
...@@ -1167,7 +1161,7 @@ export async function batchEditDevices(params: { ...@@ -1167,7 +1161,7 @@ export async function batchEditDevices(params: {
time: formatLocalTime(now), time: formatLocalTime(now),
power: edits.power, power: edits.power,
setTemp: edits.setTemp, setTemp: edits.setTemp,
linkageStart: edits.linkageStart, locked: edits.locked,
success: result.success, success: result.success,
message: result.data || (result.success ? '控制成功' : '控制失败'), message: result.data || (result.success ? '控制成功' : '控制失败'),
}, },
...@@ -1190,12 +1184,6 @@ export async function batchEditDevices(params: { ...@@ -1190,12 +1184,6 @@ export async function batchEditDevices(params: {
console.error(`[batchEditDevices] 控制设备 ${dev.device_id} 失败:`, e); 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 }; return { total, successCount, failCount, details };
...@@ -1254,6 +1242,7 @@ export async function batchEditLocalDevices(params: { ...@@ -1254,6 +1242,7 @@ export async function batchEditLocalDevices(params: {
deviceType?: string; deviceType?: string;
controlParams?: string; controlParams?: string;
deviceState?: string; deviceState?: string;
linkageStart?: string;
}; };
}) { }) {
const { regionKeys, deviceName, edits } = params; const { regionKeys, deviceName, edits } = params;
...@@ -1277,6 +1266,7 @@ export async function batchEditLocalDevices(params: { ...@@ -1277,6 +1266,7 @@ export async function batchEditLocalDevices(params: {
if (edits.deviceType !== undefined) dbEdits.device_type = edits.deviceType; if (edits.deviceType !== undefined) dbEdits.device_type = edits.deviceType;
if (edits.controlParams !== undefined) dbEdits.control_params = edits.controlParams; if (edits.controlParams !== undefined) dbEdits.control_params = edits.controlParams;
if (edits.deviceState !== undefined) dbEdits.device_state = edits.deviceState; if (edits.deviceState !== undefined) dbEdits.device_state = edits.deviceState;
if (edits.linkageStart !== undefined) dbEdits.linkage_start = edits.linkageStart;
if (Object.keys(dbEdits).length === 0) { if (Object.keys(dbEdits).length === 0) {
throw new BizError(ERRORENUM.参数错误, "edits 中至少需要一个编辑字段"); throw new BizError(ERRORENUM.参数错误, "edits 中至少需要一个编辑字段");
...@@ -1287,3 +1277,89 @@ export async function batchEditLocalDevices(params: { ...@@ -1287,3 +1277,89 @@ export async function batchEditLocalDevices(params: {
return { isSuccess: true }; return { isSuccess: true };
} }
// ======= 设备日志管理 =======
/**
* 设备管理 - 日志管理分页列表
* @param params { pageNumber, pageSize, deviceName, status }
* @returns { list, pagination }
*
* 展示字段:序号(id)、设备标识(device_id)、设备名称(device_name)、标题(operation_content.option)、内容(operation_content)、状态(status 0失败 1成功)、创建时间(created_at)
*/
export async function getDeviceLogList(params: {
pageNumber?: number;
pageSize?: number;
deviceName?: string; // 设备名称(模糊匹配,通过 device 表关联)
status?: number; // 操作状态:0失败 1成功
}) {
const pageNumber = params.pageNumber || 1;
const pageSize = params.pageSize || 10;
// 构建查询条件
const queryParam: any = {};
// 状态筛选
if (params.status !== undefined && params.status !== null) {
queryParam.status = params.status;
}
// 设备名称筛选:先通过 device 表查出匹配的 device_id 列表
if (params.deviceName) {
const deviceResult = await selectDataListByParam(
TABLENAME.设备表,
{ device_name: { "%like%": params.deviceName } },
["device_id"]
);
const deviceIds = (deviceResult.data as any[]).map((d: any) => d.device_id);
if (deviceIds.length === 0) {
return {
list: [],
pagination: { page: pageNumber, pageSize, total: 0, totalPages: 0 },
};
}
queryParam.device_id = { "%in%": deviceIds };
}
// 分页查询,按创建时间倒序
const result = await selectPaginatedDataWithOrder(
TABLENAME.设备日志表,
queryParam,
["id", "device_id", "device_type", "operation_content", "status", "created_at"],
pageNumber,
pageSize,
[["created_at", "DESC"]],
);
// 补充设备名称映射
const rows = result.data as any[];
const logDeviceIds = Array.from(new Set(rows.map((item: any) => item.device_id)));
let deviceNameMap: Record<string, string> = {};
if (logDeviceIds.length > 0) {
const devRes = await selectDataListByParam(
TABLENAME.设备表,
{ device_id: { "%in%": logDeviceIds } },
["device_id", "device_name"]
);
for (const d of (devRes.data as any[])) {
deviceNameMap[d.device_id] = d.device_name || '';
}
}
// 映射展示字段
const list = rows.map((item: any) => ({
id: item.id,
device_id: item.device_id || '',
device_name: deviceNameMap[item.device_id] || '',
option: item.operation_content?.option || '',
operation_content: item.operation_content || {},
status: item.status,
statusName: item.status === 1 ? '成功' : '失败',
created_at: item.created_at || '',
}));
return {
list,
pagination: result.pagination,
};
}
\ No newline at end of file
...@@ -297,6 +297,27 @@ export async function getIndoorUnitList(params: any): Promise<any> { ...@@ -297,6 +297,27 @@ export async function getIndoorUnitList(params: any): Promise<any> {
} }
/** /**
* 单条查询内机
*
* @param deviceId
* @returns
*/
export async function getIndoorUnitStatus(deviceId: string): Promise<any> {
const path = '/openApi/indoorUnit/list';
const body = { indoorUnitAddressFull: deviceId };
const indoorUnits = await feiYiPost(path, body);
let indoorUnit = indoorUnits.data?.rows?.[0] || null;
let result: any = {};
result.mode = indoorUnit.workMode;
result.power = indoorUnit.onOff;
result.fanSpeed = indoorUnit.fanSpeed;
result.setTemp = indoorUnit.tempSet;
result.minTemp = indoorUnit.tempSetLo;
result.maxTemp = indoorUnit.tempSetHi;
return result;
}
/**
* 空调内机控制 * 空调内机控制
* @param params * @param params
* @returns * @returns
...@@ -310,7 +331,7 @@ export async function controlIndoorUnit(params: any): Promise<any> { ...@@ -310,7 +331,7 @@ export async function controlIndoorUnit(params: any): Promise<any> {
const body = { ...params }; const body = { ...params };
const result = await feiYiPost(path, body); // {code:"", message:""} const result = await feiYiPost(path, body); // {code:"", message:""}
console.log('控制内机响应:', result?.code); console.log('控制内机响应:', result?.code);
let success = result && result.code === '00000'? true : false; let success = result && result.code === '00000' ? true : false;
return { success, data: result.message }; return { success, data: result.message };
} }
...@@ -347,7 +368,7 @@ export async function controlIndoorUnit(params: any): Promise<any> { ...@@ -347,7 +368,7 @@ export async function controlIndoorUnit(params: any): Promise<any> {
*/ */
export async function getRegionTree(): Promise<any> { export async function getRegionTree(): Promise<any> {
const path = '/openApi/building/tree'; const path = '/openApi/building/tree';
const body = { }; const body = {};
const result = await feiYiPost(path, body); const result = await feiYiPost(path, body);
return result.data; return result.data;
} }
......
...@@ -219,13 +219,7 @@ function calcMeterDeviceDaily(records: any[]): number { ...@@ -219,13 +219,7 @@ function calcMeterDeviceDaily(records: any[]): number {
* @param weekOffset 0=最近七天(默认), -1=再往前七天, ... * @param weekOffset 0=最近七天(默认), -1=再往前七天, ...
*/ */
export async function getWeeklyReportData(weekOffset: number = 0): Promise<any> { export async function getWeeklyReportData(weekOffset: number = 0): Promise<any> {
// ==== 固定查询 7月29日 ~ 8月4日 ==== const { start: monday, end: sunday, timer } = calcWeekRange(weekOffset);
// 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}`); console.log(`[周报告JSON] 查询数据: ${timer}`);
// ===== 第一步:并行查询区域 + 各馆设备分组 ===== // ===== 第一步:并行查询区域 + 各馆设备分组 =====
...@@ -400,15 +394,12 @@ const ENV_INDICATOR_KEYS = [ ...@@ -400,15 +394,12 @@ const ENV_INDICATOR_KEYS = [
*/ */
export async function getEnvIndicatorsWeeklyAvg(): Promise<any> { export async function getEnvIndicatorsWeeklyAvg(): Promise<any> {
// 1. 计算七天时间范围 // 1. 计算七天时间范围
// ==== 固定查询 7月29日 ~ 8月4日 ==== const now = new Date();
// const now = new Date(); const end = new Date(now);
// const end = new Date(now); end.setHours(23, 59, 59, 999);
// end.setHours(23, 59, 59, 999); const start = new Date(now);
// const start = new Date(now); start.setDate(now.getDate() - 6);
// start.setDate(now.getDate() - 6); start.setHours(0, 0, 0, 0);
// 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 from = formatLocalTime(start);
const to = formatLocalTime(end); const to = formatLocalTime(end);
...@@ -495,15 +486,12 @@ const LEVEL_MAP: { [key: number]: string } = { 1: "一级", 2: "二级", 3: "三 ...@@ -495,15 +486,12 @@ const LEVEL_MAP: { [key: number]: string } = { 1: "一级", 2: "二级", 3: "三
* 每组展示:故障代码、风险内容、报警时间、处理状态、报警设备 * 每组展示:故障代码、风险内容、报警时间、处理状态、报警设备
*/ */
export async function getFaultInfoLast7Days(): Promise<any> { export async function getFaultInfoLast7Days(): Promise<any> {
// ==== 固定查询 7月29日 ~ 8月4日 ==== const now = new Date();
// const now = new Date(); const end = new Date(now);
// const end = new Date(now); end.setHours(23, 59, 59, 999);
// end.setHours(23, 59, 59, 999); const start = new Date(now);
// const start = new Date(now); start.setDate(now.getDate() - 6);
// start.setDate(now.getDate() - 6); start.setHours(0, 0, 0, 0);
// 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 from = formatLocalTime(start);
const to = formatLocalTime(end); const to = formatLocalTime(end);
...@@ -570,3 +558,218 @@ export async function getFaultInfoLast7Days(): Promise<any> { ...@@ -570,3 +558,218 @@ export async function getFaultInfoLast7Days(): Promise<any> {
console.log(`[近7天故障] 共 ${totalCount} 条,${groupMap.size} 种故障码`); console.log(`[近7天故障] 共 ${totalCount} 条,${groupMap.size} 种故障码`);
return { timer, totalCount, groupCount: Object.keys(groupMap).length, groups }; return { timer, totalCount, groupCount: Object.keys(groupMap).length, groups };
} }
/**
* 温度与人流对比分析
* - 每小时最高温度和人流展示与对比(今天 vs 昨天)
* - 每天最高温度和人流展示与对比(近 N 天趋势)
*/
import { selectDataListBySql } from "../data/findData";
/** 格式化日期为 YYYY-MM-DD */
function formatDate(d: Date): string {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
/** 前一天的日期 */
function yesterdayStr(dateStr: string): string {
const d = new Date(dateStr);
d.setDate(d.getDate() - 1);
return formatDate(d);
}
// ==================== 每小时最高温度和人流对比 ====================
/**
* 查询指定日期的每小时最高温度
*/
async function getHourlyMaxTemp(dateStr: string): Promise<Record<number, number>> {
const sql = `
SELECT
HOUR(dd.received_time) AS hour,
MAX(JSON_EXTRACT(dd.data, '$.temperature')) AS max_temp
FROM device_data dd
INNER JOIN device d ON dd.device_id = d.device_id
WHERE d.device_type = '环境监测'
AND DATE(dd.received_time) = :date
GROUP BY HOUR(dd.received_time)
ORDER BY HOUR(dd.received_time)
`;
const { data } = await selectDataListBySql(sql, { date: dateStr });
const map: Record<number, number> = {};
for (const row of (data as any[])) {
map[row.hour] = Number(row.max_temp) || 0;
}
return map;
}
/**
* 查询指定日期的每小时客流总量
*/
async function getHourlyVisitorFlow(dateStr: string): Promise<Record<number, number>> {
const sql = `
SELECT
HOUR(dd.received_time) AS hour,
SUM(JSON_EXTRACT(dd.data, '$.in')) AS total_in
FROM device_data dd
INNER JOIN device d ON dd.device_id = d.device_id
WHERE d.device_type = '客流传感器'
AND DATE(dd.received_time) = :date
GROUP BY HOUR(dd.received_time)
ORDER BY HOUR(dd.received_time)
`;
const { data } = await selectDataListBySql(sql, { date: dateStr });
const map: Record<number, number> = {};
for (const row of (data as any[])) {
map[row.hour] = Number(row.total_in) || 0;
}
return map;
}
/**
* 每小时最高温度和人流展示与对比
*
* GET /api/qdm/admin/analysis/hourly?date=2026-08-11
*
* 返回:
* {
* date: "2026-08-11",
* compareDate: "2026-08-10",
* hours: [
* { hour: 0, maxTemp: 25.5, compareMaxTemp: 24.8, totalVisitors: 120, compareTotalVisitors: 100 },
* ...
* ]
* }
*/
export async function getHourlyTempVisitorComparison(date?: string) {
const targetDate = date || formatDate(new Date());
const compareDate = yesterdayStr(targetDate);
const [todayTemp, yesterdayTemp, todayVisitor, yesterdayVisitor] = await Promise.all([
getHourlyMaxTemp(targetDate),
getHourlyMaxTemp(compareDate),
getHourlyVisitorFlow(targetDate),
getHourlyVisitorFlow(compareDate),
]);
const hours: Array<{
hour: number;
maxTemp: number | null;
compareMaxTemp: number | null;
totalVisitors: number;
compareTotalVisitors: number;
}> = [];
for (let h = 0; h < 24; h++) {
hours.push({
hour: h,
maxTemp: todayTemp[h] ?? null,
compareMaxTemp: yesterdayTemp[h] ?? null,
totalVisitors: todayVisitor[h] ?? 0,
compareTotalVisitors: yesterdayVisitor[h] ?? 0,
});
}
return {
date: targetDate,
compareDate,
hours,
};
}
// ==================== 每天最高温度和人流对比 ====================
/**
* 查询近 N 天的每日最高温度
*/
async function getDailyMaxTemp(days: number): Promise<Record<string, number>> {
const sql = `
SELECT
DATE(dd.received_time) AS date,
MAX(JSON_EXTRACT(dd.data, '$.temperature')) AS max_temp
FROM device_data dd
INNER JOIN device d ON dd.device_id = d.device_id
WHERE d.device_type = '环境监测'
AND dd.received_time >= DATE_SUB(CURDATE(), INTERVAL :days DAY)
GROUP BY DATE(dd.received_time)
ORDER BY DATE(dd.received_time)
`;
const { data } = await selectDataListBySql(sql, { days });
const map: Record<string, number> = {};
for (const row of (data as any[])) {
map[row.date] = Number(row.max_temp) || 0;
}
return map;
}
/**
* 查询近 N 天的每日客流总量
*/
async function getDailyTotalVisitor(days: number): Promise<Record<string, number>> {
const sql = `
SELECT
DATE(dd.received_time) AS date,
SUM(JSON_EXTRACT(dd.data, '$.in')) AS total_in
FROM device_data dd
INNER JOIN device d ON dd.device_id = d.device_id
WHERE d.device_type = '客流传感器'
AND dd.received_time >= DATE_SUB(CURDATE(), INTERVAL :days DAY)
GROUP BY DATE(dd.received_time)
ORDER BY DATE(dd.received_time)
`;
const { data } = await selectDataListBySql(sql, { days });
const map: Record<string, number> = {};
for (const row of (data as any[])) {
map[row.date] = Number(row.total_in) || 0;
}
return map;
}
/**
* 生成近 N 天的日期列表
*/
function generateDateRange(days: number): string[] {
const dates: string[] = [];
const today = new Date();
for (let i = days - 1; i >= 0; i--) {
const d = new Date(today);
d.setDate(d.getDate() - i);
dates.push(formatDate(d));
}
return dates;
}
/**
* 每天最高温度和人流展示与对比
*
* GET /api/qdm/admin/analysis/daily?days=7
*
* 返回:
* {
* days: [
* { date: "2026-08-05", maxTemp: 27.2, totalVisitors: 3500 },
* { date: "2026-08-06", maxTemp: 26.8, totalVisitors: 4200 },
* ...
* ]
* }
*/
export async function getDailyTempVisitorComparison(days: number = 7) {
const [dailyMaxTemp, dailyTotalVisitor] = await Promise.all([
getDailyMaxTemp(days),
getDailyTotalVisitor(days),
]);
const dateRange = generateDateRange(days);
const dailyData = dateRange.map((date) => ({
date,
maxTemp: dailyMaxTemp[date] ?? null,
totalVisitors: dailyTotalVisitor[date] ?? 0,
}));
return { days: dailyData };
}
...@@ -4,7 +4,7 @@ import { ERRORENUM } from "../config/errorEnum"; ...@@ -4,7 +4,7 @@ import { ERRORENUM } from "../config/errorEnum";
import { selectOneDataByParam, selectDataListByParam } from "../data/findData"; import { selectOneDataByParam, selectDataListByParam } from "../data/findData";
import { addData } from "../data/addData"; import { addData } from "../data/addData";
import { BizError } from "../util/bizError"; import { BizError } from "../util/bizError";
import { controlIndoorUnit } from "./feiyiClient"; import { controlIndoorUnit, getIndoorUnitStatus } from "./feiyiClient";
import { getRegionList } from "./region"; import { getRegionList } from "./region";
import { startSchedule, stopSchedule } from "../config/schedule"; import { startSchedule, stopSchedule } from "../config/schedule";
...@@ -299,7 +299,7 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -299,7 +299,7 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
} }
} }
} }
if (latestAir.data.length) { if (latestAir && latestAir.data.length) {
let airData = latestAir.data[0].data; let airData = latestAir.data[0].data;
currentTemperature = `${airData.temperature ?? 0}`; currentTemperature = `${airData.temperature ?? 0}`;
currentHumidity = `${airData.humidity ?? 0}%`; currentHumidity = `${airData.humidity ?? 0}%`;
...@@ -340,33 +340,23 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -340,33 +340,23 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
for (let i = 0; i < 24; i++) { for (let i = 0; i < 24; i++) {
let key = `${i}`; let key = `${i}`;
let val = hourlyData.get(key); let val = hourlyData.get(key);
let avg = (sumKey: string) => val?.cnt ? ((val[sumKey] || 0) / val.cnt).toFixed(1) : '0'; // 不补零:仅输出实际有数据的小时,无数据/未来时段直接跳过
if (i <= currentHour) { if (i > currentHour || !val?.cnt) {
temperatureTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('tempSum') : '0' }); continue;
humidityTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('humSum') : '0' }); }
pm25Trend.push({ time: `${i}:00`, value: val?.cnt ? avg('pmSum') : '0' }); let avg = (sumKey: string) => ((val[sumKey] || 0) / val.cnt).toFixed(1);
let co2Val = val?.cnt ? ((val['co2Sum'] || 0) / val.cnt) : 0; temperatureTrend.push({ time: `${i}:00`, value: avg('tempSum') });
humidityTrend.push({ time: `${i}:00`, value: avg('humSum') });
pm25Trend.push({ time: `${i}:00`, value: avg('pmSum') });
let co2Val = (val['co2Sum'] || 0) / val.cnt;
co2TrendDetail.push({ time: `${i}:00`, value: (co2Val / 100).toFixed(1) }); co2TrendDetail.push({ time: `${i}:00`, value: (co2Val / 100).toFixed(1) });
co2Trend.push({ time: `${i}时`, value: co2Val.toFixed(0) }); co2Trend.push({ time: `${i}时`, value: co2Val.toFixed(0) });
hchoTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('hchoSum') : '0' }); hchoTrend.push({ time: `${i}:00`, value: avg('hchoSum') });
lightLevelTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('lightLevelSum') : '0' }); lightLevelTrend.push({ time: `${i}:00`, value: avg('lightLevelSum') });
pm10Trend.push({ time: `${i}:00`, value: val?.cnt ? avg('pm10Sum') : '0' }); pm10Trend.push({ time: `${i}:00`, value: avg('pm10Sum') });
pressureTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('pressureSum') : '0' }); pressureTrend.push({ time: `${i}:00`, value: avg('pressureSum') });
tvocTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('tvocSum') : '0' }); tvocTrend.push({ time: `${i}:00`, value: avg('tvocSum') });
} else {
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: '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' });
}
} }
} }
...@@ -702,12 +692,11 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -702,12 +692,11 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
} }
let currentHour = nowTime.getHours(); let currentHour = nowTime.getHours();
for (let i = 0; i < 24; i++) { for (let i = 0; i < 24; i++) {
if (i <= currentHour) { // 不补零:仅输出实际有数据的小时
let key = `${i}时`; let key = `${i}时`;
let val = hourlyData.get(key); let val = hourlyData.get(key);
acOnlineTrend.push({ time: `${i}:00`, value: val ? ((val.online / val.total) * 100).toFixed(2) : '0' }); if (i <= currentHour && val) {
} else { acOnlineTrend.push({ time: `${i}:00`, value: ((val.online / val.total) * 100).toFixed(2) });
acOnlineTrend.push({ time: `${i}:00`, value: '0' });
} }
} }
} }
...@@ -751,9 +740,12 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -751,9 +740,12 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
} }
} }
for (let i = 0; i < 24; i++) { for (let i = 0; i < 24; i++) {
// 不补零:仅输出实际有数据的小时
let key = `${i}时`; let key = `${i}时`;
let val = hourlyData.get(key); let val = hourlyData.get(key);
fanOnlineTrend.push({ time: `${i}:00`, value: val ? val.airSupply : '0' }); if (val) {
fanOnlineTrend.push({ time: `${i}:00`, value: val.airSupply });
}
} }
} }
return { fanDeviceIds, fanCount, fanOnlineCount, fanOfflineCount, fanFaultCount, fanOnlineTrend }; return { fanDeviceIds, fanCount, fanOnlineCount, fanOfflineCount, fanFaultCount, fanOnlineTrend };
...@@ -888,17 +880,73 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -888,17 +880,73 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
* 智能管控-空调设备运行弹窗 * 智能管控-空调设备运行弹窗
* @param regionKey * @param regionKey
* @param deviceId * @param deviceId
* {
* "deviceInfo": {
* "mode": [
* "制冷",
* "制热",
* "送风",
* "除湿"
* ],
* "power": [
* "on",
* "off"
* ],
* "maxTemp": "20~40",
* "minTemp": "-10~20",
* "setTemp": "16~30",
* "fanSpeed": [
* "自动",
* "弱",
* "强",
* "超强"
* ],
* "roomTemp": "-20~50",
* "autoControl": [
* "生效",
* "解除"
* ]
* },
* "latestData": {
* "mode": "制冷",
* "power": "on",
* "maxTemp": 28,
* "minTemp": 16,
* "setTemp": 19,
* "fanSpeed": 1,
* "roomTemp": 0,
* "autoControl": "生效"
* }
* }
*/ */
export async function getAcRunningPop(regionKey: number, deviceId: string) { export async function getAcRunningPop(regionKey: number, deviceId: string) {
// 获取设备信息 // 获取设备信息
let devices = await selectDataListByParam( let devices = await selectDataListByParam(
TABLENAME.设备表, TABLENAME.设备表,
{ device_id: deviceId }, // region_key: regionKey, { device_id: deviceId }, // region_key: regionKey,
["device_id", "device_type", "device_name", "control_params"] ["device_id", "device_type", "device_state", "device_name", "control_params"]
); );
if (!devices.data.length) { if (!devices.data.length) {
throw new BizError(ERRORENUM.未找到数据, `设备 ${deviceId} 在区域 ${regionKey} 中未找到`); throw new BizError(ERRORENUM.未找到数据, `设备 ${deviceId} 在区域 ${regionKey} 中未找到`);
} }
let deviceType = devices?.data[0].device_type;
if (deviceType !== "环境监测") {
// 通过飞奕内机列表获取实时设备数据
let detailData = await getIndoorUnitStatus(deviceId);
detailData.fanSpeed = detailData && detailData.fanSpeed ? acDeviceKeyMap[detailData.fanSpeed] : 2;
detailData.autoControl = devices && devices.data[0].linkage_start && devices.data[0].linkage_start === "on" ? '生效' : '解除';
return {
deviceFault: {
deviceState: devices.data[0].deviceState,
// 0 正常 1 硬件故障 2 通信故障 3 离线
faultContent: devices.data[0].deviceState===1 ? '硬件故障' : devices.data[0].deviceState===2 ? '通信故障' : '',
},
deviceInfo: devices.data[0].control_params,
latestData: detailData
}
} else {
// 获取设备最新数据 // 获取设备最新数据
let deviceData = await selectDataListByParam( let deviceData = await selectDataListByParam(
TABLENAME.设备数据表, TABLENAME.设备数据表,
...@@ -907,22 +955,20 @@ export async function getAcRunningPop(regionKey: number, deviceId: string) { ...@@ -907,22 +955,20 @@ export async function getAcRunningPop(regionKey: number, deviceId: string) {
); );
let detailData = deviceData.data.length ? deviceData.data[0].data : null; let detailData = deviceData.data.length ? deviceData.data[0].data : null;
detailData.fanSpeed = detailData && detailData.fanSpeed ? acDeviceKeyMap[detailData.fanSpeed] : 2;
return { return {
deviceInfo: devices.data[0].control_params,
latestData: detailData latestData: detailData
} }
}
} }
/** /**
* 设备运行弹框 - 空调启停 * 设备运行弹框 - 空调启停
* @param deviceId * @param {autoControl: "生效",deviceId: "2-3-0-2",fanSpeed: "3",maxTemp: "28",minTemp: "16",mode: "制冷",power: "on",setTemp: "22"}
*/ */
export async function controlAcRunning(params: {}) { export async function controlAcRunning(params: {}) {
let deviceId = params["deviceId"]; let deviceId = params["deviceId"];
let mode = params["mode"]; let mode = params["mode"] || "制冷";
let power = params["power"]; let power = params["power"] || 'on';
let setTemp = params["setTemp"]; let setTemp = params["setTemp"];
let maxTemp = params["maxTemp"] || 28; // 默认锁定温度上限 let maxTemp = params["maxTemp"] || 28; // 默认锁定温度上限
let minTemp = params["minTemp"] || 16; // 默认锁定温度下限 let minTemp = params["minTemp"] || 16; // 默认锁定温度下限
...@@ -941,11 +987,10 @@ export async function controlAcRunning(params: {}) { ...@@ -941,11 +987,10 @@ export async function controlAcRunning(params: {}) {
// 2. 接入空调控制API,更新空调设备信息 // 2. 接入空调控制API,更新空调设备信息
let acPrarms = { let acPrarms = {
"indoorUnitAddressFull": [deviceId], "indoorUnitAddressFull": [deviceId],
"workMode": mode ? acDeviceKeyMap[mode] : 1, // 关机时传一个默认模式 "workMode": acDeviceKeyMap[mode], // 关机时传一个默认模式
// "onOff": acDeviceKeyMap[power], "onOff": acDeviceKeyMap[power],
"onOff": 1, // TODO:临时处理,等待前端调好就改回通过传参处理
"tempSet": Number(setTemp), "tempSet": Number(setTemp),
"fanSpeed": acDeviceKeyMap[fanSpeed], "fanSpeed": acDeviceKeyMap[acFanSpeedKeyMap[fanSpeed]],
"onOffLock": acDeviceKeyMap[autoControl], "onOffLock": acDeviceKeyMap[autoControl],
"workModeLock": acDeviceKeyMap[autoControl], "workModeLock": acDeviceKeyMap[autoControl],
"tempSetLock": acDeviceKeyMap[autoControl], "tempSetLock": acDeviceKeyMap[autoControl],
...@@ -1328,6 +1373,13 @@ export let acDeviceKeyMap: { [key: string]: number } = { ...@@ -1328,6 +1373,13 @@ export let acDeviceKeyMap: { [key: string]: number } = {
"开": 1, "开": 1,
"关": 0 "关": 0
} }
export let acFanSpeedKeyMap: { [key: number]: string } = {
4: "高风",
3: "中风",
2: "低风",
1: "中风",
0: "低风"
}
/** /**
* 区域楼层到Key的映射 * 区域楼层到Key的映射
......
...@@ -397,6 +397,20 @@ export async function selectSumValue( tableName: string, column: string, wherePa ...@@ -397,6 +397,20 @@ export async function selectSumValue( tableName: string, column: string, wherePa
/** /**
* 执行原生SQL查询
* @param sql SQL语句
* @param replacements 参数替换
*/
export async function selectDataListBySql(sql: string, replacements?: any) {
const { mysqlDB } = require("../db/mysqlInit");
const result = await mysqlDB.query(sql, {
replacements,
type: mysqlDB.QueryTypes.SELECT,
});
return { data: result };
}
/**
* 分页查询(支持自定义排序) * 分页查询(支持自定义排序)
* @param tableName 表名 * @param tableName 表名
* @param param 查询参数 * @param param 查询参数
......
...@@ -18,6 +18,10 @@ export function setRouter(httpServer) { ...@@ -18,6 +18,10 @@ export function setRouter(httpServer) {
httpServer.get("/api/qdm/report/env/weekly/avg", asyncHandler(getEnvWeeklyAvg)); httpServer.get("/api/qdm/report/env/weekly/avg", asyncHandler(getEnvWeeklyAvg));
/** 获取近 7 天故障信息(按 fault_code 分组) */ /** 获取近 7 天故障信息(按 fault_code 分组) */
httpServer.get("/api/qdm/report/fault/weekly", asyncHandler(getFaultInfo)); httpServer.get("/api/qdm/report/fault/weekly", asyncHandler(getFaultInfo));
/** 每小时最高温度和人流展示与对比 */
httpServer.get("/api/qdm/admin/analysis/hourly", asyncHandler(getHourlyAnalysis));
/** 每天最高温度和人流展示与对比 */
httpServer.get("/api/qdm/admin/analysis/daily", asyncHandler(getDailyAnalysis));
/** 区域设备树 */ /** 区域设备树 */
httpServer.get("/api/qdm/admin/region/device", asyncHandler(getRegionDevices)); httpServer.get("/api/qdm/admin/region/device", asyncHandler(getRegionDevices));
...@@ -45,6 +49,8 @@ export function setRouter(httpServer) { ...@@ -45,6 +49,8 @@ export function setRouter(httpServer) {
/** 预警工单列表 */ /** 预警工单列表 */
httpServer.get("/api/qdm/admin/fault/list", asyncHandler(getDeviceFaultList)); httpServer.get("/api/qdm/admin/fault/list", asyncHandler(getDeviceFaultList));
/** 日志管理 - 分页列表 */
httpServer.get("/api/qdm/admin/device/logs", asyncHandler(getDeviceLogList));
} }
/** /**
...@@ -245,7 +251,7 @@ async function editDevice(req, res) { ...@@ -245,7 +251,7 @@ async function editDevice(req, res) {
* POST /api/qdm/admin/device/batch_edit * POST /api/qdm/admin/device/batch_edit
* 设备管理 - 批量编辑设备 * 设备管理 - 批量编辑设备
* body: { regionKeys, deviceName?, power?, state?, deviceMode?, startDate?, endDate?, edits } * body: { regionKeys, deviceName?, power?, state?, deviceMode?, startDate?, endDate?, edits }
* edits: { power?, setTemp?, linkageStart? } * edits: { power?, setTemp?, locked? }
*/ */
async function batchEditDevices(req, res) { async function batchEditDevices(req, res) {
let reqConf = { let reqConf = {
...@@ -270,8 +276,8 @@ async function batchEditDevices(req, res) { ...@@ -270,8 +276,8 @@ async function batchEditDevices(req, res) {
res.error("缺少必填参数:edits"); res.error("缺少必填参数:edits");
return; return;
} }
if (edits.power === undefined && edits.setTemp === undefined && edits.linkageStart === undefined) { if (edits.power === undefined && edits.setTemp === undefined && edits.locked === undefined) {
res.error("edits 中至少需要 power / setTemp / linkageStart 其中之一"); res.error("edits 中至少需要 power / setTemp / locked 其中之一");
return; return;
} }
// 校验 edits.power 值合法性 // 校验 edits.power 值合法性
...@@ -284,9 +290,9 @@ async function batchEditDevices(req, res) { ...@@ -284,9 +290,9 @@ async function batchEditDevices(req, res) {
res.error("edits.setTemp 必须为 16~30 之间的数值"); res.error("edits.setTemp 必须为 16~30 之间的数值");
return; return;
} }
// 校验 edits.linkageStart 值合法性 // 校验 edits.locked 值合法性
if (edits.linkageStart !== undefined && !['on', 'off'].includes(edits.linkageStart)) { if (edits.locked !== undefined && !['on', 'off'].includes(edits.locked)) {
res.error("edits.linkageStart 只能为 on 或 off"); res.error("edits.locked 只能为 on 或 off");
return; return;
} }
...@@ -384,3 +390,41 @@ async function getDeviceFaultList(req, res) { ...@@ -384,3 +390,41 @@ async function getDeviceFaultList(req, res) {
}); });
res.success(data); res.success(data);
} }
// ======================= 设备日志管理 =======================
/**
* GET /api/qdm/admin/device/logs
* 日志管理分页列表
* query: pageNumber, pageSize, deviceName(设备名称模糊匹配), status(0失败 1成功)
*/
async function getDeviceLogList(req, res) {
const data = await deviceBiz.getDeviceLogList({
pageNumber: Number(req.query?.pageNumber) || 1,
pageSize: Number(req.query?.pageSize) || 10,
deviceName: req.query?.deviceName || undefined,
status: req.query?.status !== undefined && req.query.status !== '' ? Number(req.query.status) : undefined,
});
res.success(data);
}
// ======================= 温度与人流对比分析 =======================
/**
* GET /api/qdm/admin/analysis/hourly?date=2026-08-11
* 每小时最高温度和人流展示与对比(今天 vs 昨天)
*/
async function getHourlyAnalysis(req, res) {
const data = await reportBiz.getHourlyTempVisitorComparison(req.query?.date);
res.success(data);
}
/**
* GET /api/qdm/admin/analysis/daily?days=7
* 每天最高温度和人流展示与对比(近 N 天趋势)
*/
async function getDailyAnalysis(req, res) {
const days = Number(req.query?.days) || 7;
const data = await reportBiz.getDailyTempVisitorComparison(days);
res.success(data);
}
...@@ -144,7 +144,7 @@ async function getRegionList(req, res) { ...@@ -144,7 +144,7 @@ async function getRegionList(req, res) {
*/ */
async function controlAcRunning(req, res) { async function controlAcRunning(req, res) {
let reqConf = {deviceId:'String', mode:'String', power:'String', setTemp:'String', maxTemp:'String', minTemp:'String', autoControl:'String', fanSpeed:'String'}; let reqConf = {deviceId:'String', mode:'String', power:'String', setTemp:'String', maxTemp:'String', minTemp:'String', autoControl:'String', fanSpeed:'String'};
const NotMustHaveKeys = [ 'maxTemp', 'minTemp', 'autoControl', 'fanSpeed' ]; const NotMustHaveKeys = [ 'power', 'mode', 'setTemp', 'maxTemp', 'minTemp', 'autoControl', 'fanSpeed' ];
let { deviceId, mode, power, setTemp, maxTemp, minTemp, autoControl, fanSpeed } = eccReqParamater(reqConf, req.body, NotMustHaveKeys); let { deviceId, mode, power, setTemp, maxTemp, minTemp, autoControl, fanSpeed } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
let params = { deviceId, mode, power, setTemp, maxTemp, minTemp, autoControl, fanSpeed }; let params = { deviceId, mode, power, setTemp, maxTemp, minTemp, autoControl, fanSpeed };
const result = await runningBiz.controlAcRunning(params); const result = await runningBiz.controlAcRunning(params);
......
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