Files
system-design/software-copyright/06-writech-app-mobile/service/api_service.dart
T
2026-03-22 15:24:40 +08:00

608 lines
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 自然写互动课堂手机端应用软件 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<T> {
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<String, dynamic> 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<String, dynamic> json) {
return AuthToken(
accessToken: json['access_token'] ?? '',
refreshToken: json['refresh_token'] ?? '',
expiresAt: json['expires_at'] ?? 0,
userRole: json['user_role'] ?? '',
);
}
Map<String, dynamic> 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<String> 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<String, dynamic> json) {
return UserInfo(
userId: json['user_id'] ?? '',
name: json['name'] ?? '',
avatar: json['avatar'] ?? '',
role: json['role'] ?? '',
phone: json['phone'] ?? '',
classIds: List<String>.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<String, dynamic> 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<String, double> knowledgeMap; // 知识点掌握度
final List<ErrorItem> 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<String, dynamic> json) {
return LearningReport(
studentId: json['student_id'] ?? '',
studentName: json['student_name'] ?? '',
subject: json['subject'] ?? '',
overallScore: (json['overall_score'] ?? 0).toDouble(),
knowledgeMap: Map<String, double>.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<String, dynamic> 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<double> scores; // 历次书写评分
final List<String> 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<String, dynamic> json) {
return WritingGrowth(
scores: List<double>.from(json['scores'] ?? []),
dates: List<String>.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<Function> _refreshQueue = [];
/// HTTP客户端实例
late final HttpClient _httpClient;
/// 离线请求队列(断网时暂存)
final List<Map<String, dynamic>> _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<ApiResponse<T>> _request<T>({
required String method,
required String path,
Map<String, dynamic>? queryParams,
Map<String, dynamic>? 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<String, dynamic>;
// 处理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<void> _refreshToken() async {
if (_isRefreshing) {
// 等待正在进行的刷新完成
final completer = Completer<void>();
_refreshQueue.add(() => completer.complete());
return completer.future;
}
_isRefreshing = true;
try {
final response = await _request<AuthToken>(
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<int> replayOfflineQueue() async {
int successCount = 0;
final queue = List<Map<String, dynamic>>.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<ApiResponse<AuthToken>> 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<ApiResponse<AuthToken>> loginByWechat(String wxCode) {
return _request(
method: 'POST',
path: '/auth/login/wechat',
body: {'wx_code': wxCode},
fromData: (data) => AuthToken.fromJson(data),
);
}
/// 获取当前用户信息
Future<ApiResponse<UserInfo>> getUserInfo() {
return _request(
method: 'GET',
path: '/user/profile',
fromData: (data) => UserInfo.fromJson(data),
);
}
/// 登出(撤销Token
Future<ApiResponse> logout() {
return _request(method: 'POST', path: '/auth/logout');
}
/* ========== 作业相关API ========== */
/// 获取作业列表(教师端)
Future<ApiResponse<List<AssignmentInfo>>> 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<ApiResponse<String>> publishAssignment({
required String title,
required String classId,
required String subject,
required int deadline,
required List<Map<String, dynamic>> questions,
}) {
return _request(
method: 'POST',
path: '/assignment/publish',
body: {
'title': title,
'class_id': classId,
'subject': subject,
'deadline': deadline,
'questions': questions,
},
);
}
/* ========== 学情报告API ========== */
/// 获取学生学情报告(家长端/教师端)
Future<ApiResponse<LearningReport>> 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<ApiResponse<Map<String, dynamic>>> getClassReport(String classId) {
return _request(
method: 'GET',
path: '/report/class/$classId',
);
}
/* ========== 消息通知API ========== */
/// 获取消息列表
Future<ApiResponse<List<Map<String, dynamic>>>> getMessageList({
int page = 1,
int pageSize = 20,
}) {
return _request(
method: 'GET',
path: '/message/list',
queryParams: {'page': page, 'page_size': pageSize},
);
}
/// 发送家校沟通消息(教师→家长)
Future<ApiResponse> 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<ApiResponse> markMessageRead(List<String> messageIds) {
return _request(
method: 'PUT',
path: '/message/read',
body: {'message_ids': messageIds},
);
}
/* ========== 笔迹数据API ========== */
/// 上传笔迹数据(教师端蓝牙收笔后上传)
Future<ApiResponse<String>> uploadStrokeData({
required String assignmentId,
required String studentId,
required List<Map<String, dynamic>> strokes,
}) {
return _request(
method: 'POST',
path: '/stroke/upload',
body: {
'assignment_id': assignmentId,
'student_id': studentId,
'strokes': strokes,
'client_time': DateTime.now().millisecondsSinceEpoch,
},
);
}
/// 获取笔迹回放数据
Future<ApiResponse<List<Map<String, dynamic>>>> 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();
}
}