/** * API 客户端 * 支持双 Token JWT 认证 */ import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios' import type { VideoReviewRequest, VideoReviewResponse, ReviewProgressResponse, ReviewResultResponse, } from '@/types/review' import type { TaskResponse, TaskListResponse, TaskScriptUploadRequest, TaskVideoUploadRequest, } from '@/types/task' const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000' const STORAGE_KEY_ACCESS = 'miaosi_access_token' const STORAGE_KEY_REFRESH = 'miaosi_refresh_token' // ==================== 类型定义 ==================== export type UserRole = 'brand' | 'agency' | 'creator' export interface User { id: string email?: string phone?: string name: string avatar?: string role: UserRole is_verified: boolean brand_id?: string agency_id?: string creator_id?: string tenant_id?: string tenant_name?: string } export interface LoginRequest { email?: string phone?: string password: string } export interface RegisterRequest { email?: string phone?: string password: string name: string role: UserRole } export interface LoginResponse { access_token: string refresh_token: string token_type: string expires_in: number user: User } export interface RefreshTokenResponse { access_token: string token_type: string expires_in: number } export interface UploadPolicyResponse { access_key_id: string policy: string signature: string host: string dir: string expire: number max_size_mb: number } // ==================== Token 管理 ==================== function getAccessToken(): string | null { if (typeof window === 'undefined') return null return localStorage.getItem(STORAGE_KEY_ACCESS) } function getRefreshToken(): string | null { if (typeof window === 'undefined') return null return localStorage.getItem(STORAGE_KEY_REFRESH) } function setTokens(accessToken: string, refreshToken: string): void { if (typeof window === 'undefined') return localStorage.setItem(STORAGE_KEY_ACCESS, accessToken) localStorage.setItem(STORAGE_KEY_REFRESH, refreshToken) } function clearTokens(): void { if (typeof window === 'undefined') return localStorage.removeItem(STORAGE_KEY_ACCESS) localStorage.removeItem(STORAGE_KEY_REFRESH) } // ==================== API 客户端 ==================== class ApiClient { private client: AxiosInstance private tenantId: string = 'default' private isRefreshing = false private refreshSubscribers: Array<(token: string) => void> = [] constructor() { this.client = axios.create({ baseURL: `${API_BASE_URL}/api/v1`, timeout: 30000, headers: { 'Content-Type': 'application/json', }, }) this.setupInterceptors() } private setupInterceptors() { // 请求拦截器:添加 Token 和租户 ID this.client.interceptors.request.use((config: InternalAxiosRequestConfig) => { const token = getAccessToken() if (token) { config.headers.Authorization = `Bearer ${token}` } config.headers['X-Tenant-ID'] = this.tenantId return config }) // 响应拦截器:处理 401 错误 this.client.interceptors.response.use( (response) => response, async (error: AxiosError) => { const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean } // 如果是 401 错误且不是刷新 Token 的请求 if (error.response?.status === 401 && !originalRequest._retry) { if (this.isRefreshing) { // 如果正在刷新,等待刷新完成后重试 return new Promise((resolve) => { this.refreshSubscribers.push((token: string) => { originalRequest.headers.Authorization = `Bearer ${token}` resolve(this.client(originalRequest)) }) }) } originalRequest._retry = true this.isRefreshing = true try { const refreshToken = getRefreshToken() if (!refreshToken) { throw new Error('No refresh token') } const response = await axios.post( `${API_BASE_URL}/api/v1/auth/refresh`, { refresh_token: refreshToken } ) const newAccessToken = response.data.access_token setTokens(newAccessToken, refreshToken) // 通知所有等待的请求 this.refreshSubscribers.forEach((callback) => callback(newAccessToken)) this.refreshSubscribers = [] // 重试原请求 originalRequest.headers.Authorization = `Bearer ${newAccessToken}` return this.client(originalRequest) } catch (refreshError) { // 刷新失败,清除 Token 并跳转登录 clearTokens() if (typeof window !== 'undefined') { window.location.href = '/login' } return Promise.reject(refreshError) } finally { this.isRefreshing = false } } const message = (error.response?.data as { detail?: string })?.detail || error.message || '请求失败' return Promise.reject(new Error(message)) } ) } setTenantId(tenantId: string) { this.tenantId = tenantId } // ==================== 认证 ==================== /** * 用户注册 */ async register(data: RegisterRequest): Promise { const response = await this.client.post('/auth/register', data) setTokens(response.data.access_token, response.data.refresh_token) return response.data } /** * 用户登录 */ async login(data: LoginRequest): Promise { const response = await this.client.post('/auth/login', data) setTokens(response.data.access_token, response.data.refresh_token) return response.data } /** * 退出登录 */ async logout(): Promise { try { await this.client.post('/auth/logout') } finally { clearTokens() } } /** * 刷新 Token */ async refreshToken(): Promise { const refreshToken = getRefreshToken() if (!refreshToken) { throw new Error('No refresh token') } const response = await axios.post( `${API_BASE_URL}/api/v1/auth/refresh`, { refresh_token: refreshToken } ) setTokens(response.data.access_token, refreshToken) return response.data } // ==================== 文件上传 ==================== /** * 获取 OSS 上传凭证 */ async getUploadPolicy(fileType: string = 'general'): Promise { const response = await this.client.post('/upload/policy', { file_type: fileType, }) return response.data } /** * 文件上传完成回调 */ async fileUploaded(fileKey: string, fileName: string, fileSize: number, fileType: string): Promise<{ url: string }> { const response = await this.client.post<{ url: string }>('/upload/complete', { file_key: fileKey, file_name: fileName, file_size: fileSize, file_type: fileType, }) return response.data } // ==================== 视频审核 ==================== /** * 提交视频审核 */ async submitVideoReview(data: VideoReviewRequest): Promise { const response = await this.client.post('/videos/review', { video_url: data.videoUrl, platform: data.platform, brand_id: data.brandId, creator_id: data.creatorId, }) return response.data } /** * 查询审核进度 */ async getReviewProgress(reviewId: string): Promise { const response = await this.client.get( `/videos/review/${reviewId}/progress` ) return response.data } /** * 查询审核结果 */ async getReviewResult(reviewId: string): Promise { const response = await this.client.get( `/videos/review/${reviewId}/result` ) return response.data } // ==================== 审核任务 ==================== /** * 查询任务列表 */ async listTasks(page: number = 1, pageSize: number = 20): Promise { const response = await this.client.get('/tasks', { params: { page, page_size: pageSize }, }) return response.data } /** * 查询任务详情 */ async getTask(taskId: string): Promise { const response = await this.client.get(`/tasks/${taskId}`) return response.data } /** * 上传/更新任务脚本 */ async uploadTaskScript(taskId: string, payload: TaskScriptUploadRequest): Promise { const response = await this.client.post(`/tasks/${taskId}/script`, payload) return response.data } /** * 上传/更新任务视频 */ async uploadTaskVideo(taskId: string, payload: TaskVideoUploadRequest): Promise { const response = await this.client.post(`/tasks/${taskId}/video`, payload) return response.data } // ==================== 健康检查 ==================== /** * 健康检查 */ async healthCheck(): Promise<{ status: string; version: string }> { const response = await this.client.get('/health') return response.data } } // 单例导出 export const api = new ApiClient() // 导出 Token 管理函数供其他模块使用 export { getAccessToken, getRefreshToken, setTokens, clearTokens } export default api