⚙️ FiberPHP 规则引擎 —— 促销规则(17 种)与运费规则(7 种)计算,支持独立/顺序/锁定最优三种计算模式。

Maintainers

Package info

gitee.com/fiberphp-ext/engine

Issues

pkg:composer/fiberphp-ext/engine

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

dev-master 2026-08-23 06:14 UTC

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 规则的匹配顺序:

  1. 按商品ID匹配(product_id
  2. 按SKU匹配(sku
  3. 按分类/标签匹配(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元

添加自定义规则

促销规则

  1. 创建规则类,继承 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);
    }
}
  1. Promotion/Promotion.phpRULE_MAP 中注册规则。

运费规则

  1. 创建规则类,继承 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);
    }
}
  1. Freight/Freight.phpRULE_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);
}

注意事项

  1. 规则配置使用关联数组格式,键名使用下划线命名
  2. 商品标签匹配采用交集方式,商品需包含规则指定的所有标签
  3. 日期格式统一使用 Y-m-d H:i:s
  4. 折扣率范围为 0-1,1 表示原价,小于1表示折扣

License

MIT