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 }