Skip to content

新增每日消费限额功能#161

Merged
ding113 merged 37 commits intodevfrom
feat/daily-limit
Nov 21, 2025
Merged

新增每日消费限额功能#161
ding113 merged 37 commits intodevfrom
feat/daily-limit

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Nov 21, 2025

No description provided.

ImgBotApp and others added 25 commits November 18, 2025 11:24
*Total -- 1,193.12kb -> 874.91kb (26.67%)

/public/readme/排行榜.png -- 158.35kb -> 110.57kb (30.17%)
/public/readme/日志.png -- 265.04kb -> 193.59kb (26.96%)
/public/readme/首页.png -- 365.05kb -> 269.36kb (26.21%)
/public/readme/供应商管理.png -- 404.68kb -> 301.38kb (25.53%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
…time

Add daily limit feature for keys and providers with a configurable daily reset time (HH:mm). Extend DB schema with limit_daily_usd and daily_reset_time, update rate-limit service to calculate and enforce daily limits using per-day windows with TTL based on reset time, extend time-utils to include daily period handling and next reset computations, and update transformers, repositories, and API handlers to propagate new fields. Also fix provider UI dialogs width for consistent layout.
[ImgBot] Optimize images
新增 `dailyResetMode` 配置项,允许用户选择每日限额的重置方式:
- `fixed`: 在每天固定时间点重置(默认行为)
- `rolling`: 从首次调用开始计算,24小时内滚动重置

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

同时扩展了 RateLimitService 中的时间范围和 TTL 计算方法以兼容新模式,
并引入两个新的 Lua 脚本用于处理 Redis 中的滚动窗口数据。
移除了页面组件中重复声明的 configPath 变量,该变量在代码中已经正确定义,
重复声明可能导致潜在的逻辑错误或维护问题。此修复确保了配置路径的正确性和代码的简洁性。
- 在密钥和提供商表单中添加日重置模式选择器(固定窗口/滚动窗口)
- 更新验证模式以包含新的daily_reset_mode字段
- 修复仓储层查询遗漏dailyResetMode字段的问题
- 改进日志记录以避免敏感数据泄露,仅在开发环境输出
- 为所有语言文件添加缺失的配置路径翻译key
- 支持Linux系统的配置文件路径并修正JSON代码块语法
- 统一限流服务调用以正确处理不同模式的日限额计算
- 调整返回类型使其支持滚动模式下的可选重置时间
- 更新用户界面文案以统一占位符语法
…et-time-fix-provider-page-size

feat(rate-limit): add daily per-day limit with customizable reset time and fix provider page UI
- 将 `costDaily.resetAt` 字段设为可选,处理可能不存在重置时间的情况
- 在显示重置时间前添加条件检查,避免未定义值导致的渲染错误
- 更新相关组件中的类型定义,确保类型一致性
- 修复 `formatDateDistance` 函数调用,传入当前日期作为参考时间
- 新增统一的错误响应格式文档,详细说明各种错误场景
- 添加限流排查脚本,便于调试和诊断消费限额问题
- 在供应商选择器中增加详细的限流和熔断器过滤信息
- 重构错误响应构建方法,支持错误类型代码和详细上下文信息
- 为前端和CLI客户端提供结构化的错误数据,便于友好显示和问题定位
- 添加对selectionContext中filteredProviders的空值检查
- 通过可选链操作符安全访问可能为空的属性
- 重构条件逻辑以正确处理不同错误场景
- 避免在filteredProviders为undefined时抛出异常
- 在错误详情对话框中增强限流和熔断错误的可视化展示,区分 JSON 错误和纯文本错误
- 新增"被过滤的供应商"显示区域,在成功请求时展示因限流或熔断被排除的供应商
- 改进错误消息解析逻辑,为限流、熔断和混合不可用错误提供专门的UI样式
- 更新错误处理器,在供应商不可用时记录详细的错误信息到数据库
- 优化 ProviderSelector 的错误判断逻辑,更准确地区分不同类型的不可用状态
- 添加多语言支持,为所有语言版本增加 filteredProviders 翻译字段
- 增强错误信息展示的用户体验,提供更清晰的错误原因和供应商状态说明
- 重命名 0018_square_ozymandias.sql → 0021_square_ozymandias.sql
- 重命名 0019_open_stephen_strange.sql → 0022_open_stephen_strange.sql
- 重命名 0020_nosy_synch.sql → 0023_nosy_synch.sql
- 更新 drizzle/meta/_journal.json 添加新的迁移条目
- 确保与上游 zsio/claude-code-hub 的迁移文件不冲突
- 合并 0021_square_ozymandias.sql, 0022_open_stephen_strange.sql, 0023_nosy_synch.sql 到 0021_daily_cost_limits.sql
- 包含完整的每日成本限额功能:字段添加、约束设置、重置模式
- 更新 drizzle/meta/_journal.json 移除多余条目
- 提升迁移文件可维护性和执行效率
feat 新增每日消费限额功能,完善限额日志显示
@claude claude bot added the enhancement New feature or request label Nov 21, 2025
@claude
Copy link
Contributor

claude bot commented Nov 21, 2025

代码审查报告 - PR #161 每日消费限额功能

总体评价

这是一个功能完整的新特性 PR,为 Key 和 Provider 新增了每日消费限额功能,支持两种重置模式(固定时间/滚动窗口)。代码质量较高,实现逻辑清晰。


✅ 优点

  1. 完整的功能实现

    • 支持 fixed(固定时间重置)和 rolling(24小时滚动窗口)两种模式
    • 自定义重置时间(HH:mm 格式)
    • 完善的 Redis 缓存策略和数据库降级机制
  2. 健壮的时间处理

    • 使用 date-fnsdate-fns-tz 处理时区问题
    • 正确处理跨天边界场景
  3. 原子性操作保证

    • 使用 Redis Lua 脚本保证滚动窗口操作的原子性
    • 新增 TRACK_COST_DAILY_ROLLING_WINDOWGET_COST_DAILY_ROLLING_WINDOW 脚本
  4. 数据库迁移

    • 合并为单一迁移文件 0021_daily_cost_limits.sql
    • 包含数据清理和约束设置
  5. 完善的验证逻辑

    • Zod schema 验证重置时间格式
    • 服务端验证 Key 限额不能超过用户限额
  6. 国际化支持

    • 添加了中英文翻译文件

⚠️ 建议改进

  1. 缺少 limit_daily_usd 与用户限额的对比验证

    • src/actions/keys.ts 中,只验证了 limit5hUsdlimitWeeklyUsdlimitMonthlyUsd 是否超过用户限额
    • 建议也添加 limitDailyUsd 与用户每日限额 (dailyLimitUsd) 的对比验证
  2. Redis Key 命名一致性

    • 固定模式使用 cost_daily_{suffix} 格式
    • 滚动模式使用 cost_daily_rolling 格式
    • 建议在文档中清楚说明这两种 Key 的区别
  3. 单元测试

    • 建议为 time-utils.ts 中的新函数添加单元测试,特别是:
      • getTimeRangeForPeriodWithMode
      • getTTLForPeriodWithMode
      • getResetInfoWithMode

📝 代码细节建议

src/lib/rate-limit/time-utils.ts:268-284

  • parseResetTime 函数可以考虑使用更严格的正则表达式,或者直接复用 Zod schema 中的正则

src/lib/rate-limit/service.ts:402-408

  • TTL 计算逻辑中有条件判断优化计算的代码,逻辑正确但可读性可以提升

🔒 安全性

  • ✅ 输入验证完善(Zod schema)
  • ✅ 权限检查正确(用户只能操作自己的 Key)
  • ✅ 无 SQL 注入风险

📊 性能考虑

  • ✅ 使用 Redis Pipeline 批量操作
  • ✅ Lua 脚本保证原子性
  • ✅ 合理的 TTL 设置避免数据堆积

总结

建议: 合并此 PR

代码实现质量高,功能完整,建议完善用户限额验证后合并。


🤖 Generated with Claude Code

- Fixes react/no-unescaped-entities error in api-test-button.tsx:574
- Replaced straight quotes with HTML entities (&ldquo; and &rdquo;)
- Resolves CI build failure in PR Build Check workflow
@claude
Copy link
Contributor

claude bot commented Nov 21, 2025

代码审查总结

这是一个功能完善的每日消费限额功能实现,整体质量很高。以下是详细的审查意见:

✅ 优点

  1. 架构设计合理

    • 支持两种重置模式(固定时间 + 滚动窗口),灵活性强
    • 使用 Redis Lua 脚本保证原子性操作,避免竞态条件
    • 完善的降级策略(Redis 失败时回退到数据库查询)
  2. 数据库迁移规范

    • 迁移脚本 0021_daily_cost_limits.sql 分步骤执行(添加字段 → 数据清理 → 设置约束)
    • 正确处理 NULL 值和默认值
    • 统一了 keys 和 providers 两个表的字段结构
  3. 国际化完整

    • 覆盖了 en/zh-CN/zh-TW/ja/ru 多语言
    • UI 文案清晰,易于理解
  4. UI/UX 设计友好

    • 在配额管理、供应商配置、密钥配置等多处集成
    • 提供清晰的帮助文本和示例
    • 实时显示限额使用情况

⚠️ 需要关注的问题

1. 时区处理的一致性

硬编码了 Asia/Shanghai 时区,可能不适合国际化部署。建议考虑支持用户级别或系统级别的时区配置。

2. Redis 缓存一致性

Redis 清空后可能导致短暂的限额超支窗口。建议在数据库查询后立即回写 Redis,避免多次回源。

3. 错误消息的可操作性

每日限额错误消息建议包含重置时间信息,方便用户了解何时恢复额度。

4. 数据库索引优化

建议为新增的限额字段添加部分索引,提升查询性能。

5. 类型安全性

建议使用 Drizzle 的枚举类型替代 varchar,增强 daily_reset_mode 的类型安全。

📝 测试建议

  1. 边界测试

    • 测试跨时区场景(如 23:59 → 00:01 的重置)
    • 测试 Redis 故障恢复场景
    • 测试滚动窗口的精度(24小时边界)
  2. 性能测试

    • 高并发场景下 Lua 脚本的性能
    • Redis 数据量增长后的清理效率
  3. 兼容性测试

    • 从旧版本升级后,现有用户的限额数据迁移
    • 确保所有 UI 组件正确显示新字段

🎯 总结

这是一个高质量的功能实现,代码逻辑清晰,考虑了多种边界情况。主要改进点在于时区灵活性、错误提示的友好性和数据库索引优化。

推荐合并,建议在后续迭代中逐步优化上述问题。


PR 状态: ✅ 可以合并
基础分支: ✅ 正确指向 dev
代码质量: ⭐⭐⭐⭐ (4/5)

* 所有自然时间窗口使用配置的时区(Asia/Shanghai)
*/
export function getTimeRangeForPeriod(period: TimePeriod): TimeRange {
export function getTimeRangeForPeriod(period: TimePeriod, resetTime = "00:00"): TimeRange {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

时区硬编码问题

当前时区固定为 Asia/Shanghai,可能不适合国际化部署场景。

建议

  1. 考虑支持用户级别或系统级别的时区配置
  2. 在文档中明确说明时区行为和影响范围
  3. 对于全球部署,建议提供时区配置选项

示例

// 支持用户级时区
export function getTimeRangeForPeriod(
  period: TimePeriod, 
  resetTime = "00:00",
  userTimezone?: string
): TimeRange {
  const timezone = userTimezone || getEnvConfig().TZ;
  // ...
}

// 金额限流配置
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 })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

类型安全性改进建议

当前 daily_reset_mode 使用 varchar,建议使用 Drizzle 的枚举类型增强类型安全:

// 在文件开头定义枚举
export const dailyResetModeEnum = pgEnum('daily_reset_mode', ['fixed', 'rolling']);

// 使用枚举替代 varchar
dailyResetMode: dailyResetModeEnum('daily_reset_mode').default('fixed').notNull()

优点

  1. 数据库层面约束,防止非法值插入
  2. TypeScript 类型推导更准确
  3. 迁移时自动验证数据合法性
  4. 更好的 IDE 自动补全支持


-- Step 1: 添加基础字段
ALTER TABLE "keys" ADD COLUMN "limit_daily_usd" numeric(10, 2);--> statement-breakpoint
ALTER TABLE "keys" ADD COLUMN "daily_reset_time" varchar(5) DEFAULT '00:00';--> statement-breakpoint
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

性能优化建议:添加部分索引

对于有限额的记录,建议添加部分索引提升查询性能:

-- 建议在下一次迁移中添加
CREATE INDEX idx_keys_daily_limit 
ON keys(limit_daily_usd, daily_reset_mode) 
WHERE limit_daily_usd IS NOT NULL;

CREATE INDEX idx_providers_daily_limit 
ON providers(limit_daily_usd, daily_reset_mode) 
WHERE limit_daily_usd IS NOT NULL;

收益

  • 查询有限额的 keys/providers 时性能提升
  • 索引只覆盖有限额的记录,空间占用小
  • 支持复合查询(同时过滤限额值和重置模式)

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @ding113, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求引入了每日消费限额功能,允许用户和提供商设置每日最大消费金额,并可选择固定时间或24小时滚动窗口作为重置模式。这一功能通过更新数据库架构、前端界面和后端限流逻辑实现,旨在提供更精细的资源使用控制和更透明的错误反馈机制。

Highlights

  • 新增每日消费限额功能: 为API密钥和提供商引入了每日消费限额,支持固定时间重置和24小时滚动窗口重置模式,增强了资源管理能力。
  • 数据库架构更新: 在keysproviders表中添加了limit_daily_usd(每日消费限额)、daily_reset_time(每日重置时间)和daily_reset_mode(每日重置模式)字段,并创建了相应的Drizzle迁移文件。
  • 前端UI更新: 仪表盘、配额管理和提供商设置页面均已更新,以支持配置和显示每日消费限额及重置时间。同时,错误详情对话框现在能更详细地展示因限流或熔断而被过滤的提供商信息。
  • 限流逻辑优化: 后端限流服务(RateLimitService)进行了重大更新,集成了每日限额的检查和追踪逻辑,包括新的Redis Lua脚本来高效处理每日滚动窗口限额。
  • 错误响应增强: 代理错误响应现在包含更详细的错误类型(code)和具体细节(details),尤其是在所有上游提供商因限流或熔断而不可用时,能提供更清晰的诊断信息。
  • 依赖更新: 更新了bun.lock文件中的多个依赖项,包括@hono/zod-openapi@hono/zod-validator@scalar/core@scalar/hono-api-reference@scalar/types@tanstack/query-core@tanstack/react-query@types/react@typescript-eslint/*系列、antdbaseline-browser-mappingcaniuse-litecontent-dispositioncore-jscore-js-compatcsstypeelectron-to-chromiumjspdfreact-hook-form等,并移除了safe-buffer@scalar/openapi-types/zod
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

本次 PR 成功地引入了每日消费限额功能。相关改动全面,覆盖了数据库结构、后端限流逻辑以及前端的配置和展示界面。固定时间和滚动窗口两种每日限额模式的实现考虑周全,特别是使用 Lua 脚本来高效操作 Redis。此外,针对供应商不可用情况的错误报告增强,是可观察性方面的一大改进。

我发现了一些可以改进的地方,主要是在密钥的 dailyResetMode 编辑功能上存在缺失,以及日志展示中一处轻微的国际化问题。具体细节请见评论。

Comment on lines 157 to 164
<TextField
label={t("dailyResetTime.label")}
placeholder={t("dailyResetTime.placeholder")}
description={t("dailyResetTime.description")}
type="time"
step={60}
{...form.getFieldProps("dailyResetTime")}
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

编辑密钥表单缺少对 dailyResetMode(每日重置模式)的编辑功能。当前用户只能在创建密钥时设置该模式,之后无法修改。建议参考 add-key-form.tsx 中的实现,为编辑表单也添加 dailyResetMode 的选择器。

<div className="flex-1">
<span className="font-medium">{p.name}</span>
<span className="text-xs ml-2">
({p.reason === 'rate_limited' ? '供应商费用限制' : '熔断器打开'})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

此处硬编码了错误原因的显示文本('供应商费用限制' 和 '熔断器打开')。为了更好的可维护性和国际化,建议将这些字符串移入 i18n 翻译文件中,并使用 t 函数进行翻译。

Suggested change
({p.reason === 'rate_limited' ? '供应商费用限制' : '熔断器打开'})
({t(`logs.details.reasons.${p.reason === 'rate_limited' ? 'rateLimited' : 'circuitOpen'}`)})

Comment on lines 202 to 215
{/* 每日重置时间 */}
<div className="grid gap-1.5">
<Label htmlFor="dailyResetTime" className="text-xs">
{t("dailyResetTime.label")}
</Label>
<Input
id="dailyResetTime"
type="time"
step={60}
value={resetTime}
onChange={(e) => setResetTime(e.target.value || "00:00")}
className="h-9"
/>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

