PAYATHON 2026

支付宝支付中如何识别同一用户批量支付并防止恶意刷单

支付老李

结论

支付宝能帮助商户识别付款账户,但仅靠支付宝用户标识无法做好防刷单。比较稳妥的方案是:

  1. 下单前,根据商户自己的 user_id、设备、IP、收货信息等维度限制频率。
  2. 支付成功后,从支付宝异步通知或主动查询结果中读取 buyer_id,识别同一支付宝账户的重复支付。
  3. 通过唯一商户订单号、数据库唯一约束和幂等处理,防止同一订单被重复创建或重复记账。
  4. 请求超限时,根据风险程度采取拒绝下单、延迟处理、验证码或人工审核等措施,不要直接封禁所有重复支付。

支付宝通常要在受理或完成交易后,才能可靠返回 buyer_id。因此,buyer_id 适合用于支付后的关联分析,不能代替下单前的风控。

可以用哪些信息识别同一用户

识别维度可以分为三类。

商户侧身份

这是下单前最可靠的主标识,主要包括:

  • 商户账号 user_id
  • 登录会话
  • 实名认证信息的内部标识
  • 会员账号
  • 收货手机号或收货地址的标准化摘要

服务端必须从登录态中取得 user_id,不能直接相信客户端提交的用户编号。

支付宝侧身份

支付宝支付结果中可能包含以下买家标识:

  • buyer_id:买家的支付宝用户标识
  • buyer_logon_id:经过脱敏处理的支付宝登录账号,是否返回取决于接口和实际响应

具体有哪些字段,应以所用支付产品的接口文档和真实响应为准。不要把 buyer_logon_id 用作唯一键,因为该字段可能不返回,而且通常已经脱敏。关联支付宝用户时,应优先使用 buyer_id。

商户不能根据客户端传入的数据判断实际付款人。这些字段应从验签通过的支付宝异步通知中读取,或者由服务端调用交易查询接口取得。

环境与行为特征

这些信息可以帮助发现账号切换和批量注册等行为:

  • IP 地址及网段
  • 设备标识
  • 浏览器或 App 环境
  • 收货手机号、地址
  • 优惠券、活动资格
  • 下单频率和金额分布
  • 创建订单后长期不支付的比例
  • 多个账号是否共用设备、地址或付款账户

IP 只能作为辅助条件。公司网络、校园网和移动运营商出口都可能让大量正常用户共用一个公网 IP。

推荐的处理流程

1. 下单接口先做基础校验

创建支付宝交易前,商户后端应先检查:

  • 用户是否已登录
  • 商品是否有效
  • 价格是否由服务端计算
  • 是否已有相同业务的未支付订单
  • 当前用户是否超过频率或数量限制
  • 活动资格、库存和优惠券是否允许重复使用

例如,同一用户再次购买同一商品时,可以返回尚未过期的待支付订单,避免不断创建新订单。

2. 为每笔业务生成唯一订单号

支付宝接口中的 out_trade_no 应对应商户系统中的唯一订单,并在数据库中建立唯一索引:

CREATE UNIQUE INDEX uk_payment_out_trade_no
ON payment_order(out_trade_no);

如果一笔业务订单只能支付一次,还应为业务订单编号建立唯一约束:

CREATE UNIQUE INDEX uk_payment_business_order
ON payment_order(business_order_id);

唯一约束是防止重复数据的最后一道防线。只采用“先查询再插入”会有并发竞争,两个请求可能同时查到记录不存在,随后分别创建订单。

3. 使用滑动窗口限制下单频率

可以根据业务设置多层阈值,例如:

  • 同一用户一分钟内创建订单的次数
  • 同一用户一小时内的未支付订单数
  • 同一设备一天内关联的账号数
  • 同一 buyer_id 一天内参加同一活动的次数
  • 同一收货信息对应的优惠订单数

阈值需要根据实际业务数据确定。普通商品购买、充值、抽奖活动和限量优惠适用的阈值不同,没有一套通用的固定数字。

4. 支付通知验签后再记录付款人

