Commit dd2d8a2ba90115c03f4b79e223243cee3d77ee1d
1 parent
72b64dc5
优化支付通知的逻辑,解决并发的问题。
同时,收到支付结果时,立马回调业务,避免延迟
Showing
16 changed files
with
363 additions
and
10 deletions
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/pay/job/notify/PayNotifyJob.java
0 → 100644
| 1 | +package cn.iocoder.yudao.adminserver.modules.pay.job.notify; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.coreservice.modules.pay.service.notify.PayNotifyCoreService; | ||
| 4 | +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; | ||
| 5 | +import lombok.extern.slf4j.Slf4j; | ||
| 6 | +import org.springframework.stereotype.Component; | ||
| 7 | + | ||
| 8 | +import javax.annotation.Resource; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * 支付通知 Job | ||
| 12 | + * 通过不断扫描待通知的 PayNotifyTaskDO 记录,回调业务线的回调接口 | ||
| 13 | + * | ||
| 14 | + * @author 芋道源码 | ||
| 15 | + */ | ||
| 16 | +@Component | ||
| 17 | +@Slf4j | ||
| 18 | +public class PayNotifyJob implements JobHandler { | ||
| 19 | + | ||
| 20 | + @Resource | ||
| 21 | + private PayNotifyCoreService payNotifyCoreService; | ||
| 22 | + | ||
| 23 | + @Override | ||
| 24 | + public String execute(String param) throws Exception { | ||
| 25 | + int notifyCount = payNotifyCoreService.executeNotify(); | ||
| 26 | + return String.format("执行支付通知 %s 个", notifyCount); | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | +} |
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/pay/job/package-info.java
0 → 100644
| 1 | +package cn.iocoder.yudao.adminserver.modules.pay.job; |
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/enums/SysErrorCodeConstants.java
| 1 | package cn.iocoder.yudao.adminserver.modules.system.enums; | 1 | package cn.iocoder.yudao.adminserver.modules.system.enums; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.common.exception.ErrorCode; | 3 | import cn.iocoder.yudao.framework.common.exception.ErrorCode; |
| 4 | -import javafx.beans.binding.MapExpression; | ||
| 5 | 4 | ||
| 6 | /** | 5 | /** |
| 7 | * System 错误码枚举类 | 6 | * System 错误码枚举类 |
yudao-admin-server/src/main/resources/application-local.yaml
| @@ -166,6 +166,9 @@ yudao: | @@ -166,6 +166,9 @@ yudao: | ||
| 166 | exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系 | 166 | exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系 |
| 167 | - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 | 167 | - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 |
| 168 | - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 | 168 | - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 |
| 169 | + pay: | ||
| 170 | + pay-notify-url: http://niubi.natapp1.cc/api/pay/order/notify | ||
| 171 | + refund-notify-url: http://niubi.natapp1.cc/api/pay/refund/notify | ||
| 169 | demo: false # 关闭演示模式 | 172 | demo: false # 关闭演示模式 |
| 170 | 173 | ||
| 171 | justauth: | 174 | justauth: |
yudao-core-service/pom.xml
| @@ -77,6 +77,12 @@ | @@ -77,6 +77,12 @@ | ||
| 77 | <artifactId>yudao-spring-boot-starter-mq</artifactId> | 77 | <artifactId>yudao-spring-boot-starter-mq</artifactId> |
| 78 | </dependency> | 78 | </dependency> |
| 79 | 79 | ||
| 80 | + <!-- 服务保障相关 --> | ||
| 81 | + <dependency> | ||
| 82 | + <groupId>cn.iocoder.boot</groupId> | ||
| 83 | + <artifactId>yudao-spring-boot-starter-protection</artifactId> | ||
| 84 | + </dependency> | ||
| 85 | + | ||
| 80 | <!-- Test 测试相关 --> | 86 | <!-- Test 测试相关 --> |
| 81 | <dependency> | 87 | <dependency> |
| 82 | <groupId>cn.iocoder.boot</groupId> | 88 | <groupId>cn.iocoder.boot</groupId> |
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/notify/PayNotifyLogDO.java
| @@ -34,15 +34,11 @@ public class PayNotifyLogDO extends BaseDO { | @@ -34,15 +34,11 @@ public class PayNotifyLogDO extends BaseDO { | ||
| 34 | */ | 34 | */ |
| 35 | private Integer notifyTimes; | 35 | private Integer notifyTimes; |
| 36 | /** | 36 | /** |
| 37 | - * 请求参数 | ||
| 38 | - */ | ||
| 39 | - private String request; | ||
| 40 | - /** | ||
| 41 | - * 响应结果 | 37 | + * HTTP 响应结果 |
| 42 | */ | 38 | */ |
| 43 | private String response; | 39 | private String response; |
| 44 | /** | 40 | /** |
| 45 | - * 状态 | 41 | + * 支付通知状态 |
| 46 | * | 42 | * |
| 47 | * 外键 {@link PayNotifyStatusEnum} | 43 | * 外键 {@link PayNotifyStatusEnum} |
| 48 | */ | 44 | */ |
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/notify/PayNotifyTaskCoreMapper.java
| 1 | package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.notify; | 1 | package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.notify; |
| 2 | 2 | ||
| 3 | -import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO; | ||
| 4 | import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify.PayNotifyTaskDO; | 3 | import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify.PayNotifyTaskDO; |
| 4 | +import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyStatusEnum; | ||
| 5 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | 5 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 6 | +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | ||
| 6 | import org.apache.ibatis.annotations.Mapper; | 7 | import org.apache.ibatis.annotations.Mapper; |
| 7 | 8 | ||
| 9 | +import java.util.Date; | ||
| 10 | +import java.util.List; | ||
| 11 | + | ||
| 8 | @Mapper | 12 | @Mapper |
| 9 | public interface PayNotifyTaskCoreMapper extends BaseMapperX<PayNotifyTaskDO> { | 13 | public interface PayNotifyTaskCoreMapper extends BaseMapperX<PayNotifyTaskDO> { |
| 14 | + | ||
| 15 | + /** | ||
| 16 | + * 获得需要通知的 PayNotifyTaskDO 记录。需要满足如下条件: | ||
| 17 | + * | ||
| 18 | + * 1. status 非成功 | ||
| 19 | + * 2. nextNotifyTime 小于当前时间 | ||
| 20 | + * | ||
| 21 | + * @return PayTransactionNotifyTaskDO 数组 | ||
| 22 | + */ | ||
| 23 | + default List<PayNotifyTaskDO> selectListByNotify() { | ||
| 24 | + return selectList(new QueryWrapper<PayNotifyTaskDO>() | ||
| 25 | + .in("status", PayNotifyStatusEnum.WAITING.getStatus(), PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus(), | ||
| 26 | + PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()) | ||
| 27 | + .le("next_notify_time", new Date())); | ||
| 28 | + } | ||
| 29 | + | ||
| 10 | } | 30 | } |
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/redis/PayRedisKeyCoreConstants.java
0 → 100644
| 1 | +package cn.iocoder.yudao.coreservice.modules.pay.dal.redis; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine; | ||
| 4 | +import org.redisson.api.RLock; | ||
| 5 | + | ||
| 6 | +import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.HASH; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * Lock4j Redis Key 枚举类 | ||
| 10 | + * | ||
| 11 | + * @author 芋道源码 | ||
| 12 | + */ | ||
| 13 | +public interface PayRedisKeyCoreConstants { | ||
| 14 | + | ||
| 15 | + RedisKeyDefine PAY_NOTIFY_LOCK = new RedisKeyDefine("通知任务的分布式锁", | ||
| 16 | + "pay_notify:lock:", // 参数来自 DefaultLockKeyBuilder 类 | ||
| 17 | + HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson 的 Lock 锁,使用 Hash 数据结构 | ||
| 18 | + | ||
| 19 | +} |
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/redis/notify/PayNotifyLockCoreRedisDAO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.coreservice.modules.pay.dal.redis.notify; | ||
| 2 | + | ||
| 3 | +import org.redisson.api.RLock; | ||
| 4 | +import org.redisson.api.RedissonClient; | ||
| 5 | +import org.springframework.stereotype.Repository; | ||
| 6 | + | ||
| 7 | +import javax.annotation.Resource; | ||
| 8 | +import java.util.concurrent.TimeUnit; | ||
| 9 | + | ||
| 10 | +import static cn.iocoder.yudao.coreservice.modules.pay.dal.redis.PayRedisKeyCoreConstants.PAY_NOTIFY_LOCK; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * 支付通知的锁 Redis DAO | ||
| 14 | + * | ||
| 15 | + * @author 芋道源码 | ||
| 16 | + */ | ||
| 17 | +@Repository | ||
| 18 | +public class PayNotifyLockCoreRedisDAO { | ||
| 19 | + | ||
| 20 | + @Resource | ||
| 21 | + private RedissonClient redissonClient; | ||
| 22 | + | ||
| 23 | + public void lock(Long id, Long timeoutMillis, Runnable runnable) { | ||
| 24 | + String lockKey = formatKey(id); | ||
| 25 | + RLock lock = redissonClient.getLock(lockKey); | ||
| 26 | + try { | ||
| 27 | + lock.lock(timeoutMillis, TimeUnit.MILLISECONDS); | ||
| 28 | + // 执行逻辑 | ||
| 29 | + runnable.run(); | ||
| 30 | + } finally { | ||
| 31 | + lock.unlock(); | ||
| 32 | + } | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + private static String formatKey(Long id) { | ||
| 36 | + return String.format(PAY_NOTIFY_LOCK.getKeyTemplate(), id); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | +} |
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/PayNotifyCoreService.java
| @@ -22,7 +22,8 @@ public interface PayNotifyCoreService { | @@ -22,7 +22,8 @@ public interface PayNotifyCoreService { | ||
| 22 | * 执行支付通知 | 22 | * 执行支付通知 |
| 23 | * | 23 | * |
| 24 | * 注意,该方法提供给定时任务调用。目前是 yudao-admin-server 进行调用 | 24 | * 注意,该方法提供给定时任务调用。目前是 yudao-admin-server 进行调用 |
| 25 | + * @return 通知数量 | ||
| 25 | */ | 26 | */ |
| 26 | - void executeNotify(); | 27 | + int executeNotify() throws InterruptedException; |
| 27 | 28 | ||
| 28 | } | 29 | } |
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/impl/PayNotifyCoreServiceImpl.java
| 1 | package cn.iocoder.yudao.coreservice.modules.pay.service.notify.impl; | 1 | package cn.iocoder.yudao.coreservice.modules.pay.service.notify.impl; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.collection.CollUtil; | ||
| 4 | +import cn.hutool.http.HttpUtil; | ||
| 3 | import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify.PayNotifyTaskDO; | 5 | import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify.PayNotifyTaskDO; |
| 4 | import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO; | 6 | import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO; |
| 5 | import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.notify.PayNotifyTaskCoreMapper; | 7 | import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.notify.PayNotifyTaskCoreMapper; |
| 8 | +import cn.iocoder.yudao.coreservice.modules.pay.dal.redis.notify.PayNotifyLockCoreRedisDAO; | ||
| 6 | import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyStatusEnum; | 9 | import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyStatusEnum; |
| 7 | import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum; | 10 | import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum; |
| 8 | import cn.iocoder.yudao.coreservice.modules.pay.service.notify.PayNotifyCoreService; | 11 | import cn.iocoder.yudao.coreservice.modules.pay.service.notify.PayNotifyCoreService; |
| 9 | import cn.iocoder.yudao.coreservice.modules.pay.service.notify.dto.PayNotifyTaskCreateReqDTO; | 12 | import cn.iocoder.yudao.coreservice.modules.pay.service.notify.dto.PayNotifyTaskCreateReqDTO; |
| 13 | +import cn.iocoder.yudao.coreservice.modules.pay.service.notify.vo.PayNotifyOrderReqVO; | ||
| 14 | +import cn.iocoder.yudao.coreservice.modules.pay.service.notify.vo.PayRefundOrderReqVO; | ||
| 10 | import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayOrderCoreService; | 15 | import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayOrderCoreService; |
| 16 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 17 | +import cn.iocoder.yudao.framework.common.util.date.DateUtils; | ||
| 18 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 11 | import lombok.extern.slf4j.Slf4j; | 19 | import lombok.extern.slf4j.Slf4j; |
| 12 | import org.springframework.context.annotation.Lazy; | 20 | import org.springframework.context.annotation.Lazy; |
| 21 | +import org.springframework.scheduling.annotation.Async; | ||
| 13 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; | 22 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; |
| 14 | import org.springframework.stereotype.Service; | 23 | import org.springframework.stereotype.Service; |
| 24 | +import org.springframework.transaction.annotation.Transactional; | ||
| 15 | 25 | ||
| 16 | import javax.annotation.Resource; | 26 | import javax.annotation.Resource; |
| 17 | import javax.validation.Valid; | 27 | import javax.validation.Valid; |
| 18 | import java.util.Date; | 28 | import java.util.Date; |
| 29 | +import java.util.List; | ||
| 19 | import java.util.Objects; | 30 | import java.util.Objects; |
| 31 | +import java.util.concurrent.CountDownLatch; | ||
| 32 | +import java.util.concurrent.TimeUnit; | ||
| 33 | + | ||
| 34 | +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.SECOND_MILLIS; | ||
| 35 | +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; | ||
| 20 | 36 | ||
| 21 | /** | 37 | /** |
| 22 | * 支付通知 Core Service 实现类 | 38 | * 支付通知 Core Service 实现类 |
| @@ -28,6 +44,15 @@ import java.util.Objects; | @@ -28,6 +44,15 @@ import java.util.Objects; | ||
| 28 | @Slf4j | 44 | @Slf4j |
| 29 | public class PayNotifyCoreServiceImpl implements PayNotifyCoreService { | 45 | public class PayNotifyCoreServiceImpl implements PayNotifyCoreService { |
| 30 | 46 | ||
| 47 | + /** | ||
| 48 | + * 通知超时时间,单位:秒 | ||
| 49 | + */ | ||
| 50 | + public static final int NOTIFY_TIMEOUT = 120; | ||
| 51 | + /** | ||
| 52 | + * {@link #NOTIFY_TIMEOUT} 的毫秒 | ||
| 53 | + */ | ||
| 54 | + public static final long NOTIFY_TIMEOUT_MILLIS = 120 * SECOND_MILLIS; | ||
| 55 | + | ||
| 31 | @Resource | 56 | @Resource |
| 32 | @Lazy // 循环依赖,避免报错 | 57 | @Lazy // 循环依赖,避免报错 |
| 33 | private PayOrderCoreService payOrderCoreService; | 58 | private PayOrderCoreService payOrderCoreService; |
| @@ -38,6 +63,13 @@ public class PayNotifyCoreServiceImpl implements PayNotifyCoreService { | @@ -38,6 +63,13 @@ public class PayNotifyCoreServiceImpl implements PayNotifyCoreService { | ||
| 38 | @Resource | 63 | @Resource |
| 39 | private ThreadPoolTaskExecutor threadPoolTaskExecutor; // TODO 芋艿:未来提供独立的线程池 | 64 | private ThreadPoolTaskExecutor threadPoolTaskExecutor; // TODO 芋艿:未来提供独立的线程池 |
| 40 | 65 | ||
| 66 | + @Resource | ||
| 67 | + private PayNotifyLockCoreRedisDAO payNotifyLockCoreRedisDAO; | ||
| 68 | + | ||
| 69 | + @Resource | ||
| 70 | + @Lazy // 循环依赖(自己依赖自己),避免报错 | ||
| 71 | + private PayNotifyCoreServiceImpl self; | ||
| 72 | + | ||
| 41 | @Override | 73 | @Override |
| 42 | public void createPayNotifyTask(PayNotifyTaskCreateReqDTO reqDTO) { | 74 | public void createPayNotifyTask(PayNotifyTaskCreateReqDTO reqDTO) { |
| 43 | PayNotifyTaskDO task = new PayNotifyTaskDO(); | 75 | PayNotifyTaskDO task = new PayNotifyTaskDO(); |
| @@ -56,11 +88,132 @@ public class PayNotifyCoreServiceImpl implements PayNotifyCoreService { | @@ -56,11 +88,132 @@ public class PayNotifyCoreServiceImpl implements PayNotifyCoreService { | ||
| 56 | 88 | ||
| 57 | // 执行插入 | 89 | // 执行插入 |
| 58 | payNotifyTaskCoreMapper.insert(task); | 90 | payNotifyTaskCoreMapper.insert(task); |
| 91 | + | ||
| 92 | + // 异步直接发起任务。虽然会有定时任务扫描,但是会导致延迟 | ||
| 93 | + self.executeNotifyAsync(task); | ||
| 59 | } | 94 | } |
| 60 | 95 | ||
| 61 | @Override | 96 | @Override |
| 62 | - public void executeNotify() { | 97 | + public int executeNotify() throws InterruptedException { |
| 98 | + // 获得需要通知的任务 | ||
| 99 | + List<PayNotifyTaskDO> tasks = payNotifyTaskCoreMapper.selectListByNotify(); | ||
| 100 | + if (CollUtil.isEmpty(tasks)) { | ||
| 101 | + return 0; | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + // 遍历,逐个通知 | ||
| 105 | + CountDownLatch latch = new CountDownLatch(tasks.size()); | ||
| 106 | + tasks.forEach(task -> threadPoolTaskExecutor.execute(() -> { | ||
| 107 | + try { | ||
| 108 | + executeNotify(task); | ||
| 109 | + } finally { | ||
| 110 | + latch.countDown(); | ||
| 111 | + } | ||
| 112 | + })); | ||
| 113 | + // 等待完成 | ||
| 114 | + this.awaitExecuteNotify(latch); | ||
| 115 | + // 返回执行完成的任务数(成功 + 失败) | ||
| 116 | + return tasks.size(); | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + /** | ||
| 120 | + * 等待全部支付通知的完成 | ||
| 121 | + * 每 1 秒会打印一次剩余任务数量 | ||
| 122 | + * | ||
| 123 | + * @param latch Latch | ||
| 124 | + * @throws InterruptedException 如果被打断 | ||
| 125 | + */ | ||
| 126 | + private void awaitExecuteNotify(CountDownLatch latch) throws InterruptedException { | ||
| 127 | + long size = latch.getCount(); | ||
| 128 | + for (int i = 0; i < NOTIFY_TIMEOUT; i++) { | ||
| 129 | + if (latch.await(1L, TimeUnit.SECONDS)) { | ||
| 130 | + return; | ||
| 131 | + } | ||
| 132 | + log.info("[awaitExecuteNotify][任务处理中, 总任务数({}) 剩余任务数({})]", size, latch.getCount()); | ||
| 133 | + } | ||
| 134 | + log.error("[awaitExecuteNotify][任务未处理完,总任务数({}) 剩余任务数({})]", size, latch.getCount()); | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + /** | ||
| 138 | + * 异步执行单个支付通知 | ||
| 139 | + * | ||
| 140 | + * @param task 通知任务 | ||
| 141 | + */ | ||
| 142 | + @Async | ||
| 143 | + public void executeNotifyAsync(PayNotifyTaskDO task) { | ||
| 144 | + self.executeNotify(task); // 使用 self,避免事务不发起 | ||
| 145 | + } | ||
| 63 | 146 | ||
| 147 | + /** | ||
| 148 | + * 同步执行单个支付通知 | ||
| 149 | + * | ||
| 150 | + * @param task 通知任务 | ||
| 151 | + */ | ||
| 152 | + public void executeNotify(PayNotifyTaskDO task) { | ||
| 153 | + // 分布式锁,避免并发问题 | ||
| 154 | + payNotifyLockCoreRedisDAO.lock(task.getId(), NOTIFY_TIMEOUT_MILLIS, () -> { | ||
| 155 | + // 校验,当前任务是否已经被通知过 | ||
| 156 | + // 虽然已经通过分布式加锁,但是可能同时满足通知的条件,然后都去获得锁。此时,第一个执行完后,第二个还是能拿到锁,然后会再执行一次。 | ||
| 157 | + PayNotifyTaskDO dbTask = payNotifyTaskCoreMapper.selectById(task.getId()); | ||
| 158 | + if (DateUtils.afterNow(dbTask.getNextNotifyTime())) { | ||
| 159 | + log.info("[executeNotify][dbTask({}) 任务被忽略,原因是未到达下次通知时间,可能是因为并发执行了]", toJsonString(dbTask)); | ||
| 160 | + return; | ||
| 161 | + } | ||
| 162 | + | ||
| 163 | + // 执行通知 | ||
| 164 | + executeNotify0(dbTask); | ||
| 165 | + }); | ||
| 166 | + } | ||
| 167 | + | ||
| 168 | + @Transactional | ||
| 169 | + public void executeNotify0(PayNotifyTaskDO task) { | ||
| 170 | + // 发起回调 | ||
| 171 | + CommonResult<?> invokeResult = null; | ||
| 172 | + Throwable invokeException = null; | ||
| 173 | + try { | ||
| 174 | + invokeResult = executeNotifyInvoke(task); | ||
| 175 | + } catch (Throwable e) { | ||
| 176 | + invokeException = e; | ||
| 177 | + } | ||
| 178 | + | ||
| 179 | + // 设置通用的更新 PayNotifyTaskDO 的字段 | ||
| 180 | + PayNotifyTaskDO updateTask = new PayNotifyTaskDO() | ||
| 181 | + .setId(task.getId()) | ||
| 182 | + .setLastExecuteTime(new Date()) | ||
| 183 | + .setNotifyTimes(task.getNotifyTimes() + 1); | ||
| 184 | + | ||
| 185 | + // 情况一:调用成功 | ||
| 186 | + | ||
| 187 | + // 情况二:调用失败 | ||
| 188 | + | ||
| 189 | + // 调用三:调用异常 | ||
| 190 | + | ||
| 191 | + // 记录 PayNotifyLog 日志 | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + /** | ||
| 195 | + * 执行单个支付任务的 HTTP 调用 | ||
| 196 | + * | ||
| 197 | + * @param task 通知任务 | ||
| 198 | + * @return HTTP 响应 | ||
| 199 | + */ | ||
| 200 | + private CommonResult<?> executeNotifyInvoke(PayNotifyTaskDO task) { | ||
| 201 | + // 拼接参数 | ||
| 202 | + Object request; | ||
| 203 | + if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) { | ||
| 204 | + request = PayNotifyOrderReqVO.builder().merchantOrderId(task.getMerchantOrderId()) | ||
| 205 | + .payOrderId(task.getDataId()).build(); | ||
| 206 | + } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) { | ||
| 207 | + request = PayRefundOrderReqVO.builder().merchantOrderId(task.getMerchantOrderId()) | ||
| 208 | + .payRefundId(task.getDataId()).build(); | ||
| 209 | + } else { | ||
| 210 | + throw new RuntimeException("未知的通知任务类型:" + toJsonString(task)); | ||
| 211 | + } | ||
| 212 | + // 请求地址 | ||
| 213 | + String response = HttpUtil.post(task.getNotifyUrl(), toJsonString(request), | ||
| 214 | + (int) NOTIFY_TIMEOUT_MILLIS); | ||
| 215 | + // 解析结果 | ||
| 216 | + return JsonUtils.parseObject(response, CommonResult.class); | ||
| 64 | } | 217 | } |
| 65 | 218 | ||
| 66 | } | 219 | } |
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/vo/PayNotifyOrderReqVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.coreservice.modules.pay.service.notify.vo; | ||
| 2 | + | ||
| 3 | +import io.swagger.annotations.ApiModel; | ||
| 4 | +import io.swagger.annotations.ApiModelProperty; | ||
| 5 | +import lombok.AllArgsConstructor; | ||
| 6 | +import lombok.Builder; | ||
| 7 | +import lombok.Data; | ||
| 8 | +import lombok.NoArgsConstructor; | ||
| 9 | + | ||
| 10 | +import javax.validation.constraints.NotEmpty; | ||
| 11 | +import javax.validation.constraints.NotNull; | ||
| 12 | + | ||
| 13 | +@ApiModel(value = "支付单的通知 Request VO", description = "业务方接入支付回调时,使用该 VO 对象") | ||
| 14 | +@Data | ||
| 15 | +@Builder | ||
| 16 | +@NoArgsConstructor | ||
| 17 | +@AllArgsConstructor | ||
| 18 | +public class PayNotifyOrderReqVO { | ||
| 19 | + | ||
| 20 | + @ApiModelProperty(value = "商户订单编号", required = true, example = "10") | ||
| 21 | + @NotEmpty(message = "商户订单号不能为空") | ||
| 22 | + private String merchantOrderId; | ||
| 23 | + | ||
| 24 | + @ApiModelProperty(value = "支付订单编号", required = true, example = "20") | ||
| 25 | + @NotNull(message = "支付订单编号不能为空") | ||
| 26 | + private Long payOrderId; | ||
| 27 | + | ||
| 28 | +} |
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/vo/PayRefundOrderReqVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.coreservice.modules.pay.service.notify.vo; | ||
| 2 | + | ||
| 3 | +import io.swagger.annotations.ApiModel; | ||
| 4 | +import io.swagger.annotations.ApiModelProperty; | ||
| 5 | +import lombok.AllArgsConstructor; | ||
| 6 | +import lombok.Builder; | ||
| 7 | +import lombok.Data; | ||
| 8 | +import lombok.NoArgsConstructor; | ||
| 9 | + | ||
| 10 | +import javax.validation.constraints.NotEmpty; | ||
| 11 | +import javax.validation.constraints.NotNull; | ||
| 12 | + | ||
| 13 | +@ApiModel(value = "退款单的通知 Request VO", description = "业务方接入退款回调时,使用该 VO 对象") | ||
| 14 | +@Data | ||
| 15 | +@Builder | ||
| 16 | +@NoArgsConstructor | ||
| 17 | +@AllArgsConstructor | ||
| 18 | +public class PayRefundOrderReqVO { | ||
| 19 | + | ||
| 20 | + @ApiModelProperty(value = "商户订单编号", required = true, example = "10") | ||
| 21 | + @NotEmpty(message = "商户订单号不能为空") | ||
| 22 | + private String merchantOrderId; | ||
| 23 | + | ||
| 24 | + @ApiModelProperty(value = "支付退款编号", required = true, example = "20") | ||
| 25 | + @NotNull(message = "支付退款编号不能为空") | ||
| 26 | + private Long payRefundId; | ||
| 27 | + | ||
| 28 | +} |
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/vo/package-info.java
0 → 100644
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java
| @@ -6,6 +6,8 @@ import java.util.Date; | @@ -6,6 +6,8 @@ import java.util.Date; | ||
| 6 | 6 | ||
| 7 | /** | 7 | /** |
| 8 | * 时间工具类 | 8 | * 时间工具类 |
| 9 | + * | ||
| 10 | + * @author 芋道源码 | ||
| 9 | */ | 11 | */ |
| 10 | public class DateUtils { | 12 | public class DateUtils { |
| 11 | 13 | ||
| @@ -14,6 +16,11 @@ public class DateUtils { | @@ -14,6 +16,11 @@ public class DateUtils { | ||
| 14 | */ | 16 | */ |
| 15 | public static final String TIME_ZONE_DEFAULT = "GMT+8"; | 17 | public static final String TIME_ZONE_DEFAULT = "GMT+8"; |
| 16 | 18 | ||
| 19 | + /** | ||
| 20 | + * 秒转换成毫秒 | ||
| 21 | + */ | ||
| 22 | + public static final long SECOND_MILLIS = 1000; | ||
| 23 | + | ||
| 17 | public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss"; | 24 | public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss"; |
| 18 | 25 | ||
| 19 | public static Date addTime(Duration duration) { | 26 | public static Date addTime(Duration duration) { |
| @@ -74,4 +81,12 @@ public class DateUtils { | @@ -74,4 +81,12 @@ public class DateUtils { | ||
| 74 | return a.compareTo(b) > 0 ? a : b; | 81 | return a.compareTo(b) > 0 ? a : b; |
| 75 | } | 82 | } |
| 76 | 83 | ||
| 84 | + public static boolean beforeNow(Date date) { | ||
| 85 | + return date.getTime() < System.currentTimeMillis(); | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + public static boolean afterNow(Date date) { | ||
| 89 | + return date.getTime() >= System.currentTimeMillis(); | ||
| 90 | + } | ||
| 91 | + | ||
| 77 | } | 92 | } |
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/shop/controller/ShopOrderController.java
| 1 | package cn.iocoder.yudao.userserver.modules.shop.controller; | 1 | package cn.iocoder.yudao.userserver.modules.shop.controller; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.coreservice.modules.pay.service.notify.vo.PayNotifyOrderReqVO; | ||
| 3 | import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayOrderCoreService; | 4 | import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayOrderCoreService; |
| 4 | import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderCreateReqDTO; | 5 | import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderCreateReqDTO; |
| 5 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 6 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| @@ -10,10 +11,12 @@ import io.swagger.annotations.ApiOperation; | @@ -10,10 +11,12 @@ import io.swagger.annotations.ApiOperation; | ||
| 10 | import lombok.extern.slf4j.Slf4j; | 11 | import lombok.extern.slf4j.Slf4j; |
| 11 | import org.springframework.validation.annotation.Validated; | 12 | import org.springframework.validation.annotation.Validated; |
| 12 | import org.springframework.web.bind.annotation.PostMapping; | 13 | import org.springframework.web.bind.annotation.PostMapping; |
| 14 | +import org.springframework.web.bind.annotation.RequestBody; | ||
| 13 | import org.springframework.web.bind.annotation.RequestMapping; | 15 | import org.springframework.web.bind.annotation.RequestMapping; |
| 14 | import org.springframework.web.bind.annotation.RestController; | 16 | import org.springframework.web.bind.annotation.RestController; |
| 15 | 17 | ||
| 16 | import javax.annotation.Resource; | 18 | import javax.annotation.Resource; |
| 19 | +import javax.validation.Valid; | ||
| 17 | import java.time.Duration; | 20 | import java.time.Duration; |
| 18 | 21 | ||
| 19 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | 22 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| @@ -52,4 +55,11 @@ public class ShopOrderController { | @@ -52,4 +55,11 @@ public class ShopOrderController { | ||
| 52 | .payOrderId(payOrderId).build()); | 55 | .payOrderId(payOrderId).build()); |
| 53 | } | 56 | } |
| 54 | 57 | ||
| 58 | + @PostMapping("/pay-notify") | ||
| 59 | + @ApiOperation("支付回调") | ||
| 60 | + public CommonResult<Boolean> payNotify(@RequestBody @Valid PayNotifyOrderReqVO reqVO) { | ||
| 61 | + log.info("[payNotify][回调成功]"); | ||
| 62 | + return success(true); | ||
| 63 | + } | ||
| 64 | + | ||
| 55 | } | 65 | } |