1723e480c1
- 打开订单管理 tab 时 invalidate 重建列表 provider(保留旧值展示不闪屏), 新下单/已删单不再需要手动刷新才同步 - license.dart 全部时间字段解析后 toLocal()(同 session.dart 惯例), 修复订单创建/支付时间显示为 UTC(慢 8 小时) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
252 lines
8.2 KiB
Dart
252 lines
8.2 KiB
Dart
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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) => PurchaseOrder(
|
||
outTradeNo: json['out_trade_no'] as String? ?? '',
|
||
renderType: json['render_type'] as String? ?? 'redirect',
|
||
payload:
|
||
(json['payload'] as Map?)?.cast<String, dynamic>() ?? 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<PurchaseRecord> items;
|
||
final int total;
|
||
final PurchaseSummary summary;
|
||
|
||
const PurchaseListResult({
|
||
required this.items,
|
||
required this.total,
|
||
required this.summary,
|
||
});
|
||
|
||
factory PurchaseListResult.fromJson(Map<String, dynamic> json) =>
|
||
PurchaseListResult(
|
||
items: (json['items'] as List<dynamic>? ?? const [])
|
||
.map((e) => PurchaseRecord.fromJson(e as Map<String, dynamic>))
|
||
.toList(),
|
||
total: (json['total'] as num?)?.toInt() ?? 0,
|
||
summary: PurchaseSummary.fromJson(
|
||
(json['summary'] as Map<String, dynamic>?) ?? const {}),
|
||
);
|
||
}
|