商福通sdk客户端与必要支付接口实现
This commit is contained in:
43
pkg/env/env.go
vendored
43
pkg/env/env.go
vendored
@@ -347,6 +347,49 @@ func loadAliyun() {
|
|||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
// region 商福通
|
||||||
|
|
||||||
|
var (
|
||||||
|
SftPayAppId string
|
||||||
|
SftPayAppSecret string
|
||||||
|
SftPayAppPrivateKey string
|
||||||
|
SftPayPublicKey string
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadSftPay() {
|
||||||
|
var value string
|
||||||
|
|
||||||
|
value = os.Getenv("SFTPAY_APP_ID")
|
||||||
|
if value == "" {
|
||||||
|
panic("环境变量 ALIYUN_SMS_TEMPLATE_LOGIN 的值不能为空")
|
||||||
|
} else {
|
||||||
|
SftPayAppId = value
|
||||||
|
}
|
||||||
|
|
||||||
|
value = os.Getenv("SFTPAY_APP_PRIVATE_KEY")
|
||||||
|
if value == "" {
|
||||||
|
panic("环境变量 SFTPAY_APP_PRIVATE_KEY 的值不能为空")
|
||||||
|
} else {
|
||||||
|
SftPayAppPrivateKey = value
|
||||||
|
}
|
||||||
|
|
||||||
|
value = os.Getenv("SFTPAY_PUBLIC_KEY")
|
||||||
|
if value == "" {
|
||||||
|
panic("环境变量 SFTPAY_PUBLIC_KEY 的值不能为空")
|
||||||
|
} else {
|
||||||
|
SftPayPublicKey = value
|
||||||
|
}
|
||||||
|
|
||||||
|
value = os.Getenv("SFTPAY_APP_SECRET")
|
||||||
|
if value == "" {
|
||||||
|
panic("环境变量 SFTPAY_APP_SECRET 的值不能为空")
|
||||||
|
} else {
|
||||||
|
SftPayAppSecret = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
// region debug
|
// region debug
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -19,3 +19,11 @@ func Z[T any](v *T) T {
|
|||||||
}
|
}
|
||||||
return *v
|
return *v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Or[T any](v *T, or T) T {
|
||||||
|
if v == nil {
|
||||||
|
return or
|
||||||
|
} else {
|
||||||
|
return *v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
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) {
|
switch alipay.TradeStatus(notification.NotifyType) {
|
||||||
|
|
||||||
// 等待支付
|
|
||||||
case alipay.TradeStatusWaitBuyerPay:
|
|
||||||
// 不需要处理
|
|
||||||
|
|
||||||
// 支付关闭
|
// 支付关闭
|
||||||
case alipay.TradeStatusClosed:
|
case alipay.TradeStatusClosed:
|
||||||
|
|
||||||
|
// todo 退款
|
||||||
|
|
||||||
|
// 非退款
|
||||||
switch trade2.Type(trade.Type) {
|
switch trade2.Type(trade.Type) {
|
||||||
|
|
||||||
// 购买产品
|
// 购买产品
|
||||||
@@ -73,6 +73,10 @@ func AlipayCallback(c *fiber.Ctx) (err error) {
|
|||||||
|
|
||||||
// 余额充值
|
// 余额充值
|
||||||
case trade2.TypeRecharge:
|
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,
|
Payment: payment,
|
||||||
Time: paidAt,
|
Time: paidAt,
|
||||||
}
|
}
|
||||||
switch trade2.Type(trade.Type) {
|
|
||||||
|
|
||||||
// 余额充值
|
// todo 退款
|
||||||
case trade2.TypeRecharge:
|
|
||||||
err := s.User.RechargeConfirm(notification.OutTradeNo, verified)
|
// 非退款
|
||||||
if err != nil {
|
switch trade2.Type(trade.Type) {
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 购买产品
|
// 购买产品
|
||||||
case trade2.TypePurchase:
|
case trade2.TypePurchase:
|
||||||
@@ -107,11 +108,14 @@ func AlipayCallback(c *fiber.Ctx) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 交易结束
|
// 余额充值
|
||||||
case alipay.TradeStatusFinished:
|
case trade2.TypeRecharge:
|
||||||
// 结束交易状态
|
err := s.User.RechargeConfirm(notification.OutTradeNo, verified)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.SendString("success")
|
return c.SendString("success")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
q "platform/web/queries"
|
q "platform/web/queries"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var User = &userService{}
|
var User = &userService{}
|
||||||
@@ -43,3 +44,11 @@ func (s *userService) RechargeConfirm(tradeNo string, verified *TradeSuccessResu
|
|||||||
|
|
||||||
return nil
|
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
|
// listen
|
||||||
slog.Info("Server started on :8080")
|
slog.Info("Server started on :8080")
|
||||||
err := s.fiber.Listen(":8080")
|
err := s.fiber.Listen("0.0.0.0:8080")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to start server", slog.Any("err", err))
|
slog.Error("Failed to start server", slog.Any("err", err))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user