/// 自然写互动课堂手机端应用软件 V1.0 /// 云平台API服务 - 封装所有REST API通信逻辑 /// /// 功能说明: /// 1. HTTP客户端配置(Dio拦截器、超时设置、重试策略) /// 2. JWT Token自动管理(存储、刷新、过期处理) /// 3. 请求签名(HMAC-SHA256防篡改) /// 4. 证书锁定(Certificate Pinning防中间人攻击) /// 5. 全部业务API封装(登录、作业、学情、消息等) /// 6. 离线请求队列(断网时暂存请求,恢复后自动重放) import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:crypto/crypto.dart'; /* ========== 数据模型 ========== */ /// API响应统一包装 class ApiResponse { final int code; // 业务状态码(0=成功) final String message; // 状态消息 final T? data; // 响应数据 final int timestamp; // 服务端时间戳 ApiResponse({ required this.code, required this.message, this.data, required this.timestamp, }); /// 判断请求是否成功 bool get isSuccess => code == 0; /// 从JSON反序列化 factory ApiResponse.fromJson(Map json, T Function(dynamic)? fromData) { return ApiResponse( code: json['code'] ?? -1, message: json['message'] ?? '', data: json['data'] != null && fromData != null ? fromData(json['data']) : null, timestamp: json['timestamp'] ?? 0, ); } } /// 用户登录凭证 class AuthToken { final String accessToken; // 访问令牌(有效期2小时) final String refreshToken; // 刷新令牌(有效期7天) final int expiresAt; // 访问令牌过期时间戳(毫秒) final String userRole; // 用户角色: teacher / parent / admin AuthToken({ required this.accessToken, required this.refreshToken, required this.expiresAt, required this.userRole, }); /// 判断Token是否即将过期(提前5分钟刷新) bool get isExpiringSoon { return DateTime.now().millisecondsSinceEpoch > (expiresAt - 5 * 60 * 1000); } factory AuthToken.fromJson(Map json) { return AuthToken( accessToken: json['access_token'] ?? '', refreshToken: json['refresh_token'] ?? '', expiresAt: json['expires_at'] ?? 0, userRole: json['user_role'] ?? '', ); } Map toJson() => { 'access_token': accessToken, 'refresh_token': refreshToken, 'expires_at': expiresAt, 'user_role': userRole, }; } /// 用户信息模型 class UserInfo { final String userId; final String name; final String avatar; final String role; final String phone; final List classIds; // 关联的班级ID列表 UserInfo({ required this.userId, required this.name, required this.avatar, required this.role, required this.phone, required this.classIds, }); factory UserInfo.fromJson(Map json) { return UserInfo( userId: json['user_id'] ?? '', name: json['name'] ?? '', avatar: json['avatar'] ?? '', role: json['role'] ?? '', phone: json['phone'] ?? '', classIds: List.from(json['class_ids'] ?? []), ); } } /// 作业信息模型 class AssignmentInfo { final String id; final String title; final String subject; // 科目 final String type; // 类型: homework / exam / practice final String classId; final int publishTime; // 发布时间 final int deadline; // 截止时间 final int submittedCount; // 已提交人数 final int totalCount; // 应提交人数 final int status; // 0=进行中, 1=已截止, 2=已批改 AssignmentInfo({ required this.id, required this.title, required this.subject, required this.type, required this.classId, required this.publishTime, required this.deadline, required this.submittedCount, required this.totalCount, required this.status, }); factory AssignmentInfo.fromJson(Map json) { return AssignmentInfo( id: json['id'] ?? '', title: json['title'] ?? '', subject: json['subject'] ?? '', type: json['type'] ?? '', classId: json['class_id'] ?? '', publishTime: json['publish_time'] ?? 0, deadline: json['deadline'] ?? 0, submittedCount: json['submitted_count'] ?? 0, totalCount: json['total_count'] ?? 0, status: json['status'] ?? 0, ); } } /// 学情报告模型 class LearningReport { final String studentId; final String studentName; final String subject; final double overallScore; // 综合评分(0-100) final Map knowledgeMap; // 知识点掌握度 final List topErrors; // 高频错题 final WritingGrowth writingGrowth; // 书写成长数据 LearningReport({ required this.studentId, required this.studentName, required this.subject, required this.overallScore, required this.knowledgeMap, required this.topErrors, required this.writingGrowth, }); factory LearningReport.fromJson(Map json) { return LearningReport( studentId: json['student_id'] ?? '', studentName: json['student_name'] ?? '', subject: json['subject'] ?? '', overallScore: (json['overall_score'] ?? 0).toDouble(), knowledgeMap: Map.from(json['knowledge_map'] ?? {}), topErrors: (json['top_errors'] as List? ?? []) .map((e) => ErrorItem.fromJson(e)) .toList(), writingGrowth: WritingGrowth.fromJson(json['writing_growth'] ?? {}), ); } } /// 错题条目 class ErrorItem { final String questionId; final String content; final String knowledgePoint; final int errorCount; final String errorReason; ErrorItem({ required this.questionId, required this.content, required this.knowledgePoint, required this.errorCount, required this.errorReason, }); factory ErrorItem.fromJson(Map json) { return ErrorItem( questionId: json['question_id'] ?? '', content: json['content'] ?? '', knowledgePoint: json['knowledge_point'] ?? '', errorCount: json['error_count'] ?? 0, errorReason: json['error_reason'] ?? '', ); } } /// 书写成长数据 class WritingGrowth { final List scores; // 历次书写评分 final List dates; // 对应日期 final double strokeAccuracy; // 笔顺正确率 final double writingNeatness; // 书写规范性 final String improvement; // 进步趋势描述 WritingGrowth({ required this.scores, required this.dates, required this.strokeAccuracy, required this.writingNeatness, required this.improvement, }); factory WritingGrowth.fromJson(Map json) { return WritingGrowth( scores: List.from(json['scores'] ?? []), dates: List.from(json['dates'] ?? []), strokeAccuracy: (json['stroke_accuracy'] ?? 0).toDouble(), writingNeatness: (json['writing_neatness'] ?? 0).toDouble(), improvement: json['improvement'] ?? '', ); } } /* ========== API服务实现 ========== */ /// 云平台API服务 - 管理所有HTTP通信 /// 采用Dio作为HTTP客户端,支持拦截器链、证书锁定、自动重试 class CloudApiService { /// 云平台API基础地址 static const String _baseUrl = 'https://api.writech.com/v1'; /// HMAC签名密钥(从安全存储中加载) final String _hmacSecret; /// 当前认证令牌 AuthToken? _authToken; /// Token刷新锁(防止并发刷新) bool _isRefreshing = false; final List _refreshQueue = []; /// HTTP客户端实例 late final HttpClient _httpClient; /// 离线请求队列(断网时暂存) final List> _offlineQueue = []; /// 最大重试次数 static const int _maxRetries = 3; CloudApiService({String hmacSecret = ''}) : _hmacSecret = hmacSecret { _httpClient = HttpClient() ..connectionTimeout = const Duration(seconds: 15) ..idleTimeout = const Duration(seconds: 60); // 配置证书锁定(防止中间人攻击) _httpClient.badCertificateCallback = (X509Certificate cert, String host, int port) { // 验证证书指纹是否匹配预置的服务器证书 final fingerprint = sha256.convert(cert.der).toString(); const expectedFingerprint = 'a1b2c3d4e5f6...'; // 预置证书指纹 return fingerprint == expectedFingerprint; }; } /// 设置认证令牌(登录成功后调用) void setAuthToken(AuthToken token) { _authToken = token; } /// 生成请求签名(HMAC-SHA256) /// 签名内容: METHOD + PATH + TIMESTAMP + BODY_HASH String _generateSignature(String method, String path, int timestamp, String body) { final bodyHash = sha256.convert(utf8.encode(body)).toString(); final content = '$method\n$path\n$timestamp\n$bodyHash'; final hmacSha256 = Hmac(sha256, utf8.encode(_hmacSecret)); return hmacSha256.convert(utf8.encode(content)).toString(); } /// 统一HTTP请求方法(带签名、Token、重试) Future> _request({ required String method, required String path, Map? queryParams, Map? body, T Function(dynamic)? fromData, int retryCount = 0, }) async { // 检查Token是否需要刷新 if (_authToken != null && _authToken!.isExpiringSoon) { await _refreshToken(); } final uri = Uri.parse('$_baseUrl$path').replace(queryParameters: queryParams?.map((k, v) => MapEntry(k, v.toString()))); final timestamp = DateTime.now().millisecondsSinceEpoch; final bodyStr = body != null ? jsonEncode(body) : ''; final signature = _generateSignature(method, path, timestamp, bodyStr); try { final request = await _httpClient.openUrl(method, uri); // 设置请求头 request.headers.set('Content-Type', 'application/json'); request.headers.set('X-Timestamp', timestamp.toString()); request.headers.set('X-Signature', signature); request.headers.set('X-Client', 'writech-mobile/1.0'); if (_authToken != null) { request.headers.set('Authorization', 'Bearer ${_authToken!.accessToken}'); } // 写入请求体 if (body != null) { request.write(bodyStr); } // 发送请求并接收响应 final response = await request.close(); final responseBody = await response.transform(utf8.decoder).join(); final jsonData = jsonDecode(responseBody) as Map; // 处理401未授权(Token过期) if (response.statusCode == 401 && retryCount < 1) { await _refreshToken(); return _request( method: method, path: path, queryParams: queryParams, body: body, fromData: fromData, retryCount: retryCount + 1, ); } return ApiResponse.fromJson(jsonData, fromData); } on SocketException { // 网络不可用,加入离线队列 if (method == 'POST' || method == 'PUT') { _offlineQueue.add({ 'method': method, 'path': path, 'body': body, 'timestamp': timestamp, }); } return ApiResponse(code: -1, message: '网络连接不可用', timestamp: timestamp); } catch (e) { // 重试逻辑(指数退避) if (retryCount < _maxRetries) { await Future.delayed(Duration(seconds: 1 << retryCount)); return _request( method: method, path: path, queryParams: queryParams, body: body, fromData: fromData, retryCount: retryCount + 1, ); } return ApiResponse(code: -1, message: '请求失败: $e', timestamp: timestamp); } } /// 刷新Token(使用Refresh Token获取新的Access Token) Future _refreshToken() async { if (_isRefreshing) { // 等待正在进行的刷新完成 final completer = Completer(); _refreshQueue.add(() => completer.complete()); return completer.future; } _isRefreshing = true; try { final response = await _request( method: 'POST', path: '/auth/refresh', body: {'refresh_token': _authToken?.refreshToken ?? ''}, fromData: (data) => AuthToken.fromJson(data), ); if (response.isSuccess && response.data != null) { _authToken = response.data; // 持久化新Token到安全存储 _persistToken(_authToken!); } } finally { _isRefreshing = false; // 通知所有等待的请求继续 for (final callback in _refreshQueue) { callback(); } _refreshQueue.clear(); } } /// 持久化Token到Keychain/KeyStore void _persistToken(AuthToken token) { // 使用flutter_secure_storage存储到系统安全存储 // iOS: Keychain Android: KeyStore } /// 重放离线队列中的请求(网络恢复后调用) Future replayOfflineQueue() async { int successCount = 0; final queue = List>.from(_offlineQueue); _offlineQueue.clear(); for (final item in queue) { final response = await _request( method: item['method'], path: item['path'], body: item['body'], ); if (response.isSuccess) successCount++; } return successCount; } /* ========== 认证相关API ========== */ /// 手机号+验证码登录 Future> loginByPhone(String phone, String code) { return _request( method: 'POST', path: '/auth/login/phone', body: {'phone': phone, 'code': code}, fromData: (data) => AuthToken.fromJson(data), ); } /// 微信OAuth登录 Future> loginByWechat(String wxCode) { return _request( method: 'POST', path: '/auth/login/wechat', body: {'wx_code': wxCode}, fromData: (data) => AuthToken.fromJson(data), ); } /// 获取当前用户信息 Future> getUserInfo() { return _request( method: 'GET', path: '/user/profile', fromData: (data) => UserInfo.fromJson(data), ); } /// 登出(撤销Token) Future logout() { return _request(method: 'POST', path: '/auth/logout'); } /* ========== 作业相关API ========== */ /// 获取作业列表(教师端) Future>> getAssignmentList({ required String classId, int page = 1, int pageSize = 20, String? status, }) { return _request( method: 'GET', path: '/assignment/list', queryParams: { 'class_id': classId, 'page': page, 'page_size': pageSize, if (status != null) 'status': status, }, fromData: (data) => (data as List) .map((e) => AssignmentInfo.fromJson(e)) .toList(), ); } /// 发布新作业(教师端) Future> publishAssignment({ required String title, required String classId, required String subject, required int deadline, required List> questions, }) { return _request( method: 'POST', path: '/assignment/publish', body: { 'title': title, 'class_id': classId, 'subject': subject, 'deadline': deadline, 'questions': questions, }, ); } /* ========== 学情报告API ========== */ /// 获取学生学情报告(家长端/教师端) Future> getStudentReport(String studentId, {String? subject}) { return _request( method: 'GET', path: '/report/student/$studentId', queryParams: subject != null ? {'subject': subject} : null, fromData: (data) => LearningReport.fromJson(data), ); } /// 获取班级学情概览(教师端) Future>> getClassReport(String classId) { return _request( method: 'GET', path: '/report/class/$classId', ); } /* ========== 消息通知API ========== */ /// 获取消息列表 Future>>> getMessageList({ int page = 1, int pageSize = 20, }) { return _request( method: 'GET', path: '/message/list', queryParams: {'page': page, 'page_size': pageSize}, ); } /// 发送家校沟通消息(教师→家长) Future sendMessage({ required String toUserId, required String content, String type = 'text', }) { return _request( method: 'POST', path: '/message/send', body: {'to_user_id': toUserId, 'content': content, 'type': type}, ); } /// 标记消息已读 Future markMessageRead(List messageIds) { return _request( method: 'PUT', path: '/message/read', body: {'message_ids': messageIds}, ); } /* ========== 笔迹数据API ========== */ /// 上传笔迹数据(教师端蓝牙收笔后上传) Future> uploadStrokeData({ required String assignmentId, required String studentId, required List> strokes, }) { return _request( method: 'POST', path: '/stroke/upload', body: { 'assignment_id': assignmentId, 'student_id': studentId, 'strokes': strokes, 'client_time': DateTime.now().millisecondsSinceEpoch, }, ); } /// 获取笔迹回放数据 Future>>> getStrokeReplay({ required String assignmentId, required String studentId, }) { return _request( method: 'GET', path: '/stroke/replay', queryParams: { 'assignment_id': assignmentId, 'student_id': studentId, }, ); } /// 销毁HTTP客户端 void dispose() { _httpClient.close(); _offlineQueue.clear(); _refreshQueue.clear(); } }