You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

350 lines
10 KiB
JavaScript

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
};