运行分析数据修改

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
......@@ -72,7 +72,7 @@ function generateSign(params: Record<string, any>, timestamp: string, nonce: str
*/
export async function getFeiYiToken(): Promise<string> {
const url = `${FEIYI_CONFIG.baseUrl}/oauth2/openapi/token`;
// 校验token是否有效,有效则直接返回
const now = Date.now();
if (feiYiToken && expireTime > now) {
......@@ -259,34 +259,34 @@ export async function getGatewayDeviceList(page: number = 1, limit: number = 10)
* @param params
* @returns
{
"code": "00000", //状态码 00000 为正常 其他为异常
"message": "success", //返回状态 success 为成功, 其他为失败
"data": { // 返回数据
"page": 1, //当前页码
"limit": 0, //每页条数
"total": 579, //总数
"rows": [{
"indoorUnitId": "98566729709518848", //内机 ID
"roomId": "98452624688414720", //房间 ID
"floorId": "98452624680026112", //楼层 ID
"buildingId": "34390184208629760", //楼栋 ID
"indoorUnitAddressFull": "11-1-1-1", //内机四段地址
"onOffLock": 1, //是否开关锁定 1 锁定 0 未锁定
"onOff": 0, //开关状态 1 开 0 关
"workModeLock": 0, //是否工作模式锁定 1 锁定 0 未锁定
"workMode": 2, //工作模式 1 制冷 2 制热 3 送风 4 除湿
"tempSetLock": 0, //是否温度锁定 1 锁定 0 未锁定
"tempSet": 16.0, // 设定温度
"tempSetLo": 16.0, // 锁定下限温度
"tempSetHi": 32.0, // 锁定下限温度
"fanSpeed": 4, // 风速 1 高风 2 中风 4 低风
"hasTimingTasks": 0, //是否有定时任务
"alarmCode": "LOST", //空调故障码
"roomTemp": 27 //室内温度(实际为空调回风温度,有一定的误差仅做参考)
"lockedSwitch": 0, // 锁定的开关:0 关 1 开
"lockedMode": 0, // 锁定模式 1 制冷 2 制热 3 送风 4 除湿 10 制热/送风 11 制冷/送风 12 制冷/除湿 13 制冷/除湿/送风 14 除湿/送风
}]
}
"code": "00000", //状态码 00000 为正常 其他为异常
"message": "success", //返回状态 success 为成功, 其他为失败
"data": { // 返回数据
"page": 1, //当前页码
"limit": 0, //每页条数
"total": 579, //总数
"rows": [{
"indoorUnitId": "98566729709518848", //内机 ID
"roomId": "98452624688414720", //房间 ID
"floorId": "98452624680026112", //楼层 ID
"buildingId": "34390184208629760", //楼栋 ID
"indoorUnitAddressFull": "11-1-1-1", //内机四段地址
"onOffLock": 1, //是否开关锁定 1 锁定 0 未锁定
"onOff": 0, //开关状态 1 开 0 关
"workModeLock": 0, //是否工作模式锁定 1 锁定 0 未锁定
"workMode": 2, //工作模式 1 制冷 2 制热 3 送风 4 除湿
"tempSetLock": 0, //是否温度锁定 1 锁定 0 未锁定
"tempSet": 16.0, // 设定温度
"tempSetLo": 16.0, // 锁定下限温度
"tempSetHi": 32.0, // 锁定下限温度
"fanSpeed": 4, // 风速 1 高风 2 中风 4 低风
"hasTimingTasks": 0, //是否有定时任务
"alarmCode": "LOST", //空调故障码
"roomTemp": 27 //室内温度(实际为空调回风温度,有一定的误差仅做参考)
"lockedSwitch": 0, // 锁定的开关:0 关 1 开
"lockedMode": 0, // 锁定模式 1 制冷 2 制热 3 送风 4 除湿 10 制热/送风 11 制冷/送风 12 制冷/除湿 13 制冷/除湿/送风 14 除湿/送风
}]
}
}
*/
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
* @returns
......@@ -310,7 +331,7 @@ export async function controlIndoorUnit(params: any): Promise<any> {
const body = { ...params };
const result = await feiYiPost(path, body); // {code:"", message:""}
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 };
}
......@@ -319,35 +340,35 @@ export async function controlIndoorUnit(params: any): Promise<any> {
* @param params
* @returns
{
"code": "00000", //状态码 00000 为正常,其他为异常
"message": "success", //状态信息 success 为成功,其他为异常
"data": [{
"buildingId": "97474765970866176", //建筑物 ID
"parentId": "0", //建筑物父 ID
"buildingName": "飞奕 1", // 建筑物名称
"buildingType": 1, //建筑物类型 1 楼栋 2 楼层 3 房间
"children": [ //子建筑物
{
"buildingId": "97474766339964928",
"parentId": "97474765970866176",
"buildingName": "1 层",
"buildingType": 2,
"children": [{
"buildingId": "97474766696480768",
"parentId": "97474766339964928",
"buildingName": "1-0",
"buildingType": 3,
"children": []
}]
}
]
}],
"timestamp": 1714290880301
"code": "00000", //状态码 00000 为正常,其他为异常
"message": "success", //状态信息 success 为成功,其他为异常
"data": [{
"buildingId": "97474765970866176", //建筑物 ID
"parentId": "0", //建筑物父 ID
"buildingName": "飞奕 1", // 建筑物名称
"buildingType": 1, //建筑物类型 1 楼栋 2 楼层 3 房间
"children": [ //子建筑物
{
"buildingId": "97474766339964928",
"parentId": "97474765970866176",
"buildingName": "1 层",
"buildingType": 2,
"children": [{
"buildingId": "97474766696480768",
"parentId": "97474766339964928",
"buildingName": "1-0",
"buildingType": 3,
"children": []
}]
}
]
}],
"timestamp": 1714290880301
}
*/
export async function getRegionTree(): Promise<any> {
const path = '/openApi/building/tree';
const body = { };
const body = {};
const result = await feiYiPost(path, body);
return result.data;
}
......@@ -357,29 +378,29 @@ export async function getRegionTree(): Promise<any> {
* @param params
* @returns
{
"code": "00000", //状态码 00000 为成功,其他为失败
"message": "success", //提示信息 success 为成功 其他为失败
"data": {
"page": 0, //当前页码
"limit": 0, //每页条数
"total": 6, //总条数
"rows": [{
"meterId": "110510431701041152", //电表 ID
"gatewayComId": , //电表网关设备通信 ID
"gatewayCode": , //电表网关设备编码
"protocolType": "DLT645-2007", //电表协议类型
"ammeterReadingAfterTransformationRatio": 6547.68, //电表变比后读数
"transformationRatio": 80.0, //电表变比
"meterComId": "210505015447" //电表地址
}, {
"gatewayComId": , //电表网关设备通信 ID
"gatewayCode": , //电表网关设备编码
"meterId": "110510346153885697",
"transformationRatio": 50.0,
"meterComId": "210505015560"
}]
},
"timestamp": 1713245734235
"code": "00000", //状态码 00000 为成功,其他为失败
"message": "success", //提示信息 success 为成功 其他为失败
"data": {
"page": 0, //当前页码
"limit": 0, //每页条数
"total": 6, //总条数
"rows": [{
"meterId": "110510431701041152", //电表 ID
"gatewayComId": , //电表网关设备通信 ID
"gatewayCode": , //电表网关设备编码
"protocolType": "DLT645-2007", //电表协议类型
"ammeterReadingAfterTransformationRatio": 6547.68, //电表变比后读数
"transformationRatio": 80.0, //电表变比
"meterComId": "210505015447" //电表地址
}, {
"gatewayComId": , //电表网关设备通信 ID
"gatewayCode": , //电表网关设备编码
"meterId": "110510346153885697",
"transformationRatio": 50.0,
"meterComId": "210505015560"
}]
},
"timestamp": 1713245734235
}
*/
export async function getElectricMaterList(params: any): Promise<any> {
......@@ -394,27 +415,27 @@ export async function getElectricMaterList(params: any): Promise<any> {
* @param params
* @returns
{
"code": "00000", //状态码 00000 表示成功,其他为失败
"message": "success", //提示信息 success 为成功,其他为失败
"data": {
"page": 1, //当前页码
"limit": 0, //每页条数
"total": 6, //总条数
"rows": [{
"electricMeterStateId": "112111594590437376", //抄表记录 ID
"gatewayId": "97477557167063040", //主机设备 ID
"gatewayCode": "fa000001400001240231212000200026" //主机设备编码
"meterComId": "210505015595", //电表地址
"gatewayComId": 11, // 电表网关通信 ID
"transformationRatio": 100.0, //电表变比
"powerConsumptionAfterTransformationRatio": 0.08, //电表变比后用电量
"ammeterReadingAfterTransformationRatio": 19167.4, // 电表变比后读数
"ammeterReadingBeforeTransformationRatio": 191.67, //电表变比前读数
"energyLoss": 0.00, //损耗电量
"deviceTime": 1706371200000, //设备时间
}]
},
"timestamp": 1713145563759
"code": "00000", //状态码 00000 表示成功,其他为失败
"message": "success", //提示信息 success 为成功,其他为失败
"data": {
"page": 1, //当前页码
"limit": 0, //每页条数
"total": 6, //总条数
"rows": [{
"electricMeterStateId": "112111594590437376", //抄表记录 ID
"gatewayId": "97477557167063040", //主机设备 ID
"gatewayCode": "fa000001400001240231212000200026" //主机设备编码
"meterComId": "210505015595", //电表地址
"gatewayComId": 11, // 电表网关通信 ID
"transformationRatio": 100.0, //电表变比
"powerConsumptionAfterTransformationRatio": 0.08, //电表变比后用电量
"ammeterReadingAfterTransformationRatio": 19167.4, // 电表变比后读数
"ammeterReadingBeforeTransformationRatio": 191.67, //电表变比前读数
"energyLoss": 0.00, //损耗电量
"deviceTime": 1706371200000, //设备时间
}]
},
"timestamp": 1713145563759
}
*/
export async function getMaterStateList(params: any): Promise<any> {
......@@ -429,29 +450,29 @@ export async function getMaterStateList(params: any): Promise<any> {
* @param params
* @returns
{
"code": "00000",
"message": "success",
"path": null,
"data": {
"page": 1,
"limit": 0,
"total": 278,
"rows": [{
"indoorUnitEventId": "140718238987321344", //内机事件 ID
"indoorUnitId": "97478680942739586", //内机 ID
"gatewayComId": 1, //主机通信 ID
"roomId": "97474774841819136", //内机所在房间 ID
"floorId": "97474774485303296", //内机所在楼层 ID
"buildingId": "97474765970866176", //内机所在楼栋 ID
"indoorUnitAddressFull": "1-8-128-16", //内机四段地址
"onOff": 0, //内机开关标志 0 关 1 开
"workMode": 4, //工作模式 1 制冷 2 制热 3 送风 4 除湿
"tempSet": 18.0, //内机温度设置
"fanSpeed": 2, //内机风速 风速 1 高风 2 中风 4 低风
"createTime": 1713173801018 //事件发生时间
}]
},
"timestamp": 1713173831195
"code": "00000",
"message": "success",
"path": null,
"data": {
"page": 1,
"limit": 0,
"total": 278,
"rows": [{
"indoorUnitEventId": "140718238987321344", //内机事件 ID
"indoorUnitId": "97478680942739586", //内机 ID
"gatewayComId": 1, //主机通信 ID
"roomId": "97474774841819136", //内机所在房间 ID
"floorId": "97474774485303296", //内机所在楼层 ID
"buildingId": "97474765970866176", //内机所在楼栋 ID
"indoorUnitAddressFull": "1-8-128-16", //内机四段地址
"onOff": 0, //内机开关标志 0 关 1 开
"workMode": 4, //工作模式 1 制冷 2 制热 3 送风 4 除湿
"tempSet": 18.0, //内机温度设置
"fanSpeed": 2, //内机风速 风速 1 高风 2 中风 4 低风
"createTime": 1713173801018 //事件发生时间
}]
},
"timestamp": 1713173831195
}
*/
export async function getIndoorUnitStateList(params: any): Promise<any> {
......
......@@ -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 };
}
......@@ -4,7 +4,7 @@ import { ERRORENUM } from "../config/errorEnum";
import { selectOneDataByParam, selectDataListByParam } from "../data/findData";
import { addData } from "../data/addData";
import { BizError } from "../util/bizError";
import { controlIndoorUnit } from "./feiyiClient";
import { controlIndoorUnit, getIndoorUnitStatus } from "./feiyiClient";
import { getRegionList } from "./region";
import { startSchedule, stopSchedule } from "../config/schedule";
......@@ -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;
currentTemperature = `${airData.temperature ?? 0}`;
currentHumidity = `${airData.humidity ?? 0}%`;
......@@ -340,33 +340,23 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
for (let i = 0; i < 24; i++) {
let key = `${i}`;
let val = hourlyData.get(key);
let avg = (sumKey: string) => val?.cnt ? ((val[sumKey] || 0) / val.cnt).toFixed(1) : '0';
if (i <= currentHour) {
temperatureTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('tempSum') : '0' });
humidityTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('humSum') : '0' });
pm25Trend.push({ time: `${i}:00`, value: val?.cnt ? avg('pmSum') : '0' });
let co2Val = val?.cnt ? ((val['co2Sum'] || 0) / val.cnt) : 0;
co2TrendDetail.push({ time: `${i}:00`, value: (co2Val / 100).toFixed(1) });
co2Trend.push({ time: `${i}时`, value: co2Val.toFixed(0) });
hchoTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('hchoSum') : '0' });
lightLevelTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('lightLevelSum') : '0' });
pm10Trend.push({ time: `${i}:00`, value: val?.cnt ? avg('pm10Sum') : '0' });
pressureTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('pressureSum') : '0' });
tvocTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('tvocSum') : '0' });
} 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' });
// 不补零:仅输出实际有数据的小时,无数据/未来时段直接跳过
if (i > currentHour || !val?.cnt) {
continue;
}
let avg = (sumKey: string) => ((val[sumKey] || 0) / val.cnt).toFixed(1);
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) });
co2Trend.push({ time: `${i}时`, value: co2Val.toFixed(0) });
hchoTrend.push({ time: `${i}:00`, value: avg('hchoSum') });
lightLevelTrend.push({ time: `${i}:00`, value: avg('lightLevelSum') });
pm10Trend.push({ time: `${i}:00`, value: avg('pm10Sum') });
pressureTrend.push({ time: `${i}:00`, value: avg('pressureSum') });
tvocTrend.push({ time: `${i}:00`, value: avg('tvocSum') });
}
}
......@@ -702,12 +692,11 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
}
let currentHour = nowTime.getHours();
for (let i = 0; i < 24; i++) {
if (i <= currentHour) {
let key = `${i}时`;
let val = hourlyData.get(key);
acOnlineTrend.push({ time: `${i}:00`, value: val ? ((val.online / val.total) * 100).toFixed(2) : '0' });
} else {
acOnlineTrend.push({ time: `${i}:00`, value: '0' });
// 不补零:仅输出实际有数据的小时
let key = `${i}时`;
let val = hourlyData.get(key);
if (i <= currentHour && val) {
acOnlineTrend.push({ time: `${i}:00`, value: ((val.online / val.total) * 100).toFixed(2) });
}
}
}
......@@ -751,9 +740,12 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
}
}
for (let i = 0; i < 24; i++) {
// 不补零:仅输出实际有数据的小时
let key = `${i}时`;
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 };
......@@ -888,41 +880,95 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
* 智能管控-空调设备运行弹窗
* @param regionKey
* @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) {
// 获取设备信息
let devices = await selectDataListByParam(
TABLENAME.设备表,
{ 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) {
throw new BizError(ERRORENUM.未找到数据, `设备 ${deviceId} 在区域 ${regionKey} 中未找到`);
}
// 获取设备最新数据
let deviceData = await selectDataListByParam(
TABLENAME.设备数据表,
{ device_id: deviceId, "%orderDesc%": "device_time", "%limit%": 1 },
["data", "device_time"]
);
let detailData = deviceData.data.length ? deviceData.data[0].data : null;
detailData.fanSpeed = detailData && detailData.fanSpeed ? acDeviceKeyMap[detailData.fanSpeed] : 2;
return {
deviceInfo: devices.data[0].control_params,
latestData: detailData
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(
TABLENAME.设备数据表,
{ device_id: deviceId, "%orderDesc%": "device_time", "%limit%": 1 },
["data", "device_time"]
);
let detailData = deviceData.data.length ? deviceData.data[0].data : null;
return {
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: {}) {
let deviceId = params["deviceId"];
let mode = params["mode"];
let power = params["power"];
let mode = params["mode"] || "制冷";
let power = params["power"] || 'on';
let setTemp = params["setTemp"];
let maxTemp = params["maxTemp"] || 28; // 默认锁定温度上限
let minTemp = params["minTemp"] || 16; // 默认锁定温度下限
......@@ -941,11 +987,10 @@ export async function controlAcRunning(params: {}) {
// 2. 接入空调控制API,更新空调设备信息
let acPrarms = {
"indoorUnitAddressFull": [deviceId],
"workMode": mode ? acDeviceKeyMap[mode] : 1, // 关机时传一个默认模式
// "onOff": acDeviceKeyMap[power],
"onOff": 1, // TODO:临时处理,等待前端调好就改回通过传参处理
"workMode": acDeviceKeyMap[mode], // 关机时传一个默认模式
"onOff": acDeviceKeyMap[power],
"tempSet": Number(setTemp),
"fanSpeed": acDeviceKeyMap[fanSpeed],
"fanSpeed": acDeviceKeyMap[acFanSpeedKeyMap[fanSpeed]],
"onOffLock": acDeviceKeyMap[autoControl],
"workModeLock": acDeviceKeyMap[autoControl],
"tempSetLock": acDeviceKeyMap[autoControl],
......@@ -1328,6 +1373,13 @@ export let acDeviceKeyMap: { [key: string]: number } = {
"开": 1,
"关": 0
}
export let acFanSpeedKeyMap: { [key: number]: string } = {
4: "高风",
3: "中风",
2: "低风",
1: "中风",
0: "低风"
}
/**
* 区域楼层到Key的映射
......
......@@ -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