部署服务、调整数据结构、更新获取区域接口

parent d7ed4a95
...@@ -14,6 +14,6 @@ ...@@ -14,6 +14,6 @@
<mysqlPort>3306</mysqlPort> <mysqlPort>3306</mysqlPort>
<mysqlUser>root</mysqlUser> <mysqlUser>root</mysqlUser>
<mysqlPwd>qaz123456</mysqlPwd> <mysqlPwd>qaz123456</mysqlPwd>
<dataBase>unicarbon_platform</dataBase> --> <dataBase>beermuseumplatform</dataBase> -->
</mysqldb> </mysqldb>
</config> </config>
/**
* 飞奕云平台 API 客户端
* 参考 region.ts 的编码风格:async/await、BizError、清晰的注释
*/
import * as crypto from 'crypto';
import { post } from '../util/request';
import { BizError } from '../util/bizError';
import { ERRORENUM } from '../config/errorEnum';
/**
* 飞奕云平台配置(建议从环境变量或配置文件中读取)
*/
const FEIYI_CONFIG = {
baseUrl: 'https://m.achelp.cn',
clientId: process.env.FEIYI_CLIENT_ID || '419636996764598272', // 我方提供的 clientId
secretKey: process.env.FEIYI_SECRET_KEY || '02fc75446b4544b3a02444a2b5f9be2b', // 我方提供的 appSecret
};
/**
* 生成随机字符串 nonce
* @param length 长度,默认 16
*/
function generateNonce(length: number = 16): string {
return crypto.randomBytes(length).toString('hex');
}
/**
* 生成签名 sign
* 算法:
* 1. 除去空值请求参数(accessKey 即使为空串也保留)
* 2. 按照参数名的字母升序排列非空请求参数(包含 accessKey)
* 3. 使用 URL 键值对格式拼接(key1=value1&key2=value2...)
* 4. 拼接上 SecretKey 得到 stringSignTemp
* 5. 对 stringSignTemp 进行 MD5 运算,并将结果所有字符转换为大写
* @param params 请求参数对象
* @param secretKey 密钥
*/
function generateSign(params: Record<string, any>, secretKey: string): string {
// 1. 过滤掉值为 null 或 undefined 的参数(空字符串保留)
const filteredParams: Record<string, any> = {};
for (const key in params) {
if (params[key] !== null && params[key] !== undefined) {
filteredParams[key] = params[key];
}
}
// 2. 按 key 升序排序
const sortedKeys = Object.keys(filteredParams).sort();
// 3. 拼接成 key1=value1&key2=value2...
const paramStr = sortedKeys.map(key => `${key}=${filteredParams[key]}`).join('&');
// 4. 拼接 SecretKey
const stringSignTemp = paramStr + secretKey;
// 5. MD5 并转大写
const md5 = crypto.createHash('md5').update(stringSignTemp).digest('hex');
return md5.toUpperCase();
}
/**
* 获取飞奕云平台 access_token
* @returns {Promise<string>} access_token
* @throws 如果请求失败或响应码非 00000,则抛出 BizError
*/
export async function getFeiYiToken(): Promise<string> {
const timestamp = Date.now().toString(); // 毫秒时间戳
const nonce = generateNonce();
const params = {
accessKey: '', // 文档要求传空串
clientId: FEIYI_CONFIG.clientId,
nonce: nonce,
timestamp: timestamp,
};
const sign = generateSign(params, FEIYI_CONFIG.secretKey);
const requestBody = {
...params,
sign: sign,
};
const url = `${FEIYI_CONFIG.baseUrl}/open/oauth2/openapi/token`;
// 注意:原 post 方法有 bug(成功时 resolve,失败时没有 reject),此处建议使用修复后的 post 或直接使用 request 库
// 这里为了兼容,使用修复逻辑(若原 post 不可靠,可用下面的实现)
let result: any;
try {
result = await post(url, requestBody, {});
} catch (err) {
throw new BizError(ERRORENUM.网络错误, `获取 token 网络失败: ${err.message}`);
}
// 检查响应结构:文档中返回 { code, data: { access_token } }
if (!result || result.code !== '00000') {
throw new BizError(ERRORENUM.第三方接口错误, `获取 token 失败: ${result?.message || '未知错误'}`);
}
const accessToken = result.data?.access_token;
if (!accessToken) {
throw new BizError(ERRORENUM.第三方接口错误, '响应中未包含 access_token');
}
return accessToken;
}
/**
* 带 Token 的 POST 请求封装
* 自动获取/刷新 token(简单实现,生产环境建议做 token 缓存和过期刷新)
* @param path 接口路径(相对路径)
* @param body 请求体
* @param retry 是否重试(用于 token 过期重试)
*/
async function feiYiPost(path: string, body: any, retry: boolean = true): Promise<any> {
const token = await getFeiYiToken(); // 每次都获取最新 token(简单起见,可缓存)
const url = `${FEIYI_CONFIG.baseUrl}${path}`;
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
};
let result: any;
try {
result = await post(url, body, headers);
} catch (err) {
throw new BizError(ERRORENUM.网络错误, `请求 ${path} 失败: ${err.message}`);
}
// 如果 token 过期(示例错误码可能为 token 过期,文档未明确,假设 code 为 401 或其他)
// 这里简单判断如果 code 不为 '00000' 且包含 token 相关错误,则重试一次
if (result.code !== '00000') {
if (retry && (result.code === '401' || result.message?.includes('token'))) {
// 可选:清除 token 缓存,重新获取
return feiYiPost(path, body, false);
}
throw new BizError(ERRORENUM.第三方接口错误, `接口 ${path} 调用失败: ${result.message || JSON.stringify(result)}`);
}
return result;
}
/**
* 获取网关设备列表(分页)
* @param page 当前页码,默认 1
* @param limit 每页条数,默认 10
* @returns 网关设备数据,包含 rows、total 等
*/
export async function getGatewayDeviceList(page: number = 1, limit: number = 10): Promise<any> {
const path = '/open/openApi/gw/page';
const body = { page, limit };
const result = await feiYiPost(path, body);
// 返回 data 部分,包含 page, limit, total, rows
return result.data;
}
\ No newline at end of file
...@@ -128,23 +128,19 @@ export async function handleDeviceFaultPush( deviceId: string, faultType: string ...@@ -128,23 +128,19 @@ export async function handleDeviceFaultPush( deviceId: string, faultType: string
/** /**
* 智能管控-运行分析 * 智能管控-运行分析
* @param regionKey 区域标识(可选),如“A馆1F对应1”,如果提供则返回该区域的分析数据 * @param regionGroup 区域组(可选),如“A馆”,如果提供则返回该类型下所有区域的分析数据
* @param regionType 区域类型(可选),如“A馆”,如果提供则返回该类型下所有区域的分析数据 * @param regionType 区域类型(可选),如“1F”,如果提供则返回该区域的分析数据
* @returns 大屏所需的所有指标数据 * @returns 大屏所需的所有指标数据
*/ */
export async function getRunAnalysis(regionKey: number, regionType: string) { export async function getRunAnalysis(regionGroup: string, regionType: string) {
// 根据区域类型和名称筛选设备ID列表(如果提供了区域信息) // 根据区域类型和名称筛选设备ID列表(如果提供了区域信息)
let regionKeys: any = []; let regionParam: any = { groups: regionGroup };
if (regionType) { if (regionType) {
let regionInfo: any = await selectDataListByParam(TABLENAME.区域表, { regionParam.type = regionType;
type: regionType
}, ["id"]);
regionKeys = regionInfo.data.map((r: any) => r.id);
}
if (regionKey) {
regionKeys.push(regionKey);
} }
let regionInfo: any = await selectDataListByParam(TABLENAME.区域表, regionParam, ["id"]);
let regionKeys = regionInfo.data.map((r: any) => r.id);
console.log("筛选区域Key:", regionKeys); console.log("筛选区域Key:", regionKeys);
// 1.1 获取所选区域三相电表及电能监测设备的用电数据(用于能耗管理) // 1.1 获取所选区域三相电表及电能监测设备的用电数据(用于能耗管理)
...@@ -489,7 +485,7 @@ export async function getRunAnalysis(regionKey: number, regionType: string) { ...@@ -489,7 +485,7 @@ export async function getRunAnalysis(regionKey: number, regionType: string) {
} }
// 7.1 获取区域列表接口,供前端下拉选择区域类型和区域名称 // 7.1 获取区域列表接口,供前端下拉选择区域类型和区域名称
let regionList: any = await getRegionList(regionType); let regionList: any = await getRegionList(regionGroup);
// 最终组装返回数据 // 最终组装返回数据
return { return {
...@@ -580,6 +576,13 @@ export async function getAcRunningPop(regionKey: number, deviceId: string) { ...@@ -580,6 +576,13 @@ export async function getAcRunningPop(regionKey: number, deviceId: string) {
} }
} }
/**
* 设备运行弹框 - 空调启停
* @param deviceId
*/
export async function controlAcRunning(deviceId?: string) {
}
// 辅助函数:获取用电趋势(按间隔分组) // 辅助函数:获取用电趋势(按间隔分组)
async function getEnergyTrend(deviceIds: string[], startTime: Date, endTime: Date, granularity: 'hour' | 'day' | 'month') { async function getEnergyTrend(deviceIds: string[], startTime: Date, endTime: Date, granularity: 'hour' | 'day' | 'month') {
if (deviceIds.length === 0) return []; if (deviceIds.length === 0) return [];
...@@ -686,25 +689,29 @@ let deviceTypeKeyMap: { [key: string]: string } = { ...@@ -686,25 +689,29 @@ let deviceTypeKeyMap: { [key: string]: string } = {
* 获取所有区域列表(供前端下拉选择) * 获取所有区域列表(供前端下拉选择)
* @returns 区域列表,按 sort_order 排序 * @returns 区域列表,按 sort_order 排序
*/ */
export async function getRegionList( regionType: string ) { export async function getRegionList( regionGroup: string ) {
let where: any = { "%orderAsc%": "sort_order" }; let where: any = { "%orderAsc%": "sort_order" };
if (regionType) { if (regionGroup) {
where.type = regionType; where.groups = regionGroup;
} }
let regionList = await selectDataListByParam( let regionList = await selectDataListByParam(
TABLENAME.区域表, TABLENAME.区域表,
where, where,
["id", "name", "type"], ["type", "groups"],
); );
let regionGroupTypeArr: any = [];
let regionMap: any = {}; let regionMap: any = {};
regionList.data.forEach((r: any) => { regionList.data.forEach((r: any) => {
let regionArr = []; let regionArr = [];
if (regionMap[r.type]) { if (regionMap[r.groups]) {
regionArr = regionMap[r.type]; regionArr = regionMap[r.groups];
}
if (!regionGroupTypeArr.includes(r.groups + "-" + r.type)) {
regionGroupTypeArr.push(r.groups + "-" + r.type);
regionArr.push({regionType: r.type, regionGroup: r.groups});
regionMap[r.groups] = regionArr;
} }
regionArr.push({regionKey: r.id, regionName: r.name});
regionMap[r.type] = regionArr;
}); });
console.log("区域列表:", regionMap); console.log("区域列表:", regionMap);
......
...@@ -45,7 +45,9 @@ export enum ERRORENUM { ...@@ -45,7 +45,9 @@ export enum ERRORENUM {
空文件失败, 空文件失败,
文件上传失败, 文件上传失败,
只能上传docdocxexcelpngjpg图片, 只能上传docdocxexcelpngjpg图片,
该时间段已有预约 该时间段已有预约,
网络错误,
第三方接口错误
} }
/** /**
......
import { level } from "winston";
const { Sequelize, DataTypes } = require('sequelize'); const { Sequelize, DataTypes } = require('sequelize');
export const TablesConfig = [ export const TablesConfig = [
// 区域配置表
{
tableNameCn: '区域配置表',
tableName: 'region',
schema: {
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '区域编号(regionKey)'
},
name: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '楼层'
},
type: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '所属楼栋'
},
max_capacity: {
type: DataTypes.NUMBER(10, 2),
allowNull: false,
comment: '最大承载量'
},
sort_order: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '排序序号'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '创建时间'
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '更新时间'
}
},
association: [
{ type: "hasMany", target: "device", foreignKey: "region_key" }
]
},
// 设备主表(关联区域表)
{
tableNameCn: '设备表',
tableName: 'device',
schema: {
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '自增主键'
},
device_id: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true,
comment: '设备唯一标识(由设备商提供)'
},
region_key: {
type: DataTypes.INTEGER,
allowNull: false,
comment: '区域编号,关联region.id'
},
device_type: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '设备类型,如“空调”“出风口”'
},
device_name: {
type: DataTypes.STRING(100),
allowNull: true,
comment: '设备名称(可选)'
},
control_params: {
type: DataTypes.JSON,
allowNull: true,
comment: '该设备支持的参数定义,JSON格式{"power":["on","off"]}'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '创建时间'
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '更新时间'
}
},
association: [
{ type: "belongsTo", target: "region", foreignKey: "region_key" },
{ type: "hasMany", target: "device_data", foreignKey: "device_id" }
]
},
// 设备数据记录表(存储推送的实时数据) // 设备数据记录表(存储推送的实时数据)
{ {
tableNameCn: '设备数据表', tableNameCn: '设备数据表',
...@@ -129,7 +21,7 @@ export const TablesConfig = [ ...@@ -129,7 +21,7 @@ export const TablesConfig = [
data: { data: {
type: DataTypes.JSON, type: DataTypes.JSON,
allowNull: false, allowNull: false,
comment: '推送的数据,如{"power":"on","temperature":24,"mode":"cool","fan_speed":"medium","auto_control":"on"}' comment: '推送的数据,如{power:on,temperature:24,mode:cool,fan_speed:medium,auto_control:on}'
}, },
received_time: { received_time: {
type: DataTypes.DATE, type: DataTypes.DATE,
...@@ -149,7 +41,6 @@ export const TablesConfig = [ ...@@ -149,7 +41,6 @@ export const TablesConfig = [
} }
}, },
association: [ association: [
{ type: "belongsTo", target: "device", foreignKey: "device_id" }
] ]
}, },
// 设备故障表 // 设备故障表
...@@ -215,7 +106,62 @@ export const TablesConfig = [ ...@@ -215,7 +106,62 @@ export const TablesConfig = [
} }
}, },
association: [ association: [
{ type: "belongsTo", target: "device", foreignKey: "device_id" } ]
},
// 设备主表(关联区域表)
{
tableNameCn: '设备表',
tableName: 'device',
schema: {
id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '自增主键'
},
device_id: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true,
comment: '设备唯一标识(由设备商提供)'
},
region_key: {
type: DataTypes.INTEGER,
allowNull: false,
comment: '区域编号,关联region.id'
},
device_type: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '设备类型,如“空调”“出风口”'
},
device_name: {
type: DataTypes.STRING(100),
allowNull: true,
comment: '设备名称(可选)'
},
control_params: {
type: DataTypes.JSON,
allowNull: true,
comment: '该设备支持的参数定义,JSON格式{"power":["on","off"]}'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '创建时间'
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '更新时间'
}
},
association: [
{ type: "hasMany", target: "device_data", foreignKey: "device_id" },
{ type: "hasMany", target: "device_fault", foreignKey: "device_id" }
] ]
}, },
// 人员信息表 // 人员信息表
...@@ -251,7 +197,64 @@ export const TablesConfig = [ ...@@ -251,7 +197,64 @@ export const TablesConfig = [
type: Sequelize.STRING(255), type: Sequelize.STRING(255),
Comment: '用户名称' Comment: '用户名称'
} }
},
association: [
]
},
// 区域配置表
{
tableNameCn: '区域配置表',
tableName: 'region',
schema: {
id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '区域编号(regionKey)'
},
name: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '区域名称'
},
type: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '所属楼层'
},
groups: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '所属楼栋'
},
max_capacity: {
type: DataTypes.INTEGER,
allowNull: false,
comment: '最大承载量'
},
sort_order: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '排序序号'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '创建时间'
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '更新时间'
} }
} },
association: [
{ type: "hasMany", target: "device", foreignKey: "region_key" }
]
},
]; ];
...@@ -27,6 +27,8 @@ export function setRouter(httpServer) { ...@@ -27,6 +27,8 @@ export function setRouter(httpServer) {
httpServer.post('/api/zc/run/analysis/Floor', asyncHandler(getRunAnalysisByFloor)); httpServer.post('/api/zc/run/analysis/Floor', asyncHandler(getRunAnalysisByFloor));
/** 空调运行弹框 */ /** 空调运行弹框 */
httpServer.post('/api/zc/run/monitor/acPop', asyncHandler(getAcRunningPop)); httpServer.post('/api/zc/run/monitor/acPop', asyncHandler(getAcRunningPop));
/** 空调开关 */
httpServer.post('/api/zc/run/monitor/acRun', asyncHandler(controlAcRunning));
/**运行分析-弹窗 */ /**运行分析-弹窗 */
httpServer.post('/api/zc/run/analysis/pop', asyncHandler(getRunAnalysisPop)); httpServer.post('/api/zc/run/analysis/pop', asyncHandler(getRunAnalysisPop));
...@@ -47,11 +49,16 @@ export function setRouter(httpServer) { ...@@ -47,11 +49,16 @@ export function setRouter(httpServer) {
res.success(result); res.success(result);
} }
/**
* 获取区域列表接口,供前端下拉选择区域类型和区域名称
* @param req
* @param res
*/
async function getRegionList(req, res) { async function getRegionList(req, res) {
let reqConf = {regionType:'String'}; let reqConf = {regionGroup:'String'};
const NotMustHaveKeys = []; const NotMustHaveKeys = [];
let { regionType } = eccReqParamater(reqConf, req.body, NotMustHaveKeys); let { regionGroup } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await regionBiz.getRegionList(regionType); const result = await regionBiz.getRegionList(regionGroup);
res.success(result); res.success(result);
} }
...@@ -87,7 +94,7 @@ async function deviceFaultPush(req, res) { ...@@ -87,7 +94,7 @@ async function deviceFaultPush(req, res) {
*/ */
async function getRunAnalysis(req, res) { async function getRunAnalysis(req, res) {
const { } = req.body; const { } = req.body;
const result = await regionBiz.getRunAnalysis(0,""); const result = await regionBiz.getRunAnalysis("","");
res.success(result); res.success(result);
} }
...@@ -95,10 +102,10 @@ async function deviceFaultPush(req, res) { ...@@ -95,10 +102,10 @@ async function deviceFaultPush(req, res) {
* 运行分析-楼栋 * 运行分析-楼栋
*/ */
async function getRunAnalysisByBuilding(req, res) { async function getRunAnalysisByBuilding(req, res) {
let reqConf = {regionType:'String'}; let reqConf = {regionGroup:'String'};
const NotMustHaveKeys = []; const NotMustHaveKeys = [];
let { regionType } = eccReqParamater(reqConf, req.body, NotMustHaveKeys); let { regionGroup } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await regionBiz.getRunAnalysis(0, regionType); const result = await regionBiz.getRunAnalysis("", regionGroup);
res.success(result); res.success(result);
} }
...@@ -106,10 +113,10 @@ async function deviceFaultPush(req, res) { ...@@ -106,10 +113,10 @@ async function deviceFaultPush(req, res) {
* 运行分析-楼层 * 运行分析-楼层
*/ */
async function getRunAnalysisByFloor(req, res) { async function getRunAnalysisByFloor(req, res) {
let reqConf = {regionKey:'Number'}; let reqConf = {regionGroup:'String', regionType:'String'};
const NotMustHaveKeys = []; const NotMustHaveKeys = [];
let { regionKey } = eccReqParamater(reqConf, req.body, NotMustHaveKeys); let { regionGroup, regionType } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await regionBiz.getRunAnalysis(regionKey, ""); const result = await regionBiz.getRunAnalysis(regionType, regionGroup);
res.success(result); res.success(result);
} }
...@@ -125,6 +132,17 @@ async function deviceFaultPush(req, res) { ...@@ -125,6 +132,17 @@ async function deviceFaultPush(req, res) {
} }
/** /**
* 控制空调开关
*/
async function controlAcRunning(req, res) {
let reqConf = {deviceId:'String'};
const NotMustHaveKeys = [];
let { deviceId } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await regionBiz.controlAcRunning(deviceId);
res.success(result);
}
/**
* 运行分析-弹窗 * 运行分析-弹窗
*/ */
async function getRunAnalysisPop(req, res) { async function getRunAnalysisPop(req, res) {
......
/**
*
*/
import asyncHandler from 'express-async-handler';
import * as feiyiClientBiz from '../biz/feiyiClient';
import { eccReqParamater } from '../util/verificationParam';
export function setRouter(httpServer) {
/** 注册/更新设备信息 */
httpServer.post('/api/zc/feiyi/gateway/list', asyncHandler(getFeiyiGatewayList));
}
/**
* 注册或更新设备信息
*/
async function getFeiyiGatewayList(req, res) {
let reqConf = {page:'Number', limit:'Number'};
const NotMustHaveKeys = [ "page", "limit" ];
let { page, limit } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await feiyiClientBiz.getGatewayDeviceList(page, limit);
res.success(result);
}
...@@ -3,9 +3,11 @@ ...@@ -3,9 +3,11 @@
*/ */
import * as deviceRouter from './device'; import * as deviceRouter from './device';
import * as feiyiClientRouter from './feiyiClient';
export function setRouter(httpServer) { export function setRouter(httpServer) {
deviceRouter.setRouter(httpServer); deviceRouter.setRouter(httpServer);
feiyiClientRouter.setRouter(httpServer);
} }
......
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