Files
tokens-reef/deploy/docs-backup/MODULE_08_SUBSCRIPTION.md
Developer 349d783fd1 refactor: clean up project structure
- Remove old review reports (keep latest only)
- Move docs/ to deploy/docs-backup/
- Move performance-testing/ to deploy/
- Clean up test output files
- Organize root directory
2026-04-06 23:36:03 +08:00

11 KiB
Raw Permalink Blame History

Sub2API 模块分析报告:订阅与兑换码模块

1. 模块概述

1.1 模块定位

订阅与兑换码模块是Sub2API系统的权益管理核心负责管理用户的订阅服务、套餐计划以及通过兑换码进行余额充值和权益发放。

1.2 核心职责

  • 订阅管理:管理用户的订阅服务(包月、包年等)
  • 套餐计划:创建和管理订阅套餐
  • 兑换码系统:生成、验证、兑换激活码/充值码
  • 权益发放:自动将权益发放到用户账户

2. 代码结构分析

2.1 核心文件

文件路径 职责 代码行数
service/subscription_service.go 订阅核心服务 ~800行
service/redeem_service.go 兑换码服务 ~500行
handler/subscription_handler.go 订阅API处理器 ~300行
handler/redeem_handler.go 兑换码API处理器 ~200行
repository/user_subscription_repo.go 订阅数据访问层 ~500行
repository/redeem_code_repo.go 兑换码数据访问层 ~400行

2.2 核心数据模型

// 用户订阅 - ent/schema/usersubscription.go
type UserSubscription struct {
    ID              int64
    UserID          int64
    GroupID         int64      // 订阅的分组
    PlanID          string    // 套餐ID
    PlanName        string    // 套餐名称
    Status          string    // active/expired/canceled
    StartDate       time.Time // 开始日期
    EndDate         time.Time // 结束日期
    AutoRenew       bool      // 自动续费
    CreatedAt       time.Time
    UpdatedAt       time.Time
}

// 订阅套餐 - 配置定义
type SubscriptionPlan struct {
    ID          string
    Name        string
    GroupID     int64
    DurationDays int       // 时长(天)
    Price       float64    // 价格
    ModelLimits string     // 模型限制JSON
}

// 兑换码 - ent/schema/redeemcode.go  
type RedeemCode struct {
    ID            int64
    Code          string    // 兑换码
    Type          string    // balance/concurrency/subscription/invitation
    Value         float64  // 金额或并发数
    Status        string    // unused/used/expired
    GroupID       *int64   // 订阅类型专用
    ValidDays     int      // 有效期(天)
    UserID        int64    // 使用者
    UsedAt        *time.Time
    Notes        string
    CreatedAt     time.Time
}

3. 功能详细分析

3.1 订阅管理

3.1.1 创建订阅

// service/subscription_service.go - CreateSubscription
func (s *SubscriptionService) CreateSubscription(ctx context.Context, userID int64, planID string) (*UserSubscription, error) {
    // 1. 获取套餐信息
    plan := s.getPlan(planID)
    if plan == nil {
        return nil, ErrPlanNotFound
    }
    
    // 2. 验证分组权限
    if !s.userCanAccessGroup(ctx, userID, plan.GroupID) {
        return nil, ErrNoAccessToGroup
    }
    
    // 3. 计算订阅时间
    now := time.Now()
    endDate := now.AddDate(0, 0, plan.DurationDays)
    
    // 4. 创建订阅记录
    subscription := &UserSubscription{
        UserID:     userID,
        GroupID:    plan.GroupID,
        PlanID:     planID,
        PlanName:   plan.Name,
        Status:     StatusActive,
        StartDate:  now,
        EndDate:    endDate,
        AutoRenew:  false,
    }
    
    return s.subscriptionRepo.Create(ctx, subscription)
}

3.1.2 订阅验证

// 验证用户是否有有效订阅
func (s *SubscriptionService) ValidateSubscription(ctx context.Context, userID int64, groupID int64) error {
    // 1. 查询用户在该分组的有效订阅
    sub, err := s.subscriptionRepo.GetActiveByUserAndGroup(ctx, userID, groupID)
    if err != nil {
        return err
    }
    
    // 2. 检查是否过期
    if time.Now().After(sub.EndDate) {
        return ErrSubscriptionExpired
    }
    
    return nil
}

