支付安全:大厂如何防止订单重复支付的揭秘

在如今数字支付横行的时代,我们常常在购物、服务支付等场景中使用在线支付。然而,你可曾想过,背后的支付系统是如何确保你的订单不会被重复支付,保障交易安全的呢?本文将带你深入了解大厂是如何通过巧妙的设计和多层次的防护机制来避免订单重复支付的。

支付流程概览

首先,我们先来简单了解一下典型的支付流程。通常,当我们提交订单后,支付流程会经过以下关键步骤:

1,提交订单: 用户在商家平台上提交订单,生成支付请求。

2,支付网关: 订单经过支付网关,连接着第三方支付渠道(如微信、支付宝、银联等)。

3,支付中心交互: 支付中心与第三方支付渠道进行交互,完成支付。

4,支付结果通知: 支付中心异步接收支付结果,更新支付状态,并通知业务应用进行订单状态更新。

常见问题:订单掉单

在这一过程中,可能会面临一个棘手的问题,即订单掉单。无论是由于网络超时、程序错误,还是其他原因,都有可能导致支付成功但订单状态未更新,从而引发用户投诉或重复支付。

1,外部掉单和内部掉单:

a,外部掉单: 由提交订单到支付成功的过程中,可能出现超时未收到回调通知等问题。

b,内部掉单: 在支付成功后,由于支付中心或业务应用自身问题,未能及时更新订单状态。

防范措施:支付流水状态和超时处理

2,防止订单重复支付,可以采取一系列措施:
a,支付流水状态: 在支付订单中引入一个中间状态,“支付中”。在支付时,检查是否有相同订单状态为“支付中”的支付流水,使用锁确保支付的原子性。支付完成后,再将状态更新为“支付成功”。

b,超时处理: 设定支付中心的自定义超时时间,如30秒。若在规定时间内未收到支付成功回调,主动查询支付结果,以确保及时更新。可在10秒、20秒、30秒等时间点查询,超过最大查询次数则进行异常处理。

3,异步通知和接口幂等性
在支付结果通知阶段,需要注意以下两个重要方面:

异步通知: 支付中心收到支付结果后,将结果同步给业务系统。这可以通过消息队列(MQ)或直接调用实现,但直接调用需考虑重试机制,可以定义实现重试策略或借助现成框架实现如SpringBoot Retry。

接口幂等性: 无论是支付中心还是业务应用,在接收支付结果通知时都要保证接口的幂等性,即同一消息只处理一次,忽略其余的重复通知。

4,业务应用的主动查询和超时处理
为了更全面地确保支付安全,业务应用也应主动参与:

a,超时主动查询: 发起支付时,将支付订单放入一张表中,通过定时任务定期扫描并查询支付结果。这确保即使异步通知失败,业务应用也能及时更新订单状态。

b,防止订单重复提交,可以通过业务数据唯一性生成字段,比如根据订单核心信息(单号,金额,日期,买方等核心信息)生成唯一哈希值,然后根据该唯一值创建重复提交判断,一般会创建一个分布式锁注解,通过配置索的key,在Redis中检查是否存在相同的标识,若存在则不允许重复提交,不存在则生成新的标识并设置过期时间,然后创建订单。
示例,分布式锁注解:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
    String key() default "";

    long timeout() default 5L;
}

AOP实现具体的锁的过程

@Aspect
@Component
@EnableAspectJAutoProxy(
    exposeProxy = true
)
public class DistributedLockAspect {
    private static final Logger log = LoggerFactory.getLogger(DistributedLockAspect.class);
    @Autowired
    private RedisUtil redisUtil;
    private static RedisUtil REDIS_UTIL;

    @PostConstruct
    public void init() {
        REDIS_UTIL = this.redisUtil;
    }

    public DistributedLockAspect() {
    }

