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
Commit
12ea0c19
authored
Sep 16, 2025
by
Little Write
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
完善 订单处理,以及 优化 ui
parent
edf46c70
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
274 additions
and
34 deletions
+274
-34
controller/topup_creem.go
+227
-28
model/option.go
+3
-0
model/topup.go
+25
-3
setting/payment_creem.go
+1
-0
web/src/components/settings/PaymentSetting.js
+1
-0
web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js
+17
-3
No files found.
controller/topup_creem.go
View file @
12ea0c19
...
@@ -2,6 +2,9 @@ package controller
...
@@ -2,6 +2,9 @@ package controller
import
(
import
(
"bytes"
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"encoding/json"
"fmt"
"fmt"
"io"
"io"
...
@@ -18,10 +21,29 @@ import (
...
@@ -18,10 +21,29 @@ import (
const
(
const
(
PaymentMethodCreem
=
"creem"
PaymentMethodCreem
=
"creem"
CreemSignatureHeader
=
"creem-signature"
)
)
var
creemAdaptor
=
&
CreemAdaptor
{}
var
creemAdaptor
=
&
CreemAdaptor
{}
// 生成HMAC-SHA256签名
func
generateCreemSignature
(
payload
string
,
secret
string
)
string
{
h
:=
hmac
.
New
(
sha256
.
New
,
[]
byte
(
secret
))
h
.
Write
([]
byte
(
payload
))
return
hex
.
EncodeToString
(
h
.
Sum
(
nil
))
}
// 验证Creem webhook签名
func
verifyCreemSignature
(
payload
string
,
signature
string
,
secret
string
)
bool
{
if
secret
==
""
{
log
.
Printf
(
"Creem webhook secret未配置,跳过签名验证"
)
return
true
// 如果没有配置secret,跳过验证
}
expectedSignature
:=
generateCreemSignature
(
payload
,
secret
)
return
hmac
.
Equal
([]
byte
(
signature
),
[]
byte
(
expectedSignature
))
}
type
CreemPayRequest
struct
{
type
CreemPayRequest
struct
{
ProductId
string
`json:"product_id"`
ProductId
string
`json:"product_id"`
PaymentMethod
string
`json:"payment_method"`
PaymentMethod
string
`json:"payment_method"`
...
@@ -75,41 +97,65 @@ func (*CreemAdaptor) RequestPay(c *gin.Context, req *CreemPayRequest) {
...
@@ -75,41 +97,65 @@ func (*CreemAdaptor) RequestPay(c *gin.Context, req *CreemPayRequest) {
id
:=
c
.
GetInt
(
"id"
)
id
:=
c
.
GetInt
(
"id"
)
user
,
_
:=
model
.
GetUserById
(
id
,
false
)
user
,
_
:=
model
.
GetUserById
(
id
,
false
)
// 生成唯一的订单引用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
))
referenceId
:=
"ref_"
+
common
.
Sha1
([]
byte
(
reference
))
referenceId
:=
"ref_"
+
common
.
Sha1
([]
byte
(
reference
))
checkoutUrl
,
err
:=
genCreemLink
(
referenceId
,
selectedProduct
,
user
.
Email
,
user
.
Username
)
// 先创建订单记录,使用产品配置的金额和充值额度
if
err
!=
nil
{
log
.
Println
(
"获取Creem支付链接失败"
,
err
)
c
.
JSON
(
200
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"拉起支付失败"
})
return
}
topUp
:=
&
model
.
TopUp
{
topUp
:=
&
model
.
TopUp
{
UserId
:
id
,
UserId
:
id
,
Amount
:
selectedProduct
.
Quota
,
Amount
:
selectedProduct
.
Quota
,
// 充值额度
Money
:
selectedProduct
.
Price
,
Money
:
selectedProduct
.
Price
,
// 支付金额
TradeNo
:
referenceId
,
TradeNo
:
referenceId
,
CreateTime
:
time
.
Now
()
.
Unix
(),
CreateTime
:
time
.
Now
()
.
Unix
(),
Status
:
common
.
TopUpStatusPending
,
Status
:
common
.
TopUpStatusPending
,
}
}
err
=
topUp
.
Insert
()
err
=
topUp
.
Insert
()
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"创建Creem订单失败: %v"
,
err
)
c
.
JSON
(
200
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"创建订单失败"
})
c
.
JSON
(
200
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"创建订单失败"
})
return
return
}
}
// 创建支付链接,传入用户邮箱
checkoutUrl
,
err
:=
genCreemLink
(
referenceId
,
selectedProduct
,
user
.
Email
,
user
.
Username
)
if
err
!=
nil
{
log
.
Printf
(
"获取Creem支付链接失败: %v"
,
err
)
c
.
JSON
(
200
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"拉起支付失败"
})
return
}
log
.
Printf
(
"Creem订单创建成功 - 用户ID: %d, 订单号: %s, 产品: %s, 充值额度: %d, 支付金额: %.2f"
,
id
,
referenceId
,
selectedProduct
.
Name
,
selectedProduct
.
Quota
,
selectedProduct
.
Price
)
c
.
JSON
(
200
,
gin
.
H
{
c
.
JSON
(
200
,
gin
.
H
{
"message"
:
"success"
,
"message"
:
"success"
,
"data"
:
gin
.
H
{
"data"
:
gin
.
H
{
"checkout_url"
:
checkoutUrl
,
"checkout_url"
:
checkoutUrl
,
"order_id"
:
referenceId
,
},
},
})
})
}
}
func
RequestCreemPay
(
c
*
gin
.
Context
)
{
func
RequestCreemPay
(
c
*
gin
.
Context
)
{
var
req
CreemPayRequest
var
req
CreemPayRequest
err
:=
c
.
ShouldBindJSON
(
&
req
)
// 读取body内容用于打印,同时保留原始数据供后续使用
bodyBytes
,
err
:=
io
.
ReadAll
(
c
.
Request
.
Body
)
if
err
!=
nil
{
log
.
Printf
(
"读取请求body失败: %v"
,
err
)
c
.
JSON
(
200
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"读取请求失败"
})
return
}
// 打印body内容
log
.
Printf
(
"creem pay request body: %s"
,
string
(
bodyBytes
))
// 重新设置body供后续的ShouldBindJSON使用
c
.
Request
.
Body
=
io
.
NopCloser
(
bytes
.
NewReader
(
bodyBytes
))
err
=
c
.
ShouldBindJSON
(
&
req
)
log
.
Printf
(
" json body is %+v"
,
req
)
if
err
!=
nil
{
if
err
!=
nil
{
c
.
JSON
(
200
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"参数错误"
})
c
.
JSON
(
200
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"参数错误"
})
return
return
...
@@ -117,6 +163,68 @@ func RequestCreemPay(c *gin.Context) {
...
@@ -117,6 +163,68 @@ func RequestCreemPay(c *gin.Context) {
creemAdaptor
.
RequestPay
(
c
,
&
req
)
creemAdaptor
.
RequestPay
(
c
,
&
req
)
}
}
// 新的Creem Webhook结构体,匹配实际的webhook数据格式
type
CreemWebhookEvent
struct
{
Id
string
`json:"id"`
EventType
string
`json:"eventType"`
CreatedAt
int64
`json:"created_at"`
Object
struct
{
Id
string
`json:"id"`
Object
string
`json:"object"`
RequestId
string
`json:"request_id"`
Order
struct
{
Object
string
`json:"object"`
Id
string
`json:"id"`
Customer
string
`json:"customer"`
Product
string
`json:"product"`
Amount
int
`json:"amount"`
Currency
string
`json:"currency"`
SubTotal
int
`json:"sub_total"`
TaxAmount
int
`json:"tax_amount"`
AmountDue
int
`json:"amount_due"`
AmountPaid
int
`json:"amount_paid"`
Status
string
`json:"status"`
Type
string
`json:"type"`
Transaction
string
`json:"transaction"`
CreatedAt
string
`json:"created_at"`
UpdatedAt
string
`json:"updated_at"`
Mode
string
`json:"mode"`
}
`json:"order"`
Product
struct
{
Id
string
`json:"id"`
Object
string
`json:"object"`
Name
string
`json:"name"`
Description
string
`json:"description"`
Price
int
`json:"price"`
Currency
string
`json:"currency"`
BillingType
string
`json:"billing_type"`
BillingPeriod
string
`json:"billing_period"`
Status
string
`json:"status"`
TaxMode
string
`json:"tax_mode"`
TaxCategory
string
`json:"tax_category"`
DefaultSuccessUrl
*
string
`json:"default_success_url"`
CreatedAt
string
`json:"created_at"`
UpdatedAt
string
`json:"updated_at"`
Mode
string
`json:"mode"`
}
`json:"product"`
Units
int
`json:"units"`
Customer
struct
{
Id
string
`json:"id"`
Object
string
`json:"object"`
Email
string
`json:"email"`
Name
string
`json:"name"`
Country
string
`json:"country"`
CreatedAt
string
`json:"created_at"`
UpdatedAt
string
`json:"updated_at"`
Mode
string
`json:"mode"`
}
`json:"customer"`
Status
string
`json:"status"`
Metadata
map
[
string
]
string
`json:"metadata"`
Mode
string
`json:"mode"`
}
`json:"object"`
}
// 保留旧的结构体作为兼容
type
CreemWebhookData
struct
{
type
CreemWebhookData
struct
{
Type
string
`json:"type"`
Type
string
`json:"type"`
Data
struct
{
Data
struct
{
...
@@ -127,38 +235,122 @@ type CreemWebhookData struct {
...
@@ -127,38 +235,122 @@ type CreemWebhookData struct {
}
}
func
CreemWebhook
(
c
*
gin
.
Context
)
{
func
CreemWebhook
(
c
*
gin
.
Context
)
{
// 解析 webhook 数据
// 读取body内容用于打印,同时保留原始数据供后续使用
var
webhookData
CreemWebhookData
bodyBytes
,
err
:=
io
.
ReadAll
(
c
.
Request
.
Body
)
if
err
:=
c
.
ShouldBindJSON
(
&
webhookData
);
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"解析Creem Webhook参数失败: %v
\n
"
,
err
)
log
.
Printf
(
"读取Creem Webhook请求body失败: %v"
,
err
)
c
.
AbortWithStatus
(
http
.
StatusBadRequest
)
return
}
// 获取签名头
signature
:=
c
.
GetHeader
(
CreemSignatureHeader
)
// 打印请求信息用于调试
log
.
Printf
(
"Creem Webhook - URI: %s, Query: %s"
,
c
.
Request
.
RequestURI
,
c
.
Request
.
URL
.
RawQuery
)
log
.
Printf
(
"Creem Webhook - Signature: %s"
,
signature
)
log
.
Printf
(
"Creem Webhook - Body: %s"
,
string
(
bodyBytes
))
// 验证签名
if
!
verifyCreemSignature
(
string
(
bodyBytes
),
signature
,
setting
.
CreemWebhookSecret
)
{
log
.
Printf
(
"Creem Webhook签名验证失败"
)
c
.
AbortWithStatus
(
http
.
StatusUnauthorized
)
return
}
log
.
Printf
(
"Creem Webhook签名验证成功"
)
// 重新设置body供后续的ShouldBindJSON使用
c
.
Request
.
Body
=
io
.
NopCloser
(
bytes
.
NewReader
(
bodyBytes
))
// 解析新格式的webhook数据
var
webhookEvent
CreemWebhookEvent
if
err
:=
c
.
ShouldBindJSON
(
&
webhookEvent
);
err
!=
nil
{
log
.
Printf
(
"解析Creem Webhook参数失败: %v"
,
err
)
c
.
AbortWithStatus
(
http
.
StatusBadRequest
)
c
.
AbortWithStatus
(
http
.
StatusBadRequest
)
return
return
}
}
// 检查事件类型
log
.
Printf
(
"Creem Webhook解析成功 - EventType: %s, EventId: %s"
,
webhookEvent
.
EventType
,
webhookEvent
.
Id
)
if
webhookData
.
Type
!=
"checkout.completed"
{
log
.
Printf
(
"忽略Creem Webhook事件类型: %s"
,
webhookData
.
Type
)
// 根据事件类型处理不同的webhook
switch
webhookEvent
.
EventType
{
case
"checkout.completed"
:
handleCheckoutCompleted
(
c
,
&
webhookEvent
)
default
:
log
.
Printf
(
"忽略Creem Webhook事件类型: %s"
,
webhookEvent
.
EventType
)
c
.
Status
(
http
.
StatusOK
)
}
}
// 处理支付完成事件
func
handleCheckoutCompleted
(
c
*
gin
.
Context
,
event
*
CreemWebhookEvent
)
{
// 验证订单状态
if
event
.
Object
.
Order
.
Status
!=
"paid"
{
log
.
Printf
(
"订单状态不是已支付: %s, 跳过处理"
,
event
.
Object
.
Order
.
Status
)
c
.
Status
(
http
.
StatusOK
)
c
.
Status
(
http
.
StatusOK
)
return
return
}
}
// 获取引用ID
// 获取引用ID
(这是我们创建订单时传递的request_id)
referenceId
:=
webhookData
.
Data
.
RequestId
referenceId
:=
event
.
Object
.
RequestId
if
referenceId
==
""
{
if
referenceId
==
""
{
log
.
Println
(
"Creem Webhook缺少request_id字段"
)
log
.
Println
(
"Creem Webhook缺少request_id字段"
)
c
.
AbortWithStatus
(
http
.
StatusBadRequest
)
c
.
AbortWithStatus
(
http
.
StatusBadRequest
)
return
return
}
}
// 处理支付完成事件
// 验证订单类型,目前只处理一次性付款
err
:=
model
.
RechargeCreem
(
referenceId
)
if
event
.
Object
.
Order
.
Type
!=
"onetime"
{
log
.
Printf
(
"暂不支持的订单类型: %s, 跳过处理"
,
event
.
Object
.
Order
.
Type
)
c
.
Status
(
http
.
StatusOK
)
return
}
// 记录详细的支付信息
log
.
Printf
(
"处理Creem支付完成 - 订单号: %s, Creem订单ID: %s, 支付金额: %d %s, 客户邮箱: %s, 产品: %s"
,
referenceId
,
event
.
Object
.
Order
.
Id
,
event
.
Object
.
Order
.
AmountPaid
,
event
.
Object
.
Order
.
Currency
,
event
.
Object
.
Customer
.
Email
,
event
.
Object
.
Product
.
Name
)
// 查询本地订单确认存在
topUp
:=
model
.
GetTopUpByTradeNo
(
referenceId
)
if
topUp
==
nil
{
log
.
Printf
(
"Creem充值订单不存在: %s"
,
referenceId
)
c
.
AbortWithStatus
(
http
.
StatusBadRequest
)
return
}
if
topUp
.
Status
!=
common
.
TopUpStatusPending
{
log
.
Printf
(
"Creem充值订单状态错误: %s, 当前状态: %s"
,
referenceId
,
topUp
.
Status
)
c
.
Status
(
http
.
StatusOK
)
// 已处理过的订单,返回成功避免重复处理
return
}
// 处理充值,传入客户邮箱和姓名信息
customerEmail
:=
event
.
Object
.
Customer
.
Email
customerName
:=
event
.
Object
.
Customer
.
Name
// 防护性检查,确保邮箱和姓名不为空字符串
if
customerEmail
==
""
{
log
.
Printf
(
"警告:Creem回调中客户邮箱为空 - 订单号: %s"
,
referenceId
)
}
if
customerName
==
""
{
log
.
Printf
(
"警告:Creem回调中客户姓名为空 - 订单号: %s"
,
referenceId
)
}
err
:=
model
.
RechargeCreem
(
referenceId
,
customerEmail
,
customerName
)
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Print
ln
(
"Creem充值处理失败:
"
,
err
.
Error
(),
referenceId
)
log
.
Print
f
(
"Creem充值处理失败: %s, 订单号: %s
"
,
err
.
Error
(),
referenceId
)
c
.
AbortWithStatus
(
http
.
StatusInternalServerError
)
c
.
AbortWithStatus
(
http
.
StatusInternalServerError
)
return
return
}
}
log
.
Printf
(
"Creem充值成功: %s"
,
referenceId
)
log
.
Printf
(
"Creem充值成功 - 订单号: %s, 充值额度: %d, 支付金额: %.2f, 客户邮箱: %s, 客户姓名: %s"
,
referenceId
,
topUp
.
Amount
,
topUp
.
Money
,
customerEmail
,
customerName
)
c
.
Status
(
http
.
StatusOK
)
c
.
Status
(
http
.
StatusOK
)
}
}
...
@@ -185,20 +377,23 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
...
@@ -185,20 +377,23 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
apiUrl
:=
"https://api.creem.io/v1/checkouts"
apiUrl
:=
"https://api.creem.io/v1/checkouts"
if
setting
.
CreemTestMode
{
if
setting
.
CreemTestMode
{
apiUrl
=
"https://test-api.creem.io/v1/checkouts"
apiUrl
=
"https://test-api.creem.io/v1/checkouts"
log
.
Printf
(
"使用Creem测试环境: %s"
,
apiUrl
)
}
}
// 构建请求数据
// 构建请求数据
,确保包含用户邮箱
requestData
:=
CreemCheckoutRequest
{
requestData
:=
CreemCheckoutRequest
{
ProductId
:
product
.
ProductId
,
ProductId
:
product
.
ProductId
,
RequestId
:
referenceId
,
RequestId
:
referenceId
,
// 这个作为订单ID传递给Creem
Customer
:
struct
{
Customer
:
struct
{
Email
string
`json:"email"`
Email
string
`json:"email"`
}{
}{
Email
:
email
,
Email
:
email
,
// 用户邮箱会在支付页面预填充
},
},
Metadata
:
map
[
string
]
string
{
Metadata
:
map
[
string
]
string
{
"username"
:
username
,
"username"
:
username
,
"reference_id"
:
referenceId
,
"reference_id"
:
referenceId
,
"product_name"
:
product
.
Name
,
"quota"
:
fmt
.
Sprintf
(
"%d"
,
product
.
Quota
),
},
},
}
}
...
@@ -218,6 +413,9 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
...
@@ -218,6 +413,9 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
req
.
Header
.
Set
(
"x-api-key"
,
setting
.
CreemApiKey
)
req
.
Header
.
Set
(
"x-api-key"
,
setting
.
CreemApiKey
)
log
.
Printf
(
"发送Creem支付请求 - URL: %s, 产品ID: %s, 用户邮箱: %s, 订单号: %s"
,
apiUrl
,
product
.
ProductId
,
email
,
referenceId
)
// 发送请求
// 发送请求
client
:=
&
http
.
Client
{
client
:=
&
http
.
Client
{
Timeout
:
30
*
time
.
Second
,
Timeout
:
30
*
time
.
Second
,
...
@@ -227,7 +425,6 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
...
@@ -227,7 +425,6 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
return
""
,
fmt
.
Errorf
(
"发送HTTP请求失败: %v"
,
err
)
return
""
,
fmt
.
Errorf
(
"发送HTTP请求失败: %v"
,
err
)
}
}
defer
resp
.
Body
.
Close
()
defer
resp
.
Body
.
Close
()
log
.
Printf
(
" creem req host: %s, key %s req 【%s】"
,
apiUrl
,
setting
.
CreemApiKey
,
jsonData
)
// 读取响应
// 读取响应
body
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
body
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
...
@@ -235,6 +432,8 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
...
@@ -235,6 +432,8 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
return
""
,
fmt
.
Errorf
(
"读取响应失败: %v"
,
err
)
return
""
,
fmt
.
Errorf
(
"读取响应失败: %v"
,
err
)
}
}
log
.
Printf
(
"Creem API响应 - 状态码: %d, 响应体: %s"
,
resp
.
StatusCode
,
string
(
body
))
// 检查响应状态
// 检查响应状态
if
resp
.
StatusCode
!=
http
.
StatusOK
{
if
resp
.
StatusCode
!=
http
.
StatusOK
{
return
""
,
fmt
.
Errorf
(
"Creem API 返回错误状态 %d: %s"
,
resp
.
StatusCode
,
string
(
body
))
return
""
,
fmt
.
Errorf
(
"Creem API 返回错误状态 %d: %s"
,
resp
.
StatusCode
,
string
(
body
))
...
@@ -251,6 +450,6 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
...
@@ -251,6 +450,6 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
return
""
,
fmt
.
Errorf
(
"Creem API 未返回支付链接"
)
return
""
,
fmt
.
Errorf
(
"Creem API 未返回支付链接"
)
}
}
log
.
Printf
(
"Creem 支付链接创建成功
: %s, 订单ID: %s"
,
referenceId
,
checkoutResp
.
Id
)
log
.
Printf
(
"Creem 支付链接创建成功
- 订单号: %s, 支付链接: %s"
,
referenceId
,
checkoutResp
.
CheckoutUrl
)
return
checkoutResp
.
CheckoutUrl
,
nil
return
checkoutResp
.
CheckoutUrl
,
nil
}
}
model/option.go
View file @
12ea0c19
...
@@ -84,6 +84,7 @@ func InitOptionMap() {
...
@@ -84,6 +84,7 @@ func InitOptionMap() {
common
.
OptionMap
[
"CreemApiKey"
]
=
setting
.
CreemApiKey
common
.
OptionMap
[
"CreemApiKey"
]
=
setting
.
CreemApiKey
common
.
OptionMap
[
"CreemProducts"
]
=
setting
.
CreemProducts
common
.
OptionMap
[
"CreemProducts"
]
=
setting
.
CreemProducts
common
.
OptionMap
[
"CreemTestMode"
]
=
strconv
.
FormatBool
(
setting
.
CreemTestMode
)
common
.
OptionMap
[
"CreemTestMode"
]
=
strconv
.
FormatBool
(
setting
.
CreemTestMode
)
common
.
OptionMap
[
"CreemWebhookSecret"
]
=
setting
.
CreemWebhookSecret
common
.
OptionMap
[
"TopupGroupRatio"
]
=
common
.
TopupGroupRatio2JSONString
()
common
.
OptionMap
[
"TopupGroupRatio"
]
=
common
.
TopupGroupRatio2JSONString
()
common
.
OptionMap
[
"Chats"
]
=
setting
.
Chats2JsonString
()
common
.
OptionMap
[
"Chats"
]
=
setting
.
Chats2JsonString
()
common
.
OptionMap
[
"AutoGroups"
]
=
setting
.
AutoGroups2JsonString
()
common
.
OptionMap
[
"AutoGroups"
]
=
setting
.
AutoGroups2JsonString
()
...
@@ -335,6 +336,8 @@ func updateOptionMap(key string, value string) (err error) {
...
@@ -335,6 +336,8 @@ func updateOptionMap(key string, value string) (err error) {
setting
.
CreemProducts
=
value
setting
.
CreemProducts
=
value
case
"CreemTestMode"
:
case
"CreemTestMode"
:
setting
.
CreemTestMode
=
value
==
"true"
setting
.
CreemTestMode
=
value
==
"true"
case
"CreemWebhookSecret"
:
setting
.
CreemWebhookSecret
=
value
case
"TopupGroupRatio"
:
case
"TopupGroupRatio"
:
err
=
common
.
UpdateTopupGroupRatioByJSONString
(
value
)
err
=
common
.
UpdateTopupGroupRatioByJSONString
(
value
)
case
"GitHubClientId"
:
case
"GitHubClientId"
:
...
...
model/topup.go
View file @
12ea0c19
...
@@ -99,7 +99,7 @@ func Recharge(referenceId string, customerId string) (err error) {
...
@@ -99,7 +99,7 @@ func Recharge(referenceId string, customerId string) (err error) {
return
nil
return
nil
}
}
func
RechargeCreem
(
referenceId
string
)
(
err
error
)
{
func
RechargeCreem
(
referenceId
string
,
customerEmail
string
,
customerName
string
)
(
err
error
)
{
if
referenceId
==
""
{
if
referenceId
==
""
{
return
errors
.
New
(
"未提供支付单号"
)
return
errors
.
New
(
"未提供支付单号"
)
}
}
...
@@ -131,7 +131,29 @@ func RechargeCreem(referenceId string) (err error) {
...
@@ -131,7 +131,29 @@ func RechargeCreem(referenceId string) (err error) {
// Creem 直接使用 Amount 作为充值额度
// Creem 直接使用 Amount 作为充值额度
quota
=
float64
(
topUp
.
Amount
)
quota
=
float64
(
topUp
.
Amount
)
err
=
tx
.
Model
(
&
User
{})
.
Where
(
"id = ?"
,
topUp
.
UserId
)
.
Update
(
"quota"
,
gorm
.
Expr
(
"quota + ?"
,
quota
))
.
Error
// 构建更新字段,优先使用邮箱,如果邮箱为空则使用用户名
updateFields
:=
map
[
string
]
interface
{}{
"quota"
:
gorm
.
Expr
(
"quota + ?"
,
quota
),
}
// 如果有客户邮箱,尝试更新用户邮箱(仅当用户邮箱为空时)
if
customerEmail
!=
""
{
// 先检查用户当前邮箱是否为空
var
user
User
err
=
tx
.
Where
(
"id = ?"
,
topUp
.
UserId
)
.
First
(
&
user
)
.
Error
if
err
!=
nil
{
return
err
}
// 如果用户邮箱为空,则更新为支付时使用的邮箱
if
user
.
Email
==
""
{
updateFields
[
"email"
]
=
customerEmail
fmt
.
Printf
(
"更新用户邮箱:用户ID %d, 新邮箱 %s
\n
"
,
topUp
.
UserId
,
customerEmail
)
}
}
err
=
tx
.
Model
(
&
User
{})
.
Where
(
"id = ?"
,
topUp
.
UserId
)
.
Updates
(
updateFields
)
.
Error
if
err
!=
nil
{
if
err
!=
nil
{
return
err
return
err
}
}
...
@@ -143,7 +165,7 @@ func RechargeCreem(referenceId string) (err error) {
...
@@ -143,7 +165,7 @@ func RechargeCreem(referenceId string) (err error) {
return
errors
.
New
(
"充值失败,"
+
err
.
Error
())
return
errors
.
New
(
"充值失败,"
+
err
.
Error
())
}
}
RecordLog
(
topUp
.
UserId
,
LogTypeTopup
,
fmt
.
Sprintf
(
"使用Creem充值成功,充值额度: %v,支付金额:%.2f
"
,
common
.
FormatQuota
(
int
(
quota
)),
topUp
.
Money
))
RecordLog
(
topUp
.
UserId
,
LogTypeTopup
,
fmt
.
Sprintf
(
"使用Creem充值成功,充值额度: %v,支付金额:%.2f
,客户邮箱:%s"
,
common
.
FormatQuota
(
int
(
quota
)),
topUp
.
Money
,
customerEmail
))
return
nil
return
nil
}
}
setting/payment_creem.go
View file @
12ea0c19
...
@@ -3,3 +3,4 @@ package setting
...
@@ -3,3 +3,4 @@ package setting
var
CreemApiKey
=
""
var
CreemApiKey
=
""
var
CreemProducts
=
"[]"
var
CreemProducts
=
"[]"
var
CreemTestMode
=
false
var
CreemTestMode
=
false
var
CreemWebhookSecret
=
""
web/src/components/settings/PaymentSetting.js
View file @
12ea0c19
...
@@ -28,6 +28,7 @@ const PaymentSetting = () => {
...
@@ -28,6 +28,7 @@ const PaymentSetting = () => {
StripeMinTopUp
:
1
,
StripeMinTopUp
:
1
,
CreemApiKey
:
''
,
CreemApiKey
:
''
,
CreemWebhookSecret
:
''
,
CreemProducts
:
'[]'
,
CreemProducts
:
'[]'
,
});
});
...
...
web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js
View file @
12ea0c19
...
@@ -27,6 +27,7 @@ export default function SettingsPaymentGatewayCreem(props) {
...
@@ -27,6 +27,7 @@ export default function SettingsPaymentGatewayCreem(props) {
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
inputs
,
setInputs
]
=
useState
({
const
[
inputs
,
setInputs
]
=
useState
({
CreemApiKey
:
''
,
CreemApiKey
:
''
,
CreemWebhookSecret
:
''
,
CreemProducts
:
'[]'
,
CreemProducts
:
'[]'
,
CreemTestMode
:
false
,
CreemTestMode
:
false
,
});
});
...
@@ -47,6 +48,7 @@ export default function SettingsPaymentGatewayCreem(props) {
...
@@ -47,6 +48,7 @@ export default function SettingsPaymentGatewayCreem(props) {
if
(
props
.
options
&&
formApiRef
.
current
)
{
if
(
props
.
options
&&
formApiRef
.
current
)
{
const
currentInputs
=
{
const
currentInputs
=
{
CreemApiKey
:
props
.
options
.
CreemApiKey
||
''
,
CreemApiKey
:
props
.
options
.
CreemApiKey
||
''
,
CreemWebhookSecret
:
props
.
options
.
CreemWebhookSecret
||
''
,
CreemProducts
:
props
.
options
.
CreemProducts
||
'[]'
,
CreemProducts
:
props
.
options
.
CreemProducts
||
'[]'
,
CreemTestMode
:
props
.
options
.
CreemTestMode
===
'true'
,
CreemTestMode
:
props
.
options
.
CreemTestMode
===
'true'
,
};
};
...
@@ -77,6 +79,10 @@ export default function SettingsPaymentGatewayCreem(props) {
...
@@ -77,6 +79,10 @@ export default function SettingsPaymentGatewayCreem(props) {
options
.
push
({
key
:
'CreemApiKey'
,
value
:
inputs
.
CreemApiKey
});
options
.
push
({
key
:
'CreemApiKey'
,
value
:
inputs
.
CreemApiKey
});
}
}
if
(
inputs
.
CreemWebhookSecret
&&
inputs
.
CreemWebhookSecret
!==
''
)
{
options
.
push
({
key
:
'CreemWebhookSecret'
,
value
:
inputs
.
CreemWebhookSecret
});
}
// Save test mode setting
// Save test mode setting
options
.
push
({
key
:
'CreemTestMode'
,
value
:
inputs
.
CreemTestMode
?
'true'
:
'false'
});
options
.
push
({
key
:
'CreemTestMode'
,
value
:
inputs
.
CreemTestMode
?
'true'
:
'false'
});
...
@@ -243,7 +249,7 @@ export default function SettingsPaymentGatewayCreem(props) {
...
@@ -243,7 +249,7 @@ export default function SettingsPaymentGatewayCreem(props) {
/
>
/
>
<
Row
gutter
=
{{
xs
:
8
,
sm
:
16
,
md
:
24
,
lg
:
24
,
xl
:
24
,
xxl
:
24
}}
>
<
Row
gutter
=
{{
xs
:
8
,
sm
:
16
,
md
:
24
,
lg
:
24
,
xl
:
24
,
xxl
:
24
}}
>
<
Col
xs
=
{
24
}
sm
=
{
24
}
md
=
{
12
}
lg
=
{
12
}
xl
=
{
12
}
>
<
Col
xs
=
{
24
}
sm
=
{
24
}
md
=
{
8
}
lg
=
{
8
}
xl
=
{
8
}
>
<
Form
.
Input
<
Form
.
Input
field
=
'CreemApiKey'
field
=
'CreemApiKey'
label
=
{
t
(
'API 密钥'
)}
label
=
{
t
(
'API 密钥'
)}
...
@@ -251,11 +257,19 @@ export default function SettingsPaymentGatewayCreem(props) {
...
@@ -251,11 +257,19 @@ export default function SettingsPaymentGatewayCreem(props) {
type
=
'password'
type
=
'password'
/>
/>
<
/Col
>
<
/Col
>
<
Col
xs
=
{
24
}
sm
=
{
24
}
md
=
{
12
}
lg
=
{
12
}
xl
=
{
12
}
>
<
Col
xs
=
{
24
}
sm
=
{
24
}
md
=
{
8
}
lg
=
{
8
}
xl
=
{
8
}
>
<
Form
.
Input
field
=
'CreemWebhookSecret'
label
=
{
t
(
'Webhook 密钥'
)}
placeholder
=
{
t
(
'用于验证 Webhook 请求的密钥,敏感信息不显示'
)}
type
=
'password'
/>
<
/Col
>
<
Col
xs
=
{
24
}
sm
=
{
24
}
md
=
{
8
}
lg
=
{
8
}
xl
=
{
8
}
>
<
Form
.
Switch
<
Form
.
Switch
field
=
'CreemTestMode'
field
=
'CreemTestMode'
label
=
{
t
(
'测试模式'
)}
label
=
{
t
(
'测试模式'
)}
extraText
=
{
t
(
'启用后将使用 Creem 测试环境,可使用测试卡号 4242 4242 4242 4242 进行测试
'
)}
extraText
=
{
t
(
'启用后将使用 Creem Test Mode
'
)}
/
>
/
>
<
/Col
>
<
/Col
>
<
/Row
>
<
/Row
>
...
...
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