当商品价格随库存量变化,如何精准计算可售库存?这个电商常见功能背后,藏着不少技术门道。
在电商系统中,我们经常会遇到这样的场景:商品价格不是固定的,而是根据购买数量的不同而变化。买得越多,单价越便宜——这就是经典的“弹性价格”或“阶梯价格”策略。
最近,我们在一个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
用户看到库存时,到底应该显示什么?经过业务讨论,我们确定了这样的规则:
- 库存≤100:显示实际库存(比如50件就显示50)
- 100<库存≤200:显示超出第一阶梯的部分(比如150件显示50)
- 200<库存≤300:显示超出第二阶梯的部分(比如250件显示50)
- 库存>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);
}
}
六、经验总结
通过这个项目,我们学到了几个重要的经验:
- 业务理解是关键:技术实现前,一定要和产品经理充分沟通,理解业务需求
- 多种方案备选:同一个问题可能有多种解决方案,要根据实际情况选择
- 性能不容忽视:即使是简单的计算,在大数据量下也可能成为性能瓶颈
- 测试必须充分:边界条件往往是最容易出错的地方
弹性价格功能看似简单,实则涉及数据库设计、业务逻辑、性能优化等多个方面。在Laravel中,我们可以充分利用其优雅的语法和强大的功能,让实现过程更加顺畅。