diff --git a/src/actions/my-usage.ts b/src/actions/my-usage.ts index 58f538007..31a014f42 100644 --- a/src/actions/my-usage.ts +++ b/src/actions/my-usage.ts @@ -4,12 +4,11 @@ import { and, eq, gte, isNull, sql } from "drizzle-orm"; import { db } from "@/drizzle/db"; import { keys as keysTable, messageRequest } from "@/drizzle/schema"; import { getSession } from "@/lib/auth"; -import { getEnvConfig } from "@/lib/config"; import { logger } from "@/lib/logger"; import { RateLimitService } from "@/lib/rate-limit/service"; +import type { DailyResetMode } from "@/lib/rate-limit/time-utils"; import { SessionTracker } from "@/lib/session-tracker"; import type { CurrencyCode } from "@/lib/utils"; -import { sumUserCostToday } from "@/repository/statistics"; import { getSystemSettings } from "@/repository/system-config"; import { findUsageLogsWithDetails, @@ -199,6 +198,18 @@ export async function getMyQuota(): Promise> { const key = session.key; const user = session.user; + // 获取用户每日消费时使用用户的 dailyResetTime 和 dailyResetMode 配置 + // 导入时间工具函数 + const { getTimeRangeForPeriodWithMode } = await import("@/lib/rate-limit/time-utils"); + const { sumUserCostInTimeRange } = await import("@/repository/statistics"); + + // 计算用户每日消费的时间范围(使用用户的配置) + const userDailyTimeRange = getTimeRangeForPeriodWithMode( + "daily", + user.dailyResetTime ?? "00:00", + (user.dailyResetMode as DailyResetMode | undefined) ?? "fixed" + ); + const [ keyCost5h, keyCostDaily, @@ -226,7 +237,8 @@ export async function getMyQuota(): Promise> { getTotalUsageForKey(key.key), SessionTracker.getKeySessionCount(key.id), sumUserCost(user.id, "5h"), - sumUserCostToday(user.id), + // 修复: 使用与 Key 层级相同的时间范围逻辑来计算用户每日消费 + sumUserCostInTimeRange(user.id, userDailyTimeRange.startTime, userDailyTimeRange.endTime), sumUserCost(user.id, "weekly"), sumUserCost(user.id, "monthly"), sumUserCost(user.id, "total"), @@ -290,8 +302,13 @@ export async function getMyTodayStats(): Promise> { const billingModelSource = settings.billingModelSource; const currencyCode = settings.currencyDisplay; - const timezone = getEnvConfig().TZ || "UTC"; - const startOfDay = sql`(CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date`; + // 修复: 使用 Key 的 dailyResetTime 和 dailyResetMode 来计算时间范围 + const { getTimeRangeForPeriodWithMode } = await import("@/lib/rate-limit/time-utils"); + const timeRange = getTimeRangeForPeriodWithMode( + "daily", + session.key.dailyResetTime ?? "00:00", + (session.key.dailyResetMode as DailyResetMode | undefined) ?? "fixed" + ); const [aggregate] = await db .select({ @@ -305,7 +322,8 @@ export async function getMyTodayStats(): Promise> { and( eq(messageRequest.key, session.key.key), isNull(messageRequest.deletedAt), - sql`(${messageRequest.createdAt} AT TIME ZONE ${timezone})::date = ${startOfDay}` + gte(messageRequest.createdAt, timeRange.startTime), + sql`${messageRequest.createdAt} < ${timeRange.endTime}` ) ); @@ -323,7 +341,8 @@ export async function getMyTodayStats(): Promise> { and( eq(messageRequest.key, session.key.key), isNull(messageRequest.deletedAt), - sql`(${messageRequest.createdAt} AT TIME ZONE ${timezone})::date = ${startOfDay}` + gte(messageRequest.createdAt, timeRange.startTime), + sql`${messageRequest.createdAt} < ${timeRange.endTime}` ) ) .groupBy(messageRequest.model, messageRequest.originalModel); diff --git a/src/app/[locale]/dashboard/quotas/keys/_components/edit-user-quota-dialog.tsx b/src/app/[locale]/dashboard/quotas/keys/_components/edit-user-quota-dialog.tsx index 223fd205f..114faf946 100644 --- a/src/app/[locale]/dashboard/quotas/keys/_components/edit-user-quota-dialog.tsx +++ b/src/app/[locale]/dashboard/quotas/keys/_components/edit-user-quota-dialog.tsx @@ -22,7 +22,7 @@ import { CURRENCY_CONFIG, type CurrencyCode } from "@/lib/utils/currency"; interface UserQuota { rpm: { current: number; limit: number; window: "per_minute" }; - dailyCost: { current: number; limit: number; resetAt: Date }; + dailyCost: { current: number; limit: number; resetAt?: Date }; } interface EditUserQuotaDialogProps { diff --git a/src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-client.tsx b/src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-client.tsx index 0cbc7adb9..da477c559 100644 --- a/src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-client.tsx +++ b/src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-client.tsx @@ -34,7 +34,7 @@ interface KeyQuota { interface UserQuota { rpm: { current: number; limit: number; window: "per_minute" }; - dailyCost: { current: number; limit: number; resetAt: Date }; + dailyCost: { current: number; limit: number; resetAt?: Date }; } interface KeyWithQuota { diff --git a/src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-manager.tsx b/src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-manager.tsx index d1dd37b60..f5e396eaf 100644 --- a/src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-manager.tsx +++ b/src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-manager.tsx @@ -25,7 +25,7 @@ interface KeyQuota { interface UserQuota { rpm: { current: number; limit: number; window: "per_minute" }; - dailyCost: { current: number; limit: number; resetAt: Date }; + dailyCost: { current: number; limit: number; resetAt?: Date }; } interface KeyWithQuota { diff --git a/src/lib/rate-limit/service.ts b/src/lib/rate-limit/service.ts index 9489027bd..7bf09d82c 100644 --- a/src/lib/rate-limit/service.ts +++ b/src/lib/rate-limit/service.ts @@ -75,7 +75,7 @@ import { TRACK_COST_DAILY_ROLLING_WINDOW, } from "@/lib/redis/lua-scripts"; import { SessionTracker } from "@/lib/session-tracker"; -import { sumKeyTotalCost, sumUserCostToday, sumUserTotalCost } from "@/repository/statistics"; +import { sumKeyTotalCost, sumUserCostInTimeRange, sumUserTotalCost } from "@/repository/statistics"; import { type DailyResetMode, getTimeRangeForPeriodWithMode, @@ -1017,7 +1017,12 @@ export class RateLimitService { } else { // Cache Miss: 从数据库恢复 logger.info(`[RateLimit] Cache miss for ${key}, querying database`); - currentCost = await sumUserCostToday(userId); + const { startTime, endTime } = getTimeRangeForPeriodWithMode( + "daily", + normalizedResetTime, + mode + ); + currentCost = await sumUserCostInTimeRange(userId, startTime, endTime); // Cache Warming: 写回 Redis const ttl = getTTLForPeriodWithMode("daily", normalizedResetTime, "fixed"); @@ -1027,7 +1032,12 @@ export class RateLimitService { } else { // Slow Path: 数据库查询(Redis 不可用) logger.warn("[RateLimit] Redis unavailable, querying database for user daily cost"); - currentCost = await sumUserCostToday(userId); + const { startTime, endTime } = getTimeRangeForPeriodWithMode( + "daily", + normalizedResetTime, + mode + ); + currentCost = await sumUserCostInTimeRange(userId, startTime, endTime); } if (currentCost >= dailyLimitUsd) { diff --git a/src/lib/utils/quota-helpers.ts b/src/lib/utils/quota-helpers.ts index d0dd9c568..0ea6d4517 100644 --- a/src/lib/utils/quota-helpers.ts +++ b/src/lib/utils/quota-helpers.ts @@ -15,7 +15,7 @@ export type KeyQuota = { export type UserQuota = { rpm: { current: number; limit: number; window: "per_minute" }; - dailyCost: { current: number; limit: number; resetAt: Date }; + dailyCost: { current: number; limit: number; resetAt?: Date }; } | null; /** diff --git a/src/repository/statistics.ts b/src/repository/statistics.ts index deb6d75df..899ba5a3c 100644 --- a/src/repository/statistics.ts +++ b/src/repository/statistics.ts @@ -712,6 +712,11 @@ export async function getMixedStatisticsFromDB( /** * 查询用户今日总消费(所有 Key 的消费总和) * 用于用户层每日限额检查(Redis 降级) + * + * DEPRECATED: 该函数使用简单的日期比较,不考虑用户的 dailyResetTime 配置。 + * 请使用 sumUserCostInTimeRange() 配合 getTimeRangeForPeriodWithMode() 来获取正确的时间范围。 + * + * @deprecated 使用 sumUserCostInTimeRange() 替代 */ export async function sumUserCostToday(userId: number): Promise { const timezone = getEnvConfig().TZ;