微信小程序支付V3零基础教程(发起支付和支付回调)

微信小程序发起微信支付和微信支付回调介绍,本文提供了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

微信公众平台 获取:

  1. 登录 https://mp.weixin.qq.com/
  2. 左侧菜单:开发开发管理开发设置
  3. 开发者ID里就能看到:AppID(小程序ID)

2. mch-id(商户号)

你需要有一个微信支付商户号,并把它和你的小程序绑定。

申请步骤:

  1. 注册微信支付商户号

    • 访问 https://pay.weixin.qq.com/
    • 点击右上角「接入微信支付」,按流程提交资料(营业执照、法人身份证、银行账户等)。
    • 审核通过后(通常1-3个工作日),你会收到微信支付发来的邮件,里面包含 商户号(mchid)
  2. 绑定小程序到商户号(关键!否则无法支付):

    • 登录微信支付商户平台 https://pay.weixin.qq.com/
    • 顶部菜单:产品中心AppID账号管理关联AppID
    • 输入你的小程序 AppID,提交关联。
    • 然后去微信公众平台(mp.weixin.qq.com)确认关联。

3. 在商户平台设置的:api-v3-key

这个是你自己设置的 APIv3 密钥,用于加密解密。

设置步骤:

  1. 登录微信支付商户平台 https://pay.weixin.qq.com/
  2. 顶部菜单:账户中心API安全
  3. 找到「APIv3密钥」,点击「设置密钥」。
  4. 它会让你下载一个安全工具(或者用手机验证码),验证通过后,输入一个32位的自定义字符串(建议用随机生成的,保存好,丢了只能重置)。
  5. 这个字符串就是你的 api-v3-key

4. 需要下载证书文件的:private-key-pathmerchant-serial-number

这两个是一对,都来自于商户API证书

申请/下载步骤:

  1. 同样在 账户中心API安全 页面。
  2. 找到「API证书」,点击「申请证书」。
  3. 按流程操作,最后会下载一个压缩包(通常叫 wxpay_cert_xxxxxx.zip)。
  4. 解压这个压缩包,你会看到几个文件,我们只需要这两个:
    • apiclient_key.pem商户私钥文件(这个就是 private-key-path 指向的文件)
    • apiclient_cert.pem:商户证书(用来查看序列号)

配置 private-key-path

  1. 在你的项目 src/main/resources/ 下新建一个文件夹,叫 cert
  2. 把刚才解压得到的 apiclient_key.pem 复制进去。
  3. 配置文件里填:classpath:cert/apiclient_key.pem

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值