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" )