- 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
11 KiB
11 KiB
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 注意事项
- 安全性:兑换码需要足够的随机性
- 幂等性:兑换操作需要支持幂等
- 原子性:兑换和权益发放需要事务保证
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.0 最后更新:2025-01 分析基于:Sub2API v0.1.104