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' ? '按月付费' : '按年付费';
+ })()}
+
{/* 价格明细 */}