Files
platform/web/globals/shangfutong.go

426 lines
12 KiB
Go
Raw Normal View History

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() {
if !env.SftPayEnable {
panic("商福通支付未启用,请检查环境变量 SFTPAY_ENABLE")
}
SFTPay = SftClient{
appid: env.SftPayAppId,
routeId: env.SftPayRouteId,
}
// 加载私钥
private, err := base64.StdEncoding.DecodeString(env.SftPayAppPrivateKey)
if err != nil {
panic("解析商福通私钥失败: " + err.Error())
}
var privateKey *rsa.PrivateKey
privateKey, err = x509.ParsePKCS1PrivateKey(private)
if err != nil {
pkcs8, err := x509.ParsePKCS8PrivateKey(private)
if err != nil {
panic("解析商福通私钥失败: " + err.Error())
}
var ok bool
privateKey, ok = pkcs8.(*rsa.PrivateKey)
if !ok {
panic("解析商福通私钥失败")
}
}
SFTPay.privateKey = privateKey
// 加载公钥
public, err := base64.StdEncoding.DecodeString(env.SftPayPublicKey)
if err != nil {
panic("解析商福通公钥失败: " + err.Error())
}
var publicKey *rsa.PublicKey
pkix, err := x509.ParsePKIXPublicKey(public)
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"
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.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"`
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 QueryTradeReq struct {
PayOrderId *string `json:"payOrderId"`
MchOrderNo *string `json:"mchOrderNo"`
}
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 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")
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)
}
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)
}
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) (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("响应数据签名为空")
}
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[:], []byte(*resp.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 %vMsg %v, SignType %v",
r.BizData == nil, r.Msg == nil, r.SignType == nil,
))
}
return fmt.Sprintf(
"bizData=%s&code=%s&msg=%s&signType=%s&timestamp=%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"
)