微信-商家转账到零钱

1,593 阅读8分钟

商家转账至零钱

0、功能介绍

效果演示

商家转账到零钱提供商户同时向多个用户微信零钱转账的能力。具有高效、免费、快速到账、安全等优点。商户可以使用商家转账到零钱用于费用报销、员工福利等场景,提高财务人员转账效率。

效果图如下

image.png

开通条件

需满足以下三点

  1. 入驻满90天
  2. 连续正常交易30天
  3. 保持正常健康交易

功能开通

微信商户平台 -》产品中心 -》我的产品 -》商家转账至零钱

开通后需设置产品,结合自身业务进行设置

产品介绍文档:pay.weixin.qq.com/wiki/doc/ap…

微信支付商户平台:pay.weixin.qq.com/index.php/c…

appid、商户证书序列号、商户号请仔细阅读文档获取,本文不概述如何获取

1、环境准备

所需环境准备

<!-- https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient -->
<dependency>
  <groupId>com.github.wechatpay-apiv3</groupId>
  <artifactId>wechatpay-apache-httpclient</artifactId>
  <version>0.2.2</version>
</dependency>
<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-all</artifactId>
  <version>5.5.7</version>
</dependency>
<!-- apache 工具类 -->
<dependency>
  <groupId>commons-codec</groupId>
  <artifactId>commons-codec</artifactId>
  <version>1.11</version>
</dependency>
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
  <version>3.4</version>
</dependency>
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-io</artifactId>
  <version>1.3.2</version>
</dependency>

所需:Appid、Api密钥、mchId、商户证书,公钥、私钥等等

长这个样

商家转账到零钱接入前参考文档pay.weixin.qq.com/wiki/doc/ap…

关于API V3

与企业转账至零钱使用Api不一样,商家转账至零钱使用全新API

参考文档:pay.weixin.qq.com/wiki/doc/ap…

构建Client

import cn.hutool.core.io.FileUtil;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import org.apache.http.impl.client.CloseableHttpClient;

import java.io.File;
import java.io.InputStream;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Arrays;

/**
 * WxClient
 *
 */
public class WeChatClient {

    /**
     * 商户号
     */
    private static String merchantId = "";
    /**
     * 证书编码
     */
    private static String serialNo = "";

    /**
     * 微信通讯client
     *
     * @return CloseableHttpClient
     */
    public static CloseableHttpClient getClient() {
        // 商户私钥文件
        File privateKeyFile = new File("apiclient_key.pem");
        InputStream privateKeyInputStream = FileUtil.getInputStream(privateKeyFile);

        // 微信平台公钥文件
        File platFormKeyFile= new File("wechatpay.pem");
        InputStream platformCertInputStream = FileUtil.getInputStream(platFormKeyFile);

        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(privateKeyInputStream);
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(merchantId, serialNo, merchantPrivateKey)
                .withWechatpay(Arrays.asList(PemUtil.loadCertificate(platformCertInputStream)));
        return builder.build();
    }

    /**
     * 微信敏感数据加密,用户加密转账人姓名等等
     * 加密前:张三
     * 
     *
     * @return
     */
    public static X509Certificate getSaveCertificates() {
        // 用标准RSA算法,公钥由微信侧提供
        File file2 = new File("public_key.pem");
        InputStream platformCertInputStream = FileUtil.getInputStream(file2);
        return PemUtil.loadCertificate(platformCertInputStream);
    }
}

2、请求构建

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import static org.apache.http.HttpHeaders.ACCEPT;
import static org.apache.http.entity.ContentType.APPLICATION_JSON;
import static org.apache.http.HttpHeaders.CONTENT_TYPE;

/**
* 微信支付专用类,请求操作方法
* 参考文章:https://blog.csdn.net/zkinghao/article/details/122428226?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165638560516782388092496%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=165638560516782388092496&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-2-122428226-null-null.142^v24^huaweicloudv2,157^v15^new_3&utm_term=%E5%BE%AE%E4%BF%A1%E5%95%86%E5%AE%B6%E8%BD%AC%E8%B4%A6%E8%87%B3%E9%9B%B6%E9%92%B1+%E5%95%86%E5%AE%B6%E6%98%8E%E7%BB%86%E5%8D%95%E5%8F%B7%E6%9F%A5%E8%AF%A2%E6%98%8E%E7%BB%86%E5%8D%95&spm=1018.2226.3001.4187
*
*/
public class WxHttpUtil {
    
