Files
flutter-template/lib/features/auth/data/datasources/auth_remote_datasource.dart
T
SkyJourney 61017f1c39 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
2026-05-14 12:51:05 +08:00

134 lines
4.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}