Files
vf_react/docs/NEW_PAYMENT_SYSTEM_DESIGN.md
2025-11-19 19:41:26 +08:00

18 KiB
Raw Blame History

订阅支付系统重新设计方案

📊 问题分析

现有系统的问题

  1. 价格配置混乱

    • 季付和月付价格相同(配置错误)
    • monthly_priceyearly_price 字段命名不清晰
    • 缺少季付、半年付等周期的价格配置
  2. 升级逻辑复杂且不合理

    • 计算剩余价值折算(按天计算 remaining_value
    • 用户难以理解升级价格
    • 续费用户和新用户价格不一致
    • 逻辑复杂,容易出错
  3. 按钮文案不清晰

    • 已订阅用户应显示"续费 Pro"/"续费 Max"
    • 而不是"升级至 Pro"/"切换至 Pro"
  4. 数据库表设计问题

    • SubscriptionUpgrade 表记录升级,但逻辑过于复杂
    • PaymentOrder 表缺少必要字段
    • 价格配置分散在多个字段

新设计方案

核心原则

  1. 简化续费逻辑: 续费用户与新用户价格完全一致,不做任何折算
  2. 清晰的价格体系: 每个套餐每个周期都有明确的价格
  3. 统一的用户体验: 无论是新购还是续费,价格透明一致
  4. 独立的订阅记录: 每次支付都创建新的订阅记录(历史可追溯)

📐 数据库表设计

1. subscription_plans - 订阅套餐表(重构)

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='订阅套餐配置表';

示例数据:

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 - 用户订阅记录表(重构)

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 - 支付订单表(重构)

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 - 优惠码表(保持不变,微调)

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 - 优惠码使用记录表(保持不变)

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. 价格计算逻辑(简化版)

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. 创建订单逻辑

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. 支付成功后的订阅激活逻辑

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. 按钮文案逻辑

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