const { query, queryOne } = require('../../db'); const METRIC_FIELD_MAP = { '正向有功总电能': 'forward_active_total_energy', '正向无功总电能': 'forward_reactive_total_energy', '总有功功率': 'total_active_power', '总无功功率': 'total_reactive_power', A相电压: 'phase_a_voltage', B相电压: 'phase_b_voltage', C相电压: 'phase_c_voltage', A相电流: 'phase_a_current', B相电流: 'phase_b_current', C相电流: 'phase_c_current', 总功率因数: 'total_power_factor', 水表读数: 'water_meter_reading', 用水量: 'water_meter_reading', 温度读数: 'temperature_reading', 平均温度: 'temperature_reading', 最高温度: 'temperature_reading', 最低温度: 'temperature_reading' }; const AGGREGATION_SQL = { avg: (field) => `avg(${field})`, sum: (field) => `sum(${field})`, max: (field) => `max(${field})`, min: (field) => `min(${field})` }; const AGGREGATION_RULES = new Set([...Object.keys(AGGREGATION_SQL), 'subtract']); const COLLECTION_FIELDS = new Set(Object.values(METRIC_FIELD_MAP)); const LINE_INTERVAL_SECONDS = new Set([5, 10, 15, 30, 60, 300, 600, 1800, 3600, 86400]); function splitIds(value) { if (Array.isArray(value)) { return value.map((item) => String(item).trim()).filter(Boolean); } return String(value || '') .split(',') .map((item) => item.trim()) .filter(Boolean); } function pad(value) { return String(value).padStart(2, '0'); } function formatDateTime(date) { return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; } function normalizeDateTime(value, fallbackDate) { if (!value) { return formatDateTime(fallbackDate); } const text = String(value).trim(); if (/^\d{4}-\d{2}-\d{2}$/.test(text)) { return `${text} 00:00:00`; } if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(text)) { return `${text}:00`; } if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(text)) { return `${text.replace('T', ' ')}:00`; } return text.replace('T', ' ').slice(0, 19); } function defaultRange() { const end = new Date(); const start = new Date(end.getTime() - 24 * 60 * 60 * 1000); return { beginTime: formatDateTime(start), endTime: formatDateTime(end) }; } function ok(data) { return { code: 200, msg: '查询成功', data }; } function emptyLine(beginTime, endTime, metric, interval) { return ok({ xAxis: [], series: [], devices: [], records: [], metric, beginTime, endTime, interval }); } async function getMetric(config, metricId) { const metric = metricId ? await queryOne( `select id, type, unit, metric_name, collection_field, aggregation_rule from energy_metric_library where id = :metricId and type = :deviceType limit 1`, { metricId, deviceType: String(config.deviceType) } ) : null; if (metric) { return { id: metric.id, type: String(metric.type), name: metric.metric_name, unit: metric.unit || '', aggregationRule: metric.aggregation_rule || config.defaultAggregationRule, field: COLLECTION_FIELDS.has(metric.collection_field) ? metric.collection_field : METRIC_FIELD_MAP[metric.metric_name] }; } return { id: '', type: String(config.deviceType), name: config.defaultMetricName, unit: config.defaultUnit || '', aggregationRule: config.defaultAggregationRule, field: config.defaultField }; } function normalizeAggregationRule(rule) { return AGGREGATION_RULES.has(rule) ? rule : 'avg'; } function normalizeInterval(value) { if (value === null || value === undefined || value === '') { return undefined; } const interval = Number(value); return LINE_INTERVAL_SECONDS.has(interval) ? interval : undefined; } function buildReportTimeSql(interval) { if (!interval) { return "date_format(report_time, '%Y-%m-%d %H:%i:%s')"; } return `date_format(from_unixtime(floor(unix_timestamp(report_time) / ${interval}) * ${interval}), '%Y-%m-%d %H:%i:%s')`; } function buildLocationMaps(rows) { const byId = new Map(rows.map((row) => [String(row.id), row])); const nameMap = new Map(); const locationMap = new Map(); function pathOf(row) { const names = []; let current = row; const visited = new Set(); while (current && !visited.has(String(current.id))) { visited.add(String(current.id)); if (current.name && current.isDevice !== '1' && current.isDeviceGroup !== '1') { names.unshift(current.name); } current = byId.get(String(current.pid)); } return names.join(' / '); } rows.forEach((row) => { if (row.deviceId) { nameMap.set(row.deviceId, row.name || row.deviceId); locationMap.set(row.deviceId, pathOf(row)); } }); return { nameMap, locationMap }; } function createMetricLineService(config) { async function devices() { const rows = await query( `select device_id as deviceId, name from \`floorInfo\` where del_flag = '0' and is_device = '1' and device_type = :deviceType and device_id is not null and device_id <> '' order by create_time asc, id asc`, { deviceType: String(config.deviceType) } ); return ok(rows.map((row) => ({ label: row.name || row.deviceId, value: row.deviceId }))); } async function line(queryParams = {}) { const range = defaultRange(); const beginTime = normalizeDateTime(queryParams.beginTime, new Date(range.beginTime)); const endTime = normalizeDateTime(queryParams.endTime, new Date(range.endTime)); const deviceIds = splitIds(queryParams.deviceIds); const metric = await getMetric(config, queryParams.metricId); const interval = normalizeInterval(queryParams.interval); if (!metric.field || !deviceIds.length) { return emptyLine(beginTime, endTime, metric, interval); } const aggregationRule = normalizeAggregationRule(metric.aggregationRule); const reportTimeSql = buildReportTimeSql(interval); const params = { beginTime, endTime }; const placeholders = deviceIds.map((deviceId, index) => { params[`deviceId${index}`] = deviceId; return `:deviceId${index}`; }); const nodeRows = await query( `select id, pid, name, is_device as isDevice, is_device_group as isDeviceGroup, device_id as deviceId from \`floorInfo\` where del_flag = '0' order by create_time asc, id asc` ); const { nameMap, locationMap } = buildLocationMaps(nodeRows); let rows; if (!interval) { rows = await query( `select device_id as deviceId, ${reportTimeSql} as reportTime, ${metric.field} as value from device_collection_data_info where report_time >= :beginTime and report_time <= :endTime and ${metric.field} is not null and device_id in (${placeholders.join(', ')}) order by report_time asc, id asc, device_id asc`, params ); } else if (aggregationRule === 'subtract') { rows = await query( `select deviceId, reportTime, max(case when rnDesc = 1 then metricValue end) - max(case when rnAsc = 1 then metricValue end) as value from ( select device_id as deviceId, ${reportTimeSql} as reportTime, ${metric.field} as metricValue, row_number() over ( partition by device_id, ${reportTimeSql} order by report_time asc, id asc ) as rnAsc, row_number() over ( partition by device_id, ${reportTimeSql} order by report_time desc, id desc ) as rnDesc from device_collection_data_info where report_time >= :beginTime and report_time <= :endTime and ${metric.field} is not null and device_id in (${placeholders.join(', ')}) ) source group by deviceId, reportTime order by reportTime asc, deviceId asc`, params ); } else { rows = await query( `select device_id as deviceId, ${reportTimeSql} as reportTime, ${AGGREGATION_SQL[aggregationRule](metric.field)} as value from device_collection_data_info where report_time >= :beginTime and report_time <= :endTime and ${metric.field} is not null and device_id in (${placeholders.join(', ')}) group by device_id, reportTime order by reportTime asc, device_id asc`, params ); } const xAxis = [...new Set(rows.map((row) => row.reportTime))]; const byDevice = new Map(); rows.forEach((row) => { if (!byDevice.has(row.deviceId)) { byDevice.set(row.deviceId, new Map()); } byDevice.get(row.deviceId).set(row.reportTime, Number(row.value)); }); const series = deviceIds.map((deviceId) => { const values = byDevice.get(deviceId) || new Map(); return { deviceId, name: nameMap.get(deviceId) || deviceId, type: 'line', smooth: true, connectNulls: true, data: xAxis.map((time) => values.get(time) ?? null) }; }); const records = rows.map((row) => ({ deviceId: row.deviceId, deviceName: nameMap.get(row.deviceId) || row.deviceId, deviceLocation: locationMap.get(row.deviceId) || '', reportTime: row.reportTime, metricName: metric.name, metricUnit: metric.unit, value: row.value === null || row.value === undefined ? null : Number(row.value) })); return ok({ xAxis, series, devices: deviceIds.map((deviceId) => ({ deviceId, name: nameMap.get(deviceId) || deviceId })), metric: { id: metric.id, name: metric.name, unit: metric.unit, aggregationRule }, records, beginTime, endTime, interval }); } return { devices, line }; } module.exports = { createMetricLineService };