部署第一版

parent af021737
<config> <config>
<port>13269</port> <port>13269</port>
<sign></sign> <sign></sign>
<img>http://127.0.0.1:13269</img> <img>http://127.0.0.1:80</img>
<mysqldb> <mysqldb>
<!-- 本地mysql配置 --> <!-- 本地mysql配置 -->
<mysqlHost>127.0.0.1</mysqlHost> <!-- <mysqlHost>127.0.0.1</mysqlHost>
<mysqlPort>3306</mysqlPort> <mysqlPort>3306</mysqlPort>
<mysqlUser>root</mysqlUser> <mysqlUser>root</mysqlUser>
<mysqlPwd>123456</mysqlPwd> <mysqlPwd>123456</mysqlPwd>
<dataBase>dst_technology_platform</dataBase> <dataBase>dst_technology_platform</dataBase> -->
<!-- 服务器mysql配置 --> <!-- 服务器mysql配置 -->
<!-- <mysqlHost>127.0.0.1</mysqlHost> <!-- <mysqlHost>127.0.0.1</mysqlHost>
<mysqlPort>13306</mysqlPort> <mysqlPort>13306</mysqlPort>
<mysqlUser>root</mysqlUser> <mysqlUser>root</mysqlUser>
<mysqlPwd>root</mysqlPwd> <mysqlPwd>root</mysqlPwd>
<dataBase>dst_technology_platform</dataBase> --> <dataBase>dst_technology_platform</dataBase> -->
<!-- dst服务器mysql配置 49.235.185.26 -->
<mysqlHost>127.0.0.1</mysqlHost>
<mysqlPort>3306</mysqlPort>
<mysqlUser>deploy</mysqlUser>
<mysqlPwd>Qaz123456!</mysqlPwd>
<dataBase>dst_technology_platform</dataBase>
</mysqldb> </mysqldb>
<feiyi> <feiyi>
<feiyiAppKey>0</feiyiAppKey> <feiyiAppKey>444940462151958528</feiyiAppKey>
<feiyiSecretKey>0</feiyiSecretKey> <feiyiSecretKey>d30353e191c94ed3a3b1c05345e284b1</feiyiSecretKey>
<feiyiBaseUrl>https://m.achelp.cn/open</feiyiBaseUrl> <feiyiBaseUrl>https://m.achelp.cn/open</feiyiBaseUrl>
</feiyi> </feiyi>
<diqin> <diqin>
<diqinAppKey>178428005410049</diqinAppKey> <!-- <diqinAppKey>178428005410049</diqinAppKey> -->
<diqinSecretKey>pGAII3TfpdrJ4zjrPncERwtt</diqinSecretKey> <!-- <diqinSecretKey>pGAII3TfpdrJ4zjrPncERwtt</diqinSecretKey> -->
<diqinAppKey>178486189110050</diqinAppKey>
<diqinSecretKey>CQuwrqgtaQhnwgcChB9Ejgtt</diqinSecretKey>
<diqinBaseUrl>https://airiccc.com</diqinBaseUrl> <diqinBaseUrl>https://airiccc.com</diqinBaseUrl>
</diqin> </diqin>
</config> </config>
...@@ -16,6 +16,7 @@ import { ...@@ -16,6 +16,7 @@ import {
} from "./diqinClient"; } from "./diqinClient";
import { mysqlModelMap } from "../model/sqlModelBind"; import { mysqlModelMap } from "../model/sqlModelBind";
import Sequelize from "sequelize"; import Sequelize from "sequelize";
import { DEVICE_STATE } from "../config/businessEnum";
import moment from "moment"; import moment from "moment";
import XLSX from "xlsx"; import XLSX from "xlsx";
import path from "path"; import path from "path";
...@@ -229,28 +230,54 @@ export async function region() { ...@@ -229,28 +230,54 @@ export async function region() {
// 3. 逐房间检查并插入 // 3. 逐房间检查并插入
let insertCount = 0; let insertCount = 0;
let skipCount = 0; let skipCount = 0;
let swapCount = 0;
for (const room of rooms) { for (const room of rooms) {
// 校验是否已存在(通过 room_id // 校验是否已存在(通过 region_key
const existing = await regionModel.findOne({ where: { room_id: room.roomId } }); let existing = await regionModel.findOne({ where: { region_key: room.roomId } });
if (existing) { if (existing) {
skipCount++; skipCount++;
continue; continue;
} }
// 按名称查是否存在(处理跨平台同名情况,如迪勤先创建了同名区域)
existing = await regionModel.findOne({ where: { region_name: room.roomName } });
if (existing) {
// 同名区域已存在,判断 region_key 是否一致
if (existing.region_key !== room.roomId) {
// 不一致:旧key移入 region_ad,region_key 改为飞奕的 roomId
const oldKey = existing.region_key;
await regionModel.update(
{
region_key: room.roomId,
region_ad: oldKey,
},
{ where: { id: existing.id } },
);
swapCount++;
console.log(`[区域集成] region_key 交换: ${room.roomName} (${oldKey}${room.roomId})`);
} else {
skipCount++;
}
continue;
}
// 插入新区域记录 // 插入新区域记录
// 楼栋/楼层层次信息存入 region_param JSON
await regionModel.create({ await regionModel.create({
room_id: room.roomId, region_key: room.roomId,
name: room.roomName, region_name: room.roomName,
floor_id: room.floorId, region_type: room.floorName,
type: room.floorName, // type 对应楼层名称 region_param: {
building_id: room.buildingId, floorId: room.floorId,
groups: room.buildingName, // groups 对应楼栋名称 buildingId: room.buildingId,
buildingName: room.buildingName,
},
sort_order: 0, sort_order: 0,
}); });
insertCount++; insertCount++;
} }
console.log(`[区域集成] 完成,新增 ${insertCount} 条,跳过 ${skipCount} 条已存在`); console.log(`[区域集成] 完成,新增 ${insertCount} 条,跳过 ${skipCount} 条已存在,交换 ${swapCount}`);
// 2. 一次性查询所有 region,后续两步共用内存数据,避免 DB 读写时序问题 // 2. 一次性查询所有 region,后续两步共用内存数据,避免 DB 读写时序问题
const allRegions = await regionModel.findAll({ raw: true }) as any[]; const allRegions = await regionModel.findAll({ raw: true }) as any[];
...@@ -282,11 +309,17 @@ async function syncRegionAddressByAcDevices(regionModel: any, allRegions: any[]) ...@@ -282,11 +309,17 @@ async function syncRegionAddressByAcDevices(regionModel: any, allRegions: any[])
const rows = await fetchAllPages((page, limit) => getIndoorUnitList({ page, limit })); const rows = await fetchAllPages((page, limit) => getIndoorUnitList({ page, limit }));
console.log(`[同步region.address] 共获取 ${rows.length} 条空调内机`); console.log(`[同步region.address] 共获取 ${rows.length} 条空调内机`);
// 构建 roomId → region 映射(使用外部传入的 allRegions) // 构建 region_key → region 映射(地址存储在 region_param.address 中)
const roomIdToRegion = new Map<string, any>(); const keyToRegion = new Map<string, any>();
// 辅助函数:从 region_param 中读取 address
const getRegionAddress = (r: any): string => {
if (!r.region_param) return '';
const param = typeof r.region_param === 'string' ? JSON.parse(r.region_param) : r.region_param;
return param?.address || '';
};
for (const r of allRegions) { for (const r of allRegions) {
if (r.room_id) { if (r.region_key) {
roomIdToRegion.set(r.room_id, r); keyToRegion.set(r.region_key, r);
} }
} }
...@@ -294,7 +327,7 @@ async function syncRegionAddressByAcDevices(regionModel: any, allRegions: any[]) ...@@ -294,7 +327,7 @@ async function syncRegionAddressByAcDevices(regionModel: any, allRegions: any[])
for (const item of rows) { for (const item of rows) {
if (!item.roomId || !item.indoorUnitAddressFull) continue; if (!item.roomId || !item.indoorUnitAddressFull) continue;
const region = roomIdToRegion.get(item.roomId); const region = keyToRegion.get(item.roomId);
if (!region) continue; if (!region) continue;
// 用内机四段地址前三位作为外机地址 // 用内机四段地址前三位作为外机地址
...@@ -302,14 +335,18 @@ async function syncRegionAddressByAcDevices(regionModel: any, allRegions: any[]) ...@@ -302,14 +335,18 @@ async function syncRegionAddressByAcDevices(regionModel: any, allRegions: any[])
if (!outerAddr) continue; if (!outerAddr) continue;
// address 为空或值不同时才更新 // address 为空或值不同时才更新
if (region.address === outerAddr) continue; if (getRegionAddress(region) === outerAddr) continue;
// 更新 region_param,写入 address
const param = typeof region.region_param === 'string'
? JSON.parse(region.region_param) : (region.region_param || {});
param.address = outerAddr;
await regionModel.update( await regionModel.update(
{ address: outerAddr }, { region_param: param },
{ where: { id: region.id } }, { where: { id: region.id } },
); );
// 同步更新内存中的值,后续 syncMeterDeviceRegionKey 可直接使用 // 同步更新内存中的值,后续 syncMeterDeviceRegionKey 可直接使用
region.address = outerAddr; region.region_param = param;
updateCount++; updateCount++;
} }
console.log(`[同步region.address] 完成,更新 ${updateCount} 条`); console.log(`[同步region.address] 完成,更新 ${updateCount} 条`);
...@@ -340,18 +377,25 @@ async function syncMeterDeviceRegionKey(allRegions: any[]) { ...@@ -340,18 +377,25 @@ async function syncMeterDeviceRegionKey(allRegions: any[]) {
return; return;
} }
// 2. 使用外部传入的 allRegions 构建 address → regionKey 映射(复用已更新 address 的内存数据) // 2. 使用外部传入的 allRegions 构建 address → regionKey 映射
// address 存储在 region_param JSON 中
const getRegionAddress = (r: any): string => {
if (!r.region_param) return '';
const param = typeof r.region_param === 'string' ? JSON.parse(r.region_param) : r.region_param;
return param?.address || '';
};
const addrToRegionKey = new Map<string, string>(); const addrToRegionKey = new Map<string, string>();
for (const r of allRegions) { for (const r of allRegions) {
if (r.address && r.region_key) { const addr = getRegionAddress(r);
addrToRegionKey.set(r.address, r.region_key); if (addr && r.region_key) {
addrToRegionKey.set(addr, r.region_key);
} }
} }
console.log(`[同步电表region_key] region.address 映射共 ${addrToRegionKey.size} 条`); console.log(`[同步电表region_key] region_param.address 映射共 ${addrToRegionKey.size} 条`);
// 3. 查询所有电表设备 // 3. 查询所有电表设备
const meterDevices = await deviceModel.findAll({ const meterDevices = await deviceModel.findAll({
where: { device_type: '电能监测' }, where: { device_type: '电' },
raw: true, raw: true,
}); });
console.log(`[同步电表region_key] 共 ${meterDevices.length} 个电表设备`); console.log(`[同步电表region_key] 共 ${meterDevices.length} 个电表设备`);
...@@ -426,7 +470,7 @@ export async function device() { ...@@ -426,7 +470,7 @@ export async function device() {
/** /**
* 空调设备集成 * 空调设备集成
* device_id = indoorUnitAddressFull * device_id = indoorUnitAddressFull
* device_ad = indoorUnitId * 额外参数(indoorUnitId 等)存储在 device_param JSON 中
*/ */
async function integrateAcDevices() { async function integrateAcDevices() {
console.log('[空调设备集成] 开始...'); console.log('[空调设备集成] 开始...');
...@@ -438,7 +482,7 @@ async function integrateAcDevices() { ...@@ -438,7 +482,7 @@ async function integrateAcDevices() {
// 查询未处理的故障设备信息 // 查询未处理的故障设备信息
const faultDevices = await deviceModel.findAll({ const faultDevices = await deviceModel.findAll({
where: { device_type: '空调', device_state: 2 }, where: { device_type: '空调', device_state: DEVICE_STATE.故障 },
raw: true, raw: true,
}); });
console.log(`[空调设备集成] 共 ${faultDevices.length} 个故障设备`); console.log(`[空调设备集成] 共 ${faultDevices.length} 个故障设备`);
...@@ -485,11 +529,11 @@ async function integrateAcDevices() { ...@@ -485,11 +529,11 @@ async function integrateAcDevices() {
if (item.roomId) { if (item.roomId) {
const regionModel = mysqlModelMap['region']; const regionModel = mysqlModelMap['region'];
if (regionModel) { if (regionModel) {
const region = await regionModel.findOne({ where: { room_id: item.roomId } }); const region = await regionModel.findOne({ where: { region_key: item.roomId } });
if (region) { if (region) {
regionKey = region.region_key; regionKey = region.region_key;
// 如果区域地址包含新风,则空调视为新风机 // 如果区域名称包含新风,则空调视为新风机
if (region.name && region.name.includes('新风')) { if (region.region_name && region.region_name.includes('新风')) {
deviceType = '新风'; deviceType = '新风';
deviceName = '新风机'; deviceName = '新风机';
} }
...@@ -500,11 +544,10 @@ async function integrateAcDevices() { ...@@ -500,11 +544,10 @@ async function integrateAcDevices() {
// 插入新设备 // 插入新设备
await deviceModel.create({ await deviceModel.create({
device_id: deviceId, device_id: deviceId,
device_ad: item.indoorUnitId || '',
region_key: regionKey, region_key: regionKey,
device_type: deviceType, device_type: deviceType,
device_name: `${deviceName}-${deviceId}`, device_name: `${deviceName}-${deviceId}`,
control_params: AC_CONTROL_PARAMS, device_param: { ad: item.indoorUnitId || '', ...AC_CONTROL_PARAMS },
device_state: item.state ?? 0 device_state: item.state ?? 0
}); });
insertCount++; insertCount++;
...@@ -515,8 +558,8 @@ async function integrateAcDevices() { ...@@ -515,8 +558,8 @@ async function integrateAcDevices() {
if (faultModel) { if (faultModel) {
const now = new Date(); const now = new Date();
await faultModel.update( await faultModel.update(
{ status: 2, resolved_time: now, updated_at: now }, { fault_status: 2, handle_time: now, updated_at: now },
{ where: { device_id: { [Op.in]: deviceNomalIds }, status: { [Op.ne]: 2 } } }, { where: { device_id: { [Op.in]: deviceNomalIds }, fault_status: { [Op.ne]: 2 } } },
); );
console.log(`[空调设备集成] 批量解决 ${deviceNomalIds.length} 台设备故障`); console.log(`[空调设备集成] 批量解决 ${deviceNomalIds.length} 台设备故障`);
} }
...@@ -528,7 +571,7 @@ async function integrateAcDevices() { ...@@ -528,7 +571,7 @@ async function integrateAcDevices() {
/** /**
* 电表设备集成 * 电表设备集成
* device_id = gatewayCode * device_id = gatewayCode
* device_ad = meterId * 额外参数(meterId 等)存储在 device_param JSON 中
*/ */
async function integrateMeterDevices() { async function integrateMeterDevices() {
console.log('[电表设备集成] 开始...'); console.log('[电表设备集成] 开始...');
...@@ -561,11 +604,10 @@ async function integrateMeterDevices() { ...@@ -561,11 +604,10 @@ async function integrateMeterDevices() {
// 插入新设备 // 插入新设备
await deviceModel.create({ await deviceModel.create({
device_id: deviceId, device_id: deviceId,
device_ad: item.meterId || '',
region_key: '', // 电表暂无区域关联,后续由 syncMeterDeviceRegionKey 回填 region_key: '', // 电表暂无区域关联,后续由 syncMeterDeviceRegionKey 回填
device_type: '电能监测', device_type: '电',
device_name: `电表-${item.meterComId || item.meterId || ''}`, device_name: `电表-${item.meterComId || item.meterId || ''}`,
control_params: METER_CONTROL_PARAMS, device_param: { meterId: item.meterId || '', ...METER_CONTROL_PARAMS },
}); });
insertCount++; insertCount++;
} }
...@@ -670,7 +712,7 @@ async function integrateAcDeviceData() { ...@@ -670,7 +712,7 @@ async function integrateAcDeviceData() {
}; };
toInsert.push({ toInsert.push({
device_id: deviceId, device_id: deviceId,
data: data, device_data: data,
received_time: new Date(), received_time: new Date(),
device_time: itemTime, device_time: itemTime,
}); });
...@@ -749,7 +791,7 @@ async function integrateMeterDeviceData() { ...@@ -749,7 +791,7 @@ async function integrateMeterDeviceData() {
}; };
toInsert.push({ toInsert.push({
device_id: deviceId, device_id: deviceId,
data: data, device_data: data,
received_time: new Date(), received_time: new Date(),
device_time: itemTime, device_time: itemTime,
}); });
...@@ -873,11 +915,11 @@ export async function alertWorkData() { ...@@ -873,11 +915,11 @@ export async function alertWorkData() {
device_id: deviceId, device_id: deviceId,
fault_type: getFaultTypeName(r.alarmType), fault_type: getFaultTypeName(r.alarmType),
fault_code: r.alarmCode || '', fault_code: r.alarmCode || '',
fault_description: errorMsg, fault_info: errorMsg,
fault_origin_id: r.indoorUnitAlarmId, fault_origin_id: r.indoorUnitAlarmId,
occurred_time: r.deviceTime ? new Date(r.deviceTime) : new Date(), fault_time: r.deviceTime ? new Date(r.deviceTime) : new Date(),
level: 2, fault_level: 2,
status: 0, fault_status: 0,
}); });
} }
...@@ -913,7 +955,7 @@ export async function feiyiRegionDeviceRel() { ...@@ -913,7 +955,7 @@ export async function feiyiRegionDeviceRel() {
// 1. 查询所有已绑定 region_key 的飞奕设备(空调 + 电表) // 1. 查询所有已绑定 region_key 的飞奕设备(空调 + 电表)
const devices = await deviceModel.findAll({ const devices = await deviceModel.findAll({
where: { where: {
device_type: ['空调', '新风', '电能监测'], device_type: ['空调', '新风', '电'],
region_key: { [Op.ne]: null }, region_key: { [Op.ne]: null },
}, },
attributes: ['device_id', 'region_key', 'device_type'], attributes: ['device_id', 'region_key', 'device_type'],
...@@ -993,36 +1035,98 @@ export async function diqinRegion() { ...@@ -993,36 +1035,98 @@ export async function diqinRegion() {
} }
console.log(`[迪勤区域集成] 共获取 ${locations.length} 个场所`); console.log(`[迪勤区域集成] 共获取 ${locations.length} 个场所`);
// 2. 逐一 upsert // 2. 逐一 upsert 场所 + 其下监测点
let insertCount = 0; let locInsert = 0, locUpdate = 0;
let updateCount = 0; let stInsert = 0, stUpdate = 0;
for (const loc of locations) { for (const loc of locations) {
const regionKey = loc.id; const regionKey = loc.id;
if (!regionKey) continue; if (!regionKey) continue;
const existing = await regionModel.findOne({ where: { region_key: regionKey } }); // 2.1 upsert 场所本身
let existing = await regionModel.findOne({ where: { region_key: regionKey } });
if (existing) { if (existing) {
// 更新名称(如有变化) if (existing.region_name !== loc.name || existing.region_type !== '场所') {
if (existing.region_name !== loc.name) {
await regionModel.update( await regionModel.update(
{ region_name: loc.name, updated_at: new Date() }, { region_name: loc.name, region_type: '场所', updated_at: new Date() },
{ where: { id: existing.id } }, { where: { id: existing.id } },
); );
updateCount++; locUpdate++;
} }
} else { } else {
await regionModel.create({ // 按名称查是否存在同名区域(飞奕或其他平台已创建)
region_key: regionKey, existing = await regionModel.findOne({ where: { region_name: loc.name } });
region_name: loc.name, if (existing) {
parent_id: null, // 同名区域已存在,将迪勤内部ID写入 region_ad
parent_key: null, if (!existing.region_ad || existing.region_ad !== regionKey) {
region_type: null, await regionModel.update(
sort_order: 0, { region_ad: regionKey, updated_at: new Date() },
}); { where: { id: existing.id } },
insertCount++; );
locUpdate++;
console.log(`[迪勤区域集成] 同名场所补充 region_ad: ${loc.name}${regionKey}`);
}
} else {
await regionModel.create({
region_key: regionKey,
region_name: loc.name,
parent_id: null,
parent_key: null,
region_type: '场所',
sort_order: 0,
});
locInsert++;
}
}
// 2.2 同步该场所下的监测点(作为子区域)
try {
const detail = await getLocationDetail(loc.id);
const stations = detail?.stations || [];
for (const st of stations) {
if (!st.id || !st.name) continue;
let stExisting = await regionModel.findOne({ where: { region_key: st.id } });
if (stExisting) {
if (stExisting.region_name !== st.name
|| stExisting.parent_key !== regionKey
|| stExisting.region_type !== '监测点') {
await regionModel.update(
{ region_name: st.name, parent_key: regionKey, region_type: '监测点', updated_at: new Date() },
{ where: { id: stExisting.id } },
);
stUpdate++;
}
} else {
// 按名称查是否存在同名区域(飞奕或其他平台已创建)
stExisting = await regionModel.findOne({ where: { region_name: st.name } });
if (stExisting) {
// 同名区域已存在,将迪勤内部ID写入 region_ad
if (!stExisting.region_ad || stExisting.region_ad !== st.id) {
await regionModel.update(
{ region_ad: st.id, updated_at: new Date() },
{ where: { id: stExisting.id } },
);
stUpdate++;
console.log(`[迪勤区域集成] 同名监测点补充 region_ad: ${st.name}${st.id}`);
}
} else {
await regionModel.create({
region_key: st.id,
region_name: st.name,
parent_id: null,
parent_key: regionKey,
region_type: '监测点',
sort_order: 0,
});
stInsert++;
}
}
}
} catch (err: any) {
console.warn(`[迪勤区域集成] 获取场所 ${loc.name} 监测点失败:`, err.message);
} }
} }
console.log(`[迪勤区域集成] 完成,新增 ${insertCount} 条,更新 ${updateCount} 条`);
console.log(`[迪勤区域集成] 完成:场所新增${locInsert}/更新${locUpdate},监测点新增${stInsert}/更新${stUpdate}`);
} catch (err) { } catch (err) {
console.error('[迪勤区域集成] 执行异常:', err); console.error('[迪勤区域集成] 执行异常:', err);
} }
...@@ -1089,7 +1193,7 @@ export async function diqinDevice() { ...@@ -1089,7 +1193,7 @@ export async function diqinDevice() {
region_key: null, region_key: null,
device_type: deviceType, device_type: deviceType,
device_name: deviceName, device_name: deviceName,
device_state: 1, // 默认在线 device_state: DEVICE_STATE.在线, // 默认在线
}); });
insertCount++; insertCount++;
} }
...@@ -1126,28 +1230,62 @@ export async function diqinRegionDeviceRel() { ...@@ -1126,28 +1230,62 @@ export async function diqinRegionDeviceRel() {
} }
console.log(`[迪勤区域设备关联] IEQ 设备共 ${ieqSources.length} 个`); console.log(`[迪勤区域设备关联] IEQ 设备共 ${ieqSources.length} 个`);
// 2. 获取所有场所及其监测点 // 2. 获取所有场所及其监测点,构建两级映射
// 构建 stationName → locationId 映射 // stationRegionKeyMap: stationName → stationId(监测点)
// locationRegionKeyMap: locationId → location region_key(场所,兜底用)
const locations = await getLocations(); const locations = await getLocations();
if (!locations || !Array.isArray(locations)) return; if (!locations || !Array.isArray(locations)) return;
const stationLocMap = new Map<string, string>(); // stationName → locationId const stationRegionKeyMap = new Map<string, string>(); // stationName → stationId
const locationRegionKeyMap = new Map<string, string>(); // locationId → location region_key(场所id)
for (const loc of locations) { for (const loc of locations) {
locationRegionKeyMap.set(loc.id, loc.id);
try { try {
const detail = await getLocationDetail(loc.id); const detail = await getLocationDetail(loc.id);
const stations = detail?.stations || []; const stations = detail?.stations || [];
for (const st of stations) { for (const st of stations) {
if (st.name) { if (st.name && st.id) {
stationLocMap.set(st.name, loc.id); stationRegionKeyMap.set(st.name, st.id);
} }
} }
} catch (err) { } catch (err: any) {
console.warn(`[迪勤区域设备关联] 获取场所 ${loc.name} 明细失败:`, err.message); console.warn(`[迪勤区域设备关联] 获取场所 ${loc.name} 明细失败:`, err.message);
} }
} }
console.log(`[迪勤区域设备关联] stationLocMap 共 ${stationLocMap.size} 条`); console.log(`[迪勤区域设备关联] station 映射 ${stationRegionKeyMap.size} 条, location 映射 ${locationRegionKeyMap.size} 条`);
// 2.1 收集所有迪勤内部ID,查询 region 表获取实际 region_key
// (处理跨平台同名区域:region_key 可能已被飞奕替换,region_ad 存储了迪勤ID)
const allDiqinIds = [...new Set([
...stationRegionKeyMap.values(),
...locationRegionKeyMap.keys(),
])];
const regionModel = mysqlModelMap['region'];
const diqinIdToRegionKey = new Map<string, string>();
if (regionModel && allDiqinIds.length > 0) {
const regionRecords = await regionModel.findAll({
where: {
[Op.or]: [
{ region_key: { [Op.in]: allDiqinIds } },
{ region_ad: { [Op.in]: allDiqinIds } },
],
},
raw: true,
}) as any[];
for (const r of regionRecords) {
// region_key 匹配迪勤ID(未被飞奕交换的场景)
if (r.region_key && allDiqinIds.includes(r.region_key)) {
diqinIdToRegionKey.set(r.region_key, r.region_key);
}
// region_ad 匹配迪勤ID(被飞奕交换的场景,用实际的 region_key)
if (r.region_ad && allDiqinIds.includes(r.region_ad)) {
diqinIdToRegionKey.set(r.region_ad, r.region_key);
}
}
console.log(`[迪勤区域设备关联] 解析到 ${diqinIdToRegionKey.size} 个迪勤ID→实际region_key映射`);
}
// 3. 匹配:station.name 包含 data_source.name → 建立关联 // 3. 匹配:优先子级 station → 找不到再回退父级 location
const newRels: Array<{ region_key: string; device_id: string }> = []; const newRels: Array<{ region_key: string; device_id: string }> = [];
const ieqDeviceIds: string[] = []; const ieqDeviceIds: string[] = [];
...@@ -1156,11 +1294,30 @@ export async function diqinRegionDeviceRel() { ...@@ -1156,11 +1294,30 @@ export async function diqinRegionDeviceRel() {
if (!dsName) continue; if (!dsName) continue;
ieqDeviceIds.push(ds.id); ieqDeviceIds.push(ds.id);
// 遍历所有 station,找包含 device name 的 let matched = false;
for (const [stationName, locationId] of stationLocMap) {
// 3.1 先尝试匹配监测点(子级区域)
for (const [stationName, stationRegionKey] of stationRegionKeyMap) {
if (stationName.includes(dsName)) { if (stationName.includes(dsName)) {
newRels.push({ region_key: locationId, device_id: ds.id }); const resolvedKey = diqinIdToRegionKey.get(stationRegionKey) || stationRegionKey;
break; // 一个设备只匹配第一个命中的 station newRels.push({ region_key: resolvedKey, device_id: ds.id });
matched = true;
break;
}
}
// 3.2 未匹配到子级监测点 → 回退匹配父级场所
if (!matched) {
for (const [locationId, locationRegionKey] of locationRegionKeyMap) {
// 用 locationId 代表的场所名(从 locations 列表中获取)
const loc = locations.find((l: any) => l.id === locationId);
const locName = loc?.name || '';
if (locName.toLowerCase().includes(dsName.toLowerCase())) {
const resolvedKey = diqinIdToRegionKey.get(locationRegionKey) || locationRegionKey;
newRels.push({ region_key: resolvedKey, device_id: ds.id });
matched = true;
break;
}
} }
} }
} }
...@@ -1346,6 +1503,85 @@ export async function diqinSyncAll() { ...@@ -1346,6 +1503,85 @@ export async function diqinSyncAll() {
console.log('[迪勤全量同步] 完成'); console.log('[迪勤全量同步] 完成');
} }
// ==================== device.region_key 数据迁移 ====================
/**
* 数据迁移:将 device.region_key 从旧值(region.id 数字)映射为 region.region_key(字符串)
* 覆盖所有设备类型(空调、电表、IEQ传感器)
* 幂等设计:已为正确 region_key 值的记录跳过,可安全重复执行
*/
export async function migrateDeviceRegionKey() {
console.log('[region_key迁移] 开始...');
try {
const deviceModel = mysqlModelMap['device'];
const regionModel = mysqlModelMap['region'];
if (!deviceModel || !regionModel) {
console.error('[region_key迁移] 模型未初始化,跳过');
return;
}
// 1. 构建 id → region_key 映射
const allRegions = await regionModel.findAll({
attributes: ['id', 'region_key'],
raw: true,
}) as any[];
const idToRegionKey = new Map<number, string>();
for (const r of allRegions) {
if (r.region_key) {
idToRegionKey.set(r.id, String(r.region_key));
}
}
console.log(`[region_key迁移] region 表共 ${idToRegionKey.size} 条映射`);
// 2. 查询所有设备
const allDevices = await deviceModel.findAll({
attributes: ['id', 'device_id', 'region_key', 'device_type'],
raw: true,
}) as any[];
console.log(`[region_key迁移] device 表共 ${allDevices.length} 条`);
// 3. 逐条判定并更新
let updateCount = 0;
let skipCorrect = 0;
let skipNoRegion = 0;
for (const d of allDevices) {
const currentKey = d.region_key;
// 空值/无关联 → 跳过(后续由各同步函数回填)
if (!currentKey || currentKey === '0' || currentKey === '') {
skipNoRegion++;
continue;
}
const currentKeyStr = String(currentKey);
// 如果当前值包含字母(如 "B1-F1-01"),说明已是正确格式
if (/[a-zA-Z]/.test(currentKeyStr)) {
skipCorrect++;
continue;
}
// 纯数字字符串(如 "1", "2"),按 region.id 映射为 region_key
const numericId = Number(currentKeyStr);
const targetKey = idToRegionKey.get(numericId);
if (!targetKey) {
skipNoRegion++;
continue; // 映射不到,跳过
}
await deviceModel.update(
{ region_key: targetKey, updated_at: new Date() },
{ where: { id: d.id } },
);
updateCount++;
}
console.log(`[region_key迁移] 完成:已迁移 ${updateCount} 条,已正确 ${skipCorrect} 条,无关联 ${skipNoRegion} 条`);
} catch (err) {
console.error('[region_key迁移] 执行异常:', err);
}
}
// ==================== 故障状态自动更新 ==================== // ==================== 故障状态自动更新 ====================
/** /**
...@@ -1366,8 +1602,8 @@ export async function processFaultStatus() { ...@@ -1366,8 +1602,8 @@ export async function processFaultStatus() {
try { try {
// ① 一次查出所有未处理/处理中的故障 // ① 一次查出所有未处理/处理中的故障
const activeFaults = await faultModel.findAll({ const activeFaults = await faultModel.findAll({
where: { status: { [Op.in]: [0, 1] } }, where: { fault_status: { [Op.in]: [0, 1] } },
attributes: ['id', 'device_id', 'occurred_time'], attributes: ['id', 'device_id', 'fault_time'],
raw: true, raw: true,
}); });
console.log(`[故障状态更新] 共 ${activeFaults.length} 条活跃故障`); console.log(`[故障状态更新] 共 ${activeFaults.length} 条活跃故障`);
...@@ -1385,7 +1621,7 @@ export async function processFaultStatus() { ...@@ -1385,7 +1621,7 @@ export async function processFaultStatus() {
const deviceFaultTimeMap = new Map<string, Date>(); const deviceFaultTimeMap = new Map<string, Date>();
for (const f of activeFaults as any[]) { for (const f of activeFaults as any[]) {
deviceSet.add(f.device_id); deviceSet.add(f.device_id);
const t = f.occurred_time ? new Date(f.occurred_time) : null; const t = f.fault_time ? new Date(f.fault_time) : null;
if (!t) continue; if (!t) continue;
// 全局最早时间(SQL 查询范围) // 全局最早时间(SQL 查询范围)
if (!globalMinOccurredTime || t < globalMinOccurredTime) { if (!globalMinOccurredTime || t < globalMinOccurredTime) {
...@@ -1406,7 +1642,7 @@ export async function processFaultStatus() { ...@@ -1406,7 +1642,7 @@ export async function processFaultStatus() {
device_id: { [Op.in]: deviceIds }, device_id: { [Op.in]: deviceIds },
...(globalMinOccurredTime ? { device_time: { [Op.gte]: globalMinOccurredTime } } : {}), ...(globalMinOccurredTime ? { device_time: { [Op.gte]: globalMinOccurredTime } } : {}),
}, },
attributes: ['device_id', 'data', 'device_time'], attributes: ['device_id', 'device_data', 'device_time'],
order: [['device_id', 'ASC'], ['device_time', 'ASC']], order: [['device_id', 'ASC'], ['device_time', 'ASC']],
raw: true, raw: true,
}); });
...@@ -1418,7 +1654,7 @@ export async function processFaultStatus() { ...@@ -1418,7 +1654,7 @@ export async function processFaultStatus() {
const powerOnTimesMap = new Map<string, Date[]>(); const powerOnTimesMap = new Map<string, Date[]>();
for (const r of deviceDataRecords as any[]) { for (const r of deviceDataRecords as any[]) {
let power: string | undefined; let power: string | undefined;
const rawData = r.data; const rawData = r.device_data;
if (typeof rawData === 'string') { if (typeof rawData === 'string') {
try { power = JSON.parse(rawData).power; } catch { continue; } try { power = JSON.parse(rawData).power; } catch { continue; }
} else if (rawData && typeof rawData === 'object') { } else if (rawData && typeof rawData === 'object') {
...@@ -1442,18 +1678,18 @@ export async function processFaultStatus() { ...@@ -1442,18 +1678,18 @@ export async function processFaultStatus() {
// ⑤ 逐条故障比对:查找该故障发生时间之后最早的开机记录 // ⑤ 逐条故障比对:查找该故障发生时间之后最早的开机记录
// 关键修复:不再拿"全量最早开机时间"与故障时间对比, // 关键修复:不再拿"全量最早开机时间"与故障时间对比,
// 而是在该设备故障后出现的开机记录中找第一条,确保 isAfter 判定正确 // 而是在该设备故障后出现的开机记录中找第一条,确保 isAfter 判定正确
const toUpdate: { id: number; resolved_time: Date }[] = []; const toUpdate: { id: number; handle_time: Date }[] = [];
for (const fault of activeFaults as any[]) { for (const fault of activeFaults as any[]) {
const powerOnTimes = powerOnTimesMap.get(fault.device_id); const powerOnTimes = powerOnTimesMap.get(fault.device_id);
if (!powerOnTimes || powerOnTimes.length === 0) continue; if (!powerOnTimes || powerOnTimes.length === 0) continue;
const occurredTime = fault.occurred_time ? new Date(fault.occurred_time) : null; const occurredTime = fault.fault_time ? new Date(fault.fault_time) : null;
if (!occurredTime) continue; if (!occurredTime) continue;
// 数据已按 device_time ASC 排序,找第一条在故障时间之后的记录 // 数据已按 device_time ASC 排序,找第一条在故障时间之后的记录
const resolvedTime = powerOnTimes.find(dt => dt > occurredTime); const resolvedTime = powerOnTimes.find(dt => dt > occurredTime);
if (resolvedTime) { if (resolvedTime) {
toUpdate.push({ id: fault.id, resolved_time: resolvedTime }); toUpdate.push({ id: fault.id, handle_time: resolvedTime });
} }
} }
...@@ -1465,7 +1701,7 @@ export async function processFaultStatus() { ...@@ -1465,7 +1701,7 @@ export async function processFaultStatus() {
// ⑥ 逐条更新 status=2 // ⑥ 逐条更新 status=2
for (const item of toUpdate) { for (const item of toUpdate) {
await faultModel.update( await faultModel.update(
{ status: 2, resolved_time: item.resolved_time, updated_at: new Date() }, { fault_status: 2, handle_time: item.handle_time, updated_at: new Date() },
{ where: { id: item.id } }, { where: { id: item.id } },
); );
} }
......
...@@ -21,8 +21,8 @@ function getFeiYiConfig() { ...@@ -21,8 +21,8 @@ function getFeiYiConfig() {
const config = systemConfig.feiyi; const config = systemConfig.feiyi;
return { return {
baseUrl: (config && config.baseUrl) || process.env.FEIYI_BASE_URL || 'https://m.achelp.cn/open', baseUrl: (config && config.baseUrl) || process.env.FEIYI_BASE_URL || 'https://m.achelp.cn/open',
clientId: (config && config.appKey) || process.env.FEIYI_CLIENT_ID || '419636996764598272', clientId: (config && config.appKey) || process.env.FEIYI_CLIENT_ID || '444940462151958528',
secretKey: (config && config.secretKey) || process.env.FEIYI_SECRET_KEY || '02fc75446b4544b3a02444a2b5f9be2b', secretKey: (config && config.secretKey) || process.env.FEIYI_SECRET_KEY || 'd30353e191c94ed3a3b1c05345e284b1',
}; };
} }
......
...@@ -7,26 +7,31 @@ import * as path from 'path'; ...@@ -7,26 +7,31 @@ import * as path from 'path';
import { post } from '../util/request'; import { post } from '../util/request';
import { analysisXml } from '../util/myXML'; import { analysisXml } from '../util/myXML';
import { planaryArrayBecomeOfBlockData } from '../util/analysisExcel'; import { planaryArrayBecomeOfBlockData } from '../util/analysisExcel';
import { Op, Sequelize } from 'sequelize';
import { selectDataListByParam } from '../data/findData'; import { selectDataListByParam } from '../data/findData';
import { mysqlModelMap } from '../model/sqlModelBind';
import { TABLENAME } from '../config/dbEnum'; import { TABLENAME } from '../config/dbEnum';
import { formatTime } from '../util/dateUtils';
const xlsx = require('node-xlsx'); const xlsx = require('node-xlsx');
/** /**
* 区域位置 * 区域位置
* @param gatewayPage 网关页面 1:运行分析 2:智能监控
*
* @returns 区域位置数据 * @returns 区域位置数据
*/ */
export async function getRegionLocation(gatewayPage: number) { export async function getRegionLocation(gatewayPage: number, hour: number) {
// const result = [{"regionKey":"32","regionName":"三十二位","xz":1587,"yz":487,"qualityIndex":"优","gatewayPage":gatewayPage}]; // const result = [{"regionKey":"32","regionName":"三十二位","xz":1587,"yz":487,"qualityIndex":"优","gatewayPage":gatewayPage}];
// 读取区域坐标分布.xlsx文件中的表格数据,更新区域表中的区域参数字段 // 读取区域坐标分布.xlsx文件中的表格数据,更新区域表中的区域参数字段
const result = await readRegionAxis(gatewayPage); const result = await readRegionAxis(gatewayPage, hour);
return result; return result;
} }
/** /**
* 读取区域坐标 * 读取区域坐标
*/ */
export async function readRegionAxis(gatewayPage: number) { export async function readRegionAxis(gatewayPage: number, hour: number) {
let result = []; let result = [];
const filePath = path.join(__dirname.substring(0, __dirname.indexOf("out")), "res", "区域坐标分布.xlsx"); const filePath = path.join(__dirname.substring(0, __dirname.indexOf("out")), "res", "区域坐标分布.xlsx");
...@@ -38,24 +43,103 @@ export async function readRegionAxis(gatewayPage: number) { ...@@ -38,24 +43,103 @@ export async function readRegionAxis(gatewayPage: number) {
} }
if (gatewayPage === 1) { if (gatewayPage === 1) {
let analysisSheet = sheetMap["运行分析"]; let analysisSheet = sheetMap["运行分析"];
result = await readSheet(analysisSheet, gatewayPage); result = await readSheet(analysisSheet, gatewayPage, 0);
} else if (gatewayPage === 2) { } else if (gatewayPage === 2) {
let monitorSheet = sheetMap["智能监控"]; let monitorSheet = sheetMap["智能监控"];
result = await readSheet(monitorSheet, gatewayPage); result = await readSheet(monitorSheet, gatewayPage, hour);
} }
return result; return result;
} }
/** /**
* 根据 CO2 和 PM2.5 判断环境质量等级(取两指标中较差的等级)
* 优: CO2 < 1000ppm AND PM2.5 ≤ 45
* 良: CO2 ≤ 1000ppm AND PM2.5 ≤ 50
* 中: CO2 ≤ 1500ppm AND PM2.5 ≤ 75
* 差: 不满足以上任一
* hasCo2/hasPm25 为 false 时忽略该指标
*/
function judgeQuality(co2: number, pm25: number, hasCo2: boolean, hasPm25: boolean): string {
if (!hasCo2 && !hasPm25) return '--';
const co2Excellent = !hasCo2 || co2 < 1000;
const co2Good = !hasCo2 || co2 <= 1000;
const co2Normal = !hasCo2 || co2 <= 1500;
const pm25Excellent = !hasPm25 || pm25 <= 45;
const pm25Good = !hasPm25 || pm25 <= 50;
const pm25Normal = !hasPm25 || pm25 <= 75;
if (co2Excellent && pm25Excellent) return '优';
if (co2Good && pm25Good) return '良';
if (co2Normal && pm25Normal) return '中';
return '差';
}
/**
* 四指标分级判定(甲醛/O2/NH3/H2S),取最差级别。
* 供 IEQ-EC 异味传感器及后续其他场景复用。
*
* 甲醛 a31001: ≤0.08=1级, 0.08~0.15=2级, 0.15~0.3=3级, >0.3=4级 (GB/T 18883-2022)
* O2 a19001: 19.5~23.5=1级, 18~19.5=2级, <18=3级, >23.5=4级 (室内/作业场景安全分级)
* NH3 a21001: ≤0.20=1级, 0.20~0.5=2级, 0.5~1.0=3级, >1.0=4级 (GB/T 18883-2022)
* H2S a21028: ≤10=1级, 10~20=2级, 20~50=3级, >50=4级 (GBZ 2.1-2019)
*
* @returns '优' | '良' | '中' | '差' | '--'(全部无数据)
*/
function judgeFourIndicators(
hcho: number, o2: number, nh3: number, h2s: number,
hasHcho: boolean, hasO2: boolean, hasNh3: boolean, hasH2s: boolean
): string {
if (!hasHcho && !hasO2 && !hasNh3 && !hasH2s) return '--';
const delta: number[] = [];
// 1=优, 2=良, 3=中, 4=差
if (hasHcho) {
if (hcho <= 0.08) delta.push(1);
else if (hcho <= 0.15) delta.push(2);
else if (hcho <= 0.3) delta.push(3);
else delta.push(4);
}
if (hasO2) {
if (o2 >= 19.5 && o2 <= 23.5) delta.push(1);
else if (o2 >= 18 && o2 < 19.5) delta.push(2);
else if (o2 < 18) delta.push(3);
else delta.push(4);
}
if (hasNh3) {
if (nh3 <= 0.20) delta.push(1);
else if (nh3 <= 0.5) delta.push(2);
else if (nh3 <= 1.0) delta.push(3);
else delta.push(4);
}
if (hasH2s) {
if (h2s <= 10) delta.push(1);
else if (h2s <= 20) delta.push(2);
else if (h2s <= 50) delta.push(3);
else delta.push(4);
}
const worst = Math.max(...delta);
const levels = ['优', '良', '中', '差'];
return levels[worst - 1];
}
/**
* 动态查询:根据 regionName→regionKey 映射,从数据库查询各区域下的设备及最新功率状态 * 动态查询:根据 regionName→regionKey 映射,从数据库查询各区域下的设备及最新功率状态
* @returns 格式与 mock 数据一致: { regionName: [{deviceId, deviceType, power}, ...] } * 同时提取最新 co2/pm25 数据计算各区域环境质量
* @returns { deviceMap, qualityMap }
*/ */
async function buildDeviceMapDynamic(regionMap: { [regionName: string]: string }): Promise<{ [regionName: string]: any[] }> { async function buildDeviceMapDynamic(
regionMap: { [regionName: string]: string },
ieqTagMap: { [regionName: string]: string },
hour: number,
): Promise<{ deviceMap: { [regionName: string]: any[] }; qualityMap: { [regionName: string]: string } }> {
const deviceMap: { [regionName: string]: any[] } = {}; const deviceMap: { [regionName: string]: any[] } = {};
const qualityMap: { [regionName: string]: string } = {};
// 1. 收集所有 region_key // 1. 收集所有 region_key
const regionKeys = Object.values(regionMap) as string[]; const regionKeys = Object.values(regionMap) as string[];
if (regionKeys.length === 0) return deviceMap; if (regionKeys.length === 0) return { deviceMap, qualityMap };
// 2. 反向映射:regionKey → regionName // 2. 反向映射:regionKey → regionName
const regionKeyToName: { [key: string]: string } = {}; const regionKeyToName: { [key: string]: string } = {};
...@@ -71,28 +155,72 @@ async function buildDeviceMapDynamic(regionMap: { [regionName: string]: string } ...@@ -71,28 +155,72 @@ async function buildDeviceMapDynamic(regionMap: { [regionName: string]: string }
); );
const devices = (deviceResult.data || []) as any[]; const devices = (deviceResult.data || []) as any[];
// 4. 批量查询这些设备的最新一条 device_data,提取 power 状态 // 4. 批量查询这些设备的最新一条 device_data,提取 power、co2、pm25,并保留完整解析数据供虚拟传感器计算
const deviceIds = devices.map((d) => d.device_id).filter(Boolean); const deviceIds = devices.map((d) => d.device_id).filter(Boolean);
const powerMap = new Map<string, string>(); const powerMap = new Map<string, string>();
const deviceDataMap = new Map<string, any>(); // device_id → 最新 device_data 解析对象
// 按区域收集 co2 / pm25 值
const regionEnvMap: { [regionName: string]: { co2List: number[]; pm25List: number[] } } = {};
for (const name of Object.keys(regionMap)) {
regionEnvMap[name] = { co2List: [], pm25List: [] };
}
if (deviceIds.length > 0) { if (deviceIds.length > 0) {
const dataResult = await selectDataListByParam( // 计算时间截断点:当前时间减去 hour 小时(不截断整点)
TABLENAME.设备数据表, const cutoffTime = new Date(Date.now() - hour * 3600 * 1000);
{ const cutoffTimeStr = formatTime(cutoffTime);
device_id: { '%in%': deviceIds },
'%orderDesc%': 'device_time', // 查询1:GROUP BY 取每个设备的 device_time 最大值(利用索引,高效)
'%limit%': deviceIds.length, const tableModel = mysqlModelMap[TABLENAME.设备数据表];
}, const whereGroup: any = { device_id: { [Op.in]: deviceIds } };
['device_id', 'device_data'], if (hour > 0) {
); whereGroup.device_time = { [Op.lte]: cutoffTimeStr };
// 按 device_time 降序返回,去重取每个设备的第一条(最新) }
const seen = new Set<string>(); const latestRows = await tableModel.findAll({
(dataResult.data || []).forEach((r: any) => { attributes: [
if (!seen.has(r.device_id)) { 'device_id',
seen.add(r.device_id); [Sequelize.fn('MAX', Sequelize.col('device_time')), 'latest_time'],
],
where: whereGroup,
group: ['device_id'],
raw: true,
}) as any[];
// 查询2:按 (device_id, device_time) 精确等值查询完整数据
if (latestRows.length > 0) {
const orConditions = latestRows.map((row: any) => ({
device_id: row.device_id,
device_time: row.latest_time,
}));
const dataResult = await selectDataListByParam(
TABLENAME.设备数据表,
{ '%or%': orConditions },
['device_id', 'device_data'],
);
(dataResult.data || []).forEach((r: any) => {
const parsed = typeof r.device_data === 'string' ? JSON.parse(r.device_data) : r.device_data; const parsed = typeof r.device_data === 'string' ? JSON.parse(r.device_data) : r.device_data;
powerMap.set(r.device_id, parsed?.power || 'on'); powerMap.set(r.device_id, parsed?.power || 'on');
deviceDataMap.set(r.device_id, parsed);
// 提取环境数据
const rk = devices.find(d => d.device_id === r.device_id)?.region_key;
const rn = rk ? regionKeyToName[rk] : undefined;
if (rn && regionEnvMap[rn]) {
if (typeof parsed?.co2 === 'number') regionEnvMap[rn].co2List.push(parsed.co2);
if (typeof parsed?.pm25 === 'number') regionEnvMap[rn].pm25List.push(parsed.pm25);
}
});
}
// 时间窗口内无数据的设备:推 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 数据) // 5. 组装 deviceMap(按 regionName 分组,格式对齐 mock 数据)
...@@ -102,6 +230,7 @@ async function buildDeviceMapDynamic(regionMap: { [regionName: string]: string } ...@@ -102,6 +230,7 @@ async function buildDeviceMapDynamic(regionMap: { [regionName: string]: string }
if (!deviceMap[regionName]) { if (!deviceMap[regionName]) {
deviceMap[regionName] = []; deviceMap[regionName] = [];
} }
deviceMap[regionName].push({ deviceMap[regionName].push({
deviceId: device.device_id, deviceId: device.device_id,
deviceType: device.device_type, deviceType: device.device_type,
...@@ -109,61 +238,127 @@ async function buildDeviceMapDynamic(regionMap: { [regionName: string]: string } ...@@ -109,61 +238,127 @@ async function buildDeviceMapDynamic(regionMap: { [regionName: string]: string }
}); });
} }
return deviceMap; // 5.5 根据 IEQ 标签拆分虚拟传感器(ME=人在, EC=异味)
for (const [regionName, ieqTag] of Object.entries(ieqTagMap)) {
if (!ieqTag || ieqTag === 'SD') continue;
const tags = ieqTag.split(',').map((t: string) => t.trim());
if (!deviceMap[regionName]) continue;
// 找到该区域下的 IEQ 设备
const ieqDevice = deviceMap[regionName].find((d: any) => d.deviceType === 'IEQ传感器');
if (!ieqDevice) continue;
const ieqData = deviceDataMap.get(ieqDevice.deviceId);
// ME: 人在传感器
if (tags.includes('ME')) {
const mePower = ieqData && typeof ieqData.a09527 === 'number' && ieqData.a09527 > 0 ? 'on' : 'off';
deviceMap[regionName].push({
deviceId: ieqDevice.deviceId,
deviceType: '人在传感器',
power: mePower,
});
}
// EC: 异味传感器
if (tags.includes('EC')) {
const hasHcho = ieqData && typeof ieqData.a31001 === 'number';
const hasO2 = ieqData && typeof ieqData.a19001 === 'number';
const hasNh3 = ieqData && typeof ieqData.a21001 === 'number';
const hasH2s = ieqData && typeof ieqData.a21028 === 'number';
const ecLevel = judgeFourIndicators(
hasHcho ? ieqData.a31001 : 0,
hasO2 ? ieqData.a19001 : 0,
hasNh3 ? ieqData.a21001 : 0,
hasH2s ? ieqData.a21028 : 0,
hasHcho, hasO2, hasNh3, hasH2s,
);
deviceMap[regionName].push({
deviceId: ieqDevice.deviceId,
deviceType: '异味传感器',
power: ecLevel==='优' ? 'off' : 'on',
});
}
}
// 6. 根据各区域环境数据计算空气质量等级
for (const [name, entry] of Object.entries(regionEnvMap)) {
const avgCo2 = entry.co2List.length > 0
? entry.co2List.reduce((a, b) => a + b, 0) / entry.co2List.length : 0;
const avgPm25 = entry.pm25List.length > 0
? entry.pm25List.reduce((a, b) => a + b, 0) / entry.pm25List.length : 0;
qualityMap[name] = judgeQuality(avgCo2, avgPm25, entry.co2List.length > 0, entry.pm25List.length > 0);
}
return { deviceMap, qualityMap };
} }
async function readSheet(sheet: any[][], gatewayPage: number) { async function readSheet(sheet: any[][], gatewayPage: number, hour: number) {
// 取出所有区域名称,查询区域表获取区域数据 // 取出所有区域名称,查询区域表获取区域数据
let regionList = await selectDataListByParam(TABLENAME.区域表, {}); let regionList = await selectDataListByParam(TABLENAME.区域表, {});
// let regionMap = { // let regionMap = {
// "会议室": "1001", // "会议室": "1001",
// "公共办公区": "1002", // "大办公室": "1002",
// "办公室": "2001", // "办公室": "2001",
// "仓库": "2002", // "仓库": "2002",
// "CEO办公室": "2003", // "CEO办公室": "2003",
// "传感器生产间": "3001", // "传感器测试车间": "3001",
// "前台": "0001", // "前台休息区": "0001",
// "卫生间": "3000" // "卫生间": "3000"
// } // }
let regionMap:any = {}; let regionMap:any = {};
regionList.data.map((bean)=>{ regionList.data.map((bean)=>{
regionMap[bean.regionName] = bean.regionKey regionMap[bean.region_name] = bean.region_key
}); });
// 根据区域信息查询所有环境设备,通过环境设备查询最新环境指标 // 根据区域信息查询所有环境设备,通过环境设备查询最新环境指标
// ====== MOCK 数据(注释保留作为格式参考) ====== // ====== MOCK 数据(注释保留作为格式参考) ======
// let deviceMap = { // let deviceMap = {
// "会议室": [ // "会议室": [
// { "deviceId": "AC1", "deviceType": "空调", "power": "off" }, // { "deviceId": "AC1", "deviceType": "空调", "power": "off" },
// { "deviceId": "Z1", "deviceType": "照明", "power": "on" } // { "deviceId": "Z1", "deviceType": "照明", "power": "on" }
// ], // ],
// "公共办公区": [ // "大办公室": [
// { "deviceId": "AC2", "deviceType": "空调", "power": "on" }, // { "deviceId": "AC2", "deviceType": "空调", "power": "on" },
// { "deviceId": "Z12", "deviceType": "照明", "power": "on" }, // { "deviceId": "Z12", "deviceType": "照明", "power": "on" },
// { "deviceId": "F1", "deviceType": "排风", "power": "on" }, // { "deviceId": "F1", "deviceType": "排风", "power": "on" },
// { "deviceId": "E1", "deviceType": "IEQ传感器", "power": "on" }, // { "deviceId": "E1", "deviceType": "IEQ传感器", "power": "on" },
// { "deviceId": "Y1", "deviceType": "烟感器", "power": "on" } // { "deviceId": "Y1", "deviceType": "烟感器", "power": "on" }
// ], // ],
// "办公室": [ // "办公室": [
// { "deviceId": "AC3", "deviceType": "空调", "power": "on" }, // { "deviceId": "AC3", "deviceType": "空调", "power": "on" },
// { "deviceId": "Z2", "deviceType": "照明", "power": "on" }, // { "deviceId": "Z2", "deviceType": "照明", "power": "on" },
// { "deviceId": "F2", "deviceType": "排风", "power": "on" } // { "deviceId": "F2", "deviceType": "排风", "power": "on" }
// ], // ],
// "仓库": [{ "deviceId": "AC11", "deviceType": "空调", "power": "off" }], // "仓库": [],
// "CEO办公室": [{ "deviceId": "AC12", "deviceType": "空调", "power": "off" }], // "CEO办公室": [{ "deviceId": "AC12", "deviceType": "空调", "power": "off" }],
// "传感器生产间": [{ "deviceId": "AC13", "deviceType": "空调", "power": "off" }], // "传感器测试车间": [{ "deviceId": "AC13", "deviceType": "空调", "power": "off" }],
// "卫生间": [ // "卫生间": [
// { "deviceId": "S1", "deviceType": "摄像头", "power": "on" }, // { "deviceId": "S1", "deviceType": "摄像头", "power": "on" },
// { "deviceId": "E100", "deviceType": "IEQ传感器", "power": "on" }, // { "deviceId": "E100", "deviceType": "IEQ传感器", "power": "on" },
// { "deviceId": "X1", "deviceType": "音响", "power": "on" } // { "deviceId": "X1", "deviceType": "音响", "power": "on" }
// ], // ],
// "前台": [ // "前台休息区": [
// { "deviceId": "S0", "deviceType": "摄像头", "power": "on" }, // { "deviceId": "S0", "deviceType": "摄像头", "power": "on" },
// { "deviceId": "T1", "deviceType": "人流监测", "power": "on" } // { "deviceId": "T1", "deviceType": "人流监测", "power": "on" }
// ] // ]
// } // }
// ====== 动态查询:从数据库获取真实设备及最新状态 ====== // ====== 动态查询:从数据库获取真实设备及最新状态 ======
const deviceMap = await buildDeviceMapDynamic(regionMap); // 先从 sheet 中解析 IEQ 列,构建 regionName → IEQ 标签映射
// 循环区域,根据各区域环境指标计算各区域的环境质量得出优良中差四个值 const ieqTagMap: { [regionName: string]: string } = {};
if (sheet && sheet.length > 0) {
const headerRow = sheet[0] as any[];
const ieqColIdx = headerRow.findIndex((h: any) => String(h).trim() === 'IEQ');
if (ieqColIdx >= 0) {
for (let i = 1; i < sheet.length; i++) {
const row = sheet[i] as any[];
const regionName = String(row[0] ?? '').trim();
if (regionName && row[ieqColIdx] != null) {
ieqTagMap[regionName] = String(row[ieqColIdx]).trim();
}
}
}
}
const { deviceMap, qualityMap } = await buildDeviceMapDynamic(regionMap, ieqTagMap, hour);
let sheetData = await planaryArrayBecomeOfBlockData(sheet); let sheetData = await planaryArrayBecomeOfBlockData(sheet);
let result = []; let result = [];
if (sheetData && sheetData[0].blockData) { if (sheetData && sheetData[0].blockData) {
...@@ -177,7 +372,7 @@ async function readSheet(sheet: any[][], gatewayPage: number) { ...@@ -177,7 +372,7 @@ async function readSheet(sheet: any[][], gatewayPage: number) {
regionName: String(row[index] ?? ""), regionName: String(row[index] ?? ""),
xz: row[xindex], xz: row[xindex],
yz: row[yindex], yz: row[yindex],
qualityIndex: row[index] ? row[xindex]<500?"优":row[xindex]<1000?"良":row[xindex]<1200?"中":"差" : "优", qualityIndex: qualityMap[row[0]] ?? '优',
gatewayPage: gatewayPage, gatewayPage: gatewayPage,
devices: deviceMap[row[0]] devices: deviceMap[row[0]]
}))?? []; }))?? [];
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import { BizError } from '../util/bizError'; import { BizError } from '../util/bizError';
import { ERRORENUM } from '../config/errorEnum'; import { ERRORENUM } from '../config/errorEnum';
import { TABLENAME } from '../config/dbEnum'; import { TABLENAME } from '../config/dbEnum';
import { DEVICE_STATE } from '../config/businessEnum';
import { selectDataListByParam, selectOneDataByParam, selectDataCountByParam } from '../data/findData'; import { selectDataListByParam, selectOneDataByParam, selectDataCountByParam } from '../data/findData';
import { controlIndoorUnit } from './feiyiClient'; import { controlIndoorUnit } from './feiyiClient';
import { import {
...@@ -25,21 +26,6 @@ import { ...@@ -25,21 +26,6 @@ import {
formatMonthKey, formatMonthKey,
} from '../util/dateUtils'; } from '../util/dateUtils';
// ==================== 常量定义 ====================
const deviceTypeList: string[] = [
"IEQ传感器",
"烟感器",
"摄像头",
"空调",
"音响",
"照明",
"等离子",
"人流监测",
"电表",
"排风",
];
/** /**
* 空调中文到Key的映射 * 空调中文到Key的映射
* 1 制冷 2 制热 3 送风 4 除湿 * 1 制冷 2 制热 3 送风 4 除湿
...@@ -134,6 +120,32 @@ function getOverallQuality(co2: number, pm25: number): string { ...@@ -134,6 +120,32 @@ function getOverallQuality(co2: number, pm25: number): string {
/** 环境指标 key 映射:device_data JSON 中的 key → 展示用的 key */ /** 环境指标 key 映射:device_data JSON 中的 key → 展示用的 key */
const ENV_INDICATOR_KEYS = ['temperature', 'humidity', 'pm25', 'co2', 'hcho', 'lightLevel', 'pm10', 'pressure', 'tvoc']; const ENV_INDICATOR_KEYS = ['temperature', 'humidity', 'pm25', 'co2', 'hcho', 'lightLevel', 'pm10', 'pressure', 'tvoc'];
/** 指标 key → 中文名映射(涵盖标准指标 + 迪勤编码指标) */
const INDICATOR_NAME_MAP: Record<string, string> = {
// 标准指标
'temperature': '温度',
'humidity': '湿度',
'pm25': 'PM2.5',
'pm10': 'PM10',
'co2': '二氧化碳',
'tvoc': 'TVOC',
'hcho': '甲醛',
'lightLevel': '光照度',
'pressure': '气压',
// 迪勤编码指标
'a01006': '气压',
'a05024': '臭氧',
'a09527': '人在检测',
'a14186': 'PID-TVOC',
'a14263': '噪音',
'a19001': '氧含量',
'a21001': '氨气',
'a21004': '二氧化氮',
'a21005': '一氧化碳',
'a21028': '硫化氢',
'a31001': '甲醛',
};
/** /**
* 查询所有 "空气质量监测" 类型设备的最新一条 device_data * 查询所有 "空气质量监测" 类型设备的最新一条 device_data
* @returns Map<device_id, { data: JSON, device_time: string }> * @returns Map<device_id, { data: JSON, device_time: string }>
...@@ -485,77 +497,156 @@ export async function getRunAnalysis() { ...@@ -485,77 +497,156 @@ export async function getRunAnalysis() {
}; };
// ======================================================================== // ========================================================================
// 二、设备耗电量监控 // 二、设备耗电量监控(按区域聚合,分四个时间范围)
/* ====== MOCK DATA(保留,勿删) ====== /* ====== MOCK DATA(保留,勿删) ======
let deviceTypeMap = new Map();
let deviceElectricityMap = new Map();
let deviceElectricityDatas = deviceTypeList.map((deviceType) => {
return {
"deviceType": deviceType,
"deviceCount": deviceTypeMap.get(deviceType) || 0,
"electricity": deviceElectricityMap.get(deviceType) || 0
}
});
let deviceElectricity = { let deviceElectricity = {
titleList: ["设备","数量","耗电情况"], last24Hours: {
dataList: deviceElectricityDatas titleList: ["区域", "设备数量", "耗电情况", "耗电量(kwh)"],
dataList: [
{ "region": "汇报厅", "deviceCount": 12, "electricityStatus": 52.3, "kwh": 15.8 },
]
},
last7Days: {
titleList: ["区域", "设备数量", "耗电情况", "耗电量(kwh)"],
dataList: [
{ "region": "汇报厅", "deviceCount": 12, "electricityStatus": 50.1, "kwh": 110.5 },
]
},
last30Days: {
titleList: ["区域", "设备数量", "耗电情况", "耗电量(kwh)"],
dataList: [
{ "region": "汇报厅", "deviceCount": 12, "electricityStatus": 48.6, "kwh": 450.2 },
]
},
lastYear: {
titleList: ["区域", "设备数量", "耗电情况", "耗电量(kwh)"],
dataList: [
{ "region": "汇报厅", "deviceCount": 12, "electricityStatus": 45.3, "kwh": 5200.0 },
]
},
}; };
====== MOCK DATA END ====== */ ====== MOCK DATA END ====== */
// ---- 真实查询 ---- // ---- 真实查询 ----
// 查询所有设备,按 device_type 分组统计数量 // 查询所有区域
const regionResult = await selectDataListByParam(
TABLENAME.区域表,
{},
['region_key', 'region_name']
);
const allRegions: { region_key: string; region_name: string }[] = regionResult.data || [];
const regionNameMap = new Map<string, string>();
allRegions.forEach(r => regionNameMap.set(r.region_key, r.region_name || ''));
// 查询所有设备(含 region_key),按区域统计数量
const allDevicesResult = await selectDataListByParam( const allDevicesResult = await selectDataListByParam(
TABLENAME.设备表, TABLENAME.设备表,
{}, {},
['device_id', 'device_type'] ['device_id', 'region_key', 'device_state']
); );
const allDevices = allDevicesResult.data || []; const allDevices = allDevicesResult.data || [];
// 按类型统计设备数量 // 按区域统计设备数量
const deviceTypeCountMap = new Map<string, number>(); const regionDeviceCountMap = new Map<string, number>();
allDevices.forEach((d: any) => { allDevices.forEach((d: any) => {
const dt = d.device_type; const rk = d.region_key || '';
deviceTypeCountMap.set(dt, (deviceTypeCountMap.get(dt) || 0) + 1); if (rk) {
regionDeviceCountMap.set(rk, (regionDeviceCountMap.get(rk) || 0) + 1);
}
}); });
// 查询当日各电表设备的总电量 // 汇总有设备的区域 key
const allRegionKeys = new Set<string>();
regionDeviceCountMap.forEach((_, rk) => allRegionKeys.add(rk));
// 查询所有电表设备(含 region_key)
const meterDevicesResult = await selectDataListByParam( const meterDevicesResult = await selectDataListByParam(
TABLENAME.设备表, TABLENAME.设备表,
{ device_type: '电表' }, { device_type: '电表' },
['device_id'] ['device_id', 'region_key']
); );
const meterDeviceIds = (meterDevicesResult.data || []).map((d: any) => d.device_id); const meterDevices = meterDevicesResult.data || [];
const meterDeviceIds = meterDevices.map((d: any) => d.device_id);
// 构建电表→区域映射
const meterRegionMap = new Map<string, string>();
meterDevices.forEach((d: any) => {
meterRegionMap.set(d.device_id, d.region_key || '');
});
// 按设备类型统计耗电:仅电表类型有数据 // 耗电量查询终点:当前时刻(起点复用上方已定义的 last24hStart/last7dStart/last30dStart/lastYearStart)
let deviceElectricityMap = new Map<string, number>(); const elecNowStr = formatTime(new Date());
if (meterDeviceIds.length > 0) {
const meterDataResult = await selectDataListByParam( /** 查询指定时间范围内各区域的耗电量 */
async function queryRegionElectricity(startTime: string, endTime: string): Promise<Map<string, number>> {
const map = new Map<string, number>();
if (meterDeviceIds.length === 0) return map;
const result = await selectDataListByParam(
TABLENAME.设备数据表, TABLENAME.设备数据表,
{ {
device_id: { '%in%': meterDeviceIds }, device_id: { '%in%': meterDeviceIds },
device_time: { '%gte%': todayStart }, device_time: { '%gte%': startTime, '%lte%': endTime },
}, },
['device_data'] ['device_id', 'device_data']
); );
let meterTotal = 0; (result.data || []).forEach((r: any) => {
(meterDataResult.data || []).forEach((r: any) => {
const parsed = parseDeviceData(r.device_data); const parsed = parseDeviceData(r.device_data);
const val = parsed[ELECTRICITY_KEY]; const val = parsed[ELECTRICITY_KEY];
if (typeof val === 'number') meterTotal += val; if (typeof val === 'number') {
const rk = meterRegionMap.get(r.device_id) || '';
if (rk) {
map.set(rk, (map.get(rk) || 0) + val);
}
}
}); });
deviceElectricityMap.set('电表', Math.round(meterTotal * 100) / 100); return map;
} }
let deviceElectricityDatas = deviceTypeList.map((deviceType) => { /** 根据耗电量 map 构建 dataList */
return { function buildElectricityDataList(electricityMap: Map<string, number>) {
"deviceType": deviceType, let total = 0;
"deviceCount": deviceTypeCountMap.get(deviceType) || 0, electricityMap.forEach(v => total += v);
"electricity": deviceElectricityMap.get(deviceType) || 0
} return Array.from(allRegionKeys).map(rk => {
}); const kwh = Math.round((electricityMap.get(rk) || 0) * 100) / 100;
const electricityStatus = total > 0 ? Math.round((kwh / total) * 1000) / 10 : 0;
return {
region: regionNameMap.get(rk) || rk,
deviceCount: regionDeviceCountMap.get(rk) || 0,
electricityStatus,
kwh,
};
});
}
const electricityTitleList = ["区域", "设备数量", "耗电情况", "耗电量(kwh)"];
// 分别查询四个时间范围
const [last24hMap, last7dMap, last30dMap, lastYearMap] = await Promise.all([
queryRegionElectricity(last24hStart, elecNowStr),
queryRegionElectricity(last7dStart, elecNowStr),
queryRegionElectricity(last30dStart, elecNowStr),
queryRegionElectricity(lastYearStart, elecNowStr),
]);
let deviceElectricity = { let deviceElectricity = {
titleList: ["设备", "数量", "耗电情况"], last24Hours: {
dataList: deviceElectricityDatas titleList: electricityTitleList,
dataList: buildElectricityDataList(last24hMap),
},
last7Days: {
titleList: electricityTitleList,
dataList: buildElectricityDataList(last7dMap),
},
last30Days: {
titleList: electricityTitleList,
dataList: buildElectricityDataList(last30dMap),
},
lastYear: {
titleList: electricityTitleList,
dataList: buildElectricityDataList(lastYearMap),
},
}; };
// ======================================================================== // ========================================================================
...@@ -573,9 +664,9 @@ export async function getRunAnalysis() { ...@@ -573,9 +664,9 @@ export async function getRunAnalysis() {
// ---- 真实查询 ---- // ---- 真实查询 ----
const allDevFull = allDevices; const allDevFull = allDevices;
const total = allDevFull.length; const total = allDevFull.length;
const onlineTotal = allDevFull.filter((d: any) => d.device_state === 1).length; const onlineTotal = allDevFull.filter((d: any) => d.device_state === DEVICE_STATE.在线).length;
const offlineTotal = allDevFull.filter((d: any) => d.device_state === 0).length; const offlineTotal = allDevFull.filter((d: any) => d.device_state === DEVICE_STATE.离线).length;
const faultTotal = allDevFull.filter((d: any) => d.device_state === 2).length; const faultTotal = allDevFull.filter((d: any) => d.device_state === DEVICE_STATE.故障).length;
let deviceMonitor = { let deviceMonitor = {
total: total, total: total,
...@@ -1001,9 +1092,9 @@ export async function getRunAnalysis() { ...@@ -1001,9 +1092,9 @@ export async function getRunAnalysis() {
====== MOCK DATA END ====== */ ====== MOCK DATA END ====== */
// ---- 真实查询 ---- // ---- 真实查询 ----
const runningDevices = allDevFull.filter((d: any) => d.device_state === 1).length; const runningDevices = allDevFull.filter((d: any) => d.device_state === DEVICE_STATE.在线).length;
const faultDevices = allDevFull.filter((d: any) => d.device_state === 2).length; const faultDevices = allDevFull.filter((d: any) => d.device_state === DEVICE_STATE.故障).length;
const offlineDevicesSummary = allDevFull.filter((d: any) => d.device_state === 0).length; const offlineDevicesSummary = allDevFull.filter((d: any) => d.device_state === DEVICE_STATE.离线).length;
const normalRate = total > 0 ? ((runningDevices / total) * 100).toFixed(2) + '%' : '0.00%'; const normalRate = total > 0 ? ((runningDevices / total) * 100).toFixed(2) + '%' : '0.00%';
let summaryData = { let summaryData = {
...@@ -1074,9 +1165,9 @@ export async function getAnalysisPopup(regionKey: string) { ...@@ -1074,9 +1165,9 @@ export async function getAnalysisPopup(regionKey: string) {
// 设备状态监测(总数/在线/离线/故障) // 设备状态监测(总数/在线/离线/故障)
const total = devices.length; const total = devices.length;
const online = devices.filter((d: any) => d.device_state === 1).length; const online = devices.filter((d: any) => d.device_state === DEVICE_STATE.在线).length;
const offline = devices.filter((d: any) => d.device_state === 0).length; const offline = devices.filter((d: any) => d.device_state === DEVICE_STATE.离线).length;
const fault = devices.filter((d: any) => d.device_state === 2).length; const fault = devices.filter((d: any) => d.device_state === DEVICE_STATE.故障).length;
let sbztjc = { let sbztjc = {
"total": total, "total": total,
...@@ -1117,9 +1208,9 @@ export async function getAnalysisPopup(regionKey: string) { ...@@ -1117,9 +1208,9 @@ export async function getAnalysisPopup(regionKey: string) {
let hOnline = 0, hOffline = 0, hFault = 0; let hOnline = 0, hOffline = 0, hFault = 0;
devices.forEach((d: any) => { devices.forEach((d: any) => {
const reported = reportedDeviceSet.has(d.device_id); const reported = reportedDeviceSet.has(d.device_id);
if (reported && d.device_state === 1) hOnline++; if (reported && d.device_state === DEVICE_STATE.在线) hOnline++;
else if (d.device_state === 0) hOffline++; else if (d.device_state === DEVICE_STATE.离线) hOffline++;
else if (d.device_state === 2) hFault++; else if (d.device_state === DEVICE_STATE.故障) hFault++;
else if (!reported) hOffline++; // 未上报也算离线 else if (!reported) hOffline++; // 未上报也算离线
}); });
sbztjcqs.push({ sbztjcqs.push({
...@@ -1471,8 +1562,8 @@ export async function getRunEnvironmental() { ...@@ -1471,8 +1562,8 @@ export async function getRunEnvironmental() {
const regionDevices = envDevices.filter((d: any) => d.region_key === region.region_key); const regionDevices = envDevices.filter((d: any) => d.region_key === region.region_key);
if (regionDevices.length === 0) continue; if (regionDevices.length === 0) continue;
const onlineCount = regionDevices.filter((d: any) => d.device_state === 1).length; const onlineCount = regionDevices.filter((d: any) => d.device_state === DEVICE_STATE.在线).length;
const offlineCount = regionDevices.filter((d: any) => d.device_state === 0).length; const offlineCount = regionDevices.filter((d: any) => d.device_state === DEVICE_STATE.离线).length;
const totalCount = regionDevices.length; const totalCount = regionDevices.length;
let deviceStatus: string; let deviceStatus: string;
...@@ -1601,9 +1692,9 @@ export async function getRunMonitoring() { ...@@ -1601,9 +1692,9 @@ export async function getRunMonitoring() {
const totalCount = regionDevices.length; const totalCount = regionDevices.length;
if (totalCount === 0) return; // 无设备区域跳过 if (totalCount === 0) return; // 无设备区域跳过
const onlineCount = regionDevices.filter((d: any) => d.device_state === 1).length; const onlineCount = regionDevices.filter((d: any) => d.device_state === DEVICE_STATE.在线).length;
const offlineCount = regionDevices.filter((d: any) => d.device_state === 0).length; const offlineCount = regionDevices.filter((d: any) => d.device_state === DEVICE_STATE.离线).length;
const faultCount = regionDevices.filter((d: any) => d.device_state === 2).length; const faultCount = regionDevices.filter((d: any) => d.device_state === DEVICE_STATE.故障).length;
// status: 区域内所有设备正常=0,存在离线或故障=1(异常) // status: 区域内所有设备正常=0,存在离线或故障=1(异常)
const hasAbnormal = (offlineCount + faultCount) > 0; const hasAbnormal = (offlineCount + faultCount) > 0;
...@@ -1790,9 +1881,9 @@ export async function getRunMonitoring() { ...@@ -1790,9 +1881,9 @@ export async function getRunMonitoring() {
// ---- 真实查询 ---- // ---- 真实查询 ----
const total = allDevices.length; const total = allDevices.length;
const runningDevices = allDevices.filter((d: any) => d.device_state === 1).length; const runningDevices = allDevices.filter((d: any) => d.device_state === DEVICE_STATE.在线).length;
const offlineDevicesSummary = allDevices.filter((d: any) => d.device_state === 0).length; const offlineDevicesSummary = allDevices.filter((d: any) => d.device_state === DEVICE_STATE.离线).length;
const faultDevices = allDevices.filter((d: any) => d.device_state === 2).length; const faultDevices = allDevices.filter((d: any) => d.device_state === DEVICE_STATE.故障).length;
const normalRate = total > 0 ? ((runningDevices / total) * 100).toFixed(2) + '%' : '0.00%'; const normalRate = total > 0 ? ((runningDevices / total) * 100).toFixed(2) + '%' : '0.00%';
let summaryData = { let summaryData = {
...@@ -1840,15 +1931,16 @@ export async function getMonitorPopup(regionKey: string) { ...@@ -1840,15 +1931,16 @@ export async function getMonitorPopup(regionKey: string) {
"deviceName": "空调内机8-1-0-2", "deviceName": "空调内机8-1-0-2",
"deviceType": "空调", "deviceType": "空调",
"status": 0, "status": 0,
"parameter": "8-1-0-2", // "parameter": "8-1-0-2",
"monitoringData": "" "monitoringData": ""
},{ },{
"deviceId": "24E124710E466083", "deviceId": "24E124710E466083",
"deviceName": "环境监测cgq6", "deviceName": "环境监测cgq6",
"deviceType": "IEQ传感器", "deviceType": "IEQ传感器",
"status": 0, "status": 0,
"parameter": "o3,co2,hpa,pir,hcho,pm10,pm25,tvoc,humidity,lighting,pressure,temperature", // "parameter": "o3,co2,hpa,pir,hcho,pm10,pm25,tvoc,humidity,lighting,pressure,temperature",
"monitoringData": "10、2000、1260、5、1.25、1000、1000、500、100、L5、5000、60" // "monitoringData": "10、2000、1260、5、1.25、1000、1000、500、100、L5、5000、60"
"monitoringData": "温度: 26、湿度: 60、人在检测: 0、噪音: 39.5、甲醛: 10"
} }
] ]
} }
...@@ -1860,7 +1952,7 @@ export async function getMonitorPopup(regionKey: string) { ...@@ -1860,7 +1952,7 @@ export async function getMonitorPopup(regionKey: string) {
// 查询区域信息和设备列表 // 查询区域信息和设备列表
const regionName = await getRegionName(regionKey); const regionName = await getRegionName(regionKey);
const devices = await getDevicesByRegion(regionKey, ['device_id', 'device_type', 'device_name', 'device_param', 'device_state']); const devices = await getDevicesByRegion(regionKey, ['device_id', 'device_type', 'device_name', 'device_state']);
// ==================== IEQ传感器监测 ==================== // ==================== IEQ传感器监测 ====================
// 查询该区域下 IEQ传感器 设备的最新数据 // 查询该区域下 IEQ传感器 设备的最新数据
...@@ -1930,17 +2022,25 @@ export async function getMonitorPopup(regionKey: string) { ...@@ -1930,17 +2022,25 @@ export async function getMonitorPopup(regionKey: string) {
const deviceName = device.device_name || ''; const deviceName = device.device_name || '';
const deviceType = device.device_type || ''; const deviceType = device.device_type || '';
const deviceState = device.device_state; const deviceState = device.device_state;
// status: 在线=0,离线=1,故障=2 // deviceState: 在线=0,离线=1,故障=2
const status = deviceState === 1 ? 0 : (deviceState === 0 ? 1 : 2); // status: 1 on 0 off
const parameter = device.device_param || ''; const status = deviceState === DEVICE_STATE.在线 ? 1 : 0;
// 从批量查询结果中获取该设备最新监测数据 // 从批量查询结果中获取该设备最新监测数据
let monitoringData = ''; let monitoringData = '';
const latestData = latestDataMap.get(deviceId); const latestData = latestDataMap.get(deviceId);
if (latestData) { if (latestData) {
const vals = ENV_INDICATOR_KEYS.map(k => latestData[k]).filter(v => v !== undefined && v !== null); const items: string[] = [];
if (vals.length > 0) { for (const key of Object.keys(latestData)) {
monitoringData = vals.join('、'); const val = latestData[key];
if (val === undefined || val === null) continue;
const name = INDICATOR_NAME_MAP[key];
if (name) {
items.push(`${name}:${val}`);
}
}
if (items.length > 0) {
monitoringData = items.join('、');
} }
} }
...@@ -1949,7 +2049,6 @@ export async function getMonitorPopup(regionKey: string) { ...@@ -1949,7 +2049,6 @@ export async function getMonitorPopup(regionKey: string) {
deviceName, deviceName,
deviceType, deviceType,
status, status,
parameter,
monitoringData, monitoringData,
}; };
}); });
...@@ -1958,7 +2057,7 @@ export async function getMonitorPopup(regionKey: string) { ...@@ -1958,7 +2057,7 @@ export async function getMonitorPopup(regionKey: string) {
"regionName": regionName, "regionName": regionName,
"kqzljc": kqzljc, "kqzljc": kqzljc,
"sbjc": { "sbjc": {
"titleList": ["编号", "设备", "状态", "参数", "监测数据"], "titleList": ["编号", "设备", "状态", "监测数据"],
"dataList": sbjcDataList, "dataList": sbjcDataList,
} }
}; };
......
// ==================== 设备相关枚举 ====================
/**
* 设备类型枚举
*/
export enum DEVICE_TYPE {
/** IEQ传感器(迪勤) */
IEQ传感器 = 'IEQ传感器',
/** 烟感器(规划中) */
烟感器 = '烟感器',
/** 摄像头(规划中) */
摄像头 = '摄像头',
/** 空调内机(飞奕) */
空调 = '空调',
/** 新风机(飞奕,区域名含"新风"时自动判定) */
新风 = '新风',
/** 音响(规划中) */
音响 = '音响',
/** 照明(规划中) */
照明 = '照明',
/** 等离子(规划中) */
等离子 = '等离子',
/** 人流监测(规划中) */
人流监测 = '人流监测',
/** 电表(飞奕) */
电表 = '电表',
/** 排风(规划中) */
排风 = '排风',
}
/**
* 设备状态枚举
*/
export enum DEVICE_STATE {
/** 在线 */
在线 = 0,
/** 离线 */
离线 = 1,
/** 故障 */
故障 = 2,
}
// ==================== 故障相关枚举 ====================
/**
* 故障状态枚举
*/
export enum FAULT_STATUS {
/** 未处理 */
未处理 = 0,
/** 处理中 */
处理中 = 1,
/** 已处理 */
已处理 = 2,
}
/**
* 故障等级枚举
*/
export enum FAULT_LEVEL {
/** 一级(轻微) */
一级 = 1,
/** 二级(一般) */
二级 = 2,
/** 三级(严重) */
三级 = 3,
}
// ==================== 区域相关枚举 ====================
/**
* 区域类型枚举
*/
export enum REGION_TYPE {
/** 楼栋 */
楼栋 = '楼栋',
/** 房间 */
房间 = '房间',
/** 办公室 */
办公室 = '办公室',
/** 机房 */
机房 = '机房',
/** 车间 */
车间 = '车间',
/** 迪勤场所 */
场所 = '场所',
/** 迪勤监测点 */
监测点 = '监测点',
}
// ==================== 关联相关枚举 ====================
/**
* 区域设备关联类型枚举
*/
export enum RELATION_TYPE {
/** 区域一对多设备 */
region_to_device = 'region_to_device',
/** 设备一对多区域 */
device_to_region = 'device_to_region',
}
// ==================== 空调控制相关枚举 ====================
/**
* 工作模式枚举
*/
export enum WORK_MODE {
制冷 = 1,
制热 = 2,
送风 = 3,
除湿 = 4,
}
/**
* 开关状态枚举
*/
export enum POWER_STATE {
on = 'on',
off = 'off',
}
/**
* 风速枚举
*/
export enum FAN_SPEED {
超强 = 1,
= 2,
= 4,
}
/**
* 自动控制状态枚举
*/
export enum AUTO_CONTROL {
生效 = '生效',
解除 = '解除',
}
...@@ -68,6 +68,12 @@ export const TablesConfig = [ ...@@ -68,6 +68,12 @@ export const TablesConfig = [
unique: true, unique: true,
comment: '区域编码,唯一' comment: '区域编码,唯一'
}, },
region_ad: {
type: Sequelize.STRING(100),
allowNull: true,
defaultValue: null,
comment: '备用区域编码,多平台接入同一区域时存储其他平台的region_key'
},
region_name: { region_name: {
type: Sequelize.STRING(255), type: Sequelize.STRING(255),
allowNull: false, allowNull: false,
...@@ -162,13 +168,13 @@ export const TablesConfig = [ ...@@ -162,13 +168,13 @@ export const TablesConfig = [
device_type: { device_type: {
type: Sequelize.STRING(50), type: Sequelize.STRING(50),
allowNull: false, allowNull: false,
comment: '设备类型:IEQ传感器, 烟感器, 摄像头, 空调, 音响, 照明, 等离子, 人流监测, 电表, 排风' comment: '设备类型:IEQ传感器, 烟感器, 摄像头, 空调, 新风, 音响, 照明, 等离子, 人流监测, 电表, 排风'
}, },
device_state: { device_state: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
defaultValue: 0, defaultValue: 0,
comment: '设备状态:0=离线, 1=在线, 2=故障' comment: '设备状态:0=在线, 1=离线, 2=故障'
}, },
device_name: { device_name: {
type: Sequelize.STRING(255), type: Sequelize.STRING(255),
...@@ -482,9 +488,9 @@ export const TablesConfig = [ ...@@ -482,9 +488,9 @@ export const TablesConfig = [
comment: '故障类型' comment: '故障类型'
}, },
fault_level: { fault_level: {
type: Sequelize.STRING(50), type: Sequelize.INTEGER,
allowNull: true, allowNull: true,
comment: '故障等级:一级, 二级, 三级' comment: '故障等级: 1=一级, 2=二级, 3=三级'
}, },
fault_info: { fault_info: {
type: DataTypes.JSON, type: DataTypes.JSON,
...@@ -497,10 +503,10 @@ export const TablesConfig = [ ...@@ -497,10 +503,10 @@ export const TablesConfig = [
comment: '故障时间' comment: '故障时间'
}, },
fault_status: { fault_status: {
type: Sequelize.STRING(50), type: Sequelize.INTEGER,
allowNull: false, allowNull: false,
defaultValue: '未处理', defaultValue: 0,
comment: '故障状态:未处理, 处理中, 已处理' comment: '故障状态: 0=未处理, 1=处理中, 2=已处理'
}, },
handle_time: { handle_time: {
type: DataTypes.DATE, type: DataTypes.DATE,
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
* - 任务锁死时(超时未释放),会主动强制释放锁,确保后续任务能正常执行 * - 任务锁死时(超时未释放),会主动强制释放锁,确保后续任务能正常执行
*/ */
import { region, device, deviceData, alertWorkData, processFaultStatus, diqinSyncAll } from "../biz/dataIntegration"; import { region, device, deviceData, alertWorkData, processFaultStatus, diqinSyncAll, migrateDeviceRegionKey } from "../biz/dataIntegration";
// ==================== 通用任务锁 ==================== // ==================== 通用任务锁 ====================
...@@ -133,6 +133,7 @@ const dataSyncTask = createScheduledTask('定时任务', TASK_INTERVAL_MS, dataS ...@@ -133,6 +133,7 @@ const dataSyncTask = createScheduledTask('定时任务', TASK_INTERVAL_MS, dataS
await device(); await device();
await deviceData(); await deviceData();
await diqinSyncAll(); await diqinSyncAll();
await migrateDeviceRegionKey();
}); });
// 预警工单任务 // 预警工单任务
......
...@@ -15,9 +15,9 @@ export function setRouter(httpServer) { ...@@ -15,9 +15,9 @@ export function setRouter(httpServer) {
* 区域位置 * 区域位置
*/ */
async function getRegionLocation(req, res) { async function getRegionLocation(req, res) {
let reqConf = {gatewayPage:'Number'}; let reqConf = {gatewayPage:'Number', hour: 'Number'};
const NotMustHaveKeys = []; const NotMustHaveKeys = [ 'hour' ];
let { gatewayPage } = eccReqParamater(reqConf, req.body, NotMustHaveKeys); let { gatewayPage, hour } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await regionBiz.getRegionLocation(gatewayPage); const result = await regionBiz.getRegionLocation(gatewayPage, hour);
res.success(result); res.success(result);
} }
\ No newline at end of file
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