Merge branch 'feature_bugfix/251104_event' of https://git.valuefrontier.cn/vf/vf_react into feature_bugfix/251104_event

* 'feature_bugfix/251104_event' of https://git.valuefrontier.cn/vf/vf_react:
  加入优惠码机制,预置3个优惠码
This commit is contained in:
zdl
2025-11-06 01:40:28 +08:00
3 changed files with 823 additions and 31 deletions

View File

@@ -29,6 +29,9 @@ import {
Td,
Heading,
Collapse,
Input,
InputGroup,
InputRightElement,
} from '@chakra-ui/react';
import React, { useState, useEffect } from 'react';
import { logger } from '../../utils/logger';
@@ -85,6 +88,13 @@ export default function SubscriptionContent() {
const [forceUpdating, setForceUpdating] = useState(false);
const [openFaqIndex, setOpenFaqIndex] = useState(null);
// 优惠码相关state
const [promoCode, setPromoCode] = useState('');
const [promoCodeApplied, setPromoCodeApplied] = useState(false);
const [promoCodeError, setPromoCodeError] = useState('');
const [validatingPromo, setValidatingPromo] = useState(false);
const [priceInfo, setPriceInfo] = useState(null); // 价格信息(包含升级计算)
// 加载订阅套餐数据
useEffect(() => {
fetchSubscriptionPlans();
@@ -149,7 +159,88 @@ export default function SubscriptionContent() {
}
};
const handleSubscribe = (plan) => {
// 计算价格(包含升级和优惠码)
const calculatePrice = async (plan, cycle, promoCodeValue = null) => {
try {
const response = await fetch('/api/subscription/calculate-price', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
to_plan: plan.name,
to_cycle: cycle,
promo_code: promoCodeValue || null
})
});
if (response.ok) {
const data = await response.json();
if (data.success) {
setPriceInfo(data.data);
return data.data;
}
}
return null;
} catch (error) {
logger.error('SubscriptionContent', 'calculatePrice', error);
return null;
}
};
// 验证优惠码
const handleValidatePromoCode = async () => {
if (!promoCode.trim()) {
setPromoCodeError('请输入优惠码');
return;
}
if (!selectedPlan) {
setPromoCodeError('请先选择套餐');
return;
}
setValidatingPromo(true);
setPromoCodeError('');
try {
// 重新计算价格,包含优惠码
const result = await calculatePrice(selectedPlan, selectedCycle, promoCode);
if (result && !result.promo_error) {
setPromoCodeApplied(true);
toast({
title: '优惠码已应用',
description: `节省 ¥${result.discount_amount.toFixed(2)}`,
status: 'success',
duration: 3000,
isClosable: true,
});
} else {
setPromoCodeError(result?.promo_error || '优惠码无效');
setPromoCodeApplied(false);
}
} catch (error) {
setPromoCodeError('验证失败,请重试');
setPromoCodeApplied(false);
} finally {
setValidatingPromo(false);
}
};
// 移除优惠码
const handleRemovePromoCode = async () => {
setPromoCode('');
setPromoCodeApplied(false);
setPromoCodeError('');
// 重新计算价格(不含优惠码)
if (selectedPlan) {
await calculatePrice(selectedPlan, selectedCycle, null);
}
};
const handleSubscribe = async (plan) => {
if (!user) {
toast({
title: '请先登录',
@@ -178,6 +269,10 @@ export default function SubscriptionContent() {
);
setSelectedPlan(plan);
// 计算价格(包含升级判断)
await calculatePrice(plan, selectedCycle, promoCodeApplied ? promoCode : null);
onPaymentModalOpen();
};
@@ -186,7 +281,7 @@ export default function SubscriptionContent() {
setLoading(true);
try {
const price = selectedCycle === 'monthly' ? selectedPlan.monthly_price : selectedPlan.yearly_price;
const price = priceInfo?.final_amount || (selectedCycle === 'monthly' ? selectedPlan.monthly_price : selectedPlan.yearly_price);
// 🎯 追踪支付发起
subscriptionEvents.trackPaymentInitiated({
@@ -205,7 +300,8 @@ export default function SubscriptionContent() {
credentials: 'include',
body: JSON.stringify({
plan_name: selectedPlan.name,
billing_cycle: selectedCycle
billing_cycle: selectedCycle,
promo_code: promoCodeApplied ? promoCode : null
})
});
@@ -488,6 +584,27 @@ export default function SubscriptionContent() {
return `年付节省 ${percentage}%`;
};
// 获取按钮文字(根据用户当前订阅判断是升级还是新订阅)
const getButtonText = (plan, user) => {
if (!user || user.subscription_type === 'free') {
return `选择 ${plan.display_name}`;
}
// 判断是否为升级
const planLevels = { 'free': 0, 'pro': 1, 'max': 2 };
const currentLevel = planLevels[user.subscription_type] || 0;
const targetLevel = planLevels[plan.name] || 0;
if (targetLevel > currentLevel) {
return `升级至 ${plan.display_name}`;
} else if (targetLevel < currentLevel) {
return `切换至 ${plan.display_name}`;
} else {
// 同级别,可能是切换周期
return `切换至 ${plan.display_name}`;
}
};
// 统一的功能列表定义 - 基于商业定价(10月15日)文档
const allFeatures = [
// 新闻催化分析模块
@@ -838,7 +955,8 @@ export default function SubscriptionContent() {
onClick={() => handleSubscribe(plan)}
isDisabled={
user?.subscription_type === plan.name &&
user?.subscription_status === 'active'
user?.subscription_status === 'active' &&
user?.billing_cycle === selectedCycle
}
_hover={{
transform: 'scale(1.02)',
@@ -846,9 +964,10 @@ export default function SubscriptionContent() {
transition="all 0.2s"
>
{user?.subscription_type === plan.name &&
user?.subscription_status === 'active'
? '✓ 已订阅'
: `选择 ${plan.display_name}`
user?.subscription_status === 'active' &&
user?.billing_cycle === selectedCycle
? '✓ 当前套餐'
: getButtonText(plan, user)
}
</Button>
</VStack>
@@ -1084,14 +1203,62 @@ export default function SubscriptionContent() {
<Text color={secondaryText}>计费周期:</Text>
<Text>{selectedCycle === 'monthly' ? '按月付费' : '按年付费'}</Text>
</Flex>
<Divider />
<Flex justify="space-between" align="baseline">
<Text color={secondaryText}>应付金额:</Text>
<Text fontSize="2xl" fontWeight="bold" color="blue.500">
¥{getCurrentPrice(selectedPlan).toFixed(2)}
{/* 价格明细 */}
<Divider my={2} />
{priceInfo && priceInfo.is_upgrade && (
<Box bg="blue.50" p={3} borderRadius="md" mb={2}>
<HStack spacing={2} mb={2}>
<Icon as={FaCheck} color="blue.500" boxSize={4} />
<Text fontSize="sm" fontWeight="bold" color="blue.700">
{priceInfo.upgrade_type === 'plan_upgrade' ? '套餐升级' :
priceInfo.upgrade_type === 'cycle_change' ? '周期变更' : '套餐和周期调整'}
</Text>
</HStack>
<VStack spacing={1} align="stretch" fontSize="xs">
<Flex justify="space-between" color="gray.600">
<Text>当前订阅: {priceInfo.current_plan === 'pro' ? 'Pro版' : 'Max版'} ({priceInfo.current_cycle === 'monthly' ? '月付' : '年付'})</Text>
</Flex>
<Flex justify="space-between" color="gray.600">
<Text>剩余价值:</Text>
<Text>¥{priceInfo.remaining_value.toFixed(2)}</Text>
</Flex>
</VStack>
</Box>
)}
<Flex justify="space-between">
<Text color={secondaryText}>
{priceInfo && priceInfo.is_upgrade ? '新套餐价格:' : '套餐价格:'}
</Text>
<Text fontWeight="medium">
¥{priceInfo ? priceInfo.new_plan_price.toFixed(2) : getCurrentPrice(selectedPlan).toFixed(2)}
</Text>
</Flex>
{getSavingsText(selectedPlan) && (
{priceInfo && priceInfo.is_upgrade && priceInfo.remaining_value > 0 && (
<Flex justify="space-between" color="blue.600">
<Text>已付剩余抵扣:</Text>
<Text>-¥{priceInfo.remaining_value.toFixed(2)}</Text>
</Flex>
)}
{priceInfo && priceInfo.discount_amount > 0 && (
<Flex justify="space-between" color="green.600">
<Text>优惠码折扣:</Text>
<Text>-¥{priceInfo.discount_amount.toFixed(2)}</Text>
</Flex>
)}
<Divider />
<Flex justify="space-between" align="baseline">
<Text fontSize="lg" fontWeight="bold">实付金额:</Text>
<Text fontSize="2xl" fontWeight="bold" color="blue.500">
¥{priceInfo ? priceInfo.final_amount.toFixed(2) : getCurrentPrice(selectedPlan).toFixed(2)}
</Text>
</Flex>
{getSavingsText(selectedPlan) && !priceInfo?.is_upgrade && (
<Badge colorScheme="green" alignSelf="flex-end" fontSize="xs">
{getSavingsText(selectedPlan)}
</Badge>
@@ -1104,6 +1271,53 @@ export default function SubscriptionContent() {
</Box>
)}
{/* 优惠码输入 */}
{selectedPlan && (
<Box>
<HStack spacing={2}>
<Input
placeholder="输入优惠码(可选)"
value={promoCode}
onChange={(e) => {
setPromoCode(e.target.value.toUpperCase());
setPromoCodeError('');
}}
size="md"
isDisabled={promoCodeApplied}
/>
<Button
colorScheme="purple"
onClick={handleValidatePromoCode}
isLoading={validatingPromo}
isDisabled={!promoCode || promoCodeApplied}
minW="80px"
>
应用
</Button>
</HStack>
{promoCodeError && (
<Text color="red.500" fontSize="sm" mt={2}>
{promoCodeError}
</Text>
)}
{promoCodeApplied && priceInfo && (
<HStack mt={2} p={2} bg="green.50" borderRadius="md">
<Icon as={FaCheck} color="green.500" />
<Text color="green.700" fontSize="sm" fontWeight="medium" flex={1}>
优惠码已应用节省 ¥{priceInfo.discount_amount.toFixed(2)}
</Text>
<Icon
as={FaTimes}
color="gray.500"
cursor="pointer"
onClick={handleRemovePromoCode}
_hover={{ color: 'red.500' }}
/>
</HStack>
)}
</Box>
)}
<Button
colorScheme="green"
size="lg"