PAYATHON 2026

如何在 ASP.NET Session 过期前延长 timeout

支付阿杰

结论

可以在用户跳转到支付网关前,通过 Session.Timeout 临时延长当前用户的 Session,例如从 20 分钟延长到 60 分钟。不过,ASP.NET 没有可靠的机制可以“暂停 Session 过期”。用户停留在第三方支付页面时,网站收不到请求,Session 仍会继续计时。

订单、座位锁定状态和支付结果不应只保存在 Session 中。应将待支付订单持久化到数据库,并通过订单号关联支付请求、同步返回和异步通知。这样,即使 Session 已经过期,系统仍能确认订单状态。

临时延长当前 Session

跳转到支付网关前,可以设置 Session.Timeout。该属性以分钟为单位,只影响当前 Session:

protected void PayButton_Click(object sender, EventArgs e)
{
    string orderId = CreatePendingOrder();

    Session["PendingOrderId"] = orderId;

    // 临时将当前 Session 的超时时间延长到 60 分钟
    Session.Timeout = 60;

    string paymentUrl = BuildPaymentUrl(orderId);
    Response.Redirect(paymentUrl);
}

如果用户成功返回网站,并且原 Session 还没有过期,可以恢复默认值:

protected void PaymentReturn_Load(object sender, EventArgs e)
{
    string orderId = Request.QueryString["orderId"];

    VerifyAndProcessPayment(orderId);

    // 恢复当前 Session 的默认超时时间
    Session.Timeout = 20;
}

这种做法只能降低支付期间 Session 过期的概率,无法保证 Session 一直存在。应用程序重启、应用程序池回收,以及使用 InProc 模式时服务器进程异常退出,都可能导致 Session 提前丢失。

global.asax 能做什么

可以在 Session_Start 中设置默认超时时间:

protected void Session_Start(object sender, EventArgs e)
{
    Session.Timeout = 20;
}

这种方式适合设置所有 Session 的初始值,但无法判断用户是否正在第三方支付页面。用户离开网站后,global.asax 不会持续收到其状态,也不能暂停 Session 计时。

理论上,可以在部分请求事件中根据当前路径调整 Session.Timeout。但最后一次请求结束后,ASP.NET 仍然只能等待设定的时间,无法判断用户正在支付、已经关闭浏览器,还是离开了网站。

也不要依赖 Session_End 释放座位或取消订单。它只会在部分 Session 存储模式下触发,不能作为可靠的业务事件通知。

推荐的支付流程

跳转到支付网关前,应先创建一条持久化订单记录,其中可以包含:

  • 不可猜测的订单号
  • 用户标识
  • 电影场次和座位
  • 应付金额
  • Pending、Paid、Cancelled 等订单状态
  • 座位保留截止时间
  • 支付网关的交易号
  • 创建时间和支付完成时间

流程示例如下:

public ActionResult StartPayment()
{
    PendingOrder order = orderService.CreatePendingOrder(
        userId: GetCurrentUserId(),
        showId: GetSelectedShowId(),
        seats: GetSelectedSeats()
    );

    // Session 只用于改善用户体验,不作为订单的唯一存储位置
    Session["PendingOrderId"] = order.Id;
    Session.Timeout = 60;

    string paymentUrl = paymentGateway.CreatePaymentUrl(
        orderId: order.Id,
        amount: order.Amount,
        returnUrl: Url.Action(
            "PaymentReturn",
            "Payment",
            null,
            Request.Url.Scheme
        )
    );

    return Redirect(paymentUrl);
}

处理支付返回时,应使用支付网关返回的订单号重新查询数据库,不能只读取 Session:

public ActionResult PaymentReturn(string orderId)
{
    PendingOrder order = orderService.GetById(orderId);

    if (order == null)
    {
        return HttpNotFound();
    }

    PaymentResult result = paymentGateway.VerifyPayment(Request);

    if (result.IsSuccessful &&
        result.OrderId == order.Id &&
        result.Amount == order.Amount)
    {
        orderService.MarkAsPaid(order.Id, result.TransactionId);
    }

    if (Session != null)
    {
        Session.Timeout = 20;
    }

    return View(orderService.GetById(order.Id));
}

签名字段和验签方式取决于具体的支付网关。不能只相信浏览器查询参数中的“支付成功”标志,金额、订单号和交易状态都需要在服务端验证。

还需要处理异步支付通知

用户完成支付后不一定会返回网站,也可能直接关闭支付页面。因此,最好通过支付网关的服务器端回调或 Webhook 确认支付结果:

[HttpPost]
public ActionResult PaymentCallback()
{
    PaymentResult result = paymentGateway.VerifyCallback(Request);

    if (!result.IsSuccessful)
    {
        return new HttpStatusCodeResult(400);
    }

    PendingOrder order = orderService.GetById(result.OrderId);

    if (order == null || order.Amount != result.Amount)
    {
        return new HttpStatusCodeResult(400);
    }

    // 应实现幂等处理,避免重复通知造成重复扣库存或重复出票
    orderService.MarkAsPaid(order.Id, result.TransactionId);

    return Content("OK");
}

注意事项

延长 Session.Timeout 只能作为辅助措施。支付和订票涉及关键业务状态,必须持久化,并设置独立的订单或座位保留期限,例如“创建订单后 15 分钟内完成支付”。该期限应由数据库时间和后台任务控制,不能取决于 Session 是否存在。

如果 Session 使用 InProc 模式,应用程序池回收会清空全部 Session。需要提高普通 Session 的可用性时,可以根据部署环境考虑 StateServer、SQL Server Session State 或其他分布式存储。即使采用这些方案,也不能把 Session 当作支付订单的唯一数据源。

备注:内容仅供参考。