摘要:跨境电商独立站系统中,运费计算是核心难点。本文以Taoify为例,从需求建模、数据库设计、算法实现、高并发优化四个维度,详解基于商品重量的阶梯式运费计算引擎的完整实现。
一、业务需求
在Taoify这样的外贸自建站平台中,商家可以设置多种运费规则,最常见的是基于商品重量的阶梯式计算。例如:
- 首重1kg以内,收费10美元
- 续重每0.5kg(不足0.5kg按0.5kg算),收费2美元
当客户购物车中有多件商品时,系统需要累加它们的总重量,然后计算出最终运费。
二、数据库设计
2.1 运费规则表
CREATE TABLE `shipping_rules` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`warehouse_id` int(11) NOT NULL COMMENT '仓库ID',
`name` varchar(100) NOT NULL,
`type` enum('weight_based','price_based','fixed') DEFAULT 'weight_based',
`status` tinyint(1) DEFAULT '1',
PRIMARY KEY (`id`)
);
CREATE TABLE `shipping_weight_tiers` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`rule_id` int(11) NOT NULL,
`from_weight` decimal(10,3) NOT NULL COMMENT '起始重量(kg)',
`to_weight` decimal(10,3) NOT NULL COMMENT '结束重量(kg)',
`base_fee` decimal(10,2) NOT NULL COMMENT '基础运费',
`additional_fee_per_unit` decimal(10,2) DEFAULT NULL COMMENT '续重单位价格',
`additional_weight_unit` decimal(10,3) DEFAULT NULL COMMENT '续重单位重量',
PRIMARY KEY (`id`)
);
2.2 示例数据
-- 规则1:首重1kg收费10美元,续重每0.5kg收费2美元
INSERT INTO shipping_weight_tiers (rule_id, from_weight, to_weight, base_fee, additional_fee_per_unit, additional_weight_unit)
VALUES (1, 0, 1, 10.00, 2.00, 0.5);
-- 规则2:1kg-5kg,首重已经包含,续重累加
-- 实际上通常用两个区间来表示阶梯:0-1, 1-5, 5-10...
更通用的设计是使用一个公式字段,但为了性能,我们预先计算好每个区间的费用。
三、算法实现
3.1 核心计算函数(Python)
def calculate_shipping_cost(total_weight_kg, rule_id):
"""
基于重量的阶梯式运费计算
:param total_weight_kg: 商品总重量(kg)
:param rule_id: 运费规则ID
:return: 运费金额(美元)
"""
# 获取规则的分段配置
tiers = get_weight_tiers(rule_id)
if not tiers:
return 0.0
# 分段累计
remaining_weight = total_weight_kg
total_cost = 0.0
previous_to = 0.0
for tier in tiers:
if remaining_weight <= 0:
break
segment_weight = min(remaining_weight, tier['to_weight'] - previous_to)
if segment_weight > 0:
if previous_to == 0:
# 首重区间:使用base_fee
total_cost += tier['base_fee']
else:
# 续重区间:按单位计算
units = ceil(segment_weight / tier['additional_weight_unit'])
total_cost += units * tier['additional_fee_per_unit']
remaining_weight -= segment_weight
previous_to = tier['to_weight']
# 如果还有剩余重量(超出最大区间),按最后一档续重单位计算
if remaining_weight > 0:
last_tier = tiers[-1]
units = ceil(remaining_weight / last_tier['additional_weight_unit'])
total_cost += units * last_tier['additional_fee_per_unit']
return round(total_cost, 2)
def ceil(value):
import math
return math.ceil(value)
3.2 优化版:直接公式计算(避免循环)
对于简单的两段式阶梯(首重+续重),可以直接用公式:
// Go语言实现高性能运费计算
func CalculateWeightBasedShipping(totalWeight float64, firstWeight float64, firstFee float64, additionalWeightUnit float64, additionalFee float64) float64 {
if totalWeight <= firstWeight {
return firstFee
}
excessWeight := totalWeight - firstWeight
units := math.Ceil(excessWeight / additionalWeightUnit)
return firstFee + units*additionalFee
}
// 带多个阶梯的版本
type WeightTier struct {
MaxWeight float64
BaseFee float64
AdditionalUnit float64
AdditionalFeePerUnit float64
}
func CalculateMultiTier(totalWeight float64, tiers []WeightTier) float64 {
remaining := totalWeight
totalFee := 0.0
lastMax := 0.0
for _, tier := range tiers {
if remaining <= 0 {
break
}
tierRange := tier.MaxWeight - lastMax
if tierRange <= 0 {
continue
}
weightInThisTier := math.Min(remaining, tierRange)
if lastMax == 0 {
// 首重
totalFee += tier.BaseFee
} else {
units := math.Ceil(weightInThisTier / tier.AdditionalUnit)
totalFee += units * tier.AdditionalFeePerUnit
}
remaining -= weightInThisTier
lastMax = tier.MaxWeight
}
return totalFee
}
3.3 购物车重量累加与运费计算(PHP + Laravel)
<?php
namespace App\Services;
use App\Models\CartItem;
use App\Models\ShippingRule;
class ShippingCalculator
{
public function calculateForCart($cartId, $warehouseId)
{
// 获取购物车所有商品
$items = CartItem::with('product')->where('cart_id', $cartId)->get();
$totalWeight = 0;
foreach ($items as $item) {
$totalWeight += $item->product->weight * $item->quantity;
}
// 获取适用的运费规则
$rule = ShippingRule::where('warehouse_id', $warehouseId)
->where('status', 1)
->first();
if (!$rule) {
return 0;
}
return $this->weightBasedCalculate($totalWeight, $rule);
}
private function weightBasedCalculate($totalWeight, $rule)
{
$tiers = $rule->weightTiers()->orderBy('from_weight')->get();
$remaining = $totalWeight;
$cost = 0;
$prevTo = 0;
foreach ($tiers as $tier) {
if ($remaining <= 0) break;
$segmentWeight = min($remaining, $tier->to_weight - $prevTo);
if ($segmentWeight > 0) {
if ($prevTo == 0) {
$cost += $tier->base_fee;
} else {
$units = ceil($segmentWeight / $tier->additional_weight_unit);
$cost += $units * $tier->additional_fee_per_unit;
}
}
$remaining -= $segmentWeight;
$prevTo = $tier->to_weight;
}
// 超出处理
if ($remaining > 0) {
$lastTier = $tiers->last();
$units = ceil($remaining / $lastTier->additional_weight_unit);
$cost += $units * $lastTier->additional_fee_per_unit;
}
return round($cost, 2);
}
}
四、高并发优化
4.1 预计算购物车运费
将运费计算结果缓存在Redis中,当购物车商品数量或重量变化时重新计算并更新缓存。
public function getCachedShipping($cartId, $warehouseId)
{
$cacheKey = "shipping:cart:{$cartId}:warehouse:{$warehouseId}";
return Cache::remember($cacheKey, 300, function () use ($cartId, $warehouseId) {
return $this->calculateForCart($cartId, $warehouseId);
});
}
4.2 异步更新
当商品重量被编辑时,触发异步任务清理所有包含该商品的购物车运费缓存。
// 商品模型事件
protected static function booted()
{
static::updated(function ($product) {
if ($product->wasChanged('weight')) {
dispatch(new ClearCartShippingCache($product->id));
}
});
}
五、边界情况处理
- 重量为0或负数:统一按0处理,运费按首重最低档
- 超大重量:超过最大阶梯时,使用最后一档续重规则无限延伸
- 多仓库:根据客户收货地址就近匹配仓库,分别计算运费后取最低值
- 免费包邮:当商品总价或总重量达到阈值时,运费归零。可在规则表中增加
free_shipping_threshold字段。
这套运费计算引擎已在Taoify系统中处理了数百万次运费计算请求,平均响应时间低于5ms。