# 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 核心数据模型 ```go // 用户订阅 - 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 创建订阅 ```go // 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 订阅验证 ```go // 验证用户是否有有效订阅 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 自动续期 ```go // 自动检查和续期订阅 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 生成兑换码 ```go // 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 兑换流程 ```go // 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 权益发放 ```go // 订阅权益发放 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) ```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 兑换码配置 ```yaml redeem_code: # 生成配置 length: 32 format: "XXXX-XXXX-XXXX-XXXX" batch_max: 1000 # 有效期配置 default_valid_days: 30 max_valid_days: 365 ``` ## 5. 修改和扩展指南 ### 5.1 常见修改场景 **场景1:添加新套餐** ```go // 在配置中添加新套餐 const PlanIDEnterprise = "enterprise" var SubscriptionPlans = map[string]*SubscriptionPlan{ PlanIDEnterprise: { ID: PlanIDEnterprise, Name: "企业版", DurationDays: 365, Price: 9999.00, }, } ``` **场景2:自定义兑换码前缀** ```go // 生成带前缀的兑换码 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*