Просмотр исходного кода

feat(timezone): unify timezone handling across frontend and backend

- Add system timezone setting with IANA validation and UI configuration
- Create parseDateInputAsTimezone helper for date-only (end-of-day) and
  datetime inputs parsed in system timezone
- Update key/user expiresAt parsing to use system timezone
- Replace hardcoded zh-CN locale in webhook date formatting with
  formatInTimeZone for locale-independent output
- Add resolveSystemTimezone with fallback chain: DB -> env TZ -> UTC
- Configure next-intl with system timezone for consistent SSR/CSR display
- Add comprehensive tests for DST transitions and timezone boundaries

Co-Authored-By: Claude Opus 4.5 <[email protected]>
ding113 2 недель назад
Родитель
Сommit
edf12a842f
99 измененных файлов с 4453 добавлено и 646 удалено
  1. 6 0
      drizzle/0048_add_system_timezone.sql
  2. 2 0
      drizzle/0059_safe_xorn.sql
  3. 2937 0
      drizzle/meta/0059_snapshot.json
  4. 7 0
      drizzle/meta/_journal.json
  5. 3 0
      messages/en/settings/config.json
  6. 3 0
      messages/ja/settings/config.json
  7. 3 0
      messages/ru/settings/config.json
  8. 3 0
      messages/zh-CN/settings/config.json
  9. 3 0
      messages/zh-TW/settings/config.json
  10. 31 23
      src/actions/keys.ts
  11. 15 10
      src/actions/my-usage.ts
  12. 4 2
      src/actions/notifications.ts
  13. 8 8
      src/actions/providers.ts
  14. 6 3
      src/actions/system-config.ts
  15. 15 8
      src/actions/users.ts
  16. 4 1
      src/actions/webhook-targets.ts
  17. 9 14
      src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
  18. 9 16
      src/app/[locale]/dashboard/_components/rate-limit-events-chart.tsx
  19. 10 21
      src/app/[locale]/dashboard/_components/statistics/chart.tsx
  20. 9 4
      src/app/[locale]/dashboard/_components/user/key-list-header.tsx
  21. 14 20
      src/app/[locale]/dashboard/availability/_components/availability-view.tsx
  22. 4 2
      src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx
  23. 4 6
      src/app/[locale]/dashboard/availability/_components/endpoint/latency-curve.tsx
  24. 8 3
      src/app/[locale]/dashboard/availability/_components/endpoint/probe-grid.tsx
  25. 10 5
      src/app/[locale]/dashboard/availability/_components/endpoint/probe-terminal.tsx
  26. 18 18
      src/app/[locale]/dashboard/availability/_components/provider/lane-chart.tsx
  27. 4 5
      src/app/[locale]/dashboard/availability/_components/provider/latency-chart.tsx
  28. 6 2
      src/app/[locale]/dashboard/logs/_components/filters/time-filters.tsx
  29. 2 2
      src/app/[locale]/dashboard/logs/_components/filters/types.ts
  30. 3 3
      src/app/[locale]/dashboard/logs/_components/usage-logs-sections.tsx
  31. 4 7
      src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/request-list-sidebar.tsx
  32. 1 0
      src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/session-messages-client-actions.test.tsx
  33. 16 6
      src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/session-stats.tsx
  34. 5 1
      src/app/[locale]/layout.tsx
  35. 4 1
      src/app/[locale]/my-usage/_components/expiration-info.tsx
  36. 6 2
      src/app/[locale]/my-usage/_components/usage-logs-table.tsx
  37. 1 0
      src/app/[locale]/my-usage/page.tsx
  38. 34 0
      src/app/[locale]/settings/config/_components/system-settings-form.tsx
  39. 1 0
      src/app/[locale]/settings/config/page.tsx
  40. 4 2
      src/app/[locale]/settings/error-rules/_components/rule-list-table.tsx
  41. 6 5
      src/app/[locale]/settings/notifications/_components/webhook-target-card.tsx
  42. 15 5
      src/app/[locale]/settings/prices/_components/price-list.tsx
  43. 6 3
      src/app/[locale]/settings/providers/_components/forms/test-result-card.tsx
  44. 4 8
      src/app/[locale]/settings/providers/_components/provider-list-item.legacy.tsx
  45. 4 2
      src/app/[locale]/settings/sensitive-words/_components/word-list-table.tsx
  46. 14 6
      src/app/v1/_lib/proxy/rate-limit-guard.ts
  47. 4 2
      src/components/customs/version-checker.tsx
  48. 2 1
      src/components/ui/data-table.tsx
  49. 6 4
      src/components/ui/relative-time.tsx
  50. 8 2
      src/drizzle/schema.ts
  51. 5 12
      src/i18n/request.ts
  52. 1 0
      src/lib/config/system-settings-cache.ts
  53. 19 4
      src/lib/notification/notification-queue.ts
  54. 4 2
      src/lib/notification/tasks/daily-leaderboard.ts
  55. 1 1
      src/lib/rate-limit/lease-service.ts
  56. 4 4
      src/lib/rate-limit/lease.ts
  57. 20 12
      src/lib/rate-limit/service.ts
  58. 24 21
      src/lib/rate-limit/time-utils.ts
  59. 12 7
      src/lib/redis/leaderboard-cache.ts
  60. 18 3
      src/lib/utils/date-format.ts
  61. 52 0
      src/lib/utils/date-input.ts
  62. 14 4
      src/lib/utils/date.ts
  63. 170 0
      src/lib/utils/timezone.ts
  64. 10 0
      src/lib/validation/schemas.ts
  65. 1 0
      src/lib/webhook/renderers/custom.ts
  66. 5 4
      src/lib/webhook/renderers/dingtalk.ts
  67. 3 2
      src/lib/webhook/renderers/feishu.ts
  68. 5 4
      src/lib/webhook/renderers/telegram.ts
  69. 3 2
      src/lib/webhook/renderers/wechat.ts
  70. 5 2
      src/lib/webhook/templates/circuit-breaker.ts
  71. 6 5
      src/lib/webhook/templates/placeholders.ts
  72. 14 8
      src/lib/webhook/templates/test-messages.ts
  73. 2 0
      src/lib/webhook/types.ts
  74. 18 14
      src/lib/webhook/utils/date.ts
  75. 1 0
      src/repository/_shared/transformers.test.ts
  76. 1 0
      src/repository/_shared/transformers.ts
  77. 29 29
      src/repository/leaderboard.ts
  78. 5 3
      src/repository/notification-bindings.ts
  79. 4 4
      src/repository/overview.ts
  80. 3 3
      src/repository/provider.ts
  81. 6 6
      src/repository/statistics.ts
  82. 8 0
      src/repository/system-config.ts
  83. 8 0
      src/types/system-config.ts
  84. 1 0
      tests/integration/billing-model-source.test.ts
  85. 5 5
      tests/unit/actions/my-usage-date-range-dst.test.ts
  86. 1 1
      tests/unit/actions/my-usage-token-aggregation.test.ts
  87. 1 0
      tests/unit/dashboard/availability/latency-chart.test.tsx
  88. 1 0
      tests/unit/dashboard/availability/latency-curve.test.tsx
  89. 1 0
      tests/unit/lib/config/system-settings-cache.test.ts
  90. 60 0
      tests/unit/lib/date-format-timezone.test.ts
  91. 11 0
      tests/unit/lib/rate-limit/lease-service.test.ts
  92. 46 79
      tests/unit/lib/rate-limit/lease.test.ts
  93. 16 16
      tests/unit/lib/rate-limit/rolling-window-5h.test.ts
  94. 100 156
      tests/unit/lib/rate-limit/time-utils.test.ts
  95. 119 0
      tests/unit/lib/timezone/system-timezone.test.ts
  96. 180 0
      tests/unit/lib/timezone/timezone-resolver.test.ts
  97. 123 0
      tests/unit/lib/utils/date-input.test.ts
  98. 1 0
      tests/unit/proxy/pricing-no-price.test.ts
  99. 1 0
      tests/unit/proxy/session.test.ts

+ 6 - 0
drizzle/0048_add_system_timezone.sql

@@ -0,0 +1,6 @@
+-- Add timezone column to system_settings table
+-- Stores IANA timezone identifier (e.g., 'Asia/Shanghai', 'America/New_York')
+-- NULL means: use TZ environment variable or fallback to UTC
+
+ALTER TABLE "system_settings"
+ADD COLUMN IF NOT EXISTS "timezone" varchar(64);

+ 2 - 0
drizzle/0059_safe_xorn.sql

@@ -0,0 +1,2 @@
+ALTER TABLE "keys" ALTER COLUMN "expires_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
+ALTER TABLE "system_settings" ADD COLUMN "timezone" varchar(64);

+ 2937 - 0
drizzle/meta/0059_snapshot.json

