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
This commit is contained in:
iven
2026-06-01 00:55:51 +08:00
parent d0653614e0
commit 5e6c6fdd62
18 changed files with 2205 additions and 1 deletions

View File

@@ -0,0 +1,149 @@
// 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);
}
}