微信支付完善

This commit is contained in:
xiafang
2020-12-09 11:49:24 +08:00
parent 086ebece42
commit 115b02e7e1
12 changed files with 370 additions and 63 deletions

View File

@@ -1,13 +1,13 @@
# 移动支付 Spring Boot 组件 多租户版
# 移动支付 Spring Boot 组件
为了满足业务中出现app支付、公众号支付、小程序支付等多appid并存的场景对原有的进行了增强开发出了多租户版本。
## 支持类型
- [x] **微信支付V3** 全量支持。
- [x] **微信支付V3** 全量支持,并支持多租户
- [x] **支付宝** 提供所有实现,具体以签约项目为准。
## 进度
- 微信支付营销-代金券 `WechatMarketingFavorApi` 100%
- 微信支付 支付功能 `WechatPayApi` app预支付
- 微信支付 支付功能 `WechatPayApi` 100%
## 采用技术
- Spring

View File

@@ -73,15 +73,4 @@ public class WechatPayConfiguration {
public WechatApiProvider wechatApiProvider(WechatPayClient wechatPayClient){
return new WechatApiProvider(wechatPayClient);
}
/**
* 微信支付回调工具.
*
* @param signatureProvider the signature provider
* @return the wechat pay callback
*/
@Bean
public WechatPayCallback wechatPayCallback(SignatureProvider signatureProvider) {
return new WechatPayCallback(signatureProvider);
}
}

View File

@@ -0,0 +1,38 @@
package cn.felord.payment.wechat.enumeration;
/**
* 微信侧返回交易状态
*
* @author Dax
* @since 11:37
*/
public enum TradeState {
/**
* 支付成功
*/
SUCCESS,
/**
* 转入退款
*/
REFUND,
/**
* 未支付
*/
NOTPAY,
/**
* 已关闭
*/
CLOSED,
/**
* 已撤销(付款码支付)
*/
REVOKED,
/**
* 用户支付中(付款码支付)
*/
USERPAYING,
/**
* 支付失败(其他原因,如银行返回失败)
*/
PAYERROR,
}

View File

@@ -0,0 +1,34 @@
package cn.felord.payment.wechat.enumeration;
/**
* 微信侧返回交易类型
*
* @author Dax
* @since 11:34
*/
public enum TradeType {
/**
* 公众号支付
*/
JSAPI,
/**
* 扫码支付
*/
NATIVE,
/**
* APP支付
*/
APP,
/**
* 付款码支付
*/
MICROPAY,
/**
* H5支付
*/
MWEB,
/**
* 刷脸支付
*/
FACEPAY,
}

View File

