This commit is contained in:
xiafang
2020-10-29 09:29:30 +08:00
parent 74618a51dd
commit 04d870be1a
39 changed files with 2177 additions and 0 deletions

View File

@@ -6,6 +6,18 @@
<groupId>com.enongm.dianji</groupId>
<artifactId>payment-spring-boot-autoconfigure</artifactId>
<version>1.0.0.RELEASE</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>7</source>
<target>7</target>
</configuration>
</plugin>
</plugins>
</build>
<packaging>jar</packaging>
<modelVersion>4.0.0</modelVersion>
@@ -21,6 +33,14 @@
</dependencies>
</dependencyManagement>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -47,6 +67,18 @@
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,91 @@
package com.enongm.dianji.payment.alipay;
import org.springframework.util.CollectionUtils;
import java.util.*;
/**
* The type Ali pay core.
*/
public class SignatureProvider {
/**
* 生成签名结果
*
* @param params 要签名的数组
* @param key 签名密钥
* @param signType 签名类型
* @return 签名结果字符串
*/
public static String requestSign(Map<String, String> params, String key, String signType) {
String preStr = createLinkString(params);
/* if (SignType.MD5.getType().equals(signType)) {
return SecureUtil.md5(preStr.concat(key));
}*/
return null;
}
/**
* 生成要请求给支付宝的参数数组
*
* @param params 请求前的参数数组
* @param key 商户的私钥
* @param signType 签名类型
* @return 要请求的参数数组 map
*/
public static Map<String, String> buildRequestPara(Map<String, String> params, String key, String signType) {
// 除去数组中的空值和签名参数
Map<String, String> tempMap = paraFilter(params);
// 生成签名结果
String mySign = requestSign(params, key, signType);
// 签名结果与签名方式加入请求提交参数组中
tempMap.put("sign", mySign);
tempMap.put("sign_type", signType);
return tempMap;
}
/**
* 除去数组中的空值和签名参数
*
* @param sArray 签名参数组
* @return 去掉空值与签名参数后的新签名参数组 map
*/
public static Map<String, String> paraFilter(Map<String, String> sArray) {
Map<String, String> result = new HashMap<String, String>(sArray.size());
if (CollectionUtils.isEmpty(sArray)) {
return Collections.emptyMap();
}
for (String key : sArray.keySet()) {
String value = sArray.get(key);
if (value == null || "".equals(value) || "sign".equalsIgnoreCase(key)
|| "sign_type".equalsIgnoreCase(key)) {
continue;
}
result.put(key, value);
}
return result;
}
/**
* 参数自然排序拼接
*
* @param params {'k':'b','e':'f','c':'d'} -> {'c':'d','e':'f','k':'b'}
* @return 拼接后字符串 c=d&e=f&k=b
*/
public static String createLinkString(Map<String, String> params) {
TreeMap<String, String> treeMap = new TreeMap<>(params);
Set<String> keySet = treeMap.keySet();
StringBuilder content = new StringBuilder();
for (String key : keySet) {
content.append(key).append("=")
.append(treeMap.get(key))
.append("&");
}
return content.substring(0, content.length() - 1);
}
}

View File

@@ -0,0 +1,13 @@
package com.enongm.dianji.payment.alipay.configuration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author Dax
* @since 14:35
*/
@Configuration
@EnableConfigurationProperties(AliPayProperties.class)
public class AliPayConfiguration {
}

View File

@@ -0,0 +1,51 @@
package com.enongm.dianji.payment.alipay.configuration;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
/**
* @author Dax
* @since 14:13
*/
@Data
@ConfigurationProperties("ali.pay")
public class AliPayProperties {
/**
* alipay api version 1.0
*/
@NestedConfigurationProperty
private V1 v1;
@Data
public static class V1{
/**
* alipay server
*/
private String serverUrl = "https://openapi.alipay.com/gateway.do";
/**
* your app ID
*/
private String appId;
/**
* your app private key
*/
private String appPrivateKey;
/**
* sign type
*/
private String signType = "md5";
/**
* alipay public cert path
*/
private String alipayPublicCertPath;
/**
* alipay root cert path
*/
private String alipayRootCertPath;
}
}

View File

@@ -0,0 +1,70 @@
package com.enongm.dianji.payment.autoconfigure;
import com.enongm.dianji.payment.wechat.KeyPairFactory;
import com.enongm.dianji.payment.wechat.WechatPayV3Service;
import com.enongm.dianji.payment.wechat.v2.WechatPayV2Service;
import com.enongm.dianji.payment.wechat.v3.SignatureProvider;
import com.enongm.dianji.payment.wechat.v3.model.WechatMetaBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* The type Wechat pay configuration.
*/
@Configuration
@EnableConfigurationProperties(WechatPayProperties.class)
public class WechatPayConfiguration {
private static final String CERT_ALIAS = "Tenpay Certificate";
/**
* 微信支付公私钥 以及序列号.
*
* @param wechatPayProperties the wechat pay properties
* @return the wechat cert bean
*/
@Bean
WechatMetaBean wechatMetaBean(WechatPayProperties wechatPayProperties) {
String certPath = wechatPayProperties.getV3().getCertPath();
String mchId = wechatPayProperties.getV3().getMchId();
WechatMetaBean wechatMetaBean = new KeyPairFactory().createPKCS12(certPath, CERT_ALIAS, mchId);
wechatMetaBean.setWechatPayProperties(wechatPayProperties);
return wechatMetaBean;
}
/**
* Signature provider signature provider.
*
* @param wechatMetaBean the wechat meta bean
* @return the signature provider
*/
@Bean
public SignatureProvider signatureProvider(WechatMetaBean wechatMetaBean) {
return new SignatureProvider(wechatMetaBean);
}
/**
* 微信支付V2 只实现V3支付没有的支付业务。
*
* @param wechatPayProperties the wechat pay properties
* @return the wechat pay v 2 service
*/
@Bean
public WechatPayV2Service wechatPayV2Service(WechatPayProperties wechatPayProperties) {
return new WechatPayV2Service(wechatPayProperties);
}
/**
* Wechat pay service wechat pay service.
*
* @param signatureProvider the signature provider
* @return the wechat pay service
*/
@Bean
public WechatPayV3Service wechatPayService(SignatureProvider signatureProvider) {
return new WechatPayV3Service(signatureProvider);
}
}

