Files
yunzerwebsiteallinone/go/services/payment/channel_wechat.go
T
2026-09-15 12:46:29 +08:00

317 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}