forked from 77media/video-flow
303 lines
7.4 KiB
TypeScript
303 lines
7.4 KiB
TypeScript
/**
|
||
* 视频编辑API错误处理器
|
||
*/
|
||
|
||
import { debugLog, errorHandlingConfig } from './config';
|
||
|
||
export interface ApiError {
|
||
code: number;
|
||
message: string;
|
||
details?: any;
|
||
timestamp: string;
|
||
endpoint?: string;
|
||
}
|
||
|
||
export interface RetryOptions {
|
||
maxRetries?: number;
|
||
retryDelay?: number;
|
||
shouldRetry?: (error: ApiError) => boolean;
|
||
onRetry?: (attempt: number, error: ApiError) => void;
|
||
}
|
||
|
||
/**
|
||
* 创建API错误对象
|
||
*/
|
||
export function createApiError(
|
||
code: number,
|
||
message: string,
|
||
details?: any,
|
||
endpoint?: string
|
||
): ApiError {
|
||
return {
|
||
code,
|
||
message,
|
||
details,
|
||
timestamp: new Date().toISOString(),
|
||
endpoint
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 判断错误是否可重试
|
||
*/
|
||
export function isRetryableError(error: ApiError): boolean {
|
||
// 网络错误、超时、5xx服务器错误可重试
|
||
return (
|
||
error.code === 0 || // 网络错误
|
||
error.code === 408 || // 请求超时
|
||
error.code === 429 || // 请求过多
|
||
(error.code >= 500 && error.code < 600) // 服务器错误
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 计算重试延迟(指数退避)
|
||
*/
|
||
export function calculateRetryDelay(attempt: number, baseDelay: number = 1000): number {
|
||
return Math.min(baseDelay * Math.pow(2, attempt - 1), 10000); // 最大10秒
|
||
}
|
||
|
||
/**
|
||
* 带重试的API调用包装器
|
||
*/
|
||
export async function withRetry<T>(
|
||
apiCall: () => Promise<T>,
|
||
options: RetryOptions = {}
|
||
): Promise<T> {
|
||
const {
|
||
maxRetries = errorHandlingConfig.maxRetries,
|
||
retryDelay = errorHandlingConfig.retryDelay,
|
||
shouldRetry = isRetryableError,
|
||
onRetry
|
||
} = options;
|
||
|
||
let lastError: ApiError;
|
||
|
||
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
|
||
try {
|
||
const result = await apiCall();
|
||
|
||
// 如果之前有重试,记录成功
|
||
if (attempt > 1) {
|
||
debugLog(`API调用在第${attempt}次尝试后成功`);
|
||
}
|
||
|
||
return result;
|
||
} catch (error: any) {
|
||
// 转换为标准错误格式
|
||
lastError = error instanceof Error
|
||
? createApiError(0, error.message, error)
|
||
: error;
|
||
|
||
debugLog(`API调用失败 (尝试 ${attempt}/${maxRetries + 1})`, lastError);
|
||
|
||
// 如果是最后一次尝试,或错误不可重试,直接抛出
|
||
if (attempt > maxRetries || !shouldRetry(lastError)) {
|
||
throw lastError;
|
||
}
|
||
|
||
// 计算延迟时间
|
||
const delay = calculateRetryDelay(attempt, retryDelay);
|
||
|
||
// 调用重试回调
|
||
onRetry?.(attempt, lastError);
|
||
|
||
debugLog(`${delay}ms后进行第${attempt + 1}次重试`);
|
||
|
||
// 等待后重试
|
||
await new Promise(resolve => setTimeout(resolve, delay));
|
||
}
|
||
}
|
||
|
||
throw lastError!;
|
||
}
|
||
|
||
/**
|
||
* API错误分类
|
||
*/
|
||
export enum ErrorCategory {
|
||
NETWORK = 'network',
|
||
VALIDATION = 'validation',
|
||
AUTHENTICATION = 'authentication',
|
||
AUTHORIZATION = 'authorization',
|
||
NOT_FOUND = 'not_found',
|
||
SERVER = 'server',
|
||
UNKNOWN = 'unknown'
|
||
}
|
||
|
||
/**
|
||
* 根据错误码分类错误
|
||
*/
|
||
export function categorizeError(error: ApiError): ErrorCategory {
|
||
const { code } = error;
|
||
|
||
if (code === 0) return ErrorCategory.NETWORK;
|
||
if (code === 400) return ErrorCategory.VALIDATION;
|
||
if (code === 401) return ErrorCategory.AUTHENTICATION;
|
||
if (code === 403) return ErrorCategory.AUTHORIZATION;
|
||
if (code === 404) return ErrorCategory.NOT_FOUND;
|
||
if (code >= 500) return ErrorCategory.SERVER;
|
||
|
||
return ErrorCategory.UNKNOWN;
|
||
}
|
||
|
||
/**
|
||
* 获取用户友好的错误消息
|
||
*/
|
||
export function getUserFriendlyMessage(error: ApiError): string {
|
||
const category = categorizeError(error);
|
||
|
||
switch (category) {
|
||
case ErrorCategory.NETWORK:
|
||
return '网络连接失败,请检查网络连接后重试';
|
||
case ErrorCategory.VALIDATION:
|
||
return '请求参数有误,请检查输入内容';
|
||
case ErrorCategory.AUTHENTICATION:
|
||
return '登录已过期,请重新登录';
|
||
case ErrorCategory.AUTHORIZATION:
|
||
return '权限不足,无法执行此操作';
|
||
case ErrorCategory.NOT_FOUND:
|
||
return '请求的资源不存在';
|
||
case ErrorCategory.SERVER:
|
||
return '服务器暂时不可用,请稍后重试';
|
||
default:
|
||
return error.message || '发生未知错误';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 错误通知处理
|
||
*/
|
||
export function handleErrorNotification(error: ApiError): void {
|
||
const category = categorizeError(error);
|
||
const message = getUserFriendlyMessage(error);
|
||
|
||
// 根据配置决定是否显示通知
|
||
const { notifications } = errorHandlingConfig;
|
||
|
||
let shouldNotify = false;
|
||
|
||
switch (category) {
|
||
case ErrorCategory.NETWORK:
|
||
shouldNotify = notifications.showNetworkErrors;
|
||
break;
|
||
case ErrorCategory.VALIDATION:
|
||
shouldNotify = notifications.showValidationErrors;
|
||
break;
|
||
case ErrorCategory.SERVER:
|
||
shouldNotify = notifications.showServerErrors;
|
||
break;
|
||
default:
|
||
shouldNotify = true;
|
||
}
|
||
|
||
if (shouldNotify) {
|
||
// 这里可以集成实际的通知系统
|
||
console.error('API错误通知:', message, error);
|
||
|
||
// 如果有全局通知系统,在这里调用
|
||
if (typeof window !== 'undefined' && (window as any).msg) {
|
||
(window as any).msg.error(message);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 错误恢复策略
|
||
*/
|
||
export interface RecoveryStrategy {
|
||
name: string;
|
||
canRecover: (error: ApiError) => boolean;
|
||
recover: (error: ApiError) => Promise<any>;
|
||
}
|
||
|
||
/**
|
||
* Mock API回退策略
|
||
*/
|
||
export const mockFallbackStrategy: RecoveryStrategy = {
|
||
name: 'mock-fallback',
|
||
canRecover: (error: ApiError) => {
|
||
return errorHandlingConfig.fallbackToMock &&
|
||
(error.code === 404 || error.code >= 500);
|
||
},
|
||
recover: async (error: ApiError) => {
|
||
debugLog('API失败,回退到Mock模式', error);
|
||
// 这里可以动态切换到Mock API
|
||
return null;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 缓存回退策略
|
||
*/
|
||
export const cacheStrategy: RecoveryStrategy = {
|
||
name: 'cache-fallback',
|
||
canRecover: (error: ApiError) => {
|
||
return error.code === 0 || error.code >= 500;
|
||
},
|
||
recover: async (error: ApiError) => {
|
||
debugLog('尝试从缓存恢复数据', error);
|
||
// 这里可以从本地缓存获取数据
|
||
return null;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 默认恢复策略列表
|
||
*/
|
||
export const defaultRecoveryStrategies: RecoveryStrategy[] = [
|
||
cacheStrategy,
|
||
mockFallbackStrategy
|
||
];
|
||
|
||
/**
|
||
* 尝试错误恢复
|
||
*/
|
||
export async function attemptRecovery(
|
||
error: ApiError,
|
||
strategies: RecoveryStrategy[] = defaultRecoveryStrategies
|
||
): Promise<any> {
|
||
for (const strategy of strategies) {
|
||
if (strategy.canRecover(error)) {
|
||
try {
|
||
debugLog(`尝试恢复策略: ${strategy.name}`);
|
||
const result = await strategy.recover(error);
|
||
if (result !== null) {
|
||
debugLog(`恢复策略 ${strategy.name} 成功`);
|
||
return result;
|
||
}
|
||
} catch (recoveryError) {
|
||
debugLog(`恢复策略 ${strategy.name} 失败`, recoveryError);
|
||
}
|
||
}
|
||
}
|
||
|
||
debugLog('所有恢复策略都失败了');
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 完整的错误处理流程
|
||
*/
|
||
export async function handleApiError(error: any, endpoint?: string): Promise<any> {
|
||
// 标准化错误格式
|
||
const apiError: ApiError = error instanceof Error
|
||
? createApiError(0, error.message, error, endpoint)
|
||
: { ...error, endpoint };
|
||
|
||
// 记录错误
|
||
debugLog('API错误', apiError);
|
||
|
||
// 显示通知
|
||
handleErrorNotification(apiError);
|
||
|
||
// 尝试恢复
|
||
const recoveryResult = await attemptRecovery(apiError);
|
||
if (recoveryResult !== null) {
|
||
return recoveryResult;
|
||
}
|
||
|
||
// 抛出错误
|
||
throw apiError;
|
||
}
|