This commit is contained in:
D8D Developer
2025-06-27 02:01:03 +00:00
parent 24f7f73ebd
commit 612c91f9ff
6 changed files with 0 additions and 655 deletions

View File

@@ -1,39 +0,0 @@
export async function fetchWithRetry(
url: string,
options: {
method: string
headers: Record<string, string>
body: string
timeout: number
maxRetries: number
}
): Promise<Response> {
let lastError: unknown = null
for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
try {
const controller = new AbortController()
const timeoutId = setTimeout(
() => controller.abort(),
options.timeout
)
const response = await fetch(url, {
...options,
signal: controller.signal
})
clearTimeout(timeoutId)
return response
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error))
if (attempt < options.maxRetries) {
await new Promise(resolve =>
setTimeout(resolve, 1000 * (attempt + 1)))
}
}
}
throw lastError instanceof Error ? lastError : new Error(String(lastError || '请求失败'))
}

View File

@@ -1,154 +0,0 @@
import debug from "debug"
import net from 'node:net';
const log = {
app: debug('ip_monitor')
};
// IP 监控配置接口
interface IPMonitorConfig {
timeout?: number; // 超时时间(毫秒)
retries?: number; // 重试次数
interval?: number; // 监控间隔(毫秒)
}
// IP 监控结果接口
export interface IPMonitorResult {
success: boolean; // 是否成功
responseTime?: number; // 响应时间(毫秒)
packetLoss?: number; // 丢包率(百分比)
error?: string; // 错误信息
}
// 默认配置
const DEFAULT_CONFIG: IPMonitorConfig = {
timeout: 1000,
retries: 3,
interval: 60000
};
// IP 监控类
export class IPMonitor {
private config: IPMonitorConfig;
private monitoring: Map<string, NodeJS.Timeout> = new Map();
constructor(config: IPMonitorConfig = {}) {
this.config = { ...DEFAULT_CONFIG, ...config };
}
// 开始监控指定 IP
async startMonitor(ip: string, callback: (result: IPMonitorResult) => void) {
if (this.monitoring.has(ip)) {
log.app(`IP ${ip} 已经在监控中`);
return;
}
const monitor = async () => {
try {
const result = await this.ping(ip);
callback(result);
} catch (error) {
log.app(`监控 IP ${ip} 时发生错误:`, error);
callback({
success: false,
error: error instanceof Error ? error.message : '未知错误'
});
}
};
// 立即执行一次
await monitor();
// 设置定时监控
const interval = setInterval(monitor, this.config.interval);
this.monitoring.set(ip, interval);
log.app(`开始监控 IP: ${ip}`);
}
// 停止监控指定 IP
stopMonitor(ip: string) {
const interval = this.monitoring.get(ip);
if (interval) {
clearInterval(interval);
this.monitoring.delete(ip);
log.app(`停止监控 IP: ${ip}`);
}
}
// 停止所有监控
stopAll() {
for (const [ip, interval] of this.monitoring) {
clearInterval(interval);
this.monitoring.delete(ip);
}
log.app('已停止所有 IP 监控');
}
// 执行 ping 操作
private async ping(ip: string): Promise<IPMonitorResult> {
const results: number[] = [];
let successCount = 0;
let totalTime = 0;
for (let i = 0; i < this.config.retries!; i++) {
try {
const startTime = Date.now();
const result = await this.tcpPing(ip);
const endTime = Date.now();
if (result) {
const responseTime = endTime - startTime;
results.push(responseTime);
totalTime += responseTime;
successCount++;
}
} catch (error) {
log.app(`Ping 尝试 ${i + 1} 失败:`, error);
}
}
// 计算平均响应时间和丢包率
const avgResponseTime = successCount > 0 ? totalTime / successCount : undefined;
const packetLoss = ((this.config.retries! - successCount) / this.config.retries!) * 100;
return {
success: successCount > 0,
responseTime: avgResponseTime,
packetLoss: packetLoss
};
}
// TCP ping 实现
private async tcpPing(ip: string): Promise<boolean> {
return new Promise((resolve, reject) => {
const socket = new net.Socket();
let timeout = false;
// 设置超时
const timer = setTimeout(() => {
timeout = true;
socket.destroy();
reject(new Error('连接超时'));
}, this.config.timeout);
socket.on('connect', () => {
clearTimeout(timer);
socket.destroy();
resolve(true);
});
socket.on('error', (error) => {
clearTimeout(timer);
if (!timeout) {
reject(error);
}
});
// 尝试连接
socket.connect(80, ip);
});
}
}
// 导出单例实例
export const ipMonitor = new IPMonitor();

