chore: 初始化脚手架 — sunny_mochi 企业级 Flutter 模板

Core 基础设施:Flavor 三包体系、Drift+SQLCipher 加密数据库
7 拦截器 Dio 网络栈、CrashReporter 崩溃日志、Sealed Failure 错误体系
5 色板主题系统、Auth 认证骨架(Clean Architecture)、Dev Panel、Mock Adapter

通过验证:flutter analyze(0 errors)、flutter test(全绿)、staging APK 74.8MB
This commit is contained in:
SkyJourney
2026-05-14 12:51:05 +08:00
commit 61017f1c39
204 changed files with 15386 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:sunny_mochi/core/crash/pending_crash_report.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'crash_reporter.g.dart';
/// Riverpod provider:崩溃报告在 main.dart 通过 overrideWithValue 注入。
/// 默认值为 null(正常启动时无崩溃)。
@Riverpod(keepAlive: true)
PendingCrashReport? pendingCrash(Ref ref) => null;
/// 全局崩溃日志服务(纯静态,不依赖 Riverpod / Flutter framework)。
///
/// **生命周期**
/// 1. `preInit()` — main() 最早期调用,异步缓存路径 + 设备信息
/// 2. `consumePending()` — 读取并删除上次崩溃文件(一次性消费)
/// 3. `installHooks()` — 在 SentrySetup.init appRunner 内调用,链式挂载钩子
///
/// **崩溃写入**:仅用 `File.writeAsStringSync(flush: true)`,绝对不能 await。
abstract class CrashReporter {
static String? _crashFilePath;
static String _deviceModel = 'unknown';
static String _osVersion = 'unknown';
static String _appVersion = 'unknown';
static String _appBuild = '0';
/// 步骤 1:异步预初始化(收集路径 + 设备信息)。
static Future<void> preInit() async {
try {
final dir = await getApplicationDocumentsDirectory();
_crashFilePath = p.join(dir.path, 'crash_report.json');
final pkg = await PackageInfo.fromPlatform();
_appVersion = pkg.version;
_appBuild = pkg.buildNumber;
if (kIsWeb) return;
if (Platform.isAndroid) {
final info = await DeviceInfoPlugin().androidInfo;
_deviceModel = '${info.manufacturer} ${info.model}'.trim();
_osVersion = 'Android ${info.version.release}';
} else if (Platform.isIOS) {
final info = await DeviceInfoPlugin().iosInfo;
_deviceModel = info.utsname.machine;
_osVersion = '${info.systemName} ${info.systemVersion}';
} else {
_deviceModel = Platform.operatingSystem;
_osVersion = Platform.operatingSystemVersion;
}
} catch (_) {
// preInit 失败不能阻断启动流程
}
}
/// 步骤 2:一次性消费崩溃文件(读取 + 立即删除)。
/// 返回 null 表示本次是正常启动。
static PendingCrashReport? consumePending() {
final path = _crashFilePath;
if (path == null) return null;
final file = File(path);
if (!file.existsSync()) return null;
try {
final raw = file.readAsStringSync();
file.deleteSync();
return PendingCrashReport.fromRawJson(raw);
} catch (_) {
try {
file.deleteSync();
} catch (_) {}
return null;
}
}
/// 步骤 3:安装全局崩溃钩子(必须在 SentrySetup appRunner 内调用)。
///
/// 采用链式挂载:先读出 Sentry 已设置的 handler,我们包一层写回,
/// 确保 Sentry 依然能接到所有错误。
static void installHooks() {
final previousOnError = PlatformDispatcher.instance.onError;
PlatformDispatcher.instance.onError = (error, stack) {
_writeCrashSync(kind: 'dart', error: error, stackTrace: stack);
return previousOnError?.call(error, stack) ?? false;
};
}
/// 同步写入崩溃文件(crash hook 专用,不能有任何 await)。
static void _writeCrashSync({
required String kind,
required Object error,
required StackTrace stackTrace,
}) {
final path = _crashFilePath;
if (path == null) return;
try {
final stackStr = stackTrace.toString();
final truncated = stackStr.length > 4096
? '${stackStr.substring(0, 4096)}\n...(truncated)'
: stackStr;
final report = PendingCrashReport(
kind: kind,
error: error.toString(),
stackTrace: truncated,
occurredAt: DateTime.now().toUtc().toIso8601String(),
deviceModel: _deviceModel,
osVersion: _osVersion,
appVersion: _appVersion,
appBuild: _appBuild,
);
File(path).writeAsStringSync(
report.toRawJson(),
flush: true,
);
} catch (_) {
// crash hook 内绝对不能抛出异常
}
}
}