
作为一名独立开发者,接入微信支付可能是你项目变现的关键一步。但面对复杂的文档、各种证书、密钥和回调机制,很多人会在第一步就卡住。
本教程基于微信支付 APIv3 和官方 Go SDK,从零开始带你完成 JSAPI 支付的完整接入。我们将使用最新的微信支付公钥模式(适用于 2023 年后新申请的商户号),相比传统的平台证书模式更加简洁。
你将学到:
- JSAPI 支付的完整业务流程
- 如何获取用户 OpenID(微信授权)
- 如何创建订单、调起支付、处理回调
- 如何使用微信支付公钥进行验签
- 完整的可运行代码示例
前置要求:
- 基础的 Go 语言知识
- 已注册微信支付商户号(支持公钥模式)
- 已注册微信公众号/服务号(已认证)
- 一个外网可访问的域名
在开始之前,请确保已完成以下准备工作:
- 了解哪种微信支付产品更适合你的业务场景
- 如何注册微信商户号
- 商户号与APPID绑定
- 准备Go 1.21+开发环境(可选)
👉 点击查看《微信支付系列教程》文章目录
在《微信支付系列教程》系列文章中,我们将详细介绍如何从0到1在自己的网站中接入微信支付。以下是该系列文章的全部内容:
微信支付JSAPI支付产品概述
什么是 JSAPI 支付?
JSAPI 支付是微信支付为微信内置浏览器提供的支付能力。用户在微信客户端内访问商户的网页,选购商品后可直接在微信内完成支付,无需跳出微信。
根据官方文档定义:
JSAPI支付,提供商户在微信客户端内部浏览器网页中使用微信支付收款的能力。
核心特点
| 特性 | 说明 |
|---|---|
| 环境限制 | 必须在微信内置浏览器中使用 |
| 用户标识 | 需要获取用户的 OpenID |
| 支付流程 | 用户点击支付 → 调起微信收银台 → 输入密码 → 支付完成 |
| 回调机制 | 支持前端回调 + 后端异步通知双重确认 |
JSAPI支付与传统 H5 支付的区别
| 对比项 | JSAPI 支付 | H5 支付 |
|---|---|---|
| 使用环境 | 微信内置浏览器 | 外部浏览器(Safari/Chrome等) |
| 用户标识 | 需要 OpenID | 不需要 |
| 体验流畅度 | 优(无需跳出) | 一般(需跳转微信) |
| 接入复杂度 | 一般(需授权) | 较高 (但需要先接入Native支付后单独申请开通) |
| 费率 | 相同 | 相同 |
在微信以外的浏览器下单会出现一个你可能比较熟悉的页面提示:
JSAPI扫码支付与Native扫码支付的区别
- JSAPI 扫的二维码是商品展示网页的链接,进入的是商户 H5 页面
- Native 扫的是支付协议二维码,直接进入微信原生收银台
PC 网站推荐用 Native 支付,但也可以用 JSAPI 支付:
| PC 网站方案 | 实现方式 | 用户体验 |
|---|---|---|
| 推荐:Native 支付 | 展示支付二维码,用户微信扫码 | 扫码后直接支付,体验好 |
| 可选:JSAPI 支付 | 展示网页二维码,用户微信扫码打开网页 | 多一步操作,但可以展示更多信息 |
适用场景与业务模式
根据官方文档,JSAPI 支付主要适用于以下场景:
场景一:公众号商城
用户通过公众号菜单或推文进入商户网页,选购商品后支付。
流程:
- 用户关注公众号,点击菜单进入商城
- 浏览商品,加入购物车
- 进入结算页,确认订单
- 调起微信支付,输入密码
- 支付成功,返回商户页面查看订单
场景二:扫码支付
用户扫描商户二维码,在微信中打开网页完成支付。
流程:
- 用户扫描商户收款二维码
- 微信自动打开商户页面
- 输入金额或选择商品
- 调起支付完成付款
场景三:社交分享
用户通过好友分享的链接进入商品页,直接购买。
官方文档提示:
用户可以通过公众号、扫一扫、分享链接等方式在微信客户端内部浏览器中打开商户页面,完成支付。
微信支付JSAPI支付接入前准备
第一步:开通 JSAPI 支付产品
- 登录 微信支付商户平台
- 进入"产品中心" → “我的产品”
- 找到"JSAPI 支付",点击申请开通
- 按提示完成开通流程
第二步:绑定 AppID
JSAPI 支付必须将商户号与公众号/小程序 AppID 绑定:
- 商户平台 → 产品中心 → AppID 账号管理
- 点击"关联 AppID"
- 输入公众号的 AppID(格式:wx 开头,18位字符)
- 前往微信公众平台确认绑定
相关阅读推荐: 商户号绑定APPID操作指南
第三步:配置支付授权目录
只有配置了授权目录的网页才能调起支付:
- 商户平台 → 产品中心 → JSAPI 支付 → 支付授权目录
- 点击"添加"
- 填写你的域名(如
https://your-domain.com/) - 注意:目录必须以
/结尾,且是完整的 URL
所有使用JS API方式发起支付请求的链接地址,都必须在当前页面所配置的支付授权目录之下。下单前需要调用【 网页授权获取用户信息 】接口获取到用户的Openid
第四步:获取开发必要参数
在开始开发前,你需要准备以下参数:
| 参数名称 | 参数说明 | 获取方式 |
|---|---|---|
| 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 - 设置后立即生效,请妥善保存
详细参数说明可参考: 普通商户模式开发必要参数说明 。
第五步:配置微信公众号服务号
- 登录 微信公众平台
- 设置与开发 → 账号设置 → 功能设置 → JS接口安全域名 → 设置(绑定父级域名即可,其子域名也是可用的)
- 设置与开发 → 账号设置 → 功能设置 → 网页授权域名 → 设置(父级域名不可替代子域名)
设置「JS接口安全域名」后,开发者可在该域名下调用微信开放的JS接口,可以设置5个,添加父级域名后子域名自动可用。
而「网页授权域名」是用户在授权给本账号时的重定向地址的域名,微信会将授权数据传给一个回调地址,该地址的域名是需要区分子域名的,最多只能配2个。
配置域名时都需要验证域名所有者身份,具体验证方法如下:
接入JSAPI支付关于域名相关的配置操作,这里再次总结一下:
- 商户号后台配置「支付授权目录(需要区分子域名)」
- 服务号后台配置「JS接口安全域名(不需要区分子域名,用父级域名即可)」和「网页授权域名(需要区分子域名)」
开发环境搭建
项目结构
wechatpay-jsapi-demo/
├── .env # 环境变量配置(不提交Git)
├── .env.example # 配置示例
├── go.mod # Go模块
├── main.go # 入口文件
├── config/
│ └── config.go # 配置加载
├── handlers/
│ ├── auth.go # 微信授权(获取OpenID)
│ └── payment.go # 支付相关接口
├── templates/
│ ├── index.html # 商品列表页
│ ├── pay.html # 支付确认页
│ └── success.html # 支付成功页
└── certs/
├── apiclient_key.pem # 商户私钥
└── wechatpay_pub_key.pem # 微信支付公钥
安装依赖
创建 go.mod:
module wechatpay-jsapi-demo
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/joho/godotenv v1.5.1
github.com/wechatpay-apiv3/wechatpay-go v0.2.21
)
安装依赖:
go mod tidy
环境变量配置
创建 .env 文件:
# 服务器配置
SERVER_PORT=8080
BASE_URL=https://your-domain.com
# 微信支付配置
MCH_ID=1900000000
MCH_API_V3_KEY=YourAPIv3KeyHere
MCH_CERT_SERIAL_NO=YourCertSerialNo
MCH_PRIVATE_KEY_PATH=./certs/apiclient_key.pem
# 微信支付公钥配置(新商户必填)
WECHAT_PAY_PUBLIC_KEY_ID=PUB_KEY_ID_xxxxxxxx
WECHAT_PAY_PUBLIC_KEY_PATH=./certs/wechatpay_pub_key.pem
# 公众号配置
APP_ID=wx0000000000000000
APP_SECRET=YourAppSecret
# 支付配置
NOTIFY_URL=https://your-domain.com/api/pay/notify
ORDER_EXPIRE_MINUTES=30
微信支付 JSAPI 支付核心业务流程详解
根据官方文档,JSAPI 支付的整体业务流程分为五个阶段:
阶段一:商户下单
商户后端调用 JSAPI 下单接口,获取预支付 ID(prepay_id)。
关键参数说明:
| 参数 | 说明 | 注意事项 |
|---|---|---|
openid | 用户标识 | 必须通过网页授权获取 |
out_trade_no | 商户订单号 | 同一商户号下唯一,6-32字符 |
description | 商品描述 | 用户账单可见,127字符以内 |
amount.total | 订单金额 | 单位为分,不能有小数 |
time_expire | 支付结束时间 | RFC3339格式,如2025-02-14T10:00:00+08:00 |
notify_url | 回调地址 | 必须外网可访问,HTTPS |
prepay_id 有效期: 2小时,过期需重新下单。
阶段二:商户调起支付
前端通过微信内置对象 WeixinJSBridge 调起支付。
调起支付参数:
| 参数 | 说明 |
|---|---|
appId | 公众号 AppID |
timeStamp | 当前时间戳(秒级) |
nonceStr | 随机字符串 |
package | prepay_id=xxx |
signType | 固定值 RSA |
paySign | 签名值 |
商户调起支付前,请确保已在商户平台配置好 JSAPI 支付授权目录(只有配置了 JSAPI 支付授权目录的网页才能调起支付)。
阶段三:用户支付
用户在微信收银台完成支付或取消支付。
前端回调结果:
| err_msg | 含义 | 处理方式 |
|---|---|---|
get_brand_wcpay_request:ok | 支付成功 | 调后端查单确认 |
get_brand_wcpay_request:cancel | 用户取消 | 展示取消页面 |
get_brand_wcpay_request:fail | 支付失败 | 展示失败原因 |
重要提示:
前端回调并不保证它绝对可靠,不可只依赖前端回调判断订单支付状态,订单状态需以后端查询订单和支付成功回调通知为准。
阶段四:支付结果通知
微信支付异步通知商户支付结果。
通知特点:
- 异步 POST 请求
- 需要验签确保来源可信
- 需要返回
{"code": "SUCCESS"}表示处理成功 - 同一笔订单可能通知多次(需幂等处理)
阶段五:订单状态流转
根据官方文档,订单状态流转如下:
生成订单 → NOTPAY(未支付)
↓
支付成功 → SUCCESS(支付成功)→ 可申请退款 → REFUND(转入退款)
↓
支付失败/超时 → CLOSED(已关闭)
终态说明:
SUCCESS:支付成功,可退款CLOSED:已关闭,不可再支付REFUND:已退款
JSAPI支付完整代码实战(Golang实现)
由于文章篇幅不易太长,完整本地可运行的demo项目代码我已打包上传,
需要完整项目源码的可以关注我的公众号「 人言兑 」,私信发送「jsapi demo」即可获取。
1. 配置加载(config/config.go)
package config
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/joho/godotenv"
)
type Config struct {
ServerPort string
BaseURL string
MchID string
MchAPIv3Key string
MchCertSerialNo string
MchPrivateKeyPath string
WechatPayPublicKeyID string
WechatPayPublicKeyPath string
AppID string
AppSecret string
NotifyURL string
OrderExpireMinutes int
}
var GlobalConfig *Config
func Load() *Config {
if err := godotenv.Load(); err != nil {
log.Println("Warning: .env file not found")
}
config := &Config{
ServerPort: getEnv("SERVER_PORT", "8080"),
BaseURL: getEnv("BASE_URL", ""),
MchID: getEnv("MCH_ID", ""),
MchAPIv3Key: getEnv("MCH_API_V3_KEY", ""),
MchCertSerialNo: getEnv("MCH_CERT_SERIAL_NO", ""),
MchPrivateKeyPath: getEnv("MCH_PRIVATE_KEY_PATH", "./certs/apiclient_key.pem"),
WechatPayPublicKeyID: getEnv("WECHAT_PAY_PUBLIC_KEY_ID", ""),
WechatPayPublicKeyPath: getEnv("WECHAT_PAY_PUBLIC_KEY_PATH", "./certs/wechatpay_pub_key.pem"),
AppID: getEnv("APP_ID", ""),
AppSecret: getEnv("APP_SECRET", ""),
NotifyURL: getEnv("NOTIFY_URL", ""),
OrderExpireMinutes: getEnvAsInt("ORDER_EXPIRE_MINUTES", 30),
}
if err := config.validate(); err != nil {
log.Fatalf("Config validation failed: %v", err)
}
config.MchPrivateKeyPath = toAbsPath(config.MchPrivateKeyPath)
config.WechatPayPublicKeyPath = toAbsPath(config.WechatPayPublicKeyPath)
GlobalConfig = config
return config
}
func (c *Config) validate() error {
required := []string{"MCH_ID", "MCH_API_V3_KEY", "MCH_CERT_SERIAL_NO",
"WECHAT_PAY_PUBLIC_KEY_ID", "WECHAT_PAY_PUBLIC_KEY_PATH",
"APP_ID", "APP_SECRET", "NOTIFY_URL", "BASE_URL"}
for _, field := range required {
if getEnv(field, "") == "" {
return fmt.Errorf("%s is required", field)
}
}
return nil
}
func toAbsPath(path string) string {
if !filepath.IsAbs(path) {
if abs, err := filepath.Abs(path); err == nil {
return abs
}
}
return path
}
func getEnv(key, defaultVal string) string {
if v := os.Getenv(key); v != "" {
return v
}
return defaultVal
}
func getEnvAsInt(key string, defaultVal int) int {
if v := os.Getenv(key); v != "" {
var i int
if _, err := fmt.Sscanf(v, "%d", &i); err == nil {
return i
}
}
return defaultVal
}
2. 微信授权获取用户openid(handlers/auth.go)
package handlers
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"wechatpay-jsapi-demo/config"
"github.com/gin-gonic/gin"
)
type AuthHandler struct {
cfg *config.Config
}
func NewAuthHandler() *AuthHandler {
return &AuthHandler{cfg: config.GlobalConfig}
}
// Auth 发起微信授权
func (h *AuthHandler) Auth(c *gin.Context) {
redirectURI := url.QueryEscape(h.cfg.BaseURL + "/auth/callback")
state := "STATE123" // 生产环境应使用随机数并验证
authURL := fmt.Sprintf(
"https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=%s#wechat_redirect",
h.cfg.AppID, redirectURI, state,
)
/*
目前网页授权有两个 scope,分别是:snsapi_base 和 snsapi_userinfo,解释如下:
- snsapi_base:用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。(不会弹出信息确认框,仅能获取用户 openid 等信息,无法获取昵称头像)
- snsapi_userinfo:用来获取用户的基本信息的。但这种授权需要用户手动同意。(由于用户同意过,所以无须依赖用户关注服务号,就可在授权后获取该用户的基本信息)
*/
c.Redirect(http.StatusFound, authURL)
}
// Callback 授权回调
func (h *AuthHandler) Callback(c *gin.Context) {
code := c.Query("code")
if code == "" {
c.String(http.StatusBadRequest, "Authorization failed")
return
}
openID, err := h.getOpenID(code)
if err != nil {
c.String(http.StatusInternalServerError, "Failed to get openid")
return
}
// 存储到 cookie
c.SetCookie("openid", openID, 3600, "/", "", false, true)
redirect := c.DefaultQuery("redirect", "/pay")
c.Redirect(http.StatusFound, redirect)
}
func (h *AuthHandler) getOpenID(code string) (string, error) {
tokenURL := fmt.Sprintf(
"https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
h.cfg.AppID, h.cfg.AppSecret, code,
)
resp, err := http.Get(tokenURL)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result struct {
OpenID string `json:"openid"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
if err := json.Unmarshal(body, &result); err != nil {
return "", err
}
if result.ErrCode != 0 {
return "", fmt.Errorf("wechat error: %s", result.ErrMsg)
}
return result.OpenID, nil
}
// GetOpenID 从 cookie 获取
func GetOpenID(c *gin.Context) (string, error) {
openid, err := c.Cookie("openid")
if err != nil {
return "", fmt.Errorf("not authorized")
}
return openid, nil
}
3. 支付处理(handlers/payment.go)
package handlers
import (
"context"
"crypto/rsa"
"fmt"
"log"
"net/http"
"time"
"wechatpay-jsapi-demo/config"
"github.com/gin-gonic/gin"
"github.com/wechatpay-apiv3/wechatpay-go/core"
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi"
)
type PaymentHandler struct {
client *core.Client
cfg *config.Config
svc jsapi.JsapiApiService
wechatPayPublicKey *rsa.PublicKey
}
func NewPaymentHandler(client *core.Client, pubKey *rsa.PublicKey) *PaymentHandler {
return &PaymentHandler{
client: client,
cfg: config.GlobalConfig,
svc: jsapi.JsapiApiService{Client: client},
wechatPayPublicKey: pubKey,
}
}
// Index 商品页
func (h *PaymentHandler) Index(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
}
// PayPage 支付页
func (h *PaymentHandler) PayPage(c *gin.Context) {
openid, err := GetOpenID(c)
if err != nil {
redirect := url.QueryEscape("/pay")
c.Redirect(http.StatusFound, "/auth?redirect="+redirect)
return
}
c.HTML(http.StatusOK, "pay.html", gin.H{"openid": openid})
}
// CreateOrder 创建订单
func (h *PaymentHandler) CreateOrder(c *gin.Context) {
var req struct {
ProductID string `json:"product_id"`
ProductName string `json:"product_name"`
Price float64 `json:"price"` // 单位:元(实际业务中最好不要使用浮点数避免精度问题,这里只是方便演示)
OpenID string `json:"openid"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 金额转换:元 → 分
amount := int64(req.Price * 100)
outTradeNo := generateOrderNo()
expireTime := time.Now().Add(time.Duration(h.cfg.OrderExpireMinutes) * time.Minute)
resp, _, err := h.svc.PrepayWithRequestPayment(context.Background(),
jsapi.PrepayRequest{
Appid: core.String(h.cfg.AppID),
Mchid: core.String(h.cfg.MchID),
Description: core.String(req.ProductName),
OutTradeNo: core.String(outTradeNo),
Attach: core.String(req.ProductID),
NotifyUrl: core.String(h.cfg.NotifyURL),
Amount: &jsapi.Amount{
Total: core.Int64(amount),
},
Payer: &jsapi.Payer{
Openid: core.String(req.OpenID),
},
TimeExpire: core.Time(expireTime),
},
)
if err != nil {
log.Printf("Create order failed: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "create order failed"})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"out_trade_no": outTradeNo,
"appId": resp.Appid,
"timeStamp": resp.TimeStamp,
"nonceStr": resp.NonceStr,
"package": resp.Package,
"signType": resp.SignType,
"paySign": resp.PaySign,
})
}
// Notify 支付回调
func (h *PaymentHandler) Notify(c *gin.Context) {
handler, err := notify.NewRSANotifyHandler(
h.cfg.MchAPIv3Key,
verifiers.NewSHA256WithRSAPubkeyVerifier(h.cfg.WechatPayPublicKeyID, *h.wechatPayPublicKey),
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": "FAIL"})
return
}
transaction := new(payments.Transaction)
if _, err := handler.ParseNotifyRequest(context.Background(), c.Request, transaction); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"code": "FAIL"})
return
}
// 处理业务逻辑
if transaction.TradeState != nil && *transaction.TradeState == "SUCCESS" {
log.Printf("Payment success: %s, amount: %d",
*transaction.OutTradeNo, *transaction.Amount.Total)
// TODO: 更新订单状态(注意幂等性)
}
c.JSON(http.StatusOK, gin.H{"code": "SUCCESS"})
}
func generateOrderNo() string {
return fmt.Sprintf("D%s%06d",
time.Now().Format("20060102150405"),
time.Now().Nanosecond()%1000000)
}
4. 主入口(main.go)
package main
import (
"context"
"fmt"
"log"
"wechatpay-jsapi-demo/config"
"wechatpay-jsapi-demo/handlers"
"github.com/gin-gonic/gin"
"github.com/wechatpay-apiv3/wechatpay-go/core"
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
"github.com/wechatpay-apiv3/wechatpay-go/utils"
)
func main() {
cfg := config.Load()
// 加载证书
mchPrivateKey, err := utils.LoadPrivateKeyWithPath(cfg.MchPrivateKeyPath)
if err != nil {
log.Fatalf("load private key failed: %v", err)
}
wechatPayPublicKey, err := utils.LoadPublicKeyWithPath(cfg.WechatPayPublicKeyPath)
if err != nil {
log.Fatalf("load public key failed: %v", err)
}
// 使用公钥模式初始化客户端
opts := []core.ClientOption{
option.WithWechatPayPublicKeyAuthCipher(
cfg.MchID,
cfg.MchCertSerialNo,
mchPrivateKey,
cfg.WechatPayPublicKeyID,
wechatPayPublicKey,
),
}
client, err := core.NewClient(context.Background(), opts...)
if err != nil {
log.Fatalf("init client failed: %v", err)
}
// 设置路由
r := gin.Default()
r.LoadHTMLGlob("templates/*")
paymentHandler := handlers.NewPaymentHandler(client, wechatPayPublicKey)
authHandler := handlers.NewAuthHandler()
r.GET("/", paymentHandler.Index)
r.GET("/pay", paymentHandler.PayPage)
r.GET("/auth", authHandler.Auth)
r.GET("/auth/callback", authHandler.Callback)
r.POST("/api/pay/create", paymentHandler.CreateOrder)
r.POST("/api/pay/notify", paymentHandler.Notify)
r.Run(":" + cfg.ServerPort)
}
5. 前端页面
templates/index.html(商品列表):
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>商品列表</title>
<style>
body {
font-family: -apple-system, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.product {
border: 1px solid #ddd;
padding: 15px;
margin: 10px 0;
border-radius: 8px;
cursor: pointer;
}
.product:hover {
border-color: #07c160;
}
.price {
color: #ff6b6b;
font-size: 20px;
font-weight: bold;
}
</style>
</head>
<body>
<h2>选择商品</h2>
<div class="product" onclick="buy('P001', '测试商品-可乐', 0.01)">
<h3>测试商品-可乐</h3>
<p class="price">¥0.01</p>
</div>
<div class="product" onclick="buy('P002', '测试商品-雪碧', 0.01)">
<h3>测试商品-雪碧</h3>
<p class="price">¥0.01</p>
</div>
<script>
function buy(id, name, price) {
// price 单位:元
sessionStorage.setItem("product", JSON.stringify({ id, name, price }));
location.href = "/pay";
}
</script>
</body>
</html>
templates/pay.html(支付确认):
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>确认支付</title>
<style>
body {
font-family: -apple-system, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
text-align: center;
}
.amount {
font-size: 48px;
color: #333;
margin: 30px 0;
}
.btn {
background: #07c160;
color: white;
padding: 15px 40px;
border: none;
border-radius: 5px;
font-size: 18px;
}
</style>
</head>
<body>
<h2>确认支付</h2>
<div id="productName"></div>
<div class="amount">¥<span id="price">0.00</span></div>
<button class="btn" onclick="pay()">立即支付</button>
<script>
const product = JSON.parse(sessionStorage.getItem("product"));
document.getElementById("productName").textContent = product.name;
document.getElementById("price").textContent = product.price.toFixed(2);
async function pay() {
const openid = "{{.openid}}";
// 金额转换:元 → 分
const amountInCent = Math.round(product.price * 100);
const res = await fetch("/api/pay/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
product_id: product.id,
product_name: product.name,
price: product.price, // 实际业务中应该后端根据商品id查询价格,而不是前端指定价格,这里只是为了方便演示
openid: openid,
}),
});
const data = await res.json();
if (!data.success) {
alert("创建订单失败");
return;
}
// 调起微信支付
WeixinJSBridge.invoke(
"getBrandWCPayRequest",
{
appId: data.appId,
timeStamp: data.timeStamp,
nonceStr: data.nonceStr,
package: data.package,
signType: data.signType,
paySign: data.paySign,
},
function (res) {
if (res.err_msg === "get_brand_wcpay_request:ok") {
alert("支付成功");
location.href = "/";
} else if (res.err_msg === "get_brand_wcpay_request:cancel") {
alert("已取消");
} else {
alert("支付失败: " + res.err_msg);
}
},
);
}
</script>
</body>
</html>
测试与调试
将服务部署到公网,或者使用 ngrok 获取临时公网域名:
测试流程:
1. 访问商品页:在微信中打开 https://your-domain/
2. 选择商品:点击商品进入支付页
3. 授权获取 OpenID:首次访问自动静默授权
4. 确认支付:显示金额 ¥0.01,点击支付
5. 调起收银台:输入支付密码
6. 查看结果:支付成功后返回商户页面
常见问题排查
问题一:redirect_uri 域名与后台配置不一致(错误码 10003)
原因:微信授权回调域名配置不正确
解决:
- 检查微信公众平台 → 接口权限 → 网页授权获取用户基本信息
- 确保配置的域名与
BASE_URL完全一致(不含 https:// 和端口号) - 如果使用 ngrok,每次重启后域名会变化,需要重新配置
问题二:JSAPI 缺少参数或配置错误
排查清单:
- 商户号已开通 JSAPI 支付
- 商户号与 AppID 已绑定
- 已配置 JSAPI 支付授权目录
- 页面在授权目录下访问
- 使用微信内置浏览器
问题三:回调验签失败
原因:使用了错误的验签方式
解决:
- 确认使用的是微信支付公钥验签(
NewSHA256WithRSAPubkeyVerifier) - 检查公钥文件和公钥 ID 是否匹配
安全与生产环境建议
1. 证书与密钥安全
- 永远不要将私钥文件提交到 Git
- 生产环境使用密钥管理服务(如 AWS KMS、阿里云 KMS)
- 定期轮换 APIv3 密钥
2. 回调安全
- 必须验签,确保通知来自微信
- 处理幂等性,同一笔订单的多次通知只处理一次
- 回调处理逻辑尽量简单,异步处理复杂业务
3. 订单安全
- 回调中校验返回金额与订单金额是否一致
- 设置合理的订单超时时间(建议 30 分钟)
- 超时未支付订单及时关闭
4. 日志与监控
- 记录所有支付相关日志
- 设置异常告警(如回调失败、订单状态异常)
- 定期对账,确保每笔订单状态正确
参考文档链接
结语
通过本教程,你应该已经掌握了 JSAPI 支付的完整接入流程。从获取 OpenID、创建订单、调起支付到处理回调,每一步都有详细的代码示例和官方文档支持。
作为独立开发者,接入微信支付是项目变现的重要一步。希望这篇教程能帮助你少走弯路,快速上线支付功能。如果在接入过程中遇到问题,建议仔细阅读官方文档的错误码说明,或在微信支付开发者社区寻求帮助。
祝你开发顺利,产品大卖!
本教程基于微信支付 APIv3 和官方 Go SDK v0.2.21 编写,内容可能会随官方更新而变化,请以最新官方文档为准。