View File

@@ -5,5 +5,4 @@ export const logger = {
api: debug('backend:api'),
db: debug('backend:db'),
middleware: debug('backend:middleware'),
k8s: debug('backend:k8s')
};

View File

@@ -1,149 +0,0 @@
import { Socket } from "node:net";
import { Buffer } from "node:buffer";
interface ModbusRTUTestOptions {
ip: string;
port: number;
frameHexStrings?: string[];
}
interface ModbusRTUTestResult {
connected: boolean;
message: string;
response?: string;
error?: string;
}
/**
* Modbus RTU设备连接
* @param options 连接参数 {ip, port, frameHexStrings}
* @returns 连接结果
*/
export async function modbusRTUConnection(
options: ModbusRTUTestOptions
): Promise<ModbusRTUTestResult> {
try {
const { ip, port, frameHexStrings } = options;
if (!ip || !port) {
throw new Error("缺少IP或端口参数");
}
const socket = new Socket();
const timeout = 2000;
// 默认测试帧: 设备地址2, 功能码4(读取输入寄存器), 起始地址0, 读取2个寄存器
const defaultTestFrame = [0x02, 0x04, 0x00, 0x00, 0x00, 0x02, 0x71, 0xF8];
let testFrame;
if (frameHexStrings) {
// 16进制字符串数组格式处理
if (!Array.isArray(frameHexStrings)) {
throw new Error("测试帧格式错误: 必须是16进制字符串数组");
}
try {
testFrame = Buffer.from(frameHexStrings.map(s => parseInt(s, 16)));
} catch (e) {
throw new Error("测试帧格式错误: 16进制字符串转换失败");
}
} else {
testFrame = Buffer.from(defaultTestFrame);
}
return await new Promise((resolve) => {
socket.setTimeout(timeout);
socket.on('connect', () => {
socket.write(testFrame);
});
socket.on('data', (data) => {
const hexResponse = data.toString('hex');
socket.destroy();
resolve({
connected: true,
message: "连接成功",
response: hexResponse
});
});
socket.on('timeout', () => {
socket.destroy();
resolve({
connected: false,
message: "连接超时"
});
});
socket.on('error', (error) => {
socket.destroy();
resolve({
connected: false,
message: `连接失败: ${error.message}`
});
});
socket.connect(Number(port), ip);
});
} catch (error: unknown) {
console.error("Modbus RTU测试连接错误:", error);
const message = error instanceof Error ? error.message : "未知错误";
return {
connected: false,
message: `测试连接失败: ${message}`
};
}
}
/**
* 读取Modbus RTU寄存器数据
* @param address 设备地址
* @param register 寄存器地址
* @param length 读取长度
* @returns 寄存器数据数组
*/
export async function readModbusRTU(
address: string,
register: number,
length: number
): Promise<number[] | null> {
try {
const [ip, portStr] = address.split(':');
const port = parseInt(portStr);
if (!ip || !port) {
throw new Error("设备地址格式错误应为IP:PORT");
}
// 构建读取输入寄存器命令帧
const frame = [
0x02, // 设备地址
0x04, // 功能码(读取输入寄存器)
(register >> 8) & 0xFF, // 寄存器地址高字节
register & 0xFF, // 寄存器地址低字节
(length >> 8) & 0xFF, // 长度高字节
length & 0xFF, // 长度低字节
0x71, 0xF8 // CRC校验(示例值)
];
const result = await modbusRTUConnection({
ip,
port,
frameHexStrings: frame.map(b => b.toString(16))
});
if (result.connected && result.response) {
// 解析返回数据(示例)
const bytes = Buffer.from(result.response, 'hex');
const data = [];
for (let i = 3; i < bytes.length - 2; i++) {
data.push(bytes[i]);
}
return data;
}
return null;
} catch (error) {
console.error("读取Modbus寄存器失败:", error);
return null;
}
}