View File

@@ -0,0 +1,51 @@
package com.enongm.dianji.payment.autoconfigure;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
/**
* The type Wechat pay properties.
*/
@Data
@ConfigurationProperties("wechat.pay")
public class WechatPayProperties {
/**
* wechat pay V3 properties
*/
@NestedConfigurationProperty
private V3 v3;
@Data
public static class V3 {
/**
* app id for wechat pay is required
*/
private String appId;
/**
* app secret for wechat pay is required
*/
private String appSecret;
/**
* app V3 secret is required by wechat pay V3
*/
private String appV3Secret;
/**
* mchId for wechat pay is required
*/
private String mchId;
/**
* partnerKey for wechat pay is optional
*/
private String partnerKey;
/**
* wechat pay certificate Path
*/
private String certPath;
/**
* your pay server domain
*/
private String domain;
}
}

View File

@@ -0,0 +1,32 @@
package com.enongm.dianji.payment.wechat;
import com.enongm.dianji.payment.wechat.v3.WechatPayRequest;
import java.util.ArrayList;
import java.util.List;
/**
* @author Dax
* @since 16:16
*/
public class DefaultPayFilterChain implements PayFilterChain {
private int pos = 0;
private final List<PayFilter> filters = new ArrayList<>();
@Override
public void register(PayFilter filter) {
filters.add(filter);
}
@Override
public void doChain(WechatPayRequest request) {
int size = filters.size();
if (pos < size) {
PayFilter payFilter = filters.get(pos++);
payFilter.doFilter(request, this);
}
}
}

View File

@@ -0,0 +1,58 @@
package com.enongm.dianji.payment.wechat;
import com.enongm.dianji.payment.wechat.v3.model.WechatMetaBean;
import org.springframework.core.io.ClassPathResource;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
/**
* KeyPairFactory
*
* @author dax
* @since 13:41
**/
public class KeyPairFactory {
private KeyStore store;
private final Object lock = new Object();
/**
* 获取公私钥.
*
* @param keyPath the key path
* @param keyAlias the key alias
* @param keyPass password
* @return the key pair
*/
public WechatMetaBean createPKCS12(String keyPath, String keyAlias, String keyPass) {
ClassPathResource resource = new ClassPathResource(keyPath);
char[] pem = keyPass.toCharArray();
try {
synchronized (lock) {
if (store == null) {
synchronized (lock) {
store = KeyStore.getInstance("PKCS12");
store.load(resource.getInputStream(), pem);
}
}
}
X509Certificate certificate = (X509Certificate) store.getCertificate(keyAlias);
certificate.checkValidity();
String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase();
PublicKey publicKey = certificate.getPublicKey();
PrivateKey storeKey = (PrivateKey) store.getKey(keyAlias, pem);
WechatMetaBean wechatMetaBean = new WechatMetaBean();
wechatMetaBean.setKeyPair(new KeyPair(publicKey, storeKey));
wechatMetaBean.setSerialNumber(serialNumber);
return wechatMetaBean;
} catch (Exception e) {
throw new IllegalStateException("Cannot load keys from store: " + resource, e);
}
}
}

View File

@@ -0,0 +1,23 @@
package com.enongm.dianji.payment.wechat;
import com.enongm.dianji.payment.wechat.v3.WechatPayRequest;
/**
* The interface Pay filter.
*
* @author Dax
* @since 15 :08
*/
public interface PayFilter {
/**
* Do filter.
*
* @param request the request
* @param chain the chain
*/
void doFilter(WechatPayRequest request, PayFilterChain chain);
}

View File

@@ -0,0 +1,27 @@
package com.enongm.dianji.payment.wechat;
import com.enongm.dianji.payment.wechat.v3.WechatPayRequest;
/**
* The interface Pay filter chain.
*
* @author Dax
* @since 16 :11
*/
public interface PayFilterChain {
/**
* Do chain.
*
* @param request the request
*/
void doChain(WechatPayRequest request);
/**
* Register.
*
* @param filter the filter
*/
void register(PayFilter filter);
}

View File

@@ -0,0 +1,104 @@
package com.enongm.dianji.payment.wechat;
import com.enongm.dianji.payment.wechat.v3.SignatureProvider;
import com.enongm.dianji.payment.wechat.v3.WechatPayRequest;
import com.enongm.dianji.payment.wechat.v3.filter.BodyMergeFilter;
import com.enongm.dianji.payment.wechat.v3.filter.HeaderFilter;
import com.enongm.dianji.payment.wechat.v3.filter.HttpRequestFilter;
import com.enongm.dianji.payment.wechat.v3.filter.WechatServerFilter;
import com.enongm.dianji.payment.wechat.v3.model.BaseModel;
import com.enongm.dianji.payment.wechat.v3.model.WechatMetaBean;
import java.util.function.Function;
/**
* The type Wechat pay service.
*
* @author Dax
* @since 11 :43
*/
public class WechatPayV3Service {
private final PayFilterChain payFilterChain;
/**
* Instantiates a new Wechat pay service.
*
* @param payFilterChain the pay filter chain
*/
public WechatPayV3Service(PayFilterChain payFilterChain) {
this.payFilterChain = payFilterChain;
}
/**
* Instantiates a new Wechat pay service.
*
* @param signatureProvider the signature provider
*/
public WechatPayV3Service(SignatureProvider signatureProvider) {
WechatMetaBean wechatMetaBean = signatureProvider.getWechatMetaBean();
DefaultPayFilterChain defaultPayFilterChain = new DefaultPayFilterChain();
// 微信服务器选择
defaultPayFilterChain.register(new WechatServerFilter());
// 对请求体注入业务无关参数
defaultPayFilterChain.register(new BodyMergeFilter(wechatMetaBean));
// 构造私钥签名
defaultPayFilterChain.register(new HeaderFilter(signatureProvider));
defaultPayFilterChain.register(new HttpRequestFilter(signatureProvider));
this.payFilterChain = defaultPayFilterChain;
}
/**
* Exec executor.
*
* @param <M> the type parameter
* @param <R> the type parameter
* @param requestFunction the request function
* @return the executor
*/
public <M extends BaseModel, R extends WechatPayRequest> Executor<M, R> request(Function<M, R> requestFunction) {
return new Executor<>(this.payFilterChain, requestFunction);
}
/**
* The type Executor.
*
* @param <M> the type parameter
* @param <R> the type parameter
*/
public static class Executor<M extends BaseModel, R extends WechatPayRequest> {
/**
* The Pay filter chain.
*/
PayFilterChain payFilterChain;
/**
* The Request function.
*/
Function<M, R> requestFunction;
/**
* Instantiates a new Executor.
*
* @param payFilterChain the pay filter chain
* @param requestFunction the request function
*/
public Executor(PayFilterChain payFilterChain, Function<M, R> requestFunction) {
this.payFilterChain = payFilterChain;
this.requestFunction = requestFunction;
}
/**
* With model.
*
* @param model the model
*/
public void withModel(M model) {
payFilterChain.doChain(requestFunction.apply(model));
}
}
}

