迁移 Flutter 端与训练脚本,模型/训练产物移出 git(遵循纯代码约定)
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
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<void> 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<String> 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<Order> 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<String, dynamic>() ?? {};
|
||||
return Order(
|
||||
orderId: json['orderId'] as String,
|
||||
wechatParams: params,
|
||||
alipayOrderStr: params['orderStr'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// 客户端支付完成后通知服务端(幂等),服务端据异步回调落授权
|
||||
Future<void> confirmOrder(String orderId) async {
|
||||
final res =
|
||||
await _post('/api/v1/orders/$orderId/confirm', jsonEncode({}));
|
||||
_decode(res);
|
||||
}
|
||||
|
||||
/// 拉取套餐价格方案(config.yml 静态定价,客户端不硬编码)
|
||||
Future<List<Plan>> 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<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// 查询授权状态(服务端为准)
|
||||
Future<LicenseStatus> 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<http.Response> _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 <String, String>{},
|
||||
},
|
||||
body: body,
|
||||
)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
} catch (e) {
|
||||
if (e is OrderApiException) rethrow;
|
||||
throw OrderApiException('网络请求失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _authHeaders() async {
|
||||
final token = await sessionStore.readToken();
|
||||
if (token == null || token.isEmpty) {
|
||||
throw const SessionExpiredException();
|
||||
}
|
||||
return {'Authorization': 'Bearer $token'};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _decode(http.Response res) {
|
||||
final Map<String, dynamic> json;
|
||||
try {
|
||||
json = jsonDecode(res.body) as Map<String, dynamic>;
|
||||
} 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<String, dynamic>;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user