完善客流数据接入

parent 42295692
......@@ -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_mac: 'C0:BA:1F:02:98:F6',
* device_name: 'People Counter',
* 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)'
* }
* device_info: { ... },
* line_trigger_data: [ { line: 1, line_name: 'Line1', line_uuid: '...', total: { in: 2, out: 2 } } ],
* time_info: { ... }
* }
*
* 旧格式(已弃用,注释保留备用):
* // region_trigger_data: { region_count_data: [ { region: 1, region_name: 'Region1', region_uuid: '...', total: { current_total: 9 } } ] }
*/
export async function customerDeviceData(message) {
try {
......@@ -152,14 +153,42 @@ export async function customerDeviceData(message) {
const timeInfo = data.time_info;
const deviceTime = timeInfo && timeInfo.time ? new Date(timeInfo.time) : new Date();
// ===== 提取客流数据 =====
const regionTriggerData = data.region_trigger_data;
if (!regionTriggerData || !regionTriggerData.region_count_data || regionTriggerData.region_count_data.length === 0) {
console.warn('消息缺少客流区域数据,跳过');
// ===== 提取客流数据(当前使用新格式 line_trigger_data) =====
let triggerItems: CustomerTriggerItem[] = [];
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;
}
const regionCountData = regionTriggerData.region_count_data;
const now = new Date();
// ===== 设备校验/新增 =====
......@@ -167,13 +196,13 @@ export async function customerDeviceData(message) {
TABLENAME.设备表, { device_id: deviceSn }
);
if (!deviceRow) {
// 设备不存在,为每个区域创建一条设备记录
for (const regionData of regionCountData) {
// 设备不存在,为每条线路创建一条设备记录
for (const item of triggerItems) {
await addData(
TABLENAME.设备表,
{
device_id: deviceSn,
region_key: regionData.region,
region_key: item.triggerKey,
device_type: '客流传感器',
device_name: deviceName,
control_params: data,
......@@ -181,32 +210,30 @@ export async function customerDeviceData(message) {
updated_at: now,
}
);
console.log(`客流设备已创建: ${deviceSn}, 区域 ${regionData.region}`);
console.log(`客流设备已创建: ${deviceSn}, 线路 ${item.triggerKey}`);
}
}
// 遍历每个区域的客流数据,逐条入库
for (const regionData of regionCountData) {
const regionUuid = regionData.region_uuid;
const regionName = regionData.region_name || '';
const regionNumber = regionData.region;
const currentTotal = regionData.total?.current_total ?? 0;
// 遍历每条线路的客流数据,逐条入库
for (const item of triggerItems) {
const inCount = item.total?.in ?? 0;
const outCount = item.total?.out ?? 0;
await addData(
TABLENAME.设备数据表,
{
device_id: deviceSn,
data: regionData.total,
data: item.total,
received_time: now,
device_time: deviceTime,
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) {
console.error('客流数据处理失败:', error);
......
......@@ -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 } {
let sum = 0, count = 0;
for (const rec of records) {
const d = rec.data || {};
if (d.current_total != null) {
sum += Number(d.current_total);
// ====== 新格式使用 in ======
if (d.in != null) {
sum += Number(d.in);
count++;
}
// ====== 旧格式(已弃用,保留备用) ======
// if (d.current_total != null) {
// sum += Number(d.current_total);
// count++;
// }
}
return { visitorSum: sum, visitorCount: count };
}
......
......@@ -296,43 +296,118 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
tvocTrend: tvocTrend
};
// 5.1 预警监控和预警工单处理进度分析
let alertStatus = {
totalAlerts: 3,
resolved: 1,
responded: 1,
unresolved: 1
};
let alertWorkOrders = [
{ riskContent: "空调压缩机异常", alertTime: "26/02/01", status: "已解决" },
{ riskContent: "温度传感器离线", alertTime: "26/02/01", status: "已响应" },
{ riskContent: "湿度超标", alertTime: "26/02/02", status: "未处理" }
];
let alertWorkThreeOrders = [
{ 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: "已响应" },
{ riskContent: "湿度超标", alertTime: "26/02/02", alertAddress: "A馆1F走廊", device_id: "2-4-0-0", faultCode: "13", status: "未处理" }
];
let alertWorkLevel = {
"一级预警": 1,
"二级预警": 1,
"三级预警": 1
};
let alertWorkType = [{ count: 1, name: "设备故障", proportion: '50%' },
{ count: 2, name: "环境异常", proportion: '50%' },
{ count: 0, name: "安全事件", proportion: '0%' }];
// 5.2. 预警监控数据
resultAny.alertManagement = {
alertStatus: alertStatus,
alertWorkLevel: alertWorkLevel,
alertWorkType: alertWorkType,
alertWorkOrders: alertWorkOrders
};
// 5.1 预警监控和预警工单处理进度分析(从 device_fault 表获取,近30天)
const faultStatusTextMap: { [key: number]: string } = { 0: "未处理", 1: "已响应", 2: "已解决" };
// 降级默认值:查询或统计失败时使用,保证其他模块不受影响
let alertStatus = { totalAlerts: 0, resolved: 0, responded: 0, unresolved: 0 };
let alertWorkLevel: { [key: string]: number } = { "一级预警": 0, "二级预警": 0, "三级预警": 0 };
let alertWorkType: any[] = [];
let alertWorkOrders: any[] = [];
try {
// 时间范围:近30天
const thirtyDaysAgo = new Date(nowTime.getTime() - 30 * 86400000);
const timeFilter = { "%gte%": formatLocalTime(thirtyDaysAgo) };
// ---- 统计查询:只取 status/level/fault_type,避免传输无用字段 ----
const statsRes = await selectDataListByParam(TABLENAME.设备故障表,
{ occurred_time: timeFilter },
["status", "level", "fault_type"]);
const statsFaults: any[] = Array.isArray(statsRes.data) ? statsRes.data : [];
const totalAlerts = statsFaults.length;
// alertStatus:按处理状态一次遍历统计
let resolved = 0, responded = 0, unresolved = 0;
for (const f of statsFaults) {
if (f.status === 2) resolved++;
else if (f.status === 1) responded++;
else if (f.status === 0) unresolved++;
}
alertStatus = { totalAlerts, resolved, responded, unresolved };
// 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%'
});
}
// 三级页面,预警信息展示不同
if (regionType) {
resultAny.alertManagement.alertWorkOrders = alertWorkThreeOrders;
// ---- 工单列表查询: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 = {
alertStatus,
alertWorkLevel,
alertWorkType,
alertWorkOrders,
};
// 6.1 人流监控及客流量趋势分析
let regionInfos: any = [];
......@@ -370,16 +445,25 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
"%orderAsc%": "device_time"
}, ["device_id", "data", "device_time"]);
// 统计区域类型的客流量
// 统计区域类型的客流量(新格式使用 in/out)
for (let rec of customerDeviceDatas.data) {
let data = rec.data;
// if (data.presence === '进') {
if (data.current_total) {
totalRegionCustomerNum += data.current_total;
// ====== 旧格式(已弃用,保留备用) ======
// if (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)) {
regionCustomerMap.set(regionName, data.current_total);
regionCustomerMap.set(regionName, netVisitor);
} 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) {
let yesterdayCustomer = 0;
// 计算每个区域的饱和度(客流量/区域最大承载量),并转换成表格
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)}%`;
regionCustomerTable.push({ regionName, customerNum, saturation });
todayCustomer += customerNum;
......@@ -419,8 +503,10 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
let d = new Date(rec.device_time);
let hourKey = `${d.getHours()}时`;
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)) {
hourlyCustomerMap.set(hourKey, customerNum);
} else {
......@@ -443,8 +529,10 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
}, ["device_id", "data", "device_time"]);
for (let rec of yesterdayDatas.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;
}
}
......
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