/// ============================================================ /// 闲言APP — 纠错状态管理 /// 创建时间: 2026-04-28 /// 更新时间: 2026-06-19 /// 作用: 纠错提交功能状态管理 + 纠错历史本地缓存(drift) /// 上次更新: 修复JSON类型安全问题,使用SafeJson.parseInt替代as int? ?? 0 /// ============================================================ import 'package:drift/drift.dart' show Value; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:xianyan/core/utils/safe_json.dart'; import '../../../core/network/api_client.dart'; import '../../../core/storage/database/app_database.dart'; import '../../../core/utils/logger.dart'; /// 纠错记录视图模型(统一本地与服务端字段) /// /// 用于在 UI 层展示,避免直接暴露 drift 数据类与服务端 Map。 class CorrectionItem { const CorrectionItem({ required this.type, required this.sourceType, required this.sourceId, required this.switchVal, required this.isLocal, required this.createtime, this.content = '', this.username = '', this.email = '', this.sourceUrl = '', this.isAnonymous = false, this.localId, }); /// 纠错类型: error/typo/missing/suggestion final String type; /// 内容类型: article/hanzi/cy/poetry/zc/riddle/other final String sourceType; /// 内容 ID final int sourceId; /// 状态码: 0=待处理, 1=已处理, 2=已拒绝 final int switchVal; /// 是否本地来源 final bool isLocal; /// 服务端创建时间戳(秒级) final int createtime; /// 纠错描述内容 final String content; /// 提交者用户名 final String username; /// 提交者邮箱 final String email; /// 来源 URL final String sourceUrl; /// 是否匿名 final bool isAnonymous; /// 本地数据库 ID(仅本地缓存记录有值) final int? localId; /// 从服务端 Map 构造 factory CorrectionItem.fromServerMap(Map map) { final rawSourceId = map['source_id']; int sourceId = 0; if (rawSourceId is int) { sourceId = rawSourceId; } else if (rawSourceId is String) { sourceId = int.tryParse(rawSourceId) ?? 0; } final rawSwitch = map['switch']; int switchVal = 0; if (rawSwitch is int) { switchVal = rawSwitch; } else if (rawSwitch is String) { switchVal = int.tryParse(rawSwitch) ?? 0; } final rawIsLocal = map['is_local']; bool isLocal = false; if (rawIsLocal is bool) { isLocal = rawIsLocal; } else if (rawIsLocal is int) { isLocal = rawIsLocal == 1; } return CorrectionItem( type: map['type'] as String? ?? 'error', sourceType: map['source_type'] as String? ?? 'other', sourceId: sourceId, switchVal: switchVal, isLocal: isLocal, createtime: SafeJson.parseInt(map['createtime']), content: map['content'] as String? ?? '', username: map['username'] as String? ?? '', email: map['mail'] as String? ?? '', sourceUrl: map['source_url'] as String? ?? '', ); } /// 从 drift 数据行构造 factory CorrectionItem.fromDb(CorrectionRecord row) { return CorrectionItem( type: row.type, sourceType: row.sourceType, sourceId: row.sourceId, switchVal: row.switchVal, isLocal: row.isLocal, createtime: row.createtime, content: row.content, username: row.username, email: row.email, sourceUrl: row.sourceUrl, isAnonymous: row.isAnonymous, localId: row.id, ); } /// 转换为 drift Companion(用于本地写入) CorrectionRecordsCompanion toCompanion({bool sync = true}) { final now = DateTime.now(); return CorrectionRecordsCompanion( type: Value(type), sourceType: Value(sourceType), sourceId: Value(sourceId), content: Value(content), username: Value(username), email: Value(email), sourceUrl: Value(sourceUrl), switchVal: Value(switchVal), isLocal: Value(isLocal), isAnonymous: Value(isAnonymous), isSynced: Value(sync), createtime: Value(createtime), localCreatedAt: Value(now), updatedAt: Value(now), ); } } class CorrectionState { const CorrectionState({ this.isSubmitting = false, this.isSuccess = false, this.error, this.corrections = const [], this.total = 0, this.isLoadingFromCache = false, this.isSyncing = false, }); final bool isSubmitting; final bool isSuccess; final String? error; final List corrections; final int total; /// 是否正在从本地缓存加载 final bool isLoadingFromCache; /// 是否正在与服务器同步 final bool isSyncing; CorrectionState copyWith({ bool? isSubmitting, bool? isSuccess, String? error, bool clearError = false, List? corrections, int? total, bool? isLoadingFromCache, bool? isSyncing, }) { return CorrectionState( isSubmitting: isSubmitting ?? this.isSubmitting, isSuccess: isSuccess ?? this.isSuccess, error: clearError ? null : (error ?? this.error), corrections: corrections ?? this.corrections, total: total ?? this.total, isLoadingFromCache: isLoadingFromCache ?? this.isLoadingFromCache, isSyncing: isSyncing ?? this.isSyncing, ); } } class CorrectionNotifier extends Notifier { @override CorrectionState build() { // 启动时异步加载本地缓存 Future.microtask(_loadFromCache); return const CorrectionState(); } CorrectionNotifier(); final ApiClient _api = ApiClient.instance; AppDatabase get _db => AppDatabase.instance; /// 从本地缓存加载纠错历史(离线可用) Future _loadFromCache() async { state = state.copyWith(isLoadingFromCache: true); try { final rows = await _db.getCorrectionRecords(); final items = rows.map(CorrectionItem.fromDb).toList(); final count = await _db.getCorrectionRecordCount(); state = state.copyWith( corrections: items, total: count, isLoadingFromCache: false, ); Log.i('纠错历史本地缓存加载: ${items.length} 条'); } catch (e) { Log.e('纠错历史本地缓存加载失败', e); state = state.copyWith(isLoadingFromCache: false); } } /// 提交纠错 /// /// 成功后将记录写入本地缓存;失败时也写入本地(标记 isSynced=false), /// 便于后续同步。返回 true 表示服务器接收成功。 Future submitCorrection({ required String targetType, required int targetId, required String content, String type = 'error', String? username, String? email, String? sourceUrl, bool isAnonymous = false, }) async { state = state.copyWith( isSubmitting: true, clearError: true, isSuccess: false, ); try { final submitData = { 'content': content, 'source_type': targetType, 'source_id': targetId, 'type': type, 'switch': isAnonymous ? 1 : 0, }; if (username != null && username.isNotEmpty) { submitData['username'] = username; } if (email != null && email.isNotEmpty) { submitData['mail'] = email; } if (sourceUrl != null && sourceUrl.isNotEmpty) { submitData['source_url'] = sourceUrl; } final response = await _api.post>( '/api/webapi/correction_submit', data: submitData, ); final respData = response.data as Map; final code = SafeJson.parseInt(respData['code']); if (code == 1) { // 提交成功,写入本地缓存 final nowSec = DateTime.now().millisecondsSinceEpoch ~/ 1000; final item = CorrectionItem( type: type, sourceType: targetType, sourceId: targetId, switchVal: 0, isLocal: true, createtime: nowSec, content: content, username: username ?? '', email: email ?? '', sourceUrl: sourceUrl ?? '', isAnonymous: isAnonymous, ); await _db.insertCorrectionRecord(item.toCompanion()); // 刷新本地缓存视图 await _loadFromCache(); state = state.copyWith(isSubmitting: false, isSuccess: true); Log.i('纠错提交成功,已写入本地缓存'); return true; } else { state = state.copyWith( isSubmitting: false, error: respData['msg'] as String? ?? '提交失败', ); return false; } } catch (e) { Log.e('纠错提交异常', e); // 网络异常时也写入本地(标记未同步),便于后续重试 final nowSec = DateTime.now().millisecondsSinceEpoch ~/ 1000; final item = CorrectionItem( type: type, sourceType: targetType, sourceId: targetId, switchVal: 0, isLocal: true, createtime: nowSec, content: content, username: username ?? '', email: email ?? '', sourceUrl: sourceUrl ?? '', isAnonymous: isAnonymous, ); await _db.insertCorrectionRecord(item.toCompanion(sync: false)); await _loadFromCache(); state = state.copyWith(isSubmitting: false, error: '提交失败: $e'); return false; } } /// 加载纠错历史(先读本地 → 立即渲染 → 再请求服务器 → 更新本地) Future loadCorrections({int page = 1, int limit = 20}) async { // 1. 先读本地缓存,立即渲染(离线可用) if (state.corrections.isEmpty) { await _loadFromCache(); } // 2. 请求服务器同步 state = state.copyWith(isSyncing: true); try { final response = await _api.get>( '/api/webapi/correction_list', queryParameters: {'page': page, 'limit': limit}, ); final data = response.data as Map; final code = SafeJson.parseInt(data['code']); if (code == 1) { final result = data['data'] as Map? ?? {}; final list = (result['list'] as List? ?? []) .map((e) => CorrectionItem.fromServerMap(e as Map)) .toList(); final total = SafeJson.parseInt(result['total']); // 3. 全量替换本地缓存 await _db.replaceCorrectionRecords( list.map((e) => e.toCompanion()).toList(), ); state = state.copyWith( corrections: list, total: total, isSyncing: false, ); Log.i('纠错历史服务器同步成功: ${list.length} 条'); } else { state = state.copyWith(isSyncing: false); } } catch (e) { Log.e('加载纠错列表失败(保留本地缓存)', e); state = state.copyWith(isSyncing: false); } } /// 清空本地纠错缓存(用于设置页"清除缓存"等场景) Future clearLocalCache() async { await _db.clearCorrectionRecords(); state = state.copyWith(corrections: [], total: 0); Log.i('纠错历史本地缓存已清空'); } } final correctionProvider = NotifierProvider( CorrectionNotifier.new, );