import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, Between, MoreThanOrEqual, LessThanOrEqual } from 'typeorm'; import { Cron, CronExpression } from '@nestjs/schedule'; import { AnalyticsRecord } from '@database/entities/analytics-record.entity'; import { AnalyticsSummary } from '@database/entities/analytics-summary.entity'; import { AnalyticsQueryDto, CreateAnalyticsRecordDto } from '../dto/analytics-query.dto'; @Injectable() export class AnalyticsService { private readonly logger = new Logger(AnalyticsService.name); constructor( @InjectRepository(AnalyticsRecord) private readonly analyticsRecordRepository: Repository, @InjectRepository(AnalyticsSummary) private readonly analyticsSummaryRepository: Repository, ) {} /** * 记录分析事件 */ async recordEvent(createDto: CreateAnalyticsRecordDto, request?: any): Promise { const now = new Date(); const record = this.analyticsRecordRepository.create({ ...createDto, timestamp: now, date: new Date(now.getFullYear(), now.getMonth(), now.getDate()), ipAddress: request?.ip, userAgent: request?.headers?.['user-agent'], sessionId: request?.headers?.['x-session-id'], requestId: request?.headers?.['x-request-id'], }); const savedRecord = await this.analyticsRecordRepository.save(record); this.logger.debug(`记录分析事件: ${createDto.eventType} - ${createDto.eventName}`); return savedRecord; } /** * 批量记录事件 */ async recordEvents(events: CreateAnalyticsRecordDto[], request?: any): Promise { const now = new Date(); const date = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const records = events.map(event => this.analyticsRecordRepository.create({ ...event, timestamp: now, date, ipAddress: request?.ip, userAgent: request?.headers?.['user-agent'], sessionId: request?.headers?.['x-session-id'], requestId: request?.headers?.['x-request-id'], }) ); const savedRecords = await this.analyticsRecordRepository.save(records); this.logger.debug(`批量记录分析事件: ${events.length} 个`); return savedRecords; } /** * 查询分析数据 */ async queryAnalytics(queryDto: AnalyticsQueryDto): Promise { const { metricType, entityType, entityId, startDate, endDate, period = 'day', groupBy, aggregation = 'count', limit = 1000, } = queryDto; // 构建查询条件 const whereConditions: any = { eventName: metricType, timestamp: Between(new Date(startDate), new Date(endDate)), }; if (entityType) { whereConditions.entityType = entityType; } if (entityId) { whereConditions.entityId = entityId; } // 如果有预计算的汇总数据,优先使用 if (!groupBy && period && ['day', 'week', 'month'].includes(period)) { const summaryData = await this.querySummaryData(queryDto); if (summaryData.length > 0) { return this.formatSummaryData(summaryData, period); } } // 实时查询原始数据 return await this.queryRawData(queryDto); } /** * 查询汇总数据 */ private async querySummaryData(queryDto: AnalyticsQueryDto): Promise { const { metricType, entityType, entityId, startDate, endDate, period, } = queryDto; const whereConditions: any = { metricType, period, date: Between(new Date(startDate), new Date(endDate)), }; if (entityType) { whereConditions.entityType = entityType; } if (entityId) { whereConditions.entityId = entityId; } return await this.analyticsSummaryRepository.find({ where: whereConditions, order: { date: 'ASC' }, }); } /** * 查询原始数据 */ private async queryRawData(queryDto: AnalyticsQueryDto): Promise { const { metricType, entityType, entityId, startDate, endDate, groupBy, aggregation, limit, } = queryDto; const queryBuilder = this.analyticsRecordRepository.createQueryBuilder('record'); // 基础条件 queryBuilder .where('record.eventName = :metricType', { metricType }) .andWhere('record.timestamp BETWEEN :startDate AND :endDate', { startDate: new Date(startDate), endDate: new Date(endDate), }); if (entityType) { queryBuilder.andWhere('record.entityType = :entityType', { entityType }); } if (entityId) { queryBuilder.andWhere('record.entityId = :entityId', { entityId }); } // 分组和聚合 if (groupBy && groupBy.length > 0) { const groupFields = groupBy.map(field => `record.${field}`); queryBuilder.groupBy(groupFields.join(', ')); groupFields.forEach(field => { queryBuilder.addSelect(field); }); // 聚合函数 switch (aggregation) { case 'count': queryBuilder.addSelect('COUNT(*)', 'value'); break; case 'sum': queryBuilder.addSelect('SUM(record.value)', 'value'); break; case 'avg': queryBuilder.addSelect('AVG(record.value)', 'value'); break; case 'min': queryBuilder.addSelect('MIN(record.value)', 'value'); break; case 'max': queryBuilder.addSelect('MAX(record.value)', 'value'); break; } } else { // 时间序列聚合 queryBuilder .select('DATE(record.timestamp)', 'date') .addSelect('COUNT(*)', 'count') .addSelect('AVG(record.value)', 'avgValue') .addSelect('SUM(record.value)', 'sumValue') .groupBy('DATE(record.timestamp)') .orderBy('date', 'ASC'); } queryBuilder.limit(limit); return await queryBuilder.getRawMany(); } /** * 格式化汇总数据 */ private formatSummaryData(summaries: AnalyticsSummary[], period: string): any { return summaries.map(summary => ({ date: summary.date, period, totalCount: summary.totalCount, successCount: summary.successCount, failureCount: summary.failureCount, totalValue: summary.totalValue, averageValue: summary.averageValue, minValue: summary.minValue, maxValue: summary.maxValue, metrics: summary.metrics, dimensions: summary.dimensions, })); } /** * 获取实时指标 */ async getRealtimeMetrics(): Promise { const now = new Date(); const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); const [ lastHourEvents, lastDayEvents, topEvents, errorEvents, ] = await Promise.all([ this.analyticsRecordRepository.count({ where: { timestamp: MoreThanOrEqual(oneHourAgo) }, }), this.analyticsRecordRepository.count({ where: { timestamp: MoreThanOrEqual(oneDayAgo) }, }), this.getTopEvents(10), this.analyticsRecordRepository.count({ where: { eventType: 'error_event', timestamp: MoreThanOrEqual(oneDayAgo), }, }), ]); return { lastHourEvents, lastDayEvents, topEvents, errorEvents, timestamp: now, }; } /** * 获取热门事件 */ async getTopEvents(limit: number = 10): Promise { const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); return await this.analyticsRecordRepository .createQueryBuilder('record') .select('record.eventName', 'eventName') .addSelect('record.eventType', 'eventType') .addSelect('COUNT(*)', 'count') .where('record.timestamp >= :oneDayAgo', { oneDayAgo }) .groupBy('record.eventName, record.eventType') .orderBy('count', 'DESC') .limit(limit) .getRawMany(); } /** * 获取用户活动分析 */ async getUserActivityAnalytics( startDate: string, endDate: string, userId?: number ): Promise { const queryBuilder = this.analyticsRecordRepository.createQueryBuilder('record'); queryBuilder .select('DATE(record.timestamp)', 'date') .addSelect('COUNT(DISTINCT record.userId)', 'activeUsers') .addSelect('COUNT(*)', 'totalEvents') .addSelect('record.eventType', 'eventType') .where('record.timestamp BETWEEN :startDate AND :endDate', { startDate: new Date(startDate), endDate: new Date(endDate), }) .andWhere('record.eventType = :eventType', { eventType: 'user_action' }); if (userId) { queryBuilder.andWhere('record.userId = :userId', { userId }); } queryBuilder .groupBy('DATE(record.timestamp), record.eventType') .orderBy('date', 'ASC'); return await queryBuilder.getRawMany(); } /** * 获取性能指标分析 */ async getPerformanceAnalytics( startDate: string, endDate: string, metricName?: string ): Promise { const queryBuilder = this.analyticsRecordRepository.createQueryBuilder('record'); queryBuilder .select('DATE(record.timestamp)', 'date') .addSelect('record.eventName', 'metricName') .addSelect('AVG(record.value)', 'averageValue') .addSelect('MIN(record.value)', 'minValue') .addSelect('MAX(record.value)', 'maxValue') .addSelect('COUNT(*)', 'count') .where('record.eventType = :eventType', { eventType: 'performance_metric' }) .andWhere('record.timestamp BETWEEN :startDate AND :endDate', { startDate: new Date(startDate), endDate: new Date(endDate), }) .andWhere('record.value IS NOT NULL'); if (metricName) { queryBuilder.andWhere('record.eventName = :metricName', { metricName }); } queryBuilder .groupBy('DATE(record.timestamp), record.eventName') .orderBy('date', 'ASC'); return await queryBuilder.getRawMany(); } /** * 获取错误分析 */ async getErrorAnalytics( startDate: string, endDate: string ): Promise { const [ errorTrends, errorTypes, topErrors, ] = await Promise.all([ this.getErrorTrends(startDate, endDate), this.getErrorTypes(startDate, endDate), this.getTopErrors(startDate, endDate), ]); return { trends: errorTrends, types: errorTypes, topErrors, }; } /** * 获取错误趋势 */ private async getErrorTrends(startDate: string, endDate: string): Promise { return await this.analyticsRecordRepository .createQueryBuilder('record') .select('DATE(record.timestamp)', 'date') .addSelect('COUNT(*)', 'errorCount') .where('record.eventType = :eventType', { eventType: 'error_event' }) .andWhere('record.timestamp BETWEEN :startDate AND :endDate', { startDate: new Date(startDate), endDate: new Date(endDate), }) .groupBy('DATE(record.timestamp)') .orderBy('date', 'ASC') .getRawMany(); } /** * 获取错误类型分布 */ private async getErrorTypes(startDate: string, endDate: string): Promise { return await this.analyticsRecordRepository .createQueryBuilder('record') .select('record.eventName', 'errorType') .addSelect('COUNT(*)', 'count') .where('record.eventType = :eventType', { eventType: 'error_event' }) .andWhere('record.timestamp BETWEEN :startDate AND :endDate', { startDate: new Date(startDate), endDate: new Date(endDate), }) .groupBy('record.eventName') .orderBy('count', 'DESC') .getRawMany(); } /** * 获取热门错误 */ private async getTopErrors(startDate: string, endDate: string): Promise { return await this.analyticsRecordRepository .createQueryBuilder('record') .select('record.eventName', 'errorName') .addSelect('record.eventData', 'errorData') .addSelect('COUNT(*)', 'count') .addSelect('MAX(record.timestamp)', 'lastOccurrence') .where('record.eventType = :eventType', { eventType: 'error_event' }) .andWhere('record.timestamp BETWEEN :startDate AND :endDate', { startDate: new Date(startDate), endDate: new Date(endDate), }) .groupBy('record.eventName, record.eventData') .orderBy('count', 'DESC') .limit(20) .getRawMany(); } /** * 定时任务:汇总小时数据 */ @Cron(CronExpression.EVERY_HOUR) async summarizeHourlyData(): Promise { const now = new Date(); const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); this.logger.log('开始汇总小时数据'); try { await this.generateSummaries('hour', oneHourAgo, now); this.logger.log('小时数据汇总完成'); } catch (error) { this.logger.error(`小时数据汇总失败: ${error.message}`); } } /** * 定时任务:汇总日数据 */ @Cron(CronExpression.EVERY_DAY_AT_2AM) async summarizeDailyData(): Promise { const now = new Date(); const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); this.logger.log('开始汇总日数据'); try { await this.generateSummaries('day', yesterday, now); this.logger.log('日数据汇总完成'); } catch (error) { this.logger.error(`日数据汇总失败: ${error.message}`); } } /** * 生成汇总数据 */ private async generateSummaries( period: string, startTime: Date, endTime: Date ): Promise { // 获取需要汇总的指标类型 const metricTypes = await this.analyticsRecordRepository .createQueryBuilder('record') .select('DISTINCT record.eventName', 'eventName') .addSelect('record.eventType', 'eventType') .addSelect('record.entityType', 'entityType') .where('record.timestamp BETWEEN :startTime AND :endTime', { startTime, endTime, }) .getRawMany(); for (const metric of metricTypes) { await this.generateSummaryForMetric( period, metric.eventName, metric.eventType, metric.entityType, startTime, endTime ); } } /** * 为特定指标生成汇总 */ private async generateSummaryForMetric( period: string, eventName: string, eventType: string, entityType: string, startTime: Date, endTime: Date ): Promise { const date = new Date(startTime.getFullYear(), startTime.getMonth(), startTime.getDate()); // 计算汇总数据 const summaryData = await this.analyticsRecordRepository .createQueryBuilder('record') .select('COUNT(*)', 'totalCount') .addSelect('SUM(CASE WHEN record.eventData->>"$.success" = "true" THEN 1 ELSE 0 END)', 'successCount') .addSelect('SUM(CASE WHEN record.eventData->>"$.success" = "false" THEN 1 ELSE 0 END)', 'failureCount') .addSelect('SUM(record.value)', 'totalValue') .addSelect('AVG(record.value)', 'averageValue') .addSelect('MIN(record.value)', 'minValue') .addSelect('MAX(record.value)', 'maxValue') .where('record.eventName = :eventName', { eventName }) .andWhere('record.eventType = :eventType', { eventType }) .andWhere('record.entityType = :entityType', { entityType }) .andWhere('record.timestamp BETWEEN :startTime AND :endTime', { startTime, endTime, }) .getRawOne(); if (summaryData.totalCount > 0) { // 保存或更新汇总数据 await this.analyticsSummaryRepository.upsert({ metricType: eventName, entityType: entityType || 'global', period, date, totalCount: parseInt(summaryData.totalCount), successCount: parseInt(summaryData.successCount) || 0, failureCount: parseInt(summaryData.failureCount) || 0, totalValue: parseFloat(summaryData.totalValue) || 0, averageValue: parseFloat(summaryData.averageValue) || null, minValue: parseFloat(summaryData.minValue) || null, maxValue: parseFloat(summaryData.maxValue) || null, }, ['metricType', 'entityType', 'period', 'date']); } } /** * 清理过期数据 */ @Cron(CronExpression.EVERY_DAY_AT_3AM) async cleanupOldData(): Promise { const retentionDays = parseInt(process.env.ANALYTICS_RETENTION_DAYS) || 90; const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - retentionDays); this.logger.log(`开始清理 ${retentionDays} 天前的分析数据`); try { const result = await this.analyticsRecordRepository .createQueryBuilder() .delete() .where('timestamp < :cutoffDate', { cutoffDate }) .execute(); this.logger.log(`清理完成,删除了 ${result.affected} 条记录`); } catch (error) { this.logger.error(`数据清理失败: ${error.message}`); } } }