自定义messageConverter

This commit is contained in:
felord.cn
2020-12-01 22:36:32 +08:00
parent 0e1efd4e94
commit 30d04ec4da
57 changed files with 101 additions and 133 deletions

View File

@@ -0,0 +1,27 @@
package cn.felord.payment;
/**
* @author Dax
* @since 14:18
*/
public class PayException extends RuntimeException {
public PayException() {
}
public PayException(String message) {
super(message);
}
public PayException(String message, Throwable cause) {
super(message, cause);
}
public PayException(Throwable cause) {
super(cause);
}
public PayException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@@ -0,0 +1,73 @@
package cn.felord.payment.alipay;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.CertAlipayRequest;
import com.alipay.api.DefaultAlipayClient;
import cn.felord.payment.PayException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
/**
* @author Dax
* @since 14:35
*/
@Slf4j
@Configuration
@ConditionalOnProperty(prefix = "ali.pay", name = "v1.app-id")
@EnableConfigurationProperties(AliPayProperties.class)
public class AliPayConfiguration {
@Bean
public AlipayClient alipayClient(AliPayProperties aliPayProperties) throws AlipayApiException {
PropertyMapper propertyMapper = PropertyMapper.get().alwaysApplyingWhenNonNull();
AliPayProperties.V1 v1 = aliPayProperties.getV1();
CertAlipayRequest certAlipayRequest = new CertAlipayRequest();
propertyMapper.from(v1::getServerUrl).to(certAlipayRequest::setServerUrl);
propertyMapper.from(v1::getAppId).to(certAlipayRequest::setAppId);
propertyMapper.from(v1::getAppPrivateKeyPath).as(this::appRSAPrivateKey).to(certAlipayRequest::setPrivateKey);
propertyMapper.from(v1::getFormat).to(certAlipayRequest::setFormat);
propertyMapper.from(v1::getCharset).to(certAlipayRequest::setCharset);
propertyMapper.from(v1::getSignType).to(certAlipayRequest::setSignType);
propertyMapper.from(v1::getAppCertPublicKeyPath).as(this::getFileAbsolutePath).to(certAlipayRequest::setCertPath);
propertyMapper.from(v1::getAlipayPublicCertPath).as(this::getFileAbsolutePath).to(certAlipayRequest::setAlipayPublicCertPath);
propertyMapper.from(v1::getAlipayRootCertPath).as(this::getFileAbsolutePath).to(certAlipayRequest::setRootCertPath);
return new DefaultAlipayClient(certAlipayRequest);
}
private String getFileAbsolutePath(String classPath) {
try {
return new ClassPathResource(classPath).getFile().getAbsolutePath();
} catch (IOException e) {
log.error("ali pay cert path is not exist ,{}", e.getMessage());
throw new PayException("ali pay cert path is not exist");
}
}
private String appRSAPrivateKey(String classPath) {
try {
File file = new ClassPathResource(classPath).getFile();
return new BufferedReader(new FileReader(file)).readLine();
} catch (IOException e) {
log.error("ali pay app private key is required ,{}", e.getMessage());
throw new PayException("ali pay app private key is required");
}
}
}

View File

@@ -0,0 +1,68 @@
package cn.felord.payment.alipay;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
/**
* The type Ali pay properties.
*
* @author Dax
* @since 14 :13
*/
@Data
@ConfigurationProperties("ali.pay")
public class AliPayProperties {
/**
* alipay api version 1.0
*/
@NestedConfigurationProperty
private V1 v1;
/**
* The type V 1.
*/
@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, which must be in a single line
*/
private String appPrivateKeyPath;
/**
* sign type default RSA2
*/
private String signType = "RSA2";
/**
* data format only json now
*/
private String format ="json";
/**
* charset default utf-8
*/
private String charset ="utf-8";
/**
* alipay public cert path
*/
private String alipayPublicCertPath;
/**
* alipay root cert path
*/
private String alipayRootCertPath;
/**
* appCertPublicKey
*/
private String appCertPublicKeyPath;
}
}

View File

@@ -0,0 +1,16 @@
package cn.felord.payment.autoconfigure;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
* @author Dax
* @since 9:49
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(PayConfiguration.class)
public @interface EnableMobilePay {
}

View File

@@ -0,0 +1,15 @@
package cn.felord.payment.autoconfigure;
import cn.felord.payment.alipay.AliPayConfiguration;
import cn.felord.payment.wechat.WechatPayConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
* @author Dax
* @since 14:47
*/
@Configuration
@Import({WechatPayConfiguration.class, AliPayConfiguration.class})
public class PayConfiguration {
}

View File

@@ -0,0 +1,109 @@
package cn.felord.payment.wechat;
import cn.felord.payment.wechat.oauth2.OAuth2AuthorizationRequestRedirectProvider;
import cn.felord.payment.wechat.v3.*;
import cn.felord.payment.wechat.v3.*;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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
@ConditionalOnProperty(prefix = "wechat.pay", name = "v3.app-id")
@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;
}
/**
* 微信支付V3签名工具.
*
* @param wechatMetaBean the wechat meta bean
* @return the signature provider
*/
@Bean
SignatureProvider signatureProvider(WechatMetaBean wechatMetaBean) {
return new SignatureProvider(wechatMetaBean);
}
/**
* 微信支付V3 客户端.
*
* @param signatureProvider the signature provider
* @return the wechat pay service
*/
@Bean
public WechatPayClient wechatPayService(SignatureProvider signatureProvider) {
return new WechatPayClient(signatureProvider);
}
/**
* Wechat pay v3 api.
*
* @param wechatPayClient the wechat pay v 3 client
* @param wechatMetaBean the wechat meta bean
* @return the wechat pay v 3 api
*/
@Bean
public WechatPayApi wechatPayApi(WechatPayClient wechatPayClient, WechatMetaBean wechatMetaBean) {
return new WechatPayApi(wechatPayClient, wechatMetaBean);
}
/**
* Wechat marketing api wechat marketing api.
*
* @param wechatPayClient the wechat pay client
* @param wechatMetaBean the wechat meta bean
* @return the wechat marketing api
*/
@Bean
public WechatMarketingApi wechatMarketingApi(WechatPayClient wechatPayClient, WechatMetaBean wechatMetaBean) {
return new WechatMarketingApi(wechatPayClient, wechatMetaBean);
}
/**
* Wechat pay callback.
*
* @param signatureProvider the signature provider
* @return the wechat pay callback
*/
@Bean
public WechatPayCallback wechatPayCallback(SignatureProvider signatureProvider) {
return new WechatPayCallback(signatureProvider);
}
/**
* 公众号授权工具用于获取用户openId需要配置{@link WechatPayProperties.Mp}.
*
* @param wechatPayProperties the wechat pay properties
* @return the o auth 2 authorization request redirect provider
*/
@Bean
@ConditionalOnProperty(prefix = "wechat.pay", name = "v3.mp.app-id")
public OAuth2AuthorizationRequestRedirectProvider oAuth2Provider(WechatPayProperties wechatPayProperties) {
WechatPayProperties.Mp mp = wechatPayProperties.getV3().getMp();
return new OAuth2AuthorizationRequestRedirectProvider(mp.getAppId(), mp.getAppSecret());
}
}

View File