3.1.3 自动续期

// 自动检查和续期订阅
func (s *SubscriptionService) ProcessAutoRenew() {
    // 1. 获取即将到期的订阅
    subscriptions := s.subscriptionRepo.GetExpiringSoon(3) // 3天内到期
    
    for _, sub := range subscriptions {
        // 2. 检查自动续费开关
        if !sub.AutoRenew {
            continue
        }
        
        // 3. 尝试扣款
        user, _ := s.userRepo.GetByID(ctx, sub.UserID)
        plan := s.getPlan(sub.PlanID)
        
        if user.Balance >= plan.Price {
            // 4. 扣款并延长
            s.userRepo.DeductBalance(ctx, sub.UserID, plan.Price)
            s.extendSubscription(ctx, sub.ID, plan.DurationDays)
        } else {
            // 5. 余额不足,标记为即将过期
            s.sendRenewalReminder(ctx, sub.UserID, sub.ID)
        }
    }
}

3.2 兑换码系统

3.2.1 生成兑换码

// service/redeem_service.go - GenerateCodes
func (s *RedeemService) GenerateCodes(ctx context.Context, req GenerateCodesRequest) ([]RedeemCode, error) {
    // 1. 验证请求
    if req.Count <= 0 || req.Count > 1000 {
        return nil, ErrInvalidCount
    }
    
    // 2. 生成随机码
    codes := make([]RedeemCode, 0, req.Count)
    for i := 0; i < req.Count; i++ {
        // 格式XXXX-XXXX-XXXX-XXXX
        code := generateCode()
        
        codes = append(codes, RedeemCode{
            Code:       code,
            Type:       req.Type,
            Value:      req.Value,
            Status:     StatusUnused,
            GroupID:    req.GroupID,
            ValidDays:  req.ValidDays,
        })
    }
    
    // 3. 批量保存
    return s.redeemRepo.CreateBatch(ctx, codes)
}

func generateCode() string {
    // 生成16字节随机数转为32位hex分4段
    bytes := make([]byte, 16)
    rand.Read(bytes)
    hex := hex.EncodeToString(bytes)
    
    // 格式XXXX-XXXX-XXXX-XXXX
    return fmt.Sprintf("%s-%s-%s-%s", 
        strings.ToUpper(hex[0:8]),
        strings.ToUpper(hex[8:16]),
        strings.ToUpper(hex[16:24]),
        strings.ToUpper(hex[24:32]))
}

3.2.2 兑换流程

// service/redeem_service.go - Redeem
func (s *RedeemService) Redeem(ctx context.Context, userID int64, code string) (*RedeemCode, error) {
    // 1. 获取分布式锁(防止并发兑换)
    if !s.acquireLock(ctx, code) {
        return nil, ErrCodeLocked
    }
    defer s.releaseLock(ctx, code)
    
    // 2. 查询兑换码
    redeemCode, err := s.redeemRepo.GetByCode(ctx, code)
    if err != nil {
        return nil, ErrCodeNotFound
    }
    
    // 3. 验证状态
    if redeemCode.Status != StatusUnused {
        return nil, ErrCodeAlreadyUsed
    }
    
    // 4. 验证有效期
    if redeemCode.CreatedAt.AddDate(0, 0, redeemCode.ValidDays).Before(time.Now()) {
        return nil, ErrCodeExpired
    }
    
    // 5. 执行兑换逻辑
    switch redeemCode.Type {
    case TypeBalance:
        // 增加余额
        err = s.userRepo.AddBalance(ctx, userID, redeemCode.Value)
        
    case TypeConcurrency:
        // 增加并发
        err = s.userRepo.AddConcurrency(ctx, userID, int(redeemCode.Value))
        
    case TypeSubscription:
        // 开通订阅
        err = s.createSubscription(ctx, userID, redeemCode.GroupID, redeemCode.Value)
    }
    
    if err != nil {
        return nil, err
    }
    
    // 6. 标记为已使用
    err = s.redeemRepo.MarkAsUsed(ctx, redeemCode.ID, userID)
    if err != nil {
        return nil, err
    }
    
    // 7. 返回更新后的记录
    return s.redeemRepo.GetByID(ctx, redeemCode.ID)
}

