管理后台联调和问题修复

parent b29582fb
......@@ -4,11 +4,11 @@
<img>http://127.0.0.1:80</img>
<mysqldb>
<!-- 本地mysql配置 -->
<!-- <mysqlHost>127.0.0.1</mysqlHost>
<mysqlHost>127.0.0.1</mysqlHost>
<mysqlPort>3306</mysqlPort>
<mysqlUser>root</mysqlUser>
<mysqlPwd>123456</mysqlPwd>
<dataBase>dst_technology_platform</dataBase> -->
<dataBase>dst_technology_platform</dataBase>
<!-- 服务器mysql配置 -->
<!-- <mysqlHost>127.0.0.1</mysqlHost>
<mysqlPort>13306</mysqlPort>
......@@ -16,11 +16,11 @@
<mysqlPwd>root</mysqlPwd>
<dataBase>dst_technology_platform</dataBase> -->
<!-- dst服务器mysql配置 49.235.185.26 -->
<mysqlHost>127.0.0.1</mysqlHost>
<!-- <mysqlHost>127.0.0.1</mysqlHost>
<mysqlPort>3306</mysqlPort>
<mysqlUser>deploy</mysqlUser>
<mysqlPwd>Qaz123456!</mysqlPwd>
<dataBase>dst_technology_platform</dataBase>
<dataBase>dst_technology_platform</dataBase> -->
</mysqldb>
<feiyi>
<feiyiAppKey>444940462151958528</feiyiAppKey>
......@@ -33,5 +33,7 @@
<diqinAppKey>178486189110050</diqinAppKey>
<diqinSecretKey>CQuwrqgtaQhnwgcChB9Ejgtt</diqinSecretKey>
<diqinBaseUrl>https://airiccc.com</diqinBaseUrl>
<!-- PID-TVOC 换算系数(ppb → μg/m³),入库时换算,默认 0.0013 -->
<diqinTvocConvertFactor>0.0013</diqinTvocConvertFactor>
</diqin>
</config>
......@@ -15,8 +15,9 @@ import {
getDataSourceDetail,
} from "./diqinClient";
import { mysqlModelMap } from "../model/sqlModelBind";
import { systemConfig } from "../config/serverConfig";
import Sequelize from "sequelize";
import { DEVICE_STATE } from "../config/businessEnum";
import { DEVICE_STATE, WORK_MODE_TEXT_MAP, FAN_SPEED_TEXT_MAP } from "../config/businessEnum";
import moment from "moment";
import XLSX from "xlsx";
import path from "path";
......@@ -96,23 +97,29 @@ async function buildMaxTimeMap(
}
/**
* 从 Excel 文件读取电表设备编号 → 外机地址 的映射表
* 从 Excel 文件读取电表地址 → 空调外机地址(一对多)映射表
* Excel 列:电表主机编码 | 电表地址 | 空调外机地址 | 空调内机所属房间 | 区域名称
* 返回 Map<meterComId, string[]>(一个电表地址可对应多个外机地址)
*/
function loadMeterOuterAddrMap(): Map<string, string> {
const map = new Map<string, string>();
function loadMeterOuterAddrMap(): Map<string, string[]> {
const map = new Map<string, string[]>();
try {
const filePath = path.resolve(__dirname, '../../res/电表对应区域.xlsx');
const workbook = XLSX.readFile(filePath);
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rows: any[] = XLSX.utils.sheet_to_json(sheet);
for (const row of rows) {
const meterCode = row['电表设备编号'];
const meterComId = row['电表地址'];
const outerAddr = row['空调外机地址'];
if (meterCode && outerAddr) {
map.set(String(meterCode), String(outerAddr));
if (meterComId && outerAddr) {
const key = String(meterComId);
const addr = String(outerAddr);
const list = map.get(key) || [];
if (!list.includes(addr)) list.push(addr);
map.set(key, list);
}
}
console.log(`[工具] 电表对应区域.xlsx 读取完成,共 ${map.size} 映射`);
console.log(`[工具] 电表对应区域.xlsx 读取完成,共 ${map.size} 个电表地址映射`);
} catch (err) {
console.error('[工具] 读取电表对应区域.xlsx 失败:', err);
}
......@@ -285,8 +292,8 @@ export async function region() {
// 3. 同步空调内机 → 反写 region.address
const updatedRegions = await syncRegionAddressByAcDevices(regionModel, allRegions);
// 4. 同步电表设备 region_key(复用上一步已更新 address 的内存数据)
await syncMeterDeviceRegionKey(updatedRegions);
// 4. 同步电表设备与区域关联(一对多,写 region_device_rel,复用上一步已更新 address 的内存数据)
await syncMeterRegionRel(updatedRegions);
// 5. 同步飞奕设备 → region_device_rel 关联表
await feiyiRegionDeviceRel();
......@@ -345,7 +352,7 @@ async function syncRegionAddressByAcDevices(regionModel: any, allRegions: any[])
{ region_param: param },
{ where: { id: region.id } },
);
// 同步更新内存中的值,后续 syncMeterDeviceRegionKey 可直接使用
// 同步更新内存中的值,后续 syncMeterRegionRel 可直接使用
region.region_param = param;
updateCount++;
}
......@@ -358,22 +365,25 @@ async function syncRegionAddressByAcDevices(regionModel: any, allRegions: any[])
}
/**
* 通过 Excel 映射 + region.address 同步电表设备的 region_key
* 逻辑:Excel(电表设备编号→外机地址) → region.address → region.id → 更新 device.region_key
* 通过 Excel 映射 + region.address 同步电表设备与区域的关联(一对多)
* 逻辑:Excel(电表地址→外机地址列表) → region.address → region_key 列表
* → 写 region_device_rel(relation_type=device_to_region)
* device.region_key 不写入:单值字段无法表达一对多,保持为空
*/
async function syncMeterDeviceRegionKey(allRegions: any[]) {
console.log('[同步电表region_key] 开始...');
async function syncMeterRegionRel(allRegions: any[]) {
console.log('[同步电表region关联] 开始...');
try {
const deviceModel = mysqlModelMap['device'];
if (!deviceModel) {
console.error('[同步电表region_key] device 表模型未初始化,跳过');
const relModel = mysqlModelMap['region_device_rel'];
if (!deviceModel || !relModel) {
console.error('[同步电表region关联] device 或 region_device_rel 表模型未初始化,跳过');
return;
}
// 1. 读取 Excel 映射:电表设备编号 → 外机地址
// 1. 读取 Excel 映射:电表地址 → 外机地址列表
const meterOuterMap = loadMeterOuterAddrMap();
if (meterOuterMap.size === 0) {
console.warn('[同步电表region_key] Excel 映射为空,跳过');
console.warn('[同步电表region关联] Excel 映射为空,跳过');
return;
}
......@@ -391,41 +401,51 @@ async function syncMeterDeviceRegionKey(allRegions: any[]) {
addrToRegionKey.set(addr, r.region_key);
}
}
console.log(`[同步电表region_key] region_param.address 映射共 ${addrToRegionKey.size} 条`);
console.log(`[同步电表region关联] region_param.address 映射共 ${addrToRegionKey.size} 条`);
// 3. 查询所有电表设备
const meterDevices = await deviceModel.findAll({
where: { device_type: '电表' },
raw: true,
});
console.log(`[同步电表region_key] 共 ${meterDevices.length} 个电表设备`);
console.log(`[同步电表region关联] 共 ${meterDevices.length} 个电表设备`);
// 4. 逐一匹配更新
let updateCount = 0;
// 4. 逐一匹配:先删该设备旧关联,再插入新关联(一个电表 → 多个区域)
let relCount = 0;
let missCount = 0;
for (const device of meterDevices as any[]) {
const outerAddr = meterOuterMap.get(device.device_id);
if (!outerAddr) {
const outerAddrs = meterOuterMap.get(device.device_id);
if (!outerAddrs || outerAddrs.length === 0) {
missCount++;
continue;
}
const regionKey = addrToRegionKey.get(outerAddr);
if (!regionKey) {
// 外机地址 → region_key(去重、过滤无匹配)
const regionKeySet = new Set<string>();
for (const addr of outerAddrs) {
const rk = addrToRegionKey.get(addr);
if (rk) regionKeySet.add(rk);
}
const regionKeys = Array.from(regionKeySet);
if (regionKeys.length === 0) {
missCount++;
continue;
}
// region_key 已正确则跳过
if (device.region_key === regionKey) continue;
await deviceModel.update(
{ region_key: regionKey },
{ where: { id: device.id } },
);
updateCount++;
// 先删旧关联,再插新关联
await relModel.destroy({
where: { device_id: device.device_id, relation_type: 'device_to_region' },
});
const newRels = regionKeys.map(rk => ({
region_key: rk,
device_id: device.device_id,
relation_type: 'device_to_region',
}));
await relModel.bulkCreate(newRels);
relCount += newRels.length;
}
console.log(`[同步电表region_key] 完成,更新 ${updateCount} 条,未匹配 ${missCount}`);
console.log(`[同步电表region关联] 完成,新增关联 ${relCount} 条,未匹配 ${missCount} 台设备`);
} catch (err) {
console.error('[同步电表region_key] 执行异常:', err);
console.error('[同步电表region关联] 执行异常:', err);
}
}
......@@ -435,7 +455,7 @@ async function syncMeterDeviceRegionKey(allRegions: any[]) {
const AC_CONTROL_PARAMS = {
power: ["on", "off"],
mode: ["制冷", "制热", "送风", "除湿"],
fanSpeed: ["自动", "弱", "强", "超强"],
fanSpeed: ["自动", "低风", "中风", "高风"],
setTemp: "16~30",
roomTemp: "-20~50",
maxTemp: "20~40",
......@@ -588,7 +608,7 @@ async function integrateMeterDevices() {
let insertCount = 0;
let skipCount = 0;
for (const item of rows) {
const deviceId = item.gatewayCode; // device_id 用 gatewayCode
const deviceId = item.meterComId; // device_id 用 meterComId(电表地址,一台网关下可有多个电表)
if (!deviceId) {
skipCount++;
continue;
......@@ -604,10 +624,15 @@ async function integrateMeterDevices() {
// 插入新设备
await deviceModel.create({
device_id: deviceId,
region_key: '', // 电表暂无区域关联,后续由 syncMeterDeviceRegionKey 回填
region_key: '', // 电表不写单值 region_key,区域关联(一对多)走 region_device_rel
device_type: '电表',
device_name: `电表-${item.meterComId || item.meterId || ''}`,
device_param: { meterId: item.meterId || '', ...METER_CONTROL_PARAMS },
device_param: {
meterId: item.meterId || '',
meterComId: item.meterComId || '',
gatewayCode: item.gatewayCode || '',
...METER_CONTROL_PARAMS,
},
});
insertCount++;
}
......@@ -617,21 +642,6 @@ async function integrateMeterDevices() {
// ==================== 设备数据集成 ====================
/** 工作模式映射:1=制冷 2=制热 3=送风 4=除湿 */
const WORK_MODE_MAP: Record<number, string> = {
1: '制冷',
2: '制热',
3: '送风',
4: '除湿',
};
/** 风速映射:1=高风 2=中风 4=低风 */
const FAN_SPEED_MAP: Record<number, string> = {
1: '超强',
2: '强',
4: '弱',
};
/**
* 数据集成-空调内机、电表设备数据集成
*/
......@@ -666,10 +676,7 @@ async function integrateAcDeviceData() {
{ occurrenceTime: todayDate },
);
console.log(`[空调设备数据集成] 共获取 ${rows.length} 条开关机记录`);
if (rows.length === 0) {
console.log('[空调设备数据集成] 当天无开关机记录,跳过');
return;
}
// 注意:即使无开关机记录也不早退,交由下方补偿机制拉取实时状态兜底
// 2. 提取所有涉及的 device_id(去重)
const deviceIdSet = new Set<string>();
......@@ -702,8 +709,8 @@ async function integrateAcDeviceData() {
// 构造数据(按设备数据结构.md 空调设备数据格式)
const data = {
power: item.onOff === 1 ? 'on' : 'off',
mode: WORK_MODE_MAP[item.workMode] || '未知',
fanSpeed: FAN_SPEED_MAP[item.fanSpeed] || '自动',
mode: WORK_MODE_TEXT_MAP[item.workMode] || '未知',
fanSpeed: FAN_SPEED_TEXT_MAP[item.fanSpeed] || '自动',
setTemp: item.tempSet || 0,
roomTemp: item.roomTemp || 0,
maxTemp: item.tempSetHi || 28,
......@@ -720,7 +727,10 @@ async function integrateAcDeviceData() {
// TODO: 考虑是否需要在插入前对数据按 device_time 排序,避免数据库存储顺序混乱
// 5. 批量插入
// 5. 补偿:当天无开关机记录的设备,拉取内机列表实时状态兜底写库(30 分钟节流)
await compensateAcRealtimeData(deviceModel, deviceDataModel, deviceIdSet, toInsert);
// 6. 批量插入
if (toInsert.length > 0) {
await deviceDataModel.bulkCreate(toInsert);
}
......@@ -728,9 +738,91 @@ async function integrateAcDeviceData() {
console.log(`[空调设备数据集成] 完成,新增 ${toInsert.length} 条,跳过 ${skipCount} 条`);
}
// 距上次实时补偿的时间(ms),用于节流避免频繁全量拉取
let lastAcRealtimeCompensateAt = 0;
const AC_REALTIME_COMPENSATE_INTERVAL_MS = 30 * 60 * 1000; // 30 分钟
/**
* 空调数据补偿机制:
* 当天无开关机记录(或记录不全)的设备,拉取内机列表实时状态作为快照写入 device_data,
* 避免"不调温不开关机时空调数据不更新"。
* 只补 rows 未覆盖的设备;完全无记录时补所有空调设备。
*/
async function compensateAcRealtimeData(
deviceModel: any,
deviceDataModel: any,
eventDeviceIds: Set<string>,
toInsert: any[],
): Promise<void> {
// 节流:距上次补偿不足 30 分钟则跳过
const now = Date.now();
if (now - lastAcRealtimeCompensateAt < AC_REALTIME_COMPENSATE_INTERVAL_MS) {
return;
}
// 查询所有空调设备
const acDevices = await deviceModel.findAll({
where: { device_type: '空调' },
attributes: ['device_id'],
raw: true,
}) as any[];
const allAcDeviceIds = acDevices.map((d: any) => d.device_id).filter(Boolean);
if (allAcDeviceIds.length === 0) {
return;
}
// 找出当天无开关机记录的设备(记录不全时只补这些)
const missingDeviceIds = allAcDeviceIds.filter((id: string) => !eventDeviceIds.has(id));
if (missingDeviceIds.length === 0) {
// 全部设备今天都有开关机记录,无需补偿(仍更新节流时间,避免每次轮询都查设备表)
lastAcRealtimeCompensateAt = now;
return;
}
console.log(`[空调数据补偿] ${missingDeviceIds.length} 台设备当天无开关机记录,拉取实时状态兜底...`);
// 分页拉取所有内机实时状态,建立 indoorUnitAddressFull → 内机映射
const unitRows = await fetchAllPages((page, limit) => getIndoorUnitList({ page, limit }));
const unitMap = new Map<string, any>();
for (const unit of unitRows) {
if (unit.indoorUnitAddressFull) {
unitMap.set(unit.indoorUnitAddressFull, unit);
}
}
// 只对缺失设备构造实时快照(device_time 用当前轮询时间,source 标记区分)
const compensateTime = new Date();
let compensateCount = 0;
for (const deviceId of missingDeviceIds) {
const unit = unitMap.get(deviceId);
if (!unit) continue;
const data = {
power: unit.onOff === 1 ? 'on' : 'off',
mode: WORK_MODE_TEXT_MAP[unit.workMode] || '未知',
fanSpeed: FAN_SPEED_TEXT_MAP[unit.fanSpeed] || '自动',
setTemp: unit.tempSet || 0,
roomTemp: unit.roomTemp || 0,
maxTemp: unit.tempSetHi || 28,
minTemp: unit.tempSetLo || 16,
autoControl: '生效',
source: 'realtime', // 与开关机记录区分,便于溯源
};
toInsert.push({
device_id: deviceId,
device_data: data,
received_time: compensateTime,
device_time: compensateTime,
});
compensateCount++;
}
lastAcRealtimeCompensateAt = now;
console.log(`[空调数据补偿] 完成,新增 ${compensateCount} 条实时状态(共 ${missingDeviceIds.length} 台缺失设备)`);
}
/**
* 电表设备数据集成(抄表记录)
* 用 gatewayCode 匹配 device 表的 device_id
* 用 meterComId(电表地址)匹配 device 表的 device_id
*/
async function integrateMeterDeviceData() {
console.log('[电表设备数据集成] 开始...');
......@@ -756,8 +848,8 @@ async function integrateMeterDeviceData() {
// 2. 提取所有涉及的 device_id(去重)
const deviceIdSet = new Set<string>();
for (const item of rows) {
if (item.gatewayCode) {
deviceIdSet.add(item.gatewayCode);
if (item.meterComId) {
deviceIdSet.add(item.meterComId);
}
}
const deviceIds = Array.from(deviceIdSet);
......@@ -769,7 +861,7 @@ async function integrateMeterDeviceData() {
const toInsert: any[] = [];
let skipCount = 0;
for (const item of rows) {
const deviceId = item.gatewayCode;
const deviceId = item.meterComId;
if (!deviceId) {
skipCount++;
continue;
......@@ -788,6 +880,7 @@ async function integrateMeterDeviceData() {
power: item.ammeterReadingAfterTransformationRatio || 0,
energy: item.ammeterReadingAfterTransformationRatio || 0,
switch: 'open',
meterComId: item.meterComId || '', // 记录电表地址,便于溯源
};
toInsert.push({
device_id: deviceId,
......@@ -939,7 +1032,7 @@ export async function alertWorkData() {
/**
* 数据集成-飞奕区域设备关联同步
* 从 device 表中读取飞奕设备已绑定的 region_key,同步写入 region_device_rel 表
* 涵盖空调设备和电表设备
* 仅涵盖空调、新风设备;电表走 device_to_region 多对多,由 syncMeterRegionRel 单独维护
* 同步策略:先删后插,删除本批次涉及设备的全部旧关联,再插入新关联
*/
export async function feiyiRegionDeviceRel() {
......@@ -952,10 +1045,10 @@ export async function feiyiRegionDeviceRel() {
return;
}
// 1. 查询所有已绑定 region_key 的飞奕设备(空调 + 电表
// 1. 查询所有已绑定 region_key 的飞奕设备(空调 + 新风
const devices = await deviceModel.findAll({
where: {
device_type: ['空调', '新风', '电表'],
device_type: ['空调', '新风'],
region_key: { [Op.ne]: null },
},
attributes: ['device_id', 'region_key', 'device_type'],
......@@ -1003,11 +1096,18 @@ export async function feiyiRegionDeviceRel() {
* 将迪勤的 [{ indicator, value }] 格式转为 { key: value } 的平面对象
* 特殊映射:light → lightLevel(与系统现有字段名对齐)
*/
function parseDiqinReading(reading: Array<{ indicator: string; value: number }> | undefined): Record<string, number> {
function parseDiqinReading(reading: Array<{ indicator: string; value: number }> | undefined): Record<string, number | boolean> {
if (!reading || !Array.isArray(reading)) return {};
const result: Record<string, number> = {};
const result: Record<string, number | boolean> = {};
for (const item of reading) {
const key = item.indicator === 'light' ? 'lightLevel' : item.indicator;
// PID-TVOC 单位归一化:迪勤 tvoc 原始值单位 ppb,入库前 × 配置系数换算为 μg/m³
// 同时写入 tvocConverted 标记,供历史数据迁移区分新旧数据(迁移只处理无标记的旧数据)
if (key === 'tvoc') {
result['tvoc'] = Math.round(item.value * systemConfig.diqin.tvocConvertFactor * 100) / 100;
result['tvocConverted'] = true;
continue;
}
result[key] = item.value;
}
return result;
......@@ -1582,6 +1682,73 @@ export async function migrateDeviceRegionKey() {
}
}
// ==================== TVOC 单位迁移(tvoc: ppb → μg/m³) ====================
/**
* 数据迁移:将 device_data 中旧的 TVOC 原始值(key=tvoc,单位 ppb)
* 按配置系数换算为 μg/m³,并打上 tvocConverted 标记。
* 幂等设计:仅处理"含 tvoc 且无 tvocConverted 标记"的记录。
* - 历史数据(改动前写入):tvoc 为 ppb、无标记 → 命中迁移
* - 迁移完成 / 新代码写入:tvoc 为 μg/m³ 且带 tvocConverted 标记 → 不命中
* 可安全重复执行。
*/
export async function migrateDiqinTvocUnit() {
console.log('[TVOC单位迁移] 开始...');
try {
const deviceDataModel = mysqlModelMap['device_data'];
if (!deviceDataModel) {
console.error('[TVOC单位迁移] device_data 模型未初始化,跳过');
return;
}
const factor = systemConfig.diqin.tvocConvertFactor;
if (!factor || factor <= 0) {
console.error('[TVOC单位迁移] tvocConvertFactor 未配置或非法,跳过');
return;
}
// 分批处理:每轮取当前仍符合条件的第一批,迁移后记录不再匹配、数据集自然收缩,直到查空。
// 不使用 offset 翻页:避免数据集随迁移收缩时翻页跳位导致漏数据。
const BATCH_SIZE = 500;
let migratedCount = 0;
while (true) {
const batch = await deviceDataModel.findAll({
where: Sequelize.literal(
"JSON_EXTRACT(device_data, '$.tvoc') IS NOT NULL AND JSON_EXTRACT(device_data, '$.tvocConverted') IS NULL"
),
attributes: ['id', 'device_data'],
raw: true,
limit: BATCH_SIZE,
}) as any[];
if (batch.length === 0) break;
for (const row of batch) {
// MySQL JSON 列在 raw 查询下可能返回字符串或对象,兼容处理
const data = typeof row.device_data === 'string'
? JSON.parse(row.device_data)
: { ...(row.device_data || {}) };
if (data.tvoc === undefined || data.tvocConverted) continue;
const tvoc = Math.round(Number(data.tvoc) * factor * 100) / 100;
data.tvoc = tvoc;
data.tvocConverted = true;
await deviceDataModel.update(
{ device_data: data, updated_at: new Date() },
{ where: { id: row.id } },
);
migratedCount++;
}
console.log(`[TVOC单位迁移] 本批 ${batch.length} 条,累计已迁移 ${migratedCount} 条`);
}
console.log(`[TVOC单位迁移] 完成:共迁移 ${migratedCount} 条`);
} catch (err) {
console.error('[TVOC单位迁移] 执行异常:', err);
}
}
// ==================== 故障状态自动更新 ====================
/**
......
import { selectOneDataByParam, selectDataListByParam, selectDataCountByParam, selectDataListToPageByParam } from "../data/findData";
import { TABLENAME } from "../config/dbEnum";
import { BizError } from "../util/bizError";
import { ERRORENUM } from "../config/errorEnum";
import { addData } from "../data/addData";
import { updateManyData } from "../data/updateData";
import { getIndoorUnitList, controlIndoorUnit } from "./feiyiClient";
import { powerMap, workModeMap, fanSpeedMap, WORK_MODE_TEXT_MAP, FAN_SPEED_TEXT_MAP, DEVICE_STATE_TEXT_MAP } from "../config/businessEnum";
import { formatLocalTime } from "../config/eccParam/admin";
/**
* 获取设备列表
* @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],
'告警': [2],
'离线': [1],
};
if (params.state && stateMap[params.state]) {
paramAnys.device_state = { "%in%": stateMap[params.state] };
}
// 4. 判断是否需要飞奕后置过滤
const needFeiyiFilter = params.power || params.deviceMode;
const targetOnOff = params.power ? powerMap[params.power] : undefined;
const targetMode = params.deviceMode ? workModeMap[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 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.区域表,
{ region_key: { "%in%": regionIdSet } },
["region_key", "region_name"]
);
(regionRes.data as any[]).forEach((r: any) => regionMap.set(String(r.region_key), 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.region_name || '' : '',
power: feiyiRow.onOff === 1 ? '开' : '关',
setTemp: feiyiRow.tempSet ?? '',
fanSpeed: FAN_SPEED_TEXT_MAP[feiyiRow.fanSpeed] || String(feiyiRow.fanSpeed || ''),
deviceMode: WORK_MODE_TEXT_MAP[feiyiRow.workMode] || String(feiyiRow.workMode || ''),
state: DEVICE_STATE_TEXT_MAP[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.区域表,
{ region_key: { "%in%": regionIdSet } },
["region_key", "region_name"]
);
(regionRes.data as any[]).forEach((r: any) => regionMap.set(String(r.region_key), 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 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.region_name || '' : '',
power: row.onOff === 1 ? '开' : '关',
setTemp: row.tempSet ?? '',
fanSpeed: FAN_SPEED_TEXT_MAP[row.fanSpeed] || String(row.fanSpeed || ''),
deviceMode: WORK_MODE_TEXT_MAP[row.workMode] || String(row.workMode || ''),
state: DEVICE_STATE_TEXT_MAP[device.device_state] ?? String(device.device_state ?? ''),
linkageStart: device.linkage_start ?? 'on',
updateTime,
};
});
return { total, rows };
}
}
/**
* 根据区域获取设备列表
* @param params
*/
export async function getDevicesByRegion(params: { regionKeys: number[] }) {
const { regionKeys } = params;
if (!regionKeys || !Array.isArray(regionKeys) || regionKeys.length === 0) {
throw new BizError(ERRORENUM.参数错误, `请选择区域`);
}
// 1. 查询区域设备关联表,获取设备列表
let regionDeviceRels = await selectDataListByParam(
TABLENAME.区域设备关联表,
{ region_key: { "%in%": regionKeys } },
["id", "device_id", "region_key"]
);
// 2. 查询设备表,获取设备名称
let deviceIds = [...new Set((regionDeviceRels.data || []).map((rel: any) => rel.device_id).filter(Boolean))];
let devices = await selectDataListByParam(
TABLENAME.设备表,
{ device_id: { "%in%": deviceIds } },
["device_id", "device_name", "device_type"]
);
let deviceMap = new Map<string, any>();
(devices.data || []).forEach((d: any) => {
deviceMap.set(d.device_id, d);
});
// 3. 查询区域表,获取区域名称
let regionMap = new Map<string, any>();
let regionRes = await selectDataListByParam(
TABLENAME.区域表,
{ region_key: { "%in%": regionKeys } },
["region_key", "region_name"]
);
(regionRes.data || []).forEach((r: any) => {
regionMap.set(r.region_key, r);
});
// 4. 组装返回数据
let rows = (regionDeviceRels.data || []).map((rel: any) => {
const device = deviceMap.get(rel.device_id);
const region = regionMap.get(rel.region_key);
return {
regionKey: rel.region_key,
deviceId: rel.device_id,
deviceName: device ? device.device_name || '' : '',
deviceType: device ? device.device_type || '' : '',
regionName: region ? region.region_name || '' : '',
};
});
return { total: rows.length, list: 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": powerMap[power] ?? 1,
"openApiAction": "control",
};
if (setTemp != null) acParams.tempSet = setTemp;
if (fanSpeed) acParams.fanSpeed = fanSpeedMap[fanSpeed];
if (deviceMode) acParams.workMode = workModeMap[deviceMode];
// 3. 调用飞奕控制接口
const result = await controlIndoorUnit(acParams);
// 4. 记录操作日志
const now = new Date();
const logEntry = {
device_id: deviceId,
device_type: '空调',
title: '设备管理编辑',
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 ? 'success' : 'fail',
operation_time: formatLocalTime(now),
operator: null,
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],
'告警': [2],
'离线': [1],
};
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 needFeiyiFilter = !!(power || deviceMode);
const targetOnOff = power ? powerMap[power] : undefined;
const targetMode = deviceMode ? workModeMap[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: '空调',
title: '批量编辑',
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 ? 'success' : 'fail',
operation_time: formatLocalTime(now),
operator: null,
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.device_param = 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
......@@ -219,6 +219,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
......
import { selectDataListToPageByParam, selectDataCountByParam, selectDataListByParam } from "../data/findData";
import { TABLENAME } from "../config/dbEnum";
/**
* 获取设备日志列表(分页)
* 列表展示:标题、区域、设备、操作内容、操作人、操作时间
* 通过关联查询补全设备名称和区域名称
*/
export async function getDeviceLogsList(params: {
pageNumber?: number;
pageSize?: number;
}) {
const pageNumber = params.pageNumber || 1;
const pageSize = params.pageSize || 10;
// 1. 查询总数
const countRes = await selectDataCountByParam(TABLENAME.设备日志表, {});
const total = typeof countRes.data === 'number' ? countRes.data : 0;
if (total === 0) {
return { total: 0, rows: [] };
}
// 2. 分页查询日志(按操作时间倒序)
const logRes = await selectDataListToPageByParam(
TABLENAME.设备日志表,
{ '%orderDesc%': 'operation_time' },
['id', 'device_id', 'title', 'operation_content', 'operator', 'operation_time'],
pageNumber,
pageSize
);
const logs = (logRes.data || []) as any[];
if (logs.length === 0) {
return { total, rows: [] };
}
// 3. 收集所有涉及到的 device_id,批量查设备信息
const deviceIdSet = [...new Set(logs.map((l: any) => l.device_id).filter(Boolean))];
let deviceMap = new Map<string, any>();
let regionMap = new Map<string, any>();
if (deviceIdSet.length > 0) {
const deviceRes = await selectDataListByParam(
TABLENAME.设备表,
{ device_id: { '%in%': deviceIdSet } },
['device_id', 'device_name', 'region_key']
);
const devices = (deviceRes.data || []) as any[];
// 构建设备映射
devices.forEach((d: any) => {
deviceMap.set(d.device_id, d);
});
// 收集区域 key,批量查区域
const regionKeySet = [...new Set(devices.map((d: any) => d.region_key).filter(Boolean))];
if (regionKeySet.length > 0) {
const regionRes = await selectDataListByParam(
TABLENAME.区域表,
{ region_key: { '%in%': regionKeySet } },
['region_key', 'region_name']
);
(regionRes.data || []).forEach((r: any) => {
regionMap.set(r.region_key, r);
});
}
}
// 4. 组装返回数据
const rows = logs.map((log: any) => {
const device = deviceMap.get(log.device_id);
const deviceName = device ? device.device_name : log.device_id;
const regionKey = device ? device.region_key : null;
const regionInfo = regionKey ? regionMap.get(regionKey) : null;
const regionName = regionInfo ? regionInfo.region_name : '';
// operation_content JSON 转字符串
let contentStr = '';
if (log.operation_content) {
const parsed = typeof log.operation_content === 'string'
? JSON.parse(log.operation_content)
: log.operation_content;
// 提取关键信息拼接,排除 option/device_id/success/message 等元信息
const keyLabels: Record<string, string> = {
power: '开关',
setTemp: '设定温度',
fanSpeed: '风速',
deviceMode: '模式',
linkageStart: '联动',
};
const parts: string[] = [];
for (const [k, label] of Object.entries(keyLabels)) {
if (parsed[k] !== undefined) {
parts.push(`${label}:${parsed[k]}`);
}
}
if (parts.length > 0) {
contentStr = parts.join(',');
} else {
contentStr = JSON.stringify(parsed);
}
}
return {
id: log.id,
title: log.title || '',
region: regionName,
device: deviceName,
operationContent: contentStr,
operator: log.operator || '',
operationTime: log.operation_time,
};
});
return { total, list: rows };
}
......@@ -8,7 +8,10 @@ import { post } from '../util/request';
import { analysisXml } from '../util/myXML';
import { planaryArrayBecomeOfBlockData } from '../util/analysisExcel';
import { Op, Sequelize } from 'sequelize';
import { selectDataListByParam } from '../data/findData';
import { selectDataListByParam, selectOneDataByParam } from '../data/findData';
import { addData } from '../data/addData';
import { updateManyData } from '../data/updateData';
import { delData } from '../data/delData';
import { mysqlModelMap } from '../model/sqlModelBind';
import { TABLENAME } from '../config/dbEnum';
import { formatTime } from '../util/dateUtils';
......@@ -16,10 +19,10 @@ import { formatTime } from '../util/dateUtils';
const xlsx = require('node-xlsx');
/**
* 区域位置
* 区域坐标位置
* @param gatewayPage 网关页面 1:运行分析 2:智能监控
*
* @returns 区域位置数据
* @returns 区域坐标位置数据
*/
export async function getRegionLocation(gatewayPage: number, hour: number) {
// const result = [{"regionKey":"32","regionName":"三十二位","xz":1587,"yz":487,"qualityIndex":"优","gatewayPage":gatewayPage}];
......@@ -29,6 +32,18 @@ export async function getRegionLocation(gatewayPage: number, hour: number) {
}
/**
* 区域下拉选择
*
* @returns 区域下拉选择
*/
export async function getRegionList() {
// const result = [{"regionKey":"32","regionName":"三十二位"},{"regionKey":"33","regionName":"三十三位"}];
// 读取区域坐标分布.xlsx文件中IEQ列不为空的表格数据
const result = await readSheetByIeq();
return result;
}
/**
* 读取区域坐标
*/
export async function readRegionAxis(gatewayPage: number, hour: number) {
......@@ -211,16 +226,6 @@ async function buildDeviceMapDynamic(
});
}
// 时间窗口内无数据的设备:推 0 值(无数据设为零)
for (const device of devices) {
if (!deviceDataMap.has(device.device_id)) {
const rn = regionKeyToName[device.region_key];
if (rn && regionEnvMap[rn]) {
regionEnvMap[rn].co2List.push(0);
regionEnvMap[rn].pm25List.push(0);
}
}
}
}
// 5. 组装 deviceMap(按 regionName 分组,格式对齐 mock 数据)
......@@ -293,6 +298,8 @@ async function buildDeviceMapDynamic(
return { deviceMap, qualityMap };
}
// ================================== 读取区域坐标分布.xlsx ============================================
async function readSheet(sheet: any[][], gatewayPage: number, hour: number) {
// 取出所有区域名称,查询区域表获取区域数据
let regionList = await selectDataListByParam(TABLENAME.区域表, {});
......@@ -372,10 +379,237 @@ async function readSheet(sheet: any[][], gatewayPage: number, hour: number) {
regionName: String(row[index] ?? ""),
xz: row[xindex],
yz: row[yindex],
qualityIndex: qualityMap[row[0]] ?? '',
qualityIndex: qualityMap[row[0]] ?? '--',
gatewayPage: gatewayPage,
devices: deviceMap[row[0]]
}))?? [];
}
return result;
}
async function readSheetByIeq() {
// 读取文件
const filePath = path.join(__dirname.substring(0, __dirname.indexOf("out")), "res", "区域坐标分布.xlsx");
const workSheets = xlsx.parse(filePath);
const sheetInfo = workSheets[0].data;
let sheetData = await planaryArrayBecomeOfBlockData(sheetInfo);
// 取出所有区域名称,查询区域表获取区域数据
let regionList = await selectDataListByParam(TABLENAME.区域表, {});
let regionMap:any = {};
regionList.data.map((bean)=>{
regionMap[bean.region_name] = bean.region_key
});
// 封装返回数据
let result = [];
if (sheetData && sheetData[0].blockData) {
let block = sheetData[0];
const headerRow = block.blockData[0];
const index = headerRow.indexOf("区域名称");
const ieq = headerRow.indexOf("IEQ");
result = block.blockData.slice(1).filter((row: any) => row[ieq] != null)
.map((row: any) => ({
regionKey: regionMap[row[0]],
regionName: String(row[index] ?? ""),
}))?? [];
}
return result;
}
// ================================== 管理后台区域 ============================================
/**
* 获取区域设备树
* 按 parent_key 构建层级树,叶子节点挂载设备
* @param params.regionName 区域名称模糊搜索
* @param params.regionType 区域类型精准搜索
* @returns 区域树 [{ id, key, value, regionType, sub: [...], devices: [...] }]
*/
export async function getRegionDevices(params?: {
regionName?: string;
regionType?: string;
}) {
// 1. 查询所有区域,按 sort_order 排序
let where: any = { "%orderAsc%": "sort_order" };
// region_name 模糊搜索
if (params?.regionName) {
where.region_name = { "%like%": params.regionName };
}
// region_type 精准搜索
if (params?.regionType) {
where.region_type = params.regionType;
}
let regionList = await selectDataListByParam(
TABLENAME.区域表,
where,
["id", "region_key", "region_name", "region_type", "parent_id", "parent_key", "sort_order", "created_at"],
);
const regions: any[] = regionList.data || [];
if (regions.length === 0) return [];
// 5. 递归构建子树
const buildTreeNode = (region: any): any => {
const children = regions.filter((r: any) => r.parent_key === region.region_key);
const sub = children.length > 0
? children.map((c: any) => buildTreeNode(c))
: [];
const node: any = {
id: region.id,
key: region.region_key,
value: region.region_name,
regionType: region.region_type || null,
sub,
createTime: region.created_at,
};
return node;
};
// 6. 找出根节点(parent_key 为 null),构建树
const roots = regions.filter((r: any) => r.parent_key === null);
const result = roots.map((r: any) => buildTreeNode(r)).filter((node: any) => node.sub && node.sub.length > 0); // 只保留有子节点的根节点
return result;
}
/**
* 新增区域
* @param params.regionKey 区域编码(必填,唯一)
* @param params.regionName 区域名称(必填)
* @param params.regionType 区域类型(可选)
* @param params.parentKey 父级区域key(可选)
* @param params.parentId 父级区域ID(可选)
* @param params.sortOrder 排序号(可选)
* @param params.maxCapacity 最大容量(可选)
*/
export async function addRegion(params: {
regionKey: string;
regionName: string;
regionType?: string;
parentKey?: string;
parentId?: number;
sortOrder?: number;
maxCapacity?: number;
}) {
const { regionKey, regionName, regionType, parentKey, parentId, sortOrder, maxCapacity } = params;
// 校验 region_key 唯一性
const exist = await selectOneDataByParam(TABLENAME.区域表, { region_key: regionKey });
if (exist.data && Object.keys(exist.data).length > 0) {
throw new Error(`区域编码 ${regionKey} 已存在`);
}
const now = new Date();
await addData(TABLENAME.区域表, {
region_key: regionKey,
region_name: regionName,
region_type: regionType || null,
parent_key: parentKey || null,
parent_id: parentId || null,
sort_order: sortOrder ?? 0,
max_capacity: maxCapacity ?? 35,
created_at: now,
updated_at: now,
});
return { success: true };
}
/**
* 编辑区域
* @param params.regionKey 区域编码(必填,用于定位)
* @param params.regionName 区域名称(可选)
* @param params.regionType 区域类型(可选)
* @param params.parentKey 父级区域key(可选)
* @param params.parentId 父级区域ID(可选)
* @param params.sortOrder 排序号(可选)
* @param params.maxCapacity 最大容量(可选)
*/
export async function editRegion(params: {
regionKey: string;
regionName?: string;
regionType?: string;
parentKey?: string;
parentId?: number;
sortOrder?: number;
maxCapacity?: number;
}) {
const { regionKey, regionName, regionType, parentKey, parentId, sortOrder, maxCapacity } = params;
// 校验区域是否存在
const exist = await selectOneDataByParam(TABLENAME.区域表, { region_key: regionKey });
if (!exist.data || Object.keys(exist.data).length === 0) {
throw new Error(`区域编码 ${regionKey} 不存在`);
}
// 只更新传入的字段
const updates: any = { updated_at: new Date() };
if (regionName !== undefined) updates.region_name = regionName;
if (regionType !== undefined) updates.region_type = regionType;
if (parentKey !== undefined) updates.parent_key = parentKey;
if (parentId !== undefined) updates.parent_id = parentId;
if (sortOrder !== undefined) updates.sort_order = sortOrder;
if (maxCapacity !== undefined) updates.max_capacity = maxCapacity;
await updateManyData(TABLENAME.区域表, { region_key: regionKey }, updates);
return { success: true };
}
/**
* 删除区域
* @param regionKey 区域编码(必填)
*/
export async function deleteRegion(regionKey: string) {
// 校验区域是否存在
const exist = await selectOneDataByParam(TABLENAME.区域表, { region_key: regionKey });
if (!exist.data || Object.keys(exist.data).length === 0) {
throw new Error(`区域编码 ${regionKey} 不存在`);
}
// 检查是否存在子区域
const children = await selectDataListByParam(TABLENAME.区域表, { parent_key: regionKey });
if (children.data && children.data.length > 0) {
throw new Error(`区域 ${regionKey} 下存在 ${children.data.length} 个子区域,请先删除子区域`);
}
await delData(TABLENAME.区域表, { region_key: regionKey });
return { success: true };
}
/**
* 获取区域详情
* @param regionKey 区域编码(必填)
* @returns 父级区域、区域标识、区域名称、区域类型、排序号、最大容量
*/
export async function getRegionDetail(regionKey: string) {
const result = await selectOneDataByParam(TABLENAME.区域表, { region_key: regionKey });
if (!result.data || Object.keys(result.data).length === 0) {
throw new Error(`区域 ${regionKey} 不存在`);
}
const region = result.data as any;
let parentName: string | null = null;
// 如果有父级区域 key,查询父级区域名称
if (region.parent_key) {
const parentResult = await selectOneDataByParam(TABLENAME.区域表, { region_key: region.parent_key });
if (parentResult.data && Object.keys(parentResult.data).length > 0) {
parentName = (parentResult.data as any).region_name || null;
}
}
return {
parent_key: region.parent_key || null,
parent_name: parentName,
region_key: region.region_key,
region_name: region.region_name,
region_type: region.region_type || null,
sort_order: region.sort_order ?? 0,
max_capacity: region.max_capacity ?? 35,
};
}
\ No newline at end of file
......@@ -5,9 +5,9 @@
import { BizError } from '../util/bizError';
import { ERRORENUM } from '../config/errorEnum';
import { TABLENAME } from '../config/dbEnum';
import { DEVICE_STATE } from '../config/businessEnum';
import { DEVICE_STATE, powerMap, workModeMap, fanSpeedMap, lockMap, acPopFanSpeedMap } from '../config/businessEnum';
import { selectDataListByParam, selectOneDataByParam, selectDataCountByParam } from '../data/findData';
import { controlIndoorUnit } from './feiyiClient';
import { controlIndoorUnit, getIndoorUnitStatus } from './feiyiClient';
import {
formatTime,
getTodayStart,
......@@ -25,27 +25,7 @@ import {
formatDayKey,
formatMonthKey,
} from '../util/dateUtils';
/**
* 空调中文到Key的映射
* 1 制冷 2 制热 3 送风 4 除湿
* 0 关-off 1 开-on
* 1 高风-超强 2 中风-强 4 低风-弱
* 0 不锁定-生效 1 锁定-解除
*/
const acDeviceKeyMap: { [key: string]: number } = {
"制冷": 1,
"制热": 2,
"送风": 3,
"除湿": 4,
"on": 1,
"off": 0,
"超强": 1,
"强": 2,
"弱": 4,
"解除": 1,
"生效": 0,
};
import { get } from 'http';
// ==================== 质量判定函数(从 device.ts 移植,适配新系统) ====================
......@@ -71,7 +51,7 @@ const QUALITY_THRESHOLDS: { [key: string]: { you: [number, number]; liang: [numb
pm10: { you: [0, 50], liang: [50, 150], zhong: [150, 250] },
pm2_5: { you: [0, 35], liang: [35, 75], zhong: [75, 150] },
hcho: { you: [0, 0.03], liang: [0.03, 0.08], zhong: [0.08, 0.10] },
tvoc: { you: [0, 0.3], liang: [0.3, 0.6], zhong: [0.6, 1.0] },
tvoc: { you: [0, 300], liang: [300, 600], zhong: [600, 1000] },
o3: { you: [0, 0.05], liang: [0.05, 0.1], zhong: [0.1, 0.16] },
light_level: { you: [300, 500], liang: [100, 300], zhong: [50, 100] },
};
......@@ -214,7 +194,8 @@ async function getHourlyEnvData(startTime: string, endTime: string): Promise<Map
records.forEach((r: any) => {
const t = new Date(r.device_time);
const hourKey = `${String(t.getHours()).padStart(2, '0')}:00`;
// key 格式 "YYYY-MM-DD HH:00":dateKey 用于按天聚合,timeKey 用于小时趋势
const hourKey = `${t.getFullYear()}-${String(t.getMonth() + 1).padStart(2, '0')}-${String(t.getDate()).padStart(2, '0')} ${String(t.getHours()).padStart(2, '0')}:00`;
const parsed = parseDeviceData(r.device_data);
if (!hourlyMap.has(hourKey)) {
......@@ -259,37 +240,60 @@ async function getRegionName(regionKey: string): Promise<string> {
return regionInfo ? regionInfo.region_name : '';
}
/** 根据 regionKey 查询该区域下的设备列表 */
/** 根据 regionKey 查询该区域下的设备列表(兼容 device.region_key 与 region_device_rel 关联表) */
async function getDevicesByRegion(regionKey: string, columns: string[]): Promise<any[]> {
const deviceResult = await selectDataListByParam(
TABLENAME.设备表,
{ region_key: regionKey },
columns
);
return deviceResult.data || [];
let devices: any[] = deviceResult.data || [];
// 补充 region_device_rel 关联表中的设备(电表等多对多设备 region_key 为空,仅存在于关联表)
const relResult = await selectDataListByParam(
TABLENAME.区域设备关联表,
{ region_key: regionKey },
['device_id']
);
const relDeviceIds = (relResult.data || []).map((r: any) => r.device_id).filter(Boolean);
const existingIds = new Set(devices.map((d: any) => d.device_id));
const extraIds = relDeviceIds.filter((id: string) => !existingIds.has(id));
if (extraIds.length > 0) {
const extraResult = await selectDataListByParam(
TABLENAME.设备表,
{ device_id: { '%in%': extraIds } },
columns
);
devices = devices.concat(extraResult.data || []);
}
return devices;
}
/** 查询指定设备列表在当天的最新一条 device_data(去重取第一条),返回 Map<device_id, parsed_data> */
/** 查询指定设备列表在当天的最新一条 device_data,返回 Map<device_id, parsed_data> */
async function getLatestDeviceDataMap(deviceIds: string[], startTime: string): Promise<Map<string, any>> {
if (deviceIds.length === 0) return new Map();
// 逐台设备单独查询最新一条(limit=1 + device_time 降序),保证每台设备都取到各自最新的数据。
// 不能合并成 IN + limit=N:多台设备数据量不均时,最新 N 条可能被数据密集的设备占满,其他设备取不到。
const latestMap = new Map<string, any>();
await Promise.all(
deviceIds.map(async (deviceId) => {
const dataResult = await selectDataListByParam(
TABLENAME.设备数据表,
{
device_id: { '%in%': deviceIds },
device_id: deviceId,
device_time: { '%gte%': startTime },
'%orderDesc%': 'device_time',
'%limit%': deviceIds.length,
'%limit%': 1,
},
['device_id', 'device_data']
);
const latestMap = new Map<string, any>();
(dataResult.data || []).forEach((r: any) => {
if (!latestMap.has(r.device_id)) {
latestMap.set(r.device_id, parseDeviceData(r.device_data));
const row = (dataResult.data || [])[0];
if (row) {
latestMap.set(row.device_id, parseDeviceData(row.device_data));
}
});
})
);
return latestMap;
}
......@@ -538,19 +542,19 @@ export async function getRunAnalysis() {
const regionNameMap = new Map<string, string>();
allRegions.forEach(r => regionNameMap.set(r.region_key, r.region_name || ''));
// 查询所有设备(含 region_key),按区域统计数量
// 查询所有设备(含 region_key、device_type),按区域统计数量
const allDevicesResult = await selectDataListByParam(
TABLENAME.设备表,
{},
['device_id', 'region_key', 'device_state']
['device_id', 'region_key', 'device_state', 'device_type']
);
const allDevices = allDevicesResult.data || [];
// 按区域统计设备数量
// 按区域统计设备数量(电表除外,电表区域归属走关联表,下方单独补充)
const regionDeviceCountMap = new Map<string, number>();
allDevices.forEach((d: any) => {
const rk = d.region_key || '';
if (rk) {
if (rk && d.device_type !== '电表') {
regionDeviceCountMap.set(rk, (regionDeviceCountMap.get(rk) || 0) + 1);
}
});
......@@ -559,7 +563,7 @@ export async function getRunAnalysis() {
const allRegionKeys = new Set<string>();
regionDeviceCountMap.forEach((_, rk) => allRegionKeys.add(rk));
// 查询所有电表设备(含 region_key)
// 查询所有电表设备(含 region_key,兼容存量旧电表
const meterDevicesResult = await selectDataListByParam(
TABLENAME.设备表,
{ device_type: '电表' },
......@@ -568,10 +572,33 @@ export async function getRunAnalysis() {
const meterDevices = meterDevicesResult.data || [];
const meterDeviceIds = meterDevices.map((d: any) => d.device_id);
// 构建电表→区域映射
const meterRegionMap = new Map<string, string>();
// 构建电表→区域映射(一对多):优先读 region_device_rel 关联表,存量旧电表 fallback 到 device.region_key
const meterRegionMap = new Map<string, Set<string>>();
meterDevices.forEach((d: any) => meterRegionMap.set(d.device_id, new Set<string>()));
if (meterDeviceIds.length > 0) {
const relResult = await selectDataListByParam(
TABLENAME.区域设备关联表,
{ device_id: { '%in%': meterDeviceIds } },
['device_id', 'region_key']
);
(relResult.data || []).forEach((rel: any) => {
const rk = rel.region_key;
if (rk && meterRegionMap.has(rel.device_id)) {
meterRegionMap.get(rel.device_id)!.add(rk);
}
});
}
meterDevices.forEach((d: any) => {
meterRegionMap.set(d.device_id, d.region_key || '');
const rk = d.region_key || '';
if (rk) meterRegionMap.get(d.device_id)!.add(rk);
});
// 电表关联区域计入区域列表与设备数量(同一电表关联多个区域时各计一次)
meterRegionMap.forEach((rkSet) => {
rkSet.forEach(rk => {
allRegionKeys.add(rk);
regionDeviceCountMap.set(rk, (regionDeviceCountMap.get(rk) || 0) + 1);
});
});
// 耗电量查询终点:当前时刻(起点复用上方已定义的 last24hStart/last7dStart/last30dStart/lastYearStart)
......@@ -594,9 +621,12 @@ export async function getRunAnalysis() {
const parsed = parseDeviceData(r.device_data);
const val = parsed[ELECTRICITY_KEY];
if (typeof val === 'number') {
const rk = meterRegionMap.get(r.device_id) || '';
if (rk) {
const rkSet = meterRegionMap.get(r.device_id);
if (rkSet) {
// 一对多:电表关联的每个区域都累计该电表的耗电量
rkSet.forEach(rk => {
map.set(rk, (map.get(rk) || 0) + val);
});
}
}
});
......@@ -827,7 +857,7 @@ export async function getRunAnalysis() {
const co2 = indicatorMap.get('co2') || 0;
const q = Math.max(0, Math.min(100, Math.round((1 - co2 / 2000) * 100)));
iaqQualityTrend.push({
key: formatHourKey(new Date(`${todayStart.split(' ')[0]} ${hourKey}`)),
key: formatHourKey(new Date(hourKey)),
value: q,
});
});
......@@ -902,14 +932,16 @@ export async function getRunAnalysis() {
const currentPressure = getAvg('pressure');
const currentTvoc = getAvg('tvoc');
// 构建各指标趋势(按小时,time 格式 "0:00")
// 构建各指标趋势(按小时,time 格式 "0:00",无数据的小时不填充0
const buildTrend = (indicatorKey: string): { time: string; value: string }[] => {
const trend: { time: string; value: string }[] = [];
const hours = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0') + ':00');
hours.forEach(h => {
const hourData = hourlyEnvData.get(h);
const val = hourData ? (hourData.get(indicatorKey) || 0) : 0;
const hourData = hourlyEnvData.get(`${todayStart.split(' ')[0]} ${h}`);
const val = hourData?.get(indicatorKey);
if (val !== undefined) {
trend.push({ time: h, value: String(val) });
}
});
return trend;
};
......@@ -1254,11 +1286,45 @@ export async function getAnalysisPopup(regionKey: string) {
}
}
// 环境监测(该区域电表24小时趋势)
let hjjc:any = {
wdhjjc: [{ key: "2026-08-10 08:00:00", value: 10 }],
sdhjjc: [{ key: "2026-08-10 08:00:00", value: 20 }],
pm25hjjc: [{ key: "2026-08-10 08:00:00", value: 30 }],
co2hjjc: [{ key: "2026-08-10 08:00:00", value: 40 }],
pm10hjjc: [{ key: "2026-08-10 08:00:00", value: 50 }],
tvochjjc: [{ key: "2026-08-10 08:00:00", value: 60 }],
};
return {
"regionName": regionName,
"sbztjc": sbztjc,
"sbztjcqs": sbztjcqs,
"nhjc": nhjc
"nhjc": nhjc,
"hjjc": hjjc
};
}
/**
* 运行分析弹窗 - 区域对比趋势图
* @param currRegionKey 当前区域
* @param selectedRegionKey 已选择区域
*/
export async function getAnalysisPopupSubTrend(currRegionKey: string, selectedRegionKey: string) {
// 能耗监测(该区域电表24小时趋势)
let nhjcdb: { key: string; dqqy: string, dbqy: string }[] = [];
// 环境监测(该区域电表24小时趋势)
let hjjcdb: any = {
wdhjjc: [{ key: "2028-08-10 08:00:00", dqqy: 10, dbqy: 15 }],
sdhjjc: [{ key: "2028-08-10 08:00:00", dqqy: 20, dbqy: 25 }],
pm25hjjc: [{ key: "2028-08-10 08:00:00", dqqy: 30, dbqy: 35 }],
co2hjjc: [{ key: "2028-08-10 08:00:00", dqqy: 40, dbqy: 45 }],
pm10hjjc: [{ key: "2028-08-10 08:00:00", dqqy: 50, dbqy: 55 }],
tvochjjc: [{ key: "2028-08-10 08:00:00", dqqy: 60, dbqy: 65 }],
};
return {
"nhjc": nhjcdb,
"hjjc": hjjcdb
};
}
......@@ -1444,12 +1510,28 @@ export async function getRunEnvironmental() {
for (const cfg of indicatorConfigs) {
const todayStats = calcAvgFromMap(todayLatestMap, cfg.key);
const yesterdayStats = calcAvgFromMap(yesterdayLatestMap, cfg.key);
const hasData = todayStats.count > 0;
// 无数据:全部用占位符
if (!hasData) {
const emptyItem: any = {
"current": '--',
"environmental": '--',
"zrtb": '--',
"max": '--',
};
if (['temperature', 'humidity', 'co2', 'lightLevel', 'tvoc'].includes(cfg.key)) {
emptyItem["min"] = '--';
}
hjsjzl[cfg.key] = emptyItem;
continue;
}
const current = todayStats.count > 0 ? Math.round((todayStats.sum / todayStats.count) * 100) / 100 : 0;
const current = Math.round((todayStats.sum / todayStats.count) * 100) / 100;
const yesterdayAvg = yesterdayStats.count > 0 ? yesterdayStats.sum / yesterdayStats.count : 0;
const zrtb = yesterdayAvg > 0
? (current > yesterdayAvg ? '+' : '') + calcCompareRate(current, yesterdayAvg) + '%'
: '0%';
: '--';
const quality = getQualityLevel(cfg.mapKey, current) || 'N/A';
const item: any = {
......@@ -1470,13 +1552,13 @@ export async function getRunEnvironmental() {
const todayHourly = await getHourlyEnvData(todayStart, todayEnd);
const yesterdayHourly = await getHourlyEnvData(yesterdayStart, yesterdayEnd);
// 本周(近7天每日趋势
const weekStart = getDaysAgoStart(7);
// 本周(过去 7 天每日趋势,含今天,如 13 号 → 从 7 号开始
const weekStart = getDaysAgoStart(6);
const weekHourly = await getHourlyEnvData(weekStart, todayEnd);
// 本月(近30天每日趋势
const monthStart30 = getDaysAgoStart(30);
const monthHourly = await getHourlyEnvData(monthStart30, todayEnd);
// 本月(过去 30 天每日趋势,含今天
const monthStart = getDaysAgoStart(29);
const monthHourly = await getHourlyEnvData(monthStart, todayEnd);
// 按天聚合 hourly 数据
function aggregateByDay(hourlyMap: Map<string, Map<string, number>>): Map<string, Map<string, number>> {
......@@ -1620,7 +1702,7 @@ export async function getRunEnvironmental() {
envKeys.forEach(k => {
const s = rSum.get(k) || 0;
const c = rCount.get(k) || 0;
envData[k] = c > 0 ? Math.round((s / c) * 100) / 100 : 0;
envData[k] = c > 0 ? Math.round((s / c) * 100) / 100 : null;
});
}
......@@ -1628,15 +1710,15 @@ export async function getRunEnvironmental() {
"regionName": region.region_name,
"deviceStatus": deviceStatus,
"updateTime": updateTime,
"wd": envData['temperature'] || 0,
"sd": envData['humidity'] || 0,
"pm25": envData['pm25'] || 0,
"co2": envData['co2'] || 0,
"tvoc": envData['tvoc'] || 0,
"hcho": envData['hcho'] || 0,
"hyl": 0, // 含氧量暂无数据源
"gz": envData['lightLevel'] || 0,
"zy": 0, // 噪音暂无数据源
"wd": typeof envData['temperature'] === 'number' ? envData['temperature'] : '--',
"sd": typeof envData['humidity'] === 'number' ? envData['humidity'] : '--',
"pm25": typeof envData['pm25'] === 'number' ? envData['pm25'] : '--',
"co2": typeof envData['co2'] === 'number' ? envData['co2'] : '--',
"tvoc": typeof envData['tvoc'] === 'number' ? envData['tvoc'] : '--',
"hcho": typeof envData['hcho'] === 'number' ? envData['hcho'] : '--',
"hyl": '--', // 含氧量暂无数据源
"gz": typeof envData['lightLevel'] === 'number' ? envData['lightLevel'] : '--',
"zy": '--', // 噪音暂无数据源
});
}
......@@ -1684,11 +1766,34 @@ export async function getRunMonitoring() {
);
const allDevices = allDevResult.data || [];
// 查询区域设备关联表(兼容电表等 device.region_key 为空、关联走关联表的设备)
const regionRelResult = await selectDataListByParam(
TABLENAME.区域设备关联表,
{},
['device_id', 'region_key']
);
const regionRelDeviceMap = new Map<string, Set<string>>();
(regionRelResult.data || []).forEach((rel: any) => {
const rk = rel.region_key;
if (!rk) return;
if (!regionRelDeviceMap.has(rk)) regionRelDeviceMap.set(rk, new Set());
regionRelDeviceMap.get(rk)!.add(rel.device_id);
});
// 按区域聚合
const znjcqkDataList: { regionName: string; status: number; deviceUseStatus: string; onlineRate: string }[] = [];
allRegions.forEach((region: any) => {
const regionDevices = allDevices.filter((d: any) => d.region_key === region.region_key);
const relDeviceIds = regionRelDeviceMap.get(region.region_key);
if (relDeviceIds) {
// 补充关联表中属于该区域但 device.region_key 为空的设备
allDevices.forEach((d: any) => {
if (relDeviceIds.has(d.device_id) && !regionDevices.some((x: any) => x.device_id === d.device_id)) {
regionDevices.push(d);
}
});
}
const totalCount = regionDevices.length;
if (totalCount === 0) return; // 无设备区域跳过
......@@ -1849,13 +1954,16 @@ export async function getRunMonitoring() {
const todayEnd = getTodayEnd();
const hourlyEnvData = await getHourlyEnvData(todayStart, todayEnd);
// 构建各指标趋势(无数据的小时不填充0)
const buildTrend = (indicatorKey: string): { time: string; value: string }[] => {
const trend: { time: string; value: string }[] = [];
const hours = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0') + ':00');
hours.forEach(h => {
const hourData = hourlyEnvData.get(h);
const val = hourData ? (hourData.get(indicatorKey) || 0) : 0;
const hourData = hourlyEnvData.get(`${todayStart.split(' ')[0]} ${h}`);
const val = hourData?.get(indicatorKey);
if (val !== undefined) {
trend.push({ time: h, value: String(val) });
}
});
return trend;
};
......@@ -2006,7 +2114,7 @@ export async function getMonitorPopup(regionKey: string) {
"humidity": avgHumidity > 0 ? avgHumidity + '%' : 'N/A',
"pm25": avgPm25 > 0 ? avgPm25 + 'μg/m³' : 'N/A',
"co2": avgCo2 > 0 ? avgCo2 + 'ppm' : 'N/A',
"tvoc": getAvgVal('tvoc') > 0 ? getAvgVal('tvoc') + 'ppb' : 'N/A',
"tvoc": getAvgVal('tvoc') > 0 ? getAvgVal('tvoc').toFixed(2) + 'μg/m³' : 'N/A',
"hcho": getAvgVal('hcho') > 0 ? getAvgVal('hcho') + 'ppb' : 'N/A',
"lightLevel": getAvgVal('lightLevel') > 0 ? getAvgVal('lightLevel') + 'lux' : 'N/A',
"oxygen": "N/A", // 含氧量暂无设备数据源
......@@ -2023,13 +2131,17 @@ export async function getMonitorPopup(regionKey: string) {
const deviceType = device.device_type || '';
const deviceState = device.device_state;
// deviceState: 在线=0,离线=1,故障=2
// status: 1 on 0 off
const status = deviceState === DEVICE_STATE.在线 ? 1 : 0;
// status: 0=关, 1=开
let status = deviceState === DEVICE_STATE.在线 ? 1 : 0;
// 从批量查询结果中获取该设备最新监测数据
let monitoringData = '';
const latestData = latestDataMap.get(deviceId);
if (latestData) {
// 空调设备等通过power==on判断是否开机
if (latestData.power === 'off') {
status = 0;
}
const items: string[] = [];
for (const key of Object.keys(latestData)) {
const val = latestData[key];
......@@ -2095,7 +2207,7 @@ export async function controlDeviceRunning(params: {
}
// 第二步:设置设备运行状态并下发
// 目前仅支持开关状态控制(status: 1=开/在线, 0=关/离线
// 目前仅支持开关状态控制(status: 0=关, 1=开
// 后续可扩展:根据 deviceType 调用不同的设备控制协议(如照明、音响、排风等)
return { isSuccess: true, deviceId: deviceId, status: status };
}
......@@ -2115,15 +2227,21 @@ export async function getAcRunningPop(regionKey: string, deviceId: string) {
if (!devices.data.length) {
throw new BizError(ERRORENUM.未找到数据, `设备 ${deviceId} 在区域 ${regionKey} 中未找到`);
}
// 获取设备最新数据
let deviceData = await selectDataListByParam(
TABLENAME.设备数据表,
{ device_id: deviceId, "%orderDesc%": "device_time", "%limit%": 1 },
["device_data", "device_time"]
);
// // 获取设备最新数据
// let deviceData = await selectDataListByParam(
// TABLENAME.设备数据表,
// { device_id: deviceId, "%orderDesc%": "device_time", "%limit%": 1 },
// ["device_data", "device_time"]
// );
// 通过飞奕内机列表获取实时设备数据
let deviceData = await getIndoorUnitStatus(deviceId);
let fanSpeed = deviceData && deviceData.fanSpeed ? fanSpeedMap[deviceData.fanSpeed] : 3; // 默认中速风
deviceData.fanSpeed = acPopFanSpeedMap[fanSpeed];
return {
deviceInfo: devices.data[0].device_param,
latestData: deviceData.data.length ? deviceData.data[0].device_data : null
latestData: deviceData
}
}
......@@ -2139,7 +2257,8 @@ export async function controlAcRunning(params: {}) {
let maxTemp = params["maxTemp"] || 30; // 默认锁定温度上限
let minTemp = params["minTemp"] || 16; // 默认锁定温度下限
let autoControl = params["autoControl"] || "生效";
let fanSpeed = params["fanSpeed"];
let fanSpeed = acPopFanSpeedMap[params["fanSpeed"]];
console.log(fanSpeed)
// 1. 通过设备信息,获取内机地址
let device = await selectOneDataByParam(
......@@ -2153,13 +2272,13 @@ export async function controlAcRunning(params: {}) {
// 2. 接入空调控制API,更新空调设备信息
let acPrarms = {
"indoorUnitAddressFull": [deviceId],
"workMode": acDeviceKeyMap[mode],
"onOff": acDeviceKeyMap[power],
"workMode": workModeMap[mode],
"onOff": powerMap[power],
"tempSet": Number(setTemp),
"fanSpeed": acDeviceKeyMap[fanSpeed],
"onOffLock": acDeviceKeyMap[autoControl],
"workModeLock": acDeviceKeyMap[autoControl],
"tempSetLock": acDeviceKeyMap[autoControl],
"fanSpeed": fanSpeedMap[fanSpeed],
"onOffLock": lockMap[autoControl],
"workModeLock": lockMap[autoControl],
"tempSetLock": lockMap[autoControl],
"tempSetLo": Number(minTemp),
"tempSetHi": Number(maxTemp),
"openApiAction": autoControl === "生效" ? "control" : "lock"
......
/**
* 日程管理 业务逻辑
*/
import { selectDataListByParam, selectDataListToPageByParam, selectDataCountByParam } from "../data/findData";
import { addData } from "../data/addData";
import { delData } from "../data/delData";
import { updateManyData } from "../data/updateData";
import { TABLENAME } from "../config/dbEnum";
// ======================= 公共工具 =======================
/** 格式化为本地时间字符串 YYYY-MM-DD HH:mm:ss */
function formatLocalTime(d: Date): string {
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
// ======================= 日程列表 =======================
/**
* 查询日程列表(分页 + 按有效期/区域过滤)
*/
export async function getScheduleList(params: {
pageNumber?: number;
pageSize?: number;
startDate?: string;
endDate?: string;
deviceIds?: any;
regionKeys?: any;
}): Promise<any> {
const pageNumber = params.pageNumber || 1;
const pageSize = params.pageSize || 10;
const { startDate, endDate, deviceIds, regionKeys } = params;
console.log(`[日程列表] page=${pageNumber}, size=${pageSize}, startDate=${startDate}, endDate=${endDate}, deviceIds=${deviceIds}, regionKeys=${regionKeys}`);
// 构建日程表查询条件
const scheduleWhere: any = { "%orderDesc%": "id" };
// 有效期过滤:日程有效时间与查询区间有交集
if (startDate) {
scheduleWhere.begin_validity = { "%lte%": startDate };
}
if (endDate) {
scheduleWhere.end_validity = { "%gte%": endDate };
}
// 如果按区域过滤,先查出该区域下的 schedule_id 集合
let filteredScheduleIds: number[] | null = null;
let paramAnys: any = {};
if (deviceIds && deviceIds.length) {
paramAnys.device_id = { "%in%": deviceIds };
}
// if (regionKeys && regionKeys.length) {
// paramAnys.region_key = { "%in%": regionKeys };
// }
if ((deviceIds && deviceIds.length) || (regionKeys && regionKeys.length)) {
const sdRes = await selectDataListByParam(TABLENAME.日程设备关联表, paramAnys, ["schedule_id"]);
filteredScheduleIds = (sdRes.data || []).map((r: any) => r.schedule_id);
if (filteredScheduleIds.length === 0) {
return { total: 0, list: [] };
}
scheduleWhere.id = { "%in%": filteredScheduleIds };
}
// 分页查询
const columns = ["id", "title", "ac_controls", "schedule_time", "begin_validity", "end_validity", "created_at"];
const listRes = await selectDataListByParam(
TABLENAME.日程表, scheduleWhere, columns
);
const rows: any[] = listRes.data || [];
// 总数
const countRes = await selectDataCountByParam(TABLENAME.日程表, scheduleWhere);
const total = countRes.data || 0;
// 收集所有日程 id
const scheduleIds = rows.map(r => r.id);
// 批量查关联的设备关联记录
let deviceMap: { [sid: number]: any[] } = {};
if (scheduleIds.length > 0) {
const sdRes = await selectDataListByParam(TABLENAME.日程设备关联表, {
schedule_id: { "%in%": scheduleIds },
}, ["schedule_id", "device_id", "region_key"]);
const sdRows = sdRes.data || [];
for (const sd of sdRows) {
if (!deviceMap[sd.schedule_id]) deviceMap[sd.schedule_id] = [];
deviceMap[sd.schedule_id].push(sd);
}
}
// 批量查区域名称(需要去重 region_key)
const regionKeySet = new Set<number>();
for (const sid of scheduleIds) {
for (const sd of (deviceMap[sid] || [])) {
regionKeySet.add(sd.region_key);
}
}
let regionNameMap: { [rk: number]: string } = {};
if (regionKeySet.size > 0) {
const regionRes = await selectDataListByParam(TABLENAME.区域表, {
id: { "%in%": [...regionKeySet] },
"%orderAsc%": "sort_order",
}, ["id", "region_name"]);
for (const r of (regionRes.data || [])) {
regionNameMap[r.id] = r.region_name;
}
}
// 组装返回列表
const list = rows.map(row => {
const devices = deviceMap[row.id] || [];
const regionNames = [...new Set(devices.map((d: any) => regionNameMap[d.region_key] || ""))].filter(Boolean);
let validity = "";
if (row.begin_validity || row.end_validity) {
const from = row.begin_validity
? (typeof row.begin_validity === "string" ? row.begin_validity.split(" ")[0] : formatLocalTime(new Date(row.begin_validity)).split(" ")[0])
: "";
const to = row.end_validity
? (typeof row.end_validity === "string" ? row.end_validity.split(" ")[0] : formatLocalTime(new Date(row.end_validity)).split(" ")[0])
: "";
validity = `${from}${to}`;
}
return {
id: row.id,
title: row.title,
acControl: row.ac_controls,
scheduleTime: row.schedule_time || "",
validity,
deviceCount: devices.length,
regionNames,
createdAt: row.created_at
? (typeof row.created_at === "string" ? row.created_at : formatLocalTime(new Date(row.created_at)))
: "",
};
});
console.log(`[日程列表] total=${total}, rows=${list.length}`);
return { total, list };
}
// ======================= 新增日程 =======================
/**
* 新增日程(含区域设备关联)
*/
export async function addSchedule(params: {
title: string;
schedule_time: string;
ac_controls: string;
begin_validity?: string;
end_validity?: string;
deviceIds: string[];
regionKeys: number[];
}): Promise<any> {
const { title, schedule_time, ac_controls, begin_validity, end_validity, deviceIds, regionKeys } = params;
console.log(`[新增日程] title=${title}, time=${schedule_time}, action=${ac_controls}, devices=${deviceIds}`);
// 1. 插入 schedule 主表
await addData(TABLENAME.日程表, {
title,
schedule_time,
ac_controls,
begin_validity: begin_validity || null,
end_validity: end_validity || null,
});
// 2. 通过 addData 无法直接获取自增 id,需要反向查询
const inserted = await selectDataListByParam(TABLENAME.日程表, {
title,
schedule_time,
ac_controls,
"%orderDesc%": "id",
"%limit%": 1,
}, ["id"]);
const scheduleId = (inserted.data && inserted.data[0]) ? inserted.data[0].id : null;
if (!scheduleId) {
console.warn("[新增日程] 无法获取插入的 schedule id");
return { isSuccess: false, msg: "日程创建失败" };
}
// 3. 查这些区域下的所有空调设备
let deviceParams:any = {
device_type: "空调",
};
if (deviceIds && deviceIds.length) {
deviceParams.device_id = { "%in%": deviceIds };
}
if (regionKeys && regionKeys.length) {
deviceParams.region_key = { "%in%": regionKeys };
}
const deviceRes = await selectDataListByParam(TABLENAME.设备表, deviceParams, ["device_id", "region_key"]);
const devices = deviceRes.data || [];
// 4. 批量插入 schedule_device 关联记录
if (devices.length > 0) {
const sdRecords = devices.map((d: any) => ({
schedule_id: scheduleId,
device_id: d.device_id,
region_key: d.region_key,
}));
await addData(TABLENAME.日程设备关联表, sdRecords);
console.log(`[新增日程] 关联设备 ${sdRecords.length} 条`);
} else if (!regionKeys && !regionKeys.length) {
// 无设备时也插入一条区域占位记录(允许区域下暂时无设备)
for (const rk of regionKeys) {
await addData(TABLENAME.日程设备关联表, {
schedule_id: scheduleId,
device_id: "",
region_key: rk,
});
}
console.log(`[新增日程] 区域 ${regionKeys.length} 个,暂无设备`);
}
return { isSuccess: true, scheduleId };
}
// ======================= 日程详情 =======================
/**
* 获取日程详情(含关联区域/设备)
*/
export async function getScheduleDetail(scheduleId: number): Promise<any> {
console.log(`[日程详情] scheduleId=${scheduleId}`);
// 1. 查日程主表
const scheduleRes = await selectDataListByParam(TABLENAME.日程表, {
id: scheduleId,
});
const rows = scheduleRes.data || [];
if (rows.length === 0) {
return null;
}
const row = rows[0];
// 2. 查关联设备
const sdRes = await selectDataListByParam(TABLENAME.日程设备关联表, {
schedule_id: scheduleId,
}, ["device_id", "region_key"]);
const sdRows = sdRes.data || [];
const selectedRegions = [...new Set(sdRows.map((r: any) => r.region_key))];
const selectedDevices = sdRows.filter((r: any) => r.device_id).map((r: any) => r.device_id);
return {
id: row.id,
title: row.title,
scheduleTime: row.schedule_time || "",
acControl: row.ac_controls,
beginValidity: row.begin_validity
? (typeof row.begin_validity === "string" ? row.begin_validity.split(" ")[0] : formatLocalTime(new Date(row.begin_validity)).split(" ")[0])
: "",
endValidity: row.end_validity
? (typeof row.end_validity === "string" ? row.end_validity.split(" ")[0] : formatLocalTime(new Date(row.end_validity)).split(" ")[0])
: "",
selectedRegions,
selectedDevices,
};
}
// ======================= 编辑日程 =======================
/**
* 编辑日程(更新主表 + 重建关联)
*/
export async function editSchedule(params: {
scheduleId: number;
title: string;
schedule_time: string;
ac_controls: string;
begin_validity?: string;
end_validity?: string;
deviceIds: string[];
regionKeys: number[];
}): Promise<any> {
const { scheduleId, title, schedule_time, ac_controls, begin_validity, end_validity, deviceIds, regionKeys } = params;
console.log(`[编辑日程] id=${scheduleId}, title=${title}`);
// 1. 更新 schedule 主表
await updateManyData(TABLENAME.日程表, { id: scheduleId }, {
title,
schedule_time,
ac_controls,
begin_validity: begin_validity || null,
end_validity: end_validity || null,
updated_at: formatLocalTime(new Date()),
});
// 2. 删除旧的关联记录
await delData(TABLENAME.日程设备关联表, { schedule_id: scheduleId });
// 3. 查区域下的空调设备
let deviceParams:any = {
device_type: "空调",
};
if (deviceIds && deviceIds.length) {
deviceParams.device_id = { "%in%": deviceIds };
}
if (regionKeys && regionKeys.length) {
deviceParams.region_key = { "%in%": regionKeys };
}
const deviceRes = await selectDataListByParam(TABLENAME.设备表, deviceParams, ["device_id", "region_key"]);
const devices = deviceRes.data || [];
// 4. 重建关联
if (devices.length > 0) {
const sdRecords = devices.map((d: any) => ({
schedule_id: scheduleId,
device_id: d.device_id,
region_key: d.region_key,
}));
await addData(TABLENAME.日程设备关联表, sdRecords);
console.log(`[编辑日程] 重建关联设备 ${sdRecords.length} 条`);
} else if (!regionKeys && !regionKeys.length) {
for (const rk of regionKeys) {
await addData(TABLENAME.日程设备关联表, {
schedule_id: scheduleId,
device_id: "",
region_key: rk,
});
}
console.log(`[编辑日程] 区域 ${regionKeys.length} 个,暂无设备`);
}
return { isSuccess: true };
}
// ======================= 删除日程 =======================
/**
* 删除日程(含关联记录)
*/
export async function deleteSchedule(scheduleId: number): Promise<any> {
console.log(`[删除日程] id=${scheduleId}`);
// 1. 删除关联记录
await delData(TABLENAME.日程设备关联表, { schedule_id: scheduleId });
// 2. 删除主记录
await delData(TABLENAME.日程表, { id: scheduleId });
return { isSuccess: true };
}
/**
* 用户管理业务
*/
import { selectOneDataByParam, selectPaginatedData } from '../data/findData';
import { addData } from '../data/addData';
import { updateManyData } from '../data/updateData';
import { delData } from '../data/delData';
import { TABLENAME } from '../config/dbEnum';
import { getPwdMd5 } from '../tools/system';
const USER_COLUMNS = ['login_id', 'name', 'phone', 'role', 'department', 'status', 'created_at', 'updated_at'];
/**
* 用户管理列表(分页)
* @param pageNumber 页码
* @param pageSize 每页条数
*/
export async function getUsersList(pageNumber: number, pageSize: number) {
const data = await selectPaginatedData(TABLENAME.用户信息表, {}, USER_COLUMNS, pageNumber, pageSize);
return data;
}
/**
* 用户详情
* @param loginId 登录账号
*/
export async function getUsersDetail(loginId: string) {
const result = await selectOneDataByParam(TABLENAME.用户信息表, { login_id: loginId }, USER_COLUMNS);
if (!result.data || Object.keys(result.data).length === 0) {
return null;
}
return result.data;
}
/**
* 新增用户
* @param params.login_id 账号
* @param params.name 用户名称
* @param params.phone 手机号
* @param params.role 角色
* @param params.department 部门
* @param params.pwd 密码
*/
export async function addUser(params: {
login_id: string;
name: string;
phone: string;
role: string;
department: string;
pwd: string;
}) {
// 校验账号唯一性
const exist = await selectOneDataByParam(TABLENAME.用户信息表, { login_id: params.login_id });
if (exist.data && Object.keys(exist.data).length > 0) {
throw new Error(`账号 ${params.login_id} 已存在`);
}
const now = new Date();
const hashedPwd = getPwdMd5(params.login_id, params.pwd);
await addData(TABLENAME.用户信息表, {
login_id: params.login_id,
name: params.name,
phone: params.phone,
role: params.role,
department: params.department,
pwd: hashedPwd,
status: '启用',
created_at: now,
updated_at: now,
});
return { success: true };
}
/**
* 编辑用户
* @param params.login_id 账号(定位用,必填)
* @param params.name 用户名称(可选)
* @param params.phone 手机号(可选)
* @param params.role 角色(可选)
* @param params.department 部门(可选)
* @param params.status 状态(可选)
* @param params.pwd 密码(可选,传了则更新)
*/
export async function editUser(params: {
login_id: string;
name?: string;
phone?: string;
role?: string;
department?: string;
status?: string;
pwd?: string;
}) {
// 校验账号是否存在
const exist = await selectOneDataByParam(TABLENAME.用户信息表, { login_id: params.login_id });
if (!exist.data || Object.keys(exist.data).length === 0) {
throw new Error(`账号 ${params.login_id} 不存在`);
}
const updates: any = { updated_at: new Date() };
if (params.name !== undefined) updates.name = params.name;
if (params.phone !== undefined) updates.phone = params.phone;
if (params.role !== undefined) updates.role = params.role;
if (params.department !== undefined) updates.department = params.department;
if (params.status !== undefined) updates.status = params.status;
if (params.pwd !== undefined) updates.pwd = getPwdMd5(params.login_id, params.pwd);
await updateManyData(TABLENAME.用户信息表, { login_id: params.login_id }, updates);
return { success: true };
}
/**
* 删除用户
* @param loginId 登录账号
*/
export async function deleteUser(loginId: string) {
// 校验账号是否存在
const exist = await selectOneDataByParam(TABLENAME.用户信息表, { login_id: loginId });
if (!exist.data || Object.keys(exist.data).length === 0) {
throw new Error(`账号 ${loginId} 不存在`);
}
await delData(TABLENAME.用户信息表, { login_id: loginId });
return { success: true };
}
......@@ -103,36 +103,57 @@ export enum RELATION_TYPE {
// ==================== 空调控制相关枚举 ====================
/**
* 工作模式枚举
* 开关控制映射:开=1, 关=0
*/
export enum WORK_MODE {
制冷 = 1,
制热 = 2,
送风 = 3,
除湿 = 4,
export enum powerMap {
"开" = 1,
"关" = 0,
"on" = 1,
"off" = 0,
}
/**
* 开关状态枚举
* 工作模式映射:1=制冷 2=制热 3=送风 4=除湿 0=自动
*/
export enum POWER_STATE {
on = 'on',
off = 'off',
export enum workModeMap {
"制冷" = 1,
"制热" = 2,
"送风" = 3,
"除湿" = 4,
"自动" = 0,
}
/**
* 风速枚举
* 风速映射:1=高风 2=中风 4=低风
*/
export enum FAN_SPEED {
超强 = 1,
= 2,
= 4,
export enum fanSpeedMap {
"高风" = 1,
"中风" = 2,
"低风" = 4,
}
/**
* 自动控制状态枚举
* acPop风速映射:1-自动 2-低风 3-中风 4-高风
*/
export enum AUTO_CONTROL {
生效 = '生效',
解除 = '解除',
export enum acPopFanSpeedMap {
"自动" = 1,
"低风" = 2,
"中风" = 3,
"高风" = 4,
}
/**
* 自动控制锁定映射:生效=0, 解除=1
*/
export enum lockMap {
"生效" = 0,
"解除" = 1,
}
/**
* 反向映射(数值 → 中文,展示用)
*/
export const POWER_TEXT_MAP: Record<number, string> = { 1: '开', 0: '关' };
export const WORK_MODE_TEXT_MAP: Record<number, string> = { 1: '制冷', 2: '制热', 3: '送风', 4: '除湿', 0: '自动' };
export const FAN_SPEED_TEXT_MAP: Record<number, string> = { 1: '高风', 2: '中风', 4: '低风' };
export const DEVICE_STATE_TEXT_MAP: Record<number, string> = { 0: '在线', 1: '离线', 2: '故障' };
......@@ -9,7 +9,8 @@ export enum TABLENAME {
区域表 = 'region',
区域设备关联表 = 'region_device_rel',
日程表 = 'schedule',
设备操作日志表 = 'device_logs',
设备日志表 = 'device_logs',
日程设备关联表 = 'schedule_device',
设备故障表 = 'device_fault'
};
......
......@@ -19,3 +19,12 @@
gender_limit:{type:"Number"}, //性别限制(1-无,2-只限男生,3-只限女生)
restrict_to_class:{type:"Boolean"} //是否限制只能带本班级学生
};
/**
* 将 Date 对象格式化为本地时间字符串 'YYYY-MM-DD HH:mm:ss'
* 替代 toISOString() 避免 UTC 时区偏移问题
*/
export function formatLocalTime(date: Date): string {
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())}`;
}
......@@ -34,6 +34,27 @@ export const TablesConfig = [
type: Sequelize.STRING(255),
comment: '用户名称'
},
phone: {
type: Sequelize.STRING(20),
allowNull: true,
comment: '手机号'
},
role: {
type: Sequelize.STRING(50),
allowNull: true,
comment: '角色'
},
department: {
type: Sequelize.STRING(100),
allowNull: true,
comment: '部门'
},
status: {
type: Sequelize.STRING(20),
allowNull: false,
defaultValue: '启用',
comment: '状态:启用/停用'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
......@@ -336,7 +357,7 @@ export const TablesConfig = [
},
schedule_date: {
type: DataTypes.DATE,
allowNull: false,
allowNull: true,
comment: '日期'
},
ac_controls: {
......@@ -390,9 +411,48 @@ export const TablesConfig = [
association: [
]
},
// 设备操作日志
// 日程设备关联
{
tableNameCn: '设备操作日志表',
tableNameCn: '日程设备关联表',
tableName: 'schedule_device',
schema: {
id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '自增主键'
},
schedule_id: {
type: DataTypes.BIGINT,
allowNull: false,
comment: '日程ID,关联schedule.id'
},
device_id: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '设备ID,关联device.device_id'
},
region_key: {
type: DataTypes.BIGINT,
allowNull: false,
comment: '区域key,关联region.id,表示设备所属区域'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '创建时间'
}
},
association: [
{ type: "hasMany", target: "device", foreignKey: "device_id" },
{ type: "hasMany", target: "region", foreignKey: "region_key" }
]
},
// 设备日志表
{
tableNameCn: '设备日志表',
tableName: 'device_logs',
schema: {
id: {
......@@ -412,6 +472,11 @@ export const TablesConfig = [
allowNull: false,
comment: '设备类型'
},
title: {
type: Sequelize.STRING(255),
allowNull: true,
comment: '日志标题'
},
operation_content: {
type: Sequelize.TEXT,
allowNull: true,
......@@ -474,7 +539,7 @@ export const TablesConfig = [
},
fault_origin_id: {
type: Sequelize.STRING(100),
allowNull: false,
allowNull: true,
comment: '原始故障id'
},
fault_code: {
......
......@@ -6,7 +6,7 @@
* - 任务锁死时(超时未释放),会主动强制释放锁,确保后续任务能正常执行
*/
import { region, device, deviceData, alertWorkData, processFaultStatus, diqinSyncAll, migrateDeviceRegionKey } from "../biz/dataIntegration";
import { region, device, deviceData, alertWorkData, processFaultStatus, diqinSyncAll, migrateDeviceRegionKey, migrateDiqinTvocUnit } from "../biz/dataIntegration";
// ==================== 通用任务锁 ====================
......@@ -134,6 +134,7 @@ const dataSyncTask = createScheduledTask('定时任务', TASK_INTERVAL_MS, dataS
await deviceData();
await diqinSyncAll();
await migrateDeviceRegionKey();
await migrateDiqinTvocUnit();
});
// 预警工单任务
......
......@@ -49,10 +49,11 @@ export async function initConfig() {
// 迪勤云平台配置
if (diqin) {
let diqinConfig = diqin[0];
systemConfig.diqin = { appKey: '', secretKey: '', baseUrl: '' };
systemConfig.diqin = { appKey: '', secretKey: '', baseUrl: '', tvocConvertFactor: 0.0013 };
if (diqinConfig.diqinAppKey) systemConfig.diqin.appKey = diqinConfig.diqinAppKey[0];
if (diqinConfig.diqinSecretKey) systemConfig.diqin.secretKey = diqinConfig.diqinSecretKey[0];
if (diqinConfig.diqinBaseUrl) systemConfig.diqin.baseUrl = diqinConfig.diqinBaseUrl[0];
if (diqinConfig.diqinTvocConvertFactor) systemConfig.diqin.tvocConvertFactor = parseFloat(diqinConfig.diqinTvocConvertFactor[0]);
}
} catch(err) {
......
......@@ -26,5 +26,7 @@ export class ServerConfig {
appKey: string;
secretKey: string;
baseUrl: string;
/** PID-TVOC 换算系数(ppb → μg/m³),入库时换算,默认 0.0013 */
tvocConvertFactor: number;
}
}
\ No newline at end of file
......@@ -320,13 +320,8 @@ export async function selectPaginatedData( tableName: string, param: QueryParam,
const result = await tableModel.findAndCountAll(selectParam);
return {
data: result.rows,
pagination: {
page: pageNumber,
pageSize,
total: result.count,
totalPages: Math.ceil(result.count / pageSize)
}
list: result.rows,
total: result.count
};
}
......
/**
* 后端管理系统 路由
*/
import asyncHandler from "express-async-handler";
import * as regionBiz from "../biz/region";
import * as scheduleBiz from "../biz/schedule";
import * as deviceBiz from "../biz/device";
import * as usersBiz from "../biz/users";
import * as logBiz from "../biz/log";
import { eccReqParamater } from "../util/verificationParam";
import { get } from "http";
export function setRouter(httpServer) {
/** 区域树 */
httpServer.get("/api/dq/admin/region/device", asyncHandler(getRegionDevices));
/** 区域详情 */
httpServer.get("/api/dq/admin/region/detail", asyncHandler(getRegionDetail));
/** 区域管理新增 */
httpServer.post("/api/dq/admin/region/add", asyncHandler(addRegion));
/** 区域管理编辑 */
httpServer.post("/api/dq/admin/region/edit", asyncHandler(editRegion));
/** 区域管理删除 */
httpServer.post("/api/dq/admin/region/delete", asyncHandler(deleteRegion));
/** 日程管理列表 */
httpServer.get("/api/dq/admin/schedule/list", asyncHandler(getScheduleList));
/** 日程管理详情 */
httpServer.get("/api/dq/admin/schedule/detail", asyncHandler(getScheduleDetail));
/** 日程管理新增 */
httpServer.post("/api/dq/admin/schedule/add", asyncHandler(addSchedule));
/** 日程管理编辑 */
httpServer.post("/api/dq/admin/schedule/edit", asyncHandler(editSchedule));
/** 日程管理删除 */
httpServer.post("/api/dq/admin/schedule/delete", asyncHandler(deleteSchedule));
/** 设备管理列表 */
httpServer.get("/api/dq/admin/device/list", asyncHandler(getDeviceList));
/** 设备管理 - 区域查询 */
httpServer.post("/api/dq/admin/device/devices", asyncHandler(getDevicesByRegion));
/** 设备管理 - 编辑空调控制参数 */
httpServer.post("/api/dq/admin/device/edit", asyncHandler(editDevice));
/** 设备管理 - 批量编辑设备 */
httpServer.post("/api/dq/admin/device/edit_batch", asyncHandler(batchEditDevices));
/** 设备管理 - 编辑设备信息 */
httpServer.post("/api/dq/admin/device/editDevice", asyncHandler(editLocalDevice));
/** 设备管理 - 批量编辑设备信息 */
httpServer.post("/api/dq/admin/device/editDevice_batch", asyncHandler(batchEditLocalDevices));
/** 用户管理 - 列表 */
httpServer.get("/api/dq/admin/users/list", asyncHandler(getUsersList));
/** 用户管理 - 详情 */
httpServer.get("/api/dq/admin/users/detail", asyncHandler(getUsersDetail));
/** 用户管理 - 添加用户 */
httpServer.post("/api/dq/admin/users/add", asyncHandler(addUsers));
/** 用户管理 - 修改用户 */
httpServer.post("/api/dq/admin/users/edit", asyncHandler(editUsers));
/** 用户管理 - 删除用户 */
httpServer.post("/api/dq/admin/users/delete", asyncHandler(deleteUsers));
/** 日志管理 - 列表 */
httpServer.get("/api/dq/admin/device/logs", asyncHandler(getDeviceLogs));
}
// ======================= 区域设备 =======================
/**
* GET /api/dq/admin/region/device
* 获取区域设备树(馆 → 层 → 区域 → 设备)
* query: regionName(模糊), regionType(精准)
*/
async function getRegionDevices(req, res) {
const { regionName, regionType } = req.query || {};
const data = await regionBiz.getRegionDevices({ regionName, regionType });
res.success(data);
}
/**
* GET /api/dq/admin/region/detail
* 区域详情:父级区域、区域标识、区域名称、区域类型、排序号、最大容量
* query: regionKey
*/
async function getRegionDetail(req, res) {
const regionKey: string = req.query?.regionKey as string;
if (!regionKey) {
res.success({success: false, msg: "缺少参数 regionKey"});
return;
}
const data = await regionBiz.getRegionDetail(regionKey);
res.success(data);
}
/**
* POST /api/dq/admin/region/add
* 新增区域:父级标识、区域标识、区域名称、区域类型、父级ID、排序号、最大容量
* body: parentKey, regionKey, regionName, regionType, parentId, sortOrder, maxCapacity
*/
async function addRegion(req, res) {
let reqConf = { parentKey: 'String', regionKey: 'String', regionName: 'String', regionType: 'String', parentId: 'Number', sortOrder: 'Number', maxCapacity: 'Number' };
const NotMustHaveKeys = ["parentKey", "parentId", "regionType", "maxCapacity"];
let { regionName, regionType, parentKey, regionKey, parentId, sortOrder, maxCapacity } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const data = await regionBiz.addRegion({ regionName, regionType, parentKey, regionKey, parentId, sortOrder, maxCapacity });
res.success(data);
}
/**
* POST /api/dq/admin/region/edit
* 编辑区域:regionKey定位,其余字段可选
* body: regionKey, regionName?, regionType?, parentKey?, parentId?, sortOrder?, maxCapacity?
*/
async function editRegion(req, res) {
let reqConf = { regionKey: 'String', regionName: 'String', regionType: 'String', parentKey: 'String', parentId: 'Number', sortOrder: 'Number', maxCapacity: 'Number' };
const NotMustHaveKeys = ["regionName", "regionType", "parentKey", "parentId", "sortOrder", "maxCapacity"];
let { regionKey, regionName, regionType, parentKey, parentId, sortOrder, maxCapacity } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!regionKey) {
res.success({success: false, msg: "缺少必填参数:regionKey"});
return;
}
const data = await regionBiz.editRegion({ regionKey, regionName, regionType, parentKey, parentId, sortOrder, maxCapacity });
res.success(data);
}
/**
* POST /api/dq/admin/region/delete
* 删除区域:检查无子区域才能删
* body: regionKey
*/
async function deleteRegion(req, res) {
const { regionKey } = req.body || {};
if (!regionKey) {
res.success({success: false, msg: "缺少参数 regionKey"});
return;
}
const data = await regionBiz.deleteRegion(regionKey);
res.success(data);
}
// ======================= 日程管理 =======================
/**
* GET /api/dq/admin/schedule/list
* 日程列表(分页 + 有效期 + 区域过滤)
* query: pageNumber, pageSize, startDate, endDate, regionKey
*/
async function getScheduleList(req, res) {
const data = await scheduleBiz.getScheduleList({
pageNumber: Number(req.query?.pageNumber) || 1,
pageSize: Number(req.query?.pageSize) || 10,
startDate: req.query && req.query.startDate?req.query.startDate:'',
endDate: req.query && req.query.endDate?req.query.endDate:'',
deviceIds: req.query?.deviceIds ? req.query.deviceIds : undefined,
regionKeys: req.query?.regionKeys ? req.query.regionKeys : undefined,
});
res.success(data);
}
/**
* GET /api/dq/admin/schedule/detail
* 日程详情(含关联区域/设备)
* query: scheduleId
*/
async function getScheduleDetail(req, res) {
const scheduleId = Number(req.query?.scheduleId);
if (!scheduleId) {
res.success({success: false, msg: "缺少参数 scheduleId"});
return;
}
const data = await scheduleBiz.getScheduleDetail(scheduleId);
if (!data) {
res.success({success: false, msg: "日程不存在"});
return;
}
res.success(data);
}
/**
* POST /api/dq/admin/schedule/add
* 新增日程
* body: title, scheduleTime, acControl, beginValidity, endValidity, regionKeys
*/
async function addSchedule(req, res) {
let reqConf = { title: 'String', scheduleTime: 'String', acControl: 'String', beginValidity: 'Date', endValidity: 'Date', deviceIds: 'Array', regionKeys: 'Array' };
const NotMustHaveKeys = ["deviceIds", "regionKeys"];
let { title, scheduleTime, acControl, beginValidity, endValidity, deviceIds, regionKeys } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!title || !scheduleTime || !acControl) {
res.success({success: false, msg: "缺少必填参数:title, scheduleTime, acControl"});
return;
}
if ((!deviceIds && !regionKeys) || (!deviceIds.length && !regionKeys.length)) {
res.success({success: false, msg: "至少选择一个区域"});
return;
}
const data = await scheduleBiz.addSchedule({
title,
schedule_time: scheduleTime,
ac_controls: acControl,
begin_validity: beginValidity || undefined,
end_validity: endValidity || undefined,
deviceIds: deviceIds ?? [],
regionKeys: regionKeys ?? []
});
res.success(data);
}
/**
* POST /api/dq/admin/schedule/edit
* 编辑日程
* body: scheduleId, title, scheduleTime, acControl, beginValidity, endValidity, regionKeys
*/
async function editSchedule(req, res) {
let reqConf = { scheduleId: 'Number', title: 'String', scheduleTime: 'String', acControl: 'String', beginValidity: 'Date', endValidity: 'Date', deviceIds: 'Array', regionKeys: 'Array' };
const NotMustHaveKeys = ["deviceIds", "regionKeys"];
let { scheduleId, title, scheduleTime, acControl, beginValidity, endValidity, deviceIds, regionKeys } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!scheduleId || !title || !scheduleTime || !acControl) {
res.success({success: false, msg: "缺少必填参数:title, scheduleTime, acControl"});
return;
}
if ((!deviceIds && !regionKeys) || (!deviceIds.length && !regionKeys.length)) {
res.success({success: false, msg: "至少选择一个区域"});
return;
}
const data = await scheduleBiz.editSchedule({
scheduleId,
title,
schedule_time: scheduleTime,
ac_controls: acControl,
begin_validity: beginValidity || undefined,
end_validity: endValidity || undefined,
deviceIds: deviceIds ?? [],
regionKeys: regionKeys ?? []
});
res.success(data);
}
/**
* POST /api/dq/admin/schedule/delete
* 删除日程
* body: scheduleId
*/
async function deleteSchedule(req, res) {
const { scheduleId } = req.body || {};
if (!scheduleId) {
res.success({success: false, msg: "缺少参数 scheduleId"});
return;
}
const data = await scheduleBiz.deleteSchedule(Number(scheduleId));
res.success(data);
}
// ======================= 设备管理 =======================
/**
* GET /api/dq/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);
}
/**
* GET /api/dq/admin/device/devices
* 设备管理 - 区域查询
* query: regionKeys
*/
async function getDevicesByRegion(req, res) {
let reqConf = { regionKeys: 'Array' };
let { regionKeys } = eccReqParamater(reqConf, req.body);
const data = await deviceBiz.getDevicesByRegion({
regionKeys: regionKeys
});
res.success(data);
}
/**
* POST /api/dq/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.success({success: false, msg: "缺少必填参数:deviceId, power"});
return;
}
const data = await deviceBiz.editAcDeviceControl({ deviceId, power, setTemp, fanSpeed, deviceMode });
res.success(data);
}
/**
* POST /api/dq/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.success({success: false, msg: "缺少必填参数:regionKeys"});
return;
}
if (!edits || typeof edits !== 'object') {
res.success({success: false, msg: "缺少必填参数:edits"});
return;
}
if (edits.power === undefined && edits.setTemp === undefined && edits.linkageStart === undefined) {
res.success({success: false, msg: "edits 中至少需要 power / setTemp / linkageStart 其中之一"});
return;
}
// 校验 edits.power 值合法性
if (edits.power !== undefined && !['开', '关'].includes(edits.power)) {
res.success({success: false, msg: "edits.power 只能为 开 或 关"});
return;
}
// 校验 edits.setTemp 值合法性
if (edits.setTemp !== undefined && (typeof edits.setTemp !== 'number' || edits.setTemp < 16 || edits.setTemp > 30)) {
res.success({success: false, msg: "edits.setTemp 必须为 16~30 之间的数值"});
return;
}
// 校验 edits.linkageStart 值合法性
if (edits.linkageStart !== undefined && !['on', 'off'].includes(edits.linkageStart)) {
res.success({success: false, msg: "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/dq/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.success({success: false, msg: "缺少必填参数:deviceId"});
return;
}
const hasEdit = deviceName !== undefined || regionKey !== undefined
|| deviceType !== undefined || deviceState !== undefined || linkageStart !== undefined;
if (!hasEdit) {
res.success({success: false, msg: "至少需要提供一个编辑字段(deviceName/regionKey/deviceType/controlParams/deviceState)"});
return;
}
const data = await deviceBiz.editLocalDevice({ deviceId, deviceName, regionKey, deviceType, deviceState, linkageStart });
res.success(data);
}
/**
* POST /api/dq/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.success({success: false, msg: "缺少必填参数:edits"});
return;
}
if ((!regionKeys || regionKeys.length === 0) && !deviceName) {
res.success({success: false, msg: "至少需要提供 regionKeys 或 deviceName 筛选条件"});
return;
}
const validEditKeys = ["deviceName", "regionKey", "deviceType", "controlParams", "deviceState"];
const hasEdit = validEditKeys.some(k => edits[k] !== undefined);
if (!hasEdit) {
res.success({success: false, msg: "edits 中至少需要一个编辑字段(deviceName/regionKey/deviceType/controlParams/deviceState)"});
return;
}
const data = await deviceBiz.batchEditLocalDevices({ regionKeys, deviceName, edits });
res.success(data);
}
// ======================= 用户管理 =======================
/**
* GET /api/dq/admin/users/list
* 用户管理列表(分页)
* query: pageNumber, pageSize
*/
async function getUsersList(req, res) {
const pageNumber = Number(req.query?.pageNumber) || 1;
const pageSize = Number(req.query?.pageSize) || 10;
const data = await usersBiz.getUsersList(pageNumber, pageSize);
res.success(data);
}
/**
* GET /api/dq/admin/users/detail
* 用户详情
* query: loginId
*/
async function getUsersDetail(req, res) {
const loginId: string = req.query?.loginId as string;
if (!loginId) {
res.success({success: false, msg: "缺少参数 loginId"});
return;
}
const data = await usersBiz.getUsersDetail(loginId);
if (!data) {
res.success({success: false, msg: "用户不存在"});
return;
}
res.success({ success: true, data });
}
/**
* POST /api/dq/admin/users/add
* 新增用户
* body: loginId, name, phone, role, department, pwd
*/
async function addUsers(req, res) {
let reqConf = { loginId: 'String', name: 'String', phone: 'String', role: 'String', department: 'String', pwd: 'String' };
let { loginId, name, phone, role, department, pwd } = eccReqParamater(reqConf, req.body, []);
if (!loginId || !name || !phone || !role || !department || !pwd) {
res.success({success: false, msg: "缺少必填参数:loginId, name, phone, role, department, pwd"});
return;
}
const data = await usersBiz.addUser({ login_id: loginId, name, phone, role, department, pwd });
res.success(data);
}
/**
* POST /api/dq/admin/users/edit
* 编辑用户
* body: loginId, name?, phone?, role?, department?, status?, pwd?
*/
async function editUsers(req, res) {
let reqConf = { loginId: 'String', name: 'String', phone: 'String', role: 'String', department: 'String', status: 'String', pwd: 'String' };
const NotMustHaveKeys = ["name", "phone", "role", "department", "status", "pwd"];
let { loginId, name, phone, role, department, status, pwd } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!loginId) {
res.success({success: false, msg: "缺少必填参数:loginId"});
return;
}
const data = await usersBiz.editUser({ login_id: loginId, name, phone, role, department, status, pwd });
res.success(data);
}
/**
* POST /api/dq/admin/users/delete
* 删除用户
* body: loginId
*/
async function deleteUsers(req, res) {
const loginId: string = req.body?.loginId as string;
if (!loginId) {
res.success({success: false, msg: "缺少参数 loginId"});
return;
}
const data = await usersBiz.deleteUser(loginId);
res.success(data);
}
// ======================= 日志管理 =======================
/**
* GET /api/dq/admin/device/logs
* 日志管理列表(分页)
* query: pageNumber, pageSize
*/
async function getDeviceLogs(req, res) {
const pageNumber = Number(req.query?.pageNumber) || 1;
const pageSize = Number(req.query?.pageSize) || 10;
const data = await logBiz.getDeviceLogsList({ pageNumber, pageSize });
res.success(data);
}
\ No newline at end of file
......@@ -7,12 +7,14 @@ import { eccReqParamater } from '../util/verificationParam';
export function setRouter(httpServer) {
/** 区域位置 */
/** 区域坐标位置 */
httpServer.post('/api/dq/region/location', asyncHandler(getRegionLocation));
/** 区域下拉选择 */
httpServer.get('/api/dq/region/list', asyncHandler(getRegionList));
}
/**
* 区域位置
* 区域坐标位置
*/
async function getRegionLocation(req, res) {
let reqConf = {gatewayPage:'Number', hour: 'Number'};
......@@ -21,3 +23,11 @@ async function getRegionLocation(req, res) {
const result = await regionBiz.getRegionLocation(gatewayPage, hour);
res.success(result);
}
/**
* 区域下拉选择
*/
async function getRegionList(req, res) {
const result = await regionBiz.getRegionList();
res.success(result);
}
\ No newline at end of file
......@@ -5,6 +5,7 @@ import * as feiyiClientRouter from './feiyiClient';
import * as diqinClientRouter from './diqinClient';
import * as regionRouter from './region';
import * as runningRouter from './running';
import * as adminRouter from './admin';
export function setRouter(httpServer) {
......@@ -13,6 +14,8 @@ export function setRouter(httpServer) {
regionRouter.setRouter(httpServer);
runningRouter.setRouter(httpServer);
adminRouter.setRouter(httpServer);
}
......
......@@ -11,6 +11,8 @@ export function setRouter(httpServer) {
httpServer.post('/api/dq/run/analysis', asyncHandler(getRunAnalysis));
/** 运行分析弹窗 */
httpServer.post('/api/dq/run/analysis/popup', asyncHandler(getAnalysisPopup));
/** 运行分析弹窗 - 趋势图 */
httpServer.post('/api/dq/run/analysis/popup/trend', asyncHandler(getAnalysisPopupSubTrend));
/** 环境态势感知 */
httpServer.post('/api/dq/run/analysis/environmental', asyncHandler(getRunEnvironmental));
/** 智能监控 */
......@@ -45,6 +47,16 @@ async function getAnalysisPopup(req, res) {
}
/**
* 运行分析弹窗 - 趋势图
*/
async function getAnalysisPopupSubTrend(req, res) {
let reqConf = { currRegionKey: 'String', selectedRegionKey: 'String' };
let { currRegionKey, selectedRegionKey } = eccReqParamater(reqConf, req.body);
const result = await runningBiz.getAnalysisPopupSubTrend(currRegionKey, selectedRegionKey);
res.success(result);
}
/**
* 环境态势感知
*/
async function getRunEnvironmental(req, res) {
......
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