合单支付

This commit is contained in:
felord.cn
2020-12-13 22:06:51 +08:00
parent 9c186818cd
commit 3ddc00ba4c
11 changed files with 505 additions and 40 deletions

View File

@@ -17,7 +17,7 @@ public enum WechatPayV3Type {
/**
* 文件下载
*/
FILE_DOWNLOAD(HttpMethod.GET,"%s/v3/billdownload/file"),
FILE_DOWNLOAD(HttpMethod.GET, "%s/v3/billdownload/file"),
/**
* 微信公众号支付或者小程序支付.
@@ -34,11 +34,6 @@ public enum WechatPayV3Type {
*/
APP(HttpMethod.POST, "%s/v3/pay/transactions/app"),
/**
* 合单下单-APP支付API.
*/
COMBINE_APP(HttpMethod.POST, "%s/v3/combine-transactions/app"),
/**
* H5支付.
*/
@@ -57,27 +52,55 @@ public enum WechatPayV3Type {
TRANSACTION_OUT_TRADE_NO(HttpMethod.GET, "%s/v3/pay/transactions/out-trade-no/{out_trade_no}"),
/**
* 合单下单-APP支付API.
*/
COMBINE_APP(HttpMethod.POST, "%s/v3/combine-transactions/app"),
/**
* 合单下单-微信公众号支付或者小程序支付.
*/
COMBINE_JSAPI(HttpMethod.POST, "%s/v3/pay/combine-transactions/jsapi"),
/**
* 合单下单-H5支付API.
*/
COMBINE_MWEB(HttpMethod.POST, "%s/v3/pay/combine-transactions/h5"),
/**
* 合单下单-Native支付API. /v3/combine-transactions/out-trade-no/{combine_out_trade_no}
*/
COMBINE_NATIVE(HttpMethod.POST, "%s/v3/pay/combine-transactions/native"),
/**
* 合单查询订单API.
*/
COMBINE_TRANSACTION_OUT_TRADE_NO(HttpMethod.GET, "%s/v3/combine-transactions/out-trade-no/{combine_out_trade_no}"),
/**
* 合单关闭订单API.
*/
COMBINE_CLOSE(HttpMethod.POST, "%s/v3/combine-transactions/out-trade-no/{combine_out_trade_no}/close"),
/**
* 创建代金券批次API.
*/
MARKETING_FAVOR_STOCKS_COUPON_STOCKS(HttpMethod.POST,"%s/v3/marketing/favor/coupon-stocks"),
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_START(HttpMethod.POST, "%s/v3/marketing/favor/stocks/{stock_id}/start"),
/**
* 暂停代金券批次API.
*/
MARKETING_FAVOR_STOCKS_PAUSE(HttpMethod.POST,"%s/v3/marketing/favor/stocks/{stock_id}/pause"),
MARKETING_FAVOR_STOCKS_PAUSE(HttpMethod.POST, "%s/v3/marketing/favor/stocks/{stock_id}/pause"),
/**
* 发放代金券API、根据商户号查用户的券.
*/
MARKETING_FAVOR_USERS_COUPONS(HttpMethod.POST,"%s/v3/marketing/favor/users/{openid}/coupons"),
MARKETING_FAVOR_USERS_COUPONS(HttpMethod.POST, "%s/v3/marketing/favor/users/{openid}/coupons"),
/**
* 重启代金券API.
*/
MARKETING_FAVOR_STOCKS_RESTART(HttpMethod.POST,"%s/v3/marketing/favor/stocks/{stock_id}/restart"),
MARKETING_FAVOR_STOCKS_RESTART(HttpMethod.POST, "%s/v3/marketing/favor/stocks/{stock_id}/restart"),
/**
* 条件查询批次列表API.
*/
@@ -162,7 +185,7 @@ public enum WechatPayV3Type {
* @return the string
*/
public String uri(WeChatServer weChatServer) {
return String.format(this.pattern,weChatServer.domain());
return String.format(this.pattern, weChatServer.domain());
}
}

View File

@@ -7,6 +7,9 @@ package cn.felord.payment.wechat.v3;
* @since 1.0.0.RELEASE
*/
public class WechatApiProvider {
/**
* The Wechat pay client.
*/
private final WechatPayClient wechatPayClient;
/**
@@ -24,8 +27,8 @@ public class WechatApiProvider {
* @param tenantId the tenant id
* @return the wechat marketing favor api
*/
public WechatMarketingFavorApi favorApi(String tenantId){
return new WechatMarketingFavorApi(this.wechatPayClient,tenantId);
public WechatMarketingFavorApi favorApi(String tenantId) {
return new WechatMarketingFavorApi(this.wechatPayClient, tenantId);
}
/**
@@ -34,8 +37,18 @@ public class WechatApiProvider {
* @param tenantId the tenant id
* @return the wechat pay api
*/
public WechatDirectPayApi directPayApi(String tenantId){
return new WechatDirectPayApi(wechatPayClient,tenantId);
public WechatDirectPayApi directPayApi(String tenantId) {
return new WechatDirectPayApi(wechatPayClient, tenantId);
}
/**
* 合单支付.
*
* @param tenantId the tenant id
* @return the wechat combine pay api
*/
public WechatCombinePayApi combinePayApi(String tenantId) {
return new WechatCombinePayApi(wechatPayClient, tenantId);
}
/**
@@ -44,8 +57,8 @@ public class WechatApiProvider {
* @param tenantId the tenant id
* @return the wechat pay callback
*/
public WechatPayCallback callback(String tenantId){
return new WechatPayCallback(wechatPayClient.signatureProvider(),tenantId);
public WechatPayCallback callback(String tenantId) {
return new WechatPayCallback(wechatPayClient.signatureProvider(), tenantId);
}
}

View File

@@ -3,6 +3,8 @@ package cn.felord.payment.wechat.v3;
import cn.felord.payment.wechat.WechatPayProperties;
import cn.felord.payment.wechat.enumeration.WeChatServer;
import cn.felord.payment.wechat.enumeration.WechatPayV3Type;
import cn.felord.payment.wechat.v3.model.combine.CombineCloseParams;
import cn.felord.payment.wechat.v3.model.combine.CombineH5PayParams;
import cn.felord.payment.wechat.v3.model.combine.CombinePayParams;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.http.RequestEntity;
@@ -40,7 +42,7 @@ public class WechatCombinePayApi extends AbstractApi{
*/
public WechatResponseEntity<ObjectNode> appPay(CombinePayParams combinePayParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.APP, combinePayParams)
this.client().withType(WechatPayV3Type.COMBINE_APP, combinePayParams)
.function(this::combinePayFunction)
.consumer(wechatResponseEntity::convert)
.request();
@@ -48,6 +50,26 @@ public class WechatCombinePayApi extends AbstractApi{
}
/**
* 合单下单-JSAPI支付/小程序支付API
* <p>
* 使用合单支付接口用户只输入一次密码即可完成多个订单的支付。目前最多一次可支持50笔订单进行合单支付。
* <p>
* 注意:
* • 订单如果需要进行抽佣等需要在合单中指定需要进行分账profit_sharing为true指定后交易资金进入二级商户账户处于冻结状态可在后续使用分账接口进行分账利用分账完结进行资金解冻实现抽佣和对二级商户的账期。
*
* @param combinePayParams the combine pay params
* @return wechat response entity
*/
public WechatResponseEntity<ObjectNode> jsPay(CombinePayParams combinePayParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.COMBINE_JSAPI, combinePayParams)
.function(this::combinePayFunction)
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* Combine pay function request entity.
*
@@ -68,4 +90,107 @@ public class WechatCombinePayApi extends AbstractApi{
return Post(uri, params);
}
/**
* 合单下单-H5支付API.
* <p>
* 使用合单支付接口用户只输入一次密码即可完成多个订单的支付。目前最多一次可支持50笔订单进行合单支付。
* <p>
* 注意:
* • 订单如果需要进行抽佣等需要在合单中指定需要进行分账profit_sharing为true指定后交易资金进入二级商户账户处于冻结状态可在后续使用分账接口进行分账利用分账完结进行资金解冻实现抽佣和对二级商户的账期。
*
* @param combineH5PayParams the combine h 5 pay params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> h5Pay(CombineH5PayParams combineH5PayParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.COMBINE_MWEB, combineH5PayParams)
.function(this::combinePayFunction)
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* Combine pay function request entity.
*
* @param type the type
* @param params the params
* @return the request entity
*/
private RequestEntity<?> combinePayFunction(WechatPayV3Type type, CombineH5PayParams params) {
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
params.setCombineAppid(v3.getAppId());
params.setCombineMchid(v3.getMchId());
String notifyUrl = v3.getDomain().concat(params.getNotifyUrl());
params.setNotifyUrl(notifyUrl);
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.build()
.toUri();
return Post(uri, params);
}
/**
* 合单下单-Native支付API.
* <p>
* 使用合单支付接口用户只输入一次密码即可完成多个订单的支付。目前最多一次可支持50笔订单进行合单支付。
* <p>
* 注意:
* • 订单如果需要进行抽佣等需要在合单中指定需要进行分账profit_sharing为true指定后交易资金进入二级商户账户处于冻结状态可在后续使用分账接口进行分账利用分账完结进行资金解冻实现抽佣和对二级商户的账期。
*
* @param combinePayParams the combine pay params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> nativePay(CombinePayParams combinePayParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.COMBINE_NATIVE, combinePayParams)
.function(this::combinePayFunction)
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 合单查询订单API.
*
* @param combineOutTradeNo the combine out trade no
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> queryTransactionByOutTradeNo(String combineOutTradeNo) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.COMBINE_TRANSACTION_OUT_TRADE_NO, combineOutTradeNo)
.function((wechatPayV3Type, outTradeNo) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.expand(outTradeNo)
.toUri();
return Get(uri);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 合单关闭订单API.
* <p>
* 合单支付订单只能使用此合单关单api完成关单。
*
* 微信服务器返回 204。
*
* @param combineCloseParams the combine close params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> close(CombineCloseParams combineCloseParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.COMBINE_NATIVE, combineCloseParams)
.function((wechatPayV3Type, params) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build().toUri();
return Post(uri,params);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
}

View File

@@ -1,10 +1,7 @@
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 cn.felord.payment.wechat.v3.model.TransactionConsumeData;
import cn.felord.payment.wechat.v3.model.*;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
@@ -26,8 +23,17 @@ import java.util.function.Consumer;
*/
@Slf4j
public class WechatPayCallback {
/**
* The constant MAPPER.
*/
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* The Signature provider.
*/
private final SignatureProvider signatureProvider;
/**
* The Tenant id.
*/
private final String tenantId;
static {
@@ -72,20 +78,44 @@ public class WechatPayCallback {
* <p>
* 无需开发者判断,只有扣款成功微信才会回调此接口
*
* @param params the params
* @param couponConsumeDataConsumer the coupon consume data consumer
* @param params the params
* @param transactionConsumeDataConsumer the transaction consume data consumer
* @return the map
*/
@SneakyThrows
public Map<String, ?> transactionCallback(ResponseSignVerifyParams params, Consumer<TransactionConsumeData> couponConsumeDataConsumer) {
public Map<String, ?> transactionCallback(ResponseSignVerifyParams params, Consumer<TransactionConsumeData> transactionConsumeDataConsumer) {
String data = callback(params, EventType.TRANSACTION);
TransactionConsumeData transactionConsumeData = MAPPER.readValue(data, TransactionConsumeData.class);
couponConsumeDataConsumer.accept(transactionConsumeData);
transactionConsumeDataConsumer.accept(transactionConsumeData);
return Collections.singletonMap("code", "SUCCESS");
}
/**
* 微信合单支付成功回调.
* <p>
* 无需开发者判断,只有扣款成功微信才会回调此接口
*
* @param params the params
* @param combineTransactionConsumeDataConsumer the combine transaction consume data consumer
* @return the map
*/
@SneakyThrows
public Map<String, ?> combineTransactionCallback(ResponseSignVerifyParams params, Consumer<CombineTransactionConsumeData> combineTransactionConsumeDataConsumer) {
String data = callback(params, EventType.TRANSACTION);
CombineTransactionConsumeData combineTransactionConsumeData = MAPPER.readValue(data, CombineTransactionConsumeData.class);
combineTransactionConsumeDataConsumer.accept(combineTransactionConsumeData);
return Collections.singletonMap("code", "SUCCESS");
}
/**
* Callback string.
*
* @param params the params
* @param eventType the event type
* @return the string
*/
@SneakyThrows
private String callback(ResponseSignVerifyParams params, EventType eventType) {
if (signatureProvider.responseSignVerify(params)) {
@@ -122,8 +152,16 @@ public class WechatPayCallback {
*/
TRANSACTION("TRANSACTION.SUCCESS");
/**
* The Event.
*/
private final String event;
/**
* Instantiates a new Event type.
*
* @param event the event
*/
EventType(String event) {
this.event = event;
}

View File

@@ -219,10 +219,7 @@ public class WechatPayClient {
ObjectNode body = responseEntity.getBody();
HttpStatus statusCode = responseEntity.getStatusCode();
if (!statusCode.is2xxSuccessful()) {
throw new PayException("wechat pay server error,statusCode "+ statusCode +",result : " + body);
}
if (Objects.isNull(body)) {
throw new PayException("cant obtain wechat response body");
throw new PayException("wechat pay server error,statusCode " + statusCode + ",result : " + body);
}
ResponseSignVerifyParams params = new ResponseSignVerifyParams();
@@ -235,7 +232,9 @@ public class WechatPayClient {
//构造验签名串
params.setWechatpayTimestamp(headers.getFirst("Wechatpay-Timestamp"));
params.setWechatpayNonce(headers.getFirst("Wechatpay-Nonce"));
params.setBody(body.toString());
String content = Objects.isNull(body) ? "" : body.toString();
params.setBody(content);
// 验证微信服务器签名
if (signatureProvider.responseSignVerify(params)) {
@@ -263,12 +262,12 @@ public class WechatPayClient {
String body = responseEntity.getBody();
HttpStatus statusCode = responseEntity.getStatusCode();
if (!statusCode.is2xxSuccessful()) {
throw new PayException("wechat pay server error,statusCode "+ statusCode +",result : " + body);
throw new PayException("wechat pay server error,statusCode " + statusCode + ",result : " + body);
}
if (Objects.isNull(body)) {
throw new PayException("cant obtain wechat response body");
}
return body;
return body;
}
}

View File

@@ -0,0 +1,137 @@
package cn.felord.payment.wechat.v3.model;
import cn.felord.payment.wechat.v3.model.combine.CombinePayerInfo;
import lombok.Data;
import java.util.List;
/**
* 合单支付回调解密数据.
*
* @author felord.cn
* @since 1.0.0.RELEASE
*/
@Data
public class CombineTransactionConsumeData {
/**
* 合单商户appid.
*/
private String combineAppid;
/**
* 合单商户号.
*/
private String combineMchid;
/**
* 合单商户订单号.
*/
private String combineOutTradeNo;
/**
* 支付者.
*/
private CombinePayerInfo combinePayerInfo;
/**
* 场景信息合单支付回调只返回device_id
*/
private SceneInfo sceneInfo;
/**
* 合单支付回调子订单.
*/
private List<SubOrderCallback> subOrders;
/**
* 合单支付回调子订单.
*
* @author felord.cn
* @since 1.0.0.RELEASE
*/
@Data
public static class SubOrderCallback {
/**
* The Amount.
*/
private CombineAmount amount;
/**
* The Attach.
*/
private String attach;
/**
* The Bank type.
*/
private String bankType;
/**
* The Mchid.
*/
private String mchid;
/**
* The Out trade no.
*/
private String outTradeNo;
/**
* The Sub mchid.
*/
private String subMchid;
/**
* The Success time.
*/
private String successTime;
/**
* The Trade state.
*/
private String tradeState;
/**
* The Trade type.
*/
private String tradeType;
/**
* The Transaction id.
*/
private String transactionId;
}
/**
* 订单金额信息.
*
* @author felord.cn
* @since 1.0.0.RELEASE
*/
@Data
public static class CombineAmount{
/**
* 标价金额,单位为分.
*/
private Long totalAmount;
/**
* 标价币种.
*/
private String currency;
/**
* 现金支付金额.
*/
private Long payerAmount;
/**
* 现金支付币种.
*/
private String payerCurrency;
}
}

View File

@@ -0,0 +1,56 @@
package cn.felord.payment.wechat.v3.model.combine;
import lombok.Data;
import java.util.List;
/**
* 合单支付 关单参数.
*
* @author felord.cn
* @since 1.0.0.RELEASE
*/
@Data
public class CombineCloseParams {
/**
* 合单商户appid必填
*/
private String combineAppid;
/**
* 合单商户订单号,必填,商户侧需要保证同一商户下唯一
*/
private String combineOutTradeNo;
/**
* 子单信息必填最多50单
*/
private List<ClosingSubOrder> subOrders;
/**
* 关单-子单信息最多50单.
*
* @author felord.cn
* @since 1.0.0.RELEASE
*/
@Data
public static class ClosingSubOrder {
/**
* 子单发起方商户号必填必须与发起方appid有绑定关系。
*/
private String mchid;
/**
* 子单商户订单号必填商户系统内部订单号要求32个字符内只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。
*/
private String outTradeNo;
/**
* 二级商户商户号,由微信支付生成并下发。
* <p>
* 服务商子商户的商户号,被合单方。
* <p>
* 直连商户不用传二级商户号。
*/
private String subMchid;
}
}

View File

@@ -0,0 +1,60 @@
package cn.felord.payment.wechat.v3.model.combine;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.OffsetDateTime;
import java.util.List;
/**
* 合单支付 H5支付参数.
*
* @author felord.cn
* @since 1.0.0.RELEASE
*/
@Data
public class CombineH5PayParams {
/**
* 合单商户appid必填
*/
private String combineAppid;
/**
* 合单商户号,必填
*/
private String combineMchid;
/**
* 合单商户订单号,必填,商户侧需要保证同一商户下唯一
*/
private String combineOutTradeNo;
/**
* 合单支付者信息,选填
*/
private CombinePayerInfo combinePayerInfo;
/**
* 通知地址必填接收微信支付异步通知回调地址通知url必须为直接可访问的URL不能携带参数。
* <p>
* <strong>合单支付需要独立的通知地址。</strong>
*/
private String notifyUrl;
/**
* 合单支付场景信息描述,选填
*/
private CombineH5SceneInfo sceneInfo;
/**
* 子单信息必填最多50单
*/
private List<SubOrder> subOrders;
/**
* 交易起始时间,选填
*/
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "GMT+8")
private OffsetDateTime timeStart;
/**
* 交易结束时间,选填
*/
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "GMT+8")
private OffsetDateTime timeExpire;
}

View File

@@ -0,0 +1,11 @@
package cn.felord.payment.wechat.v3.model.combine;
import cn.felord.payment.wechat.v3.model.H5Info;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class CombineH5SceneInfo extends CombineSceneInfo {
private H5Info h5Info;
}

View File

@@ -1,14 +1,14 @@
package cn.felord.payment.wechat.v3.model.combine;
import cn.felord.payment.wechat.v3.model.SettleInfo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.OffsetDateTime;
import java.util.List;
/**
* 合单支付参数.
* 合单支付 APP支付、JSAPI支付、小程序支付、Native支付参数.
*
* @author felord.cn
* @since 1.0.0.RELEASE
@@ -46,17 +46,15 @@ public class CombinePayParams {
* 子单信息必填最多50单
*/
private List<SubOrder> subOrders;
/**
* 结算信息,选填
*/
private SettleInfo settleInfo;
/**
* 交易起始时间,选填
*/
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "GMT+8")
private OffsetDateTime timeStart;
/**
* 交易结束时间,选填
*/
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "GMT+8")
private OffsetDateTime timeExpire;
}

View File

@@ -1,6 +1,7 @@
package cn.felord.payment.wechat.v3.model.combine;
import cn.felord.payment.wechat.v3.model.SettleInfo;
import lombok.Data;
/**
@@ -45,5 +46,9 @@ public class SubOrder {
*/
private String subMchid;
/**
* 结算信息,选填
*/
private SettleInfo settleInfo;
}