此对话框同样缺少对 dailyResetMode(每日重置模式)的编辑功能。用户在此处无法切换每日限额的重置模式(固定时间或滚动窗口)。建议添加一个选择器来允许用户修改此设置。

ding113 and others added 8 commits November 21, 2025 23:35
- Add validation to ensure Key's daily limit does not exceed user's daily quota
- Apply validation in both addKey and editKey functions
- Add dailyResetMode parameter to editKey function signature
- Addresses code review feedback from PR #161
- Add dailyResetMode field (fixed/rolling) to EditKeyFormProps
- Import and implement Select component for reset mode selection
- Conditionally display dailyResetTime input only for 'fixed' mode
- Add i18n translations for dailyResetMode in en and zh-CN locales
- Maintain consistency with add-key-form implementation
- Addresses code review feedback from PR #161
- Add logs.details.reasons.rateLimited and circuitOpen keys to all locales
- Replace hardcoded Chinese text with t() function in error-details-dialog
- Support 5 languages: en, zh-CN, zh-TW, ja, ru
- Improves maintainability and internationalization
- Addresses code review feedback from PR #161
- Add dailyResetMode field to EditKeyQuotaDialogProps interface
- Implement Select component for reset mode selection (fixed/rolling)
- Conditionally display dailyResetTime input only for 'fixed' mode
- Include dailyResetMode in both edit and clear quota operations
- Add dynamic description text based on selected mode
- Update KeyWithQuota interface to include dailyResetMode field
- Addresses code review feedback from PR #161
- Add pgEnum definition for daily_reset_mode with values ['fixed', 'rolling']
- Replace varchar type with dailyResetModeEnum in keys and providers tables
- Maintain .default('fixed').notNull() constraints
- Generate migration 0022_simple_stardust.sql for type conversion
- Benefits: database-level constraint, better TypeScript inference, automatic validation
- Addresses code review feedback from PR #161
- Add idx_keys_daily_limit index on keys(limit_daily_usd, daily_reset_mode)
- Add idx_providers_daily_limit index on providers(limit_daily_usd, daily_reset_mode)
- Use partial indexes (WHERE limit_daily_usd IS NOT NULL) for efficiency
- Benefits: faster queries for records with daily limits, smaller index size
- Supports compound queries filtering by both limit and reset mode
- Addresses code review feedback from PR #161
- Add Redis Key Architecture section to CLAUDE.md
- Document fixed vs rolling window naming conventions
- Explain why fixed mode needs suffix and rolling mode doesn't
- Add detailed examples for all time periods (daily/5h/weekly/monthly)
- Include debugging commands and troubleshooting guide
- Add inline documentation in rate-limit/service.ts
- Benefits: easier debugging, better maintainability, clearer design rationale
- Addresses code review feedback from PR #161
@claude
Copy link
Contributor

