支付系统中,当我们接收到三方支付的异步通知后,如果我们自己也有回调通知的业务需求该如何解决呢?
网上给出了很多解决方案,例如使用消息队列中的延时消息.或者采用动态定时器的方式.
这里我们采用的是定时器的方式,更简单,更轻便.
接触过支付的可能都会了解到,当用户支付成功之后,支付平台会给我们发送多次通知,告诉我们支付成功,在接受到通知消息后,我们就可以对订单进行操作.以完成整个支付流程 .但如果我们要搭建自己的支付平台,同样的需要给使用我们平台的用户发送通知. 通常这个通知是以多次,不同时间间隔进行发送.避免网络问题,用户接收不到.下面就是使用定时器的方式给用户发送通知.当然前提和其他三方支付一样,用户下单时需要给我传递一个回调的地址.我们需要向这个地址发送多次通知.
这里采用的hutool的动态定时器的方式,代码如下:
- 开启hutool定时器,再启动类里开启定时器秒级任务
@SpringBootApplication
public class XsPlatformApplication {
public static void main(String[] args) {
SpringApplication.run(XsPlatformApplication.class, args);
// 支持秒级别定时任务
CronUtil.setMatchSecond(true);
CronUtil.start();
}
}
2. 编写发送异步通知的方法(下面的代码我们对发送回调通知的方法添加了@Async.使他不会影响主业务,同时优化主业务的处理速度)
/**
* 充值回调
*
* @author jy
* @since 2020/10/25 2:03
*/
@Async("taskExecutor")
public void action(NotifyVo notifyVo, String url, LocalDateTime payTime, Long orderId) {
// 获取用户信息及秘钥
MerUser merUser = merUserService.getOneByAccount(notifyVo.getMerchantId());
String secret = merUser.getSecret();
// 对回调信息进行加密,避免回调信息被拦截篡改,加密方式因人而异,可以是rsa,md5等等
String generateSign = notifyVo.generateSign(secret);
notifyVo.setCode(generateSign);
// 这里定义发送三次回调
for (int i = 1; i < 3; i++) {
//计算发送回调的时间
LocalDateTime offset = LocalDateTimeUtil.offset(payTime, 5 * i, ChronoUnit.SECONDS);
int year = offset.getYear();
int monthValue = offset.getMonthValue();
int dayOfMonth = offset.getDayOfMonth();
int hour = offset.getHour();
int minute = offset.getMinute();
int second = offset.getSecond();
int finalI = i;
//这里一定要生成定时器的id,方便发送之后销毁定时器
String id = IdUtil.fastUUID();
CronUtil.schedule(id, StrUtil.format("{} {} {} {} {} ? {}", second, minute, hour, dayOfMonth, monthValue, year), (Task) () -> {
log.info("第{}次执行回调任务,订单id:{},发送时间:{}", finalI + 1, orderId, LocalDateTime.now());
// 发送
sendAction(url, JSONUtil.toJsonStr(notifyVo), orderId, id);
});
}
log.info("第一次执行回调任务,订单id:{},发送时间:{}", orderId, LocalDateTime.now());
// 此处表示立即发送第一次
sendAction(url, JSONUtil.toJsonStr(notifyVo), orderId, null);
}
void sendAction(String url, String body, Long orderId, String cronId) {
String post = null;
// 无论是否发送成功 只要有发送动作 就记为已发送 并且不应影响之后的定时任务
try {
post = HttpUtil.post(url, body);
log.info("发送支付回调请求,订单id:{},发送时间:{},响应结果:{},发送内容:{}", orderId, LocalDateTime.now(), post, body);
} catch (Exception e) {
log.error("发送支付回调请求失败,订单id:" + orderId + ",发送时间:" + LocalDateTime.now() + ",发送内容:" + body, e);
} finally {
// 这里注意,定时器不是执行之后就会自动销毁,这里必须手动销毁,不然运行久了会导致内存溢出等问题
if (StrUtil.isNotBlank(cronId)) {
log.info("移除支付回调定时任务,id:{}", cronId);
CronUtil.remove(cronId);
}
}
// 订单都会有个通知状态,未发送,已发送,已确认
// 如果发送回调,用户给我们返回Success,表示他们已经正确接收
LambdaUpdateWrapper<Order> updateWrapper = Wrappers.lambdaUpdate();
updateWrapper.eq(Order::getId, orderId)
.set(Order::getNotifyStatus, StrUtil.equalsIgnoreCase(post, "SUCCESS") ? OrderNotifyStatus.CONFIRMED : OrderNotifyStatus.SENDED)
.and(e -> e.eq(Order::getNotifyStatus, OrderNotifyStatus.unsent).or().eq(Order::getNotifyStatus, OrderNotifyStatus.SENDED));
orderService.update(updateWrapper);
}
3. 封装回调实体类及调用
回调实体类的内容没有固定格式,主要包含支付金额,商户id,订单号.订单状态
该方法,主要用于接收到三方支付回调后,再次向我们自己平台的商户发送回调,或者用户补发订单回调通知
@Override
public void sendNotify(Order order) {
Order res = getById(order);
NotifyVo notifyVo = new NotifyVo();
// 商户id
notifyVo.setMerchantId(res.getUserId());
// 支付金额
notifyVo.setAmount(NumberUtil.mul(res.getMoney(), 100).setScale(0, RoundingMode.DOWN).toString());
// 商户下单传递的订单id
notifyVo.setRequestId(res.getUserSetId());
// 我们平台生成的订单id
notifyVo.setOrderId(res.getId());
// 订单支付状态
notifyVo.setStatus("SUCCESS");
// 商户下单传递的通知地址
String notifyUrl = res.getNotifyUrl();
// 调用发送通知
sendNotify.action(notifyVo, notifyUrl, LocalDateTime.now(), order.getId());
}