Files
pay/internal/channel/alipay.go
T
wangjia 6da6964451 初始提交:岩美 pay 收款服务(支付宝当面付 + 多商户多渠道架构)
- Go/Gin/GORM + 纯 Go SQLite(无 cgo)
- Channel 多渠道接口:支付宝当面付(precreate)/电脑网站支付(page.pay) 已实现,微信占位
- 多商户 merchants 表,回调验签+金额核对+幂等+查单兜底
- 收款页/结果页/二维码端点;docs/ 设计文档与部署 Runbook
- 密钥走环境变量/Bitwarden,不入库

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 07:21:48 +08:00

104 lines
3.3 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) {
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
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
}