update pay function
This commit is contained in:
76
app.py
76
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
|
||||
|
||||
@@ -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 (
|
||||
<Box
|
||||
minH="100vh"
|
||||
@@ -562,6 +593,115 @@ export default function SubscriptionContentNew() {
|
||||
</motion.div>
|
||||
</VStack>
|
||||
|
||||
{/* 当前订阅状态 */}
|
||||
{user && user.subscription_type && user.subscription_type !== 'free' && user.subscription_status === 'active' && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
>
|
||||
<Box
|
||||
mb={12}
|
||||
p={6}
|
||||
borderRadius="2xl"
|
||||
bg="rgba(212, 175, 55, 0.05)"
|
||||
border="2px solid"
|
||||
borderColor="rgba(212, 175, 55, 0.3)"
|
||||
backdropFilter="blur(20px)"
|
||||
maxW="600px"
|
||||
mx="auto"
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
_before={{
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '4px',
|
||||
bgGradient: 'linear-gradient(90deg, rgba(212, 175, 55, 0.5), rgba(212, 175, 55, 1), rgba(212, 175, 55, 0.5))',
|
||||
}}
|
||||
>
|
||||
<VStack spacing={4} align="stretch">
|
||||
<HStack justify="space-between" align="center">
|
||||
<HStack spacing={3}>
|
||||
<Icon
|
||||
as={user.subscription_type === 'max' ? FaCrown : FaGem}
|
||||
color="#D4AF37"
|
||||
boxSize={6}
|
||||
/>
|
||||
<VStack align="start" spacing={0}>
|
||||
<Text color="white" fontSize="lg" fontWeight="bold">
|
||||
当前订阅: {user.subscription_type === 'max' ? 'Max 旗舰版' : 'Pro 专业版'}
|
||||
</Text>
|
||||
<Text color="rgba(255, 255, 255, 0.6)" fontSize="xs">
|
||||
{user.billing_cycle === 'monthly' ? '月付' :
|
||||
user.billing_cycle === 'quarterly' ? '季付' :
|
||||
user.billing_cycle === 'semiannual' ? '半年付' : '年付'}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
<Badge
|
||||
px={3}
|
||||
py={1}
|
||||
borderRadius="full"
|
||||
bg="rgba(76, 175, 80, 0.2)"
|
||||
border="1px solid rgba(76, 175, 80, 0.4)"
|
||||
color="green.300"
|
||||
fontSize="xs"
|
||||
fontWeight="medium"
|
||||
>
|
||||
使用中
|
||||
</Badge>
|
||||
</HStack>
|
||||
|
||||
<Divider borderColor="rgba(212, 175, 55, 0.2)" />
|
||||
|
||||
<Flex justify="space-between" align="center">
|
||||
<HStack spacing={2}>
|
||||
<Icon as={FaClock} color="rgba(212, 175, 55, 0.8)" boxSize={4} />
|
||||
<Text color="rgba(255, 255, 255, 0.7)" fontSize="sm">
|
||||
到期时间
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text color="#D4AF37" fontSize="md" fontWeight="bold">
|
||||
{user.end_date
|
||||
? new Date(user.end_date).toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
: '永久有效'
|
||||
}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
{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 (
|
||||
<Box
|
||||
p={2}
|
||||
borderRadius="md"
|
||||
bg="rgba(255, 165, 0, 0.1)"
|
||||
border="1px solid rgba(255, 165, 0, 0.3)"
|
||||
>
|
||||
<Text color="orange.300" fontSize="xs" textAlign="center">
|
||||
⚠️ 还有 {daysLeft} 天到期,记得及时续费哦
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</VStack>
|
||||
</Box>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* 计费周期选择器 */}
|
||||
<VStack spacing={6} mb={16}>
|
||||
<Text fontSize="md" color="rgba(255, 255, 255, 0.7)">
|
||||
@@ -791,35 +931,27 @@ 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
|
||||
? {
|
||||
onClick={() => handleSubscribe(plan)}
|
||||
isDisabled={isButtonDisabled(plan)}
|
||||
cursor="pointer"
|
||||
_hover={{
|
||||
transform: 'translateY(-2px)',
|
||||
shadow: isPremium
|
||||
? '0 8px 30px rgba(212, 175, 55, 0.4)'
|
||||
@@ -827,19 +959,13 @@ export default function SubscriptionContentNew() {
|
||||
bg: isPremium
|
||||
? 'linear-gradient(135deg, #E5C047 0%, #C9A52F 100%)'
|
||||
: 'rgba(255, 255, 255, 0.08)',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
_active={
|
||||
!isCurrentPlan
|
||||
? {
|
||||
}}
|
||||
_active={{
|
||||
transform: 'translateY(0)',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
}}
|
||||
transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
||||
>
|
||||
{isCurrentPlan ? '当前套餐' : `选择${plan.displayName}`}
|
||||
{getButtonText(plan)}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -980,6 +1106,57 @@ export default function SubscriptionContentNew() {
|
||||
<ModalBody pb={6}>
|
||||
{!paymentOrder ? (
|
||||
<VStack spacing={6} align="stretch">
|
||||
{/* 订阅类型提示 */}
|
||||
{selectedPlan && priceInfo && (
|
||||
<>
|
||||
{priceInfo.is_upgrade && (
|
||||
<Box
|
||||
p={3}
|
||||
bg="rgba(76, 175, 80, 0.1)"
|
||||
borderRadius="lg"
|
||||
border="1px solid rgba(76, 175, 80, 0.3)"
|
||||
>
|
||||
<HStack spacing={2}>
|
||||
<Icon as={FaCheck} color="green.400" />
|
||||
<Text color="green.400" fontSize="sm" fontWeight="medium">
|
||||
升级到{selectedPlan.displayName},立即生效!按差价补缴费用
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
{priceInfo.is_downgrade && (
|
||||
<Box
|
||||
p={3}
|
||||
bg="rgba(255, 165, 0, 0.1)"
|
||||
borderRadius="lg"
|
||||
border="1px solid rgba(255, 165, 0, 0.3)"
|
||||
>
|
||||
<HStack spacing={2}>
|
||||
<Icon as={FaClock} color="orange.400" />
|
||||
<Text color="orange.400" fontSize="sm" fontWeight="medium">
|
||||
当前{priceInfo.current_plan?.toUpperCase()}订阅到期后自动切换到{selectedPlan.displayName}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
{priceInfo.is_renewal && (
|
||||
<Box
|
||||
p={3}
|
||||
bg="rgba(33, 150, 243, 0.1)"
|
||||
borderRadius="lg"
|
||||
border="1px solid rgba(33, 150, 243, 0.3)"
|
||||
>
|
||||
<HStack spacing={2}>
|
||||
<Icon as={FaRedo} color="blue.400" />
|
||||
<Text color="blue.400" fontSize="sm" fontWeight="medium">
|
||||
续费{selectedPlan.displayName},在当前到期日基础上延长时长
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 价格明细 */}
|
||||
{selectedPlan && priceInfo && (
|
||||
<Box
|
||||
|
||||
@@ -158,12 +158,16 @@ export const subscriptionConfig = {
|
||||
answer: '我们目前支持微信支付。扫描支付二维码后,系统会自动检测支付状态并激活您的订阅。支付过程安全可靠,所有交易都经过加密处理。',
|
||||
},
|
||||
{
|
||||
question: '升级或切换套餐时,原套餐的费用怎么办?',
|
||||
answer: '当您升级套餐或切换计费周期时,系统会自动计算您当前订阅的剩余价值并用于抵扣新套餐的费用。\n\n计算方式:\n• 剩余价值 = 原套餐价格 × (剩余天数 / 总天数)\n• 实付金额 = 新套餐价格 - 剩余价值 - 优惠码折扣\n\n例如:您购买了年付Pro版(¥2699),使用了180天后升级到Max版(¥5399/年),剩余价值约¥1350将自动抵扣,实付约¥4049。',
|
||||
question: '同级续费如何计费?',
|
||||
answer: '如果您是Pro用户续费Pro版本,或Max用户续费Max版本,支付后将在当前订阅到期日基础上延长相应时长。例如:您的Max年付版本还有30天到期,续费Max年付后,新的到期时间将延长至395天后(30天+365天)。',
|
||||
},
|
||||
{
|
||||
question: '可以在不同计费周期之间切换吗?',
|
||||
answer: '可以。您可以随时更改计费周期。如果从短期切换到长期,系统会计算剩余价值并应用到新的订阅中。长期套餐(季付、半年付、年付)可享受更大的折扣优惠。',
|
||||
question: 'Pro用户如何升级到Max?',
|
||||
answer: '从Pro升级到Max需要补差价,升级后立即生效。系统会根据您Pro订阅的剩余价值计算需要补缴的费用。支付成功后,您将立即获得Max版本的所有功能。',
|
||||
},
|
||||
{
|
||||
question: 'Max用户可以切换到Pro吗?',
|
||||
answer: '可以。Max用户购买Pro套餐后,系统会在当前Max订阅到期后自动切换到Pro版本,并从到期日开始计算Pro的订阅时长。在Max到期前,您仍可继续使用Max的全部功能。',
|
||||
},
|
||||
{
|
||||
question: '是否支持退款?',
|
||||
|
||||
Reference in New Issue
Block a user