claude bot commented Nov 21, 2025

代码审查报告

感谢提交这个全面的每日消费限额功能!这是一个非常有价值的增强功能。经过详细审查,代码质量很高,架构设计合理。以下是详细的审查意见:

✅ 优点

  1. 架构设计优秀

    • 清晰区分了固定窗口(STRING)和滚动窗口(ZSET)两种模式
    • Redis Key 命名规范设计合理,解决了多用户不同重置时间的冲突问题
    • 使用 Lua 脚本保证原子性操作,避免竞态条件
    • Fail Open 降级策略确保 Redis 故障不影响服务可用性
  2. 文档完善

    • CLAUDE.md 中新增了 165 行详细的 Redis Key 架构文档
    • 代码注释详尽(service.ts 前 66 行都是注释说明)
    • 包含调试和故障排查指南
  3. 数据库设计规范

    • 使用 PostgreSQL Enum (daily_reset_mode) 提供类型安全
    • 添加了部分索引优化查询性能(migration 0023)
    • 字段设计向后兼容(nullable for backward compatibility)
  4. 国际化支持

    • 完整的多语言翻译(中文、英文、日文、俄文)
    • UI 组件提供友好的用户体验
  5. 代码质量

    • TypeScript 类型定义完整
    • 时间处理使用 date-fns 和 date-fns-tz,正确处理时区
    • 良好的错误处理和日志记录

