联动修改

parent 027f2027
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
/node_modules /node_modules
/test /test
/public /public
/scripts
/logs /logs
/video /video
/files /files
......
...@@ -24,4 +24,7 @@ ...@@ -24,4 +24,7 @@
<username>admin</username> <username>admin</username>
<password>admin123</password> <password>admin123</password>
</mqttsrv> </mqttsrv>
<controller>
<is_use>true</is_use>
</controller>
</config> </config>
import { selectOneDataByParam, selectDataListByParam } from "../data/findData"; import { selectOneDataByParam, selectDataListByParam, selectDataCountByParam, selectDataListToPageByParam } 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";
import { ERRORENUM } from "../config/errorEnum"; import { ERRORENUM } from "../config/errorEnum";
import { addData } from "../data/addData"; import { addData } from "../data/addData";
import { updateManyData } from "../data/updateData"; import { updateManyData } from "../data/updateData";
import { controlAcByEnvironment } from "./running"; import { controlAcByEnvironment, acDeviceKeyMap, formatLocalTime } from "./running";
import { getIndoorUnitList, controlIndoorUnit } from "./feiyiClient";
// ======= 模块级缓存(低频变化的数据) ======= // ======= 模块级缓存(低频变化的数据) =======
let deviceTreeCache: { data: any; timestamp: number } | null = null; let deviceTreeCache: { data: any; timestamp: number } | null = null;
...@@ -762,3 +763,526 @@ export async function getEnvQualityPop(regionGroup?: string, regionType?: string ...@@ -762,3 +763,526 @@ export async function getEnvQualityPop(regionGroup?: string, regionType?: string
return { total: dataList.length, dataList }; return { total: dataList.length, dataList };
} }
/**
* 获取设备列表
* @param params
*/
export async function getDeviceList(params: {
pageNumber?: number;
pageSize?: number;
regionKeys?: any;
deviceName?: string;
power?: string;
state?: string;
deviceMode?: string;
}) {
const pageNumber = params.pageNumber || 1;
const pageSize = params.pageSize || 10;
// 解析 regionKeys(兼容字符串和数组格式)
let regionKeys: string[] = [];
if (params.regionKeys) {
if (typeof params.regionKeys === 'string') {
regionKeys = params.regionKeys.split(',').map((k: string) => k.trim()).filter(Boolean);
} else if (Array.isArray(params.regionKeys)) {
regionKeys = params.regionKeys;
}
}
// 1. 构建查询参数,只查空调设备,排除 region_key=0 的设备
let paramAnys: any = { device_type: '空调' };
if (regionKeys.length) {
paramAnys.region_key = { "%in%": regionKeys };
} else {
paramAnys.region_key = { "%ne%": 0 };
}
// 2. deviceName 模糊搜索
if (params.deviceName) {
paramAnys.device_name = { "%like%": params.deviceName };
}
// 3. state 映射:全部/正常/告警/离线
const stateMap: Record<string, number[]> = {
'正常': [0],
'告警': [1, 2],
'离线': [3],
};
if (params.state && stateMap[params.state]) {
paramAnys.device_state = { "%in%": stateMap[params.state] };
}
// 4. 判断是否需要飞奕后置过滤
const powerMap: Record<string, number> = { '开': 1, '关': 0 };
const modeMap: Record<string, number> = { '制冷': 1, '制热': 2, '通风': 3, '除湿': 4 };
const needFeiyiFilter = params.power || params.deviceMode;
const targetOnOff = params.power ? powerMap[params.power] : undefined;
const targetMode = params.deviceMode ? modeMap[params.deviceMode] : undefined;
if (needFeiyiFilter) {
// ==== 飞奕筛选模式:查全量设备,后置过滤再内存分页 ====
const deviceRes = await selectDataListByParam(
TABLENAME.设备表, paramAnys,
["device_id", "region_key", "device_state", "linkage_start"]
);
let devices = (deviceRes.data || []) as any[];
let filteredRows: any[] = [];
const fanSpeedMap: Record<number, string> = { 1: '高风', 2: '中风', 4: '低风' };
const workModeMap: Record<number, string> = { 1: '制冷', 2: '制热', 3: '送风', 4: '除湿' };
const deviceStateMap: Record<number, string> = { 0: '正常', 1: '硬件故障', 2: '通信故障', 3: '离线' };
const pad = (n: number) => String(n).padStart(2, '0');
const now = new Date();
const updateTime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
if (devices.length > 0) {
// 查区域信息
const regionIdSet = [...new Set(devices.map((d: any) => d.region_key).filter(Boolean))];
let regionMap: Map<string, any> = new Map();
if (regionIdSet.length) {
const regionRes = await selectDataListByParam(
TABLENAME.区域表,
{ id: { "%in%": regionIdSet } },
["id", "groups", "type", "name"]
);
(regionRes.data as any[]).forEach((r: any) => regionMap.set(String(r.id), r));
}
// 逐台调飞奕,收集符合条件的
for (const device of devices) {
let feiyiRow: any = {};
try {
const result = await getIndoorUnitList({ indoorUnitAddressFull: device.device_id, page: 1, limit: 1 });
feiyiRow = result.rows[0] || {};
} catch (e) {
console.error(`[getDeviceList] 获取设备 ${device.device_id} 飞奕数据失败:`, e);
}
// 飞奕筛选
if (targetOnOff !== undefined && feiyiRow.onOff !== targetOnOff) continue;
if (targetMode !== undefined && feiyiRow.workMode !== targetMode) continue;
const region = regionMap.get(String(device.region_key));
filteredRows.push({
deviceId: device.device_id,
regionName: region
? `${region.groups || ''}_${region.type || ''}_${region.name || ''}`
: '',
power: feiyiRow.onOff === 1 ? '开' : '关',
setTemp: feiyiRow.tempSet ?? '',
fanSpeed: fanSpeedMap[feiyiRow.fanSpeed] || String(feiyiRow.fanSpeed || ''),
deviceMode: workModeMap[feiyiRow.workMode] || String(feiyiRow.workMode || ''),
state: deviceStateMap[device.device_state] ?? String(device.device_state ?? ''),
linkageStart: device.linkage_start ?? 'on',
updateTime,
});
}
}
const total = filteredRows.length;
const startIdx = (pageNumber - 1) * pageSize;
const rows = filteredRows.slice(startIdx, startIdx + pageSize);
return { total, rows };
} else {
// ==== 无飞奕筛选:DB 层分页(原逻辑) ====
// 查询总数
const countRes = await selectDataCountByParam(TABLENAME.设备表, paramAnys);
const total = countRes.data as number;
if (total === 0) {
return { total: 0, rows: [] };
}
// 分页查询设备
const deviceRes = await selectDataListToPageByParam(
TABLENAME.设备表, paramAnys,
["device_id", "region_key", "device_state", "linkage_start"],
pageNumber, pageSize
);
const devices = deviceRes.data as any[];
if (!devices.length) {
return { total: 0, rows: [] };
}
// 查询这批设备对应的区域信息(region_key 对应 region.id)
const regionIdSet = [...new Set(devices.map((d: any) => d.region_key).filter(Boolean))];
let regionMap: Map<string, any> = new Map();
if (regionIdSet.length) {
const regionRes = await selectDataListByParam(
TABLENAME.区域表,
{ id: { "%in%": regionIdSet } },
["id", "groups", "type", "name"]
);
(regionRes.data as any[]).forEach((r: any) => regionMap.set(String(r.id), r));
}
// 逐台串行调用飞奕接口获取实时数据
const feiyiResults: any[] = [];
for (const device of devices) {
try {
const result = await getIndoorUnitList({ indoorUnitAddressFull: device.device_id, page: 1, limit: 1 });
feiyiResults.push(result.rows[0] || {});
} catch (e) {
console.error(`[getDeviceList] 获取设备 ${device.device_id} 飞奕数据失败:`, e);
feiyiResults.push({ });
}
}
// 组装返回数据
const fanSpeedMap: Record<number, string> = { 1: '高风', 2: '中风', 4: '低风' };
const workModeMap: Record<number, string> = { 1: '制冷', 2: '制热', 3: '送风', 4: '除湿' };
const deviceStateMap: Record<number, string> = { 0: '正常', 1: '硬件故障', 2: '通信故障', 3: '离线' };
const pad = (n: number) => String(n).padStart(2, '0');
const now = new Date();
const updateTime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
const rows = devices.map((device: any, index: number) => {
const region = regionMap.get(String(device.region_key));
const row = feiyiResults[index] || {};
return {
deviceId: device.device_id,
regionName: region
? `${region.groups || ''}_${region.type || ''}_${region.name || ''}`
: '',
power: row.onOff === 1 ? '开' : '关',
setTemp: row.tempSet ?? '',
fanSpeed: fanSpeedMap[row.fanSpeed] || String(row.fanSpeed || ''),
deviceMode: workModeMap[row.workMode] || String(row.workMode || ''),
state: deviceStateMap[device.device_state] ?? String(device.device_state ?? ''),
linkageStart: device.linkage_start ?? 'on',
updateTime,
};
});
return { total, rows };
}
}
/**
* 设备管理 - 编辑空调控制参数(调用飞奕接口 + 记录日志)
* @returns { success: boolean, message: string }
*/
export async function editAcDeviceControl(params: {
deviceId: string;
power: string;
setTemp?: number;
fanSpeed?: string;
deviceMode?: string;
}): Promise<{ success: boolean; message: string }> {
const { deviceId, power, setTemp, fanSpeed, deviceMode } = params;
// 1. 校验设备是否存在
let device = await selectOneDataByParam(
TABLENAME.设备表,
{ device_id: deviceId },
["id", "device_id", "device_name"]
);
if (!device.data || !device.data.id) {
throw new BizError(ERRORENUM.参数错误, `设备 ${deviceId} 未注册`);
}
// 2. 构建飞奕控制参数
let acParams: any = {
"indoorUnitAddressFull": [deviceId],
"onOff": acDeviceKeyMap[power] ?? 1,
"openApiAction": "control",
};
if (setTemp != null) acParams.tempSet = setTemp;
if (fanSpeed) acParams.fanSpeed = acDeviceKeyMap[fanSpeed];
if (deviceMode) acParams.workMode = acDeviceKeyMap[deviceMode];
// 3. 调用飞奕控制接口
const result = await controlIndoorUnit(acParams);
// 4. 记录操作日志
const now = new Date();
const logEntry = {
device_id: deviceId,
device_type: '空调',
operation_content: {
option: '设备管理编辑',
device_id: deviceId,
time: formatLocalTime(now),
power: power,
setTemp: setTemp,
fanSpeed: fanSpeed,
deviceMode: deviceMode,
success: result.success,
message: result.data || (result.success ? '控制成功' : '控制失败'),
},
status: result.success ? 1 : 0,
created_at: now,
updated_at: now,
};
await addData(TABLENAME.设备日志表, [logEntry]);
if (result.success) {
return { success: true, message: '控制成功' };
} else {
throw new BizError(ERRORENUM.操作失败, result.data || '控制空调设备失败');
}
}
/**
* 设备管理 - 批量编辑设备
* 根据筛选条件查询符合条件的设备,批量修改 power / setTemp
* @returns { total, successCount, failCount, details }
*/
export async function batchEditDevices(params: {
regionKeys: number[];
deviceName?: string;
power?: string;
state?: string;
deviceMode?: string;
startDate?: string;
endDate?: string;
edits: {
power?: string;
setTemp?: number;
linkageStart?: string;
};
}): Promise<{ total: number; successCount: number; failCount: number; details: any[] }> {
const { regionKeys, deviceName, power, state, deviceMode, startDate, endDate, edits } = params;
// 校验必填参数
if (!regionKeys || !Array.isArray(regionKeys) || regionKeys.length === 0) {
throw new BizError(ERRORENUM.参数错误, "regionKeys 不能为空");
}
if (!edits || typeof edits !== 'object') {
throw new BizError(ERRORENUM.参数错误, "edits 不能为空");
}
const hasEdit = edits.power !== undefined || edits.setTemp !== undefined || edits.linkageStart !== undefined;
if (!hasEdit) {
throw new BizError(ERRORENUM.参数错误, "edits 中至少需要 power / setTemp / linkageStart 其中之一");
}
// 1. 构建设备查询条件
let whereParam: any = { device_type: '空调' };
whereParam.region_key = { "%in%": regionKeys };
if (deviceName) {
whereParam.device_name = { "%like%": deviceName };
}
// state 筛选(正常/告警/离线 → device_state)
const stateMap: Record<string, number[]> = {
'正常': [0],
'告警': [1, 2],
'离线': [3],
};
if (state && stateMap[state]) {
whereParam.device_state = { "%in%": stateMap[state] };
}
// 日期范围筛选
if (startDate) {
whereParam.created_at = { ...(whereParam.created_at || {}), "%gte%": startDate + ' 00:00:00' };
}
if (endDate) {
whereParam.created_at = { ...(whereParam.created_at || {}), "%lte%": endDate + ' 23:59:59' };
}
// 2. 查询符合条件的设备
const deviceRes = await selectDataListByParam(
TABLENAME.设备表, whereParam,
["id", "device_id", "region_key", "device_name", "device_state", "created_at"]
);
let devices = (deviceRes.data || []) as any[];
// 3. 飞奕后置过滤:power / deviceMode 需要调飞奕接口逐台确认
const powerMap: Record<string, number> = { '开': 1, '关': 0 };
const modeMap: Record<string, number> = { '制冷': 1, '制热': 2, '通风': 3, '除湿': 4, '自动': 0 };
const needFeiyiFilter = !!(power || deviceMode);
const targetOnOff = power ? powerMap[power] : undefined;
const targetMode = deviceMode ? modeMap[deviceMode] : undefined;
if (needFeiyiFilter && devices.length > 0) {
let filteredDevices: any[] = [];
for (const dev of devices) {
try {
const feiyiRes = await getIndoorUnitList({ indoorUnitAddressFull: dev.device_id, page: 1, limit: 1 });
const row = (feiyiRes && feiyiRes.rows && feiyiRes.rows[0]) ? feiyiRes.rows[0] : {};
if (targetOnOff !== undefined && row.onOff !== targetOnOff) continue;
if (targetMode !== undefined && row.workMode !== targetMode) continue;
filteredDevices.push(dev);
} catch (e) {
console.error(`[batchEditDevices] 获取设备 ${dev.device_id} 飞奕数据失败:`, e);
// 无飞奕数据时默认跳过
}
}
devices = filteredDevices;
}
const total = devices.length;
if (total === 0) {
return { total: 0, successCount: 0, failCount: 0, details: [] };
}
const details: any[] = [];
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;
if (needAcControl) {
const now = new Date();
for (const dev of devices) {
try {
let acParams: any = {
"indoorUnitAddressFull": [dev.device_id],
"openApiAction": "control",
};
if (edits.power !== undefined) {
acParams.onOff = powerMap[edits.power] ?? 1;
}
if (edits.setTemp !== undefined) {
acParams.tempSet = edits.setTemp;
}
const result = await controlIndoorUnit(acParams);
// 记录操作日志
const logEntry = {
device_id: dev.device_id,
device_type: '空调',
operation_content: {
option: '批量编辑',
device_id: dev.device_id,
time: formatLocalTime(now),
power: edits.power,
setTemp: edits.setTemp,
linkageStart: edits.linkageStart,
success: result.success,
message: result.data || (result.success ? '控制成功' : '控制失败'),
},
status: result.success ? 1 : 0,
created_at: now,
updated_at: now,
};
await addData(TABLENAME.设备日志表, [logEntry]);
if (result.success) {
successCount++;
details.push({ deviceId: dev.device_id, success: true });
} else {
failCount++;
details.push({ deviceId: dev.device_id, success: false, message: result.data || '控制失败' });
}
} catch (e: any) {
failCount++;
details.push({ deviceId: dev.device_id, success: false, message: e.message || '未知错误' });
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 };
}
// ======= 设备表本地信息编辑(不涉及飞奕API) =======
/**
* 编辑单台设备的表信息
* @param params deviceId + 可编辑字段(至少一个)
*/
export async function editLocalDevice(params: {
deviceId: string;
deviceName?: string;
regionKey?: number;
deviceType?: string;
deviceState?: string;
linkageStart?: string;
}) {
const { deviceId, deviceName, regionKey, deviceType, deviceState, linkageStart } = params;
// 校验设备存在
const deviceRes = await selectOneDataByParam(TABLENAME.设备表, { device_id: deviceId });
if (!deviceRes.data) {
throw new BizError(ERRORENUM.参数错误, `设备 ${deviceId} 不存在`);
}
// 构建更新字段(驼峰 → 数据库字段)
const dbEdits: any = {};
if (deviceName !== undefined) dbEdits.device_name = deviceName;
if (regionKey !== undefined) dbEdits.region_key = regionKey;
if (deviceType !== undefined) dbEdits.device_type = deviceType;
if (deviceState !== undefined) dbEdits.device_state = deviceState;
if (linkageStart !== undefined) dbEdits.linkage_start = linkageStart;
if (Object.keys(dbEdits).length === 0) {
throw new BizError(ERRORENUM.参数错误, "至少需要提供一个编辑字段");
}
await updateManyData(TABLENAME.设备表, { device_id: deviceId }, dbEdits);
console.log(`[editLocalDevice] 更新设备 ${deviceId}:`, dbEdits);
return { isSuccess: true, deviceId };
}
/**
* 批量编辑设备表信息
* @param params regionKeys/deviceName 筛选条件 + edits 编辑内容
*/
export async function batchEditLocalDevices(params: {
regionKeys?: string[];
deviceName?: string;
edits: {
deviceName?: string;
regionKey?: number;
deviceType?: string;
controlParams?: string;
deviceState?: string;
};
}) {
const { regionKeys, deviceName, edits } = params;
// 构建查询条件
const where: any = {};
if (regionKeys && regionKeys.length > 0) {
where.region_key = { "%in%": regionKeys };
}
if (deviceName) {
where.device_name = { "%like%": deviceName };
}
if (Object.keys(where).length === 0) {
throw new BizError(ERRORENUM.参数错误, "至少需要提供 regionKeys 或 deviceName 筛选条件");
}
// 构建更新字段
const dbEdits: any = {};
if (edits.deviceName !== undefined) dbEdits.device_name = edits.deviceName;
if (edits.regionKey !== undefined) dbEdits.region_key = edits.regionKey;
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 (Object.keys(dbEdits).length === 0) {
throw new BizError(ERRORENUM.参数错误, "edits 中至少需要一个编辑字段");
}
await updateManyData(TABLENAME.设备表, where, dbEdits);
console.log(`[batchEditLocalDevices] 批量更新,条件:`, where, "更新内容:", dbEdits);
return { isSuccess: true };
}
\ No newline at end of file
...@@ -7,6 +7,8 @@ import * as crypto from 'crypto'; ...@@ -7,6 +7,8 @@ import * as crypto from 'crypto';
import { post, get } from '../util/request'; import { post, get } from '../util/request';
import { BizError } from '../util/bizError'; import { BizError } from '../util/bizError';
import { ERRORENUM } from '../config/errorEnum'; import { ERRORENUM } from '../config/errorEnum';
import { addData } from '../data/addData';
import { TABLENAME } from '../config/dbEnum';
// 全局变量缓存 token 和过期时间 // 全局变量缓存 token 和过期时间
let feiYiToken = ""; let feiYiToken = "";
...@@ -124,6 +126,20 @@ export async function getFeiYiToken(): Promise<string> { ...@@ -124,6 +126,20 @@ export async function getFeiYiToken(): Promise<string> {
} }
/** /**
* 接口路径 -> 中文描述映射
*/
const PATH_DESC_MAP: Record<string, string> = {
'/openApi/gw/page': '查询网关设备列表',
'/openApi/indoorUnit/list': '查询内机列表',
'/openApi/indoorUnit/control': '控制内机开关',
'/openApi/building/tree': '查询建筑物树结构',
'/openApi/electricMeter/getAll': '查询电表列表',
'/openApi/electricMeterState/list': '查询抄表记录',
'/openApi/indoorUnit/event/switch': '查询内机开关机记录',
'/openApi/indoorUnit/alarm/errorHis': '查询内机故障记录',
};
/**
* 带 Token 的 POST 请求封装 * 带 Token 的 POST 请求封装
* 自动获取/刷新 token * 自动获取/刷新 token
* @param path 接口路径(相对路径) * @param path 接口路径(相对路径)
...@@ -131,32 +147,96 @@ export async function getFeiYiToken(): Promise<string> { ...@@ -131,32 +147,96 @@ export async function getFeiYiToken(): Promise<string> {
* @param retry 是否重试(用于 token 过期重试) * @param retry 是否重试(用于 token 过期重试)
*/ */
async function feiYiPost(path: string, body: any, retry: boolean = true): Promise<any> { async function feiYiPost(path: string, body: any, retry: boolean = true): Promise<any> {
const url = `${FEIYI_CONFIG.baseUrl}${path}`;
const requestTime = new Date();
// 获取 token // 获取 token
const token = await getFeiYiToken(); const token = await getFeiYiToken();
const url = `${FEIYI_CONFIG.baseUrl}${path}`;
const headers = { const headers = {
'Authorization': `Bearer ${token}`, 'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
let result: any; let result: any;
let responseStatus: string;
let responseTime: Date;
let dbStatus: number;
try { try {
result = await post(url, body, headers); result = await post(url, body, headers);
responseTime = new Date();
console.log(`请求 ${path} 成功,响应:`, result?.code); console.log(`请求 ${path} 成功,响应:`, result?.code);
if (result.code === '00000') {
dbStatus = 1;
responseStatus = '正常';
} else {
dbStatus = 0;
responseStatus = '请求报错';
}
} catch (err) { } catch (err) {
responseTime = new Date();
dbStatus = 0;
// 区分连接超时和其他网络错误
if (err.code === 'ETIMEDOUT' || err.code === 'ECONNABORTED' || err.message?.includes('timeout')) {
responseStatus = '连接超时';
} else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
responseStatus = '连接失败';
} else {
responseStatus = '请求报错';
}
// 记录日志(网络异常,无响应数据)
addData(TABLENAME.数据接入日志表, {
partner_name: '飞奕',
request_url: url,
request_desc: PATH_DESC_MAP[path] || '',
request_params: body,
response_data: null,
status: dbStatus,
response_status: responseStatus,
request_time: requestTime,
response_time: responseTime,
}).catch(e => console.error('写入数据接入日志失败:', e));
throw new BizError(ERRORENUM.网络错误, `请求 ${path} 失败: ${err.message}`); throw new BizError(ERRORENUM.网络错误, `请求 ${path} 失败: ${err.message}`);
} }
// 如果 token 过期(示例错误码可能为 token 过期,文档未明确,假设 code 为 401 或其他) // 如果 token 过期,重试一次
// 这里简单判断如果 code 不为 '00000' 且包含 token 相关错误,则重试一次
if (result.code !== '00000') { if (result.code !== '00000') {
if (retry && (result.code === '401' || result.message?.includes('token'))) { if (retry && (result.code === '401' || result.message?.includes('token'))) {
// 可选:清除 token 缓存,重新获取
return feiYiPost(path, body, false); return feiYiPost(path, body, false);
} }
throw new BizError(ERRORENUM.第三方接口错误, `接口 ${path} 调用失败: ${result.message}`); // || JSON.stringify(result) // 非 token 错误,记录日志后抛异常
addData(TABLENAME.数据接入日志表, {
partner_name: '飞奕',
request_url: url,
request_desc: PATH_DESC_MAP[path] || '',
request_params: body,
response_data: result,
status: dbStatus,
response_status: responseStatus,
request_time: requestTime,
response_time: responseTime,
}).catch(e => console.error('写入数据接入日志失败:', e));
throw new BizError(ERRORENUM.第三方接口错误, `接口 ${path} 调用失败: ${result.message}`);
} }
// 成功也记录日志
addData(TABLENAME.数据接入日志表, {
partner_name: '飞奕',
request_url: url,
request_desc: PATH_DESC_MAP[path] || '',
request_params: body,
response_data: result,
status: dbStatus,
response_status: responseStatus,
request_time: requestTime,
response_time: responseTime,
}).catch(e => console.error('写入数据接入日志失败:', e));
return result; return result;
} }
......
...@@ -2,6 +2,7 @@ import { TABLENAME } from "../config/dbEnum"; ...@@ -2,6 +2,7 @@ import { TABLENAME } from "../config/dbEnum";
import { addData } from "../data/addData"; import { addData } from "../data/addData";
import { selectOneDataByParam } from "../data/findData"; import { selectOneDataByParam } from "../data/findData";
import { controlAcByEnvironment } from "./running"; import { controlAcByEnvironment } from "./running";
import { systemConfig } from "../config/serverConfig";
/** /**
* 处理接收到的环境设备数据,并存入数据库 * 处理接收到的环境设备数据,并存入数据库
...@@ -26,14 +27,32 @@ import { controlAcByEnvironment } from "./running"; ...@@ -26,14 +27,32 @@ import { controlAcByEnvironment } from "./running";
* } * }
*/ */
export async function processDeviceData(message) { export async function processDeviceData(message) {
const requestTime = new Date();
const msgStr = message.toString();
const topic = systemConfig.mqttsrv.topic_env;
const writeLog = (status: number, responseStatus: string, responseData?: any) => {
addData(TABLENAME.数据接入日志表, {
partner_name: '星纵物联',
request_url: topic,
request_desc: '环境监测数据推送',
request_params: msgStr,
response_data: responseData ?? null,
status,
response_status: responseStatus,
request_time: requestTime,
response_time: new Date(),
}).catch(e => console.error('写入数据接入日志失败:', e));
};
try { try {
// 解析JSON消息 // 解析JSON消息
const msgStr = message.toString();
let data: any; let data: any;
try { try {
data = JSON.parse(msgStr); data = JSON.parse(msgStr);
} catch (e) { } catch (e) {
console.error('消息不是合法JSON,跳过:', msgStr); console.error('消息不是合法JSON,跳过:', msgStr);
writeLog(0, '消息格式错误');
return; return;
} }
console.log('收到环境数据:', data.length); console.log('收到环境数据:', data.length);
...@@ -42,6 +61,7 @@ export async function processDeviceData(message) { ...@@ -42,6 +61,7 @@ export async function processDeviceData(message) {
const devEUI = data.devEUI ? data.devEUI.toUpperCase() : null; const devEUI = data.devEUI ? data.devEUI.toUpperCase() : null;
if (!devEUI) { if (!devEUI) {
console.warn('消息缺少devEUI字段,跳过'); console.warn('消息缺少devEUI字段,跳过');
writeLog(0, '缺少devEUI');
return; return;
} }
const gatewayTime = data.gatewayTime ? new Date(data.gatewayTime) : new Date(); const gatewayTime = data.gatewayTime ? new Date(data.gatewayTime) : new Date();
...@@ -59,6 +79,7 @@ export async function processDeviceData(message) { ...@@ -59,6 +79,7 @@ export async function processDeviceData(message) {
); );
if (!deviceRow && !deviceRow.data) { if (!deviceRow && !deviceRow.data) {
console.warn(`设备不存在,跳过入库: ${devEUI}`); console.warn(`设备不存在,跳过入库: ${devEUI}`);
writeLog(0, '设备不存在');
return; return;
} }
...@@ -88,6 +109,7 @@ export async function processDeviceData(message) { ...@@ -88,6 +109,7 @@ export async function processDeviceData(message) {
); );
console.log(`数据入库成功: 设备 ${devEUI}`); console.log(`数据入库成功: 设备 ${devEUI}`);
writeLog(1, '正常');
// 晚上 23:00 ~ 次日 05:00 不执行空调自控(休眠时段) // 晚上 23:00 ~ 次日 05:00 不执行空调自控(休眠时段)
const currentHour = new Date().getHours(); const currentHour = new Date().getHours();
...@@ -114,6 +136,7 @@ export async function processDeviceData(message) { ...@@ -114,6 +136,7 @@ export async function processDeviceData(message) {
} catch (error) { } catch (error) {
console.error('数据处理失败:', error); console.error('数据处理失败:', error);
writeLog(0, '数据处理异常');
} }
} }
...@@ -143,14 +166,32 @@ interface CustomerTriggerItem { ...@@ -143,14 +166,32 @@ interface CustomerTriggerItem {
* // region_trigger_data: { region_count_data: [ { region: 1, region_name: 'Region1', region_uuid: '...', total: { current_total: 9 } } ] } * // region_trigger_data: { region_count_data: [ { region: 1, region_name: 'Region1', region_uuid: '...', total: { current_total: 9 } } ] }
*/ */
export async function customerDeviceData(message) { export async function customerDeviceData(message) {
const requestTime = new Date();
const msgStr = message.toString();
const topic = systemConfig.mqttsrv.topic_crowd;
const writeLog = (status: number, responseStatus: string, responseData?: any) => {
addData(TABLENAME.数据接入日志表, {
partner_name: '星纵物联',
request_url: topic,
request_desc: '客流数据推送',
request_params: msgStr,
response_data: responseData ?? null,
status,
response_status: responseStatus,
request_time: requestTime,
response_time: new Date(),
}).catch(e => console.error('写入数据接入日志失败:', e));
};
try { try {
// 解析JSON消息 // 解析JSON消息
const msgStr = message.toString();
let data: any; let data: any;
try { try {
data = JSON.parse(msgStr); data = JSON.parse(msgStr);
} catch (e) { } catch (e) {
console.error('消息不是合法JSON,跳过:', msgStr); console.error('消息不是合法JSON,跳过:', msgStr);
writeLog(0, '消息格式错误');
return; return;
} }
console.log('收到客流数据:', msgStr); console.log('收到客流数据:', msgStr);
...@@ -159,11 +200,13 @@ export async function customerDeviceData(message) { ...@@ -159,11 +200,13 @@ export async function customerDeviceData(message) {
const deviceInfo = data.device_info; const deviceInfo = data.device_info;
if (!deviceInfo) { if (!deviceInfo) {
console.warn('消息缺少device_info字段,跳过'); console.warn('消息缺少device_info字段,跳过');
writeLog(0, '缺少device_info');
return; return;
} }
const deviceSn = deviceInfo.device_sn ? deviceInfo.device_sn.toUpperCase() : null; // 设备序列号,转大写作为设备标识 const deviceSn = deviceInfo.device_sn ? deviceInfo.device_sn.toUpperCase() : null; // 设备序列号,转大写作为设备标识
if (!deviceSn) { if (!deviceSn) {
console.warn('消息缺少device_info.device_sn字段,跳过'); console.warn('消息缺少device_info.device_sn字段,跳过');
writeLog(0, '缺少device_sn');
return; return;
} }
const deviceMac = deviceInfo.device_mac; const deviceMac = deviceInfo.device_mac;
...@@ -206,6 +249,7 @@ export async function customerDeviceData(message) { ...@@ -206,6 +249,7 @@ export async function customerDeviceData(message) {
if (triggerItems.length === 0) { if (triggerItems.length === 0) {
console.warn('消息缺少客流线路数据,跳过'); console.warn('消息缺少客流线路数据,跳过');
writeLog(0, '缺少客流线路数据');
return; return;
} }
...@@ -254,9 +298,11 @@ export async function customerDeviceData(message) { ...@@ -254,9 +298,11 @@ export async function customerDeviceData(message) {
} }
console.log(`客流数据处理完成: 设备 ${deviceSn}, 共入库 ${triggerItems.length} 条数据`); console.log(`客流数据处理完成: 设备 ${deviceSn}, 共入库 ${triggerItems.length} 条数据`);
writeLog(1, '正常');
} catch (error) { } catch (error) {
console.error('客流数据处理失败:', error); console.error('客流数据处理失败:', error);
writeLog(0, '数据处理异常');
} }
} }
......
...@@ -219,7 +219,13 @@ function calcMeterDeviceDaily(records: any[]): number { ...@@ -219,7 +219,13 @@ 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> {
const { start: monday, end: sunday, timer } = calcWeekRange(weekOffset); // ==== 固定查询 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)}`;
console.log(`[周报告JSON] 查询数据: ${timer}`); console.log(`[周报告JSON] 查询数据: ${timer}`);
// ===== 第一步:并行查询区域 + 各馆设备分组 ===== // ===== 第一步:并行查询区域 + 各馆设备分组 =====
...@@ -394,12 +400,15 @@ const ENV_INDICATOR_KEYS = [ ...@@ -394,12 +400,15 @@ const ENV_INDICATOR_KEYS = [
*/ */
export async function getEnvIndicatorsWeeklyAvg(): Promise<any> { export async function getEnvIndicatorsWeeklyAvg(): Promise<any> {
// 1. 计算七天时间范围 // 1. 计算七天时间范围
const now = new Date(); // ==== 固定查询 7月29日 ~ 8月4日 ====
const end = new Date(now); // const now = new Date();
end.setHours(23, 59, 59, 999); // const end = new Date(now);
const start = new Date(now); // end.setHours(23, 59, 59, 999);
start.setDate(now.getDate() - 6); // const start = new Date(now);
start.setHours(0, 0, 0, 0); // 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 from = formatLocalTime(start); const from = formatLocalTime(start);
const to = formatLocalTime(end); const to = formatLocalTime(end);
...@@ -486,12 +495,15 @@ const LEVEL_MAP: { [key: number]: string } = { 1: "一级", 2: "二级", 3: "三 ...@@ -486,12 +495,15 @@ const LEVEL_MAP: { [key: number]: string } = { 1: "一级", 2: "二级", 3: "三
* 每组展示:故障代码、风险内容、报警时间、处理状态、报警设备 * 每组展示:故障代码、风险内容、报警时间、处理状态、报警设备
*/ */
export async function getFaultInfoLast7Days(): Promise<any> { export async function getFaultInfoLast7Days(): Promise<any> {
const now = new Date(); // ==== 固定查询 7月29日 ~ 8月4日 ====
const end = new Date(now); // const now = new Date();
end.setHours(23, 59, 59, 999); // const end = new Date(now);
const start = new Date(now); // end.setHours(23, 59, 59, 999);
start.setDate(now.getDate() - 6); // const start = new Date(now);
start.setHours(0, 0, 0, 0); // 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 from = formatLocalTime(start); const from = formatLocalTime(start);
const to = formatLocalTime(end); const to = formatLocalTime(end);
......
...@@ -27,7 +27,7 @@ const envControlCache: Map<string, { tempSet?: number; workMode?: number; fanSpe ...@@ -27,7 +27,7 @@ const envControlCache: Map<string, { tempSet?: number; workMode?: number; fanSpe
* 将 Date 对象格式化为本地时间字符串 'YYYY-MM-DD HH:mm:ss' * 将 Date 对象格式化为本地时间字符串 'YYYY-MM-DD HH:mm:ss'
* 替代 toISOString() 避免 UTC 时区偏移问题 * 替代 toISOString() 避免 UTC 时区偏移问题
*/ */
function formatLocalTime(date: Date): string { export function formatLocalTime(date: Date): string {
const pad = (n: number) => String(n).padStart(2, '0'); const pad = (n: number) => String(n).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
} }
...@@ -68,6 +68,21 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -68,6 +68,21 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
let currentMonthStart = new Date(); currentMonthStart.setDate(1); currentMonthStart.setHours(0,0,0,0); let currentMonthStart = new Date(); currentMonthStart.setDate(1); currentMonthStart.setHours(0,0,0,0);
let lastMonthStart = new Date(); lastMonthStart.setMonth(lastMonthStart.getMonth() - 1); lastMonthStart.setDate(1); lastMonthStart.setHours(0,0,0,0); let lastMonthStart = new Date(); lastMonthStart.setMonth(lastMonthStart.getMonth() - 1); lastMonthStart.setDate(1); lastMonthStart.setHours(0,0,0,0);
let lastMonthEnd = new Date(currentMonthStart.getTime() - 1); let lastMonthEnd = new Date(currentMonthStart.getTime() - 1);
// 昨日同时段结束时间(日同比窗口)
let yesterdaySameTimeEnd = new Date(yesterdayStart);
yesterdaySameTimeEnd.setHours(nowTime.getHours(), nowTime.getMinutes(), nowTime.getSeconds(), nowTime.getMilliseconds());
// 上月同比截止日(处理月份天数差异)
let todayDay = nowTime.getDate();
let thisMonthMaxDay = new Date(nowTime.getFullYear(), nowTime.getMonth() + 1, 0).getDate();
let lastMonthMaxDay = new Date(nowTime.getFullYear(), nowTime.getMonth(), 0).getDate();
let comparisonDay = todayDay;
if (todayDay == thisMonthMaxDay && thisMonthMaxDay < lastMonthMaxDay) {
comparisonDay = lastMonthMaxDay;
} else {
comparisonDay = Math.min(todayDay, lastMonthMaxDay);
}
let lastMonthComparisonEnd = new Date(nowTime.getFullYear(), nowTime.getMonth() - 1, comparisonDay,
nowTime.getHours(), nowTime.getMinutes(), nowTime.getSeconds(), nowTime.getMilliseconds());
let last24hStart = new Date(nowTime.getTime() - 24 * 3600000); let last24hStart = new Date(nowTime.getTime() - 24 * 3600000);
let last7DaysStart = new Date(nowTime.getTime() - 7 * 86400000); let last7DaysStart = new Date(nowTime.getTime() - 7 * 86400000);
let last30DaysStart = new Date(nowTime.getTime() - 30 * 86400000); let last30DaysStart = new Date(nowTime.getTime() - 30 * 86400000);
...@@ -85,11 +100,13 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -85,11 +100,13 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
}, ["device_id", "data", "device_time"]); }, ["device_id", "data", "device_time"]);
// 一次遍历,同时产出:4个时段总量 + 4组趋势数据 // 一次遍历,同时产出:4个时段总量 + 4组趋势数据
let todayTotal = 0, yesterdayTotal = 0, currentMonthTotal = 0, previousMonthTotal = 0; let todayTotal = 0, yesterdayTotal = 0, yesterdayWindowTotal = 0, currentMonthTotal = 0, previousMonthTotal = 0, previousMonthWindowTotal = 0;
let deviceTodayMap = new Map<string, number>(); let deviceTodayMap = new Map<string, number>();
let deviceYesterdayMap = new Map<string, number>(); let deviceYesterdayMap = new Map<string, number>();
let deviceYesterdayWindowMap = new Map<string, number>();
let deviceMonthMap = new Map<string, number>(); let deviceMonthMap = new Map<string, number>();
let devicePrevMonthMap = new Map<string, number>(); let devicePrevMonthMap = new Map<string, number>();
let devicePrevMonthWindowMap = new Map<string, number>();
let hourlyMap = new Map<string, number>(); let hourlyMap = new Map<string, number>();
let daily7Map = new Map<string, number>(); let daily7Map = new Map<string, number>();
let daily30Map = new Map<string, number>(); let daily30Map = new Map<string, number>();
...@@ -111,12 +128,18 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -111,12 +128,18 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
if (ts >= yesterdayStart.getTime() && ts <= yesterdayEnd.getTime()) { if (ts >= yesterdayStart.getTime() && ts <= yesterdayEnd.getTime()) {
deviceYesterdayMap.set(deviceId, (deviceYesterdayMap.get(deviceId) || 0) + energy); deviceYesterdayMap.set(deviceId, (deviceYesterdayMap.get(deviceId) || 0) + energy);
} }
if (ts >= yesterdayStart.getTime() && ts <= yesterdaySameTimeEnd.getTime()) {
deviceYesterdayWindowMap.set(deviceId, (deviceYesterdayWindowMap.get(deviceId) || 0) + energy);
}
if (ts >= currentMonthStart.getTime()) { if (ts >= currentMonthStart.getTime()) {
deviceMonthMap.set(deviceId, (deviceMonthMap.get(deviceId) || 0) + energy); deviceMonthMap.set(deviceId, (deviceMonthMap.get(deviceId) || 0) + energy);
} }
if (ts >= lastMonthStart.getTime() && ts <= lastMonthEnd.getTime()) { if (ts >= lastMonthStart.getTime() && ts <= lastMonthEnd.getTime()) {
devicePrevMonthMap.set(deviceId, (devicePrevMonthMap.get(deviceId) || 0) + energy); devicePrevMonthMap.set(deviceId, (devicePrevMonthMap.get(deviceId) || 0) + energy);
} }
if (ts >= lastMonthStart.getTime() && ts <= lastMonthComparisonEnd.getTime()) {
devicePrevMonthWindowMap.set(deviceId, (devicePrevMonthWindowMap.get(deviceId) || 0) + energy);
}
// --- 趋势分组 --- // --- 趋势分组 ---
if (ts >= last24hStart.getTime()) { if (ts >= last24hStart.getTime()) {
...@@ -138,8 +161,10 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -138,8 +161,10 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
// 汇总各时段总量 // 汇总各时段总量
for (let v of deviceTodayMap.values()) todayTotal += v; for (let v of deviceTodayMap.values()) todayTotal += v;
for (let v of deviceYesterdayMap.values()) yesterdayTotal += v; for (let v of deviceYesterdayMap.values()) yesterdayTotal += v;
for (let v of deviceYesterdayWindowMap.values()) yesterdayWindowTotal += v;
for (let v of deviceMonthMap.values()) currentMonthTotal += v; for (let v of deviceMonthMap.values()) currentMonthTotal += v;
for (let v of devicePrevMonthMap.values()) previousMonthTotal += v; for (let v of devicePrevMonthMap.values()) previousMonthTotal += v;
for (let v of devicePrevMonthWindowMap.values()) previousMonthWindowTotal += v;
// map 转为排序后的数组 // map 转为排序后的数组
let hourlyEnergy: { key: string, value: string }[] = []; let hourlyEnergy: { key: string, value: string }[] = [];
...@@ -158,8 +183,8 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -158,8 +183,8 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
monthlyMap.forEach((v, k) => monthlyEnergyYear.push({ key: k, value: v.toFixed(0) })); monthlyMap.forEach((v, k) => monthlyEnergyYear.push({ key: k, value: v.toFixed(0) }));
monthlyEnergyYear.sort((a, b) => a.key.localeCompare(b.key)); monthlyEnergyYear.sort((a, b) => a.key.localeCompare(b.key));
let dailyYearOnYear = yesterdayTotal === 0 ? 0 : (((todayTotal - yesterdayTotal) / yesterdayTotal) * 100).toFixed(2); let dailyYearOnYear = yesterdayWindowTotal === 0 ? 0 : (((todayTotal - yesterdayWindowTotal) / yesterdayWindowTotal) * 100).toFixed(2);
let monthlyYearOnYear = previousMonthTotal === 0 ? 0 : (((currentMonthTotal - previousMonthTotal) / previousMonthTotal) * 100).toFixed(2); let monthlyYearOnYear = previousMonthWindowTotal === 0 ? 0 : (((currentMonthTotal - previousMonthWindowTotal) / previousMonthWindowTotal) * 100).toFixed(2);
// 补全 x 轴坐标的工具函数 // 补全 x 轴坐标的工具函数
function padTrend(trend: any[], allKeys: string[]): any[] { function padTrend(trend: any[], allKeys: string[]): any[] {
...@@ -196,10 +221,12 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -196,10 +221,12 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
// 1.4. 能耗监控数据和能耗监控趋势图数据 // 1.4. 能耗监控数据和能耗监控趋势图数据
resultAny.energyManagement = { resultAny.energyManagement = {
todayElectricity: todayTotal.toFixed(0), todayElectricity: todayTotal.toFixed(0),
yesterdayElectricity: yesterdayTotal.toFixed(0), yesterdayElectricity: yesterdayWindowTotal.toFixed(0),
yesterdayTotalElectricity: yesterdayTotal.toFixed(0),
dailyYearOnYear, dailyYearOnYear,
currentMonthElectricity: currentMonthTotal.toFixed(0), currentMonthElectricity: currentMonthTotal.toFixed(0),
previousMonthElectricity: previousMonthTotal.toFixed(0), previousMonthElectricity: previousMonthWindowTotal.toFixed(0),
previousMonthTotalElectricity: previousMonthTotal.toFixed(0),
monthlyYearOnYear monthlyYearOnYear
}; };
resultAny.electricityTrend = { resultAny.electricityTrend = {
...@@ -293,44 +320,52 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -293,44 +320,52 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
device_time: { "%gte%": formatLocalTime(todayStartEnv), "%lte%": formatLocalTime(nowTime) } device_time: { "%gte%": formatLocalTime(todayStartEnv), "%lte%": formatLocalTime(nowTime) }
}, ["data", "device_time"]); }, ["data", "device_time"]);
let hourlyData = new Map(); let hourlyData = new Map();
const fieldMap: Record<string, string> = { temp: 'temperature', hum: 'humidity', pm: 'pm2_5', co2: 'co2', hcho: 'hcho', lightLevel: 'light_level', pm10: 'pm10', pressure: 'pressure', tvoc: 'tvoc' };
for (let rec of airHistory.data) { for (let rec of airHistory.data) {
let d = new Date(rec.device_time); let d = new Date(rec.device_time);
let hourKey = `${d.getHours()}`; let hourKey = `${d.getHours()}`;
let data = rec.data; let data = rec.data;
if (!hourlyData.has(hourKey)) { let cur = hourlyData.get(hourKey);
hourlyData.set(hourKey, { temp: data.temperature, hum: data.humidity, pm: data.pm2_5, co2: data.co2 if (!cur) {
, hcho: data.hcho, lightLevel: data.light_level, pm10: data.pm10, pressure: data.pressure, tvoc: data.tvoc }); cur = { cnt: 0 };
hourlyData.set(hourKey, cur);
}
cur.cnt++;
for (let field of ['temp', 'hum', 'pm', 'co2', 'hcho', 'lightLevel', 'pm10', 'pressure', 'tvoc']) {
let key = field + 'Sum';
cur[key] = (cur[key] || 0) + (Number(data[fieldMap[field]]) || 0);
} }
} }
const currentHour = nowTime.getHours(); const currentHour = nowTime.getHours();
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) {
temperatureTrend.push({ time: `${i}:00`, value: val?.temp?.toString() ?? '0' }); temperatureTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('tempSum') : '0' });
humidityTrend.push({ time: `${i}:00`, value: val?.hum?.toString() ?? '0' }); humidityTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('humSum') : '0' });
pm25Trend.push({ time: `${i}:00`, value: val?.pm?.toString() ?? '0' }); pm25Trend.push({ time: `${i}:00`, value: val?.cnt ? avg('pmSum') : '0' });
let co2Val = val?.co2 ?? 0; let co2Val = val?.cnt ? ((val['co2Sum'] || 0) / val.cnt) : 0;
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.toString() }); co2Trend.push({ time: `${i}时`, value: co2Val.toFixed(0) });
hchoTrend.push({ time: `${i}:00`, value: val?.hcho?.toString() ?? '0' }); hchoTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('hchoSum') : '0' });
lightLevelTrend.push({ time: `${i}:00`, value: val?.lightLevel?.toString() ?? '0' }); lightLevelTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('lightLevelSum') : '0' });
pm10Trend.push({ time: `${i}:00`, value: val?.pm10?.toString() ?? '0' }); pm10Trend.push({ time: `${i}:00`, value: val?.cnt ? avg('pm10Sum') : '0' });
pressureTrend.push({ time: `${i}:00`, value: val?.pressure?.toString() ?? '0' }); pressureTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('pressureSum') : '0' });
tvocTrend.push({ time: `${i}:00`, value: val?.tvoc?.toString() ?? '0' }); tvocTrend.push({ time: `${i}:00`, value: val?.cnt ? avg('tvocSum') : '0' });
} else { } else {
temperatureTrend.push({ time: `${i}:00`, value: null }); temperatureTrend.push({ time: `${i}:00`, value: '0' });
humidityTrend.push({ time: `${i}:00`, value: null }); humidityTrend.push({ time: `${i}:00`, value: '0' });
pm25Trend.push({ time: `${i}:00`, value: null }); pm25Trend.push({ time: `${i}:00`, value: '0' });
co2TrendDetail.push({ time: `${i}:00`, value: null }); co2TrendDetail.push({ time: `${i}:00`, value: '0' });
co2Trend.push({ time: `${i}时`, value: null }); co2Trend.push({ time: `${i}时`, value: '0' });
hchoTrend.push({ time: `${i}:00`, value: null }); hchoTrend.push({ time: `${i}:00`, value: '0' });
lightLevelTrend.push({ time: `${i}:00`, value: null }); lightLevelTrend.push({ time: `${i}:00`, value: '0' });
pm10Trend.push({ time: `${i}:00`, value: null }); pm10Trend.push({ time: `${i}:00`, value: '0' });
pressureTrend.push({ time: `${i}:00`, value: null }); pressureTrend.push({ time: `${i}:00`, value: '0' });
tvocTrend.push({ time: `${i}:00`, value: null }); tvocTrend.push({ time: `${i}:00`, value: '0' });
} }
} }
} }
...@@ -377,31 +412,29 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -377,31 +412,29 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
let alertWorkOrders: any[] = []; let alertWorkOrders: any[] = [];
try { try {
const todayFilter = { "%gte%": formatLocalTime(todayStart) };
const thirtyDaysAgo = new Date(nowTime.getTime() - 30 * 86400000); const thirtyDaysAgo = new Date(nowTime.getTime() - 30 * 86400000);
const timeFilter = { "%gte%": formatLocalTime(thirtyDaysAgo) }; const thirtyDayFilter = { "%gte%": formatLocalTime(thirtyDaysAgo) };
const statsRes = await selectDataListByParam(TABLENAME.设备故障表, // alertWorkLevel:查今日三个级别的预警数量
{ occurred_time: timeFilter }, const levelRes = await selectDataListByParam(TABLENAME.设备故障表,
["status", "level", "fault_type"]); { occurred_time: todayFilter },
const statsFaults: any[] = Array.isArray(statsRes.data) ? statsRes.data : []; ["level"]);
const totalAlerts = statsFaults.length; const levelFaults: any[] = Array.isArray(levelRes.data) ? levelRes.data : [];
for (const f of levelFaults) {
let resolved = 0, responded = 0, unresolved = 0;
for (const f of statsFaults) {
if (f.status === 2) resolved++;
else if (f.status === 1) responded++;
else if (f.status === 0) unresolved++;
}
alertStatus = { totalAlerts, resolved, responded, unresolved };
for (const f of statsFaults) {
if (f.level === 1) alertWorkLevel["一级预警"]++; if (f.level === 1) alertWorkLevel["一级预警"]++;
else if (f.level === 2) alertWorkLevel["二级预警"]++; else if (f.level === 2) alertWorkLevel["二级预警"]++;
else if (f.level === 3) alertWorkLevel["三级预警"]++; else if (f.level === 3) alertWorkLevel["三级预警"]++;
} }
// alertWorkType:查今日各故障类型占比
const todayTypeRes = await selectDataListByParam(TABLENAME.设备故障表,
{ occurred_time: todayFilter },
["fault_type"]);
const todayTypeFaults: any[] = Array.isArray(todayTypeRes.data) ? todayTypeRes.data : [];
const todayAlertTotal = todayTypeFaults.length;
let faultTypeCountMap = new Map<string, number>(); let faultTypeCountMap = new Map<string, number>();
for (const f of statsFaults) { for (const f of todayTypeFaults) {
const typeName = f.fault_type || "其他"; const typeName = f.fault_type || "其他";
faultTypeCountMap.set(typeName, (faultTypeCountMap.get(typeName) || 0) + 1); faultTypeCountMap.set(typeName, (faultTypeCountMap.get(typeName) || 0) + 1);
} }
...@@ -409,13 +442,30 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -409,13 +442,30 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
alertWorkType.push({ alertWorkType.push({
count, count,
name, name,
proportion: totalAlerts > 0 ? ((count / totalAlerts) * 100).toFixed(0) + '%' : '0%' proportion: todayAlertTotal > 0 ? ((count / todayAlertTotal) * 100).toFixed(0) + '%' : '0%'
}); });
} }
// alertStatus:查近 30 天不同状态的预警
const statsRes = await selectDataListByParam(TABLENAME.设备故障表,
{ occurred_time: thirtyDayFilter },
["status"]);
const statsFaults: any[] = Array.isArray(statsRes.data) ? statsRes.data : [];
const totalAlerts = statsFaults.length;
let resolved = 0, responded = 0, unresolved = 0;
for (const f of statsFaults) {
if (f.status === 2) resolved++;
else if (f.status === 1) responded++;
else if (f.status === 0) unresolved++;
}
alertStatus = { totalAlerts, resolved, responded, unresolved };
// alertWorkOrders:查近 30 天未处理的预警列表
const ordersRes = await selectDataListByParam(TABLENAME.设备故障表, const ordersRes = await selectDataListByParam(TABLENAME.设备故障表,
{ {
occurred_time: timeFilter, occurred_time: thirtyDayFilter,
status: 0,
"%orderDesc%": "occurred_time", "%orderDesc%": "occurred_time",
"%limit%": 10 "%limit%": 10
}, },
...@@ -626,10 +676,9 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -626,10 +676,9 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
let acOnlineTrend: { time: string; value: string }[] = []; let acOnlineTrend: { time: string; value: string }[] = [];
if (acDeviceIds.length) { if (acDeviceIds.length) {
let last24h = new Date(nowTime.getTime() - 24 * 3600000);
let acHistory = await selectDataListByParam(TABLENAME.设备数据表, { let acHistory = await selectDataListByParam(TABLENAME.设备数据表, {
device_id: { "%in%": acDeviceIds }, device_id: { "%in%": acDeviceIds },
device_time: { "%gte%": formatLocalTime(last24h) } device_time: { "%gte%": formatLocalTime(todayStart), "%lte%": formatLocalTime(nowTime) }
}, ["device_id", "data", "device_time"]); }, ["device_id", "data", "device_time"]);
let hourlyData = new Map(); let hourlyData = new Map();
for (let rec of acHistory.data) { for (let rec of acHistory.data) {
...@@ -645,10 +694,15 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -645,10 +694,15 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
hourlyData.set(hourKey, v); hourlyData.set(hourKey, v);
} }
} }
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' }); acOnlineTrend.push({ time: `${i}:00`, value: val ? ((val.online / val.total) * 100).toFixed(2) : '0' });
} else {
acOnlineTrend.push({ time: `${i}:00`, value: '0' });
}
} }
} }
return { acDeviceIds, acCount, acOnlineCount, acOfflineCount, acFaultCount, acOnlineTrend }; return { acDeviceIds, acCount, acOnlineCount, acOfflineCount, acFaultCount, acOnlineTrend };
...@@ -898,13 +952,14 @@ export async function controlAcRunning(params: {}) { ...@@ -898,13 +952,14 @@ export async function controlAcRunning(params: {}) {
// 手动控制日志 // 手动控制日志
const now = new Date(); const now = new Date();
const operationTime = new Date(); const operationTime = new Date();
const operationTimeStr = formatLocalTime(operationTime);
const acLogs = [{ const acLogs = [{
device_id: deviceId, device_id: deviceId,
device_type: '空调', device_type: '空调',
operation_content: { operation_content: {
option: '手动控制', option: '手动控制',
device_id: deviceId, device_id: deviceId,
time: operationTime, time: operationTimeStr,
mode: mode, mode: mode,
power: power, power: power,
setTemp: setTemp, setTemp: setTemp,
...@@ -994,9 +1049,9 @@ function getSeason(): 'summer' | 'winter' { ...@@ -994,9 +1049,9 @@ function getSeason(): 'summer' | 'winter' {
function calcAcTempSet(temperature: number, season: 'summer' | 'winter'): { tempSet: number; fanSpeed: number; onOff: number } { function calcAcTempSet(temperature: number, season: 'summer' | 'winter'): { tempSet: number; fanSpeed: number; onOff: number } {
if (season === 'summer') { if (season === 'summer') {
// 夏季制冷:温度越高,设定越低 // 夏季制冷:温度越高,设定越低
if (temperature > 21) return { tempSet: 16, fanSpeed: 1, onOff: 1 }; // 未达需求:高风制冷 if (temperature > 24) return { tempSet: 16, fanSpeed: 1, onOff: 1 }; // 未达需求:高风制冷
if (temperature >= 16) return { tempSet: 16, fanSpeed: 4, onOff: 1}; // 舒适区:四舍五入21℃待机 Math.round(temperature) === 21 ? 0 : 1 if (temperature >= 21) return { tempSet: 18, fanSpeed: 2, onOff: 1}; // 舒适区:四舍五入21℃待机 Math.round(temperature) === 21 ? 0 : 1
return { tempSet: 18, fanSpeed: 4, onOff: 1 }; // 已达需求:低风维持 return { tempSet: 19, fanSpeed: 4, onOff: 1 }; // 已达需求:低风维持
} else { } else {
// 冬季制热:温度越低,设定越高 // 冬季制热:温度越低,设定越高
if (temperature < 18) return { tempSet: 21, fanSpeed: 1, onOff: 1 }; // 未达需求:高风制热 if (temperature < 18) return { tempSet: 21, fanSpeed: 1, onOff: 1 }; // 未达需求:高风制热
...@@ -1021,7 +1076,7 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region ...@@ -1021,7 +1076,7 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
}, ["id", "device_name", "region_key"]); }, ["id", "device_name", "region_key"]);
const now = new Date(); const now = new Date();
const operationTime = now.toISOString().replace('T', ' ').slice(0, 19); const operationTime = formatLocalTime(now);
const season = getSeason(); const season = getSeason();
const temperature = data.temperature; const temperature = data.temperature;
const co2 = data.co2; const co2 = data.co2;
...@@ -1038,12 +1093,14 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region ...@@ -1038,12 +1093,14 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
if (fanParams) { if (fanParams) {
let fanDevices = await selectDataListByParam(TABLENAME.设备表, { let fanDevices = await selectDataListByParam(TABLENAME.设备表, {
region_key: regionKey, region_key: regionKey,
device_type: { "%in%": ["新风", "空调"] } device_type: { "%in%": ["新风", "空调"] },
linkage_start: 'on'
}, ["id", "device_name", "device_id", "device_ad", "device_type"]); }, ["id", "device_name", "device_id", "device_ad", "device_type"]);
if (fanDevices.data.length > 0) { if (fanDevices.data.length > 0) {
// CO2联动:高→送风模式(3)+高风(1),低→不传workMode(不切换模式)+低风(4) // CO2联动:高→送风模式(3)+高风(1),低→不传workMode(不切换模式)+低风(4)
const fanWorkMode: number | undefined = fanParams.action === 'high' ? 3 : undefined; // const fanWorkMode: number | undefined = fanParams.action === 'high' ? 3 : undefined;
const fanWorkMode = 1;
const fanSpeedVal = fanParams.action === 'high' ? 1 : 4; const fanSpeedVal = fanParams.action === 'high' ? 1 : 4;
// 去重 // 去重
...@@ -1067,8 +1124,8 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region ...@@ -1067,8 +1124,8 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
device_id: deviceId, device_id: deviceId,
time: operationTime, time: operationTime,
roomCo2: co2, roomCo2: co2,
mode: fanParams.action === 'high' ? '送风' : '低风待机', mode: fanParams.action === 'high' ? '送风制冷' : '低风待机',
action: fanParams.action === 'high' ? '送风高风' : '低风不切换模式', action: fanParams.action === 'high' ? '送风制冷高风' : '低风不切换模式',
success: fanControlRes.success, success: fanControlRes.success,
message: fanControlRes.message || (fanControlRes.success ? '控制成功' : '控制失败'), message: fanControlRes.message || (fanControlRes.success ? '控制成功' : '控制失败'),
setTemp: temp?.tempSet, setTemp: temp?.tempSet,
...@@ -1080,7 +1137,7 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region ...@@ -1080,7 +1137,7 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
await addData(TABLENAME.设备日志表, fanLogs); await addData(TABLENAME.设备日志表, fanLogs);
if (fanControlRes.success) { if (fanControlRes.success) {
console.log(`新风联动成功: CO2 ${co2}ppm → ${fanParams.action === 'high' ? '送风高风' : '低风待机'}, 设备 ${deviceId}`); console.log(`新风联动成功: CO2 ${co2}ppm → ${fanParams.action === 'high' ? '送风制冷高风' : '低风待机'}, 设备 ${deviceId}`);
} else { } else {
console.error(`新风联动失败: ${fanControlRes.message}`); console.error(`新风联动失败: ${fanControlRes.message}`);
} }
...@@ -1093,7 +1150,8 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region ...@@ -1093,7 +1150,8 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
if (temperature != null) { if (temperature != null) {
let acDevices = await selectDataListByParam(TABLENAME.设备表, { let acDevices = await selectDataListByParam(TABLENAME.设备表, {
region_key: regionKey, region_key: regionKey,
device_type: "空调" device_type: "空调",
linkage_start: 'on'
}, ["id", "device_name", "device_id", "device_ad", "control_params"]); }, ["id", "device_name", "device_id", "device_ad", "control_params"]);
if (acDevices.data.length > 0) { if (acDevices.data.length > 0) {
...@@ -1180,6 +1238,8 @@ async function controlAcRunningApi(acDevices?: any, workMode?: number, tempSet?: ...@@ -1180,6 +1238,8 @@ async function controlAcRunningApi(acDevices?: any, workMode?: number, tempSet?:
// workMode 为 undefined 时不传,飞奕不切换模式 // workMode 为 undefined 时不传,飞奕不切换模式
if (workMode != null) { if (workMode != null) {
params.workMode = workMode; params.workMode = workMode;
} else {
params.workMode = 1; // 设置默认为制冷模式
} }
// 如果传入了设定温度,一并下发 // 如果传入了设定温度,一并下发
if (tempSet != null) { if (tempSet != null) {
...@@ -1244,7 +1304,7 @@ export async function stopScheduleTask() { ...@@ -1244,7 +1304,7 @@ export async function stopScheduleTask() {
* 1 高风-超强 2 中风-强 4 低风-弱 * 1 高风-超强 2 中风-强 4 低风-弱
* 0 不锁定-生效 1 锁定-解除 * 0 不锁定-生效 1 锁定-解除
*/ */
let acDeviceKeyMap: { [key: string]: number } = { export let acDeviceKeyMap: { [key: string]: number } = {
"制冷": 1, "制冷": 1,
"制热": 2, "制热": 2,
"送风": 3, "送风": 3,
...@@ -1255,7 +1315,12 @@ let acDeviceKeyMap: { [key: string]: number } = { ...@@ -1255,7 +1315,12 @@ let acDeviceKeyMap: { [key: string]: number } = {
"强": 2, "强": 2,
"弱": 4, "弱": 4,
"解除": 1, "解除": 1,
"生效": 0 "生效": 0,
"高风": 1,
"中风": 2,
"低风": 4,
"开": 1,
"关": 0
} }
/** /**
......
...@@ -10,7 +10,8 @@ export enum TABLENAME { ...@@ -10,7 +10,8 @@ export enum TABLENAME {
用户信息表 = 'user_info', 用户信息表 = 'user_info',
设备日志表 = 'device_logs', 设备日志表 = 'device_logs',
日程表 = 'schedule', 日程表 = 'schedule',
日程设备关联表 = 'schedule_device' 日程设备关联表 = 'schedule_device',
数据接入日志表 = 'data_access_log'
}; };
/** /**
......
import { response } from "express";
const { Sequelize, DataTypes } = require('sequelize'); const { Sequelize, DataTypes } = require('sequelize');
export const TablesConfig = [ export const TablesConfig = [
...@@ -169,6 +171,12 @@ export const TablesConfig = [ ...@@ -169,6 +171,12 @@ export const TablesConfig = [
defaultValue: 0, defaultValue: 0,
comment: '设备状态:0=正常 1=硬件故障 2=通信故障 3=离线' comment: '设备状态:0=正常 1=硬件故障 2=通信故障 3=离线'
}, },
linkage_start: {
type: DataTypes.STRING(10),
allowNull: true,
defaultValue: 'on',
comment: '联动启用:on=开 off=关'
},
created_at: { created_at: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: false, allowNull: false,
...@@ -417,6 +425,80 @@ export const TablesConfig = [ ...@@ -417,6 +425,80 @@ export const TablesConfig = [
{ type: "hasMany", target: "schedule_device", foreignKey: "schedule_id" } { type: "hasMany", target: "schedule_device", foreignKey: "schedule_id" }
] ]
}, },
// 数据接入日志表
{
tableNameCn: '数据接入日志表',
tableName: 'data_access_log',
schema: {
id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '自增主键'
},
partner_name: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '对接方名称,如“飞奕”'
},
request_url: {
type: DataTypes.STRING(500),
allowNull: false,
comment: '请求的第三方接口地址'
},
request_desc: {
type: DataTypes.STRING(200),
allowNull: true,
comment: '请求描述,如"查询设备列表""控制空调开关"'
},
request_params: {
type: DataTypes.JSON,
allowNull: true,
comment: '请求参数'
},
response_data: {
type: DataTypes.JSON,
allowNull: true,
comment: '第三方返回的数据'
},
status: {
type: DataTypes.TINYINT,
allowNull: false,
defaultValue: 1,
comment: '请求状态,0失败1成功'
},
request_time: {
type: DataTypes.DATE,
allowNull: true,
comment: '请求时间'
},
response_time: {
type: DataTypes.DATE,
allowNull: true,
comment: '响应时间'
},
response_status: {
type: DataTypes.STRING(100),
allowNull: true,
comment: '响应状态描述,如"连接超时""请求报错""正常"等'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '创建时间'
}
},
association: [],
indexes: [
{ fields: ['partner_name'] },
{ fields: ['status'] },
{ fields: ['request_time'] },
{ fields: ['partner_name', 'status'] },
{ fields: ['partner_name', 'request_time'] },
]
},
// 日程设备关联表 // 日程设备关联表
{ {
tableNameCn: '日程设备关联表', tableNameCn: '日程设备关联表',
......
...@@ -19,13 +19,13 @@ async function lanuch() { ...@@ -19,13 +19,13 @@ async function lanuch() {
/**创建http服务 */ /**创建http服务 */
httpServer.createServer(systemConfig.port); httpServer.createServer(systemConfig.port);
/**启动MQTT订阅服务 */ /**启动MQTT订阅服务 */
// await mqttSrv.startMqttClient(); await mqttSrv.startMqttClient();
/**启动定时任务 */ /**启动定时任务 */
// startSchedule(); startSchedule();
/**启动预警工单定时任务(10分钟) */ /**启动预警工单定时任务(10分钟) */
// startAlertSchedule(); startAlertSchedule();
/**启动故障状态更新定时任务(5分钟) */ /**启动故障状态更新定时任务(5分钟) */
// startFaultStatusSchedule(); startFaultStatusSchedule();
/**启动日程执行器 */ /**启动日程执行器 */
// await initScheduleTasks(); // await initScheduleTasks();
......
...@@ -25,7 +25,7 @@ export async function initMysqlModel() { ...@@ -25,7 +25,7 @@ export async function initMysqlModel() {
/**第一步:初始化所有表 */ /**第一步:初始化所有表 */
for (let i = 0; i < TablesConfig.length; i++) { for (let i = 0; i < TablesConfig.length; i++) {
let { tableName, schema } = TablesConfig[i]; let { tableName, schema, indexes } = TablesConfig[i];
if (!tableName) { if (!tableName) {
console.warn(`⚠️ 第 ${i} 个表配置缺少 tableName,跳过`); console.warn(`⚠️ 第 ${i} 个表配置缺少 tableName,跳过`);
...@@ -39,10 +39,13 @@ export async function initMysqlModel() { ...@@ -39,10 +39,13 @@ export async function initMysqlModel() {
console.log(`🔄 正在初始化表: ${tableName}`); console.log(`🔄 正在初始化表: ${tableName}`);
let schemaConf = { let schemaConf: any = {
freezeTableName: true, freezeTableName: true,
timestamps: false timestamps: false
}; };
if (indexes && Array.isArray(indexes) && indexes.length) {
schemaConf.indexes = indexes;
}
try { try {
let model = mysqlDB.define(tableName, schema, schemaConf); let model = mysqlDB.define(tableName, schema, schemaConf);
......
...@@ -6,6 +6,7 @@ import asyncHandler from "express-async-handler"; ...@@ -6,6 +6,7 @@ import asyncHandler from "express-async-handler";
import * as reportBiz from "../biz/report"; import * as reportBiz from "../biz/report";
import * as regionBiz from "../biz/region"; import * as regionBiz from "../biz/region";
import * as scheduleBiz from "../biz/schedule"; import * as scheduleBiz from "../biz/schedule";
import * as deviceBiz from "../biz/device";
import { reloadScheduleTasks } from "../biz/scheduleExecutor"; import { reloadScheduleTasks } from "../biz/scheduleExecutor";
import { eccReqParamater } from "../util/verificationParam"; import { eccReqParamater } from "../util/verificationParam";
...@@ -29,6 +30,18 @@ export function setRouter(httpServer) { ...@@ -29,6 +30,18 @@ export function setRouter(httpServer) {
httpServer.post("/api/qdm/admin/schedule/edit", asyncHandler(editSchedule)); httpServer.post("/api/qdm/admin/schedule/edit", asyncHandler(editSchedule));
/** 日程管理删除 */ /** 日程管理删除 */
httpServer.post("/api/qdm/admin/schedule/delete", asyncHandler(deleteSchedule)); httpServer.post("/api/qdm/admin/schedule/delete", asyncHandler(deleteSchedule));
/** 设备管理列表 */
httpServer.get("/api/qdm/admin/device/list", asyncHandler(getDeviceList));
/** 设备管理 - 编辑空调控制参数 */
httpServer.post("/api/qdm/admin/device/edit", asyncHandler(editDevice));
/** 设备管理 - 批量编辑设备 */
httpServer.post("/api/qdm/admin/device/edit_batch", asyncHandler(batchEditDevices));
/** 设备管理 - 编辑设备信息 */
httpServer.post("/api/qdm/admin/device/editDevice", asyncHandler(editLocalDevice));
/** 设备管理 - 批量编辑设备信息 */
httpServer.post("/api/qdm/admin/device/editDevice_batch", asyncHandler(batchEditLocalDevices));
} }
/** /**
...@@ -97,12 +110,12 @@ async function getScheduleList(req, res) { ...@@ -97,12 +110,12 @@ async function getScheduleList(req, res) {
async function getScheduleDetail(req, res) { async function getScheduleDetail(req, res) {
const scheduleId = Number(req.query?.scheduleId); const scheduleId = Number(req.query?.scheduleId);
if (!scheduleId) { if (!scheduleId) {
res.fail("缺少参数 scheduleId"); res.error("缺少参数 scheduleId");
return; return;
} }
const data = await scheduleBiz.getScheduleDetail(scheduleId); const data = await scheduleBiz.getScheduleDetail(scheduleId);
if (!data) { if (!data) {
res.fail("日程不存在"); res.error("日程不存在");
return; return;
} }
res.success(data); res.success(data);
...@@ -118,11 +131,11 @@ async function addSchedule(req, res) { ...@@ -118,11 +131,11 @@ async function addSchedule(req, res) {
const NotMustHaveKeys = ["deviceIds", "regionKeys"]; const NotMustHaveKeys = ["deviceIds", "regionKeys"];
let { title, scheduleTime, acControl, beginValidity, endValidity, deviceIds, regionKeys } = eccReqParamater(reqConf, req.body, NotMustHaveKeys); let { title, scheduleTime, acControl, beginValidity, endValidity, deviceIds, regionKeys } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!title || !scheduleTime || !acControl) { if (!title || !scheduleTime || !acControl) {
res.fail("缺少必填参数:title, scheduleTime, acControl"); res.error("缺少必填参数:title, scheduleTime, acControl");
return; return;
} }
if ((!deviceIds && !regionKeys) || (!deviceIds.length && !regionKeys.length)) { if ((!deviceIds && !regionKeys) || (!deviceIds.length && !regionKeys.length)) {
res.fail("至少选择一个区域"); res.error("至少选择一个区域");
return; return;
} }
const data = await scheduleBiz.addSchedule({ const data = await scheduleBiz.addSchedule({
...@@ -149,11 +162,11 @@ async function editSchedule(req, res) { ...@@ -149,11 +162,11 @@ async function editSchedule(req, res) {
const NotMustHaveKeys = ["deviceIds", "regionKeys"]; const NotMustHaveKeys = ["deviceIds", "regionKeys"];
let { scheduleId, title, scheduleTime, acControl, beginValidity, endValidity, deviceIds, regionKeys } = eccReqParamater(reqConf, req.body, NotMustHaveKeys); let { scheduleId, title, scheduleTime, acControl, beginValidity, endValidity, deviceIds, regionKeys } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!scheduleId || !title || !scheduleTime || !acControl) { if (!scheduleId || !title || !scheduleTime || !acControl) {
res.fail("缺少必填参数:title, scheduleTime, acControl"); res.error("缺少必填参数:title, scheduleTime, acControl");
return; return;
} }
if ((!deviceIds && !regionKeys) || (!deviceIds.length && !regionKeys.length)) { if ((!deviceIds && !regionKeys) || (!deviceIds.length && !regionKeys.length)) {
res.fail("至少选择一个区域"); res.error("至少选择一个区域");
return; return;
} }
const data = await scheduleBiz.editSchedule({ const data = await scheduleBiz.editSchedule({
...@@ -179,7 +192,7 @@ async function editSchedule(req, res) { ...@@ -179,7 +192,7 @@ async function editSchedule(req, res) {
async function deleteSchedule(req, res) { async function deleteSchedule(req, res) {
const { scheduleId } = req.body || {}; const { scheduleId } = req.body || {};
if (!scheduleId) { if (!scheduleId) {
res.fail("缺少参数 scheduleId"); res.error("缺少参数 scheduleId");
return; return;
} }
const data = await scheduleBiz.deleteSchedule(Number(scheduleId)); const data = await scheduleBiz.deleteSchedule(Number(scheduleId));
...@@ -187,3 +200,164 @@ async function deleteSchedule(req, res) { ...@@ -187,3 +200,164 @@ async function deleteSchedule(req, res) {
// 删除日程后重载执行器 // 删除日程后重载执行器
reloadScheduleTasks().catch((err) => console.error("[日程重载] 删除后重载失败:", err)); reloadScheduleTasks().catch((err) => console.error("[日程重载] 删除后重载失败:", err));
} }
// ======================= 设备管理 =======================
/**
* GET /api/qdm/admin/device/list
* 设备列表
* query: pageNumber, pageSize, regionKeys, deviceName, power, state, deviceMode
*/
async function getDeviceList(req, res) {
const data = await deviceBiz.getDeviceList({
pageNumber: Number(req.query?.pageNumber) || 1,
pageSize: Number(req.query?.pageSize) || 10,
regionKeys: req.query?.regionKeys || undefined,
deviceName: req.query?.deviceName || undefined,
power: req.query?.power || undefined,
state: req.query?.state || undefined,
deviceMode: req.query?.deviceMode || undefined,
});
res.success(data);
}
/**
* POST /api/qdm/admin/device/edit
* 设备管理 - 编辑空调控制参数
* body: { deviceId, power, setTemp, fanSpeed, deviceMode }
*/
async function editDevice(req, res) {
let reqConf = { deviceId: 'String', power: 'String', setTemp: 'Number', fanSpeed: 'String', deviceMode: 'String' };
const NotMustHaveKeys = ["fanSpeed", "deviceMode"];
let { deviceId, power, setTemp, fanSpeed, deviceMode } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!deviceId || !power) {
res.error("缺少必填参数:deviceId, power");
return;
}
const data = await deviceBiz.editAcDeviceControl({ deviceId, power, setTemp, fanSpeed, deviceMode });
res.success(data);
}
/**
* POST /api/qdm/admin/device/batch_edit
* 设备管理 - 批量编辑设备
* body: { regionKeys, deviceName?, power?, state?, deviceMode?, startDate?, endDate?, edits }
* edits: { power?, setTemp?, linkageStart? }
*/
async function batchEditDevices(req, res) {
let reqConf = {
regionKeys: 'Array',
deviceName: 'String',
power: 'String',
state: 'String',
deviceMode: 'String',
startDate: 'String',
endDate: 'String',
edits: 'Object',
};
const NotMustHaveKeys = ["deviceName", "power", "state", "deviceMode", "startDate", "endDate"];
let { regionKeys, deviceName, power, state, deviceMode, startDate, endDate, edits } =
eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!regionKeys || !regionKeys.length) {
res.error("缺少必填参数:regionKeys");
return;
}
if (!edits || typeof edits !== 'object') {
res.error("缺少必填参数:edits");
return;
}
if (edits.power === undefined && edits.setTemp === undefined && edits.linkageStart === undefined) {
res.error("edits 中至少需要 power / setTemp / linkageStart 其中之一");
return;
}
// 校验 edits.power 值合法性
if (edits.power !== undefined && !['开', '关'].includes(edits.power)) {
res.error("edits.power 只能为 开 或 关");
return;
}
// 校验 edits.setTemp 值合法性
if (edits.setTemp !== undefined && (typeof edits.setTemp !== 'number' || edits.setTemp < 16 || edits.setTemp > 30)) {
res.error("edits.setTemp 必须为 16~30 之间的数值");
return;
}
// 校验 edits.linkageStart 值合法性
if (edits.linkageStart !== undefined && !['on', 'off'].includes(edits.linkageStart)) {
res.error("edits.linkageStart 只能为 on 或 off");
return;
}
const data = await deviceBiz.batchEditDevices({
regionKeys,
deviceName: deviceName || undefined,
power: power || undefined,
state: state || undefined,
deviceMode: deviceMode || undefined,
startDate: startDate || undefined,
endDate: endDate || undefined,
edits,
});
res.success(data);
}
/**
* POST /api/qdm/admin/device/editDevice
* 设备管理 - 编辑单台设备表信息
* body: { deviceId, deviceName?, regionKey?, deviceType?, controlParams?, deviceState? }
*/
async function editLocalDevice(req, res) {
let reqConf = {
deviceId: 'String',
deviceName: 'String',
regionKey: 'Number',
deviceType: 'String',
deviceState: 'String',
linkageStart: 'String',
};
const NotMustHaveKeys = ["deviceName", "regionKey", "deviceType", "deviceState", "linkageStart"];
let { deviceId, deviceName, regionKey, deviceType, deviceState, linkageStart } =
eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!deviceId) {
res.error("缺少必填参数:deviceId");
return;
}
const hasEdit = deviceName !== undefined || regionKey !== undefined
|| deviceType !== undefined || deviceState !== undefined || linkageStart !== undefined;
if (!hasEdit) {
res.error("至少需要提供一个编辑字段(deviceName/regionKey/deviceType/controlParams/deviceState)");
return;
}
const data = await deviceBiz.editLocalDevice({ deviceId, deviceName, regionKey, deviceType, deviceState, linkageStart });
res.success(data);
}
/**
* POST /api/qdm/admin/device/editDevice_batch
* 设备管理 - 批量编辑设备表信息
* body: { regionKeys?, deviceName?, edits: { deviceName?, regionKey?, deviceType?, controlParams?, deviceState? } }
*/
async function batchEditLocalDevices(req, res) {
let reqConf = {
regionKeys: 'Array',
deviceName: 'String',
edits: 'Object',
};
const NotMustHaveKeys = ["regionKeys", "deviceName"];
let { regionKeys, deviceName, edits } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!edits || typeof edits !== 'object') {
res.error("缺少必填参数:edits");
return;
}
if ((!regionKeys || regionKeys.length === 0) && !deviceName) {
res.error("至少需要提供 regionKeys 或 deviceName 筛选条件");
return;
}
const validEditKeys = ["deviceName", "regionKey", "deviceType", "controlParams", "deviceState"];
const hasEdit = validEditKeys.some(k => edits[k] !== undefined);
if (!hasEdit) {
res.error("edits 中至少需要一个编辑字段(deviceName/regionKey/deviceType/controlParams/deviceState)");
return;
}
const data = await deviceBiz.batchEditLocalDevices({ regionKeys, deviceName, edits });
res.success(data);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment