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
@@ -0,0 +1,99 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:sunny_mochi/core/config/api_paths.dart';
import 'package:sunny_mochi/core/config/env.dart' show Env;
import 'package:sunny_mochi/core/network/mock/mock_response_loader.dart';
import 'package:sunny_mochi/core/observability/talker_setup.dart';
/// 把 Dio 请求拦截,根据 path + method 返回 fixtures 中的预设响应。
///
/// 启用方式:在 dio_client.dart 中检查 [Env.useMock]
/// ```dart
/// if (Env.useMock) {
/// dio.httpClientAdapter = buildDefaultMockAdapter();
/// }
/// ```
class DioMockAdapter implements HttpClientAdapter {
DioMockAdapter({this.delay = const Duration(milliseconds: 200)});
final Map<String, String> _routes = {};
final Duration delay;
bool _closed = false;
/// 注册一条 mock 路由:method+path → fixtures 文件名(不含 .json 后缀)。
void register(String method, String path, String fixtureName) {
_routes['${method.toUpperCase()} $path'] = fixtureName;
}
@override
void close({bool force = false}) {
_closed = true;
}
@override
Future<ResponseBody> fetch(
RequestOptions options,
Stream<Uint8List>? requestStream,
Future<void>? cancelFuture,
) async {
if (_closed) {
throw DioException(requestOptions: options, message: 'adapter closed');
}
await Future<void>.delayed(delay);
final key = '${options.method} ${options.path}';
final exact = _routes[key];
var matched = exact;
if (matched == null) {
for (final entry in _routes.entries) {
if (key.startsWith(entry.key)) {
matched = entry.value;
break;
}
}
}
if (matched == null) {
appTalker.warning('[Mock] 未注册 fixture$key — 返回空 envelope');
final body = jsonEncode({
'code': '00000',
'msg': 'mock-empty',
'data': null,
});
return ResponseBody.fromString(
body,
200,
headers: {'content-type': ['application/json']},
);
}
appTalker.verbose('[Mock] $key → fixtures/$matched.json');
final json = await MockResponseLoader.load(matched);
final body = jsonEncode(json);
return ResponseBody.fromString(
body,
200,
headers: {'content-type': ['application/json']},
);
}
}
/// 默认的全局 mock 路由表。
// TODO: 按项目实际 API 路径扩展此路由表
DioMockAdapter buildDefaultMockAdapter() {
return DioMockAdapter()
// === Auth ===
..register('POST', ApiPaths.authLoginSms, 'auth/login_success')
..register('POST', ApiPaths.authLoginPwd, 'auth/login_success')
..register('POST', ApiPaths.authSmsSend, 'auth/send_sms_code_success')
..register('POST', ApiPaths.authRefresh, 'auth/refresh_success')
..register('POST', ApiPaths.crashReport, 'common/generic_success')
// === User / Mine ===
..register('GET', ApiPaths.userProfile, 'mine/profile')
..register('POST', ApiPaths.userChangePassword, 'common/generic_success')
..register('POST', ApiPaths.userAvatarUpload, 'mine/avatar_upload');
}
@@ -0,0 +1,21 @@
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
/// 从 assets/fixtures/ 加载 JSON 响应。
///
/// 使用:
/// ```dart
/// final body = await MockResponseLoader.load('auth/login_success');
/// ```
abstract class MockResponseLoader {
static final Map<String, Map<String, dynamic>> _cache = {};
static Future<Map<String, dynamic>> load(String name) async {
if (_cache.containsKey(name)) return _cache[name]!;
final raw = await rootBundle.loadString('assets/fixtures/$name.json');
final json = jsonDecode(raw) as Map<String, dynamic>;
_cache[name] = json;
return json;
}
}