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

437 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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*