feat: 基于V2实现现金红包以及企业付款到零钱功能

- 实现现金红包接口
- 实现企业付款到零钱功能
- 增加V2需要的依赖
This commit is contained in:
felord.cn
2021-01-20 10:21:41 +08:00
committed by felord.cn
parent 82b7293e32
commit 0bab0b987d
13 changed files with 632 additions and 0 deletions

View File

@@ -56,6 +56,16 @@
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,90 @@
/*
* 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.v2;
import cn.felord.payment.wechat.WechatPayProperties;
import cn.felord.payment.wechat.v2.model.GroupRedpackModel;
import cn.felord.payment.wechat.v2.model.RedpackInfoModel;
import cn.felord.payment.wechat.v2.model.RedpackModel;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.http.HttpMethod;
/**
* 现金红包
*
* @author felord.cn
* @since 1.0.5.RELEASE
*/
public class WechatPayRedpackApi {
private final WechatV2Client wechatV2Client;
/**
* Instantiates a new Wechat pay redpack api.
*
* @param wechatV2Client the wechat v 2 client
*/
public WechatPayRedpackApi(WechatV2Client wechatV2Client) {
this.wechatV2Client = wechatV2Client;
}
/**
* 发放随机红包
*
* @param redpackModel the redpack model
* @return the json node
*/
public JsonNode sendRedpack(RedpackModel redpackModel) {
WechatPayProperties.V3 v3 = wechatV2Client.getWechatMetaBean().getV3();
redpackModel.setWxappid(v3.getAppId());
redpackModel.setMchId(v3.getMchId());
return wechatV2Client.wechatPayRequest(redpackModel,
HttpMethod.POST,
"https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack");
}
/**
* 发放裂变红包
*
* @param groupRedpackModel the group redpack model
* @return the json node
*/
public JsonNode sendRedpack(GroupRedpackModel groupRedpackModel) {
WechatPayProperties.V3 v3 = wechatV2Client.getWechatMetaBean().getV3();
groupRedpackModel.setWxappid(v3.getAppId());
groupRedpackModel.setMchId(v3.getMchId());
return wechatV2Client.wechatPayRequest(groupRedpackModel,
HttpMethod.POST,
"https://api.mch.weixin.qq.com/mmpaymkttransfers/sendgroupredpack");
}
/**
* 查询红包信息
*
* @param redpackInfoModel the redpack info model
* @return the json node
*/
public JsonNode redpackInfo(RedpackInfoModel redpackInfoModel) {
WechatPayProperties.V3 v3 = wechatV2Client.getWechatMetaBean().getV3();
redpackInfoModel.setAppid(v3.getAppId());
redpackInfoModel.setMchId(v3.getMchId());
return wechatV2Client.wechatPayRequest(redpackInfoModel,
HttpMethod.POST,
"https://api.mch.weixin.qq.com/mmpaymkttransfers/gethbinfo");
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.v2;
import cn.felord.payment.wechat.WechatPayProperties;
import cn.felord.payment.wechat.v2.model.TransferInfoModel;
import cn.felord.payment.wechat.v2.model.TransferModel;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.http.HttpMethod;
/**
* 企业付款
* <p>
* TODO 暂时没有企业付款到银行卡
*
* @author felord.cn
* @since 1.0.5.RELEASE
*/
public class WechatPayTransfersApi {
private final WechatV2Client wechatV2Client;
/**
* Instantiates a new Wechat pay transfers api.
*
* @param wechatV2Client the wechat v 2 client
*/
public WechatPayTransfersApi(WechatV2Client wechatV2Client) {
this.wechatV2Client = wechatV2Client;
}
/**
* 企业付款到零钱
*
* @param transferModel the transfer model
* @return json node
*/
public JsonNode transfer(TransferModel transferModel) {
WechatPayProperties.V3 v3 = wechatV2Client.getWechatMetaBean().getV3();
transferModel.setMchAppid(v3.getAppId());
transferModel.setMchid(v3.getMchId());
return wechatV2Client.wechatPayRequest(transferModel,
HttpMethod.POST,
"https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers");
}
/**
* 查询企业付款
*
* @param transferModel the transfer model
* @return the json node
*/
public JsonNode transferInfo(TransferInfoModel transferModel) {
WechatPayProperties.V3 v3 = wechatV2Client.getWechatMetaBean().getV3();
transferModel.setAppid(v3.getAppId());
transferModel.setMchId(v3.getMchId());
return wechatV2Client.wechatPayRequest(transferModel,
HttpMethod.POST,
"https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo");
}
}

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.v2;
import cn.felord.payment.wechat.WechatPayProperties;
import cn.felord.payment.wechat.v2.model.BaseModel;
import cn.felord.payment.wechat.v3.WechatMetaBean;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.http.HttpMethod;
/**
* 微信支付V2 客户端
* <p>
* V3接口不完善的临时性解决方案
*
* @author felord.cn
* @since 1.0.5.RELEASE
*/
public class WechatV2Client {
private final WechatMetaBean wechatMetaBean;
public WechatV2Client(WechatMetaBean wechatMetaBean) {
this.wechatMetaBean = wechatMetaBean;
}
public <M extends BaseModel> JsonNode wechatPayRequest(M model, HttpMethod method, String url) {
WechatPayProperties.V3 v3 = wechatMetaBean.getV3();
return model
.appSecret(v3.getAppSecret())
.request(v3.getMchId(), method, url);
}
public WechatMetaBean getWechatMetaBean() {
return wechatMetaBean;
}
}

View File

@@ -0,0 +1,188 @@
/*
* 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.v2.model;
import cn.felord.payment.PayException;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import lombok.Getter;
import lombok.SneakyThrows;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.MD5Digest;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.util.AlternativeJdkIdGenerator;
import org.springframework.util.Assert;
import org.springframework.util.IdGenerator;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.CertificateException;
/**
* The type Base model.
*
* @author felord.cn
* @since 1.0.5.RELEASE
*/
@Getter
public class BaseModel {
private static final XmlMapper XML_MAPPER = new XmlMapper();
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
static {
// 忽略null
XML_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL)
// 属性使用 驼峰首字母小写
.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
OBJECT_MAPPER.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true)
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
}
private static final IdGenerator ID_GENERATOR = new AlternativeJdkIdGenerator();
private final String nonceStr = ID_GENERATOR.generateId()
.toString()
.replaceAll("-", "");
private String sign;
@JsonIgnore
private String appSecret;
public BaseModel appSecret(String appSecret) {
this.appSecret = appSecret;
return this;
}
/**
* Xml string.
*
* @return the string
*/
@SneakyThrows
private String xml() {
String link = link(this);
this.sign = this.md5(link);
return XML_MAPPER.writer()
.withRootName("xml")
.writeValueAsString(this);
}
/**
* md5摘要.
*
* @param src the src
* @return the string
*/
private String md5(String src) {
Digest digest = new MD5Digest();
byte[] bytes = src.getBytes(StandardCharsets.UTF_8);
digest.update(bytes, 0, bytes.length);
byte[] md5Bytes = new byte[digest.getDigestSize()];
digest.doFinal(md5Bytes, 0);
return Hex.toHexString(md5Bytes).toUpperCase();
}
/**
* 按照格式拼接参数以生成签名
*
* @param <T> the type parameter
* @param t the t
* @return the map
*/
@SneakyThrows
private <T> String link(T t) {
Assert.hasText(appSecret, "wechat pay appSecret is required");
return OBJECT_MAPPER
.writer()
.writeValueAsString(t)
.replaceAll("\":\"", "=")
.replaceAll("\",\"", "&")
.replaceAll("\\{\"", "")
.replaceAll("\"}", "")
.concat("&key=").concat(this.appSecret);
}
@SneakyThrows
public JsonNode request(String mchId, HttpMethod method, String url) {
String xml = this.xml();
RequestEntity<String> body = RequestEntity.method(method, UriComponentsBuilder.fromHttpUrl(url)
.build()
.toUri())
.header("Content-Type", "application/x-www-form-urlencoded")
.body(xml);
ResponseEntity<String> responseEntity = this.getRestTemplateClientAuthentication(mchId)
.exchange(url, method, body, String.class);
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
throw new PayException("wechat pay v2 error ");
}
String result = responseEntity.getBody();
return XML_MAPPER.readTree(result);
}
private RestTemplate getRestTemplateClientAuthentication(String mchId)
throws IOException, UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException,
KeyStoreException, KeyManagementException {
ClassPathResource resource = new ClassPathResource("wechat/apiclient_cert.p12");
char[] pem = mchId.toCharArray();
KeyStore store = KeyStore.getInstance("PKCS12");
store.load(resource.getInputStream(), pem);
// Trust own CA and all self-signed certs
SSLContext sslcontext = SSLContextBuilder.create()
.loadKeyMaterial(store, pem)
.build();
// Allow TLSv1 protocol only
HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, new String[]{"TLSv1"},
null, hostnameVerifier);
CloseableHttpClient httpclient = HttpClients.custom()
.setSSLSocketFactory(sslsf)
.build();
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpclient);
return new RestTemplate(clientHttpRequestFactory);
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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.v2.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author felord.cn
* @since 1.0.5.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class GroupRedpackModel extends BaseModel {
private String wxappid;
private String mchId;
private String mchBillno;
private String sendName;
private String reOpenid;
private String totalAmount;
private String totalNum;
private String amtType = "ALL_RAND";
private String wishing;
private String clientIp;
private String actName;
private String remark;
private String sceneId;
private String riskInfo;
}

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.v2.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author felord.cn
* @since 1.0.5.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class RedpackInfoModel extends BaseModel {
private String appid;
private String mchId;
private String mchBillno;
private String billType = "MCHT";
}

