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) }