提交前必读(请勿删除本节)
您当前的 newapi 版本
v1.0.0+(基于最新 main 分支代码)
提交确认
功能描述
为订阅套餐增加分组限制功能,允许管理员创建仅适用于特定用户分组的订阅套餐。
核心设计:
SubscriptionPlan 和 UserSubscription 新增 AllowedGroups 字段(逗号分隔字符串,空值 = 适用所有分组)
- 计费时根据请求的
UsingGroup 匹配订阅,只消费匹配的订阅额度
- 完全向后兼容:现有订阅
AllowedGroups 为空,行为不变
应用场景
当前订阅系统的配额是全站共用的,无法区分不同分组。这在以下场景中存在问题:
-
多产品线运营:运营方同时提供多个 AI 服务(如 Codex 代码助手、Claude 对话服务),希望为每个产品线创建独立的订阅套餐,实现配额隔离和差异化定价。
-
企业客户管理:企业客户购买特定模型的订阅(如仅购买 GPT-4 访问权限),不希望额度被用于其他模型分组。
-
成本控制:不同分组的上游成本差异大(如 o1 模型成本远高于 GPT-3.5),需要独立定价和额度管理。
期望行为示例:
- 购买「Codex 套餐」→ 增加 100 刀额度,仅供 codex 分组使用,不能用于 claude 分组
- 购买「Claude 套餐」→ 增加 50 刀额度,仅供 claude 分组使用
- 购买「通用套餐」→ 额度适用于所有分组(向后兼容现有行为)
详细实现方案
1. 数据模型(model/subscription.go)
SubscriptionPlan 新增字段(第 169 行后):
AllowedGroups string `json:"allowed_groups" gorm:"type:text;default:''"`
UserSubscription 新增字段(第 251 行后):
AllowedGroups string `json:"allowed_groups" gorm:"type:text;default:''"`
新增辅助函数:
// isGroupAllowed checks if a group is allowed by the allowedGroups setting.
// Empty allowedGroups means all groups are allowed.
func isGroupAllowed(allowedGroups string, group string) bool {
if strings.TrimSpace(allowedGroups) == "" {
return true
}
groups := strings.Split(allowedGroups, ",")
for _, g := range groups {
if strings.TrimSpace(g) == group {
return true
}
}
return false
}
2. 核心逻辑修改
CreateUserSubscriptionFromPlanTx(第 485 行):
sub := &UserSubscription{
// ... 现有字段 ...
AllowedGroups: plan.AllowedGroups, // 新增:拷贝套餐的分组限制
}
PreConsumeUserSubscription(第 956 行):
- 函数签名修改:
func PreConsumeUserSubscription(requestId string, userId int, modelName string, quotaType int, amount int64, usingGroup string) (*SubscriptionPreConsumeResult, error)
- 在候选订阅遍历循环(第 1002 行)中加入分组过滤:
for _, candidate := range subs {
sub := candidate
// 新增:检查分组是否匹配
if !isGroupAllowed(sub.AllowedGroups, usingGroup) {
continue
}
// ... 现有逻辑 ...
}
新增 HasActiveUserSubscriptionForGroup:
// HasActiveUserSubscriptionForGroup checks if user has active subscription for specific group.
func HasActiveUserSubscriptionForGroup(userId int, usingGroup string) (bool, error) {
if userId <= 0 {
return false, errors.New("invalid userId")
}
now := common.GetTimestamp()
var subs []UserSubscription
if err := DB.Model(&UserSubscription{}).
Select("id, allowed_groups").
Where("user_id = ? AND status = ? AND end_time > ?", userId, "active", now).
Find(&subs).Error; err != nil {
return false, err
}
for _, sub := range subs {
if isGroupAllowed(sub.AllowedGroups, usingGroup) {
return true, nil
}
}
return false, nil
}
3. 数据库迁移(model/main.go)
SQLite(ensureSubscriptionPlanTableSQLite 函数,第 381 行):
- CREATE TABLE 语句中添加:
allowed_groups TEXT DEFAULT ''
- required columns 数组添加:
"allowed_groups"
MySQL/PostgreSQL:GORM AutoMigrate 自动处理新增字段。
4. 计费层(service/)
funding_source.go(第 70 行):
type SubscriptionFunding struct {
// ... 现有字段 ...
usingGroup string // 新增
}
PreConsume 方法(第 86 行):
res, err := model.PreConsumeUserSubscription(
s.requestId,
s.userId,
s.modelName,
0,
s.amount,
s.usingGroup, // 新增参数
)
billing_session.go(第 255 行):
trySubscription 闭包(第 292 行):
session := &BillingSession{
relayInfo: relayInfo,
funding: &SubscriptionFunding{
requestId: relayInfo.RequestId,
userId: relayInfo.UserId,
modelName: relayInfo.OriginModelName,
amount: subConsume,
usingGroup: relayInfo.UsingGroup, // 新增
},
}
subscription_first 分支(第 328 行):
hasSub, subCheckErr := model.HasActiveUserSubscriptionForGroup(relayInfo.UserId, relayInfo.UsingGroup)
5. 管理接口(controller/subscription.go)
AdminCreateSubscriptionPlan(第 110 行)和 AdminUpdateSubscriptionPlan(第 168 行):
添加分组验证(在 UpgradeGroup 验证后):
// 验证 AllowedGroups
req.Plan.AllowedGroups = strings.TrimSpace(req.Plan.AllowedGroups)
if req.Plan.AllowedGroups != "" {
groups := strings.Split(req.Plan.AllowedGroups, ",")
groupRatios := ratio_setting.GetGroupRatioCopy()
for _, g := range groups {
g = strings.TrimSpace(g)
if g != "" {
if _, ok := groupRatios[g]; !ok {
common.ApiErrorMsg(c, fmt.Sprintf("分组 %s 不存在", g))
return
}
}
}
}
Update map 中添加(第 223 行):
updateMap := map[string]interface{}{
// ... 现有字段 ...
"allowed_groups": req.Plan.AllowedGroups, // 新增
}
6. 前端
管理后台表单(web/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx):
- 添加分组多选
Form.Select(复用已有的 groupOptions)
getInitValues 和 buildFormValues 添加 allowed_groups 处理(数组 ↔ 逗号分隔字符串转换)
管理后台表格(web/src/components/table/subscriptions/SubscriptionsColumnDefs.jsx):
- 新增「适用分组」列
renderPlanTitle popover 中添加适用分组展示
用户侧展示(web/src/components/topup/SubscriptionPlansCard.jsx):
i18n:
en.json 添加:"适用分组" → "Allowed Groups","所有分组" → "All Groups"
- 运行
bun run i18n:sync 同步其他语言
向后兼容性
| 场景 |
行为 |
| 现有订阅(AllowedGroups 为空) |
适用所有分组,行为不变 |
| 新订阅指定分组 |
仅匹配的分组请求消费该订阅 |
| 无匹配订阅时 |
按现有逻辑 fallback 到钱包 |
| API 响应 |
新字段默认空字符串,前端兼容 |
涉及文件清单
后端:
model/subscription.go
model/main.go
service/funding_source.go
service/billing_session.go
controller/subscription.go
前端:
web/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx
web/src/components/table/subscriptions/SubscriptionsColumnDefs.jsx
web/src/components/topup/SubscriptionPlansCard.jsx
web/src/i18n/locales/en.json(及其他语言文件)
提交前必读(请勿删除本节)
您当前的 newapi 版本
v1.0.0+(基于最新 main 分支代码)
提交确认
功能描述
为订阅套餐增加分组限制功能,允许管理员创建仅适用于特定用户分组的订阅套餐。
核心设计:
SubscriptionPlan和UserSubscription新增AllowedGroups字段(逗号分隔字符串,空值 = 适用所有分组)UsingGroup匹配订阅,只消费匹配的订阅额度AllowedGroups为空,行为不变应用场景
当前订阅系统的配额是全站共用的,无法区分不同分组。这在以下场景中存在问题:
多产品线运营:运营方同时提供多个 AI 服务(如 Codex 代码助手、Claude 对话服务),希望为每个产品线创建独立的订阅套餐,实现配额隔离和差异化定价。
企业客户管理:企业客户购买特定模型的订阅(如仅购买 GPT-4 访问权限),不希望额度被用于其他模型分组。
成本控制:不同分组的上游成本差异大(如 o1 模型成本远高于 GPT-3.5),需要独立定价和额度管理。
期望行为示例:
详细实现方案
1. 数据模型(
model/subscription.go)SubscriptionPlan 新增字段(第 169 行后):
UserSubscription 新增字段(第 251 行后):
新增辅助函数:
2. 核心逻辑修改
CreateUserSubscriptionFromPlanTx(第 485 行):
PreConsumeUserSubscription(第 956 行):
func PreConsumeUserSubscription(requestId string, userId int, modelName string, quotaType int, amount int64, usingGroup string) (*SubscriptionPreConsumeResult, error)新增 HasActiveUserSubscriptionForGroup:
3. 数据库迁移(
model/main.go)SQLite(
ensureSubscriptionPlanTableSQLite函数,第 381 行):allowed_groups TEXT DEFAULT ''"allowed_groups"MySQL/PostgreSQL:GORM AutoMigrate 自动处理新增字段。
4. 计费层(
service/)funding_source.go(第 70 行):PreConsume 方法(第 86 行):
billing_session.go(第 255 行):trySubscription闭包(第 292 行):subscription_first分支(第 328 行):5. 管理接口(
controller/subscription.go)AdminCreateSubscriptionPlan(第 110 行)和 AdminUpdateSubscriptionPlan(第 168 行):
添加分组验证(在 UpgradeGroup 验证后):
Update map 中添加(第 223 行):
6. 前端
管理后台表单(
web/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx):Form.Select(复用已有的 groupOptions)getInitValues和buildFormValues添加allowed_groups处理(数组 ↔ 逗号分隔字符串转换)管理后台表格(
web/src/components/table/subscriptions/SubscriptionsColumnDefs.jsx):renderPlanTitlepopover 中添加适用分组展示用户侧展示(
web/src/components/topup/SubscriptionPlansCard.jsx):i18n:
en.json添加:"适用分组" → "Allowed Groups","所有分组" → "All Groups"bun run i18n:sync同步其他语言向后兼容性
涉及文件清单
后端:
model/subscription.gomodel/main.goservice/funding_source.goservice/billing_session.gocontroller/subscription.go前端:
web/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsxweb/src/components/table/subscriptions/SubscriptionsColumnDefs.jsxweb/src/components/topup/SubscriptionPlansCard.jsxweb/src/i18n/locales/en.json(及其他语言文件)