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';
@@ -0,0 +1,42 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:sunny_mochi/features/auth/domain/entities/user_entity.dart';
part 'user_model.freezed.dart';
part 'user_model.g.dart';
@freezed
abstract class UserModel with _$UserModel {
const factory UserModel({
String? userId,
String? username,
String? realName,
String? phone,
String? avatar,
String? token,
String? tokenName,
String? refreshToken,
String? email,
DateTime? birthday,
int? gender,
String? employeeNo,
String? company,
String? department,
}) = _UserModel;
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);
}
extension UserModelX on UserModel {
UserEntity toEntity() => UserEntity(
userId: userId,
username: username,
realName: realName,
phone: phone,
avatar: avatar,
token: token,
employeeNo: employeeNo,
company: company,
department: department,
);
}
@@ -0,0 +1,316 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'user_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$UserModel {
String? get userId; String? get username; String? get realName; String? get phone; String? get avatar; String? get token; String? get tokenName; String? get refreshToken; String? get email; DateTime? get birthday; int? get gender; String? get employeeNo; String? get company; String? get department;
/// Create a copy of UserModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$UserModelCopyWith<UserModel> get copyWith => _$UserModelCopyWithImpl<UserModel>(this as UserModel, _$identity);
/// Serializes this UserModel to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is UserModel&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.username, username) || other.username == username)&&(identical(other.realName, realName) || other.realName == realName)&&(identical(other.phone, phone) || other.phone == phone)&&(identical(other.avatar, avatar) || other.avatar == avatar)&&(identical(other.token, token) || other.token == token)&&(identical(other.tokenName, tokenName) || other.tokenName == tokenName)&&(identical(other.refreshToken, refreshToken) || other.refreshToken == refreshToken)&&(identical(other.email, email) || other.email == email)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.employeeNo, employeeNo) || other.employeeNo == employeeNo)&&(identical(other.company, company) || other.company == company)&&(identical(other.department, department) || other.department == department));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,userId,username,realName,phone,avatar,token,tokenName,refreshToken,email,birthday,gender,employeeNo,company,department);
@override
String toString() {
return 'UserModel(userId: $userId, username: $username, realName: $realName, phone: $phone, avatar: $avatar, token: $token, tokenName: $tokenName, refreshToken: $refreshToken, email: $email, birthday: $birthday, gender: $gender, employeeNo: $employeeNo, company: $company, department: $department)';
}
}
/// @nodoc
abstract mixin class $UserModelCopyWith<$Res> {
factory $UserModelCopyWith(UserModel value, $Res Function(UserModel) _then) = _$UserModelCopyWithImpl;
@useResult
$Res call({
String? userId, String? username, String? realName, String? phone, String? avatar, String? token, String? tokenName, String? refreshToken, String? email, DateTime? birthday, int? gender, String? employeeNo, String? company, String? department
});
}
/// @nodoc
class _$UserModelCopyWithImpl<$Res>
implements $UserModelCopyWith<$Res> {
_$UserModelCopyWithImpl(this._self, this._then);
final UserModel _self;
final $Res Function(UserModel) _then;
/// Create a copy of UserModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? userId = freezed,Object? username = freezed,Object? realName = freezed,Object? phone = freezed,Object? avatar = freezed,Object? token = freezed,Object? tokenName = freezed,Object? refreshToken = freezed,Object? email = freezed,Object? birthday = freezed,Object? gender = freezed,Object? employeeNo = freezed,Object? company = freezed,Object? department = freezed,}) {
return _then(_self.copyWith(
userId: freezed == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable
as String?,username: freezed == username ? _self.username : username // ignore: cast_nullable_to_non_nullable
as String?,realName: freezed == realName ? _self.realName : realName // ignore: cast_nullable_to_non_nullable
as String?,phone: freezed == phone ? _self.phone : phone // ignore: cast_nullable_to_non_nullable
as String?,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable
as String?,token: freezed == token ? _self.token : token // ignore: cast_nullable_to_non_nullable
as String?,tokenName: freezed == tokenName ? _self.tokenName : tokenName // ignore: cast_nullable_to_non_nullable
as String?,refreshToken: freezed == refreshToken ? _self.refreshToken : refreshToken // ignore: cast_nullable_to_non_nullable
as String?,email: freezed == email ? _self.email : email // ignore: cast_nullable_to_non_nullable
as String?,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
as DateTime?,gender: freezed == gender ? _self.gender : gender // ignore: cast_nullable_to_non_nullable
as int?,employeeNo: freezed == employeeNo ? _self.employeeNo : employeeNo // ignore: cast_nullable_to_non_nullable
as String?,company: freezed == company ? _self.company : company // ignore: cast_nullable_to_non_nullable
as String?,department: freezed == department ? _self.department : department // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// Adds pattern-matching-related methods to [UserModel].
extension UserModelPatterns on UserModel {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _UserModel value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _UserModel() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _UserModel value) $default,){
final _that = this;
switch (_that) {
case _UserModel():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _UserModel value)? $default,){
final _that = this;
switch (_that) {
case _UserModel() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? userId, String? username, String? realName, String? phone, String? avatar, String? token, String? tokenName, String? refreshToken, String? email, DateTime? birthday, int? gender, String? employeeNo, String? company, String? department)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _UserModel() when $default != null:
return $default(_that.userId,_that.username,_that.realName,_that.phone,_that.avatar,_that.token,_that.tokenName,_that.refreshToken,_that.email,_that.birthday,_that.gender,_that.employeeNo,_that.company,_that.department);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? userId, String? username, String? realName, String? phone, String? avatar, String? token, String? tokenName, String? refreshToken, String? email, DateTime? birthday, int? gender, String? employeeNo, String? company, String? department) $default,) {final _that = this;
switch (_that) {
case _UserModel():
return $default(_that.userId,_that.username,_that.realName,_that.phone,_that.avatar,_that.token,_that.tokenName,_that.refreshToken,_that.email,_that.birthday,_that.gender,_that.employeeNo,_that.company,_that.department);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? userId, String? username, String? realName, String? phone, String? avatar, String? token, String? tokenName, String? refreshToken, String? email, DateTime? birthday, int? gender, String? employeeNo, String? company, String? department)? $default,) {final _that = this;
switch (_that) {
case _UserModel() when $default != null:
return $default(_that.userId,_that.username,_that.realName,_that.phone,_that.avatar,_that.token,_that.tokenName,_that.refreshToken,_that.email,_that.birthday,_that.gender,_that.employeeNo,_that.company,_that.department);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _UserModel implements UserModel {
const _UserModel({this.userId, this.username, this.realName, this.phone, this.avatar, this.token, this.tokenName, this.refreshToken, this.email, this.birthday, this.gender, this.employeeNo, this.company, this.department});
factory _UserModel.fromJson(Map<String, dynamic> json) => _$UserModelFromJson(json);
@override final String? userId;
@override final String? username;
@override final String? realName;
@override final String? phone;
@override final String? avatar;
@override final String? token;
@override final String? tokenName;
@override final String? refreshToken;
@override final String? email;
@override final DateTime? birthday;
@override final int? gender;
@override final String? employeeNo;
@override final String? company;
@override final String? department;
/// Create a copy of UserModel
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$UserModelCopyWith<_UserModel> get copyWith => __$UserModelCopyWithImpl<_UserModel>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$UserModelToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _UserModel&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.username, username) || other.username == username)&&(identical(other.realName, realName) || other.realName == realName)&&(identical(other.phone, phone) || other.phone == phone)&&(identical(other.avatar, avatar) || other.avatar == avatar)&&(identical(other.token, token) || other.token == token)&&(identical(other.tokenName, tokenName) || other.tokenName == tokenName)&&(identical(other.refreshToken, refreshToken) || other.refreshToken == refreshToken)&&(identical(other.email, email) || other.email == email)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.employeeNo, employeeNo) || other.employeeNo == employeeNo)&&(identical(other.company, company) || other.company == company)&&(identical(other.department, department) || other.department == department));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,userId,username,realName,phone,avatar,token,tokenName,refreshToken,email,birthday,gender,employeeNo,company,department);
@override
String toString() {
return 'UserModel(userId: $userId, username: $username, realName: $realName, phone: $phone, avatar: $avatar, token: $token, tokenName: $tokenName, refreshToken: $refreshToken, email: $email, birthday: $birthday, gender: $gender, employeeNo: $employeeNo, company: $company, department: $department)';
}
}
/// @nodoc
abstract mixin class _$UserModelCopyWith<$Res> implements $UserModelCopyWith<$Res> {
factory _$UserModelCopyWith(_UserModel value, $Res Function(_UserModel) _then) = __$UserModelCopyWithImpl;
@override @useResult
$Res call({
String? userId, String? username, String? realName, String? phone, String? avatar, String? token, String? tokenName, String? refreshToken, String? email, DateTime? birthday, int? gender, String? employeeNo, String? company, String? department
});
}
/// @nodoc
class __$UserModelCopyWithImpl<$Res>
implements _$UserModelCopyWith<$Res> {
__$UserModelCopyWithImpl(this._self, this._then);
final _UserModel _self;
final $Res Function(_UserModel) _then;
/// Create a copy of UserModel
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? userId = freezed,Object? username = freezed,Object? realName = freezed,Object? phone = freezed,Object? avatar = freezed,Object? token = freezed,Object? tokenName = freezed,Object? refreshToken = freezed,Object? email = freezed,Object? birthday = freezed,Object? gender = freezed,Object? employeeNo = freezed,Object? company = freezed,Object? department = freezed,}) {
return _then(_UserModel(
userId: freezed == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable
as String?,username: freezed == username ? _self.username : username // ignore: cast_nullable_to_non_nullable
as String?,realName: freezed == realName ? _self.realName : realName // ignore: cast_nullable_to_non_nullable
as String?,phone: freezed == phone ? _self.phone : phone // ignore: cast_nullable_to_non_nullable
as String?,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable
as String?,token: freezed == token ? _self.token : token // ignore: cast_nullable_to_non_nullable
as String?,tokenName: freezed == tokenName ? _self.tokenName : tokenName // ignore: cast_nullable_to_non_nullable
as String?,refreshToken: freezed == refreshToken ? _self.refreshToken : refreshToken // ignore: cast_nullable_to_non_nullable
as String?,email: freezed == email ? _self.email : email // ignore: cast_nullable_to_non_nullable
as String?,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
as DateTime?,gender: freezed == gender ? _self.gender : gender // ignore: cast_nullable_to_non_nullable
as int?,employeeNo: freezed == employeeNo ? _self.employeeNo : employeeNo // ignore: cast_nullable_to_non_nullable
as String?,company: freezed == company ? _self.company : company // ignore: cast_nullable_to_non_nullable
as String?,department: freezed == department ? _self.department : department // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
// dart format on
@@ -0,0 +1,44 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_UserModel _$UserModelFromJson(Map<String, dynamic> json) => _UserModel(
userId: json['userId'] as String?,
username: json['username'] as String?,
realName: json['realName'] as String?,
phone: json['phone'] as String?,
avatar: json['avatar'] as String?,
token: json['token'] as String?,
tokenName: json['tokenName'] as String?,
refreshToken: json['refreshToken'] as String?,
email: json['email'] as String?,
birthday: json['birthday'] == null
? null
: DateTime.parse(json['birthday'] as String),
gender: (json['gender'] as num?)?.toInt(),
employeeNo: json['employeeNo'] as String?,
company: json['company'] as String?,
department: json['department'] as String?,
);
Map<String, dynamic> _$UserModelToJson(_UserModel instance) =>
<String, dynamic>{
'userId': instance.userId,
'username': instance.username,
'realName': instance.realName,
'phone': instance.phone,
'avatar': instance.avatar,
'token': instance.token,
'tokenName': instance.tokenName,
'refreshToken': instance.refreshToken,
'email': instance.email,
'birthday': instance.birthday?.toIso8601String(),
'gender': instance.gender,
'employeeNo': instance.employeeNo,
'company': instance.company,
'department': instance.department,
};
@@ -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';