fiberphp-ext / engine
⚙️ FiberPHP 规则引擎 —— 促销规则(17 种)与运费规则(7 种)计算,支持独立/顺序/锁定最优三种计算模式。
dev-master
2026-08-23 06:14 UTC
Requires
- php: >=8.3
- ext-json: *
- fiberphp/framework: *
Requires (Dev)
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-08-23 06:15:03 UTC
README
一个灵活、可扩展的规则引擎,支持促销规则和运费规则的计算。
目录结构
src/
├── Engine.php # 统一入口 (FiberPHP\Engine\Engine)
├── Helper.php # 工具类 (类名解析、购物车创建等)
├── Install.php # 安装器(#[Package] 发布配置)
├── Common/ # 公共组件
│ ├── Contract/ # 接口与抽象基类
│ │ ├── CalculatorInterface.php
│ │ ├── AbstractCalculator.php
│ │ ├── RuleInterface.php
│ │ ├── AbstractRule.php
│ │ ├── ValidatorInterface.php
│ │ └── AbstractValidator.php
│ ├── Model/ # 数据模型
│ │ ├── Cart.php
│ │ └── Member.php
│ └── Exception/ # 异常处理
│ └── EngineException.php
├── Promotion/ # 促销规则引擎
│ ├── Promotion.php # 促销计算核心
│ ├── Rule/ # 促销规则
│ │ ├── AbstractRule.php
│ │ ├── TieredReduction.php # 阶梯满减
│ │ ├── TieredDiscount.php # 阶梯满折
│ │ ├── TieredQuantityReduction.php # 阶梯数量立减
│ │ ├── TieredQuantityDiscount.php # 阶梯数量折扣
│ │ ├── TieredNthItemDiscount.php # 阶梯第N件折扣
│ │ ├── TieredNthItemReduction.php # 阶梯第N件立减
│ │ ├── RankDiscount.php # 会员等级折扣
│ │ ├── RankReduction.php # 会员等级立减
│ │ ├── Coupon.php # 优惠券
│ │ ├── PointsRedemption.php # 积分抵扣
│ │ ├── PointsMultiplier.php # 积分翻倍
│ │ ├── MemberDayDiscount.php # 会员日折扣
│ │ ├── BirthdayDiscount.php # 生日折扣
│ │ ├── HolidayDiscount.php # 节日折扣
│ │ ├── TimeSlotPricing.php # 时段定价
│ │ ├── BuyGift.php # 买赠规则
│ │ ├── FullGift.php # 满赠规则
│ │ ├── GiftSelection.php # 赠品选择
│ │ └── TradeIn.php # 满额换购
│ ├── Calculator/ # 计算策略
│ │ ├── Independent.php # 独立计算(叠加)
│ │ ├── Sequential.php # 顺序计算(依次减免)
│ │ └── Lock.php # 锁定最优(选最优)
│ └── Validate/ # 促销规则验证器
│ ├── AbstractValidator.php
│ ├── TieredReduction.php
│ ├── TieredDiscount.php
│ ├── TieredQuantityReduction.php
│ ├── TieredQuantityDiscount.php
│ ├── TieredNthItemDiscount.php
│ ├── TieredNthItemReduction.php
│ ├── RankDiscount.php
│ ├── RankReduction.php
│ ├── Coupon.php
│ ├── PointsRedemption.php
│ ├── PointsMultiplier.php
│ ├── MemberDayDiscount.php
│ ├── BirthdayDiscount.php
│ ├── HolidayDiscount.php
│ ├── TimeSlotPricing.php
│ ├── BuyGift.php
│ ├── FullGift.php
│ ├── GiftSelection.php
│ └── TradeIn.php
└── Freight/ # 运费规则引擎
├── Freight.php # 运费计算核心
├── Rule/ # 运费规则
│ ├── AbstractRule.php
│ ├── FullFree.php # 满额包邮
│ ├── WeightBased.php # 按重量计费
│ ├── RegionBased.php # 按地区计费
│ ├── MemberFree.php # 会员免运费
│ ├── PointsDeduction.php # 积分抵扣运费
│ ├── Discount.php # 运费折扣
│ └── ItemSpecific.php # 商品级别运费
├── Calculator/ # 计算策略
│ ├── Independent.php # 独立计算(叠加)
│ ├── Sequential.php # 顺序计算
│ └── Lock.php # 锁定最优
└── Validate/ # 运费规则验证器
├── AbstractValidator.php
├── FullFree.php
├── WeightBased.php
├── RegionBased.php
├── MemberFree.php
├── PointsDeduction.php
├── Discount.php
└── ItemSpecific.php
快速开始
安装
使用 Composer 安装:
composer require fiberphp-ext/engine
促销计算
use FiberPHP\Engine\Engine;
// 购物车数据
$cart = [
['name' => '商品A', 'price' => 200, 'quantity' => 2, 'tags' => ['electronics']],
['name' => '商品B', 'price' => 100, 'quantity' => 1, 'tags' => ['clothing']],
];
// 会员等级
$rankId = 2;
// 规则配置(使用下划线命名)
$promotionRules = [
'TieredReduction' => [
'tiers' => [300 => 50, 500 => 100],
'activity_id' => 1001
]
];
// 计算优惠
$result = Engine::calcPromotion($cart, $rankId, $promotionRules);
// 输出结果
echo "原价: {$result['original']} 元\n";
echo "优惠: {$result['discount']} 元\n";
echo "实付: {$result['final']} 元\n";
运费计算
use FiberPHP\Engine\Engine;
// 购物车数据(包含重量)
$cart = [
['name' => '商品A', 'price' => 200, 'quantity' => 1, 'weight' => 2.5],
['name' => '商品B', 'price' => 100, 'quantity' => 1, 'weight' => 1.0],
];
// 运费规则配置
$freightRules = [
'FullFree' => [
'threshold' => 200,
'activity_id' => 3001
]
];
// 计算运费(支持计算模式)
$result = Engine::calcFreight($cart, $rankId, $freightRules, $storeId, $memberInfo, $region, 'lock');
// 输出结果
echo "运费原价: {$result['original']} 元\n";
echo "运费优惠: {$result['discount']} 元\n";
echo "实际运费: {$result['final']} 元\n";
订单总额计算
use FiberPHP\Engine\Engine;
// 计算订单总额(促销+运费)
$result = Engine::calcOrder(
$cart,
$rankId,
$promotionRules,
$freightRules,
'independent', // 促销计算模式
$storeId,
$memberInfo,
$region,
'lock' // 运费计算模式
);
// 订单汇总
echo "订单原价: {$result['order']['original_total']} 元\n";
echo "总优惠: {$result['order']['total_discount']} 元\n";
echo "应付总额: {$result['order']['final_total']} 元\n";
促销规则配置
1. 阶梯满减 (TieredReduction)
'TieredReduction' => [
'tiers' => [
300 => 50, // 满300减50
500 => 100, // 满500减100
800 => 180, // 满800减180
],
'tags' => ['electronics'], // 适用商品标签(可选)
'priority' => 1, // 优先级(可选)
'activity_id' => 1001 // 活动ID
]
2. 阶梯满折 (TieredDiscount)
'TieredDiscount' => [
'tiers' => [
300 => 0.9, // 满300打9折
500 => 0.85, // 满500打85折
],
'tags' => ['clothing'],
'activity_id' => 1002
]
3. 会员等级折扣 (RankDiscount)
'RankDiscount' => [
'discounts' => [
1 => 0.95, // 等级1会员打95折
2 => 0.9, // 等级2会员打9折
3 => 0.85, // 等级3会员打85折
],
'activity_id' => 1003
]
4. 优惠券 (Coupon)
'Coupon' => [
'type' => 'full_reduction', // full_reduction:满减, discount:折扣, direct:直减
'threshold' => 200, // 门槛金额
'discount' => 30, // 优惠金额/折扣率
'code' => 'COUPON2024', // 优惠券码
'activity_id' => 1004
]
5. 积分抵扣 (PointsRedemption)
'PointsRedemption' => [
'points_rate' => 0.01, // 1积分抵0.01元
'min_points' => 100, // 最低抵扣积分
'max_points' => 5000, // 最高抵扣积分
'max_percent' => 0.5, // 最高抵扣比例(0-1)
'activity_id' => 1005
]
6. 生日折扣 (BirthdayDiscount)
'BirthdayDiscount' => [
'discount_rate' => 0.8, // 8折
'days' => 7, // 生日前后共7天
'applicable_ranks' => [1,2,3],
'tags' => ['gift'],
'activity_id' => 1006
]
运费规则配置
1. 满额包邮 (FullFree)
'FullFree' => [
'threshold' => 200, // 满200元包邮
'activity_id' => 3001
]
2. 按重量计费 (WeightBased)
'WeightBased' => [
'first_weight' => 1, // 首重(kg)
'first_price' => 10, // 首重价格
'additional_weight' => 1, // 续重单位(kg)
'additional_price' => 5, // 续重价格
'activity_id' => 3002
]
3. 按地区计费 (RegionBased)
'RegionBased' => [
'regions' => [
'北京' => ['base_price' => 10, 'additional_kg_price' => 2],
'上海' => ['base_price' => 12, 'additional_kg_price' => 3],
'广州' => ['base_price' => 8, 'additional_kg_price' => 1.5],
],
'activity_id' => 3003
]
4. 会员免运费 (MemberFree)
'MemberFree' => [
'applicable_ranks' => [2, 3], // 等级2和3会员免运费
'activity_id' => 3004
]
5. 积分抵扣运费 (PointsDeduction)
'PointsDeduction' => [
'points_rate' => 0.01, // 1积分抵0.01元运费
'max_percent' => 0.5, // 最高抵扣50%
'activity_id' => 3005
]
6. 运费折扣 (Discount)
'Discount' => [
'discount_rate' => 0.8, // 运费8折
'activity_id' => 3006
]
7. 商品级别运费 (ItemSpecific)
'ItemSpecific' => [
'rules' => [
['product_id' => 1001, 'freight' => 15], // 商品1001固定运费15元
['category' => 'clothing', 'freight' => 8], // 服装类每件8元
['category' => 'fresh', 'rate' => 0.1], // 生鲜类按金额10%计费
],
'activity_id' => 3007
]
计算模式
促销计算模式
| 模式 | 说明 |
|---|---|
| independent | 所有规则独立计算,优惠可以叠加 |
| sequential | 按规则优先级依次计算,后一个规则基于前一个规则计算后的金额 |
| lock | 选择优惠最大的规则生效,其他规则不生效 |
// 使用不同计算模式
$result = Engine::calcPromotion($cart, $rankId, $rules, 'lock');
运费计算模式
运费规则同样支持三种计算模式,与促销规则保持一致:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| independent | 所有规则独立计算,优惠可以叠加 | 多种运费优惠可同时享受 |
| sequential | 按规则优先级依次计算,后一个规则基于前一个规则计算后的金额 | 需要按顺序应用运费优惠 |
| lock | 选择优惠最大的规则生效,其他规则不生效 | 多个运费规则互斥,择优选取 |
// 使用 lock 模式选择最优运费规则
$result = Engine::calcFreight($cart, $rankId, $rules, $storeId, $memberInfo, $region, 'lock');
计算模式示例
场景:购物车总价 350 元,总重量 5kg,会员等级2
运费规则配置:
$freightRules = [
'FullFree' => [
'threshold' => 200, // 满200包邮
'activity_id' => 3001
],
'MemberFree' => [
'applicable_ranks' => [2, 3], // 等级2会员免运费
'activity_id' => 3004
],
'Discount' => [
'discount_rate' => 0.8, // 运费8折
'activity_id' => 3006
]
];
independent 模式(叠加优惠):
- 满额包邮:运费减25元(原价25元)
- 会员免运费:运费减25元
- 运费8折:运费减5元
- 总优惠:55元(注:实际取最优,避免运费为负)
lock 模式(锁定最优):
- 选择优惠最大的规则(满额包邮或会员免运费,优惠25元)
- 其他规则不生效
sequential 模式(顺序计算):
- 按优先级依次应用规则
- 后一个规则基于前一个规则的结果计算
返回结果结构
促销计算结果
$result = [
'original' => 500, // 原价
'discount' => 100, // 优惠金额
'final' => 400, // 实付金额
'details' => [ // 优惠详情列表
[
'activity_id' => 1001,
'activity_type' => 'TieredReduction',
'discount' => 50,
'description' => '满300减50'
]
]
];
运费计算结果
$result = [
'original' => 20, // 运费原价
'discount' => 10, // 运费优惠
'final' => 10, // 实际运费
'weight' => 3.5, // 总重量
'details' => [ // 运费详情列表
[
'activity_id' => 3001,
'activity_type' => 'FullFree',
'discount' => 0,
'description' => '未达到包邮门槛'
]
]
];
订单总额计算结果
$result = [
'promotion' => [...], // 促销计算结果
'freight' => [...], // 运费计算结果
'order' => [
'original_total' => 520, // 订单原价(商品+运费)
'total_discount' => 110, // 总优惠
'final_total' => 410 // 应付总额
]
];
商品级别支持
所有运费规则均支持商品级别筛选,通过 applicable_tags 配置来限制适用的商品范围。
基于标签的筛选
'WeightBased' => [
'first_weight' => 1,
'first_price' => 15,
'applicable_tags' => ['heavy'], // 仅适用于标记为 heavy 的商品
'activity_id' => 3002
]
商品级别运费规则
ItemSpecific 规则支持为不同商品或分类设置不同运费:
'ItemSpecific' => [
'rules' => [
// 按商品ID匹配
['product_id' => 1001, 'freight' => 15],
// 按分类(标签)匹配,固定运费
['category' => 'clothing', 'freight' => 8],
// 按分类匹配,按金额比例计费
['category' => 'fresh', 'rate' => 0.1],
// 按SKU匹配
['sku' => 'SKU001', 'freight' => 12],
],
'activity_id' => 3007
]
规则匹配优先级
ItemSpecific 规则的匹配顺序:
- 按商品ID匹配(
product_id) - 按SKU匹配(
sku) - 按分类/标签匹配(
category)
运费计算方式
ItemSpecific 支持两种计费方式:
- 固定运费:设置
freight参数,按件计费 - 比例计费:设置
rate参数,按商品金额的百分比计费
适用商品筛选示例
// 购物车包含多种类型商品
$cart = [
['id' => 1001, 'name' => '电子产品', 'price' => 500, 'quantity' => 1, 'tags' => ['electronics'], 'weight' => 2.0],
['id' => 1002, 'name' => '服装', 'price' => 100, 'quantity' => 2, 'tags' => ['clothing'], 'weight' => 0.5],
['id' => 1003, 'name' => '生鲜', 'price' => 50, 'quantity' => 3, 'tags' => ['fresh'], 'weight' => 1.0],
];
// 商品级别运费规则
$freightRules = [
'ItemSpecific' => [
'rules' => [
['product_id' => 1001, 'freight' => 15], // 电子产品固定运费15元
['category' => 'clothing', 'freight' => 8], // 服装类每件8元
['category' => 'fresh', 'rate' => 0.1], // 生鲜类按金额10%计费
],
'activity_id' => 3007
]
];
// 计算结果:15 + (2*8) + (3*50*0.1) = 15 + 16 + 15 = 46元
添加自定义规则
促销规则
- 创建规则类,继承
AbstractRule:
namespace FiberPHP\Engine\Promotion\Rule;
use FiberPHP\Engine\Common\Model\Cart;
use FiberPHP\Engine\Common\Model\Member;
class CustomRule extends AbstractRule
{
public function __construct(array $config = [])
{
parent::__construct($config);
$this->ruleType = 'CustomRule';
}
public function apply(Cart $cart, Member $user, array $eligibleIndexes = []): array
{
// 实现规则逻辑
return $this->result($discount, $description);
}
}
- 在
Promotion/Promotion.php的RULE_MAP中注册规则。
运费规则
- 创建规则类,继承
AbstractRule:
namespace FiberPHP\Engine\Freight\Rule;
use FiberPHP\Engine\Common\Model\Cart;
class CustomFreightRule extends AbstractRule
{
public function __construct(array $config = [])
{
parent::__construct($config);
$this->ruleType = 'CustomFreightRule';
}
public function apply(Cart $cart, Member $user, array $eligibleIndexes = []): array
{
// 实现运费计算逻辑
return $this->freightResult($freight, $discount, $description);
}
}
- 在
Freight/Freight.php的RULE_MAP中注册规则。
命名规范
- 数组字段名:下划线命名(如
activity_id,start_date) - 对象属性:驼峰命名(如
$this->activityId,$this->startDate) - 全局函数:下划线命名(如
qualify_class(),create_cart()) - 日期格式:
Y-m-d H:i:s - 折扣率范围:0-1(1表示原价,小于1表示折扣)
测试
# 安装依赖
composer install
# 运行所有测试
composer test
测试文件结构
tests/
├── PromotionTest.php # 促销规则测试
├── FreightTest.php # 运费规则测试
└── EngineTest.php # 引擎综合测试
测试示例
促销规则测试:
public function testTieredReduction()
{
$cart = [
['name' => '商品A', 'price' => 100, 'quantity' => 3, 'tags' => ['clothing']],
];
$rules = [
'TieredReduction' => [
'tiers' => [200 => 30, 500 => 80],
'activity_id' => 1001
]
];
$result = Engine::calcPromotion($cart, 0, $rules);
$this->assertEquals(300, $result['original']);
$this->assertEquals(30, $result['discount']);
$this->assertEquals(270, $result['final']);
}
运费规则测试:
public function testWeightBased()
{
$cart = [
['name' => '商品A', 'price' => 100, 'quantity' => 2, 'weight' => 3],
];
$rules = [
'WeightBased' => [
'first_weight' => 1,
'first_price' => 10,
'additional_weight' => 1,
'additional_price' => 5,
'activity_id' => 2001
]
];
$result = Engine::calcFreight($cart, 0, $rules);
$this->assertEquals(20, $result['final']);
}
订单总额测试:
public function testCalcOrder()
{
$cart = [
['name' => '商品A', 'price' => 150, 'quantity' => 2, 'weight' => 2, 'tags' => ['clothing']],
];
$promotionRules = ['TieredReduction' => ['tiers' => [200 => 30], 'activity_id' => 1001]];
$freightRules = ['WeightBased' => ['first_weight' => 1, 'first_price' => 10, 'additional_weight' => 1, 'additional_price' => 5, 'activity_id' => 2001]];
$result = Engine::calcOrder($cart, 0, $promotionRules, $freightRules);
$this->assertEquals(300, $result['promotion']['original']);
$this->assertEquals(30, $result['promotion']['discount']);
$this->assertEquals(15, $result['freight']['final']);
$this->assertEquals(285, $result['order']['final_total']);
}
异常测试:
public function testUnknownRule()
{
$this->expectException(EngineException::class);
$this->expectExceptionCode(1001);
$cart = [['name' => '商品', 'price' => 100, 'quantity' => 1]];
$rules = ['UnknownRule' => []];
Engine::calcPromotion($cart, 0, $rules);
}
注意事项
- 规则配置使用关联数组格式,键名使用下划线命名
- 商品标签匹配采用交集方式,商品需包含规则指定的所有标签
- 日期格式统一使用
Y-m-d H:i:s - 折扣率范围为 0-1,1 表示原价,小于1表示折扣
License
MIT