353 lines
9.3 KiB
Go
353 lines
9.3 KiB
Go
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"
|
|
)
|