ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

微信支付V3实战:小程序JSAPI支付从签名到回调全流程

微信支付V3实战:小程序JSAPI支付从签名到回调全流程 这阵子帮一个小程序商城项目接微信支付V3的JSAPI支付第一版代码跑起来就吃了当头一棒——下单接口直接抛“缺少参数openid”后端日志看了一圈才发现自己把JSAPI和Native支付的参数要求搞混了。改成V3流程之后又接连踩了签名串拼错、回调解密失败、投诉回调忘接、退款重复通知等一堆坑前后折腾了差不多一个礼拜才算把整个支付闭环彻底打通。这篇文章就把这次实战从头到尾整理出来JAVA代码全部可编译可运行重点讲清楚为什么这么写、哪些参数容易踩雷、回调链路如何排查。如果你正准备给小程序接入JSAPI支付或者正被openid、签名、回调这几个点卡住这篇应该能帮你省下几天的排查时间。代码侧我会按几个核心模块来拆APIv3请求签名工具、小程序下单服务、回调验签解密处理器、小程序端参数拼装最后单独聊一聊投诉回调这个很多团队容易忽略的入口。技术栈用的是Spring Boot 3.x JDK 17没有引入官方SDK纯手工实现核心逻辑这样原理看得透将来出问题也好排查。1. 微信支付V3和小程序先搞清楚JSAPI和openid这层关系先说结论JSAPI支付是微信支付针对公众号、小程序场景提供的支付方式核心特征就是必须传openid。很多人第一次接V3习惯性去翻Native支付或者H5支付的文档结果自然对不上。1.1 JSAPI和Native、H5到底有什么区别微信支付按场景分成好几种开发时最容易混淆的是这三种支付方式使用场景是否依赖openid调起方式JSAPI微信内网页、小程序必须小程序端wx.requestPaymentNative扫码支付不需要返回二维码链接用户扫码H5微信外浏览器不需要返回mweb_url跳转微信APP支付所以当你的业务是“用户打开小程序在小程序内点支付”那毫无疑问是JSAPI。支付订单创建接口要用POST /v3/pay/transactions/jsapi参数里必须带上payer.openid。你要是像我之前一样想省事拿Native的代码改一改直接用微信就直接给你弹参数错误。1.2 openid为什么绕不开openid是用户在某个appid下的唯一标识可以理解成用户在小程序生态里的身份证号。微信支付需要凭借openid确认这笔订单归属哪个用户、从哪个小程序发起才能完成支付授权。每个用户在每个小程序下的openid都不同同一个用户在不同的小程序里openid不一样所以不存在“全局通用openid”的说法。openid不是你随便从哪拿的必须通过小程序登录流程获取前端wx.login拿到临时code后端拿这个code调用微信的code2Session接口换回openid和session_key。这里有个高频坑——code是一次性的用一次就失效官方有效期大约5分钟。很多人喜欢在前端把code存起来反复使用或者后端缓存了code去重复换openid结果就是偶尔能取到、偶尔报错非常诡异。正确做法是openid可以缓存复用code本身绝不能重复使用。1.3 V3和V2的差异为什么新项目直接选V3简单说V2时代签名用MD5或HMAC-SHA256接口返回XML回调解密用AES-128-ECB整体是一套偏老派的风格。V3全面改成RESTful风格请求返回JSON请求签名用商户私钥做SHA256withRSA验签用微信支付平台证书回调解密统一用AES-256-GCM。V3在安全性、扩展性上都比V2好很多而且微信支付新商户已经不再开放V2能力存量V2接口也在逐步迁移新项目没必要再往老路上走。2. 接入前的地基证书、密钥和回调域名的准备要点代码写得再好前置参数搞错了一样白搭。微信支付V3的接入参数有好几个必须一一对应才能跑通。2.1 必配参数清单参数获取位置用途商户号mchid商户平台首页下单、退款、对账都依赖AppID小程序后台标识小程序APIv3密钥商户平台-API安全AES-256-GCM回调数据解密商户API证书商户平台-API安全请求签名证书里含公钥和私钥商户证书序列号serial_no商户平台-API安全或证书文件读取请求头里标识商户身份小程序appSecret小程序后台code2Session换openid这里有几个细节很多教程不会专门提醒APIv3密钥是32字节字符串设置时不能和商户号、AppID相同否则平台直接拒绝。这个密钥是回调解密的唯一钥匙一旦泄露要人工申请重置周期不短所以生产环境一定要放到配置中心或密钥管理服务里绝不要写死在代码仓库。商户API证书下载后你会得到apiclient_cert.pem和apiclient_key.pem两个文件请求签名用的是apiclient_key.pem中的私钥。证书序列号可以通过openssl命令读取openssl x509 -in apiclient_cert.pem -noout -serial输出结果是十六进制大写字符串不带冒号填到配置里的时候注意别漏字符。2.2 回调域名与公网可达性V3的notify_url必须是HTTPS域名并且微信服务器要能访问到。不能填localhost也不能填IP地址。开发调试阶段很多人用内网穿透工具这个没问题但要确认穿透后的域名在微信侧能正常回推。需要注意回调地址一旦配置错误微信侧会按策略重试最多24次重试过程中你的系统如果已经标记了订单状态一定要做幂等不然会出现重复发货、重复发券的严重事故。小程序端本身的合法域名和白名单只影响你自己后端接口的request请求wx.login不校验合法域名微信支付接口的调用也不在这个限制范围内。如果你的项目涉及直播带货、虚拟支付等场景商品描述字段里的词要非常谨慎“充值”“代币”“永久会员”这类词都可能引起审核驳回微信对绝对化用语和虚拟支付资质审查都挺严格的别光顾着技术实现忽略了平台运营规则。2.3 订单号的幂等设计V3接口对同一商户号下的out_trade_no是幂等的同一个订单号重复请求不会重复扣款但是订单号一旦生成在有效期内不能被其他订单复用。这就意味着订单号不能用简单的“yyyyMMddHHmmss随机数”这种拼法并发量上来很容易撞车。我一般用雪花算法或数据库自增业务前缀来生成确保全局唯一。金额字段设计上一定用Integer/Long表示“分”不要用Double表示“元”浮点精度问题会在对账环节给你挖大坑。3. JAVA侧动手签名、下单与小程序参数拼装这章直接给能跑的代码。我会先实现一个完整的APIv3签名工具再做下单服务和前端调起参数的拼装最后封装一个简单的HTTP客户端。3.1 配置类与配置文件先定义配置属性类把商户参数统管起来Component ConfigurationProperties(prefix wechat.pay) Data public class WechatPayConfig { private String appId; private String mchId; private String apiV3Key; private String privateKeyPath; private String serialNo; private String notifyUrl; private String refundNotifyUrl; }application.yml中对应配置wechat: pay: app-id: wx1234567890abcdef mch-id: 1900000001 api-v3-key: 你的32位密钥字符串 private-key-path: /etc/wxpay/certs/apiclient_key.pem serial-no: 1FB3D6B959BDF5C13A223E535D6DC5F0C3E7F6 notify-url: https://api.example.com/pay/notify refund-notify-url: https://api.example.com/pay/refundNotify私钥路径我习惯放到classpath之外的固定目录比如/etc/wxpay/certs并用环境变量或配置中心注入部署到服务器后不会因为相对路径问题找不到文件。3.2 APIv3请求签名工具微信支付V3的每个接口请求都要携带Authorization请求头格式固定WECHATPAY2-SHA256-RSA2048 mchid1900000001,nonce_str随机字符串,timestamp1710000000,serial_no证书序列号,signature签名值签名内容由五个字段拼接而成HTTP方法\n 请求路径\n 时间戳\n 随机字符串\n 请求体\n请求体是POST请求的原始JSON字符串GET请求时为空字符串。然后用商户私钥做SHA256withRSA签名签名结果base64编码后填入signature。public class WechatPayV3SignUtil { private static final MapString, PrivateKey PRIVATE_KEY_CACHE new ConcurrentHashMap(); public static PrivateKey loadPrivateKey(String privateKeyPath) throws Exception { return PRIVATE_KEY_CACHE.computeIfAbsent(privateKeyPath, path - { try { String content new String(Files.readAllBytes(Paths.get(path))) .replace(-----BEGIN PRIVATE KEY-----, ) .replace(-----END PRIVATE KEY-----, ) .replaceAll(\\s, ); byte[] keyBytes Base64.getDecoder().decode(content); PKCS8EncodedKeySpec spec new PKCS8EncodedKeySpec(keyBytes); KeyFactory kf KeyFactory.getInstance(RSA); return kf.generatePrivate(spec); } catch (Exception e) { throw new RuntimeException(加载商户私钥失败, e); } }); } public static String buildMessage(String method, String urlPath, String timestamp, String nonceStr, String body) { return method \n urlPath \n timestamp \n nonceStr \n body \n; } public static String sign(PrivateKey privateKey, String message) throws Exception { Signature signer Signature.getInstance(SHA256withRSA); signer.initSign(privateKey); signer.update(message.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(signer.sign()); } public static String buildAuthHeader(String mchId, String serialNo, String nonceStr, String timestamp, String signature) { return WECHATPAY2-SHA256-RSA2048 mchid\ mchId \, nonce_str\ nonceStr \, timestamp\ timestamp \, serial_no\ serialNo \, signature\ signature \; } }我把私钥加载做了缓存避免每次请求都重新读文件。这里有一个特别容易翻车的点私钥文件里有换行符和头尾标记如果直接用字符串替换不干净解析会失败。上面的代码把\s全部去掉保证base64解码不出问题。3.3 小程序下单服务核心下单接口是POST /v3/pay/transactions/jsapi请求体关键字段如下{ appid: wx1234567890abcdef, mchid: 1900000001, description: 测试商品-普通版, out_trade_no: 20240301000001, notify_url: https://api.example.com/pay/notify, amount: { total: 100, currency: CNY }, payer: { openid: oUpF8uMuAJO_M2pxb1Q9zNjWeS6o } }amount.total单位是分1元等于100分必须是整数。payer.openid就是前面说的小程序用户openid。notify_url是支付结果回调地址必须和后端实际暴露的接口完全一致。Service RequiredArgsConstructor public class JsapiPayService { private final WechatPayConfig config; private final WechatPayV3HttpClient httpClient; public JsapiPayResponse createOrder(String openid, String outTradeNo, Integer totalFee, String desc) throws Exception { MapString, Object amount new HashMap(); amount.put(total, totalFee); amount.put(currency, CNY); MapString, Object payer new HashMap(); payer.put(openid, openid); MapString, Object body new HashMap(); body.put(appid, config.getAppId()); body.put(mchid, config.getMchId()); body.put(description, desc); body.put(out_trade_no, outTradeNo); body.put(notify_url, config.getNotifyUrl()); body.put(amount, amount); body.put(payer, payer); String respBody httpClient.post(/v3/pay/transactions/jsapi, body); JSONObject json JSON.parseObject(respBody); String prepayId json.getString(prepay_id); return buildAppletPayParams(prepayId); } }下单成功会返回prepay_id这个参数是后面调起支付的核心凭证。3.4 小程序端调起支付参数拼装拿到prepay_id后不能直接把prepay_id丢给小程序。小程序端wx.requestPayment需要的参数是timeStamp、nonceStr、package、signType、paySign五个其中paySign需要后端用商户私钥再签一次名。private JsapiPayResponse buildAppletPayParams(String prepayId) throws Exception { long timestamp System.currentTimeMillis() / 1000; String nonceStr UUID.randomUUID().toString().replace(-, ); String pkg prepay_id prepayId; String message config.getAppId() \n timestamp \n nonceStr \n pkg \n; String paySign WechatPayV3SignUtil.sign( WechatPayV3SignUtil.loadPrivateKey(config.getPrivateKeyPath()), message); JsapiPayResponse response new JsapiPayResponse(); response.setTimeStamp(String.valueOf(timestamp)); response.setNonceStr(nonceStr); response.setPackage(pkg); response.setSignType(RSA); response.setPaySign(paySign); return response; }这里有个细节我反复强调过签名字符串里的第一行是appId不是appid大小写跟下单接口不一样很多人直接照抄下单的字段名结果小程序端一直报签名校验失败。package字段必须写成prepay_idxxx的完整格式不要只传prepay_id的值。3.5 封装一个轻量HTTP客户端我不喜欢在核心模块里引入一大堆依赖直接用JDK自带的java.net.http.HttpClientComponent RequiredArgsConstructor public class WechatPayV3HttpClient { private final WechatPayConfig config; public String post(String urlPath, Object body) throws Exception { String bodyJson JSON.toJSONString(body); long timestamp System.currentTimeMillis() / 1000; String nonce UUID.randomUUID().toString().replace(-, ); String message WechatPayV3SignUtil.buildMessage(POST, urlPath, String.valueOf(timestamp), nonce, bodyJson); String sign WechatPayV3SignUtil.sign( WechatPayV3SignUtil.loadPrivateKey(config.getPrivateKeyPath()), message); String authHeader WechatPayV3SignUtil.buildAuthHeader( config.getMchId(), config.getSerialNo(), nonce, String.valueOf(timestamp), sign); HttpClient client HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(https://api.mch.weixin.qq.com urlPath)) .header(Authorization, authHeader) .header(Content-Type, application/json) .header(Accept, application/json) .POST(HttpRequest.BodyPublishers.ofString(bodyJson, StandardCharsets.UTF_8)) .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() 300) { throw new RuntimeException(微信支付接口调用失败: response.body()); } return response.body(); } }关于签名一致性要特别强调代码里先序列化出bodyJson再用同一个bodyJson去拼签名内容和发送请求体。如果你先用一个JSON字符串去签名发送的时候又做了一次序列化两次结果在字段顺序或空格上不一致微信验签就会失败。4. 回调通知验签、解密与业务状态落库支付成功后微信会往notify_url推一条POST请求这个环节出的问题最隐蔽。很多人支付流程看着通了订单状态却一直不变十有八九是回调链路没处理好。4.1 回调报文长什么样微信支付V3的通知报文是统一结构{ id: EV-20240301120000, create_time: 2024-03-01T12:00:0008:00, resource_type: encrypt-resource, event_type: TRANSACTION.SUCCESS, summary: 支付成功, resource: { original_type: transaction, algorithm: AEAD_AES_256_GCM, ciphertext: BASE64密文, associated_data: transaction, nonce: 随机串 } }event_type常见的几种event_type含义TRANSACTION.SUCCESS支付成功REFUND.SUCCESS退款成功REFUND.ABNORMAL退款异常complaint.COMPLAINT_CREATE新投诉创建complaint.COMPLAINT_NOTIFY投诉状态变化很多人只写了支付成功回调退款回调、投诉回调一直空着一旦用户发起退款或投诉系统完全收不到通知处理时效就失控了。4.2 验签判断通知真的来自微信微信支付V3要求验证回调请求的签名防止伪造通知。验签需要微信支付平台证书或平台公钥。回调请求头里有四个关键字段Wechatpay-Timestamp Wechatpay-Nonce Wechatpay-Signature Wechatpay-Serial验签的message格式是时间戳\n 随机串\n 请求体\n然后用对应serial_no的微信支付平台公钥做SHA256withRSA验签。public boolean verifyWechatSign(String timestamp, String nonce, String requestBody, String signature, String serialNo) { try { String message timestamp \n nonce \n requestBody \n; Certificate certificate platformCertificateProvider.getCertificate(serialNo); Signature verifier Signature.getInstance(SHA256withRSA); verifier.initVerify(certificate.getPublicKey()); verifier.update(message.getBytes(StandardCharsets.UTF_8)); return verifier.verify(Base64.getDecoder().decode(signature)); } catch (Exception e) { log.error(微信回调验签失败, e); return false; } }这里的难点在于微信支付平台证书会轮换。生产环境必须实现证书缓存和更新机制可以通过GET /v3/certificates接口主动拉取最新证书列表定时刷新。不要写死一个证书否则证书过期后回调全部验签失败线下排查半天找不出原因。4.3 AES-256-GCM解密验签通过后resource字段里的ciphertext是AES-256-GCM加密的密文需要用APIv3密钥解密。public static String decryptToString(byte[] apiV3Key, String associatedData, String nonce, String ciphertext) throws Exception { try { Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); SecretKeySpec key new SecretKeySpec(apiV3Key, AES); GCMParameterSpec spec new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8)); cipher.init(Cipher.DECRYPT_MODE, key, spec); cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8)); return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), StandardCharsets.UTF_8); } catch (Exception e) { throw new SecurityException(AES解密失败, e); } }解密后的明文是交易数据JSON里面包含out_trade_no、transaction_id、payer、amount等关键字段。这里有一个新手常犯的错直接用解密后的金额和本地订单比较时要区分amount.total和payer.total_fee前者是订单金额后者是用户实付金额部分场景下两者不一致。4.4 订单状态更新的完整流程正确的回调处理流程应该是验签失败直接返回4xx解密resource解析交易数据根据out_trade_no查本地订单校验订单金额和状态是否一致使用分布式锁防止并发重复处理更新订单状态为支付成功触发后续的发货、开卡等业务返回给微信200响应体为{code:SUCCESS,message:成功}这里最忌讳的是“先改状态再验签”或者“捕获所有异常直接返回SUCCESS”。微信的重试机制是收到非200响应或超时会按递增间隔重试24小时内最多重试24次。如果你因为业务异常返回了500微信会继续重推如果你捕获了业务异常还返回SUCCESS那订单状态就永远不会更新了。所以我的建议是业务处理失败时返回5xx让微信重试但每一次重试都要有幂等保护。同一个out_trade_no的重复通知直接查订单状态已经支付成功就返回SUCCESS不再触发业务逻辑。4.5 投诉回调合规红线不能拖投诉回调是很多人做完支付就完全忘掉的一环。微信支付要求商户在收到投诉后尽快处理否则会影响商户评分甚至支付权限。投诉通知的报文和解密方式和支付回调完全一样只是event_type是complaint.COMPLAINT_CREATE等。解密后的数据里有complaint_id、openid、complaint_state、amount等字段。接入投诉回调后你的系统可以自动生成工单分配给客服处理还可以调用投诉回复接口主动给用户留言。这块做得好不仅能避免商户评级下降还能减少用户升级投诉的概率。我之前遇到一个项目上线三个月没接投诉回调某天用户集中投诉积分商品未到账系统毫无感知最后被平台警告才紧急补救。5. 小程序端配合与联调经验后端接口全部就绪后小程序端的调用反而简单但几个配合细节必须对齐。5.1 小程序端核心代码Page({ onClickPay() { const openid wx.getStorageSync(openid); if (!openid) { wx.showToast({ title: 未获取到openid请重新登录, icon: none }); return; } wx.request({ url: https://api.example.com/pay/jsapi, method: POST, data: { openid, orderId: this.data.orderId }, success: (res) { if (res.data.code ! 0) { wx.showToast({ title: res.data.msg, icon: none }); return; } const payParams res.data.data; wx.requestPayment({ timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, signType: payParams.signType, paySign: payParams.paySign, success: () { wx.showToast({ title: 支付成功, icon: success }); this.loadOrderDetail(); }, fail: (err) { if (err.errMsg err.errMsg.includes(cancel)) { wx.showToast({ title: 已取消支付, icon: none }); } else { wx.showToast({ title: 支付失败, icon: none }); } } }); } }); } });timeStamp官方要求是字符串不是数字后端已经toString处理过前端不要再转一次。package字段直接用后端返回的完整字符串不要自己拼。signType固定为RSA这是V3平台的算法标识。5.2 后端接口的安全设计后端下单接口不要接受前端传过来的openid当作可信数据正确做法是前端传登录code或者其他会话凭证后端调code2Session重新换取openid。PostMapping(/jsapi) public ResultJsapiPayResponse jsapiPay(RequestBody JsapiPayRequest request) { String openid sessionService.getOpenidBySessionKey(request.getSessionKey()); Order order orderService.getById(request.getOrderId()); return Result.success(payService.createOrder(openid, order.getOutTradeNo(), order.getTotalFee(), order.getTitle())); }如果前端能随意传openid就意味着任意用户可以用他人的openid发起支付请求虽然最终支付还是要用户本人操作但订单归属就乱套了。支付场景的安全设计从入口就要兜住。5.3 联调时的三个关键观察点第一看后端日志的下单接口返回确认prepay_id能拿到。拿不到就先解决签名、参数问题。第二看wx.requestPayment的fail回调如果报签名错误优先检查后端拼paySign时的appId字段和签名串格式。第三支付成功后等几秒看回调日志确认notify_url收到微信请求再确认解密和订单更新正常。这三个观察点分别对应了下单链路、前端调起链路、异步回调链路任何一个环节出错都能快速定位。6. 高频踩坑从openid缺失到消息重复推送最后把这次实战和过去几个项目里真实遇到的坑集中复盘一遍每个问题都会附排查链路照着走能省不少时间。6.1 openid缺失或无效报错信息类似 {code:PARAM_ERROR,message:openid参数缺失或错误}排查链路确认调用的是/v3/pay/transactions/jsapi不是/native。确认请求体里payer.openid字段存在且不是空字符串。确认openid对应的appid和下单参数里的appid一致。小程序A的openid不能在小程序B的支付里用。确认openid是实时通过code2Session换取的最新值不是从缓存里翻出来的旧值。最容易踩坑的是第4条。小程序切换账号登录后openid会变如果前端只用第一次登录的openid做缓存用户换号后支付就会失败。6.2 APIv3签名失败报错信息类似 {code:SIGN_ERROR,message:sign error}排查链路检查商户私钥加载是否完整。私钥文件内容复制时丢失换行会导致解析失败。检查serial_no是否和正在使用的证书匹配。证书更新后老的serial_no必须同步换掉。检查签名时使用的body字符串和实际发送的请求体是否完全一致。字段顺序不影响但内容必须一致。检查系统时间是否正确。签名timestamp和请求头timestamp是同一个值系统时间偏差超过一定范围会被判定为请求过期。服务器统一用NTP同步时间。检查请求头格式逗号和引号必须是英文半角。6.3 支付成功但回调没收到表现是用户钱付了订单状态却一直是待支付。排查链路确认notify_url在公网可以被访问微信侧能正常请求。可以先手动访问一下看看通不通。确认notify_url的HTTPS证书是正规机构签发的微信不会请求自签名证书的地址。检查回调代码里有没有验签失败直接抛异常的逻辑。验签失败时先确认自己的验签实现是否正确不要一失败就return false否则微信重试再多也进不来。在回调入口加日志记录请求来源IP、请求头、原始报文。微信服务器IP段是固定的日志能帮助你判断请求是否真的到达。如果服务有多个环境dev、test、prod确认回调配置指向的是当前环境。6.4 回调重复推送导致重复发货微信支付V3的通知机制是“尽力而为”不保证只推一次。同一笔订单可能因为网络超时等原因收到多次回调。代码里必须做幂等public PayNotifyResult handlePayNotify(JSONObject notifyData) { String eventType notifyData.getString(event_type); if (!TRANSACTION.SUCCESS.equals(eventType)) { return PayNotifyResult.fail(未知事件类型); } String plainText wechatPayAesUtil.decrypt(notifyData.getJSONObject(resource)); JSONObject transaction JSON.parseObject(plainText); String outTradeNo transaction.getString(out_trade_no); return transactionService.handlePaidOrder(outTradeNo, transaction.getLong(transaction_id)); }幂等处理的核心是状态机订单状态从“待支付”到“支付成功”只允许转换一次。如果订单已经是支付成功状态再次收到通知直接返回SUCCESS不要再走一遍发奖逻辑。可以使用数据库唯一约束、Redis分布式锁或者乐观锁版本号来控制。6.5 退款的回调处理退款接口是POST /v3/refund/domestic/refunds请求体里需要原订单号或微信支付单号、退款单号、退款金额、原订单金额。退款单号out_refund_no也要保证全局唯一不能和订单号相同。退款金额不能大于原订单金额。退款成功回调的event_type是REFUND.SUCCESS解密后的数据里有refund_status状态为SUCCESS才代表退款真正完成。一定要把支付回调和退款回调分开处理不能统一当成支付成功事件。退款回调处理失败同样会被微信重试也需要幂等。6.6 证书轮换的运维策略商户API证书和微信支付平台证书都有有效期。商户API证书过期前微信会发站内信和短信提醒提前更新后serial_no会变化。如果支付服务部署在多个节点建议把证书序列号、私钥信息放到配置中心统一管理支持动态刷新。这样切换到新证书时可以逐个节点灰度避免一次性替换导致部分请求签名校验失败。最后说点实际经验这套支付代码前后服务过三四个小程序项目从最早图省事用官方SDK到后来全部改成自己的实现最大的感受是微信支付V3的难点不在接口参数多复杂而在异步链路的理解上。下单只是开始回调验签解密、退款状态机、投诉工单联动这些才是串联整个支付闭环的核心。如果你刚上手建议先不要急着引SDK把签名工具、下单服务、回调验签解密这几个方法各写一个单元测试用微信官方提供的模拟数据跑通。尤其是AES-256-GCM解密和RSA验签一旦这两个方法有bug线上问题排查成本远高于测试阶段多写两行用例的成本。另外在正式上线前一定把“重复回调”“退款通知”“投诉通知”这三个场景都模拟一遍确认系统的幂等和异常处理没问题再放量。希望这篇实战整理能帮你少走点弯路。
返回列表