446 lines
13 KiB
Go
446 lines
13 KiB
Go
package globals
|
||
|
||
import (
|
||
"crypto"
|
||
"crypto/rand"
|
||
"crypto/rsa"
|
||
"crypto/sha256"
|
||
"crypto/x509"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/http/httputil"
|
||
"platform/pkg/env"
|
||
"platform/pkg/u"
|
||
"platform/web/core"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
var SFTPay SftClient
|
||
|
||
type SftClient struct {
|
||
appid string
|
||
routeId string
|
||
privateKey *rsa.PrivateKey
|
||
publicKey *rsa.PublicKey
|
||
}
|
||
|
||
func initSft() error {
|
||
if !env.SftPayEnable {
|
||
return fmt.Errorf("商福通支付未启用,请检查环境变量 SFTPAY_ENABLE")
|
||
}
|
||
|
||
SFTPay = SftClient{
|
||
appid: env.SftPayAppId,
|
||
routeId: env.SftPayRouteId,
|
||
}
|
||
|
||
// 加载私钥
|
||
private, err := base64.StdEncoding.DecodeString(env.SftPayAppPrivateKey)
|
||
if err != nil {
|
||
return fmt.Errorf("解析商福通私钥失败: %w", err)
|
||
}
|
||
|
||
var privateKey *rsa.PrivateKey
|
||
privateKey, err = x509.ParsePKCS1PrivateKey(private)
|
||
if err != nil {
|
||
pkcs8, err := x509.ParsePKCS8PrivateKey(private)
|
||
if err != nil {
|
||
return fmt.Errorf("解析商福通私钥失败: %w", err)
|
||
}
|
||
|
||
var ok bool
|
||
privateKey, ok = pkcs8.(*rsa.PrivateKey)
|
||
if !ok {
|
||
return fmt.Errorf("解析商福通私钥失败")
|
||
}
|
||
}
|
||
SFTPay.privateKey = privateKey
|
||
|
||
// 加载公钥
|
||
public, err := base64.StdEncoding.DecodeString(env.SftPayPublicKey)
|
||
if err != nil {
|
||
return fmt.Errorf("解析商福通公钥失败: %w", err)
|
||
}
|
||
|
||
var publicKey *rsa.PublicKey
|
||
pkix, err := x509.ParsePKIXPublicKey(public)
|
||
if err != nil {
|
||
return fmt.Errorf("解析商福通公钥失败: %w", err)
|
||
}
|
||
|
||
var ok bool
|
||
publicKey, ok = pkix.(*rsa.PublicKey)
|
||
if !ok {
|
||
return fmt.Errorf("解析商福通公钥失败")
|
||
}
|
||
SFTPay.publicKey = publicKey
|
||
return nil
|
||
}
|
||
|
||
func (s *SftClient) PaymentScanPay(req *PaymentScanPayReq) (*PaymentScanPayResp, error) {
|
||
const url = "https://pay.rscygroup.com/api/open/payment/scanpay"
|
||
req.ReturnUrl = u.X(env.SftReturnUrl)
|
||
req.NotifyUrl = u.X(env.SftNotifyUrl)
|
||
req.RouteNo = u.P(s.routeId)
|
||
return call[PaymentScanPayResp](s, url, req)
|
||
}
|
||
|
||
func (s *SftClient) PaymentH5Pay(req *PaymentH5PayReq) (*PaymentH5PayResp, error) {
|
||
const url = "https://pay.rscygroup.com/api/open/payment/h5pay"
|
||
req.ReturnUrl = u.X(env.SftReturnUrl)
|
||
req.NotifyUrl = u.X(env.SftNotifyUrl)
|
||
req.RouteNo = u.P(s.routeId)
|
||
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)
|
||
}
|
||
|
||
func (s *SftClient) QueryTrade(req *QueryTradeReq) (*QueryTradeResp, error) {
|
||
const url = "https://pay.rscygroup.com/api/open/query/trade"
|
||
return call[QueryTradeResp](s, url, req)
|
||
}
|
||
|
||
type PaymentScanPayReq 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,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"`
|
||
}
|
||
|
||
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,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"`
|
||
}
|
||
|
||
type QueryTradeReq struct {
|
||
PayOrderId *string `json:"payOrderId,omitempty"`
|
||
MchOrderNo *string `json:"mchOrderNo,omitempty"`
|
||
}
|
||
|
||
type OrderCloseReq struct {
|
||
MchOrderNo *string `json:"mchOrderNo,omitempty"`
|
||
PayOrderId *string `json:"payOrderId,omitempty"`
|
||
}
|
||
|
||
// 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 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"`
|
||
}
|
||
|
||
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)
|
||
}
|
||
request.Header.Set("Content-Type", "application/json")
|
||
|
||
if env.DebugHttpDump == true {
|
||
reqDump, err := httputil.DumpRequest(request, true)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("请求内容转储失败: %w", err)
|
||
}
|
||
println(string(reqDump) + "\n\n")
|
||
}
|
||
|
||
response, err := http.DefaultClient.Do(request)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("请求失败: %w", err)
|
||
}
|
||
|
||
if env.DebugHttpDump == true {
|
||
respDump, err := httputil.DumpResponse(response, true)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("响应内容转储失败: %w", err)
|
||
}
|
||
println(string(respDump) + "\n\n")
|
||
}
|
||
|
||
if response.StatusCode != http.StatusOK {
|
||
return nil, fmt.Errorf("请求响应失败: %d", response.StatusCode)
|
||
}
|
||
defer func(body io.ReadCloser) {
|
||
_ = body.Close()
|
||
}(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)
|
||
}
|
||
|
||
var resp = new(T)
|
||
err = json.Unmarshal([]byte(decode), 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)
|
||
}
|
||
|
||
if env.DebugHttpDump {
|
||
pretty, _ := json.MarshalIndent(msg, "", " ")
|
||
println("content:\n" + string(pretty) + "\n\n")
|
||
}
|
||
|
||
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 = base64.StdEncoding.EncodeToString(signature)
|
||
return &body, nil
|
||
}
|
||
|
||
func (s *SftClient) verify(str []byte) (string, error) {
|
||
|
||
var resp = new(response)
|
||
err := json.Unmarshal(str, resp)
|
||
if err != nil {
|
||
return "", fmt.Errorf("解析响应正文失败: %w", err)
|
||
}
|
||
|
||
if resp.Code != "000000" {
|
||
return "", fmt.Errorf("接口的响应为失败: %s", u.Z(resp.Msg))
|
||
}
|
||
|
||
if resp.Sign == nil {
|
||
return "", core.NewServErr("响应数据签名为空")
|
||
}
|
||
|
||
sign, err := base64.StdEncoding.DecodeString(*resp.Sign)
|
||
if err != nil {
|
||
return "", core.NewServErr("响应数据签名 base64 解码失败")
|
||
}
|
||
|
||
ser, err := resp.String()
|
||
if err != nil {
|
||
return "", fmt.Errorf("格式化响应内容失败: %w", err)
|
||
}
|
||
|
||
hashed := sha256.Sum256([]byte(ser))
|
||
err = rsa.VerifyPKCS1v15(s.publicKey, crypto.SHA256, hashed[:], sign)
|
||
if err != nil {
|
||
return "", fmt.Errorf("验签失败: %w", err)
|
||
}
|
||
|
||
return *resp.BizData, nil
|
||
}
|
||
|
||
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, error) {
|
||
if r.BizData == nil || r.Msg == nil || r.SignType == nil {
|
||
return "", core.NewServErr(fmt.Sprintf(
|
||
"上游数据返回有空值: BizData %v,Msg %v, SignType %v",
|
||
r.BizData == nil, r.Msg == nil, r.SignType == nil,
|
||
))
|
||
}
|
||
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,
|
||
), nil
|
||
}
|
||
|
||
type SftPayType string
|
||
|
||
const (
|
||
SftAlipay SftPayType = "ALIPAY"
|
||
SftWeChat SftPayType = "WECHAT"
|
||
SftUnionPay SftPayType = "UNIONPAY"
|
||
)
|
||
|
||
type SftTradeState string
|
||
|
||
const (
|
||
SftInit SftTradeState = "INIT"
|
||
SftTradeAwait SftTradeState = "TRADE_WAIT"
|
||
SftTradeSuccess SftTradeState = "TRADE_SUCCESS"
|
||
SftTradeFail SftTradeState = "TRADE_FAIL"
|
||
SftTradeCancel SftTradeState = "TRADE_CANCEL"
|
||
SftTradeRefund SftTradeState = "TRADE_REFUND"
|
||
SftRefundIng SftTradeState = "REFUND_ING"
|
||
SftTradeClosed SftTradeState = "TRADE_CLOSED"
|
||
)
|