Browse Source

feat: synchronize initial data in UsageLogsSection and enhance cost tracking in RateLimitService

- Added useEffect to sync initialData in UsageLogsSection for better state management.
- Updated trackUserDailyCost method to accept optional parameters for requestId and createdAtMs, improving cost tracking accuracy.
- Refactored cost calculation logic in RateLimitService to utilize detailed cost entries for rolling window calculations.

These changes enhance data consistency and tracking capabilities across the application.
ding113 2 months ago
parent
commit
37472c1ed7

+ 8 - 0
src/app/[locale]/my-usage/_components/usage-logs-section.tsx

@@ -57,6 +57,14 @@ export function UsageLogsSection({
   const [isPending, startTransition] = useTransition();
   const [error, setError] = useState<string | null>(null);
 
+  // Sync initialData from parent when it becomes available
+  // (useState only uses initialData on first mount, not on subsequent updates)
+  useEffect(() => {
+    if (initialData) {
+      setData(initialData);
+    }
+  }, [initialData]);
+
   useEffect(() => {
     setIsModelsLoading(true);
     setIsEndpointsLoading(true);

+ 5 - 1
src/app/v1/_lib/proxy/response-handler.ts

@@ -1801,7 +1801,11 @@ async function trackCostToRedis(session: ProxySession, usage: UsageMetrics | nul
     user.id,
     costFloat,
     user.dailyResetTime,
-    user.dailyResetMode
+    user.dailyResetMode,
+    {
+      requestId: messageContext.id,
+      createdAtMs: messageContext.createdAt.getTime(),
+    }
   );
 
   // 刷新 session 时间戳(滑动窗口)

+ 23 - 5
src/lib/rate-limit/service.ts

@@ -988,8 +988,22 @@ export class RateLimitService {
               logger.info(
                 `[RateLimit] Cache miss for user:${userId}:cost_daily_rolling, querying database`
               );
-              currentCost = await sumUserCostToday(userId);
-              // Note: Cache Warming 在 rolling 模式下由 trackUserDailyCost 处理
+
+              // 导入明细查询函数
+              const { findUserCostEntriesInTimeRange } = await import("@/repository/statistics");
+
+              // 计算滚动窗口的时间范围
+              const startTime = new Date(now - window24h);
+              const endTime = new Date(now);
+
+              // 查询明细并计算总和
+              const costEntries = await findUserCostEntriesInTimeRange(userId, startTime, endTime);
+              currentCost = costEntries.reduce((sum, row) => sum + row.costUsd, 0);
+
+              // Cache Warming: 重建 ZSET
+              if (costEntries.length > 0) {
+                await RateLimitService.warmRollingCostZset(key, costEntries, 90000); // 25 hours TTL
+              }
             }
           }
         } else {
@@ -1035,12 +1049,14 @@ export class RateLimitService {
    * 累加用户今日消费(在 trackCost 后调用)
    * @param resetTime - 重置时间 (HH:mm),仅 fixed 模式使用
    * @param resetMode - 重置模式:fixed 或 rolling
+   * @param options - 可选参数:requestId 和 createdAtMs 用于与 DB 时间轴保持一致
    */
   static async trackUserDailyCost(
     userId: number,
     cost: number,
     resetTime?: string,
-    resetMode?: DailyResetMode
+    resetMode?: DailyResetMode,
+    options?: { requestId?: number; createdAtMs?: number }
   ): Promise<void> {
     if (!RateLimitService.redis || cost <= 0) return;
 
@@ -1051,8 +1067,9 @@ export class RateLimitService {
       if (mode === "rolling") {
         // Rolling 模式:使用 ZSET + Lua 脚本
         const key = `user:${userId}:cost_daily_rolling`;
-        const now = Date.now();
+        const now = options?.createdAtMs ?? Date.now();
         const window24h = 24 * 60 * 60 * 1000;
+        const requestId = options?.requestId != null ? String(options.requestId) : "";
 
         await RateLimitService.redis.eval(
           TRACK_COST_DAILY_ROLLING_WINDOW,
@@ -1060,7 +1077,8 @@ export class RateLimitService {
           key,
           cost.toString(),
           now.toString(),
-          window24h.toString()
+          window24h.toString(),
+          requestId
         );
 
         logger.debug(`[RateLimit] Tracked user daily cost (rolling): user=${userId}, cost=${cost}`);