Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,583 @@
|
||||
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<AnalyticsRecord>,
|
||||
@InjectRepository(AnalyticsSummary)
|
||||
private readonly analyticsSummaryRepository: Repository<AnalyticsSummary>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 记录分析事件
|
||||
*/
|
||||
async recordEvent(createDto: CreateAnalyticsRecordDto, request?: any): Promise<AnalyticsRecord> {
|
||||
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<AnalyticsRecord[]> {
|
||||
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<any> {
|
||||
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<AnalyticsSummary[]> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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<any[]> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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<any[]> {
|
||||
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<any[]> {
|
||||
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<any[]> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
// 获取需要汇总的指标类型
|
||||
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<void> {
|
||||
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<void> {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user