9fe1e2766f
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
145 lines
5.3 KiB
Dart
145 lines
5.3 KiB
Dart
import 'package:dio/dio.dart';
|
|
import '../core/api/api_client.dart';
|
|
import '../core/device/device_id.dart';
|
|
import '../core/exceptions.dart';
|
|
import '../models/license.dart';
|
|
|
|
class LicenseRepository {
|
|
final ApiClient _client;
|
|
const LicenseRepository(this._client);
|
|
|
|
Future<LicenseInfo?> getInfo() async {
|
|
try {
|
|
final resp = await _client.get('/license/info');
|
|
final data = resp.data['data'];
|
|
if (data == null) return null;
|
|
return LicenseInfo.fromJson(data as Map<String, dynamic>);
|
|
} on DioException catch (e) {
|
|
throw AppException(
|
|
e.response?.data?['error'] as String? ?? '获取授权信息失败',
|
|
statusCode: e.response?.statusCode,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Activate a license key for this device.
|
|
Future<void> activate(String licenseKey, {String? deviceName}) async {
|
|
final deviceId = await DeviceId.get();
|
|
try {
|
|
await _client.post('/license/activate', data: {
|
|
'license_key': licenseKey,
|
|
'device_id': deviceId,
|
|
'device_name': deviceName ?? DeviceId.platformName,
|
|
'platform': DeviceId.platformName,
|
|
});
|
|
} on DioException catch (e) {
|
|
throw AppException(
|
|
e.response?.data?['error'] as String? ?? '激活失败',
|
|
statusCode: e.response?.statusCode,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 在线购买/续费下单(仅管理员),返回 pay 收银台跳转信息。
|
|
/// [clientType] 终端类型("pc"/"mobile",可选):显式声明收银台形态——
|
|
/// mobile 走支付宝手机网站支付(H5 拉起 App),pc 走电脑扫码页;
|
|
/// 不传时后端按请求 UA 判断(浏览器场景 UA 真实,App 场景须显式传)。
|
|
Future<PurchaseOrder> createPurchase(String bizCode,
|
|
{String? clientType}) async {
|
|
try {
|
|
final resp = await _client.post('/license/purchase', data: {
|
|
'biz_code': bizCode,
|
|
if (clientType != null) 'client_type': clientType,
|
|
});
|
|
return PurchaseOrder.fromJson(resp.data['data'] as Map<String, dynamic>);
|
|
} on DioException catch (e) {
|
|
throw AppException(
|
|
e.response?.data?['error'] as String? ?? '下单失败,请稍后重试',
|
|
statusCode: e.response?.statusCode,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 查询购买单状态(到账轮询)。到账(paid)后返回续期后的授权到期时间。
|
|
Future<PurchaseStatusInfo> purchaseStatus(String outTradeNo) async {
|
|
try {
|
|
final resp = await _client.get('/license/purchase/$outTradeNo');
|
|
return PurchaseStatusInfo.fromJson(
|
|
resp.data['data'] as Map<String, dynamic>);
|
|
} on DioException catch (e) {
|
|
throw AppException(
|
|
e.response?.data?['error'] as String? ?? '查询订单状态失败',
|
|
statusCode: e.response?.statusCode,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 取消一笔未支付订单(防 pending 单堆积)。仅管理员可用。
|
|
/// 返回 true=已取消;订单不存在(404)或不可取消(409,如已支付/已取消)
|
|
/// 视为业务性「取消不了」,返回 false 而非抛异常。
|
|
Future<bool> cancelPurchase(String outTradeNo) async {
|
|
try {
|
|
final resp = await _client.post('/license/purchase/$outTradeNo/cancel');
|
|
final data = resp.data['data'] as Map<String, dynamic>?;
|
|
return data?['canceled'] as bool? ?? false;
|
|
} on DioException catch (e) {
|
|
final status = e.response?.statusCode;
|
|
// 409 分支防御性保留:本后端实现里非 pending 单取消返回 200 +
|
|
// canceled:false(业务性失败,非 HTTP 错误),故 409 实际永不触发;
|
|
// 保留兜底以防后端未来改走标准 HTTP 语义时前端仍能优雅降级。
|
|
if (status == 404 || status == 409) return false;
|
|
throw AppException(
|
|
e.response?.data?['error'] as String? ?? '取消订单失败,请稍后重试',
|
|
statusCode: status,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 购买订单列表(订单管理 tab,分页 + 状态筛选)。仅管理员可用。
|
|
Future<PurchaseListResult> listPurchases({
|
|
int page = 1,
|
|
int pageSize = 20,
|
|
String? status,
|
|
}) async {
|
|
try {
|
|
final resp = await _client.get('/license/purchases', params: {
|
|
'page': page,
|
|
'page_size': pageSize,
|
|
if (status != null) 'status': status,
|
|
});
|
|
return PurchaseListResult.fromJson(
|
|
resp.data['data'] as Map<String, dynamic>);
|
|
} on DioException catch (e) {
|
|
throw AppException(
|
|
e.response?.data?['error'] as String? ?? '获取订单列表失败',
|
|
statusCode: e.response?.statusCode,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 本店首月特惠是否已享用(购买弹窗据此置灰特惠档)。
|
|
Future<bool> promoUsed() async {
|
|
try {
|
|
final resp = await _client.get('/license/promo-status');
|
|
final data = resp.data['data'] as Map<String, dynamic>?;
|
|
return data?['used'] as bool? ?? false;
|
|
} on DioException {
|
|
// 查询失败不阻塞弹窗:按未享用展示,下单时服务端仍会强校验
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Deactivate (unbind) this device from its license.
|
|
Future<void> deactivate() async {
|
|
final deviceId = await DeviceId.get();
|
|
try {
|
|
await _client.post('/license/deactivate', data: {'device_id': deviceId});
|
|
} on DioException catch (e) {
|
|
throw AppException(
|
|
e.response?.data?['error'] as String? ?? '解绑失败',
|
|
statusCode: e.response?.statusCode,
|
|
);
|
|
}
|
|
}
|
|
}
|