news 2026/8/31 17:45:32

SpringBoot+Uniapp实现微信支付V3(JSAPI)全流程实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Uniapp实现微信支付V3(JSAPI)全流程实战指南

1. 环境准备与项目初始化

大家好,我是老张,一个在Java和移动端摸爬滚打了十多年的老码农。今天咱们不聊虚的,直接上手,把SpringBoot后端和Uniapp前端怎么打通微信支付V3的JSAPI支付,给你讲得明明白白。这玩意儿说难不难,但坑是真不少,尤其是V3版本,签名规则、证书加载、回调解密,每一步都可能让你折腾半天。我敢说,跟着我这篇实战指南走,哪怕你之前没碰过微信支付,也能在半天内把支付流程完整跑通。

首先,咱们得把“战场”打扫干净。你需要准备几个关键的东西,这就像做饭前得先备好菜和调料。第一,你得有一个认证过的微信公众号,并且开通了微信支付功能,拿到你的商户号(mchid)公众号AppID。第二,在微信支付商户平台,你得设置你的APIv3密钥,这个密钥非常重要,后续回调解密全靠它,一定保管好,别泄露了。第三,你得下载商户API证书,包含apiclient_cert.pem(证书)和apiclient_key.pem(私钥)两个文件,同时记下证书的序列号(merchantSerialNumber)

后端我们使用SpringBoot,版本2.x或3.x都行。新建一个SpringBoot项目后,第一件事就是在pom.xml里引入微信支付官方提供的Java SDK。这里有个小建议,别自己去造轮子处理那些复杂的签名和HTTP请求,用官方SDK能省下至少80%的麻烦。依赖长这样:

<dependency> <groupId>com.github.wechatpay-apiv3</groupId> <artifactId>wechatpay-java</artifactId> <version>0.2.11</version> <!-- 写文章时最新版,你可以检查下有没有更新 --> </dependency>

前端我们用Uniapp,因为它能一套代码编译到多个平台,对于H5支付场景特别友好。你可以在HBuilderX里新建一个Uniapp项目,选择默认模板就行。关键是要确保你的项目能运行在微信公众号环境里,这意味着你需要配置JS接口安全域名,并在需要调用支付的页面引入微信的JS-SDK。不过别担心,Uniapp有现成的插件和API来处理这些,我们后面会具体说。

我建议你在项目里建立一个清晰的包结构。比如,后端可以建一个config包放微信支付的配置类,一个service包处理支付核心逻辑,一个controller包暴露API接口,再有一个util包放工具类(比如后面要用的AES解密工具)。前端则在pages目录下新建一个支付页面,逻辑清晰了,后面调试和维护都会轻松很多。

2. 后端核心:配置与下单接口

配置是第一步,也是最容易出错的一步。很多朋友在这里卡住,就是因为几个参数没搞对地方。我们新建一个配置类,比如叫WxPayConfig,用@ConfigurationProperties注解绑定配置文件中的属性是个好习惯,方便管理。核心是构建一个Config对象,微信支付Java SDK主要支持RSAAutoCertificateConfig这种自动管理平台证书的配置方式,非常省心。

