software copyright
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* 自然写互动课堂PC端应用软件 V1.0
|
||||
*
|
||||
* cloud_api.ts - 云平台API通信层
|
||||
*
|
||||
* 功能说明:
|
||||
* - HTTP REST API封装(Axios)
|
||||
* - JWT Token管理与自动刷新
|
||||
* - 请求拦截器(签名/认证/日志)
|
||||
* - 响应拦截器(错误处理/重试)
|
||||
* - API类型定义
|
||||
* - 离线请求队列
|
||||
*/
|
||||
|
||||
/* ======================== 类型定义 ======================== */
|
||||
|
||||
/** 统一响应格式 */
|
||||
interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
/** 分页参数 */
|
||||
interface PageParams {
|
||||
page: number;
|
||||
size: number;
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
/** 分页响应 */
|
||||
interface PageResult<T> {
|
||||
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<string> {
|
||||
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<string>(resolve => {
|
||||
refreshQueue.push(resolve);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return currentTokens.accessToken;
|
||||
}
|
||||
|
||||
/* ======================== HTTP请求封装 ======================== */
|
||||
|
||||
/**
|
||||
* 通用HTTP请求方法
|
||||
*/
|
||||
async function request<T>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
path: string,
|
||||
data?: any,
|
||||
retryCount: number = 0
|
||||
): Promise<ApiResponse<T>> {
|
||||
const url = `${API_BASE_URL}${path}`;
|
||||
const headers: Record<string, string> = {
|
||||
'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<T> = 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<T>(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<T>(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<void> {
|
||||
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<ApiResponse<AuthTokens>> {
|
||||
const result = await request<AuthTokens>('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<AuthTokens> {
|
||||
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<AuthTokens> = await resp.json();
|
||||
if (json.code !== 200 || !json.data) {
|
||||
throw new Error('Token刷新失败');
|
||||
}
|
||||
return json.data;
|
||||
}
|
||||
|
||||
/** 获取当前用户信息 */
|
||||
async function getUserInfo(): Promise<ApiResponse<UserInfo>> {
|
||||
return request<UserInfo>('GET', '/api/v1/user/me');
|
||||
}
|
||||
|
||||
/** 获取班级列表 */
|
||||
async function getClassrooms(): Promise<ApiResponse<ClassroomInfo[]>> {
|
||||
return request<ClassroomInfo[]>('GET', '/api/v1/classroom/list');
|
||||
}
|
||||
|
||||
/** 获取作业列表 */
|
||||
async function getAssignments(classId: string, params: PageParams): Promise<ApiResponse<PageResult<AssignmentInfo>>> {
|
||||
return request<PageResult<AssignmentInfo>>('GET',
|
||||
`/api/v1/assignment/list?class_id=${classId}&page=${params.page}&size=${params.size}`);
|
||||
}
|
||||
|
||||
/** 发布作业 */
|
||||
async function publishAssignment(assignment: Partial<AssignmentInfo>): Promise<ApiResponse<{ assignmentId: string }>> {
|
||||
return request<{ assignmentId: string }>('POST', '/api/v1/assignment/publish', assignment);
|
||||
}
|
||||
|
||||
/** 上传笔迹数据 */
|
||||
async function uploadStrokeData(assignmentId: string, studentId: string,
|
||||
strokeData: any[]): Promise<ApiResponse<void>> {
|
||||
return request<void>('POST', '/api/v1/stroke/upload', {
|
||||
assignment_id: assignmentId,
|
||||
student_id: studentId,
|
||||
strokes: strokeData
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取AI批改结果 */
|
||||
async function getGradingResult(assignmentId: string): Promise<ApiResponse<any>> {
|
||||
return request<any>('GET', `/api/v1/result/${assignmentId}`);
|
||||
}
|
||||
|
||||
/** 获取学情报告 */
|
||||
async function getLearningReport(studentId: string): Promise<ApiResponse<LearningReport>> {
|
||||
return request<LearningReport>('GET', `/api/v1/report/student/${studentId}`);
|
||||
}
|
||||
|
||||
/** 下载课件资源 */
|
||||
async function getResourceDownloadUrl(resourceId: string): Promise<ApiResponse<{ url: string }>> {
|
||||
return request<{ url: string }>('GET', `/api/v1/resource/download/${resourceId}`);
|
||||
}
|
||||
|
||||
/** 退出登录 */
|
||||
async function logout(): Promise<void> {
|
||||
await request<void>('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
|
||||
};
|
||||
Reference in New Issue
Block a user