fd5f56f7de
- 微信支付 V3 Native 渠道 (wechat.go):Native下单/回调AES-GCM解密验签/查单 - 支付宝:手机网站支付 wap.pay + 按 UA 自适应(PC page.pay扫码 / 手机拉App);qr_pay_mode=2 完整扫码收银台 - 业务对接:下单接口扩展(biz_system/biz_ref/return_url)+ HMAC 签名鉴权;支付成功 webhook 主动推送业务方 + 60s 重试 + BizNotifyLog - 通用多业务:config.biz 改 map,加业务只改配置(BIZ_<SYS>_SECRET/_CALLBACK_URL) - seedPlans:四档真实套餐 + promo_first_month(¥1) + test_liandiao(0.01),均挂 biz_code;/products 暴露 biz_code - 删除沙箱 pay.html;对接设计文档入 docs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UQmqWmV67sXGLrb3U1XXn
124 lines
4.0 KiB
Go
124 lines
4.0 KiB
Go
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
|
||
}
|