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,607 @@
/// 自然写互动课堂手机端应用软件 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();
}
}