package channel import ( "context" "fmt" "net/http" "github.com/smartwalle/alipay/v3" "github.com/wangjia/pay/internal/model" ) type alipayChannel struct { client *alipay.Client } func newAlipay(m *model.Merchant) (Channel, error) { if m.AppID == "" || m.AppPrivateKey == "" || m.AlipayPublicKey == "" { return nil, fmt.Errorf("商户 %s 的支付宝凭证不完整(app_id/app_private_key/alipay_public_key)", m.Code) } client, err := alipay.New(m.AppID, m.AppPrivateKey, m.Production) if err != nil { return nil, fmt.Errorf("初始化支付宝客户端失败: %w", err) } // 公钥模式(沙箱常用)。若改用证书模式,这里换成 LoadAppCertPublicKey 等。 if err := client.LoadAliPayPublicKey(m.AlipayPublicKey); err != nil { return nil, fmt.Errorf("加载支付宝公钥失败: %w", err) } return &alipayChannel{client: client}, nil } func (a *alipayChannel) Name() string { return "alipay" } func (a *alipayChannel) PagePay(_ context.Context, req CreateReq) (string, error) { // 手机浏览器:手机网站支付 wap.pay,H5 收银台可直接拉起支付宝 App 付款。 if req.IsMobile { var p = alipay.TradeWapPay{} p.OutTradeNo = req.OutTradeNo p.Subject = req.Subject p.TotalAmount = req.Amount p.ProductCode = "QUICK_WAP_WAY" p.NotifyURL = req.NotifyURL p.ReturnURL = req.ReturnURL u, err := a.client.TradeWapPay(p) if err != nil { return "", fmt.Errorf("支付宝手机网站下单失败: %w", err) } return u.String(), nil } // PC 浏览器:电脑网站支付 page.pay。 var p = alipay.TradePagePay{} p.OutTradeNo = req.OutTradeNo p.Subject = req.Subject p.TotalAmount = req.Amount p.ProductCode = "FAST_INSTANT_TRADE_PAY" p.NotifyURL = req.NotifyURL p.ReturnURL = req.ReturnURL // qr_pay_mode=2 跳转模式:跳到支付宝完整扫码收银台(含收款方/金额/订单详情), // 而非默认「登录付款」页;区别于 4(光秃秃的嵌入式二维码)。 p.QRPayMode = "2" u, err := a.client.TradePagePay(p) if err != nil { return "", fmt.Errorf("支付宝下单失败: %w", err) } return u.String(), nil } func (a *alipayChannel) PreCreate(ctx context.Context, req CreateReq) (string, error) { var p = alipay.TradePreCreate{} p.OutTradeNo = req.OutTradeNo p.Subject = req.Subject p.TotalAmount = req.Amount p.NotifyURL = req.NotifyURL rsp, err := a.client.TradePreCreate(ctx, p) if err != nil { return "", fmt.Errorf("支付宝预下单调用失败: %w", err) } if rsp.IsFailure() { return "", fmt.Errorf("支付宝预下单失败: %s / %s", rsp.Msg, rsp.SubMsg) } return rsp.QRCode, nil } func (a *alipayChannel) VerifyNotify(ctx context.Context, r *http.Request) (*NotifyResult, error) { if err := r.ParseForm(); err != nil { return nil, fmt.Errorf("解析回调表单失败: %w", err) } // DecodeNotification 内部用已加载的支付宝公钥验签,验签不过会返回错误。 noti, err := a.client.DecodeNotification(ctx, r.Form) if err != nil { return nil, fmt.Errorf("回调验签失败: %w", err) } paid := noti.TradeStatus == alipay.TradeStatusSuccess || noti.TradeStatus == alipay.TradeStatusFinished return &NotifyResult{ OutTradeNo: noti.OutTradeNo, TradeNo: noti.TradeNo, Amount: noti.TotalAmount, BuyerLogonID: noti.BuyerLogonId, Paid: paid, Raw: r.Form.Encode(), }, nil } func (a *alipayChannel) Query(ctx context.Context, outTradeNo string) (*QueryResult, error) { var p = alipay.TradeQuery{OutTradeNo: outTradeNo} rsp, err := a.client.TradeQuery(ctx, p) if err != nil { return nil, fmt.Errorf("支付宝查单调用失败: %w", err) } if rsp.IsFailure() { // 交易不存在(TRADE_NOT_EXIST)等:视为未找到,不报错,交由上层决定。 return &QueryResult{Found: false}, nil } paid := rsp.TradeStatus == alipay.TradeStatusSuccess || rsp.TradeStatus == alipay.TradeStatusFinished return &QueryResult{ Found: true, OutTradeNo: rsp.OutTradeNo, TradeNo: rsp.TradeNo, Amount: rsp.TotalAmount, Paid: paid, }, nil }