技术分享

Laravel电商系统进阶:巧解商品弹性价格的库存计算难题

作者头像 人称外号大脸猫
22 阅读
Laravel电商系统进阶:巧解商品弹性价格的库存计算难题

当商品价格随库存量变化,如何精准计算可售库存?这个电商常见功能背后,藏着不少技术门道。

在电商系统中,我们经常会遇到这样的场景:商品价格不是固定的,而是根据购买数量的不同而变化。买得越多,单价越便宜——这就是经典的“弹性价格”或“阶梯价格”策略。

最近,我们在一个Laravel项目中实现了这个功能,但在处理库存计算时遇到了几个有趣的挑战。今天,就来和大家分享一下我们的解决方案。

一、需求背景:弹性价格如何运作?

想象一下,你正在销售一款热门商品:

  • 购买1-100件:每件¥10
  • 购买101-200件:每件¥8
  • 购买201-300件:每件¥6
  • 超过300件:每件¥6(保持最低价)

为了实现这个功能,我们设计了这样的数据库表:

CREATE TABLE `product_step_prices` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `product_id` int(11) DEFAULT NULL,
  `stock` int(11) DEFAULT NULL,  -- 库存阈值
  `price` int(11) DEFAULT NULL,  -- 对应价格
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

看起来很简单,对吧?但当我们开始实现库存计算时,问题出现了。

二、核心问题:库存到底该怎么算?

最初我们认为库存计算很简单:当前库存减去已售出数量。但在弹性价格场景下,事情变得复杂了。

假设我们有三个价格阶梯:

  • 阶梯1:库存100,价格¥10
  • 阶梯2:库存200,价格¥8
  • 阶梯3:库存300,价格¥6

用户看到库存时,到底应该显示什么?经过业务讨论,我们确定了这样的规则:

  1. 库存≤100:显示实际库存(比如50件就显示50)
  2. 100<库存≤200:显示超出第一阶梯的部分(比如150件显示50)
  3. 200<库存≤300:显示超出第二阶梯的部分(比如250件显示50)
  4. 库存>300:显示超出第三阶梯的部分(比如350件显示50)

为什么这样设计?因为这能告诉用户还需要多少才能达到下一个价格阶梯,刺激用户多购买。

三、技术实现:四种方案大比拼

方案一:基础条件判断法

这种方法最简单直观,适合阶梯数量固定的场景:

public function getStockAttribute()
{
    if ($this->type != 2) return $this->attributes['stock'];
    
    $currentStock = $this->attributes['stock'];
    $steps = $this->getStepStocks();
    
    if ($currentStock <= $steps[0]) {
        return $currentStock;
    } elseif ($currentStock <= ($steps[1] ?? PHP_INT_MAX)) {
        return $currentStock - $steps[0];
    } elseif ($currentStock <= ($steps[2] ?? PHP_INT_MAX)) {
        return $currentStock - $steps[1];
    } else {
        return $currentStock - end($steps);
    }
}

优点:代码简单,易于理解 缺点:阶梯数量变化时需要修改代码

方案二:通用循环遍历法

这种方法可以处理任意数量的价格阶梯:

public function getStockAttribute()
{
    if ($this->type != 2) return $this->attributes['stock'];
    
    $currentStock = $this->attributes['stock'];
    $steps = $this->getStepStocksSorted();
    
    if ($currentStock <= $steps[0]) {
        return $currentStock;
    }
    
    for ($i = 0; $i < count($steps); $i++) {
        if ($i === count($steps) - 1) {
            if ($currentStock >= $steps[$i]) {
                return $currentStock - $steps[$i];
            }
        } else {
            if ($currentStock > $steps[$i] && $currentStock <= $steps[$i + 1]) {
                return $currentStock - $steps[$i];
            }
        }
    }
    
    return $currentStock;
}

优点:适应性强,阶梯数量变化无需修改代码 缺点:时间复杂度O(n),阶梯多时性能稍差

方案三:二分查找优化法

