Template
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user