Initial commit
This commit is contained in:
111
lib/utils/api-response.ts
Normal file
111
lib/utils/api-response.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { AppError } from "../errors/app-error";
|
||||
|
||||
export interface ApiResponse<T = unknown> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: {
|
||||
message: string;
|
||||
code?: string;
|
||||
details?: unknown;
|
||||
};
|
||||
meta?: {
|
||||
timestamp: string;
|
||||
requestId?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class ApiResponseHandler {
|
||||
/**
|
||||
* 成功响应
|
||||
*/
|
||||
static success<T>(
|
||||
data: T,
|
||||
statusCode: number = 200,
|
||||
meta?: { requestId?: string }
|
||||
): NextResponse<ApiResponse<T>> {
|
||||
const response: ApiResponse<T> = {
|
||||
success: true,
|
||||
data,
|
||||
meta: {
|
||||
timestamp: new Date().toISOString(),
|
||||
...meta,
|
||||
},
|
||||
};
|
||||
|
||||
return NextResponse.json(response, { status: statusCode });
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误响应
|
||||
*/
|
||||
static error(
|
||||
error: AppError | Error,
|
||||
meta?: { requestId?: string }
|
||||
): NextResponse<ApiResponse> {
|
||||
const isAppError = error instanceof AppError;
|
||||
const statusCode = isAppError ? error.statusCode : 500;
|
||||
const code = isAppError ? error.code : "INTERNAL_SERVER_ERROR";
|
||||
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message,
|
||||
code,
|
||||
details: isAppError && !error.isOperational ? error.stack : undefined,
|
||||
},
|
||||
meta: {
|
||||
timestamp: new Date().toISOString(),
|
||||
...meta,
|
||||
},
|
||||
};
|
||||
|
||||
return NextResponse.json(response, { status: statusCode });
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应
|
||||
*/
|
||||
static paginated<T>(
|
||||
data: T[],
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
},
|
||||
meta?: { requestId?: string }
|
||||
): NextResponse<ApiResponse<{ data: T[]; pagination: typeof pagination }>> {
|
||||
const response: ApiResponse<{ data: T[]; pagination: typeof pagination }> =
|
||||
{
|
||||
success: true,
|
||||
data: {
|
||||
data,
|
||||
pagination,
|
||||
},
|
||||
meta: {
|
||||
timestamp: new Date().toISOString(),
|
||||
...meta,
|
||||
},
|
||||
};
|
||||
|
||||
return NextResponse.json(response, { status: 200 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建响应
|
||||
*/
|
||||
static created<T>(
|
||||
data: T,
|
||||
meta?: { requestId?: string }
|
||||
): NextResponse<ApiResponse<T>> {
|
||||
return this.success(data, 201, meta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 无内容响应
|
||||
*/
|
||||
static noContent(): NextResponse {
|
||||
return new NextResponse(null, { status: 204 });
|
||||
}
|
||||
}
|
||||
6
lib/utils/cn.ts
Normal file
6
lib/utils/cn.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
148
lib/utils/logger.ts
Normal file
148
lib/utils/logger.ts
Normal file
@ -0,0 +1,148 @@
|
||||
import { config } from "../config";
|
||||
|
||||
export enum LogLevel {
|
||||
DEBUG = 0,
|
||||
INFO = 1,
|
||||
WARN = 2,
|
||||
ERROR = 3,
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: string;
|
||||
level: LogLevel;
|
||||
message: string;
|
||||
context?: Record<string, unknown>;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
class Logger {
|
||||
private logLevel: LogLevel;
|
||||
|
||||
constructor() {
|
||||
this.logLevel =
|
||||
process.env.NODE_ENV === "development" ? LogLevel.DEBUG : LogLevel.INFO;
|
||||
}
|
||||
|
||||
private formatMessage(entry: LogEntry): string {
|
||||
const { timestamp, level, message, context, error } = entry;
|
||||
const levelName = LogLevel[level];
|
||||
const contextStr = context ? ` ${JSON.stringify(context)}` : "";
|
||||
const errorStr = error ? `\n${error.stack}` : "";
|
||||
|
||||
return `[${timestamp}] ${levelName}: ${message}${contextStr}${errorStr}`;
|
||||
}
|
||||
|
||||
private shouldLog(level: LogLevel): boolean {
|
||||
return level >= this.logLevel;
|
||||
}
|
||||
|
||||
private log(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
context?: Record<string, unknown>,
|
||||
error?: Error
|
||||
): void {
|
||||
if (!this.shouldLog(level)) return;
|
||||
|
||||
const entry: LogEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
message,
|
||||
context,
|
||||
error,
|
||||
};
|
||||
|
||||
const formattedMessage = this.formatMessage(entry);
|
||||
|
||||
switch (level) {
|
||||
case LogLevel.DEBUG:
|
||||
console.debug(formattedMessage);
|
||||
break;
|
||||
case LogLevel.INFO:
|
||||
console.info(formattedMessage);
|
||||
break;
|
||||
case LogLevel.WARN:
|
||||
console.warn(formattedMessage);
|
||||
break;
|
||||
case LogLevel.ERROR:
|
||||
console.error(formattedMessage);
|
||||
break;
|
||||
}
|
||||
|
||||
// 在生产环境中,可以发送到外部日志服务
|
||||
if (process.env.NODE_ENV === "production" && level >= LogLevel.ERROR) {
|
||||
this.sendToExternalService(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private sendToExternalService(entry: LogEntry): void {
|
||||
// 这里可以集成 Sentry, LogRocket 等外部日志服务
|
||||
// 示例:发送到 Sentry
|
||||
if (process.env.SENTRY_DSN) {
|
||||
// Sentry.captureException(entry.error || new Error(entry.message))
|
||||
}
|
||||
}
|
||||
|
||||
debug(message: string, context?: Record<string, unknown>): void {
|
||||
this.log(LogLevel.DEBUG, message, context);
|
||||
}
|
||||
|
||||
info(message: string, context?: Record<string, unknown>): void {
|
||||
this.log(LogLevel.INFO, message, context);
|
||||
}
|
||||
|
||||
warn(
|
||||
message: string,
|
||||
context?: Record<string, unknown>,
|
||||
error?: Error
|
||||
): void {
|
||||
this.log(LogLevel.WARN, message, context, error);
|
||||
}
|
||||
|
||||
error(
|
||||
message: string,
|
||||
context?: Record<string, unknown>,
|
||||
error?: Error
|
||||
): void {
|
||||
this.log(LogLevel.ERROR, message, context, error);
|
||||
}
|
||||
|
||||
// 记录 API 请求
|
||||
logApiRequest(
|
||||
method: string,
|
||||
url: string,
|
||||
statusCode: number,
|
||||
duration: number
|
||||
): void {
|
||||
this.info("API Request", {
|
||||
method,
|
||||
url,
|
||||
statusCode,
|
||||
duration: `${duration}ms`,
|
||||
});
|
||||
}
|
||||
|
||||
// 记录数据库操作
|
||||
logDbOperation(operation: string, table: string, duration: number): void {
|
||||
this.debug("Database Operation", {
|
||||
operation,
|
||||
table,
|
||||
duration: `${duration}ms`,
|
||||
});
|
||||
}
|
||||
|
||||
// 记录用户操作
|
||||
logUserAction(
|
||||
userId: string,
|
||||
action: string,
|
||||
details?: Record<string, unknown>
|
||||
): void {
|
||||
this.info("User Action", {
|
||||
userId,
|
||||
action,
|
||||
...details,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = new Logger();
|
||||
163
lib/utils/notifications.ts
Normal file
163
lib/utils/notifications.ts
Normal file
@ -0,0 +1,163 @@
|
||||
// 通知类型
|
||||
export type NotificationType = "success" | "error" | "warning" | "info";
|
||||
|
||||
export interface AppNotification {
|
||||
id: string;
|
||||
type: NotificationType;
|
||||
title: string;
|
||||
message: string;
|
||||
duration?: number;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
// 通知管理器
|
||||
class NotificationManager {
|
||||
private notifications: AppNotification[] = [];
|
||||
private listeners: ((notifications: AppNotification[]) => void)[] = [];
|
||||
|
||||
// 添加通知
|
||||
addNotification(
|
||||
type: NotificationType,
|
||||
title: string,
|
||||
message: string,
|
||||
duration: number = 5000
|
||||
): string {
|
||||
const id = `notification-${Date.now()}-${Math.random()}`;
|
||||
const notification: AppNotification = {
|
||||
id,
|
||||
type,
|
||||
title,
|
||||
message,
|
||||
duration,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
this.notifications.push(notification);
|
||||
this.notifyListeners();
|
||||
|
||||
// 自动移除通知
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
this.removeNotification(id);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
// 移除通知
|
||||
removeNotification(id: string): void {
|
||||
this.notifications = this.notifications.filter((n) => n.id !== id);
|
||||
this.notifyListeners();
|
||||
}
|
||||
|
||||
// 清空所有通知
|
||||
clearAll(): void {
|
||||
this.notifications = [];
|
||||
this.notifyListeners();
|
||||
}
|
||||
|
||||
// 获取所有通知
|
||||
getNotifications(): AppNotification[] {
|
||||
return [...this.notifications];
|
||||
}
|
||||
|
||||
// 添加监听器
|
||||
addListener(listener: (notifications: AppNotification[]) => void): void {
|
||||
this.listeners.push(listener);
|
||||
}
|
||||
|
||||
// 移除监听器
|
||||
removeListener(listener: (notifications: AppNotification[]) => void): void {
|
||||
this.listeners = this.listeners.filter((l) => l !== listener);
|
||||
}
|
||||
|
||||
// 通知所有监听器
|
||||
private notifyListeners(): void {
|
||||
this.listeners.forEach((listener) => listener(this.notifications));
|
||||
}
|
||||
|
||||
// 便捷方法
|
||||
success(title: string, message: string, duration?: number): string {
|
||||
return this.addNotification("success", title, message, duration);
|
||||
}
|
||||
|
||||
error(title: string, message: string, duration?: number): string {
|
||||
return this.addNotification("error", title, message, duration);
|
||||
}
|
||||
|
||||
warning(title: string, message: string, duration?: number): string {
|
||||
return this.addNotification("warning", title, message, duration);
|
||||
}
|
||||
|
||||
info(title: string, message: string, duration?: number): string {
|
||||
return this.addNotification("info", title, message, duration);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局通知管理器实例
|
||||
export const notificationManager = new NotificationManager();
|
||||
|
||||
// 浏览器通知 API
|
||||
export class BrowserNotifications {
|
||||
static async requestPermission(): Promise<boolean> {
|
||||
if (!("Notification" in window)) {
|
||||
console.warn("此浏览器不支持通知");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Notification.permission === "granted") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Notification.permission === "denied") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const permission = await Notification.requestPermission();
|
||||
return permission === "granted";
|
||||
}
|
||||
|
||||
static async showNotification(
|
||||
title: string,
|
||||
options?: NotificationOptions
|
||||
): Promise<globalThis.Notification | null> {
|
||||
if (!("Notification" in window)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Notification.permission !== "granted") {
|
||||
const granted = await this.requestPermission();
|
||||
if (!granted) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return new globalThis.Notification(title, {
|
||||
icon: "/favicon.ico",
|
||||
badge: "/favicon.ico",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
static async showRecordingComplete(): Promise<void> {
|
||||
await this.showNotification("录音完成", {
|
||||
body: "您的录音已成功保存",
|
||||
tag: "recording-complete",
|
||||
});
|
||||
}
|
||||
|
||||
static async showUploadComplete(): Promise<void> {
|
||||
await this.showNotification("上传完成", {
|
||||
body: "录音文件已成功上传到服务器",
|
||||
tag: "upload-complete",
|
||||
});
|
||||
}
|
||||
|
||||
static async showUploadError(): Promise<void> {
|
||||
await this.showNotification("上传失败", {
|
||||
body: "录音文件上传失败,请重试",
|
||||
tag: "upload-error",
|
||||
});
|
||||
}
|
||||
}
|
||||
193
lib/utils/performance.ts
Normal file
193
lib/utils/performance.ts
Normal file
@ -0,0 +1,193 @@
|
||||
import { logger } from "./logger";
|
||||
|
||||
export interface PerformanceMetric {
|
||||
name: string;
|
||||
duration: number;
|
||||
timestamp: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
class PerformanceMonitor {
|
||||
private metrics: PerformanceMetric[] = [];
|
||||
private maxMetrics = 1000;
|
||||
|
||||
/**
|
||||
* 测量函数执行时间
|
||||
*/
|
||||
async measure<T>(
|
||||
name: string,
|
||||
fn: () => Promise<T>,
|
||||
metadata?: Record<string, unknown>
|
||||
): Promise<T> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const result = await fn();
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.recordMetric(name, duration, metadata);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
this.recordMetric(`${name}_error`, duration, {
|
||||
...metadata,
|
||||
error: true,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步测量函数执行时间
|
||||
*/
|
||||
measureSync<T>(
|
||||
name: string,
|
||||
fn: () => T,
|
||||
metadata?: Record<string, unknown>
|
||||
): T {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const result = fn();
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.recordMetric(name, duration, metadata);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
this.recordMetric(`${name}_error`, duration, {
|
||||
...metadata,
|
||||
error: true,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录性能指标
|
||||
*/
|
||||
recordMetric(
|
||||
name: string,
|
||||
duration: number,
|
||||
metadata?: Record<string, unknown>
|
||||
): void {
|
||||
const metric: PerformanceMetric = {
|
||||
name,
|
||||
duration,
|
||||
timestamp: Date.now(),
|
||||
metadata,
|
||||
};
|
||||
|
||||
this.metrics.push(metric);
|
||||
|
||||
// 限制指标数量
|
||||
if (this.metrics.length > this.maxMetrics) {
|
||||
this.metrics = this.metrics.slice(-this.maxMetrics / 2);
|
||||
}
|
||||
|
||||
// 记录慢查询
|
||||
if (duration > 1000) {
|
||||
logger.warn("Slow operation detected", {
|
||||
name,
|
||||
duration,
|
||||
metadata,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能统计
|
||||
*/
|
||||
getStats(): {
|
||||
totalMetrics: number;
|
||||
averageDuration: number;
|
||||
slowestOperations: PerformanceMetric[];
|
||||
fastestOperations: PerformanceMetric[];
|
||||
operationCounts: Record<string, number>;
|
||||
} {
|
||||
if (this.metrics.length === 0) {
|
||||
return {
|
||||
totalMetrics: 0,
|
||||
averageDuration: 0,
|
||||
slowestOperations: [],
|
||||
fastestOperations: [],
|
||||
operationCounts: {},
|
||||
};
|
||||
}
|
||||
|
||||
const totalDuration = this.metrics.reduce(
|
||||
(sum, metric) => sum + metric.duration,
|
||||
0
|
||||
);
|
||||
const averageDuration = totalDuration / this.metrics.length;
|
||||
|
||||
// 按名称分组统计
|
||||
const operationCounts: Record<string, number> = {};
|
||||
this.metrics.forEach((metric) => {
|
||||
operationCounts[metric.name] = (operationCounts[metric.name] || 0) + 1;
|
||||
});
|
||||
|
||||
// 获取最慢的操作
|
||||
const slowestOperations = [...this.metrics]
|
||||
.sort((a, b) => b.duration - a.duration)
|
||||
.slice(0, 10);
|
||||
|
||||
// 获取最快的操作
|
||||
const fastestOperations = [...this.metrics]
|
||||
.sort((a, b) => a.duration - b.duration)
|
||||
.slice(0, 10);
|
||||
|
||||
return {
|
||||
totalMetrics: this.metrics.length,
|
||||
averageDuration,
|
||||
slowestOperations,
|
||||
fastestOperations,
|
||||
operationCounts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理旧指标
|
||||
*/
|
||||
cleanup(maxAge: number = 24 * 60 * 60 * 1000): void {
|
||||
const cutoff = Date.now() - maxAge;
|
||||
this.metrics = this.metrics.filter((metric) => metric.timestamp > cutoff);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出性能报告
|
||||
*/
|
||||
generateReport(): string {
|
||||
const stats = this.getStats();
|
||||
|
||||
return `
|
||||
Performance Report
|
||||
==================
|
||||
|
||||
Total Metrics: ${stats.totalMetrics}
|
||||
Average Duration: ${stats.averageDuration.toFixed(2)}ms
|
||||
|
||||
Slowest Operations:
|
||||
${stats.slowestOperations
|
||||
.map((op) => ` ${op.name}: ${op.duration}ms`)
|
||||
.join("\n")}
|
||||
|
||||
Operation Counts:
|
||||
${Object.entries(stats.operationCounts)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.map(([name, count]) => ` ${name}: ${count}`)
|
||||
.join("\n")}
|
||||
`.trim();
|
||||
}
|
||||
}
|
||||
|
||||
export const performanceMonitor = new PerformanceMonitor();
|
||||
|
||||
// 定期清理旧指标
|
||||
if (typeof window === "undefined") {
|
||||
setInterval(() => {
|
||||
performanceMonitor.cleanup();
|
||||
}, 60 * 60 * 1000); // 每小时清理一次
|
||||
}
|
||||
Reference in New Issue
Block a user