448 lines
13 KiB
Go
448 lines
13 KiB
Go
package payment
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"hfb_sys/backend/internal/integrations/payment/lakala"
|
|
"hfb_sys/backend/internal/integrations/payment/leshua"
|
|
"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
|
|
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 {
|
|
return nil, 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 {
|
|
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 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 {
|
|
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 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
|
|
}
|
|
|
|
func buildChannelClient(dto *paymentconfig.ConfigDTO) (channelClient, error) {
|
|
switch strings.ToLower(strings.TrimSpace(dto.Provider)) {
|
|
case "leshua":
|
|
return 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 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 "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" {
|
|
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" {
|
|
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
|
|
}
|