简介这是一份面向Java开发者的微信支付V3版工具类资源覆盖微信支付、退款、交易状态查询以及企业打款到个人零钱旧版接口等核心场景。资源基于作者企业项目实战封装调用方只需传入对应参数即可快速接入省去重复对接官方接口的繁琐流程适合需要快速集成微信支付能力的后端工程或个人项目参考。压缩包共7个文件包含5个Java源码类、1个IntelliJ模块描述文件iml以及1个XML配置文件如Maven依赖或Spring扫描配置整体仅11KB代码轻量、结构清晰便于直接阅读和移植到现有业务项目中。目前已有2277人学习下载说明这套工具类在同类场景下具备一定参考价值。通过阅读源码可掌握微信支付V3版签名、下单、回调验签、退款与状态轮询等关键实现思路同时旧版企业打款逻辑也能为处理零钱转账需求提供对照与排错启发。1. 微信支付V3工具类真金白银的交易代码封装比业务设计更重要接手企业支付系统时最怕的不是微信支付API文档读不懂而是文档更新太快、签名版本太多、参数稍错就报错。我拆过不少开源支付项目坦白说很多写得像能跑就行等到对接退款、回调验签、企业打款时才发现坑一个接一个。这次拆的这套Java微信支付工具类V3版是一线企业项目里沉淀下来的封装覆盖了微信支付V3、微信退款V3、交易状态查询、企业打款到零钱旧版四条链路调用方只管传参签名、证书、报文封装全部黑匣子化。适不适合你如果你是后端开发项目要接微信支付或者你正在找一套可以直接塞进Spring Boot项目的支付封装这篇笔记能把边界、参数和踩坑一次讲透。2. 微信支付V3的核心APIv3签名、证书序列号与HTTP客户端选型2.1 微信支付V3和V2的区别为什么必须用V3这套工具类微信支付V3版在2019年后全面推行和V2最大的差别在于三件事接口路径带版本号且全部走HTTPS、签名机制从MD5升级为WECHATPAY2-SHA256-RSA2048、证书体系从API证书平台证书双证书变成API私钥签名平台证书验签。这套工具类直接按V3标准实现方法内部封装了HttpClient请求、签名生成、响应验签三步。// 初始化微信支付V3配置 WxPayV3Config config new WxPayV3Config(); config.setMchId(商户号); config.setApiV3Key(APIv3密钥32位); config.setPrivateKeyPath(/cert/apiclient_key.pem); config.setSerialNo(商户API证书序列号); config.setPlatformCertPath(/cert/platform_cert.pem);代码逻辑说明配置类负责把商户号、APIv3密钥、API私钥和平台证书路径集中管理所有请求方法内部通过这个config构建签名头和验签逻辑。参数说明serialNo是商户API证书的序列号不是证书文件本身在微信商户平台-账户中心-API安全里能看到platformCertPath是微信平台证书用于验证微信响应的签名首次对接时需要从平台下载或者通过证书下载接口获取。2.2 签名生成与请求封装照抄会翻车理解才行V3签名生成的规则是用请求方法GET/POST、请求路径含query string、时间戳、随机串、请求体POST时拼接成待签名字符串然后用商户API私钥做SHA256withRSA签名。这套工具类把签名逻辑封装在WxPayV3SignUtil里返回的Authorization头格式固定为WECHATPAY2-SHA256-RSA2048 mchid...,nonce_str...,signature...,timestamp...,serial_no...。public String buildAuthorizationHeader(String method, String urlPath, String body) throws Exception { long timestamp System.currentTimeMillis() / 1000; String nonceStr UUID.randomUUID().toString().replaceAll(-, ); String message method \n urlPath \n timestamp \n nonceStr \n body \n; Signature signature Signature.getInstance(SHA256withRSA); signature.initSign(privateKey); signature.update(message.getBytes(StandardCharsets.UTF_8)); String signStr Base64.getEncoder().encodeToString(signature.sign()); return String.format(WECHATPAY2-SHA256-RSA2048 mchid\%s\,nonce_str\%s\,timestamp\%d\,serial_no\%s\,signature\%s\, mchId, nonceStr, timestamp, serialNo, signStr); }代码逻辑说明message的拼接顺序是方法、换行、路径、换行、时间戳、换行、随机串、换行、请求体、换行。注意GET请求和无body的POST请求body部分传空字符串但仍然要拼一个空串加换行。参数说明urlPath只包含路径和查询参数不包含域名timestamp必须和实际请求时间一致偏差超过五分钟会被拒绝。2.3 平台证书验签微信响应也要验否则有重放风险很多人在对接V3时只做了请求签名没做响应验签。微信支付的响应头里有Wechatpay-Timestamp、Wechatpay-Nonce、Wechatpay-Signature、Wechatpay-Serial四个字段需要用微信平台证书对响应体做验签。这套工具类在每个请求方法内部自动完成验签如果验签失败直接抛出异常。public boolean verifyWechatResponse(String body, String timestamp, String nonce, String signature) { String message timestamp \n nonce \n body \n; try { Signature verifySignature Signature.getInstance(SHA256withRSA); verifySignature.initVerify(platformPublicKey); verifySignature.update(message.getBytes(StandardCharsets.UTF_8)); return verifySignature.verify(Base64.getDecoder().decode(signature)); } catch (Exception e) { log.error(微信响应验签失败, e); return false; } }代码逻辑说明验签的message格式是时间戳、换行、随机串、换行、响应体、换行和请求签名格式不同。platformPublicKey是从平台证书里解析出来的公钥证书文件本身是一个PEM格式的Base64编码块。参数说明这里的timestamp和nonce必须取响应头里的值不能从业务参数里拿验签失败时优先检查平台证书是否过期微信平台证书每五年轮换一次但接口地址不变。3. 下单与支付微信支付V3版工具类的调用方式和参数边界3.1 统一下单JSAPI支付小程序和公众号支付的核心入口这套工具类里最常用的方法是统一下单对应微信支付V3的/v3/pay/transactions/jsapi接口。调用方只需要传入订单号、金额、用户openId和回调地址工具类内部完成请求封装和签名返回prepay_id后再封装成前端需要的支付参数。MapString, Object orderParams new HashMap(); orderParams.put(appid, 公众号或小程序appid); orderParams.put(mchid, config.getMchId()); orderParams.put(description, 测试商品); orderParams.put(out_trade_no, 20250101120000); orderParams.put(notify_url, https://yourdomain.com/api/pay/notify); MapString, Object amount new HashMap(); amount.put(total, 1); // 单位分 amount.put(currency, CNY); orderParams.put(amount, amount); MapString, Object payer new HashMap(); payer.put(openid, 用户的openid); orderParams.put(payer, payer); String prepayId wxPayV3Service.jsapiPay(orderParams); MapString, String payParams wxPayV3Service.buildJsapiPayParams(prepayId);代码逻辑说明金额单位是分total1表示一分钱out_trade_no必须是商户系统内的唯一单号重复会直接报错payer.openid是用户在小程序或公众号下的openid与appid对应。buildJsapiPayParams方法内部会重新用appId、时间戳、随机串、prepayId拼接签名参数返回给前端调wx.requestPayment。参数说明notify_url必须是公网能访问的HTTPS地址微信会携带订单数据做POST回调如果回调地址不通订单会一直处于待支付状态需要主动调用查询接口确认。3.2 App支付与Native支付同工具类、不同接口地址这套工具类的设计思路是所有支付方式共用签名和验签逻辑只是换了URL路径和返回参数的组装方式。App支付调的是/v3/pay/transactions/app参数里没有payer.openid而是返回prepay_id后需要拼一个App端专用的签名串Native支付调的是/v3/pay/transactions/native返回的是二维码链接code_url网关上转成二维码图片给用户扫。MapString, Object nativeOrderParams new HashMap(); nativeOrderParams.put(appid, appid); nativeOrderParams.put(mchid, config.getMchId()); nativeOrderParams.put(description, PC扫码支付订单); nativeOrderParams.put(out_trade_no, orderNo); nativeOrderParams.put(notify_url, notifyUrl); MapString, Object nativeAmount new HashMap(); nativeAmount.put(total, orderAmount); nativeAmount.put(currency, CNY); nativeOrderParams.put(amount, nativeAmount); String codeUrl wxPayV3Service.nativePay(nativeOrderParams);代码逻辑说明nativePay方法在工具类内部走了与jsapiPay几乎相同的请求链路只是URL从/jsapi换成了/native返回的字段从prepay_id变成了code_url。注意native支付不需要openid适合PC端扫码场景。参数说明code_url的有效期是2小时超时后需要重新下单如果商户后台配置了多个回调地址以订单参数里的notify_url为准。3.3 回调通知处理验签、解密、返回应答一个不能少这是支付对接里最容易出问题的一段。微信支付V3的回调通知body是加密的需要用APIv3密钥做AEAD_AES_256_GCM解密解密后才能拿到订单数据。工具类里封装了decodeNotifyBody方法同时自动完成响应头验签防止伪造回调。public WxPayNotifyResult parseNotify(String body, MapString, String headers) throws Exception { // 1. 验签 boolean verified verifyWechatResponse(body, headers.get(Wechatpay-Timestamp), headers.get(Wechatpay-Nonce), headers.get(Wechatpay-Signature)); if (!verified) { throw new PayException(回调验签失败); } // 2. 解密 JSONObject notifyData JSON.parseObject(body); String ciphertext notifyData.getJSONObject(resource).getString(ciphertext); String nonce notifyData.getJSONObject(resource).getString(nonce); String associatedData notifyData.getJSONObject(resource).getString(associated_data); String plaintext AesGcmUtil.decrypt(ciphertext, nonce, associatedData, apiV3Key); // 3. 业务处理 return parsePlaintext(plaintext); }代码逻辑说明解密后的plaintext里有out_trade_no、trade_state、transaction_id等字段拿到后先更新本地订单状态再返回给微信应答。返回应答必须是{code:SUCCESS,message:成功}HTTP状态码200否则微信会按照间隔重试回调。参数说明回调验签时headers的key大小写在不同网关下可能不同建议取出后统一转小写再匹配解密时associated_data可能为空字符串但参数必须传。3.4 交易状态查询确认订单真实状态的兜底方案回调通知是异步的存在丢包、网络超时的可能。工具类封装了/v3/pay/transactions/out-trade-no/{out_trade_no}查询接口入参只需要商户订单号返回订单状态、支付金额、付款人等信息。public WxPayOrderQueryResult queryOrder(String outTradeNo) { String urlPath /v3/pay/transactions/out-trade-no/ outTradeNo ?mchid config.getMchId(); String response getRequest(urlPath); return JSON.parseObject(response, WxPayOrderQueryResult.class); }代码逻辑说明查询接口是GET请求没有请求体但签名时body部分传空字符串。mchid必须作为query参数拼在URL后面且会被包含在待签名字符串里。参数说明trade_state取值有SUCCESS、REFUND、NOTPAY、CLOSED、REVOKED、USERPAYING等模拟支付环境下USERPAYING状态可能会卡住不建议在测试时校验太严查询接口有频率限制一般订单确认用回调定时主动查兜底不要每次请求都查微信。4. 退款与撤销微信退款V3的幂等性、金额校验和原路退回逻辑4.1 申请退款V3接口参数比支付多校验比支付严微信退款V3接口是/v3/refund/domestic/refunds请求参数里需要原商户订单号、退款单号、退款金额、原订单金额和退款原因。工具类封装好之后业务侧只需传四个业务参数。MapString, Object refundParams new HashMap(); refundParams.put(out_trade_no, 原交易商户订单号); refundParams.put(out_refund_no, 退款单号商户系统内唯一); MapString, Object refundAmount new HashMap(); refundAmount.put(refund, 1); // 退款金额单位分不能大于原订单金额 refundAmount.put(total, 100); // 原订单金额单位分 refundAmount.put(currency, CNY); refundParams.put(amount, refundAmount); refundParams.put(reason, 用户申请退款); String refundId wxPayV3Service.refund(refundParams);代码逻辑说明out_refund_no用于幂等同一个退款单号重复调用不会生成多条退款而是返回同一个退款结果。金额部分比较坑refund是本次要退的金额total是原订单金额如果refund加已退金额大于total微信直接报错。参数说明reason选填但建议传平台审核和用户账单都会展示部分退款时可以多次调用但每次的out_refund_no必须不同且累计退款不能超过total。4.2 退款状态查询异步结果必须主动拉别等回调退款结果也是异步通知和支付回调一样走/v3/refund/domestic/refunds/{out_refund_no}查询接口。工具类里封装了queryRefund方法返回的status字段才是退款最终状态。public WxRefundQueryResult queryRefund(String outRefundNo) { String urlPath /v3/refund/domestic/refunds/ outRefundNo; String response getRequest(urlPath); return JSON.parseObject(response, WxRefundQueryResult.class); }代码逻辑说明退款查询不需要额外的query参数路径里带上退款单号即可。status取值有SUCCESS、CLOSED、PROCESSING、ABNORMALSUCCESS才是真正退到用户账户PROCESSING不能视为成功。参数说明退款到微信零钱一般是实时到账但部分银行通道会延迟到T1PROCESSING状态持续24小时以上时需要人工介入退款回调通知里refund_status为空时必须主动查询。4.3 退款结果的边界问题原路退回、余额不足、关单失败企业项目里退款最容易踩三个边界问题。第一个是退款金额精度微信支付金额单位是分如果业务系统存的是元用double计算0.10.2等于0.30000000000000004转分时四舍五入会差一分钱真实遇到过用户投诉第二个是原路退回限制退款必须走原支付渠道如果用户支付时用了零钱银行卡的组合支付退款也是分渠道退的查询结果里有promotion_detail数组描述每张代金券和渠道的退款情况第三个是关单与退款共存订单先关单后不能退款必须先查询订单状态再决定走退款还是关单流程。5. 避坑指南与常见问题微信支付V3对接中容易被坑的五个细节5.1 现象请求返回 Invalid serial no原因Authorization头里的serial_no填错了常见的是把证书文件的编号和平台证书序列号搞混。解决登录微信商户平台-账户中心-API安全查看「API证书序列号」这个是一串32位十六进制字符不是证书文件里subject的CN字段。修改工具类配置里的serialNo重启后重新请求。5.2 现象微信回调通知解密报错 AEAD_AES_256_GCM decrypt failed原因APIv3密钥不是证书的私钥密码而是商户平台-账户中心-API安全里单独设置的32字节密钥另外解密时nonce参数大小写要和回调JSON里的resource.nonce完全一致。解决将工具类里apiV3Key配置项改为商户平台设置的APIv3密钥检查AES-GCM解密时是否把nonce和ciphertext搞混ciphertext是Base64编码的密文解密前要解码。实际开发中遇到三次都是这个原因一次是配置错了密钥两次是解密顺序写反。5.3 现象支付成功后订单状态没更新回调地址收到了通知但业务没处理原因回调处理逻辑抛异常时没有返回错误应答微信会隔一段时间重试或者业务处理完后返回了非200状态码。解决回调方法入口就做验签和解密解密失败也返回200空串但需要在日志里记录业务状态更新成功后立即返回{code:SUCCESS,message:成功}同时把微信的transaction_id存到本地订单表用于幂等。5.4 现象企业打款接口旧版提示权限不足或敏感接口报错原因企业打款到零钱是V2版本的接口权限需要在商户平台单独申请不是签约了微信支付就能用另外V2版本接口需要用商户证书做双向认证和V3的签名机制不兼容。解决这套工具类里企业打款方法单独走了另一套HttpClient配置使用V2证书和MD5签名如果只是新项目建议直接申请商家转账到零钱V3版本接口更稳定且权限申请更快。5.5 现象金额对比时明明一样却报金额不匹配原因前端传的金额是字符串10.00元后端转分时用了Double.parseDouble(10.00) * 100得到1000.0再强转成int得到1000但如果传的是9.99浮点换算会出现999.99999被强转成999。解决统一用BigDecimal转换new BigDecimal(10.00).movePointRight(2).intValue()工具类里所有金额入参都定义为int分业务侧在入口处做转换即可。这个坑在公司支付系统上线第二个月出现过一次线上差额一分钱的退款单排查了两个小时。6. 从工具类到生产可用证书轮换、日志记录与压测验证技巧6.1 证书轮换时不用重启服务微信平台证书有有效期轮换后旧的验签会失败。工具类里如果只放了一个固定的platformCertPath每次轮换都要改配置重启。我一般会在工程里做一层证书自动更新第一次从微信接口拉取平台证书存库之后定时任务每天检查一次/v3/certificates接口有变化就更新内存里的公钥。public void updatePlatformCert() { String certResponse getRequest(/v3/certificates); JSONArray data JSON.parseObject(certResponse).getJSONArray(data); for (int i 0; i data.size(); i) { JSONObject certObj data.getJSONObject(i).getJSONObject(encrypt_certificate); String certPlain AesGcmUtil.decrypt( certObj.getString(ciphertext), certObj.getString(nonce), certObj.getString(associated_data), apiV3Key); platformPublicKeyMap.put(data.getJSONObject(i).getString(serial_no), parsePublicKey(certPlain)); } }代码逻辑说明证书接口返回的数据也是加密的解密方式和回调通知一致。序列号放在响应体的serial_no字段里证书内容是一段PEM格式的Base64文本。参数说明多个平台证书时用序列号做key存Map验签时根据响应头Wechatpay-Serial选择对应公钥新证书和旧证书并存一段时间避免旧证书还没过期时新证书已经下发。6.2 日志要记录原始报文支付类接口出问题时效性要求极高日志如果只记业务参数找不到原因。我在封装的方法里统一打了两条日志请求发出前记录method、urlPath、请求体脱敏后收到响应后记录HTTP状态码、响应体。注意请求体里不打印商户私钥和APIv3密钥。log.info(微信支付请求: method{}, url{}, body{}, method, urlPath, sensitiveMask(body)); log.info(微信支付响应: status{}, body{}, httpStatus, responseBody);6.3 压测时先跑通1分钱闭环上线前不要直接压大金额订单先用1分钱订单跑完整链路下单、支付回调、退款申请、退款回调、交易查询。确认五个环节全是SUCCESS后再做并发测试。注意微信支付有沙箱环境但V3沙箱只支持部分接口企业转账类建议直接在测试商户号上用小金额验证。6.4 对接微信支付V3本质是处理不确定性支付系统的难点在业务异常路径用户支付后没收到回调怎么办退款处理中用户又发起退款怎么办企业打款失败但钱已经扣了怎么办。工具类能解决的是签名、验签、加解密这些确定性工程问题但不确定性部分需要业务侧做对账。我现在的习惯是每天凌晨跑一次对账任务拉取前一天微信账单文件和本地订单表做配对只差账不补账。从那以后支付体感上的问题少了八成遇到问题也有完整日志支撑定位。这套工具类能帮你加速落地但请记住支付无小事每一分钱都要有账可查。希望帮到你。本文还有配套的精品资源点击获取