Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
47ba9d2c
authored
Aug 14, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(topup): guard wallet quota during recharge
parent
bbf67df0
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
238 additions
and
62 deletions
+238
-62
controller/topup.go
+30
-14
controller/topup_creem.go
+8
-4
controller/topup_quota_limit_test.go
+61
-7
controller/topup_stripe.go
+8
-6
controller/topup_waffo.go
+4
-4
controller/topup_waffo_pancake.go
+4
-4
model/payment_method_guard_test.go
+45
-0
model/topup.go
+78
-23
No files found.
controller/topup.go
View file @
47ba9d2c
...
@@ -216,31 +216,47 @@ func getMaxTopUpAmount() int64 {
...
@@ -216,31 +216,47 @@ func getMaxTopUpAmount() int64 {
return
maxStoredAmount
.
IntPart
()
return
maxStoredAmount
.
IntPart
()
}
}
func
validateCreditedQuota
(
quota
decimal
.
Decimal
)
error
{
func
validateCreditedQuota
(
quota
decimal
.
Decimal
)
(
int
,
error
)
{
value
,
err
:=
common
.
QuotaFromDecimalStrict
(
quota
)
value
,
err
:=
common
.
QuotaFromDecimalStrict
(
quota
)
if
err
!=
nil
{
if
err
!=
nil
{
return
errors
.
New
(
"充值额度超出系统可表示范围"
)
return
0
,
errors
.
New
(
"充值额度超出系统可表示范围"
)
}
}
if
value
<=
0
{
if
value
<=
0
{
return
errors
.
New
(
"充值额度必须大于 0"
)
return
0
,
errors
.
New
(
"充值额度必须大于 0"
)
}
}
return
nil
return
value
,
nil
}
}
func
validateTopUpQuota
(
amount
int64
)
error
{
func
validateTopUpQuota
(
amount
int64
)
(
int
,
error
)
{
quota
,
err
:=
getTopUpQuota
(
amount
)
quota
,
err
:=
getTopUpQuota
(
amount
)
if
err
==
nil
&&
quota
>
0
{
if
err
==
nil
&&
quota
>
0
{
return
nil
return
quota
,
nil
}
}
maxAmount
:=
getMaxTopUpAmount
()
maxAmount
:=
getMaxTopUpAmount
()
if
maxAmount
>
0
&&
amount
>
maxAmount
{
if
maxAmount
>
0
&&
amount
>
maxAmount
{
return
fmt
.
Errorf
(
"单笔充值数量不能大于 %d"
,
maxAmount
)
return
0
,
fmt
.
Errorf
(
"单笔充值数量不能大于 %d"
,
maxAmount
)
}
return
0
,
errors
.
New
(
"充值数量无效"
)
}
func
rejectInvalidCreditedQuota
(
c
*
gin
.
Context
,
userId
int
,
quota
decimal
.
Decimal
)
bool
{
creditedQuota
,
err
:=
validateCreditedQuota
(
quota
)
if
err
==
nil
{
err
=
model
.
ValidateTopUpQuotaCapacity
(
userId
,
creditedQuota
)
}
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
err
.
Error
()})
return
true
}
}
return
errors
.
New
(
"充值数量无效"
)
return
false
}
}
func
rejectInvalidTopUpQuota
(
c
*
gin
.
Context
,
amount
int64
)
bool
{
func
rejectInvalidTopUpQuota
(
c
*
gin
.
Context
,
userId
int
,
amount
int64
)
bool
{
if
err
:=
validateTopUpQuota
(
amount
);
err
!=
nil
{
creditedQuota
,
err
:=
validateTopUpQuota
(
amount
)
if
err
==
nil
{
err
=
model
.
ValidateTopUpQuotaCapacity
(
userId
,
creditedQuota
)
}
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
err
.
Error
()})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
err
.
Error
()})
return
true
return
true
}
}
...
@@ -258,11 +274,11 @@ func RequestEpay(c *gin.Context) {
...
@@ -258,11 +274,11 @@ func RequestEpay(c *gin.Context) {
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
fmt
.
Sprintf
(
"充值数量不能小于 %d"
,
getMinTopup
())})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
fmt
.
Sprintf
(
"充值数量不能小于 %d"
,
getMinTopup
())})
return
return
}
}
if
rejectInvalidTopUpQuota
(
c
,
req
.
Amount
)
{
id
:=
c
.
GetInt
(
"id"
)
if
rejectInvalidTopUpQuota
(
c
,
id
,
req
.
Amount
)
{
return
return
}
}
id
:=
c
.
GetInt
(
"id"
)
group
,
err
:=
model
.
GetUserGroup
(
id
,
true
)
group
,
err
:=
model
.
GetUserGroup
(
id
,
true
)
if
err
!=
nil
{
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"获取用户分组失败"
})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"获取用户分组失败"
})
...
@@ -475,10 +491,10 @@ func RequestAmount(c *gin.Context) {
...
@@ -475,10 +491,10 @@ func RequestAmount(c *gin.Context) {
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
fmt
.
Sprintf
(
"充值数量不能小于 %d"
,
getMinTopup
())})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
fmt
.
Sprintf
(
"充值数量不能小于 %d"
,
getMinTopup
())})
return
return
}
}
if
rejectInvalidTopUpQuota
(
c
,
req
.
Amount
)
{
id
:=
c
.
GetInt
(
"id"
)
if
rejectInvalidTopUpQuota
(
c
,
id
,
req
.
Amount
)
{
return
return
}
}
id
:=
c
.
GetInt
(
"id"
)
group
,
err
:=
model
.
GetUserGroup
(
id
,
true
)
group
,
err
:=
model
.
GetUserGroup
(
id
,
true
)
if
err
!=
nil
{
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"获取用户分组失败"
})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"获取用户分组失败"
})
...
...
controller/topup_creem.go
View file @
47ba9d2c
...
@@ -97,13 +97,17 @@ func (*CreemAdaptor) RequestPay(c *gin.Context, req *CreemPayRequest) {
...
@@ -97,13 +97,17 @@ func (*CreemAdaptor) RequestPay(c *gin.Context, req *CreemPayRequest) {
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"产品不存在"
})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"产品不存在"
})
return
return
}
}
if
err
:=
validateCreditedQuota
(
decimal
.
NewFromInt
(
selectedProduct
.
Quota
));
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
err
.
Error
()})
id
:=
c
.
GetInt
(
"id"
)
if
rejectInvalidCreditedQuota
(
c
,
id
,
decimal
.
NewFromInt
(
selectedProduct
.
Quota
))
{
return
return
}
}
id
:=
c
.
GetInt
(
"id"
)
user
,
err
:=
model
.
GetUserById
(
id
,
false
)
user
,
_
:=
model
.
GetUserById
(
id
,
false
)
if
err
!=
nil
||
user
==
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"用户不存在"
})
return
}
// 生成唯一的订单引用ID
// 生成唯一的订单引用ID
reference
:=
fmt
.
Sprintf
(
"creem-api-ref-%d-%d-%s"
,
user
.
Id
,
time
.
Now
()
.
UnixMilli
(),
randstr
.
String
(
4
))
reference
:=
fmt
.
Sprintf
(
"creem-api-ref-%d-%d-%s"
,
user
.
Id
,
time
.
Now
()
.
UnixMilli
(),
randstr
.
String
(
4
))
...
...
controller/topup_quota_limit_test.go
View file @
47ba9d2c
...
@@ -7,11 +7,14 @@ import (
...
@@ -7,11 +7,14 @@ import (
"testing"
"testing"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"github.com/shopspring/decimal"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
)
func
TestTopUpQuotaValidation
(
t
*
testing
.
T
)
{
func
TestTopUpQuotaValidation
(
t
*
testing
.
T
)
{
...
@@ -84,8 +87,9 @@ func TestValidateTopUpQuotaReturnsMaximumAmount(t *testing.T) {
...
@@ -84,8 +87,9 @@ func TestValidateTopUpQuotaReturnsMaximumAmount(t *testing.T) {
Div
(
decimal
.
NewFromFloat
(
common
.
QuotaPerUnit
))
.
Div
(
decimal
.
NewFromFloat
(
common
.
QuotaPerUnit
))
.
Floor
()
.
IntPart
()
Floor
()
.
IntPart
()
require
.
NoError
(
t
,
validateTopUpQuota
(
maxAmount
))
_
,
err
:=
validateTopUpQuota
(
maxAmount
)
err
:=
validateTopUpQuota
(
maxAmount
+
1
)
require
.
NoError
(
t
,
err
)
_
,
err
=
validateTopUpQuota
(
maxAmount
+
1
)
require
.
EqualError
(
t
,
err
,
"单笔充值数量不能大于 4294"
)
require
.
EqualError
(
t
,
err
,
"单笔充值数量不能大于 4294"
)
}
}
...
@@ -115,12 +119,60 @@ func TestRequestAmountRejectsTopUpThatCannotBeSettled(t *testing.T) {
...
@@ -115,12 +119,60 @@ func TestRequestAmountRejectsTopUpThatCannotBeSettled(t *testing.T) {
assert
.
JSONEq
(
t
,
`{"message":"error","data":"单笔充值数量不能大于 4294"}`
,
recorder
.
Body
.
String
())
assert
.
JSONEq
(
t
,
`{"message":"error","data":"单笔充值数量不能大于 4294"}`
,
recorder
.
Body
.
String
())
}
}
func
TestRequestAmountRejectsTopUpThatWouldOverflowWallet
(
t
*
testing
.
T
)
{
oldQuotaPerUnit
:=
common
.
QuotaPerUnit
oldDisplayType
:=
operation_setting
.
GetGeneralSetting
()
.
QuotaDisplayType
oldDB
:=
model
.
DB
common
.
QuotaPerUnit
=
500000
operation_setting
.
GetGeneralSetting
()
.
QuotaDisplayType
=
operation_setting
.
QuotaDisplayTypeUSD
db
,
err
:=
gorm
.
Open
(
sqlite
.
Open
(
":memory:"
),
&
gorm
.
Config
{})
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
db
.
AutoMigrate
(
&
model
.
User
{}))
model
.
DB
=
db
t
.
Cleanup
(
func
()
{
common
.
QuotaPerUnit
=
oldQuotaPerUnit
operation_setting
.
GetGeneralSetting
()
.
QuotaDisplayType
=
oldDisplayType
model
.
DB
=
oldDB
sqlDB
,
dbErr
:=
db
.
DB
()
if
dbErr
==
nil
{
require
.
NoError
(
t
,
sqlDB
.
Close
())
}
})
require
.
NoError
(
t
,
model
.
DB
.
Create
(
&
model
.
User
{
Id
:
42
,
Username
:
"topup_capacity_user"
,
Quota
:
1
_000_000
,
Status
:
common
.
UserStatusEnabled
,
})
.
Error
)
gin
.
SetMode
(
gin
.
TestMode
)
recorder
:=
httptest
.
NewRecorder
()
ctx
,
_
:=
gin
.
CreateTestContext
(
recorder
)
ctx
.
Set
(
"id"
,
42
)
ctx
.
Request
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/user/amount"
,
strings
.
NewReader
(
`{"amount":4294}`
),
)
ctx
.
Request
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
RequestAmount
(
ctx
)
assert
.
Equal
(
t
,
http
.
StatusOK
,
recorder
.
Code
)
assert
.
JSONEq
(
t
,
`{"message":"error","data":"top-up quota limit exceeded"}`
,
recorder
.
Body
.
String
())
}
func
TestValidateCreditedQuotaRejectsOverflow
(
t
*
testing
.
T
)
{
func
TestValidateCreditedQuotaRejectsOverflow
(
t
*
testing
.
T
)
{
require
.
NoError
(
t
,
validateCreditedQuota
(
decimal
.
NewFromInt
(
common
.
MaxQuota
-
1
)))
_
,
err
:=
validateCreditedQuota
(
decimal
.
NewFromInt
(
common
.
MaxQuota
-
1
))
require
.
EqualError
(
t
,
validateCreditedQuota
(
decimal
.
Zero
),
"充值额度必须大于 0"
)
require
.
NoError
(
t
,
err
)
_
,
err
=
validateCreditedQuota
(
decimal
.
Zero
)
require
.
EqualError
(
t
,
err
,
"充值额度必须大于 0"
)
_
,
err
=
validateCreditedQuota
(
decimal
.
NewFromInt
(
common
.
MaxQuota
))
require
.
EqualError
(
require
.
EqualError
(
t
,
t
,
validateCreditedQuota
(
decimal
.
NewFromInt
(
common
.
MaxQuota
))
,
err
,
"充值额度超出系统可表示范围"
,
"充值额度超出系统可表示范围"
,
)
)
}
}
...
@@ -135,8 +187,10 @@ func TestStripeCreditedQuotaIncludesGroupRatio(t *testing.T) {
...
@@ -135,8 +187,10 @@ func TestStripeCreditedQuotaIncludesGroupRatio(t *testing.T) {
require
.
NoError
(
t
,
common
.
UpdateTopupGroupRatioByJSONString
(
oldTopupGroupRatio
))
require
.
NoError
(
t
,
common
.
UpdateTopupGroupRatioByJSONString
(
oldTopupGroupRatio
))
})
})
require
.
NoError
(
t
,
validateCreditedQuota
(
getStripeCreditedQuota
(
2147
,
"vip"
)))
_
,
err
:=
validateCreditedQuota
(
getStripeCreditedQuota
(
2147
,
"vip"
))
require
.
Error
(
t
,
validateCreditedQuota
(
getStripeCreditedQuota
(
2148
,
"vip"
)))
require
.
NoError
(
t
,
err
)
_
,
err
=
validateCreditedQuota
(
getStripeCreditedQuota
(
2148
,
"vip"
))
require
.
Error
(
t
,
err
)
require
.
NoError
(
t
,
common
.
UpdateTopupGroupRatioByJSONString
(
`{"free":0}`
))
require
.
NoError
(
t
,
common
.
UpdateTopupGroupRatioByJSONString
(
`{"free":0}`
))
assert
.
True
(
t
,
decimal
.
NewFromInt
(
500000
)
.
Equal
(
getStripeCreditedQuota
(
1
,
"free"
)))
assert
.
True
(
t
,
decimal
.
NewFromInt
(
500000
)
.
Equal
(
getStripeCreditedQuota
(
1
,
"free"
)))
...
...
controller/topup_stripe.go
View file @
47ba9d2c
...
@@ -58,8 +58,7 @@ func (*StripeAdaptor) RequestAmount(c *gin.Context, req *StripePayRequest) {
...
@@ -58,8 +58,7 @@ func (*StripeAdaptor) RequestAmount(c *gin.Context, req *StripePayRequest) {
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"获取用户分组失败"
})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"获取用户分组失败"
})
return
return
}
}
if
err
:=
validateCreditedQuota
(
getStripeCreditedQuota
(
req
.
Amount
,
group
));
err
!=
nil
{
if
rejectInvalidCreditedQuota
(
c
,
id
,
getStripeCreditedQuota
(
req
.
Amount
,
group
))
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
err
.
Error
()})
return
return
}
}
payMoney
:=
getStripePayMoney
(
float64
(
req
.
Amount
),
group
)
payMoney
:=
getStripePayMoney
(
float64
(
req
.
Amount
),
group
)
...
@@ -95,12 +94,15 @@ func (*StripeAdaptor) RequestPay(c *gin.Context, req *StripePayRequest) {
...
@@ -95,12 +94,15 @@ func (*StripeAdaptor) RequestPay(c *gin.Context, req *StripePayRequest) {
}
}
id
:=
c
.
GetInt
(
"id"
)
id
:=
c
.
GetInt
(
"id"
)
user
,
_
:=
model
.
GetUserById
(
id
,
false
)
user
,
err
:=
model
.
GetUserById
(
id
,
false
)
if
err
!=
nil
||
user
==
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"用户不存在"
})
return
}
chargedMoney
:=
GetChargedAmount
(
float64
(
req
.
Amount
),
*
user
)
chargedMoney
:=
GetChargedAmount
(
float64
(
req
.
Amount
),
*
user
)
if
err
:=
validateCreditedQuota
(
if
rejectInvalidCreditedQuota
(
c
,
id
,
decimal
.
NewFromFloat
(
chargedMoney
)
.
Mul
(
decimal
.
NewFromFloat
(
common
.
QuotaPerUnit
)),
decimal
.
NewFromFloat
(
chargedMoney
)
.
Mul
(
decimal
.
NewFromFloat
(
common
.
QuotaPerUnit
)),
);
err
!=
nil
{
)
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
err
.
Error
()})
return
return
}
}
...
...
controller/topup_waffo.go
View file @
47ba9d2c
...
@@ -123,11 +123,11 @@ func RequestWaffoAmount(c *gin.Context) {
...
@@ -123,11 +123,11 @@ func RequestWaffoAmount(c *gin.Context) {
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
fmt
.
Sprintf
(
"充值数量不能小于 %d"
,
waffoMinTopup
)})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
fmt
.
Sprintf
(
"充值数量不能小于 %d"
,
waffoMinTopup
)})
return
return
}
}
if
rejectInvalidTopUpQuota
(
c
,
req
.
Amount
)
{
id
:=
c
.
GetInt
(
"id"
)
if
rejectInvalidTopUpQuota
(
c
,
id
,
req
.
Amount
)
{
return
return
}
}
id
:=
c
.
GetInt
(
"id"
)
group
,
err
:=
model
.
GetUserGroup
(
id
,
true
)
group
,
err
:=
model
.
GetUserGroup
(
id
,
true
)
if
err
!=
nil
{
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"获取用户分组失败"
})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"获取用户分组失败"
})
...
@@ -160,11 +160,11 @@ func RequestWaffoPay(c *gin.Context) {
...
@@ -160,11 +160,11 @@ func RequestWaffoPay(c *gin.Context) {
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
fmt
.
Sprintf
(
"充值数量不能小于 %d"
,
waffoMinTopup
)})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
fmt
.
Sprintf
(
"充值数量不能小于 %d"
,
waffoMinTopup
)})
return
return
}
}
if
rejectInvalidTopUpQuota
(
c
,
req
.
Amount
)
{
id
:=
c
.
GetInt
(
"id"
)
if
rejectInvalidTopUpQuota
(
c
,
id
,
req
.
Amount
)
{
return
return
}
}
id
:=
c
.
GetInt
(
"id"
)
user
,
err
:=
model
.
GetUserById
(
id
,
false
)
user
,
err
:=
model
.
GetUserById
(
id
,
false
)
if
err
!=
nil
||
user
==
nil
{
if
err
!=
nil
||
user
==
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"用户不存在"
})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"用户不存在"
})
...
...
controller/topup_waffo_pancake.go
View file @
47ba9d2c
...
@@ -33,11 +33,11 @@ func RequestWaffoPancakeAmount(c *gin.Context) {
...
@@ -33,11 +33,11 @@ func RequestWaffoPancakeAmount(c *gin.Context) {
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
fmt
.
Sprintf
(
"充值数量不能小于 %d"
,
setting
.
WaffoPancakeMinTopUp
)})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
fmt
.
Sprintf
(
"充值数量不能小于 %d"
,
setting
.
WaffoPancakeMinTopUp
)})
return
return
}
}
if
rejectInvalidTopUpQuota
(
c
,
req
.
Amount
)
{
id
:=
c
.
GetInt
(
"id"
)
if
rejectInvalidTopUpQuota
(
c
,
id
,
req
.
Amount
)
{
return
return
}
}
id
:=
c
.
GetInt
(
"id"
)
group
,
err
:=
model
.
GetUserGroup
(
id
,
true
)
group
,
err
:=
model
.
GetUserGroup
(
id
,
true
)
if
err
!=
nil
{
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"获取用户分组失败"
})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"获取用户分组失败"
})
...
@@ -354,11 +354,11 @@ func RequestWaffoPancakePay(c *gin.Context) {
...
@@ -354,11 +354,11 @@ func RequestWaffoPancakePay(c *gin.Context) {
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
fmt
.
Sprintf
(
"充值数量不能小于 %d"
,
setting
.
WaffoPancakeMinTopUp
)})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
fmt
.
Sprintf
(
"充值数量不能小于 %d"
,
setting
.
WaffoPancakeMinTopUp
)})
return
return
}
}
if
rejectInvalidTopUpQuota
(
c
,
req
.
Amount
)
{
id
:=
c
.
GetInt
(
"id"
)
if
rejectInvalidTopUpQuota
(
c
,
id
,
req
.
Amount
)
{
return
return
}
}
id
:=
c
.
GetInt
(
"id"
)
user
,
err
:=
model
.
GetUserById
(
id
,
false
)
user
,
err
:=
model
.
GetUserById
(
id
,
false
)
if
err
!=
nil
||
user
==
nil
{
if
err
!=
nil
||
user
==
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"用户不存在"
})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"用户不存在"
})
...
...
model/payment_method_guard_test.go
View file @
47ba9d2c
...
@@ -308,3 +308,48 @@ func TestRechargeEpayRejectsQuotaOverflowBeforeCompletingOrder(t *testing.T) {
...
@@ -308,3 +308,48 @@ func TestRechargeEpayRejectsQuotaOverflowBeforeCompletingOrder(t *testing.T) {
assert
.
Equal
(
t
,
3
,
getUserQuotaForPaymentGuardTest
(
t
,
user
.
Id
))
assert
.
Equal
(
t
,
3
,
getUserQuotaForPaymentGuardTest
(
t
,
user
.
Id
))
assert
.
Equal
(
t
,
common
.
TopUpStatusPending
,
getTopUpStatusForPaymentGuardTest
(
t
,
order
.
TradeNo
))
assert
.
Equal
(
t
,
common
.
TopUpStatusPending
,
getTopUpStatusForPaymentGuardTest
(
t
,
order
.
TradeNo
))
}
}
func
TestRechargeEpayEnforcesFinalWalletQuotaLimit
(
t
*
testing
.
T
)
{
oldQuotaPerUnit
:=
common
.
QuotaPerUnit
common
.
QuotaPerUnit
=
500000
t
.
Cleanup
(
func
()
{
common
.
QuotaPerUnit
=
oldQuotaPerUnit
})
testCases
:=
[]
struct
{
name
string
currentQuota
int
wantErr
bool
wantQuota
int
wantStatus
string
}{
{
name
:
"allows exact highest representable wallet balance"
,
currentQuota
:
common
.
MaxQuota
-
1
-
1
_000_000
,
wantQuota
:
common
.
MaxQuota
-
1
,
wantStatus
:
common
.
TopUpStatusSuccess
,
},
{
name
:
"rejects balance above int32 quota domain"
,
currentQuota
:
common
.
MaxQuota
-
1
_000_000
,
wantErr
:
true
,
wantQuota
:
common
.
MaxQuota
-
1
_000_000
,
wantStatus
:
common
.
TopUpStatusPending
,
},
}
for
_
,
tc
:=
range
testCases
{
t
.
Run
(
tc
.
name
,
func
(
t
*
testing
.
T
)
{
truncateTables
(
t
)
user
:=
insertUserForPaymentGuardTest
(
t
,
506
,
tc
.
currentQuota
)
order
:=
createEpayTestOrder
(
t
,
user
.
Id
,
"EPAYTESTWALLETLIMIT"
,
PaymentProviderEpay
,
common
.
TopUpStatusPending
)
_
,
err
:=
RechargeEpay
(
order
.
TradeNo
,
"alipay"
,
"127.0.0.1"
)
if
tc
.
wantErr
{
require
.
ErrorIs
(
t
,
err
,
ErrTopUpQuotaLimitExceeded
)
}
else
{
require
.
NoError
(
t
,
err
)
}
assert
.
Equal
(
t
,
tc
.
wantQuota
,
getUserQuotaForPaymentGuardTest
(
t
,
user
.
Id
))
assert
.
Equal
(
t
,
tc
.
wantStatus
,
getTopUpStatusForPaymentGuardTest
(
t
,
order
.
TradeNo
))
})
}
}
model/topup.go
View file @
47ba9d2c
...
@@ -45,6 +45,8 @@ var (
...
@@ -45,6 +45,8 @@ var (
ErrPaymentMethodMismatch
=
errors
.
New
(
"payment method mismatch"
)
ErrPaymentMethodMismatch
=
errors
.
New
(
"payment method mismatch"
)
ErrTopUpNotFound
=
errors
.
New
(
"topup not found"
)
ErrTopUpNotFound
=
errors
.
New
(
"topup not found"
)
ErrTopUpStatusInvalid
=
errors
.
New
(
"topup status invalid"
)
ErrTopUpStatusInvalid
=
errors
.
New
(
"topup status invalid"
)
ErrInvalidTopUpQuota
=
errors
.
New
(
"invalid top-up quota"
)
ErrTopUpQuotaLimitExceeded
=
errors
.
New
(
"top-up quota limit exceeded"
)
)
)
func
(
topUp
*
TopUp
)
Insert
()
error
{
func
(
topUp
*
TopUp
)
Insert
()
error
{
...
@@ -53,6 +55,67 @@ func (topUp *TopUp) Insert() error {
...
@@ -53,6 +55,67 @@ func (topUp *TopUp) Insert() error {
return
err
return
err
}
}
func
topUpQuotaMaxCurrent
(
creditedQuota
int
)
(
int
,
error
)
{
if
creditedQuota
<=
0
||
creditedQuota
>=
common
.
MaxQuota
{
return
0
,
ErrInvalidTopUpQuota
}
return
common
.
MaxQuota
-
1
-
creditedQuota
,
nil
}
// ValidateTopUpQuotaCapacity performs the user-facing pre-payment check. The
// settlement path repeats the same invariant with an atomic conditional
// update, because the wallet balance can change after checkout creation.
func
ValidateTopUpQuotaCapacity
(
userId
int
,
creditedQuota
int
)
error
{
maxCurrentQuota
,
err
:=
topUpQuotaMaxCurrent
(
creditedQuota
)
if
err
!=
nil
{
return
err
}
var
user
User
if
err
:=
DB
.
Select
(
"quota"
)
.
Where
(
"id = ?"
,
userId
)
.
First
(
&
user
)
.
Error
;
err
!=
nil
{
return
err
}
if
user
.
Quota
>
maxCurrentQuota
{
return
ErrTopUpQuotaLimitExceeded
}
return
nil
}
// creditTopUpQuota atomically enforces the int32 wallet ceiling while adding
// quota. Keeping the predicate and increment in one UPDATE prevents two
// concurrent callbacks from both passing a separate read/check.
func
creditTopUpQuota
(
tx
*
gorm
.
DB
,
userId
int
,
creditedQuota
int
,
updates
map
[
string
]
interface
{})
error
{
maxCurrentQuota
,
err
:=
topUpQuotaMaxCurrent
(
creditedQuota
)
if
err
!=
nil
{
return
err
}
updateFields
:=
make
(
map
[
string
]
interface
{},
len
(
updates
)
+
1
)
for
key
,
value
:=
range
updates
{
updateFields
[
key
]
=
value
}
updateFields
[
"quota"
]
=
gorm
.
Expr
(
"quota + ?"
,
creditedQuota
)
result
:=
tx
.
Model
(
&
User
{})
.
Where
(
"id = ? AND quota <= ?"
,
userId
,
maxCurrentQuota
)
.
Updates
(
updateFields
)
if
result
.
Error
!=
nil
{
return
result
.
Error
}
if
result
.
RowsAffected
==
1
{
return
nil
}
var
count
int64
if
err
:=
tx
.
Model
(
&
User
{})
.
Where
(
"id = ?"
,
userId
)
.
Count
(
&
count
)
.
Error
;
err
!=
nil
{
return
err
}
if
count
==
0
{
return
gorm
.
ErrRecordNotFound
}
return
ErrTopUpQuotaLimitExceeded
}
func
(
topUp
*
TopUp
)
Update
()
error
{
func
(
topUp
*
TopUp
)
Update
()
error
{
var
err
error
var
err
error
err
=
DB
.
Save
(
topUp
)
.
Error
err
=
DB
.
Save
(
topUp
)
.
Error
...
@@ -144,21 +207,14 @@ func RechargeEpay(tradeNo string, actualPaymentMethod string, callerIp string) (
...
@@ -144,21 +207,14 @@ func RechargeEpay(tradeNo string, actualPaymentMethod string, callerIp string) (
decimal
.
NewFromInt
(
topUp
.
Amount
)
.
Mul
(
decimal
.
NewFromFloat
(
common
.
QuotaPerUnit
)),
decimal
.
NewFromInt
(
topUp
.
Amount
)
.
Mul
(
decimal
.
NewFromFloat
(
common
.
QuotaPerUnit
)),
)
)
if
quotaErr
!=
nil
||
quotaToAdd
<=
0
{
if
quotaErr
!=
nil
||
quotaToAdd
<=
0
{
return
errors
.
New
(
"无效的充值额度"
)
return
ErrInvalidTopUpQuota
}
}
topUp
.
CompleteTime
=
common
.
GetTimestamp
()
topUp
.
CompleteTime
=
common
.
GetTimestamp
()
topUp
.
Status
=
common
.
TopUpStatusSuccess
topUp
.
Status
=
common
.
TopUpStatusSuccess
if
err
:=
tx
.
Save
(
topUp
)
.
Error
;
err
!=
nil
{
if
err
:=
tx
.
Save
(
topUp
)
.
Error
;
err
!=
nil
{
return
err
return
err
}
}
result
:=
tx
.
Model
(
&
User
{})
.
Where
(
"id = ?"
,
topUp
.
UserId
)
.
Update
(
"quota"
,
gorm
.
Expr
(
"quota + ?"
,
quotaToAdd
))
return
creditTopUpQuota
(
tx
,
topUp
.
UserId
,
quotaToAdd
,
nil
)
if
result
.
Error
!=
nil
{
return
result
.
Error
}
if
result
.
RowsAffected
!=
1
{
return
gorm
.
ErrRecordNotFound
}
return
nil
})
})
if
err
!=
nil
{
if
err
!=
nil
{
if
!
errors
.
Is
(
err
,
ErrTopUpNotFound
)
&&
!
errors
.
Is
(
err
,
ErrPaymentMethodMismatch
)
&&
!
errors
.
Is
(
err
,
ErrTopUpStatusInvalid
)
{
if
!
errors
.
Is
(
err
,
ErrTopUpNotFound
)
&&
!
errors
.
Is
(
err
,
ErrPaymentMethodMismatch
)
&&
!
errors
.
Is
(
err
,
ErrTopUpStatusInvalid
)
{
...
@@ -214,10 +270,11 @@ func Recharge(referenceId string, customerId string, callerIp string) (err error
...
@@ -214,10 +270,11 @@ func Recharge(referenceId string, customerId string, callerIp string) (err error
decimal
.
NewFromFloat
(
topUp
.
Money
)
.
Mul
(
decimal
.
NewFromFloat
(
common
.
QuotaPerUnit
)),
decimal
.
NewFromFloat
(
topUp
.
Money
)
.
Mul
(
decimal
.
NewFromFloat
(
common
.
QuotaPerUnit
)),
)
)
if
err
!=
nil
||
quota
<=
0
{
if
err
!=
nil
||
quota
<=
0
{
return
errors
.
New
(
"无效的充值额度"
)
return
ErrInvalidTopUpQuota
}
}
return
tx
.
Model
(
&
User
{})
.
Where
(
"id = ?"
,
topUp
.
UserId
)
.
return
creditTopUpQuota
(
tx
,
topUp
.
UserId
,
quota
,
map
[
string
]
interface
{}{
Updates
(
map
[
string
]
interface
{}{
"stripe_customer"
:
customerId
,
"quota"
:
gorm
.
Expr
(
"quota + ?"
,
quota
)})
.
Error
"stripe_customer"
:
customerId
,
})
})
})
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -434,7 +491,7 @@ func ManualCompleteTopUp(tradeNo string, callerIp string) error {
...
@@ -434,7 +491,7 @@ func ManualCompleteTopUp(tradeNo string, callerIp string) error {
)
)
}
}
if
quotaErr
!=
nil
||
quotaToAdd
<=
0
{
if
quotaErr
!=
nil
||
quotaToAdd
<=
0
{
return
errors
.
New
(
"无效的充值额度"
)
return
ErrInvalidTopUpQuota
}
}
// 标记完成
// 标记完成
...
@@ -445,7 +502,7 @@ func ManualCompleteTopUp(tradeNo string, callerIp string) error {
...
@@ -445,7 +502,7 @@ func ManualCompleteTopUp(tradeNo string, callerIp string) error {
}
}
// 增加用户额度(立即写库,保持一致性)
// 增加用户额度(立即写库,保持一致性)
if
err
:=
tx
.
Model
(
&
User
{})
.
Where
(
"id = ?"
,
topUp
.
UserId
)
.
Update
(
"quota"
,
gorm
.
Expr
(
"quota + ?"
,
quotaToAdd
))
.
Error
;
err
!=
nil
{
if
err
:=
creditTopUpQuota
(
tx
,
topUp
.
UserId
,
quotaToAdd
,
nil
)
;
err
!=
nil
{
return
err
return
err
}
}
...
@@ -501,13 +558,11 @@ func RechargeCreem(referenceId string, customerEmail string, customerName string
...
@@ -501,13 +558,11 @@ func RechargeCreem(referenceId string, customerEmail string, customerName string
// Creem 直接使用 Amount 作为充值额度(整数)
// Creem 直接使用 Amount 作为充值额度(整数)
quota
,
err
=
common
.
QuotaFromDecimalStrict
(
decimal
.
NewFromInt
(
topUp
.
Amount
))
quota
,
err
=
common
.
QuotaFromDecimalStrict
(
decimal
.
NewFromInt
(
topUp
.
Amount
))
if
err
!=
nil
||
quota
<=
0
{
if
err
!=
nil
||
quota
<=
0
{
return
errors
.
New
(
"无效的充值额度"
)
return
ErrInvalidTopUpQuota
}
}
// 构建更新字段,优先使用邮箱,如果邮箱为空则使用用户名
// 构建更新字段,优先使用邮箱,如果邮箱为空则使用用户名
updateFields
:=
map
[
string
]
interface
{}{
updateFields
:=
map
[
string
]
interface
{}{}
"quota"
:
gorm
.
Expr
(
"quota + ?"
,
quota
),
}
// 如果有客户邮箱,尝试更新用户邮箱(仅当用户邮箱为空时)
// 如果有客户邮箱,尝试更新用户邮箱(仅当用户邮箱为空时)
if
customerEmail
!=
""
{
if
customerEmail
!=
""
{
...
@@ -524,7 +579,7 @@ func RechargeCreem(referenceId string, customerEmail string, customerName string
...
@@ -524,7 +579,7 @@ func RechargeCreem(referenceId string, customerEmail string, customerName string
}
}
}
}
return
tx
.
Model
(
&
User
{})
.
Where
(
"id = ?"
,
topUp
.
UserId
)
.
Updates
(
updateFields
)
.
Error
return
creditTopUpQuota
(
tx
,
topUp
.
UserId
,
quota
,
updateFields
)
})
})
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -573,7 +628,7 @@ func RechargeWaffo(tradeNo string, callerIp string) (err error) {
...
@@ -573,7 +628,7 @@ func RechargeWaffo(tradeNo string, callerIp string) (err error) {
decimal
.
NewFromInt
(
topUp
.
Amount
)
.
Mul
(
decimal
.
NewFromFloat
(
common
.
QuotaPerUnit
)),
decimal
.
NewFromInt
(
topUp
.
Amount
)
.
Mul
(
decimal
.
NewFromFloat
(
common
.
QuotaPerUnit
)),
)
)
if
err
!=
nil
||
quotaToAdd
<=
0
{
if
err
!=
nil
||
quotaToAdd
<=
0
{
return
errors
.
New
(
"无效的充值额度"
)
return
ErrInvalidTopUpQuota
}
}
topUp
.
CompleteTime
=
common
.
GetTimestamp
()
topUp
.
CompleteTime
=
common
.
GetTimestamp
()
...
@@ -582,7 +637,7 @@ func RechargeWaffo(tradeNo string, callerIp string) (err error) {
...
@@ -582,7 +637,7 @@ func RechargeWaffo(tradeNo string, callerIp string) (err error) {
return
err
return
err
}
}
return
tx
.
Model
(
&
User
{})
.
Where
(
"id = ?"
,
topUp
.
UserId
)
.
Update
(
"quota"
,
gorm
.
Expr
(
"quota + ?"
,
quotaToAdd
))
.
Error
return
creditTopUpQuota
(
tx
,
topUp
.
UserId
,
quotaToAdd
,
nil
)
})
})
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -633,7 +688,7 @@ func RechargeWaffoPancake(tradeNo string) (err error) {
...
@@ -633,7 +688,7 @@ func RechargeWaffoPancake(tradeNo string) (err error) {
decimal
.
NewFromInt
(
topUp
.
Amount
)
.
Mul
(
decimal
.
NewFromFloat
(
common
.
QuotaPerUnit
)),
decimal
.
NewFromInt
(
topUp
.
Amount
)
.
Mul
(
decimal
.
NewFromFloat
(
common
.
QuotaPerUnit
)),
)
)
if
err
!=
nil
||
quotaToAdd
<=
0
{
if
err
!=
nil
||
quotaToAdd
<=
0
{
return
errors
.
New
(
"无效的充值额度"
)
return
ErrInvalidTopUpQuota
}
}
topUp
.
CompleteTime
=
common
.
GetTimestamp
()
topUp
.
CompleteTime
=
common
.
GetTimestamp
()
...
@@ -642,7 +697,7 @@ func RechargeWaffoPancake(tradeNo string) (err error) {
...
@@ -642,7 +697,7 @@ func RechargeWaffoPancake(tradeNo string) (err error) {
return
err
return
err
}
}
return
tx
.
Model
(
&
User
{})
.
Where
(
"id = ?"
,
topUp
.
UserId
)
.
Update
(
"quota"
,
gorm
.
Expr
(
"quota + ?"
,
quotaToAdd
))
.
Error
return
creditTopUpQuota
(
tx
,
topUp
.
UserId
,
quotaToAdd
,
nil
)
})
})
if
err
!=
nil
{
if
err
!=
nil
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment