feat: add local auth billing and usage management
This commit is contained in:
1 parent
f642b5e71f
commit
196fdde83f
119 files changed
+15695
-2650
No files matched your search
+180
@@ -0,0 +1,180 @@
|
||||
import type { GenerationCapability, GenerationProvider } from "@/lib/types";
|
||||
|
||||
export const USAGE_TIME_ZONE = "Asia/Shanghai";
|
||||
export const UNASSIGNED_ORGANIZATION_ID = "__unassigned__";
|
||||
|
||||
export type UsagePreset = "today" | "7d" | "30d" | "month";
|
||||
|
||||
export type UsageDateRange = {
|
||||
from: string;
|
||||
to: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
label: string;
|
||||
dayCount: number;
|
||||
};
|
||||
|
||||
export type UsageCountItem = {
|
||||
key: string;
|
||||
label: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type UsageRecordView = {
|
||||
id: string;
|
||||
jobId: string;
|
||||
ownerId: string;
|
||||
accountName: string;
|
||||
accountUsername?: string;
|
||||
organizationId: string;
|
||||
organizationName: string;
|
||||
capability: GenerationCapability;
|
||||
capabilityLabel: string;
|
||||
provider?: GenerationProvider;
|
||||
providerLabel: string;
|
||||
reqKey?: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type PersonalUsageReport = {
|
||||
preset: UsagePreset;
|
||||
range: UsageDateRange;
|
||||
total: number;
|
||||
byCapability: UsageCountItem[];
|
||||
recent: UsageRecordView[];
|
||||
};
|
||||
|
||||
export type UsageTrendPoint = {
|
||||
date: string;
|
||||
label: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type UsageOrganizationRow = {
|
||||
organizationId: string;
|
||||
organizationName: string;
|
||||
count: number;
|
||||
accountCount: number;
|
||||
lastUsedAt?: string;
|
||||
};
|
||||
|
||||
export type UsageAccountRow = {
|
||||
ownerId: string;
|
||||
accountName: string;
|
||||
accountUsername?: string;
|
||||
organizationId: string;
|
||||
organizationName: string;
|
||||
count: number;
|
||||
lastUsedAt?: string;
|
||||
};
|
||||
|
||||
export type UsageFilterOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type AdminUsageReport = {
|
||||
range: UsageDateRange;
|
||||
summary: {
|
||||
total: number;
|
||||
activeAccounts: number;
|
||||
activeOrganizations: number;
|
||||
averagePerDay: number;
|
||||
};
|
||||
trend: UsageTrendPoint[];
|
||||
byCapability: UsageCountItem[];
|
||||
byProvider: UsageCountItem[];
|
||||
organizations: UsageOrganizationRow[];
|
||||
accounts: UsageAccountRow[];
|
||||
recent: UsageRecordView[];
|
||||
options: {
|
||||
organizations: UsageFilterOption[];
|
||||
accounts: UsageFilterOption[];
|
||||
capabilities: UsageFilterOption[];
|
||||
providers: UsageFilterOption[];
|
||||
};
|
||||
warnings?: string[];
|
||||
};
|
||||
|
||||
export const USAGE_PRESET_OPTIONS: Array<{ value: UsagePreset; label: string }> = [
|
||||
{ value: "today", label: "今天" },
|
||||
{ value: "7d", label: "近 7 天" },
|
||||
{ value: "30d", label: "近 30 天" },
|
||||
{ value: "month", label: "本月" }
|
||||
];
|
||||
|
||||
export const CAPABILITY_OPTIONS: Array<{ value: GenerationCapability; label: string }> = [
|
||||
{ value: "image.generate", label: "图片生成" },
|
||||
{ value: "video.generate", label: "视频生成" }
|
||||
];
|
||||
|
||||
export function capabilityLabel(capability: GenerationCapability): string {
|
||||
return CAPABILITY_OPTIONS.find((item) => item.value === capability)?.label || capability;
|
||||
}
|
||||
|
||||
export function providerLabel(provider?: GenerationProvider): string {
|
||||
if (provider === "volcengine-visual") return "即梦图片";
|
||||
if (provider === "evolink") return "EvoLink";
|
||||
if (provider === "seedance") return "Seedance";
|
||||
if (provider === "bailian") return "阿里云百炼";
|
||||
if (provider === "mock") return "系统";
|
||||
return "未知服务商";
|
||||
}
|
||||
|
||||
export function usagePresetRange(preset: UsagePreset, now = new Date()): UsageDateRange {
|
||||
const today = dateKeyInChina(now);
|
||||
let startDate = today;
|
||||
if (preset === "7d") startDate = shiftDateKey(today, -6);
|
||||
if (preset === "30d") startDate = shiftDateKey(today, -29);
|
||||
if (preset === "month") startDate = `${today.slice(0, 7)}-01`;
|
||||
return usageDateRange(startDate, today, USAGE_PRESET_OPTIONS.find((item) => item.value === preset)?.label);
|
||||
}
|
||||
|
||||
export function usageDateRange(startDate: string, endDate: string, label?: string): UsageDateRange {
|
||||
if (!isDateKey(startDate) || !isDateKey(endDate)) throw badDateRange("日期格式必须为 YYYY-MM-DD。");
|
||||
if (startDate > endDate) throw badDateRange("开始日期不能晚于结束日期。");
|
||||
const dayCount = daysBetween(startDate, endDate) + 1;
|
||||
if (dayCount > 3660) throw badDateRange("单次查询最多支持 10 年。");
|
||||
return {
|
||||
from: `${startDate}T00:00:00+08:00`,
|
||||
to: `${shiftDateKey(endDate, 1)}T00:00:00+08:00`,
|
||||
startDate,
|
||||
endDate,
|
||||
label: label || (startDate === endDate ? startDate : `${startDate} 至 ${endDate}`),
|
||||
dayCount
|
||||
};
|
||||
}
|
||||
|
||||
export function usageDateKey(iso: string): string {
|
||||
return dateKeyInChina(new Date(iso));
|
||||
}
|
||||
|
||||
export function shiftDateKey(date: string, days: number): string {
|
||||
const value = new Date(`${date}T00:00:00Z`);
|
||||
value.setUTCDate(value.getUTCDate() + days);
|
||||
return value.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function dateKeyInChina(date: Date): string {
|
||||
const parts = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: USAGE_TIME_ZONE,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit"
|
||||
}).formatToParts(date);
|
||||
const values = Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
||||
return `${values.year}-${values.month}-${values.day}`;
|
||||
}
|
||||
|
||||
function isDateKey(value: string): boolean {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
||||
return new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value;
|
||||
}
|
||||
|
||||
function daysBetween(startDate: string, endDate: string): number {
|
||||
return Math.round((Date.parse(`${endDate}T00:00:00Z`) - Date.parse(`${startDate}T00:00:00Z`)) / 86_400_000);
|
||||
}
|
||||
|
||||
function badDateRange(message: string): Error & { status: number } {
|
||||
return Object.assign(new Error(message), { status: 400 });
|
||||
}
|
||||
Reference in new issue
Block a user