优惠码Bug修复

This commit is contained in:
2025-11-07 07:53:07 +08:00
parent dcc88251df
commit 83c6abdfba
2 changed files with 200 additions and 36 deletions

28
app.py
View File

@@ -706,11 +706,38 @@ class SubscriptionPlan(db.Model):
monthly_price = db.Column(db.Numeric(10, 2), nullable=False)
yearly_price = db.Column(db.Numeric(10, 2), nullable=False)
features = db.Column(db.Text, nullable=True)
pricing_options = db.Column(db.Text, nullable=True) # JSON格式[{"months": 1, "price": 99}, {"months": 12, "price": 999}]
is_active = db.Column(db.Boolean, default=True)
sort_order = db.Column(db.Integer, default=0)
created_at = db.Column(db.DateTime, default=beijing_now)
def to_dict(self):
# 解析pricing_options如果存在
pricing_opts = None
if self.pricing_options:
try:
pricing_opts = json.loads(self.pricing_options)
except:
pricing_opts = None
# 如果没有pricing_options则从monthly_price和yearly_price生成默认选项
if not pricing_opts:
pricing_opts = [
{
'months': 1,
'price': float(self.monthly_price) if self.monthly_price else 0,
'label': '月付',
'cycle_key': 'monthly'
},
{
'months': 12,
'price': float(self.yearly_price) if self.yearly_price else 0,
'label': '年付',
'cycle_key': 'yearly',
'discount_percent': 20 # 年付默认20%折扣
}
]
return {
'id': self.id,
'name': self.name,
@@ -718,6 +745,7 @@ class SubscriptionPlan(db.Model):
'description': self.description,
'monthly_price': float(self.monthly_price) if self.monthly_price else 0,
'yearly_price': float(self.yearly_price) if self.yearly_price else 0,
'pricing_options': pricing_opts, # 新增:灵活计费周期选项
'features': json.loads(self.features) if self.features else [],
'is_active': self.is_active,
'sort_order': self.sort_order

View File

@@ -79,7 +79,8 @@ export default function SubscriptionContent() {
// State
const [subscriptionPlans, setSubscriptionPlans] = useState([]);
const [selectedPlan, setSelectedPlan] = useState(null);
const [selectedCycle, setSelectedCycle] = useState('monthly');
const [selectedCycle, setSelectedCycle] = useState('monthly'); // 保持向后兼容,默认月付
const [selectedCycleOption, setSelectedCycleOption] = useState(null); // 当前选中的pricing_option对象
const [paymentOrder, setPaymentOrder] = useState(null);
const [loading, setLoading] = useState(false);
const [paymentCountdown, setPaymentCountdown] = useState(0);
@@ -587,15 +588,62 @@ export default function SubscriptionContent() {
const getCurrentPrice = (plan) => {
if (!plan) return 0;
// 如果有pricing_options使用它
if (plan.pricing_options && plan.pricing_options.length > 0) {
// 查找当前选中的周期选项
const option = plan.pricing_options.find(opt =>
opt.cycle_key === selectedCycle ||
(selectedCycle === 'monthly' && opt.months === 1) ||
(selectedCycle === 'yearly' && opt.months === 12)
);
return option ? option.price : plan.monthly_price;
}
// 向后兼容回退到monthly_price/yearly_price
return selectedCycle === 'monthly' ? plan.monthly_price : plan.yearly_price;
};
const getSavingsText = (plan) => {
if (!plan || selectedCycle !== 'yearly') return null;
if (!plan) return null;
// 如果有pricing_options从中查找discount_percent
if (plan.pricing_options && plan.pricing_options.length > 0) {
const currentOption = plan.pricing_options.find(opt =>
opt.cycle_key === selectedCycle ||
(selectedCycle === 'monthly' && opt.months === 1) ||
(selectedCycle === 'yearly' && opt.months === 12)
);
if (currentOption && currentOption.discount_percent) {
return `立减 ${currentOption.discount_percent}%`;
}
// 如果没有discount_percent尝试计算节省金额
if (currentOption && currentOption.months > 1) {
const monthlyOption = plan.pricing_options.find(opt => opt.months === 1);
if (monthlyOption) {
const expectedTotal = monthlyOption.price * currentOption.months;
const savings = expectedTotal - currentOption.price;
if (savings > 0) {
const percentage = Math.round((savings / expectedTotal) * 100);
return `${currentOption.label || `${currentOption.months}个月`}节省 ${percentage}%`;
}
}
}
}
// 向后兼容:计算年付节省
if (selectedCycle === 'yearly') {
const yearlyTotal = plan.monthly_price * 12;
const savings = yearlyTotal - plan.yearly_price;
if (savings > 0) {
const percentage = Math.round((savings / yearlyTotal) * 100);
return `年付节省 ${percentage}%`;
}
}
return null;
};
// 获取按钮文字(根据用户当前订阅判断是升级还是新订阅)
@@ -717,26 +765,39 @@ export default function SubscriptionContent() {
p={1}
border="1px solid"
borderColor={borderColor}
flexWrap="wrap"
>
{(() => {
// 获取第一个套餐的pricing_options作为周期选项假设所有套餐都有相同的周期
const firstPlan = subscriptionPlans.find(plan => plan.pricing_options);
const cycleOptions = firstPlan?.pricing_options || [
{ cycle_key: 'monthly', label: '按月付费', months: 1 },
{ cycle_key: 'yearly', label: '按年付费', months: 12, discount_percent: 20 }
];
return cycleOptions.map((option, index) => {
const cycleKey = option.cycle_key || (option.months === 1 ? 'monthly' : option.months === 12 ? 'yearly' : `${option.months}months`);
const isSelected = selectedCycle === cycleKey;
return (
<Button
variant={selectedCycle === 'monthly' ? 'solid' : 'ghost'}
colorScheme={selectedCycle === 'monthly' ? 'blue' : 'gray'}
key={index}
variant={isSelected ? 'solid' : 'ghost'}
colorScheme={isSelected ? 'blue' : 'gray'}
size="md"
onClick={() => setSelectedCycle('monthly')}
onClick={() => setSelectedCycle(cycleKey)}
borderRadius="md"
>
按月付费
</Button>
<Button
variant={selectedCycle === 'yearly' ? 'solid' : 'ghost'}
colorScheme={selectedCycle === 'yearly' ? 'blue' : 'gray'}
size="md"
onClick={() => setSelectedCycle('yearly')}
borderRadius="md"
>
按年付费
<Badge ml={2} colorScheme="red" fontSize="xs">省20%</Badge>
{option.label || `${option.months}个月`}
{option.discount_percent && (
<Badge ml={2} colorScheme="red" fontSize="xs">
{option.discount_percent}%
</Badge>
)}
</Button>
);
});
})()}
</HStack>
</Flex>
@@ -911,7 +972,22 @@ export default function SubscriptionContent() {
{getCurrentPrice(plan).toFixed(0)}
</Text>
<Text fontSize="sm" color={secondaryText}>
/{selectedCycle === 'monthly' ? '' : ''}
{(() => {
if (plan.pricing_options) {
const option = plan.pricing_options.find(opt =>
opt.cycle_key === selectedCycle ||
(selectedCycle === 'monthly' && opt.months === 1) ||
(selectedCycle === 'yearly' && opt.months === 12)
);
if (option) {
// 如果months是1显示"/月"如果是12显示"/年"否则显示周期label
if (option.months === 1) return '/月';
if (option.months === 12) return '/年';
return `/${option.months}个月`;
}
}
return selectedCycle === 'monthly' ? '/月' : '/年';
})()}
</Text>
</HStack>
</Flex>
@@ -1091,7 +1167,7 @@ export default function SubscriptionContent() {
align="center"
>
<Text fontWeight="semibold" color={textColor}>
可以在月付和年付之间切换吗
升级或切换套餐时原套餐的费用怎么办
</Text>
<Icon
as={openFaqIndex === 2 ? FaChevronUp : FaChevronDown}
@@ -1100,14 +1176,28 @@ export default function SubscriptionContent() {
</Flex>
<Collapse in={openFaqIndex === 2}>
<Box p={4} pt={0} color={secondaryText}>
<VStack spacing={2} align="stretch">
<Text>
可以您可以随时更改计费周期如果从月付切换到年付系统会计算剩余价值并应用到新的订阅中年付用户可享受20%的折扣优惠
当您升级套餐或切换计费周期时系统会自动计算您当前订阅的剩余价值并用于抵扣新套餐的费用
</Text>
<Text fontWeight="medium" fontSize="sm">
计算方式
</Text>
<Text fontSize="sm" pl={3}>
<strong>剩余价值</strong> = 原套餐价格 × (剩余天数 / 总天数)
</Text>
<Text fontSize="sm" pl={3}>
<strong>实付金额</strong> = - -
</Text>
<Text fontSize="sm" color="blue.600" mt={2}>
例如您购买了年付Pro版¥999使用了180天后升级到Max版¥1999/剩余价值约¥500将自动抵扣实付约¥1499
</Text>
</VStack>
</Box>
</Collapse>
</Box>
{/* FAQ 4 */}
{/* FAQ 4 - 原FAQ 3 */}
<Box
border="1px solid"
borderColor={borderColor}
@@ -1125,7 +1215,7 @@ export default function SubscriptionContent() {
align="center"
>
<Text fontWeight="semibold" color={textColor}>
是否提供退款
可以在月付和年付之间切换吗
</Text>
<Icon
as={openFaqIndex === 3 ? FaChevronUp : FaChevronDown}
@@ -1135,13 +1225,13 @@ export default function SubscriptionContent() {
<Collapse in={openFaqIndex === 3}>
<Box p={4} pt={0} color={secondaryText}>
<Text>
我们提供7天无理由退款保证如果您在订阅后7天内对服务不满意可以申请全额退款超过7天后我们将根据实际使用情况进行评估
可以您可以随时更改计费周期如果从月付切换到年付系统会计算剩余价值并应用到新的订阅中年付用户可享受20%的折扣优惠
</Text>
</Box>
</Collapse>
</Box>
{/* FAQ 5 */}
{/* FAQ 5 - 原FAQ 4 */}
<Box
border="1px solid"
borderColor={borderColor}
@@ -1159,7 +1249,7 @@ export default function SubscriptionContent() {
align="center"
>
<Text fontWeight="semibold" color={textColor}>
Pro版和Max版有什么区别
是否提供退款
</Text>
<Icon
as={openFaqIndex === 4 ? FaChevronUp : FaChevronDown}
@@ -1167,6 +1257,40 @@ export default function SubscriptionContent() {
/>
</Flex>
<Collapse in={openFaqIndex === 4}>
<Box p={4} pt={0} color={secondaryText}>
<Text>
我们提供7天无理由退款保证如果您在订阅后7天内对服务不满意可以申请全额退款超过7天后我们将根据实际使用情况进行评估
</Text>
</Box>
</Collapse>
</Box>
{/* FAQ 6 - 原FAQ 5 */}
<Box
border="1px solid"
borderColor={borderColor}
borderRadius="lg"
overflow="hidden"
>
<Flex
p={4}
cursor="pointer"
onClick={() => setOpenFaqIndex(openFaqIndex === 5 ? null : 5)}
bg={openFaqIndex === 5 ? bgAccent : 'transparent'}
_hover={{ bg: bgAccent }}
transition="all 0.2s"
justify="space-between"
align="center"
>
<Text fontWeight="semibold" color={textColor}>
Pro版和Max版有什么区别
</Text>
<Icon
as={openFaqIndex === 5 ? FaChevronUp : FaChevronDown}
color={textColor}
/>
</Flex>
<Collapse in={openFaqIndex === 5}>
<Box p={4} pt={0} color={secondaryText}>
<Text>
Pro版适合个人专业用户提供高级图表历史数据分析等功能Max版则是为团队和企业设计额外提供实时数据推送API访问无限制的数据存储和团队协作功能并享有优先技术支持
@@ -1220,7 +1344,19 @@ export default function SubscriptionContent() {
</Flex>
<Flex justify="space-between">
<Text color={secondaryText}>计费周期:</Text>
<Text>{selectedCycle === 'monthly' ? '按月付费' : '按年付费'}</Text>
<Text>
{(() => {
if (selectedPlan?.pricing_options) {
const option = selectedPlan.pricing_options.find(opt =>
opt.cycle_key === selectedCycle ||
(selectedCycle === 'monthly' && opt.months === 1) ||
(selectedCycle === 'yearly' && opt.months === 12)
);
return option?.label || (selectedCycle === 'monthly' ? '按月付费' : '按年付费');
}
return selectedCycle === 'monthly' ? '按月付费' : '按年付费';
})()}
</Text>
</Flex>
{/* 价格明细 */}