尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
微信支付V3工具类封装实战:从下单、退款到回调验签
简介这套微信支付工具类V3版是作者在企业项目中沉淀并封装的微信交易接口方案主要面向需要对接微信支付V3、退款V3、交易状态查询及企业打款到零钱的后端开发者。工具类将微信支付官方接口中的签名生成、证书加载、请求发送、响应解析等环节统一封装覆盖微信支付V3版、微信退款V3版、微信交易状态查询和企业打款到个人零钱旧版四类高频业务场景调用方只需按业务传入订单号、金额、商户号等参数即可快速完成交易闭环免去重复对接底层接口的繁琐工作。整个资源压缩后仅11KB共7个文件其中5个是核心源文件负责各业务场景的具体实现1个是Maven工程配置文件xml用于管理依赖1个是IDE模块文件iml导入开发环境后可直接识别项目结构整体轻量、易迁移。目前已有2275人学习尤其适合正在开发电商、收银台、企业付款等系统的初中级开发工程师作为参考亦可在现有项目中直接改造复用降低接入微信支付体系的时间与踩坑成本。1. 微信支付工具类为什么值得自己再封一层做 Java 服务端的人对接微信支付 V3第一反应是引入官方wechatpay-javaSDK但真把这个 SDK 放进企业项目后你会发现它解决的是能不能调通的问题没解决好不好接的问题。官方 SDK 把请求签名、HTTP 客户端、证书加载做掉了可业务系统需要的是一个能直接传业务参数、返回统一结果对象的工具类而不是每次调用都去拼HttpRequest、处理HttpResponse、再手动解析 JSON。这套extend-weixin工具类就是在这样的背景下从企业项目里拆出来的它把微信支付 V3 下单、退款、交易状态查询、以及旧版企业打款到零钱都封成了简单方法适合那些已经在用 Spring Boot 或纯 Java 项目、希望把微信支付模块收敛到一个包里的团队。下面从支付 V3 的封装说起加入我实际拆读这套代码时的观察和补充。2. 微信支付 V3 下单封装与签名链路处理2.1 数据模型设计请求参数与响应结果分离工具类的第一个设计决策是把微信支付 V3 的请求参数和响应结果拆成独立的数据类而不是直接使用官方 SDK 的 DTO。以JsapiPayRequest为例它对应 JSAPI 下单接口的公共参数核心字段包括appid、mchid、description、out_trade_no、notify_url、amount。amount是嵌套对象包含total和currency金额单位是分不是元。public class JsapiPayRequest { private String appid; private String mchid; private String description; private String out_trade_no; private String notify_url; private Amount amount; private Payer payer; public static class Amount { private Integer total; private String currency CNY; // getter/setter 省略 } public static class Payer { private String openid; // getter/setter 省略 } }这段代码最需要注意的地方是amount.total用了Integer而不是BigDecimal。微信支付 V3 的金额单位是分如果用BigDecimal存元序列化出来会是total: 0.01接口直接报参数错误。我在实际对接时就把下单金额统一在 Service 层做元转分工具类内部不再做任何单位换算避免不同调用方换算出错。响应结果同理WxPayResponse只暴露code、message、data三个字段。code是工具类内部定义的状态200表示微信接口返回成功-1表示网络异常或签名失败-2表示微信返回业务错误例如余额不足。这样上层业务只需要判断code而不需要关心微信返回的status和resultCode的组合。2.2 签名与 HTTP 客户端封装微信支付 V3 要求对请求头中的Authorization做 RSA-SHA256 签名签名串格式固定工具类把这部分封装在WechatPayV3Client里。对外只暴露一个doPost(String url, String body)方法内部负责生成Authorization头、设置User-Agent、发送请求并解析响应。public String doPost(String url, String body) throws Exception { long timestamp System.currentTimeMillis() / 1000; String nonceStr UUID.randomUUID().toString().replace(-, ); String message POST\n url \n timestamp \n nonceStr \n body \n; Signature signature Signature.getInstance(SHA256withRSA); signature.initSign(privateKey); signature.update(message.getBytes(StandardCharsets.UTF_8)); String sign Base64.getEncoder().encodeToString(signature.sign()); String authHeader WECHATPAY2-SHA256-RSA2048 mchid\ mchId \, nonce_str\ nonceStr \, signature\ sign \, timestamp\ timestamp \, serial_no\ serialNo \; HttpRequest request HttpRequest.newBuilder() .uri(URI.create(https://api.mch.weixin.qq.com url)) .header(Authorization, authHeader) .header(Content-Type, application/json) .header(Accept, application/json) .header(User-Agent, extend-weixin/1.0) .POST(BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); // 发送请求并返回响应体 }这里有几个参数必须和商户号配置完全一致serial_no是商户 API 证书的序列号不是证书内容本身privateKey是从apiclient_key.pem中读取的商户私钥。很多人遇到401是私钥格式问题微信要求 PKCS#8 的 PEM而很多网上下载的私钥是 PKCS#1需要先用 OpenSSL 转换openssl pkcs8 -topk8 -inform PEM -in apiclient_key.pem -outform PEM -nocrypt -out apiclient_key_pkcs8.pem工具类里建议直接配置转换后的apiclient_key_pkcs8.pem路径并在启动时做一次私钥解析校验失败就抛异常避免上线后所有支付请求全部签名失败。2.3 下单方法的使用方式工具类提供的支付方法叫wxPayV3.createJsapiOrder(JsapiPayRequest req)内部会先对必填字段做空值校验然后按微信接口要求给payer.openid字段赋值最后返回预付单信息prepay_id。public WxPayResponse createJsapiOrder(JsapiPayRequest req) { // 校验 out_trade_no 不能为空且长度不超过 32 // 校验 amount.total 必须大于 0 String body gson.toJson(req); String resp client.doPost(/v3/pay/transactions/jsapi, body); // 解析响应如果 code200从 data 中提取 prepay_id }调用方在业务代码里只需要两行JsapiPayRequest req new JsapiPayRequest(); req.setOutTradeNo(orderNo); req.setAmount(totalInCents); req.setPayer(openid); WxPayResponse resp wxPayV3.createJsapiOrder(req);WxPayResponse.data里存的是prepay_id前端拿到这个 ID 后调用wx.requestPayment拉起收银台。工具类不负责维护订单状态只负责把微信的响应原样透传状态管理由业务系统自己的订单表完成。如果resp.code不是200建议把resp.message原样打到日志里方便排查微信返回的code、message字段它们分别是微信侧的code和message。3. 微信退款 V3 封装与交易状态查询的组合实战3.1 退款接口的业务约束与参数设计退款 V3 接口和支付 V3 的签名方式完全一样只是请求路径从/v3/pay/transactions/jsapi换成了/v3/refund/domestic/refunds。工具类封装了一个refundV3.refund(RefundRequest req)方法核心参数是out_trade_no、out_refund_no、amount.refund、amount.total、amount.currency。这里有一个容易踩坑的参数组合amount.total必须和下单时的订单总金额一致amount.refund必须小于等于amount.total否则微信返回AMOUNT_EXCEED错误。RefundRequest req new RefundRequest(); req.setOutTradeNo(originalOrderNo); req.setOutRefundNo(refundOrderNo); req.setReason(用户申请退款); req.setNotifyUrl(refundNotifyUrl); RefundRequest.Amount amount new RefundRequest.Amount(); amount.setRefund(1500); amount.setTotal(1500); amount.setCurrency(CNY); req.setAmount(amount); WxPayResponse resp refundV3.refund(req); if (resp.getCode() 200) { // 退款申请受理但不是立即退款成功 }退款接口返回的code 200只代表微信接受了退款申请此时退款单状态是PROCESSING真正到账需要等微信内部处理通常几秒到几分钟。业务上应该在退款回调通知里更新订单状态而不是在调用退款接口后立即把订单标记为已退款。3.2 交易状态查询的兜底机制只依赖回调有一个问题如果微信回调因为网络抖动或者服务重启丢失订单状态就永远停在PROCESSING。所以工具类提供了queryV3.queryOrder(String outTradeNo)和queryV3.queryRefund(String outRefundNo)两个主动查询方法用来做定时对账和异常订单补偿。public WxPayResponse queryOrder(String outTradeNo) { String url /v3/pay/transactions/out-trade-no/ outTradeNo ?mchid mchId; String resp client.doGet(url); // 解析后返回 trade_state }trade_state的取值有SUCCESS、REFUND、NOTPAY、CLOSED、REVOKED、USERPAYING、PAYERROR。其中USERPAYING是用户正在支付中不能直接判定为支付失败需要等待几秒钟再次查询NOTPAY可以允许用户在 2 小时内重复支付CLOSED是订单已关闭不能再发起支付。下面是查询状态与业务动作的对应关系表trade_state业务含义建议动作SUCCESS支付成功更新订单为已支付发货REFUND已退款更新订单为已退款NOTPAY未支付不处理允许继续支付CLOSED已关闭更新订单为已关闭USERPAYING支付中3 秒后重新查询PAYERROR支付失败更新订单为支付失败提示用户重新支付这个查询方法会暴露给定时任务比如每 5 分钟扫描一次超过 30 分钟未收到回调但状态还是待支付的订单主动调查询接口确认结果。工具类里没有内置定时任务但我通常建议在业务层单独加一个Scheduled方法调用queryV3完成补偿。3.3 退款状态查询与轮询上限退款查询接口的路径是/v3/refund/domestic/refunds/{out_refund_no}返回的status有SUCCESS退款成功、CLOSED退款关闭、PROCESSING退款处理中、ABNORMAL退款异常。工具类在queryRefund方法里直接把refund_status放到data.refundStatus字段供上层直接取值。为了保证不无限轮询我在写这类工具类时习惯加一个轮询次数上限参数默认 5 次每次间隔 30 秒。如果达到上限仍然处于PROCESSING就降级为等待退款回调回调同时把订单标记为退款异常待人工处理。这个逻辑不属于微信接口本身但却是企业项目里必备的容错手段。4. 企业打款到零钱的旧版兼容处理与安全配置4.1 企业打款到零钱的版本差异工具类中保留的企业打款到个人零钱功能走的是微信支付的旧版接口/mmpaymkttransfers/promotion/transfers现在叫企业付款到零钱官方新接口叫商家转账到零钱。旧版接口用的是MD5签名和 V3 的RSA-SHA256完全不同而且请求格式不是 JSON而是 XML。同一个工具类里同时处理两种签名协议需要把企业打款相关的代码独立成EnterprisePayService避免和 V3 的签名逻辑混在一起。旧版接口需要的核心参数比 V3 少很多mch_appid、mchid、partner_trade_no、openid、check_name、amount、desc、spbill_create_ip。check_name有三个可选值NO_CHECK不校验真实姓名FORCE_CHECK强校验真实姓名。如果是FORCE_CHECK还需要传re_user_name一旦用户姓名对不上转账直接失败。EnterprisePayRequest req new EnterprisePayRequest(); req.setPartnerTradeNo(transferNo); req.setOpenid(targetOpenid); req.setCheckName(FORCE_CHECK); req.setReUserName(张三); req.setAmount(100L); // 单位分 req.setDesc(微信提现到零钱); req.setSpbillCreateIp(127.0.0.1); String xml buildXml(req); String respXml enterprisePayClient.doPostXml(/mmpaymkttransfers/promotion/transfers, xml); // 解析 XML 中的 result_code 和 err_code注意这里的amount的单位同样是分但是类型是Long因为转账金额不受Integer的分限制实际上分限制不变只是个别语言习惯防止金额溢出。partner_trade_no和支付订单号一样要求商户侧唯一。4.2 旧版接口的证书坑与回调缺失问题企业打款到零钱旧版接口请求时需要在请求体中携带商户证书双向 TLS工具类里配置的是apiclient_cert.p12而不是 V3 使用的apiclient_key.pem。在 Java 中加载.p12证书的方式KeyStore ks KeyStore.getInstance(PKCS12); try (InputStream in new FileInputStream(certP12Path)) { ks.load(in, mchId.toCharArray()); }这里必须确保mchId就是商户号如果加载时报keystore password was incorrect第一个要检查的不是密码而是这个.p12文件是不是从业务证书类型里下载的不是 API 证书。API 证书在微信商户平台下载后是apiclient_cert.pem需要先转成.p12才能被 Java 的KeyStore加载。转账旧版接口没有回调通知只能靠主动查询接口确认结果。查询路径是/mmpaymkttransfers/gettransferinfo请求参数是partner_trade_no和mch_id。返回的status有SUCCESS、FAILED、PROCESSING其中PROCESSING状态需要每隔一段时间查询一次微信官方建议最多等 20 秒再查如果一直PROCESSING大概率是银行卡入账延迟不要重复发起转账否则会重复打款。工具类里把这个查询方法命名为transferV3.queryTransfer虽然名字带 V3但走的其实是旧版接口只是为了保持工具类对外方法名一致。调用方如果已经上线新版的商家转账接口不建议再使用这个旧方法因为新接口v3/transfer/batches的返回结构和签名方式完全不同混用会让同一个 Service 类出现两套签名逻辑。5. 回调通知验签与幂等处理把这套工具类用得更稳5.1 V3 回调验签必须自己实现微信支付 V3 的回调通知是 HTTP POST请求头里带有Wechatpay-Timestamp、Wechatpay-Nonce、Wechatpay-Signature、Wechatpay-Serial请求体是加密后的 JSON。很多工具类版本只提供了下单和查询没有回调验签方法导致业务方只能拿到明文后直接信任。这种做法在本地测试没问题一旦上线会非常危险因为任何人都可以伪造回调通知你的服务器发货。public boolean verifyNotifySignature(NotifyRequest notifyReq) { String message notifyReq.getTimestamp() \n notifyReq.getNonce() \n notifyReq.getBody() \n; // 用微信平台证书公钥验签 PublicKey publicKey loadWechatPayPublicKey(notifyReq.getSerial()); Signature signature Signature.getInstance(SHA256withRSA); signature.initVerify(publicKey); signature.update(message.getBytes(StandardCharsets.UTF_8)); return signature.verify(Base64.getDecoder().decode(notifyReq.getSignature())); }验签通过后还需要解密回调数据。V3 回调数据是 AES-256-GCM 加密密钥是 APIv3 密钥32 字节解密出来的 JSON 里有transaction_id、out_trade_no、trade_state等字段。这里有一个容易搞混的点Wechatpay-Serial是微信平台证书的序列号不是商户 API 证书的序列号工具类里不能混着用。5.2 回调幂等与业务防重我在企业项目里遇到过重复回调微信支付在未收到商户成功响应时会间隔几秒重新发送同一个通知所以回调处理逻辑必须天然幂等。工具类不直接处理业务但调用方可以借助out_trade_no在 Redis 里做去重SETNX pay_notify_ORDER_20250101001 1 EX 600在回调方法里先执行SETNX如果返回false说明已经处理过直接返回{code:SUCCESS}。如果返回true再执行业务更新最后记得响应微信成功报文{code: SUCCESS, message: 成功}响应体必须严格使用微信要求的格式如果响应{}或者500微信会认为回调失败并继续重试。5.3 工具类的可观测性建议这套工具类的方法都带有WxPayResponse返回对象我在实际使用中会建议在出入参处都打印日志但要把敏感字段脱敏。比如openid只保留前 4 位后 4 位amount不打日志signature不打日志。一个最简单的做法是在WxPayV3Client里用 AOP 拦截记录每个方法的耗时及其状态码。另外工具类里的nonceStr和timestamp都要在日志里体现这两个值在排查微信请求签名问题时是唯一线索。5.4 针对工具类的快速自检清单如果刚拿到这套代码先跑通一个支付单再改业务逻辑。自检时重点看这几个位置pom.xml里的依赖是否包含httpclient和gson配置文件里的mchId、serialNo、privateKeyPath是否指向你自己的测试商户号调用createJsapiOrder前是否把元转成了分。工具类不负责单元测试但建议你用一个沙箱商户号跑一次真实的下单和退款流程确认回调验签能通过后再接入正式环境。最后提一个容易被忽略的参数spbill_create_ip在企业打款接口里是必传项但业务方往往不关心这个值我一般取用户下单时的客户端 IP 或者网关 IP如果后端分布式部署就在 Nginx 层把X-Forwarded-For透传下来否则转账请求会被微信风控拦截。本文还有配套的精品资源点击获取
RELATED

相关推荐

Web热敏小票打印:PDF中间层方案实战指南

Web热敏小票打印:PDF中间层方案实战指南

1. 项目概述:为什么热敏小票的 Web 打印不是“点一下就完事”的事热敏小票、Web打印、58mm、80mm、web-print-pdf——这五个词凑在一起,表面看是个再普通不过的前端需求:用户在网页下单后,点个“打印小票”按钮,打印机…

📅 2026/9/16 6:17:13
Git GUI入门:为什么推荐先学图形界面而不是命令行?

Git GUI入门:为什么推荐先学图形界面而不是命令行?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/16 6:17:13
接口幂等性设计实战:从Token到数据库唯一约束

接口幂等性设计实战:从Token到数据库唯一约束

1. 接口幂等性到底是什么?先从一次线上事故说起做后端开发这些年,“接口幂等性”是被问得最多的技术话题之一,也是面试里出现频率极高的考点。但说实话,真正把它讲清楚、做对的人并不多。大多数人对幂等的理解停留在“同一个请求多…

📅 2026/9/16 6:17:13
MORE NEWS

更多资讯

📰

Java安卓IM源码解析:长连接、消息可靠与线程安全实战

简介:本资源是一份基于Java开发的仿QQ原生安卓APP实训项目源码,面向Android初学者与Java移动端学习者,旨在通过真实社交场景实践掌握Activity生命周期、Fragment导航、RecyclerView列表渲染、SharedPreferences本地存储及基础网络请求等核心开…

📰

低代码平台:提升开发效率的新选择

1. 为什么说"手写代码已老"?在传统软件开发中,程序员需要逐行编写代码来实现业务逻辑。这种方式存在几个明显痛点:开发周期长:从需求分析到代码实现通常需要数周甚至数月人力成本高:需要雇佣专业开发团队维护…

📰

洋葱质量检测数据集与YOLO模型实战指南

1. 项目概述:洋葱质量检测数据集的价值与应用在农产品质量检测领域,计算机视觉技术正逐步替代传统人工分拣方式。这个包含1015张标注图像的VOCYOLO格式数据集,专门针对洋葱的三种状态(好、坏、腐烂)进行标注&#xff0…

📰

Colibri Tagger:开源跨平台音频标签批量整理神器

不管是做播客归档、DJ 曲库整理,还是单纯想把自己收藏了十几年的 MP3 变成像样的数字音乐库,你大概率会遇到同一个尴尬:文件全挤在一个文件夹里,歌名乱写、专辑名缺失、封面还是当年从百度图片里随手存下来的 200200 小图。拿通用…

📰

Linux查找搜索与管道重定向精讲|Xshell真机实操|完整知识体系

Linux查找搜索与管道重定向精讲|Xshell真机实操|完整知识体系(第三篇CSDN原创) 📝 简介:本文为第三篇Linux原创实操博文,聚焦课程核心重难点:find文件查找、grep文本搜索、管道符、输…

📰

粒子群算法在电力经济调度中的优化应用

1. 项目背景与核心价值电力系统最优潮流(OPF)问题一直是电力行业的核心挑战之一。我在某区域电网调度中心工作的第3年,第一次亲身体会到传统优化方法在面对复杂约束时的无力感——那是一次冬季负荷高峰期的紧急调度,常规算法耗时2…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