From 83c6abdfba2189d2d20f8d803f7442c37cd358f2 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Fri, 7 Nov 2025 07:53:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E6=83=A0=E7=A0=81Bug=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 28 +++ .../Subscription/SubscriptionContent.js | 208 +++++++++++++++--- 2 files changed, 200 insertions(+), 36 deletions(-) diff --git a/app.py b/app.py index 7cf4d8ce..796f2c93 100755 --- a/app.py +++ b/app.py @@ -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 diff --git a/src/components/Subscription/SubscriptionContent.js b/src/components/Subscription/SubscriptionContent.js index f66d60a7..d43caec1 100644 --- a/src/components/Subscription/SubscriptionContent.js +++ b/src/components/Subscription/SubscriptionContent.js @@ -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; - const yearlyTotal = plan.monthly_price * 12; - const savings = yearlyTotal - plan.yearly_price; - const percentage = Math.round((savings / yearlyTotal) * 100); - return `年付节省 ${percentage}%`; + 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 ( + + ); + }); + })()} @@ -911,7 +972,22 @@ export default function SubscriptionContent() { {getCurrentPrice(plan).toFixed(0)} - /{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' ? '/月' : '/年'; + })()} @@ -1091,7 +1167,7 @@ export default function SubscriptionContent() { align="center" > - 可以在月付和年付之间切换吗? + 升级或切换套餐时,原套餐的费用怎么办? - - 可以。您可以随时更改计费周期。如果从月付切换到年付,系统会计算剩余价值并应用到新的订阅中。年付用户可享受20%的折扣优惠。 - + + + 当您升级套餐或切换计费周期时,系统会自动计算您当前订阅的剩余价值并用于抵扣新套餐的费用。 + + + 计算方式: + + + • 剩余价值 = 原套餐价格 × (剩余天数 / 总天数) + + + • 实付金额 = 新套餐价格 - 剩余价值 - 优惠码折扣 + + + 例如:您购买了年付Pro版(¥999),使用了180天后升级到Max版(¥1999/年),剩余价值约¥500将自动抵扣,实付约¥1499。 + + - {/* FAQ 4 */} + {/* FAQ 4 - 原FAQ 3 */} - 是否提供退款? + 可以在月付和年付之间切换吗? - 我们提供7天无理由退款保证。如果您在订阅后7天内对服务不满意,可以申请全额退款。超过7天后,我们将根据实际使用情况进行评估。 + 可以。您可以随时更改计费周期。如果从月付切换到年付,系统会计算剩余价值并应用到新的订阅中。年付用户可享受20%的折扣优惠。 - {/* FAQ 5 */} + {/* FAQ 5 - 原FAQ 4 */} - Pro版和Max版有什么区别? + 是否提供退款? + + + 我们提供7天无理由退款保证。如果您在订阅后7天内对服务不满意,可以申请全额退款。超过7天后,我们将根据实际使用情况进行评估。 + + + + + + {/* FAQ 6 - 原FAQ 5 */} + + setOpenFaqIndex(openFaqIndex === 5 ? null : 5)} + bg={openFaqIndex === 5 ? bgAccent : 'transparent'} + _hover={{ bg: bgAccent }} + transition="all 0.2s" + justify="space-between" + align="center" + > + + Pro版和Max版有什么区别? + + + + Pro版适合个人专业用户,提供高级图表、历史数据分析等功能。Max版则是为团队和企业设计,额外提供实时数据推送、API访问、无限制的数据存储和团队协作功能,并享有优先技术支持。 @@ -1220,7 +1344,19 @@ export default function SubscriptionContent() { 计费周期: - {selectedCycle === 'monthly' ? '按月付费' : '按年付费'} + + {(() => { + 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' ? '按月付费' : '按年付费'; + })()} + {/* 价格明细 */}