微信小程序发起微信支付和微信支付回调介绍,本文提供了Java后端和Vue前端的主要实现代码,具体支付的参数和业务代码不展示。
一、整体流程图

二、关键配置(application.yml)
pay:
wx:
app-id: xxx # 小程序AppID
app-secret: xxx # 小程序AppSecret
merchant-id: xxx # 微信支付商户号
merchant-serial-no: xxx # 商户API证书序列号
api-v3-key: xxx # 32位APIv3密钥
private-key-path: classpath:cert/apiclient_key.pem # PKCS8格式商户私钥
notify-url: xxx/wxPayNotify/v1 # 公网HTTPS回调地址
支付的参数可以参考网上的,本文末尾也介绍了,需要营业职照、法人身份证等才可以申请。
三、核心后端代码
1. 实体类(WxPayNotifyRecord.java、NbWechatPayRecord.java)
@Data
@TableName("wx_pay_notify_record")
public class WxPayNotifyRecord {
@TableId(type = IdType.INPUT) // 【关键】手动输入UUID
private String id;
private String merTranNo;
private String rawNotifyBody;
private String outTradeNo;
private String transactionId;
private String bankTradeNo;
private String tradeState;
private String totalAmount;
private String handleStatus;
private String handleMsg;
private LocalDateTime createTime;
}
@Data
@TableName("nb_wechat_pay_record")
public class NbWechatPayRecord {
@TableId(type = IdType.AUTO)
private Long id;
private Long billId;
private String payMerTranNo;
private String bankTradeNo;
private String openid;
private BigDecimal payAmount;
private String payStatus;
private String prepayId;
private LocalDateTime expireTime;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
2. Controller层(核心支付+回调)
@Slf4j
@RestController
@RequestMapping("/nbBillRecord")
public class NbBillRecordController {
@Autowired
private WxPayConfig wxPayConfig;
@Autowired
private BankCallbackService bankCallbackService;
@Autowired
private WxPayNotifyRecordMapper wxPayNotifyRecordMapper;
/**
* 【免认证】Code换OpenID
*/
@Anonymous
@GetMapping("/getWxOpenId")
public AjaxResult getWxOpenId(@RequestParam String code) {
log.info("【微信支付】获取OpenID,code:{}", code);
try {
String openid = WxPayV3Util.getOpenIdByCode(wxPayConfig.getAppId(), wxPayConfig.getAppSecret(), code);
return AjaxResult.success(openid);
} catch (Exception e) {
log.error("【微信支付】获取OpenID失败", e);
return AjaxResult.error("获取OpenID失败:" + e.getMessage());
}
}
/**
* 【免认证】创建支付订单
*/
@Anonymous
@PostMapping("/startWxPay")
public AjaxResult startWxPay(@RequestBody StartPaymentDTO dto) {
log.info("【微信支付】创建订单,账单ID:{},OpenID:{}", dto.getBillRecordId(), dto.getOpenid());
try {
Map<String, String> payParams = bankCallbackService.createWxPayOrder(dto);
return AjaxResult.success(payParams);
} catch (Exception e) {
log.error("【微信支付】创建订单失败", e);
return AjaxResult.error(e.getMessage());
}
}
/**
* 【免认证】微信支付异步回调
*/
@Anonymous
@PostMapping("/wxPayNotify/v1")
@Transactional(rollbackFor = Exception.class)
public String wxPayNotify(@RequestBody String notifyJson) {
log.info("【微信支付回调】收到通知:{}", notifyJson);
try {
JSONObject notifyObj = JSONObject.parseObject(notifyJson);
JSONObject resource = notifyObj.getJSONObject("resource");
String decryptStr = bankCallbackService.decryptWxNotify(resource);
JSONObject bizContent = JSONObject.parseObject(decryptStr);
log.info("【微信回调解密】:{}", decryptStr);
String merTranNo = bizContent.getString("out_trade_no");
String tradeState = bizContent.getString("trade_state");
// 保存回调记录
saveWxPayNotifyRecord(notifyJson, bizContent, merTranNo);
// 处理支付结果
if ("SUCCESS".equals(tradeState)) {
// 支付成功逻辑:更新订单状态、入账等
// bankCallbackService.handleWxPaySuccess(payRecord, bizContent);
}
return "{\"code\":\"SUCCESS\"}";
} catch (Exception e) {
log.error("【微信回调】处理异常", e);
return "{\"code\":\"FAIL\",\"message\":\"" + e.getMessage() + "\"}";
}
}
private void saveWxPayNotifyRecord(String notifyJson, JSONObject bizContent, String merTranNo) {
WxPayNotifyRecord notifyRecord = new WxPayNotifyRecord();
notifyRecord.setId(UUID.randomUUID().toString().replace("-", ""));
notifyRecord.setMerTranNo(merTranNo);
notifyRecord.setRawNotifyBody(notifyJson);
notifyRecord.setTradeState(bizContent.getString("trade_state"));
notifyRecord.setTotalAmount(bizContent.getJSONObject("amount").getString("total"));
notifyRecord.setBankTradeNo(bizContent.getString("transaction_id"));
notifyRecord.setHandleStatus("SUCCESS");
notifyRecord.setHandleMsg("微信支付回调处理成功");
notifyRecord.setCreateTime(LocalDateTime.now());
wxPayNotifyRecordMapper.insert(notifyRecord);
}
}
3. Service层(核心签名逻辑)
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, String> createWxPayOrder(StartPaymentDTO dto) {
// ... 省略业务校验代码 ...
// 1. 调用微信统一下单接口
long requestTimestamp = System.currentTimeMillis() / 1000;
String requestNonce = UUID.randomUUID().toString().replace("-", "");
JSONObject reqJson = new JSONObject();
reqJson.put("appid", wxPayConfig.getAppId());
reqJson.put("mchid", wxPayConfig.getMerchantId());
reqJson.put("description", "账单支付");
reqJson.put("out_trade_no", merTranNo);
reqJson.put("notify_url", wxPayConfig.getNotifyUrl());
// 金额处理:元转分
Map<String, Object> amountMap = new HashMap<>();
amountMap.put("total", needPay.multiply(new BigDecimal("100")).intValue());
reqJson.put("amount", amountMap);
// 用户openid
Map<String, Object> payerMap = new HashMap<>();
payerMap.put("openid", openid);
reqJson.put("payer", payerMap);
// 2. 生成下单签名,请求微信接口
String requestSignStr = "POST\n/v3/pay/transactions/jsapi\n" + requestTimestamp + "\n" + requestNonce + "\n" + reqJson.toJSONString() + "\n";
String requestSign = WxPayV3Util.sign(requestSignStr, wxPayConfig.getPrivateKey());
// ... 省略请求微信下单代码 ...
String prepayId = resJson.getString("prepay_id");
// ==============================================
// 【核心避坑】小程序支付签名串只有4行!不要signType!
// ==============================================
String appId = wxPayConfig.getAppId();
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
String nonceStr = UUID.randomUUID().toString().replace("-", "");
String packageValue = "prepay_id=" + prepayId;
String signType = "RSA";
// 签名串严格4行:appId\n + timeStamp\n + nonceStr\n + package\n
String paySignStr = appId + "\n"
+ timeStamp + "\n"
+ nonceStr + "\n"
+ packageValue + "\n";
// 生成签名
String paySign = WxPayV3Util.sign(paySignStr, wxPayConfig.getPrivateKey());
// 3. 返回给前端(不传appId,避免前端误传)
Map<String, String> result = new HashMap<>();
result.put("timeStamp", timeStamp);
result.put("nonceStr", nonceStr);
result.put("package", packageValue);
result.put("signType", signType);
result.put("paySign", paySign);
return result;
}
工具类
@Data
@Configuration
@ConfigurationProperties(prefix = "pay.wx")
public class WxPayConfig {
@Autowired
private ResourceLoader resourceLoader;
private String appId;
private String appSecret;
private String merchantId;
private String merchantSerialNo;
private String apiV3Key;
private String privateKeyPath;
private String notifyUrl;
/**
* 加载微信支付商户私钥(兼容 Java 8,无 readAllBytes())
*/
public PrivateKey getPrivateKey() {
try {
Resource resource = resourceLoader.getResource(privateKeyPath);
InputStream inputStream = resource.getInputStream();
// Java 8 兼容读取字节流
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
byte[] bytes = outputStream.toByteArray();
// 关闭流
inputStream.close();
outputStream.close();
String key = new String(bytes, StandardCharsets.UTF_8)
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(key));
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePrivate(spec);
} catch (Exception e) {
throw new RuntimeException("微信支付私钥加载失败", e);
}
}
}
@Slf4j
public class WxPayV3Util {
public static boolean SANDBOX_MODE = false;
/**
* Code换OpenID(完全兼容正式环境)
*/
public static String getOpenIdByCode(String appId, String appSecret, String code) {
if (SANDBOX_MODE) {
log.info("【微信沙箱】模拟OpenID:sandbox_openid_123");
return "sandbox_openid_123";
}
String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appId +
"&secret=" + appSecret + "&js_code=" + code + "&grant_type=authorization_code";
try {
RestTemplate restTemplate = new RestTemplate();
String res = restTemplate.getForObject(url, String.class);
JSONObject json = JSONObject.parseObject(res);
if (json.containsKey("openid")) {
return json.getString("openid");
}
log.error("Code换OpenID失败,微信返回:{}", res);
throw new RuntimeException("获取用户标识失败:" + json.getString("errmsg"));
} catch (Exception e) {
log.error("Code获取OpenID异常", e);
throw new RuntimeException("获取用户标识失败", e);
}
}
/**
* 【核心修复】微信支付SHA256withRSA签名(严格对标官方)
*/
public static String sign(String signContent, PrivateKey privateKey) throws Exception {
// log.info("【微信签名】待签名内容:\n{}", signContent);
// 强制指定签名算法为SHA256withRSA,和signType=RSA对应
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(signContent.getBytes(StandardCharsets.UTF_8));
byte[] signBytes = signature.sign();
// 标准Base64编码,无换行、无空格
String signResult = Base64.getEncoder().encodeToString(signBytes);
// log.info("【微信签名】生成签名结果:{}", signResult);
return signResult;
}
/**
* 微信回调AES-GCM解密(修复后合规版)
*/
public static String decryptNotify(String apiV3Key, String ad, String nonce, String ciphertext) throws Exception {
if (SANDBOX_MODE) {
log.info("【微信沙箱】模拟回调解密,返回支付成功数据");
return "{\"out_trade_no\":\"WX1234567890\",\"transaction_id\":\"WX_SANDBOX_001\",\"trade_state\":\"SUCCESS\",\"amount\":{\"total\":1}}";
}
SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8)));
if (ad != null && !ad.isEmpty()) {
cipher.updateAAD(ad.getBytes(StandardCharsets.UTF_8));
}
return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), StandardCharsets.UTF_8);
}
/**
* 构建微信V3接口请求头
*/
public static String buildAuthHeader(String mchId, String serialNo, String sign, String nonce, long timestamp) {
if (SANDBOX_MODE) {
return "WECHATPAY2-SHA256-RSA2048 mchid=\"sandbox_mch\",serial_no=\"sandbox_serial\",nonce_str=\"" + nonce + "\",timestamp=\"" + timestamp + "\",signature=\"" + sign + "\"";
}
return "WECHATPAY2-SHA256-RSA2048 mchid=\"" + mchId +
"\",serial_no=\"" + serialNo +
"\",nonce_str=\"" + nonce +
"\",timestamp=\"" + timestamp +
"\",signature=\"" + sign + "\"";
}
/**
* 【新增】微信支付签名自校验(生成签名后,立即用公钥验证)
* 注意:这里需要你的商户公钥(从证书里提取)
*/
public static boolean verifySign(String signContent, String sign, String publicKeyStr) throws Exception {
// 解析公钥
publicKeyStr = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s+", "");
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyStr);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
// 验证签名
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(signContent.getBytes(StandardCharsets.UTF_8));
boolean result = signature.verify(Base64.getDecoder().decode(sign));
log.info("【签名自校验】结果:{}", result ? "✅ 签名合法" : "❌ 签名非法");
return result;
}
}
四、核心前端代码(Vue)
<template>
<view class="pay-container">
<view class="title">微信原生支付</view>
<button class="pay-btn" @tap="onPayClick" :disabled="isLoading">
{{ isLoading ? '处理中...' : '支付 0.01元' }}
</button>
</view>
</template>
<script>
export default {
name: 'WxPayPage',
data() {
return {
billRecordId: 1476,
isLoading: false,
baseUrl: 'https://pw0wn0qh0i.fy.takin.cc'
}
},
methods: {
async onPayClick() {
try {
this.isLoading = true;
// 1. 获取微信登录Code
const code = await this.getWxLoginCode();
// 2. 获取OpenID
const openid = await this.getOpenId(code);
// 3. 调用创建支付订单接口
const payParams = await this.startWxPay(openid);
// 4. 调起微信支付
await this.callWxPayment(payParams);
wx.showToast({ title: '支付成功', icon: 'success' });
} catch (err) {
wx.showToast({ title: err.message, icon: 'none' });
} finally {
this.isLoading = false;
}
},
getWxLoginCode() {
return new Promise((resolve, reject) => {
wx.login({
success: (res) => res.code ? resolve(res.code) : reject(new Error("获取Code失败")),
fail: (err) => reject(new Error("登录失败"))
});
});
},
getOpenId(code) {
return new Promise((resolve, reject) => {
wx.request({
url: this.baseUrl + '/nbBillRecord/getWxOpenId',
data: { code },
success: (res) => res.data.code === 200 ? resolve(res.data.data) : reject(new Error(res.data.msg)),
fail: () => reject(new Error("网络请求失败"))
});
});
},
startWxPay(openid) {
return new Promise((resolve, reject) => {
wx.request({
url: this.baseUrl + '/nbBillRecord/startWxPay',
method: "POST",
data: { billRecordId: this.billRecordId, openid: openid },
header: { "content-type": "application/json" },
success: (res) => res.data.code === 200 ? resolve(res.data.data) : reject(new Error(res.data.msg)),
fail: () => reject(new Error("网络请求失败"))
});
});
},
/**
* 【核心避坑】调起支付只传5个参数,不传appId!
*/
callWxPayment(payParams) {
return new Promise((resolve, reject) => {
wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType,
paySign: payParams.paySign,
success: () => resolve(),
fail: (err) => {
err.errMsg.includes("cancel") ? reject(new Error("用户取消")) : reject(new Error(err.errMsg));
}
});
});
}
}
}
</script>
五、避坑指南(核心笔记)
| 坑点 | 错误做法 | 正确做法 |
|---|---|---|
| 签名串行数 | 5行(加了signType) | 4行(appId\n + timeStamp\n + nonceStr\n + package\n) |
| 前端调起参数 | 传了appId | 不传appId,只传timeStamp/nonceStr/package/signType/paySign |
| 签名串换行符 | 最后一行不加\n | 前4行末尾都加\n,包括package |
| 实体类主键 | 没有@TableId注解 | 必须加@TableId(type = IdType.INPUT) |
| 私钥格式 | PKCS1格式 | 必须是PKCS8格式(开头是-----BEGIN PRIVATE KEY-----) |
| 回调地址 | HTTP地址 | 必须是公网可访问的HTTPS地址 |
六、数据库建表语句
CREATE TABLE `wx_pay_notify_record` (
`id` varchar(64) NOT NULL COMMENT '主键ID(UUID)',
`mer_tran_no` varchar(64) DEFAULT NULL COMMENT '商户订单号',
`raw_notify_body` text COMMENT '原始回调报文',
`out_trade_no` varchar(64) DEFAULT NULL COMMENT '兼容字段',
`transaction_id` varchar(64) DEFAULT NULL COMMENT '微信订单号',
`bank_trade_no` varchar(64) DEFAULT NULL COMMENT '兼容字段',
`trade_state` varchar(32) DEFAULT NULL COMMENT '交易状态',
`total_amount` varchar(32) DEFAULT NULL COMMENT '支付金额(分)',
`handle_status` varchar(32) DEFAULT NULL COMMENT '处理状态',
`handle_msg` varchar(512) DEFAULT NULL COMMENT '处理结果',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_mer_tran_no` (`mer_tran_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信支付回调记录表';
CREATE TABLE `nb_wechat_pay_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`bill_id` bigint NOT NULL COMMENT '账单id',
`pay_mer_tran_no` varchar(50) NOT NULL COMMENT '商户交易编号(系统订单号)',
`bank_trade_no` varchar(50) DEFAULT NULL COMMENT '微信支付订单号',
`openid` varchar(50) DEFAULT NULL COMMENT '微信用户openid',
`pay_amount` decimal(10,2) NOT NULL COMMENT '支付金额',
`pay_status` varchar(20) NOT NULL COMMENT 'INIT/ PAYING / SUCCESS / FAIL / CLOSED',
`prepay_id` varchar(100) DEFAULT NULL COMMENT '微信预支付ID',
`expire_time` datetime DEFAULT NULL COMMENT '订单失效时间',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pay_mer_tran_no` (`pay_mer_tran_no`),
KEY `idx_bill_id` (`bill_id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='微信小程序支付记录表';
参数获取方法:
1. app-id
在 微信公众平台 获取:
- 登录 https://mp.weixin.qq.com/
- 左侧菜单:开发 → 开发管理 → 开发设置
- 开发者ID里就能看到:
AppID(小程序ID)
2. mch-id(商户号)
你需要有一个微信支付商户号,并把它和你的小程序绑定。
申请步骤:
-
注册微信支付商户号:
- 访问 https://pay.weixin.qq.com/
- 点击右上角「接入微信支付」,按流程提交资料(营业执照、法人身份证、银行账户等)。
- 审核通过后(通常1-3个工作日),你会收到微信支付发来的邮件,里面包含 商户号(mchid)。
-
绑定小程序到商户号(关键!否则无法支付):
- 登录微信支付商户平台 https://pay.weixin.qq.com/
- 顶部菜单:产品中心 → AppID账号管理 → 关联AppID
- 输入你的小程序 AppID,提交关联。
- 然后去微信公众平台(mp.weixin.qq.com)确认关联。
3. 在商户平台设置的:api-v3-key
这个是你自己设置的 APIv3 密钥,用于加密解密。
设置步骤:
- 登录微信支付商户平台 https://pay.weixin.qq.com/
- 顶部菜单:账户中心 → API安全
- 找到「APIv3密钥」,点击「设置密钥」。
- 它会让你下载一个安全工具(或者用手机验证码),验证通过后,输入一个32位的自定义字符串(建议用随机生成的,保存好,丢了只能重置)。
- 这个字符串就是你的
api-v3-key。
4. 需要下载证书文件的:private-key-path 和 merchant-serial-number
这两个是一对,都来自于商户API证书。
申请/下载步骤:
- 同样在 账户中心 → API安全 页面。
- 找到「API证书」,点击「申请证书」。
- 按流程操作,最后会下载一个压缩包(通常叫
wxpay_cert_xxxxxx.zip)。 - 解压这个压缩包,你会看到几个文件,我们只需要这两个:
apiclient_key.pem:商户私钥文件(这个就是private-key-path指向的文件)apiclient_cert.pem:商户证书(用来查看序列号)
配置 private-key-path:
- 在你的项目
src/main/resources/下新建一个文件夹,叫cert。 - 把刚才解压得到的
apiclient_key.pem复制进去。 - 配置文件里填:
classpath:cert/apiclient_key.pem
&spm=1001.2101.3001.5002&articleId=158967712&d=1&t=3&u=cd5c2527ac044d6facad150281c3a141)
1万+

被折叠的 条评论
为什么被折叠?



