u
This commit is contained in:
@@ -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 || '请求失败'))
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
@@ -5,5 +5,4 @@ export const logger = {
|
|||||||
api: debug('backend:api'),
|
api: debug('backend:api'),
|
||||||
db: debug('backend:db'),
|
db: debug('backend:db'),
|
||||||
middleware: debug('backend:middleware'),
|
middleware: debug('backend:middleware'),
|
||||||
k8s: debug('backend:k8s')
|
|
||||||
};
|
};
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user