迁移 Flutter 端与训练脚本,模型/训练产物移出 git(遵循纯代码约定)

This commit is contained in:
2026-08-24 12:35:24 +08:00
parent d056f01965
commit 961523d94c
218 changed files with 13391 additions and 3232 deletions
+170
View File
@@ -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>;
}
}