PAYATHON 2026

电脑网站支付未扫码时如何使支付宝旧价格支付页失效?

支付小周

结论

在支付宝电脑网站支付中,alipay.trade.page.pay 生成支付页面并返回浏览器后,页面里已经包含签名的 out_trade_no、total_amount 等参数。即使用户还没有扫码,支付宝侧也尚未创建交易,商户仍然无法主动撤销这份已经签名的旧支付页面。

所以,调用 alipay.trade.close 并不能让金额为 50 元的页面立即失效。建议这样处理:

  1. 每次改价都创建新的支付版本,并使用新的 out_trade_no。
  2. 在商户系统中将旧支付版本标记为失效,不再生成或展示对应的旧支付页面。
  3. 处理异步通知时,严格校验支付版本、金额和订单状态。
  4. 如果用户仍通过已经打开的旧页面支付了 50 元,不得按 60 元订单交付,应自动退款或转人工处理。
  5. 旧支付尝试一旦在支付宝侧生成交易,再调用 alipay.trade.close 将其关闭。

商户可以阻止旧支付记录继续履约,但无法保证已经发送到用户浏览器的支付宝签名页面绝对不能付款。

为什么无法直接关闭旧页面

alipay.trade.close 关闭的是支付宝侧已经存在的交易,不是浏览器中的 HTML 页面、跳转表单或二维码。

用户尚未扫码时,系统中可能只有商户调用 alipay.trade.page.pay 后生成的签名请求,支付宝侧还没有可以关闭的交易。此时:

  • 支付参数已经签名,商户无法事后修改其中的 total_amount;
  • 浏览器中的旧页面已经不受商户服务器控制;
  • 支付宝侧没有交易记录,alipay.trade.close 也就没有明确的关闭对象;
  • 用户之后仍可能提交旧表单,或者扫描旧页面中的二维码。

删除商户页面、清除缓存,或者把业务订单价格改成 60 元,都不会让用户已经打开的支付宝页面自动失效。

推荐的订单设计

不要让一个 out_trade_no 对应多个价格版本。业务订单和支付尝试应分别保存,例如:

字段示例说明
business_order_noA商户业务订单号
price_version2当前价格版本
pay_out_trade_noA-P2-202609090001本次支付尝试对应的支付宝商户订单号
expected_amount60.00该支付版本应付金额
statusPENDING支付尝试状态

订单从 50 元调整到 60 元时,应执行以下操作:

  1. 将原支付尝试标记为 INVALID 或 CANCELLED。
  2. 提升订单的价格版本。
  3. 使用新的 out_trade_no 和 total_amount=60.00 创建支付请求。
  4. 查询旧 out_trade_no 是否已经在支付宝侧形成交易。
  5. 如果交易已经形成且仍可关闭,调用 alipay.trade.close。
  6. 如果旧交易已经支付成功,进入退款流程,不再继续履约。

这种设计可以避免同一个 out_trade_no 对应不同金额,也更容易处理支付和改价同时发生的竞态。

支付页面生成前必须再次校验

商户自己的“去支付”接口不能直接信任前端提交的金额。服务端应读取业务订单的当前有效价格,并校验支付版本。

下面是简化的 Java 伪代码:

@PostMapping("/orders/{orderNo}/pay")
public String createPayment(@PathVariable String orderNo) throws Exception {
    BusinessOrder order = orderService.lockByOrderNo(orderNo);

    if (!order.isPayable()) {
        throw new IllegalStateException("订单当前不可支付");
    }

    PaymentAttempt attempt = paymentAttemptService.create(
        order.getOrderNo(),
        order.getPriceVersion(),
        order.getAmount()
    );

    AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
    request.setNotifyUrl("https://merchant.example.com/alipay/notify");
    request.setReturnUrl("https://merchant.example.com/pay/result");

    AlipayTradePagePayModel model = new AlipayTradePagePayModel();
    model.setOutTradeNo(attempt.getOutTradeNo());
    model.setTotalAmount(order.getAmount().setScale(2).toPlainString());
    model.setSubject("订单 " + order.getOrderNo());
    model.setProductCode("FAST_INSTANT_TRADE_PAY");

    request.setBizModel(model);

    return alipayClient.pageExecute(request).getBody();
}

total_amount 必须由服务端根据当前订单数据生成,不能直接采用浏览器传入的值。

改价时使商户侧旧支付尝试失效

改价操作应放在事务中,同时更新订单价格和支付版本:

@Transactional
public void changePrice(String orderNo, BigDecimal newAmount) {
    BusinessOrder order = orderRepository.lockByOrderNo(orderNo);

    if (order.isPaid()) {
        throw new IllegalStateException("订单已支付,不能直接改价");
    }

    paymentAttemptRepository.invalidatePendingAttempts(orderNo);

    order.setAmount(newAmount.setScale(2));
    order.setPriceVersion(order.getPriceVersion() + 1);
    orderRepository.save(order);
}

这一步只能让旧支付尝试在商户系统中失效,无法撤回已经到达浏览器的支付宝支付请求。因此,支付通知处理仍需承担最后一道校验。

异步通知必须核对金额和支付版本

收到支付宝异步通知后,除了验签,还要核对以下内容:

  • app_id 是否属于当前商户应用;
  • seller_id 是否为预期收款账号;
  • out_trade_no 是否存在;
  • trade_status 是否属于业务接受的状态;
  • total_amount 是否与该支付尝试的预期金额一致;
  • 支付尝试是否仍对应业务订单的当前价格版本;
  • 业务订单是否已被其他支付尝试完成;
  • 该通知是否已经被幂等处理。

