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,180 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:sunny_mochi/core/config/env.dart';
|
||||
import 'package:sunny_mochi/core/crypto/rsa_helper.dart';
|
||||
import 'package:sunny_mochi/core/error/failures.dart';
|
||||
import 'package:sunny_mochi/core/storage/app_database.dart';
|
||||
import 'package:sunny_mochi/core/storage/daos/users_dao.dart';
|
||||
import 'package:sunny_mochi/core/storage/secure_storage.dart';
|
||||
import 'package:sunny_mochi/features/auth/data/datasources/auth_remote_datasource.dart';
|
||||
import 'package:sunny_mochi/features/auth/data/models/user_model.dart';
|
||||
import 'package:sunny_mochi/features/auth/domain/entities/user_entity.dart';
|
||||
import 'package:sunny_mochi/features/auth/domain/repositories/auth_repository.dart';
|
||||
|
||||
part 'auth_repository_impl.g.dart';
|
||||
|
||||
@riverpod
|
||||
AuthRepository authRepository(Ref ref) => AuthRepositoryImpl(
|
||||
ref.watch(authRemoteDatasourceProvider),
|
||||
ref.watch(secureStorageProvider),
|
||||
usersDaoFactory: () => ref.read(usersDaoProvider),
|
||||
);
|
||||
|
||||
class AuthRepositoryImpl implements AuthRepository {
|
||||
AuthRepositoryImpl(
|
||||
this._remote,
|
||||
this._storage, {
|
||||
String? rsaPublicKey,
|
||||
UsersDao Function()? usersDaoFactory,
|
||||
}) : _rsaPublicKey = rsaPublicKey ?? Env.rsaPublicKey,
|
||||
_usersDaoFactory = usersDaoFactory;
|
||||
|
||||
final AuthRemoteDatasource _remote;
|
||||
final SecureStorage _storage;
|
||||
final String _rsaPublicKey;
|
||||
final UsersDao Function()? _usersDaoFactory;
|
||||
|
||||
@override
|
||||
Future<UserEntity> loginWithPhone({
|
||||
required String phone,
|
||||
required String code,
|
||||
}) async {
|
||||
final model = await _remote.loginWithPhone(phone, code);
|
||||
await _persistUser(model);
|
||||
return model.toEntity();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserEntity> loginWithPassword({
|
||||
required String phone,
|
||||
required String password,
|
||||
}) async {
|
||||
final String encrypted;
|
||||
try {
|
||||
encrypted = RsaHelper.encrypt(password, publicKeyDerBase64: _rsaPublicKey);
|
||||
} on RsaEncryptionException catch (e, st) {
|
||||
throw AuthFailure(
|
||||
message: '密码加密失败,请重试',
|
||||
kind: AuthFailureKind.cryptoFailed,
|
||||
cause: e,
|
||||
stackTrace: st,
|
||||
);
|
||||
}
|
||||
final model = await _remote.loginWithPassword(phone, encrypted);
|
||||
await _persistUser(model);
|
||||
return model.toEntity();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> logout() async {
|
||||
final userId = await _storage.getUserId();
|
||||
await _storage.clearAll();
|
||||
unawaited(_backgroundLogoutCleanup(userId));
|
||||
}
|
||||
|
||||
Future<void> _backgroundLogoutCleanup(String? userId) async {
|
||||
try {
|
||||
await _remote.logout();
|
||||
} on Object {
|
||||
// best-effort
|
||||
}
|
||||
if (userId != null && _usersDaoFactory != null) {
|
||||
try {
|
||||
await _usersDaoFactory().deleteById(userId);
|
||||
} on Object {
|
||||
// mock/测试场景静默
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserEntity?> getCurrentUser() async {
|
||||
final token = await _storage.getToken();
|
||||
if (token == null) return null;
|
||||
final userId = await _storage.getUserId();
|
||||
if (userId != null && _usersDaoFactory != null) {
|
||||
try {
|
||||
final row = await _usersDaoFactory().getById(userId);
|
||||
if (row != null) {
|
||||
return UserEntity(
|
||||
userId: userId,
|
||||
token: token,
|
||||
username: row.username,
|
||||
realName: row.realName,
|
||||
avatar: row.avatar,
|
||||
phone: row.phone,
|
||||
employeeNo: row.employeeNo,
|
||||
company: row.company,
|
||||
department: row.department,
|
||||
);
|
||||
}
|
||||
} on Object {
|
||||
// Drift 不可用时降级
|
||||
}
|
||||
}
|
||||
return UserEntity(userId: userId, token: token);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> sendSmsCode(String phone) => _remote.sendSmsCode(phone);
|
||||
|
||||
@override
|
||||
Future<UserEntity> fetchUserProfile() async {
|
||||
final model = await _remote.getUserProfile();
|
||||
final userId = await _storage.getUserId();
|
||||
if (userId != null && _usersDaoFactory != null) {
|
||||
try {
|
||||
await _usersDaoFactory().upsert(
|
||||
UsersCompanion(
|
||||
userId: Value(userId),
|
||||
username: Value(model.username),
|
||||
realName: Value(model.realName),
|
||||
phone: Value(model.phone),
|
||||
avatar: Value(model.avatar),
|
||||
email: Value(model.email),
|
||||
gender: Value(model.gender),
|
||||
employeeNo: Value(model.employeeNo),
|
||||
company: Value(model.company),
|
||||
department: Value(model.department),
|
||||
),
|
||||
);
|
||||
} on Object {
|
||||
// dao 不可用静默
|
||||
}
|
||||
}
|
||||
return model.toEntity();
|
||||
}
|
||||
|
||||
Future<void> _persistUser(UserModel model) async {
|
||||
if (model.token != null) await _storage.setToken(model.token!);
|
||||
if (model.tokenName != null && model.tokenName!.isNotEmpty) {
|
||||
await _storage.setTokenName(model.tokenName!);
|
||||
}
|
||||
if (model.refreshToken != null) {
|
||||
await _storage.setRefreshToken(model.refreshToken!);
|
||||
}
|
||||
if (model.userId != null) await _storage.setUserId(model.userId!);
|
||||
final userId = model.userId;
|
||||
if (userId != null && _usersDaoFactory != null) {
|
||||
try {
|
||||
await _usersDaoFactory().upsert(
|
||||
UsersCompanion(
|
||||
userId: Value(userId),
|
||||
username: Value(model.username),
|
||||
realName: Value(model.realName),
|
||||
phone: Value(model.phone),
|
||||
avatar: Value(model.avatar),
|
||||
email: Value(model.email),
|
||||
birthday: Value(model.birthday),
|
||||
gender: Value(model.gender),
|
||||
refreshToken: Value(model.refreshToken),
|
||||
),
|
||||
);
|
||||
} on Object {
|
||||
// dao 不可用静默
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'auth_repository_impl.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(authRepository)
|
||||
final authRepositoryProvider = AuthRepositoryProvider._();
|
||||
|
||||
final class AuthRepositoryProvider
|
||||
extends $FunctionalProvider<AuthRepository, AuthRepository, AuthRepository>
|
||||
with $Provider<AuthRepository> {
|
||||
AuthRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'authRepositoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$authRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<AuthRepository> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
AuthRepository create(Ref ref) {
|
||||
return authRepository(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(AuthRepository value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<AuthRepository>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$authRepositoryHash() => r'3a8f259bef644802bc3ef4169a8456a3a05aaea8';
|
||||
Reference in New Issue
Block a user