Files
tokens-reef/deploy/docs-backup/MODULE_08_SUBSCRIPTION.md

437 lines
11 KiB
Markdown
Raw Permalink Normal View 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 核心数据模型
```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*