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,232 @@
// 同步引擎 — WiFi 增量同步 + 操作队列
//
// 设计思路:
// - 所有本地修改先入队 [PendingOperation]
// - 网络恢复时自动批量同步
// - 版本号冲突检测Phase 1 使用"本地优先"策略
// - 最大重试次数限制,超过后标记为冲突供用户手动解决
//
// Phase 1 策略:本地优先
// - 离线时正常使用,操作入队等待
// - 联网后自动推送待同步操作
// - 版本冲突时本地版本覆盖远端(简单策略)
import 'dart:collection';
import 'package:connectivity_plus/connectivity_plus.dart';
import '../remote/api_client.dart';
/// 同步操作类型
enum SyncOperationType {
create('POST'),
update('PUT'),
delete('DELETE');
const SyncOperationType(this.httpMethod);
final String httpMethod;
}
/// 同步状态
enum SyncStatus {
idle, // 空闲,无待同步操作
syncing, // 正在同步
paused, // 暂停(网络不可用)
error, // 出错,需要重试
}
/// 待同步操作 — 记录一次本地修改
class PendingOperation {
final String id;
final SyncOperationType type;
final String endpoint;
final Map<String, dynamic> data;
final int version;
final DateTime createdAt;
final int retryCount;
/// 最大重试次数
static const int maxRetryCount = 5;
const PendingOperation({
required this.id,
required this.type,
required this.endpoint,
required this.data,
required this.version,
required this.createdAt,
this.retryCount = 0,
});
PendingOperation copyWith({
String? id,
SyncOperationType? type,
String? endpoint,
Map<String, dynamic>? data,
int? version,
DateTime? createdAt,
int? retryCount,
}) =>
PendingOperation(
id: id ?? this.id,
type: type ?? this.type,
endpoint: endpoint ?? this.endpoint,
data: data ?? this.data,
version: version ?? this.version,
createdAt: createdAt ?? this.createdAt,
retryCount: retryCount ?? this.retryCount,
);
/// 是否已超过最大重试次数
bool get isExhausted => retryCount >= maxRetryCount;
}
/// 同步引擎 — 管理 WiFi 增量同步和操作队列
///
/// 使用方式:
/// ```dart
/// final engine = SyncEngine(apiClient: apiClient);
///
/// // 本地修改后入队
/// engine.enqueue(PendingOperation(
/// id: 'op-1',
/// type: SyncOperationType.create,
/// endpoint: '/diary/entries',
/// data: entry.toJson(),
/// version: 1,
/// createdAt: DateTime.now(),
/// ));
///
/// // 网络恢复时触发同步
/// await engine.trySync();
/// ```
class SyncEngine {
final ApiClient _apiClient;
final Queue<PendingOperation> _pendingQueue = Queue();
SyncStatus _status = SyncStatus.idle;
String? _lastError;
SyncEngine({required this._apiClient});
/// 当前同步状态
SyncStatus get status => _status;
/// 最近一次错误信息
String? get lastError => _lastError;
/// 待同步操作数量
int get pendingCount => _pendingQueue.length;
/// 是否有操作正在同步
bool get isSyncing => _status == SyncStatus.syncing;
/// 添加待同步操作到队列尾部
void enqueue(PendingOperation operation) {
_pendingQueue.add(operation);
if (_status == SyncStatus.idle) {
_status = SyncStatus.paused;
}
}
/// 批量添加待同步操作
void enqueueAll(List<PendingOperation> operations) {
for (final op in operations) {
_pendingQueue.add(op);
}
if (_status == SyncStatus.idle && _pendingQueue.isNotEmpty) {
_status = SyncStatus.paused;
}
}
/// 检查网络状态并尝试同步全部待处理操作
///
/// 同步策略:
/// 1. 检查网络是否可用
/// 2. 按先进先出顺序处理队列
/// 3. 每个操作最多重试 [PendingOperation.maxRetryCount] 次
/// 4. 超过重试次数的操作标记为冲突,移出队列
/// 5. 网络中断时暂停同步,保留剩余操作
Future<void> trySync() async {
if (_status == SyncStatus.syncing) return; // 防止重入
if (_pendingQueue.isEmpty) {
_status = SyncStatus.idle;
return;
}
// 检查网络
final connectivity = Connectivity();
final result = await connectivity.checkConnectivity();
final isOnline = result.any((r) => r != ConnectivityResult.none);
if (!isOnline) {
_status = SyncStatus.paused;
_lastError = '网络不可用';
return;
}
// WiFi 优先策略:仅在 WiFi 下自动同步Phase 1 简化)
// TODO: 添加用户设置允许蜂窝数据同步
_status = SyncStatus.syncing;
_lastError = null;
while (_pendingQueue.isNotEmpty) {
final operation = _pendingQueue.removeFirst();
try {
await _executeOperation(operation);
} on OfflineException {
// 网络中断,操作放回队列头部
_pendingQueue.addFirst(operation);
_status = SyncStatus.paused;
_lastError = '同步中断:网络不可用';
return;
} catch (e) {
// 操作失败,增加重试计数
final retried = operation.copyWith(retryCount: operation.retryCount + 1);
if (retried.isExhausted) {
// 超过最大重试次数标记为冲突Phase 1 简化:丢弃)
// TODO: Phase 2 将冲突操作持久化,提供 UI 让用户手动解决
_lastError = '操作同步失败(已耗尽重试次数): ${operation.endpoint}';
continue;
}
// 放回队列头部,下次重试
_pendingQueue.addFirst(retried);
_status = SyncStatus.error;
_lastError = '同步失败: $e';
return;
}
}
// 全部同步完成
_status = SyncStatus.idle;
_lastError = null;
}
/// 执行单个同步操作
Future<void> _executeOperation(PendingOperation operation) async {
switch (operation.type) {
case SyncOperationType.create:
await _apiClient.post(operation.endpoint, data: operation.data);
case SyncOperationType.update:
await _apiClient.put(operation.endpoint, data: operation.data);
case SyncOperationType.delete:
await _apiClient.delete(operation.endpoint);
}
}
/// 清空队列(数据已全部同步完成或需要强制清空时调用)
void clear() {
_pendingQueue.clear();
_status = SyncStatus.idle;
_lastError = null;
}
/// 获取当前队列中所有操作的快照(用于持久化到本地存储)
///
/// 应用退出时调用此方法,将待同步操作保存到 Isar
/// 下次启动时通过 [enqueueAll] 恢复。
List<PendingOperation> get snapshot => _pendingQueue.toList();
}