software copyright

This commit is contained in:
jiahong
2026-03-22 15:24:40 +08:00
parent e303bb868a
commit 60f336e345
155 changed files with 127262 additions and 0 deletions
@@ -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
};