Files
wangjia 1723e480c1 fix(client): 订单管理打开自动重拉列表 + 订单时间 UTC 显示转本地时区
- 打开订单管理 tab 时 invalidate 重建列表 provider(保留旧值展示不闪屏),
  新下单/已删单不再需要手动刷新才同步
- license.dart 全部时间字段解析后 toLocal()(同 session.dart 惯例),
  修复订单创建/支付时间显示为 UTC(慢 8 小时)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 00:39:29 +08:00

252 lines
8.2 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.dartTask 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 {}),
);
}