@@ -0,0 +1,75 @@
package cn.felord.payment.wechat;
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;
/**
* wechat pay v3 properties.
*/
@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;
/**
* wechat mp binding with mch
*/
private Mp mp;
}
/**
* wechat mp for send coupons and notification.
*/
@Data
public static class Mp {
/**
* app id for wechat pay is required
*/
private String appId;
/**
* app secret for wechat pay is required
*/
private String appSecret;
}
}

View File

@@ -0,0 +1,18 @@
package cn.felord.payment.wechat;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.DefaultResponseErrorHandler;
import java.io.IOException;
/**
* @author Dax
* @since 12:57
*/
public class WechatPayResponseErrorHandler extends DefaultResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return false;
}
}

View File

@@ -0,0 +1,209 @@
package cn.felord.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,32 @@
package cn.felord.payment.wechat.enumeration;
/**
* 优惠券背景色
* <p>
* https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/marketing/convention/chapter3_1.shtml#menu1
*
* @author Dax
* @since 14:50
*/
public enum CouponBgColor {
COLOR010("#63B359"),
COLOR020("#2C9F67"),
COLOR030("#509FC9"),
COLOR040("#5885CF"),
COLOR050("#9062C0"),
COLOR060("#D09A45"),
COLOR070("#E4B138"),
COLOR080("#EE903C"),
COLOR090("#DD6549"),
COLOR100("#CC463D");
private final String color;
CouponBgColor(String color) {
this.color = color;
}
public String color() {
return this.color;
}
}

View File

@@ -0,0 +1,22 @@
package cn.felord.payment.wechat.enumeration;
/**
* The enum Coupon status.
*
* @author Dax
* @since 10 :57
*/
public enum CouponStatus {
/**
* 可用.
*/
SENDED,
/**
* 已实扣.
*/
USED,
/**
* 已过期.
*/
EXPIRED
}

View File

@@ -0,0 +1,56 @@
package cn.felord.payment.wechat.enumeration;
/**
* The enum Stock status.
*
* @author Dax
* @since 15 :17
*/
public enum StockStatus {
/**
* Unactivated stock status.
*/
UNACTIVATED("unactivated", "未激活"),
/**
* Audit stock status.
*/
AUDIT("audit", "审核中"),
/**
* Running stock status.
*/
RUNNING("running", "运行中"),
/**
* Stoped stock status.
*/
STOPED("stoped", "已停止"),
/**
* Paused stock status.
*/
PAUSED("paused", "暂停发放");
private final String value;
private final String description;
StockStatus(String value, String description) {
this.value = value;
this.description = description;
}
/**
* Value string.
*
* @return the string
*/
public String value() {
return this.value;
}
/**
* Description string.
*
* @return the string
*/
public String description() {
return this.description;
}
}

View File

@@ -0,0 +1,56 @@
package cn.felord.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,115 @@
package cn.felord.payment.wechat.enumeration;
import org.springframework.http.HttpMethod;
/**
* The Wechat Pay V3 type.
*
* @author Dax
* @since 14 :25
*/
public enum WechatPayV3Type {
/**
* 获取证书.
*/
CERT(HttpMethod.GET, "%s/v3/certificates"),
/**
* 微信公众号支付或者小程序支付.
*/
JSAPI(HttpMethod.POST, "%s/v3/pay/transactions/jsapi"),
/**
* 微信扫码支付.
*/
NATIVE(HttpMethod.POST, "%s/v3/pay/transactions/native"),
/**
* 微信APP支付.
*/
APP(HttpMethod.POST, "%s/v3/pay/transactions/app"),
/**
* H5支付.
*/
MWEB(HttpMethod.POST, "%s/v3/pay/transactions/h5"),
/**
* 创建代金券批次API.
*/
MARKETING_FAVOR_STOCKS_COUPON_STOCKS(HttpMethod.POST,"%s/v3/marketing/favor/coupon-stocks"),
/**
* 激活代金券批次API.
*/
MARKETING_FAVOR_STOCKS_START(HttpMethod.POST,"%s/v3/marketing/favor/stocks/{stock_id}/start"),
/**
* 重启代金券
*/
MARKETING_FAVOR_STOCKS_RESTART(HttpMethod.POST,"%s/v3/marketing/favor/stocks/{stock_id}/restart"),
/**
* 发放代金券API & 根据商户号查用户的券.
*/
MARKETING_FAVOR_USERS_COUPONS(HttpMethod.POST,"%s/v3/marketing/favor/users/{openid}/coupons"),
/**
* 查询代金券可用商户.
*/
MARKETING_FAVOR_STOCKS_MERCHANTS(HttpMethod.GET, "%s/v3/marketing/favor/stocks/{stock_id}/merchants"),
/**
* 条件查询批次列表API.
*/
MARKETING_FAVOR_STOCKS(HttpMethod.GET, "%s/v3/marketing/favor/stocks"),
/**
* 查询批次详情API.
*/
MARKETING_FAVOR_STOCKS_DETAIL(HttpMethod.GET, "%s/v3/marketing/favor/stocks/{stock_id}"),
/**
* 营销图片上传API.
*/
MARKETING_IMAGE_UPLOAD(HttpMethod.POST, "%s/v3/marketing/favor/media/image-upload"),
/**
* 设置核销回调通知API.
*/
MARKETING_FAVOR_CALLBACKS(HttpMethod.POST, "%s/v3/marketing/favor/callbacks");
private final String pattern;
private final HttpMethod method;
WechatPayV3Type(HttpMethod method, String pattern) {
this.method = method;
this.pattern = pattern;
}
/**
* Method string.
*
* @return the string
*/
public HttpMethod method() {
return this.method;
}
/**
* Pattern string.
*
* @return the string
*/
public String pattern() {
return this.pattern;
}
/**
* 默认支付URI.
*
* @param weChatServer the we chat server
* @return the string
*/
public String uri(WeChatServer weChatServer) {
return String.format(this.pattern,weChatServer.domain());
}
}

View File

@@ -0,0 +1,107 @@
package cn.felord.payment.wechat.oauth2;
import cn.felord.payment.PayException;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import lombok.SneakyThrows;
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.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
* OAuth2 获取用户的公众号授权openid.
* 1.需要微信公众号服务号
* 2.需要微信公众号服务号绑定微信开放平台
* 3.需要正确设置授权回调
*
* @author Dax
* @since 11 :39
*/
public class OAuth2AuthorizationRequestRedirectProvider {
private static final String AUTHORIZATION_URI = "https://open.weixin.qq.com/connect/oauth2/authorize";
private static final String TOKEN_URI = "https://api.weixin.qq.com/sns/oauth2/access_token";
private final RestOperations restOperations = new RestTemplate();
private final ObjectMapper objectMapper;
private final String appId;
private final String secret;
/**
* Instantiates a new O auth 2 authorization request redirect provider.
*
* @param appId the app id
* @param secret the secret
*/
public OAuth2AuthorizationRequestRedirectProvider(String appId, String secret) {
this.objectMapper = new ObjectMapper();
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
this.appId = appId;
this.secret = secret;
}
/**
* 拼接微信公众号授权的url,用以触发用户点击跳转微信授权.
*
* @param state the state
* @param redirectUri the redirect uri
* @return uri components
*/
@SneakyThrows
public String redirect(String state, String redirectUri) {
Assert.hasText(redirectUri, "redirectUri is required");
String encode = URLEncoder.encode(redirectUri, StandardCharsets.UTF_8.name());
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("appid", appId);
queryParams.add("redirect_uri", encode);
queryParams.add("response_type", "code");
queryParams.add("scope", "snsapi_base");
queryParams.add("state", state);
return UriComponentsBuilder.fromHttpUrl(AUTHORIZATION_URI).queryParams(queryParams).build().toUriString() + "#wechat_redirect";
}
/**
* 微信服务器授权成功后调用redirectUri的处理逻辑.
*
* @param code the code
* @return the string
*/
@SneakyThrows
public OAuth2Exchange exchange(String code, String state) {
Assert.hasText(code, "wechat pay oauth2 code is required");
Assert.hasText(state, "wechat pay oauth2 state is required");
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("appid", appId);
queryParams.add("secret", secret);
queryParams.add("code", code);
queryParams.add("state", state);
queryParams.add("grant_type", "authorization_code");
URI uri = UriComponentsBuilder.fromHttpUrl(TOKEN_URI)
.queryParams(queryParams)
.build()
.toUri();
RequestEntity<Void> requestEntity = RequestEntity.get(uri).build();
ResponseEntity<String> responseEntity = restOperations.exchange(requestEntity, String.class);
String body = responseEntity.getBody();
if (Objects.nonNull(body)) {
return objectMapper.readValue(body, OAuth2Exchange.class);
}
throw new PayException("Wechat OAuth2 Authorization failed");
}
}

