You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

退出ASP.NET结账页面时,如何回滚已执行的库存扣减操作?

ASP.NET结账页面退出时库存回滚的解决方案

首先得说,你现在的流程(Page_Load里直接扣减库存)其实是有风险的——用户可能刚进页面就关掉,或者没完成结账就跳转走,这时候库存就平白少了。所以核心思路要么是把库存扣减和订单提交绑定(原子操作),要么是临时锁定库存+超时自动回滚,下面给你具体拆解:

方案一:用数据库事务绑定订单提交与库存扣减(最推荐)

这是最稳妥的方式,从根源上避免了“需要回滚”的情况——只有当用户真正提交订单时,才执行库存扣减,而且和订单插入放在同一个事务里,要么都成功,要么都失败。

调整后的流程:

  1. Page_Load只做库存查询:进入页面时,只查询当前商品的可用库存并展示,不做任何扣减操作。
  2. 订单提交时执行事务:用户点击“提交订单”按钮后,开启数据库事务,先扣减库存,再插入订单记录,全部成功就提交事务,失败就回滚。

代码示例:

// 页面加载时仅查询库存
protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        int itemID = GetCurrentItemID(); // 从页面/URL获取商品ID
        // 查询可用库存并展示
        using (OleDbConnection conn = new OleDbConnection("你的Oracle连接字符串"))
        {
            conn.Open();
            string sql = "SELECT count FROM stock WHERE item_id = @itemID";
            using (OleDbCommand cmd = new OleDbCommand(sql, conn))
            {
                cmd.Parameters.AddWithValue("@itemID", itemID);
                int availableStock = Convert.ToInt32(cmd.ExecuteScalar());
                lblAvailableStock.Text = $"可用库存:{availableStock}";
            }
        }
    }
}

// 提交订单按钮点击事件
protected void btnSubmitOrder_Click(object sender, EventArgs e)
{
    int itemID = GetCurrentItemID();
    int stockToBePurchased = GetPurchaseQuantity(); // 从页面输入获取购买数量

    using (OleDbConnection conn = new OleDbConnection("你的Oracle连接字符串"))
    {
        conn.Open();
        // 开启事务
        using (OleDbTransaction tran = conn.BeginTransaction())
        {
            try
            {
                // 第一步:扣减库存(带库存校验)
                string decreaseSql = @"UPDATE stock 
                                      SET count = count - @stock 
                                      WHERE item_id = @itemID AND count >= @stock";
                using (OleDbCommand cmd = new OleDbCommand(decreaseSql, conn, tran))
                {
                    cmd.Parameters.AddWithValue("@stock", stockToBePurchased);
                    cmd.Parameters.AddWithValue("@itemID", itemID);
                    int rowsAffected = cmd.ExecuteNonQuery();

                    if (rowsAffected == 0)
                    {
                        throw new Exception("库存不足或商品不存在,无法完成订单");
                    }
                }

                // 第二步:插入订单记录
                string insertOrderSql = @"INSERT INTO orders 
                                         (item_id, quantity, create_time) 
                                         VALUES (@itemID, @stock, SYSDATE)";
                using (OleDbCommand cmd = new OleDbCommand(insertOrderSql, conn, tran))
                {
                    cmd.Parameters.AddWithValue("@itemID", itemID);
                    cmd.Parameters.AddWithValue("@stock", stockToBePurchased);
                    cmd.ExecuteNonQuery();
                }

                // 提交事务,所有操作生效
                tran.Commit();
                lblMessage.Text = "订单提交成功!";
            }
            catch (Exception ex)
            {
                // 回滚事务,所有操作撤销
                tran.Rollback();
                lblMessage.Text = $"提交失败:{ex.Message}";
            }
        }
    }
}

这个方案的好处是完全符合数据库事务的原子性,没有任何“中间状态”的库存损失,是电商场景下的标准做法。

方案二:临时库存锁定+超时自动回滚(适合需要提前锁库存的场景)

如果你的业务要求用户进入结账页面后就锁定库存(防止其他用户买走),那可以给库存表加临时标记,配合后台任务清理超时的锁定。

