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,133 @@
import 'package:dio/dio.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sunny_mochi/core/config/api_paths.dart';
import 'package:sunny_mochi/core/error/exception_mapper.dart';
import 'package:sunny_mochi/core/error/failures.dart';
import 'package:sunny_mochi/core/network/api_response.dart';
import 'package:sunny_mochi/core/network/dio_client.dart';
import 'package:sunny_mochi/features/auth/data/models/user_model.dart';
part 'auth_remote_datasource.g.dart';
@riverpod
AuthRemoteDatasource authRemoteDatasource(Ref ref) =>
AuthRemoteDatasource(ref.watch(dioClientProvider));
class AuthRemoteDatasource {
const AuthRemoteDatasource(this._dio);
final Dio _dio;
static const _mapper = ExceptionMapper();
/// 登录/发码端点跳过 AuthInterceptor,避免携带残留 token。
static final _skipAuthOptions = Options(extra: const {'skip_auth': true});
Future<UserModel> loginWithPhone(String phone, String code) async {
final resp = await _dio.post<Map<String, dynamic>>(
ApiPaths.authLoginSms,
data: {'account': phone, 'smsCode': code, 'loginType': 2},
options: _skipAuthOptions,
);
return _unwrapLogin(resp.data);
}
Future<UserModel> loginWithPassword(String phone, String password) async {
final resp = await _dio.post<Map<String, dynamic>>(
ApiPaths.authLoginPwd,
data: {'account': phone, 'password': password, 'loginType': 1},
options: _skipAuthOptions,
);
return _unwrapLogin(resp.data);
}
/// 返回 dev/test 环境后端回显的验证码明文(release 返回 null)。
Future<String?> sendSmsCode(String phone) async {
final resp = await _dio.post<Map<String, dynamic>>(
ApiPaths.authSmsSend,
data: {'phone': phone, 'codeType': 1},
options: _skipAuthOptions,
);
final apiResp = parseEnvelope<String?>(resp.data, (j) => j as String?);
if (!apiResp.isSuccess) {
throw _mapper.fromBusinessCode(apiResp.retCode, apiResp.retMsg);
}
return apiResp.retData;
}
Future<void> logout() async {
await _dio.post<void>(ApiPaths.authLogout);
}
Future<UserModel> getUserProfile() async {
final resp = await _dio.get<Map<String, dynamic>>(ApiPaths.userProfile);
final body = resp.data;
if (body == null) throw const ServerFailure(message: '服务器响应为空');
final apiResp = ApiResponse.fromJson(
body,
(json) => json as Map<String, dynamic>,
);
if (!apiResp.isSuccess) {
throw _mapper.fromBusinessCode(apiResp.retCode, apiResp.retMsg);
}
final data = apiResp.retData ?? const <String, dynamic>{};
return UserModel(
userId: _flexStr(data['id'] ?? data['userId']),
realName: _clean(data['realName']),
username: _clean(data['realName']),
avatar: _clean(data['avatar']),
phone: _flexStr(data['phone']),
email: _clean(data['email']),
gender: (data['sex'] as num?)?.toInt(),
employeeNo: _clean(data['workNo']),
company: _clean(data['orgName']),
department: _clean(data['deptName']),
);
}
static String? _clean(dynamic v) {
if (v == null) return null;
final s = v.toString();
if (s.isEmpty || s == '<null>' || s == 'null') return null;
return s;
}
/// 解析登录 envelopedata = {saTokenInfo: {...}, userInfo: {...}}
UserModel _unwrapLogin(Map<String, dynamic>? body) {
if (body == null) throw const ServerFailure(message: '服务器响应为空');
final apiResp = ApiResponse.fromJson(
body,
(json) => json as Map<String, dynamic>,
);
if (!apiResp.isSuccess) {
throw _mapper.fromBusinessCode(apiResp.retCode, apiResp.retMsg);
}
final data = apiResp.retData;
if (data == null) throw const ServerFailure(message: '登录响应缺少用户数据');
final tokenInfo = data['saTokenInfo'] as Map<String, dynamic>?;
final userInfo = data['userInfo'] as Map<String, dynamic>?;
if (tokenInfo == null || userInfo == null) {
throw const ServerFailure(message: '登录响应结构异常');
}
return UserModel(
token: tokenInfo['tokenValue'] as String?,
tokenName: tokenInfo['tokenName'] as String?,
userId: _flexStr(tokenInfo['loginId']),
realName: userInfo['realName'] as String?,
username: userInfo['realName'] as String?,
avatar: userInfo['avatar'] as String?,
phone: _flexStr(userInfo['phone']),
email: userInfo['email'] as String?,
gender: (userInfo['sex'] as num?)?.toInt(),
employeeNo: userInfo['workNo'] as String?,
company: userInfo['orgName'] as String?,
department: userInfo['deptName'] as String?,
);
}
static String? _flexStr(dynamic v) {
if (v == null) return null;
if (v is String) return v.isEmpty ? null : v;
if (v is int) return v.toString();
return null;
}
}
@@ -0,0 +1,58 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'auth_remote_datasource.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(authRemoteDatasource)
final authRemoteDatasourceProvider = AuthRemoteDatasourceProvider._();
final class AuthRemoteDatasourceProvider
extends
$FunctionalProvider<
AuthRemoteDatasource,
AuthRemoteDatasource,
AuthRemoteDatasource
>
with $Provider<AuthRemoteDatasource> {
AuthRemoteDatasourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'authRemoteDatasourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$authRemoteDatasourceHash();
@$internal
@override
$ProviderElement<AuthRemoteDatasource> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
AuthRemoteDatasource create(Ref ref) {
return authRemoteDatasource(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(AuthRemoteDatasource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<AuthRemoteDatasource>(value),
);
}
}
String _$authRemoteDatasourceHash() =>
r'81f99f43779be5f073a2953ab9a3c1b80883aa9e';