Initial commit: Telegram Management System
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:
你的用户名
2025-11-04 15:37:50 +08:00
commit 237c7802e5
3674 changed files with 525172 additions and 0 deletions

View File

@@ -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}`);
}
}
}