package payment import ( "context" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "strconv" "strings" "time" "server/models" "github.com/wechatpay-apiv3/wechatpay-go/core" "github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers" "github.com/wechatpay-apiv3/wechatpay-go/core/consts" "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/jsapi" "github.com/wechatpay-apiv3/wechatpay-go/services/payments/native" "github.com/wechatpay-apiv3/wechatpay-go/utils" ) // WechatChannel 微信支付(JSAPI / Native 扫码) // // 渠道参数(config_json): // // mch_id 商户号 // appid 公众号 / 小程序 APPID // cert_serial_no 商户 API 证书序列号 // api_v3_key APIv3 密钥 // // 证书(cert_json): // // key_path apiclient_key.pem 上传后的服务端路径(不入库内容,见渠道证书上传接口) type WechatChannel struct{} func (c *WechatChannel) Code() string { return ChannelWechat } func (c *WechatChannel) Name() string { return "微信支付" } // buildClient 创建微信支付 APIv3 客户端(带自动证书更新与加解密能力) func (c *WechatChannel) buildClient(cfg *ChannelConfig) (*core.Client, string, error) { mchID := cfg.Get("mch_id") serial := cfg.Get("cert_serial_no") apiV3Key := cfg.Get("api_v3_key") if mchID == "" || serial == "" || apiV3Key == "" { return nil, "", fmt.Errorf("微信支付参数不完整:需要 mch_id / cert_serial_no / api_v3_key") } keyPath := cfg.CertPaths["key_path"] if keyPath == "" { return nil, "", fmt.Errorf("微信支付缺少商户私钥:请先上传 apiclient_key.pem") } pem, err := os.ReadFile(keyPath) if err != nil { return nil, "", fmt.Errorf("读取商户私钥失败: %w", err) } privateKey, err := utils.LoadPrivateKey(string(pem)) if err != nil { return nil, "", fmt.Errorf("解析商户私钥失败: %w", err) } client, err := core.NewClient(context.Background(), option.WithMerchantCredential(mchID, serial, privateKey), option.WithWechatPayAutoAuthCipher(mchID, serial, privateKey, apiV3Key), ) if err != nil { return nil, "", fmt.Errorf("创建微信支付客户端失败: %w", err) } return client, mchID, nil } func (c *WechatChannel) Prepay(ctx context.Context, order *models.PlatformPaymentOrder, cfg *ChannelConfig, opt PrepayOption) (*PayParams, error) { client, mchID, err := c.buildClient(cfg) if err != nil { return nil, err } appID := cfg.Get("appid") if appID == "" { return nil, fmt.Errorf("微信支付参数不完整:需要 appid") } notifyURL := cfg.CallbackURL amount := &native.Amount{Total: core.Int64(order.Amount), Currency: core.String("CNY")} // 公众号内支付 if strings.EqualFold(opt.PayType, PayTypeJSAPI) { if opt.OpenID == "" { return nil, fmt.Errorf("微信 JSAPI 支付需要 OpenID") } svc := jsapi.JsapiApiService{Client: client} resp, _, err := svc.PrepayWithRequestPayment(ctx, jsapi.PrepayRequest{ Appid: core.String(appID), Mchid: core.String(mchID), Description: core.String(order.Subject), OutTradeNo: core.String(order.PayNo), NotifyUrl: core.String(notifyURL), Amount: &jsapi.Amount{Total: core.Int64(order.Amount), Currency: core.String("CNY")}, Payer: &jsapi.Payer{Openid: core.String(opt.OpenID)}, }) if err != nil { return nil, fmt.Errorf("微信 JSAPI 下单失败: %w", err) } if resp == nil || resp.PrepayId == nil { return nil, fmt.Errorf("微信 JSAPI 下单未返回 prepay_id") } // 按微信规范自行生成 JSAPI 调起参数(timeStamp/nonceStr/paySign) ts := strconv.FormatInt(time.Now().Unix(), 10) nonce, _ := randomNonce(16) message := appID + "\n" + ts + "\n" + nonce + "\n" + "prepay_id=" + *resp.PrepayId + "\n" sign, err := client.Sign(ctx, message) if err != nil { return nil, fmt.Errorf("生成 JSAPI 签名失败: %w", err) } return &PayParams{ Channel: ChannelWechat, PayType: PayTypeJSAPI, JSAPI: map[string]string{ "appId": appID, "timeStamp": ts, "nonceStr": nonce, "package": "prepay_id=" + *resp.PrepayId, "signType": "RSA", "paySign": sign.Signature, }, }, nil } // 默认:Native 扫码(PC 收银台) req := native.PrepayRequest{ Appid: core.String(appID), Mchid: core.String(mchID), Description: core.String(order.Subject), OutTradeNo: core.String(order.PayNo), NotifyUrl: core.String(notifyURL), Amount: amount, } if order.ExpireAt != nil { req.TimeExpire = order.ExpireAt } svc := native.NativeApiService{Client: client} resp, _, err := svc.Prepay(ctx, req) if err != nil { return nil, fmt.Errorf("微信 Native 下单失败: %w", err) } if resp == nil || resp.CodeUrl == nil { return nil, fmt.Errorf("微信 Native 下单未返回 code_url") } return &PayParams{Channel: ChannelWechat, PayType: PayTypeQR, CodeURL: *resp.CodeUrl}, nil } // normalizeWechatState 微信交易状态 -> 归一化状态 func normalizeWechatState(state string) string { switch state { case "SUCCESS": return StateSuccess case "NOTPAY", "USERPAYING": return StatePending case "CLOSED", "REVOKED": return StateClosed case "PAYERROR": return StateFailed case "REFUND": return StateRefunded default: return StatePending } } func (c *WechatChannel) Query(ctx context.Context, order *models.PlatformPaymentOrder, cfg *ChannelConfig) (*ChannelState, error) { client, mchID, err := c.buildClient(cfg) if err != nil { return nil, err } url := consts.WechatPayAPIServer + "/v3/pay/transactions/out-trade-no/" + order.PayNo + "?mchid=" + mchID result, err := client.Get(ctx, url) if err != nil { return nil, fmt.Errorf("微信查询订单失败: %w", err) } body, err := io.ReadAll(result.Response.Body) if err != nil { return nil, fmt.Errorf("读取微信查询响应失败: %w", err) } tx := &payments.Transaction{} if err := json.Unmarshal(body, tx); err != nil { return nil, fmt.Errorf("解析微信查询响应失败: %w", err) } state := &ChannelState{Raw: string(body)} if tx.TransactionId != nil { state.ChannelTradeNo = *tx.TransactionId } state.TradeState = normalizeWechatState(valueOrEmpty(tx.TradeState)) if tx.Amount != nil && tx.Amount.Total != nil { state.Amount = *tx.Amount.Total } if tx.SuccessTime != nil { if t, perr := time.Parse(time.RFC3339, *tx.SuccessTime); perr == nil { state.PaidAt = &t } } return state, nil } func (c *WechatChannel) ParseNotify(ctx context.Context, r *http.Request, cfg *ChannelConfig) (*NotifyResult, error) { mchID := cfg.Get("mch_id") apiV3Key := cfg.Get("api_v3_key") // 平台证书由 WithWechatPayAutoAuthCipher 注册的下载器维护,直接复用其证书访问器验签 visitor := downloader.MgrInstance().GetCertificateVisitor(mchID) handler, err := notify.NewRSANotifyHandler(apiV3Key, verifiers.NewSHA256WithRSAVerifier(visitor)) if err != nil { return nil, fmt.Errorf("创建微信通知处理器失败: %w", err) } tx := &payments.Transaction{} req, err := handler.ParseNotifyRequest(ctx, r, tx) if err != nil { return nil, fmt.Errorf("微信通知验签/解密失败: %w", err) } result := &NotifyResult{ EventType: req.EventType, EventID: req.ID, AckBody: `{"code":"SUCCESS","message":"成功"}`, Raw: req.Resource.Plaintext, } if tx.OutTradeNo != nil { result.OutTradeNo = *tx.OutTradeNo result.PayNo = *tx.OutTradeNo } if tx.TransactionId != nil { result.ChannelTradeNo = *tx.TransactionId } if tx.Amount != nil && tx.Amount.Total != nil { result.Amount = *tx.Amount.Total } if tx.SuccessTime != nil { if t, perr := time.Parse(time.RFC3339, *tx.SuccessTime); perr == nil { _ = t } } result.TradeState = normalizeWechatState(valueOrEmpty(tx.TradeState)) result.Paid = result.TradeState == StateSuccess return result, nil } func (c *WechatChannel) 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 } body := map[string]any{ "out_trade_no": order.PayNo, "out_refund_no": refundNo, "reason": reason, "notify_url": "", // 退回结果以主动查询为准;如需退回回调,配置后填入 "amount": map[string]any{ "refund": amount, "total": order.Amount, "currency": "CNY", }, } result, err := client.Post(ctx, consts.WechatPayAPIServer+"/v3/refund/domestic/refunds", body) if err != nil { return "", fmt.Errorf("微信退回请求失败: %w", err) } respBody, _ := io.ReadAll(result.Response.Body) var resp struct { RefundID string `json:"refund_id"` Status string `json:"status"` } _ = json.Unmarshal(respBody, &resp) if resp.Status != "" && resp.Status != "SUCCESS" && resp.Status != "PROCESSING" { return resp.RefundID, fmt.Errorf("微信退回未受理,状态: %s", resp.Status) } return resp.RefundID, nil } func (c *WechatChannel) TestConnect(ctx context.Context, cfg *ChannelConfig) (string, error) { client, _, err := c.buildClient(cfg) if err != nil { return "", err } // 平台证书接口只做签名校验,不产生任何交易 result, err := client.Get(ctx, consts.WechatPayAPIServer+"/v3/certificates") if err != nil { return "", fmt.Errorf("微信凭证校验失败: %w", err) } body, _ := io.ReadAll(result.Response.Body) return fmt.Sprintf("连接成功:商户证书与 APIv3 密钥校验通过(平台证书响应 %d 字节)", len(body)), nil } /* ---------------- 小工具 ---------------- */ func valueOrEmpty(v *string) string { if v == nil { return "" } return *v } // randomNonce 生成 n 字节随机数的十六进制串 func randomNonce(n int) (string, error) { buf := make([]byte, n) if _, err := rand.Read(buf); err != nil { return "", err } return hex.EncodeToString(buf), nil }