重启批次接口 营销回调接口

This commit is contained in:
xiafang
2020-11-27 09:21:00 +08:00
parent ff6d7a3c3a
commit 9f7e66445b
17 changed files with 367 additions and 58 deletions

View File

@@ -60,14 +60,26 @@ public class WechatPayConfiguration {
* Wechat pay v3 api.
*
* @param wechatPayClient the wechat pay v 3 client
* @param wechatMetaBean the wechat meta bean
* @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);
return new WechatPayApi(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}.
*
@@ -76,7 +88,7 @@ public class WechatPayConfiguration {
*/
@Bean
@ConditionalOnProperty(prefix = "wechat.pay", name = "v3.mp.app-id")
public OAuth2AuthorizationRequestRedirectProvider oAuth2Provider(WechatPayProperties wechatPayProperties){
public OAuth2AuthorizationRequestRedirectProvider oAuth2Provider(WechatPayProperties wechatPayProperties) {
WechatPayProperties.Mp mp = wechatPayProperties.getV3().getMp();
return new OAuth2AuthorizationRequestRedirectProvider(mp.getAppId(), mp.getAppSecret());
}

View File

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

View File

@@ -43,6 +43,10 @@ public enum WechatPayV3Type {
* 激活代金券批次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 & 根据商户号查用户的券.
*/
@@ -62,7 +66,11 @@ public enum WechatPayV3Type {
/**
* 营销图片上传API.
*/
MARKETING_IMAGE_UPLOAD(HttpMethod.POST, "%s/v3/marketing/favor/media/image-upload");
MARKETING_IMAGE_UPLOAD(HttpMethod.POST, "%s/v3/marketing/favor/media/image-upload"),
/**
* 设置核销回调通知API.
*/
MARKETING_FAVOR_CALLBACKS(HttpMethod.POST, "%s/v3/marketing/favor/callbacks");

View File

@@ -4,6 +4,7 @@ package com.enongm.dianji.payment.wechat.oauth2;
import com.enongm.dianji.payment.PayException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.SneakyThrows;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
@@ -11,10 +12,11 @@ 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.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
@@ -48,14 +50,17 @@ public class OAuth2AuthorizationRequestRedirectProvider {
* @param redirectUri the redirect uri
* @return uri components
*/
public UriComponents redirect(String phoneNumber, String redirectUri) {
@SneakyThrows
public String redirect(String phoneNumber, 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", redirectUri);
queryParams.add("redirect_uri", encode);
queryParams.add("response_type", "code");
queryParams.add("scope", "snsapi_base");
queryParams.add("state", phoneNumber);
return UriComponentsBuilder.fromHttpUrl(AUTHORIZATION_URI).queryParams(queryParams).build();
return UriComponentsBuilder.fromHttpUrl(AUTHORIZATION_URI).queryParams(queryParams).build().toUriString() + "#wechat_redirect";
}

View File

@@ -2,8 +2,9 @@ package com.enongm.dianji.payment.wechat.v3;
import com.enongm.dianji.payment.PayException;
import com.enongm.dianji.payment.wechat.enumeration.WechatPayV3Type;
import com.enongm.dianji.payment.wechat.enumeration.WeChatServer;
import com.enongm.dianji.payment.wechat.enumeration.WechatPayV3Type;
import com.enongm.dianji.payment.wechat.v3.model.ResponseSignVerifyParams;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -107,27 +108,24 @@ public class SignatureProvider {
/**
* 我方对响应验签,和应答签名做比较,使用微信平台证书.
*
* @param wechatpaySerial response.headers['Wechatpay-Serial'] 当前使用的微信平台证书序列号
* @param wechatpaySignature response.headers['Wechatpay-Signature'] 微信平台签名
* @param wechatpayTimestamp response.headers['Wechatpay-Timestamp'] 微信服务器的时间戳
* @param wechatpayNonce response.headers['Wechatpay-Nonce'] 微信服务器提供的随机串
* @param body response.body 微信服务器的响应体
* @param params the params
* @return the boolean
*/
@SneakyThrows
public boolean responseSignVerify(String wechatpaySerial, String wechatpaySignature, String wechatpayTimestamp, String wechatpayNonce, String body) {
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(wechatpayTimestamp, wechatpayNonce, body);
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(wechatpaySignature));
return signer.verify(Base64Utils.decodeFromString(params.getWechatpaySignature()));
}
@@ -207,7 +205,6 @@ public class SignatureProvider {
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));
byte[] bytes;
try {
bytes = cipher.doFinal(Base64Utils.decodeFromString(ciphertext));
@@ -221,6 +218,11 @@ public class SignatureProvider {
}
}
/**
* Gets wechat meta bean.
*
* @return the wechat meta bean
*/
public WechatMetaBean getWechatMetaBean() {
return wechatMetaBean;
}

View File

@@ -79,13 +79,7 @@ public class WechatPayApi {
String httpUrl = type.uri(WeChatServer.CHINA);
URI uri = UriComponentsBuilder.fromHttpUrl(httpUrl).build().toUri();
params.setBelongMerchant(mchId);
try {
return RequestEntity.post(uri)
.body(MAPPER.writeValueAsString(params));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
throw new PayException("wechat app pay json failed");
return postRequestEntity(uri, params);
}
/**
@@ -97,27 +91,35 @@ public class WechatPayApi {
public WechatResponseEntity<ObjectNode> startStock(String stockId) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
wechatPayClient.withType(WechatPayV3Type.MARKETING_FAVOR_STOCKS_START, stockId)
.function(this::startStockFunction)
.function(this::startAndRestartStockFunction)
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
private RequestEntity<?> startStockFunction(WechatPayV3Type type, String stockId) {
/**
* 重启代金券批次API.
*
* @param stockId the stock id
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> restartStock(String stockId) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
wechatPayClient.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 = wechatMetaBean.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();
Map<String, String> map = new HashMap<>();
map.put("stock_creator_mchid", mchId);
try {
return RequestEntity.post(uri)
.body(MAPPER.writeValueAsString(map));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
throw new PayException("wechat app pay json failed");
return postRequestEntity(uri, body);
}
/**
@@ -247,13 +249,34 @@ public class WechatPayApi {
String httpUrl = type.uri(WeChatServer.CHINA);
URI uri = UriComponentsBuilder.fromHttpUrl(httpUrl).build().expand(params.getOpenid()).toUri();
params.setOpenid(null);
try {
return RequestEntity.post(uri)
.body(MAPPER.writeValueAsString(params));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
throw new PayException("wechat app pay json failed");
return postRequestEntity(uri, params);
}
/**
* 代金券核销回调通知.
*
* @param notifyUrl the notify url
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> marketingFavorCallback(String notifyUrl) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
wechatPayClient.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 = wechatMetaBean.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);
}
/**
@@ -277,13 +300,19 @@ public class WechatPayApi {
payParams.setMchid(v3.getMchId());
String httpUrl = type.uri(WeChatServer.CHINA);
URI uri = UriComponentsBuilder.fromHttpUrl(httpUrl).build().toUri();
return postRequestEntity(uri, payParams);
}
private static RequestEntity<?> postRequestEntity(URI uri, Object params) {
try {
return RequestEntity.post(uri)
.body(MAPPER.writeValueAsString(payParams));
.body(MAPPER.writeValueAsString(params));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
throw new PayException("wechat app pay json failed");
}
}

View File

@@ -0,0 +1,49 @@
package com.enongm.dianji.payment.wechat.v3;
import com.enongm.dianji.payment.PayException;
import com.enongm.dianji.payment.wechat.v3.model.CallbackParams;
import com.enongm.dianji.payment.wechat.v3.model.CouponConsumeData;
import com.enongm.dianji.payment.wechat.v3.model.ResponseSignVerifyParams;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import lombok.SneakyThrows;
import org.springframework.util.Assert;
/**
* @author Dax
* @since 10:21
*/
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 CouponConsumeData wechatPayCouponCallback(ResponseSignVerifyParams params) {
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");
return MAPPER.readValue(data, CouponConsumeData.class);
}
throw new PayException("invalid wechat pay coupon callback");
}
}

View File

@@ -4,6 +4,7 @@ package com.enongm.dianji.payment.wechat.v3;
import com.enongm.dianji.payment.PayException;
import com.enongm.dianji.payment.wechat.WechatPayResponseErrorHandler;
import com.enongm.dianji.payment.wechat.enumeration.WechatPayV3Type;
import com.enongm.dianji.payment.wechat.v3.model.ResponseSignVerifyParams;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.SneakyThrows;
import org.springframework.http.*;
@@ -97,7 +98,7 @@ public class WechatPayClient {
/**
* Function executor.
*
* @param requestEntityBiFunction the request entity bi function
* @param requestEntityBiFunction the request entity bifunction
* @return the executor
*/
public Executor<M> function(BiFunction<WechatPayV3Type, M, RequestEntity<?>> requestEntityBiFunction) {
@@ -170,26 +171,28 @@ public class WechatPayClient {
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 IllegalStateException("cant obtain response body");
}
ResponseSignVerifyParams params = new ResponseSignVerifyParams();
// 微信请求回调id
// String RequestId = response.header("Request-ID");
// 微信平台证书序列号 用来取微信平台证书
String wechatpaySerial = headers.getFirst("Wechatpay-Serial");
params.setWechatpaySerial(headers.getFirst("Wechatpay-Serial"));
//获取应答签名
String wechatpaySignature = headers.getFirst("Wechatpay-Signature");
params.setWechatpaySignature(headers.getFirst("Wechatpay-Signature"));
//构造验签名串
String wechatpayTimestamp = headers.getFirst("Wechatpay-Timestamp");
String wechatpayNonce = headers.getFirst("Wechatpay-Nonce");
params.setWechatpayTimestamp(headers.getFirst("Wechatpay-Timestamp"));
params.setWechatpayNonce(headers.getFirst("Wechatpay-Nonce"));
params.setBody(body.toString());
// 验证微信服务器签名
if (signatureProvider.responseSignVerify(wechatpaySerial,
wechatpaySignature,
wechatpayTimestamp,
wechatpayNonce,
body.toString())) {
if (signatureProvider.responseSignVerify(params)) {
Consumer<ResponseEntity<ObjectNode>> responseConsumer = requestEntity.getResponseBodyConsumer();
if (Objects.nonNull(responseConsumer)) {
// 验证通过消费

View File

@@ -31,7 +31,10 @@ public class WechatResponseEntity<T> {
}
}
public boolean successful() {
return this.httpStatus == HttpStatus.OK.value();
public boolean is2xxSuccessful() {
if (log.isDebugEnabled()) {
log.debug("wechat httpStatus {}", this.httpStatus);
}
return HttpStatus.valueOf(this.httpStatus).is2xxSuccessful();
}
}

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
package com.enongm.dianji.payment.wechat.v3.model;
import lombok.Data;
/**
* 微信的响应签名校验参数
*
* @author Dax
* @see com.enongm.dianji.payment.wechat.v3.SignatureProvider#responseSignVerify(String, String, String, String, String)
* @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,14 @@
package com.enongm.dianji.payment.wechat.v3.model;
import lombok.Data;
/**
* 单品优惠特定信息
*/
@Data
public class SingleitemDiscountOff {
private Long singlePriceMax;
}