mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-07 01:56:53 +00:00
feat(subscription): add support for wallet overflow and downgrade group functionality in subscription plans
This commit is contained in:
@@ -168,6 +168,9 @@ func AdminCreateSubscriptionPlan(c *gin.Context) {
|
|||||||
if req.Plan.AllowBalancePay == nil {
|
if req.Plan.AllowBalancePay == nil {
|
||||||
req.Plan.AllowBalancePay = common.GetPointer(true)
|
req.Plan.AllowBalancePay = common.GetPointer(true)
|
||||||
}
|
}
|
||||||
|
if req.Plan.AllowWalletOverflow == nil {
|
||||||
|
req.Plan.AllowWalletOverflow = common.GetPointer(true)
|
||||||
|
}
|
||||||
if req.Plan.DurationUnit == "" {
|
if req.Plan.DurationUnit == "" {
|
||||||
req.Plan.DurationUnit = model.SubscriptionDurationMonth
|
req.Plan.DurationUnit = model.SubscriptionDurationMonth
|
||||||
}
|
}
|
||||||
@@ -189,6 +192,13 @@ func AdminCreateSubscriptionPlan(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
req.Plan.DowngradeGroup = strings.TrimSpace(req.Plan.DowngradeGroup)
|
||||||
|
if req.Plan.DowngradeGroup != "" {
|
||||||
|
if _, ok := ratio_setting.GetGroupRatioCopy()[req.Plan.DowngradeGroup]; !ok {
|
||||||
|
common.ApiErrorMsg(c, "降级分组不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
req.Plan.QuotaResetPeriod = model.NormalizeResetPeriod(req.Plan.QuotaResetPeriod)
|
req.Plan.QuotaResetPeriod = model.NormalizeResetPeriod(req.Plan.QuotaResetPeriod)
|
||||||
if req.Plan.QuotaResetPeriod == model.SubscriptionResetCustom && req.Plan.QuotaResetCustomSeconds <= 0 {
|
if req.Plan.QuotaResetPeriod == model.SubscriptionResetCustom && req.Plan.QuotaResetCustomSeconds <= 0 {
|
||||||
common.ApiErrorMsg(c, "自定义重置周期需大于0秒")
|
common.ApiErrorMsg(c, "自定义重置周期需大于0秒")
|
||||||
@@ -256,6 +266,13 @@ func AdminUpdateSubscriptionPlan(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
req.Plan.DowngradeGroup = strings.TrimSpace(req.Plan.DowngradeGroup)
|
||||||
|
if req.Plan.DowngradeGroup != "" {
|
||||||
|
if _, ok := ratio_setting.GetGroupRatioCopy()[req.Plan.DowngradeGroup]; !ok {
|
||||||
|
common.ApiErrorMsg(c, "降级分组不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
req.Plan.QuotaResetPeriod = model.NormalizeResetPeriod(req.Plan.QuotaResetPeriod)
|
req.Plan.QuotaResetPeriod = model.NormalizeResetPeriod(req.Plan.QuotaResetPeriod)
|
||||||
if req.Plan.QuotaResetPeriod == model.SubscriptionResetCustom && req.Plan.QuotaResetCustomSeconds <= 0 {
|
if req.Plan.QuotaResetPeriod == model.SubscriptionResetCustom && req.Plan.QuotaResetCustomSeconds <= 0 {
|
||||||
common.ApiErrorMsg(c, "自定义重置周期需大于0秒")
|
common.ApiErrorMsg(c, "自定义重置周期需大于0秒")
|
||||||
@@ -280,6 +297,7 @@ func AdminUpdateSubscriptionPlan(c *gin.Context) {
|
|||||||
"max_purchase_per_user": req.Plan.MaxPurchasePerUser,
|
"max_purchase_per_user": req.Plan.MaxPurchasePerUser,
|
||||||
"total_amount": req.Plan.TotalAmount,
|
"total_amount": req.Plan.TotalAmount,
|
||||||
"upgrade_group": req.Plan.UpgradeGroup,
|
"upgrade_group": req.Plan.UpgradeGroup,
|
||||||
|
"downgrade_group": req.Plan.DowngradeGroup,
|
||||||
"quota_reset_period": req.Plan.QuotaResetPeriod,
|
"quota_reset_period": req.Plan.QuotaResetPeriod,
|
||||||
"quota_reset_custom_seconds": req.Plan.QuotaResetCustomSeconds,
|
"quota_reset_custom_seconds": req.Plan.QuotaResetCustomSeconds,
|
||||||
"updated_at": common.GetTimestamp(),
|
"updated_at": common.GetTimestamp(),
|
||||||
@@ -287,6 +305,9 @@ func AdminUpdateSubscriptionPlan(c *gin.Context) {
|
|||||||
if req.Plan.AllowBalancePay != nil {
|
if req.Plan.AllowBalancePay != nil {
|
||||||
updateMap["allow_balance_pay"] = *req.Plan.AllowBalancePay
|
updateMap["allow_balance_pay"] = *req.Plan.AllowBalancePay
|
||||||
}
|
}
|
||||||
|
if req.Plan.AllowWalletOverflow != nil {
|
||||||
|
updateMap["allow_wallet_overflow"] = *req.Plan.AllowWalletOverflow
|
||||||
|
}
|
||||||
if err := tx.Model(&model.SubscriptionPlan{}).Where("id = ?", id).Updates(updateMap).Error; err != nil {
|
if err := tx.Model(&model.SubscriptionPlan{}).Where("id = ?", id).Updates(updateMap).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -398,11 +398,13 @@ func ensureSubscriptionPlanTableSQLite() error {
|
|||||||
` + "`enabled`" + ` numeric DEFAULT 1,
|
` + "`enabled`" + ` numeric DEFAULT 1,
|
||||||
` + "`sort_order`" + ` integer DEFAULT 0,
|
` + "`sort_order`" + ` integer DEFAULT 0,
|
||||||
` + "`allow_balance_pay`" + ` numeric DEFAULT 1,
|
` + "`allow_balance_pay`" + ` numeric DEFAULT 1,
|
||||||
|
` + "`allow_wallet_overflow`" + ` numeric DEFAULT 1,
|
||||||
` + "`stripe_price_id`" + ` varchar(128) DEFAULT '',
|
` + "`stripe_price_id`" + ` varchar(128) DEFAULT '',
|
||||||
` + "`creem_product_id`" + ` varchar(128) DEFAULT '',
|
` + "`creem_product_id`" + ` varchar(128) DEFAULT '',
|
||||||
` + "`waffo_pancake_product_id`" + ` varchar(128) DEFAULT '',
|
` + "`waffo_pancake_product_id`" + ` varchar(128) DEFAULT '',
|
||||||
` + "`max_purchase_per_user`" + ` integer DEFAULT 0,
|
` + "`max_purchase_per_user`" + ` integer DEFAULT 0,
|
||||||
` + "`upgrade_group`" + ` varchar(64) DEFAULT '',
|
` + "`upgrade_group`" + ` varchar(64) DEFAULT '',
|
||||||
|
` + "`downgrade_group`" + ` varchar(64) DEFAULT '',
|
||||||
` + "`total_amount`" + ` bigint NOT NULL DEFAULT 0,
|
` + "`total_amount`" + ` bigint NOT NULL DEFAULT 0,
|
||||||
` + "`quota_reset_period`" + ` varchar(16) DEFAULT 'never',
|
` + "`quota_reset_period`" + ` varchar(16) DEFAULT 'never',
|
||||||
` + "`quota_reset_custom_seconds`" + ` bigint DEFAULT 0,
|
` + "`quota_reset_custom_seconds`" + ` bigint DEFAULT 0,
|
||||||
@@ -433,11 +435,13 @@ PRIMARY KEY (` + "`id`" + `)
|
|||||||
{Name: "enabled", DDL: "`enabled` numeric DEFAULT 1"},
|
{Name: "enabled", DDL: "`enabled` numeric DEFAULT 1"},
|
||||||
{Name: "sort_order", DDL: "`sort_order` integer DEFAULT 0"},
|
{Name: "sort_order", DDL: "`sort_order` integer DEFAULT 0"},
|
||||||
{Name: "allow_balance_pay", DDL: "`allow_balance_pay` numeric DEFAULT 1"},
|
{Name: "allow_balance_pay", DDL: "`allow_balance_pay` numeric DEFAULT 1"},
|
||||||
|
{Name: "allow_wallet_overflow", DDL: "`allow_wallet_overflow` numeric DEFAULT 1"},
|
||||||
{Name: "stripe_price_id", DDL: "`stripe_price_id` varchar(128) DEFAULT ''"},
|
{Name: "stripe_price_id", DDL: "`stripe_price_id` varchar(128) DEFAULT ''"},
|
||||||
{Name: "creem_product_id", DDL: "`creem_product_id` varchar(128) DEFAULT ''"},
|
{Name: "creem_product_id", DDL: "`creem_product_id` varchar(128) DEFAULT ''"},
|
||||||
{Name: "waffo_pancake_product_id", DDL: "`waffo_pancake_product_id` varchar(128) DEFAULT ''"},
|
{Name: "waffo_pancake_product_id", DDL: "`waffo_pancake_product_id` varchar(128) DEFAULT ''"},
|
||||||
{Name: "max_purchase_per_user", DDL: "`max_purchase_per_user` integer DEFAULT 0"},
|
{Name: "max_purchase_per_user", DDL: "`max_purchase_per_user` integer DEFAULT 0"},
|
||||||
{Name: "upgrade_group", DDL: "`upgrade_group` varchar(64) DEFAULT ''"},
|
{Name: "upgrade_group", DDL: "`upgrade_group` varchar(64) DEFAULT ''"},
|
||||||
|
{Name: "downgrade_group", DDL: "`downgrade_group` varchar(64) DEFAULT ''"},
|
||||||
{Name: "total_amount", DDL: "`total_amount` bigint NOT NULL DEFAULT 0"},
|
{Name: "total_amount", DDL: "`total_amount` bigint NOT NULL DEFAULT 0"},
|
||||||
{Name: "quota_reset_period", DDL: "`quota_reset_period` varchar(16) DEFAULT 'never'"},
|
{Name: "quota_reset_period", DDL: "`quota_reset_period` varchar(16) DEFAULT 'never'"},
|
||||||
{Name: "quota_reset_custom_seconds", DDL: "`quota_reset_custom_seconds` bigint DEFAULT 0"},
|
{Name: "quota_reset_custom_seconds", DDL: "`quota_reset_custom_seconds` bigint DEFAULT 0"},
|
||||||
|
|||||||
+91
-32
@@ -162,6 +162,9 @@ type SubscriptionPlan struct {
|
|||||||
|
|
||||||
AllowBalancePay *bool `json:"allow_balance_pay" gorm:"default:true"`
|
AllowBalancePay *bool `json:"allow_balance_pay" gorm:"default:true"`
|
||||||
|
|
||||||
|
// Allow falling back to wallet balance after subscription quota is exhausted (empty = true)
|
||||||
|
AllowWalletOverflow *bool `json:"allow_wallet_overflow" gorm:"default:true"`
|
||||||
|
|
||||||
StripePriceId string `json:"stripe_price_id" gorm:"type:varchar(128);default:''"`
|
StripePriceId string `json:"stripe_price_id" gorm:"type:varchar(128);default:''"`
|
||||||
CreemProductId string `json:"creem_product_id" gorm:"type:varchar(128);default:''"`
|
CreemProductId string `json:"creem_product_id" gorm:"type:varchar(128);default:''"`
|
||||||
WaffoPancakeProductId string `json:"waffo_pancake_product_id" gorm:"type:varchar(128);default:''"`
|
WaffoPancakeProductId string `json:"waffo_pancake_product_id" gorm:"type:varchar(128);default:''"`
|
||||||
@@ -172,6 +175,9 @@ type SubscriptionPlan struct {
|
|||||||
// Upgrade user group after purchase (empty = no change)
|
// Upgrade user group after purchase (empty = no change)
|
||||||
UpgradeGroup string `json:"upgrade_group" gorm:"type:varchar(64);default:''"`
|
UpgradeGroup string `json:"upgrade_group" gorm:"type:varchar(64);default:''"`
|
||||||
|
|
||||||
|
// Downgrade user group on expiry (empty = revert to the group held before purchase)
|
||||||
|
DowngradeGroup string `json:"downgrade_group" gorm:"type:varchar(64);default:''"`
|
||||||
|
|
||||||
// Total quota (amount in quota units, 0 = unlimited)
|
// Total quota (amount in quota units, 0 = unlimited)
|
||||||
TotalAmount int64 `json:"total_amount" gorm:"type:bigint;not null;default:0"`
|
TotalAmount int64 `json:"total_amount" gorm:"type:bigint;not null;default:0"`
|
||||||
|
|
||||||
@@ -199,6 +205,9 @@ func (p *SubscriptionPlan) NormalizeDefaults() {
|
|||||||
if p.AllowBalancePay == nil {
|
if p.AllowBalancePay == nil {
|
||||||
p.AllowBalancePay = common.GetPointer(true)
|
p.AllowBalancePay = common.GetPointer(true)
|
||||||
}
|
}
|
||||||
|
if p.AllowWalletOverflow == nil {
|
||||||
|
p.AllowWalletOverflow = common.GetPointer(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscription order (payment -> webhook -> create UserSubscription)
|
// Subscription order (payment -> webhook -> create UserSubscription)
|
||||||
@@ -261,6 +270,12 @@ type UserSubscription struct {
|
|||||||
UpgradeGroup string `json:"upgrade_group" gorm:"type:varchar(64);default:''"`
|
UpgradeGroup string `json:"upgrade_group" gorm:"type:varchar(64);default:''"`
|
||||||
PrevUserGroup string `json:"prev_user_group" gorm:"type:varchar(64);default:''"`
|
PrevUserGroup string `json:"prev_user_group" gorm:"type:varchar(64);default:''"`
|
||||||
|
|
||||||
|
// Downgrade target group on expiry (snapshot from plan; empty = revert to PrevUserGroup)
|
||||||
|
DowngradeGroup string `json:"downgrade_group" gorm:"type:varchar(64);default:''"`
|
||||||
|
|
||||||
|
// Whether wallet fallback is allowed after this subscription's quota is exhausted (snapshot from plan)
|
||||||
|
AllowWalletOverflow bool `json:"allow_wallet_overflow" gorm:"default:true"`
|
||||||
|
|
||||||
CreatedAt int64 `json:"created_at" gorm:"bigint"`
|
CreatedAt int64 `json:"created_at" gorm:"bigint"`
|
||||||
UpdatedAt int64 `json:"updated_at" gorm:"bigint"`
|
UpdatedAt int64 `json:"updated_at" gorm:"bigint"`
|
||||||
}
|
}
|
||||||
@@ -416,17 +431,17 @@ func downgradeUserGroupForSubscriptionTx(tx *gorm.DB, sub *UserSubscription, now
|
|||||||
if tx == nil || sub == nil {
|
if tx == nil || sub == nil {
|
||||||
return "", errors.New("invalid downgrade args")
|
return "", errors.New("invalid downgrade args")
|
||||||
}
|
}
|
||||||
|
downgradeGroup := strings.TrimSpace(sub.DowngradeGroup)
|
||||||
upgradeGroup := strings.TrimSpace(sub.UpgradeGroup)
|
upgradeGroup := strings.TrimSpace(sub.UpgradeGroup)
|
||||||
if upgradeGroup == "" {
|
// Nothing to do if neither an explicit downgrade target nor an upgrade snapshot exists.
|
||||||
|
if downgradeGroup == "" && upgradeGroup == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
currentGroup, err := getUserGroupByIdTx(tx, sub.UserId)
|
currentGroup, err := getUserGroupByIdTx(tx, sub.UserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if currentGroup != upgradeGroup {
|
// If another active upgraded subscription exists, keep the current group.
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
var activeSub UserSubscription
|
var activeSub UserSubscription
|
||||||
activeQuery := tx.Where("user_id = ? AND status = ? AND end_time > ? AND id <> ? AND upgrade_group <> ''",
|
activeQuery := tx.Where("user_id = ? AND status = ? AND end_time > ? AND id <> ? AND upgrade_group <> ''",
|
||||||
sub.UserId, "active", now, sub.Id).
|
sub.UserId, "active", now, sub.Id).
|
||||||
@@ -436,15 +451,24 @@ func downgradeUserGroupForSubscriptionTx(tx *gorm.DB, sub *UserSubscription, now
|
|||||||
if activeQuery.Error == nil && activeQuery.RowsAffected > 0 {
|
if activeQuery.Error == nil && activeQuery.RowsAffected > 0 {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
prevGroup := strings.TrimSpace(sub.PrevUserGroup)
|
// Determine the downgrade target: an explicit downgrade group takes precedence,
|
||||||
if prevGroup == "" || prevGroup == currentGroup {
|
// otherwise revert to the group held before purchase (legacy behavior).
|
||||||
|
target := downgradeGroup
|
||||||
|
if target == "" {
|
||||||
|
// Legacy behavior: only revert when the subscription actually elevated the user.
|
||||||
|
if currentGroup != upgradeGroup {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
target = strings.TrimSpace(sub.PrevUserGroup)
|
||||||
|
}
|
||||||
|
if target == "" || target == currentGroup {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
if err := tx.Model(&User{}).Where("id = ?", sub.UserId).
|
if err := tx.Model(&User{}).Where("id = ?", sub.UserId).
|
||||||
Update("group", prevGroup).Error; err != nil {
|
Update("group", target).Error; err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return prevGroup, nil
|
return target, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateUserSubscriptionFromPlanTx(tx *gorm.DB, userId int, plan *SubscriptionPlan, source string) (*UserSubscription, error) {
|
func CreateUserSubscriptionFromPlanTx(tx *gorm.DB, userId int, plan *SubscriptionPlan, source string) (*UserSubscription, error) {
|
||||||
@@ -495,21 +519,27 @@ func CreateUserSubscriptionFromPlanTx(tx *gorm.DB, userId int, plan *Subscriptio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
allowWalletOverflow := true
|
||||||
|
if plan.AllowWalletOverflow != nil {
|
||||||
|
allowWalletOverflow = *plan.AllowWalletOverflow
|
||||||
|
}
|
||||||
sub := &UserSubscription{
|
sub := &UserSubscription{
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
PlanId: plan.Id,
|
PlanId: plan.Id,
|
||||||
AmountTotal: plan.TotalAmount,
|
AmountTotal: plan.TotalAmount,
|
||||||
AmountUsed: 0,
|
AmountUsed: 0,
|
||||||
StartTime: now.Unix(),
|
StartTime: now.Unix(),
|
||||||
EndTime: endUnix,
|
EndTime: endUnix,
|
||||||
Status: "active",
|
Status: "active",
|
||||||
Source: source,
|
Source: source,
|
||||||
LastResetTime: lastReset,
|
LastResetTime: lastReset,
|
||||||
NextResetTime: nextReset,
|
NextResetTime: nextReset,
|
||||||
UpgradeGroup: upgradeGroup,
|
UpgradeGroup: upgradeGroup,
|
||||||
PrevUserGroup: prevGroup,
|
PrevUserGroup: prevGroup,
|
||||||
CreatedAt: common.GetTimestamp(),
|
DowngradeGroup: strings.TrimSpace(plan.DowngradeGroup),
|
||||||
UpdatedAt: common.GetTimestamp(),
|
AllowWalletOverflow: allowWalletOverflow,
|
||||||
|
CreatedAt: common.GetTimestamp(),
|
||||||
|
UpdatedAt: common.GetTimestamp(),
|
||||||
}
|
}
|
||||||
if err := tx.Create(sub).Error; err != nil {
|
if err := tx.Create(sub).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -811,6 +841,24 @@ func HasActiveUserSubscription(userId int) (bool, error) {
|
|||||||
return count > 0, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserActiveSubscriptionsAllowWalletOverflow returns whether wallet balance may be used
|
||||||
|
// after the user's subscription quota is exhausted. A single active subscription that
|
||||||
|
// disallows wallet overflow (allow_wallet_overflow = false) blocks the fallback.
|
||||||
|
func UserActiveSubscriptionsAllowWalletOverflow(userId int) (bool, error) {
|
||||||
|
if userId <= 0 {
|
||||||
|
return false, errors.New("invalid userId")
|
||||||
|
}
|
||||||
|
now := common.GetTimestamp()
|
||||||
|
var strictCount int64
|
||||||
|
if err := DB.Model(&UserSubscription{}).
|
||||||
|
Where("user_id = ? AND status = ? AND end_time > ? AND allow_wallet_overflow = ?",
|
||||||
|
userId, "active", now, false).
|
||||||
|
Count(&strictCount).Error; err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return strictCount == 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetAllUserSubscriptions returns all subscriptions (active and expired) for a user.
|
// GetAllUserSubscriptions returns all subscriptions (active and expired) for a user.
|
||||||
func GetAllUserSubscriptions(userId int) ([]SubscriptionSummary, error) {
|
func GetAllUserSubscriptions(userId int) ([]SubscriptionSummary, error) {
|
||||||
if userId <= 0 {
|
if userId <= 0 {
|
||||||
@@ -982,9 +1030,10 @@ func ExpireDueSubscriptions(limit int) (int, error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// No active upgraded subscription, downgrade to previous group if needed.
|
// Find the most recently expired subscription that defines a group transition
|
||||||
|
// (an explicit downgrade target or an upgrade snapshot to revert).
|
||||||
var lastExpired UserSubscription
|
var lastExpired UserSubscription
|
||||||
expiredQuery := tx.Where("user_id = ? AND status = ? AND upgrade_group <> ''",
|
expiredQuery := tx.Where("user_id = ? AND status = ? AND (downgrade_group <> '' OR upgrade_group <> '')",
|
||||||
userId, "expired").
|
userId, "expired").
|
||||||
Order("end_time desc, id desc").
|
Order("end_time desc, id desc").
|
||||||
Limit(1).
|
Limit(1).
|
||||||
@@ -992,23 +1041,33 @@ func ExpireDueSubscriptions(limit int) (int, error) {
|
|||||||
if expiredQuery.Error != nil || expiredQuery.RowsAffected == 0 {
|
if expiredQuery.Error != nil || expiredQuery.RowsAffected == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
upgradeGroup := strings.TrimSpace(lastExpired.UpgradeGroup)
|
|
||||||
prevGroup := strings.TrimSpace(lastExpired.PrevUserGroup)
|
|
||||||
if upgradeGroup == "" || prevGroup == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
currentGroup, err := getUserGroupByIdTx(tx, userId)
|
currentGroup, err := getUserGroupByIdTx(tx, userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if currentGroup != upgradeGroup || currentGroup == prevGroup {
|
// An explicit downgrade group takes precedence; otherwise revert to the
|
||||||
|
// group held before purchase (legacy behavior, only when the subscription
|
||||||
|
// actually elevated the user).
|
||||||
|
target := strings.TrimSpace(lastExpired.DowngradeGroup)
|
||||||
|
if target == "" {
|
||||||
|
upgradeGroup := strings.TrimSpace(lastExpired.UpgradeGroup)
|
||||||
|
prevGroup := strings.TrimSpace(lastExpired.PrevUserGroup)
|
||||||
|
if upgradeGroup == "" || prevGroup == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if currentGroup != upgradeGroup {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
target = prevGroup
|
||||||
|
}
|
||||||
|
if target == "" || target == currentGroup {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := tx.Model(&User{}).Where("id = ?", userId).
|
if err := tx.Model(&User{}).Where("id = ?", userId).
|
||||||
Update("group", prevGroup).Error; err != nil {
|
Update("group", target).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cacheGroup = prevGroup
|
cacheGroup = target
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -425,7 +425,15 @@ func NewBillingSession(c *gin.Context, relayInfo *relaycommon.RelayInfo, preCons
|
|||||||
session, apiErr := trySubscription()
|
session, apiErr := trySubscription()
|
||||||
if apiErr != nil {
|
if apiErr != nil {
|
||||||
if apiErr.GetErrorCode() == types.ErrorCodeInsufficientUserQuota {
|
if apiErr.GetErrorCode() == types.ErrorCodeInsufficientUserQuota {
|
||||||
return tryWallet()
|
// 仅当用户的活跃订阅允许钱包回退时才回退到钱包,否则返回订阅额度不足错误
|
||||||
|
allowOverflow, overflowErr := model.UserActiveSubscriptionsAllowWalletOverflow(relayInfo.UserId)
|
||||||
|
if overflowErr != nil {
|
||||||
|
return nil, types.NewError(overflowErr, types.ErrorCodeQueryDataError, types.ErrOptionWithSkipRetry())
|
||||||
|
}
|
||||||
|
if allowOverflow {
|
||||||
|
return tryWallet()
|
||||||
|
}
|
||||||
|
return nil, apiErr
|
||||||
}
|
}
|
||||||
return nil, apiErr
|
return nil, apiErr
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -297,7 +297,7 @@ export function SubscriptionPurchaseDialog(props: Props) {
|
|||||||
)}
|
)}
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<span className='text-muted-foreground text-sm'>
|
<span className='text-muted-foreground text-sm'>
|
||||||
{t('Received amount')}
|
{t('Plan Quota')}
|
||||||
</span>
|
</span>
|
||||||
<span className='flex items-center gap-1 text-sm'>
|
<span className='flex items-center gap-1 text-sm'>
|
||||||
<Package className='h-3.5 w-3.5' />
|
<Package className='h-3.5 w-3.5' />
|
||||||
|
|||||||
+4
-1
@@ -52,6 +52,7 @@ import {
|
|||||||
invalidateUserSubscription,
|
invalidateUserSubscription,
|
||||||
deleteUserSubscription,
|
deleteUserSubscription,
|
||||||
} from '../../api'
|
} from '../../api'
|
||||||
|
import { formatQuota } from '@/lib/format'
|
||||||
import { formatTimestamp } from '../../lib'
|
import { formatTimestamp } from '../../lib'
|
||||||
import type { PlanRecord, UserSubscriptionRecord } from '../../types'
|
import type { PlanRecord, UserSubscriptionRecord } from '../../types'
|
||||||
|
|
||||||
@@ -301,7 +302,9 @@ export function UserSubscriptionsDialog(props: Props) {
|
|||||||
const sub = record.subscription
|
const sub = record.subscription
|
||||||
const total = Number(sub.amount_total || 0)
|
const total = Number(sub.amount_total || 0)
|
||||||
const used = Number(sub.amount_used || 0)
|
const used = Number(sub.amount_used || 0)
|
||||||
return total > 0 ? `${used}/${total}` : t('Unlimited')
|
return total > 0
|
||||||
|
? `${formatQuota(used)}/${formatQuota(total)}`
|
||||||
|
: t('Unlimited')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -160,7 +160,7 @@ export function useSubscriptionsColumns(): ColumnDef<PlanRecord>[] {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'total_amount',
|
id: 'total_amount',
|
||||||
header: t('Received amount'),
|
header: t('Plan Quota'),
|
||||||
meta: { mobileHidden: true },
|
meta: { mobileHidden: true },
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const total = Number(row.original.plan.total_amount || 0)
|
const total = Number(row.original.plan.total_amount || 0)
|
||||||
|
|||||||
+102
-23
@@ -66,6 +66,7 @@ import {
|
|||||||
createWaffoPancakeSubscriptionProduct,
|
createWaffoPancakeSubscriptionProduct,
|
||||||
listWaffoPancakeSubscriptionProductOptions,
|
listWaffoPancakeSubscriptionProductOptions,
|
||||||
} from '../api'
|
} from '../api'
|
||||||
|
import { getCurrencyDisplay, getCurrencyLabel } from '@/lib/currency'
|
||||||
import { getDurationUnitOptions, getResetPeriodOptions } from '../constants'
|
import { getDurationUnitOptions, getResetPeriodOptions } from '../constants'
|
||||||
import {
|
import {
|
||||||
getPlanFormSchema,
|
getPlanFormSchema,
|
||||||
@@ -91,6 +92,9 @@ export function SubscriptionsMutateDrawer({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const isEdit = !!currentRow?.plan?.id
|
const isEdit = !!currentRow?.plan?.id
|
||||||
const { triggerRefresh } = useSubscriptions()
|
const { triggerRefresh } = useSubscriptions()
|
||||||
|
const { meta: currencyMeta } = getCurrencyDisplay()
|
||||||
|
const tokensOnly = currencyMeta.kind === 'tokens'
|
||||||
|
const currencyLabel = getCurrencyLabel()
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [groupOptions, setGroupOptions] = useState<string[]>([])
|
const [groupOptions, setGroupOptions] = useState<string[]>([])
|
||||||
const [creatingPancakeProduct, setCreatingPancakeProduct] = useState(false)
|
const [creatingPancakeProduct, setCreatingPancakeProduct] = useState(false)
|
||||||
@@ -314,7 +318,7 @@ export function SubscriptionsMutateDrawer({
|
|||||||
name='price_amount'
|
name='price_amount'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('Actual Amount')}</FormLabel>
|
<FormLabel>{t('Plan Price')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
@@ -336,12 +340,22 @@ export function SubscriptionsMutateDrawer({
|
|||||||
name='total_amount'
|
name='total_amount'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('Received amount')}</FormLabel>
|
<FormLabel>
|
||||||
|
{t('Quota ({{currency}})', { currency: currencyLabel })}
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
type='number'
|
type='number'
|
||||||
min={0}
|
min={0}
|
||||||
|
step={tokensOnly ? 1 : 0.01}
|
||||||
|
placeholder={
|
||||||
|
tokensOnly
|
||||||
|
? t('Enter quota in tokens')
|
||||||
|
: t('Enter quota in {{currency}}', {
|
||||||
|
currency: currencyLabel,
|
||||||
|
})
|
||||||
|
}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
field.onChange(parseFloat(e.target.value) || 0)
|
field.onChange(parseFloat(e.target.value) || 0)
|
||||||
}
|
}
|
||||||
@@ -349,7 +363,7 @@ export function SubscriptionsMutateDrawer({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t(
|
||||||
'0 means unlimited. The value is converted to quota units when saved.'
|
'Total quota included in the plan, usable per billing period. 0 means unlimited.'
|
||||||
)}
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -398,6 +412,53 @@ export function SubscriptionsMutateDrawer({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='downgrade_group'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('Downgrade Group')}</FormLabel>
|
||||||
|
<Select
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
value: '__none__',
|
||||||
|
label: t('Downgrade to pre-purchase group'),
|
||||||
|
},
|
||||||
|
...groupOptions.map((g) => ({ value: g, label: g })),
|
||||||
|
]}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
field.onChange(v === '__none__' ? '' : v)
|
||||||
|
}
|
||||||
|
value={field.value || ''}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t('Downgrade to pre-purchase group')}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent alignItemWithTrigger={false}>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value='__none__'>
|
||||||
|
{t('Downgrade to pre-purchase group')}
|
||||||
|
</SelectItem>
|
||||||
|
{groupOptions.map((g) => (
|
||||||
|
<SelectItem key={g} value={g}>
|
||||||
|
{g}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
{t('Downgrade to this group after the subscription expires')}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='max_purchase_per_user'
|
name='max_purchase_per_user'
|
||||||
@@ -423,27 +484,27 @@ export function SubscriptionsMutateDrawer({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid grid-cols-1 gap-3 sm:grid-cols-2'>
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name='sort_order'
|
||||||
name='sort_order'
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<FormLabel>{t('Sort Order')}</FormLabel>
|
||||||
<FormLabel>{t('Sort Order')}</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
<Input
|
||||||
<Input
|
{...field}
|
||||||
{...field}
|
type='number'
|
||||||
type='number'
|
onChange={(e) =>
|
||||||
onChange={(e) =>
|
field.onChange(parseInt(e.target.value, 10) || 0)
|
||||||
field.onChange(parseInt(e.target.value, 10) || 0)
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</FormControl>
|
||||||
</FormControl>
|
<FormMessage />
|
||||||
<FormMessage />
|
</FormItem>
|
||||||
</FormItem>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-3'>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='enabled'
|
name='enabled'
|
||||||
@@ -479,6 +540,24 @@ export function SubscriptionsMutateDrawer({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='allow_wallet_overflow'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className={sideDrawerSwitchItemClassName()}>
|
||||||
|
<FormLabel className='!mt-0'>
|
||||||
|
{t('Allow wallet balance after quota used up')}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SideDrawerSection>
|
</SideDrawerSection>
|
||||||
|
|
||||||
|
|||||||
@@ -40,9 +40,11 @@ export function getPlanFormSchema(t: TFunction) {
|
|||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
sort_order: z.coerce.number(),
|
sort_order: z.coerce.number(),
|
||||||
allow_balance_pay: z.boolean(),
|
allow_balance_pay: z.boolean(),
|
||||||
|
allow_wallet_overflow: z.boolean(),
|
||||||
max_purchase_per_user: z.coerce.number().min(0),
|
max_purchase_per_user: z.coerce.number().min(0),
|
||||||
total_amount: z.coerce.number().min(0),
|
total_amount: z.coerce.number().min(0),
|
||||||
upgrade_group: z.string().optional(),
|
upgrade_group: z.string().optional(),
|
||||||
|
downgrade_group: z.string().optional(),
|
||||||
stripe_price_id: z.string().optional(),
|
stripe_price_id: z.string().optional(),
|
||||||
creem_product_id: z.string().optional(),
|
creem_product_id: z.string().optional(),
|
||||||
waffo_pancake_product_id: z.string().optional(),
|
waffo_pancake_product_id: z.string().optional(),
|
||||||
@@ -63,9 +65,11 @@ export const PLAN_FORM_DEFAULTS: PlanFormValues = {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
sort_order: 0,
|
sort_order: 0,
|
||||||
allow_balance_pay: true,
|
allow_balance_pay: true,
|
||||||
|
allow_wallet_overflow: true,
|
||||||
max_purchase_per_user: 0,
|
max_purchase_per_user: 0,
|
||||||
total_amount: 0,
|
total_amount: 0,
|
||||||
upgrade_group: '',
|
upgrade_group: '',
|
||||||
|
downgrade_group: '',
|
||||||
stripe_price_id: '',
|
stripe_price_id: '',
|
||||||
creem_product_id: '',
|
creem_product_id: '',
|
||||||
waffo_pancake_product_id: '',
|
waffo_pancake_product_id: '',
|
||||||
@@ -84,9 +88,11 @@ export function planToFormValues(plan: SubscriptionPlan): PlanFormValues {
|
|||||||
enabled: plan.enabled !== false,
|
enabled: plan.enabled !== false,
|
||||||
sort_order: Number(plan.sort_order || 0),
|
sort_order: Number(plan.sort_order || 0),
|
||||||
allow_balance_pay: plan.allow_balance_pay !== false,
|
allow_balance_pay: plan.allow_balance_pay !== false,
|
||||||
|
allow_wallet_overflow: plan.allow_wallet_overflow !== false,
|
||||||
max_purchase_per_user: Number(plan.max_purchase_per_user || 0),
|
max_purchase_per_user: Number(plan.max_purchase_per_user || 0),
|
||||||
total_amount: quotaUnitsToDollars(Number(plan.total_amount || 0)),
|
total_amount: quotaUnitsToDollars(Number(plan.total_amount || 0)),
|
||||||
upgrade_group: plan.upgrade_group || '',
|
upgrade_group: plan.upgrade_group || '',
|
||||||
|
downgrade_group: plan.downgrade_group || '',
|
||||||
stripe_price_id: plan.stripe_price_id || '',
|
stripe_price_id: plan.stripe_price_id || '',
|
||||||
creem_product_id: plan.creem_product_id || '',
|
creem_product_id: plan.creem_product_id || '',
|
||||||
waffo_pancake_product_id: plan.waffo_pancake_product_id || '',
|
waffo_pancake_product_id: plan.waffo_pancake_product_id || '',
|
||||||
@@ -110,6 +116,7 @@ export function formValuesToPlanPayload(values: PlanFormValues): PlanPayload {
|
|||||||
max_purchase_per_user: Number(values.max_purchase_per_user || 0),
|
max_purchase_per_user: Number(values.max_purchase_per_user || 0),
|
||||||
total_amount: parseQuotaFromDollars(Number(values.total_amount || 0)),
|
total_amount: parseQuotaFromDollars(Number(values.total_amount || 0)),
|
||||||
upgrade_group: values.upgrade_group || '',
|
upgrade_group: values.upgrade_group || '',
|
||||||
|
downgrade_group: values.downgrade_group || '',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,11 @@ export const subscriptionPlanSchema = z.object({
|
|||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
sort_order: z.number(),
|
sort_order: z.number(),
|
||||||
allow_balance_pay: z.boolean().optional().default(true),
|
allow_balance_pay: z.boolean().optional().default(true),
|
||||||
|
allow_wallet_overflow: z.boolean().optional().default(true),
|
||||||
max_purchase_per_user: z.number(),
|
max_purchase_per_user: z.number(),
|
||||||
total_amount: z.number(),
|
total_amount: z.number(),
|
||||||
upgrade_group: z.string().optional(),
|
upgrade_group: z.string().optional(),
|
||||||
|
downgrade_group: z.string().optional(),
|
||||||
stripe_price_id: z.string().optional(),
|
stripe_price_id: z.string().optional(),
|
||||||
creem_product_id: z.string().optional(),
|
creem_product_id: z.string().optional(),
|
||||||
waffo_pancake_product_id: z.string().optional(),
|
waffo_pancake_product_id: z.string().optional(),
|
||||||
|
|||||||
Vendored
+7
@@ -138,6 +138,9 @@
|
|||||||
"Active models": "Active models",
|
"Active models": "Active models",
|
||||||
"active users": "active users",
|
"active users": "active users",
|
||||||
"Actual Amount": "Actual Amount",
|
"Actual Amount": "Actual Amount",
|
||||||
|
"Plan Price": "Plan Price",
|
||||||
|
"Plan Quota": "Plan Quota",
|
||||||
|
"Total quota included in the plan, usable per billing period. 0 means unlimited.": "Total quota included in the plan, usable per billing period. 0 means unlimited.",
|
||||||
"Actual Model": "Actual Model",
|
"Actual Model": "Actual Model",
|
||||||
"Actual Model:": "Actual Model:",
|
"Actual Model:": "Actual Model:",
|
||||||
"Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.",
|
"Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.",
|
||||||
@@ -276,6 +279,10 @@
|
|||||||
"Allocated Memory": "Allocated Memory",
|
"Allocated Memory": "Allocated Memory",
|
||||||
"Allow accountFilter parameter": "Allow accountFilter parameter",
|
"Allow accountFilter parameter": "Allow accountFilter parameter",
|
||||||
"Allow balance redemption": "Allow balance redemption",
|
"Allow balance redemption": "Allow balance redemption",
|
||||||
|
"Allow wallet balance after quota used up": "Allow wallet balance after quota used up",
|
||||||
|
"Downgrade Group": "Downgrade Group",
|
||||||
|
"Downgrade to pre-purchase group": "Downgrade to pre-purchase group",
|
||||||
|
"Downgrade to this group after the subscription expires": "Downgrade to this group after the subscription expires",
|
||||||
"Allow Claude beta query passthrough": "Allow Claude beta query passthrough",
|
"Allow Claude beta query passthrough": "Allow Claude beta query passthrough",
|
||||||
"Allow clients to query configured ratios via `/api/ratio`.": "Allow clients to query configured ratios via `/api/ratio`.",
|
"Allow clients to query configured ratios via `/api/ratio`.": "Allow clients to query configured ratios via `/api/ratio`.",
|
||||||
"Allow HTTP image requests": "Allow HTTP image requests",
|
"Allow HTTP image requests": "Allow HTTP image requests",
|
||||||
|
|||||||
Vendored
+7
@@ -138,6 +138,9 @@
|
|||||||
"Active models": "Modèles actifs",
|
"Active models": "Modèles actifs",
|
||||||
"active users": "utilisateurs actifs",
|
"active users": "utilisateurs actifs",
|
||||||
"Actual Amount": "Montant réel",
|
"Actual Amount": "Montant réel",
|
||||||
|
"Plan Price": "Prix du forfait",
|
||||||
|
"Plan Quota": "Quota du forfait",
|
||||||
|
"Total quota included in the plan, usable per billing period. 0 means unlimited.": "Quota total inclus dans le forfait, utilisable par période de facturation. 0 signifie illimité.",
|
||||||
"Actual Model": "Modèle réel",
|
"Actual Model": "Modèle réel",
|
||||||
"Actual Model:": "Modèle réel :",
|
"Actual Model:": "Modèle réel :",
|
||||||
"Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "Adapter les requêtes avec le suffixe `-thinking` au comportement de pensée natif d’Anthropic tout en gardant une facturation prévisible.",
|
"Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "Adapter les requêtes avec le suffixe `-thinking` au comportement de pensée natif d’Anthropic tout en gardant une facturation prévisible.",
|
||||||
@@ -276,6 +279,10 @@
|
|||||||
"Allocated Memory": "Mémoire allouée",
|
"Allocated Memory": "Mémoire allouée",
|
||||||
"Allow accountFilter parameter": "Autoriser le paramètre accountFilter",
|
"Allow accountFilter parameter": "Autoriser le paramètre accountFilter",
|
||||||
"Allow balance redemption": "Autoriser le paiement avec le solde",
|
"Allow balance redemption": "Autoriser le paiement avec le solde",
|
||||||
|
"Allow wallet balance after quota used up": "Autoriser le solde du portefeuille une fois le quota épuisé",
|
||||||
|
"Downgrade Group": "Groupe de rétrogradation",
|
||||||
|
"Downgrade to pre-purchase group": "Rétrograder vers le groupe d'avant l'achat",
|
||||||
|
"Downgrade to this group after the subscription expires": "Rétrograder vers ce groupe après l'expiration de l'abonnement",
|
||||||
"Allow Claude beta query passthrough": "Autoriser le passage des requêtes bêta Claude",
|
"Allow Claude beta query passthrough": "Autoriser le passage des requêtes bêta Claude",
|
||||||
"Allow clients to query configured ratios via `/api/ratio`.": "Autoriser les clients à interroger les ratios configurés via `/api/ratio`.",
|
"Allow clients to query configured ratios via `/api/ratio`.": "Autoriser les clients à interroger les ratios configurés via `/api/ratio`.",
|
||||||
"Allow HTTP image requests": "Autoriser les requêtes d'images HTTP",
|
"Allow HTTP image requests": "Autoriser les requêtes d'images HTTP",
|
||||||
|
|||||||
Vendored
+7
@@ -138,6 +138,9 @@
|
|||||||
"Active models": "アクティブなモデル",
|
"Active models": "アクティブなモデル",
|
||||||
"active users": "アクティブユーザー",
|
"active users": "アクティブユーザー",
|
||||||
"Actual Amount": "実際の金額",
|
"Actual Amount": "実際の金額",
|
||||||
|
"Plan Price": "プラン価格",
|
||||||
|
"Plan Quota": "プランのクォータ",
|
||||||
|
"Total quota included in the plan, usable per billing period. 0 means unlimited.": "プランに含まれる合計クォータ。請求期間ごとに使用可能。0 は無制限を意味します。",
|
||||||
"Actual Model": "実際のモデル",
|
"Actual Model": "実際のモデル",
|
||||||
"Actual Model:": "実際のモデル:",
|
"Actual Model:": "実際のモデル:",
|
||||||
"Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "`-thinking` サフィックス付きリクエストを Anthropic ネイティブの思考動作に適配し、課金を予測可能に保ちます。",
|
"Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "`-thinking` サフィックス付きリクエストを Anthropic ネイティブの思考動作に適配し、課金を予測可能に保ちます。",
|
||||||
@@ -276,6 +279,10 @@
|
|||||||
"Allocated Memory": "割り当て済みメモリ",
|
"Allocated Memory": "割り当て済みメモリ",
|
||||||
"Allow accountFilter parameter": "accountFilter パラメータを許可",
|
"Allow accountFilter parameter": "accountFilter パラメータを許可",
|
||||||
"Allow balance redemption": "残高での交換を許可",
|
"Allow balance redemption": "残高での交換を許可",
|
||||||
|
"Allow wallet balance after quota used up": "クォータ使い切り後にウォレット残高の使用を許可",
|
||||||
|
"Downgrade Group": "ダウングレードグループ",
|
||||||
|
"Downgrade to pre-purchase group": "購入前のグループにダウングレード",
|
||||||
|
"Downgrade to this group after the subscription expires": "サブスクリプションの有効期限が切れた後、このグループにダウングレードします",
|
||||||
"Allow Claude beta query passthrough": "Claude ベータクエリのパススルーを許可",
|
"Allow Claude beta query passthrough": "Claude ベータクエリのパススルーを許可",
|
||||||
"Allow clients to query configured ratios via `/api/ratio`.": "クライアントが `/api/ratio` 経由で設定された比率を照会できるようにします。",
|
"Allow clients to query configured ratios via `/api/ratio`.": "クライアントが `/api/ratio` 経由で設定された比率を照会できるようにします。",
|
||||||
"Allow HTTP image requests": "HTTP画像リクエストを許可",
|
"Allow HTTP image requests": "HTTP画像リクエストを許可",
|
||||||
|
|||||||
Vendored
+7
@@ -138,6 +138,9 @@
|
|||||||
"Active models": "Активные модели",
|
"Active models": "Активные модели",
|
||||||
"active users": "активных пользователей",
|
"active users": "активных пользователей",
|
||||||
"Actual Amount": "Фактическая сумма",
|
"Actual Amount": "Фактическая сумма",
|
||||||
|
"Plan Price": "Цена тарифа",
|
||||||
|
"Plan Quota": "Квота тарифа",
|
||||||
|
"Total quota included in the plan, usable per billing period. 0 means unlimited.": "Общая квота, включённая в тариф, доступна за каждый расчётный период. 0 означает безлимит.",
|
||||||
"Actual Model": "Фактическая модель",
|
"Actual Model": "Фактическая модель",
|
||||||
"Actual Model:": "Фактическая модель:",
|
"Actual Model:": "Фактическая модель:",
|
||||||
"Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "Адаптировать запросы с суффиксом `-thinking` к собственному режиму размышления Anthropic, сохраняя предсказуемость биллинга.",
|
"Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "Адаптировать запросы с суффиксом `-thinking` к собственному режиму размышления Anthropic, сохраняя предсказуемость биллинга.",
|
||||||
@@ -276,6 +279,10 @@
|
|||||||
"Allocated Memory": "Выделенная память",
|
"Allocated Memory": "Выделенная память",
|
||||||
"Allow accountFilter parameter": "Разрешить параметр accountFilter",
|
"Allow accountFilter parameter": "Разрешить параметр accountFilter",
|
||||||
"Allow balance redemption": "Разрешить оплату балансом",
|
"Allow balance redemption": "Разрешить оплату балансом",
|
||||||
|
"Allow wallet balance after quota used up": "Разрешить использование баланса кошелька после исчерпания квоты",
|
||||||
|
"Downgrade Group": "Понизить группу",
|
||||||
|
"Downgrade to pre-purchase group": "Понизить до группы до покупки",
|
||||||
|
"Downgrade to this group after the subscription expires": "Понизить до этой группы после истечения подписки",
|
||||||
"Allow Claude beta query passthrough": "Разрешить проброс бета-запросов Claude",
|
"Allow Claude beta query passthrough": "Разрешить проброс бета-запросов Claude",
|
||||||
"Allow clients to query configured ratios via `/api/ratio`.": "Разрешить клиентам запрашивать настроенные соотношения через `/api/ratio`.",
|
"Allow clients to query configured ratios via `/api/ratio`.": "Разрешить клиентам запрашивать настроенные соотношения через `/api/ratio`.",
|
||||||
"Allow HTTP image requests": "Разрешить HTTP-запросы изображений",
|
"Allow HTTP image requests": "Разрешить HTTP-запросы изображений",
|
||||||
|
|||||||
Vendored
+7
@@ -138,6 +138,9 @@
|
|||||||
"Active models": "Mô hình đang hoạt động",
|
"Active models": "Mô hình đang hoạt động",
|
||||||
"active users": "Người dùng tích cực",
|
"active users": "Người dùng tích cực",
|
||||||
"Actual Amount": "Số tiền thực tế",
|
"Actual Amount": "Số tiền thực tế",
|
||||||
|
"Plan Price": "Giá gói",
|
||||||
|
"Plan Quota": "Hạn ngạch gói",
|
||||||
|
"Total quota included in the plan, usable per billing period. 0 means unlimited.": "Tổng hạn ngạch bao gồm trong gói, dùng được mỗi kỳ thanh toán. 0 nghĩa là không giới hạn.",
|
||||||
"Actual Model": "Mô hình thực tế",
|
"Actual Model": "Mô hình thực tế",
|
||||||
"Actual Model:": "Mô hình thực tế:",
|
"Actual Model:": "Mô hình thực tế:",
|
||||||
"Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "Điều chỉnh các yêu cầu có hậu tố `-thinking` sang hành vi suy luận gốc của Anthropic trong khi vẫn giữ tính phí dễ dự đoán.",
|
"Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "Điều chỉnh các yêu cầu có hậu tố `-thinking` sang hành vi suy luận gốc của Anthropic trong khi vẫn giữ tính phí dễ dự đoán.",
|
||||||
@@ -276,6 +279,10 @@
|
|||||||
"Allocated Memory": "Bộ nhớ đã cấp phát",
|
"Allocated Memory": "Bộ nhớ đã cấp phát",
|
||||||
"Allow accountFilter parameter": "Cho phép tham số accountFilter",
|
"Allow accountFilter parameter": "Cho phép tham số accountFilter",
|
||||||
"Allow balance redemption": "Cho phép thanh toán bằng số dư",
|
"Allow balance redemption": "Cho phép thanh toán bằng số dư",
|
||||||
|
"Allow wallet balance after quota used up": "Cho phép dùng số dư ví sau khi dùng hết hạn ngạch",
|
||||||
|
"Downgrade Group": "Nhóm hạ cấp",
|
||||||
|
"Downgrade to pre-purchase group": "Hạ xuống nhóm trước khi mua",
|
||||||
|
"Downgrade to this group after the subscription expires": "Hạ xuống nhóm này sau khi đăng ký hết hạn",
|
||||||
"Allow Claude beta query passthrough": "Cho phép chuyển tiếp truy vấn beta Claude",
|
"Allow Claude beta query passthrough": "Cho phép chuyển tiếp truy vấn beta Claude",
|
||||||
"Allow clients to query configured ratios via `/api/ratio`.": "Cho phép khách hàng truy vấn các tỷ lệ đã cấu hình thông qua `/api/ratio`.",
|
"Allow clients to query configured ratios via `/api/ratio`.": "Cho phép khách hàng truy vấn các tỷ lệ đã cấu hình thông qua `/api/ratio`.",
|
||||||
"Allow HTTP image requests": "Cho phép yêu cầu hình ảnh HTTP",
|
"Allow HTTP image requests": "Cho phép yêu cầu hình ảnh HTTP",
|
||||||
|
|||||||
Vendored
+7
@@ -138,6 +138,9 @@
|
|||||||
"Active models": "活跃模型",
|
"Active models": "活跃模型",
|
||||||
"active users": "活跃用户",
|
"active users": "活跃用户",
|
||||||
"Actual Amount": "实付金额",
|
"Actual Amount": "实付金额",
|
||||||
|
"Plan Price": "套餐价格",
|
||||||
|
"Plan Quota": "套餐额度",
|
||||||
|
"Total quota included in the plan, usable per billing period. 0 means unlimited.": "套餐包含的总额度,每个计费周期可用;0 表示不限量",
|
||||||
"Actual Model": "实际模型",
|
"Actual Model": "实际模型",
|
||||||
"Actual Model:": "实际模型:",
|
"Actual Model:": "实际模型:",
|
||||||
"Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "将带 `-thinking` 后缀的请求适配为 Anthropic 原生思考请求,并保持计费可预测。",
|
"Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "将带 `-thinking` 后缀的请求适配为 Anthropic 原生思考请求,并保持计费可预测。",
|
||||||
@@ -276,6 +279,10 @@
|
|||||||
"Allocated Memory": "已分配内存",
|
"Allocated Memory": "已分配内存",
|
||||||
"Allow accountFilter parameter": "允许 accountFilter 参数",
|
"Allow accountFilter parameter": "允许 accountFilter 参数",
|
||||||
"Allow balance redemption": "允许余额兑换",
|
"Allow balance redemption": "允许余额兑换",
|
||||||
|
"Allow wallet balance after quota used up": "额度用尽后允许使用钱包余额",
|
||||||
|
"Downgrade Group": "降级分组",
|
||||||
|
"Downgrade to pre-purchase group": "降级到购买前分组",
|
||||||
|
"Downgrade to this group after the subscription expires": "订阅过期后降级到该分组",
|
||||||
"Allow Claude beta query passthrough": "允许 Claude beta 查询透传",
|
"Allow Claude beta query passthrough": "允许 Claude beta 查询透传",
|
||||||
"Allow clients to query configured ratios via `/api/ratio`.": "允许客户端通过 `/api/ratio` 查询配置的比例。",
|
"Allow clients to query configured ratios via `/api/ratio`.": "允许客户端通过 `/api/ratio` 查询配置的比例。",
|
||||||
"Allow HTTP image requests": "允许 HTTP 图像请求",
|
"Allow HTTP image requests": "允许 HTTP 图像请求",
|
||||||
|
|||||||
Vendored
+7
@@ -350,6 +350,10 @@ export const STATIC_I18N_KEYS = [
|
|||||||
'Priority',
|
'Priority',
|
||||||
'Payment Channel',
|
'Payment Channel',
|
||||||
'No Upgrade',
|
'No Upgrade',
|
||||||
|
'Downgrade to pre-purchase group',
|
||||||
|
'Downgrade Group',
|
||||||
|
'Downgrade to this group after the subscription expires',
|
||||||
|
'Allow wallet balance after quota used up',
|
||||||
'Unlimited',
|
'Unlimited',
|
||||||
'Update plan info',
|
'Update plan info',
|
||||||
'Create new subscription plan',
|
'Create new subscription plan',
|
||||||
@@ -361,6 +365,9 @@ export const STATIC_I18N_KEYS = [
|
|||||||
'Plan Subtitle',
|
'Plan Subtitle',
|
||||||
'e.g. Suitable for light usage',
|
'e.g. Suitable for light usage',
|
||||||
'Actual Amount',
|
'Actual Amount',
|
||||||
|
'Plan Price',
|
||||||
|
'Plan Quota',
|
||||||
|
'Total quota included in the plan, usable per billing period. 0 means unlimited.',
|
||||||
'Total Quota',
|
'Total Quota',
|
||||||
'0 means unlimited',
|
'0 means unlimited',
|
||||||
'Sort Order',
|
'Sort Order',
|
||||||
|
|||||||
Reference in New Issue
Block a user