当价格阶梯很多时,我们可以用二分查找来提升性能:

public function getStockAttribute()
{
    if ($this->type != 2) return $this->attributes['stock'];
    
    $currentStock = $this->attributes['stock'];
    $steps = $this->getStepStocksSorted();
    
    if ($currentStock <= $steps[0]) {
        return $currentStock;
    }
    
    $left = 0; $right = count($steps) - 1; $foundIndex = 0;
    
    while ($left <= $right) {
        $mid = intval(($left + $right) / 2);
        
        if ($currentStock <= $steps[$mid]) {
            $right = $mid - 1;
        } else {
            $foundIndex = $mid;
            $left = $mid + 1;
        }
    }
    
    return $currentStock - $steps[$foundIndex];
}

优点:性能好,时间复杂度O(log n) 缺点:代码相对复杂

方案四:Laravel集合优雅法

利用Laravel集合的强大功能,让代码更简洁:

public function getStockAttribute()
{
    if ($this->type != 2) return $this->attributes['stock'];
    
    $currentStock = $this->attributes['stock'];
    $steps = $this->step_prices()->orderBy('stock', 'asc')->pluck('stock');
    
    if ($steps->isEmpty()) return $currentStock;
    if ($currentStock <= $steps->first()) return $currentStock;
    
    $applicableStep = $steps->filter(function ($step) use ($currentStock) {
        return $step < $currentStock;
    })->last();
    
    return $currentStock - $applicableStep;
}

优点:代码简洁优雅,充分利用Laravel特性 缺点:需要理解Laravel集合的工作原理

四、性能优化:让计算飞起来

在实际项目中,我们还需要考虑性能问题。以下是几个优化技巧:

1. 添加缓存,减少数据库查询

protected function getStepStocksSorted()
{
    return Cache::remember(
        "product_step_stocks_{$this->id}",
        now()->addHours(2),
        function () {
            return $this->step_prices()
                ->orderBy('stock', 'asc')
                ->pluck('stock')
                ->toArray();
        }
    );
}

2. 数据库索引优化

-- 添加复合索引,大幅提升查询速度
ALTER TABLE `product_step_prices` 
ADD INDEX `idx_product_stock` (`product_id`, `stock`);

3. 避免N+1查询问题

// 查询时预加载关联数据
$products = Product::with('stepPrices')->where('type', 2)->get();

五、实战测试:确保万无一失

任何复杂的逻辑都需要充分的测试。我们编写了详细的测试用例:

public function testStepPriceStockCalculation()
{
    $product = Product::factory()->create(['type' => 2]);
    
    $product->stepPrices()->createMany([
        ['stock' => 100, 'price' => 1000],
        ['stock' => 200, 'price' => 800],
        ['stock' => 300, 'price' => 600],
    ]);
    
    $testCases = [
        50   => 50,    // 小于第一阶段
        100  => 100,   // 等于第一阶段
        150  => 50,    // 大于100,小于200
        200  => 100,   // 等于第二阶段
        250  => 50,    // 大于200,小于300
        300  => 100,   // 等于第三阶段
        350  => 50,    // 大于第三阶段
    ];
    
    foreach ($testCases as $inputStock => $expectedDisplay) {
        $product->stock = $inputStock;
        $this->assertEquals($expectedDisplay, $product->stock);
    }
}

六、经验总结

通过这个项目,我们学到了几个重要的经验:

  1. 业务理解是关键:技术实现前,一定要和产品经理充分沟通,理解业务需求
  2. 多种方案备选:同一个问题可能有多种解决方案,要根据实际情况选择
  3. 性能不容忽视:即使是简单的计算,在大数据量下也可能成为性能瓶颈
  4. 测试必须充分:边界条件往往是最容易出错的地方

弹性价格功能看似简单,实则涉及数据库设计、业务逻辑、性能优化等多个方面。在Laravel中,我们可以充分利用其优雅的语法和强大的功能,让实现过程更加顺畅。