🔍 需要关注的点

1. 数据库迁移文件版本跳号

drizzle/0021_daily_cost_limits.sql  ✅
drizzle/0022_simple_stardust.sql    ✅
drizzle/0023_daily_limit_partial_indexes.sql  ✅
drizzle/meta/0018_snapshot.json     ⚠️ 
drizzle/meta/0019_snapshot.json     ⚠️ 
drizzle/meta/0020_snapshot.json     ⚠️ 
drizzle/meta/0022_snapshot.json     ⚠️  (缺少 0021?)

建议:确认 0021_snapshot.json 是否应该存在,或者是否是有意跳过的。

2. 时间工具函数的边界情况

time-utils.ts:234-245 中:

function getCustomDailyResetTime(now: Date, resetTime: string, timezone: string): Date {
  const { hours, minutes } = parseResetTime(resetTime);
  const zonedNow = toZonedTime(now, timezone);
  const zonedResetToday = buildZonedDate(zonedNow, hours, minutes);
  const resetToday = fromZonedTime(zonedResetToday, timezone);

  if (now >= resetToday) {
    return resetToday;  // ✅ 今天已过重置时间,返回今天的重置时间
  }

  return addDays(resetToday, -1);  // ⚠️ 今天未到重置时间,返回昨天的重置时间
}