View File

@@ -0,0 +1,209 @@
package com.enongm.dianji.payment.wechat.enumeration;
/**
* The enum Bank code.
*/
public enum BankCode {
/**
* 工商银行
*/
BK_1002("1002","工商银行"),
/**
* 农业银行
*/
BK_1005("1005","农业银行"),
/**
* 建设银行
*/
BK_1003("1003","建设银行"),
/**
* 中国银行
*/
BK_1026("1026","中国银行"),
/**
* 交通银行
*/
BK_1020("1020","交通银行"),
/**
* 招商银行
*/
BK_1001("1001","招商银行"),
/**
* 邮储银行
*/
BK_1066("1066","邮储银行"),
/**
* 民生银行
*/
BK_1006("1006","民生银行"),
/**
* 平安银行
*/
BK_1010("1010","平安银行"),
/**
* 中信银行
*/
BK_1021("1021","中信银行"),
/**
* 浦发银行
*/
BK_1004("1004","浦发银行"),
/**
* 兴业银行
*/
BK_1009("1009","兴业银行"),
/**
* 光大银行
*/
BK_1022("1022","光大银行"),
/**
* 广发银行
*/
BK_1027("1027","广发银行"),
/**
* 华夏银行
*/
BK_1025("1025","华夏银行"),
/**
* 宁波银行
*/
BK_1056("1056","宁波银行"),
/**
* 北京银行
*/
BK_4836("4836","北京银行"),
/**
* 上海银行
*/
BK_1024("1024","上海银行"),
/**
* 南京银行
*/
BK_1054("1054","南京银行"),
/**
* 长子县融汇村镇银行
*/
BK_4755("4755","长子县融汇村镇银行"),
/**
* 长沙银行
*/
BK_4216("4216","长沙银行"),
/**
* 浙江泰隆商业银行
*/
BK_4051("4051","浙江泰隆商业银行"),
/**
* 中原银行
*/
BK_4753("4753","中原银行"),
/**
* 企业银行(中国)
*/
BK_4761("4761","企业银行(中国)"),
/**
* 顺德农商银行
*/
BK_4036("4036","顺德农商银行"),
/**
* 衡水银行
*/
BK_4752("4752","衡水银行"),
/**
* 长治银行
*/
BK_4756("4756","长治银行"),
/**
* 大同银行
*/
BK_4767("4767","大同银行"),
/**
* 河南省农村信用社
*/
BK_4115("4115","河南省农村信用社"),
/**
* 宁夏黄河农村商业银行
*/
BK_4150("4150","宁夏黄河农村商业银行"),
/**
* 山西省农村信用社
*/
BK_4156("4156","山西省农村信用社"),
/**
* 安徽省农村信用社
*/
BK_4166("4166","安徽省农村信用社"),
/**
* 甘肃省农村信用社
*/
BK_4157("4157","甘肃省农村信用社"),
/**
* 天津农村商业银行
*/
BK_4153("4153","天津农村商业银行"),
/**
* 广西壮族自治区农村信用社
*/
BK_4113("4113","广西壮族自治区农村信用社"),
/**
* 陕西省农村信用社
*/
BK_4108("4108","陕西省农村信用社"),
/**
* 深圳农村商业银行
*/
BK_4076("4076","深圳农村商业银行"),
/**
* 宁波鄞州农村商业银行
*/
BK_4052("4052","宁波鄞州农村商业银行"),
/**
* 浙江省农村信用社联合社
*/
BK_4764("4764","浙江省农村信用社联合社"),
/**
* 江苏省农村信用社联合社
*/
BK_4217("4217","江苏省农村信用社联合社"),
/**
* 江苏紫金农村商业银行股份有限公司
*/
BK_4072("4072","江苏紫金农村商业银行"),
/**
* 北京中关村银行股份有限公司
*/
BK_4769("4769","北京中关村银行"),
/**
* 星展银行(中国)有限公司
*/
BK_4778("4778","星展银行(中国)"),
/**
* 枣庄银行股份有限公司
*/
BK_4766("4766","枣庄银行"),
/**
* 海口联合农村商业银行股份有限公司
*/
BK_4758("4758","海口联合农村商业银行"),
/**
* 南洋商业银行(中国)有限公司
*/
BK_4763("4763","南洋商业银行(中国)");
private final String code;
private final String bankName;
BankCode(String code, String bankName) {
this.code = code;
this.bankName = bankName;
}
public String code() {
return this.code;
}
public String bankName() {
return this.bankName;
}
}

View File

@@ -0,0 +1,83 @@
package com.enongm.dianji.payment.wechat.enumeration;
/**
* @author Dax
* @since 10:33
*/
public enum V2PayType {
/**
* 企业向微信用户个人付款,目前支持向指定微信用户的openid付款。
*/
PAY_TO_WECHAT("POST", "%s%s/mmpaymkttransfers/promotion/transfers");
private final String method;
private final String pattern;
V2PayType(String method, String pattern) {
this.method = method;
this.pattern = pattern;
}
/**
* Method string.
*
* @return the string
*/
public String method() {
return this.method;
}
/**
* 默认支付URI.
*
* @param weChatServer the we chat server
* @return the string
*/
public String defaultUri(WeChatServer weChatServer) {
return uri(weChatServer, false, false);
}
/**
* 默认支付沙盒URI.
*
* @param weChatServer the we chat server
* @return the string
*/
public String defaultSandboxUri(WeChatServer weChatServer) {
return uri(weChatServer, true, false);
}
/**
* 合作商支付URI.
*
* @param weChatServer the we chat server
* @return the string
*/
public String partnerUri(WeChatServer weChatServer) {
return uri(weChatServer, false, true);
}
/**
* 合作商支付沙盒URI.
*
* @param weChatServer the we chat server
* @return the string
*/
public String partnerSandboxUri(WeChatServer weChatServer) {
return uri(weChatServer, true, true);
}
/**
* @param isSandbox 是否是沙盒测试
* @param isPartner 是否是合作商
* @return uri
*/
private String uri(WeChatServer weChatServer, boolean isSandbox, boolean isPartner) {
final String sandboxKey = isSandbox ? "/sandboxnew" : "";
final String partnerKey = isPartner ? "/partner" : "";
return String.format(this.pattern, weChatServer.domain(), sandboxKey, partnerKey);
}
}

