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 loginWithPhone(String phone, String code) async { final resp = await _dio.post>( ApiPaths.authLoginSms, data: {'account': phone, 'smsCode': code, 'loginType': 2}, options: _skipAuthOptions, ); return _unwrapLogin(resp.data); } Future loginWithPassword(String phone, String password) async { final resp = await _dio.post>( ApiPaths.authLoginPwd, data: {'account': phone, 'password': password, 'loginType': 1}, options: _skipAuthOptions, ); return _unwrapLogin(resp.data); } /// 返回 dev/test 环境后端回显的验证码明文(release 返回 null)。 Future sendSmsCode(String phone) async { final resp = await _dio.post>( ApiPaths.authSmsSend, data: {'phone': phone, 'codeType': 1}, options: _skipAuthOptions, ); final apiResp = parseEnvelope(resp.data, (j) => j as String?); if (!apiResp.isSuccess) { throw _mapper.fromBusinessCode(apiResp.retCode, apiResp.retMsg); } return apiResp.retData; } Future logout() async { await _dio.post(ApiPaths.authLogout); } Future getUserProfile() async { final resp = await _dio.get>(ApiPaths.userProfile); final body = resp.data; if (body == null) throw const ServerFailure(message: '服务器响应为空'); final apiResp = ApiResponse.fromJson( body, (json) => json as Map, ); if (!apiResp.isSuccess) { throw _mapper.fromBusinessCode(apiResp.retCode, apiResp.retMsg); } final data = apiResp.retData ?? const {}; 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 == '' || s == 'null') return null; return s; } /// 解析登录 envelope:data = {saTokenInfo: {...}, userInfo: {...}} UserModel _unwrapLogin(Map? body) { if (body == null) throw const ServerFailure(message: '服务器响应为空'); final apiResp = ApiResponse.fromJson( body, (json) => json as Map, ); 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?; final userInfo = data['userInfo'] as Map?; 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; } }