ba53a0d478
- api_config.dart:API 基址单源(消除各处重复 _kApiUrl) - api_client.dart:统一鉴权 HTTP 客户端,401→AuthNotifier.refresh→重试一次, 错误统一 AuthApiException;+ 单测覆盖 401 刷新重试 - 模型 me/device/plan/usage_point(对齐后端 snake_case 契约) - account_api.dart:/v1/me、/v1/plans、/v1/me/devices(列表+删)、/v1/usage、 /v1/redeem、/v1/ads/unlock 封装 - auth_provider:注入 AuthApi + refresh();account_providers 装配 ApiClient/AccountApi - 顺带修复 onboarding 提交遗留的测试 stub(_NullTokenStore 缺 isOnboarded/markOnboarded) flutter analyze 0 error;114 tests passed。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
76 lines
2.6 KiB
Dart
76 lines
2.6 KiB
Dart
// account_api.dart — 账户域端点封装(受 JWT 保护,走 ApiClient)。
|
|
//
|
|
// 覆盖:/v1/me、/v1/plans、/v1/me/devices(列表+删除)、/v1/usage、
|
|
// /v1/redeem、/v1/ads/unlock。错误统一 AuthApiException(由 ApiClient 抛)。
|
|
import '../models/device.dart';
|
|
import '../models/me.dart';
|
|
import '../models/plan.dart';
|
|
import '../models/usage_point.dart';
|
|
import 'api_client.dart';
|
|
|
|
/// 兑换结果(POST /v1/redeem)。
|
|
class RedeemResult {
|
|
const RedeemResult({
|
|
required this.plan,
|
|
required this.durationDays,
|
|
this.idempotent = false,
|
|
this.expiresAt,
|
|
});
|
|
|
|
final String plan;
|
|
final int durationDays;
|
|
final bool idempotent;
|
|
final DateTime? expiresAt;
|
|
|
|
factory RedeemResult.fromJson(Map<String, dynamic> m) {
|
|
final exp = m['expires_at'] as String?;
|
|
return RedeemResult(
|
|
plan: m['plan'] as String? ?? '',
|
|
durationDays: (m['duration_days'] as num?)?.toInt() ?? 0,
|
|
idempotent: m['idempotent'] as bool? ?? false,
|
|
expiresAt: (exp != null && exp.isNotEmpty) ? DateTime.tryParse(exp) : null,
|
|
);
|
|
}
|
|
}
|
|
|
|
class AccountApi {
|
|
AccountApi(this._c);
|
|
|
|
final ApiClient _c;
|
|
|
|
/// GET /v1/me — 账户聚合视图。
|
|
Future<Me> me() async => Me.fromJson(await _c.getJson('/v1/me'));
|
|
|
|
/// GET /v1/plans — 套餐定义。
|
|
Future<List<Plan>> plans() async {
|
|
final body = await _c.getJson('/v1/plans');
|
|
final raw = (body['plans'] as List<dynamic>?) ?? const [];
|
|
return raw.map((e) => Plan.fromJson(e as Map<String, dynamic>)).toList();
|
|
}
|
|
|
|
/// GET /v1/me/devices — 已登录设备列表。
|
|
Future<List<Device>> devices() async {
|
|
final body = await _c.getJson('/v1/me/devices');
|
|
final raw = (body['devices'] as List<dynamic>?) ?? const [];
|
|
return raw.map((e) => Device.fromJson(e as Map<String, dynamic>)).toList();
|
|
}
|
|
|
|
/// DELETE /v1/me/devices/{uuid} — 移除设备。
|
|
Future<void> removeDevice(String uuid) => _c.delete('/v1/me/devices/$uuid');
|
|
|
|
/// GET /v1/usage?days=N — 最近 N 天用量(默认 7,后端范围 [1,90])。
|
|
Future<List<UsagePoint>> usage({int days = 7}) async {
|
|
final body = await _c.getJson('/v1/usage?days=$days');
|
|
final raw = (body['points'] as List<dynamic>?) ?? const [];
|
|
return raw.map((e) => UsagePoint.fromJson(e as Map<String, dynamic>)).toList();
|
|
}
|
|
|
|
/// POST /v1/redeem — 兑换码。
|
|
Future<RedeemResult> redeem(String code) async =>
|
|
RedeemResult.fromJson(await _c.postJson('/v1/redeem', {'code': code}));
|
|
|
|
/// POST /v1/ads/unlock — 看广告解锁今日免费额度。
|
|
Future<void> adUnlock({required String deviceId, required String adToken}) =>
|
|
_c.postJson('/v1/ads/unlock', {'device_id': deviceId, 'ad_token': adToken});
|
|
}
|