微信商家转账API V3教程(2025新版) JAVA全源码 无缝对接(无需踩坑)
前言:
1、本教程非常简单,全源码
2、本文章仅介绍java后端对接流程,功能开通以及前端调用(基础版无需提交资料审核,不过额度只有200),请参考官方文档产品介绍_商家转账|微信支付商户文档中心
3、从去年年底开始,原微信商家转账到零钱功能已停止开放,替换为商家转账功能。与之前的商家转账到零钱相比,流程增加了用户确认步骤,通过API发起转账以后,还需要用户通过商户小程序点击确认(前端指引->JSAPI调起用户确认收款_商家转账|微信支付商户文档中心)
一、引入微信支付官方SDK(不需要重复造轮子)
com.github.wechatpay-apiv3 wechatpay-java 0.2.15
二、封装实体类
后端API一共三个方法,发起转账->查询转账->撤销转账,状态变更都有异步回调通知,所以我们封装以下实体类(请勿更改@serializedName注解)
1、请求实体类(只有发起转账需要封装,其它接口都是路径传参)
/** * 发起商家转账参数 */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class TransferCreateRequest { @SerializedName("appid") private String appid; @SerializedName("out_bill_no") private String outBillNo; @SerializedName("transfer_scene_id") private String transferSceneId; @SerializedName("openid") private String openid; @SerializedName("user_name") private String userName; @SerializedName("transfer_remark") private String transferRemark; @SerializedName("transfer_amount") private Integer transferAmount; @SerializedName("notify_url") private String notifyUrl; /** * 【用户收款感知】 * 用户收款时感知到的收款原因将根据转账场景自动展示默认内容。 * 如有其他展示需求,可在本字段传入。 * 各场景展示的默认内容和支持传入的内容,可查看产品文档了解。 */ @SerializedName("user_recv_perception") private String userRecvPerception; /** * 【转账场景报备信息】 各转账场景下需报备的内容,商户需要按照所属转账场景规则传参,详见转账场景报备信息字段说明。 */ @SerializedName("transfer_scene_report_infos") private List transferSceneReportInfos; @Override public String toString() { return GsonUtil.getGson().toJson(this); } }
/** * 转账场景报备信息 */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class TransferSceneReportInfo { /** * 【信息类型】 不能超过15个字符,商户所属转账场景下的信息类型,此字段内容为固定值,需严格按照转账场景报备信息字段说明传参。 */ @SerializedName("info_type") private String infoType; /** * 【信息内容】 不能超过32个字符,商户所属转账场景下的信息内容,商户可按实际业务场景自定义传参,需严格按照转账场景报备信息字段说明传参。 */ @SerializedName("info_content") private String infoContent; @Override public String toString() { return GsonUtil.getGson().toJson(this); } }
2、响应实体类
@Data public class TransferCreateResponse { @SerializedName("transfer_bill_no") private String transferBillNo; @SerializedName("out_bill_no") private String outBillNo; @SerializedName("create_time") private Date createTime; /** * 【单据状态】 商家转账订单状态 * 可选取值 * ACCEPTED: 转账已受理 * PROCESSING: 转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试。 * WAIT_USER_CONFIRM: 待收款用户确认,可拉起微信收款确认页面进行收款确认 * TRANSFERING: 转账中,可拉起微信收款确认页面再次重试确认收款 * SUCCESS: 转账成功 * FAIL: 转账失败 * CANCELING: 商户撤销请求受理成功,该笔转账正在撤销中 * CANCELLED: 转账撤销完成 */ @SerializedName("state") private String state; /** * 【失败原因】 订单已失败或者已退资金时,会返回订单失败原因 * ... */ @SerializedName("fail_reason") private String failReason; /** * 【跳转领取页面的package信息】 跳转微信支付收款页的package信息,APP调起用户确认收款或者JSAPI调起用户确认收款 时需要使用的参数。 * 单据创建后,用户24小时内不领取将过期关闭,建议拉起用户确认收款页面前,先查单据状态:如单据状态为待收款用户确认,可用之前的package信息拉起;单据到终态时需更换单号重新发起转账。 */ @SerializedName("package_info") private String packageInfo; }
@Data public class TransferQueryResponse { @SerializedName("mch_id") private String mchid; @SerializedName("transfer_bill_no") private String transferBillNo; @SerializedName("out_bill_no") private String outBillNo; @SerializedName("appid") private String appid; /** * 【单据状态】 商家转账订单状态 * 可选取值 * ACCEPTED: 转账已受理 * PROCESSING: 转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试。 * WAIT_USER_CONFIRM: 待收款用户确认,可拉起微信收款确认页面进行收款确认 * TRANSFERING: 转账中,可拉起微信收款确认页面再次重试确认收款 * SUCCESS: 转账成功 * FAIL: 转账失败 * CANCELING: 商户撤销请求受理成功,该笔转账正在撤销中 * CANCELLED: 转账撤销完成 */ @SerializedName("state") private String state; @SerializedName("transfer_amount") private Integer transferAmount; @SerializedName("transfer_remark") private String transferRemark; @SerializedName("fail_reason") private String failReason; @SerializedName("openid") private String openid; @SerializedName("user_name") private String userName; /** * 【单据创建时间】遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE, * yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss.表示时分秒, * TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。 * 例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒。 */ @SerializedName("create_time") private Date createTime; /** * 【最后一次状态变更时间】遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE, * yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss.表示时分秒, * TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。 * 例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒。 */ @SerializedName("update_time") private Date updateTime; }
@Data public class TransferCancelResponse { /** * 商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一 */ @SerializedName("out_bill_no") private String outBillNo; /** * 商家转账订单的主键,唯一定义此资源的标识 */ @SerializedName("transfer_bill_no") private String transferBillNo; /** * 【单据状态】 CANCELING: 撤销中;CANCELLED:已撤销 */ @SerializedName("state") private String state; /** * 【最后一次单据状态变更时间】 按照使用rfc3339所定义的格式,格式为yyyy-MM-DDThh:mm:ss+TIMEZONE */ @SerializedName("update_time") private Date updateTime; }
3、异步回调参数
@Data public class TransferNotification { @SerializedName("transfer_bill_no") private String transferBillNo; @SerializedName("out_bill_no") private String outBillNo; /** * 【单据状态】 商家转账订单状态 * 可选取值 * ACCEPTED: 转账已受理 * PROCESSING: 转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试。 * WAIT_USER_CONFIRM: 待收款用户确认,可拉起微信收款确认页面进行收款确认 * TRANSFERING: 转账中,可拉起微信收款确认页面再次重试确认收款 * SUCCESS: 转账成功 * FAIL: 转账失败 * CANCELING: 商户撤销请求受理成功,该笔转账正在撤销中 * CANCELLED: 转账撤销完成 */ @SerializedName("state") private String state; @SerializedName("mch_id") private String mchId; @SerializedName("transfer_amount") private Integer transferAmount; @SerializedName("openid") private String openid; /** * 【失败原因】 订单已失败或者已退资金时,会返回订单失败原因 * ... */ @SerializedName("fail_reason") private String failReason; /** * 【单据创建时间】遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE, * yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss.表示时分秒, * TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。 * 例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒。 */ @SerializedName("create_time") private Date createTime; /** * 【最后一次状态变更时间】遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE, * yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss.表示时分秒, * TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。 * 例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒。 */ @SerializedName("update_time") private Date updateTime; }
三、封装service(如果只兼容商家转账的朋友,且单商户配置的,可以在service上加@component注解,在构造方法上加入@autowired,即可实现单例模式(官方建议))
public class TransferNewService { private final HttpClient httpClient; private final PrivacyEncryptor encryptor; private final PrivacyDecryptor decryptor; /** * 实际注入实体类为 RSAAutoCertificateConfig * @param config 不要引用错了 包路径 com.wechat.pay.java.core.Config; */ public TransferNewService(Config config) { this.httpClient = new DefaultHttpClientBuilder() .credential(requireNonNull(config.createCredential())) .validator(requireNonNull(config.createValidator())) .build(); this.encryptor = config.createEncryptor(); this.decryptor = config.createDecryptor(); } /** * 创建商家转账订单 * @param request 请求参数 * @return 响应参数 */ public TransferCreateResponse createTransferOrder(TransferCreateRequest request) { String requestPath = "https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills"; Objects.requireNonNull(this.encryptor); request.setUserName(this.encryptor.encrypt(request.getUserName())); return executeHttpRequest(requestPath,HttpMethod.POST,request.toString(),TransferCreateResponse.class); } /** * 撤销商家转账订单 * 商户通过转账接口发起付款后,在用户确认收款之前可以通过该接口撤销付款。 * 该接口返回成功仅表示撤销请求已受理, * 系统会异步处理退款等操作, * 以最终查询单据返回状态为准。 * @param outBillNo 商户转账单号 * @return 响应参数 */ public TransferCancelResponse cancelTransferOrder(String outBillNo){ String requestPath = "https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}/cancel"; requestPath = requestPath.replace("{" + "out_bill_no" + "}", outBillNo); return executeHttpRequest(requestPath,HttpMethod.POST,null,TransferCancelResponse.class); } public TransferQueryResponse queryTransferByOutBillNo(String outBillNo){ String requestPath = "https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}"; requestPath = requestPath.replace("{" + "out_bill_no" + "}", outBillNo); TransferQueryResponse response = executeHttpRequest(requestPath, HttpMethod.GET, null, TransferQueryResponse.class); response.setUserName(decryptor.decrypt(response.getUserName())); return response; } public TransferQueryResponse queryTransferByTransferBillNo(String transferBillNo){ String requestPath = "https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/transfer-bill-no/{transfer_bill_no}"; requestPath = requestPath.replace("{" + "transfer_bill_no" + "}", transferBillNo); TransferQueryResponse response = executeHttpRequest(requestPath, HttpMethod.GET, null, TransferQueryResponse.class); response.setUserName(decryptor.decrypt(response.getUserName())); return response; } public T executeHttpRequest(String requestPath,HttpMethod method,String body,Class responseClass){ HttpHeaders headers = new HttpHeaders(); headers.addHeader(Constant.ACCEPT, MediaType.APPLICATION_JSON.getValue()); headers.addHeader(Constant.CONTENT_TYPE, MediaType.APPLICATION_JSON.getValue()); HttpRequest httpRequest = new HttpRequest.Builder() .httpMethod(method) .url(requestPath) .headers(headers) .body(StringUtils.hasText(body)?new JsonRequestBody.Builder().body(body).build():null) .build(); HttpResponse httpResponse = httpClient.execute(httpRequest, responseClass); return httpResponse.getServiceResponse(); } }
四、配置注入(单商户模式只需完成Config配置类注册即可调用)
非常重要****如果是24年11月以后注册的商户,请检查一下商户后台配置,评论区有兄弟提到平台证书模式可能新商户不支持
账户中心->API安全
根据自己的商户号证书类型,按照以下配置类类型注入到容器
1、使用公钥(注册以下配置类):
// 可以根据实际情况使用publicKeyFromPath或publicKey加载公钥 Config config = new RSAPublicKeyConfig.Builder() .merchantId("1900007291") //微信支付的商户号 .privateKeyFromPath("/Users/yourname/yourpath/apiclient_key.pem") // 商户API证书私钥的存放路径 .publicKeyFromPath("/Users/yourname/yourpath/pub_key.pem") //微信支付公钥的存放路径 .publicKeyId("PUB_KEY_ID_00000000000000000000000000000000") //微信支付公钥ID .merchantSerialNumber("5157F09EFDC096DE15EBE81A47057A72********") //商户API证书序列号 .apiV3Key("F09E**") //APIv3密钥 .build();
2、使用平台证书:
Config rsaConfig = new RSAAutoCertificateConfig.Builder() .merchantId(mchid) .privateKeyFromPath(privateKeyFromPath) .merchantSerialNumber(serialNumber) .apiV3Key(apiV3Key) .build();
单商户号可直接引入TransferNewService即可调用。
多商户或需要微信多执行器的朋友,需要单独开一个配置类,并且注入多个上图配置(或实现动态注册bean)
@Slf4j @Component public class WechatConfigManage { private final Map paymentServiceMap; private final Map transferNewServiceMap; private final Map refundServiceMap; private final Map serviceorderServiceMap; private final Map mchConfigs; public WechatConfigManage(ApplicationContext applicationContext) { //获取IOC容器中所有RSA配置类 mchConfigs = new HashMap(); Map configs = applicationContext.getBeansOfType(Config.class); paymentServiceMap = new HashMap(); transferNewServiceMap = new HashMap(); refundServiceMap = new HashMap(); serviceorderServiceMap = new HashMap(); //根据不同的RSAConfig生成不同的service (key=mchid,value=service实例) //不需要其它service的请删除其它service相关代码(serviceorderService也是自定义的微信支付分的) for (String key : configs.keySet()) { Config config = configs.get(key); String mchid = config.createCredential().getMerchantId(); mchConfigs.put(mchid, config); paymentServiceMap.put(mchid, new JsapiServiceExtension.Builder().config(config).build()); transferNewServiceMap.put(mchid, new TransferNewService(config)); refundServiceMap.put(mchid, new RefundService.Builder().config(config).build()); serviceorderServiceMap.put(mchid, new ServiceorderService(config)); } } /** * 根据获取微信支付配置信息类(用于生成异步回调parser) * @param mchid 商户号 * @return 配置信息 */ public Config getRSAAConfig(String mchid){ return this.mchConfigs.get(mchid); } /** * 获取商家转账service * @param mchid 商户号 * @return service */ public TransferNewService getTransferService(String mchid){ TransferNewService service = this.transferNewServiceMap.get(mchid); if (service==null) throw new PayException(PayExceptionCode.WECHAT_PAY_CONFIG_ERROR); return service; } }
五、内部调用(请完整填写封装的请求参数,仅示范发起转账调用)
1、单商户模式直接引入TransferNewService
2、多商户模式:
六、异步回调(只要引入了第一步的依赖,以下方法适用于任何微信的异步回调)
1、业务逻辑请自己实现,因为流程较之前的微信转账到零钱有变化,一定要判断好异步回调返回的状态
2、如果多商户配置,请在发起转账请求时在异步回调地址用路径传参带上商户号(或者可以自定义商户号键值)
3、new NotificationParser(config),参数请按照注册的配置类类型强转(详情见第四步)
@Override public void wechatTransferNotify(String mchid) { HttpServletResponse response = ServletUtils.getResponse(); assert response != null; try { // 以支付通知回调为例,验签、解密并转换成 Transaction RequestParam param = getRequestParam(); // 以下强转类型 请按照自己的公钥或证书配置类类型来转换 TransferNotification transfer = new NotificationParser((RSAAutoCertificateConfig)wechatConfigManage.getRSAAConfig(mchid)).parse(param, TransferNotification.class); log.info("[微信转账异步回调] transfer:{}",transfer); } catch (ValidationException e) { // 签名验证失败,返回 401 UNAUTHORIZED 状态码 log.error("微信签名验证失败", e); response.setStatus(401); } catch (Exception e){ log.error("系统异常",e); response.setStatus(500); } }
/** * 获取请求参数 请求体-json(未json解析之前的原主体)+请求头 * @return 请求参数 */ private RequestParam getRequestParam(){ HttpServletRequest request = ServletUtils.getRequest(); assert request != null; String body = getReqStreamContent(request); return new RequestParam.Builder() .serialNumber(request.getHeader("Wechatpay-Serial")) .nonce(request.getHeader("Wechatpay-Nonce")) .signature(request.getHeader("Wechatpay-Signature")) .timestamp(request.getHeader("Wechatpay-Timestamp")) .body(body) .build(); }
/** * 获取请求流中的数据请求数据 * @param req 请求对象 * @return String */ public static String getReqStreamContent(HttpServletRequest req) { try { BufferedReader br = new BufferedReader(new InputStreamReader(req.getInputStream(), StandardCharsets.UTF_8)); String line ; StringBuilder sb = new StringBuilder(); while ((line = br.readLine()) != null){ sb.append(line); } br.close(); return sb.toString(); } catch (IOException e) { log.error("[WechatUtils getReqStreamContent] get req fail req:{} error message:{}" ,req,e.getMessage()); } return "fail"; }
七、提现回查
如果转账单据一直是未领取状态,需要进行回查,以下示例代码以xxl-job的定时任务示例
@Component @Slf4j public class TransferCheckJob extends IJobHandler { @Resource private TransferRecordService transferRecordService; @Resource private TransferNewService transferNewService; @Override @XxlJob("TransferCheckJob") public void execute() throws Exception { //这里按你们自己的数据库记录查询,超过24小时未领取的单据 //数据库必须存在以下字段: // 状态、转账时间、商家单号、微信单号(多商户配置还需保存mchid) LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); wrapper.eq(TransferRecord::getStatus,1) .le(TransferRecord::getTransferTime, DateUtils.addDays(new Date(),-1)) .eq(TransferRecord::getTransferState, TransferStateEnum.WAIT_USER_CONFIRM.getState()); List list = transferRecordService.list(wrapper); log.info("[微信商家转账回查任务] size:{}",list.size()); if (ObjectUtil.isNotEmpty(list)){ for (TransferRecord record : list) { TransferQueryResponse response = transferNewService.queryTransferByOutBillNo(record.getOutBillNo()); log.info("[提现回查] response:{}",response); if (response.getState().equals(TransferStateEnum.SUCCESS.getState())){ //转账成功 业务逻辑 log.info("[转账成功]"); }else if (response.getState().equals(TransferStateEnum.FAIL.getState())){ //转账失败 业务逻辑 log.info("[转账失败]"); }else if (response.getState().equals(TransferStateEnum.WAIT_USER_CONFIRM.getState())){ //超时未领取,示例业务逻辑为直接取消,用户重新申请 //根据自己的业务逻辑可以取消,或者重新在同一个转账单号下申请单据信息(package_info) TransferCancelResponse cancelResponse = transferNewService.cancelTransferOrder(record.getWithdrawalNo()); log.info("[取消转账] response:{}",cancelResponse); if (cancelResponse.getState().equals(TransferStateEnum.CANCELLED.getState())){ //取消成功 }else if (cancelResponse.getState().equals(TransferStateEnum.CANCELING.getState())){ //取消中 等待异步回调(一般情况下,返回取消中可以当做取消成功处理) } }else if (response.getState().equals(TransferStateEnum.CANCELLED.getState())) { log.info("[转账已取消]"); //同步数据库数据 } } } } }
/* * 转账状态枚举类 */ public enum TransferStateEnum { ACCEPTED("ACCEPTED"), PROCESSING("PROCESSING"), WAIT_USER_CONFIRM("WAIT_USER_CONFIRM"), TRANSFERING("TRANSFERING"), SUCCESS("SUCCESS"), FAIL("FAIL"), CANCELING("CANCELING"), CANCELLED("CANCELLED"); private final String state; TransferStateEnum(String state){ this.state = state; } public String getState(){ return this.state; } }
写在最后:
之前对接了微信转账到零钱的朋友请注意,流程变化很大,发起转账成功以后,请一定要保存好返回的packageInfo数据,前端需要通过此数据,给用户弹出确认提示,且24小时内不确认会自动过期。