/** * 自然写互动课堂PC端应用软件 V1.0 * * cloud_api.ts - 云平台API通信层 * * 功能说明: * - HTTP REST API封装(Axios) * - JWT Token管理与自动刷新 * - 请求拦截器(签名/认证/日志) * - 响应拦截器(错误处理/重试) * - API类型定义 * - 离线请求队列 */ /* ======================== 类型定义 ======================== */ /** 统一响应格式 */ interface ApiResponse { code: number; msg: string; data: T; } /** 分页参数 */ interface PageParams { page: number; size: number; sort?: string; } /** 分页响应 */ interface PageResult { total: number; pages: number; current: number; records: T[]; } /** 用户信息 */ interface UserInfo { userId: string; name: string; role: 'admin' | 'teacher' | 'student' | 'parent'; phone: string; schoolId: string; schoolName: string; avatar: string; } /** 课堂信息 */ interface ClassroomInfo { classroomId: string; className: string; grade: string; teacherId: string; teacherName: string; studentCount: number; gatewayId: string; } /** 作业信息 */ interface AssignmentInfo { assignmentId: string; title: string; type: 'homework' | 'exam' | 'practice'; classId: string; deadline: string; status: 'draft' | 'published' | 'closed'; totalStudents: number; submittedCount: number; } /** 学情报告 */ interface LearningReport { studentId: string; studentName: string; subject: string; overallScore: number; writingScore: number; strokeOrderAccuracy: number; knowledgePoints: { name: string; mastery: number }[]; trend: { date: string; score: number }[]; } /** 认证令牌 */ interface AuthTokens { accessToken: string; refreshToken: string; expiresIn: number; /* 有效期(秒) */ tokenType: string; } /* ======================== 配置 ======================== */ /** API基础URL */ const API_BASE_URL = 'https://api.writech.cn'; /** 请求超时 */ const REQUEST_TIMEOUT = 30000; /** Token刷新提前量(毫秒) */ const TOKEN_REFRESH_AHEAD = 5 * 60 * 1000; /** 最大重试次数 */ const MAX_RETRIES = 3; /* ======================== Token管理 ======================== */ /** 存储的Token信息 */ let currentTokens: AuthTokens | null = null; /** Token过期时间戳 */ let tokenExpiresAt: number = 0; /** 是否正在刷新Token */ let isRefreshing: boolean = false; /** 等待Token刷新的请求队列 */ let refreshQueue: Array<(token: string) => void> = []; /** * 保存认证令牌 */ function saveTokens(tokens: AuthTokens): void { currentTokens = tokens; tokenExpiresAt = Date.now() + tokens.expiresIn * 1000; /* 持久化到electron-store */ console.log(`[API] Token已保存, 有效期至 ${new Date(tokenExpiresAt).toLocaleString()}`); } /** * 获取当前Access Token * 如果即将过期则自动刷新 */ async function getValidToken(): Promise { if (!currentTokens) { throw new Error('未登录'); } /* 检查是否需要刷新 */ if (Date.now() + TOKEN_REFRESH_AHEAD > tokenExpiresAt) { if (!isRefreshing) { isRefreshing = true; try { const newTokens = await refreshToken(currentTokens.refreshToken); saveTokens(newTokens); /* 通知所有等待中的请求 */ refreshQueue.forEach(resolve => resolve(newTokens.accessToken)); refreshQueue = []; } finally { isRefreshing = false; } } else { /* 等待正在进行的刷新完成 */ return new Promise(resolve => { refreshQueue.push(resolve); }); } } return currentTokens.accessToken; } /* ======================== HTTP请求封装 ======================== */ /** * 通用HTTP请求方法 */ async function request( method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, data?: any, retryCount: number = 0 ): Promise> { const url = `${API_BASE_URL}${path}`; const headers: Record = { 'Content-Type': 'application/json', 'Accept': 'application/json' }; /* 添加认证头 */ try { const token = await getValidToken(); headers['Authorization'] = `Bearer ${token}`; } catch { /* 登录接口不需要Token */ } /* 添加请求签名 */ const timestamp = Date.now().toString(); headers['X-Timestamp'] = timestamp; headers['X-Device-Id'] = getDeviceId(); try { const response = await fetch(url, { method, headers, body: data ? JSON.stringify(data) : undefined, signal: AbortSignal.timeout(REQUEST_TIMEOUT) }); const json: ApiResponse = await response.json(); /* 处理业务错误 */ if (json.code === 401 && retryCount < 1) { /* Token过期,尝试刷新后重试 */ console.log('[API] Token过期, 刷新后重试'); if (currentTokens) { const newTokens = await refreshToken(currentTokens.refreshToken); saveTokens(newTokens); return request(method, path, data, retryCount + 1); } } if (json.code !== 200 && json.code !== 0) { console.warn(`[API] 业务错误: ${method} ${path} code=${json.code} msg=${json.msg}`); } return json; } catch (error: any) { console.error(`[API] 请求失败: ${method} ${path}`, error.message); /* 网络错误重试 */ if (retryCount < MAX_RETRIES && isNetworkError(error)) { const delay = Math.pow(2, retryCount) * 1000; console.log(`[API] ${delay}ms后重试 (${retryCount + 1}/${MAX_RETRIES})`); await sleep(delay); return request(method, path, data, retryCount + 1); } return { code: -1, msg: error.message || '网络错误', data: null as any }; } } function isNetworkError(error: any): boolean { return error.name === 'TypeError' || error.name === 'AbortError'; } function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } function getDeviceId(): string { return 'PC-' + (typeof window !== 'undefined' ? navigator.userAgent.slice(-8) : 'unknown'); } /* ======================== API方法 ======================== */ /** 用户登录 */ async function login(username: string, password: string): Promise> { const result = await request('POST', '/api/v1/auth/login', { username, password, device_type: 'pc' }); if (result.code === 200 && result.data) { saveTokens(result.data); } return result; } /** 刷新Token */ async function refreshToken(token: string): Promise { const resp = await fetch(`${API_BASE_URL}/api/v1/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: token }) }); const json: ApiResponse = await resp.json(); if (json.code !== 200 || !json.data) { throw new Error('Token刷新失败'); } return json.data; } /** 获取当前用户信息 */ async function getUserInfo(): Promise> { return request('GET', '/api/v1/user/me'); } /** 获取班级列表 */ async function getClassrooms(): Promise> { return request('GET', '/api/v1/classroom/list'); } /** 获取作业列表 */ async function getAssignments(classId: string, params: PageParams): Promise>> { return request>('GET', `/api/v1/assignment/list?class_id=${classId}&page=${params.page}&size=${params.size}`); } /** 发布作业 */ async function publishAssignment(assignment: Partial): Promise> { return request<{ assignmentId: string }>('POST', '/api/v1/assignment/publish', assignment); } /** 上传笔迹数据 */ async function uploadStrokeData(assignmentId: string, studentId: string, strokeData: any[]): Promise> { return request('POST', '/api/v1/stroke/upload', { assignment_id: assignmentId, student_id: studentId, strokes: strokeData }); } /** 获取AI批改结果 */ async function getGradingResult(assignmentId: string): Promise> { return request('GET', `/api/v1/result/${assignmentId}`); } /** 获取学情报告 */ async function getLearningReport(studentId: string): Promise> { return request('GET', `/api/v1/report/student/${studentId}`); } /** 下载课件资源 */ async function getResourceDownloadUrl(resourceId: string): Promise> { return request<{ url: string }>('GET', `/api/v1/resource/download/${resourceId}`); } /** 退出登录 */ async function logout(): Promise { await request('POST', '/api/v1/auth/logout'); currentTokens = null; tokenExpiresAt = 0; console.log('[API] 已退出登录'); } /* ======================== 导出 ======================== */ export { login, logout, getUserInfo, getClassrooms, getAssignments, publishAssignment, uploadStrokeData, getGradingResult, getLearningReport, getResourceDownloadUrl, saveTokens }; export type { ApiResponse, UserInfo, ClassroomInfo, AssignmentInfo, LearningReport, AuthTokens, PageParams, PageResult };