管理后台接口对接和点位

parent bae8c3b0
/**
* 客流体验页面 - 迪勤智能展厅环境指标
*
* 数据来源:迪勤 IEQ 传感器实时数据
* 计算标准:ISO 7730 / GB/T 18883 / GB 50034
* 产品逻辑:参见 室内环境多维度指标计算逻辑说明.md(Luis 整理)
*
* ══════════════════════════════════════════════════════
* 以下为开发文档参考用的原始假数据(保留不删):
*
* // 热湿环境
* rshj: { pmv: "+0.28", ppd: "5.6%", wd: "22.5℃", sd: "50%", pmvTrend: [...] }
* // 空气品质
* kqpz: { co2: "0.28", pm25: "5.6", tvoc: "22.5", pm10: "50", kqpzTrend: [...] }
* // 光环境
* ghj: { lux: "+0.28", ugr: "5.6%", ghjTrend: [...] }
* // 声环境
* shj: { dB: "28", nr: "NR-222", maxdB: "56", shjTrend: [...] }
* // 区域综合舒适度
* qyzhssd: [{ regionName: "办公室", pmv: "0.28", co2: "0.28", lux: "0.28", dB: "0.28", zhssd: "28", zb: "28%" }, ...]
* // 实时异常监控
* ssycjk: [{ timer: "2023-06-01 00:00:00", regionName: "办公室", remark: "描述" }, ...]
* // 数据汇总
* sjhz: { zhjkpz: "优", rshjdbl: "88%", kqpzdbl: "85%", ghjdbl: "90%", shjdbl: "80%" }
* ══════════════════════════════════════════════════════
*/
import { mysqlModelMap } from "../model/sqlModelBind";
import { mysqlDB } from "../db/mysqlInit";
import Sequelize from "sequelize";
import {
calcPMV, calcPPD, calcUGR, calcNR,
assessThermalEnvironment, assessAirQuality,
assessLightEnvironment, assessSoundEnvironment,
calcComplianceRate, calcOverallComfort,
} from "./customsIndicators";
const Op = Sequelize.Op;
/** 格式化日期为本地时间的 "YYYY-MM-DD HH:00:00"(小时级 key) */
function formatHourKey(d: Date): string {
const pad = (n: number) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:00:00`;
}
/** 格式化日期为本地时间的 "YYYY-MM-DD HH:mm:ss" */
function formatLocalDateTime(d: Date): string {
const pad = (n: number) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
/**
* 从 IEQ 传感器获取最新的环境数据
* 使用子查询绕过 ONLY_FULL_GROUP_BY 限制
*/
async function getLatestIEQData(): Promise<{
temperature: number; humidity: number;
co2: number; pm25: number; tvoc: number;
a31001: number; lightLevel: number; lightUniformity: number; a14263: number;
pm10?: number; pressure?: number;
}> {
const deviceModel = mysqlModelMap['device'];
if (!deviceModel) {
throw new Error('device 表模型未初始化');
}
// 查询所有 IEQ 传感器设备
const ieqDevices = await deviceModel.findAll({
where: { device_type: 'IEQ传感器' },
attributes: ['device_id', 'device_name', 'region_key'],
raw: true,
}) as any[];
if (!ieqDevices.length) {
return {
temperature: 0, humidity: 0, co2: 0, pm25: 0,
tvoc: 0, a31001: 0, lightLevel: 0, lightUniformity: 0, a14263: 0,
};
}
const deviceIds = ieqDevices.map(d => d.device_id);
// 使用子查询获取每台设备的最新数据,避免 ONLY_FULL_GROUP_BY 报错
const placeholders = deviceIds.map(() => '?').join(',');
const sql = `
SELECT dd.device_id, dd.device_data
FROM device_data dd
INNER JOIN (
SELECT device_id, MAX(device_time) AS max_time
FROM device_data
WHERE device_id IN (${placeholders})
GROUP BY device_id
) latest ON dd.device_id = latest.device_id AND dd.device_time = latest.max_time
`;
const latestData = await mysqlDB.query(sql, {
replacements: deviceIds,
type: Sequelize.QueryTypes.SELECT,
}) as any[];
// 汇总所有设备的最新数据
const sums: Record<string, number> = {};
const fieldCounts: Record<string, number> = {};
let minLight: number | null = null;
let count = 0;
for (const row of latestData) {
const data = row.device_data;
if (!data) continue;
for (const key of ['temperature', 'humidity', 'co2', 'pm25', 'tvoc', 'a31001', 'lightLevel', 'a14263', 'pm10', 'pressure']) {
const val = data[key];
if (val !== undefined && val !== null && typeof val === 'number') {
sums[key] = (sums[key] || 0) + val;
fieldCounts[key] = (fieldCounts[key] || 0) + 1;
}
}
// 记录最小照度值,用于计算均匀度
const lightVal = data['lightLevel'];
if (lightVal !== undefined && lightVal !== null && typeof lightVal === 'number') {
if (minLight === null || lightVal < minLight) minLight = lightVal;
}
count++;
}
if (count === 0) {
return {
temperature: 0, humidity: 0, co2: 0, pm25: 0,
tvoc: 0, a31001: 0, lightLevel: 0, lightUniformity: 0, a14263: 0,
};
}
const avg = (key: string): number => {
const cnt = fieldCounts[key] || 0;
return cnt > 0 ? Math.round((sums[key] / cnt) * 100) / 100 : 0;
};
const avgLight = avg('lightLevel');
// 照度均匀度 = 最小照度值 / 平均照度值
const lightUniformity = avgLight > 0 && minLight !== null
? Math.round((minLight / avgLight) * 100) / 100
: 0;
return {
temperature: avg('temperature'),
humidity: avg('humidity'),
co2: avg('co2'),
pm25: avg('pm25'),
tvoc: avg('tvoc'),
a31001: avg('a31001'),
lightLevel: avgLight,
lightUniformity,
a14263: avg('a14263'),
pm10: avg('pm10'),
pressure: avg('pressure'),
};
}
/**
* 获取近 24 小时的 PMV 趋势数据(每小时一个点)
*/
async function getPMVTrend(): Promise<Array<{ key: string; value: string }>> {
const dataModel = mysqlModelMap['device_data'];
const deviceModel = mysqlModelMap['device'];
if (!dataModel || !deviceModel) return [];
const ieqDevices = await deviceModel.findAll({
where: { device_type: 'IEQ传感器' },
attributes: ['device_id'],
raw: true,
}) as any[];
const deviceIds = ieqDevices.map(d => d.device_id);
if (!deviceIds.length) return [];
const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const records = await dataModel.findAll({
attributes: ['device_data', 'device_time'],
where: {
device_id: { [Op.in]: deviceIds },
device_time: { [Op.gte]: yesterday },
},
order: [['device_time', 'ASC']],
raw: true,
}) as any[];
// 按小时分组取平均
const hourlyMap: Record<string, { tempSum: number; humSum: number; count: number }> = {};
for (const r of records) {
if (!r.device_time || !r.device_data) continue;
const hourKey = formatHourKey(new Date(r.device_time));
const key = hourKey.replace('T', ' ');
const temp = r.device_data.temperature;
const hum = r.device_data.humidity;
if (temp === undefined || hum === undefined) continue;
if (!hourlyMap[key]) hourlyMap[key] = { tempSum: 0, humSum: 0, count: 0 };
hourlyMap[key].tempSum += temp;
hourlyMap[key].humSum += hum;
hourlyMap[key].count++;
}
return Object.entries(hourlyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, val]) => {
const avgTemp = val.tempSum / val.count;
const avgHum = val.humSum / val.count;
const pmv = calcPMV(avgTemp, avgHum);
return { key, value: pmv.toFixed(2) };
});
}
/**
* 获取近 24 小时的空气质量趋势
*/
async function getAirQualityTrend(): Promise<Array<{ key: string; co2: string; pm25: string }>> {
const dataModel = mysqlModelMap['device_data'];
const deviceModel = mysqlModelMap['device'];
if (!dataModel || !deviceModel) return [];
const ieqDevices = await deviceModel.findAll({
where: { device_type: 'IEQ传感器' },
attributes: ['device_id'],
raw: true,
}) as any[];
const deviceIds = ieqDevices.map(d => d.device_id);
if (!deviceIds.length) return [];
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
const records = await dataModel.findAll({
attributes: ['device_data', 'device_time'],
where: {
device_id: { [Op.in]: deviceIds },
device_time: { [Op.gte]: yesterday },
},
order: [['device_time', 'ASC']],
raw: true,
}) as any[];
const hourlyMap: Record<string, { co2Sum: number; pm25Sum: number; count: number }> = {};
for (const r of records) {
if (!r.device_time || !r.device_data) continue;
const key = formatHourKey(new Date(r.device_time));
const co2 = r.device_data.co2;
const pm25 = r.device_data.pm25;
if (co2 === undefined || pm25 === undefined) continue;
if (!hourlyMap[key]) hourlyMap[key] = { co2Sum: 0, pm25Sum: 0, count: 0 };
hourlyMap[key].co2Sum += co2;
hourlyMap[key].pm25Sum += pm25;
hourlyMap[key].count++;
}
return Object.entries(hourlyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, val]) => ({
key,
co2: (val.co2Sum / val.count).toFixed(2),
pm25: (val.pm25Sum / val.count).toFixed(2),
}));
}
/**
* 获取各区域的最新环境数据
*/
async function getRegionData(): Promise<Array<{
regionName: string; pmv: string; co2: string; lux: string; dB: string; zhssd: string; zb: string;
}>> {
const deviceModel = mysqlModelMap['device'];
const dataModel = mysqlModelMap['device_data'];
const regionModel = mysqlModelMap['region'];
if (!deviceModel || !dataModel || !regionModel) return [];
// 查询所有 IEQ 设备及其区域信息
const ieqDevices = await deviceModel.findAll({
where: { device_type: 'IEQ传感器' },
attributes: ['device_id', 'region_key'],
raw: true,
}) as any[];
if (!ieqDevices.length) return [];
// 查询区域名称
const regionKeys = [...new Set(ieqDevices.map(d => d.region_key).filter(Boolean))];
const regions = await regionModel.findAll({
where: { region_key: { [Op.in]: regionKeys } },
attributes: ['region_key', 'region_name'],
raw: true,
}) as any[];
const regionNameMap: Record<string, string> = {};
for (const r of regions) {
regionNameMap[r.region_key] = r.region_name;
}
// 按区域分组查询最新数据(使用子查询避免 ONLY_FULL_GROUP_BY)
const deviceIds = ieqDevices.map(d => d.device_id);
const placeholders = deviceIds.map(() => '?').join(',');
const latestData = await mysqlDB.query(`
SELECT dd.device_id, dd.device_data
FROM device_data dd
INNER JOIN (
SELECT device_id, MAX(device_time) AS max_time
FROM device_data
WHERE device_id IN (${placeholders})
GROUP BY device_id
) latest ON dd.device_id = latest.device_id AND dd.device_time = latest.max_time
`, {
replacements: deviceIds,
type: Sequelize.QueryTypes.SELECT,
}) as any[];
// 按 region_key 汇总
const regionAgg: Record<string, { temp: number[]; hum: number[]; co2: number[]; lux: number[]; a14263: number[] }> = {};
const deviceRegionMap: Record<string, string> = {};
for (const d of ieqDevices) {
deviceRegionMap[d.device_id] = d.region_key;
}
for (const row of latestData) {
const rk = deviceRegionMap[row.device_id];
if (!rk || !row.device_data) continue;
if (!regionAgg[rk]) regionAgg[rk] = { temp: [], hum: [], co2: [], lux: [], a14263: [] };
const d = row.device_data;
if (d.temperature !== undefined) regionAgg[rk].temp.push(d.temperature);
if (d.humidity !== undefined) regionAgg[rk].hum.push(d.humidity);
if (d.co2 !== undefined) regionAgg[rk].co2.push(d.co2);
if (d.lightLevel !== undefined) regionAgg[rk].lux.push(d.lightLevel);
if (d.a14263 !== undefined) regionAgg[rk].a14263.push(d.a14263);
}
const avg = (arr: number[]) => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
return Object.entries(regionAgg).map(([rk, vals]) => {
const temp = avg(vals.temp);
const hum = avg(vals.hum);
const co2 = avg(vals.co2);
const lux = avg(vals.lux);
const noise = avg(vals.a14263);
const pmv = calcPMV(temp, hum);
// 计算该区域综合舒适度(简化:四项指标达标率平均)
const dims = [
temp >= 18 && temp <= 28 ? 1 : 0,
co2 >= 0 && co2 <= 1000 ? 1 : 0,
lux >= 300 ? 1 : 0,
noise >= 0 && noise <= 55 ? 1 : 0,
];
const rate = dims.filter(Boolean).length / dims.length;
const zhssd = (rate * 100).toFixed(2);
const zb = Math.round(rate * 100) + '%';
return {
regionName: regionNameMap[rk] || rk,
pmv: pmv.toFixed(2),
co2: co2.toFixed(2),
lux: lux.toFixed(2),
dB: noise.toFixed(2),
zhssd,
zb,
};
});
}
/**
* 获取近 24 小时某字段的每小时均值趋势
*/
async function getFieldTrend(field: string): Promise<Array<{ key: string; value: string }>> {
const dataModel = mysqlModelMap['device_data'];
const deviceModel = mysqlModelMap['device'];
if (!dataModel || !deviceModel) return [];
const ieqDevices = await deviceModel.findAll({
where: { device_type: 'IEQ传感器' },
attributes: ['device_id'],
raw: true,
}) as any[];
const deviceIds = ieqDevices.map(d => d.device_id);
if (!deviceIds.length) return [];
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
const records = await dataModel.findAll({
attributes: ['device_data', 'device_time'],
where: {
device_id: { [Op.in]: deviceIds },
device_time: { [Op.gte]: yesterday },
},
order: [['device_time', 'ASC']],
raw: true,
}) as any[];
const hourlyMap: Record<string, { sum: number; count: number }> = {};
for (const r of records) {
if (!r.device_time || !r.device_data) continue;
const key = formatHourKey(new Date(r.device_time));
const val = r.device_data[field];
if (val === undefined || val === null || typeof val !== 'number') continue;
if (!hourlyMap[key]) hourlyMap[key] = { sum: 0, count: 0 };
hourlyMap[key].sum += val;
hourlyMap[key].count++;
}
return Object.entries(hourlyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, val]) => ({
key,
value: (val.sum / val.count).toFixed(2),
}));
}
/**
* 获取近 24 小时某字段的最大值
*/
async function getFieldMax24h(field: string): Promise<number> {
const dataModel = mysqlModelMap['device_data'];
const deviceModel = mysqlModelMap['device'];
if (!dataModel || !deviceModel) return 0;
const ieqDevices = await deviceModel.findAll({
where: { device_type: 'IEQ传感器' },
attributes: ['device_id'],
raw: true,
}) as any[];
const deviceIds = ieqDevices.map(d => d.device_id);
if (!deviceIds.length) return 0;
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
const records = await dataModel.findAll({
attributes: ['device_data', 'device_time'],
where: {
device_id: { [Op.in]: deviceIds },
device_time: { [Op.gte]: yesterday },
},
order: [['device_time', 'ASC']],
raw: true,
}) as any[];
let maxVal = 0;
for (const r of records) {
if (!r.device_data) continue;
const val = r.device_data[field];
if (val !== undefined && val !== null && typeof val === 'number' && val > maxVal) {
maxVal = val;
}
}
return Math.round(maxVal * 100) / 100;
}
/**
* 客流体验分析 - 返回大屏所需的所有真实指标数据
*/
export async function getCustomsAnalysis() {
const sensorData = await getLatestIEQData();
const pmvTrend = await getPMVTrend();
const airTrend = await getAirQualityTrend();
const lightTrend = await getFieldTrend('lightLevel');
const noiseTrend = await getFieldTrend('a14263');
const maxNoise = await getFieldMax24h('a14263');
const regionData = await getRegionData();
// 计算光环境区域达标率(照度 ≥ 200 lx 的区域占比)
const lightRegions = regionData.filter(r => {
const lux = parseFloat(r.lux);
return !isNaN(lux);
});
const passedLightRegions = lightRegions.filter(r => parseFloat(r.lux) >= 200).length;
// 计算衍生指标
const pmv = calcPMV(sensorData.temperature, sensorData.humidity);
const ppd = calcPPD(pmv);
const ugr = calcUGR(sensorData.lightLevel);
const nr = calcNR(sensorData.a14263);
// 四维度达标评估
const thermal = assessThermalEnvironment(sensorData.temperature, sensorData.humidity, pmv, ppd);
const air = assessAirQuality(sensorData.co2, sensorData.pm25, sensorData.tvoc, sensorData.a31001);
const light = assessLightEnvironment(passedLightRegions, lightRegions.length, ugr);
const sound = assessSoundEnvironment(sensorData.a14263, nr);
// 达标率
const thermalRate = thermal.passed / thermal.total;
const airRate = air.passed / air.total;
const lightRate = light.passed / light.total;
const soundRate = sound.passed / sound.total;
const overall = calcOverallComfort(thermalRate, airRate, lightRate, soundRate);
// 构造异常监控(仅返回不达标的指标)
const anomalies: Array<{ timer: string; regionName: string; remark: string }> = [];
const now = formatLocalDateTime(new Date());
for (const [name, result] of Object.entries(thermal.details)) {
if (!result.status) anomalies.push({ timer: now, regionName: '热湿环境', remark: `${name} ${result.value} 不达标` });
}
for (const [name, result] of Object.entries(air.details)) {
if (!result.status) anomalies.push({ timer: now, regionName: '空气质量', remark: `${name} ${result.value} 不达标` });
}
// 光环境:照度不达标的区域逐条列出
const lightAnomalyRegions = regionData.filter(r => {
const lux = parseFloat(r.lux);
return !isNaN(lux) && lux < 200;
});
for (const r of lightAnomalyRegions) {
anomalies.push({ timer: now, regionName: r.regionName, remark: `照度 ${r.lux} lx 低于展厅标准 200 lx` });
}
for (const [name, result] of Object.entries(sound.details)) {
if (!result.status) anomalies.push({ timer: now, regionName: '声环境', remark: `${name} ${result.value} 不达标` });
}
// 噪音 24h 峰值告警(> 65 dB(A))
if (maxNoise > 65) {
anomalies.push({ timer: now, regionName: '声环境', remark: `噪音 24h 峰值 ${maxNoise} dB(A) 超过告警阈值 65 dB(A)` });
}
// 趋势数据(取最近 24 个点,避免数据太多)
const trendLimit = 24;
const pmvTrendSliced = pmvTrend.slice(-trendLimit);
const airTrendSliced = airTrend.slice(-trendLimit);
const lightTrendSliced = lightTrend.slice(-trendLimit);
const noiseTrendSliced = noiseTrend.slice(-trendLimit);
return {
rshj: {
pmv: (pmv >= 0 ? '+' : '') + pmv.toFixed(2),
ppd: ppd.toFixed(1) + '%',
wd: sensorData.temperature.toFixed(1) + '℃',
sd: sensorData.humidity.toFixed(1) + '%',
pmvTrend: pmvTrendSliced,
},
kqpz: {
co2: sensorData.co2.toFixed(2),
pm25: sensorData.pm25.toFixed(2),
tvoc: sensorData.tvoc.toFixed(2),
pm10: (sensorData.pm10 || 0).toFixed(2),
kqpzTrend: airTrendSliced,
},
ghj: {
lux: light.regionRate,
ugr: ugr.toFixed(1),
ghjTrend: lightTrendSliced,
},
shj: {
dB: sensorData.a14263.toFixed(2),
nr: 'NR-' + Math.round(nr),
maxdB: maxNoise.toFixed(2),
shjTrend: noiseTrendSliced,
},
qyzhssd: regionData,
ssycjk: anomalies,
sjhz: {
zhjkpz: overall.level,
rshjdbl: calcComplianceRate(thermal.passed, thermal.total),
kqpzdbl: calcComplianceRate(air.passed, air.total),
ghjdbl: light.regionRate,
shjdbl: calcComplianceRate(sound.passed, sound.total),
},
};
}
\ No newline at end of file
/**
* 室内环境多维度指标计算模块
* 基于 ISO 7730 / GB/T 18883 / GB 50034 等标准
*
* 产品逻辑文档参考:室内环境多维度维度指标计算逻辑说明.md
*/
// ==================== 常量定义 ====================
/** 国标舒适区间(展厅场景) */
export const STANDARDS = {
temperature: { min: 18, max: 28, unit: '℃' },
humidity: { min: 40, max: 70, unit: '%RH' },
co2: { min: 0, max: 1000, unit: 'ppm' },
pm25: { min: 0, max: 75, unit: 'μg/m³' },
tvoc: { min: 0, max: 0.6, unit: 'mg/m³' },
hcho: { min: 0, max: 0.1, unit: 'mg/m³' },
lightLevel: { min: 200, max: 2000, unit: 'lx' }, // 展厅 ≥ 200 lx
noise: { min: 0, max: 60, unit: 'dB(A)' }, // 展厅 ≤ 60 dB(A)
};
/** PMV 舒适区间 [-0.5, +0.5] */
const PMV_COMFORT_MIN = -0.5;
const PMV_COMFORT_MAX = 0.5;
/** PPD 限值 ≤ 20% */
const PPD_LIMIT = 20;
/** UGR 展厅限值 ≤ 22 */
const UGR_LIMIT = 22;
/** NR 办公场所限值 ≤ 40 */
const NR_LIMIT = 40;
/** 噪音 24h 峰值告警阈值 > 65 dB(A) */
export const NOISE_ALERT_THRESHOLD = 65;
// ==================== PMV 计算(ISO 7730) ====================
/**
* 计算 PMV(预测平均投票数)
* 基于 ISO 7730 标准简化实现
*
* @param ta 空气温度 (℃)
* @param rh 相对湿度 (%)
* @param tr 平均辐射温度 (℃),默认等于空气温度
* @param vel 空气流速 (m/s),默认 0.1
* @param met 代谢率 (met),默认 1.2(办公室静坐)
* @param clo 服装热阻 (clo),默认 0.5(夏季轻薄服装)
*/
export function calcPMV(
ta: number,
rh: number,
tr: number = ta,
vel: number = 0.1,
met: number = 1.2,
clo: number = 0.5
): number {
// 单位转换
const metW = met * 58.15; // met → W/m²
const cloI = clo * 0.155; // clo → m²·K/W
// 服装表面积系数
const fCl = clo <= 0.078
? 1.00 + 1.290 * cloI
: 1.05 + 0.645 * cloI;
// 对流换热系数
const hc = 2.38 * Math.pow(Math.abs(ta - 35.7 + 0.028 * metW), 0.25)
+ 12.1 * Math.sqrt(vel);
const hcFinal = Math.max(hc, 12.1 * Math.sqrt(vel));
// 服装表面温度(迭代求解)
let tCl = 35.7 - 0.028 * metW - cloI * (
3.96e-8 * fCl * (Math.pow((35.7 - 0.028 * metW + 273), 4) - Math.pow((tr + 273), 4))
+ fCl * hcFinal * (35.7 - 0.028 * metW - ta)
);
// 迭代收敛
for (let i = 0; i < 20; i++) {
const tClNew = 35.7 - 0.028 * metW - cloI * (
3.96e-8 * fCl * (Math.pow((tCl + 273), 4) - Math.pow((tr + 273), 4))
+ fCl * hcFinal * (tCl - ta)
);
if (Math.abs(tClNew - tCl) < 0.001) break;
tCl = tClNew;
}
// 辐射热损失
const R = 3.96e-8 * fCl * (Math.pow((tCl + 273), 4) - Math.pow((tr + 273), 4));
// 对流热损失
const C = fCl * hcFinal * (tCl - ta);
// 呼吸显热损失
const ERes = 0.0014 * metW * (34 - ta);
// 呼吸潜热损失
const pa = rh * 10 * Math.exp(16.6536 - 4030.183 / (ta + 235));
const EResL = 0.0173 * metW * (5.87 - pa);
// 皮肤蒸发散热
const w = 0.06; // 皮肤湿润度
const E = 3.05e-3 * (5733 - 6.99 * metW - pa) + 0.42 * (metW - 58.15);
const EMax = 16.7 * hcFinal * (pa - 5.87 - 0.5 * (35.7 - ta));
const wActual = Math.min(w, E / Math.max(EMax, 0.1));
const EAct = wActual * EMax;
// 热平衡
const H = metW - 0; // 假设外功 W = 0
const L = H - (R + C + ERes + EResL + EAct);
// PMV
const pmv = (0.303 * Math.exp(-0.036 * metW) + 0.028) * L;
return Math.round(pmv * 100) / 100;
}
/**
* 计算 PPD(预测不满意百分数)
* PPD = 100 - 95 × e^(-0.03353 × PMV⁴ - 0.2179 × PMV²)
*/
export function calcPPD(pmv: number): number {
const ppd = 100 - 95 * Math.exp(-0.03353 * Math.pow(pmv, 4) - 0.2179 * Math.pow(pmv, 2));
return Math.round(ppd * 10) / 10;
}
// ==================== UGR 估算(简化版) ====================
/**
* 估算 UGR(统一眩光值)
* 简化版:基于照度水平和均匀度估算
* 办公室标准 UGR ≤ 19
*
* @param lightLevel 照度 (lx)
* @param uniformity 照度均匀度,默认 0.6(常规办公室)
*/
export function calcUGR(lightLevel: number, uniformity: number = 0.6): number {
if (lightLevel <= 0) return 25; // 无光照时视为高眩光
// 简化模型:照度越低、均匀度越差,UGR 越高
const baseUgr = 22 - Math.min(lightLevel / 100, 10);
const uniformityPenalty = (0.7 - Math.min(uniformity, 0.7)) * 10;
const ugr = baseUgr + uniformityPenalty;
return Math.round(Math.max(10, Math.min(ugr, 28)) * 10) / 10;
}
// ==================== NR 计算 ====================
/**
* 计算 NR(噪声评价数)
* NR ≈ dB(A) - 5(ISO 推荐简化公式)
* 办公场所 NR ≤ 40
*/
export function calcNR(noiseDb: number): number {
const nr = Math.max(0, noiseDb - 5);
return Math.round(nr * 10) / 10;
}
// ==================== 达标判定 ====================
/** 判定单指标是否达标 */
export function isCompliant(value: number, standard: { min: number; max: number }): boolean {
return value >= standard.min && value <= standard.max;
}
/** 计算达标率 */
export function calcComplianceRate(passed: number, total: number): string {
if (total <= 0) return '0%';
return Math.round((passed / total) * 100) + '%';
}
// ==================== 综合舒适度 ====================
/**
* 计算综合舒适度评分(0-100)
* 基于四维度达标率的加权平均
*/
export function calcOverallComfort(
thermalRate: number, // 热湿环境达标率 (0-1)
airRate: number, // 空气质量达标率 (0-1)
lightRate: number, // 光环境达标率 (0-1)
soundRate: number, // 声环境达标率 (0-1)
): { score: number; level: string } {
// 加权:热湿 0.3, 空气 0.3, 光 0.2, 声 0.2
const score = Math.round(
(thermalRate * 0.3 + airRate * 0.3 + lightRate * 0.2 + soundRate * 0.2) * 100
);
let level: string;
if (score >= 90) level = '优';
else if (score >= 75) level = '良好';
else if (score >= 60) level = '一般';
else level = '差';
return { score, level };
}
// ==================== 维度判定 ====================
/** 热湿环境判定:温度、湿度、PMV、PPD */
export function assessThermalEnvironment(
temp: number, humidity: number, pmv: number, ppd: number
): { passed: number; total: number; details: Record<string, { value: string; status: boolean }> } {
const details: Record<string, { value: string; status: boolean }> = {};
details['温度'] = {
value: temp + '℃',
status: isCompliant(temp, STANDARDS.temperature),
};
details['湿度'] = {
value: humidity + '%RH',
status: isCompliant(humidity, STANDARDS.humidity),
};
details['PMV'] = {
value: pmv.toFixed(2),
status: pmv >= PMV_COMFORT_MIN && pmv <= PMV_COMFORT_MAX,
};
details['PPD'] = {
value: ppd.toFixed(1) + '%',
status: ppd <= PPD_LIMIT,
};
const passed = Object.values(details).filter(d => d.status).length;
return { passed, total: 4, details };
}
/** 空气质量判定:CO₂、PM2.5、TVOC、甲醛 */
export function assessAirQuality(
co2: number, pm25: number, tvoc: number, hcho: number
): { passed: number; total: number; details: Record<string, { value: string; status: boolean }> } {
const details: Record<string, { value: string; status: boolean }> = {};
details['CO₂'] = {
value: co2 + ' ppm',
status: isCompliant(co2, STANDARDS.co2),
};
details['PM2.5'] = {
value: pm25 + ' μg/m³',
status: isCompliant(pm25, STANDARDS.pm25),
};
details['TVOC'] = {
value: tvoc + ' mg/m³',
status: isCompliant(tvoc, STANDARDS.tvoc),
};
details['甲醛'] = {
value: hcho + ' mg/m³',
status: isCompliant(hcho, STANDARDS.hcho),
};
const passed = Object.values(details).filter(d => d.status).length;
return { passed, total: 4, details };
}
/** 光环境判定:区域达标率 + UGR */
export function assessLightEnvironment(
passedRegions: number, totalRegions: number, ugr: number
): { passed: number; total: number; details: Record<string, { value: string; status: boolean }>; regionRate: string } {
const rate = totalRegions > 0 ? (passedRegions / totalRegions * 100).toFixed(1) + '%' : '0%';
const details: Record<string, { value: string; status: boolean }> = {};
details['照度区域达标率'] = {
value: `${passedRegions}/${totalRegions} (${rate})`,
status: true, // 如实展示,不设达标/不达标
};
details['UGR'] = {
value: ugr.toFixed(1),
status: ugr <= UGR_LIMIT,
};
// 光环境达标率 = 照度达标区域占比(作为主指标)
const passed = passedRegions;
const total = totalRegions;
return { passed, total, details, regionRate: rate };
}
/** 声环境判定:dB(A)、NR */
export function assessSoundEnvironment(
noise: number, nr: number
): { passed: number; total: number; details: Record<string, { value: string; status: boolean }> } {
const details: Record<string, { value: string; status: boolean }> = {};
details['声级'] = {
value: noise + ' dB(A)',
status: isCompliant(noise, STANDARDS.noise),
};
details['NR'] = {
value: nr.toFixed(1),
status: nr <= NR_LIMIT,
};
const passed = Object.values(details).filter(d => d.status).length;
return { passed, total: 2, details };
}
\ No newline at end of file
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
getIndoorUnitAlarmErrorHis, getIndoorUnitAlarmErrorHis,
getFaultCodeInfo, getFaultCodeInfo,
getFaultTypeName, getFaultTypeName,
saveRoomExpensesToExpansion,
} from "./feiyiClient"; } from "./feiyiClient";
import { import {
getLocations, getLocations,
...@@ -651,6 +652,9 @@ export async function deviceData() { ...@@ -651,6 +652,9 @@ export async function deviceData() {
await integrateAcDeviceData(); await integrateAcDeviceData();
await integrateMeterDeviceData(); await integrateMeterDeviceData();
console.log('[设备数据集成] 完成'); console.log('[设备数据集成] 完成');
// 电费数据集成:拉取当天房间电费日使用详情,去重后写入 region_expenses
await saveRoomExpensesToExpansion(getTodayDateStr());
console.log('[电费数据集成] 完成');
} catch (err) { } catch (err) {
console.error('[设备数据集成] 执行异常:', err); console.error('[设备数据集成] 执行异常:', err);
} }
......
...@@ -497,6 +497,39 @@ export async function batchEditDevices(params: { ...@@ -497,6 +497,39 @@ export async function batchEditDevices(params: {
// ======= 设备表本地信息编辑(不涉及飞奕API) ======= // ======= 设备表本地信息编辑(不涉及飞奕API) =======
/** /**
* 获取单台设备表详情
* @param params deviceId 必填
* @returns { deviceId, deviceName, regionKey, deviceType, deviceState, linkageStart }
*/
export async function getDeviceDetail(params: { deviceId: string }) {
const { deviceId } = params;
if (!deviceId) {
throw new BizError(ERRORENUM.参数错误, "缺少必填参数:deviceId");
}
// 查询设备表
const deviceRes = await selectOneDataByParam(
TABLENAME.设备表,
{ device_id: deviceId },
["device_id", "device_name", "region_key", "device_type", "device_state", "linkage_start"]
);
const device = deviceRes.data as any;
if (!device || !device.device_id) {
throw new BizError(ERRORENUM.参数错误, `设备 ${deviceId} 不存在`);
}
return {
deviceId: device.device_id,
deviceName: device.device_name || '',
regionKey: device.region_key !== undefined && device.region_key !== null ? String(device.region_key) : '',
deviceType: device.device_type || '',
deviceState: device.device_state !== undefined && device.device_state !== null ? String(device.device_state) : '',
linkageStart: device.linkage_start ?? 'on',
};
}
/**
* 新增单台设备表信息 * 新增单台设备表信息
* @param params deviceId 必填;其余字段可省略(有默认值) * @param params deviceId 必填;其余字段可省略(有默认值)
* @returns { isSuccess, deviceId } * @returns { isSuccess, deviceId }
...@@ -504,7 +537,7 @@ export async function batchEditDevices(params: { ...@@ -504,7 +537,7 @@ export async function batchEditDevices(params: {
export async function addLocalDevice(params: { export async function addLocalDevice(params: {
deviceId: string; deviceId: string;
deviceName?: string; deviceName?: string;
regionKey?: number; regionKey?: string;
deviceType?: string; deviceType?: string;
deviceState?: string; deviceState?: string;
linkageStart?: string; linkageStart?: string;
...@@ -547,7 +580,7 @@ export async function addLocalDevice(params: { ...@@ -547,7 +580,7 @@ export async function addLocalDevice(params: {
export async function editLocalDevice(params: { export async function editLocalDevice(params: {
deviceId: string; deviceId: string;
deviceName?: string; deviceName?: string;
regionKey?: number; regionKey?: string;
deviceType?: string; deviceType?: string;
deviceState?: string; deviceState?: string;
linkageStart?: string; linkageStart?: string;
......
...@@ -9,6 +9,11 @@ import { BizError } from '../util/bizError'; ...@@ -9,6 +9,11 @@ import { BizError } from '../util/bizError';
import { ERRORENUM } from '../config/errorEnum'; import { ERRORENUM } from '../config/errorEnum';
import { FEIYI } from '../config/clientEnum'; import { FEIYI } from '../config/clientEnum';
import { systemConfig } from '../config/serverConfig'; import { systemConfig } from '../config/serverConfig';
import { addData } from '../data/addData';
import { selectDataListByParam } from '../data/findData';
import { TABLENAME } from '../config/dbEnum';
import path from 'path';
import XLSX from 'xlsx';
// 全局变量缓存 token 和过期时间 // 全局变量缓存 token 和过期时间
let feiYiToken = ""; let feiYiToken = "";
...@@ -478,3 +483,181 @@ export function getFaultTypeName(alarmType: number | string): string { ...@@ -478,3 +483,181 @@ export function getFaultTypeName(alarmType: number | string): string {
const num = typeof alarmType === 'string' ? parseInt(alarmType, 10) : alarmType; const num = typeof alarmType === 'string' ? parseInt(alarmType, 10) : alarmType;
return getFaultTypeReverseMap().get(num) || String(alarmType); return getFaultTypeReverseMap().get(num) || String(alarmType);
} }
/**
* 读取 res/电表对应区域.xlsx,构建 房间ID(空调内机所属房间) → 电表设备编号 的映射
* 说明:该文件是电表与区域的原始映射数据,比 device 表更全面(一台电表对应多个房间/内机)
* 列结构:电表设备编号 | 空调外机地址 | 空调内机地址 | 空调内机所属房间 | 区域名称
* 已验证:一个房间唯一对应一台电表(roomId → 电表设备编号 1:1)
*/
function loadMeterRoomMap(): Map<string, string> {
const map = new Map<string, string>();
try {
const filePath = path.resolve(__dirname, '../../res/电表对应区域.xlsx');
const workbook = XLSX.readFile(filePath);
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rows: any[] = XLSX.utils.sheet_to_json(sheet);
for (const row of rows) {
const meterCode = row['电表设备编号'];
const roomId = row['空调内机所属房间'];
if (meterCode && roomId) {
map.set(String(roomId), String(meterCode));
}
}
console.log(`[工具] 电表对应区域.xlsx 读取完成,共 ${map.size} 条 房间→电表 映射`);
} catch (err) {
console.error('[工具] 读取电表对应区域.xlsx 失败:', err);
}
return map;
}
/**
* 将"房间电费日使用详情"写入设备扩展表 device_expansion
* 逻辑:
* 1. 调用 getRoomExpensesList 分页拉取指定日期的房间电费数据
* 2. 通过 region.room_id 关联本地区域,得到 region_key
* 3. 匹配电表设备:优先用 电表对应区域.xlsx(原始数据)的 房间→电表 映射;
* 若 xlsx 读取失败/为空,回退按区域查 device 表中 device_type 匹配的设备
* 4. 每条电费记录 → 匹配到的电表设备写一条 device_expansion
* original_data 形如 { power: 114.761593, electricityUsageFee: 114.761593, deviceTime: 1706544000000 }
* @param occurrenceTime 日期 yyyyMMdd(必填)
* @param deviceType 设备类型,默认"电能监测"
* @returns { total, success, skip } 处理统计
*/
/**
* 区域电费数据写入 region_expenses 表
* 拉取飞奕"房间电费日使用详情",按 roomId(= region_key)直接写入区域电费表
*
* @param occurrenceTime 日期(格式 yyyyMMdd)
* @returns { total, success, skip }
*/
export async function saveRoomExpensesToExpansion(occurrenceTime: string): Promise<any> {
if (!occurrenceTime) {
throw new BizError(ERRORENUM.参数错误, "日期 occurrenceTime 不能为空(格式 yyyyMMdd)");
}
// 1. 分页拉取房间电费日使用详情
const pageSize = 100;
const allRows: any[] = [];
let page = 1;
while (true) {
const res: any = await getRoomExpensesList({ page, limit: pageSize, occurrenceTime });
const rows = res?.rows || [];
allRows.push(...rows);
const total = res?.total || allRows.length;
if (allRows.length >= total || rows.length === 0) break;
page++;
}
console.log(`拉取房间电费记录 ${allRows.length} 条(日期 ${occurrenceTime})`);
// 2. 通过 region_key 关联本地区域(飞奕 roomId = region_key)
const roomIds = [...new Set(allRows.map((r: any) => r.roomId))];
const regions = roomIds.length > 0
? (await selectDataListByParam(TABLENAME.区域表, { region_key: { "%in%": roomIds } }, ["region_key"])).data || []
: [];
const validRegionKeys = new Set(regions.map((r: any) => r.region_key));
// 3. 去重:查 region_expenses 中同一区域+同一日期已有记录
const existingKeys = new Set<string>();
if (validRegionKeys.size > 0) {
const existingRows = (await selectDataListByParam(TABLENAME.区域电费表, {
region_key: { "%in%": [...validRegionKeys] },
expense_date: occurrenceTime,
}, ["region_key"])).data || [];
existingRows.forEach((r: any) => {
existingKeys.add(r.region_key);
});
}
// 4. 组装 region_expenses 数据
const expensesData: any[] = [];
let skip = 0;
allRows.forEach((row: any) => {
const regionKey = row.roomId;
if (!regionKey || !validRegionKeys.has(regionKey)) { skip++; return; }
// 去重:同一区域+同一日期已存在则跳过
if (existingKeys.has(regionKey)) { skip++; return; }
expensesData.push({
region_key: regionKey,
use_power: row.usePower ?? 0,
electricity_fee: row.electricityUsageFee ?? 0,
energy_loss: row.energyLoss ?? 0,
loss_fee: row.lossEnergyFee ?? 0,
expense_date: occurrenceTime,
device_time: row.deviceTime ? new Date(row.deviceTime) : null,
original_data: row,
created_at: new Date(),
});
});
if (expensesData.length > 0) {
await addData(TABLENAME.区域电费表, expensesData);
}
console.log(`写入区域电费表 ${expensesData.length} 条,跳过 ${skip} 条(未匹配到区域或已存在)`);
return { total: allRows.length, success: expensesData.length, skip };
}
/**
* 单个内机分摊电量和电费
* 查询指定内机(或房间)在某一日的分摊用电量、电费、损耗等
* @param params { page, limit, roomId, indoorUnitId, indoorUnitAddressFull, occurrenceTime(yyyyMMdd) }
* @returns result.data { page, limit, total, rows: [{ indoorUnitId, roomId, floorId, buildingId, roomName, floorName, buildingName, tenantUserName, electricityPrice(电价), usePower(用电量), electricityUsageFee(电费), energyLoss(损耗电量), lossEnergyFee(损耗电费), deviceTime }] }
{
"code": "00000",
"message": "success",
"data": {
"page": 1,
"limit": 10,
"total": 9,
"rows": [{
"indoorUnitId": "97485639955775488", //内机ID
"roomId": "97474792772468736", //房间ID
"electricityPrice": 1.000, //电价
"usePower": 17457.00, //用电量
"electricityUsageFee": 17457.00, //电费
"energyLoss": 0.00, //损耗电量
"lossEnergyFee": 0.00, //损耗电费
"deviceTime": 1704423600000 //设备时间
}]
},
"timestamp": 1714465179333
}
*/
export async function getIndoorUnitExpenses(params: any): Promise<any> {
const path = '/openApi/indoorUnit/meter/expenses';
const body = { ...params };
const result = await feiYiPost(path, body);
return result.data;
}
/**
* 房间电费日使用详情
* 按房间/楼层/楼栋维度查询某日的用电量和电费
* @param params { page, limit, roomIds, floorIds, buildingIds, occurrenceTime(yyyyMMdd) }
* @returns result.data { page, limit, total, rows: [{ buildingId, floorId, roomId, usePower(房间用电量), electricityUsageFee(房间电费), deviceTime }] }
{
"code": "00000",
"message": "success",
"data": {
"page": 1,
"limit": 5,
"total": 9730,
"rows": [{
"buildingId": "97474832886792192", //楼栋ID
"floorId": "97474833251696640", //楼层ID
"roomId": "97474833612406784", //房间ID
"usePower": 114.761593, //房间用电量
"electricityUsageFee": 114.761593, //房间电费
"deviceTime": 1706544000000 //记录时间
}]
},
"timestamp": 1713255660803
}
*/
export async function getRoomExpensesList(params: any): Promise<any> {
const path = '/openApi/indoorUnit/meter/expenses/list';
const body = { ...params };
const result = await feiYiPost(path, body);
return result.data;
}
\ No newline at end of file
...@@ -62,6 +62,9 @@ export async function readRegionAxis(gatewayPage: number, hour: number) { ...@@ -62,6 +62,9 @@ export async function readRegionAxis(gatewayPage: number, hour: number) {
} else if (gatewayPage === 2) { } else if (gatewayPage === 2) {
let monitorSheet = sheetMap["智能监控"]; let monitorSheet = sheetMap["智能监控"];
result = await readSheet(monitorSheet, gatewayPage, hour); result = await readSheet(monitorSheet, gatewayPage, hour);
} else if (gatewayPage === 3) {
let monitorSheet = sheetMap["客流体验"];
result = await readSheet(monitorSheet, gatewayPage, 0);
} }
return result; return result;
} }
...@@ -148,13 +151,31 @@ async function buildDeviceMapDynamic( ...@@ -148,13 +151,31 @@ async function buildDeviceMapDynamic(
regionMap: { [regionName: string]: string }, regionMap: { [regionName: string]: string },
ieqTagMap: { [regionName: string]: string }, ieqTagMap: { [regionName: string]: string },
hour: number, hour: number,
): Promise<{ deviceMap: { [regionName: string]: any[] }; qualityMap: { [regionName: string]: string } }> { deviceIdMap?: { [deviceId: string]: any },
): Promise<{
deviceMap: { [regionName: string]: any[] };
qualityMap: { [regionName: string]: string };
dataMap: { [deviceId: string]: any };
}> {
const deviceMap: { [regionName: string]: any[] } = {}; const deviceMap: { [regionName: string]: any[] } = {};
const qualityMap: { [regionName: string]: string } = {}; const qualityMap: { [regionName: string]: string } = {};
const dataMap: { [deviceId: string]: any } = {};
// 0. 根据 sheet 解析出的设备编号(已剔除占位符)查询设备表,构建 device_id → 设备信息映射
const sheetDeviceIds = deviceIdMap ? Object.keys(deviceIdMap).filter((id) => id && String(id).trim()) : [];
if (sheetDeviceIds.length > 0) {
const sheetDeviceResult = await selectDataListByParam(
TABLENAME.设备表,
{ device_id: { '%in%': sheetDeviceIds } },
);
(sheetDeviceResult.data || []).forEach((d: any) => {
if (d && d.device_id) dataMap[d.device_id] = d;
});
}
// 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, qualityMap }; if (regionKeys.length === 0) return { deviceMap, qualityMap, dataMap };
// 2. 反向映射:regionKey → regionName // 2. 反向映射:regionKey → regionName
const regionKeyToName: { [key: string]: string } = {}; const regionKeyToName: { [key: string]: string } = {};
...@@ -295,7 +316,7 @@ async function buildDeviceMapDynamic( ...@@ -295,7 +316,7 @@ async function buildDeviceMapDynamic(
qualityMap[name] = judgeQuality(avgCo2, avgPm25, entry.co2List.length > 0, entry.pm25List.length > 0); qualityMap[name] = judgeQuality(avgCo2, avgPm25, entry.co2List.length > 0, entry.pm25List.length > 0);
} }
return { deviceMap, qualityMap }; return { deviceMap, qualityMap, dataMap };
} }
// ================================== 读取区域坐标分布.xlsx ============================================ // ================================== 读取区域坐标分布.xlsx ============================================
...@@ -350,8 +371,9 @@ async function readSheet(sheet: any[][], gatewayPage: number, hour: number) { ...@@ -350,8 +371,9 @@ async function readSheet(sheet: any[][], gatewayPage: number, hour: number) {
// ] // ]
// } // }
// ====== 动态查询:从数据库获取真实设备及最新状态 ====== // ====== 动态查询:从数据库获取真实设备及最新状态 ======
// 先从 sheet 中解析 IEQ 列,构建 regionName → IEQ 标签映射 // 先从 sheet 中解析 IEQ 列,构建 regionName → IEQ 标签映射 || 从sheet中解析设备列,构建 device_id → 设备信息映射
const ieqTagMap: { [regionName: string]: string } = {}; const ieqTagMap: { [regionName: string]: string } = {};
const deviceIdMap: { [deviceId: string]: any } = {};
if (sheet && sheet.length > 0) { if (sheet && sheet.length > 0) {
const headerRow = sheet[0] as any[]; const headerRow = sheet[0] as any[];
const ieqColIdx = headerRow.findIndex((h: any) => String(h).trim() === 'IEQ'); const ieqColIdx = headerRow.findIndex((h: any) => String(h).trim() === 'IEQ');
...@@ -364,8 +386,20 @@ async function readSheet(sheet: any[][], gatewayPage: number, hour: number) { ...@@ -364,8 +386,20 @@ async function readSheet(sheet: any[][], gatewayPage: number, hour: number) {
} }
} }
} }
// 从 sheet 中解析设备列,构建 device_id → 设备信息映射(剔除占位符:空串、"--"、"-" 等)
const deviceColIdx = headerRow.findIndex((h: any) => String(h).trim() === '设备');
if (deviceColIdx >= 0) {
for (let i = 1; i < sheet.length; i++) {
const row = sheet[i] as any[];
const deviceId = row[deviceColIdx];
if (deviceId == null) continue;
const idStr = String(deviceId).trim();
if (!idStr || idStr === '--' || idStr === '-') continue;
if (!deviceIdMap[idStr]) deviceIdMap[idStr] = { deviceId: idStr };
}
}
} }
const { deviceMap, qualityMap } = await buildDeviceMapDynamic(regionMap, ieqTagMap, hour); const { deviceMap, qualityMap, dataMap } = await buildDeviceMapDynamic(regionMap, ieqTagMap, hour, deviceIdMap);
let sheetData = await planaryArrayBecomeOfBlockData(sheet); let sheetData = await planaryArrayBecomeOfBlockData(sheet);
let result = []; let result = [];
if (sheetData && sheetData[0].blockData) { if (sheetData && sheetData[0].blockData) {
...@@ -374,6 +408,7 @@ async function readSheet(sheet: any[][], gatewayPage: number, hour: number) { ...@@ -374,6 +408,7 @@ async function readSheet(sheet: any[][], gatewayPage: number, hour: number) {
const index = headerRow.indexOf("区域名称"); const index = headerRow.indexOf("区域名称");
const xindex = headerRow.indexOf("X轴坐标"); const xindex = headerRow.indexOf("X轴坐标");
const yindex = headerRow.indexOf("Y轴坐标"); const yindex = headerRow.indexOf("Y轴坐标");
const dindex = headerRow.indexOf("设备");
result = block.blockData.slice(1).map((row: any) => ({ result = block.blockData.slice(1).map((row: any) => ({
regionKey: regionMap[row[0]], regionKey: regionMap[row[0]],
regionName: String(row[index] ?? ""), regionName: String(row[index] ?? ""),
...@@ -381,7 +416,10 @@ async function readSheet(sheet: any[][], gatewayPage: number, hour: number) { ...@@ -381,7 +416,10 @@ async function readSheet(sheet: any[][], gatewayPage: number, hour: number) {
yz: row[yindex], yz: row[yindex],
qualityIndex: qualityMap[row[0]] ?? '--', qualityIndex: qualityMap[row[0]] ?? '--',
gatewayPage: gatewayPage, gatewayPage: gatewayPage,
devices: deviceMap[row[0]] deviceId: row[dindex],
power: row[dindex] ? (dataMap[row[dindex]]?.device_state && dataMap[row[dindex]]?.device_state === '1' ? 'off' : 'on') : undefined,
devices: deviceMap[row[0]],
targets: []
}))?? []; }))?? [];
} }
return result; return result;
......
...@@ -8,6 +8,7 @@ import { TABLENAME } from '../config/dbEnum'; ...@@ -8,6 +8,7 @@ import { TABLENAME } from '../config/dbEnum';
import { DEVICE_STATE, powerMap, workModeMap, fanSpeedMap, lockMap, acPopFanSpeedMap } from '../config/businessEnum'; import { DEVICE_STATE, powerMap, workModeMap, fanSpeedMap, lockMap, acPopFanSpeedMap } from '../config/businessEnum';
import { selectDataListByParam, selectOneDataByParam, selectDataCountByParam } from '../data/findData'; import { selectDataListByParam, selectOneDataByParam, selectDataCountByParam } from '../data/findData';
import { controlIndoorUnit, getIndoorUnitStatus } from './feiyiClient'; import { controlIndoorUnit, getIndoorUnitStatus } from './feiyiClient';
import { INDICATOR_NAME_MAP, ENV_INDICATOR_KEYS } from '../config/businessEnum';
import { import {
formatTime, formatTime,
getTodayStart, getTodayStart,
...@@ -97,35 +98,6 @@ function getOverallQuality(co2: number, pm25: number): string { ...@@ -97,35 +98,6 @@ function getOverallQuality(co2: number, pm25: number): string {
// ==================== 环境监测相关辅助 ==================== // ==================== 环境监测相关辅助 ====================
/** 环境指标 key 映射:device_data JSON 中的 key → 展示用的 key */
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 }>
...@@ -471,8 +443,8 @@ export async function getRunAnalysis() { ...@@ -471,8 +443,8 @@ export async function getRunAnalysis() {
// ---- 真实查询 ---- // ---- 真实查询 ----
const now24h = new Date(); const now24h = new Date();
const last24hStart = getHoursAgo(24); const last24hStart = getHoursAgo(24);
const last7dStart = getDaysAgoStart(7); const last7dStart = getDaysAgoStart(6); // 因为0000的缘故,所以减6
const last30dStart = getDaysAgoStart(30); const last30dStart = getDaysAgoStart(29);
const lastYearStart = getMonthsAgoStart(12); const lastYearStart = getMonthsAgoStart(12);
const last24HoursData = await getElectricityTrendData(last24hStart, formatTime(now24h), 'hour'); const last24HoursData = await getElectricityTrendData(last24hStart, formatTime(now24h), 'hour');
...@@ -604,30 +576,27 @@ export async function getRunAnalysis() { ...@@ -604,30 +576,27 @@ export async function getRunAnalysis() {
// 耗电量查询终点:当前时刻(起点复用上方已定义的 last24hStart/last7dStart/last30dStart/lastYearStart) // 耗电量查询终点:当前时刻(起点复用上方已定义的 last24hStart/last7dStart/last30dStart/lastYearStart)
const elecNowStr = formatTime(new Date()); const elecNowStr = formatTime(new Date());
/** 查询指定时间范围内各区域的耗电量 */ /** 查询指定时间范围内各区域的耗电量(从 region_expenses 取数) */
async function queryRegionElectricity(startTime: string, endTime: string): Promise<Map<string, number>> { async function queryRegionElectricity(startTime: string, endTime: string): Promise<Map<string, number>> {
const map = new Map<string, number>(); const map = new Map<string, number>();
if (meterDeviceIds.length === 0) return map; if (allRegionKeys.size === 0) return map;
// region_expenses 的 expense_date 是 DATEONLY 类型,需要截取 startTime 的日期部分
const startDate = startTime.split(' ')[0];
const endDate = endTime.split(' ')[0];
const result = await selectDataListByParam( const result = await selectDataListByParam(
TABLENAME.设备数据表, TABLENAME.区域电费表,
{ {
device_id: { '%in%': meterDeviceIds }, region_key: { '%in%': [...allRegionKeys] },
device_time: { '%gte%': startTime, '%lte%': endTime }, expense_date: { '%gte%': startDate, '%lte%': endDate },
}, },
['device_id', 'device_data'] ['region_key', 'use_power']
); );
(result.data || []).forEach((r: any) => { (result.data || []).forEach((r: any) => {
const parsed = parseDeviceData(r.device_data); const val = Number(r.use_power) || 0;
const val = parsed[ELECTRICITY_KEY]; if (val > 0) {
if (typeof val === 'number') { map.set(r.region_key, (map.get(r.region_key) || 0) + val);
const rkSet = meterRegionMap.get(r.device_id);
if (rkSet) {
// 一对多:电表关联的每个区域都累计该电表的耗电量
rkSet.forEach(rk => {
map.set(rk, (map.get(rk) || 0) + val);
});
}
} }
}); });
return map; return map;
...@@ -1010,10 +979,10 @@ export async function getRunAnalysis() { ...@@ -1010,10 +979,10 @@ export async function getRunAnalysis() {
====== MOCK DATA END ====== */ ====== MOCK DATA END ====== */
// ---- 真实查询 ---- // ---- 真实查询 ----
// 查询 region_type = '机房' 的区域 // 查询 region_name like '车间' 的区域
const serverRoomResult = await selectDataListByParam( const serverRoomResult = await selectDataListByParam(
TABLENAME.区域表, TABLENAME.区域表,
{ region_type: '机房' }, { region_name: { '%like%': '车间' } },
['id', 'region_key', 'region_name'] ['id', 'region_key', 'region_name']
); );
const serverRegions = serverRoomResult.data || []; const serverRegions = serverRoomResult.data || [];
...@@ -1092,55 +1061,35 @@ export async function getRunAnalysis() { ...@@ -1092,55 +1061,35 @@ export async function getRunAnalysis() {
]; ];
} }
// 2、机房功率监控 // 2、机房功率监控(从 region_expenses 取区域电费数据)
// TODO: 待机房环境监控逻辑确认后,同步恢复真实查询 let compRoomPower: { key: string; value: number }[] = [];
let compRoomPower: { key: string; value: number }[] = [{
key: "2026-06-28 07时", value: 200
},{
key: "2026-06-28 17时", value: 400
}];
/* ====== 真实查询逻辑(暂存,待确认后启用) ======
if (serverRegions.length > 0) { if (serverRegions.length > 0) {
const serverRegionKeys = serverRegions.map((r: any) => r.region_key); const serverRegionKeys = serverRegions.map((r: any) => r.region_key);
const srMeterResult = await selectDataListByParam( // 从 region_expenses 查询近 30 天机房区域电费数据
TABLENAME.设备表, const expenseResult = await selectDataListByParam(
TABLENAME.区域电费表,
{ {
region_key: { '%in%': serverRegionKeys }, region_key: { '%in%': serverRegionKeys },
device_type: '电表', expense_date: { '%gte%': getDaysAgoStart(29) },
'%orderAsc%': 'expense_date',
}, },
['device_id'] ['region_key', 'use_power', 'expense_date']
); );
const srMeterIds = (srMeterResult.data || []).map((d: any) => d.device_id);
if (srMeterIds.length > 0) {
const srPowerResult = await selectDataListByParam(
TABLENAME.设备数据表,
{
device_id: { '%in%': srMeterIds },
device_time: { '%gte%': getHoursAgo(24) },
'%orderAsc%': 'device_time',
},
['device_data', 'device_time']
);
const srPowerHourly = new Map<string, number>(); const dailyPower = new Map<string, number>();
(srPowerResult.data || []).forEach((r: any) => { (expenseResult.data || []).forEach((r: any) => {
const t = new Date(r.device_time); const key = r.expense_date || '';
const key = formatHourKey(t); const val = Number(r.use_power) || 0;
const parsed = parseDeviceData(r.device_data); if (key) {
const val = parsed[ELECTRICITY_KEY]; dailyPower.set(key, (dailyPower.get(key) || 0) + val);
if (typeof val === 'number') { }
srPowerHourly.set(key, (srPowerHourly.get(key) || 0) + val); });
}
});
compRoomPower = Array.from(srPowerHourly.entries()) compRoomPower = Array.from(dailyPower.entries())
.map(([key, value]) => ({ key, value: Math.round(value) })) .map(([key, value]) => ({ key, value: Math.round(value * 100) / 100 }))
.sort((a, b) => a.key.localeCompare(b.key)); .sort((a, b) => a.key.localeCompare(b.key));
}
} }
====== 真实查询逻辑 END ====== */
// ======================================================================== // ========================================================================
// 七、设备汇总 // 七、设备汇总
...@@ -2365,7 +2314,7 @@ export async function getAcRunningPop(regionKey: string, deviceId: string) { ...@@ -2365,7 +2314,7 @@ export async function getAcRunningPop(regionKey: string, deviceId: string) {
// 获取设备信息 // 获取设备信息
let devices = await selectDataListByParam( let devices = await selectDataListByParam(
TABLENAME.设备表, TABLENAME.设备表,
{ region_key: regionKey, device_id: deviceId }, { device_id: deviceId }, // region_key: regionKey,
["device_id", "device_type", "device_name", "device_param"] ["device_id", "device_type", "device_name", "device_param"]
); );
if (!devices.data.length) { if (!devices.data.length) {
......
...@@ -7,10 +7,67 @@ import { addData } from '../data/addData'; ...@@ -7,10 +7,67 @@ import { addData } from '../data/addData';
import { updateManyData } from '../data/updateData'; import { updateManyData } from '../data/updateData';
import { delData } from '../data/delData'; import { delData } from '../data/delData';
import { TABLENAME } from '../config/dbEnum'; import { TABLENAME } from '../config/dbEnum';
import { getPwdMd5 } from '../tools/system'; import { getPwdMd5, getToken } from '../tools/system';
import { BizError } from '../util/bizError';
import { ERRORENUM } from '../config/errorEnum';
import { getMySqlMs } from '../tools/systemTools';
const USER_COLUMNS = ['login_id', 'name', 'phone', 'role', 'department', 'status', 'created_at', 'updated_at']; const USER_COLUMNS = ['login_id', 'name', 'phone', 'role', 'department', 'status', 'created_at', 'updated_at'];
/**
* 注册
* @param loginId
* @param pwd
*/
export async function registerUser(loginId:string, pwd:string, name:string) {
let filesList = ["loginId", "id", "pwd", "name"];
let adminUserInfo : any = await selectOneDataByParam(TABLENAME.用户信息表, {loginId}, filesList);
if (!adminUserInfo) {
throw new BizError(ERRORENUM.用户已存在);
}
await addData(TABLENAME.用户信息表, {loginId, pwd: getPwdMd5(loginId, pwd), name: name});
return { success: true };
}
/**
* 登录
* @param loginId
* @param pwd
* @returns
*/
export async function adminLogin(loginId:string, pwd:string) {
let filesList = ["name", "id", "login_id", "pwd", "role"];
let adminUserInfo : any = await selectOneDataByParam(TABLENAME.用户信息表, {login_id: loginId}, filesList);
console.log(adminUserInfo.data);
if (!adminUserInfo.data || !adminUserInfo.data.id) {
throw new BizError(ERRORENUM.账号或密码错误);
}
const encryptedPwd = getPwdMd5(adminUserInfo.data.login_id, pwd);
if (adminUserInfo.data.pwd != encryptedPwd) {
throw new BizError(ERRORENUM.账号或密码错误);
}
let updateUserInfo = {
token : getToken(adminUserInfo.data.login_id),
token_ms : getMySqlMs()
};
await updateManyData(TABLENAME.用户信息表, {id: adminUserInfo.data.id}, updateUserInfo);
let userInfo = {
userId:adminUserInfo.data.id,
loginId:adminUserInfo.data.login_id,
userName:adminUserInfo.data.name,
token:updateUserInfo.token,
role:adminUserInfo.data.role,
};
return {dataInfo:userInfo};
}
/** /**
* 用户管理列表(分页) * 用户管理列表(分页)
* @param pageNumber 页码 * @param pageNumber 页码
......
...@@ -120,7 +120,6 @@ export enum workModeMap { ...@@ -120,7 +120,6 @@ export enum workModeMap {
"制热" = 2, "制热" = 2,
"送风" = 3, "送风" = 3,
"除湿" = 4, "除湿" = 4,
"自动" = 0,
} }
/** /**
...@@ -136,7 +135,6 @@ export enum fanSpeedMap { ...@@ -136,7 +135,6 @@ export enum fanSpeedMap {
* acPop风速映射:1-自动 2-低风 3-中风 4-高风 * acPop风速映射:1-自动 2-低风 3-中风 4-高风
*/ */
export enum acPopFanSpeedMap { export enum acPopFanSpeedMap {
"自动" = 1,
"低风" = 2, "低风" = 2,
"中风" = 3, "中风" = 3,
"高风" = 4, "高风" = 4,
...@@ -157,3 +155,33 @@ export const POWER_TEXT_MAP: Record<number, string> = { 1: '开', 0: '关' }; ...@@ -157,3 +155,33 @@ export const POWER_TEXT_MAP: Record<number, string> = { 1: '开', 0: '关' };
export const WORK_MODE_TEXT_MAP: Record<number, string> = { 1: '制冷', 2: '制热', 3: '送风', 4: '除湿', 0: '自动' }; export const WORK_MODE_TEXT_MAP: Record<number, string> = { 1: '制冷', 2: '制热', 3: '送风', 4: '除湿', 0: '自动' };
export const FAN_SPEED_TEXT_MAP: Record<number, string> = { 1: '高风', 2: '中风', 4: '低风' }; export const FAN_SPEED_TEXT_MAP: Record<number, string> = { 1: '高风', 2: '中风', 4: '低风' };
export const DEVICE_STATE_TEXT_MAP: Record<number, string> = { 0: '在线', 1: '离线', 2: '故障' }; export const DEVICE_STATE_TEXT_MAP: Record<number, string> = { 0: '在线', 1: '离线', 2: '故障' };
/** 环境指标 key 映射:device_data JSON 中的 key → 展示用的 key */
export const ENV_INDICATOR_KEYS = ['temperature', 'humidity', 'pm25', 'co2', 'hcho', 'lightLevel', 'pm10', 'pressure', 'tvoc'];
/** 指标 key → 中文名映射(涵盖标准指标 + 迪勤编码指标) */
export 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': '甲醛',
};
\ No newline at end of file
...@@ -11,7 +11,10 @@ export enum TABLENAME { ...@@ -11,7 +11,10 @@ export enum TABLENAME {
日程表 = 'schedule', 日程表 = 'schedule',
设备日志表 = 'device_logs', 设备日志表 = 'device_logs',
日程设备关联表 = 'schedule_device', 日程设备关联表 = 'schedule_device',
设备故障表 = 'device_fault' 设备故障表 = 'device_fault',
设备联动调节表 = 'device_linkage',
设备扩展表 = 'device_expansion',
区域电费表 = 'region_expenses',
}; };
/** /**
......
...@@ -46,7 +46,8 @@ export enum ERRORENUM { ...@@ -46,7 +46,8 @@ export enum ERRORENUM {
只能上传doc或docx或excel或png或jpg图片, 只能上传doc或docx或excel或png或jpg图片,
该时间段已有预约, 该时间段已有预约,
网络错误, 网络错误,
第三方接口错误 第三方接口错误,
用户已存在
} }
/** /**
......
...@@ -601,4 +601,163 @@ export const TablesConfig = [ ...@@ -601,4 +601,163 @@ export const TablesConfig = [
} }
] ]
}, },
// 设备联动调节表
{
tableNameCn: '设备联动调节表',
tableName: 'device_linkage',
schema: {
id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '自增主键'
},
device_id: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '设备编号,关联device.device_id'
},
linkage_type: {
type: DataTypes.STRING(20),
allowNull: false,
comment: '联动类型:制冷/制热/新风'
},
threshold_max: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '阈值上限,如"26"'
},
threshold_min: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '阈值下限,如"22"'
},
threshold_mid: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '中间阈值,如"24"'
},
setting_max: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '上限设置:高于阈值上限时的设定值'
},
setting_min: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '下限设置:低于阈值下限时的设定值'
},
setting_mid: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '中间设置:处于阈值区间内的设定值'
},
divide_method: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '划分方式,如"按温度三段式"'
},
begin_validity: {
type: DataTypes.DATE,
allowNull: true,
comment: '有效期起,不设置则为空'
},
end_validity: {
type: DataTypes.DATE,
allowNull: true,
comment: '有效期止,不设置则为空'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '创建时间'
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '修改时间'
}
},
association: [],
indexes: [
{ fields: ['device_id'] },
{ fields: ['linkage_type'] },
{ fields: ['device_id', 'linkage_type'] },
]
},
// 区域电费表(按区域存储电费数据,不再关联设备)
{
tableNameCn: '区域电费表',
tableName: 'region_expenses',
schema: {
id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '自增主键'
},
region_key: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '区域编码,关联region.region_key'
},
use_power: {
type: DataTypes.DECIMAL(12, 2),
allowNull: true,
defaultValue: 0,
comment: '用电量(kWh)'
},
electricity_fee: {
type: DataTypes.DECIMAL(12, 2),
allowNull: true,
defaultValue: 0,
comment: '电费金额(元)'
},
energy_loss: {
type: DataTypes.DECIMAL(12, 2),
allowNull: true,
defaultValue: 0,
comment: '损耗电量(kWh)'
},
loss_fee: {
type: DataTypes.DECIMAL(12, 2),
allowNull: true,
defaultValue: 0,
comment: '损耗电费(元)'
},
expense_date: {
type: DataTypes.DATEONLY,
allowNull: false,
comment: '费用日期'
},
device_time: {
type: DataTypes.DATE,
allowNull: true,
comment: '数据原始时间(飞奕平台时间)'
},
original_data: {
type: DataTypes.JSON,
allowNull: true,
comment: '原始数据备份'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '创建时间'
},
},
association: [
{ type: "hasMany", target: "region", foreignKey: "region_key" }
],
indexes: [
{ fields: ['region_key'] },
{ fields: ['expense_date'] },
{ fields: ['region_key', 'expense_date'] },
]
},
]; ];
\ No newline at end of file
...@@ -10,61 +10,69 @@ import * as usersBiz from "../biz/users"; ...@@ -10,61 +10,69 @@ import * as usersBiz from "../biz/users";
import * as logBiz from "../biz/log"; import * as logBiz from "../biz/log";
import { eccReqParamater } from "../util/verificationParam"; import { eccReqParamater } from "../util/verificationParam";
import { get } from "http"; import { get } from "http";
import { checkUser } from "../tools/system";
export function setRouter(httpServer) { export function setRouter(httpServer) {
/** 区域树 */ /** 区域树 */
httpServer.get("/api/dq/admin/region/device", asyncHandler(getRegionDevices)); httpServer.get("/api/dq/admin/region/device", checkUser, asyncHandler(getRegionDevices));
/** 区域详情 */ /** 区域详情 */
httpServer.get("/api/dq/admin/region/detail", asyncHandler(getRegionDetail)); httpServer.get("/api/dq/admin/region/detail", checkUser, asyncHandler(getRegionDetail));
/** 区域管理新增 */ /** 区域管理新增 */
httpServer.post("/api/dq/admin/region/add", asyncHandler(addRegion)); httpServer.post("/api/dq/admin/region/add", checkUser, asyncHandler(addRegion));
/** 区域管理编辑 */ /** 区域管理编辑 */
httpServer.post("/api/dq/admin/region/edit", asyncHandler(editRegion)); httpServer.post("/api/dq/admin/region/edit", checkUser, asyncHandler(editRegion));
/** 区域管理删除 */ /** 区域管理删除 */
httpServer.post("/api/dq/admin/region/delete", asyncHandler(deleteRegion)); httpServer.post("/api/dq/admin/region/delete", checkUser, asyncHandler(deleteRegion));
/** 日程管理列表 */ /** 日程管理列表 */
httpServer.get("/api/dq/admin/schedule/list", asyncHandler(getScheduleList)); httpServer.get("/api/dq/admin/schedule/list", checkUser, asyncHandler(getScheduleList));
/** 日程管理详情 */ /** 日程管理详情 */
httpServer.get("/api/dq/admin/schedule/detail", asyncHandler(getScheduleDetail)); httpServer.get("/api/dq/admin/schedule/detail", checkUser, asyncHandler(getScheduleDetail));
/** 日程管理新增 */ /** 日程管理新增 */
httpServer.post("/api/dq/admin/schedule/add", asyncHandler(addSchedule)); httpServer.post("/api/dq/admin/schedule/add", checkUser, asyncHandler(addSchedule));
/** 日程管理编辑 */ /** 日程管理编辑 */
httpServer.post("/api/dq/admin/schedule/edit", asyncHandler(editSchedule)); httpServer.post("/api/dq/admin/schedule/edit", checkUser, asyncHandler(editSchedule));
/** 日程管理删除 */ /** 日程管理删除 */
httpServer.post("/api/dq/admin/schedule/delete", asyncHandler(deleteSchedule)); httpServer.post("/api/dq/admin/schedule/delete", checkUser, asyncHandler(deleteSchedule));
/** 设备管理列表 */ /** 设备管理列表 */
httpServer.get("/api/dq/admin/device/list", asyncHandler(getDeviceList)); httpServer.get("/api/dq/admin/device/list", checkUser, asyncHandler(getDeviceList));
/** 设备管理详情 */
httpServer.get("/api/dq/admin/device/detail", checkUser, asyncHandler(getDeviceDetail));
/** 设备管理 - 区域查询 */ /** 设备管理 - 区域查询 */
httpServer.post("/api/dq/admin/device/devices", asyncHandler(getDevicesByRegion)); httpServer.post("/api/dq/admin/device/devices", checkUser, asyncHandler(getDevicesByRegion));
/** 设备管理 - 编辑空调控制参数 */ /** 设备管理 - 编辑空调控制参数 */
httpServer.post("/api/dq/admin/device/edit", asyncHandler(editDevice)); httpServer.post("/api/dq/admin/device/edit", checkUser, asyncHandler(editDevice));
/** 设备管理 - 批量编辑设备 */ /** 设备管理 - 批量编辑设备 */
httpServer.post("/api/dq/admin/device/edit_batch", asyncHandler(batchEditDevices)); httpServer.post("/api/dq/admin/device/edit_batch", checkUser, asyncHandler(batchEditDevices));
/** 设备管理 - 新增设备信息 */ /** 设备管理 - 新增设备信息 */
httpServer.post("/api/dq/admin/device/addDevice", asyncHandler(addLocalDevice)); httpServer.post("/api/dq/admin/device/addDevice", checkUser, asyncHandler(addLocalDevice));
/** 设备管理 - 编辑设备信息 */ /** 设备管理 - 编辑设备信息 */
httpServer.post("/api/dq/admin/device/editDevice", asyncHandler(editLocalDevice)); httpServer.post("/api/dq/admin/device/editDevice", checkUser, asyncHandler(editLocalDevice));
/** 设备管理 - 批量编辑设备信息 */ /** 设备管理 - 批量编辑设备信息 */
httpServer.post("/api/dq/admin/device/editDevice_batch", asyncHandler(batchEditLocalDevices)); httpServer.post("/api/dq/admin/device/editDevice_batch", checkUser, asyncHandler(batchEditLocalDevices));
/** 设备管理 - 删除设备信息 */ /** 设备管理 - 删除设备信息 */
httpServer.post("/api/dq/admin/device/deleteDevice", asyncHandler(deleteLocalDevice)); httpServer.post("/api/dq/admin/device/deleteDevice", checkUser, asyncHandler(deleteLocalDevice));
/** 用户管理 - 列表 */ /** 用户管理 - 列表 */
httpServer.get("/api/dq/admin/users/list", asyncHandler(getUsersList)); httpServer.get("/api/dq/admin/users/list", checkUser, asyncHandler(getUsersList));
/** 用户管理 - 详情 */ /** 用户管理 - 详情 */
httpServer.get("/api/dq/admin/users/detail", asyncHandler(getUsersDetail)); httpServer.get("/api/dq/admin/users/detail", checkUser, asyncHandler(getUsersDetail));
/** 用户管理 - 添加用户 */ /** 用户管理 - 添加用户 */
httpServer.post("/api/dq/admin/users/add", asyncHandler(addUsers)); httpServer.post("/api/dq/admin/users/add", checkUser, asyncHandler(addUsers));
/** 用户管理 - 修改用户 */ /** 用户管理 - 修改用户 */
httpServer.post("/api/dq/admin/users/edit", asyncHandler(editUsers)); httpServer.post("/api/dq/admin/users/edit", checkUser, asyncHandler(editUsers));
/** 用户管理 - 删除用户 */ /** 用户管理 - 删除用户 */
httpServer.post("/api/dq/admin/users/delete", asyncHandler(deleteUsers)); httpServer.post("/api/dq/admin/users/delete", checkUser, asyncHandler(deleteUsers));
/** 角色管理 - 列表 */
httpServer.get("/api/dq/admin/roles/list", checkUser, asyncHandler(getRolesList));
/** 角色管理 - 集合 */
httpServer.get("/api/dq/admin/roles/all", checkUser, asyncHandler(getRolesAll));
/** 日志管理 - 列表 */ /** 日志管理 - 列表 */
httpServer.get("/api/dq/admin/device/logs", asyncHandler(getDeviceLogs)); httpServer.get("/api/dq/admin/device/logs", checkUser, asyncHandler(getDeviceLogs));
} }
// ======================= 区域设备 ======================= // ======================= 区域设备 =======================
...@@ -273,6 +281,21 @@ async function getDeviceList(req, res) { ...@@ -273,6 +281,21 @@ async function getDeviceList(req, res) {
} }
/** /**
* GET /api/dq/admin/device/detail
* 设备管理详情
* query: deviceId
*/
async function getDeviceDetail(req, res) {
const deviceId: string = req.query?.deviceId as string;
if (!deviceId) {
res.success({success: false, msg: "缺少参数 deviceId"});
return;
}
const data = await deviceBiz.getDeviceDetail({ deviceId });
res.success(data);
}
/**
* GET /api/dq/admin/device/devices * GET /api/dq/admin/device/devices
* 设备管理 - 区域查询 * 设备管理 - 区域查询
* query: regionKeys * query: regionKeys
...@@ -374,7 +397,7 @@ async function addLocalDevice(req, res) { ...@@ -374,7 +397,7 @@ async function addLocalDevice(req, res) {
let reqConf = { let reqConf = {
deviceId: 'String', deviceId: 'String',
deviceName: 'String', deviceName: 'String',
regionKey: 'Number', regionKey: 'String',
deviceType: 'String', deviceType: 'String',
deviceState: 'String', deviceState: 'String',
linkageStart: 'String', linkageStart: 'String',
...@@ -399,7 +422,7 @@ async function editLocalDevice(req, res) { ...@@ -399,7 +422,7 @@ async function editLocalDevice(req, res) {
let reqConf = { let reqConf = {
deviceId: 'String', deviceId: 'String',
deviceName: 'String', deviceName: 'String',
regionKey: 'Number', regionKey: 'String',
deviceType: 'String', deviceType: 'String',
deviceState: 'String', deviceState: 'String',
linkageStart: 'String', linkageStart: 'String',
...@@ -548,6 +571,36 @@ async function deleteUsers(req, res) { ...@@ -548,6 +571,36 @@ async function deleteUsers(req, res) {
res.success(data); res.success(data);
} }
// ======================= 角色管理 =======================
/**
* GET /api/dq/admin/roles/list
* 角色管理列表(分页)
*
* query: pageNumber, pageSize
*/
async function getRolesList(req, res) {
const pageNumber = Number(req.query?.pageNumber) || 1;
const pageSize = Number(req.query?.pageSize) || 10;
// const data = await usersBiz.getRolesList(pageNumber, pageSize);
res.success([]);
}
/**
* GET /api/dq/admin/roles/all
* 角色管理 - 获取所有角色集合
* query: none
*/
async function getRolesAll(req, res) {
// const data = await usersBiz.getRolesAll();
const data = [
{ code: "admin", name: "管理员" },
{ code: "operator", name: "操作员" },
{ code: "guest", name: "访客" }
];
res.success(data);
}
// ======================= 日志管理 ======================= // ======================= 日志管理 =======================
/** /**
......
/**
* 客户体验路由
*/
import asyncHandler from 'express-async-handler';
import * as customsBiz from '../biz/customs';
import { eccReqParamater } from '../util/verificationParam';
export function setRouter(httpServer) {
/** 客流体验分析 */
httpServer.post('/api/dq/customs/analysis', asyncHandler(getCustomsAnalysis));
}
/**
* 客流体验分析
*/
async function getCustomsAnalysis(req, res) {
const { } = req.body;
const result = await customsBiz.getCustomsAnalysis();
res.success(result);
}
\ No newline at end of file
...@@ -57,4 +57,60 @@ async function getFeiyiIndoorUnitControl(req, res) { ...@@ -57,4 +57,60 @@ async function getFeiyiIndoorUnitControl(req, res) {
res.success(result); res.success(result);
} }
/**
* 单个内机分摊电量和电费
* @param page 当前页码(选填)
* @param limit 每页条数(选填)
* @param roomId 房间ID(选填)
* @param indoorUnitId 内机ID(选填)
* @param indoorUnitAddressFull 内机地址(选填)
* @param occurrenceTime 日期 yyyyMMdd(必填)
*/
async function getIndoorUnitExpensesApi(req, res) {
let reqConf = {
page: 'Number', limit: 'Number',
roomId: 'String', indoorUnitId: 'String', indoorUnitAddressFull: 'String',
occurrenceTime: 'String',
};
const NotMustHaveKeys = [ "page", "limit", "roomId", "indoorUnitId", "indoorUnitAddressFull" ];
let params = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await feiyiClientBiz.getIndoorUnitExpenses(params);
res.success(result);
}
/**
* 房间电费日使用详情
* @param page 当前页码(选填)
* @param limit 每页条数(选填)
* @param roomIds 房间ID集合(选填)
* @param floorIds 楼层ID集合(选填)
* @param buildingIds 楼栋ID集合(选填)
* @param occurrenceTime 日期 yyyyMMdd(必填)
*/
async function getRoomExpensesListApi(req, res) {
let reqConf = {
page: 'Number', limit: 'Number',
roomIds: 'Array', floorIds: 'Array', buildingIds: 'Array',
occurrenceTime: 'String',
};
const NotMustHaveKeys = [ "page", "limit", "roomIds", "floorIds", "buildingIds" ];
let params = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await feiyiClientBiz.getRoomExpensesList(params);
res.success(result);
}
/**
* 电费数据写入区域电费表 region_expenses
* 拉取"房间电费日使用详情",按 roomId(= region_key)直接写入区域电费表
* @param occurrenceTime 日期 yyyyMMdd(必填)
*/
async function saveRoomExpensesToExpansionApi(req, res) {
let reqConf = { occurrenceTime: 'String' };
let params = eccReqParamater(reqConf, req.body);
const result = await feiyiClientBiz.saveRoomExpensesToExpansion(
params.occurrenceTime,
);
res.success(result);
}
/**
* 公共资源路由
*/
import asyncHandler = require('express-async-handler');
import * as enumBiz from '../config/businessEnum';
const config = {
"/api/public/deviceType":enumBiz.DEVICE_TYPE,//设备类型
"/api/public/deviceState":enumBiz.DEVICE_STATE,//设备状态
"/api/public/regionType":enumBiz.REGION_TYPE,//区域类型
}
export function setRouter(httpServer) {
for (let modelKey in config) {
httpServer.post(modelKey, asyncHandler(getEnumItf(config[modelKey])));
}
}
/**
*
* @param req
* @param res
*/
function getEnumItf(enumCof) {
return async function (req, res) {
let dataList = [];
for (let key in enumCof) {
let anyKey:any = key;
if (isNaN(anyKey)) {
dataList.push({key, value:enumCof[key]});
}
}
res.success({data: dataList});
}
}
\ No newline at end of file
...@@ -5,7 +5,10 @@ import * as feiyiClientRouter from './feiyiClient'; ...@@ -5,7 +5,10 @@ import * as feiyiClientRouter from './feiyiClient';
import * as diqinClientRouter from './diqinClient'; import * as diqinClientRouter from './diqinClient';
import * as regionRouter from './region'; import * as regionRouter from './region';
import * as runningRouter from './running'; import * as runningRouter from './running';
import * as customsRouter from './customs';
import * as adminRouter from './admin'; import * as adminRouter from './admin';
import * as publicRouter from './public';
import * as usersRouter from './users';
export function setRouter(httpServer) { export function setRouter(httpServer) {
...@@ -14,8 +17,11 @@ export function setRouter(httpServer) { ...@@ -14,8 +17,11 @@ export function setRouter(httpServer) {
regionRouter.setRouter(httpServer); regionRouter.setRouter(httpServer);
runningRouter.setRouter(httpServer); runningRouter.setRouter(httpServer);
customsRouter.setRouter(httpServer);
adminRouter.setRouter(httpServer); adminRouter.setRouter(httpServer);
publicRouter.setRouter(httpServer);
usersRouter.setRouter(httpServer);
} }
......
...@@ -103,7 +103,7 @@ async function controlDeviceRunning(req, res) { ...@@ -103,7 +103,7 @@ async function controlDeviceRunning(req, res) {
*/ */
async function getAcRunningPop(req, res) { async function getAcRunningPop(req, res) {
let reqConf = {regionKey:'String', deviceId:'String'}; let reqConf = {regionKey:'String', deviceId:'String'};
const NotMustHaveKeys = []; const NotMustHaveKeys = [ "regionKey" ];
let { regionKey, deviceId } = eccReqParamater(reqConf, req.body, NotMustHaveKeys); let { regionKey, deviceId } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await runningBiz.getAcRunningPop(regionKey, deviceId); const result = await runningBiz.getAcRunningPop(regionKey, deviceId);
res.success(result); res.success(result);
...@@ -117,6 +117,6 @@ async function controlAcRunning(req, res) { ...@@ -117,6 +117,6 @@ async function controlAcRunning(req, res) {
const NotMustHaveKeys = [ 'power', 'maxTemp', 'minTemp', 'autoControl', 'regionKey' ]; const NotMustHaveKeys = [ 'power', 'maxTemp', 'minTemp', 'autoControl', 'regionKey' ];
let { deviceId, mode, power, setTemp, maxTemp, minTemp, autoControl, fanSpeed, regionKey } = eccReqParamater(reqConf, req.body, NotMustHaveKeys); let { deviceId, mode, power, setTemp, maxTemp, minTemp, autoControl, fanSpeed, regionKey } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
let params = { deviceId, mode, power, setTemp, maxTemp, minTemp, autoControl, fanSpeed, regionKey }; let params = { deviceId, mode, power, setTemp, maxTemp, minTemp, autoControl, fanSpeed, regionKey };
const result = await runningBiz.controlAcRunning(params); // const result = await runningBiz.controlAcRunning(params);
res.success(result); res.success({success:true});
} }
\ No newline at end of file
/**
* 青岛博物馆用户注册登录接口
*/
import asyncHandler from 'express-async-handler';
import * as usersBiz from '../biz/users';
import { eccReqParamater } from '../util/verificationParam';
import { checkUser } from '../tools/system';
export function setRouter(httpServer) {
/** 用户注册 */
httpServer.post('/api/dq/user/register', asyncHandler(registerUser));
/** 用户登录 */
httpServer.post('/api/dq/user/login', asyncHandler(login));
/** 用户注销 */
httpServer.post('/api/dq/user/logout', checkUser, asyncHandler(logout));
/**
* 注册信息
*/
async function registerUser(req, res) {
let reqConf = {username:'String', password:'String', name:'String'};
const NotMustHaveKeys = [];
let { username, password, name } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await usersBiz.registerUser(username, password, name);
res.success(result);
}
/**
* 登录信息
*/
async function login(req, res) {
let reqConf = {username:'String', password:'String'};
const NotMustHaveKeys = [];
let { username, password } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await usersBiz.adminLogin(username, password);
res.success(result);
}
/**
* 注销信息
*/
async function logout(req, res) {
let reqConf = {username:'String'};
const NotMustHaveKeys = [];
let { username } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
// const result = await usersBiz.logout(username);
res.success({success:true});
}
}
\ No newline at end of file
...@@ -8,6 +8,8 @@ import moment = require("moment"); ...@@ -8,6 +8,8 @@ import moment = require("moment");
import { selectOneDataByParam } from "../data/findData"; import { selectOneDataByParam } from "../data/findData";
import { TABLENAME } from "../config/dbEnum"; import { TABLENAME } from "../config/dbEnum";
import { systemConfig } from "../config/serverConfig"; import { systemConfig } from "../config/serverConfig";
import { BizError } from "../util/bizError";
import { ERRORENUM } from "../config/errorEnum";
const md5 = require("md5"); const md5 = require("md5");
...@@ -464,4 +466,30 @@ export function formatAttachmentNameUrl(attachment: string | null): Array<{ name ...@@ -464,4 +466,30 @@ export function formatAttachmentNameUrl(attachment: string | null): Array<{ name
}); });
} }
/**
* 校验用户身份信息
* @param req
* @param res
* @param next
*/
export async function checkUser(req, res, next) {
if (!req.headers) req.headers = {};
const userId = req.headers.userid || "";
const reqToken = req.headers.token || "";
// if (!userId) return next(new BizError(ERRORENUM.身份验证失败, `studentId:${userId} token:${reqToken}`));
let userDbData:any = await selectOneDataByParam(TABLENAME.用户信息表, {id: userId}, ["login_id", "token"]);
// if (!userDbData || !userDbData.data || !userDbData.data.loginId) return next(new BizError(ERRORENUM.非法登录, `userId:${userId} token:${reqToken}`));
if (userDbData.data.token != reqToken) return next(new BizError(ERRORENUM.身份验证失败, `studentId:${userId} `));
req.userInfo = {
id:userId,
loginId:userDbData.data.login_id,
token:userDbData.data.token,
}
next();
}
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