import 'dart:convert'; import 'dart:io' show Platform; import 'package:cupertino_http/cupertino_http.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:http/http.dart' as http; import '../auth/session_store.dart'; import '../config/app_config.dart'; import 'models.dart'; /// 后端账号/支付/授权 API 客户端(契约见 docs/PaymentApi.md)。 /// 后端未部署或请求失败时抛出 [OrderApiException];登录失效抛出 [SessionExpiredException], /// 由 UI 层回登录页。 class OrderApiException implements Exception { final String message; const OrderApiException(this.message); @override String toString() => message; } /// 登录已失效(后端返回 code 61):token 过期/被清,UI 应清除会话回登录页 class SessionExpiredException extends OrderApiException { const SessionExpiredException() : super('登录已失效,请重新登录'); } class OrderApi { final String baseUrl; final SessionStore sessionStore; final http.Client _client; // iOS 26 对 dart:io 原生 socket 访问本地网络存在拦截 bug(权限已允许仍 // 拒绝连接),改用 NSURLSession 网络栈(CupertinoClient)绕过;非 Apple // 平台回退 IOClient。 OrderApi({String? baseUrl, required this.sessionStore, http.Client? client}) : baseUrl = baseUrl ?? AppConfig.apiBaseUrl, _client = client ?? _defaultHttpClient(); static http.Client _defaultHttpClient() { if (!kIsWeb && Platform.isIOS) { return CupertinoClient.defaultSessionConfiguration(); } return http.Client(); } /// 注册账号;重复注册等业务错误由 [_decode] 抛出 Future register({ required String phone, required String password, }) async { final res = await _post('/api/v1/auth/register', jsonEncode({'phone': phone, 'password': password}), auth: false); _decode(res); } /// 登录,成功返回 token(由调用方存入 SessionStore) Future login({ required String phone, required String password, }) async { final res = await _post('/api/v1/auth/login', jsonEncode({'phone': phone, 'password': password}), auth: false); final json = _decode(res); return json['token'] as String; } /// 创建订单,返回支付参数(微信 prepay 参数或支付宝 orderStr) Future createOrder({ required String planId, required PayChannel channel, }) async { final body = jsonEncode({'planId': planId, 'channel': channel.name}); final res = await _post('/api/v1/orders', body); final json = _decode(res); final params = (json['payParams'] as Map?)?.cast() ?? {}; return Order( orderId: json['orderId'] as String, wechatParams: params, alipayOrderStr: params['orderStr'] as String?, ); } /// 客户端支付完成后通知服务端(幂等),服务端据异步回调落授权 Future confirmOrder(String orderId) async { final res = await _post('/api/v1/orders/$orderId/confirm', jsonEncode({})); _decode(res); } /// 拉取套餐价格方案(config.yml 静态定价,客户端不硬编码) Future> fetchPlans() async { final http.Response res; try { res = await _client.get( Uri.parse('$baseUrl/api/v1/plans'), headers: {'Accept': 'application/json', ...await _authHeaders()}, ).timeout(const Duration(seconds: 15)); } catch (e) { if (e is OrderApiException) rethrow; throw OrderApiException('网络请求失败: $e'); } final json = _decode(res); return (json['list'] as List) .map((e) => Plan.fromJson(e as Map)) .toList(); } /// 查询授权状态(服务端为准) Future fetchLicense() async { final http.Response res; try { res = await _client.get( Uri.parse('$baseUrl/api/v1/license'), headers: {'Accept': 'application/json', ...await _authHeaders()}, ).timeout(const Duration(seconds: 15)); } catch (e) { if (e is OrderApiException) rethrow; throw OrderApiException('网络请求失败: $e'); } final json = _decode(res); return LicenseStatus.fromJson(json); } Future _post(String path, String body, {bool auth = true}) async { try { return await _client .post( Uri.parse('$baseUrl$path'), headers: { 'Content-Type': 'application/json', ...auth ? await _authHeaders() : const {}, }, body: body, ) .timeout(const Duration(seconds: 15)); } catch (e) { if (e is OrderApiException) rethrow; throw OrderApiException('网络请求失败: $e'); } } Future> _authHeaders() async { final token = await sessionStore.readToken(); if (token == null || token.isEmpty) { throw const SessionExpiredException(); } return {'Authorization': 'Bearer $token'}; } Map _decode(http.Response res) { final Map json; try { json = jsonDecode(res.body) as Map; } catch (_) { throw OrderApiException('服务端响应异常 (${res.statusCode})'); } if (res.statusCode != 200 || json['code'] != 0) { final message = json['message'] as String? ?? '服务端错误 (${res.statusCode})'; if (json['code'] == 61) { throw const SessionExpiredException(); } throw OrderApiException(message); } return json['data'] as Map; } }