api: migrate usage stats from KV to db (#1739)
This commit is contained in:
@@ -4,6 +4,7 @@ import { corsAllMethods, runMiddleware } from '@/utils/cors';
|
||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||
import { getDailyTranslationPlanData, getUserPlan, validateUserAndToken } from '@/utils/access';
|
||||
import { ErrorCodes } from '@/services/translators';
|
||||
import { UsageStatsManager } from '@/utils/usage';
|
||||
|
||||
const DEFAULT_DEEPL_FREE_API = 'https://api-free.deepl.com/v2/translate';
|
||||
const DEFAULT_DEEPL_PRO_API = 'https://api.deepl.com/v2/translate';
|
||||
@@ -34,47 +35,36 @@ const generateCacheKey = (text: string, sourceLang: string, targetLang: string):
|
||||
return `tr:${hash}`;
|
||||
};
|
||||
|
||||
const getDailyUsageKey = (userId?: string, token?: string) => {
|
||||
let dailyUsageKey = '';
|
||||
if (userId && token) {
|
||||
const currentDate = new Date().toISOString().split('T')[0]!;
|
||||
dailyUsageKey = `daily_usage:${currentDate}:${userId}`;
|
||||
}
|
||||
return dailyUsageKey;
|
||||
};
|
||||
|
||||
const checkDailyUsage = async (
|
||||
userId: string,
|
||||
token: string,
|
||||
chars: number,
|
||||
translationsKV?: KVNamespace,
|
||||
) => {
|
||||
const usageKey = getDailyUsageKey(userId, token);
|
||||
const dailyUsage = (await translationsKV?.get(usageKey)) || '0';
|
||||
const checkDailyUsage = async (userId: string, token: string, chars: number) => {
|
||||
const { quota: dailyQuota } = getDailyTranslationPlanData(token);
|
||||
const dailyUsageChars = parseInt(dailyUsage);
|
||||
if (dailyQuota <= dailyUsageChars + chars) {
|
||||
const dailyUsage = await UsageStatsManager.getCurrentUsage(userId, 'translation_chars', 'daily');
|
||||
|
||||
if (dailyQuota <= dailyUsage + chars) {
|
||||
throw new Error(ErrorCodes.DAILY_QUOTA_EXCEEDED);
|
||||
}
|
||||
return dailyUsageChars;
|
||||
return dailyUsage;
|
||||
};
|
||||
|
||||
const updateDailyUsage = async (
|
||||
userId: string | undefined,
|
||||
token: string | undefined,
|
||||
newUsage: number,
|
||||
translationsKV?: KVNamespace,
|
||||
incrementUsage: number,
|
||||
) => {
|
||||
const usageKey = getDailyUsageKey(userId, token);
|
||||
if (!usageKey) return 0;
|
||||
if (!userId || !token) return 0;
|
||||
|
||||
try {
|
||||
const dailyUsage = (await translationsKV?.get(usageKey)) || '0';
|
||||
const newDailyUsage = parseInt(dailyUsage) + newUsage;
|
||||
await translationsKV?.put(usageKey, newDailyUsage.toString(), {
|
||||
expirationTtl: 86400 * 30,
|
||||
});
|
||||
return newDailyUsage;
|
||||
const userPlan = getUserPlan(token);
|
||||
const newUsage = await UsageStatsManager.trackUsage(
|
||||
userId,
|
||||
'translation_chars',
|
||||
incrementUsage,
|
||||
{
|
||||
plan_type: userPlan,
|
||||
source: 'deepl_api',
|
||||
},
|
||||
);
|
||||
|
||||
return newUsage;
|
||||
} catch (cacheError) {
|
||||
console.error('Update daily usage error:', cacheError);
|
||||
}
|
||||
@@ -139,7 +129,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
}
|
||||
|
||||
if (!user || !token) return res.status(401).json({ error: ErrorCodes.UNAUTHORIZED });
|
||||
await checkDailyUsage(user?.id, token, singleText.length, env['TRANSLATIONS_KV']);
|
||||
await checkDailyUsage(user?.id, token, singleText.length);
|
||||
|
||||
return await callDeepLAPI(
|
||||
singleText,
|
||||
@@ -158,7 +148,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
user?.id,
|
||||
token,
|
||||
originalCharsCount + translatedCharsCount,
|
||||
env['TRANSLATIONS_KV'],
|
||||
);
|
||||
translations.forEach((translation) => {
|
||||
if (translation && translation.text) {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { createSupabaseAdminClient } from '@/utils/supabase';
|
||||
|
||||
export const USAGE_TYPES = {
|
||||
TRANSLATION_CHARS: 'translation_chars',
|
||||
} as const;
|
||||
|
||||
export const QUOTA_TYPES = {
|
||||
DAILY: 'daily',
|
||||
MONTHLY: 'monthly',
|
||||
YEARLY: 'yearly',
|
||||
} as const;
|
||||
|
||||
export class UsageStatsManager {
|
||||
static async trackUsage(
|
||||
userId: string,
|
||||
usageType: string,
|
||||
increment: number = 1,
|
||||
metadata: Record<string, string | number> = {},
|
||||
): Promise<number> {
|
||||
try {
|
||||
const supabase = createSupabaseAdminClient();
|
||||
const { data, error } = await supabase.rpc('increment_daily_usage', {
|
||||
p_user_id: userId,
|
||||
p_usage_type: usageType,
|
||||
p_usage_date: new Date().toISOString().split('T')[0],
|
||||
p_increment: increment,
|
||||
p_metadata: metadata,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Usage tracking error:', error);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return data || 0;
|
||||
} catch (error) {
|
||||
console.error('Usage tracking failed:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static async getCurrentUsage(
|
||||
userId: string,
|
||||
usageType: string,
|
||||
period: 'daily' | 'monthly' = 'daily',
|
||||
): Promise<number> {
|
||||
try {
|
||||
const supabase = createSupabaseAdminClient();
|
||||
const { data, error } = await supabase.rpc('get_current_usage', {
|
||||
p_user_id: userId,
|
||||
p_usage_type: usageType,
|
||||
p_period: period,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Get current usage error:', error);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return data || 0;
|
||||
} catch (error) {
|
||||
console.error('Get current usage failed:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user