new
This commit is contained in:
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user