    @Around("@annotation(com.atshuo.annotation.DistributedLock)")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes)Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        String ip = WebUtils.getIP(request);
        MethodSignature signature = (MethodSignature)point.getSignature();
        Method method = signature.getMethod();
        String className = method.getDeclaringClass().getName();
        String methodName = method.getName();
        String methodKey = String.format("%s#%s", className, methodName);
        int ipCode = MathUtils.abs(ip.hashCode());
        int methodCode = MathUtils.abs(methodKey.hashCode());
        String keyFmt = "%s_%d";
        String key = String.format(keyFmt, ipCode, methodCode);
        DistributedLock distributedLock = (DistributedLock)method.getAnnotation(DistributedLock.class);
        if (!sameUrlSubmit.isIpKey()) {
            key = String.format(keyFmt, "", methodCode);
        }

        if (StringUtils.isNotEmpty(distributedLock.key())) {
            List<String> paramNameList = Arrays.asList(signature.getParameterNames());
            List<Object> paramList = Arrays.asList(point.getArgs());
            ExpressionParser parser = new SpelExpressionParser();
            EvaluationContext ctx = new StandardEvaluationContext();

            int keyCode;
            for(keyCode = 0; keyCode < paramNameList.size(); ++keyCode) {
                ctx.setVariable((String)paramNameList.get(keyCode), paramList.get(keyCode));
            }

            keyCode = MathUtils.abs(parser.parseExpression(sameUrlSubmit.key()).getValue(ctx).toString().hashCode());
            key = key + "_" + keyCode;
        }

        long timeout = distributedLock.timeout();
        if (timeout < 0L) {
            timeout = CommonConstant.TIME_TO_SUBMIT;
        }

        boolean result = false;
        String cacheKey = "DistributedLock Key:" + key;

        try {
            result = REDIS_UTIL.setNX(cacheKey, UUID.randomUUID().toString(), timeout * 1000L);
        } catch (Exception var19) {
            if (Objects.isNull(REDIS_UTIL.get(cacheKey))) {
                REDIS_UTIL.set(cacheKey, UUID.randomUUID().toString());
                REDIS_UTIL.expire(cacheKey, timeout);
                return true;
            }

            return false;
        }

        if (!result) {
            throw new RuntimeException("请勿重复提交");
        } else {
            return point.proceed();
        }
    }
}

应用示例:在需要进行防重复提交的业务方法,增加注解,指定key和超时时间,截图如下
在这里插入图片描述

微信支付最佳实践

微信支付作为业界的佼佼者,提出了一些建议来优化支付系统:

1,重复提交锁:通过订单在提交支付时,先生成预支付单号,然后再发起微信支付,从而在外部实现防重复。而业务系统生成预支付单也进行防重复,即可保证整个订单支付环节的防重复支付。

2,合理设置超时时间: 根据支付业务的特点和实际情况,合理设置支付超时时间,确保用户支付体验和系统的高效运行。

3,合理使用异步通知: 合理设置异步通知的机制,保证系统可靠性和数据的一致性。

4,灵活配置支付方式: 根据不同支付方式的特点,进行灵活配置,确保系统的稳定性和安全性。
在这里插入图片描述

总结
通过上述措施,大型在线支付系统能够有效地防止订单重复支付,保障了用户的支付安全和交易顺利进行。从支付流水状态的控制、超时处理的设定,到异步通知和接口的幂等性保障,再到业务应用的主动查询和订单重复提交的防范,每个环节都是为了构建一个安全、高效的支付生态系统。

对于普通用户而言,这些繁琐的步骤都在幕后默默保护着我们的每一笔交易,确保我们的支付安全和便利。因此,在享受数字支付带来的便捷时,也可以更加放心地相信这些大厂背后的支付系统,它们正在用技术的力量为我们的支付保驾护航。

  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在uniapp开发环境下,如果你需要在iOS应用中实现支付功能,你需要使用苹果内购支付。这是因为苹果要求所有在App Store上架的应用,如果应用中涉及虚拟商品的购买,必须使用苹果内购支付,并且不能使用其他支付方式,如微信、支付宝等支付SDK。这是苹果的审核规定,如果违反规定,应用有可能被下架。\[1\] 与其他支付方式相比,苹果内购支付在技术实现上没有本质区别,都属于支付渠道的一种。然而,由于苹果服务器的原因,使用苹果内购支付可能会遇到一些特殊问题,例如回调时间长甚至没有回调、掉单、回调异常等情况。这些问题在uniapp的开发环境下尤为突出,因为uniapp在iOS上没有超时的回调机制。\[3\] 因此,如果你在uniapp开发中需要实现iOS支付功能,你需要注意处理这些特殊问题,并寻找解决方案来确保支付的稳定性和可靠性。\[2\] #### 引用[.reference_title] - *1* *2* *3* [【iOS内购支付】Uniapp拉起苹果内购支付注意事项、实现步骤以及踩过的坑(手把手教程)](https://blog.csdn.net/m0_46983722/article/details/129305869)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老D不是传说

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值