import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:new_learn/core/api/auth/model.dart'; import 'package:new_learn/core/models/user_model.dart'; import 'package:new_learn/core/network/dio_client.dart'; import 'package:new_learn/core/storage/user_storage.dart'; /// ============================================================ /// 【UserApi】用户相关接口(对齐 Vue 端 api/user/index.ts) /// /// 分层职责(像 Vue 项目把接口收敛到 api 目录一样): /// Page(UI) → UserApi(业务方法) → DioClient(Dio 封装) → 服务器 /// /// 一、已实现: /// login → POST /api/v1/User/Login /// getCurrentUserExtension → GET /api/v1/User/GetCurrentUserExtension /// (登录后拿完整用户信息:昵称/头像/班级/等级等) /// getCurrentUser → 统一入口(本地缓存优先) /// updateUserAvatar → 更改用户头像 /// /// 二、后续继续写(对齐 Vue,用到再加): /// checkStudyPackStatus / setChangeUserOnline / getUserPointLevel /// /// 三、高频坑: /// 1. 登录密码 MD5 加密(对齐 Vue stringToMd5),提交前在 Api 层完成; /// 2. getCurrentUserExtension 必须带 Token 才有效 —— /// 登录成功已把 Token 写进 DioClient.accessToken,自动带上; /// 3. 不要在这里 catch ApiException —— 交给页面统一处理展示。 /// ============================================================ class UserApi { // ==================== 登录 ==================== /// 登录 → POST /api/v1/User/Login /// 密码规则对齐 Vue 端 stringToMd5:MD5 加密后再提交 static Future login({ required String account, required String password, }) async { final request = LoginRequest( account: account.trim(), password: _md5(password.trim()), ); final data = await DioClient.instance.post( '/api/v1/User/Login', data: request.toJson(), ); return LoginResponse.fromJson(data as Map); } /// 登录后拉取完整用户信息 → GET /api/v1/User/GetCurrentUserExtension static Future getCurrentUserExtension() async { final data = await DioClient.instance.get( '/api/v1/User/GetCurrentUserExtension', ); return CurrentUserExtension.fromJson(data as Map); } /// 获取当前用户(统一入口,缓存优先): /// 1. 有本地缓存 → 直接返回(不费流量不转圈); /// 2. 无缓存或 forceRefresh → 调 GetCurrentUserExtension 拉最新 /// (Token 由 Dio 拦截器自动带进 header)并写回缓存; /// 3. 接口失败 → 回退返回本地缓存,缓存也没有才抛异常。 /// 页面展示用户信息一律走这里,别直接调 getCurrentUserExtension。 static Future getCurrentUser({ bool forceRefresh = false, }) async { final cached = await UserStorage.instance.readUserInfo(); if (!forceRefresh && cached != null) return cached; try { final ext = await getCurrentUserExtension(); await UserStorage.instance.saveUserInfo(ext); return ext; } catch (_) { if (cached != null) return cached; // 网络失败回退本地缓存 rethrow; } } /// 更改用户头像 → POST /api/v1/User/UpdateUserAvatar /// /// 对齐 UniApp:头像先传 OSS,拿到 url 后再把 {avatar: url} 提交给业务接口。 static Future updateUserAvatar(String avatar) async { await DioClient.instance.post( '/api/v1/User/UpdateUserAvatar', data: {'avatar': avatar}, ); } /// 注销账号 → POST /api/v1/User/CloseAccount /// /// 对齐 Vue api/login: `closeAccount = () => request.post('/api/v1/User/CloseAccount')` /// 调用方负责弹确认框 + 注销后清理本地状态。 static Future closeAccount() async { await DioClient.instance.post('/api/v1/User/CloseAccount'); } // ==================== 注册 ==================== /// 注册 → POST /api/v1/User/Register /// /// 对齐 Vue api/login `register`: { account, password },密码同样 MD5 加密 /// Vue RegisterResponse = { code, message }(业务响应壳),Flutter 端 DioClient /// 已自动处理成功/失败分支,成功不关心 body,失败直接抛 ApiException。 static Future register({ required String account, required String password, }) async { await DioClient.instance.post( '/api/v1/User/Register', data: {'account': account.trim(), 'password': _md5(password.trim())}, ); } // ==================== 审核模式 ==================== /// 获取审核模式(决定是否展示"去注册"入口)→ GET /api/v1/Public/GetIsAuditMode /// /// 对齐 Vue api/login: `getAuditMode()`,返回值本身就是 boolean /// (request 拦截器已解包),接口不可用时安全回退 false(不展示注册入口)。 static Future getAuditMode() async { try { final data = await DioClient.instance.get( '/api/v1/Public/GetIsAuditMode', ); if (data is bool) return data; if (data is Map) { // 兼容 { isAuditMode: true } 包装 final v = data['isAuditMode']; if (v is bool) return v; } return false; } catch (_) { return false; } } // ==================== 报告状态更新 ==================== /// 更新入门测试报告显示状态(看完后置回 false) /// → POST /api/v1/User/ChangeShowReport /// /// 对齐 Vue api/user/index.ts 的 UpdateReportStatus(), /// 在 reportComputation finishedPopup 点"立即开启学习蝶变"后调用。 static Future changeShowReport() async { await DioClient.instance.post('/api/v1/User/ChangeShowReport'); } /// 更新出门测试报告显示状态(看完后置回 false) /// → POST /api/v1/User/ChangeShowAfterReport /// /// 对齐 H5 壳子 api/mock/index.ts 的 updateAfterReportStatus(), /// 在出门测 H5 壳子 navigateReport() 里由 Flutter JS Bridge 拦截后调用。 static Future changeShowAfterReport() async { await DioClient.instance.post('/api/v1/User/ChangeShowAfterReport'); } /// 更新攻克报告引导状态(报告页引导只出现一次,默认 false,触发后变 true) /// → POST /api/v1/User/ChangeShowGuide /// /// 对齐 Vue api/user/index.ts 的 UpdateGuideStatus(), /// 在攻克报告页 GuideModal 关闭时调用。 static Future changeShowGuide() async { await DioClient.instance.post('/api/v1/User/ChangeShowGuide'); } /// MD5 加密(UniApp stringToMd5 的等价实现) static String _md5(String text) { return md5.convert(utf8.encode(text)).toString(); } }