diff --git a/src/actions/my-usage.ts b/src/actions/my-usage.ts index 58f538007..401aa7f0d 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, @@ -121,44 +120,25 @@ export interface MyUsageLogsResult { billingModelSource: BillingModelSource; } -function getPeriodStart(period: "5h" | "weekly" | "monthly" | "total" | "today") { - const now = new Date(); - if (period === "total") return null; - if (period === "today") { - const start = new Date(now); - start.setHours(0, 0, 0, 0); - return start; - } - if (period === "5h") { - return new Date(now.getTime() - 5 * 60 * 60 * 1000); - } - if (period === "weekly") { - return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); - } - if (period === "monthly") { - return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); - } - return null; -} - +/** + * 查询用户在指定周期内的消费 + * 使用与 Key 层级和限额检查相同的时间范围计算逻辑 + * + * @deprecated 此函数已被重构为使用统一的时间范围计算逻辑 + */ async function sumUserCost(userId: number, period: "5h" | "weekly" | "monthly" | "total") { - const conditions = [ - eq(keysTable.userId, userId), - isNull(keysTable.deletedAt), - isNull(messageRequest.deletedAt), - ]; - const start = getPeriodStart(period); - if (start) { - conditions.push(gte(messageRequest.createdAt, start)); - } + // 动态导入避免循环依赖 + const { sumUserCostInTimeRange, sumUserTotalCost } = await import("@/repository/statistics"); + const { getTimeRangeForPeriod } = await import("@/lib/rate-limit/time-utils"); - const [row] = await db - .select({ total: sql`COALESCE(sum(${messageRequest.costUsd}), 0)` }) - .from(messageRequest) - .innerJoin(keysTable, eq(messageRequest.key, keysTable.key)) - .where(and(...conditions)); + // 总消费:使用专用函数 + if (period === "total") { + return await sumUserTotalCost(userId); + } - return Number(row?.total ?? 0); + // 其他周期:使用统一的时间范围计算 + const { startTime, endTime } = getTimeRangeForPeriod(period); + return await sumUserCostInTimeRange(userId, startTime, endTime); } export async function getMyUsageMetadata(): Promise> { @@ -199,6 +179,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 +218,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 +283,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 +303,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 +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}` ) ) .groupBy(messageRequest.model, messageRequest.originalModel); diff --git a/src/actions/users.ts b/src/actions/users.ts index 86df7bdb8..3d97a9374 100644 --- a/src/actions/users.ts +++ b/src/actions/users.ts @@ -1233,7 +1233,7 @@ export async function removeUser(userId: number): Promise { export async function getUserLimitUsage(userId: number): Promise< ActionResult<{ rpm: { current: number; limit: number | null; window: "per_minute" }; - dailyCost: { current: number; limit: number | null; resetAt: Date }; + dailyCost: { current: number; limit: number | null; resetAt?: Date }; }> > { try { @@ -1260,16 +1260,23 @@ export async function getUserLimitUsage(userId: number): Promise< } // 动态导入避免循环依赖 - const { sumUserCostToday } = await import("@/repository/statistics"); - const { getDailyResetTime } = await import("@/lib/rate-limit/time-utils"); + const { sumUserCostInTimeRange } = await import("@/repository/statistics"); + const { getResetInfoWithMode, getTimeRangeForPeriodWithMode } = await import( + "@/lib/rate-limit/time-utils" + ); // 获取当前 RPM 使用情况(从 Redis) // 注意:RPM 是实时的滑动窗口,无法直接获取"当前值",这里返回 0 // 实际的 RPM 检查在请求时进行 const rpmCurrent = 0; // RPM 是动态滑动窗口,此处无法精确获取 - // 获取每日消费(直接查询数据库) - const dailyCost = await sumUserCostToday(userId); + // 获取每日消费(使用用户的 dailyResetTime 和 dailyResetMode 配置) + const resetTime = user.dailyResetTime ?? "00:00"; + const resetMode = user.dailyResetMode ?? "fixed"; + const { startTime, endTime } = getTimeRangeForPeriodWithMode("daily", resetTime, resetMode); + const dailyCost = await sumUserCostInTimeRange(userId, startTime, endTime); + const resetInfo = getResetInfoWithMode("daily", resetTime, resetMode); + const resetAt = resetInfo.resetAt; return { ok: true, @@ -1282,7 +1289,7 @@ export async function getUserLimitUsage(userId: number): Promise< dailyCost: { current: dailyCost, limit: user.dailyQuota, - resetAt: getDailyResetTime(), + resetAt, }, }, }; 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..be54a92d0 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 @@ -21,8 +21,8 @@ import { Label } from "@/components/ui/label"; 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 }; + rpm: { current: number; limit: number | null; window: "per_minute" }; + dailyCost: { current: number; limit: number | null; resetAt?: Date }; } interface EditUserQuotaDialogProps { @@ -116,7 +116,7 @@ export function EditUserQuotaDialog({

{t("rpm.current", { current: currentQuota.rpm.current, - limit: currentQuota.rpm.limit, + limit: currentQuota.rpm.limit ?? t("unlimited"), })}

)} 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..8b1938652 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 @@ -33,8 +33,8 @@ interface KeyQuota { } interface UserQuota { - rpm: { current: number; limit: number; window: "per_minute" }; - dailyCost: { current: number; limit: number; resetAt: Date }; + rpm: { current: number; limit: number | null; window: "per_minute" }; + dailyCost: { current: number; limit: number | null; 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..9609b90ad 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 @@ -24,8 +24,8 @@ interface KeyQuota { } interface UserQuota { - rpm: { current: number; limit: number; window: "per_minute" }; - dailyCost: { current: number; limit: number; resetAt: Date }; + rpm: { current: number; limit: number | null; window: "per_minute" }; + dailyCost: { current: number; limit: number | null; resetAt?: Date }; } interface KeyWithQuota { diff --git a/src/app/[locale]/dashboard/quotas/users/_components/types.ts b/src/app/[locale]/dashboard/quotas/users/_components/types.ts index 9c703c4e9..f2d4824d4 100644 --- a/src/app/[locale]/dashboard/quotas/users/_components/types.ts +++ b/src/app/[locale]/dashboard/quotas/users/_components/types.ts @@ -2,7 +2,7 @@ import type { CurrencyCode } from "@/lib/utils/currency"; export interface UserQuotaSnapshot { rpm: { current: number; limit: number | null; window: "per_minute" }; - dailyCost: { current: number; limit: number | null; resetAt: Date }; + dailyCost: { current: number; limit: number | null; resetAt?: Date }; } export interface UserKeyWithUsage { diff --git a/src/app/[locale]/dashboard/users/users-page-client.tsx b/src/app/[locale]/dashboard/users/users-page-client.tsx index 64584c2b1..eeaa7e990 100644 --- a/src/app/[locale]/dashboard/users/users-page-client.tsx +++ b/src/app/[locale]/dashboard/users/users-page-client.tsx @@ -610,9 +610,7 @@ function UsersPageContent({ currentUser }: UsersPageClientProps) { ) : (
-
- {isRefreshing ? : null} -
+
{isRefreshing ? : null}
= dailyLimitUsd) { diff --git a/src/lib/utils/quota-helpers.ts b/src/lib/utils/quota-helpers.ts index d0dd9c568..074481e92 100644 --- a/src/lib/utils/quota-helpers.ts +++ b/src/lib/utils/quota-helpers.ts @@ -14,8 +14,8 @@ export type KeyQuota = { } | null; export type UserQuota = { - rpm: { current: number; limit: number; window: "per_minute" }; - dailyCost: { current: number; limit: number; resetAt: Date }; + rpm: { current: number; limit: number | null; window: "per_minute" }; + dailyCost: { current: number; limit: number | null; resetAt?: Date }; } | null; /** @@ -57,8 +57,9 @@ export function getUsageRate(current: number, limit: number | null): number { export function isUserExceeded(userQuota: UserQuota): boolean { if (!userQuota) return false; - const rpmExceeded = userQuota.rpm.current >= userQuota.rpm.limit; - const dailyExceeded = userQuota.dailyCost.current >= userQuota.dailyCost.limit; + const rpmExceeded = userQuota.rpm.limit !== null && userQuota.rpm.current >= userQuota.rpm.limit; + const dailyExceeded = + userQuota.dailyCost.limit !== null && userQuota.dailyCost.current >= userQuota.dailyCost.limit; return rpmExceeded || dailyExceeded; } 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;