Przeglądaj źródła

feat(rate-limit): 添加每日限额重置模式支持(固定时间与滚动窗口)

新增 `dailyResetMode` 配置项,允许用户选择每日限额的重置方式:
- `fixed`: 在每天固定时间点重置(默认行为)
- `rolling`: 从首次调用开始计算,24小时内滚动重置

该功能影响 Key 和 Provider 的限流逻辑,并更新了相关数据库字段、表单控件及 Redis 脚本。前端界面已添加对应选项和描述说明,确保用户可理解两种模式的区别。

同时扩展了 RateLimitService 中的时间范围和 TTL 计算方法以兼容新模式,
并引入两个新的 Lua 脚本用于处理 Redis 中的滚动窗口数据。
Abner 3 miesięcy temu
rodzic
commit
fc6226096c

+ 2 - 0
drizzle/0020_nosy_synch.sql

@@ -0,0 +1,2 @@
+ALTER TABLE "keys" ADD COLUMN "daily_reset_mode" varchar(10) DEFAULT 'fixed' NOT NULL;--> statement-breakpoint
+ALTER TABLE "providers" ADD COLUMN "daily_reset_mode" varchar(10) DEFAULT 'fixed' NOT NULL;

+ 1368 - 0
drizzle/meta/0020_snapshot.json

@@ -0,0 +1,1368 @@
+{
+  "id": "d6a11d21-58d9-43a0-82cb-f57b6cdf7c55",
+  "prevId": "627f1a29-75cb-4616-87db-505bcf347ef6",
+  "version": "7",
+  "dialect": "postgresql",
+  "tables": {
+    "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",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "can_login_web_ui": {
+          "name": "can_login_web_ui",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "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": "varchar(10)",
+          "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_concurrent_sessions": {
+          "name": "limit_concurrent_sessions",
+          "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_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
+        },
+        "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": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "output_tokens": {
+          "name": "output_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_input_tokens": {
+          "name": "cache_creation_input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_read_input_tokens": {
+          "name": "cache_read_input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_message": {
+          "name": "error_message",
+          "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_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_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
+        },
+        "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": {}
+        }
+      },
+      "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
+        },
+        "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.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
+        },
+        "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'"
+        },
+        "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'"
+        },
+        "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": "varchar(10)",
+          "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_concurrent_sessions": {
+          "name": "limit_concurrent_sessions",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "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
+        },
+        "website_url": {
+          "name": "website_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "favicon_url": {
+          "name": "favicon_url",
+          "type": "text",
+          "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": {}
+        }
+      },
+      "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'"
+        },
+        "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
+        },
+        "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,
+          "default": 60
+        },
+        "daily_limit_usd": {
+          "name": "daily_limit_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'100.00'"
+        },
+        "provider_group": {
+          "name": "provider_group",
+          "type": "varchar(50)",
+          "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_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_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
+    }
+  },
+  "enums": {},
+  "schemas": {},
+  "sequences": {},
+  "roles": {},
+  "policies": {},
+  "views": {},
+  "_meta": {
+    "columns": {},
+    "schemas": {},
+    "tables": {}
+  }
+}

+ 7 - 0
drizzle/meta/_journal.json

@@ -141,6 +141,13 @@
       "when": 1763482001307,
       "tag": "0019_open_stephen_strange",
       "breakpoints": true
+    },
+    {
+      "idx": 20,
+      "version": "7",
+      "when": 1763525530558,
+      "tag": "0020_nosy_synch",
+      "breakpoints": true
     }
   ]
 }

+ 11 - 0
messages/en/settings.json

@@ -933,6 +933,17 @@
             "label": "Daily Spend Limit (USD)",
             "placeholder": "Leave empty for unlimited"
           },
+          "dailyResetMode": {
+            "label": "Daily Reset Mode",
+            "options": {
+              "fixed": "Fixed Time Reset",
+              "rolling": "Rolling Window (24h)"
+            },
+            "desc": {
+              "fixed": "Reset quota at a fixed time each day",
+              "rolling": "Reset 24 hours after first API call"
+            }
+          },
           "dailyResetTime": {
             "label": "Daily Reset Time (HH:mm)"
           },

+ 11 - 0
messages/zh-CN/settings.json

@@ -545,6 +545,17 @@
             "label": "每日消费上限 (USD)",
             "placeholder": "留空表示无限制"
           },
+          "dailyResetMode": {
+            "label": "每日重置模式",
+            "options": {
+              "fixed": "固定时间重置",
+              "rolling": "滚动窗口(24小时)"
+            },
+            "desc": {
+              "fixed": "每天固定时间点重置配额",
+              "rolling": "从首次调用开始计算,24小时后重置"
+            }
+          },
           "dailyResetTime": {
             "label": "每日重置时间 (HH:mm)"
           },

+ 4 - 0
src/actions/keys.ts

