Commit 15313c2992258c248433b73b6d921189ad775d4f
1 parent
09d45e63
转账 - 增加转账定时任务
Showing
15 changed files
with
313 additions
and
82 deletions
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
| ... | ... | @@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; |
| 6 | 6 | import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; |
| 7 | 7 | import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO; |
| 8 | 8 | import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; |
| 9 | +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; | |
| 9 | 10 | |
| 10 | 11 | import java.util.Map; |
| 11 | 12 | |
| ... | ... | @@ -86,4 +87,12 @@ public interface PayClient { |
| 86 | 87 | */ |
| 87 | 88 | PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO); |
| 88 | 89 | |
| 90 | + /** | |
| 91 | + * 获得转账订单信息 | |
| 92 | + * | |
| 93 | + * @param outTradeNo 外部订单号 | |
| 94 | + * @param type 转账类型 | |
| 95 | + * @return 转账信息 | |
| 96 | + */ | |
| 97 | + PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type); | |
| 89 | 98 | } | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java
| ... | ... | @@ -53,11 +53,24 @@ public class PayTransferRespDTO { |
| 53 | 53 | /** |
| 54 | 54 | * 创建【WAITING】状态的转账返回 |
| 55 | 55 | */ |
| 56 | - public static PayTransferRespDTO waitingOf(String channelOrderNo, | |
| 56 | + public static PayTransferRespDTO waitingOf(String channelTransferNo, | |
| 57 | 57 | String outTransferNo, Object rawData) { |
| 58 | 58 | PayTransferRespDTO respDTO = new PayTransferRespDTO(); |
| 59 | 59 | respDTO.status = PayTransferStatusRespEnum.WAITING.getStatus(); |
| 60 | - respDTO.channelTransferNo = channelOrderNo; | |
| 60 | + respDTO.channelTransferNo = channelTransferNo; | |
| 61 | + respDTO.outTransferNo = outTransferNo; | |
| 62 | + respDTO.rawData = rawData; | |
| 63 | + return respDTO; | |
| 64 | + } | |
| 65 | + | |
| 66 | + /** | |
| 67 | + * 创建【IN_PROGRESS】状态的转账返回 | |
| 68 | + */ | |
| 69 | + public static PayTransferRespDTO dealingOf(String channelTransferNo, | |
| 70 | + String outTransferNo, Object rawData) { | |
| 71 | + PayTransferRespDTO respDTO = new PayTransferRespDTO(); | |
| 72 | + respDTO.status = PayTransferStatusRespEnum.IN_PROGRESS.getStatus(); | |
| 73 | + respDTO.channelTransferNo = channelTransferNo; | |
| 61 | 74 | respDTO.outTransferNo = outTransferNo; |
| 62 | 75 | respDTO.rawData = rawData; |
| 63 | 76 | return respDTO; | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
| ... | ... | @@ -188,11 +188,11 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen |
| 188 | 188 | |
| 189 | 189 | @Override |
| 190 | 190 | public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) { |
| 191 | + validatePayTransferReqDTO(reqDTO); | |
| 191 | 192 | PayTransferRespDTO resp; |
| 192 | - try{ | |
| 193 | - validatePayTransferReqDTO(reqDTO); | |
| 193 | + try { | |
| 194 | 194 | resp = doUnifiedTransfer(reqDTO); |
| 195 | - }catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 | |
| 195 | + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 | |
| 196 | 196 | throw ex; |
| 197 | 197 | } catch (Throwable ex) { |
| 198 | 198 | // 系统异常,则包装成 PayException 异常抛出 |
| ... | ... | @@ -219,9 +219,25 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen |
| 219 | 219 | } |
| 220 | 220 | } |
| 221 | 221 | |
| 222 | + @Override | |
| 223 | + public final PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type) { | |
| 224 | + try { | |
| 225 | + return doGetTransfer(outTradeNo, type); | |
| 226 | + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 | |
| 227 | + throw ex; | |
| 228 | + } catch (Throwable ex) { | |
| 229 | + log.error("[getTransfer][客户端({}) outTradeNo({}) type({}) 查询转账单异常]", | |
| 230 | + getId(), outTradeNo, type, ex); | |
| 231 | + throw buildPayException(ex); | |
| 232 | + } | |
| 233 | + } | |
| 234 | + | |
| 222 | 235 | protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) |
| 223 | 236 | throws Throwable; |
| 224 | 237 | |
| 238 | + protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) | |
| 239 | + throws Throwable; | |
| 240 | + | |
| 225 | 241 | // ========== 各种工具方法 ========== |
| 226 | 242 | |
| 227 | 243 | private PayException buildPayException(Throwable ex) { | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
| ... | ... | @@ -23,14 +23,8 @@ import com.alipay.api.AlipayResponse; |
| 23 | 23 | import com.alipay.api.DefaultAlipayClient; |
| 24 | 24 | import com.alipay.api.domain.*; |
| 25 | 25 | import com.alipay.api.internal.util.AlipaySignature; |
| 26 | -import com.alipay.api.request.AlipayFundTransUniTransferRequest; | |
| 27 | -import com.alipay.api.request.AlipayTradeFastpayRefundQueryRequest; | |
| 28 | -import com.alipay.api.request.AlipayTradeQueryRequest; | |
| 29 | -import com.alipay.api.request.AlipayTradeRefundRequest; | |
| 30 | -import com.alipay.api.response.AlipayFundTransUniTransferResponse; | |
| 31 | -import com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse; | |
| 32 | -import com.alipay.api.response.AlipayTradeQueryResponse; | |
| 33 | -import com.alipay.api.response.AlipayTradeRefundResponse; | |
| 26 | +import com.alipay.api.request.*; | |
| 27 | +import com.alipay.api.response.*; | |
| 34 | 28 | import lombok.Getter; |
| 35 | 29 | import lombok.SneakyThrows; |
| 36 | 30 | import lombok.extern.slf4j.Slf4j; |
| ... | ... | @@ -126,7 +120,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa |
| 126 | 120 | } |
| 127 | 121 | // 2.2 解析订单的状态 |
| 128 | 122 | Integer status = parseStatus(response.getTradeStatus()); |
| 129 | - Assert.notNull(status, () -> { | |
| 123 | + Assert.notNull(status, () -> { | |
| 130 | 124 | throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody())); |
| 131 | 125 | }); |
| 132 | 126 | return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()), |
| ... | ... | @@ -228,7 +222,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa |
| 228 | 222 | protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException { |
| 229 | 223 | // 1.1 校验公钥类型 必须使用公钥证书模式 |
| 230 | 224 | if (!Objects.equals(config.getMode(), MODE_CERTIFICATE)) { |
| 231 | - throw exception0(ERROR_CONFIGURATION.getCode(),"支付宝单笔转账必须使用公钥证书模式"); | |
| 225 | + throw exception0(ERROR_CONFIGURATION.getCode(), "支付宝单笔转账必须使用公钥证书模式"); | |
| 232 | 226 | } |
| 233 | 227 | // 1.2 构建 AlipayFundTransUniTransferModel |
| 234 | 228 | AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel(); |
| ... | ... | @@ -238,44 +232,96 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa |
| 238 | 232 | model.setOutBizNo(reqDTO.getOutTransferNo()); |
| 239 | 233 | model.setProductCode("TRANS_ACCOUNT_NO_PWD"); // 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD |
| 240 | 234 | model.setBizScene("DIRECT_TRANSFER"); // 业务场景 单笔无密转账固定为 DIRECT_TRANSFER |
| 241 | - model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras())); | |
| 235 | + if (reqDTO.getChannelExtras() != null) { | |
| 236 | + model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras())); | |
| 237 | + } | |
| 238 | + // ② 个性化的参数 | |
| 239 | + Participant payeeInfo = new Participant(); | |
| 242 | 240 | PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType()); |
| 243 | 241 | switch (transferType) { |
| 244 | 242 | // TODO @jason:是不是不用传递 transferType 参数哈?因为应该已经明确是支付宝啦? |
| 245 | 243 | // @芋艿。 是不是还要考虑转账到银行卡。所以传 transferType 但是转账到银行卡不知道要如何测试?? |
| 246 | 244 | case ALIPAY_BALANCE: { |
| 247 | - // ② 个性化的参数 | |
| 248 | - Participant payeeInfo = new Participant(); | |
| 249 | 245 | payeeInfo.setIdentityType("ALIPAY_LOGON_ID"); |
| 250 | 246 | payeeInfo.setIdentity(reqDTO.getAlipayLogonId()); // 支付宝登录号 |
| 251 | 247 | payeeInfo.setName(reqDTO.getUserName()); // 支付宝账号姓名 |
| 252 | 248 | model.setPayeeInfo(payeeInfo); |
| 253 | - // 1.3 构建 AlipayFundTransUniTransferRequest | |
| 254 | - AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest(); | |
| 255 | - request.setBizModel(model); | |
| 256 | - // 执行请求 | |
| 257 | - AlipayFundTransUniTransferResponse response = client.certificateExecute(request); | |
| 258 | - // 处理结果 | |
| 259 | - if (!response.isSuccess()) { | |
| 260 | - // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询 | |
| 261 | - if (ObjectUtils.equalsAny(response.getSubCode(), "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) { | |
| 262 | - return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response); | |
| 263 | - } | |
| 264 | - return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), | |
| 265 | - reqDTO.getOutTransferNo(), response); | |
| 266 | - } | |
| 267 | - return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()), | |
| 268 | - response.getOutBizNo(), response); | |
| 249 | + break; | |
| 269 | 250 | } |
| 270 | 251 | case BANK_CARD: { |
| 271 | - Participant payeeInfo = new Participant(); | |
| 272 | 252 | payeeInfo.setIdentityType("BANKCARD_ACCOUNT"); |
| 273 | 253 | // TODO 待实现 |
| 274 | 254 | throw exception(NOT_IMPLEMENTED); |
| 275 | 255 | } |
| 276 | 256 | default: { |
| 277 | - throw exception0(BAD_REQUEST.getCode(),"不正确的转账类型: {}",transferType); | |
| 257 | + throw exception0(BAD_REQUEST.getCode(), "不正确的转账类型: {}", transferType); | |
| 258 | + } | |
| 259 | + } | |
| 260 | + // 1.3 构建 AlipayFundTransUniTransferRequest | |
| 261 | + AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest(); | |
| 262 | + request.setBizModel(model); | |
| 263 | + // 执行请求 | |
| 264 | + AlipayFundTransUniTransferResponse response = client.certificateExecute(request); | |
| 265 | + // 处理结果 | |
| 266 | + if (!response.isSuccess()) { | |
| 267 | + // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询,或相同 outBizNo 重新发起转账 | |
| 268 | + // 发现 outBizNo 相同 两次请求参数相同. 会返回 "PAYMENT_INFO_INCONSISTENCY", 不知道哪里的问题. 暂时返回 WAIT. 后续job 会轮询 | |
| 269 | + if (ObjectUtils.equalsAny(response.getSubCode(),"PAYMENT_INFO_INCONSISTENCY", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) { | |
| 270 | + return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response); | |
| 271 | + } | |
| 272 | + return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), | |
| 273 | + reqDTO.getOutTransferNo(), response); | |
| 274 | + } else { | |
| 275 | + if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL" | |
| 276 | + return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), | |
| 277 | + reqDTO.getOutTransferNo(), response); | |
| 278 | + } | |
| 279 | + if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中 | |
| 280 | + return PayTransferRespDTO.dealingOf(response.getOrderId(), reqDTO.getOutTransferNo(), response); | |
| 281 | + } | |
| 282 | + return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()), | |
| 283 | + response.getOutBizNo(), response); | |
| 284 | + } | |
| 285 | + | |
| 286 | + } | |
| 287 | + | |
| 288 | + @Override | |
| 289 | + protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) throws Throwable { | |
| 290 | + // 1.1 构建 AlipayFundTransCommonQueryModel | |
| 291 | + AlipayFundTransCommonQueryModel model = new AlipayFundTransCommonQueryModel(); | |
| 292 | + model.setProductCode(type == PayTransferTypeEnum.BANK_CARD ? "TRANS_BANKCARD_NO_PWD" : "TRANS_ACCOUNT_NO_PWD"); | |
| 293 | + model.setBizScene("DIRECT_TRANSFER"); //业务场景 | |
| 294 | + model.setOutBizNo(outTradeNo); | |
| 295 | + // 1.2 构建 AlipayFundTransCommonQueryRequest | |
| 296 | + AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest(); | |
| 297 | + request.setBizModel(model); | |
| 298 | + | |
| 299 | + // 2.1 执行请求 | |
| 300 | + AlipayFundTransCommonQueryResponse response; | |
| 301 | + if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式 | |
| 302 | + response = client.certificateExecute(request); | |
| 303 | + } else { | |
| 304 | + response = client.execute(request); | |
| 305 | + } | |
| 306 | + // 2.2 处理返回结果 | |
| 307 | + if (response.isSuccess()) { | |
| 308 | + if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL" | |
| 309 | + return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), | |
| 310 | + outTradeNo, response); | |
| 311 | + } | |
| 312 | + if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中 | |
| 313 | + return PayTransferRespDTO.dealingOf(response.getOrderId(), outTradeNo, response); | |
| 278 | 314 | } |
| 315 | + return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getPayDate()), | |
| 316 | + response.getOutBizNo(), response); | |
| 317 | + } else { | |
| 318 | + // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账 | |
| 319 | + // 当出现 ORDER_NOT_EXIST 可能是转账还在处理中,也可能是转账处理失败. 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账 | |
| 320 | + if (ObjectUtils.equalsAny(response.getSubCode(), "ORDER_NOT_EXIST", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) { | |
| 321 | + return PayTransferRespDTO.waitingOf(null, outTradeNo, response); | |
| 322 | + } | |
| 323 | + return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), | |
| 324 | + outTradeNo, response); | |
| 279 | 325 | } |
| 280 | 326 | } |
| 281 | 327 | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java
| ... | ... | @@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifie |
| 9 | 9 | import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; |
| 10 | 10 | import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig; |
| 11 | 11 | import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; |
| 12 | +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; | |
| 12 | 13 | |
| 13 | 14 | import java.time.LocalDateTime; |
| 14 | 15 | import java.util.Map; |
| ... | ... | @@ -71,4 +72,9 @@ public class MockPayClient extends AbstractPayClient<NonePayClientConfig> { |
| 71 | 72 | throw new UnsupportedOperationException("待实现"); |
| 72 | 73 | } |
| 73 | 74 | |
| 75 | + @Override | |
| 76 | + protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) { | |
| 77 | + throw new UnsupportedOperationException("待实现"); | |
| 78 | + } | |
| 79 | + | |
| 74 | 80 | } | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
| ... | ... | @@ -16,6 +16,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDT |
| 16 | 16 | import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; |
| 17 | 17 | import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; |
| 18 | 18 | import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum; |
| 19 | +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; | |
| 19 | 20 | import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; |
| 20 | 21 | import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result; |
| 21 | 22 | import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult; |
| ... | ... | @@ -431,6 +432,12 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC |
| 431 | 432 | protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) { |
| 432 | 433 | throw new UnsupportedOperationException("待实现"); |
| 433 | 434 | } |
| 435 | + | |
| 436 | + @Override | |
| 437 | + protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) { | |
| 438 | + throw new UnsupportedOperationException("待实现"); | |
| 439 | + } | |
| 440 | + | |
| 434 | 441 | // ========== 各种工具方法 ========== |
| 435 | 442 | |
| 436 | 443 | static String formatDateV2(LocalDateTime time) { | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java
| ... | ... | @@ -38,4 +38,8 @@ public enum PayTransferStatusRespEnum { |
| 38 | 38 | public static boolean isClosed(Integer status) { |
| 39 | 39 | return Objects.equals(status, CLOSED.getStatus()); |
| 40 | 40 | } |
| 41 | + | |
| 42 | + public static boolean isInProgress(Integer status) { | |
| 43 | + return Objects.equals(status, IN_PROGRESS.getStatus()); | |
| 44 | + } | |
| 41 | 45 | } | ... | ... |
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
| ... | ... | @@ -66,9 +66,9 @@ public interface ErrorCodeConstants { |
| 66 | 66 | // ========== 转账模块 1-007-009-000 ========== |
| 67 | 67 | ErrorCode PAY_TRANSFER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1_007_009_000, "发起转账报错,错误码:{},错误提示:{}"); |
| 68 | 68 | ErrorCode PAY_TRANSFER_NOT_FOUND = new ErrorCode(1_007_009_001, "转账单不存在"); |
| 69 | - ErrorCode PAY_TRANSFER_STATUS_IS_SUCCESS = new ErrorCode(1_007_009_002, "转账单已成功转账"); | |
| 70 | - ErrorCode PAY_TRANSFER_EXISTS = new ErrorCode(1_007_009_003, "已经存在转账单"); | |
| 71 | - ErrorCode PAY_MERCHANT_TRANSFER_EXISTS = new ErrorCode(1_007_009_004, "该笔业务的转账已经存在,请查询转账订单相关状态"); | |
| 69 | + ErrorCode PAY_SAME_MERCHANT_TRANSFER_TYPE_NOT_MATCH = new ErrorCode(1_007_009_002, "两次相同转账请求的类型不匹配"); | |
| 70 | + ErrorCode PAY_SAME_MERCHANT_TRANSFER_PRICE_NOT_MATCH = new ErrorCode(1_007_009_003, "两次相同转账请求的金额不匹配"); | |
| 71 | + ErrorCode PAY_MERCHANT_TRANSFER_EXISTS = new ErrorCode(1_007_009_004, "该笔业务的转账已经发起,请查询转账订单相关状态"); | |
| 72 | 72 | ErrorCode PAY_TRANSFER_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_009_005, "转账单不处于待转账"); |
| 73 | 73 | ErrorCode PAY_TRANSFER_STATUS_IS_NOT_PENDING = new ErrorCode(1_007_009_006, "转账单不处于待转账或转账中"); |
| 74 | 74 | ... | ... |
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/transfer/PayTransferStatusEnum.java
| ... | ... | @@ -40,9 +40,13 @@ public enum PayTransferStatusEnum { |
| 40 | 40 | public static boolean isClosed(Integer status) { |
| 41 | 41 | return Objects.equals(status, CLOSED.getStatus()); |
| 42 | 42 | } |
| 43 | + | |
| 43 | 44 | public static boolean isWaiting(Integer status) { |
| 44 | 45 | return Objects.equals(status, WAITING.getStatus()); |
| 45 | 46 | } |
| 47 | + public static boolean isInProgress(Integer status) { | |
| 48 | + return Objects.equals(status, IN_PROGRESS.getStatus()); | |
| 49 | + } | |
| 46 | 50 | |
| 47 | 51 | /** |
| 48 | 52 | * 是否处于待转账或者转账中的状态 | ... | ... |
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/transfer/PayTransferConvert.java
| ... | ... | @@ -18,7 +18,7 @@ public interface PayTransferConvert { |
| 18 | 18 | |
| 19 | 19 | PayTransferDO convert(PayTransferCreateReqDTO dto); |
| 20 | 20 | |
| 21 | - PayTransferUnifiedReqDTO convert2(PayTransferCreateReqDTO dto); | |
| 21 | + PayTransferUnifiedReqDTO convert2(PayTransferDO dto); | |
| 22 | 22 | |
| 23 | 23 | PayTransferCreateReqDTO convert(PayTransferCreateReqVO vo); |
| 24 | 24 | ... | ... |
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferMapper.java
| 1 | 1 | package cn.iocoder.yudao.module.pay.dal.mysql.transfer; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | |
| 4 | 5 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| 5 | 6 | import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO; |
| 6 | 7 | import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO; |
| 7 | -import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | |
| 8 | 8 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| 9 | 9 | import org.apache.ibatis.annotations.Mapper; |
| 10 | 10 | |
| ... | ... | @@ -40,6 +40,10 @@ public interface PayTransferMapper extends BaseMapperX<PayTransferDO> { |
| 40 | 40 | .betweenIfPresent(PayTransferDO::getCreateTime, reqVO.getCreateTime()) |
| 41 | 41 | .orderByDesc(PayTransferDO::getId)); |
| 42 | 42 | } |
| 43 | + | |
| 44 | + default List<PayTransferDO> selectListByStatus(Integer status){ | |
| 45 | + return selectList(PayTransferDO::getStatus, status); | |
| 46 | + } | |
| 43 | 47 | } |
| 44 | 48 | |
| 45 | 49 | ... | ... |
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java
| ... | ... | @@ -14,6 +14,7 @@ import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; |
| 14 | 14 | import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig; |
| 15 | 15 | import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; |
| 16 | 16 | import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum; |
| 17 | +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; | |
| 17 | 18 | import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO; |
| 18 | 19 | import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; |
| 19 | 20 | import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletTransactionDO; |
| ... | ... | @@ -181,4 +182,9 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> { |
| 181 | 182 | throw new UnsupportedOperationException("待实现"); |
| 182 | 183 | } |
| 183 | 184 | |
| 185 | + @Override | |
| 186 | + protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) { | |
| 187 | + throw new UnsupportedOperationException("待实现"); | |
| 188 | + } | |
| 189 | + | |
| 184 | 190 | } | ... | ... |
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/transfer/PayTransferSyncJob.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.pay.job.transfer; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 4 | +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; | |
| 5 | +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; | |
| 6 | +import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService; | |
| 7 | +import org.springframework.stereotype.Component; | |
| 8 | + | |
| 9 | +import javax.annotation.Resource; | |
| 10 | + | |
| 11 | +/** | |
| 12 | + * 转账订单的同步 Job | |
| 13 | + * | |
| 14 | + * 由于转账订单的转账结果,有些渠道是异步通知进行同步的,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。 | |
| 15 | + * | |
| 16 | + * @author jason | |
| 17 | + */ | |
| 18 | +@Component | |
| 19 | +public class PayTransferSyncJob implements JobHandler { | |
| 20 | + | |
| 21 | + @Resource | |
| 22 | + private PayTransferService transferService; | |
| 23 | + | |
| 24 | + @Override | |
| 25 | + @TenantJob | |
| 26 | + public String execute(String param) { | |
| 27 | + int count = transferService.syncTransfer(); | |
| 28 | + return StrUtil.format("同步转账订单 {} 个", count); | |
| 29 | + } | |
| 30 | +} | ... | ... |
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferService.java
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
| 1 | 1 | package cn.iocoder.yudao.module.pay.service.transfer; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | +import cn.hutool.core.util.ObjectUtil; | |
| 4 | 5 | import cn.hutool.extra.spring.SpringUtil; |
| 5 | -import cn.iocoder.yudao.framework.common.exception.ServiceException; | |
| 6 | 6 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 7 | 7 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| 8 | 8 | import cn.iocoder.yudao.framework.pay.core.client.PayClient; |
| 9 | 9 | import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO; |
| 10 | 10 | import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; |
| 11 | 11 | import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum; |
| 12 | +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; | |
| 13 | +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; | |
| 12 | 14 | import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO; |
| 13 | 15 | import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferCreateReqVO; |
| 14 | 16 | import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO; |
| ... | ... | @@ -18,6 +20,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO; |
| 18 | 20 | import cn.iocoder.yudao.module.pay.dal.mysql.transfer.PayTransferMapper; |
| 19 | 21 | import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO; |
| 20 | 22 | import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; |
| 23 | +import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum; | |
| 21 | 24 | import cn.iocoder.yudao.module.pay.service.app.PayAppService; |
| 22 | 25 | import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; |
| 23 | 26 | import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService; |
| ... | ... | @@ -27,7 +30,7 @@ import org.springframework.transaction.annotation.Transactional; |
| 27 | 30 | |
| 28 | 31 | import javax.annotation.Resource; |
| 29 | 32 | import javax.validation.Validator; |
| 30 | -import java.util.Objects; | |
| 33 | +import java.util.List; | |
| 31 | 34 | |
| 32 | 35 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 33 | 36 | import static cn.iocoder.yudao.module.pay.convert.transfer.PayTransferConvert.INSTANCE; |
| ... | ... | @@ -75,56 +78,60 @@ public class PayTransferServiceImpl implements PayTransferService { |
| 75 | 78 | |
| 76 | 79 | @Override |
| 77 | 80 | public Long createTransfer(PayTransferCreateReqDTO reqDTO) { |
| 78 | - // 1.1 校验转账单是否可以提交 | |
| 79 | - validateTransferCanCreate(reqDTO.getAppId(), reqDTO.getMerchantTransferId()); | |
| 80 | - // 1.2 校验 App | |
| 81 | + // 1.1 校验 App | |
| 81 | 82 | PayAppDO payApp = appService.validPayApp(reqDTO.getAppId()); |
| 82 | - // 1.3 校验支付渠道是否有效 | |
| 83 | + // 1.2 校验支付渠道是否有效 | |
| 83 | 84 | PayChannelDO channel = channelService.validPayChannel(reqDTO.getAppId(), reqDTO.getChannelCode()); |
| 84 | 85 | PayClient client = channelService.getPayClient(channel.getId()); |
| 85 | 86 | if (client == null) { |
| 86 | 87 | log.error("[createTransfer][渠道编号({}) 找不到对应的支付客户端]", channel.getId()); |
| 87 | 88 | throw exception(CHANNEL_NOT_FOUND); |
| 88 | 89 | } |
| 89 | - // 2.创建转账单 | |
| 90 | - String no = noRedisDAO.generate(TRANSFER_NO_PREFIX); | |
| 91 | - PayTransferDO transfer = INSTANCE.convert(reqDTO) | |
| 92 | - .setChannelId(channel.getId()) | |
| 93 | - .setNo(no).setStatus(WAITING.getStatus()) | |
| 94 | - .setNotifyUrl(payApp.getTransferNotifyUrl()); | |
| 95 | - transferMapper.insert(transfer); | |
| 96 | - PayTransferRespDTO unifiedTransferResp = null; | |
| 90 | + // 1.3 校验转账单已经发起过转账。 | |
| 91 | + PayTransferDO transfer = validateTransferCanCreate(reqDTO); | |
| 92 | + | |
| 93 | + if (transfer == null) { | |
| 94 | + // 2.不存在创建转账单. 否则允许使用相同的 no 再次发起转账 | |
| 95 | + String no = noRedisDAO.generate(TRANSFER_NO_PREFIX); | |
| 96 | + transfer = INSTANCE.convert(reqDTO) | |
| 97 | + .setChannelId(channel.getId()) | |
| 98 | + .setNo(no).setStatus(WAITING.getStatus()) | |
| 99 | + .setNotifyUrl(payApp.getTransferNotifyUrl()); | |
| 100 | + transferMapper.insert(transfer); | |
| 101 | + } | |
| 97 | 102 | try { |
| 98 | 103 | // 3. 调用三方渠道发起转账 |
| 99 | - PayTransferUnifiedReqDTO transferUnifiedReq = INSTANCE.convert2(reqDTO) | |
| 100 | - .setOutTransferNo(no); | |
| 101 | - unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq); | |
| 102 | - } catch (ServiceException ex) { | |
| 103 | - // 业务异常.直接返回转账失败的结果 | |
| 104 | - log.error("[createTransfer][转账 id({}) requestDTO({}) 发生业务异常]", transfer.getId(), reqDTO, ex); | |
| 105 | - unifiedTransferResp = PayTransferRespDTO.closedOf("", "", no, ex); | |
| 104 | + PayTransferUnifiedReqDTO transferUnifiedReq = INSTANCE.convert2(transfer) | |
| 105 | + .setOutTransferNo(transfer.getNo()); | |
| 106 | + PayTransferRespDTO unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq); | |
| 107 | + // 4. 通知转账结果 | |
| 108 | + getSelf().notifyTransfer(channel, unifiedTransferResp); | |
| 106 | 109 | } catch (Throwable e) { |
| 107 | 110 | // 注意这里仅打印异常,不进行抛出。 |
| 108 | - // 原因是:虽然调用支付渠道进行转账发生异常(网络请求超时),实际转账成功。这个结果,后续通过转账回调、或者转账轮询可以拿到。 | |
| 109 | - // TODO 需要加转账回调业务接口 和 转账轮询未实现 | |
| 110 | - // 最终,在异常的情况下,支付中心会异步回调业务的转账回调接口,提供转账结果 | |
| 111 | + // 原因是:虽然调用支付渠道进行转账发生异常(网络请求超时),实际转账成功。这个结果,后续转账轮询可以拿到。 | |
| 112 | + // 或者使用相同 no 再次发起转账请求 | |
| 111 | 113 | log.error("[createTransfer][转账 id({}) requestDTO({}) 发生异常]", transfer.getId(), reqDTO, e); |
| 112 | 114 | } |
| 113 | - if (Objects.nonNull(unifiedTransferResp)) { | |
| 114 | - // 4. 通知转账结果 | |
| 115 | - getSelf().notifyTransfer(channel, unifiedTransferResp); | |
| 116 | - } | |
| 117 | - return transfer.getId(); | |
| 118 | - } | |
| 119 | 115 | |
| 120 | - @Override | |
| 121 | - public PayTransferDO getTransfer(Long id) { | |
| 122 | - return transferMapper.selectById(id); | |
| 116 | + return transfer.getId(); | |
| 123 | 117 | } |
| 124 | 118 | |
| 125 | - @Override | |
| 126 | - public PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO) { | |
| 127 | - return transferMapper.selectPage(pageReqVO); | |
| 119 | + private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto) { | |
| 120 | + PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(dto.getAppId(), dto.getMerchantTransferId()); | |
| 121 | + if (transfer != null) { | |
| 122 | + // 已经存在,并且状态不为等待状态。说明已经调用渠道转账并返回结果. | |
| 123 | + if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) { | |
| 124 | + throw exception(PAY_MERCHANT_TRANSFER_EXISTS); | |
| 125 | + } | |
| 126 | + if (ObjectUtil.notEqual(dto.getPrice(), transfer.getPrice())) { | |
| 127 | + throw exception(PAY_SAME_MERCHANT_TRANSFER_PRICE_NOT_MATCH); | |
| 128 | + } | |
| 129 | + if (ObjectUtil.notEqual(dto.getType(), transfer.getType())) { | |
| 130 | + throw exception(PAY_SAME_MERCHANT_TRANSFER_TYPE_NOT_MATCH); | |
| 131 | + } | |
| 132 | + } | |
| 133 | + // 如果状态为等待状态。不知道渠道转账是否发起成功。 允许使用相同的 no 再次发起转账,渠道会保证幂等 | |
| 134 | + return transfer; | |
| 128 | 135 | } |
| 129 | 136 | |
| 130 | 137 | @Transactional(rollbackFor = Exception.class) |
| ... | ... | @@ -138,17 +145,40 @@ public class PayTransferServiceImpl implements PayTransferService { |
| 138 | 145 | if (PayTransferStatusRespEnum.isClosed(notify.getStatus())) { |
| 139 | 146 | notifyTransferClosed(channel, notify); |
| 140 | 147 | } |
| 148 | + // 转账处理中的回调 | |
| 149 | + if (PayTransferStatusRespEnum.isInProgress(notify.getStatus())) { | |
| 150 | + notifyTransferInProgress(channel, notify); | |
| 151 | + } | |
| 141 | 152 | // WAITING 状态无需处理 |
| 142 | - // TODO IN_PROGRESS 待处理 | |
| 143 | 153 | } |
| 144 | 154 | |
| 145 | - private void validateTransferCanCreate(Long appId, String merchantTransferId) { | |
| 146 | - PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(appId, merchantTransferId); | |
| 147 | - if (transfer != null) { // 是否存在 | |
| 148 | - throw exception(PAY_MERCHANT_TRANSFER_EXISTS); | |
| 155 | + private void notifyTransferInProgress(PayChannelDO channel, PayTransferRespDTO notify) { | |
| 156 | + // 1.校验 | |
| 157 | + PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo()); | |
| 158 | + if (transfer == null) { | |
| 159 | + throw exception(PAY_TRANSFER_NOT_FOUND); | |
| 160 | + } | |
| 161 | + if (isInProgress(transfer.getStatus())) { // 如果已经是转账中,直接返回,不用重复更新 | |
| 162 | + return; | |
| 163 | + } | |
| 164 | + if (!isWaiting(transfer.getStatus())) { | |
| 165 | + throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING); | |
| 149 | 166 | } |
| 167 | + // 2.更新 | |
| 168 | + int updateCounts = transferMapper.updateByIdAndStatus(transfer.getId(), | |
| 169 | + CollUtil.newArrayList(WAITING.getStatus()), | |
| 170 | + new PayTransferDO().setStatus(IN_PROGRESS.getStatus())); | |
| 171 | + if (updateCounts == 0) { | |
| 172 | + throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING); | |
| 173 | + } | |
| 174 | + log.info("[notifyTransferInProgress][transfer({}) 更新为转账进行中状态]", transfer.getId()); | |
| 175 | + | |
| 176 | + // 3. 插入转账通知记录 | |
| 177 | + notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(), | |
| 178 | + transfer.getId()); | |
| 150 | 179 | } |
| 151 | 180 | |
| 181 | + | |
| 152 | 182 | private void notifyTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) { |
| 153 | 183 | // 1.校验 |
| 154 | 184 | PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo()); |
| ... | ... | @@ -210,6 +240,56 @@ public class PayTransferServiceImpl implements PayTransferService { |
| 210 | 240 | |
| 211 | 241 | } |
| 212 | 242 | |
| 243 | + @Override | |
| 244 | + public PayTransferDO getTransfer(Long id) { | |
| 245 | + return transferMapper.selectById(id); | |
| 246 | + } | |
| 247 | + | |
| 248 | + @Override | |
| 249 | + public PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO) { | |
| 250 | + return transferMapper.selectPage(pageReqVO); | |
| 251 | + } | |
| 252 | + | |
| 253 | + @Override | |
| 254 | + public int syncTransfer() { | |
| 255 | + List<PayTransferDO> list = transferMapper.selectListByStatus(WAITING.getStatus()); | |
| 256 | + if (CollUtil.isEmpty(list)) { | |
| 257 | + return 0; | |
| 258 | + } | |
| 259 | + int count = 0; | |
| 260 | + for (PayTransferDO transfer : list) { | |
| 261 | + count += syncTransfer(transfer) ? 1 : 0; | |
| 262 | + } | |
| 263 | + return count; | |
| 264 | + } | |
| 265 | + | |
| 266 | + private boolean syncTransfer(PayTransferDO transfer) { | |
| 267 | + try { | |
| 268 | + // 1. 查询转账订单信息 | |
| 269 | + PayClient payClient = channelService.getPayClient(transfer.getChannelId()); | |
| 270 | + if (payClient == null) { | |
| 271 | + log.error("[syncTransfer][渠道编号({}) 找不到对应的支付客户端]", transfer.getChannelId()); | |
| 272 | + return false; | |
| 273 | + } | |
| 274 | + PayTransferRespDTO resp = payClient.getTransfer(transfer.getNo(), | |
| 275 | + PayTransferTypeEnum.typeOf(transfer.getType())); | |
| 276 | + | |
| 277 | + // 2. 回调转账结果 | |
| 278 | + notifyTransfer(transfer.getChannelId(), resp); | |
| 279 | + return true; | |
| 280 | + } catch (Throwable ex) { | |
| 281 | + log.error("[syncTransfer][transfer({}) 同步转账单状态异常]", transfer.getId(), ex); | |
| 282 | + return false; | |
| 283 | + } | |
| 284 | + } | |
| 285 | + | |
| 286 | + private void notifyTransfer(Long channelId, PayTransferRespDTO notify) { | |
| 287 | + // 校验渠道是否有效 | |
| 288 | + PayChannelDO channel = channelService.validPayChannel(channelId); | |
| 289 | + // 通知转账结果给对应的业务 | |
| 290 | + TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyTransfer(channel, notify)); | |
| 291 | + } | |
| 292 | + | |
| 213 | 293 | /** |
| 214 | 294 | * 获得自身的代理对象,解决 AOP 生效问题 |
| 215 | 295 | * | ... | ... |