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 && (