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:
|
if price <= 0:
|
||||||
return {'error': f'{to_cycle} 周期价格未配置'}
|
return {'error': f'{to_cycle} 周期价格未配置'}
|
||||||
|
|
||||||
# 4. 判断是新购还是续费
|
# 4. 判断订阅类型和计算价格
|
||||||
is_renewal = False
|
is_renewal = False
|
||||||
|
is_upgrade = False
|
||||||
|
is_downgrade = False
|
||||||
subscription_type = 'new'
|
subscription_type = 'new'
|
||||||
current_plan = None
|
current_plan = None
|
||||||
current_cycle = None
|
current_cycle = None
|
||||||
|
remaining_value = 0
|
||||||
|
final_price = price
|
||||||
|
|
||||||
if current_sub and current_sub.subscription_type in ['pro', 'max']:
|
if current_sub and current_sub.subscription_type in ['pro', 'max']:
|
||||||
# 如果当前是付费用户,则为续费
|
|
||||||
is_renewal = True
|
|
||||||
subscription_type = 'renew'
|
|
||||||
current_plan = current_sub.subscription_type
|
current_plan = current_sub.subscription_type
|
||||||
current_cycle = current_sub.billing_cycle
|
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 = {
|
result = {
|
||||||
'is_renewal': is_renewal,
|
'is_renewal': is_renewal,
|
||||||
|
'is_upgrade': is_upgrade,
|
||||||
|
'is_downgrade': is_downgrade,
|
||||||
'subscription_type': subscription_type,
|
'subscription_type': subscription_type,
|
||||||
'current_plan': current_plan,
|
'current_plan': current_plan,
|
||||||
'current_cycle': current_cycle,
|
'current_cycle': current_cycle,
|
||||||
'new_plan_price': price,
|
'new_plan_price': price,
|
||||||
|
'original_price': price, # 新套餐原价
|
||||||
|
'remaining_value': remaining_value, # 当前订阅剩余价值(仅升级时有效)
|
||||||
'original_amount': price,
|
'original_amount': price,
|
||||||
'discount_amount': 0,
|
'discount_amount': 0,
|
||||||
'final_amount': price,
|
'final_amount': final_price,
|
||||||
'promo_code': None,
|
'promo_code': None,
|
||||||
'promo_error': None
|
'promo_error': None
|
||||||
}
|
}
|
||||||
|
|
||||||
# 6. 应用优惠码
|
# 6. 应用优惠码(基于差价后的金额)
|
||||||
if promo_code and promo_code.strip():
|
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:
|
if promo:
|
||||||
discount = calculate_discount(promo, price)
|
discount = calculate_discount(promo, final_price)
|
||||||
result['discount_amount'] = float(discount)
|
result['discount_amount'] = float(discount)
|
||||||
result['final_amount'] = price - float(discount)
|
result['final_amount'] = final_price - float(discount)
|
||||||
result['promo_code'] = promo.code
|
result['promo_code'] = promo.code
|
||||||
elif error:
|
elif error:
|
||||||
result['promo_error'] = 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 [selectedPlan, setSelectedPlan] = useState(null);
|
||||||
const [subscriptionPlans, setSubscriptionPlans] = useState([]);
|
const [subscriptionPlans, setSubscriptionPlans] = useState([]);
|
||||||
const [priceInfo, setPriceInfo] = useState(null);
|
const [priceInfo, setPriceInfo] = useState(null);
|
||||||
@@ -491,6 +491,37 @@ export default function SubscriptionContentNew() {
|
|||||||
return icons[iconName] || FaStar;
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
minH="100vh"
|
minH="100vh"
|
||||||
@@ -562,6 +593,115 @@ export default function SubscriptionContentNew() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</VStack>
|
</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}>
|
<VStack spacing={6} mb={16}>
|
||||||
<Text fontSize="md" color="rgba(255, 255, 255, 0.7)">
|
<Text fontSize="md" color="rgba(255, 255, 255, 0.7)">
|
||||||
@@ -791,55 +931,41 @@ export default function SubscriptionContentNew() {
|
|||||||
size="lg"
|
size="lg"
|
||||||
h="56px"
|
h="56px"
|
||||||
bg={
|
bg={
|
||||||
isCurrentPlan
|
isPremium
|
||||||
? 'transparent'
|
|
||||||
: isPremium
|
|
||||||
? 'linear-gradient(135deg, #D4AF37 0%, #B8941F 100%)'
|
? 'linear-gradient(135deg, #D4AF37 0%, #B8941F 100%)'
|
||||||
: 'rgba(255, 255, 255, 0.05)'
|
: 'rgba(255, 255, 255, 0.05)'
|
||||||
}
|
}
|
||||||
color={
|
color={
|
||||||
isCurrentPlan
|
isPremium
|
||||||
? 'rgba(255, 255, 255, 0.5)'
|
|
||||||
: isPremium
|
|
||||||
? '#000'
|
? '#000'
|
||||||
: '#fff'
|
: '#fff'
|
||||||
}
|
}
|
||||||
border={
|
border={
|
||||||
isCurrentPlan
|
isPremium
|
||||||
? '1px solid rgba(255, 255, 255, 0.2)'
|
|
||||||
: isPremium
|
|
||||||
? 'none'
|
? 'none'
|
||||||
: '1px solid rgba(255, 255, 255, 0.1)'
|
: '1px solid rgba(255, 255, 255, 0.1)'
|
||||||
}
|
}
|
||||||
fontWeight="bold"
|
fontWeight="bold"
|
||||||
fontSize="md"
|
fontSize="md"
|
||||||
borderRadius="lg"
|
borderRadius="lg"
|
||||||
onClick={() => !isCurrentPlan && handleSubscribe(plan)}
|
onClick={() => handleSubscribe(plan)}
|
||||||
isDisabled={isCurrentPlan}
|
isDisabled={isButtonDisabled(plan)}
|
||||||
cursor={isCurrentPlan ? 'not-allowed' : 'pointer'}
|
cursor="pointer"
|
||||||
_hover={
|
_hover={{
|
||||||
!isCurrentPlan
|
transform: 'translateY(-2px)',
|
||||||
? {
|
shadow: isPremium
|
||||||
transform: 'translateY(-2px)',
|
? '0 8px 30px rgba(212, 175, 55, 0.4)'
|
||||||
shadow: isPremium
|
: '0 8px 20px rgba(255, 255, 255, 0.1)',
|
||||||
? '0 8px 30px rgba(212, 175, 55, 0.4)'
|
bg: isPremium
|
||||||
: '0 8px 20px rgba(255, 255, 255, 0.1)',
|
? 'linear-gradient(135deg, #E5C047 0%, #C9A52F 100%)'
|
||||||
bg: isPremium
|
: 'rgba(255, 255, 255, 0.08)',
|
||||||
? 'linear-gradient(135deg, #E5C047 0%, #C9A52F 100%)'
|
}}
|
||||||
: 'rgba(255, 255, 255, 0.08)',
|
_active={{
|
||||||
}
|
transform: 'translateY(0)',
|
||||||
: {}
|
}}
|
||||||
}
|
|
||||||
_active={
|
|
||||||
!isCurrentPlan
|
|
||||||
? {
|
|
||||||
transform: 'translateY(0)',
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
||||||
>
|
>
|
||||||
{isCurrentPlan ? '当前套餐' : `选择${plan.displayName}`}
|
{getButtonText(plan)}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -980,6 +1106,57 @@ export default function SubscriptionContentNew() {
|
|||||||
<ModalBody pb={6}>
|
<ModalBody pb={6}>
|
||||||
{!paymentOrder ? (
|
{!paymentOrder ? (
|
||||||
<VStack spacing={6} align="stretch">
|
<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 && (
|
{selectedPlan && priceInfo && (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -158,12 +158,16 @@ export const subscriptionConfig = {
|
|||||||
answer: '我们目前支持微信支付。扫描支付二维码后,系统会自动检测支付状态并激活您的订阅。支付过程安全可靠,所有交易都经过加密处理。',
|
answer: '我们目前支持微信支付。扫描支付二维码后,系统会自动检测支付状态并激活您的订阅。支付过程安全可靠,所有交易都经过加密处理。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: '升级或切换套餐时,原套餐的费用怎么办?',
|
question: '同级续费如何计费?',
|
||||||
answer: '当您升级套餐或切换计费周期时,系统会自动计算您当前订阅的剩余价值并用于抵扣新套餐的费用。\n\n计算方式:\n• 剩余价值 = 原套餐价格 × (剩余天数 / 总天数)\n• 实付金额 = 新套餐价格 - 剩余价值 - 优惠码折扣\n\n例如:您购买了年付Pro版(¥2699),使用了180天后升级到Max版(¥5399/年),剩余价值约¥1350将自动抵扣,实付约¥4049。',
|
answer: '如果您是Pro用户续费Pro版本,或Max用户续费Max版本,支付后将在当前订阅到期日基础上延长相应时长。例如:您的Max年付版本还有30天到期,续费Max年付后,新的到期时间将延长至395天后(30天+365天)。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: '可以在不同计费周期之间切换吗?',
|
question: 'Pro用户如何升级到Max?',
|
||||||
answer: '可以。您可以随时更改计费周期。如果从短期切换到长期,系统会计算剩余价值并应用到新的订阅中。长期套餐(季付、半年付、年付)可享受更大的折扣优惠。',
|
answer: '从Pro升级到Max需要补差价,升级后立即生效。系统会根据您Pro订阅的剩余价值计算需要补缴的费用。支付成功后,您将立即获得Max版本的所有功能。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Max用户可以切换到Pro吗?',
|
||||||
|
answer: '可以。Max用户购买Pro套餐后,系统会在当前Max订阅到期后自动切换到Pro版本,并从到期日开始计算Pro的订阅时长。在Max到期前,您仍可继续使用Max的全部功能。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: '是否支持退款?',
|
question: '是否支持退款?',
|
||||||
|
|||||||
Reference in New Issue
Block a user