    /**
    * 发起批量转账API
    * 文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_1.shtml
    *
    * @param url        批量转账API地址
    * @param paramsMap  批量转账参数
    * @throws Exception 请求相关错误信息
    * @return 请求字符串,需自身处理
    */
    public static String batchPay(String url, Map<String, Object> paramsMap) throws Exception{
        CloseableHttpClient httpClient = WeChatClient.getClient();
        
        String body = JSONUtil.toJsonStr(paramsMap);
        log.info("发送商家转账至零钱请求,请求参数:{}", body);
        
        CloseableHttpResponse response = postRequestApi(url, body, httpClient);
        try {
            String result = EntityUtils.toString(response.getEntity());
            log.info("发送商家转账至零钱请求,返回结果{}", result);
            return result;
        } catch (Exception e){
            log.error("发送商家转账至零钱请求出错,错误信息{}", e);
        } finally {
            response.close();
        }
        return StringUtils.EMPTY;
    }
    
    /**
    * 请求指定api
    *
    * @param url 请求地址
    * @param body 请求参数,需转换为JSON
    * @param httpClient httpclient
    * @return 结果
    * @throws Exception
    */
    public static CloseableHttpResponse postRequestApi(String url, String body, CloseableHttpClient httpClient) throws Exception {
        HttpPost httpPost = new HttpPost(url);
        httpPost.addHeader(ACCEPT, APPLICATION_JSON.toString());
        httpPost.addHeader(CONTENT_TYPE, APPLICATION_JSON.toString());
        httpPost.addHeader("Wechatpay-Serial", "微信平台证书序列号");
        httpPost.setEntity(new StringEntity(body, "UTF-8"));
        return httpClient.execute(httpPost);
    }
}

3、发送请求

class Test{
    public Object merchantTransfer() {
        Map<String, Object> merchantMap = getMerchantParamsMap();
        try {
            String responseResult = WxHttpUtil.batchPay("https://api.mch.weixin.qq.com/v3/transfer/batches", merchantMap);
            TransferResponseEntity responseEntity = JsonUtils.jsonToPojo(responseResult, TransferResponseEntity.class);
            return responseEntity
            } catch (Exception e) {
              return null;
        }
    }
    
    /**
    * 获取商家转账参数
    *
    * @return
    */
    private Map<String, Object> getMerchantParamsMap() {
        String outNo = ""; 	   // 商家批次单号 需唯一
        String sub_outNo = ""; // 商家明细单号 需唯一
        
        Map<String, Object> merchantMap = getMerchantParams(outNo, "公众号appid或小程序appid");
        List<Map> detailList = getDetailParams(sub_outNo);
        
        merchantMap.put("transfer_detail_list", detailList);
        return merchantMap;
    }
    
    /**
    * 商家转账参数信息
    *
    * @param outNo 商家批次单号,此参数只能由数字、大小写字母组成,在商户系统内部唯一
    * @param appId 直连商户的appid
    * @param merchant 转账参数
    * @return 封装好的Map
    */
    private Map<String, Object> getMerchantParams(String outNo, String appId) {
        Map<String, Object> postMap = new HashMap<String, Object>();
        // 直连商户的appid 小程序 或 公众号
        postMap.put("appid", appId);
        // 商家批次单号 根据改单号可以查询对应商家订单
        postMap.put("out_batch_no", outNo);
        //该笔批量转账的名称
        postMap.put("batch_name", "测试转账名称");
        //转账说明,UTF8编码,最多允许32个字符
        postMap.put("batch_remark", "测试备注");
        //转账金额单位为“分”。 总金额
        postMap.put("total_amount", 100);
        //转账总笔数
        postMap.put("total_num", 1);
        return postMap;
    }
    
    /**
    * 转账明细列表
    *
    * @param outNo   商家明细单号
    * @return
    */
    private List<Map> getDetailParams(String outNo) {
        List<Map> list = new ArrayList<>();
        Map<String, Object> subMap = new HashMap<>(4);
        //商家明细单号
        subMap.put("out_detail_no", outNo);
        //转账金额
        subMap.put("transfer_amount", 100);
        //转账备注
        subMap.put("transfer_remark", "转账备注");
        //用户在直连商户应用下的用户标示
        subMap.put("openid", merchant.getOpenid());
        // 收款用户姓名,需参考文档,进行加密
        //        if(StringUtils.isNotEmpty(merchant.getUserName())){
        //            subMap.put("user_name", merchant.getUserName());
        //        }
        list.add(subMap);
        return list;
    }
}

