# 订阅支付系统重新设计方案 ## 📊 问题分析 ### 现有系统的问题 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