params, String body)
+ throws Throwable;
+
+ @Override
+ public final PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type) {
+ try {
+ return doGetTransfer(outTradeNo, type);
+ } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
+ throw ex;
+ } catch (Throwable ex) {
+ log.error("[getTransfer][客户端({}) outTradeNo({}) type({}) 查询转账单异常]",
+ getId(), outTradeNo, type, ex);
+ throw buildPayException(ex);
+ }
+ }
+
+ protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO)
+ throws Throwable;
+
+ protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type)
+ throws Throwable;
+
+ // ========== 各种工具方法 ==========
+
+ private PayException buildPayException(Throwable ex) {
+ if (ex instanceof PayException) {
+ return (PayException) ex;
+ }
+ throw new PayException(ex);
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/NonePayClientConfig.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/NonePayClientConfig.java
new file mode 100644
index 0000000..6396db0
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/NonePayClientConfig.java
@@ -0,0 +1,30 @@
+package com.tashow.cloud.sdk.payment.client.impl;
+
+import com.tashow.cloud.sdk.payment.client.PayClientConfig;
+import jakarta.validation.Validator;
+import lombok.Data;
+
+/**
+ * 无需任何配置 PayClientConfig 实现类
+ *
+ * @author jason
+ */
+@Data
+public class NonePayClientConfig implements PayClientConfig {
+
+ /**
+ * 配置名称
+ *
+ * 如果不加任何属性,JsonUtils.parseObject2 解析会报错,所以暂时加个名称
+ */
+ private String name;
+
+ public NonePayClientConfig(){
+ this.name = "none-config";
+ }
+
+ @Override
+ public void validate(Validator validator) {
+ // 无任何配置不需要校验
+ }
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/PayClientFactoryImpl.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/PayClientFactoryImpl.java
new file mode 100644
index 0000000..cf29aed
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/PayClientFactoryImpl.java
@@ -0,0 +1,97 @@
+package com.tashow.cloud.sdk.payment.client.impl;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.ReflectUtil;
+import com.tashow.cloud.sdk.payment.client.PayClient;
+import com.tashow.cloud.sdk.payment.client.PayClientConfig;
+import com.tashow.cloud.sdk.payment.client.PayClientFactory;
+import com.tashow.cloud.sdk.payment.client.impl.alipay.*;
+import com.tashow.cloud.sdk.payment.client.impl.weixin.*;
+import com.tashow.cloud.sdk.payment.enums.channel.PayChannelEnum;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import static com.tashow.cloud.sdk.payment.enums.channel.PayChannelEnum.*;
+
+
+/**
+ * 支付客户端的工厂实现类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class PayClientFactoryImpl implements PayClientFactory {
+
+ /**
+ * 支付客户端 Map
+ *
+ * key:渠道编号
+ */
+ private final ConcurrentMap> clients = new ConcurrentHashMap<>();
+
+ /**
+ * 支付客户端 Class Map
+ */
+ private final Map> clientClass = new ConcurrentHashMap<>();
+
+ public PayClientFactoryImpl() {
+ // 微信支付客户端
+ clientClass.put(WX_PUB, WxPubPayClient.class);
+ clientClass.put(WX_LITE, WxLitePayClient.class);
+ clientClass.put(WX_APP, WxAppPayClient.class);
+ clientClass.put(WX_BAR, WxBarPayClient.class);
+ clientClass.put(WX_NATIVE, WxNativePayClient.class);
+ clientClass.put(WX_WAP, WxWapPayClient.class);
+ // 支付包支付客户端
+ clientClass.put(ALIPAY_WAP, AlipayWapPayClient.class);
+ clientClass.put(ALIPAY_QR, AlipayQrPayClient.class);
+ clientClass.put(ALIPAY_APP, AlipayAppPayClient.class);
+ clientClass.put(ALIPAY_PC, AlipayPcPayClient.class);
+ clientClass.put(ALIPAY_BAR, AlipayBarPayClient.class);
+ // Mock 支付客户端
+// clientClass.put(MOCK, MockPayClient.class);
+ }
+
+ @Override
+ public void registerPayClientClass(PayChannelEnum channel, Class> payClientClass) {
+ clientClass.put(channel, payClientClass);
+ }
+
+ @Override
+ public PayClient getPayClient(Long channelId) {
+ AbstractPayClient> client = clients.get(channelId);
+ if (client == null) {
+ log.error("[getPayClient][渠道编号({}) 找不到客户端]", channelId);
+ }
+ return client;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public PayClient createOrUpdatePayClient(Long channelId, String channelCode,
+ Config config) {
+ AbstractPayClient client = (AbstractPayClient) clients.get(channelId);
+ if (client == null) {
+ client = this.createPayClient(channelId, channelCode, config);
+ client.init();
+ clients.put(client.getId(), client);
+ } else {
+ client.refresh(config);
+ }
+ return client;
+ }
+
+ @SuppressWarnings("unchecked")
+ private AbstractPayClient createPayClient(Long channelId, String channelCode,
+ Config config) {
+ PayChannelEnum channelEnum = PayChannelEnum.getByCode(channelCode);
+ Assert.notNull(channelEnum, String.format("支付渠道(%s) 为空", channelCode));
+ Class> payClientClass = clientClass.get(channelEnum);
+ Assert.notNull(payClientClass, String.format("支付渠道(%s) Class 为空", channelCode));
+ return (AbstractPayClient) ReflectUtil.newInstance(payClientClass, channelId, config);
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AbstractAlipayPayClient.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AbstractAlipayPayClient.java
new file mode 100644
index 0000000..75c0b9b
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AbstractAlipayPayClient.java
@@ -0,0 +1,348 @@
+package com.tashow.cloud.sdk.payment.client.impl.alipay;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpUtil;
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.AlipayConfig;
+import com.alipay.api.AlipayResponse;
+import com.alipay.api.DefaultAlipayClient;
+import com.alipay.api.domain.*;
+import com.alipay.api.internal.util.AlipaySignature;
+import com.alipay.api.request.*;
+import com.alipay.api.response.*;
+import com.tashow.cloud.common.util.json.JsonUtils;
+import com.tashow.cloud.common.util.object.ObjectUtils;
+import com.tashow.cloud.sdk.payment.client.impl.AbstractPayClient;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderRespDTO;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderUnifiedReqDTO;
+import com.tashow.cloud.sdk.payment.dto.refund.PayRefundRespDTO;
+import com.tashow.cloud.sdk.payment.dto.refund.PayRefundUnifiedReqDTO;
+import com.tashow.cloud.sdk.payment.dto.transfer.PayTransferRespDTO;
+import com.tashow.cloud.sdk.payment.dto.transfer.PayTransferUnifiedReqDTO;
+import com.tashow.cloud.sdk.payment.enums.order.PayOrderStatusRespEnum;
+import com.tashow.cloud.sdk.payment.enums.transfer.PayTransferTypeEnum;
+import lombok.Getter;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
+import static com.tashow.cloud.common.exception.enums.GlobalErrorCodeConstants.*;
+import static com.tashow.cloud.common.exception.util.ServiceExceptionUtil.exception;
+import static com.tashow.cloud.common.exception.util.ServiceExceptionUtil.exception0;
+import static com.tashow.cloud.sdk.payment.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
+
+/**
+ * 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款)
+ *
+ * @author jason
+ */
+@Slf4j
+public abstract class AbstractAlipayPayClient extends AbstractPayClient {
+
+ @Getter // 仅用于单测场景
+ protected DefaultAlipayClient client;
+
+ public AbstractAlipayPayClient(Long channelId, String channelCode, AlipayPayClientConfig config) {
+ super(channelId, channelCode, config);
+ }
+
+ @Override
+ @SneakyThrows
+ protected void doInit() {
+ AlipayConfig alipayConfig = new AlipayConfig();
+ BeanUtil.copyProperties(config, alipayConfig, false);
+ this.client = new DefaultAlipayClient(alipayConfig);
+ }
+
+ // ============ 支付相关 ==========
+
+ /**
+ * 构造支付关闭的 {@link PayOrderRespDTO} 对象
+ *
+ * @return 支付关闭的 {@link PayOrderRespDTO} 对象
+ */
+ protected PayOrderRespDTO buildClosedPayOrderRespDTO(PayOrderUnifiedReqDTO reqDTO, AlipayResponse response) {
+ Assert.isFalse(response.isSuccess());
+ return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+ reqDTO.getOutTradeNo(), response);
+ }
+
+ @Override
+ public PayOrderRespDTO doParseOrderNotify(Map params, String body) throws Throwable {
+ // 1. 校验回调数据
+ Map bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
+ AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),
+ StandardCharsets.UTF_8.name(), config.getSignType());
+
+ // 2. 解析订单的状态
+ // 额外说明:支付宝不仅仅支付成功会回调,再各种触发支付单数据变化时,都会进行回调,所以这里 status 的解析会写的比较复杂
+ Integer status = parseStatus(bodyObj.get("trade_status"));
+ // 特殊逻辑: 支付宝没有退款成功的状态,所以,如果有退款金额,我们认为是退款成功
+ if (MapUtil.getDouble(bodyObj, "refund_fee", 0D) > 0) {
+ status = PayOrderStatusRespEnum.REFUND.getStatus();
+ }
+ Assert.notNull(status, (Supplier) () -> {
+ throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body));
+ });
+ return PayOrderRespDTO.of(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")),
+ bodyObj.get("out_trade_no"), body);
+ }
+
+ @Override
+ protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable {
+ // 1.1 构建 AlipayTradeRefundModel 请求
+ AlipayTradeQueryModel model = new AlipayTradeQueryModel();
+ model.setOutTradeNo(outTradeNo);
+ // 1.2 构建 AlipayTradeQueryRequest 请求
+ AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
+ request.setBizModel(model);
+ AlipayTradeQueryResponse response;
+ if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
+ // 证书模式
+ response = client.certificateExecute(request);
+ } else {
+ response = client.execute(request);
+ }
+ if (!response.isSuccess()) { // 不成功,例如说订单不存在
+ return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+ outTradeNo, response);
+ }
+ // 2.2 解析订单的状态
+ Integer status = parseStatus(response.getTradeStatus());
+ Assert.notNull(status, () -> {
+ throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody()));
+ });
+ return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()),
+ outTradeNo, response);
+ }
+
+ private static Integer parseStatus(String tradeStatus) {
+ return Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusRespEnum.WAITING.getStatus()
+ : ObjectUtils.equalsAny(tradeStatus, "TRADE_FINISHED", "TRADE_SUCCESS") ? PayOrderStatusRespEnum.SUCCESS.getStatus()
+ : Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusRespEnum.CLOSED.getStatus() : null;
+ }
+
+ // ============ 退款相关 ==========
+
+ /**
+ * 支付宝统一的退款接口 alipay.trade.refund
+ *
+ * @param reqDTO 退款请求 request DTO
+ * @return 退款请求 Response
+ */
+ @Override
+ protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws AlipayApiException {
+ // 1.1 构建 AlipayTradeRefundModel 请求
+ AlipayTradeRefundModel model = new AlipayTradeRefundModel();
+ model.setOutTradeNo(reqDTO.getOutTradeNo());
+ model.setOutRequestNo(reqDTO.getOutRefundNo());
+ model.setRefundAmount(formatAmount(reqDTO.getRefundPrice()));
+ model.setRefundReason(reqDTO.getReason());
+ // 1.2 构建 AlipayTradePayRequest 请求
+ AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
+ request.setBizModel(model);
+
+ // 2.1 执行请求
+ AlipayTradeRefundResponse response;
+ if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
+ response = client.certificateExecute(request);
+ } else {
+ response = client.execute(request);
+ }
+ if (!response.isSuccess()) {
+ // 当出现 ACQ.SYSTEM_ERROR, 退款可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询
+ if (ObjectUtils.equalsAny(response.getSubCode(), "ACQ.SYSTEM_ERROR", "SYSTEM_ERROR")) {
+ return PayRefundRespDTO.waitingOf(null, reqDTO.getOutRefundNo(), response);
+ }
+ return PayRefundRespDTO.failureOf(response.getSubCode(), response.getSubMsg(), reqDTO.getOutRefundNo(), response);
+ }
+ // 2.2 创建返回结果
+ // 支付宝只要退款调用返回 success,就认为退款成功,不需要回调。具体可见 parseNotify 方法的说明。
+ // 另外,支付宝没有退款单号,所以不用设置
+ return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()),
+ reqDTO.getOutRefundNo(), response);
+ }
+
+ @Override
+ public PayRefundRespDTO doParseRefundNotify(Map params, String body) {
+ // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。
+ // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调
+ // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有
+ // 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。
+ // 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。
+ throw new UnsupportedOperationException("支付宝无退款回调");
+ }
+
+ @Override
+ protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws AlipayApiException {
+ // 1.1 构建 AlipayTradeFastpayRefundQueryModel 请求
+ AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel();
+ model.setOutTradeNo(outTradeNo);
+ model.setOutRequestNo(outRefundNo);
+ model.setQueryOptions(Collections.singletonList("gmt_refund_pay"));
+ // 1.2 构建 AlipayTradeFastpayRefundQueryRequest 请求
+ AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
+ request.setBizModel(model);
+
+ // 2.1 执行请求
+ AlipayTradeFastpayRefundQueryResponse response;
+ if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
+ response = client.certificateExecute(request);
+ } else {
+ response = client.execute(request);
+ }
+ if (!response.isSuccess()) {
+ // 明确不存在的情况,应该就是失败,可进行关闭
+ if (ObjectUtils.equalsAny(response.getSubCode(), "TRADE_NOT_EXIST", "ACQ.TRADE_NOT_EXIST")) {
+ return PayRefundRespDTO.failureOf(outRefundNo, response);
+ }
+ // 可能存在“ACQ.SYSTEM_ERROR”系统错误等情况,所以返回 WAIT 继续等待
+ return PayRefundRespDTO.waitingOf(null, outRefundNo, response);
+ }
+ // 2.2 创建返回结果
+ if (Objects.equals(response.getRefundStatus(), "REFUND_SUCCESS")) {
+ return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()),
+ outRefundNo, response);
+ }
+ return PayRefundRespDTO.waitingOf(null, outRefundNo, response);
+ }
+
+ @Override
+ protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException {
+ // 1.1 校验公钥类型 必须使用公钥证书模式
+ if (!Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
+ throw exception0(ERROR_CONFIGURATION.getCode(), "支付宝单笔转账必须使用公钥证书模式");
+ }
+ // 1.2 构建 AlipayFundTransUniTransferModel
+ AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel();
+ // ① 通用的参数
+ model.setTransAmount(formatAmount(reqDTO.getPrice())); // 转账金额
+ model.setOrderTitle(reqDTO.getSubject()); // 转账业务的标题,用于在支付宝用户的账单里显示。
+ model.setOutBizNo(reqDTO.getOutTransferNo());
+ model.setProductCode("TRANS_ACCOUNT_NO_PWD"); // 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD
+ model.setBizScene("DIRECT_TRANSFER"); // 业务场景 单笔无密转账固定为 DIRECT_TRANSFER
+ if (reqDTO.getChannelExtras() != null) {
+ model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
+ }
+ // ② 个性化的参数
+ Participant payeeInfo = new Participant();
+ PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType());
+ switch (transferType) {
+ // TODO @jason:是不是不用传递 transferType 参数哈?因为应该已经明确是支付宝啦?
+ // @芋艿。 是不是还要考虑转账到银行卡。所以传 transferType 但是转账到银行卡不知道要如何测试??
+ case ALIPAY_BALANCE: {
+ payeeInfo.setIdentityType("ALIPAY_LOGON_ID");
+ payeeInfo.setIdentity(reqDTO.getAlipayLogonId()); // 支付宝登录号
+ payeeInfo.setName(reqDTO.getUserName()); // 支付宝账号姓名
+ model.setPayeeInfo(payeeInfo);
+ break;
+ }
+ case BANK_CARD: {
+ payeeInfo.setIdentityType("BANKCARD_ACCOUNT");
+ // TODO 待实现
+ throw exception(NOT_IMPLEMENTED);
+ }
+ default: {
+ throw exception0(BAD_REQUEST.getCode(), "不正确的转账类型: {}", transferType);
+ }
+ }
+ // 1.3 构建 AlipayFundTransUniTransferRequest
+ AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
+ request.setBizModel(model);
+ // 执行请求
+ AlipayFundTransUniTransferResponse response = client.certificateExecute(request);
+ // 处理结果
+ if (!response.isSuccess()) {
+ // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询,或相同 outBizNo 重新发起转账
+ // 发现 outBizNo 相同 两次请求参数相同. 会返回 "PAYMENT_INFO_INCONSISTENCY", 不知道哪里的问题. 暂时返回 WAIT. 后续job 会轮询
+ if (ObjectUtils.equalsAny(response.getSubCode(),"PAYMENT_INFO_INCONSISTENCY", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
+ return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response);
+ }
+ return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+ reqDTO.getOutTransferNo(), response);
+ } else {
+ if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
+ return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+ reqDTO.getOutTransferNo(), response);
+ }
+ if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中
+ return PayTransferRespDTO.dealingOf(response.getOrderId(), reqDTO.getOutTransferNo(), response);
+ }
+ return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
+ response.getOutBizNo(), response);
+ }
+
+ }
+
+ @Override
+ protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) throws Throwable {
+ // 1.1 构建 AlipayFundTransCommonQueryModel
+ AlipayFundTransCommonQueryModel model = new AlipayFundTransCommonQueryModel();
+ model.setProductCode(type == PayTransferTypeEnum.BANK_CARD ? "TRANS_BANKCARD_NO_PWD" : "TRANS_ACCOUNT_NO_PWD");
+ model.setBizScene("DIRECT_TRANSFER"); //业务场景
+ model.setOutBizNo(outTradeNo);
+ // 1.2 构建 AlipayFundTransCommonQueryRequest
+ AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest();
+ request.setBizModel(model);
+
+ // 2.1 执行请求
+ AlipayFundTransCommonQueryResponse response;
+ if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
+ response = client.certificateExecute(request);
+ } else {
+ response = client.execute(request);
+ }
+ // 2.2 处理返回结果
+ if (response.isSuccess()) {
+ if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
+ return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+ outTradeNo, response);
+ }
+ if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中
+ return PayTransferRespDTO.dealingOf(response.getOrderId(), outTradeNo, response);
+ }
+ return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getPayDate()),
+ response.getOutBizNo(), response);
+ } else {
+ // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
+ // 当出现 ORDER_NOT_EXIST 可能是转账还在处理中,也可能是转账处理失败. 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
+ if (ObjectUtils.equalsAny(response.getSubCode(), "ORDER_NOT_EXIST", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
+ return PayTransferRespDTO.waitingOf(null, outTradeNo, response);
+ }
+ return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+ outTradeNo, response);
+ }
+ }
+
+ // TODO @chihuo:这里是不是也要实现,支付宝的。
+ @Override
+ protected PayTransferRespDTO doParseTransferNotify(Map params, String body) throws Throwable {
+ throw new UnsupportedOperationException("未实现");
+ }
+
+ // ========== 各种工具方法 ==========
+
+ protected String formatAmount(Integer amount) {
+ return String.valueOf(amount / 100.0);
+ }
+
+ protected String formatTime(LocalDateTime time) {
+ return LocalDateTimeUtil.format(time, NORM_DATETIME_FORMATTER);
+ }
+
+ protected LocalDateTime parseTime(String str) {
+ return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER);
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayAppPayClient.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayAppPayClient.java
new file mode 100644
index 0000000..1067ab8
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayAppPayClient.java
@@ -0,0 +1,60 @@
+package com.tashow.cloud.sdk.payment.client.impl.alipay;
+
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.domain.AlipayTradeAppPayModel;
+import com.alipay.api.request.AlipayTradeAppPayRequest;
+import com.alipay.api.response.AlipayTradeAppPayResponse;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderRespDTO;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderUnifiedReqDTO;
+import com.tashow.cloud.sdk.payment.enums.channel.PayChannelEnum;
+import com.tashow.cloud.sdk.payment.enums.order.PayOrderDisplayModeEnum;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 支付宝【App 支付】的 PayClient 实现类
+ *
+ * 文档:App 支付
+ *
+ * // TODO 芋艿:未详细测试,因为手头没 App
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class AlipayAppPayClient extends AbstractAlipayPayClient {
+
+ public AlipayAppPayClient(Long channelId, AlipayPayClientConfig config) {
+ super(channelId, PayChannelEnum.ALIPAY_APP.getCode(), config);
+ }
+
+ @Override
+ public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
+ // 1.1 构建 AlipayTradeAppPayModel 请求
+ AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
+ // ① 通用的参数
+ model.setOutTradeNo(reqDTO.getOutTradeNo());
+ model.setSubject(reqDTO.getSubject());
+ model.setBody(reqDTO.getBody() + "test");
+ model.setTotalAmount(formatAmount(reqDTO.getPrice()));
+ model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
+ model.setProductCode("QUICK_MSECURITY_PAY"); // 销售产品码:无线快捷支付产品
+ // ② 个性化的参数【无】
+ // ③ 支付宝扫码支付只有一种展示
+ String displayMode = PayOrderDisplayModeEnum.APP.getMode();
+
+ // 1.2 构建 AlipayTradePrecreateRequest 请求
+ AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
+ request.setBizModel(model);
+ request.setNotifyUrl(reqDTO.getNotifyUrl());
+ request.setReturnUrl(reqDTO.getReturnUrl());
+
+ // 2.1 执行请求
+ AlipayTradeAppPayResponse response = client.sdkExecute(request);
+ // 2.2 处理结果
+ if (!response.isSuccess()) {
+ return buildClosedPayOrderRespDTO(reqDTO, response);
+ }
+ return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
+ reqDTO.getOutTradeNo(), response);
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayBarPayClient.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayBarPayClient.java
new file mode 100644
index 0000000..3466dd2
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayBarPayClient.java
@@ -0,0 +1,87 @@
+package com.tashow.cloud.sdk.payment.client.impl.alipay;
+
+import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.domain.AlipayTradePayModel;
+import com.alipay.api.request.AlipayTradePayRequest;
+import com.alipay.api.response.AlipayTradePayResponse;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderRespDTO;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderUnifiedReqDTO;
+import com.tashow.cloud.sdk.payment.enums.channel.PayChannelEnum;
+import com.tashow.cloud.sdk.payment.enums.order.PayOrderDisplayModeEnum;
+import lombok.extern.slf4j.Slf4j;
+
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+import static com.tashow.cloud.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
+import static com.tashow.cloud.common.exception.util.ServiceExceptionUtil.exception0;
+import static com.tashow.cloud.sdk.payment.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
+
+
+/**
+ * 支付宝【条码支付】的 PayClient 实现类
+ *
+ * 文档:当面付
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class AlipayBarPayClient extends AbstractAlipayPayClient {
+
+ public AlipayBarPayClient(Long channelId, AlipayPayClientConfig config) {
+ super(channelId, PayChannelEnum.ALIPAY_BAR.getCode(), config);
+ }
+
+ @Override
+ public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
+ String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code");
+ if (StrUtil.isEmpty(authCode)) {
+ throw exception0(BAD_REQUEST.getCode(), "条形码不能为空");
+ }
+
+ // 1.1 构建 AlipayTradePayModel 请求
+ AlipayTradePayModel model = new AlipayTradePayModel();
+ // ① 通用的参数
+ model.setOutTradeNo(reqDTO.getOutTradeNo());
+ model.setSubject(reqDTO.getSubject());
+ model.setBody(reqDTO.getBody());
+ model.setTotalAmount(formatAmount(reqDTO.getPrice()));
+ model.setScene("bar_code"); // 当面付条码支付场景
+ // ② 个性化的参数
+ model.setAuthCode(authCode);
+ // ③ 支付宝条码支付只有一种展示
+ String displayMode = PayOrderDisplayModeEnum.BAR_CODE.getMode();
+
+ // 1.2 构建 AlipayTradePayRequest 请求
+ AlipayTradePayRequest request = new AlipayTradePayRequest();
+ request.setBizModel(model);
+ request.setNotifyUrl(reqDTO.getNotifyUrl());
+ request.setReturnUrl(reqDTO.getReturnUrl());
+
+ // 2.1 执行请求
+ AlipayTradePayResponse response;
+ if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
+ // 证书模式
+ response = client.certificateExecute(request);
+ } else {
+ response = client.execute(request);
+ }
+ // 2.2 处理结果
+ if (!response.isSuccess()) {
+ return buildClosedPayOrderRespDTO(reqDTO, response);
+ }
+ if ("10000".equals(response.getCode())) { // 免密支付
+ LocalDateTime successTime = LocalDateTimeUtil.of(response.getGmtPayment());
+ return PayOrderRespDTO.successOf(response.getTradeNo(), response.getBuyerUserId(), successTime,
+ response.getOutTradeNo(), response)
+ .setDisplayMode(displayMode).setDisplayContent("");
+ }
+ // 大额支付,需要用户输入密码,所以返回 waiting。此时,前端一般会进行轮询
+ return PayOrderRespDTO.waitingOf(displayMode, "",
+ reqDTO.getOutTradeNo(), response);
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayPayClientConfig.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayPayClientConfig.java
new file mode 100644
index 0000000..cf10a54
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayPayClientConfig.java
@@ -0,0 +1,127 @@
+package com.tashow.cloud.sdk.payment.client.impl.alipay;
+
+import com.tashow.cloud.common.util.validation.ValidationUtils;
+import com.tashow.cloud.sdk.payment.client.PayClientConfig;
+import jakarta.validation.Validator;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 支付宝的 PayClientConfig 实现类
+ * 属性主要来自 {@link com.alipay.api.AlipayConfig} 的必要属性
+ *
+ * @author 芋道源码
+ */
+@Data
+public class AlipayPayClientConfig implements PayClientConfig {
+
+ /**
+ * 公钥类型 - 公钥模式
+ */
+ public static final Integer MODE_PUBLIC_KEY = 1;
+ /**
+ * 公钥类型 - 证书模式
+ */
+ public static final Integer MODE_CERTIFICATE = 2;
+
+ /**
+ * 接口内容加密方式 - AES 加密
+ */
+ public static final String ENC_TYPE_AES = "AES";
+
+ /**
+ * 签名算法类型 - RSA
+ */
+ public static final String SIGN_TYPE_DEFAULT = "RSA2";
+
+ /**
+ * 网关地址
+ *
+ * 1. 生产环境
+ * 2. 沙箱环境
+ */
+ @NotBlank(message = "网关地址不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
+ private String serverUrl;
+
+ /**
+ * 开放平台上创建的应用的 ID
+ */
+ @NotBlank(message = "开放平台上创建的应用的 ID不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
+ private String appId;
+
+ /**
+ * 签名算法类型,推荐:RSA2
+ *
+ * {@link #SIGN_TYPE_DEFAULT}
+ */
+ @NotBlank(message = "签名算法类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
+ private String signType;
+
+ /**
+ * 公钥类型
+ * 1. {@link #MODE_PUBLIC_KEY} 情况,privateKey + alipayPublicKey
+ * 2. {@link #MODE_CERTIFICATE} 情况,appCertContent + alipayPublicCertContent + rootCertContent
+ */
+ @NotNull(message = "公钥类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
+ private Integer mode;
+
+ // ========== 公钥模式 ==========
+ /**
+ * 商户私钥
+ */
+ @NotBlank(message = "商户私钥不能为空", groups = {ModePublicKey.class})
+ private String privateKey;
+
+ /**
+ * 支付宝公钥字符串
+ */
+ @NotBlank(message = "支付宝公钥字符串不能为空", groups = {ModePublicKey.class})
+ private String alipayPublicKey;
+
+ // ========== 证书模式 ==========
+ /**
+ * 指定商户公钥应用证书内容字符串
+ */
+ @NotBlank(message = "指定商户公钥应用证书内容不能为空", groups = {ModeCertificate.class})
+ private String appCertContent;
+ /**
+ * 指定支付宝公钥证书内容字符串
+ */
+ @NotBlank(message = "指定支付宝公钥证书内容不能为空", groups = {ModeCertificate.class})
+ private String alipayPublicCertContent;
+ /**
+ * 指定根证书内容字符串
+ */
+ @NotBlank(message = "指定根证书内容字符串不能为空", groups = {ModeCertificate.class})
+ private String rootCertContent;
+
+ /**
+ * 接口内容加密方式
+ *
+ * 1. 如果为空,将使用无加密方式
+ * 2. 如果要加密,目前支付宝只有 AES 一种加密方式
+ *
+ * @see 支付宝开放平台
+ * @see AlipayPayClientConfig#ENC_TYPE_AES
+ */
+ private String encryptType;
+
+ /**
+ * 接口内容加密的私钥
+ */
+ private String encryptKey;
+
+ public interface ModePublicKey {
+ }
+
+ public interface ModeCertificate {
+ }
+
+ @Override
+ public void validate(Validator validator) {
+ ValidationUtils.validate(validator, this,
+ MODE_PUBLIC_KEY.equals(this.getMode()) ? ModePublicKey.class : ModeCertificate.class);
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayPcPayClient.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayPcPayClient.java
new file mode 100644
index 0000000..0e45d45
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayPcPayClient.java
@@ -0,0 +1,70 @@
+package com.tashow.cloud.sdk.payment.client.impl.alipay;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.http.Method;
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.domain.AlipayTradePagePayModel;
+import com.alipay.api.request.AlipayTradePagePayRequest;
+import com.alipay.api.response.AlipayTradePagePayResponse;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderRespDTO;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderUnifiedReqDTO;
+import com.tashow.cloud.sdk.payment.enums.channel.PayChannelEnum;
+import com.tashow.cloud.sdk.payment.enums.order.PayOrderDisplayModeEnum;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Objects;
+
+/**
+ * 支付宝【PC 网站】的 PayClient 实现类
+ *
+ * 文档:电脑网站支付
+ *
+ * @author XGD
+ */
+@Slf4j
+public class AlipayPcPayClient extends AbstractAlipayPayClient {
+
+ public AlipayPcPayClient(Long channelId, AlipayPayClientConfig config) {
+ super(channelId, PayChannelEnum.ALIPAY_PC.getCode(), config);
+ }
+
+ @Override
+ public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
+ // 1.1 构建 AlipayTradePagePayModel 请求
+ AlipayTradePagePayModel model = new AlipayTradePagePayModel();
+ // ① 通用的参数
+ model.setOutTradeNo(reqDTO.getOutTradeNo());
+ model.setSubject(reqDTO.getSubject());
+ model.setBody(reqDTO.getBody());
+ model.setTotalAmount(formatAmount(reqDTO.getPrice()));
+ model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
+ model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY
+ // ② 个性化的参数
+ // 如果想弄更多个性化的参数,可参考 https://www.pingxx.com/api/支付渠道 extra 参数说明.html 的 alipay_pc_direct 部分进行拓展
+ model.setQrPayMode("2"); // 跳转模式 - 订单码,效果参见:https://help.pingxx.com/article/1137360/
+ // ③ 支付宝 PC 支付有两种展示模式:FORM、URL
+ String displayMode = ObjectUtil.defaultIfNull(reqDTO.getDisplayMode(),
+ PayOrderDisplayModeEnum.URL.getMode());
+
+ // 1.2 构建 AlipayTradePagePayRequest 请求
+ AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
+ request.setBizModel(model);
+ request.setNotifyUrl(reqDTO.getNotifyUrl());
+ request.setReturnUrl(reqDTO.getReturnUrl());
+
+ // 2.1 执行请求
+ AlipayTradePagePayResponse response;
+ if (Objects.equals(displayMode, PayOrderDisplayModeEnum.FORM.getMode())) {
+ response = client.pageExecute(request, Method.POST.name()); // 需要特殊使用 POST 请求
+ } else {
+ response = client.pageExecute(request, Method.GET.name());
+ }
+ // 2.2 处理结果
+ if (!response.isSuccess()) {
+ return buildClosedPayOrderRespDTO(reqDTO, response);
+ }
+ return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
+ reqDTO.getOutTradeNo(), response);
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayQrPayClient.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayQrPayClient.java
new file mode 100644
index 0000000..9995ef4
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayQrPayClient.java
@@ -0,0 +1,67 @@
+package com.tashow.cloud.sdk.payment.client.impl.alipay;
+
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.domain.AlipayTradePrecreateModel;
+import com.alipay.api.request.AlipayTradePrecreateRequest;
+import com.alipay.api.response.AlipayTradePrecreateResponse;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderRespDTO;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderUnifiedReqDTO;
+import com.tashow.cloud.sdk.payment.enums.channel.PayChannelEnum;
+import com.tashow.cloud.sdk.payment.enums.order.PayOrderDisplayModeEnum;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Objects;
+
+import static com.tashow.cloud.sdk.payment.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
+
+
+/**
+ * 支付宝【扫码支付】的 PayClient 实现类
+ *
+ * 文档:扫码支付
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class AlipayQrPayClient extends AbstractAlipayPayClient {
+
+ public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) {
+ super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config);
+ }
+
+ @Override
+ public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
+ // 1.1 构建 AlipayTradePrecreateModel 请求
+ AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
+ // ① 通用的参数
+ model.setOutTradeNo(reqDTO.getOutTradeNo());
+ model.setSubject(reqDTO.getSubject());
+ model.setBody(reqDTO.getBody());
+ model.setTotalAmount(formatAmount(reqDTO.getPrice()));
+ model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT
+ // ② 个性化的参数【无】
+ // ③ 支付宝扫码支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开
+ String displayMode = PayOrderDisplayModeEnum.QR_CODE.getMode();
+
+ // 1.2 构建 AlipayTradePrecreateRequest 请求
+ AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
+ request.setBizModel(model);
+ request.setNotifyUrl(reqDTO.getNotifyUrl());
+ request.setReturnUrl(reqDTO.getReturnUrl());
+
+ // 2.1 执行请求
+ AlipayTradePrecreateResponse response;
+ if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
+ // 证书模式
+ response = client.certificateExecute(request);
+ } else {
+ response = client.execute(request);
+ }
+ // 2.2 处理结果
+ if (!response.isSuccess()) {
+ return buildClosedPayOrderRespDTO(reqDTO, response);
+ }
+ return PayOrderRespDTO.waitingOf(displayMode, response.getQrCode(),
+ reqDTO.getOutTradeNo(), response);
+ }
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayWapPayClient.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayWapPayClient.java
new file mode 100644
index 0000000..560fdd8
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/alipay/AlipayWapPayClient.java
@@ -0,0 +1,59 @@
+package com.tashow.cloud.sdk.payment.client.impl.alipay;
+
+import cn.hutool.http.Method;
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.domain.AlipayTradeWapPayModel;
+import com.alipay.api.request.AlipayTradeWapPayRequest;
+import com.alipay.api.response.AlipayTradeWapPayResponse;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderRespDTO;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderUnifiedReqDTO;
+import com.tashow.cloud.sdk.payment.enums.channel.PayChannelEnum;
+import com.tashow.cloud.sdk.payment.enums.order.PayOrderDisplayModeEnum;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 支付宝【Wap 网站】的 PayClient 实现类
+ *
+ * 文档:手机网站支付接口
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class AlipayWapPayClient extends AbstractAlipayPayClient {
+
+ public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) {
+ super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config);
+ }
+
+ @Override
+ public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
+ // 1.1 构建 AlipayTradeWapPayModel 请求
+ AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
+ // ① 通用的参数
+ model.setOutTradeNo(reqDTO.getOutTradeNo());
+ model.setSubject(reqDTO.getSubject());
+ model.setBody(reqDTO.getBody());
+ model.setTotalAmount(formatAmount(reqDTO.getPrice()));
+ model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY
+ // ② 个性化的参数【无】
+ // ③ 支付宝 Wap 支付只有一种展示:URL
+ String displayMode = PayOrderDisplayModeEnum.URL.getMode();
+
+ // 1.2 构建 AlipayTradeWapPayRequest 请求
+ AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
+ request.setBizModel(model);
+ request.setNotifyUrl(reqDTO.getNotifyUrl());
+ request.setReturnUrl(reqDTO.getReturnUrl());
+ model.setQuitUrl(reqDTO.getReturnUrl());
+ model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
+
+ // 2.1 执行请求
+ AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name());
+ // 2.2 处理结果
+ if (!response.isSuccess()) {
+ return buildClosedPayOrderRespDTO(reqDTO, response);
+ }
+ return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
+ reqDTO.getOutTradeNo(), response);
+ }
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/AbstractWxPayClient.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/AbstractWxPayClient.java
new file mode 100644
index 0000000..dadea5b
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/AbstractWxPayClient.java
@@ -0,0 +1,557 @@
+package com.tashow.cloud.sdk.payment.client.impl.weixin;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.date.TemporalAccessorUtil;
+import cn.hutool.core.util.StrUtil;
+import com.github.binarywang.wxpay.bean.notify.WxPayNotifyV3Result;
+import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
+import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
+import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
+import com.github.binarywang.wxpay.bean.request.*;
+import com.github.binarywang.wxpay.bean.result.*;
+import com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesRequest;
+import com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesResult;
+import com.github.binarywang.wxpay.bean.transfer.TransferBatchesRequest;
+import com.github.binarywang.wxpay.bean.transfer.TransferBatchesResult;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
+import com.tashow.cloud.common.util.io.FileUtils;
+import com.tashow.cloud.common.util.object.ObjectUtils;
+import com.tashow.cloud.sdk.payment.client.impl.AbstractPayClient;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderRespDTO;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderUnifiedReqDTO;
+import com.tashow.cloud.sdk.payment.dto.refund.PayRefundRespDTO;
+import com.tashow.cloud.sdk.payment.dto.refund.PayRefundUnifiedReqDTO;
+import com.tashow.cloud.sdk.payment.dto.transfer.PayTransferRespDTO;
+import com.tashow.cloud.sdk.payment.dto.transfer.PayTransferUnifiedReqDTO;
+import com.tashow.cloud.sdk.payment.dto.transfer.WxPayTransferPartnerNotifyV3Result;
+import com.tashow.cloud.sdk.payment.enums.order.PayOrderStatusRespEnum;
+import com.tashow.cloud.sdk.payment.enums.transfer.PayTransferTypeEnum;
+import lombok.extern.slf4j.Slf4j;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static cn.hutool.core.date.DatePattern.*;
+import static com.tashow.cloud.sdk.payment.client.impl.weixin.WxPayClientConfig.API_VERSION_V2;
+import static com.tashow.cloud.sdk.payment.client.impl.weixin.WxPayClientConfig.API_VERSION_V3;
+
+/**
+ * 微信支付抽象类,实现微信统一的接口、以及部分实现(退款)
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public abstract class AbstractWxPayClient extends AbstractPayClient {
+
+ protected WxPayService client;
+
+ public AbstractWxPayClient(Long channelId, String channelCode, WxPayClientConfig config) {
+ super(channelId, channelCode, config);
+ }
+
+ /**
+ * 初始化 client 客户端
+ *
+ * @param tradeType 交易类型
+ */
+ protected void doInit(String tradeType) {
+ // 创建 config 配置
+ WxPayConfig payConfig = new WxPayConfig();
+ BeanUtil.copyProperties(config, payConfig, "keyContent", "privateKeyContent");
+ payConfig.setTradeType(tradeType);
+ // weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
+ if (Objects.equals(config.getApiVersion(), API_VERSION_V2)) {
+ payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
+ } else if (Objects.equals(config.getApiVersion(), API_VERSION_V3)) {
+ payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
+ }
+
+ // 创建 client 客户端
+ client = new WxPayServiceImpl();
+ client.setConfig(payConfig);
+ }
+
+ // ============ 支付相关 ==========
+
+ @Override
+ protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
+ try {
+ switch (config.getApiVersion()) {
+ case API_VERSION_V2:
+ return doUnifiedOrderV2(reqDTO);
+ case API_VERSION_V3:
+ return doUnifiedOrderV3(reqDTO);
+ default:
+ throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
+ }
+ } catch (WxPayException e) {
+ log.error("[doUnifiedOrder][退款({}) 发起微信支付异常", reqDTO, e);
+ String errorCode = getErrorCode(e);
+ String errorMessage = getErrorMessage(e);
+ return PayOrderRespDTO.closedOf(errorCode, errorMessage,
+ reqDTO.getOutTradeNo(), e.getXmlString());
+ }
+ }
+
+ /**
+ * 【V2】调用支付渠道,统一下单
+ *
+ * @param reqDTO 下单信息
+ * @return 各支付渠道的返回结果
+ */
+ protected abstract PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO)
+ throws Exception;
+
+ /**
+ * 【V3】调用支付渠道,统一下单
+ *
+ * @param reqDTO 下单信息
+ * @return 各支付渠道的返回结果
+ */
+ protected abstract PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO)
+ throws WxPayException;
+
+ /**
+ * 【V2】创建微信下单请求
+ *
+ * @param reqDTO 下信息
+ * @return 下单请求
+ */
+ protected WxPayUnifiedOrderRequest buildPayUnifiedOrderRequestV2(PayOrderUnifiedReqDTO reqDTO) {
+ return WxPayUnifiedOrderRequest.newBuilder()
+ .outTradeNo(reqDTO.getOutTradeNo())
+ .body(reqDTO.getSubject())
+ .detail(reqDTO.getBody())
+ .totalFee(reqDTO.getPrice()) // 单位分
+ .timeExpire(formatDateV2(reqDTO.getExpireTime()))
+ .spbillCreateIp(reqDTO.getUserIp())
+ .notifyUrl(reqDTO.getNotifyUrl())
+ .build();
+ }
+
+ /**
+ * 【V3】创建微信下单请求
+ *
+ * @param reqDTO 下信息
+ * @return 下单请求
+ */
+ protected WxPayUnifiedOrderV3Request buildPayUnifiedOrderRequestV3(PayOrderUnifiedReqDTO reqDTO) {
+ WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
+ request.setOutTradeNo(reqDTO.getOutTradeNo());
+ request.setDescription(reqDTO.getSubject());
+ request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())); // 单位分
+ request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));
+ request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
+ request.setNotifyUrl(reqDTO.getNotifyUrl());
+ return request;
+ }
+
+ @Override
+ public PayOrderRespDTO doParseOrderNotify(Map params, String body) throws WxPayException {
+ switch (config.getApiVersion()) {
+ case API_VERSION_V2:
+ return doParseOrderNotifyV2(body);
+ case API_VERSION_V3:
+ return doParseOrderNotifyV3(body);
+ default:
+ throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
+ }
+ }
+
+ private PayOrderRespDTO doParseOrderNotifyV2(String body) throws WxPayException {
+ // 1. 解析回调
+ WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body);
+ // 2. 构建结果
+ // V2 微信支付的回调,只有 SUCCESS 支付成功、CLOSED 支付失败两种情况,无需像支付宝一样解析的比较复杂
+ Integer status = Objects.equals(response.getResultCode(), "SUCCESS") ?
+ PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus();
+ return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
+ response.getOutTradeNo(), body);
+ }
+
+ private PayOrderRespDTO doParseOrderNotifyV3(String body) throws WxPayException {
+ // 1. 解析回调
+ WxPayNotifyV3Result response = client.parseOrderNotifyV3Result(body, null);
+ WxPayNotifyV3Result.DecryptNotifyResult result = response.getResult();
+ // 2. 构建结果
+ Integer status = parseStatus(result.getTradeState());
+ String openid = result.getPayer() != null ? result.getPayer().getOpenid() : null;
+ return PayOrderRespDTO.of(status, result.getTransactionId(), openid, parseDateV3(result.getSuccessTime()),
+ result.getOutTradeNo(), body);
+ }
+
+ @Override
+ protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable {
+ try {
+ switch (config.getApiVersion()) {
+ case API_VERSION_V2:
+ return doGetOrderV2(outTradeNo);
+ case API_VERSION_V3:
+ return doGetOrderV3(outTradeNo);
+ default:
+ throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
+ }
+ } catch (WxPayException e) {
+ if (ObjectUtils.equalsAny(e.getErrCode(), "ORDERNOTEXIST", "ORDER_NOT_EXIST")) {
+ String errorCode = getErrorCode(e);
+ String errorMessage = getErrorMessage(e);
+ return PayOrderRespDTO.closedOf(errorCode, errorMessage,
+ outTradeNo, e.getXmlString());
+ }
+ throw e;
+ }
+ }
+
+ private PayOrderRespDTO doGetOrderV2(String outTradeNo) throws WxPayException {
+ // 构建 WxPayUnifiedOrderRequest 对象
+ WxPayOrderQueryRequest request = WxPayOrderQueryRequest.newBuilder()
+ .outTradeNo(outTradeNo).build();
+ // 执行请求
+ WxPayOrderQueryResult response = client.queryOrder(request);
+
+ // 转换结果
+ Integer status = parseStatus(response.getTradeState());
+ return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
+ outTradeNo, response);
+ }
+
+ private PayOrderRespDTO doGetOrderV3(String outTradeNo) throws WxPayException {
+ // 构建 WxPayUnifiedOrderRequest 对象
+ WxPayOrderQueryV3Request request = new WxPayOrderQueryV3Request()
+ .setOutTradeNo(outTradeNo);
+ // 执行请求
+ WxPayOrderQueryV3Result response = client.queryOrderV3(request);
+
+ // 转换结果
+ Integer status = parseStatus(response.getTradeState());
+ String openid = response.getPayer() != null ? response.getPayer().getOpenid() : null;
+ return PayOrderRespDTO.of(status, response.getTransactionId(), openid, parseDateV3(response.getSuccessTime()),
+ outTradeNo, response);
+ }
+
+ private static Integer parseStatus(String tradeState) {
+ switch (tradeState) {
+ case "NOTPAY":
+ case "USERPAYING": // 支付中,等待用户输入密码(条码支付独有)
+ return PayOrderStatusRespEnum.WAITING.getStatus();
+ case "SUCCESS":
+ return PayOrderStatusRespEnum.SUCCESS.getStatus();
+ case "REFUND":
+ return PayOrderStatusRespEnum.REFUND.getStatus();
+ case "CLOSED":
+ case "REVOKED": // 已撤销(刷卡支付独有)
+ case "PAYERROR": // 支付失败(其它原因,如银行返回失败)
+ return PayOrderStatusRespEnum.CLOSED.getStatus();
+ default:
+ throw new IllegalArgumentException(StrUtil.format("未知的支付状态({})", tradeState));
+ }
+ }
+
+ // ============ 退款相关 ==========
+
+ @Override
+ protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
+ try {
+ switch (config.getApiVersion()) {
+ case API_VERSION_V2:
+ return doUnifiedRefundV2(reqDTO);
+ case API_VERSION_V3:
+ return doUnifiedRefundV3(reqDTO);
+ default:
+ throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
+ }
+ } catch (WxPayException e) {
+ String errorCode = getErrorCode(e);
+ String errorMessage = getErrorMessage(e);
+ return PayRefundRespDTO.failureOf(errorCode, errorMessage,
+ reqDTO.getOutRefundNo(), e.getXmlString());
+ }
+ }
+
+ private PayRefundRespDTO doUnifiedRefundV2(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
+ // 1. 构建 WxPayRefundRequest 请求
+ WxPayRefundRequest request = new WxPayRefundRequest()
+ .setOutTradeNo(reqDTO.getOutTradeNo())
+ .setOutRefundNo(reqDTO.getOutRefundNo())
+ .setRefundFee(reqDTO.getRefundPrice())
+ .setRefundDesc(reqDTO.getReason())
+ .setTotalFee(reqDTO.getPayPrice())
+ .setNotifyUrl(reqDTO.getNotifyUrl());
+ // 2.1 执行请求
+ WxPayRefundResult response = client.refundV2(request);
+ // 2.2 创建返回结果
+ if (Objects.equals("SUCCESS", response.getResultCode())) { // V2 情况下,不直接返回退款成功,而是等待异步通知
+ return PayRefundRespDTO.waitingOf(response.getRefundId(),
+ reqDTO.getOutRefundNo(), response);
+ }
+ return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
+ }
+
+ private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
+ // 1. 构建 WxPayRefundRequest 请求
+ WxPayRefundV3Request request = new WxPayRefundV3Request()
+ .setOutTradeNo(reqDTO.getOutTradeNo())
+ .setOutRefundNo(reqDTO.getOutRefundNo())
+ .setAmount(new WxPayRefundV3Request.Amount().setRefund(reqDTO.getRefundPrice())
+ .setTotal(reqDTO.getPayPrice()).setCurrency("CNY"))
+ .setReason(reqDTO.getReason())
+ .setNotifyUrl(reqDTO.getNotifyUrl());
+ // 2.1 执行请求
+ WxPayRefundV3Result response = client.refundV3(request);
+ // 2.2 创建返回结果
+ if (Objects.equals("SUCCESS", response.getStatus())) {
+ return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()),
+ reqDTO.getOutRefundNo(), response);
+ }
+ if (Objects.equals("PROCESSING", response.getStatus())) {
+ return PayRefundRespDTO.waitingOf(response.getRefundId(),
+ reqDTO.getOutRefundNo(), response);
+ }
+ return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
+ }
+
+ @Override
+ public PayRefundRespDTO doParseRefundNotify(Map params, String body) throws WxPayException {
+ switch (config.getApiVersion()) {
+ case API_VERSION_V2:
+ return doParseRefundNotifyV2(body);
+ case API_VERSION_V3:
+ return parseRefundNotifyV3(body);
+ default:
+ throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
+ }
+ }
+
+ private PayRefundRespDTO doParseRefundNotifyV2(String body) throws WxPayException {
+ // 1. 解析回调
+ WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body);
+ WxPayRefundNotifyResult.ReqInfo result = response.getReqInfo();
+ // 2. 构建结果
+ if (Objects.equals("SUCCESS", result.getRefundStatus())) {
+ return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV2B(result.getSuccessTime()),
+ result.getOutRefundNo(), response);
+ }
+ return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
+ }
+
+ private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException {
+ // 1. 解析回调
+ WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, null);
+ WxPayRefundNotifyV3Result.DecryptNotifyResult result = response.getResult();
+ // 2. 构建结果
+ if (Objects.equals("SUCCESS", result.getRefundStatus())) {
+ return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV3(result.getSuccessTime()),
+ result.getOutRefundNo(), response);
+ }
+ return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
+ }
+
+ @Override
+ public PayTransferRespDTO doParseTransferNotify(Map params, String body) throws WxPayException {
+ switch (config.getApiVersion()) {
+ case API_VERSION_V3:
+ return parseTransferNotifyV3(body);
+ case API_VERSION_V2:
+ throw new UnsupportedOperationException("V2 版本暂不支持,建议使用 V3 版本");
+ default:
+ throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
+ }
+ }
+
+ private PayTransferRespDTO parseTransferNotifyV3(String body) throws WxPayException {
+ // 1. 解析回调
+ // TODO @luchi:这个可以复用 wxjava 里的类么?
+ WxPayTransferPartnerNotifyV3Result response = client.baseParseOrderNotifyV3Result(body, null, WxPayTransferPartnerNotifyV3Result.class, WxPayTransferPartnerNotifyV3Result.TransferNotifyResult.class);
+ WxPayTransferPartnerNotifyV3Result.TransferNotifyResult result = response.getResult();
+ // 2. 构建结果
+ if (Objects.equals("FINISHED", result.getBatchStatus())) {
+ if (result.getFailNum() <= 0) {
+ return PayTransferRespDTO.successOf(result.getBatchId(), parseDateV3(result.getUpdateTime()),
+ result.getOutBatchNo(), response);
+ }
+ }
+ return PayTransferRespDTO.closedOf(result.getBatchStatus(), result.getCloseReason(), result.getOutBatchNo(), response);
+ }
+
+ @Override
+ protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws WxPayException {
+ try {
+ switch (config.getApiVersion()) {
+ case API_VERSION_V2:
+ return doGetRefundV2(outTradeNo, outRefundNo);
+ case API_VERSION_V3:
+ return doGetRefundV3(outTradeNo, outRefundNo);
+ default:
+ throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
+ }
+ } catch (WxPayException e) {
+ if (ObjectUtils.equalsAny(e.getErrCode(), "REFUNDNOTEXIST", "RESOURCE_NOT_EXISTS")) {
+ String errorCode = getErrorCode(e);
+ String errorMessage = getErrorMessage(e);
+ return PayRefundRespDTO.failureOf(errorCode, errorMessage,
+ outRefundNo, e.getXmlString());
+ }
+ throw e;
+ }
+ }
+
+ private PayRefundRespDTO doGetRefundV2(String outTradeNo, String outRefundNo) throws WxPayException {
+ // 1. 构建 WxPayRefundRequest 请求
+ WxPayRefundQueryRequest request = WxPayRefundQueryRequest.newBuilder()
+ .outTradeNo(outTradeNo)
+ .outRefundNo(outRefundNo)
+ .build();
+ // 2.1 执行请求
+ WxPayRefundQueryResult response = client.refundQuery(request);
+ // 2.2 创建返回结果
+ if (!Objects.equals("SUCCESS", response.getResultCode())) {
+ return PayRefundRespDTO.waitingOf(null,
+ outRefundNo, response);
+ }
+ WxPayRefundQueryResult.RefundRecord refund = CollUtil.findOne(response.getRefundRecords(),
+ record -> record.getOutRefundNo().equals(outRefundNo));
+ if (refund == null) {
+ return PayRefundRespDTO.failureOf(outRefundNo, response);
+ }
+ switch (refund.getRefundStatus()) {
+ case "SUCCESS":
+ return PayRefundRespDTO.successOf(refund.getRefundId(), parseDateV2B(refund.getRefundSuccessTime()),
+ outRefundNo, response);
+ case "PROCESSING":
+ return PayRefundRespDTO.waitingOf(refund.getRefundId(),
+ outRefundNo, response);
+ case "CHANGE": // 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,资金回流到商户的现金帐号,需要商户人工干预,通过线下或者财付通转账的方式进行退款
+ case "FAIL":
+ return PayRefundRespDTO.failureOf(outRefundNo, response);
+ default:
+ throw new IllegalArgumentException(String.format("未知的退款状态(%s)", refund.getRefundStatus()));
+ }
+ }
+
+ private PayRefundRespDTO doGetRefundV3(String outTradeNo, String outRefundNo) throws WxPayException {
+ // 1. 构建 WxPayRefundRequest 请求
+ WxPayRefundQueryV3Request request = new WxPayRefundQueryV3Request();
+ request.setOutRefundNo(outRefundNo);
+ // 2.1 执行请求
+ WxPayRefundQueryV3Result response = client.refundQueryV3(request);
+ // 2.2 创建返回结果
+ switch (response.getStatus()) {
+ case "SUCCESS":
+ return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()),
+ outRefundNo, response);
+ case "PROCESSING":
+ return PayRefundRespDTO.waitingOf(response.getRefundId(),
+ outRefundNo, response);
+ case "ABNORMAL": // 退款异常
+ case "CLOSED":
+ return PayRefundRespDTO.failureOf(outRefundNo, response);
+ default:
+ throw new IllegalArgumentException(String.format("未知的退款状态(%s)", response.getStatus()));
+ }
+ }
+
+ @Override
+ protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws WxPayException {
+ // 1. 构建 TransferBatchesRequest 请求
+ List transferDetailList = Collections.singletonList(
+ TransferBatchesRequest.TransferDetail.newBuilder()
+ .outDetailNo(reqDTO.getOutTransferNo())
+ .transferAmount(reqDTO.getPrice())
+ .transferRemark(reqDTO.getSubject())
+ .openid(reqDTO.getOpenid())
+ .build());
+ // TODO @luchi:能不能我们搞个 TransferBatchesRequestX extends TransferBatchesRequest,这样更简洁一点。
+ TransferBatchesRequest transferBatches = TransferBatchesRequest.newBuilder()
+ .appid(this.config.getAppId())
+ .outBatchNo(reqDTO.getOutTransferNo())
+ .batchName(reqDTO.getSubject())
+ .batchRemark(reqDTO.getSubject())
+ .totalAmount(reqDTO.getPrice())
+ .totalNum(transferDetailList.size())
+ .transferDetailList(transferDetailList).build()
+// .setNotifyUrl(reqDTO.getNotifyUrl())
+ ;
+ // 2.1 执行请求
+ TransferBatchesResult transferBatchesResult = client.getTransferService().transferBatches(transferBatches);
+ // 2.2 创建返回结果
+ return PayTransferRespDTO.dealingOf(transferBatchesResult.getBatchId(), reqDTO.getOutTransferNo(), transferBatchesResult);
+ }
+
+ @Override
+ protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) throws WxPayException {
+ QueryTransferBatchesRequest request = QueryTransferBatchesRequest.newBuilder()
+ .outBatchNo(outTradeNo).needQueryDetail(true).offset(0).limit(20).detailStatus("ALL")
+ .build();
+ QueryTransferBatchesResult response = client.getTransferService().transferBatchesOutBatchNo(request);
+ QueryTransferBatchesResult.TransferBatch transferBatch = response.getTransferBatch();
+ if (Objects.equals("FINISHED", transferBatch.getBatchStatus())) {
+ // 明细中全部成功则成功,任一失败则失败
+ if (response.getTransferDetailList().stream().allMatch(detail -> Objects.equals("SUCCESS", detail.getDetailStatus()))) {
+ return PayTransferRespDTO.successOf(transferBatch.getBatchId(), parseDateV3(transferBatch.getUpdateTime()),
+ transferBatch.getOutBatchNo(), response);
+ }
+ if (response.getTransferDetailList().stream().anyMatch(detail -> Objects.equals("FAIL", detail.getDetailStatus()))) {
+ return PayTransferRespDTO.closedOf(transferBatch.getBatchStatus(), transferBatch.getCloseReason(),
+ transferBatch.getOutBatchNo(), response);
+ }
+ }
+ if (Objects.equals("CLOSED", transferBatch.getBatchStatus())) {
+ return PayTransferRespDTO.closedOf(transferBatch.getBatchStatus(), transferBatch.getCloseReason(),
+ transferBatch.getOutBatchNo(), response);
+ }
+ return PayTransferRespDTO.dealingOf(transferBatch.getBatchId(), transferBatch.getOutBatchNo(), response);
+ }
+
+ // ========== 各种工具方法 ==========
+
+ static String formatDateV2(LocalDateTime time) {
+ return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN);
+ }
+
+ static LocalDateTime parseDateV2(String time) {
+ return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN);
+ }
+
+ static LocalDateTime parseDateV2B(String time) {
+ return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN);
+ }
+
+ static String formatDateV3(LocalDateTime time) {
+ return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN);
+ }
+
+ static LocalDateTime parseDateV3(String time) {
+ return LocalDateTimeUtil.parse(time, UTC_WITH_XXX_OFFSET_PATTERN);
+ }
+
+ static String getErrorCode(WxPayException e) {
+ if (StrUtil.isNotEmpty(e.getErrCode())) {
+ return e.getErrCode();
+ }
+ if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
+ return "CUSTOM_ERROR";
+ }
+ return e.getReturnCode();
+ }
+
+ static String getErrorMessage(WxPayException e) {
+ if (StrUtil.isNotEmpty(e.getErrCode())) {
+ return e.getErrCodeDes();
+ }
+ if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
+ return e.getCustomErrorMsg();
+ }
+ return e.getReturnMsg();
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxAppPayClient.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxAppPayClient.java
new file mode 100644
index 0000000..aa0eb68
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxAppPayClient.java
@@ -0,0 +1,64 @@
+package com.tashow.cloud.sdk.payment.client.impl.weixin;
+
+import com.github.binarywang.wxpay.bean.order.WxPayAppOrderResult;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
+import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
+import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
+import com.github.binarywang.wxpay.constant.WxPayConstants;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderRespDTO;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderUnifiedReqDTO;
+import com.tashow.cloud.sdk.payment.enums.channel.PayChannelEnum;
+import com.tashow.cloud.sdk.payment.enums.order.PayOrderDisplayModeEnum;
+import lombok.extern.slf4j.Slf4j;
+
+import static com.tashow.cloud.common.util.json.JsonUtils.toJsonString;
+
+
+/**
+ * 微信支付【App 支付】的 PayClient 实现类
+ *
+ * 文档:App 支付
+ *
+ * // TODO 芋艿:未详细测试,因为手头没 App
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class WxAppPayClient extends AbstractWxPayClient {
+
+ public WxAppPayClient(Long channelId, WxPayClientConfig config) {
+ super(channelId, PayChannelEnum.WX_APP.getCode(), config);
+ }
+
+ @Override
+ protected void doInit() {
+ super.doInit(WxPayConstants.TradeType.APP);
+ }
+
+ @Override
+ protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+ // 构建 WxPayUnifiedOrderRequest 对象
+ WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO);
+ // 执行请求
+ WxPayAppOrderResult response = client.createOrder(request);
+
+ // 转换结果
+ return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
+ reqDTO.getOutTradeNo(), response);
+ }
+
+ @Override
+ protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+ // 构建 WxPayUnifiedOrderV3Request 对象
+ WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO);
+ // 执行请求
+ WxPayUnifiedOrderV3Result.AppResult response = client.createOrderV3(TradeTypeEnum.APP, request);
+
+ // 转换结果
+ return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
+ reqDTO.getOutTradeNo(), response);
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxBarPayClient.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxBarPayClient.java
new file mode 100644
index 0000000..21c17e8
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxBarPayClient.java
@@ -0,0 +1,108 @@
+package com.tashow.cloud.sdk.payment.client.impl.weixin;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.thread.ThreadUtil;
+import cn.hutool.core.util.StrUtil;
+import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
+import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult;
+import com.github.binarywang.wxpay.constant.WxPayConstants;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.tashow.cloud.common.util.date.LocalDateTimeUtils;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderRespDTO;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderUnifiedReqDTO;
+import com.tashow.cloud.sdk.payment.enums.channel.PayChannelEnum;
+import com.tashow.cloud.sdk.payment.enums.order.PayOrderDisplayModeEnum;
+import lombok.extern.slf4j.Slf4j;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.concurrent.TimeUnit;
+
+import static com.tashow.cloud.common.exception.util.ServiceExceptionUtil.invalidParamException;
+import static com.tashow.cloud.common.util.json.JsonUtils.toJsonString;
+
+
+/**
+ * 微信支付【付款码支付】的 PayClient 实现类
+ *
+ * 文档:付款码支付
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class WxBarPayClient extends AbstractWxPayClient {
+
+ /**
+ * 微信付款码的过期时间
+ */
+ private static final Duration AUTH_CODE_EXPIRE = Duration.ofMinutes(3);
+
+ public WxBarPayClient(Long channelId, WxPayClientConfig config) {
+ super(channelId, PayChannelEnum.WX_BAR.getCode(), config);
+ }
+
+ @Override
+ protected void doInit() {
+ super.doInit(WxPayConstants.TradeType.MICROPAY);
+ }
+
+ @Override
+ protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+ // 由于付款码需要不断轮询,所以需要在较短的时间完成支付
+ LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE);
+ if (expireTime.isAfter(reqDTO.getExpireTime())) {
+ expireTime = reqDTO.getExpireTime();
+ }
+ // 构建 WxPayMicropayRequest 对象
+ WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder()
+ .outTradeNo(reqDTO.getOutTradeNo())
+ .body(reqDTO.getSubject())
+ .detail(reqDTO.getBody())
+ .totalFee(reqDTO.getPrice()) // 单位分
+ .timeExpire(formatDateV2(expireTime))
+ .spbillCreateIp(reqDTO.getUserIp())
+ .authCode(getAuthCode(reqDTO))
+ .build();
+ // 执行请求,重试直到失败(过期),或者成功
+ WxPayException lastWxPayException = null;
+ for (int i = 1; i < Byte.MAX_VALUE; i++) {
+ try {
+ WxPayMicropayResult response = client.micropay(request);
+ // 支付成功,例如说:1)用户输入了密码;2)用户免密支付
+ return PayOrderRespDTO.successOf(response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
+ response.getOutTradeNo(), response)
+ .setDisplayMode(PayOrderDisplayModeEnum.BAR_CODE.getMode());
+ } catch (WxPayException ex) {
+ lastWxPayException = ex;
+ // 如果不满足这 3 种任一的,则直接抛出 WxPayException 异常,不仅需处理
+ // 1. SYSTEMERROR:接口返回错误:请立即调用被扫订单结果查询API,查询当前订单状态,并根据订单的状态决定下一步的操作。
+ // 2. USERPAYING:用户支付中,需要输入密码:等待 5 秒,然后调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。
+ // 3. BANKERROR:银行系统异常:请立即调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。
+ if (!StrUtil.equalsAny(ex.getErrCode(), "SYSTEMERROR", "USERPAYING", "BANKERROR")) {
+ throw ex;
+ }
+ // 等待 5 秒,继续下一轮重新发起支付
+ log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i,
+ toJsonString(request), ex.getMessage());
+ ThreadUtil.sleep(5, TimeUnit.SECONDS);
+ }
+ }
+ throw lastWxPayException;
+ }
+
+ @Override
+ protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+ return doUnifiedOrderV2(reqDTO);
+ }
+
+ // ========== 各种工具方法 ==========
+
+ static String getAuthCode(PayOrderUnifiedReqDTO reqDTO) {
+ String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "authCode");
+ if (StrUtil.isEmpty(authCode)) {
+ throw invalidParamException("支付请求的 authCode 不能为空!");
+ }
+ return authCode;
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxLitePayClient.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxLitePayClient.java
new file mode 100644
index 0000000..0f764fb
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxLitePayClient.java
@@ -0,0 +1,22 @@
+package com.tashow.cloud.sdk.payment.client.impl.weixin;
+
+import com.tashow.cloud.sdk.payment.enums.channel.PayChannelEnum;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 微信支付【小程序】的 PayClient 实现类
+ *
+ * 由于公众号和小程序的微信支付逻辑一致,所以直接进行继承
+ *
+ * 文档:JSAPI 下单>
+ *
+ * @author zwy
+ */
+@Slf4j
+public class WxLitePayClient extends WxPubPayClient {
+
+ public WxLitePayClient(Long channelId, WxPayClientConfig config) {
+ super(channelId, PayChannelEnum.WX_LITE.getCode(), config);
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxNativePayClient.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxNativePayClient.java
new file mode 100644
index 0000000..55423fc
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxNativePayClient.java
@@ -0,0 +1,59 @@
+package com.tashow.cloud.sdk.payment.client.impl.weixin;
+
+import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
+import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
+import com.github.binarywang.wxpay.constant.WxPayConstants;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderRespDTO;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderUnifiedReqDTO;
+import com.tashow.cloud.sdk.payment.enums.channel.PayChannelEnum;
+import com.tashow.cloud.sdk.payment.enums.order.PayOrderDisplayModeEnum;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 微信支付【Native 二维码】的 PayClient 实现类
+ *
+ * 文档:Native 下单
+ *
+ * @author zwy
+ */
+@Slf4j
+public class WxNativePayClient extends AbstractWxPayClient {
+
+ public WxNativePayClient(Long channelId, WxPayClientConfig config) {
+ super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config);
+ }
+
+ @Override
+ protected void doInit() {
+ super.doInit(WxPayConstants.TradeType.NATIVE);
+ }
+
+ @Override
+ protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+ // 构建 WxPayUnifiedOrderRequest 对象
+ WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO)
+ .setProductId(reqDTO.getOutTradeNo()); // V2 必须传递 productId,无需在微信配置。该参数在 V3 简化,无需传递!
+ // 执行请求
+ WxPayNativeOrderResult response = client.createOrder(request);
+
+ // 转换结果
+ return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response.getCodeUrl(),
+ reqDTO.getOutTradeNo(), response);
+ }
+
+ @Override
+ protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+ // 构建 WxPayUnifiedOrderV3Request 对象
+ WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO);
+ // 执行请求
+ String response = client.createOrderV3(TradeTypeEnum.NATIVE, request);
+
+ // 转换结果
+ return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response,
+ reqDTO.getOutTradeNo(), response);
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxPayClientConfig.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxPayClientConfig.java
new file mode 100644
index 0000000..a83a6f1
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxPayClientConfig.java
@@ -0,0 +1,102 @@
+package com.tashow.cloud.sdk.payment.client.impl.weixin;
+
+import com.tashow.cloud.common.util.validation.ValidationUtils;
+import com.tashow.cloud.sdk.payment.client.PayClientConfig;
+import jakarta.validation.Validator;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+/**
+ * 微信支付的 PayClientConfig 实现类
+ * 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性
+ *
+ * @author 芋道源码
+ */
+@Data
+public class WxPayClientConfig implements PayClientConfig {
+
+ /**
+ * API 版本 - V2
+ *
+ * V2 协议说明
+ */
+ public static final String API_VERSION_V2 = "v2";
+ /**
+ * API 版本 - V3
+ *
+ * V3 协议说明
+ */
+ public static final String API_VERSION_V3 = "v3";
+
+ /**
+ * 公众号或者小程序的 appid
+ *
+ * 只有公众号或小程序需要该字段
+ */
+ @NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class})
+ private String appId;
+ /**
+ * 商户号
+ */
+ @NotBlank(message = "商户号不能为空", groups = {V2.class, V3.class})
+ private String mchId;
+ /**
+ * API 版本
+ */
+ @NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class})
+ private String apiVersion;
+
+ // ========== V2 版本的参数 ==========
+
+ /**
+ * 商户密钥
+ */
+ @NotBlank(message = "商户密钥不能为空", groups = V2.class)
+ private String mchKey;
+ /**
+ * apiclient_cert.p12 证书文件的对应字符串【base64 格式】
+ *
+ * 为什么采用 base64 格式?因为 p12 读取后是二进制,需要转换成 base64 格式才好传输和存储
+ */
+ @NotBlank(message = "apiclient_cert.p12 不能为空", groups = V2.class)
+ private String keyContent;
+
+ // ========== V3 版本的参数 ==========
+ /**
+ * apiclient_key.pem 证书文件的对应字符串
+ */
+ @NotBlank(message = "apiclient_key 不能为空", groups = V3.class)
+ private String privateKeyContent;
+ /**
+ * apiV3 密钥值
+ */
+ @NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class)
+ private String apiV3Key;
+ /**
+ * 证书序列号
+ */
+ @NotBlank(message = "证书序列号不能为空", groups = V3.class)
+ private String certSerialNo;
+
+ @Deprecated // TODO 芋艿:V2.3.0 进行移除
+ private String privateCertContent;
+
+ /**
+ * 分组校验 v2版本
+ */
+ public interface V2 {
+ }
+
+ /**
+ * 分组校验 v3版本
+ */
+ public interface V3 {
+ }
+
+ @Override
+ public void validate(Validator validator) {
+ ValidationUtils.validate(validator, this,
+ API_VERSION_V2.equals(this.getApiVersion()) ? V2.class : V3.class);
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxPubPayClient.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxPubPayClient.java
new file mode 100644
index 0000000..d694052
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxPubPayClient.java
@@ -0,0 +1,81 @@
+package com.tashow.cloud.sdk.payment.client.impl.weixin;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
+import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
+import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
+import com.github.binarywang.wxpay.constant.WxPayConstants;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderRespDTO;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderUnifiedReqDTO;
+import com.tashow.cloud.sdk.payment.enums.channel.PayChannelEnum;
+import com.tashow.cloud.sdk.payment.enums.order.PayOrderDisplayModeEnum;
+import lombok.extern.slf4j.Slf4j;
+
+import static com.tashow.cloud.common.exception.util.ServiceExceptionUtil.invalidParamException;
+import static com.tashow.cloud.common.util.json.JsonUtils.toJsonString;
+
+
+/**
+ * 微信支付(公众号)的 PayClient 实现类
+ *
+ * 文档:JSAPI 下单>
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class WxPubPayClient extends AbstractWxPayClient {
+
+ public WxPubPayClient(Long channelId, WxPayClientConfig config) {
+ super(channelId, PayChannelEnum.WX_PUB.getCode(), config);
+ }
+
+ protected WxPubPayClient(Long channelId, String channelCode, WxPayClientConfig config) {
+ super(channelId, channelCode, config);
+ }
+
+ @Override
+ protected void doInit() {
+ super.doInit(WxPayConstants.TradeType.JSAPI);
+ }
+
+ @Override
+ protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+ // 构建 WxPayUnifiedOrderRequest 对象
+ WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO)
+ .setOpenid(getOpenid(reqDTO));
+ // 执行请求
+ WxPayMpOrderResult response = client.createOrder(request);
+
+ // 转换结果
+ return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
+ reqDTO.getOutTradeNo(), response);
+ }
+
+ @Override
+ protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+ // 构建 WxPayUnifiedOrderRequest 对象
+ WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO)
+ .setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
+ // 执行请求
+ WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.JSAPI, request);
+
+ // 转换结果
+ return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
+ reqDTO.getOutTradeNo(), response);
+ }
+
+ // ========== 各种工具方法 ==========
+
+ static String getOpenid(PayOrderUnifiedReqDTO reqDTO) {
+ String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid");
+ if (StrUtil.isEmpty(openid)) {
+ throw invalidParamException("支付请求的 openid 不能为空!");
+ }
+ return openid;
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxWapPayClient.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxWapPayClient.java
new file mode 100644
index 0000000..731f377
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/weixin/WxWapPayClient.java
@@ -0,0 +1,62 @@
+package com.tashow.cloud.sdk.payment.client.impl.weixin;
+
+import com.github.binarywang.wxpay.bean.order.WxPayMwebOrderResult;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
+import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
+import com.github.binarywang.wxpay.constant.WxPayConstants;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderRespDTO;
+import com.tashow.cloud.sdk.payment.dto.order.PayOrderUnifiedReqDTO;
+import com.tashow.cloud.sdk.payment.enums.channel.PayChannelEnum;
+import com.tashow.cloud.sdk.payment.enums.order.PayOrderDisplayModeEnum;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 微信支付(H5 网页)的 PayClient 实现类
+ *
+ * 文档:H5下单API>
+ *
+ * @author YYQ
+ */
+@Slf4j
+public class WxWapPayClient extends AbstractWxPayClient {
+
+ public WxWapPayClient(Long channelId, WxPayClientConfig config) {
+ super(channelId, PayChannelEnum.WX_WAP.getCode(), config);
+ }
+
+ protected WxWapPayClient(Long channelId, String channelCode, WxPayClientConfig config) {
+ super(channelId, channelCode, config);
+ }
+
+ @Override
+ protected void doInit() {
+ super.doInit(WxPayConstants.TradeType.MWEB);
+ }
+
+ @Override
+ protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+ // 构建 WxPayUnifiedOrderRequest 对象
+ WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO);
+ // 执行请求
+ WxPayMwebOrderResult response = client.createOrder(request);
+
+ // 转换结果
+ return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.URL.getMode(), response.getMwebUrl(),
+ reqDTO.getOutTradeNo(), response);
+ }
+
+ @Override
+ protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+ // 构建 WxPayUnifiedOrderRequest 对象
+ WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO);
+ // 执行请求
+ String response = client.createOrderV3(TradeTypeEnum.H5, request);
+
+ // 转换结果
+ return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.URL.getMode(), response,
+ reqDTO.getOutTradeNo(), response);
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/order/PayOrderRespDTO.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/order/PayOrderRespDTO.java
new file mode 100644
index 0000000..8e9411b
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/order/PayOrderRespDTO.java
@@ -0,0 +1,141 @@
+package com.tashow.cloud.sdk.payment.dto.order;
+
+import com.tashow.cloud.sdk.payment.enums.order.PayOrderDisplayModeEnum;
+import com.tashow.cloud.sdk.payment.enums.order.PayOrderStatusRespEnum;
+import com.tashow.cloud.sdk.payment.exception.PayException;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 渠道支付订单 Response DTO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class PayOrderRespDTO {
+
+ /**
+ * 支付状态
+ *
+ * 枚举:{@link PayOrderStatusRespEnum}
+ */
+ private Integer status;
+
+ /**
+ * 外部订单号
+ *
+ * 对应 PayOrderExtensionDO 的 no 字段
+ */
+ private String outTradeNo;
+
+ /**
+ * 支付渠道编号
+ */
+ private String channelOrderNo;
+ /**
+ * 支付渠道用户编号
+ */
+ private String channelUserId;
+
+ /**
+ * 支付成功时间
+ */
+ private LocalDateTime successTime;
+
+ /**
+ * 原始的同步/异步通知结果
+ */
+ private Object rawData;
+
+ // ========== 主动发起支付时,会返回的字段 ==========
+
+ /**
+ * 展示模式
+ *
+ * 枚举 {@link PayOrderDisplayModeEnum} 类
+ */
+ private String displayMode;
+ /**
+ * 展示内容
+ */
+ private String displayContent;
+
+ /**
+ * 调用渠道的错误码
+ *
+ * 注意:这里返回的是业务异常,而是不系统异常。
+ * 如果是系统异常,则会抛出 {@link PayException}
+ */
+ private String channelErrorCode;
+ /**
+ * 调用渠道报错时,错误信息
+ */
+ private String channelErrorMsg;
+
+ public PayOrderRespDTO() {
+ }
+
+ /**
+ * 创建【WAITING】状态的订单返回
+ */
+ public static PayOrderRespDTO waitingOf(String displayMode, String displayContent,
+ String outTradeNo, Object rawData) {
+ PayOrderRespDTO respDTO = new PayOrderRespDTO();
+ respDTO.status = PayOrderStatusRespEnum.WAITING.getStatus();
+ respDTO.displayMode = displayMode;
+ respDTO.displayContent = displayContent;
+ // 相对通用的字段
+ respDTO.outTradeNo = outTradeNo;
+ respDTO.rawData = rawData;
+ return respDTO;
+ }
+
+ /**
+ * 创建【SUCCESS】状态的订单返回
+ */
+ public static PayOrderRespDTO successOf(String channelOrderNo, String channelUserId, LocalDateTime successTime,
+ String outTradeNo, Object rawData) {
+ PayOrderRespDTO respDTO = new PayOrderRespDTO();
+ respDTO.status = PayOrderStatusRespEnum.SUCCESS.getStatus();
+ respDTO.channelOrderNo = channelOrderNo;
+ respDTO.channelUserId = channelUserId;
+ respDTO.successTime = successTime;
+ // 相对通用的字段
+ respDTO.outTradeNo = outTradeNo;
+ respDTO.rawData = rawData;
+ return respDTO;
+ }
+
+ /**
+ * 创建指定状态的订单返回,适合支付渠道回调时
+ */
+ public static PayOrderRespDTO of(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime,
+ String outTradeNo, Object rawData) {
+ PayOrderRespDTO respDTO = new PayOrderRespDTO();
+ respDTO.status = status;
+ respDTO.channelOrderNo = channelOrderNo;
+ respDTO.channelUserId = channelUserId;
+ respDTO.successTime = successTime;
+ // 相对通用的字段
+ respDTO.outTradeNo = outTradeNo;
+ respDTO.rawData = rawData;
+ return respDTO;
+ }
+
+ /**
+ * 创建【CLOSED】状态的订单返回,适合调用支付渠道失败时
+ */
+ public static PayOrderRespDTO closedOf(String channelErrorCode, String channelErrorMsg,
+ String outTradeNo, Object rawData) {
+ PayOrderRespDTO respDTO = new PayOrderRespDTO();
+ respDTO.status = PayOrderStatusRespEnum.CLOSED.getStatus();
+ respDTO.channelErrorCode = channelErrorCode;
+ respDTO.channelErrorMsg = channelErrorMsg;
+ // 相对通用的字段
+ respDTO.outTradeNo = outTradeNo;
+ respDTO.rawData = rawData;
+ return respDTO;
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/order/PayOrderUnifiedReqDTO.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/order/PayOrderUnifiedReqDTO.java
new file mode 100644
index 0000000..9d400a6
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/order/PayOrderUnifiedReqDTO.java
@@ -0,0 +1,92 @@
+package com.tashow.cloud.sdk.payment.dto.order;
+
+import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
+import jakarta.validation.constraints.DecimalMin;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+import org.hibernate.validator.constraints.URL;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+/**
+ * 统一下单 Request DTO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class PayOrderUnifiedReqDTO {
+
+ /**
+ * 用户 IP
+ */
+ @NotEmpty(message = "用户 IP 不能为空")
+ private String userIp;
+
+ // ========== 商户相关字段 ==========
+
+ /**
+ * 外部订单号
+ *
+ * 对应 PayOrderExtensionDO 的 no 字段
+ */
+ @NotEmpty(message = "外部订单编号不能为空")
+ private String outTradeNo;
+ /**
+ * 商品标题
+ */
+ @NotEmpty(message = "商品标题不能为空")
+ @Length(max = 32, message = "商品标题不能超过 32")
+ private String subject;
+ /**
+ * 商品描述信息
+ */
+ @Length(max = 128, message = "商品描述信息长度不能超过128")
+ private String body;
+ /**
+ * 支付结果的 notify 回调地址
+ */
+ @NotEmpty(message = "支付结果的回调地址不能为空")
+ @URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
+ private String notifyUrl;
+ /**
+ * 支付结果的 return 回调地址
+ */
+ @URL(message = "支付结果的 return 回调地址必须是 URL 格式")
+ private String returnUrl;
+
+ // ========== 订单相关字段 ==========
+
+ /**
+ * 支付金额,单位:分
+ */
+ @NotNull(message = "支付金额不能为空")
+ @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
+ private Integer price;
+
+ /**
+ * 支付过期时间
+ */
+ @NotNull(message = "支付过期时间不能为空")
+ private LocalDateTime expireTime;
+
+ // ========== 拓展参数 ==========
+ /**
+ * 支付渠道的额外参数
+ *
+ * 例如说,微信公众号需要传递 openid 参数
+ */
+ private Map channelExtras;
+
+ /**
+ * 展示模式
+ *
+ * 如果不传递,则每个支付渠道使用默认的方式
+ *
+ * 枚举 {@link PayOrderDisplayModeEnum}
+ */
+ private String displayMode;
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/refund/PayRefundRespDTO.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/refund/PayRefundRespDTO.java
new file mode 100644
index 0000000..5c0bb77
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/refund/PayRefundRespDTO.java
@@ -0,0 +1,115 @@
+package com.tashow.cloud.sdk.payment.dto.refund;
+
+import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
+import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 渠道退款订单 Response DTO
+ *
+ * @author jason
+ */
+@Data
+public class PayRefundRespDTO {
+
+ /**
+ * 退款状态
+ *
+ * 枚举 {@link PayRefundStatusRespEnum}
+ */
+ private Integer status;
+
+ /**
+ * 外部退款号
+ *
+ * 对应 PayRefundDO 的 no 字段
+ */
+ private String outRefundNo;
+
+ /**
+ * 渠道退款单号
+ *
+ * 对应 PayRefundDO.channelRefundNo 字段
+ */
+ private String channelRefundNo;
+
+ /**
+ * 退款成功时间
+ */
+ private LocalDateTime successTime;
+
+ /**
+ * 原始的异步通知结果
+ */
+ private Object rawData;
+
+ /**
+ * 调用渠道的错误码
+ *
+ * 注意:这里返回的是业务异常,而是不系统异常。
+ * 如果是系统异常,则会抛出 {@link PayException}
+ */
+ private String channelErrorCode;
+ /**
+ * 调用渠道报错时,错误信息
+ */
+ private String channelErrorMsg;
+
+ private PayRefundRespDTO() {
+ }
+
+ /**
+ * 创建【WAITING】状态的退款返回
+ */
+ public static PayRefundRespDTO waitingOf(String channelRefundNo,
+ String outRefundNo, Object rawData) {
+ PayRefundRespDTO respDTO = new PayRefundRespDTO();
+ respDTO.status = PayRefundStatusRespEnum.WAITING.getStatus();
+ respDTO.channelRefundNo = channelRefundNo;
+ // 相对通用的字段
+ respDTO.outRefundNo = outRefundNo;
+ respDTO.rawData = rawData;
+ return respDTO;
+ }
+
+ /**
+ * 创建【SUCCESS】状态的退款返回
+ */
+ public static PayRefundRespDTO successOf(String channelRefundNo, LocalDateTime successTime,
+ String outRefundNo, Object rawData) {
+ PayRefundRespDTO respDTO = new PayRefundRespDTO();
+ respDTO.status = PayRefundStatusRespEnum.SUCCESS.getStatus();
+ respDTO.channelRefundNo = channelRefundNo;
+ respDTO.successTime = successTime;
+ // 相对通用的字段
+ respDTO.outRefundNo = outRefundNo;
+ respDTO.rawData = rawData;
+ return respDTO;
+ }
+
+ /**
+ * 创建【FAILURE】状态的退款返回
+ */
+ public static PayRefundRespDTO failureOf(String outRefundNo, Object rawData) {
+ return failureOf(null, null,
+ outRefundNo, rawData);
+ }
+
+ /**
+ * 创建【FAILURE】状态的退款返回
+ */
+ public static PayRefundRespDTO failureOf(String channelErrorCode, String channelErrorMsg,
+ String outRefundNo, Object rawData) {
+ PayRefundRespDTO respDTO = new PayRefundRespDTO();
+ respDTO.status = PayRefundStatusRespEnum.FAILURE.getStatus();
+ respDTO.channelErrorCode = channelErrorCode;
+ respDTO.channelErrorMsg = channelErrorMsg;
+ // 相对通用的字段
+ respDTO.outRefundNo = outRefundNo;
+ respDTO.rawData = rawData;
+ return respDTO;
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/refund/PayRefundUnifiedReqDTO.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/refund/PayRefundUnifiedReqDTO.java
new file mode 100644
index 0000000..81f6699
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/refund/PayRefundUnifiedReqDTO.java
@@ -0,0 +1,69 @@
+package com.tashow.cloud.sdk.payment.dto.refund;
+
+import jakarta.validation.constraints.DecimalMin;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.hibernate.validator.constraints.URL;
+
+/**
+ * 统一 退款 Request DTO
+ *
+ * @author jason
+ */
+@Accessors(chain = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+public class PayRefundUnifiedReqDTO {
+
+ /**
+ * 外部订单号
+ *
+ * 对应 PayOrderExtensionDO 的 no 字段
+ */
+ @NotEmpty(message = "外部订单编号不能为空")
+ private String outTradeNo;
+
+ /**
+ * 外部退款号
+ *
+ * 对应 PayRefundDO 的 no 字段
+ */
+ @NotEmpty(message = "退款请求单号不能为空")
+ private String outRefundNo;
+
+ /**
+ * 退款原因
+ */
+ @NotEmpty(message = "退款原因不能为空")
+ private String reason;
+
+ /**
+ * 支付金额,单位:分
+ *
+ * 目前微信支付在退款的时候,必须传递该字段
+ */
+ @NotNull(message = "支付金额不能为空")
+ @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
+ private Integer payPrice;
+ /**
+ * 退款金额,单位:分
+ */
+ @NotNull(message = "退款金额不能为空")
+ @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
+ private Integer refundPrice;
+
+ /**
+ * 退款结果的 notify 回调地址
+ */
+ @NotEmpty(message = "支付结果的回调地址不能为空")
+ @URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
+ private String notifyUrl;
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/transfer/PayTransferRespDTO.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/transfer/PayTransferRespDTO.java
new file mode 100644
index 0000000..0b65d31
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/transfer/PayTransferRespDTO.java
@@ -0,0 +1,109 @@
+package com.tashow.cloud.sdk.payment.dto.transfer;
+
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 统一转账 Response DTO
+ *
+ * @author jason
+ */
+@Data
+public class PayTransferRespDTO {
+
+ /**
+ * 转账状态
+ *
+ * 关联 {@link PayTransferStatusRespEnum#getStatus()}
+ */
+ private Integer status;
+
+ /**
+ * 外部转账单号
+ *
+ */
+ private String outTransferNo;
+
+ /**
+ * 支付渠道编号
+ */
+ private String channelTransferNo;
+
+ /**
+ * 支付成功时间
+ */
+ private LocalDateTime successTime;
+
+ /**
+ * 原始的返回结果
+ */
+ private Object rawData;
+
+ /**
+ * 调用渠道的错误码
+ */
+ private String channelErrorCode;
+ /**
+ * 调用渠道报错时,错误信息
+ */
+ private String channelErrorMsg;
+
+ /**
+ * 创建【WAITING】状态的转账返回
+ */
+ public static PayTransferRespDTO waitingOf(String channelTransferNo,
+ String outTransferNo, Object rawData) {
+ PayTransferRespDTO respDTO = new PayTransferRespDTO();
+ respDTO.status = PayTransferStatusRespEnum.WAITING.getStatus();
+ respDTO.channelTransferNo = channelTransferNo;
+ respDTO.outTransferNo = outTransferNo;
+ respDTO.rawData = rawData;
+ return respDTO;
+ }
+
+ /**
+ * 创建【IN_PROGRESS】状态的转账返回
+ */
+ public static PayTransferRespDTO dealingOf(String channelTransferNo,
+ String outTransferNo, Object rawData) {
+ PayTransferRespDTO respDTO = new PayTransferRespDTO();
+ respDTO.status = PayTransferStatusRespEnum.IN_PROGRESS.getStatus();
+ respDTO.channelTransferNo = channelTransferNo;
+ respDTO.outTransferNo = outTransferNo;
+ respDTO.rawData = rawData;
+ return respDTO;
+ }
+
+ /**
+ * 创建【CLOSED】状态的转账返回
+ */
+ public static PayTransferRespDTO closedOf(String channelErrorCode, String channelErrorMsg,
+ String outTransferNo, Object rawData) {
+ PayTransferRespDTO respDTO = new PayTransferRespDTO();
+ respDTO.status = PayTransferStatusRespEnum.CLOSED.getStatus();
+ respDTO.channelErrorCode = channelErrorCode;
+ respDTO.channelErrorMsg = channelErrorMsg;
+ // 相对通用的字段
+ respDTO.outTransferNo = outTransferNo;
+ respDTO.rawData = rawData;
+ return respDTO;
+ }
+
+ /**
+ * 创建【SUCCESS】状态的转账返回
+ */
+ public static PayTransferRespDTO successOf(String channelTransferNo, LocalDateTime successTime,
+ String outTransferNo, Object rawData) {
+ PayTransferRespDTO respDTO = new PayTransferRespDTO();
+ respDTO.status = PayTransferStatusRespEnum.SUCCESS.getStatus();
+ respDTO.channelTransferNo = channelTransferNo;
+ respDTO.successTime = successTime;
+ // 相对通用的字段
+ respDTO.outTransferNo = outTransferNo;
+ respDTO.rawData = rawData;
+ return respDTO;
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/transfer/PayTransferUnifiedReqDTO.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/transfer/PayTransferUnifiedReqDTO.java
new file mode 100644
index 0000000..31c3244
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/transfer/PayTransferUnifiedReqDTO.java
@@ -0,0 +1,87 @@
+package com.tashow.cloud.sdk.payment.dto.transfer;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+import org.hibernate.validator.constraints.URL;
+
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum.*;
+
+/**
+ * 统一转账 Request DTO
+ *
+ * @author jason
+ */
+@Data
+public class PayTransferUnifiedReqDTO {
+
+ /**
+ * 转账类型
+ *
+ * 关联 {@link PayTransferTypeEnum#getType()}
+ */
+ @NotNull(message = "转账类型不能为空")
+ @InEnum(PayTransferTypeEnum.class)
+ private Integer type;
+
+ /**
+ * 用户 IP
+ */
+ @NotEmpty(message = "用户 IP 不能为空")
+ private String userIp;
+
+ @NotEmpty(message = "外部转账单编号不能为空")
+ private String outTransferNo;
+
+ /**
+ * 转账金额,单位:分
+ */
+ @NotNull(message = "转账金额不能为空")
+ @Min(value = 1, message = "转账金额必须大于零")
+ private Integer price;
+
+ /**
+ * 转账标题
+ */
+ @NotEmpty(message = "转账标题不能为空")
+ @Length(max = 128, message = "转账标题不能超过 128")
+ private String subject;
+
+ /**
+ * 收款人姓名
+ */
+ @NotBlank(message = "收款人姓名不能为空", groups = {Alipay.class})
+ private String userName;
+
+ /**
+ * 支付宝登录号
+ */
+ @NotBlank(message = "支付宝登录号不能为空", groups = {Alipay.class})
+ private String alipayLogonId;
+
+ /**
+ * 微信 openId
+ */
+ @NotBlank(message = "微信 openId 不能为空", groups = {WxPay.class})
+ private String openid;
+
+ /**
+ * 支付渠道的额外参数
+ */
+ private Map channelExtras;
+
+ /**
+ * 转账结果的 notify 回调地址
+ */
+ @NotEmpty(message = "转账结果的回调地址不能为空")
+ @URL(message = "转账结果的 notify 回调地址必须是 URL 格式")
+ private String notifyUrl;
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/transfer/WxPayTransferPartnerNotifyV3Result.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/transfer/WxPayTransferPartnerNotifyV3Result.java
new file mode 100644
index 0000000..b9e6fbb
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/dto/transfer/WxPayTransferPartnerNotifyV3Result.java
@@ -0,0 +1,129 @@
+package com.tashow.cloud.sdk.payment.dto.transfer;
+
+import com.github.binarywang.wxpay.bean.notify.OriginNotifyResponse;
+import com.github.binarywang.wxpay.bean.notify.WxPayBaseNotifyV3Result;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+// TODO @luchi:这个可以复用 wxjava 里的类么?
+@NoArgsConstructor
+public class WxPayTransferPartnerNotifyV3Result implements Serializable, WxPayBaseNotifyV3Result {
+
+ private static final long serialVersionUID = -1L;
+
+ /**
+ * 源数据
+ */
+ private OriginNotifyResponse rawData;
+
+ /**
+ * 解密后的数据
+ */
+ private TransferNotifyResult result;
+
+ @Override
+ public void setRawData(OriginNotifyResponse rawData) {
+ this.rawData = rawData;
+ }
+
+ @Override
+ public void setResult(TransferNotifyResult data) {
+ this.result = data;
+ }
+
+ public TransferNotifyResult getResult() {
+ return result;
+ }
+
+ public OriginNotifyResponse getRawData() {
+ return rawData;
+ }
+
+ @Data
+ @NoArgsConstructor
+ public static class TransferNotifyResult implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /*********************** 公共字段 ********************
+
+ /**
+ * 商家批次单号
+ */
+ @SerializedName(value = "out_batch_no")
+ protected String outBatchNo;
+
+ /**
+ * 微信批次单号
+ */
+ @SerializedName(value = "batch_id")
+ protected String batchId;
+
+ /**
+ * 批次状态
+ */
+ @SerializedName(value = "batch_status")
+ protected String batchStatus;
+
+ /**
+ * 批次总笔数
+ */
+ @SerializedName(value = "total_num")
+ protected Integer totalNum;
+
+ /**
+ * 批次总金额
+ */
+ @SerializedName(value = "total_amount")
+ protected Integer totalAmount;
+
+ /**
+ * 批次更新时间
+ */
+ @SerializedName(value = "update_time")
+ private String updateTime;
+
+ /*********************** FINISHED ********************
+
+ /**
+ * 转账成功金额
+ */
+ @SerializedName(value = "success_amount")
+ protected Integer successAmount;
+
+ /**
+ * 转账成功笔数
+ */
+ @SerializedName(value = "success_num")
+ protected Integer successNum;
+
+ /**
+ * 转账失败金额
+ */
+ @SerializedName(value = "fail_amount")
+ protected Integer failAmount;
+
+ /**
+ * 转账失败笔数
+ */
+ @SerializedName(value = "fail_num")
+ protected Integer failNum;
+
+ /*********************** CLOSED ********************
+
+ /**
+ * 商户号
+ */
+ @SerializedName(value = "mchid")
+ protected String mchId;
+
+ /**
+ * 批次关闭原因
+ */
+ @SerializedName(value = "close_reason")
+ protected String closeReason;
+
+ }
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/channel/PayChannelEnum.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/channel/PayChannelEnum.java
new file mode 100644
index 0000000..4b6dd75
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/channel/PayChannelEnum.java
@@ -0,0 +1,69 @@
+package com.tashow.cloud.sdk.payment.enums.channel;
+
+import cn.hutool.core.util.ArrayUtil;
+import com.tashow.cloud.sdk.payment.client.PayClientConfig;
+import com.tashow.cloud.sdk.payment.client.impl.NonePayClientConfig;
+import com.tashow.cloud.sdk.payment.client.impl.alipay.AlipayPayClientConfig;
+import com.tashow.cloud.sdk.payment.client.impl.weixin.WxPayClientConfig;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 支付渠道的编码的枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum PayChannelEnum {
+
+ WX_PUB("wx_pub", "微信 JSAPI 支付", WxPayClientConfig.class), // 公众号网页
+ WX_LITE("wx_lite", "微信小程序支付", WxPayClientConfig.class),
+ WX_APP("wx_app", "微信 App 支付", WxPayClientConfig.class),
+ WX_NATIVE("wx_native", "微信 Native 支付", WxPayClientConfig.class),
+ WX_WAP("wx_wap", "微信 Wap 网站支付", WxPayClientConfig.class), // H5 网页
+ WX_BAR("wx_bar", "微信付款码支付", WxPayClientConfig.class),
+
+ ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付", AlipayPayClientConfig.class),
+ ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付", AlipayPayClientConfig.class),
+ ALIPAY_APP("alipay_app", "支付宝App 支付", AlipayPayClientConfig.class),
+ ALIPAY_QR("alipay_qr", "支付宝扫码支付", AlipayPayClientConfig.class),
+ ALIPAY_BAR("alipay_bar", "支付宝条码支付", AlipayPayClientConfig.class),
+ MOCK("mock", "模拟支付", NonePayClientConfig.class),
+
+ WALLET("wallet", "钱包支付", NonePayClientConfig.class);
+
+ /**
+ * 编码
+ *
+ * 参考 支付渠道属性值
+ */
+ private final String code;
+ /**
+ * 名字
+ */
+ private final String name;
+
+ /**
+ * 配置类
+ */
+ private final Class extends PayClientConfig> configClass;
+
+ /**
+ * 微信支付
+ */
+ public static final String WECHAT = "WECHAT";
+
+ /**
+ * 支付宝支付
+ */
+ public static final String ALIPAY = "ALIPAY";
+
+ public static PayChannelEnum getByCode(String code) {
+ return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values());
+ }
+
+ public static boolean isAlipay(String channelCode) {
+ return channelCode != null && channelCode.startsWith("alipay");
+ }
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/order/PayOrderDisplayModeEnum.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/order/PayOrderDisplayModeEnum.java
new file mode 100644
index 0000000..de53219
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/order/PayOrderDisplayModeEnum.java
@@ -0,0 +1,29 @@
+package com.tashow.cloud.sdk.payment.enums.order;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 支付 UI 展示模式
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum PayOrderDisplayModeEnum {
+
+ URL("url"), // Redirect 跳转链接的方式
+ IFRAME("iframe"), // IFrame 内嵌链接的方式【目前暂时用不到】
+ FORM("form"), // HTML 表单提交
+ QR_CODE("qr_code"), // 二维码的文字内容
+ QR_CODE_URL("qr_code_url"), // 二维码的图片链接
+ BAR_CODE("bar_code"), // 条形码
+ APP("app"), // 应用:Android、iOS、微信小程序、微信公众号等,需要做自定义处理的
+ ;
+
+ /**
+ * 展示模式
+ */
+ private final String mode;
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/order/PayOrderStatusRespEnum.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/order/PayOrderStatusRespEnum.java
new file mode 100644
index 0000000..4cd1c2f
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/order/PayOrderStatusRespEnum.java
@@ -0,0 +1,56 @@
+package com.tashow.cloud.sdk.payment.enums.order;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Objects;
+
+/**
+ * 渠道的支付状态枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum PayOrderStatusRespEnum {
+
+ WAITING(0, "未支付"),
+ SUCCESS(10, "支付成功"),
+ REFUND(20, "已退款"),
+ CLOSED(30, "支付关闭"),
+ ;
+
+ private final Integer status;
+ private final String name;
+
+ /**
+ * 判断是否支付成功
+ *
+ * @param status 状态
+ * @return 是否支付成功
+ */
+ public static boolean isSuccess(Integer status) {
+ return Objects.equals(status, SUCCESS.getStatus());
+ }
+
+ /**
+ * 判断是否已退款
+ *
+ * @param status 状态
+ * @return 是否支付成功
+ */
+ public static boolean isRefund(Integer status) {
+ return Objects.equals(status, REFUND.getStatus());
+ }
+
+ /**
+ * 判断是否支付关闭
+ *
+ * @param status 状态
+ * @return 是否支付关闭
+ */
+ public static boolean isClosed(Integer status) {
+ return Objects.equals(status, CLOSED.getStatus());
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/refund/PayRefundStatusRespEnum.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/refund/PayRefundStatusRespEnum.java
new file mode 100644
index 0000000..e6648cb
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/refund/PayRefundStatusRespEnum.java
@@ -0,0 +1,32 @@
+package com.tashow.cloud.sdk.payment.enums.refund;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Objects;
+
+/**
+ * 渠道的退款状态枚举
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum PayRefundStatusRespEnum {
+
+ WAITING(0, "等待退款"),
+ SUCCESS(10, "退款成功"),
+ FAILURE(20, "退款失败");
+
+ private final Integer status;
+ private final String name;
+
+ public static boolean isSuccess(Integer status) {
+ return Objects.equals(status, SUCCESS.getStatus());
+ }
+
+ public static boolean isFailure(Integer status) {
+ return Objects.equals(status, FAILURE.getStatus());
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/transfer/PayTransferStatusRespEnum.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/transfer/PayTransferStatusRespEnum.java
new file mode 100644
index 0000000..c1ce4df
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/transfer/PayTransferStatusRespEnum.java
@@ -0,0 +1,45 @@
+package com.tashow.cloud.sdk.payment.enums.transfer;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Objects;
+
+/**
+ * 渠道的转账状态枚举
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum PayTransferStatusRespEnum {
+
+ WAITING(0, "等待转账"),
+
+ /**
+ * TODO 转账到银行卡. 会有T+0 T+1 到账的请情况。 还未实现
+ * TODO @jason:可以看看其它开源项目,针对这个场景,处理策略是怎么样的?例如说,每天主动轮询?这个状态的单子?
+ */
+ IN_PROGRESS(10, "转账进行中"),
+
+ SUCCESS(20, "转账成功"),
+ /**
+ * 转账关闭 (失败,或者其它情况)
+ */
+ CLOSED(30, "转账关闭");
+
+ private final Integer status;
+ private final String name;
+
+ public static boolean isSuccess(Integer status) {
+ return Objects.equals(status, SUCCESS.getStatus());
+ }
+
+ public static boolean isClosed(Integer status) {
+ return Objects.equals(status, CLOSED.getStatus());
+ }
+
+ public static boolean isInProgress(Integer status) {
+ return Objects.equals(status, IN_PROGRESS.getStatus());
+ }
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/transfer/PayTransferTypeEnum.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/transfer/PayTransferTypeEnum.java
new file mode 100644
index 0000000..0e5513a
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/transfer/PayTransferTypeEnum.java
@@ -0,0 +1,44 @@
+package com.tashow.cloud.sdk.payment.enums.transfer;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 转账类型枚举
+ *
+ * @author jason
+ */
+@AllArgsConstructor
+@Getter
+public enum PayTransferTypeEnum implements ArrayValuable {
+
+ ALIPAY_BALANCE(1, "支付宝余额"),
+ WX_BALANCE(2, "微信余额"),
+ BANK_CARD(3, "银行卡"),
+ WALLET_BALANCE(4, "钱包余额");
+
+ public interface WxPay {
+ }
+
+ public interface Alipay {
+ }
+
+ private final Integer type;
+ private final String name;
+
+ public static final Integer[] ARRAYS = Arrays.stream(values()).map(PayTransferTypeEnum::getType).toArray(Integer[]::new);
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+ public static PayTransferTypeEnum typeOf(Integer type) {
+ return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/exception/PayException.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/exception/PayException.java
new file mode 100644
index 0000000..a6cfc6d
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/exception/PayException.java
@@ -0,0 +1,17 @@
+package com.tashow.cloud.sdk.payment.exception;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 支付系统异常 Exception
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class PayException extends RuntimeException {
+
+ public PayException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/package-info.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/package-info.java
new file mode 100644
index 0000000..5b4fda8
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/package-info.java
@@ -0,0 +1 @@
+package com.tashow.cloud.sdk.payment;
\ No newline at end of file
diff --git a/tashow-sdk/tashow-sdk-payment/src/test/java/com/tashow/cloud/AppTest.java b/tashow-sdk/tashow-sdk-payment/src/test/java/com/tashow/cloud/AppTest.java
new file mode 100644
index 0000000..255defd
--- /dev/null
+++ b/tashow-sdk/tashow-sdk-payment/src/test/java/com/tashow/cloud/AppTest.java
@@ -0,0 +1,38 @@
+package com.tashow.cloud;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+/**
+ * Unit test for simple App.
+ */
+public class AppTest
+ extends TestCase
+{
+ /**
+ * Create the test case
+ *
+ * @param testName name of the test case
+ */
+ public AppTest( String testName )
+ {
+ super( testName );
+ }
+
+ /**
+ * @return the suite of tests being tested
+ */
+ public static Test suite()
+ {
+ return new TestSuite( AppTest.class );
+ }
+
+ /**
+ * Rigourous Test :-)
+ */
+ public void testApp()
+ {
+ assertTrue( true );
+ }
+}
From 2955d028efefda619e3015be91db3952fb2018cd Mon Sep 17 00:00:00 2001
From: liwq <122639653@qq.com>
Date: Fri, 23 May 2025 18:21:04 +0800
Subject: [PATCH 2/2] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=A1=86=E6=9E=B6?=
=?UTF-8?q?=EF=BC=8C=E5=88=A0=E9=99=A4=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84?=
=?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=8C=E6=B7=BB=E5=8A=A0sdk=E6=A8=A1?=
=?UTF-8?q?=E5=9D=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../sdk/payment/client/PayClientFactory.java | 3 +-
.../client/impl/NonePayClientConfig.java | 30 -------------------
.../payment/enums/channel/PayChannelEnum.java | 5 +---
3 files changed, 2 insertions(+), 36 deletions(-)
delete mode 100644 tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/NonePayClientConfig.java
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/PayClientFactory.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/PayClientFactory.java
index ec22796..57a1093 100644
--- a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/PayClientFactory.java
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/PayClientFactory.java
@@ -25,8 +25,7 @@ public interface PayClientFactory {
* @param config 支付配置
* @return 支付客户端
*/
- PayClient createOrUpdatePayClient(Long channelId, String channelCode,
- Config config);
+ PayClient createOrUpdatePayClient(Long channelId, String channelCode, Config config);
/**
* 注册支付客户端 Class,用于模块中实现的 PayClient
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/NonePayClientConfig.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/NonePayClientConfig.java
deleted file mode 100644
index 6396db0..0000000
--- a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/client/impl/NonePayClientConfig.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.tashow.cloud.sdk.payment.client.impl;
-
-import com.tashow.cloud.sdk.payment.client.PayClientConfig;
-import jakarta.validation.Validator;
-import lombok.Data;
-
-/**
- * 无需任何配置 PayClientConfig 实现类
- *
- * @author jason
- */
-@Data
-public class NonePayClientConfig implements PayClientConfig {
-
- /**
- * 配置名称
- *
- * 如果不加任何属性,JsonUtils.parseObject2 解析会报错,所以暂时加个名称
- */
- private String name;
-
- public NonePayClientConfig(){
- this.name = "none-config";
- }
-
- @Override
- public void validate(Validator validator) {
- // 无任何配置不需要校验
- }
-}
diff --git a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/channel/PayChannelEnum.java b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/channel/PayChannelEnum.java
index 4b6dd75..efb0eff 100644
--- a/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/channel/PayChannelEnum.java
+++ b/tashow-sdk/tashow-sdk-payment/src/main/java/com/tashow/cloud/sdk/payment/enums/channel/PayChannelEnum.java
@@ -2,7 +2,6 @@ package com.tashow.cloud.sdk.payment.enums.channel;
import cn.hutool.core.util.ArrayUtil;
import com.tashow.cloud.sdk.payment.client.PayClientConfig;
-import com.tashow.cloud.sdk.payment.client.impl.NonePayClientConfig;
import com.tashow.cloud.sdk.payment.client.impl.alipay.AlipayPayClientConfig;
import com.tashow.cloud.sdk.payment.client.impl.weixin.WxPayClientConfig;
import lombok.AllArgsConstructor;
@@ -29,9 +28,7 @@ public enum PayChannelEnum {
ALIPAY_APP("alipay_app", "支付宝App 支付", AlipayPayClientConfig.class),
ALIPAY_QR("alipay_qr", "支付宝扫码支付", AlipayPayClientConfig.class),
ALIPAY_BAR("alipay_bar", "支付宝条码支付", AlipayPayClientConfig.class),
- MOCK("mock", "模拟支付", NonePayClientConfig.class),
-
- WALLET("wallet", "钱包支付", NonePayClientConfig.class);
+ ;
/**
* 编码