Files
tyapi-server/internal/domains/statistics/services/statistics_calculation_service.go
2025-09-12 01:15:09 +08:00

511 lines
16 KiB
Go
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.

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]
}
}
}
}