3.3 兑换码类型

类型 值含义 用途
balance 金额 充值余额
concurrency 数值 增加并发数
subscription 天数 开通订阅
invitation 0 邀请注册

3.4 权益发放

// 订阅权益发放
func (s *SubscriptionService) GrantSubscriptionBenefits(ctx context.Context, userID int64, groupID int64, days int) error {
    // 1. 查找或创建订阅
    sub, err := s.subscriptionRepo.GetActiveByUserAndGroup(ctx, userID, groupID)
    if err != nil {
        return err
    }
    
    if sub != nil {
        // 已有订阅,延长时间
        sub.EndDate = sub.EndDate.AddDate(0, 0, days)
        return s.subscriptionRepo.Update(ctx, sub)
    }
    
    // 2. 创建新订阅
    newSub := &UserSubscription{
        UserID:    userID,
        GroupID:   groupID,
        Status:    StatusActive,
        StartDate: time.Now(),
        EndDate:   time.Now().AddDate(0, 0, days),
    }
    
    return s.subscriptionRepo.Create(ctx, newSub)
}

4. 配置参数

4.1 订阅配置config.yaml

subscription:
  # 套餐配置
  plans:
    - id: "monthly_basic"
      name: "月度基础套餐"
      group_id: 1
      duration_days: 30
      price: 99.00
      model_limits:
        - "claude-3-5-sonnet-20241022"
        
    - id: "yearly_pro"
      name: "年度专业套餐"
      group_id: 1
      duration_days: 365
      price: 990.00
      model_limits:
        - "claude-3-5-sonnet-20241022"
        - "claude-3-opus-5-20251101"
        
  # 自动续费配置
  auto_renew:
    enabled: true
    reminder_days: 3
    retry_attempts: 3

4.2 兑换码配置

redeem_code:
  # 生成配置
  length: 32
  format: "XXXX-XXXX-XXXX-XXXX"
  batch_max: 1000
  
  # 有效期配置
  default_valid_days: 30
  max_valid_days: 365

5. 修改和扩展指南

5.1 常见修改场景

场景1添加新套餐

// 在配置中添加新套餐
const PlanIDEnterprise = "enterprise"

var SubscriptionPlans = map[string]*SubscriptionPlan{
    PlanIDEnterprise: {
        ID:           PlanIDEnterprise,
        Name:         "企业版",
        DurationDays: 365,
        Price:        9999.00,
    },
}

场景2自定义兑换码前缀

// 生成带前缀的兑换码
func generateCodeWithPrefix(prefix string) string {
    bytes := make([]byte, 16)
    rand.Read(bytes)
    hex := hex.EncodeToString(bytes)
    
    return fmt.Sprintf("%s-%s-%s-%s", 
        prefix,
        strings.ToUpper(hex[0:8]),
        strings.ToUpper(hex[8:16]),
        strings.ToUpper(hex[16:24]))
}

5.2 注意事项

  1. 安全性:兑换码需要足够的随机性
  2. 幂等性:兑换操作需要支持幂等
  3. 原子性:兑换和权益发放需要事务保证

6. 测试覆盖

6.1 测试场景

测试文件 场景
subscription_service_test.go 订阅CRUD
redeem_service_test.go 兑换码生成和兑换

7. 监控与运维

7.1 关键指标

指标 说明
subscription_active_count 有效订阅数
subscription_expiring_soon 即将到期订阅数
redeem_code_used_rate 兑换码使用率

7.2 运维任务

任务 频率 说明
订阅到期处理 每天 处理到期订阅
兑换码统计 每周 统计使用情况

8. 总结

订阅与兑换码模块特点:

  • 多种权益类型:支持余额、并发、订阅等多种权益
  • 完善的验证:状态、有效期检查确保安全
  • 分布式锁:防止并发兑换导致的问题

潜在改进点:

  1. 兑换码目前无系统标识,存在跨实例使用风险
  2. 可增加更丰富的套餐类型

修改建议:

  • 如需解决跨实例使用,需在码中嵌入实例标识

文档版本1.0 最后更新2025-01 分析基于Sub2API v0.1.104