增加支付功能
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/smartwalle/alipay/v3"
|
||||
)
|
||||
|
||||
// AlipayChannel 支付宝(电脑网站支付 / 手机网站支付,RSA2)
|
||||
//
|
||||
// 渠道参数(config_json):
|
||||
//
|
||||
// appid 开放平台应用 APPID
|
||||
// app_private_key 应用私钥(RSA2,PKCS8)
|
||||
// alipay_public_key 支付宝公钥(回显公钥,用于验签)
|
||||
// is_production "1" 正式环境 / 其他为沙箱
|
||||
type AlipayChannel struct{}
|
||||
|
||||
func (c *AlipayChannel) Code() string { return ChannelAlipay }
|
||||
func (c *AlipayChannel) Name() string { return "支付宝" }
|
||||
|
||||
func (c *AlipayChannel) buildClient(cfg *ChannelConfig) (*alipay.Client, error) {
|
||||
appID := cfg.Get("appid")
|
||||
priv := cfg.Get("app_private_key")
|
||||
pub := cfg.Get("alipay_public_key")
|
||||
if appID == "" || priv == "" || pub == "" {
|
||||
return nil, fmt.Errorf("支付宝参数不完整:需要 appid / app_private_key / alipay_public_key")
|
||||
}
|
||||
client, err := alipay.New(appID, priv, cfg.Get("is_production") == "1")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建支付宝客户端失败: %w", err)
|
||||
}
|
||||
if err := client.LoadAliPayPublicKey(pub); err != nil {
|
||||
return nil, fmt.Errorf("加载支付宝公钥失败: %w", err)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *AlipayChannel) Prepay(ctx context.Context, order *models.PlatformPaymentOrder, cfg *ChannelConfig, opt PrepayOption) (*PayParams, error) {
|
||||
client, err := c.buildClient(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trade := alipay.Trade{
|
||||
NotifyURL: cfg.CallbackURL,
|
||||
ReturnURL: order.ReturnURL,
|
||||
Subject: order.Subject,
|
||||
OutTradeNo: order.PayNo,
|
||||
TotalAmount: FenToYuan(order.Amount),
|
||||
}
|
||||
if order.ExpireAt != nil {
|
||||
mins := int(time.Until(*order.ExpireAt).Minutes())
|
||||
if mins < 1 {
|
||||
mins = 1
|
||||
}
|
||||
trade.TimeoutExpress = fmt.Sprintf("%dm", mins)
|
||||
}
|
||||
|
||||
if strings.EqualFold(opt.PayType, PayTypeH5) {
|
||||
// 手机网站支付
|
||||
trade.ProductCode = "QUICK_WAP_WAY"
|
||||
u, err := client.TradeWapPay(alipay.TradeWapPay{Trade: trade})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("支付宝手机网站下单失败: %w", err)
|
||||
}
|
||||
return &PayParams{Channel: ChannelAlipay, PayType: PayTypeH5, RedirectURL: u.String()}, nil
|
||||
}
|
||||
|
||||
// 默认:电脑网站支付(PC 收银台)
|
||||
trade.ProductCode = "FAST_INSTANT_TRADE_PAY"
|
||||
u, err := client.TradePagePay(alipay.TradePagePay{Trade: trade})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("支付宝电脑网站下单失败: %w", err)
|
||||
}
|
||||
return &PayParams{Channel: ChannelAlipay, PayType: PayTypeWeb, RedirectURL: u.String()}, nil
|
||||
}
|
||||
|
||||
// normalizeAlipayStatus 支付宝交易状态 -> 归一化状态
|
||||
func normalizeAlipayStatus(status alipay.TradeStatus) string {
|
||||
switch status {
|
||||
case alipay.TradeStatusSuccess, alipay.TradeStatusFinished:
|
||||
return StateSuccess
|
||||
case alipay.TradeStatusClosed:
|
||||
return StateClosed
|
||||
case alipay.TradeStatusWaitBuyerPay:
|
||||
return StatePending
|
||||
default:
|
||||
return StatePending
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AlipayChannel) Query(ctx context.Context, order *models.PlatformPaymentOrder, cfg *ChannelConfig) (*ChannelState, error) {
|
||||
client, err := c.buildClient(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rsp, err := client.TradeQuery(ctx, alipay.TradeQuery{OutTradeNo: order.PayNo})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("支付宝查询订单失败: %w", err)
|
||||
}
|
||||
// 交易不存在(如超时未付被渠道关闭):不视为失败,返回待支付以便走关闭逻辑
|
||||
if rsp.Code != "10000" {
|
||||
if rsp.SubCode == "ACQ.TRADE_NOT_EXIST" {
|
||||
return &ChannelState{TradeState: StatePending, Raw: rsp.SubMsg}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("支付宝查询失败: %s %s %s", rsp.Code, rsp.SubCode, rsp.SubMsg)
|
||||
}
|
||||
|
||||
state := &ChannelState{
|
||||
ChannelTradeNo: rsp.TradeNo,
|
||||
TradeState: normalizeAlipayStatus(rsp.TradeStatus),
|
||||
Raw: fmt.Sprintf("trade_no=%s status=%s total=%s", rsp.TradeNo, rsp.TradeStatus, rsp.TotalAmount),
|
||||
}
|
||||
if fen, err := YuanToFen(rsp.TotalAmount); err == nil {
|
||||
state.Amount = fen
|
||||
}
|
||||
if state.TradeState == StateSuccess {
|
||||
now := time.Now()
|
||||
state.PaidAt = &now
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func (c *AlipayChannel) ParseNotify(ctx context.Context, r *http.Request, cfg *ChannelConfig) (*NotifyResult, error) {
|
||||
client, err := c.buildClient(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return nil, fmt.Errorf("解析支付宝通知参数失败: %w", err)
|
||||
}
|
||||
notification, err := client.DecodeNotification(ctx, r.Form)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("支付宝通知验签失败: %w", err)
|
||||
}
|
||||
|
||||
result := &NotifyResult{
|
||||
EventType: notification.NotifyType,
|
||||
EventID: notification.NotifyId,
|
||||
OutTradeNo: notification.OutTradeNo,
|
||||
PayNo: notification.OutTradeNo,
|
||||
ChannelTradeNo: notification.TradeNo,
|
||||
AckBody: "success",
|
||||
}
|
||||
if fen, err := YuanToFen(notification.TotalAmount); err == nil {
|
||||
result.Amount = fen
|
||||
}
|
||||
result.TradeState = normalizeAlipayStatus(notification.TradeStatus)
|
||||
result.Paid = result.TradeState == StateSuccess
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *AlipayChannel) Refund(ctx context.Context, order *models.PlatformPaymentOrder, refundNo string, amount int64, reason string, cfg *ChannelConfig) (string, error) {
|
||||
client, err := c.buildClient(cfg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rsp, err := client.TradeRefund(ctx, alipay.TradeRefund{
|
||||
OutTradeNo: order.PayNo,
|
||||
RefundAmount: FenToYuan(amount),
|
||||
RefundReason: reason,
|
||||
OutRequestNo: refundNo,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("支付宝退回请求失败: %w", err)
|
||||
}
|
||||
if rsp.Code != "10000" {
|
||||
return "", fmt.Errorf("支付宝退回失败: %s %s %s", rsp.Code, rsp.SubCode, rsp.SubMsg)
|
||||
}
|
||||
return rsp.TradeNo, nil
|
||||
}
|
||||
|
||||
func (c *AlipayChannel) TestConnect(ctx context.Context, cfg *ChannelConfig) (string, error) {
|
||||
client, err := c.buildClient(cfg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 查询一笔必然不存在的交易:能返回「交易不存在」即说明 APPID 与签名配置正确
|
||||
rsp, err := client.TradeQuery(ctx, alipay.TradeQuery{OutTradeNo: "__connect_test__"})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("支付宝凭证校验失败: %w", err)
|
||||
}
|
||||
if rsp.Code == "10000" || rsp.SubCode == "ACQ.TRADE_NOT_EXIST" {
|
||||
return "连接成功:APPID 与 RSA2 签名校验通过", nil
|
||||
}
|
||||
return "", fmt.Errorf("支付宝凭证校验未通过: %s %s %s", rsp.Code, rsp.SubCode, rsp.SubMsg)
|
||||
}
|
||||
Reference in New Issue
Block a user