import 'package:dio/dio.dart'; import '../config.dart'; /// 公司账号登录接口返回的数据 class AuthResult { final String userID; final String token; final String nickname; AuthResult({required this.userID, required this.token, required this.nickname}); factory AuthResult.fromJson(Map json) => AuthResult( userID: json['userID']?.toString() ?? '', token: json['imToken']?.toString() ?? '', nickname: json['nickname']?.toString() ?? '', ); } /// 登录失败,message 直接给用户看(简体中文) class AuthException implements Exception { final String message; AuthException(this.message); @override String toString() => message; } /// 公司账号登录 HTTP 客户端。 /// /// 接口契约(account-service,见仓库 account-service/README.md): /// - POST {authApiBase}/api/login /// body: {"staffNo": "工号", "password": "...", "platformID": 1 iOS / 2 Android} /// 响应统一包络 {"code": 0, "msg": "", "data": {...}},失败也是 HTTP 200 + code != 0 /// 成功 data: {"userID", "nickname", "imToken", "expireTimeSeconds"}(userID/imToken 直接用于 OpenIM SDK 登录) /// - POST {authApiBase}/api/rtc_token(语音通话换 LiveKit 进房 token) /// 请求头: Authorization: Bearer {imToken} /// body: {"room": "房间号", "identity": "当前用户 userID"} /// 成功 data: {"token": "LiveKit 访问 token"} class AuthApi { final Dio _dio = Dio(BaseOptions( baseUrl: authApiBase, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), )); /// 拆统一响应包络:code == 0 返回 data,否则用 msg 抛 [AuthException] Map _unwrap(dynamic body) { if (body is Map) { if (body['code'] == 0 && body['data'] is Map) { return Map.from(body['data'] as Map); } final msg = body['msg']?.toString(); throw AuthException((msg != null && msg.isNotEmpty) ? msg : '服务器返回的数据不对,请联系管理员'); } throw AuthException('服务器返回的数据不对,请联系管理员'); } /// 工号 + 密码登录,成功后返回 OpenIM 登录所需的 userID/imToken Future login({required String staffNo, required String password, required int platformID}) async { try { final resp = await _dio.post('/api/login', data: { 'staffNo': staffNo, 'password': password, 'platformID': platformID, }); final result = AuthResult.fromJson(_unwrap(resp.data)); if (result.userID.isNotEmpty && result.token.isNotEmpty) return result; throw AuthException('服务器返回的数据不对,请联系管理员'); } on DioException catch (e) { // 401 等 HTTP 层失败也带 {code, msg} 包络 final data = e.response?.data; if (data is Map && data['msg'] != null && data['msg'].toString().isNotEmpty) { throw AuthException(data['msg'].toString()); } throw AuthException('连不上服务器,请检查网络'); } catch (e) { if (e is AuthException) rethrow; throw AuthException('登录出错了,请稍后再试'); } } /// 获取 LiveKit 房间 token(语音通话用)。authToken 即登录返回的 imToken。 Future getRtcToken({required String room, required String identity, required String authToken}) async { try { final resp = await _dio.post( '/api/rtc_token', data: {'room': room, 'identity': identity}, options: Options(headers: {'Authorization': 'Bearer $authToken'}), ); final data = _unwrap(resp.data); final token = data['token']?.toString(); if (token != null && token.isNotEmpty) { final live = data['liveURL']?.toString(); return LiveKitCredential( token: token, liveURL: live != null && live.isNotEmpty ? live : livekitUrl, ); } throw AuthException('服务器返回的数据不对,请联系管理员'); } on DioException catch (e) { final data = e.response?.data; if (data is Map && data['msg'] != null && data['msg'].toString().isNotEmpty) { throw AuthException(data['msg'].toString()); } throw AuthException('连不上服务器,请检查网络'); } catch (e) { if (e is AuthException) rethrow; throw AuthException('发起通话失败,请稍后再试'); } } } /// LiveKit 进房凭证 class LiveKitCredential { final String token; final String liveURL; LiveKitCredential({required this.token, required this.liveURL}); }