package payment import ( "context" "errors" "fmt" "net/http" "sort" "strings" "time" "github.com/wechatpay-apiv3/wechatpay-go/core" "github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers" "github.com/wechatpay-apiv3/wechatpay-go/core/downloader" "github.com/wechatpay-apiv3/wechatpay-go/core/notify" "github.com/wechatpay-apiv3/wechatpay-go/core/option" "github.com/wechatpay-apiv3/wechatpay-go/services/payments" "github.com/wechatpay-apiv3/wechatpay-go/services/payments/app" "github.com/wechatpay-apiv3/wechatpay-go/services/payments/h5" "github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi" "github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic" "github.com/wechatpay-apiv3/wechatpay-go/utils" ) const ( TradeStateSuccess = "SUCCESS" // 支付成功 TradeStateRefund = "REFUND" // 转入退款 TradeStateNotPay = "NOTPAY" // 未支付 TradeStateClosed = "CLOSED" // 已关闭 TradeStateRevoked = "REVOKED" // 已撤销(付款码支付) TradeStateUserPaying = "USERPAYING" // 用户支付中(付款码支付) TradeStatePayError = "PAYERROR" // 支付失败(其他原因,如银行返回失败) ) // WechatPayConfig 微信支付配置 type WechatPayConfig struct { AppID string `json:"app_id"` // 应用ID MchID string `json:"mch_id"` // 商户号 MchAPIv3Key string `json:"mch_api_v3_key"` // 商户APIv3密钥 SerialNo string `json:"serial_no"` // 商户证书序列号 PrivateKey string `json:"private_key"` // 商户私钥 NotifyURL string `json:"notify_url"` // 支付通知地址 RefundURL string `json:"refund_url"` // 退款通知地址 } // WechatPayService 微信支付服务 type WechatPayService struct { config WechatPayConfig client *core.Client jsapiService *jsapi.JsapiApiService } // NewWechatPayService 创建微信支付服务 func NewWechatPayService(config *WechatPayConfig) (*WechatPayService, error) { if err := validateWechatPayConfig(config); err != nil { return nil, err } // 加载商户私钥 privateKey, err := utils.LoadPrivateKey(config.PrivateKey) if err != nil { return nil, fmt.Errorf("加载商户私钥失败: %w", err) } // 创建微信支付客户端 opts := []core.ClientOption{ option.WithWechatPayAutoAuthCipher(config.MchID, config.SerialNo, privateKey, config.MchAPIv3Key), } client, err := core.NewClient(context.Background(), opts...) if err != nil { return nil, fmt.Errorf("创建微信支付客户端失败: %w", err) } // 下载并加载微信支付平台证书 certDownloader, err := downloader.NewCertificateDownloader(context.Background(), config.MchID, privateKey, config.MchAPIv3Key, config.SerialNo) if err != nil { return nil, fmt.Errorf("创建证书下载器失败: %w", err) } if err := certDownloader.DownloadCertificates(context.Background()); err != nil { return nil, fmt.Errorf("下载微信支付平台证书失败: %w", err) } return &WechatPayService{ config: *config, client: client, jsapiService: &jsapi.JsapiApiService{Client: client}, }, nil } // CreateOrder 创建支付订单 func (w *WechatPayService) CreateOrder(order *Order) (*OrderResult, error) { if err := validateCreateOrder(order); err != nil { return nil, err } switch order.Platform { case PlatformApp: return w.createAppOrder(order) case PlatformH5: return w.createH5Order(order) case PlatformMiniProg: return w.createJsapiOrder(order) default: return nil, errors.New("不支持的支付平台") } } // createAppOrder 创建APP支付订单 func (w *WechatPayService) createAppOrder(order *Order) (*OrderResult, error) { svc := app.AppApiService{Client: w.client} amount := &app.Amount{ Total: core.Int64(int64(order.Amount * 100)), // 转换为分 Currency: core.String("CNY"), } req := app.PrepayRequest{ Appid: core.String(w.config.AppID), Mchid: core.String(w.config.MchID), Description: core.String(order.Subject), OutTradeNo: core.String(order.OutTradeNo), NotifyUrl: core.String(w.config.NotifyURL), Amount: amount, } resp, _, err := svc.Prepay(context.Background(), req) if err != nil { return nil, fmt.Errorf("创建APP支付订单失败: %w", err) } // 生成支付参数 params, err := w.generateAppPayParams(resp.PrepayId) if err != nil { return nil, err } return &OrderResult{ OrderNo: order.OutTradeNo, PayParams: params, ExpireTime: time.Now().Add(30 * time.Minute), CreateTime: time.Now(), }, nil } // createH5Order 创建H5支付订单 func (w *WechatPayService) createH5Order(order *Order) (*OrderResult, error) { svc := h5.H5ApiService{Client: w.client} amount := &h5.Amount{ Total: core.Int64(int64(order.Amount * 100)), // 转换为分 Currency: core.String("CNY"), } req := h5.PrepayRequest{ Appid: core.String(w.config.AppID), Mchid: core.String(w.config.MchID), Description: core.String(order.Subject), OutTradeNo: core.String(order.OutTradeNo), NotifyUrl: core.String(w.config.NotifyURL), Amount: amount, SceneInfo: &h5.SceneInfo{ PayerClientIp: core.String("127.0.0.1"), // TODO: 获取真实IP H5Info: &h5.H5Info{ Type: core.String("Wap"), }, }, } resp, _, err := svc.Prepay(context.Background(), req) if err != nil { return nil, fmt.Errorf("创建H5支付订单失败: %w", err) } return &OrderResult{ OrderNo: order.OutTradeNo, PayParams: resp.H5Url, ExpireTime: time.Now().Add(30 * time.Minute), CreateTime: time.Now(), }, nil } // createJsapiOrder 创建小程序支付订单 func (w *WechatPayService) createJsapiOrder(order *Order) (*OrderResult, error) { if order.OpenID == "" { return nil, errors.New("小程序支付必须提供OpenID") } svc := jsapi.JsapiApiService{Client: w.client} amount := &jsapi.Amount{ Total: core.Int64(int64(order.Amount * 100)), // 转换为分 Currency: core.String("CNY"), } req := jsapi.PrepayRequest{ Appid: core.String(w.config.AppID), Mchid: core.String(w.config.MchID), Description: core.String(order.Subject), OutTradeNo: core.String(order.OutTradeNo), NotifyUrl: core.String(w.config.NotifyURL), Amount: amount, Payer: &jsapi.Payer{ Openid: core.String(order.OpenID), }, } resp, _, err := svc.Prepay(context.Background(), req) if err != nil { return nil, fmt.Errorf("创建小程序支付订单失败: %w", err) } // 生成支付参数 params, err := w.generateJsapiPayParams(resp.PrepayId) if err != nil { return nil, err } return &OrderResult{ OrderNo: order.OutTradeNo, PayParams: params, ExpireTime: time.Now().Add(30 * time.Minute), CreateTime: time.Now(), }, nil } // Refund 申请退款 func (w *WechatPayService) Refund(refund *Refund) (*RefundResult, error) { if err := validateRefund(refund); err != nil { return nil, err } svc := refunddomestic.RefundsApiService{Client: w.client} amount := &refunddomestic.AmountReq{ Refund: core.Int64(int64(refund.RefundAmount * 100)), // 转换为分 Total: core.Int64(int64(refund.TotalAmount * 100)), // 转换为分 Currency: core.String("CNY"), } req := refunddomestic.CreateRequest{ OutTradeNo: core.String(refund.OrderNo), OutRefundNo: core.String(refund.RefundNo), Reason: core.String(refund.Reason), NotifyUrl: core.String(refund.NotifyURL), Amount: amount, } resp, _, err := svc.Create(context.Background(), req) if err != nil { return nil, fmt.Errorf("申请退款失败: %w", err) } return &RefundResult{ OrderNo: refund.OrderNo, RefundNo: refund.RefundNo, RefundID: *resp.RefundId, Status: string(*resp.Status), RefundTime: time.Now(), RefundAmount: refund.RefundAmount, }, nil } // QueryOrder 查询订单 func (w *WechatPayService) QueryOrder(query *OrderQuery) (*OrderQueryResult, error) { if err := validateOrderQuery(query); err != nil { return nil, err } var resp *payments.Transaction var err error if query.TransactionID != "" { resp, _, err = w.jsapiService.QueryOrderById(context.Background(), jsapi.QueryOrderByIdRequest{ TransactionId: core.String(query.TransactionID), }) } else { resp, _, err = w.jsapiService.QueryOrderByOutTradeNo(context.Background(), jsapi.QueryOrderByOutTradeNoRequest{ OutTradeNo: core.String(query.OrderNo), Mchid: core.String(w.config.MchID), }) } if err != nil { return nil, fmt.Errorf("查询订单失败: %w", err) } // 转换支付状态 status := convertWechatPayStatus(*resp.TradeState) // 解析支付时间 payTime, _ := time.Parse(time.RFC3339, *resp.SuccessTime) return &OrderQueryResult{ OrderNo: *resp.OutTradeNo, TransactionID: *resp.TransactionId, Status: status, Amount: float64(*resp.Amount.Total) / 100, // 转换为元 PayTime: payTime, PayMethod: MethodWechat, Platform: getWechatPayPlatform(*resp.TradeType), }, nil } // HandlePaymentNotification 处理支付结果通知 func (w *WechatPayService) HandlePaymentNotification(r *http.Request) (*OrderQueryResult, error) { // 初始化通知处理器 handler := notify.NewNotifyHandler(w.config.MchAPIv3Key, verifiers.NewSHA256WithRSAVerifier(downloader.MgrInstance().GetCertificateVisitor(w.config.MchID))) // 解析通知数据 transaction := new(payments.Transaction) notifyReq, err := handler.ParseNotifyRequest(context.Background(), r, transaction) if err != nil { return nil, fmt.Errorf("解析支付通知失败: %w", err) } // 验证通知数据 if notifyReq.EventType != "TRANSACTION.SUCCESS" { return nil, errors.New("非支付成功通知") } // 转换支付状态 status := convertWechatPayStatus(*transaction.TradeState) // 解析支付时间 payTime, _ := time.Parse(time.RFC3339, *transaction.SuccessTime) return &OrderQueryResult{ OrderNo: *transaction.OutTradeNo, TransactionID: *transaction.TransactionId, Status: status, Amount: float64(*transaction.Amount.Total) / 100, // 转换为元 PayTime: payTime, PayMethod: MethodWechat, Platform: getWechatPayPlatform(*transaction.TradeType), }, nil } // 生成APP支付参数 func (w *WechatPayService) generateAppPayParams(prepayID *string) (map[string]string, error) { if prepayID == nil { return nil, errors.New("预支付ID不能为空") } params := make(map[string]string) params["appid"] = w.config.AppID params["partnerid"] = w.config.MchID params["prepayid"] = *prepayID params["package"] = "Sign=WXPay" nonce, err := utils.GenerateNonce() if err != nil { return nil, fmt.Errorf("生成随机字符串失败: %w", err) } params["noncestr"] = nonce params["timestamp"] = fmt.Sprintf("%d", time.Now().Unix()) // 生成签名 sign, err := w.signParams(params) if err != nil { return nil, err } params["sign"] = sign return params, nil } // 生成小程序支付参数 func (w *WechatPayService) generateJsapiPayParams(prepayID *string) (map[string]string, error) { if prepayID == nil { return nil, errors.New("预支付ID不能为空") } params := make(map[string]string) params["appId"] = w.config.AppID params["timeStamp"] = fmt.Sprintf("%d", time.Now().Unix()) nonce, err := utils.GenerateNonce() if err != nil { return nil, fmt.Errorf("生成随机字符串失败: %w", err) } params["nonceStr"] = nonce params["package"] = "prepay_id=" + *prepayID params["signType"] = "RSA" // 生成签名 sign, err := w.signParams(params) if err != nil { return nil, err } params["paySign"] = sign return params, nil } // 签名参数 func (w *WechatPayService) signParams(params map[string]string) (string, error) { // 按字典序排序参数 var keys []string for k := range params { keys = append(keys, k) } sort.Strings(keys) var pairs []string for _, k := range keys { pairs = append(pairs, fmt.Sprintf("%s=%s", k, params[k])) } message := strings.Join(pairs, "&") // 使用商户私钥签名 privateKey, err := utils.LoadPrivateKey(w.config.PrivateKey) if err != nil { return "", fmt.Errorf("加载商户私钥失败: %w", err) } signature, err := utils.SignSHA256WithRSA(message, privateKey) if err != nil { return "", fmt.Errorf("签名失败: %w", err) } return signature, nil } // 转换微信支付状态 func convertWechatPayStatus(tradeState string) PaymentStatus { switch tradeState { case "SUCCESS": return StatusSuccess case "REFUND": return StatusRefunded case "NOTPAY": return StatusPending case "CLOSED": return StatusClosed case "PAYERROR": return StatusFailed default: return StatusPending } } // 获取微信支付平台 func getWechatPayPlatform(tradeType string) PaymentPlatform { switch tradeType { case "APP": return PlatformApp case "JSAPI": return PlatformMiniProg case "NATIVE", "MWEB": return PlatformH5 default: return PlatformApp } } // 验证微信支付配置 func validateWechatPayConfig(config *WechatPayConfig) error { if config == nil { return errors.New("配置不能为空") } if config.AppID == "" { return errors.New("应用ID不能为空") } if config.MchID == "" { return errors.New("商户号不能为空") } if config.MchAPIv3Key == "" { return errors.New("商户APIv3密钥不能为空") } if config.SerialNo == "" { return errors.New("商户证书序列号不能为空") } if config.PrivateKey == "" { return errors.New("商户私钥不能为空") } if config.NotifyURL == "" { return errors.New("支付通知地址不能为空") } return nil } // 验证创建订单参数 func validateCreateOrder(order *Order) error { if order == nil { return errors.New("订单信息不能为空") } if order.OutTradeNo == "" { return errors.New("商户订单号不能为空") } if order.Subject == "" { return errors.New("商品描述不能为空") } if order.Amount <= 0 { return errors.New("支付金额必须大于0") } if order.NotifyURL == "" { return errors.New("通知地址不能为空") } return nil } // 验证退款参数 func validateRefund(refund *Refund) error { if refund == nil { return errors.New("退款信息不能为空") } if refund.OrderNo == "" { return errors.New("商户订单号不能为空") } if refund.RefundNo == "" { return errors.New("商户退款单号不能为空") } if refund.RefundAmount <= 0 { return errors.New("退款金额必须大于0") } if refund.TotalAmount <= 0 { return errors.New("订单总金额必须大于0") } if refund.RefundAmount > refund.TotalAmount { return errors.New("退款金额不能大于订单总金额") } if refund.NotifyURL == "" { return errors.New("退款通知地址不能为空") } return nil } // 验证订单查询参数 func validateOrderQuery(query *OrderQuery) error { if query == nil { return errors.New("查询参数不能为空") } if query.OrderNo == "" && query.TransactionID == "" { return errors.New("商户订单号和交易号不能同时为空") } return nil } // GenerateOutTradeNo 生成商户订单号 func (w *WechatPayService) GenerateOutTradeNo() string { return GenerateOrderNo(WechatPrefix) }