huage/flexible-billing-engine

灵活计费引擎 - 支持多层级条件判断、公式计算、规则匹配

Maintainers

Package info

github.com/gaorunhua/fee-billing-engine

pkg:composer/huage/flexible-billing-engine

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v1.0.1 2026-01-22 08:15 UTC

This package is auto-updated.

Last update: 2026-03-27 08:12:21 UTC


README

composer require huage/flexible-billing-engine:dev-main

计费引擎完整功能说明文档

一、功能概述

本计费引擎是一个灵活的、可扩展的 PHP 计费系统,支持:

  • 单条计费:计算单个订单/地址的费用
  • 批量计费:批量计算多个地址的费用,支持组合超重、拦截、超SKU等复杂场景
  • 多种计费场景:配比费用、组合价格、超重、超箱、超SKU等
  • 灵活的条件判断:支持多种运算符和公式计算

二、核心架构

1. 单条计费流程

公共参数 + 地址参数 → RuleEngine → BillingRule → 条件匹配 → BillingResult

2. 批量计费流程

公共参数 → BatchBillingEngine → 一批地址参数 → 批量计算 → 多个 BatchBillingResult

三、支持的条件类型

1. 公共条件(CommonCondition)

用于过滤规则,只有满足所有公共条件的规则才会被评估。

支持的运算符

  • ==, !=, >, <, >=, <=:比较运算符
  • in, not_in:集合运算符
  • between:范围运算符
  • formula:公式运算符

2. 配比费用条件(RatioCondition)

当条件满足时,使用公式计算费用。

特点

  • 支持变量替换
  • 支持数学表达式
  • 支持自定义公式

3. 拦截条件(InterceptCondition)

优先级最高,满足后直接返回,不再检查其他条件。

批量模式下的处理

  • 返回基础提拆费用
  • 同时返回空费用,备注"拦截后续费用"

4. 组合价格条件(CombinationCondition)

支持多变量、精度处理、最小值限制的组合价格计算。

匹配规则

  • 组合价格的匹配条件是:address_id地址类型address_group
  • address_group 是一个数组,包含多个地址ID或地址类型
  • 只要 address_id地址类型 其中一个在 address_group 中,就匹配成功

新增功能

  • 类型支持提拆提拆派
  • 提拆类型:使用配置的单价进行计算
  • 提拆派类型
    • 需要匹配地址价格(从公共参数的地址价格映射中获取)
    • 计算公式:地址价格 * 变量(托盘/体积)
    • 如果地址未匹配:
      • 检查地址类型
      • 如果是"散板",抛出异常
      • 如果不是"散板",使用默认单价
  • 私人地址处理
    • 如果地址类型是"私人地址"
    • 返回组合价格 + 空费用(备注:详见后续账单)

匹配示例

// 规则配置
$combination = new CombinationCondition(
    [
        ['field' => 'address_group', 'operator' => 'in', 'value' => [1, 2, 3, '标准地址', '特殊地址']]
    ],
    200,
    '提拆',
    ...
);

// 地址参数1:address_id = 1,匹配成功(1 在 address_group 中)
$address1 = ['address_id' => 1, '托盘' => 5];

// 地址参数2:地址类型 = '标准地址',匹配成功('标准地址' 在 address_group 中)
$address2 = ['地址类型' => '标准地址', '托盘' => 5];

// 地址参数3:address_id = 5,地址类型 = '其他类型',不匹配(都不在 address_group 中)
$address3 = ['address_id' => 5, '地址类型' => '其他类型', '托盘' => 5];

变量配置

  • name:变量名
  • precision:精度(如 0.5、1),自动向上取整
  • min:最小值
  • formula:变量公式

内置公式

  • BUILTIN_VAR1_DIV_VAR2_MUL_PRICE:变量1 / 变量2 * 单价

5. 超重条件(OverweightCondition)

根据重量和板数计算超重费用。

判断公式(重量 * 2.2046 - 阈值 * 板数) > 0

计算逻辑

  • 超重值 = (重量 * 2.2046 - 阈值 * 板数)
  • 倍数 = ceil(超重值 / 500) * 0.5,最小为 1
  • 费用 = 倍数 * 单价

批量模式下的两种处理方式

