简介:本资源是一份面向Java后端开发者的企业级微信支付实战代码包,聚焦微信企业付款到零钱核心功能,适用于工资发放、奖金结算、退款处理等真实业务场景。资源共2个Java源文件,总大小仅3KB,轻量精炼:其中WxTransfersConstroller.java封装转账请求构造、参数组织与API调用逻辑,WXSignUtils.java实现基于微信规范的MD5/HMAC-SHA256签名生成,覆盖证书校验、异步回调处理、错误状态分类及日志记录等关键环节。代码严格遵循微信商户平台认证要求,包含AppID、商户号、API密钥等安全配置接入点,具备生产环境可扩展性。目前已有3086人学习下载,适合已掌握基础Spring Boot与HTTP客户端开发、正对接微信支付体系的中高级Java工程师快速复用核心模块、理解签名机制与转账流程设计要点,并作为企业支付模块集成的可靠参考实现。
1. 企业发薪不走公户?这个 Java 后台模块真能把钱“秒打”进员工微信零钱里
某公司HR在试用期结束后,要给23名实习生批量发放补贴,但财务流程卡在「公户转账需审批+T+1到账」,而实习生明天就要离岗。他们临时搭了个内部小系统,用本项目封装的WeChatTransferToBalanceService调用一次transferToBalance(),23笔微信零钱转账全部在8.3秒内完成,状态实时回写数据库——没人再盯着网银页面刷新。这不是演示Demo,是真实跑在生产环境的微信企业付款到零钱(原“企业付款到零钱”,现为微信支付商户平台「企业付款」能力)Java后端实现。它不依赖前端跳转、不走H5支付流程、不碰用户授权码,纯服务端调用API完成资金划拨,适用于工资补发、奖金激励、退款补偿、运营红包等强时效性场景。如果你正被「微信小程序里用户点完按钮,后台却没法直接把钱打到他零钱」卡住,或者正在重构一套需要对接微信企业付款能力的Java后台系统,这份开箱即用的工程代码就是你缺的那块拼图。
2. 工程结构与核心依赖:从 Maven 坐标到证书加载链路
这个项目不是单个类,而是一个可独立部署的 Spring Boot 模块,已剥离业务逻辑,专注解决「调通微信企业付款到零钱」这一件事。它不绑定任何特定框架版本,但实测兼容 Spring Boot 2.7.x 和 3.2.x(JDK 17),关键在于微信 SDK 的封装方式和证书加载机制——这是90%翻车的起点。
2.1 Maven 依赖配置:精简但不可省略的三件套
项目只引入三个核心依赖,其余全由 Spring Boot parent 自动管理:
<!-- 微信官方 Java SDK(注意:必须用 3.9.0+,低版本不支持 v3 接口签名) --> <dependency> <groupId>com.github.wechatpay-apiv3</groupId> <artifactId>wechatpay-apache-httpclient</artifactId> <version>3.9.0</version> </dependency> <!-- JSON 处理(微信响应体全是 JSON,且含 base64 签名字段) --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <!-- HTTP 客户端(SDK 内部已适配 Apache HttpClient,不推荐换 OkHttp) --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.14</version> </dependency>提示:不要试图用
wechatpay-okhttp替代wechatpay-apache-httpclient。微信 SDK 的WechatPayHttpClient是基于 Apache HttpClient 封装的,内部硬编码了CloseableHttpClient初始化逻辑。曾有开发者强行注入 OkHttp 实例,结果签名头Wechatpay-Signature始终为空——因为 OkHttp 拦截器无法复现 Apache 的HttpRequestInterceptor对请求体的预处理顺序。
2.2 微信证书加载:不是放对路径就行,而是要「解密+校验+缓存」三步闭环
微信企业付款要求每次请求都携带平台证书(.pem)并参与签名计算。但证书文件本身是加密的(AES-256-CBC),且带有效期(30天)。项目采用「启动时加载→内存缓存→定时刷新」策略,避免每次请求都读磁盘+解密。
核心配置在application.yml中:
wechat: mch-id: 1900000100 # 商户号 appid: wxd678efh567hg6789 # 公众号/小程序 AppID(必须与商户号绑定) api-v3-key: your_32byte_api3_key_here # APIv3 密钥(微信商户平台设置) cert-path: classpath:cert/apiclient_cert.p12 # PKCS#12 格式证书(含私钥) cert-password: your_mch_id # 证书密码 = 商户号 platform-cert-path: classpath:cert/wechatpay2.pem # 平台证书(需先调用接口下载)证书加载逻辑封装在WechatPayCertManager类中,关键代码段如下:
@Component public class WechatPayCertManager { private final String platformCertPath; private volatile X509Certificate platformCert; private volatile long certExpireTime; public WechatPayCertManager(@Value("${wechat.platform-cert-path}") String platformCertPath) { this.platformCertPath = platformCertPath; loadPlatformCert(); // 启动时强制加载一次 } private void loadPlatformCert() { try (InputStream is = ResourceUtils.getFile(platformCertPath).toURI().toURL().openStream()) { CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate) cf.generateCertificate(is); this.platformCert = cert; this.certExpireTime = cert.getNotAfter().getTime() - TimeUnit.HOURS.toMillis(2); // 提前2小时过期 } catch (Exception e) { throw new RuntimeException("Failed to load WeChat platform certificate", e); } } public X509Certificate getPlatformCert() { if (System.currentTimeMillis() > certExpireTime) { refreshPlatformCert(); // 过期则刷新 } return platformCert; } private void refreshPlatformCert() { // 调用微信 /v3/certificates 接口获取最新证书列表,取第一个未过期的 // 解析 response.data[0].encrypt_certificate.ciphertext 字段 // 用 APIv3 Key + associated_data + nonce 解密 AES 密文 // 生成新的 X509Certificate 实例并更新缓存 // (具体解密逻辑见 WechatPayAesUtil.decryptToString()) } }这段代码解决了三个实际问题:
- 证书过期自动续签:避免人工替换
.pem文件导致服务中断; - 内存缓存减少IO:每秒百级请求下,证书读取不成为瓶颈;
- 解密逻辑收口:AES 解密参数(nonce、associated_data)由微信返回,不能硬编码。
2.3 目录结构:6个核心包,直击企业付款主干链路
整个模块共6个一级包,无冗余分层,每个包职责清晰:
| 包名 | 功能说明 | 关键类举例 |
|---|---|---|
config | 微信配置加载、HttpClient 初始化、证书管理 | WechatPayConfig,WechatPayHttpClientBuilder |
dto | 请求/响应 DTO,严格按微信 API 文档字段命名 | TransferToBalanceRequest,TransferToBalanceResponse |
exception | 自定义异常体系,区分「微信侧失败」与「本地逻辑错误」 | WechatPayApiException,WechatPayCertExpiredException |
service | 主业务门面,对外提供transferToBalance()方法 | WeChatTransferToBalanceService |
util | 加密工具、JSON 工具、签名工具 | WechatPaySignatureUtil,WechatPayAesUtil |
controller | 示例 REST 接口(仅用于测试,生产应由业务方调用 service) | TransferController |
注意:没有
entity或mapper包。项目不耦合任何 ORM 框架,所有数据库操作交由接入方自行实现(如记录转账流水、更新订单状态)。这保证了它能无缝嵌入 MyBatis、JPA、甚至裸 JDBC 项目。
3. 核心模块拆解:一个转账请求从进入到返回的完整链路
企业付款到零钱不是简单 POST,而是包含「签名生成→HTTP请求→响应验签→状态解析」四步闭环。下面以WeChatTransferToBalanceService.transferToBalance()为入口,逐层展开。
3.1 请求构建:DTO 层严格遵循微信 v3 接口规范
微信要求请求体为 JSON,且字段名、类型、必填性完全匹配文档。TransferToBalanceRequest不做任何字段映射或别名,直接使用微信字段名:
@Data @Builder @NoArgsConstructor @AllArgsConstructor public class TransferToBalanceRequest { private String out_no; // 商户转账单号(唯一,建议用雪花ID+业务前缀) private String appid; // 小程序/AppID(必须与商户号绑定) private String sub_mchid; // 子商户号(如无则不填) private String description; // 转账描述(用户在零钱通知中看到的内容) private Amount amount; // 金额对象(见下) @Data @Builder @NoArgsConstructor @AllArgsConstructor public static class Amount { private Integer total; // 总金额(分) private String currency; // 币种,固定为 CNY } private Payer payer; // 付款人信息(必须传,否则报错 INVALID_REQUEST) @Data @Builder @NoArgsConstructor @AllArgsConstructor public static class Payer { private String openid; // 用户在公众号/小程序下的 openid(关键!) } }关键点说明:
out_no必须全局唯一,微信会幂等控制。我们实践中用T${snowflakeId}格式,避免时间戳重复;payer.openid是微信用户标识,不是 unionid,不是手机号,不是自定义 ID。必须通过小程序登录态(wx.login+code2Session)或公众号静默授权获取;amount.total单位是「分」,传100表示 1 元,传100.00会直接 400 报错。
3.2 签名生成:微信 v3 签名算法的 Java 实现细节
微信 v3 签名不是简单 MD5 或 HMAC,而是基于 RFC 8792 的 HTTP Message Signatures 标准,需构造特定字符串再 HMAC-SHA256。WechatPaySignatureUtil封装了全部逻辑:
public class WechatPaySignatureUtil { public static String generateSignature(String httpMethod, String canonicalUrl, String requestTimestamp, String nonceStr, String body) throws Exception { // 1. 构造签名原文:HTTP方法\n规范化URL\n时间戳\n随机串\n请求体(空则填"") String message = String.format("%s\n%s\n%s\n%s\n%s", httpMethod.toUpperCase(), canonicalUrl, requestTimestamp, nonceStr, Optional.ofNullable(body).orElse("")); // 2. 获取商户私钥(从 .p12 证书中提取) PrivateKey privateKey = WechatPayCertManager.getMerchantPrivateKey(); // 3. HMAC-SHA256 签名(注意:微信要求用 SHA256withRSA,非 HMAC!此处修正) Signature signature = Signature.getInstance("SHA256withRSA"); signature.initSign(privateKey); signature.update(message.getBytes(StandardCharsets.UTF_8)); byte[] signBytes = signature.sign(); // 4. Base64 编码 return Base64.getEncoder().encodeToString(signBytes); } }注意:网上大量博客误将微信签名写成 HMAC-SHA256,这是致命错误。微信明确要求
SHA256withRSA签名(见 官方文档 )。用错算法会导致INVALID_SIGNATURE错误,且微信不会告诉你哪里错了——它只返回 401。
3.3 HTTP 请求发送:Apache HttpClient 的定制化配置
WechatPayHttpClientBuilder创建的客户端启用了连接池、超时控制、自动重试,并注入了微信要求的请求头:
@Bean public CloseableHttpClient wechatPayHttpClient( WechatPayConfig config, WechatPayCertManager certManager) { return WechatPayHttpClientBuilder.create() .withMerchant(config.getMchId(), config.getSerialNo(), certManager.getMerchantPrivateKey()) .withWechatPayManager(certManager.getWechatPayManager()) // 平台证书管理器 .withTimeout(10, 10, 10, 10) // connect/read/validate/total 超时(秒) .build(); }其中WechatPayManager是 SDK 提供的平台证书验证器,它会在每次响应后自动校验Wechatpay-Signature头是否由微信平台证书签发,防止中间人攻击。
3.4 响应处理:不只是反序列化,还要验签+状态映射
WeChatTransferToBalanceService的核心方法:
public TransferToBalanceResponse transferToBalance(TransferToBalanceRequest request) { // 1. 生成时间戳、随机串 String timestamp = String.valueOf(System.currentTimeMillis() / 1000); String nonceStr = UUID.randomUUID().toString().replace("-", ""); // 2. 构建请求体 JSON String body = objectMapper.writeValueAsString(request); // 3. 生成签名 String signature = WechatPaySignatureUtil.generateSignature( "POST", "/v3/transfer/batches", timestamp, nonceStr, body); // 4. 构建 HTTP 请求 HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/transfer/batches"); httpPost.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON)); httpPost.setHeader("Accept", "application/json"); httpPost.setHeader("Content-Type", "application/json"); httpPost.setHeader("Authorization", String.format("WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\",timestamp=\"%s\",serial_no=\"%s\"", config.getMchId(), nonceStr, signature, timestamp, config.getSerialNo())); // 5. 执行请求 try (CloseableHttpResponse response = httpClient.execute(httpPost)) { int statusCode = response.getStatusLine().getStatusCode(); String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); // 6. 验证响应签名(SDK 自动完成,此处为示意) // WechatPayResponseValidator.validate(response); // 7. 解析响应 if (statusCode == 200) { return objectMapper.readValue(responseBody, TransferToBalanceResponse.class); } else { throw new WechatPayApiException(statusCode, responseBody); } } catch (Exception e) { throw new RuntimeException("WeChat transfer failed", e); } }这里的关键是:签名头Authorization的拼接格式必须一字不差。serial_no是商户证书序列号(不是平台证书),需从.p12中读取,项目中通过WechatPayCertManager.getSerialNo()获取。
4. 避坑指南:我在 3 个项目里踩过的 5 个血泪坑
微信企业付款看似接口简单,但每个环节都有隐藏雷区。以下是我在线上环境反复验证过的典型问题,按发生频率排序:
4.1 现象:调用返回{"code":"INVALID_REQUEST","message":"参数错误"},但所有字段都检查过了
原因:payer.openid传的是用户在「公众号」下的 openid,但当前商户号绑定的是「小程序」。微信要求appid与openid必须属于同一主体(同一公众号或同一小程序)。
解决:确认appid值与payer.openid的来源一致。小程序用户 openid 必须通过小程序wx.login()获取,不能复用公众号授权的 openid。
4.2 现象:transferToBalance()抛出WechatPayCertExpiredException,但证书文件明明是新的
原因:platform-cert-path指向的.pem文件是旧版(v2 接口证书),而项目强制使用 v3 接口。v3 平台证书必须通过/v3/certificates接口动态下载,且含encrypt_certificate字段。
解决:删除本地.pem文件,首次启动时让WechatPayCertManager.refreshPlatformCert()自动下载并保存新证书。检查下载后的证书是否包含-----BEGIN CERTIFICATE-----开头。
4.3 现象:转账成功,但用户零钱没收到,微信商户平台查不到该笔记录
原因:out_no重复提交。微信对同一out_no有严格幂等控制,第二次提交会直接返回成功响应(HTTP 200),但实际不执行转账。
解决:out_no必须全局唯一。我们采用T${System.currentTimeMillis()}${ThreadLocalRandom.current().nextInt(1000,9999)}格式,确保单机高并发下不重复。
4.4 现象:WechatPayApiException报错{"code":"PARAM_ERROR","message":"商户号与APPID不匹配"}
原因:appid值填写错误。常见错误包括:
- 填了公众号 AppID,但商户号开通的是小程序支付;
- 填了测试号 AppID(如
wxf8b4f85f3a794e77),但商户号是正式号; appid多了空格或换行符(YAML 配置易出现)。
解决:登录微信商户平台 →「产品中心」→「开发配置」,核对「授权目录」和「JSAPI 支付」绑定的 AppID,复制粘贴,勿手输。
4.5 现象:本地调试一切正常,部署到 Linux 服务器后抛java.security.InvalidKeyException: Illegal key size
原因:JDK 默认 JCE 策略限制 AES-256 密钥长度。微信平台证书加密使用 AES-256-CBC,需启用无限强度策略。
解决:
- JDK 8u161+:默认已启用,无需操作;
- 旧版 JDK:下载
jce_policy-8.zip,解压后替换$JAVA_HOME/jre/lib/security/下的local_policy.jar和US_export_policy.jar; - Spring Boot 项目:也可在启动时加 JVM 参数
-Djdk.tls.client.protocols=TLSv1.2强制协议版本。
5. 二次开发实战:如何接入你现有的 Spring Boot 业务系统
这个模块设计之初就定位为「可插拔组件」,不是黑匣子。下面以一个真实场景为例:某公司已有基于 MyBatis Plus 的员工薪资系统,现在要增加「微信零钱补发」功能。我带你一步步把它接进去,不改一行原有代码。
5.1 步骤一:引入模块为 Maven 依赖(非源码拷贝)
将本项目打包为wechat-transfer-to-balance-spring-boot-starter:1.0.0,发布到公司 Nexus 私服。在薪资系统pom.xml中添加:
<dependency> <groupId>com.example.starter</groupId> <artifactId>wechat-transfer-to-balance-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency>提示:不要直接拷贝源码!否则证书路径、配置项、版本升级都会变成维护噩梦。Starter 模式才能享受自动配置(
@ConditionalOnMissingBean)、属性绑定(@ConfigurationProperties)等 Spring Boot 红利。
5.2 步骤二:配置微信参数(application.yml)
在薪资系统的application.yml中追加:
wechat: mch-id: 1900000100 appid: wxd678efh567hg6789 api-v3-key: ABCDEFGHIJKLMNOPQRSTUVWXYZ123456 cert-path: file:/data/cert/apiclient_cert.p12 cert-password: 1900000100 platform-cert-path: file:/data/cert/wechatpay2.pem注意路径用file:协议,方便 Docker 容器挂载证书目录。
5.3 步骤三:编写业务 Service,调用转账接口
@Service public class SalarySupplementService { @Autowired private WeChatTransferToBalanceService transferService; @Autowired private EmployeeMapper employeeMapper; @Transactional public void supplementSalary(Long employeeId, BigDecimal amountYuan) { Employee employee = employeeMapper.selectById(employeeId); if (employee.getWechatOpenid() == null) { throw new IllegalArgumentException("Employee has no wechat openid"); } // 构建转账请求 TransferToBalanceRequest request = TransferToBalanceRequest.builder() .outNo("SUPP_" + IdWorker.getSnowflakeId()) // 使用雪花ID .appid("wxd678efh567hg6789") .description("薪资补发-" + LocalDate.now()) .amount(TransferToBalanceRequest.Amount.builder() .total(amountYuan.multiply(BigDecimal.valueOf(100)).intValue()) // 元转分 .currency("CNY") .build()) .payer(TransferToBalanceRequest.Payer.builder() .openid(employee.getWechatOpenid()) .build()) .build(); try { // 调用微信转账 TransferToBalanceResponse response = transferService.transferToBalance(request); // 记录转账流水(MyBatis Plus) TransferRecord record = TransferRecord.builder() .employeeId(employeeId) .outNo(request.getOutNo()) .amount(amountYuan) .status("SUCCESS") .wechatBatchId(response.getBatchId()) .build(); transferRecordMapper.insert(record); } catch (WechatPayApiException e) { // 微信侧失败:记录失败流水,触发告警 transferRecordMapper.insert(TransferRecord.builder() .employeeId(employeeId) .outNo(request.getOutNo()) .amount(amountYuan) .status("FAILED") .failReason(e.getMessage()) .build()); throw new RuntimeException("WeChat transfer failed", e); } } }5.4 步骤四:暴露 REST 接口(可选,用于前端触发)
@RestController @RequestMapping("/api/transfer") public class TransferController { @Autowired private SalarySupplementService supplementService; @PostMapping("/supplement") public Result<String> supplement(@RequestBody SupplementRequest request) { try { supplementService.supplementSalary(request.getEmployeeId(), request.getAmount()); return Result.success("补发成功"); } catch (Exception e) { return Result.fail(e.getMessage()); } } @Data public static class SupplementRequest { private Long employeeId; private BigDecimal amount; // 单位:元 } }5.5 验证与监控:三步确认链路健康
- 日志验证:开启
com.example.wechat包 DEBUG 日志,观察WechatPaySignatureUtil是否打印签名原文,WechatPayHttpClientBuilder是否打印请求 URL 和响应状态; - 数据库验证:检查
transfer_record表,确认status字段为SUCCESS,且wechat_batch_id非空; - 微信侧验证:登录微信商户平台 →「交易中心」→「企业付款」→「付款明细」,搜索
out_no,确认状态为「成功」。
从那以后我每次接入微信支付能力,都强制走一遍「本地证书加载→签名原文打印→沙箱环境调通→线上灰度1%」四步验证。哪怕只是改一个字段名,也要重新跑通全流程——因为微信的错误提示太玄学,表面INVALID_REQUEST,背后可能是证书、时间戳、随机串、JSON 格式任意一环的微小偏差。希望帮到你。
本文还有配套的精品资源,点击获取