微信商家转账API V3教程(2025新版) JAVA全源码 无缝对接(无需踩坑)

前言:

        1、本教程非常简单,全源码

        2、本文章仅介绍java后端对接流程,功能开通以及前端调用(基础版无需提交资料审核,不过额度只有200),请参考官方文档产品介绍_商家转账|微信支付商户文档中心

        3、从去年年底开始,原微信商家转账到零钱功能已停止开放,替换为商家转账功能。与之前的商家转账到零钱相比,流程增加了用户确认步骤,通过API发起转账以后,还需要用户通过商户小程序点击确认(前端指引->JSAPI调起用户确认收款_商家转账|微信支付商户文档中心

一、引入微信支付官方SDK(不需要重复造轮子)

<!-- 微信支付sdk  -->
<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-java</artifactId>
    <version>0.2.15</version>
</dependency>

二、封装实体类

        后端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<TransferSceneReportInfo> 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;

    /**
     * 【失败原因】 订单已失败或者已退资金时,会返回订单失败原因
     * <a href="https://pay.weixin.qq.com/doc/v3/merchant/4013774966">...</a>
     */
    @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;

    /**
     * 【失败原因】 订单已失败或者已退资金时,会返回订单失败原因
     * <a href="https://pay.weixin.qq.com/doc/v3/merchant/4013774966">...</a>
     */
    @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> T executeHttpRequest(String requestPath,HttpMethod method,String body,Class<T> 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<T> 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<String, JsapiServiceExtension> paymentServiceMap;

    private final Map<String, TransferNewService> transferNewServiceMap;

    private final Map<String, RefundService> refundServiceMap;

    private final Map<String, ServiceorderService> serviceorderServiceMap;

    private final Map<String, Config> mchConfigs;

    public WechatConfigManage(ApplicationContext applicationContext) {
        //获取IOC容器中所有RSA配置类
        mchConfigs = new HashMap<>();
        Map<String, Config> 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";
    }

写在最后:

        之前对接了微信转账到零钱的朋友请注意,流程变化很大,发起转账成功以后,请一定要保存好返回的packageInfo数据,前端需要通过此数据,给用户弹出确认提示,且24小时内不确认会自动过期。

微信商家转账到零钱 V3 的实现需要通过微信支付 API 接口来实现,以下是 JAVA 版本的代码实现: 1. 导入依赖包 ```java import java.util.HashMap; import java.util.Map; import com.github.wxpay.sdk.WXPay; import com.github.wxpay.sdk.WXPayConfig; import com.github.wxpay.sdk.WXPayConstants; import com.github.wxpay.sdk.WXPayUtil; ``` 2. 构造微信支付配置对象 ```java public class WXPayConfigImpl implements WXPayConfig { private String appID; // 公众账号ID或应用ID private String mchID; // 商户号 private String key; // 商户密钥 private String certPath; // 商户证书路径 private int httpConnectTimeoutMs = 6 * 1000; // 连接超时时间 private int httpReadTimeoutMs = 8 * 1000; // 读取超时时间 public WXPayConfigImpl(String appID, String mchID, String key, String certPath) { this.appID = appID; this.mchID = mchID; this.key = key; this.certPath = certPath; } @Override public String getAppID() { return appID; } @Override public String getMchID() { return mchID; } @Override public String getKey() { return key; } @Override public InputStream getCertStream() { try { return new FileInputStream(new File(certPath)); } catch (FileNotFoundException e) { e.printStackTrace(); } return null; } @Override public int getHttpConnectTimeoutMs() { return httpConnectTimeoutMs; } @Override public int getHttpReadTimeoutMs() { return httpReadTimeoutMs; } } ``` 3. 构造微信支付对象 ```java WXPayConfig wxPayConfig = new WXPayConfigImpl(appID, mchID, key, certPath); WXPay wxPay = new WXPay(wxPayConfig, WXPayConstants.SignType.MD5, true); ``` 4. 构造参数并调用接口 ```java // 构造请求参数 Map<String, String> reqData = new HashMap<String, String>(); reqData.put("mch_appid", appID); reqData.put("mchid", mchID); reqData.put("nonce_str", WXPayUtil.generateNonceStr()); reqData.put("partner_trade_no", "xxxxxxxxxxxx"); // 商户订单号 reqData.put("openid", "xxxxxxxxxxxx"); // 用户openid reqData.put("check_name", "NO_CHECK"); // 不校验真实姓名 reqData.put("amount", "100"); // 转账金额 reqData.put("desc", "测试转账"); // 转账描述 reqData.put("spbill_create_ip", "127.0.0.1"); // 调用接口的机器IP地址 // 调用接口 Map<String, String> respData = wxPay.transfer(reqData); ``` 其中,`appID`、`mchID`、`key`、`certPath` 等参数需要根据实际情况填写。`wxPay.transfer(reqData)` 方法返回的是一个 Map 对象,包含了接口响应的所有信息,可以根据业务需求进行处理。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值