feat: 微信支付V3批量转账到零钱

- 增加批量转账到零钱API调用入口WechatBatchTransferApi
- 实现发起批量转账
- 实现微信批次单号查询批次单API
- 实现微信明细单号查询明细单API
- 实现商家批次单号查询批次单API
- 实现商家明细单号查询明细单API
- 实现转账电子回单申请受理API
- 实现查询转账电子回单API,同时实现直接下载功能
This commit is contained in:
felord.cn
2021-02-04 18:15:42 +08:00
committed by felord.cn
parent cc968d5be2
commit d177bee2d3
9 changed files with 589 additions and 9 deletions

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.enumeration;
/**
* 转账明细.
*
* @author xfa00
* @since 1.0.6.RELEASE
*/
public enum DetailStatus {
/**
* 全部。需要同时查询转账成功和转账失败的明细单
*/
ALL,
/**
* 转账成功。只查询转账成功的明细单
*/
SUCCESS,
/**
* 转账失败。只查询转账失败的明细单
*/
FAIL
}

View File

@@ -28,9 +28,7 @@ import org.springframework.http.HttpMethod;
* @since 1.0.0.RELEASE
*/
public enum WechatPayV3Type {
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/**
* 获取证书.
*
@@ -445,8 +443,50 @@ public enum WechatPayV3Type {
*
* @since 1.0.4.RELEASES
*/
MARKETING_BUSI_FAVOR_DEACTIVATE(HttpMethod.POST, "%s/v3/marketing/busifavor/coupons/deactivate");
MARKETING_BUSI_FAVOR_DEACTIVATE(HttpMethod.POST, "%s/v3/marketing/busifavor/coupons/deactivate"),
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/**
* 发起批量转账API.
*
* @since 1.0.6.RELEASES
*/
BATCH_TRANSFER_REQ(HttpMethod.POST, "%s/v3/transfer/batches"),
/**
* 微信批次单号查询批次单API.
*
* @since 1.0.6.RELEASES
*/
BATCH_TRANSFER_BATCH_ID(HttpMethod.GET, "%s/v3/transfer/batches/batch-id/{batch_id}"),
/**
* 微信明细单号查询明细单API.
*
* @since 1.0.6.RELEASES
*/
BATCH_TRANSFER_DETAIL_WECHAT(HttpMethod.GET, "%s/v3/transfer/batches/batch-id/{batch_id}/details/detail-id/{detail_id}"),
/**
* 商家批次单号查询批次单API.
*
* @since 1.0.6.RELEASES
*/
BATCH_TRANSFER_OUT_BATCH_NO(HttpMethod.GET, "%s/v3/transfer/batches/out-batch-no/{out_batch_no}"),
/**
* 商家明细单号查询明细单API.
*
* @since 1.0.6.RELEASES
*/
BATCH_TRANSFER_DETAIL_MCH(HttpMethod.GET, "%s/v3/transfer/batches/out-batch-no/{out_batch_no}/details/out-detail-no/{out_detail_no}"),
/**
* 转账电子回单申请受理API.
*
* @since 1.0.6.RELEASES
*/
BATCH_TRANSFER_BILL_RECEIPT(HttpMethod.POST, "%s/v3/transfer/bill-receipt"),
/**
* 查询转账电子回单并下载.
*
* @since 1.0.6.RELEASES
*/
BATCH_TRANSFER_DOWNLOAD_BILL(HttpMethod.GET, "%s/v3/transfer/bill-receipt/{out_batch_no}");
/**
* The Pattern.
*
@@ -492,7 +532,6 @@ public enum WechatPayV3Type {
return this.pattern;
}
/**
* 默认支付URI.
*
@@ -503,5 +542,4 @@ public enum WechatPayV3Type {
public String uri(WeChatServer weChatServer) {
return String.format(this.pattern, weChatServer.domain());
}
}

View File

@@ -28,7 +28,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.core.io.Resource;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -166,10 +168,10 @@ public abstract class AbstractApi {
/**
* 对账单下载
* 对账单内容下载,非流文件
*
* @param link the link
* @return 对账单内容,有可能为空字符 “”
* @return 对账单内容 ,有可能为空字符 “”
*/
protected String billDownload(String link) {
return this.client().withType(WechatPayV3Type.FILE_DOWNLOAD, link)
@@ -182,6 +184,22 @@ public abstract class AbstractApi {
.download();
}
/**
* 对账单下载,流文件。
*
* @param link the link
* @return response entity
*/
protected ResponseEntity<Resource> billResource(String link) {
return this.client().withType(WechatPayV3Type.FILE_DOWNLOAD, link)
.function((type, downloadUrl) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(downloadUrl)
.build()
.toUri();
return Get(uri);
})
.resource();
}
/**
* 申请交易账单API

View File

@@ -110,6 +110,19 @@ public class WechatApiProvider {
return new WechatMarketingBusiFavorApi(wechatPayClient, tenantId);
}
/**
* 批量转账到零钱.
* <p>
* 批量转账到零钱提供商户同时向多个用户微信零钱转账的能力。商户可以使用批量转账到零钱用于费用报销、员工福利发放、合作伙伴货款或服务款项支付等场景,提高转账效率。
*
* @param tenantId the tenant id
* @return the WechatBatchTransferApi
* @since 1.0.6.RELEASE
*/
public WechatBatchTransferApi batchTransferApi(String tenantId) {
return new WechatBatchTransferApi(wechatPayClient, tenantId);
}
/**
* 回调.
* <p>

View File

@@ -0,0 +1,255 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v3;
import cn.felord.payment.wechat.enumeration.WeChatServer;
import cn.felord.payment.wechat.enumeration.WechatPayV3Type;
import cn.felord.payment.wechat.v3.model.batchtransfer.CreateBatchTransferParams;
import cn.felord.payment.wechat.v3.model.batchtransfer.QueryBatchTransferDetailParams;
import cn.felord.payment.wechat.v3.model.batchtransfer.QueryBatchTransferParams;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.core.io.Resource;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import sun.security.x509.X509CertImpl;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 批量转账到零钱
*
* @author felord.cn
* @since 1.0.6.RELEASE
*/
public class WechatBatchTransferApi extends AbstractApi {
/**
* Instantiates a new Abstract api.
*
* @param wechatPayClient the wechat pay client
* @param tenantId the tenant id
*/
public WechatBatchTransferApi(WechatPayClient wechatPayClient, String tenantId) {
super(wechatPayClient, tenantId);
}
/**
* 发起批量转账API
*
* @param createBatchTransferParams the batchTransferParams
* @return the wechat response entity
* @since 1.0.6.RELEASE
*/
public WechatResponseEntity<ObjectNode> batchTransfer(CreateBatchTransferParams createBatchTransferParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.BATCH_TRANSFER_REQ, createBatchTransferParams)
.function(this::batchTransferFunction)
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
private RequestEntity<?> batchTransferFunction(WechatPayV3Type type, CreateBatchTransferParams createBatchTransferParams) {
List<CreateBatchTransferParams.TransferDetailListItem> transferDetailList = createBatchTransferParams.getTransferDetailList();
SignatureProvider signatureProvider = this.client().signatureProvider();
final X509CertImpl certificate = signatureProvider.getCertificate();
List<CreateBatchTransferParams.TransferDetailListItem> encrypted = transferDetailList.stream()
.peek(transferDetailListItem -> {
String userName = transferDetailListItem.getUserName();
String encryptedUserName = signatureProvider.encryptRequestMessage(userName, certificate);
transferDetailListItem.setUserName(encryptedUserName);
String userIdCard = transferDetailListItem.getUserIdCard();
if (StringUtils.hasText(userIdCard)) {
String encryptedUserIdCard = signatureProvider.encryptRequestMessage(userIdCard, certificate);
transferDetailListItem.setUserIdCard(encryptedUserIdCard);
}
}).collect(Collectors.toList());
createBatchTransferParams.setTransferDetailList(encrypted);
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.build()
.toUri();
return Post(uri, createBatchTransferParams);
}
/**
* 微信批次单号查询批次单API
*
* @param queryBatchTransferParams the queryBatchTransferParams
* @return the wechat response entity
* @since 1.0.6.RELEASE
*/
public WechatResponseEntity<ObjectNode> queryBatchByBatchId(QueryBatchTransferParams queryBatchTransferParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.BATCH_TRANSFER_BATCH_ID, queryBatchTransferParams)
.function((type, params) -> {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("need_query_detail", params.getNeedQueryDetail().toString());
queryParams.add("offset", params.getOffset().toString());
queryParams.add("limit", params.getLimit().toString());
queryParams.add("detail_status", params.getDetailStatus().name());
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.queryParams(queryParams)
.build()
.expand(params.getCode())
.toUri();
return Get(uri);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 微信明细单号查询明细单API
*
* @param queryBatchTransferDetailParams the queryBatchTransferDetailParams
* @return the wechat response entity
* @since 1.0.6.RELEASE
*/
public WechatResponseEntity<ObjectNode> queryBatchDetailByWechat(QueryBatchTransferDetailParams queryBatchTransferDetailParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.BATCH_TRANSFER_DETAIL_WECHAT, queryBatchTransferDetailParams)
.function((type, params) -> {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("batch_id", params.getBatchIdOrOutBatchNo());
queryParams.add("detail_id", params.getDetailIdOrOutDetailNo());
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.build()
.expand(queryParams)
.toUri();
return Get(uri);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 微信批次单号查询批次单API
*
* @param queryBatchTransferParams the queryBatchTransferParams
* @return the wechat response entity
* @since 1.0.6.RELEASE
*/
public WechatResponseEntity<ObjectNode> queryBatchByOutBatchNo(QueryBatchTransferParams queryBatchTransferParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.BATCH_TRANSFER_OUT_BATCH_NO, queryBatchTransferParams)
.function((type, params) -> {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("need_query_detail", params.getNeedQueryDetail().toString());
queryParams.add("offset", params.getOffset().toString());
queryParams.add("limit", params.getLimit().toString());
queryParams.add("detail_status", params.getDetailStatus().name());
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.queryParams(queryParams)
.build()
.expand(params.getCode())
.toUri();
return Get(uri);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 商家明细单号查询明细单API
*
* @param queryBatchTransferDetailParams the queryBatchTransferDetailParams
* @return the wechat response entity
* @since 1.0.6.RELEASE
*/
public WechatResponseEntity<ObjectNode> queryBatchDetailByMch(QueryBatchTransferDetailParams queryBatchTransferDetailParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.BATCH_TRANSFER_DETAIL_MCH, queryBatchTransferDetailParams)
.function((type, params) -> {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("out_batch_no", params.getBatchIdOrOutBatchNo());
queryParams.add("out_detail_no", params.getDetailIdOrOutDetailNo());
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.build()
.expand(queryParams)
.toUri();
return Get(uri);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 转账电子回单申请受理API
*
* @param outBatchNo the outBatchNo
* @return the wechat response entity
* @since 1.0.6.RELEASE
*/
public WechatResponseEntity<ObjectNode> receiptBill(String outBatchNo) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.BATCH_TRANSFER_BILL_RECEIPT, outBatchNo)
.function((type, batchNo) -> {
Map<String, String> body = new HashMap<>(1);
body.put("out_batch_no", outBatchNo);
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.build()
.toUri();
return Post(uri, body);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 查询并下载转账电子回单API
*
* @param outBatchNo the outBatchNo
* @return the wechat response entity
* @since 1.0.6.RELEASE
*/
public ResponseEntity<Resource> downloadBill(String outBatchNo) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.BATCH_TRANSFER_DOWNLOAD_BILL, outBatchNo)
.function((type, batchNo) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.build()
.expand(batchNo)
.toUri();
return Get(uri);
})
.consumer(wechatResponseEntity::convert)
.request();
String downloadUrl = wechatResponseEntity.getBody().get("download_url").asText();
Assert.hasText(downloadUrl,"download url has no text");
return this.billResource(downloadUrl);
}
}

View File

@@ -24,6 +24,7 @@ import cn.felord.payment.wechat.WechatPayResponseErrorHandler;
import cn.felord.payment.wechat.enumeration.WechatPayV3Type;
import cn.felord.payment.wechat.v3.model.ResponseSignVerifyParams;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.core.io.Resource;
import org.springframework.http.*;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
@@ -176,6 +177,17 @@ public class WechatPayClient {
WechatRequestEntity<?> wechatRequestEntity = WechatRequestEntity.of(requestEntity, this.responseBodyConsumer);
return this.doDownload(this.header(wechatRequestEntity));
}
/**
* Download string.
*
* @return the string
* @since 1.0.6.RELEASE
*/
public ResponseEntity<Resource> resource() {
RequestEntity<?> requestEntity = this.requestEntityBiFunction.apply(this.wechatPayV3Type, this.model);
WechatRequestEntity<?> wechatRequestEntity = WechatRequestEntity.of(requestEntity, this.responseBodyConsumer);
return this.doResource(this.header(wechatRequestEntity));
}
/**
@@ -267,7 +279,7 @@ public class WechatPayClient {
}
/**
* Do download string.
* 下载文件返回的是字符串类型的.
*
* @param <T> the type parameter
* @param requestEntity the request entity
@@ -286,6 +298,28 @@ public class WechatPayClient {
return Optional.ofNullable(responseEntity.getBody()).orElse("");
}
/**
* 下载文件返回的是流类型的.
*
* @param <T> the type parameter
* @param requestEntity the request entity
* @return the resource
* @since 1.0.6.RELEASE
*/
private <T> ResponseEntity<Resource> doResource(WechatRequestEntity<T> requestEntity) {
ResponseEntity<Resource> responseEntity = restOperations.exchange(requestEntity, Resource.class);
HttpStatus statusCode = responseEntity.getStatusCode();
// 微信请求id
String requestId = requestEntity.getHeaders().getFirst("Request-ID");
if (!statusCode.is2xxSuccessful()) {
throw new PayException("wechat pay server error, Request-ID " + requestId + " , statusCode " + statusCode + ",result : " + responseEntity);
}
return responseEntity;
}
}
/**

View File

@@ -0,0 +1,97 @@
/*
*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package cn.felord.payment.wechat.v3.model.batchtransfer;
import lombok.Data;
import java.util.List;
/**
* 批量转账到零钱API请求参数.
*
* @author felord.cn
* @since 1.0.6.RELEASE
*/
@Data
public class CreateBatchTransferParams {
/**
* 直连商户的appid
*/
private String appid;
/**
* 商家批次单号
*/
private String outBatchNo;
/**
* 批次名称
*/
private String batchName;
/**
* 批次备注
*/
private String batchRemark;
/**
* 发起批量转账的明细列表,最多三千笔
*/
private List<TransferDetailListItem> transferDetailList;
/**
* 转账总金额,单位为“分”。转账总金额必须与批次内所有明细转账金额之和保持一致,否则无法发起转账操作
*/
private Integer totalAmount;
/**
* 转账总笔数,一个转账批次单最多发起三千笔转账。转账总笔数必须与批次内所有明细之和保持一致,否则无法发起转账操作
*/
private Integer totalNum;
/**
* 转账明细.
*
* @author felord.cn
* @since 1.0.6.RELEASE
*/
@Data
public static class TransferDetailListItem{
/**
* 商家明细单号
*/
private String outDetailNo;
/**
* 转账金额,单位为分
*/
private Integer transferAmount;
/**
* 单条转账备注微信用户会收到该备注UTF8编码最多允许32个字符
*/
private String transferRemark;
/**
* 用户在直连商户appid下的唯一标识
*/
private String openid;
/**
* 收款用户姓名
*/
private String userName;
/**
* 收款用户身份证
*/
private String userIdCard;
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v3.model.batchtransfer;
import lombok.Data;
/**
* @author felord.cn
* @since 1.0.6.RELEASE
*/
@Data
public class QueryBatchTransferDetailParams {
/**
* 微信批次单号 或 商家批次单号
*/
private String batchIdOrOutBatchNo;
/**
* 微信明细单号 或 商家明细单号
*/
private String detailIdOrOutDetailNo;
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v3.model.batchtransfer;
import cn.felord.payment.wechat.enumeration.DetailStatus;
import lombok.Data;
/**
* 微信批次单号查询批次单API 或者 商家批次单号查询批次单API 请求参数
*
* @author felord.cn
* @since 1.0.6.RELEASE
*/
@Data
public class QueryBatchTransferParams {
/**
* 微信批次单号 或 商家批次单号
*/
private String code;
/**
* 是否查询转账明细单
*/
private Boolean needQueryDetail;
/**
* 请求资源起始位置
*/
private Integer offset;
/**
* 最大资源条数
*/
private Integer limit;
/**
* 明细状态
*/
private DetailStatus detailStatus;
}