模式1:单条超重模式(默认)

  • 每条费用如果判定超重,除了返回组合单价,还要返回超重费用
  • 费用备注会写清楚超重计算方式
  • 示例:
    费用项1:组合价格 1000 CNY
    费用项2:超重费用 200 CNY
    备注:超重计算方式:(重量500 * 2.2046 - 阈值1000 * 板数1) = 102.3,倍数 = ceil(102.3 / 500) * 0.5 = 1
    

模式2:组合超重模式

  • 当获得了一批超重费用时,如果公共参数传的是组合超重模式
  • 不再返回单个费用的超重费用,而是返回一个组合超重费用
  • 组合的超重费用计算:把组合的全部重量加起来,然后计算超重费用
  • 费用备注会写清楚超重计算方式
  • 组合超重费用会添加到第一个地址的结果中
  • 示例:
    地址1费用项:组合价格 1000 CNY
    地址2费用项:组合价格 1200 CNY
    地址1费用项(追加):组合超重费用 300 CNY
    备注:组合超重计算方式:(总重量1000 * 2.2046 - 阈值1000 * 总板数2) = 1204.6,倍数 = ceil(1204.6 / 500) * 0.5 = 1.5
    

6. 超箱条件(OverContainerCondition)

根据箱数计算超箱费用。

判断公式箱数 - 阈值 > 0

计算逻辑

  • 超箱值 = 箱数 - 阈值
  • 计数 = ceil(超箱值 / 500)
  • 费用 = 计数 * 单价

7. 超SKU条件(OverSkuCondition)

根据SKU数计算超SKU费用。

判断公式sku数 - 阈值 > 0

计算逻辑

  • 超SKU值 = sku数 - 阈值
  • 费用 = 超SKU值 * 单价