TransferResponseEntity

/**
 * 商家转账接口返回数据
 *
 */
@Data
@NoArgsConstructor
public class TransferResponseEntity implements Serializable {
    /**
     * 错误code
     */
    private String code;
    /**
     * 错误信息
     */
    private String message;
    /**
     * 微信批次单号
     */
    private String batch_id;
    /**
     * 商家批次单号
     */
    private String out_batch_no;
    /**
     * 批次创建时间
     */
    private Date create_time;
}

工具类

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.List;

public class JsonUtils {
    
    // 定义jackson对象
    private static final ObjectMapper MAPPER = new ObjectMapper();
    
    /**
    * 将对象转换成json字符串。
    * @param data
    * @return
    */
    public static String objectToJson(Object data) {
        try {
            String string = MAPPER.writeValueAsString(data);
            return string;
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }
    
    /**
    * 将json结果集转化为对象
    * 
    * @param jsonData json数据
    * @param beanType 对象中的object类型
    * @return
    */
    public static <T> T jsonToPojo(String jsonData, Class<T> beanType) {
        try {
            T t = MAPPER.readValue(jsonData, beanType);
            return t;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    
    /**
    * 将json数据转换成pojo对象list
    * @param jsonData
    * @param beanType
    * @return
    */
    public static <T>List<T> jsonToList(String jsonData, Class<T> beanType) {
        JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
        try {
            List<T> list = MAPPER.readValue(jsonData, javaType);
            return list;
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return null;
    }
    
}

接口返回参数信息

{
  "code": null,
  "message": null,
  "batch_id": "103000109xxxxxxxxxxxxxxxxxxx",
  "out_batch_no": "220629xxxxxxxxxxxxx",
  "create_time": "2022-06-29T02:31:09.000+0000"
}

到账时间并非实时

image.png

如长时间用户未收到转账,大概率该转账出错,需要根据商家明细单号,商家批次单号查询详细信息。

默认转账需管理员确认,设置对应金额免密即可不用管理员确认

参考文档:pay.weixin.qq.com/wiki/doc/ap…

67adb741c52d094fda72898ab47e4d9.png

4、结果查询

开发参考文档:pay.weixin.qq.com/wiki/doc/ap…

如果请求发送成功,但是用户未收到商家转账,很可能是转账出错,需要根据 商家明细单号,商家批次单号 查询明细信息。

根据接口返回的信息,对照文档错误码,即可知道具体信息

image.png

返回参数信息

{
  "message": null,
  "code": null,
  "batch_id": "xxxxxxxxxxxxxxxxxxxxxxxxxx",
  "openid": "xxxx",
  "detail_status": "SUCCESS",
  "sp_mchid": "xxxx",
  "out_batch_no": "xxxxxx",
  "detail_id": "xxxxxxxxxxxxxxxxxxxxxxxxxx",
  "update_time": "2022-06-28T03:37:03.000+0000",
  "transfer_amount": 100,
  "appid": "xxxxxxxxxxxxxxxxxxxxxxxxxx",
  "out_detail_no": "xxxxxxxxxxxxxxxxxxxxxxxxxx",
  "initiate_time": "2022-06-28T03:37:02.000+0000",
  "transfer_remark": "转账备注",
  "fail_reason": ""
}

构建

public class WxHttpUtil{

    public static final Integer SUCCESS_CODE = 200;
    
    /**
    * 商家明细单号查询明细单API
    * 文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_6.shtml
    *
    * @param batchCode 批次号
    * @param detailCode 明细号
    * @throws Exception
    * @return 请求结果
    */
    public static String queryMerchantDetail(String batchCode, String detailCode) throws Exception {
        CloseableHttpClient httpClient = WeChatClient.getClient();
        String requestUrl = getDetailsUrl(batchCode, detailCode);
        CloseableHttpResponse response = getRequestApiResult(requestUrl, httpClient);
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            JSONObject jsonObject = JSONUtil.parseObj(bodyAsString);
            if (SUCCESS_CODE.equals(response.getStatusLine().getStatusCode())) {
                log.info("查询商家明细单成功,接口返回结果:{}", bodyAsString);
                return jsonObject.toString();
            }
        } catch (Exception e){
            log.error("查询商家明细单号出错,出错信息:{}", e);
        } finally {
            response.close();
        }
        return StringUtils.EMPTY;
    }
    
    /**
    * 获取请求商家明细单地址
    *
    * @param batchCode 商家明细单号
    * @param detailCode 商家批次单号
    * @return
    */
    private static String getDetailsUrl(String batchCode, String detailCode){
        StringBuilder url = new StringBuilder("https://api.mch.weixin.qq.com/v3/partner-transfer/batches/out-batch-no/");
        url.append(batchCode).append("/details/out-detail-no/").append(detailCode);
        return url.toString();
    }
    
    /**
    * 请求指定api,返回结果
    *
    * @param requestUrl 请求地址
    * @param httpClient httpclient
    * @return 结果
    * @throws Exception
    */
    private static CloseableHttpResponse getRequestApiResult(String requestUrl, CloseableHttpClient
                                                             httpClient) throws Exception{
        URIBuilder uriBuilder = new URIBuilder(requestUrl);
        HttpGet httpGet = new HttpGet(uriBuilder.build());
        httpGet.addHeader(ACCEPT, APPLICATION_JSON.toString());
        return httpClient.execute(httpGet);
    }
}

请求

class Test{
    public Object getMerchantDetail(MerchantDetailDTO merchantDetailDTO) throws IOException, URISyntaxException {
        try {
            String responseResult = WxHttpUtil.queryMerchantDetail(merchantDetailDTO.getOutBatchNo(),
                                                                   merchantDetailDTO.getOutDetailNo());
            if(StringUtils.isEmpty(responseResult)){
                return requestError();
            }
            
            MerchantDetailBO merchantDetailBO = JsonUtils.jsonToPojo(responseResult, MerchantDetailBO.class);
            return Object;
        } catch (Exception e) {
            logger.info("查询商家明细单失败, 错误原因: {}", e);
        }
        return requestError();
    }
}

BO

/**
 * 查询商家转账至零钱明细接口
 *
 */
@Data
public class MerchantDetailDTO {

    /**
     * 商家明细单号
     */
    @NotBlank(message = "商家明细单号不能为空")
    private String outBatchNo;
    /**
     * 商家批次单号
     */
    @NotBlank(message = "商家批次单号不能为空")
    private String outDetailNo;
}

5、参考文章

还有许多Api,参考以下文章进行开发

blog.csdn.net/zkinghao/ar…

6、完整Code

WxHttpUtil

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.wechat.pay.contrib.apache.httpclient.util.RsaCryptoUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import javax.crypto.IllegalBlockSizeException;
import java.security.cert.X509Certificate;
import java.util.Map;

import static org.apache.http.HttpHeaders.ACCEPT;
import static org.apache.http.entity.ContentType.APPLICATION_JSON;
import static org.apache.http.HttpHeaders.CONTENT_TYPE;

/**
 * 微信支付专用类,请求操作方法
 * 参考文章:https://blog.csdn.net/zkinghao/article/details/122428226?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165638560516782388092496%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=165638560516782388092496&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-2-122428226-null-null.142^v24^huaweicloudv2,157^v15^new_3&utm_term=%E5%BE%AE%E4%BF%A1%E5%95%86%E5%AE%B6%E8%BD%AC%E8%B4%A6%E8%87%B3%E9%9B%B6%E9%92%B1+%E5%95%86%E5%AE%B6%E6%98%8E%E7%BB%86%E5%8D%95%E5%8F%B7%E6%9F%A5%E8%AF%A2%E6%98%8E%E7%BB%86%E5%8D%95&spm=1018.2226.3001.4187
 *
 */
@Slf4j
public class WxHttpUtil {

    public static final Integer SUCCESS_CODE = 200;

    /**
     * 商家明细单号查询明细单API
     * 文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_6.shtml
     *
     * @param batchCode 批次号
     * @param detailCode 明细号
     * @throws Exception
     * @return 请求结果
     */
    public static String queryMerchantDetail(String batchCode, String detailCode) throws Exception {
        CloseableHttpClient httpClient = WeChatClient.getClient();
        String requestUrl = getDetailsUrl(batchCode, detailCode);
        CloseableHttpResponse response = getRequestApiResult(requestUrl, httpClient);
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            JSONObject jsonObject = JSONUtil.parseObj(bodyAsString);
            if (SUCCESS_CODE.equals(response.getStatusLine().getStatusCode())) {
                log.info("查询商家明细单成功,接口返回结果:{}", bodyAsString);
                return jsonObject.toString();
            }
        } catch (Exception e){
            log.error("查询商家明细单号出错,出错信息:{}", e);
        } finally {
            response.close();
        }
        return StringUtils.EMPTY;
    }

    /**
     * 请求指定api,返回结果
     * @param requestUrl 请求地址
     * @param httpClient httpclient
     * @return 结果
     * @throws Exception
     */
    private static CloseableHttpResponse getRequestApiResult(String requestUrl, CloseableHttpClient
            httpClient) throws Exception{
        URIBuilder uriBuilder = new URIBuilder(requestUrl);
        HttpGet httpGet = new HttpGet(uriBuilder.build());
        httpGet.addHeader(ACCEPT, APPLICATION_JSON.toString());
        return httpClient.execute(httpGet);
    }

    /**
     * 获取请求商家明细单地址
     *
     * @param batchCode 商家明细单号
     * @param detailCode 商家批次单号
     * @return
     */
    private static String getDetailsUrl(String batchCode, String detailCode){
        StringBuilder url = new StringBuilder("https://api.mch.weixin.qq.com/v3/partner-transfer/batches/out-batch-no/");
        url.append(batchCode).append("/details/out-detail-no/").append(detailCode);
        return url.toString();
    }

    /**
     * 发起批量转账API
     * 文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_1.shtml
     *
     * @param url        批量转账API地址
     * @param paramsMap  批量转账参数
     * @throws Exception 请求相关错误信息
     * @return 请求字符串,需自身处理
     */
    public static String batchPay(String url, Map<String, Object> paramsMap) throws Exception{
        CloseableHttpClient httpClient = WeChatClient.getClient();
        // encryptUserName("user_name", WeChatClient.getSaveCertificates());

        String body = JSONUtil.toJsonStr(paramsMap);
        log.info("发送商家转账至零钱请求,请求参数:{}", body);

        CloseableHttpResponse response = postRequestApi(url, body, httpClient);
        try {
            String result = EntityUtils.toString(response.getEntity());
            log.info("发送商家转账至零钱请求,返回结果{}", result);
            return result;
        } catch (Exception e){
            log.error("发送商家转账至零钱请求出错,错误信息{}", e);
        } finally {
            response.close();
        }
        return StringUtils.EMPTY;
    }

    /**
     * 1、收款方姓名。采用标准RSA算法,公钥由微信侧提供
     * 2、该字段需进行加密处理,加密方法详见敏感信息加密说明。(提醒:必须在HTTP头中上送Wechatpay-Serial)
     * 3、商户需确保收集用户的姓名信息,以及向微信支付传输用户姓名和账号标识信息做一致性校验已合法征得用户授权
     * 文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_1.shtml
     *
     * @param userName 用户姓名信息
     * @param x509Certificate 证书
     * @return 加密后字符串
     * @throws IllegalBlockSizeException
     */
    public static String encryptUserName(String userName, X509Certificate x509Certificate) throws IllegalBlockSizeException {
        return RsaCryptoUtil.encryptOAEP(userName, x509Certificate);
    }

    /**
     * 请求指定api
     *
     * @param url 请求地址
     * @param body 请求参数,需转换为JSON
     * @param httpClient httpclient
     * @return 结果
     * @throws Exception
     */
    public static CloseableHttpResponse postRequestApi(String url, String body, CloseableHttpClient httpClient) throws Exception {
        HttpPost httpPost = new HttpPost(url);
        httpPost.addHeader(ACCEPT, APPLICATION_JSON.toString());
        httpPost.addHeader(CONTENT_TYPE, APPLICATION_JSON.toString());
        httpPost.addHeader("Wechatpay-Serial", "微信平台证书序列号");
        httpPost.setEntity(new StringEntity(body, "UTF-8"));
        return httpClient.execute(httpPost);
    }
}


参考文档地址:

商家转账到零钱产品介绍: pay.weixin.qq.com/wiki/doc/ap…

微信商户平台: pay.weixin.qq.com/index.php/c…

商家转账到零钱接入前准备:pay.weixin.qq.com/wiki/doc/ap…

APIV3简介:pay.weixin.qq.com/wiki/doc/ap…

商家明细单号查询明细单API:pay.weixin.qq.com/wiki/doc/ap…

发起商家转账API:pay.weixin.qq.com/wiki/doc/ap…

参考文章:blog.csdn.net/zkinghao/ar…

不足之处还请提出