管理后台联调和问题修复

parent b29582fb
......@@ -4,11 +4,11 @@
<img>http://127.0.0.1:80</img>
<mysqldb>
<!-- 本地mysql配置 -->
<!-- <mysqlHost>127.0.0.1</mysqlHost>
<mysqlHost>127.0.0.1</mysqlHost>
<mysqlPort>3306</mysqlPort>
<mysqlUser>root</mysqlUser>
<mysqlPwd>123456</mysqlPwd>
<dataBase>dst_technology_platform</dataBase> -->
<dataBase>dst_technology_platform</dataBase>
<!-- 服务器mysql配置 -->
<!-- <mysqlHost>127.0.0.1</mysqlHost>
<mysqlPort>13306</mysqlPort>
......@@ -16,11 +16,11 @@
<mysqlPwd>root</mysqlPwd>
<dataBase>dst_technology_platform</dataBase> -->
<!-- dst服务器mysql配置 49.235.185.26 -->
<mysqlHost>127.0.0.1</mysqlHost>
<!-- <mysqlHost>127.0.0.1</mysqlHost>
<mysqlPort>3306</mysqlPort>
<mysqlUser>deploy</mysqlUser>
<mysqlPwd>Qaz123456!</mysqlPwd>
<dataBase>dst_technology_platform</dataBase>
<dataBase>dst_technology_platform</dataBase> -->
</mysqldb>
<feiyi>
<feiyiAppKey>444940462151958528</feiyiAppKey>
......@@ -33,5 +33,7 @@
<diqinAppKey>178486189110050</diqinAppKey>
<diqinSecretKey>CQuwrqgtaQhnwgcChB9Ejgtt</diqinSecretKey>
<diqinBaseUrl>https://airiccc.com</diqinBaseUrl>
<!-- PID-TVOC 换算系数(ppb → μg/m³),入库时换算,默认 0.0013 -->
<diqinTvocConvertFactor>0.0013</diqinTvocConvertFactor>
</diqin>
</config>
......@@ -219,6 +219,27 @@ export async function getIndoorUnitList(params: any): Promise<any> {
}
/**
* 单条查询内机
*
* @param deviceId
* @returns
*/
export async function getIndoorUnitStatus(deviceId: string): Promise<any> {
const path = '/openApi/indoorUnit/list';
const body = { indoorUnitAddressFull: deviceId };
const indoorUnits = await feiYiPost(path, body);
let indoorUnit = indoorUnits.data?.rows?.[0] || null;
let result: any = {};
result.mode = indoorUnit.workMode;
result.power = indoorUnit.onOff;
result.fanSpeed = indoorUnit.fanSpeed;
result.setTemp = indoorUnit.tempSet;
result.minTemp = indoorUnit.tempSetLo;
result.maxTemp = indoorUnit.tempSetHi;
return result;
}
/**
* 空调内机控制
* @param params
* @returns
......
import { selectDataListToPageByParam, selectDataCountByParam, selectDataListByParam } from "../data/findData";
import { TABLENAME } from "../config/dbEnum";
/**
* 获取设备日志列表(分页)
* 列表展示:标题、区域、设备、操作内容、操作人、操作时间
* 通过关联查询补全设备名称和区域名称
*/
export async function getDeviceLogsList(params: {
pageNumber?: number;
pageSize?: number;
}) {
const pageNumber = params.pageNumber || 1;
const pageSize = params.pageSize || 10;
// 1. 查询总数
const countRes = await selectDataCountByParam(TABLENAME.设备日志表, {});
const total = typeof countRes.data === 'number' ? countRes.data : 0;
if (total === 0) {
return { total: 0, rows: [] };
}
// 2. 分页查询日志(按操作时间倒序)
const logRes = await selectDataListToPageByParam(
TABLENAME.设备日志表,
{ '%orderDesc%': 'operation_time' },
['id', 'device_id', 'title', 'operation_content', 'operator', 'operation_time'],
pageNumber,
pageSize
);
const logs = (logRes.data || []) as any[];
if (logs.length === 0) {
return { total, rows: [] };
}
// 3. 收集所有涉及到的 device_id,批量查设备信息
const deviceIdSet = [...new Set(logs.map((l: any) => l.device_id).filter(Boolean))];
let deviceMap = new Map<string, any>();
let regionMap = new Map<string, any>();
if (deviceIdSet.length > 0) {
const deviceRes = await selectDataListByParam(
TABLENAME.设备表,
{ device_id: { '%in%': deviceIdSet } },
['device_id', 'device_name', 'region_key']
);
const devices = (deviceRes.data || []) as any[];
// 构建设备映射
devices.forEach((d: any) => {
deviceMap.set(d.device_id, d);
});
// 收集区域 key,批量查区域
const regionKeySet = [...new Set(devices.map((d: any) => d.region_key).filter(Boolean))];
if (regionKeySet.length > 0) {
const regionRes = await selectDataListByParam(
TABLENAME.区域表,
{ region_key: { '%in%': regionKeySet } },
['region_key', 'region_name']
);
(regionRes.data || []).forEach((r: any) => {
regionMap.set(r.region_key, r);
});
}
}
// 4. 组装返回数据
const rows = logs.map((log: any) => {
const device = deviceMap.get(log.device_id);
const deviceName = device ? device.device_name : log.device_id;
const regionKey = device ? device.region_key : null;
const regionInfo = regionKey ? regionMap.get(regionKey) : null;
const regionName = regionInfo ? regionInfo.region_name : '';
// operation_content JSON 转字符串
let contentStr = '';
if (log.operation_content) {
const parsed = typeof log.operation_content === 'string'
? JSON.parse(log.operation_content)
: log.operation_content;
// 提取关键信息拼接,排除 option/device_id/success/message 等元信息
const keyLabels: Record<string, string> = {
power: '开关',
setTemp: '设定温度',
fanSpeed: '风速',
deviceMode: '模式',
linkageStart: '联动',
};
const parts: string[] = [];
for (const [k, label] of Object.entries(keyLabels)) {
if (parsed[k] !== undefined) {
parts.push(`${label}:${parsed[k]}`);
}
}
if (parts.length > 0) {
contentStr = parts.join(',');
} else {
contentStr = JSON.stringify(parsed);
}
}
return {
id: log.id,
title: log.title || '',
region: regionName,
device: deviceName,
operationContent: contentStr,
operator: log.operator || '',
operationTime: log.operation_time,
};
});
return { total, list: rows };
}
/**
* 用户管理业务
*/
import { selectOneDataByParam, selectPaginatedData } from '../data/findData';
import { addData } from '../data/addData';
import { updateManyData } from '../data/updateData';
import { delData } from '../data/delData';
import { TABLENAME } from '../config/dbEnum';
import { getPwdMd5 } from '../tools/system';
const USER_COLUMNS = ['login_id', 'name', 'phone', 'role', 'department', 'status', 'created_at', 'updated_at'];
/**
* 用户管理列表(分页)
* @param pageNumber 页码
* @param pageSize 每页条数
*/
export async function getUsersList(pageNumber: number, pageSize: number) {
const data = await selectPaginatedData(TABLENAME.用户信息表, {}, USER_COLUMNS, pageNumber, pageSize);
return data;
}
/**
* 用户详情
* @param loginId 登录账号
*/
export async function getUsersDetail(loginId: string) {
const result = await selectOneDataByParam(TABLENAME.用户信息表, { login_id: loginId }, USER_COLUMNS);
if (!result.data || Object.keys(result.data).length === 0) {
return null;
}
return result.data;
}
/**
* 新增用户
* @param params.login_id 账号
* @param params.name 用户名称
* @param params.phone 手机号
* @param params.role 角色
* @param params.department 部门
* @param params.pwd 密码
*/
export async function addUser(params: {
login_id: string;
name: string;
phone: string;
role: string;
department: string;
pwd: string;
}) {
// 校验账号唯一性
const exist = await selectOneDataByParam(TABLENAME.用户信息表, { login_id: params.login_id });
if (exist.data && Object.keys(exist.data).length > 0) {
throw new Error(`账号 ${params.login_id} 已存在`);
}
const now = new Date();
const hashedPwd = getPwdMd5(params.login_id, params.pwd);
await addData(TABLENAME.用户信息表, {
login_id: params.login_id,
name: params.name,
phone: params.phone,
role: params.role,
department: params.department,
pwd: hashedPwd,
status: '启用',
created_at: now,
updated_at: now,
});
return { success: true };
}
/**
* 编辑用户
* @param params.login_id 账号(定位用,必填)
* @param params.name 用户名称(可选)
* @param params.phone 手机号(可选)
* @param params.role 角色(可选)
* @param params.department 部门(可选)
* @param params.status 状态(可选)
* @param params.pwd 密码(可选,传了则更新)
*/
export async function editUser(params: {
login_id: string;
name?: string;
phone?: string;
role?: string;
department?: string;
status?: string;
pwd?: string;
}) {
// 校验账号是否存在
const exist = await selectOneDataByParam(TABLENAME.用户信息表, { login_id: params.login_id });
if (!exist.data || Object.keys(exist.data).length === 0) {
throw new Error(`账号 ${params.login_id} 不存在`);
}
const updates: any = { updated_at: new Date() };
if (params.name !== undefined) updates.name = params.name;
if (params.phone !== undefined) updates.phone = params.phone;
if (params.role !== undefined) updates.role = params.role;
if (params.department !== undefined) updates.department = params.department;
if (params.status !== undefined) updates.status = params.status;
if (params.pwd !== undefined) updates.pwd = getPwdMd5(params.login_id, params.pwd);
await updateManyData(TABLENAME.用户信息表, { login_id: params.login_id }, updates);
return { success: true };
}
/**
* 删除用户
* @param loginId 登录账号
*/
export async function deleteUser(loginId: string) {
// 校验账号是否存在
const exist = await selectOneDataByParam(TABLENAME.用户信息表, { login_id: loginId });
if (!exist.data || Object.keys(exist.data).length === 0) {
throw new Error(`账号 ${loginId} 不存在`);
}
await delData(TABLENAME.用户信息表, { login_id: loginId });
return { success: true };
}
......@@ -103,36 +103,57 @@ export enum RELATION_TYPE {
// ==================== 空调控制相关枚举 ====================
/**
* 工作模式枚举
* 开关控制映射:开=1, 关=0
*/
export enum WORK_MODE {
制冷 = 1,
制热 = 2,
送风 = 3,
除湿 = 4,
export enum powerMap {
"开" = 1,
"关" = 0,
"on" = 1,
"off" = 0,
}
/**
* 开关状态枚举
* 工作模式映射:1=制冷 2=制热 3=送风 4=除湿 0=自动
*/
export enum POWER_STATE {
on = 'on',
off = 'off',
export enum workModeMap {
"制冷" = 1,
"制热" = 2,
"送风" = 3,
"除湿" = 4,
"自动" = 0,
}
/**
* 风速枚举
* 风速映射:1=高风 2=中风 4=低风
*/
export enum FAN_SPEED {
超强 = 1,
= 2,
= 4,
export enum fanSpeedMap {
"高风" = 1,
"中风" = 2,
"低风" = 4,
}
/**
* 自动控制状态枚举
* acPop风速映射:1-自动 2-低风 3-中风 4-高风
*/
export enum AUTO_CONTROL {
生效 = '生效',
解除 = '解除',
export enum acPopFanSpeedMap {
"自动" = 1,
"低风" = 2,
"中风" = 3,
"高风" = 4,
}
/**
* 自动控制锁定映射:生效=0, 解除=1
*/
export enum lockMap {
"生效" = 0,
"解除" = 1,
}
/**
* 反向映射(数值 → 中文,展示用)
*/
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 FAN_SPEED_TEXT_MAP: Record<number, string> = { 1: '高风', 2: '中风', 4: '低风' };
export const DEVICE_STATE_TEXT_MAP: Record<number, string> = { 0: '在线', 1: '离线', 2: '故障' };
......@@ -9,7 +9,8 @@ export enum TABLENAME {
区域表 = 'region',
区域设备关联表 = 'region_device_rel',
日程表 = 'schedule',
设备操作日志表 = 'device_logs',
设备日志表 = 'device_logs',
日程设备关联表 = 'schedule_device',
设备故障表 = 'device_fault'
};
......
......@@ -19,3 +19,12 @@
gender_limit:{type:"Number"}, //性别限制(1-无,2-只限男生,3-只限女生)
restrict_to_class:{type:"Boolean"} //是否限制只能带本班级学生
};
/**
* 将 Date 对象格式化为本地时间字符串 'YYYY-MM-DD HH:mm:ss'
* 替代 toISOString() 避免 UTC 时区偏移问题
*/
export function formatLocalTime(date: Date): string {
const pad = (n: number) => String(n).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
......@@ -34,6 +34,27 @@ export const TablesConfig = [
type: Sequelize.STRING(255),
comment: '用户名称'
},
phone: {
type: Sequelize.STRING(20),
allowNull: true,
comment: '手机号'
},
role: {
type: Sequelize.STRING(50),
allowNull: true,
comment: '角色'
},
department: {
type: Sequelize.STRING(100),
allowNull: true,
comment: '部门'
},
status: {
type: Sequelize.STRING(20),
allowNull: false,
defaultValue: '启用',
comment: '状态:启用/停用'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
......@@ -336,7 +357,7 @@ export const TablesConfig = [
},
schedule_date: {
type: DataTypes.DATE,
allowNull: false,
allowNull: true,
comment: '日期'
},
ac_controls: {
......@@ -390,9 +411,48 @@ export const TablesConfig = [
association: [
]
},
// 设备操作日志
// 日程设备关联
{
tableNameCn: '设备操作日志表',
tableNameCn: '日程设备关联表',
tableName: 'schedule_device',
schema: {
id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '自增主键'
},
schedule_id: {
type: DataTypes.BIGINT,
allowNull: false,
comment: '日程ID,关联schedule.id'
},
device_id: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '设备ID,关联device.device_id'
},
region_key: {
type: DataTypes.BIGINT,
allowNull: false,
comment: '区域key,关联region.id,表示设备所属区域'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '创建时间'
}
},
association: [
{ type: "hasMany", target: "device", foreignKey: "device_id" },
{ type: "hasMany", target: "region", foreignKey: "region_key" }
]
},
// 设备日志表
{
tableNameCn: '设备日志表',
tableName: 'device_logs',
schema: {
id: {
......@@ -412,6 +472,11 @@ export const TablesConfig = [
allowNull: false,
comment: '设备类型'
},
title: {
type: Sequelize.STRING(255),
allowNull: true,
comment: '日志标题'
},
operation_content: {
type: Sequelize.TEXT,
allowNull: true,
......@@ -474,7 +539,7 @@ export const TablesConfig = [
},
fault_origin_id: {
type: Sequelize.STRING(100),
allowNull: false,
allowNull: true,
comment: '原始故障id'
},
fault_code: {
......
......@@ -6,7 +6,7 @@
* - 任务锁死时(超时未释放),会主动强制释放锁,确保后续任务能正常执行
*/
import { region, device, deviceData, alertWorkData, processFaultStatus, diqinSyncAll, migrateDeviceRegionKey } from "../biz/dataIntegration";
import { region, device, deviceData, alertWorkData, processFaultStatus, diqinSyncAll, migrateDeviceRegionKey, migrateDiqinTvocUnit } from "../biz/dataIntegration";
// ==================== 通用任务锁 ====================
......@@ -134,6 +134,7 @@ const dataSyncTask = createScheduledTask('定时任务', TASK_INTERVAL_MS, dataS
await deviceData();
await diqinSyncAll();
await migrateDeviceRegionKey();
await migrateDiqinTvocUnit();
});
// 预警工单任务
......
......@@ -49,10 +49,11 @@ export async function initConfig() {
// 迪勤云平台配置
if (diqin) {
let diqinConfig = diqin[0];
systemConfig.diqin = { appKey: '', secretKey: '', baseUrl: '' };
systemConfig.diqin = { appKey: '', secretKey: '', baseUrl: '', tvocConvertFactor: 0.0013 };
if (diqinConfig.diqinAppKey) systemConfig.diqin.appKey = diqinConfig.diqinAppKey[0];
if (diqinConfig.diqinSecretKey) systemConfig.diqin.secretKey = diqinConfig.diqinSecretKey[0];
if (diqinConfig.diqinBaseUrl) systemConfig.diqin.baseUrl = diqinConfig.diqinBaseUrl[0];
if (diqinConfig.diqinTvocConvertFactor) systemConfig.diqin.tvocConvertFactor = parseFloat(diqinConfig.diqinTvocConvertFactor[0]);
}
} catch(err) {
......
......@@ -26,5 +26,7 @@ export class ServerConfig {
appKey: string;
secretKey: string;
baseUrl: string;
/** PID-TVOC 换算系数(ppb → μg/m³),入库时换算,默认 0.0013 */
tvocConvertFactor: number;
}
}
\ No newline at end of file
......@@ -320,13 +320,8 @@ export async function selectPaginatedData( tableName: string, param: QueryParam,
const result = await tableModel.findAndCountAll(selectParam);
return {
data: result.rows,
pagination: {
page: pageNumber,
pageSize,
total: result.count,
totalPages: Math.ceil(result.count / pageSize)
}
list: result.rows,
total: result.count
};
}
......
......@@ -7,12 +7,14 @@ import { eccReqParamater } from '../util/verificationParam';
export function setRouter(httpServer) {
/** 区域位置 */
/** 区域坐标位置 */
httpServer.post('/api/dq/region/location', asyncHandler(getRegionLocation));
/** 区域下拉选择 */
httpServer.get('/api/dq/region/list', asyncHandler(getRegionList));
}
/**
* 区域位置
* 区域坐标位置
*/
async function getRegionLocation(req, res) {
let reqConf = {gatewayPage:'Number', hour: 'Number'};
......@@ -20,4 +22,12 @@ async function getRegionLocation(req, res) {
let { gatewayPage, hour } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await regionBiz.getRegionLocation(gatewayPage, hour);
res.success(result);
}
/**
* 区域下拉选择
*/
async function getRegionList(req, res) {
const result = await regionBiz.getRegionList();
res.success(result);
}
\ No newline at end of file
......@@ -5,6 +5,7 @@ import * as feiyiClientRouter from './feiyiClient';
import * as diqinClientRouter from './diqinClient';
import * as regionRouter from './region';
import * as runningRouter from './running';
import * as adminRouter from './admin';
export function setRouter(httpServer) {
......@@ -13,6 +14,8 @@ export function setRouter(httpServer) {
regionRouter.setRouter(httpServer);
runningRouter.setRouter(httpServer);
adminRouter.setRouter(httpServer);
}
......
......@@ -11,6 +11,8 @@ export function setRouter(httpServer) {
httpServer.post('/api/dq/run/analysis', asyncHandler(getRunAnalysis));
/** 运行分析弹窗 */
httpServer.post('/api/dq/run/analysis/popup', asyncHandler(getAnalysisPopup));
/** 运行分析弹窗 - 趋势图 */
httpServer.post('/api/dq/run/analysis/popup/trend', asyncHandler(getAnalysisPopupSubTrend));
/** 环境态势感知 */
httpServer.post('/api/dq/run/analysis/environmental', asyncHandler(getRunEnvironmental));
/** 智能监控 */
......@@ -45,6 +47,16 @@ async function getAnalysisPopup(req, res) {
}
/**
* 运行分析弹窗 - 趋势图
*/
async function getAnalysisPopupSubTrend(req, res) {
let reqConf = { currRegionKey: 'String', selectedRegionKey: 'String' };
let { currRegionKey, selectedRegionKey } = eccReqParamater(reqConf, req.body);
const result = await runningBiz.getAnalysisPopupSubTrend(currRegionKey, selectedRegionKey);
res.success(result);
}
/**
* 环境态势感知
*/
async function getRunEnvironmental(req, res) {
......
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