Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions common/gin.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,23 @@ func GetContextKeyInt(c *gin.Context, key constant.ContextKey) int {
return c.GetInt(string(key))
}

func GetContextKeyInt64(c *gin.Context, key constant.ContextKey) int64 {
value, ok := c.Get(string(key))
if !ok {
return 0
}
switch v := value.(type) {
case int64:
return v
case int:
return int64(v)
case float64:
return int64(v)
default:
return 0
}
}

func GetContextKeyBool(c *gin.Context, key constant.ContextKey) bool {
return c.GetBool(string(key))
}
Expand Down
12 changes: 6 additions & 6 deletions controller/billing.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ import (
)

func GetSubscription(c *gin.Context) {
var remainQuota int
var usedQuota int
var remainQuota int64
var usedQuota int64
var err error
var token *model.Token
var expiredTime int64
if common.DisplayTokenStatEnabled {
tokenId := c.GetInt("token_id")
token, err = model.GetTokenById(tokenId)
expiredTime = token.ExpiredTime
remainQuota = token.RemainQuota
usedQuota = token.UsedQuota
remainQuota = int64(token.RemainQuota)
usedQuota = int64(token.UsedQuota)
} else {
userId := c.GetInt("id")
remainQuota, err = model.GetUserQuota(userId, false)
Expand Down Expand Up @@ -69,13 +69,13 @@ func GetSubscription(c *gin.Context) {
}

func GetUsage(c *gin.Context) {
var quota int
var quota int64
var err error
var token *model.Token
if common.DisplayTokenStatEnabled {
tokenId := c.GetInt("token_id")
token, err = model.GetTokenById(tokenId)
quota = token.UsedQuota
quota = int64(token.UsedQuota)
} else {
userId := c.GetInt("id")
quota, err = model.GetUserUsedQuota(userId)
Expand Down
14 changes: 7 additions & 7 deletions controller/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ func GenerateAccessToken(c *gin.Context) {
}

type TransferAffQuotaRequest struct {
Quota int `json:"quota" binding:"required"`
Quota int64 `json:"quota" binding:"required"`
}

func TransferAffQuota(c *gin.Context) {
Expand Down Expand Up @@ -991,19 +991,19 @@ func CreateUser(c *gin.Context) {
type ManageRequest struct {
Id int `json:"id"`
Action string `json:"action"`
Value int `json:"value"`
Value int64 `json:"value"`
Mode string `json:"mode"`
}

func isValidQuotaOverride(value int) bool {
return value >= 0 && int64(value) <= maxUserQuotaValue
func isValidQuotaOverride(value int64) bool {
return value >= 0 && value <= maxUserQuotaValue
}

func isValidQuotaAddition(current int, delta int) bool {
if delta <= 0 || int64(delta) > maxUserQuotaValue {
func isValidQuotaAddition(current int64, delta int64) bool {
if delta <= 0 || delta > maxUserQuotaValue {
return false
}
return int64(current) <= maxUserQuotaValue-int64(delta)
return current <= maxUserQuotaValue-delta
}

// ManageUser Only admin user can do this
Expand Down
25 changes: 24 additions & 1 deletion controller/user_setting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,14 +217,37 @@ func TestRegisterConsumesEmailVerificationCode(t *testing.T) {
}

func TestQuotaBoundsValidation(t *testing.T) {
legacyInt32Max := 1<<31 - 1
legacyInt32Max := int64(1<<31 - 1)
aboveLegacyInt32Max := legacyInt32Max + 1
maxQuota := maxUserQuotaValue
overflowingHalfMax := maxQuota/2 + 1

require.True(t, isValidQuotaOverride(0))
require.True(t, isValidQuotaOverride(aboveLegacyInt32Max))
require.True(t, isValidQuotaOverride(maxQuota))
require.False(t, isValidQuotaOverride(-1))

require.True(t, isValidQuotaAddition(aboveLegacyInt32Max, 1))
require.True(t, isValidQuotaAddition(0, aboveLegacyInt32Max))
require.True(t, isValidQuotaAddition(maxQuota-1, 1))
require.True(t, isValidQuotaAddition(0, maxQuota))
require.False(t, isValidQuotaAddition(maxQuota, 1))
require.False(t, isValidQuotaAddition(1, maxQuota))
require.False(t, isValidQuotaAddition(overflowingHalfMax, overflowingHalfMax))
require.False(t, isValidQuotaAddition(maxQuota, maxQuota))
require.False(t, isValidQuotaAddition(0, 0))
}

func TestManageUserRejectsQuotaValueAboveInt64Max(t *testing.T) {
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/user/manage", strings.NewReader(
`{"id":1,"action":"add_quota","mode":"override","value":9223372036854775808}`,
))
ctx.Request.Header.Set("Content-Type", "application/json")

ManageUser(ctx)

require.Equal(t, http.StatusOK, recorder.Code)
require.Contains(t, recorder.Body.String(), `"success":false`)
}
8 changes: 6 additions & 2 deletions logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,11 @@ func logHelper(ctx context.Context, level string, msg string) {
}
}

func LogQuota(quota int) string {
type quotaInteger interface {
~int | ~int64
}

func LogQuota[T quotaInteger](quota T) string {
// 新逻辑:根据额度展示类型输出
q := float64(quota)
switch operation_setting.GetQuotaDisplayType() {
Expand All @@ -146,7 +150,7 @@ func LogQuota(quota int) string {
}
}

func FormatQuota(quota int) string {
func FormatQuota[T quotaInteger](quota T) string {
q := float64(quota)
switch operation_setting.GetQuotaDisplayType() {
case operation_setting.QuotaDisplayTypeCNY:
Expand Down
2 changes: 1 addition & 1 deletion model/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -854,7 +854,7 @@ func EditChannelByTag(tag string, newTag *string, modelMapping *string, models *

func UpdateChannelUsedQuota(id int, quota int) {
if common.BatchUpdateEnabled {
addNewRecord(BatchUpdateTypeChannelUsedQuota, id, quota)
addNewRecord(BatchUpdateTypeChannelUsedQuota, id, int64(quota))
return
}
updateChannelUsedQuota(id, quota)
Expand Down
23 changes: 18 additions & 5 deletions model/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package model

import (
"database/sql"
"errors"
"fmt"
"log"
Expand Down Expand Up @@ -666,6 +667,10 @@ func migrateTokenModelLimitsToText() error {
return nil
}

// migrateUserQuotaColumnsToBigInt runs during startup. On MySQL/PostgreSQL,
// changing large users table columns can hold table-level or rewrite locks for
// a noticeable time, so operators with large deployments should schedule this
// upgrade off-peak or run an equivalent online-DDL migration before booting.
func migrateUserQuotaColumnsToBigInt() error {
if DB == nil || common.UsingSQLite || !DB.Migrator().HasTable(&User{}) {
return nil
Expand All @@ -692,15 +697,23 @@ func migrateUserQuotaColumnsToBigInt() error {
alterSQL = fmt.Sprintf(`ALTER TABLE "%s" ALTER COLUMN "%s" TYPE bigint USING "%s"::bigint`,
tableName, columnName, columnName)
} else if common.UsingMySQL {
var columnType string
if err := DB.Raw(`SELECT COLUMN_TYPE FROM information_schema.columns
var columnMetadata struct {
ColumnType string `gorm:"column:column_type"`
IsNullable string `gorm:"column:is_nullable"`
ColumnDefault sql.NullString `gorm:"column:column_default"`
}
if err := DB.Raw(`SELECT COLUMN_TYPE AS column_type, IS_NULLABLE AS is_nullable, COLUMN_DEFAULT AS column_default
FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?`,
tableName, columnName).Scan(&columnType).Error; err != nil {
tableName, columnName).Scan(&columnMetadata).Error; err != nil {
common.SysLog(fmt.Sprintf("Warning: failed to query metadata for %s.%s: %v", tableName, columnName, err))
} else if strings.HasPrefix(strings.ToLower(columnType), "bigint") {
} else if strings.HasPrefix(strings.ToLower(strings.TrimSpace(columnMetadata.ColumnType)), "bigint") &&
strings.EqualFold(columnMetadata.IsNullable, "NO") &&
columnMetadata.ColumnDefault.Valid &&
strings.TrimSpace(columnMetadata.ColumnDefault.String) == "0" {
continue
}
alterSQL = fmt.Sprintf("ALTER TABLE `%s` MODIFY COLUMN `%s` bigint DEFAULT 0", tableName, columnName)
alterSQL = fmt.Sprintf("ALTER TABLE `%s` MODIFY COLUMN `%s` bigint NOT NULL DEFAULT 0", tableName, columnName)
}

if alterSQL == "" {
Expand Down
16 changes: 8 additions & 8 deletions model/payment_method_guard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func insertUserForPaymentGuardTest(t *testing.T, id int, quota int) {
Id: id,
Username: "payment_guard_user",
Status: common.UserStatusEnabled,
Quota: quota,
Quota: int64(quota),
}
require.NoError(t, DB.Create(user).Error)
}
Expand Down Expand Up @@ -89,7 +89,7 @@ func countTopUpsForPaymentGuardTest(t *testing.T, tradeNo string) int64 {
return count
}

func getUserQuotaForPaymentGuardTest(t *testing.T, userID int) int {
func getUserQuotaForPaymentGuardTest(t *testing.T, userID int) int64 {
t.Helper()
var user User
require.NoError(t, DB.Select("quota").Where("id = ?", userID).First(&user).Error)
Expand All @@ -108,7 +108,7 @@ func TestRechargeWaffoPancake_RejectsMismatchedPaymentMethod(t *testing.T) {
topUp := GetTopUpByTradeNo("waffo-pancake-guard")
require.NotNil(t, topUp)
assert.Equal(t, common.TopUpStatusPending, topUp.Status)
assert.Equal(t, 0, getUserQuotaForPaymentGuardTest(t, 101))
assert.EqualValues(t, 0, getUserQuotaForPaymentGuardTest(t, 101))
}

func TestUpdatePendingTopUpStatus_RejectsMismatchedPaymentProvider(t *testing.T) {
Expand Down Expand Up @@ -212,7 +212,7 @@ func TestPurchaseSubscriptionWithBalance_InsufficientQuotaDoesNotOverdraw(t *tes

err = PurchaseSubscriptionWithBalance(505, plan.Id)
require.Error(t, err)
assert.Equal(t, requiredQuota-1, getUserQuotaForPaymentGuardTest(t, 505))
assert.EqualValues(t, requiredQuota-1, getUserQuotaForPaymentGuardTest(t, 505))
assert.Zero(t, countUserSubscriptionsForPaymentGuardTest(t, 505))
}

Expand All @@ -235,7 +235,7 @@ func TestRedeem_UsedCodeDoesNotDoubleCredit(t *testing.T) {
quota, err = Redeem("redeem-guard-code", 606)
require.ErrorIs(t, err, ErrRedeemFailed)
assert.Zero(t, quota)
assert.Equal(t, 123, getUserQuotaForPaymentGuardTest(t, 606))
assert.EqualValues(t, 123, getUserQuotaForPaymentGuardTest(t, 606))

var reloaded Redemption
require.NoError(t, DB.Where("id = ?", redemption.Id).First(&reloaded).Error)
Expand Down Expand Up @@ -282,7 +282,7 @@ func TestRedeemRejectsNonPositiveQuotaWithoutUsingCode(t *testing.T) {
quota, err := Redeem("redeem-zero-quota", 609)
require.ErrorIs(t, err, ErrRedeemFailed)
assert.Zero(t, quota)
assert.Equal(t, 0, getUserQuotaForPaymentGuardTest(t, 609))
assert.EqualValues(t, 0, getUserQuotaForPaymentGuardTest(t, 609))

var reloaded Redemption
require.NoError(t, DB.Where("id = ?", redemption.Id).First(&reloaded).Error)
Expand All @@ -309,7 +309,7 @@ func TestRechargeCreemRejectsZeroQuotaBeforeCompletingOrder(t *testing.T) {
err := RechargeCreem("creem-zero-quota", "", "", "127.0.0.1")
require.Error(t, err)
assert.Equal(t, common.TopUpStatusPending, getTopUpStatusForPaymentGuardTest(t, "creem-zero-quota"))
assert.Equal(t, 0, getUserQuotaForPaymentGuardTest(t, 610))
assert.EqualValues(t, 0, getUserQuotaForPaymentGuardTest(t, 610))
}

func TestRechargeCreemSkipsDuplicateCustomerEmailBinding(t *testing.T) {
Expand Down Expand Up @@ -337,7 +337,7 @@ func TestRechargeCreemSkipsDuplicateCustomerEmailBinding(t *testing.T) {
require.NoError(t, DB.First(&got, 612).Error)
assert.Empty(t, got.Email)
assert.Empty(t, got.NormalizedEmail)
assert.Equal(t, 2, got.Quota)
assert.EqualValues(t, 2, got.Quota)
assert.Equal(t, common.TopUpStatusSuccess, getTopUpStatusForPaymentGuardTest(t, "creem-duplicate-email"))

count, err := CountUsersByEmail("taken@example.com")
Expand Down
2 changes: 1 addition & 1 deletion model/subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,7 @@ func PurchaseSubscriptionWithBalance(userId int, planId int) error {
if err := withRowLock(tx).Where("id = ?", userId).First(&user).Error; err != nil {
return err
}
if requiredQuota > 0 && user.Quota < requiredQuota {
if requiredQuota > 0 && user.Quota < int64(requiredQuota) {
return errors.New("余额不足")
}
if requiredQuota > 0 {
Expand Down
4 changes: 2 additions & 2 deletions model/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ func IncreaseTokenQuota(tokenId int, key string, quota int) (err error) {
})
}
if common.BatchUpdateEnabled {
addNewRecord(BatchUpdateTypeTokenQuota, tokenId, quota)
addNewRecord(BatchUpdateTypeTokenQuota, tokenId, int64(quota))
return nil
}
return increaseTokenQuota(tokenId, quota)
Expand Down Expand Up @@ -443,7 +443,7 @@ func DecreaseTokenQuota(id int, key string, quota int) (err error) {
})
}
if common.BatchUpdateEnabled {
addNewRecord(BatchUpdateTypeTokenQuota, id, -quota)
addNewRecord(BatchUpdateTypeTokenQuota, id, int64(-quota))
return nil
}
return decreaseTokenQuota(id, quota)
Expand Down
Loading
Loading