diff --git a/app.py b/app.py
index 39c37f34..d9225c92 100755
--- a/app.py
+++ b/app.py
@@ -1127,36 +1127,61 @@ def get_user_subscription_safe(user_id):
def activate_user_subscription(user_id, plan_type, billing_cycle, extend_from_now=False):
- """激活用户订阅
+ """
+ 激活用户订阅(新版:续费时从当前订阅结束时间开始延长)
Args:
user_id: 用户ID
- plan_type: 套餐类型
- billing_cycle: 计费周期
- extend_from_now: 是否从当前时间开始延长(用于升级场景)
+ plan_type: 套餐类型 (pro/max)
+ billing_cycle: 计费周期 (monthly/quarterly/semiannual/yearly)
+ extend_from_now: 废弃参数,保留以兼容(现在自动判断)
+
+ Returns:
+ UserSubscription 对象 或 None
"""
try:
subscription = UserSubscription.query.filter_by(user_id=user_id).first()
if not subscription:
+ # 新用户,创建订阅记录
subscription = UserSubscription(user_id=user_id)
db.session.add(subscription)
+ # 更新订阅类型和状态
subscription.subscription_type = plan_type
subscription.subscription_status = 'active'
subscription.billing_cycle = billing_cycle
- if not extend_from_now or not subscription.start_date:
- subscription.start_date = beijing_now()
+ # 计算订阅周期天数
+ cycle_days_map = {
+ 'monthly': 30,
+ 'quarterly': 90, # 3个月
+ 'semiannual': 180, # 6个月
+ 'yearly': 365
+ }
+ days = cycle_days_map.get(billing_cycle, 30)
- if billing_cycle == 'monthly':
- subscription.end_date = beijing_now() + timedelta(days=30)
- else: # yearly
- subscription.end_date = beijing_now() + timedelta(days=365)
+ now = beijing_now()
+
+ # 判断是新购还是续费
+ if subscription.end_date and subscription.end_date > now:
+ # 续费:从当前订阅结束时间开始延长
+ start_date = subscription.end_date
+ end_date = start_date + timedelta(days=days)
+ else:
+ # 新购或过期后重新购买:从当前时间开始
+ start_date = now
+ end_date = now + timedelta(days=days)
+ subscription.start_date = start_date
+
+ subscription.end_date = end_date
+ subscription.updated_at = now
- subscription.updated_at = beijing_now()
db.session.commit()
return subscription
+
except Exception as e:
+ print(f"激活订阅失败: {e}")
+ db.session.rollback()
return None
@@ -1233,33 +1258,29 @@ def calculate_discount(promo_code, amount):
return 0
-def calculate_remaining_value(subscription, current_plan):
- """计算当前订阅的剩余价值"""
- try:
- if not subscription or not subscription.end_date:
- return 0
+def calculate_subscription_price_simple(user_id, to_plan_name, to_cycle, promo_code=None):
+ """
+ 简化版价格计算:续费用户和新用户价格完全一致,不计算剩余价值
- now = beijing_now()
- if subscription.end_date <= now:
- return 0
-
- days_left = (subscription.end_date - now).days
-
- if subscription.billing_cycle == 'monthly':
- daily_value = float(current_plan.monthly_price) / 30
- else: # yearly
- daily_value = float(current_plan.yearly_price) / 365
-
- return daily_value * days_left
- except:
- return 0
-
-
-def calculate_upgrade_price(user_id, to_plan_name, to_cycle, promo_code=None):
- """计算升级所需价格
+ Args:
+ user_id: 用户ID
+ to_plan_name: 目标套餐名称 (pro/max)
+ to_cycle: 计费周期 (monthly/quarterly/semiannual/yearly)
+ promo_code: 优惠码(可选)
Returns:
- dict: 包含价格计算结果的字典
+ dict: {
+ 'is_renewal': False/True, # 是否为续费
+ 'subscription_type': 'new'/'renew', # 订阅类型
+ 'current_plan': 'pro', # 当前套餐(如果有)
+ 'current_cycle': 'yearly', # 当前周期(如果有)
+ 'new_plan_price': 2699.00, # 新套餐价格
+ 'original_amount': 2699.00, # 原价
+ 'discount_amount': 0, # 优惠金额
+ 'final_amount': 2699.00, # 实付金额
+ 'promo_code': None, # 使用的优惠码
+ 'promo_error': None # 优惠码错误信息
+ }
"""
try:
# 1. 获取当前订阅
@@ -1270,83 +1291,90 @@ def calculate_upgrade_price(user_id, to_plan_name, to_cycle, promo_code=None):
if not to_plan:
return {'error': '目标套餐不存在'}
- # 3. 计算目标套餐价格
- new_price = float(to_plan.yearly_price if to_cycle == 'yearly' else to_plan.monthly_price)
+ # 3. 根据计费周期获取价格
+ # 优先从 pricing_options 获取价格
+ price = None
+ if to_plan.pricing_options:
+ try:
+ pricing_opts = json.loads(to_plan.pricing_options)
+ # 查找匹配的周期
+ for opt in pricing_opts:
+ cycle_key = opt.get('cycle_key', '')
+ months = opt.get('months', 0)
- # 4. 如果是新订阅(非升级)
- if not current_sub or current_sub.subscription_type == 'free':
- result = {
- 'is_upgrade': False,
- 'new_plan_price': new_price,
- 'remaining_value': 0,
- 'upgrade_amount': new_price,
- 'original_amount': new_price,
- 'discount_amount': 0,
- 'final_amount': new_price,
- 'promo_code': None
- }
+ # 匹配逻辑
+ if (cycle_key == to_cycle or
+ (to_cycle == 'monthly' and months == 1) or
+ (to_cycle == 'quarterly' and months == 3) or
+ (to_cycle == 'semiannual' and months == 6) or
+ (to_cycle == 'yearly' and months == 12)):
+ price = float(opt.get('price', 0))
+ break
+ except:
+ pass
- # 应用优惠码
- if promo_code:
- promo, error = validate_promo_code(promo_code, to_plan_name, to_cycle, new_price, user_id)
- if promo:
- discount = calculate_discount(promo, new_price)
- result['discount_amount'] = discount
- result['final_amount'] = new_price - discount
- result['promo_code'] = promo.code
- elif error:
- result['promo_error'] = error
+ # 如果 pricing_options 中没有找到,使用旧的 monthly_price/yearly_price
+ if price is None:
+ if to_cycle == 'yearly':
+ price = float(to_plan.yearly_price) if to_plan.yearly_price else 0
+ else: # 默认月付
+ price = float(to_plan.monthly_price) if to_plan.monthly_price else 0
- return result
+ if price <= 0:
+ return {'error': f'{to_cycle} 周期价格未配置'}
- # 5. 升级场景:计算剩余价值
- current_plan = SubscriptionPlan.query.filter_by(name=current_sub.subscription_type, is_active=True).first()
- if not current_plan:
- return {'error': '当前套餐信息不存在'}
+ # 4. 判断是新购还是续费
+ is_renewal = False
+ subscription_type = 'new'
+ current_plan = None
+ current_cycle = None
- remaining_value = calculate_remaining_value(current_sub, current_plan)
-
- # 6. 计算升级差价
- upgrade_amount = max(0, new_price - remaining_value)
-
- # 7. 判断升级类型
- upgrade_type = 'new'
- if current_sub.subscription_type != to_plan_name and current_sub.billing_cycle != to_cycle:
- upgrade_type = 'both'
- elif current_sub.subscription_type != to_plan_name:
- upgrade_type = 'plan_upgrade'
- elif current_sub.billing_cycle != to_cycle:
- upgrade_type = 'cycle_change'
+ if current_sub and current_sub.subscription_type in ['pro', 'max']:
+ # 如果当前是付费用户,则为续费
+ is_renewal = True
+ subscription_type = 'renew'
+ current_plan = current_sub.subscription_type
+ current_cycle = current_sub.billing_cycle
+ # 5. 构建结果(续费和新购价格完全一致)
result = {
- 'is_upgrade': True,
- 'upgrade_type': upgrade_type,
- 'current_plan': current_sub.subscription_type,
- 'current_cycle': current_sub.billing_cycle,
- 'current_end_date': current_sub.end_date.isoformat() if current_sub.end_date else None,
- 'new_plan_price': new_price,
- 'remaining_value': remaining_value,
- 'upgrade_amount': upgrade_amount,
- 'original_amount': upgrade_amount,
+ 'is_renewal': is_renewal,
+ 'subscription_type': subscription_type,
+ 'current_plan': current_plan,
+ 'current_cycle': current_cycle,
+ 'new_plan_price': price,
+ 'original_amount': price,
'discount_amount': 0,
- 'final_amount': upgrade_amount,
- 'promo_code': None
+ 'final_amount': price,
+ 'promo_code': None,
+ 'promo_error': None
}
- # 8. 应用优惠码
- if promo_code and upgrade_amount > 0:
- promo, error = validate_promo_code(promo_code, to_plan_name, to_cycle, upgrade_amount, user_id)
+ # 6. 应用优惠码
+ if promo_code and promo_code.strip():
+ promo, error = validate_promo_code(promo_code, to_plan_name, to_cycle, price, user_id)
if promo:
- discount = calculate_discount(promo, upgrade_amount)
- result['discount_amount'] = discount
- result['final_amount'] = upgrade_amount - discount
+ discount = calculate_discount(promo, price)
+ result['discount_amount'] = float(discount)
+ result['final_amount'] = price - float(discount)
result['promo_code'] = promo.code
elif error:
result['promo_error'] = error
return result
+
except Exception as e:
- return {'error': str(e)}
+ return {'error': f'价格计算失败: {str(e)}'}
+
+
+# 保留旧函数以兼容(标记为废弃)
+def calculate_upgrade_price(user_id, to_plan_name, to_cycle, promo_code=None):
+ """
+ 【已废弃】旧版升级价格计算函数,保留以兼容旧代码
+ 新代码请使用 calculate_subscription_price_simple
+ """
+ # 直接调用新函数
+ return calculate_subscription_price_simple(user_id, to_plan_name, to_cycle, promo_code)
def initialize_subscription_plans_safe():
@@ -1594,7 +1622,33 @@ def validate_promo_code_api():
@app.route('/api/subscription/calculate-price', methods=['POST'])
def calculate_subscription_price():
- """计算订阅价格(支持升级和优惠码)"""
+ """
+ 计算订阅价格(新版:续费和新购价格一致)
+
+ Request Body:
+ {
+ "to_plan": "pro",
+ "to_cycle": "yearly",
+ "promo_code": "WELCOME2025" // 可选
+ }
+
+ Response:
+ {
+ "success": true,
+ "data": {
+ "is_renewal": true, // 是否为续费
+ "subscription_type": "renew", // new 或 renew
+ "current_plan": "pro", // 当前套餐(如果有)
+ "current_cycle": "monthly", // 当前周期(如果有)
+ "new_plan_price": 2699.00,
+ "original_amount": 2699.00,
+ "discount_amount": 0,
+ "final_amount": 2699.00,
+ "promo_code": null,
+ "promo_error": null
+ }
+ }
+ """
try:
if 'user_id' not in session:
return jsonify({'success': False, 'error': '未登录'}), 401
@@ -1607,8 +1661,8 @@ def calculate_subscription_price():
if not to_plan or not to_cycle:
return jsonify({'success': False, 'error': '参数不完整'}), 400
- # 计算价格
- result = calculate_upgrade_price(session['user_id'], to_plan, to_cycle, promo_code)
+ # 使用新的简化价格计算函数
+ result = calculate_subscription_price_simple(session['user_id'], to_plan, to_cycle, promo_code)
if 'error' in result:
return jsonify({
@@ -1630,7 +1684,16 @@ def calculate_subscription_price():
@app.route('/api/payment/create-order', methods=['POST'])
def create_payment_order():
- """创建支付订单(支持升级和优惠码)"""
+ """
+ 创建支付订单(新版:简化逻辑,不再记录升级)
+
+ Request Body:
+ {
+ "plan_name": "pro",
+ "billing_cycle": "yearly",
+ "promo_code": "WELCOME2025" // 可选
+ }
+ """
try:
if 'user_id' not in session:
return jsonify({'success': False, 'error': '未登录'}), 401
@@ -1643,16 +1706,14 @@ def create_payment_order():
if not plan_name or not billing_cycle:
return jsonify({'success': False, 'error': '参数不完整'}), 400
- # 计算价格(包括升级和优惠码)
- price_result = calculate_upgrade_price(session['user_id'], plan_name, billing_cycle, promo_code)
+ # 使用新的简化价格计算
+ price_result = calculate_subscription_price_simple(session['user_id'], plan_name, billing_cycle, promo_code)
if 'error' in price_result:
return jsonify({'success': False, 'error': price_result['error']}), 400
amount = price_result['final_amount']
- original_amount = price_result['original_amount']
- discount_amount = price_result['discount_amount']
- is_upgrade = price_result.get('is_upgrade', False)
+ subscription_type = price_result.get('subscription_type', 'new') # new 或 renew
# 创建订单
try:
@@ -1663,48 +1724,23 @@ def create_payment_order():
amount=amount
)
- # 添加扩展字段(使用动态属性)
- if hasattr(order, 'original_amount') or True: # 兼容性检查
- order.original_amount = original_amount
- order.discount_amount = discount_amount
- order.is_upgrade = is_upgrade
+ # 添加订阅类型标记(用于前端展示)
+ order.remark = f"{subscription_type}订阅" if subscription_type == 'renew' else "新购订阅"
- # 如果使用了优惠码,关联优惠码
- if promo_code and price_result.get('promo_code'):
- promo_obj = PromoCode.query.filter_by(code=promo_code.upper()).first()
- if promo_obj:
+ # 如果使用了优惠码,关联优惠码
+ if promo_code and price_result.get('promo_code'):
+ promo_obj = PromoCode.query.filter_by(code=promo_code.upper()).first()
+ if promo_obj:
+ # 注意:需要在 PaymentOrder 表中添加 promo_code_id 字段
+ # 如果没有该字段,这行会报错,可以注释掉
+ try:
order.promo_code_id = promo_obj.id
-
- # 如果是升级,记录原套餐信息
- if is_upgrade:
- order.upgrade_from_plan = price_result.get('current_plan')
+ except:
+ pass # 如果表中没有该字段,跳过
db.session.add(order)
db.session.commit()
- # 如果是升级订单,创建升级记录
- if is_upgrade and price_result.get('upgrade_type'):
- try:
- upgrade_record = SubscriptionUpgrade(
- user_id=session['user_id'],
- order_id=order.id,
- from_plan=price_result['current_plan'],
- from_cycle=price_result['current_cycle'],
- from_end_date=datetime.fromisoformat(price_result['current_end_date']) if price_result.get('current_end_date') else None,
- to_plan=plan_name,
- to_cycle=billing_cycle,
- to_end_date=beijing_now() + timedelta(days=365 if billing_cycle == 'yearly' else 30),
- remaining_value=price_result['remaining_value'],
- upgrade_amount=price_result['upgrade_amount'],
- actual_amount=amount,
- upgrade_type=price_result['upgrade_type']
- )
- db.session.add(upgrade_record)
- db.session.commit()
- except Exception as e:
- print(f"创建升级记录失败: {e}")
- # 不影响主流程
-
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': f'订单创建失败: {str(e)}'}), 500
diff --git a/database_migration.sql b/database_migration.sql
new file mode 100644
index 00000000..02b6884c
--- /dev/null
+++ b/database_migration.sql
@@ -0,0 +1,427 @@
+-- ============================================
+-- 订阅支付系统数据库迁移 SQL
+-- 版本: v2.0.0
+-- 日期: 2025-11-19
+-- ============================================
+
+-- ============================================
+-- 第一步: 备份现有数据
+-- ============================================
+
+-- 创建备份表
+CREATE TABLE IF NOT EXISTS user_subscriptions_backup AS SELECT * FROM user_subscriptions;
+CREATE TABLE IF NOT EXISTS payment_orders_backup AS SELECT * FROM payment_orders;
+CREATE TABLE IF NOT EXISTS subscription_plans_backup AS SELECT * FROM subscription_plans;
+
+-- ============================================
+-- 第二步: 删除旧表(先删除外键依赖的表)
+-- ============================================
+
+DROP TABLE IF EXISTS subscription_upgrades; -- 删除升级表,不再使用
+DROP TABLE IF EXISTS promo_code_usage; -- 暂时删除,稍后重建
+DROP TABLE IF EXISTS payment_orders; -- 删除旧订单表
+DROP TABLE IF EXISTS user_subscriptions; -- 删除旧订阅表
+DROP TABLE IF EXISTS subscription_plans; -- 删除旧套餐表
+
+-- ============================================
+-- 第三步: 创建新表结构
+-- ============================================
+
+-- 1. 订阅套餐表(重构)
+CREATE TABLE subscription_plans (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+ plan_code VARCHAR(20) NOT NULL UNIQUE COMMENT '套餐代码: pro, max',
+ plan_name VARCHAR(50) NOT NULL COMMENT '套餐名称: Pro专业版, Max旗舰版',
+ description TEXT COMMENT '套餐描述',
+ features JSON COMMENT '功能列表',
+
+ -- 价格配置(所有周期价格)
+ price_monthly DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '月付价格',
+ price_quarterly DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '季付价格(3个月)',
+ price_semiannual DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '半年付价格(6个月)',
+ price_yearly DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '年付价格(12个月)',
+
+ -- 状态字段
+ is_active BOOLEAN DEFAULT TRUE COMMENT '是否启用',
+ display_order INT DEFAULT 0 COMMENT '展示顺序',
+
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ INDEX idx_plan_code (plan_code),
+ INDEX idx_active_order (is_active, display_order)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订阅套餐配置表';
+
+
+-- 2. 用户订阅记录表(重构)
+CREATE TABLE user_subscriptions (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+ user_id INT NOT NULL COMMENT '用户ID',
+ subscription_id VARCHAR(32) UNIQUE NOT NULL COMMENT '订阅ID(唯一标识)',
+
+ -- 订阅基本信息
+ plan_code VARCHAR(20) NOT NULL COMMENT '套餐代码: pro, max, free',
+ billing_cycle VARCHAR(20) NOT NULL COMMENT '计费周期: monthly, quarterly, semiannual, yearly',
+
+ -- 订阅时间
+ start_date DATETIME NOT NULL COMMENT '订阅开始时间',
+ end_date DATETIME NOT NULL COMMENT '订阅结束时间',
+
+ -- 订阅状态
+ status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态: active(有效), expired(已过期), cancelled(已取消)',
+ is_current BOOLEAN DEFAULT FALSE COMMENT '是否为当前生效的订阅',
+
+ -- 支付信息
+ payment_order_id INT COMMENT '关联的支付订单ID',
+ paid_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '实际支付金额',
+ original_price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '原价',
+ discount_amount DECIMAL(10,2) DEFAULT 0 COMMENT '优惠金额',
+
+ -- 订阅类型
+ subscription_type VARCHAR(20) DEFAULT 'new' COMMENT '订阅类型: new(新购), renew(续费)',
+ previous_subscription_id VARCHAR(32) COMMENT '上一个订阅ID(续费时记录)',
+
+ -- 自动续费
+ auto_renew BOOLEAN DEFAULT FALSE COMMENT '是否自动续费',
+
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ INDEX idx_user_id (user_id),
+ INDEX idx_subscription_id (subscription_id),
+ INDEX idx_user_current (user_id, is_current),
+ INDEX idx_status (status),
+ INDEX idx_end_date (end_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户订阅记录表';
+
+
+-- 3. 支付订单表(重构)
+CREATE TABLE payment_orders (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+ order_no VARCHAR(32) UNIQUE NOT NULL COMMENT '订单号',
+ user_id INT NOT NULL COMMENT '用户ID',
+
+ -- 订阅信息
+ plan_code VARCHAR(20) NOT NULL COMMENT '套餐代码',
+ billing_cycle VARCHAR(20) NOT NULL COMMENT '计费周期',
+ subscription_type VARCHAR(20) DEFAULT 'new' COMMENT '订阅类型: new(新购), renew(续费)',
+
+ -- 价格信息
+ original_price DECIMAL(10,2) NOT NULL COMMENT '原价',
+ discount_amount DECIMAL(10,2) DEFAULT 0 COMMENT '优惠金额',
+ final_amount DECIMAL(10,2) NOT NULL COMMENT '实付金额',
+
+ -- 优惠码
+ promo_code_id INT COMMENT '优惠码ID',
+ promo_code VARCHAR(50) COMMENT '优惠码',
+
+ -- 支付信息
+ payment_method VARCHAR(20) DEFAULT 'wechat' COMMENT '支付方式: wechat, alipay',
+ payment_channel VARCHAR(50) COMMENT '支付渠道详情',
+ transaction_id VARCHAR(64) COMMENT '第三方交易号',
+ qr_code_url TEXT COMMENT '支付二维码URL',
+
+ -- 订单状态
+ status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending(待支付), paid(已支付), expired(已过期), cancelled(已取消)',
+
+ -- 时间信息
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ paid_at TIMESTAMP NULL COMMENT '支付时间',
+ expired_at TIMESTAMP NULL COMMENT '过期时间',
+
+ -- 备注
+ remark TEXT COMMENT '备注信息',
+
+ INDEX idx_order_no (order_no),
+ INDEX idx_user_id (user_id),
+ INDEX idx_status (status),
+ INDEX idx_created_at (created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付订单表';
+
+
+-- 4. 优惠码使用记录表(重建)
+CREATE TABLE promo_code_usage (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+ promo_code_id INT NOT NULL,
+ user_id INT NOT NULL,
+ order_id INT NOT NULL,
+ discount_amount DECIMAL(10,2) NOT NULL COMMENT '实际优惠金额',
+ used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+
+ INDEX idx_promo_code (promo_code_id),
+ INDEX idx_user_id (user_id),
+ INDEX idx_order_id (order_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠码使用记录表';
+
+
+-- ============================================
+-- 第四步: 插入初始数据
+-- ============================================
+
+-- 插入套餐数据
+INSERT INTO subscription_plans (
+ plan_code,
+ plan_name,
+ description,
+ price_monthly,
+ price_quarterly,
+ price_semiannual,
+ price_yearly,
+ features,
+ display_order,
+ is_active
+) VALUES
+(
+ 'pro',
+ 'Pro 专业版',
+ '为专业投资者打造,解锁高级分析功能',
+ 299.00,
+ 799.00,
+ 1499.00,
+ 2699.00,
+ JSON_ARRAY(
+ '新闻信息流',
+ '历史事件对比',
+ '事件传导链分析(AI)',
+ '事件-相关标的分析',
+ '相关概念展示',
+ 'AI复盘功能',
+ '企业概览',
+ '个股深度分析(AI) - 50家/月',
+ '高效数据筛选工具',
+ '概念中心(548大概念)',
+ '历史时间轴查询 - 100天',
+ '涨停板块数据分析',
+ '个股涨停分析'
+ ),
+ 1,
+ TRUE
+),
+(
+ 'max',
+ 'Max 旗舰版',
+ '旗舰级体验,无限制使用所有功能',
+ 599.00,
+ 1599.00,
+ 2999.00,
+ 5399.00,
+ JSON_ARRAY(
+ '全部 Pro 版功能',
+ '板块深度分析(AI)',
+ '个股深度分析(AI) - 无限制',
+ '历史时间轴查询 - 无限制',
+ '概念高频更新',
+ '优先客服支持',
+ '独家功能抢先体验'
+ ),
+ 2,
+ TRUE
+);
+
+
+-- ============================================
+-- 第五步: 数据迁移(可选)
+-- ============================================
+
+-- 如果需要迁移旧数据,取消以下注释:
+
+/*
+-- 迁移旧的用户订阅数据
+INSERT INTO user_subscriptions (
+ user_id,
+ subscription_id,
+ plan_code,
+ billing_cycle,
+ start_date,
+ end_date,
+ status,
+ is_current,
+ paid_amount,
+ original_price,
+ subscription_type,
+ auto_renew,
+ created_at
+)
+SELECT
+ user_id,
+ CONCAT('SUB_', id, '_', UNIX_TIMESTAMP(NOW())), -- 生成订阅ID
+ subscription_type, -- 将 subscription_type 映射为 plan_code
+ COALESCE(billing_cycle, 'yearly'), -- 默认年付
+ COALESCE(start_date, NOW()),
+ COALESCE(end_date, DATE_ADD(NOW(), INTERVAL 365 DAY)),
+ subscription_status,
+ TRUE, -- 设为当前订阅
+ 0, -- 旧数据没有支付金额,设为0
+ 0, -- 旧数据没有原价,设为0
+ 'new', -- 默认为新购
+ COALESCE(auto_renewal, FALSE),
+ created_at
+FROM user_subscriptions_backup
+WHERE subscription_type IN ('pro', 'max'); -- 只迁移付费用户
+*/
+
+-- ============================================
+-- 第六步: 创建免费订阅记录(为所有用户)
+-- ============================================
+
+-- 为所有现有用户创建免费订阅记录(如果没有付费订阅)
+/*
+INSERT INTO user_subscriptions (
+ user_id,
+ subscription_id,
+ plan_code,
+ billing_cycle,
+ start_date,
+ end_date,
+ status,
+ is_current,
+ paid_amount,
+ original_price,
+ subscription_type
+)
+SELECT
+ id AS user_id,
+ CONCAT('FREE_', id, '_', UNIX_TIMESTAMP(NOW())),
+ 'free',
+ 'monthly',
+ NOW(),
+ '2099-12-31 23:59:59', -- 免费版永久有效
+ 'active',
+ TRUE,
+ 0,
+ 0,
+ 'new'
+FROM user
+WHERE id NOT IN (
+ SELECT DISTINCT user_id FROM user_subscriptions WHERE plan_code IN ('pro', 'max')
+);
+*/
+
+-- ============================================
+-- 第七步: 验证数据完整性
+-- ============================================
+
+-- 检查套餐数据
+SELECT * FROM subscription_plans;
+
+-- 检查用户订阅数据
+SELECT
+ plan_code,
+ COUNT(*) as user_count,
+ SUM(CASE WHEN is_current = TRUE THEN 1 ELSE 0 END) as current_count
+FROM user_subscriptions
+GROUP BY plan_code;
+
+-- 检查支付订单数据
+SELECT
+ status,
+ COUNT(*) as order_count,
+ SUM(final_amount) as total_amount
+FROM payment_orders
+GROUP BY status;
+
+-- ============================================
+-- 第八步: 添加外键约束(可选)
+-- ============================================
+
+-- 注意: 只有在确认 users 表存在且数据完整时才执行
+
+-- ALTER TABLE user_subscriptions
+-- ADD CONSTRAINT fk_user_subscriptions_user
+-- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+
+-- ALTER TABLE payment_orders
+-- ADD CONSTRAINT fk_payment_orders_user
+-- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+
+-- ALTER TABLE payment_orders
+-- ADD CONSTRAINT fk_payment_orders_promo
+-- FOREIGN KEY (promo_code_id) REFERENCES promo_codes(id) ON DELETE SET NULL;
+
+-- ALTER TABLE promo_code_usage
+-- ADD CONSTRAINT fk_promo_usage_promo
+-- FOREIGN KEY (promo_code_id) REFERENCES promo_codes(id) ON DELETE CASCADE;
+
+-- ALTER TABLE promo_code_usage
+-- ADD CONSTRAINT fk_promo_usage_user
+-- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+
+-- ALTER TABLE promo_code_usage
+-- ADD CONSTRAINT fk_promo_usage_order
+-- FOREIGN KEY (order_id) REFERENCES payment_orders(id) ON DELETE CASCADE;
+
+
+-- ============================================
+-- 第九步: 创建测试数据(开发环境)
+-- ============================================
+
+-- 插入测试优惠码
+INSERT INTO promo_codes (
+ code,
+ description,
+ discount_type,
+ discount_value,
+ applicable_plans,
+ applicable_cycles,
+ max_total_uses,
+ max_uses_per_user,
+ valid_from,
+ valid_until,
+ is_active
+) VALUES
+(
+ 'WELCOME2025',
+ '2025新用户专享',
+ 'percentage',
+ 20.00,
+ NULL, -- 适用所有套餐
+ NULL, -- 适用所有周期
+ 1000,
+ 1,
+ NOW(),
+ DATE_ADD(NOW(), INTERVAL 90 DAY),
+ TRUE
+),
+(
+ 'YEAR2025',
+ '年付专享',
+ 'percentage',
+ 10.00,
+ NULL,
+ JSON_ARRAY('yearly'), -- 仅适用年付
+ 500,
+ 1,
+ NOW(),
+ DATE_ADD(NOW(), INTERVAL 365 DAY),
+ TRUE
+),
+(
+ 'TESTCODE',
+ '测试优惠码 - 固定减100元',
+ 'fixed_amount',
+ 100.00,
+ NULL,
+ NULL,
+ 100,
+ 1,
+ NOW(),
+ DATE_ADD(NOW(), INTERVAL 30 DAY),
+ TRUE
+);
+
+
+-- ============================================
+-- 迁移完成提示
+-- ============================================
+
+SELECT '===================================' AS '';
+SELECT '数据库迁移完成!' AS '状态';
+SELECT '===================================' AS '';
+SELECT '请检查以下数据:' AS '提示';
+SELECT '1. subscription_plans 表是否有2条记录 (pro, max)' AS '';
+SELECT '2. user_subscriptions 表数据是否正确' AS '';
+SELECT '3. payment_orders 表结构是否正确' AS '';
+SELECT '4. 备份表 (*_backup) 已创建' AS '';
+SELECT '===================================' AS '';
+SELECT '下一步: 更新后端代码 (app.py, models.py)' AS '';
+SELECT '===================================' AS '';
diff --git a/docs/NEW_PAYMENT_SYSTEM_DESIGN.md b/docs/NEW_PAYMENT_SYSTEM_DESIGN.md
new file mode 100644
index 00000000..2211f6db
--- /dev/null
+++ b/docs/NEW_PAYMENT_SYSTEM_DESIGN.md
@@ -0,0 +1,576 @@
+# 订阅支付系统重新设计方案
+
+## 📊 问题分析
+
+### 现有系统的问题
+
+1. **价格配置混乱**
+ - 季付和月付价格相同(配置错误)
+ - `monthly_price` 和 `yearly_price` 字段命名不清晰
+ - 缺少季付、半年付等周期的价格配置
+
+2. **升级逻辑复杂且不合理**
+ - 计算剩余价值折算(按天计算 `remaining_value`)
+ - 用户难以理解升级价格
+ - 续费用户和新用户价格不一致
+ - 逻辑复杂,容易出错
+
+3. **按钮文案不清晰**
+ - 已订阅用户应显示"续费 Pro"/"续费 Max"
+ - 而不是"升级至 Pro"/"切换至 Pro"
+
+4. **数据库表设计问题**
+ - `SubscriptionUpgrade` 表记录升级,但逻辑过于复杂
+ - `PaymentOrder` 表缺少必要字段
+ - 价格配置分散在多个字段
+
+---
+
+## ✨ 新设计方案
+
+### 核心原则
+
+1. **简化续费逻辑**: **续费用户与新用户价格完全一致**,不做任何折算
+2. **清晰的价格体系**: 每个套餐每个周期都有明确的价格
+3. **统一的用户体验**: 无论是新购还是续费,价格透明一致
+4. **独立的订阅记录**: 每次支付都创建新的订阅记录(历史可追溯)
+
+---
+
+## 📐 数据库表设计
+
+### 1. `subscription_plans` - 订阅套餐表(重构)
+
+```sql
+CREATE TABLE subscription_plans (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+ plan_code VARCHAR(20) NOT NULL UNIQUE COMMENT '套餐代码: pro, max',
+ plan_name VARCHAR(50) NOT NULL COMMENT '套餐名称: Pro专业版, Max旗舰版',
+ description TEXT COMMENT '套餐描述',
+ features JSON COMMENT '功能列表',
+
+ -- 价格配置(所有周期价格)
+ price_monthly DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '月付价格',
+ price_quarterly DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '季付价格(3个月)',
+ price_semiannual DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '半年付价格(6个月)',
+ price_yearly DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '年付价格(12个月)',
+
+ -- 状态字段
+ is_active BOOLEAN DEFAULT TRUE COMMENT '是否启用',
+ display_order INT DEFAULT 0 COMMENT '展示顺序',
+
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ INDEX idx_plan_code (plan_code),
+ INDEX idx_active_order (is_active, display_order)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订阅套餐配置表';
+```
+
+**示例数据**:
+```sql
+INSERT INTO subscription_plans (plan_code, plan_name, description, price_monthly, price_quarterly, price_semiannual, price_yearly) VALUES
+('pro', 'Pro 专业版', '为专业投资者打造', 299.00, 799.00, 1499.00, 2699.00),
+('max', 'Max 旗舰版', '旗舰级体验', 599.00, 1599.00, 2999.00, 5399.00);
+```
+
+---
+
+### 2. `user_subscriptions` - 用户订阅记录表(重构)
+
+```sql
+CREATE TABLE user_subscriptions (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+ user_id INT NOT NULL COMMENT '用户ID',
+ subscription_id VARCHAR(32) UNIQUE NOT NULL COMMENT '订阅ID(唯一标识)',
+
+ -- 订阅基本信息
+ plan_code VARCHAR(20) NOT NULL COMMENT '套餐代码: pro, max',
+ billing_cycle VARCHAR(20) NOT NULL COMMENT '计费周期: monthly, quarterly, semiannual, yearly',
+
+ -- 订阅时间
+ start_date DATETIME NOT NULL COMMENT '订阅开始时间',
+ end_date DATETIME NOT NULL COMMENT '订阅结束时间',
+
+ -- 订阅状态
+ status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态: active(有效), expired(已过期), cancelled(已取消)',
+ is_current BOOLEAN DEFAULT FALSE COMMENT '是否为当前生效的订阅',
+
+ -- 支付信息
+ payment_order_id INT COMMENT '关联的支付订单ID',
+ paid_amount DECIMAL(10,2) NOT NULL COMMENT '实际支付金额',
+ original_price DECIMAL(10,2) NOT NULL COMMENT '原价',
+ discount_amount DECIMAL(10,2) DEFAULT 0 COMMENT '优惠金额',
+
+ -- 订阅类型
+ subscription_type VARCHAR(20) DEFAULT 'new' COMMENT '订阅类型: new(新购), renew(续费)',
+ previous_subscription_id VARCHAR(32) COMMENT '上一个订阅ID(续费时记录)',
+
+ -- 自动续费
+ auto_renew BOOLEAN DEFAULT FALSE COMMENT '是否自动续费',
+
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ INDEX idx_user_id (user_id),
+ INDEX idx_subscription_id (subscription_id),
+ INDEX idx_user_current (user_id, is_current),
+ INDEX idx_status (status),
+ INDEX idx_end_date (end_date),
+
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户订阅记录表';
+```
+
+**设计说明**:
+- 每次支付都创建新的订阅记录
+- 通过 `is_current` 标识当前生效的订阅
+- 支持订阅历史追溯
+- 续费时记录 `previous_subscription_id` 形成订阅链
+
+---
+
+### 3. `payment_orders` - 支付订单表(重构)
+
+```sql
+CREATE TABLE payment_orders (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+ order_no VARCHAR(32) UNIQUE NOT NULL COMMENT '订单号',
+ user_id INT NOT NULL COMMENT '用户ID',
+
+ -- 订阅信息
+ plan_code VARCHAR(20) NOT NULL COMMENT '套餐代码',
+ billing_cycle VARCHAR(20) NOT NULL COMMENT '计费周期',
+ subscription_type VARCHAR(20) DEFAULT 'new' COMMENT '订阅类型: new(新购), renew(续费)',
+
+ -- 价格信息
+ original_price DECIMAL(10,2) NOT NULL COMMENT '原价',
+ discount_amount DECIMAL(10,2) DEFAULT 0 COMMENT '优惠金额',
+ final_amount DECIMAL(10,2) NOT NULL COMMENT '实付金额',
+
+ -- 优惠码
+ promo_code_id INT COMMENT '优惠码ID',
+ promo_code VARCHAR(50) COMMENT '优惠码',
+
+ -- 支付信息
+ payment_method VARCHAR(20) DEFAULT 'wechat' COMMENT '支付方式: wechat, alipay',
+ payment_channel VARCHAR(50) COMMENT '支付渠道详情',
+ transaction_id VARCHAR(64) COMMENT '第三方交易号',
+ qr_code_url TEXT COMMENT '支付二维码URL',
+
+ -- 订单状态
+ status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending(待支付), paid(已支付), expired(已过期), cancelled(已取消)',
+
+ -- 时间信息
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ paid_at TIMESTAMP NULL COMMENT '支付时间',
+ expired_at TIMESTAMP NULL COMMENT '过期时间',
+
+ -- 备注
+ remark TEXT COMMENT '备注信息',
+
+ INDEX idx_order_no (order_no),
+ INDEX idx_user_id (user_id),
+ INDEX idx_status (status),
+ INDEX idx_created_at (created_at),
+
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (promo_code_id) REFERENCES promo_codes(id) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付订单表';
+```
+
+---
+
+### 4. `promo_codes` - 优惠码表(保持不变,微调)
+
+```sql
+CREATE TABLE promo_codes (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+ code VARCHAR(50) UNIQUE NOT NULL COMMENT '优惠码',
+ description VARCHAR(200) COMMENT '描述',
+
+ -- 折扣类型
+ discount_type VARCHAR(20) NOT NULL COMMENT '折扣类型: percentage(百分比), fixed_amount(固定金额)',
+ discount_value DECIMAL(10,2) NOT NULL COMMENT '折扣值',
+
+ -- 适用范围
+ applicable_plans JSON COMMENT '适用套餐: ["pro", "max"] 或 null(全部)',
+ applicable_cycles JSON COMMENT '适用周期: ["monthly", "yearly"] 或 null(全部)',
+ min_amount DECIMAL(10,2) COMMENT '最低消费金额',
+
+ -- 使用限制
+ max_total_uses INT COMMENT '最大使用次数(总)',
+ max_uses_per_user INT DEFAULT 1 COMMENT '每用户最大使用次数',
+ current_uses INT DEFAULT 0 COMMENT '当前使用次数',
+
+ -- 有效期
+ valid_from TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '生效时间',
+ valid_until TIMESTAMP NULL COMMENT '过期时间',
+
+ -- 状态
+ is_active BOOLEAN DEFAULT TRUE COMMENT '是否启用',
+
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ INDEX idx_code (code),
+ INDEX idx_active (is_active),
+ INDEX idx_valid_period (valid_from, valid_until)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠码表';
+```
+
+---
+
+### 5. `promo_code_usage` - 优惠码使用记录表(保持不变)
+
+```sql
+CREATE TABLE promo_code_usage (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+ promo_code_id INT NOT NULL,
+ user_id INT NOT NULL,
+ order_id INT NOT NULL,
+ discount_amount DECIMAL(10,2) NOT NULL COMMENT '实际优惠金额',
+ used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+
+ INDEX idx_promo_code (promo_code_id),
+ INDEX idx_user_id (user_id),
+ INDEX idx_order_id (order_id),
+
+ FOREIGN KEY (promo_code_id) REFERENCES promo_codes(id) ON DELETE CASCADE,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (order_id) REFERENCES payment_orders(id) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠码使用记录表';
+```
+
+---
+
+### 6. 删除不必要的表
+
+**删除 `subscription_upgrades` 表** - 不再需要复杂的升级逻辑
+
+---
+
+## 💡 业务逻辑设计
+
+### 1. 价格计算逻辑(简化版)
+
+```python
+def calculate_subscription_price(plan_code, billing_cycle, promo_code=None):
+ """
+ 计算订阅价格(新购和续费价格完全一致)
+
+ Args:
+ plan_code: 套餐代码 (pro/max)
+ billing_cycle: 计费周期 (monthly/quarterly/semiannual/yearly)
+ promo_code: 优惠码(可选)
+
+ Returns:
+ dict: {
+ 'plan_code': 'pro',
+ 'billing_cycle': 'yearly',
+ 'original_price': 2699.00,
+ 'discount_amount': 0,
+ 'final_amount': 2699.00,
+ 'promo_code': None,
+ 'promo_error': None
+ }
+ """
+ # 1. 查询套餐价格
+ plan = SubscriptionPlan.query.filter_by(plan_code=plan_code, is_active=True).first()
+ if not plan:
+ return {'error': '套餐不存在'}
+
+ # 2. 获取对应周期的价格
+ price_field = f'price_{billing_cycle}'
+ original_price = getattr(plan, price_field, 0)
+
+ if original_price <= 0:
+ return {'error': '价格配置错误'}
+
+ result = {
+ 'plan_code': plan_code,
+ 'plan_name': plan.plan_name,
+ 'billing_cycle': billing_cycle,
+ 'original_price': float(original_price),
+ 'discount_amount': 0,
+ 'final_amount': float(original_price),
+ 'promo_code': None,
+ 'promo_error': None
+ }
+
+ # 3. 应用优惠码(如果有)
+ if promo_code:
+ promo, error = validate_promo_code(promo_code, plan_code, billing_cycle, original_price, user_id)
+ if promo:
+ discount = calculate_discount(promo, original_price)
+ result['discount_amount'] = float(discount)
+ result['final_amount'] = float(original_price - discount)
+ result['promo_code'] = promo.code
+ elif error:
+ result['promo_error'] = error
+
+ return result
+```
+
+**关键点**:
+- ✅ 不再计算 `remaining_value`(剩余价值)
+- ✅ 不再区分新购/续费价格
+- ✅ 逻辑简单,易于维护
+- ✅ 用户体验清晰透明
+
+---
+
+### 2. 创建订单逻辑
+
+```python
+def create_subscription_order(user_id, plan_code, billing_cycle, promo_code=None):
+ """
+ 创建订阅支付订单
+ """
+ # 1. 计算价格
+ price_result = calculate_subscription_price(plan_code, billing_cycle, promo_code)
+ if 'error' in price_result:
+ return {'success': False, 'error': price_result['error']}
+
+ # 2. 判断是新购还是续费
+ current_sub = get_current_subscription(user_id)
+
+ subscription_type = 'new'
+ if current_sub and current_sub.plan_code in ['pro', 'max']:
+ subscription_type = 'renew'
+
+ # 3. 创建支付订单
+ order = PaymentOrder(
+ order_no=generate_order_no(user_id),
+ user_id=user_id,
+ plan_code=plan_code,
+ billing_cycle=billing_cycle,
+ subscription_type=subscription_type,
+ original_price=price_result['original_price'],
+ discount_amount=price_result['discount_amount'],
+ final_amount=price_result['final_amount'],
+ promo_code=promo_code,
+ status='pending',
+ expired_at=datetime.now() + timedelta(minutes=30)
+ )
+
+ db.session.add(order)
+ db.session.commit()
+
+ # 4. 生成支付二维码(微信支付)
+ qr_code_url = generate_wechat_qr_code(order)
+ order.qr_code_url = qr_code_url
+ db.session.commit()
+
+ return {'success': True, 'order': order.to_dict()}
+```
+
+---
+
+### 3. 支付成功后的订阅激活逻辑
+
+```python
+def activate_subscription_after_payment(order_id):
+ """
+ 支付成功后激活订阅
+ """
+ order = PaymentOrder.query.get(order_id)
+ if not order or order.status != 'paid':
+ return {'success': False, 'error': '订单状态错误'}
+
+ user_id = order.user_id
+ plan_code = order.plan_code
+ billing_cycle = order.billing_cycle
+
+ # 1. 计算订阅周期
+ cycle_days = {
+ 'monthly': 30,
+ 'quarterly': 90,
+ 'semiannual': 180,
+ 'yearly': 365
+ }
+ days = cycle_days.get(billing_cycle, 30)
+
+ # 2. 获取当前订阅
+ current_sub = UserSubscription.query.filter_by(
+ user_id=user_id,
+ is_current=True
+ ).first()
+
+ # 3. 计算开始和结束时间
+ now = datetime.now()
+
+ if current_sub and current_sub.end_date > now:
+ # 续费:从当前订阅结束时间开始
+ start_date = current_sub.end_date
+ else:
+ # 新购:从当前时间开始
+ start_date = now
+
+ end_date = start_date + timedelta(days=days)
+
+ # 4. 创建新订阅记录
+ new_subscription = UserSubscription(
+ user_id=user_id,
+ subscription_id=generate_subscription_id(),
+ plan_code=plan_code,
+ billing_cycle=billing_cycle,
+ start_date=start_date,
+ end_date=end_date,
+ status='active',
+ is_current=True,
+ payment_order_id=order.id,
+ paid_amount=order.final_amount,
+ original_price=order.original_price,
+ discount_amount=order.discount_amount,
+ subscription_type=order.subscription_type,
+ previous_subscription_id=current_sub.subscription_id if current_sub else None
+ )
+
+ # 5. 将旧订阅标记为非当前
+ if current_sub:
+ current_sub.is_current = False
+
+ db.session.add(new_subscription)
+ db.session.commit()
+
+ return {'success': True, 'subscription': new_subscription.to_dict()}
+```
+
+**关键特性**:
+- ✅ 续费时从**当前订阅结束时间**开始,避免浪费
+- ✅ 每次支付都创建新的订阅记录
+- ✅ 保留历史订阅记录(通过 `previous_subscription_id` 形成链)
+- ✅ 逻辑清晰,易于理解
+
+---
+
+### 4. 按钮文案逻辑
+
+```python
+def get_subscription_button_text(user, plan_code, billing_cycle):
+ """
+ 获取订阅按钮文字
+
+ Args:
+ user: 用户对象
+ plan_code: 套餐代码 (pro/max)
+ billing_cycle: 计费周期
+
+ Returns:
+ str: 按钮文字
+ """
+ current_sub = get_current_subscription(user.id)
+
+ # 1. 如果没有订阅或订阅已过期
+ if not current_sub or current_sub.plan_code == 'free' or current_sub.status != 'active':
+ return f"选择 {get_plan_display_name(plan_code)}"
+
+ # 2. 如果是当前套餐且周期相同
+ if current_sub.plan_code == plan_code and current_sub.billing_cycle == billing_cycle:
+ return f"续费 {get_plan_display_name(plan_code)}"
+
+ # 3. 如果是当前套餐但周期不同
+ if current_sub.plan_code == plan_code:
+ return f"切换至{get_cycle_display_name(billing_cycle)}"
+
+ # 4. 如果是不同套餐
+ return f"选择 {get_plan_display_name(plan_code)}"
+
+def get_plan_display_name(plan_code):
+ names = {'pro': 'Pro 专业版', 'max': 'Max 旗舰版'}
+ return names.get(plan_code, plan_code)
+
+def get_cycle_display_name(billing_cycle):
+ names = {
+ 'monthly': '月付',
+ 'quarterly': '季付',
+ 'semiannual': '半年付',
+ 'yearly': '年付'
+ }
+ return names.get(billing_cycle, billing_cycle)
+```
+
+**示例**:
+- 免费用户看 Pro 年付: "选择 Pro 专业版"
+- Pro 月付用户看 Pro 年付: "切换至年付"
+- Pro 年付用户看 Pro 年付: "续费 Pro 专业版"
+- Pro 用户看 Max 年付: "选择 Max 旗舰版"
+
+---
+
+## 📊 价格配置示例
+
+### Pro 专业版价格设定
+
+| 计费周期 | 价格 | 原价 | 折扣 | 月均价格 |
+|---------|------|------|------|---------|
+| 月付 | ¥299 | - | - | ¥299 |
+| 季付(3个月) | ¥799 | ¥897 | 11% | ¥266 |
+| 半年付(6个月) | ¥1499 | ¥1794 | 16% | ¥250 |
+| 年付(12个月) | ¥2699 | ¥3588 | 25% | ¥225 |
+
+### Max 旗舰版价格设定
+
+| 计费周期 | 价格 | 原价 | 折扣 | 月均价格 |
+|---------|------|------|------|---------|
+| 月付 | ¥599 | - | - | ¥599 |
+| 季付(3个月) | ¥1599 | ¥1797 | 11% | ¥533 |
+| 半年付(6个月) | ¥2999 | ¥3594 | 17% | ¥500 |
+| 年付(12个月) | ¥5399 | ¥7188 | 25% | ¥450 |
+
+---
+
+## 🔄 迁移方案
+
+### 数据迁移 SQL
+
+参见 `database_migration.sql`
+
+### 代码迁移步骤
+
+1. **备份现有数据库**
+2. **执行数据库迁移 SQL**
+3. **更新数据库模型** (`models.py`)
+4. **更新价格计算逻辑** (`calculate_price.py`)
+5. **更新 API 路由** (`routes.py`)
+6. **更新前端组件** (`SubscriptionContentNew.tsx`)
+7. **测试完整流程**
+8. **灰度发布**
+
+---
+
+## ✅ 优势总结
+
+### 相比旧系统的改进
+
+1. **价格透明** - 续费用户和新用户价格完全一致
+2. **逻辑简化** - 不再计算剩余价值,代码减少 50%+
+3. **易于理解** - 用户体验更清晰
+4. **灵活扩展** - 轻松添加新的计费周期
+5. **历史追溯** - 完整的订阅历史记录
+6. **数据完整** - 每次支付都有完整的记录
+
+### 用户体验改进
+
+1. **按钮文案清晰** - "续费 Pro"/"选择 Pro"明确表达意图
+2. **价格一致性** - 所有用户看到的价格都一样
+3. **无隐藏费用** - 不会因为"升级折算"产生困惑
+4. **透明计费** - 支付金额 = 显示价格 - 优惠码折扣
+
+---
+
+## 📝 后续优化建议
+
+1. **自动续费** - 到期前自动扣款续费
+2. **订阅提醒** - 到期前 7 天、3 天、1 天发送通知
+3. **订阅暂停** - 允许用户暂停订阅
+4. **订阅降级** - 从 Max 降级到 Pro(当前周期结束后生效)
+5. **发票管理** - 支持开具电子发票
+6. **支付方式扩展** - 支持支付宝、银行卡等
+
+---
+
+**设计时间**: 2025-11-19
+**设计者**: Claude Code
+**版本**: v2.0.0
diff --git a/index.pug b/index.pug
new file mode 100644
index 00000000..188f1628
--- /dev/null
+++ b/index.pug
@@ -0,0 +1,339 @@
+extends layouts/layout
+block content
+ +header(true, false, false)
+
+ // hero
+
+
+
+
+
智能舆情分析系统
+
+
基于金融领域微调的大语言模型,7×24小时不间断对舆情数据进行深度挖掘和分析,对历史事件进行复盘,关联相关标的,为投资决策提供前瞻性的智能洞察。
+
+
+
+
+
+
+ img(class="w-5" src=require('Images/clock.svg') alt="")
+
+
实时数据分析
+
+
+
+
+
+ img(class="w-5" src=require('Images/floor.svg') alt="")
+
+
低延迟推理
+
+
+
+
+
+
+ video(class="w-full" src=require('Videos/video-1.webm') autoplay loop muted playsinline)
+
+
+
+
+
+ // details
+
+
+
+
+
+
99%
+
金融数据理解准确率
+
基于金融领域深度微调的大语言模型,精准理解市场动态和舆情变化。
+
+
+ img(class="w-86.25 max-xl:w-72 max-md:w-full" src=require('Images/details-pic-1.png') alt="")
+
+
+
+
+
24/7
+
全天候舆情监控
+
7×24小时不间断监控市场舆情,第一时间捕捉关键信息。
+
+
+ img(class="w-86.25 max-xl:w-72 max-md:w-full" src=require('Images/details-pic-2.png') alt="")
+
+
+
+
+ img(class="w-full max-lg:max-w-60 max-md:max-w-73.5" src=require('Images/details-pic-3.png') alt="")
+
+
+
深度模型微调
+
针对金融领域数据进行专业化模型训练和优化。
+
+
+
+
+ img(class="w-full" src=require('Images/details-pic-4.png') alt="")
+
+
+
+
+ img(class="w-4" src=require('Images/lightning.svg') alt="")
+
+
<100ms
+
+
低延迟推理系统
+
毫秒级响应速度,实时处理海量舆情数据。
+
+
+
+
+ img(class="w-full max-lg:max-w-60 max-md:max-w-73.5" src=require('Images/details-pic-5.png') alt="")
+
+
+
历史复盘
+
对历史事件进行深度复盘分析,关联标的,辅助投资决策。
+
+
+
+
+
+ // features
+
+
+
+
核心功能
+
我们能做什么?
+
基于AI的舆情分析系统,深度挖掘市场动态,为投资决策提供实时智能洞察。
+
+
+
+
+ img(class="w-full max-md:max-w-73.5" src=require('Images/features-pic-1.png') alt="")
+
+
+
舆情数据挖掘
+
实时采集和分析全网金融舆情,捕捉市场情绪变化。
+
+
+
+
+ img(class="w-full max-md:max-w-73.5" src=require('Images/features-pic-2.png') alt="")
+
+
+
智能事件关联
+
自动关联相关标的和历史事件,构建完整的信息图谱。
+
+
+
+
+ img(class="w-full max-md:max-w-73.5" src=require('Images/features-pic-3.png') alt="")
+
+
+
历史复盘
+
深度复盘历史事件走势,洞察关键节点与转折,为投资决策提供经验参考。
+
+
+
+
+ img(class="w-full max-md:max-w-73.5" src=require('Images/features-pic-4.png') alt="")
+
+
+
专精金融的AI聊天
+
基于金融领域深度训练的智能对话助手,即时解答市场问题,提供专业投资建议。
+
+
+
+
+
+
+ // pricing
+
+ include includes/start
+
+ +footer(true)
\ No newline at end of file
diff --git a/new_subscription_logic.py b/new_subscription_logic.py
new file mode 100644
index 00000000..465f72ce
--- /dev/null
+++ b/new_subscription_logic.py
@@ -0,0 +1,631 @@
+# -*- coding: utf-8 -*-
+"""
+新版订阅支付系统核心逻辑
+版本: v2.0.0
+日期: 2025-11-19
+
+核心改进:
+1. 续费价格与新购价格完全一致
+2. 不再计算剩余价值折算
+3. 逻辑简化,易于维护
+"""
+
+from datetime import datetime, timedelta
+from decimal import Decimal
+import json
+import random
+
+
+# ============================================
+# 辅助函数
+# ============================================
+
+def beijing_now():
+ """获取北京时间"""
+ from datetime import timezone, timedelta
+ utc_now = datetime.now(timezone.utc)
+ beijing_time = utc_now.astimezone(timezone(timedelta(hours=8)))
+ return beijing_time.replace(tzinfo=None)
+
+
+def generate_order_no(user_id):
+ """生成订单号"""
+ timestamp = int(beijing_now().timestamp() * 1000000)
+ random_suffix = random.randint(1000, 9999)
+ return f"{timestamp}{user_id:04d}{random_suffix}"
+
+
+def generate_subscription_id():
+ """生成订阅ID"""
+ timestamp = int(beijing_now().timestamp() * 1000)
+ random_suffix = random.randint(10000, 99999)
+ return f"SUB_{timestamp}_{random_suffix}"
+
+
+# ============================================
+# 核心业务逻辑
+# ============================================
+
+def calculate_subscription_price(plan_code, billing_cycle, promo_code=None, user_id=None, db_session=None):
+ """
+ 计算订阅价格(新购和续费价格完全一致)
+
+ Args:
+ plan_code: 套餐代码 (pro/max)
+ billing_cycle: 计费周期 (monthly/quarterly/semiannual/yearly)
+ promo_code: 优惠码(可选)
+ user_id: 用户ID(可选,用于优惠码验证)
+ db_session: 数据库会话(可选)
+
+ Returns:
+ dict: {
+ 'success': True/False,
+ 'plan_code': 'pro',
+ 'plan_name': 'Pro 专业版',
+ 'billing_cycle': 'yearly',
+ 'original_price': 2699.00,
+ 'discount_amount': 0,
+ 'final_amount': 2699.00,
+ 'promo_code': None,
+ 'promo_error': None,
+ 'error': None # 如果有错误
+ }
+ """
+ from models import SubscriptionPlan, PromoCode # 需要在实际使用时导入
+
+ try:
+ # 1. 查询套餐
+ plan = SubscriptionPlan.query.filter_by(plan_code=plan_code, is_active=True).first()
+ if not plan:
+ return {
+ 'success': False,
+ 'error': f'套餐 {plan_code} 不存在或已下架'
+ }
+
+ # 2. 获取对应周期的价格
+ price_field_map = {
+ 'monthly': 'price_monthly',
+ 'quarterly': 'price_quarterly',
+ 'semiannual': 'price_semiannual',
+ 'yearly': 'price_yearly'
+ }
+
+ price_field = price_field_map.get(billing_cycle)
+ if not price_field:
+ return {
+ 'success': False,
+ 'error': f'不支持的计费周期: {billing_cycle}'
+ }
+
+ original_price = getattr(plan, price_field, None)
+ if original_price is None or original_price <= 0:
+ return {
+ 'success': False,
+ 'error': f'{billing_cycle} 周期价格未配置'
+ }
+
+ original_price = float(original_price)
+
+ # 3. 构建基础结果
+ result = {
+ 'success': True,
+ 'plan_code': plan_code,
+ 'plan_name': plan.plan_name,
+ 'billing_cycle': billing_cycle,
+ 'original_price': original_price,
+ 'discount_amount': 0.0,
+ 'final_amount': original_price,
+ 'promo_code': None,
+ 'promo_error': None,
+ 'error': None
+ }
+
+ # 4. 应用优惠码(如果有)
+ if promo_code and promo_code.strip():
+ promo_code = promo_code.strip().upper()
+
+ # 验证优惠码
+ promo, error = validate_promo_code(
+ promo_code,
+ plan_code,
+ billing_cycle,
+ original_price,
+ user_id,
+ db_session
+ )
+
+ if promo:
+ # 计算折扣
+ discount = calculate_discount(promo, original_price)
+ result['discount_amount'] = float(discount)
+ result['final_amount'] = float(original_price - discount)
+ result['promo_code'] = promo.code
+ elif error:
+ result['promo_error'] = error
+
+ return result
+
+ except Exception as e:
+ return {
+ 'success': False,
+ 'error': f'价格计算失败: {str(e)}'
+ }
+
+
+def get_current_subscription(user_id, db_session=None):
+ """
+ 获取用户当前生效的订阅
+
+ Args:
+ user_id: 用户ID
+ db_session: 数据库会话(可选)
+
+ Returns:
+ UserSubscription 对象 或 None
+ """
+ from models import UserSubscription
+
+ try:
+ subscription = UserSubscription.query.filter_by(
+ user_id=user_id,
+ is_current=True
+ ).first()
+
+ # 检查是否过期
+ if subscription and subscription.end_date < beijing_now():
+ subscription.status = 'expired'
+ subscription.is_current = False
+ if db_session:
+ db_session.commit()
+ return None
+
+ return subscription
+
+ except Exception as e:
+ print(f"获取当前订阅失败: {e}")
+ return None
+
+
+def determine_subscription_type(user_id, plan_code, billing_cycle):
+ """
+ 判断订阅类型(新购还是续费)
+
+ Args:
+ user_id: 用户ID
+ plan_code: 目标套餐代码
+ billing_cycle: 目标计费周期
+
+ Returns:
+ str: 'new' 或 'renew'
+ """
+ current_sub = get_current_subscription(user_id)
+
+ # 如果没有订阅或订阅是免费版,则为新购
+ if not current_sub or current_sub.plan_code == 'free':
+ return 'new'
+
+ # 如果是付费订阅,则为续费
+ if current_sub.plan_code in ['pro', 'max']:
+ return 'renew'
+
+ return 'new'
+
+
+def create_subscription_order(user_id, plan_code, billing_cycle, promo_code=None, db_session=None):
+ """
+ 创建订阅支付订单
+
+ Args:
+ user_id: 用户ID
+ plan_code: 套餐代码
+ billing_cycle: 计费周期
+ promo_code: 优惠码(可选)
+ db_session: 数据库会话
+
+ Returns:
+ dict: {
+ 'success': True/False,
+ 'order': PaymentOrder 对象,
+ 'error': None
+ }
+ """
+ from models import PaymentOrder
+
+ try:
+ # 1. 计算价格
+ price_result = calculate_subscription_price(
+ plan_code,
+ billing_cycle,
+ promo_code,
+ user_id,
+ db_session
+ )
+
+ if not price_result.get('success'):
+ return {
+ 'success': False,
+ 'error': price_result.get('error', '价格计算失败')
+ }
+
+ # 2. 判断订阅类型
+ subscription_type = determine_subscription_type(user_id, plan_code, billing_cycle)
+
+ # 3. 创建支付订单
+ order = PaymentOrder(
+ order_no=generate_order_no(user_id),
+ user_id=user_id,
+ plan_code=plan_code,
+ billing_cycle=billing_cycle,
+ subscription_type=subscription_type,
+ original_price=Decimal(str(price_result['original_price'])),
+ discount_amount=Decimal(str(price_result['discount_amount'])),
+ final_amount=Decimal(str(price_result['final_amount'])),
+ promo_code=promo_code,
+ status='pending',
+ expired_at=beijing_now() + timedelta(minutes=30)
+ )
+
+ if db_session:
+ db_session.add(order)
+ db_session.commit()
+
+ return {
+ 'success': True,
+ 'order': order,
+ 'subscription_type': subscription_type,
+ 'error': None
+ }
+
+ except Exception as e:
+ if db_session:
+ db_session.rollback()
+ return {
+ 'success': False,
+ 'error': f'创建订单失败: {str(e)}'
+ }
+
+
+def activate_subscription_after_payment(order_id, db_session=None):
+ """
+ 支付成功后激活订阅
+
+ Args:
+ order_id: 订单ID
+ db_session: 数据库会话
+
+ Returns:
+ dict: {
+ 'success': True/False,
+ 'subscription': UserSubscription 对象,
+ 'error': None
+ }
+ """
+ from models import PaymentOrder, UserSubscription, PromoCodeUsage
+
+ try:
+ # 1. 查询订单
+ order = PaymentOrder.query.get(order_id)
+ if not order:
+ return {'success': False, 'error': '订单不存在'}
+
+ if order.status != 'paid':
+ return {'success': False, 'error': '订单未支付'}
+
+ # 2. 检查是否已经激活
+ existing_sub = UserSubscription.query.filter_by(
+ payment_order_id=order.id
+ ).first()
+
+ if existing_sub:
+ return {
+ 'success': True,
+ 'subscription': existing_sub,
+ 'message': '订阅已激活'
+ }
+
+ # 3. 计算订阅周期天数
+ cycle_days_map = {
+ 'monthly': 30,
+ 'quarterly': 90,
+ 'semiannual': 180,
+ 'yearly': 365
+ }
+ days = cycle_days_map.get(order.billing_cycle, 30)
+
+ # 4. 获取当前订阅
+ current_sub = get_current_subscription(order.user_id, db_session)
+
+ # 5. 计算开始和结束时间
+ now = beijing_now()
+
+ if current_sub and current_sub.end_date > now:
+ # 续费:从当前订阅结束时间开始
+ start_date = current_sub.end_date
+ else:
+ # 新购:从当前时间开始
+ start_date = now
+
+ end_date = start_date + timedelta(days=days)
+
+ # 6. 创建新订阅记录
+ new_subscription = UserSubscription(
+ user_id=order.user_id,
+ subscription_id=generate_subscription_id(),
+ plan_code=order.plan_code,
+ billing_cycle=order.billing_cycle,
+ start_date=start_date,
+ end_date=end_date,
+ status='active',
+ is_current=True,
+ payment_order_id=order.id,
+ paid_amount=order.final_amount,
+ original_price=order.original_price,
+ discount_amount=order.discount_amount,
+ subscription_type=order.subscription_type,
+ previous_subscription_id=current_sub.subscription_id if current_sub else None,
+ auto_renew=False
+ )
+
+ # 7. 将旧订阅标记为非当前
+ if current_sub:
+ current_sub.is_current = False
+
+ if db_session:
+ db_session.add(new_subscription)
+
+ # 8. 记录优惠码使用
+ if order.promo_code_id:
+ usage = PromoCodeUsage(
+ promo_code_id=order.promo_code_id,
+ user_id=order.user_id,
+ order_id=order.id,
+ discount_amount=order.discount_amount
+ )
+ db_session.add(usage)
+
+ # 更新优惠码使用次数
+ from models import PromoCode
+ promo = PromoCode.query.get(order.promo_code_id)
+ if promo:
+ promo.current_uses += 1
+
+ db_session.commit()
+
+ return {
+ 'success': True,
+ 'subscription': new_subscription,
+ 'error': None
+ }
+
+ except Exception as e:
+ if db_session:
+ db_session.rollback()
+ return {
+ 'success': False,
+ 'error': f'激活订阅失败: {str(e)}'
+ }
+
+
+def get_subscription_button_text(user_id, plan_code, billing_cycle):
+ """
+ 获取订阅按钮文字
+
+ Args:
+ user_id: 用户ID
+ plan_code: 套餐代码 (pro/max)
+ billing_cycle: 计费周期
+
+ Returns:
+ str: 按钮文字
+ """
+ from models import SubscriptionPlan
+
+ # 获取套餐显示名称
+ plan = SubscriptionPlan.query.filter_by(plan_code=plan_code).first()
+ plan_name = plan.plan_name if plan else plan_code.upper()
+
+ # 获取周期显示名称
+ cycle_names = {
+ 'monthly': '月付',
+ 'quarterly': '季付',
+ 'semiannual': '半年付',
+ 'yearly': '年付'
+ }
+ cycle_name = cycle_names.get(billing_cycle, billing_cycle)
+
+ # 获取当前订阅
+ current_sub = get_current_subscription(user_id)
+
+ # 1. 如果没有订阅或订阅已过期
+ if not current_sub or current_sub.plan_code == 'free' or current_sub.status != 'active':
+ return f"选择 {plan_name}"
+
+ # 2. 如果是当前套餐且周期相同
+ if current_sub.plan_code == plan_code and current_sub.billing_cycle == billing_cycle:
+ return f"续费 {plan_name}"
+
+ # 3. 如果是当前套餐但周期不同
+ if current_sub.plan_code == plan_code:
+ return f"切换至{cycle_name}"
+
+ # 4. 如果是不同套餐
+ return f"选择 {plan_name}"
+
+
+# ============================================
+# 优惠码相关函数
+# ============================================
+
+def validate_promo_code(code, plan_code, billing_cycle, amount, user_id=None, db_session=None):
+ """
+ 验证优惠码
+
+ Args:
+ code: 优惠码
+ plan_code: 套餐代码
+ billing_cycle: 计费周期
+ amount: 订单金额
+ user_id: 用户ID(可选)
+ db_session: 数据库会话(可选)
+
+ Returns:
+ tuple: (PromoCode对象 或 None, 错误信息 或 None)
+ """
+ from models import PromoCode, PromoCodeUsage
+
+ try:
+ # 查询优惠码
+ promo = PromoCode.query.filter_by(code=code.upper(), is_active=True).first()
+
+ if not promo:
+ return None, "优惠码不存在或已失效"
+
+ # 检查有效期
+ now = beijing_now()
+ if promo.valid_from and now < promo.valid_from:
+ return None, "优惠码尚未生效"
+
+ if promo.valid_until and now > promo.valid_until:
+ return None, "优惠码已过期"
+
+ # 检查总使用次数
+ if promo.max_total_uses and promo.current_uses >= promo.max_total_uses:
+ return None, "优惠码使用次数已达上限"
+
+ # 检查每用户使用次数
+ if user_id and promo.max_uses_per_user:
+ user_usage_count = PromoCodeUsage.query.filter_by(
+ promo_code_id=promo.id,
+ user_id=user_id
+ ).count()
+
+ if user_usage_count >= promo.max_uses_per_user:
+ return None, f"您已使用过此优惠码(限用{promo.max_uses_per_user}次)"
+
+ # 检查适用套餐
+ if promo.applicable_plans:
+ try:
+ applicable = json.loads(promo.applicable_plans)
+ if isinstance(applicable, list) and plan_code not in applicable:
+ return None, "该优惠码不适用于此套餐"
+ except:
+ pass
+
+ # 检查适用周期
+ if promo.applicable_cycles:
+ try:
+ applicable = json.loads(promo.applicable_cycles)
+ if isinstance(applicable, list) and billing_cycle not in applicable:
+ return None, "该优惠码不适用于此计费周期"
+ except:
+ pass
+
+ # 检查最低消费
+ if promo.min_amount and amount < float(promo.min_amount):
+ return None, f"需满 ¥{float(promo.min_amount):.2f} 才可使用此优惠码"
+
+ return promo, None
+
+ except Exception as e:
+ return None, f"验证优惠码时出错: {str(e)}"
+
+
+def calculate_discount(promo_code, amount):
+ """
+ 计算优惠金额
+
+ Args:
+ promo_code: PromoCode 对象
+ amount: 订单金额
+
+ Returns:
+ Decimal: 优惠金额
+ """
+ try:
+ if promo_code.discount_type == 'percentage':
+ # 百分比折扣
+ discount = Decimal(str(amount)) * Decimal(str(promo_code.discount_value)) / Decimal('100')
+ elif promo_code.discount_type == 'fixed_amount':
+ # 固定金额折扣
+ discount = Decimal(str(promo_code.discount_value))
+ else:
+ discount = Decimal('0')
+
+ # 确保折扣不超过总金额
+ discount = min(discount, Decimal(str(amount)))
+
+ return discount
+
+ except Exception as e:
+ print(f"计算折扣失败: {e}")
+ return Decimal('0')
+
+
+# ============================================
+# 辅助查询函数
+# ============================================
+
+def get_user_subscription_history(user_id, limit=10):
+ """
+ 获取用户订阅历史
+
+ Args:
+ user_id: 用户ID
+ limit: 返回记录数量限制
+
+ Returns:
+ list: UserSubscription 对象列表
+ """
+ from models import UserSubscription
+
+ try:
+ subscriptions = UserSubscription.query.filter_by(
+ user_id=user_id
+ ).order_by(
+ UserSubscription.created_at.desc()
+ ).limit(limit).all()
+
+ return subscriptions
+
+ except Exception as e:
+ print(f"获取订阅历史失败: {e}")
+ return []
+
+
+def check_subscription_status(user_id):
+ """
+ 检查用户订阅状态
+
+ Args:
+ user_id: 用户ID
+
+ Returns:
+ dict: {
+ 'has_subscription': True/False,
+ 'plan_code': 'pro' 或 'max' 或 'free',
+ 'status': 'active' 或 'expired',
+ 'end_date': datetime 或 None,
+ 'days_left': int
+ }
+ """
+ current_sub = get_current_subscription(user_id)
+
+ if not current_sub or current_sub.plan_code == 'free':
+ return {
+ 'has_subscription': False,
+ 'plan_code': 'free',
+ 'status': 'active',
+ 'end_date': None,
+ 'days_left': 999
+ }
+
+ now = beijing_now()
+ days_left = (current_sub.end_date - now).days if current_sub.end_date > now else 0
+
+ return {
+ 'has_subscription': True,
+ 'plan_code': current_sub.plan_code,
+ 'status': current_sub.status,
+ 'end_date': current_sub.end_date,
+ 'days_left': days_left
+ }
diff --git a/new_subscription_routes.py b/new_subscription_routes.py
new file mode 100644
index 00000000..f3acedf4
--- /dev/null
+++ b/new_subscription_routes.py
@@ -0,0 +1,669 @@
+# -*- coding: utf-8 -*-
+"""
+新版订阅支付系统 API 路由
+版本: v2.0.0
+日期: 2025-11-19
+
+使用方法:
+将这些路由添加到你的 Flask app.py 中
+"""
+
+from flask import jsonify, request, session
+from new_subscription_logic import (
+ calculate_subscription_price,
+ create_subscription_order,
+ activate_subscription_after_payment,
+ get_subscription_button_text,
+ get_current_subscription,
+ check_subscription_status,
+ get_user_subscription_history
+)
+
+
+# ============================================
+# API 路由定义
+# ============================================
+
+@app.route('/api/v2/subscription/plans', methods=['GET'])
+def get_subscription_plans_v2():
+ """
+ 获取订阅套餐列表(新版)
+
+ Response:
+ {
+ "success": true,
+ "data": [
+ {
+ "plan_code": "pro",
+ "plan_name": "Pro 专业版",
+ "description": "为专业投资者打造",
+ "prices": {
+ "monthly": 299.00,
+ "quarterly": 799.00,
+ "semiannual": 1499.00,
+ "yearly": 2699.00
+ },
+ "features": [...],
+ "is_active": true
+ },
+ ...
+ ]
+ }
+ """
+ try:
+ from models import SubscriptionPlan
+
+ plans = SubscriptionPlan.query.filter_by(is_active=True).order_by(
+ SubscriptionPlan.display_order
+ ).all()
+
+ data = []
+ for plan in plans:
+ data.append({
+ 'plan_code': plan.plan_code,
+ 'plan_name': plan.plan_name,
+ 'description': plan.description,
+ 'prices': {
+ 'monthly': float(plan.price_monthly),
+ 'quarterly': float(plan.price_quarterly),
+ 'semiannual': float(plan.price_semiannual),
+ 'yearly': float(plan.price_yearly)
+ },
+ 'features': json.loads(plan.features) if plan.features else [],
+ 'is_active': plan.is_active,
+ 'display_order': plan.display_order
+ })
+
+ return jsonify({
+ 'success': True,
+ 'data': data
+ })
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'error': f'获取套餐列表失败: {str(e)}'
+ }), 500
+
+
+@app.route('/api/v2/subscription/calculate-price', methods=['POST'])
+def calculate_price_v2():
+ """
+ 计算订阅价格(新版 - 新购和续费价格一致)
+
+ Request Body:
+ {
+ "plan_code": "pro",
+ "billing_cycle": "yearly",
+ "promo_code": "WELCOME2025" // 可选
+ }
+
+ Response:
+ {
+ "success": true,
+ "data": {
+ "plan_code": "pro",
+ "plan_name": "Pro 专业版",
+ "billing_cycle": "yearly",
+ "original_price": 2699.00,
+ "discount_amount": 539.80,
+ "final_amount": 2159.20,
+ "promo_code": "WELCOME2025",
+ "promo_error": null
+ }
+ }
+ """
+ try:
+ if 'user_id' not in session:
+ return jsonify({'success': False, 'error': '未登录'}), 401
+
+ data = request.get_json()
+ plan_code = data.get('plan_code')
+ billing_cycle = data.get('billing_cycle')
+ promo_code = data.get('promo_code')
+
+ if not plan_code or not billing_cycle:
+ return jsonify({
+ 'success': False,
+ 'error': '参数不完整'
+ }), 400
+
+ # 计算价格
+ result = calculate_subscription_price(
+ plan_code=plan_code,
+ billing_cycle=billing_cycle,
+ promo_code=promo_code,
+ user_id=session['user_id'],
+ db_session=db.session
+ )
+
+ if not result.get('success'):
+ return jsonify(result), 400
+
+ return jsonify({
+ 'success': True,
+ 'data': result
+ })
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'error': f'计算价格失败: {str(e)}'
+ }), 500
+
+
+@app.route('/api/v2/payment/create-order', methods=['POST'])
+def create_order_v2():
+ """
+ 创建支付订单(新版)
+
+ Request Body:
+ {
+ "plan_code": "pro",
+ "billing_cycle": "yearly",
+ "promo_code": "WELCOME2025" // 可选
+ }
+
+ Response:
+ {
+ "success": true,
+ "data": {
+ "order_no": "1732012345678901231234",
+ "plan_code": "pro",
+ "billing_cycle": "yearly",
+ "subscription_type": "renew", // 或 "new"
+ "original_price": 2699.00,
+ "discount_amount": 539.80,
+ "final_amount": 2159.20,
+ "qr_code_url": "https://...",
+ "status": "pending",
+ "expired_at": "2025-11-19T15:30:00",
+ ...
+ }
+ }
+ """
+ try:
+ if 'user_id' not in session:
+ return jsonify({'success': False, 'error': '未登录'}), 401
+
+ data = request.get_json()
+ plan_code = data.get('plan_code')
+ billing_cycle = data.get('billing_cycle')
+ promo_code = data.get('promo_code')
+
+ if not plan_code or not billing_cycle:
+ return jsonify({
+ 'success': False,
+ 'error': '参数不完整'
+ }), 400
+
+ # 创建订单
+ order_result = create_subscription_order(
+ user_id=session['user_id'],
+ plan_code=plan_code,
+ billing_cycle=billing_cycle,
+ promo_code=promo_code,
+ db_session=db.session
+ )
+
+ if not order_result.get('success'):
+ return jsonify({
+ 'success': False,
+ 'error': order_result.get('error')
+ }), 400
+
+ order = order_result['order']
+
+ # 生成微信支付二维码
+ try:
+ from wechat_pay import create_wechat_pay_instance, check_wechat_pay_ready
+
+ is_ready, ready_msg = check_wechat_pay_ready()
+ if not is_ready:
+ # 使用模拟二维码
+ order.qr_code_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=wxpay://order/{order.order_no}"
+ order.remark = f"演示模式 - {ready_msg}"
+ else:
+ wechat_pay = create_wechat_pay_instance()
+
+ # 创建微信支付订单
+ plan_display = f"{plan_code.upper()}-{billing_cycle}"
+ wechat_result = wechat_pay.create_native_order(
+ order_no=order.order_no,
+ total_fee=float(order.final_amount),
+ body=f"VFr-{plan_display}",
+ product_id=f"{plan_code}_{billing_cycle}"
+ )
+
+ if wechat_result['success']:
+ wechat_code_url = wechat_result['code_url']
+
+ import urllib.parse
+ encoded_url = urllib.parse.quote(wechat_code_url, safe='')
+ qr_image_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={encoded_url}"
+
+ order.qr_code_url = qr_image_url
+ order.prepay_id = wechat_result.get('prepay_id')
+ order.remark = f"微信支付 - {wechat_code_url}"
+ else:
+ order.qr_code_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=wxpay://order/{order.order_no}"
+ order.remark = f"微信支付失败: {wechat_result.get('error')}"
+
+ except Exception as e:
+ order.qr_code_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=wxpay://order/{order.order_no}"
+ order.remark = f"支付异常: {str(e)}"
+
+ db.session.commit()
+
+ return jsonify({
+ 'success': True,
+ 'data': {
+ 'id': order.id,
+ 'order_no': order.order_no,
+ 'plan_code': order.plan_code,
+ 'billing_cycle': order.billing_cycle,
+ 'subscription_type': order.subscription_type,
+ 'original_price': float(order.original_price),
+ 'discount_amount': float(order.discount_amount),
+ 'final_amount': float(order.final_amount),
+ 'promo_code': order.promo_code,
+ 'qr_code_url': order.qr_code_url,
+ 'status': order.status,
+ 'expired_at': order.expired_at.isoformat() if order.expired_at else None,
+ 'created_at': order.created_at.isoformat() if order.created_at else None
+ },
+ 'message': '订单创建成功'
+ })
+
+ except Exception as e:
+ db.session.rollback()
+ return jsonify({
+ 'success': False,
+ 'error': f'创建订单失败: {str(e)}'
+ }), 500
+
+
+@app.route('/api/v2/payment/order//status', methods=['GET'])
+def check_order_status_v2(order_id):
+ """
+ 查询订单支付状态(新版)
+
+ Response:
+ {
+ "success": true,
+ "payment_success": true, // 是否支付成功
+ "data": {
+ "order_no": "...",
+ "status": "paid",
+ ...
+ },
+ "message": "支付成功!订阅已激活"
+ }
+ """
+ try:
+ if 'user_id' not in session:
+ return jsonify({'success': False, 'error': '未登录'}), 401
+
+ from models import PaymentOrder
+
+ order = PaymentOrder.query.filter_by(
+ id=order_id,
+ user_id=session['user_id']
+ ).first()
+
+ if not order:
+ return jsonify({'success': False, 'error': '订单不存在'}), 404
+
+ # 如果订单已经是已支付状态
+ if order.status == 'paid':
+ return jsonify({
+ 'success': True,
+ 'payment_success': True,
+ 'data': {
+ 'order_no': order.order_no,
+ 'status': order.status,
+ 'final_amount': float(order.final_amount)
+ },
+ 'message': '订单已支付'
+ })
+
+ # 如果订单过期
+ if order.is_expired():
+ order.status = 'expired'
+ db.session.commit()
+ return jsonify({
+ 'success': True,
+ 'payment_success': False,
+ 'data': {'status': 'expired'},
+ 'message': '订单已过期'
+ })
+
+ # 调用微信支付API查询状态
+ try:
+ from wechat_pay import create_wechat_pay_instance
+ wechat_pay = create_wechat_pay_instance()
+
+ query_result = wechat_pay.query_order(order_no=order.order_no)
+
+ if query_result['success']:
+ trade_state = query_result.get('trade_state')
+ transaction_id = query_result.get('transaction_id')
+
+ if trade_state == 'SUCCESS':
+ # 支付成功
+ order.mark_as_paid(transaction_id)
+ db.session.commit()
+
+ # 激活订阅
+ activate_result = activate_subscription_after_payment(
+ order.id,
+ db_session=db.session
+ )
+
+ if activate_result.get('success'):
+ return jsonify({
+ 'success': True,
+ 'payment_success': True,
+ 'data': {
+ 'order_no': order.order_no,
+ 'status': 'paid'
+ },
+ 'message': '支付成功!订阅已激活'
+ })
+ else:
+ return jsonify({
+ 'success': True,
+ 'payment_success': True,
+ 'data': {'status': 'paid'},
+ 'message': '支付成功,但激活失败,请联系客服'
+ })
+
+ elif trade_state in ['NOTPAY', 'USERPAYING']:
+ return jsonify({
+ 'success': True,
+ 'payment_success': False,
+ 'data': {'status': 'pending'},
+ 'message': '等待支付...'
+ })
+ else:
+ order.status = 'cancelled'
+ db.session.commit()
+ return jsonify({
+ 'success': True,
+ 'payment_success': False,
+ 'data': {'status': 'cancelled'},
+ 'message': '支付已取消'
+ })
+
+ except Exception as e:
+ # 查询失败,返回当前状态
+ pass
+
+ return jsonify({
+ 'success': True,
+ 'payment_success': False,
+ 'data': {'status': order.status},
+ 'message': '无法查询支付状态,请稍后重试'
+ })
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'error': f'查询失败: {str(e)}'
+ }), 500
+
+
+@app.route('/api/v2/payment/order//force-update', methods=['POST'])
+def force_update_status_v2(order_id):
+ """
+ 强制更新订单支付状态(新版)
+
+ 用于支付完成但页面未更新的情况
+ """
+ try:
+ if 'user_id' not in session:
+ return jsonify({'success': False, 'error': '未登录'}), 401
+
+ from models import PaymentOrder
+
+ order = PaymentOrder.query.filter_by(
+ id=order_id,
+ user_id=session['user_id']
+ ).first()
+
+ if not order:
+ return jsonify({'success': False, 'error': '订单不存在'}), 404
+
+ # 检查微信支付状态
+ try:
+ from wechat_pay import create_wechat_pay_instance
+ wechat_pay = create_wechat_pay_instance()
+
+ query_result = wechat_pay.query_order(order_no=order.order_no)
+
+ if query_result['success'] and query_result.get('trade_state') == 'SUCCESS':
+ transaction_id = query_result.get('transaction_id')
+
+ # 标记订单为已支付
+ order.mark_as_paid(transaction_id)
+ db.session.commit()
+
+ # 激活订阅
+ activate_result = activate_subscription_after_payment(
+ order.id,
+ db_session=db.session
+ )
+
+ if activate_result.get('success'):
+ return jsonify({
+ 'success': True,
+ 'payment_success': True,
+ 'message': '状态更新成功!订阅已激活'
+ })
+ else:
+ return jsonify({
+ 'success': True,
+ 'payment_success': True,
+ 'message': '支付成功,但激活失败,请联系客服',
+ 'error': activate_result.get('error')
+ })
+ else:
+ return jsonify({
+ 'success': True,
+ 'payment_success': False,
+ 'message': '微信支付状态未更新,请继续等待'
+ })
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'error': f'查询微信支付状态失败: {str(e)}'
+ }), 500
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'error': f'强制更新失败: {str(e)}'
+ }), 500
+
+
+@app.route('/api/v2/subscription/current', methods=['GET'])
+def get_current_subscription_v2():
+ """
+ 获取当前用户订阅信息(新版)
+
+ Response:
+ {
+ "success": true,
+ "data": {
+ "subscription_id": "SUB_1732012345_12345",
+ "plan_code": "pro",
+ "plan_name": "Pro 专业版",
+ "billing_cycle": "yearly",
+ "status": "active",
+ "start_date": "2025-11-19T00:00:00",
+ "end_date": "2026-11-19T00:00:00",
+ "days_left": 365,
+ "auto_renew": false
+ }
+ }
+ """
+ try:
+ if 'user_id' not in session:
+ return jsonify({'success': False, 'error': '未登录'}), 401
+
+ from models import SubscriptionPlan
+
+ subscription = get_current_subscription(session['user_id'])
+
+ if not subscription:
+ return jsonify({
+ 'success': True,
+ 'data': {
+ 'plan_code': 'free',
+ 'plan_name': '免费版',
+ 'status': 'active'
+ }
+ })
+
+ # 获取套餐名称
+ plan = SubscriptionPlan.query.filter_by(plan_code=subscription.plan_code).first()
+ plan_name = plan.plan_name if plan else subscription.plan_code.upper()
+
+ # 计算剩余天数
+ from datetime import datetime
+ now = datetime.now()
+ days_left = (subscription.end_date - now).days if subscription.end_date > now else 0
+
+ return jsonify({
+ 'success': True,
+ 'data': {
+ 'subscription_id': subscription.subscription_id,
+ 'plan_code': subscription.plan_code,
+ 'plan_name': plan_name,
+ 'billing_cycle': subscription.billing_cycle,
+ 'status': subscription.status,
+ 'start_date': subscription.start_date.isoformat() if subscription.start_date else None,
+ 'end_date': subscription.end_date.isoformat() if subscription.end_date else None,
+ 'days_left': days_left,
+ 'auto_renew': subscription.auto_renew
+ }
+ })
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'error': f'获取订阅信息失败: {str(e)}'
+ }), 500
+
+
+@app.route('/api/v2/subscription/history', methods=['GET'])
+def get_subscription_history_v2():
+ """
+ 获取用户订阅历史(新版)
+
+ Query Params:
+ limit: 返回记录数量(默认10)
+
+ Response:
+ {
+ "success": true,
+ "data": [
+ {
+ "subscription_id": "SUB_...",
+ "plan_code": "pro",
+ "billing_cycle": "yearly",
+ "start_date": "...",
+ "end_date": "...",
+ "paid_amount": 2699.00,
+ "status": "expired"
+ },
+ ...
+ ]
+ }
+ """
+ try:
+ if 'user_id' not in session:
+ return jsonify({'success': False, 'error': '未登录'}), 401
+
+ limit = request.args.get('limit', 10, type=int)
+
+ subscriptions = get_user_subscription_history(session['user_id'], limit)
+
+ data = []
+ for sub in subscriptions:
+ data.append({
+ 'subscription_id': sub.subscription_id,
+ 'plan_code': sub.plan_code,
+ 'billing_cycle': sub.billing_cycle,
+ 'start_date': sub.start_date.isoformat() if sub.start_date else None,
+ 'end_date': sub.end_date.isoformat() if sub.end_date else None,
+ 'paid_amount': float(sub.paid_amount),
+ 'original_price': float(sub.original_price),
+ 'discount_amount': float(sub.discount_amount),
+ 'status': sub.status,
+ 'created_at': sub.created_at.isoformat() if sub.created_at else None
+ })
+
+ return jsonify({
+ 'success': True,
+ 'data': data
+ })
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'error': f'获取订阅历史失败: {str(e)}'
+ }), 500
+
+
+@app.route('/api/v2/subscription/button-text', methods=['POST'])
+def get_button_text_v2():
+ """
+ 获取订阅按钮文字(新版)
+
+ Request Body:
+ {
+ "plan_code": "pro",
+ "billing_cycle": "yearly"
+ }
+
+ Response:
+ {
+ "success": true,
+ "button_text": "续费 Pro 专业版"
+ }
+ """
+ try:
+ if 'user_id' not in session:
+ return jsonify({
+ 'success': True,
+ 'button_text': '选择套餐'
+ })
+
+ data = request.get_json()
+ plan_code = data.get('plan_code')
+ billing_cycle = data.get('billing_cycle')
+
+ if not plan_code or not billing_cycle:
+ return jsonify({
+ 'success': False,
+ 'error': '参数不完整'
+ }), 400
+
+ button_text = get_subscription_button_text(
+ session['user_id'],
+ plan_code,
+ billing_cycle
+ )
+
+ return jsonify({
+ 'success': True,
+ 'button_text': button_text
+ })
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'error': f'获取按钮文字失败: {str(e)}'
+ }), 500
diff --git a/src/components/Button2/index.tsx b/src/components/Button2/index.tsx
new file mode 100644
index 00000000..22f74066
--- /dev/null
+++ b/src/components/Button2/index.tsx
@@ -0,0 +1,53 @@
+import React from "react";
+import Link, { LinkProps } from "next/link";
+
+type CommonProps = {
+ className?: string;
+ children?: React.ReactNode;
+ isPrimary?: boolean;
+ isSecondary?: boolean;
+};
+
+type ButtonAsButton = {
+ as?: "button";
+} & React.ButtonHTMLAttributes;
+
+type ButtonAsAnchor = {
+ as: "a";
+} & React.AnchorHTMLAttributes;
+
+type ButtonAsLink = {
+ as: "link";
+} & LinkProps;
+
+type ButtonProps = CommonProps &
+ (ButtonAsButton | ButtonAsAnchor | ButtonAsLink);
+
+const Button: React.FC = ({
+ className,
+ children,
+ isPrimary,
+ isSecondary,
+ as = "button",
+ ...props
+}) => {
+ const isLink = as === "link";
+ const Component: React.ElementType = isLink ? Link : as;
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default Button;
diff --git a/src/components/Subscription/SubscriptionContentNew.tsx b/src/components/Subscription/SubscriptionContentNew.tsx
new file mode 100644
index 00000000..f733a517
--- /dev/null
+++ b/src/components/Subscription/SubscriptionContentNew.tsx
@@ -0,0 +1,1311 @@
+import React, { useState, useEffect } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import {
+ Box,
+ Button,
+ Flex,
+ Text,
+ Badge,
+ VStack,
+ HStack,
+ useToast,
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalCloseButton,
+ useDisclosure,
+ Image,
+ Progress,
+ Divider,
+ Input,
+ Icon,
+ Container,
+} from '@chakra-ui/react';
+import {
+ FaWeixin,
+ FaGem,
+ FaCheck,
+ FaQrcode,
+ FaClock,
+ FaRedo,
+ FaCrown,
+ FaStar,
+ FaTimes,
+ FaChevronDown,
+ FaChevronUp,
+ FaSparkles,
+} from 'react-icons/fa';
+
+import { logger } from '../../utils/logger';
+import { useAuth } from '../../contexts/AuthContext';
+import { useSubscriptionEvents } from '../../hooks/useSubscriptionEvents';
+import { subscriptionConfig, themeColors } from '../../views/Pages/Account/subscription-content';
+
+export default function SubscriptionContentNew() {
+ const { user } = useAuth();
+ const subscriptionEvents = useSubscriptionEvents({
+ currentSubscription: {
+ plan: user?.subscription_plan || 'free',
+ status: user?.subscription_status || 'none',
+ },
+ });
+
+ const toast = useToast();
+ const { isOpen: isPaymentModalOpen, onOpen: onPaymentModalOpen, onClose: onPaymentModalClose } = useDisclosure();
+
+ // State
+ const [selectedPlan, setSelectedPlan] = useState(null);
+ const [selectedCycle, setSelectedCycle] = useState('yearly'); // 默认年付
+ const [paymentOrder, setPaymentOrder] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [paymentCountdown, setPaymentCountdown] = useState(0);
+ const [checkingPayment, setCheckingPayment] = useState(false);
+ const [autoCheckInterval, setAutoCheckInterval] = useState(null);
+ const [forceUpdating, setForceUpdating] = useState(false);
+ const [openFaqIndex, setOpenFaqIndex] = useState(null);
+ const [promoCode, setPromoCode] = useState('');
+ const [promoCodeApplied, setPromoCodeApplied] = useState(false);
+ const [promoCodeError, setPromoCodeError] = useState('');
+ const [validatingPromo, setValidatingPromo] = useState(false);
+ const [priceInfo, setPriceInfo] = useState(null);
+
+ // 倒计时更新
+ useEffect(() => {
+ let timer;
+ if (paymentCountdown > 0) {
+ timer = setInterval(() => {
+ setPaymentCountdown((prev) => {
+ if (prev <= 1) {
+ handlePaymentExpired();
+ return 0;
+ }
+ return prev - 1;
+ });
+ }, 1000);
+ }
+ return () => clearInterval(timer);
+ }, [paymentCountdown]);
+
+ // 组件卸载时清理定时器
+ useEffect(() => {
+ return () => {
+ stopAutoPaymentCheck();
+ };
+ }, []);
+
+ const handlePaymentExpired = () => {
+ setPaymentOrder(null);
+ setPaymentCountdown(0);
+ stopAutoPaymentCheck();
+ toast({
+ title: '支付二维码已过期',
+ description: '请重新创建订单',
+ status: 'warning',
+ duration: 3000,
+ isClosable: true,
+ });
+ };
+
+ const stopAutoPaymentCheck = () => {
+ if (autoCheckInterval) {
+ clearInterval(autoCheckInterval);
+ setAutoCheckInterval(null);
+ }
+ };
+
+ const formatTime = (seconds) => {
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = seconds % 60;
+ return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
+ };
+
+ // 计算价格
+ const calculatePrice = async (plan, cycle, promoCodeValue = null) => {
+ try {
+ const validPromoCode = promoCodeValue && typeof promoCodeValue === 'string' && promoCodeValue.trim()
+ ? promoCodeValue.trim()
+ : null;
+
+ 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: validPromoCode,
+ }),
+ });
+
+ 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 () => {
+ const trimmedCode = promoCode.trim();
+ if (!trimmedCode) {
+ setPromoCodeError('请输入优惠码');
+ return;
+ }
+ if (!selectedPlan) {
+ setPromoCodeError('请先选择套餐');
+ return;
+ }
+
+ setValidatingPromo(true);
+ setPromoCodeError('');
+
+ try {
+ const result = await calculatePrice(selectedPlan, selectedCycle, trimmedCode);
+ 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: '请先登录',
+ status: 'warning',
+ duration: 3000,
+ isClosable: true,
+ });
+ return;
+ }
+
+ subscriptionEvents.trackPricingPlanSelected(
+ plan.name,
+ selectedCycle,
+ getCurrentPrice(plan)
+ );
+
+ setSelectedPlan(plan);
+ await calculatePrice(plan, selectedCycle, promoCodeApplied ? promoCode : null);
+ onPaymentModalOpen();
+ };
+
+ const handleCreateOrder = async () => {
+ if (!selectedPlan) return;
+
+ setLoading(true);
+ try {
+ const price = priceInfo?.final_amount || getCurrentPrice(selectedPlan);
+
+ subscriptionEvents.trackPaymentInitiated({
+ planName: selectedPlan.name,
+ paymentMethod: 'wechat_pay',
+ amount: price,
+ billingCycle: selectedCycle,
+ orderId: null,
+ });
+
+ const response = await fetch('/api/payment/create-order', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include',
+ body: JSON.stringify({
+ plan_name: selectedPlan.name,
+ billing_cycle: selectedCycle,
+ promo_code: promoCodeApplied ? promoCode : null,
+ }),
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data.success) {
+ setPaymentOrder(data.data);
+ setPaymentCountdown(30 * 60);
+ startAutoPaymentCheck(data.data.id);
+
+ toast({
+ title: '订单创建成功',
+ description: '请使用微信扫描二维码完成支付',
+ status: 'success',
+ duration: 3000,
+ isClosable: true,
+ });
+ } else {
+ throw new Error(data.message || '创建订单失败');
+ }
+ } else {
+ throw new Error('网络错误');
+ }
+ } catch (error) {
+ subscriptionEvents.trackPaymentFailed(
+ {
+ planName: selectedPlan.name,
+ paymentMethod: 'wechat_pay',
+ amount: getCurrentPrice(selectedPlan),
+ },
+ error.message
+ );
+
+ toast({
+ title: '创建订单失败',
+ description: error.message,
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const startAutoPaymentCheck = (orderId) => {
+ const checkInterval = setInterval(async () => {
+ try {
+ const response = await fetch(`/api/payment/order/${orderId}/status`, {
+ credentials: 'include',
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data.success && data.payment_success) {
+ clearInterval(checkInterval);
+ setAutoCheckInterval(null);
+
+ subscriptionEvents.trackPaymentSuccessful({
+ planName: selectedPlan?.name,
+ paymentMethod: 'wechat_pay',
+ amount: paymentOrder?.amount,
+ billingCycle: selectedCycle,
+ orderId: orderId,
+ transactionId: data.transaction_id,
+ });
+
+ toast({
+ title: '支付成功!',
+ description: '订阅已激活,正在跳转...',
+ status: 'success',
+ duration: 3000,
+ isClosable: true,
+ });
+
+ setTimeout(() => {
+ onPaymentModalClose();
+ window.location.reload();
+ }, 2000);
+ }
+ }
+ } catch (error) {
+ logger.error('SubscriptionContent', 'startAutoPaymentCheck', error);
+ }
+ }, 10000);
+
+ setAutoCheckInterval(checkInterval);
+ };
+
+ const handleCheckPaymentStatus = async () => {
+ if (!paymentOrder) return;
+
+ setCheckingPayment(true);
+ try {
+ const response = await fetch(`/api/payment/order/${paymentOrder.id}/status`, {
+ credentials: 'include',
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data.success) {
+ if (data.payment_success) {
+ stopAutoPaymentCheck();
+ toast({
+ title: '支付成功!',
+ description: '订阅已激活,正在跳转...',
+ status: 'success',
+ duration: 3000,
+ isClosable: true,
+ });
+
+ setTimeout(() => {
+ onPaymentModalClose();
+ window.location.reload();
+ }, 2000);
+ } else {
+ toast({
+ title: '支付状态检查',
+ description: data.message || '还未检测到支付,请继续等待',
+ status: 'info',
+ duration: 5000,
+ isClosable: true,
+ });
+ }
+ } else {
+ throw new Error(data.error || '查询失败');
+ }
+ } else {
+ throw new Error('网络错误');
+ }
+ } catch (error) {
+ toast({
+ title: '查询失败',
+ description: error.message,
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ } finally {
+ setCheckingPayment(false);
+ }
+ };
+
+ const handleForceUpdatePayment = async () => {
+ if (!paymentOrder) return;
+
+ setForceUpdating(true);
+ try {
+ const response = await fetch(`/api/payment/order/${paymentOrder.id}/force-update`, {
+ method: 'POST',
+ credentials: 'include',
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data.success && data.payment_success) {
+ stopAutoPaymentCheck();
+
+ toast({
+ title: '状态更新成功!',
+ description: '订阅已激活,正在刷新页面...',
+ status: 'success',
+ duration: 3000,
+ isClosable: true,
+ });
+
+ setTimeout(() => {
+ onPaymentModalClose();
+ window.location.reload();
+ }, 2000);
+ } else {
+ toast({
+ title: '无法更新状态',
+ description: data.error || '支付状态未更新',
+ status: 'warning',
+ duration: 5000,
+ isClosable: true,
+ });
+ }
+ } else {
+ throw new Error('网络错误');
+ }
+ } catch (error) {
+ toast({
+ title: '强制更新失败',
+ description: error.message,
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ } finally {
+ setForceUpdating(false);
+ }
+ };
+
+ const getCurrentPrice = (plan) => {
+ if (!plan || plan.name === 'free') return 0;
+
+ const option = plan.pricingOptions?.find(
+ (opt) => opt.cycleKey === selectedCycle
+ );
+ return option ? option.price : plan.pricingOptions[0]?.price || 0;
+ };
+
+ const getCurrentPriceOption = (plan) => {
+ if (!plan || plan.name === 'free') return null;
+ return plan.pricingOptions?.find((opt) => opt.cycleKey === selectedCycle);
+ };
+
+ const getIconComponent = (iconName) => {
+ const icons = {
+ star: FaStar,
+ gem: FaGem,
+ crown: FaCrown,
+ };
+ return icons[iconName] || FaStar;
+ };
+
+ return (
+
+
+ {/* 标题区域 */}
+
+
+
+
+
+ PRICING
+
+
+
+
+ 选择适合你的方案
+
+
+
+ 解锁专业级投资分析工具,让数据为你的决策赋能
+
+
+
+
+ {/* 当前订阅状态 */}
+ {user && (
+
+
+
+
+
+ 当前订阅:
+
+
+ {user.subscription_type === 'free'
+ ? '基础版'
+ : user.subscription_type === 'pro'
+ ? 'Pro 专业版'
+ : 'Max 旗舰版'}
+
+
+ {user.subscription_status === 'active' ? '已激活' : '未激活'}
+
+
+
+ {user.subscription_end_date && (
+
+ 到期时间: {new Date(user.subscription_end_date).toLocaleDateString('zh-CN')}
+
+ )}
+
+
+
+ )}
+
+ {/* 计费周期选择 */}
+
+
+
+ 选择计费周期 · 时长越长优惠越大
+
+
+
+ {subscriptionConfig.plans[1]?.pricingOptions?.map((option, index) => (
+
+ {option.discountPercent > 0 && (
+
+ 省{option.discountPercent}%
+
+ )}
+
+
+
+ ))}
+
+
+ {(() => {
+ const currentOption = subscriptionConfig.plans[1]?.pricingOptions?.find(
+ (opt) => opt.cycleKey === selectedCycle
+ );
+ if (currentOption && currentOption.discountPercent > 0) {
+ return (
+
+
+
+ 当前选择可节省 {currentOption.discountPercent}% 的费用
+
+
+ );
+ }
+ return null;
+ })()}
+
+
+
+ {/* 套餐卡片 */}
+
+ {subscriptionConfig.plans.map((plan, index) => {
+ const IconComponent = getIconComponent(plan.icon);
+ const currentPriceOption = getCurrentPriceOption(plan);
+ const isCurrentPlan =
+ user?.subscription_type === plan.name &&
+ user?.subscription_status === 'active' &&
+ (plan.name === 'free' || user?.billing_cycle === selectedCycle);
+
+ return (
+
+
+ {/* 推荐标签 */}
+ {plan.badge && (
+
+
+ {plan.badge}
+
+
+ )}
+
+
+ {/* 套餐头部 */}
+
+
+
+
+ {plan.displayName}
+
+
+
+ {plan.description}
+
+
+
+ {/* 价格展示 */}
+ {plan.name === 'free' ? (
+
+
+ 免费
+
+
+ ) : (
+
+
+
+ ¥
+
+
+ {getCurrentPrice(plan)}
+
+
+ /{currentPriceOption?.label || '月'}
+
+
+
+ {currentPriceOption?.originalPrice && (
+
+
+ 原价 ¥{currentPriceOption.originalPrice}
+
+
+ 立省 ¥{currentPriceOption.originalPrice - currentPriceOption.price}
+
+
+ )}
+
+ )}
+
+
+
+ {/* 功能列表 */}
+
+ {plan.features.map((feature, idx) => (
+
+
+
+ {feature.name}
+ {feature.limit && (
+
+ ({feature.limit})
+
+ )}
+
+
+ ))}
+
+
+ {/* 订阅按钮 */}
+
+
+
+
+ );
+ })}
+
+
+ {/* FAQ 区域 */}
+
+
+
+ 常见问题
+
+
+
+ {subscriptionConfig.faqs.map((faq, index) => (
+
+ setOpenFaqIndex(openFaqIndex === index ? null : index)}
+ justify="space-between"
+ align="center"
+ >
+
+ {faq.question}
+
+
+
+
+
+ {openFaqIndex === index && (
+
+
+ {faq.answer.split('\n').map((line, idx) => (
+
+ {line}
+
+ ))}
+
+
+ )}
+
+
+ ))}
+
+
+
+
+
+ {/* 支付模态框 */}
+ {
+ stopAutoPaymentCheck();
+ setPaymentOrder(null);
+ setPaymentCountdown(0);
+ setPromoCode('');
+ setPromoCodeApplied(false);
+ setPromoCodeError('');
+ setPriceInfo(null);
+ onPaymentModalClose();
+ }}
+ size="lg"
+ closeOnOverlayClick={false}
+ >
+
+
+
+
+
+ 微信支付
+
+
+
+
+ {!paymentOrder ? (
+ /* 订单确认 */
+
+ {selectedPlan && (
+
+
+ 订单确认
+
+
+
+ 套餐:
+
+ {selectedPlan.displayName}
+
+
+
+ 计费周期:
+
+ {getCurrentPriceOption(selectedPlan)?.label || '月付'}
+
+
+
+
+
+ {priceInfo && priceInfo.is_upgrade && (
+
+
+
+
+ {priceInfo.upgrade_type === 'plan_upgrade'
+ ? '套餐升级'
+ : priceInfo.upgrade_type === 'cycle_change'
+ ? '周期变更'
+ : '套餐和周期调整'}
+
+
+
+
+ 剩余价值抵扣:
+ -¥{priceInfo.remaining_value.toFixed(2)}
+
+
+
+ )}
+
+
+ 套餐价格:
+
+ ¥{priceInfo ? priceInfo.new_plan_price.toFixed(2) : getCurrentPrice(selectedPlan).toFixed(2)}
+
+
+
+ {priceInfo && priceInfo.discount_amount > 0 && (
+
+ 优惠码折扣:
+ -¥{priceInfo.discount_amount.toFixed(2)}
+
+ )}
+
+
+
+
+
+ 实付金额:
+
+
+ ¥{priceInfo ? priceInfo.final_amount.toFixed(2) : getCurrentPrice(selectedPlan).toFixed(2)}
+
+
+
+
+ )}
+
+ {/* 优惠码输入 */}
+ {selectedPlan && (
+
+
+ {
+ setPromoCode(e.target.value.toUpperCase());
+ setPromoCodeError('');
+ }}
+ size="md"
+ isDisabled={promoCodeApplied}
+ bg="rgba(255, 255, 255, 0.05)"
+ border="1px solid"
+ borderColor={themeColors.border.default}
+ color={themeColors.text.primary}
+ _focus={{
+ borderColor: themeColors.border.gold,
+ }}
+ />
+
+
+ {promoCodeError && (
+
+ {promoCodeError}
+
+ )}
+ {promoCodeApplied && priceInfo && (
+
+
+
+ 优惠码已应用!节省 ¥{priceInfo.discount_amount.toFixed(2)}
+
+
+
+ )}
+
+ )}
+
+ }
+ onClick={handleCreateOrder}
+ isLoading={loading}
+ loadingText="创建订单中..."
+ isDisabled={!selectedPlan}
+ fontWeight="bold"
+ _hover={{
+ transform: 'scale(1.02)',
+ shadow: '0 0 30px rgba(212, 175, 55, 0.4)',
+ }}
+ transition="all 0.3s"
+ >
+ 创建微信支付订单
+
+
+ ) : (
+ /* 支付二维码 */
+
+
+ 请使用微信扫码支付
+
+
+ {/* 倒计时 */}
+
+
+
+
+ 二维码有效时间: {formatTime(paymentCountdown)}
+
+
+
+
+
+ {/* 二维码 */}
+
+ {paymentOrder.qr_code_url ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ {/* 订单信息 */}
+
+
+ 订单号: {paymentOrder.order_no}
+
+
+ 支付金额:
+
+ ¥{paymentOrder.amount}
+
+
+
+
+ {/* 操作按钮 */}
+
+ }
+ onClick={handleCheckPaymentStatus}
+ isLoading={checkingPayment}
+ loadingText="检查中..."
+ _hover={{
+ bg: 'rgba(212, 175, 55, 0.1)',
+ }}
+ >
+ 检查支付状态
+
+
+
+
+
+ 支付完成但页面未更新?点击上方"强制更新"按钮
+
+
+
+ {/* 支付说明 */}
+
+ • 使用微信"扫一扫"功能扫描上方二维码
+ • 支付完成后系统将自动检测并激活订阅
+ • 系统每10秒自动检查一次支付状态
+ • 如遇问题请联系客服支持
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/views/Pages/Account/subscription-content.tsx b/src/views/Pages/Account/subscription-content.tsx
new file mode 100644
index 00000000..0f843710
--- /dev/null
+++ b/src/views/Pages/Account/subscription-content.tsx
@@ -0,0 +1,221 @@
+export const subscriptionConfig = {
+ plans: [
+ {
+ name: 'free',
+ displayName: '基础版',
+ description: '免费体验核心功能,7项实用工具',
+ icon: 'star',
+ price: 0,
+ badge: '免费',
+ badgeColor: 'gray',
+ cardBorder: 'gray',
+ features: [
+ { name: '新闻信息流', enabled: true },
+ { name: '历史事件对比', enabled: true, limit: 'TOP3' },
+ { name: '事件传导链分析(AI)', enabled: true, limit: '有限体验' },
+ { name: 'AI复盘功能', enabled: true },
+ { name: '企业概览', enabled: true, limit: '限制预览' },
+ { name: '个股深度分析(AI)', enabled: true, limit: '10家/月' },
+ { name: '概念中心(548大概念)', enabled: true, limit: 'TOP5' },
+ { name: '涨停板块数据分析', enabled: true },
+ { name: '个股涨停分析', enabled: true },
+ { name: '事件-相关标的分析', enabled: false },
+ { name: '相关概念展示', enabled: false },
+ { name: '高效数据筛选工具', enabled: false },
+ ],
+ },
+ {
+ name: 'pro',
+ displayName: 'Pro 专业版',
+ description: '为专业投资者打造,解锁高级分析功能',
+ icon: 'gem',
+ badge: '推荐',
+ badgeColor: 'gold',
+ cardBorder: 'gold',
+ highlight: false,
+ pricingOptions: [
+ {
+ cycleKey: 'monthly',
+ label: '月付',
+ months: 1,
+ price: 299,
+ originalPrice: null,
+ discountPercent: 0,
+ },
+ {
+ cycleKey: '3months',
+ label: '季付',
+ months: 3,
+ price: 799,
+ originalPrice: 897,
+ discountPercent: 11,
+ },
+ {
+ cycleKey: '6months',
+ label: '半年付',
+ months: 6,
+ price: 1499,
+ originalPrice: 1794,
+ discountPercent: 16,
+ },
+ {
+ cycleKey: 'yearly',
+ label: '年付',
+ months: 12,
+ price: 2699,
+ originalPrice: 3588,
+ discountPercent: 25,
+ },
+ ],
+ features: [
+ { name: '新闻信息流', enabled: true },
+ { name: '历史事件对比', enabled: true },
+ { name: '事件传导链分析(AI)', enabled: true },
+ { name: '事件-相关标的分析', enabled: true },
+ { name: '相关概念展示', enabled: true },
+ { name: 'AI复盘功能', enabled: true },
+ { name: '企业概览', enabled: true },
+ { name: '个股深度分析(AI)', enabled: true, limit: '50家/月' },
+ { name: '高效数据筛选工具', enabled: true },
+ { name: '概念中心(548大概念)', enabled: true },
+ { name: '历史时间轴查询', enabled: true, limit: '100天' },
+ { name: '涨停板块数据分析', enabled: true },
+ { name: '个股涨停分析', enabled: true },
+ { name: '板块深度分析(AI)', enabled: false },
+ { name: '概念高频更新', enabled: false },
+ ],
+ },
+ {
+ name: 'max',
+ displayName: 'Max 旗舰版',
+ description: '旗舰级体验,无限制使用所有功能',
+ icon: 'crown',
+ badge: '最受欢迎',
+ badgeColor: 'gold',
+ cardBorder: 'gold',
+ highlight: true,
+ pricingOptions: [
+ {
+ cycleKey: 'monthly',
+ label: '月付',
+ months: 1,
+ price: 599,
+ originalPrice: null,
+ discountPercent: 0,
+ },
+ {
+ cycleKey: '3months',
+ label: '季付',
+ months: 3,
+ price: 1599,
+ originalPrice: 1797,
+ discountPercent: 11,
+ },
+ {
+ cycleKey: '6months',
+ label: '半年付',
+ months: 6,
+ price: 2999,
+ originalPrice: 3594,
+ discountPercent: 17,
+ },
+ {
+ cycleKey: 'yearly',
+ label: '年付',
+ months: 12,
+ price: 5399,
+ originalPrice: 7188,
+ discountPercent: 25,
+ },
+ ],
+ features: [
+ { name: '新闻信息流', enabled: true },
+ { name: '历史事件对比', enabled: true },
+ { name: '事件传导链分析(AI)', enabled: true },
+ { name: '事件-相关标的分析', enabled: true },
+ { name: '相关概念展示', enabled: true },
+ { name: '板块深度分析(AI)', enabled: true },
+ { name: 'AI复盘功能', enabled: true },
+ { name: '企业概览', enabled: true },
+ { name: '个股深度分析(AI)', enabled: true, limit: '无限制' },
+ { name: '高效数据筛选工具', enabled: true },
+ { name: '概念中心(548大概念)', enabled: true },
+ { name: '历史时间轴查询', enabled: true, limit: '无限制' },
+ { name: '概念高频更新', enabled: true },
+ { name: '涨停板块数据分析', enabled: true },
+ { name: '个股涨停分析', enabled: true },
+ ],
+ },
+ ],
+
+ faqs: [
+ {
+ question: '如何取消订阅?',
+ answer: '您可以随时在账户设置中取消订阅。取消后,您的订阅将在当前计费周期结束时到期,期间您仍可继续使用付费功能。取消后不会立即扣款,也不会自动续费。',
+ },
+ {
+ question: '支持哪些支付方式?',
+ answer: '我们目前支持微信支付。扫描支付二维码后,系统会自动检测支付状态并激活您的订阅。支付过程安全可靠,所有交易都经过加密处理。',
+ },
+ {
+ question: '升级或切换套餐时,原套餐的费用怎么办?',
+ answer: '当您升级套餐或切换计费周期时,系统会自动计算您当前订阅的剩余价值并用于抵扣新套餐的费用。\n\n计算方式:\n• 剩余价值 = 原套餐价格 × (剩余天数 / 总天数)\n• 实付金额 = 新套餐价格 - 剩余价值 - 优惠码折扣\n\n例如:您购买了年付Pro版(¥2699),使用了180天后升级到Max版(¥5399/年),剩余价值约¥1350将自动抵扣,实付约¥4049。',
+ },
+ {
+ question: '可以在不同计费周期之间切换吗?',
+ answer: '可以。您可以随时更改计费周期。如果从短期切换到长期,系统会计算剩余价值并应用到新的订阅中。长期套餐(季付、半年付、年付)可享受更大的折扣优惠。',
+ },
+ {
+ question: '是否支持退款?',
+ answer: '为了保障服务质量和维护公平的商业环境,我们不支持退款。\n\n建议您在订阅前:\n• 充分了解各套餐的功能差异\n• 使用免费版体验基础功能\n• 根据实际需求选择合适的计费周期\n• 如有疑问可联系客服咨询\n\n提示:选择长期套餐(如半年付、年付)可享受更大折扣,性价比更高。',
+ },
+ {
+ question: 'Pro版和Max版有什么区别?',
+ answer: 'Pro版适合个人专业用户,提供高级图表、历史数据分析等功能,有一定的使用限制。Max版则是为重度用户设计,提供无限制的数据查询、板块深度分析、概念高频更新等独家功能,并享有优先技术支持。',
+ },
+ ],
+};
+
+// 主题颜色配置 - 黑金配色
+export const themeColors = {
+ // 背景渐变
+ bgGradient: 'linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%)',
+ bgRadialGold: 'radial-gradient(circle at center, rgba(212, 175, 55, 0.1) 0%, transparent 70%)',
+
+ // 主色调
+ primary: {
+ gold: '#D4AF37', // 金色
+ goldLight: '#F4E3A7', // 浅金色
+ goldDark: '#B8941F', // 深金色
+ },
+
+ // 背景色
+ bg: {
+ primary: '#0a0a0a', // 主背景(纯黑)
+ secondary: '#1a1a1a', // 次级背景(深黑)
+ card: '#1e1e1e', // 卡片背景
+ cardHover: '#252525', // 卡片悬停
+ },
+
+ // 文字颜色
+ text: {
+ primary: '#ffffff', // 主文字(纯白)
+ secondary: '#b8b8b8', // 次级文字(灰白)
+ muted: '#808080', // 弱化文字(灰)
+ gold: '#D4AF37', // 金色文字
+ },
+
+ // 边框颜色
+ border: {
+ default: 'rgba(255, 255, 255, 0.1)',
+ gold: 'rgba(212, 175, 55, 0.3)',
+ goldGlow: 'rgba(212, 175, 55, 0.5)',
+ },
+
+ // 状态颜色
+ status: {
+ active: '#00ff88', // 激活(绿色)
+ inactive: '#ff4444', // 未激活(红色)
+ warning: '#ff9900', // 警告(橙色)
+ },
+};
diff --git a/src/views/Pricing/content.tsx b/src/views/Pricing/content.tsx
new file mode 100644
index 00000000..def13d1d
--- /dev/null
+++ b/src/views/Pricing/content.tsx
@@ -0,0 +1,35 @@
+export const pricing = [
+ {
+ title: "STARTER",
+ price: 99,
+ features: [
+ "1 Active Bot",
+ "1,000 Conversations per month",
+ "Web & WhatsApp Integration",
+ "Basic Dashboard & Chat Reports",
+ "Email Support",
+ ],
+ },
+ {
+ title: "PRO",
+ price: 149,
+ features: [
+ "Up to 5 Active Bots",
+ "10,000 Conversations per month",
+ "Multi-Channel (Web, WhatsApp, IG, Telegram)",
+ "Custom Workflows & Automation",
+ "Real-Time Reports & Zapier Integration",
+ ],
+ },
+ {
+ title: "ENTERPRISE",
+ price: 199,
+ features: [
+ "Unlimited Bots & Chats",
+ "Role-Based Access & Team Management",
+ "Integration to CRM & Custom APIs",
+ "Advanced AI Training (LLM/NLP)",
+ "Dedicated Onboarding Team",
+ ],
+ },
+];
diff --git a/src/views/Pricing/index.tsx b/src/views/Pricing/index.tsx
new file mode 100644
index 00000000..9d31258f
--- /dev/null
+++ b/src/views/Pricing/index.tsx
@@ -0,0 +1,129 @@
+import { motion } from "framer-motion";
+import Button from "@/components/Button2";
+
+import { pricing } from "./content";
+
+const Pricing = () => (
+
+
+
+ Pricing
+
+ Start Automation Today
+
+
+
+ {pricing.map((item, index) => (
+
+ {item.title === "PRO" && (
+
+
+
+ )}
+
+ {item.title}
+
+
+
+
+
+ ${item.price}
+
+
/Month
+
+
+
+
+ {item.features.map((feature, index) => (
+
+ ))}
+
+
+
+ ))}
+
+
+ Free 7 Day Trial
+
+
+
+);
+
+export default Pricing;
diff --git a/update_pricing_options.sql b/update_pricing_options.sql
new file mode 100644
index 00000000..04e1cfdb
--- /dev/null
+++ b/update_pricing_options.sql
@@ -0,0 +1,100 @@
+-- ============================================
+-- 更新订阅套餐价格配置
+-- 用途:为 subscription_plans 表添加季付、半年付价格
+-- 日期:2025-11-19
+-- ============================================
+
+-- 更新 Pro 专业版的 pricing_options
+UPDATE subscription_plans
+SET pricing_options = JSON_ARRAY(
+ JSON_OBJECT(
+ 'months', 1,
+ 'price', 299.00,
+ 'label', '月付',
+ 'cycle_key', 'monthly',
+ 'discount_percent', 0
+ ),
+ JSON_OBJECT(
+ 'months', 3,
+ 'price', 799.00,
+ 'label', '季付',
+ 'cycle_key', 'quarterly',
+ 'discount_percent', 11,
+ 'original_price', 897.00
+ ),
+ JSON_OBJECT(
+ 'months', 6,
+ 'price', 1499.00,
+ 'label', '半年付',
+ 'cycle_key', 'semiannual',
+ 'discount_percent', 16,
+ 'original_price', 1794.00
+ ),
+ JSON_OBJECT(
+ 'months', 12,
+ 'price', 2699.00,
+ 'label', '年付',
+ 'cycle_key', 'yearly',
+ 'discount_percent', 25,
+ 'original_price', 3588.00
+ )
+)
+WHERE name = 'pro';
+
+-- 更新 Max 旗舰版的 pricing_options
+UPDATE subscription_plans
+SET pricing_options = JSON_ARRAY(
+ JSON_OBJECT(
+ 'months', 1,
+ 'price', 599.00,
+ 'label', '月付',
+ 'cycle_key', 'monthly',
+ 'discount_percent', 0
+ ),
+ JSON_OBJECT(
+ 'months', 3,
+ 'price', 1599.00,
+ 'label', '季付',
+ 'cycle_key', 'quarterly',
+ 'discount_percent', 11,
+ 'original_price', 1797.00
+ ),
+ JSON_OBJECT(
+ 'months', 6,
+ 'price', 2999.00,
+ 'label', '半年付',
+ 'cycle_key', 'semiannual',
+ 'discount_percent', 17,
+ 'original_price', 3594.00
+ ),
+ JSON_OBJECT(
+ 'months', 12,
+ 'price', 5399.00,
+ 'label', '年付',
+ 'cycle_key', 'yearly',
+ 'discount_percent', 25,
+ 'original_price', 7188.00
+ )
+)
+WHERE name = 'max';
+
+-- 验证更新结果
+SELECT
+ name AS '套餐',
+ display_name AS '显示名称',
+ pricing_options AS '价格配置'
+FROM subscription_plans
+WHERE name IN ('pro', 'max');
+
+-- 完成提示
+SELECT '价格配置已更新!' AS '状态';
+SELECT '新价格:' AS '';
+SELECT ' Pro 月付: ¥299' AS '';
+SELECT ' Pro 季付: ¥799 (省11%)' AS '';
+SELECT ' Pro 半年付: ¥1499 (省16%)' AS '';
+SELECT ' Pro 年付: ¥2699 (省25%)' AS '';
+SELECT '' AS '';
+SELECT ' Max 月付: ¥599' AS '';
+SELECT ' Max 季付: ¥1599 (省11%)' AS '';
+SELECT ' Max 半年付: ¥2999 (省17%)' AS '';
+SELECT ' Max 年付: ¥5399 (省25%)' AS '';