24123eb93c
- 新增套餐目录 license_plans.dart(标准/高级 × 月付/年付,与官网 checkout 一致) - license 模型/仓库新增 PurchaseOrder/PurchaseStatusInfo 与下单、查单接口 - 系统设置授权面板:管理员显示「在线购买 / 续费」按钮,弹窗选套餐→ 外部浏览器打开支付宝收银台→3s 轮询到账→自动刷新授权并展示新到期日 - 用户手册(docs.md + user-manual.html)同步补充在线购买入口说明 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
121 lines
3.6 KiB
Dart
121 lines
3.6 KiB
Dart
import '../core/config/app_constants.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)
|
||
: 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 收银台跳转信息。
|
||
class PurchaseOrder {
|
||
final String payUrl;
|
||
final String outTradeNo;
|
||
final String amount; // 实付金额(pay 侧权威价,如 "2999.00")
|
||
final String subject;
|
||
|
||
const PurchaseOrder({
|
||
required this.payUrl,
|
||
required this.outTradeNo,
|
||
required this.amount,
|
||
required this.subject,
|
||
});
|
||
|
||
factory PurchaseOrder.fromJson(Map<String, dynamic> json) => PurchaseOrder(
|
||
payUrl: json['pay_url'] as String? ?? '',
|
||
outTradeNo: json['out_trade_no'] as String? ?? '',
|
||
amount: json['amount'] as String? ?? '',
|
||
subject: json['subject'] as String? ?? '',
|
||
);
|
||
}
|
||
|
||
/// GET /license/purchase/:otn 响应:购买单状态(到账轮询用)。
|
||
class PurchaseStatusInfo {
|
||
final String outTradeNo;
|
||
final String status; // pending | paid | failed
|
||
final String amount;
|
||
final DateTime? paidAt;
|
||
final DateTime? expiresAt; // 续期后的门店授权到期时间(仅 paid 时有值)
|
||
|
||
const PurchaseStatusInfo({
|
||
required this.outTradeNo,
|
||
required this.status,
|
||
required this.amount,
|
||
this.paidAt,
|
||
this.expiresAt,
|
||
});
|
||
|
||
bool get isPaid => status == 'paid';
|
||
|
||
factory PurchaseStatusInfo.fromJson(Map<String, dynamic> json) =>
|
||
PurchaseStatusInfo(
|
||
outTradeNo: json['out_trade_no'] as String? ?? '',
|
||
status: json['status'] as String? ?? 'pending',
|
||
amount: json['amount'] as String? ?? '',
|
||
paidAt: json['paid_at'] != null
|
||
? DateTime.tryParse(json['paid_at'] as String)
|
||
: null,
|
||
expiresAt: json['expires_at'] != null
|
||
? DateTime.tryParse(json['expires_at'] as String)
|
||
: null,
|
||
);
|
||
}
|