运行分析数据修改

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 { getMySqlMs } from "../tools/systemTools";
import { BizError } from "../util/bizError";
......@@ -791,7 +791,7 @@ export async function getDeviceList(params: {
}
// 1. 构建查询参数,只查空调设备,排除 region_key=0 的设备
let paramAnys: any = { device_type: '空调' };
let paramAnys: any = { device_type: ['空调', '新风'] };
if (regionKeys.length) {
paramAnys.region_key = { "%in%": regionKeys };
} else {
......@@ -1041,7 +1041,7 @@ export async function batchEditDevices(params: {
edits: {
power?: string;
setTemp?: number;
linkageStart?: string;
locked?: string;
};
}): Promise<{ total: number; successCount: number; failCount: number; details: any[] }> {
const { regionKeys, deviceName, power, state, deviceMode, startDate, endDate, edits } = params;
......@@ -1053,9 +1053,9 @@ export async function batchEditDevices(params: {
if (!edits || typeof edits !== 'object') {
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) {
throw new BizError(ERRORENUM.参数错误, "edits 中至少需要 power / setTemp / linkageStart 其中之一");
throw new BizError(ERRORENUM.参数错误, "edits 中至少需要 power / setTemp / locked 其中之一");
}
// 1. 构建设备查询条件
......@@ -1124,20 +1124,8 @@ export async function batchEditDevices(params: {
let successCount = 0;
let failCount = 0;
// 4. 处理 linkageStart 编辑(纯数据库更新,一次性批量更新)
if (edits.linkageStart !== undefined) {
try {
const deviceIds = devices.map(d => d.device_id);
await updateManyData(TABLENAME.设备表, { device_id: { "%in%": deviceIds } }, { linkage_start: edits.linkageStart });
console.log(`[batchEditDevices] 批量设置联动为 ${edits.linkageStart},涉及 ${deviceIds.length} 台设备`);
} catch (e: any) {
console.error('[batchEditDevices] 批量更新 linkage_start 失败:', e);
throw new BizError(ERRORENUM.操作失败, `批量更新联动状态失败: ${e.message}`);
}
}
// 5. 处理 power / setTemp 编辑(逐台调用飞奕接口)
const needAcControl = edits.power !== undefined || edits.setTemp !== undefined;
// 4. 处理 power / setTemp / locked 编辑(逐台调用飞奕接口)
const needAcControl = edits.power !== undefined || edits.setTemp !== undefined || edits.locked !== undefined;
if (needAcControl) {
const now = new Date();
......@@ -1154,6 +1142,12 @@ export async function batchEditDevices(params: {
if (edits.setTemp !== undefined) {
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);
......@@ -1167,7 +1161,7 @@ export async function batchEditDevices(params: {
time: formatLocalTime(now),
power: edits.power,
setTemp: edits.setTemp,
linkageStart: edits.linkageStart,
locked: edits.locked,
success: result.success,
message: result.data || (result.success ? '控制成功' : '控制失败'),
},
......@@ -1190,12 +1184,6 @@ export async function batchEditDevices(params: {
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 };
......@@ -1254,6 +1242,7 @@ export async function batchEditLocalDevices(params: {
deviceType?: string;
controlParams?: string;
deviceState?: string;
linkageStart?: string;
};
}) {
const { regionKeys, deviceName, edits } = params;
......@@ -1277,6 +1266,7 @@ export async function batchEditLocalDevices(params: {
if (edits.deviceType !== undefined) dbEdits.device_type = edits.deviceType;
if (edits.controlParams !== undefined) dbEdits.control_params = edits.controlParams;
if (edits.deviceState !== undefined) dbEdits.device_state = edits.deviceState;
if (edits.linkageStart !== undefined) dbEdits.linkage_start = edits.linkageStart;
if (Object.keys(dbEdits).length === 0) {
throw new BizError(ERRORENUM.参数错误, "edits 中至少需要一个编辑字段");
......@@ -1286,4 +1276,90 @@ export async function batchEditLocalDevices(params: {
console.log(`[batchEditLocalDevices] 批量更新,条件:`, where, "更新内容:", dbEdits);
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
......@@ -219,13 +219,7 @@ function calcMeterDeviceDaily(records: any[]): number {
* @param weekOffset 0=最近七天(默认), -1=再往前七天, ...
*/
export async function getWeeklyReportData(weekOffset: number = 0): Promise<any> {
// ==== 固定查询 7月29日 ~ 8月4日 ====
// const { start: monday, end: sunday, timer } = calcWeekRange(weekOffset);
const start = new Date(2026, 6, 29, 0, 0, 0, 0); // 7月29日 00:00
const end = new Date(2026, 7, 4, 23, 59, 59, 999); // 8月4日 23:59
const monday = start;
const sunday = end;
const timer = `${formatChineseDate(monday)}${formatChineseDate(sunday)}`;
const { start: monday, end: sunday, timer } = calcWeekRange(weekOffset);
console.log(`[周报告JSON] 查询数据: ${timer}`);
// ===== 第一步:并行查询区域 + 各馆设备分组 =====
......@@ -400,15 +394,12 @@ const ENV_INDICATOR_KEYS = [
*/
export async function getEnvIndicatorsWeeklyAvg(): Promise<any> {
// 1. 计算七天时间范围
// ==== 固定查询 7月29日 ~ 8月4日 ====
// const now = new Date();
// const end = new Date(now);
// end.setHours(23, 59, 59, 999);
// const start = new Date(now);
// start.setDate(now.getDate() - 6);
// start.setHours(0, 0, 0, 0);
const start = new Date(2026, 6, 29, 0, 0, 0, 0); // 7月29日 00:00
const end = new Date(2026, 7, 4, 23, 59, 59, 999); // 8月4日 23:59
const 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);
......@@ -495,15 +486,12 @@ const LEVEL_MAP: { [key: number]: string } = { 1: "一级", 2: "二级", 3: "三
* 每组展示:故障代码、风险内容、报警时间、处理状态、报警设备
*/
export async function getFaultInfoLast7Days(): Promise<any> {
// ==== 固定查询 7月29日 ~ 8月4日 ====
// const now = new Date();
// const end = new Date(now);
// end.setHours(23, 59, 59, 999);
// const start = new Date(now);
// start.setDate(now.getDate() - 6);
// start.setHours(0, 0, 0, 0);
const start = new Date(2026, 6, 29, 0, 0, 0, 0); // 7月29日 00:00
const end = new Date(2026, 7, 4, 23, 59, 59, 999); // 8月4日 23:59
const 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);
......@@ -570,3 +558,218 @@ export async function getFaultInfoLast7Days(): Promise<any> {
console.log(`[近7天故障] 共 ${totalCount} 条,${groupMap.size} 种故障码`);
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 };
}
......@@ -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 param 查询参数
......
......@@ -18,6 +18,10 @@ export function setRouter(httpServer) {
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/analysis/hourly", asyncHandler(getHourlyAnalysis));
/** 每天最高温度和人流展示与对比 */
httpServer.get("/api/qdm/admin/analysis/daily", asyncHandler(getDailyAnalysis));
/** 区域设备树 */
httpServer.get("/api/qdm/admin/region/device", asyncHandler(getRegionDevices));
......@@ -45,6 +49,8 @@ export function setRouter(httpServer) {
/** 预警工单列表 */
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) {
* POST /api/qdm/admin/device/batch_edit
* 设备管理 - 批量编辑设备
* body: { regionKeys, deviceName?, power?, state?, deviceMode?, startDate?, endDate?, edits }
* edits: { power?, setTemp?, linkageStart? }
* edits: { power?, setTemp?, locked? }
*/
async function batchEditDevices(req, res) {
let reqConf = {
......@@ -270,8 +276,8 @@ async function batchEditDevices(req, res) {
res.error("缺少必填参数:edits");
return;
}
if (edits.power === undefined && edits.setTemp === undefined && edits.linkageStart === undefined) {
res.error("edits 中至少需要 power / setTemp / linkageStart 其中之一");
if (edits.power === undefined && edits.setTemp === undefined && edits.locked === undefined) {
res.error("edits 中至少需要 power / setTemp / locked 其中之一");
return;
}
// 校验 edits.power 值合法性
......@@ -284,9 +290,9 @@ async function batchEditDevices(req, res) {
res.error("edits.setTemp 必须为 16~30 之间的数值");
return;
}
// 校验 edits.linkageStart 值合法性
if (edits.linkageStart !== undefined && !['on', 'off'].includes(edits.linkageStart)) {
res.error("edits.linkageStart 只能为 on 或 off");
// 校验 edits.locked 值合法性
if (edits.locked !== undefined && !['on', 'off'].includes(edits.locked)) {
res.error("edits.locked 只能为 on 或 off");
return;
}
......@@ -384,3 +390,41 @@ async function getDeviceFaultList(req, res) {
});
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) {
*/
async function controlAcRunning(req, res) {
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 params = { deviceId, mode, power, setTemp, maxTemp, minTemp, autoControl, fanSpeed };
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