Files
NianAIGC/lib/usage.ts
2026-08-17 23:44:14 +08:00

181 lines
5.2 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 === "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 });
}