From 392e404d68f1720efc881f632fc21a9e854b80b8 Mon Sep 17 00:00:00 2001 From: luorijun Date: Wed, 4 Jun 2025 19:02:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=95=86=E7=A6=8F=E9=80=9Asdk=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E4=B8=8E=E5=BF=85=E8=A6=81=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/env/env.go | 43 ++++++ pkg/u/u.go | 8 + web/globals/shangfutong.go | 300 +++++++++++++++++++++++++++++++++++++ web/handlers/trade.go | 34 +++-- web/services/user.go | 9 ++ web/web.go | 2 +- 6 files changed, 380 insertions(+), 16 deletions(-) create mode 100644 web/globals/shangfutong.go diff --git a/pkg/env/env.go b/pkg/env/env.go index 1dae0ab..217a453 100644 --- a/pkg/env/env.go +++ b/pkg/env/env.go @@ -347,6 +347,49 @@ func loadAliyun() { // 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 var ( diff --git a/pkg/u/u.go b/pkg/u/u.go index 30378a6..6c193de 100644 --- a/pkg/u/u.go +++ b/pkg/u/u.go @@ -19,3 +19,11 @@ func Z[T any](v *T) T { } return *v } + +func Or[T any](v *T, or T) T { + if v == nil { + return or + } else { + return *v + } +} diff --git a/web/globals/shangfutong.go b/web/globals/shangfutong.go new file mode 100644 index 0000000..ecd0f6a --- /dev/null +++ b/web/globals/shangfutong.go @@ -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"` +} diff --git a/web/handlers/trade.go b/web/handlers/trade.go index 7d0c3a4..7519aca 100644 --- a/web/handlers/trade.go +++ b/web/handlers/trade.go @@ -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") diff --git a/web/services/user.go b/web/services/user.go index c97a373..a8ac9b1 100644 --- a/web/services/user.go +++ b/web/services/user.go @@ -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") +} diff --git a/web/web.go b/web/web.go index 5d73271..ab80db9 100644 --- a/web/web.go +++ b/web/web.go @@ -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)) }