View File

@@ -0,0 +1,108 @@
package com.enongm.dianji.payment.wechat.enumeration;
/**
* The enum Pay type.
*
* @author Dax
* @since 14 :25
*/
public enum V3PayType {
/**
* 获取证书.
*/
CERT("GET", "%s/v3/certificates"),
/**
* 微信公众号支付或者小程序支付
*/
JSAPI("POST", "%s%s/v3/pay%s/transactions/jsapi"),
/**
* 微信扫码支付
*/
NATIVE("POST", "%s%s/v3/pay%s/transactions/native"),
/**
* 微信APP支付
*/
APP("POST", "%s%s/v3/pay%s/transactions/app"),
/**
* H5支付
*/
MWEB("POST", "%s%s/v3/pay%s/transactions/h5");
private final String pattern;
private final String method;
V3PayType(String method, String pattern) {
this.method = method;
this.pattern = pattern;
}
/**
* Method string.
*
* @return the string
*/
public String method() {
return this.method;
}
/**
* 默认支付URI.
*
* @param weChatServer the we chat server
* @return the string
*/
public String defaultUri(WeChatServer weChatServer) {
return uri(weChatServer, false, false);
}
/**
* 默认支付沙盒URI.
*
* @param weChatServer the we chat server
* @return the string
*/
public String defaultSandboxUri(WeChatServer weChatServer) {
return uri(weChatServer, true, false);
}
/**
* 合作商支付URI.
*
* @param weChatServer the we chat server
* @return the string
*/
public String partnerUri(WeChatServer weChatServer) {
return uri(weChatServer, false, true);
}
/**
* 合作商支付沙盒URI.
*
* @param weChatServer the we chat server
* @return the string
*/
public String partnerSandboxUri(WeChatServer weChatServer) {
return uri(weChatServer, true, true);
}
/**
* @param isSandbox 是否是沙盒测试
* @param isPartner 是否是合作商
* @return uri
*/
private String uri(WeChatServer weChatServer, boolean isSandbox, boolean isPartner) {
if (this.equals(V3PayType.CERT)) {
return String.format(this.pattern, WeChatServer.CHINA.domain());
}
final String sandboxKey = isSandbox ? "/sandboxnew" : "";
final String partnerKey = isPartner ? "/partner" : "";
return String.format(this.pattern, weChatServer.domain(), sandboxKey, partnerKey);
}
}

View File

@@ -0,0 +1,56 @@
package com.enongm.dianji.payment.wechat.enumeration;
/**
* The enum We chat server domain.
*/
public enum WeChatServer {
/**
* 中国
*/
CHINA("https://api.mch.weixin.qq.com"),
/**
* 中国国内(备用域名)
*/
CHINA2("https://api2.mch.weixin.qq.com"),
/**
* 香港
*/
HK("https://apihk.mch.weixin.qq.com"),
/**
* 美国
*/
US("https://apius.mch.weixin.qq.com"),
/**
* 获取公钥
*/
FRAUD("https://fraud.mch.weixin.qq.com"),
/**
* 活动
*/
ACTION("https://action.weixin.qq.com");
/**
* 域名
*/
private final String domain;
WeChatServer(String domain) {
this.domain = domain;
}
/**
* Gets type.
*
* @return the type
*/
public String domain() {
return domain;
}
@Override
public String toString() {
return domain;
}
}

View File

@@ -0,0 +1,26 @@
package com.enongm.dianji.payment.wechat.v2;
import com.enongm.dianji.payment.autoconfigure.WechatPayProperties;
import com.enongm.dianji.payment.wechat.v2.model.BaseModel;
/**
* @author Dax
* @since 15:15
*/
public class WechatPayV2Service {
private final WechatPayProperties wechatPayProperties;
public WechatPayV2Service(WechatPayProperties wechatPayProperties) {
this.wechatPayProperties = wechatPayProperties;
}
public <M extends BaseModel> BaseModel model(M model) {
WechatPayProperties.V3 v3 = wechatPayProperties.getV3();
return model.appId(v3.getAppId())
.mchId(v3.getMchId());
}
}

View File

@@ -0,0 +1,24 @@
package com.enongm.dianji.payment.wechat.v2;
import lombok.Data;
/**
* @author Dax
* @since 15:28
*/
@Data
public class WechatResponseBody {
private String returnCode;
private String returnMsg;
private String mchAppid;
private String mchid;
private String deviceInfo;
private String nonceStr;
private String resultCode;
private String errCode;
private String errCodeDes;
private String partnerTradeNo;
private String paymentNo;
private String paymentTime;
}

View File

