主要变更: 1. 重构存储层导入路径,将app_kv_store替换为kv_storage 2. 移除AppKVStore初始化代码,统一使用KvStorage 3. 修复壁纸健康检测逻辑,使用最新检查时间判断检测间隔 4. 调整主页头部容器高度与裁剪行为 5. 新增引导页下次显示开关与Riverpod提供者 6. 修复API响应List类型转换崩溃问题 7. 优化部分文件头注释格式
208 lines
5.5 KiB
Dart
208 lines
5.5 KiB
Dart
/// ============================================================
|
|
/// 闲言APP — 崩溃日志服务
|
|
/// 创建时间: 2026-05-21
|
|
/// 更新时间: 2026-05-21
|
|
/// 作用: 持久化存储崩溃/异常日志,支持增删查清空,自动限制数量
|
|
/// 上次更新: 初始创建
|
|
/// ============================================================
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
import '../utils/logger.dart';
|
|
|
|
class CrashLogEntry {
|
|
const CrashLogEntry({
|
|
required this.id,
|
|
required this.errorId,
|
|
required this.time,
|
|
required this.error,
|
|
required this.stackTrace,
|
|
this.deviceInfo,
|
|
this.appVersion,
|
|
});
|
|
|
|
final String id;
|
|
final String errorId;
|
|
final DateTime time;
|
|
final String error;
|
|
final String stackTrace;
|
|
final String? deviceInfo;
|
|
final String? appVersion;
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'id': id,
|
|
'errorId': errorId,
|
|
'time': time.toIso8601String(),
|
|
'error': error,
|
|
'stackTrace': stackTrace,
|
|
if (deviceInfo != null) 'deviceInfo': deviceInfo,
|
|
if (appVersion != null) 'appVersion': appVersion,
|
|
};
|
|
|
|
factory CrashLogEntry.fromJson(Map<String, dynamic> json) => CrashLogEntry(
|
|
id: json['id'] as String,
|
|
errorId: json['errorId'] as String,
|
|
time: DateTime.parse(json['time'] as String),
|
|
error: json['error'] as String,
|
|
stackTrace: json['stackTrace'] as String,
|
|
deviceInfo: json['deviceInfo'] as String?,
|
|
appVersion: json['appVersion'] as String?,
|
|
);
|
|
|
|
String toDisplayText() {
|
|
final buffer = StringBuffer();
|
|
buffer.writeln('[$errorId] ${time.toIso8601String()}');
|
|
if (appVersion != null) buffer.writeln('版本: $appVersion');
|
|
if (deviceInfo != null) buffer.writeln('设备: $deviceInfo');
|
|
buffer.writeln();
|
|
buffer.writeln('Error:');
|
|
buffer.writeln(error);
|
|
buffer.writeln();
|
|
buffer.writeln('Stack Trace:');
|
|
buffer.writeln(stackTrace);
|
|
return buffer.toString();
|
|
}
|
|
}
|
|
|
|
class CrashLogService {
|
|
CrashLogService._();
|
|
static final CrashLogService instance = CrashLogService._();
|
|
|
|
static const _maxLogs = 100;
|
|
static const _fileName = 'crash_logs.json';
|
|
|
|
List<CrashLogEntry> _logs = [];
|
|
List<CrashLogEntry> get logs => List.unmodifiable(_logs);
|
|
int get count => _logs.length;
|
|
|
|
bool _loaded = false;
|
|
|
|
Future<File> _getFile() async {
|
|
final dir = await getApplicationSupportDirectory();
|
|
return File('${dir.path}/$_fileName');
|
|
}
|
|
|
|
Future<void> ensureLoaded() async {
|
|
if (_loaded) return;
|
|
await load();
|
|
}
|
|
|
|
Future<void> load() async {
|
|
try {
|
|
final file = await _getFile();
|
|
if (!await file.exists()) {
|
|
_logs = [];
|
|
_loaded = true;
|
|
return;
|
|
}
|
|
final content = await file.readAsString();
|
|
final list = jsonDecode(content) as List<dynamic>;
|
|
_logs = list
|
|
.map((e) => CrashLogEntry.fromJson(e as Map<String, dynamic>))
|
|
.toList();
|
|
_logs.sort((a, b) => b.time.compareTo(a.time));
|
|
_loaded = true;
|
|
Log.d('CrashLogService: 已加载 ${_logs.length} 条崩溃日志');
|
|
} catch (e) {
|
|
Log.e('CrashLogService: 加载失败', e);
|
|
_logs = [];
|
|
_loaded = true;
|
|
}
|
|
}
|
|
|
|
Future<void> _save() async {
|
|
try {
|
|
final file = await _getFile();
|
|
final json = jsonEncode(_logs.map((e) => e.toJson()).toList());
|
|
await file.writeAsString(json);
|
|
} catch (e) {
|
|
Log.e('CrashLogService: 保存失败', e);
|
|
}
|
|
}
|
|
|
|
Future<CrashLogEntry> addLog({
|
|
required String error,
|
|
required String stackTrace,
|
|
String? deviceInfo,
|
|
String? appVersion,
|
|
}) async {
|
|
await ensureLoaded();
|
|
final now = DateTime.now();
|
|
final id = now.millisecondsSinceEpoch.toString();
|
|
final errorId =
|
|
'ERR-${now.millisecondsSinceEpoch.toRadixString(36).toUpperCase()}';
|
|
|
|
final entry = CrashLogEntry(
|
|
id: id,
|
|
errorId: errorId,
|
|
time: now,
|
|
error: error,
|
|
stackTrace: stackTrace,
|
|
deviceInfo: deviceInfo,
|
|
appVersion: appVersion,
|
|
);
|
|
|
|
_logs.insert(0, entry);
|
|
|
|
if (_logs.length > _maxLogs) {
|
|
_logs.removeRange(_maxLogs, _logs.length);
|
|
}
|
|
|
|
await _save();
|
|
Log.d('CrashLogService: 新增崩溃日志 [$errorId]');
|
|
return entry;
|
|
}
|
|
|
|
Future<void> deleteLog(String id) async {
|
|
await ensureLoaded();
|
|
_logs.removeWhere((e) => e.id == id);
|
|
await _save();
|
|
}
|
|
|
|
Future<void> deleteLogs(List<String> ids) async {
|
|
await ensureLoaded();
|
|
final idSet = ids.toSet();
|
|
_logs.removeWhere((e) => idSet.contains(e.id));
|
|
await _save();
|
|
}
|
|
|
|
Future<void> clearAll() async {
|
|
_logs.clear();
|
|
await _save();
|
|
Log.i('CrashLogService: 已清空所有崩溃日志');
|
|
}
|
|
|
|
CrashLogEntry? getById(String id) {
|
|
for (final e in _logs) {
|
|
if (e.id == id) return e;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<String> exportAllAsText() async {
|
|
await ensureLoaded();
|
|
final buffer = StringBuffer();
|
|
buffer.writeln('闲言APP 崩溃日志导出');
|
|
buffer.writeln('导出时间: ${DateTime.now().toIso8601String()}');
|
|
buffer.writeln('条目数量: ${_logs.length}');
|
|
buffer.writeln('=' * 60);
|
|
|
|
for (final entry in _logs) {
|
|
buffer.writeln(entry.toDisplayText());
|
|
buffer.writeln('-' * 60);
|
|
}
|
|
|
|
return buffer.toString();
|
|
}
|
|
}
|
|
|
|
final crashLogServiceProvider = Provider<CrashLogService>((ref) {
|
|
final service = CrashLogService.instance;
|
|
ref.onDispose(() {});
|
|
return service;
|
|
});
|