问题:在跨越夏令时(DST)边界时,addDays(resetToday, -1) 可能产生意外结果,因为 resetToday 已经是 UTC 时间戳。

建议

// 在时区时间上操作,然后转换回 UTC
const zonedYesterday = addDays(zonedResetToday, -1);
return fromZonedTime(zonedYesterday, timezone);

3. Lua 脚本错误处理

lua-scripts.ts 中新增的 Lua 脚本需要确保:

  • ZSET 操作失败时有适当的错误返回
  • 时间戳边界值(如 0、负数)的处理

建议:在 PR 合并前进行集成测试,特别是:

  • 跨越重置时间的场景(如 23:59:59 → 00:00:01)
  • Redis 故障恢复场景
  • 高并发场景下的原子性验证

4. UI 组件的数据获取性能

edit-key-quota-dialog.tsx:91 和相关组件中,如果用户/Key 数量很多,可能需要考虑:

  • 分页加载
  • 虚拟滚动
  • 搜索防抖(代码中已使用 use-debounce,很好!)

🎯 测试建议

  1. 单元测试(缺失)

    • time-utils.ts 的时间计算逻辑
    • normalizeResetTime 的边界情况
    • TTL 计算的正确性
  2. 集成测试

    • 固定窗口和滚动窗口的切换
    • Redis Key 的正确性(包含 suffix)
    • 跨越重置时间的行为
  3. 压力测试

    • 高并发场景下的原子性
    • Redis Lua 脚本的性能

