客流体验环境指标弹窗

parent 44f637ff
......@@ -19,7 +19,7 @@ import { mysqlModelMap } from "../model/sqlModelBind";
import { acLinkageControl } from "./acLinkage";
import { systemConfig } from "../config/serverConfig";
import Sequelize from "sequelize";
import { DEVICE_STATE, WORK_MODE_TEXT_MAP, FAN_SPEED_TEXT_MAP } from "../config/businessEnum";
import { DEVICE_STATE, WORK_MODE_TEXT_MAP, FAN_SPEED_TEXT_MAP, REGION_NAME } from "../config/businessEnum";
import moment from "moment";
import XLSX from "xlsx";
import path from "path";
......@@ -1230,34 +1230,62 @@ export async function diqinRegion() {
const stations = detail?.stations || [];
for (const st of stations) {
if (!st.id || !st.name) continue;
// 转换后的统一区域名:迪勤监测点与飞奕区域(房间)为同一物理房间时,
// 用飞奕侧 region_name(如 大会议室→亚马逊)入库,使其能与飞奕已建区域按名合并,
// 避免同一房间在 region 表生成两条记录(未配置映射的监测点保持迪勤原始名)。
const mappedName: string =
(REGION_NAME as Record<string, string>)[st.name] || st.name;
// 当前迪勤监测点 id 是否已有入库记录
let stExisting = await regionModel.findOne({ where: { region_key: st.id } });
// mappedName 被谁占用(飞奕已建区域,或历史遗留的迪勤冗余记录)
const nameOwner = await regionModel.findOne({
where: stExisting
? { region_name: mappedName, id: { [Op.ne]: stExisting.id } }
: { region_name: mappedName },
});
if (stExisting) {
if (stExisting.region_name !== st.name
if (nameOwner) {
// 撞名:存量里该监测点曾以原始名建过记录(stExisting),而 mappedName
// 已归属 nameOwner(通常是飞奕房间)→ 把迪勤id并入 nameOwner 后清理冗余记录
if (!nameOwner.region_ad || nameOwner.region_ad !== st.id) {
await regionModel.update(
{ region_ad: st.id, updated_at: new Date() },
{ where: { id: nameOwner.id } },
);
stUpdate++;
console.log(`[迪勤区域集成] 监测点撞名合并: ${st.name}(${st.id}) → ${mappedName}(${nameOwner.region_key})`);
}
await regionModel.destroy({ where: { id: stExisting.id } });
stUpdate++;
} else if (stExisting.region_name !== mappedName
|| stExisting.parent_key !== regionKey
|| stExisting.region_type !== '监测点') {
// 无撞名:按需更新为转换后的名称/归属
await regionModel.update(
{ region_name: st.name, parent_key: regionKey, region_type: '监测点', updated_at: new Date() },
{ region_name: mappedName, parent_key: regionKey, region_type: '监测点', updated_at: new Date() },
{ where: { id: stExisting.id } },
);
stUpdate++;
}
} else {
// 按名称查是否存在同名区域(飞奕或其他平台已创建)
stExisting = await regionModel.findOne({ where: { region_name: st.name } });
if (stExisting) {
// 同名区域已存在,将迪勤内部ID写入 region_ad
if (!stExisting.region_ad || stExisting.region_ad !== st.id) {
if (nameOwner) {
// 同名区域已存在(飞奕或其他平台已创建),将迪勤内部ID写入 region_ad,不新建
if (!nameOwner.region_ad || nameOwner.region_ad !== st.id) {
await regionModel.update(
{ region_ad: st.id, updated_at: new Date() },
{ where: { id: stExisting.id } },
{ where: { id: nameOwner.id } },
);
stUpdate++;
console.log(`[迪勤区域集成] 同名监测点补充 region_ad: ${st.name} → ${st.id}`);
console.log(`[迪勤区域集成] 同名监测点补充 region_ad: ${st.name} → ${mappedName}(${st.id})`);
}
} else {
await regionModel.create({
region_key: st.id,
region_name: st.name,
region_name: mappedName,
parent_id: null,
parent_key: regionKey,
region_type: '监测点',
......@@ -1318,7 +1346,7 @@ export async function diqinDevice() {
if (!deviceId) continue;
const deviceType = DIQIN_DEVICE_TYPE_MAP[ds.data_source_model_name];
const deviceName = ds.name || ds.identifier || deviceId;
const deviceName = `IEQ-${ds.name || ds.identifier || deviceId}`;
const existing = await deviceModel.findOne({ where: { device_id: deviceId } });
if (existing) {
......
......@@ -325,6 +325,7 @@ export async function editAcDeviceControl(params: {
*/
export async function batchEditDevices(params: {
regionKeys: number[];
deviceIds: string[];
deviceName?: string;
power?: string;
state?: string;
......@@ -337,11 +338,11 @@ export async function batchEditDevices(params: {
linkageStart?: string;
};
}): Promise<{ total: number; successCount: number; failCount: number; details: any[] }> {
const { regionKeys, deviceName, power, state, deviceMode, startDate, endDate, edits } = params;
const { regionKeys, deviceIds, deviceName, power, state, deviceMode, startDate, endDate, edits } = params;
// 校验必填参数
if (!regionKeys || !Array.isArray(regionKeys) || regionKeys.length === 0) {
throw new BizError(ERRORENUM.参数错误, "regionKeys 不能为空");
if ((!regionKeys || !Array.isArray(regionKeys) || regionKeys.length === 0) && (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0)) {
throw new BizError(ERRORENUM.参数错误, "regionKeys 和 deviceIds 不能同时为空");
}
if (!edits || typeof edits !== 'object') {
throw new BizError(ERRORENUM.参数错误, "edits 不能为空");
......@@ -353,7 +354,12 @@ export async function batchEditDevices(params: {
// 1. 构建设备查询条件
let whereParam: any = { device_type: '空调' };
if (regionKeys && regionKeys.length > 0) {
whereParam.region_key = { "%in%": regionKeys };
}
if (deviceIds && deviceIds.length > 0) {
whereParam.device_id = { "%in%": deviceIds };
}
if (deviceName) {
whereParam.device_name = { "%like%": deviceName };
......
......@@ -21,14 +21,14 @@ const USER_COLUMNS = ['login_id', 'name', 'phone', 'role', 'department', 'status
* @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);
let filesList = ["login_id", "id", "pwd", "name"];
let adminUserInfo : any = await selectOneDataByParam(TABLENAME.用户信息表, {login_id: loginId}, filesList);
if (!adminUserInfo) {
throw new BizError(ERRORENUM.用户已存在);
}
await addData(TABLENAME.用户信息表, {loginId, pwd: getPwdMd5(loginId, pwd), name: name});
await addData(TABLENAME.用户信息表, {login_id: loginId, pwd: getPwdMd5(loginId, pwd), name: name});
return { success: true };
}
......
......@@ -88,6 +88,17 @@ export enum REGION_TYPE {
监测点 = '监测点',
}
/**
* 区域名称映射
*/
export enum REGION_NAME {
/** 迪勤 - 飞奕 */
大会议室 = '亚马逊',
大办公室 = '环球港',
小办公室 = '综合办',
仓库 = '仓库408',
}
// ==================== 关联相关枚举 ====================
/**
......
......@@ -334,6 +334,11 @@ export const TablesConfig = [
comment: '修改时间'
}
},
// 大表查询索引:device_id + device_time 组合覆盖“按设备查时间区间”,device_time 单独覆盖跨设备按时间区间查询
indexes: [
{ name: 'idx_device_data_device_id_time', fields: ['device_id', 'device_time'] },
{ name: 'idx_device_data_device_time', fields: ['device_time'] }
],
association: [
{
type: 'belongsTo',
......@@ -597,6 +602,11 @@ export const TablesConfig = [
comment: '修改时间'
}
},
// 故障统计/列表查询索引:device_id 覆盖单设备维度,fault_status + fault_time 覆盖状态计数与最新故障排序
indexes: [
{ name: 'idx_device_fault_device_id', fields: ['device_id'] },
{ name: 'idx_device_fault_status_time', fields: ['fault_status', 'fault_time'] }
],
association: [
{
type: 'belongsTo',
......
......@@ -25,7 +25,7 @@ export async function initMysqlModel() {
/**第一步:初始化所有表 */
for (let i = 0; i < TablesConfig.length; i++) {
let { tableName, schema } = TablesConfig[i];
let { tableName, schema, indexes } = TablesConfig[i];
if (!tableName) {
console.warn(`⚠️ 第 ${i} 个表配置缺少 tableName,跳过`);
......@@ -39,11 +39,16 @@ export async function initMysqlModel() {
console.log(`🔄 正在初始化表: ${tableName}`);
let schemaConf = {
let schemaConf: any = {
freezeTableName: true,
timestamps: false
};
// 表级索引(新环境 sync 建表时生效;存量库需人工执行对应 DDL)
if (indexes && indexes.length > 0) {
schemaConf.indexes = indexes;
}
try {
let model = mysqlDB.define(tableName, schema, schemaConf);
mysqlModelMap[tableName] = await model.sync({});
......
......@@ -352,6 +352,7 @@ async function editDevice(req, res) {
async function batchEditDevices(req, res) {
let reqConf = {
regionKeys: 'Array',
deviceIds: 'Array',
deviceName: 'String',
power: 'String',
state: 'String',
......@@ -360,12 +361,12 @@ async function batchEditDevices(req, res) {
endDate: 'String',
edits: 'Object',
};
const NotMustHaveKeys = ["deviceName", "power", "state", "deviceMode", "startDate", "endDate"];
let { regionKeys, deviceName, power, state, deviceMode, startDate, endDate, edits } =
const NotMustHaveKeys = ["regionKeys", "deviceIds", "deviceName", "power", "state", "deviceMode", "startDate", "endDate"];
let { regionKeys, deviceIds, deviceName, power, state, deviceMode, startDate, endDate, edits } =
eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!regionKeys || !regionKeys.length) {
res.success({success: false, msg: "缺少必填参数:regionKeys"});
if ((!regionKeys || !regionKeys.length) && (!deviceIds || !deviceIds.length)) {
res.success({success: false, msg: "缺少必填参数:regionKeys 或 deviceIds"});
return;
}
if (!edits || typeof edits !== 'object') {
......@@ -394,6 +395,7 @@ async function batchEditDevices(req, res) {
const data = await deviceBiz.batchEditDevices({
regionKeys,
deviceIds: deviceIds,
deviceName: deviceName || undefined,
power: power || undefined,
state: state || undefined,
......@@ -533,14 +535,14 @@ async function getLinkageList(req, res) {
async function getLinkageDetail(req, res) {
const id = Number(req.query?.id);
if (!id) {
res.error("缺少必填参数:id");
res.success({success: false, msg: "缺少必填参数:id"});
return;
}
const data = await deviceLinkageBiz.getLinkageDetail(id);
if (data.isSuccess) {
res.success(data.data);
} else {
res.error(data.msg);
res.success({success: false, msg: data.msg});
}
}
......@@ -571,7 +573,7 @@ async function addLinkage(req, res) {
let { deviceId, linkageType, thresholdMax, thresholdMin, thresholdMid, settingMax, settingMin, settingMid, fanSpeedMax, fanSpeedMid, fanSpeedMin, divideMethod, beginValidity, endValidity } =
eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!deviceId || !linkageType) {
res.error("缺少必填参数:deviceId, linkageType");
res.success({success: false, msg: "缺少必填参数:deviceId, linkageType"});
return;
}
const data = await deviceLinkageBiz.addLinkage({
......@@ -620,7 +622,7 @@ async function batchAddLinkage(req, res) {
let { deviceIds, linkageType, thresholdMax, thresholdMin, thresholdMid, settingMax, settingMin, settingMid, fanSpeedMax, fanSpeedMid, fanSpeedMin, divideMethod, beginValidity, endValidity } =
eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0 || !linkageType) {
res.error("缺少必填参数:deviceIds, linkageType");
res.success({success: false, msg: "缺少必填参数:deviceIds, linkageType"});
return;
}
const data = await deviceLinkageBiz.batchAddLinkage({
......@@ -669,7 +671,7 @@ async function editLinkage(req, res) {
const NotMustHaveKeys = ["deviceId", "linkageType", "thresholdMax", "thresholdMin", "thresholdMid", "settingMax", "settingMin", "settingMid", "fanSpeedMax", "fanSpeedMid", "fanSpeedMin", "isUse", "divideMethod", "beginValidity", "endValidity"];
let { id, deviceId, linkageType, thresholdMax, thresholdMin, thresholdMid, settingMax, settingMin, settingMid, fanSpeedMax, fanSpeedMid, fanSpeedMin, isUse, divideMethod, beginValidity, endValidity } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!id) {
res.error("缺少必填参数:id");
res.success({success: false, msg: "缺少必填参数:id"});
return;
}
const data = await deviceLinkageBiz.editLinkage({
......@@ -701,7 +703,7 @@ async function editLinkage(req, res) {
async function deleteLinkage(req, res) {
const { id } = req.body || {};
if (!id) {
res.error("缺少必填参数:id");
res.success({success: false, msg: "缺少必填参数:id"});
return;
}
const data = await deviceLinkageBiz.deleteLinkage(Number(id));
......
......@@ -4,18 +4,38 @@
import asyncHandler from 'express-async-handler';
import * as customsBiz from '../biz/customs';
import { eccReqParamater } from '../util/verificationParam';
import { getCache, setCache } from '../util/ttlCache';
export function setRouter(httpServer) {
/** 客流体验分析 */
httpServer.post('/api/dq/customs/analysis', asyncHandler(getCustomsAnalysis));
/** 客流体验环境指标弹窗 */
httpServer.post('/api/dq/customs/env_popup', asyncHandler(getCustomsEnvPopup));
}
/**
* 客流体验分析
* 大屏只读接口:加 30s TTL 缓存兜底,避免高频轮询重复打库
*/
async function getCustomsAnalysis(req, res) {
const CACHE_KEY = 'dq:customs:analysis';
const CACHE_TTL = 30 * 1000;
async function getCustomsAnalysis(req, res) {
const { } = req.body;
const cached = getCache(CACHE_KEY);
if (cached) return res.success(cached);
const result = await customsBiz.getCustomsAnalysis();
setCache(CACHE_KEY, result, CACHE_TTL);
res.success(result);
}
/**
* 客流体验环境指标弹窗
* @param req
* @param res
*/
async function getCustomsEnvPopup(req, res) {
const { env } = req.body;
const result = await customsBiz.getCustomsEnvPopup(env);
res.success(result);
}
\ No newline at end of file
......@@ -8,6 +8,8 @@ const config = {
"/api/public/deviceType":enumBiz.DEVICE_TYPE,//设备类型
"/api/public/deviceState":enumBiz.DEVICE_STATE,//设备状态
"/api/public/regionType":enumBiz.REGION_TYPE,//区域类型
"/api/public/fanSpeed":enumBiz.fanSpeedMap,//风速
"/api/public/workMode":enumBiz.workModeMap,//模式
}
export function setRouter(httpServer) {
......
......@@ -4,6 +4,7 @@
import asyncHandler from 'express-async-handler';
import * as runningBiz from '../biz/running';
import { eccReqParamater } from '../util/verificationParam';
import { getCache, setCache } from '../util/ttlCache';
export function setRouter(httpServer) {
......@@ -29,10 +30,16 @@ export function setRouter(httpServer) {
/**
* 运行分析
* 大屏只读接口:加 30s TTL 缓存兜底,避免高频轮询重复打库
*/
const CACHE_TTL = 30 * 1000;
async function getRunAnalysis(req, res) {
const { } = req.body;
const CACHE_KEY = 'dq:run:analysis';
const cached = getCache(CACHE_KEY);
if (cached) return res.success(cached);
const result = await runningBiz.getRunAnalysis();
setCache(CACHE_KEY, result, CACHE_TTL);
res.success(result);
}
......@@ -67,10 +74,15 @@ async function getRunEnvironmental(req, res) {
/**
* 智能监控
* 大屏只读接口:加 30s TTL 缓存兜底,避免高频轮询重复打库
*/
async function getRunMonitoring(req, res) {
const { } = req.body;
const CACHE_KEY = 'dq:run:monitoring';
const cached = getCache(CACHE_KEY);
if (cached) return res.success(cached);
const result = await runningBiz.getRunMonitoring();
setCache(CACHE_KEY, result, CACHE_TTL);
res.success(result);
}
......
/**
* 进程内 TTL 缓存(用于大屏只读分析接口防抖)
* 注意:仅适合无参或参数极少的只读接口;服务重启后缓存失效。
*/
interface CacheItem {
expire: number;
data: any;
}
const DEFAULT_TTL = 30 * 1000; // 默认 30 秒
const MAX_SIZE = 200; // 缓存条目上限,防止异常 key 膨胀
const store = new Map<string, CacheItem>();
export function getCache<T = any>(key: string): T | undefined {
const item = store.get(key);
if (!item) return undefined;
if (item.expire <= Date.now()) {
store.delete(key);
return undefined;
}
return item.data;
}
export function setCache(key: string, data: any, ttlMs: number = DEFAULT_TTL) {
if (store.size >= MAX_SIZE) {
// 简单淘汰:优先清掉已过期项;都没有过期则整体清空(极少发生)
const now = Date.now();
for (const [k, v] of store) {
if (v.expire <= now) {
store.delete(k);
break;
}
}
if (store.size >= MAX_SIZE) store.clear();
}
store.set(key, { expire: Date.now() + ttlMs, data });
}
/** 主动失效(例如数据被写入后需要立刻反映的场景) */
export function delCache(key: string) {
store.delete(key);
}
......@@ -10,6 +10,7 @@
"strict": false,
"noImplicitAny": false,
"strictNullChecks": false,
"skipLibCheck": true,
"types": ["node"]
},
"exclude": [
......
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