Files
MaliangAINovalWriter/AINoval/lib/models/admin/subscription_models.dart
2025-09-10 00:07:52 +08:00

429 lines
12 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 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import '../../utils/date_time_parser.dart';
part 'subscription_models.g.dart';
/// 订阅计划模型
@JsonSerializable()
class SubscriptionPlan extends Equatable {
final String? id;
final String planName;
final String? description;
final double price;
final String currency;
final BillingCycle billingCycle;
final String? roleId;
final int? creditsGranted;
final bool active;
final bool recommended;
final int priority;
final Map<String, dynamic>? features;
final int trialDays;
final int maxUsers;
final DateTime? createdAt;
final DateTime? updatedAt;
const SubscriptionPlan({
this.id,
required this.planName,
this.description,
required this.price,
required this.currency,
required this.billingCycle,
this.roleId,
this.creditsGranted,
this.active = true,
this.recommended = false,
this.priority = 0,
this.features,
this.trialDays = 0,
this.maxUsers = -1,
this.createdAt,
this.updatedAt,
});
factory SubscriptionPlan.fromJson(Map<String, dynamic> json) {
// 兼容后端字段可能为空或类型不一致(如 BigDecimal 序列化为字符串)
final dynamic priceRaw = json['price'];
final double parsedPrice = priceRaw is num
? priceRaw.toDouble()
: (priceRaw is String ? double.tryParse(priceRaw) ?? 0.0 : 0.0);
final dynamic priorityRaw = json['priority'];
final int parsedPriority = priorityRaw is num
? priorityRaw.toInt()
: (priorityRaw is String ? int.tryParse(priorityRaw) ?? 0 : 0);
final dynamic creditsRaw = json['creditsGranted'];
final int? parsedCredits = creditsRaw == null
? null
: (creditsRaw is num
? creditsRaw.toInt()
: (creditsRaw is String ? int.tryParse(creditsRaw) : null));
final dynamic activeRaw = json['active'];
final bool parsedActive = activeRaw is bool
? activeRaw
: (activeRaw is String ? activeRaw.toLowerCase() == 'true' : true);
final dynamic recommendedRaw = json['recommended'];
final bool parsedRecommended = recommendedRaw is bool
? recommendedRaw
: (recommendedRaw is String ? recommendedRaw.toLowerCase() == 'true' : false);
final featuresRaw = json['features'];
final Map<String, dynamic>? parsedFeatures =
featuresRaw is Map<String, dynamic> ? featuresRaw : null;
return SubscriptionPlan(
id: json['id'] as String?,
planName: (json['planName'] as String?) ?? '未命名套餐',
description: json['description'] as String?,
price: parsedPrice,
currency: (json['currency'] as String?) ?? 'CNY',
billingCycle: _parseBillingCycle(json['billingCycle']),
roleId: json['roleId'] as String?,
creditsGranted: parsedCredits,
active: parsedActive,
recommended: parsedRecommended,
priority: parsedPriority,
features: parsedFeatures,
trialDays: ((json['trialDays'] is String)
? int.tryParse(json['trialDays'])
: (json['trialDays'] as num?))
?.toInt() ?? 0,
maxUsers: ((json['maxUsers'] is String)
? int.tryParse(json['maxUsers'])
: (json['maxUsers'] as num?))
?.toInt() ?? -1,
createdAt: json['createdAt'] != null ? parseBackendDateTime(json['createdAt']) : null,
updatedAt: json['updatedAt'] != null ? parseBackendDateTime(json['updatedAt']) : null,
);
}
Map<String, dynamic> toJson() => _$SubscriptionPlanToJson(this);
@override
List<Object?> get props => [
id,
planName,
description,
price,
currency,
billingCycle,
roleId,
creditsGranted,
active,
recommended,
priority,
features,
trialDays,
maxUsers,
createdAt,
updatedAt,
];
/// 获取月度等价价格
double get monthlyEquivalentPrice {
switch (billingCycle) {
case BillingCycle.monthly:
return price;
case BillingCycle.quarterly:
return price / 3;
case BillingCycle.yearly:
return price / 12;
case BillingCycle.lifetime:
return price / 120; // 假设10年使用期
}
}
/// 获取计费周期显示文本
String get billingCycleText {
switch (billingCycle) {
case BillingCycle.monthly:
return '月付';
case BillingCycle.quarterly:
return '季付';
case BillingCycle.yearly:
return '年付';
case BillingCycle.lifetime:
return '终身';
}
}
/// 获取格式化价格
String get formattedPrice {
return '$currency ${price.toStringAsFixed(2)}';
}
/// 解析BillingCycle枚举
static BillingCycle _parseBillingCycle(dynamic value) {
if (value == null) return BillingCycle.monthly;
final stringValue = value.toString().toUpperCase();
switch (stringValue) {
case 'MONTHLY':
return BillingCycle.monthly;
case 'QUARTERLY':
return BillingCycle.quarterly;
case 'YEARLY':
return BillingCycle.yearly;
case 'LIFETIME':
return BillingCycle.lifetime;
default:
return BillingCycle.monthly;
}
}
/// 创建副本
SubscriptionPlan copyWith({
String? id,
String? planName,
String? description,
double? price,
String? currency,
BillingCycle? billingCycle,
String? roleId,
int? creditsGranted,
bool? active,
bool? recommended,
int? priority,
Map<String, dynamic>? features,
int? trialDays,
int? maxUsers,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return SubscriptionPlan(
id: id ?? this.id,
planName: planName ?? this.planName,
description: description ?? this.description,
price: price ?? this.price,
currency: currency ?? this.currency,
billingCycle: billingCycle ?? this.billingCycle,
roleId: roleId ?? this.roleId,
creditsGranted: creditsGranted ?? this.creditsGranted,
active: active ?? this.active,
recommended: recommended ?? this.recommended,
priority: priority ?? this.priority,
features: features ?? this.features,
trialDays: trialDays ?? this.trialDays,
maxUsers: maxUsers ?? this.maxUsers,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}
/// 计费周期枚举
enum BillingCycle {
@JsonValue('MONTHLY')
monthly,
@JsonValue('QUARTERLY')
quarterly,
@JsonValue('YEARLY')
yearly,
@JsonValue('LIFETIME')
lifetime,
}
/// 用户订阅模型
@JsonSerializable()
class UserSubscription extends Equatable {
final String? id;
final String userId;
final String planId;
final DateTime? startDate;
final DateTime? endDate;
final SubscriptionStatus status;
final bool autoRenewal;
final String? paymentMethod;
final String? transactionId;
final int creditsUsed;
final int totalCredits;
final DateTime? canceledAt;
final String? cancelReason;
final DateTime? trialEndDate;
final bool isTrial;
final DateTime? createdAt;
final DateTime? updatedAt;
const UserSubscription({
this.id,
required this.userId,
required this.planId,
this.startDate,
this.endDate,
required this.status,
this.autoRenewal = false,
this.paymentMethod,
this.transactionId,
this.creditsUsed = 0,
this.totalCredits = 0,
this.canceledAt,
this.cancelReason,
this.trialEndDate,
this.isTrial = false,
this.createdAt,
this.updatedAt,
});
factory UserSubscription.fromJson(Map<String, dynamic> json) {
return UserSubscription(
id: json['id'] as String?,
userId: json['userId'] as String,
planId: json['planId'] as String,
startDate: json['startDate'] != null ? parseBackendDateTime(json['startDate']) : null,
endDate: json['endDate'] != null ? parseBackendDateTime(json['endDate']) : null,
status: _parseSubscriptionStatus(json['status']),
autoRenewal: json['autoRenewal'] as bool? ?? false,
paymentMethod: json['paymentMethod'] as String?,
transactionId: json['transactionId'] as String?,
creditsUsed: (json['creditsUsed'] as num?)?.toInt() ?? 0,
totalCredits: (json['totalCredits'] as num?)?.toInt() ?? 0,
canceledAt: json['canceledAt'] != null ? parseBackendDateTime(json['canceledAt']) : null,
cancelReason: json['cancelReason'] as String?,
trialEndDate: json['trialEndDate'] != null ? parseBackendDateTime(json['trialEndDate']) : null,
isTrial: json['isTrial'] as bool? ?? false,
createdAt: json['createdAt'] != null ? parseBackendDateTime(json['createdAt']) : null,
updatedAt: json['updatedAt'] != null ? parseBackendDateTime(json['updatedAt']) : null,
);
}
Map<String, dynamic> toJson() => _$UserSubscriptionToJson(this);
@override
List<Object?> get props => [
id,
userId,
planId,
startDate,
endDate,
status,
autoRenewal,
paymentMethod,
transactionId,
creditsUsed,
totalCredits,
canceledAt,
cancelReason,
trialEndDate,
isTrial,
createdAt,
updatedAt,
];
/// 获取剩余积分
int get remainingCredits => (totalCredits - creditsUsed).clamp(0, totalCredits);
/// 检查订阅是否有效
bool get isValid {
final now = DateTime.now();
return (status == SubscriptionStatus.active || status == SubscriptionStatus.trial) &&
(endDate == null || endDate!.isAfter(now));
}
/// 检查是否即将过期7天内
bool get isExpiringSoon {
if (endDate == null) return false;
final now = DateTime.now();
final sevenDaysLater = now.add(const Duration(days: 7));
return endDate!.isBefore(sevenDaysLater) && endDate!.isAfter(now);
}
/// 解析SubscriptionStatus枚举
static SubscriptionStatus _parseSubscriptionStatus(dynamic value) {
if (value == null) return SubscriptionStatus.active;
final stringValue = value.toString().toUpperCase();
switch (stringValue) {
case 'ACTIVE':
return SubscriptionStatus.active;
case 'TRIAL':
return SubscriptionStatus.trial;
case 'CANCELED':
return SubscriptionStatus.canceled;
case 'EXPIRED':
return SubscriptionStatus.expired;
case 'SUSPENDED':
return SubscriptionStatus.suspended;
case 'REFUNDED':
return SubscriptionStatus.refunded;
default:
return SubscriptionStatus.active;
}
}
/// 获取状态显示文本
String get statusText {
switch (status) {
case SubscriptionStatus.active:
return '活跃';
case SubscriptionStatus.trial:
return '试用期';
case SubscriptionStatus.canceled:
return '已取消';
case SubscriptionStatus.expired:
return '已过期';
case SubscriptionStatus.suspended:
return '暂停';
case SubscriptionStatus.refunded:
return '已退款';
}
}
}
/// 订阅状态枚举
enum SubscriptionStatus {
@JsonValue('ACTIVE')
active,
@JsonValue('TRIAL')
trial,
@JsonValue('CANCELED')
canceled,
@JsonValue('EXPIRED')
expired,
@JsonValue('SUSPENDED')
suspended,
@JsonValue('REFUNDED')
refunded,
}
/// 订阅统计信息
@JsonSerializable()
class SubscriptionStatistics extends Equatable {
final int totalPlans;
final int activePlans;
final int totalSubscriptions;
final int activeSubscriptions;
final int trialSubscriptions;
final double monthlyRevenue;
final double yearlyRevenue;
const SubscriptionStatistics({
required this.totalPlans,
required this.activePlans,
required this.totalSubscriptions,
required this.activeSubscriptions,
required this.trialSubscriptions,
required this.monthlyRevenue,
required this.yearlyRevenue,
});
factory SubscriptionStatistics.fromJson(Map<String, dynamic> json) =>
_$SubscriptionStatisticsFromJson(json);
Map<String, dynamic> toJson() => _$SubscriptionStatisticsToJson(this);
@override
List<Object?> get props => [
totalPlans,
activePlans,
totalSubscriptions,
activeSubscriptions,
trialSubscriptions,
monthlyRevenue,
yearlyRevenue,
];
}