View File

@@ -1,108 +0,0 @@
import debug from 'debug';
import { MetricType } from '@/share/monitorTypes';
const log = debug('app:monitor-utils');
/**
* 解析温湿度传感器十六进制数据
* @param hexData 十六进制数据字符串,格式如"[02 04 04 01 02 02 65 A9 F3]"
* @returns { temperature: number, humidity: number } 解析后的温湿度对象
* @throws 如果数据格式无效会抛出错误
*/
export function parseTemperatureHumidity(hexData: string): {
temperature: { value: number; unit: string };
humidity: { value: number; unit: string };
error?: string;
} {
try {
// 验证数据格式
if (!/^\[\s*(?:[0-9A-Fa-f]{2}\s*)+\]$/.test(hexData)) {
return {
temperature: { value: 0, unit: '°C' },
humidity: { value: 0, unit: '%' },
error: '数据格式错误: 必须以方括号开头和结尾'
};
}
// 提取十六进制字节数组
const bytes = hexData
.replace(/[\[\]\s]/g, '')
.match(/.{1,2}/g)
?.map(byte => parseInt(byte, 16)) || [];
if (bytes.length < 7) {
return {
temperature: { value: 0, unit: '°C' },
humidity: { value: 0, unit: '%' },
error: '数据长度不足: 至少需要7个十六进制字节'
};
}
// 解析温度值 (01位)
const tempInt = bytes[3]; // 整数部分
const tempFrac = bytes[4]; // 小数部分
const temperature = parseFloat(`${tempInt}.${tempFrac}`);
// 解析湿度值 (02位)
const humidityInt = bytes[5]; // 整数部分
const humidityFrac = bytes[6]; // 小数部分
const humidity = parseFloat(`${humidityInt}.${humidityFrac}`);
return {
temperature: { value: temperature, unit: '°C' },
humidity: { value: humidity, unit: '%' }
};
} catch (error) {
return {
temperature: { value: 0, unit: '°C' },
humidity: { value: 0, unit: '%' },
error: '数据解析失败: ' + (error instanceof Error ? error.message : String(error))
};
}
}
/**
* 从MODBUS设备读取传感器数据
*/
export async function readModbusSensorData(
address: string,
metricType: string
): Promise<{ value: number; unit: string } | null> {
try {
// 根据传感器类型设置不同的寄存器地址
let registerAddress = 0;
switch (metricType) {
case MetricType.TEMPERATURE:
registerAddress = 0x1000; // 温度寄存器地址
break;
case MetricType.HUMIDITY:
registerAddress = 0x1002; // 湿度寄存器地址
break;
case 'smoke':
registerAddress = 0x1004; // 烟感寄存器地址
break;
case 'water':
registerAddress = 0x1006; // 水浸寄存器地址
break;
}
const { readModbusRTU } = await import('@/server/utils/modbus_rtu');
const result = await readModbusRTU(address, registerAddress, 2);
// 解析返回的数据
if (result && result.length >= 2) {
const value = (result[0] << 8) | result[1]; // 组合高低字节
return {
value: metricType === 'smoke' || metricType === 'water'
? value > 0 ? 1 : 0 // 烟感/水浸为开关量
: value / 10, // 温湿度为模拟量除以10得到实际值
unit: metricType === MetricType.TEMPERATURE ? '°C' :
metricType === MetricType.HUMIDITY ? '%' : ''
};
}
return null;
} catch (error) {
log(`读取MODBUS传感器数据失败: ${error}`);
return null;
}
}

View File