示例逻辑:

@PostMapping("/alipay/notify")
public String handleNotify(HttpServletRequest request) {
    Map<String, String> params = extractAlipayParams(request);

    if (!AlipaySignature.rsaCheckV1(
            params,
            alipayPublicKey,
            StandardCharsets.UTF_8.name(),
            "RSA2")) {
        return "failure";
    }

    String outTradeNo = params.get("out_trade_no");
    String tradeNo = params.get("trade_no");
    String tradeStatus = params.get("trade_status");
    BigDecimal paidAmount = new BigDecimal(params.get("total_amount"));

    PaymentAttempt attempt =
        paymentAttemptService.lockByOutTradeNo(outTradeNo);

    if (attempt == null) {
        riskService.recordUnknownPayment(outTradeNo, tradeNo, paidAmount);
        return "success";
    }

    if (!paidAmount.equals(attempt.getExpectedAmount())) {
        refundService.scheduleRefund(tradeNo, paidAmount, "支付金额不匹配");
        return "success";
    }

    BusinessOrder order =
        orderService.lockByOrderNo(attempt.getBusinessOrderNo());

    boolean currentVersion =
        attempt.getPriceVersion() == order.getPriceVersion();

    if (!currentVersion || !attempt.isPending() || order.isPaid()) {
        refundService.scheduleRefund(tradeNo, paidAmount, "支付版本已失效");
        return "success";
    }

    if ("TRADE_SUCCESS".equals(tradeStatus)
            || "TRADE_FINISHED".equals(tradeStatus)) {
        orderService.markPaid(order, attempt, tradeNo, paidAmount);
    }

    return "success";
}

这段代码只展示业务结构。退款通常应交给独立、可重试且有幂等控制的流程执行,不能只记录一条日志。

返回 failure 只表示通知处理失败,支付宝可能再次发送通知。这并不意味着交易会自动撤销或退款。旧价格交易已经成功时,应明确调用退款接口处理。

发现支付宝交易后及时关闭

价格修改后,可以通过旧支付尝试的 out_trade_no 查询支付宝交易状态。如果交易已经创建但尚未支付,可以尝试调用:

alipay.trade.close

典型处理逻辑如下:

void closeOldPaymentAttempt(String oldOutTradeNo) {
    AlipayTradeQueryModel queryModel = new AlipayTradeQueryModel();
    queryModel.setOutTradeNo(oldOutTradeNo);

    // 查询到支付宝交易后,根据实际交易状态决定是否调用关闭接口。
    // 如果已经支付成功,应进入退款或异常支付处理流程。
}

查询和关闭之间存在时间窗口,用户可能正好在这段时间完成支付。因此,无论关闭接口正在调用还是已经调用成功,异步通知处理都必须继续校验支付版本,不能只依赖关闭接口。

可以增加支付有效期,但不能替代改价控制

创建支付请求时,可以根据当前接口实际支持的业务参数设置支付有效期,例如 timeout_express 或 time_expire。具体可用参数、格式和限制,应以正在使用的支付宝接口文档及 SDK 模型为准。

有效期只能缩短旧页面的可支付时间,不能在改价时动态修改已经签名的支付请求。如果业务要求“改价后立即禁止旧金额成交”,仅设置超时时间仍然不够。

更稳妥的交互方式

为了缩短旧页面仍可使用的时间,可以采用以下设计:

  • 用户点击“确认支付”后,服务端再创建支付宝支付请求;
  • 尽量缩短支付请求的有效期;
  • 改价后强制刷新订单确认页;
  • 每次支付都使用一次性的支付尝试编号;
  • 页面轮询订单版本,发现改价后立即停止商户侧跳转;
  • 不提前长期保存或重复展示已经生成的支付表单。

这些措施可以降低用户继续使用旧支付页面的概率,但无法彻底消除这种情况。签名后的支付宝页面一旦交给用户,就要按“旧金额仍可能被支付”来设计后续流程,通过拦截履约、关闭交易和自动退款兜底。

注意事项

  1. 不要用同一个 out_trade_no 反复提交不同的 total_amount。支付宝交易一旦形成,关键交易信息通常不能由新的支付请求随意覆盖。

  2. 比较金额时应使用 BigDecimal,不要使用 double:

BigDecimal expected = new BigDecimal("60.00");
BigDecimal actual = new BigDecimal(params.get("total_amount"));

if (expected.compareTo(actual) != 0) {
    // 金额不一致
}
  1. 不能根据同步跳转页面判断支付成功。支付结果应以验签后的异步通知为主,必要时再调用交易查询接口确认。

  2. 改价、支付通知、交易关闭和退款可能并发发生。数据库操作应使用事务、条件更新或行锁,通知、关闭和退款操作也要有幂等控制。

  3. 如果用户按旧价格支付成功,不应简单补差价后自动发货,除非业务规则明确允许,而且用户已经确认。默认做法应是拦截履约并退款。

这个问题无法通过“撤销浏览器中的旧支付宝页面”解决。系统需要隔离不同的支付版本,在服务端校验状态,并结合交易关闭和退款机制,确保订单不会按错误价格履约。

备注:内容仅供参考。