import '../core/config/app_constants.dart'; import '../core/utils/money.dart'; class LicenseInfo { final int id; final String type; // trial | monthly | annual | lifetime final bool isActive; final int maxDevices; final DateTime? expiresAt; final String phase; // normal | grace | readonly | locked const LicenseInfo({ required this.id, required this.type, required this.isActive, required this.maxDevices, required this.phase, this.expiresAt, }); factory LicenseInfo.fromJson(Map json) { return LicenseInfo( id: (json['id'] as num?)?.toInt() ?? 0, type: json['type'] as String? ?? 'trial', isActive: json['is_active'] as bool? ?? false, maxDevices: (json['max_devices'] as num?)?.toInt() ?? AppConstants.defaultMaxDevices, expiresAt: json['expires_at'] != null ? DateTime.tryParse(json['expires_at'] as String)?.toLocal() : null, phase: json['phase'] as String? ?? 'normal', ); } String get typeLabel { switch (type) { case 'monthly': return '月度授权'; case 'annual': return '年度授权'; case 'lifetime': return '永久授权'; default: return '试用版'; } } bool get isExpired => expiresAt != null && DateTime.now().isAfter(expiresAt!); int? get daysRemaining { if (expiresAt == null) return null; final diff = expiresAt!.difference(DateTime.now()).inDays; return diff < 0 ? 0 : diff; } /// 已过期天数(未过期或永久授权返回 0)。 int get daysExpired { if (expiresAt == null) return 0; final diff = DateTime.now().difference(expiresAt!).inDays; return diff < 0 ? 0 : diff; } bool get isReadOnlyPhase => phase == 'readonly' || phase == 'locked'; bool get isLockedPhase => phase == 'locked'; bool get needsAttention => phase == 'grace' || phase == 'readonly' || phase == 'locked'; } /// POST /license/purchase 响应:pay v2 收银台会话信息(render_type 多态)。 class PurchaseOrder { final String outTradeNo; final String renderType; // redirect | qr | ... final Map payload; final int amountMinor; final String currency; final String subject; const PurchaseOrder({ required this.outTradeNo, required this.renderType, required this.payload, required this.amountMinor, required this.currency, required this.subject, }); /// render_type == redirect 时的跳转链接(payload['url'])。 String get redirectUrl => payload['url'] as String? ?? ''; /// render_type == qr 时的二维码内容(payload['qr_content'])。 String get qrContent => payload['qr_content'] as String? ?? ''; /// 兼容旧字段:purchase_card.dart(Task 7 改造前)仍读 payUrl/amount。 String get payUrl => redirectUrl; String get amount => yuanFromMinor(amountMinor); factory PurchaseOrder.fromJson(Map json) => PurchaseOrder( outTradeNo: json['out_trade_no'] as String? ?? '', renderType: json['render_type'] as String? ?? 'redirect', payload: (json['payload'] as Map?)?.cast() ?? const {}, amountMinor: (json['amount_minor'] as num?)?.toInt() ?? 0, currency: json['currency'] as String? ?? 'CNY', subject: json['subject'] as String? ?? '', ); } /// GET /license/purchase/:otn 响应:购买单状态(到账轮询用)。 class PurchaseStatusInfo { final String outTradeNo; final String status; // pending | paid | failed final int amountMinor; final String currency; final String amount; // Deprecated:后端 formatMinor 串回退,串失败时用 amountMinor final DateTime? paidAt; final DateTime? expiresAt; // 续期后的门店授权到期时间(仅 paid 时有值) const PurchaseStatusInfo({ required this.outTradeNo, required this.status, required this.amountMinor, required this.currency, required this.amount, this.paidAt, this.expiresAt, }); bool get isPaid => status == 'paid'; /// 展示用金额:优先按 amount_minor 换算,退回旧 amount 串兜底。 String get displayAmount => amountMinor > 0 ? yuanFromMinor(amountMinor) : amount; factory PurchaseStatusInfo.fromJson(Map json) => PurchaseStatusInfo( outTradeNo: json['out_trade_no'] as String? ?? '', status: json['status'] as String? ?? 'pending', amountMinor: (json['amount_minor'] as num?)?.toInt() ?? 0, currency: json['currency'] as String? ?? 'CNY', amount: json['amount'] as String? ?? '', paidAt: json['paid_at'] != null ? DateTime.tryParse(json['paid_at'] as String)?.toLocal() : null, expiresAt: json['expires_at'] != null ? DateTime.tryParse(json['expires_at'] as String)?.toLocal() : null, ); } /// GET /license/purchases 列表项。 class PurchaseRecord { final String outTradeNo; final String productBizCode; final int amountMinor; final String currency; final String amount; // Deprecated:后端 formatMinor 串回退 final String status; // pending | paid | failed final String payUrl; final String userName; final DateTime? createdAt; final DateTime? paidAt; final DateTime? renewedTo; const PurchaseRecord({ required this.outTradeNo, required this.productBizCode, required this.amountMinor, required this.currency, required this.amount, required this.status, required this.payUrl, required this.userName, this.createdAt, this.paidAt, this.renewedTo, }); bool get isPaid => status == 'paid'; /// 展示用金额:优先按 amount_minor 换算,退回旧 amount 串兜底。 String get displayAmount => amountMinor > 0 ? yuanFromMinor(amountMinor) : amount; factory PurchaseRecord.fromJson(Map json) => PurchaseRecord( outTradeNo: json['out_trade_no'] as String? ?? '', productBizCode: json['product_biz_code'] as String? ?? '', amountMinor: (json['amount_minor'] as num?)?.toInt() ?? 0, currency: json['currency'] as String? ?? 'CNY', amount: json['amount'] as String? ?? '', status: json['status'] as String? ?? 'pending', payUrl: json['pay_url'] as String? ?? '', userName: json['user_name'] as String? ?? '', // 服务端时间为 UTC(RFC3339 Z),展示前转本地时区(同 session.dart 惯例) createdAt: json['created_at'] != null ? DateTime.tryParse(json['created_at'] as String)?.toLocal() : null, paidAt: json['paid_at'] != null ? DateTime.tryParse(json['paid_at'] as String)?.toLocal() : null, renewedTo: json['renewed_to'] != null ? DateTime.tryParse(json['renewed_to'] as String)?.toLocal() : null, ); } /// GET /license/purchases 汇总(订单管理 tab KPI)。 class PurchaseSummary { final int paidTotalMinor; final int paidCount; final int pendingCount; final int totalCount; const PurchaseSummary({ required this.paidTotalMinor, required this.paidCount, required this.pendingCount, required this.totalCount, }); factory PurchaseSummary.fromJson(Map json) => PurchaseSummary( paidTotalMinor: (json['paid_total_minor'] as num?)?.toInt() ?? 0, paidCount: (json['paid_count'] as num?)?.toInt() ?? 0, pendingCount: (json['pending_count'] as num?)?.toInt() ?? 0, totalCount: (json['total_count'] as num?)?.toInt() ?? 0, ); } /// GET /license/purchases 响应整体。 class PurchaseListResult { final List items; final int total; final PurchaseSummary summary; const PurchaseListResult({ required this.items, required this.total, required this.summary, }); factory PurchaseListResult.fromJson(Map json) => PurchaseListResult( items: (json['items'] as List? ?? const []) .map((e) => PurchaseRecord.fromJson(e as Map)) .toList(), total: (json['total'] as num?)?.toInt() ?? 0, summary: PurchaseSummary.fromJson( (json['summary'] as Map?) ?? const {}), ); }