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,114 @@
import 'dart:async';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sunny_mochi/core/error/exception_mapper.dart';
import 'package:sunny_mochi/features/auth/auth_providers.dart';
import 'package:sunny_mochi/features/auth/domain/entities/user_entity.dart';
import 'package:sunny_mochi/features/auth/presentation/notifiers/auth_status_provider.dart';
part 'auth_notifier.freezed.dart';
part 'auth_notifier.g.dart';
@freezed
sealed class AuthState with _$AuthState {
const factory AuthState.initial() = AuthInitial;
const factory AuthState.loading() = AuthLoading;
const factory AuthState.authenticated(UserEntity user) = AuthAuthenticated;
const factory AuthState.unauthenticated() = AuthUnauthenticated;
const factory AuthState.error(String message) = AuthError;
}
@riverpod
class AuthNotifier extends _$AuthNotifier {
static const _mapper = ExceptionMapper();
// generation 计数:防止旧 async 回调覆盖最新登录态
int _generation = 0;
@override
AuthState build() {
final gen = ++_generation;
unawaited(_checkAuth(gen));
return const AuthState.initial();
}
Future<void> _checkAuth(int gen) async {
final user = await ref.read(authRepositoryProvider).getCurrentUser();
if (_generation != gen) return;
if (user != null) {
state = AuthState.authenticated(user);
ref.read(authStatusProvider).markLoggedIn();
unawaited(refreshUserInfo());
} else {
state = const AuthState.unauthenticated();
ref.read(authStatusProvider).markLoggedOut();
}
}
Future<void> loginWithPhone(String phone, String code) async {
_generation++;
state = const AuthState.loading();
try {
final user = await ref
.read(authRepositoryProvider)
.loginWithPhone(phone: phone, code: code);
state = AuthState.authenticated(user);
ref.read(authStatusProvider).markLoggedIn();
unawaited(refreshUserInfo());
} on Object catch (e, st) {
final failure = _mapper.fromUnknown(e, st);
state = AuthState.error(failure.message);
}
}
Future<void> loginWithPassword(String phone, String password) async {
_generation++;
state = const AuthState.loading();
try {
final user = await ref
.read(authRepositoryProvider)
.loginWithPassword(phone: phone, password: password);
state = AuthState.authenticated(user);
ref.read(authStatusProvider).markLoggedIn();
unawaited(refreshUserInfo());
} on Object catch (e, st) {
final failure = _mapper.fromUnknown(e, st);
state = AuthState.error(failure.message);
}
}
Future<void> logout() async {
_generation++;
state = const AuthState.unauthenticated();
ref.read(authStatusProvider).markLoggedOut();
unawaited(ref.read(authRepositoryProvider).logout());
}
/// 发送短信验证码。返回 dev/test 环境后端回显的验证码(release 为 null),供 UI 自动填入。
Future<String?> sendSmsCode(String phone) async {
try {
return await ref.read(authRepositoryProvider).sendSmsCode(phone);
} on Object catch (e, st) {
throw _mapper.fromUnknown(e, st);
}
}
Future<void> refreshUserInfo() async {
final current = state;
if (current is! AuthAuthenticated) return;
final gen = _generation;
try {
final fresh = await ref.read(authRepositoryProvider).fetchUserProfile();
if (_generation != gen) return;
state = AuthState.authenticated(
fresh.copyWith(
token: current.user.token,
userId: fresh.userId ?? current.user.userId,
),
);
} on Object {
// profile 拉取失败不应改变登录状态
}
}
}