@@ -1,204 +0,0 @@
import type { Context } from 'hono'
import type {
DeviceStatus,
SmsItem,
SmsConfig,
SmsApiRequest,
SmsApiResponse,
SmsError,
SmsMetrics
} from '../types/smsTypes.js'
import { getSystemSettings } from './systemSettings.js'
import { createHash } from 'node:crypto'
import { fetchWithRetry } from './http.js'
import { logger } from './logger.js'
// 加密密钥(应从环境变量获取)
const ENCRYPT_KEY = process.env.SMS_ENCRYPT_KEY || 'default-encrypt-key'
// 性能指标
const smsMetrics: SmsMetrics = {
requestCount: 0,
successCount: 0,
failureCount: 0,
averageLatency: 0
}
// 加密函数
function encryptPassword(password: string): string {
return createHash('sha256')
.update(password + ENCRYPT_KEY)
.digest('hex')
}
// 生成Basic认证头
function generateAuthHeader(username: string, password: string): string {
const text = `${username}:${password}`
const bytes = new TextEncoder().encode(text)
const token = btoa(String.fromCharCode(...bytes))
return `Basic ${token}`
}
// 获取短信配置
async function getSmsConfig(): Promise<SmsConfig> {
const settings = await getSystemSettings()
return {
apiUrl: settings.apiUrl,
username: settings.username,
encryptedPassword: settings.encryptedPassword,
timeout: settings.timeout ?? 5000,
maxRetries: settings.maxRetries ?? 3
}
}
// 模拟数据存储
const mockDeviceStatus: DeviceStatus = {
signalStrength: 85,
carrier: '中国移动',
mode: '短信'
}
const mockSmsList: SmsItem[] = []
export const SmsController = {
async login(ctx: Context) {
const { username, password } = await ctx.req.json<SmsApiRequest>()
if (username === 'vsmsd' && password === 'Vsmsd123') {
return ctx.json({
success: true,
token: 'dummy-token-for-demo'
})
}
return ctx.json({ success: false }, 401)
},
async getDeviceStatus(ctx: Context) {
return ctx.json({
data: {
status: mockDeviceStatus,
list: mockSmsList
}
})
},
async sendSms(ctx: Context) {
const { phone, content } = await ctx.req.json<SmsApiRequest>()
if (!phone || !content) {
return ctx.json({
success: false,
message: '手机号和短信内容不能为空'
}, 400)
}
const taskId = `task-${Date.now()}`
const newSms: SmsItem = {
id: Date.now().toString(),
phone,
content,
taskId,
status: 'pending',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
try {
const config = await getSmsConfig()
const startTime = Date.now()
const response = await fetchWithRetry(config.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': generateAuthHeader(config.username, config.encryptedPassword)
},
body: JSON.stringify({ phone, content }),
timeout: config.timeout ?? 5000,
maxRetries: config.maxRetries ?? 3
})
const data: SmsApiResponse = await response.json()
if (data.success) {
newSms.status = 'success'
smsMetrics.successCount++
logger.info(`短信发送成功: ${taskId}`, { phone, taskId })
} else {
newSms.status = 'failed'
smsMetrics.failureCount++
logger.error(`短信发送失败: ${data.code} - ${data.message}`, {
phone,
taskId,
error: data
})
}
const latency = Date.now() - startTime
smsMetrics.requestCount++
smsMetrics.averageLatency =
(smsMetrics.averageLatency * (smsMetrics.requestCount - 1) + latency) /
smsMetrics.requestCount
smsMetrics.lastRequestTime = new Date().toISOString()
} catch (error) {
// 真实接口失败时使用模拟发送作为fallback
newSms.status = 'success' // 模拟成功
if (error instanceof Error) {
logger.warn(`使用模拟短信发送: ${error.message}`, {
phone,
taskId,
error: error.stack
})
} else {
logger.warn(`使用模拟短信发送: ${String(error)}`, {
phone,
taskId
})
}
}
mockSmsList.unshift(newSms)
return ctx.json({
success: true,
data: newSms
})
},
async getSmsResult(ctx: Context) {
const id = ctx.req.param('id')
const sms = mockSmsList.find(item => item.id === id)
if (!sms) {
return ctx.json({
success: false,
message: '未找到该短信记录'
}, 404)
}
// 模拟短信发送结果详情
return ctx.json({
success: true,
data: {
...sms,
results: [
{
serial: '001',
carrier: '中国移动',
time: new Date().toISOString(),
status: sms.status === 'pending' ? '处理中' :
sms.status === 'success' ? '成功' : '失败'
}
]
}
})
},
async getMetrics(ctx: Context) {
return ctx.json({
success: true,
data: smsMetrics
})
}
}