完善客流数据接入

parent 42295692
...@@ -97,29 +97,30 @@ export async function processDeviceData(message) { ...@@ -97,29 +97,30 @@ export async function processDeviceData(message) {
} }
} }
/** 客流触发的单条线路数据 */
interface CustomerTriggerItem {
/** 线路编号 */
triggerKey: number;
/** 线路名称 */
triggerName: string;
/** 线路UUID */
triggerUuid: string;
/** 统计对象 { in: number, out: number } */
total: any;
}
/** /**
* 处理接收到的客流设备数据,并存入数据库 * 处理接收到的客流设备数据,并存入数据库
* *
* 消息示例 * 当前格式(line_trigger_data)
* { * {
* device_info: { * device_info: { ... },
* device_mac: 'C0:BA:1F:02:98:F6', * line_trigger_data: [ { line: 1, line_name: 'Line1', line_uuid: '...', total: { in: 2, out: 2 } } ],
* device_name: 'People Counter', * time_info: { ... }
* device_sn: '6537F53875140008',
* firmware_version: 'V_125-LW.1.0.3-r4',
* hardware_version: 'V1.1',
* ip_address: '192.168.125.114',
* running_time: 4764787,
* wlan_mac: 'C0:BA:1F:02:98:F7'
* },
* region_trigger_data: { region_count_data: [ { region: 1, region_name: 'Region1', region_uuid: 'b63ffafc-857f-4d12-9dc7-d7fddfeb5f0f', total: { current_total: 9 } } ] },
* time_info: {
* dst_status: false,
* enable_dst: false,
* time: '2026-06-18T05:19:23-00:00',
* time_zone: 'UTC-0:00 Western European Time (WET), Greenwich Mean Time (GMT)'
* }
* } * }
*
* 旧格式(已弃用,注释保留备用):
* // region_trigger_data: { region_count_data: [ { region: 1, region_name: 'Region1', region_uuid: '...', total: { current_total: 9 } } ] }
*/ */
export async function customerDeviceData(message) { export async function customerDeviceData(message) {
try { try {
...@@ -152,14 +153,42 @@ export async function customerDeviceData(message) { ...@@ -152,14 +153,42 @@ export async function customerDeviceData(message) {
const timeInfo = data.time_info; const timeInfo = data.time_info;
const deviceTime = timeInfo && timeInfo.time ? new Date(timeInfo.time) : new Date(); const deviceTime = timeInfo && timeInfo.time ? new Date(timeInfo.time) : new Date();
// ===== 提取客流数据 ===== // ===== 提取客流数据(当前使用新格式 line_trigger_data) =====
const regionTriggerData = data.region_trigger_data; let triggerItems: CustomerTriggerItem[] = [];
if (!regionTriggerData || !regionTriggerData.region_count_data || regionTriggerData.region_count_data.length === 0) {
console.warn('消息缺少客流区域数据,跳过'); if (data.line_trigger_data && Array.isArray(data.line_trigger_data)) {
// 新格式:line_trigger_data,直接是数组,total: { in, out }
for (const item of data.line_trigger_data) {
triggerItems.push({
triggerKey: item.line,
triggerName: item.line_name || '',
triggerUuid: item.line_uuid || '',
total: item.total,
});
}
console.log(`[客流] 使用新格式 line_trigger_data,共 ${triggerItems.length} 条线路`);
}
/*
// ====== 旧格式(已弃用,保留备用) ======
else if (data.region_trigger_data?.region_count_data?.length > 0) {
// 旧格式:region_trigger_data.region_count_data,total: { current_total }
for (const item of data.region_trigger_data.region_count_data) {
triggerItems.push({
triggerKey: item.region,
triggerName: item.region_name || '',
triggerUuid: item.region_uuid || '',
total: item.total,
});
}
console.log(`[客流] 使用旧格式 region_trigger_data,共 ${triggerItems.length} 条区域`);
}
*/
if (triggerItems.length === 0) {
console.warn('消息缺少客流线路数据,跳过');
return; return;
} }
const regionCountData = regionTriggerData.region_count_data;
const now = new Date(); const now = new Date();
// ===== 设备校验/新增 ===== // ===== 设备校验/新增 =====
...@@ -167,13 +196,13 @@ export async function customerDeviceData(message) { ...@@ -167,13 +196,13 @@ export async function customerDeviceData(message) {
TABLENAME.设备表, { device_id: deviceSn } TABLENAME.设备表, { device_id: deviceSn }
); );
if (!deviceRow) { if (!deviceRow) {
// 设备不存在,为每个区域创建一条设备记录 // 设备不存在,为每条线路创建一条设备记录
for (const regionData of regionCountData) { for (const item of triggerItems) {
await addData( await addData(
TABLENAME.设备表, TABLENAME.设备表,
{ {
device_id: deviceSn, device_id: deviceSn,
region_key: regionData.region, region_key: item.triggerKey,
device_type: '客流传感器', device_type: '客流传感器',
device_name: deviceName, device_name: deviceName,
control_params: data, control_params: data,
...@@ -181,32 +210,30 @@ export async function customerDeviceData(message) { ...@@ -181,32 +210,30 @@ export async function customerDeviceData(message) {
updated_at: now, updated_at: now,
} }
); );
console.log(`客流设备已创建: ${deviceSn}, 区域 ${regionData.region}`); console.log(`客流设备已创建: ${deviceSn}, 线路 ${item.triggerKey}`);
} }
} }
// 遍历每个区域的客流数据,逐条入库 // 遍历每条线路的客流数据,逐条入库
for (const regionData of regionCountData) { for (const item of triggerItems) {
const regionUuid = regionData.region_uuid; const inCount = item.total?.in ?? 0;
const regionName = regionData.region_name || ''; const outCount = item.total?.out ?? 0;
const regionNumber = regionData.region;
const currentTotal = regionData.total?.current_total ?? 0;
await addData( await addData(
TABLENAME.设备数据表, TABLENAME.设备数据表,
{ {
device_id: deviceSn, device_id: deviceSn,
data: regionData.total, data: item.total,
received_time: now, received_time: now,
device_time: deviceTime, device_time: deviceTime,
created_at: now, created_at: now,
} }
); );
console.log(`客流数据入库成功: 设备 ${deviceSn}, 区域 ${regionName || regionUuid}, 当前人数 ${currentTotal}`); console.log(`客流数据入库成功: 设备 ${deviceSn}, ${item.triggerName || item.triggerUuid}, 进 ${inCount}${outCount}`);
} }
console.log(`客流数据处理完成: 设备 ${deviceSn}, 共入库 ${regionCountData.length} 条区域数据`); console.log(`客流数据处理完成: 设备 ${deviceSn}, 共入库 ${triggerItems.length}数据`);
} catch (error) { } catch (error) {
console.error('客流数据处理失败:', error); console.error('客流数据处理失败:', error);
......
...@@ -181,16 +181,23 @@ function calcEnvDeviceDaily(records: any[]): { tempSum: number; tempCount: numbe ...@@ -181,16 +181,23 @@ function calcEnvDeviceDaily(records: any[]): { tempSum: number; tempCount: numbe
/** /**
* 从客流传感器数据中计算当日平均在馆人数 * 从客流传感器数据中计算当日平均在馆人数
* current_total 为当时在场人数(瞬时值),取当日所有采集值的算术平均 * 新格式使用 data.in(进)和 data.out(出)
* // 旧格式(已弃用,保留备用):current_total 为当时在场人数(瞬时值),取当日所有采集值的算术平均
*/ */
function calcCustomerDeviceDaily(records: any[]): { visitorSum: number; visitorCount: number } { function calcCustomerDeviceDaily(records: any[]): { visitorSum: number; visitorCount: number } {
let sum = 0, count = 0; let sum = 0, count = 0;
for (const rec of records) { for (const rec of records) {
const d = rec.data || {}; const d = rec.data || {};
if (d.current_total != null) { // ====== 新格式使用 in ======
sum += Number(d.current_total); if (d.in != null) {
sum += Number(d.in);
count++; count++;
} }
// ====== 旧格式(已弃用,保留备用) ======
// if (d.current_total != null) {
// sum += Number(d.current_total);
// count++;
// }
} }
return { visitorSum: sum, visitorCount: count }; return { visitorSum: sum, visitorCount: count };
} }
......
...@@ -296,43 +296,118 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -296,43 +296,118 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
tvocTrend: tvocTrend tvocTrend: tvocTrend
}; };
// 5.1 预警监控和预警工单处理进度分析 // 5.1 预警监控和预警工单处理进度分析(从 device_fault 表获取,近30天)
let alertStatus = { const faultStatusTextMap: { [key: number]: string } = { 0: "未处理", 1: "已响应", 2: "已解决" };
totalAlerts: 3,
resolved: 1, // 降级默认值:查询或统计失败时使用,保证其他模块不受影响
responded: 1, let alertStatus = { totalAlerts: 0, resolved: 0, responded: 0, unresolved: 0 };
unresolved: 1 let alertWorkLevel: { [key: string]: number } = { "一级预警": 0, "二级预警": 0, "三级预警": 0 };
}; let alertWorkType: any[] = [];
let alertWorkOrders = [ let alertWorkOrders: any[] = [];
{ riskContent: "空调压缩机异常", alertTime: "26/02/01", status: "已解决" },
{ riskContent: "温度传感器离线", alertTime: "26/02/01", status: "已响应" }, try {
{ riskContent: "湿度超标", alertTime: "26/02/02", status: "未处理" } // 时间范围:近30天
]; const thirtyDaysAgo = new Date(nowTime.getTime() - 30 * 86400000);
let alertWorkThreeOrders = [ const timeFilter = { "%gte%": formatLocalTime(thirtyDaysAgo) };
{ riskContent: "空调压缩机异常", alertTime: "26/02/01", alertAddress: "A馆1F辉煌厅", device_id: "3-1-0-0", faultCode: "LU", status: "已解决" },
{ riskContent: "温度传感器离线", alertTime: "26/02/01", alertAddress: "A馆1F海外厅", device_id: "3-1-0-1", faultCode: "018", status: "已响应" }, // ---- 统计查询:只取 status/level/fault_type,避免传输无用字段 ----
{ riskContent: "湿度超标", alertTime: "26/02/02", alertAddress: "A馆1F走廊", device_id: "2-4-0-0", faultCode: "13", status: "未处理" } const statsRes = await selectDataListByParam(TABLENAME.设备故障表,
]; { occurred_time: timeFilter },
let alertWorkLevel = { ["status", "level", "fault_type"]);
"一级预警": 1, const statsFaults: any[] = Array.isArray(statsRes.data) ? statsRes.data : [];
"二级预警": 1, const totalAlerts = statsFaults.length;
"三级预警": 1
}; // alertStatus:按处理状态一次遍历统计
let alertWorkType = [{ count: 1, name: "设备故障", proportion: '50%' }, let resolved = 0, responded = 0, unresolved = 0;
{ count: 2, name: "环境异常", proportion: '50%' }, for (const f of statsFaults) {
{ count: 0, name: "安全事件", proportion: '0%' }]; if (f.status === 2) resolved++;
// 5.2. 预警监控数据 else if (f.status === 1) responded++;
else if (f.status === 0) unresolved++;
}
alertStatus = { totalAlerts, resolved, responded, unresolved };
// alertWorkLevel:按故障等级统计
for (const f of statsFaults) {
if (f.level === 1) alertWorkLevel["一级预警"]++;
else if (f.level === 2) alertWorkLevel["二级预警"]++;
else if (f.level === 3) alertWorkLevel["三级预警"]++;
}
// alertWorkType:按故障类型统计
let faultTypeCountMap = new Map<string, number>();
for (const f of statsFaults) {
const typeName = f.fault_type || "其他";
faultTypeCountMap.set(typeName, (faultTypeCountMap.get(typeName) || 0) + 1);
}
for (const [name, count] of faultTypeCountMap) {
alertWorkType.push({
count,
name,
proportion: totalAlerts > 0 ? ((count / totalAlerts) * 100).toFixed(0) + '%' : '0%'
});
}
// ---- 工单列表查询:DB 层倒序 + LIMIT 10,避免全量拉到内存排序 ----
const ordersRes = await selectDataListByParam(TABLENAME.设备故障表,
{
occurred_time: timeFilter,
"%orderDesc%": "occurred_time",
"%limit%": 10
},
["id", "device_id", "fault_type", "fault_code", "fault_description", "occurred_time", "level", "status"]);
const sortedFaults: any[] = Array.isArray(ordersRes.data) ? [...ordersRes.data] : [];
// 三级页面需要展示 alertAddress,需查设备表和区域表
let deviceRegionMap = new Map<string, string>();
if (regionType && sortedFaults.length > 0) {
const faultUniqueDeviceIds = [...new Set(sortedFaults.map((f: any) => f.device_id))];
const devicesRes = await selectDataListByParam(TABLENAME.设备表,
{ device_id: { "%in%": faultUniqueDeviceIds } },
["device_id", "region_key"]);
const regionKeySet = new Set<string>();
const deviceRegionKeyMap = new Map<string, string>();
for (const d of devicesRes.data) {
deviceRegionKeyMap.set(d.device_id, d.region_key);
if (d.region_key) regionKeySet.add(d.region_key);
}
if (regionKeySet.size > 0) {
const regionsRes = await selectDataListByParam(TABLENAME.区域表,
{ id: { "%in%": [...regionKeySet] } },
["id", "name"]);
const regionNameMap = new Map<string, string>();
for (const r of regionsRes.data) {
regionNameMap.set(String(r.id), r.name);
}
for (const [deviceId, regionKey] of deviceRegionKeyMap) {
deviceRegionMap.set(deviceId, regionNameMap.get(regionKey) || '');
}
}
alertWorkOrders = sortedFaults.map((f: any) => ({
riskContent: f.fault_description || f.fault_type || '',
alertTime: f.occurred_time || '',
status: faultStatusTextMap[f.status] || '未知',
alertAddress: deviceRegionMap.get(f.device_id) || '',
device_id: f.device_id || '',
faultCode: f.fault_code || '',
}));
} else {
alertWorkOrders = sortedFaults.map((f: any) => ({
riskContent: f.fault_description || f.fault_type || '',
alertTime: f.occurred_time || '',
status: faultStatusTextMap[f.status] || '未知',
}));
}
} catch (err) {
console.error("预警监控数据查询失败,使用空数据降级:", err);
}
resultAny.alertManagement = { resultAny.alertManagement = {
alertStatus: alertStatus, alertStatus,
alertWorkLevel: alertWorkLevel, alertWorkLevel,
alertWorkType: alertWorkType, alertWorkType,
alertWorkOrders: alertWorkOrders alertWorkOrders,
}; };
// 三级页面,预警信息展示不同
if (regionType) {
resultAny.alertManagement.alertWorkOrders = alertWorkThreeOrders;
}
// 6.1 人流监控及客流量趋势分析 // 6.1 人流监控及客流量趋势分析
let regionInfos: any = []; let regionInfos: any = [];
...@@ -370,16 +445,25 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -370,16 +445,25 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
"%orderAsc%": "device_time" "%orderAsc%": "device_time"
}, ["device_id", "data", "device_time"]); }, ["device_id", "data", "device_time"]);
// 统计区域类型的客流量 // 统计区域类型的客流量(新格式使用 in/out)
for (let rec of customerDeviceDatas.data) { for (let rec of customerDeviceDatas.data) {
let data = rec.data; let data = rec.data;
// if (data.presence === '进') { // ====== 旧格式(已弃用,保留备用) ======
if (data.current_total) { // if (data.current_total) {
totalRegionCustomerNum += data.current_total; // totalRegionCustomerNum += data.current_total;
// if (!regionCustomerMap.has(regionName)) {
// regionCustomerMap.set(regionName, data.current_total);
// } else {
// regionCustomerMap.set(regionName, totalNum + data.current_total);
// }
// }
const netVisitor = (data.in ?? 0);
if (netVisitor > 0) {
totalRegionCustomerNum += netVisitor;
if (!regionCustomerMap.has(regionName)) { if (!regionCustomerMap.has(regionName)) {
regionCustomerMap.set(regionName, data.current_total); regionCustomerMap.set(regionName, netVisitor);
} else { } else {
regionCustomerMap.set(regionName, totalNum + data.current_total); regionCustomerMap.set(regionName, totalNum + netVisitor);
} }
} }
} }
...@@ -393,7 +477,7 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -393,7 +477,7 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
let yesterdayCustomer = 0; let yesterdayCustomer = 0;
// 计算每个区域的饱和度(客流量/区域最大承载量),并转换成表格 // 计算每个区域的饱和度(客流量/区域最大承载量),并转换成表格
for (let [regionName, customerNum] of regionCustomerMap.entries()) { for (let [regionName, customerNum] of regionCustomerMap.entries()) {
let regionMaxCustomer = regionMaxCustomerMap.get(regionName) || 10000; // 默认最大承载量为10000 let regionMaxCustomer = regionMaxCustomerMap.get(regionName) || 1000; // 默认最大承载量为1000
let saturation = `${((customerNum / regionMaxCustomer) * 100).toFixed(2)}%`; let saturation = `${((customerNum / regionMaxCustomer) * 100).toFixed(2)}%`;
regionCustomerTable.push({ regionName, customerNum, saturation }); regionCustomerTable.push({ regionName, customerNum, saturation });
todayCustomer += customerNum; todayCustomer += customerNum;
...@@ -419,8 +503,10 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -419,8 +503,10 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
let d = new Date(rec.device_time); let d = new Date(rec.device_time);
let hourKey = `${d.getHours()}时`; let hourKey = `${d.getHours()}时`;
let data = rec.data; let data = rec.data;
// let customerNum = data.presence === '进' ? 1 : 0; // ====== 旧格式(已弃用,保留备用) ======
let customerNum = data.current_total; // let customerNum = data.current_total;
// ====== 新格式使用 in ======
let customerNum = data.in ?? 0;
if (!hourlyCustomerMap.has(hourKey)) { if (!hourlyCustomerMap.has(hourKey)) {
hourlyCustomerMap.set(hourKey, customerNum); hourlyCustomerMap.set(hourKey, customerNum);
} else { } else {
...@@ -443,8 +529,10 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) { ...@@ -443,8 +529,10 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
}, ["device_id", "data", "device_time"]); }, ["device_id", "data", "device_time"]);
for (let rec of yesterdayDatas.data) { for (let rec of yesterdayDatas.data) {
let data = rec.data; let data = rec.data;
// let customerNum = data.presence === '进' ? 1 : 0; // ====== 旧格式(已弃用,保留备用) ======
let customerNum = data.current_total; // let customerNum = data.current_total;
// ====== 新格式使用 in ======
let customerNum = data.in ?? 0;
yesterdayCustomer += customerNum; yesterdayCustomer += customerNum;
} }
} }
......
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