182 lines
5.3 KiB
TypeScript
182 lines
5.3 KiB
TypeScript
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 === "seedream") return "Seedream 5.0 Pro";
|
|
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 });
|
|
}
|