📝 代码风格建议

  1. 类型导出(src/types/key.ts:9)

    export type DailyResetMode = 'fixed' | 'rolling';  // ✅ 已定义

    建议在 src/types/quota.ts 中统一所有 quota 相关类型。

  2. 魔法数字

    const window = 24 * 60 * 60 * 1000;  // 24 小时

    建议提取为常量:

    const ONE_DAY_MS = 24 * 60 * 60 * 1000;
    const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;

✨ 总结

这是一个高质量的 PR,代码设计合理,文档详尽,考虑了向后兼容性和故障降级。主要关注点是时间处理的边界情况和缺失的测试覆盖。

建议的合并前检查清单

  • 确认数据库迁移文件版本号一致性
  • 修复 getCustomDailyResetTime 的 DST 边界问题
  • 添加时间工具函数的单元测试
  • 进行跨越重置时间的集成测试
  • 验证 Redis Lua 脚本的错误处理

整体来说,这个功能为系统增加了非常有价值的灵活性,允许用户根据实际需求选择不同的限额模式。代码质量和架构设计都很出色!👏

return resetToday;
}

return addDays(resetToday, -1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 潜在的夏令时(DST)边界问题

当前代码在 UTC 时间戳上执行 addDays(resetToday, -1),在跨越夏令时边界时可能产生意外结果。

建议修复

// 在时区时间上操作日期,然后转换回 UTC
const zonedYesterday = addDays(zonedResetToday, -1);
return fromZonedTime(zonedYesterday, timezone);

这样可以确保日期计算在正确的时区上下文中进行,避免 DST 切换导致的小时偏移问题。

// 金额限流配置
limit5hUsd: numeric('limit_5h_usd', { precision: 10, scale: 2 }),
limitDailyUsd: numeric('limit_daily_usd', { precision: 10, scale: 2 }),
dailyResetMode: dailyResetModeEnum('daily_reset_mode')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

类型安全的 Enum 设计

使用 PostgreSQL Enum 而不是 VARCHAR 约束,提供了:

  1. 数据库层面的类型检查:无效值会被数据库拒绝
  2. 更好的性能:Enum 在数据库内部存储为整数
  3. 清晰的 API 契约:TypeScript 类型直接映射到数据库 Enum

这是最佳实践!🎯

注意:如果未来需要添加新的模式(如 weekly_rolling),需要执行 ALTER TYPE 迁移:

ALTER TYPE daily_reset_mode ADD VALUE 'weekly_rolling';

@@ -206,6 +206,171 @@ src/
4. **Redis Lua 脚本** - 原子性检查和递增(解决竞态条件)
5. **Fail Open 策略** - Redis 不可用时降级,不影响服务
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📚 优秀的文档实践

这段 Redis Key 命名规范文档非常详尽,包含了:

  • 核心设计原则和权衡
  • 具体的 Key 命名模式和示例
  • 数据结构对比表
  • 调试和监控指南
  • 常见问题排查

这对于后续的运维和故障排查至关重要。建议将这段文档也同步到项目的 Wiki 或专门的运维手册中,便于团队成员查阅。

The previous CI run (19575549792) started at 15:38:51 UTC but the
format fix commit (f9272f3) was pushed at 15:39:17 UTC. This empty
commit triggers a new CI run with the corrected code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Changed trailing period from ASCII `.` to Chinese `。` for consistency
with the rest of the Chinese text in the documentation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants