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,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;
|
||||
}
|
||||
|
||||
/// 解析登录 envelope:data = {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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user