From 850183c2697f68c1261e659dfb9b81984f05d0a7 Mon Sep 17 00:00:00 2001 From: longgexx Date: Thu, 15 Jan 2026 11:22:13 +0800 Subject: [PATCH] =?UTF-8?q?fix(dashboard):=20=E4=BF=AE=E5=A4=8D=E9=A2=84?= =?UTF-8?q?=E8=81=9A=E5=90=88=E8=A1=A8=E4=BD=BF=E7=94=A8UTC=E6=97=B6?= =?UTF-8?q?=E5=8C=BA=E5=AF=BC=E8=87=B4=E4=BB=8A=E6=97=A5=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E4=B8=8D=E5=87=86=E7=A1=AE=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 dashboard_aggregation_repo.go 和 usage_log_repo.go 中的时区处理 从 UTC 改为使用服务器配置时区(默认 Asia/Shanghai),确保"今日" 统计数据与用户预期一致。 Co-Authored-By: Claude Opus 4.5 --- .../repository/dashboard_aggregation_repo.go | 47 ++++++++++--------- backend/internal/repository/usage_log_repo.go | 2 +- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/backend/internal/repository/dashboard_aggregation_repo.go b/backend/internal/repository/dashboard_aggregation_repo.go index d238e320..3543e061 100644 --- a/backend/internal/repository/dashboard_aggregation_repo.go +++ b/backend/internal/repository/dashboard_aggregation_repo.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/Wei-Shaw/sub2api/internal/pkg/timezone" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/lib/pq" ) @@ -41,21 +42,22 @@ func isPostgresDriver(db *sql.DB) bool { } func (r *dashboardAggregationRepository) AggregateRange(ctx context.Context, start, end time.Time) error { - startUTC := start.UTC() - endUTC := end.UTC() - if !endUTC.After(startUTC) { + loc := timezone.Location() + startLocal := start.In(loc) + endLocal := end.In(loc) + if !endLocal.After(startLocal) { return nil } - hourStart := startUTC.Truncate(time.Hour) - hourEnd := endUTC.Truncate(time.Hour) - if endUTC.After(hourEnd) { + hourStart := startLocal.Truncate(time.Hour) + hourEnd := endLocal.Truncate(time.Hour) + if endLocal.After(hourEnd) { hourEnd = hourEnd.Add(time.Hour) } - dayStart := truncateToDayUTC(startUTC) - dayEnd := truncateToDayUTC(endUTC) - if endUTC.After(dayEnd) { + dayStart := truncateToDay(startLocal) + dayEnd := truncateToDay(endLocal) + if endLocal.After(dayEnd) { dayEnd = dayEnd.Add(24 * time.Hour) } @@ -146,38 +148,41 @@ func (r *dashboardAggregationRepository) EnsureUsageLogsPartitions(ctx context.C } func (r *dashboardAggregationRepository) insertHourlyActiveUsers(ctx context.Context, start, end time.Time) error { + tzName := timezone.Name() query := ` INSERT INTO usage_dashboard_hourly_users (bucket_start, user_id) SELECT DISTINCT - date_trunc('hour', created_at AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' AS bucket_start, + date_trunc('hour', created_at AT TIME ZONE $3) AT TIME ZONE $3 AS bucket_start, user_id FROM usage_logs WHERE created_at >= $1 AND created_at < $2 ON CONFLICT DO NOTHING ` - _, err := r.sql.ExecContext(ctx, query, start.UTC(), end.UTC()) + _, err := r.sql.ExecContext(ctx, query, start, end, tzName) return err } func (r *dashboardAggregationRepository) insertDailyActiveUsers(ctx context.Context, start, end time.Time) error { + tzName := timezone.Name() query := ` INSERT INTO usage_dashboard_daily_users (bucket_date, user_id) SELECT DISTINCT - (bucket_start AT TIME ZONE 'UTC')::date AS bucket_date, + (bucket_start AT TIME ZONE $3)::date AS bucket_date, user_id FROM usage_dashboard_hourly_users WHERE bucket_start >= $1 AND bucket_start < $2 ON CONFLICT DO NOTHING ` - _, err := r.sql.ExecContext(ctx, query, start.UTC(), end.UTC()) + _, err := r.sql.ExecContext(ctx, query, start, end, tzName) return err } func (r *dashboardAggregationRepository) upsertHourlyAggregates(ctx context.Context, start, end time.Time) error { + tzName := timezone.Name() query := ` WITH hourly AS ( SELECT - date_trunc('hour', created_at AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' AS bucket_start, + date_trunc('hour', created_at AT TIME ZONE $3) AT TIME ZONE $3 AS bucket_start, COUNT(*) AS total_requests, COALESCE(SUM(input_tokens), 0) AS input_tokens, COALESCE(SUM(output_tokens), 0) AS output_tokens, @@ -236,15 +241,16 @@ func (r *dashboardAggregationRepository) upsertHourlyAggregates(ctx context.Cont active_users = EXCLUDED.active_users, computed_at = EXCLUDED.computed_at ` - _, err := r.sql.ExecContext(ctx, query, start.UTC(), end.UTC()) + _, err := r.sql.ExecContext(ctx, query, start, end, tzName) return err } func (r *dashboardAggregationRepository) upsertDailyAggregates(ctx context.Context, start, end time.Time) error { + tzName := timezone.Name() query := ` WITH daily AS ( SELECT - (bucket_start AT TIME ZONE 'UTC')::date AS bucket_date, + (bucket_start AT TIME ZONE $5)::date AS bucket_date, COALESCE(SUM(total_requests), 0) AS total_requests, COALESCE(SUM(input_tokens), 0) AS input_tokens, COALESCE(SUM(output_tokens), 0) AS output_tokens, @@ -255,7 +261,7 @@ func (r *dashboardAggregationRepository) upsertDailyAggregates(ctx context.Conte COALESCE(SUM(total_duration_ms), 0) AS total_duration_ms FROM usage_dashboard_hourly WHERE bucket_start >= $1 AND bucket_start < $2 - GROUP BY (bucket_start AT TIME ZONE 'UTC')::date + GROUP BY (bucket_start AT TIME ZONE $5)::date ), user_counts AS ( SELECT bucket_date, COUNT(*) AS active_users @@ -303,7 +309,7 @@ func (r *dashboardAggregationRepository) upsertDailyAggregates(ctx context.Conte active_users = EXCLUDED.active_users, computed_at = EXCLUDED.computed_at ` - _, err := r.sql.ExecContext(ctx, query, start.UTC(), end.UTC(), start.UTC(), end.UTC()) + _, err := r.sql.ExecContext(ctx, query, start, end, start, end, tzName) return err } @@ -376,9 +382,8 @@ func (r *dashboardAggregationRepository) createUsageLogsPartition(ctx context.Co return err } -func truncateToDayUTC(t time.Time) time.Time { - t = t.UTC() - return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC) +func truncateToDay(t time.Time) time.Time { + return timezone.StartOfDay(t) } func truncateToMonthUTC(t time.Time) time.Time { diff --git a/backend/internal/repository/usage_log_repo.go b/backend/internal/repository/usage_log_repo.go index 911bf621..338a3a3b 100644 --- a/backend/internal/repository/usage_log_repo.go +++ b/backend/internal/repository/usage_log_repo.go @@ -455,7 +455,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsAggregated(ctx context.Conte FROM usage_dashboard_hourly WHERE bucket_start = $1 ` - hourStart := now.UTC().Truncate(time.Hour) + hourStart := now.In(timezone.Location()).Truncate(time.Hour) if err := scanSingleRow(ctx, r.sql, hourlyActiveQuery, []any{hourStart}, &stats.HourlyActiveUsers); err != nil { if err != sql.ErrNoRows { return err