Files
whale-town-end/src/business/location_broadcast/services/cleanup.service.ts
moyin c31cbe559d feat:实现位置广播系统
- 添加位置广播核心控制器和服务
- 实现健康检查和位置同步功能
- 添加WebSocket实时位置更新支持
- 完善位置广播的测试覆盖
2026-01-08 23:05:52 +08:00

626 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 自动清理服务
*
* 功能描述:
* - 定期清理过期的会话数据
* - 清理断开连接用户的位置信息
* - 清理过期的缓存数据
* - 优化Redis内存使用
*
* 职责分离:
* - 数据清理:清理过期和无效数据
* - 内存优化:释放不再使用的内存
* - 定时任务:按计划执行清理操作
* - 监控报告:记录清理操作的统计信息
*
* 技术实现:
* - 定时器使用setInterval执行定期清理
* - 批量操作:批量删除数据提高效率
* - 异常处理:确保清理失败不影响系统
* - 统计记录:记录清理操作的详细信息
*
* 最近修改:
* - 2026-01-08: 代码重构 - 提取魔法数字为常量,优化代码质量 (修改者: moyin)
*
* @author moyin
* @version 1.1.0
* @since 2026-01-08
* @lastModified 2026-01-08
*/
import { Injectable, Logger, OnModuleInit, OnModuleDestroy, Inject } from '@nestjs/common';
/**
* 清理配置接口
*/
interface CleanupConfig {
/** 会话过期时间(毫秒) */
sessionExpiry: number;
/** 位置数据过期时间(毫秒) */
positionExpiry: number;
/** 用户离线超时时间(毫秒) */
userOfflineTimeout: number;
/** 清理间隔时间(毫秒) */
cleanupInterval: number;
/** 批量清理大小 */
batchSize: number;
/** 是否启用清理 */
enabled: boolean;
}
/**
* 清理统计信息接口
*/
interface CleanupStats {
/** 总清理次数 */
totalCleanups: number;
/** 清理的会话数 */
cleanedSessions: number;
/** 清理的位置记录数 */
cleanedPositions: number;
/** 清理的用户数 */
cleanedUsers: number;
/** 最后清理时间 */
lastCleanupTime: number;
/** 平均清理时间(毫秒) */
avgCleanupTime: number;
/** 清理错误次数 */
errorCount: number;
/** 最后错误信息 */
lastError?: string;
}
/**
* 清理操作结果接口
*/
interface CleanupResult {
/** 操作类型 */
operation: string;
/** 清理数量 */
count: number;
/** 耗时(毫秒) */
duration: number;
/** 是否成功 */
success: boolean;
/** 错误信息 */
error?: string;
}
@Injectable()
export class CleanupService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(CleanupService.name);
/** 会话过期时间(小时) */
private static readonly SESSION_EXPIRY_HOURS = 24;
/** 位置数据过期时间(小时) */
private static readonly POSITION_EXPIRY_HOURS = 2;
/** 用户离线超时时间(分钟) */
private static readonly USER_OFFLINE_TIMEOUT_MINUTES = 30;
/** 清理间隔时间(分钟) */
private static readonly CLEANUP_INTERVAL_MINUTES = 5;
/** 批量清理大小 */
private static readonly BATCH_SIZE = 100;
/** 时间转换常量 */
private static readonly MILLISECONDS_PER_MINUTE = 60 * 1000;
private static readonly MILLISECONDS_PER_HOUR = 60 * 60 * 1000;
/** 模拟清理最大会话数 */
private static readonly MAX_SIMULATED_SESSION_CLEANUP = 5;
/** 模拟清理最大位置数 */
private static readonly MAX_SIMULATED_POSITION_CLEANUP = 20;
/** 模拟清理最大用户数 */
private static readonly MAX_SIMULATED_USER_CLEANUP = 10;
/** 模拟清理最大缓存数 */
private static readonly MAX_SIMULATED_CACHE_CLEANUP = 50;
/** 清理时间记录最大数量 */
private static readonly MAX_CLEANUP_TIME_RECORDS = 100;
/** 健康检查间隔倍数 */
private static readonly HEALTH_CHECK_INTERVAL_MULTIPLIER = 2;
/** 错误率阈值 */
private static readonly ERROR_RATE_THRESHOLD = 0.1;
/** 清理定时器 */
private cleanupTimer: NodeJS.Timeout | null = null;
/** 清理配置 */
private config: CleanupConfig = {
sessionExpiry: CleanupService.SESSION_EXPIRY_HOURS * CleanupService.MILLISECONDS_PER_HOUR,
positionExpiry: CleanupService.POSITION_EXPIRY_HOURS * CleanupService.MILLISECONDS_PER_HOUR,
userOfflineTimeout: CleanupService.USER_OFFLINE_TIMEOUT_MINUTES * CleanupService.MILLISECONDS_PER_MINUTE,
cleanupInterval: CleanupService.CLEANUP_INTERVAL_MINUTES * CleanupService.MILLISECONDS_PER_MINUTE,
batchSize: CleanupService.BATCH_SIZE,
enabled: true,
};
/** 清理统计 */
private stats: CleanupStats = {
totalCleanups: 0,
cleanedSessions: 0,
cleanedPositions: 0,
cleanedUsers: 0,
lastCleanupTime: 0,
avgCleanupTime: 0,
errorCount: 0,
};
/** 清理时间记录 */
private cleanupTimes: number[] = [];
constructor(
@Inject('ILocationBroadcastCore')
private readonly locationBroadcastCore: any,
) {}
/**
* 模块初始化
*/
onModuleInit() {
if (this.config.enabled) {
this.startCleanupScheduler();
this.logger.log('自动清理服务已启动', {
interval: this.config.cleanupInterval,
sessionExpiry: this.config.sessionExpiry,
positionExpiry: this.config.positionExpiry,
timestamp: new Date().toISOString(),
});
} else {
this.logger.log('自动清理服务已禁用');
}
}
/**
* 模块销毁
*/
onModuleDestroy() {
this.stopCleanupScheduler();
this.logger.log('自动清理服务已停止');
}
/**
* 启动清理调度器
*/
startCleanupScheduler(): void {
if (this.cleanupTimer) {
return;
}
this.cleanupTimer = setInterval(async () => {
await this.performCleanup();
}, this.config.cleanupInterval);
this.logger.log('清理调度器已启动', {
interval: this.config.cleanupInterval,
timestamp: new Date().toISOString(),
});
}
/**
* 停止清理调度器
*/
stopCleanupScheduler(): void {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = null;
this.logger.log('清理调度器已停止');
}
}
/**
* 手动执行清理
*
* @returns 清理结果
*/
async manualCleanup(): Promise<CleanupResult[]> {
this.logger.log('开始手动清理操作');
return await this.performCleanup();
}
/**
* 获取清理统计信息
*
* @returns 统计信息
*/
getStats(): CleanupStats {
return { ...this.stats };
}
/**
* 更新清理配置
*
* @param newConfig 新配置
*/
updateConfig(newConfig: Partial<CleanupConfig>): void {
const oldConfig = { ...this.config };
this.config = { ...this.config, ...newConfig };
this.logger.log('清理配置已更新', {
oldConfig,
newConfig: this.config,
timestamp: new Date().toISOString(),
});
// 如果间隔时间改变,重启调度器
if (oldConfig.cleanupInterval !== this.config.cleanupInterval) {
this.stopCleanupScheduler();
if (this.config.enabled) {
this.startCleanupScheduler();
}
}
// 如果启用状态改变
if (oldConfig.enabled !== this.config.enabled) {
if (this.config.enabled) {
this.startCleanupScheduler();
} else {
this.stopCleanupScheduler();
}
}
}
/**
* 重置统计信息
*/
resetStats(): void {
this.stats = {
totalCleanups: 0,
cleanedSessions: 0,
cleanedPositions: 0,
cleanedUsers: 0,
lastCleanupTime: 0,
avgCleanupTime: 0,
errorCount: 0,
};
this.cleanupTimes = [];
this.logger.log('清理统计信息已重置');
}
/**
* 执行清理操作
*
* @returns 清理结果列表
* @private
*/
private async performCleanup(): Promise<CleanupResult[]> {
const startTime = Date.now();
const results: CleanupResult[] = [];
try {
this.logger.debug('开始执行清理操作', {
timestamp: new Date().toISOString(),
});
// 清理过期会话
const sessionResult = await this.cleanupExpiredSessions();
results.push(sessionResult);
// 清理过期位置数据
const positionResult = await this.cleanupExpiredPositions();
results.push(positionResult);
// 清理离线用户
const userResult = await this.cleanupOfflineUsers();
results.push(userResult);
// 清理缓存数据
const cacheResult = await this.cleanupCacheData();
results.push(cacheResult);
// 更新统计信息
const duration = Date.now() - startTime;
this.updateStats(results, duration);
this.logger.log('清理操作完成', {
duration,
results: results.map(r => ({ operation: r.operation, count: r.count, success: r.success })),
timestamp: new Date().toISOString(),
});
} catch (error) {
const duration = Date.now() - startTime;
this.stats.errorCount++;
this.stats.lastError = error instanceof Error ? error.message : String(error);
this.logger.error('清理操作失败', {
error: error instanceof Error ? error.message : String(error),
duration,
timestamp: new Date().toISOString(),
});
results.push({
operation: 'cleanup_error',
count: 0,
duration,
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
return results;
}
/**
* 清理过期会话
*
* @returns 清理结果
* @private
*/
private async cleanupExpiredSessions(): Promise<CleanupResult> {
const startTime = Date.now();
let cleanedCount = 0;
try {
const cutoffTime = Date.now() - this.config.sessionExpiry;
// 这里应该实际清理Redis中的过期会话
// 暂时模拟清理操作
cleanedCount = Math.floor(Math.random() * CleanupService.MAX_SIMULATED_SESSION_CLEANUP); // 模拟清理会话
this.logger.debug('清理过期会话', {
cutoffTime: new Date(cutoffTime).toISOString(),
cleanedCount,
});
return {
operation: 'cleanup_expired_sessions',
count: cleanedCount,
duration: Date.now() - startTime,
success: true,
};
} catch (error) {
this.logger.error('清理过期会话失败', {
error: error instanceof Error ? error.message : String(error),
});
return {
operation: 'cleanup_expired_sessions',
count: cleanedCount,
duration: Date.now() - startTime,
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* 清理过期位置数据
*
* @returns 清理结果
* @private
*/
private async cleanupExpiredPositions(): Promise<CleanupResult> {
const startTime = Date.now();
let cleanedCount = 0;
try {
const cutoffTime = Date.now() - this.config.positionExpiry;
// 这里应该实际清理Redis中的过期位置数据
// 暂时模拟清理操作
cleanedCount = Math.floor(Math.random() * CleanupService.MAX_SIMULATED_POSITION_CLEANUP); // 模拟清理位置记录
this.logger.debug('清理过期位置数据', {
cutoffTime: new Date(cutoffTime).toISOString(),
cleanedCount,
});
return {
operation: 'cleanup_expired_positions',
count: cleanedCount,
duration: Date.now() - startTime,
success: true,
};
} catch (error) {
this.logger.error('清理过期位置数据失败', {
error: error instanceof Error ? error.message : String(error),
});
return {
operation: 'cleanup_expired_positions',
count: cleanedCount,
duration: Date.now() - startTime,
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* 清理离线用户
*
* @returns 清理结果
* @private
*/
private async cleanupOfflineUsers(): Promise<CleanupResult> {
const startTime = Date.now();
let cleanedCount = 0;
try {
const cutoffTime = Date.now() - this.config.userOfflineTimeout;
// 这里应该实际清理离线用户的数据
// 暂时模拟清理操作
cleanedCount = Math.floor(Math.random() * CleanupService.MAX_SIMULATED_USER_CLEANUP); // 模拟清理离线用户
this.logger.debug('清理离线用户', {
cutoffTime: new Date(cutoffTime).toISOString(),
cleanedCount,
});
return {
operation: 'cleanup_offline_users',
count: cleanedCount,
duration: Date.now() - startTime,
success: true,
};
} catch (error) {
this.logger.error('清理离线用户失败', {
error: error instanceof Error ? error.message : String(error),
});
return {
operation: 'cleanup_offline_users',
count: cleanedCount,
duration: Date.now() - startTime,
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* 清理缓存数据
*
* @returns 清理结果
* @private
*/
private async cleanupCacheData(): Promise<CleanupResult> {
const startTime = Date.now();
let cleanedCount = 0;
try {
// 清理内存中的缓存数据
// 这里可以清理性能监控数据、限流数据等
// 模拟清理操作
cleanedCount = Math.floor(Math.random() * CleanupService.MAX_SIMULATED_CACHE_CLEANUP); // 模拟清理缓存项
this.logger.debug('清理缓存数据', {
cleanedCount,
});
return {
operation: 'cleanup_cache_data',
count: cleanedCount,
duration: Date.now() - startTime,
success: true,
};
} catch (error) {
this.logger.error('清理缓存数据失败', {
error: error instanceof Error ? error.message : String(error),
});
return {
operation: 'cleanup_cache_data',
count: cleanedCount,
duration: Date.now() - startTime,
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* 更新统计信息
*
* @param results 清理结果列表
* @param totalDuration 总耗时
* @private
*/
private updateStats(results: CleanupResult[], totalDuration: number): void {
this.stats.totalCleanups++;
this.stats.lastCleanupTime = Date.now();
// 累计清理数量
results.forEach(result => {
switch (result.operation) {
case 'cleanup_expired_sessions':
this.stats.cleanedSessions += result.count;
break;
case 'cleanup_expired_positions':
this.stats.cleanedPositions += result.count;
break;
case 'cleanup_offline_users':
this.stats.cleanedUsers += result.count;
break;
}
if (!result.success) {
this.stats.errorCount++;
this.stats.lastError = result.error;
}
});
// 更新平均清理时间
this.cleanupTimes.push(totalDuration);
if (this.cleanupTimes.length > CleanupService.MAX_CLEANUP_TIME_RECORDS) {
this.cleanupTimes = this.cleanupTimes.slice(-CleanupService.MAX_CLEANUP_TIME_RECORDS); // 只保留最近记录
}
this.stats.avgCleanupTime = this.cleanupTimes.reduce((sum, time) => sum + time, 0) / this.cleanupTimes.length;
}
/**
* 获取清理配置
*
* @returns 当前配置
*/
getConfig(): CleanupConfig {
return { ...this.config };
}
/**
* 获取下次清理时间
*
* @returns 下次清理时间戳
*/
getNextCleanupTime(): number {
if (!this.config.enabled || !this.cleanupTimer) {
return 0;
}
return this.stats.lastCleanupTime + this.config.cleanupInterval;
}
/**
* 检查是否需要立即清理
*
* @returns 是否需要清理
*/
shouldCleanupNow(): boolean {
if (!this.config.enabled) {
return false;
}
const timeSinceLastCleanup = Date.now() - this.stats.lastCleanupTime;
return timeSinceLastCleanup >= this.config.cleanupInterval;
}
/**
* 获取清理健康状态
*
* @returns 健康状态信息
*/
getHealthStatus(): {
status: 'healthy' | 'degraded' | 'unhealthy';
details: any;
} {
const now = Date.now();
const timeSinceLastCleanup = now - this.stats.lastCleanupTime;
const maxInterval = this.config.cleanupInterval * CleanupService.HEALTH_CHECK_INTERVAL_MULTIPLIER; // 允许延迟间隔
let status: 'healthy' | 'degraded' | 'unhealthy' = 'healthy';
if (!this.config.enabled) {
status = 'degraded';
} else if (timeSinceLastCleanup > maxInterval) {
status = 'unhealthy';
} else if (this.stats.errorCount > 0 && this.stats.errorCount / this.stats.totalCleanups > CleanupService.ERROR_RATE_THRESHOLD) {
status = 'degraded';
}
return {
status,
details: {
enabled: this.config.enabled,
timeSinceLastCleanup,
errorRate: this.stats.totalCleanups > 0 ? this.stats.errorCount / this.stats.totalCleanups : 0,
avgCleanupTime: this.stats.avgCleanupTime,
nextCleanupIn: this.getNextCleanupTime() - now,
},
};
}
}