Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions src/actions/my-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -199,6 +198,18 @@ export async function getMyQuota(): Promise<ActionResult<MyUsageQuota>> {
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,
Expand Down Expand Up @@ -226,7 +237,8 @@ export async function getMyQuota(): Promise<ActionResult<MyUsageQuota>> {
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"),
Expand Down Expand Up @@ -290,8 +302,13 @@ export async function getMyTodayStats(): Promise<ActionResult<MyTodayStats>> {
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({
Expand All @@ -305,7 +322,8 @@ export async function getMyTodayStats(): Promise<ActionResult<MyTodayStats>> {
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}`
)
);

Expand All @@ -323,7 +341,8 @@ export async function getMyTodayStats(): Promise<ActionResult<MyTodayStats>> {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
16 changes: 13 additions & 3 deletions src/lib/rate-limit/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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");
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/utils/quota-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down
5 changes: 5 additions & 0 deletions src/repository/statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,11 @@ export async function getMixedStatisticsFromDB(
/**
* 查询用户今日总消费(所有 Key 的消费总和)
* 用于用户层每日限额检查(Redis 降级)
*
* DEPRECATED: 该函数使用简单的日期比较,不考虑用户的 dailyResetTime 配置。
* 请使用 sumUserCostInTimeRange() 配合 getTimeRangeForPeriodWithMode() 来获取正确的时间范围。
*
* @deprecated 使用 sumUserCostInTimeRange() 替代
*/
export async function sumUserCostToday(userId: number): Promise<number> {
const timezone = getEnvConfig().TZ;
Expand Down