View File

@@ -0,0 +1,17 @@
package cn.felord.payment.wechat.oauth2;
import lombok.Data;
/**
* OAuth2 Exchange
* @author Dax
* @since 13:14
*/
@Data
public class OAuth2Exchange {
private String accessToken;
private String refreshToken;
private Long expiresIn;
private String openid;
private String scope;
}

View File

@@ -0,0 +1,50 @@
package cn.felord.payment.wechat.v3;
import cn.felord.payment.PayException;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import org.springframework.http.RequestEntity;
import java.net.URI;
/**
* @author Dax
* @since 18:23
*/
public abstract class AbstractApi {
private final ObjectMapper mapper;
private final WechatPayClient wechatPayClient;
private final WechatMetaBean wechatMetaBean;
public AbstractApi(WechatPayClient wechatPayClient, WechatMetaBean wechatMetaBean) {
this.mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
this.wechatPayClient = wechatPayClient;
this.wechatMetaBean = wechatMetaBean;
}
public ObjectMapper getMapper() {
return mapper;
}
public WechatPayClient getWechatPayClient() {
return wechatPayClient;
}
public WechatMetaBean getWechatMetaBean() {
return wechatMetaBean;
}
protected RequestEntity<?> postRequestEntity(URI uri, Object params) {
try {
return RequestEntity.post(uri)
.body(mapper.writeValueAsString(params));
} catch (JsonProcessingException e) {
throw new PayException("wechat app pay json failed");
}
}
}

View File

@@ -0,0 +1,310 @@
package cn.felord.payment.wechat.v3;
import org.springframework.http.*;
import org.springframework.http.converter.*;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @see AllEncompassingFormHttpMessageConverter
*/
final class ExtensionFormHttpMessageConverter extends FormHttpMessageConverter {
private static final String BOUNDARY = "boundary";
private static final boolean jaxb2Present;
private static final boolean jackson2Present;
private static final boolean jackson2XmlPresent;
private static final boolean jackson2SmilePresent;
private static final boolean gsonPresent;
private static final boolean jsonbPresent;
private final List<HttpMessageConverter<?>> partConverters = new ArrayList<>();
static {
ClassLoader classLoader = AllEncompassingFormHttpMessageConverter.class.getClassLoader();
jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
}
/**
* Instantiates a new Upload http message converter.
*/
public ExtensionFormHttpMessageConverter() {
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setWriteAcceptCharset(false); // see SPR-7316
this.partConverters.add(new ByteArrayHttpMessageConverter());
this.partConverters.add(stringHttpMessageConverter);
this.partConverters.add(new ResourceHttpMessageConverter());
try {
this.partConverters.add(new SourceHttpMessageConverter<>());
} catch (Error err) {
// Ignore when no TransformerFactory implementation is available
}
if (jaxb2Present && !jackson2XmlPresent) {
this.partConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
if (jackson2Present) {
this.partConverters.add(new MappingJackson2HttpMessageConverter());
} else if (gsonPresent) {
this.partConverters.add(new GsonHttpMessageConverter());
} else if (jsonbPresent) {
this.partConverters.add(new JsonbHttpMessageConverter());
}
if (jackson2XmlPresent) {
this.partConverters.add(new MappingJackson2XmlHttpMessageConverter());
}
if (jackson2SmilePresent) {
this.partConverters.add(new MappingJackson2SmileHttpMessageConverter());
}
this.setPartConverters(this.partConverters);
applyDefaultCharset();
}
/**
* Apply the configured charset as a default to registered part converters.
*/
private void applyDefaultCharset() {
for (HttpMessageConverter<?> candidate : this.partConverters) {
if (candidate instanceof AbstractHttpMessageConverter) {
AbstractHttpMessageConverter<?> converter = (AbstractHttpMessageConverter<?>) candidate;
// Only override default charset if the converter operates with a charset to begin with...
if (converter.getDefaultCharset() != null) {
converter.setDefaultCharset(DEFAULT_CHARSET);
}
}
}
}
@Override
@SuppressWarnings("unchecked")
public void write(MultiValueMap<String, ?> map, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
if (!isMultipart(map, contentType)) {
writeForm((MultiValueMap<String, Object>) map, contentType, outputMessage);
} else {
writeMultipart((MultiValueMap<String, Object>) map, outputMessage);
}
}
private boolean isMultipart(MultiValueMap<String, ?> map, @Nullable MediaType contentType) {
if (contentType != null) {
return MediaType.MULTIPART_FORM_DATA.includes(contentType);
}
for (List<?> values : map.values()) {
for (Object value : values) {
if (value != null && !(value instanceof String)) {
return true;
}
}
}
return false;
}
private void writeForm(MultiValueMap<String, Object> formData, @Nullable MediaType contentType,
HttpOutputMessage outputMessage) throws IOException {
contentType = getMediaType(contentType);
outputMessage.getHeaders().setContentType(contentType);
Charset charset = contentType.getCharset();
Assert.notNull(charset, "No charset"); // should never occur
final byte[] bytes = serializeForm(formData, charset).getBytes(charset);
outputMessage.getHeaders().setContentLength(bytes.length);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(bytes, outputStream));
} else {
StreamUtils.copy(bytes, outputMessage.getBody());
}
}
private MediaType getMediaType(@Nullable MediaType mediaType) {
if (mediaType == null) {
return new MediaType(MediaType.APPLICATION_FORM_URLENCODED, DEFAULT_CHARSET);
} else if (mediaType.getCharset() == null) {
return new MediaType(mediaType, DEFAULT_CHARSET);
} else {
return mediaType;
}
}
private void writeMultipart(final MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage)
throws IOException {
Map<String, String> parameters = new LinkedHashMap<>(1);
parameters.put(BOUNDARY, BOUNDARY);
MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters);
HttpHeaders headers = outputMessage.getHeaders();
headers.setContentType(contentType);
byte[] boundaryBytes = BOUNDARY.getBytes();
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(outputStream -> {
writeParts(outputStream, parts, boundaryBytes);
writeEnd(outputStream, boundaryBytes);
});
} else {
writeParts(outputMessage.getBody(), parts, boundaryBytes);
writeEnd(outputMessage.getBody(), boundaryBytes);
}
}
private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
String name = entry.getKey();
for (Object part : entry.getValue()) {
if (part != null) {
writeBoundary(os, boundary);
writePart(name, getHttpEntity(part), os);
writeNewLine(os);
}
}
}
}
@SuppressWarnings("unchecked")
private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
Object partBody = partEntity.getBody();
if (partBody == null) {
throw new IllegalStateException("Empty body for part '" + name + "': " + partEntity);
}
Class<?> partType = partBody.getClass();
HttpHeaders partHeaders = partEntity.getHeaders();
MediaType partContentType = partHeaders.getContentType();
for (HttpMessageConverter<?> messageConverter : this.partConverters) {
if (messageConverter.canWrite(partType, partContentType)) {
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os, DEFAULT_CHARSET);
multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
if (!partHeaders.isEmpty()) {
multipartMessage.getHeaders().putAll(partHeaders);
}
((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartMessage);
return;
}
}
throw new HttpMessageNotWritableException("Could not write request: no suitable HttpMessageConverter " +
"found for request type [" + partType.getName() + "]");
}
private void writeBoundary(OutputStream os, byte[] boundary) throws IOException {
os.write('-');
os.write('-');
os.write(boundary);
writeNewLine(os);
}
private static void writeEnd(OutputStream os, byte[] boundary) throws IOException {
os.write('-');
os.write('-');
os.write(boundary);
os.write('-');
os.write('-');
writeNewLine(os);
}
private static void writeNewLine(OutputStream os) throws IOException {
os.write('\r');
os.write('\n');
}
private static class MultipartHttpOutputMessage implements HttpOutputMessage {
private final OutputStream outputStream;
private final Charset charset;
private final HttpHeaders headers = new HttpHeaders();
private boolean headersWritten = false;
/**
* Instantiates a new Multipart http output message.
*
* @param outputStream the output stream
* @param charset the charset
*/
public MultipartHttpOutputMessage(OutputStream outputStream, Charset charset) {
this.outputStream = outputStream;
this.charset = charset;
}
@Override
public HttpHeaders getHeaders() {
return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
}
@Override
public OutputStream getBody() throws IOException {
writeHeaders();
return this.outputStream;
}
private void writeHeaders() throws IOException {
if (!this.headersWritten) {
for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
byte[] headerName = getBytes(entry.getKey());
for (String headerValueString : entry.getValue()) {
byte[] headerValue = getBytes(headerValueString);
this.outputStream.write(headerName);
this.outputStream.write(':');
this.outputStream.write(' ');
this.outputStream.write(headerValue);
writeNewLine(this.outputStream);
}
}
writeNewLine(this.outputStream);
this.headersWritten = true;
}
}
private byte[] getBytes(String name) {
return name.getBytes(this.charset);
}
}
}