import com.wechat.pay.java.core.Config; import com.wechat.pay.java.core.RSAAutoCertificateConfig; @Configuration public class WxPayConfig { @Value("${wxpay.merchant-id}") private String merchantId; @Value("${wxpay.merchant-serial-number}") private String merchantSerialNumber; @Value("${wxpay.private-key-path}") private String privateKeyPath; @Value("${wxpay.api-v3-key}") private String apiV3Key; @Value("${wxpay.appid}") private String appid; @Bean public Config payConfig() { // 这里强烈建议私钥文件放在resources/cert目录下,用ClassPathResource读取 // 避免写绝对路径,不然部署到服务器又要改 return new RSAAutoCertificateConfig.Builder() .merchantId(merchantId) .privateKeyFromPath(privateKeyPath) .merchantSerialNumber(merchantSerialNumber) .apiV3Key(apiV3Key) .build(); } }

你的application.yml里就要对应填上这些值。注意private-key-path,如果你把apiclient_key.pem放在了项目的src/main/resources/cert/目录下,这里可以写classpath:cert/apiclient_key.pemapiV3Key就是在商户平台设置的那个32位密钥。

配置搞定,接下来就是重头戏——创建预支付订单。我们在Controller里写一个接口,比如/api/pay/jsapi/order。这个接口接收前端传过来的商品信息、订单号、总金额(单位分)以及最重要的——当前支付用户的openid。这个openid必须是你公众号下用户的唯一标识,前端在微信公众号环境里通过微信JS-SDK可以获取到。

@RestController @RequestMapping("/api/pay") public class PayController { @Autowired private JsapiService jsapiService; // 注入JsapiService @Value("${wxpay.appid}") private String appid; @Value("${wxpay.merchant-id}") private String merchantId; @Value("${wxpay.notify-url}") private String notifyUrl; @PostMapping("/jsapi/order") public AjaxResult createJsapiOrder(@RequestBody OrderCreateDTO orderDTO, HttpServletRequest request) { // 1. 实际项目中,这里需要验证订单信息、用户身份等业务逻辑 // 2. 生成你自己的业务订单号,确保唯一性 String outTradeNo = "YOUR_ORDER_PREFIX_" + System.currentTimeMillis(); // 3. 构建微信支付请求对象 PrepayRequest request = new PrepayRequest(); Amount amount = new Amount(); amount.setTotal(orderDTO.getTotal()); // 总金额,单位分 amount.setCurrency("CNY"); request.setAmount(amount); request.setAppid(appid); request.setMchid(merchantId); request.setDescription(orderDTO.getDescription()); // 商品描述 request.setNotifyUrl(notifyUrl); // 支付结果回调地址,必须是公网能访问的HTTPS request.setOutTradeNo(outTradeNo); // 你的商户订单号 Payer payer = new Payer(); payer.setOpenid(orderDTO.getOpenid()); // 支付者openid request.setPayer(payer); // 4. 调用SDK,发起预支付 try { PrepayResponse response = jsapiService.prepay(request); String prepayId = response.getPrepayId(); // 预支付交易会话标识 // 这里最好把你自己的订单号和prepayId关联存储到数据库,后续回调有用 return AjaxResult.success("下单成功", prepayId); } catch (HttpException e) { // 处理微信支付返回的错误 log.error("微信支付下单失败,状态码: {}, 返回信息: {}", e.getHttpStatusCode(), e.getMessage()); return AjaxResult.error("支付下单失败: " + e.getMessage()); } catch (Exception e) { log.error("系统异常", e); return AjaxResult.error("系统异常"); } } }

这里我踩过一个坑,notifyUrl(支付回调地址)一定要填对,并且确保是HTTPS协议,能被外网访问。开发测试时,可以用内网穿透工具(如ngrok、花生壳)把本地服务暴露成一个公网HTTPS地址。另外,outTradeNo(商户订单号)自己系统生成,要保证唯一性,建议带上业务前缀和时间戳。

3. 前端交互:获取支付参数与调起支付

后端生成了prepay_id,这只是万里长征第一步。前端需要拿着这个prepay_id,再向你的后端请求一次,获取调起支付所需的全部参数(时间戳、随机串、签名等)。这是因为微信支付的签名必须由商户后端使用私钥完成,前端不能持有私钥。

在Uniapp的支付页面,我们首先调用第一个接口获取prepay_id。这里假设你已经通过微信授权拿到了用户的openid

// 在Uniapp的页面methods中 methods: { async createOrder() { // 假设这是你的商品信息 const orderInfo = { total: 1, // 金额,单位分,这里是1分钱,测试用 description: '测试商品', openid: this.userOpenid // 从全局状态或登录获取的openid }; const res = await this.$http.post('/api/pay/jsapi/order', orderInfo); if (res.code === 200) { const prepayId = res.data; // 拿到后端返回的prepay_id this.requestPaymentParams(prepayId); } else { uni.showToast({ title: '订单创建失败:' + res.msg, icon: 'none' }); } },

拿到prepay_id后,马上调用第二个后端接口,获取支付参数。这个接口需要做几件事:获取微信JS-SDK的jsapi_ticket(其实V3签名已经不需要这个了,但有些步骤保留着老的习惯,我们按最稳妥的来),生成随机字符串和时间戳,最关键的是使用商户私钥对特定字符串进行SHA256withRSA签名。

// 后端第二个接口:获取支付参数 @PostMapping("/jsapi/payParams") public AjaxResult getJsapiPayParams(@RequestBody PayParamsDTO paramsDTO) { String prepayId = paramsDTO.getPrepayId(); // 生成随机字符串和时间戳 String nonceStr = UUID.randomUUID().toString().replace("-", "").substring(0, 32); long timestamp = System.currentTimeMillis() / 1000; // 秒级时间戳 // 构建签名字符串,格式固定,非常重要! String signStr = this.appid + "\n" + timestamp + "\n" + nonceStr + "\n" + "prepay_id=" + prepayId + "\n"; // 使用商户私钥进行签名 String paySign = this.signWithPrivateKey(signStr); // 封装返回给前端的参数 Map<String, Object> map = new HashMap<>(); map.put("appId", this.appid); // 公众号AppId map.put("timeStamp", String.valueOf(timestamp)); // 必须转成字符串!苹果手机坑点 map.put("nonceStr", nonceStr); map.put("package", "prepay_id=" + prepayId); map.put("signType", "RSA"); map.put("paySign", paySign); // 支付签名 return AjaxResult.success(map); } // 签名工具方法 private String signWithPrivateKey(String data) throws Exception { // 这里读取你的apiclient_key.pem私钥文件内容 String privateKeyContent = ...; // 从配置文件或类路径读取 privateKeyContent = privateKeyContent.replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s", ""); // 去除所有空白字符 byte[] keyBytes = Base64.getDecoder().decode(privateKeyContent); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec); Signature signature = Signature.getInstance("SHA256withRSA"); signature.initSign(privateKey); signature.update(data.getBytes(StandardCharsets.UTF_8)); byte[] signed = signature.sign(); return Base64.getEncoder().encodeToString(signed); }

这里有个超级大坑!返回给前端的timeStamp字段,必须是**字符串(String)**类型,不能是数字(Number)。在安卓上可能没问题,但在iOS的微信环境里,如果传的是数字,会直接报错“缺少参数timeStamp”,支付窗口根本弹不出来。我当初就被这个问题坑了一下午,所以上面代码里特意用了String.valueOf(timestamp)

前端拿到这些参数后,就可以调用微信的JSAPI调起支付了。在Uniapp的H5环境里,我们需要判断是否在微信内,并确保微信JS-SDK已准备就绪。

// 接上面的 requestPaymentParams 方法 async requestPaymentParams(prepayId) { const res = await this.$http.post('/api/pay/jsapi/payParams', { prepayId: prepayId }); if (res.code === 200) { const payParams = res.data; // 调起微信支付 if (typeof WeixinJSBridge === 'undefined') { // 如果WeixinJSBridge未注入,监听其注入事件 if (document.addEventListener) { document.addEventListener('WeixinJSBridgeReady', () => { this.invokeWxPay(payParams); }, false); } } else { this.invokeWxPay(payParams); } } }, invokeWxPay(payParams) { WeixinJSBridge.invoke( 'getBrandWCPayRequest', { "appId": payParams.appId, // 公众号AppId "timeStamp": payParams.timeStamp, // 字符串时间戳 "nonceStr": payParams.nonceStr, // 随机字符串 "package": payParams.package, // 预支付标识,格式 prepay_id=xxx "signType": payParams.signType, // 签名类型,固定为RSA "paySign": payParams.paySign // 支付签名 }, (res) => { // 支付结果回调 if (res.err_msg === 'get_brand_wcpay_request:ok') { uni.showToast({ title: '支付成功!', icon: 'success' }); // 跳转到成功页面,或者查询本地订单状态 // 注意:这里前端返回成功并不代表支付最终成功,必须以服务端异步回调为准! } else if (res.err_msg === 'get_brand_wcpay_request:cancel') { uni.showToast({ title: '支付已取消', icon: 'none' }); } else { uni.showToast({ title: '支付失败:' + res.err_msg, icon: 'none' }); } } ); }

前端支付窗口弹出,用户输入密码完成支付,这前端的流程就算走完了。但对我们开发者来说,最重要的一环才刚刚开始——支付结果异步通知。

4. 支付回调处理与订单状态更新

用户支付成功后,微信支付服务器会向你在下单时设置的notifyUrl发起一个POST请求,通知你支付结果。这个回调是确保交易状态最终一致性的唯一可靠依据。前端那个get_brand_wcpay_request:ok只能作为引导用户操作的参考,绝不能作为更新订单状态的依据,因为网络波动或用户行为可能导致前端回调不可靠。

回调接口的编写有几个关键点:无需权限校验(微信的请求不带你的业务token)、快速响应(收到后先返回成功应答,再处理业务)、数据解密(V3回调的支付信息是加密的)、幂等性处理(同一条通知可能多次发送,你要能识别并避免重复更新订单)。

首先,微信会用一个JSON格式的请求体来调用你的接口,结构大概是这样:

{ "id": "EV-2018022511223320873", "create_time": "2015-05-20T13:29:35+08:00", "resource_type": "encrypt-resource", "event_type": "TRANSACTION.SUCCESS", "summary": "支付成功", "resource": { "original_type": "transaction", "algorithm": "AEAD_AES_256_GCM", "ciphertext": "...", // 加密的支付结果 "associated_data": "transaction", "nonce": "..." } }

我们的任务就是从resource对象里解密出真正的支付结果。解密需要用到你之前设置的apiV3Key。我强烈建议你封装一个工具类,比如叫AesUtil,专门处理AES-256-GCM解密。

@Slf4j @RestController @RequestMapping("/api/pay/notify") public class PayNotifyController { @Value("${wxpay.api-v3-key}") private String apiV3Key; @PostMapping("/jsapi") public Map<String, String> handleJsapiNotify(@RequestBody String notifyData) { log.info("收到微信支付回调,原始数据:{}", notifyData); Map<String, String> response = new HashMap<>(); try { // 1. 解析回调JSON JSONObject jsonObject = JSONObject.parseObject(notifyData); JSONObject resource = jsonObject.getJSONObject("resource"); String associatedData = resource.getString("associated_data"); String nonce = resource.getString("nonce"); String ciphertext = resource.getString("ciphertext"); // 2. 使用apiV3Key解密ciphertext String decryptData = new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8)) .decryptToString( associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext ); log.info("支付回调解密后数据:{}", decryptData); // 3. 解析解密后的交易数据 JSONObject transaction = JSONObject.parseObject(decryptData); String outTradeNo = transaction.getString("out_trade_no"); // 你的商户订单号 String transactionId = transaction.getString("transaction_id"); // 微信支付订单号 String tradeState = transaction.getString("trade_state"); // 交易状态 String successTime = transaction.getString("success_time"); // 支付完成时间 // 4. 首先返回成功应答给微信,避免微信重复通知 response.put("code", "SUCCESS"); response.put("message", "成功"); // 5. 异步处理你的业务逻辑(更新订单状态、发货等) // !!!重要:一定要根据outTradeNo查询本地订单,判断该订单是否已处理过(幂等性) // 如果已处理,直接跳过。如果未处理,再更新状态。 this.processOrderAfterPayment(outTradeNo, transactionId, tradeState, successTime); } catch (Exception e) { log.error("处理支付回调异常", e); response.put("code", "FAIL"); response.put("message", "处理失败"); } return response; } // 异步处理业务,可以用@Async或消息队列 private void processOrderAfterPayment(String outTradeNo, String transactionId, String tradeState, String successTime) { // 这里写你的业务逻辑:更新订单状态为已支付,记录微信订单号等 log.info("订单{}支付成功,微信订单号:{}, 支付时间:{}", outTradeNo, transactionId, successTime); // 注意:tradeState 可能是 "SUCCESS",但也可能是其他状态如 "REFUND"等,需要判断 if ("SUCCESS".equals(tradeState)) { // 更新你的数据库订单状态 // orderService.updateOrderStatusPaid(outTradeNo, transactionId, successTime); } } }

AesUtil工具类的代码和原始文章里提供的基本一致,就是使用Java的Cipher类,指定AES/GCM/NoPadding算法进行解密。这里一定要确保传入的apiV3Key是正确的32位密钥字节数组。解密成功后,你就能拿到明文的交易信息,里面包含了你的商户订单号(out_trade_no)、微信支付订单号(transaction_id)、金额(amount.total)、支付完成时间(success_time)等关键信息。

幂等性处理是核心。微信可能会因为网络等原因,对同一笔支付发送多次回调。你的业务逻辑必须能够识别重复通知。通常的做法是:在processOrderAfterPayment方法里,先根据out_trade_no去数据库查询订单当前状态。如果订单已经是“已支付”状态,那么直接忽略这次回调,记录日志即可;如果订单是“待支付”状态,才执行更新状态、记录流水等操作。这样可以保证你的订单数据不会因为重复回调而出错。

5. 常见问题排查与实战技巧

走通了整个流程,并不意味着万事大吉。在实际开发和线上运维中,你肯定会遇到各种各样的问题。我把这些年踩过的坑和解决方案总结一下,希望能帮你快速排雷。

问题一:证书加载失败,报“Invalid private key”或“文件找不到”。这通常是因为私钥文件路径不对或者格式有问题。首先,确保你的apiclient_key.pem文件内容是正确的,以-----BEGIN PRIVATE KEY-----开头。其次,路径问题很常见。如果你用privateKeyFromPath方法,开发环境可以用绝对路径,但生产环境建议把证书文件放在类路径下(比如resources/cert/),然后用ClassPathResource来读取文件流,再通过privateKeyFromInputStream方法加载,这样更灵活。

问题二:下单接口返回“APPID和商户号不匹配”。请百分百确认你下单时传入的appidmchid是配对的。appid是你的公众号AppID,mchid是你的微信支付商户号,它们必须在微信支付商户平台绑定关系。去商户平台“产品中心”-“AppID账号管理”里检查一下绑定关系。

问题三:前端调支付,一直提示“缺少参数”或“签名错误”。这是最高频的问题。请按以下清单逐一核对:

  1. 参数名大小写timeStamp是驼峰,nonceStr的‘S’是大写,package是全小写。一个字母都不能错。
  2. 参数类型:再次强调,timeStamp必须是字符串。即使后端生成了数字,传给前端前也要转成字符串。
  3. 签名串格式:后端生成paySign时拼接的字符串,必须是四行,以换行符\n结尾,顺序是appId\n时间戳\n随机串\nprepay_id=xxx\n。多一个空格少一个换行都会导致签名验证失败。你可以把拼接前的字符串打印出来,和微信官方文档的例子仔细比对。
  4. 私钥匹配:确保你签名用的私钥(apiclient_key.pem)和你在商户平台配置的APIv3密钥所属的证书是同一套。

问题四:支付回调收不到,或者解密失败。

  1. 收不到回调:检查notifyUrl是否是公网HTTPS地址。本地开发必须用内网穿透。检查服务器防火墙和安全组规则,是否放通了微信支付服务器IP段的入站请求(微信支付服务器IP段会变,最好在商户平台配置回调域名)。
  2. 解密失败:99%的原因是apiV3Key不对。去微信支付商户平台“API安全”里,确认你代码里用的apiV3Key和平台设置的一致。这个密钥是32位,可以重置,但重置后所有用到它的地方都要更新。

问题五:调试技巧。微信支付调试比较黑盒,我常用的方法是“抓包”和“打日志”。在后端每个关键节点(下单前参数、下单后响应、生成签名的字符串、回调接收的原始数据、解密后的数据)都打印详细的日志。对于前端,可以用微信开发者工具的“真机调试”功能,或者使用alertconsole.log在关键步骤输出参数,查看是否传递正确。另外,微信支付商户平台有“交易中心”和“API排查工具”,可以根据订单号查询支付状态和API调用日志,这是官方定位问题最直接的途径。

最后,关于代码安全,切记不要把商户号、APIv3密钥、证书文件等敏感信息硬编码在代码里或提交到Git。一定要使用配置文件(如application.yml),并且生产环境的配置文件通过环境变量或配置中心来管理。证书文件也可以考虑放在安全的文件服务器或密钥管理服务中。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/14 17:21:52

从源码解析WindowInsets:为什么你的fitsSystemWindows总不生效?

从源码解析WindowInsets&#xff1a;为什么你的fitsSystemWindows总不生效&#xff1f; 如果你在Android开发中尝试过沉浸式状态栏、全屏适配或者处理软键盘遮挡&#xff0c;那么android:fitsSystemWindows这个属性大概率让你头疼过。明明在布局文件里设置了true&#xff0c;状…

作者头像 李华
网站建设 2026/7/14 17:21:54

Super Qwen Voice World与Xshell集成的语音运维助手

Super Qwen Voice World与Xshell集成的语音运维助手 1. 引言 想象一下这样的场景&#xff1a;深夜两点&#xff0c;服务器突然告警&#xff0c;你睡眼惺忪地打开Xshell&#xff0c;手指在键盘上机械地敲打着排查命令。突然一个误操作&#xff0c;差点把生产环境给重启了——这…

作者头像 李华
网站建设 2026/7/14 17:21:37

Linux系统下向日葵远程控制工具的安装与常见问题解决

1. 为什么选择向日葵&#xff1f;聊聊Linux下的远程控制 如果你和我一样&#xff0c;是个长期和Linux打交道的开发者或者运维&#xff0c;肯定遇到过这样的场景&#xff1a;家里的主力开发机是Ubuntu&#xff0c;公司服务器是CentOS&#xff0c;有时候出门在外&#xff0c;突然…

作者头像 李华
网站建设 2026/7/14 17:21:53

避坑指南:Unity新版InputSystem的5个常见使用误区与正确姿势

避坑指南&#xff1a;Unity新版InputSystem的5个常见使用误区与正确姿势 如果你是从Unity的旧输入系统&#xff08;Input类&#xff09;迁移到新Input System的开发者&#xff0c;大概率已经体会到了新系统带来的强大与灵活。事件驱动、跨平台输入抽象、复合动作支持……这些特…

作者头像 李华