Files
pay/internal/channel/alipay.go
T
wangjia fd5f56f7de feat: 微信V3 + 支付宝手机站/UA自适应 + 业务对接(签名下单/webhook) + 通用多业务
- 微信支付 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
2026-07-03 22:44:34 +08:00

124 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}