Files
hfb_sys/backend/internal/modules/payment/channel_adapter.go
T

600 lines
18 KiB
Go

package payment
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"hfb_sys/backend/internal/integrations/payment/lakala"
"hfb_sys/backend/internal/integrations/payment/leshua"
"hfb_sys/backend/internal/integrations/payment/shuncheng"
"hfb_sys/backend/internal/modules/paymentconfig"
)
type channelClient interface {
CreatePayment(ctx context.Context, req channelCreatePaymentRequest) (*channelCreatePaymentResponse, error)
QueryPayment(ctx context.Context, thirdOrderID string, providerOrderID string) (*channelQueryPaymentResponse, error)
CreateRefund(ctx context.Context, req channelCreateRefundRequest) (*channelCreateRefundResponse, error)
QueryRefund(ctx context.Context, req channelQueryRefundRequest) (*channelQueryRefundResponse, error)
VerifyNotify(params map[string]string, rawPayload string, contentType string, authorization string) (channelVerifyNotifyResult, error)
}
type channelCreatePaymentRequest struct {
ThirdOrderID string
AmountCent int64
PayWay string
JSPayFlag string
NotifyURL string
JumpURL string
ClientIP string
Body string
Attach string
}
type channelCreatePaymentResponse struct {
OK bool
ErrorMessage string
ProviderOrderID string
PayWay string
Status string
PayTime string
TDCode string
JSPayURL string
JSPayInfo string
RawRequest map[string]string
Raw map[string]string
}
type channelQueryPaymentResponse struct {
OK bool
ErrorMessage string
ProviderOrderID string
Status string
Amount string
PayWay string
PayTime string
Raw map[string]string
}
type channelCreateRefundRequest struct {
ThirdOrderID string
ProviderOrderID string
MerchantRefundID string
RefundAmountCent int64
NotifyURL string
Attach string
Remark string
ClientIP string
}
type channelCreateRefundResponse struct {
OK bool
ErrorMessage string
ProviderRefundID string
Status string
RefundAmount string
RawRequest map[string]string
Raw map[string]string
}
type channelQueryRefundRequest struct {
ThirdOrderID string
ProviderOrderID string
MerchantRefundID string
ProviderRefundID string
}
type channelQueryRefundResponse struct {
OK bool
ErrorMessage string
ProviderRefundID string
Status string
RefundAmount string
RefundTime string
RawRequest map[string]string
Raw map[string]string
}
type channelVerifyNotifyResult struct {
OK bool
MatchedKey string
Got string
Expected map[string]string
BaseString map[string]string
ParamKeys []string
}
type leshuaChannel struct {
client *leshua.Client
}
func newLeshuaChannel(cfg leshua.Config) channelClient {
return leshuaChannel{client: leshua.NewClient(cfg)}
}
func (c leshuaChannel) CreatePayment(ctx context.Context, req channelCreatePaymentRequest) (*channelCreatePaymentResponse, error) {
resp, rawReq, err := c.client.CreatePayment(ctx, leshua.CreatePaymentRequest{
ThirdOrderID: req.ThirdOrderID,
AmountCent: req.AmountCent,
PayWay: req.PayWay,
JSPayFlag: req.JSPayFlag,
NotifyURL: req.NotifyURL,
JumpURL: req.JumpURL,
ClientIP: req.ClientIP,
Body: req.Body,
Attach: req.Attach,
})
if err != nil {
return nil, err
}
return &channelCreatePaymentResponse{
OK: resp.RespCode == "0" && resp.ResultCode == "0",
ErrorMessage: resp.ErrorMessage,
ProviderOrderID: resp.ProviderOrderID,
PayWay: resp.PayWay,
Status: normalizeLeshuaPaymentStatus(resp.Raw["status"]),
PayTime: resp.Raw["pay_time"],
TDCode: resp.TDCode,
JSPayURL: resp.JSPayURL,
JSPayInfo: resp.JSPayInfo,
RawRequest: rawReq,
Raw: resp.Raw,
}, nil
}
func (c leshuaChannel) QueryPayment(ctx context.Context, thirdOrderID string, providerOrderID string) (*channelQueryPaymentResponse, error) {
resp, err := c.client.QueryPayment(ctx, thirdOrderID, providerOrderID)
if err != nil {
return nil, err
}
return &channelQueryPaymentResponse{
OK: resp.RespCode == "0" && resp.ResultCode == "0",
ErrorMessage: resp.ErrorMessage,
ProviderOrderID: resp.ProviderOrderID,
Status: normalizeLeshuaPaymentStatus(resp.Status),
Amount: resp.Amount,
PayWay: resp.PayWay,
PayTime: resp.PayTime,
Raw: resp.Raw,
}, nil
}
func (c leshuaChannel) CreateRefund(ctx context.Context, req channelCreateRefundRequest) (*channelCreateRefundResponse, error) {
resp, rawReq, err := c.client.CreateRefund(ctx, leshua.CreateRefundRequest{
ThirdOrderID: req.ThirdOrderID,
LeshuaOrderID: req.ProviderOrderID,
MerchantRefundID: req.MerchantRefundID,
RefundAmountCent: req.RefundAmountCent,
NotifyURL: req.NotifyURL,
Attach: req.Attach,
})
if err != nil {
if rawReq == nil {
return nil, err
}
return &channelCreateRefundResponse{
RawRequest: rawReq,
}, err
}
return &channelCreateRefundResponse{
OK: resp.RespCode == "0" && resp.ResultCode == "0",
ErrorMessage: resp.ErrorMessage,
ProviderRefundID: resp.LeshuaRefundID,
Status: normalizeLeshuaRefundStatus(resp.Status),
RefundAmount: resp.RefundAmount,
RawRequest: rawReq,
Raw: resp.Raw,
}, nil
}
func (c leshuaChannel) QueryRefund(ctx context.Context, req channelQueryRefundRequest) (*channelQueryRefundResponse, error) {
resp, err := c.client.QueryRefund(ctx, leshua.QueryRefundRequest{
ThirdOrderID: req.ThirdOrderID,
LeshuaOrderID: req.ProviderOrderID,
MerchantRefundID: req.MerchantRefundID,
LeshuaRefundID: req.ProviderRefundID,
})
if err != nil {
return nil, err
}
return &channelQueryRefundResponse{
OK: resp.RespCode == "0" && resp.ResultCode == "0",
ErrorMessage: resp.ErrorMessage,
ProviderRefundID: resp.LeshuaRefundID,
Status: normalizeLeshuaRefundStatus(resp.Status),
RefundAmount: resp.RefundAmount,
RefundTime: resp.RefundTime,
Raw: resp.Raw,
}, nil
}
func (c leshuaChannel) VerifyNotify(params map[string]string, rawPayload string, contentType string, authorization string) (channelVerifyNotifyResult, error) {
verify := c.client.VerifyNotifyDetail(params)
result := channelVerifyNotifyResult{
OK: verify.OK,
MatchedKey: verify.MatchedKey,
Got: verify.Got,
Expected: verify.Expected,
BaseString: verify.BaseString,
ParamKeys: verify.ParamKeys,
}
if !result.OK {
return result, ErrPaymentVerifyFailed
}
return result, nil
}
type lakalaChannel struct {
client *lakala.Client
}
func newLakalaChannel(cfg lakala.Config) channelClient {
return lakalaChannel{client: lakala.NewClient(cfg)}
}
func (c lakalaChannel) CreatePayment(ctx context.Context, req channelCreatePaymentRequest) (*channelCreatePaymentResponse, error) {
resp, err := c.client.CreatePayment(ctx, lakala.CreatePaymentRequest{
ThirdOrderID: req.ThirdOrderID,
AmountCent: req.AmountCent,
PayWay: req.PayWay,
JSPayFlag: req.JSPayFlag,
NotifyURL: req.NotifyURL,
JumpURL: req.JumpURL,
ClientIP: req.ClientIP,
Body: req.Body,
Attach: req.Attach,
})
if err != nil {
return nil, err
}
return &channelCreatePaymentResponse{
OK: resp.OK,
ErrorMessage: resp.ErrorMessage,
ProviderOrderID: resp.ProviderOrderID,
PayWay: resp.PayWay,
Status: resp.Status,
PayTime: resp.PayTime,
TDCode: resp.TDCode,
JSPayURL: resp.JSPayURL,
JSPayInfo: resp.JSPayInfo,
RawRequest: resp.RawRequest,
Raw: resp.Raw,
}, nil
}
func (c lakalaChannel) QueryPayment(ctx context.Context, thirdOrderID string, providerOrderID string) (*channelQueryPaymentResponse, error) {
resp, err := c.client.QueryPayment(ctx, thirdOrderID, providerOrderID)
if err != nil {
return nil, err
}
return &channelQueryPaymentResponse{
OK: resp.OK,
ErrorMessage: resp.ErrorMessage,
ProviderOrderID: resp.ProviderOrderID,
Status: resp.Status,
Amount: resp.Amount,
PayWay: resp.PayWay,
PayTime: resp.PayTime,
Raw: resp.Raw,
}, nil
}
func (c lakalaChannel) CreateRefund(ctx context.Context, req channelCreateRefundRequest) (*channelCreateRefundResponse, error) {
resp, err := c.client.CreateRefund(ctx, lakala.CreateRefundRequest{
ThirdOrderID: req.ThirdOrderID,
ProviderOrderID: req.ProviderOrderID,
MerchantRefundID: req.MerchantRefundID,
RefundAmountCent: req.RefundAmountCent,
NotifyURL: req.NotifyURL,
Attach: req.Attach,
RefundReason: req.Remark,
ClientIP: req.ClientIP,
})
if err != nil {
if resp == nil {
return nil, err
}
return &channelCreateRefundResponse{
RawRequest: resp.RawRequest,
}, err
}
return &channelCreateRefundResponse{
OK: resp.OK,
ErrorMessage: resp.ErrorMessage,
ProviderRefundID: resp.ProviderRefundID,
Status: resp.Status,
RefundAmount: resp.RefundAmount,
RawRequest: resp.RawRequest,
Raw: resp.Raw,
}, nil
}
func (c lakalaChannel) QueryRefund(ctx context.Context, req channelQueryRefundRequest) (*channelQueryRefundResponse, error) {
resp, err := c.client.QueryRefund(ctx, lakala.QueryRefundRequest{
ThirdOrderID: req.ThirdOrderID,
ProviderOrderID: req.ProviderOrderID,
MerchantRefundID: req.MerchantRefundID,
ProviderRefundID: req.ProviderRefundID,
})
if err != nil {
if resp == nil {
return nil, err
}
return &channelQueryRefundResponse{
RawRequest: resp.RawRequest,
}, err
}
return &channelQueryRefundResponse{
OK: resp.OK,
ErrorMessage: resp.ErrorMessage,
ProviderRefundID: resp.ProviderRefundID,
Status: resp.Status,
RefundAmount: resp.RefundAmount,
RefundTime: resp.RefundTime,
RawRequest: resp.RawRequest,
Raw: resp.Raw,
}, nil
}
func (c lakalaChannel) VerifyNotify(params map[string]string, rawPayload string, contentType string, authorization string) (channelVerifyNotifyResult, error) {
verify := c.client.VerifyNotifyDetail(rawPayload, authorization)
result := channelVerifyNotifyResult{
OK: verify.OK,
MatchedKey: verify.MatchedKey,
Got: verify.Got,
Expected: verify.Expected,
BaseString: verify.BaseString,
ParamKeys: verify.ParamKeys,
}
if !result.OK {
return result, ErrPaymentVerifyFailed
}
return result, nil
}
type shunchengChannel struct {
client *shuncheng.Client
}
func newShunchengChannel(cfg shuncheng.Config) channelClient {
return shunchengChannel{client: shuncheng.NewClient(cfg)}
}
func (c shunchengChannel) CreatePayment(ctx context.Context, req channelCreatePaymentRequest) (*channelCreatePaymentResponse, error) {
resp, err := c.client.CreatePayment(ctx, shuncheng.CreatePaymentRequest{
ThirdOrderID: req.ThirdOrderID,
AmountCent: req.AmountCent,
PayWay: req.PayWay,
JSPayFlag: req.JSPayFlag,
NotifyURL: req.NotifyURL,
JumpURL: req.JumpURL,
ClientIP: req.ClientIP,
Body: req.Body,
Attach: req.Attach,
})
if err != nil {
return nil, err
}
return &channelCreatePaymentResponse{
OK: resp.OK,
ErrorMessage: resp.ErrorMessage,
ProviderOrderID: resp.ProviderOrderID,
PayWay: resp.PayWay,
Status: resp.Status,
PayTime: resp.PayTime,
TDCode: resp.TDCode,
JSPayURL: resp.JSPayURL,
JSPayInfo: resp.JSPayInfo,
RawRequest: resp.RawRequest,
Raw: resp.Raw,
}, nil
}
func (c shunchengChannel) QueryPayment(ctx context.Context, thirdOrderID string, providerOrderID string) (*channelQueryPaymentResponse, error) {
resp, err := c.client.QueryPayment(ctx, thirdOrderID, providerOrderID)
if err != nil {
return nil, err
}
return &channelQueryPaymentResponse{
OK: resp.OK,
ErrorMessage: resp.ErrorMessage,
ProviderOrderID: resp.ProviderOrderID,
Status: resp.Status,
Amount: resp.Amount,
PayWay: resp.PayWay,
PayTime: resp.PayTime,
Raw: resp.Raw,
}, nil
}
func (c shunchengChannel) CreateRefund(ctx context.Context, req channelCreateRefundRequest) (*channelCreateRefundResponse, error) {
resp, err := c.client.CreateRefund(ctx, shuncheng.CreateRefundRequest{
ThirdOrderID: req.ThirdOrderID,
ProviderOrderID: req.ProviderOrderID,
MerchantRefundID: req.MerchantRefundID,
RefundAmountCent: req.RefundAmountCent,
NotifyURL: req.NotifyURL,
Attach: req.Attach,
RefundReason: req.Remark,
ClientIP: req.ClientIP,
})
if err != nil {
return nil, err
}
return &channelCreateRefundResponse{
OK: resp.OK,
ErrorMessage: resp.ErrorMessage,
ProviderRefundID: resp.ProviderRefundID,
Status: resp.Status,
RefundAmount: resp.RefundAmount,
RawRequest: resp.RawRequest,
Raw: resp.Raw,
}, nil
}
func (c shunchengChannel) QueryRefund(ctx context.Context, req channelQueryRefundRequest) (*channelQueryRefundResponse, error) {
resp, err := c.client.QueryRefund(ctx, shuncheng.QueryRefundRequest{
ThirdOrderID: req.ThirdOrderID,
ProviderOrderID: req.ProviderOrderID,
MerchantRefundID: req.MerchantRefundID,
ProviderRefundID: req.ProviderRefundID,
})
if err != nil {
return nil, err
}
return &channelQueryRefundResponse{
OK: resp.OK,
ErrorMessage: resp.ErrorMessage,
ProviderRefundID: resp.ProviderRefundID,
Status: resp.Status,
RefundAmount: resp.RefundAmount,
RefundTime: resp.RefundTime,
Raw: resp.Raw,
}, nil
}
func (c shunchengChannel) VerifyNotify(params map[string]string, rawPayload string, contentType string, authorization string) (channelVerifyNotifyResult, error) {
verify := c.client.VerifyNotifyDetail(params)
result := channelVerifyNotifyResult{
OK: verify.OK,
MatchedKey: verify.MatchedKey,
Got: verify.Got,
Expected: verify.Expected,
BaseString: verify.BaseString,
ParamKeys: verify.ParamKeys,
}
if !result.OK {
return result, ErrPaymentVerifyFailed
}
return result, nil
}
func buildChannelClient(dto *paymentconfig.ConfigDTO) (channelClient, error) {
switch strings.ToLower(strings.TrimSpace(dto.Provider)) {
case "leshua":
return withChannelBreaker("leshua", newLeshuaChannel(leshua.Config{
GatewayURL: dto.GatewayURL,
MerchantID: dto.MerchantID,
SignKey: dto.SignKey,
NotifyKey: dto.NotifyKey,
NotifyURL: dto.NotifyURL,
JumpURL: dto.JumpURL,
PayWay: firstNonEmpty(dto.PayWay, "ZFBZF"),
JSPayFlag: firstNonEmpty(dto.JSPayFlag, "2"),
SignType: firstNonEmpty(dto.SignType, "MD5"),
})), nil
case "lakala":
return withChannelBreaker("lakala", newLakalaChannel(lakala.Config{
GatewayURL: dto.GatewayURL,
AppID: extraString(dto.ExtraConfig, "app_id"),
SerialNo: extraString(dto.ExtraConfig, "serial_no"),
MerchantID: dto.MerchantID,
TermNo: extraString(dto.ExtraConfig, "term_no"),
PrivateKey: dto.SignKey,
LakalaCert: extraString(dto.ExtraConfig, "lakala_cert"),
NotifyCert: dto.NotifyKey,
NotifyURL: dto.NotifyURL,
JumpURL: dto.JumpURL,
PayWay: firstNonEmpty(dto.PayWay, "ZFBZF"),
JSPayFlag: firstNonEmpty(dto.JSPayFlag, "2"),
PayMode: extraString(dto.ExtraConfig, "pay_mode"),
OrderExpireMinutes: extraInt(dto.ExtraConfig, "order_expire_minutes"),
})), nil
case "shuncheng":
return withChannelBreaker("shuncheng", newShunchengChannel(shuncheng.Config{
GatewayURL: dto.GatewayURL,
MerchantID: dto.MerchantID,
SecretID: extraString(dto.ExtraConfig, "secret_id"),
SecretKey: dto.SignKey,
NotifyKey: dto.NotifyKey,
NotifyURL: dto.NotifyURL,
JumpURL: dto.JumpURL,
PayWay: firstNonEmpty(dto.PayWay, "ZFBZF"),
JSPayFlag: firstNonEmpty(dto.JSPayFlag, "2"),
SignType: firstNonEmpty(dto.SignType, "MD5"),
ShopNo: extraString(dto.ExtraConfig, "shop_no"),
RefundType: extraString(dto.ExtraConfig, "refund_type"),
TokenMargin: time.Duration(extraInt(dto.ExtraConfig, "token_refresh_margin_seconds")) * time.Second,
})), nil
case "mock":
return nil, nil
default:
return nil, fmt.Errorf("unsupported payment provider: %s", dto.Provider)
}
}
func normalizeLeshuaPaymentStatus(status string) string {
switch status {
case "2", "30":
return "paid"
case "6":
return "closed"
case "8":
return "failed"
default:
return "paying"
}
}
func normalizeLeshuaRefundStatus(status string) string {
switch status {
case "11":
return "refunded"
case "12":
return "failed"
default:
return "refunding"
}
}
func normalizeNotifyPaymentStatus(provider string, status string) string {
if provider == "leshua" || provider == "shuncheng" {
return normalizeLeshuaPaymentStatus(status)
}
switch status {
case "paid", "closed", "failed", "paying":
return status
default:
return "paying"
}
}
func normalizeNotifyRefundStatus(provider string, status string) string {
if provider == "leshua" || provider == "shuncheng" {
return normalizeLeshuaRefundStatus(status)
}
switch status {
case "refunded", "failed", "refunding":
return status
default:
return "refunding"
}
}
func extraString(config map[string]any, key string) string {
if config == nil {
return ""
}
value, ok := config[key]
if !ok || value == nil {
return ""
}
switch typed := value.(type) {
case string:
return strings.TrimSpace(typed)
default:
return strings.TrimSpace(fmt.Sprint(typed))
}
}
func extraInt(config map[string]any, key string) int {
value := extraString(config, key)
if value == "" {
return 0
}
n, _ := strconv.Atoi(value)
return n
}