@@ -0,0 +1,152 @@
package com.enongm.dianji.payment.wechat.v2.model;
import com.enongm.dianji.payment.wechat.enumeration.V2PayType;
import com.enongm.dianji.payment.wechat.enumeration.WeChatServer;
import com.enongm.dianji.payment.wechat.v2.WechatResponseBody;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
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 okhttp3.*;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.MD5Digest;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.util.AlternativeJdkIdGenerator;
import org.springframework.util.IdGenerator;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
* The type Base model.
*
* @author Dax
* @since 16 :03
*/
@Getter
public class BaseModel {
private static final XmlMapper MAPPER = new XmlMapper();
static {
// 忽略null
MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL)
// 属性使用 驼峰首字母小写
.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
}
private static final OkHttpClient CLIENT = new OkHttpClient();
private static final IdGenerator ID_GENERATOR = new AlternativeJdkIdGenerator();
private final String nonceStr = ID_GENERATOR.generateId()
.toString()
.replaceAll("-", "");
private String mchAppid;
private String mchid;
private String sign;
@JsonIgnore
private String key;
@JsonIgnore
private V2PayType payType;
public BaseModel appId(String appId) {
this.mchAppid = appId;
return this;
}
public BaseModel mchId(String mchId) {
this.mchid = mchId;
return this;
}
public BaseModel payType(V2PayType payType) {
this.payType = payType;
return this;
}
/**
* Xml string.
*
* @return the string
*/
@SneakyThrows
public String xml() {
this.key = "Djkjchina19491001";
String link = link(this, key);
this.sign = this.bouncyCastleMD5(link);
return MAPPER.writer()
.withRootName("xml")
.writeValueAsString(this);
}
/**
* md5摘要.
*
* @param src the src
* @return the string
*/
private String bouncyCastleMD5(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, String apiKey) {
return new ObjectMapper()
.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true)
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
.writer()
.writeValueAsString(t)
.replaceAll("\":\"", "=")
.replaceAll("\",\"", "&")
.replaceAll("\\{\"", "")
.replaceAll("\"}", "")
.concat("&key=").concat(apiKey);
}
@SneakyThrows
public WechatResponseBody request() {
Request request = new Request.Builder()
.method(payType.method(), RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), this.xml()))
.url(payType.defaultUri(WeChatServer.CHINA))
.build();
System.out.println("request.toString() = " + request.toString());
Response response = CLIENT.newCall(request).execute();
ResponseBody body = response.body();
if (Objects.nonNull(body)) {
return MAPPER.readValue(body.string(), WechatResponseBody.class);
}
throw new IllegalStateException("wechat pay response body is empty");
}
}

View File

@@ -0,0 +1,22 @@
package com.enongm.dianji.payment.wechat.v2.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author Dax
* @since 15:48
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class PayToWechatModel extends BaseModel {
private String deviceInfo;
private String partnerTradeNo;
private String openid;
private String checkName = "NO_CHECK";
private String reUserName;
private String amount;
private String desc;
private String spbillCreateIp;
}

View File

@@ -0,0 +1,248 @@
package com.enongm.dianji.payment.wechat.v3;
import com.enongm.dianji.payment.wechat.enumeration.V3PayType;
import com.enongm.dianji.payment.wechat.enumeration.WeChatServer;
import com.enongm.dianji.payment.wechat.v3.model.WechatMetaBean;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.SneakyThrows;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.util.AlternativeJdkIdGenerator;
import org.springframework.util.Base64Utils;
import org.springframework.util.IdGenerator;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 签名 加签 验签
* <p>
* 我方请求微信服务器时需要根据我方的API证书对参数进行加签微信服务器会根据我方签名验签以确定请求来自我方服务器
* <p>
* 然后微信服务器响应我方请求并在响应报文中使用【微信平台证书】加签 我方需要根据规则验签是否响应来自微信支付服务器
* <p>
* 其中【微信平台证书】定期会进行更新,不受我方管控,我方需要适当的时候获取最新的证书列表。
*
* @author Dax
* @since 16 :48
*/
public class SignatureProvider {
/**
* The constant APPLICATION_JSON.
*/
public static final String APPLICATION_JSON = "application/json";
private static final IdGenerator ID_GENERATOR = new AlternativeJdkIdGenerator();
private static final String SCHEMA = "WECHATPAY2-SHA256-RSA2048 ";
/**
* The constant TOKEN_PATTERN.
*/
public static final String TOKEN_PATTERN = "mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\"";
private final WechatMetaBean wechatMetaBean;
/**
* 微信平台证书容器 key = 序列号 value = 证书对象
*/
private static final Map<String, Certificate> CERTIFICATE_MAP = new ConcurrentHashMap<>();
/**
* Instantiates a new Signature provider.
*
* @param wechatMetaBean the wechat meta bean
*/
public SignatureProvider(WechatMetaBean wechatMetaBean) {
this.wechatMetaBean = wechatMetaBean;
}
/**
* 我方请求时加签使用API证书.
*
* @param method the method
* @param canonicalUrl the canonical url
* @param body the body
* @return the string
*/
@SneakyThrows
public String requestSign(String method, String canonicalUrl, String body) {
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(wechatMetaBean.getKeyPair().getPrivate());
long timestamp = System.currentTimeMillis() / 1000;
String nonceStr = ID_GENERATOR.generateId()
.toString()
.replaceAll("-", "");
final String signatureStr = createSign(method, canonicalUrl, String.valueOf(timestamp), nonceStr, body);
signer.update(signatureStr.getBytes(StandardCharsets.UTF_8));
String encode = Base64Utils.encodeToString(signer.sign());
// 序列号
String serialNo = wechatMetaBean.getSerialNumber();
// 生成token
String token = String.format(TOKEN_PATTERN,
wechatMetaBean.getWechatPayProperties().getV3().getMchId(),
nonceStr, timestamp, serialNo, encode);
return SCHEMA.concat(token);
}
/**
* 我方对响应验签,和应答签名做比较,使用微信平台证书.
*
* @param wechatpaySerial response.headers['Wechatpay-Serial'] 当前使用的微信平台证书序列号
* @param wechatpaySignature response.headers['Wechatpay-Signature'] 微信平台签名
* @param wechatpayTimestamp response.headers['Wechatpay-Timestamp'] 微信服务器的时间戳
* @param wechatpayNonce response.headers['Wechatpay-Nonce'] 微信服务器提供的随机串
* @param body response.body 微信服务器的响应体
* @return the boolean
*/
@SneakyThrows
public boolean responseSignVerify(String wechatpaySerial, String wechatpaySignature, String wechatpayTimestamp, String wechatpayNonce, String body) {
if (CERTIFICATE_MAP.isEmpty() || !CERTIFICATE_MAP.containsKey(wechatpaySerial)) {
refreshCertificate();
}
Certificate certificate = CERTIFICATE_MAP.get(wechatpaySerial);
final String signatureStr = createSign(wechatpayTimestamp, wechatpayNonce, body);
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initVerify(certificate);
signer.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return signer.verify(Base64Utils.decodeFromString(wechatpaySignature));
}
/**
* 当我方服务器不存在平台证书或者证书同当前响应报文中的证书序列号不一致时应当刷新 调用/v3/certificates
*/
@SneakyThrows
private synchronized void refreshCertificate() {
String url = V3PayType.CERT.defaultUri(WeChatServer.CHINA);
HttpUrl httpUrl = HttpUrl.get(url);
String canonicalUrl = httpUrl.encodedPath();
String encodedQuery = httpUrl.encodedQuery();
if (encodedQuery != null) {
canonicalUrl += "?" + encodedQuery;
}
// 签名
String authorization = requestSign(V3PayType.CERT.method(), canonicalUrl, "");
Request request = new Request.Builder()
.get()
.url(httpUrl)
.addHeader("Accept", APPLICATION_JSON)
.addHeader("Authorization", authorization)
.addHeader("Content-Type", APPLICATION_JSON)
.addHeader("User-Agent", "pay-service")
.build();
Response response = new OkHttpClient().newCall(request).execute();
if (Objects.isNull(response.body())) {
throw new RuntimeException("cant obtain the response body");
}
String body = response.body().string();
ObjectMapper MAPPER = new ObjectMapper();
ObjectNode bodyObjectNode = MAPPER.readValue(body, ObjectNode.class);
ArrayNode certificates = bodyObjectNode.withArray("data");
if (certificates.isArray() && certificates.size() > 0) {
final CertificateFactory cf = CertificateFactory.getInstance("X509");
certificates.forEach(objectNode -> {
JsonNode encryptCertificate = objectNode.get("encrypt_certificate");
String associatedData = encryptCertificate.get("associated_data").asText();
String nonce = encryptCertificate.get("nonce").asText();
String ciphertext = encryptCertificate.get("ciphertext").asText();
String publicKey = decryptResponseBody(associatedData, nonce, ciphertext);
ByteArrayInputStream inputStream = new ByteArrayInputStream(publicKey.getBytes(StandardCharsets.UTF_8));
Certificate certificate = null;
try {
certificate = cf.generateCertificate(inputStream);
} catch (CertificateException e) {
e.printStackTrace();
}
String responseSerialNo = objectNode.get("serial_no").asText();
CERTIFICATE_MAP.clear();
CERTIFICATE_MAP.put(responseSerialNo, certificate);
});
}
}
/**
* 解密响应体.
*
* @param associatedData the associated data
* @param nonce the nonce
* @param ciphertext the ciphertext
* @return the string
*/
public String decryptResponseBody(String associatedData, String nonce, String ciphertext) {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
String apiV3Key = wechatMetaBean.getWechatPayProperties().getV3().getAppV3Secret();
SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));
byte[] bytes;
try {
bytes = cipher.doFinal(Base64Utils.decodeFromString(ciphertext));
} catch (GeneralSecurityException e) {
throw new IllegalArgumentException(e);
}
return new String(bytes, StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
public WechatMetaBean getWechatMetaBean() {
return wechatMetaBean;
}
/**
* 请求时设置签名 组件
*
* @param components the components
* @return string string
*/
private String createSign(String... components) {
return Arrays.stream(components)
.collect(Collectors.joining("\n", "", "\n"));
}
}

