// Package payment 统一承载支付宝、微信和钱包支付请求及渠道回调。 package payment import ( "context" "encoding/json" "errors" "fmt" "os" "strings" "git.apinb.com/heqiapp/platforms/backend/api/internal/config" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "github.com/smartwalle/alipay/v3" wechatcore "github.com/wechatpay-apiv3/wechatpay-go/core" "github.com/wechatpay-apiv3/wechatpay-go/core/option" wechatapp "github.com/wechatpay-apiv3/wechatpay-go/services/payments/app" wechatjsapi "github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi" wechatnative "github.com/wechatpay-apiv3/wechatpay-go/services/payments/native" "github.com/wechatpay-apiv3/wechatpay-go/utils" ) var ErrChannelUnavailable = errors.New("payment channel is not configured") func money(amount int64) string { return fmt.Sprintf("%d.%02d", amount/100, amount%100) } func createChannelOrder(ctx context.Context, order models.PaymentOrder, openID string) (string, error) { switch order.Channel { case "alipay": return createAlipayOrder(order) case "wechat": return createWechatOrder(ctx, order, openID) default: return "", errors.New("unsupported payment channel") } } func alipayClient() (*alipay.Client, error) { cfg := config.Spec.Payment.Alipay if !cfg.Enabled || cfg.AppID == "" || cfg.PrivateKeyPath == "" { return nil, ErrChannelUnavailable } privateKey, err := os.ReadFile(cfg.PrivateKeyPath) if err != nil { return nil, err } client, err := alipay.New(cfg.AppID, string(privateKey), cfg.Production) if err != nil { return nil, err } if err = client.LoadAppCertPublicKeyFromFile(cfg.AppPublicCertPath); err != nil { return nil, err } if err = client.LoadAliPayRootCertFromFile(cfg.AlipayRootCertPath); err != nil { return nil, err } if err = client.LoadAlipayCertPublicKeyFromFile(cfg.AlipayPublicCertPath); err != nil { return nil, err } return client, nil } func createAlipayOrder(order models.PaymentOrder) (string, error) { client, err := alipayClient() if err != nil { return "", err } trade := alipay.Trade{NotifyURL: config.Spec.Payment.Alipay.NotifyURL, ReturnURL: config.Spec.Payment.Alipay.ReturnURL, Subject: order.Subject, OutTradeNo: order.PaymentNo, TotalAmount: money(order.Amount), TimeoutExpress: fmt.Sprintf("%dm", config.Spec.Payment.ExpireMinutes)} if order.PayType == "app" { trade.ProductCode = "QUICK_MSECURITY_PAY" return client.TradeAppPay(alipay.TradeAppPay{Trade: trade}) } if order.PayType == "wap" { trade.ProductCode = "QUICK_WAP_WAY" value, err := client.TradeWapPay(alipay.TradeWapPay{Trade: trade}) if err != nil { return "", err } return value.String(), nil } return "", errors.New("unsupported alipay product") } func wechatClient(ctx context.Context) (*wechatcore.Client, error) { cfg := config.Spec.Payment.Wechat if !cfg.Enabled || cfg.MerchantID == "" || cfg.MerchantPrivateKeyPath == "" || len(cfg.APIv3Key) != 32 { return nil, ErrChannelUnavailable } key, err := utils.LoadPrivateKeyWithPath(cfg.MerchantPrivateKeyPath) if err != nil { return nil, err } return wechatcore.NewClient(ctx, option.WithWechatPayAutoAuthCipher(cfg.MerchantID, cfg.MerchantCertificateSerial, key, cfg.APIv3Key)) } func createWechatOrder(ctx context.Context, order models.PaymentOrder, openID string) (string, error) { cfg := config.Spec.Payment.Wechat client, err := wechatClient(ctx) if err != nil { return "", err } marshal := func(value any) (string, error) { raw, err := json.Marshal(value); return string(raw), err } switch order.PayType { case "app": resp, _, err := (&wechatapp.AppApiService{Client: client}).PrepayWithRequestPayment(ctx, wechatapp.PrepayRequest{ Appid: wechatcore.String(cfg.AppAppID), Mchid: wechatcore.String(cfg.MerchantID), Description: wechatcore.String(order.Subject), OutTradeNo: wechatcore.String(order.PaymentNo), TimeExpire: &order.ExpiresAt, NotifyUrl: wechatcore.String(cfg.NotifyURL), Amount: &wechatapp.Amount{Total: wechatcore.Int64(order.Amount)}}) if err != nil { return "", err } return marshal(resp) case "jsapi", "mini": if strings.TrimSpace(openID) == "" { return "", errors.New("openid is required") } appid := cfg.OfficialAccountAppID if order.PayType == "mini" { appid = cfg.MiniProgramAppID } resp, _, err := (&wechatjsapi.JsapiApiService{Client: client}).PrepayWithRequestPayment(ctx, wechatjsapi.PrepayRequest{ Appid: wechatcore.String(appid), Mchid: wechatcore.String(cfg.MerchantID), Description: wechatcore.String(order.Subject), OutTradeNo: wechatcore.String(order.PaymentNo), TimeExpire: &order.ExpiresAt, NotifyUrl: wechatcore.String(cfg.NotifyURL), Amount: &wechatjsapi.Amount{Total: wechatcore.Int64(order.Amount)}, Payer: &wechatjsapi.Payer{Openid: wechatcore.String(openID)}}) if err != nil { return "", err } return marshal(resp) case "native": resp, _, err := (&wechatnative.NativeApiService{Client: client}).Prepay(ctx, wechatnative.PrepayRequest{ Appid: wechatcore.String(cfg.AppAppID), Mchid: wechatcore.String(cfg.MerchantID), Description: wechatcore.String(order.Subject), OutTradeNo: wechatcore.String(order.PaymentNo), TimeExpire: &order.ExpiresAt, NotifyUrl: wechatcore.String(cfg.NotifyURL), Amount: &wechatnative.Amount{Total: wechatcore.Int64(order.Amount)}}) if err != nil { return "", err } return marshal(resp) } return "", errors.New("unsupported wechat product") }