批量模式下的处理

  • 如果传入的公共参数里有"整柜提拆SKU数"满足条件:
    • 返回固定费用(整柜提拆SKU费用
    • 然后所有地址都走散板匹配逻辑
    • 但只有地址类型是"散板"的才走散板费用
  • 如果设置了"超SKU阈值"和"超SKU单价":
    • 如果总SKU数超过阈值,返回费用:(总SKU数 - 超SKU阈值) * 超SKU单价

四、批量计费功能

1. BatchBillingEngine(批量计费引擎)

功能

  • 支持传入公共参数和一批地址参数
  • 返回多个费用结果(每个地址一个结果)
  • 支持组合超重模式
  • 支持拦截处理
  • 支持超SKU处理

2. 使用流程

// 1. 创建批量计费引擎
$batchEngine = new BatchBillingEngine();

// 2. 设置公共参数
$commonContext = new BillingContext([
    'user_id' => 1,
    'container_type' => '40HQ',
    '组合超重模式' => false,  // true=组合模式,false=单条模式
    '基础提拆费用' => 500,
    '超重阈值' => 1000,
    '超重单位' => 500,
    '超重单价' => 200,
    '整柜提拆SKU数' => 10,      // 可选:超过10个SKU触发整柜提拆逻辑
    '整柜提拆SKU费用' => 500,    // 可选:整柜提拆SKU时返回的固定费用
    '超SKU规则' => [             // 可选:超SKU规则数组
        ['threshold' => 8, 'price' => 50],   // 超过8个SKU,单价50
        ['threshold' => 15, 'price' => 100], // 超过15个SKU,单价100
    ],
    '地址价格' => [       // 提拆派类型需要
        1 => 200,
        2 => 250,
    ],
    'currency' => 'CNY'
]);
$batchEngine->setCommonContext($commonContext);

// 3. 设置规则(与单条计费相同)
$rule = new BillingRule('rule_001', '综合规则', 10);
// ... 添加各种条件
$batchEngine->getRuleEngine()->addRule($rule);

// 4. 批量计算
$addressList = [
    ['address_id' => 1, 'address_group' => '组1', '重量' => 500, '板数' => 1, '托盘' => 5],
    ['address_id' => 2, 'address_group' => '组2', '重量' => 600, '板数' => 1, '托盘' => 6],
];
$results = $batchEngine->calculateBatch($addressList);

// 5. 处理结果
foreach ($results as $index => $result) {
    foreach ($result->getItems() as $item) {
        echo "{$item->getFeeType()}: {$item->getAmount()} {$item->getCurrency()}\n";
        if ($item->getRemark()) {
            echo "  备注: {$item->getRemark()}\n";
        }
    }
}

3. 费用项结构(FeeItem)

每个费用项包含:

  • amount:费用金额
  • currency:币种
  • remark:备注(用于说明计算方式)
  • feeType:费用类型

4. 批量结果结构(BatchBillingResult)

每个批量结果包含:

  • items:费用项列表(可能多个)
  • totalAmount:总金额
  • currency:币种
  • matchedRule:匹配的规则

五、特殊场景处理

1. 拦截处理

触发条件:在获取组合费用时判定是拦截

处理逻辑

  • 按照基础提拆费用来计算费用
  • 同时返回一条空的费用,备注是"拦截后续费用"

示例

费用项1:拦截基础提拆费用 500 CNY,备注:拦截:按照基础提拆费用计算
费用项2:空费用 0 CNY,备注:拦截后续费用

2. 整柜提拆SKU数处理

触发条件:如果传入的公共参数里有"整柜提拆SKU数"满足条件

处理逻辑

  • 计算所有地址的总SKU数
  • 如果总SKU数超过"整柜提拆SKU数":
    • 返回固定费用(整柜提拆SKU费用
    • 然后所有地址都走散板匹配逻辑
    • 但只有地址类型是"散板"的才走散板费用

公共参数配置

$commonContext->set('整柜提拆SKU数', 10);      // 超过10个SKU触发
$commonContext->set('整柜提拆SKU费用', 500);    // 返回的固定费用

示例

地址1:整柜提拆SKU费用 500 CNY(地址类型不是散板,不走散板费用)
地址2:整柜提拆SKU费用 500 CNY + 散板费用 300 CNY(地址类型是散板)
地址3:整柜提拆SKU费用 500 CNY(地址类型不是散板,不走散板费用)

3. 超SKU规则处理

触发条件:如果设置了"超SKU规则"(数组格式)

处理逻辑

  • 计算所有地址的总SKU数
  • 遍历所有超SKU规则,如果总SKU数超过某个规则的阈值:
    • 计算该规则的费用:(总SKU数 - 阈值) * 单价
    • 累加所有匹配规则的费用
    • 费用添加到第一个地址的结果中

公共参数配置

$commonContext->set('超SKU规则', [
    ['threshold' => 8, 'price' => 50],   // 超过8个SKU,单价50
    ['threshold' => 15, 'price' => 100], // 超过15个SKU,单价100
    // 或者使用中文键名
    ['超SKU阈值' => 20, '超SKU单价' => 150], // 超过20个SKU,单价150
]);

示例

总SKU数 = 25
规则1:threshold = 8, price = 50,匹配,费用 = (25 - 8) * 50 = 850
规则2:threshold = 15, price = 100,匹配,费用 = (25 - 15) * 100 = 1000
规则3:threshold = 20, price = 150,匹配,费用 = (25 - 20) * 150 = 750
总超SKU费用 = 850 + 1000 + 750 = 2600 CNY
添加到第一个地址的结果中

3. 提拆派类型处理

特点

  • 需要匹配地址价格
  • 公共参数需要传入地址价格映射
  • 匹配成功后返回:地址价格 * 变量(托盘/体积)
  • 如果没有匹配上:
    • 检查地址类型
    • 如果是"散板",抛出异常
    • 如果不是"散板",使用默认单价

示例

// 公共参数
$commonContext->set('地址价格', [
    1 => 200,  // 地址1价格200
    2 => 250,  // 地址2价格250
]);

// 地址参数
$address = [
    'address_id' => 1,
    '托盘' => 5
];

// 结果:200 * 5 = 1000 CNY

4. 私人地址处理

触发条件:组合价格计算时,地址类型是"私人地址"

处理逻辑

  • 返回组合价格
  • 同时返回一个空价格,备注是"详见后续账单"

示例

费用项1:组合价格 1000 CNY
费用项2:空费用 0 CNY,备注:详见后续账单

六、公共参数说明

重要说明

  • 必填参数:只有 currency(币种)是必填的
  • 可选参数:其他所有参数都是可选的,根据不同的计费场景来设置
  • 场景参数:每个场景需要设置相应的参数,不需要的场景可以不设置
  • 最小配置示例
    $commonContext = new BillingContext([
        'currency' => 'CNY',  // 只有币种是必填的
    ]);
参数名 类型 说明 必填 默认值 使用场景
currency string 币种 CNY 所有场景
组合超重模式 bool true=组合模式,false=单条模式 false 组合超重场景
基础提拆费用 float 拦截时使用的基础费用 0 拦截场景
超重阈值 float 超重判断阈值 1000 超重场景
超重单位 float 每多少单位增加0.5 500 超重场景
超重单价 float 超重单价 0 超重场景
整柜提拆SKU数 int 超过几个SKU触发整柜提拆逻辑 - 整柜提拆SKU场景
整柜提拆SKU费用 float 整柜提拆SKU时返回的固定费用 - 整柜提拆SKU场景
超SKU规则 array 超SKU规则数组,格式:[['threshold' => 8, 'price' => 50], ...] [] 超SKU场景
地址价格 array 地址价格映射(提拆派类型需要) [] 提拆派场景

七、地址参数说明

参数名 类型 说明 必填
address_id int/string 地址ID(提拆派类型需要) 是(提拆派)
address_name string 地址名称(提拆派类型备用)
address_group string 地址组(用于条件匹配) 是(条件匹配)
地址类型 string 地址类型(如:标准地址、私人地址、散板)
重量 float 重量(超重场景需要) 是(超重)
板数 float 板数(超重场景需要) 是(超重)
托盘 float 托盘数(组合价格需要) 是(组合价格)
sku数 int SKU数(超SKU场景需要) 是(超SKU)

八、费用类型说明

费用类型 说明
combination 组合价格
overweight 超重费用(单条模式)
combined_overweight 组合超重费用(组合模式)
intercept_base_fee 拦截基础提拆费用
intercept_follow 拦截后续费用(空费用)
base_fee 基础提拆费用(超SKU时使用)
private_address 私人地址后续账单(空费用)
over_container 超箱费用
over_sku 超SKU费用
ratio 配比费用

九、完整使用示例

示例1:单条计费

use FlexibleBilling\Rule\RuleEngine;
use FlexibleBilling\Rule\BillingRule;
use FlexibleBilling\Condition\CommonCondition;
use FlexibleBilling\Condition\CombinationCondition;
use FlexibleBilling\Context\BillingContext;

$engine = new RuleEngine();
$rule = new BillingRule('rule_001', '组合价格规则', 10);
$rule->addCommonCondition(new CommonCondition('user_id', '==', 1));

$combination = new CombinationCondition(
    [['field' => 'address_group', 'operator' => '==', 'value' => '组1']],
    200,
    '提拆',
    [['name' => '托盘', 'precision' => 1, 'min' => 1]],
    '{单价} * {托盘}',
    ''
);
$rule->addCombinationCondition($combination);

$engine->addRule($rule);

$context = new BillingContext([
    'user_id' => 1,
    'address_group' => '组1',
    '托盘' => 5
]);

$result = $engine->calculate($context);
echo "费用: {$result->getAmount()} {$result->getCurrency()}\n";

示例2:批量计费(组合超重模式)

use FlexibleBilling\Context\BillingContext;
use FlexibleBilling\Rule\BatchBillingEngine;
use FlexibleBilling\Rule\BillingRule;
use FlexibleBilling\Condition\CombinationCondition;

$batchEngine = new BatchBillingEngine();

// 设置公共参数
$commonContext = new BillingContext([
    'user_id' => 1,
    'container_type' => '40HQ',
    '组合超重模式' => true,  // 组合超重模式
    '基础提拆费用' => 500,
    '超重阈值' => 1000,
    '超重单位' => 500,
    '超重单价' => 200,
    'currency' => 'CNY'
]);
$batchEngine->setCommonContext($commonContext);

// 设置规则
$rule = new BillingRule('rule_001', '组合价格规则', 10);
$rule->addCommonCondition(new CommonCondition('user_id', '==', 1));

$combination = new CombinationCondition(
    [['field' => 'address_group', 'operator' => '==', 'value' => '组1']],
    200,
    '提拆',
    [['name' => '托盘', 'precision' => 1, 'min' => 1]],
    '{单价} * {托盘}',
    ''
);
$rule->addCombinationCondition($combination);

$batchEngine->getRuleEngine()->addRule($rule);

// 批量计算
$addressList = [
    [
        'address_group' => '组1',
        '重量' => 500,
        '板数' => 1,
        '托盘' => 5
    ],
    [
        'address_group' => '组1',
        '重量' => 600,
        '板数' => 1,
        '托盘' => 6
    ]
];

$results = $batchEngine->calculateBatch($addressList);

// 处理结果
foreach ($results as $index => $result) {
    echo "地址" . ($index + 1) . ":\n";
    foreach ($result->getItems() as $item) {
        echo "  {$item->getFeeType()}: {$item->getAmount()} {$item->getCurrency()}\n";
        if ($item->getRemark()) {
            echo "    备注: {$item->getRemark()}\n";
        }
    }
    echo "  总计: {$result->getTotalAmount()} {$result->getCurrency()}\n\n";
}

示例3:提拆派类型

// 设置公共参数(包含地址价格映射)
$commonContext = new BillingContext([
    'user_id' => 1,
    '地址价格' => [
        1 => 200,  // 地址1价格200
        2 => 250,  // 地址2价格250
    ],
    'currency' => 'CNY'
]);

// 设置规则(提拆派类型)
$combination = new CombinationCondition(
    [['field' => 'address_group', 'operator' => '==', 'value' => '组2']],
    200,  // 默认单价(如果地址未匹配且不是散板,使用此单价)
    '提拆派',  // 类型
    [['name' => '托盘', 'precision' => 1, 'min' => 1]],
    '',
    ''
);

// 地址参数
$address = [
    'address_id' => 1,
    'address_group' => '组2',
    '地址类型' => '标准地址',
    '托盘' => 5
];

// 结果:200 * 5 = 1000 CNY(地址1价格200)

十、功能总结

已实现的功能

单条计费:支持单个订单/地址的费用计算 ✅ 批量计费:支持批量计算多个地址的费用 ✅ 组合超重:支持两种模式(单条模式、组合模式) ✅ 拦截处理:支持拦截时返回基础提拆费用 + 空费用 ✅ 超SKU处理:支持组合模式下的超SKU处理 ✅ 提拆派类型:支持地址价格匹配,散板异常处理 ✅ 私人地址:支持私人地址时返回空费用备注 ✅ 费用备注:超重费用包含详细的计算方式说明 ✅ 变量验证:自定义公式和内置公式的变量验证 ✅ 多费用项:支持一个地址返回多个费用项

设计特点

  • 灵活扩展:支持自定义运算符、自定义公式
  • 类型安全:变量验证机制,防止计算错误
  • 易于使用:清晰的API,丰富的示例
  • 高性能:短路评估,按优先级排序
  • 批量处理:支持批量计算,提高效率

十一、匹配流程总结

根据需求,匹配流程应该是:

1. 传入公共参数
2. 设置各种组合的价格信息(规则配置)
3. 设置一批地址参数
4. 最后执行获取一堆价格(批量计算)

实现方式

// 1. 传入公共参数
$commonContext = new BillingContext([...公共参数...]);

// 2. 设置各种组合的价格信息(规则配置)
$rule = new BillingRule(...);
$rule->addCombinationCondition(...);
$batchEngine->getRuleEngine()->addRule($rule);

// 3. 设置一批地址参数
$addressList = [
    [...地址1参数...],
    [...地址2参数...],
    ...
];

// 4. 最后执行获取一堆价格
$results = $batchEngine->calculateBatch($addressList);

十二、注意事项

  1. 组合超重模式:如果启用,所有地址的超重费用会合并为一个组合超重费用,添加到第一个地址的结果中
  2. 拦截处理:拦截时使用基础提拆费用,不再计算其他费用
  3. 超SKU处理:如果总SKU数超过阈值,所有地址都返回基础提拆费用
  4. 提拆派类型:必须配置地址价格映射,否则无法匹配会抛出异常(如果地址类型是散板)
  5. 私人地址:会自动添加"详见后续账单"的空费用项
  6. 费用备注:超重费用会包含详细的计算方式说明
  7. 变量验证:使用自定义公式时,确保所有变量都已配置或存在于上下文中