View File

@@ -0,0 +1,28 @@
package cn.felord.payment.wechat.v2.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author felord.cn
* @since 1.0.5.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class RedpackModel extends BaseModel {
private String wxappid;
private String mchId;
private String mchBillno;
private String sendName;
private String reOpenid;
private String totalAmount;
private String totalNum;
private String wishing;
private String clientIp;
private String actName;
private String remark;
private String sceneId;
private String riskInfo;
}

View File

@@ -0,0 +1,33 @@
/*
* 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.v2.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author felord.cn
* @since 1.0.5.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class TransferInfoModel extends BaseModel {
private String appid;
private String mchId;
private String partnerTradeNo;
}

View File

@@ -0,0 +1,23 @@
package cn.felord.payment.wechat.v2.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author felord.cn
* @since 1.0.5.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class TransferModel extends BaseModel{
private String mchAppid;
private String mchid;
private String deviceInfo;
private String partnerTradeNo;
private String openid;
private String checkName;
private String reUserName;
private String amount;
private String desc;
private String spbillCreateIp;
}

View File

@@ -18,6 +18,10 @@
*/
package cn.felord.payment.wechat.v3;
import cn.felord.payment.wechat.v2.WechatPayRedpackApi;
import cn.felord.payment.wechat.v2.WechatPayTransfersApi;
import cn.felord.payment.wechat.v2.WechatV2Client;
/**
* 微信支付工具.
*
@@ -118,4 +122,34 @@ public class WechatApiProvider {
return new WechatPayCallback(wechatPayClient.signatureProvider(), tenantId);
}
/**
* 现金红包基于V2
*
* @param tenantId the tenant id
* @return wechat pay redpack api
* @since 1.0.5.RELEASE
*/
public WechatPayRedpackApi redpackApi(String tenantId) {
WechatMetaBean wechatMeta = wechatPayClient.signatureProvider()
.wechatMetaContainer()
.getWechatMeta(tenantId);
WechatV2Client wechatV2Client = new WechatV2Client(wechatMeta);
return new WechatPayRedpackApi(wechatV2Client);
}
/**
* 企业付款到零钱目前不包括到银行卡基于V2
*
* @param tenantId the tenant id
* @return wechat pay redpack api
* @since 1.0.5.RELEASE
*/
public WechatPayTransfersApi transfersApi(String tenantId) {
WechatMetaBean wechatMeta = wechatPayClient.signatureProvider()
.wechatMetaContainer()
.getWechatMeta(tenantId);
WechatV2Client wechatV2Client = new WechatV2Client(wechatMeta);
return new WechatPayTransfersApi(wechatV2Client);
}
}