View File

@@ -0,0 +1,58 @@
package cn.felord.payment.wechat.v3;
import cn.felord.payment.PayException;
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 PayException("Cannot load keys from store: " + resource, e);
}
}
}

View File

@@ -0,0 +1,239 @@
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.ResponseSignVerifyParams;
import cn.felord.payment.PayException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.SneakyThrows;
import org.springframework.http.*;
import org.springframework.util.AlternativeJdkIdGenerator;
import org.springframework.util.Base64Utils;
import org.springframework.util.IdGenerator;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
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.Collections;
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 {
private static final IdGenerator ID_GENERATOR = new AlternativeJdkIdGenerator();
private static final String SCHEMA = "WECHATPAY2-SHA256-RSA2048 ";
private final RestOperations restOperations = new RestTemplate();
/**
* 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 params the params
* @return the boolean
*/
@SneakyThrows
public boolean responseSignVerify(ResponseSignVerifyParams params) {
String wechatpaySerial = params.getWechatpaySerial();
if (CERTIFICATE_MAP.isEmpty() || !CERTIFICATE_MAP.containsKey(wechatpaySerial)) {
refreshCertificate();
}
Certificate certificate = CERTIFICATE_MAP.get(wechatpaySerial);
final String signatureStr = createSign(params.getWechatpayTimestamp(), params.getWechatpayNonce(), params.getBody());
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initVerify(certificate);
signer.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return signer.verify(Base64Utils.decodeFromString(params.getWechatpaySignature()));
}
/**
* 当我方服务器不存在平台证书或者证书同当前响应报文中的证书序列号不一致时应当刷新 调用/v3/certificates
*/
@SneakyThrows
private synchronized void refreshCertificate() {
String url = WechatPayV3Type.CERT.uri(WeChatServer.CHINA);
UriComponents uri = UriComponentsBuilder.fromHttpUrl(url).build();
String canonicalUrl = uri.getPath();
String encodedQuery = uri.getQuery();
if (encodedQuery != null) {
canonicalUrl += "?" + encodedQuery;
}
// 签名
HttpMethod httpMethod = WechatPayV3Type.CERT.method();
String authorization = requestSign(httpMethod.name(), canonicalUrl, "");
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add("Authorization", authorization);
headers.add("User-Agent", "X-Pay-Service");
RequestEntity<?> requestEntity = new RequestEntity<>(headers, httpMethod, uri.toUri());
ResponseEntity<ObjectNode> responseEntity = restOperations.exchange(requestEntity, ObjectNode.class);
ObjectNode bodyObjectNode = responseEntity.getBody();
if (Objects.isNull(bodyObjectNode)) {
throw new PayException("cant obtain the response body");
}
ArrayNode certificates = bodyObjectNode.withArray("data");
if (certificates.isArray() && certificates.size() > 0) {
CERTIFICATE_MAP.clear();
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.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 PayException(e);
}
return new String(bytes, StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException |
InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new PayException(e);
}
}
/**
* Gets wechat meta bean.
*
* @return the wechat meta bean
*/
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,308 @@
package cn.felord.payment.wechat.v3;
import cn.felord.payment.wechat.WechatPayProperties;
import cn.felord.payment.wechat.enumeration.StockStatus;
import cn.felord.payment.wechat.enumeration.WeChatServer;
import cn.felord.payment.wechat.enumeration.WechatPayV3Type;
import cn.felord.payment.wechat.v3.model.StocksCreateParams;
import cn.felord.payment.wechat.v3.model.StocksQueryParams;
import cn.felord.payment.wechat.v3.model.StocksSendParams;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.SneakyThrows;
import org.bouncycastle.jcajce.provider.digest.SHA256;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
/**
* The type Wechat marketing api.
*
* @author Dax
* @since 18 :22
*/
public class WechatMarketingApi extends AbstractApi {
/**
* Instantiates a new Wechat marketing api.
*
* @param wechatPayClient the wechat pay client
* @param wechatMetaBean the wechat meta bean
*/
public WechatMarketingApi(WechatPayClient wechatPayClient, WechatMetaBean wechatMetaBean) {
super(wechatPayClient, wechatMetaBean);
}
/**
* 创建代金券批次API.
*
* @param params the params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> createStock(StocksCreateParams params) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.getWechatPayClient().withType(WechatPayV3Type.MARKETING_FAVOR_STOCKS_COUPON_STOCKS, params)
.function(this::createStocksFunction)
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
private RequestEntity<?> createStocksFunction(WechatPayV3Type type, StocksCreateParams params) {
WechatPayProperties.V3 v3 = this.getWechatMetaBean().getWechatPayProperties().getV3();
String mchId = v3.getMchId();
String httpUrl = type.uri(WeChatServer.CHINA);
URI uri = UriComponentsBuilder.fromHttpUrl(httpUrl).build().toUri();
params.setBelongMerchant(mchId);
return postRequestEntity(uri, params);
}
/**
* 激活代金券批次API
*
* @param stockId the stock id
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> startStock(String stockId) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.getWechatPayClient().withType(WechatPayV3Type.MARKETING_FAVOR_STOCKS_START, stockId)
.function(this::startAndRestartStockFunction)
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 重启代金券批次API.
*
* @param stockId the stock id
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> restartStock(String stockId) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.getWechatPayClient().withType(WechatPayV3Type.MARKETING_FAVOR_STOCKS_RESTART, stockId)
.function(this::startAndRestartStockFunction)
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
private RequestEntity<?> startAndRestartStockFunction(WechatPayV3Type type, String stockId) {
WechatPayProperties.V3 v3 = this.getWechatMetaBean().getWechatPayProperties().getV3();
String mchId = v3.getMchId();
Map<String, String> body = new HashMap<>();
body.put("stock_creator_mchid", mchId);
String httpUrl = type.uri(WeChatServer.CHINA);
URI uri = UriComponentsBuilder.fromHttpUrl(httpUrl).build().expand(stockId).toUri();
return postRequestEntity(uri, body);
}
/**
* 查询批次详情API.
*
* @param stockId the stock id
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> queryStockDetail(String stockId) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.getWechatPayClient().withType(WechatPayV3Type.MARKETING_FAVOR_STOCKS_DETAIL, stockId)
.function(this::stockDetailFunction)
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
private RequestEntity<?> stockDetailFunction(WechatPayV3Type type, String stockId) {
WechatPayProperties.V3 v3 = this.getWechatMetaBean().getWechatPayProperties().getV3();
String httpUrl = type.uri(WeChatServer.CHINA);
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("stock_creator_mchid", v3.getMchId());
URI uri = UriComponentsBuilder.fromHttpUrl(httpUrl).queryParams(queryParams).build().expand(stockId).toUri();
return RequestEntity.get(uri).build();
}
/**
* 查询该代金券可用的商户
*
* @param params the params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> queryMerchantsByStockId(StocksQueryParams params) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.getWechatPayClient().withType(WechatPayV3Type.MARKETING_FAVOR_STOCKS_MERCHANTS, params)
.function(this::queryStocksFunction)
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 分页查询商户下的代金券批次.
*
* @param params the params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> queryStocksByMch(StocksQueryParams params) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.getWechatPayClient().withType(WechatPayV3Type.MARKETING_FAVOR_STOCKS, params)
.function(this::queryStocksFunction)
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
private RequestEntity<?> queryStocksFunction(WechatPayV3Type type, StocksQueryParams params) {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("offset", String.valueOf(params.getOffset()));
queryParams.add("limit", String.valueOf(params.getLimit()));
WechatPayProperties.V3 v3 = this.getWechatMetaBean().getWechatPayProperties().getV3();
queryParams.add("stock_creator_mchid", v3.getMchId());
LocalDateTime createStartTime = params.getCreateStartTime();
if (Objects.nonNull(createStartTime)) {
//rfc 3339 YYYY-MM-DDTHH:mm:ss.sss+TIMEZONE
queryParams.add("create_start_time", createStartTime.atOffset(ZoneOffset.ofHours(8))
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
}
LocalDateTime createEndTime = params.getCreateEndTime();
if (Objects.nonNull(createEndTime)) {
//rfc 3339 YYYY-MM-DDTHH:mm:ss.sss+TIMEZONE
queryParams.add("create_end_time", createEndTime.atOffset(ZoneOffset.ofHours(8))
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
}
StockStatus status = params.getStatus();
if (Objects.nonNull(status)) {
queryParams.add("status", status.value());
}
String stockId = params.getStockId();
UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.queryParams(queryParams)
.build();
if (StringUtils.hasText(stockId)) {
uriComponents = uriComponents.expand(stockId);
}
URI uri = uriComponents
.toUri();
return RequestEntity.get(uri).build();
}
/**
* 发放代金券API.
*
* @param params the params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> sendStock(StocksSendParams params) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.getWechatPayClient().withType(WechatPayV3Type.MARKETING_FAVOR_USERS_COUPONS, params)
.function(this::sendStocksFunction)
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
private RequestEntity<?> sendStocksFunction(WechatPayV3Type type, StocksSendParams params) {
WechatPayProperties.V3 v3 = this.getWechatMetaBean().getWechatPayProperties().getV3();
// 服务号
params.setAppid(v3.getMp().getAppId());
params.setStockCreatorMchid(v3.getMchId());
String httpUrl = type.uri(WeChatServer.CHINA);
URI uri = UriComponentsBuilder.fromHttpUrl(httpUrl).build().expand(params.getOpenid()).toUri();
params.setOpenid(null);
return postRequestEntity(uri, params);
}
/**
* 营销图片上传API.
*
* @param file the file
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> marketingImageUpload(MultipartFile file) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.getWechatPayClient().withType(WechatPayV3Type.MARKETING_IMAGE_UPLOAD, file)
.function(this::marketingImageUploadFunction)
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
@SneakyThrows
private RequestEntity<?> marketingImageUploadFunction(WechatPayV3Type type, MultipartFile file) {
Map<String, Object> meta = new LinkedHashMap<>(2);
meta.put("filename", file.getOriginalFilename());
byte[] digest = SHA256.Digest.getInstance("SHA-256").digest(file.getBytes());
meta.put("sha256", Hex.toHexString(digest));
String httpUrl = type.uri(WeChatServer.CHINA);
URI uri = UriComponentsBuilder.fromHttpUrl(httpUrl).build().toUri();
MultiValueMap<Object, Object> body = new LinkedMultiValueMap<>();
body.add("meta", meta);
body.add("file", file.getResource());
// 签名
String metaStr = this.getMapper().writeValueAsString(meta);
return RequestEntity.post(uri)
.header("Content-Type", MediaType.MULTIPART_FORM_DATA_VALUE)
.header("Meta-Info",metaStr)
.body(body);
}
/**
* 代金券核销回调通知.
*
* @param notifyUrl the notify url
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> marketingFavorCallback(String notifyUrl) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.getWechatPayClient().withType(WechatPayV3Type.MARKETING_FAVOR_CALLBACKS, notifyUrl)
.function(this::marketingFavorCallbackFunction)
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
private RequestEntity<?> marketingFavorCallbackFunction(WechatPayV3Type type, String notifyUrl) {
WechatPayProperties.V3 v3 = this.getWechatMetaBean().getWechatPayProperties().getV3();
Map<String, Object> body = new HashMap<>(3);
body.put("mchid", v3.getMchId());
body.put("notify_url", notifyUrl);
body.put("switch", true);
String httpUrl = type.uri(WeChatServer.CHINA);
URI uri = UriComponentsBuilder.fromHttpUrl(httpUrl).build().toUri();
return postRequestEntity(uri, body);
}
}

View File

@@ -0,0 +1,27 @@
package cn.felord.payment.wechat.v3;
import cn.felord.payment.wechat.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 is required");
Assert.notNull(keyPair, "wechat pay p12 certificate is required");
Assert.hasText(serialNumber, "wechat pay p12 certificate SerialNumber is required");
}
}

View File

@@ -0,0 +1,49 @@
package cn.felord.payment.wechat.v3;
import cn.felord.payment.wechat.enumeration.WeChatServer;
import cn.felord.payment.wechat.WechatPayProperties;
import cn.felord.payment.wechat.enumeration.WechatPayV3Type;
import cn.felord.payment.wechat.v3.model.AppPayParams;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.http.RequestEntity;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
/**
* The type Wechat pay api.
*
* @author Dax
* @since 16 :15
*/
public class WechatPayApi extends AbstractApi {
public WechatPayApi(WechatPayClient wechatPayClient, WechatMetaBean wechatMetaBean) {
super(wechatPayClient, wechatMetaBean);
}
/**
* APP下单API.
*
* @param payParams the pay params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> appPay(AppPayParams payParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.getWechatPayClient().withType(WechatPayV3Type.APP, payParams)
.function(this::appPayFunction)
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
private RequestEntity<?> appPayFunction(WechatPayV3Type type, AppPayParams payParams) {
WechatPayProperties.V3 v3 = this.getWechatMetaBean().getWechatPayProperties().getV3();
payParams.setAppid(v3.getAppId());
payParams.setMchid(v3.getMchId());
String httpUrl = type.uri(WeChatServer.CHINA);
URI uri = UriComponentsBuilder.fromHttpUrl(httpUrl).build().toUri();
return postRequestEntity(uri, payParams);
}
}

View File

@@ -0,0 +1,60 @@
package cn.felord.payment.wechat.v3;
import cn.felord.payment.wechat.v3.model.CouponConsumeData;
import cn.felord.payment.wechat.v3.model.ResponseSignVerifyParams;
import cn.felord.payment.PayException;
import cn.felord.payment.wechat.v3.model.CallbackParams;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
/**
* @author Dax
* @since 10:21
*/
@Slf4j
public class WechatPayCallback {
private static final ObjectMapper MAPPER = new ObjectMapper();
private final SignatureProvider signatureProvider;
static {
MAPPER.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
public WechatPayCallback(SignatureProvider signatureProvider) {
this.signatureProvider = signatureProvider;
}
@SneakyThrows
public Map<String, ?> wechatPayCouponCallback(ResponseSignVerifyParams params, Consumer<CouponConsumeData> couponConsumeDataConsumer) {
if (signatureProvider.responseSignVerify(params)) {
CallbackParams callbackParams = MAPPER.readValue(params.getBody(), CallbackParams.class);
CallbackParams.Resource resource = callbackParams.getResource();
String associatedData = resource.getAssociatedData();
String nonce = resource.getNonce();
String ciphertext = resource.getCiphertext();
String data = signatureProvider.decryptResponseBody(associatedData, nonce, ciphertext);
Assert.hasText(data, "decryptData is required");
CouponConsumeData couponConsumeData = MAPPER.readValue(data, CouponConsumeData.class);
couponConsumeDataConsumer.accept(couponConsumeData);
Map<String, Object> responseBody = new HashMap<>();
responseBody.put("code", 200);
responseBody.put("message", "核销成功");
return responseBody;
}
throw new PayException("invalid wechat pay coupon callback");
}
}

View File

@@ -0,0 +1,225 @@
package cn.felord.payment.wechat.v3;
import cn.felord.payment.PayException;
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 lombok.SneakyThrows;
import org.springframework.http.*;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Consumer;
/**
* The type Wechat pay client.
*
* @author Dax
* @since 11 :43
*/
public class WechatPayClient {
private final SignatureProvider signatureProvider;
private RestOperations restOperations;
/**
* Instantiates a new Wechat pay service.
*
* @param signatureProvider the signature provider
*/
public WechatPayClient(SignatureProvider signatureProvider) {
this.signatureProvider = signatureProvider;
applyDefaultRestTemplate();
}
/**
* 构造 {@link WechatRequestEntity}.
*
* @param <M> the type parameter
* @param wechatPayV3Type the v 3 pay type
* @param m the m
* @return the executor
*/
public <M> Executor<M> withType(WechatPayV3Type wechatPayV3Type, M m) {
return new Executor<>(wechatPayV3Type, m, this.signatureProvider,this.restOperations);
}
/**
* The type Executor.
*
* @param <M> the type parameter
*/
public static class Executor<M> {
/**
* The V3 pay type.
*/
private final WechatPayV3Type wechatPayV3Type;
private final RestOperations restOperations;
private final SignatureProvider signatureProvider;
private final M model;
/**
* The Request entity bi function.
*/
private BiFunction<WechatPayV3Type, M, RequestEntity<?>> requestEntityBiFunction;
/**
* The Response body consumer.
*/
private Consumer<ResponseEntity<ObjectNode>> responseBodyConsumer;
/**
* Instantiates a new Executor.
*
* @param wechatPayV3Type the v 3 pay type
* @param model the model
* @param signatureProvider the signature provider
*/
Executor(WechatPayV3Type wechatPayV3Type,
M model,
SignatureProvider signatureProvider, RestOperations restOperations) {
this.wechatPayV3Type = wechatPayV3Type;
this.model = model;
this.signatureProvider = signatureProvider;
this.restOperations = restOperations;
}
/**
* Function executor.
*
* @param requestEntityBiFunction the request entity bifunction
* @return the executor
*/
public Executor<M> function(BiFunction<WechatPayV3Type, M, RequestEntity<?>> requestEntityBiFunction) {
this.requestEntityBiFunction = requestEntityBiFunction;
return this;
}
/**
* Consumer executor.
*
* @param responseBodyConsumer the response body consumer
* @return the executor
*/
public Executor<M> consumer(Consumer<ResponseEntity<ObjectNode>> responseBodyConsumer) {
this.responseBodyConsumer = responseBodyConsumer;
return this;
}
/**
* Request.
*/
@SneakyThrows
public void request() {
RequestEntity<?> requestEntity = this.requestEntityBiFunction.apply(this.wechatPayV3Type, this.model);
WechatRequestEntity<?> wechatRequestEntity = WechatRequestEntity.of(requestEntity, this.responseBodyConsumer);
this.doExecute(this.header(wechatRequestEntity));
}
/**
* 构造私钥签名.
*
* @param <T> the type parameter
* @param requestEntity the request entity
*/
private <T> WechatRequestEntity<T> header(WechatRequestEntity<T> requestEntity) {
UriComponents uri = UriComponentsBuilder.fromUri(requestEntity.getUrl()).build();
String canonicalUrl = uri.getPath();
String encodedQuery = uri.getQuery();
Assert.notNull(canonicalUrl,"canonicalUrl is required");
if (encodedQuery != null) {
canonicalUrl += "?" + encodedQuery;
}
// 签名
HttpMethod httpMethod = requestEntity.getMethod();
Assert.notNull(httpMethod, "httpMethod is required");
T entityBody = requestEntity.getBody();
String body = requestEntity.hasBody() ? Objects.requireNonNull(entityBody).toString() : "";
if (WechatPayV3Type.MARKETING_IMAGE_UPLOAD.pattern().contains(canonicalUrl)) {
body = Objects.requireNonNull(requestEntity.getHeaders().get("Meta-Info")).get(0);
}
String authorization = signatureProvider.requestSign(httpMethod.name(), canonicalUrl, body);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.addAll(requestEntity.getHeaders());
httpHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
// 兼容图片上传,自定义优先级最高
if (Objects.isNull(httpHeaders.getContentType())) {
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
}
httpHeaders.add("Authorization", authorization);
httpHeaders.add("User-Agent", "X-Pay-Service");
return requestEntity.headers(httpHeaders);
}
private <T> void doExecute(WechatRequestEntity<T> requestEntity) {
ResponseEntity<ObjectNode> responseEntity = restOperations.exchange(requestEntity, ObjectNode.class);
HttpHeaders headers = responseEntity.getHeaders();
ObjectNode body = responseEntity.getBody();
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
throw new PayException("wechat pay server error,result : " + body);
}
if (Objects.isNull(body)) {
throw new PayException("cant obtain wechat response body");
}
ResponseSignVerifyParams params = new ResponseSignVerifyParams();
// 微信请求回调id
// String RequestId = response.header("Request-ID");
// 微信平台证书序列号 用来取微信平台证书
params.setWechatpaySerial(headers.getFirst("Wechatpay-Serial"));
//获取应答签名
params.setWechatpaySignature(headers.getFirst("Wechatpay-Signature"));
//构造验签名串
params.setWechatpayTimestamp(headers.getFirst("Wechatpay-Timestamp"));
params.setWechatpayNonce(headers.getFirst("Wechatpay-Nonce"));
params.setBody(body.toString());
// 验证微信服务器签名
if (signatureProvider.responseSignVerify(params)) {
Consumer<ResponseEntity<ObjectNode>> responseConsumer = requestEntity.getResponseBodyConsumer();
if (Objects.nonNull(responseConsumer)) {
// 验证通过消费
responseConsumer.accept(responseEntity);
}
} else {
throw new PayException("wechat pay signature failed");
}
}
}
private void applyDefaultRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
DefaultResponseErrorHandler errorHandler = new WechatPayResponseErrorHandler();
restTemplate.setErrorHandler(errorHandler);
List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
messageConverters.removeIf(httpMessageConverter -> httpMessageConverter instanceof AllEncompassingFormHttpMessageConverter);
messageConverters.add(new ExtensionFormHttpMessageConverter());
restTemplate.setMessageConverters(messageConverters);
this.restOperations = restTemplate;
}
}

View File

@@ -0,0 +1,73 @@
package cn.felord.payment.wechat.v3;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.function.Consumer;
/**
* The type Wechat request entity.
*
* @param <T> the type parameter
* @author Dax
* @since 14 :01
*/
@Getter
public class WechatRequestEntity<T> extends RequestEntity<T> {
private final Consumer<ResponseEntity<ObjectNode>> responseBodyConsumer;
/**
* Instantiates a new Wechat request entity.
*
* @param body the body
* @param headers the headers
* @param method the method
* @param url the url
* @param type the type
* @param responseBodyConsumer the response body consumer
*/
public WechatRequestEntity(T body, MultiValueMap<String, String> headers, HttpMethod method, URI url, Type type, Consumer<ResponseEntity<ObjectNode>> responseBodyConsumer) {
super(body, headers, method, url, type);
this.responseBodyConsumer = responseBodyConsumer;
}
/**
* Headers wechat request entity.
*
* @param httpHeaders the http headers
* @return the wechat request entity
*/
public WechatRequestEntity<T> headers(HttpHeaders httpHeaders) {
return new WechatRequestEntity<>(this.getBody(),
httpHeaders,
this.getMethod(),
this.getUrl(),
this.getType(),
this.responseBodyConsumer);
}
/**
* Of wechat request entity.
*
* @param requestEntity the request entity
* @param responseBodyConsumer the response body consumer
* @return the wechat request entity
*/
public static WechatRequestEntity<?> of(RequestEntity<?> requestEntity, Consumer<ResponseEntity<ObjectNode>> responseBodyConsumer) {
return new WechatRequestEntity<>(requestEntity.getBody(),
requestEntity.getHeaders(),
requestEntity.getMethod(),
requestEntity.getUrl(),
requestEntity.getType(),
responseBodyConsumer);
}
}

View File

@@ -0,0 +1,102 @@
package cn.felord.payment.wechat.v3;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.util.Objects;
/**
* The type Wechat response entity.
*
* @param <T> the type parameter
* @author Dax
* @since 13 :20
*/
@Slf4j
@Data
public class WechatResponseEntity<T> {
private int httpStatus;
private T body;
/**
* Convert {@link ResponseEntity} to {@link WechatResponseEntity}.
*
* @param responseEntity the response entity
*/
public void convert(ResponseEntity<T> responseEntity) {
if (log.isDebugEnabled()) {
log.debug("wechat response {}", responseEntity);
}
if (Objects.nonNull(responseEntity)) {
this.httpStatus = responseEntity.getStatusCodeValue();
this.body = responseEntity.getBody();
} else {
this.httpStatus = HttpStatus.REQUEST_TIMEOUT.value();
this.body = null;
}
}
/**
* Is 1 xx informational boolean.
*
* @return the boolean
*/
public boolean is1xxInformational() {
if (log.isDebugEnabled()) {
log.debug("wechat httpStatus {}", this.httpStatus);
}
return HttpStatus.valueOf(this.httpStatus).is1xxInformational();
}
/**
* Is 2xx successful boolean.
*
* @return the boolean
*/
public boolean is2xxSuccessful() {
if (log.isDebugEnabled()) {
log.debug("wechat httpStatus {}", this.httpStatus);
}
return HttpStatus.valueOf(this.httpStatus).is2xxSuccessful();
}
/**
* Is 3xx redirection boolean.
*
* @return the boolean
*/
public boolean is3xxRedirection() {
if (log.isDebugEnabled()) {
log.debug("wechat httpStatus {}", this.httpStatus);
}
return HttpStatus.valueOf(this.httpStatus).is3xxRedirection();
}
/**
* Is 4xx client error boolean.
*
* @return the boolean
*/
public boolean is4xxClientError() {
if (log.isDebugEnabled()) {
log.debug("wechat httpStatus {}", this.httpStatus);
}
return HttpStatus.valueOf(this.httpStatus).is4xxClientError();
}
/**
* Is 5xx server error boolean.
*
* @return the boolean
*/
public boolean is5xxServerError() {
if (log.isDebugEnabled()) {
log.debug("wechat httpStatus {}", this.httpStatus);
}
return HttpStatus.valueOf(this.httpStatus).is5xxServerError();
}
}

View File

@@ -0,0 +1,14 @@
package cn.felord.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,51 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
/**
* The type App pay model.
*
* @author Dax
* @since 17 :10
*/
@Data
public class AppPayParams {
private String appid;
private String mchid;
/**
* 商品描述
* 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;
private Detail detail;
private SceneInfo sceneInfo;
}

View File

@@ -0,0 +1,28 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
/**
* @author Dax
* @since 10:13
*/
@Data
public class CallbackParams {
private String id;
private String createTime;
private String eventType;
private String resourceType;
private String summary;
private Resource resource;
@Data
public static class Resource {
private String algorithm;
private String ciphertext;
private String associatedData;
private String nonce;
private String originalType;
}
}

View File

@@ -0,0 +1,22 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
import java.util.List;
/**
* 已实扣代金券信息
*/
@Data
public class ConsumeInformation {
private String consumeMchid;
private String consumeTime;
private List<GoodsDetail> goodsDetail;
private String transactionId;
}

View File

@@ -0,0 +1,11 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
@Data
public class CouponAvailableTime {
private Long availableTimeAfterReceive;
private FixAvailableTime fixAvailableTime;
private Boolean secondDayAvailable;
}

View File

@@ -0,0 +1,31 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
/**
* 微信代金券核销通知参数
*/
@Data
public class CouponConsumeData {
private String availableBeginTime;
private String availableEndTime;
private ConsumeInformation consumeInformation;
private String couponId;
private String couponName;
private String couponType;
private String createTime;
private String description;
private DiscountTo discountTo;
private boolean noCash;
private NormalCouponInformation normalCouponInformation;
private boolean singleitem;
private SingleitemDiscountOff singleitemDiscountOff;
private String status;
private String stockCreatorMchid;
private String stockId;
}

View File

@@ -0,0 +1,28 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
import java.util.List;
/**
* 核销规则.
*/
@Data
public class CouponUseRule {
private List<String> availableItems;
private List<String> availableMerchants;
private Boolean combineUse;
private CouponAvailableTime couponAvailableTime;
private FixedNormalCoupon fixedNormalCoupon;
private List<String> goodsTag;
private String tradeType;
}

View File

@@ -0,0 +1,25 @@
package cn.felord.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,15 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
/**
* 减至优惠限定字段,仅减至优惠场景有返回
*/
@Data
public class DiscountTo {
private Long cutToPrice;
private Long maxPrice;
}

View File

@@ -0,0 +1,15 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
import java.util.List;
@Data
public class FixAvailableTime {
private List<Long> availableWeekDay;
private Long beginTime;
private Long endTime;
}

View File

@@ -0,0 +1,25 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
/**
* 固定面额满减券使用规则, stock_type为NORMAL时必填
*
* 满transactionMinimum 减少 couponAmount
*
*/
@Data
public class FixedNormalCoupon {
/**
* 面额,单位分
*/
private Long couponAmount;
/**
* 门槛 满M元可用
*/
private Long transactionMinimum;
}

View File

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

View File

@@ -0,0 +1,17 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
/**
* 商户下单接口传的单品信息
*/
@Data
public class GoodsDetail {
private Long discountAmount;
private String goodsId;
private Long price;
private Long quantity;
}

View File

@@ -0,0 +1,31 @@
package cn.felord.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,15 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
/**
* 普通满减券面额、门槛信息
*/
@Data
public class NormalCouponInformation {
private Long couponAmount;
private Long transactionMinimum;
}

View File

@@ -0,0 +1,36 @@
package cn.felord.payment.wechat.v3.model;
import cn.felord.payment.wechat.enumeration.CouponBgColor;
import lombok.Data;
/**
* 优惠券样式
*
* @author Dax
* @since 15:09
*/
@Data
public class PatternInfo {
/**
* 背景色
*/
private CouponBgColor backgroundColor;
/**
* 券详情图片
*/
private String couponImage;
/**
* 使用说明
*/
private String description;
/**
* 商户logo
*/
private String merchantLogo;
/**
* 商户名称
*/
private String merchantName;
}

View File

@@ -0,0 +1,23 @@
package cn.felord.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,35 @@
package cn.felord.payment.wechat.v3.model;
import cn.felord.payment.wechat.v3.SignatureProvider;
import lombok.Data;
/**
* 微信的响应签名校验参数
*
* @author Dax
* @see SignatureProvider#responseSignVerify(ResponseSignVerifyParams)
* @since 16:32
*/
@Data
public class ResponseSignVerifyParams {
/**
* response.headers['Wechatpay-Serial'] 当前使用的微信平台证书序列号
*/
private String wechatpaySerial;
/**
* response.headers['Wechatpay-Signature'] 微信平台签名
*/
private String wechatpaySignature;
/**
* response.headers['Wechatpay-Timestamp'] 微信服务器的时间戳
*/
private String wechatpayTimestamp;
/**
* response.headers['Wechatpay-Nonce'] 微信服务器提供的随机串
*/
private String wechatpayNonce;
/**
* response.body 微信服务器的响应体
*/
private String body;
}

View File

@@ -0,0 +1,27 @@
package cn.felord.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,19 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
/**
* @author Dax
* @since 17:05
*/
@Data
public class SettleInfo {
/**
* 是否指定分账
*/
private boolean profitSharing;
/**
* 补差金额
*/
private int subsidyAmount;
}

View File

@@ -0,0 +1,14 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
/**
* 单品优惠特定信息
*/
@Data
public class SingleitemDiscountOff {
private Long singlePriceMax;
}

View File

@@ -0,0 +1,39 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
/**
* 批次使用规则
*
* @author Dax
* @since 15:08
*/
@Data
public class StockUseRule {
/**
* 总消耗金额,单位:分。
* max_amount需要等于coupon_amount面额 * max_coupons发放总上限
*/
private Long maxAmount;
/**
* 单天最高消耗金额,单位:分
*/
private Long maxAmountByDay;
/**
* 最大发券数
*/
private Long maxCoupons;
/**
* 单个用户可领个数每个用户最多60张券
*/
private Long maxCouponsPerUser;
/**
* 是否开启自然人限制
*/
private Boolean naturalPersonLimit;
/**
* api发券防刷
*/
private Boolean preventApiAbuse;
}

View File

@@ -0,0 +1,62 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
/**
* 创建优惠券批次参数.
*
* @author Dax
* @since 14 :19
*/
@Data
public class StocksCreateParams {
/**
* 批次名称
*/
private String stockName;
/**
* 仅配置商户可见,用于自定义信息
*/
private String comment;
/**
* 批次归属商户号
*/
private String belongMerchant;
/**
* 批次开始时间 rfc 3339 YYYY-MM-DDTHH:mm:ss.sss+TIMEZONE
*/
private String availableBeginTime;
/**
* 批次结束时间 rfc 3339 YYYY-MM-DDTHH:mm:ss.sss+TIMEZONE
*/
private String availableEndTime;
/**
* 是否无资金流
*/
private Boolean noCash;
/**
* 批次类型
*/
private String stockType = "NORMAL";
/**
* 商户单据号
*/
private String outRequestNo;
/**
* 扩展属性
*/
private String ext_info;
/**
* 批次使用规则
*/
private StockUseRule stockUseRule;
/**
* 核销规则
*/
private CouponUseRule couponUseRule;
/**
* 代金券样式
*/
private PatternInfo patternInfo;
}

View File

@@ -0,0 +1,21 @@
package cn.felord.payment.wechat.v3.model;
import cn.felord.payment.wechat.enumeration.StockStatus;
import lombok.Data;
import java.time.LocalDateTime;
/**
* @author Dax
* @since 15:16
*/
@Data
public class StocksQueryParams {
private int offset =0;
private int limit = 10;
private String stockId;
private LocalDateTime createStartTime;
private LocalDateTime createEndTime;
private StockStatus status;
}

View File

@@ -0,0 +1,35 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
@Data
public class StocksSendParams {
/**
* 批次号 必须为代金券(全场券或单品券)批次号,不支持立减与折扣。
*/
private String stockId;
/**
* 用户openid 该openid需要与接口传入中的appid有对应关系。
*/
private String openid;
/**
* 商户单据号
*/
private String outRequestNo;
/**
* 公众账号ID
*/
private String appid;
/**
* 创建批次的商户号
*/
private String stockCreatorMchid;
/**
* 指定面额发券,面额
*/
private String couponValue;
/**
* 指定面额发券,券门槛
*/
private String couponMinimum;
}

View File

@@ -0,0 +1,27 @@
package cn.felord.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;
}