步骤:

  1. 修改库存表结构:新增两个字段:
    • temp_count(INT):记录临时扣减的库存数量
    • lock_expire_time(DATE):记录锁定的过期时间(比如15分钟)
  2. 进入页面时临时扣减库存:
private void TempLockStock(int itemID, int stockToBePurchased)
{
    using (OleDbConnection conn = new OleDbConnection("你的Oracle连接字符串"))
    {
        conn.Open();
        // 扣减可用库存,同时记录临时扣减和过期时间
        string sql = @"UPDATE stock 
                      SET count = count - @stock, 
                          temp_count = temp_count + @stock, 
                          lock_expire_time = SYSDATE + INTERVAL '15' MINUTE 
                      WHERE item_id = @itemID AND count >= @stock";
        using (OleDbCommand cmd = new OleDbCommand(sql, conn))
        {
            cmd.Parameters.AddWithValue("@stock", stockToBePurchased);
            cmd.Parameters.AddWithValue("@itemID", itemID);
            cmd.ExecuteNonQuery();
        }
    }
}
  1. 订单提交时确认扣减:把临时扣减转为正式扣减,清除临时标记:
private void ConfirmStockLock(int itemID, int stockToBePurchased)
{
    using (OleDbConnection conn = new OleDbConnection("你的Oracle连接字符串"))
    {
        conn.Open();
        using (OleDbTransaction tran = conn.BeginTransaction())
        {
            try
            {
                // 清除临时扣减标记
                string clearTempSql = @"UPDATE stock 
                                      SET temp_count = temp_count - @stock, 
                                          lock_expire_time = NULL 
                                      WHERE item_id = @itemID AND temp_count >= @stock";
                using (OleDbCommand cmd = new OleDbCommand(clearTempSql, conn, tran))
                {
                    cmd.Parameters.AddWithValue("@stock", stockToBePurchased);
                    cmd.Parameters.AddWithValue("@itemID", itemID);
                    cmd.ExecuteNonQuery();
                }

                // 插入订单记录(和方案一一致)
                // ...

                tran.Commit();
            }
            catch
            {
                tran.Rollback();
                throw;
            }
        }
    }
}
  1. 后台定时清理过期锁定:用Oracle的定时Job或者ASP.NET后台任务(比如Hangfire)定期执行SQL,把过期的临时扣减回滚:
UPDATE stock 
SET count = count + temp_count, 
    temp_count = 0, 
    lock_expire_time = NULL 
WHERE lock_expire_time IS NOT NULL AND lock_expire_time < SYSDATE;

方案三:Session结束事件回滚(局限性大,不推荐作为主方案)

ASP.NET的Session_End事件会在会话超时或用户主动注销时触发,但这个方案有很多限制:

  • 只有当Session模式为InProc(进程内)时才可靠,用StateServer或SQL Server Session时不会触发
  • 用户关闭浏览器后,Session不会立即结束,要等超时(默认20分钟),这段时间库存会不准确
  • 服务器重启或回收应用池时,Session丢失,无法回滚

如果一定要尝试,代码示例如下:

  1. Page_Load时记录扣减信息到Session:
protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        int itemID = 123;
        int stockToBePurchased = 2;
        DecreaseStockCount(itemID, stockToBePurchased);
        // 把回滚需要的信息存在Session里
        Session["StockRollbackData"] = new { ItemID = itemID, StockAmount = stockToBePurchased };
    }
}
  1. Global.asax中处理Session_End:
void Session_End(object sender, EventArgs e)
{
    var rollbackData = Session["StockRollbackData"] as dynamic;
    if (rollbackData != null)
    {
        // 执行库存回滚(增加库存)
        using (OleDbConnection conn = new OleDbConnection("你的Oracle连接字符串"))
        {
            conn.Open();
            string sql = "UPDATE stock SET count = count + @stock WHERE item_id = @itemID";
            using (OleDbCommand cmd = new OleDbCommand(sql, conn))
            {
                cmd.Parameters.AddWithValue("@stock", rollbackData.StockAmount);
                cmd.Parameters.AddWithValue("@itemID", rollbackData.ItemID);
                cmd.ExecuteNonQuery();
            }
        }
    }
}

内容的提问来源于stack exchange,提问作者Joseph

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 06:19:50