@@ -38,6 +38,10 @@ public enum WechatPayV3Type {
* H5支付.
*/
MWEB(HttpMethod.POST, "%s/v3/pay/transactions/h5"),
/**
* 关闭订单.
*/
CLOSE(HttpMethod.POST, "%s/v3/pay/transactions/out-trade-no/{out_trade_no}/close"),
/**
* 微信支付订单号查询.
*/

View File

@@ -1,17 +1,21 @@
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 org.springframework.http.RequestEntity;
import org.springframework.util.Assert;
import java.net.URI;
/**
* The type Abstract api.
*
* @author Dax
* @since 18:23
* @since 18 :23
*/
public abstract class AbstractApi {
private final ObjectMapper mapper;
@@ -19,43 +23,94 @@ public abstract class AbstractApi {
private final String tenantId;
public AbstractApi(WechatPayClient wechatPayClient,String tenantId) {
/**
* Instantiates a new Abstract api.
*
* @param wechatPayClient the wechat pay client
* @param tenantId the tenant id
*/
public AbstractApi(WechatPayClient wechatPayClient, String tenantId) {
this.mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
this.wechatPayClient = wechatPayClient;
Assert.hasText(tenantId, "tenantId is required");
if (!container().getTenantIds().contains(tenantId)) {
throw new PayException("tenantId is not in wechatMetaContainer ");
throw new PayException("tenantId is not in wechatMetaContainer");
}
this.tenantId = tenantId;
}
/**
* Gets mapper.
*
* @return the mapper
*/
public ObjectMapper getMapper() {
return mapper;
}
/**
* Client wechat pay client.
*
* @return the wechat pay client
*/
public WechatPayClient client() {
return wechatPayClient;
}
/**
* Tenant id string.
*
* @return the string
*/
public String tenantId() {
return tenantId;
}
/**
* Container wechat meta container.
*
* @return the wechat meta container
*/
public WechatMetaContainer container() {
return wechatPayClient.signatureProvider().wechatMetaContainer();
}
protected RequestEntity<?> post(URI uri, Object params,String tenantId) {
/**
* Wechat meta bean wechat meta bean.
*
* @return the wechat meta bean
*/
public WechatMetaBean wechatMetaBean() {
return container().getWechatMeta(tenantId);
}
/**
* Post request entity.
*
* @param uri the uri
* @param params the params
* @return the request entity
*/
protected RequestEntity<?> Post(URI uri, Object params) {
try {
return RequestEntity.post(uri).header("Pay-TenantId",tenantId)
return RequestEntity.post(uri).header("Pay-TenantId", tenantId)
.body(mapper.writeValueAsString(params));
} catch (JsonProcessingException e) {
throw new PayException("wechat app pay json failed");
}
}
/**
* Get request entity.
*
* @param uri the uri
* @return the request entity
*/
protected RequestEntity<?> Get(URI uri) {
return RequestEntity.get(uri).header("Pay-TenantId", tenantId)
.build();
}
}

View File

@@ -1,22 +1,51 @@
package cn.felord.payment.wechat.v3;
/**
* The type Wechat api provider.
*
* @author Dax
* @since 17:32
* @since 17 :32
*/
public class WechatApiProvider {
private final WechatPayClient wechatPayClient;
/**
* Instantiates a new Wechat api provider.
*
* @param wechatPayClient the wechat pay client
*/
public WechatApiProvider(WechatPayClient wechatPayClient) {
this.wechatPayClient = wechatPayClient;
}
/**
* 代金券.
*
* @param tenantId the tenant id
* @return the wechat marketing favor api
*/
public WechatMarketingFavorApi favorApi(String tenantId){
return new WechatMarketingFavorApi(this.wechatPayClient,tenantId);
}
/**
* 支付.
*
* @param tenantId the tenant id
* @return the wechat pay api
*/
public WechatPayApi payApi(String tenantId){
return new WechatPayApi(wechatPayClient,tenantId);
}
/**
* 回调.
*
* @param tenantId the tenant id
* @return the wechat pay callback
*/
public WechatPayCallback callback(String tenantId){
return new WechatPayCallback(wechatPayClient.signatureProvider(),tenantId);
}
}

View File

@@ -1,6 +1,5 @@
package cn.felord.payment.wechat.v3;
import cn.felord.payment.PayException;
import cn.felord.payment.wechat.WechatPayProperties;
import cn.felord.payment.wechat.enumeration.StockStatus;
import cn.felord.payment.wechat.enumeration.WeChatServer;
@@ -59,14 +58,14 @@ public class WechatMarketingFavorApi extends AbstractApi {
}
private RequestEntity<?> createStocksFunction(WechatPayV3Type type, StocksCreateParams params) {
WechatPayProperties.V3 v3 = this.container().getWechatMeta(tenantId()).getV3();
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
String mchId = v3.getMchId();
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.build()
.toUri();
params.setBelongMerchant(mchId);
return post(uri, params, tenantId());
return Post(uri, params);
}
/**
@@ -102,7 +101,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
private RequestEntity<?> sendStocksFunction(WechatPayV3Type type, StocksSendParams params) {
WechatPayProperties.V3 v3 = this.container().getWechatMeta(tenantId()).getV3();
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
params.setAppid(v3.getApp().getAppId());
params.setStockCreatorMchid(v3.getMchId());
@@ -111,7 +110,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
.expand(params.getOpenid())
.toUri();
params.setOpenid(null);
return post(uri, params, tenantId());
return Post(uri, params);
}
/**
@@ -145,7 +144,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
}
private RequestEntity<?> startAndRestartAndPauseStockFunction(WechatPayV3Type type, String stockId) {
WechatPayProperties.V3 v3 = this.container().getWechatMeta(tenantId()).getV3();
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
String mchId = v3.getMchId();
Map<String, String> body = new HashMap<>();
body.put("stock_creator_mchid", mchId);
@@ -154,7 +153,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
.build()
.expand(stockId)
.toUri();
return post(uri, body, tenantId());
return Post(uri, body);
}
/**
@@ -179,7 +178,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("offset", String.valueOf(params.getOffset()));
queryParams.add("limit", String.valueOf(params.getLimit()));
WechatPayProperties.V3 v3 = this.container().getWechatMeta(tenantId()).getV3();
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
queryParams.add("stock_creator_mchid", v3.getMchId());
LocalDateTime createStartTime = params.getCreateStartTime();
@@ -211,7 +210,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
URI uri = uriComponents
.toUri();
return RequestEntity.get(uri).header("Pay-TenantId", tenantId()).build();
return Get(uri);
}
/**
@@ -231,8 +230,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
private RequestEntity<?> stockDetailFunction(WechatPayV3Type type, String stockId) {
WechatPayProperties.V3 v3 = this.container().getWechatMeta(tenantId()).getV3();
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("stock_creator_mchid", v3.getMchId());
@@ -242,7 +240,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
.build()
.expand(stockId)
.toUri();
return RequestEntity.get(uri).header("Pay-TenantId", tenantId()).build();
return Get(uri);
}
@@ -264,13 +262,11 @@ public class WechatMarketingFavorApi extends AbstractApi {
private RequestEntity<?> couponDetailFunction(WechatPayV3Type type, CouponDetailsQueryParams params) {
WechatPayProperties.V3 v3 = this.container().getWechatMeta(tenantId()).getV3();
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("appid", v3.getApp().getAppId());
MultiValueMap<String, String> pathParams = new LinkedMultiValueMap<>();
pathParams.add("openid", params.getOpenId());
pathParams.add("coupon_id", params.getCouponId());
@@ -279,7 +275,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
.build()
.expand(pathParams)
.toUri();
return RequestEntity.get(uri).header("Pay-TenantId", tenantId()).build();
return Get(uri);
}
@@ -332,7 +328,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
private RequestEntity<?> queryUserCouponsFunction(WechatPayV3Type type, UserCouponsQueryParams params) {
WechatPayProperties.V3 v3 = this.container().getWechatMeta(tenantId()).getV3();
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("appid", v3.getApp().getAppId());
@@ -363,7 +359,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
.build()
.expand(params.getOpenId())
.toUri();
return RequestEntity.get(uri).header("Pay-TenantId", tenantId()).build();
return Get(uri);
}
@@ -406,7 +402,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
.build()
.expand(stockId)
.toUri();
return RequestEntity.get(uri).header("Pay-TenantId", tenantId()).build();
return Get(uri);
}
/**
@@ -467,7 +463,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
}
private RequestEntity<?> setMarketingFavorCallbackFunction(WechatPayV3Type type, String notifyUrl) {
WechatPayProperties.V3 v3 = this.container().getWechatMeta(tenantId()).getV3();
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
Map<String, Object> body = new HashMap<>(3);
body.put("mchid", v3.getMchId());
@@ -476,7 +472,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.build()
.toUri();
return post(uri, body, tenantId());
return Post(uri, body);
}
public String billDownload(String link) {
@@ -490,7 +486,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
URI uri = UriComponentsBuilder.fromHttpUrl(link)
.build()
.toUri();
return RequestEntity.get(uri).header("Pay-TenantId", tenantId()).build();
return Get(uri);
}
}

View File

@@ -92,13 +92,13 @@ public class WechatPayApi extends AbstractApi {
}
private RequestEntity<?> payFunction(WechatPayV3Type type, PayParams payParams) {
WechatPayProperties.V3 v3 = this.container().getWechatMeta(tenantId()).getV3();
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
payParams.setAppid(v3.getAppId());
payParams.setMchid(v3.getMchId());
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.build()
.toUri();
return post(uri, payParams,tenantId());
return Post(uri, payParams);
}
/**
@@ -132,17 +132,46 @@ public class WechatPayApi extends AbstractApi {
}
private RequestEntity<?> queryTransactionFunction(WechatPayV3Type type, TransactionQueryParams params) {
WechatPayProperties.V3 v3 = this.container().getWechatMeta(tenantId()).getV3();
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("mchid", params.getMchId());
queryParams.add("mchid", v3.getMchId());
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.queryParams(queryParams)
.build()
.expand(params.getTransactionIdOrOutTradeNo())
.toUri();
return RequestEntity.get(uri).header("Pay-TenantId",tenantId()).build();
return Get(uri);
}
/**
* 关单API
*
* @param outTradeNo the out trade no
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> closeByOutTradeNo(String outTradeNo) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.CLOSE, outTradeNo)
.function(this::closeByOutTradeNoFunction)
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
private RequestEntity<?> closeByOutTradeNoFunction(WechatPayV3Type type, String outTradeNo) {
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("mchid", v3.getMchId());
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.queryParams(queryParams)
.build()
.expand(outTradeNo)
.toUri();
return Post(uri,queryParams);
}
}

View File

@@ -4,6 +4,7 @@ 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 cn.felord.payment.wechat.v3.model.TransactionConsumeData;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
@@ -11,12 +12,14 @@ import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
/**
* The type Wechat pay callback.
* 微信支付回调工具.
*
* @author Dax
* @since 10 :21
@@ -25,6 +28,7 @@ import java.util.function.Consumer;
public class WechatPayCallback {
private static final ObjectMapper MAPPER = new ObjectMapper();
private final SignatureProvider signatureProvider;
private final String tenantId;
static {
MAPPER.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
@@ -35,40 +39,91 @@ public class WechatPayCallback {
* Instantiates a new Wechat pay callback.
*
* @param signatureProvider the signature provider
* @param tenantId the tenant id
*/
public WechatPayCallback(SignatureProvider signatureProvider) {
public WechatPayCallback(SignatureProvider signatureProvider, String tenantId) {
this.signatureProvider = signatureProvider;
Assert.hasText(tenantId, "tenantId is required");
this.tenantId = tenantId;
}
/**
* 微信支付代金券核销回调工具.
* 微信支付代金券核销回调.
*
* @param tenantId the tenant id
* @param params the params
* @param couponConsumeDataConsumer the coupon consume data consumer
* @return the map
*/
@SneakyThrows
public Map<String, ?> wechatPayCouponCallback(String tenantId, ResponseSignVerifyParams params, Consumer<CouponConsumeData> couponConsumeDataConsumer) {
public Map<String, ?> couponCallback(ResponseSignVerifyParams params, Consumer<CouponConsumeData> couponConsumeDataConsumer) {
String data = callback(params, EventType.COUPON);
CouponConsumeData couponConsumeData = MAPPER.readValue(data, CouponConsumeData.class);
couponConsumeDataConsumer.accept(couponConsumeData);
Map<String, Object> responseBody = new HashMap<>();
responseBody.put("code", 200);
responseBody.put("message", "SUCCESS");
return responseBody;
}
/**
* 微信支付成功回调.
* <p>
* 无需开发者判断,只有扣款成功微信才会回调此接口
*
* @param params the params
* @param couponConsumeDataConsumer the coupon consume data consumer
* @return the map
*/
@SneakyThrows
public Map<String, ?> transactionCallback(ResponseSignVerifyParams params, Consumer<TransactionConsumeData> couponConsumeDataConsumer) {
String data = callback(params, EventType.TRANSACTION);
TransactionConsumeData transactionConsumeData = MAPPER.readValue(data, TransactionConsumeData.class);
couponConsumeDataConsumer.accept(transactionConsumeData);
return Collections.singletonMap("code", "SUCCESS");
}
@SneakyThrows
private String callback(ResponseSignVerifyParams params, EventType eventType) {
if (signatureProvider.responseSignVerify(params)) {
CallbackParams callbackParams = MAPPER.readValue(params.getBody(), CallbackParams.class);
if (!Objects.equals(callbackParams.getEventType(), eventType.event)) {
log.error("wechat pay event type is not matched, callbackParams {}", callbackParams);
throw new PayException(" wechat pay event type is not matched");
}
CallbackParams.Resource resource = callbackParams.getResource();
String associatedData = resource.getAssociatedData();
String nonce = resource.getNonce();
String ciphertext = resource.getCiphertext();
String data = signatureProvider.decryptResponseBody(tenantId,associatedData, nonce, ciphertext);
String data = signatureProvider.decryptResponseBody(tenantId, 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;
return data;
}
throw new PayException("invalid wechat pay callback");
}
/**
* 事件类型用于处理回调.
*/
enum EventType {
/**
* 优惠券核销事件.
*/
COUPON("COUPON.USE"),
/**
* 支付成功事件.
*/
TRANSACTION("TRANSACTION.SUCCESS");
private final String event;
EventType(String event) {
this.event = event;
}
throw new PayException("invalid wechat pay coupon callback");
}

View File

@@ -0,0 +1,32 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
import java.util.List;
public class PromotionDetail {
private Long amount;
private String couponId;
private String currency;
private List<GoodsDetail> goodsDetail;
private Long merchantContribute;
private String name;
private Long otherContribute;
private String scope;
private String stockId;
private String type;
private Long wechatpayContribute;
@Data
public static class GoodsDetail {
private String goodsId;
private Long quantity;
private Long unitPrice;
private Long discountAmount;
private String goodsRemark;
}
}

View File

@@ -0,0 +1,46 @@
package cn.felord.payment.wechat.v3.model;
import lombok.Data;
import java.util.List;
@Data
public class TransactionConsumeData {
private Amount amount;
private String appid;
private String attach;
private String bankType;
private String mchid;
private String outTradeNo;
private Payer payer;
private List<PromotionDetail> promotionDetail;
private SceneInfo sceneInfo;
private String successTime;
private String tradeState;
private String tradeStateDesc;
private String tradeType;
private String transactionId;
@Data
public static class Payer {
private String openid;
}
@Data
public static class SceneInfo {
private String deviceId;
}
@Data
public static class Amount {
private int total;
private int payerTotal;
private String currency;
private String payerCurrency;
}
}