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 +
+
+
+
+ + + + 金融AI技术领航者 +
+
智能舆情分析系统
+
+
+ + + + 深度数据挖掘 +
+
+ + + + 7×24小时监控 +
+
+
基于金融领域微调的大语言模型,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 +
+
+
+
订阅方案
+
立即开启智能决策
+
+
+
+
PRO
+
+
+
+
¥198
+
/月
+
+ 选择Pro版 +
+
+
+
+ + + +
+
事件关联股票深度分析
+
+
+
+ + + +
+
历史事件智能对比复盘
+
+
+
+ + + +
+
事件概念关联与挖掘
+
+
+
+ + + +
+
概念板块个股追踪
+
+
+
+ + + +
+
概念深度研报与解读
+
+
+
+ + + +
+
个股异动实时预警
+
+
+
+
+
+
+ video(class="w-full" src=require('Videos/video-1.webm') autoplay loop muted playsinline) +
+
MAX
+
+
+
+
¥998
+
/月
+
+ 选择Max版 +
+
+
+
+ + + +
+
包含Pro版全部功能
+
+
+
+ + + +
+
事件传导链路智能分析
+
+
+
+ + + +
+
概念演变时间轴追溯
+
+
+
+ + + +
+
个股全方位深度研究
+
+
+
+ + + +
+
价小前投研助手无限使用
+
+
+
+ + + +
+
新功能优先体验权
+
+
+
+ + + +
+
专属客服一对一服务
+
+
+
+
+
+
+
+ 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)} + + + + )} + + )} + + + + ) : ( + /* 支付二维码 */ + + + 请使用微信扫码支付 + + + {/* 倒计时 */} + + + + + 二维码有效时间: {formatTime(paymentCountdown)} + + + + + + {/* 二维码 */} + + {paymentOrder.qr_code_url ? ( + 微信支付二维码 + ) : ( + + + + )} + + + {/* 订单信息 */} + + + 订单号: {paymentOrder.order_no} + + + 支付金额: + + ¥{paymentOrder.amount} + + + + + {/* 操作按钮 */} + + + + + + + 支付完成但页面未更新?点击上方"强制更新"按钮 + + + + {/* 支付说明 */} + + • 使用微信"扫一扫"功能扫描上方二维码 + • 支付完成后系统将自动检测并激活订阅 + • 系统每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) => ( +
+
+ + + +
+ {feature} +
+ ))} +
+
+
+ ))} +
+
+ 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 '';