Files
platform/web/globals/shangfutong.go
2025-11-21 14:12:41 +08:00

446 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 %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"
)