商福通sdk客户端与必要支付接口实现
This commit is contained in:
300
web/globals/shangfutong.go
Normal file
300
web/globals/shangfutong.go
Normal 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"`
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user