package services import ( "cmbone/internal/models" "database/sql" "time" ) const ( defaultTrendDays = 7 maxTrendDays = 30 imageTaskStatusFailed = "failed" ) type MetricsService struct { store *SuiStore } func NewMetricsService(store *SuiStore) *MetricsService { return &MetricsService{store: store} } func (s *MetricsService) GetDashboardMetrics() (models.DashboardMetrics, error) { start := localDayStart(time.Now()) end := start.AddDate(0, 0, 1) taskMetric, err := s.queryTaskMetric(start, end) if err != nil { return models.DashboardMetrics{}, err } auditLogs, err := s.countAuditLogs(start, end) if err != nil { return models.DashboardMetrics{}, err } return models.DashboardMetrics{ TodayProcessedImages: taskMetric.ProcessedImages, SuccessRate: successRate(taskMetric.SucceededTasks, taskMetric.ProcessedImages), AverageDurationMs: taskMetric.AverageDuration, FailedTasks: taskMetric.FailedTasks, TodayAuditLogs: auditLogs, }, nil } func (s *MetricsService) GetProcessingTrend(days int) ([]models.DailyMetric, error) { days = normalizeTrendDays(days) today := localDayStart(time.Now()) start := today.AddDate(0, 0, -days+1) trend := make([]models.DailyMetric, 0, days) for i := 0; i < days; i++ { dayStart := start.AddDate(0, 0, i) dayEnd := dayStart.AddDate(0, 0, 1) taskMetric, err := s.queryTaskMetric(dayStart, dayEnd) if err != nil { return nil, err } auditLogs, err := s.countAuditLogs(dayStart, dayEnd) if err != nil { return nil, err } trend = append(trend, models.DailyMetric{ Date: dayStart.Format("2006-01-02"), ProcessedImages: taskMetric.ProcessedImages, SucceededTasks: taskMetric.SucceededTasks, FailedTasks: taskMetric.FailedTasks, AverageDurationMs: taskMetric.AverageDuration, AuditLogs: auditLogs, }) } return trend, nil } type taskMetric struct { ProcessedImages int64 SucceededTasks int64 FailedTasks int64 AverageDuration float64 } func (s *MetricsService) queryTaskMetric(start time.Time, end time.Time) (taskMetric, error) { var metric taskMetric var average sql.NullFloat64 err := s.store.DB.QueryRow(` SELECT COUNT(*), COALESCE(SUM(CASE WHEN status = ? THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN status = ? THEN 1 ELSE 0 END), 0), AVG(CASE WHEN duration_ms > 0 THEN duration_ms END) FROM image_tasks WHERE created_at >= ? AND created_at < ? `, imageTaskStatusSucceeded, imageTaskStatusFailed, formatMetricTime(start), formatMetricTime(end)).Scan( &metric.ProcessedImages, &metric.SucceededTasks, &metric.FailedTasks, &average, ) if err != nil { return taskMetric{}, err } if average.Valid { metric.AverageDuration = average.Float64 } return metric, nil } func (s *MetricsService) countAuditLogs(start time.Time, end time.Time) (int64, error) { var count int64 err := s.store.DB.QueryRow(` SELECT COUNT(*) FROM audit_logs WHERE created_at >= ? AND created_at < ? `, formatMetricTime(start), formatMetricTime(end)).Scan(&count) return count, err } func normalizeTrendDays(days int) int { if days <= 0 { return defaultTrendDays } if days > maxTrendDays { return maxTrendDays } return days } func localDayStart(now time.Time) time.Time { local := now.In(time.Local) return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, local.Location()) } func formatMetricTime(value time.Time) string { return value.UTC().Format(time.RFC3339) } func successRate(succeeded int64, total int64) float64 { if total <= 0 { return 0 } return float64(succeeded) * 100 / float64(total) }