diff --git a/app.py b/app.py
index d9225c92..77c4c012 100755
--- a/app.py
+++ b/app.py
@@ -1323,40 +1323,96 @@ def calculate_subscription_price_simple(user_id, to_plan_name, to_cycle, promo_c
if price <= 0:
return {'error': f'{to_cycle} 周期价格未配置'}
- # 4. 判断是新购还是续费
+ # 4. 判断订阅类型和计算价格
is_renewal = False
+ is_upgrade = False
+ is_downgrade = False
subscription_type = 'new'
current_plan = None
current_cycle = None
+ remaining_value = 0
+ final_price = price
if current_sub and current_sub.subscription_type in ['pro', 'max']:
- # 如果当前是付费用户,则为续费
- is_renewal = True
- subscription_type = 'renew'
current_plan = current_sub.subscription_type
current_cycle = current_sub.billing_cycle
- # 5. 构建结果(续费和新购价格完全一致)
+ if current_plan == to_plan_name:
+ # 同级续费:延长时长,全价购买
+ is_renewal = True
+ subscription_type = 'renew'
+ elif current_plan == 'pro' and to_plan_name == 'max':
+ # 升级:Pro → Max,需要计算差价
+ is_upgrade = True
+ subscription_type = 'upgrade'
+
+ # 计算当前订阅的剩余价值
+ if current_sub.end_date and current_sub.end_date > datetime.utcnow():
+ # 获取当前套餐的原始价格
+ current_plan_obj = SubscriptionPlan.query.filter_by(name=current_plan, is_active=True).first()
+ if current_plan_obj and current_plan_obj.pricing_options:
+ try:
+ pricing_opts = json.loads(current_plan_obj.pricing_options)
+ current_price = None
+ for opt in pricing_opts:
+ if opt.get('cycle_key') == current_cycle:
+ current_price = float(opt.get('price', 0))
+ break
+
+ if current_price and current_price > 0:
+ # 计算剩余天数
+ remaining_days = (current_sub.end_date - datetime.utcnow()).days
+
+ # 计算总天数
+ cycle_days_map = {
+ 'monthly': 30,
+ 'quarterly': 90,
+ 'semiannual': 180,
+ 'yearly': 365
+ }
+ total_days = cycle_days_map.get(current_cycle, 30)
+
+ # 计算剩余价值
+ if total_days > 0 and remaining_days > 0:
+ remaining_value = current_price * (remaining_days / total_days)
+ # 实付金额 = 新套餐价格 - 剩余价值
+ final_price = max(0, price - remaining_value)
+ except:
+ pass
+ elif current_plan == 'max' and to_plan_name == 'pro':
+ # 降级:Max → Pro,到期后切换,全价购买
+ is_downgrade = True
+ subscription_type = 'downgrade'
+ else:
+ # 其他情况视为新购
+ subscription_type = 'new'
+
+ # 5. 构建结果
result = {
'is_renewal': is_renewal,
+ 'is_upgrade': is_upgrade,
+ 'is_downgrade': is_downgrade,
'subscription_type': subscription_type,
'current_plan': current_plan,
'current_cycle': current_cycle,
'new_plan_price': price,
+ 'original_price': price, # 新套餐原价
+ 'remaining_value': remaining_value, # 当前订阅剩余价值(仅升级时有效)
'original_amount': price,
'discount_amount': 0,
- 'final_amount': price,
+ 'final_amount': final_price,
'promo_code': None,
'promo_error': None
}
- # 6. 应用优惠码
+ # 6. 应用优惠码(基于差价后的金额)
if promo_code and promo_code.strip():
- promo, error = validate_promo_code(promo_code, to_plan_name, to_cycle, price, user_id)
+ # 优惠码作用于差价后的金额
+ promo, error = validate_promo_code(promo_code, to_plan_name, to_cycle, final_price, user_id)
if promo:
- discount = calculate_discount(promo, price)
+ discount = calculate_discount(promo, final_price)
result['discount_amount'] = float(discount)
- result['final_amount'] = price - float(discount)
+ result['final_amount'] = final_price - float(discount)
result['promo_code'] = promo.code
elif error:
result['promo_error'] = error
diff --git a/src/components/Subscription/SubscriptionContentNew.tsx b/src/components/Subscription/SubscriptionContentNew.tsx
index 00fb6315..3720d784 100644
--- a/src/components/Subscription/SubscriptionContentNew.tsx
+++ b/src/components/Subscription/SubscriptionContentNew.tsx
@@ -51,7 +51,7 @@ export default function SubscriptionContentNew() {
},
});
- const [selectedCycle, setSelectedCycle] = useState('yearly');
+ const [selectedCycle, setSelectedCycle] = useState('monthly');
const [selectedPlan, setSelectedPlan] = useState(null);
const [subscriptionPlans, setSubscriptionPlans] = useState([]);
const [priceInfo, setPriceInfo] = useState(null);
@@ -491,6 +491,37 @@ export default function SubscriptionContentNew() {
return icons[iconName] || FaStar;
};
+ // 获取按钮文字
+ const getButtonText = (plan: any) => {
+ const currentPlanName = user?.subscription_type;
+ const isActive = user?.subscription_status === 'active';
+
+ if (!isActive || !currentPlanName || currentPlanName === 'free') {
+ return `选择${plan.displayName}`;
+ }
+
+ if (currentPlanName === plan.name) {
+ // 同级续费
+ return `续费${plan.displayName}`;
+ }
+
+ // 升级或降级
+ if (currentPlanName === 'pro' && plan.name === 'max') {
+ return `升级为${plan.displayName}`;
+ }
+
+ if (currentPlanName === 'max' && plan.name === 'pro') {
+ return `到期后切换到${plan.displayName}`;
+ }
+
+ return `选择${plan.displayName}`;
+ };
+
+ // 判断按钮是否可点击
+ const isButtonDisabled = (plan: any) => {
+ return false; // 所有套餐都可以选择,包括当前套餐(续费)
+ };
+
return (
+ {/* 当前订阅状态 */}
+ {user && user.subscription_type && user.subscription_type !== 'free' && user.subscription_status === 'active' && (
+
+
+
+
+
+
+
+
+ 当前订阅: {user.subscription_type === 'max' ? 'Max 旗舰版' : 'Pro 专业版'}
+
+
+ {user.billing_cycle === 'monthly' ? '月付' :
+ user.billing_cycle === 'quarterly' ? '季付' :
+ user.billing_cycle === 'semiannual' ? '半年付' : '年付'}
+
+
+
+
+ 使用中
+
+
+
+
+
+
+
+
+
+ 到期时间
+
+
+
+ {user.end_date
+ ? new Date(user.end_date).toLocaleDateString('zh-CN', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ })
+ : '永久有效'
+ }
+
+
+
+ {user.end_date && (() => {
+ const endDate = new Date(user.end_date);
+ const today = new Date();
+ const daysLeft = Math.ceil((endDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
+
+ if (daysLeft > 0 && daysLeft <= 30) {
+ return (
+
+
+ ⚠️ 还有 {daysLeft} 天到期,记得及时续费哦
+
+
+ );
+ }
+ return null;
+ })()}
+
+
+
+ )}
+
{/* 计费周期选择器 */}
@@ -791,55 +931,41 @@ export default function SubscriptionContentNew() {
size="lg"
h="56px"
bg={
- isCurrentPlan
- ? 'transparent'
- : isPremium
+ isPremium
? 'linear-gradient(135deg, #D4AF37 0%, #B8941F 100%)'
: 'rgba(255, 255, 255, 0.05)'
}
color={
- isCurrentPlan
- ? 'rgba(255, 255, 255, 0.5)'
- : isPremium
+ isPremium
? '#000'
: '#fff'
}
border={
- isCurrentPlan
- ? '1px solid rgba(255, 255, 255, 0.2)'
- : isPremium
+ isPremium
? 'none'
: '1px solid rgba(255, 255, 255, 0.1)'
}
fontWeight="bold"
fontSize="md"
borderRadius="lg"
- onClick={() => !isCurrentPlan && handleSubscribe(plan)}
- isDisabled={isCurrentPlan}
- cursor={isCurrentPlan ? 'not-allowed' : 'pointer'}
- _hover={
- !isCurrentPlan
- ? {
- transform: 'translateY(-2px)',
- shadow: isPremium
- ? '0 8px 30px rgba(212, 175, 55, 0.4)'
- : '0 8px 20px rgba(255, 255, 255, 0.1)',
- bg: isPremium
- ? 'linear-gradient(135deg, #E5C047 0%, #C9A52F 100%)'
- : 'rgba(255, 255, 255, 0.08)',
- }
- : {}
- }
- _active={
- !isCurrentPlan
- ? {
- transform: 'translateY(0)',
- }
- : {}
- }
+ onClick={() => handleSubscribe(plan)}
+ isDisabled={isButtonDisabled(plan)}
+ cursor="pointer"
+ _hover={{
+ transform: 'translateY(-2px)',
+ shadow: isPremium
+ ? '0 8px 30px rgba(212, 175, 55, 0.4)'
+ : '0 8px 20px rgba(255, 255, 255, 0.1)',
+ bg: isPremium
+ ? 'linear-gradient(135deg, #E5C047 0%, #C9A52F 100%)'
+ : 'rgba(255, 255, 255, 0.08)',
+ }}
+ _active={{
+ transform: 'translateY(0)',
+ }}
transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
>
- {isCurrentPlan ? '当前套餐' : `选择${plan.displayName}`}
+ {getButtonText(plan)}
@@ -980,6 +1106,57 @@ export default function SubscriptionContentNew() {
{!paymentOrder ? (
+ {/* 订阅类型提示 */}
+ {selectedPlan && priceInfo && (
+ <>
+ {priceInfo.is_upgrade && (
+
+
+
+
+ 升级到{selectedPlan.displayName},立即生效!按差价补缴费用
+
+
+
+ )}
+ {priceInfo.is_downgrade && (
+
+
+
+
+ 当前{priceInfo.current_plan?.toUpperCase()}订阅到期后自动切换到{selectedPlan.displayName}
+
+
+
+ )}
+ {priceInfo.is_renewal && (
+
+
+
+
+ 续费{selectedPlan.displayName},在当前到期日基础上延长时长
+
+
+
+ )}
+ >
+ )}
+
{/* 价格明细 */}
{selectedPlan && priceInfo && (