收到异步通知后,应按以下顺序处理:

  1. 验证支付宝签名。
  2. 检查 app_id、商户身份和订单号等信息是否匹配。
  3. 查询本地订单并核对订单金额。
  4. 检查交易状态是否符合业务要求。
  5. 记录支付宝交易号 trade_no 和 buyer_id。
  6. 幂等更新订单状态并发放权益。
  7. 返回支付宝要求的成功响应。

不能因为请求参数中有 trade_status=TRADE_SUCCESS 就直接发货,因为未验签的参数可以被伪造。

5. 处理支付后的关联风险

取得 buyer_id 后,可以统计同一付款账户关联了多少商户账号、活动订单或收货信息。

如果支付已经成功,通常不应简单地把订单改为失败。可以根据业务风险采取以下措施:

  • 暂缓发货或延迟发放高价值权益
  • 将订单转入人工审核
  • 禁止继续参加同一优惠活动
  • 提高后续订单的验证等级
  • 在规则允许且确有业务依据时退款

退款和冻结权益等处理需要结合商品性质、活动规则与用户协议。不能仅因多个账号共用付款账户,就认定存在恶意行为。家庭成员代付、企业统一采购也可能是正常情况。

Java 示例:下单限流与订单复用

下面使用 Redis 计数说明基本思路。示例只包含核心逻辑,实际项目还需要处理 Redis 异常、数据库事务和订单过期。

public PaymentOrder createPaymentOrder(
        long userId,
        long productId,
        String deviceId,
        String clientIp) {

    String userRateKey = "pay:create:user:" + userId;
    long userCount = redisCounter.increment(userRateKey, Duration.ofMinutes(1));

    if (userCount > 5) {
        throw new BizException("操作过于频繁,请稍后再试");
    }

    String deviceRateKey = "pay:create:device:" + sha256(deviceId);
    long deviceCount = redisCounter.increment(
            deviceRateKey,
            Duration.ofMinutes(10)
    );

    if (deviceCount > 20) {
        riskService.record(
                userId,
                "HIGH_FREQUENCY_DEVICE",
                clientIp
        );
        throw new BizException("当前请求存在风险,请稍后再试");
    }

    PaymentOrder pendingOrder =
            paymentOrderRepository.findValidPendingOrder(userId, productId);

    if (pendingOrder != null) {
        return pendingOrder;
    }

    Product product = productRepository.findAvailable(productId);
    if (product == null) {
        throw new BizException("商品不存在或已下架");
    }

    PaymentOrder order = new PaymentOrder();
    order.setOutTradeNo(orderNoGenerator.next());
    order.setUserId(userId);
    order.setProductId(productId);
    order.setAmount(product.getCurrentPrice());
    order.setStatus("PENDING");
    order.setExpiredAt(Instant.now().plus(Duration.ofMinutes(15)));

    paymentOrderRepository.insert(order);
    return order;
}

代码中的 5 和 20 仅用于演示,不是推荐的生产阈值。正式上线前,应结合正常用户的下单分布、峰值流量和误拦截成本进行配置,并支持动态调整。

Redis 的“递增并设置过期时间”操作应使用 Lua 脚本或其他原子方案实现,以免计数成功后未能设置过期时间。

local count = redis.call("INCR", KEYS[1])

if count == 1 then
    redis.call("PEXPIRE", KEYS[1], ARGV[1])
end

return count

Java 示例:处理支付宝异步通知

不同版本的支付宝 Java SDK 可能使用不同的类和方法。下面的代码主要展示处理顺序:

