商福通sdk客户端与必要支付接口实现

This commit is contained in:
2025-06-04 19:02:21 +08:00
parent a9de63c3f9
commit 392e404d68
6 changed files with 380 additions and 16 deletions

300
web/globals/shangfutong.go Normal file
View File

@@ -0,0 +1,300 @@
package globals
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"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
appSecret string
privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey
}
func init() {
SFTPay = SftClient{
appid: env.SftPayAppId,
appSecret: env.SftPayAppSecret,
}
// 加载私钥
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
}
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 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 string `json:"payType"`
IfCode string `json:"ifCode"`
ExtParam *string `json:"extParam"`
PayInfo *string `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"`
}
func (s *SftClient) PaymentScanPay(req *PaymentScanPayResp) (*PaymentScanPayResp, error) {
const url = "https://pay.rscygroup.com/api/open/payment/scanpay"
return call[PaymentScanPayResp](s, url, req)
}
type PaymentH5PayReq struct {
Subject string `json:"subject"`
Body string `json:"body"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
PayType string `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 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 string `json:"payType"`
IfCode string `json:"ifCode"`
ExtParam *string `json:"extParam"`
PayInfo *string `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"`
}
func (s *SftClient) CreateOrderByRedirect(req *PaymentH5PayReq) (*PaymentH5PayResp, error) {
const url = "https://pay.rscygroup.com/api/open/payment/h5pay"
return call[PaymentH5PayResp](s, url, req)
}
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),
}
encrypted, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, crypto.SHA256, []byte(body.String(s.appSecret)))
if err != nil {
return nil, fmt.Errorf("签名失败:%w", err)
}
body.Sign = string(encrypted)
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 {
err := rsa.VerifyPKCS1v15(s.publicKey, crypto.SHA256, str, []byte(s.appSecret))
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(secret string) string {
return fmt.Sprintf(
"appId=%s&bizData=%s&reqId=%s&reqTime=%s&signType=%s&version=%s&appSecret=%s",
r.AppId, r.BizData, r.ReqId, r.ReqTime, r.SignType, r.Version, secret,
)
}
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"`
}

View File

@@ -56,12 +56,12 @@ func AlipayCallback(c *fiber.Ctx) (err error) {
}
switch alipay.TradeStatus(notification.NotifyType) {
// 等待支付
case alipay.TradeStatusWaitBuyerPay:
// 不需要处理
// 支付关闭
case alipay.TradeStatusClosed:
// todo 退款
// 非退款
switch trade2.Type(trade.Type) {
// 购买产品
@@ -73,6 +73,10 @@ func AlipayCallback(c *fiber.Ctx) (err error) {
// 余额充值
case trade2.TypeRecharge:
err = s.User.RechargeCancel(notification.OutTradeNo, time.Now())
if err != nil {
return err
}
}
// 支付成功
@@ -92,14 +96,11 @@ func AlipayCallback(c *fiber.Ctx) (err error) {
Payment: payment,
Time: paidAt,
}
switch trade2.Type(trade.Type) {
// 余额充值
case trade2.TypeRecharge:
err := s.User.RechargeConfirm(notification.OutTradeNo, verified)
if err != nil {
return err
}
// todo 退款
// 非退款
switch trade2.Type(trade.Type) {
// 购买产品
case trade2.TypePurchase:
@@ -107,11 +108,14 @@ func AlipayCallback(c *fiber.Ctx) (err error) {
if err != nil {
return err
}
}
// 交易结束
case alipay.TradeStatusFinished:
// 结束交易状态
// 余额充值
case trade2.TypeRecharge:
err := s.User.RechargeConfirm(notification.OutTradeNo, verified)
if err != nil {
return err
}
}
}
return c.SendString("success")

View File

@@ -2,6 +2,7 @@ package services
import (
q "platform/web/queries"
"time"
)
var User = &userService{}
@@ -43,3 +44,11 @@ func (s *userService) RechargeConfirm(tradeNo string, verified *TradeSuccessResu
return nil
}
func (s *userService) RechargeCancel(tradeNo string, now time.Time) error {
panic("not implemented")
}
func (s *userService) RechargeRefund(tradeNo string, now time.Time) error {
panic("not implemented")
}

View File

@@ -75,7 +75,7 @@ func (s *Server) Run() error {
// listen
slog.Info("Server started on :8080")
err := s.fiber.Listen(":8080")
err := s.fiber.Listen("0.0.0.0:8080")
if err != nil {
slog.Error("Failed to start server", slog.Any("err", err))
}