Files
jiu/client/lib/models/license.dart
T
wangjia 24123eb93c feat(client): App 内在线购买/续费授权(管理员支付宝下单+到账轮询)
- 新增套餐目录 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
2026-07-03 21:57:03 +08:00

121 lines
3.6 KiB
Dart
Raw 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';
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,
);
}