View File

@@ -0,0 +1,118 @@
package com.enongm.dianji.payment.wechat.v3;
import com.enongm.dianji.payment.wechat.enumeration.V3PayType;
import com.enongm.dianji.payment.wechat.enumeration.WeChatServer;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
/**
* The type Wechat pay request.
*
* @author Dax
* @since 9 :18
*/
@Getter
public class WechatPayRequest {
private V3PayType v3PayType;
private WeChatServer weChatServer;
private String body;
private final Map<String, String> headers = new HashMap<>();
private Consumer<String> responseBodyConsumer;
/**
* Pay type wechat pay request.
*
* @param v3PayType the pay type
* @return the wechat pay request
*/
public WechatPayRequest v3PayType(V3PayType v3PayType) {
this.v3PayType = v3PayType;
return this;
}
/**
* We chat server.
*
* @param weChatServer the we chat server
*/
public void weChatServer(WeChatServer weChatServer) {
this.weChatServer = weChatServer;
}
/**
* Body wechat pay request.
*
* @param body the body
* @return the wechat pay request
*/
public WechatPayRequest body(String body) {
this.body = body;
return this;
}
/**
* Add header wechat pay request.
*
* @param name the name
* @param value the value
* @return the wechat pay request
*/
public WechatPayRequest addHeader(String name, String value) {
headers.put(name, value);
return this;
}
/**
* Response consumer wechat pay request.
*
* @param responseConsumer the response consumer
* @return the wechat pay request
*/
public WechatPayRequest responseConsumer(Consumer<String> responseConsumer) {
this.responseBodyConsumer = responseConsumer;
return this;
}
/**
* Url string.
*
* @return the string
*/
public String url() {
return this.v3PayType.defaultUri(this.weChatServer);
}
/**
* Default sandbox uri string.
*
* @return the string
*/
public String defaultSandboxUri() {
return this.v3PayType.defaultSandboxUri(this.weChatServer);
}
/**
* Partner uri string.
*
* @return the string
*/
public String partnerUri() {
return this.v3PayType.partnerUri(this.weChatServer);
}
/**
* Partner sandbox uri string.
*
* @return the string
*/
public String partnerSandboxUri() {
return this.v3PayType.partnerSandboxUri(this.weChatServer);
}
}

View File

