2025-06-04 19:02:21 +08:00
|
|
|
|
package globals
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"crypto"
|
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
|
"crypto/rsa"
|
2025-06-05 12:59:07 +08:00
|
|
|
|
"crypto/sha256"
|
2025-06-04 19:02:21 +08:00
|
|
|
|
"crypto/x509"
|
2025-06-17 10:53:05 +08:00
|
|
|
|
"encoding/base64"
|
2025-06-04 19:02:21 +08:00
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"io"
|
|
|
|
|
|
"net/http"
|
2025-06-17 10:53:05 +08:00
|
|
|
|
"net/http/httputil"
|
2025-06-04 19:02:21 +08:00
|
|
|
|
"platform/pkg/env"
|
|
|
|
|
|
"platform/pkg/u"
|
2025-06-17 10:53:05 +08:00
|
|
|
|
"platform/web/core"
|
2025-06-04 19:02:21 +08:00
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var SFTPay SftClient
|
|
|
|
|
|
|
|
|
|
|
|
type SftClient struct {
|
|
|
|
|
|
appid string
|
2025-06-17 10:53:05 +08:00
|
|
|
|
routeId string
|
2025-06-04 19:02:21 +08:00
|
|
|
|
privateKey *rsa.PrivateKey
|
|
|
|
|
|
publicKey *rsa.PublicKey
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-17 18:38:10 +08:00
|
|
|
|
func initSft() error {
|
2025-06-05 12:59:07 +08:00
|
|
|
|
if !env.SftPayEnable {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return fmt.Errorf("商福通支付未启用,请检查环境变量 SFTPAY_ENABLE")
|
2025-06-05 12:59:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-04 19:02:21 +08:00
|
|
|
|
SFTPay = SftClient{
|
2025-06-17 10:53:05 +08:00
|
|
|
|
appid: env.SftPayAppId,
|
|
|
|
|
|
routeId: env.SftPayRouteId,
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加载私钥
|
2025-06-17 10:53:05 +08:00
|
|
|
|
private, err := base64.StdEncoding.DecodeString(env.SftPayAppPrivateKey)
|
|
|
|
|
|
if err != nil {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return fmt.Errorf("解析商福通私钥失败: %w", err)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var privateKey *rsa.PrivateKey
|
2025-06-17 10:53:05 +08:00
|
|
|
|
privateKey, err = x509.ParsePKCS1PrivateKey(private)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
if err != nil {
|
2025-06-17 10:53:05 +08:00
|
|
|
|
pkcs8, err := x509.ParsePKCS8PrivateKey(private)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
if err != nil {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return fmt.Errorf("解析商福通私钥失败: %w", err)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var ok bool
|
|
|
|
|
|
privateKey, ok = pkcs8.(*rsa.PrivateKey)
|
|
|
|
|
|
if !ok {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return fmt.Errorf("解析商福通私钥失败")
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
SFTPay.privateKey = privateKey
|
|
|
|
|
|
|
|
|
|
|
|
// 加载公钥
|
2025-06-17 10:53:05 +08:00
|
|
|
|
public, err := base64.StdEncoding.DecodeString(env.SftPayPublicKey)
|
|
|
|
|
|
if err != nil {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return fmt.Errorf("解析商福通公钥失败: %w", err)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var publicKey *rsa.PublicKey
|
2025-06-17 10:53:05 +08:00
|
|
|
|
pkix, err := x509.ParsePKIXPublicKey(public)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
if err != nil {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return fmt.Errorf("解析商福通公钥失败: %w", err)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var ok bool
|
|
|
|
|
|
publicKey, ok = pkix.(*rsa.PublicKey)
|
|
|
|
|
|
if !ok {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return fmt.Errorf("解析商福通公钥失败")
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
SFTPay.publicKey = publicKey
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return nil
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-05 12:59:07 +08:00
|
|
|
|
func (s *SftClient) PaymentScanPay(req *PaymentScanPayReq) (*PaymentScanPayResp, error) {
|
|
|
|
|
|
const url = "https://pay.rscygroup.com/api/open/payment/scanpay"
|
2025-11-17 18:38:10 +08:00
|
|
|
|
req.ReturnUrl = u.X(env.SftReturnUrl)
|
|
|
|
|
|
req.NotifyUrl = u.X(env.SftNotifyUrl)
|
2025-06-17 10:53:05 +08:00
|
|
|
|
req.RouteNo = u.P(s.routeId)
|
2025-06-05 12:59:07 +08:00
|
|
|
|
return call[PaymentScanPayResp](s, url, req)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *SftClient) PaymentH5Pay(req *PaymentH5PayReq) (*PaymentH5PayResp, error) {
|
|
|
|
|
|
const url = "https://pay.rscygroup.com/api/open/payment/h5pay"
|
2025-11-17 18:38:10 +08:00
|
|
|
|
req.ReturnUrl = u.X(env.SftReturnUrl)
|
|
|
|
|
|
req.NotifyUrl = u.X(env.SftNotifyUrl)
|
2025-06-17 10:53:05 +08:00
|
|
|
|
req.RouteNo = u.P(s.routeId)
|
2025-06-05 12:59:07 +08:00
|
|
|
|
return call[PaymentH5PayResp](s, url, req)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *SftClient) OrderClose(req *OrderCloseReq) (*OrderCloseResp, error) {
|
|
|
|
|
|
const url = "https://pay.rscygroup.com/api/open/order/close"
|
|
|
|
|
|
return call[OrderCloseResp](s, url, req)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-17 10:53:05 +08:00
|
|
|
|
func (s *SftClient) QueryTrade(req *QueryTradeReq) (*QueryTradeResp, error) {
|
|
|
|
|
|
const url = "https://pay.rscygroup.com/api/open/query/trade"
|
|
|
|
|
|
return call[QueryTradeResp](s, url, req)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-04 19:02:21 +08:00
|
|
|
|
type PaymentScanPayReq struct {
|
2025-06-24 11:36:27 +08:00
|
|
|
|
Subject string `json:"subject"`
|
|
|
|
|
|
Body string `json:"body"`
|
|
|
|
|
|
Amount int64 `json:"amount"`
|
|
|
|
|
|
Currency string `json:"currency"`
|
|
|
|
|
|
PayType SftPayType `json:"payType"`
|
|
|
|
|
|
ClientIp string `json:"clientIp"`
|
|
|
|
|
|
MchOrderNo string `json:"mchOrderNo"`
|
|
|
|
|
|
StoreId *string `json:"storeId,omitempty"`
|
|
|
|
|
|
RouteNo *string `json:"routeNo,omitempty"`
|
|
|
|
|
|
HbFqNum *int `json:"hbFqNum,omitempty"`
|
|
|
|
|
|
HbFqPercent *int `json:"hbFqPercent,omitempty"`
|
|
|
|
|
|
BuyerRemark *string `json:"buyerRemark,omitempty"`
|
|
|
|
|
|
NotifyUrl *string `json:"notifyUrl,omitempty"`
|
|
|
|
|
|
ReturnUrl *string `json:"returnUrl,omitempty"`
|
|
|
|
|
|
ExpiredTime *int `json:"expiredTime,omitempty"`
|
|
|
|
|
|
OrderTimeout *string `json:"orderTimeout,omitempty"`
|
|
|
|
|
|
ExtParam *string `json:"extParam,omitempty"`
|
|
|
|
|
|
LimitPay *int `json:"limitPay,omitempty"`
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-05 12:59:07 +08:00
|
|
|
|
type PaymentH5PayReq struct {
|
|
|
|
|
|
Subject string `json:"subject"`
|
|
|
|
|
|
Body string `json:"body"`
|
|
|
|
|
|
Amount int64 `json:"amount"`
|
|
|
|
|
|
Currency string `json:"currency"`
|
|
|
|
|
|
PayType SftPayType `json:"payType"`
|
|
|
|
|
|
ClientIp string `json:"clientIp"`
|
|
|
|
|
|
MchOrderNo string `json:"mchOrderNo"`
|
2025-06-19 17:44:56 +08:00
|
|
|
|
StoreId *string `json:"storeId,omitempty"`
|
|
|
|
|
|
RouteNo *string `json:"routeNo,omitempty"`
|
|
|
|
|
|
HbFqNum *int `json:"hbFqNum,omitempty"`
|
|
|
|
|
|
HbFqPercent *int `json:"hbFqPercent,omitempty"`
|
|
|
|
|
|
BuyerRemark *string `json:"buyerRemark,omitempty"`
|
|
|
|
|
|
NotifyUrl *string `json:"notifyUrl,omitempty"`
|
|
|
|
|
|
ReturnUrl *string `json:"returnUrl,omitempty"`
|
|
|
|
|
|
ExpiredTime *int `json:"expiredTime,omitempty"`
|
|
|
|
|
|
OrderTimeout *string `json:"orderTimeout,omitempty"`
|
|
|
|
|
|
ExtParam *string `json:"extParam,omitempty"`
|
|
|
|
|
|
LimitPay *int `json:"limitPay,omitempty"`
|
2025-06-05 12:59:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-17 10:53:05 +08:00
|
|
|
|
type QueryTradeReq struct {
|
2025-06-19 17:44:56 +08:00
|
|
|
|
PayOrderId *string `json:"payOrderId,omitempty"`
|
|
|
|
|
|
MchOrderNo *string `json:"mchOrderNo,omitempty"`
|
2025-06-17 10:53:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-05 12:59:07 +08:00
|
|
|
|
type OrderCloseReq struct {
|
2025-06-19 17:44:56 +08:00
|
|
|
|
MchOrderNo *string `json:"mchOrderNo,omitempty"`
|
|
|
|
|
|
PayOrderId *string `json:"payOrderId,omitempty"`
|
2025-06-05 12:59:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// type OrderRefundReq struct {
|
|
|
|
|
|
// mchRefundNo
|
|
|
|
|
|
// payOrderId
|
|
|
|
|
|
// mchOrderNo
|
|
|
|
|
|
// refundReason
|
|
|
|
|
|
// refundAmount
|
|
|
|
|
|
// notifyUrl
|
|
|
|
|
|
// extParam
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
2025-06-04 19:02:21 +08:00
|
|
|
|
type PaymentScanPayResp struct {
|
2025-06-05 12:59:07 +08:00
|
|
|
|
Amount int64 `json:"amount"`
|
|
|
|
|
|
MchOrderNo string `json:"mchOrderNo"`
|
|
|
|
|
|
PayOrderId string `json:"payOrderId"`
|
|
|
|
|
|
MercNo string `json:"mercNo"`
|
|
|
|
|
|
ChannelSendNo *string `json:"channelSendNo"`
|
|
|
|
|
|
ChannelTradeNo *string `json:"channelTradeNo"`
|
|
|
|
|
|
State string `json:"state"`
|
|
|
|
|
|
PayType SftPayType `json:"payType"`
|
|
|
|
|
|
IfCode string `json:"ifCode"`
|
|
|
|
|
|
ExtParam *string `json:"extParam"`
|
|
|
|
|
|
PayInfo *struct {
|
|
|
|
|
|
QrCodeUrl *string `json:"qrCodeUrl"`
|
|
|
|
|
|
} `json:"payInfo"`
|
2025-06-04 19:02:21 +08:00
|
|
|
|
Note *string `json:"note"`
|
|
|
|
|
|
TradeFee *int64 `json:"tradeFee"`
|
|
|
|
|
|
StoreId *string `json:"storeId"`
|
|
|
|
|
|
Subject *string `json:"subject"`
|
|
|
|
|
|
DrType *string `json:"drType"`
|
|
|
|
|
|
RefundAmt *int64 `json:"refundAmt"`
|
|
|
|
|
|
RefundState *int `json:"refundState"`
|
|
|
|
|
|
CashFee *int64 `json:"cashFee"`
|
|
|
|
|
|
SettlementType *string `json:"settlementType"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type PaymentH5PayResp struct {
|
2025-06-05 12:59:07 +08:00
|
|
|
|
Amount int64 `json:"amount"`
|
|
|
|
|
|
MchOrderNo string `json:"mchOrderNo"`
|
|
|
|
|
|
PayOrderId string `json:"payOrderId"`
|
|
|
|
|
|
MercNo string `json:"mercNo"`
|
|
|
|
|
|
ChannelSendNo *string `json:"channelSendNo"`
|
|
|
|
|
|
ChannelTradeNo *string `json:"channelTradeNo"`
|
|
|
|
|
|
State string `json:"state"`
|
|
|
|
|
|
PayType SftPayType `json:"payType"`
|
|
|
|
|
|
IfCode string `json:"ifCode"`
|
|
|
|
|
|
ExtParam *string `json:"extParam"`
|
|
|
|
|
|
PayInfo *struct {
|
|
|
|
|
|
PayUrl *string `json:"payUrl"`
|
|
|
|
|
|
} `json:"payInfo"`
|
2025-06-04 19:02:21 +08:00
|
|
|
|
Note *string `json:"note"`
|
|
|
|
|
|
TradeFee *int64 `json:"tradeFee"`
|
|
|
|
|
|
StoreId *string `json:"storeId"`
|
|
|
|
|
|
Subject *string `json:"subject"`
|
|
|
|
|
|
DrType *string `json:"drType"`
|
|
|
|
|
|
RefundAmt *int64 `json:"refundAmt"`
|
|
|
|
|
|
RefundState *int `json:"refundState"`
|
|
|
|
|
|
CashFee *int64 `json:"cashFee"`
|
|
|
|
|
|
SettlementType *string `json:"settlementType"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-17 10:53:05 +08:00
|
|
|
|
type QueryTradeResp struct {
|
|
|
|
|
|
Amount int64 `json:"amount"`
|
|
|
|
|
|
ChannelSendNo *string `json:"channelSendNo"`
|
|
|
|
|
|
IfCode *string `json:"ifCode"`
|
|
|
|
|
|
MercNo string `json:"mercNo"`
|
|
|
|
|
|
MchOrderNo string `json:"mchOrderNo"`
|
|
|
|
|
|
PayOrderId *string `json:"payOrderId"`
|
|
|
|
|
|
PayType string `json:"payType"`
|
|
|
|
|
|
ChannelTradeNo *string `json:"channelTradeNo"`
|
|
|
|
|
|
State SftTradeState `json:"state"`
|
|
|
|
|
|
RefundAmt *int64 `json:"refundAmt"`
|
|
|
|
|
|
RefundState int32 `json:"refundState"`
|
|
|
|
|
|
DrType *string `json:"drType"`
|
|
|
|
|
|
ExtParam *string `json:"extParam"`
|
|
|
|
|
|
PayTime *string `json:"payTime"`
|
|
|
|
|
|
Subject string `json:"subject"`
|
|
|
|
|
|
TradeFee *int64 `json:"tradeFee"`
|
|
|
|
|
|
CashFee *int64 `json:"cashFee"`
|
|
|
|
|
|
StoreId *string `json:"storeId"`
|
|
|
|
|
|
UserId *string `json:"userId"`
|
|
|
|
|
|
SettlementType *string `json:"settlementType"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-05 12:59:07 +08:00
|
|
|
|
type OrderCloseResp struct {
|
|
|
|
|
|
MchOrderNo string `json:"mchOrderNo"`
|
|
|
|
|
|
PayOrderId string `json:"payOrderId"`
|
|
|
|
|
|
MercNo string `json:"mercNo"`
|
|
|
|
|
|
Amount int64 `json:"amount"`
|
|
|
|
|
|
State string `json:"state"`
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func call[T any](s *SftClient, url string, req any) (*T, error) {
|
|
|
|
|
|
if req == nil {
|
|
|
|
|
|
return nil, fmt.Errorf("请求参数不能为空")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
encode, err := s.sign(req)
|
|
|
|
|
|
if err != nil {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return nil, fmt.Errorf("加密请求内容失败: %w", err)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bytes, err := json.Marshal(encode)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("格式化请求内容失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
request, err := http.NewRequest("POST", url, strings.NewReader(string(bytes)))
|
|
|
|
|
|
if err != nil {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return nil, fmt.Errorf("创建请求失败: %w", err)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
2025-06-17 10:53:05 +08:00
|
|
|
|
request.Header.Set("Content-Type", "application/json")
|
|
|
|
|
|
|
2025-06-24 11:36:27 +08:00
|
|
|
|
if env.DebugHttpDump == true {
|
2025-06-23 11:27:28 +08:00
|
|
|
|
reqDump, err := httputil.DumpRequest(request, true)
|
|
|
|
|
|
if err != nil {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return nil, fmt.Errorf("请求内容转储失败: %w", err)
|
2025-06-23 11:27:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
println(string(reqDump) + "\n\n")
|
2025-06-17 10:53:05 +08:00
|
|
|
|
}
|
2025-06-04 19:02:21 +08:00
|
|
|
|
|
|
|
|
|
|
response, err := http.DefaultClient.Do(request)
|
|
|
|
|
|
if err != nil {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return nil, fmt.Errorf("请求失败: %w", err)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
2025-06-17 10:53:05 +08:00
|
|
|
|
|
2025-06-24 11:36:27 +08:00
|
|
|
|
if env.DebugHttpDump == true {
|
2025-06-23 11:27:28 +08:00
|
|
|
|
respDump, err := httputil.DumpResponse(response, true)
|
|
|
|
|
|
if err != nil {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return nil, fmt.Errorf("响应内容转储失败: %w", err)
|
2025-06-23 11:27:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
println(string(respDump) + "\n\n")
|
2025-06-17 10:53:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-04 19:02:21 +08:00
|
|
|
|
if response.StatusCode != http.StatusOK {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return nil, fmt.Errorf("请求响应失败: %d", response.StatusCode)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
2025-06-17 10:53:05 +08:00
|
|
|
|
defer func(body io.ReadCloser) {
|
|
|
|
|
|
_ = body.Close()
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}(response.Body)
|
|
|
|
|
|
|
|
|
|
|
|
body, err := io.ReadAll(response.Body)
|
|
|
|
|
|
if err != nil {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return nil, fmt.Errorf("读取响应内容失败: %w", err)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
decode, err := s.verify(body)
|
|
|
|
|
|
if err != nil {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return nil, fmt.Errorf("解密响应内容失败: %w", err)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var resp = new(T)
|
2025-06-17 10:53:05 +08:00
|
|
|
|
err = json.Unmarshal([]byte(decode), resp)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
if err != nil {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return nil, fmt.Errorf("响应正文解析失败: %w", err)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return resp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *SftClient) sign(msg any) (*request, error) {
|
|
|
|
|
|
|
|
|
|
|
|
bytes, err := json.Marshal(msg)
|
|
|
|
|
|
if err != nil {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return nil, fmt.Errorf("格式化加密正文失败: %w", err)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-26 09:28:42 +08:00
|
|
|
|
if env.DebugHttpDump {
|
|
|
|
|
|
pretty, _ := json.MarshalIndent(msg, "", " ")
|
|
|
|
|
|
println("content:\n" + string(pretty) + "\n\n")
|
|
|
|
|
|
}
|
2025-06-19 17:44:56 +08:00
|
|
|
|
|
2025-06-04 19:02:21 +08:00
|
|
|
|
body := request{
|
|
|
|
|
|
AppId: s.appid,
|
|
|
|
|
|
Version: "1.0",
|
|
|
|
|
|
SignType: "RSA2",
|
|
|
|
|
|
ReqTime: time.Now().Format("20060102150405"),
|
|
|
|
|
|
ReqId: rand.Text(),
|
|
|
|
|
|
BizData: string(bytes),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-05 12:59:07 +08:00
|
|
|
|
hashed := sha256.Sum256([]byte(body.String()))
|
|
|
|
|
|
signature, err := rsa.SignPKCS1v15(nil, s.privateKey, crypto.SHA256, hashed[:])
|
2025-06-04 19:02:21 +08:00
|
|
|
|
if err != nil {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return nil, fmt.Errorf("签名失败: %w", err)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-19 17:44:56 +08:00
|
|
|
|
body.Sign = base64.StdEncoding.EncodeToString(signature)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
return &body, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-17 10:53:05 +08:00
|
|
|
|
func (s *SftClient) verify(str []byte) (string, error) {
|
2025-06-04 19:02:21 +08:00
|
|
|
|
|
|
|
|
|
|
var resp = new(response)
|
|
|
|
|
|
err := json.Unmarshal(str, resp)
|
|
|
|
|
|
if err != nil {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return "", fmt.Errorf("解析响应正文失败: %w", err)
|
2025-06-17 10:53:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if resp.Code != "000000" {
|
2025-11-21 12:59:05 +08:00
|
|
|
|
return "", fmt.Errorf("接口的响应为失败: %s", u.Z(resp.Msg))
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-17 10:53:05 +08:00
|
|
|
|
if resp.Sign == nil {
|
|
|
|
|
|
return "", core.NewServErr("响应数据签名为空")
|
|
|
|
|
|
}
|
2025-06-05 12:59:07 +08:00
|
|
|
|
|
2025-06-19 17:44:56 +08:00
|
|
|
|
sign, err := base64.StdEncoding.DecodeString(*resp.Sign)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", core.NewServErr("响应数据签名 base64 解码失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-17 10:53:05 +08:00
|
|
|
|
ser, err := resp.String()
|
|
|
|
|
|
if err != nil {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return "", fmt.Errorf("格式化响应内容失败: %w", err)
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-17 10:53:05 +08:00
|
|
|
|
hashed := sha256.Sum256([]byte(ser))
|
2025-06-19 17:44:56 +08:00
|
|
|
|
err = rsa.VerifyPKCS1v15(s.publicKey, crypto.SHA256, hashed[:], sign)
|
2025-06-17 10:53:05 +08:00
|
|
|
|
if err != nil {
|
2025-11-17 18:38:10 +08:00
|
|
|
|
return "", fmt.Errorf("验签失败: %w", err)
|
2025-06-17 10:53:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return *resp.BizData, nil
|
2025-06-04 19:02:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type request struct {
|
|
|
|
|
|
AppId string `json:"appId"`
|
|
|
|
|
|
Version string `json:"version"`
|
|
|
|
|
|
SignType string `json:"signType"`
|
|
|
|
|
|
Sign string `json:"sign"`
|
|
|
|
|
|
ReqId string `json:"reqId"`
|
|
|
|
|
|
ReqTime string `json:"reqTime"`
|
|
|
|
|
|
BizData string `json:"bizData"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-05 12:59:07 +08:00
|
|
|
|
func (r request) String() string {
|
2025-06-04 19:02:21 +08:00
|
|
|
|
return fmt.Sprintf(
|
2025-06-05 12:59:07 +08:00
|
|
|
|
"appId=%s&bizData=%s&reqId=%s&reqTime=%s&signType=%s&version=%s",
|
|
|
|
|
|
r.AppId, r.BizData, r.ReqId, r.ReqTime, r.SignType, r.Version,
|
2025-06-04 19:02:21 +08:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type response struct {
|
|
|
|
|
|
Code string `json:"code"`
|
|
|
|
|
|
Msg *string `json:"msg"`
|
|
|
|
|
|
Sign *string `json:"sign"`
|
|
|
|
|
|
BizData *string `json:"bizData"`
|
|
|
|
|
|
SignType *string `json:"signType"`
|
|
|
|
|
|
Timestamp string `json:"timestamp"`
|
|
|
|
|
|
}
|
2025-06-05 12:59:07 +08:00
|
|
|
|
|
2025-06-17 10:53:05 +08:00
|
|
|
|
func (r response) String() (string, error) {
|
|
|
|
|
|
if r.BizData == nil || r.Msg == nil || r.SignType == nil {
|
|
|
|
|
|
return "", core.NewServErr(fmt.Sprintf(
|
2025-11-17 18:38:10 +08:00
|
|
|
|
"上游数据返回有空值: BizData %v,Msg %v, SignType %v",
|
2025-06-17 10:53:05 +08:00
|
|
|
|
r.BizData == nil, r.Msg == nil, r.SignType == nil,
|
|
|
|
|
|
))
|
|
|
|
|
|
}
|
2025-06-05 12:59:07 +08:00
|
|
|
|
return fmt.Sprintf(
|
|
|
|
|
|
"bizData=%s&code=%s&msg=%s&signType=%s×tamp=%s",
|
|
|
|
|
|
u.Z(r.BizData), r.Code, u.Z(r.Msg), u.Z(r.SignType), r.Timestamp,
|
2025-06-17 10:53:05 +08:00
|
|
|
|
), nil
|
2025-06-05 12:59:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type SftPayType string
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
SftAlipay SftPayType = "ALIPAY"
|
|
|
|
|
|
SftWeChat SftPayType = "WECHAT"
|
|
|
|
|
|
SftUnionPay SftPayType = "UNIONPAY"
|
|
|
|
|
|
)
|
2025-06-17 10:53:05 +08:00
|
|
|
|
|
|
|
|
|
|
type SftTradeState string
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
SftInit SftTradeState = "INIT"
|
2025-06-24 11:36:27 +08:00
|
|
|
|
SftTradeAwait SftTradeState = "TRADE_WAIT"
|
2025-06-17 10:53:05 +08:00
|
|
|
|
SftTradeSuccess SftTradeState = "TRADE_SUCCESS"
|
|
|
|
|
|
SftTradeFail SftTradeState = "TRADE_FAIL"
|
|
|
|
|
|
SftTradeCancel SftTradeState = "TRADE_CANCEL"
|
|
|
|
|
|
SftTradeRefund SftTradeState = "TRADE_REFUND"
|
|
|
|
|
|
SftRefundIng SftTradeState = "REFUND_ING"
|
|
|
|
|
|
SftTradeClosed SftTradeState = "TRADE_CLOSED"
|
|
|
|
|
|
)
|