@@ -28,6 +28,7 @@ export async function addKey(data: {
   canLoginWebUi?: boolean;
   limit5hUsd?: number | null;
   limitDailyUsd?: number | null;
+  dailyResetMode?: "fixed" | "rolling";
   dailyResetTime?: string;
   limitWeeklyUsd?: number | null;
   limitMonthlyUsd?: number | null;
@@ -49,6 +50,7 @@ export async function addKey(data: {
       canLoginWebUi: data.canLoginWebUi,
       limit5hUsd: data.limit5hUsd,
       limitDailyUsd: data.limitDailyUsd,
+      dailyResetMode: data.dailyResetMode,
       dailyResetTime: data.dailyResetTime,
       limitWeeklyUsd: data.limitWeeklyUsd,
       limitMonthlyUsd: data.limitMonthlyUsd,
@@ -79,6 +81,7 @@ export async function addKey(data: {
       can_login_web_ui: validatedData.canLoginWebUi,
       limit_5h_usd: validatedData.limit5hUsd,
       limit_daily_usd: validatedData.limitDailyUsd,
+      daily_reset_mode: validatedData.dailyResetMode,
       daily_reset_time: validatedData.dailyResetTime,
       limit_weekly_usd: validatedData.limitWeeklyUsd,
       limit_monthly_usd: validatedData.limitMonthlyUsd,
@@ -139,6 +142,7 @@ export async function editKey(
       can_login_web_ui: validatedData.canLoginWebUi,
       limit_5h_usd: validatedData.limit5hUsd,
       limit_daily_usd: validatedData.limitDailyUsd,
+      daily_reset_mode: validatedData.dailyResetMode,
       daily_reset_time: validatedData.dailyResetTime,
       limit_weekly_usd: validatedData.limitWeeklyUsd,
       limit_monthly_usd: validatedData.limitMonthlyUsd,

+ 3 - 0
src/actions/providers.ts

@@ -123,6 +123,7 @@ export async function getProviders(): Promise<ProviderDisplay[]> {
         codexInstructionsStrategy: provider.codexInstructionsStrategy,
         limit5hUsd: provider.limit5hUsd,
         limitDailyUsd: provider.limitDailyUsd,
+        dailyResetMode: provider.dailyResetMode,
         dailyResetTime: provider.dailyResetTime,
         limitWeeklyUsd: provider.limitWeeklyUsd,
         limitMonthlyUsd: provider.limitMonthlyUsd,
@@ -176,6 +177,7 @@ export async function addProvider(data: {
   join_claude_pool?: boolean;
   limit_5h_usd?: number | null;
   limit_daily_usd?: number | null;
+  daily_reset_mode?: "fixed" | "rolling";
   daily_reset_time?: string;
   limit_weekly_usd?: number | null;
   limit_monthly_usd?: number | null;
@@ -237,6 +239,7 @@ export async function addProvider(data: {
       ...validated,
       limit_5h_usd: validated.limit_5h_usd ?? null,
       limit_daily_usd: validated.limit_daily_usd ?? null,
+      daily_reset_mode: validated.daily_reset_mode ?? "fixed",
       daily_reset_time: validated.daily_reset_time ?? "00:00",
       limit_weekly_usd: validated.limit_weekly_usd ?? null,
       limit_monthly_usd: validated.limit_monthly_usd ?? null,

+ 2 - 0
src/app/[locale]/dashboard/_components/user/forms/add-key-form.tsx

@@ -29,6 +29,7 @@ export function AddKeyForm({ userId, onSuccess }: AddKeyFormProps) {
       canLoginWebUi: true,
       limit5hUsd: null,
       limitDailyUsd: null,
+      dailyResetMode: "fixed" as const,
       dailyResetTime: "00:00",
       limitWeeklyUsd: null,
       limitMonthlyUsd: null,
@@ -47,6 +48,7 @@ export function AddKeyForm({ userId, onSuccess }: AddKeyFormProps) {
           canLoginWebUi: data.canLoginWebUi,
           limit5hUsd: data.limit5hUsd,
           limitDailyUsd: data.limitDailyUsd,
+          dailyResetMode: data.dailyResetMode,
           dailyResetTime: data.dailyResetTime,
           limitWeeklyUsd: data.limitWeeklyUsd,
           limitMonthlyUsd: data.limitMonthlyUsd,

+ 50 - 10
src/app/[locale]/settings/providers/_components/forms/provider-form.tsx

@@ -83,6 +83,9 @@ export function ProviderForm({
   const [limitDailyUsd, setLimitDailyUsd] = useState<number | null>(
     sourceProvider?.limitDailyUsd ?? null
   );
+  const [dailyResetMode, setDailyResetMode] = useState<"fixed" | "rolling">(
+    sourceProvider?.dailyResetMode ?? "fixed"
+  );
   const [dailyResetTime, setDailyResetTime] = useState<string>(
     sourceProvider?.dailyResetTime ?? "00:00"
   );
@@ -228,6 +231,9 @@ export function ProviderForm({
             cost_multiplier?: number;
             group_tag?: string | null;
             limit_5h_usd?: number | null;
+            limit_daily_usd?: number | null;
+            daily_reset_mode?: "fixed" | "rolling";
+            daily_reset_time?: string;
             limit_weekly_usd?: number | null;
             limit_monthly_usd?: number | null;
             limit_concurrent_sessions?: number | null;
@@ -255,6 +261,7 @@ export function ProviderForm({
             group_tag: groupTag.trim() || null,
             limit_5h_usd: limit5hUsd,
             limit_daily_usd: limitDailyUsd,
+            daily_reset_mode: dailyResetMode,
             daily_reset_time: dailyResetTime,
             limit_weekly_usd: limitWeeklyUsd,
             limit_monthly_usd: limitMonthlyUsd,
@@ -298,6 +305,7 @@ export function ProviderForm({
             group_tag: groupTag.trim() || null,
             limit_5h_usd: limit5hUsd,
             limit_daily_usd: limitDailyUsd,
+            daily_reset_mode: dailyResetMode,
             daily_reset_time: dailyResetTime,
             limit_weekly_usd: limitWeeklyUsd,
             limit_monthly_usd: limitMonthlyUsd,
@@ -803,19 +811,51 @@ export function ProviderForm({
 
               <div className="grid grid-cols-2 gap-4">
                 <div className="space-y-2">
-                  <Label htmlFor={isEdit ? "edit-daily-reset" : "daily-reset"}>
-                    {t("sections.rateLimit.dailyResetTime.label")}
+                  <Label htmlFor={isEdit ? "edit-daily-reset-mode" : "daily-reset-mode"}>
+                    {t("sections.rateLimit.dailyResetMode.label")}
                   </Label>
-                  <Input
-                    id={isEdit ? "edit-daily-reset" : "daily-reset"}
-                    type="time"
-                    value={dailyResetTime}
-                    onChange={(e) => setDailyResetTime(e.target.value || "00:00")}
-                    placeholder="00:00"
+                  <Select
+                    value={dailyResetMode}
+                    onValueChange={(value: "fixed" | "rolling") => setDailyResetMode(value)}
                     disabled={isPending}
-                    step="60"
-                  />
+                  >
+                    <SelectTrigger id={isEdit ? "edit-daily-reset-mode" : "daily-reset-mode"}>
+                      <SelectValue />
+                    </SelectTrigger>
+                    <SelectContent>
+                      <SelectItem value="fixed">
+                        {t("sections.rateLimit.dailyResetMode.options.fixed")}
+                      </SelectItem>
+                      <SelectItem value="rolling">
+                        {t("sections.rateLimit.dailyResetMode.options.rolling")}
+                      </SelectItem>
+                    </SelectContent>
+                  </Select>
+                  <p className="text-xs text-muted-foreground">
+                    {dailyResetMode === "fixed"
+                      ? t("sections.rateLimit.dailyResetMode.desc.fixed")
+                      : t("sections.rateLimit.dailyResetMode.desc.rolling")}
+                  </p>
                 </div>
+                {dailyResetMode === "fixed" && (
+                  <div className="space-y-2">
+                    <Label htmlFor={isEdit ? "edit-daily-reset" : "daily-reset"}>
+                      {t("sections.rateLimit.dailyResetTime.label")}
+                    </Label>
+                    <Input
+                      id={isEdit ? "edit-daily-reset" : "daily-reset"}
+                      type="time"
+                      value={dailyResetTime}
+                      onChange={(e) => setDailyResetTime(e.target.value || "00:00")}
+                      placeholder="00:00"
+                      disabled={isPending}
+                      step="60"
+                    />
+                  </div>
+                )}
+              </div>
+
+              <div className="grid grid-cols-2 gap-4">
                 <div className="space-y-2">
                   <Label htmlFor={isEdit ? "edit-limit-weekly" : "limit-weekly"}>
                     {t("sections.rateLimit.limitWeekly.label")}

+ 1 - 0
src/app/v1/_lib/proxy/provider-selector.ts

@@ -620,6 +620,7 @@ export class ProxyProviderResolver {
         const costCheck = await RateLimitService.checkCostLimits(p.id, "provider", {
           limit_5h_usd: p.limit5hUsd,
           limit_daily_usd: p.limitDailyUsd,
+          daily_reset_mode: p.dailyResetMode,
           daily_reset_time: p.dailyResetTime,
           limit_weekly_usd: p.limitWeeklyUsd,
           limit_monthly_usd: p.limitMonthlyUsd,

+ 1 - 0
src/app/v1/_lib/proxy/rate-limit-guard.ts

@@ -34,6 +34,7 @@ export class ProxyRateLimitGuard {
     const costCheck = await RateLimitService.checkCostLimits(key.id, "key", {
       limit_5h_usd: key.limit5hUsd,
       limit_daily_usd: key.limitDailyUsd,
+      daily_reset_mode: key.dailyResetMode,
       daily_reset_time: key.dailyResetTime,
       limit_weekly_usd: key.limitWeeklyUsd,
       limit_monthly_usd: key.limitMonthlyUsd,

+ 2 - 0
src/app/v1/_lib/proxy/response-handler.ts

@@ -823,7 +823,9 @@ async function trackCostToRedis(session: ProxySession, usage: UsageMetrics | nul
     costFloat,
     {
       keyResetTime: key.dailyResetTime,
+      keyResetMode: key.dailyResetMode,
       providerResetTime: provider.dailyResetTime,
+      providerResetMode: provider.dailyResetMode,
     }
   );
 

+ 10 - 2
src/drizzle/schema.ts

@@ -47,9 +47,13 @@ export const keys = pgTable('keys', {
   // 金额限流配置
   limit5hUsd: numeric('limit_5h_usd', { precision: 10, scale: 2 }),
   limitDailyUsd: numeric('limit_daily_usd', { precision: 10, scale: 2 }),
+  dailyResetMode: varchar('daily_reset_mode', { length: 10 })
+    .default('fixed')
+    .notNull()
+    .$type<'fixed' | 'rolling'>(), // fixed: 固定时间重置, rolling: 滚动窗口(24小时)
   dailyResetTime: varchar('daily_reset_time', { length: 5 })
     .default('00:00')
-    .notNull(), // HH:mm 格式,如 "18:00"
+    .notNull(), // HH:mm 格式,如 "18:00"(仅 fixed 模式使用)
   limitWeeklyUsd: numeric('limit_weekly_usd', { precision: 10, scale: 2 }),
   limitMonthlyUsd: numeric('limit_monthly_usd', { precision: 10, scale: 2 }),
   limitConcurrentSessions: integer('limit_concurrent_sessions').default(0),
@@ -115,9 +119,13 @@ export const providers = pgTable('providers', {
   // 金额限流配置
   limit5hUsd: numeric('limit_5h_usd', { precision: 10, scale: 2 }),
   limitDailyUsd: numeric('limit_daily_usd', { precision: 10, scale: 2 }),
+  dailyResetMode: varchar('daily_reset_mode', { length: 10 })
+    .default('fixed')
+    .notNull()
+    .$type<'fixed' | 'rolling'>(), // fixed: 固定时间重置, rolling: 滚动窗口(24小时)
   dailyResetTime: varchar('daily_reset_time', { length: 5 })
     .default('00:00')
-    .notNull(),
+    .notNull(), // HH:mm 格式,如 "18:00"(仅 fixed 模式使用)
   limitWeeklyUsd: numeric('limit_weekly_usd', { precision: 10, scale: 2 }),
   limitMonthlyUsd: numeric('limit_monthly_usd', { precision: 10, scale: 2 }),
   limitConcurrentSessions: integer('limit_concurrent_sessions').default(0),

+ 1 - 0
src/lib/auth.ts

@@ -39,6 +39,7 @@ export async function validateKey(keyString: string): Promise<AuthSession | null
       canLoginWebUi: true, // Admin Token 始终可以登录 Web UI
       limit5hUsd: null,
       limitDailyUsd: null,
+      dailyResetMode: "fixed",
       dailyResetTime: "00:00",
       limitWeeklyUsd: null,
       limitMonthlyUsd: null,

+ 181 - 25
src/lib/rate-limit/service.ts

@@ -5,20 +5,26 @@ import {
   CHECK_AND_TRACK_SESSION,
   TRACK_COST_5H_ROLLING_WINDOW,
   GET_COST_5H_ROLLING_WINDOW,
+  TRACK_COST_DAILY_ROLLING_WINDOW,
+  GET_COST_DAILY_ROLLING_WINDOW,
 } from "@/lib/redis/lua-scripts";
 import { sumUserCostToday } from "@/repository/statistics";
 import {
   getTimeRangeForPeriod,
+  getTimeRangeForPeriodWithMode,
   getTTLForPeriod,
+  getTTLForPeriodWithMode,
   getSecondsUntilMidnight,
   normalizeResetTime,
+  type DailyResetMode,
 } from "./time-utils";
 
 interface CostLimit {
   amount: number | null;
   period: "5h" | "daily" | "weekly" | "monthly";
   name: string;
-  resetTime?: string; // 自定义重置时间(仅 daily 使用,格式 "HH:mm")
+  resetTime?: string; // 自定义重置时间(仅 daily + fixed 模式使用,格式 "HH:mm")
+  resetMode?: DailyResetMode; // 日限额重置模式(仅 daily 使用)
 }
 
 export class RateLimitService {
@@ -43,11 +49,13 @@ export class RateLimitService {
       limit_5h_usd: number | null;
       limit_daily_usd: number | null;
       daily_reset_time?: string;
+      daily_reset_mode?: DailyResetMode;
       limit_weekly_usd: number | null;
       limit_monthly_usd: number | null;
     }
   ): Promise<{ allowed: boolean; reason?: string }> {
     const normalizedDailyReset = normalizeResetTime(limits.daily_reset_time);
+    const dailyResetMode = limits.daily_reset_mode ?? "fixed";
     const costLimits: CostLimit[] = [
       { amount: limits.limit_5h_usd, period: "5h", name: "5小时" },
       {
@@ -55,6 +63,7 @@ export class RateLimitService {
         period: "daily",
         name: "每日",
         resetTime: normalizedDailyReset,
+        resetMode: dailyResetMode,
       },
       { amount: limits.limit_weekly_usd, period: "weekly", name: "周" },
       { amount: limits.limit_monthly_usd, period: "monthly", name: "月" },
@@ -102,8 +111,40 @@ export class RateLimitService {
               );
               return await this.checkCostLimitsFromDatabase(id, type, costLimits);
             }
+          } else if (limit.period === "daily" && limit.resetMode === "rolling") {
+            // daily 滚动窗口:使用 ZSET + Lua 脚本
+            try {
+              const key = `${type}:${id}:cost_daily_rolling`;
+              const window24h = 24 * 60 * 60 * 1000;
+              const result = (await this.redis.eval(
+                GET_COST_DAILY_ROLLING_WINDOW,
+                1,
+                key,
+                now.toString(),
+                window24h.toString()
+              )) as string;
+
+              current = parseFloat(result || "0");
+
+              // Cache Miss 检测
+              if (current === 0) {
+                const exists = await this.redis.exists(key);
+                if (!exists) {
+                  logger.info(
+                    `[RateLimit] Cache miss for ${type}:${id}:cost_daily_rolling, querying database`
+                  );
+                  return await this.checkCostLimitsFromDatabase(id, type, costLimits);
+                }
+              }
+            } catch (error) {
+              logger.error(
+                "[RateLimit] Daily rolling window query failed, fallback to database:",
+                error
+              );
+              return await this.checkCostLimitsFromDatabase(id, type, costLimits);
+            }
           } else {
-            // daily/周/月使用普通 GET
+            // daily fixed/周/月使用普通 GET
             const { suffix } = this.resolveDailyReset(limit.resetTime);
             const periodKey = limit.period === "daily" ? `${limit.period}_${suffix}` : limit.period;
             const value = await this.redis.get(`${type}:${id}:cost_${periodKey}`);
@@ -154,8 +195,12 @@ export class RateLimitService {
     for (const limit of costLimits) {
       if (!limit.amount || limit.amount <= 0) continue;
 
-      // 计算时间范围(使用新的时间工具函数)
-      const { startTime, endTime } = getTimeRangeForPeriod(limit.period, limit.resetTime);
+      // 计算时间范围(使用支持模式的时间工具函数)
+      const { startTime, endTime } = getTimeRangeForPeriodWithMode(
+        limit.period,
+        limit.resetTime,
+        limit.resetMode
+      );
 
       // 查询数据库
       const current =
@@ -184,10 +229,30 @@ export class RateLimitService {
 
               logger.info(`[RateLimit] Cache warmed for ${key}, value=${current} (rolling window)`);
             }
+          } else if (limit.period === "daily" && limit.resetMode === "rolling") {
+            // daily 滚动窗口:使用 ZSET + Lua 脚本
+            if (current > 0) {
+              const now = Date.now();
+              const window24h = 24 * 60 * 60 * 1000;
+              const key = `${type}:${id}:cost_daily_rolling`;
+
+              await this.redis.eval(
+                TRACK_COST_DAILY_ROLLING_WINDOW,
+                1,
+                key,
+                current.toString(),
+                now.toString(),
+                window24h.toString()
+              );
+
+              logger.info(
+                `[RateLimit] Cache warmed for ${key}, value=${current} (daily rolling window)`
+              );
+            }
           } else {
-            // daily/周/月固定窗口:使用 STRING + 动态 TTL
+            // daily fixed/周/月固定窗口:使用 STRING + 动态 TTL
             const { normalized, suffix } = this.resolveDailyReset(limit.resetTime);
-            const ttl = getTTLForPeriod(limit.period, normalized);
+            const ttl = getTTLForPeriodWithMode(limit.period, normalized, limit.resetMode);
             const periodKey = limit.period === "daily" ? `${limit.period}_${suffix}` : limit.period;
             await this.redis.set(`${type}:${id}:cost_${periodKey}`, current.toString(), "EX", ttl);
             logger.info(
@@ -308,29 +373,38 @@ export class RateLimitService {
 
   /**
    * 累加消费(请求结束后调用)
-   * 5h 使用滚动窗口(ZSET),daily/周/月使用固定窗口(STRING)
+   * 5h 使用滚动窗口(ZSET),daily 根据模式选择滚动/固定窗口,周/月使用固定窗口(STRING)
    */
   static async trackCost(
     keyId: number,
     providerId: number,
     sessionId: string,
     cost: number,
-    options?: { keyResetTime?: string; providerResetTime?: string }
+    options?: {
+      keyResetTime?: string;
+      keyResetMode?: DailyResetMode;
+      providerResetTime?: string;
+      providerResetMode?: DailyResetMode;
+    }
   ): Promise<void> {
     if (!this.redis || cost <= 0) return;
 
     try {
       const keyDailyReset = this.resolveDailyReset(options?.keyResetTime);
       const providerDailyReset = this.resolveDailyReset(options?.providerResetTime);
+      const keyDailyMode = options?.keyResetMode ?? "fixed";
+      const providerDailyMode = options?.providerResetMode ?? "fixed";
       const now = Date.now();
       const window5h = 5 * 60 * 60 * 1000; // 5 hours in ms
+      const window24h = 24 * 60 * 60 * 1000; // 24 hours in ms
 
       // 计算动态 TTL(daily/周/月)
-      const ttlDailyKey = getTTLForPeriod("daily", keyDailyReset.normalized);
+      const ttlDailyKey = getTTLForPeriodWithMode("daily", keyDailyReset.normalized, keyDailyMode);
       const ttlDailyProvider =
-        keyDailyReset.normalized === providerDailyReset.normalized
+        keyDailyReset.normalized === providerDailyReset.normalized &&
+        keyDailyMode === providerDailyMode
           ? ttlDailyKey
-          : getTTLForPeriod("daily", providerDailyReset.normalized);
+          : getTTLForPeriodWithMode("daily", providerDailyReset.normalized, providerDailyMode);
       const ttlWeekly = getTTLForPeriod("weekly");
       const ttlMonthly = getTTLForPeriod("monthly");
 
@@ -355,13 +429,38 @@ export class RateLimitService {
         window5h.toString()
       );
 
-      // 2. daily/周/月固定窗口:使用 STRING + 动态 TTL
+      // 2. daily 滚动窗口:使用 Lua 脚本(ZSET)
+      if (keyDailyMode === "rolling") {
+        await this.redis.eval(
+          TRACK_COST_DAILY_ROLLING_WINDOW,
+          1,
+          `key:${keyId}:cost_daily_rolling`,
+          cost.toString(),
+          now.toString(),
+          window24h.toString()
+        );
+      }
+
+      if (providerDailyMode === "rolling") {
+        await this.redis.eval(
+          TRACK_COST_DAILY_ROLLING_WINDOW,
+          1,
+          `provider:${providerId}:cost_daily_rolling`,
+          cost.toString(),
+          now.toString(),
+          window24h.toString()
+        );
+      }
+
+      // 3. daily fixed/周/月固定窗口:使用 STRING + 动态 TTL
       const pipeline = this.redis.pipeline();
 
-      // Key 的 daily/周/月消费
-      const keyDailyKey = `key:${keyId}:cost_daily_${keyDailyReset.suffix}`;
-      pipeline.incrbyfloat(keyDailyKey, cost);
-      pipeline.expire(keyDailyKey, ttlDailyKey);
+      // Key 的 daily fixed/周/月消费
+      if (keyDailyMode === "fixed") {
+        const keyDailyKey = `key:${keyId}:cost_daily_${keyDailyReset.suffix}`;
+        pipeline.incrbyfloat(keyDailyKey, cost);
+        pipeline.expire(keyDailyKey, ttlDailyKey);
+      }
 
       pipeline.incrbyfloat(`key:${keyId}:cost_weekly`, cost);
       pipeline.expire(`key:${keyId}:cost_weekly`, ttlWeekly);
@@ -369,10 +468,12 @@ export class RateLimitService {
       pipeline.incrbyfloat(`key:${keyId}:cost_monthly`, cost);
       pipeline.expire(`key:${keyId}:cost_monthly`, ttlMonthly);
 
-      // Provider 的 daily/周/月消费
-      const providerDailyKey = `provider:${providerId}:cost_daily_${providerDailyReset.suffix}`;
-      pipeline.incrbyfloat(providerDailyKey, cost);
-      pipeline.expire(providerDailyKey, ttlDailyProvider);
+      // Provider 的 daily fixed/周/月消费
+      if (providerDailyMode === "fixed") {
+        const providerDailyKey = `provider:${providerId}:cost_daily_${providerDailyReset.suffix}`;
+        pipeline.incrbyfloat(providerDailyKey, cost);
+        pipeline.expire(providerDailyKey, ttlDailyProvider);
+      }
 
       pipeline.incrbyfloat(`provider:${providerId}:cost_weekly`, cost);
       pipeline.expire(`provider:${providerId}:cost_weekly`, ttlWeekly);
@@ -397,7 +498,8 @@ export class RateLimitService {
     id: number,
     type: "key" | "provider",
     period: "5h" | "daily" | "weekly" | "monthly",
-    resetTime = "00:00"
+    resetTime = "00:00",
+    resetMode: DailyResetMode = "fixed"
   ): Promise<number> {
     try {
       const dailyResetInfo = this.resolveDailyReset(resetTime);
@@ -434,8 +536,38 @@ export class RateLimitService {
             // Key 存在但值为 0,说明真的是 0
             return 0;
           }
+        } else if (period === "daily" && resetMode === "rolling") {
+          // daily 滚动窗口:使用 ZSET + Lua 脚本
+          const now = Date.now();
+          const window24h = 24 * 60 * 60 * 1000;
+          const key = `${type}:${id}:cost_daily_rolling`;
+
+          const result = (await this.redis.eval(
+            GET_COST_DAILY_ROLLING_WINDOW,
+            1,
+            key,
+            now.toString(),
+            window24h.toString()
+          )) as string;
+
+          current = parseFloat(result || "0");
+
+          // Cache Hit
+          if (current > 0) {
+            return current;
+          }
+
+          // Cache Miss 检测
+          const exists = await this.redis.exists(key);
+          if (!exists) {
+            logger.info(
+              `[RateLimit] Cache miss for ${type}:${id}:cost_daily_rolling, querying database`
+            );
+          } else {
+            return 0;
+          }
         } else {
-          // daily/周/月使用普通 GET
+          // daily fixed/周/月使用普通 GET
           const redisKey = period === "daily" ? `${period}_${dailyResetInfo.suffix}` : period;
           const value = await this.redis.get(`${type}:${id}:cost_${redisKey}`);
 
@@ -458,7 +590,11 @@ export class RateLimitService {
         "@/repository/statistics"
       );
 
-      const { startTime, endTime } = getTimeRangeForPeriod(period, dailyResetInfo.normalized);
+      const { startTime, endTime } = getTimeRangeForPeriodWithMode(
+        period,
+        dailyResetInfo.normalized,
+        resetMode
+      );
       const current =
         type === "key"
           ? await sumKeyCostInTimeRange(id, startTime, endTime)
@@ -487,10 +623,30 @@ export class RateLimitService {
 
               logger.info(`[RateLimit] Cache warmed for ${key}, value=${current} (rolling window)`);
             }
+          } else if (period === "daily" && resetMode === "rolling") {
+            // daily 滚动窗口:使用 ZSET + Lua 脚本
+            if (current > 0) {
+              const now = Date.now();
+              const window24h = 24 * 60 * 60 * 1000;
+              const key = `${type}:${id}:cost_daily_rolling`;
+
+              await this.redis.eval(
+                TRACK_COST_DAILY_ROLLING_WINDOW,
+                1,
+                key,
+                current.toString(),
+                now.toString(),
+                window24h.toString()
+              );
+
+              logger.info(
+                `[RateLimit] Cache warmed for ${key}, value=${current} (daily rolling window)`
+              );
+            }
           } else {
-            // daily/周/月固定窗口:使用 STRING + 动态 TTL
+            // daily fixed/周/月固定窗口:使用 STRING + 动态 TTL
             const redisKey = period === "daily" ? `${period}_${dailyResetInfo.suffix}` : period;
-            const ttl = getTTLForPeriod(period, dailyResetInfo.normalized);
+            const ttl = getTTLForPeriodWithMode(period, dailyResetInfo.normalized, resetMode);
             await this.redis.set(`${type}:${id}:cost_${redisKey}`, current.toString(), "EX", ttl);
             logger.info(
               `[RateLimit] Cache warmed for ${type}:${id}:cost_${redisKey}, value=${current}, ttl=${ttl}s`

+ 61 - 0
src/lib/rate-limit/time-utils.ts

@@ -18,6 +18,7 @@ import { toZonedTime, fromZonedTime } from "date-fns-tz";
 import { getEnvConfig } from "@/lib/config";
 
 export type TimePeriod = "5h" | "daily" | "weekly" | "monthly";
+export type DailyResetMode = "fixed" | "rolling";
 
 export interface TimeRange {
   startTime: Date;
@@ -78,6 +79,30 @@ export function getTimeRangeForPeriod(period: TimePeriod, resetTime = "00:00"):
   return { startTime, endTime };
 }
 
+/**
+ * 根据周期和模式计算时间范围(支持滚动窗口模式)
+ * - daily + rolling: 滚动窗口(过去 24 小时)
+ * - daily + fixed: 固定时间重置(使用 resetTime)
+ * - 其他周期:使用原有逻辑
+ */
+export function getTimeRangeForPeriodWithMode(
+  period: TimePeriod,
+  resetTime = "00:00",
+  mode: DailyResetMode = "fixed"
+): TimeRange {
+  if (period === "daily" && mode === "rolling") {
+    // 滚动窗口:过去 24 小时
+    const now = new Date();
+    return {
+      startTime: new Date(now.getTime() - 24 * 60 * 60 * 1000),
+      endTime: now,
+    };
+  }
+
+  // 其他情况使用原有逻辑
+  return getTimeRangeForPeriod(period, resetTime);
+}
+
 /**
  * 根据周期计算 Redis Key 的 TTL(秒)
  * - 5h: 5 小时(固定)
@@ -121,6 +146,24 @@ export function getTTLForPeriod(period: TimePeriod, resetTime = "00:00"): number
   }
 }
 
+/**
+ * 根据周期和模式计算 Redis Key 的 TTL(秒)
+ * - daily + rolling: 24 小时(固定)
+ * - daily + fixed: 到下一个自定义重置时间的秒数
+ * - 其他周期:使用原有逻辑
+ */
+export function getTTLForPeriodWithMode(
+  period: TimePeriod,
+  resetTime = "00:00",
+  mode: DailyResetMode = "fixed"
+): number {
+  if (period === "daily" && mode === "rolling") {
+    return 24 * 3600; // 24 小时
+  }
+
+  return getTTLForPeriod(period, resetTime);
+}
+
 /**
  * 获取重置信息(用于前端展示)
  */
@@ -170,6 +213,24 @@ export function getResetInfo(period: TimePeriod, resetTime = "00:00"): ResetInfo
   }
 }
 
+/**
+ * 获取重置信息(支持滚动窗口模式)
+ */
+export function getResetInfoWithMode(
+  period: TimePeriod,
+  resetTime = "00:00",
+  mode: DailyResetMode = "fixed"
+): ResetInfo {
+  if (period === "daily" && mode === "rolling") {
+    return {
+      type: "rolling",
+      period: "24 小时",
+    };
+  }
+
+  return getResetInfo(period, resetTime);
+}
+
 function getCustomDailyResetTime(now: Date, resetTime: string, timezone: string): Date {
   const { hours, minutes } = parseResetTime(resetTime);
   const zonedNow = toZonedTime(now, timezone);

+ 80 - 0
src/lib/redis/lua-scripts.ts

@@ -180,3 +180,83 @@ end
 
 return tostring(total)
 `;
+
+/**
+ * 追踪 24小时滚动窗口消费(使用 ZSET)
+ *
+ * 功能:
+ * 1. 清理 24 小时前的消费记录
+ * 2. 添加当前消费记录(带时间戳)
+ * 3. 计算当前窗口内的总消费
+ * 4. 设置兜底 TTL(25 小时)
+ *
+ * KEYS[1]: key:${id}:cost_daily_rolling 或 provider:${id}:cost_daily_rolling
+ * ARGV[1]: cost(本次消费金额)
+ * ARGV[2]: now(当前时间戳,毫秒)
+ * ARGV[3]: window(窗口时长,毫秒,默认 86400000 = 24小时)
+ *
+ * 返回值:string - 当前窗口内的总消费
+ */
+export const TRACK_COST_DAILY_ROLLING_WINDOW = `
+local key = KEYS[1]
+local cost = tonumber(ARGV[1])
+local now_ms = tonumber(ARGV[2])
+local window_ms = tonumber(ARGV[3])  -- 24 hours = 86400000 ms
+
+-- 1. 清理过期记录(24 小时前的数据)
+redis.call('ZREMRANGEBYSCORE', key, '-inf', now_ms - window_ms)
+
+-- 2. 添加当前消费记录(member = timestamp:cost,便于调试和追踪)
+local member = now_ms .. ':' .. cost
+redis.call('ZADD', key, now_ms, member)
+
+-- 3. 计算窗口内总消费
+local records = redis.call('ZRANGE', key, 0, -1)
+local total = 0
+for _, record in ipairs(records) do
+  -- 解析 member 格式:"timestamp:cost"
+  local cost_str = string.match(record, ':(.+)')
+  if cost_str then
+    total = total + tonumber(cost_str)
+  end
+end
+
+-- 4. 设置兜底 TTL(25 小时,防止数据永久堆积)
+redis.call('EXPIRE', key, 90000)
+
+return tostring(total)
+`;
+
+/**
+ * 查询 24小时滚动窗口当前消费
+ *
+ * 功能:
+ * 1. 清理 24 小时前的消费记录
+ * 2. 计算当前窗口内的总消费
+ *
+ * KEYS[1]: key:${id}:cost_daily_rolling 或 provider:${id}:cost_daily_rolling
+ * ARGV[1]: now(当前时间戳,毫秒)
+ * ARGV[2]: window(窗口时长,毫秒,默认 86400000 = 24小时)
+ *
+ * 返回值:string - 当前窗口内的总消费
+ */
+export const GET_COST_DAILY_ROLLING_WINDOW = `
+local key = KEYS[1]
+local now_ms = tonumber(ARGV[1])
+local window_ms = tonumber(ARGV[2])  -- 24 hours = 86400000 ms
+
+-- 1. 清理过期记录
+redis.call('ZREMRANGEBYSCORE', key, '-inf', now_ms - window_ms)
+
+-- 2. 计算窗口内总消费
+local records = redis.call('ZRANGE', key, 0, -1)
+local total = 0
+for _, record in ipairs(records) do
+  local cost_str = string.match(record, ':(.+)')
+  if cost_str then
+    total = total + tonumber(cost_str)
+  end
+end
+
+return tostring(total)
+`;

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

@@ -70,6 +70,7 @@ export const KeyFormSchema = z.object({
     .max(10000, "每日消费上限不能超过10000美元")
     .nullable()
     .optional(),
+  dailyResetMode: z.enum(["fixed", "rolling"]).optional().default("fixed"),
   dailyResetTime: z
     .string()
     .regex(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, "重置时间格式必须为 HH:mm")
@@ -147,6 +148,7 @@ export const CreateProviderSchema = z.object({
     .max(10000, "每日消费上限不能超过10000美元")
     .nullable()
     .optional(),
+  daily_reset_mode: z.enum(["fixed", "rolling"]).optional().default("fixed"),
   daily_reset_time: z
     .string()
     .regex(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, "重置时间格式必须为 HH:mm")

+ 3 - 0
src/repository/key.ts

@@ -72,6 +72,7 @@ export async function createKey(keyData: CreateKeyData): Promise<Key> {
     canLoginWebUi: keyData.can_login_web_ui ?? true,
     limit5hUsd: keyData.limit_5h_usd != null ? keyData.limit_5h_usd.toString() : null,
     limitDailyUsd: keyData.limit_daily_usd != null ? keyData.limit_daily_usd.toString() : null,
+    dailyResetMode: keyData.daily_reset_mode ?? "fixed",
     dailyResetTime: keyData.daily_reset_time ?? "00:00",
     limitWeeklyUsd: keyData.limit_weekly_usd != null ? keyData.limit_weekly_usd.toString() : null,
     limitMonthlyUsd:
@@ -89,6 +90,7 @@ export async function createKey(keyData: CreateKeyData): Promise<Key> {
     canLoginWebUi: keys.canLoginWebUi,
     limit5hUsd: keys.limit5hUsd,
     limitDailyUsd: keys.limitDailyUsd,
+    dailyResetMode: keys.dailyResetMode,
     dailyResetTime: keys.dailyResetTime,
     limitWeeklyUsd: keys.limitWeeklyUsd,
     limitMonthlyUsd: keys.limitMonthlyUsd,
@@ -119,6 +121,7 @@ export async function updateKey(id: number, keyData: UpdateKeyData): Promise<Key
   if (keyData.limit_daily_usd !== undefined)
     dbData.limitDailyUsd =
       keyData.limit_daily_usd != null ? keyData.limit_daily_usd.toString() : null;
+  if (keyData.daily_reset_mode !== undefined) dbData.dailyResetMode = keyData.daily_reset_mode;
   if (keyData.daily_reset_time !== undefined) dbData.dailyResetTime = keyData.daily_reset_time;
   if (keyData.limit_weekly_usd !== undefined)
     dbData.limitWeeklyUsd =

+ 3 - 0
src/repository/provider.ts

@@ -27,6 +27,7 @@ export async function createProvider(providerData: CreateProviderData): Promise<
     limit5hUsd: providerData.limit_5h_usd != null ? providerData.limit_5h_usd.toString() : null,
     limitDailyUsd:
       providerData.limit_daily_usd != null ? providerData.limit_daily_usd.toString() : null,
+    dailyResetMode: providerData.daily_reset_mode ?? "fixed",
     dailyResetTime: providerData.daily_reset_time ?? "00:00",
     limitWeeklyUsd:
       providerData.limit_weekly_usd != null ? providerData.limit_weekly_usd.toString() : null,
@@ -223,6 +224,8 @@ export async function updateProvider(
   if (providerData.limit_daily_usd !== undefined)
     dbData.limitDailyUsd =
       providerData.limit_daily_usd != null ? providerData.limit_daily_usd.toString() : null;
+  if (providerData.daily_reset_mode !== undefined)
+    dbData.dailyResetMode = providerData.daily_reset_mode;
   if (providerData.daily_reset_time !== undefined)
     dbData.dailyResetTime = providerData.daily_reset_time;
   if (providerData.limit_weekly_usd !== undefined)

+ 3 - 0
src/types/key.ts

@@ -15,6 +15,7 @@ export interface Key {
   // 金额限流配置
   limit5hUsd: number | null;
   limitDailyUsd: number | null;
+  dailyResetMode: "fixed" | "rolling";
   dailyResetTime: string; // HH:mm 格式
   limitWeeklyUsd: number | null;
   limitMonthlyUsd: number | null;
@@ -39,6 +40,7 @@ export interface CreateKeyData {
   // 金额限流配置
   limit_5h_usd?: number | null;
   limit_daily_usd?: number | null;
+  daily_reset_mode?: "fixed" | "rolling";
   daily_reset_time?: string;
   limit_weekly_usd?: number | null;
   limit_monthly_usd?: number | null;
@@ -57,6 +59,7 @@ export interface UpdateKeyData {
   // 金额限流配置
   limit_5h_usd?: number | null;
   limit_daily_usd?: number | null;
+  daily_reset_mode?: "fixed" | "rolling";
   daily_reset_time?: string;
   limit_weekly_usd?: number | null;
   limit_monthly_usd?: number | null;

+ 4 - 0
src/types/provider.ts

@@ -39,6 +39,7 @@ export interface Provider {
   // 金额限流配置
   limit5hUsd: number | null;
   limitDailyUsd: number | null;
+  dailyResetMode: "fixed" | "rolling";
   dailyResetTime: string;
   limitWeeklyUsd: number | null;
   limitMonthlyUsd: number | null;
@@ -96,6 +97,7 @@ export interface ProviderDisplay {
   // 金额限流配置
   limit5hUsd: number | null;
   limitDailyUsd: number | null;
+  dailyResetMode: "fixed" | "rolling";
   dailyResetTime: string;
   limitWeeklyUsd: number | null;
   limitMonthlyUsd: number | null;
@@ -148,6 +150,7 @@ export interface CreateProviderData {
   // 金额限流配置
   limit_5h_usd?: number | null;
   limit_daily_usd?: number | null;
+  daily_reset_mode?: "fixed" | "rolling";
   daily_reset_time?: string;
   limit_weekly_usd?: number | null;
   limit_monthly_usd?: number | null;
@@ -201,6 +204,7 @@ export interface UpdateProviderData {
   // 金额限流配置
   limit_5h_usd?: number | null;
   limit_daily_usd?: number | null;
+  daily_reset_mode?: "fixed" | "rolling";
   daily_reset_time?: string;
   limit_weekly_usd?: number | null;
   limit_monthly_usd?: number | null;