本文已同步发布到微信公众号「人言兑」👈
扫描二维码关注,第一时间获取更新!🔗
点击前往微信公众号阅读本文作为一名独立开发者,接入微信支付可能是你项目变现的关键一步。但面对复杂的文档、各种证书、密钥和回调机制,很多人会在第一步就卡住。
本教程基于微信支付 APIv3 和官方 Go SDK,从零开始带你完成 JSAPI 支付的完整接入。我们将使用最新的微信支付公钥模式(适用于 2023 年后新申请的商户号),相比传统的平台证书模式更加简洁。
你将学到:
JSAPI 支付的完整业务流程如何获取用户 OpenID(微信授权)如何创建订单、调起支付、处理回调如何使用微信支付公钥进行验签完整的可运行代码示例
前置要求:
基础的 Go 语言知识已注册微信支付商户号(支持公钥模式)已注册微信公众号/服务号(已认证)一个外网可访问的域名在开始之前,请确保已完成以下准备工作:
了解哪种微信支付产品更适合你的业务场景如何注册微信商户号商户号与APPID绑定准备Go 1.21+开发环境(可选)👉 点击查看《微信支付系列教程》文章目录在《微信支付系列教程》系列文章中,我们将详细介绍如何从0到1在自己的网站中接入微信支付。以下是该系列文章的全部内容:
微信支付接入教程(独立开发者实战版):微信支付商户号注册全流程指南(最新版)| 如何开通微信支付收款功能微信支付接入教程(独立开发者实战版):微信支付全部产品与功能一文搞懂微信支付接入教程(独立开发者实战版):微信公众号服务号认证完全指南微信支付接入教程(独立开发者实战版):商户号与APPID绑定完全指南微信支付接入教程(独立开发者实战版):Native 支付完整接入指南微信支付接入教程(独立开发者实战版):JSAPI 支付完整接入指南微信支付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 支付",点击申请开通按提示完成开通流程第二步:绑定 AppIDJSAPI 支付必须将商户号与公众号/小程序 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:00notify_url回调地址必须外网可访问,HTTPSprepay_id 有效期: 2小时,过期需重新下单。
阶段二:商户调起支付前端通过微信内置对象 WeixinJSBridge 调起支付。
调起支付参数:
参数说明appId公众号 AppIDtimeStamp当前时间戳(秒级)nonceStr随机字符串packageprepay_id=xxxsignType固定值 RSApaySign签名值商户调起支付前,请确保已在商户平台配置好 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(商品列表):
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;
}
选择商品
测试商品-可乐
¥0.01
测试商品-雪碧
¥0.01
function buy(id, name, price) {
// price 单位:元
sessionStorage.setItem("product", JSON.stringify({ id, name, price }));
location.href = "/pay";
}
templates/pay.html(支付确认):
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;
}
确认支付
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);
}
},
);
}
测试与调试将服务部署到公网,或者使用 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 支付产品介绍JSAPI 开发指引JSAPI/小程序下单 APIJSAPI 调起支付微信支付公钥使用指南支付回调通知查询订单 API关闭订单 API申请退款 API微信支付 Go SDK结语通过本教程,你应该已经掌握了 JSAPI 支付的完整接入流程。从获取 OpenID、创建订单、调起支付到处理回调,每一步都有详细的代码示例和官方文档支持。
作为独立开发者,接入微信支付是项目变现的重要一步。希望这篇教程能帮助你少走弯路,快速上线支付功能。如果在接入过程中遇到问题,建议仔细阅读官方文档的错误码说明,或在微信支付开发者社区寻求帮助。
祝你开发顺利,产品大卖!
本教程基于微信支付 APIv3 和官方 Go SDK v0.2.21 编写,内容可能会随官方更新而变化,请以最新官方文档为准。
版权声明未经授权,禁止转载本站文章。如需转载必须保留原文链接并注明出处。只要保留链接并注明出处,即视为默认获得授权。未注明出处或删除链接将视为侵权,必追究法律责任!
本文原文链接:https://blog.axiaoxin.com/post/wechat-pay-jsapi-guide/