微信直连商户公众号 JSAPI 支付,详细教程+源码
大家好,我是小悟。
适用场景与入口:JSAPI 支付用于微信公众号内的网页调起微信收银台,常见于在公众号菜单、文章页或 H5 活动页中完成支付。该方式依赖微信内置浏览器环境,非微信浏览器无法调起。
身份标识:下单前必须获取用户在当前公众号下的OpenID,通常通过 OAuth2 授权码 code → access_token → openid 流程获得。
下单与预支付:商户后端调用微信统一下单 API,传入必要参数(如 appid、mchid、out_trade_no、total、body、notify_url、openid、trade_type=JSAPI),微信返回prepay_id。
前端调起:前端使用微信 JSSDK 的 chooseWXPay 调起支付,传入由后端生成的支付参数与签名(appId、timeStamp、nonceStr、package=prepay_id=…、signType、paySign)。
结果处理:前端仅用于展示结果;商户以微信服务器的异步通知(notify)为准更新订单状态,并做好幂等。
域名与目录:需在公众号后台配置网页授权域名,在商户平台配置支付授权目录,否则会报“当前页面的 URL 未注册”等错误。
安全要点:全程使用 HTTPS,参数签名校验、金额一致性校验、通知去重与状态机控制。
以上要点与流程为微信公众号内网页支付的标准实践,适用于公众号场景的 JSAPI 调起与结果处理。
后端 Spring Boot 实现(Java)
说明:以下示例采用微信支付v3 版 API + SDK,包含配置、统一下单、回调验签与通知回执,便于快速落地。
- 1、Maven 依赖(pom.xml)
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 微信支付 v3 Java SDK -->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.4.12</version>
</dependency>
<!-- JSON 处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
说明:使用官方维护的 wechatpay-java SDK,简化签名、验签与 HTTP 调用,适配 v3 接口与证书自动更新。
2、配置文件(application.yml)
wechat:
appId: wx8888888888888888
mchId: 1900000001
merchantSerialNumber: 444C3F6B5A7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F
privateKeyPath: classpath:cert/merchant/apiclient_key.pem
apiV3Key: 7788990011223344556677889900112233445566
notifyUrl: https://yourdomain.com/api/wxpay/notify
说明:配置包含公众号 appId、商户号 mchId、商户证书序列号、商户私钥路径、APIv3 密钥与支付回调地址。其中 APIv3 密钥用于回调内容解密与验签。
3、配置类与 SDK 初始化
package com.example.wxpay.config;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.util.PemUtil;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
@Configuration
@ConfigurationProperties(prefix = "wechat")
public class WechatPayConfig {
private String appId;
private String mchId;
private String merchantSerialNumber;
private String privateKeyPath;
private String apiV3Key;
private String notifyUrl;
@Bean
public JsapiService jsapiService() throws Exception {
// 读取商户私钥(PKCS#8 PEM)
InputStream keyInputStream = getClass().getClassLoader().getResourceAsStream(privateKeyPath);
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(keyInputStream);
RSAAutoCertificateConfig config = new RSAAutoCertificateConfig.Builder()
.merchantId(mchId)
.privateKeyFromPath(privateKeyPath)
.merchantSerialNumber(merchantSerialNumber)
.apiV3Key(apiV3Key.getBytes(StandardCharsets.UTF_8))
.build();
return new JsapiService.Builder().config(config).build();
}
// getter/setter 省略
}
说明:通过 RSAAutoCertificateConfig 自动管理平台证书并完成请求签名,是微信支付 v3 推荐的接入方式。
4、统一下单与生成前端调起参数
package com.example.wxpay.service;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import com.wechat.pay.java.service.payments.jsapi.model.Amount;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayResponse;
import com.wechat.pay.java.service.payments.nativepay.model.Amount as NativeAmount;
import com.wechat.pay.java.service.payments.nativepay.model.Transaction;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.UUID;
@Service
public class WxPayService {
private final JsapiService jsapiService;
private final String appId;
private final String notifyUrl;
public WxPayService(JsapiService jsapiService,
@Value("${wechat.appId}") String appId,
@Value("${wechat.notifyUrl}") String notifyUrl) {
this.jsapiService = jsapiService;
this.appId = appId;
this.notifyUrl = notifyUrl;
}
// 金额单位:分 -> 元(微信统一下单以“分”为单位)
private long fenToYuan(Long fen) {
return fen == null ? 0L : fen / 100L;
}
public PrepayResponse jsapiPrepay(String openid, Long totalFee, String body, String outTradeNo) throws Exception {
PrepayRequest request = new PrepayRequest();
Amount amount = new Amount();
amount.setTotal(fenToYuan(totalFee)); // 微信金额单位:分
request.setAmount(amount);
request.setAppid(appId);
request.setMchid(mchId);
request.setOutTradeNo(outTradeNo);
request.setNonceStr(UUID.randomUUID().toString().replace("-", ""));
request.setBody(body);
request.setNotifyUrl(notifyUrl);
request.setPayer(new com.wechat.pay.java.service.payments.jsapi.model.Payer().setOpenid(openid));
return jsapiService.prepay(request);
}
}
说明:调用 JsapiService.prepay 获取 prepay_id,后续用于前端调起支付。注意金额单位为分,openid 为必填。
5、回调控制器(验签 + 解密 + 回执)
ackage com.example.wxpay.controller;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.NotificationRequest;
import com.wechat.pay.java.core.notification.NotificationResponse;
import com.wechat.pay.java.service.payments.nativepay.model.Transaction;
import com.wechat.pay.java.service.payments.jsapi.model.Amount;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Slf4j
@RestController
@RequestMapping("/api/wxpay")
public class WxPayNotifyController {
private final NotificationParser notificationParser;
private final ObjectMapper objectMapper = new ObjectMapper();
public WxPayNotifyController(RSAAutoCertificateConfig config) {
this.notificationParser = new NotificationParser(config);
}
@PostMapping(value = "/notify", consumes = "application/json", produces = "application/json")
public ResponseEntity<byte[]> handleNotify(HttpServletRequest request) {
try {
// 1) 解析并验签,自动解密 resource.data
NotificationRequest req = notificationParser.parse(request.getInputStream());
String resourceType = req.getResourceType();
String plaintext = new String(req.getPlaintext(), StandardCharsets.UTF_8);
String serialNo = req.getWechatpaySerial();
String signature = req.getWechatpaySignature();
String nonce = req.getWechatpayNonce();
// 2) 根据资源类型处理(此处以 JSAPI 为例,实际可能为 TRANSACTION.TRANSACTION 或 JSAPI.PREPAYMENT_ID 等)
if ("TRANSACTION.TRANSACTION".equals(resourceType)) {
Transaction txn = objectMapper.readValue(plaintext, Transaction.class);
String tradeState = txn.getTradeState();
String outTradeNo = txn.getOutTradeNo();
log.info("支付回调 trade_state={}, out_trade_no={}", tradeState, outTradeNo);
// TODO: 校验商户订单号、金额一致性、幂等处理、更新订单状态
// 3) 返回成功回执(必须)
return ResponseEntity.ok().body("{\"code\":\"SUCCESS\",\"message\":\"成功\"}".getBytes(StandardCharsets.UTF_8));
} else {
log.warn("暂不支持的回调类型: {}", resourceType);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("{\"code\":\"FAIL\",\"message\":\"不支持的回调类型\"}".getBytes(StandardCharsets.UTF_8));
}
} catch (Exception e) {
log.error("处理微信回调异常", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("{\"code\":\"FAIL\",\"message\":\"处理失败\"}".getBytes(StandardCharsets.UTF_8));
}
}
}
说明:使用 NotificationParser 自动完成验签与解密,业务侧仅需按资源类型解析并处理。回调必须返回200 与成功 JSON,否则微信会重试。
前端 UniApp 实现(Vue 2⁄3 通用思路)
说明:前端在公众号内页面调用 JSSDK 的 chooseWXPay,参数由后端生成并签名。注意使用 HTTPS 与微信授权域名,并在支付回调中仅作页面跳转,以服务器异步通知为准更新订单。
1、安装并引入 JSSDK(NPM)
npm install jweixin-module --save
说明:使用官方推荐的 jweixin-module 在小程序/公众号 H5 中调用微信 JSAPI。
2、获取 OpenID(示例:OAuth2 简化流程)
// utils/auth.js
export function getWxCode(callback) {
const appid = 'wx8888888888888888';
const redirectUri = encodeURIComponent('https://yourdomain.com/pages/pay/index');
const scope = 'snsapi_base'; // 静默授权,仅获取 openid
const state = 'STATE';
const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
// 若当前页面无 code,则跳转授权
if (!getLocationSearch('code')) {
window.location.href = url;
} else {
const code = getLocationSearch('code');
// 将 code 发送给后端换取 openid(示例)
// axios.post('/api/wxpay/auth', { code }).then(res => callback(res.data.openid));
callback('MOCK_OPENID'); // 仅演示,实际请请求后端
}
}
function getLocationSearch(key) {
const reg = new RegExp('(^|&)' + key + '=([^&]*)(&|$)');
const r = window.location.search.substr(1).match(reg);
return r ? decodeURIComponent(r[2]) : null;
}
说明:通过 snsapi_base 静默授权获取 openid,用于统一下单必填字段。生产环境请在后端完成换取与校验。
3、调起支付页面(Vue 示例)
<template>
<view class="pay-page">
<button type="primary" @click="handlePay">微信支付</button>
</view>
</template>
<script>
import jweixin from 'jweixin-module';
import { getWxCode } from '@/utils/auth';
export default {
data() {
return {
payParams: null, // { appId, timeStamp, nonceStr, package: 'prepay_id=...', signType, paySign }
};
},
async onLoad() {
await getWxCode((openid) => {
this.createOrder(openid);
});
},
methods: {
async createOrder(openid) {
try {
const res = await uni.request({
url: 'https://yourdomain.com/api/wxpay/jsapi-prepay',
method: 'POST',
data: { openid, totalFee: 100, body: '测试商品', outTradeNo: 'ORDER_' + Date.now() },
});
this.payParams = res.data.data; // 后端返回的支付参数
this.initWxConfig();
} catch (err) {
uni.showToast({ title: '下单失败', icon: 'none' });
}
},
initWxConfig() {
jweixin.config({
debug: false,
appId: this.payParams.appId,
timestamp: parseInt(this.payParams.timeStamp, 10),
nonceStr: this.payParams.nonceStr,
signature: this.payParams.paySign,
jsApiList: ['chooseWXPay'],
});
jweixin.ready(() => {
// 可选:检测接口可用性
jweixin.checkJsApi({
jsApiList: ['chooseWXPay'],
});
});
jweixin.error((err) => {
console.error('JSSDK 配置失败', err);
uni.showToast({ title: '支付初始化失败', icon: 'none' });
});
},
pay() {
if (!this.payParams) return;
uni.showLoading({ title: '调起支付中' });
jweixin.chooseWXPay({
timestamp: parseInt(this.payParams.timeStamp, 10), // 注意:JSSDK 字段名为小写 timeStamp
nonceStr: this.payParams.nonceStr,
package: this.payParams.package, // 形如:prepay_id=wx123456...
signType: this.payParams.signType, // 通常为 MD5
paySign: this.payParams.paySign,
success: (res) => {
uni.hideLoading();
uni.showToast({ title: '支付成功', icon: 'success' });
// 注意:前端成功回调并非最终结果,需以服务器异步通知为准
uni.redirectTo({ url: '/pages/pay/result?status=success' });
},
cancel: () => {
uni.hideLoading();
uni.showToast({ title: '支付取消', icon: 'none' });
},
fail: (err) => {
uni.hideLoading();
console.error('支付失败', err);
uni.showToast({ title: '支付失败', icon: 'none' });
},
});
},
},
};
</script>
说明:前端通过 chooseWXPay 调起支付,参数中的 package=prepay_id=… 与 paySign 由后端提供。注意 timeStamp 在 JSSDK 中为小写,与后端统一下单的 timeStamp(大写)命名不同。支付结果以服务器异步通知为准,前端回调仅用于页面提示与跳转。
部署与安全要点
域名与目录:在公众号后台配置网页授权域名,在商户平台配置支付授权目录,页面 URL 必须与配置完全匹配,否则无法调起支付。
HTTPS 与证书:全链路使用 HTTPS;商户 API 证书妥善保管,SDK 已支持自动更新平台证书,无需重启。
金额与签名:金额统一用分;签名使用微信指定算法(v3 为 RSA-SHA256),严格按字典序与规则拼接。
幂等与去重:以微信异步通知为准更新订单,做好幂等(如按商户订单号去重、状态机校验)。
以上为公众号支付的关键注意事项,能有效避免“未注册 URL”“签名错误”“无法调起”等常见问题。

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海
- 点赞
- 收藏
- 关注作者
评论(0)