This commit is contained in:
felord.cn
2020-12-13 00:17:56 +08:00
parent 82f2986508
commit eab68846af
199 changed files with 48868 additions and 57 deletions

View File

@@ -55,11 +55,6 @@
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>

View File

@@ -1,11 +1,12 @@
package cn.felord.payment.wechat.v3;
import cn.felord.payment.PayException;
import cn.felord.payment.wechat.WechatPayProperties;
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 com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.http.RequestEntity;
import org.springframework.util.Assert;
@@ -31,17 +32,23 @@ public abstract class AbstractApi {
*/
public AbstractApi(WechatPayClient wechatPayClient, String tenantId) {
this.mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
applyObjectMapper(this.mapper);
this.wechatPayClient = wechatPayClient;
Assert.hasText(tenantId, "tenantId is required");
if (!container().getTenantIds().contains(tenantId)) {
throw new PayException("tenantId is not in wechatMetaContainer");
}
this.tenantId = tenantId;
}
private void applyObjectMapper(ObjectMapper mapper) {
mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
SimpleModule module = new JavaTimeModule();
mapper.registerModule(module);
}
/**
* Gets mapper.
*

View File

@@ -25,24 +25,50 @@ import java.util.List;
import java.util.Map;
/**
* 用于微信支付处理上传的自定义消息转换器.
*
* @see AllEncompassingFormHttpMessageConverter
*/
final class ExtensionFormHttpMessageConverter extends FormHttpMessageConverter {
/**
* The constant BOUNDARY.
*/
private static final String BOUNDARY = "boundary";
/**
* The constant jaxb2Present.
*/
private static final boolean jaxb2Present;
/**
* The constant jackson2Present.
*/
private static final boolean jackson2Present;
/**
* The constant jackson2XmlPresent.
*/
private static final boolean jackson2XmlPresent;
/**
* The constant jackson2SmilePresent.
*/
private static final boolean jackson2SmilePresent;
/**
* The constant gsonPresent.
*/
private static final boolean gsonPresent;
/**
* The constant jsonbPresent.
*/
private static final boolean jsonbPresent;
/**
* The Part converters.
*/
private final List<HttpMessageConverter<?>> partConverters = new ArrayList<>();
static {
@@ -123,6 +149,13 @@ final class ExtensionFormHttpMessageConverter extends FormHttpMessageConverter {
}
}
/**
* Is multipart boolean.
*
* @param map the map
* @param contentType the content type
* @return the boolean
*/
private boolean isMultipart(MultiValueMap<String, ?> map, @Nullable MediaType contentType) {
if (contentType != null) {
return MediaType.MULTIPART_FORM_DATA.includes(contentType);
@@ -137,6 +170,14 @@ final class ExtensionFormHttpMessageConverter extends FormHttpMessageConverter {
return false;
}
/**
* Write form.
*
* @param formData the form data
* @param contentType the content type
* @param outputMessage the output message
* @throws IOException the io exception
*/
private void writeForm(MultiValueMap<String, Object> formData, @Nullable MediaType contentType,
HttpOutputMessage outputMessage) throws IOException {
@@ -157,6 +198,12 @@ final class ExtensionFormHttpMessageConverter extends FormHttpMessageConverter {
}
}
/**
* Gets media type.
*
* @param mediaType the media type
* @return the media type
*/
private MediaType getMediaType(@Nullable MediaType mediaType) {
if (mediaType == null) {
return new MediaType(MediaType.APPLICATION_FORM_URLENCODED, DEFAULT_CHARSET);
@@ -167,6 +214,13 @@ final class ExtensionFormHttpMessageConverter extends FormHttpMessageConverter {
}
}
/**
* Write multipart.
*
* @param parts the parts
* @param outputMessage the output message
* @throws IOException the io exception
*/
private void writeMultipart(final MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage)
throws IOException {
@@ -192,6 +246,14 @@ final class ExtensionFormHttpMessageConverter extends FormHttpMessageConverter {
}
}
/**
* Write parts.
*
* @param os the os
* @param parts the parts
* @param boundary the boundary
* @throws IOException the io exception
*/
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();
@@ -205,6 +267,14 @@ final class ExtensionFormHttpMessageConverter extends FormHttpMessageConverter {
}
}
/**
* Write part.
*
* @param name the name
* @param partEntity the part entity
* @param os the os
* @throws IOException the io exception
*/
@SuppressWarnings("unchecked")
private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
Object partBody = partEntity.getBody();
@@ -231,6 +301,13 @@ final class ExtensionFormHttpMessageConverter extends FormHttpMessageConverter {
}
/**
* Write boundary.
*
* @param os the os
* @param boundary the boundary
* @throws IOException the io exception
*/
private void writeBoundary(OutputStream os, byte[] boundary) throws IOException {
os.write('-');
os.write('-');
@@ -238,6 +315,13 @@ final class ExtensionFormHttpMessageConverter extends FormHttpMessageConverter {
writeNewLine(os);
}
/**
* Write end.
*
* @param os the os
* @param boundary the boundary
* @throws IOException the io exception
*/
private static void writeEnd(OutputStream os, byte[] boundary) throws IOException {
os.write('-');
os.write('-');
@@ -247,19 +331,40 @@ final class ExtensionFormHttpMessageConverter extends FormHttpMessageConverter {
writeNewLine(os);
}
/**
* Write new line.
*
* @param os the os
* @throws IOException the io exception
*/
private static void writeNewLine(OutputStream os) throws IOException {
os.write('\r');
os.write('\n');
}
/**
* The type Multipart http output message.
*/
private static class MultipartHttpOutputMessage implements HttpOutputMessage {
/**
* The Output stream.
*/
private final OutputStream outputStream;
/**
* The Charset.
*/
private final Charset charset;
/**
* The Headers.
*/
private final HttpHeaders headers = new HttpHeaders();
/**
* The Headers written.
*/
private boolean headersWritten = false;
/**
@@ -284,6 +389,11 @@ final class ExtensionFormHttpMessageConverter extends FormHttpMessageConverter {
return this.outputStream;
}
/**
* Write headers.
*
* @throws IOException the io exception
*/
private void writeHeaders() throws IOException {
if (!this.headersWritten) {
for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
@@ -302,6 +412,12 @@ final class ExtensionFormHttpMessageConverter extends FormHttpMessageConverter {
}
}
/**
* Get bytes byte [ ].
*
* @param name the name
* @return the byte [ ]
*/
private byte[] getBytes(String name) {
return name.getBytes(this.charset);
}

View File

@@ -11,7 +11,7 @@ import java.security.PublicKey;
import java.security.cert.X509Certificate;
/**
* KeyPairFactory
* 证书工具
*
* @author dax
* @since 13:41

View File

@@ -49,7 +49,13 @@ import java.util.stream.Collectors;
*/
public class SignatureProvider {
/**
* The constant ID_GENERATOR.
*/
private static final IdGenerator ID_GENERATOR = new AlternativeJdkIdGenerator();
/**
* The constant SCHEMA.
*/
private static final String SCHEMA = "WECHATPAY2-SHA256-RSA2048 ";
/**
* The constant TOKEN_PATTERN.
@@ -59,7 +65,13 @@ public class SignatureProvider {
* 微信平台证书容器 key = 序列号 value = 证书对象
*/
private static final Map<String, Certificate> CERTIFICATE_MAP = new ConcurrentHashMap<>();
/**
* The Rest operations.
*/
private final RestOperations restOperations = new RestTemplate();
/**
* The Wechat meta container.
*/
private final WechatMetaContainer wechatMetaContainer;
/**
@@ -131,9 +143,11 @@ public class SignatureProvider {
/**
* 当我方服务器不存在平台证书或者证书同当前响应报文中的证书序列号不一致时应当刷新 调用/v3/certificates
*
* @param tenantId tenantId
*/
@SneakyThrows
private synchronized void refreshCertificate(String propertiesKey) {
private synchronized void refreshCertificate(String tenantId) {
String url = WechatPayV3Type.CERT.uri(WeChatServer.CHINA);
UriComponents uri = UriComponentsBuilder.fromHttpUrl(url).build();
@@ -146,7 +160,7 @@ public class SignatureProvider {
}
// 签名
HttpMethod httpMethod = WechatPayV3Type.CERT.method();
String authorization = requestSign(propertiesKey,httpMethod.name(), canonicalUrl, "");
String authorization = requestSign(tenantId,httpMethod.name(), canonicalUrl, "");
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
@@ -169,7 +183,7 @@ public class SignatureProvider {
String associatedData = encryptCertificate.get("associated_data").asText();
String nonce = encryptCertificate.get("nonce").asText();
String ciphertext = encryptCertificate.get("ciphertext").asText();
String publicKey = decryptResponseBody(propertiesKey,associatedData, nonce, ciphertext);
String publicKey = decryptResponseBody(tenantId,associatedData, nonce, ciphertext);
ByteArrayInputStream inputStream = new ByteArrayInputStream(publicKey.getBytes(StandardCharsets.UTF_8));
Certificate certificate = null;

View File

@@ -19,8 +19,7 @@ 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.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.LinkedHashMap;
@@ -181,20 +180,20 @@ public class WechatMarketingFavorApi extends AbstractApi {
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
queryParams.add("stock_creator_mchid", v3.getMchId());
LocalDateTime createStartTime = params.getCreateStartTime();
OffsetDateTime 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))
queryParams.add("create_start_time", createStartTime
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
}
LocalDateTime createEndTime = params.getCreateEndTime();
OffsetDateTime 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))
queryParams.add("create_end_time", createEndTime
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
}
StockStatus status = params.getStatus();
if (Objects.nonNull(status)) {
if (Objects.nonNull(status) && Objects.equals(WechatPayV3Type.MARKETING_FAVOR_STOCKS, type)) {
queryParams.add("status", status.value());
}
@@ -204,12 +203,11 @@ public class WechatMarketingFavorApi extends AbstractApi {
.queryParams(queryParams)
.build();
if (StringUtils.hasText(stockId)) {
if (StringUtils.hasText(stockId) && !Objects.equals(WechatPayV3Type.MARKETING_FAVOR_STOCKS, type)) {
uriComponents = uriComponents.expand(stockId);
}
URI uri = uriComponents
.toUri();
URI uri = uriComponents.toUri();
return Get(uri);
}

View File

@@ -3,27 +3,32 @@ 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;
/**
* 微信支付元数据Bean.
*
* @author Dax
* @since 15:50
* @since 15 :50
*/
@Data
public class WechatMetaBean implements InitializingBean {
public class WechatMetaBean {
/**
* The Key pair.
*/
private KeyPair keyPair;
/**
* The Serial number.
*/
private String serialNumber;
/**
* The Tenant id.
*/
private String tenantId;
/**
* The V3.
*/
private WechatPayProperties.V3 v3;
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(v3, "wechatPayProperties.V3 is required");
Assert.notNull(keyPair, "wechat pay p12 certificate is required");
Assert.hasText(serialNumber, "wechat pay p12 certificate SerialNumber is required");
Assert.hasText(tenantId, "wechat pay tenantId is required");
}
}

View File

@@ -6,14 +6,10 @@ 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 lombok.extern.slf4j.Slf4j;
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.util.MimeType;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
@@ -33,7 +29,13 @@ import java.util.function.Consumer;
* @since 11 :43
*/
public class WechatPayClient {
/**
* The Signature provider.
*/
private final SignatureProvider signatureProvider;
/**
* The Rest operations.
*/
private RestOperations restOperations;
/**
@@ -70,8 +72,17 @@ public class WechatPayClient {
* The V3 pay type.
*/
private final WechatPayV3Type wechatPayV3Type;
/**
* The Rest operations.
*/
private final RestOperations restOperations;
/**
* The Signature provider.
*/
private final SignatureProvider signatureProvider;
/**
* The Model.
*/
private final M model;
/**
@@ -151,6 +162,7 @@ public class WechatPayClient {
*
* @param <T> the type parameter
* @param requestEntity the request entity
* @return the wechat request entity
*/
private <T> WechatRequestEntity<T> header(WechatRequestEntity<T> requestEntity) {
@@ -192,6 +204,12 @@ public class WechatPayClient {
}
/**
* Do execute.
*
* @param <T> the type parameter
* @param requestEntity the request entity
*/
private <T> void doExecute(WechatRequestEntity<T> requestEntity) {
ResponseEntity<ObjectNode> responseEntity = restOperations.exchange(requestEntity, ObjectNode.class);
@@ -229,6 +247,13 @@ public class WechatPayClient {
}
}
/**
* Do download string.
*
* @param <T> the type parameter
* @param requestEntity the request entity
* @return the string
*/
private <T> String doDownload(WechatRequestEntity<T> requestEntity) {
ResponseEntity<String> responseEntity = restOperations.exchange(requestEntity, String.class);
@@ -255,6 +280,9 @@ public class WechatPayClient {
return signatureProvider;
}
/**
* Apply default rest template.
*/
private void applyDefaultRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
DefaultResponseErrorHandler errorHandler = new WechatPayResponseErrorHandler();

View File

@@ -11,7 +11,8 @@ import lombok.Data;
@Data
public class CouponDetailsQueryParams {
/**
* 用户在公众号服务号配置{@link cn.felord.payment.wechat.WechatPayProperties.Mp}下授权得到的openid
* 用户在appid下授权得到的openid
* <p>
* 参考发券
*/
private String openId;

View File

@@ -1,7 +1,10 @@
package cn.felord.payment.wechat.v3.model;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.OffsetDateTime;
/**
* 创建优惠券批次参数.
*
@@ -23,13 +26,15 @@ public class StocksCreateParams {
*/
private String belongMerchant;
/**
* 批次开始时间 rfc 3339 YYYY-MM-DDTHH:mm:ss.sss+TIMEZONE
* 批次开始时间 rfc 3339 yyyy-mm-ddthh:mm:ss.sss+timezone
*/
private String availableBeginTime;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "GMT+8")
private OffsetDateTime availableBeginTime;
/**
* 批次结束时间 rfc 3339 YYYY-MM-DDTHH:mm:ss.sss+TIMEZONE
*/
private String availableEndTime;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "GMT+8")
private OffsetDateTime availableEndTime;
/**
* 是否无资金流
*/

View File

@@ -1,21 +1,63 @@
package cn.felord.payment.wechat.v3.model;
import cn.felord.payment.wechat.enumeration.StockStatus;
import cn.felord.payment.wechat.v3.WechatMarketingFavorApi;
import lombok.Data;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
/**
* 查询参数,适用以下接口:
* <p>
* 条件查询批次列表API、查询代金券可用商户API、查询代金券可用单品API
*
* @author Dax
* @since 15:16
* @since 15 :16
*/
@Data
public class StocksQueryParams {
private Integer offset =0;
/**
* 必填
* <p>
* 条件查询批次列表API 页码从0开始默认第0页传递1可能出错。
* <p>
* 查询代金券可用商户API 分页页码最大1000。
* <p>
* 查询代金券可用单品API 最大500。
*/
private Integer offset = 0;
/**
* 必填
* <p>
* 条件查询批次列表API 分页大小最大10。
* <p>
* 查询代金券可用商户API 最大50。
* <p>
* 查询代金券可用单品API 最大100。
*/
private Integer limit = 10;
/**
* 根据API而定
* <p>
* 批次ID对条件查询批次列表API{@link WechatMarketingFavorApi#queryStocksByMch(StocksQueryParams)}无效。
*/
private String stockId;
private LocalDateTime createStartTime;
private LocalDateTime createEndTime;
/**
* 选填
* <p>
* 起始时间 最终满足格式 {@code yyyy-MM-dd'T'HH:mm:ss.SSSXXX}
*/
private OffsetDateTime createStartTime;
/**
* 选填
* <p>
* 终止时间 最终满足格式 {@code yyyy-MM-dd'T'HH:mm:ss.SSSXXX}
*/
private OffsetDateTime createEndTime;
/**
* 根据API而定
* <p>
* 批次状态只对条件查询批次列表API{@link WechatMarketingFavorApi#queryStocksByMch(StocksQueryParams)}有效。
*/
private StockStatus status;
}