@@ -0,0 +1,2937 @@
+{
+  "id": "5fd37dcd-8e23-4450-9177-cea694050745",
+  "prevId": "a0d35aff-e8d6-4b37-8546-f7c5fe30c925",
+  "version": "7",
+  "dialect": "postgresql",
+  "tables": {
+    "public.error_rules": {
+      "name": "error_rules",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "pattern": {
+          "name": "pattern",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "match_type": {
+          "name": "match_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'regex'"
+        },
+        "category": {
+          "name": "category",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "override_response": {
+          "name": "override_response",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "override_status_code": {
+          "name": "override_status_code",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "is_default": {
+          "name": "is_default",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "priority": {
+          "name": "priority",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_error_rules_enabled": {
+          "name": "idx_error_rules_enabled",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "priority",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "unique_pattern": {
+          "name": "unique_pattern",
+          "columns": [
+            {
+              "expression": "pattern",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": true,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_category": {
+          "name": "idx_category",
+          "columns": [
+            {
+              "expression": "category",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_match_type": {
+          "name": "idx_match_type",
+          "columns": [
+            {
+              "expression": "match_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.keys": {
+      "name": "keys",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "can_login_web_ui": {
+          "name": "can_login_web_ui",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "limit_5h_usd": {
+          "name": "limit_5h_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_daily_usd": {
+          "name": "limit_daily_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_reset_mode": {
+          "name": "daily_reset_mode",
+          "type": "daily_reset_mode",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'fixed'"
+        },
+        "daily_reset_time": {
+          "name": "daily_reset_time",
+          "type": "varchar(5)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'00:00'"
+        },
+        "limit_weekly_usd": {
+          "name": "limit_weekly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_monthly_usd": {
+          "name": "limit_monthly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_total_usd": {
+          "name": "limit_total_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_concurrent_sessions": {
+          "name": "limit_concurrent_sessions",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "provider_group": {
+          "name": "provider_group",
+          "type": "varchar(200)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'default'"
+        },
+        "cache_ttl_preference": {
+          "name": "cache_ttl_preference",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_keys_user_id": {
+          "name": "idx_keys_user_id",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_keys_created_at": {
+          "name": "idx_keys_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_keys_deleted_at": {
+          "name": "idx_keys_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.message_request": {
+      "name": "message_request",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "provider_id": {
+          "name": "provider_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "model": {
+          "name": "model",
+          "type": "varchar(128)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "duration_ms": {
+          "name": "duration_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cost_usd": {
+          "name": "cost_usd",
+          "type": "numeric(21, 15)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0'"
+        },
+        "cost_multiplier": {
+          "name": "cost_multiplier",
+          "type": "numeric(10, 4)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "request_sequence": {
+          "name": "request_sequence",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 1
+        },
+        "provider_chain": {
+          "name": "provider_chain",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "status_code": {
+          "name": "status_code",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "api_type": {
+          "name": "api_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "endpoint": {
+          "name": "endpoint",
+          "type": "varchar(256)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "original_model": {
+          "name": "original_model",
+          "type": "varchar(128)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "input_tokens": {
+          "name": "input_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "output_tokens": {
+          "name": "output_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "ttfb_ms": {
+          "name": "ttfb_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_input_tokens": {
+          "name": "cache_creation_input_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_read_input_tokens": {
+          "name": "cache_read_input_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_5m_input_tokens": {
+          "name": "cache_creation_5m_input_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_1h_input_tokens": {
+          "name": "cache_creation_1h_input_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_ttl_applied": {
+          "name": "cache_ttl_applied",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "context_1m_applied": {
+          "name": "context_1m_applied",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "special_settings": {
+          "name": "special_settings",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_message": {
+          "name": "error_message",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_stack": {
+          "name": "error_stack",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_cause": {
+          "name": "error_cause",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "blocked_by": {
+          "name": "blocked_by",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "blocked_reason": {
+          "name": "blocked_reason",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "user_agent": {
+          "name": "user_agent",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "messages_count": {
+          "name": "messages_count",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_message_request_user_date_cost": {
+          "name": "idx_message_request_user_date_cost",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "cost_usd",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_user_query": {
+          "name": "idx_message_request_user_query",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_session_id": {
+          "name": "idx_message_request_session_id",
+          "columns": [
+            {
+              "expression": "session_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_session_id_prefix": {
+          "name": "idx_message_request_session_id_prefix",
+          "columns": [
+            {
+              "expression": "\"session_id\" varchar_pattern_ops",
+              "asc": true,
+              "isExpression": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_session_seq": {
+          "name": "idx_message_request_session_seq",
+          "columns": [
+            {
+              "expression": "session_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "request_sequence",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_endpoint": {
+          "name": "idx_message_request_endpoint",
+          "columns": [
+            {
+              "expression": "endpoint",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_blocked_by": {
+          "name": "idx_message_request_blocked_by",
+          "columns": [
+            {
+              "expression": "blocked_by",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_provider_id": {
+          "name": "idx_message_request_provider_id",
+          "columns": [
+            {
+              "expression": "provider_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_user_id": {
+          "name": "idx_message_request_user_id",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_key": {
+          "name": "idx_message_request_key",
+          "columns": [
+            {
+              "expression": "key",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_created_at": {
+          "name": "idx_message_request_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_deleted_at": {
+          "name": "idx_message_request_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.model_prices": {
+      "name": "model_prices",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "model_name": {
+          "name": "model_name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "price_data": {
+          "name": "price_data",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "source": {
+          "name": "source",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'litellm'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_model_prices_latest": {
+          "name": "idx_model_prices_latest",
+          "columns": [
+            {
+              "expression": "model_name",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_model_prices_model_name": {
+          "name": "idx_model_prices_model_name",
+          "columns": [
+            {
+              "expression": "model_name",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_model_prices_created_at": {
+          "name": "idx_model_prices_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_model_prices_source": {
+          "name": "idx_model_prices_source",
+          "columns": [
+            {
+              "expression": "source",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.notification_settings": {
+      "name": "notification_settings",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "use_legacy_mode": {
+          "name": "use_legacy_mode",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "circuit_breaker_enabled": {
+          "name": "circuit_breaker_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "circuit_breaker_webhook": {
+          "name": "circuit_breaker_webhook",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_leaderboard_enabled": {
+          "name": "daily_leaderboard_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "daily_leaderboard_webhook": {
+          "name": "daily_leaderboard_webhook",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_leaderboard_time": {
+          "name": "daily_leaderboard_time",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'09:00'"
+        },
+        "daily_leaderboard_top_n": {
+          "name": "daily_leaderboard_top_n",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 5
+        },
+        "cost_alert_enabled": {
+          "name": "cost_alert_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "cost_alert_webhook": {
+          "name": "cost_alert_webhook",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cost_alert_threshold": {
+          "name": "cost_alert_threshold",
+          "type": "numeric(5, 2)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0.80'"
+        },
+        "cost_alert_check_interval": {
+          "name": "cost_alert_check_interval",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 60
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.notification_target_bindings": {
+      "name": "notification_target_bindings",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "notification_type": {
+          "name": "notification_type",
+          "type": "notification_type",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "target_id": {
+          "name": "target_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "schedule_cron": {
+          "name": "schedule_cron",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "schedule_timezone": {
+          "name": "schedule_timezone",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'Asia/Shanghai'"
+        },
+        "template_override": {
+          "name": "template_override",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "unique_notification_target_binding": {
+          "name": "unique_notification_target_binding",
+          "columns": [
+            {
+              "expression": "notification_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "target_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": true,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_notification_bindings_type": {
+          "name": "idx_notification_bindings_type",
+          "columns": [
+            {
+              "expression": "notification_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_notification_bindings_target": {
+          "name": "idx_notification_bindings_target",
+          "columns": [
+            {
+              "expression": "target_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {
+        "notification_target_bindings_target_id_webhook_targets_id_fk": {
+          "name": "notification_target_bindings_target_id_webhook_targets_id_fk",
+          "tableFrom": "notification_target_bindings",
+          "tableTo": "webhook_targets",
+          "columnsFrom": [
+            "target_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.provider_endpoint_probe_logs": {
+      "name": "provider_endpoint_probe_logs",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "endpoint_id": {
+          "name": "endpoint_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "source": {
+          "name": "source",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'scheduled'"
+        },
+        "ok": {
+          "name": "ok",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "status_code": {
+          "name": "status_code",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "latency_ms": {
+          "name": "latency_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_type": {
+          "name": "error_type",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_message": {
+          "name": "error_message",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_provider_endpoint_probe_logs_endpoint_created_at": {
+          "name": "idx_provider_endpoint_probe_logs_endpoint_created_at",
+          "columns": [
+            {
+              "expression": "endpoint_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_endpoint_probe_logs_created_at": {
+          "name": "idx_provider_endpoint_probe_logs_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {
+        "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk": {
+          "name": "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk",
+          "tableFrom": "provider_endpoint_probe_logs",
+          "tableTo": "provider_endpoints",
+          "columnsFrom": [
+            "endpoint_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.provider_endpoints": {
+      "name": "provider_endpoints",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "vendor_id": {
+          "name": "vendor_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "provider_type": {
+          "name": "provider_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'claude'"
+        },
+        "url": {
+          "name": "url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "label": {
+          "name": "label",
+          "type": "varchar(200)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "sort_order": {
+          "name": "sort_order",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "last_probed_at": {
+          "name": "last_probed_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_probe_ok": {
+          "name": "last_probe_ok",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_probe_status_code": {
+          "name": "last_probe_status_code",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_probe_latency_ms": {
+          "name": "last_probe_latency_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_probe_error_type": {
+          "name": "last_probe_error_type",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_probe_error_message": {
+          "name": "last_probe_error_message",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "uniq_provider_endpoints_vendor_type_url": {
+          "name": "uniq_provider_endpoints_vendor_type_url",
+          "columns": [
+            {
+              "expression": "vendor_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "provider_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "url",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": true,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_endpoints_vendor_type": {
+          "name": "idx_provider_endpoints_vendor_type",
+          "columns": [
+            {
+              "expression": "vendor_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "provider_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"provider_endpoints\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_endpoints_enabled": {
+          "name": "idx_provider_endpoints_enabled",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "vendor_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "provider_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"provider_endpoints\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_endpoints_created_at": {
+          "name": "idx_provider_endpoints_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_endpoints_deleted_at": {
+          "name": "idx_provider_endpoints_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {
+        "provider_endpoints_vendor_id_provider_vendors_id_fk": {
+          "name": "provider_endpoints_vendor_id_provider_vendors_id_fk",
+          "tableFrom": "provider_endpoints",
+          "tableTo": "provider_vendors",
+          "columnsFrom": [
+            "vendor_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.provider_vendors": {
+      "name": "provider_vendors",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "website_domain": {
+          "name": "website_domain",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "display_name": {
+          "name": "display_name",
+          "type": "varchar(200)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "website_url": {
+          "name": "website_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "favicon_url": {
+          "name": "favicon_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "uniq_provider_vendors_website_domain": {
+          "name": "uniq_provider_vendors_website_domain",
+          "columns": [
+            {
+              "expression": "website_domain",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": true,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_vendors_created_at": {
+          "name": "idx_provider_vendors_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.providers": {
+      "name": "providers",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "url": {
+          "name": "url",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "provider_vendor_id": {
+          "name": "provider_vendor_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "weight": {
+          "name": "weight",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 1
+        },
+        "priority": {
+          "name": "priority",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "cost_multiplier": {
+          "name": "cost_multiplier",
+          "type": "numeric(10, 4)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'1.0'"
+        },
+        "group_tag": {
+          "name": "group_tag",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "provider_type": {
+          "name": "provider_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'claude'"
+        },
+        "preserve_client_ip": {
+          "name": "preserve_client_ip",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "model_redirects": {
+          "name": "model_redirects",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "allowed_models": {
+          "name": "allowed_models",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'null'::jsonb"
+        },
+        "join_claude_pool": {
+          "name": "join_claude_pool",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "codex_instructions_strategy": {
+          "name": "codex_instructions_strategy",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'auto'"
+        },
+        "mcp_passthrough_type": {
+          "name": "mcp_passthrough_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'none'"
+        },
+        "mcp_passthrough_url": {
+          "name": "mcp_passthrough_url",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_5h_usd": {
+          "name": "limit_5h_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_daily_usd": {
+          "name": "limit_daily_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_reset_mode": {
+          "name": "daily_reset_mode",
+          "type": "daily_reset_mode",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'fixed'"
+        },
+        "daily_reset_time": {
+          "name": "daily_reset_time",
+          "type": "varchar(5)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'00:00'"
+        },
+        "limit_weekly_usd": {
+          "name": "limit_weekly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_monthly_usd": {
+          "name": "limit_monthly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_total_usd": {
+          "name": "limit_total_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "total_cost_reset_at": {
+          "name": "total_cost_reset_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_concurrent_sessions": {
+          "name": "limit_concurrent_sessions",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "max_retry_attempts": {
+          "name": "max_retry_attempts",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "circuit_breaker_failure_threshold": {
+          "name": "circuit_breaker_failure_threshold",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 5
+        },
+        "circuit_breaker_open_duration": {
+          "name": "circuit_breaker_open_duration",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 1800000
+        },
+        "circuit_breaker_half_open_success_threshold": {
+          "name": "circuit_breaker_half_open_success_threshold",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 2
+        },
+        "proxy_url": {
+          "name": "proxy_url",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "proxy_fallback_to_direct": {
+          "name": "proxy_fallback_to_direct",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "first_byte_timeout_streaming_ms": {
+          "name": "first_byte_timeout_streaming_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "streaming_idle_timeout_ms": {
+          "name": "streaming_idle_timeout_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "request_timeout_non_streaming_ms": {
+          "name": "request_timeout_non_streaming_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "website_url": {
+          "name": "website_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "favicon_url": {
+          "name": "favicon_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_ttl_preference": {
+          "name": "cache_ttl_preference",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "context_1m_preference": {
+          "name": "context_1m_preference",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "codex_reasoning_effort_preference": {
+          "name": "codex_reasoning_effort_preference",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "codex_reasoning_summary_preference": {
+          "name": "codex_reasoning_summary_preference",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "codex_text_verbosity_preference": {
+          "name": "codex_text_verbosity_preference",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "codex_parallel_tool_calls_preference": {
+          "name": "codex_parallel_tool_calls_preference",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "tpm": {
+          "name": "tpm",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "rpm": {
+          "name": "rpm",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "rpd": {
+          "name": "rpd",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "cc": {
+          "name": "cc",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_providers_enabled_priority": {
+          "name": "idx_providers_enabled_priority",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "priority",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "weight",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"providers\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_group": {
+          "name": "idx_providers_group",
+          "columns": [
+            {
+              "expression": "group_tag",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"providers\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_created_at": {
+          "name": "idx_providers_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_deleted_at": {
+          "name": "idx_providers_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_vendor_type": {
+          "name": "idx_providers_vendor_type",
+          "columns": [
+            {
+              "expression": "provider_vendor_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "provider_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"providers\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {
+        "providers_provider_vendor_id_provider_vendors_id_fk": {
+          "name": "providers_provider_vendor_id_provider_vendors_id_fk",
+          "tableFrom": "providers",
+          "tableTo": "provider_vendors",
+          "columnsFrom": [
+            "provider_vendor_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "restrict",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.request_filters": {
+      "name": "request_filters",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "scope": {
+          "name": "scope",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "action": {
+          "name": "action",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "match_type": {
+          "name": "match_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "target": {
+          "name": "target",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "replacement": {
+          "name": "replacement",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "priority": {
+          "name": "priority",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "binding_type": {
+          "name": "binding_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'global'"
+        },
+        "provider_ids": {
+          "name": "provider_ids",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "group_tags": {
+          "name": "group_tags",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_request_filters_enabled": {
+          "name": "idx_request_filters_enabled",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "priority",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_request_filters_scope": {
+          "name": "idx_request_filters_scope",
+          "columns": [
+            {
+              "expression": "scope",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_request_filters_action": {
+          "name": "idx_request_filters_action",
+          "columns": [
+            {
+              "expression": "action",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_request_filters_binding": {
+          "name": "idx_request_filters_binding",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "binding_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.sensitive_words": {
+      "name": "sensitive_words",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "word": {
+          "name": "word",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "match_type": {
+          "name": "match_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'contains'"
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_sensitive_words_enabled": {
+          "name": "idx_sensitive_words_enabled",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "match_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_sensitive_words_created_at": {
+          "name": "idx_sensitive_words_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.system_settings": {
+      "name": "system_settings",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "site_title": {
+          "name": "site_title",
+          "type": "varchar(128)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'Claude Code Hub'"
+        },
+        "allow_global_usage_view": {
+          "name": "allow_global_usage_view",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "currency_display": {
+          "name": "currency_display",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'USD'"
+        },
+        "billing_model_source": {
+          "name": "billing_model_source",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'original'"
+        },
+        "timezone": {
+          "name": "timezone",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "enable_auto_cleanup": {
+          "name": "enable_auto_cleanup",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "cleanup_retention_days": {
+          "name": "cleanup_retention_days",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 30
+        },
+        "cleanup_schedule": {
+          "name": "cleanup_schedule",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0 2 * * *'"
+        },
+        "cleanup_batch_size": {
+          "name": "cleanup_batch_size",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 10000
+        },
+        "enable_client_version_check": {
+          "name": "enable_client_version_check",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "verbose_provider_error": {
+          "name": "verbose_provider_error",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "enable_http2": {
+          "name": "enable_http2",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "intercept_anthropic_warmup_requests": {
+          "name": "intercept_anthropic_warmup_requests",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "enable_thinking_signature_rectifier": {
+          "name": "enable_thinking_signature_rectifier",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "enable_codex_session_id_completion": {
+          "name": "enable_codex_session_id_completion",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "enable_response_fixer": {
+          "name": "enable_response_fixer",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "response_fixer_config": {
+          "name": "response_fixer_config",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'{\"fixTruncatedJson\":true,\"fixSseFormat\":true,\"fixEncoding\":true,\"maxJsonDepth\":200,\"maxFixSize\":1048576}'::jsonb"
+        },
+        "quota_db_refresh_interval_seconds": {
+          "name": "quota_db_refresh_interval_seconds",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 10
+        },
+        "quota_lease_percent_5h": {
+          "name": "quota_lease_percent_5h",
+          "type": "numeric(5, 4)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0.05'"
+        },
+        "quota_lease_percent_daily": {
+          "name": "quota_lease_percent_daily",
+          "type": "numeric(5, 4)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0.05'"
+        },
+        "quota_lease_percent_weekly": {
+          "name": "quota_lease_percent_weekly",
+          "type": "numeric(5, 4)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0.05'"
+        },
+        "quota_lease_percent_monthly": {
+          "name": "quota_lease_percent_monthly",
+          "type": "numeric(5, 4)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0.05'"
+        },
+        "quota_lease_cap_usd": {
+          "name": "quota_lease_cap_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.users": {
+      "name": "users",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "role": {
+          "name": "role",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'user'"
+        },
+        "rpm_limit": {
+          "name": "rpm_limit",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_limit_usd": {
+          "name": "daily_limit_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "provider_group": {
+          "name": "provider_group",
+          "type": "varchar(200)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'default'"
+        },
+        "tags": {
+          "name": "tags",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'[]'::jsonb"
+        },
+        "limit_5h_usd": {
+          "name": "limit_5h_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_weekly_usd": {
+          "name": "limit_weekly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_monthly_usd": {
+          "name": "limit_monthly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_total_usd": {
+          "name": "limit_total_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_concurrent_sessions": {
+          "name": "limit_concurrent_sessions",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_reset_mode": {
+          "name": "daily_reset_mode",
+          "type": "daily_reset_mode",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'fixed'"
+        },
+        "daily_reset_time": {
+          "name": "daily_reset_time",
+          "type": "varchar(5)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'00:00'"
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "allowed_clients": {
+          "name": "allowed_clients",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'[]'::jsonb"
+        },
+        "allowed_models": {
+          "name": "allowed_models",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'[]'::jsonb"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_users_active_role_sort": {
+          "name": "idx_users_active_role_sort",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "role",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"users\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_users_enabled_expires_at": {
+          "name": "idx_users_enabled_expires_at",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "expires_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"users\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_users_created_at": {
+          "name": "idx_users_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_users_deleted_at": {
+          "name": "idx_users_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.webhook_targets": {
+      "name": "webhook_targets",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "provider_type": {
+          "name": "provider_type",
+          "type": "webhook_provider_type",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "webhook_url": {
+          "name": "webhook_url",
+          "type": "varchar(1024)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "telegram_bot_token": {
+          "name": "telegram_bot_token",
+          "type": "varchar(256)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "telegram_chat_id": {
+          "name": "telegram_chat_id",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "dingtalk_secret": {
+          "name": "dingtalk_secret",
+          "type": "varchar(256)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "custom_template": {
+          "name": "custom_template",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "custom_headers": {
+          "name": "custom_headers",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "proxy_url": {
+          "name": "proxy_url",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "proxy_fallback_to_direct": {
+          "name": "proxy_fallback_to_direct",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "last_test_at": {
+          "name": "last_test_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_test_result": {
+          "name": "last_test_result",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    }
+  },
+  "enums": {
+    "public.daily_reset_mode": {
+      "name": "daily_reset_mode",
+      "schema": "public",
+      "values": [
+        "fixed",
+        "rolling"
+      ]
+    },
+    "public.notification_type": {
+      "name": "notification_type",
+      "schema": "public",
+      "values": [
+        "circuit_breaker",
+        "daily_leaderboard",
+        "cost_alert"
+      ]
+    },
+    "public.webhook_provider_type": {
+      "name": "webhook_provider_type",
+      "schema": "public",
+      "values": [
+        "wechat",
+        "feishu",
+        "dingtalk",
+        "telegram",
+        "custom"
+      ]
+    }
+  },
+  "schemas": {},
+  "sequences": {},
+  "roles": {},
+  "policies": {},
+  "views": {},
+  "_meta": {
+    "columns": {},
+    "schemas": {},
+    "tables": {}
+  }
+}

+ 7 - 0
drizzle/meta/_journal.json

@@ -414,6 +414,13 @@
       "when": 1769523406503,
       "tag": "0058_silly_sleepwalker",
       "breakpoints": true
+    },
+    {
+      "idx": 59,
+      "version": "7",
+      "when": 1769539222210,
+      "tag": "0059_safe_xorn",
+      "breakpoints": true
     }
   ]
 }

+ 3 - 0
messages/en/settings/config.json

@@ -74,6 +74,9 @@
     "siteTitleRequired": "Site title cannot be empty",
     "verboseProviderError": "Verbose Provider Error",
     "verboseProviderErrorDesc": "When enabled, return detailed error messages when all providers are unavailable (including provider count, rate limit reasons, etc.); when disabled, only return a simple error code.",
+    "timezoneLabel": "System Timezone",
+    "timezoneDescription": "Set the system timezone for unified backend time boundary calculations and frontend date/time display. Leave empty to use the TZ environment variable or default to UTC.",
+    "timezoneAuto": "Auto (use TZ env variable)",
     "quotaLease": {
       "title": "Quota Lease Settings",
       "description": "Configure lease refresh interval and slice percentages for rate limit checks. The lease mechanism reduces DB query pressure while maintaining rate limit accuracy.",

+ 3 - 0
messages/ja/settings/config.json

@@ -74,6 +74,9 @@
     "siteTitleRequired": "サイトタイトルは空にできません",
     "verboseProviderError": "詳細なプロバイダーエラー",
     "verboseProviderErrorDesc": "有効にすると、すべてのプロバイダーが利用不可の場合に詳細なエラーメッセージ(プロバイダー数、レート制限の理由など)を返します。無効の場合は簡潔なエラーコードのみを返します。",
+    "timezoneLabel": "システムタイムゾーン",
+    "timezoneDescription": "バックエンドの時間境界計算とフロントエンドの日付/時刻表示を統一するためのシステムタイムゾーンを設定します。空のままにすると環境変数 TZ またはデフォルトの UTC が使用されます。",
+    "timezoneAuto": "自動 (環境変数 TZ を使用)",
     "quotaLease": {
       "title": "クォータリース設定",
       "description": "レート制限チェック時のリース更新間隔とスライス比率を設定します。リース機構はDBクエリの負荷を軽減しながら、レート制限の精度を維持します。",

+ 3 - 0
messages/ru/settings/config.json

@@ -74,6 +74,9 @@
     "siteTitleRequired": "Название сайта не может быть пустым",
     "verboseProviderError": "Подробные ошибки провайдеров",
     "verboseProviderErrorDesc": "При включении возвращает подробные сообщения об ошибках при недоступности всех провайдеров (количество провайдеров, причины ограничений и т.д.); при отключении возвращает только простой код ошибки.",
+    "timezoneLabel": "Системная Временная Зона",
+    "timezoneDescription": "Установите системную временную зону для единых вычислений временных границ в бэкенде и отображения даты/времени в интерфейсе. Оставьте пустым для использования переменной окружения TZ или UTC по умолчанию.",
+    "timezoneAuto": "Авто (использовать переменную окружения TZ)",
     "quotaLease": {
       "title": "Настройки аренды квоты",
       "description": "Настройка интервала обновления аренды и процентов среза для проверки лимитов. Механизм аренды снижает нагрузку на БД, сохраняя точность лимитов.",

+ 3 - 0
messages/zh-CN/settings/config.json

@@ -87,6 +87,9 @@
       "KRW": "₩ 韩元 (KRW)",
       "SGD": "S$ 新加坡元 (SGD)"
     },
+    "timezoneLabel": "系统时区",
+    "timezoneDescription": "设置系统时区,用于统一后端时间边界计算和前端日期/时间显示。留空时使用环境变量 TZ 或默认 UTC。",
+    "timezoneAuto": "自动 (使用环境变量 TZ)",
     "quotaLease": {
       "title": "配额租约设置",
       "description": "配置限额检查时的租约刷新间隔和切片比例。租约机制用于减少 DB 查询压力,同时保持限额精度。",

+ 3 - 0
messages/zh-TW/settings/config.json

@@ -74,6 +74,9 @@
     "siteTitleRequired": "站台標題不能為空",
     "verboseProviderError": "詳細供應商錯誤資訊",
     "verboseProviderErrorDesc": "開啟後,當所有供應商不可用時返回詳細錯誤資訊(包含供應商數量、限流原因等);關閉後僅返回簡潔錯誤碼。",
+    "timezoneLabel": "系統時區",
+    "timezoneDescription": "設定系統時區,用於統一後端時間邊界計算和前端日期/時間顯示。留空時使用環境變數 TZ 或預設 UTC。",
+    "timezoneAuto": "自動 (使用環境變數 TZ)",
     "quotaLease": {
       "title": "配額租約設定",
       "description": "設定限額檢查時的租約刷新間隔和切片比例。租約機制用於減少 DB 查詢壓力,同時保持限額精度。",

+ 31 - 23
src/actions/keys.ts

@@ -10,6 +10,8 @@ import { getSession } from "@/lib/auth";
 import { PROVIDER_GROUP } from "@/lib/constants/provider.constants";
 import { logger } from "@/lib/logger";
 import { ERROR_CODES } from "@/lib/utils/error-messages";
+import { parseDateInputAsTimezone } from "@/lib/utils/date-input";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import { normalizeProviderGroup, parseProviderGroups } from "@/lib/utils/provider-group";
 import { KeyFormSchema } from "@/lib/validation/schemas";
 import type { KeyStatistics } from "@/repository/key";
@@ -280,9 +282,12 @@ export async function addKey(data: {
 
     const generatedKey = `sk-${randomBytes(16).toString("hex")}`;
 
-    // 转换 expiresAt: undefined → null(永不过期),string → Date(设置日期)
+    // 转换 expiresAt: undefined → null(永不过期),string → Date(按系统时区解析)
+    const timezone = await resolveSystemTimezone();
     const expiresAt =
-      validatedData.expiresAt === undefined ? null : new Date(validatedData.expiresAt);
+      validatedData.expiresAt === undefined
+        ? null
+        : parseDateInputAsTimezone(validatedData.expiresAt, timezone);
 
     await createKey({
       user_id: data.userId,
@@ -497,22 +502,26 @@ export async function editKey(
 
     // 移除 providerGroup 子集校验(用户分组由 Key 分组自动计算)
 
-    // 转换 expiresAt:
+    // 转换 expiresAt(按系统时区解析)
     // - 未携带 expiresAt:不更新该字段
     // - 携带 expiresAt 但为空:清除(永不过期)
     // - 携带 expiresAt 且为字符串:设置为对应 Date
-    const expiresAt = hasExpiresAtField
-      ? validatedData.expiresAt === undefined
-        ? null
-        : new Date(validatedData.expiresAt)
-      : undefined;
-
-    if (expiresAt && Number.isNaN(expiresAt.getTime())) {
-      return {
-        ok: false,
-        error: tError("INVALID_FORMAT"),
-        errorCode: ERROR_CODES.INVALID_FORMAT,
-      };
+    let expiresAt: Date | null | undefined = undefined;
+    if (hasExpiresAtField) {
+      if (validatedData.expiresAt === undefined) {
+        expiresAt = null;
+      } else {
+        try {
+          const timezone = await resolveSystemTimezone();
+          expiresAt = parseDateInputAsTimezone(validatedData.expiresAt, timezone);
+        } catch {
+          return {
+            ok: false,
+            error: tError("INVALID_FORMAT"),
+            errorCode: ERROR_CODES.INVALID_FORMAT,
+          };
+        }
+      }
     }
 
     const isAdmin = session.user.role === "admin";
@@ -721,14 +730,14 @@ export async function getKeyLimitUsage(keyId: number): Promise<
       ]);
 
     // 获取重置时间
-    const resetInfo5h = getResetInfo("5h");
-    const resetInfoDaily = getResetInfoWithMode(
+    const resetInfo5h = await getResetInfo("5h");
+    const resetInfoDaily = await getResetInfoWithMode(
       "daily",
       key.dailyResetTime,
       key.dailyResetMode ?? "fixed"
     );
-    const resetInfoWeekly = getResetInfo("weekly");
-    const resetInfoMonthly = getResetInfo("monthly");
+    const resetInfoWeekly = await getResetInfo("weekly");
+    const resetInfoMonthly = await getResetInfo("monthly");
 
     return {
       ok: true,
@@ -1058,10 +1067,9 @@ export async function renewKeyExpiresAt(
       };
     }
 
-    const expiresAt = new Date(data.expiresAt);
-    if (Number.isNaN(expiresAt.getTime())) {
-      return { ok: false, error: tError("INVALID_FORMAT"), errorCode: ERROR_CODES.INVALID_FORMAT };
-    }
+    // 按系统时区解析过期日期
+    const timezone = await resolveSystemTimezone();
+    const expiresAt = parseDateInputAsTimezone(data.expiresAt, timezone);
 
     await updateKey(keyId, {
       expires_at: expiresAt,

+ 15 - 10
src/actions/my-usage.ts

@@ -5,12 +5,12 @@ import { and, eq, gte, isNull, lt, sql } from "drizzle-orm";
 import { db } from "@/drizzle/db";
 import { keys as keysTable, messageRequest } from "@/drizzle/schema";
 import { getSession } from "@/lib/auth";
-import { getEnvConfig } from "@/lib/config";
 import { logger } from "@/lib/logger";
 import { RateLimitService } from "@/lib/rate-limit/service";
 import type { DailyResetMode } from "@/lib/rate-limit/time-utils";
 import { SessionTracker } from "@/lib/session-tracker";
 import type { CurrencyCode } from "@/lib/utils";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import { EXCLUDE_WARMUP_CONDITION } from "@/repository/_shared/message-request-conditions";
 import { getSystemSettings } from "@/repository/system-config";
 import {
@@ -31,9 +31,10 @@ import type { ActionResult } from "./types";
  */
 function parseDateRangeInServerTimezone(
   startDate?: string,
-  endDate?: string
+  endDate?: string,
+  timezone?: string
 ): { startTime?: number; endTime?: number } {
-  const timezone = getEnvConfig().TZ;
+  const tz = timezone ?? "UTC";
 
   const toIsoDate = (dateStr: string): { ok: true; value: string } | { ok: false } => {
     return /^\d{4}-\d{2}-\d{2}$/.test(dateStr) ? { ok: true, value: dateStr } : { ok: false };
@@ -58,12 +59,12 @@ function parseDateRangeInServerTimezone(
   const endIso = endDate ? toIsoDate(endDate) : { ok: false as const };
 
   const parsedStart = startIso.ok
-    ? fromZonedTime(`${startIso.value}T00:00:00`, timezone).getTime()
+    ? fromZonedTime(`${startIso.value}T00:00:00`, tz).getTime()
     : Number.NaN;
 
   const endExclusiveDate = endIso.ok ? addIsoDays(endIso.value, 1) : null;
   const parsedEndExclusive = endExclusiveDate
-    ? fromZonedTime(`${endExclusiveDate}T00:00:00`, timezone).getTime()
+    ? fromZonedTime(`${endExclusiveDate}T00:00:00`, tz).getTime()
     : Number.NaN;
 
   return {
@@ -193,7 +194,7 @@ async function sumUserCost(userId: number, period: "5h" | "weekly" | "monthly" |
   }
 
   // 其他周期:使用统一的时间范围计算
-  const { startTime, endTime } = getTimeRangeForPeriod(period);
+  const { startTime, endTime } = await getTimeRangeForPeriod(period);
   return await sumUserCostInTimeRange(userId, startTime, endTime);
 }
 
@@ -241,7 +242,7 @@ export async function getMyQuota(): Promise<ActionResult<MyUsageQuota>> {
     const { sumUserCostInTimeRange } = await import("@/repository/statistics");
 
     // 计算用户每日消费的时间范围(使用用户的配置)
-    const userDailyTimeRange = getTimeRangeForPeriodWithMode(
+    const userDailyTimeRange = await getTimeRangeForPeriodWithMode(
       "daily",
       user.dailyResetTime ?? "00:00",
       (user.dailyResetMode as DailyResetMode | undefined) ?? "fixed"
@@ -345,7 +346,7 @@ export async function getMyTodayStats(): Promise<ActionResult<MyTodayStats>> {
 
     // 修复: 使用 Key 的 dailyResetTime 和 dailyResetMode 来计算时间范围
     const { getTimeRangeForPeriodWithMode } = await import("@/lib/rate-limit/time-utils");
-    const timeRange = getTimeRangeForPeriodWithMode(
+    const timeRange = await getTimeRangeForPeriodWithMode(
       "daily",
       session.key.dailyResetTime ?? "00:00",
       (session.key.dailyResetMode as DailyResetMode | undefined) ?? "fixed"
@@ -444,9 +445,11 @@ export async function getMyUsageLogs(
     const pageSize = Math.min(rawPageSize, 100);
     const page = filters.page && filters.page > 0 ? filters.page : 1;
 
+    const timezone = await resolveSystemTimezone();
     const { startTime, endTime } = parseDateRangeInServerTimezone(
       filters.startDate,
-      filters.endDate
+      filters.endDate,
+      timezone
     );
 
     const usageFilters: UsageLogFilters = {
@@ -586,9 +589,11 @@ export async function getMyStatsSummary(
     const settings = await getSystemSettings();
     const currencyCode = settings.currencyDisplay;
 
+    const timezone = await resolveSystemTimezone();
     const { startTime, endTime } = parseDateRangeInServerTimezone(
       filters.startDate,
-      filters.endDate
+      filters.endDate,
+      timezone
     );
 
     // Get aggregated stats using existing repository function

+ 4 - 2
src/actions/notifications.ts

@@ -3,6 +3,7 @@
 import { getSession } from "@/lib/auth";
 import type { NotificationJobType } from "@/lib/constants/notification.constants";
 import { logger } from "@/lib/logger";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import { WebhookNotifier } from "@/lib/webhook";
 import { buildTestMessage } from "@/lib/webhook/templates/test-messages";
 import {
@@ -80,8 +81,9 @@ export async function testWebhookAction(
 
   try {
     const notifier = new WebhookNotifier(trimmedUrl, { maxRetries: 1 });
-    const testMessage = buildTestMessage(type);
-    return notifier.send(testMessage);
+    const timezone = await resolveSystemTimezone();
+    const testMessage = buildTestMessage(type, timezone);
+    return notifier.send(testMessage, { timezone });
   } catch (error) {
     return {
       success: false,

+ 8 - 8
src/actions/providers.ts

@@ -1198,14 +1198,14 @@ export async function getProviderLimitUsage(providerId: number): Promise<
     ]);
 
     // 获取重置时间信息
-    const reset5h = getResetInfo("5h");
-    const resetDaily = getResetInfoWithMode(
+    const reset5h = await getResetInfo("5h");
+    const resetDaily = await getResetInfoWithMode(
       "daily",
       provider.dailyResetTime,
       provider.dailyResetMode ?? "fixed"
     );
-    const resetWeekly = getResetInfo("weekly");
-    const resetMonthly = getResetInfo("monthly");
+    const resetWeekly = await getResetInfo("weekly");
+    const resetMonthly = await getResetInfo("monthly");
 
     return {
       ok: true,
@@ -1322,15 +1322,15 @@ export async function getProviderLimitUsageBatch(
       const sessionCount = sessionCountMap.get(provider.id) || 0;
 
       // 获取重置时间信息
-      const reset5h = getResetInfo("5h");
+      const reset5h = await getResetInfo("5h");
       const dailyResetMode = (provider.dailyResetMode ?? "fixed") as "fixed" | "rolling";
-      const resetDaily = getResetInfoWithMode(
+      const resetDaily = await getResetInfoWithMode(
         "daily",
         provider.dailyResetTime ?? undefined,
         dailyResetMode
       );
-      const resetWeekly = getResetInfo("weekly");
-      const resetMonthly = getResetInfo("monthly");
+      const resetWeekly = await getResetInfo("weekly");
+      const resetMonthly = await getResetInfo("monthly");
 
       result.set(provider.id, {
         cost5h: {

+ 6 - 3
src/actions/system-config.ts

@@ -2,8 +2,9 @@
 
 import { revalidatePath } from "next/cache";
 import { getSession } from "@/lib/auth";
-import { getEnvConfig, invalidateSystemSettingsCache } from "@/lib/config";
+import { invalidateSystemSettingsCache } from "@/lib/config";
 import { logger } from "@/lib/logger";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import { UpdateSystemSettingsSchema } from "@/lib/validation/schemas";
 import { getSystemSettings, updateSystemSettings } from "@/repository/system-config";
 import type { ResponseFixerConfig, SystemSettings } from "@/types/system-config";
@@ -31,8 +32,8 @@ export async function getServerTimeZone(): Promise<ActionResult<{ timeZone: stri
       return { ok: false, error: "未授权" };
     }
 
-    const { TZ } = getEnvConfig();
-    return { ok: true, data: { timeZone: TZ } };
+    const timeZone = await resolveSystemTimezone();
+    return { ok: true, data: { timeZone } };
   } catch (error) {
     logger.error("获取时区失败:", error);
     return { ok: false, error: "获取时区失败" };
@@ -45,6 +46,7 @@ export async function saveSystemSettings(formData: {
   allowGlobalUsageView?: boolean;
   currencyDisplay?: string;
   billingModelSource?: string;
+  timezone?: string | null;
   enableAutoCleanup?: boolean;
   cleanupRetentionDays?: number;
   cleanupSchedule?: string;
@@ -77,6 +79,7 @@ export async function saveSystemSettings(formData: {
       allowGlobalUsageView: validated.allowGlobalUsageView,
       currencyDisplay: validated.currencyDisplay,
       billingModelSource: validated.billingModelSource,
+      timezone: validated.timezone,
       enableAutoCleanup: validated.enableAutoCleanup,
       cleanupRetentionDays: validated.cleanupRetentionDays,
       cleanupSchedule: validated.cleanupSchedule,

+ 15 - 8
src/actions/users.ts

@@ -11,7 +11,9 @@ import { PROVIDER_GROUP } from "@/lib/constants/provider.constants";
 import { logger } from "@/lib/logger";
 import { getUnauthorizedFields } from "@/lib/permissions/user-field-permissions";
 import { ERROR_CODES } from "@/lib/utils/error-messages";
+import { parseDateInputAsTimezone } from "@/lib/utils/date-input";
 import { normalizeProviderGroup } from "@/lib/utils/provider-group";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import { maskKey } from "@/lib/utils/validation";
 import { formatZodError } from "@/lib/utils/zod-i18n";
 import { CreateUserSchema, UpdateUserSchema } from "@/lib/validation/schemas";
@@ -1285,9 +1287,13 @@ export async function getUserLimitUsage(userId: number): Promise<
     // 获取每日消费(使用用户的 dailyResetTime 和 dailyResetMode 配置)
     const resetTime = user.dailyResetTime ?? "00:00";
     const resetMode = user.dailyResetMode ?? "fixed";
-    const { startTime, endTime } = getTimeRangeForPeriodWithMode("daily", resetTime, resetMode);
+    const { startTime, endTime } = await getTimeRangeForPeriodWithMode(
+      "daily",
+      resetTime,
+      resetMode
+    );
     const dailyCost = await sumUserCostInTimeRange(userId, startTime, endTime);
-    const resetInfo = getResetInfoWithMode("daily", resetTime, resetMode);
+    const resetInfo = await getResetInfoWithMode("daily", resetTime, resetMode);
     const resetAt = resetInfo.resetAt;
 
     return {
@@ -1336,8 +1342,9 @@ export async function renewUser(
       };
     }
 
-    // Parse and validate expiration date
-    const expiresAt = new Date(data.expiresAt);
+    // Parse and validate expiration date (using system timezone)
+    const timezone = await resolveSystemTimezone();
+    const expiresAt = parseDateInputAsTimezone(data.expiresAt, timezone);
 
     // 验证过期时间
     const validationResult = await validateExpiresAt(expiresAt, tError);
@@ -1477,10 +1484,10 @@ export async function getUserAllLimitUsage(userId: number): Promise<
     const { sumUserCostInTimeRange, sumUserTotalCost } = await import("@/repository/statistics");
 
     // 获取各时间范围
-    const range5h = getTimeRangeForPeriod("5h");
-    const rangeDaily = getTimeRangeForPeriod("daily", user.dailyResetTime || "00:00");
-    const rangeWeekly = getTimeRangeForPeriod("weekly");
-    const rangeMonthly = getTimeRangeForPeriod("monthly");
+    const range5h = await getTimeRangeForPeriod("5h");
+    const rangeDaily = await getTimeRangeForPeriod("daily", user.dailyResetTime || "00:00");
+    const rangeWeekly = await getTimeRangeForPeriod("weekly");
+    const rangeMonthly = await getTimeRangeForPeriod("monthly");
 
     // 并行查询各时间范围的消费
     const [usage5h, usageDaily, usageWeekly, usageMonthly, usageTotal] = await Promise.all([

+ 4 - 1
src/actions/webhook-targets.ts

@@ -5,6 +5,7 @@ import { getSession } from "@/lib/auth";
 import type { NotificationJobType } from "@/lib/constants/notification.constants";
 import { logger } from "@/lib/logger";
 import { isValidProxyUrl } from "@/lib/proxy-agent";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import { WebhookNotifier } from "@/lib/webhook";
 import { buildTestMessage } from "@/lib/webhook/templates/test-messages";
 import { getNotificationSettings, updateNotificationSettings } from "@/repository/notifications";
@@ -380,12 +381,14 @@ export async function testWebhookTargetAction(
     }
 
     const validatedType = NotificationTypeSchema.parse(notificationType);
-    const testMessage = buildTestMessage(toJobType(validatedType));
+    const timezone = await resolveSystemTimezone();
+    const testMessage = buildTestMessage(toJobType(validatedType), timezone);
 
     const notifier = new WebhookNotifier(target);
     const result = await notifier.send(testMessage, {
       notificationType: validatedType,
       data: buildTestData(validatedType),
+      timezone,
     });
 
     const latencyMs = Date.now() - start;

+ 9 - 14
src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx

@@ -1,11 +1,13 @@
 "use client";
 
-import { useLocale, useTranslations } from "next-intl";
+import { formatInTimeZone } from "date-fns-tz";
+import { useLocale, useTimeZone, useTranslations } from "next-intl";
 import * as React from "react";
 import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
 import { type ChartConfig, ChartContainer, ChartTooltip } from "@/components/ui/chart";
 import type { CurrencyCode } from "@/lib/utils";
 import { cn, Decimal, formatCurrency, toDecimal } from "@/lib/utils";
+import { getDateFnsLocale } from "@/lib/utils/date-format";
 import type { TimeRange, UserStatisticsData } from "@/types/statistics";
 import { TIME_RANGE_OPTIONS } from "@/types/statistics";
 import { BentoCard } from "./bento-grid";
@@ -40,6 +42,8 @@ export function StatisticsChartCard({
 }: StatisticsChartCardProps) {
   const t = useTranslations("dashboard.statistics");
   const locale = useLocale();
+  const timeZone = useTimeZone() ?? "UTC";
+  const dateFnsLocale = getDateFnsLocale(locale);
   const [activeChart, setActiveChart] = React.useState<"cost" | "calls">("cost");
   const [chartMode, setChartMode] = React.useState<"stacked" | "overlay">("overlay");
 
@@ -150,26 +154,17 @@ export function StatisticsChartCard({
   const formatDate = (dateStr: string) => {
     const date = new Date(dateStr);
     if (data.resolution === "hour") {
-      return date.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" });
+      return formatInTimeZone(date, timeZone, "HH:mm", { locale: dateFnsLocale });
     }
-    return date.toLocaleDateString(locale, { month: "numeric", day: "numeric" });
+    return formatInTimeZone(date, timeZone, "M/d", { locale: dateFnsLocale });
   };
 
   const formatTooltipDate = (dateStr: string) => {
     const date = new Date(dateStr);
     if (data.resolution === "hour") {
-      return date.toLocaleString(locale, {
-        month: "long",
-        day: "numeric",
-        hour: "2-digit",
-        minute: "2-digit",
-      });
+      return formatInTimeZone(date, timeZone, "MMMM d HH:mm", { locale: dateFnsLocale });
     }
-    return date.toLocaleDateString(locale, {
-      year: "numeric",
-      month: "long",
-      day: "numeric",
-    });
+    return formatInTimeZone(date, timeZone, "yyyy MMMM d", { locale: dateFnsLocale });
   };
 
   return (

+ 9 - 16
src/app/[locale]/dashboard/_components/rate-limit-events-chart.tsx

@@ -1,10 +1,12 @@
 "use client";
 
-import { useLocale, useTranslations } from "next-intl";
+import { formatInTimeZone } from "date-fns-tz";
+import { useLocale, useTimeZone, useTranslations } from "next-intl";
 import * as React from "react";
 import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 import { type ChartConfig, ChartContainer, ChartTooltip } from "@/components/ui/chart";
+import { getDateFnsLocale } from "@/lib/utils/date-format";
 import type { EventTimeline } from "@/types/statistics";
 
 export interface RateLimitEventsChartProps {
@@ -18,6 +20,8 @@ export interface RateLimitEventsChartProps {
 export function RateLimitEventsChart({ data }: RateLimitEventsChartProps) {
   const t = useTranslations("dashboard.rateLimits.chart");
   const locale = useLocale();
+  const timeZone = useTimeZone() ?? "UTC";
+  const dateFnsLocale = getDateFnsLocale(locale);
 
   const chartConfig = React.useMemo(
     () =>
@@ -30,27 +34,16 @@ export function RateLimitEventsChart({ data }: RateLimitEventsChartProps) {
     [t]
   );
 
-  // 格式化小时显示
+  // Format hour display with timezone
   const formatHour = (hourStr: string) => {
     const date = new Date(hourStr);
-    return date.toLocaleTimeString(locale, {
-      month: "numeric",
-      day: "numeric",
-      hour: "2-digit",
-      minute: "2-digit",
-    });
+    return formatInTimeZone(date, timeZone, "M/d HH:mm", { locale: dateFnsLocale });
   };
 
-  // 格式化 tooltip 显示
+  // Format tooltip display with timezone
   const formatTooltipHour = (hourStr: string) => {
     const date = new Date(hourStr);
-    return date.toLocaleString(locale, {
-      year: "numeric",
-      month: "long",
-      day: "numeric",
-      hour: "2-digit",
-      minute: "2-digit",
-    });
+    return formatInTimeZone(date, timeZone, "yyyy MMMM d HH:mm", { locale: dateFnsLocale });
   };
 
   // 计算总事件数

+ 10 - 21
src/app/[locale]/dashboard/_components/statistics/chart.tsx

@@ -1,12 +1,14 @@
 "use client";
 
-import { useLocale, useTranslations } from "next-intl";
+import { formatInTimeZone } from "date-fns-tz";
+import { useLocale, useTimeZone, useTranslations } from "next-intl";
 import * as React from "react";
 import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 import { type ChartConfig, ChartContainer, ChartLegend, ChartTooltip } from "@/components/ui/chart";
 import type { CurrencyCode } from "@/lib/utils";
 import { cn, Decimal, formatCurrency, toDecimal } from "@/lib/utils";
+import { getDateFnsLocale } from "@/lib/utils/date-format";
 import type { TimeRange, UserStatisticsData } from "@/types/statistics";
 import { TimeRangeSelector } from "./time-range-selector";
 
@@ -58,6 +60,8 @@ export function UserStatisticsChart({
 }: UserStatisticsChartProps) {
   const t = useTranslations("dashboard.statistics");
   const locale = useLocale();
+  const timeZone = useTimeZone() ?? "UTC";
+  const dateFnsLocale = getDateFnsLocale(locale);
   const [activeChart, setActiveChart] = React.useState<"cost" | "calls">("cost");
   const [chartMode, setChartMode] = React.useState<"stacked" | "overlay">("overlay");
 
@@ -230,34 +234,19 @@ export function UserStatisticsChart({
   const formatDate = (dateStr: string) => {
     const date = new Date(dateStr);
     if (data.resolution === "hour") {
-      return date.toLocaleTimeString(locale, {
-        hour: "2-digit",
-        minute: "2-digit",
-      });
+      return formatInTimeZone(date, timeZone, "HH:mm", { locale: dateFnsLocale });
     } else {
-      return date.toLocaleDateString(locale, {
-        month: "numeric",
-        day: "numeric",
-      });
+      return formatInTimeZone(date, timeZone, "M/d", { locale: dateFnsLocale });
     }
   };
 
-  // 格式化tooltip日期
+  // Format tooltip date with timezone
   const formatTooltipDate = (dateStr: string) => {
     const date = new Date(dateStr);
     if (data.resolution === "hour") {
-      return date.toLocaleString(locale, {
-        month: "long",
-        day: "numeric",
-        hour: "2-digit",
-        minute: "2-digit",
-      });
+      return formatInTimeZone(date, timeZone, "MMMM d HH:mm", { locale: dateFnsLocale });
     } else {
-      return date.toLocaleDateString(locale, {
-        year: "numeric",
-        month: "long",
-        day: "numeric",
-      });
+      return formatInTimeZone(date, timeZone, "yyyy MMMM d", { locale: dateFnsLocale });
     }
   };
 

+ 9 - 4
src/app/[locale]/dashboard/_components/user/key-list-header.tsx

@@ -1,7 +1,8 @@
 "use client";
 import { useQuery } from "@tanstack/react-query";
+import { formatInTimeZone } from "date-fns-tz";
 import { CheckCircle, Copy, Eye, EyeOff, ListPlus } from "lucide-react";
-import { useLocale, useTranslations } from "next-intl";
+import { useLocale, useTimeZone, useTranslations } from "next-intl";
 import { useEffect, useMemo, useState } from "react";
 import { getProxyStatus } from "@/actions/proxy-status";
 import { FormErrorBoundary } from "@/components/form-error-boundary";
@@ -37,7 +38,10 @@ async function fetchProxyStatus(): Promise<ProxyStatusResponse> {
   throw new Error(result.error || "Failed to fetch proxy status");
 }
 
-function createFormatRelativeTime(t: (key: string, params?: Record<string, number>) => string) {
+function createFormatRelativeTime(
+  t: (key: string, params?: Record<string, number>) => string,
+  timeZone: string
+) {
   return (timestamp: number): string => {
     const diff = Date.now() - timestamp;
     if (diff <= 0) {
@@ -67,7 +71,7 @@ function createFormatRelativeTime(t: (key: string, params?: Record<string, numbe
       return t("proxyStatus.timeAgo.daysAgo", { count: days });
     }
 
-    return new Date(timestamp).toLocaleDateString();
+    return formatInTimeZone(new Date(timestamp), timeZone, "yyyy-MM-dd");
   };
 }
 
@@ -99,6 +103,7 @@ export function KeyListHeader({
   const t = useTranslations("dashboard.keyListHeader");
   const tUsers = useTranslations("users");
   const locale = useLocale();
+  const timeZone = useTimeZone() ?? "UTC";
 
   // 检测 clipboard 是否可用
   useEffect(() => {
@@ -108,7 +113,7 @@ export function KeyListHeader({
   const totalTodayUsage =
     activeUser?.keys.reduce((sum, key) => sum + (key.todayUsage ?? 0), 0) ?? 0;
 
-  const formatRelativeTime = useMemo(() => createFormatRelativeTime(t), [t]);
+  const formatRelativeTime = useMemo(() => createFormatRelativeTime(t, timeZone), [t, timeZone]);
 
   // 获取用户状态和过期信息
   const userStatusInfo = useMemo(() => {

+ 14 - 20
src/app/[locale]/dashboard/availability/_components/availability-view.tsx

@@ -1,7 +1,8 @@
 "use client";
 
+import { formatInTimeZone } from "date-fns-tz";
 import { Activity, CheckCircle2, HelpCircle, RefreshCw, XCircle } from "lucide-react";
-import { useTranslations } from "next-intl";
+import { useTimeZone, useTranslations } from "next-intl";
 import { useCallback, useEffect, useMemo, useState } from "react";
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
@@ -63,31 +64,19 @@ function getAvailabilityColor(score: number, hasData: boolean): string {
 /**
  * Format bucket time for display in tooltip
  */
-function formatBucketTime(isoString: string, bucketSizeMinutes: number): string {
+function formatBucketTime(isoString: string, bucketSizeMinutes: number, timeZone?: string): string {
   const date = new Date(isoString);
+  const tz = timeZone ?? "UTC";
   if (bucketSizeMinutes >= 1440) {
-    // Daily buckets: show date
-    return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
+    return formatInTimeZone(date, tz, "MMM d");
   }
   if (bucketSizeMinutes >= 60) {
-    // Hourly buckets: show date + hour
-    return date.toLocaleString(undefined, {
-      month: "short",
-      day: "numeric",
-      hour: "2-digit",
-      minute: "2-digit",
-    });
+    return formatInTimeZone(date, tz, "MMM d HH:mm");
   }
-  // Sub-hour buckets: show full time with seconds for precision
   if (bucketSizeMinutes < 1) {
-    return date.toLocaleTimeString(undefined, {
-      hour: "2-digit",
-      minute: "2-digit",
-      second: "2-digit",
-    });
+    return formatInTimeZone(date, tz, "HH:mm:ss");
   }
-  // Minute buckets: show time
-  return date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
+  return formatInTimeZone(date, tz, "HH:mm");
 }
 
 /**
@@ -107,6 +96,7 @@ function _formatBucketSizeDisplay(minutes: number): string {
 
 export function AvailabilityView() {
   const t = useTranslations("dashboard.availability");
+  const timeZone = useTimeZone() ?? "UTC";
   const [data, setData] = useState<AvailabilityQueryResult | null>(null);
   const [loading, setLoading] = useState(true);
   const [error, setError] = useState<string | null>(null);
@@ -455,7 +445,11 @@ export function AvailabilityView() {
                               <TooltipContent side="top" className="max-w-xs">
                                 <div className="text-sm space-y-1">
                                   <div className="font-medium">
-                                    {formatBucketTime(bucketStart, data?.bucketSizeMinutes ?? 5)}
+                                    {formatBucketTime(
+                                      bucketStart,
+                                      data?.bucketSizeMinutes ?? 5,
+                                      timeZone
+                                    )}
                                   </div>
                                   {hasData && bucket ? (
                                     <>

+ 4 - 2
src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx

@@ -1,7 +1,8 @@
 "use client";
 
+import { formatInTimeZone } from "date-fns-tz";
 import { Activity, CheckCircle2, Play, RefreshCw, XCircle } from "lucide-react";
-import { useTranslations } from "next-intl";
+import { useTimeZone, useTranslations } from "next-intl";
 import { useCallback, useEffect, useState } from "react";
 import { toast } from "sonner";
 import { getProviderVendors, probeProviderEndpoint } from "@/actions/provider-endpoints";
@@ -44,6 +45,7 @@ const PROVIDER_TYPES: ProviderType[] = [
 export function EndpointProbeHistory() {
   const t = useTranslations("dashboard.availability");
   const tErrors = useTranslations("errors");
+  const timeZone = useTimeZone() ?? "UTC";
 
   const [vendors, setVendors] = useState<ProviderVendor[]>([]);
   const [selectedVendorId, setSelectedVendorId] = useState<string>("");
@@ -259,7 +261,7 @@ export function EndpointProbeHistory() {
                 logs.map((log) => (
                   <TableRow key={log.id}>
                     <TableCell className="font-mono text-xs">
-                      {new Date(log.createdAt).toLocaleString()}
+                      {formatInTimeZone(new Date(log.createdAt), timeZone, "yyyy-MM-dd HH:mm:ss")}
                       <div className="text-[10px] text-muted-foreground mt-0.5 uppercase tracking-wider">
                         {t(`probeHistory.${log.source === "manual" ? "manual" : "auto"}`)}
                       </div>

+ 4 - 6
src/app/[locale]/dashboard/availability/_components/endpoint/latency-curve.tsx

@@ -1,6 +1,7 @@
 "use client";
 
-import { useTranslations } from "next-intl";
+import { formatInTimeZone } from "date-fns-tz";
+import { useTimeZone, useTranslations } from "next-intl";
 import { useMemo } from "react";
 import { CartesianGrid, Line, LineChart, ResponsiveContainer, XAxis, YAxis } from "recharts";
 import {
@@ -26,6 +27,7 @@ const chartConfig = {
 
 export function LatencyCurve({ logs, className }: LatencyCurveProps) {
   const t = useTranslations("dashboard.availability.latencyCurve");
+  const timeZone = useTimeZone() ?? "UTC";
 
   // Transform logs to chart data
   const chartData = useMemo(() => {
@@ -56,11 +58,7 @@ export function LatencyCurve({ logs, className }: LatencyCurveProps) {
 
   const formatTime = (time: string) => {
     const date = new Date(time);
-    return date.toLocaleTimeString(undefined, {
-      hour: "2-digit",
-      minute: "2-digit",
-      second: "2-digit",
-    });
+    return formatInTimeZone(date, timeZone, "HH:mm:ss");
   };
 
   const formatLatency = (value: number) => {

+ 8 - 3
src/app/[locale]/dashboard/availability/_components/endpoint/probe-grid.tsx

@@ -1,7 +1,8 @@
 "use client";
 
+import { formatInTimeZone } from "date-fns-tz";
 import { CheckCircle2, HelpCircle, XCircle } from "lucide-react";
-import { useTranslations } from "next-intl";
+import { useTimeZone, useTranslations } from "next-intl";
 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
 import { cn } from "@/lib/utils";
 import type { ProviderEndpoint } from "@/types/provider";
@@ -47,9 +48,12 @@ function formatLatency(ms: number | null): string {
   return `${(ms / 1000).toFixed(2)}s`;
 }
 
-function formatTime(date: Date | string | null): string {
+function formatTime(date: Date | string | null, timeZone?: string): string {
   if (!date) return "-";
   const d = typeof date === "string" ? new Date(date) : date;
+  if (timeZone) {
+    return formatInTimeZone(d, timeZone, "HH:mm:ss");
+  }
   return d.toLocaleTimeString(undefined, {
     hour: "2-digit",
     minute: "2-digit",
@@ -64,6 +68,7 @@ export function ProbeGrid({
   className,
 }: ProbeGridProps) {
   const t = useTranslations("dashboard.availability.probeGrid");
+  const timeZone = useTimeZone() ?? "UTC";
 
   if (endpoints.length === 0) {
     return (
@@ -126,7 +131,7 @@ export function ProbeGrid({
                   <div className="flex items-center justify-between mt-2 pt-2 border-t border-border/30">
                     <span className="text-xs text-muted-foreground">{t("lastProbe")}</span>
                     <span className="text-xs font-mono text-muted-foreground">
-                      {formatTime(endpoint.lastProbedAt)}
+                      {formatTime(endpoint.lastProbedAt, timeZone)}
                     </span>
                   </div>
 

+ 10 - 5
src/app/[locale]/dashboard/availability/_components/endpoint/probe-terminal.tsx

@@ -1,7 +1,8 @@
 "use client";
 
+import { formatInTimeZone } from "date-fns-tz";
 import { AlertCircle, CheckCircle2, Download, Trash2, XCircle } from "lucide-react";
-import { useTranslations } from "next-intl";
+import { useTimeZone, useTranslations } from "next-intl";
 import { useEffect, useRef, useState } from "react";
 import { Button } from "@/components/ui/button";
 import { cn } from "@/lib/utils";
@@ -15,8 +16,11 @@ interface ProbeTerminalProps {
   className?: string;
 }
 
-function formatTime(date: Date | string): string {
+function formatTime(date: Date | string, timeZone?: string): string {
   const d = typeof date === "string" ? new Date(date) : date;
+  if (timeZone) {
+    return formatInTimeZone(d, timeZone, "HH:mm:ss");
+  }
   return d.toLocaleTimeString(undefined, {
     hour: "2-digit",
     minute: "2-digit",
@@ -68,6 +72,7 @@ export function ProbeTerminal({
   className,
 }: ProbeTerminalProps) {
   const t = useTranslations("dashboard.availability.terminal");
+  const timeZone = useTimeZone() ?? "UTC";
   const containerRef = useRef<HTMLDivElement>(null);
   const [userScrolled, setUserScrolled] = useState(false);
   const [filter, setFilter] = useState("");
@@ -103,7 +108,7 @@ export function ProbeTerminal({
   const handleDownload = () => {
     const content = filteredLogs
       .map((log) => {
-        const time = formatTime(log.createdAt);
+        const time = formatTime(log.createdAt, timeZone);
         const status = log.ok ? "OK" : "FAIL";
         const latency = formatLatency(log.latencyMs);
         const error = log.errorMessage || "";
@@ -199,7 +204,7 @@ export function ProbeTerminal({
               >
                 {/* Timestamp */}
                 <span className="text-muted-foreground opacity-60 w-20 shrink-0">
-                  [{formatTime(log.createdAt)}]
+                  [{formatTime(log.createdAt, timeZone)}]
                 </span>
 
                 {/* Status */}
@@ -257,7 +262,7 @@ export function ProbeTerminal({
         {/* Loading indicator */}
         {logs.length > 0 && (
           <div className="flex items-center gap-2 px-2 py-1 text-muted-foreground animate-pulse">
-            <span className="opacity-50">[{formatTime(new Date())}]</span>
+            <span className="opacity-50">[{formatTime(new Date(), timeZone)}]</span>
             <span>...</span>
           </div>
         )}

+ 18 - 18
src/app/[locale]/dashboard/availability/_components/provider/lane-chart.tsx

@@ -1,6 +1,7 @@
 "use client";
 
-import { useTranslations } from "next-intl";
+import { formatInTimeZone } from "date-fns-tz";
+import { useTimeZone, useTranslations } from "next-intl";
 import { useMemo } from "react";
 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
 import type { ProviderAvailabilitySummary, TimeBucketMetrics } from "@/lib/availability";
@@ -38,27 +39,19 @@ function getStatusColor(status: string): string {
   }
 }
 
-function formatBucketTime(isoString: string, bucketSizeMinutes: number): string {
+function formatBucketTime(isoString: string, bucketSizeMinutes: number, timeZone?: string): string {
   const date = new Date(isoString);
+  const tz = timeZone ?? "UTC";
   if (bucketSizeMinutes >= 1440) {
-    return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
+    return formatInTimeZone(date, tz, "MMM d");
   }
   if (bucketSizeMinutes >= 60) {
-    return date.toLocaleString(undefined, {
-      month: "short",
-      day: "numeric",
-      hour: "2-digit",
-      minute: "2-digit",
-    });
+    return formatInTimeZone(date, tz, "MMM d HH:mm");
   }
   if (bucketSizeMinutes < 1) {
-    return date.toLocaleTimeString(undefined, {
-      hour: "2-digit",
-      minute: "2-digit",
-      second: "2-digit",
-    });
+    return formatInTimeZone(date, tz, "HH:mm:ss");
   }
-  return date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
+  return formatInTimeZone(date, tz, "HH:mm");
 }
 
 function formatLatency(ms: number): string {
@@ -79,6 +72,7 @@ export function LaneChart({
   className,
 }: LaneChartProps) {
   const t = useTranslations("dashboard.availability.laneChart");
+  const timeZone = useTimeZone() ?? "UTC";
 
   // Generate unified time buckets
   const unifiedBuckets = useMemo(() => {
@@ -106,12 +100,12 @@ export function LaneChart({
     for (let i = 0; i < unifiedBuckets.length; i += step) {
       labels.push({
         position: (i / unifiedBuckets.length) * 100,
-        label: formatBucketTime(unifiedBuckets[i], bucketSizeMinutes),
+        label: formatBucketTime(unifiedBuckets[i], bucketSizeMinutes, timeZone),
       });
     }
 
     return labels;
-  }, [unifiedBuckets, bucketSizeMinutes]);
+  }, [unifiedBuckets, bucketSizeMinutes, timeZone]);
 
   const getBucketData = (
     provider: ProviderAvailabilitySummary,
@@ -215,6 +209,7 @@ export function LaneChart({
                                 bucketStart={bucketStart}
                                 bucket={bucket}
                                 bucketSizeMinutes={bucketSizeMinutes}
+                                timeZone={timeZone}
                               />
                             </TooltipContent>
                           </Tooltip>
@@ -255,6 +250,7 @@ export function LaneChart({
                                 bucketStart={bucketStart}
                                 bucket={bucket}
                                 bucketSizeMinutes={bucketSizeMinutes}
+                                timeZone={timeZone}
                               />
                             </TooltipContent>
                           </Tooltip>
@@ -301,17 +297,21 @@ function BucketTooltip({
   bucketStart,
   bucket,
   bucketSizeMinutes,
+  timeZone,
 }: {
   bucketStart: string;
   bucket: TimeBucketMetrics | null;
   bucketSizeMinutes: number;
+  timeZone: string;
 }) {
   const t = useTranslations("dashboard.availability.laneChart");
   const hasData = bucket !== null && bucket.totalRequests > 0;
 
   return (
     <div className="text-sm space-y-1">
-      <div className="font-medium">{formatBucketTime(bucketStart, bucketSizeMinutes)}</div>
+      <div className="font-medium">
+        {formatBucketTime(bucketStart, bucketSizeMinutes, timeZone)}
+      </div>
       {hasData && bucket ? (
         <>
           <div>{t("requests", { count: bucket.totalRequests })}</div>

+ 4 - 5
src/app/[locale]/dashboard/availability/_components/provider/latency-chart.tsx

@@ -1,6 +1,7 @@
 "use client";
 
-import { useTranslations } from "next-intl";
+import { formatInTimeZone } from "date-fns-tz";
+import { useTimeZone, useTranslations } from "next-intl";
 import { useMemo } from "react";
 import { Area, AreaChart, CartesianGrid, ResponsiveContainer, XAxis, YAxis } from "recharts";
 import {
@@ -34,6 +35,7 @@ const chartConfig = {
 
 export function LatencyChart({ providers, className }: LatencyChartProps) {
   const t = useTranslations("dashboard.availability.latencyChart");
+  const timeZone = useTimeZone() ?? "UTC";
 
   // Aggregate latency data across all providers
   const chartData = useMemo(() => {
@@ -85,10 +87,7 @@ export function LatencyChart({ providers, className }: LatencyChartProps) {
 
   const formatTime = (time: string) => {
     const date = new Date(time);
-    return date.toLocaleTimeString(undefined, {
-      hour: "2-digit",
-      minute: "2-digit",
-    });
+    return formatInTimeZone(date, timeZone, "HH:mm");
   };
 
   const formatLatency = (value: number) => {

+ 6 - 2
src/app/[locale]/dashboard/logs/_components/filters/time-filters.tsx

@@ -50,8 +50,12 @@ export function TimeFilters({ filters, onFiltersChange, serverTimeZone }: TimeFi
   const displayEndDate = useMemo(() => {
     if (!filters.endTime) return undefined;
     const inclusiveEndTime = inclusiveEndTimestampFromExclusive(filters.endTime);
-    return format(new Date(inclusiveEndTime), "yyyy-MM-dd");
-  }, [filters.endTime]);
+    const date = new Date(inclusiveEndTime);
+    if (serverTimeZone) {
+      return formatInTimeZone(date, serverTimeZone, "yyyy-MM-dd");
+    }
+    return format(date, "yyyy-MM-dd");
+  }, [filters.endTime, serverTimeZone]);
 
   const displayEndClock = useMemo(() => {
     if (!filters.endTime) return undefined;

+ 2 - 2
src/app/[locale]/dashboard/logs/_components/filters/types.ts

@@ -9,9 +9,9 @@ export interface UsageLogFilters {
   keyId?: number;
   providerId?: number;
   sessionId?: string;
-  /** Start timestamp (ms, local timezone 00:00:00) */
+  /** Start timestamp (ms, system timezone 00:00:00) */
   startTime?: number;
-  /** End timestamp (ms, local timezone next day 00:00:00, for < comparison) */
+  /** End timestamp (ms, system timezone next day 00:00:00, for < comparison) */
   endTime?: number;
   statusCode?: number;
   excludeStatusCode200?: boolean;

+ 3 - 3
src/app/[locale]/dashboard/logs/_components/usage-logs-sections.tsx

@@ -1,6 +1,6 @@
 import { cache } from "react";
 import { ActiveSessionsList } from "@/components/customs/active-sessions-list";
-import { getEnvConfig } from "@/lib/config";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import { getSystemSettings } from "@/repository/system-config";
 import { UsageLogsViewVirtualized } from "./usage-logs-view-virtualized";
 
@@ -29,14 +29,14 @@ export async function UsageLogsDataSection({
   searchParams,
 }: UsageLogsDataSectionProps) {
   const resolvedSearchParams = await searchParams;
-  const { TZ } = getEnvConfig();
+  const serverTimeZone = await resolveSystemTimezone();
 
   return (
     <UsageLogsViewVirtualized
       isAdmin={isAdmin}
       userId={userId}
       searchParams={resolvedSearchParams}
-      serverTimeZone={TZ}
+      serverTimeZone={serverTimeZone}
     />
   );
 }

+ 4 - 7
src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/request-list-sidebar.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { formatInTimeZone } from "date-fns-tz";
 import {
   AlertCircle,
   ArrowDownUp,
@@ -8,7 +9,7 @@ import {
   MoreHorizontal,
   Search,
 } from "lucide-react";
-import { useTranslations } from "next-intl";
+import { useTimeZone, useTranslations } from "next-intl";
 import { useCallback, useEffect, useState } from "react";
 import { getSessionRequests } from "@/actions/active-sessions";
 import { Badge } from "@/components/ui/badge";
@@ -45,6 +46,7 @@ export function RequestListSidebar({
   className,
 }: RequestListSidebarProps) {
   const t = useTranslations("dashboard.sessions");
+  const timeZone = useTimeZone() ?? "UTC";
   const [requests, setRequests] = useState<RequestItem[]>([]);
   const [total, setTotal] = useState(0);
   const [page, setPage] = useState(1);
@@ -85,12 +87,7 @@ export function RequestListSidebar({
   // Formatter functions
   const formatTime = (date: Date | null) => {
     if (!date) return "-";
-    return new Date(date).toLocaleTimeString(undefined, {
-      hour: "2-digit",
-      minute: "2-digit",
-      second: "2-digit",
-      hour12: false,
-    });
+    return formatInTimeZone(new Date(date), timeZone, "HH:mm:ss");
   };
 
   const getStatusColor = (statusCode: number | null) => {

+ 1 - 0
src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/session-messages-client-actions.test.tsx

@@ -18,6 +18,7 @@ vi.mock("next-intl", () => {
   const t = (key: string) => key;
   return {
     useTranslations: () => t,
+    useTimeZone: () => "UTC",
   };
 });
 

+ 16 - 6
src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/session-stats.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { formatInTimeZone } from "date-fns-tz";
 import {
   Calendar,
   Clock,
@@ -11,7 +12,7 @@ import {
   Server,
   Zap,
 } from "lucide-react";
-import { useTranslations } from "next-intl";
+import { useTimeZone, useTranslations } from "next-intl";
 import { Badge } from "@/components/ui/badge";
 import { Separator } from "@/components/ui/separator";
 import { cn } from "@/lib/utils";
@@ -38,6 +39,7 @@ interface SessionStatsProps {
 
 export function SessionStats({ stats, currencyCode = "USD", className }: SessionStatsProps) {
   const t = useTranslations("dashboard.sessions.details");
+  const timeZone = useTimeZone() ?? "UTC";
 
   const totalTokens =
     stats.totalInputTokens +
@@ -165,8 +167,8 @@ export function SessionStats({ stats, currencyCode = "USD", className }: Session
         </h4>
 
         <div className="space-y-3">
-          <TimeRow label={t("firstRequest")} date={stats.firstRequestAt} />
-          <TimeRow label={t("lastRequest")} date={stats.lastRequestAt} />
+          <TimeRow label={t("firstRequest")} date={stats.firstRequestAt} timeZone={timeZone} />
+          <TimeRow label={t("lastRequest")} date={stats.lastRequestAt} timeZone={timeZone} />
         </div>
       </div>
     </div>
@@ -217,15 +219,23 @@ function TokenRow({
   );
 }
 
-function TimeRow({ label, date }: { label: string; date: Date | null }) {
+function TimeRow({
+  label,
+  date,
+  timeZone,
+}: {
+  label: string;
+  date: Date | null;
+  timeZone: string;
+}) {
   if (!date) return null;
   const d = date instanceof Date ? date : new Date(date);
   return (
     <div className="flex flex-col gap-0.5">
       <span className="text-[10px] text-muted-foreground uppercase">{label}</span>
       <div className="flex items-center justify-between text-xs font-mono">
-        <span>{d.toLocaleDateString()}</span>
-        <span className="text-muted-foreground">{d.toLocaleTimeString()}</span>
+        <span>{formatInTimeZone(d, timeZone, "yyyy-MM-dd")}</span>
+        <span className="text-muted-foreground">{formatInTimeZone(d, timeZone, "HH:mm:ss")}</span>
       </div>
     </div>
   );

+ 5 - 1
src/app/[locale]/layout.tsx

@@ -7,6 +7,7 @@ import { Footer } from "@/components/customs/footer";
 import { Toaster } from "@/components/ui/sonner";
 import { type Locale, locales } from "@/i18n/config";
 import { logger } from "@/lib/logger";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import { getSystemSettings } from "@/repository/system-config";
 import { AppProviders } from "../providers";
 
@@ -70,11 +71,14 @@ export default async function RootLayout({
 
   // Load translation messages
   const messages = await getMessages();
+  const timeZone = await resolveSystemTimezone();
+  // Create a stable `now` timestamp to avoid SSR/CSR hydration mismatch for relative time
+  const now = new Date();
 
   return (
     <html lang={locale} suppressHydrationWarning>
       <body className="antialiased">
-        <NextIntlClientProvider messages={messages}>
+        <NextIntlClientProvider messages={messages} timeZone={timeZone} now={now}>
           <AppProviders>
             <div className="flex min-h-screen flex-col bg-background text-foreground">
               <main className="flex-1">{children}</main>

+ 4 - 1
src/app/[locale]/my-usage/_components/expiration-info.tsx

@@ -11,6 +11,8 @@ interface ExpirationInfoProps {
   userExpiresAt: Date | null;
   userRpmLimit?: number | null;
   className?: string;
+  /** IANA timezone for display (e.g., "Asia/Shanghai"). Falls back to local time when omitted. */
+  timezone?: string;
 }
 
 const SEVEN_DAYS_IN_SECONDS = 7 * 24 * 60 * 60;
@@ -23,6 +25,7 @@ export function ExpirationInfo({
   userExpiresAt,
   userRpmLimit,
   className,
+  timezone,
 }: ExpirationInfoProps) {
   const t = useTranslations("myUsage.expiration");
   const locale = useLocale();
@@ -32,7 +35,7 @@ export function ExpirationInfo({
 
   const formatExpiry = (value: Date | null) => {
     if (!value) return t("neverExpires");
-    const formatted = formatDate(value, getLocaleDateFormat(locale, "long"), locale);
+    const formatted = formatDate(value, getLocaleDateFormat(locale, "long"), locale, timezone);
     return formatted;
   };
 

+ 6 - 2
src/app/[locale]/my-usage/_components/usage-logs-table.tsx

@@ -1,6 +1,7 @@
 "use client";
 
-import { useTranslations } from "next-intl";
+import { formatInTimeZone } from "date-fns-tz";
+import { useTimeZone, useTranslations } from "next-intl";
 import type { MyUsageLogEntry } from "@/actions/my-usage";
 import { Badge } from "@/components/ui/badge";
 import { Skeleton } from "@/components/ui/skeleton";
@@ -37,6 +38,7 @@ export function UsageLogsTable({
   loadingLabel,
 }: UsageLogsTableProps) {
   const t = useTranslations("myUsage.logs");
+  const timeZone = useTimeZone() ?? "UTC";
   const totalPages = Math.max(1, Math.ceil(total / pageSize));
 
   const formatTokenAmount = (value: number | null | undefined): string => {
@@ -80,7 +82,9 @@ export function UsageLogsTable({
               logs.map((log) => (
                 <TableRow key={log.id}>
                   <TableCell className="whitespace-nowrap text-xs text-muted-foreground">
-                    {log.createdAt ? new Date(log.createdAt).toLocaleString() : "-"}
+                    {log.createdAt
+                      ? formatInTimeZone(new Date(log.createdAt), timeZone, "yyyy-MM-dd HH:mm:ss")
+                      : "-"}
                   </TableCell>
                   <TableCell className="space-y-1">
                     <div className="text-sm">{log.model ?? t("unknownModel")}</div>

+ 1 - 0
src/app/[locale]/my-usage/page.tsx

@@ -76,6 +76,7 @@ export default function MyUsagePage() {
             keyExpiresAt={keyExpiresAt}
             userExpiresAt={userExpiresAt}
             userRpmLimit={quota.userRpmLimit}
+            timezone={serverTimeZone}
           />
         </div>
       ) : null}

+ 34 - 0
src/app/[locale]/settings/config/_components/system-settings-form.tsx

@@ -5,6 +5,7 @@ import {
   Clock,
   Eye,
   FileCode,
+  Globe,
   Network,
   Pencil,
   Terminal,
@@ -30,6 +31,7 @@ import {
 import { Switch } from "@/components/ui/switch";
 import type { CurrencyCode } from "@/lib/utils";
 import { CURRENCY_CONFIG } from "@/lib/utils";
+import { COMMON_TIMEZONES, getTimezoneLabel } from "@/lib/utils/timezone";
 import type { BillingModelSource, SystemSettings } from "@/types/system-config";
 
 interface SystemSettingsFormProps {
@@ -39,6 +41,7 @@ interface SystemSettingsFormProps {
     | "allowGlobalUsageView"
     | "currencyDisplay"
     | "billingModelSource"
+    | "timezone"
     | "verboseProviderError"
     | "enableHttp2"
     | "interceptAnthropicWarmupRequests"
@@ -69,6 +72,7 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps)
   const [billingModelSource, setBillingModelSource] = useState<BillingModelSource>(
     initialSettings.billingModelSource
   );
+  const [timezone, setTimezone] = useState<string | null>(initialSettings.timezone);
   const [verboseProviderError, setVerboseProviderError] = useState(
     initialSettings.verboseProviderError
   );
@@ -122,6 +126,7 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps)
         allowGlobalUsageView,
         currencyDisplay,
         billingModelSource,
+        timezone,
         verboseProviderError,
         enableHttp2,
         interceptAnthropicWarmupRequests,
@@ -147,6 +152,7 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps)
         setAllowGlobalUsageView(result.data.allowGlobalUsageView);
         setCurrencyDisplay(result.data.currencyDisplay);
         setBillingModelSource(result.data.billingModelSource);
+        setTimezone(result.data.timezone);
         setVerboseProviderError(result.data.verboseProviderError);
         setEnableHttp2(result.data.enableHttp2);
         setInterceptAnthropicWarmupRequests(result.data.interceptAnthropicWarmupRequests);
@@ -241,6 +247,34 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps)
         <p className="text-xs text-muted-foreground">{t("billingModelSourceDesc")}</p>
       </div>
 
+      {/* Timezone Select */}
+      <div className="space-y-2">
+        <Label htmlFor="timezone" className="text-sm font-medium text-foreground">
+          <div className="flex items-center gap-2">
+            <Globe className="h-4 w-4" />
+            {t("timezoneLabel")}
+          </div>
+        </Label>
+        <Select
+          value={timezone ?? "__auto__"}
+          onValueChange={(value) => setTimezone(value === "__auto__" ? null : value)}
+          disabled={isPending}
+        >
+          <SelectTrigger id="timezone" className={selectTriggerClassName}>
+            <SelectValue />
+          </SelectTrigger>
+          <SelectContent>
+            <SelectItem value="__auto__">{t("timezoneAuto")}</SelectItem>
+            {COMMON_TIMEZONES.map((tz) => (
+              <SelectItem key={tz} value={tz}>
+                {getTimezoneLabel(tz)}
+              </SelectItem>
+            ))}
+          </SelectContent>
+        </Select>
+        <p className="text-xs text-muted-foreground">{t("timezoneDescription")}</p>
+      </div>
+
       {/* Toggle Settings */}
       <div className="space-y-3">
         {/* Allow Global Usage View */}

+ 1 - 0
src/app/[locale]/settings/config/page.tsx

@@ -44,6 +44,7 @@ async function SettingsConfigContent() {
             allowGlobalUsageView: settings.allowGlobalUsageView,
             currencyDisplay: settings.currencyDisplay,
             billingModelSource: settings.billingModelSource,
+            timezone: settings.timezone,
             verboseProviderError: settings.verboseProviderError,
             enableHttp2: settings.enableHttp2,
             interceptAnthropicWarmupRequests: settings.interceptAnthropicWarmupRequests,

+ 4 - 2
src/app/[locale]/settings/error-rules/_components/rule-list-table.tsx

@@ -1,7 +1,8 @@
 "use client";
 
+import { formatInTimeZone } from "date-fns-tz";
 import { AlertTriangle, Pencil, Trash2 } from "lucide-react";
-import { useTranslations } from "next-intl";
+import { useTimeZone, useTranslations } from "next-intl";
 import { useState } from "react";
 import { toast } from "sonner";
 import { deleteErrorRuleAction, updateErrorRuleAction } from "@/actions/error-rules";
@@ -29,6 +30,7 @@ const categoryColors: Record<string, { bg: string; text: string }> = {
 
 export function RuleListTable({ rules }: RuleListTableProps) {
   const t = useTranslations("settings");
+  const timeZone = useTimeZone() ?? "UTC";
   const [selectedRule, setSelectedRule] = useState<ErrorRule | null>(null);
   const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
 
@@ -140,7 +142,7 @@ export function RuleListTable({ rules }: RuleListTableProps) {
                     </p>
                   )}
                   <p className="text-[10px] text-muted-foreground/60 mt-1">
-                    {new Date(rule.createdAt).toLocaleString("zh-CN")}
+                    {formatInTimeZone(new Date(rule.createdAt), timeZone, "yyyy-MM-dd HH:mm:ss")}
                   </p>
                 </div>
               </div>

+ 6 - 5
src/app/[locale]/settings/notifications/_components/webhook-target-card.tsx

@@ -1,7 +1,8 @@
 "use client";
 
+import { formatInTimeZone } from "date-fns-tz";
 import { ExternalLink, MoreHorizontal, Pencil, Trash2 } from "lucide-react";
-import { useLocale, useTranslations } from "next-intl";
+import { useTimeZone, useTranslations } from "next-intl";
 import { useMemo, useState } from "react";
 import {
   AlertDialog,
@@ -36,12 +37,12 @@ interface WebhookTargetCardProps {
   onTest: (id: number, type: NotificationType) => Promise<void> | void;
 }
 
-function formatLastTest(target: WebhookTargetState, locale: string): string | null {
+function formatLastTest(target: WebhookTargetState, timeZone: string): string | null {
   if (!target.lastTestAt) return null;
   try {
     const date =
       typeof target.lastTestAt === "string" ? new Date(target.lastTestAt) : target.lastTestAt;
-    return date.toLocaleString(locale, { hour12: false });
+    return formatInTimeZone(date, timeZone, "yyyy-MM-dd HH:mm:ss");
   } catch {
     return null;
   }
@@ -55,7 +56,7 @@ export function WebhookTargetCard({
   onTest,
 }: WebhookTargetCardProps) {
   const t = useTranslations("settings");
-  const locale = useLocale();
+  const timeZone = useTimeZone() ?? "UTC";
   const [isDeleting, setIsDeleting] = useState(false);
   const [showDeleteDialog, setShowDeleteDialog] = useState(false);
 
@@ -63,7 +64,7 @@ export function WebhookTargetCard({
     return t(`notifications.targetDialog.types.${target.providerType}` as any);
   }, [t, target.providerType]);
 
-  const lastTestText = useMemo(() => formatLastTest(target, locale), [target, locale]);
+  const lastTestText = useMemo(() => formatLastTest(target, timeZone), [target, timeZone]);
   const lastTestOk = target.lastTestResult?.success;
   const lastTestLatency = target.lastTestResult?.latencyMs;
 

+ 15 - 5
src/app/[locale]/settings/prices/_components/price-list.tsx

@@ -1,6 +1,7 @@
 "use client";
 
 import { Claude, Gemini, OpenAI } from "@lobehub/icons";
+import { formatInTimeZone } from "date-fns-tz";
 import {
   Braces,
   ChevronLeft,
@@ -19,7 +20,7 @@ import {
   Terminal,
   Trash2,
 } from "lucide-react";
-import { useLocale, useTranslations } from "next-intl";
+import { useLocale, useTimeZone, useTranslations } from "next-intl";
 import { useCallback, useEffect, useRef, useState } from "react";
 import { toast } from "sonner";
 import { Badge } from "@/components/ui/badge";
@@ -70,6 +71,7 @@ export function PriceList({
   const t = useTranslations("settings.prices");
   const tCommon = useTranslations("common");
   const locale = useLocale();
+  const timeZone = useTimeZone() ?? "UTC";
   const [searchTerm, setSearchTerm] = useState(initialSearchTerm);
   const [sourceFilter, setSourceFilter] = useState<ModelPriceSource | "">(initialSourceFilter);
   const [litellmProviderFilter, setLitellmProviderFilter] = useState(initialLitellmProviderFilter);
@@ -612,7 +614,11 @@ export function PriceList({
                     )}
                   </td>
                   <td className="py-3 px-4 text-sm text-muted-foreground">
-                    {new Date(price.updatedAt ?? price.createdAt).toLocaleDateString(locale)}
+                    {formatInTimeZone(
+                      new Date(price.updatedAt ?? price.createdAt),
+                      timeZone,
+                      "yyyy-MM-dd"
+                    )}
                   </td>
                   <td className="py-3 px-4">
                     <DropdownMenu>
@@ -771,9 +777,13 @@ export function PriceList({
           {t("stats.lastUpdated", {
             time:
               prices.length > 0
-                ? new Date(
-                    Math.max(...prices.map((p) => new Date(p.updatedAt ?? p.createdAt).getTime()))
-                  ).toLocaleDateString(locale)
+                ? formatInTimeZone(
+                    new Date(
+                      Math.max(...prices.map((p) => new Date(p.updatedAt ?? p.createdAt).getTime()))
+                    ),
+                    timeZone,
+                    "yyyy-MM-dd"
+                  )
                 : "-",
           })}
         </div>

+ 6 - 3
src/app/[locale]/settings/providers/_components/forms/test-result-card.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { formatInTimeZone } from "date-fns-tz";
 import {
   AlertTriangle,
   CheckCircle2,
@@ -10,7 +11,7 @@ import {
   XCircle,
   Zap,
 } from "lucide-react";
-import { useTranslations } from "next-intl";
+import { useTimeZone, useTranslations } from "next-intl";
 import { useState } from "react";
 import { toast } from "sonner";
 import { Badge } from "@/components/ui/badge";
@@ -98,6 +99,7 @@ const STATUS_ICONS: Record<TestStatus, React.ReactNode> = {
  */
 export function TestResultCard({ result }: TestResultCardProps) {
   const t = useTranslations("settings.providers.form.apiTest");
+  const timeZone = useTimeZone() ?? "UTC";
   const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false);
 
   const colors = STATUS_COLORS[result.status];
@@ -132,7 +134,7 @@ export function TestResultCard({ result }: TestResultCardProps) {
       result.content &&
         `${ct("response")}: ${result.content.slice(0, 200)}${result.content.length > 200 ? "..." : ""}`,
       result.errorMessage && `${ct("error")}: ${result.errorMessage}`,
-      `${ct("testedAt")}: ${new Date(result.testedAt).toLocaleString()}`,
+      `${ct("testedAt")}: ${formatInTimeZone(new Date(result.testedAt), timeZone, "yyyy-MM-dd HH:mm:ss")}`,
       "",
       `${ct("validationDetails")}:`,
       `  ${ct("httpCheck")}: ${vp(result.validationDetails.httpPassed, "http")}`,
@@ -294,6 +296,7 @@ function TestResultDetails({
   onCopy: () => void;
 }) {
   const t = useTranslations("settings.providers.form.apiTest");
+  const timeZone = useTimeZone() ?? "UTC";
 
   return (
     <div className="space-y-6 mt-4">
@@ -374,7 +377,7 @@ function TestResultDetails({
           )}
           <div>
             <span className="text-muted-foreground">{t("resultCard.timing.testedAt")}:</span>{" "}
-            <span className="font-mono">{new Date(result.testedAt).toLocaleString()}</span>
+            <span className="font-mono">{formatInTimeZone(new Date(result.testedAt), timeZone, "yyyy-MM-dd HH:mm:ss")}</span>
           </div>
         </div>
       </div>

+ 4 - 8
src/app/[locale]/settings/providers/_components/provider-list-item.legacy.tsx

@@ -1,7 +1,8 @@
 "use client";
+import { formatInTimeZone } from "date-fns-tz";
 import { CheckCircle, Copy, Edit, Globe, Key, RotateCcw } from "lucide-react";
 import { useRouter } from "next/navigation";
-import { useTranslations } from "next-intl";
+import { useTimeZone, useTranslations } from "next-intl";
 import { useEffect, useState, useTransition } from "react";
 import { toast } from "sonner";
 import { getUnmaskedProviderKey, resetProviderCircuit } from "@/actions/providers";
@@ -62,6 +63,7 @@ export function ProviderListItem({
   enableMultiProviderTypes,
 }: ProviderListItemProps) {
   const router = useRouter();
+  const timeZone = useTimeZone() ?? "UTC";
   const [openEdit, setOpenEdit] = useState(false);
   const [openClone, setOpenClone] = useState(false);
   const [showKeyDialog, setShowKeyDialog] = useState(false);
@@ -351,13 +353,7 @@ export function ProviderListItem({
           <span className="font-medium text-foreground/80">最近调用:</span>
           <span className="tabular-nums">
             {item.lastCallTime
-              ? new Date(item.lastCallTime).toLocaleString("zh-CN", {
-                  year: "numeric",
-                  month: "2-digit",
-                  day: "2-digit",
-                  hour: "2-digit",
-                  minute: "2-digit",
-                })
+              ? formatInTimeZone(new Date(item.lastCallTime), timeZone, "yyyy-MM-dd HH:mm")
               : "-"}
             {item.lastCallModel && item.lastCallTime ? ` - ${item.lastCallModel}` : ""}
           </span>

+ 4 - 2
src/app/[locale]/settings/sensitive-words/_components/word-list-table.tsx

@@ -1,7 +1,8 @@
 "use client";
 
+import { formatInTimeZone } from "date-fns-tz";
 import { Pencil, Trash2 } from "lucide-react";
-import { useTranslations } from "next-intl";
+import { useTimeZone, useTranslations } from "next-intl";
 import { useState } from "react";
 import { toast } from "sonner";
 import { deleteSensitiveWordAction, updateSensitiveWordAction } from "@/actions/sensitive-words";
@@ -23,6 +24,7 @@ const matchTypeColors = {
 
 export function WordListTable({ words }: WordListTableProps) {
   const t = useTranslations("settings");
+  const timeZone = useTimeZone() ?? "UTC";
   const [selectedWord, setSelectedWord] = useState<SensitiveWord | null>(null);
   const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
 
@@ -122,7 +124,7 @@ export function WordListTable({ words }: WordListTableProps) {
                   />
                 </td>
                 <td className="py-3 px-4 text-sm text-muted-foreground">
-                  {new Date(word.createdAt).toLocaleString("zh-CN")}
+                  {formatInTimeZone(new Date(word.createdAt), timeZone, "yyyy-MM-dd HH:mm:ss")}
                 </td>
                 <td className="py-3 px-4 text-right">
                   <div className="flex justify-end gap-1">

+ 14 - 6
src/app/v1/_lib/proxy/rate-limit-guard.ts

@@ -325,7 +325,11 @@ export class ProxyRateLimitGuard {
         );
       } else {
         // fixed 模式:有固定重置时间
-        const resetInfo = getResetInfoWithMode("daily", key.dailyResetTime, key.dailyResetMode);
+        const resetInfo = await getResetInfoWithMode(
+          "daily",
+          key.dailyResetTime,
+          key.dailyResetMode
+        );
         const resetTime =
           resetInfo.resetAt?.toISOString() ??
           new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
@@ -390,7 +394,11 @@ export class ProxyRateLimitGuard {
           );
         } else {
           // fixed 模式:有固定重置时间
-          const resetInfo = getResetInfoWithMode("daily", user.dailyResetTime, user.dailyResetMode);
+          const resetInfo = await getResetInfoWithMode(
+            "daily",
+            user.dailyResetTime,
+            user.dailyResetMode
+          );
           const resetTime =
             resetInfo.resetAt?.toISOString() ??
             new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
@@ -432,7 +440,7 @@ export class ProxyRateLimitGuard {
       logger.warn(`[RateLimit] Key weekly limit exceeded: key=${key.id}, ${keyWeeklyCheck.reason}`);
 
       const { currentUsage, limitValue } = parseLimitInfo(keyWeeklyCheck.reason!);
-      const resetInfo = getResetInfo("weekly");
+      const resetInfo = await getResetInfo("weekly");
       const resetTime = resetInfo.resetAt?.toISOString() || new Date().toISOString();
 
       const { getLocale } = await import("next-intl/server");
@@ -468,7 +476,7 @@ export class ProxyRateLimitGuard {
       );
 
       const { currentUsage, limitValue } = parseLimitInfo(userWeeklyCheck.reason!);
-      const resetInfo = getResetInfo("weekly");
+      const resetInfo = await getResetInfo("weekly");
       const resetTime = resetInfo.resetAt?.toISOString() || new Date().toISOString();
 
       const { getLocale } = await import("next-intl/server");
@@ -504,7 +512,7 @@ export class ProxyRateLimitGuard {
       );
 
       const { currentUsage, limitValue } = parseLimitInfo(keyMonthlyCheck.reason!);
-      const resetInfo = getResetInfo("monthly");
+      const resetInfo = await getResetInfo("monthly");
       const resetTime = resetInfo.resetAt?.toISOString() || new Date().toISOString();
 
       const { getLocale } = await import("next-intl/server");
@@ -540,7 +548,7 @@ export class ProxyRateLimitGuard {
       );
 
       const { currentUsage, limitValue } = parseLimitInfo(userMonthlyCheck.reason!);
-      const resetInfo = getResetInfo("monthly");
+      const resetInfo = await getResetInfo("monthly");
       const resetTime = resetInfo.resetAt?.toISOString() || new Date().toISOString();
 
       const { getLocale } = await import("next-intl/server");

+ 4 - 2
src/components/customs/version-checker.tsx

@@ -1,7 +1,8 @@
 "use client";
 
+import { formatInTimeZone } from "date-fns-tz";
 import { ExternalLink, RefreshCw } from "lucide-react";
-import { useTranslations } from "next-intl";
+import { useTimeZone, useTranslations } from "next-intl";
 import { useCallback, useEffect, useState } from "react";
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
@@ -18,6 +19,7 @@ interface VersionInfo {
 
 export function VersionChecker() {
   const t = useTranslations("customs");
+  const timeZone = useTimeZone() ?? "UTC";
   const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null);
   const [loading, setLoading] = useState(true);
 
@@ -93,7 +95,7 @@ export function VersionChecker() {
                 {versionInfo.publishedAt && (
                   <p className="mt-1 text-xs text-muted-foreground">
                     {t("version.publishedAt")}{" "}
-                    {new Date(versionInfo.publishedAt).toLocaleDateString("zh-CN")}
+                    {formatInTimeZone(new Date(versionInfo.publishedAt), timeZone, "yyyy-MM-dd")}
                   </p>
                 )}
               </div>

+ 2 - 1
src/components/ui/data-table.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { formatInTimeZone } from "date-fns-tz";
 import { useTranslations } from "next-intl";
 import type { ReactNode } from "react";
 import {
@@ -238,7 +239,7 @@ export const TableColumnTypes = {
     title,
     render: (value) => {
       if (!value) return "-";
-      return new Date(value).toLocaleDateString();
+      return formatInTimeZone(new Date(value), "UTC", "yyyy-MM-dd");
     },
     ...options,
   }),

+ 6 - 4
src/components/ui/relative-time.tsx

@@ -1,7 +1,8 @@
 "use client";
 
 import { format as formatDate } from "date-fns";
-import { useLocale, useTranslations } from "next-intl";
+import { formatInTimeZone } from "date-fns-tz";
+import { useLocale, useTimeZone, useTranslations } from "next-intl";
 import { useCallback, useEffect, useMemo, useState } from "react";
 import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
 import { formatDateDistance } from "@/lib/utils/date-format";
@@ -35,6 +36,7 @@ export function RelativeTime({
   const [timeAgo, setTimeAgo] = useState<string>(fallback);
   const [mounted, setMounted] = useState(false);
   const locale = useLocale();
+  const timeZone = useTimeZone() ?? "UTC";
   const tShort = useTranslations("common.relativeTimeShort");
 
   // Format short distance with i18n
@@ -70,10 +72,10 @@ export function RelativeTime({
     if (!date) return fallback;
     const dateObj = typeof date === "string" ? new Date(date) : date;
     if (Number.isNaN(dateObj.getTime())) return fallback;
-    // date-fns does not fully support `z` for IANA abbreviations; use `OOOO` to show GMT offset.
+    // Use system timezone from next-intl for consistent display.
     // Example output: 2024-05-01 13:45:12 GMT+08:00
-    return formatDate(dateObj, "yyyy-MM-dd HH:mm:ss OOOO");
-  }, [date, fallback]);
+    return formatInTimeZone(dateObj, timeZone, "yyyy-MM-dd HH:mm:ss OOOO");
+  }, [date, fallback, timeZone]);
 
   useEffect(() => {
     // 如果 date 为 null,直接显示 fallback

+ 8 - 2
src/drizzle/schema.ts

@@ -94,7 +94,7 @@ export const keys = pgTable('keys', {
   key: varchar('key').notNull(),
   name: varchar('name').notNull(),
   isEnabled: boolean('is_enabled').default(true),
-  expiresAt: timestamp('expires_at'),
+  expiresAt: timestamp('expires_at', { withTimezone: true }),
 
   // Web UI 登录权限控制
   canLoginWebUi: boolean('can_login_web_ui').default(false),
@@ -563,6 +563,11 @@ export const systemSettings = pgTable('system_settings', {
   // 计费模型来源配置: 'original' (重定向前) | 'redirected' (重定向后)
   billingModelSource: varchar('billing_model_source', { length: 20 }).notNull().default('original'),
 
+  // 系统时区配置 (IANA timezone identifier)
+  // 用于统一后端时间边界计算和前端日期/时间显示
+  // null 表示使用环境变量 TZ 或默认 UTC
+  timezone: varchar('timezone', { length: 64 }),
+
   // 日志清理配置
   enableAutoCleanup: boolean('enable_auto_cleanup').default(false),
   cleanupRetentionDays: integer('cleanup_retention_days').default(30),
@@ -695,8 +700,9 @@ export const notificationTargetBindings = pgTable(
     isEnabled: boolean('is_enabled').notNull().default(true),
 
     // 定时配置覆盖(可选,仅用于定时类通知)
+    // null 表示使用系统时区(由运行时 resolveSystemTimezone() 决定)
     scheduleCron: varchar('schedule_cron', { length: 100 }),
-    scheduleTimezone: varchar('schedule_timezone', { length: 50 }).default('Asia/Shanghai'),
+    scheduleTimezone: varchar('schedule_timezone', { length: 50 }),
 
     // 模板覆盖(可选,主要用于 custom webhook)
     templateOverride: jsonb('template_override'),

+ 5 - 12
src/i18n/request.ts

@@ -4,6 +4,7 @@
  */
 
 import { getRequestConfig } from "next-intl/server";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import type { Locale } from "./config";
 import { routing } from "./routing";
 
@@ -21,21 +22,13 @@ export default getRequestConfig(async ({ requestLocale }) => {
   // The `settings` namespace is composed by `messages/<locale>/settings/index.ts` so key paths stay stable.
   const messages = await import(`../../messages/${locale}`).then((module) => module.default);
 
+  const timeZone = await resolveSystemTimezone();
+
   return {
     locale,
     messages,
-    // Optional: Configure date/time/number formatting
-    // formats: {
-    //   dateTime: {
-    //     short: {
-    //       day: 'numeric',
-    //       month: 'short',
-    //       year: 'numeric'
-    //     }
-    //   }
-    // },
-    // Optional: Configure time zone
-    // timeZone: 'Asia/Shanghai',
+    timeZone,
+    now: new Date(),
     // Optional: Enable runtime warnings for missing translations in development
     onError:
       process.env.NODE_ENV === "development"

+ 1 - 0
src/lib/config/system-settings-cache.ts

@@ -96,6 +96,7 @@ export async function getCachedSystemSettings(): Promise<SystemSettings> {
       allowGlobalUsageView: false,
       currencyDisplay: "USD",
       billingModelSource: "original",
+      timezone: null,
       verboseProviderError: false,
       enableAutoCleanup: false,
       cleanupRetentionDays: 30,

+ 19 - 4
src/lib/notification/notification-queue.ts

@@ -2,6 +2,7 @@ import type { Job } from "bull";
 import Queue from "bull";
 import type { NotificationJobType } from "@/lib/constants/notification.constants";
 import { logger } from "@/lib/logger";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import {
   buildCircuitBreakerMessage,
   buildCostAlertMessage,
@@ -137,13 +138,25 @@ function setupQueueProcessor(queue: Queue.Queue<NotificationJobData>): void {
     });
 
     try {
+      // Resolve timezone for formatting
+      // Priority: binding's scheduleTimezone > system timezone
+      let timezone: string | undefined;
+      if (bindingId) {
+        const { getBindingById } = await import("@/repository/notification-bindings");
+        const binding = await getBindingById(bindingId);
+        timezone = binding?.scheduleTimezone ?? undefined;
+      }
+      if (!timezone) {
+        timezone = await resolveSystemTimezone();
+      }
+
       // 构建结构化消息
       let message: StructuredMessage;
       let templateData: CircuitBreakerAlertData | DailyLeaderboardData | CostAlertData | undefined =
         data;
       switch (type) {
         case "circuit-breaker":
-          message = buildCircuitBreakerMessage(data as CircuitBreakerAlertData);
+          message = buildCircuitBreakerMessage(data as CircuitBreakerAlertData, timezone);
           break;
         case "daily-leaderboard": {
           // 动态生成排行榜数据
@@ -193,7 +206,7 @@ function setupQueueProcessor(queue: Queue.Queue<NotificationJobData>): void {
       // 发送通知
       let result;
       if (webhookUrl) {
-        result = await sendWebhookMessage(webhookUrl, message);
+        result = await sendWebhookMessage(webhookUrl, message, { timezone });
       } else if (targetId) {
         const { getWebhookTargetById } = await import("@/repository/webhook-targets");
         const target = await getWebhookTargetById(targetId);
@@ -221,6 +234,7 @@ function setupQueueProcessor(queue: Queue.Queue<NotificationJobData>): void {
           notificationType,
           data: templateData,
           templateOverride,
+          timezone,
         });
       } else {
         throw new Error("Missing notification destination (webhookUrl/targetId)");
@@ -411,6 +425,7 @@ export async function scheduleNotifications() {
     } else {
       // 新模式:按绑定调度(支持 cron 覆盖)
       const { getEnabledBindingsByType } = await import("@/repository/notification-bindings");
+      const systemTimezone = await resolveSystemTimezone();
 
       if (settings.dailyLeaderboardEnabled) {
         const bindings = await getEnabledBindingsByType("daily_leaderboard");
@@ -419,7 +434,7 @@ export async function scheduleNotifications() {
 
         for (const binding of bindings) {
           const cron = binding.scheduleCron ?? defaultCron;
-          const tz = binding.scheduleTimezone ?? "Asia/Shanghai";
+          const tz = binding.scheduleTimezone ?? systemTimezone;
 
           await queue.add(
             {
@@ -449,7 +464,7 @@ export async function scheduleNotifications() {
 
         for (const binding of bindings) {
           const cron = binding.scheduleCron ?? defaultCron;
-          const tz = binding.scheduleTimezone ?? "Asia/Shanghai";
+          const tz = binding.scheduleTimezone ?? systemTimezone;
 
           await queue.add(
             {

+ 4 - 2
src/lib/notification/tasks/daily-leaderboard.ts

@@ -1,4 +1,5 @@
 import { logger } from "@/lib/logger";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import type { DailyLeaderboardData } from "@/lib/webhook";
 import { findLast24HoursLeaderboard } from "@/repository/leaderboard";
 
@@ -29,11 +30,12 @@ export async function generateDailyLeaderboard(topN: number): Promise<DailyLeade
     const totalRequests = leaderboard.reduce((sum, entry) => sum + entry.totalRequests, 0);
     const totalCost = leaderboard.reduce((sum, entry) => sum + entry.totalCost, 0);
 
-    // 格式化日期 (YYYY-MM-DD)
+    // 格式化日期 (YYYY-MM-DD) 使用系统时区
     const today = new Date();
+    const timezone = await resolveSystemTimezone();
     const dateStr = today
       .toLocaleDateString("zh-CN", {
-        timeZone: "Asia/Shanghai",
+        timeZone: timezone,
         year: "numeric",
         month: "2-digit",
         day: "2-digit",

+ 1 - 1
src/lib/rate-limit/lease-service.ts

@@ -163,7 +163,7 @@ export class LeaseService {
       const percent = LeaseService.getLeasePercent(window, leasePercentConfig);
 
       // Calculate time range for DB query
-      const { startTime, endTime } = getLeaseTimeRange(window, resetTime, resetMode);
+      const { startTime, endTime } = await getLeaseTimeRange(window, resetTime, resetMode);
 
       // Query DB for current usage
       const currentUsage = await LeaseService.queryDbUsage(

+ 4 - 4
src/lib/rate-limit/lease.ts

@@ -63,11 +63,11 @@ export function buildLeaseKey(
  * Get time range for a lease window
  * Delegates to time-utils for consistent behavior
  */
-export function getLeaseTimeRange(
+export async function getLeaseTimeRange(
   window: LeaseWindowType,
   resetTime = "00:00",
   mode: DailyResetMode = "fixed"
-): { startTime: Date; endTime: Date } {
+): Promise<{ startTime: Date; endTime: Date }> {
   return getTimeRangeForPeriodWithMode(window as TimePeriod, resetTime, mode);
 }
 
@@ -75,11 +75,11 @@ export function getLeaseTimeRange(
  * Get TTL in seconds for a lease window
  * Delegates to time-utils for consistent behavior
  */
-export function getLeaseTtlSeconds(
+export async function getLeaseTtlSeconds(
   window: LeaseWindowType,
   resetTime = "00:00",
   mode: DailyResetMode = "fixed"
-): number {
+): Promise<number> {
   return getTTLForPeriodWithMode(window as TimePeriod, resetTime, mode);
 }
 

+ 20 - 12
src/lib/rate-limit/service.ts

@@ -397,7 +397,7 @@ export class RateLimitService {
       if (!limit.amount || limit.amount <= 0) continue;
 
       // 计算时间范围(使用支持模式的时间工具函数)
-      const { startTime, endTime } = getTimeRangeForPeriodWithMode(
+      const { startTime, endTime } = await getTimeRangeForPeriodWithMode(
         limit.period,
         limit.resetTime,
         limit.resetMode
@@ -470,7 +470,7 @@ export class RateLimitService {
           } else {
             // daily fixed/周/月固定窗口:使用 STRING + 动态 TTL
             const { normalized, suffix } = RateLimitService.resolveDailyReset(limit.resetTime);
-            const ttl = getTTLForPeriodWithMode(limit.period, normalized, limit.resetMode);
+            const ttl = await getTTLForPeriodWithMode(limit.period, normalized, limit.resetMode);
             const periodKey = limit.period === "daily" ? `${limit.period}_${suffix}` : limit.period;
             await RateLimitService.redis.set(
               `${type}:${id}:cost_${periodKey}`,
@@ -629,14 +629,22 @@ export class RateLimitService {
       const window24h = 24 * 60 * 60 * 1000; // 24 hours in ms
 
       // 计算动态 TTL(daily/周/月)
-      const ttlDailyKey = getTTLForPeriodWithMode("daily", keyDailyReset.normalized, keyDailyMode);
+      const ttlDailyKey = await getTTLForPeriodWithMode(
+        "daily",
+        keyDailyReset.normalized,
+        keyDailyMode
+      );
       const ttlDailyProvider =
         keyDailyReset.normalized === providerDailyReset.normalized &&
         keyDailyMode === providerDailyMode
           ? ttlDailyKey
-          : getTTLForPeriodWithMode("daily", providerDailyReset.normalized, providerDailyMode);
-      const ttlWeekly = getTTLForPeriod("weekly");
-      const ttlMonthly = getTTLForPeriod("monthly");
+          : await getTTLForPeriodWithMode(
+              "daily",
+              providerDailyReset.normalized,
+              providerDailyMode
+            );
+      const ttlWeekly = await getTTLForPeriod("weekly");
+      const ttlMonthly = await getTTLForPeriod("monthly");
 
       // 1. 5h 滚动窗口:使用 Lua 脚本(ZSET)
       // Key 的 5h 滚动窗口
@@ -827,7 +835,7 @@ export class RateLimitService {
         sumProviderCostInTimeRange,
       } = await import("@/repository/statistics");
 
-      const { startTime, endTime } = getTimeRangeForPeriodWithMode(
+      const { startTime, endTime } = await getTimeRangeForPeriodWithMode(
         period,
         dailyResetInfo.normalized,
         resetMode
@@ -891,7 +899,7 @@ export class RateLimitService {
           } else {
             // daily fixed/周/月固定窗口:使用 STRING + 动态 TTL
             const redisKey = period === "daily" ? `${period}_${dailyResetInfo.suffix}` : period;
-            const ttl = getTTLForPeriodWithMode(period, dailyResetInfo.normalized, resetMode);
+            const ttl = await getTTLForPeriodWithMode(period, dailyResetInfo.normalized, resetMode);
             await RateLimitService.redis.set(
               `${type}:${id}:cost_${redisKey}`,
               current.toString(),
@@ -1062,7 +1070,7 @@ export class RateLimitService {
           } else {
             // Cache Miss: 从数据库恢复
             logger.info(`[RateLimit] Cache miss for ${key}, querying database`);
-            const { startTime, endTime } = getTimeRangeForPeriodWithMode(
+            const { startTime, endTime } = await getTimeRangeForPeriodWithMode(
               "daily",
               normalizedResetTime,
               mode
@@ -1070,14 +1078,14 @@ export class RateLimitService {
             currentCost = await sumUserCostInTimeRange(userId, startTime, endTime);
 
             // Cache Warming: 写回 Redis
-            const ttl = getTTLForPeriodWithMode("daily", normalizedResetTime, "fixed");
+            const ttl = await getTTLForPeriodWithMode("daily", normalizedResetTime, "fixed");
             await RateLimitService.redis.set(key, currentCost.toString(), "EX", ttl);
           }
         }
       } else {
         // Slow Path: 数据库查询(Redis 不可用)
         logger.warn("[RateLimit] Redis unavailable, querying database for user daily cost");
-        const { startTime, endTime } = getTimeRangeForPeriodWithMode(
+        const { startTime, endTime } = await getTimeRangeForPeriodWithMode(
           "daily",
           normalizedResetTime,
           mode
@@ -1141,7 +1149,7 @@ export class RateLimitService {
         // Fixed 模式:使用 STRING 类型
         const suffix = normalizedResetTime.replace(":", "");
         const key = `user:${userId}:cost_daily_${suffix}`;
-        const ttl = getTTLForPeriodWithMode("daily", normalizedResetTime, "fixed");
+        const ttl = await getTTLForPeriodWithMode("daily", normalizedResetTime, "fixed");
 
         await RateLimitService.redis.pipeline().incrbyfloat(key, cost).expire(key, ttl).exec();
 

+ 24 - 21
src/lib/rate-limit/time-utils.ts

@@ -15,7 +15,7 @@ import {
   startOfWeek,
 } from "date-fns";
 import { fromZonedTime, toZonedTime } from "date-fns-tz";
-import { getEnvConfig } from "@/lib/config";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 
 export type TimePeriod = "5h" | "daily" | "weekly" | "monthly";
 export type DailyResetMode = "fixed" | "rolling";
@@ -38,10 +38,13 @@ export interface ResetInfo {
  * - weekly: 自然周(本周一 00:00 到现在)
  * - monthly: 自然月(本月 1 号 00:00 到现在)
  *
- * 所有自然时间窗口使用配置的时区(Asia/Shanghai
+ * 所有自然时间窗口使用系统配置时区(通过 resolveSystemTimezone 获取
  */
-export function getTimeRangeForPeriod(period: TimePeriod, resetTime = "00:00"): TimeRange {
-  const timezone = getEnvConfig().TZ; // 'Asia/Shanghai'
+export async function getTimeRangeForPeriod(
+  period: TimePeriod,
+  resetTime = "00:00"
+): Promise<TimeRange> {
+  const timezone = await resolveSystemTimezone();
   const normalizedResetTime = normalizeResetTime(resetTime);
   const now = new Date();
   const endTime = now;
@@ -60,7 +63,7 @@ export function getTimeRangeForPeriod(period: TimePeriod, resetTime = "00:00"):
     }
 
     case "weekly": {
-      // 自然周:本周一 00:00 (Asia/Shanghai)
+      // 自然周:本周一 00:00 (系统时区)
       const zonedNow = toZonedTime(now, timezone);
       const zonedStartOfWeek = startOfWeek(zonedNow, { weekStartsOn: 1 }); // 周一
       startTime = fromZonedTime(zonedStartOfWeek, timezone);
@@ -68,7 +71,7 @@ export function getTimeRangeForPeriod(period: TimePeriod, resetTime = "00:00"):
     }
 
     case "monthly": {
-      // 自然月:本月 1 号 00:00 (Asia/Shanghai)
+      // 自然月:本月 1 号 00:00 (系统时区)
       const zonedNow = toZonedTime(now, timezone);
       const zonedStartOfMonth = startOfMonth(zonedNow);
       startTime = fromZonedTime(zonedStartOfMonth, timezone);
@@ -85,11 +88,11 @@ export function getTimeRangeForPeriod(period: TimePeriod, resetTime = "00:00"):
  * - daily + fixed: 固定时间重置(使用 resetTime)
  * - 其他周期:使用原有逻辑
  */
-export function getTimeRangeForPeriodWithMode(
+export async function getTimeRangeForPeriodWithMode(
   period: TimePeriod,
   resetTime = "00:00",
   mode: DailyResetMode = "fixed"
-): TimeRange {
+): Promise<TimeRange> {
   if (period === "daily" && mode === "rolling") {
     // 滚动窗口:过去 24 小时
     const now = new Date();
@@ -110,8 +113,8 @@ export function getTimeRangeForPeriodWithMode(
  * - weekly: 到下周一 00:00 的秒数
  * - monthly: 到下月 1 号 00:00 的秒数
  */
-export function getTTLForPeriod(period: TimePeriod, resetTime = "00:00"): number {
-  const timezone = getEnvConfig().TZ;
+export async function getTTLForPeriod(period: TimePeriod, resetTime = "00:00"): Promise<number> {
+  const timezone = await resolveSystemTimezone();
   const now = new Date();
   const normalizedResetTime = normalizeResetTime(resetTime);
 
@@ -152,11 +155,11 @@ export function getTTLForPeriod(period: TimePeriod, resetTime = "00:00"): number
  * - daily + fixed: 到下一个自定义重置时间的秒数
  * - 其他周期:使用原有逻辑
  */
-export function getTTLForPeriodWithMode(
+export async function getTTLForPeriodWithMode(
   period: TimePeriod,
   resetTime = "00:00",
   mode: DailyResetMode = "fixed"
-): number {
+): Promise<number> {
   if (period === "daily" && mode === "rolling") {
     return 24 * 3600; // 24 小时
   }
@@ -167,8 +170,8 @@ export function getTTLForPeriodWithMode(
 /**
  * 获取重置信息(用于前端展示)
  */
-export function getResetInfo(period: TimePeriod, resetTime = "00:00"): ResetInfo {
-  const timezone = getEnvConfig().TZ;
+export async function getResetInfo(period: TimePeriod, resetTime = "00:00"): Promise<ResetInfo> {
+  const timezone = await resolveSystemTimezone();
   const now = new Date();
   const normalizedResetTime = normalizeResetTime(resetTime);
 
@@ -216,11 +219,11 @@ export function getResetInfo(period: TimePeriod, resetTime = "00:00"): ResetInfo
 /**
  * 获取重置信息(支持滚动窗口模式)
  */
-export function getResetInfoWithMode(
+export async function getResetInfoWithMode(
   period: TimePeriod,
   resetTime = "00:00",
   mode: DailyResetMode = "fixed"
-): ResetInfo {
+): Promise<ResetInfo> {
   if (period === "daily" && mode === "rolling") {
     return {
       type: "rolling",
@@ -290,10 +293,10 @@ export function normalizeResetTime(resetTime?: string): string {
 
 /**
  * 计算距离午夜的秒数(用于每日限额)
- * 使用配置时区(Asia/Shanghai)而非服务器本地时区
+ * 使用系统配置时区而非服务器本地时区
  */
-export function getSecondsUntilMidnight(): number {
-  const timezone = getEnvConfig().TZ;
+export async function getSecondsUntilMidnight(): Promise<number> {
+  const timezone = await resolveSystemTimezone();
   const now = new Date();
   const zonedNow = toZonedTime(now, timezone);
   const zonedTomorrow = addDays(zonedNow, 1);
@@ -316,8 +319,8 @@ export function getSecondsUntilMidnight(): number {
 /**
  * 获取每日限额的重置时间
  */
-export function getDailyResetTime(): Date {
-  const timezone = getEnvConfig().TZ;
+export async function getDailyResetTime(): Promise<Date> {
+  const timezone = await resolveSystemTimezone();
   const now = new Date();
   const zonedNow = toZonedTime(now, timezone);
   const zonedTomorrow = addDays(zonedNow, 1);

+ 12 - 7
src/lib/redis/leaderboard-cache.ts

@@ -1,6 +1,6 @@
 import { formatInTimeZone } from "date-fns-tz";
-import { getEnvConfig } from "@/lib/config";
 import { logger } from "@/lib/logger";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import {
   type DateRangeParams,
   findAllTimeLeaderboard,
@@ -50,16 +50,17 @@ export interface LeaderboardFilters {
 
 /**
  * 构建缓存键
+ * @param timezone - 已解析的系统时区(调用者应使用 resolveSystemTimezone() 获取)
  */
 function buildCacheKey(
   period: LeaderboardPeriod,
   currencyDisplay: string,
+  timezone: string,
   scope: LeaderboardScope = "user",
   dateRange?: DateRangeParams,
   filters?: LeaderboardFilters
 ): string {
   const now = new Date();
-  const tz = getEnvConfig().TZ; // ensure date formatting aligns with configured timezone
   const providerTypeSuffix = filters?.providerType ? `:providerType:${filters.providerType}` : "";
 
   let userFilterSuffix = "";
@@ -78,15 +79,15 @@ function buildCacheKey(
     return `leaderboard:${scope}:custom:${dateRange.startDate}_${dateRange.endDate}:${currencyDisplay}${providerTypeSuffix}${userFilterSuffix}`;
   } else if (period === "daily") {
     // leaderboard:{scope}:daily:2025-01-15:USD
-    const dateStr = formatInTimeZone(now, tz, "yyyy-MM-dd");
+    const dateStr = formatInTimeZone(now, timezone, "yyyy-MM-dd");
     return `leaderboard:${scope}:daily:${dateStr}:${currencyDisplay}${providerTypeSuffix}${userFilterSuffix}`;
   } else if (period === "weekly") {
     // leaderboard:{scope}:weekly:2025-W03:USD (ISO week)
-    const weekStr = formatInTimeZone(now, tz, "yyyy-'W'ww");
+    const weekStr = formatInTimeZone(now, timezone, "yyyy-'W'ww");
     return `leaderboard:${scope}:weekly:${weekStr}:${currencyDisplay}${providerTypeSuffix}${userFilterSuffix}`;
   } else if (period === "monthly") {
     // leaderboard:{scope}:monthly:2025-01:USD
-    const monthStr = formatInTimeZone(now, tz, "yyyy-MM");
+    const monthStr = formatInTimeZone(now, timezone, "yyyy-MM");
     return `leaderboard:${scope}:monthly:${monthStr}:${currencyDisplay}${providerTypeSuffix}${userFilterSuffix}`;
   } else {
     // allTime: leaderboard:{scope}:allTime:USD (no date component)
@@ -221,7 +222,9 @@ export async function getLeaderboardWithCache(
     return await queryDatabase(period, scope, dateRange, filters);
   }
 
-  const cacheKey = buildCacheKey(period, currencyDisplay, scope, dateRange, filters);
+  // Resolve timezone once per request
+  const timezone = await resolveSystemTimezone();
+  const cacheKey = buildCacheKey(period, currencyDisplay, timezone, scope, dateRange, filters);
   const lockKey = `${cacheKey}:lock`;
 
   try {
@@ -306,7 +309,9 @@ export async function invalidateLeaderboardCache(
     return;
   }
 
-  const cacheKey = buildCacheKey(period, currencyDisplay, scope);
+  // Resolve timezone once per request
+  const timezone = await resolveSystemTimezone();
+  const cacheKey = buildCacheKey(period, currencyDisplay, timezone, scope);
 
   try {
     await redis.del(cacheKey);

+ 18 - 3
src/lib/utils/date-format.ts

@@ -1,11 +1,13 @@
 /**
  * Date Formatting Utilities with Locale Support
- * Provides locale-aware date formatting using date-fns and next-intl
+ * Provides locale-aware date formatting using date-fns and next-intl.
+ * Supports optional IANA timezone via date-fns-tz for timezone-aware display.
  */
 
 import type { Locale } from "date-fns";
 import { format, formatDistance, formatRelative } from "date-fns";
 import { enUS, ja, ru, zhCN, zhTW } from "date-fns/locale";
+import { formatInTimeZone } from "date-fns-tz";
 
 /**
  * Map next-intl locale codes to date-fns locale objects
@@ -28,20 +30,33 @@ export function getDateFnsLocale(locale: string): Locale {
 }
 
 /**
- * Format date with locale support
+ * Format date with locale support and optional timezone.
+ *
+ * When a valid IANA timezone is provided (e.g., "Asia/Shanghai"), the date is
+ * rendered in that timezone using `formatInTimeZone` from date-fns-tz.
+ * Otherwise, the browser/server local timezone is used (original behaviour).
+ *
  * @param date - Date to format
  * @param formatString - Format string (e.g., "yyyy-MM-dd", "PPP")
  * @param locale - next-intl locale code
+ * @param timezone - Optional IANA timezone identifier (e.g., "America/New_York")
  * @returns Formatted date string
  */
 export function formatDate(
   date: Date | number | string,
   formatString: string,
-  locale: string = "zh-CN"
+  locale: string = "zh-CN",
+  timezone?: string
 ): string {
   const dateObj = typeof date === "string" ? new Date(date) : date;
   const dateFnsLocale = getDateFnsLocale(locale);
 
+  if (timezone) {
+    return formatInTimeZone(dateObj, timezone, formatString, {
+      locale: dateFnsLocale,
+    });
+  }
+
   return format(dateObj, formatString, { locale: dateFnsLocale });
 }
 

+ 52 - 0
src/lib/utils/date-input.ts

@@ -0,0 +1,52 @@
+import { parse } from "date-fns";
+import { fromZonedTime } from "date-fns-tz";
+
+/**
+ * Parse a date string input and interpret it in the specified timezone.
+ *
+ * - Date-only (YYYY-MM-DD): Interprets as end-of-day (23:59:59) in timezone
+ * - Full datetime: Interprets as local time in timezone
+ *
+ * Returns a UTC Date that represents the correct instant.
+ *
+ * @param input - Date string in YYYY-MM-DD or ISO datetime format
+ * @param timezone - IANA timezone identifier (e.g., "Asia/Shanghai", "America/New_York")
+ * @returns Date object in UTC representing the input interpreted in the given timezone
+ * @throws Error if input is invalid
+ *
+ * @example
+ * // "2024-12-31" in Asia/Shanghai becomes 2024-12-31 23:59:59 Shanghai = 2024-12-31 15:59:59 UTC
+ * parseDateInputAsTimezone("2024-12-31", "Asia/Shanghai")
+ *
+ * @example
+ * // "2024-12-31T10:30:00" in Asia/Shanghai becomes 2024-12-31 10:30:00 Shanghai = 2024-12-31 02:30:00 UTC
+ * parseDateInputAsTimezone("2024-12-31T10:30:00", "Asia/Shanghai")
+ */
+export function parseDateInputAsTimezone(input: string, timezone: string): Date {
+  if (!input) {
+    throw new Error("Invalid date input: empty string");
+  }
+
+  // Date-only format: YYYY-MM-DD
+  if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
+    // Parse as end-of-day (23:59:59) in the given timezone
+    const localDateTime = parse(`${input} 23:59:59`, "yyyy-MM-dd HH:mm:ss", new Date());
+
+    if (Number.isNaN(localDateTime.getTime())) {
+      throw new Error(`Invalid date input: ${input}`);
+    }
+
+    // Convert from timezone local time to UTC
+    return fromZonedTime(localDateTime, timezone);
+  }
+
+  // ISO datetime or other formats: parse and treat as timezone local time
+  const localDate = new Date(input);
+
+  if (Number.isNaN(localDate.getTime())) {
+    throw new Error(`Invalid date input: ${input}`);
+  }
+
+  // Convert from timezone local time to UTC
+  return fromZonedTime(localDate, timezone);
+}

+ 14 - 4
src/lib/utils/date.ts

@@ -1,3 +1,5 @@
+import { formatInTimeZone } from "date-fns-tz";
+
 /**
  * Format a date to relative time (e.g., "2 hours ago")
  */
@@ -34,9 +36,13 @@ export function formatRelativeTime(date: Date): string {
 }
 
 /**
- * Format a date to string
+ * Format a date to string.
+ * When a timezone is provided, uses formatInTimeZone for consistent display.
  */
-export function formatDate(date: Date, locale = "zh-CN"): string {
+export function formatDate(date: Date, locale = "zh-CN", timezone?: string): string {
+  if (timezone) {
+    return formatInTimeZone(date, timezone, "yyyy-MM-dd");
+  }
   return date.toLocaleDateString(locale, {
     year: "numeric",
     month: "2-digit",
@@ -45,9 +51,13 @@ export function formatDate(date: Date, locale = "zh-CN"): string {
 }
 
 /**
- * Format a date to datetime string
+ * Format a date to datetime string.
+ * When a timezone is provided, uses formatInTimeZone for consistent display.
  */
-export function formatDateTime(date: Date, locale = "zh-CN"): string {
+export function formatDateTime(date: Date, locale = "zh-CN", timezone?: string): string {
+  if (timezone) {
+    return formatInTimeZone(date, timezone, "yyyy-MM-dd HH:mm:ss");
+  }
   return date.toLocaleString(locale, {
     year: "numeric",
     month: "2-digit",

+ 170 - 0
src/lib/utils/timezone.ts

@@ -0,0 +1,170 @@
+/**
+ * Timezone Utilities
+ *
+ * Provides timezone validation and resolution functions.
+ * Uses IANA timezone database identifiers (e.g., "Asia/Shanghai", "America/New_York").
+ *
+ * resolveSystemTimezone() implements the fallback chain:
+ *   DB timezone -> env TZ -> UTC
+ */
+
+import { getEnvConfig } from "@/lib/config/env.schema";
+import { getCachedSystemSettings } from "@/lib/config/system-settings-cache";
+import { logger } from "@/lib/logger";
+
+/**
+ * Common IANA timezone identifiers for dropdown UI.
+ * Organized by region for better UX.
+ */
+export const COMMON_TIMEZONES = [
+  // UTC
+  "UTC",
+  // Asia
+  "Asia/Shanghai",
+  "Asia/Tokyo",
+  "Asia/Seoul",
+  "Asia/Singapore",
+  "Asia/Hong_Kong",
+  "Asia/Taipei",
+  "Asia/Bangkok",
+  "Asia/Dubai",
+  "Asia/Kolkata",
+  // Europe
+  "Europe/London",
+  "Europe/Paris",
+  "Europe/Berlin",
+  "Europe/Moscow",
+  "Europe/Amsterdam",
+  "Europe/Rome",
+  "Europe/Madrid",
+  // Americas
+  "America/New_York",
+  "America/Los_Angeles",
+  "America/Chicago",
+  "America/Denver",
+  "America/Toronto",
+  "America/Vancouver",
+  "America/Sao_Paulo",
+  "America/Mexico_City",
+  // Pacific
+  "Pacific/Auckland",
+  "Pacific/Sydney",
+  "Australia/Melbourne",
+  "Australia/Perth",
+] as const;
+
+export type CommonTimezone = (typeof COMMON_TIMEZONES)[number];
+
+/**
+ * Validates if a string is a valid IANA timezone identifier.
+ *
+ * Uses the Intl.DateTimeFormat API which is based on the IANA timezone database.
+ * This approach is more reliable than maintaining a static list.
+ *
+ * @param timezone - The timezone string to validate
+ * @returns true if the timezone is valid, false otherwise
+ *
+ * @example
+ * isValidIANATimezone("Asia/Shanghai") // true
+ * isValidIANATimezone("America/New_York") // true
+ * isValidIANATimezone("UTC") // true
+ * isValidIANATimezone("Invalid/Zone") // false
+ * isValidIANATimezone("CST") // false (abbreviations not valid)
+ */
+export function isValidIANATimezone(timezone: string): boolean {
+  if (!timezone || typeof timezone !== "string") {
+    return false;
+  }
+
+  try {
+    // Intl.DateTimeFormat will throw if the timezone is invalid
+    Intl.DateTimeFormat(undefined, { timeZone: timezone });
+    return true;
+  } catch {
+    return false;
+  }
+}
+
+/**
+ * Gets the display label for a timezone.
+ * Returns the offset and common name for UI display.
+ *
+ * @param timezone - IANA timezone identifier
+ * @param locale - Locale for formatting (default: "en")
+ * @returns Display label like "(UTC+08:00) Asia/Shanghai"
+ */
+export function getTimezoneLabel(timezone: string, locale = "en"): string {
+  if (!isValidIANATimezone(timezone)) {
+    return timezone;
+  }
+
+  try {
+    const now = new Date();
+    const formatter = new Intl.DateTimeFormat(locale, {
+      timeZone: timezone,
+      timeZoneName: "longOffset",
+    });
+
+    const parts = formatter.formatToParts(now);
+    const offsetPart = parts.find((p) => p.type === "timeZoneName");
+    const offset = offsetPart?.value || "";
+
+    // Format: "(UTC+08:00) Asia/Shanghai" or "(GMT+08:00) Asia/Shanghai"
+    return `(${offset}) ${timezone}`;
+  } catch {
+    return timezone;
+  }
+}
+
+/**
+ * Gets the current UTC offset in minutes for a timezone.
+ *
+ * @param timezone - IANA timezone identifier
+ * @returns Offset in minutes (positive = ahead of UTC, negative = behind)
+ */
+export function getTimezoneOffsetMinutes(timezone: string): number {
+  if (!isValidIANATimezone(timezone)) {
+    return 0;
+  }
+
+  const now = new Date();
+  const utcDate = new Date(now.toLocaleString("en-US", { timeZone: "UTC" }));
+  const tzDate = new Date(now.toLocaleString("en-US", { timeZone: timezone }));
+
+  return (tzDate.getTime() - utcDate.getTime()) / (1000 * 60);
+}
+
+/**
+ * Resolves the system timezone using the fallback chain:
+ *   1. DB system_settings.timezone (via cached settings)
+ *   2. env TZ variable
+ *   3. "UTC" as final fallback
+ *
+ * Each candidate is validated via isValidIANATimezone before being accepted.
+ *
+ * @returns Resolved IANA timezone identifier (always valid)
+ */
+export async function resolveSystemTimezone(): Promise<string> {
+  // Step 1: Try DB timezone from cached system settings
+  try {
+    const settings = await getCachedSystemSettings();
+    if (settings.timezone && isValidIANATimezone(settings.timezone)) {
+      return settings.timezone;
+    }
+  } catch (error) {
+    logger.warn("[TimezoneResolver] Failed to read cached system settings", { error });
+  }
+
+  // Step 2: Fallback to env TZ
+  try {
+    const { TZ } = getEnvConfig();
+    if (TZ && isValidIANATimezone(TZ)) {
+      return TZ;
+    }
+  } catch (error) {
+    logger.warn("[TimezoneResolver] Failed to read env TZ", { error });
+  }
+
+  // Step 3: Ultimate fallback
+  return "UTC";
+}

+ 10 - 0
src/lib/validation/schemas.ts

@@ -6,6 +6,7 @@ import {
 } from "@/lib/constants/provider.constants";
 import { USER_LIMITS } from "@/lib/constants/user.constants";
 import { CURRENCY_CONFIG } from "@/lib/utils/currency";
+import { isValidIANATimezone } from "@/lib/utils/timezone";
 
 const CACHE_TTL_PREFERENCE = z.enum(["inherit", "5m", "1h"]);
 const CONTEXT_1M_PREFERENCE = z.enum(["inherit", "force_enable", "disabled"]);
@@ -728,6 +729,15 @@ export const UpdateSystemSettingsSchema = z.object({
   billingModelSource: z
     .enum(["original", "redirected"], { message: "不支持的计费模型来源" })
     .optional(),
+  // 系统时区配置(可选)
+  // 必须是有效的 IANA 时区标识符(如 "Asia/Shanghai", "America/New_York")
+  timezone: z
+    .string()
+    .refine((val) => isValidIANATimezone(val), {
+      message: "无效的时区标识符,请使用 IANA 时区格式(如 Asia/Shanghai)",
+    })
+    .nullable()
+    .optional(),
   // 日志清理配置(可选)
   enableAutoCleanup: z.boolean().optional(),
   cleanupRetentionDays: z.coerce

+ 1 - 0
src/lib/webhook/renderers/custom.ts

@@ -18,6 +18,7 @@ export class CustomRenderer implements Renderer {
       message,
       notificationType: options?.notificationType,
       data: options?.data,
+      timezone: options?.timezone,
     });
 
     const bodyObject = this.interpolate(template, variables);

+ 5 - 4
src/lib/webhook/renderers/dingtalk.ts

@@ -4,24 +4,25 @@ import type {
   SectionContent,
   StructuredMessage,
   WebhookPayload,
+  WebhookSendOptions,
 } from "../types";
 import { formatTimestamp } from "../utils/date";
 import type { Renderer } from "./index";
 
 export class DingTalkRenderer implements Renderer {
-  render(message: StructuredMessage): WebhookPayload {
+  render(message: StructuredMessage, options?: WebhookSendOptions): WebhookPayload {
     const markdown = {
       msgtype: "markdown",
       markdown: {
         title: this.escapeText(message.header.title),
-        text: this.buildMarkdown(message),
+        text: this.buildMarkdown(message, options?.timezone),
       },
     };
 
     return { body: JSON.stringify(markdown) };
   }
 
-  private buildMarkdown(message: StructuredMessage): string {
+  private buildMarkdown(message: StructuredMessage, timezone?: string): string {
     const lines: string[] = [];
 
     lines.push(`### ${this.escapeText(message.header.title)}`);
@@ -40,7 +41,7 @@ export class DingTalkRenderer implements Renderer {
       lines.push("");
     }
 
-    lines.push(formatTimestamp(message.timestamp));
+    lines.push(formatTimestamp(message.timestamp, timezone || "UTC"));
     return lines.join("\n").trim();
   }
 

+ 3 - 2
src/lib/webhook/renderers/feishu.ts

@@ -5,6 +5,7 @@ import type {
   SectionContent,
   StructuredMessage,
   WebhookPayload,
+  WebhookSendOptions,
 } from "../types";
 import { formatDateTime } from "../utils/date";
 import type { Renderer } from "./index";
@@ -15,7 +16,7 @@ interface CardElement {
 }
 
 export class FeishuCardRenderer implements Renderer {
-  render(message: StructuredMessage): WebhookPayload {
+  render(message: StructuredMessage, options?: WebhookSendOptions): WebhookPayload {
     const elements: CardElement[] = [];
 
     // Sections
@@ -34,7 +35,7 @@ export class FeishuCardRenderer implements Renderer {
     // Timestamp
     elements.push({
       tag: "markdown",
-      content: formatDateTime(message.timestamp),
+      content: formatDateTime(message.timestamp, options?.timezone || "UTC"),
       text_size: "notation",
     });
 

+ 5 - 4
src/lib/webhook/renderers/telegram.ts

@@ -4,6 +4,7 @@ import type {
   SectionContent,
   StructuredMessage,
   WebhookPayload,
+  WebhookSendOptions,
 } from "../types";
 import { formatTimestamp } from "../utils/date";
 import type { Renderer } from "./index";
@@ -11,8 +12,8 @@ import type { Renderer } from "./index";
 export class TelegramRenderer implements Renderer {
   constructor(private readonly chatId: string) {}
 
-  render(message: StructuredMessage): WebhookPayload {
-    const html = this.buildHtml(message);
+  render(message: StructuredMessage, options?: WebhookSendOptions): WebhookPayload {
+    const html = this.buildHtml(message, options?.timezone);
     return {
       body: JSON.stringify({
         chat_id: this.chatId,
@@ -23,7 +24,7 @@ export class TelegramRenderer implements Renderer {
     };
   }
 
-  private buildHtml(message: StructuredMessage): string {
+  private buildHtml(message: StructuredMessage, timezone?: string): string {
     const lines: string[] = [];
 
     lines.push(`<b>${this.escapeHtml(message.header.title)}</b>`);
@@ -42,7 +43,7 @@ export class TelegramRenderer implements Renderer {
       lines.push("");
     }
 
-    lines.push(this.escapeHtml(formatTimestamp(message.timestamp)));
+    lines.push(this.escapeHtml(formatTimestamp(message.timestamp, timezone || "UTC")));
     return lines.join("\n").trim();
   }
 

+ 3 - 2
src/lib/webhook/renderers/wechat.ts

@@ -4,12 +4,13 @@ import type {
   SectionContent,
   StructuredMessage,
   WebhookPayload,
+  WebhookSendOptions,
 } from "../types";
 import { formatTimestamp } from "../utils/date";
 import type { Renderer } from "./index";
 
 export class WeChatRenderer implements Renderer {
-  render(message: StructuredMessage): WebhookPayload {
+  render(message: StructuredMessage, options?: WebhookSendOptions): WebhookPayload {
     const lines: string[] = [];
 
     // Header
@@ -32,7 +33,7 @@ export class WeChatRenderer implements Renderer {
     }
 
     // Timestamp
-    lines.push(formatTimestamp(message.timestamp));
+    lines.push(formatTimestamp(message.timestamp, options?.timezone || "UTC"));
 
     const content = lines.join("\n");
 

+ 5 - 2
src/lib/webhook/templates/circuit-breaker.ts

@@ -1,10 +1,13 @@
 import type { CircuitBreakerAlertData, StructuredMessage } from "../types";
 import { formatDateTime } from "../utils/date";
 
-export function buildCircuitBreakerMessage(data: CircuitBreakerAlertData): StructuredMessage {
+export function buildCircuitBreakerMessage(
+  data: CircuitBreakerAlertData,
+  timezone?: string
+): StructuredMessage {
   const fields = [
     { label: "失败次数", value: `${data.failureCount} 次` },
-    { label: "预计恢复", value: formatDateTime(data.retryAt) },
+    { label: "预计恢复", value: formatDateTime(data.retryAt, timezone || "UTC") },
   ];
 
   if (data.lastError) {

+ 6 - 5
src/lib/webhook/templates/placeholders.ts

@@ -26,7 +26,7 @@ export const TEMPLATE_PLACEHOLDERS = {
     {
       key: "{{timestamp_local}}",
       label: "本地时间",
-      description: "本地格式化时间(Asia/Shanghai)",
+      description: "本地格式化时间(系统时区)",
     },
     { key: "{{title}}", label: "消息标题", description: "通知标题" },
     { key: "{{level}}", label: "消息级别", description: "info / warning / error" },
@@ -70,14 +70,15 @@ export function buildTemplateVariables(params: {
   message: StructuredMessage;
   notificationType?: WebhookNotificationType;
   data?: unknown;
+  timezone?: string;
 }): Record<string, string> {
-  const { message, notificationType, data } = params;
+  const { message, notificationType, data, timezone } = params;
 
   const values: Record<string, string> = {};
 
   // 通用字段
   values["{{timestamp}}"] = message.timestamp.toISOString();
-  values["{{timestamp_local}}"] = formatLocalTimestamp(message.timestamp);
+  values["{{timestamp_local}}"] = formatLocalTimestamp(message.timestamp, timezone);
   values["{{title}}"] = message.header.title;
   values["{{level}}"] = message.header.level;
   values["{{sections}}"] = renderMessageSections(message);
@@ -129,9 +130,9 @@ function safeJsonStringify(value: unknown): string {
   }
 }
 
-function formatLocalTimestamp(date: Date): string {
+function formatLocalTimestamp(date: Date, timezone?: string): string {
   return date.toLocaleString("zh-CN", {
-    timeZone: "Asia/Shanghai",
+    timeZone: timezone || "UTC",
     year: "numeric",
     month: "2-digit",
     day: "2-digit",

+ 14 - 8
src/lib/webhook/templates/test-messages.ts

@@ -7,17 +7,23 @@ import { buildDailyLeaderboardMessage } from "./daily-leaderboard";
 /**
  * 根据通知类型构建测试消息
  * 使用模拟数据,完整展示真实消息格式
+ *
+ * @param type - Notification job type
+ * @param timezone - IANA timezone identifier for date/time formatting (optional, defaults to UTC)
  */
-export function buildTestMessage(type: NotificationJobType): StructuredMessage {
+export function buildTestMessage(type: NotificationJobType, timezone?: string): StructuredMessage {
   switch (type) {
     case "circuit-breaker":
-      return buildCircuitBreakerMessage({
-        providerName: "测试供应商",
-        providerId: 0,
-        failureCount: 3,
-        retryAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
-        lastError: "Connection timeout (示例错误)",
-      });
+      return buildCircuitBreakerMessage(
+        {
+          providerName: "测试供应商",
+          providerId: 0,
+          failureCount: 3,
+          retryAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
+          lastError: "Connection timeout (示例错误)",
+        },
+        timezone
+      );
 
     case "cost-alert":
       return buildCostAlertMessage({

+ 2 - 0
src/lib/webhook/types.ts

@@ -103,6 +103,8 @@ export interface WebhookSendOptions {
   notificationType?: WebhookNotificationType;
   data?: unknown;
   templateOverride?: Record<string, unknown> | null;
+  /** IANA timezone identifier for date/time formatting */
+  timezone?: string;
 }
 
 export interface WebhookPayload {

+ 18 - 14
src/lib/webhook/utils/date.ts

@@ -1,20 +1,24 @@
+import { formatInTimeZone } from "date-fns-tz";
+
 /**
- * 格式化日期时间为中国时区的本地化字符串
+ * Format date-time for webhook messages.
+ * Uses ISO-like format (yyyy/MM/dd HH:mm:ss) for consistency across locales.
+ *
+ * @param date - Date object or ISO string to format
+ * @param timezone - IANA timezone identifier (required, use resolveSystemTimezone() for system default)
+ * @returns Formatted datetime string in the specified timezone
+ *
+ * @example
+ * formatDateTime(new Date(), "Asia/Shanghai") // "2024/01/15 14:30:00"
  */
-export function formatDateTime(date: Date | string): string {
+export function formatDateTime(date: Date | string, timezone: string): string {
   const d = typeof date === "string" ? new Date(date) : date;
-  return d.toLocaleString("zh-CN", {
-    timeZone: "Asia/Shanghai",
-    year: "numeric",
-    month: "2-digit",
-    day: "2-digit",
-    hour: "2-digit",
-    minute: "2-digit",
-    second: "2-digit",
-    hour12: false,
-  });
+  return formatInTimeZone(d, timezone, "yyyy/MM/dd HH:mm:ss");
 }
 
-export function formatTimestamp(date: Date): string {
-  return formatDateTime(date);
+/**
+ * Alias for formatDateTime for backward compatibility
+ */
+export function formatTimestamp(date: Date, timezone: string): string {
+  return formatDateTime(date, timezone);
 }

+ 1 - 0
src/repository/_shared/transformers.test.ts

@@ -277,6 +277,7 @@ describe("src/repository/_shared/transformers.ts", () => {
       expect(result.allowGlobalUsageView).toBe(true);
       expect(result.currencyDisplay).toBe("USD");
       expect(result.billingModelSource).toBe("original");
+      expect(result.timezone).toBeNull();
       expect(result.enableAutoCleanup).toBe(false);
       expect(result.cleanupRetentionDays).toBe(30);
       expect(result.cleanupSchedule).toBe("0 2 * * *");

+ 1 - 0
src/repository/_shared/transformers.ts

@@ -179,6 +179,7 @@ export function toSystemSettings(dbSettings: any): SystemSettings {
     allowGlobalUsageView: dbSettings?.allowGlobalUsageView ?? true,
     currencyDisplay: dbSettings?.currencyDisplay ?? "USD",
     billingModelSource: dbSettings?.billingModelSource ?? "original",
+    timezone: dbSettings?.timezone ?? null,
     enableAutoCleanup: dbSettings?.enableAutoCleanup ?? false,
     cleanupRetentionDays: dbSettings?.cleanupRetentionDays ?? 30,
     cleanupSchedule: dbSettings?.cleanupSchedule ?? "0 2 * * *",

+ 29 - 29
src/repository/leaderboard.ts

@@ -3,7 +3,7 @@
 import { and, desc, eq, isNull, sql } from "drizzle-orm";
 import { db } from "@/drizzle/db";
 import { messageRequest, providers, users } from "@/drizzle/schema";
-import { getEnvConfig } from "@/lib/config";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import type { ProviderType } from "@/types/provider";
 import { EXCLUDE_WARMUP_CONDITION } from "./_shared/message-request-conditions";
 import { getSystemSettings } from "./system-config";
@@ -73,34 +73,34 @@ export interface ModelLeaderboardEntry {
 
 /**
  * 查询今日消耗排行榜(不限制数量)
- * 使用 SQL AT TIME ZONE 进行时区转换,确保"今日"基于配置时区(Asia/Shanghai)
+ * 使用 SQL AT TIME ZONE 进行时区转换,确保"今日"基于系统时区
  */
 export async function findDailyLeaderboard(
   userFilters?: UserLeaderboardFilters
 ): Promise<LeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findLeaderboardWithTimezone("daily", timezone, undefined, userFilters);
 }
 
 /**
  * 查询本月消耗排行榜(不限制数量)
- * 使用 SQL AT TIME ZONE 进行时区转换,确保"本月"基于配置时区(Asia/Shanghai)
+ * 使用 SQL AT TIME ZONE 进行时区转换,确保"本月"基于系统时区
  */
 export async function findMonthlyLeaderboard(
   userFilters?: UserLeaderboardFilters
 ): Promise<LeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findLeaderboardWithTimezone("monthly", timezone, undefined, userFilters);
 }
 
 /**
  * 查询本周消耗排行榜(不限制数量)
- * 使用 SQL AT TIME ZONE 进行时区转换,确保"本周"基于配置时区
+ * 使用 SQL AT TIME ZONE 进行时区转换,确保"本周"基于系统时区
  */
 export async function findWeeklyLeaderboard(
   userFilters?: UserLeaderboardFilters
 ): Promise<LeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findLeaderboardWithTimezone("weekly", timezone, undefined, userFilters);
 }
 
@@ -110,7 +110,7 @@ export async function findWeeklyLeaderboard(
 export async function findAllTimeLeaderboard(
   userFilters?: UserLeaderboardFilters
 ): Promise<LeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findLeaderboardWithTimezone("allTime", timezone, undefined, userFilters);
 }
 
@@ -119,7 +119,7 @@ export async function findAllTimeLeaderboard(
  * 使用滚动24小时窗口而非日历日
  */
 export async function findLast24HoursLeaderboard(): Promise<LeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findLeaderboardWithTimezone("last24h", timezone);
 }
 
@@ -244,29 +244,29 @@ export async function findCustomRangeLeaderboard(
   dateRange: DateRangeParams,
   userFilters?: UserLeaderboardFilters
 ): Promise<LeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findLeaderboardWithTimezone("custom", timezone, dateRange, userFilters);
 }
 
 /**
  * 查询今日供应商消耗排行榜(不限制数量)
- * 使用 SQL AT TIME ZONE 进行时区转换,确保"今日"基于配置时区(Asia/Shanghai)
+ * 使用 SQL AT TIME ZONE 进行时区转换,确保"今日"基于系统时区
  */
 export async function findDailyProviderLeaderboard(
   providerType?: ProviderType
 ): Promise<ProviderLeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findProviderLeaderboardWithTimezone("daily", timezone, undefined, providerType);
 }
 
 /**
  * 查询本月供应商消耗排行榜(不限制数量)
- * 使用 SQL AT TIME ZONE 进行时区转换,确保"本月"基于配置时区(Asia/Shanghai)
+ * 使用 SQL AT TIME ZONE 进行时区转换,确保"本月"基于系统时区
  */
 export async function findMonthlyProviderLeaderboard(
   providerType?: ProviderType
 ): Promise<ProviderLeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findProviderLeaderboardWithTimezone("monthly", timezone, undefined, providerType);
 }
 
@@ -276,7 +276,7 @@ export async function findMonthlyProviderLeaderboard(
 export async function findWeeklyProviderLeaderboard(
   providerType?: ProviderType
 ): Promise<ProviderLeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findProviderLeaderboardWithTimezone("weekly", timezone, undefined, providerType);
 }
 
@@ -286,7 +286,7 @@ export async function findWeeklyProviderLeaderboard(
 export async function findAllTimeProviderLeaderboard(
   providerType?: ProviderType
 ): Promise<ProviderLeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findProviderLeaderboardWithTimezone("allTime", timezone, undefined, providerType);
 }
 
@@ -296,7 +296,7 @@ export async function findAllTimeProviderLeaderboard(
 export async function findDailyProviderCacheHitRateLeaderboard(
   providerType?: ProviderType
 ): Promise<ProviderCacheHitRateLeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findProviderCacheHitRateLeaderboardWithTimezone(
     "daily",
     timezone,
@@ -311,7 +311,7 @@ export async function findDailyProviderCacheHitRateLeaderboard(
 export async function findMonthlyProviderCacheHitRateLeaderboard(
   providerType?: ProviderType
 ): Promise<ProviderCacheHitRateLeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findProviderCacheHitRateLeaderboardWithTimezone(
     "monthly",
     timezone,
@@ -326,7 +326,7 @@ export async function findMonthlyProviderCacheHitRateLeaderboard(
 export async function findWeeklyProviderCacheHitRateLeaderboard(
   providerType?: ProviderType
 ): Promise<ProviderCacheHitRateLeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findProviderCacheHitRateLeaderboardWithTimezone(
     "weekly",
     timezone,
@@ -341,7 +341,7 @@ export async function findWeeklyProviderCacheHitRateLeaderboard(
 export async function findAllTimeProviderCacheHitRateLeaderboard(
   providerType?: ProviderType
 ): Promise<ProviderCacheHitRateLeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findProviderCacheHitRateLeaderboardWithTimezone(
     "allTime",
     timezone,
@@ -508,7 +508,7 @@ export async function findCustomRangeProviderLeaderboard(
   dateRange: DateRangeParams,
   providerType?: ProviderType
 ): Promise<ProviderLeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findProviderLeaderboardWithTimezone("custom", timezone, dateRange, providerType);
 }
 
@@ -519,7 +519,7 @@ export async function findCustomRangeProviderCacheHitRateLeaderboard(
   dateRange: DateRangeParams,
   providerType?: ProviderType
 ): Promise<ProviderCacheHitRateLeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findProviderCacheHitRateLeaderboardWithTimezone(
     "custom",
     timezone,
@@ -530,19 +530,19 @@ export async function findCustomRangeProviderCacheHitRateLeaderboard(
 
 /**
  * 查询今日模型调用排行榜(不限制数量)
- * 使用 SQL AT TIME ZONE 进行时区转换,确保"今日"基于配置时区(Asia/Shanghai)
+ * 使用 SQL AT TIME ZONE 进行时区转换,确保"今日"基于系统时区
  */
 export async function findDailyModelLeaderboard(): Promise<ModelLeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findModelLeaderboardWithTimezone("daily", timezone);
 }
 
 /**
  * 查询本月模型调用排行榜(不限制数量)
- * 使用 SQL AT TIME ZONE 进行时区转换,确保"本月"基于配置时区(Asia/Shanghai)
+ * 使用 SQL AT TIME ZONE 进行时区转换,确保"本月"基于系统时区
  */
 export async function findMonthlyModelLeaderboard(): Promise<ModelLeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findModelLeaderboardWithTimezone("monthly", timezone);
 }
 
@@ -550,7 +550,7 @@ export async function findMonthlyModelLeaderboard(): Promise<ModelLeaderboardEnt
  * 查询本周模型调用排行榜(不限制数量)
  */
 export async function findWeeklyModelLeaderboard(): Promise<ModelLeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findModelLeaderboardWithTimezone("weekly", timezone);
 }
 
@@ -558,7 +558,7 @@ export async function findWeeklyModelLeaderboard(): Promise<ModelLeaderboardEntr
  * 查询全部时间模型调用排行榜(不限制数量)
  */
 export async function findAllTimeModelLeaderboard(): Promise<ModelLeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findModelLeaderboardWithTimezone("allTime", timezone);
 }
 
@@ -631,6 +631,6 @@ async function findModelLeaderboardWithTimezone(
 export async function findCustomRangeModelLeaderboard(
   dateRange: DateRangeParams
 ): Promise<ModelLeaderboardEntry[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   return findModelLeaderboardWithTimezone("custom", timezone, dateRange);
 }

+ 5 - 3
src/repository/notification-bindings.ts

@@ -3,12 +3,11 @@
 import { and, desc, eq, notInArray } from "drizzle-orm";
 import { db } from "@/drizzle/db";
 import { notificationTargetBindings, webhookTargets } from "@/drizzle/schema";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import type { WebhookProviderType, WebhookTarget, WebhookTestResult } from "./webhook-targets";
 
 export type NotificationType = "circuit_breaker" | "daily_leaderboard" | "cost_alert";
 
-const DEFAULT_TIMEZONE = "Asia/Shanghai";
-
 export interface NotificationBinding {
   id: number;
   notificationType: NotificationType;
@@ -159,12 +158,15 @@ export async function upsertBindings(
   type: NotificationType,
   bindings: BindingInput[]
 ): Promise<void> {
+  // Resolve system timezone for default value
+  const defaultTimezone = await resolveSystemTimezone();
+
   const normalized = bindings
     .map((b) => ({
       targetId: b.targetId,
       isEnabled: b.isEnabled ?? true,
       scheduleCron: b.scheduleCron ?? null,
-      scheduleTimezone: b.scheduleTimezone ?? DEFAULT_TIMEZONE,
+      scheduleTimezone: b.scheduleTimezone ?? defaultTimezone,
       templateOverride: b.templateOverride ?? null,
     }))
     .filter((b) => Number.isFinite(b.targetId) && b.targetId > 0);

+ 4 - 4
src/repository/overview.ts

@@ -3,8 +3,8 @@
 import { and, avg, count, eq, gte, isNull, sql, sum } from "drizzle-orm";
 import { db } from "@/drizzle/db";
 import { messageRequest } from "@/drizzle/schema";
-import { getEnvConfig } from "@/lib/config";
 import { Decimal, toCostDecimal } from "@/lib/utils/currency";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import { EXCLUDE_WARMUP_CONDITION } from "./_shared/message-request-conditions";
 
 /**
@@ -38,10 +38,10 @@ export interface OverviewMetricsWithComparison extends OverviewMetrics {
 /**
  * 获取今日概览统计数据
  * 包括:今日总请求数、今日总消耗、平均响应时间、今日错误率
- * 使用 SQL AT TIME ZONE 确保"今日"基于配置时区(TZ 环境变量)
+ * 使用 SQL AT TIME ZONE 确保"今日"基于系统时区配置
  */
 export async function getOverviewMetrics(): Promise<OverviewMetrics> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
 
   const [result] = await db
     .select({
@@ -88,7 +88,7 @@ export async function getOverviewMetrics(): Promise<OverviewMetrics> {
 export async function getOverviewMetricsWithComparison(
   userId?: number
 ): Promise<OverviewMetricsWithComparison> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
 
   // 用户过滤条件
   const userCondition = userId ? eq(messageRequest.userId, userId) : undefined;

+ 3 - 3
src/repository/provider.ts

@@ -4,8 +4,8 @@ import { and, desc, eq, isNotNull, isNull, ne, sql } from "drizzle-orm";
 import { db } from "@/drizzle/db";
 import { providers } from "@/drizzle/schema";
 import { getCachedProviders } from "@/lib/cache/provider-cache";
-import { getEnvConfig } from "@/lib/config";
 import { logger } from "@/lib/logger";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import type { CreateProviderData, Provider, UpdateProviderData } from "@/types/provider";
 import { toProvider } from "./_shared/transformers";
 import {
@@ -793,9 +793,9 @@ export async function getProviderStatistics(): Promise<
   }>
 > {
   try {
-    // 统一的时区处理:使用 PostgreSQL AT TIME ZONE + 环境变量 TZ
+    // 统一的时区处理:使用 PostgreSQL AT TIME ZONE + 系统时区配置
     // 参考 getUserStatisticsFromDB 的实现,避免 Node.js Date 带来的时区偏移
-    const timezone = getEnvConfig().TZ;
+    const timezone = await resolveSystemTimezone();
 
     // ⭐ 使用 providerChain 最后一项的 providerId 来确定最终供应商(兼容重试切换)
     // 如果 provider_chain 为空(无重试),则使用 provider_id 字段

+ 6 - 6
src/repository/statistics.ts

@@ -3,7 +3,7 @@
 import { and, eq, gte, isNull, lt, sql } from "drizzle-orm";
 import { db } from "@/drizzle/db";
 import { keys, messageRequest } from "@/drizzle/schema";
-import { getEnvConfig } from "@/lib/config";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import type {
   DatabaseKey,
   DatabaseKeyStatRow,
@@ -21,7 +21,7 @@ import { EXCLUDE_WARMUP_CONDITION } from "./_shared/message-request-conditions";
  * 注意:这个函数使用原生SQL,因为涉及到PostgreSQL特定的generate_series函数
  */
 export async function getUserStatisticsFromDB(timeRange: TimeRange): Promise<DatabaseStatRow[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   let query;
 
   switch (timeRange) {
@@ -199,7 +199,7 @@ export async function getKeyStatisticsFromDB(
   userId: number,
   timeRange: TimeRange
 ): Promise<DatabaseKeyStatRow[]> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   let query;
 
   switch (timeRange) {
@@ -402,7 +402,7 @@ export async function getMixedStatisticsFromDB(
   ownKeys: DatabaseKeyStatRow[];
   othersAggregate: DatabaseStatRow[];
 }> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   let ownKeysQuery;
   let othersQuery;
 
@@ -720,7 +720,7 @@ export async function getMixedStatisticsFromDB(
  * @deprecated 使用 sumUserCostInTimeRange() 替代
  */
 export async function sumUserCostToday(userId: number): Promise<number> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
 
   const query = sql`
     SELECT COALESCE(SUM(mr.cost_usd), 0) AS total_cost
@@ -1043,7 +1043,7 @@ export async function findKeyCostEntriesInTimeRange(
 export async function getRateLimitEventStats(
   filters: RateLimitEventFilters = {}
 ): Promise<RateLimitEventStats> {
-  const timezone = getEnvConfig().TZ;
+  const timezone = await resolveSystemTimezone();
   const { user_id, provider_id, limit_type, start_time, end_time, key_id } = filters;
 
   // 构建 WHERE 条件

+ 8 - 0
src/repository/system-config.ts

@@ -140,6 +140,7 @@ function createFallbackSettings(): SystemSettings {
     allowGlobalUsageView: false,
     currencyDisplay: "USD",
     billingModelSource: "original",
+    timezone: null,
     enableAutoCleanup: false,
     cleanupRetentionDays: 30,
     cleanupSchedule: "0 2 * * *",
@@ -180,6 +181,7 @@ export async function getSystemSettings(): Promise<SystemSettings> {
       allowGlobalUsageView: systemSettings.allowGlobalUsageView,
       currencyDisplay: systemSettings.currencyDisplay,
       billingModelSource: systemSettings.billingModelSource,
+      timezone: systemSettings.timezone,
       enableAutoCleanup: systemSettings.enableAutoCleanup,
       cleanupRetentionDays: systemSettings.cleanupRetentionDays,
       cleanupSchedule: systemSettings.cleanupSchedule,
@@ -294,6 +296,11 @@ export async function updateSystemSettings(
       updates.billingModelSource = payload.billingModelSource;
     }
 
+    // 系统时区配置字段(如果提供)
+    if (payload.timezone !== undefined) {
+      updates.timezone = payload.timezone;
+    }
+
     // 日志清理配置字段(如果提供)
     if (payload.enableAutoCleanup !== undefined) {
       updates.enableAutoCleanup = payload.enableAutoCleanup;
@@ -381,6 +388,7 @@ export async function updateSystemSettings(
         allowGlobalUsageView: systemSettings.allowGlobalUsageView,
         currencyDisplay: systemSettings.currencyDisplay,
         billingModelSource: systemSettings.billingModelSource,
+        timezone: systemSettings.timezone,
         enableAutoCleanup: systemSettings.enableAutoCleanup,
         cleanupRetentionDays: systemSettings.cleanupRetentionDays,
         cleanupSchedule: systemSettings.cleanupSchedule,

+ 8 - 0
src/types/system-config.ts

@@ -22,6 +22,11 @@ export interface SystemSettings {
   // 计费模型来源配置
   billingModelSource: BillingModelSource;
 
+  // 系统时区配置 (IANA timezone identifier)
+  // 用于统一后端时间边界计算和前端日期/时间显示
+  // null 表示使用环境变量 TZ 或默认 UTC
+  timezone: string | null;
+
   // 日志清理配置
   enableAutoCleanup?: boolean;
   cleanupRetentionDays?: number;
@@ -75,6 +80,9 @@ export interface UpdateSystemSettingsInput {
   // 计费模型来源配置(可选)
   billingModelSource?: BillingModelSource;
 
+  // 系统时区配置(可选)
+  timezone?: string | null;
+
   // 日志清理配置(可选)
   enableAutoCleanup?: boolean;
   cleanupRetentionDays?: number;

+ 1 - 0
tests/integration/billing-model-source.test.ts

@@ -103,6 +103,7 @@ function makeSystemSettings(
     allowGlobalUsageView: false,
     currencyDisplay: "USD",
     billingModelSource,
+    timezone: null,
     enableAutoCleanup: false,
     cleanupRetentionDays: 30,
     cleanupSchedule: "0 2 * * *",

+ 5 - 5
tests/unit/actions/my-usage-date-range-dst.test.ts

@@ -5,7 +5,7 @@ const mocks = vi.hoisted(() => ({
   getSession: vi.fn(),
   getSystemSettings: vi.fn(),
   findUsageLogsWithDetails: vi.fn(),
-  getEnvConfig: vi.fn(),
+  resolveSystemTimezone: vi.fn(),
 }));
 
 vi.mock("@/lib/auth", () => ({
@@ -24,14 +24,14 @@ vi.mock("@/repository/usage-logs", async (importOriginal) => {
   };
 });
 
-vi.mock("@/lib/config", () => ({
-  getEnvConfig: mocks.getEnvConfig,
+vi.mock("@/lib/utils/timezone", () => ({
+  resolveSystemTimezone: mocks.resolveSystemTimezone,
 }));
 
 describe("my-usage date range parsing", () => {
   it("computes exclusive endTime as next local midnight across DST start", async () => {
     const tz = "America/Los_Angeles";
-    mocks.getEnvConfig.mockReturnValue({ TZ: tz });
+    mocks.resolveSystemTimezone.mockResolvedValue(tz);
 
     mocks.getSession.mockResolvedValue({
       key: { id: 1, key: "k" },
@@ -60,7 +60,7 @@ describe("my-usage date range parsing", () => {
 
   it("computes exclusive endTime as next local midnight across DST end", async () => {
     const tz = "America/Los_Angeles";
-    mocks.getEnvConfig.mockReturnValue({ TZ: tz });
+    mocks.resolveSystemTimezone.mockResolvedValue(tz);
 
     mocks.getSession.mockResolvedValue({
       key: { id: 1, key: "k" },

+ 1 - 1
tests/unit/actions/my-usage-token-aggregation.test.ts

@@ -124,7 +124,7 @@ describe("my-usage token aggregation", () => {
       return selectQueue.shift() ?? createThenableQuery([]);
     });
 
-    mocks.getTimeRangeForPeriodWithMode.mockReturnValue({
+    mocks.getTimeRangeForPeriodWithMode.mockResolvedValue({
       startTime: new Date("2024-01-01T00:00:00.000Z"),
       endTime: new Date("2024-01-02T00:00:00.000Z"),
     });

+ 1 - 0
tests/unit/dashboard/availability/latency-chart.test.tsx

@@ -10,6 +10,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
 // Mock next-intl
 vi.mock("next-intl", () => ({
   useTranslations: () => (key: string) => key,
+  useTimeZone: () => "UTC",
 }));
 
 // Mock recharts to expose color props via data-* attributes

+ 1 - 0
tests/unit/dashboard/availability/latency-curve.test.tsx

@@ -10,6 +10,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
 // Mock next-intl
 vi.mock("next-intl", () => ({
   useTranslations: () => (key: string) => key,
+  useTimeZone: () => "UTC",
 }));
 
 // Mock recharts to expose color props via data-* attributes

+ 1 - 0
tests/unit/lib/config/system-settings-cache.test.ts

@@ -30,6 +30,7 @@ function createSettings(overrides: Partial<SystemSettings> = {}): SystemSettings
     allowGlobalUsageView: false,
     currencyDisplay: "USD",
     billingModelSource: "original",
+    timezone: null,
     enableAutoCleanup: false,
     cleanupRetentionDays: 30,
     cleanupSchedule: "0 2 * * *",

+ 60 - 0
tests/unit/lib/date-format-timezone.test.ts

@@ -0,0 +1,60 @@
+import { describe, expect, it } from "vitest";
+import { formatDate } from "@/lib/utils/date-format";
+
+describe("formatDate with timezone parameter", () => {
+  // Fixed UTC timestamp: 2025-01-15T23:30:00Z
+  const utcDate = new Date("2025-01-15T23:30:00Z");
+
+  it("returns formatted date without timezone (original behaviour)", () => {
+    const result = formatDate(utcDate, "yyyy-MM-dd", "en");
+    // Without timezone, result depends on local TZ - just verify it returns a string
+    expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
+  });
+
+  it("formats date in UTC timezone", () => {
+    const result = formatDate(utcDate, "yyyy-MM-dd HH:mm", "en", "UTC");
+    expect(result).toBe("2025-01-15 23:30");
+  });
+
+  it("formats date in Asia/Shanghai timezone (UTC+8)", () => {
+    // 2025-01-15T23:30:00Z => 2025-01-16T07:30:00 in Asia/Shanghai
+    const result = formatDate(utcDate, "yyyy-MM-dd HH:mm", "en", "Asia/Shanghai");
+    expect(result).toBe("2025-01-16 07:30");
+  });
+
+  it("formats date in America/New_York timezone (UTC-5 in January)", () => {
+    // 2025-01-15T23:30:00Z => 2025-01-15T18:30:00 in America/New_York (EST)
+    const result = formatDate(utcDate, "yyyy-MM-dd HH:mm", "en", "America/New_York");
+    expect(result).toBe("2025-01-15 18:30");
+  });
+
+  it("handles date-only format with timezone that crosses midnight", () => {
+    // 2025-01-15T23:30:00Z is already 2025-01-16 in Asia/Shanghai
+    const dateOnly = formatDate(utcDate, "yyyy-MM-dd", "en", "Asia/Shanghai");
+    expect(dateOnly).toBe("2025-01-16");
+  });
+
+  it("preserves locale formatting with timezone", () => {
+    const result = formatDate(utcDate, "PPP", "en", "UTC");
+    // PPP in en locale: "January 15th, 2025"
+    expect(result).toContain("January");
+    expect(result).toContain("2025");
+  });
+
+  it("works with string date input and timezone", () => {
+    const result = formatDate("2025-06-01T12:00:00Z", "yyyy-MM-dd HH:mm", "en", "Asia/Tokyo");
+    // UTC 12:00 => JST 21:00
+    expect(result).toBe("2025-06-01 21:00");
+  });
+
+  it("works with numeric timestamp and timezone", () => {
+    const ts = utcDate.getTime();
+    const result = formatDate(ts, "yyyy-MM-dd HH:mm", "en", "UTC");
+    expect(result).toBe("2025-01-15 23:30");
+  });
+
+  it("falls back to local format when timezone is undefined", () => {
+    const result = formatDate(utcDate, "yyyy-MM-dd", "en", undefined);
+    expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
+  });
+});

+ 11 - 0
tests/unit/lib/rate-limit/lease-service.test.ts

@@ -119,6 +119,7 @@ describe("LeaseService", () => {
         allowGlobalUsageView: false,
         currencyDisplay: "USD",
         billingModelSource: "original",
+        timezone: null,
         verboseProviderError: false,
         enableAutoCleanup: false,
         cleanupRetentionDays: 30,
@@ -195,6 +196,7 @@ describe("LeaseService", () => {
         allowGlobalUsageView: false,
         currencyDisplay: "USD",
         billingModelSource: "original",
+        timezone: null,
         verboseProviderError: false,
         enableAutoCleanup: false,
         cleanupRetentionDays: 30,
@@ -258,6 +260,7 @@ describe("LeaseService", () => {
         allowGlobalUsageView: false,
         currencyDisplay: "USD",
         billingModelSource: "original",
+        timezone: null,
         verboseProviderError: false,
         enableAutoCleanup: false,
         cleanupRetentionDays: 30,
@@ -315,6 +318,7 @@ describe("LeaseService", () => {
         allowGlobalUsageView: false,
         currencyDisplay: "USD",
         billingModelSource: "original",
+        timezone: null,
         verboseProviderError: false,
         enableAutoCleanup: false,
         cleanupRetentionDays: 30,
@@ -383,6 +387,7 @@ describe("LeaseService", () => {
         allowGlobalUsageView: false,
         currencyDisplay: "USD",
         billingModelSource: "original",
+        timezone: null,
         verboseProviderError: false,
         enableAutoCleanup: false,
         cleanupRetentionDays: 30,
@@ -441,6 +446,7 @@ describe("LeaseService", () => {
         allowGlobalUsageView: false,
         currencyDisplay: "USD",
         billingModelSource: "original",
+        timezone: null,
         verboseProviderError: false,
         enableAutoCleanup: false,
         cleanupRetentionDays: 30,
@@ -499,6 +505,7 @@ describe("LeaseService", () => {
         allowGlobalUsageView: false,
         currencyDisplay: "USD",
         billingModelSource: "original",
+        timezone: null,
         verboseProviderError: false,
         enableAutoCleanup: false,
         cleanupRetentionDays: 30,
@@ -558,6 +565,7 @@ describe("LeaseService", () => {
         allowGlobalUsageView: false,
         currencyDisplay: "USD",
         billingModelSource: "original",
+        timezone: null,
         verboseProviderError: false,
         enableAutoCleanup: false,
         cleanupRetentionDays: 30,
@@ -619,6 +627,7 @@ describe("LeaseService", () => {
         allowGlobalUsageView: false,
         currencyDisplay: "USD",
         billingModelSource: "original",
+        timezone: null,
         verboseProviderError: false,
         enableAutoCleanup: false,
         cleanupRetentionDays: 30,
@@ -676,6 +685,7 @@ describe("LeaseService", () => {
         allowGlobalUsageView: false,
         currencyDisplay: "USD",
         billingModelSource: "original",
+        timezone: null,
         verboseProviderError: false,
         enableAutoCleanup: false,
         cleanupRetentionDays: 30,
@@ -735,6 +745,7 @@ describe("LeaseService", () => {
         allowGlobalUsageView: false,
         currencyDisplay: "USD",
         billingModelSource: "original",
+        timezone: null,
         verboseProviderError: false,
         enableAutoCleanup: false,
         cleanupRetentionDays: 30,

+ 46 - 79
tests/unit/lib/rate-limit/lease.test.ts

@@ -6,12 +6,12 @@
 
 import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
 
-// Mock getEnvConfig before importing lease module
-vi.mock("@/lib/config", () => ({
-  getEnvConfig: vi.fn(() => ({ TZ: "Asia/Shanghai" })),
+// Mock resolveSystemTimezone before importing lease module
+vi.mock("@/lib/utils/timezone", () => ({
+  resolveSystemTimezone: vi.fn(async () => "Asia/Shanghai"),
 }));
 
-import { getEnvConfig } from "@/lib/config";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 
 describe("lease module", () => {
   const nowMs = 1706400000000; // 2024-01-28 00:00:00 UTC
@@ -99,7 +99,7 @@ describe("lease module", () => {
     it("should return 5h rolling window range", async () => {
       const { getLeaseTimeRange } = await import("@/lib/rate-limit/lease");
 
-      const range = getLeaseTimeRange("5h");
+      const range = await getLeaseTimeRange("5h");
 
       expect(range.endTime.getTime()).toBe(nowMs);
       expect(range.startTime.getTime()).toBe(nowMs - 5 * 60 * 60 * 1000);
@@ -108,7 +108,7 @@ describe("lease module", () => {
     it("should return daily rolling window range (24h)", async () => {
       const { getLeaseTimeRange } = await import("@/lib/rate-limit/lease");
 
-      const range = getLeaseTimeRange("daily", "00:00", "rolling");
+      const range = await getLeaseTimeRange("daily", "00:00", "rolling");
 
       expect(range.endTime.getTime()).toBe(nowMs);
       expect(range.startTime.getTime()).toBe(nowMs - 24 * 60 * 60 * 1000);
@@ -117,7 +117,7 @@ describe("lease module", () => {
     it("should return daily fixed window range with custom reset time", async () => {
       const { getLeaseTimeRange } = await import("@/lib/rate-limit/lease");
 
-      const range = getLeaseTimeRange("daily", "18:00", "fixed");
+      const range = await getLeaseTimeRange("daily", "18:00", "fixed");
 
       // Should calculate based on fixed reset time
       expect(range.endTime.getTime()).toBe(nowMs);
@@ -127,7 +127,7 @@ describe("lease module", () => {
     it("should return weekly natural window range", async () => {
       const { getLeaseTimeRange } = await import("@/lib/rate-limit/lease");
 
-      const range = getLeaseTimeRange("weekly");
+      const range = await getLeaseTimeRange("weekly");
 
       expect(range.endTime.getTime()).toBe(nowMs);
       // Should start from Monday 00:00
@@ -137,7 +137,7 @@ describe("lease module", () => {
     it("should return monthly natural window range", async () => {
       const { getLeaseTimeRange } = await import("@/lib/rate-limit/lease");
 
-      const range = getLeaseTimeRange("monthly");
+      const range = await getLeaseTimeRange("monthly");
 
       expect(range.endTime.getTime()).toBe(nowMs);
       // Should start from 1st of month 00:00
@@ -149,7 +149,7 @@ describe("lease module", () => {
     it("should return 5h TTL for 5h window", async () => {
       const { getLeaseTtlSeconds } = await import("@/lib/rate-limit/lease");
 
-      const ttl = getLeaseTtlSeconds("5h");
+      const ttl = await getLeaseTtlSeconds("5h");
 
       expect(ttl).toBe(5 * 3600);
     });
@@ -157,7 +157,7 @@ describe("lease module", () => {
     it("should return 24h TTL for daily rolling window", async () => {
       const { getLeaseTtlSeconds } = await import("@/lib/rate-limit/lease");
 
-      const ttl = getLeaseTtlSeconds("daily", "00:00", "rolling");
+      const ttl = await getLeaseTtlSeconds("daily", "00:00", "rolling");
 
       expect(ttl).toBe(24 * 3600);
     });
@@ -165,7 +165,7 @@ describe("lease module", () => {
     it("should return dynamic TTL for daily fixed window", async () => {
       const { getLeaseTtlSeconds } = await import("@/lib/rate-limit/lease");
 
-      const ttl = getLeaseTtlSeconds("daily", "18:00", "fixed");
+      const ttl = await getLeaseTtlSeconds("daily", "18:00", "fixed");
 
       // Should be positive and less than 24h
       expect(ttl).toBeGreaterThan(0);
@@ -175,7 +175,7 @@ describe("lease module", () => {
     it("should return dynamic TTL for weekly window", async () => {
       const { getLeaseTtlSeconds } = await import("@/lib/rate-limit/lease");
 
-      const ttl = getLeaseTtlSeconds("weekly");
+      const ttl = await getLeaseTtlSeconds("weekly");
 
       // Should be positive and less than 7 days
       expect(ttl).toBeGreaterThan(0);
@@ -185,7 +185,7 @@ describe("lease module", () => {
     it("should return dynamic TTL for monthly window", async () => {
       const { getLeaseTtlSeconds } = await import("@/lib/rate-limit/lease");
 
-      const ttl = getLeaseTtlSeconds("monthly");
+      const ttl = await getLeaseTtlSeconds("monthly");
 
       // Should be positive and less than 31 days
       expect(ttl).toBeGreaterThan(0);
@@ -398,18 +398,15 @@ describe("lease timezone consistency", () => {
 
   describe("getLeaseTimeRange timezone behavior", () => {
     it("should use configured timezone for daily fixed window", async () => {
-      const { getEnvConfig } = await import("@/lib/config");
       const { getLeaseTimeRange } = await import("@/lib/rate-limit/lease");
 
       // 2024-01-15 02:00:00 UTC = 2024-01-15 10:00:00 Shanghai
       const utcTime = new Date("2024-01-15T02:00:00.000Z");
       vi.setSystemTime(utcTime);
-      vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-        typeof getEnvConfig
-      >);
+      vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
       // Reset at 08:00 Shanghai, we've passed it
-      const range = getLeaseTimeRange("daily", "08:00", "fixed");
+      const range = await getLeaseTimeRange("daily", "08:00", "fixed");
 
       // Window starts at 08:00 Shanghai = 00:00 UTC
       expect(range.startTime.toISOString()).toBe("2024-01-15T00:00:00.000Z");
@@ -417,50 +414,41 @@ describe("lease timezone consistency", () => {
     });
 
     it("should use configured timezone for weekly window", async () => {
-      const { getEnvConfig } = await import("@/lib/config");
       const { getLeaseTimeRange } = await import("@/lib/rate-limit/lease");
 
       // 2024-01-17 00:00:00 UTC = Wednesday 08:00 Shanghai
       const utcTime = new Date("2024-01-17T00:00:00.000Z");
       vi.setSystemTime(utcTime);
-      vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-        typeof getEnvConfig
-      >);
+      vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-      const range = getLeaseTimeRange("weekly");
+      const range = await getLeaseTimeRange("weekly");
 
       // Monday 00:00 Shanghai = Sunday 16:00 UTC
       expect(range.startTime.toISOString()).toBe("2024-01-14T16:00:00.000Z");
     });
 
     it("should use configured timezone for monthly window", async () => {
-      const { getEnvConfig } = await import("@/lib/config");
       const { getLeaseTimeRange } = await import("@/lib/rate-limit/lease");
 
       // 2024-01-15 00:00:00 UTC = 2024-01-15 08:00 Shanghai
       const utcTime = new Date("2024-01-15T00:00:00.000Z");
       vi.setSystemTime(utcTime);
-      vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-        typeof getEnvConfig
-      >);
+      vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-      const range = getLeaseTimeRange("monthly");
+      const range = await getLeaseTimeRange("monthly");
 
       // Jan 1 00:00 Shanghai = Dec 31 16:00 UTC
       expect(range.startTime.toISOString()).toBe("2023-12-31T16:00:00.000Z");
     });
 
     it("should ignore timezone for rolling windows (5h)", async () => {
-      const { getEnvConfig } = await import("@/lib/config");
       const { getLeaseTimeRange } = await import("@/lib/rate-limit/lease");
 
       const utcTime = new Date("2024-01-15T12:00:00.000Z");
       vi.setSystemTime(utcTime);
-      vi.mocked(getEnvConfig).mockReturnValue({ TZ: "America/New_York" } as ReturnType<
-        typeof getEnvConfig
-      >);
+      vi.mocked(resolveSystemTimezone).mockResolvedValue("America/New_York");
 
-      const range = getLeaseTimeRange("5h");
+      const range = await getLeaseTimeRange("5h");
 
       // 5h is always rolling, timezone doesn't matter
       expect(range.startTime.toISOString()).toBe("2024-01-15T07:00:00.000Z");
@@ -468,16 +456,13 @@ describe("lease timezone consistency", () => {
     });
 
     it("should ignore timezone for daily rolling window", async () => {
-      const { getEnvConfig } = await import("@/lib/config");
       const { getLeaseTimeRange } = await import("@/lib/rate-limit/lease");
 
       const utcTime = new Date("2024-01-15T12:00:00.000Z");
       vi.setSystemTime(utcTime);
-      vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Europe/London" } as ReturnType<
-        typeof getEnvConfig
-      >);
+      vi.mocked(resolveSystemTimezone).mockResolvedValue("Europe/London");
 
-      const range = getLeaseTimeRange("daily", "08:00", "rolling");
+      const range = await getLeaseTimeRange("daily", "08:00", "rolling");
 
       // Daily rolling is 24h back, timezone doesn't matter
       expect(range.startTime.toISOString()).toBe("2024-01-14T12:00:00.000Z");
@@ -487,55 +472,45 @@ describe("lease timezone consistency", () => {
 
   describe("getLeaseTtlSeconds timezone behavior", () => {
     it("should calculate TTL based on configured timezone for daily fixed", async () => {
-      const { getEnvConfig } = await import("@/lib/config");
       const { getLeaseTtlSeconds } = await import("@/lib/rate-limit/lease");
 
       // 2024-01-15 02:00:00 UTC = 2024-01-15 10:00:00 Shanghai
       const utcTime = new Date("2024-01-15T02:00:00.000Z");
       vi.setSystemTime(utcTime);
-      vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-        typeof getEnvConfig
-      >);
+      vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
       // Next reset at 08:00 Shanghai tomorrow = 22 hours away
-      const ttl = getLeaseTtlSeconds("daily", "08:00", "fixed");
+      const ttl = await getLeaseTtlSeconds("daily", "08:00", "fixed");
 
       expect(ttl).toBe(22 * 3600);
     });
 
     it("should calculate TTL based on configured timezone for weekly", async () => {
-      const { getEnvConfig } = await import("@/lib/config");
       const { getLeaseTtlSeconds } = await import("@/lib/rate-limit/lease");
 
       // 2024-01-17 00:00:00 UTC = Wednesday 08:00 Shanghai
       const utcTime = new Date("2024-01-17T00:00:00.000Z");
       vi.setSystemTime(utcTime);
-      vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-        typeof getEnvConfig
-      >);
+      vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
       // Next Monday 00:00 Shanghai = 112 hours away
-      const ttl = getLeaseTtlSeconds("weekly");
+      const ttl = await getLeaseTtlSeconds("weekly");
 
       expect(ttl).toBe(112 * 3600);
     });
 
     it("should return fixed TTL for rolling windows", async () => {
-      const { getEnvConfig } = await import("@/lib/config");
       const { getLeaseTtlSeconds } = await import("@/lib/rate-limit/lease");
 
-      vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Pacific/Auckland" } as ReturnType<
-        typeof getEnvConfig
-      >);
+      vi.mocked(resolveSystemTimezone).mockResolvedValue("Pacific/Auckland");
 
-      expect(getLeaseTtlSeconds("5h")).toBe(5 * 3600);
-      expect(getLeaseTtlSeconds("daily", "08:00", "rolling")).toBe(24 * 3600);
+      expect(await getLeaseTtlSeconds("5h")).toBe(5 * 3600);
+      expect(await getLeaseTtlSeconds("daily", "08:00", "rolling")).toBe(24 * 3600);
     });
   });
 
   describe("cross-module consistency", () => {
     it("should produce same results as time-utils for daily fixed", async () => {
-      const { getEnvConfig } = await import("@/lib/config");
       const { getLeaseTimeRange, getLeaseTtlSeconds } = await import("@/lib/rate-limit/lease");
       const { getTimeRangeForPeriod, getTTLForPeriod } = await import(
         "@/lib/rate-limit/time-utils"
@@ -543,24 +518,21 @@ describe("lease timezone consistency", () => {
 
       const utcTime = new Date("2024-01-15T02:00:00.000Z");
       vi.setSystemTime(utcTime);
-      vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-        typeof getEnvConfig
-      >);
+      vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-      const leaseRange = getLeaseTimeRange("daily", "08:00", "fixed");
-      const timeUtilsRange = getTimeRangeForPeriod("daily", "08:00");
+      const leaseRange = await getLeaseTimeRange("daily", "08:00", "fixed");
+      const timeUtilsRange = await getTimeRangeForPeriod("daily", "08:00");
 
       expect(leaseRange.startTime.toISOString()).toBe(timeUtilsRange.startTime.toISOString());
       expect(leaseRange.endTime.toISOString()).toBe(timeUtilsRange.endTime.toISOString());
 
-      const leaseTtl = getLeaseTtlSeconds("daily", "08:00", "fixed");
-      const timeUtilsTtl = getTTLForPeriod("daily", "08:00");
+      const leaseTtl = await getLeaseTtlSeconds("daily", "08:00", "fixed");
+      const timeUtilsTtl = await getTTLForPeriod("daily", "08:00");
 
       expect(leaseTtl).toBe(timeUtilsTtl);
     });
 
     it("should produce same results as time-utils for weekly", async () => {
-      const { getEnvConfig } = await import("@/lib/config");
       const { getLeaseTimeRange, getLeaseTtlSeconds } = await import("@/lib/rate-limit/lease");
       const { getTimeRangeForPeriod, getTTLForPeriod } = await import(
         "@/lib/rate-limit/time-utils"
@@ -568,23 +540,20 @@ describe("lease timezone consistency", () => {
 
       const utcTime = new Date("2024-01-17T00:00:00.000Z");
       vi.setSystemTime(utcTime);
-      vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-        typeof getEnvConfig
-      >);
+      vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-      const leaseRange = getLeaseTimeRange("weekly");
-      const timeUtilsRange = getTimeRangeForPeriod("weekly");
+      const leaseRange = await getLeaseTimeRange("weekly");
+      const timeUtilsRange = await getTimeRangeForPeriod("weekly");
 
       expect(leaseRange.startTime.toISOString()).toBe(timeUtilsRange.startTime.toISOString());
 
-      const leaseTtl = getLeaseTtlSeconds("weekly");
-      const timeUtilsTtl = getTTLForPeriod("weekly");
+      const leaseTtl = await getLeaseTtlSeconds("weekly");
+      const timeUtilsTtl = await getTTLForPeriod("weekly");
 
       expect(leaseTtl).toBe(timeUtilsTtl);
     });
 
     it("should produce same results as time-utils for monthly", async () => {
-      const { getEnvConfig } = await import("@/lib/config");
       const { getLeaseTimeRange, getLeaseTtlSeconds } = await import("@/lib/rate-limit/lease");
       const { getTimeRangeForPeriod, getTTLForPeriod } = await import(
         "@/lib/rate-limit/time-utils"
@@ -592,17 +561,15 @@ describe("lease timezone consistency", () => {
 
       const utcTime = new Date("2024-01-15T00:00:00.000Z");
       vi.setSystemTime(utcTime);
-      vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-        typeof getEnvConfig
-      >);
+      vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-      const leaseRange = getLeaseTimeRange("monthly");
-      const timeUtilsRange = getTimeRangeForPeriod("monthly");
+      const leaseRange = await getLeaseTimeRange("monthly");
+      const timeUtilsRange = await getTimeRangeForPeriod("monthly");
 
       expect(leaseRange.startTime.toISOString()).toBe(timeUtilsRange.startTime.toISOString());
 
-      const leaseTtl = getLeaseTtlSeconds("monthly");
-      const timeUtilsTtl = getTTLForPeriod("monthly");
+      const leaseTtl = await getLeaseTtlSeconds("monthly");
+      const timeUtilsTtl = await getTTLForPeriod("monthly");
 
       expect(leaseTtl).toBe(timeUtilsTtl);
     });

+ 16 - 16
tests/unit/lib/rate-limit/rolling-window-5h.test.ts

@@ -12,9 +12,9 @@
 
 import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
 
-// Mock getEnvConfig before importing modules
-vi.mock("@/lib/config", () => ({
-  getEnvConfig: vi.fn(() => ({ TZ: "Asia/Shanghai" })),
+// Mock resolveSystemTimezone before importing modules
+vi.mock("@/lib/utils/timezone", () => ({
+  resolveSystemTimezone: vi.fn(async () => "Asia/Shanghai"),
 }));
 
 const pipelineCommands: Array<unknown[]> = [];
@@ -369,7 +369,7 @@ describe("RateLimitService - 5h rolling window behavior", () => {
     it("should not have any fixed reset time concept", async () => {
       const { getResetInfo } = await import("@/lib/rate-limit/time-utils");
 
-      const info = getResetInfo("5h");
+      const info = await getResetInfo("5h");
 
       // 5h window is rolling type, no resetAt timestamp
       expect(info.type).toBe("rolling");
@@ -383,7 +383,7 @@ describe("RateLimitService - 5h rolling window behavior", () => {
       const now1 = new Date("2024-01-15T10:00:00.000Z").getTime();
       vi.setSystemTime(new Date(now1));
 
-      const range1 = getTimeRangeForPeriod("5h");
+      const range1 = await getTimeRangeForPeriod("5h");
       expect(range1.endTime.getTime()).toBe(now1);
       expect(range1.startTime.getTime()).toBe(now1 - 5 * 60 * 60 * 1000);
 
@@ -391,7 +391,7 @@ describe("RateLimitService - 5h rolling window behavior", () => {
       const now2 = new Date("2024-01-16T15:30:00.000Z").getTime();
       vi.setSystemTime(new Date(now2));
 
-      const range2 = getTimeRangeForPeriod("5h");
+      const range2 = await getTimeRangeForPeriod("5h");
       expect(range2.endTime.getTime()).toBe(now2);
       expect(range2.startTime.getTime()).toBe(now2 - 5 * 60 * 60 * 1000);
     });
@@ -500,7 +500,7 @@ describe("5h limit exceeded - error message and resetTime", () => {
     it("5h window getResetInfo should return rolling type without resetAt", async () => {
       const { getResetInfo } = await import("@/lib/rate-limit/time-utils");
 
-      const info = getResetInfo("5h");
+      const info = await getResetInfo("5h");
 
       // Rolling windows have no fixed reset time
       expect(info.type).toBe("rolling");
@@ -520,12 +520,12 @@ describe("5h limit exceeded - error message and resetTime", () => {
 
       const t1 = baseTime;
       vi.setSystemTime(new Date(t1));
-      const info1 = getResetInfo("5h");
+      const info1 = await getResetInfo("5h");
 
       // Move forward 3 hours
       const t2 = baseTime + 3 * 60 * 60 * 1000;
       vi.setSystemTime(new Date(t2));
-      const info2 = getResetInfo("5h");
+      const info2 = await getResetInfo("5h");
 
       // Both should indicate rolling type, no specific resetAt
       expect(info1.type).toBe("rolling");
@@ -540,14 +540,14 @@ describe("5h limit exceeded - error message and resetTime", () => {
       // T1: Check time range
       const t1 = baseTime;
       vi.setSystemTime(new Date(t1));
-      const range1 = getTimeRangeForPeriod("5h");
+      const range1 = await getTimeRangeForPeriod("5h");
       expect(range1.startTime.getTime()).toBe(t1 - 5 * 60 * 60 * 1000);
       expect(range1.endTime.getTime()).toBe(t1);
 
       // T2: 3 hours later, time range should shift
       const t2 = baseTime + 3 * 60 * 60 * 1000;
       vi.setSystemTime(new Date(t2));
-      const range2 = getTimeRangeForPeriod("5h");
+      const range2 = await getTimeRangeForPeriod("5h");
       expect(range2.startTime.getTime()).toBe(t2 - 5 * 60 * 60 * 1000);
       expect(range2.endTime.getTime()).toBe(t2);
 
@@ -571,7 +571,7 @@ describe("5h limit exceeded - error message and resetTime", () => {
       //   "5-hour cost limit exceeded. Oldest usage will roll off in X hours."
 
       const { getResetInfo } = await import("@/lib/rate-limit/time-utils");
-      const info = getResetInfo("5h");
+      const info = await getResetInfo("5h");
 
       // The info should clearly indicate this is a rolling window
       expect(info.type).toBe("rolling");
@@ -584,7 +584,7 @@ describe("5h limit exceeded - error message and resetTime", () => {
     it("daily fixed window SHOULD have a specific reset time", async () => {
       const { getResetInfo } = await import("@/lib/rate-limit/time-utils");
 
-      const info = getResetInfo("daily", "18:00");
+      const info = await getResetInfo("daily", "18:00");
 
       // Daily fixed windows have a specific reset time
       expect(info.type).toBe("custom");
@@ -595,7 +595,7 @@ describe("5h limit exceeded - error message and resetTime", () => {
     it("daily rolling window should NOT have a specific reset time", async () => {
       const { getResetInfoWithMode } = await import("@/lib/rate-limit/time-utils");
 
-      const info = getResetInfoWithMode("daily", "18:00", "rolling");
+      const info = await getResetInfoWithMode("daily", "18:00", "rolling");
 
       // Daily rolling also has no fixed reset
       expect(info.type).toBe("rolling");
@@ -608,7 +608,7 @@ describe("5h limit exceeded - error message and resetTime", () => {
     it("weekly window should have natural reset time (next Monday)", async () => {
       const { getResetInfo } = await import("@/lib/rate-limit/time-utils");
 
-      const info = getResetInfo("weekly");
+      const info = await getResetInfo("weekly");
 
       expect(info.type).toBe("natural");
       expect(info.resetAt).toBeDefined();
@@ -617,7 +617,7 @@ describe("5h limit exceeded - error message and resetTime", () => {
     it("monthly window should have natural reset time (1st of next month)", async () => {
       const { getResetInfo } = await import("@/lib/rate-limit/time-utils");
 
-      const info = getResetInfo("monthly");
+      const info = await getResetInfo("monthly");
 
       expect(info.type).toBe("natural");
       expect(info.resetAt).toBeDefined();

+ 100 - 156
tests/unit/lib/rate-limit/time-utils.test.ts

@@ -1,11 +1,11 @@
 import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
 
-// Mock getEnvConfig before importing time-utils
-vi.mock("@/lib/config", () => ({
-  getEnvConfig: vi.fn(() => ({ TZ: "Asia/Shanghai" })),
+// Mock resolveSystemTimezone before importing time-utils
+vi.mock("@/lib/utils/timezone", () => ({
+  resolveSystemTimezone: vi.fn(async () => "Asia/Shanghai"),
 }));
 
-import { getEnvConfig } from "@/lib/config";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import {
   getDailyResetTime,
   getResetInfo,
@@ -30,39 +30,39 @@ describe("rate-limit time-utils", () => {
     vi.useRealTimers();
   });
 
-  it("normalizeResetTime:非法时间应回退到安全默认值", () => {
+  it("normalizeResetTime: illegal time should fallback to safe default", () => {
     expect(normalizeResetTime("abc")).toBe("00:00");
     expect(normalizeResetTime("99:10")).toBe("00:10");
     expect(normalizeResetTime("12:70")).toBe("12:00");
   });
 
-  it("getTimeRangeForPeriodWithMode:daily rolling 应返回过去 24 小时窗口", () => {
-    const { startTime, endTime } = getTimeRangeForPeriodWithMode("daily", "00:00", "rolling");
+  it("getTimeRangeForPeriodWithMode: daily rolling should return past 24h window", async () => {
+    const { startTime, endTime } = await getTimeRangeForPeriodWithMode("daily", "00:00", "rolling");
 
     expect(endTime.getTime()).toBe(nowMs);
     expect(startTime.getTime()).toBe(nowMs - 24 * 60 * 60 * 1000);
   });
 
-  it("getResetInfoWithMode:daily rolling 应返回 rolling 语义", () => {
-    const info = getResetInfoWithMode("daily", "00:00", "rolling");
+  it("getResetInfoWithMode: daily rolling should return rolling semantics", async () => {
+    const info = await getResetInfoWithMode("daily", "00:00", "rolling");
     expect(info.type).toBe("rolling");
     expect(info.period).toBe("24 小时");
   });
 
-  it("getTTLForPeriodWithMode:daily rolling TTL 应为 24 小时", () => {
-    expect(getTTLForPeriodWithMode("daily", "00:00", "rolling")).toBe(24 * 3600);
+  it("getTTLForPeriodWithMode: daily rolling TTL should be 24h", async () => {
+    expect(await getTTLForPeriodWithMode("daily", "00:00", "rolling")).toBe(24 * 3600);
   });
 
-  it("getTTLForPeriod:5h TTL 应为 5 小时", () => {
-    expect(getTTLForPeriod("5h")).toBe(5 * 3600);
+  it("getTTLForPeriod: 5h TTL should be 5h", async () => {
+    expect(await getTTLForPeriod("5h")).toBe(5 * 3600);
   });
 
-  it("getSecondsUntilMidnight/getDailyResetTime:应能计算出合理的每日重置时间", () => {
-    const seconds = getSecondsUntilMidnight();
+  it("getSecondsUntilMidnight/getDailyResetTime: should compute reasonable daily reset time", async () => {
+    const seconds = await getSecondsUntilMidnight();
     expect(seconds).toBeGreaterThan(0);
     expect(seconds).toBeLessThanOrEqual(24 * 3600);
 
-    const resetAt = getDailyResetTime();
+    const resetAt = await getDailyResetTime();
     expect(resetAt.getTime()).toBeGreaterThan(nowMs);
   });
 });
@@ -70,7 +70,7 @@ describe("rate-limit time-utils", () => {
 /**
  * Timezone Consistency Tests
  *
- * Verify that all time calculations use getEnvConfig().TZ consistently
+ * Verify that all time calculations use resolveSystemTimezone() consistently
  * and produce correct results across different timezone configurations.
  */
 describe("timezone consistency", () => {
@@ -83,21 +83,19 @@ describe("timezone consistency", () => {
     vi.useRealTimers();
   });
 
-  it("should use TZ from getEnvConfig for daily fixed calculations", async () => {
+  it("should use timezone from resolveSystemTimezone for daily fixed calculations", async () => {
     // Set time to 2024-01-15 02:00:00 UTC
     // In Asia/Shanghai (+8), this is 2024-01-15 10:00:00
     const utcTime = new Date("2024-01-15T02:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
     // Reset time 08:00 Shanghai = 00:00 UTC
     // At Shanghai 10:00, we've passed 08:00, so window starts at today's 08:00 Shanghai = 00:00 UTC
-    const { startTime } = getTimeRangeForPeriod("daily", "08:00");
+    const { startTime } = await getTimeRangeForPeriod("daily", "08:00");
 
-    // Verify getEnvConfig was called
-    expect(getEnvConfig).toHaveBeenCalled();
+    // Verify resolveSystemTimezone was called
+    expect(resolveSystemTimezone).toHaveBeenCalled();
 
     // Start should be 2024-01-15 00:00:00 UTC (08:00 Shanghai)
     expect(startTime.toISOString()).toBe("2024-01-15T00:00:00.000Z");
@@ -108,11 +106,9 @@ describe("timezone consistency", () => {
     // Reset at 08:00 Shanghai, we're exactly at reset time
     const utcTime = new Date("2024-01-15T00:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const { startTime, endTime } = getTimeRangeForPeriod("daily", "08:00");
+    const { startTime, endTime } = await getTimeRangeForPeriod("daily", "08:00");
 
     // At exactly 08:00 Shanghai, window starts at 08:00 Shanghai today = 00:00 UTC
     expect(startTime.toISOString()).toBe("2024-01-15T00:00:00.000Z");
@@ -124,11 +120,9 @@ describe("timezone consistency", () => {
     // Reset at 08:00 Shanghai, we haven't reached it yet
     const utcTime = new Date("2024-01-14T23:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const { startTime } = getTimeRangeForPeriod("daily", "08:00");
+    const { startTime } = await getTimeRangeForPeriod("daily", "08:00");
 
     // Before 08:00 Shanghai, window starts at yesterday's 08:00 Shanghai = 2024-01-14 00:00 UTC
     expect(startTime.toISOString()).toBe("2024-01-14T00:00:00.000Z");
@@ -139,11 +133,9 @@ describe("timezone consistency", () => {
     // Reset at 08:00 New York, we've passed it
     const utcTime = new Date("2024-01-15T14:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "America/New_York" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("America/New_York");
 
-    const { startTime } = getTimeRangeForPeriod("daily", "08:00");
+    const { startTime } = await getTimeRangeForPeriod("daily", "08:00");
 
     // 08:00 New York = 13:00 UTC
     expect(startTime.toISOString()).toBe("2024-01-15T13:00:00.000Z");
@@ -155,11 +147,9 @@ describe("timezone consistency", () => {
     // Week starts Monday 00:00 Shanghai = 2024-01-15 00:00 Shanghai = 2024-01-14 16:00 UTC
     const utcTime = new Date("2024-01-17T00:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const { startTime } = getTimeRangeForPeriod("weekly");
+    const { startTime } = await getTimeRangeForPeriod("weekly");
 
     // Monday 00:00 Shanghai = Sunday 16:00 UTC
     expect(startTime.toISOString()).toBe("2024-01-14T16:00:00.000Z");
@@ -170,11 +160,9 @@ describe("timezone consistency", () => {
     // Month starts Jan 1 00:00 Shanghai = Dec 31 16:00 UTC
     const utcTime = new Date("2024-01-15T00:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const { startTime } = getTimeRangeForPeriod("monthly");
+    const { startTime } = await getTimeRangeForPeriod("monthly");
 
     // Jan 1 00:00 Shanghai = Dec 31 16:00 UTC
     expect(startTime.toISOString()).toBe("2023-12-31T16:00:00.000Z");
@@ -185,11 +173,9 @@ describe("timezone consistency", () => {
     // Reset at 08:00 Shanghai - we're in Shanghai's "tomorrow" but before reset
     const utcTime = new Date("2024-01-15T23:30:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const { startTime } = getTimeRangeForPeriod("daily", "08:00");
+    const { startTime } = await getTimeRangeForPeriod("daily", "08:00");
 
     // In Shanghai it's Jan 16 07:30, before 08:00 reset
     // So window starts at Jan 15 08:00 Shanghai = Jan 15 00:00 UTC
@@ -199,11 +185,9 @@ describe("timezone consistency", () => {
   it("should use rolling mode regardless of timezone for daily rolling", async () => {
     const utcTime = new Date("2024-01-15T12:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "America/New_York" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("America/New_York");
 
-    const { startTime, endTime } = getTimeRangeForPeriodWithMode("daily", "08:00", "rolling");
+    const { startTime, endTime } = await getTimeRangeForPeriodWithMode("daily", "08:00", "rolling");
 
     // Rolling mode: always 24 hours back, timezone doesn't matter
     expect(endTime.toISOString()).toBe("2024-01-15T12:00:00.000Z");
@@ -213,11 +197,9 @@ describe("timezone consistency", () => {
   it("should use 5h rolling window regardless of timezone", async () => {
     const utcTime = new Date("2024-01-15T12:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Europe/London" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Europe/London");
 
-    const { startTime, endTime } = getTimeRangeForPeriod("5h");
+    const { startTime, endTime } = await getTimeRangeForPeriod("5h");
 
     // 5h rolling: always 5 hours back
     expect(endTime.toISOString()).toBe("2024-01-15T12:00:00.000Z");
@@ -240,83 +222,71 @@ describe("TTL timezone consistency", () => {
     vi.useRealTimers();
   });
 
-  it("should calculate daily fixed TTL based on configured timezone", () => {
+  it("should calculate daily fixed TTL based on configured timezone", async () => {
     // 2024-01-15 02:00:00 UTC = 2024-01-15 10:00:00 Shanghai
     // Reset at 08:00 Shanghai, next reset is tomorrow 08:00 Shanghai = 2024-01-16 00:00 UTC
     // TTL = 22 hours = 79200 seconds
     const utcTime = new Date("2024-01-15T02:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const ttl = getTTLForPeriod("daily", "08:00");
+    const ttl = await getTTLForPeriod("daily", "08:00");
 
     // From 10:00 Shanghai to next 08:00 Shanghai = 22 hours
     expect(ttl).toBe(22 * 3600);
   });
 
-  it("should calculate daily fixed TTL correctly when close to reset time", () => {
+  it("should calculate daily fixed TTL correctly when close to reset time", async () => {
     // 2024-01-14 23:30:00 UTC = 2024-01-15 07:30:00 Shanghai
     // Reset at 08:00 Shanghai, next reset is in 30 minutes
     const utcTime = new Date("2024-01-14T23:30:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const ttl = getTTLForPeriod("daily", "08:00");
+    const ttl = await getTTLForPeriod("daily", "08:00");
 
     // 30 minutes = 1800 seconds
     expect(ttl).toBe(30 * 60);
   });
 
-  it("should calculate weekly TTL based on configured timezone", () => {
+  it("should calculate weekly TTL based on configured timezone", async () => {
     // 2024-01-17 00:00:00 UTC = Wednesday 08:00 Shanghai
     // Next Monday 00:00 Shanghai = 2024-01-22 00:00 Shanghai = 2024-01-21 16:00 UTC
     const utcTime = new Date("2024-01-17T00:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const ttl = getTTLForPeriod("weekly");
+    const ttl = await getTTLForPeriod("weekly");
 
     // From Wed 08:00 to Mon 00:00 = 4 days + 16 hours = 112 hours
     expect(ttl).toBe(112 * 3600);
   });
 
-  it("should calculate monthly TTL based on configured timezone", () => {
+  it("should calculate monthly TTL based on configured timezone", async () => {
     // 2024-01-30 00:00:00 UTC = 2024-01-30 08:00:00 Shanghai
     // Next month Feb 1 00:00 Shanghai = 2024-01-31 16:00 UTC
     const utcTime = new Date("2024-01-30T00:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const ttl = getTTLForPeriod("monthly");
+    const ttl = await getTTLForPeriod("monthly");
 
     // From Jan 30 08:00 to Feb 1 00:00 Shanghai = 1 day + 16 hours = 40 hours
     expect(ttl).toBe(40 * 3600);
   });
 
-  it("should return 24h TTL for daily rolling regardless of timezone", () => {
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Pacific/Auckland" } as ReturnType<
-      typeof getEnvConfig
-    >);
+  it("should return 24h TTL for daily rolling regardless of timezone", async () => {
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Pacific/Auckland");
 
-    const ttl = getTTLForPeriodWithMode("daily", "08:00", "rolling");
+    const ttl = await getTTLForPeriodWithMode("daily", "08:00", "rolling");
 
     expect(ttl).toBe(24 * 3600);
   });
 
-  it("should return 5h TTL for 5h period regardless of timezone", () => {
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "America/Los_Angeles" } as ReturnType<
-      typeof getEnvConfig
-    >);
+  it("should return 5h TTL for 5h period regardless of timezone", async () => {
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("America/Los_Angeles");
 
-    const ttl = getTTLForPeriod("5h");
+    const ttl = await getTTLForPeriod("5h");
 
     expect(ttl).toBe(5 * 3600);
   });
@@ -337,69 +307,59 @@ describe("ResetInfo timezone consistency", () => {
     vi.useRealTimers();
   });
 
-  it("should return next reset time in configured timezone for daily", () => {
+  it("should return next reset time in configured timezone for daily", async () => {
     // 2024-01-15 02:00:00 UTC = 2024-01-15 10:00:00 Shanghai
     // Next reset at 08:00 Shanghai = 2024-01-16 00:00:00 UTC
     const utcTime = new Date("2024-01-15T02:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const info = getResetInfo("daily", "08:00");
+    const info = await getResetInfo("daily", "08:00");
 
     expect(info.type).toBe("custom");
     expect(info.resetAt?.toISOString()).toBe("2024-01-16T00:00:00.000Z");
   });
 
-  it("should return next Monday for weekly in configured timezone", () => {
+  it("should return next Monday for weekly in configured timezone", async () => {
     // 2024-01-17 00:00:00 UTC = Wednesday 08:00 Shanghai
     // Next Monday 00:00 Shanghai = 2024-01-21 16:00 UTC
     const utcTime = new Date("2024-01-17T00:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const info = getResetInfo("weekly");
+    const info = await getResetInfo("weekly");
 
     expect(info.type).toBe("natural");
     expect(info.resetAt?.toISOString()).toBe("2024-01-21T16:00:00.000Z");
   });
 
-  it("should return next month start for monthly in configured timezone", () => {
+  it("should return next month start for monthly in configured timezone", async () => {
     // 2024-01-15 00:00:00 UTC = 2024-01-15 08:00 Shanghai
     // Feb 1 00:00 Shanghai = 2024-01-31 16:00 UTC
     const utcTime = new Date("2024-01-15T00:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const info = getResetInfo("monthly");
+    const info = await getResetInfo("monthly");
 
     expect(info.type).toBe("natural");
     expect(info.resetAt?.toISOString()).toBe("2024-01-31T16:00:00.000Z");
   });
 
-  it("should return rolling type for 5h period", () => {
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+  it("should return rolling type for 5h period", async () => {
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const info = getResetInfo("5h");
+    const info = await getResetInfo("5h");
 
     expect(info.type).toBe("rolling");
     expect(info.period).toBe("5 小时");
     expect(info.resetAt).toBeUndefined();
   });
 
-  it("should return rolling type for daily rolling mode", () => {
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+  it("should return rolling type for daily rolling mode", async () => {
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const info = getResetInfoWithMode("daily", "08:00", "rolling");
+    const info = await getResetInfoWithMode("daily", "08:00", "rolling");
 
     expect(info.type).toBe("rolling");
     expect(info.period).toBe("24 小时");
@@ -419,139 +379,123 @@ describe("timezone edge cases", () => {
     vi.useRealTimers();
   });
 
-  it("should handle midnight reset time (00:00)", () => {
+  it("should handle midnight reset time (00:00)", async () => {
     // 2024-01-15 18:00:00 UTC = 2024-01-16 02:00:00 Shanghai
     // Reset at 00:00 Shanghai, window starts at 2024-01-16 00:00 Shanghai = 2024-01-15 16:00 UTC
     const utcTime = new Date("2024-01-15T18:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const { startTime } = getTimeRangeForPeriod("daily", "00:00");
+    const { startTime } = await getTimeRangeForPeriod("daily", "00:00");
 
     expect(startTime.toISOString()).toBe("2024-01-15T16:00:00.000Z");
   });
 
-  it("should handle late night reset time (23:59)", () => {
+  it("should handle late night reset time (23:59)", async () => {
     // 2024-01-15 16:30:00 UTC = 2024-01-16 00:30:00 Shanghai
     // Reset at 23:59 Shanghai, we're past it (just after midnight)
     // Window starts at 2024-01-15 23:59 Shanghai = 2024-01-15 15:59 UTC
     const utcTime = new Date("2024-01-15T16:30:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const { startTime } = getTimeRangeForPeriod("daily", "23:59");
+    const { startTime } = await getTimeRangeForPeriod("daily", "23:59");
 
     expect(startTime.toISOString()).toBe("2024-01-15T15:59:00.000Z");
   });
 
-  it("should handle year boundary for monthly window", () => {
+  it("should handle year boundary for monthly window", async () => {
     // 2024-01-05 00:00:00 UTC = 2024-01-05 08:00:00 Shanghai
     // Month starts Jan 1 00:00 Shanghai = 2023-12-31 16:00 UTC
     const utcTime = new Date("2024-01-05T00:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const { startTime } = getTimeRangeForPeriod("monthly");
+    const { startTime } = await getTimeRangeForPeriod("monthly");
 
     expect(startTime.toISOString()).toBe("2023-12-31T16:00:00.000Z");
   });
 
-  it("should handle week boundary crossing year", () => {
+  it("should handle week boundary crossing year", async () => {
     // 2024-01-03 00:00:00 UTC = Wednesday = 2024-01-03 08:00 Shanghai
     // Week started Monday 2024-01-01 00:00 Shanghai = 2023-12-31 16:00 UTC
     const utcTime = new Date("2024-01-03T00:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const { startTime } = getTimeRangeForPeriod("weekly");
+    const { startTime } = await getTimeRangeForPeriod("weekly");
 
     expect(startTime.toISOString()).toBe("2023-12-31T16:00:00.000Z");
   });
 
-  it("should handle negative UTC offset timezone (America/New_York)", () => {
+  it("should handle negative UTC offset timezone (America/New_York)", async () => {
     // 2024-01-15 03:00:00 UTC = 2024-01-14 22:00:00 New York (EST -5)
     // Reset at 08:00 New York, we're before it (still previous day in NY)
     // Window starts at 2024-01-14 08:00 NY = 2024-01-14 13:00 UTC
     const utcTime = new Date("2024-01-15T03:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "America/New_York" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("America/New_York");
 
-    const { startTime } = getTimeRangeForPeriod("daily", "08:00");
+    const { startTime } = await getTimeRangeForPeriod("daily", "08:00");
 
     expect(startTime.toISOString()).toBe("2024-01-14T13:00:00.000Z");
   });
 
-  it("should handle UTC timezone", () => {
+  it("should handle UTC timezone", async () => {
     // 2024-01-15 10:00:00 UTC
     // Reset at 08:00 UTC, we've passed it
     // Window starts at 2024-01-15 08:00 UTC
     const utcTime = new Date("2024-01-15T10:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "UTC" } as ReturnType<typeof getEnvConfig>);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("UTC");
 
-    const { startTime } = getTimeRangeForPeriod("daily", "08:00");
+    const { startTime } = await getTimeRangeForPeriod("daily", "08:00");
 
     expect(startTime.toISOString()).toBe("2024-01-15T08:00:00.000Z");
   });
 
-  it("should handle large positive UTC offset (Pacific/Auckland +13)", () => {
+  it("should handle large positive UTC offset (Pacific/Auckland +13)", async () => {
     // 2024-01-15 10:00:00 UTC = 2024-01-15 23:00:00 Auckland
     // Reset at 08:00 Auckland, we've passed it
     // Window starts at 2024-01-15 08:00 Auckland = 2024-01-14 19:00 UTC
     const utcTime = new Date("2024-01-15T10:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Pacific/Auckland" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Pacific/Auckland");
 
-    const { startTime } = getTimeRangeForPeriod("daily", "08:00");
+    const { startTime } = await getTimeRangeForPeriod("daily", "08:00");
 
     expect(startTime.toISOString()).toBe("2024-01-14T19:00:00.000Z");
   });
 
-  it("should calculate correct TTL at exact reset moment", () => {
+  it("should calculate correct TTL at exact reset moment", async () => {
     // 2024-01-15 00:00:00 UTC = 2024-01-15 08:00:00 Shanghai (exactly at reset)
     // Next reset is 2024-01-16 08:00 Shanghai = 2024-01-16 00:00 UTC
     // TTL = 24 hours
     const utcTime = new Date("2024-01-15T00:00:00.000Z");
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
-    const ttl = getTTLForPeriod("daily", "08:00");
+    const ttl = await getTTLForPeriod("daily", "08:00");
 
     expect(ttl).toBe(24 * 3600);
   });
 
-  it("should handle different reset times consistently", () => {
+  it("should handle different reset times consistently", async () => {
     // Test multiple reset times to ensure consistency
     const utcTime = new Date("2024-01-15T12:00:00.000Z"); // 20:00 Shanghai
     vi.setSystemTime(utcTime);
-    vi.mocked(getEnvConfig).mockReturnValue({ TZ: "Asia/Shanghai" } as ReturnType<
-      typeof getEnvConfig
-    >);
+    vi.mocked(resolveSystemTimezone).mockResolvedValue("Asia/Shanghai");
 
     // 06:00 Shanghai = passed, window starts today 06:00 = 2024-01-14 22:00 UTC
-    const range06 = getTimeRangeForPeriod("daily", "06:00");
+    const range06 = await getTimeRangeForPeriod("daily", "06:00");
     expect(range06.startTime.toISOString()).toBe("2024-01-14T22:00:00.000Z");
 
     // 18:00 Shanghai = passed, window starts today 18:00 = 2024-01-15 10:00 UTC
-    const range18 = getTimeRangeForPeriod("daily", "18:00");
+    const range18 = await getTimeRangeForPeriod("daily", "18:00");
     expect(range18.startTime.toISOString()).toBe("2024-01-15T10:00:00.000Z");
 
     // 21:00 Shanghai = not yet, window starts yesterday 21:00 = 2024-01-14 13:00 UTC
-    const range21 = getTimeRangeForPeriod("daily", "21:00");
+    const range21 = await getTimeRangeForPeriod("daily", "21:00");
     expect(range21.startTime.toISOString()).toBe("2024-01-14T13:00:00.000Z");
   });
 });

+ 119 - 0
tests/unit/lib/timezone/system-timezone.test.ts

@@ -0,0 +1,119 @@
+/**
+ * System Timezone Tests
+ *
+ * TDD tests for the system timezone feature:
+ * 1. Timezone field in SystemSettings
+ * 2. IANA timezone validation
+ * 3. Timezone resolver with fallback chain
+ */
+
+import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
+
+describe("System Timezone", () => {
+  describe("IANA Timezone Validation", () => {
+    it("should accept valid IANA timezone strings", async () => {
+      const { isValidIANATimezone } = await import("@/lib/utils/timezone");
+
+      expect(isValidIANATimezone("Asia/Shanghai")).toBe(true);
+      expect(isValidIANATimezone("America/New_York")).toBe(true);
+      expect(isValidIANATimezone("Europe/London")).toBe(true);
+      expect(isValidIANATimezone("UTC")).toBe(true);
+      expect(isValidIANATimezone("Pacific/Auckland")).toBe(true);
+    });
+
+    it("should reject invalid timezone strings", async () => {
+      const { isValidIANATimezone } = await import("@/lib/utils/timezone");
+
+      expect(isValidIANATimezone("")).toBe(false);
+      expect(isValidIANATimezone("Invalid/Timezone")).toBe(false);
+      // Note: Some abbreviations like "CST" may be valid in Intl API depending on environment
+      // We test clearly invalid values
+      expect(isValidIANATimezone("NotATimezone/AtAll")).toBe(false);
+      expect(isValidIANATimezone(null as unknown as string)).toBe(false);
+      expect(isValidIANATimezone(undefined as unknown as string)).toBe(false);
+    });
+  });
+
+  describe("toSystemSettings transformer", () => {
+    beforeEach(() => {
+      vi.useFakeTimers();
+      vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
+    });
+
+    afterEach(() => {
+      vi.useRealTimers();
+    });
+
+    it("should map timezone field from database", async () => {
+      const { toSystemSettings } = await import("@/repository/_shared/transformers");
+
+      const result = toSystemSettings({
+        id: 1,
+        timezone: "Europe/Paris",
+      });
+
+      expect(result.timezone).toBe("Europe/Paris");
+    });
+
+    it("should default to null when timezone is not set", async () => {
+      const { toSystemSettings } = await import("@/repository/_shared/transformers");
+
+      const result = toSystemSettings({
+        id: 1,
+      });
+
+      expect(result.timezone).toBeNull();
+    });
+  });
+
+  describe("UpdateSystemSettingsSchema", () => {
+    it("should accept valid IANA timezone", async () => {
+      const { UpdateSystemSettingsSchema } = await import("@/lib/validation/schemas");
+
+      const result = UpdateSystemSettingsSchema.safeParse({
+        timezone: "Asia/Tokyo",
+      });
+
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.timezone).toBe("Asia/Tokyo");
+      }
+    });
+
+    it("should reject invalid timezone", async () => {
+      const { UpdateSystemSettingsSchema } = await import("@/lib/validation/schemas");
+
+      const result = UpdateSystemSettingsSchema.safeParse({
+        timezone: "Invalid/Zone",
+      });
+
+      expect(result.success).toBe(false);
+    });
+
+    it("should accept undefined timezone (no update)", async () => {
+      const { UpdateSystemSettingsSchema } = await import("@/lib/validation/schemas");
+
+      const result = UpdateSystemSettingsSchema.safeParse({
+        siteTitle: "Test Site",
+      });
+
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.timezone).toBeUndefined();
+      }
+    });
+
+    it("should accept null timezone (clear setting)", async () => {
+      const { UpdateSystemSettingsSchema } = await import("@/lib/validation/schemas");
+
+      const result = UpdateSystemSettingsSchema.safeParse({
+        timezone: null,
+      });
+
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.timezone).toBeNull();
+      }
+    });
+  });
+});

+ 180 - 0
tests/unit/lib/timezone/timezone-resolver.test.ts

@@ -0,0 +1,180 @@
+/**
+ * Timezone Resolver Tests (Task 2)
+ *
+ * TDD tests for the system timezone resolver:
+ * - Fallback chain: DB timezone -> env TZ -> UTC
+ * - Validation of resolved timezone
+ * - Integration with cached system settings
+ */
+
+import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
+
+// Mock the system settings cache
+vi.mock("@/lib/config/system-settings-cache", () => ({
+  getCachedSystemSettings: vi.fn(),
+}));
+
+// Mock env config
+vi.mock("@/lib/config/env.schema", () => ({
+  getEnvConfig: vi.fn(),
+  isDevelopment: vi.fn(() => false),
+}));
+
+// Mock logger
+vi.mock("@/lib/logger", () => ({
+  logger: {
+    debug: vi.fn(),
+    warn: vi.fn(),
+    info: vi.fn(),
+    error: vi.fn(),
+  },
+}));
+
+import { getCachedSystemSettings } from "@/lib/config/system-settings-cache";
+import { getEnvConfig } from "@/lib/config/env.schema";
+import type { SystemSettings } from "@/types/system-config";
+
+const getCachedSystemSettingsMock = vi.mocked(getCachedSystemSettings);
+const getEnvConfigMock = vi.mocked(getEnvConfig);
+
+function createSettings(overrides: Partial<SystemSettings> = {}): SystemSettings {
+  return {
+    id: 1,
+    siteTitle: "Claude Code Hub",
+    allowGlobalUsageView: false,
+    currencyDisplay: "USD",
+    billingModelSource: "original",
+    timezone: null,
+    enableAutoCleanup: false,
+    cleanupRetentionDays: 30,
+    cleanupSchedule: "0 2 * * *",
+    cleanupBatchSize: 10000,
+    enableClientVersionCheck: false,
+    verboseProviderError: false,
+    enableHttp2: false,
+    interceptAnthropicWarmupRequests: false,
+    enableThinkingSignatureRectifier: true,
+    enableCodexSessionIdCompletion: true,
+    enableResponseFixer: true,
+    responseFixerConfig: {
+      fixTruncatedJson: true,
+      fixSseFormat: true,
+      fixEncoding: true,
+      maxJsonDepth: 200,
+      maxFixSize: 1024 * 1024,
+    },
+    quotaDbRefreshIntervalSeconds: 10,
+    quotaLeasePercent5h: 0.05,
+    quotaLeasePercentDaily: 0.05,
+    quotaLeasePercentWeekly: 0.05,
+    quotaLeasePercentMonthly: 0.05,
+    quotaLeaseCapUsd: null,
+    createdAt: new Date("2026-01-01T00:00:00.000Z"),
+    updatedAt: new Date("2026-01-01T00:00:00.000Z"),
+    ...overrides,
+  };
+}
+
+function mockEnvConfig(tz = "Asia/Shanghai") {
+  getEnvConfigMock.mockReturnValue({
+    NODE_ENV: "test",
+    TZ: tz,
+    PORT: 23000,
+    AUTO_MIGRATE: true,
+    ENABLE_RATE_LIMIT: true,
+    ENABLE_SECURE_COOKIES: true,
+    SESSION_TTL: 300,
+    STORE_SESSION_MESSAGES: false,
+    DEBUG_MODE: false,
+    LOG_LEVEL: "info",
+    ENABLE_CIRCUIT_BREAKER_ON_NETWORK_ERRORS: false,
+    ENABLE_PROVIDER_CACHE: true,
+    MAX_RETRY_ATTEMPTS_DEFAULT: 2,
+    FETCH_BODY_TIMEOUT: 600000,
+    FETCH_HEADERS_TIMEOUT: 600000,
+    FETCH_CONNECT_TIMEOUT: 30000,
+    REDIS_TLS_REJECT_UNAUTHORIZED: true,
+  } as ReturnType<typeof getEnvConfig>);
+}
+
+beforeEach(() => {
+  vi.clearAllMocks();
+});
+
+describe("resolveSystemTimezone", () => {
+  it("should return DB timezone when set and valid", async () => {
+    const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
+
+    getCachedSystemSettingsMock.mockResolvedValue(createSettings({ timezone: "America/New_York" }));
+    mockEnvConfig("Asia/Shanghai");
+
+    const result = await resolveSystemTimezone();
+    expect(result).toBe("America/New_York");
+  });
+
+  it("should fallback to env TZ when DB timezone is null", async () => {
+    const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
+
+    getCachedSystemSettingsMock.mockResolvedValue(createSettings({ timezone: null }));
+    mockEnvConfig("Europe/London");
+
+    const result = await resolveSystemTimezone();
+    expect(result).toBe("Europe/London");
+  });
+
+  it("should fallback to env TZ when DB timezone is invalid", async () => {
+    const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
+
+    getCachedSystemSettingsMock.mockResolvedValue(
+      createSettings({ timezone: "Invalid/Timezone_Zone" })
+    );
+    mockEnvConfig("Asia/Tokyo");
+
+    const result = await resolveSystemTimezone();
+    expect(result).toBe("Asia/Tokyo");
+  });
+
+  it("should fallback to UTC when both DB timezone and env TZ are invalid", async () => {
+    const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
+
+    getCachedSystemSettingsMock.mockResolvedValue(createSettings({ timezone: "Invalid/Zone" }));
+    // Empty string TZ won't pass isValidIANATimezone
+    mockEnvConfig("");
+
+    const result = await resolveSystemTimezone();
+    expect(result).toBe("UTC");
+  });
+
+  it("should fallback to UTC when getCachedSystemSettings throws", async () => {
+    const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
+
+    getCachedSystemSettingsMock.mockRejectedValue(new Error("DB connection failed"));
+    mockEnvConfig("Asia/Shanghai");
+
+    const result = await resolveSystemTimezone();
+    // Should still try env TZ fallback
+    expect(result).toBe("Asia/Shanghai");
+  });
+
+  it("should fallback to UTC when getCachedSystemSettings throws and env TZ is empty", async () => {
+    const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
+
+    getCachedSystemSettingsMock.mockRejectedValue(new Error("DB connection failed"));
+    mockEnvConfig("");
+
+    const result = await resolveSystemTimezone();
+    expect(result).toBe("UTC");
+  });
+
+  it("should handle empty string DB timezone as null", async () => {
+    const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
+
+    getCachedSystemSettingsMock.mockResolvedValue(
+      createSettings({ timezone: "" as unknown as null })
+    );
+    mockEnvConfig("Europe/Paris");
+
+    const result = await resolveSystemTimezone();
+    expect(result).toBe("Europe/Paris");
+  });
+});

+ 123 - 0
tests/unit/lib/utils/date-input.test.ts

@@ -0,0 +1,123 @@
+import { describe, expect, it } from "vitest";
+import { parseDateInputAsTimezone } from "@/lib/utils/date-input";
+
+describe("parseDateInputAsTimezone", () => {
+  describe("date-only input (YYYY-MM-DD)", () => {
+    it("should interpret date-only as end-of-day (23:59:59) in given timezone", () => {
+      // Input: "2024-12-31" in Asia/Shanghai (UTC+8)
+      // Expected: 2024-12-31 23:59:59 in Shanghai = 2024-12-31 15:59:59 UTC
+      const result = parseDateInputAsTimezone("2024-12-31", "Asia/Shanghai");
+
+      expect(result.getUTCFullYear()).toBe(2024);
+      expect(result.getUTCMonth()).toBe(11); // December = 11
+      expect(result.getUTCDate()).toBe(31);
+      expect(result.getUTCHours()).toBe(15); // 23:59:59 Shanghai = 15:59:59 UTC
+      expect(result.getUTCMinutes()).toBe(59);
+      expect(result.getUTCSeconds()).toBe(59);
+    });
+
+    it("should handle UTC timezone correctly", () => {
+      // Input: "2024-06-15" in UTC
+      // Expected: 2024-06-15 23:59:59 UTC
+      const result = parseDateInputAsTimezone("2024-06-15", "UTC");
+
+      expect(result.getUTCFullYear()).toBe(2024);
+      expect(result.getUTCMonth()).toBe(5); // June = 5
+      expect(result.getUTCDate()).toBe(15);
+      expect(result.getUTCHours()).toBe(23);
+      expect(result.getUTCMinutes()).toBe(59);
+      expect(result.getUTCSeconds()).toBe(59);
+    });
+
+    it("should handle negative offset timezone (America/New_York)", () => {
+      // Input: "2024-07-04" in America/New_York (UTC-4 during DST)
+      // Expected: 2024-07-04 23:59:59 in NY = 2024-07-05 03:59:59 UTC
+      const result = parseDateInputAsTimezone("2024-07-04", "America/New_York");
+
+      expect(result.getUTCFullYear()).toBe(2024);
+      expect(result.getUTCMonth()).toBe(6); // July = 6
+      expect(result.getUTCDate()).toBe(5); // Next day in UTC
+      expect(result.getUTCHours()).toBe(3); // 23:59:59 NY (UTC-4) = 03:59:59 UTC next day
+      expect(result.getUTCMinutes()).toBe(59);
+      expect(result.getUTCSeconds()).toBe(59);
+    });
+
+    it("should handle date at year boundary", () => {
+      // Input: "2024-01-01" in Asia/Tokyo (UTC+9)
+      // Expected: 2024-01-01 23:59:59 in Tokyo = 2024-01-01 14:59:59 UTC
+      const result = parseDateInputAsTimezone("2024-01-01", "Asia/Tokyo");
+
+      expect(result.getUTCFullYear()).toBe(2024);
+      expect(result.getUTCMonth()).toBe(0); // January = 0
+      expect(result.getUTCDate()).toBe(1);
+      expect(result.getUTCHours()).toBe(14); // 23:59:59 Tokyo (UTC+9) = 14:59:59 UTC
+    });
+  });
+
+  describe("ISO datetime input", () => {
+    it("should handle ISO datetime string", () => {
+      // Input: "2024-12-31T10:30:00" in Asia/Shanghai
+      // Expected: 2024-12-31 10:30:00 in Shanghai = 2024-12-31 02:30:00 UTC
+      const result = parseDateInputAsTimezone("2024-12-31T10:30:00", "Asia/Shanghai");
+
+      expect(result.getUTCFullYear()).toBe(2024);
+      expect(result.getUTCMonth()).toBe(11);
+      expect(result.getUTCDate()).toBe(31);
+      expect(result.getUTCHours()).toBe(2); // 10:30 Shanghai = 02:30 UTC
+      expect(result.getUTCMinutes()).toBe(30);
+    });
+
+    it("should handle ISO datetime with Z suffix - note: behavior depends on runtime TZ", () => {
+      // NOTE: Z-suffixed input is not a typical use case for this function.
+      // User input from date pickers typically doesn't include Z suffix.
+      // When Z suffix is present, new Date() parses it as UTC, but fromZonedTime
+      // reads the LOCAL time components (which depend on runtime timezone).
+      //
+      // For this reason, we recommend NOT using Z-suffixed input with this function.
+      // This test documents the behavior for awareness, not for correctness assertion.
+      const result = parseDateInputAsTimezone("2024-12-31T10:30:00Z", "Asia/Shanghai");
+
+      // Just verify it doesn't throw and returns a valid date
+      expect(result).toBeInstanceOf(Date);
+      expect(Number.isNaN(result.getTime())).toBe(false);
+    });
+  });
+
+  describe("error handling", () => {
+    it("should throw for invalid date string", () => {
+      expect(() => parseDateInputAsTimezone("invalid-date", "UTC")).toThrow(
+        "Invalid date input: invalid-date"
+      );
+    });
+
+    it("should throw for empty string", () => {
+      expect(() => parseDateInputAsTimezone("", "UTC")).toThrow();
+    });
+  });
+
+  describe("DST edge cases", () => {
+    it("should handle DST transition date in spring (America/New_York)", () => {
+      // March 10, 2024 is when DST starts in US (clocks spring forward at 2am)
+      // Input: "2024-03-10" in America/New_York
+      // Expected: 2024-03-10 23:59:59 in NY (UTC-4 after DST) = 2024-03-11 03:59:59 UTC
+      const result = parseDateInputAsTimezone("2024-03-10", "America/New_York");
+
+      expect(result.getUTCFullYear()).toBe(2024);
+      expect(result.getUTCMonth()).toBe(2); // March = 2
+      expect(result.getUTCDate()).toBe(11); // Next day in UTC
+      expect(result.getUTCHours()).toBe(3); // UTC-4 offset after DST
+    });
+
+    it("should handle DST transition date in fall (America/New_York)", () => {
+      // November 3, 2024 is when DST ends in US (clocks fall back at 2am)
+      // Input: "2024-11-03" in America/New_York
+      // Expected: 2024-11-03 23:59:59 in NY (UTC-5 after DST ends) = 2024-11-04 04:59:59 UTC
+      const result = parseDateInputAsTimezone("2024-11-03", "America/New_York");
+
+      expect(result.getUTCFullYear()).toBe(2024);
+      expect(result.getUTCMonth()).toBe(10); // November = 10
+      expect(result.getUTCDate()).toBe(4); // Next day in UTC
+      expect(result.getUTCHours()).toBe(4); // UTC-5 offset after DST ends
+    });
+  });
+});

+ 1 - 0
tests/unit/proxy/pricing-no-price.test.ts

@@ -78,6 +78,7 @@ function makeSystemSettings(
     allowGlobalUsageView: false,
     currencyDisplay: "USD",
     billingModelSource,
+    timezone: null,
     enableAutoCleanup: false,
     cleanupRetentionDays: 30,
     cleanupSchedule: "0 2 * * *",

+ 1 - 0
tests/unit/proxy/session.test.ts

@@ -25,6 +25,7 @@ function makeSystemSettings(
     allowGlobalUsageView: false,
     currencyDisplay: "USD",
     billingModelSource,
+    timezone: null,
     enableAutoCleanup: false,
     cleanupRetentionDays: 30,
     cleanupSchedule: "0 2 * * *",