// 功能描述:封装用户端 HTTP 请求,并将服务端错误转换为安全、可读的中文提示。 // 版本:1.1.0 import 'dart:convert'; import 'dart:typed_data'; import 'package:http/http.dart' as http; const Map _apiErrorMessages = { 404: '记录不存在', 1001: '请求标识缺失', 1002: '登录信息缺失,请重新登录', 1003: '接口密钥缺失', 1004: '请求头参数缺失', 1101: '暂无数据', 1102: '请求解析失败', 1103: '请填写必填参数', 1104: '无权执行此操作', 1105: '服务端数据处理失败', 1106: '服务端数据处理失败', 1107: '服务内部错误,请稍后重试', 1108: '密码错误', 1110: '账号已禁用', 1111: '当前记录已停用', 1112: '记录不存在', 1301: '登录状态已失效,请重新登录', 1302: '登录状态已失效,请重新登录', 1303: '登录状态已失效,请重新登录', 1304: '登录状态已失效,请重新登录', 1305: '登录状态已失效,请重新登录', 1306: '登录状态已失效,请重新登录', 1307: '登录状态已变更,请重新登录', 1308: '登录已过期,请重新登录', 1309: '登录状态已失效,请重新登录', 1310: '登录状态已失效,请重新登录', 1311: '登录状态已失效,请重新登录', 1312: '登录状态已失效,请重新登录', 1313: '登录失败,请稍后重试', 1314: '登录状态已失效,请重新登录', 1501: '服务暂时不可用,请稍后重试', 1502: '服务暂时不可用,请稍后重试', 1503: '服务暂时不可用,请稍后重试', 1504: '服务暂时不可用,请稍后重试', 1505: '服务暂时不可用,请稍后重试', 1506: '服务暂时不可用,请稍后重试', 1507: '服务暂时不可用,请稍后重试', 1702: '操作已取消', 1703: '操作失败,请稍后重试', 1704: '请求参数错误', 1705: '请求超时,请稍后重试', 1706: '记录已存在', 1707: '无权执行此操作', 1708: '系统繁忙,请稍后重试', 1709: '当前状态不支持此操作', 1710: '操作已中止', 1711: '输入内容超出允许范围', 1712: '当前功能暂不支持', 1713: '服务暂时不可用,请稍后重试', 1714: '服务数据异常,请稍后重试', 1715: '请先登录', }; final RegExp _chineseCharacterPattern = RegExp(r'[\u3400-\u9fff]'); /// 将服务端错误转换为中文用户提示。 /// /// [code] 是稳定错误码,[message] 是服务端原始消息;返回值不会透传未知英文技术信息。 String localizeApiErrorMessage(int code, String? message) { final mappedMessage = _apiErrorMessages[code]; if (mappedMessage != null) return mappedMessage; final originalMessage = message?.trim() ?? ''; if (_chineseCharacterPattern.hasMatch(originalMessage)) return originalMessage; return '操作失败,请稍后重试'; } /// 表示客户端可识别并可安全展示的接口异常。 class ApiException implements Exception { const ApiException(this.code, this.message); final int code; final String message; @override String toString() => message; } /// 负责用户端统一 HTTP 请求、鉴权头和响应解析。 class ApiClient { ApiClient( this._tokenProvider, { http.Client? client, String? baseUrl, }) : _client = client ?? http.Client(), baseUrl = baseUrl ?? const String.fromEnvironment( 'API_BASE_URL', defaultValue: 'http://10.0.2.2:12426', ); final String baseUrl; final String Function() _tokenProvider; final http.Client _client; Future get(String path, {bool authenticated = true}) => _send('GET', path, authenticated: authenticated); /// 读取需要鉴权的二进制资源;资源不存在时返回空值。 Future getBytes(String path) async { final request = http.Request('GET', Uri.parse('$baseUrl$path')); request.headers['accept'] = 'image/jpeg, image/png'; final token = _tokenProvider(); if (token.isNotEmpty) request.headers['authorization'] = token; final response = await _sendRequest(request); if (response.statusCode == 404) return null; if (response.statusCode < 200 || response.statusCode >= 300) { throw ApiException(response.statusCode, '头像加载失败'); } return response.bodyBytes.isEmpty ? null : response.bodyBytes; } Future post( String path, { Map? body, bool authenticated = true, }) => _send('POST', path, body: body, authenticated: authenticated); Future put(String path, {Map? body}) => _send('PUT', path, body: body); Future delete(String path, {Map? body}) => _send('DELETE', path, body: body); Future _send( String method, String path, { Map? body, bool authenticated = true, }) async { final request = http.Request(method, Uri.parse('$baseUrl$path')); request.headers['accept'] = 'application/json'; if (authenticated) { final token = _tokenProvider(); if (token.isNotEmpty) request.headers['authorization'] = token; } if (body != null) { request.headers['content-type'] = 'application/json; charset=UTF-8'; request.body = jsonEncode(body); } final response = await _sendRequest(request); return _decode(response); } /// 发送请求并统一处理网络连接异常。 Future _sendRequest(http.BaseRequest request) async { try { return await http.Response.fromStream(await _client.send(request)); } catch (_) { throw const ApiException(500, '网络连接失败,请稍后重试'); } } /// 解析统一响应结构,并按错误码生成中文提示。 Object? _decode(http.Response response) { if (response.statusCode < 200 || response.statusCode >= 300) { throw ApiException(response.statusCode, '网络请求失败(${response.statusCode})'); } Object? decoded; try { decoded = jsonDecode(response.body); } on FormatException { throw const ApiException(500, '服务端响应格式错误'); } if (decoded is! Map) { throw const ApiException(500, '服务端响应格式错误'); } final code = (decoded['code'] as num?)?.toInt() ?? 500; if (code != 0) { throw ApiException( code, localizeApiErrorMessage(code, decoded['message'] as String?), ); } return decoded['details']; } } Map jsonMap(Object? value) { if (value is Map) return value; if (value is Map) return value.map((key, item) => MapEntry(key.toString(), item)); throw const ApiException(500, '服务端数据格式错误'); } List> jsonList(Object? value) { if (value is! List) return const []; return value.map>(jsonMap).toList(growable: false); }