Java之微信 APIv3 商家转账到零钱

1、微信专用支付类 WxHttpUtil :

import lombok.extern.slf4j.Slf4j;
import org.apache.http.*;
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.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.io.IOException;

import static org.apache.http.HttpHeaders.*;
import static org.apache.http.entity.ContentType.APPLICATION_JSON;

/**
 * 微信支付专用类 请求操作方法
 *
 */
@Slf4j
public class WxHttpUtil {
    /**
     * 发起批量转账API 批量转账到零钱
     *
     * @param requestUrl     请求地址
     * @param requestJson    组合参数
     * @param paySerialNo    商户证书序列号
     * @param privateKeyPath 商户私钥证书路径
     * @return String
     */
    public static String postTransBatRequest(
            String requestUrl,
            String requestJson,
            String paySerialNo,
            String mchId,
            String privateKeyPath, String url) {
        CloseableHttpResponse response = null;
        HttpEntity entity = null;
        CloseableHttpClient httpClient = null;
        try {
            HttpPost httpPost = createHttpPost(requestUrl, requestJson, paySerialNo, mchId, privateKeyPath, url);
            httpClient = HttpClients.createDefault();
            // 发起转账请求
            response = httpClient.execute(httpPost);
            log.info("response:{}", response);
            entity = response.getEntity();
            // 获取返回的数据
            log.info(String.format("-----getHeaders.Request-ID:%s", response.getHeaders("Request-ID")));
            return EntityUtils.toString(entity);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭流
            try {
                if (httpClient != null) {
                    httpClient.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * 账单查询
     *
     * @param requestUrl     请求完整地址
     * @param paySerialNo    商户证书序列号
     * @param privateKeyPath 商户私钥证书路径
     * @param privateKeyPath 支付秘钥
     * @return String
     */
    public static String getTransBatRequest(
            String requestUrl,
            String paySerialNo,
            String mchId,
            String privateKeyPath, String url) {
        CloseableHttpResponse response = null;
        HttpEntity entity = null;
        CloseableHttpClient httpClient = null;
        try {
            HttpGet httpPost = createHttpGet(requestUrl, paySerialNo, mchId, privateKeyPath, url);
            httpClient = HttpClients.createDefault();
            // 发起转账请求
            response = httpClient.execute(httpPost);
            log.info("response:{}", response);
            entity = response.getEntity();
            // 获取返回的数据
            log.info(String.format("-----getHeaders.Request-ID:%s", response.getHeaders("Request-ID")));
            return EntityUtils.toString(entity);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭流
            try {
                if (httpClient != null) {
                    httpClient.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * @param requestUrl     请求完整地址
     * @param requestJson    请求参数
     * @param paySerialNo    支付证书序列号
     * @param mchId          商户号
     * @param privateKeyPath 私钥路径
     * @param servletPath    相对路径
     * @return HttpPost
     */
    private static HttpPost createHttpPost(String requestUrl,
                                           String requestJson,
                                           String paySerialNo,
                                           String mchId,
                                           String privateKeyPath, String servletPath) {
        //商户私钥证书
        HttpPost httpPost = new HttpPost(requestUrl);
        // NOTE: 建议指定charset=utf-8。低于4.4.6版本的HttpCore,不能正确的设置字符集,可能导致签名错误
        httpPost.addHeader(ACCEPT, APPLICATION_JSON.toString());
        httpPost.addHeader(CONTENT_TYPE, APPLICATION_JSON.toString());
        httpPost.addHeader("Wechatpay-Serial", paySerialNo);

        //-------------------------核心认证 start-----------------------------------------------------------------
        String strToken = null;
        try {
            log.info("requestJson:{}", requestJson);
            strToken = WxPayV3Util.getToken("POST",
                    servletPath,
                    requestJson, mchId, paySerialNo, privateKeyPath);
        } catch (Exception e) {
            log.error("createHttpPost error:", e);
            e.printStackTrace();
        }
        StringEntity reqEntity = new StringEntity(requestJson, APPLICATION_JSON);
        log.info("token " + strToken);
        // 添加认证信息
        httpPost.addHeader("Authorization",
                "WECHATPAY2-SHA256-RSA2048" + " "
                        + strToken);
        //---------------------------核心认证 end---------------------------------------------------------------
        httpPost.setEntity(reqEntity);
        return httpPost;
    }

    /**
     * 创建get 请求
     *
     * @param requestUrl     请求完整地址
     * @param paySerialNo    支付证书序列号
     * @param mchId          商户号
     * @param privateKeyPath 私钥路径
     * @param servletPath    相对路径  请求地址上如果有参数 则此处需要带上参数
     * @return HttpGet
     */
    private static HttpGet createHttpGet(String requestUrl,
                                         String paySerialNo,
                                         String mchId,
                                         String privateKeyPath, String servletPath) {
        //商户私钥证书
        HttpGet httpGet = new HttpGet(requestUrl);
        // NOTE: 建议指定charset=utf-8。低于4.4.6版本的HttpCore,不能正确的设置字符集,可能导致签名错误
        httpGet.addHeader("Content-Type", "application/json");
        httpGet.addHeader("Accept", "application/json");
        httpGet.addHeader("Wechatpay-Serial", paySerialNo);
        //-------------------------核心认证 start-----------------------------------------------------------------
        String strToken = null;
        try {
            strToken = WxPayV3Util.getToken("GET",
                    servletPath,
                    "", mchId, paySerialNo, privateKeyPath);
        } catch (Exception e) {
            log.error("createHttpGet error:", e);
            e.printStackTrace();
        }
        log.info("token " + strToken);
        // 添加认证信息
        httpGet.addHeader("Authorization",
                "WECHATPAY2-SHA256-RSA2048" + " "
                        + strToken);
        //---------------------------核心认证 end---------------------------------------------------------------
        return httpGet;
    }
}

2、微信专用支付类 WxPayV3Util :

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Random;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;

@Slf4j
public class WxPayV3Util {
    /**
     * @param method       请求方法 post
     * @param canonicalUrl 请求地址
     * @param body         请求参数   GET请求传空字符
     * @param merchantId   这里用的商户号
     * @param certSerialNo 商户证书序列号
     * @param keyPath      私钥商户证书地址
     * @return String
     * @throws Exception 异常信息
     */
    public static String getToken(
            String method,
            String canonicalUrl,
            String body,
            String merchantId,
            String certSerialNo,
            String keyPath) throws Exception {
        String signStr = "";
        //获取32位随机字符串
        String nonceStr = getRandomString(32);
        //当前系统运行时间
        long timestamp = System.currentTimeMillis() / 1000;
        String message = buildMessage(method, canonicalUrl, timestamp, nonceStr, body);
        //签名操作
        String signature = sign(message.getBytes(StandardCharsets.UTF_8), keyPath);
        //组装参数
        signStr = "mchid=\"" + merchantId + "\"," +
                "timestamp=\"" + timestamp + "\"," +
                "nonce_str=\"" + nonceStr + "\"," +
                "serial_no=\"" + certSerialNo + "\"," +
                "signature=\"" + signature + "\"";

        return signStr;
    }

    public static String buildMessage(String method, String canonicalUrl, long timestamp, String nonceStr, String body) {
        return method + "\n" + canonicalUrl + "\n" + timestamp + "\n" + nonceStr + "\n" + body + "\n";
    }

    public static String sign(byte[] message, String keyPath) throws Exception {
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(getPrivateKey(keyPath));
        sign.update(message);
        return Base64.encodeBase64String(sign.sign());
    }

    /**
     * 微信支付-前端唤起支付参数-获取商户私钥
     *
     * @param filename 私钥文件路径  (required)
     * @return 私钥对象
     */
    public static PrivateKey getPrivateKey(String filename) throws IOException {
        String content = new String(Files.readAllBytes(Paths.get(filename)), StandardCharsets.UTF_8);
        try {
            String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
                    .replace("-----END PRIVATE KEY-----", "")
                    .replaceAll("\\s+", "");
            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePrivate(
                    new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey)));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }

    /**
     * 获取随机位数的字符串
     *
     * @param length 需要的长度
     * @return String
     */
    public static String getRandomString(int length) {
        String base = "abcdefghijklmnopqrstuvwxyz0123456789";
        Random random = new Random();
        StringBuffer stringBuffer = new StringBuffer();
        for (int i = 0; i < length; i++) {
            int number = random.nextInt(base.length());
            stringBuffer.append(base.charAt(number));
        }
        return stringBuffer.toString();
    }
}

3、接口类 ITransferService :

public interface ITransferService {
    /**
     * 微信商家转账到零钱
     *
     * @param dto 请求DTO
     * @return String
     */
    String wxTransfer(TransferDTO dto);

    /**
     * 通过商家明细单号查询明细单
     *
     * @param dto 请求DTO
     * @return 返回VO
     */
    TransferQueryVO wxTransferQuery(TransferQueryDTO dto);
}

4、获取配置类 WxTransferConfig :

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Configuration
@Data
public class WxTransferConfig {
    /**
     * 商户appId
     */
    @Value("${wxTransfer.appId}")
    private String appId;

    /**
     * 微信请求基础URL
     */
    @Value("${wxTransfer.baseUrl}")
    private String baseUrl;
    /**
     * 发起商家转账URL
     */
    @Value("${wxTransfer.transferUrl}")
    private String transferUrl;

    /**
     * 商家转账查询URL
     */
    @Value("${wxTransfer.transferInfoUrl}")
    private String transferInfoUrl;

    /**
     * 商家转账查询URL
     */
    @Value("${wxTransfer.transferQueryUrl}")
    private String transferQueryUrl;

    /**
     * 商户证书序列号
     */
    @Value("${wxTransfer.paySerialNo}")
    private String paySerialNo;

    /**
     * 商户号
     */
    @Value("${wxTransfer.mchId}")
    private String mchId;

    /**
     * 商户私钥证书
     */
    @Value("${wxTransfer.privateKeyPath}")
    private String privateKeyPath;
}

 5、参数配置 bootstrap.xml :

wxTransfer:
  appId: wx1234567891011121
  baseUrl: https://api.mch.weixin.qq.com
  transferUrl: /v3/transfer/batches
  transferInfoUrl: /v3/transfer/batches/out-batch-no
  transferQueryUrl: /v3/transfer/batches/out-batch-no/{0}/details/out-detail-no/{1}
  paySerialNo: 1234567891011121314151617181920212223242
  mchId: 1234567899
  privateKeyPath: src/main/resources/apiclient_key.pem

6、转账枚举类 WxTransferEnum :

public enum WxTransferEnum {
    /**
     * 明细状态 INIT-初始化 WAIT_PAY-待确认 PROCESSING-转账中 SUCCESS-转账成功 FAIL-转账失败
     */
    INIT("PROCESSING", "转账中"),
    WAIT_PAY("PROCESSING", "转账中"),
    PROCESSING("PROCESSING", "转账中"),
    SUCCESS("SUCCESS", "转账成功"),
    FAIL("FAIL", "转账失败"),
    ;

    private String code;
    private String message;

    WxTransferEnum(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public static String getMessageByName(String detailName) {
        for (WxTransferEnum value : WxTransferEnum.values()) {
            if (value.name().equals(detailName)) {
                return value.getMessage();
            }
        }
        return null;
    }

    public static String getCodeByName(String detailName) {
        for (WxTransferEnum value : WxTransferEnum.values()) {
            if (value.name().equals(detailName)) {
                return value.getCode();
            }
        }
        return null;
    }



    public String getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

7、转账失败枚举类 WxTransferFailEnum :

import com.wechat.pay.java.service.transferbatch.model.FailReasonType;

public enum WxTransferFailEnum {
    /**
     * 明细失败原因
     */
    ACCOUNT_FROZEN("该用户账户被冻结"),
    REAL_NAME_CHECK_FAIL("收款人未实名认证,需要用户完成微信实名认证"),
    NAME_NOT_CORRECT("收款人姓名校验不通过,请核实信息"),
    OPENID_INVALID("Openid格式错误或者不属于商家公众账号"),
    TRANSFER_QUOTA_EXCEED("超过用户单笔收款额度,核实产品设置是否准确"),
    DAY_RECEIVED_QUOTA_EXCEED("超过用户单日收款额度,核实产品设置是否准确"),
    MONTH_RECEIVED_QUOTA_EXCEED("超过用户单月收款额度,核实产品设置是否准确"),
    DAY_RECEIVED_COUNT_EXCEED("超过用户单日收款次数,核实产品设置是否准确"),
    PRODUCT_AUTH_CHECK_FAIL("未开通该权限或权限被冻结,请核实产品权限状态"),
    OVERDUE_CLOSE("超过系统重试期,系统自动关闭"),
    ID_CARD_NOT_CORRECT("收款人身份证校验不通过,请核实信息"),
    ACCOUNT_NOT_EXIST("该用户账户不存在"),
    TRANSFER_RISK("该笔转账可能存在风险,已被微信拦截"),
    OTHER_FAIL_REASON_TYPE("其它失败原因"),
    REALNAME_ACCOUNT_RECEIVED_QUOTA_EXCEED("用户账户收款受限,请引导用户在微信支付查看详情"),
    RECEIVE_ACCOUNT_NOT_PERMMIT("未配置该用户为转账收款人,请在产品设置中调整,添加该用户为收款人"),
    PAYEE_ACCOUNT_ABNORMAL("用户账户收款异常,请联系用户完善其在微信支付的身份信息以继续收款"),
    PAYER_ACCOUNT_ABNORMAL("商户账户付款受限,可前往商户平台获取解除功能限制指引"),
    TRANSFER_SCENE_UNAVAILABLE("该转账场景暂不可用,请确认转账场景ID是否正确"),
    TRANSFER_SCENE_INVALID("你尚未获取该转账场景,请确认转账场景ID是否正确"),
    TRANSFER_REMARK_SET_FAIL("转账备注设置失败, 请调整后重新再试"),
    RECEIVE_ACCOUNT_NOT_CONFIGURE("请前往商户平台-商家转账到零钱-前往功能-转账场景中添加"),
    BLOCK_B2C_USERLIMITAMOUNT_BSRULE_MONTH("超出用户单月转账收款20w限额,本月不支持继续向该用户付款"),
    BLOCK_B2C_USERLIMITAMOUNT_MONTH("用户账户存在风险收款受限,本月不支持继续向该用户付款"),
    MERCHANT_REJECT("商户员工(转账验密人)已驳回转账"),
    MERCHANT_NOT_CONFIRM("商户员工(转账验密人)超时未验密"),
    SYSTEM_ERROR("打款失败"),
    ;

    private String reason;

    public String getReason() {
        return reason;
    }

    WxTransferFailEnum(String reason) {
        this.reason = reason;
    }

    public static String getReasonByName(String reasonName) {
        for (WxTransferFailEnum value : WxTransferFailEnum.values()) {
            if (value.name().equals(reasonName)) {
                return value.getReason();
            }
        }
        return null;
    }

    /**
     * 引入 sdk 版
     * @param failReasonType 失败枚举类
     * @return String
     */
    public static String getReasonByReason(FailReasonType failReasonType) {
        for (WxTransferFailEnum value : WxTransferFailEnum.values()) {
            if (value.name().equals(failReasonType.name())) {
                return value.getReason();
            }
        }
        return null;
    }
}

8、转账请求参数类 TransferDTO :

import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@NoArgsConstructor
public class TransferDTO implements Serializable {

    private static final long serialVersionUID = 9152333275186294043L;

    public TransferDTO(String openId, Long amount) {
        this.openId = openId;
        this.amount = amount;
    }

    /**
     * 商户系统内部的商家批次单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
     * 可不传,默认格式:2306211424589664124
     */
    private String outBatchNo;
    /**
     * 该笔批量转账的名称
     * 可不传,默认:商家转账到零钱
     */
    private String batchName;
    /**
     * 转账说明,UTF8编码,最多允许32个字符
     * 可不传,默认:商家转账到零钱
     */
    private String batchRemark;
    /**
     * 商户appId下,某用户的openId
     * 必传
     */
    private String openId;
    /**
     * 转账金额单位为“分”
     * 必传
     */
    private Long amount;
}

9、转账查询请求参数类 TransferQueryDTO :

import lombok.Data;

import java.io.Serializable;

@Data
public class TransferQueryDTO implements Serializable {
    private static final long serialVersionUID = -9039158496135815154L;
    /**
     * 商户系统内部的商家批次单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
      * 必传
     */
    private String outBatchNo;
}

9、转账查询返回类 TransferQueryVO :

import lombok.Data;

import java.io.Serializable;

@Data
public class TransferQueryVO implements Serializable {
    private static final long serialVersionUID = -1724534950699781783L;
    /**
     * 业务结果:成功-SUCCESS 失败-FAIL 转账中-PROCESSING
     */
    private String resCode;
    /**
     * 业务结果描述
     */
    private String resCodeDes;
}

10、实现类 TransferServiceImpl  :

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import WxTransferConfig;
import WxHttpUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;

@Service
@Slf4j
public class TransferServiceImpl implements ITransferService {
    @Resource
    private WxTransferConfig wxTransferConfig;
    private static final String BATCH_REMARK = "商家转账到零钱";

    @Override
    public String wxTransfer(TransferDTO dto) {
        SortedMap<String, Object> parameters = transferParam(dto);
        // 微信请求基础URL
        String baseUrl = wxTransferConfig.getBaseUrl();
        // 发起商家转账请求URL
        String transferUrl = wxTransferConfig.getTransferUrl();
        // 商户证书序列号
        String paySerialNo = wxTransferConfig.getPaySerialNo();
        // 商户号
        String mchId = wxTransferConfig.getMchId();
        // 商户私钥证书
        String privateKeyPath = wxTransferConfig.getPrivateKeyPath();
        String result = WxHttpUtil.postTransBatRequest(
                String.format("%s%s", baseUrl, transferUrl),
                JSONObject.toJSONString(parameters),
                paySerialNo,
                mchId,
                privateKeyPath, transferUrl);
        JSONObject jsonObject = JSONObject.parseObject(result);
        log.info("转账返回信息:{}", JSON.toJSONString(jsonObject));
        if (null == jsonObject) {
            throw new RuntimeException("打款失败");
        }
        if (jsonObject.containsKey("code")) {
            log.info("转账失败信息:[{},{}]", jsonObject.getString("code"), jsonObject.getString("message"));
            throw new RuntimeException(jsonObject.getString("message"));
        } else {
            if (!jsonObject.containsKey("out_batch_no")) {
                throw new RuntimeException("打款失败");
            }
        }
        log.info("转账受理成功-商家转账批次单号:{}", jsonObject.getString("out_batch_no"));
        return jsonObject.getString("out_batch_no");
    }

    @Override
    public TransferQueryVO wxTransferQuery(TransferQueryDTO dto) {
        TransferQueryVO transferQueryVO = new TransferQueryVO();
        //商家转账批次单号
        String outBatchNo = dto.getOutBatchNo();
        //商家转账明细单号
        String outDetailNo = dto.getOutBatchNo();
        // 微信请求基础URL
        String baseUrl = wxTransferConfig.getBaseUrl();
        // 商家转账查询请求URL
        String transferQueryUrl = wxTransferConfig.getTransferQueryUrl();
        // 商户证书序列号
        String paySerialNo = wxTransferConfig.getPaySerialNo();
        // 商户号
        String mchId = wxTransferConfig.getMchId();
        // 商户私钥证书
        String privateKeyPath = wxTransferConfig.getPrivateKeyPath();
        // 返回信息接收
        String params = "{" +
                "\"out_batch_no\":\"" + outBatchNo +
                "\",\"out_detail_no\":\"" + outDetailNo
                + "\"}";
        log.info("转账查询请求参数:{}", JSON.toJSONString(params));
        String queryUrl = new MessageFormat(transferQueryUrl).format(new Object[]{outBatchNo, outDetailNo});
        String result = WxHttpUtil.getTransBatRequest(
                String.format("%s%s", baseUrl, queryUrl),
                paySerialNo,
                mchId,
                privateKeyPath, queryUrl);
        JSONObject jsonObject = JSONObject.parseObject(result);
        log.info("转账查询返回信息:{}", JSON.toJSONString(jsonObject));
        if (null != jsonObject) {
            if (jsonObject.containsKey("code")) {
                log.info("转账失败信息:[{},{}]", jsonObject.getString("code"), jsonObject.getString("message"));
                transferQueryVO.setResCode(jsonObject.getString("code"));
                transferQueryVO.setResCodeDes(jsonObject.getString("message"));
            } else {
                transferQueryVO.setResCode(WxTransferEnum.getCodeByName(jsonObject.getString("detail_status")));
                transferQueryVO.setResCodeDes(WxTransferEnum.getMessageByName(jsonObject.getString("detail_status")));
                if (WxTransferEnum.FAIL.getCode().equals(jsonObject.getString("detail_status"))) {
                    transferQueryVO.setResCodeDes(WxTransferFailEnum.getReasonByName(jsonObject.getString("fail_reason")));
                }
            }
        } else {
            transferQueryVO.setResCode(WxTransferEnum.FAIL.getCode());
            transferQueryVO.setResCodeDes("打款失败");
        }
        log.info("转账查询返回信息II:{}", transferQueryVO);
        return transferQueryVO;
    }

    /**
     * 封装请求参数
     *
     * @param dto DTO请求
     * @return SortedMap
     */
    private SortedMap<String, Object> transferParam(TransferDTO dto) {
        // 商户appId
        String appId = wxTransferConfig.getAppId();
        String outBatchNo = "".equals(dto.getOutBatchNo()) || null == dto.getOutBatchNo() ? generateOutNo() : dto.getOutBatchNo();
        String batchName = "".equals(dto.getBatchName()) || null == dto.getBatchName() ? BATCH_REMARK : dto.getBatchName();
        String batchRemark = "".equals(dto.getBatchRemark()) || null == dto.getBatchRemark() ? BATCH_REMARK : dto.getBatchRemark();
        Long totalAmount = dto.getAmount();
        Integer totalNum = 1;
        String openId = dto.getOpenId();

        SortedMap<String, Object> parameters = new TreeMap<>();
        // 【商户appid】 申请商户号的appid或商户号绑定的appid
        parameters.put("appid", appId);
        // 【商家批次单号】 商户系统内部的商家批次单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
        parameters.put("out_batch_no", outBatchNo);
        // 【批次名称】 该笔批量转账的名称
        parameters.put("batch_name", batchName);
        // 【批次备注】 转账说明,UTF8编码,最多允许32个字符
        parameters.put("batch_remark", batchRemark);
        // 【转账总金额】 转账金额单位为“分”。账总金额必须与批次内所有明细转账金额之和保持一致,否则无法发起转账操作
        parameters.put("total_amount", totalAmount);
        // 【转账总笔数】 一个转账批次单最多发起一千笔转账。转账总笔数必须与批次内所有明细之和保持一致,否则无法发起转账操作
        parameters.put("total_num", totalNum);
        List<Map> transferDetailList = new ArrayList<>();
        Map<String, Object> subMap = new HashMap<>(4);
        //商家明细单号 该商家下唯一
        subMap.put("out_detail_no", outBatchNo);
        //转账金额
        subMap.put("transfer_amount", totalAmount);
        //转账备注
        subMap.put("transfer_remark", batchRemark);
        //用户在直连商户应用下的用户标示
        subMap.put("openid", openId);
        transferDetailList.add(subMap);
        // 【转账明细列表】 发起批量转账的明细列表,最多一千笔
        parameters.put("transfer_detail_list", transferDetailList);
        log.info("转账请求参数:[{}]", JSON.toJSONString(parameters));
        return parameters;
    }

    private String generateOutNo() {
        String time = cn.hutool.core.date.DateUtil.format(new Date(), "yyyyMMddHHmmssSSS").substring(2);
        int randomInt = ThreadLocalRandom.current().nextInt(1000, 10000);
        return time + randomInt;
    }
}

 11、文件过滤 pom.xml :

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <configuration>
                    <nonFilteredFileExtensions>
                        <nonFilteredFileExtension>pem</nonFilteredFileExtension>
                    </nonFilteredFileExtensions>
                </configuration>
            </plugin>
        </plugins>
    </build>

12、文件拷贝 apiclient_key.pem :

将 apiclient_key.pem 拷贝至 resources 目录下。

13、测试类 TransferTest :

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;
import java.util.concurrent.ThreadLocalRandom;

@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class TransferTest {
    @Resource
    TransferServiceImpl transferService;

    @Test
    //@Ignore
    public void wxTransfer() {
        TransferDTO dto = new TransferDTO();
        Long amount = 10L;
        String openId = "1234567";
        dto.setOpenId(openId);
        //金额单位:分
        dto.setAmount(amount);
        transferService.wxTransfer(dto);
    }

    @Test
    public void wxTransferQuery() {
        TransferQueryDTO dto = new TransferQueryDTO();
        dto.setOutBatchNo("12345678910111213");
        transferService.wxTransferQuery(dto);
    }
}
  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值