public String handleAlipayNotify(Map<String, String> params) {
    boolean signatureValid = AlipaySignature.rsaCheckV1(
            params,
            alipayPublicKey,
            "UTF-8",
            "RSA2"
    );

    if (!signatureValid) {
        return "failure";
    }

    String appId = params.get("app_id");
    String outTradeNo = params.get("out_trade_no");
    String tradeNo = params.get("trade_no");
    String tradeStatus = params.get("trade_status");
    String buyerId = params.get("buyer_id");
    String totalAmount = params.get("total_amount");

    if (!configuredAppId.equals(appId)) {
        return "failure";
    }

    PaymentOrder order =
            paymentOrderRepository.findByOutTradeNo(outTradeNo);

    if (order == null) {
        return "failure";
    }

    BigDecimal notifiedAmount = new BigDecimal(totalAmount);
    if (order.getAmount().compareTo(notifiedAmount) != 0) {
        riskService.record(
                order.getUserId(),
                "PAYMENT_AMOUNT_MISMATCH",
                outTradeNo
        );
        return "failure";
    }

    if (!"TRADE_SUCCESS".equals(tradeStatus)
            && !"TRADE_FINISHED".equals(tradeStatus)) {
        return "success";
    }

    paymentService.confirmPaymentIdempotently(
            order.getId(),
            tradeNo,
            buyerId
    );

    return "success";
}

幂等确认应放在数据库事务中,并使用条件更新或唯一索引防止重复记账:

UPDATE payment_order
SET status = 'PAID',
    alipay_trade_no = :tradeNo,
    alipay_buyer_id = :buyerId,
    paid_at = CURRENT_TIMESTAMP
WHERE id = :orderId
  AND status = 'PENDING';

程序还应检查实际更新的行数:

  • 更新一行:本次支付确认成功。
  • 更新零行:订单可能已经处理,需要查询当前状态,不能再次发货。

还可以为 alipay_trade_no 建立唯一索引,防止同一笔支付宝交易被错误关联到多笔本地订单:

CREATE UNIQUE INDEX uk_payment_alipay_trade_no
ON payment_order(alipay_trade_no);

字段是否允许为空,以及唯一索引如何处理 NULL,需要根据实际使用的数据库确认。

建立风险评分,而不是只设一条拦截规则

单一规则很容易误伤正常用户。生产环境通常会结合多个信号计算风险分:

risk_score =
    user_frequency_score
  + device_account_score
  + ip_frequency_score
  + buyer_account_score
  + unpaid_order_score
  + promotion_abuse_score

不同风险等级可以采用不同的处理方式:

  • 低风险:正常创建支付。
  • 中风险:复用已有订单、要求验证码或缩短订单有效期。
  • 高风险:拒绝用户继续参加优惠活动,或者转入人工审核。
  • 已确认滥用:限制账号、设备或活动资格,并保留审计记录。

风控规则最好与普通支付能力分开处理。用户即使失去某项优惠活动的资格,通常仍可以按正常价格购买商品,除非已经存在明确的安全风险。

容易忽略的问题

不要在客户端生成金额

客户端只传递商品、数量或业务参数。最终金额必须由服务端按照商品和优惠规则重新计算,并在收到支付通知后再次核对。

不要把支付成功通知当成只会发送一次

支付宝异步通知可能重复发送,也可能乱序到达。订单更新、库存扣减、优惠券核销和权益发放都需要支持幂等。

不要只按 IP 封禁

共享网络、代理和运营商 NAT 都可能造成误判。IP 适合用于风险评分,不适合单独用来识别用户。

不要把 buyer_id 暴露给客户端

buyer_id 是支付关联数据,只应在服务端保存和使用。日志、报表和风控系统也需要限制访问权限,必要时可进行脱敏或摘要处理。

做好降级策略

如果 Redis 或风控服务暂时不可用,应提前确定相应的处理策略:

  • 普通低价值商品可以降级放行,同时加强事后监控。
  • 高价值优惠、充值或虚拟商品可以暂缓处理。
  • 即使风控降级,数据库唯一约束和支付幂等也不能取消。

区分“重复下单”和“重复支付”

重复创建未支付订单,可以通过订单复用、限流和过期机制处理。同一订单收到重复通知,应通过幂等机制处理。同一付款账户购买多笔不同订单,则属于业务风控问题。这三种情况的原因和处理方式不同,不应放在同一条判断规则中。

整个方案应有两道防线。下单前,根据商户账号和行为特征实时限制;支付后,再根据验签结果中的 buyer_id 关联账户并复核风险。这样既能发现批量支付,也能避免把正常的重复购买或家庭代付都判定为刷单。

备注:内容仅供参考。