chore: 并入 iotlcm-admin-svr 历史到 admin-svr/(原提交 49818c6)

This commit is contained in:
MiFaZhan
2026-11-02 13:27:59 +08:00
1250 changed files with 154111 additions and 0 deletions
@@ -0,0 +1,437 @@
package pay
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rabbitmq"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/websocket"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
"github.com/smartwalle/alipay/v3"
)
var (
alipayClient *alipay.Client
alipayClientOnce sync.Once
alipayClientErr error
)
type sAlipay struct{}
func init() {
service.RegisterAlipay(NewAlipay())
}
func NewAlipay() *sAlipay {
return &sAlipay{}
}
func (s *sAlipay) Pay(ctx context.Context, req *model.AlipayPayInput) (res *model.AlipayPayOutput, err error) {
g.Log().Infof(ctx, "执行了支付宝Pay方法")
outTradeNo := strings.TrimSpace(req.OutTradeNo)
if outTradeNo == "" {
outTradeNo = fmt.Sprintf("%s_%d", "ALI", time.Now().UnixNano())
}
subject := strings.TrimSpace(req.Subject)
totalAmount := strings.TrimSpace(req.TotalAmount)
payType := strings.TrimSpace(req.PayType)
if payType == "" {
payType = "page"
}
g.Log().Infof(ctx, "执行了支付宝Pay方法, outTradeNo:%s, subject:%s, totalAmount:%s, payType:%s", outTradeNo, subject, totalAmount, payType)
cfg, err := s.getConfig(ctx)
if err != nil {
return nil, err
}
client, err := s.getClient(ctx)
if err != nil {
return nil, err
}
isBalanceRecharge := req.BusinessType == "balanceRecharge"
var balanceRechargeTimeoutDelay time.Duration
if isBalanceRecharge {
balanceRechargeTimeoutDelay, err = getBalanceRechargeTimeoutDelay(ctx)
if err != nil {
return nil, err
}
if err = service.Trade().CreateRechargePending(ctx, outTradeNo, totalAmount); err != nil {
return nil, err
}
}
expireTime := ""
err = g.Cfg().MustGet(ctx, "pay.expireTime").Scan(&expireTime)
if err != nil {
return nil, gerror.Wrap(err, "获取支付过期时间失败")
}
expireMinutes := gconv.Int(expireTime)
if isBalanceRecharge {
expireTime = time.Now().Add(balanceRechargeTimeoutDelay).Format("2006-01-02 15:04:05")
} else {
expireTime = time.Now().Add(time.Duration(expireMinutes) * time.Minute).Format("2006-01-02 15:04:05")
}
g.Log().Infof(ctx, "执行了支付宝Pay方法 mexpireTime:%s", expireTime)
var payURL *url.URL
switch payType {
case "page":
p := alipay.TradePagePay{
Trade: alipay.Trade{
Subject: subject,
OutTradeNo: outTradeNo,
TimeExpire: expireTime,
TotalAmount: totalAmount,
ProductCode: "FAST_INSTANT_TRADE_PAY",
},
}
p.ReturnURL = cfg.ReturnUrl
p.NotifyURL = cfg.NotifyUrl
payURL, err = client.TradePagePay(p)
case "wap":
p := alipay.TradeWapPay{
Trade: alipay.Trade{
Subject: subject,
OutTradeNo: outTradeNo,
TimeExpire: expireTime,
TotalAmount: totalAmount,
ProductCode: "QUICK_WAP_WAY",
},
}
p.ReturnURL = cfg.ReturnUrl
p.NotifyURL = cfg.NotifyUrl
payURL, err = client.TradeWapPay(p)
default:
return nil, gerror.New("支付类型只支持 page 或 wap")
}
if err != nil {
if isBalanceRecharge {
_ = service.Trade().MarkRechargeFailed(ctx, outTradeNo)
}
return nil, gerror.Wrap(err, "创建支付宝支付链接失败")
}
if isBalanceRecharge {
if err = rabbitmq.PublishDelayed(ctx, consts.QueueBalanceRechargeDelay, consts.QueueBalanceRechargeTimeout, balanceRechargeTimeoutDelay, model.BalanceRechargeTimeoutMessage{OutTradeNo: outTradeNo}); err != nil {
_ = service.Trade().MarkRechargeFailed(ctx, outTradeNo)
return nil, gerror.Wrap(err, "创建充值超时检查任务失败")
}
}
g.Log().Infof(ctx, "alipay pay url created, production:%v, payType:%s, outTradeNo:%s, subject:%s, totalAmount:%s, notifyUrl:%s, returnUrl:%s", cfg.IsProduction, payType, outTradeNo, subject, totalAmount, cfg.NotifyUrl, cfg.ReturnUrl)
return &model.AlipayPayOutput{
OutTradeNo: outTradeNo,
PayUrl: payURL.String(),
}, nil
}
func getBalanceRechargeTimeoutDelay(ctx context.Context) (time.Duration, error) {
value := strings.TrimSpace(g.Cfg().MustGet(ctx, "pay.balanceRecharge.timeoutDelay", "3m").String())
delay, err := time.ParseDuration(value)
if err != nil || delay <= 0 {
return 0, gerror.Newf("余额充值超时时间配置无效: %q", value)
}
return delay, nil
}
func (s *sAlipay) Notify(ctx context.Context, req *model.AlipayNotifyInput) (res *model.AlipayNotifyOutput, err error) {
g.Log().Infof(ctx, "执行了支付宝Notify方法")
r := g.RequestFromCtx(ctx)
client, err := s.getClient(ctx)
if err != nil {
r.Response.Write("failure")
return &model.AlipayNotifyOutput{}, nil
}
notification, err := client.GetTradeNotification(r.Request)
if err != nil {
g.Log().Errorf(ctx, "alipay notify verify failed: %+v", err)
r.Response.Write("failure")
return &model.AlipayNotifyOutput{}, nil
}
body, _ := json.Marshal(notification)
g.Log().Infof(ctx, "alipay notify success: %s", string(body))
// 支付宝回调成功,通知前端查询交易并调用充值接口。
if notification.OutTradeNo != "" && notification.TradeStatus == alipay.TradeStatusSuccess {
// WebSocket通知
wsTag := "ws:tag:pay:alipay:" + notification.OutTradeNo
g.Log().Infof(ctx, "alipay notify websocket broadcast tag:%s count:%d", wsTag, websocket.GetTagCount(wsTag))
websocket.SendToTag(wsTag, &websocket.WResponse{
Event: "ws:event:pay:alipay:notify",
Data: g.Map{
"outTradeNo": notification.OutTradeNo,
"tradeNo": notification.TradeNo,
"tradeStatus": string(notification.TradeStatus),
"totalAmount": notification.TotalAmount,
},
})
}
r.Response.Write("success")
return &model.AlipayNotifyOutput{}, nil
}
func (s *sAlipay) Return(ctx context.Context, req *model.AlipayReturnInput) (res *model.AlipayReturnOutput, err error) {
g.Log().Infof(ctx, "执行了支付宝Return方法")
r := g.RequestFromCtx(ctx)
client, err := s.getClient(ctx)
if err != nil {
return nil, err
}
if err = client.VerifySign(ctx, r.Request.URL.Query()); err != nil {
return nil, gerror.Wrap(err, "支付宝同步回跳验签失败")
}
return &model.AlipayReturnOutput{
OutTradeNo: r.Get("out_trade_no").String(),
TradeNo: r.Get("trade_no").String(),
TotalAmount: r.Get("total_amount").String(),
Message: "支付完成",
}, nil
}
func (s *sAlipay) Query(ctx context.Context, req *model.AlipayTradeQueryInput) (res *model.AlipayTradeQueryOutput, err error) {
g.Log().Infof(ctx, "执行了支付宝Query方法")
if req == nil {
return nil, gerror.New("查询参数不能为空")
}
outTradeNo := strings.TrimSpace(req.OutTradeNo)
tradeNo := strings.TrimSpace(req.TradeNo)
if outTradeNo == "" && tradeNo == "" {
return nil, gerror.New("商户订单号和支付宝交易号不能同时为空")
}
cfg, err := s.getConfig(ctx)
if err != nil {
return nil, err
}
client, err := s.getClient(ctx)
if err != nil {
return nil, err
}
query := alipay.TradeQuery{
OutTradeNo: outTradeNo,
TradeNo: tradeNo,
}
g.Log().Infof(ctx, "alipay trade query start, production:%v, outTradeNo:%s, tradeNo:%s", cfg.IsProduction, outTradeNo, tradeNo)
rsp, err := s.tradeQueryWithRetry(ctx, client, query)
if err != nil {
if isAlipayTradeNotFoundError(err) {
return pendingAlipayTradeQuery(outTradeNo, tradeNo), nil
}
g.Log().Errorf(ctx, "alipay trade query failed, outTradeNo:%s, tradeNo:%s, err:%+v", outTradeNo, tradeNo, err)
return nil, gerror.Wrap(err, "查询支付宝交易失败")
}
if rsp == nil {
return nil, gerror.New("支付宝交易查询未返回结果")
}
if rsp.IsFailure() {
if isAlipayTradeNotFound(string(rsp.Code), rsp.SubCode, rsp.SubMsg) {
g.Log().Infof(ctx, "alipay trade query pending, outTradeNo:%s, tradeNo:%s", outTradeNo, tradeNo)
return pendingAlipayTradeQuery(outTradeNo, tradeNo), nil
}
return nil, gerror.Newf("支付宝交易查询失败: code=%s, subCode=%s, subMsg=%s", rsp.Code, rsp.SubCode, rsp.SubMsg)
}
if rsp.TradeStatus == alipay.TradeStatusSuccess || rsp.TradeStatus == alipay.TradeStatusFinished {
rechargeModel := &model.TBalanceDetailRechargeModel{
OutTradeNo: rsp.OutTradeNo,
TradeNo: rsp.TradeNo,
TradeStatus: string(rsp.TradeStatus),
TotalAmount: rsp.TotalAmount,
BuyerLogonId: rsp.BuyerLogonId,
BuyerUserId: rsp.BuyerUserId,
BuyerOpenId: rsp.BuyerOpenId,
SendPayDate: rsp.SendPayDate,
}
if err = service.Trade().Recharge(ctx, rechargeModel); err != nil {
return nil, gerror.Wrap(err, "支付宝充值入账失败")
}
} else {
g.Log().Infof(ctx, "alipay trade query not paid, outTradeNo:%s, tradeNo:%s, tradeStatus:%s", rsp.OutTradeNo, rsp.TradeNo, rsp.TradeStatus)
}
return &model.AlipayTradeQueryOutput{
OutTradeNo: rsp.OutTradeNo,
TradeNo: rsp.TradeNo,
TradeStatus: string(rsp.TradeStatus),
TotalAmount: rsp.TotalAmount,
BuyerLogonId: rsp.BuyerLogonId,
BuyerUserId: rsp.BuyerUserId,
BuyerOpenId: rsp.BuyerOpenId,
SendPayDate: rsp.SendPayDate,
}, nil
}
func pendingAlipayTradeQuery(outTradeNo, tradeNo string) *model.AlipayTradeQueryOutput {
return &model.AlipayTradeQueryOutput{
OutTradeNo: outTradeNo,
TradeNo: tradeNo,
TradeStatus: string(alipay.TradeStatusWaitBuyerPay),
}
}
func (s *sAlipay) tradeQueryWithRetry(ctx context.Context, client *alipay.Client, query alipay.TradeQuery) (rsp *alipay.TradeQueryRsp, err error) {
const maxRetry = 3
for i := 1; i <= maxRetry; i++ {
rsp, err = client.TradeQuery(ctx, query)
if err == nil {
if i > 1 {
g.Log().Infof(ctx, "alipay trade query retry success, attempt:%d, outTradeNo:%s, tradeNo:%s", i, query.OutTradeNo, query.TradeNo)
}
return rsp, nil
}
if !isRetryableAlipayError(err) || i == maxRetry {
return nil, err
}
g.Log().Warningf(ctx, "alipay trade query retryable error, attempt:%d, outTradeNo:%s, tradeNo:%s, err:%+v", i, query.OutTradeNo, query.TradeNo, err)
if err = sleepWithContext(ctx, time.Duration(i)*300*time.Millisecond); err != nil {
return nil, err
}
}
return nil, err
}
func isRetryableAlipayError(err error) bool {
if err == nil {
return false
}
if errors.Is(err, context.DeadlineExceeded) {
return true
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "invalid character '<'") ||
strings.Contains(msg, "unexpected end of json") ||
strings.Contains(msg, "bad response") ||
strings.Contains(msg, "eof") ||
strings.Contains(msg, "context deadline exceeded") ||
strings.Contains(msg, "timeout") ||
strings.Contains(msg, "connection reset") ||
strings.Contains(msg, "connection refused") ||
strings.Contains(msg, "tls handshake timeout") ||
strings.Contains(msg, "temporary") ||
strings.Contains(msg, "too many requests") ||
strings.Contains(msg, "429") ||
strings.Contains(msg, "500") ||
strings.Contains(msg, "502") ||
strings.Contains(msg, "503") ||
strings.Contains(msg, "504")
}
func isAlipayTradeNotFoundError(err error) bool {
if err == nil {
return false
}
return isAlipayTradeNotFound(err.Error())
}
func isAlipayTradeNotFound(parts ...string) bool {
msg := strings.ToLower(strings.Join(parts, " "))
return strings.Contains(msg, "acq.trade_not_exist") ||
strings.Contains(msg, "trade_not_exist") ||
strings.Contains(msg, "交易不存在") ||
strings.Contains(msg, "订单不存在")
}
func sleepWithContext(ctx context.Context, d time.Duration) error {
timer := time.NewTimer(d)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}
func (s *sAlipay) Refund(ctx context.Context, req *model.AlipayRefundInput) (res *model.AlipayRefundOutput, err error) {
g.Log().Infof(ctx, "执行了支付宝Refund方法")
refundRequest, err := req.Normalize()
if err != nil {
return nil, err
}
client, err := s.getClient(ctx)
if err != nil {
return nil, err
}
refund := alipay.TradeRefund{
OutTradeNo: refundRequest.OutTradeNo,
TradeNo: refundRequest.TradeNo,
RefundAmount: refundRequest.RefundAmount,
OutRequestNo: refundRequest.OutRequestNo,
RefundReason: refundRequest.RefundReason,
}
g.Log().Infof(ctx, "alipay refund start, outTradeNo:%s, tradeNo:%s, outRequestNo:%s, refundAmount:%s", refundRequest.OutTradeNo, refundRequest.TradeNo, refundRequest.OutRequestNo, refundRequest.RefundAmount)
rsp, err := client.TradeRefund(ctx, refund)
if err != nil {
g.Log().Errorf(ctx, "alipay refund failed, outTradeNo:%s, tradeNo:%s, outRequestNo:%s, err:%+v", refundRequest.OutTradeNo, refundRequest.TradeNo, refundRequest.OutRequestNo, err)
return nil, gerror.Wrap(err, "支付宝退款失败")
}
if rsp == nil {
return nil, gerror.New("支付宝退款未返回结果")
}
if rsp.IsFailure() {
return nil, gerror.Newf("支付宝退款失败: code=%s, subCode=%s, subMsg=%s", rsp.Code, rsp.SubCode, rsp.SubMsg)
}
return &model.AlipayRefundOutput{
TradeNo: rsp.TradeNo,
OutTradeNo: rsp.OutTradeNo,
BuyerLogonId: rsp.BuyerLogonId,
RefundFee: rsp.RefundFee,
}, nil
}
func (s *sAlipay) getClient(ctx context.Context) (*alipay.Client, error) {
alipayClientOnce.Do(func() {
cfg, err := s.getConfig(ctx)
if err != nil {
alipayClientErr = err
return
}
client, err := alipay.New(
cfg.AppId,
cfg.PrivateKey,
cfg.IsProduction,
alipay.WithHTTPClient(&http.Client{Timeout: 10 * time.Second}),
)
if err != nil {
alipayClientErr = gerror.Wrap(err, "初始化支付宝客户端失败")
return
}
if err = client.LoadAliPayPublicKey(cfg.AlipayPublicKey); err != nil {
alipayClientErr = gerror.Wrap(err, "加载支付宝公钥失败")
return
}
alipayClient = client
})
if alipayClientErr != nil {
return nil, alipayClientErr
}
if alipayClient == nil {
return nil, gerror.New("支付宝客户端未初始化")
}
return alipayClient, nil
}
func (s *sAlipay) getConfig(ctx context.Context) (cfg model.AlipayConfig, err error) {
if err = g.Cfg().MustGet(ctx, "alipay").Scan(&cfg); err != nil {
return cfg, gerror.Wrap(err, "读取支付宝配置失败")
}
if strings.TrimSpace(cfg.AppId) == "" {
return cfg, gerror.New("支付宝 appId 未配置")
}
if strings.TrimSpace(cfg.PrivateKey) == "" {
return cfg, gerror.New("支付宝应用私钥未配置")
}
if strings.TrimSpace(cfg.AlipayPublicKey) == "" {
return cfg, gerror.New("支付宝公钥未配置")
}
return cfg, nil
}