本文面向有基础编程能力的独立开发者,手把手教你从零开始在PC网站接入微信支付Native支付。全文基于 微信支付官方APIv3文档 ,提供可直接运行的Go语言Demo代码,预计30分钟即可完成支付流程跑通。
👉 点击查看《微信支付系列教程》文章目录
在《微信支付系列教程》系列文章中,我们将详细介绍如何从0到1在自己的网站中接入微信支付。以下是该系列文章的全部内容:
〇、本文适合谁阅读
- 需要在PC网站集成微信扫码支付的个人开发者
- 已完成微信商户号注册,但不知如何调用支付接口的技术人员
- 希望系统学习微信支付APIv3接口对接的后端开发者
- 寻找完整可运行示例代码的独立站长
阅读本文后,你将能够独立完成:
- Native支付权限申请与参数配置
- 调用微信支付接口生成支付二维码
- 处理支付结果回调通知(含签名验证与解密)
- 实现订单查询、关单、退款等完整支付闭环
- 搭建可运行的MVP支付Demo
在开始之前,请确保已完成以下准备工作:
- 了解哪种微信支付产品更适合你的业务场景
- 如何注册微信商户号
- 商户号与APPID绑定
- 准备Go 1.21+开发环境(可选)
一、Native支付产品介绍
1.1 什么是Native支付
Native支付是微信支付提供的一种支付方式,主要面向PC端网页浏览器场景,允许商户在自己的网站中生成支付二维码,用户使用微信"扫一扫"功能扫描二维码完成支付。
根据 微信支付官方文档 ,Native支付的核心特点是:
- 商户下单获取二维码链接:商户系统调用Native支付下单接口,获取订单的二维码链接
code_url - 前端转换为二维码图片:将
code_url转换为二维码图片展示给用户 - 用户微信扫码支付:用户使用微信"扫一扫"扫描页面上的二维码(注意:不支持通过相册识别或长按识别二维码)
- 支付确认与完成:扫码后进入微信支付确认界面,用户确认金额和收款方后完成支付
- 支付结果通知:支付成功后,微信会向商户系统发送支付成功回调通知
1.2 适用场景
Native支付特别适用于以下业务场景:
- PC网站支付:用户在电脑浏览器中浏览商品,需要支付时展示二维码,用户使用手机微信扫码完成支付
- 实体店单品或订单支付:在实体门店的收银系统中,生成订单二维码供顾客扫码支付
- 媒体广告支付:在PC端展示的广告内容中嵌入支付二维码
- 自助终端支付:在自助服务终端设备上展示支付二维码
不适用场景:
- 移动端H5页面(应使用H5支付或JSAPI支付)
- 微信小程序内(应使用小程序支付)
- 移动APP内(应使用APP支付)
1.3 Native支付模式详解
根据 官方Native支付文档 ,Native支付的完整流程如下:
第一阶段:商户下单生成二维码
- 用户在商户网站选择商品并确认下单
- 商户后端调用Native支付下单API,传入订单信息
- 微信支付系统返回
code_url(二维码链接) - 商户前端将
code_url转换为二维码图片展示给用户
第二阶段:用户扫码支付
- 用户打开微信"扫一扫"功能
- 扫描商户页面展示的二维码
- 微信解析二维码内容,调起微信支付收银台
- 用户在收银台确认收款方和金额
- 用户输入支付密码或使用指纹/面容识别完成支付
- 微信支付系统处理支付请求
第三阶段:支付结果处理
- 支付成功后,微信支付系统向商户系统发送支付成功回调通知
- 商户系统接收回调,验证签名,解密通知内容
- 商户系统更新订单状态为"已支付"
- 商户前端展示支付成功页面给用户
二、Native支付接入前准备
2.1 资质要求
根据 Native支付快速开始文档 和 权限申请文档 ,接入Native支付需要满足以下条件:
1. 公众账号要求(三选一)
- 已认证的服务号
- 政府或媒体类型的已认证公众号
- 已认证的小程序
- 已认证的移动应用(开放平台账号)
2. 商户号要求
- 已完成入驻的微信商户号
- 商户号已开通Native支付权限
3. 绑定关系
- 商户号与APPID已完成绑定授权
- 绑定路径:商户平台 -> 产品中心 -> APPID账号管理 -> 关联APPID
2.2 开通Native支付权限
情况一:已有商户号,新增Native支付权限
如果你已经有商户号但未开通Native支付权限,按以下步骤操作:
- 登录商户平台:访问 https://pay.weixin.qq.com
- 进入产品中心:点击顶部导航栏"产品中心"
- 找到Native支付:在支付产品列表中找到"Native支付"
- 申请开通:点击"申请开通"按钮
- 等待审核:通常需要7个工作日内完成审核
详细操作可参考 申请Native支付权限指引 。
情况二:新申请商户号并开通Native支付
如果你还没有商户号,可以在申请商户号的同时申请Native支付权限:
- 访问商户入驻页面: 微信支付商户入驻
- 填写经营与行业信息:
- 在"经营场景"中必须勾选"PC网站"
- 上传网站授权函(若备案主体与申请主体不同)
- 填写PC网站对应的服务号/公众号APPID
- 填写PC网站域名(需ICP备案)
- 提交审核:审核通过后将自动开通Native支付权限
注意事项:
- 支持申请Native支付的主体类型:个体工商户、企业、事业单位、政府机关、社会组织
- 申请时填写的APPID会自动发起绑定申请,需在公众平台确认授权
2.3 获取开发必要参数
在开始开发前,你需要准备以下参数:
| 参数名称 | 参数说明 | 获取方式 |
|---|---|---|
| APPID | 公众账号ID | 公众平台/开放平台查看 |
| MCHID | 商户号 | 商户平台查看 |
| 商户API证书 | 用于API请求签名 | 商户平台下载 |
| 微信支付公钥 | 用于验证微信支付回调签名 | 通过API获取或手动下载 |
| APIv3密钥 | 用于加密敏感数据和验证签名 | 商户平台设置 |
获取路径:
- APPID获取:
- 服务号/公众号:公众平台 -> 设置与开发 -> 账号设置 -> 注册信息 -> AppID
- 小程序:公众平台 -> 开发与服务 -> 开发管理 -> 开发设置 -> AppID
- 移动应用:开放平台 -> 管理中心 -> 移动应用 -> 查看详情 -> APPID
- 商户号(MCHID):
- 商户平台 -> 账户中心 -> 商户信息 -> 基本账户信息 -> 商户号
- 获取微信支付公钥:
- 商户平台 -> 账户中心 -> API安全 -> 管理公钥
- 下载公钥
pub_key.pem - 复制公钥ID
- 商户API证书下载:
- 商户平台 -> 账户中心 -> API安全 -> 申请API证书 -> 下载证书工具 -> 解压缩文件并按步骤操作获得请求串 -> 商户API证书获取方法及功能介绍
- 下载后包含:证书序列号、证书私钥(
apiclient_key.pem)、证书文件(apiclient_cert.pem) - 获取证书序列号:
openssl x509 -in cert/apiclient_cert.pem -noout -serial | cut -d= -f2 | tr '[:upper:]' '[:lower:]'
- APIv3密钥设置:
- 商户平台 -> 账户中心 -> API安全 -> 设置APIv3密钥
- 生成32个字符的密钥:
openssl rand -hex 16 - 设置后立即生效,请妥善保存
详细参数说明可参考: 普通商户模式开发必要参数说明 。
另外,还需要在产品中心-开发配置中配置Native支付的回调链接:
三、接入微信Native支付代码实战:开发环境搭建
3.1 技术栈选择
本教程使用以下技术栈演示:
- 后端Web服务:Go语言Gin框架 github.com/gin-gonic/gin
- 前端: Go语言HTML模板语法 + JavaScript(原生)
- 二维码生成:JavaScript库(qrcode.js)
- 微信官方wechatpay-go SDK:github.com/wechatpay-apiv3/wechatpay-go
3.2 项目结构
./wechatpay-native-demo
├── README.md
├── cert
│ ├── apiclient_cert.p12
│ ├── apiclient_cert.pem
│ ├── apiclient_key.pem
│ └── pub_key.pem
├── config
│ └── config.go
├── go.mod
├── go.sum
├── handler
│ ├── notify.go
│ └── payment.go
├── main.go
├── service
│ └── wechatpay.go
└── templates
├── index.html
├── pay.html
└── success.html
四、Native支付核心开发流程详解
根据 Native支付开发指引文档 ,Native支付的完整开发流程分为四个阶段:
4.1 阶段一:商户下单生成二维码
4.1.1 接口说明
Native支付下单API:
- 请求方式:POST
- 请求URL:
/v3/pay/transactions/native - 请求域名:
https://api.mch.weixin.qq.com(主域名)或https://api2.mch.weixin.qq.com(备域名)
详细接口文档可参考 Native下单API文档 。
4.1.2 请求参数详解
| 参数 | 必填 | 类型 | 描述 |
|---|---|---|---|
appid | 是 | string(32) | 公众账号ID(服务号/小程序/移动应用) |
mchid | 是 | string(32) | 商户号 |
description | 是 | string(127) | 商品描述,会显示在微信账单中 |
out_trade_no | 是 | string(32) | 商户系统内部订单号(6-32字符,同一商户号下唯一) |
time_expire | 否 | string(64) | 支付结束时间(rfc3339格式,如2025-01-29T12:00:00+08:00) |
attach | 否 | string(128) | 商户数据包,原样返回 |
notify_url | 是 | string(255) | 支付结果回调地址,必须为外网可访问URL |
goods_tag | 否 | string(32) | 订单优惠标记,用于代金券 |
support_fapiao | 否 | boolean | 是否开通电子发票入口 |
amount | 是 | object | 订单金额信息 |
detail | 否 | object | 商品详情 |
amount对象:
| 参数 | 必填 | 类型 | 描述 |
|---|---|---|---|
| total | 是 | int | 订单总金额,单位为分 |
| currency | 否 | string(16) | 货币类型,默认CNY(人民币) |
关键参数说明:
- time_expire(支付结束时间):
- 格式:
yyyy-MM-DDTHH:mm:ss+TIMEZONE,如2025-01-29T13:29:35+08:00 - 若未指定,默认订单有效期为7天
- 支付结束时间不能早于下单时间后1分钟
- 支付结束时间需在下单时间的15天以内
- 超过支付结束时间后,用户支付将收到"订单已超过商户设置的最晚支付成功时间,请重新发起支付"的提示
- 格式:
- code_url有效期:
- 返回的
code_url有效期为2小时 - 超过2小时后,需要使用原下单参数重新请求下单接口,获取新的
code_url
- 返回的
4.1.3 服务端核心逻辑Go代码实现
创建 config/config.go:
package config
import (
"os"
)
type WechatPayConfig struct {
AppID string // 公众账号ID
MchID string // 商户号
APIv3Key string // APIv3密钥
CertSerialNo string // 商户证书序列号
PrivateKeyPath string // 商户私钥文件路径
PublicKeyID string // 微信支付公钥ID
PublicKeyPath string // 微信支付公钥文件路径
NotifyURL string // 支付结果回调地址
}
func LoadConfig() *WechatPayConfig {
return &WechatPayConfig{
AppID: os.Getenv("WXPAY_APPID"),
MchID: os.Getenv("WXPAY_MCHID"),
APIv3Key: os.Getenv("WXPAY_APIV3_KEY"),
CertSerialNo: os.Getenv("WXPAY_CERT_SERIAL_NO"),
PrivateKeyPath: os.Getenv("WXPAY_PRIVATE_KEY_PATH"),
PublicKeyID: os.Getenv("WXPAY_PUBLIC_KEY_ID"),
PublicKeyPath: os.Getenv("WXPAY_PUBLIC_KEY_PATH"),
NotifyURL: os.Getenv("WXPAY_NOTIFY_URL"),
}
}
创建 service/wechatpay.go:
package service
import (
"context"
"fmt"
"wechatpay-native-demo/config"
"github.com/wechatpay-apiv3/wechatpay-go/core"
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
"github.com/wechatpay-apiv3/wechatpay-go/utils"
)
type WechatPayService struct {
client *core.Client
config *config.WechatPayConfig
}
// NewWechatPayService 创建微信支付服务(使用微信支付公钥模式)
func NewWechatPayService(cfg *config.WechatPayConfig) (*WechatPayService, error) {
// 加载商户私钥
privateKey, err := utils.LoadPrivateKeyWithPath(cfg.PrivateKeyPath)
if err != nil {
return nil, fmt.Errorf("加载商户私钥失败: %v", err)
}
// 加载微信支付公钥(用于验签)
wechatPayPublicKey, err := utils.LoadPublicKeyWithPath(cfg.PublicKeyPath)
if err != nil {
return nil, fmt.Errorf("加载微信支付公钥失败: %v", err)
}
// 创建商户配置(使用公钥模式)
opts := []core.ClientOption{
// 使用商户私钥进行请求签名,使用微信支付公钥验证响应
option.WithWechatPayPublicKeyAuthCipher(
cfg.MchID, // 商户号
cfg.CertSerialNo, // 商户证书序列号
privateKey, // 商户私钥
cfg.PublicKeyID, // 微信支付公钥ID
wechatPayPublicKey, // 微信支付公钥
),
}
// 创建客户端
client, err := core.NewClient(context.Background(), opts...)
if err != nil {
return nil, fmt.Errorf("创建微信支付客户端失败: %v", err)
}
return &WechatPayService{
client: client,
config: cfg,
}, nil
}
// CreateNativeOrder 创建Native支付订单
func (s *WechatPayService) CreateNativeOrder(outTradeNo, description string, totalFee int64) (string, error) {
svc := native.NativeApiService{Client: s.client}
resp, result, err := svc.Prepay(context.Background(), native.PrepayRequest{
Appid: core.String(s.config.AppID),
Mchid: core.String(s.config.MchID),
Description: core.String(description),
OutTradeNo: core.String(outTradeNo),
NotifyUrl: core.String(s.config.NotifyURL),
Amount: &native.Amount{
Total: core.Int64(totalFee),
},
})
if err != nil {
return "", fmt.Errorf("创建订单失败: %v, result: %+v", err, result)
}
return *resp.CodeUrl, nil
}
// QueryOrder 查询订单
func (s *WechatPayService) QueryOrder(outTradeNo string) (*payments.Transaction, error) {
svc := native.NativeApiService{Client: s.client}
resp, result, err := svc.QueryOrderByOutTradeNo(context.Background(), native.QueryOrderByOutTradeNoRequest{
OutTradeNo: core.String(outTradeNo),
Mchid: core.String(s.config.MchID),
})
if err != nil {
return nil, fmt.Errorf("查询订单失败: %v, result: %+v", err, result)
}
return resp, nil
}
创建 handler/payment.go:
package handler
import (
"fmt"
"net/http"
"time"
"wechatpay-native-demo/service"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type PaymentHandler struct {
wechatPay *service.WechatPayService
}
func NewPaymentHandler(wechatPay *service.WechatPayService) *PaymentHandler {
return &PaymentHandler{wechatPay: wechatPay}
}
// CreateOrderRequest 创建订单请求
type CreateOrderRequest struct {
ProductName string `json:"product_name" binding:"required"`
Amount int64 `json:"amount" binding:"required,min=1"` // 单位:分
}
// CreateOrderResponse 创建订单响应
type CreateOrderResponse struct {
OrderID string `json:"order_id"`
CodeURL string `json:"code_url"`
Amount int64 `json:"amount"`
ProductName string `json:"product_name"`
}
// CreateOrder 创建支付订单
func (h *PaymentHandler) CreateOrder(c *gin.Context) {
var req CreateOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 生成订单号(实际项目中应使用更严谨的订单号生成策略)
outTradeNo := fmt.Sprintf("NATIVE%s%s", time.Now().Format("20060102150405"), uuid.New().String()[:8])
// 调用微信支付接口创建订单
codeURL, err := h.wechatPay.CreateNativeOrder(outTradeNo, req.ProductName, req.Amount)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 保存订单信息到数据库(实际项目中应持久化存储)
// TODO: 保存订单到数据库
c.JSON(http.StatusOK, CreateOrderResponse{
OrderID: outTradeNo,
CodeURL: codeURL,
Amount: req.Amount,
ProductName: req.ProductName,
})
}
// QueryOrder 查询订单状态
func (h *PaymentHandler) QueryOrder(c *gin.Context) {
orderID := c.Param("order_id")
// TODO: 从数据库查询订单状态
// 调用微信支付查询接口
resp, err := h.wechatPay.QueryOrder(orderID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
// 构建响应
result := gin.H{
"order_id": orderID,
"status": "NOTPAY", // 默认未支付
}
if resp.TradeState != nil {
result["status"] = *resp.TradeState
// 如果支付成功,返回更多信息
if *resp.TradeState == "SUCCESS" {
if resp.TransactionId != nil {
result["transaction_id"] = *resp.TransactionId
}
if resp.SuccessTime != nil {
result["success_time"] = *resp.SuccessTime
}
if resp.Amount != nil && resp.Amount.Total != nil {
result["total_amount"] = *resp.Amount.Total
}
if resp.Payer != nil && resp.Payer.Openid != nil {
result["payer_openid"] = *resp.Payer.Openid
}
}
}
// 添加交易状态描述
if resp.TradeStateDesc != nil {
result["trade_state_desc"] = *resp.TradeStateDesc
}
c.JSON(http.StatusOK, result)
}
4.2 阶段二:前端展示二维码
创建 templates/pay.html:
<body>
<div class="container">
<div class="header">
<h1>微信扫码支付</h1>
<p id="product-name">商品名称加载中...</p>
</div>
<div class="amount"><span>¥</span><span id="amount">0.00</span></div>
<div class="qrcode-container">
<div id="qrcode"></div>
</div>
<div class="tips">
<p>请使用微信扫一扫扫描二维码完成支付</p>
<p class="highlight">注意:不支持长按识别或从相册识别</p>
</div>
<div class="countdown" id="countdown">
二维码有效期:<span id="timer">30:00</span>
</div>
<button class="refresh-btn" onclick="refreshOrder()">刷新二维码</button>
<div id="status" class="status"></div>
</div>
<script>
let orderId = "";
let codeUrl = "";
let checkInterval = null;
let countdownInterval = null;
let remainingTime = 30 * 60; // 30分钟(秒)
// 页面加载时执行
window.onload = function () {
createOrder();
};
// 创建订单
function createOrder() {
const productName =
new URLSearchParams(window.location.search).get("product") ||
"测试商品";
const amount =
new URLSearchParams(window.location.search).get("amount") || 1;
fetch("/api/order", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
product_name: productName,
amount: parseInt(amount),
}),
})
.then((response) => response.json())
.then((data) => {
if (data.error) {
showStatus("error", "创建订单失败:" + data.error);
return;
}
orderId = data.order_id;
codeUrl = data.code_url;
// 更新页面信息
document.getElementById("product-name").textContent =
data.product_name;
document.getElementById("amount").textContent = (
data.amount / 100
).toFixed(2);
// 生成二维码
generateQRCode(codeUrl);
// 开始倒计时
startCountdown();
// 开始轮询查询订单状态
startPolling();
})
.catch((error) => {
showStatus("error", "网络错误:" + error.message);
});
}
// 生成二维码
function generateQRCode(url) {
// 清空之前的二维码
const container = document.getElementById("qrcode");
container.innerHTML = "";
// 生成新二维码
new QRCode(container, {
text: url,
width: 256,
height: 256,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.M,
});
}
// 开始倒计时
function startCountdown() {
remainingTime = 30 * 60; // 重置为30分钟
if (countdownInterval) {
clearInterval(countdownInterval);
}
countdownInterval = setInterval(function () {
remainingTime--;
if (remainingTime <= 0) {
clearInterval(countdownInterval);
document.getElementById("timer").textContent = "已过期";
document.getElementById("countdown").style.background = "#f8d7da";
document.getElementById("countdown").style.color = "#721c24";
return;
}
const minutes = Math.floor(remainingTime / 60);
const seconds = remainingTime % 60;
document.getElementById("timer").textContent =
minutes.toString().padStart(2, "0") +
":" +
seconds.toString().padStart(2, "0");
}, 1000);
}
// 开始轮询查询订单状态
function startPolling() {
if (checkInterval) {
clearInterval(checkInterval);
}
// 每3秒查询一次
checkInterval = setInterval(function () {
checkOrderStatus();
}, 3000);
}
// 查询订单状态
function checkOrderStatus() {
if (!orderId) return;
fetch("/api/order/" + orderId)
.then((response) => response.json())
.then((data) => {
if (data.status === "SUCCESS") {
// 支付成功
clearInterval(checkInterval);
clearInterval(countdownInterval);
showStatus("success", "支付成功!即将跳转...");
// 2秒后跳转到成功页面
setTimeout(function () {
window.location.href = "/success?order_id=" + orderId;
}, 2000);
} else if (data.status === "CLOSED") {
// 订单已关闭
clearInterval(checkInterval);
showStatus("error", "订单已关闭,请重新下单");
}
})
.catch((error) => {
console.error("查询订单状态失败:", error);
});
}
// 刷新订单
function refreshOrder() {
clearInterval(checkInterval);
clearInterval(countdownInterval);
createOrder();
}
// 显示状态信息
function showStatus(type, message) {
const statusDiv = document.getElementById("status");
statusDiv.className = "status " + type;
statusDiv.textContent = message;
}
// 页面关闭时清理定时器
window.onbeforeunload = function () {
if (checkInterval) clearInterval(checkInterval);
if (countdownInterval) clearInterval(countdownInterval);
};
</script>
</body>
4.3 阶段三:处理支付结果通知
根据
支付成功回调通知文档
,当用户支付成功后,微信支付会向商户指定的 notify_url 发送回调通知。
4.3.1 回调处理流程
步骤1:接收回调通知
- 微信支付通过POST请求发送回调通知
- 请求体包含JSON格式的加密通知数据
步骤2:验证签名
- 从HTTP头获取签名信息:
Wechatpay-Serial:验签的微信支付平台证书序列号Wechatpay-Signature:签名值Wechatpay-Timestamp:时间戳Wechatpay-Nonce:随机字符串
- 使用平台证书验证签名,确保通知来自微信支付
步骤3:解密通知内容
- 使用APIv3密钥解密
resource.ciphertext - 加密算法:
AEAD_AES_256_GCM - 解密参数:
nonce:加密使用的随机串associated_data:附加数据(通常为"transaction")
步骤4:处理业务逻辑
- 更新订单状态为"已支付"
- 记录支付完成时间、微信支付订单号等信息
- 处理库存、发货等后续业务
步骤5:响应微信支付
- 验签通过:返回HTTP状态码200或204,无需返回报文
- 验签失败:返回HTTP状态码5XX或4XX,并返回错误信息
重要提醒:
- 必须在5秒内完成验签并应答
- 推荐先应答成功,再异步处理业务逻辑
- 商户系统不能仅依赖回调通知,需结合查询接口使用
- 若未正确应答,微信支付会按照特定频次重复发送通知(最多15次)
4.3.2 Go代码实现接收微信支付回调通知
创建 handler/notify.go:
package handler
import (
"context"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
"github.com/wechatpay-apiv3/wechatpay-go/utils"
)
// NotifyHandler 支付回调处理器
type NotifyHandler struct {
handler *notify.Handler
apiV3Key string
}
// NewNotifyHandler 创建支付回调处理器
func NewNotifyHandler(apiV3Key, wechatPayPublicKeyID, publicKeyPath string) (*NotifyHandler, error) {
// 加载微信支付公钥
wechatPayPublicKey, err := utils.LoadPublicKeyWithPath(publicKeyPath)
if err != nil {
return nil, fmt.Errorf("加载微信支付公钥失败: %v", err)
}
// 初始化 notify.Handler,使用 SDK 提供的验签方法
handler := notify.NewNotifyHandler(
apiV3Key,
verifiers.NewSHA256WithRSAPubkeyVerifier(wechatPayPublicKeyID, *wechatPayPublicKey),
)
return &NotifyHandler{
handler: handler,
apiV3Key: apiV3Key,
}, nil
}
// PaymentNotify 支付结果回调
func (h *NotifyHandler) PaymentNotify(c *gin.Context) {
// 使用 SDK 的 Handler 解析通知
transaction := new(Transaction)
notifyReq, err := h.handler.ParseNotifyRequest(context.Background(), c.Request, transaction)
if err != nil {
fmt.Printf("[Notify] 解析通知失败: %v\n", err)
c.JSON(http.StatusUnauthorized, gin.H{"code": "FAIL", "message": "验签未通过,或者解密失败"})
return
}
// 记录日志
fmt.Printf("[Notify] 收到支付通知: EventType=%s, OrderID=%s\n",
notifyReq.EventType, transaction.OutTradeNo)
// 检查事件类型
if notifyReq.EventType != "TRANSACTION.SUCCESS" {
c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "非支付成功通知"})
return
}
// 异步处理业务逻辑
go func() {
processPaymentSuccess(*transaction)
}()
// 返回成功响应
c.Status(http.StatusNoContent)
}
// Transaction 支付通知中的订单信息
type Transaction struct {
AppID string `json:"appid"`
MchID string `json:"mchid"`
OutTradeNo string `json:"out_trade_no"`
TransactionID string `json:"transaction_id"`
TradeType string `json:"trade_type"`
TradeState string `json:"trade_state"`
TradeStateDesc string `json:"trade_state_desc"`
BankType string `json:"bank_type"`
Attach string `json:"attach"`
SuccessTime string `json:"success_time"`
Payer struct {
OpenID string `json:"openid"`
} `json:"payer"`
Amount struct {
Total int `json:"total"`
PayerTotal int `json:"payer_total"`
Currency string `json:"currency"`
PayerCurrency string `json:"payer_currency"`
} `json:"amount"`
}
// processPaymentSuccess 处理支付成功业务逻辑
func processPaymentSuccess(result Transaction) {
fmt.Printf("[Process] 处理支付成功订单: %s, 微信支付订单号: %s, 支付金额: %d分\n",
result.OutTradeNo, result.TransactionID, result.Amount.Total)
// TODO: 实现订单状态更新、库存扣减、发货通知等业务逻辑
// 注意:需要处理重复通知的情况(幂等性)
}
4.4 阶段四:订单查询与关单
根据 微信支付订单号查询订单 和 商户订单号查询订单 文档,商户可以主动查询订单状态。
4.4.1 查询订单API
通过微信支付订单号查询:
- GET
/v3/pay/transactions/id/{transaction_id}?mchid={mchid}
通过商户订单号查询:
- GET
/v3/pay/transactions/out-trade-no/{out_trade_no}?mchid={mchid}
订单状态说明:
| 状态值 | 说明 |
|---|---|
| SUCCESS | 支付成功 |
| REFUND | 转入退款 |
| NOTPAY | 未支付 |
| CLOSED | 已关闭 |
| REVOKED | 已撤销(仅付款码支付) |
| USERPAYING | 用户支付中(仅付款码支付) |
| PAYERROR | 支付失败(仅付款码支付) |
4.4.2 关闭订单API
根据 关闭订单文档 ,对于未支付的订单,商户可以调用关单接口:
- POST
/v3/pay/transactions/out-trade-no/{out_trade_no}/close - 请求体:
{"mchid": "1230000109"}
关单场景:
- 用户主动取消订单
- 订单超时未支付
- 订单金额或信息需要修改
4.4.3 Go代码实现
在 service/wechat_pay.go 中添加关单方法:
// CloseOrder 关闭订单
func (s *WechatPayService) CloseOrder(outTradeNo string) error {
svc := native.NativeApiService{Client: s.client}
result, err := svc.CloseOrder(context.Background(), native.CloseOrderRequest{
OutTradeNo: core.String(outTradeNo),
Mchid: core.String(s.config.MchID),
})
if err != nil {
return fmt.Errorf("关闭订单失败: %v, result: %+v", err, result)
}
return nil
}
五、退款功能接入
根据 退款申请文档 ,交易完成后一年内可申请退款。
5.1 退款申请API
- POST
/v3/refund/domestic/refunds
重要限制:
- 一笔订单最多支持50次部分退款
- 申请退款接口调用频率限制:150QPS(成功),6QPS(失败)
- 一个月前的订单退款需要降低频率
请求参数:
| 参数 | 必填 | 说明 |
|---|---|---|
transaction_id | 二选一 | 微信支付订单号 |
out_trade_no | 二选一 | 商户订单号 |
out_refund_no | 是 | 商户退款单号(64字符内,唯一) |
reason | 否 | 退款原因(80字符内) |
notify_url | 否 | 退款结果回调地址 |
amount | 是 | 退款金额信息 |
5.2 退款查询与异常处理
查询退款:
- GET
/v3/refund/domestic/refunds/{out_refund_no}
发起异常退款(当退款到银行卡失败时):
- POST
/v3/refund/domestic/refunds/{refund_id}/apply-abnormal-refund - 支持退款至用户其他银行卡或商户银行账户
5.3 Go代码实现
// ApplyRefund 申请退款
func (s *WechatPayService) ApplyRefund(outTradeNo, outRefundNo string, refundFee int64, reason string) (*refund.Refund, error) {
svc := refund.RefundsApiService{Client: s.client}
resp, result, err := svc.Create(context.Background(), refund.CreateRequest{
OutTradeNo: core.String(outTradeNo),
OutRefundNo: core.String(outRefundNo),
Reason: core.String(reason),
Amount: &refund.AmountReq{
Refund: core.Int64(refundFee),
Total: core.Int64(refundFee), // 原订单金额,这里简化处理
Currency: core.String("CNY"),
},
})
if err != nil {
return nil, fmt.Errorf("申请退款失败: %v, result: %+v", err, result)
}
return resp, nil
}
六、完整MVP Demo代码
由于文章篇幅不易太长,完整本地可运行的demo项目代码我已打包上传, 需要完整项目源码的可以关注我的公众号「 人言兑 」,私信发送「
native demo」即可获取。
6.1 主程序入口 main.go
package main
import (
"log"
"os"
"wechatpay-native-demo/config"
"wechatpay-native-demo/handler"
"wechatpay-native-demo/service"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
)
func main() {
// 加载环境变量
if err := godotenv.Load(); err != nil {
log.Println("未找到.env文件,使用系统环境变量")
}
// 加载配置
cfg := config.LoadConfig()
// 验证必要配置
if cfg.AppID == "" || cfg.MchID == "" || cfg.APIv3Key == "" || cfg.PublicKeyID == "" {
log.Fatal("缺少必要的微信支付配置,请检查环境变量")
}
// 创建微信支付服务
wechatPay, err := service.NewWechatPayService(cfg)
if err != nil {
log.Fatalf("初始化微信支付服务失败: %v", err)
}
// 创建处理器
paymentHandler := handler.NewPaymentHandler(wechatPay)
notifyHandler, err := handler.NewNotifyHandler(cfg.APIv3Key, cfg.PublicKeyID, cfg.PublicKeyPath)
if err != nil {
log.Fatalf("初始化通知处理器失败: %v", err)
}
// 创建Gin路由
r := gin.Default()
// 加载HTML模板
r.LoadHTMLGlob("templates/*")
// 静态文件服务
r.Static("/static", "./static")
// 页面路由
r.GET("/", func(c *gin.Context) {
c.HTML(200, "index.html", nil)
})
r.GET("/pay", func(c *gin.Context) {
c.HTML(200, "pay.html", nil)
})
r.GET("/success", func(c *gin.Context) {
c.HTML(200, "success.html", gin.H{
"order_id": c.Query("order_id"),
})
})
// API路由
api := r.Group("/api")
{
api.POST("/order", paymentHandler.CreateOrder)
api.GET("/order/:order_id", paymentHandler.QueryOrder)
api.POST("/notify", notifyHandler.PaymentNotify)
}
// 启动服务器
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("服务器启动在 http://localhost:%s", port)
if err := r.Run(":" + port); err != nil {
log.Fatalf("启动服务器失败: %v", err)
}
}
6.2 商品展示页面 templates/index.html
<body>
<div class="container">
<h1>微信支付 Native支付 Demo</h1>
<div class="products">
<div class="product-card">
<div class="product-image">📱</div>
<div class="product-info">
<div class="product-name">高级会员 1个月</div>
<div class="product-desc">
解锁全部高级功能,享受VIP专属服务,无限制使用所有特性。
</div>
<div class="product-footer">
<div class="price"><span>¥</span>29.90</div>
<button class="buy-btn" onclick="buy('高级会员 1个月', 2990)">
立即购买
</button>
</div>
</div>
</div>
<div class="product-card">
<div class="product-image">💎</div>
<div class="product-info">
<div class="product-name">高级会员 1年</div>
<div class="product-desc">
年度会员更优惠,相当于每月仅需19.9元,节省33%。
</div>
<div class="product-footer">
<div class="price"><span>¥</span>199.00</div>
<button class="buy-btn" onclick="buy('高级会员 1年', 19900)">
立即购买
</button>
</div>
</div>
</div>
<div class="product-card">
<div class="product-image">🎁</div>
<div class="product-info">
<div class="product-name">测试商品 0.01元</div>
<div class="product-desc">
用于测试支付流程,实际支付0.01元,支付后可申请退款。
</div>
<div class="product-footer">
<div class="price"><span>¥</span>0.01</div>
<button class="buy-btn" onclick="buy('测试商品', 1)">
立即购买
</button>
</div>
</div>
</div>
</div>
</div>
<script>
function buy(productName, amount) {
// 跳转到支付页面
window.location.href =
"/pay?product=" + encodeURIComponent(productName) + "&amount=" + amount;
}
</script>
</body>
6.3 环境变量配置 .env
# 微信支付配置
# 公众账号ID(服务号/小程序/移动应用的AppID)
WXPAY_APPID=wx1234567890abcdef
# 商户号
WXPAY_MCHID=1234567890
# APIv3密钥(32位字符,在商户平台-API安全设置)
WXPAY_APIV3_KEY=YourAPIv3KeyHere
# 商户证书序列号
# 获取命令:openssl x509 -in apiclient_cert.pem -noout -serial | cut -d= -f2 | tr '[:upper:]' '[:lower:]'
WXPAY_CERT_SERIAL_NO=1234567890ABCDEF1234567890ABCDEF
# 商户私钥路径
WXPAY_PRIVATE_KEY_PATH=./cert/apiclient_key.pem
# 微信支付公钥模式(2024年后新商户默认使用)
# 公钥ID在商户平台获取,格式如:PUB_KEY_ID_xxxxxxxxxxxxxxxx
# 获取路径:商户平台 → API安全 → 微信支付公钥 → 查看公钥ID
WXPAY_PUBLIC_KEY_ID=PUB_KEY_ID_xxxxxxxxxxxxxxxx
WXPAY_PUBLIC_KEY_PATH=./cert/pub_key.pem
# 在商户号上配置的Native支付回调地址(必须外网可访问)
WXPAY_NOTIFY_URL=https://yourdomain.com/api/notify
# 服务器配置
PORT=8080
七、常见问题与解决方案
根据 官方Native支付常见问题文档 ,整理以下常见问题:
7.1 前端问题
Q1:调用Native支付统一下单成功,但扫描二维码支付时返回"系统繁忙,请稍后再试"
解决方案:
- 检查传入的参数是否有空格或特殊字符
- 检查传入的参数值是否为null,不需要的参数可以不传递,避免以null形式传入
- 确认APPID与商户号已正确绑定
Q2:长按识别Native支付二维码返回:“该商户暂不支持通过长按识别二维码完成支付”
解决方案:
- 这是微信支付的限制,Native支付不支持长按识别或从相册识别二维码
- 必须引导用户使用微信"扫一扫"功能直接扫描屏幕上的二维码
Q3:用户支付时报错:“商家订单信息有误,请重新下单支付”
解决方案:
- 同一笔订单不允许使用不同的微信号扫码调起支付
- 如果用户A扫描了二维码但未支付,用户B再扫描同一个二维码会报此错误
- 需要重新下单生成新的二维码
Q4:Native支付二维码能否实现1分钟刷新?
解决方案:
- 可以通过定时原参数重入下单的方式实现刷新
- 重入下单返回的
code_url会改变,同时旧的code_url将会过期失效 - 前端可以设置定时器,每1分钟调用创建订单接口获取新的二维码
7.2 回调问题
Q1:收不到支付成功回调通知
排查步骤:
- 检查
notify_url是否为外网可访问的URL,不能是localhost或内网地址 - 检查商户平台配置的回调地址是否正确
- 检查服务器防火墙是否放行了微信支付的回调IP段
- 查看服务器日志,确认是否收到请求但处理失败
- 检查是否正确返回了HTTP 200/204状态码
Q2:回调通知重复发送
解决方案:
- 这是正常现象,如果商户未及时响应或网络超时,微信会重复发送通知
- 实现幂等性处理:根据通知ID或订单号去重,避免重复处理同一笔订单
7.3 服务端问题
SIGN_ERROR 签名错误
StatusCode: 401 Code: "SIGN_ERROR"
Message: 签名错误
常见原因及修复:未设置证书序列号或证书序列号错误。
使用以下命令获取微信支付商户平台证书序列号:
openssl x509 -in ./cert/apiclient_cert.pem -noout -serial | cut -d= -f2 | tr '[:upper:]' '[:lower:]'
APPID_MCHID_NOT_MATCH appid和mch_id不匹配
StatusCode: 400
Code: "APPID_MCHID_NOT_MATCH"
Message: appid和mch_id不匹配,请检查后再试
检查appid和mch_id是否配置正确,或者未在商户平台-产品中心进行关联appid
NO_AUTH 此商家的收款功能已被限制,暂无法支付
StatusCode: 403
Code: "NO_AUTH"
Message: 此商家的收款功能已被限制,暂无法支付。商家可登录微信商户平台/微信支付商家助手小程序/经营账户页面查看原因和解决方案。
登录商户平台查看「消息中心」看看有没有「业务限制通知」,按照里面的说明操作接触相应限制即可。
RESOURCE_NOT_EXISTS 无可用的平台证书
StatusCode: 404 Code: "RESOURCE_NOT_EXISTS"
Message: 无可用的平台证书,请在商户平台-API安全申请使用微信支付公钥。可查看指引https://pay.weixin.qq.com/doc/v3/merchant/4012153196
这个错误说明微信支付现在要求使用微信支付公钥而不是传统的平台证书。根据错误信息中的指引 https://pay.weixin.qq.com/doc/v3/merchant/4012153196 ,最新商户号需要使用公钥方式。
7.4 其他问题
Q1:Native支付和其他基础支付的区别
说明:
- Native支付与其他基础支付(JSAPI、APP、H5等)除了下单接口和调起支付方式不同外,其余接口(查询、关闭、退款等)都是相同的
- 可以根据具体场景选择合适的支付方式
Q2:未支付的订单在微信里能看到吗?
说明:
- 未支付的订单在微信客户端不会显示
- 用户可通过商户系统找到待支付订单主动发起支付操作
八、安全注意事项
- 密钥安全:
- APIv3密钥和商户私钥必须妥善保管,不要提交到代码仓库
- 生产环境建议使用密钥管理服务(KMS)
- 签名验证:
- 必须验证微信支付的回调签名,防止伪造通知
- 定期更新微信支付平台证书
- 数据加密:
- 敏感字段(如用户姓名、银行卡号)需要使用微信支付公钥加密
- 遵循最小权限原则,只收集必要的用户信息
- 幂等性处理:
- 创建订单、处理回调等操作需要实现幂等性
- 防止网络重试导致重复处理
- 日志记录:
- 记录所有API请求的Request-ID,便于问题排查
- 不要记录敏感信息(如密钥、用户支付密码等)
九、相关文档链接
- Native支付产品介绍
- 快速开始
- 开发指引
- Native下单API
- 微信支付订单号查询订单
- 商户订单号查询订单
- 关闭订单
- 支付成功回调通知
- 退款申请
- 查询单笔退款
- 发起异常退款
- 退款结果回调通知
- 常见问题
- 管理商户号绑定的APPID账号
- 申请Native支付权限指引
十、总结
本文详细介绍了微信支付Native支付的完整接入流程,包括:
- 产品理解:Native支付适用于PC网站扫码支付场景,用户通过微信扫一扫完成支付
- 准备工作:注册商户号、开通Native支付权限、获取开发参数、绑定APPID
- 开发流程:商户下单生成二维码 -> 前端展示二维码 -> 用户扫码支付 -> 处理支付回调 -> 订单查询与关单
- 核心代码:提供了完整的Go语言实现,包括下单、回调处理、订单查询等功能
- MVP Demo:包含商品展示页面、支付页面、完整的交互逻辑
- 注意事项:强调了二维码有效期、回调处理、幂等性等关键点
通过本文的指引,你应该能够独立完成Native支付的接入开发。如果在接入过程中遇到问题,建议参考官方文档或联系微信支付技术支持。
本教程基于微信支付官方文档整理,接口参数和行为可能随官方更新而变化,请以 微信支付官方文档 为准。








