This commit is contained in:
2025-09-12 01:15:09 +08:00
parent c563b2266b
commit e05ad9e223
103 changed files with 20034 additions and 1041 deletions

View File

@@ -0,0 +1,388 @@
package services
import (
"context"
"fmt"
"time"
"go.uber.org/zap"
"tyapi-server/internal/domains/statistics/entities"
"tyapi-server/internal/domains/statistics/repositories"
)
// StatisticsAggregateService 统计聚合服务接口
// 负责统计数据的聚合和计算
type StatisticsAggregateService interface {
// 实时统计
UpdateRealtimeMetric(ctx context.Context, metricType, metricName string, value float64) error
GetRealtimeMetrics(ctx context.Context, metricType string) (map[string]float64, error)
// 历史统计聚合
AggregateHourlyMetrics(ctx context.Context, date time.Time) error
AggregateDailyMetrics(ctx context.Context, date time.Time) error
AggregateWeeklyMetrics(ctx context.Context, date time.Time) error
AggregateMonthlyMetrics(ctx context.Context, date time.Time) error
// 统计查询
GetMetricsByType(ctx context.Context, metricType string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
GetMetricsByDimension(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
// 统计计算
CalculateGrowthRate(ctx context.Context, metricType, metricName string, currentPeriod, previousPeriod time.Time) (float64, error)
CalculateTrend(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (string, error)
}
// StatisticsAggregateServiceImpl 统计聚合服务实现
type StatisticsAggregateServiceImpl struct {
metricRepo repositories.StatisticsRepository
logger *zap.Logger
}
// NewStatisticsAggregateService 创建统计聚合服务
func NewStatisticsAggregateService(
metricRepo repositories.StatisticsRepository,
logger *zap.Logger,
) StatisticsAggregateService {
return &StatisticsAggregateServiceImpl{
metricRepo: metricRepo,
logger: logger,
}
}
// UpdateRealtimeMetric 更新实时统计指标
func (s *StatisticsAggregateServiceImpl) UpdateRealtimeMetric(ctx context.Context, metricType, metricName string, value float64) error {
if metricType == "" {
return fmt.Errorf("指标类型不能为空")
}
if metricName == "" {
return fmt.Errorf("指标名称不能为空")
}
// 创建或更新实时指标
metric, err := entities.NewStatisticsMetric(metricType, metricName, "realtime", value, time.Now())
if err != nil {
s.logger.Error("创建统计指标失败",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Error(err))
return fmt.Errorf("创建统计指标失败: %w", err)
}
// 保存到数据库
err = s.metricRepo.Save(ctx, metric)
if err != nil {
s.logger.Error("保存统计指标失败",
zap.String("metric_id", metric.ID),
zap.Error(err))
return fmt.Errorf("保存统计指标失败: %w", err)
}
s.logger.Info("实时统计指标更新成功",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Float64("value", value))
return nil
}
// GetRealtimeMetrics 获取实时统计指标
func (s *StatisticsAggregateServiceImpl) GetRealtimeMetrics(ctx context.Context, metricType string) (map[string]float64, error) {
if metricType == "" {
return nil, fmt.Errorf("指标类型不能为空")
}
// 获取今天的实时指标
today := time.Now().Truncate(24 * time.Hour)
tomorrow := today.Add(24 * time.Hour)
metrics, err := s.metricRepo.FindByTypeAndDateRange(ctx, metricType, today, tomorrow)
if err != nil {
s.logger.Error("查询实时统计指标失败",
zap.String("metric_type", metricType),
zap.Error(err))
return nil, fmt.Errorf("查询实时统计指标失败: %w", err)
}
// 转换为map格式
result := make(map[string]float64)
for _, metric := range metrics {
if metric.Dimension == "realtime" {
result[metric.MetricName] = metric.Value
}
}
return result, nil
}
// AggregateHourlyMetrics 聚合小时级统计指标
func (s *StatisticsAggregateServiceImpl) AggregateHourlyMetrics(ctx context.Context, date time.Time) error {
s.logger.Info("开始聚合小时级统计指标", zap.Time("date", date))
// 获取指定小时的所有实时指标
startTime := date.Truncate(time.Hour)
endTime := startTime.Add(time.Hour)
// 聚合不同类型的指标
metricTypes := []string{"api_calls", "users", "finance", "products", "certification"}
for _, metricType := range metricTypes {
err := s.aggregateMetricsByType(ctx, metricType, startTime, endTime, "hourly")
if err != nil {
s.logger.Error("聚合小时级指标失败",
zap.String("metric_type", metricType),
zap.Error(err))
return fmt.Errorf("聚合小时级指标失败: %w", err)
}
}
s.logger.Info("小时级统计指标聚合完成", zap.Time("date", date))
return nil
}
// AggregateDailyMetrics 聚合日级统计指标
func (s *StatisticsAggregateServiceImpl) AggregateDailyMetrics(ctx context.Context, date time.Time) error {
s.logger.Info("开始聚合日级统计指标", zap.Time("date", date))
// 获取指定日期的所有小时级指标
startTime := date.Truncate(24 * time.Hour)
endTime := startTime.Add(24 * time.Hour)
// 聚合不同类型的指标
metricTypes := []string{"api_calls", "users", "finance", "products", "certification"}
for _, metricType := range metricTypes {
err := s.aggregateMetricsByType(ctx, metricType, startTime, endTime, "daily")
if err != nil {
s.logger.Error("聚合日级指标失败",
zap.String("metric_type", metricType),
zap.Error(err))
return fmt.Errorf("聚合日级指标失败: %w", err)
}
}
s.logger.Info("日级统计指标聚合完成", zap.Time("date", date))
return nil
}
// AggregateWeeklyMetrics 聚合周级统计指标
func (s *StatisticsAggregateServiceImpl) AggregateWeeklyMetrics(ctx context.Context, date time.Time) error {
s.logger.Info("开始聚合周级统计指标", zap.Time("date", date))
// 获取指定周的所有日级指标
startTime := date.Truncate(24 * time.Hour)
endTime := startTime.Add(7 * 24 * time.Hour)
// 聚合不同类型的指标
metricTypes := []string{"api_calls", "users", "finance", "products", "certification"}
for _, metricType := range metricTypes {
err := s.aggregateMetricsByType(ctx, metricType, startTime, endTime, "weekly")
if err != nil {
s.logger.Error("聚合周级指标失败",
zap.String("metric_type", metricType),
zap.Error(err))
return fmt.Errorf("聚合周级指标失败: %w", err)
}
}
s.logger.Info("周级统计指标聚合完成", zap.Time("date", date))
return nil
}
// AggregateMonthlyMetrics 聚合月级统计指标
func (s *StatisticsAggregateServiceImpl) AggregateMonthlyMetrics(ctx context.Context, date time.Time) error {
s.logger.Info("开始聚合月级统计指标", zap.Time("date", date))
// 获取指定月的所有日级指标
startTime := date.Truncate(24 * time.Hour)
endTime := startTime.AddDate(0, 1, 0)
// 聚合不同类型的指标
metricTypes := []string{"api_calls", "users", "finance", "products", "certification"}
for _, metricType := range metricTypes {
err := s.aggregateMetricsByType(ctx, metricType, startTime, endTime, "monthly")
if err != nil {
s.logger.Error("聚合月级指标失败",
zap.String("metric_type", metricType),
zap.Error(err))
return fmt.Errorf("聚合月级指标失败: %w", err)
}
}
s.logger.Info("月级统计指标聚合完成", zap.Time("date", date))
return nil
}
// GetMetricsByType 根据类型获取统计指标
func (s *StatisticsAggregateServiceImpl) GetMetricsByType(ctx context.Context, metricType string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error) {
if metricType == "" {
return nil, fmt.Errorf("指标类型不能为空")
}
metrics, err := s.metricRepo.FindByTypeAndDateRange(ctx, metricType, startDate, endDate)
if err != nil {
s.logger.Error("查询统计指标失败",
zap.String("metric_type", metricType),
zap.Time("start_date", startDate),
zap.Time("end_date", endDate),
zap.Error(err))
return nil, fmt.Errorf("查询统计指标失败: %w", err)
}
return metrics, nil
}
// GetMetricsByDimension 根据维度获取统计指标
func (s *StatisticsAggregateServiceImpl) GetMetricsByDimension(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error) {
if metricType == "" {
return nil, fmt.Errorf("指标类型不能为空")
}
if dimension == "" {
return nil, fmt.Errorf("统计维度不能为空")
}
metrics, err := s.metricRepo.FindByTypeDimensionAndDateRange(ctx, metricType, dimension, startDate, endDate)
if err != nil {
s.logger.Error("查询统计指标失败",
zap.String("metric_type", metricType),
zap.String("dimension", dimension),
zap.Time("start_date", startDate),
zap.Time("end_date", endDate),
zap.Error(err))
return nil, fmt.Errorf("查询统计指标失败: %w", err)
}
return metrics, nil
}
// CalculateGrowthRate 计算增长率
func (s *StatisticsAggregateServiceImpl) CalculateGrowthRate(ctx context.Context, metricType, metricName string, currentPeriod, previousPeriod time.Time) (float64, error) {
if metricType == "" || metricName == "" {
return 0, fmt.Errorf("指标类型和名称不能为空")
}
// 获取当前周期的指标值
currentMetrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, currentPeriod, currentPeriod.Add(24*time.Hour))
if err != nil {
return 0, fmt.Errorf("查询当前周期指标失败: %w", err)
}
// 获取上一周期的指标值
previousMetrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, previousPeriod, previousPeriod.Add(24*time.Hour))
if err != nil {
return 0, fmt.Errorf("查询上一周期指标失败: %w", err)
}
// 计算总值
var currentValue, previousValue float64
for _, metric := range currentMetrics {
currentValue += metric.Value
}
for _, metric := range previousMetrics {
previousValue += metric.Value
}
// 计算增长率
if previousValue == 0 {
if currentValue > 0 {
return 100, nil // 从0增长到正数增长率为100%
}
return 0, nil // 都是0增长率为0%
}
growthRate := ((currentValue - previousValue) / previousValue) * 100
return growthRate, nil
}
// CalculateTrend 计算趋势
func (s *StatisticsAggregateServiceImpl) CalculateTrend(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (string, error) {
if metricType == "" || metricName == "" {
return "", fmt.Errorf("指标类型和名称不能为空")
}
// 获取时间范围内的指标
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
if err != nil {
return "", fmt.Errorf("查询指标失败: %w", err)
}
if len(metrics) < 2 {
return "insufficient_data", nil // 数据不足
}
// 按时间排序
sortMetricsByDate(metrics)
// 计算趋势
firstValue := metrics[0].Value
lastValue := metrics[len(metrics)-1].Value
if lastValue > firstValue {
return "increasing", nil // 上升趋势
} else if lastValue < firstValue {
return "decreasing", nil // 下降趋势
} else {
return "stable", nil // 稳定趋势
}
}
// aggregateMetricsByType 按类型聚合指标
func (s *StatisticsAggregateServiceImpl) aggregateMetricsByType(ctx context.Context, metricType string, startTime, endTime time.Time, dimension string) error {
// 获取源数据(实时或小时级数据)
sourceDimension := "realtime"
if dimension == "daily" {
sourceDimension = "hourly"
} else if dimension == "weekly" || dimension == "monthly" {
sourceDimension = "daily"
}
// 查询源数据
sourceMetrics, err := s.metricRepo.FindByTypeDimensionAndDateRange(ctx, metricType, sourceDimension, startTime, endTime)
if err != nil {
return fmt.Errorf("查询源数据失败: %w", err)
}
// 按指标名称分组聚合
metricGroups := make(map[string][]*entities.StatisticsMetric)
for _, metric := range sourceMetrics {
metricGroups[metric.MetricName] = append(metricGroups[metric.MetricName], metric)
}
// 聚合每个指标
for metricName, metrics := range metricGroups {
var totalValue float64
for _, metric := range metrics {
totalValue += metric.Value
}
// 创建聚合后的指标
aggregatedMetric, err := entities.NewStatisticsMetric(metricType, metricName, dimension, totalValue, startTime)
if err != nil {
return fmt.Errorf("创建聚合指标失败: %w", err)
}
// 保存聚合指标
err = s.metricRepo.Save(ctx, aggregatedMetric)
if err != nil {
return fmt.Errorf("保存聚合指标失败: %w", err)
}
}
return nil
}
// sortMetricsByDate 按日期排序指标
func sortMetricsByDate(metrics []*entities.StatisticsMetric) {
// 简单的冒泡排序
n := len(metrics)
for i := 0; i < n-1; i++ {
for j := 0; j < n-i-1; j++ {
if metrics[j].Date.After(metrics[j+1].Date) {
metrics[j], metrics[j+1] = metrics[j+1], metrics[j]
}
}
}
}

View File

@@ -0,0 +1,510 @@
package services
import (
"context"
"fmt"
"math"
"time"
"go.uber.org/zap"
"tyapi-server/internal/domains/statistics/entities"
"tyapi-server/internal/domains/statistics/repositories"
)
// StatisticsCalculationService 统计计算服务接口
// 负责各种统计计算和分析
type StatisticsCalculationService interface {
// 基础统计计算
CalculateTotal(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error)
CalculateAverage(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error)
CalculateMax(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error)
CalculateMin(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error)
// 高级统计计算
CalculateGrowthRate(ctx context.Context, metricType, metricName string, currentPeriod, previousPeriod time.Time) (float64, error)
CalculateTrend(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (string, error)
CalculateCorrelation(ctx context.Context, metricType1, metricName1, metricType2, metricName2 string, startDate, endDate time.Time) (float64, error)
// 业务指标计算
CalculateSuccessRate(ctx context.Context, startDate, endDate time.Time) (float64, error)
CalculateConversionRate(ctx context.Context, startDate, endDate time.Time) (float64, error)
CalculateRetentionRate(ctx context.Context, startDate, endDate time.Time) (float64, error)
// 时间序列分析
CalculateMovingAverage(ctx context.Context, metricType, metricName string, startDate, endDate time.Time, windowSize int) ([]float64, error)
CalculateSeasonality(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (map[string]float64, error)
}
// StatisticsCalculationServiceImpl 统计计算服务实现
type StatisticsCalculationServiceImpl struct {
metricRepo repositories.StatisticsRepository
logger *zap.Logger
}
// NewStatisticsCalculationService 创建统计计算服务
func NewStatisticsCalculationService(
metricRepo repositories.StatisticsRepository,
logger *zap.Logger,
) StatisticsCalculationService {
return &StatisticsCalculationServiceImpl{
metricRepo: metricRepo,
logger: logger,
}
}
// CalculateTotal 计算总值
func (s *StatisticsCalculationServiceImpl) CalculateTotal(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error) {
if metricType == "" || metricName == "" {
return 0, fmt.Errorf("指标类型和名称不能为空")
}
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
if err != nil {
s.logger.Error("查询指标失败",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Error(err))
return 0, fmt.Errorf("查询指标失败: %w", err)
}
var total float64
for _, metric := range metrics {
total += metric.Value
}
s.logger.Info("计算总值完成",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Float64("total", total))
return total, nil
}
// CalculateAverage 计算平均值
func (s *StatisticsCalculationServiceImpl) CalculateAverage(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error) {
if metricType == "" || metricName == "" {
return 0, fmt.Errorf("指标类型和名称不能为空")
}
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
if err != nil {
s.logger.Error("查询指标失败",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Error(err))
return 0, fmt.Errorf("查询指标失败: %w", err)
}
if len(metrics) == 0 {
return 0, nil
}
var total float64
for _, metric := range metrics {
total += metric.Value
}
average := total / float64(len(metrics))
s.logger.Info("计算平均值完成",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Float64("average", average))
return average, nil
}
// CalculateMax 计算最大值
func (s *StatisticsCalculationServiceImpl) CalculateMax(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error) {
if metricType == "" || metricName == "" {
return 0, fmt.Errorf("指标类型和名称不能为空")
}
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
if err != nil {
s.logger.Error("查询指标失败",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Error(err))
return 0, fmt.Errorf("查询指标失败: %w", err)
}
if len(metrics) == 0 {
return 0, nil
}
max := metrics[0].Value
for _, metric := range metrics {
if metric.Value > max {
max = metric.Value
}
}
s.logger.Info("计算最大值完成",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Float64("max", max))
return max, nil
}
// CalculateMin 计算最小值
func (s *StatisticsCalculationServiceImpl) CalculateMin(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error) {
if metricType == "" || metricName == "" {
return 0, fmt.Errorf("指标类型和名称不能为空")
}
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
if err != nil {
s.logger.Error("查询指标失败",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Error(err))
return 0, fmt.Errorf("查询指标失败: %w", err)
}
if len(metrics) == 0 {
return 0, nil
}
min := metrics[0].Value
for _, metric := range metrics {
if metric.Value < min {
min = metric.Value
}
}
s.logger.Info("计算最小值完成",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Float64("min", min))
return min, nil
}
// CalculateGrowthRate 计算增长率
func (s *StatisticsCalculationServiceImpl) CalculateGrowthRate(ctx context.Context, metricType, metricName string, currentPeriod, previousPeriod time.Time) (float64, error) {
if metricType == "" || metricName == "" {
return 0, fmt.Errorf("指标类型和名称不能为空")
}
// 获取当前周期的总值
currentTotal, err := s.CalculateTotal(ctx, metricType, metricName, currentPeriod, currentPeriod.Add(24*time.Hour))
if err != nil {
return 0, fmt.Errorf("计算当前周期总值失败: %w", err)
}
// 获取上一周期的总值
previousTotal, err := s.CalculateTotal(ctx, metricType, metricName, previousPeriod, previousPeriod.Add(24*time.Hour))
if err != nil {
return 0, fmt.Errorf("计算上一周期总值失败: %w", err)
}
// 计算增长率
if previousTotal == 0 {
if currentTotal > 0 {
return 100, nil // 从0增长到正数增长率为100%
}
return 0, nil // 都是0增长率为0%
}
growthRate := ((currentTotal - previousTotal) / previousTotal) * 100
s.logger.Info("计算增长率完成",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Float64("growth_rate", growthRate))
return growthRate, nil
}
// CalculateTrend 计算趋势
func (s *StatisticsCalculationServiceImpl) CalculateTrend(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (string, error) {
if metricType == "" || metricName == "" {
return "", fmt.Errorf("指标类型和名称不能为空")
}
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
if err != nil {
s.logger.Error("查询指标失败",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Error(err))
return "", fmt.Errorf("查询指标失败: %w", err)
}
if len(metrics) < 2 {
return "insufficient_data", nil // 数据不足
}
// 按时间排序
sortMetricsByDateCalc(metrics)
// 计算趋势
firstValue := metrics[0].Value
lastValue := metrics[len(metrics)-1].Value
var trend string
if lastValue > firstValue {
trend = "increasing" // 上升趋势
} else if lastValue < firstValue {
trend = "decreasing" // 下降趋势
} else {
trend = "stable" // 稳定趋势
}
s.logger.Info("计算趋势完成",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.String("trend", trend))
return trend, nil
}
// CalculateCorrelation 计算相关性
func (s *StatisticsCalculationServiceImpl) CalculateCorrelation(ctx context.Context, metricType1, metricName1, metricType2, metricName2 string, startDate, endDate time.Time) (float64, error) {
if metricType1 == "" || metricName1 == "" || metricType2 == "" || metricName2 == "" {
return 0, fmt.Errorf("指标类型和名称不能为空")
}
// 获取两个指标的数据
metrics1, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType1, metricName1, startDate, endDate)
if err != nil {
return 0, fmt.Errorf("查询指标1失败: %w", err)
}
metrics2, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType2, metricName2, startDate, endDate)
if err != nil {
return 0, fmt.Errorf("查询指标2失败: %w", err)
}
if len(metrics1) != len(metrics2) || len(metrics1) < 2 {
return 0, fmt.Errorf("数据点数量不足或不对称")
}
// 计算皮尔逊相关系数
correlation := s.calculatePearsonCorrelation(metrics1, metrics2)
s.logger.Info("计算相关性完成",
zap.String("metric1", metricType1+"."+metricName1),
zap.String("metric2", metricType2+"."+metricName2),
zap.Float64("correlation", correlation))
return correlation, nil
}
// CalculateSuccessRate 计算成功率
func (s *StatisticsCalculationServiceImpl) CalculateSuccessRate(ctx context.Context, startDate, endDate time.Time) (float64, error) {
// 获取成功调用次数
successTotal, err := s.CalculateTotal(ctx, "api_calls", "success_count", startDate, endDate)
if err != nil {
return 0, fmt.Errorf("计算成功调用次数失败: %w", err)
}
// 获取总调用次数
totalCalls, err := s.CalculateTotal(ctx, "api_calls", "total_count", startDate, endDate)
if err != nil {
return 0, fmt.Errorf("计算总调用次数失败: %w", err)
}
if totalCalls == 0 {
return 0, nil
}
successRate := (successTotal / totalCalls) * 100
s.logger.Info("计算成功率完成",
zap.Float64("success_rate", successRate))
return successRate, nil
}
// CalculateConversionRate 计算转化率
func (s *StatisticsCalculationServiceImpl) CalculateConversionRate(ctx context.Context, startDate, endDate time.Time) (float64, error) {
// 获取认证用户数
certifiedUsers, err := s.CalculateTotal(ctx, "users", "certified_count", startDate, endDate)
if err != nil {
return 0, fmt.Errorf("计算认证用户数失败: %w", err)
}
// 获取总用户数
totalUsers, err := s.CalculateTotal(ctx, "users", "total_count", startDate, endDate)
if err != nil {
return 0, fmt.Errorf("计算总用户数失败: %w", err)
}
if totalUsers == 0 {
return 0, nil
}
conversionRate := (certifiedUsers / totalUsers) * 100
s.logger.Info("计算转化率完成",
zap.Float64("conversion_rate", conversionRate))
return conversionRate, nil
}
// CalculateRetentionRate 计算留存率
func (s *StatisticsCalculationServiceImpl) CalculateRetentionRate(ctx context.Context, startDate, endDate time.Time) (float64, error) {
// 获取活跃用户数
activeUsers, err := s.CalculateTotal(ctx, "users", "active_count", startDate, endDate)
if err != nil {
return 0, fmt.Errorf("计算活跃用户数失败: %w", err)
}
// 获取总用户数
totalUsers, err := s.CalculateTotal(ctx, "users", "total_count", startDate, endDate)
if err != nil {
return 0, fmt.Errorf("计算总用户数失败: %w", err)
}
if totalUsers == 0 {
return 0, nil
}
retentionRate := (activeUsers / totalUsers) * 100
s.logger.Info("计算留存率完成",
zap.Float64("retention_rate", retentionRate))
return retentionRate, nil
}
// CalculateMovingAverage 计算移动平均
func (s *StatisticsCalculationServiceImpl) CalculateMovingAverage(ctx context.Context, metricType, metricName string, startDate, endDate time.Time, windowSize int) ([]float64, error) {
if metricType == "" || metricName == "" {
return nil, fmt.Errorf("指标类型和名称不能为空")
}
if windowSize <= 0 {
return nil, fmt.Errorf("窗口大小必须大于0")
}
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
if err != nil {
s.logger.Error("查询指标失败",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Error(err))
return nil, fmt.Errorf("查询指标失败: %w", err)
}
if len(metrics) < windowSize {
return nil, fmt.Errorf("数据点数量不足")
}
// 按时间排序
sortMetricsByDateCalc(metrics)
// 计算移动平均
var movingAverages []float64
for i := windowSize - 1; i < len(metrics); i++ {
var sum float64
for j := i - windowSize + 1; j <= i; j++ {
sum += metrics[j].Value
}
average := sum / float64(windowSize)
movingAverages = append(movingAverages, average)
}
s.logger.Info("计算移动平均完成",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Int("window_size", windowSize),
zap.Int("result_count", len(movingAverages)))
return movingAverages, nil
}
// CalculateSeasonality 计算季节性
func (s *StatisticsCalculationServiceImpl) CalculateSeasonality(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (map[string]float64, error) {
if metricType == "" || metricName == "" {
return nil, fmt.Errorf("指标类型和名称不能为空")
}
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
if err != nil {
s.logger.Error("查询指标失败",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Error(err))
return nil, fmt.Errorf("查询指标失败: %w", err)
}
if len(metrics) < 7 {
return nil, fmt.Errorf("数据点数量不足至少需要7个数据点")
}
// 按星期几分组
weeklyAverages := make(map[string][]float64)
for _, metric := range metrics {
weekday := metric.Date.Weekday().String()
weeklyAverages[weekday] = append(weeklyAverages[weekday], metric.Value)
}
// 计算每个星期几的平均值
seasonality := make(map[string]float64)
for weekday, values := range weeklyAverages {
var sum float64
for _, value := range values {
sum += value
}
seasonality[weekday] = sum / float64(len(values))
}
s.logger.Info("计算季节性完成",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Int("weekday_count", len(seasonality)))
return seasonality, nil
}
// calculatePearsonCorrelation 计算皮尔逊相关系数
func (s *StatisticsCalculationServiceImpl) calculatePearsonCorrelation(metrics1, metrics2 []*entities.StatisticsMetric) float64 {
n := len(metrics1)
if n < 2 {
return 0
}
// 计算均值
var sum1, sum2 float64
for i := 0; i < n; i++ {
sum1 += metrics1[i].Value
sum2 += metrics2[i].Value
}
mean1 := sum1 / float64(n)
mean2 := sum2 / float64(n)
// 计算协方差和方差
var numerator, denominator1, denominator2 float64
for i := 0; i < n; i++ {
diff1 := metrics1[i].Value - mean1
diff2 := metrics2[i].Value - mean2
numerator += diff1 * diff2
denominator1 += diff1 * diff1
denominator2 += diff2 * diff2
}
// 计算相关系数
if denominator1 == 0 || denominator2 == 0 {
return 0
}
correlation := numerator / math.Sqrt(denominator1*denominator2)
return correlation
}
// sortMetricsByDateCalc 按日期排序指标
func sortMetricsByDateCalc(metrics []*entities.StatisticsMetric) {
// 简单的冒泡排序
n := len(metrics)
for i := 0; i < n-1; i++ {
for j := 0; j < n-i-1; j++ {
if metrics[j].Date.After(metrics[j+1].Date) {
metrics[j], metrics[j+1] = metrics[j+1], metrics[j]
}
}
}
}

View File

@@ -0,0 +1,582 @@
package services
import (
"context"
"encoding/json"
"fmt"
"time"
"go.uber.org/zap"
"tyapi-server/internal/domains/statistics/entities"
"tyapi-server/internal/domains/statistics/repositories"
)
// StatisticsReportService 报告生成服务接口
// 负责统计报告的生成和管理
type StatisticsReportService interface {
// 报告生成
GenerateDashboardReport(ctx context.Context, userRole string, period string) (*entities.StatisticsReport, error)
GenerateSummaryReport(ctx context.Context, period string, startDate, endDate time.Time) (*entities.StatisticsReport, error)
GenerateDetailedReport(ctx context.Context, reportType string, startDate, endDate time.Time, filters map[string]interface{}) (*entities.StatisticsReport, error)
// 报告管理
GetReport(ctx context.Context, reportID string) (*entities.StatisticsReport, error)
GetReportsByUser(ctx context.Context, userID string, limit, offset int) ([]*entities.StatisticsReport, error)
DeleteReport(ctx context.Context, reportID string) error
// 报告状态管理
StartReportGeneration(ctx context.Context, reportID, generatedBy string) error
CompleteReportGeneration(ctx context.Context, reportID string, content string) error
FailReportGeneration(ctx context.Context, reportID string, reason string) error
// 报告清理
CleanupExpiredReports(ctx context.Context) error
}
// StatisticsReportServiceImpl 报告生成服务实现
type StatisticsReportServiceImpl struct {
reportRepo repositories.StatisticsReportRepository
metricRepo repositories.StatisticsRepository
calcService StatisticsCalculationService
logger *zap.Logger
}
// NewStatisticsReportService 创建报告生成服务
func NewStatisticsReportService(
reportRepo repositories.StatisticsReportRepository,
metricRepo repositories.StatisticsRepository,
calcService StatisticsCalculationService,
logger *zap.Logger,
) StatisticsReportService {
return &StatisticsReportServiceImpl{
reportRepo: reportRepo,
metricRepo: metricRepo,
calcService: calcService,
logger: logger,
}
}
// GenerateDashboardReport 生成仪表板报告
func (s *StatisticsReportServiceImpl) GenerateDashboardReport(ctx context.Context, userRole string, period string) (*entities.StatisticsReport, error) {
if userRole == "" {
return nil, fmt.Errorf("用户角色不能为空")
}
if period == "" {
return nil, fmt.Errorf("统计周期不能为空")
}
// 创建报告实体
title := fmt.Sprintf("%s仪表板报告 - %s", s.getRoleDisplayName(userRole), s.getPeriodDisplayName(period))
report, err := entities.NewStatisticsReport("dashboard", title, period, userRole)
if err != nil {
s.logger.Error("创建仪表板报告失败",
zap.String("user_role", userRole),
zap.String("period", period),
zap.Error(err))
return nil, fmt.Errorf("创建仪表板报告失败: %w", err)
}
// 保存报告
err = s.reportRepo.Save(ctx, report)
if err != nil {
s.logger.Error("保存仪表板报告失败",
zap.String("report_id", report.ID),
zap.Error(err))
return nil, fmt.Errorf("保存仪表板报告失败: %w", err)
}
s.logger.Info("仪表板报告创建成功",
zap.String("report_id", report.ID),
zap.String("user_role", userRole),
zap.String("period", period))
return report, nil
}
// GenerateSummaryReport 生成汇总报告
func (s *StatisticsReportServiceImpl) GenerateSummaryReport(ctx context.Context, period string, startDate, endDate time.Time) (*entities.StatisticsReport, error) {
if period == "" {
return nil, fmt.Errorf("统计周期不能为空")
}
// 创建报告实体
title := fmt.Sprintf("汇总报告 - %s (%s 至 %s)",
s.getPeriodDisplayName(period),
startDate.Format("2006-01-02"),
endDate.Format("2006-01-02"))
report, err := entities.NewStatisticsReport("summary", title, period, "admin")
if err != nil {
s.logger.Error("创建汇总报告失败",
zap.String("period", period),
zap.Error(err))
return nil, fmt.Errorf("创建汇总报告失败: %w", err)
}
// 生成报告内容
content, err := s.generateSummaryContent(ctx, startDate, endDate)
if err != nil {
s.logger.Error("生成汇总报告内容失败",
zap.String("report_id", report.ID),
zap.Error(err))
return nil, fmt.Errorf("生成汇总报告内容失败: %w", err)
}
// 完成报告生成
err = report.CompleteGeneration(content)
if err != nil {
s.logger.Error("完成汇总报告生成失败",
zap.String("report_id", report.ID),
zap.Error(err))
return nil, fmt.Errorf("完成汇总报告生成失败: %w", err)
}
// 保存报告
err = s.reportRepo.Save(ctx, report)
if err != nil {
s.logger.Error("保存汇总报告失败",
zap.String("report_id", report.ID),
zap.Error(err))
return nil, fmt.Errorf("保存汇总报告失败: %w", err)
}
s.logger.Info("汇总报告生成成功",
zap.String("report_id", report.ID),
zap.String("period", period))
return report, nil
}
// GenerateDetailedReport 生成详细报告
func (s *StatisticsReportServiceImpl) GenerateDetailedReport(ctx context.Context, reportType string, startDate, endDate time.Time, filters map[string]interface{}) (*entities.StatisticsReport, error) {
if reportType == "" {
return nil, fmt.Errorf("报告类型不能为空")
}
// 创建报告实体
title := fmt.Sprintf("详细报告 - %s (%s 至 %s)",
reportType,
startDate.Format("2006-01-02"),
endDate.Format("2006-01-02"))
report, err := entities.NewStatisticsReport("detailed", title, "custom", "admin")
if err != nil {
s.logger.Error("创建详细报告失败",
zap.String("report_type", reportType),
zap.Error(err))
return nil, fmt.Errorf("创建详细报告失败: %w", err)
}
// 生成报告内容
content, err := s.generateDetailedContent(ctx, reportType, startDate, endDate, filters)
if err != nil {
s.logger.Error("生成详细报告内容失败",
zap.String("report_id", report.ID),
zap.Error(err))
return nil, fmt.Errorf("生成详细报告内容失败: %w", err)
}
// 完成报告生成
err = report.CompleteGeneration(content)
if err != nil {
s.logger.Error("完成详细报告生成失败",
zap.String("report_id", report.ID),
zap.Error(err))
return nil, fmt.Errorf("完成详细报告生成失败: %w", err)
}
// 保存报告
err = s.reportRepo.Save(ctx, report)
if err != nil {
s.logger.Error("保存详细报告失败",
zap.String("report_id", report.ID),
zap.Error(err))
return nil, fmt.Errorf("保存详细报告失败: %w", err)
}
s.logger.Info("详细报告生成成功",
zap.String("report_id", report.ID),
zap.String("report_type", reportType))
return report, nil
}
// GetReport 获取报告
func (s *StatisticsReportServiceImpl) GetReport(ctx context.Context, reportID string) (*entities.StatisticsReport, error) {
if reportID == "" {
return nil, fmt.Errorf("报告ID不能为空")
}
report, err := s.reportRepo.FindByID(ctx, reportID)
if err != nil {
s.logger.Error("查询报告失败",
zap.String("report_id", reportID),
zap.Error(err))
return nil, fmt.Errorf("查询报告失败: %w", err)
}
return report, nil
}
// GetReportsByUser 获取用户的报告列表
func (s *StatisticsReportServiceImpl) GetReportsByUser(ctx context.Context, userID string, limit, offset int) ([]*entities.StatisticsReport, error) {
if userID == "" {
return nil, fmt.Errorf("用户ID不能为空")
}
reports, err := s.reportRepo.FindByUser(ctx, userID, limit, offset)
if err != nil {
s.logger.Error("查询用户报告失败",
zap.String("user_id", userID),
zap.Error(err))
return nil, fmt.Errorf("查询用户报告失败: %w", err)
}
return reports, nil
}
// DeleteReport 删除报告
func (s *StatisticsReportServiceImpl) DeleteReport(ctx context.Context, reportID string) error {
if reportID == "" {
return fmt.Errorf("报告ID不能为空")
}
err := s.reportRepo.Delete(ctx, reportID)
if err != nil {
s.logger.Error("删除报告失败",
zap.String("report_id", reportID),
zap.Error(err))
return fmt.Errorf("删除报告失败: %w", err)
}
s.logger.Info("报告删除成功", zap.String("report_id", reportID))
return nil
}
// StartReportGeneration 开始报告生成
func (s *StatisticsReportServiceImpl) StartReportGeneration(ctx context.Context, reportID, generatedBy string) error {
if reportID == "" {
return fmt.Errorf("报告ID不能为空")
}
if generatedBy == "" {
return fmt.Errorf("生成者ID不能为空")
}
report, err := s.reportRepo.FindByID(ctx, reportID)
if err != nil {
return fmt.Errorf("查询报告失败: %w", err)
}
err = report.StartGeneration(generatedBy)
if err != nil {
s.logger.Error("开始报告生成失败",
zap.String("report_id", reportID),
zap.Error(err))
return fmt.Errorf("开始报告生成失败: %w", err)
}
err = s.reportRepo.Save(ctx, report)
if err != nil {
s.logger.Error("保存报告状态失败",
zap.String("report_id", reportID),
zap.Error(err))
return fmt.Errorf("保存报告状态失败: %w", err)
}
s.logger.Info("报告生成开始",
zap.String("report_id", reportID),
zap.String("generated_by", generatedBy))
return nil
}
// CompleteReportGeneration 完成报告生成
func (s *StatisticsReportServiceImpl) CompleteReportGeneration(ctx context.Context, reportID string, content string) error {
if reportID == "" {
return fmt.Errorf("报告ID不能为空")
}
if content == "" {
return fmt.Errorf("报告内容不能为空")
}
report, err := s.reportRepo.FindByID(ctx, reportID)
if err != nil {
return fmt.Errorf("查询报告失败: %w", err)
}
err = report.CompleteGeneration(content)
if err != nil {
s.logger.Error("完成报告生成失败",
zap.String("report_id", reportID),
zap.Error(err))
return fmt.Errorf("完成报告生成失败: %w", err)
}
err = s.reportRepo.Save(ctx, report)
if err != nil {
s.logger.Error("保存报告内容失败",
zap.String("report_id", reportID),
zap.Error(err))
return fmt.Errorf("保存报告内容失败: %w", err)
}
s.logger.Info("报告生成完成", zap.String("report_id", reportID))
return nil
}
// FailReportGeneration 报告生成失败
func (s *StatisticsReportServiceImpl) FailReportGeneration(ctx context.Context, reportID string, reason string) error {
if reportID == "" {
return fmt.Errorf("报告ID不能为空")
}
report, err := s.reportRepo.FindByID(ctx, reportID)
if err != nil {
return fmt.Errorf("查询报告失败: %w", err)
}
err = report.FailGeneration(reason)
if err != nil {
s.logger.Error("标记报告生成失败",
zap.String("report_id", reportID),
zap.Error(err))
return fmt.Errorf("标记报告生成失败: %w", err)
}
err = s.reportRepo.Save(ctx, report)
if err != nil {
s.logger.Error("保存报告状态失败",
zap.String("report_id", reportID),
zap.Error(err))
return fmt.Errorf("保存报告状态失败: %w", err)
}
s.logger.Info("报告生成失败",
zap.String("report_id", reportID),
zap.String("reason", reason))
return nil
}
// CleanupExpiredReports 清理过期报告
func (s *StatisticsReportServiceImpl) CleanupExpiredReports(ctx context.Context) error {
s.logger.Info("开始清理过期报告")
// 获取所有已完成的报告
reports, err := s.reportRepo.FindByStatus(ctx, "completed")
if err != nil {
s.logger.Error("查询已完成报告失败", zap.Error(err))
return fmt.Errorf("查询已完成报告失败: %w", err)
}
var deletedCount int
for _, report := range reports {
if report.IsExpired() {
err = report.MarkAsExpired()
if err != nil {
s.logger.Error("标记报告过期失败",
zap.String("report_id", report.ID),
zap.Error(err))
continue
}
err = s.reportRepo.Save(ctx, report)
if err != nil {
s.logger.Error("保存过期报告状态失败",
zap.String("report_id", report.ID),
zap.Error(err))
continue
}
deletedCount++
}
}
s.logger.Info("过期报告清理完成", zap.Int("deleted_count", deletedCount))
return nil
}
// generateSummaryContent 生成汇总报告内容
func (s *StatisticsReportServiceImpl) generateSummaryContent(ctx context.Context, startDate, endDate time.Time) (string, error) {
content := make(map[string]interface{})
// API调用统计
apiCallsTotal, err := s.calcService.CalculateTotal(ctx, "api_calls", "total_count", startDate, endDate)
if err != nil {
s.logger.Warn("计算API调用总数失败", zap.Error(err))
}
apiCallsSuccess, err := s.calcService.CalculateTotal(ctx, "api_calls", "success_count", startDate, endDate)
if err != nil {
s.logger.Warn("计算API调用成功数失败", zap.Error(err))
}
// 用户统计
usersTotal, err := s.calcService.CalculateTotal(ctx, "users", "total_count", startDate, endDate)
if err != nil {
s.logger.Warn("计算用户总数失败", zap.Error(err))
}
usersCertified, err := s.calcService.CalculateTotal(ctx, "users", "certified_count", startDate, endDate)
if err != nil {
s.logger.Warn("计算认证用户数失败", zap.Error(err))
}
// 财务统计
financeTotal, err := s.calcService.CalculateTotal(ctx, "finance", "total_amount", startDate, endDate)
if err != nil {
s.logger.Warn("计算财务总额失败", zap.Error(err))
}
content["api_calls"] = map[string]interface{}{
"total": apiCallsTotal,
"success": apiCallsSuccess,
"rate": s.calculateRate(apiCallsSuccess, apiCallsTotal),
}
content["users"] = map[string]interface{}{
"total": usersTotal,
"certified": usersCertified,
"rate": s.calculateRate(usersCertified, usersTotal),
}
content["finance"] = map[string]interface{}{
"total_amount": financeTotal,
}
content["period"] = map[string]interface{}{
"start_date": startDate.Format("2006-01-02"),
"end_date": endDate.Format("2006-01-02"),
}
content["generated_at"] = time.Now().Format("2006-01-02 15:04:05")
// 转换为JSON字符串
jsonContent, err := json.Marshal(content)
if err != nil {
return "", fmt.Errorf("序列化报告内容失败: %w", err)
}
return string(jsonContent), nil
}
// generateDetailedContent 生成详细报告内容
func (s *StatisticsReportServiceImpl) generateDetailedContent(ctx context.Context, reportType string, startDate, endDate time.Time, filters map[string]interface{}) (string, error) {
content := make(map[string]interface{})
// 根据报告类型生成不同的内容
switch reportType {
case "api_calls":
content = s.generateApiCallsDetailedContent(ctx, startDate, endDate, filters)
case "users":
content = s.generateUsersDetailedContent(ctx, startDate, endDate, filters)
case "finance":
content = s.generateFinanceDetailedContent(ctx, startDate, endDate, filters)
default:
return "", fmt.Errorf("不支持的报告类型: %s", reportType)
}
content["report_type"] = reportType
content["period"] = map[string]interface{}{
"start_date": startDate.Format("2006-01-02"),
"end_date": endDate.Format("2006-01-02"),
}
content["generated_at"] = time.Now().Format("2006-01-02 15:04:05")
// 转换为JSON字符串
jsonContent, err := json.Marshal(content)
if err != nil {
return "", fmt.Errorf("序列化报告内容失败: %w", err)
}
return string(jsonContent), nil
}
// generateApiCallsDetailedContent 生成API调用详细内容
func (s *StatisticsReportServiceImpl) generateApiCallsDetailedContent(ctx context.Context, startDate, endDate time.Time, filters map[string]interface{}) map[string]interface{} {
content := make(map[string]interface{})
// 获取API调用统计数据
totalCalls, _ := s.calcService.CalculateTotal(ctx, "api_calls", "total_count", startDate, endDate)
successCalls, _ := s.calcService.CalculateTotal(ctx, "api_calls", "success_count", startDate, endDate)
failedCalls, _ := s.calcService.CalculateTotal(ctx, "api_calls", "failed_count", startDate, endDate)
avgResponseTime, _ := s.calcService.CalculateAverage(ctx, "api_calls", "response_time", startDate, endDate)
content["total_calls"] = totalCalls
content["success_calls"] = successCalls
content["failed_calls"] = failedCalls
content["success_rate"] = s.calculateRate(successCalls, totalCalls)
content["avg_response_time"] = avgResponseTime
return content
}
// generateUsersDetailedContent 生成用户详细内容
func (s *StatisticsReportServiceImpl) generateUsersDetailedContent(ctx context.Context, startDate, endDate time.Time, filters map[string]interface{}) map[string]interface{} {
content := make(map[string]interface{})
// 获取用户统计数据
totalUsers, _ := s.calcService.CalculateTotal(ctx, "users", "total_count", startDate, endDate)
certifiedUsers, _ := s.calcService.CalculateTotal(ctx, "users", "certified_count", startDate, endDate)
activeUsers, _ := s.calcService.CalculateTotal(ctx, "users", "active_count", startDate, endDate)
content["total_users"] = totalUsers
content["certified_users"] = certifiedUsers
content["active_users"] = activeUsers
content["certification_rate"] = s.calculateRate(certifiedUsers, totalUsers)
content["retention_rate"] = s.calculateRate(activeUsers, totalUsers)
return content
}
// generateFinanceDetailedContent 生成财务详细内容
func (s *StatisticsReportServiceImpl) generateFinanceDetailedContent(ctx context.Context, startDate, endDate time.Time, filters map[string]interface{}) map[string]interface{} {
content := make(map[string]interface{})
// 获取财务统计数据
totalAmount, _ := s.calcService.CalculateTotal(ctx, "finance", "total_amount", startDate, endDate)
rechargeAmount, _ := s.calcService.CalculateTotal(ctx, "finance", "recharge_amount", startDate, endDate)
deductAmount, _ := s.calcService.CalculateTotal(ctx, "finance", "deduct_amount", startDate, endDate)
content["total_amount"] = totalAmount
content["recharge_amount"] = rechargeAmount
content["deduct_amount"] = deductAmount
content["net_amount"] = rechargeAmount - deductAmount
return content
}
// calculateRate 计算比率
func (s *StatisticsReportServiceImpl) calculateRate(numerator, denominator float64) float64 {
if denominator == 0 {
return 0
}
return (numerator / denominator) * 100
}
// getRoleDisplayName 获取角色显示名称
func (s *StatisticsReportServiceImpl) getRoleDisplayName(role string) string {
roleNames := map[string]string{
"admin": "管理员",
"user": "用户",
"manager": "经理",
"analyst": "分析师",
}
if name, exists := roleNames[role]; exists {
return name
}
return role
}
// getPeriodDisplayName 获取周期显示名称
func (s *StatisticsReportServiceImpl) getPeriodDisplayName(period string) string {
periodNames := map[string]string{
"today": "今日",
"week": "本周",
"month": "本月",
"quarter": "本季度",
"year": "本年",
}
if name, exists := periodNames[period]; exists {
return name
}
return period
}