@@ -0,0 +1,70 @@
package com.enongm.dianji.payment.wechat.v3.filter;
import com.enongm.dianji.payment.autoconfigure.WechatPayProperties;
import com.enongm.dianji.payment.wechat.PayFilter;
import com.enongm.dianji.payment.wechat.PayFilterChain;
import com.enongm.dianji.payment.wechat.v3.WechatPayRequest;
import com.enongm.dianji.payment.wechat.v3.model.WechatMetaBean;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.SneakyThrows;
import org.springframework.util.StringUtils;
/**
* The type Body merge filter.
* 2
*
* @author Dax
* @since 11 :13
*/
public class BodyMergeFilter implements PayFilter {
private static final ObjectMapper MAPPER = new ObjectMapper();
private final WechatMetaBean wechatMetaBean;
static {
MAPPER.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
public BodyMergeFilter(WechatMetaBean wechatMetaBean) {
this.wechatMetaBean = wechatMetaBean;
}
@Override
public void doFilter(WechatPayRequest request, PayFilterChain chain) {
mergeAppIdAndMchIdIntoBody(request);
chain.doChain(request);
}
/**
* 将直连商户申请的公众号或移动应用appid 、商户号mchid 放入请求体
*/
@SneakyThrows
private void mergeAppIdAndMchIdIntoBody(WechatPayRequest request) {
String requestBody = request.getBody();
if (StringUtils.hasText(requestBody)) {
WechatPayProperties wechatPayProperties = wechatMetaBean.getWechatPayProperties();
String appId = wechatPayProperties.getV3().getAppId();
String mchId = wechatPayProperties.getV3().getMchId();
ObjectNode jsonNodes = MAPPER.readValue(requestBody, ObjectNode.class);
JsonNode notify = jsonNodes.get("notify_url");
String notifyUrl = wechatPayProperties.getV3().getDomain()
.concat(notify.asText());
// ^https?://([^\\s/?#\\[\\]\\@]+\\@)?([^\\s/?#\\@:]+)(?::\\d{2,5})?([^\\s?#\\[\\]]*)$
jsonNodes.put("notify_url", notifyUrl);
jsonNodes.put("appid", appId);
jsonNodes.put("mchid", mchId);
request.body(MAPPER.writeValueAsString(jsonNodes));
}
}
}

View File

@@ -0,0 +1,48 @@
package com.enongm.dianji.payment.wechat.v3.filter;
import com.enongm.dianji.payment.wechat.PayFilter;
import com.enongm.dianji.payment.wechat.PayFilterChain;
import com.enongm.dianji.payment.wechat.v3.SignatureProvider;
import com.enongm.dianji.payment.wechat.v3.WechatPayRequest;
import okhttp3.HttpUrl;
/**
* 微信支付 给请求添加必要的请求头.
* 3
*
* @author Dax
* @since 15 :12
*/
public class HeaderFilter implements PayFilter {
private static final String APPLICATION_JSON = "application/json";
private final SignatureProvider signatureProvider;
public HeaderFilter(SignatureProvider signatureProvider) {
this.signatureProvider = signatureProvider;
}
@Override
public void doFilter(WechatPayRequest request, PayFilterChain chain) {
// 签名
HttpUrl url = HttpUrl.get(request.url());
String canonicalUrl = url.encodedPath();
String encodedQuery = url.encodedQuery();
if (encodedQuery != null) {
canonicalUrl += "?" + encodedQuery;
}
String method = request.getV3PayType().method();
String body = "GET".equals(method) ? "" : request.getBody();
String authorization = signatureProvider.requestSign(method, canonicalUrl, body);
request.addHeader("Accept", APPLICATION_JSON)
.addHeader("Authorization", authorization)
.addHeader("Content-Type", APPLICATION_JSON)
.addHeader("User-Agent", "pay-service");
chain.doChain(request);
}
}

View File

@@ -0,0 +1,91 @@
package com.enongm.dianji.payment.wechat.v3.filter;
import com.enongm.dianji.payment.wechat.PayFilter;
import com.enongm.dianji.payment.wechat.PayFilterChain;
import com.enongm.dianji.payment.wechat.v3.SignatureProvider;
import com.enongm.dianji.payment.wechat.v3.WechatPayRequest;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import java.io.IOException;
import java.util.Objects;
import java.util.function.Consumer;
/**
* 微信支付用来请求微信支付服务器
*
* @author Dax
* @since 10:42
*/
@Slf4j
public class HttpRequestFilter implements PayFilter {
private final OkHttpClient client;
private final SignatureProvider signatureProvider;
public HttpRequestFilter(SignatureProvider signatureProvider) {
this.signatureProvider = signatureProvider;
this.client = new OkHttpClient();
}
public HttpRequestFilter(SignatureProvider signatureProvider, OkHttpClient client) {
this.signatureProvider = signatureProvider;
this.client = client;
}
@Override
public void doFilter(WechatPayRequest request, PayFilterChain chain) {
Request.Builder builder = new Request.Builder()
.url(request.url())
.headers(Headers.of(request.getHeaders()));
if (!request.getV3PayType().method().equals("GET")) {
RequestBody requestBody =
RequestBody.create(MediaType.parse("application/json"), request.getBody());
builder.method(request.getV3PayType().method(), requestBody);
}
Request httpRequest = builder.build();
try {
Response response = client.newCall(httpRequest).execute();
ResponseBody responseBody = response.body();
if (Objects.isNull(responseBody)) {
throw new IllegalStateException("cant obtain response body");
}
// 微信请求回调id
String RequestId = response.header("Request-ID");
// 微信平台证书序列号 用来取微信平台证书
String wechatpaySerial = response.header("Wechatpay-Serial");
//获取应答签名
String wechatpaySignature = response.header("Wechatpay-Signature");
String body = responseBody.string();
//构造验签名串
String wechatpayTimestamp = response.header("Wechatpay-Timestamp");
String wechatpayNonce = response.header("Wechatpay-Nonce");
// 验证微信服务器签名
if (signatureProvider.responseSignVerify(wechatpaySerial, wechatpaySignature, wechatpayTimestamp, wechatpayNonce, body)) {
;
Consumer<String> responseConsumer = request.getResponseBodyConsumer();
if (Objects.nonNull(responseConsumer)) {
// 验证通过消费
responseConsumer.accept(body);
}
} else {
throw new IllegalArgumentException("签名验证失败");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,25 @@
package com.enongm.dianji.payment.wechat.v3.filter;
import com.enongm.dianji.payment.wechat.PayFilter;
import com.enongm.dianji.payment.wechat.PayFilterChain;
import com.enongm.dianji.payment.wechat.enumeration.V3PayType;
import com.enongm.dianji.payment.wechat.enumeration.WeChatServer;
import com.enongm.dianji.payment.wechat.v3.WechatPayRequest;
/**
* 根据{@link V3PayType} 组装URL
* 1
*
* @author Dax
* @since 9:43
*/
public class WechatServerFilter implements PayFilter {
@Override
public void doFilter(WechatPayRequest request, PayFilterChain chain) {
request.weChatServer(WeChatServer.CHINA);
chain.doChain(request);
}
}

View File

@@ -0,0 +1,14 @@
package com.enongm.dianji.payment.wechat.v3.model;
import lombok.Data;
/**
* 支付金额 货币单位【分】默认使用人民币标识CNY
* @author Dax
* @since 16:45
*/
@Data
public class Amount {
private int total;
private String currency ="CNY";
}

View File

@@ -0,0 +1,17 @@
package com.enongm.dianji.payment.wechat.v3.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author Dax
* @since 17:10
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class AppPayModel extends BaseModel {
private Amount amount;
private Detail detail;
private SceneInfo sceneInfo;
}

View File

@@ -0,0 +1,54 @@
package com.enongm.dianji.payment.wechat.v3.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import lombok.Data;
import lombok.SneakyThrows;
/**
* @author Dax
* @since 16:34
*/
@Data
public class BaseModel {
/**
* 商品描述
* Image形象店-深圳腾大-QQ公仔
*/
private String description;
/**
* 商户系统内部订单号只能是数字、大小写字母_-*且在同一个商户号下唯一,详见【商户订单号】。
* 示例值1217752501201407033233368018
*/
private String outTradeNo;
/**
* 订单失效时间 YYYY-MM-DDTHH:mm:ss+TIMEZONE
*/
private String timeExpire;
/**
* 附加数据在查询API和支付通知中原样返回可作为自定义参数使用
*/
private String attach;
/**
* 通知URL必须为直接可访问的URL不允许携带查询串。
*/
private String notifyUrl;
/**
* 订单优惠标记
*/
private String goodsTag;
/**
* 支付金额
*/
private Amount amount;
@SneakyThrows
public String jsonBody() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return objectMapper.writeValueAsString(this);
}
}

View File

@@ -0,0 +1,25 @@
package com.enongm.dianji.payment.wechat.v3.model;
import lombok.Data;
import java.util.List;
/**
* @author Dax
* @since 17:01
*/
@Data
public class Detail {
/**
* 订单原价
*/
private int costPrice;
/**
* 商品小票ID
*/
private String invoiceId;
/**
* 单品列表
*/
private List<Goods> goodsDetail;
}

View File

@@ -0,0 +1,28 @@
package com.enongm.dianji.payment.wechat.v3.model;
/**
* @author Dax
* @since 17:02
*/
public class Goods {
/**
* 商户侧商品编码
*/
private String merchantGoodsId;
/**
* 微信侧商品编码
*/
private String wechatpayGoodsId;
/**
* 商品名称
*/
private String goodsName;
/**
* 商品数量
*/
private int quantity;
/**
* 商品单价
*/
private int unitPrice;
}

View File

@@ -0,0 +1,31 @@
package com.enongm.dianji.payment.wechat.v3.model;
import lombok.Data;
/**
* @author Dax
* @since 17:03
*/
@Data
public class H5Info {
/**
* 场景类型
*/
private String type;
/**
* 应用名称
*/
private String appName;
/**
* 网站URL
*/
private String appUrl;
/**
* IOS 平台 BundleID
*/
private String bundleId;
/**
* Android 平台 PackageName
*/
private String packageName;
}

View File

@@ -0,0 +1,23 @@
package com.enongm.dianji.payment.wechat.v3.model;
import lombok.Data;
/**
* @author Dax
* @since 17:04
*/
@Data
public class Payer {
/**
* 用户标识
*/
private String openid;
/**
* 用户服务标识
*/
private String spOpenid;
/**
* 用户子标识
*/
private String subOpenid;
}

View File

@@ -0,0 +1,27 @@
package com.enongm.dianji.payment.wechat.v3.model;
import lombok.Data;
/**
* @author Dax
* @since 17:05
*/
@Data
public class SceneInfo {
/**
* 用户终端IP
*/
private String payerClientIp;
/**
* 商户端设备号
*/
private String deviceId;
/**
* 商户门店信息
*/
private StoreInfo storeInfo;
/**
* H5 场景信息
*/
private H5Info h5Info;
}

View File

@@ -0,0 +1,16 @@
package com.enongm.dianji.payment.wechat.v3.model;
/**
* @author Dax
* @since 17:05
*/
public class SettleInfo {
/**
* 是否指定分账
*/
private boolean profitSharing;
/**
* 补差金额
*/
private int subsidyAmount;
}

View File

@@ -0,0 +1,27 @@
package com.enongm.dianji.payment.wechat.v3.model;
import lombok.Data;
/**
* @author Dax
* @since 17:06
*/
@Data
public class StoreInfo {
/**
* 门店编号
*/
private String id;
/**
* 门店名称
*/
private String name;
/**
* 地区编码
*/
private String areaCode;
/**
* 详细地址
*/
private String address;
}

View File

@@ -0,0 +1,27 @@
package com.enongm.dianji.payment.wechat.v3.model;
import com.enongm.dianji.payment.autoconfigure.WechatPayProperties;
import lombok.Data;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import java.security.KeyPair;
/**
* @author Dax
* @since 15:50
*/
@Data
public class WechatMetaBean implements InitializingBean {
private KeyPair keyPair;
private String serialNumber;
private WechatPayProperties wechatPayProperties;
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(wechatPayProperties, "wechatPayProperties must not be null");
Assert.notNull(keyPair, "wechat pay p12 certificate has not loaded");
Assert.hasText(serialNumber, "wechat pay p12 certificate SerialNumber must not null");
}
}

View File

@@ -0,0 +1,31 @@
package com.enongm.dianji.payment.wechat.v3.service;
import com.enongm.dianji.payment.wechat.enumeration.V3PayType;
import com.enongm.dianji.payment.wechat.v3.WechatPayRequest;
import com.enongm.dianji.payment.wechat.v3.model.AppPayModel;
/**
* The type Wechat pay v 3 api.
*
* @author Dax
* @since 17 :47
*/
public class WechatPayV3Api {
/**
* APP 支付.
*
* @param appPayModel the app pay model
* @return the wechat pay request
*/
public static WechatPayRequest appPay(AppPayModel appPayModel) {
return new WechatPayRequest().responseConsumer(System.out::println)
// 如果需要支持容灾 需要对server 进行一些处理
.v3PayType(V3PayType.APP)
.body(appPayModel.jsonBody());
}
}