Files
nj/app/lib/data/remote/api_client.dart
iven 5e6c6fdd62 feat(diary): 数据层 + 班级系统 (Phase F1 + B3)
Flutter 数据层 (Phase F1):
- journal_entry.dart: 日记数据模型 (Mood/Weather/tags/version)
- journal_element.dart: 元素模型 (text/image/sticker/handwriting_ref/tape)
- school_class.dart: 班级模型
- user_settings.dart: 用户设置 (主题/画笔/字号)
- isar_database.dart: Isar 初始化
- api_client.dart: Dio + JWT注入 + 离线感知 + 401处理
- journal_repository.dart: 抽象接口 + InMemory实现 (乐观锁)
- sync_engine.dart: WiFi同步 + 操作队列 + 重试(5次) + 快照持久化

Rust 班级系统 (Phase B3):
- class_service.rs: 创建班级(6位码) + 加入班级 + 成员管理
- topic_service.rs: 老师布置主题 + 主题列表
- comment_service.rs: 老师点评 + 评语列表
- class_handler.rs: 5个API端点 + 权限守卫
- topic_handler.rs: 2个API端点
- comment_handler.rs: 2个API端点
- dto.rs: 新增5个DTO (ClassMemberResp/CreateTopicReq/TopicResp/CreateCommentReq/CommentResp)
- 6条新路由注册

验证: cargo check 通过, 433测试全绿, flutter analyze 1 warning
2026-06-01 00:55:51 +08:00

150 lines
3.9 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.
// API 客户端 — Dio 封装 + JWT 注入 + 离线感知
//
// 核心职责:
// - 封装 Dio HTTP 客户端,统一配置超时和头信息
// - JWT token 自动注入(请求拦截器)
// - 离线状态感知(网络不可用时抛出明确异常)
// - 为 SyncEngine 提供远程操作能力
import 'package:dio/dio.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
/// 网络离线异常 — 网络不可用时由 ApiClient 抛出
class OfflineException implements Exception {
final String message;
const OfflineException([this.message = '网络不可用,请检查网络连接']);
@override
String toString() => 'OfflineException: $message';
}
/// API 客户端 — 所有远程请求的统一入口
class ApiClient {
late final Dio _dio;
String? _token;
/// 基础 URL默认指向本地开发服务器
final String baseUrl;
ApiClient({this.baseUrl = 'http://localhost:8080/api/v1'}) {
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
sendTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
// 请求拦截器:注入 JWT token
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
if (_token != null) {
options.headers['Authorization'] = 'Bearer $_token';
}
handler.next(options);
},
));
// 响应拦截器:统一错误处理
_dio.interceptors.add(InterceptorsWrapper(
onError: (error, handler) {
// 401 时自动清除 token需要重新登录
if (error.response?.statusCode == 401) {
_token = null;
}
handler.next(error);
},
));
}
/// 设置 JWT token登录成功后调用
void setToken(String token) => _token = token;
/// 清除 JWT token退出登录时调用
void clearToken() => _token = null;
/// 当前是否已登录
bool get isAuthenticated => _token != null;
/// 检查网络是否可用
Future<bool> _isOnline() async {
final result = await Connectivity().checkConnectivity();
// connectivity_plus 返回 List<ConnectivityResult>
return result.any((r) => r != ConnectivityResult.none);
}
/// 确保网络可用,否则抛出 OfflineException
Future<void> _ensureOnline() async {
final online = await _isOnline();
if (!online) {
throw const OfflineException();
}
}
// ===== CRUD 方法 =====
/// GET 请求
Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParams,
}) async {
await _ensureOnline();
return _dio.get<T>(path, queryParameters: queryParams);
}
/// POST 请求(创建资源)
Future<Response<T>> post<T>(
String path, {
dynamic data,
}) async {
await _ensureOnline();
return _dio.post<T>(path, data: data);
}
/// PUT 请求(更新资源)
Future<Response<T>> put<T>(
String path, {
dynamic data,
}) async {
await _ensureOnline();
return _dio.put<T>(path, data: data);
}
/// DELETE 请求
Future<Response<T>> delete<T>(
String path, {
dynamic data,
}) async {
await _ensureOnline();
return _dio.delete<T>(path, data: data);
}
/// PATCH 请求(部分更新)
Future<Response<T>> patch<T>(
String path, {
dynamic data,
}) async {
await _ensureOnline();
return _dio.patch<T>(path, data: data);
}
/// 文件上传multipart/form-data
Future<Response<T>> upload<T>(
String path, {
required String filePath,
required String fileName,
String fieldName = 'file',
Map<String, dynamic>? extraFields,
}) async {
await _ensureOnline();
final formData = FormData.fromMap({
fieldName: await MultipartFile.fromFile(filePath, filename: fileName),
...?extraFields,
});
return _dio.post<T>(path, data: formData);
}
}