feat(payment): add support for CryptoSaaS payment platform and enhance configuration handling

This commit is contained in:
Chang lue Tsen 2025-09-06 12:22:39 -04:00
parent 47446ef410
commit 4d95834c22
7 changed files with 157 additions and 45 deletions

View File

@ -26,7 +26,7 @@ func PaymentNotifyHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
} }
switch payment.ParsePlatform(platform) { switch payment.ParsePlatform(platform) {
case payment.EPay: case payment.EPay, payment.CryptoSaaS:
req := &types.EPayNotifyRequest{} req := &types.EPayNotifyRequest{}
if err := c.ShouldBind(req); err != nil { if err := c.ShouldBind(req); err != nil {
result.HttpResult(c, nil, err) result.HttpResult(c, nil, err)

View File

@ -8,7 +8,7 @@ import (
"github.com/perfect-panel/server/pkg/result" "github.com/perfect-panel/server/pkg/result"
) )
// Purchase Checkout // PurchaseCheckoutHandler Purchase Checkout
func PurchaseCheckoutHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { func PurchaseCheckoutHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) { return func(c *gin.Context) {
var req types.CheckoutOrderRequest var req types.CheckoutOrderRequest

View File

@ -55,10 +55,9 @@ func (l *CreatePaymentMethodLogic) CreatePaymentMethod(req *types.CreatePaymentM
Token: random.KeyNew(8, 1), Token: random.KeyNew(8, 1),
} }
err = l.svcCtx.PaymentModel.Transaction(l.ctx, func(tx *gorm.DB) error { err = l.svcCtx.PaymentModel.Transaction(l.ctx, func(tx *gorm.DB) error {
if req.Platform == "Stripe" { if req.Platform == "Stripe" {
var cfg paymentModel.StripeConfig var cfg paymentModel.StripeConfig
if err := cfg.Unmarshal(paymentMethod.Config); err != nil { if err = cfg.Unmarshal([]byte(paymentMethod.Config)); err != nil {
l.Errorf("[CreatePaymentMethod] unmarshal stripe config error: %s", err.Error()) l.Errorf("[CreatePaymentMethod] unmarshal stripe config error: %s", err.Error())
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal stripe config error: %s", err.Error()) return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal stripe config error: %s", err.Error())
} }
@ -79,7 +78,8 @@ func (l *CreatePaymentMethodLogic) CreatePaymentMethod(req *types.CreatePaymentM
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "create stripe webhook endpoint error: %s", err.Error()) return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "create stripe webhook endpoint error: %s", err.Error())
} }
cfg.WebhookSecret = endpoint.Secret cfg.WebhookSecret = endpoint.Secret
paymentMethod.Config = cfg.Marshal() content, _ := cfg.Marshal()
paymentMethod.Config = string(content)
} }
if err = tx.Model(&paymentModel.Payment{}).Create(paymentMethod).Error; err != nil { if err = tx.Model(&paymentModel.Payment{}).Create(paymentMethod).Error; err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert payment method error: %s", err.Error()) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert payment method error: %s", err.Error())
@ -101,27 +101,36 @@ func (l *CreatePaymentMethodLogic) CreatePaymentMethod(req *types.CreatePaymentM
func parsePaymentPlatformConfig(ctx context.Context, platform payment.Platform, config interface{}) string { func parsePaymentPlatformConfig(ctx context.Context, platform payment.Platform, config interface{}) string {
data, err := json.Marshal(config) data, err := json.Marshal(config)
if err != nil { if err != nil {
logger.WithContext(ctx).Errorw("parse payment platform config error", logger.Field("platform", platform), logger.Field("config", config), logger.Field("error", err.Error())) logger.WithContext(ctx).Errorw("marshal config error", logger.Field("platform", platform), logger.Field("config", config), logger.Field("error", err.Error()))
return ""
} }
// 通用处理函数
handleConfig := func(name string, target interface {
Unmarshal([]byte) error
Marshal() ([]byte, error)
}) string {
if err = target.Unmarshal(data); err != nil {
logger.WithContext(ctx).Errorw("parse "+name+" config error", logger.Field("config", string(data)), logger.Field("error", err.Error()))
return ""
}
content, err := target.Marshal()
if err != nil {
logger.WithContext(ctx).Errorw("marshal "+name+" config error", logger.Field("error", err.Error()))
return ""
}
return string(content)
}
switch platform { switch platform {
case payment.Stripe: case payment.Stripe:
stripe := &paymentModel.StripeConfig{} return handleConfig("Stripe", &paymentModel.StripeConfig{})
if err := stripe.Unmarshal(string(data)); err != nil {
logger.WithContext(ctx).Errorw("parse stripe config error", logger.Field("config", string(data)), logger.Field("error", err.Error()))
}
return stripe.Marshal()
case payment.AlipayF2F: case payment.AlipayF2F:
alipay := &paymentModel.AlipayF2FConfig{} return handleConfig("Alipay", &paymentModel.AlipayF2FConfig{})
if err := alipay.Unmarshal(string(data)); err != nil {
logger.WithContext(ctx).Errorw("parse alipay config error", logger.Field("config", string(data)), logger.Field("error", err.Error()))
}
return alipay.Marshal()
case payment.EPay: case payment.EPay:
epay := &paymentModel.EPayConfig{} return handleConfig("Epay", &paymentModel.EPayConfig{})
if err := epay.Unmarshal(string(data)); err != nil { case payment.CryptoSaaS:
logger.WithContext(ctx).Errorw("parse epay config error", logger.Field("config", string(data)), logger.Field("error", err.Error())) return handleConfig("CryptoSaaS", &paymentModel.CryptoSaaSConfig{})
}
return epay.Marshal()
default: default:
return "" return ""
} }

View File

@ -19,7 +19,7 @@ type UpdatePaymentMethodLogic struct {
svcCtx *svc.ServiceContext svcCtx *svc.ServiceContext
} }
// Update Payment Method // NewUpdatePaymentMethodLogic Update Payment Method
func NewUpdatePaymentMethodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePaymentMethodLogic { func NewUpdatePaymentMethodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePaymentMethodLogic {
return &UpdatePaymentMethodLogic{ return &UpdatePaymentMethodLogic{
Logger: logger.WithContext(ctx), Logger: logger.WithContext(ctx),

View File

@ -106,6 +106,17 @@ func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest
CheckoutUrl: url, CheckoutUrl: url,
} }
case paymentPlatform.CryptoSaaS:
// Process EPay payment - generates payment URL for redirect
url, err := l.epayPayment(paymentConfig, orderInfo, req.ReturnUrl)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "epayPayment error: %v", err.Error())
}
resp = &types.CheckoutOrderResponse{
CheckoutUrl: url,
Type: "url", // Client should redirect to URL
}
case paymentPlatform.Balance: case paymentPlatform.Balance:
// Process balance payment - validate user and process payment immediately // Process balance payment - validate user and process payment immediately
if orderInfo.UserId == 0 { if orderInfo.UserId == 0 {
@ -140,8 +151,8 @@ func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest
// It handles currency conversion and creates a pre-payment trade for QR code scanning // It handles currency conversion and creates a pre-payment trade for QR code scanning
func (l *PurchaseCheckoutLogic) alipayF2fPayment(pay *payment.Payment, info *order.Order) (string, error) { func (l *PurchaseCheckoutLogic) alipayF2fPayment(pay *payment.Payment, info *order.Order) (string, error) {
// Parse Alipay F2F configuration from payment settings // Parse Alipay F2F configuration from payment settings
f2FConfig := payment.AlipayF2FConfig{} f2FConfig := &payment.AlipayF2FConfig{}
if err := json.Unmarshal([]byte(pay.Config), &f2FConfig); err != nil { if err := f2FConfig.Unmarshal([]byte(pay.Config)); err != nil {
l.Errorw("[PurchaseCheckout] Unmarshal Alipay config error", logger.Field("error", err.Error())) l.Errorw("[PurchaseCheckout] Unmarshal Alipay config error", logger.Field("error", err.Error()))
return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error())
} }
@ -191,8 +202,9 @@ func (l *PurchaseCheckoutLogic) alipayF2fPayment(pay *payment.Payment, info *ord
// It supports various payment methods including WeChat Pay and Alipay through Stripe // It supports various payment methods including WeChat Pay and Alipay through Stripe
func (l *PurchaseCheckoutLogic) stripePayment(config string, info *order.Order, identifier string) (*types.StripePayment, error) { func (l *PurchaseCheckoutLogic) stripePayment(config string, info *order.Order, identifier string) (*types.StripePayment, error) {
// Parse Stripe configuration from payment settings // Parse Stripe configuration from payment settings
stripeConfig := payment.StripeConfig{} stripeConfig := &payment.StripeConfig{}
if err := json.Unmarshal([]byte(config), &stripeConfig); err != nil {
if err := stripeConfig.Unmarshal([]byte(config)); err != nil {
l.Errorw("[PurchaseCheckout] Unmarshal Stripe config error", logger.Field("error", err.Error())) l.Errorw("[PurchaseCheckout] Unmarshal Stripe config error", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error())
} }
@ -249,12 +261,11 @@ func (l *PurchaseCheckoutLogic) stripePayment(config string, info *order.Order,
// It handles currency conversion and creates a payment URL for external payment processing // It handles currency conversion and creates a payment URL for external payment processing
func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order.Order, returnUrl string) (string, error) { func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order.Order, returnUrl string) (string, error) {
// Parse EPay configuration from payment settings // Parse EPay configuration from payment settings
epayConfig := payment.EPayConfig{} epayConfig := &payment.EPayConfig{}
if err := json.Unmarshal([]byte(config.Config), &epayConfig); err != nil { if err := epayConfig.Unmarshal([]byte(config.Config)); err != nil {
l.Errorw("[PurchaseCheckout] Unmarshal EPay config error", logger.Field("error", err.Error())) l.Errorw("[PurchaseCheckout] Unmarshal EPay config error", logger.Field("error", err.Error()))
return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error())
} }
// Initialize EPay client with merchant credentials // Initialize EPay client with merchant credentials
client := epay.NewClient(epayConfig.Pid, epayConfig.Url, epayConfig.Key) client := epay.NewClient(epayConfig.Pid, epayConfig.Url, epayConfig.Key)
@ -288,6 +299,48 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order
return url, nil return url, nil
} }
// CryptoSaaSPayment processes CryptoSaaSPayment payment by generating a payment URL for redirect
// It handles currency conversion and creates a payment URL for external payment processing
func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info *order.Order, returnUrl string) (string, error) {
// Parse EPay configuration from payment settings
epayConfig := &payment.CryptoSaaSConfig{}
if err := epayConfig.Unmarshal([]byte(config.Config)); err != nil {
l.Errorw("[PurchaseCheckout] Unmarshal EPay config error", logger.Field("error", err.Error()))
return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error())
}
// Initialize EPay client with merchant credentials
client := epay.NewClient(epayConfig.AccountID, epayConfig.Endpoint, epayConfig.SecretKey)
// Convert order amount to CNY using current exchange rate
amount, err := l.queryExchangeRate("CNY", info.Amount)
if err != nil {
return "", err
}
// Build notification URL for payment status callbacks
notifyUrl := ""
if config.Domain != "" {
notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token
} else {
host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string)
if !ok {
host = l.svcCtx.Config.Host
}
notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token
}
// Create payment URL for user redirection
url := client.CreatePayUrl(epay.Order{
Name: l.svcCtx.Config.Site.SiteName,
Amount: amount,
OrderNo: info.OrderNo,
SignType: "MD5",
NotifyUrl: notifyUrl,
ReturnUrl: returnUrl,
})
return url, nil
}
// queryExchangeRate converts the order amount from system currency to target currency // queryExchangeRate converts the order amount from system currency to target currency
// It retrieves the current exchange rate and performs currency conversion if needed // It retrieves the current exchange rate and performs currency conversion if needed
func (l *PurchaseCheckoutLogic) queryExchangeRate(to string, src int64) (amount float64, err error) { func (l *PurchaseCheckoutLogic) queryExchangeRate(to string, src int64) (amount float64, err error) {

View File

@ -46,13 +46,19 @@ type StripeConfig struct {
Payment string `json:"payment"` Payment string `json:"payment"`
} }
func (l *StripeConfig) Marshal() string { func (l *StripeConfig) Marshal() ([]byte, error) {
b, _ := json.Marshal(l) type Alias StripeConfig
return string(b) return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(l),
})
} }
func (l *StripeConfig) Unmarshal(s string) error { func (l *StripeConfig) Unmarshal(data []byte) error {
return json.Unmarshal([]byte(s), l) type Alias StripeConfig
aux := (*Alias)(l)
return json.Unmarshal(data, &aux)
} }
type AlipayF2FConfig struct { type AlipayF2FConfig struct {
@ -63,13 +69,19 @@ type AlipayF2FConfig struct {
Sandbox bool `json:"sandbox"` Sandbox bool `json:"sandbox"`
} }
func (l *AlipayF2FConfig) Marshal() string { func (l *AlipayF2FConfig) Marshal() ([]byte, error) {
b, _ := json.Marshal(l) type Alias AlipayF2FConfig
return string(b) return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(l),
})
} }
func (l *AlipayF2FConfig) Unmarshal(s string) error { func (l *AlipayF2FConfig) Unmarshal(data []byte) error {
return json.Unmarshal([]byte(s), l) type Alias AlipayF2FConfig
aux := (*Alias)(l)
return json.Unmarshal(data, &aux)
} }
type EPayConfig struct { type EPayConfig struct {
@ -78,11 +90,38 @@ type EPayConfig struct {
Key string `json:"key"` Key string `json:"key"`
} }
func (l *EPayConfig) Marshal() string { func (l *EPayConfig) Marshal() ([]byte, error) {
b, _ := json.Marshal(l) type Alias EPayConfig
return string(b) return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(l),
})
} }
func (l *EPayConfig) Unmarshal(s string) error { func (l *EPayConfig) Unmarshal(data []byte) error {
return json.Unmarshal([]byte(s), l) type Alias EPayConfig
aux := (*Alias)(l)
return json.Unmarshal(data, &aux)
}
type CryptoSaaSConfig struct {
Endpoint string `json:"endpoint"`
AccountID string `json:"account_id"`
SecretKey string `json:"secret_key"`
}
func (l *CryptoSaaSConfig) Marshal() ([]byte, error) {
type Alias CryptoSaaSConfig
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(l),
})
}
func (l *CryptoSaaSConfig) Unmarshal(data []byte) error {
type Alias CryptoSaaSConfig
aux := (*Alias)(l)
return json.Unmarshal(data, &aux)
} }

View File

@ -9,10 +9,12 @@ const (
AlipayF2F AlipayF2F
EPay EPay
Balance Balance
UNSUPPORTED CryptoSaaS
UNSUPPORTED Platform = -1
) )
var platformNames = map[string]Platform{ var platformNames = map[string]Platform{
"CryptoSaaS": CryptoSaaS,
"Stripe": Stripe, "Stripe": Stripe,
"AlipayF2F": AlipayF2F, "AlipayF2F": AlipayF2F,
"EPay": EPay, "EPay": EPay,
@ -68,5 +70,14 @@ func GetSupportedPlatforms() []types.PlatformInfo {
"key": "Key", "key": "Key",
}, },
}, },
{
Platform: CryptoSaaS.String(),
PlatformUrl: "https://t.me/CryptoSaaSBot",
PlatformFieldDescription: map[string]string{
"endpoint": "API Endpoint",
"account_id": "Account ID",
"secret_key": "Secret Key",
},
},
} }
} }