package provider import ( "context" "fmt" "net/url" "strconv" "strings" "sync" "time" "github.com/Wei-Shaw/sub2api/internal/payment" "github.com/smartwalle/alipay/v3" ) // Alipay product codes. const ( alipayProductCodePagePay = "FAST_INSTANT_TRADE_PAY" alipayProductCodeWapPay = "QUICK_WAP_WAY" ) // Alipay response constants. const ( alipayFundChangeYes = "Y" alipayErrTradeNotExist = "ACQ.TRADE_NOT_EXIST" alipayRefundSuffix = "-refund" ) // Alipay implements payment.Provider and payment.CancelableProvider using the smartwalle/alipay SDK. type Alipay struct { instanceID string config map[string]string // appId, privateKey, publicKey (or alipayPublicKey), notifyUrl, returnUrl mu sync.Mutex client *alipay.Client } // NewAlipay creates a new Alipay provider instance. func NewAlipay(instanceID string, config map[string]string) (*Alipay, error) { required := []string{"appId", "privateKey"} for _, k := range required { if config[k] == "" { return nil, fmt.Errorf("alipay config missing required key: %s", k) } } return &Alipay{ instanceID: instanceID, config: config, }, nil } func (a *Alipay) getClient() (*alipay.Client, error) { a.mu.Lock() defer a.mu.Unlock() if a.client != nil { return a.client, nil } client, err := alipay.New(a.config["appId"], a.config["privateKey"], true) if err != nil { return nil, fmt.Errorf("alipay init client: %w", err) } pubKey := a.config["publicKey"] if pubKey == "" { pubKey = a.config["alipayPublicKey"] } if pubKey == "" { return nil, fmt.Errorf("alipay config missing required key: publicKey (or alipayPublicKey)") } if err := client.LoadAliPayPublicKey(pubKey); err != nil { return nil, fmt.Errorf("alipay load public key: %w", err) } a.client = client return a.client, nil } func (a *Alipay) Name() string { return "Alipay" } func (a *Alipay) ProviderKey() string { return payment.TypeAlipay } func (a *Alipay) SupportedTypes() []payment.PaymentType { return []payment.PaymentType{payment.TypeAlipayDirect} } // CreatePayment creates an Alipay payment page URL. func (a *Alipay) CreatePayment(_ context.Context, req payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) { client, err := a.getClient() if err != nil { return nil, err } notifyURL := a.config["notifyUrl"] if req.NotifyURL != "" { notifyURL = req.NotifyURL } returnURL := a.config["returnUrl"] if req.ReturnURL != "" { returnURL = req.ReturnURL } if req.IsMobile { return a.createTrade(client, req, notifyURL, returnURL, true) } return a.createTrade(client, req, notifyURL, returnURL, false) } func (a *Alipay) createTrade(client *alipay.Client, req payment.CreatePaymentRequest, notifyURL, returnURL string, isMobile bool) (*payment.CreatePaymentResponse, error) { if isMobile { param := alipay.TradeWapPay{} param.OutTradeNo = req.OrderID param.TotalAmount = req.Amount param.Subject = req.Subject param.ProductCode = alipayProductCodeWapPay param.NotifyURL = notifyURL param.ReturnURL = returnURL payURL, err := client.TradeWapPay(param) if err != nil { return nil, fmt.Errorf("alipay TradeWapPay: %w", err) } return &payment.CreatePaymentResponse{ TradeNo: req.OrderID, PayURL: payURL.String(), }, nil } param := alipay.TradePagePay{} param.OutTradeNo = req.OrderID param.TotalAmount = req.Amount param.Subject = req.Subject param.ProductCode = alipayProductCodePagePay param.NotifyURL = notifyURL param.ReturnURL = returnURL payURL, err := client.TradePagePay(param) if err != nil { return nil, fmt.Errorf("alipay TradePagePay: %w", err) } return &payment.CreatePaymentResponse{ TradeNo: req.OrderID, PayURL: payURL.String(), QRCode: payURL.String(), }, nil } // QueryOrder queries the trade status via Alipay. func (a *Alipay) QueryOrder(ctx context.Context, tradeNo string) (*payment.QueryOrderResponse, error) { client, err := a.getClient() if err != nil { return nil, err } result, err := client.TradeQuery(ctx, alipay.TradeQuery{OutTradeNo: tradeNo}) if err != nil { if isTradeNotExist(err) { return &payment.QueryOrderResponse{ TradeNo: tradeNo, Status: payment.ProviderStatusPending, }, nil } return nil, fmt.Errorf("alipay TradeQuery: %w", err) } status := payment.ProviderStatusPending switch result.TradeStatus { case alipay.TradeStatusSuccess, alipay.TradeStatusFinished: status = payment.ProviderStatusPaid case alipay.TradeStatusClosed: status = payment.ProviderStatusFailed } amount, err := strconv.ParseFloat(result.TotalAmount, 64) if err != nil { return nil, fmt.Errorf("alipay parse amount %q: %w", result.TotalAmount, err) } return &payment.QueryOrderResponse{ TradeNo: result.TradeNo, Status: status, Amount: amount, PaidAt: result.SendPayDate, }, nil } // VerifyNotification decodes and verifies an Alipay async notification. func (a *Alipay) VerifyNotification(ctx context.Context, rawBody string, _ map[string]string) (*payment.PaymentNotification, error) { client, err := a.getClient() if err != nil { return nil, err } values, err := url.ParseQuery(rawBody) if err != nil { return nil, fmt.Errorf("alipay parse notification: %w", err) } notification, err := client.DecodeNotification(ctx, values) if err != nil { return nil, fmt.Errorf("alipay verify notification: %w", err) } status := payment.ProviderStatusFailed if notification.TradeStatus == alipay.TradeStatusSuccess || notification.TradeStatus == alipay.TradeStatusFinished { status = payment.ProviderStatusSuccess } amount, err := strconv.ParseFloat(notification.TotalAmount, 64) if err != nil { return nil, fmt.Errorf("alipay parse notification amount %q: %w", notification.TotalAmount, err) } return &payment.PaymentNotification{ TradeNo: notification.TradeNo, OrderID: notification.OutTradeNo, Amount: amount, Status: status, RawData: rawBody, }, nil } // Refund requests a refund through Alipay. func (a *Alipay) Refund(ctx context.Context, req payment.RefundRequest) (*payment.RefundResponse, error) { client, err := a.getClient() if err != nil { return nil, err } result, err := client.TradeRefund(ctx, alipay.TradeRefund{ OutTradeNo: req.OrderID, RefundAmount: req.Amount, RefundReason: req.Reason, OutRequestNo: fmt.Sprintf("%s-refund-%d", req.OrderID, time.Now().UnixNano()), }) if err != nil { return nil, fmt.Errorf("alipay TradeRefund: %w", err) } refundStatus := payment.ProviderStatusPending if result.FundChange == alipayFundChangeYes { refundStatus = payment.ProviderStatusSuccess } refundID := result.TradeNo if refundID == "" { refundID = req.OrderID + alipayRefundSuffix } return &payment.RefundResponse{ RefundID: refundID, Status: refundStatus, }, nil } // CancelPayment closes a pending trade on Alipay. func (a *Alipay) CancelPayment(ctx context.Context, tradeNo string) error { client, err := a.getClient() if err != nil { return err } _, err = client.TradeClose(ctx, alipay.TradeClose{OutTradeNo: tradeNo}) if err != nil { if isTradeNotExist(err) { return nil } return fmt.Errorf("alipay TradeClose: %w", err) } return nil } func isTradeNotExist(err error) bool { if err == nil { return false } return strings.Contains(err.Error(), alipayErrTradeNotExist) } // Ensure interface compliance. var ( _ payment.Provider = (*Alipay)(nil) _ payment.CancelableProvider = (*Alipay)(nil) )