package globals import ( "crypto" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/json" "encoding/pem" "fmt" "io" "net/http" "platform/pkg/env" "platform/pkg/u" "strings" "time" ) var SFTPay SftClient type SftClient struct { appid string privateKey *rsa.PrivateKey publicKey *rsa.PublicKey } func init() { if !env.SftPayEnable { return } SFTPay = SftClient{ appid: env.SftPayAppId, } // 加载私钥 block, _ := pem.Decode([]byte(env.SftPayAppPrivateKey)) if block == nil || block.Type != "RSA PRIVATE KEY" { panic("加载商福通私钥失败") } var privateKey *rsa.PrivateKey privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { pkcs8, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { panic("解析商福通私钥失败: " + err.Error()) } var ok bool privateKey, ok = pkcs8.(*rsa.PrivateKey) if !ok { panic("解析商福通私钥失败") } } SFTPay.privateKey = privateKey // 加载公钥 block, _ = pem.Decode([]byte(env.SftPayPublicKey)) if block == nil || block.Type != "PUBLIC KEY" { panic("加载商福通公钥失败") } var publicKey *rsa.PublicKey pkix, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { panic("解析商福通公钥失败: " + err.Error()) } var ok bool publicKey, ok = pkix.(*rsa.PublicKey) if !ok { panic("解析商福通公钥失败") } SFTPay.publicKey = publicKey } func (s *SftClient) PaymentScanPay(req *PaymentScanPayReq) (*PaymentScanPayResp, error) { const url = "https://pay.rscygroup.com/api/open/payment/scanpay" return call[PaymentScanPayResp](s, url, req) } func (s *SftClient) PaymentH5Pay(req *PaymentH5PayReq) (*PaymentH5PayResp, error) { const url = "https://pay.rscygroup.com/api/open/payment/h5pay" 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) } type PaymentScanPayReq struct { Subject string `json:"subject"` Body string `json:"body"` Amount int64 `json:"amount"` Currency string `json:"currency"` ClientIp string `json:"clientIp"` MchOrderNo string `json:"mchOrderNo"` StoreId *string `json:"storeId"` RouteNo *string `json:"routeNo"` HbFqNum *int `json:"hbFqNum"` HbFqPercent *int `json:"hbFqPercent"` BuyerRemark *string `json:"buyerRemark"` NotifyUrl *string `json:"notifyUrl"` ReturnUrl *string `json:"returnUrl"` ExpiredTime *int `json:"expiredTime"` OrderTimeout *string `json:"orderTimeout"` ExtParam *string `json:"extParam"` LimitPay *int `json:"limitPay"` } 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"` StoreId *string `json:"storeId"` RouteNo *string `json:"routeNo"` HbFqNum *int `json:"hbFqNum"` HbFqPercent *int `json:"hbFqPercent"` BuyerRemark *string `json:"buyerRemark"` NotifyUrl *string `json:"notifyUrl"` ReturnUrl *string `json:"returnUrl"` ExpiredTime *int `json:"expiredTime"` OrderTimeout *string `json:"orderTimeout"` ExtParam *string `json:"extParam"` LimitPay *int `json:"limitPay"` } type OrderCloseReq struct { MchOrderNo *string `json:"mchOrderNo"` PayOrderId *string `json:"payOrderId"` } // type OrderRefundReq struct { // mchRefundNo // payOrderId // mchOrderNo // refundReason // refundAmount // notifyUrl // extParam // } type PaymentScanPayResp struct { 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"` 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 { 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"` 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 OrderCloseResp struct { MchOrderNo string `json:"mchOrderNo"` PayOrderId string `json:"payOrderId"` MercNo string `json:"mercNo"` Amount int64 `json:"amount"` State string `json:"state"` } 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 { return nil, fmt.Errorf("加密请求内容失败:%w", err) } 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 { return nil, fmt.Errorf("创建请求失败:%w", err) } response, err := http.DefaultClient.Do(request) if err != nil { return nil, fmt.Errorf("请求失败:%w", err) } if response.StatusCode != http.StatusOK { return nil, fmt.Errorf("请求响应失败:%d", response.StatusCode) } defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { } }(response.Body) body, err := io.ReadAll(response.Body) if err != nil { return nil, fmt.Errorf("读取响应内容失败:%w", err) } decode, err := s.verify(body) if err != nil { return nil, fmt.Errorf("解密响应内容失败:%w", err) } if decode.Code != "000000" { return nil, fmt.Errorf("请求业务响应失败:%s", u.Z(decode.Msg)) } if decode.BizData == nil { return nil, nil } var resp = new(T) err = json.Unmarshal([]byte(*decode.BizData), resp) if err != nil { return nil, fmt.Errorf("响应正文解析失败:%w", err) } return resp, nil } func (s *SftClient) sign(msg any) (*request, error) { bytes, err := json.Marshal(msg) if err != nil { return nil, fmt.Errorf("格式化加密正文失败:%w", err) } body := request{ AppId: s.appid, Version: "1.0", SignType: "RSA2", ReqTime: time.Now().Format("20060102150405"), ReqId: rand.Text(), BizData: string(bytes), } hashed := sha256.Sum256([]byte(body.String())) signature, err := rsa.SignPKCS1v15(nil, s.privateKey, crypto.SHA256, hashed[:]) if err != nil { return nil, fmt.Errorf("签名失败:%w", err) } body.Sign = string(signature) return &body, nil } func (s *SftClient) verify(str []byte) (*response, error) { var resp = new(response) err := json.Unmarshal(str, resp) if err != nil { return nil, fmt.Errorf("解析响应正文失败:%w", err) } if resp.Sign != nil || resp.SignType != nil || resp.BizData != nil { hashed := sha256.Sum256([]byte(resp.String())) err := rsa.VerifyPKCS1v15(s.publicKey, crypto.SHA256, hashed[:], []byte(*resp.Sign)) if err != nil { return nil, fmt.Errorf("验签失败:%w", err) } } return resp, err } 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"` } func (r request) String() string { return fmt.Sprintf( "appId=%s&bizData=%s&reqId=%s&reqTime=%s&signType=%s&version=%s", r.AppId, r.BizData, r.ReqId, r.ReqTime, r.SignType, r.Version, ) } 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"` } func (r response) String() string { 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, ) } type SftPayType string const ( SftAlipay SftPayType = "ALIPAY" SftWeChat SftPayType = "WECHAT" SftUnionPay SftPayType = "UNIONPAY" )